diff --git a/src/commands/init.ts b/src/commands/init.ts index acfbc0ce..9b115adf 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -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", diff --git a/src/commands/slice-connect.ts b/src/commands/slice-connect.ts index dfc335b2..f4ea5395 100644 --- a/src/commands/slice-connect.ts +++ b/src/commands/slice-connect.ts @@ -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 ??= {}; diff --git a/src/commands/slice-disconnect.ts b/src/commands/slice-disconnect.ts index e48c0122..d5ba931c 100644 --- a/src/commands/slice-disconnect.ts +++ b/src/commands/slice-disconnect.ts @@ -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)) { diff --git a/src/lib/command.ts b/src/lib/command.ts index 21e733a2..59cb27ac 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -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; positionals?: Record; options?: Record< @@ -86,6 +88,7 @@ export function createCommand( if (result.values.help) { const names = Object.keys(positionals); const blocks: Record = { + ...agentsBlock(config.name, config.agents), USAGE: ` ${[config.name, ...names.map((name) => `<${name}>`), "[options]"].join(" ")}`, }; if (names.length > 0) { @@ -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} [options]`, COMMANDS: formatTable( Object.entries(config.commands) @@ -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): string[][] { const all: typeof options = isAgent ? { ...options, ...AGENT_OPTIONS } : options; const rows: string[][] = []; diff --git a/test/index.test.ts b/test/index.test.ts index ac649394..b4b27c5f 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -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"); }); diff --git a/test/init.test.ts b/test/init.test.ts index ba769d8f..2957e093 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -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, diff --git a/test/slice-connect.test.ts b/test/slice-connect.test.ts index 46033429..df3092fb 100644 --- a/test/slice-connect.test.ts +++ b/test/slice-connect.test.ts @@ -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.`, + ); +}); diff --git a/test/slice-disconnect.test.ts b/test/slice-disconnect.test.ts index e058c6de..fb01c98a 100644 --- a/test/slice-disconnect.test.ts +++ b/test/slice-disconnect.test.ts @@ -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.`, + ); +});