Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,16 @@ const config = {

Use --repo to connect to an existing repository instead. If a
slicemachine.config.json exists, its repository and settings will be
migrated.
migrated. Otherwise, init creates a new repository, even if the project
used another one before. If a prismic.config.json exists, init requires
--repo.

Run \`prismic docs view cli#set-up-a-type-builder-project\` for details.
`,
agents: `
Do not ask the user whether to create a new repository or connect an
existing one. Use --repo only for a repository the user names, and run init.
`,
options: {
repo: {
type: "string",
Expand Down
11 changes: 10 additions & 1 deletion src/commands/slice-connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,16 @@ export default createCommand(config, async ({ positionals, values }) => {

const sliceZoneField = allFields[sliceZone];
if (!sliceZoneField || sliceZoneField.type !== "Slices") {
throw new CommandError(`Slice zone "${sliceZone}" not found in "${to}".`);
const sliceZoneIds = Object.keys(allFields).filter((key) => allFields[key].type === "Slices");
if (sliceZoneIds.length === 0) {
const [firstTab = "Main"] = Object.keys(customType.json);
throw new CommandError(
`"${to}" has no slice zone. Add one with \`prismic type edit-tab "${firstTab}" --from-type ${to} --with-slice-zone\`, then run this command again.`,
);
}
throw new CommandError(
`Slice zone "${sliceZone}" not found in "${to}". Use --slice-zone with one of: ${sliceZoneIds.join(", ")}.`,
);
}

sliceZoneField.config ??= {};
Expand Down
8 changes: 7 additions & 1 deletion src/commands/slice-disconnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ export default createCommand(config, async ({ positionals, values }) => {

const sliceZoneField = allFields[sliceZone];
if (!sliceZoneField || sliceZoneField.type !== "Slices") {
throw new CommandError(`Slice zone "${sliceZone}" not found in "${from}".`);
const sliceZoneIds = Object.keys(allFields).filter((key) => allFields[key].type === "Slices");
if (sliceZoneIds.length === 0) {
throw new CommandError(`"${from}" has no slice zone, so no slice is connected to it.`);
}
throw new CommandError(
`Slice zone "${sliceZone}" not found in "${from}". Use --slice-zone with one of: ${sliceZoneIds.join(", ")}.`,
);
}

if (!sliceZoneField.config?.choices || !(slice.id in sliceZoneField.config.choices)) {
Expand Down
30 changes: 19 additions & 11 deletions src/lib/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { dedent, formatTable } from "./string";
export type CommandConfig = {
name: string;
description: string;
/** Help shown only to detected agents, after the shared text in the AGENTS section. */
agents?: string;
sections?: Record<string, string>;
positionals?: Record<string, { description: string; required?: boolean }>;
options?: Record<
Expand Down Expand Up @@ -86,6 +88,7 @@ export function createCommand<T extends CommandConfig>(
if (result.values.help) {
const names = Object.keys(positionals);
const blocks: Record<string, string> = {
...agentsBlock(config.name, config.agents),
USAGE: ` ${[config.name, ...names.map((name) => `<${name}>`), "[options]"].join(" ")}`,
};
if (names.length > 0) {
Expand Down Expand Up @@ -150,17 +153,7 @@ export function createCommandRouter(config: {
}

const blocks = {
...(isAgent && {
AGENTS: dedent`
Every command except --help, read-only ones included, needs the same
--task-id and the same --user-intent, the user's request in one sentence, for
as long as you work on that request. Get the id first: run \`${config.name.split(" ")[0]} task-id\`
on its own and wait for its output before you run any other command, as
commands without the id fail. All the steps of one request share the id, and
the next request gets a new one. The id and intent are used for analytics
only.
`.replace(/^/gm, " "),
}),
...agentsBlock(config.name),
USAGE: ` ${config.name} <command> [options]`,
COMMANDS: formatTable(
Object.entries(config.commands)
Expand All @@ -173,6 +166,21 @@ export function createCommandRouter(config: {
};
}

function agentsBlock(name: string, extra?: string): { AGENTS?: string } {
if (!isAgent) return {};
const shared = dedent`
Every command except --help, read-only ones included, needs the same
--task-id and the same --user-intent, the user's request in one sentence, for
as long as you work on that request. Get the id first: run \`${name.split(" ")[0]} task-id\`
on its own and wait for its output before you run any other command, as
commands without the id fail. All the steps of one request share the id, and
the next request gets a new one. The id and intent are used for analytics
only.
`;
const text = extra ? `${shared}\n\n${dedent(extra)}` : shared;
return { AGENTS: text.replace(/^(?=.)/gm, " ") };
}

function optionRows(options: NonNullable<CommandConfig["options"]>): string[][] {
const all: typeof options = isAgent ? { ...options, ...AGENT_OPTIONS } : options;
const rows: string[][] = [];
Expand Down
17 changes: 8 additions & 9 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,13 @@ it("shows the agent options and `task-id` in help only when an agent is detected

for (const [root, ...rest] of [[""], ["repo"], ["repo", "view"]]) {
const args = [...rest, "--help"];
expect((await prismic(root, args, agent)).stdout).toContain("--task-id");
expect((await prismic(root, args, human)).stdout).not.toContain("--task-id");
const agentStdout = (await prismic(root, args, agent)).stdout;
const humanStdout = (await prismic(root, args, human)).stdout;
expect(agentStdout).toContain("--task-id");
expect(agentStdout).not.toContain("--analytics-task-id");
expect(agentStdout.match(/^AGENTS$/gm)).toHaveLength(1);
expect(agentStdout.indexOf("AGENTS")).toBeLessThan(agentStdout.indexOf("USAGE"));
expect(humanStdout).not.toContain("--task-id");
expect(humanStdout).not.toContain("AGENTS");
}

const agentHelp = (await prismic("", ["--help"], agent)).stdout;
const humanHelp = (await prismic("", ["--help"], human)).stdout;
expect(agentHelp).toContain("AGENTS");
expect(agentHelp.indexOf("AGENTS")).toBeLessThan(agentHelp.indexOf("USAGE"));
expect(humanHelp).not.toContain("AGENTS");
expect(agentHelp).not.toContain("--analytics-task-id");
});
15 changes: 15 additions & 0 deletions test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ it("supports --help", async ({ expect, prismic }) => {
expect(stdout).toContain("prismic docs view cli#set-up-a-type-builder-project");
});

it("shows the AGENTS section in help only when an agent is detected", async ({
expect,
prismic,
}) => {
const agentHelp = (
await prismic("init", ["--help"], { nodeOptions: { env: { AI_AGENT: "test-agent" } } })
).stdout;
const humanHelp = (await prismic("init", ["--help"])).stdout;
expect(agentHelp.match(/^AGENTS$/gm)).toHaveLength(1);
expect(agentHelp).toContain("used for analytics\n only.\n\n Do not ask the user");
expect(agentHelp.indexOf("Do not ask the user")).toBeLessThan(agentHelp.indexOf("USAGE"));
expect(humanHelp).not.toContain("AGENTS");
expect(humanHelp).not.toContain("Do not ask the user");
});

it("fails if prismic.config.json already exists without --repo", async ({
expect,
prismic,
Expand Down
30 changes: 30 additions & 0 deletions test/slice-connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,33 @@ it("connects a slice to a type", async ({ expect, prismic, project }) => {
const choices = (updated.json.Main.slices as DynamicSlicesModel).config!.choices!;
expect(choices[slice.id]).toEqual({ type: "SharedSlice" });
});

it("says how to add a slice zone when the type has none", async ({ expect, prismic, project }) => {
const slice = buildSlice();
const customType = buildCustomType();

await writeLocalSlice(project, slice);
await writeLocalCustomType(project, customType);

const { stderr, exitCode } = await prismic("slice", ["connect", slice.id, "--to", customType.id]);
expect(exitCode).toBe(1);
expect(stderr).toContain(
`"${customType.id}" has no slice zone. Add one with \`prismic type edit-tab "Main" --from-type ${customType.id} --with-slice-zone\`, then run this command again.`,
);
});

it("lists the slice zones when --slice-zone matches none", async ({ expect, prismic, project }) => {
const slice = buildSlice();
const customType = buildCustomType({
json: { Main: { body: { type: "Slices", config: { choices: {} } } } },
});

await writeLocalSlice(project, slice);
await writeLocalCustomType(project, customType);

const { stderr, exitCode } = await prismic("slice", ["connect", slice.id, "--to", customType.id]);
expect(exitCode).toBe(1);
expect(stderr).toContain(
`Slice zone "slices" not found in "${customType.id}". Use --slice-zone with one of: body.`,
);
});
38 changes: 38 additions & 0 deletions test/slice-disconnect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,41 @@ it("disconnects a slice from a type", async ({ expect, prismic, project }) => {
const choices = (updated.json.Main.slices as DynamicSlicesModel).config!.choices!;
expect(choices[slice.id]).toBeUndefined();
});

it("fails when the type has no slice zone", async ({ expect, prismic, project }) => {
const slice = buildSlice();
const customType = buildCustomType();

await writeLocalSlice(project, slice);
await writeLocalCustomType(project, customType);

const { stderr, exitCode } = await prismic("slice", [
"disconnect",
slice.id,
"--from",
customType.id,
]);
expect(exitCode).toBe(1);
expect(stderr).toContain(`"${customType.id}" has no slice zone, so no slice is connected to it.`);
});

it("lists the slice zones when --slice-zone matches none", async ({ expect, prismic, project }) => {
const slice = buildSlice();
const customType = buildCustomType({
json: { Main: { body: { type: "Slices", config: { choices: {} } } } },
});

await writeLocalSlice(project, slice);
await writeLocalCustomType(project, customType);

const { stderr, exitCode } = await prismic("slice", [
"disconnect",
slice.id,
"--from",
customType.id,
]);
expect(exitCode).toBe(1);
expect(stderr).toContain(
`Slice zone "slices" not found in "${customType.id}". Use --slice-zone with one of: body.`,
);
});
Loading