@smeltjs/core 0.2.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 +3 -3
  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 -70
  7. package/dist/cli/args.d.ts.map +1 -1
  8. package/dist/cli/args.js +18 -338
  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 +40 -0
  13. package/dist/cli/config.d.ts.map +1 -1
  14. package/dist/cli/config.js +55 -1
  15. package/dist/cli/config.js.map +1 -1
  16. package/dist/cli/hooks.d.ts +33 -38
  17. package/dist/cli/hooks.d.ts.map +1 -1
  18. package/dist/cli/hooks.js +140 -485
  19. package/dist/cli/hooks.js.map +1 -1
  20. package/dist/cli/init.d.ts +30 -15
  21. package/dist/cli/init.d.ts.map +1 -1
  22. package/dist/cli/init.js +167 -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 -341
  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 +14 -14
  140. package/dist/hooks/guard-core.d.ts.map +1 -1
  141. package/dist/hooks/guard-core.js +15 -37
  142. package/dist/hooks/guard-core.js.map +1 -1
  143. package/dist/hooks/shim.d.ts +95 -4
  144. package/dist/hooks/shim.d.ts.map +1 -1
  145. package/dist/hooks/shim.js +87 -2
  146. package/dist/hooks/shim.js.map +1 -1
  147. package/dist/hooks/shims/claude-code.d.ts +5 -18
  148. package/dist/hooks/shims/claude-code.d.ts.map +1 -1
  149. package/dist/hooks/shims/claude-code.js +9 -56
  150. package/dist/hooks/shims/claude-code.js.map +1 -1
  151. package/dist/hooks/shims/cline.d.ts +5 -12
  152. package/dist/hooks/shims/cline.d.ts.map +1 -1
  153. package/dist/hooks/shims/cline.js +9 -34
  154. package/dist/hooks/shims/cline.js.map +1 -1
  155. package/dist/hooks/shims/codex.d.ts +5 -18
  156. package/dist/hooks/shims/codex.d.ts.map +1 -1
  157. package/dist/hooks/shims/codex.js +9 -51
  158. package/dist/hooks/shims/codex.js.map +1 -1
  159. package/dist/hooks/shims/cursor.d.ts +5 -14
  160. package/dist/hooks/shims/cursor.d.ts.map +1 -1
  161. package/dist/hooks/shims/cursor.js +9 -42
  162. package/dist/hooks/shims/cursor.js.map +1 -1
  163. package/dist/hooks/shims/gemini.d.ts +5 -18
  164. package/dist/hooks/shims/gemini.d.ts.map +1 -1
  165. package/dist/hooks/shims/gemini.js +9 -48
  166. package/dist/hooks/shims/gemini.js.map +1 -1
  167. package/dist/hooks/shims/grok.d.ts +5 -13
  168. package/dist/hooks/shims/grok.d.ts.map +1 -1
  169. package/dist/hooks/shims/grok.js +9 -32
  170. package/dist/hooks/shims/grok.js.map +1 -1
  171. package/dist/hooks/shims/hermes.d.ts +5 -17
  172. package/dist/hooks/shims/hermes.d.ts.map +1 -1
  173. package/dist/hooks/shims/hermes.js +9 -45
  174. package/dist/hooks/shims/hermes.js.map +1 -1
  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 +6 -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 +11 -11
  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
package/dist/cli/args.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { parseArgs } from 'node:util';
2
- import { SUPPORTED_LANGUAGES } from '../detect.js';
3
2
  import { CliUsageError } from '../errors.js';
4
- import { isStrategy, STRATEGIES } from '../plan/planners.js';
5
- import { STRUCTURAL_LANGUAGES } from '../plan/structural.js';
6
- /** The command people type. Independent of the package name. */
7
- export const CLI_NAME = 'smelt';
3
+ import { CLI_NAME } from './shell.js';
4
+ import { CLI_FLAGS } from './subcommands/flags.js';
5
+ import { refuseForeignFlags, subcommandFor } from './subcommands/registry.js';
6
+ export { CLI_NAME } from './shell.js';
7
+ export { cliUsage } from './usage.js';
8
8
  /**
9
9
  * Argument parsing on `node:util.parseArgs` — stable since Node 20, which `engines`
10
10
  * already requires.
@@ -13,6 +13,12 @@ export const CLI_NAME = 'smelt';
13
13
  * import is the reason that is free: it adds no dependency, so the argument the second
14
14
  * package existed to win — keeping the library's dependency tree small — is already won.
15
15
  *
16
+ * The shape of this function is the shape of the subcommand seam: split the argv into
17
+ * flags and positionals, answer the two global flags, **look the verb up in
18
+ * `SUBCOMMANDS`**, refuse every flag that verb does not own with one generated
19
+ * message, and let the verb do its own validation. There is no per-verb branching left
20
+ * here — a seventh verb is a seventh file, not a seventh `if`.
21
+ *
16
22
  * @throws {CliUsageError} on anything the user got wrong. Never guesses.
17
23
  */
18
24
  export function parseSmeltArgs(argv) {
@@ -22,19 +28,7 @@ export function parseSmeltArgs(argv) {
22
28
  args: [...argv],
23
29
  allowPositionals: true,
24
30
  strict: true,
25
- options: {
26
- budget: { type: 'string' },
27
- focus: { type: 'string', multiple: true },
28
- language: { type: 'string' },
29
- strategy: { type: 'string' },
30
- ignore: { type: 'string', multiple: true },
31
- cache: { type: 'string' },
32
- harness: { type: 'string' },
33
- json: { type: 'boolean' },
34
- reconstruct: { type: 'boolean' },
35
- help: { type: 'boolean', short: 'h' },
36
- version: { type: 'boolean' },
37
- },
31
+ options: CLI_FLAGS,
38
32
  });
39
33
  }
40
34
  catch (cause) {
@@ -42,328 +36,14 @@ export function parseSmeltArgs(argv) {
42
36
  `Run \`${CLI_NAME} --help\`.`);
43
37
  }
44
38
  const { values, positionals } = parsed;
39
+ // Answered before any verb, so `smelt map --help` prints the help rather than being
40
+ // refused for a flag `map` does not own. No verb may claim these two.
45
41
  if (values.help === true)
46
42
  return { mode: 'help', focus: [], json: false };
47
- if (values.version === true) {
43
+ if (values.version === true)
48
44
  return { mode: 'version', focus: [], json: false };
49
- }
50
- if (positionals[0] === 'init') {
51
- // `smelt init` is a subcommand, so a file literally named `init` needs `./init`.
52
- if (positionals.length > 1) {
53
- throw new CliUsageError(`${CLI_NAME}: init takes no further arguments.`);
54
- }
55
- const flags = Object.entries(values).filter(([, value]) => value !== undefined);
56
- if (flags.length > 0) {
57
- throw new CliUsageError(`${CLI_NAME}: init is interactive and takes no flags ` +
58
- `(got --${flags.map(([name]) => name).join(', --')}). It asks instead.`);
59
- }
60
- return { mode: 'init', focus: [], json: false };
61
- }
62
- if (positionals[0] === 'map') {
63
- // `smelt map` is a subcommand, so a file literally named `map` needs `./map`.
64
- return parseMapArgs(values, positionals);
65
- }
66
- if (positionals[0] === 'retrieve') {
67
- // A subcommand, like map and init — a file literally named `retrieve` needs `./retrieve`.
68
- return parseRetrieveArgs(values, positionals);
69
- }
70
- if (positionals[0] === 'stats') {
71
- return parseStatsArgs(values, positionals);
72
- }
73
- if (positionals[0] === 'hooks') {
74
- return parseHooksArgs(values, positionals);
75
- }
76
- // These two belong to `smelt map` alone; silently ignoring a flag the user typed
77
- // would be a setting they believed was in force.
78
- if (values.ignore !== undefined || values.cache !== undefined) {
79
- throw new CliUsageError(`${CLI_NAME}: --ignore and --cache belong to \`${CLI_NAME} map\`. A single-blob ` +
80
- `run reads one file or stdin; there is no tree to walk and nothing to cache.`);
81
- }
82
- if (values.harness !== undefined) {
83
- throw new CliUsageError(`${CLI_NAME}: --harness belongs to \`${CLI_NAME} hooks\`. A single-blob run ` +
84
- `has no harness to install into.`);
85
- }
86
- if (positionals.length > 1) {
87
- throw new CliUsageError(`${CLI_NAME}: expected at most one file, got ${String(positionals.length)} ` +
88
- `(${positionals.join(', ')}). smelt reads one blob at a time.`);
89
- }
90
- const file = positionals[0];
91
- if (values.reconstruct === true) {
92
- if (values.budget !== undefined) {
93
- throw new CliUsageError(`${CLI_NAME}: --budget makes no sense with --reconstruct. Reconstruction puts ` +
94
- `every byte back; there is nothing to fit.`);
95
- }
96
- return {
97
- mode: 'reconstruct',
98
- ...(file === undefined ? {} : { file }),
99
- focus: [],
100
- json: false,
101
- };
102
- }
103
- const budgetBytes = parseBudget(values.budget);
104
- const chosenStrategy = strategy(values.strategy);
105
- return {
106
- mode: 'smelt',
107
- ...(file === undefined ? {} : { file }),
108
- ...(budgetBytes === undefined ? {} : { budgetBytes }),
109
- focus: values.focus ?? [],
110
- ...(values.language === undefined ? {} : { language: language(values.language) }),
111
- ...(chosenStrategy === undefined ? {} : { strategy: chosenStrategy }),
112
- json: values.json === true,
113
- };
114
- }
115
- /**
116
- * The `map` subcommand's own validation: exactly one directory, the map-only flags,
117
- * and the same budget rules as everything else — a missing `--budget` is not an
118
- * error *here* (the config may carry `defaultBudgetBytes`), a malformed one always is.
119
- */
120
- function parseMapArgs(values, positionals) {
121
- if (positionals.length < 2) {
122
- throw new CliUsageError(`${CLI_NAME}: map needs the directory to read.\n` +
123
- ` ${CLI_NAME} map <dir> --budget <bytes> [--focus <term>]...`);
124
- }
125
- if (positionals.length > 2) {
126
- throw new CliUsageError(`${CLI_NAME}: map takes exactly one directory, got ` +
127
- `${String(positionals.length - 1)} (${positionals.slice(1).join(', ')}).`);
128
- }
129
- if (values.reconstruct === true) {
130
- throw new CliUsageError(`${CLI_NAME}: --reconstruct makes no sense with map. A map elides nothing, so ` +
131
- `there is nothing to put back.`);
132
- }
133
- if (values.language !== undefined || values.strategy !== undefined) {
134
- throw new CliUsageError(`${CLI_NAME}: --language and --strategy apply to single-blob runs. map reads a ` +
135
- `whole tree, detects each file's language itself, and is not a planner ` +
136
- `strategy — it returns a map, not an elision plan.`);
137
- }
138
- if (values.harness !== undefined) {
139
- throw new CliUsageError(`${CLI_NAME}: --harness belongs to \`${CLI_NAME} hooks\`. map has no harness ` +
140
- `to install into.`);
141
- }
142
- const budgetBytes = parseBudget(values.budget);
143
- return {
144
- mode: 'map',
145
- dir: positionals[1],
146
- ...(budgetBytes === undefined ? {} : { budgetBytes }),
147
- focus: values.focus ?? [],
148
- ignore: values.ignore ?? [],
149
- ...(values.cache === undefined ? {} : { cacheDir: values.cache }),
150
- json: values.json === true,
151
- };
152
- }
153
- /**
154
- * `retrieve` takes the hash and nothing else. Every flag is refused rather than
155
- * ignored — the command prints the exact original bytes on stdout and nothing more,
156
- * so a flag that changed the output would break the one contract it has, and a flag
157
- * silently dropped would be a setting the user believed was in force. In particular
158
- * there is no `--json`: the bytes ARE the output, and wrapping them would re-encode
159
- * what the command exists to hand back verbatim.
160
- */
161
- function parseRetrieveArgs(values, positionals) {
162
- const flags = Object.entries(values).filter(([, value]) => value !== undefined);
163
- if (flags.length > 0) {
164
- throw new CliUsageError(`${CLI_NAME}: retrieve takes no flags (got --${flags.map(([name]) => name).join(', --')}). ` +
165
- `It prints the exact original bytes for one hash, nothing else — even --json ` +
166
- `would wrap what must come back verbatim.`);
167
- }
168
- if (positionals.length !== 2) {
169
- throw new CliUsageError(`${CLI_NAME}: retrieve needs exactly one hash — the one a marker printed.\n` +
170
- ` ${CLI_NAME} retrieve 84998967370f38bc`);
171
- }
172
- return { mode: 'retrieve', hash: positionals[1] };
173
- }
174
- /** `stats` reads counters; `--json` is its only flag, everything else is refused. */
175
- function parseStatsArgs(values, positionals) {
176
- if (positionals.length > 1) {
177
- throw new CliUsageError(`${CLI_NAME}: stats takes no further arguments, got ` +
178
- `${positionals.slice(1).join(', ')}. It reports on the one configured store.`);
179
- }
180
- const flags = Object.entries(values).filter(([name, value]) => value !== undefined && name !== 'json');
181
- if (flags.length > 0) {
182
- throw new CliUsageError(`${CLI_NAME}: stats takes only --json ` +
183
- `(got --${flags.map(([name]) => name).join(', --')}). It reads counters; there ` +
184
- `is nothing to budget, focus or plan.`);
185
- }
186
- return { mode: 'stats', json: values['json'] === true };
187
- }
188
- /**
189
- * `hooks` takes an action and at most `--harness`. It is interactive like `init` —
190
- * the wizard asks everything else — so every other flag is refused rather than
191
- * ignored.
192
- */
193
- function parseHooksArgs(values, positionals) {
194
- const action = positionals[1];
195
- if (action !== 'install' && action !== 'remove') {
196
- throw new CliUsageError(`${CLI_NAME}: hooks needs an action — install or remove.\n` +
197
- ` ${CLI_NAME} hooks install [--harness <id>]\n` +
198
- ` ${CLI_NAME} hooks remove [--harness <id>]`);
199
- }
200
- if (positionals.length > 2) {
201
- throw new CliUsageError(`${CLI_NAME}: hooks ${action} takes no further arguments, got ` +
202
- `${positionals.slice(2).join(', ')}.`);
203
- }
204
- const flags = Object.entries(values).filter(([name, value]) => value !== undefined && name !== 'harness');
205
- if (flags.length > 0) {
206
- throw new CliUsageError(`${CLI_NAME}: hooks takes only --harness ` +
207
- `(got --${flags.map(([name]) => name).join(', --')}). The wizard asks the rest.`);
208
- }
209
- const harness = values['harness'];
210
- return {
211
- mode: 'hooks',
212
- action,
213
- ...(typeof harness === 'string' ? { harness } : {}),
214
- };
215
- }
216
- /**
217
- * `--budget` has no built-in default, for the same reason `smelt()` has none: a budget
218
- * smelt invented would be smelt deciding how much of the caller's context to throw
219
- * away, silently, at a number nobody chose. A *missing* flag is not an error here,
220
- * though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose
221
- * explicitly, and the runner errors only when neither exists. A malformed value is
222
- * always an error.
223
- */
224
- function parseBudget(raw) {
225
- if (raw === undefined)
226
- return undefined;
227
- if (!/^\d+$/.test(raw)) {
228
- throw new CliUsageError(`${CLI_NAME}: --budget must be a whole number of bytes, got "${raw}".`);
229
- }
230
- const value = Number(raw);
231
- if (value <= 0) {
232
- throw new CliUsageError(`${CLI_NAME}: --budget must be greater than zero, got "${raw}".`);
233
- }
234
- return value;
235
- }
236
- function language(raw) {
237
- const known = [...SUPPORTED_LANGUAGES, 'unknown'];
238
- if (!known.includes(raw)) {
239
- throw new CliUsageError(`${CLI_NAME}: unknown --language "${raw}". Known: ${known.join(', ')}.`);
240
- }
241
- return raw;
242
- }
243
- /** Membership in the {@link PLANNERS} registry is the whole validation. */
244
- function strategy(raw) {
245
- if (raw === undefined)
246
- return undefined;
247
- if (!isStrategy(raw)) {
248
- throw new CliUsageError(`${CLI_NAME}: unknown --strategy "${raw}". Known: ${STRATEGIES.join(', ')}.`);
249
- }
250
- return raw;
251
- }
252
- /** The help text. Also the closest thing the CLI has to documentation. */
253
- export function cliUsage() {
254
- return `${CLI_NAME} — shrink text for a model, without lying about what was removed.
255
-
256
- USAGE
257
- ${CLI_NAME} <file> --budget <bytes> [--focus <term>]...
258
- ${CLI_NAME} --budget <bytes> [--focus <term>]... < input
259
- ${CLI_NAME} map <dir> --budget <bytes> [--focus <term>]... [--ignore <entry>]... [--cache <dir>]
260
- ${CLI_NAME} retrieve <hash>
261
- ${CLI_NAME} stats [--json]
262
- ${CLI_NAME} hooks install [--harness <id>]
263
- ${CLI_NAME} hooks remove [--harness <id>]
264
- ${CLI_NAME} --reconstruct <result.json>
265
- ${CLI_NAME} --reconstruct < result.json
266
- ${CLI_NAME} init
267
-
268
- Smelted text goes to stdout and the report goes to stderr, so the two can be piped
269
- apart: ${CLI_NAME} big.log --budget 4000 > small.log
270
-
271
- CONFIG
272
- ${CLI_NAME} init walks you through writing a smelt.config.json (and optional typed
273
- stubs), one question at a time; nothing is written until a final confirm. Runs read
274
- the nearest smelt.config.json, walking up from the working directory, for DEFAULTS
275
- only — budget, strategy, store. An explicit flag always wins, and a malformed config
276
- is a usage error, never silently ignored.
277
-
278
- MAP
279
- ${CLI_NAME} map <dir> renders a ranked symbol map of a whole repository — modelled
280
- on Aider's repo-map (aider.chat/docs/repomap.html, design by Paul Gauthier) — to
281
- stdout, with a short report on stderr. Local files only: symlinks are never
282
- followed, binary files are skipped, and the map writes nothing to disk unless
283
- --cache names a directory. Every included symbol carries a receipt: its
284
- definition site and the measured reference counts that ranked it. Unlike a
285
- smelt run, map never exits
286
- 1: a plan can come back over budget because ${CLI_NAME} refuses to cut regions you
287
- asked to keep, but the map fits itself to the budget by construction — symbols
288
- are appended in rank order until the next line would not fit.
289
-
290
- RETRIEVE & STATS
291
- Every marker carries the hash of the bytes it replaced — <<smelt/v1: … —
292
- retrieve("hash")>> — and the marker's retrieve("hash") is this command:
293
- ${CLI_NAME} retrieve <hash> prints the exact original bytes on stdout, byte for
294
- byte, nothing else. That closes the loop from pure shell: an agent that got a
295
- marker asks for the bytes back with a command instead of a tool call, and the
296
- retrieval is counted — asking for material back is exactly what the expansion
297
- rate measures. An unknown hash and damaged bytes are distinct refusals (exit 3):
298
- "never elided" and "the store was corrupted" call for different responses.
299
-
300
- ${CLI_NAME} stats prints the same store's counters, one \`name value\` per line —
301
- elisionsStored, bytesStored, retrieveCalls, uniqueRetrieved, expansionRate,
302
- allElisionsRetrieved — and reading them is NOT counted as a retrieval. --json
303
- emits the RetrieveStats verbatim in its own versioned envelope.
304
-
305
- Both need somewhere for elisions to outlive the run that made them: a
306
- smelt.config.json with a directory store (\`${CLI_NAME} init\` writes one). With a
307
- memory store — or no config — every run's store dies with its process, so there
308
- is nothing to retrieve across runs, and that is a usage error rather than a
309
- quiet empty answer.
310
-
311
- HOOKS
312
- ${CLI_NAME} hooks install wires the smelt guard into agent-harness hooks: a
313
- PreToolUse size-guard that refuses oversized raw reads with the exact ${CLI_NAME}
314
- replacement command (default on), \`${CLI_NAME} stats\` at session end (default
315
- on), and an opening \`${CLI_NAME} map\` at session start (opt-in) — plus an
316
- instruction-file snippet that teaches \`${CLI_NAME} retrieve\` after a deny.
317
- Harnesses are tiered honestly: verified (Claude Code, Codex), experimental
318
- (Gemini, Grok, Hermes, Cursor, opencode, Cline — schemas from the capability
319
- matrix, not yet smoke-tested), advisory (KiloCode, Aider — instructions only,
320
- nothing enforced). Same discipline as init: every file listed before a final
321
- confirm, no existing file overwritten without a per-file yes, re-runs edit
322
- toggles. ${CLI_NAME} hooks remove takes it back out. Guard settings live in
323
- smelt.config.json ("hooks": {"thresholdBytes", "enforcement": "deny"|"rewrite"});
324
- deny is the default — rewrite substitutes commands in-flight only where a
325
- harness supports it, and never silently.
326
-
327
- OPTIONS
328
- --budget <bytes> Required, unless smelt.config.json sets defaultBudgetBytes.
329
- Soft ceiling for the output, in UTF-8 bytes (for map: a hard
330
- ceiling, met by construction). No built-in default: a budget
331
- ${CLI_NAME} invented would decide for you.
332
- --focus <term> What you were looking for. Repeatable. Matching regions and
333
- their context survive; the runs between them collapse. For
334
- map: symbols matching a term (by name or path) are promoted
335
- to the front of the fill order, ranks unchanged.
336
- --language <id> Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.
337
- --strategy <id> ${STRATEGIES.join(' or ')}. Defaults to lexical, unless
338
- smelt.config.json says otherwise. structural parses
339
- ${STRUCTURAL_LANGUAGES.join(', ')};
340
- any other language is refused, never approximated.
341
- --ignore <entry> map only. Repeatable. Replaces the default ignore list
342
- (.git, node_modules): a bare name matches any path segment,
343
- an entry containing / is a root-relative prefix.
344
- --cache <dir> map only. Directory for the tags cache, keyed by content
345
- hash. Only when given does the map write to disk at all.
346
- --harness <id> hooks only. Skip harness detection and target one id:
347
- claude-code, codex, gemini, grok, hermes, cursor, opencode,
348
- cline, kilocode, aider.
349
- --json Print a JSON envelope on stdout instead of the text:
350
- { format, result, elided } for a smelt run — \`result\` is
351
- the SmeltResult verbatim, \`elided\` carries the bytes, so
352
- the envelope can be reconstructed; feed it back with
353
- --reconstruct. For map: { format, map }, the RepoMap
354
- structure verbatim.
355
- --reconstruct Read a --json envelope and print the original text, byte for
356
- byte. This is Law 3 you can run from a shell.
357
- -h, --help This text.
358
- --version The package version.
359
-
360
- EXIT CODES
361
- 0 under budget (map is always under budget by construction)
362
- 1 over budget — the plan did not fit, and the report says so. Never silent.
363
- map never exits 1; see MAP above.
364
- 2 usage error
365
- 3 smelt refused (a SmeltError: an unbuilt planner, an unknown hash, a corrupt store)
366
- 4 unexpected internal error
367
- `;
45
+ const command = subcommandFor(positionals);
46
+ refuseForeignFlags(command, values);
47
+ return command.parse(values, positionals);
368
48
  }
369
49
  //# sourceMappingURL=args.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE9E,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAWtC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,cAAc,CAAC,IAAuB;IACpD,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,SAAS,CAAC;YACjB,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;YACf,gBAAgB,EAAE,IAAI;YACtB,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE,SAAS;SACnB,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,KAAK,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI;YACxE,SAAS,QAAQ,YAAY,CAChC,CAAC;IACJ,CAAC;IAED,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC;IAEvC,oFAAoF;IACpF,sEAAsE;IACtE,IAAI,MAAM,CAAC,IAAI,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAC1E,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAEhF,MAAM,OAAO,GAAG,aAAa,CAAC,WAAW,CAAC,CAAC;IAC3C,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACpC,OAAO,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAC5C,CAAC","sourcesContent":["import { parseArgs } from 'node:util';\n\nimport { CliUsageError } from '../errors.ts';\n\nimport { CLI_NAME } from './shell.ts';\nimport { CLI_FLAGS } from './subcommands/flags.ts';\nimport { refuseForeignFlags, subcommandFor } from './subcommands/registry.ts';\n\nexport { CLI_NAME } from './shell.ts';\nexport { cliUsage } from './usage.ts';\nexport type { CliInvocation } from './subcommands/registry.ts';\nexport type { HooksInvocation } from './subcommands/hooks.ts';\nexport type { InitInvocation } from './subcommands/init.ts';\nexport type { MapInvocation } from './subcommands/map.ts';\nexport type { RetrieveInvocation } from './subcommands/retrieve.ts';\nexport type { SmeltInvocation } from './subcommands/smelt.ts';\nexport type { StatsInvocation } from './subcommands/stats.ts';\n\nimport type { CliInvocation } from './subcommands/registry.ts';\n\n/**\n * Argument parsing on `node:util.parseArgs` — stable since Node 20, which `engines`\n * already requires.\n *\n * The CLI ships as a `bin` on the library rather than as a second package, and this\n * import is the reason that is free: it adds no dependency, so the argument the second\n * package existed to win — keeping the library's dependency tree small — is already won.\n *\n * The shape of this function is the shape of the subcommand seam: split the argv into\n * flags and positionals, answer the two global flags, **look the verb up in\n * `SUBCOMMANDS`**, refuse every flag that verb does not own with one generated\n * message, and let the verb do its own validation. There is no per-verb branching left\n * here — a seventh verb is a seventh file, not a seventh `if`.\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\nexport function parseSmeltArgs(argv: readonly string[]): CliInvocation {\n let parsed;\n try {\n parsed = parseArgs({\n args: [...argv],\n allowPositionals: true,\n strict: true,\n options: CLI_FLAGS,\n });\n } catch (cause) {\n throw new CliUsageError(\n `${CLI_NAME}: ${cause instanceof Error ? cause.message : String(cause)}\\n` +\n `Run \\`${CLI_NAME} --help\\`.`,\n );\n }\n\n const { values, positionals } = parsed;\n\n // Answered before any verb, so `smelt map --help` prints the help rather than being\n // refused for a flag `map` does not own. No verb may claim these two.\n if (values.help === true) return { mode: 'help', focus: [], json: false };\n if (values.version === true) return { mode: 'version', focus: [], json: false };\n\n const command = subcommandFor(positionals);\n refuseForeignFlags(command, values);\n return command.parse(values, positionals);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\nimport { readFileSync, readSync } from 'node:fs';\nimport { isatty } from 'node:tty';\nimport process from 'node:process';\n\nimport { EXIT, runCli } from './run.ts';\n\n/**\n * The `smelt` binary: the thinnest possible shell around {@link runCli}.\n *\n * Everything decidable lives in `run.ts`, which returns an exit code instead of\n * calling `exit`, so the whole CLI is testable in-process. This file owns only the\n * things that cannot be tested without a real process: the shebang, stdin on fd 0, and\n * the exit code itself. `test/cli-bin.test.ts` spawns the *built* binary for exactly\n * those things — an in-process suite cannot see them.\n *\n * Two deliberate subtleties around fd 0:\n *\n * - **fd 0 may be non-blocking, and must still be read to EOF.** Two separate ways\n * that happens in the wild: merely accessing `process.stdin` (even `.isTTY`) makes\n * Node flip fd 0 into non-blocking mode, and a parent that spawned this process\n * with piped stdio may hand over a socketpair that was *born* non-blocking. Either\n * way a one-shot `readFileSync(0)` throws `EAGAIN` whenever the producer is slower\n * than Node's startup — `(sleep 1; echo hi) | smelt --budget 100` used to die with\n * an internal error. So: the TTY check is `tty.isatty(0)` (a plain syscall, no\n * stream initialization), `process.stdin` is only handed over lazily to the one\n * mode that needs a stream (`init`), and the read itself retries `EAGAIN` with a\n * synchronous back-off until EOF.\n * - **Bytes that are not UTF-8 are refused, never mangled.** Decoding invalid bytes\n * would silently replace them with U+FFFD, and the result would still smelt,\n * round-trip, and verify — of the wrong bytes. That violates the reversibility\n * story at its root, so the binary refuses (exit code {@link EXIT.refused}) and\n * names the first offending byte offset instead.\n */\n\n/** Reading fd 0 synchronously is the whole of \"works in a pipe\", with no stream plumbing. */\nfunction readStdin(): string {\n if (isatty(0)) {\n process.stderr.write(\n 'smelt: no input. Pass a file, or pipe text in:\\n' +\n ' smelt src/server.ts --budget 4000\\n' +\n ' smelt --budget 4000 --focus TypeError < build.log\\n',\n );\n process.exit(EXIT.usage);\n }\n const bytes = readAllOfFd0();\n const offset = firstInvalidUtf8Offset(bytes);\n if (offset !== -1) {\n process.stderr.write(\n `smelt: stdin is not valid UTF-8 — first invalid byte at offset ${String(offset)} ` +\n `(0x${bytes[offset]!.toString(16).padStart(2, '0')}). Decoding it would silently ` +\n `replace bytes with U+FFFD, and the \"original\" smelt could then reconstruct would ` +\n `not be your original. Refusing instead.\\n`,\n );\n process.exit(EXIT.refused);\n }\n return bytes.toString('utf8');\n}\n\n/** `Atomics.wait` is the only dependency-free synchronous sleep Node offers. */\nconst sleeper = new Int32Array(new SharedArrayBuffer(4));\n\n/**\n * Every byte of fd 0, to EOF, whether the descriptor is blocking (a shell pipe, a\n * redirected file) or non-blocking (a socketpair from a spawning parent). `EAGAIN`\n * means \"no bytes yet, not EOF\" — wait briefly and try again; only a zero-byte read\n * (or Windows' `EOF` on a closed pipe) ends the loop.\n */\nfunction readAllOfFd0(): Buffer {\n const chunks: Buffer[] = [];\n const chunk = Buffer.alloc(1 << 16);\n for (;;) {\n let bytesRead: number;\n try {\n bytesRead = readSync(0, chunk, 0, chunk.length, null);\n } catch (error) {\n const code = (error as { code?: string }).code;\n if (code === 'EAGAIN') {\n Atomics.wait(sleeper, 0, 0, 10); // producer not ready — 10ms, then retry\n continue;\n }\n if (code === 'EOF') break; // Windows raises EOF instead of returning 0\n throw error;\n }\n if (bytesRead === 0) break;\n chunks.push(Buffer.from(chunk.subarray(0, bytesRead)));\n }\n return Buffer.concat(chunks);\n}\n\n/**\n * The byte offset where `bytes` stops being well-formed UTF-8, or `-1` when it is\n * well-formed throughout. Implements the WHATWG/Unicode table exactly: continuation\n * ranges tightened for E0/ED/F0/F4 so overlongs, surrogates and > U+10FFFF are\n * invalid — the same sequences `Buffer.prototype.toString` would fold into U+FFFD.\n */\nfunction firstInvalidUtf8Offset(bytes: Uint8Array): number {\n let i = 0;\n while (i < bytes.length) {\n const lead = bytes[i]!;\n if (lead <= 0x7f) {\n i += 1;\n continue;\n }\n let length: number;\n let firstLow = 0x80;\n let firstHigh = 0xbf;\n if (lead >= 0xc2 && lead <= 0xdf) {\n length = 2;\n } else if (lead >= 0xe0 && lead <= 0xef) {\n length = 3;\n if (lead === 0xe0) firstLow = 0xa0; // overlong\n if (lead === 0xed) firstHigh = 0x9f; // UTF-16 surrogates\n } else if (lead >= 0xf0 && lead <= 0xf4) {\n length = 4;\n if (lead === 0xf0) firstLow = 0x90; // overlong\n if (lead === 0xf4) firstHigh = 0x8f; // above U+10FFFF\n } else {\n return i; // a bare continuation byte, or C0/C1/F5–FF — invalid as a lead\n }\n if (i + length > bytes.length) return i; // truncated sequence\n for (let k = 1; k < length; k += 1) {\n const continuation = bytes[i + k]!;\n const low = k === 1 ? firstLow : 0x80;\n const high = k === 1 ? firstHigh : 0xbf;\n if (continuation < low || continuation > high) return i;\n }\n i += length;\n }\n return -1;\n}\n\n/** `--version` should agree with the manifest, not with a second copy of the number. */\nfunction packageVersion(): string {\n const manifest = readFileSync(new URL('../../package.json', import.meta.url), 'utf8');\n const parsed = JSON.parse(manifest) as { version?: string };\n return parsed.version ?? '0.0.0';\n}\n\ntry {\n process.exitCode = await runCli(process.argv.slice(2), {\n stdout: (text) => void process.stdout.write(text),\n stderr: (text) => void process.stderr.write(text),\n stdin: readStdin,\n version: packageVersion(),\n cwd: process.cwd(),\n // `smelt init` reads answers line by line, so it gets the stream, not readStdin's\n // one-shot slurp of fd 0. A *getter*, because touching `process.stdin` at all\n // flips fd 0 into non-blocking mode and breaks readStdin's readFileSync(0) for\n // every other mode — only the `init` branch of runCli ever reads this property.\n get initInput() {\n return process.stdin;\n },\n });\n} catch (error) {\n process.stderr.write(\n `smelt: unexpected internal error — this is a bug, please report it.\\n` +\n `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n );\n process.exitCode = EXIT.unexpected;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"bin.js","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { readFileSync, readSync } from 'node:fs';\nimport { isatty } from 'node:tty';\nimport process from 'node:process';\n\nimport { EXIT, runCli } from './run.ts';\n\n/**\n * The `smelt` binary: the thinnest possible shell around {@link runCli}.\n *\n * Everything decidable lives in `run.ts`, which returns an exit code instead of\n * calling `exit`, so the whole CLI is testable in-process. This file owns only the\n * things that cannot be tested without a real process: the shebang, stdin on fd 0, and\n * the exit code itself. `test/cli-bin.test.ts` spawns the *built* binary for exactly\n * those things — an in-process suite cannot see them.\n *\n * Two deliberate subtleties around fd 0:\n *\n * - **fd 0 may be non-blocking, and must still be read to EOF.** Two separate ways\n * that happens in the wild: merely accessing `process.stdin` (even `.isTTY`) makes\n * Node flip fd 0 into non-blocking mode, and a parent that spawned this process\n * with piped stdio may hand over a socketpair that was *born* non-blocking. Either\n * way a one-shot `readFileSync(0)` throws `EAGAIN` whenever the producer is slower\n * than Node's startup — `(sleep 1; echo hi) | smelt --budget 100` used to die with\n * an internal error. So: the TTY check is `tty.isatty(0)` (a plain syscall, no\n * stream initialization), `process.stdin` is only handed over lazily to the one\n * mode that needs a stream (`init`), and the read itself retries `EAGAIN` with a\n * synchronous back-off until EOF.\n * - **Bytes that are not UTF-8 are refused, never mangled.** Decoding invalid bytes\n * would silently replace them with U+FFFD, and the result would still smelt,\n * round-trip, and verify — of the wrong bytes. That violates the reversibility\n * story at its root, so the binary refuses (exit code {@link EXIT.refused}) and\n * names the first offending byte offset instead.\n */\n\n/** Reading fd 0 synchronously is the whole of \"works in a pipe\", with no stream plumbing. */\nfunction readStdin(): string {\n if (isatty(0)) {\n process.stderr.write(\n 'smelt: no input. Pass a file, or pipe text in:\\n' +\n ' smelt src/server.ts --budget 4000\\n' +\n ' smelt --budget 4000 --focus TypeError < build.log\\n',\n );\n process.exit(EXIT.usage);\n }\n const bytes = readAllOfFd0();\n const offset = firstInvalidUtf8Offset(bytes);\n if (offset !== -1) {\n process.stderr.write(\n `smelt: stdin is not valid UTF-8 — first invalid byte at offset ${String(offset)} ` +\n `(0x${bytes[offset]!.toString(16).padStart(2, '0')}). Decoding it would silently ` +\n `replace bytes with U+FFFD, and the \"original\" smelt could then reconstruct would ` +\n `not be your original. Refusing instead.\\n`,\n );\n process.exit(EXIT.refused);\n }\n return bytes.toString('utf8');\n}\n\n/** `Atomics.wait` is the only dependency-free synchronous sleep Node offers. */\nconst sleeper = new Int32Array(new SharedArrayBuffer(4));\n\n/**\n * Every byte of fd 0, to EOF, whether the descriptor is blocking (a shell pipe, a\n * redirected file) or non-blocking (a socketpair from a spawning parent). `EAGAIN`\n * means \"no bytes yet, not EOF\" — wait briefly and try again; only a zero-byte read\n * (or Windows' `EOF` on a closed pipe) ends the loop.\n */\nfunction readAllOfFd0(): Buffer {\n const chunks: Buffer[] = [];\n const chunk = Buffer.alloc(1 << 16);\n for (;;) {\n let bytesRead: number;\n try {\n bytesRead = readSync(0, chunk, 0, chunk.length, null);\n } catch (error) {\n const code = (error as { code?: string }).code;\n if (code === 'EAGAIN') {\n Atomics.wait(sleeper, 0, 0, 10); // producer not ready — 10ms, then retry\n continue;\n }\n if (code === 'EOF') break; // Windows raises EOF instead of returning 0\n throw error;\n }\n if (bytesRead === 0) break;\n chunks.push(Buffer.from(chunk.subarray(0, bytesRead)));\n }\n return Buffer.concat(chunks);\n}\n\n/**\n * The byte offset where `bytes` stops being well-formed UTF-8, or `-1` when it is\n * well-formed throughout. Implements the WHATWG/Unicode table exactly: continuation\n * ranges tightened for E0/ED/F0/F4 so overlongs, surrogates and > U+10FFFF are\n * invalid — the same sequences `Buffer.prototype.toString` would fold into U+FFFD.\n */\nfunction firstInvalidUtf8Offset(bytes: Uint8Array): number {\n let i = 0;\n while (i < bytes.length) {\n const lead = bytes[i]!;\n if (lead <= 0x7f) {\n i += 1;\n continue;\n }\n let length: number;\n let firstLow = 0x80;\n let firstHigh = 0xbf;\n if (lead >= 0xc2 && lead <= 0xdf) {\n length = 2;\n } else if (lead >= 0xe0 && lead <= 0xef) {\n length = 3;\n if (lead === 0xe0) firstLow = 0xa0; // overlong\n if (lead === 0xed) firstHigh = 0x9f; // UTF-16 surrogates\n } else if (lead >= 0xf0 && lead <= 0xf4) {\n length = 4;\n if (lead === 0xf0) firstLow = 0x90; // overlong\n if (lead === 0xf4) firstHigh = 0x8f; // above U+10FFFF\n } else {\n return i; // a bare continuation byte, or C0/C1/F5–FF — invalid as a lead\n }\n if (i + length > bytes.length) return i; // truncated sequence\n for (let k = 1; k < length; k += 1) {\n const continuation = bytes[i + k]!;\n const low = k === 1 ? firstLow : 0x80;\n const high = k === 1 ? firstHigh : 0xbf;\n if (continuation < low || continuation > high) return i;\n }\n i += length;\n }\n return -1;\n}\n\n/** `--version` should agree with the manifest, not with a second copy of the number. */\nfunction packageVersion(): string {\n const manifest = readFileSync(new URL('../../package.json', import.meta.url), 'utf8');\n const parsed = JSON.parse(manifest) as { version?: string };\n return parsed.version ?? '0.0.0';\n}\n\ntry {\n process.exitCode = await runCli(process.argv.slice(2), {\n stdout: (text) => void process.stdout.write(text),\n stderr: (text) => void process.stderr.write(text),\n stdin: readStdin,\n version: packageVersion(),\n cwd: process.cwd(),\n // `smelt init` reads answers line by line, so it gets the stream, not readStdin's\n // one-shot slurp of fd 0. A *getter*, because touching `process.stdin` at all\n // flips fd 0 into non-blocking mode and breaks readStdin's readFileSync(0) for\n // every other mode — only the `init` branch of runCli ever reads this property.\n get initInput() {\n return process.stdin;\n },\n });\n} catch (error) {\n process.stderr.write(\n `smelt: unexpected internal error — this is a bug, please report it.\\n` +\n `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n );\n process.exitCode = EXIT.unexpected;\n}\n"]}
@@ -90,6 +90,46 @@ export declare function loadNearestConfig(cwd: string): LoadedConfig | undefined
90
90
  * @throws {CliUsageError} naming the file and the exact problem.
91
91
  */
92
92
  export declare function parseConfig(text: string, path: string): SmeltConfig;
93
+ /**
94
+ * Serialize a config back to the bytes of a `smelt.config.json` — {@link parseConfig}'s
95
+ * inverse, and the **only** writer of this file.
96
+ *
97
+ * The module that owns the schema owns both directions. It used to own only the read:
98
+ * two modules hand-built a config object and stringified it — the `init` wizard and
99
+ * `hooks install` — and they had already drifted, one always emitting `strategy` and
100
+ * the other only when it was carried. A sixth key would have been written by whichever
101
+ * module its author was editing and silently dropped by the other, which is the
102
+ * "setting the user believed was in force" failure this file refuses everywhere else.
103
+ *
104
+ * Key order is fixed here — the reader's own key order, top level and inside `store`
105
+ * and `hooks` — so a re-run diffs cleanly no matter which verb wrote the file, and two
106
+ * writers can never disagree about shape. What goes *into* the config stays each
107
+ * verb's policy: the wizard's choices, and `hooks install`'s directory-store injection.
108
+ * Absent keys are omitted rather than written as `null`, so
109
+ * `parseConfig(renderConfig(c))` returns `c` field for field —
110
+ * `test/guards/config-writer.test.ts` pins that round trip.
111
+ */
112
+ export declare function renderConfig(config: SmeltConfig): string;
93
113
  /** `store.path` is relative to the config file, so the config works from any cwd. */
94
114
  export declare function resolveStorePath(loaded: LoadedConfig, path: string): string;
115
+ /**
116
+ * The store decision a config carries, with `path` already resolved against the config
117
+ * file's directory — one config serves every subdirectory it covers without scattering
118
+ * store roots. `'memory'` is the built-in default: a fresh in-memory store per run.
119
+ */
120
+ export type ConfiguredStore = {
121
+ readonly kind: 'memory';
122
+ } | {
123
+ readonly kind: 'directory';
124
+ readonly path: string;
125
+ };
126
+ /**
127
+ * The store leg of every merge, read once here rather than in each verb that needs it.
128
+ *
129
+ * There is no store flag, so this leg is config-or-built-in only — which is exactly
130
+ * why it belongs to the config module and not to a verb: `smelt` builds a store from
131
+ * it, and `retrieve`/`stats` refuse when it is not a directory (see `resolveStoreRun`
132
+ * in `subcommands/retrieve.ts`). Three verbs, one reading of the same key.
133
+ */
134
+ export declare function configuredStore(config: LoadedConfig | undefined): ConfiguredStore;
95
135
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/cli/config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD;;;;;;;;;;;;;;;GAeG;AAEH,mEAAmE;AACnE,eAAO,MAAM,gBAAgB,sBAAsB,CAAC;AAEpD,sDAAsD;AACtD,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,wDAAwD;AACxD,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IACE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEN;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,eAAe,CAAC;CACxC;AAED,2FAA2F;AAC3F,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,OAAO,cAAc,CAAC;IAC5C,sFAAsF;IACtF,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,oEAAoE;IACpE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;CACnC;AAED,sFAAsF;AACtF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAS9D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAIvE;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CA+DnE;AAsED,qFAAqF;AACrF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3E"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/cli/config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD;;;;;;;;;;;;;;;GAeG;AAEH,mEAAmE;AACnE,eAAO,MAAM,gBAAgB,sBAAsB,CAAC;AAEpD,sDAAsD;AACtD,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,wDAAwD;AACxD,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IACE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEN;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,eAAe,CAAC;CACxC;AAED,2FAA2F;AAC3F,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,OAAO,cAAc,CAAC;IAC5C,sFAAsF;IACtF,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,oEAAoE;IACpE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;CACnC;AAED,sFAAsF;AACtF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAS9D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAIvE;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CA+DnE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAWxD;AAiFD,qFAAqF;AACrF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtF;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS,GAAG,eAAe,CAKjF","sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\n\nimport { CliUsageError } from '../errors.ts';\nimport { ENFORCEMENT_MODES } from '../hooks/guard-core.ts';\nimport type { EnforcementMode } from '../hooks/guard-core.ts';\nimport { isStrategy, STRATEGIES } from '../plan/planners.ts';\nimport type { Strategy } from '../plan/planners.ts';\n\nimport { CLI_NAME } from './shell.ts';\n\n/**\n * `smelt.config.json` — CLI defaults, and nothing more.\n *\n * This file exists so `smelt` can be run without retyping the same flags, and it is\n * deliberately a *CLI* concern: the programmatic API never reads it. A library that\n * reads config files off the caller's disk is a library whose behaviour depends on\n * where it was invoked from, which is exactly the class of invisible input smelt\n * refuses. `createSmelter()` takes explicit arguments; the CLI translates this file\n * into those arguments, and explicit flags always win over it.\n *\n * The schema is versioned (`\"smeltConfig\": 1`) for the same reason the marker and the\n * `--json` envelope are: files outlive the binaries that wrote them, and a mismatch\n * must be identifiable rather than half-understood. Parsing is strict — an unknown\n * key, a wrong type, or an unknown version is a {@link CliUsageError}, never a shrug.\n * A config the CLI silently ignored would be a budget the user *thought* they set.\n */\n\n/** The file name looked for, from the working directory upward. */\nexport const CONFIG_FILE_NAME = 'smelt.config.json';\n\n/** The schema version this build reads and writes. */\nexport const CONFIG_VERSION = 1;\n\n/** Where the CLI's elision store lives between runs. */\nexport type SmeltConfigStore =\n | { readonly kind: 'memory' }\n | {\n readonly kind: 'directory';\n /** Resolved relative to the directory holding the config file, not the cwd. */\n readonly path: string;\n };\n\n/**\n * The `hooks` block — settings for the harness guard (`smelt hooks install`).\n *\n * Parsed strictly here, like every other key: the CLI refuses a malformed config.\n * The guard core (`src/hooks/guard-core.ts`) reads the same file with its own\n * *tolerant* reader — a guard running inside somebody's session fails open where the\n * CLI correctly refuses — and `test/hooks-guard-core.test.ts` pins the two readers to\n * the same key names and defaults so they cannot drift apart.\n */\nexport interface SmeltConfigHooks {\n /** Reads at or under this many bytes pass the guard untouched. Default 8192. */\n readonly thresholdBytes?: number;\n /**\n * `'deny'` (default): oversized raw reads are refused with a reason naming the\n * exact replacement command. `'rewrite'`: on harnesses whose hooks can modify tool\n * input, the command is substituted instead (never silently — the reason says so).\n */\n readonly enforcement?: EnforcementMode;\n}\n\n/** The parsed shape of `smelt.config.json`. Every field beyond the version is optional. */\nexport interface SmeltConfig {\n readonly smeltConfig: typeof CONFIG_VERSION;\n /** Used when a `smelt` run omits `--budget`. UTF-8 bytes, like every smelt budget. */\n readonly defaultBudgetBytes?: number;\n /** Used when a run omits `--strategy`. Validated against the {@link PLANNERS} registry. */\n readonly strategy?: Strategy;\n /** Used for every run; there is no store flag. Defaults to memory when absent. */\n readonly store?: SmeltConfigStore;\n /** Settings for the harness guard. See {@link SmeltConfigHooks}. */\n readonly hooks?: SmeltConfigHooks;\n}\n\n/** A config plus where it was found — the path matters for resolving `store.path`. */\nexport interface LoadedConfig {\n readonly path: string;\n readonly config: SmeltConfig;\n}\n\n/**\n * The nearest `smelt.config.json`, walking up from `cwd` to the filesystem root —\n * the same discovery shape as `package.json`, so a config at a repo root covers the\n * whole tree. `undefined` when there is none, which is not an error: flags alone are\n * a complete interface.\n */\nexport function findConfigFile(cwd: string): string | undefined {\n let dir = resolve(cwd);\n for (;;) {\n const candidate = join(dir, CONFIG_FILE_NAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n/**\n * Find and parse the nearest config. `undefined` when no file exists; a file that\n * exists but is malformed **throws** — see {@link parseConfig}. The distinction is the\n * point: \"no config\" is a fine state, \"a config you cannot have meant\" is not.\n *\n * @throws {CliUsageError} when a config file exists and is malformed.\n */\nexport function loadNearestConfig(cwd: string): LoadedConfig | undefined {\n const path = findConfigFile(cwd);\n if (path === undefined) return undefined;\n return { path, config: parseConfig(readFileSync(path, 'utf8'), path) };\n}\n\n/**\n * Parse and validate one config file, strictly.\n *\n * Strict means: unknown keys are refused, not skipped. A typo'd `defaultBudgetByte`\n * that parsed cleanly would leave the user believing a default was set while every\n * run ignored it — a config error whose failure mode is *silence*, which is the one\n * failure shape this project refuses everywhere else too.\n *\n * @throws {CliUsageError} naming the file and the exact problem.\n */\nexport function parseConfig(text: string, path: string): SmeltConfig {\n const bad = (why: string): CliUsageError =>\n new CliUsageError(\n `${CLI_NAME}: malformed ${CONFIG_FILE_NAME} at ${path}: ${why}\\n` +\n `Fix it or delete it — a config smelt cannot read is never silently ignored. ` +\n `\\`${CLI_NAME} init\\` can rewrite it.`,\n );\n\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (cause) {\n throw bad(`not JSON (${cause instanceof Error ? cause.message : String(cause)}).`);\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw bad('expected a JSON object at the top level.');\n }\n const fields = value as Record<string, unknown>;\n\n if (fields['smeltConfig'] !== CONFIG_VERSION) {\n throw bad(\n `\"smeltConfig\" is ${JSON.stringify(fields['smeltConfig'])}; this build reads version ` +\n `${String(CONFIG_VERSION)}. The schema is versioned so a mismatch is visible ` +\n `instead of half-understood.`,\n );\n }\n\n const known = ['smeltConfig', 'defaultBudgetBytes', 'strategy', 'store', 'hooks'];\n const unknown = Object.keys(fields).filter((key) => !known.includes(key));\n if (unknown.length > 0) {\n throw bad(\n `unknown key${unknown.length === 1 ? '' : 's'} ${unknown.map((k) => `\"${k}\"`).join(', ')}. ` +\n `Known keys: ${known.join(', ')}. Unknown keys are refused because a typo that ` +\n `parsed cleanly would be a setting you believed was set.`,\n );\n }\n\n const budget = fields['defaultBudgetBytes'];\n if (budget !== undefined && (typeof budget !== 'number' || !Number.isInteger(budget))) {\n throw bad(`\"defaultBudgetBytes\" must be a whole number of UTF-8 bytes.`);\n }\n if (typeof budget === 'number' && budget <= 0) {\n throw bad(`\"defaultBudgetBytes\" must be greater than zero, got ${String(budget)}.`);\n }\n\n const strategy = fields['strategy'];\n if (strategy !== undefined && (typeof strategy !== 'string' || !isStrategy(strategy))) {\n throw bad(\n `\"strategy\" must be ${STRATEGIES.map((s) => `\"${s}\"`).join(' or ')}, ` +\n `got ${JSON.stringify(strategy)}.`,\n );\n }\n\n const store = parseStore(fields['store'], bad);\n const hooks = parseHooks(fields['hooks'], bad);\n\n return {\n smeltConfig: CONFIG_VERSION,\n ...(budget === undefined ? {} : { defaultBudgetBytes: budget }),\n ...(strategy === undefined ? {} : { strategy }),\n ...(store === undefined ? {} : { store }),\n ...(hooks === undefined ? {} : { hooks }),\n };\n}\n\n/**\n * Serialize a config back to the bytes of a `smelt.config.json` — {@link parseConfig}'s\n * inverse, and the **only** writer of this file.\n *\n * The module that owns the schema owns both directions. It used to own only the read:\n * two modules hand-built a config object and stringified it — the `init` wizard and\n * `hooks install` — and they had already drifted, one always emitting `strategy` and\n * the other only when it was carried. A sixth key would have been written by whichever\n * module its author was editing and silently dropped by the other, which is the\n * \"setting the user believed was in force\" failure this file refuses everywhere else.\n *\n * Key order is fixed here — the reader's own key order, top level and inside `store`\n * and `hooks` — so a re-run diffs cleanly no matter which verb wrote the file, and two\n * writers can never disagree about shape. What goes *into* the config stays each\n * verb's policy: the wizard's choices, and `hooks install`'s directory-store injection.\n * Absent keys are omitted rather than written as `null`, so\n * `parseConfig(renderConfig(c))` returns `c` field for field —\n * `test/guards/config-writer.test.ts` pins that round trip.\n */\nexport function renderConfig(config: SmeltConfig): string {\n const ordered: SmeltConfig = {\n smeltConfig: CONFIG_VERSION,\n ...(config.defaultBudgetBytes === undefined\n ? {}\n : { defaultBudgetBytes: config.defaultBudgetBytes }),\n ...(config.strategy === undefined ? {} : { strategy: config.strategy }),\n ...(config.store === undefined ? {} : { store: renderStore(config.store) }),\n ...(config.hooks === undefined ? {} : { hooks: renderHooks(config.hooks) }),\n };\n return `${JSON.stringify(ordered, null, 2)}\\n`;\n}\n\nfunction renderStore(store: SmeltConfigStore): SmeltConfigStore {\n return store.kind === 'memory' ? { kind: 'memory' } : { kind: 'directory', path: store.path };\n}\n\nfunction renderHooks(hooks: SmeltConfigHooks): SmeltConfigHooks {\n return {\n ...(hooks.thresholdBytes === undefined ? {} : { thresholdBytes: hooks.thresholdBytes }),\n ...(hooks.enforcement === undefined ? {} : { enforcement: hooks.enforcement }),\n };\n}\n\nfunction parseStore(\n value: unknown,\n bad: (why: string) => CliUsageError,\n): SmeltConfigStore | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw bad(`\"store\" must be an object like {\"kind\":\"memory\"} or {\"kind\":\"directory\",\"path\":…}.`);\n }\n const fields = value as Record<string, unknown>;\n const kind = fields['kind'];\n if (kind === 'memory') {\n const extra = Object.keys(fields).filter((key) => key !== 'kind');\n if (extra.length > 0) throw bad(`\"store\" of kind \"memory\" takes no other keys.`);\n return { kind: 'memory' };\n }\n if (kind === 'directory') {\n const path = fields['path'];\n if (typeof path !== 'string' || path === '') {\n throw bad(`\"store\" of kind \"directory\" needs a non-empty \"path\".`);\n }\n const extra = Object.keys(fields).filter((key) => key !== 'kind' && key !== 'path');\n if (extra.length > 0) throw bad(`\"store\" of kind \"directory\" takes only \"kind\" and \"path\".`);\n return { kind: 'directory', path };\n }\n throw bad(`\"store\".kind must be \"memory\" or \"directory\", got ${JSON.stringify(kind)}.`);\n}\n\nfunction parseHooks(\n value: unknown,\n bad: (why: string) => CliUsageError,\n): SmeltConfigHooks | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw bad(`\"hooks\" must be an object like {\"thresholdBytes\":8192,\"enforcement\":\"deny\"}.`);\n }\n const fields = value as Record<string, unknown>;\n const extra = Object.keys(fields).filter(\n (key) => key !== 'thresholdBytes' && key !== 'enforcement',\n );\n if (extra.length > 0) {\n throw bad(\n `\"hooks\" takes only \"thresholdBytes\" and \"enforcement\", ` +\n `got ${extra.map((k) => `\"${k}\"`).join(', ')}.`,\n );\n }\n const threshold = fields['thresholdBytes'];\n if (\n threshold !== undefined &&\n (typeof threshold !== 'number' || !Number.isInteger(threshold) || threshold <= 0)\n ) {\n throw bad(`\"hooks\".thresholdBytes must be a whole number of bytes greater than zero.`);\n }\n const enforcement = fields['enforcement'];\n if (\n enforcement !== undefined &&\n !(ENFORCEMENT_MODES as readonly unknown[]).includes(enforcement)\n ) {\n throw bad(\n `\"hooks\".enforcement must be ${ENFORCEMENT_MODES.map((m) => `\"${m}\"`).join(' or ')}, ` +\n `got ${JSON.stringify(enforcement)}.`,\n );\n }\n return {\n ...(threshold === undefined ? {} : { thresholdBytes: threshold as number }),\n ...(enforcement === undefined ? {} : { enforcement: enforcement as EnforcementMode }),\n };\n}\n\n/** `store.path` is relative to the config file, so the config works from any cwd. */\nexport function resolveStorePath(loaded: LoadedConfig, path: string): string {\n return resolve(dirname(loaded.path), path);\n}\n\n/**\n * The store decision a config carries, with `path` already resolved against the config\n * file's directory — one config serves every subdirectory it covers without scattering\n * store roots. `'memory'` is the built-in default: a fresh in-memory store per run.\n */\nexport type ConfiguredStore =\n { readonly kind: 'memory' } | { readonly kind: 'directory'; readonly path: string };\n\n/**\n * The store leg of every merge, read once here rather than in each verb that needs it.\n *\n * There is no store flag, so this leg is config-or-built-in only — which is exactly\n * why it belongs to the config module and not to a verb: `smelt` builds a store from\n * it, and `retrieve`/`stats` refuse when it is not a directory (see `resolveStoreRun`\n * in `subcommands/retrieve.ts`). Three verbs, one reading of the same key.\n */\nexport function configuredStore(config: LoadedConfig | undefined): ConfiguredStore {\n if (config?.config.store === undefined || config.config.store.kind === 'memory') {\n return { kind: 'memory' };\n }\n return { kind: 'directory', path: resolveStorePath(config, config.config.store.path) };\n}\n"]}