@smeltjs/core 0.4.0 → 0.6.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 (181) hide show
  1. package/dist/apply.d.ts.map +1 -1
  2. package/dist/apply.js +8 -2
  3. package/dist/apply.js.map +1 -1
  4. package/dist/cli/bin.d.ts.map +1 -1
  5. package/dist/cli/bin.js +9 -4
  6. package/dist/cli/bin.js.map +1 -1
  7. package/dist/cli/doctor.d.ts +66 -0
  8. package/dist/cli/doctor.d.ts.map +1 -0
  9. package/dist/cli/doctor.js +137 -0
  10. package/dist/cli/doctor.js.map +1 -0
  11. package/dist/cli/hooks.d.ts +36 -2
  12. package/dist/cli/hooks.d.ts.map +1 -1
  13. package/dist/cli/hooks.js +108 -64
  14. package/dist/cli/hooks.js.map +1 -1
  15. package/dist/cli/init.d.ts.map +1 -1
  16. package/dist/cli/init.js +12 -16
  17. package/dist/cli/init.js.map +1 -1
  18. package/dist/cli/installed.d.ts +56 -0
  19. package/dist/cli/installed.d.ts.map +1 -0
  20. package/dist/cli/installed.js +105 -0
  21. package/dist/cli/installed.js.map +1 -0
  22. package/dist/cli/lava.d.ts +31 -0
  23. package/dist/cli/lava.d.ts.map +1 -0
  24. package/dist/cli/lava.js +82 -0
  25. package/dist/cli/lava.js.map +1 -0
  26. package/dist/cli/report.d.ts +10 -1
  27. package/dist/cli/report.d.ts.map +1 -1
  28. package/dist/cli/report.js +16 -1
  29. package/dist/cli/report.js.map +1 -1
  30. package/dist/cli/setup.d.ts +87 -0
  31. package/dist/cli/setup.d.ts.map +1 -0
  32. package/dist/cli/setup.js +513 -0
  33. package/dist/cli/setup.js.map +1 -0
  34. package/dist/cli/shell.d.ts +11 -4
  35. package/dist/cli/shell.d.ts.map +1 -1
  36. package/dist/cli/shell.js.map +1 -1
  37. package/dist/cli/subcommands/agents.d.ts.map +1 -1
  38. package/dist/cli/subcommands/agents.js +2 -1
  39. package/dist/cli/subcommands/agents.js.map +1 -1
  40. package/dist/cli/subcommands/doctor.d.ts +15 -0
  41. package/dist/cli/subcommands/doctor.d.ts.map +1 -0
  42. package/dist/cli/subcommands/doctor.js +43 -0
  43. package/dist/cli/subcommands/doctor.js.map +1 -0
  44. package/dist/cli/subcommands/flags.d.ts +16 -0
  45. package/dist/cli/subcommands/flags.d.ts.map +1 -1
  46. package/dist/cli/subcommands/flags.js +43 -4
  47. package/dist/cli/subcommands/flags.js.map +1 -1
  48. package/dist/cli/subcommands/hooks.d.ts.map +1 -1
  49. package/dist/cli/subcommands/hooks.js +10 -2
  50. package/dist/cli/subcommands/hooks.js.map +1 -1
  51. package/dist/cli/subcommands/init.d.ts.map +1 -1
  52. package/dist/cli/subcommands/init.js +2 -1
  53. package/dist/cli/subcommands/init.js.map +1 -1
  54. package/dist/cli/subcommands/registry.d.ts +3 -1
  55. package/dist/cli/subcommands/registry.d.ts.map +1 -1
  56. package/dist/cli/subcommands/registry.js +6 -0
  57. package/dist/cli/subcommands/registry.js.map +1 -1
  58. package/dist/cli/subcommands/setup.d.ts +23 -0
  59. package/dist/cli/subcommands/setup.d.ts.map +1 -0
  60. package/dist/cli/subcommands/setup.js +87 -0
  61. package/dist/cli/subcommands/setup.js.map +1 -0
  62. package/dist/cli/subcommands/smelt.d.ts +3 -0
  63. package/dist/cli/subcommands/smelt.d.ts.map +1 -1
  64. package/dist/cli/subcommands/smelt.js +5 -1
  65. package/dist/cli/subcommands/smelt.js.map +1 -1
  66. package/dist/cli/subcommands/stats.d.ts +8 -3
  67. package/dist/cli/subcommands/stats.d.ts.map +1 -1
  68. package/dist/cli/subcommands/stats.js +15 -5
  69. package/dist/cli/subcommands/stats.js.map +1 -1
  70. package/dist/cli/subcommands/subcommand.d.ts +1 -1
  71. package/dist/cli/subcommands/subcommand.d.ts.map +1 -1
  72. package/dist/cli/subcommands/subcommand.js.map +1 -1
  73. package/dist/cli/wizard.d.ts +72 -0
  74. package/dist/cli/wizard.d.ts.map +1 -0
  75. package/dist/cli/wizard.js +86 -0
  76. package/dist/cli/wizard.js.map +1 -0
  77. package/dist/errors.d.ts +10 -0
  78. package/dist/errors.d.ts.map +1 -1
  79. package/dist/errors.js +10 -0
  80. package/dist/errors.js.map +1 -1
  81. package/dist/harness/claude-code.d.ts.map +1 -1
  82. package/dist/harness/claude-code.js +10 -0
  83. package/dist/harness/claude-code.js.map +1 -1
  84. package/dist/harness/codex.d.ts.map +1 -1
  85. package/dist/harness/codex.js +1 -0
  86. package/dist/harness/codex.js.map +1 -1
  87. package/dist/harness/grok.d.ts.map +1 -1
  88. package/dist/harness/grok.js +4 -1
  89. package/dist/harness/grok.js.map +1 -1
  90. package/dist/harness/kilocode.d.ts.map +1 -1
  91. package/dist/harness/kilocode.js +1 -1
  92. package/dist/harness/kilocode.js.map +1 -1
  93. package/dist/harness/opencode.d.ts.map +1 -1
  94. package/dist/harness/opencode.js +10 -0
  95. package/dist/harness/opencode.js.map +1 -1
  96. package/dist/harness/profile.d.ts +23 -2
  97. package/dist/harness/profile.d.ts.map +1 -1
  98. package/dist/harness/profile.js.map +1 -1
  99. package/dist/harness/snippet.d.ts +14 -1
  100. package/dist/harness/snippet.d.ts.map +1 -1
  101. package/dist/harness/snippet.js +18 -2
  102. package/dist/harness/snippet.js.map +1 -1
  103. package/dist/hooks/focus-terms.d.ts +57 -0
  104. package/dist/hooks/focus-terms.d.ts.map +1 -0
  105. package/dist/hooks/focus-terms.js +230 -0
  106. package/dist/hooks/focus-terms.js.map +1 -0
  107. package/dist/hooks/guard-core.d.ts +8 -16
  108. package/dist/hooks/guard-core.d.ts.map +1 -1
  109. package/dist/hooks/guard-core.js +22 -120
  110. package/dist/hooks/guard-core.js.map +1 -1
  111. package/dist/index.d.ts +16 -4
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +12 -3
  114. package/dist/index.js.map +1 -1
  115. package/dist/ops/index.d.ts +3 -3
  116. package/dist/ops/index.d.ts.map +1 -1
  117. package/dist/ops/index.js +2 -2
  118. package/dist/ops/index.js.map +1 -1
  119. package/dist/ops/verbs.d.ts +57 -1
  120. package/dist/ops/verbs.d.ts.map +1 -1
  121. package/dist/ops/verbs.js +49 -1
  122. package/dist/ops/verbs.js.map +1 -1
  123. package/dist/plan/auto.d.ts +9 -2
  124. package/dist/plan/auto.d.ts.map +1 -1
  125. package/dist/plan/auto.js +14 -2
  126. package/dist/plan/auto.js.map +1 -1
  127. package/dist/plan/diff.d.ts +45 -0
  128. package/dist/plan/diff.d.ts.map +1 -0
  129. package/dist/plan/diff.js +284 -0
  130. package/dist/plan/diff.js.map +1 -0
  131. package/dist/plan/json.d.ts +37 -0
  132. package/dist/plan/json.d.ts.map +1 -0
  133. package/dist/plan/json.js +181 -0
  134. package/dist/plan/json.js.map +1 -0
  135. package/dist/plan/kind.d.ts +28 -0
  136. package/dist/plan/kind.d.ts.map +1 -0
  137. package/dist/plan/kind.js +57 -0
  138. package/dist/plan/kind.js.map +1 -0
  139. package/dist/plan/offsets.d.ts +8 -0
  140. package/dist/plan/offsets.d.ts.map +1 -0
  141. package/dist/plan/offsets.js +19 -0
  142. package/dist/plan/offsets.js.map +1 -0
  143. package/dist/plan/planners.d.ts +15 -5
  144. package/dist/plan/planners.d.ts.map +1 -1
  145. package/dist/plan/planners.js +13 -5
  146. package/dist/plan/planners.js.map +1 -1
  147. package/dist/plan/structural.d.ts.map +1 -1
  148. package/dist/plan/structural.js +106 -14
  149. package/dist/plan/structural.js.map +1 -1
  150. package/dist/retrieve.d.ts +22 -1
  151. package/dist/retrieve.d.ts.map +1 -1
  152. package/dist/retrieve.js +59 -0
  153. package/dist/retrieve.js.map +1 -1
  154. package/dist/setup/recipe.d.ts +73 -0
  155. package/dist/setup/recipe.d.ts.map +1 -0
  156. package/dist/setup/recipe.js +76 -0
  157. package/dist/setup/recipe.js.map +1 -0
  158. package/dist/smelter.d.ts +4 -0
  159. package/dist/smelter.d.ts.map +1 -1
  160. package/dist/smelter.js +4 -0
  161. package/dist/smelter.js.map +1 -1
  162. package/dist/stats.d.ts +16 -1
  163. package/dist/stats.d.ts.map +1 -1
  164. package/dist/stats.js +29 -0
  165. package/dist/stats.js.map +1 -1
  166. package/dist/store-dir.d.ts +9 -2
  167. package/dist/store-dir.d.ts.map +1 -1
  168. package/dist/store-dir.js +48 -7
  169. package/dist/store-dir.js.map +1 -1
  170. package/dist/store.d.ts +4 -2
  171. package/dist/store.d.ts.map +1 -1
  172. package/dist/store.js +12 -2
  173. package/dist/store.js.map +1 -1
  174. package/dist/text/json-edit.d.ts +15 -1
  175. package/dist/text/json-edit.d.ts.map +1 -1
  176. package/dist/text/json-edit.js +76 -5
  177. package/dist/text/json-edit.js.map +1 -1
  178. package/dist/types.d.ts +116 -2
  179. package/dist/types.d.ts.map +1 -1
  180. package/dist/types.js.map +1 -1
  181. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/hooks.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;GAQG;AAEH,gEAAgE;AAChE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAkBD,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,eAAe,EAAE,eAAe,CAoErE,CAAC","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.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/hooks.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;GAQG;AAEH,gEAAgE;AAChE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAkBD,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,eAAe,EAAE,eAAe,CA6ErE,CAAC","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,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"]}
@@ -22,6 +22,8 @@ export interface SmeltInvocation {
22
22
  */
23
23
  readonly budgetBytes?: number;
24
24
  readonly focus: readonly string[];
25
+ /** The command whose output the blob is, for focus derivation. See `--producer`. */
26
+ readonly producer?: string;
25
27
  readonly language?: DetectedLanguage;
26
28
  /** `undefined` means the flag was not given — the config default may apply. */
27
29
  readonly strategy?: Strategy;
@@ -51,6 +53,7 @@ export interface ResolvedRun {
51
53
  /** Path to read. `undefined` means stdin. Flags only; the config has no say. */
52
54
  readonly file?: string;
53
55
  readonly focus: readonly string[];
56
+ readonly producer?: string;
54
57
  readonly language?: DetectedLanguage;
55
58
  readonly json: boolean;
56
59
  }