From cbd2a4cf8ffe37754b6ceb7b02263d5a94ada3a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 03:58:31 +0000 Subject: [PATCH 1/4] Type command `values` from declared option presets Each command's run body typed its `values` shape by hand, re-listing the flags its option presets ("ns", "control", "json", ...) expand to. That duplicated CLI_OPTION_PRESETS in common.js and could silently drift from it -- a preset gaining or dropping a flag would not surface in the handler's type. Add a `PresetFlags

` typedef to lib/command.js: `PresetValueMap` records the `values` each preset contributes (kept beside the preset list it mirrors), and `PresetFlags<"ns" | "control" | "json">` intersects a chosen set via a `UnionToIntersection` helper. Commands now name their presets and intersect only their own defineCliOption flags inline, e.g. `PresetFlags<"ns" | "control" | "json"> & { worker?: string, yes?: boolean }`. This is type-only: no runtime change, zero `any`, and the preset-contributed portion of every handler's `values` is now derived from one source instead of restated per command. Converted config, d1, delete, deploy, doctor, r2, secret, tail, token, whoami, and workflows; workers reads `context.values` (no typed binding) and init uses its own parse shell, so both are untouched. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Lu Zhang --- commands/config.js | 2 +- commands/d1.js | 18 +++++++++--------- commands/delete.js | 2 +- commands/deploy.js | 2 +- commands/doctor.js | 2 +- commands/r2.js | 2 +- commands/secret.js | 2 +- commands/tail.js | 2 +- commands/token.js | 2 +- commands/whoami.js | 2 +- commands/workflows.js | 2 +- lib/command.js | 32 +++++++++++++++++++++++++++++++- 12 files changed, 50 insertions(+), 20 deletions(-) diff --git a/commands/config.js b/commands/config.js index edfe6b9..2234b55 100644 --- a/commands/config.js +++ b/commands/config.js @@ -18,7 +18,7 @@ export const main = command.main; export const runConfigCommand = command.run; export const meta = command.meta; -/** @param {{ values: { json?: boolean }, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ +/** @param {{ values: import("../lib/command.js").PresetFlags<"ns" | "control" | "json">, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ async function runConfig({ values, positionals, context }) { const [subcommand, extra] = positionals; if (subcommand !== "explain" || extra) throw new CliError(usageText()); diff --git a/commands/d1.js b/commands/d1.js index 38a0871..53a2390 100644 --- a/commands/d1.js +++ b/commands/d1.js @@ -48,15 +48,15 @@ export const runD1Command = command.run; export const meta = command.meta; /** - * @typedef {object} D1Flags - * @property {string} [sql] - * @property {string} [file] - * @property {string} [mode] - * @property {string} [params] - * @property {string} [dir] - * @property {string} [env] - * @property {boolean} [yes] - * @property {boolean} [json] + * @typedef {import("../lib/command.js").PresetFlags<"ns" | "control" | "json"> & { + * sql?: string, + * file?: string, + * mode?: string, + * params?: string, + * dir?: string, + * env?: string, + * yes?: boolean, + * }} D1Flags */ /** @param {{ values: D1Flags, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ diff --git a/commands/delete.js b/commands/delete.js index 2db7712..fe48cf7 100644 --- a/commands/delete.js +++ b/commands/delete.js @@ -27,7 +27,7 @@ export const main = command.main; export const runDeleteCommand = command.run; export const meta = command.meta; -/** @param {{ values: { worker?: string, version?: string, "dry-run"?: boolean, yes?: boolean, json?: boolean }, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ +/** @param {{ values: import("../lib/command.js").PresetFlags<"ns" | "control" | "json"> & { worker?: string, version?: string, "dry-run"?: boolean, yes?: boolean }, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ async function runDelete({ values, positionals, context }) { const { stdout, stderr, stdin } = context; diff --git a/commands/deploy.js b/commands/deploy.js index bd2f272..ff417db 100644 --- a/commands/deploy.js +++ b/commands/deploy.js @@ -164,7 +164,7 @@ export const meta = command.meta; * @typedef {import("../lib/command.js").CommandContext & { execFile: typeof execFileSync }} DeployContext */ -/** @param {{ values: { env?: string, verbose?: boolean }, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ +/** @param {{ values: import("../lib/command.js").PresetFlags<"ns" | "control"> & { env?: string, verbose?: boolean }, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ async function runDeploy({ values, positionals, context: baseContext }) { const context = /** @type {DeployContext} */ (baseContext); const { env, stdout, stderr, cwd, execFile } = context; diff --git a/commands/doctor.js b/commands/doctor.js index 5985c82..8772349 100644 --- a/commands/doctor.js +++ b/commands/doctor.js @@ -43,7 +43,7 @@ export const meta = command.meta; * @typedef {import("../lib/command.js").CommandContext & { execFile: typeof execFileSync }} DoctorContext */ -/** @param {{ values: { ns?: string, "control-url"?: string, token?: string, "no-token-store"?: boolean, json?: boolean }, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ +/** @param {{ values: import("../lib/command.js").PresetFlags<"ns" | "control" | "json">, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ async function runDoctor({ values, positionals, context: baseContext }) { if (positionals.length > 0) throw new CliError(usageText()); diff --git a/commands/r2.js b/commands/r2.js index 9539bca..95aea55 100644 --- a/commands/r2.js +++ b/commands/r2.js @@ -45,7 +45,7 @@ export const meta = command.meta; /** * @param {{ - * values: { prefix?: string, delimiter?: string, cursor?: string, limit?: string, out?: string, yes?: boolean, json?: boolean }, + * values: import("../lib/command.js").PresetFlags<"ns" | "control" | "json"> & { prefix?: string, delimiter?: string, cursor?: string, limit?: string, out?: string, yes?: boolean }, * positionals: string[], * context: import("../lib/command.js").CommandContext, * }} arg diff --git a/commands/secret.js b/commands/secret.js index f54c2e2..840830c 100644 --- a/commands/secret.js +++ b/commands/secret.js @@ -48,7 +48,7 @@ export const meta = command.meta; * @property {PromoteWarning[]} [warnings] */ -/** @param {{ values: { worker?: string, scope?: string, yes?: boolean, json?: boolean }, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ +/** @param {{ values: import("../lib/command.js").PresetFlags<"ns" | "control" | "json"> & { worker?: string, scope?: string, yes?: boolean }, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ async function runSecret({ values, positionals, context }) { const { stdout, stderr, stdin } = context; diff --git a/commands/tail.js b/commands/tail.js index 1372e32..051f514 100644 --- a/commands/tail.js +++ b/commands/tail.js @@ -113,7 +113,7 @@ export const meta = command.meta; * }} TailContext */ -/** @param {{ values: { raw?: boolean, since?: string, "max-reconnects"?: string, ns?: string, "control-url"?: string, token?: string, "no-token-store"?: boolean }, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ +/** @param {{ values: import("../lib/command.js").PresetFlags<"ns" | "control"> & { raw?: boolean, since?: string, "max-reconnects"?: string }, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ async function runTail({ values, positionals, context: baseContext }) { const context = /** @type {TailContext} */ (baseContext); const { stdout, stderr, transport, sleepFn, now } = context; diff --git a/commands/token.js b/commands/token.js index 10dd040..e897e89 100644 --- a/commands/token.js +++ b/commands/token.js @@ -42,7 +42,7 @@ export const runTokenCommand = command.run; export const meta = command.meta; /** - * @typedef {{ label?: string, default?: boolean, ns?: string, "control-url"?: string, json?: boolean }} TokenValues + * @typedef {import("../lib/command.js").PresetFlags<"endpoint"> & { label?: string, default?: boolean, ns?: string, json?: boolean }} TokenValues */ /** @param {{ values: TokenValues, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ diff --git a/commands/whoami.js b/commands/whoami.js index e9d8cf1..9d81c15 100644 --- a/commands/whoami.js +++ b/commands/whoami.js @@ -26,7 +26,7 @@ export const main = command.main; export const runWhoamiCommand = command.run; export const meta = command.meta; -/** @param {{ values: { json?: boolean }, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ +/** @param {{ values: import("../lib/command.js").PresetFlags<"ns" | "control" | "json">, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ async function runWhoami({ values, positionals, context }) { if (positionals.length > 0) throw new CliError(usageText()); diff --git a/commands/workflows.js b/commands/workflows.js index 0c64294..f7a966c 100644 --- a/commands/workflows.js +++ b/commands/workflows.js @@ -33,7 +33,7 @@ export const main = command.main; export const runWorkflowsCommand = command.run; export const meta = command.meta; -/** @param {{ values: { limit?: string, cursor?: string, "include-steps"?: boolean, "step-limit"?: string, yes?: boolean, json?: boolean }, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ +/** @param {{ values: import("../lib/command.js").PresetFlags<"ns" | "control" | "json"> & { limit?: string, cursor?: string, "include-steps"?: boolean, "step-limit"?: string, yes?: boolean }, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ async function runWorkflows({ values, positionals, context }) { const { stdout, stderr, stdin } = context; diff --git a/lib/command.js b/lib/command.js index 89bd379..efd57ba 100644 --- a/lib/command.js +++ b/lib/command.js @@ -19,7 +19,8 @@ import { resolveControlContext, resolveNamespace, warnIfInsecureControlUrl } fro * Flag-preset names accepted in a command's `options` list; each expands to * the matching shared option specs: * "ns" -> --ns - * "control" -> --control-url, --token + * "control" -> --control-url, --token, --no-token-store + * "endpoint"-> --control-url * "env" -> --env * "json" -> --json * "yes" -> --yes @@ -27,6 +28,35 @@ import { resolveControlContext, resolveNamespace, warnIfInsecureControlUrl } fro * @typedef {string} OptionPreset */ +/** + * @template U + * @typedef {(U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never} UnionToIntersection + */ + +/** + * The parsed `values` each option preset contributes, keyed by preset name. + * Keep in sync with CLI_OPTION_PRESETS in common.js: a command names its presets + * via PresetFlags<...> instead of re-typing their flags, so the schema and the + * handler's `values` type can't drift (the `--control` preset has no `control` + * key — it's --control-url/--token/--no-token-store). + * @typedef {object} PresetValueMap + * @property {{ ns?: string }} ns + * @property {{ env?: string }} env + * @property {{ "control-url"?: string, token?: string, "no-token-store"?: boolean }} control + * @property {{ "control-url"?: string }} endpoint + * @property {{ json?: boolean }} json + * @property {{ yes?: boolean }} yes + * @property {{ help?: boolean }} help + */ + +/** + * The `values` contributed by a set of option presets, e.g. + * `PresetFlags<"ns" | "control" | "json">`. Intersect with a command's own + * flags: `PresetFlags<"ns"> & { raw?: boolean }`. + * @template {keyof PresetValueMap} P + * @typedef {UnionToIntersection} PresetFlags + */ + /** * The object handed to every command's run body. Framework members are typed, * so a misspelled access (e.g. `context.fetchJson` typed as `fetchJsonn`) is a From 82ca423d8ebdc5e4bf370fb5dec4c96bf288b5e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 04:13:04 +0000 Subject: [PATCH 2/4] Type workers `values` from its option presets too `runWorkers` was the last defineCommand handler still reading the json flag off the untyped `context.values` (Record), leaving its ["ns", "control", "json"] schema uncovered by the typed preset composition. Bind a typed `values: PresetFlags<"ns" | "control" | "json">` in the run body and thread the json boolean into printWorkersList as a parameter, so the read goes through the derived type instead of the loose context bag. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Lu Zhang --- commands/workers.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/commands/workers.js b/commands/workers.js index 1400cfb..63e9646 100644 --- a/commands/workers.js +++ b/commands/workers.js @@ -19,21 +19,24 @@ export const meta = command.meta; // Re-exported for the existing test import surface; logic lives in lib/. export { formatWorkersList }; -/** @param {{ positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ -async function runWorkers({ positionals, context }) { +/** @param {{ values: import("../lib/command.js").PresetFlags<"ns" | "control" | "json">, positionals: string[], context: import("../lib/command.js").CommandContext }} arg */ +async function runWorkers({ values, positionals, context }) { const ns = context.resolveNamespace(); if (positionals.length > 0) throw new CliError(usageText()); if (!ns) throw new CliError(usageText()); - await printWorkersList(context); + await printWorkersList(context, values.json === true); } -/** @param {import("../lib/command.js").CommandContext} context */ -async function printWorkersList(context) { +/** + * @param {import("../lib/command.js").CommandContext} context + * @param {boolean} json + */ +async function printWorkersList(context, json) { const { headers } = context.resolveControl(); const body = /** @type {{ workers?: import("../lib/workers-format.js").WorkerSummary[] }} */ ( await context.fetchJson(context.nsUrl("workers"), { headers }, "list workers") ); - writeResult(context.values.json === true, body, () => formatWorkersList(body), context.stdout); + writeResult(json, body, () => formatWorkersList(body), context.stdout); } function usageText() { From d2a1f5850b8111541e6a8cdb2cffff4e68c5e674 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 04:24:36 +0000 Subject: [PATCH 3/4] Clarify PresetValueMap doc: preset names vs values keys Reword the parenthetical so it no longer says "--control preset" (the preset is the string "control"; there is no --control flag) and separates preset names from the parsed `values` keys they map to. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Lu Zhang --- lib/command.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/command.js b/lib/command.js index efd57ba..3708546 100644 --- a/lib/command.js +++ b/lib/command.js @@ -37,8 +37,10 @@ import { resolveControlContext, resolveNamespace, warnIfInsecureControlUrl } fro * The parsed `values` each option preset contributes, keyed by preset name. * Keep in sync with CLI_OPTION_PRESETS in common.js: a command names its presets * via PresetFlags<...> instead of re-typing their flags, so the schema and the - * handler's `values` type can't drift (the `--control` preset has no `control` - * key — it's --control-url/--token/--no-token-store). + * handler's `values` type can't drift. Each entry is keyed by preset name; its + * value lists the parsed `values` keys that preset contributes — which need not + * match the name (the "control" preset contributes "control-url", "token", and + * "no-token-store", not a "control" key). * @typedef {object} PresetValueMap * @property {{ ns?: string }} ns * @property {{ env?: string }} env From 0b45aa2790fba58b25081e8c4af4b92adaf21aa0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 04:31:41 +0000 Subject: [PATCH 4/4] Trim redundant phrasing in PresetValueMap doc The clarification repeated "keyed by preset name"; collapse it to one line. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Lu Zhang --- lib/command.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/command.js b/lib/command.js index 3708546..9f70269 100644 --- a/lib/command.js +++ b/lib/command.js @@ -37,10 +37,8 @@ import { resolveControlContext, resolveNamespace, warnIfInsecureControlUrl } fro * The parsed `values` each option preset contributes, keyed by preset name. * Keep in sync with CLI_OPTION_PRESETS in common.js: a command names its presets * via PresetFlags<...> instead of re-typing their flags, so the schema and the - * handler's `values` type can't drift. Each entry is keyed by preset name; its - * value lists the parsed `values` keys that preset contributes — which need not - * match the name (the "control" preset contributes "control-url", "token", and - * "no-token-store", not a "control" key). + * handler's `values` type can't drift. A preset's value keys need not match its + * name — "control" contributes "control-url", "token", and "no-token-store". * @typedef {object} PresetValueMap * @property {{ ns?: string }} ns * @property {{ env?: string }} env