@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 @@
1
+ {"version":3,"file":"flags.js","sourceRoot":"","sources":["../../../src/cli/subcommands/flags.ts"],"names":[],"mappings":"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","sourcesContent":["import { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESS_IDS } from '../../harness/registry.ts';\nimport { budgetFault, budgetMalformed } from '../../ops/inputs.ts';\nimport type { BudgetFault } from '../../ops/inputs.ts';\nimport { STRATEGIES, DEFAULT_STRATEGY } from '../../plan/planners.ts';\nimport { STRUCTURAL_LANGUAGES } from '../../plan/structural.ts';\nimport { DEFAULT_REPO_IGNORE } from '../../repomap/map.ts';\nimport { CLI_NAME } from '../shell.ts';\n\n/**\n * Every flag the CLI accepts, once.\n *\n * The table is the companion the {@link Subcommand} registry needs: a verb declares\n * the flags it owns as `FlagName[]`, so the type of that list — and therefore whether\n * a verb can claim a flag that does not exist — comes from here. It carries three\n * things that used to be written in three places:\n *\n * 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a\n * literal inside `parseSmeltArgs`.\n * 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is\n * spelled once and every verb's ownership list typechecks against it.\n * 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`\n * prefix, which is *not* stored here: it is generated from the registry's\n * ownership (see `renderOptions` in `cli/usage.ts`), because \"which verb owns\n * this flag\" is a fact the registry already holds and prose that restates it is\n * prose that can go stale.\n *\n * Key order is meaningful: it is the order the OPTIONS block renders and the order a\n * refusal lists flags in, so keep it stable and append new flags where they read best.\n */\nexport const CLI_FLAGS = {\n budget: { type: 'string' },\n focus: { type: 'string', multiple: true },\n language: { type: 'string' },\n strategy: { type: 'string' },\n ignore: { type: 'string', multiple: true },\n cache: { type: 'string' },\n harness: { type: 'string' },\n 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,18 @@
1
+ import type { Subcommand } from './subcommand.ts';
2
+ /**
3
+ * `smelt hooks install` / `smelt hooks remove` — the harness-hooks installer's front
4
+ * door. The installer itself is `cli/hooks.ts`; this file is only the verb.
5
+ *
6
+ * Like `init`, it is interactive — the wizard asks everything except which harness —
7
+ * so `--harness` is the one flag it owns, and the registry refuses the rest. The id
8
+ * itself is validated in `cli/hooks.ts` against the harness registry in `src/harness/`,
9
+ * which is also where the `--harness` help list comes from.
10
+ */
11
+ /** `smelt hooks <install|remove> [--harness <id>]` — parsed. */
12
+ export interface HooksInvocation {
13
+ readonly mode: 'hooks';
14
+ readonly action: 'install' | 'remove';
15
+ readonly harness?: string;
16
+ }
17
+ export declare const hooksCommand: Subcommand<HooksInvocation, HooksInvocation>;
18
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/hooks.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;GAQG;AAEH,gEAAgE;AAChE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,eAAe,EAAE,eAAe,CAoErE,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runHooks } from '../hooks.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt hooks install` / `smelt hooks remove` — the harness-hooks installer's front\n * door. The installer itself is `cli/hooks.ts`; this file is only the verb.\n *\n * Like `init`, it is interactive — the wizard asks everything except which harness —\n * so `--harness` is the one flag it owns, and the registry refuses the rest. The id\n * itself is validated in `cli/hooks.ts` against the harness registry in `src/harness/`,\n * which is also where the `--harness` help list comes from.\n */\n\n/** `smelt hooks <install|remove> [--harness <id>]` — parsed. */\nexport interface HooksInvocation {\n readonly mode: 'hooks';\n readonly action: 'install' | 'remove';\n readonly harness?: string;\n}\n\nexport const hooksCommand: Subcommand<HooksInvocation, HooksInvocation> = {\n name: 'hooks',\n flags: ['harness'],\n refusal: `hooks is interactive; the wizard asks the rest.`,\n usage: {\n synopsis: ['hooks install [--harness <id>]', 'hooks remove [--harness <id>]'],\n section: {\n heading: 'HOOKS',\n body:\n ` ${CLI_NAME} hooks install wires the smelt guard into agent-harness hooks: a\\n` +\n ` PreToolUse size-guard that refuses oversized raw reads with the exact ${CLI_NAME}\\n` +\n ` replacement command (default on), \\`${CLI_NAME} stats\\` at session end (default\\n` +\n ` on), and an opening \\`${CLI_NAME} map\\` at session start (opt-in) — plus an\\n` +\n ` instruction-file snippet that teaches \\`${CLI_NAME} retrieve\\` after a deny.\\n` +\n ` Harnesses are tiered honestly: verified (Claude Code, Codex), experimental\\n` +\n ` (Gemini, Grok, Hermes, Cursor, opencode, Cline — schemas from the capability\\n` +\n ` matrix, not yet smoke-tested), advisory (KiloCode, Aider — instructions only,\\n` +\n ` nothing enforced). Same discipline as init: every file listed before a final\\n` +\n ` confirm, no existing file overwritten without a per-file yes, re-runs edit\\n` +\n ` toggles. ${CLI_NAME} hooks remove takes it back out. Guard settings live in\\n` +\n ` smelt.config.json (\"hooks\": {\"thresholdBytes\", \"enforcement\": \"deny\"|\"rewrite\"});\\n` +\n ` deny is the default — rewrite substitutes commands in-flight only where a\\n` +\n ` harness supports it, and never silently.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): HooksInvocation {\n const action = positionals[1];\n if (action !== 'install' && action !== 'remove') {\n throw new CliUsageError(\n `${CLI_NAME}: hooks needs an action — install or remove.\\n` +\n ` ${CLI_NAME} hooks install [--harness <id>]\\n` +\n ` ${CLI_NAME} hooks remove [--harness <id>]`,\n );\n }\n if (positionals.length > 2) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks ${action} takes no further arguments, got ` +\n `${positionals.slice(2).join(', ')}.`,\n );\n }\n return {\n mode: 'hooks',\n action,\n ...(values.harness === undefined ? {} : { harness: values.harness }),\n };\n },\n\n /** Nothing to merge: the wizard asks, and reads `smelt.config.json` itself. */\n resolve(invocation: HooksInvocation): HooksInvocation {\n return invocation;\n },\n\n /** Interactive like `init`, so it needs the same stream, and refuses without one. */\n async run(resolved: HooksInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks ${resolved.action} is interactive, and this invocation has ` +\n `no interactive input stream. Run \\`${CLI_NAME} hooks ${resolved.action}\\` ` +\n `from a terminal.`,\n );\n }\n return await runHooks(resolved.action, resolved.harness, {\n input: io.initInput,\n output: io.stdout,\n cwd: io.cwd ?? process.cwd(),\n });\n },\n};\n"]}
@@ -0,0 +1,64 @@
1
+ import process from 'node:process';
2
+ import { CliUsageError } from '../../errors.js';
3
+ import { runHooks } from '../hooks.js';
4
+ import { CLI_NAME } from '../shell.js';
5
+ export const hooksCommand = {
6
+ name: 'hooks',
7
+ flags: ['harness'],
8
+ refusal: `hooks is interactive; the wizard asks the rest.`,
9
+ usage: {
10
+ synopsis: ['hooks install [--harness <id>]', 'hooks remove [--harness <id>]'],
11
+ section: {
12
+ heading: 'HOOKS',
13
+ body: ` ${CLI_NAME} hooks install wires the smelt guard into agent-harness hooks: a\n` +
14
+ ` PreToolUse size-guard that refuses oversized raw reads with the exact ${CLI_NAME}\n` +
15
+ ` replacement command (default on), \`${CLI_NAME} stats\` at session end (default\n` +
16
+ ` on), and an opening \`${CLI_NAME} map\` at session start (opt-in) — plus an\n` +
17
+ ` instruction-file snippet that teaches \`${CLI_NAME} retrieve\` after a deny.\n` +
18
+ ` Harnesses are tiered honestly: verified (Claude Code, Codex), experimental\n` +
19
+ ` (Gemini, Grok, Hermes, Cursor, opencode, Cline — schemas from the capability\n` +
20
+ ` matrix, not yet smoke-tested), advisory (KiloCode, Aider — instructions only,\n` +
21
+ ` nothing enforced). Same discipline as init: every file listed before a final\n` +
22
+ ` confirm, no existing file overwritten without a per-file yes, re-runs edit\n` +
23
+ ` toggles. ${CLI_NAME} hooks remove takes it back out. Guard settings live in\n` +
24
+ ` smelt.config.json ("hooks": {"thresholdBytes", "enforcement": "deny"|"rewrite"});\n` +
25
+ ` deny is the default — rewrite substitutes commands in-flight only where a\n` +
26
+ ` harness supports it, and never silently.`,
27
+ },
28
+ },
29
+ parse(values, positionals) {
30
+ const action = positionals[1];
31
+ if (action !== 'install' && action !== 'remove') {
32
+ throw new CliUsageError(`${CLI_NAME}: hooks needs an action — install or remove.\n` +
33
+ ` ${CLI_NAME} hooks install [--harness <id>]\n` +
34
+ ` ${CLI_NAME} hooks remove [--harness <id>]`);
35
+ }
36
+ if (positionals.length > 2) {
37
+ throw new CliUsageError(`${CLI_NAME}: hooks ${action} takes no further arguments, got ` +
38
+ `${positionals.slice(2).join(', ')}.`);
39
+ }
40
+ return {
41
+ mode: 'hooks',
42
+ action,
43
+ ...(values.harness === undefined ? {} : { harness: values.harness }),
44
+ };
45
+ },
46
+ /** Nothing to merge: the wizard asks, and reads `smelt.config.json` itself. */
47
+ resolve(invocation) {
48
+ return invocation;
49
+ },
50
+ /** Interactive like `init`, so it needs the same stream, and refuses without one. */
51
+ async run(resolved, io) {
52
+ if (io.initInput === undefined) {
53
+ throw new CliUsageError(`${CLI_NAME}: hooks ${resolved.action} is interactive, and this invocation has ` +
54
+ `no interactive input stream. Run \`${CLI_NAME} hooks ${resolved.action}\` ` +
55
+ `from a terminal.`);
56
+ }
57
+ return await runHooks(resolved.action, resolved.harness, {
58
+ input: io.initInput,
59
+ output: io.stdout,
60
+ cwd: io.cwd ?? process.cwd(),
61
+ });
62
+ },
63
+ };
64
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../../src/cli/subcommands/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAuBvC,MAAM,CAAC,MAAM,YAAY,GAAiD;IACxE,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,CAAC,SAAS,CAAC;IAClB,OAAO,EAAE,iDAAiD;IAC1D,KAAK,EAAE;QACL,QAAQ,EAAE,CAAC,gCAAgC,EAAE,+BAA+B,CAAC;QAC7E,OAAO,EAAE;YACP,OAAO,EAAE,OAAO;YAChB,IAAI,EACF,KAAK,QAAQ,oEAAoE;gBACjF,2EAA2E,QAAQ,IAAI;gBACvF,yCAAyC,QAAQ,oCAAoC;gBACrF,2BAA2B,QAAQ,8CAA8C;gBACjF,6CAA6C,QAAQ,6BAA6B;gBAClF,gFAAgF;gBAChF,kFAAkF;gBAClF,mFAAmF;gBACnF,kFAAkF;gBAClF,gFAAgF;gBAChF,cAAc,QAAQ,2DAA2D;gBACjF,uFAAuF;gBACvF,+EAA+E;gBAC/E,4CAA4C;SAC/C;KACF;IAED,KAAK,CAAC,MAAkB,EAAE,WAA8B;QACtD,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;QAC9B,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YAChD,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,gDAAgD;gBACzD,KAAK,QAAQ,mCAAmC;gBAChD,KAAK,QAAQ,gCAAgC,CAChD,CAAC;QACJ,CAAC;QACD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,WAAW,MAAM,mCAAmC;gBAC7D,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACxC,CAAC;QACJ,CAAC;QACD,OAAO;YACL,IAAI,EAAE,OAAO;YACb,MAAM;YACN,GAAG,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;SACrE,CAAC;IACJ,CAAC;IAED,+EAA+E;IAC/E,OAAO,CAAC,UAA2B;QACjC,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,qFAAqF;IACrF,KAAK,CAAC,GAAG,CAAC,QAAyB,EAAE,EAAS;QAC5C,IAAI,EAAE,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,WAAW,QAAQ,CAAC,MAAM,2CAA2C;gBAC9E,sCAAsC,QAAQ,UAAU,QAAQ,CAAC,MAAM,KAAK;gBAC5E,kBAAkB,CACrB,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,OAAO,EAAE;YACvD,KAAK,EAAE,EAAE,CAAC,SAAS;YACnB,MAAM,EAAE,EAAE,CAAC,MAAM;YACjB,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;CACF,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runHooks } from '../hooks.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt hooks install` / `smelt hooks remove` — the harness-hooks installer's front\n * door. The installer itself is `cli/hooks.ts`; this file is only the verb.\n *\n * Like `init`, it is interactive — the wizard asks everything except which harness —\n * so `--harness` is the one flag it owns, and the registry refuses the rest. The id\n * itself is validated in `cli/hooks.ts` against the harness registry in `src/harness/`,\n * which is also where the `--harness` help list comes from.\n */\n\n/** `smelt hooks <install|remove> [--harness <id>]` — parsed. */\nexport interface HooksInvocation {\n readonly mode: 'hooks';\n readonly action: 'install' | 'remove';\n readonly harness?: string;\n}\n\nexport const hooksCommand: Subcommand<HooksInvocation, HooksInvocation> = {\n name: 'hooks',\n flags: ['harness'],\n refusal: `hooks is interactive; the wizard asks the rest.`,\n usage: {\n synopsis: ['hooks install [--harness <id>]', 'hooks remove [--harness <id>]'],\n section: {\n heading: 'HOOKS',\n body:\n ` ${CLI_NAME} hooks install wires the smelt guard into agent-harness hooks: a\\n` +\n ` PreToolUse size-guard that refuses oversized raw reads with the exact ${CLI_NAME}\\n` +\n ` replacement command (default on), \\`${CLI_NAME} stats\\` at session end (default\\n` +\n ` on), and an opening \\`${CLI_NAME} map\\` at session start (opt-in) — plus an\\n` +\n ` instruction-file snippet that teaches \\`${CLI_NAME} retrieve\\` after a deny.\\n` +\n ` Harnesses are tiered honestly: verified (Claude Code, Codex), experimental\\n` +\n ` (Gemini, Grok, Hermes, Cursor, opencode, Cline — schemas from the capability\\n` +\n ` matrix, not yet smoke-tested), advisory (KiloCode, Aider — instructions only,\\n` +\n ` nothing enforced). Same discipline as init: every file listed before a final\\n` +\n ` confirm, no existing file overwritten without a per-file yes, re-runs edit\\n` +\n ` toggles. ${CLI_NAME} hooks remove takes it back out. Guard settings live in\\n` +\n ` smelt.config.json (\"hooks\": {\"thresholdBytes\", \"enforcement\": \"deny\"|\"rewrite\"});\\n` +\n ` deny is the default — rewrite substitutes commands in-flight only where a\\n` +\n ` harness supports it, and never silently.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): HooksInvocation {\n const action = positionals[1];\n if (action !== 'install' && action !== 'remove') {\n throw new CliUsageError(\n `${CLI_NAME}: hooks needs an action — install or remove.\\n` +\n ` ${CLI_NAME} hooks install [--harness <id>]\\n` +\n ` ${CLI_NAME} hooks remove [--harness <id>]`,\n );\n }\n if (positionals.length > 2) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks ${action} takes no further arguments, got ` +\n `${positionals.slice(2).join(', ')}.`,\n );\n }\n return {\n mode: 'hooks',\n action,\n ...(values.harness === undefined ? {} : { harness: values.harness }),\n };\n },\n\n /** Nothing to merge: the wizard asks, and reads `smelt.config.json` itself. */\n resolve(invocation: HooksInvocation): HooksInvocation {\n return invocation;\n },\n\n /** Interactive like `init`, so it needs the same stream, and refuses without one. */\n async run(resolved: HooksInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks ${resolved.action} is interactive, and this invocation has ` +\n `no interactive input stream. Run \\`${CLI_NAME} hooks ${resolved.action}\\` ` +\n `from a terminal.`,\n );\n }\n return await runHooks(resolved.action, resolved.harness, {\n input: io.initInput,\n output: io.stdout,\n cwd: io.cwd ?? process.cwd(),\n });\n },\n};\n"]}
@@ -0,0 +1,19 @@
1
+ import type { Subcommand } from './subcommand.ts';
2
+ /**
3
+ * `smelt init` — the setup wizard's front door. The wizard itself is `cli/init.ts`,
4
+ * a pure function over an input/output pair; this file is only the verb.
5
+ *
6
+ * It owns no flags at all, and that is the whole design: the wizard asks one question
7
+ * at a time and writes nothing until a final confirm, so a flag would be an answer
8
+ * given twice. It also merges nothing — it reads `smelt.config.json` itself, with its
9
+ * own tolerance, because a wizard you run to *fix* a malformed config must not refuse
10
+ * to start because the config is malformed.
11
+ */
12
+ /** `smelt init` — no flags, no arguments, nothing but the word. */
13
+ export interface InitInvocation {
14
+ readonly mode: 'init';
15
+ readonly focus: readonly string[];
16
+ readonly json: boolean;
17
+ }
18
+ export declare const initCommand: Subcommand<InitInvocation, InitInvocation>;
19
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/init.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;GASG;AAEH,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,WAAW,EAAE,UAAU,CAAC,cAAc,EAAE,cAAc,CA4ClE,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runInit } from '../init.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt init` — the setup wizard's front door. The wizard itself is `cli/init.ts`,\n * a pure function over an input/output pair; this file is only the verb.\n *\n * It owns no flags at all, and that is the whole design: the wizard asks one question\n * at a time and writes nothing until a final confirm, so a flag would be an answer\n * given twice. It also merges nothing — it reads `smelt.config.json` itself, with its\n * own tolerance, because a wizard you run to *fix* a malformed config must not refuse\n * to start because the config is malformed.\n */\n\n/** `smelt init` — no flags, no arguments, nothing but the word. */\nexport interface InitInvocation {\n readonly mode: 'init';\n readonly focus: readonly string[];\n readonly json: boolean;\n}\n\nexport const initCommand: Subcommand<InitInvocation, InitInvocation> = {\n name: 'init',\n flags: [],\n refusal: `init is interactive: it asks instead.`,\n usage: {\n synopsis: [],\n occasional: ['init'],\n section: {\n heading: 'CONFIG',\n body:\n ` ${CLI_NAME} init walks you through writing a smelt.config.json (and optional typed\\n` +\n ` stubs), one question at a time; nothing is written until a final confirm. Runs read\\n` +\n ` the nearest smelt.config.json, walking up from the working directory, for DEFAULTS\\n` +\n ` only — budget, strategy, store. An explicit flag always wins, and a malformed config\\n` +\n ` is a usage error, never silently ignored.`,\n },\n },\n\n parse(_values: FlagValues, positionals: readonly string[]): InitInvocation {\n // `smelt init` is a subcommand, so a file literally named `init` needs `./init`.\n if (positionals.length > 1) {\n throw new CliUsageError(`${CLI_NAME}: init takes no further arguments.`);\n }\n return { mode: 'init', focus: [], json: false };\n },\n\n /** Nothing to merge: every answer is asked for, and the config is read by the wizard. */\n resolve(invocation: InitInvocation): InitInvocation {\n return invocation;\n },\n\n async run(_resolved: InitInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: init is interactive, and this invocation has no interactive ` +\n `input stream. Run \\`${CLI_NAME} init\\` from a terminal.`,\n );\n }\n return await runInit({\n input: io.initInput,\n output: io.stdout,\n cwd: io.cwd ?? process.cwd(),\n });\n },\n};\n"]}
@@ -0,0 +1,44 @@
1
+ import process from 'node:process';
2
+ import { CliUsageError } from '../../errors.js';
3
+ import { runInit } from '../init.js';
4
+ import { CLI_NAME } from '../shell.js';
5
+ export const initCommand = {
6
+ name: 'init',
7
+ flags: [],
8
+ refusal: `init is interactive: it asks instead.`,
9
+ usage: {
10
+ synopsis: [],
11
+ occasional: ['init'],
12
+ section: {
13
+ heading: 'CONFIG',
14
+ body: ` ${CLI_NAME} init walks you through writing a smelt.config.json (and optional typed\n` +
15
+ ` stubs), one question at a time; nothing is written until a final confirm. Runs read\n` +
16
+ ` the nearest smelt.config.json, walking up from the working directory, for DEFAULTS\n` +
17
+ ` only — budget, strategy, store. An explicit flag always wins, and a malformed config\n` +
18
+ ` is a usage error, never silently ignored.`,
19
+ },
20
+ },
21
+ parse(_values, positionals) {
22
+ // `smelt init` is a subcommand, so a file literally named `init` needs `./init`.
23
+ if (positionals.length > 1) {
24
+ throw new CliUsageError(`${CLI_NAME}: init takes no further arguments.`);
25
+ }
26
+ return { mode: 'init', focus: [], json: false };
27
+ },
28
+ /** Nothing to merge: every answer is asked for, and the config is read by the wizard. */
29
+ resolve(invocation) {
30
+ return invocation;
31
+ },
32
+ async run(_resolved, io) {
33
+ if (io.initInput === undefined) {
34
+ throw new CliUsageError(`${CLI_NAME}: init is interactive, and this invocation has no interactive ` +
35
+ `input stream. Run \`${CLI_NAME} init\` from a terminal.`);
36
+ }
37
+ return await runInit({
38
+ input: io.initInput,
39
+ output: io.stdout,
40
+ cwd: io.cwd ?? process.cwd(),
41
+ });
42
+ },
43
+ };
44
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/cli/subcommands/init.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAwBvC,MAAM,CAAC,MAAM,WAAW,GAA+C;IACrE,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,EAAE;IACT,OAAO,EAAE,uCAAuC;IAChD,KAAK,EAAE;QACL,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,CAAC,MAAM,CAAC;QACpB,OAAO,EAAE;YACP,OAAO,EAAE,QAAQ;YACjB,IAAI,EACF,KAAK,QAAQ,2EAA2E;gBACxF,yFAAyF;gBACzF,wFAAwF;gBACxF,0FAA0F;gBAC1F,6CAA6C;SAChD;KACF;IAED,KAAK,CAAC,OAAmB,EAAE,WAA8B;QACvD,iFAAiF;QACjF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,aAAa,CAAC,GAAG,QAAQ,oCAAoC,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAClD,CAAC;IAED,yFAAyF;IACzF,OAAO,CAAC,UAA0B;QAChC,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,SAAyB,EAAE,EAAS;QAC5C,IAAI,EAAE,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,gEAAgE;gBACzE,uBAAuB,QAAQ,0BAA0B,CAC5D,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,OAAO,CAAC;YACnB,KAAK,EAAE,EAAE,CAAC,SAAS;YACnB,MAAM,EAAE,EAAE,CAAC,MAAM;YACjB,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;CACF,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runInit } from '../init.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt init` — the setup wizard's front door. The wizard itself is `cli/init.ts`,\n * a pure function over an input/output pair; this file is only the verb.\n *\n * It owns no flags at all, and that is the whole design: the wizard asks one question\n * at a time and writes nothing until a final confirm, so a flag would be an answer\n * given twice. It also merges nothing — it reads `smelt.config.json` itself, with its\n * own tolerance, because a wizard you run to *fix* a malformed config must not refuse\n * to start because the config is malformed.\n */\n\n/** `smelt init` — no flags, no arguments, nothing but the word. */\nexport interface InitInvocation {\n readonly mode: 'init';\n readonly focus: readonly string[];\n readonly json: boolean;\n}\n\nexport const initCommand: Subcommand<InitInvocation, InitInvocation> = {\n name: 'init',\n flags: [],\n refusal: `init is interactive: it asks instead.`,\n usage: {\n synopsis: [],\n occasional: ['init'],\n section: {\n heading: 'CONFIG',\n body:\n ` ${CLI_NAME} init walks you through writing a smelt.config.json (and optional typed\\n` +\n ` stubs), one question at a time; nothing is written until a final confirm. Runs read\\n` +\n ` the nearest smelt.config.json, walking up from the working directory, for DEFAULTS\\n` +\n ` only — budget, strategy, store. An explicit flag always wins, and a malformed config\\n` +\n ` is a usage error, never silently ignored.`,\n },\n },\n\n parse(_values: FlagValues, positionals: readonly string[]): InitInvocation {\n // `smelt init` is a subcommand, so a file literally named `init` needs `./init`.\n if (positionals.length > 1) {\n throw new CliUsageError(`${CLI_NAME}: init takes no further arguments.`);\n }\n return { mode: 'init', focus: [], json: false };\n },\n\n /** Nothing to merge: every answer is asked for, and the config is read by the wizard. */\n resolve(invocation: InitInvocation): InitInvocation {\n return invocation;\n },\n\n async run(_resolved: InitInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: init is interactive, and this invocation has no interactive ` +\n `input stream. Run \\`${CLI_NAME} init\\` from a terminal.`,\n );\n }\n return await runInit({\n input: io.initInput,\n output: io.stdout,\n cwd: io.cwd ?? process.cwd(),\n });\n },\n};\n"]}
@@ -0,0 +1,75 @@
1
+ import type { RepoMap } from '../../repomap/map.ts';
2
+ import type { LoadedConfig } from '../config.ts';
3
+ import type { Subcommand } from './subcommand.ts';
4
+ /**
5
+ * `smelt map <dir>` — the repo map's front door.
6
+ *
7
+ * Deliberately **not** a `--strategy` name: `buildRepoMap` returns a {@link RepoMap},
8
+ * not an `ElisionPlan` — nothing is elided, stored, or reversible — so it gets its own
9
+ * verb and its own envelope instead of a strategy name that would lie about what comes
10
+ * back. And it never exits {@link EXIT.overBudget}: a smelt plan may refuse to cut kept
11
+ * regions and come back too big, while the map fits itself to the budget by
12
+ * construction, so no exit code pretends an over-budget map can happen.
13
+ */
14
+ /**
15
+ * `smelt map <dir>` — the repo-map subcommand, parsed. A separate shape rather than
16
+ * more optional fields on `SmeltInvocation`, because the two commands share
17
+ * almost nothing: a map has a directory instead of a file/stdin, an ignore list and
18
+ * a cache directory instead of a language and a strategy.
19
+ */
20
+ export interface MapInvocation {
21
+ readonly mode: 'map';
22
+ /** The repository root to map. Always present — `map` without a directory is a usage error. */
23
+ readonly dir: string;
24
+ /** `undefined` means the flag was not given — the config default may apply. */
25
+ readonly budgetBytes?: number;
26
+ readonly focus: readonly string[];
27
+ /** `--ignore` entries, replacing the built-in default list when non-empty. */
28
+ readonly ignore: readonly string[];
29
+ /** `--cache <dir>`: only when given does the map write to disk. */
30
+ readonly cacheDir?: string;
31
+ readonly json: boolean;
32
+ }
33
+ /**
34
+ * Everything one `smelt map` run needs, fully merged — {@link ResolvedRun}'s sibling,
35
+ * not a contortion of it. The two commands share exactly one merged value (the
36
+ * budget), so they share the *seam* that owns precedence — every verb resolving its
37
+ * own flags against the same config — not a struct whose fields would mostly be lies
38
+ * for one of them: a map has no store, no strategy, no stdin, and its ignore/cache
39
+ * legs mean nothing to a single-blob run.
40
+ */
41
+ export interface ResolvedMapRun {
42
+ readonly budgetBytes: number;
43
+ /** Where the budget came from. A missing budget never gets here — it throws. */
44
+ readonly budgetSource: 'flag' | 'config';
45
+ readonly dir: string;
46
+ readonly focus: readonly string[];
47
+ /** `undefined` means "use the library's default ignore list". Flags only. */
48
+ readonly ignore?: readonly string[];
49
+ /** Only when present does the map write to disk. Flags only; the config has no say. */
50
+ readonly cacheDir?: string;
51
+ readonly json: boolean;
52
+ }
53
+ /**
54
+ * The `smelt map --json` envelope format. Its own version line, because the two
55
+ * envelopes carry different structures and must be able to move independently —
56
+ * a map envelope has no elided bytes to carry, since a map elides nothing.
57
+ */
58
+ export declare const CLI_MAP_JSON_FORMAT = "smelt-map-cli/v1";
59
+ /** What `smelt map --json` prints: the {@link RepoMap} verbatim, versioned. */
60
+ export interface CliMapJsonEnvelope {
61
+ readonly format: string;
62
+ /** The {@link RepoMap} exactly as `buildRepoMap` returned it. */
63
+ readonly map: RepoMap;
64
+ }
65
+ export declare const mapCommand: Subcommand<MapInvocation, ResolvedMapRun>;
66
+ /**
67
+ * Merge one `'map'`-mode invocation with the loaded config and the built-ins. The
68
+ * config contributes exactly what it contributes to a smelt run — `defaultBudgetBytes`,
69
+ * a default the user chose explicitly — and nothing else: the store and strategy legs
70
+ * are single-blob concerns, and the map ignores them rather than reinterpreting them.
71
+ *
72
+ * @throws {CliUsageError} when neither `--budget` nor the config names a budget.
73
+ */
74
+ export declare function resolveMapRun(invocation: MapInvocation, config: LoadedConfig | undefined): ResolvedMapRun;
75
+ //# sourceMappingURL=map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/map.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAOjD,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;;GASG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,+FAA+F;IAC/F,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,gFAAgF;IAChF,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC;IACzC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,uFAAuF;IACvF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,qBAAqB,CAAC;AAEtD,+EAA+E;AAC/E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;CACvB;AAED,eAAO,MAAM,UAAU,EAAE,UAAU,CAAC,aAAa,EAAE,cAAc,CAgEhE,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,aAAa,EACzB,MAAM,EAAE,YAAY,GAAG,SAAS,GAC/B,cAAc,CAyBhB","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { budgetRequired, readTree } from '../../ops/inputs.ts';\nimport { mapTree } from '../../ops/verbs.ts';\nimport type { RepoMap } from '../../repomap/map.ts';\nimport { CONFIG_FILE_NAME } from '../config.ts';\nimport type { LoadedConfig } from '../config.ts';\nimport { formatMapReport } from '../report.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport { parseBudget } from './flags.ts';\nimport type { FlagValues } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt map <dir>` — the repo map's front door.\n *\n * Deliberately **not** a `--strategy` name: `buildRepoMap` returns a {@link RepoMap},\n * not an `ElisionPlan` — nothing is elided, stored, or reversible — so it gets its own\n * verb and its own envelope instead of a strategy name that would lie about what comes\n * back. And it never exits {@link EXIT.overBudget}: a smelt plan may refuse to cut kept\n * regions and come back too big, while the map fits itself to the budget by\n * construction, so no exit code pretends an over-budget map can happen.\n */\n\n/**\n * `smelt map <dir>` — the repo-map subcommand, parsed. A separate shape rather than\n * more optional fields on `SmeltInvocation`, because the two commands share\n * almost nothing: a map has a directory instead of a file/stdin, an ignore list and\n * a cache directory instead of a language and a strategy.\n */\nexport interface MapInvocation {\n readonly mode: 'map';\n /** The repository root to map. Always present — `map` without a directory is a usage error. */\n readonly dir: string;\n /** `undefined` means the flag was not given — the config default may apply. */\n readonly budgetBytes?: number;\n readonly focus: readonly string[];\n /** `--ignore` entries, replacing the built-in default list when non-empty. */\n readonly ignore: readonly string[];\n /** `--cache <dir>`: only when given does the map write to disk. */\n readonly cacheDir?: string;\n readonly json: boolean;\n}\n\n/**\n * Everything one `smelt map` run needs, fully merged — {@link ResolvedRun}'s sibling,\n * not a contortion of it. The two commands share exactly one merged value (the\n * budget), so they share the *seam* that owns precedence — every verb resolving its\n * own flags against the same config — not a struct whose fields would mostly be lies\n * for one of them: a map has no store, no strategy, no stdin, and its ignore/cache\n * legs mean nothing to a single-blob run.\n */\nexport interface ResolvedMapRun {\n readonly budgetBytes: number;\n /** Where the budget came from. A missing budget never gets here — it throws. */\n readonly budgetSource: 'flag' | 'config';\n readonly dir: string;\n readonly focus: readonly string[];\n /** `undefined` means \"use the library's default ignore list\". Flags only. */\n readonly ignore?: readonly string[];\n /** Only when present does the map write to disk. Flags only; the config has no say. */\n readonly cacheDir?: string;\n readonly json: boolean;\n}\n\n/**\n * The `smelt map --json` envelope format. Its own version line, because the two\n * envelopes carry different structures and must be able to move independently —\n * a map envelope has no elided bytes to carry, since a map elides nothing.\n */\nexport const CLI_MAP_JSON_FORMAT = 'smelt-map-cli/v1';\n\n/** What `smelt map --json` prints: the {@link RepoMap} verbatim, versioned. */\nexport interface CliMapJsonEnvelope {\n readonly format: string;\n /** The {@link RepoMap} exactly as `buildRepoMap` returned it. */\n readonly map: RepoMap;\n}\n\nexport const mapCommand: Subcommand<MapInvocation, ResolvedMapRun> = {\n name: 'map',\n flags: ['budget', 'focus', 'ignore', 'cache', 'json'],\n refusal:\n `map reads a whole tree, detects each file's language itself, and is not a planner ` +\n `strategy — it returns a map, not an elision plan, and elides nothing, so there is ` +\n `nothing to put back.`,\n usage: {\n synopsis: [\n 'map <dir> --budget <bytes> [--focus <term>]... [--ignore <entry>]... [--cache <dir>]',\n ],\n section: {\n heading: 'MAP',\n body:\n ` ${CLI_NAME} map <dir> renders a ranked symbol map of a whole repository — modelled\\n` +\n ` on Aider's repo-map (aider.chat/docs/repomap.html, design by Paul Gauthier) — to\\n` +\n ` stdout, with a short report on stderr. Local files only: symlinks are never\\n` +\n ` followed, binary files are skipped, and the map writes nothing to disk unless\\n` +\n ` --cache names a directory. Every included symbol carries a receipt: its\\n` +\n ` definition site and the measured reference counts that ranked it. Unlike a\\n` +\n ` smelt run, map never exits\\n` +\n ` 1: a plan can come back over budget because ${CLI_NAME} refuses to cut regions you\\n` +\n ` asked to keep, but the map fits itself to the budget by construction — symbols\\n` +\n ` are appended in rank order until the next line would not fit.`,\n },\n },\n\n /**\n * Exactly one directory, and the same budget rules as everywhere else — a missing\n * `--budget` is not an error *here* (the config may carry `defaultBudgetBytes`), a\n * malformed one always is.\n */\n parse(values: FlagValues, positionals: readonly string[]): MapInvocation {\n if (positionals.length < 2) {\n throw new CliUsageError(\n `${CLI_NAME}: map needs the directory to read.\\n` +\n ` ${CLI_NAME} map <dir> --budget <bytes> [--focus <term>]...`,\n );\n }\n if (positionals.length > 2) {\n throw new CliUsageError(\n `${CLI_NAME}: map takes exactly one directory, got ` +\n `${String(positionals.length - 1)} (${positionals.slice(1).join(', ')}).`,\n );\n }\n const budgetBytes = parseBudget(values.budget);\n return {\n mode: 'map',\n dir: positionals[1]!,\n ...(budgetBytes === undefined ? {} : { budgetBytes }),\n focus: values.focus ?? [],\n ignore: values.ignore ?? [],\n ...(values.cache === undefined ? {} : { cacheDir: values.cache }),\n json: values.json === true,\n };\n },\n\n resolve(invocation: MapInvocation, config: ConfigSource): ResolvedMapRun {\n return resolveMapRun(invocation, config());\n },\n\n run(resolved: ResolvedMapRun, io: CliIo): Promise<number> {\n return runMap(resolved, io);\n },\n};\n\n/**\n * Merge one `'map'`-mode invocation with the loaded config and the built-ins. The\n * config contributes exactly what it contributes to a smelt run — `defaultBudgetBytes`,\n * a default the user chose explicitly — and nothing else: the store and strategy legs\n * are single-blob concerns, and the map ignores them rather than reinterpreting them.\n *\n * @throws {CliUsageError} when neither `--budget` nor the config names a budget.\n */\nexport function resolveMapRun(\n invocation: MapInvocation,\n config: LoadedConfig | undefined,\n): ResolvedMapRun {\n const budgetBytes = invocation.budgetBytes ?? config?.config.defaultBudgetBytes;\n if (budgetBytes === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: ` +\n budgetRequired({\n knob: '--budget',\n stake: 'the map to leave out',\n advice:\n `Pass --budget, or set defaultBudgetBytes in ${CONFIG_FILE_NAME} ` +\n `(\\`${CLI_NAME} init\\` writes one).\\n` +\n ` ${CLI_NAME} map src --budget 4000 --focus handleRequest`,\n }),\n );\n }\n\n return {\n budgetBytes,\n budgetSource: invocation.budgetBytes !== undefined ? 'flag' : 'config',\n dir: invocation.dir,\n focus: invocation.focus,\n ...(invocation.ignore.length === 0 ? {} : { ignore: invocation.ignore }),\n ...(invocation.cacheDir === undefined ? {} : { cacheDir: invocation.cacheDir }),\n json: invocation.json,\n };\n}\n\n/**\n * One `smelt map` run: prove the target is a tree, hand it to {@link mapTree}, render.\n *\n * The merge, including the budget-required refusal, lives in {@link resolveMapRun} —\n * the same arrangement the smelt verb uses over its {@link ResolvedRun}. The walk\n * itself belongs to no front door: the `repo_map` tool calls the same op, and refuses\n * a file with the same sentence in its own vocabulary.\n */\nasync function runMap(run: ResolvedMapRun, io: CliIo): Promise<number> {\n const tree = readTree(run.dir, run.dir, {\n tree: 'map',\n file: `\\`${CLI_NAME} <file>\\``,\n });\n if (!tree.ok) throw new CliUsageError(`${CLI_NAME}: ${tree.refusal}`);\n\n const map = await mapTree({\n root: tree.value,\n budgetBytes: run.budgetBytes,\n focus: run.focus,\n ...(run.ignore === undefined ? {} : { ignore: run.ignore }),\n ...(run.cacheDir === undefined ? {} : { cacheDir: run.cacheDir }),\n });\n\n if (run.json) {\n const mapEnvelope: CliMapJsonEnvelope = { format: CLI_MAP_JSON_FORMAT, map };\n io.stdout(`${JSON.stringify(mapEnvelope, null, 2)}\\n`);\n } else {\n io.stdout(map.text);\n }\n io.stderr(formatMapReport({ map, source: run.dir, budgetSource: run.budgetSource }));\n\n return EXIT.ok;\n}\n"]}
@@ -0,0 +1,132 @@
1
+ import { CliUsageError } from '../../errors.js';
2
+ import { budgetRequired, readTree } from '../../ops/inputs.js';
3
+ import { mapTree } from '../../ops/verbs.js';
4
+ import { CONFIG_FILE_NAME } from '../config.js';
5
+ import { formatMapReport } from '../report.js';
6
+ import { CLI_NAME, EXIT } from '../shell.js';
7
+ import { parseBudget } from './flags.js';
8
+ /**
9
+ * The `smelt map --json` envelope format. Its own version line, because the two
10
+ * envelopes carry different structures and must be able to move independently —
11
+ * a map envelope has no elided bytes to carry, since a map elides nothing.
12
+ */
13
+ export const CLI_MAP_JSON_FORMAT = 'smelt-map-cli/v1';
14
+ export const mapCommand = {
15
+ name: 'map',
16
+ flags: ['budget', 'focus', 'ignore', 'cache', 'json'],
17
+ refusal: `map reads a whole tree, detects each file's language itself, and is not a planner ` +
18
+ `strategy — it returns a map, not an elision plan, and elides nothing, so there is ` +
19
+ `nothing to put back.`,
20
+ usage: {
21
+ synopsis: [
22
+ 'map <dir> --budget <bytes> [--focus <term>]... [--ignore <entry>]... [--cache <dir>]',
23
+ ],
24
+ section: {
25
+ heading: 'MAP',
26
+ body: ` ${CLI_NAME} map <dir> renders a ranked symbol map of a whole repository — modelled\n` +
27
+ ` on Aider's repo-map (aider.chat/docs/repomap.html, design by Paul Gauthier) — to\n` +
28
+ ` stdout, with a short report on stderr. Local files only: symlinks are never\n` +
29
+ ` followed, binary files are skipped, and the map writes nothing to disk unless\n` +
30
+ ` --cache names a directory. Every included symbol carries a receipt: its\n` +
31
+ ` definition site and the measured reference counts that ranked it. Unlike a\n` +
32
+ ` smelt run, map never exits\n` +
33
+ ` 1: a plan can come back over budget because ${CLI_NAME} refuses to cut regions you\n` +
34
+ ` asked to keep, but the map fits itself to the budget by construction — symbols\n` +
35
+ ` are appended in rank order until the next line would not fit.`,
36
+ },
37
+ },
38
+ /**
39
+ * Exactly one directory, and the same budget rules as everywhere else — a missing
40
+ * `--budget` is not an error *here* (the config may carry `defaultBudgetBytes`), a
41
+ * malformed one always is.
42
+ */
43
+ parse(values, positionals) {
44
+ if (positionals.length < 2) {
45
+ throw new CliUsageError(`${CLI_NAME}: map needs the directory to read.\n` +
46
+ ` ${CLI_NAME} map <dir> --budget <bytes> [--focus <term>]...`);
47
+ }
48
+ if (positionals.length > 2) {
49
+ throw new CliUsageError(`${CLI_NAME}: map takes exactly one directory, got ` +
50
+ `${String(positionals.length - 1)} (${positionals.slice(1).join(', ')}).`);
51
+ }
52
+ const budgetBytes = parseBudget(values.budget);
53
+ return {
54
+ mode: 'map',
55
+ dir: positionals[1],
56
+ ...(budgetBytes === undefined ? {} : { budgetBytes }),
57
+ focus: values.focus ?? [],
58
+ ignore: values.ignore ?? [],
59
+ ...(values.cache === undefined ? {} : { cacheDir: values.cache }),
60
+ json: values.json === true,
61
+ };
62
+ },
63
+ resolve(invocation, config) {
64
+ return resolveMapRun(invocation, config());
65
+ },
66
+ run(resolved, io) {
67
+ return runMap(resolved, io);
68
+ },
69
+ };
70
+ /**
71
+ * Merge one `'map'`-mode invocation with the loaded config and the built-ins. The
72
+ * config contributes exactly what it contributes to a smelt run — `defaultBudgetBytes`,
73
+ * a default the user chose explicitly — and nothing else: the store and strategy legs
74
+ * are single-blob concerns, and the map ignores them rather than reinterpreting them.
75
+ *
76
+ * @throws {CliUsageError} when neither `--budget` nor the config names a budget.
77
+ */
78
+ export function resolveMapRun(invocation, config) {
79
+ const budgetBytes = invocation.budgetBytes ?? config?.config.defaultBudgetBytes;
80
+ if (budgetBytes === undefined) {
81
+ throw new CliUsageError(`${CLI_NAME}: ` +
82
+ budgetRequired({
83
+ knob: '--budget',
84
+ stake: 'the map to leave out',
85
+ advice: `Pass --budget, or set defaultBudgetBytes in ${CONFIG_FILE_NAME} ` +
86
+ `(\`${CLI_NAME} init\` writes one).\n` +
87
+ ` ${CLI_NAME} map src --budget 4000 --focus handleRequest`,
88
+ }));
89
+ }
90
+ return {
91
+ budgetBytes,
92
+ budgetSource: invocation.budgetBytes !== undefined ? 'flag' : 'config',
93
+ dir: invocation.dir,
94
+ focus: invocation.focus,
95
+ ...(invocation.ignore.length === 0 ? {} : { ignore: invocation.ignore }),
96
+ ...(invocation.cacheDir === undefined ? {} : { cacheDir: invocation.cacheDir }),
97
+ json: invocation.json,
98
+ };
99
+ }
100
+ /**
101
+ * One `smelt map` run: prove the target is a tree, hand it to {@link mapTree}, render.
102
+ *
103
+ * The merge, including the budget-required refusal, lives in {@link resolveMapRun} —
104
+ * the same arrangement the smelt verb uses over its {@link ResolvedRun}. The walk
105
+ * itself belongs to no front door: the `repo_map` tool calls the same op, and refuses
106
+ * a file with the same sentence in its own vocabulary.
107
+ */
108
+ async function runMap(run, io) {
109
+ const tree = readTree(run.dir, run.dir, {
110
+ tree: 'map',
111
+ file: `\`${CLI_NAME} <file>\``,
112
+ });
113
+ if (!tree.ok)
114
+ throw new CliUsageError(`${CLI_NAME}: ${tree.refusal}`);
115
+ const map = await mapTree({
116
+ root: tree.value,
117
+ budgetBytes: run.budgetBytes,
118
+ focus: run.focus,
119
+ ...(run.ignore === undefined ? {} : { ignore: run.ignore }),
120
+ ...(run.cacheDir === undefined ? {} : { cacheDir: run.cacheDir }),
121
+ });
122
+ if (run.json) {
123
+ const mapEnvelope = { format: CLI_MAP_JSON_FORMAT, map };
124
+ io.stdout(`${JSON.stringify(mapEnvelope, null, 2)}\n`);
125
+ }
126
+ else {
127
+ io.stdout(map.text);
128
+ }
129
+ io.stderr(formatMapReport({ map, source: run.dir, budgetSource: run.budgetSource }));
130
+ return EXIT.ok;
131
+ }
132
+ //# sourceMappingURL=map.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map.js","sourceRoot":"","sources":["../../../src/cli/subcommands/map.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { budgetRequired, readTree } from '../../ops/inputs.ts';\nimport { mapTree } from '../../ops/verbs.ts';\nimport type { RepoMap } from '../../repomap/map.ts';\nimport { CONFIG_FILE_NAME } from '../config.ts';\nimport type { LoadedConfig } from '../config.ts';\nimport { formatMapReport } from '../report.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport { parseBudget } from './flags.ts';\nimport type { FlagValues } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt map <dir>` — the repo map's front door.\n *\n * Deliberately **not** a `--strategy` name: `buildRepoMap` returns a {@link RepoMap},\n * not an `ElisionPlan` — nothing is elided, stored, or reversible — so it gets its own\n * verb and its own envelope instead of a strategy name that would lie about what comes\n * back. And it never exits {@link EXIT.overBudget}: a smelt plan may refuse to cut kept\n * regions and come back too big, while the map fits itself to the budget by\n * construction, so no exit code pretends an over-budget map can happen.\n */\n\n/**\n * `smelt map <dir>` — the repo-map subcommand, parsed. A separate shape rather than\n * more optional fields on `SmeltInvocation`, because the two commands share\n * almost nothing: a map has a directory instead of a file/stdin, an ignore list and\n * a cache directory instead of a language and a strategy.\n */\nexport interface MapInvocation {\n readonly mode: 'map';\n /** The repository root to map. Always present — `map` without a directory is a usage error. */\n readonly dir: string;\n /** `undefined` means the flag was not given — the config default may apply. */\n readonly budgetBytes?: number;\n readonly focus: readonly string[];\n /** `--ignore` entries, replacing the built-in default list when non-empty. */\n readonly ignore: readonly string[];\n /** `--cache <dir>`: only when given does the map write to disk. */\n readonly cacheDir?: string;\n readonly json: boolean;\n}\n\n/**\n * Everything one `smelt map` run needs, fully merged — {@link ResolvedRun}'s sibling,\n * not a contortion of it. The two commands share exactly one merged value (the\n * budget), so they share the *seam* that owns precedence — every verb resolving its\n * own flags against the same config — not a struct whose fields would mostly be lies\n * for one of them: a map has no store, no strategy, no stdin, and its ignore/cache\n * legs mean nothing to a single-blob run.\n */\nexport interface ResolvedMapRun {\n readonly budgetBytes: number;\n /** Where the budget came from. A missing budget never gets here — it throws. */\n readonly budgetSource: 'flag' | 'config';\n readonly dir: string;\n readonly focus: readonly string[];\n /** `undefined` means \"use the library's default ignore list\". Flags only. */\n readonly ignore?: readonly string[];\n /** Only when present does the map write to disk. Flags only; the config has no say. */\n readonly cacheDir?: string;\n readonly json: boolean;\n}\n\n/**\n * The `smelt map --json` envelope format. Its own version line, because the two\n * envelopes carry different structures and must be able to move independently —\n * a map envelope has no elided bytes to carry, since a map elides nothing.\n */\nexport const CLI_MAP_JSON_FORMAT = 'smelt-map-cli/v1';\n\n/** What `smelt map --json` prints: the {@link RepoMap} verbatim, versioned. */\nexport interface CliMapJsonEnvelope {\n readonly format: string;\n /** The {@link RepoMap} exactly as `buildRepoMap` returned it. */\n readonly map: RepoMap;\n}\n\nexport const mapCommand: Subcommand<MapInvocation, ResolvedMapRun> = {\n name: 'map',\n flags: ['budget', 'focus', 'ignore', 'cache', 'json'],\n refusal:\n `map reads a whole tree, detects each file's language itself, and is not a planner ` +\n `strategy — it returns a map, not an elision plan, and elides nothing, so there is ` +\n `nothing to put back.`,\n usage: {\n synopsis: [\n 'map <dir> --budget <bytes> [--focus <term>]... [--ignore <entry>]... [--cache <dir>]',\n ],\n section: {\n heading: 'MAP',\n body:\n ` ${CLI_NAME} map <dir> renders a ranked symbol map of a whole repository — modelled\\n` +\n ` on Aider's repo-map (aider.chat/docs/repomap.html, design by Paul Gauthier) — to\\n` +\n ` stdout, with a short report on stderr. Local files only: symlinks are never\\n` +\n ` followed, binary files are skipped, and the map writes nothing to disk unless\\n` +\n ` --cache names a directory. Every included symbol carries a receipt: its\\n` +\n ` definition site and the measured reference counts that ranked it. Unlike a\\n` +\n ` smelt run, map never exits\\n` +\n ` 1: a plan can come back over budget because ${CLI_NAME} refuses to cut regions you\\n` +\n ` asked to keep, but the map fits itself to the budget by construction — symbols\\n` +\n ` are appended in rank order until the next line would not fit.`,\n },\n },\n\n /**\n * Exactly one directory, and the same budget rules as everywhere else — a missing\n * `--budget` is not an error *here* (the config may carry `defaultBudgetBytes`), a\n * malformed one always is.\n */\n parse(values: FlagValues, positionals: readonly string[]): MapInvocation {\n if (positionals.length < 2) {\n throw new CliUsageError(\n `${CLI_NAME}: map needs the directory to read.\\n` +\n ` ${CLI_NAME} map <dir> --budget <bytes> [--focus <term>]...`,\n );\n }\n if (positionals.length > 2) {\n throw new CliUsageError(\n `${CLI_NAME}: map takes exactly one directory, got ` +\n `${String(positionals.length - 1)} (${positionals.slice(1).join(', ')}).`,\n );\n }\n const budgetBytes = parseBudget(values.budget);\n return {\n mode: 'map',\n dir: positionals[1]!,\n ...(budgetBytes === undefined ? {} : { budgetBytes }),\n focus: values.focus ?? [],\n ignore: values.ignore ?? [],\n ...(values.cache === undefined ? {} : { cacheDir: values.cache }),\n json: values.json === true,\n };\n },\n\n resolve(invocation: MapInvocation, config: ConfigSource): ResolvedMapRun {\n return resolveMapRun(invocation, config());\n },\n\n run(resolved: ResolvedMapRun, io: CliIo): Promise<number> {\n return runMap(resolved, io);\n },\n};\n\n/**\n * Merge one `'map'`-mode invocation with the loaded config and the built-ins. The\n * config contributes exactly what it contributes to a smelt run — `defaultBudgetBytes`,\n * a default the user chose explicitly — and nothing else: the store and strategy legs\n * are single-blob concerns, and the map ignores them rather than reinterpreting them.\n *\n * @throws {CliUsageError} when neither `--budget` nor the config names a budget.\n */\nexport function resolveMapRun(\n invocation: MapInvocation,\n config: LoadedConfig | undefined,\n): ResolvedMapRun {\n const budgetBytes = invocation.budgetBytes ?? config?.config.defaultBudgetBytes;\n if (budgetBytes === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: ` +\n budgetRequired({\n knob: '--budget',\n stake: 'the map to leave out',\n advice:\n `Pass --budget, or set defaultBudgetBytes in ${CONFIG_FILE_NAME} ` +\n `(\\`${CLI_NAME} init\\` writes one).\\n` +\n ` ${CLI_NAME} map src --budget 4000 --focus handleRequest`,\n }),\n );\n }\n\n return {\n budgetBytes,\n budgetSource: invocation.budgetBytes !== undefined ? 'flag' : 'config',\n dir: invocation.dir,\n focus: invocation.focus,\n ...(invocation.ignore.length === 0 ? {} : { ignore: invocation.ignore }),\n ...(invocation.cacheDir === undefined ? {} : { cacheDir: invocation.cacheDir }),\n json: invocation.json,\n };\n}\n\n/**\n * One `smelt map` run: prove the target is a tree, hand it to {@link mapTree}, render.\n *\n * The merge, including the budget-required refusal, lives in {@link resolveMapRun} —\n * the same arrangement the smelt verb uses over its {@link ResolvedRun}. The walk\n * itself belongs to no front door: the `repo_map` tool calls the same op, and refuses\n * a file with the same sentence in its own vocabulary.\n */\nasync function runMap(run: ResolvedMapRun, io: CliIo): Promise<number> {\n const tree = readTree(run.dir, run.dir, {\n tree: 'map',\n file: `\\`${CLI_NAME} <file>\\``,\n });\n if (!tree.ok) throw new CliUsageError(`${CLI_NAME}: ${tree.refusal}`);\n\n const map = await mapTree({\n root: tree.value,\n budgetBytes: run.budgetBytes,\n focus: run.focus,\n ...(run.ignore === undefined ? {} : { ignore: run.ignore }),\n ...(run.cacheDir === undefined ? {} : { cacheDir: run.cacheDir }),\n });\n\n if (run.json) {\n const mapEnvelope: CliMapJsonEnvelope = { format: CLI_MAP_JSON_FORMAT, map };\n io.stdout(`${JSON.stringify(mapEnvelope, null, 2)}\\n`);\n } else {\n io.stdout(map.text);\n }\n io.stderr(formatMapReport({ map, source: run.dir, budgetSource: run.budgetSource }));\n\n return EXIT.ok;\n}\n"]}
@@ -0,0 +1,80 @@
1
+ import type { FlagName, FlagValues } from './flags.ts';
2
+ import type { HooksInvocation } from './hooks.ts';
3
+ import type { InitInvocation } from './init.ts';
4
+ import type { MapInvocation } from './map.ts';
5
+ import type { RetrieveInvocation } from './retrieve.ts';
6
+ import type { SmeltInvocation } from './smelt.ts';
7
+ import type { StatsInvocation } from './stats.ts';
8
+ import type { Subcommand, Verb } from './subcommand.ts';
9
+ /**
10
+ * The registry — every verb `smelt` answers to, one {@link Subcommand} each.
11
+ *
12
+ * `Record<Verb, Subcommand>` on purpose, exactly like `LANGUAGE_PROFILES` and
13
+ * `HARNESS_PROFILES`: adding a `Verb` in `subcommand.ts` without writing its file is a
14
+ * compile error, so the verb list and the facts cannot drift. Every derived view — the
15
+ * USAGE block, the help's sections, the `map only.` prefixes in OPTIONS, and above all
16
+ * the flag-ownership refusal below — is computed from this object, never written twice.
17
+ *
18
+ * Key order is meaningful: it is the order every rendered list uses (USAGE, the help
19
+ * sections, the owners named in a refusal), so keep it stable and append new verbs at
20
+ * the end.
21
+ */
22
+ export declare const SUBCOMMANDS: Readonly<Record<Verb, AnySubcommand>>;
23
+ /** Everything `parseSmeltArgs` can return. Narrow on `mode`. */
24
+ export type CliInvocation = SmeltInvocation | InitInvocation | MapInvocation | RetrieveInvocation | StatsInvocation | HooksInvocation;
25
+ /**
26
+ * One registry entry, with its verb's own invocation and resolved types erased.
27
+ *
28
+ * The erasure is what lets six differently-typed commands live in one `Record`, and it
29
+ * is safe because the two ends are never crossed: `runCli` hands a command exactly the
30
+ * invocation that command's own `parse` produced (`verbFor` maps each `mode` back to
31
+ * the verb that minted it), and the resolved value never leaves the pair of calls that
32
+ * makes and consumes it.
33
+ */
34
+ export type AnySubcommand = Subcommand<CliInvocation, unknown>;
35
+ /** Every command, in registry order. The list every rendered block walks. */
36
+ export declare const SUBCOMMAND_LIST: readonly AnySubcommand[];
37
+ /**
38
+ * The verbs with a word on the command line — everything but the default one.
39
+ *
40
+ * The default verb is excluded deliberately: `smelt smelt` is a file named `smelt`,
41
+ * not a recursive invocation, the same way `smelt map` as a *file* needs `./map`.
42
+ */
43
+ export declare const NAMED_VERBS: readonly Verb[];
44
+ /**
45
+ * Which command a command line selects: `positionals[0]` when it names a verb, the
46
+ * default verb otherwise. The whole of subcommand dispatch, in one lookup.
47
+ */
48
+ export declare function subcommandFor(positionals: readonly string[]): AnySubcommand;
49
+ /** The command that produced an invocation, for dispatch. */
50
+ export declare function commandFor(mode: Exclude<CliInvocation['mode'], 'help' | 'version'>): AnySubcommand;
51
+ /**
52
+ * The commands that own a flag, in registry order — empty for the two global flags,
53
+ * which no verb owns. A flag with exactly one owner has an unambiguous home worth
54
+ * naming in a refusal (and worth prefixing its OPTIONS entry with); a flag several
55
+ * verbs share (`--json`, `--budget`, `--focus`) has none, and pointing at one of them
56
+ * would be a guess.
57
+ */
58
+ export declare function ownersOf(flag: FlagName): readonly AnySubcommand[];
59
+ /**
60
+ * Refuse every flag the chosen verb does not own — the one message that replaced five
61
+ * hand-written ones.
62
+ *
63
+ * Before this existed, each verb refused each other verb's flags in prose, so the
64
+ * refusals were O(verbs × flags) sentences kept in sync by hand and an eleventh flag
65
+ * edited five of them. Now ownership is declared once per verb and the message is
66
+ * generated from three derived facts, in the order a reader needs them:
67
+ *
68
+ * 1. **what this verb takes**, and what it got instead — the offending flag, named;
69
+ * 2. **where the flag does belong**, when exactly one verb owns it, listed as that
70
+ * owner's *exclusively* owned flags (so `--ignore` here still reads "--ignore and
71
+ * --cache belong to `smelt map`", exactly as the hand-written message did);
72
+ * 3. **why not here** — the verb's own `refusal` sentence, the half of the old
73
+ * messages worth keeping, and the only half a verb still writes.
74
+ *
75
+ * Every refusal stays a {@link CliUsageError}, so every one of them still exits 2.
76
+ *
77
+ * @throws {CliUsageError} naming the offending flag, this verb, and the flag's owner.
78
+ */
79
+ export declare function refuseForeignFlags(command: AnySubcommand, values: FlagValues): void;
80
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/registry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AAEjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAExD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,CAO7D,CAAC;AAEF,gEAAgE;AAChE,MAAM,MAAM,aAAa,GACrB,eAAe,GACf,cAAc,GACd,aAAa,GACb,kBAAkB,GAClB,eAAe,GACf,eAAe,CAAC;AAEpB;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;AAE/D,6EAA6E;AAC7E,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAA+B,CAAC;AAEpF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,IAAI,EAEtC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,aAAa,CAI3E;AAkBD,6DAA6D;AAC7D,wBAAgB,UAAU,CACxB,IAAI,EAAE,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,GACvD,aAAa,CAEf;AAYD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,aAAa,EAAE,CAEjE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAYnF","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { CLI_NAME } from '../shell.ts';\n\nimport { flagList, VERB_FLAGS } from './flags.ts';\nimport type { FlagName, FlagValues, VerbFlag } from './flags.ts';\nimport { hooksCommand } from './hooks.ts';\nimport type { HooksInvocation } from './hooks.ts';\nimport { initCommand } from './init.ts';\nimport type { InitInvocation } from './init.ts';\nimport { mapCommand } from './map.ts';\nimport type { MapInvocation } from './map.ts';\nimport { retrieveCommand } from './retrieve.ts';\nimport type { RetrieveInvocation } from './retrieve.ts';\nimport { smeltCommand } from './smelt.ts';\nimport type { SmeltInvocation } from './smelt.ts';\nimport { statsCommand } from './stats.ts';\nimport type { StatsInvocation } from './stats.ts';\nimport { DEFAULT_VERB } from './subcommand.ts';\nimport type { Subcommand, Verb } from './subcommand.ts';\n\n/**\n * The registry — every verb `smelt` answers to, one {@link Subcommand} each.\n *\n * `Record<Verb, Subcommand>` on purpose, exactly like `LANGUAGE_PROFILES` and\n * `HARNESS_PROFILES`: adding a `Verb` in `subcommand.ts` without writing its file is a\n * compile error, so the verb list and the facts cannot drift. Every derived view — the\n * USAGE block, the help's sections, the `map only.` prefixes in OPTIONS, and above all\n * the flag-ownership refusal below — is computed from this object, never written twice.\n *\n * Key order is meaningful: it is the order every rendered list uses (USAGE, the help\n * sections, the owners named in a refusal), so keep it stable and append new verbs at\n * the end.\n */\nexport const SUBCOMMANDS: Readonly<Record<Verb, AnySubcommand>> = {\n smelt: smeltCommand,\n init: initCommand,\n map: mapCommand,\n retrieve: retrieveCommand,\n stats: statsCommand,\n hooks: hooksCommand,\n};\n\n/** Everything `parseSmeltArgs` can return. Narrow on `mode`. */\nexport type CliInvocation =\n | SmeltInvocation\n | InitInvocation\n | MapInvocation\n | RetrieveInvocation\n | StatsInvocation\n | HooksInvocation;\n\n/**\n * One registry entry, with its verb's own invocation and resolved types erased.\n *\n * The erasure is what lets six differently-typed commands live in one `Record`, and it\n * is safe because the two ends are never crossed: `runCli` hands a command exactly the\n * invocation that command's own `parse` produced (`verbFor` maps each `mode` back to\n * the verb that minted it), and the resolved value never leaves the pair of calls that\n * makes and consumes it.\n */\nexport type AnySubcommand = Subcommand<CliInvocation, unknown>;\n\n/** Every command, in registry order. The list every rendered block walks. */\nexport const SUBCOMMAND_LIST: readonly AnySubcommand[] = Object.values(SUBCOMMANDS);\n\n/**\n * The verbs with a word on the command line — everything but the default one.\n *\n * The default verb is excluded deliberately: `smelt smelt` is a file named `smelt`,\n * not a recursive invocation, the same way `smelt map` as a *file* needs `./map`.\n */\nexport const NAMED_VERBS: readonly Verb[] = SUBCOMMAND_LIST.map((command) => command.name).filter(\n (name) => name !== DEFAULT_VERB,\n);\n\n/**\n * Which command a command line selects: `positionals[0]` when it names a verb, the\n * default verb otherwise. The whole of subcommand dispatch, in one lookup.\n */\nexport function subcommandFor(positionals: readonly string[]): AnySubcommand {\n const first = positionals[0];\n const named = NAMED_VERBS.find((verb) => verb === first);\n return SUBCOMMANDS[named ?? DEFAULT_VERB];\n}\n\n/**\n * Which verb minted an invocation. Total over the modes `parseSmeltArgs` can return\n * for a verb, so a new mode without a home is a compile error — `'reconstruct'` maps\n * to the default verb because `--reconstruct` is that verb's second job, not a\n * seventh command.\n */\nconst VERB_BY_MODE = {\n smelt: 'smelt',\n reconstruct: 'smelt',\n init: 'init',\n map: 'map',\n retrieve: 'retrieve',\n stats: 'stats',\n hooks: 'hooks',\n} as const satisfies Record<Exclude<CliInvocation['mode'], 'help' | 'version'>, Verb>;\n\n/** The command that produced an invocation, for dispatch. */\nexport function commandFor(\n mode: Exclude<CliInvocation['mode'], 'help' | 'version'>,\n): AnySubcommand {\n return SUBCOMMANDS[VERB_BY_MODE[mode]];\n}\n\n/** How a command is named in prose. The default verb has no word to name it by. */\nfunction label(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : command.name;\n}\n\n/** How a command is named as the *owner* of a flag: the form you would type. */\nfunction ownerLabel(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : `\\`${CLI_NAME} ${command.name}\\``;\n}\n\n/**\n * The commands that own a flag, in registry order — empty for the two global flags,\n * which no verb owns. A flag with exactly one owner has an unambiguous home worth\n * naming in a refusal (and worth prefixing its OPTIONS entry with); a flag several\n * verbs share (`--json`, `--budget`, `--focus`) has none, and pointing at one of them\n * would be a guess.\n */\nexport function ownersOf(flag: FlagName): readonly AnySubcommand[] {\n return SUBCOMMAND_LIST.filter((command) => (command.flags as readonly string[]).includes(flag));\n}\n\n/**\n * Refuse every flag the chosen verb does not own — the one message that replaced five\n * hand-written ones.\n *\n * Before this existed, each verb refused each other verb's flags in prose, so the\n * refusals were O(verbs × flags) sentences kept in sync by hand and an eleventh flag\n * edited five of them. Now ownership is declared once per verb and the message is\n * generated from three derived facts, in the order a reader needs them:\n *\n * 1. **what this verb takes**, and what it got instead — the offending flag, named;\n * 2. **where the flag does belong**, when exactly one verb owns it, listed as that\n * owner's *exclusively* owned flags (so `--ignore` here still reads \"--ignore and\n * --cache belong to `smelt map`\", exactly as the hand-written message did);\n * 3. **why not here** — the verb's own `refusal` sentence, the half of the old\n * messages worth keeping, and the only half a verb still writes.\n *\n * Every refusal stays a {@link CliUsageError}, so every one of them still exits 2.\n *\n * @throws {CliUsageError} naming the offending flag, this verb, and the flag's owner.\n */\nexport function refuseForeignFlags(command: AnySubcommand, values: FlagValues): void {\n const foreign = VERB_FLAGS.filter(\n (flag) => values[flag] !== undefined && !command.flags.includes(flag),\n );\n if (foreign.length === 0) return;\n\n const takes =\n command.flags.length === 0\n ? `${label(command)} takes no flags (got ${flagList(foreign)}).`\n : `${label(command)} takes only ${flagList(command.flags)} (got ${flagList(foreign)}).`;\n\n throw new CliUsageError(`${CLI_NAME}: ${takes}${redirects(foreign)} ${command.refusal}`);\n}\n\n/**\n * ` --ignore and --cache belong to \\`smelt map\\`.` — one clause per single-owner verb.\n *\n * A clause names only the flags its owner owns **alone**, never the ones it shares.\n * `ownersOf(flag).length === 1` is the same test that decided the owner deserved a\n * clause at all, applied to the whole clause: a shared flag (`--budget`, `--focus`,\n * `--json`) has no single home, so naming it here would assert an ownership that the\n * OPTIONS block — which prefixes `map only.` by the same rule — correctly denies.\n */\nfunction redirects(foreign: readonly VerbFlag[]): string {\n const owners = SUBCOMMAND_LIST.filter((owner) =>\n foreign.some((flag) => {\n const claimants = ownersOf(flag);\n return claimants.length === 1 && claimants[0] === owner;\n }),\n );\n return owners\n .map((owner) => {\n const elsewhere = owner.flags.filter((flag) => ownersOf(flag).length === 1);\n const verb = elsewhere.length === 1 ? 'belongs' : 'belong';\n return ` ${flagList(elsewhere)} ${verb} to ${ownerLabel(owner)}.`;\n })\n .join('');\n}\n"]}