@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":"shell.d.ts","sourceRoot":"","sources":["../../src/cli/shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,gEAAgE;AAChE,eAAO,MAAM,QAAQ,UAAU,CAAC;AAEhC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,kFAAkF;IAClF,IAAI,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACpC,sFAAsF;IACtF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CA2C9D;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI;aACf,EAAE,EAAE,CAAC;aACL,UAAU,EAAE,CAAC;aACb,KAAK,EAAE,CAAC;aACR,OAAO,EAAE,CAAC;aACV,UAAU,EAAE,CAAC;CACL,CAAC;AAEX,2FAA2F;AAC3F,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC;IAC7B,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,YAAY,CAAC;CACnC","sourcesContent":["/**\n * The CLI's edge, in one module: the name people type, where its bytes come from and\n * go, and the codes it hands back to the shell.\n *\n * These three used to live in `args.ts` and `run.ts` — the two modules that now read\n * the subcommand registry. A verb file needs all three, so leaving them there would\n * make `args.ts → subcommands/* → args.ts` a cycle, which is exactly the shape that\n * once forced the `--harness` help list to be hand-typed (see\n * `src/harness/registry.ts`). **This file imports nothing**, so every module under\n * `cli/` can read it and nothing has to be written twice to avoid a loop.\n */\n\n/** The command people type. Independent of the package name. */\nexport const CLI_NAME = 'smelt';\n\n/**\n * Where a wizard's answers come from: lines of text arriving over time, and nothing\n * more. `process.stdin` is one, a scripted `Readable.from([...])` in a test is\n * another, an async generator is a third.\n *\n * **Stated structurally, never as `NodeJS.ReadableStream`.** A `.d.ts` that names an\n * ambient namespace only typechecks inside a compilation that pulled `@types/node`\n * into its *global* scope, and a consumer building with `skipLibCheck: false` and a\n * narrowed `types` array (or no `@types/node` at its own root — the ordinary case\n * under pnpm) fails on smelt's declarations rather than on their own code. Naming the\n * node type by import does not help: TypeScript resolves `node:stream` — and bare\n * `stream` — only through the same globally-included `@types/node`. So the published\n * surface describes the shape smelt actually consumes, which needs no node types at\n * all, and {@link answerReader} is the one adapter that reads it.\n * `test/guards/packaging.test.ts` holds the shipped declarations to it.\n */\nexport type AnswerStream = AsyncIterable<string | Uint8Array>;\n\n/**\n * One wizard's answers, one line at a time, plus the release that ends the process.\n *\n * `release` is not housekeeping — it is the difference between a wizard that exits and\n * one the user has to Ctrl-C. See {@link answerReader}.\n */\nexport interface AnswerReader {\n /** The next line, without its terminator, or `undefined` once input has ended. */\n next(): Promise<string | undefined>;\n /** Stop reading and let go of the source, so nothing it owns keeps the loop alive. */\n release(): Promise<void>;\n}\n\n/**\n * Read {@link AnswerStream} as lines — and, when the wizard is done, **let go of it**.\n *\n * This exists because the obvious adapter is a trap. Wrapping the answer stream in\n * `Readable.from(...)` and handing that to `readline` reads the source through *its*\n * async iterator, and closing the readline interface or destroying the wrapper ends\n * only the wrapper: the source is left mid-`next()`, still subscribed, still holding\n * its handle. On the real CLI that source is `process.stdin`, so `smelt init` wrote\n * every file, printed `Done.` and then sat there forever — a hang only visible on an\n * open pipe or a TTY, because EOF happens to end the iteration by itself, and EOF is\n * what every scripted test hands it.\n *\n * So the source's own iterator is held here and nothing else touches it. `release`\n * calls its `return()`, which is the contract an async iterable already has for \"I am\n * finished with you\": `process.stdin`'s destroys the stream and unrefs the handle, an\n * async generator runs its `finally`, and a plain array iterator does nothing at all.\n * Crucially `release` is called between reads, never during one — an iterator awaiting\n * `next()` cannot be returned out of, which is the very state the `Readable.from`\n * wrapper left the source in.\n *\n * Bytes are decoded as UTF-8 across chunk boundaries, so a multi-byte character split\n * across two reads survives; `\\r\\n` and a final line with no terminator both behave as\n * readline did.\n */\nexport function answerReader(input: AnswerStream): AnswerReader {\n const iterator = input[Symbol.asyncIterator]();\n const decoder = new TextDecoder('utf-8');\n let pending = '';\n let ended = false;\n\n const takeLine = (): string | undefined => {\n const newline = pending.indexOf('\\n');\n if (newline === -1) return undefined;\n const line = pending.slice(0, newline);\n pending = pending.slice(newline + 1);\n return line.endsWith('\\r') ? line.slice(0, -1) : line;\n };\n\n return {\n async next(): Promise<string | undefined> {\n for (;;) {\n const line = takeLine();\n if (line !== undefined) return line;\n if (ended) {\n if (pending === '') return undefined;\n const last = pending;\n pending = '';\n return last;\n }\n const step = await iterator.next();\n if (step.done === true) {\n ended = true;\n pending += decoder.decode(); // flush a truncated multi-byte sequence\n continue;\n }\n pending +=\n typeof step.value === 'string'\n ? step.value\n : decoder.decode(step.value, { stream: true });\n }\n },\n async release(): Promise<void> {\n ended = true;\n pending = '';\n await iterator.return?.();\n },\n };\n}\n\n/**\n * Exit codes, and why there are five of them.\n *\n * A CLI that returns 0 whatever happens is the shell-level version of a stub that\n * returns `[]`: the caller cannot tell success from failure, so a pipeline built on it\n * fails silently. **`overBudget` is the load-bearing one.** A plan that did not fit is\n * not an error — smelt refused to cut the regions the caller asked to keep, which is\n * correct — but it is also not success, and a script must be able to see the\n * difference without parsing prose.\n */\nexport const EXIT = {\n ok: 0,\n overBudget: 1,\n usage: 2,\n refused: 3,\n unexpected: 4,\n} as const;\n\n/** Where the CLI's bytes come from and go. Injected so `runCli` is testable in-process. */\nexport interface CliIo {\n readonly stdout: (text: string) => void;\n readonly stderr: (text: string) => void;\n /** All of stdin, as UTF-8. @throws {CliUsageError} when nothing is piped. */\n readonly stdin: () => string;\n /** The package version, for `--version`. */\n readonly version: string;\n /**\n * Where `smelt.config.json` discovery starts, and where `init` writes. Defaults to\n * the process working directory; tests pass a temp directory to stay hermetic.\n */\n readonly cwd?: string;\n /**\n * Interactive input for `smelt init` the wizard reads answers line by line, which\n * the one-shot `stdin()` above cannot provide. `bin.ts` passes the real stdin\n * stream; tests pass a scripted one. Absent means `init` is a usage error.\n * See {@link AnswerStream} for why the type is structural.\n */\n readonly initInput?: AnswerStream;\n}\n"]}
1
+ {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/cli/shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,gEAAgE;AAChE,eAAO,MAAM,QAAQ,UAAU,CAAC;AAEhC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,kFAAkF;IAClF,IAAI,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACpC,sFAAsF;IACtF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CA2C9D;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI;aACf,EAAE,EAAE,CAAC;aACL,UAAU,EAAE,CAAC;aACb,KAAK,EAAE,CAAC;aACR,OAAO,EAAE,CAAC;aACV,UAAU,EAAE,CAAC;CACL,CAAC;AAEX,2FAA2F;AAC3F,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC;IAC7B,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,YAAY,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B","sourcesContent":["/**\n * The CLI's edge, in one module: the name people type, where its bytes come from and\n * go, and the codes it hands back to the shell.\n *\n * These three used to live in `args.ts` and `run.ts` — the two modules that now read\n * the subcommand registry. A verb file needs all three, so leaving them there would\n * make `args.ts → subcommands/* → args.ts` a cycle, which is exactly the shape that\n * once forced the `--harness` help list to be hand-typed (see\n * `src/harness/registry.ts`). **This file imports nothing**, so every module under\n * `cli/` can read it and nothing has to be written twice to avoid a loop.\n */\n\n/** The command people type. Independent of the package name. */\nexport const CLI_NAME = 'smelt';\n\n/**\n * Where a wizard's answers come from: lines of text arriving over time, and nothing\n * more. `process.stdin` is one, a scripted `Readable.from([...])` in a test is\n * another, an async generator is a third.\n *\n * **Stated structurally, never as `NodeJS.ReadableStream`.** A `.d.ts` that names an\n * ambient namespace only typechecks inside a compilation that pulled `@types/node`\n * into its *global* scope, and a consumer building with `skipLibCheck: false` and a\n * narrowed `types` array (or no `@types/node` at its own root — the ordinary case\n * under pnpm) fails on smelt's declarations rather than on their own code. Naming the\n * node type by import does not help: TypeScript resolves `node:stream` — and bare\n * `stream` — only through the same globally-included `@types/node`. So the published\n * surface describes the shape smelt actually consumes, which needs no node types at\n * all, and {@link answerReader} is the one adapter that reads it.\n * `test/guards/packaging.test.ts` holds the shipped declarations to it.\n */\nexport type AnswerStream = AsyncIterable<string | Uint8Array>;\n\n/**\n * One wizard's answers, one line at a time, plus the release that ends the process.\n *\n * `release` is not housekeeping — it is the difference between a wizard that exits and\n * one the user has to Ctrl-C. See {@link answerReader}.\n */\nexport interface AnswerReader {\n /** The next line, without its terminator, or `undefined` once input has ended. */\n next(): Promise<string | undefined>;\n /** Stop reading and let go of the source, so nothing it owns keeps the loop alive. */\n release(): Promise<void>;\n}\n\n/**\n * Read {@link AnswerStream} as lines — and, when the wizard is done, **let go of it**.\n *\n * This exists because the obvious adapter is a trap. Wrapping the answer stream in\n * `Readable.from(...)` and handing that to `readline` reads the source through *its*\n * async iterator, and closing the readline interface or destroying the wrapper ends\n * only the wrapper: the source is left mid-`next()`, still subscribed, still holding\n * its handle. On the real CLI that source is `process.stdin`, so `smelt init` wrote\n * every file, printed `Done.` and then sat there forever — a hang only visible on an\n * open pipe or a TTY, because EOF happens to end the iteration by itself, and EOF is\n * what every scripted test hands it.\n *\n * So the source's own iterator is held here and nothing else touches it. `release`\n * calls its `return()`, which is the contract an async iterable already has for \"I am\n * finished with you\": `process.stdin`'s destroys the stream and unrefs the handle, an\n * async generator runs its `finally`, and a plain array iterator does nothing at all.\n * Crucially `release` is called between reads, never during one — an iterator awaiting\n * `next()` cannot be returned out of, which is the very state the `Readable.from`\n * wrapper left the source in.\n *\n * Bytes are decoded as UTF-8 across chunk boundaries, so a multi-byte character split\n * across two reads survives; `\\r\\n` and a final line with no terminator both behave as\n * readline did.\n */\nexport function answerReader(input: AnswerStream): AnswerReader {\n const iterator = input[Symbol.asyncIterator]();\n const decoder = new TextDecoder('utf-8');\n let pending = '';\n let ended = false;\n\n const takeLine = (): string | undefined => {\n const newline = pending.indexOf('\\n');\n if (newline === -1) return undefined;\n const line = pending.slice(0, newline);\n pending = pending.slice(newline + 1);\n return line.endsWith('\\r') ? line.slice(0, -1) : line;\n };\n\n return {\n async next(): Promise<string | undefined> {\n for (;;) {\n const line = takeLine();\n if (line !== undefined) return line;\n if (ended) {\n if (pending === '') return undefined;\n const last = pending;\n pending = '';\n return last;\n }\n const step = await iterator.next();\n if (step.done === true) {\n ended = true;\n pending += decoder.decode(); // flush a truncated multi-byte sequence\n continue;\n }\n pending +=\n typeof step.value === 'string'\n ? step.value\n : decoder.decode(step.value, { stream: true });\n }\n },\n async release(): Promise<void> {\n ended = true;\n pending = '';\n await iterator.return?.();\n },\n };\n}\n\n/**\n * Exit codes, and why there are five of them.\n *\n * A CLI that returns 0 whatever happens is the shell-level version of a stub that\n * returns `[]`: the caller cannot tell success from failure, so a pipeline built on it\n * fails silently. **`overBudget` is the load-bearing one.** A plan that did not fit is\n * not an error — smelt refused to cut the regions the caller asked to keep, which is\n * correct — but it is also not success, and a script must be able to see the\n * difference without parsing prose.\n */\nexport const EXIT = {\n ok: 0,\n overBudget: 1,\n usage: 2,\n refused: 3,\n unexpected: 4,\n} as const;\n\n/** Where the CLI's bytes come from and go. Injected so `runCli` is testable in-process. */\nexport interface CliIo {\n readonly stdout: (text: string) => void;\n readonly stderr: (text: string) => void;\n /** All of stdin, as UTF-8. @throws {CliUsageError} when nothing is piped. */\n readonly stdin: () => string;\n /** The package version, for `--version`. */\n readonly version: string;\n /**\n * Where `smelt.config.json` discovery starts, and where `init` writes. Defaults to\n * the process working directory; tests pass a temp directory to stay hermetic.\n */\n readonly cwd?: string;\n /**\n * Interactive input for the wizards — `init`, `hooks`, `agents split`, `setup` —\n * which read answers line by line, which the one-shot `stdin()` above cannot\n * provide. `bin.ts` passes the real stdin stream; tests pass a scripted one.\n * Absent means a wizard verb is a usage error (or, for setup, that only `--yes`\n * can answer it). See {@link AnswerStream} for why the type is structural.\n */\n readonly initInput?: AnswerStream;\n /**\n * True when stdout is an interactive, colour-honouring terminal and `NO_COLOR` is\n * unset — the lava renderer's only switch. Absent or false, every wizard's bytes\n * are exactly what they have always been; `bin.ts` computes it once.\n */\n readonly color?: boolean;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/cli/shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,gEAAgE;AAChE,MAAM,CAAC,MAAM,QAAQ,GAAG,OAAO,CAAC;AAiChC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,YAAY,CAAC,KAAmB;IAC9C,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC;IAC/C,MAAM,OAAO,GAAG,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,KAAK,GAAG,KAAK,CAAC;IAElB,MAAM,QAAQ,GAAG,GAAuB,EAAE;QACxC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,OAAO,KAAK,CAAC,CAAC;YAAE,OAAO,SAAS,CAAC;QACrC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QACvC,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;QACrC,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxD,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,CAAC,IAAI;YACR,SAAS,CAAC;gBACR,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;gBACxB,IAAI,IAAI,KAAK,SAAS;oBAAE,OAAO,IAAI,CAAC;gBACpC,IAAI,KAAK,EAAE,CAAC;oBACV,IAAI,OAAO,KAAK,EAAE;wBAAE,OAAO,SAAS,CAAC;oBACrC,MAAM,IAAI,GAAG,OAAO,CAAC;oBACrB,OAAO,GAAG,EAAE,CAAC;oBACb,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACnC,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;oBACvB,KAAK,GAAG,IAAI,CAAC;oBACb,OAAO,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,wCAAwC;oBACrE,SAAS;gBACX,CAAC;gBACD,OAAO;oBACL,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;wBAC5B,CAAC,CAAC,IAAI,CAAC,KAAK;wBACZ,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YACrD,CAAC;QACH,CAAC;QACD,KAAK,CAAC,OAAO;YACX,KAAK,GAAG,IAAI,CAAC;YACb,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;QAC5B,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,EAAE,EAAE,CAAC;IACL,UAAU,EAAE,CAAC;IACb,KAAK,EAAE,CAAC;IACR,OAAO,EAAE,CAAC;IACV,UAAU,EAAE,CAAC;CACL,CAAC","sourcesContent":["/**\n * The CLI's edge, in one module: the name people type, where its bytes come from and\n * go, and the codes it hands back to the shell.\n *\n * These three used to live in `args.ts` and `run.ts` — the two modules that now read\n * the subcommand registry. A verb file needs all three, so leaving them there would\n * make `args.ts → subcommands/* → args.ts` a cycle, which is exactly the shape that\n * once forced the `--harness` help list to be hand-typed (see\n * `src/harness/registry.ts`). **This file imports nothing**, so every module under\n * `cli/` can read it and nothing has to be written twice to avoid a loop.\n */\n\n/** The command people type. Independent of the package name. */\nexport const CLI_NAME = 'smelt';\n\n/**\n * Where a wizard's answers come from: lines of text arriving over time, and nothing\n * more. `process.stdin` is one, a scripted `Readable.from([...])` in a test is\n * another, an async generator is a third.\n *\n * **Stated structurally, never as `NodeJS.ReadableStream`.** A `.d.ts` that names an\n * ambient namespace only typechecks inside a compilation that pulled `@types/node`\n * into its *global* scope, and a consumer building with `skipLibCheck: false` and a\n * narrowed `types` array (or no `@types/node` at its own root — the ordinary case\n * under pnpm) fails on smelt's declarations rather than on their own code. Naming the\n * node type by import does not help: TypeScript resolves `node:stream` — and bare\n * `stream` — only through the same globally-included `@types/node`. So the published\n * surface describes the shape smelt actually consumes, which needs no node types at\n * all, and {@link answerReader} is the one adapter that reads it.\n * `test/guards/packaging.test.ts` holds the shipped declarations to it.\n */\nexport type AnswerStream = AsyncIterable<string | Uint8Array>;\n\n/**\n * One wizard's answers, one line at a time, plus the release that ends the process.\n *\n * `release` is not housekeeping — it is the difference between a wizard that exits and\n * one the user has to Ctrl-C. See {@link answerReader}.\n */\nexport interface AnswerReader {\n /** The next line, without its terminator, or `undefined` once input has ended. */\n next(): Promise<string | undefined>;\n /** Stop reading and let go of the source, so nothing it owns keeps the loop alive. */\n release(): Promise<void>;\n}\n\n/**\n * Read {@link AnswerStream} as lines — and, when the wizard is done, **let go of it**.\n *\n * This exists because the obvious adapter is a trap. Wrapping the answer stream in\n * `Readable.from(...)` and handing that to `readline` reads the source through *its*\n * async iterator, and closing the readline interface or destroying the wrapper ends\n * only the wrapper: the source is left mid-`next()`, still subscribed, still holding\n * its handle. On the real CLI that source is `process.stdin`, so `smelt init` wrote\n * every file, printed `Done.` and then sat there forever — a hang only visible on an\n * open pipe or a TTY, because EOF happens to end the iteration by itself, and EOF is\n * what every scripted test hands it.\n *\n * So the source's own iterator is held here and nothing else touches it. `release`\n * calls its `return()`, which is the contract an async iterable already has for \"I am\n * finished with you\": `process.stdin`'s destroys the stream and unrefs the handle, an\n * async generator runs its `finally`, and a plain array iterator does nothing at all.\n * Crucially `release` is called between reads, never during one — an iterator awaiting\n * `next()` cannot be returned out of, which is the very state the `Readable.from`\n * wrapper left the source in.\n *\n * Bytes are decoded as UTF-8 across chunk boundaries, so a multi-byte character split\n * across two reads survives; `\\r\\n` and a final line with no terminator both behave as\n * readline did.\n */\nexport function answerReader(input: AnswerStream): AnswerReader {\n const iterator = input[Symbol.asyncIterator]();\n const decoder = new TextDecoder('utf-8');\n let pending = '';\n let ended = false;\n\n const takeLine = (): string | undefined => {\n const newline = pending.indexOf('\\n');\n if (newline === -1) return undefined;\n const line = pending.slice(0, newline);\n pending = pending.slice(newline + 1);\n return line.endsWith('\\r') ? line.slice(0, -1) : line;\n };\n\n return {\n async next(): Promise<string | undefined> {\n for (;;) {\n const line = takeLine();\n if (line !== undefined) return line;\n if (ended) {\n if (pending === '') return undefined;\n const last = pending;\n pending = '';\n return last;\n }\n const step = await iterator.next();\n if (step.done === true) {\n ended = true;\n pending += decoder.decode(); // flush a truncated multi-byte sequence\n continue;\n }\n pending +=\n typeof step.value === 'string'\n ? step.value\n : decoder.decode(step.value, { stream: true });\n }\n },\n async release(): Promise<void> {\n ended = true;\n pending = '';\n await iterator.return?.();\n },\n };\n}\n\n/**\n * Exit codes, and why there are five of them.\n *\n * A CLI that returns 0 whatever happens is the shell-level version of a stub that\n * returns `[]`: the caller cannot tell success from failure, so a pipeline built on it\n * fails silently. **`overBudget` is the load-bearing one.** A plan that did not fit is\n * not an error — smelt refused to cut the regions the caller asked to keep, which is\n * correct — but it is also not success, and a script must be able to see the\n * difference without parsing prose.\n */\nexport const EXIT = {\n ok: 0,\n overBudget: 1,\n usage: 2,\n refused: 3,\n unexpected: 4,\n} as const;\n\n/** Where the CLI's bytes come from and go. Injected so `runCli` is testable in-process. */\nexport interface CliIo {\n readonly stdout: (text: string) => void;\n readonly stderr: (text: string) => void;\n /** All of stdin, as UTF-8. @throws {CliUsageError} when nothing is piped. */\n readonly stdin: () => string;\n /** The package version, for `--version`. */\n readonly version: string;\n /**\n * Where `smelt.config.json` discovery starts, and where `init` writes. Defaults to\n * the process working directory; tests pass a temp directory to stay hermetic.\n */\n readonly cwd?: string;\n /**\n * Interactive input for `smelt init` the wizard reads answers line by line, which\n * the one-shot `stdin()` above cannot provide. `bin.ts` passes the real stdin\n * stream; tests pass a scripted one. Absent means `init` is a usage error.\n * See {@link AnswerStream} for why the type is structural.\n */\n readonly initInput?: AnswerStream;\n}\n"]}
1
+ {"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/cli/shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,gEAAgE;AAChE,MAAM,CAAC,MAAM,QAAQ,GAAG,OAAO,CAAC;AAiChC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,YAAY,CAAC,KAAmB;IAC9C,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC;IAC/C,MAAM,OAAO,GAAG,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,KAAK,GAAG,KAAK,CAAC;IAElB,MAAM,QAAQ,GAAG,GAAuB,EAAE;QACxC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,OAAO,KAAK,CAAC,CAAC;YAAE,OAAO,SAAS,CAAC;QACrC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QACvC,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;QACrC,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxD,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,CAAC,IAAI;YACR,SAAS,CAAC;gBACR,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;gBACxB,IAAI,IAAI,KAAK,SAAS;oBAAE,OAAO,IAAI,CAAC;gBACpC,IAAI,KAAK,EAAE,CAAC;oBACV,IAAI,OAAO,KAAK,EAAE;wBAAE,OAAO,SAAS,CAAC;oBACrC,MAAM,IAAI,GAAG,OAAO,CAAC;oBACrB,OAAO,GAAG,EAAE,CAAC;oBACb,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACnC,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;oBACvB,KAAK,GAAG,IAAI,CAAC;oBACb,OAAO,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,wCAAwC;oBACrE,SAAS;gBACX,CAAC;gBACD,OAAO;oBACL,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;wBAC5B,CAAC,CAAC,IAAI,CAAC,KAAK;wBACZ,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YACrD,CAAC;QACH,CAAC;QACD,KAAK,CAAC,OAAO;YACX,KAAK,GAAG,IAAI,CAAC;YACb,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;QAC5B,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,EAAE,EAAE,CAAC;IACL,UAAU,EAAE,CAAC;IACb,KAAK,EAAE,CAAC;IACR,OAAO,EAAE,CAAC;IACV,UAAU,EAAE,CAAC;CACL,CAAC","sourcesContent":["/**\n * The CLI's edge, in one module: the name people type, where its bytes come from and\n * go, and the codes it hands back to the shell.\n *\n * These three used to live in `args.ts` and `run.ts` — the two modules that now read\n * the subcommand registry. A verb file needs all three, so leaving them there would\n * make `args.ts → subcommands/* → args.ts` a cycle, which is exactly the shape that\n * once forced the `--harness` help list to be hand-typed (see\n * `src/harness/registry.ts`). **This file imports nothing**, so every module under\n * `cli/` can read it and nothing has to be written twice to avoid a loop.\n */\n\n/** The command people type. Independent of the package name. */\nexport const CLI_NAME = 'smelt';\n\n/**\n * Where a wizard's answers come from: lines of text arriving over time, and nothing\n * more. `process.stdin` is one, a scripted `Readable.from([...])` in a test is\n * another, an async generator is a third.\n *\n * **Stated structurally, never as `NodeJS.ReadableStream`.** A `.d.ts` that names an\n * ambient namespace only typechecks inside a compilation that pulled `@types/node`\n * into its *global* scope, and a consumer building with `skipLibCheck: false` and a\n * narrowed `types` array (or no `@types/node` at its own root — the ordinary case\n * under pnpm) fails on smelt's declarations rather than on their own code. Naming the\n * node type by import does not help: TypeScript resolves `node:stream` — and bare\n * `stream` — only through the same globally-included `@types/node`. So the published\n * surface describes the shape smelt actually consumes, which needs no node types at\n * all, and {@link answerReader} is the one adapter that reads it.\n * `test/guards/packaging.test.ts` holds the shipped declarations to it.\n */\nexport type AnswerStream = AsyncIterable<string | Uint8Array>;\n\n/**\n * One wizard's answers, one line at a time, plus the release that ends the process.\n *\n * `release` is not housekeeping — it is the difference between a wizard that exits and\n * one the user has to Ctrl-C. See {@link answerReader}.\n */\nexport interface AnswerReader {\n /** The next line, without its terminator, or `undefined` once input has ended. */\n next(): Promise<string | undefined>;\n /** Stop reading and let go of the source, so nothing it owns keeps the loop alive. */\n release(): Promise<void>;\n}\n\n/**\n * Read {@link AnswerStream} as lines — and, when the wizard is done, **let go of it**.\n *\n * This exists because the obvious adapter is a trap. Wrapping the answer stream in\n * `Readable.from(...)` and handing that to `readline` reads the source through *its*\n * async iterator, and closing the readline interface or destroying the wrapper ends\n * only the wrapper: the source is left mid-`next()`, still subscribed, still holding\n * its handle. On the real CLI that source is `process.stdin`, so `smelt init` wrote\n * every file, printed `Done.` and then sat there forever — a hang only visible on an\n * open pipe or a TTY, because EOF happens to end the iteration by itself, and EOF is\n * what every scripted test hands it.\n *\n * So the source's own iterator is held here and nothing else touches it. `release`\n * calls its `return()`, which is the contract an async iterable already has for \"I am\n * finished with you\": `process.stdin`'s destroys the stream and unrefs the handle, an\n * async generator runs its `finally`, and a plain array iterator does nothing at all.\n * Crucially `release` is called between reads, never during one — an iterator awaiting\n * `next()` cannot be returned out of, which is the very state the `Readable.from`\n * wrapper left the source in.\n *\n * Bytes are decoded as UTF-8 across chunk boundaries, so a multi-byte character split\n * across two reads survives; `\\r\\n` and a final line with no terminator both behave as\n * readline did.\n */\nexport function answerReader(input: AnswerStream): AnswerReader {\n const iterator = input[Symbol.asyncIterator]();\n const decoder = new TextDecoder('utf-8');\n let pending = '';\n let ended = false;\n\n const takeLine = (): string | undefined => {\n const newline = pending.indexOf('\\n');\n if (newline === -1) return undefined;\n const line = pending.slice(0, newline);\n pending = pending.slice(newline + 1);\n return line.endsWith('\\r') ? line.slice(0, -1) : line;\n };\n\n return {\n async next(): Promise<string | undefined> {\n for (;;) {\n const line = takeLine();\n if (line !== undefined) return line;\n if (ended) {\n if (pending === '') return undefined;\n const last = pending;\n pending = '';\n return last;\n }\n const step = await iterator.next();\n if (step.done === true) {\n ended = true;\n pending += decoder.decode(); // flush a truncated multi-byte sequence\n continue;\n }\n pending +=\n typeof step.value === 'string'\n ? step.value\n : decoder.decode(step.value, { stream: true });\n }\n },\n async release(): Promise<void> {\n ended = true;\n pending = '';\n await iterator.return?.();\n },\n };\n}\n\n/**\n * Exit codes, and why there are five of them.\n *\n * A CLI that returns 0 whatever happens is the shell-level version of a stub that\n * returns `[]`: the caller cannot tell success from failure, so a pipeline built on it\n * fails silently. **`overBudget` is the load-bearing one.** A plan that did not fit is\n * not an error — smelt refused to cut the regions the caller asked to keep, which is\n * correct — but it is also not success, and a script must be able to see the\n * difference without parsing prose.\n */\nexport const EXIT = {\n ok: 0,\n overBudget: 1,\n usage: 2,\n refused: 3,\n unexpected: 4,\n} as const;\n\n/** Where the CLI's bytes come from and go. Injected so `runCli` is testable in-process. */\nexport interface CliIo {\n readonly stdout: (text: string) => void;\n readonly stderr: (text: string) => void;\n /** All of stdin, as UTF-8. @throws {CliUsageError} when nothing is piped. */\n readonly stdin: () => string;\n /** The package version, for `--version`. */\n readonly version: string;\n /**\n * Where `smelt.config.json` discovery starts, and where `init` writes. Defaults to\n * the process working directory; tests pass a temp directory to stay hermetic.\n */\n readonly cwd?: string;\n /**\n * Interactive input for the wizards — `init`, `hooks`, `agents split`, `setup` —\n * which read answers line by line, which the one-shot `stdin()` above cannot\n * provide. `bin.ts` passes the real stdin stream; tests pass a scripted one.\n * Absent means a wizard verb is a usage error (or, for setup, that only `--yes`\n * can answer it). See {@link AnswerStream} for why the type is structural.\n */\n readonly initInput?: AnswerStream;\n /**\n * True when stdout is an interactive, colour-honouring terminal and `NO_COLOR` is\n * unset — the lava renderer's only switch. Absent or false, every wizard's bytes\n * are exactly what they have always been; `bin.ts` computes it once.\n */\n readonly color?: boolean;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/agents.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAK7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAMjD,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;;;;;;;;;GAgBG;AAEH,mDAAmD;AACnD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,gFAAgF;IAChF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,wBAAwB,CAAC;AAE5D,8CAA8C;AAC9C,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;CACnC;AAED,eAAO,MAAM,aAAa,EAAE,UAAU,CAAC,gBAAgB,EAAE,iBAAiB,CAsFzE,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,gBAAgB,EAC5B,MAAM,EAAE,YAAY,GAAG,SAAS,GAC/B,iBAAiB,CASnB","sourcesContent":["import process from 'node:process';\n\nimport { lintAgents, overBudgetBytes } from '../../agents/lint.ts';\nimport type { AgentsLintReport } from '../../agents/lint.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { readTree } from '../../ops/inputs.ts';\nimport { runAgentsSplit } from '../agents.ts';\nimport { CONFIG_FILE_NAME } from '../config.ts';\nimport type { LoadedConfig } from '../config.ts';\nimport { formatAgentsReport } from '../report.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt agents` — the instruction files an agent loads on **every** request.\n *\n * The verb exists because `AGENTS.md` is the one blob in a repository that is paid for\n * on every single request, relevant or not, and nothing measures it. That is smelt's\n * own subject, so the fit is exact — and the two actions divide along the line smelt\n * divides everything else along:\n *\n * - **`lint`** measures and explains. It never edits.\n * - **`split`** edits, mechanically, under `smelt init`'s consent discipline — and\n * says plainly which half of the refactor it refused to do.\n *\n * There is no `smelt agents init`, and there will not be one: the guide this verb\n * lints against says in as many words never to auto-generate an AGENTS.md, and a tool\n * that built the thing its own source warns against would be worth less than no tool\n * (ruling R1).\n */\n\n/** `smelt agents <lint|split> [path]` — parsed. */\nexport interface AgentsInvocation {\n readonly mode: 'agents';\n readonly action: 'lint' | 'split';\n /** The directory to read. Defaults to `.`; always a directory, never a file. */\n readonly dir: string;\n /** `lint` only: any finding exits 1. */\n readonly strict: boolean;\n /** `lint` only: the versioned envelope instead of the text report. */\n readonly json: boolean;\n}\n\n/** Everything one `smelt agents lint` run needs, fully merged. */\nexport interface ResolvedAgentsRun {\n readonly action: 'lint' | 'split';\n readonly dir: string;\n readonly strict: boolean;\n readonly json: boolean;\n /**\n * The user's byte ceiling for the merged set, from `smelt.config.json`.\n *\n * **Absent means unbudgeted, and there is no built-in fallback** (ruling R2). This\n * field carries no `budgetSource`, unlike {@link ResolvedRun}'s, precisely because\n * there is only one source it can have come from: a budget here is always the\n * user's, never a flag and never smelt's.\n */\n readonly budgetBytes?: number;\n}\n\n/**\n * The `smelt agents lint --json` envelope format. Its own version line, like the map\n * and stats envelopes: a lint report carries rule ids and byte counts per level, a\n * structure that has to be able to move without dragging the other two with it.\n */\nexport const CLI_AGENTS_JSON_FORMAT = 'smelt-agents-cli/v1';\n\n/** What `smelt agents lint --json` prints. */\nexport interface CliAgentsJsonEnvelope {\n readonly format: string;\n /** The {@link AgentsLintReport} exactly as `lintAgents` returned it. */\n readonly report: AgentsLintReport;\n}\n\nexport const agentsCommand: Subcommand<AgentsInvocation, ResolvedAgentsRun> = {\n name: 'agents',\n flags: ['strict', 'json'],\n refusal:\n `agents reads the instruction files in a tree and reports on them — it plans no ` +\n `elisions, stores nothing, and has no budget of its own to take: the only budget ` +\n `it honours is agents.budgetBytes in ${CONFIG_FILE_NAME}, which is yours.`,\n usage: {\n synopsis: ['agents lint [dir] [--strict] [--json]'],\n occasional: ['agents split [dir]'],\n section: {\n heading: 'AGENTS',\n body:\n ` ${CLI_NAME} agents lint audits the instruction files an agent loads on every\\n` +\n ` request — every AGENTS.md, CLAUDE.md and GEMINI.md in the tree, because a\\n` +\n ` nested one merges with the root. A merge runs up the tree and never across\\n` +\n ` it, so it reports bytes per level, the per-request worst case (the heaviest\\n` +\n ` level plus its ancestors — what one agent actually loads) and the whole-tree\\n` +\n ` surface, plus an imperative count labelled a heuristic. Then eight advisory\\n` +\n ` rules: dead-path and dead-link (path-like tokens and links resolved against\\n` +\n ` the real tree — the check nobody else makes, and the reason to run this in\\n` +\n ` CI), forcing-language, structure-dump, generated-boilerplate, language-rule,\\n` +\n ` mirror-drift and restated-at-level. Every finding carries a stable rule id\\n` +\n ` and a sentence citing the guide it applies\\n` +\n ` (aihero.dev/a-complete-guide-to-agents-md). Findings exit 0; --strict makes\\n` +\n ` any finding exit 1. There is no built-in size limit: set agents.budgetBytes\\n` +\n ` in ${CONFIG_FILE_NAME} and exceeding it exits 1, as every other ${CLI_NAME}\\n` +\n ` budget does. The guide's own \"~150-200 instructions\" figure is printed as a\\n` +\n ` citation and compared to nothing.\\n` +\n `\\n` +\n ` ${CLI_NAME} agents split does the MECHANICAL half of the guide's refactor:\\n` +\n ` partition the root file by ## heading into one Markdown file per section\\n` +\n ` under docs/, rewrite the relative links that moved a directory deeper, and\\n` +\n ` leave a link list behind. Same discipline as init and hooks — every file\\n` +\n ` listed, one confirm, an existing file never overwritten without a per-file\\n` +\n ` yes. It does NOT decide which sections are essential: that is a reading of\\n` +\n ` your project, so it needs a model, and ${CLI_NAME} has none by law. Instead it\\n` +\n ` prints the guide's own refactor prompt with your real section headings filled\\n` +\n ` in, for you to hand to your own agent. That seam is the point, not an\\n` +\n ` omission. There is no \\`${CLI_NAME} agents init\\`: the guide says never to\\n` +\n ` auto-generate an AGENTS.md.`,\n },\n },\n\n /**\n * An action, and at most one directory. `lint` defaults to `.` because the everyday\n * invocation is \"lint here\" — unlike `map`, which is always aimed somewhere.\n */\n parse(values: FlagValues, positionals: readonly string[]): AgentsInvocation {\n const action = positionals[1];\n if (action !== 'lint' && action !== 'split') {\n throw new CliUsageError(\n `${CLI_NAME}: agents needs an action — lint or split.\\n` +\n ` ${CLI_NAME} agents lint [dir] [--strict] [--json]\\n` +\n ` ${CLI_NAME} agents split [dir]`,\n );\n }\n if (positionals.length > 3) {\n throw new CliUsageError(\n `${CLI_NAME}: agents ${action} takes at most one directory, got ` +\n `${String(positionals.length - 2)} (${positionals.slice(2).join(', ')}).`,\n );\n }\n if (action === 'split' && (values.strict === true || values.json === true)) {\n throw new CliUsageError(\n `${CLI_NAME}: --strict and --json belong to \\`${CLI_NAME} agents lint\\`. ` +\n `split is interactive and writes files; a report format and a CI exit code ` +\n `have nothing to act on there.`,\n );\n }\n return {\n mode: 'agents',\n action,\n dir: positionals[2] ?? '.',\n strict: values.strict === true,\n json: values.json === true,\n };\n },\n\n resolve(invocation: AgentsInvocation, config: ConfigSource): ResolvedAgentsRun {\n return resolveAgentsRun(invocation, config());\n },\n\n run(resolved: ResolvedAgentsRun, io: CliIo): number | Promise<number> {\n return resolved.action === 'lint' ? runLint(resolved, io) : runSplit(resolved, io);\n },\n};\n\n/**\n * Merge one `'agents'` invocation with the loaded config.\n *\n * The whole merge is one key, and it has **no flag** on purpose. A `--budget` here\n * would be a ceiling typed on the command line, which is a ceiling nobody in the\n * repository has agreed to; the number that matters is the one committed in\n * `smelt.config.json` where CI and every contributor read the same value. So unlike\n * every other verb with a budget, this one cannot refuse for a missing budget: absent\n * is a legitimate state, and it means \"measure, do not fail\".\n */\nexport function resolveAgentsRun(\n invocation: AgentsInvocation,\n config: LoadedConfig | undefined,\n): ResolvedAgentsRun {\n const budgetBytes = config?.config.agents?.budgetBytes;\n return {\n action: invocation.action,\n dir: invocation.dir,\n strict: invocation.strict,\n json: invocation.json,\n ...(budgetBytes === undefined ? {} : { budgetBytes }),\n };\n}\n\n/**\n * One `smelt agents lint` run: prove the target is a tree, lint it, render, exit.\n *\n * Three exit codes, and the reasoning for each:\n *\n * - **1 over the user's budget.** Identical to a `smelt` run that did not fit, and\n * identical for the same reason: not an error, not a success, and a script must be\n * able to tell without parsing prose. It applies with or without `--strict`,\n * because the number came from the repository rather than from smelt.\n * - **1 with `--strict` and any finding.** The CI switch. Off by default because the\n * rules are advisory heuristics.\n * - **0** otherwise, findings and all.\n */\nfunction runLint(run: ResolvedAgentsRun, io: CliIo): number {\n const tree = readTree(run.dir, run.dir, {\n tree: 'agents lint',\n file: `\\`${CLI_NAME} <file>\\``,\n });\n if (!tree.ok) throw new CliUsageError(`${CLI_NAME}: ${tree.refusal}`);\n\n const report = lintAgents({\n root: tree.value,\n ...(run.budgetBytes === undefined ? {} : { budgetBytes: run.budgetBytes }),\n });\n\n if (run.json) {\n const envelope: CliAgentsJsonEnvelope = { format: CLI_AGENTS_JSON_FORMAT, report };\n io.stdout(`${JSON.stringify(envelope, null, 2)}\\n`);\n } else {\n io.stdout(formatAgentsReport(report, { source: run.dir, strict: run.strict }));\n }\n\n if (overBudgetBytes(report) !== undefined) return EXIT.overBudget;\n if (run.strict && report.findings.length > 0) return EXIT.overBudget;\n return EXIT.ok;\n}\n\n/** `smelt agents split` is interactive like `init` and `hooks`, and refuses without a stream. */\nasync function runSplit(run: ResolvedAgentsRun, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: agents split is interactive — it lists every file it would write ` +\n `and asks before each one — and this invocation has no interactive input ` +\n `stream. Run \\`${CLI_NAME} agents split\\` from a terminal.`,\n );\n }\n return await runAgentsSplit({\n input: io.initInput,\n output: io.stdout,\n cwd: io.cwd ?? process.cwd(),\n dir: run.dir,\n });\n}\n"]}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/agents.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAM7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAMjD,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;;;;;;;;;GAgBG;AAEH,mDAAmD;AACnD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,gFAAgF;IAChF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,wBAAwB,CAAC;AAE5D,8CAA8C;AAC9C,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;CACnC;AAED,eAAO,MAAM,aAAa,EAAE,UAAU,CAAC,gBAAgB,EAAE,iBAAiB,CAsFzE,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,gBAAgB,EAC5B,MAAM,EAAE,YAAY,GAAG,SAAS,GAC/B,iBAAiB,CASnB","sourcesContent":["import process from 'node:process';\n\nimport { lintAgents, overBudgetBytes } from '../../agents/lint.ts';\nimport type { AgentsLintReport } from '../../agents/lint.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { readTree } from '../../ops/inputs.ts';\nimport { colorize } from '../lava.ts';\nimport { runAgentsSplit } from '../agents.ts';\nimport { CONFIG_FILE_NAME } from '../config.ts';\nimport type { LoadedConfig } from '../config.ts';\nimport { formatAgentsReport } from '../report.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt agents` — the instruction files an agent loads on **every** request.\n *\n * The verb exists because `AGENTS.md` is the one blob in a repository that is paid for\n * on every single request, relevant or not, and nothing measures it. That is smelt's\n * own subject, so the fit is exact — and the two actions divide along the line smelt\n * divides everything else along:\n *\n * - **`lint`** measures and explains. It never edits.\n * - **`split`** edits, mechanically, under `smelt init`'s consent discipline — and\n * says plainly which half of the refactor it refused to do.\n *\n * There is no `smelt agents init`, and there will not be one: the guide this verb\n * lints against says in as many words never to auto-generate an AGENTS.md, and a tool\n * that built the thing its own source warns against would be worth less than no tool\n * (ruling R1).\n */\n\n/** `smelt agents <lint|split> [path]` — parsed. */\nexport interface AgentsInvocation {\n readonly mode: 'agents';\n readonly action: 'lint' | 'split';\n /** The directory to read. Defaults to `.`; always a directory, never a file. */\n readonly dir: string;\n /** `lint` only: any finding exits 1. */\n readonly strict: boolean;\n /** `lint` only: the versioned envelope instead of the text report. */\n readonly json: boolean;\n}\n\n/** Everything one `smelt agents lint` run needs, fully merged. */\nexport interface ResolvedAgentsRun {\n readonly action: 'lint' | 'split';\n readonly dir: string;\n readonly strict: boolean;\n readonly json: boolean;\n /**\n * The user's byte ceiling for the merged set, from `smelt.config.json`.\n *\n * **Absent means unbudgeted, and there is no built-in fallback** (ruling R2). This\n * field carries no `budgetSource`, unlike {@link ResolvedRun}'s, precisely because\n * there is only one source it can have come from: a budget here is always the\n * user's, never a flag and never smelt's.\n */\n readonly budgetBytes?: number;\n}\n\n/**\n * The `smelt agents lint --json` envelope format. Its own version line, like the map\n * and stats envelopes: a lint report carries rule ids and byte counts per level, a\n * structure that has to be able to move without dragging the other two with it.\n */\nexport const CLI_AGENTS_JSON_FORMAT = 'smelt-agents-cli/v1';\n\n/** What `smelt agents lint --json` prints. */\nexport interface CliAgentsJsonEnvelope {\n readonly format: string;\n /** The {@link AgentsLintReport} exactly as `lintAgents` returned it. */\n readonly report: AgentsLintReport;\n}\n\nexport const agentsCommand: Subcommand<AgentsInvocation, ResolvedAgentsRun> = {\n name: 'agents',\n flags: ['strict', 'json'],\n refusal:\n `agents reads the instruction files in a tree and reports on them — it plans no ` +\n `elisions, stores nothing, and has no budget of its own to take: the only budget ` +\n `it honours is agents.budgetBytes in ${CONFIG_FILE_NAME}, which is yours.`,\n usage: {\n synopsis: ['agents lint [dir] [--strict] [--json]'],\n occasional: ['agents split [dir]'],\n section: {\n heading: 'AGENTS',\n body:\n ` ${CLI_NAME} agents lint audits the instruction files an agent loads on every\\n` +\n ` request — every AGENTS.md, CLAUDE.md and GEMINI.md in the tree, because a\\n` +\n ` nested one merges with the root. A merge runs up the tree and never across\\n` +\n ` it, so it reports bytes per level, the per-request worst case (the heaviest\\n` +\n ` level plus its ancestors — what one agent actually loads) and the whole-tree\\n` +\n ` surface, plus an imperative count labelled a heuristic. Then eight advisory\\n` +\n ` rules: dead-path and dead-link (path-like tokens and links resolved against\\n` +\n ` the real tree — the check nobody else makes, and the reason to run this in\\n` +\n ` CI), forcing-language, structure-dump, generated-boilerplate, language-rule,\\n` +\n ` mirror-drift and restated-at-level. Every finding carries a stable rule id\\n` +\n ` and a sentence citing the guide it applies\\n` +\n ` (aihero.dev/a-complete-guide-to-agents-md). Findings exit 0; --strict makes\\n` +\n ` any finding exit 1. There is no built-in size limit: set agents.budgetBytes\\n` +\n ` in ${CONFIG_FILE_NAME} and exceeding it exits 1, as every other ${CLI_NAME}\\n` +\n ` budget does. The guide's own \"~150-200 instructions\" figure is printed as a\\n` +\n ` citation and compared to nothing.\\n` +\n `\\n` +\n ` ${CLI_NAME} agents split does the MECHANICAL half of the guide's refactor:\\n` +\n ` partition the root file by ## heading into one Markdown file per section\\n` +\n ` under docs/, rewrite the relative links that moved a directory deeper, and\\n` +\n ` leave a link list behind. Same discipline as init and hooks — every file\\n` +\n ` listed, one confirm, an existing file never overwritten without a per-file\\n` +\n ` yes. It does NOT decide which sections are essential: that is a reading of\\n` +\n ` your project, so it needs a model, and ${CLI_NAME} has none by law. Instead it\\n` +\n ` prints the guide's own refactor prompt with your real section headings filled\\n` +\n ` in, for you to hand to your own agent. That seam is the point, not an\\n` +\n ` omission. There is no \\`${CLI_NAME} agents init\\`: the guide says never to\\n` +\n ` auto-generate an AGENTS.md.`,\n },\n },\n\n /**\n * An action, and at most one directory. `lint` defaults to `.` because the everyday\n * invocation is \"lint here\" — unlike `map`, which is always aimed somewhere.\n */\n parse(values: FlagValues, positionals: readonly string[]): AgentsInvocation {\n const action = positionals[1];\n if (action !== 'lint' && action !== 'split') {\n throw new CliUsageError(\n `${CLI_NAME}: agents needs an action — lint or split.\\n` +\n ` ${CLI_NAME} agents lint [dir] [--strict] [--json]\\n` +\n ` ${CLI_NAME} agents split [dir]`,\n );\n }\n if (positionals.length > 3) {\n throw new CliUsageError(\n `${CLI_NAME}: agents ${action} takes at most one directory, got ` +\n `${String(positionals.length - 2)} (${positionals.slice(2).join(', ')}).`,\n );\n }\n if (action === 'split' && (values.strict === true || values.json === true)) {\n throw new CliUsageError(\n `${CLI_NAME}: --strict and --json belong to \\`${CLI_NAME} agents lint\\`. ` +\n `split is interactive and writes files; a report format and a CI exit code ` +\n `have nothing to act on there.`,\n );\n }\n return {\n mode: 'agents',\n action,\n dir: positionals[2] ?? '.',\n strict: values.strict === true,\n json: values.json === true,\n };\n },\n\n resolve(invocation: AgentsInvocation, config: ConfigSource): ResolvedAgentsRun {\n return resolveAgentsRun(invocation, config());\n },\n\n run(resolved: ResolvedAgentsRun, io: CliIo): number | Promise<number> {\n return resolved.action === 'lint' ? runLint(resolved, io) : runSplit(resolved, io);\n },\n};\n\n/**\n * Merge one `'agents'` invocation with the loaded config.\n *\n * The whole merge is one key, and it has **no flag** on purpose. A `--budget` here\n * would be a ceiling typed on the command line, which is a ceiling nobody in the\n * repository has agreed to; the number that matters is the one committed in\n * `smelt.config.json` where CI and every contributor read the same value. So unlike\n * every other verb with a budget, this one cannot refuse for a missing budget: absent\n * is a legitimate state, and it means \"measure, do not fail\".\n */\nexport function resolveAgentsRun(\n invocation: AgentsInvocation,\n config: LoadedConfig | undefined,\n): ResolvedAgentsRun {\n const budgetBytes = config?.config.agents?.budgetBytes;\n return {\n action: invocation.action,\n dir: invocation.dir,\n strict: invocation.strict,\n json: invocation.json,\n ...(budgetBytes === undefined ? {} : { budgetBytes }),\n };\n}\n\n/**\n * One `smelt agents lint` run: prove the target is a tree, lint it, render, exit.\n *\n * Three exit codes, and the reasoning for each:\n *\n * - **1 over the user's budget.** Identical to a `smelt` run that did not fit, and\n * identical for the same reason: not an error, not a success, and a script must be\n * able to tell without parsing prose. It applies with or without `--strict`,\n * because the number came from the repository rather than from smelt.\n * - **1 with `--strict` and any finding.** The CI switch. Off by default because the\n * rules are advisory heuristics.\n * - **0** otherwise, findings and all.\n */\nfunction runLint(run: ResolvedAgentsRun, io: CliIo): number {\n const tree = readTree(run.dir, run.dir, {\n tree: 'agents lint',\n file: `\\`${CLI_NAME} <file>\\``,\n });\n if (!tree.ok) throw new CliUsageError(`${CLI_NAME}: ${tree.refusal}`);\n\n const report = lintAgents({\n root: tree.value,\n ...(run.budgetBytes === undefined ? {} : { budgetBytes: run.budgetBytes }),\n });\n\n if (run.json) {\n const envelope: CliAgentsJsonEnvelope = { format: CLI_AGENTS_JSON_FORMAT, report };\n io.stdout(`${JSON.stringify(envelope, null, 2)}\\n`);\n } else {\n io.stdout(formatAgentsReport(report, { source: run.dir, strict: run.strict }));\n }\n\n if (overBudgetBytes(report) !== undefined) return EXIT.overBudget;\n if (run.strict && report.findings.length > 0) return EXIT.overBudget;\n return EXIT.ok;\n}\n\n/** `smelt agents split` is interactive like `init` and `hooks`, and refuses without a stream. */\nasync function runSplit(run: ResolvedAgentsRun, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: agents split is interactive — it lists every file it would write ` +\n `and asks before each one — and this invocation has no interactive input ` +\n `stream. Run \\`${CLI_NAME} agents split\\` from a terminal.`,\n );\n }\n return await runAgentsSplit({\n input: io.initInput,\n output: (text) => io.stdout(colorize(text, io.color === true)),\n cwd: io.cwd ?? process.cwd(),\n dir: run.dir,\n });\n}\n"]}
@@ -2,6 +2,7 @@ import process from 'node:process';
2
2
  import { lintAgents, overBudgetBytes } from '../../agents/lint.js';
3
3
  import { CliUsageError } from '../../errors.js';
4
4
  import { readTree } from '../../ops/inputs.js';
5
+ import { colorize } from '../lava.js';
5
6
  import { runAgentsSplit } from '../agents.js';
6
7
  import { CONFIG_FILE_NAME } from '../config.js';
7
8
  import { formatAgentsReport } from '../report.js';
@@ -154,7 +155,7 @@ async function runSplit(run, io) {
154
155
  }
155
156
  return await runAgentsSplit({
156
157
  input: io.initInput,
157
- output: io.stdout,
158
+ output: (text) => io.stdout(colorize(text, io.color === true)),
158
159
  cwd: io.cwd ?? process.cwd(),
159
160
  dir: run.dir,
160
161
  });
@@ -1 +1 @@
1
- {"version":3,"file":"agents.js","sourceRoot":"","sources":["../../../src/cli/subcommands/agents.ts"],"names":[],"mappings":"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","sourcesContent":["import process from 'node:process';\n\nimport { lintAgents, overBudgetBytes } from '../../agents/lint.ts';\nimport type { AgentsLintReport } from '../../agents/lint.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { readTree } from '../../ops/inputs.ts';\nimport { runAgentsSplit } from '../agents.ts';\nimport { CONFIG_FILE_NAME } from '../config.ts';\nimport type { LoadedConfig } from '../config.ts';\nimport { formatAgentsReport } from '../report.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt agents` — the instruction files an agent loads on **every** request.\n *\n * The verb exists because `AGENTS.md` is the one blob in a repository that is paid for\n * on every single request, relevant or not, and nothing measures it. That is smelt's\n * own subject, so the fit is exact — and the two actions divide along the line smelt\n * divides everything else along:\n *\n * - **`lint`** measures and explains. It never edits.\n * - **`split`** edits, mechanically, under `smelt init`'s consent discipline — and\n * says plainly which half of the refactor it refused to do.\n *\n * There is no `smelt agents init`, and there will not be one: the guide this verb\n * lints against says in as many words never to auto-generate an AGENTS.md, and a tool\n * that built the thing its own source warns against would be worth less than no tool\n * (ruling R1).\n */\n\n/** `smelt agents <lint|split> [path]` — parsed. */\nexport interface AgentsInvocation {\n readonly mode: 'agents';\n readonly action: 'lint' | 'split';\n /** The directory to read. Defaults to `.`; always a directory, never a file. */\n readonly dir: string;\n /** `lint` only: any finding exits 1. */\n readonly strict: boolean;\n /** `lint` only: the versioned envelope instead of the text report. */\n readonly json: boolean;\n}\n\n/** Everything one `smelt agents lint` run needs, fully merged. */\nexport interface ResolvedAgentsRun {\n readonly action: 'lint' | 'split';\n readonly dir: string;\n readonly strict: boolean;\n readonly json: boolean;\n /**\n * The user's byte ceiling for the merged set, from `smelt.config.json`.\n *\n * **Absent means unbudgeted, and there is no built-in fallback** (ruling R2). This\n * field carries no `budgetSource`, unlike {@link ResolvedRun}'s, precisely because\n * there is only one source it can have come from: a budget here is always the\n * user's, never a flag and never smelt's.\n */\n readonly budgetBytes?: number;\n}\n\n/**\n * The `smelt agents lint --json` envelope format. Its own version line, like the map\n * and stats envelopes: a lint report carries rule ids and byte counts per level, a\n * structure that has to be able to move without dragging the other two with it.\n */\nexport const CLI_AGENTS_JSON_FORMAT = 'smelt-agents-cli/v1';\n\n/** What `smelt agents lint --json` prints. */\nexport interface CliAgentsJsonEnvelope {\n readonly format: string;\n /** The {@link AgentsLintReport} exactly as `lintAgents` returned it. */\n readonly report: AgentsLintReport;\n}\n\nexport const agentsCommand: Subcommand<AgentsInvocation, ResolvedAgentsRun> = {\n name: 'agents',\n flags: ['strict', 'json'],\n refusal:\n `agents reads the instruction files in a tree and reports on them — it plans no ` +\n `elisions, stores nothing, and has no budget of its own to take: the only budget ` +\n `it honours is agents.budgetBytes in ${CONFIG_FILE_NAME}, which is yours.`,\n usage: {\n synopsis: ['agents lint [dir] [--strict] [--json]'],\n occasional: ['agents split [dir]'],\n section: {\n heading: 'AGENTS',\n body:\n ` ${CLI_NAME} agents lint audits the instruction files an agent loads on every\\n` +\n ` request — every AGENTS.md, CLAUDE.md and GEMINI.md in the tree, because a\\n` +\n ` nested one merges with the root. A merge runs up the tree and never across\\n` +\n ` it, so it reports bytes per level, the per-request worst case (the heaviest\\n` +\n ` level plus its ancestors — what one agent actually loads) and the whole-tree\\n` +\n ` surface, plus an imperative count labelled a heuristic. Then eight advisory\\n` +\n ` rules: dead-path and dead-link (path-like tokens and links resolved against\\n` +\n ` the real tree — the check nobody else makes, and the reason to run this in\\n` +\n ` CI), forcing-language, structure-dump, generated-boilerplate, language-rule,\\n` +\n ` mirror-drift and restated-at-level. Every finding carries a stable rule id\\n` +\n ` and a sentence citing the guide it applies\\n` +\n ` (aihero.dev/a-complete-guide-to-agents-md). Findings exit 0; --strict makes\\n` +\n ` any finding exit 1. There is no built-in size limit: set agents.budgetBytes\\n` +\n ` in ${CONFIG_FILE_NAME} and exceeding it exits 1, as every other ${CLI_NAME}\\n` +\n ` budget does. The guide's own \"~150-200 instructions\" figure is printed as a\\n` +\n ` citation and compared to nothing.\\n` +\n `\\n` +\n ` ${CLI_NAME} agents split does the MECHANICAL half of the guide's refactor:\\n` +\n ` partition the root file by ## heading into one Markdown file per section\\n` +\n ` under docs/, rewrite the relative links that moved a directory deeper, and\\n` +\n ` leave a link list behind. Same discipline as init and hooks — every file\\n` +\n ` listed, one confirm, an existing file never overwritten without a per-file\\n` +\n ` yes. It does NOT decide which sections are essential: that is a reading of\\n` +\n ` your project, so it needs a model, and ${CLI_NAME} has none by law. Instead it\\n` +\n ` prints the guide's own refactor prompt with your real section headings filled\\n` +\n ` in, for you to hand to your own agent. That seam is the point, not an\\n` +\n ` omission. There is no \\`${CLI_NAME} agents init\\`: the guide says never to\\n` +\n ` auto-generate an AGENTS.md.`,\n },\n },\n\n /**\n * An action, and at most one directory. `lint` defaults to `.` because the everyday\n * invocation is \"lint here\" — unlike `map`, which is always aimed somewhere.\n */\n parse(values: FlagValues, positionals: readonly string[]): AgentsInvocation {\n const action = positionals[1];\n if (action !== 'lint' && action !== 'split') {\n throw new CliUsageError(\n `${CLI_NAME}: agents needs an action — lint or split.\\n` +\n ` ${CLI_NAME} agents lint [dir] [--strict] [--json]\\n` +\n ` ${CLI_NAME} agents split [dir]`,\n );\n }\n if (positionals.length > 3) {\n throw new CliUsageError(\n `${CLI_NAME}: agents ${action} takes at most one directory, got ` +\n `${String(positionals.length - 2)} (${positionals.slice(2).join(', ')}).`,\n );\n }\n if (action === 'split' && (values.strict === true || values.json === true)) {\n throw new CliUsageError(\n `${CLI_NAME}: --strict and --json belong to \\`${CLI_NAME} agents lint\\`. ` +\n `split is interactive and writes files; a report format and a CI exit code ` +\n `have nothing to act on there.`,\n );\n }\n return {\n mode: 'agents',\n action,\n dir: positionals[2] ?? '.',\n strict: values.strict === true,\n json: values.json === true,\n };\n },\n\n resolve(invocation: AgentsInvocation, config: ConfigSource): ResolvedAgentsRun {\n return resolveAgentsRun(invocation, config());\n },\n\n run(resolved: ResolvedAgentsRun, io: CliIo): number | Promise<number> {\n return resolved.action === 'lint' ? runLint(resolved, io) : runSplit(resolved, io);\n },\n};\n\n/**\n * Merge one `'agents'` invocation with the loaded config.\n *\n * The whole merge is one key, and it has **no flag** on purpose. A `--budget` here\n * would be a ceiling typed on the command line, which is a ceiling nobody in the\n * repository has agreed to; the number that matters is the one committed in\n * `smelt.config.json` where CI and every contributor read the same value. So unlike\n * every other verb with a budget, this one cannot refuse for a missing budget: absent\n * is a legitimate state, and it means \"measure, do not fail\".\n */\nexport function resolveAgentsRun(\n invocation: AgentsInvocation,\n config: LoadedConfig | undefined,\n): ResolvedAgentsRun {\n const budgetBytes = config?.config.agents?.budgetBytes;\n return {\n action: invocation.action,\n dir: invocation.dir,\n strict: invocation.strict,\n json: invocation.json,\n ...(budgetBytes === undefined ? {} : { budgetBytes }),\n };\n}\n\n/**\n * One `smelt agents lint` run: prove the target is a tree, lint it, render, exit.\n *\n * Three exit codes, and the reasoning for each:\n *\n * - **1 over the user's budget.** Identical to a `smelt` run that did not fit, and\n * identical for the same reason: not an error, not a success, and a script must be\n * able to tell without parsing prose. It applies with or without `--strict`,\n * because the number came from the repository rather than from smelt.\n * - **1 with `--strict` and any finding.** The CI switch. Off by default because the\n * rules are advisory heuristics.\n * - **0** otherwise, findings and all.\n */\nfunction runLint(run: ResolvedAgentsRun, io: CliIo): number {\n const tree = readTree(run.dir, run.dir, {\n tree: 'agents lint',\n file: `\\`${CLI_NAME} <file>\\``,\n });\n if (!tree.ok) throw new CliUsageError(`${CLI_NAME}: ${tree.refusal}`);\n\n const report = lintAgents({\n root: tree.value,\n ...(run.budgetBytes === undefined ? {} : { budgetBytes: run.budgetBytes }),\n });\n\n if (run.json) {\n const envelope: CliAgentsJsonEnvelope = { format: CLI_AGENTS_JSON_FORMAT, report };\n io.stdout(`${JSON.stringify(envelope, null, 2)}\\n`);\n } else {\n io.stdout(formatAgentsReport(report, { source: run.dir, strict: run.strict }));\n }\n\n if (overBudgetBytes(report) !== undefined) return EXIT.overBudget;\n if (run.strict && report.findings.length > 0) return EXIT.overBudget;\n return EXIT.ok;\n}\n\n/** `smelt agents split` is interactive like `init` and `hooks`, and refuses without a stream. */\nasync function runSplit(run: ResolvedAgentsRun, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: agents split is interactive — it lists every file it would write ` +\n `and asks before each one — and this invocation has no interactive input ` +\n `stream. Run \\`${CLI_NAME} agents split\\` from a terminal.`,\n );\n }\n return await runAgentsSplit({\n input: io.initInput,\n output: io.stdout,\n cwd: io.cwd ?? process.cwd(),\n dir: run.dir,\n });\n}\n"]}
1
+ {"version":3,"file":"agents.js","sourceRoot":"","sources":["../../../src/cli/subcommands/agents.ts"],"names":[],"mappings":"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","sourcesContent":["import process from 'node:process';\n\nimport { lintAgents, overBudgetBytes } from '../../agents/lint.ts';\nimport type { AgentsLintReport } from '../../agents/lint.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { readTree } from '../../ops/inputs.ts';\nimport { colorize } from '../lava.ts';\nimport { runAgentsSplit } from '../agents.ts';\nimport { CONFIG_FILE_NAME } from '../config.ts';\nimport type { LoadedConfig } from '../config.ts';\nimport { formatAgentsReport } from '../report.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt agents` — the instruction files an agent loads on **every** request.\n *\n * The verb exists because `AGENTS.md` is the one blob in a repository that is paid for\n * on every single request, relevant or not, and nothing measures it. That is smelt's\n * own subject, so the fit is exact — and the two actions divide along the line smelt\n * divides everything else along:\n *\n * - **`lint`** measures and explains. It never edits.\n * - **`split`** edits, mechanically, under `smelt init`'s consent discipline — and\n * says plainly which half of the refactor it refused to do.\n *\n * There is no `smelt agents init`, and there will not be one: the guide this verb\n * lints against says in as many words never to auto-generate an AGENTS.md, and a tool\n * that built the thing its own source warns against would be worth less than no tool\n * (ruling R1).\n */\n\n/** `smelt agents <lint|split> [path]` — parsed. */\nexport interface AgentsInvocation {\n readonly mode: 'agents';\n readonly action: 'lint' | 'split';\n /** The directory to read. Defaults to `.`; always a directory, never a file. */\n readonly dir: string;\n /** `lint` only: any finding exits 1. */\n readonly strict: boolean;\n /** `lint` only: the versioned envelope instead of the text report. */\n readonly json: boolean;\n}\n\n/** Everything one `smelt agents lint` run needs, fully merged. */\nexport interface ResolvedAgentsRun {\n readonly action: 'lint' | 'split';\n readonly dir: string;\n readonly strict: boolean;\n readonly json: boolean;\n /**\n * The user's byte ceiling for the merged set, from `smelt.config.json`.\n *\n * **Absent means unbudgeted, and there is no built-in fallback** (ruling R2). This\n * field carries no `budgetSource`, unlike {@link ResolvedRun}'s, precisely because\n * there is only one source it can have come from: a budget here is always the\n * user's, never a flag and never smelt's.\n */\n readonly budgetBytes?: number;\n}\n\n/**\n * The `smelt agents lint --json` envelope format. Its own version line, like the map\n * and stats envelopes: a lint report carries rule ids and byte counts per level, a\n * structure that has to be able to move without dragging the other two with it.\n */\nexport const CLI_AGENTS_JSON_FORMAT = 'smelt-agents-cli/v1';\n\n/** What `smelt agents lint --json` prints. */\nexport interface CliAgentsJsonEnvelope {\n readonly format: string;\n /** The {@link AgentsLintReport} exactly as `lintAgents` returned it. */\n readonly report: AgentsLintReport;\n}\n\nexport const agentsCommand: Subcommand<AgentsInvocation, ResolvedAgentsRun> = {\n name: 'agents',\n flags: ['strict', 'json'],\n refusal:\n `agents reads the instruction files in a tree and reports on them — it plans no ` +\n `elisions, stores nothing, and has no budget of its own to take: the only budget ` +\n `it honours is agents.budgetBytes in ${CONFIG_FILE_NAME}, which is yours.`,\n usage: {\n synopsis: ['agents lint [dir] [--strict] [--json]'],\n occasional: ['agents split [dir]'],\n section: {\n heading: 'AGENTS',\n body:\n ` ${CLI_NAME} agents lint audits the instruction files an agent loads on every\\n` +\n ` request — every AGENTS.md, CLAUDE.md and GEMINI.md in the tree, because a\\n` +\n ` nested one merges with the root. A merge runs up the tree and never across\\n` +\n ` it, so it reports bytes per level, the per-request worst case (the heaviest\\n` +\n ` level plus its ancestors — what one agent actually loads) and the whole-tree\\n` +\n ` surface, plus an imperative count labelled a heuristic. Then eight advisory\\n` +\n ` rules: dead-path and dead-link (path-like tokens and links resolved against\\n` +\n ` the real tree — the check nobody else makes, and the reason to run this in\\n` +\n ` CI), forcing-language, structure-dump, generated-boilerplate, language-rule,\\n` +\n ` mirror-drift and restated-at-level. Every finding carries a stable rule id\\n` +\n ` and a sentence citing the guide it applies\\n` +\n ` (aihero.dev/a-complete-guide-to-agents-md). Findings exit 0; --strict makes\\n` +\n ` any finding exit 1. There is no built-in size limit: set agents.budgetBytes\\n` +\n ` in ${CONFIG_FILE_NAME} and exceeding it exits 1, as every other ${CLI_NAME}\\n` +\n ` budget does. The guide's own \"~150-200 instructions\" figure is printed as a\\n` +\n ` citation and compared to nothing.\\n` +\n `\\n` +\n ` ${CLI_NAME} agents split does the MECHANICAL half of the guide's refactor:\\n` +\n ` partition the root file by ## heading into one Markdown file per section\\n` +\n ` under docs/, rewrite the relative links that moved a directory deeper, and\\n` +\n ` leave a link list behind. Same discipline as init and hooks — every file\\n` +\n ` listed, one confirm, an existing file never overwritten without a per-file\\n` +\n ` yes. It does NOT decide which sections are essential: that is a reading of\\n` +\n ` your project, so it needs a model, and ${CLI_NAME} has none by law. Instead it\\n` +\n ` prints the guide's own refactor prompt with your real section headings filled\\n` +\n ` in, for you to hand to your own agent. That seam is the point, not an\\n` +\n ` omission. There is no \\`${CLI_NAME} agents init\\`: the guide says never to\\n` +\n ` auto-generate an AGENTS.md.`,\n },\n },\n\n /**\n * An action, and at most one directory. `lint` defaults to `.` because the everyday\n * invocation is \"lint here\" — unlike `map`, which is always aimed somewhere.\n */\n parse(values: FlagValues, positionals: readonly string[]): AgentsInvocation {\n const action = positionals[1];\n if (action !== 'lint' && action !== 'split') {\n throw new CliUsageError(\n `${CLI_NAME}: agents needs an action — lint or split.\\n` +\n ` ${CLI_NAME} agents lint [dir] [--strict] [--json]\\n` +\n ` ${CLI_NAME} agents split [dir]`,\n );\n }\n if (positionals.length > 3) {\n throw new CliUsageError(\n `${CLI_NAME}: agents ${action} takes at most one directory, got ` +\n `${String(positionals.length - 2)} (${positionals.slice(2).join(', ')}).`,\n );\n }\n if (action === 'split' && (values.strict === true || values.json === true)) {\n throw new CliUsageError(\n `${CLI_NAME}: --strict and --json belong to \\`${CLI_NAME} agents lint\\`. ` +\n `split is interactive and writes files; a report format and a CI exit code ` +\n `have nothing to act on there.`,\n );\n }\n return {\n mode: 'agents',\n action,\n dir: positionals[2] ?? '.',\n strict: values.strict === true,\n json: values.json === true,\n };\n },\n\n resolve(invocation: AgentsInvocation, config: ConfigSource): ResolvedAgentsRun {\n return resolveAgentsRun(invocation, config());\n },\n\n run(resolved: ResolvedAgentsRun, io: CliIo): number | Promise<number> {\n return resolved.action === 'lint' ? runLint(resolved, io) : runSplit(resolved, io);\n },\n};\n\n/**\n * Merge one `'agents'` invocation with the loaded config.\n *\n * The whole merge is one key, and it has **no flag** on purpose. A `--budget` here\n * would be a ceiling typed on the command line, which is a ceiling nobody in the\n * repository has agreed to; the number that matters is the one committed in\n * `smelt.config.json` where CI and every contributor read the same value. So unlike\n * every other verb with a budget, this one cannot refuse for a missing budget: absent\n * is a legitimate state, and it means \"measure, do not fail\".\n */\nexport function resolveAgentsRun(\n invocation: AgentsInvocation,\n config: LoadedConfig | undefined,\n): ResolvedAgentsRun {\n const budgetBytes = config?.config.agents?.budgetBytes;\n return {\n action: invocation.action,\n dir: invocation.dir,\n strict: invocation.strict,\n json: invocation.json,\n ...(budgetBytes === undefined ? {} : { budgetBytes }),\n };\n}\n\n/**\n * One `smelt agents lint` run: prove the target is a tree, lint it, render, exit.\n *\n * Three exit codes, and the reasoning for each:\n *\n * - **1 over the user's budget.** Identical to a `smelt` run that did not fit, and\n * identical for the same reason: not an error, not a success, and a script must be\n * able to tell without parsing prose. It applies with or without `--strict`,\n * because the number came from the repository rather than from smelt.\n * - **1 with `--strict` and any finding.** The CI switch. Off by default because the\n * rules are advisory heuristics.\n * - **0** otherwise, findings and all.\n */\nfunction runLint(run: ResolvedAgentsRun, io: CliIo): number {\n const tree = readTree(run.dir, run.dir, {\n tree: 'agents lint',\n file: `\\`${CLI_NAME} <file>\\``,\n });\n if (!tree.ok) throw new CliUsageError(`${CLI_NAME}: ${tree.refusal}`);\n\n const report = lintAgents({\n root: tree.value,\n ...(run.budgetBytes === undefined ? {} : { budgetBytes: run.budgetBytes }),\n });\n\n if (run.json) {\n const envelope: CliAgentsJsonEnvelope = { format: CLI_AGENTS_JSON_FORMAT, report };\n io.stdout(`${JSON.stringify(envelope, null, 2)}\\n`);\n } else {\n io.stdout(formatAgentsReport(report, { source: run.dir, strict: run.strict }));\n }\n\n if (overBudgetBytes(report) !== undefined) return EXIT.overBudget;\n if (run.strict && report.findings.length > 0) return EXIT.overBudget;\n return EXIT.ok;\n}\n\n/** `smelt agents split` is interactive like `init` and `hooks`, and refuses without a stream. */\nasync function runSplit(run: ResolvedAgentsRun, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: agents split is interactive — it lists every file it would write ` +\n `and asks before each one — and this invocation has no interactive input ` +\n `stream. Run \\`${CLI_NAME} agents split\\` from a terminal.`,\n );\n }\n return await runAgentsSplit({\n input: io.initInput,\n output: (text) => io.stdout(colorize(text, io.color === true)),\n cwd: io.cwd ?? process.cwd(),\n dir: run.dir,\n });\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import type { Subcommand } from './subcommand.ts';
2
+ /**
3
+ * `smelt doctor` — the read-only half of the install seam. The flow is
4
+ * `cli/doctor.ts`; this file is only the verb. It owns one flag: `--json` turns the
5
+ * report into a receipt an agent can read. It needs no interactive stream, because a
6
+ * reader never asks — that is the whole difference between doctor and setup
7
+ * (ADR-0003: doctor reports; setup repairs).
8
+ */
9
+ /** `smelt doctor [--json]` — parsed. */
10
+ export interface DoctorInvocation {
11
+ readonly mode: 'doctor';
12
+ readonly json: boolean;
13
+ }
14
+ export declare const doctorCommand: Subcommand<DoctorInvocation, DoctorInvocation>;
15
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/doctor.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;GAMG;AAEH,wCAAwC;AACxC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,aAAa,EAAE,UAAU,CAAC,gBAAgB,EAAE,gBAAgB,CA6CxE,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runDoctor } from '../doctor.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 doctor` — the read-only half of the install seam. The flow is\n * `cli/doctor.ts`; this file is only the verb. It owns one flag: `--json` turns the\n * report into a receipt an agent can read. It needs no interactive stream, because a\n * reader never asks — that is the whole difference between doctor and setup\n * (ADR-0003: doctor reports; setup repairs).\n */\n\n/** `smelt doctor [--json]` — parsed. */\nexport interface DoctorInvocation {\n readonly mode: 'doctor';\n readonly json: boolean;\n}\n\nexport const doctorCommand: Subcommand<DoctorInvocation, DoctorInvocation> = {\n name: 'doctor',\n flags: ['json'],\n refusal: `doctor reads installed state and reports; it writes nothing, so there is nothing to answer.`,\n usage: {\n synopsis: ['doctor [--json]'],\n section: {\n heading: 'DOCTOR',\n body:\n ` ${CLI_NAME} doctor reads installed state and compares it with the binary running:\\n` +\n ` which release wrote the instruction blocks, whether the config parses and its\\n` +\n ` store directory exists, whether the MCP registration is intact, and which pieces\\n` +\n ` are orphans. Exit 0 when current (or nothing installed), the refused exit when\\n` +\n ` something is behind — and the report names the exact repair command:\\n` +\n ` ${CLI_NAME} setup, per harness where a block is behind. Doctor never writes.\\n` +\n ` The update loop is:\\n\\n` +\n ` upgrade → ${CLI_NAME} doctor → ${CLI_NAME} setup\\n`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): DoctorInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: doctor takes no further arguments, got ` +\n `${positionals.slice(1).join(', ')}.`,\n );\n }\n return { mode: 'doctor', json: values.json === true };\n },\n\n /** Nothing to merge: doctor reads everything it reports. */\n resolve(invocation: DoctorInvocation): DoctorInvocation {\n return invocation;\n },\n\n async run(resolved: DoctorInvocation, io: CliIo): Promise<number> {\n return runDoctor(\n { json: resolved.json },\n {\n output: (text) => io.stdout(colorize(text, io.color === true && !resolved.json)),\n cwd: io.cwd ?? process.cwd(),\n version: io.version,\n },\n );\n },\n};\n"]}
@@ -0,0 +1,43 @@
1
+ import process from 'node:process';
2
+ import { CliUsageError } from '../../errors.js';
3
+ import { runDoctor } from '../doctor.js';
4
+ import { colorize } from '../lava.js';
5
+ import { CLI_NAME } from '../shell.js';
6
+ export const doctorCommand = {
7
+ name: 'doctor',
8
+ flags: ['json'],
9
+ refusal: `doctor reads installed state and reports; it writes nothing, so there is nothing to answer.`,
10
+ usage: {
11
+ synopsis: ['doctor [--json]'],
12
+ section: {
13
+ heading: 'DOCTOR',
14
+ body: ` ${CLI_NAME} doctor reads installed state and compares it with the binary running:\n` +
15
+ ` which release wrote the instruction blocks, whether the config parses and its\n` +
16
+ ` store directory exists, whether the MCP registration is intact, and which pieces\n` +
17
+ ` are orphans. Exit 0 when current (or nothing installed), the refused exit when\n` +
18
+ ` something is behind — and the report names the exact repair command:\n` +
19
+ ` ${CLI_NAME} setup, per harness where a block is behind. Doctor never writes.\n` +
20
+ ` The update loop is:\n\n` +
21
+ ` upgrade → ${CLI_NAME} doctor → ${CLI_NAME} setup\n`,
22
+ },
23
+ },
24
+ parse(values, positionals) {
25
+ if (positionals.length > 1) {
26
+ throw new CliUsageError(`${CLI_NAME}: doctor takes no further arguments, got ` +
27
+ `${positionals.slice(1).join(', ')}.`);
28
+ }
29
+ return { mode: 'doctor', json: values.json === true };
30
+ },
31
+ /** Nothing to merge: doctor reads everything it reports. */
32
+ resolve(invocation) {
33
+ return invocation;
34
+ },
35
+ async run(resolved, io) {
36
+ return runDoctor({ json: resolved.json }, {
37
+ output: (text) => io.stdout(colorize(text, io.color === true && !resolved.json)),
38
+ cwd: io.cwd ?? process.cwd(),
39
+ version: io.version,
40
+ });
41
+ },
42
+ };
43
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../src/cli/subcommands/doctor.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAoBvC,MAAM,CAAC,MAAM,aAAa,GAAmD;IAC3E,IAAI,EAAE,QAAQ;IACd,KAAK,EAAE,CAAC,MAAM,CAAC;IACf,OAAO,EAAE,6FAA6F;IACtG,KAAK,EAAE;QACL,QAAQ,EAAE,CAAC,iBAAiB,CAAC;QAC7B,OAAO,EAAE;YACP,OAAO,EAAE,QAAQ;YACjB,IAAI,EACF,KAAK,QAAQ,0EAA0E;gBACvF,mFAAmF;gBACnF,sFAAsF;gBACtF,oFAAoF;gBACpF,0EAA0E;gBAC1E,KAAK,QAAQ,qEAAqE;gBAClF,2BAA2B;gBAC3B,iBAAiB,QAAQ,aAAa,QAAQ,UAAU;SAC3D;KACF;IAED,KAAK,CAAC,MAAkB,EAAE,WAA8B;QACtD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,2CAA2C;gBACpD,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACxC,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;IACxD,CAAC;IAED,4DAA4D;IAC5D,OAAO,CAAC,UAA4B;QAClC,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,QAA0B,EAAE,EAAS;QAC7C,OAAO,SAAS,CACd,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,EACvB;YACE,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YAChF,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;YAC5B,OAAO,EAAE,EAAE,CAAC,OAAO;SACpB,CACF,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runDoctor } from '../doctor.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 doctor` — the read-only half of the install seam. The flow is\n * `cli/doctor.ts`; this file is only the verb. It owns one flag: `--json` turns the\n * report into a receipt an agent can read. It needs no interactive stream, because a\n * reader never asks — that is the whole difference between doctor and setup\n * (ADR-0003: doctor reports; setup repairs).\n */\n\n/** `smelt doctor [--json]` — parsed. */\nexport interface DoctorInvocation {\n readonly mode: 'doctor';\n readonly json: boolean;\n}\n\nexport const doctorCommand: Subcommand<DoctorInvocation, DoctorInvocation> = {\n name: 'doctor',\n flags: ['json'],\n refusal: `doctor reads installed state and reports; it writes nothing, so there is nothing to answer.`,\n usage: {\n synopsis: ['doctor [--json]'],\n section: {\n heading: 'DOCTOR',\n body:\n ` ${CLI_NAME} doctor reads installed state and compares it with the binary running:\\n` +\n ` which release wrote the instruction blocks, whether the config parses and its\\n` +\n ` store directory exists, whether the MCP registration is intact, and which pieces\\n` +\n ` are orphans. Exit 0 when current (or nothing installed), the refused exit when\\n` +\n ` something is behind — and the report names the exact repair command:\\n` +\n ` ${CLI_NAME} setup, per harness where a block is behind. Doctor never writes.\\n` +\n ` The update loop is:\\n\\n` +\n ` upgrade → ${CLI_NAME} doctor → ${CLI_NAME} setup\\n`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): DoctorInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: doctor takes no further arguments, got ` +\n `${positionals.slice(1).join(', ')}.`,\n );\n }\n return { mode: 'doctor', json: values.json === true };\n },\n\n /** Nothing to merge: doctor reads everything it reports. */\n resolve(invocation: DoctorInvocation): DoctorInvocation {\n return invocation;\n },\n\n async run(resolved: DoctorInvocation, io: CliIo): Promise<number> {\n return runDoctor(\n { json: resolved.json },\n {\n output: (text) => io.stdout(colorize(text, io.color === true && !resolved.json)),\n cwd: io.cwd ?? process.cwd(),\n version: io.version,\n },\n );\n },\n};\n"]}
@@ -27,6 +27,9 @@ export declare const CLI_FLAGS: {
27
27
  readonly type: 'string';
28
28
  readonly multiple: true;
29
29
  };
30
+ readonly producer: {
31
+ readonly type: 'string';
32
+ };
30
33
  readonly language: {
31
34
  readonly type: 'string';
32
35
  };
@@ -40,8 +43,21 @@ export declare const CLI_FLAGS: {
40
43
  readonly cache: {
41
44
  readonly type: 'string';
42
45
  };
46
+ /**
47
+ * Repeatable because `setup` wires several harnesses in one run. `hooks` takes one
48
+ * per run and refuses a second id in its own parse — arity is a per-verb fact,
49
+ * expressed where the verb validates, with the one generated refusal; the table
50
+ * records how argv is read, not how many a verb accepts.
51
+ */
43
52
  readonly harness: {
44
53
  readonly type: 'string';
54
+ readonly multiple: true;
55
+ };
56
+ readonly yes: {
57
+ readonly type: 'boolean';
58
+ };
59
+ readonly 'no-mcp': {
60
+ readonly type: 'boolean';
45
61
  };
46
62
  readonly strict: {
47
63
  readonly type: 'boolean';
@@ -1 +1 @@
1
- {"version":3,"file":"flags.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/flags.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS;aACpB,MAAM;iBAAI,IAAI,EAAE,QAAQ;;aACxB,KAAK;iBAAI,IAAI,EAAE,QAAQ;iBAAE,QAAQ;;aACjC,QAAQ;iBAAI,IAAI,EAAE,QAAQ;;aAC1B,QAAQ;iBAAI,IAAI,EAAE,QAAQ;;aAC1B,MAAM;iBAAI,IAAI,EAAE,QAAQ;iBAAE,QAAQ;;aAClC,KAAK;iBAAI,IAAI,EAAE,QAAQ;;aACvB,OAAO;iBAAI,IAAI,EAAE,QAAQ;;aACzB,MAAM;iBAAI,IAAI,EAAE,SAAS;;aACzB,IAAI;iBAAI,IAAI,EAAE,SAAS;;aACvB,WAAW;iBAAI,IAAI,EAAE,SAAS;;aAC9B,IAAI;iBAAI,IAAI,EAAE,SAAS;iBAAE,KAAK,EAAE,GAAG;;aACnC,OAAO;iBAAI,IAAI,EAAE,SAAS;;CAClB,CAAC;AAEX,uFAAuF;AACvF,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,SAAS,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,YAAY,8BAA6D,CAAC;AAEvF,kEAAkE;AAClE,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAExE,oFAAoF;AACpF,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAEzC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,CAI3D;AAED,iGAAiG;AACjG,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACtD,OAAO,GACP,CAAC,SAAS;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GACnC,SAAS,MAAM,EAAE,GACjB,MAAM,CAAC;AAEb;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,EAAE,CAAC,IAAI,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEzF,6DAA6D;AAC7D,MAAM,WAAW,QAAQ;IACvB,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,IAAI,IAAI,SAAS,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAerF;AAKD;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAyF1D,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOvE","sourcesContent":["import { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESS_IDS } from '../../harness/registry.ts';\nimport { budgetFault, budgetMalformed } from '../../ops/inputs.ts';\nimport type { BudgetFault } from '../../ops/inputs.ts';\nimport { STRATEGIES, DEFAULT_STRATEGY } from '../../plan/planners.ts';\nimport { STRUCTURAL_LANGUAGES } from '../../plan/structural.ts';\nimport { DEFAULT_REPO_IGNORE } from '../../repomap/map.ts';\nimport { CLI_NAME } from '../shell.ts';\n\n/**\n * Every flag the CLI accepts, once.\n *\n * The table is the companion the {@link Subcommand} registry needs: a verb declares\n * the flags it owns as `FlagName[]`, so the type of that list — and therefore whether\n * a verb can claim a flag that does not exist — comes from here. It carries three\n * things that used to be written in three places:\n *\n * 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a\n * literal inside `parseSmeltArgs`.\n * 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is\n * spelled once and every verb's ownership list typechecks against it.\n * 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`\n * prefix, which is *not* stored here: it is generated from the registry's\n * ownership (see `renderOptions` in `cli/usage.ts`), because \"which verb owns\n * this flag\" is a fact the registry already holds and prose that restates it is\n * prose that can go stale.\n *\n * Key order is meaningful: it is the order the OPTIONS block renders and the order a\n * refusal lists flags in, so keep it stable and append new flags where they read best.\n */\nexport const CLI_FLAGS = {\n budget: { type: 'string' },\n focus: { type: 'string', multiple: true },\n language: { type: 'string' },\n strategy: { type: 'string' },\n ignore: { type: 'string', multiple: true },\n cache: { type: 'string' },\n harness: { type: 'string' },\n strict: { type: 'boolean' },\n json: { type: 'boolean' },\n reconstruct: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean' },\n} as const;\n\n/** Every flag name, as a type. A verb cannot claim a flag that is not in the table. */\nexport type FlagName = keyof typeof CLI_FLAGS;\n\n/**\n * The flags answered *before* any verb, so no verb owns them and no verb may refuse\n * them: `smelt map --help` prints the help, exactly as it always has. Every other flag\n * belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.\n */\nexport const GLOBAL_FLAGS = ['help', 'version'] as const satisfies readonly FlagName[];\n\n/** A flag a verb can own — everything but the two global ones. */\nexport type VerbFlag = Exclude<FlagName, (typeof GLOBAL_FLAGS)[number]>;\n\n/** Every ownable flag, in table order: the list a refusal checks a verb against. */\nexport const VERB_FLAGS: readonly VerbFlag[] = Object.keys(CLI_FLAGS).filter(\n (name): name is VerbFlag => !(GLOBAL_FLAGS as readonly string[]).includes(name),\n);\n\n/**\n * `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.\n *\n * Every refusal that names more than one flag spells the list this way: the\n * ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`\n * refusal in `./smelt.ts`. It lives with the table because the *order* is the\n * table's — a refusal that listed flags in the order the user happened to type them\n * would read differently every time, and two refusals disagreeing about how to spell\n * the same pair is the kind of drift this file exists to remove.\n */\nexport function flagList(flags: readonly VerbFlag[]): string {\n const spelled = VERB_FLAGS.filter((flag) => flags.includes(flag)).map((flag) => `--${flag}`);\n if (spelled.length <= 1) return spelled.join('');\n return `${spelled.slice(0, -1).join(', ')} and ${spelled.at(-1) ?? ''}`;\n}\n\n/** What one flag's parsed value looks like, derived from how `parseArgs` was told to read it. */\ntype FlagValue<F> = F extends { readonly type: 'boolean' }\n ? boolean\n : F extends { readonly multiple: true }\n ? readonly string[]\n : string;\n\n/**\n * The parsed flags, as every `Subcommand.parse` sees them: one optional field per\n * flag, typed by the table above. Absent means the user did not type it — which is\n * the only thing a refusal needs to know, and the only thing a verb may act on.\n */\nexport type FlagValues = { readonly [K in FlagName]?: FlagValue<(typeof CLI_FLAGS)[K]> };\n\n/** How one flag appears in the OPTIONS block of `--help`. */\nexport interface FlagHelp {\n /** The left column, e.g. `--budget <bytes>` or `-h, --help`. */\n readonly label: string;\n /**\n * The description, already wrapped to the OPTIONS column — a function because three\n * entries render a registry (`SUPPORTED_LANGUAGES`, `STRATEGIES` +\n * `STRUCTURAL_LANGUAGES`, `HARNESS_IDS`) rather than a hand-typed list. The\n * ownership prefix is not here; it is generated.\n */\n body(): readonly string[];\n}\n\n/**\n * A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the\n * hand-typed version wrapped. `--strategy` and `--language` render their registries on\n * one line because they fit; the harness ids do not, and a list long enough to wrap is\n * exactly the list nobody keeps in sync by hand.\n */\nexport function optionList(items: readonly string[], width: number): readonly string[] {\n const lines: string[] = [];\n let line = '';\n items.forEach((item, index) => {\n const word = index === items.length - 1 ? `${item}.` : `${item},`;\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > width) {\n lines.push(line);\n line = word;\n } else {\n line = candidate;\n }\n });\n lines.push(line);\n return lines;\n}\n\n/** The width an OPTIONS description wraps within, once its 23-column indent is removed. */\nconst OPTION_BODY_WIDTH = 65;\n\n/**\n * The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag\n * added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text\n * cannot fall behind what the parser accepts.\n */\nexport const FLAG_HELP: Readonly<Record<FlagName, FlagHelp>> = {\n budget: {\n label: '--budget <bytes>',\n body: () => [\n 'Required, unless smelt.config.json sets defaultBudgetBytes.',\n 'Soft ceiling for the output, in UTF-8 bytes (for map: a hard',\n 'ceiling, met by construction). No built-in default: a budget',\n `${CLI_NAME} invented would decide for you.`,\n ],\n },\n focus: {\n label: '--focus <term>',\n body: () => [\n 'What you were looking for. Repeatable. Matching regions and',\n 'their context survive; the runs between them collapse. For',\n 'map: symbols matching a term (by name or path) are promoted',\n 'to the front of the fill order, ranks unchanged.',\n ],\n },\n language: {\n label: '--language <id>',\n body: () => [`Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.`],\n },\n strategy: {\n label: '--strategy <id>',\n body: () => [\n `${STRATEGIES.join(', ')}. Defaults to ${DEFAULT_STRATEGY}, unless`,\n 'smelt.config.json says otherwise. structural parses',\n `${STRUCTURAL_LANGUAGES.join(', ')};`,\n 'any other language is refused, never approximated. auto picks',\n 'structural for those languages and lexical for the rest, and',\n 'the report names whichever one actually ran.',\n ],\n },\n ignore: {\n label: '--ignore <entry>',\n body: () => [\n 'Repeatable. Replaces the default ignore list',\n // Read off DEFAULT_REPO_IGNORE, never re-typed: help text that lists a default\n // by hand is help text that will one day describe a different default.\n `(${DEFAULT_REPO_IGNORE.join(', ')}): a bare name matches any path segment,`,\n 'an entry containing / is a root-relative prefix.',\n ],\n },\n cache: {\n label: '--cache <dir>',\n body: () => [\n 'Directory for the tags cache, keyed by content',\n 'hash. Only when given does the map write to disk at all.',\n ],\n },\n harness: {\n label: '--harness <id>',\n body: () => [\n 'Skip harness detection and target one id:',\n ...optionList(HARNESS_IDS, OPTION_BODY_WIDTH),\n ],\n },\n strict: {\n label: '--strict',\n body: () => [\n 'Turn any lint finding into exit 1, for CI. Findings are',\n 'advisory by default: the rules are heuristics about somebody',\n \"else's house style, and enforcing them uninvited would be\",\n `${CLI_NAME} deciding it. A budget you set yourself is different —`,\n 'exceeding agents.budgetBytes exits 1 with or without --strict.',\n ],\n },\n json: {\n label: '--json',\n body: () => [\n 'Print a JSON envelope on stdout instead of the text:',\n '{ format, result, elided } for a smelt run — `result` is',\n 'the SmeltResult verbatim, `elided` carries the bytes, so',\n 'the envelope can be reconstructed; feed it back with',\n '--reconstruct. For map: { format, map }, the RepoMap',\n 'structure verbatim. For agents lint: { format, report },',\n 'the measured levels and every finding with its rule id.',\n ],\n },\n reconstruct: {\n label: '--reconstruct',\n body: () => [\n 'Read a --json envelope and print the original text, byte for',\n 'byte. This is Law 3 you can run from a shell.',\n ],\n },\n help: { label: '-h, --help', body: () => ['This text.'] },\n version: { label: '--version', body: () => ['The package version.'] },\n};\n\n/**\n * `--budget` has no built-in default, for the same reason `smelt()` has none: a budget\n * smelt invented would be smelt deciding how much of the caller's context to throw\n * away, silently, at a number nobody chose. A *missing* flag is not an error here,\n * though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose\n * explicitly, and the verb's `resolve` errors only when neither exists. A malformed\n * value is always an error.\n *\n * It lives with the flag rather than with a verb because two verbs own `--budget`, and\n * the two of them agreeing on what \"4kb\" means is not something to leave to chance.\n *\n * The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a\n * leading `-` are answered by a digits-only test before anything numeric happens. The\n * *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also\n * where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`\n * here and `\"budgetBytes\"` there.\n */\nexport function parseBudget(raw: string | undefined): number | undefined {\n if (raw === undefined) return undefined;\n if (!/^\\d+$/.test(raw)) throw refuseBudget('not-an-integer', raw);\n const value = Number(raw);\n const fault = budgetFault(value);\n if (fault !== undefined) throw refuseBudget(fault, raw);\n return value;\n}\n\n/** The malformed-budget refusal, in the CLI's currency: prefixed, and exit 2. */\nfunction refuseBudget(fault: BudgetFault, raw: string): CliUsageError {\n return new CliUsageError(`${CLI_NAME}: ${budgetMalformed(fault, '--budget', raw)}`);\n}\n"]}
1
+ {"version":3,"file":"flags.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/flags.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS;aACpB,MAAM;iBAAI,IAAI,EAAE,QAAQ;;aACxB,KAAK;iBAAI,IAAI,EAAE,QAAQ;iBAAE,QAAQ;;aACjC,QAAQ;iBAAI,IAAI,EAAE,QAAQ;;aAC1B,QAAQ;iBAAI,IAAI,EAAE,QAAQ;;aAC1B,QAAQ;iBAAI,IAAI,EAAE,QAAQ;;aAC1B,MAAM;iBAAI,IAAI,EAAE,QAAQ;iBAAE,QAAQ;;aAClC,KAAK;iBAAI,IAAI,EAAE,QAAQ;;IACvB;;;;;OAKG;aACH,OAAO;iBAAI,IAAI,EAAE,QAAQ;iBAAE,QAAQ;;aACnC,GAAG;iBAAI,IAAI,EAAE,SAAS;;aACtB,QAAQ;iBAAI,IAAI,EAAE,SAAS;;aAC3B,MAAM;iBAAI,IAAI,EAAE,SAAS;;aACzB,IAAI;iBAAI,IAAI,EAAE,SAAS;;aACvB,WAAW;iBAAI,IAAI,EAAE,SAAS;;aAC9B,IAAI;iBAAI,IAAI,EAAE,SAAS;iBAAE,KAAK,EAAE,GAAG;;aACnC,OAAO;iBAAI,IAAI,EAAE,SAAS;;CAClB,CAAC;AAEX,uFAAuF;AACvF,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,SAAS,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,YAAY,8BAA6D,CAAC;AAEvF,kEAAkE;AAClE,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAExE,oFAAoF;AACpF,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAEzC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,CAI3D;AAED,iGAAiG;AACjG,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACtD,OAAO,GACP,CAAC,SAAS;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GACnC,SAAS,MAAM,EAAE,GACjB,MAAM,CAAC;AAEb;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,EAAE,CAAC,IAAI,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEzF,6DAA6D;AAC7D,MAAM,WAAW,QAAQ;IACvB,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,IAAI,IAAI,SAAS,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAerF;AAKD;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAuH1D,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOvE","sourcesContent":["import { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESS_IDS } from '../../harness/registry.ts';\nimport { budgetFault, budgetMalformed } from '../../ops/inputs.ts';\nimport type { BudgetFault } from '../../ops/inputs.ts';\nimport { STRATEGIES, DEFAULT_STRATEGY } from '../../plan/planners.ts';\nimport { STRUCTURAL_LANGUAGES } from '../../plan/structural.ts';\nimport { DEFAULT_REPO_IGNORE } from '../../repomap/map.ts';\nimport { CLI_NAME } from '../shell.ts';\n\n/**\n * Every flag the CLI accepts, once.\n *\n * The table is the companion the {@link Subcommand} registry needs: a verb declares\n * the flags it owns as `FlagName[]`, so the type of that list — and therefore whether\n * a verb can claim a flag that does not exist — comes from here. It carries three\n * things that used to be written in three places:\n *\n * 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a\n * literal inside `parseSmeltArgs`.\n * 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is\n * spelled once and every verb's ownership list typechecks against it.\n * 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`\n * prefix, which is *not* stored here: it is generated from the registry's\n * ownership (see `renderOptions` in `cli/usage.ts`), because \"which verb owns\n * this flag\" is a fact the registry already holds and prose that restates it is\n * prose that can go stale.\n *\n * Key order is meaningful: it is the order the OPTIONS block renders and the order a\n * refusal lists flags in, so keep it stable and append new flags where they read best.\n */\nexport const CLI_FLAGS = {\n budget: { type: 'string' },\n focus: { type: 'string', multiple: true },\n producer: { type: 'string' },\n language: { type: 'string' },\n strategy: { type: 'string' },\n ignore: { type: 'string', multiple: true },\n cache: { type: 'string' },\n /**\n * Repeatable because `setup` wires several harnesses in one run. `hooks` takes one\n * per run and refuses a second id in its own parse — arity is a per-verb fact,\n * expressed where the verb validates, with the one generated refusal; the table\n * records how argv is read, not how many a verb accepts.\n */\n harness: { type: 'string', multiple: true },\n yes: { type: 'boolean' },\n 'no-mcp': { type: 'boolean' },\n strict: { type: 'boolean' },\n json: { type: 'boolean' },\n reconstruct: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean' },\n} as const;\n\n/** Every flag name, as a type. A verb cannot claim a flag that is not in the table. */\nexport type FlagName = keyof typeof CLI_FLAGS;\n\n/**\n * The flags answered *before* any verb, so no verb owns them and no verb may refuse\n * them: `smelt map --help` prints the help, exactly as it always has. Every other flag\n * belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.\n */\nexport const GLOBAL_FLAGS = ['help', 'version'] as const satisfies readonly FlagName[];\n\n/** A flag a verb can own — everything but the two global ones. */\nexport type VerbFlag = Exclude<FlagName, (typeof GLOBAL_FLAGS)[number]>;\n\n/** Every ownable flag, in table order: the list a refusal checks a verb against. */\nexport const VERB_FLAGS: readonly VerbFlag[] = Object.keys(CLI_FLAGS).filter(\n (name): name is VerbFlag => !(GLOBAL_FLAGS as readonly string[]).includes(name),\n);\n\n/**\n * `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.\n *\n * Every refusal that names more than one flag spells the list this way: the\n * ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`\n * refusal in `./smelt.ts`. It lives with the table because the *order* is the\n * table's — a refusal that listed flags in the order the user happened to type them\n * would read differently every time, and two refusals disagreeing about how to spell\n * the same pair is the kind of drift this file exists to remove.\n */\nexport function flagList(flags: readonly VerbFlag[]): string {\n const spelled = VERB_FLAGS.filter((flag) => flags.includes(flag)).map((flag) => `--${flag}`);\n if (spelled.length <= 1) return spelled.join('');\n return `${spelled.slice(0, -1).join(', ')} and ${spelled.at(-1) ?? ''}`;\n}\n\n/** What one flag's parsed value looks like, derived from how `parseArgs` was told to read it. */\ntype FlagValue<F> = F extends { readonly type: 'boolean' }\n ? boolean\n : F extends { readonly multiple: true }\n ? readonly string[]\n : string;\n\n/**\n * The parsed flags, as every `Subcommand.parse` sees them: one optional field per\n * flag, typed by the table above. Absent means the user did not type it — which is\n * the only thing a refusal needs to know, and the only thing a verb may act on.\n */\nexport type FlagValues = { readonly [K in FlagName]?: FlagValue<(typeof CLI_FLAGS)[K]> };\n\n/** How one flag appears in the OPTIONS block of `--help`. */\nexport interface FlagHelp {\n /** The left column, e.g. `--budget <bytes>` or `-h, --help`. */\n readonly label: string;\n /**\n * The description, already wrapped to the OPTIONS column — a function because three\n * entries render a registry (`SUPPORTED_LANGUAGES`, `STRATEGIES` +\n * `STRUCTURAL_LANGUAGES`, `HARNESS_IDS`) rather than a hand-typed list. The\n * ownership prefix is not here; it is generated.\n */\n body(): readonly string[];\n}\n\n/**\n * A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the\n * hand-typed version wrapped. `--strategy` and `--language` render their registries on\n * one line because they fit; the harness ids do not, and a list long enough to wrap is\n * exactly the list nobody keeps in sync by hand.\n */\nexport function optionList(items: readonly string[], width: number): readonly string[] {\n const lines: string[] = [];\n let line = '';\n items.forEach((item, index) => {\n const word = index === items.length - 1 ? `${item}.` : `${item},`;\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > width) {\n lines.push(line);\n line = word;\n } else {\n line = candidate;\n }\n });\n lines.push(line);\n return lines;\n}\n\n/** The width an OPTIONS description wraps within, once its 23-column indent is removed. */\nconst OPTION_BODY_WIDTH = 65;\n\n/**\n * The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag\n * added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text\n * cannot fall behind what the parser accepts.\n */\nexport const FLAG_HELP: Readonly<Record<FlagName, FlagHelp>> = {\n budget: {\n label: '--budget <bytes>',\n body: () => [\n 'Required, unless smelt.config.json sets defaultBudgetBytes.',\n 'Soft ceiling for the output, in UTF-8 bytes (for map: a hard',\n 'ceiling, met by construction). No built-in default: a budget',\n `${CLI_NAME} invented would decide for you.`,\n ],\n },\n focus: {\n label: '--focus <term>',\n body: () => [\n 'What you were looking for. Repeatable. Matching regions and',\n 'their context survive; the runs between them collapse. For',\n 'map: symbols matching a term (by name or path) are promoted',\n 'to the front of the fill order, ranks unchanged.',\n ],\n },\n producer: {\n label: '--producer <cmd>',\n body: () => [\n 'The command whose output this is, e.g. \"grep -C 3 foo src\". When',\n 'no --focus is given, the focus is derived from it exactly as the',\n 'hooks guard derives it: a search pattern, only when the output',\n 'also holds non-matching lines (context flags). cat, diffs and',\n 'logs name no term; the head and tail are kept instead.',\n ],\n },\n language: {\n label: '--language <id>',\n body: () => [`Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.`],\n },\n strategy: {\n label: '--strategy <id>',\n body: () => [\n `${STRATEGIES.join(', ')}. Defaults to ${DEFAULT_STRATEGY}, unless`,\n 'smelt.config.json says otherwise. structural parses',\n `${STRUCTURAL_LANGUAGES.join(', ')};`,\n 'any other language is refused, never approximated. json cuts',\n 'members and elements; diff cuts files and hunks; each refuses',\n 'any other content. auto picks by content kind first (json,',\n 'diff), then structural for those languages and lexical for the',\n 'rest, and the report names whichever one actually ran.',\n ],\n },\n ignore: {\n label: '--ignore <entry>',\n body: () => [\n 'Repeatable. Replaces the default ignore list',\n // Read off DEFAULT_REPO_IGNORE, never re-typed: help text that lists a default\n // by hand is help text that will one day describe a different default.\n `(${DEFAULT_REPO_IGNORE.join(', ')}): a bare name matches any path segment,`,\n 'an entry containing / is a root-relative prefix.',\n ],\n },\n cache: {\n label: '--cache <dir>',\n body: () => [\n 'Directory for the tags cache, keyed by content',\n 'hash. Only when given does the map write to disk at all.',\n ],\n },\n harness: {\n label: '--harness <id>',\n body: () => [\n 'Skip harness detection and target one id:',\n ...optionList(HARNESS_IDS, OPTION_BODY_WIDTH),\n 'Repeatable for setup; hooks takes one per run.',\n ],\n },\n yes: {\n label: '--yes',\n body: () => [\n \"Non-interactive setup: the recipe's defaults, printed\",\n 'loudly as they are applied. Existing files are never',\n 'overwritten — skipped with a note; hooks install edits',\n 'them, and it asks per file.',\n ],\n },\n 'no-mcp': {\n label: '--no-mcp',\n body: () => [\n 'Setup only: skip the MCP registration step — the',\n 'printed command and its note — for a hooks-only',\n 'setup.',\n ],\n },\n strict: {\n label: '--strict',\n body: () => [\n 'Turn any lint finding into exit 1, for CI. Findings are',\n 'advisory by default: the rules are heuristics about somebody',\n \"else's house style, and enforcing them uninvited would be\",\n `${CLI_NAME} deciding it. A budget you set yourself is different —`,\n 'exceeding agents.budgetBytes exits 1 with or without --strict.',\n ],\n },\n json: {\n label: '--json',\n body: () => [\n 'Print a JSON envelope on stdout instead of the text:',\n '{ format, result, elided } for a smelt run — `result` is',\n 'the SmeltResult verbatim, `elided` carries the bytes, so',\n 'the envelope can be reconstructed; feed it back with',\n '--reconstruct. For map: { format, map }, the RepoMap',\n 'structure verbatim. For agents lint: { format, report },',\n 'the measured levels and every finding with its rule id.',\n ],\n },\n reconstruct: {\n label: '--reconstruct',\n body: () => [\n 'Read a --json envelope and print the original text, byte for',\n 'byte. This is Law 3 you can run from a shell.',\n ],\n },\n help: { label: '-h, --help', body: () => ['This text.'] },\n version: { label: '--version', body: () => ['The package version.'] },\n};\n\n/**\n * `--budget` has no built-in default, for the same reason `smelt()` has none: a budget\n * smelt invented would be smelt deciding how much of the caller's context to throw\n * away, silently, at a number nobody chose. A *missing* flag is not an error here,\n * though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose\n * explicitly, and the verb's `resolve` errors only when neither exists. A malformed\n * value is always an error.\n *\n * It lives with the flag rather than with a verb because two verbs own `--budget`, and\n * the two of them agreeing on what \"4kb\" means is not something to leave to chance.\n *\n * The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a\n * leading `-` are answered by a digits-only test before anything numeric happens. The\n * *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also\n * where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`\n * here and `\"budgetBytes\"` there.\n */\nexport function parseBudget(raw: string | undefined): number | undefined {\n if (raw === undefined) return undefined;\n if (!/^\\d+$/.test(raw)) throw refuseBudget('not-an-integer', raw);\n const value = Number(raw);\n const fault = budgetFault(value);\n if (fault !== undefined) throw refuseBudget(fault, raw);\n return value;\n}\n\n/** The malformed-budget refusal, in the CLI's currency: prefixed, and exit 2. */\nfunction refuseBudget(fault: BudgetFault, raw: string): CliUsageError {\n return new CliUsageError(`${CLI_NAME}: ${budgetMalformed(fault, '--budget', raw)}`);\n}\n"]}
@@ -30,11 +30,20 @@ import { CLI_NAME } from '../shell.js';
30
30
  export const CLI_FLAGS = {
31
31
  budget: { type: 'string' },
32
32
  focus: { type: 'string', multiple: true },
33
+ producer: { type: 'string' },
33
34
  language: { type: 'string' },
34
35
  strategy: { type: 'string' },
35
36
  ignore: { type: 'string', multiple: true },
36
37
  cache: { type: 'string' },
37
- harness: { type: 'string' },
38
+ /**
39
+ * Repeatable because `setup` wires several harnesses in one run. `hooks` takes one
40
+ * per run and refuses a second id in its own parse — arity is a per-verb fact,
41
+ * expressed where the verb validates, with the one generated refusal; the table
42
+ * records how argv is read, not how many a verb accepts.
43
+ */
44
+ harness: { type: 'string', multiple: true },
45
+ yes: { type: 'boolean' },
46
+ 'no-mcp': { type: 'boolean' },
38
47
  strict: { type: 'boolean' },
39
48
  json: { type: 'boolean' },
40
49
  reconstruct: { type: 'boolean' },
@@ -114,6 +123,16 @@ export const FLAG_HELP = {
114
123
  'to the front of the fill order, ranks unchanged.',
115
124
  ],
116
125
  },
126
+ producer: {
127
+ label: '--producer <cmd>',
128
+ body: () => [
129
+ 'The command whose output this is, e.g. "grep -C 3 foo src". When',
130
+ 'no --focus is given, the focus is derived from it exactly as the',
131
+ 'hooks guard derives it: a search pattern, only when the output',
132
+ 'also holds non-matching lines (context flags). cat, diffs and',
133
+ 'logs name no term; the head and tail are kept instead.',
134
+ ],
135
+ },
117
136
  language: {
118
137
  label: '--language <id>',
119
138
  body: () => [`Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.`],
@@ -124,9 +143,11 @@ export const FLAG_HELP = {
124
143
  `${STRATEGIES.join(', ')}. Defaults to ${DEFAULT_STRATEGY}, unless`,
125
144
  'smelt.config.json says otherwise. structural parses',
126
145
  `${STRUCTURAL_LANGUAGES.join(', ')};`,
127
- 'any other language is refused, never approximated. auto picks',
128
- 'structural for those languages and lexical for the rest, and',
129
- 'the report names whichever one actually ran.',
146
+ 'any other language is refused, never approximated. json cuts',
147
+ 'members and elements; diff cuts files and hunks; each refuses',
148
+ 'any other content. auto picks by content kind first (json,',
149
+ 'diff), then structural for those languages and lexical for the',
150
+ 'rest, and the report names whichever one actually ran.',
130
151
  ],
131
152
  },
132
153
  ignore: {
@@ -151,6 +172,24 @@ export const FLAG_HELP = {
151
172
  body: () => [
152
173
  'Skip harness detection and target one id:',
153
174
  ...optionList(HARNESS_IDS, OPTION_BODY_WIDTH),
175
+ 'Repeatable for setup; hooks takes one per run.',
176
+ ],
177
+ },
178
+ yes: {
179
+ label: '--yes',
180
+ body: () => [
181
+ "Non-interactive setup: the recipe's defaults, printed",
182
+ 'loudly as they are applied. Existing files are never',
183
+ 'overwritten — skipped with a note; hooks install edits',
184
+ 'them, and it asks per file.',
185
+ ],
186
+ },
187
+ 'no-mcp': {
188
+ label: '--no-mcp',
189
+ body: () => [
190
+ 'Setup only: skip the MCP registration step — the',
191
+ 'printed command and its note — for a hooks-only',
192
+ 'setup.',
154
193
  ],
155
194
  },
156
195
  strict: {
@@ -1 +1 @@
1
- {"version":3,"file":"flags.js","sourceRoot":"","sources":["../../../src/cli/subcommands/flags.ts"],"names":[],"mappings":"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","sourcesContent":["import { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESS_IDS } from '../../harness/registry.ts';\nimport { budgetFault, budgetMalformed } from '../../ops/inputs.ts';\nimport type { BudgetFault } from '../../ops/inputs.ts';\nimport { STRATEGIES, DEFAULT_STRATEGY } from '../../plan/planners.ts';\nimport { STRUCTURAL_LANGUAGES } from '../../plan/structural.ts';\nimport { DEFAULT_REPO_IGNORE } from '../../repomap/map.ts';\nimport { CLI_NAME } from '../shell.ts';\n\n/**\n * Every flag the CLI accepts, once.\n *\n * The table is the companion the {@link Subcommand} registry needs: a verb declares\n * the flags it owns as `FlagName[]`, so the type of that list — and therefore whether\n * a verb can claim a flag that does not exist — comes from here. It carries three\n * things that used to be written in three places:\n *\n * 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a\n * literal inside `parseSmeltArgs`.\n * 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is\n * spelled once and every verb's ownership list typechecks against it.\n * 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`\n * prefix, which is *not* stored here: it is generated from the registry's\n * ownership (see `renderOptions` in `cli/usage.ts`), because \"which verb owns\n * this flag\" is a fact the registry already holds and prose that restates it is\n * prose that can go stale.\n *\n * Key order is meaningful: it is the order the OPTIONS block renders and the order a\n * refusal lists flags in, so keep it stable and append new flags where they read best.\n */\nexport const CLI_FLAGS = {\n budget: { type: 'string' },\n focus: { type: 'string', multiple: true },\n language: { type: 'string' },\n strategy: { type: 'string' },\n ignore: { type: 'string', multiple: true },\n cache: { type: 'string' },\n harness: { type: 'string' },\n strict: { type: 'boolean' },\n json: { type: 'boolean' },\n reconstruct: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean' },\n} as const;\n\n/** Every flag name, as a type. A verb cannot claim a flag that is not in the table. */\nexport type FlagName = keyof typeof CLI_FLAGS;\n\n/**\n * The flags answered *before* any verb, so no verb owns them and no verb may refuse\n * them: `smelt map --help` prints the help, exactly as it always has. Every other flag\n * belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.\n */\nexport const GLOBAL_FLAGS = ['help', 'version'] as const satisfies readonly FlagName[];\n\n/** A flag a verb can own — everything but the two global ones. */\nexport type VerbFlag = Exclude<FlagName, (typeof GLOBAL_FLAGS)[number]>;\n\n/** Every ownable flag, in table order: the list a refusal checks a verb against. */\nexport const VERB_FLAGS: readonly VerbFlag[] = Object.keys(CLI_FLAGS).filter(\n (name): name is VerbFlag => !(GLOBAL_FLAGS as readonly string[]).includes(name),\n);\n\n/**\n * `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.\n *\n * Every refusal that names more than one flag spells the list this way: the\n * ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`\n * refusal in `./smelt.ts`. It lives with the table because the *order* is the\n * table's — a refusal that listed flags in the order the user happened to type them\n * would read differently every time, and two refusals disagreeing about how to spell\n * the same pair is the kind of drift this file exists to remove.\n */\nexport function flagList(flags: readonly VerbFlag[]): string {\n const spelled = VERB_FLAGS.filter((flag) => flags.includes(flag)).map((flag) => `--${flag}`);\n if (spelled.length <= 1) return spelled.join('');\n return `${spelled.slice(0, -1).join(', ')} and ${spelled.at(-1) ?? ''}`;\n}\n\n/** What one flag's parsed value looks like, derived from how `parseArgs` was told to read it. */\ntype FlagValue<F> = F extends { readonly type: 'boolean' }\n ? boolean\n : F extends { readonly multiple: true }\n ? readonly string[]\n : string;\n\n/**\n * The parsed flags, as every `Subcommand.parse` sees them: one optional field per\n * flag, typed by the table above. Absent means the user did not type it — which is\n * the only thing a refusal needs to know, and the only thing a verb may act on.\n */\nexport type FlagValues = { readonly [K in FlagName]?: FlagValue<(typeof CLI_FLAGS)[K]> };\n\n/** How one flag appears in the OPTIONS block of `--help`. */\nexport interface FlagHelp {\n /** The left column, e.g. `--budget <bytes>` or `-h, --help`. */\n readonly label: string;\n /**\n * The description, already wrapped to the OPTIONS column — a function because three\n * entries render a registry (`SUPPORTED_LANGUAGES`, `STRATEGIES` +\n * `STRUCTURAL_LANGUAGES`, `HARNESS_IDS`) rather than a hand-typed list. The\n * ownership prefix is not here; it is generated.\n */\n body(): readonly string[];\n}\n\n/**\n * A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the\n * hand-typed version wrapped. `--strategy` and `--language` render their registries on\n * one line because they fit; the harness ids do not, and a list long enough to wrap is\n * exactly the list nobody keeps in sync by hand.\n */\nexport function optionList(items: readonly string[], width: number): readonly string[] {\n const lines: string[] = [];\n let line = '';\n items.forEach((item, index) => {\n const word = index === items.length - 1 ? `${item}.` : `${item},`;\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > width) {\n lines.push(line);\n line = word;\n } else {\n line = candidate;\n }\n });\n lines.push(line);\n return lines;\n}\n\n/** The width an OPTIONS description wraps within, once its 23-column indent is removed. */\nconst OPTION_BODY_WIDTH = 65;\n\n/**\n * The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag\n * added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text\n * cannot fall behind what the parser accepts.\n */\nexport const FLAG_HELP: Readonly<Record<FlagName, FlagHelp>> = {\n budget: {\n label: '--budget <bytes>',\n body: () => [\n 'Required, unless smelt.config.json sets defaultBudgetBytes.',\n 'Soft ceiling for the output, in UTF-8 bytes (for map: a hard',\n 'ceiling, met by construction). No built-in default: a budget',\n `${CLI_NAME} invented would decide for you.`,\n ],\n },\n focus: {\n label: '--focus <term>',\n body: () => [\n 'What you were looking for. Repeatable. Matching regions and',\n 'their context survive; the runs between them collapse. For',\n 'map: symbols matching a term (by name or path) are promoted',\n 'to the front of the fill order, ranks unchanged.',\n ],\n },\n language: {\n label: '--language <id>',\n body: () => [`Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.`],\n },\n strategy: {\n label: '--strategy <id>',\n body: () => [\n `${STRATEGIES.join(', ')}. Defaults to ${DEFAULT_STRATEGY}, unless`,\n 'smelt.config.json says otherwise. structural parses',\n `${STRUCTURAL_LANGUAGES.join(', ')};`,\n 'any other language is refused, never approximated. auto picks',\n 'structural for those languages and lexical for the rest, and',\n 'the report names whichever one actually ran.',\n ],\n },\n ignore: {\n label: '--ignore <entry>',\n body: () => [\n 'Repeatable. Replaces the default ignore list',\n // Read off DEFAULT_REPO_IGNORE, never re-typed: help text that lists a default\n // by hand is help text that will one day describe a different default.\n `(${DEFAULT_REPO_IGNORE.join(', ')}): a bare name matches any path segment,`,\n 'an entry containing / is a root-relative prefix.',\n ],\n },\n cache: {\n label: '--cache <dir>',\n body: () => [\n 'Directory for the tags cache, keyed by content',\n 'hash. Only when given does the map write to disk at all.',\n ],\n },\n harness: {\n label: '--harness <id>',\n body: () => [\n 'Skip harness detection and target one id:',\n ...optionList(HARNESS_IDS, OPTION_BODY_WIDTH),\n ],\n },\n strict: {\n label: '--strict',\n body: () => [\n 'Turn any lint finding into exit 1, for CI. Findings are',\n 'advisory by default: the rules are heuristics about somebody',\n \"else's house style, and enforcing them uninvited would be\",\n `${CLI_NAME} deciding it. A budget you set yourself is different —`,\n 'exceeding agents.budgetBytes exits 1 with or without --strict.',\n ],\n },\n json: {\n label: '--json',\n body: () => [\n 'Print a JSON envelope on stdout instead of the text:',\n '{ format, result, elided } for a smelt run — `result` is',\n 'the SmeltResult verbatim, `elided` carries the bytes, so',\n 'the envelope can be reconstructed; feed it back with',\n '--reconstruct. For map: { format, map }, the RepoMap',\n 'structure verbatim. For agents lint: { format, report },',\n 'the measured levels and every finding with its rule id.',\n ],\n },\n reconstruct: {\n label: '--reconstruct',\n body: () => [\n 'Read a --json envelope and print the original text, byte for',\n 'byte. This is Law 3 you can run from a shell.',\n ],\n },\n help: { label: '-h, --help', body: () => ['This text.'] },\n version: { label: '--version', body: () => ['The package version.'] },\n};\n\n/**\n * `--budget` has no built-in default, for the same reason `smelt()` has none: a budget\n * smelt invented would be smelt deciding how much of the caller's context to throw\n * away, silently, at a number nobody chose. A *missing* flag is not an error here,\n * though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose\n * explicitly, and the verb's `resolve` errors only when neither exists. A malformed\n * value is always an error.\n *\n * It lives with the flag rather than with a verb because two verbs own `--budget`, and\n * the two of them agreeing on what \"4kb\" means is not something to leave to chance.\n *\n * The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a\n * leading `-` are answered by a digits-only test before anything numeric happens. The\n * *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also\n * where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`\n * here and `\"budgetBytes\"` there.\n */\nexport function parseBudget(raw: string | undefined): number | undefined {\n if (raw === undefined) return undefined;\n if (!/^\\d+$/.test(raw)) throw refuseBudget('not-an-integer', raw);\n const value = Number(raw);\n const fault = budgetFault(value);\n if (fault !== undefined) throw refuseBudget(fault, raw);\n return value;\n}\n\n/** The malformed-budget refusal, in the CLI's currency: prefixed, and exit 2. */\nfunction refuseBudget(fault: BudgetFault, raw: string): CliUsageError {\n return new CliUsageError(`${CLI_NAME}: ${budgetMalformed(fault, '--budget', raw)}`);\n}\n"]}
1
+ {"version":3,"file":"flags.js","sourceRoot":"","sources":["../../../src/cli/subcommands/flags.ts"],"names":[],"mappings":"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","sourcesContent":["import { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESS_IDS } from '../../harness/registry.ts';\nimport { budgetFault, budgetMalformed } from '../../ops/inputs.ts';\nimport type { BudgetFault } from '../../ops/inputs.ts';\nimport { STRATEGIES, DEFAULT_STRATEGY } from '../../plan/planners.ts';\nimport { STRUCTURAL_LANGUAGES } from '../../plan/structural.ts';\nimport { DEFAULT_REPO_IGNORE } from '../../repomap/map.ts';\nimport { CLI_NAME } from '../shell.ts';\n\n/**\n * Every flag the CLI accepts, once.\n *\n * The table is the companion the {@link Subcommand} registry needs: a verb declares\n * the flags it owns as `FlagName[]`, so the type of that list — and therefore whether\n * a verb can claim a flag that does not exist — comes from here. It carries three\n * things that used to be written in three places:\n *\n * 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a\n * literal inside `parseSmeltArgs`.\n * 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is\n * spelled once and every verb's ownership list typechecks against it.\n * 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`\n * prefix, which is *not* stored here: it is generated from the registry's\n * ownership (see `renderOptions` in `cli/usage.ts`), because \"which verb owns\n * this flag\" is a fact the registry already holds and prose that restates it is\n * prose that can go stale.\n *\n * Key order is meaningful: it is the order the OPTIONS block renders and the order a\n * refusal lists flags in, so keep it stable and append new flags where they read best.\n */\nexport const CLI_FLAGS = {\n budget: { type: 'string' },\n focus: { type: 'string', multiple: true },\n producer: { type: 'string' },\n language: { type: 'string' },\n strategy: { type: 'string' },\n ignore: { type: 'string', multiple: true },\n cache: { type: 'string' },\n /**\n * Repeatable because `setup` wires several harnesses in one run. `hooks` takes one\n * per run and refuses a second id in its own parse — arity is a per-verb fact,\n * expressed where the verb validates, with the one generated refusal; the table\n * records how argv is read, not how many a verb accepts.\n */\n harness: { type: 'string', multiple: true },\n yes: { type: 'boolean' },\n 'no-mcp': { type: 'boolean' },\n strict: { type: 'boolean' },\n json: { type: 'boolean' },\n reconstruct: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean' },\n} as const;\n\n/** Every flag name, as a type. A verb cannot claim a flag that is not in the table. */\nexport type FlagName = keyof typeof CLI_FLAGS;\n\n/**\n * The flags answered *before* any verb, so no verb owns them and no verb may refuse\n * them: `smelt map --help` prints the help, exactly as it always has. Every other flag\n * belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.\n */\nexport const GLOBAL_FLAGS = ['help', 'version'] as const satisfies readonly FlagName[];\n\n/** A flag a verb can own — everything but the two global ones. */\nexport type VerbFlag = Exclude<FlagName, (typeof GLOBAL_FLAGS)[number]>;\n\n/** Every ownable flag, in table order: the list a refusal checks a verb against. */\nexport const VERB_FLAGS: readonly VerbFlag[] = Object.keys(CLI_FLAGS).filter(\n (name): name is VerbFlag => !(GLOBAL_FLAGS as readonly string[]).includes(name),\n);\n\n/**\n * `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.\n *\n * Every refusal that names more than one flag spells the list this way: the\n * ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`\n * refusal in `./smelt.ts`. It lives with the table because the *order* is the\n * table's — a refusal that listed flags in the order the user happened to type them\n * would read differently every time, and two refusals disagreeing about how to spell\n * the same pair is the kind of drift this file exists to remove.\n */\nexport function flagList(flags: readonly VerbFlag[]): string {\n const spelled = VERB_FLAGS.filter((flag) => flags.includes(flag)).map((flag) => `--${flag}`);\n if (spelled.length <= 1) return spelled.join('');\n return `${spelled.slice(0, -1).join(', ')} and ${spelled.at(-1) ?? ''}`;\n}\n\n/** What one flag's parsed value looks like, derived from how `parseArgs` was told to read it. */\ntype FlagValue<F> = F extends { readonly type: 'boolean' }\n ? boolean\n : F extends { readonly multiple: true }\n ? readonly string[]\n : string;\n\n/**\n * The parsed flags, as every `Subcommand.parse` sees them: one optional field per\n * flag, typed by the table above. Absent means the user did not type it — which is\n * the only thing a refusal needs to know, and the only thing a verb may act on.\n */\nexport type FlagValues = { readonly [K in FlagName]?: FlagValue<(typeof CLI_FLAGS)[K]> };\n\n/** How one flag appears in the OPTIONS block of `--help`. */\nexport interface FlagHelp {\n /** The left column, e.g. `--budget <bytes>` or `-h, --help`. */\n readonly label: string;\n /**\n * The description, already wrapped to the OPTIONS column — a function because three\n * entries render a registry (`SUPPORTED_LANGUAGES`, `STRATEGIES` +\n * `STRUCTURAL_LANGUAGES`, `HARNESS_IDS`) rather than a hand-typed list. The\n * ownership prefix is not here; it is generated.\n */\n body(): readonly string[];\n}\n\n/**\n * A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the\n * hand-typed version wrapped. `--strategy` and `--language` render their registries on\n * one line because they fit; the harness ids do not, and a list long enough to wrap is\n * exactly the list nobody keeps in sync by hand.\n */\nexport function optionList(items: readonly string[], width: number): readonly string[] {\n const lines: string[] = [];\n let line = '';\n items.forEach((item, index) => {\n const word = index === items.length - 1 ? `${item}.` : `${item},`;\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > width) {\n lines.push(line);\n line = word;\n } else {\n line = candidate;\n }\n });\n lines.push(line);\n return lines;\n}\n\n/** The width an OPTIONS description wraps within, once its 23-column indent is removed. */\nconst OPTION_BODY_WIDTH = 65;\n\n/**\n * The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag\n * added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text\n * cannot fall behind what the parser accepts.\n */\nexport const FLAG_HELP: Readonly<Record<FlagName, FlagHelp>> = {\n budget: {\n label: '--budget <bytes>',\n body: () => [\n 'Required, unless smelt.config.json sets defaultBudgetBytes.',\n 'Soft ceiling for the output, in UTF-8 bytes (for map: a hard',\n 'ceiling, met by construction). No built-in default: a budget',\n `${CLI_NAME} invented would decide for you.`,\n ],\n },\n focus: {\n label: '--focus <term>',\n body: () => [\n 'What you were looking for. Repeatable. Matching regions and',\n 'their context survive; the runs between them collapse. For',\n 'map: symbols matching a term (by name or path) are promoted',\n 'to the front of the fill order, ranks unchanged.',\n ],\n },\n producer: {\n label: '--producer <cmd>',\n body: () => [\n 'The command whose output this is, e.g. \"grep -C 3 foo src\". When',\n 'no --focus is given, the focus is derived from it exactly as the',\n 'hooks guard derives it: a search pattern, only when the output',\n 'also holds non-matching lines (context flags). cat, diffs and',\n 'logs name no term; the head and tail are kept instead.',\n ],\n },\n language: {\n label: '--language <id>',\n body: () => [`Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.`],\n },\n strategy: {\n label: '--strategy <id>',\n body: () => [\n `${STRATEGIES.join(', ')}. Defaults to ${DEFAULT_STRATEGY}, unless`,\n 'smelt.config.json says otherwise. structural parses',\n `${STRUCTURAL_LANGUAGES.join(', ')};`,\n 'any other language is refused, never approximated. json cuts',\n 'members and elements; diff cuts files and hunks; each refuses',\n 'any other content. auto picks by content kind first (json,',\n 'diff), then structural for those languages and lexical for the',\n 'rest, and the report names whichever one actually ran.',\n ],\n },\n ignore: {\n label: '--ignore <entry>',\n body: () => [\n 'Repeatable. Replaces the default ignore list',\n // Read off DEFAULT_REPO_IGNORE, never re-typed: help text that lists a default\n // by hand is help text that will one day describe a different default.\n `(${DEFAULT_REPO_IGNORE.join(', ')}): a bare name matches any path segment,`,\n 'an entry containing / is a root-relative prefix.',\n ],\n },\n cache: {\n label: '--cache <dir>',\n body: () => [\n 'Directory for the tags cache, keyed by content',\n 'hash. Only when given does the map write to disk at all.',\n ],\n },\n harness: {\n label: '--harness <id>',\n body: () => [\n 'Skip harness detection and target one id:',\n ...optionList(HARNESS_IDS, OPTION_BODY_WIDTH),\n 'Repeatable for setup; hooks takes one per run.',\n ],\n },\n yes: {\n label: '--yes',\n body: () => [\n \"Non-interactive setup: the recipe's defaults, printed\",\n 'loudly as they are applied. Existing files are never',\n 'overwritten — skipped with a note; hooks install edits',\n 'them, and it asks per file.',\n ],\n },\n 'no-mcp': {\n label: '--no-mcp',\n body: () => [\n 'Setup only: skip the MCP registration step — the',\n 'printed command and its note — for a hooks-only',\n 'setup.',\n ],\n },\n strict: {\n label: '--strict',\n body: () => [\n 'Turn any lint finding into exit 1, for CI. Findings are',\n 'advisory by default: the rules are heuristics about somebody',\n \"else's house style, and enforcing them uninvited would be\",\n `${CLI_NAME} deciding it. A budget you set yourself is different —`,\n 'exceeding agents.budgetBytes exits 1 with or without --strict.',\n ],\n },\n json: {\n label: '--json',\n body: () => [\n 'Print a JSON envelope on stdout instead of the text:',\n '{ format, result, elided } for a smelt run — `result` is',\n 'the SmeltResult verbatim, `elided` carries the bytes, so',\n 'the envelope can be reconstructed; feed it back with',\n '--reconstruct. For map: { format, map }, the RepoMap',\n 'structure verbatim. For agents lint: { format, report },',\n 'the measured levels and every finding with its rule id.',\n ],\n },\n reconstruct: {\n label: '--reconstruct',\n body: () => [\n 'Read a --json envelope and print the original text, byte for',\n 'byte. This is Law 3 you can run from a shell.',\n ],\n },\n help: { label: '-h, --help', body: () => ['This text.'] },\n version: { label: '--version', body: () => ['The package version.'] },\n};\n\n/**\n * `--budget` has no built-in default, for the same reason `smelt()` has none: a budget\n * smelt invented would be smelt deciding how much of the caller's context to throw\n * away, silently, at a number nobody chose. A *missing* flag is not an error here,\n * though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose\n * explicitly, and the verb's `resolve` errors only when neither exists. A malformed\n * value is always an error.\n *\n * It lives with the flag rather than with a verb because two verbs own `--budget`, and\n * the two of them agreeing on what \"4kb\" means is not something to leave to chance.\n *\n * The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a\n * leading `-` are answered by a digits-only test before anything numeric happens. The\n * *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also\n * where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`\n * here and `\"budgetBytes\"` there.\n */\nexport function parseBudget(raw: string | undefined): number | undefined {\n if (raw === undefined) return undefined;\n if (!/^\\d+$/.test(raw)) throw refuseBudget('not-an-integer', raw);\n const value = Number(raw);\n const fault = budgetFault(value);\n if (fault !== undefined) throw refuseBudget(fault, raw);\n return value;\n}\n\n/** The malformed-budget refusal, in the CLI's currency: prefixed, and exit 2. */\nfunction refuseBudget(fault: BudgetFault, raw: string): CliUsageError {\n return new CliUsageError(`${CLI_NAME}: ${budgetMalformed(fault, '--budget', raw)}`);\n}\n"]}