From 35550809b17bd9689d5c477f805042a142ceb0ef Mon Sep 17 00:00:00 2001 From: Greg Brimble Date: Fri, 2 Oct 2026 08:39:08 -0400 Subject: [PATCH 1/2] WC-6122 Prepare Preview group for generated API commands --- .changeset/previews-command-group.md | 5 ++ packages/cli/AGENTS.md | 8 ++- packages/cli/generator/command-roots.ts | 25 ++++++++ packages/cli/generator/index.ts | 7 ++- .../commands/hand-written-metadata.test.ts | 5 +- .../__tests__/commands/previews-root.test.ts | 29 +++++++++ .../__tests__/generator/command-roots.test.ts | 59 +++++++++++++++++++ .../hand-written-leaf-commands.test.ts | 42 +++++++++++++ .../commands/_generated/_meta/commands.json | 2 +- .../_meta/hand-written-commands.json | 9 +-- packages/cli/src/commands/_generated/index.ts | 9 +++ .../src/commands/_generated/previews/index.ts | 22 +++++++ packages/cli/src/commands/hand-written.ts | 17 ++++-- packages/cli/src/commands/previews/deploy.ts | 3 + .../cli/src/commands/previews/deploy/index.ts | 7 +++ .../src/commands/previews/deploy/meta.json | 32 ++++++++++ packages/cli/src/commands/previews/index.ts | 21 ------- packages/cli/src/commands/previews/meta.json | 34 ----------- 18 files changed, 265 insertions(+), 71 deletions(-) create mode 100644 .changeset/previews-command-group.md create mode 100644 packages/cli/generator/command-roots.ts create mode 100644 packages/cli/src/__tests__/commands/previews-root.test.ts create mode 100644 packages/cli/src/__tests__/generator/command-roots.test.ts create mode 100644 packages/cli/src/commands/_generated/previews/index.ts create mode 100644 packages/cli/src/commands/previews/deploy/index.ts create mode 100644 packages/cli/src/commands/previews/deploy/meta.json delete mode 100644 packages/cli/src/commands/previews/index.ts delete mode 100644 packages/cli/src/commands/previews/meta.json diff --git a/.changeset/previews-command-group.md b/.changeset/previews-command-group.md new file mode 100644 index 000000000..54cead731 --- /dev/null +++ b/.changeset/previews-command-group.md @@ -0,0 +1,5 @@ +--- +"cf": patch +--- + +Keep `cf previews deploy` available in a generated command group so future schema updates can add Preview API commands without a root registration collision. diff --git a/packages/cli/AGENTS.md b/packages/cli/AGENTS.md index 21bfca0ce..2b711ba7b 100644 --- a/packages/cli/AGENTS.md +++ b/packages/cli/AGENTS.md @@ -40,8 +40,8 @@ second consumer. | Task | Location | | --------------------------- | ------------------------------------------------------------------------------------------------------------ | | Fix a generated command | Overlay in `@cloudflare/forge` → regenerate | -| Hand-written root command | `src/commands/{auth,build,cli,completions,dev,deploy,init,previews}/`, `schema.ts`, or `tools.ts` | -| Generated-tree exception | `src/commands/{access,ai,registrar,d1,tunnels,workers}/` plus the central registry and generator integration | +| Hand-written root command | `src/commands/{auth,build,cli,completions,dev,deploy,init}/`, `schema.ts`, or `tools.ts` | +| Generated-tree exception | `src/commands/{access,ai,registrar,d1,previews,tunnels,workers}/` plus the central registry and generator integration | | Hand-written registry/meta | `src/commands/hand-written.ts`, per-command `meta.json`, `generator/hand-written-overrides.ts` | | Auth / token / OAuth | `src/lib/auth.ts`, `src/lib/oauth/` | | Runtime value resolution | `src/lib/context.ts`, `src/lib/resolve.ts`, `src/lib/project-settings.ts` | @@ -77,7 +77,9 @@ Forge from the finalized OpenAPI, then runs ## Hand-Written Commands -`auth`, `complete`, `dev`, `build`, `deploy`, `migrate`, `previews`, `cli`, `schema`, and `tools` are root entries in `src/commands/hand-written.ts`, registered lazily by `src/index.ts`. `cf cli telemetry` is the telemetry-settings path. `cf migrate` uses `@cloudflare/codemods` to convert a Wrangler configuration to `cloudflare.config.ts` and report manual follow-up work. `cf dev` analyzes and, when needed, configures a project before running its canonical framework command, with an installed Cloudflare implementation as fallback. `cf build` follows the same routing and validates the resulting Build Output. `cf previews deploy [preview-name]` builds with Preview context and uploads the default Worker from the resulting Preview Build Output, or the one selected with `--worker`. `cf deploy`, `cf workers versions create`, and `cf workers triggers deploy` build first unless `--prebuilt` is passed, then read Build Output and call `@cloudflare/deploy-helpers`; trigger deployment applies only the configured triggers. Deploy-helper provisioning is enabled for supported bindings. +`auth`, `complete`, `dev`, `build`, `deploy`, `migrate`, `cli`, `schema`, and `tools` are root entries in `src/commands/hand-written.ts`, registered lazily by `src/index.ts`. `cf cli telemetry` is the telemetry-settings path. `cf migrate` uses `@cloudflare/codemods` to convert a Wrangler configuration to `cloudflare.config.ts` and report manual follow-up work. `cf dev` analyzes and, when needed, configures a project before running its canonical framework command, with an installed Cloudflare implementation as fallback. `cf build` follows the same routing and validates the resulting Build Output. `cf previews deploy [preview-name]` builds with Preview context and uploads the default Worker from the resulting Preview Build Output, or the one selected with `--worker`. `cf deploy`, `cf workers versions create`, and `cf workers triggers deploy` build first unless `--prebuilt` is passed, then read Build Output and call `@cloudflare/deploy-helpers`; trigger deployment applies only the configured triggers. Deploy-helper provisioning is enabled for supported bindings. + +`previews` uses a generated group containing the hand-written `previews deploy` workflow. Its parent override opts into `createIfMissing`, so generation creates an empty API parent when the pinned schema has no Preview CLI operations. Future schema operations join the same group without colliding with a hand-written root. The registry's other entries are spliced into the generated tree via `generator/hand-written-overrides.ts`: leaf overrides for `cf ai run` and `cf registrar registrations create`; added leaves for `cf workers check`, `cf workers types`, nested `cf workers versions create`, `cf tunnels quick-start`, `cf tunnels run`, and the process-backed `cf access` workflows; and subgroups for `cf d1 migrations` and `cf workers triggers`. The cloudflared-backed Tunnel and Access leaves delegate to the cf-managed cloudflared binary alongside their generated API-backed operations. `cf workers check` profiles the default Worker emitted through Build Output, or the one selected with `--worker`. `cf workers types` is the bounded source-config exception: it validates the Worker in the nearest `cloudflare.config.ts` and writes `.cloudflare/types/index.d.ts`. Version creation remains a project-aware Build Output workflow while the raw upload operations are SDK-only. D1 migration apply targets a database identified by ID only, with `--dir` / `--pattern` / `--table` as flags rather than config; its bookkeeping is wire-compatible with `wrangler d1 migrations apply`. diff --git a/packages/cli/generator/command-roots.ts b/packages/cli/generator/command-roots.ts new file mode 100644 index 000000000..febb1b900 --- /dev/null +++ b/packages/cli/generator/command-roots.ts @@ -0,0 +1,25 @@ +import type { ParentOverrideHandWrittenCommand } from "../src/commands/hand-written.js"; +import type { Schema } from "@cloudflare/forge"; + +export function withHandWrittenCommandRoots( + commands: ReadonlyMap, + overrides: readonly ParentOverrideHandWrittenCommand[] +): Map { + const roots = new Map(commands); + for (const override of overrides) { + if (!override.createIfMissing) continue; + if (override.parent.includes("/")) { + throw new Error("createIfMissing only supports top-level command roots."); + } + if (roots.has(override.parent)) continue; + // Keep local workflows available before their API operations opt into CLI generation. + roots.set(override.parent, { + name: override.parent, + description: override.describe, + methods: [], + globalCliArgs: [], + hideCommand: !override.expose, + }); + } + return roots; +} diff --git a/packages/cli/generator/index.ts b/packages/cli/generator/index.ts index 69f3e4f4f..4ef7f3953 100644 --- a/packages/cli/generator/index.ts +++ b/packages/cli/generator/index.ts @@ -1,10 +1,12 @@ import { leafOverrideHandWrittenCommands, leafHandWrittenCommands, + parentOverrideHandWrittenCommands, rootCommandName, rootHandWrittenCommands, subGroupHandWrittenCommands, } from "../src/commands/hand-written.js"; +import { withHandWrittenCommandRoots } from "./command-roots.js"; import { generateCommandFile, generateGroupIndexFile, @@ -283,7 +285,10 @@ export const transformer: TransformerFn = async (forge: Forge) => { hideCommand: false, }; const commandSchemas: Array<[string, Schema.command]> = [ - ...forge.commands, + ...withHandWrittenCommandRoots( + forge.commands, + parentOverrideHandWrittenCommands() + ), ["access", accessSchema], ]; diff --git a/packages/cli/src/__tests__/commands/hand-written-metadata.test.ts b/packages/cli/src/__tests__/commands/hand-written-metadata.test.ts index 491afdef4..5f2095da0 100644 --- a/packages/cli/src/__tests__/commands/hand-written-metadata.test.ts +++ b/packages/cli/src/__tests__/commands/hand-written-metadata.test.ts @@ -247,9 +247,10 @@ describe("hand-written-only command metadata", () => { dir: "auth", }); expect(commands.get("cf previews deploy")?.handWritten).toEqual({ - kind: "root", + kind: "leaf", overrides: false, - dir: "previews", + dir: "previews/deploy", + parent: "previews", }); expect(commands.get("cf d1 migrations create")?.handWritten).toEqual({ kind: "subgroup", diff --git a/packages/cli/src/__tests__/commands/previews-root.test.ts b/packages/cli/src/__tests__/commands/previews-root.test.ts new file mode 100644 index 000000000..5709d8343 --- /dev/null +++ b/packages/cli/src/__tests__/commands/previews-root.test.ts @@ -0,0 +1,29 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { runCf } from "../helpers/run-cf.js"; + +describe("cf previews group", () => { + let out: ReturnType; + let log: ReturnType; + beforeEach(() => { + out = vi.spyOn(process.stdout, "write").mockImplementation(() => true); + log = vi.spyOn(console, "log").mockImplementation(() => {}); + }); + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("keeps the deployment workflow available in group help", async () => { + await runCf(["previews", "--help"]); + const output = + out.mock.calls.map((call: unknown[]) => String(call[0])).join("") + + log.mock.calls.map((call: unknown[]) => String(call[0])).join("\n"); + expect(output).toContain("Manage Worker Previews"); + expect(output).toContain("previews deploy [preview-name]"); + }); + + it("keeps local mode unavailable for Preview deployment", async () => { + await expect( + runCf(["previews", "deploy", "feature", "--local"]) + ).rejects.toThrow("--local is not supported by cf previews deploy."); + }); +}); diff --git a/packages/cli/src/__tests__/generator/command-roots.test.ts b/packages/cli/src/__tests__/generator/command-roots.test.ts new file mode 100644 index 000000000..79e729e2d --- /dev/null +++ b/packages/cli/src/__tests__/generator/command-roots.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { withHandWrittenCommandRoots } from "../../../generator/command-roots.js"; +import { parentOverrideHandWrittenCommands } from "../../commands/hand-written.js"; +import type { Schema } from "@cloudflare/forge"; + +describe("hand-written command roots", () => { + it("creates an opted-in parent before its API operations exist", () => { + const source = new Map(); + const roots = withHandWrittenCommandRoots( + source, + parentOverrideHandWrittenCommands() + ); + expect(roots.get("previews")).toEqual({ + name: "previews", + description: "Manage Worker Previews", + methods: [], + globalCliArgs: [], + hideCommand: false, + }); + expect(roots.has("access")).toBe(false); + expect(source.size).toBe(0); + }); + + it("preserves the schema-provided parent when it arrives", () => { + const schema: Schema.command = { + name: "previews", + description: "Preview API", + methods: [ + { + name: "delete", + operationId: "fixture.previews.delete", + status: "generally-available", + }, + ], + globalCliArgs: [], + hideCommand: false, + }; + const roots = withHandWrittenCommandRoots( + new Map([[schema.name, schema]]), + parentOverrideHandWrittenCommands() + ); + expect(roots.size).toBe(1); + expect(roots.get("previews")).toBe(schema); + }); + + it("rejects creating a nested parent as a top-level root", () => { + expect(() => + withHandWrittenCommandRoots(new Map(), [ + { + kind: "parentOverride", + parent: "example/nested", + describe: "Nested group", + expose: true, + createIfMissing: true, + }, + ]) + ).toThrow("createIfMissing only supports top-level command roots."); + }); +}); diff --git a/packages/cli/src/__tests__/generator/hand-written-leaf-commands.test.ts b/packages/cli/src/__tests__/generator/hand-written-leaf-commands.test.ts index a3ce6f490..5351a22e8 100644 --- a/packages/cli/src/__tests__/generator/hand-written-leaf-commands.test.ts +++ b/packages/cli/src/__tests__/generator/hand-written-leaf-commands.test.ts @@ -196,6 +196,48 @@ describe("hand-written leaf commands", () => { ); }); + it.each([{ leaves: [] }, { leaves: ["delete"] }])( + "keeps Preview deployment with API leaves $leaves", + ({ leaves }) => { + const [registered] = handWrittenLeafCommands("previews"); + expect(registered).toEqual({ + kind: "leaf", + parent: "previews", + name: "deploy", + dir: "previews/deploy", + }); + if (registered === undefined) { + throw new Error("previews deploy is not registered"); + } + expect( + readHandWrittenLeafCommandMeta("previews", registered) + ).toMatchObject({ + command: "cf previews deploy", + fullPath: ["previews", "deploy"], + }); + const generated = generateResourceIndexFile( + { + name: "previews", + description: "Manage Worker Previews", + methods: [], + globalCliArgs: [], + hideCommand: false, + }, + leaves, + [] + ); + expect(generated).toContain( + "import $deploy from '#commands/previews/deploy/index.js';" + ); + if (leaves.length > 0) { + expect(generated).toContain(".command($delete)"); + } else { + expect(generated).not.toContain(".command($delete)"); + } + expect(generated).toContain(".command($deploy)"); + } + ); + it("binds sidecar identity to the registered command", () => { const [registered] = handWrittenLeafCommands("workers"); if (registered === undefined) { diff --git a/packages/cli/src/commands/_generated/_meta/commands.json b/packages/cli/src/commands/_generated/_meta/commands.json index 98f2b8b88..ac47f74e2 100644 --- a/packages/cli/src/commands/_generated/_meta/commands.json +++ b/packages/cli/src/commands/_generated/_meta/commands.json @@ -157441,6 +157441,7 @@ "pipelines sinks": "Destinations where pipeline data is written — R2 buckets, HTTP endpoints, or other storage", "pipelines streams": "Inbound data streams that feed events into a pipeline for processing", "precursor": "Precursor settings for a zone", + "previews": "Manage Worker Previews", "queries": "queries", "queries bulk": "Operations for bulk", "queues": "Reliable message queuing between Workers — produce, consume, and batch-process messages at scale", @@ -158049,7 +158050,6 @@ "init": "Create a new Cloudflare project or set up an existing one", "migrate": "Migrate a Wrangler project to cf", "cli": "Discover commands and configure the cf CLI", - "previews": "Manage Worker Previews", "schema": "Show API schema details for a command", "tools": "" } diff --git a/packages/cli/src/commands/_generated/_meta/hand-written-commands.json b/packages/cli/src/commands/_generated/_meta/hand-written-commands.json index cfd3584db..e9b37c3ca 100644 --- a/packages/cli/src/commands/_generated/_meta/hand-written-commands.json +++ b/packages/cli/src/commands/_generated/_meta/hand-written-commands.json @@ -1134,9 +1134,10 @@ } ], "handWritten": { - "kind": "root", + "kind": "leaf", "overrides": false, - "dir": "previews" + "dir": "previews/deploy", + "parent": "previews" } }, { @@ -1673,6 +1674,7 @@ "d1": "D1 is Cloudflare's managed, serverless database with SQLite's SQL semantics, built-in disaster recovery, and Worker and HTTP API access.", "d1 migrations": "Create, list, and apply D1 database migrations", "pages": "Full-stack application hosting with Git-integrated builds, preview deployments, and custom domains", + "previews": "Manage Worker Previews", "registrar": "registrar", "registrar registrations": "Operations for registrations", "tunnels": "tunnels", @@ -1681,7 +1683,6 @@ "workers triggers": "Manage triggers (Routes, Workflows, Cron triggers etc.)", "auth": "Manage authentication and profiles", "init": "Create a new Cloudflare project or set up an existing one", - "cli": "Discover commands and configure the cf CLI", - "previews": "Manage Worker Previews" + "cli": "Discover commands and configure the cf CLI" } } diff --git a/packages/cli/src/commands/_generated/index.ts b/packages/cli/src/commands/_generated/index.ts index 91fa59b07..6bd3d5036 100644 --- a/packages/cli/src/commands/_generated/index.ts +++ b/packages/cli/src/commands/_generated/index.ts @@ -1056,6 +1056,15 @@ export const generatedCommands: GeneratedCommand[] = [ ), hideCommand: true, }, + { + command: lazyCommand( + "previews", + "Manage Worker Previews", + () => import("./previews/index.js"), + null + ), + hideCommand: false, + }, { command: lazyCommand( "queries", diff --git a/packages/cli/src/commands/_generated/previews/index.ts b/packages/cli/src/commands/_generated/previews/index.ts new file mode 100644 index 000000000..242938188 --- /dev/null +++ b/packages/cli/src/commands/_generated/previews/index.ts @@ -0,0 +1,22 @@ +import $deploy from "#commands/previews/deploy/index.js"; +import type { CommonYargsOptions } from "#lib/cli-types.js"; +/** + * previews command + * @generated from apis/overlays/previews.ts + */ +import type { CommandModule } from "yargs"; + +const command: CommandModule = { + command: "previews", + describe: "Manage Worker Previews", + + builder: (yargs) => { + return yargs + .command($deploy) + .demandCommand(1, "Please specify a subcommand"); + }, + + handler: () => {}, +}; + +export default command; diff --git a/packages/cli/src/commands/hand-written.ts b/packages/cli/src/commands/hand-written.ts index dae390b78..9f98a25a0 100644 --- a/packages/cli/src/commands/hand-written.ts +++ b/packages/cli/src/commands/hand-written.ts @@ -30,6 +30,8 @@ export interface ParentOverrideHandWrittenCommand { parent: string; describe: string; expose: boolean; + /** Create the root for local leaves when OpenAPI does not provide it yet. */ + createIfMissing?: boolean; } export interface SubGroupHandWrittenCommand { @@ -135,12 +137,17 @@ export const handWrittenCommands: readonly HandWrittenCommand[] = [ telemetry: { command: "cli", recordArgs: false }, }, { - kind: "root", - command: "previews", + kind: "leaf", + parent: "previews", + name: "deploy", + dir: "previews/deploy", + }, + { + kind: "parentOverride", + parent: "previews", describe: "Manage Worker Previews", - dir: "previews", - load: () => import("./previews/index.js"), - telemetry: { command: "previews", recordArgs: false }, + expose: true, + createIfMissing: true, }, { kind: "root", diff --git a/packages/cli/src/commands/previews/deploy.ts b/packages/cli/src/commands/previews/deploy.ts index 5529ccdbe..7d4c084e7 100644 --- a/packages/cli/src/commands/previews/deploy.ts +++ b/packages/cli/src/commands/previews/deploy.ts @@ -202,6 +202,9 @@ const command: CommandModule = { describe: "Deploy a Worker Preview", builder, handler: async (argv) => { + if (argv.local) { + throw new Error("--local is not supported by cf previews deploy."); + } const result = await runPreviewDeploy(argv); formatOutput(createPreviewDeployOutput(result), { quiet: argv.quiet }); }, diff --git a/packages/cli/src/commands/previews/deploy/index.ts b/packages/cli/src/commands/previews/deploy/index.ts new file mode 100644 index 000000000..3d3e92b13 --- /dev/null +++ b/packages/cli/src/commands/previews/deploy/index.ts @@ -0,0 +1,7 @@ +import { withTelemetry } from "../../../lib/telemetry/index.js"; +import deployCommand from "../deploy.js"; + +export default withTelemetry(deployCommand, { + command: "previews deploy", + classification: { safeFlags: ["prebuilt"] }, +}); diff --git a/packages/cli/src/commands/previews/deploy/meta.json b/packages/cli/src/commands/previews/deploy/meta.json new file mode 100644 index 000000000..6abc20370 --- /dev/null +++ b/packages/cli/src/commands/previews/deploy/meta.json @@ -0,0 +1,32 @@ +{ + "command": "cf previews deploy", + "name": "deploy", + "fullPath": ["previews", "deploy"], + "category": "action", + "description": "Deploy a Worker Preview", + "usage": "cf previews deploy [preview-name] [options]", + "arguments": [ + { + "name": "preview-name", + "position": 0, + "type": "string", + "required": false, + "description": "Preview name. Defaults to the current CI or Git branch; required if no branch is available." + } + ], + "options": [ + { + "name": "prebuilt", + "type": "boolean", + "required": false, + "description": "Use existing Preview Build Output Specification files without building", + "default": false + }, + { + "name": "worker", + "type": "string", + "required": false, + "description": "Name of the Worker in the Build Output to use (defaults to the default Worker)" + } + ] +} diff --git a/packages/cli/src/commands/previews/index.ts b/packages/cli/src/commands/previews/index.ts deleted file mode 100644 index 0e3b23cce..000000000 --- a/packages/cli/src/commands/previews/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { withTelemetry } from "../../lib/telemetry/index.js"; -import deployCommand from "./deploy.js"; -import type { CommonYargsOptions } from "../../lib/cli-types.js"; -import type { CommandModule } from "yargs"; - -const command: CommandModule = { - command: "previews", - describe: "Manage Worker Previews", - builder: (yargs) => - yargs - .command( - withTelemetry(deployCommand, { - command: "previews deploy", - classification: { safeFlags: ["prebuilt"] }, - }) - ) - .demandCommand(1, "Please specify a subcommand"), - handler: () => {}, -}; - -export default command; diff --git a/packages/cli/src/commands/previews/meta.json b/packages/cli/src/commands/previews/meta.json deleted file mode 100644 index 1d7f37594..000000000 --- a/packages/cli/src/commands/previews/meta.json +++ /dev/null @@ -1,34 +0,0 @@ -[ - { - "command": "cf previews deploy", - "name": "deploy", - "fullPath": ["previews", "deploy"], - "category": "action", - "description": "Deploy a Worker Preview", - "usage": "cf previews deploy [preview-name] [options]", - "arguments": [ - { - "name": "preview-name", - "position": 0, - "type": "string", - "required": false, - "description": "Preview name. Defaults to the current CI or Git branch; required if no branch is available." - } - ], - "options": [ - { - "name": "prebuilt", - "type": "boolean", - "required": false, - "description": "Use existing Preview Build Output Specification files without building", - "default": false - }, - { - "name": "worker", - "type": "string", - "required": false, - "description": "Name of the Worker in the Build Output to use (defaults to the default Worker)" - } - ] - } -] From 70159e090a3ab6780e44d1895beec07033380e5b Mon Sep 17 00:00:00 2001 From: Greg Brimble Date: Fri, 2 Oct 2026 10:13:57 -0400 Subject: [PATCH 2/2] WC-6122 Test command precedence rules --- packages/cli/generator/generator.ts | 2 +- packages/cli/generator/index.ts | 6 +- .../hand-written-leaf-commands.test.ts | 16 +++++ .../generator/parent-overrides.test.ts | 63 +++++++++++++++++++ 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 packages/cli/src/__tests__/generator/parent-overrides.test.ts diff --git a/packages/cli/generator/generator.ts b/packages/cli/generator/generator.ts index 7ec390a71..28aece600 100644 --- a/packages/cli/generator/generator.ts +++ b/packages/cli/generator/generator.ts @@ -9,7 +9,7 @@ import { typedBuilderDeclaration, typedSdkTypeAliases, } from "./emit/sdk-path.js"; -import { generateCommandMeta, type GeneratedCommandMeta } from "./metadata"; +import { generateCommandMeta, type GeneratedCommandMeta } from "./metadata.js"; import { getTelemetrySafeFlags } from "./telemetry.js"; import { escapeForSingleQuote } from "./util.js"; import type { Schema } from "@cloudflare/forge"; diff --git a/packages/cli/generator/index.ts b/packages/cli/generator/index.ts index 4ef7f3953..e961a50b4 100644 --- a/packages/cli/generator/index.ts +++ b/packages/cli/generator/index.ts @@ -12,7 +12,7 @@ import { generateGroupIndexFile, generateResourceIndexFile, isMethodGroup, -} from "./generator"; +} from "./generator.js"; import { handWrittenLeafCommands, handWrittenLeafOverrideDir, @@ -29,7 +29,7 @@ import { generateSchemaInfo, type HandWrittenCommandMeta, type SchemaInfo, -} from "./metadata"; +} from "./metadata.js"; import { errorMessage, escapeForSingleQuote } from "./util.js"; /** * CLI Transformer @@ -518,7 +518,7 @@ ${entries} } } for (const command of rootHandWrittenCommands()) { - const name = command.command.split(/\s+/)[0]; + const name = rootCommandName(command); descriptions[name] = command.describe === false ? "" : command.describe; } diff --git a/packages/cli/src/__tests__/generator/hand-written-leaf-commands.test.ts b/packages/cli/src/__tests__/generator/hand-written-leaf-commands.test.ts index 5351a22e8..2ea603aac 100644 --- a/packages/cli/src/__tests__/generator/hand-written-leaf-commands.test.ts +++ b/packages/cli/src/__tests__/generator/hand-written-leaf-commands.test.ts @@ -23,6 +23,22 @@ const accessSchema = { } as Parameters[0]; describe("hand-written leaf commands", () => { + it("uses an explicit hand-written override for a schema-provided leaf", () => { + const generated = generateResourceIndexFile( + { + name: "ai", + description: "AI", + } as Parameters[0], + ["run"], + [] + ); + expect(generated).toContain( + "import $run from '#commands/ai/run/index.js';" + ); + expect(generated).not.toContain("import $run from './run.js';"); + expect(generated).toContain(".command($run)"); + }); + it("registers a leaf command against its generated product", () => { expect(handWrittenLeafCommands("workers")).toEqual([ { diff --git a/packages/cli/src/__tests__/generator/parent-overrides.test.ts b/packages/cli/src/__tests__/generator/parent-overrides.test.ts new file mode 100644 index 000000000..1e8fced27 --- /dev/null +++ b/packages/cli/src/__tests__/generator/parent-overrides.test.ts @@ -0,0 +1,63 @@ +import { Forge } from "@cloudflare/forge"; +import { describe, expect, it, vi } from "vitest"; +import { transformer } from "../../../generator/index.js"; +import type * as HandWrittenRegistry from "../../commands/hand-written.js"; + +const settings = vi.hoisted(() => ({ expose: true })); + +// Isolate parent presentation from the registry's unrelated command sidecars. +vi.mock("../../commands/hand-written.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + handWrittenCommands: [], + rootHandWrittenCommands: () => [], + leafHandWrittenCommands: () => [], + leafOverrideHandWrittenCommands: () => [], + subGroupHandWrittenCommands: () => [], + parentOverrideHandWrittenCommands: () => [ + { + kind: "parentOverride", + parent: "fixture", + describe: "Local description", + expose: settings.expose, + }, + ], + }; +}); + +describe("parent presentation overrides", () => { + it.each([true, false])( + "applies expose=%s to a hidden schema root", + async (expose) => { + settings.expose = expose; + const forge = new Forge({ + openapi: "3.0.0", + info: { title: "Fixture", version: "1" }, + paths: {}, + }); + forge.commands.set("fixture", { + name: "fixture", + description: "Schema description", + methods: [], + globalCliArgs: [], + hideCommand: true, + }); + const files = new Map( + (await forge.transform(transformer)).map((file) => [ + file.path, + file.content, + ]) + ); + expect(files.get("index.ts")).toContain( + `lazyCommand('fixture', 'Local description', () => import('./fixture/index.js'), null), hideCommand: ${!expose}` + ); + expect(files.get("fixture/index.ts")).toContain( + "describe: 'Local description'" + ); + expect( + JSON.parse(files.get("_meta/commands.json") ?? "").descriptions + ).toMatchObject({ fixture: "Local description" }); + } + ); +});