@smeltjs/core 0.1.0 → 0.3.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 (319) hide show
  1. package/README.md +4 -4
  2. package/dist/apply.d.ts.map +1 -1
  3. package/dist/apply.js.map +1 -1
  4. package/dist/cache/prefix.d.ts.map +1 -1
  5. package/dist/cache/prefix.js.map +1 -1
  6. package/dist/cli/args.d.ts +16 -59
  7. package/dist/cli/args.d.ts.map +1 -1
  8. package/dist/cli/args.js +18 -277
  9. package/dist/cli/args.js.map +1 -1
  10. package/dist/cli/bin.d.ts.map +1 -1
  11. package/dist/cli/bin.js.map +1 -1
  12. package/dist/cli/config.d.ts +62 -0
  13. package/dist/cli/config.d.ts.map +1 -1
  14. package/dist/cli/config.js +87 -2
  15. package/dist/cli/config.js.map +1 -1
  16. package/dist/cli/hooks.d.ts +148 -0
  17. package/dist/cli/hooks.d.ts.map +1 -0
  18. package/dist/cli/hooks.js +835 -0
  19. package/dist/cli/hooks.js.map +1 -0
  20. package/dist/cli/init.d.ts +30 -13
  21. package/dist/cli/init.d.ts.map +1 -1
  22. package/dist/cli/init.js +173 -38
  23. package/dist/cli/init.js.map +1 -1
  24. package/dist/cli/report.d.ts.map +1 -1
  25. package/dist/cli/report.js +2 -2
  26. package/dist/cli/report.js.map +1 -1
  27. package/dist/cli/run.d.ts +25 -88
  28. package/dist/cli/run.d.ts.map +1 -1
  29. package/dist/cli/run.js +32 -322
  30. package/dist/cli/run.js.map +1 -1
  31. package/dist/cli/shell.d.ts +106 -0
  32. package/dist/cli/shell.d.ts.map +1 -0
  33. package/dist/cli/shell.js +100 -0
  34. package/dist/cli/shell.js.map +1 -0
  35. package/dist/cli/subcommands/flags.d.ts +141 -0
  36. package/dist/cli/subcommands/flags.d.ts.map +1 -0
  37. package/dist/cli/subcommands/flags.js +208 -0
  38. package/dist/cli/subcommands/flags.js.map +1 -0
  39. package/dist/cli/subcommands/hooks.d.ts +18 -0
  40. package/dist/cli/subcommands/hooks.d.ts.map +1 -0
  41. package/dist/cli/subcommands/hooks.js +64 -0
  42. package/dist/cli/subcommands/hooks.js.map +1 -0
  43. package/dist/cli/subcommands/init.d.ts +19 -0
  44. package/dist/cli/subcommands/init.d.ts.map +1 -0
  45. package/dist/cli/subcommands/init.js +44 -0
  46. package/dist/cli/subcommands/init.js.map +1 -0
  47. package/dist/cli/subcommands/map.d.ts +75 -0
  48. package/dist/cli/subcommands/map.d.ts.map +1 -0
  49. package/dist/cli/subcommands/map.js +132 -0
  50. package/dist/cli/subcommands/map.js.map +1 -0
  51. package/dist/cli/subcommands/registry.d.ts +80 -0
  52. package/dist/cli/subcommands/registry.d.ts.map +1 -0
  53. package/dist/cli/subcommands/registry.js +138 -0
  54. package/dist/cli/subcommands/registry.js.map +1 -0
  55. package/dist/cli/subcommands/retrieve.d.ts +74 -0
  56. package/dist/cli/subcommands/retrieve.d.ts.map +1 -0
  57. package/dist/cli/subcommands/retrieve.js +84 -0
  58. package/dist/cli/subcommands/retrieve.js.map +1 -0
  59. package/dist/cli/subcommands/smelt.d.ts +107 -0
  60. package/dist/cli/subcommands/smelt.d.ts.map +1 -0
  61. package/dist/cli/subcommands/smelt.js +304 -0
  62. package/dist/cli/subcommands/smelt.js.map +1 -0
  63. package/dist/cli/subcommands/stats.d.ts +43 -0
  64. package/dist/cli/subcommands/stats.d.ts.map +1 -0
  65. package/dist/cli/subcommands/stats.js +62 -0
  66. package/dist/cli/subcommands/stats.js.map +1 -0
  67. package/dist/cli/subcommands/subcommand.d.ts +113 -0
  68. package/dist/cli/subcommands/subcommand.d.ts.map +1 -0
  69. package/dist/cli/subcommands/subcommand.js +3 -0
  70. package/dist/cli/subcommands/subcommand.js.map +1 -0
  71. package/dist/cli/usage.d.ts +3 -0
  72. package/dist/cli/usage.d.ts.map +1 -0
  73. package/dist/cli/usage.js +95 -0
  74. package/dist/cli/usage.js.map +1 -0
  75. package/dist/detect.d.ts.map +1 -1
  76. package/dist/detect.js.map +1 -1
  77. package/dist/errors.d.ts +21 -1
  78. package/dist/errors.d.ts.map +1 -1
  79. package/dist/errors.js +33 -1
  80. package/dist/errors.js.map +1 -1
  81. package/dist/harness/aider.d.ts +10 -0
  82. package/dist/harness/aider.d.ts.map +1 -0
  83. package/dist/harness/aider.js +21 -0
  84. package/dist/harness/aider.js.map +1 -0
  85. package/dist/harness/claude-code.d.ts +3 -0
  86. package/dist/harness/claude-code.d.ts.map +1 -0
  87. package/dist/harness/claude-code.js +68 -0
  88. package/dist/harness/claude-code.js.map +1 -0
  89. package/dist/harness/cline.d.ts +3 -0
  90. package/dist/harness/cline.d.ts.map +1 -0
  91. package/dist/harness/cline.js +54 -0
  92. package/dist/harness/cline.js.map +1 -0
  93. package/dist/harness/codex.d.ts +3 -0
  94. package/dist/harness/codex.d.ts.map +1 -0
  95. package/dist/harness/codex.js +91 -0
  96. package/dist/harness/codex.js.map +1 -0
  97. package/dist/harness/cursor.d.ts +3 -0
  98. package/dist/harness/cursor.d.ts.map +1 -0
  99. package/dist/harness/cursor.js +54 -0
  100. package/dist/harness/cursor.js.map +1 -0
  101. package/dist/harness/gemini.d.ts +3 -0
  102. package/dist/harness/gemini.d.ts.map +1 -0
  103. package/dist/harness/gemini.js +59 -0
  104. package/dist/harness/gemini.js.map +1 -0
  105. package/dist/harness/grok.d.ts +3 -0
  106. package/dist/harness/grok.d.ts.map +1 -0
  107. package/dist/harness/grok.js +46 -0
  108. package/dist/harness/grok.js.map +1 -0
  109. package/dist/harness/hermes.d.ts +3 -0
  110. package/dist/harness/hermes.d.ts.map +1 -0
  111. package/dist/harness/hermes.js +71 -0
  112. package/dist/harness/hermes.js.map +1 -0
  113. package/dist/harness/kilocode.d.ts +3 -0
  114. package/dist/harness/kilocode.d.ts.map +1 -0
  115. package/dist/harness/kilocode.js +35 -0
  116. package/dist/harness/kilocode.js.map +1 -0
  117. package/dist/harness/opencode.d.ts +3 -0
  118. package/dist/harness/opencode.d.ts.map +1 -0
  119. package/dist/harness/opencode.js +97 -0
  120. package/dist/harness/opencode.js.map +1 -0
  121. package/dist/harness/paths.d.ts +17 -0
  122. package/dist/harness/paths.d.ts.map +1 -0
  123. package/dist/harness/paths.js +45 -0
  124. package/dist/harness/paths.js.map +1 -0
  125. package/dist/harness/profile.d.ts +184 -0
  126. package/dist/harness/profile.d.ts.map +1 -0
  127. package/dist/harness/profile.js +26 -0
  128. package/dist/harness/profile.js.map +1 -0
  129. package/dist/harness/registry.d.ts +51 -0
  130. package/dist/harness/registry.d.ts.map +1 -0
  131. package/dist/harness/registry.js +87 -0
  132. package/dist/harness/registry.js.map +1 -0
  133. package/dist/harness/snippet.d.ts +19 -0
  134. package/dist/harness/snippet.d.ts.map +1 -0
  135. package/dist/harness/snippet.js +41 -0
  136. package/dist/harness/snippet.js.map +1 -0
  137. package/dist/hash.d.ts.map +1 -1
  138. package/dist/hash.js.map +1 -1
  139. package/dist/hooks/guard-core.d.ts +164 -0
  140. package/dist/hooks/guard-core.d.ts.map +1 -0
  141. package/dist/hooks/guard-core.js +491 -0
  142. package/dist/hooks/guard-core.js.map +1 -0
  143. package/dist/hooks/shim.d.ts +176 -0
  144. package/dist/hooks/shim.d.ts.map +1 -0
  145. package/dist/hooks/shim.js +192 -0
  146. package/dist/hooks/shim.js.map +1 -0
  147. package/dist/hooks/shims/claude-code.d.ts +10 -0
  148. package/dist/hooks/shims/claude-code.d.ts.map +1 -0
  149. package/dist/hooks/shims/claude-code.js +14 -0
  150. package/dist/hooks/shims/claude-code.js.map +1 -0
  151. package/dist/hooks/shims/cline.d.ts +10 -0
  152. package/dist/hooks/shims/cline.d.ts.map +1 -0
  153. package/dist/hooks/shims/cline.js +14 -0
  154. package/dist/hooks/shims/cline.js.map +1 -0
  155. package/dist/hooks/shims/codex.d.ts +10 -0
  156. package/dist/hooks/shims/codex.d.ts.map +1 -0
  157. package/dist/hooks/shims/codex.js +14 -0
  158. package/dist/hooks/shims/codex.js.map +1 -0
  159. package/dist/hooks/shims/cursor.d.ts +10 -0
  160. package/dist/hooks/shims/cursor.d.ts.map +1 -0
  161. package/dist/hooks/shims/cursor.js +14 -0
  162. package/dist/hooks/shims/cursor.js.map +1 -0
  163. package/dist/hooks/shims/gemini.d.ts +10 -0
  164. package/dist/hooks/shims/gemini.d.ts.map +1 -0
  165. package/dist/hooks/shims/gemini.js +14 -0
  166. package/dist/hooks/shims/gemini.js.map +1 -0
  167. package/dist/hooks/shims/grok.d.ts +10 -0
  168. package/dist/hooks/shims/grok.d.ts.map +1 -0
  169. package/dist/hooks/shims/grok.js +14 -0
  170. package/dist/hooks/shims/grok.js.map +1 -0
  171. package/dist/hooks/shims/hermes.d.ts +10 -0
  172. package/dist/hooks/shims/hermes.d.ts.map +1 -0
  173. package/dist/hooks/shims/hermes.js +14 -0
  174. package/dist/hooks/shims/hermes.js.map +1 -0
  175. package/dist/index.d.ts +33 -76
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +13 -67
  178. package/dist/index.js.map +1 -1
  179. package/dist/lang/bash.d.ts.map +1 -1
  180. package/dist/lang/bash.js.map +1 -1
  181. package/dist/lang/c.d.ts.map +1 -1
  182. package/dist/lang/c.js.map +1 -1
  183. package/dist/lang/c_sharp.d.ts.map +1 -1
  184. package/dist/lang/c_sharp.js.map +1 -1
  185. package/dist/lang/cpp.d.ts.map +1 -1
  186. package/dist/lang/cpp.js.map +1 -1
  187. package/dist/lang/go.d.ts.map +1 -1
  188. package/dist/lang/go.js.map +1 -1
  189. package/dist/lang/java.d.ts.map +1 -1
  190. package/dist/lang/java.js.map +1 -1
  191. package/dist/lang/javascript.d.ts.map +1 -1
  192. package/dist/lang/javascript.js.map +1 -1
  193. package/dist/lang/kotlin.d.ts.map +1 -1
  194. package/dist/lang/kotlin.js.map +1 -1
  195. package/dist/lang/php.d.ts.map +1 -1
  196. package/dist/lang/php.js.map +1 -1
  197. package/dist/lang/profile.d.ts.map +1 -1
  198. package/dist/lang/profile.js.map +1 -1
  199. package/dist/lang/python.d.ts.map +1 -1
  200. package/dist/lang/python.js.map +1 -1
  201. package/dist/lang/registry.d.ts.map +1 -1
  202. package/dist/lang/registry.js.map +1 -1
  203. package/dist/lang/ruby.d.ts.map +1 -1
  204. package/dist/lang/ruby.js.map +1 -1
  205. package/dist/lang/rust.d.ts.map +1 -1
  206. package/dist/lang/rust.js.map +1 -1
  207. package/dist/lang/swift.d.ts.map +1 -1
  208. package/dist/lang/swift.js.map +1 -1
  209. package/dist/lang/tsx.d.ts.map +1 -1
  210. package/dist/lang/tsx.js.map +1 -1
  211. package/dist/lang/typescript.d.ts.map +1 -1
  212. package/dist/lang/typescript.js.map +1 -1
  213. package/dist/net/policy.d.ts +18 -1
  214. package/dist/net/policy.d.ts.map +1 -1
  215. package/dist/net/policy.js +7 -4
  216. package/dist/net/policy.js.map +1 -1
  217. package/dist/ops/index.d.ts +39 -0
  218. package/dist/ops/index.d.ts.map +1 -0
  219. package/dist/ops/index.js +37 -0
  220. package/dist/ops/index.js.map +1 -0
  221. package/dist/ops/inputs.d.ts +170 -0
  222. package/dist/ops/inputs.d.ts.map +1 -0
  223. package/dist/ops/inputs.js +142 -0
  224. package/dist/ops/inputs.js.map +1 -0
  225. package/dist/ops/verbs.d.ts +136 -0
  226. package/dist/ops/verbs.d.ts.map +1 -0
  227. package/dist/ops/verbs.js +76 -0
  228. package/dist/ops/verbs.js.map +1 -0
  229. package/dist/plan/auto.d.ts +69 -0
  230. package/dist/plan/auto.d.ts.map +1 -0
  231. package/dist/plan/auto.js +70 -0
  232. package/dist/plan/auto.js.map +1 -0
  233. package/dist/plan/budget.d.ts +21 -0
  234. package/dist/plan/budget.d.ts.map +1 -0
  235. package/dist/plan/budget.js +26 -0
  236. package/dist/plan/budget.js.map +1 -0
  237. package/dist/plan/grammar.d.ts +14 -0
  238. package/dist/plan/grammar.d.ts.map +1 -1
  239. package/dist/plan/grammar.js +47 -6
  240. package/dist/plan/grammar.js.map +1 -1
  241. package/dist/plan/lexical.d.ts.map +1 -1
  242. package/dist/plan/lexical.js +1 -7
  243. package/dist/plan/lexical.js.map +1 -1
  244. package/dist/plan/planners.d.ts +35 -2
  245. package/dist/plan/planners.d.ts.map +1 -1
  246. package/dist/plan/planners.js +35 -2
  247. package/dist/plan/planners.js.map +1 -1
  248. package/dist/plan/structural.d.ts +19 -5
  249. package/dist/plan/structural.d.ts.map +1 -1
  250. package/dist/plan/structural.js +180 -33
  251. package/dist/plan/structural.js.map +1 -1
  252. package/dist/repomap/cache.d.ts +48 -0
  253. package/dist/repomap/cache.d.ts.map +1 -1
  254. package/dist/repomap/cache.js +112 -13
  255. package/dist/repomap/cache.js.map +1 -1
  256. package/dist/repomap/io.d.ts +24 -0
  257. package/dist/repomap/io.d.ts.map +1 -0
  258. package/dist/repomap/io.js +34 -0
  259. package/dist/repomap/io.js.map +1 -0
  260. package/dist/repomap/map.d.ts +69 -6
  261. package/dist/repomap/map.d.ts.map +1 -1
  262. package/dist/repomap/map.js +104 -22
  263. package/dist/repomap/map.js.map +1 -1
  264. package/dist/repomap/rank.d.ts +26 -2
  265. package/dist/repomap/rank.d.ts.map +1 -1
  266. package/dist/repomap/rank.js +20 -0
  267. package/dist/repomap/rank.js.map +1 -1
  268. package/dist/repomap/reader.d.ts +73 -0
  269. package/dist/repomap/reader.d.ts.map +1 -0
  270. package/dist/repomap/reader.js +30 -0
  271. package/dist/repomap/reader.js.map +1 -0
  272. package/dist/repomap/tags.d.ts.map +1 -1
  273. package/dist/repomap/tags.js.map +1 -1
  274. package/dist/retrieve.d.ts +8 -0
  275. package/dist/retrieve.d.ts.map +1 -1
  276. package/dist/retrieve.js +12 -1
  277. package/dist/retrieve.js.map +1 -1
  278. package/dist/smelter.d.ts +79 -0
  279. package/dist/smelter.d.ts.map +1 -0
  280. package/dist/smelter.js +65 -0
  281. package/dist/smelter.js.map +1 -0
  282. package/dist/stages.d.ts.map +1 -1
  283. package/dist/stages.js +2 -2
  284. package/dist/stages.js.map +1 -1
  285. package/dist/stats.d.ts +2 -1
  286. package/dist/stats.d.ts.map +1 -1
  287. package/dist/stats.js +2 -1
  288. package/dist/stats.js.map +1 -1
  289. package/dist/store-dir.d.ts +43 -4
  290. package/dist/store-dir.d.ts.map +1 -1
  291. package/dist/store-dir.js +48 -5
  292. package/dist/store-dir.js.map +1 -1
  293. package/dist/store.d.ts +6 -0
  294. package/dist/store.d.ts.map +1 -1
  295. package/dist/store.js +6 -0
  296. package/dist/store.js.map +1 -1
  297. package/dist/types.d.ts +35 -3
  298. package/dist/types.d.ts.map +1 -1
  299. package/dist/types.js.map +1 -1
  300. package/grammars/tree-sitter-bash.wasm +0 -0
  301. package/grammars/tree-sitter-c.wasm +0 -0
  302. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  303. package/grammars/tree-sitter-cpp.wasm +0 -0
  304. package/grammars/tree-sitter-go.wasm +0 -0
  305. package/grammars/tree-sitter-java.wasm +0 -0
  306. package/grammars/tree-sitter-javascript.wasm +0 -0
  307. package/grammars/tree-sitter-kotlin.wasm +0 -0
  308. package/grammars/tree-sitter-php.wasm +0 -0
  309. package/grammars/tree-sitter-python.wasm +0 -0
  310. package/grammars/tree-sitter-ruby.wasm +0 -0
  311. package/grammars/tree-sitter-rust.wasm +0 -0
  312. package/grammars/tree-sitter-swift.wasm +0 -0
  313. package/grammars/tree-sitter-tsx.wasm +0 -0
  314. package/grammars/tree-sitter-typescript.wasm +0 -0
  315. package/package.json +22 -14
  316. package/dist/cli/resolve.d.ts +0 -103
  317. package/dist/cli/resolve.d.ts.map +0 -1
  318. package/dist/cli/resolve.js +0 -106
  319. package/dist/cli/resolve.js.map +0 -1
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The CLI's edge, in one module: the name people type, where its bytes come from and
3
+ * go, and the codes it hands back to the shell.
4
+ *
5
+ * These three used to live in `args.ts` and `run.ts` — the two modules that now read
6
+ * the subcommand registry. A verb file needs all three, so leaving them there would
7
+ * make `args.ts → subcommands/* → args.ts` a cycle, which is exactly the shape that
8
+ * once forced the `--harness` help list to be hand-typed (see
9
+ * `src/harness/registry.ts`). **This file imports nothing**, so every module under
10
+ * `cli/` can read it and nothing has to be written twice to avoid a loop.
11
+ */
12
+ /** The command people type. Independent of the package name. */
13
+ export declare const CLI_NAME = "smelt";
14
+ /**
15
+ * Where a wizard's answers come from: lines of text arriving over time, and nothing
16
+ * more. `process.stdin` is one, a scripted `Readable.from([...])` in a test is
17
+ * another, an async generator is a third.
18
+ *
19
+ * **Stated structurally, never as `NodeJS.ReadableStream`.** A `.d.ts` that names an
20
+ * ambient namespace only typechecks inside a compilation that pulled `@types/node`
21
+ * into its *global* scope, and a consumer building with `skipLibCheck: false` and a
22
+ * narrowed `types` array (or no `@types/node` at its own root — the ordinary case
23
+ * under pnpm) fails on smelt's declarations rather than on their own code. Naming the
24
+ * node type by import does not help: TypeScript resolves `node:stream` — and bare
25
+ * `stream` — only through the same globally-included `@types/node`. So the published
26
+ * surface describes the shape smelt actually consumes, which needs no node types at
27
+ * all, and {@link answerReader} is the one adapter that reads it.
28
+ * `test/guards/packaging.test.ts` holds the shipped declarations to it.
29
+ */
30
+ export type AnswerStream = AsyncIterable<string | Uint8Array>;
31
+ /**
32
+ * One wizard's answers, one line at a time, plus the release that ends the process.
33
+ *
34
+ * `release` is not housekeeping — it is the difference between a wizard that exits and
35
+ * one the user has to Ctrl-C. See {@link answerReader}.
36
+ */
37
+ export interface AnswerReader {
38
+ /** The next line, without its terminator, or `undefined` once input has ended. */
39
+ next(): Promise<string | undefined>;
40
+ /** Stop reading and let go of the source, so nothing it owns keeps the loop alive. */
41
+ release(): Promise<void>;
42
+ }
43
+ /**
44
+ * Read {@link AnswerStream} as lines — and, when the wizard is done, **let go of it**.
45
+ *
46
+ * This exists because the obvious adapter is a trap. Wrapping the answer stream in
47
+ * `Readable.from(...)` and handing that to `readline` reads the source through *its*
48
+ * async iterator, and closing the readline interface or destroying the wrapper ends
49
+ * only the wrapper: the source is left mid-`next()`, still subscribed, still holding
50
+ * its handle. On the real CLI that source is `process.stdin`, so `smelt init` wrote
51
+ * every file, printed `Done.` and then sat there forever — a hang only visible on an
52
+ * open pipe or a TTY, because EOF happens to end the iteration by itself, and EOF is
53
+ * what every scripted test hands it.
54
+ *
55
+ * So the source's own iterator is held here and nothing else touches it. `release`
56
+ * calls its `return()`, which is the contract an async iterable already has for "I am
57
+ * finished with you": `process.stdin`'s destroys the stream and unrefs the handle, an
58
+ * async generator runs its `finally`, and a plain array iterator does nothing at all.
59
+ * Crucially `release` is called between reads, never during one — an iterator awaiting
60
+ * `next()` cannot be returned out of, which is the very state the `Readable.from`
61
+ * wrapper left the source in.
62
+ *
63
+ * Bytes are decoded as UTF-8 across chunk boundaries, so a multi-byte character split
64
+ * across two reads survives; `\r\n` and a final line with no terminator both behave as
65
+ * readline did.
66
+ */
67
+ export declare function answerReader(input: AnswerStream): AnswerReader;
68
+ /**
69
+ * Exit codes, and why there are five of them.
70
+ *
71
+ * A CLI that returns 0 whatever happens is the shell-level version of a stub that
72
+ * returns `[]`: the caller cannot tell success from failure, so a pipeline built on it
73
+ * fails silently. **`overBudget` is the load-bearing one.** A plan that did not fit is
74
+ * not an error — smelt refused to cut the regions the caller asked to keep, which is
75
+ * correct — but it is also not success, and a script must be able to see the
76
+ * difference without parsing prose.
77
+ */
78
+ export declare const EXIT: {
79
+ readonly ok: 0;
80
+ readonly overBudget: 1;
81
+ readonly usage: 2;
82
+ readonly refused: 3;
83
+ readonly unexpected: 4;
84
+ };
85
+ /** Where the CLI's bytes come from and go. Injected so `runCli` is testable in-process. */
86
+ export interface CliIo {
87
+ readonly stdout: (text: string) => void;
88
+ readonly stderr: (text: string) => void;
89
+ /** All of stdin, as UTF-8. @throws {CliUsageError} when nothing is piped. */
90
+ readonly stdin: () => string;
91
+ /** The package version, for `--version`. */
92
+ readonly version: string;
93
+ /**
94
+ * Where `smelt.config.json` discovery starts, and where `init` writes. Defaults to
95
+ * the process working directory; tests pass a temp directory to stay hermetic.
96
+ */
97
+ readonly cwd?: string;
98
+ /**
99
+ * Interactive input for `smelt init` — the wizard reads answers line by line, which
100
+ * the one-shot `stdin()` above cannot provide. `bin.ts` passes the real stdin
101
+ * stream; tests pass a scripted one. Absent means `init` is a usage error.
102
+ * See {@link AnswerStream} for why the type is structural.
103
+ */
104
+ readonly initInput?: AnswerStream;
105
+ }
106
+ //# sourceMappingURL=shell.d.ts.map
@@ -0,0 +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"]}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The CLI's edge, in one module: the name people type, where its bytes come from and
3
+ * go, and the codes it hands back to the shell.
4
+ *
5
+ * These three used to live in `args.ts` and `run.ts` — the two modules that now read
6
+ * the subcommand registry. A verb file needs all three, so leaving them there would
7
+ * make `args.ts → subcommands/* → args.ts` a cycle, which is exactly the shape that
8
+ * once forced the `--harness` help list to be hand-typed (see
9
+ * `src/harness/registry.ts`). **This file imports nothing**, so every module under
10
+ * `cli/` can read it and nothing has to be written twice to avoid a loop.
11
+ */
12
+ /** The command people type. Independent of the package name. */
13
+ export const CLI_NAME = 'smelt';
14
+ /**
15
+ * Read {@link AnswerStream} as lines — and, when the wizard is done, **let go of it**.
16
+ *
17
+ * This exists because the obvious adapter is a trap. Wrapping the answer stream in
18
+ * `Readable.from(...)` and handing that to `readline` reads the source through *its*
19
+ * async iterator, and closing the readline interface or destroying the wrapper ends
20
+ * only the wrapper: the source is left mid-`next()`, still subscribed, still holding
21
+ * its handle. On the real CLI that source is `process.stdin`, so `smelt init` wrote
22
+ * every file, printed `Done.` and then sat there forever — a hang only visible on an
23
+ * open pipe or a TTY, because EOF happens to end the iteration by itself, and EOF is
24
+ * what every scripted test hands it.
25
+ *
26
+ * So the source's own iterator is held here and nothing else touches it. `release`
27
+ * calls its `return()`, which is the contract an async iterable already has for "I am
28
+ * finished with you": `process.stdin`'s destroys the stream and unrefs the handle, an
29
+ * async generator runs its `finally`, and a plain array iterator does nothing at all.
30
+ * Crucially `release` is called between reads, never during one — an iterator awaiting
31
+ * `next()` cannot be returned out of, which is the very state the `Readable.from`
32
+ * wrapper left the source in.
33
+ *
34
+ * Bytes are decoded as UTF-8 across chunk boundaries, so a multi-byte character split
35
+ * across two reads survives; `\r\n` and a final line with no terminator both behave as
36
+ * readline did.
37
+ */
38
+ export function answerReader(input) {
39
+ const iterator = input[Symbol.asyncIterator]();
40
+ const decoder = new TextDecoder('utf-8');
41
+ let pending = '';
42
+ let ended = false;
43
+ const takeLine = () => {
44
+ const newline = pending.indexOf('\n');
45
+ if (newline === -1)
46
+ return undefined;
47
+ const line = pending.slice(0, newline);
48
+ pending = pending.slice(newline + 1);
49
+ return line.endsWith('\r') ? line.slice(0, -1) : line;
50
+ };
51
+ return {
52
+ async next() {
53
+ for (;;) {
54
+ const line = takeLine();
55
+ if (line !== undefined)
56
+ return line;
57
+ if (ended) {
58
+ if (pending === '')
59
+ return undefined;
60
+ const last = pending;
61
+ pending = '';
62
+ return last;
63
+ }
64
+ const step = await iterator.next();
65
+ if (step.done === true) {
66
+ ended = true;
67
+ pending += decoder.decode(); // flush a truncated multi-byte sequence
68
+ continue;
69
+ }
70
+ pending +=
71
+ typeof step.value === 'string'
72
+ ? step.value
73
+ : decoder.decode(step.value, { stream: true });
74
+ }
75
+ },
76
+ async release() {
77
+ ended = true;
78
+ pending = '';
79
+ await iterator.return?.();
80
+ },
81
+ };
82
+ }
83
+ /**
84
+ * Exit codes, and why there are five of them.
85
+ *
86
+ * A CLI that returns 0 whatever happens is the shell-level version of a stub that
87
+ * returns `[]`: the caller cannot tell success from failure, so a pipeline built on it
88
+ * fails silently. **`overBudget` is the load-bearing one.** A plan that did not fit is
89
+ * not an error — smelt refused to cut the regions the caller asked to keep, which is
90
+ * correct — but it is also not success, and a script must be able to see the
91
+ * difference without parsing prose.
92
+ */
93
+ export const EXIT = {
94
+ ok: 0,
95
+ overBudget: 1,
96
+ usage: 2,
97
+ refused: 3,
98
+ unexpected: 4,
99
+ };
100
+ //# sourceMappingURL=shell.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Every flag the CLI accepts, once.
3
+ *
4
+ * The table is the companion the {@link Subcommand} registry needs: a verb declares
5
+ * the flags it owns as `FlagName[]`, so the type of that list — and therefore whether
6
+ * a verb can claim a flag that does not exist — comes from here. It carries three
7
+ * things that used to be written in three places:
8
+ *
9
+ * 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a
10
+ * literal inside `parseSmeltArgs`.
11
+ * 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is
12
+ * spelled once and every verb's ownership list typechecks against it.
13
+ * 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`
14
+ * prefix, which is *not* stored here: it is generated from the registry's
15
+ * ownership (see `renderOptions` in `cli/usage.ts`), because "which verb owns
16
+ * this flag" is a fact the registry already holds and prose that restates it is
17
+ * prose that can go stale.
18
+ *
19
+ * Key order is meaningful: it is the order the OPTIONS block renders and the order a
20
+ * refusal lists flags in, so keep it stable and append new flags where they read best.
21
+ */
22
+ export declare const CLI_FLAGS: {
23
+ readonly budget: {
24
+ readonly type: 'string';
25
+ };
26
+ readonly focus: {
27
+ readonly type: 'string';
28
+ readonly multiple: true;
29
+ };
30
+ readonly language: {
31
+ readonly type: 'string';
32
+ };
33
+ readonly strategy: {
34
+ readonly type: 'string';
35
+ };
36
+ readonly ignore: {
37
+ readonly type: 'string';
38
+ readonly multiple: true;
39
+ };
40
+ readonly cache: {
41
+ readonly type: 'string';
42
+ };
43
+ readonly harness: {
44
+ readonly type: 'string';
45
+ };
46
+ readonly json: {
47
+ readonly type: 'boolean';
48
+ };
49
+ readonly reconstruct: {
50
+ readonly type: 'boolean';
51
+ };
52
+ readonly help: {
53
+ readonly type: 'boolean';
54
+ readonly short: 'h';
55
+ };
56
+ readonly version: {
57
+ readonly type: 'boolean';
58
+ };
59
+ };
60
+ /** Every flag name, as a type. A verb cannot claim a flag that is not in the table. */
61
+ export type FlagName = keyof typeof CLI_FLAGS;
62
+ /**
63
+ * The flags answered *before* any verb, so no verb owns them and no verb may refuse
64
+ * them: `smelt map --help` prints the help, exactly as it always has. Every other flag
65
+ * belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.
66
+ */
67
+ export declare const GLOBAL_FLAGS: readonly ["help", "version"];
68
+ /** A flag a verb can own — everything but the two global ones. */
69
+ export type VerbFlag = Exclude<FlagName, (typeof GLOBAL_FLAGS)[number]>;
70
+ /** Every ownable flag, in table order: the list a refusal checks a verb against. */
71
+ export declare const VERB_FLAGS: readonly VerbFlag[];
72
+ /**
73
+ * `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.
74
+ *
75
+ * Every refusal that names more than one flag spells the list this way: the
76
+ * ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`
77
+ * refusal in `./smelt.ts`. It lives with the table because the *order* is the
78
+ * table's — a refusal that listed flags in the order the user happened to type them
79
+ * would read differently every time, and two refusals disagreeing about how to spell
80
+ * the same pair is the kind of drift this file exists to remove.
81
+ */
82
+ export declare function flagList(flags: readonly VerbFlag[]): string;
83
+ /** What one flag's parsed value looks like, derived from how `parseArgs` was told to read it. */
84
+ type FlagValue<F> = F extends {
85
+ readonly type: 'boolean';
86
+ } ? boolean : F extends {
87
+ readonly multiple: true;
88
+ } ? readonly string[] : string;
89
+ /**
90
+ * The parsed flags, as every `Subcommand.parse` sees them: one optional field per
91
+ * flag, typed by the table above. Absent means the user did not type it — which is
92
+ * the only thing a refusal needs to know, and the only thing a verb may act on.
93
+ */
94
+ export type FlagValues = {
95
+ readonly [K in FlagName]?: FlagValue<(typeof CLI_FLAGS)[K]>;
96
+ };
97
+ /** How one flag appears in the OPTIONS block of `--help`. */
98
+ export interface FlagHelp {
99
+ /** The left column, e.g. `--budget <bytes>` or `-h, --help`. */
100
+ readonly label: string;
101
+ /**
102
+ * The description, already wrapped to the OPTIONS column — a function because three
103
+ * entries render a registry (`SUPPORTED_LANGUAGES`, `STRATEGIES` +
104
+ * `STRUCTURAL_LANGUAGES`, `HARNESS_IDS`) rather than a hand-typed list. The
105
+ * ownership prefix is not here; it is generated.
106
+ */
107
+ body(): readonly string[];
108
+ }
109
+ /**
110
+ * A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the
111
+ * hand-typed version wrapped. `--strategy` and `--language` render their registries on
112
+ * one line because they fit; the harness ids do not, and a list long enough to wrap is
113
+ * exactly the list nobody keeps in sync by hand.
114
+ */
115
+ export declare function optionList(items: readonly string[], width: number): readonly string[];
116
+ /**
117
+ * The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag
118
+ * added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text
119
+ * cannot fall behind what the parser accepts.
120
+ */
121
+ export declare const FLAG_HELP: Readonly<Record<FlagName, FlagHelp>>;
122
+ /**
123
+ * `--budget` has no built-in default, for the same reason `smelt()` has none: a budget
124
+ * smelt invented would be smelt deciding how much of the caller's context to throw
125
+ * away, silently, at a number nobody chose. A *missing* flag is not an error here,
126
+ * though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose
127
+ * explicitly, and the verb's `resolve` errors only when neither exists. A malformed
128
+ * value is always an error.
129
+ *
130
+ * It lives with the flag rather than with a verb because two verbs own `--budget`, and
131
+ * the two of them agreeing on what "4kb" means is not something to leave to chance.
132
+ *
133
+ * The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a
134
+ * leading `-` are answered by a digits-only test before anything numeric happens. The
135
+ * *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also
136
+ * where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`
137
+ * here and `"budgetBytes"` there.
138
+ */
139
+ export declare function parseBudget(raw: string | undefined): number | undefined;
140
+ export {};
141
+ //# sourceMappingURL=flags.d.ts.map
@@ -0,0 +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,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,CA8E1D,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 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 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.',\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"]}
@@ -0,0 +1,208 @@
1
+ import { SUPPORTED_LANGUAGES } from '../../detect.js';
2
+ import { CliUsageError } from '../../errors.js';
3
+ import { HARNESS_IDS } from '../../harness/registry.js';
4
+ import { budgetFault, budgetMalformed } from '../../ops/inputs.js';
5
+ import { STRATEGIES, DEFAULT_STRATEGY } from '../../plan/planners.js';
6
+ import { STRUCTURAL_LANGUAGES } from '../../plan/structural.js';
7
+ import { DEFAULT_REPO_IGNORE } from '../../repomap/map.js';
8
+ import { CLI_NAME } from '../shell.js';
9
+ /**
10
+ * Every flag the CLI accepts, once.
11
+ *
12
+ * The table is the companion the {@link Subcommand} registry needs: a verb declares
13
+ * the flags it owns as `FlagName[]`, so the type of that list — and therefore whether
14
+ * a verb can claim a flag that does not exist — comes from here. It carries three
15
+ * things that used to be written in three places:
16
+ *
17
+ * 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a
18
+ * literal inside `parseSmeltArgs`.
19
+ * 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is
20
+ * spelled once and every verb's ownership list typechecks against it.
21
+ * 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`
22
+ * prefix, which is *not* stored here: it is generated from the registry's
23
+ * ownership (see `renderOptions` in `cli/usage.ts`), because "which verb owns
24
+ * this flag" is a fact the registry already holds and prose that restates it is
25
+ * prose that can go stale.
26
+ *
27
+ * Key order is meaningful: it is the order the OPTIONS block renders and the order a
28
+ * refusal lists flags in, so keep it stable and append new flags where they read best.
29
+ */
30
+ export const CLI_FLAGS = {
31
+ budget: { type: 'string' },
32
+ focus: { type: 'string', multiple: true },
33
+ language: { type: 'string' },
34
+ strategy: { type: 'string' },
35
+ ignore: { type: 'string', multiple: true },
36
+ cache: { type: 'string' },
37
+ harness: { type: 'string' },
38
+ json: { type: 'boolean' },
39
+ reconstruct: { type: 'boolean' },
40
+ help: { type: 'boolean', short: 'h' },
41
+ version: { type: 'boolean' },
42
+ };
43
+ /**
44
+ * The flags answered *before* any verb, so no verb owns them and no verb may refuse
45
+ * them: `smelt map --help` prints the help, exactly as it always has. Every other flag
46
+ * belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.
47
+ */
48
+ export const GLOBAL_FLAGS = ['help', 'version'];
49
+ /** Every ownable flag, in table order: the list a refusal checks a verb against. */
50
+ export const VERB_FLAGS = Object.keys(CLI_FLAGS).filter((name) => !GLOBAL_FLAGS.includes(name));
51
+ /**
52
+ * `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.
53
+ *
54
+ * Every refusal that names more than one flag spells the list this way: the
55
+ * ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`
56
+ * refusal in `./smelt.ts`. It lives with the table because the *order* is the
57
+ * table's — a refusal that listed flags in the order the user happened to type them
58
+ * would read differently every time, and two refusals disagreeing about how to spell
59
+ * the same pair is the kind of drift this file exists to remove.
60
+ */
61
+ export function flagList(flags) {
62
+ const spelled = VERB_FLAGS.filter((flag) => flags.includes(flag)).map((flag) => `--${flag}`);
63
+ if (spelled.length <= 1)
64
+ return spelled.join('');
65
+ return `${spelled.slice(0, -1).join(', ')} and ${spelled.at(-1) ?? ''}`;
66
+ }
67
+ /**
68
+ * A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the
69
+ * hand-typed version wrapped. `--strategy` and `--language` render their registries on
70
+ * one line because they fit; the harness ids do not, and a list long enough to wrap is
71
+ * exactly the list nobody keeps in sync by hand.
72
+ */
73
+ export function optionList(items, width) {
74
+ const lines = [];
75
+ let line = '';
76
+ items.forEach((item, index) => {
77
+ const word = index === items.length - 1 ? `${item}.` : `${item},`;
78
+ const candidate = line === '' ? word : `${line} ${word}`;
79
+ if (line !== '' && candidate.length > width) {
80
+ lines.push(line);
81
+ line = word;
82
+ }
83
+ else {
84
+ line = candidate;
85
+ }
86
+ });
87
+ lines.push(line);
88
+ return lines;
89
+ }
90
+ /** The width an OPTIONS description wraps within, once its 23-column indent is removed. */
91
+ const OPTION_BODY_WIDTH = 65;
92
+ /**
93
+ * The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag
94
+ * added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text
95
+ * cannot fall behind what the parser accepts.
96
+ */
97
+ export const FLAG_HELP = {
98
+ budget: {
99
+ label: '--budget <bytes>',
100
+ body: () => [
101
+ 'Required, unless smelt.config.json sets defaultBudgetBytes.',
102
+ 'Soft ceiling for the output, in UTF-8 bytes (for map: a hard',
103
+ 'ceiling, met by construction). No built-in default: a budget',
104
+ `${CLI_NAME} invented would decide for you.`,
105
+ ],
106
+ },
107
+ focus: {
108
+ label: '--focus <term>',
109
+ body: () => [
110
+ 'What you were looking for. Repeatable. Matching regions and',
111
+ 'their context survive; the runs between them collapse. For',
112
+ 'map: symbols matching a term (by name or path) are promoted',
113
+ 'to the front of the fill order, ranks unchanged.',
114
+ ],
115
+ },
116
+ language: {
117
+ label: '--language <id>',
118
+ body: () => [`Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.`],
119
+ },
120
+ strategy: {
121
+ label: '--strategy <id>',
122
+ body: () => [
123
+ `${STRATEGIES.join(', ')}. Defaults to ${DEFAULT_STRATEGY}, unless`,
124
+ 'smelt.config.json says otherwise. structural parses',
125
+ `${STRUCTURAL_LANGUAGES.join(', ')};`,
126
+ 'any other language is refused, never approximated. auto picks',
127
+ 'structural for those languages and lexical for the rest, and',
128
+ 'the report names whichever one actually ran.',
129
+ ],
130
+ },
131
+ ignore: {
132
+ label: '--ignore <entry>',
133
+ body: () => [
134
+ 'Repeatable. Replaces the default ignore list',
135
+ // Read off DEFAULT_REPO_IGNORE, never re-typed: help text that lists a default
136
+ // by hand is help text that will one day describe a different default.
137
+ `(${DEFAULT_REPO_IGNORE.join(', ')}): a bare name matches any path segment,`,
138
+ 'an entry containing / is a root-relative prefix.',
139
+ ],
140
+ },
141
+ cache: {
142
+ label: '--cache <dir>',
143
+ body: () => [
144
+ 'Directory for the tags cache, keyed by content',
145
+ 'hash. Only when given does the map write to disk at all.',
146
+ ],
147
+ },
148
+ harness: {
149
+ label: '--harness <id>',
150
+ body: () => [
151
+ 'Skip harness detection and target one id:',
152
+ ...optionList(HARNESS_IDS, OPTION_BODY_WIDTH),
153
+ ],
154
+ },
155
+ json: {
156
+ label: '--json',
157
+ body: () => [
158
+ 'Print a JSON envelope on stdout instead of the text:',
159
+ '{ format, result, elided } for a smelt run — `result` is',
160
+ 'the SmeltResult verbatim, `elided` carries the bytes, so',
161
+ 'the envelope can be reconstructed; feed it back with',
162
+ '--reconstruct. For map: { format, map }, the RepoMap',
163
+ 'structure verbatim.',
164
+ ],
165
+ },
166
+ reconstruct: {
167
+ label: '--reconstruct',
168
+ body: () => [
169
+ 'Read a --json envelope and print the original text, byte for',
170
+ 'byte. This is Law 3 you can run from a shell.',
171
+ ],
172
+ },
173
+ help: { label: '-h, --help', body: () => ['This text.'] },
174
+ version: { label: '--version', body: () => ['The package version.'] },
175
+ };
176
+ /**
177
+ * `--budget` has no built-in default, for the same reason `smelt()` has none: a budget
178
+ * smelt invented would be smelt deciding how much of the caller's context to throw
179
+ * away, silently, at a number nobody chose. A *missing* flag is not an error here,
180
+ * though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose
181
+ * explicitly, and the verb's `resolve` errors only when neither exists. A malformed
182
+ * value is always an error.
183
+ *
184
+ * It lives with the flag rather than with a verb because two verbs own `--budget`, and
185
+ * the two of them agreeing on what "4kb" means is not something to leave to chance.
186
+ *
187
+ * The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a
188
+ * leading `-` are answered by a digits-only test before anything numeric happens. The
189
+ * *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also
190
+ * where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`
191
+ * here and `"budgetBytes"` there.
192
+ */
193
+ export function parseBudget(raw) {
194
+ if (raw === undefined)
195
+ return undefined;
196
+ if (!/^\d+$/.test(raw))
197
+ throw refuseBudget('not-an-integer', raw);
198
+ const value = Number(raw);
199
+ const fault = budgetFault(value);
200
+ if (fault !== undefined)
201
+ throw refuseBudget(fault, raw);
202
+ return value;
203
+ }
204
+ /** The malformed-budget refusal, in the CLI's currency: prefixed, and exit 2. */
205
+ function refuseBudget(fault, raw) {
206
+ return new CliUsageError(`${CLI_NAME}: ${budgetMalformed(fault, '--budget', raw)}`);
207
+ }
208
+ //# sourceMappingURL=flags.js.map