@smeltjs/core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/cli/bin.d.ts.map +1 -1
  2. package/dist/cli/bin.js +9 -4
  3. package/dist/cli/bin.js.map +1 -1
  4. package/dist/cli/doctor.d.ts +66 -0
  5. package/dist/cli/doctor.d.ts.map +1 -0
  6. package/dist/cli/doctor.js +137 -0
  7. package/dist/cli/doctor.js.map +1 -0
  8. package/dist/cli/hooks.d.ts +36 -2
  9. package/dist/cli/hooks.d.ts.map +1 -1
  10. package/dist/cli/hooks.js +108 -64
  11. package/dist/cli/hooks.js.map +1 -1
  12. package/dist/cli/init.d.ts.map +1 -1
  13. package/dist/cli/init.js +8 -14
  14. package/dist/cli/init.js.map +1 -1
  15. package/dist/cli/installed.d.ts +56 -0
  16. package/dist/cli/installed.d.ts.map +1 -0
  17. package/dist/cli/installed.js +105 -0
  18. package/dist/cli/installed.js.map +1 -0
  19. package/dist/cli/lava.d.ts +31 -0
  20. package/dist/cli/lava.d.ts.map +1 -0
  21. package/dist/cli/lava.js +82 -0
  22. package/dist/cli/lava.js.map +1 -0
  23. package/dist/cli/setup.d.ts +87 -0
  24. package/dist/cli/setup.d.ts.map +1 -0
  25. package/dist/cli/setup.js +513 -0
  26. package/dist/cli/setup.js.map +1 -0
  27. package/dist/cli/shell.d.ts +11 -4
  28. package/dist/cli/shell.d.ts.map +1 -1
  29. package/dist/cli/shell.js.map +1 -1
  30. package/dist/cli/subcommands/agents.d.ts.map +1 -1
  31. package/dist/cli/subcommands/agents.js +2 -1
  32. package/dist/cli/subcommands/agents.js.map +1 -1
  33. package/dist/cli/subcommands/doctor.d.ts +15 -0
  34. package/dist/cli/subcommands/doctor.d.ts.map +1 -0
  35. package/dist/cli/subcommands/doctor.js +43 -0
  36. package/dist/cli/subcommands/doctor.js.map +1 -0
  37. package/dist/cli/subcommands/flags.d.ts +13 -0
  38. package/dist/cli/subcommands/flags.d.ts.map +1 -1
  39. package/dist/cli/subcommands/flags.js +27 -1
  40. package/dist/cli/subcommands/flags.js.map +1 -1
  41. package/dist/cli/subcommands/hooks.d.ts.map +1 -1
  42. package/dist/cli/subcommands/hooks.js +10 -2
  43. package/dist/cli/subcommands/hooks.js.map +1 -1
  44. package/dist/cli/subcommands/init.d.ts.map +1 -1
  45. package/dist/cli/subcommands/init.js +2 -1
  46. package/dist/cli/subcommands/init.js.map +1 -1
  47. package/dist/cli/subcommands/registry.d.ts +3 -1
  48. package/dist/cli/subcommands/registry.d.ts.map +1 -1
  49. package/dist/cli/subcommands/registry.js +6 -0
  50. package/dist/cli/subcommands/registry.js.map +1 -1
  51. package/dist/cli/subcommands/setup.d.ts +23 -0
  52. package/dist/cli/subcommands/setup.d.ts.map +1 -0
  53. package/dist/cli/subcommands/setup.js +87 -0
  54. package/dist/cli/subcommands/setup.js.map +1 -0
  55. package/dist/cli/subcommands/subcommand.d.ts +1 -1
  56. package/dist/cli/subcommands/subcommand.d.ts.map +1 -1
  57. package/dist/cli/subcommands/subcommand.js.map +1 -1
  58. package/dist/cli/wizard.d.ts +72 -0
  59. package/dist/cli/wizard.d.ts.map +1 -0
  60. package/dist/cli/wizard.js +86 -0
  61. package/dist/cli/wizard.js.map +1 -0
  62. package/dist/harness/claude-code.d.ts.map +1 -1
  63. package/dist/harness/claude-code.js +10 -0
  64. package/dist/harness/claude-code.js.map +1 -1
  65. package/dist/harness/codex.d.ts.map +1 -1
  66. package/dist/harness/codex.js +1 -0
  67. package/dist/harness/codex.js.map +1 -1
  68. package/dist/harness/grok.d.ts.map +1 -1
  69. package/dist/harness/grok.js +4 -1
  70. package/dist/harness/grok.js.map +1 -1
  71. package/dist/harness/kilocode.d.ts.map +1 -1
  72. package/dist/harness/kilocode.js +1 -1
  73. package/dist/harness/kilocode.js.map +1 -1
  74. package/dist/harness/opencode.d.ts.map +1 -1
  75. package/dist/harness/opencode.js +10 -0
  76. package/dist/harness/opencode.js.map +1 -1
  77. package/dist/harness/profile.d.ts +23 -2
  78. package/dist/harness/profile.d.ts.map +1 -1
  79. package/dist/harness/profile.js.map +1 -1
  80. package/dist/harness/snippet.d.ts +14 -1
  81. package/dist/harness/snippet.d.ts.map +1 -1
  82. package/dist/harness/snippet.js +18 -2
  83. package/dist/harness/snippet.js.map +1 -1
  84. package/dist/index.d.ts +12 -0
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +9 -0
  87. package/dist/index.js.map +1 -1
  88. package/dist/setup/recipe.d.ts +73 -0
  89. package/dist/setup/recipe.d.ts.map +1 -0
  90. package/dist/setup/recipe.js +76 -0
  91. package/dist/setup/recipe.js.map +1 -0
  92. package/dist/text/json-edit.d.ts +15 -1
  93. package/dist/text/json-edit.d.ts.map +1 -1
  94. package/dist/text/json-edit.js +76 -5
  95. package/dist/text/json-edit.js.map +1 -1
  96. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import process from 'node:process';
2
2
  import { CliUsageError } from '../../errors.js';
3
3
  import { harnessesByTier, harnessNames } from '../../harness/registry.js';
4
+ import { colorize } from '../lava.js';
4
5
  import { runHooks } from '../hooks.js';
5
6
  import { CLI_NAME } from '../shell.js';
6
7
  /**
@@ -53,10 +54,16 @@ export const hooksCommand = {
53
54
  throw new CliUsageError(`${CLI_NAME}: hooks ${action} takes no further arguments, got ` +
54
55
  `${positionals.slice(2).join(', ')}.`);
55
56
  }
57
+ // `--harness` is repeatable for setup; this verb wires one action per run, and a
58
+ // second id would be a second install the user believed had happened.
59
+ if (values.harness !== undefined && values.harness.length > 1) {
60
+ throw new CliUsageError(`${CLI_NAME}: hooks takes one --harness per run — repeat the command for ` +
61
+ `each harness.`);
62
+ }
56
63
  return {
57
64
  mode: 'hooks',
58
65
  action,
59
- ...(values.harness === undefined ? {} : { harness: values.harness }),
66
+ ...(values.harness === undefined ? {} : { harness: values.harness[0] }),
60
67
  };
61
68
  },
62
69
  /** Nothing to merge: the wizard asks, and reads `smelt.config.json` itself. */
@@ -72,8 +79,9 @@ export const hooksCommand = {
72
79
  }
73
80
  return await runHooks(resolved.action, resolved.harness, {
74
81
  input: io.initInput,
75
- output: io.stdout,
82
+ output: (text) => io.stdout(colorize(text, io.color === true)),
76
83
  cwd: io.cwd ?? process.cwd(),
84
+ version: io.version,
77
85
  });
78
86
  },
79
87
  };
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../../src/cli/subcommands/hooks.ts"],"names":[],"mappings":"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","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { harnessesByTier, harnessNames } from '../../harness/registry.ts';\nimport type { HarnessTier } from '../../harness/profile.ts';\nimport { runHooks } from '../hooks.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt hooks install` / `smelt hooks remove` — the harness-hooks installer's front\n * door. The installer itself is `cli/hooks.ts`; this file is only the verb.\n *\n * Like `init`, it is interactive — the wizard asks everything except which harness —\n * so `--harness` is the one flag it owns, and the registry refuses the rest. The id\n * itself is validated in `cli/hooks.ts` against the harness registry in `src/harness/`,\n * which is also where the `--harness` help list comes from.\n */\n\n/** `smelt hooks <install|remove> [--harness <id>]` — parsed. */\nexport interface HooksInvocation {\n readonly mode: 'hooks';\n readonly action: 'install' | 'remove';\n readonly harness?: string;\n}\n\n/**\n * The harnesses at one tier, as the HOOKS paragraph spells them — from\n * `HarnessProfile.tier`, never a second list. A tier no profile claims renders empty\n * rather than naming a harness that moved.\n *\n * The line breaks around these are still hand-placed, and deliberately: this body is\n * byte-pinned by `test/__snapshots__/cli-usage.help.txt`, its paragraph is wrapped by\n * hand at no single width, and a generic wrapper would rewrite every line of the help\n * to derive three lists. The *membership* is what drifted — a promoted harness stayed\n * under its old tier — and membership is what this derives.\n */\nfunction tierNames(tier: HarnessTier): string {\n const group = harnessesByTier().find((candidate) => candidate.tier === tier);\n return group === undefined ? '' : harnessNames(group.harnesses);\n}\n\nexport const hooksCommand: Subcommand<HooksInvocation, HooksInvocation> = {\n name: 'hooks',\n flags: ['harness'],\n refusal: `hooks is interactive; the wizard asks the rest.`,\n usage: {\n synopsis: ['hooks install [--harness <id>]', 'hooks remove [--harness <id>]'],\n section: {\n heading: 'HOOKS',\n body:\n ` ${CLI_NAME} hooks install wires the smelt guard into agent-harness hooks: a\\n` +\n ` PreToolUse size-guard that refuses oversized raw reads with the exact ${CLI_NAME}\\n` +\n ` replacement command (default on), \\`${CLI_NAME} stats\\` at session end (default\\n` +\n ` on), and an opening \\`${CLI_NAME} map\\` at session start (opt-in) — plus an\\n` +\n ` instruction-file snippet that teaches \\`${CLI_NAME} retrieve\\` after a deny.\\n` +\n ` Harnesses are tiered honestly: verified (${tierNames('verified')}), experimental\\n` +\n ` (${tierNames('experimental')} — schemas from the capability\\n` +\n ` matrix, not yet smoke-tested), advisory (${tierNames('advisory')} — instructions only,\\n` +\n ` nothing enforced). Same discipline as init: every file listed before a final\\n` +\n ` confirm, no existing file overwritten without a per-file yes, re-runs edit\\n` +\n ` toggles. ${CLI_NAME} hooks remove takes it back out. Guard settings live in\\n` +\n ` smelt.config.json (\"hooks\": {\"thresholdBytes\", \"enforcement\": \"deny\"|\"rewrite\"});\\n` +\n ` deny is the default — rewrite substitutes commands in-flight only where a\\n` +\n ` harness supports it, and never silently.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): HooksInvocation {\n const action = positionals[1];\n if (action !== 'install' && action !== 'remove') {\n throw new CliUsageError(\n `${CLI_NAME}: hooks needs an action — install or remove.\\n` +\n ` ${CLI_NAME} hooks install [--harness <id>]\\n` +\n ` ${CLI_NAME} hooks remove [--harness <id>]`,\n );\n }\n if (positionals.length > 2) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks ${action} takes no further arguments, got ` +\n `${positionals.slice(2).join(', ')}.`,\n );\n }\n return {\n mode: 'hooks',\n action,\n ...(values.harness === undefined ? {} : { harness: values.harness }),\n };\n },\n\n /** Nothing to merge: the wizard asks, and reads `smelt.config.json` itself. */\n resolve(invocation: HooksInvocation): HooksInvocation {\n return invocation;\n },\n\n /** Interactive like `init`, so it needs the same stream, and refuses without one. */\n async run(resolved: HooksInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks ${resolved.action} is interactive, and this invocation has ` +\n `no interactive input stream. Run \\`${CLI_NAME} hooks ${resolved.action}\\` ` +\n `from a terminal.`,\n );\n }\n return await runHooks(resolved.action, resolved.harness, {\n input: io.initInput,\n output: io.stdout,\n cwd: io.cwd ?? process.cwd(),\n });\n },\n};\n"]}
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../../src/cli/subcommands/hooks.ts"],"names":[],"mappings":"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","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { harnessesByTier, harnessNames } from '../../harness/registry.ts';\nimport type { HarnessTier } from '../../harness/profile.ts';\nimport { colorize } from '../lava.ts';\nimport { runHooks } from '../hooks.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt hooks install` / `smelt hooks remove` — the harness-hooks installer's front\n * door. The installer itself is `cli/hooks.ts`; this file is only the verb.\n *\n * Like `init`, it is interactive — the wizard asks everything except which harness —\n * so `--harness` is the one flag it owns, and the registry refuses the rest. The id\n * itself is validated in `cli/hooks.ts` against the harness registry in `src/harness/`,\n * which is also where the `--harness` help list comes from.\n */\n\n/** `smelt hooks <install|remove> [--harness <id>]` — parsed. */\nexport interface HooksInvocation {\n readonly mode: 'hooks';\n readonly action: 'install' | 'remove';\n readonly harness?: string;\n}\n\n/**\n * The harnesses at one tier, as the HOOKS paragraph spells them — from\n * `HarnessProfile.tier`, never a second list. A tier no profile claims renders empty\n * rather than naming a harness that moved.\n *\n * The line breaks around these are still hand-placed, and deliberately: this body is\n * byte-pinned by `test/__snapshots__/cli-usage.help.txt`, its paragraph is wrapped by\n * hand at no single width, and a generic wrapper would rewrite every line of the help\n * to derive three lists. The *membership* is what drifted — a promoted harness stayed\n * under its old tier — and membership is what this derives.\n */\nfunction tierNames(tier: HarnessTier): string {\n const group = harnessesByTier().find((candidate) => candidate.tier === tier);\n return group === undefined ? '' : harnessNames(group.harnesses);\n}\n\nexport const hooksCommand: Subcommand<HooksInvocation, HooksInvocation> = {\n name: 'hooks',\n flags: ['harness'],\n refusal: `hooks is interactive; the wizard asks the rest.`,\n usage: {\n synopsis: ['hooks install [--harness <id>]', 'hooks remove [--harness <id>]'],\n section: {\n heading: 'HOOKS',\n body:\n ` ${CLI_NAME} hooks install wires the smelt guard into agent-harness hooks: a\\n` +\n ` PreToolUse size-guard that refuses oversized raw reads with the exact ${CLI_NAME}\\n` +\n ` replacement command (default on), \\`${CLI_NAME} stats\\` at session end (default\\n` +\n ` on), and an opening \\`${CLI_NAME} map\\` at session start (opt-in) — plus an\\n` +\n ` instruction-file snippet that teaches \\`${CLI_NAME} retrieve\\` after a deny.\\n` +\n ` Harnesses are tiered honestly: verified (${tierNames('verified')}), experimental\\n` +\n ` (${tierNames('experimental')} — schemas from the capability\\n` +\n ` matrix, not yet smoke-tested), advisory (${tierNames('advisory')} — instructions only,\\n` +\n ` nothing enforced). Same discipline as init: every file listed before a final\\n` +\n ` confirm, no existing file overwritten without a per-file yes, re-runs edit\\n` +\n ` toggles. ${CLI_NAME} hooks remove takes it back out. Guard settings live in\\n` +\n ` smelt.config.json (\"hooks\": {\"thresholdBytes\", \"enforcement\": \"deny\"|\"rewrite\"});\\n` +\n ` deny is the default — rewrite substitutes commands in-flight only where a\\n` +\n ` harness supports it, and never silently.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): HooksInvocation {\n const action = positionals[1];\n if (action !== 'install' && action !== 'remove') {\n throw new CliUsageError(\n `${CLI_NAME}: hooks needs an action — install or remove.\\n` +\n ` ${CLI_NAME} hooks install [--harness <id>]\\n` +\n ` ${CLI_NAME} hooks remove [--harness <id>]`,\n );\n }\n if (positionals.length > 2) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks ${action} takes no further arguments, got ` +\n `${positionals.slice(2).join(', ')}.`,\n );\n }\n // `--harness` is repeatable for setup; this verb wires one action per run, and a\n // second id would be a second install the user believed had happened.\n if (values.harness !== undefined && values.harness.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks takes one --harness per run — repeat the command for ` +\n `each harness.`,\n );\n }\n return {\n mode: 'hooks',\n action,\n ...(values.harness === undefined ? {} : { harness: values.harness[0] }),\n };\n },\n\n /** Nothing to merge: the wizard asks, and reads `smelt.config.json` itself. */\n resolve(invocation: HooksInvocation): HooksInvocation {\n return invocation;\n },\n\n /** Interactive like `init`, so it needs the same stream, and refuses without one. */\n async run(resolved: HooksInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks ${resolved.action} is interactive, and this invocation has ` +\n `no interactive input stream. Run \\`${CLI_NAME} hooks ${resolved.action}\\` ` +\n `from a terminal.`,\n );\n }\n return await runHooks(resolved.action, resolved.harness, {\n input: io.initInput,\n output: (text) => io.stdout(colorize(text, io.color === true)),\n cwd: io.cwd ?? process.cwd(),\n version: io.version,\n });\n },\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/init.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;GASG;AAEH,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,WAAW,EAAE,UAAU,CAAC,cAAc,EAAE,cAAc,CA4ClE,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runInit } from '../init.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt init` — the setup wizard's front door. The wizard itself is `cli/init.ts`,\n * a pure function over an input/output pair; this file is only the verb.\n *\n * It owns no flags at all, and that is the whole design: the wizard asks one question\n * at a time and writes nothing until a final confirm, so a flag would be an answer\n * given twice. It also merges nothing — it reads `smelt.config.json` itself, with its\n * own tolerance, because a wizard you run to *fix* a malformed config must not refuse\n * to start because the config is malformed.\n */\n\n/** `smelt init` — no flags, no arguments, nothing but the word. */\nexport interface InitInvocation {\n readonly mode: 'init';\n readonly focus: readonly string[];\n readonly json: boolean;\n}\n\nexport const initCommand: Subcommand<InitInvocation, InitInvocation> = {\n name: 'init',\n flags: [],\n refusal: `init is interactive: it asks instead.`,\n usage: {\n synopsis: [],\n occasional: ['init'],\n section: {\n heading: 'CONFIG',\n body:\n ` ${CLI_NAME} init walks you through writing a smelt.config.json (and optional typed\\n` +\n ` stubs), one question at a time; nothing is written until a final confirm. Runs read\\n` +\n ` the nearest smelt.config.json, walking up from the working directory, for DEFAULTS\\n` +\n ` only — budget, strategy, store. An explicit flag always wins, and a malformed config\\n` +\n ` is a usage error, never silently ignored.`,\n },\n },\n\n parse(_values: FlagValues, positionals: readonly string[]): InitInvocation {\n // `smelt init` is a subcommand, so a file literally named `init` needs `./init`.\n if (positionals.length > 1) {\n throw new CliUsageError(`${CLI_NAME}: init takes no further arguments.`);\n }\n return { mode: 'init', focus: [], json: false };\n },\n\n /** Nothing to merge: every answer is asked for, and the config is read by the wizard. */\n resolve(invocation: InitInvocation): InitInvocation {\n return invocation;\n },\n\n async run(_resolved: InitInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: init is interactive, and this invocation has no interactive ` +\n `input stream. Run \\`${CLI_NAME} init\\` from a terminal.`,\n );\n }\n return await runInit({\n input: io.initInput,\n output: io.stdout,\n cwd: io.cwd ?? process.cwd(),\n });\n },\n};\n"]}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/init.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;GASG;AAEH,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,WAAW,EAAE,UAAU,CAAC,cAAc,EAAE,cAAc,CA4ClE,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runInit } from '../init.ts';\nimport { colorize } from '../lava.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt init` — the setup wizard's front door. The wizard itself is `cli/init.ts`,\n * a pure function over an input/output pair; this file is only the verb.\n *\n * It owns no flags at all, and that is the whole design: the wizard asks one question\n * at a time and writes nothing until a final confirm, so a flag would be an answer\n * given twice. It also merges nothing — it reads `smelt.config.json` itself, with its\n * own tolerance, because a wizard you run to *fix* a malformed config must not refuse\n * to start because the config is malformed.\n */\n\n/** `smelt init` — no flags, no arguments, nothing but the word. */\nexport interface InitInvocation {\n readonly mode: 'init';\n readonly focus: readonly string[];\n readonly json: boolean;\n}\n\nexport const initCommand: Subcommand<InitInvocation, InitInvocation> = {\n name: 'init',\n flags: [],\n refusal: `init is interactive: it asks instead.`,\n usage: {\n synopsis: [],\n occasional: ['init'],\n section: {\n heading: 'CONFIG',\n body:\n ` ${CLI_NAME} init walks you through writing a smelt.config.json (and optional typed\\n` +\n ` stubs), one question at a time; nothing is written until a final confirm. Runs read\\n` +\n ` the nearest smelt.config.json, walking up from the working directory, for DEFAULTS\\n` +\n ` only — budget, strategy, store. An explicit flag always wins, and a malformed config\\n` +\n ` is a usage error, never silently ignored.`,\n },\n },\n\n parse(_values: FlagValues, positionals: readonly string[]): InitInvocation {\n // `smelt init` is a subcommand, so a file literally named `init` needs `./init`.\n if (positionals.length > 1) {\n throw new CliUsageError(`${CLI_NAME}: init takes no further arguments.`);\n }\n return { mode: 'init', focus: [], json: false };\n },\n\n /** Nothing to merge: every answer is asked for, and the config is read by the wizard. */\n resolve(invocation: InitInvocation): InitInvocation {\n return invocation;\n },\n\n async run(_resolved: InitInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: init is interactive, and this invocation has no interactive ` +\n `input stream. Run \\`${CLI_NAME} init\\` from a terminal.`,\n );\n }\n return await runInit({\n input: io.initInput,\n output: (text) => io.stdout(colorize(text, io.color === true)),\n cwd: io.cwd ?? process.cwd(),\n });\n },\n};\n"]}
@@ -1,6 +1,7 @@
1
1
  import process from 'node:process';
2
2
  import { CliUsageError } from '../../errors.js';
3
3
  import { runInit } from '../init.js';
4
+ import { colorize } from '../lava.js';
4
5
  import { CLI_NAME } from '../shell.js';
5
6
  export const initCommand = {
6
7
  name: 'init',
@@ -36,7 +37,7 @@ export const initCommand = {
36
37
  }
37
38
  return await runInit({
38
39
  input: io.initInput,
39
- output: io.stdout,
40
+ output: (text) => io.stdout(colorize(text, io.color === true)),
40
41
  cwd: io.cwd ?? process.cwd(),
41
42
  });
42
43
  },
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/cli/subcommands/init.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAwBvC,MAAM,CAAC,MAAM,WAAW,GAA+C;IACrE,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,EAAE;IACT,OAAO,EAAE,uCAAuC;IAChD,KAAK,EAAE;QACL,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,CAAC,MAAM,CAAC;QACpB,OAAO,EAAE;YACP,OAAO,EAAE,QAAQ;YACjB,IAAI,EACF,KAAK,QAAQ,2EAA2E;gBACxF,yFAAyF;gBACzF,wFAAwF;gBACxF,0FAA0F;gBAC1F,6CAA6C;SAChD;KACF;IAED,KAAK,CAAC,OAAmB,EAAE,WAA8B;QACvD,iFAAiF;QACjF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,aAAa,CAAC,GAAG,QAAQ,oCAAoC,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAClD,CAAC;IAED,yFAAyF;IACzF,OAAO,CAAC,UAA0B;QAChC,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,SAAyB,EAAE,EAAS;QAC5C,IAAI,EAAE,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,gEAAgE;gBACzE,uBAAuB,QAAQ,0BAA0B,CAC5D,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,OAAO,CAAC;YACnB,KAAK,EAAE,EAAE,CAAC,SAAS;YACnB,MAAM,EAAE,EAAE,CAAC,MAAM;YACjB,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;CACF,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runInit } from '../init.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt init` — the setup wizard's front door. The wizard itself is `cli/init.ts`,\n * a pure function over an input/output pair; this file is only the verb.\n *\n * It owns no flags at all, and that is the whole design: the wizard asks one question\n * at a time and writes nothing until a final confirm, so a flag would be an answer\n * given twice. It also merges nothing — it reads `smelt.config.json` itself, with its\n * own tolerance, because a wizard you run to *fix* a malformed config must not refuse\n * to start because the config is malformed.\n */\n\n/** `smelt init` — no flags, no arguments, nothing but the word. */\nexport interface InitInvocation {\n readonly mode: 'init';\n readonly focus: readonly string[];\n readonly json: boolean;\n}\n\nexport const initCommand: Subcommand<InitInvocation, InitInvocation> = {\n name: 'init',\n flags: [],\n refusal: `init is interactive: it asks instead.`,\n usage: {\n synopsis: [],\n occasional: ['init'],\n section: {\n heading: 'CONFIG',\n body:\n ` ${CLI_NAME} init walks you through writing a smelt.config.json (and optional typed\\n` +\n ` stubs), one question at a time; nothing is written until a final confirm. Runs read\\n` +\n ` the nearest smelt.config.json, walking up from the working directory, for DEFAULTS\\n` +\n ` only — budget, strategy, store. An explicit flag always wins, and a malformed config\\n` +\n ` is a usage error, never silently ignored.`,\n },\n },\n\n parse(_values: FlagValues, positionals: readonly string[]): InitInvocation {\n // `smelt init` is a subcommand, so a file literally named `init` needs `./init`.\n if (positionals.length > 1) {\n throw new CliUsageError(`${CLI_NAME}: init takes no further arguments.`);\n }\n return { mode: 'init', focus: [], json: false };\n },\n\n /** Nothing to merge: every answer is asked for, and the config is read by the wizard. */\n resolve(invocation: InitInvocation): InitInvocation {\n return invocation;\n },\n\n async run(_resolved: InitInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: init is interactive, and this invocation has no interactive ` +\n `input stream. Run \\`${CLI_NAME} init\\` from a terminal.`,\n );\n }\n return await runInit({\n input: io.initInput,\n output: io.stdout,\n cwd: io.cwd ?? process.cwd(),\n });\n },\n};\n"]}
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/cli/subcommands/init.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAwBvC,MAAM,CAAC,MAAM,WAAW,GAA+C;IACrE,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,EAAE;IACT,OAAO,EAAE,uCAAuC;IAChD,KAAK,EAAE;QACL,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,CAAC,MAAM,CAAC;QACpB,OAAO,EAAE;YACP,OAAO,EAAE,QAAQ;YACjB,IAAI,EACF,KAAK,QAAQ,2EAA2E;gBACxF,yFAAyF;gBACzF,wFAAwF;gBACxF,0FAA0F;gBAC1F,6CAA6C;SAChD;KACF;IAED,KAAK,CAAC,OAAmB,EAAE,WAA8B;QACvD,iFAAiF;QACjF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,aAAa,CAAC,GAAG,QAAQ,oCAAoC,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAClD,CAAC;IAED,yFAAyF;IACzF,OAAO,CAAC,UAA0B;QAChC,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,SAAyB,EAAE,EAAS;QAC5C,IAAI,EAAE,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,gEAAgE;gBACzE,uBAAuB,QAAQ,0BAA0B,CAC5D,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,OAAO,CAAC;YACnB,KAAK,EAAE,EAAE,CAAC,SAAS;YACnB,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC;YAC9D,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;CACF,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runInit } from '../init.ts';\nimport { colorize } from '../lava.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt init` — the setup wizard's front door. The wizard itself is `cli/init.ts`,\n * a pure function over an input/output pair; this file is only the verb.\n *\n * It owns no flags at all, and that is the whole design: the wizard asks one question\n * at a time and writes nothing until a final confirm, so a flag would be an answer\n * given twice. It also merges nothing — it reads `smelt.config.json` itself, with its\n * own tolerance, because a wizard you run to *fix* a malformed config must not refuse\n * to start because the config is malformed.\n */\n\n/** `smelt init` — no flags, no arguments, nothing but the word. */\nexport interface InitInvocation {\n readonly mode: 'init';\n readonly focus: readonly string[];\n readonly json: boolean;\n}\n\nexport const initCommand: Subcommand<InitInvocation, InitInvocation> = {\n name: 'init',\n flags: [],\n refusal: `init is interactive: it asks instead.`,\n usage: {\n synopsis: [],\n occasional: ['init'],\n section: {\n heading: 'CONFIG',\n body:\n ` ${CLI_NAME} init walks you through writing a smelt.config.json (and optional typed\\n` +\n ` stubs), one question at a time; nothing is written until a final confirm. Runs read\\n` +\n ` the nearest smelt.config.json, walking up from the working directory, for DEFAULTS\\n` +\n ` only — budget, strategy, store. An explicit flag always wins, and a malformed config\\n` +\n ` is a usage error, never silently ignored.`,\n },\n },\n\n parse(_values: FlagValues, positionals: readonly string[]): InitInvocation {\n // `smelt init` is a subcommand, so a file literally named `init` needs `./init`.\n if (positionals.length > 1) {\n throw new CliUsageError(`${CLI_NAME}: init takes no further arguments.`);\n }\n return { mode: 'init', focus: [], json: false };\n },\n\n /** Nothing to merge: every answer is asked for, and the config is read by the wizard. */\n resolve(invocation: InitInvocation): InitInvocation {\n return invocation;\n },\n\n async run(_resolved: InitInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: init is interactive, and this invocation has no interactive ` +\n `input stream. Run \\`${CLI_NAME} init\\` from a terminal.`,\n );\n }\n return await runInit({\n input: io.initInput,\n output: (text) => io.stdout(colorize(text, io.color === true)),\n cwd: io.cwd ?? process.cwd(),\n });\n },\n};\n"]}
@@ -1,10 +1,12 @@
1
1
  import type { AgentsInvocation } from './agents.ts';
2
+ import type { DoctorInvocation } from './doctor.ts';
2
3
  import type { FlagName, FlagValues } from './flags.ts';
3
4
  import type { HooksInvocation } from './hooks.ts';
4
5
  import type { InitInvocation } from './init.ts';
5
6
  import type { MapInvocation } from './map.ts';
6
7
  import type { RetrieveInvocation } from './retrieve.ts';
7
8
  import type { SmeltInvocation } from './smelt.ts';
9
+ import type { SetupInvocation } from './setup.ts';
8
10
  import type { StatsInvocation } from './stats.ts';
9
11
  import type { Subcommand, Verb } from './subcommand.ts';
10
12
  /**
@@ -22,7 +24,7 @@ import type { Subcommand, Verb } from './subcommand.ts';
22
24
  */
23
25
  export declare const SUBCOMMANDS: Readonly<Record<Verb, AnySubcommand>>;
24
26
  /** Everything `parseSmeltArgs` can return. Narrow on `mode`. */
25
- export type CliInvocation = SmeltInvocation | InitInvocation | MapInvocation | RetrieveInvocation | StatsInvocation | HooksInvocation | AgentsInvocation;
27
+ export type CliInvocation = SmeltInvocation | InitInvocation | MapInvocation | RetrieveInvocation | StatsInvocation | HooksInvocation | AgentsInvocation | SetupInvocation | DoctorInvocation;
26
28
  /**
27
29
  * One registry entry, with its verb's own invocation and resolved types erased.
28
30
  *
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/registry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AAEjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAExD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,CAQ7D,CAAC;AAEF,gEAAgE;AAChE,MAAM,MAAM,aAAa,GACrB,eAAe,GACf,cAAc,GACd,aAAa,GACb,kBAAkB,GAClB,eAAe,GACf,eAAe,GACf,gBAAgB,CAAC;AAErB;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;AAE/D,6EAA6E;AAC7E,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAA+B,CAAC;AAEpF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,IAAI,EAEtC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,aAAa,CAI3E;AAmBD,6DAA6D;AAC7D,wBAAgB,UAAU,CACxB,IAAI,EAAE,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,GACvD,aAAa,CAEf;AAYD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,aAAa,EAAE,CAEjE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAYnF","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { CLI_NAME } from '../shell.ts';\n\nimport { agentsCommand } from './agents.ts';\nimport type { AgentsInvocation } from './agents.ts';\nimport { flagList, VERB_FLAGS } from './flags.ts';\nimport type { FlagName, FlagValues, VerbFlag } from './flags.ts';\nimport { hooksCommand } from './hooks.ts';\nimport type { HooksInvocation } from './hooks.ts';\nimport { initCommand } from './init.ts';\nimport type { InitInvocation } from './init.ts';\nimport { mapCommand } from './map.ts';\nimport type { MapInvocation } from './map.ts';\nimport { retrieveCommand } from './retrieve.ts';\nimport type { RetrieveInvocation } from './retrieve.ts';\nimport { smeltCommand } from './smelt.ts';\nimport type { SmeltInvocation } from './smelt.ts';\nimport { statsCommand } from './stats.ts';\nimport type { StatsInvocation } from './stats.ts';\nimport { DEFAULT_VERB } from './subcommand.ts';\nimport type { Subcommand, Verb } from './subcommand.ts';\n\n/**\n * The registry — every verb `smelt` answers to, one {@link Subcommand} each.\n *\n * `Record<Verb, Subcommand>` on purpose, exactly like `LANGUAGE_PROFILES` and\n * `HARNESS_PROFILES`: adding a `Verb` in `subcommand.ts` without writing its file is a\n * compile error, so the verb list and the facts cannot drift. Every derived view — the\n * USAGE block, the help's sections, the `map only.` prefixes in OPTIONS, and above all\n * the flag-ownership refusal below — is computed from this object, never written twice.\n *\n * Key order is meaningful: it is the order every rendered list uses (USAGE, the help\n * sections, the owners named in a refusal), so keep it stable and append new verbs at\n * the end.\n */\nexport const SUBCOMMANDS: Readonly<Record<Verb, AnySubcommand>> = {\n smelt: smeltCommand,\n init: initCommand,\n map: mapCommand,\n retrieve: retrieveCommand,\n stats: statsCommand,\n hooks: hooksCommand,\n agents: agentsCommand,\n};\n\n/** Everything `parseSmeltArgs` can return. Narrow on `mode`. */\nexport type CliInvocation =\n | SmeltInvocation\n | InitInvocation\n | MapInvocation\n | RetrieveInvocation\n | StatsInvocation\n | HooksInvocation\n | AgentsInvocation;\n\n/**\n * One registry entry, with its verb's own invocation and resolved types erased.\n *\n * The erasure is what lets six differently-typed commands live in one `Record`, and it\n * is safe because the two ends are never crossed: `runCli` hands a command exactly the\n * invocation that command's own `parse` produced (`verbFor` maps each `mode` back to\n * the verb that minted it), and the resolved value never leaves the pair of calls that\n * makes and consumes it.\n */\nexport type AnySubcommand = Subcommand<CliInvocation, unknown>;\n\n/** Every command, in registry order. The list every rendered block walks. */\nexport const SUBCOMMAND_LIST: readonly AnySubcommand[] = Object.values(SUBCOMMANDS);\n\n/**\n * The verbs with a word on the command line — everything but the default one.\n *\n * The default verb is excluded deliberately: `smelt smelt` is a file named `smelt`,\n * not a recursive invocation, the same way `smelt map` as a *file* needs `./map`.\n */\nexport const NAMED_VERBS: readonly Verb[] = SUBCOMMAND_LIST.map((command) => command.name).filter(\n (name) => name !== DEFAULT_VERB,\n);\n\n/**\n * Which command a command line selects: `positionals[0]` when it names a verb, the\n * default verb otherwise. The whole of subcommand dispatch, in one lookup.\n */\nexport function subcommandFor(positionals: readonly string[]): AnySubcommand {\n const first = positionals[0];\n const named = NAMED_VERBS.find((verb) => verb === first);\n return SUBCOMMANDS[named ?? DEFAULT_VERB];\n}\n\n/**\n * Which verb minted an invocation. Total over the modes `parseSmeltArgs` can return\n * for a verb, so a new mode without a home is a compile error — `'reconstruct'` maps\n * to the default verb because `--reconstruct` is that verb's second job, not a\n * seventh command.\n */\nconst VERB_BY_MODE = {\n smelt: 'smelt',\n reconstruct: 'smelt',\n init: 'init',\n map: 'map',\n retrieve: 'retrieve',\n stats: 'stats',\n hooks: 'hooks',\n agents: 'agents',\n} as const satisfies Record<Exclude<CliInvocation['mode'], 'help' | 'version'>, Verb>;\n\n/** The command that produced an invocation, for dispatch. */\nexport function commandFor(\n mode: Exclude<CliInvocation['mode'], 'help' | 'version'>,\n): AnySubcommand {\n return SUBCOMMANDS[VERB_BY_MODE[mode]];\n}\n\n/** How a command is named in prose. The default verb has no word to name it by. */\nfunction label(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : command.name;\n}\n\n/** How a command is named as the *owner* of a flag: the form you would type. */\nfunction ownerLabel(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : `\\`${CLI_NAME} ${command.name}\\``;\n}\n\n/**\n * The commands that own a flag, in registry order — empty for the two global flags,\n * which no verb owns. A flag with exactly one owner has an unambiguous home worth\n * naming in a refusal (and worth prefixing its OPTIONS entry with); a flag several\n * verbs share (`--json`, `--budget`, `--focus`) has none, and pointing at one of them\n * would be a guess.\n */\nexport function ownersOf(flag: FlagName): readonly AnySubcommand[] {\n return SUBCOMMAND_LIST.filter((command) => (command.flags as readonly string[]).includes(flag));\n}\n\n/**\n * Refuse every flag the chosen verb does not own — the one message that replaced five\n * hand-written ones.\n *\n * Before this existed, each verb refused each other verb's flags in prose, so the\n * refusals were O(verbs × flags) sentences kept in sync by hand and an eleventh flag\n * edited five of them. Now ownership is declared once per verb and the message is\n * generated from three derived facts, in the order a reader needs them:\n *\n * 1. **what this verb takes**, and what it got instead — the offending flag, named;\n * 2. **where the flag does belong**, when exactly one verb owns it, listed as that\n * owner's *exclusively* owned flags (so `--ignore` here still reads \"--ignore and\n * --cache belong to `smelt map`\", exactly as the hand-written message did);\n * 3. **why not here** — the verb's own `refusal` sentence, the half of the old\n * messages worth keeping, and the only half a verb still writes.\n *\n * Every refusal stays a {@link CliUsageError}, so every one of them still exits 2.\n *\n * @throws {CliUsageError} naming the offending flag, this verb, and the flag's owner.\n */\nexport function refuseForeignFlags(command: AnySubcommand, values: FlagValues): void {\n const foreign = VERB_FLAGS.filter(\n (flag) => values[flag] !== undefined && !command.flags.includes(flag),\n );\n if (foreign.length === 0) return;\n\n const takes =\n command.flags.length === 0\n ? `${label(command)} takes no flags (got ${flagList(foreign)}).`\n : `${label(command)} takes only ${flagList(command.flags)} (got ${flagList(foreign)}).`;\n\n throw new CliUsageError(`${CLI_NAME}: ${takes}${redirects(foreign)} ${command.refusal}`);\n}\n\n/**\n * ` --ignore and --cache belong to \\`smelt map\\`.` — one clause per single-owner verb.\n *\n * A clause names only the flags its owner owns **alone**, never the ones it shares.\n * `ownersOf(flag).length === 1` is the same test that decided the owner deserved a\n * clause at all, applied to the whole clause: a shared flag (`--budget`, `--focus`,\n * `--json`) has no single home, so naming it here would assert an ownership that the\n * OPTIONS block — which prefixes `map only.` by the same rule — correctly denies.\n */\nfunction redirects(foreign: readonly VerbFlag[]): string {\n const owners = SUBCOMMAND_LIST.filter((owner) =>\n foreign.some((flag) => {\n const claimants = ownersOf(flag);\n return claimants.length === 1 && claimants[0] === owner;\n }),\n );\n return owners\n .map((owner) => {\n const elsewhere = owner.flags.filter((flag) => ownersOf(flag).length === 1);\n const verb = elsewhere.length === 1 ? 'belongs' : 'belong';\n return ` ${flagList(elsewhere)} ${verb} to ${ownerLabel(owner)}.`;\n })\n .join('');\n}\n"]}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/registry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AAEjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAExD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,CAU7D,CAAC;AAEF,gEAAgE;AAChE,MAAM,MAAM,aAAa,GACrB,eAAe,GACf,cAAc,GACd,aAAa,GACb,kBAAkB,GAClB,eAAe,GACf,eAAe,GACf,gBAAgB,GAChB,eAAe,GACf,gBAAgB,CAAC;AAErB;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;AAE/D,6EAA6E;AAC7E,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAA+B,CAAC;AAEpF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,IAAI,EAEtC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,aAAa,CAI3E;AAqBD,6DAA6D;AAC7D,wBAAgB,UAAU,CACxB,IAAI,EAAE,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,GACvD,aAAa,CAEf;AAYD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,aAAa,EAAE,CAEjE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAYnF","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { CLI_NAME } from '../shell.ts';\n\nimport { agentsCommand } from './agents.ts';\nimport type { AgentsInvocation } from './agents.ts';\nimport { doctorCommand } from './doctor.ts';\nimport type { DoctorInvocation } from './doctor.ts';\nimport { flagList, VERB_FLAGS } from './flags.ts';\nimport type { FlagName, FlagValues, VerbFlag } from './flags.ts';\nimport { hooksCommand } from './hooks.ts';\nimport type { HooksInvocation } from './hooks.ts';\nimport { initCommand } from './init.ts';\nimport type { InitInvocation } from './init.ts';\nimport { mapCommand } from './map.ts';\nimport type { MapInvocation } from './map.ts';\nimport { retrieveCommand } from './retrieve.ts';\nimport type { RetrieveInvocation } from './retrieve.ts';\nimport { smeltCommand } from './smelt.ts';\nimport type { SmeltInvocation } from './smelt.ts';\nimport { setupCommand } from './setup.ts';\nimport type { SetupInvocation } from './setup.ts';\nimport { statsCommand } from './stats.ts';\nimport type { StatsInvocation } from './stats.ts';\nimport { DEFAULT_VERB } from './subcommand.ts';\nimport type { Subcommand, Verb } from './subcommand.ts';\n\n/**\n * The registry — every verb `smelt` answers to, one {@link Subcommand} each.\n *\n * `Record<Verb, Subcommand>` on purpose, exactly like `LANGUAGE_PROFILES` and\n * `HARNESS_PROFILES`: adding a `Verb` in `subcommand.ts` without writing its file is a\n * compile error, so the verb list and the facts cannot drift. Every derived view — the\n * USAGE block, the help's sections, the `map only.` prefixes in OPTIONS, and above all\n * the flag-ownership refusal below — is computed from this object, never written twice.\n *\n * Key order is meaningful: it is the order every rendered list uses (USAGE, the help\n * sections, the owners named in a refusal), so keep it stable and append new verbs at\n * the end.\n */\nexport const SUBCOMMANDS: Readonly<Record<Verb, AnySubcommand>> = {\n smelt: smeltCommand,\n init: initCommand,\n map: mapCommand,\n retrieve: retrieveCommand,\n stats: statsCommand,\n hooks: hooksCommand,\n agents: agentsCommand,\n setup: setupCommand,\n doctor: doctorCommand,\n};\n\n/** Everything `parseSmeltArgs` can return. Narrow on `mode`. */\nexport type CliInvocation =\n | SmeltInvocation\n | InitInvocation\n | MapInvocation\n | RetrieveInvocation\n | StatsInvocation\n | HooksInvocation\n | AgentsInvocation\n | SetupInvocation\n | DoctorInvocation;\n\n/**\n * One registry entry, with its verb's own invocation and resolved types erased.\n *\n * The erasure is what lets six differently-typed commands live in one `Record`, and it\n * is safe because the two ends are never crossed: `runCli` hands a command exactly the\n * invocation that command's own `parse` produced (`verbFor` maps each `mode` back to\n * the verb that minted it), and the resolved value never leaves the pair of calls that\n * makes and consumes it.\n */\nexport type AnySubcommand = Subcommand<CliInvocation, unknown>;\n\n/** Every command, in registry order. The list every rendered block walks. */\nexport const SUBCOMMAND_LIST: readonly AnySubcommand[] = Object.values(SUBCOMMANDS);\n\n/**\n * The verbs with a word on the command line — everything but the default one.\n *\n * The default verb is excluded deliberately: `smelt smelt` is a file named `smelt`,\n * not a recursive invocation, the same way `smelt map` as a *file* needs `./map`.\n */\nexport const NAMED_VERBS: readonly Verb[] = SUBCOMMAND_LIST.map((command) => command.name).filter(\n (name) => name !== DEFAULT_VERB,\n);\n\n/**\n * Which command a command line selects: `positionals[0]` when it names a verb, the\n * default verb otherwise. The whole of subcommand dispatch, in one lookup.\n */\nexport function subcommandFor(positionals: readonly string[]): AnySubcommand {\n const first = positionals[0];\n const named = NAMED_VERBS.find((verb) => verb === first);\n return SUBCOMMANDS[named ?? DEFAULT_VERB];\n}\n\n/**\n * Which verb minted an invocation. Total over the modes `parseSmeltArgs` can return\n * for a verb, so a new mode without a home is a compile error — `'reconstruct'` maps\n * to the default verb because `--reconstruct` is that verb's second job, not a\n * seventh command.\n */\nconst VERB_BY_MODE = {\n smelt: 'smelt',\n reconstruct: 'smelt',\n init: 'init',\n map: 'map',\n retrieve: 'retrieve',\n stats: 'stats',\n hooks: 'hooks',\n agents: 'agents',\n setup: 'setup',\n doctor: 'doctor',\n} as const satisfies Record<Exclude<CliInvocation['mode'], 'help' | 'version'>, Verb>;\n\n/** The command that produced an invocation, for dispatch. */\nexport function commandFor(\n mode: Exclude<CliInvocation['mode'], 'help' | 'version'>,\n): AnySubcommand {\n return SUBCOMMANDS[VERB_BY_MODE[mode]];\n}\n\n/** How a command is named in prose. The default verb has no word to name it by. */\nfunction label(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : command.name;\n}\n\n/** How a command is named as the *owner* of a flag: the form you would type. */\nfunction ownerLabel(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : `\\`${CLI_NAME} ${command.name}\\``;\n}\n\n/**\n * The commands that own a flag, in registry order — empty for the two global flags,\n * which no verb owns. A flag with exactly one owner has an unambiguous home worth\n * naming in a refusal (and worth prefixing its OPTIONS entry with); a flag several\n * verbs share (`--json`, `--budget`, `--focus`) has none, and pointing at one of them\n * would be a guess.\n */\nexport function ownersOf(flag: FlagName): readonly AnySubcommand[] {\n return SUBCOMMAND_LIST.filter((command) => (command.flags as readonly string[]).includes(flag));\n}\n\n/**\n * Refuse every flag the chosen verb does not own — the one message that replaced five\n * hand-written ones.\n *\n * Before this existed, each verb refused each other verb's flags in prose, so the\n * refusals were O(verbs × flags) sentences kept in sync by hand and an eleventh flag\n * edited five of them. Now ownership is declared once per verb and the message is\n * generated from three derived facts, in the order a reader needs them:\n *\n * 1. **what this verb takes**, and what it got instead — the offending flag, named;\n * 2. **where the flag does belong**, when exactly one verb owns it, listed as that\n * owner's *exclusively* owned flags (so `--ignore` here still reads \"--ignore and\n * --cache belong to `smelt map`\", exactly as the hand-written message did);\n * 3. **why not here** — the verb's own `refusal` sentence, the half of the old\n * messages worth keeping, and the only half a verb still writes.\n *\n * Every refusal stays a {@link CliUsageError}, so every one of them still exits 2.\n *\n * @throws {CliUsageError} naming the offending flag, this verb, and the flag's owner.\n */\nexport function refuseForeignFlags(command: AnySubcommand, values: FlagValues): void {\n const foreign = VERB_FLAGS.filter(\n (flag) => values[flag] !== undefined && !command.flags.includes(flag),\n );\n if (foreign.length === 0) return;\n\n const takes =\n command.flags.length === 0\n ? `${label(command)} takes no flags (got ${flagList(foreign)}).`\n : `${label(command)} takes only ${flagList(command.flags)} (got ${flagList(foreign)}).`;\n\n throw new CliUsageError(`${CLI_NAME}: ${takes}${redirects(foreign)} ${command.refusal}`);\n}\n\n/**\n * ` --ignore and --cache belong to \\`smelt map\\`.` — one clause per single-owner verb.\n *\n * A clause names only the flags its owner owns **alone**, never the ones it shares.\n * `ownersOf(flag).length === 1` is the same test that decided the owner deserved a\n * clause at all, applied to the whole clause: a shared flag (`--budget`, `--focus`,\n * `--json`) has no single home, so naming it here would assert an ownership that the\n * OPTIONS block — which prefixes `map only.` by the same rule — correctly denies.\n */\nfunction redirects(foreign: readonly VerbFlag[]): string {\n const owners = SUBCOMMAND_LIST.filter((owner) =>\n foreign.some((flag) => {\n const claimants = ownersOf(flag);\n return claimants.length === 1 && claimants[0] === owner;\n }),\n );\n return owners\n .map((owner) => {\n const elsewhere = owner.flags.filter((flag) => ownersOf(flag).length === 1);\n const verb = elsewhere.length === 1 ? 'belongs' : 'belong';\n return ` ${flagList(elsewhere)} ${verb} to ${ownerLabel(owner)}.`;\n })\n .join('');\n}\n"]}
@@ -1,12 +1,14 @@
1
1
  import { CliUsageError } from '../../errors.js';
2
2
  import { CLI_NAME } from '../shell.js';
3
3
  import { agentsCommand } from './agents.js';
4
+ import { doctorCommand } from './doctor.js';
4
5
  import { flagList, VERB_FLAGS } from './flags.js';
5
6
  import { hooksCommand } from './hooks.js';
6
7
  import { initCommand } from './init.js';
7
8
  import { mapCommand } from './map.js';
8
9
  import { retrieveCommand } from './retrieve.js';
9
10
  import { smeltCommand } from './smelt.js';
11
+ import { setupCommand } from './setup.js';
10
12
  import { statsCommand } from './stats.js';
11
13
  import { DEFAULT_VERB } from './subcommand.js';
12
14
  /**
@@ -30,6 +32,8 @@ export const SUBCOMMANDS = {
30
32
  stats: statsCommand,
31
33
  hooks: hooksCommand,
32
34
  agents: agentsCommand,
35
+ setup: setupCommand,
36
+ doctor: doctorCommand,
33
37
  };
34
38
  /** Every command, in registry order. The list every rendered block walks. */
35
39
  export const SUBCOMMAND_LIST = Object.values(SUBCOMMANDS);
@@ -64,6 +68,8 @@ const VERB_BY_MODE = {
64
68
  stats: 'stats',
65
69
  hooks: 'hooks',
66
70
  agents: 'agents',
71
+ setup: 'setup',
72
+ doctor: 'doctor',
67
73
  };
68
74
  /** The command that produced an invocation, for dispatch. */
69
75
  export function commandFor(mode) {
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/cli/subcommands/registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { CLI_NAME } from '../shell.ts';\n\nimport { agentsCommand } from './agents.ts';\nimport type { AgentsInvocation } from './agents.ts';\nimport { flagList, VERB_FLAGS } from './flags.ts';\nimport type { FlagName, FlagValues, VerbFlag } from './flags.ts';\nimport { hooksCommand } from './hooks.ts';\nimport type { HooksInvocation } from './hooks.ts';\nimport { initCommand } from './init.ts';\nimport type { InitInvocation } from './init.ts';\nimport { mapCommand } from './map.ts';\nimport type { MapInvocation } from './map.ts';\nimport { retrieveCommand } from './retrieve.ts';\nimport type { RetrieveInvocation } from './retrieve.ts';\nimport { smeltCommand } from './smelt.ts';\nimport type { SmeltInvocation } from './smelt.ts';\nimport { statsCommand } from './stats.ts';\nimport type { StatsInvocation } from './stats.ts';\nimport { DEFAULT_VERB } from './subcommand.ts';\nimport type { Subcommand, Verb } from './subcommand.ts';\n\n/**\n * The registry — every verb `smelt` answers to, one {@link Subcommand} each.\n *\n * `Record<Verb, Subcommand>` on purpose, exactly like `LANGUAGE_PROFILES` and\n * `HARNESS_PROFILES`: adding a `Verb` in `subcommand.ts` without writing its file is a\n * compile error, so the verb list and the facts cannot drift. Every derived view — the\n * USAGE block, the help's sections, the `map only.` prefixes in OPTIONS, and above all\n * the flag-ownership refusal below — is computed from this object, never written twice.\n *\n * Key order is meaningful: it is the order every rendered list uses (USAGE, the help\n * sections, the owners named in a refusal), so keep it stable and append new verbs at\n * the end.\n */\nexport const SUBCOMMANDS: Readonly<Record<Verb, AnySubcommand>> = {\n smelt: smeltCommand,\n init: initCommand,\n map: mapCommand,\n retrieve: retrieveCommand,\n stats: statsCommand,\n hooks: hooksCommand,\n agents: agentsCommand,\n};\n\n/** Everything `parseSmeltArgs` can return. Narrow on `mode`. */\nexport type CliInvocation =\n | SmeltInvocation\n | InitInvocation\n | MapInvocation\n | RetrieveInvocation\n | StatsInvocation\n | HooksInvocation\n | AgentsInvocation;\n\n/**\n * One registry entry, with its verb's own invocation and resolved types erased.\n *\n * The erasure is what lets six differently-typed commands live in one `Record`, and it\n * is safe because the two ends are never crossed: `runCli` hands a command exactly the\n * invocation that command's own `parse` produced (`verbFor` maps each `mode` back to\n * the verb that minted it), and the resolved value never leaves the pair of calls that\n * makes and consumes it.\n */\nexport type AnySubcommand = Subcommand<CliInvocation, unknown>;\n\n/** Every command, in registry order. The list every rendered block walks. */\nexport const SUBCOMMAND_LIST: readonly AnySubcommand[] = Object.values(SUBCOMMANDS);\n\n/**\n * The verbs with a word on the command line — everything but the default one.\n *\n * The default verb is excluded deliberately: `smelt smelt` is a file named `smelt`,\n * not a recursive invocation, the same way `smelt map` as a *file* needs `./map`.\n */\nexport const NAMED_VERBS: readonly Verb[] = SUBCOMMAND_LIST.map((command) => command.name).filter(\n (name) => name !== DEFAULT_VERB,\n);\n\n/**\n * Which command a command line selects: `positionals[0]` when it names a verb, the\n * default verb otherwise. The whole of subcommand dispatch, in one lookup.\n */\nexport function subcommandFor(positionals: readonly string[]): AnySubcommand {\n const first = positionals[0];\n const named = NAMED_VERBS.find((verb) => verb === first);\n return SUBCOMMANDS[named ?? DEFAULT_VERB];\n}\n\n/**\n * Which verb minted an invocation. Total over the modes `parseSmeltArgs` can return\n * for a verb, so a new mode without a home is a compile error — `'reconstruct'` maps\n * to the default verb because `--reconstruct` is that verb's second job, not a\n * seventh command.\n */\nconst VERB_BY_MODE = {\n smelt: 'smelt',\n reconstruct: 'smelt',\n init: 'init',\n map: 'map',\n retrieve: 'retrieve',\n stats: 'stats',\n hooks: 'hooks',\n agents: 'agents',\n} as const satisfies Record<Exclude<CliInvocation['mode'], 'help' | 'version'>, Verb>;\n\n/** The command that produced an invocation, for dispatch. */\nexport function commandFor(\n mode: Exclude<CliInvocation['mode'], 'help' | 'version'>,\n): AnySubcommand {\n return SUBCOMMANDS[VERB_BY_MODE[mode]];\n}\n\n/** How a command is named in prose. The default verb has no word to name it by. */\nfunction label(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : command.name;\n}\n\n/** How a command is named as the *owner* of a flag: the form you would type. */\nfunction ownerLabel(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : `\\`${CLI_NAME} ${command.name}\\``;\n}\n\n/**\n * The commands that own a flag, in registry order — empty for the two global flags,\n * which no verb owns. A flag with exactly one owner has an unambiguous home worth\n * naming in a refusal (and worth prefixing its OPTIONS entry with); a flag several\n * verbs share (`--json`, `--budget`, `--focus`) has none, and pointing at one of them\n * would be a guess.\n */\nexport function ownersOf(flag: FlagName): readonly AnySubcommand[] {\n return SUBCOMMAND_LIST.filter((command) => (command.flags as readonly string[]).includes(flag));\n}\n\n/**\n * Refuse every flag the chosen verb does not own — the one message that replaced five\n * hand-written ones.\n *\n * Before this existed, each verb refused each other verb's flags in prose, so the\n * refusals were O(verbs × flags) sentences kept in sync by hand and an eleventh flag\n * edited five of them. Now ownership is declared once per verb and the message is\n * generated from three derived facts, in the order a reader needs them:\n *\n * 1. **what this verb takes**, and what it got instead — the offending flag, named;\n * 2. **where the flag does belong**, when exactly one verb owns it, listed as that\n * owner's *exclusively* owned flags (so `--ignore` here still reads \"--ignore and\n * --cache belong to `smelt map`\", exactly as the hand-written message did);\n * 3. **why not here** — the verb's own `refusal` sentence, the half of the old\n * messages worth keeping, and the only half a verb still writes.\n *\n * Every refusal stays a {@link CliUsageError}, so every one of them still exits 2.\n *\n * @throws {CliUsageError} naming the offending flag, this verb, and the flag's owner.\n */\nexport function refuseForeignFlags(command: AnySubcommand, values: FlagValues): void {\n const foreign = VERB_FLAGS.filter(\n (flag) => values[flag] !== undefined && !command.flags.includes(flag),\n );\n if (foreign.length === 0) return;\n\n const takes =\n command.flags.length === 0\n ? `${label(command)} takes no flags (got ${flagList(foreign)}).`\n : `${label(command)} takes only ${flagList(command.flags)} (got ${flagList(foreign)}).`;\n\n throw new CliUsageError(`${CLI_NAME}: ${takes}${redirects(foreign)} ${command.refusal}`);\n}\n\n/**\n * ` --ignore and --cache belong to \\`smelt map\\`.` — one clause per single-owner verb.\n *\n * A clause names only the flags its owner owns **alone**, never the ones it shares.\n * `ownersOf(flag).length === 1` is the same test that decided the owner deserved a\n * clause at all, applied to the whole clause: a shared flag (`--budget`, `--focus`,\n * `--json`) has no single home, so naming it here would assert an ownership that the\n * OPTIONS block — which prefixes `map only.` by the same rule — correctly denies.\n */\nfunction redirects(foreign: readonly VerbFlag[]): string {\n const owners = SUBCOMMAND_LIST.filter((owner) =>\n foreign.some((flag) => {\n const claimants = ownersOf(flag);\n return claimants.length === 1 && claimants[0] === owner;\n }),\n );\n return owners\n .map((owner) => {\n const elsewhere = owner.flags.filter((flag) => ownersOf(flag).length === 1);\n const verb = elsewhere.length === 1 ? 'belongs' : 'belong';\n return ` ${flagList(elsewhere)} ${verb} to ${ownerLabel(owner)}.`;\n })\n .join('');\n}\n"]}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/cli/subcommands/registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { CLI_NAME } from '../shell.ts';\n\nimport { agentsCommand } from './agents.ts';\nimport type { AgentsInvocation } from './agents.ts';\nimport { doctorCommand } from './doctor.ts';\nimport type { DoctorInvocation } from './doctor.ts';\nimport { flagList, VERB_FLAGS } from './flags.ts';\nimport type { FlagName, FlagValues, VerbFlag } from './flags.ts';\nimport { hooksCommand } from './hooks.ts';\nimport type { HooksInvocation } from './hooks.ts';\nimport { initCommand } from './init.ts';\nimport type { InitInvocation } from './init.ts';\nimport { mapCommand } from './map.ts';\nimport type { MapInvocation } from './map.ts';\nimport { retrieveCommand } from './retrieve.ts';\nimport type { RetrieveInvocation } from './retrieve.ts';\nimport { smeltCommand } from './smelt.ts';\nimport type { SmeltInvocation } from './smelt.ts';\nimport { setupCommand } from './setup.ts';\nimport type { SetupInvocation } from './setup.ts';\nimport { statsCommand } from './stats.ts';\nimport type { StatsInvocation } from './stats.ts';\nimport { DEFAULT_VERB } from './subcommand.ts';\nimport type { Subcommand, Verb } from './subcommand.ts';\n\n/**\n * The registry — every verb `smelt` answers to, one {@link Subcommand} each.\n *\n * `Record<Verb, Subcommand>` on purpose, exactly like `LANGUAGE_PROFILES` and\n * `HARNESS_PROFILES`: adding a `Verb` in `subcommand.ts` without writing its file is a\n * compile error, so the verb list and the facts cannot drift. Every derived view — the\n * USAGE block, the help's sections, the `map only.` prefixes in OPTIONS, and above all\n * the flag-ownership refusal below — is computed from this object, never written twice.\n *\n * Key order is meaningful: it is the order every rendered list uses (USAGE, the help\n * sections, the owners named in a refusal), so keep it stable and append new verbs at\n * the end.\n */\nexport const SUBCOMMANDS: Readonly<Record<Verb, AnySubcommand>> = {\n smelt: smeltCommand,\n init: initCommand,\n map: mapCommand,\n retrieve: retrieveCommand,\n stats: statsCommand,\n hooks: hooksCommand,\n agents: agentsCommand,\n setup: setupCommand,\n doctor: doctorCommand,\n};\n\n/** Everything `parseSmeltArgs` can return. Narrow on `mode`. */\nexport type CliInvocation =\n | SmeltInvocation\n | InitInvocation\n | MapInvocation\n | RetrieveInvocation\n | StatsInvocation\n | HooksInvocation\n | AgentsInvocation\n | SetupInvocation\n | DoctorInvocation;\n\n/**\n * One registry entry, with its verb's own invocation and resolved types erased.\n *\n * The erasure is what lets six differently-typed commands live in one `Record`, and it\n * is safe because the two ends are never crossed: `runCli` hands a command exactly the\n * invocation that command's own `parse` produced (`verbFor` maps each `mode` back to\n * the verb that minted it), and the resolved value never leaves the pair of calls that\n * makes and consumes it.\n */\nexport type AnySubcommand = Subcommand<CliInvocation, unknown>;\n\n/** Every command, in registry order. The list every rendered block walks. */\nexport const SUBCOMMAND_LIST: readonly AnySubcommand[] = Object.values(SUBCOMMANDS);\n\n/**\n * The verbs with a word on the command line — everything but the default one.\n *\n * The default verb is excluded deliberately: `smelt smelt` is a file named `smelt`,\n * not a recursive invocation, the same way `smelt map` as a *file* needs `./map`.\n */\nexport const NAMED_VERBS: readonly Verb[] = SUBCOMMAND_LIST.map((command) => command.name).filter(\n (name) => name !== DEFAULT_VERB,\n);\n\n/**\n * Which command a command line selects: `positionals[0]` when it names a verb, the\n * default verb otherwise. The whole of subcommand dispatch, in one lookup.\n */\nexport function subcommandFor(positionals: readonly string[]): AnySubcommand {\n const first = positionals[0];\n const named = NAMED_VERBS.find((verb) => verb === first);\n return SUBCOMMANDS[named ?? DEFAULT_VERB];\n}\n\n/**\n * Which verb minted an invocation. Total over the modes `parseSmeltArgs` can return\n * for a verb, so a new mode without a home is a compile error — `'reconstruct'` maps\n * to the default verb because `--reconstruct` is that verb's second job, not a\n * seventh command.\n */\nconst VERB_BY_MODE = {\n smelt: 'smelt',\n reconstruct: 'smelt',\n init: 'init',\n map: 'map',\n retrieve: 'retrieve',\n stats: 'stats',\n hooks: 'hooks',\n agents: 'agents',\n setup: 'setup',\n doctor: 'doctor',\n} as const satisfies Record<Exclude<CliInvocation['mode'], 'help' | 'version'>, Verb>;\n\n/** The command that produced an invocation, for dispatch. */\nexport function commandFor(\n mode: Exclude<CliInvocation['mode'], 'help' | 'version'>,\n): AnySubcommand {\n return SUBCOMMANDS[VERB_BY_MODE[mode]];\n}\n\n/** How a command is named in prose. The default verb has no word to name it by. */\nfunction label(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : command.name;\n}\n\n/** How a command is named as the *owner* of a flag: the form you would type. */\nfunction ownerLabel(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : `\\`${CLI_NAME} ${command.name}\\``;\n}\n\n/**\n * The commands that own a flag, in registry order — empty for the two global flags,\n * which no verb owns. A flag with exactly one owner has an unambiguous home worth\n * naming in a refusal (and worth prefixing its OPTIONS entry with); a flag several\n * verbs share (`--json`, `--budget`, `--focus`) has none, and pointing at one of them\n * would be a guess.\n */\nexport function ownersOf(flag: FlagName): readonly AnySubcommand[] {\n return SUBCOMMAND_LIST.filter((command) => (command.flags as readonly string[]).includes(flag));\n}\n\n/**\n * Refuse every flag the chosen verb does not own — the one message that replaced five\n * hand-written ones.\n *\n * Before this existed, each verb refused each other verb's flags in prose, so the\n * refusals were O(verbs × flags) sentences kept in sync by hand and an eleventh flag\n * edited five of them. Now ownership is declared once per verb and the message is\n * generated from three derived facts, in the order a reader needs them:\n *\n * 1. **what this verb takes**, and what it got instead — the offending flag, named;\n * 2. **where the flag does belong**, when exactly one verb owns it, listed as that\n * owner's *exclusively* owned flags (so `--ignore` here still reads \"--ignore and\n * --cache belong to `smelt map`\", exactly as the hand-written message did);\n * 3. **why not here** — the verb's own `refusal` sentence, the half of the old\n * messages worth keeping, and the only half a verb still writes.\n *\n * Every refusal stays a {@link CliUsageError}, so every one of them still exits 2.\n *\n * @throws {CliUsageError} naming the offending flag, this verb, and the flag's owner.\n */\nexport function refuseForeignFlags(command: AnySubcommand, values: FlagValues): void {\n const foreign = VERB_FLAGS.filter(\n (flag) => values[flag] !== undefined && !command.flags.includes(flag),\n );\n if (foreign.length === 0) return;\n\n const takes =\n command.flags.length === 0\n ? `${label(command)} takes no flags (got ${flagList(foreign)}).`\n : `${label(command)} takes only ${flagList(command.flags)} (got ${flagList(foreign)}).`;\n\n throw new CliUsageError(`${CLI_NAME}: ${takes}${redirects(foreign)} ${command.refusal}`);\n}\n\n/**\n * ` --ignore and --cache belong to \\`smelt map\\`.` — one clause per single-owner verb.\n *\n * A clause names only the flags its owner owns **alone**, never the ones it shares.\n * `ownersOf(flag).length === 1` is the same test that decided the owner deserved a\n * clause at all, applied to the whole clause: a shared flag (`--budget`, `--focus`,\n * `--json`) has no single home, so naming it here would assert an ownership that the\n * OPTIONS block — which prefixes `map only.` by the same rule — correctly denies.\n */\nfunction redirects(foreign: readonly VerbFlag[]): string {\n const owners = SUBCOMMAND_LIST.filter((owner) =>\n foreign.some((flag) => {\n const claimants = ownersOf(flag);\n return claimants.length === 1 && claimants[0] === owner;\n }),\n );\n return owners\n .map((owner) => {\n const elsewhere = owner.flags.filter((flag) => ownersOf(flag).length === 1);\n const verb = elsewhere.length === 1 ? 'belongs' : 'belong';\n return ` ${flagList(elsewhere)} ${verb} to ${ownerLabel(owner)}.`;\n })\n .join('');\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import type { Subcommand } from './subcommand.ts';
2
+ /**
3
+ * `smelt setup` — the one-command front door for the whole recipe. The flow itself is
4
+ * `cli/setup.ts`, a pure function over an injected input/output pair; this file is
5
+ * only the verb.
6
+ *
7
+ * Interactive from a terminal, like `init` and `hooks`; for an agent, the flags are
8
+ * the whole interface — `--yes` answers everything from the recipe, `--harness`
9
+ * (repeatable) picks the harnesses, `--no-mcp` skips the registration step, and
10
+ * `--json` (with `--yes`) turns the run into a machine-readable receipt. The refusal
11
+ * below is how an agent with no TTY learns that interface, which is the same trick
12
+ * `init`'s refusal uses.
13
+ */
14
+ /** `smelt setup [--harness <id>]... [--yes] [--no-mcp] [--json]` — parsed. */
15
+ export interface SetupInvocation {
16
+ readonly mode: 'setup';
17
+ readonly harnessIds: readonly string[];
18
+ readonly yes: boolean;
19
+ readonly noMcp: boolean;
20
+ readonly json: boolean;
21
+ }
22
+ export declare const setupCommand: Subcommand<SetupInvocation, SetupInvocation>;
23
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/setup.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;;;GAWG;AAEH,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,eAAe,EAAE,eAAe,CA2FrE,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESSES, harnessById } from '../../harness/registry.ts';\nimport { SETUP_RECIPE } from '../../setup/recipe.ts';\nimport { runSetup } from '../setup.ts';\nimport { colorize } from '../lava.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt setup` — the one-command front door for the whole recipe. The flow itself is\n * `cli/setup.ts`, a pure function over an injected input/output pair; this file is\n * only the verb.\n *\n * Interactive from a terminal, like `init` and `hooks`; for an agent, the flags are\n * the whole interface — `--yes` answers everything from the recipe, `--harness`\n * (repeatable) picks the harnesses, `--no-mcp` skips the registration step, and\n * `--json` (with `--yes`) turns the run into a machine-readable receipt. The refusal\n * below is how an agent with no TTY learns that interface, which is the same trick\n * `init`'s refusal uses.\n */\n\n/** `smelt setup [--harness <id>]... [--yes] [--no-mcp] [--json]` — parsed. */\nexport interface SetupInvocation {\n readonly mode: 'setup';\n readonly harnessIds: readonly string[];\n readonly yes: boolean;\n readonly noMcp: boolean;\n readonly json: boolean;\n}\n\nexport const setupCommand: Subcommand<SetupInvocation, SetupInvocation> = {\n name: 'setup',\n flags: ['harness', 'yes', 'no-mcp', 'json'],\n refusal: `setup applies the recipe; answer it with --yes (and --harness, --no-mcp, --json) or let it ask.`,\n usage: {\n synopsis: [],\n occasional: ['setup [--harness <id>]... [--yes] [--no-mcp] [--json]'],\n section: {\n heading: 'SETUP',\n body:\n ` ${CLI_NAME} setup applies the whole recipe in one command: smelt.config.json, the\\n` +\n ` hooks preset for the harnesses you name, the MCP registration step, and a real\\n` +\n ` smelt → retrieve round trip to prove the loop. Interactive from a terminal; for\\n` +\n ` an agent, answer everything up front:\\n\\n` +\n ` ${CLI_NAME} setup --yes [--harness <id>]... [--no-mcp] [--json]\\n\\n` +\n ` The defaults are the recipe's: budget ${SETUP_RECIPE.recommendedBudgetBytes} bytes\\n` +\n ` (written only when the config carries none), a directory store at\\n` +\n ` ${SETUP_RECIPE.store.defaultDir} (only when the config carries none). Existing\\n` +\n ` files are never overwritten — they are skipped with a note; hooks install edits\\n` +\n ` them, and it asks per file. Re-running on a current machine writes nothing and\\n` +\n ` exits 0. --json prints a receipt: every file, every check, the exit's meaning.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): SetupInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: setup takes no further arguments, got ` +\n `${positionals.slice(1).join(', ')}.`,\n );\n }\n const harnessIds = (values.harness ?? []).map((id) => {\n if (harnessById(id) === undefined) {\n throw new CliUsageError(\n `${CLI_NAME} setup: unknown harness \"${id}\". ` +\n `Known: ${HARNESSES.map((harness) => harness.id).join(', ')}.`,\n );\n }\n return id;\n });\n const yes = values.yes === true;\n const json = values.json === true;\n if (json && !yes) {\n throw new CliUsageError(\n `${CLI_NAME}: --json prints a machine receipt — pair it with --yes. ` +\n `The interactive flow's output is for humans.`,\n );\n }\n return {\n mode: 'setup',\n harnessIds,\n yes,\n noMcp: values['no-mcp'] === true,\n json,\n };\n },\n\n /**\n * Nothing to merge: the flow reads `smelt.config.json` itself. A *missing* config\n * is the thing setup writes; a *malformed* one is a loud refusal — doctor names it,\n * the user fixes it, and setup never guesses around bytes it cannot parse.\n */\n resolve(invocation: SetupInvocation): SetupInvocation {\n return invocation;\n },\n\n /**\n * Interactive unless `--yes` answered everything, so it needs the wizard stream —\n * and the refusal below is the agent-facing interface documentation.\n */\n async run(resolved: SetupInvocation, io: CliIo): Promise<number> {\n if (!resolved.yes && io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: setup is interactive unless you answer it up front. ` +\n `Non-interactive:\\n` +\n ` ${CLI_NAME} setup --yes [--harness <id>]... [--no-mcp] [--json]`,\n );\n }\n return await runSetup(resolved, {\n // `input` stays absent for `--yes` — exactOptionalPropertyTypes means \"absent\"\n // is a decision, not a field carrying undefined.\n ...(io.initInput === undefined ? {} : { input: io.initInput }),\n output: (text) =>\n io.stdout(colorize(text, io.color === true && !resolved.yes && !resolved.json)),\n cwd: io.cwd ?? process.cwd(),\n version: io.version,\n // The lava renderer is for the human at a terminal: --yes and --json are the\n // machine paths, and their bytes stay plain however pretty the screen is.\n ...(io.color === true && !resolved.yes && !resolved.json ? { color: true } : {}),\n });\n },\n};\n"]}
@@ -0,0 +1,87 @@
1
+ import process from 'node:process';
2
+ import { CliUsageError } from '../../errors.js';
3
+ import { HARNESSES, harnessById } from '../../harness/registry.js';
4
+ import { SETUP_RECIPE } from '../../setup/recipe.js';
5
+ import { runSetup } from '../setup.js';
6
+ import { colorize } from '../lava.js';
7
+ import { CLI_NAME } from '../shell.js';
8
+ export const setupCommand = {
9
+ name: 'setup',
10
+ flags: ['harness', 'yes', 'no-mcp', 'json'],
11
+ refusal: `setup applies the recipe; answer it with --yes (and --harness, --no-mcp, --json) or let it ask.`,
12
+ usage: {
13
+ synopsis: [],
14
+ occasional: ['setup [--harness <id>]... [--yes] [--no-mcp] [--json]'],
15
+ section: {
16
+ heading: 'SETUP',
17
+ body: ` ${CLI_NAME} setup applies the whole recipe in one command: smelt.config.json, the\n` +
18
+ ` hooks preset for the harnesses you name, the MCP registration step, and a real\n` +
19
+ ` smelt → retrieve round trip to prove the loop. Interactive from a terminal; for\n` +
20
+ ` an agent, answer everything up front:\n\n` +
21
+ ` ${CLI_NAME} setup --yes [--harness <id>]... [--no-mcp] [--json]\n\n` +
22
+ ` The defaults are the recipe's: budget ${SETUP_RECIPE.recommendedBudgetBytes} bytes\n` +
23
+ ` (written only when the config carries none), a directory store at\n` +
24
+ ` ${SETUP_RECIPE.store.defaultDir} (only when the config carries none). Existing\n` +
25
+ ` files are never overwritten — they are skipped with a note; hooks install edits\n` +
26
+ ` them, and it asks per file. Re-running on a current machine writes nothing and\n` +
27
+ ` exits 0. --json prints a receipt: every file, every check, the exit's meaning.`,
28
+ },
29
+ },
30
+ parse(values, positionals) {
31
+ if (positionals.length > 1) {
32
+ throw new CliUsageError(`${CLI_NAME}: setup takes no further arguments, got ` +
33
+ `${positionals.slice(1).join(', ')}.`);
34
+ }
35
+ const harnessIds = (values.harness ?? []).map((id) => {
36
+ if (harnessById(id) === undefined) {
37
+ throw new CliUsageError(`${CLI_NAME} setup: unknown harness "${id}". ` +
38
+ `Known: ${HARNESSES.map((harness) => harness.id).join(', ')}.`);
39
+ }
40
+ return id;
41
+ });
42
+ const yes = values.yes === true;
43
+ const json = values.json === true;
44
+ if (json && !yes) {
45
+ throw new CliUsageError(`${CLI_NAME}: --json prints a machine receipt — pair it with --yes. ` +
46
+ `The interactive flow's output is for humans.`);
47
+ }
48
+ return {
49
+ mode: 'setup',
50
+ harnessIds,
51
+ yes,
52
+ noMcp: values['no-mcp'] === true,
53
+ json,
54
+ };
55
+ },
56
+ /**
57
+ * Nothing to merge: the flow reads `smelt.config.json` itself. A *missing* config
58
+ * is the thing setup writes; a *malformed* one is a loud refusal — doctor names it,
59
+ * the user fixes it, and setup never guesses around bytes it cannot parse.
60
+ */
61
+ resolve(invocation) {
62
+ return invocation;
63
+ },
64
+ /**
65
+ * Interactive unless `--yes` answered everything, so it needs the wizard stream —
66
+ * and the refusal below is the agent-facing interface documentation.
67
+ */
68
+ async run(resolved, io) {
69
+ if (!resolved.yes && io.initInput === undefined) {
70
+ throw new CliUsageError(`${CLI_NAME}: setup is interactive unless you answer it up front. ` +
71
+ `Non-interactive:\n` +
72
+ ` ${CLI_NAME} setup --yes [--harness <id>]... [--no-mcp] [--json]`);
73
+ }
74
+ return await runSetup(resolved, {
75
+ // `input` stays absent for `--yes` — exactOptionalPropertyTypes means "absent"
76
+ // is a decision, not a field carrying undefined.
77
+ ...(io.initInput === undefined ? {} : { input: io.initInput }),
78
+ output: (text) => io.stdout(colorize(text, io.color === true && !resolved.yes && !resolved.json)),
79
+ cwd: io.cwd ?? process.cwd(),
80
+ version: io.version,
81
+ // The lava renderer is for the human at a terminal: --yes and --json are the
82
+ // machine paths, and their bytes stay plain however pretty the screen is.
83
+ ...(io.color === true && !resolved.yes && !resolved.json ? { color: true } : {}),
84
+ });
85
+ },
86
+ };
87
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/cli/subcommands/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESSES, harnessById } from '../../harness/registry.ts';\nimport { SETUP_RECIPE } from '../../setup/recipe.ts';\nimport { runSetup } from '../setup.ts';\nimport { colorize } from '../lava.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt setup` — the one-command front door for the whole recipe. The flow itself is\n * `cli/setup.ts`, a pure function over an injected input/output pair; this file is\n * only the verb.\n *\n * Interactive from a terminal, like `init` and `hooks`; for an agent, the flags are\n * the whole interface — `--yes` answers everything from the recipe, `--harness`\n * (repeatable) picks the harnesses, `--no-mcp` skips the registration step, and\n * `--json` (with `--yes`) turns the run into a machine-readable receipt. The refusal\n * below is how an agent with no TTY learns that interface, which is the same trick\n * `init`'s refusal uses.\n */\n\n/** `smelt setup [--harness <id>]... [--yes] [--no-mcp] [--json]` — parsed. */\nexport interface SetupInvocation {\n readonly mode: 'setup';\n readonly harnessIds: readonly string[];\n readonly yes: boolean;\n readonly noMcp: boolean;\n readonly json: boolean;\n}\n\nexport const setupCommand: Subcommand<SetupInvocation, SetupInvocation> = {\n name: 'setup',\n flags: ['harness', 'yes', 'no-mcp', 'json'],\n refusal: `setup applies the recipe; answer it with --yes (and --harness, --no-mcp, --json) or let it ask.`,\n usage: {\n synopsis: [],\n occasional: ['setup [--harness <id>]... [--yes] [--no-mcp] [--json]'],\n section: {\n heading: 'SETUP',\n body:\n ` ${CLI_NAME} setup applies the whole recipe in one command: smelt.config.json, the\\n` +\n ` hooks preset for the harnesses you name, the MCP registration step, and a real\\n` +\n ` smelt → retrieve round trip to prove the loop. Interactive from a terminal; for\\n` +\n ` an agent, answer everything up front:\\n\\n` +\n ` ${CLI_NAME} setup --yes [--harness <id>]... [--no-mcp] [--json]\\n\\n` +\n ` The defaults are the recipe's: budget ${SETUP_RECIPE.recommendedBudgetBytes} bytes\\n` +\n ` (written only when the config carries none), a directory store at\\n` +\n ` ${SETUP_RECIPE.store.defaultDir} (only when the config carries none). Existing\\n` +\n ` files are never overwritten — they are skipped with a note; hooks install edits\\n` +\n ` them, and it asks per file. Re-running on a current machine writes nothing and\\n` +\n ` exits 0. --json prints a receipt: every file, every check, the exit's meaning.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): SetupInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: setup takes no further arguments, got ` +\n `${positionals.slice(1).join(', ')}.`,\n );\n }\n const harnessIds = (values.harness ?? []).map((id) => {\n if (harnessById(id) === undefined) {\n throw new CliUsageError(\n `${CLI_NAME} setup: unknown harness \"${id}\". ` +\n `Known: ${HARNESSES.map((harness) => harness.id).join(', ')}.`,\n );\n }\n return id;\n });\n const yes = values.yes === true;\n const json = values.json === true;\n if (json && !yes) {\n throw new CliUsageError(\n `${CLI_NAME}: --json prints a machine receipt — pair it with --yes. ` +\n `The interactive flow's output is for humans.`,\n );\n }\n return {\n mode: 'setup',\n harnessIds,\n yes,\n noMcp: values['no-mcp'] === true,\n json,\n };\n },\n\n /**\n * Nothing to merge: the flow reads `smelt.config.json` itself. A *missing* config\n * is the thing setup writes; a *malformed* one is a loud refusal — doctor names it,\n * the user fixes it, and setup never guesses around bytes it cannot parse.\n */\n resolve(invocation: SetupInvocation): SetupInvocation {\n return invocation;\n },\n\n /**\n * Interactive unless `--yes` answered everything, so it needs the wizard stream —\n * and the refusal below is the agent-facing interface documentation.\n */\n async run(resolved: SetupInvocation, io: CliIo): Promise<number> {\n if (!resolved.yes && io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: setup is interactive unless you answer it up front. ` +\n `Non-interactive:\\n` +\n ` ${CLI_NAME} setup --yes [--harness <id>]... [--no-mcp] [--json]`,\n );\n }\n return await runSetup(resolved, {\n // `input` stays absent for `--yes` — exactOptionalPropertyTypes means \"absent\"\n // is a decision, not a field carrying undefined.\n ...(io.initInput === undefined ? {} : { input: io.initInput }),\n output: (text) =>\n io.stdout(colorize(text, io.color === true && !resolved.yes && !resolved.json)),\n cwd: io.cwd ?? process.cwd(),\n version: io.version,\n // The lava renderer is for the human at a terminal: --yes and --json are the\n // machine paths, and their bytes stay plain however pretty the screen is.\n ...(io.color === true && !resolved.yes && !resolved.json ? { color: true } : {}),\n });\n },\n};\n"]}
@@ -9,7 +9,7 @@ import type { FlagValues, VerbFlag } from './flags.ts';
9
9
  * Order is meaningful: it is the order `--help` renders every derived list in, and the
10
10
  * order a refusal names owners in. Keep it stable and append new verbs at the end.
11
11
  */
12
- export type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents';
12
+ export type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents' | 'setup' | 'doctor';
13
13
  /** The verb a bare invocation selects — a file or stdin, with no subcommand word. */
14
14
  export declare const DEFAULT_VERB = "smelt";
15
15
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"subcommand.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/subcommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AAExF,qFAAqF;AACrF,eAAO,MAAM,YAAY,UAAyB,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,YAAY,GAAG,SAAS,CAAC;AAE1D,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,EAAE,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC;;;;;OAKG;IACH,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7D;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,GAAG,CAAC,CAAC;IAChD,wEAAwE;IACxE,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvD","sourcesContent":["import type { LoadedConfig } from '../config.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues, VerbFlag } from './flags.ts';\n\n/**\n * The verbs `smelt` answers to. `'smelt'` is the default verb — the bare\n * `smelt <file>` / `smelt < input` run — and is the only one with no word on the\n * command line, which is why a file literally named `map` needs `./map`.\n *\n * Order is meaningful: it is the order `--help` renders every derived list in, and the\n * order a refusal names owners in. Keep it stable and append new verbs at the end.\n */\nexport type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents';\n\n/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */\nexport const DEFAULT_VERB = 'smelt' satisfies Verb;\n\n/**\n * The nearest `smelt.config.json`, loaded on demand.\n *\n * A thunk rather than a value, because two verbs must **not** load it: `init` and\n * `hooks` read the config file themselves, with their own tolerance, and a wizard you\n * run to *fix* a malformed config cannot be a wizard that refuses to start because the\n * config is malformed. Loading eagerly in the dispatcher would quietly turn that into\n * a usage error.\n */\nexport type ConfigSource = () => LoadedConfig | undefined;\n\n/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */\nexport interface SubcommandUsage {\n /**\n * The command's everyday forms, rendered in the first block of USAGE in registry\n * order. Each entry is the text *after* the CLI name.\n */\n readonly synopsis: readonly string[];\n /**\n * Forms rendered in the second block, after every command's everyday forms: the\n * round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do\n * occasionally rather than the shape of a normal run. It keeps USAGE reading\n * top-to-bottom by how often you type a thing without anyone arranging the list.\n */\n readonly occasional?: readonly string[];\n /**\n * The verb's own section of the help — a heading and its body, already indented.\n * Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &\n * STATS, because the loop is one story); their bodies are joined under it in\n * registry order.\n */\n readonly section?: { readonly heading: string; readonly body: string };\n}\n\n/**\n * One subcommand: everything the CLI knows about one verb, in one place.\n *\n * This is the seam the CLI was missing. A verb used to be a *shape* restated in four\n * modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a\n * `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in\n * `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every\n * flag it did not want. The refusals were the compounding cost: because no verb owned\n * its flags, every verb refused every other verb's flags by hand, so an eleventh flag\n * edited five messages.\n *\n * Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`\n * is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}\n * pattern, so totality is a compile error and every rendered list (USAGE, the sections,\n * the `map only.` prefixes in OPTIONS) is a derived view.\n *\n * The type parameters are the verb's own two shapes, and they never merge:\n *\n * @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so\n * the parse is testable on its own.\n * @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single\n * merge of flags + config + built-ins, each value carrying its provenance. Deliberately\n * one type per verb: `map` has no store, no strategy and no stdin, and a shared struct\n * whose fields are lies for half its users is not a seam, it is a coincidence.\n */\nexport interface Subcommand<I, R> {\n /** The word on the command line — and the registry key. `'smelt'` has no word. */\n readonly name: Verb;\n /**\n * The flags this verb owns. **The whole point.** Every flag outside this list is\n * refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather\n * than by prose the next verb has to write again. A flag silently ignored would be a\n * setting the user believed was in force.\n */\n readonly flags: readonly VerbFlag[];\n /**\n * What this verb *is*, as one sentence — the tail of the generated refusal, and the\n * only part of it a verb writes. It answers \"why not here?\", which is the half of\n * the old hand-written messages worth keeping; the offending flag, this verb's name\n * and the verb the flag does belong to are all derived from the registry.\n */\n readonly refusal: string;\n /** What this verb contributes to `--help`. Rendered, never hand-arranged. */\n readonly usage: SubcommandUsage;\n /**\n * The verb's own validation, over the flags it owns and the positionals it was\n * given (index 0 is the verb word itself, except for the default verb).\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\n parse(values: FlagValues, positionals: readonly string[]): I;\n /**\n * The verb's single merge of flags + config + built-ins. Precedence for this verb\n * lives here and nowhere else, so a precedence question is answered by one function\n * instead of by reading two files.\n *\n * @throws {CliUsageError} when a required value has no source — the budget, for the\n * two verbs that need one.\n */\n resolve(invocation: I, config: ConfigSource): R;\n /** Execute the resolved run straight-line, and return the exit code. */\n run(resolved: R, io: CliIo): number | Promise<number>;\n}\n"]}
1
+ {"version":3,"file":"subcommand.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/subcommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,GACd,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE5F,qFAAqF;AACrF,eAAO,MAAM,YAAY,UAAyB,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,YAAY,GAAG,SAAS,CAAC;AAE1D,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,EAAE,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC;;;;;OAKG;IACH,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7D;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,GAAG,CAAC,CAAC;IAChD,wEAAwE;IACxE,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvD","sourcesContent":["import type { LoadedConfig } from '../config.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues, VerbFlag } from './flags.ts';\n\n/**\n * The verbs `smelt` answers to. `'smelt'` is the default verb — the bare\n * `smelt <file>` / `smelt < input` run — and is the only one with no word on the\n * command line, which is why a file literally named `map` needs `./map`.\n *\n * Order is meaningful: it is the order `--help` renders every derived list in, and the\n * order a refusal names owners in. Keep it stable and append new verbs at the end.\n */\nexport type Verb =\n 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents' | 'setup' | 'doctor';\n\n/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */\nexport const DEFAULT_VERB = 'smelt' satisfies Verb;\n\n/**\n * The nearest `smelt.config.json`, loaded on demand.\n *\n * A thunk rather than a value, because two verbs must **not** load it: `init` and\n * `hooks` read the config file themselves, with their own tolerance, and a wizard you\n * run to *fix* a malformed config cannot be a wizard that refuses to start because the\n * config is malformed. Loading eagerly in the dispatcher would quietly turn that into\n * a usage error.\n */\nexport type ConfigSource = () => LoadedConfig | undefined;\n\n/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */\nexport interface SubcommandUsage {\n /**\n * The command's everyday forms, rendered in the first block of USAGE in registry\n * order. Each entry is the text *after* the CLI name.\n */\n readonly synopsis: readonly string[];\n /**\n * Forms rendered in the second block, after every command's everyday forms: the\n * round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do\n * occasionally rather than the shape of a normal run. It keeps USAGE reading\n * top-to-bottom by how often you type a thing without anyone arranging the list.\n */\n readonly occasional?: readonly string[];\n /**\n * The verb's own section of the help — a heading and its body, already indented.\n * Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &\n * STATS, because the loop is one story); their bodies are joined under it in\n * registry order.\n */\n readonly section?: { readonly heading: string; readonly body: string };\n}\n\n/**\n * One subcommand: everything the CLI knows about one verb, in one place.\n *\n * This is the seam the CLI was missing. A verb used to be a *shape* restated in four\n * modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a\n * `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in\n * `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every\n * flag it did not want. The refusals were the compounding cost: because no verb owned\n * its flags, every verb refused every other verb's flags by hand, so an eleventh flag\n * edited five messages.\n *\n * Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`\n * is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}\n * pattern, so totality is a compile error and every rendered list (USAGE, the sections,\n * the `map only.` prefixes in OPTIONS) is a derived view.\n *\n * The type parameters are the verb's own two shapes, and they never merge:\n *\n * @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so\n * the parse is testable on its own.\n * @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single\n * merge of flags + config + built-ins, each value carrying its provenance. Deliberately\n * one type per verb: `map` has no store, no strategy and no stdin, and a shared struct\n * whose fields are lies for half its users is not a seam, it is a coincidence.\n */\nexport interface Subcommand<I, R> {\n /** The word on the command line — and the registry key. `'smelt'` has no word. */\n readonly name: Verb;\n /**\n * The flags this verb owns. **The whole point.** Every flag outside this list is\n * refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather\n * than by prose the next verb has to write again. A flag silently ignored would be a\n * setting the user believed was in force.\n */\n readonly flags: readonly VerbFlag[];\n /**\n * What this verb *is*, as one sentence — the tail of the generated refusal, and the\n * only part of it a verb writes. It answers \"why not here?\", which is the half of\n * the old hand-written messages worth keeping; the offending flag, this verb's name\n * and the verb the flag does belong to are all derived from the registry.\n */\n readonly refusal: string;\n /** What this verb contributes to `--help`. Rendered, never hand-arranged. */\n readonly usage: SubcommandUsage;\n /**\n * The verb's own validation, over the flags it owns and the positionals it was\n * given (index 0 is the verb word itself, except for the default verb).\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\n parse(values: FlagValues, positionals: readonly string[]): I;\n /**\n * The verb's single merge of flags + config + built-ins. Precedence for this verb\n * lives here and nowhere else, so a precedence question is answered by one function\n * instead of by reading two files.\n *\n * @throws {CliUsageError} when a required value has no source — the budget, for the\n * two verbs that need one.\n */\n resolve(invocation: I, config: ConfigSource): R;\n /** Execute the resolved run straight-line, and return the exit code. */\n run(resolved: R, io: CliIo): number | Promise<number>;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"subcommand.js","sourceRoot":"","sources":["../../../src/cli/subcommands/subcommand.ts"],"names":[],"mappings":"AAeA,qFAAqF;AACrF,MAAM,CAAC,MAAM,YAAY,GAAG,OAAsB,CAAC","sourcesContent":["import type { LoadedConfig } from '../config.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues, VerbFlag } from './flags.ts';\n\n/**\n * The verbs `smelt` answers to. `'smelt'` is the default verb — the bare\n * `smelt <file>` / `smelt < input` run — and is the only one with no word on the\n * command line, which is why a file literally named `map` needs `./map`.\n *\n * Order is meaningful: it is the order `--help` renders every derived list in, and the\n * order a refusal names owners in. Keep it stable and append new verbs at the end.\n */\nexport type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents';\n\n/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */\nexport const DEFAULT_VERB = 'smelt' satisfies Verb;\n\n/**\n * The nearest `smelt.config.json`, loaded on demand.\n *\n * A thunk rather than a value, because two verbs must **not** load it: `init` and\n * `hooks` read the config file themselves, with their own tolerance, and a wizard you\n * run to *fix* a malformed config cannot be a wizard that refuses to start because the\n * config is malformed. Loading eagerly in the dispatcher would quietly turn that into\n * a usage error.\n */\nexport type ConfigSource = () => LoadedConfig | undefined;\n\n/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */\nexport interface SubcommandUsage {\n /**\n * The command's everyday forms, rendered in the first block of USAGE in registry\n * order. Each entry is the text *after* the CLI name.\n */\n readonly synopsis: readonly string[];\n /**\n * Forms rendered in the second block, after every command's everyday forms: the\n * round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do\n * occasionally rather than the shape of a normal run. It keeps USAGE reading\n * top-to-bottom by how often you type a thing without anyone arranging the list.\n */\n readonly occasional?: readonly string[];\n /**\n * The verb's own section of the help — a heading and its body, already indented.\n * Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &\n * STATS, because the loop is one story); their bodies are joined under it in\n * registry order.\n */\n readonly section?: { readonly heading: string; readonly body: string };\n}\n\n/**\n * One subcommand: everything the CLI knows about one verb, in one place.\n *\n * This is the seam the CLI was missing. A verb used to be a *shape* restated in four\n * modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a\n * `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in\n * `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every\n * flag it did not want. The refusals were the compounding cost: because no verb owned\n * its flags, every verb refused every other verb's flags by hand, so an eleventh flag\n * edited five messages.\n *\n * Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`\n * is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}\n * pattern, so totality is a compile error and every rendered list (USAGE, the sections,\n * the `map only.` prefixes in OPTIONS) is a derived view.\n *\n * The type parameters are the verb's own two shapes, and they never merge:\n *\n * @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so\n * the parse is testable on its own.\n * @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single\n * merge of flags + config + built-ins, each value carrying its provenance. Deliberately\n * one type per verb: `map` has no store, no strategy and no stdin, and a shared struct\n * whose fields are lies for half its users is not a seam, it is a coincidence.\n */\nexport interface Subcommand<I, R> {\n /** The word on the command line — and the registry key. `'smelt'` has no word. */\n readonly name: Verb;\n /**\n * The flags this verb owns. **The whole point.** Every flag outside this list is\n * refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather\n * than by prose the next verb has to write again. A flag silently ignored would be a\n * setting the user believed was in force.\n */\n readonly flags: readonly VerbFlag[];\n /**\n * What this verb *is*, as one sentence — the tail of the generated refusal, and the\n * only part of it a verb writes. It answers \"why not here?\", which is the half of\n * the old hand-written messages worth keeping; the offending flag, this verb's name\n * and the verb the flag does belong to are all derived from the registry.\n */\n readonly refusal: string;\n /** What this verb contributes to `--help`. Rendered, never hand-arranged. */\n readonly usage: SubcommandUsage;\n /**\n * The verb's own validation, over the flags it owns and the positionals it was\n * given (index 0 is the verb word itself, except for the default verb).\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\n parse(values: FlagValues, positionals: readonly string[]): I;\n /**\n * The verb's single merge of flags + config + built-ins. Precedence for this verb\n * lives here and nowhere else, so a precedence question is answered by one function\n * instead of by reading two files.\n *\n * @throws {CliUsageError} when a required value has no source — the budget, for the\n * two verbs that need one.\n */\n resolve(invocation: I, config: ConfigSource): R;\n /** Execute the resolved run straight-line, and return the exit code. */\n run(resolved: R, io: CliIo): number | Promise<number>;\n}\n"]}
1
+ {"version":3,"file":"subcommand.js","sourceRoot":"","sources":["../../../src/cli/subcommands/subcommand.ts"],"names":[],"mappings":"AAgBA,qFAAqF;AACrF,MAAM,CAAC,MAAM,YAAY,GAAG,OAAsB,CAAC","sourcesContent":["import type { LoadedConfig } from '../config.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues, VerbFlag } from './flags.ts';\n\n/**\n * The verbs `smelt` answers to. `'smelt'` is the default verb — the bare\n * `smelt <file>` / `smelt < input` run — and is the only one with no word on the\n * command line, which is why a file literally named `map` needs `./map`.\n *\n * Order is meaningful: it is the order `--help` renders every derived list in, and the\n * order a refusal names owners in. Keep it stable and append new verbs at the end.\n */\nexport type Verb =\n 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents' | 'setup' | 'doctor';\n\n/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */\nexport const DEFAULT_VERB = 'smelt' satisfies Verb;\n\n/**\n * The nearest `smelt.config.json`, loaded on demand.\n *\n * A thunk rather than a value, because two verbs must **not** load it: `init` and\n * `hooks` read the config file themselves, with their own tolerance, and a wizard you\n * run to *fix* a malformed config cannot be a wizard that refuses to start because the\n * config is malformed. Loading eagerly in the dispatcher would quietly turn that into\n * a usage error.\n */\nexport type ConfigSource = () => LoadedConfig | undefined;\n\n/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */\nexport interface SubcommandUsage {\n /**\n * The command's everyday forms, rendered in the first block of USAGE in registry\n * order. Each entry is the text *after* the CLI name.\n */\n readonly synopsis: readonly string[];\n /**\n * Forms rendered in the second block, after every command's everyday forms: the\n * round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do\n * occasionally rather than the shape of a normal run. It keeps USAGE reading\n * top-to-bottom by how often you type a thing without anyone arranging the list.\n */\n readonly occasional?: readonly string[];\n /**\n * The verb's own section of the help — a heading and its body, already indented.\n * Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &\n * STATS, because the loop is one story); their bodies are joined under it in\n * registry order.\n */\n readonly section?: { readonly heading: string; readonly body: string };\n}\n\n/**\n * One subcommand: everything the CLI knows about one verb, in one place.\n *\n * This is the seam the CLI was missing. A verb used to be a *shape* restated in four\n * modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a\n * `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in\n * `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every\n * flag it did not want. The refusals were the compounding cost: because no verb owned\n * its flags, every verb refused every other verb's flags by hand, so an eleventh flag\n * edited five messages.\n *\n * Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`\n * is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}\n * pattern, so totality is a compile error and every rendered list (USAGE, the sections,\n * the `map only.` prefixes in OPTIONS) is a derived view.\n *\n * The type parameters are the verb's own two shapes, and they never merge:\n *\n * @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so\n * the parse is testable on its own.\n * @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single\n * merge of flags + config + built-ins, each value carrying its provenance. Deliberately\n * one type per verb: `map` has no store, no strategy and no stdin, and a shared struct\n * whose fields are lies for half its users is not a seam, it is a coincidence.\n */\nexport interface Subcommand<I, R> {\n /** The word on the command line — and the registry key. `'smelt'` has no word. */\n readonly name: Verb;\n /**\n * The flags this verb owns. **The whole point.** Every flag outside this list is\n * refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather\n * than by prose the next verb has to write again. A flag silently ignored would be a\n * setting the user believed was in force.\n */\n readonly flags: readonly VerbFlag[];\n /**\n * What this verb *is*, as one sentence — the tail of the generated refusal, and the\n * only part of it a verb writes. It answers \"why not here?\", which is the half of\n * the old hand-written messages worth keeping; the offending flag, this verb's name\n * and the verb the flag does belong to are all derived from the registry.\n */\n readonly refusal: string;\n /** What this verb contributes to `--help`. Rendered, never hand-arranged. */\n readonly usage: SubcommandUsage;\n /**\n * The verb's own validation, over the flags it owns and the positionals it was\n * given (index 0 is the verb word itself, except for the default verb).\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\n parse(values: FlagValues, positionals: readonly string[]): I;\n /**\n * The verb's single merge of flags + config + built-ins. Precedence for this verb\n * lives here and nowhere else, so a precedence question is answered by one function\n * instead of by reading two files.\n *\n * @throws {CliUsageError} when a required value has no source — the budget, for the\n * two verbs that need one.\n */\n resolve(invocation: I, config: ConfigSource): R;\n /** Execute the resolved run straight-line, and return the exit code. */\n run(resolved: R, io: CliIo): number | Promise<number>;\n}\n"]}