@smeltjs/core 0.3.0 → 0.5.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 (146) hide show
  1. package/dist/agents/guide.d.ts +68 -0
  2. package/dist/agents/guide.d.ts.map +1 -0
  3. package/dist/agents/guide.js +72 -0
  4. package/dist/agents/guide.js.map +1 -0
  5. package/dist/agents/instructions.d.ts +133 -0
  6. package/dist/agents/instructions.d.ts.map +1 -0
  7. package/dist/agents/instructions.js +212 -0
  8. package/dist/agents/instructions.js.map +1 -0
  9. package/dist/agents/lint.d.ts +154 -0
  10. package/dist/agents/lint.d.ts.map +1 -0
  11. package/dist/agents/lint.js +761 -0
  12. package/dist/agents/lint.js.map +1 -0
  13. package/dist/agents/split.d.ts +113 -0
  14. package/dist/agents/split.d.ts.map +1 -0
  15. package/dist/agents/split.js +234 -0
  16. package/dist/agents/split.js.map +1 -0
  17. package/dist/cli/agents.d.ts +39 -0
  18. package/dist/cli/agents.d.ts.map +1 -0
  19. package/dist/cli/agents.js +121 -0
  20. package/dist/cli/agents.js.map +1 -0
  21. package/dist/cli/args.d.ts +1 -0
  22. package/dist/cli/args.d.ts.map +1 -1
  23. package/dist/cli/args.js.map +1 -1
  24. package/dist/cli/bin.d.ts.map +1 -1
  25. package/dist/cli/bin.js +9 -4
  26. package/dist/cli/bin.js.map +1 -1
  27. package/dist/cli/config.d.ts +20 -0
  28. package/dist/cli/config.d.ts.map +1 -1
  29. package/dist/cli/config.js +34 -1
  30. package/dist/cli/config.js.map +1 -1
  31. package/dist/cli/doctor.d.ts +66 -0
  32. package/dist/cli/doctor.d.ts.map +1 -0
  33. package/dist/cli/doctor.js +137 -0
  34. package/dist/cli/doctor.js.map +1 -0
  35. package/dist/cli/hooks.d.ts +61 -18
  36. package/dist/cli/hooks.d.ts.map +1 -1
  37. package/dist/cli/hooks.js +178 -241
  38. package/dist/cli/hooks.js.map +1 -1
  39. package/dist/cli/init.d.ts.map +1 -1
  40. package/dist/cli/init.js +8 -14
  41. package/dist/cli/init.js.map +1 -1
  42. package/dist/cli/installed.d.ts +56 -0
  43. package/dist/cli/installed.d.ts.map +1 -0
  44. package/dist/cli/installed.js +105 -0
  45. package/dist/cli/installed.js.map +1 -0
  46. package/dist/cli/lava.d.ts +31 -0
  47. package/dist/cli/lava.d.ts.map +1 -0
  48. package/dist/cli/lava.js +82 -0
  49. package/dist/cli/lava.js.map +1 -0
  50. package/dist/cli/report.d.ts +23 -0
  51. package/dist/cli/report.d.ts.map +1 -1
  52. package/dist/cli/report.js +140 -2
  53. package/dist/cli/report.js.map +1 -1
  54. package/dist/cli/run.d.ts +5 -3
  55. package/dist/cli/run.d.ts.map +1 -1
  56. package/dist/cli/run.js +2 -1
  57. package/dist/cli/run.js.map +1 -1
  58. package/dist/cli/setup.d.ts +87 -0
  59. package/dist/cli/setup.d.ts.map +1 -0
  60. package/dist/cli/setup.js +513 -0
  61. package/dist/cli/setup.js.map +1 -0
  62. package/dist/cli/shell.d.ts +11 -4
  63. package/dist/cli/shell.d.ts.map +1 -1
  64. package/dist/cli/shell.js.map +1 -1
  65. package/dist/cli/subcommands/agents.d.ts +72 -0
  66. package/dist/cli/subcommands/agents.d.ts.map +1 -0
  67. package/dist/cli/subcommands/agents.js +163 -0
  68. package/dist/cli/subcommands/agents.js.map +1 -0
  69. package/dist/cli/subcommands/doctor.d.ts +15 -0
  70. package/dist/cli/subcommands/doctor.d.ts.map +1 -0
  71. package/dist/cli/subcommands/doctor.js +43 -0
  72. package/dist/cli/subcommands/doctor.js.map +1 -0
  73. package/dist/cli/subcommands/flags.d.ts +16 -0
  74. package/dist/cli/subcommands/flags.d.ts.map +1 -1
  75. package/dist/cli/subcommands/flags.js +40 -2
  76. package/dist/cli/subcommands/flags.js.map +1 -1
  77. package/dist/cli/subcommands/hooks.d.ts.map +1 -1
  78. package/dist/cli/subcommands/hooks.js +29 -5
  79. package/dist/cli/subcommands/hooks.js.map +1 -1
  80. package/dist/cli/subcommands/init.d.ts.map +1 -1
  81. package/dist/cli/subcommands/init.js +2 -1
  82. package/dist/cli/subcommands/init.js.map +1 -1
  83. package/dist/cli/subcommands/registry.d.ts +4 -1
  84. package/dist/cli/subcommands/registry.d.ts.map +1 -1
  85. package/dist/cli/subcommands/registry.js +9 -0
  86. package/dist/cli/subcommands/registry.js.map +1 -1
  87. package/dist/cli/subcommands/setup.d.ts +23 -0
  88. package/dist/cli/subcommands/setup.d.ts.map +1 -0
  89. package/dist/cli/subcommands/setup.js +87 -0
  90. package/dist/cli/subcommands/setup.js.map +1 -0
  91. package/dist/cli/subcommands/subcommand.d.ts +1 -1
  92. package/dist/cli/subcommands/subcommand.d.ts.map +1 -1
  93. package/dist/cli/subcommands/subcommand.js.map +1 -1
  94. package/dist/cli/wizard.d.ts +72 -0
  95. package/dist/cli/wizard.d.ts.map +1 -0
  96. package/dist/cli/wizard.js +86 -0
  97. package/dist/cli/wizard.js.map +1 -0
  98. package/dist/harness/claude-code.d.ts.map +1 -1
  99. package/dist/harness/claude-code.js +10 -0
  100. package/dist/harness/claude-code.js.map +1 -1
  101. package/dist/harness/codex.d.ts.map +1 -1
  102. package/dist/harness/codex.js +2 -0
  103. package/dist/harness/codex.js.map +1 -1
  104. package/dist/harness/gemini.d.ts.map +1 -1
  105. package/dist/harness/gemini.js +1 -0
  106. package/dist/harness/gemini.js.map +1 -1
  107. package/dist/harness/grok.d.ts.map +1 -1
  108. package/dist/harness/grok.js +5 -1
  109. package/dist/harness/grok.js.map +1 -1
  110. package/dist/harness/hermes.d.ts.map +1 -1
  111. package/dist/harness/hermes.js +1 -0
  112. package/dist/harness/hermes.js.map +1 -1
  113. package/dist/harness/kilocode.d.ts.map +1 -1
  114. package/dist/harness/kilocode.js +1 -1
  115. package/dist/harness/kilocode.js.map +1 -1
  116. package/dist/harness/opencode.d.ts.map +1 -1
  117. package/dist/harness/opencode.js +10 -0
  118. package/dist/harness/opencode.js.map +1 -1
  119. package/dist/harness/profile.d.ts +49 -3
  120. package/dist/harness/profile.d.ts.map +1 -1
  121. package/dist/harness/profile.js +15 -0
  122. package/dist/harness/profile.js.map +1 -1
  123. package/dist/harness/registry.d.ts +55 -6
  124. package/dist/harness/registry.d.ts.map +1 -1
  125. package/dist/harness/registry.js +63 -6
  126. package/dist/harness/registry.js.map +1 -1
  127. package/dist/harness/snippet.d.ts +14 -1
  128. package/dist/harness/snippet.d.ts.map +1 -1
  129. package/dist/harness/snippet.js +18 -2
  130. package/dist/harness/snippet.js.map +1 -1
  131. package/dist/index.d.ts +22 -0
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +17 -0
  134. package/dist/index.js.map +1 -1
  135. package/dist/setup/recipe.d.ts +73 -0
  136. package/dist/setup/recipe.d.ts.map +1 -0
  137. package/dist/setup/recipe.js +76 -0
  138. package/dist/setup/recipe.js.map +1 -0
  139. package/dist/text/json-edit.d.ts +85 -0
  140. package/dist/text/json-edit.d.ts.map +1 -0
  141. package/dist/text/json-edit.js +290 -0
  142. package/dist/text/json-edit.js.map +1 -0
  143. package/dist/types.d.ts +0 -11
  144. package/dist/types.d.ts.map +1 -1
  145. package/dist/types.js.map +1 -1
  146. package/package.json +1 -1
@@ -34,7 +34,16 @@ export const CLI_FLAGS = {
34
34
  strategy: { type: 'string' },
35
35
  ignore: { type: 'string', multiple: true },
36
36
  cache: { type: 'string' },
37
- harness: { type: 'string' },
37
+ /**
38
+ * Repeatable because `setup` wires several harnesses in one run. `hooks` takes one
39
+ * per run and refuses a second id in its own parse — arity is a per-verb fact,
40
+ * expressed where the verb validates, with the one generated refusal; the table
41
+ * records how argv is read, not how many a verb accepts.
42
+ */
43
+ harness: { type: 'string', multiple: true },
44
+ yes: { type: 'boolean' },
45
+ 'no-mcp': { type: 'boolean' },
46
+ strict: { type: 'boolean' },
38
47
  json: { type: 'boolean' },
39
48
  reconstruct: { type: 'boolean' },
40
49
  help: { type: 'boolean', short: 'h' },
@@ -150,6 +159,34 @@ export const FLAG_HELP = {
150
159
  body: () => [
151
160
  'Skip harness detection and target one id:',
152
161
  ...optionList(HARNESS_IDS, OPTION_BODY_WIDTH),
162
+ 'Repeatable for setup; hooks takes one per run.',
163
+ ],
164
+ },
165
+ yes: {
166
+ label: '--yes',
167
+ body: () => [
168
+ "Non-interactive setup: the recipe's defaults, printed",
169
+ 'loudly as they are applied. Existing files are never',
170
+ 'overwritten — skipped with a note; hooks install edits',
171
+ 'them, and it asks per file.',
172
+ ],
173
+ },
174
+ 'no-mcp': {
175
+ label: '--no-mcp',
176
+ body: () => [
177
+ 'Setup only: skip the MCP registration step — the',
178
+ 'printed command and its note — for a hooks-only',
179
+ 'setup.',
180
+ ],
181
+ },
182
+ strict: {
183
+ label: '--strict',
184
+ body: () => [
185
+ 'Turn any lint finding into exit 1, for CI. Findings are',
186
+ 'advisory by default: the rules are heuristics about somebody',
187
+ "else's house style, and enforcing them uninvited would be",
188
+ `${CLI_NAME} deciding it. A budget you set yourself is different —`,
189
+ 'exceeding agents.budgetBytes exits 1 with or without --strict.',
153
190
  ],
154
191
  },
155
192
  json: {
@@ -160,7 +197,8 @@ export const FLAG_HELP = {
160
197
  'the SmeltResult verbatim, `elided` carries the bytes, so',
161
198
  'the envelope can be reconstructed; feed it back with',
162
199
  '--reconstruct. For map: { format, map }, the RepoMap',
163
- 'structure verbatim.',
200
+ 'structure verbatim. For agents lint: { format, report },',
201
+ 'the measured levels and every finding with its rule id.',
164
202
  ],
165
203
  },
166
204
  reconstruct: {
@@ -1 +1 @@
1
- {"version":3,"file":"flags.js","sourceRoot":"","sources":["../../../src/cli/subcommands/flags.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEnE,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC1B,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE;IACzC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC5B,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC5B,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IACzB,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC3B,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;IACzB,WAAW,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;IAChC,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE;IACrC,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;CACpB,CAAC;AAKX;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,SAAS,CAAwC,CAAC;AAKvF,oFAAoF;AACpF,MAAM,CAAC,MAAM,UAAU,GAAwB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAC1E,CAAC,IAAI,EAAoB,EAAE,CAAC,CAAE,YAAkC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAChF,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CAAC,KAA0B;IACjD,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IAC7F,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjD,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;AAC1E,CAAC;AA6BD;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,KAAwB,EAAE,KAAa;IAChE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC5B,MAAM,IAAI,GAAG,KAAK,KAAK,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC;QAClE,MAAM,SAAS,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;QACzD,IAAI,IAAI,KAAK,EAAE,IAAI,SAAS,CAAC,MAAM,GAAG,KAAK,EAAE,CAAC;YAC5C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,IAAI,GAAG,IAAI,CAAC;QACd,CAAC;aAAM,CAAC;YACN,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,CAAC;IACH,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,OAAO,KAAK,CAAC;AACf,CAAC;AAED,2FAA2F;AAC3F,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAE7B;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAyC;IAC7D,MAAM,EAAE;QACN,KAAK,EAAE,kBAAkB;QACzB,IAAI,EAAE,GAAG,EAAE,CAAC;YACV,6DAA6D;YAC7D,8DAA8D;YAC9D,8DAA8D;YAC9D,GAAG,QAAQ,iCAAiC;SAC7C;KACF;IACD,KAAK,EAAE;QACL,KAAK,EAAE,gBAAgB;QACvB,IAAI,EAAE,GAAG,EAAE,CAAC;YACV,6DAA6D;YAC7D,4DAA4D;YAC5D,6DAA6D;YAC7D,kDAAkD;SACnD;KACF;IACD,QAAQ,EAAE;QACR,KAAK,EAAE,iBAAiB;QACxB,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,+BAA+B,CAAC,GAAG,mBAAmB,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;KAC/F;IACD,QAAQ,EAAE;QACR,KAAK,EAAE,iBAAiB;QACxB,IAAI,EAAE,GAAG,EAAE,CAAC;YACV,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,gBAAgB,UAAU;YACnE,qDAAqD;YACrD,GAAG,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;YACrC,+DAA+D;YAC/D,8DAA8D;YAC9D,8CAA8C;SAC/C;KACF;IACD,MAAM,EAAE;QACN,KAAK,EAAE,kBAAkB;QACzB,IAAI,EAAE,GAAG,EAAE,CAAC;YACV,8CAA8C;YAC9C,+EAA+E;YAC/E,uEAAuE;YACvE,IAAI,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,0CAA0C;YAC5E,kDAAkD;SACnD;KACF;IACD,KAAK,EAAE;QACL,KAAK,EAAE,eAAe;QACtB,IAAI,EAAE,GAAG,EAAE,CAAC;YACV,gDAAgD;YAChD,0DAA0D;SAC3D;KACF;IACD,OAAO,EAAE;QACP,KAAK,EAAE,gBAAgB;QACvB,IAAI,EAAE,GAAG,EAAE,CAAC;YACV,2CAA2C;YAC3C,GAAG,UAAU,CAAC,WAAW,EAAE,iBAAiB,CAAC;SAC9C;KACF;IACD,IAAI,EAAE;QACJ,KAAK,EAAE,QAAQ;QACf,IAAI,EAAE,GAAG,EAAE,CAAC;YACV,sDAAsD;YACtD,0DAA0D;YAC1D,0DAA0D;YAC1D,sDAAsD;YACtD,sDAAsD;YACtD,qBAAqB;SACtB;KACF;IACD,WAAW,EAAE;QACX,KAAK,EAAE,eAAe;QACtB,IAAI,EAAE,GAAG,EAAE,CAAC;YACV,8DAA8D;YAC9D,+CAA+C;SAChD;KACF;IACD,IAAI,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE;IACzD,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,sBAAsB,CAAC,EAAE;CACtE,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,WAAW,CAAC,GAAuB;IACjD,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACxC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,MAAM,YAAY,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;IAClE,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACjC,IAAI,KAAK,KAAK,SAAS;QAAE,MAAM,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACxD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,iFAAiF;AACjF,SAAS,YAAY,CAAC,KAAkB,EAAE,GAAW;IACnD,OAAO,IAAI,aAAa,CAAC,GAAG,QAAQ,KAAK,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;AACtF,CAAC","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"]}
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 /**\n * Repeatable because `setup` wires several harnesses in one run. `hooks` takes one\n * per run and refuses a second id in its own parse — arity is a per-verb fact,\n * expressed where the verb validates, with the one generated refusal; the table\n * records how argv is read, not how many a verb accepts.\n */\n harness: { type: 'string', multiple: true },\n yes: { type: 'boolean' },\n 'no-mcp': { type: 'boolean' },\n strict: { type: 'boolean' },\n json: { type: 'boolean' },\n reconstruct: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean' },\n} as const;\n\n/** Every flag name, as a type. A verb cannot claim a flag that is not in the table. */\nexport type FlagName = keyof typeof CLI_FLAGS;\n\n/**\n * The flags answered *before* any verb, so no verb owns them and no verb may refuse\n * them: `smelt map --help` prints the help, exactly as it always has. Every other flag\n * belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.\n */\nexport const GLOBAL_FLAGS = ['help', 'version'] as const satisfies readonly FlagName[];\n\n/** A flag a verb can own — everything but the two global ones. */\nexport type VerbFlag = Exclude<FlagName, (typeof GLOBAL_FLAGS)[number]>;\n\n/** Every ownable flag, in table order: the list a refusal checks a verb against. */\nexport const VERB_FLAGS: readonly VerbFlag[] = Object.keys(CLI_FLAGS).filter(\n (name): name is VerbFlag => !(GLOBAL_FLAGS as readonly string[]).includes(name),\n);\n\n/**\n * `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.\n *\n * Every refusal that names more than one flag spells the list this way: the\n * ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`\n * refusal in `./smelt.ts`. It lives with the table because the *order* is the\n * table's — a refusal that listed flags in the order the user happened to type them\n * would read differently every time, and two refusals disagreeing about how to spell\n * the same pair is the kind of drift this file exists to remove.\n */\nexport function flagList(flags: readonly VerbFlag[]): string {\n const spelled = VERB_FLAGS.filter((flag) => flags.includes(flag)).map((flag) => `--${flag}`);\n if (spelled.length <= 1) return spelled.join('');\n return `${spelled.slice(0, -1).join(', ')} and ${spelled.at(-1) ?? ''}`;\n}\n\n/** What one flag's parsed value looks like, derived from how `parseArgs` was told to read it. */\ntype FlagValue<F> = F extends { readonly type: 'boolean' }\n ? boolean\n : F extends { readonly multiple: true }\n ? readonly string[]\n : string;\n\n/**\n * The parsed flags, as every `Subcommand.parse` sees them: one optional field per\n * flag, typed by the table above. Absent means the user did not type it — which is\n * the only thing a refusal needs to know, and the only thing a verb may act on.\n */\nexport type FlagValues = { readonly [K in FlagName]?: FlagValue<(typeof CLI_FLAGS)[K]> };\n\n/** How one flag appears in the OPTIONS block of `--help`. */\nexport interface FlagHelp {\n /** The left column, e.g. `--budget <bytes>` or `-h, --help`. */\n readonly label: string;\n /**\n * The description, already wrapped to the OPTIONS column — a function because three\n * entries render a registry (`SUPPORTED_LANGUAGES`, `STRATEGIES` +\n * `STRUCTURAL_LANGUAGES`, `HARNESS_IDS`) rather than a hand-typed list. The\n * ownership prefix is not here; it is generated.\n */\n body(): readonly string[];\n}\n\n/**\n * A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the\n * hand-typed version wrapped. `--strategy` and `--language` render their registries on\n * one line because they fit; the harness ids do not, and a list long enough to wrap is\n * exactly the list nobody keeps in sync by hand.\n */\nexport function optionList(items: readonly string[], width: number): readonly string[] {\n const lines: string[] = [];\n let line = '';\n items.forEach((item, index) => {\n const word = index === items.length - 1 ? `${item}.` : `${item},`;\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > width) {\n lines.push(line);\n line = word;\n } else {\n line = candidate;\n }\n });\n lines.push(line);\n return lines;\n}\n\n/** The width an OPTIONS description wraps within, once its 23-column indent is removed. */\nconst OPTION_BODY_WIDTH = 65;\n\n/**\n * The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag\n * added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text\n * cannot fall behind what the parser accepts.\n */\nexport const FLAG_HELP: Readonly<Record<FlagName, FlagHelp>> = {\n budget: {\n label: '--budget <bytes>',\n body: () => [\n 'Required, unless smelt.config.json sets defaultBudgetBytes.',\n 'Soft ceiling for the output, in UTF-8 bytes (for map: a hard',\n 'ceiling, met by construction). No built-in default: a budget',\n `${CLI_NAME} invented would decide for you.`,\n ],\n },\n focus: {\n label: '--focus <term>',\n body: () => [\n 'What you were looking for. Repeatable. Matching regions and',\n 'their context survive; the runs between them collapse. For',\n 'map: symbols matching a term (by name or path) are promoted',\n 'to the front of the fill order, ranks unchanged.',\n ],\n },\n 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 'Repeatable for setup; hooks takes one per run.',\n ],\n },\n yes: {\n label: '--yes',\n body: () => [\n \"Non-interactive setup: the recipe's defaults, printed\",\n 'loudly as they are applied. Existing files are never',\n 'overwritten — skipped with a note; hooks install edits',\n 'them, and it asks per file.',\n ],\n },\n 'no-mcp': {\n label: '--no-mcp',\n body: () => [\n 'Setup only: skip the MCP registration step — the',\n 'printed command and its note — for a hooks-only',\n 'setup.',\n ],\n },\n strict: {\n label: '--strict',\n body: () => [\n 'Turn any lint finding into exit 1, for CI. Findings are',\n 'advisory by default: the rules are heuristics about somebody',\n \"else's house style, and enforcing them uninvited would be\",\n `${CLI_NAME} deciding it. A budget you set yourself is different —`,\n 'exceeding agents.budgetBytes exits 1 with or without --strict.',\n ],\n },\n json: {\n label: '--json',\n body: () => [\n 'Print a JSON envelope on stdout instead of the text:',\n '{ format, result, elided } for a smelt run — `result` is',\n 'the SmeltResult verbatim, `elided` carries the bytes, so',\n 'the envelope can be reconstructed; feed it back with',\n '--reconstruct. For map: { format, map }, the RepoMap',\n 'structure verbatim. For agents lint: { format, report },',\n 'the measured levels and every finding with its rule id.',\n ],\n },\n reconstruct: {\n label: '--reconstruct',\n body: () => [\n 'Read a --json envelope and print the original text, byte for',\n 'byte. This is Law 3 you can run from a shell.',\n ],\n },\n help: { label: '-h, --help', body: () => ['This text.'] },\n version: { label: '--version', body: () => ['The package version.'] },\n};\n\n/**\n * `--budget` has no built-in default, for the same reason `smelt()` has none: a budget\n * smelt invented would be smelt deciding how much of the caller's context to throw\n * away, silently, at a number nobody chose. A *missing* flag is not an error here,\n * though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose\n * explicitly, and the verb's `resolve` errors only when neither exists. A malformed\n * value is always an error.\n *\n * It lives with the flag rather than with a verb because two verbs own `--budget`, and\n * the two of them agreeing on what \"4kb\" means is not something to leave to chance.\n *\n * The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a\n * leading `-` are answered by a digits-only test before anything numeric happens. The\n * *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also\n * where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`\n * here and `\"budgetBytes\"` there.\n */\nexport function parseBudget(raw: string | undefined): number | undefined {\n if (raw === undefined) return undefined;\n if (!/^\\d+$/.test(raw)) throw refuseBudget('not-an-integer', raw);\n const value = Number(raw);\n const fault = budgetFault(value);\n if (fault !== undefined) throw refuseBudget(fault, raw);\n return value;\n}\n\n/** The malformed-budget refusal, in the CLI's currency: prefixed, and exit 2. */\nfunction refuseBudget(fault: BudgetFault, raw: string): CliUsageError {\n return new CliUsageError(`${CLI_NAME}: ${budgetMalformed(fault, '--budget', raw)}`);\n}\n"]}
@@ -1 +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"]}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/hooks.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;GAQG;AAEH,gEAAgE;AAChE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAkBD,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,eAAe,EAAE,eAAe,CA6ErE,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { harnessesByTier, harnessNames } from '../../harness/registry.ts';\nimport type { HarnessTier } from '../../harness/profile.ts';\nimport { colorize } from '../lava.ts';\nimport { runHooks } from '../hooks.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt hooks install` / `smelt hooks remove` — the harness-hooks installer's front\n * door. The installer itself is `cli/hooks.ts`; this file is only the verb.\n *\n * Like `init`, it is interactive — the wizard asks everything except which harness —\n * so `--harness` is the one flag it owns, and the registry refuses the rest. The id\n * itself is validated in `cli/hooks.ts` against the harness registry in `src/harness/`,\n * which is also where the `--harness` help list comes from.\n */\n\n/** `smelt hooks <install|remove> [--harness <id>]` — parsed. */\nexport interface HooksInvocation {\n readonly mode: 'hooks';\n readonly action: 'install' | 'remove';\n readonly harness?: string;\n}\n\n/**\n * The harnesses at one tier, as the HOOKS paragraph spells them — from\n * `HarnessProfile.tier`, never a second list. A tier no profile claims renders empty\n * rather than naming a harness that moved.\n *\n * The line breaks around these are still hand-placed, and deliberately: this body is\n * byte-pinned by `test/__snapshots__/cli-usage.help.txt`, its paragraph is wrapped by\n * hand at no single width, and a generic wrapper would rewrite every line of the help\n * to derive three lists. The *membership* is what drifted — a promoted harness stayed\n * under its old tier — and membership is what this derives.\n */\nfunction tierNames(tier: HarnessTier): string {\n const group = harnessesByTier().find((candidate) => candidate.tier === tier);\n return group === undefined ? '' : harnessNames(group.harnesses);\n}\n\nexport const hooksCommand: Subcommand<HooksInvocation, HooksInvocation> = {\n name: 'hooks',\n flags: ['harness'],\n refusal: `hooks is interactive; the wizard asks the rest.`,\n usage: {\n synopsis: ['hooks install [--harness <id>]', 'hooks remove [--harness <id>]'],\n section: {\n heading: 'HOOKS',\n body:\n ` ${CLI_NAME} hooks install wires the smelt guard into agent-harness hooks: a\\n` +\n ` PreToolUse size-guard that refuses oversized raw reads with the exact ${CLI_NAME}\\n` +\n ` replacement command (default on), \\`${CLI_NAME} stats\\` at session end (default\\n` +\n ` on), and an opening \\`${CLI_NAME} map\\` at session start (opt-in) — plus an\\n` +\n ` instruction-file snippet that teaches \\`${CLI_NAME} retrieve\\` after a deny.\\n` +\n ` Harnesses are tiered honestly: verified (${tierNames('verified')}), experimental\\n` +\n ` (${tierNames('experimental')} — schemas from the capability\\n` +\n ` matrix, not yet smoke-tested), advisory (${tierNames('advisory')} — instructions only,\\n` +\n ` nothing enforced). Same discipline as init: every file listed before a final\\n` +\n ` confirm, no existing file overwritten without a per-file yes, re-runs edit\\n` +\n ` toggles. ${CLI_NAME} hooks remove takes it back out. Guard settings live in\\n` +\n ` smelt.config.json (\"hooks\": {\"thresholdBytes\", \"enforcement\": \"deny\"|\"rewrite\"});\\n` +\n ` deny is the default — rewrite substitutes commands in-flight only where a\\n` +\n ` harness supports it, and never silently.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): HooksInvocation {\n const action = positionals[1];\n if (action !== 'install' && action !== 'remove') {\n throw new CliUsageError(\n `${CLI_NAME}: hooks needs an action — install or remove.\\n` +\n ` ${CLI_NAME} hooks install [--harness <id>]\\n` +\n ` ${CLI_NAME} hooks remove [--harness <id>]`,\n );\n }\n if (positionals.length > 2) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks ${action} takes no further arguments, got ` +\n `${positionals.slice(2).join(', ')}.`,\n );\n }\n // `--harness` is repeatable for setup; this verb wires one action per run, and a\n // second id would be a second install the user believed had happened.\n if (values.harness !== undefined && values.harness.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks takes one --harness per run — repeat the command for ` +\n `each harness.`,\n );\n }\n return {\n mode: 'hooks',\n action,\n ...(values.harness === undefined ? {} : { harness: values.harness[0] }),\n };\n },\n\n /** Nothing to merge: the wizard asks, and reads `smelt.config.json` itself. */\n resolve(invocation: HooksInvocation): HooksInvocation {\n return invocation;\n },\n\n /** Interactive like `init`, so it needs the same stream, and refuses without one. */\n async run(resolved: HooksInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks ${resolved.action} is interactive, and this invocation has ` +\n `no interactive input stream. Run \\`${CLI_NAME} hooks ${resolved.action}\\` ` +\n `from a terminal.`,\n );\n }\n return await runHooks(resolved.action, resolved.harness, {\n input: io.initInput,\n output: (text) => io.stdout(colorize(text, io.color === true)),\n cwd: io.cwd ?? process.cwd(),\n version: io.version,\n });\n },\n};\n"]}
@@ -1,7 +1,24 @@
1
1
  import process from 'node:process';
2
2
  import { CliUsageError } from '../../errors.js';
3
+ import { harnessesByTier, harnessNames } from '../../harness/registry.js';
4
+ import { colorize } from '../lava.js';
3
5
  import { runHooks } from '../hooks.js';
4
6
  import { CLI_NAME } from '../shell.js';
7
+ /**
8
+ * The harnesses at one tier, as the HOOKS paragraph spells them — from
9
+ * `HarnessProfile.tier`, never a second list. A tier no profile claims renders empty
10
+ * rather than naming a harness that moved.
11
+ *
12
+ * The line breaks around these are still hand-placed, and deliberately: this body is
13
+ * byte-pinned by `test/__snapshots__/cli-usage.help.txt`, its paragraph is wrapped by
14
+ * hand at no single width, and a generic wrapper would rewrite every line of the help
15
+ * to derive three lists. The *membership* is what drifted — a promoted harness stayed
16
+ * under its old tier — and membership is what this derives.
17
+ */
18
+ function tierNames(tier) {
19
+ const group = harnessesByTier().find((candidate) => candidate.tier === tier);
20
+ return group === undefined ? '' : harnessNames(group.harnesses);
21
+ }
5
22
  export const hooksCommand = {
6
23
  name: 'hooks',
7
24
  flags: ['harness'],
@@ -15,9 +32,9 @@ export const hooksCommand = {
15
32
  ` replacement command (default on), \`${CLI_NAME} stats\` at session end (default\n` +
16
33
  ` on), and an opening \`${CLI_NAME} map\` at session start (opt-in) — plus an\n` +
17
34
  ` 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` +
35
+ ` Harnesses are tiered honestly: verified (${tierNames('verified')}), experimental\n` +
36
+ ` (${tierNames('experimental')} — schemas from the capability\n` +
37
+ ` matrix, not yet smoke-tested), advisory (${tierNames('advisory')} — instructions only,\n` +
21
38
  ` nothing enforced). Same discipline as init: every file listed before a final\n` +
22
39
  ` confirm, no existing file overwritten without a per-file yes, re-runs edit\n` +
23
40
  ` toggles. ${CLI_NAME} hooks remove takes it back out. Guard settings live in\n` +
@@ -37,10 +54,16 @@ export const hooksCommand = {
37
54
  throw new CliUsageError(`${CLI_NAME}: hooks ${action} takes no further arguments, got ` +
38
55
  `${positionals.slice(2).join(', ')}.`);
39
56
  }
57
+ // `--harness` is repeatable for setup; this verb wires one action per run, and a
58
+ // second id would be a second install the user believed had happened.
59
+ if (values.harness !== undefined && values.harness.length > 1) {
60
+ throw new CliUsageError(`${CLI_NAME}: hooks takes one --harness per run — repeat the command for ` +
61
+ `each harness.`);
62
+ }
40
63
  return {
41
64
  mode: 'hooks',
42
65
  action,
43
- ...(values.harness === undefined ? {} : { harness: values.harness }),
66
+ ...(values.harness === undefined ? {} : { harness: values.harness[0] }),
44
67
  };
45
68
  },
46
69
  /** Nothing to merge: the wizard asks, and reads `smelt.config.json` itself. */
@@ -56,8 +79,9 @@ export const hooksCommand = {
56
79
  }
57
80
  return await runHooks(resolved.action, resolved.harness, {
58
81
  input: io.initInput,
59
- output: io.stdout,
82
+ output: (text) => io.stdout(colorize(text, io.color === true)),
60
83
  cwd: io.cwd ?? process.cwd(),
84
+ version: io.version,
61
85
  });
62
86
  },
63
87
  };
@@ -1 +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"]}
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../../src/cli/subcommands/hooks.ts"],"names":[],"mappings":"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","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { harnessesByTier, harnessNames } from '../../harness/registry.ts';\nimport type { HarnessTier } from '../../harness/profile.ts';\nimport { colorize } from '../lava.ts';\nimport { runHooks } from '../hooks.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt hooks install` / `smelt hooks remove` — the harness-hooks installer's front\n * door. The installer itself is `cli/hooks.ts`; this file is only the verb.\n *\n * Like `init`, it is interactive — the wizard asks everything except which harness —\n * so `--harness` is the one flag it owns, and the registry refuses the rest. The id\n * itself is validated in `cli/hooks.ts` against the harness registry in `src/harness/`,\n * which is also where the `--harness` help list comes from.\n */\n\n/** `smelt hooks <install|remove> [--harness <id>]` — parsed. */\nexport interface HooksInvocation {\n readonly mode: 'hooks';\n readonly action: 'install' | 'remove';\n readonly harness?: string;\n}\n\n/**\n * The harnesses at one tier, as the HOOKS paragraph spells them — from\n * `HarnessProfile.tier`, never a second list. A tier no profile claims renders empty\n * rather than naming a harness that moved.\n *\n * The line breaks around these are still hand-placed, and deliberately: this body is\n * byte-pinned by `test/__snapshots__/cli-usage.help.txt`, its paragraph is wrapped by\n * hand at no single width, and a generic wrapper would rewrite every line of the help\n * to derive three lists. The *membership* is what drifted — a promoted harness stayed\n * under its old tier — and membership is what this derives.\n */\nfunction tierNames(tier: HarnessTier): string {\n const group = harnessesByTier().find((candidate) => candidate.tier === tier);\n return group === undefined ? '' : harnessNames(group.harnesses);\n}\n\nexport const hooksCommand: Subcommand<HooksInvocation, HooksInvocation> = {\n name: 'hooks',\n flags: ['harness'],\n refusal: `hooks is interactive; the wizard asks the rest.`,\n usage: {\n synopsis: ['hooks install [--harness <id>]', 'hooks remove [--harness <id>]'],\n section: {\n heading: 'HOOKS',\n body:\n ` ${CLI_NAME} hooks install wires the smelt guard into agent-harness hooks: a\\n` +\n ` PreToolUse size-guard that refuses oversized raw reads with the exact ${CLI_NAME}\\n` +\n ` replacement command (default on), \\`${CLI_NAME} stats\\` at session end (default\\n` +\n ` on), and an opening \\`${CLI_NAME} map\\` at session start (opt-in) — plus an\\n` +\n ` instruction-file snippet that teaches \\`${CLI_NAME} retrieve\\` after a deny.\\n` +\n ` Harnesses are tiered honestly: verified (${tierNames('verified')}), experimental\\n` +\n ` (${tierNames('experimental')} — schemas from the capability\\n` +\n ` matrix, not yet smoke-tested), advisory (${tierNames('advisory')} — instructions only,\\n` +\n ` nothing enforced). Same discipline as init: every file listed before a final\\n` +\n ` confirm, no existing file overwritten without a per-file yes, re-runs edit\\n` +\n ` toggles. ${CLI_NAME} hooks remove takes it back out. Guard settings live in\\n` +\n ` smelt.config.json (\"hooks\": {\"thresholdBytes\", \"enforcement\": \"deny\"|\"rewrite\"});\\n` +\n ` deny is the default — rewrite substitutes commands in-flight only where a\\n` +\n ` harness supports it, and never silently.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): HooksInvocation {\n const action = positionals[1];\n if (action !== 'install' && action !== 'remove') {\n throw new CliUsageError(\n `${CLI_NAME}: hooks needs an action — install or remove.\\n` +\n ` ${CLI_NAME} hooks install [--harness <id>]\\n` +\n ` ${CLI_NAME} hooks remove [--harness <id>]`,\n );\n }\n if (positionals.length > 2) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks ${action} takes no further arguments, got ` +\n `${positionals.slice(2).join(', ')}.`,\n );\n }\n // `--harness` is repeatable for setup; this verb wires one action per run, and a\n // second id would be a second install the user believed had happened.\n if (values.harness !== undefined && values.harness.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks takes one --harness per run — repeat the command for ` +\n `each harness.`,\n );\n }\n return {\n mode: 'hooks',\n action,\n ...(values.harness === undefined ? {} : { harness: values.harness[0] }),\n };\n },\n\n /** Nothing to merge: the wizard asks, and reads `smelt.config.json` itself. */\n resolve(invocation: HooksInvocation): HooksInvocation {\n return invocation;\n },\n\n /** Interactive like `init`, so it needs the same stream, and refuses without one. */\n async run(resolved: HooksInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: hooks ${resolved.action} is interactive, and this invocation has ` +\n `no interactive input stream. Run \\`${CLI_NAME} hooks ${resolved.action}\\` ` +\n `from a terminal.`,\n );\n }\n return await runHooks(resolved.action, resolved.harness, {\n input: io.initInput,\n output: (text) => io.stdout(colorize(text, io.color === true)),\n cwd: io.cwd ?? process.cwd(),\n version: io.version,\n });\n },\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/init.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;GASG;AAEH,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,WAAW,EAAE,UAAU,CAAC,cAAc,EAAE,cAAc,CA4ClE,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runInit } from '../init.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt init` — the setup wizard's front door. The wizard itself is `cli/init.ts`,\n * a pure function over an input/output pair; this file is only the verb.\n *\n * It owns no flags at all, and that is the whole design: the wizard asks one question\n * at a time and writes nothing until a final confirm, so a flag would be an answer\n * given twice. It also merges nothing — it reads `smelt.config.json` itself, with its\n * own tolerance, because a wizard you run to *fix* a malformed config must not refuse\n * to start because the config is malformed.\n */\n\n/** `smelt init` — no flags, no arguments, nothing but the word. */\nexport interface InitInvocation {\n readonly mode: 'init';\n readonly focus: readonly string[];\n readonly json: boolean;\n}\n\nexport const initCommand: Subcommand<InitInvocation, InitInvocation> = {\n name: 'init',\n flags: [],\n refusal: `init is interactive: it asks instead.`,\n usage: {\n synopsis: [],\n occasional: ['init'],\n section: {\n heading: 'CONFIG',\n body:\n ` ${CLI_NAME} init walks you through writing a smelt.config.json (and optional typed\\n` +\n ` stubs), one question at a time; nothing is written until a final confirm. Runs read\\n` +\n ` the nearest smelt.config.json, walking up from the working directory, for DEFAULTS\\n` +\n ` only — budget, strategy, store. An explicit flag always wins, and a malformed config\\n` +\n ` is a usage error, never silently ignored.`,\n },\n },\n\n parse(_values: FlagValues, positionals: readonly string[]): InitInvocation {\n // `smelt init` is a subcommand, so a file literally named `init` needs `./init`.\n if (positionals.length > 1) {\n throw new CliUsageError(`${CLI_NAME}: init takes no further arguments.`);\n }\n return { mode: 'init', focus: [], json: false };\n },\n\n /** Nothing to merge: every answer is asked for, and the config is read by the wizard. */\n resolve(invocation: InitInvocation): InitInvocation {\n return invocation;\n },\n\n async run(_resolved: InitInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: init is interactive, and this invocation has no interactive ` +\n `input stream. Run \\`${CLI_NAME} init\\` from a terminal.`,\n );\n }\n return await runInit({\n input: io.initInput,\n output: io.stdout,\n cwd: io.cwd ?? process.cwd(),\n });\n },\n};\n"]}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/init.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;GASG;AAEH,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,WAAW,EAAE,UAAU,CAAC,cAAc,EAAE,cAAc,CA4ClE,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runInit } from '../init.ts';\nimport { colorize } from '../lava.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt init` — the setup wizard's front door. The wizard itself is `cli/init.ts`,\n * a pure function over an input/output pair; this file is only the verb.\n *\n * It owns no flags at all, and that is the whole design: the wizard asks one question\n * at a time and writes nothing until a final confirm, so a flag would be an answer\n * given twice. It also merges nothing — it reads `smelt.config.json` itself, with its\n * own tolerance, because a wizard you run to *fix* a malformed config must not refuse\n * to start because the config is malformed.\n */\n\n/** `smelt init` — no flags, no arguments, nothing but the word. */\nexport interface InitInvocation {\n readonly mode: 'init';\n readonly focus: readonly string[];\n readonly json: boolean;\n}\n\nexport const initCommand: Subcommand<InitInvocation, InitInvocation> = {\n name: 'init',\n flags: [],\n refusal: `init is interactive: it asks instead.`,\n usage: {\n synopsis: [],\n occasional: ['init'],\n section: {\n heading: 'CONFIG',\n body:\n ` ${CLI_NAME} init walks you through writing a smelt.config.json (and optional typed\\n` +\n ` stubs), one question at a time; nothing is written until a final confirm. Runs read\\n` +\n ` the nearest smelt.config.json, walking up from the working directory, for DEFAULTS\\n` +\n ` only — budget, strategy, store. An explicit flag always wins, and a malformed config\\n` +\n ` is a usage error, never silently ignored.`,\n },\n },\n\n parse(_values: FlagValues, positionals: readonly string[]): InitInvocation {\n // `smelt init` is a subcommand, so a file literally named `init` needs `./init`.\n if (positionals.length > 1) {\n throw new CliUsageError(`${CLI_NAME}: init takes no further arguments.`);\n }\n return { mode: 'init', focus: [], json: false };\n },\n\n /** Nothing to merge: every answer is asked for, and the config is read by the wizard. */\n resolve(invocation: InitInvocation): InitInvocation {\n return invocation;\n },\n\n async run(_resolved: InitInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: init is interactive, and this invocation has no interactive ` +\n `input stream. Run \\`${CLI_NAME} init\\` from a terminal.`,\n );\n }\n return await runInit({\n input: io.initInput,\n output: (text) => io.stdout(colorize(text, io.color === true)),\n cwd: io.cwd ?? process.cwd(),\n });\n },\n};\n"]}
@@ -1,6 +1,7 @@
1
1
  import process from 'node:process';
2
2
  import { CliUsageError } from '../../errors.js';
3
3
  import { runInit } from '../init.js';
4
+ import { colorize } from '../lava.js';
4
5
  import { CLI_NAME } from '../shell.js';
5
6
  export const initCommand = {
6
7
  name: 'init',
@@ -36,7 +37,7 @@ export const initCommand = {
36
37
  }
37
38
  return await runInit({
38
39
  input: io.initInput,
39
- output: io.stdout,
40
+ output: (text) => io.stdout(colorize(text, io.color === true)),
40
41
  cwd: io.cwd ?? process.cwd(),
41
42
  });
42
43
  },
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/cli/subcommands/init.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAwBvC,MAAM,CAAC,MAAM,WAAW,GAA+C;IACrE,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,EAAE;IACT,OAAO,EAAE,uCAAuC;IAChD,KAAK,EAAE;QACL,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,CAAC,MAAM,CAAC;QACpB,OAAO,EAAE;YACP,OAAO,EAAE,QAAQ;YACjB,IAAI,EACF,KAAK,QAAQ,2EAA2E;gBACxF,yFAAyF;gBACzF,wFAAwF;gBACxF,0FAA0F;gBAC1F,6CAA6C;SAChD;KACF;IAED,KAAK,CAAC,OAAmB,EAAE,WAA8B;QACvD,iFAAiF;QACjF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,aAAa,CAAC,GAAG,QAAQ,oCAAoC,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAClD,CAAC;IAED,yFAAyF;IACzF,OAAO,CAAC,UAA0B;QAChC,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,SAAyB,EAAE,EAAS;QAC5C,IAAI,EAAE,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,gEAAgE;gBACzE,uBAAuB,QAAQ,0BAA0B,CAC5D,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,OAAO,CAAC;YACnB,KAAK,EAAE,EAAE,CAAC,SAAS;YACnB,MAAM,EAAE,EAAE,CAAC,MAAM;YACjB,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;CACF,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runInit } from '../init.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt init` — the setup wizard's front door. The wizard itself is `cli/init.ts`,\n * a pure function over an input/output pair; this file is only the verb.\n *\n * It owns no flags at all, and that is the whole design: the wizard asks one question\n * at a time and writes nothing until a final confirm, so a flag would be an answer\n * given twice. It also merges nothing — it reads `smelt.config.json` itself, with its\n * own tolerance, because a wizard you run to *fix* a malformed config must not refuse\n * to start because the config is malformed.\n */\n\n/** `smelt init` — no flags, no arguments, nothing but the word. */\nexport interface InitInvocation {\n readonly mode: 'init';\n readonly focus: readonly string[];\n readonly json: boolean;\n}\n\nexport const initCommand: Subcommand<InitInvocation, InitInvocation> = {\n name: 'init',\n flags: [],\n refusal: `init is interactive: it asks instead.`,\n usage: {\n synopsis: [],\n occasional: ['init'],\n section: {\n heading: 'CONFIG',\n body:\n ` ${CLI_NAME} init walks you through writing a smelt.config.json (and optional typed\\n` +\n ` stubs), one question at a time; nothing is written until a final confirm. Runs read\\n` +\n ` the nearest smelt.config.json, walking up from the working directory, for DEFAULTS\\n` +\n ` only — budget, strategy, store. An explicit flag always wins, and a malformed config\\n` +\n ` is a usage error, never silently ignored.`,\n },\n },\n\n parse(_values: FlagValues, positionals: readonly string[]): InitInvocation {\n // `smelt init` is a subcommand, so a file literally named `init` needs `./init`.\n if (positionals.length > 1) {\n throw new CliUsageError(`${CLI_NAME}: init takes no further arguments.`);\n }\n return { mode: 'init', focus: [], json: false };\n },\n\n /** Nothing to merge: every answer is asked for, and the config is read by the wizard. */\n resolve(invocation: InitInvocation): InitInvocation {\n return invocation;\n },\n\n async run(_resolved: InitInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: init is interactive, and this invocation has no interactive ` +\n `input stream. Run \\`${CLI_NAME} init\\` from a terminal.`,\n );\n }\n return await runInit({\n input: io.initInput,\n output: io.stdout,\n cwd: io.cwd ?? process.cwd(),\n });\n },\n};\n"]}
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/cli/subcommands/init.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAwBvC,MAAM,CAAC,MAAM,WAAW,GAA+C;IACrE,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,EAAE;IACT,OAAO,EAAE,uCAAuC;IAChD,KAAK,EAAE;QACL,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,CAAC,MAAM,CAAC;QACpB,OAAO,EAAE;YACP,OAAO,EAAE,QAAQ;YACjB,IAAI,EACF,KAAK,QAAQ,2EAA2E;gBACxF,yFAAyF;gBACzF,wFAAwF;gBACxF,0FAA0F;gBAC1F,6CAA6C;SAChD;KACF;IAED,KAAK,CAAC,OAAmB,EAAE,WAA8B;QACvD,iFAAiF;QACjF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,aAAa,CAAC,GAAG,QAAQ,oCAAoC,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAClD,CAAC;IAED,yFAAyF;IACzF,OAAO,CAAC,UAA0B;QAChC,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,SAAyB,EAAE,EAAS;QAC5C,IAAI,EAAE,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,gEAAgE;gBACzE,uBAAuB,QAAQ,0BAA0B,CAC5D,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,OAAO,CAAC;YACnB,KAAK,EAAE,EAAE,CAAC,SAAS;YACnB,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC;YAC9D,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;CACF,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { runInit } from '../init.ts';\nimport { colorize } from '../lava.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt init` — the setup wizard's front door. The wizard itself is `cli/init.ts`,\n * a pure function over an input/output pair; this file is only the verb.\n *\n * It owns no flags at all, and that is the whole design: the wizard asks one question\n * at a time and writes nothing until a final confirm, so a flag would be an answer\n * given twice. It also merges nothing — it reads `smelt.config.json` itself, with its\n * own tolerance, because a wizard you run to *fix* a malformed config must not refuse\n * to start because the config is malformed.\n */\n\n/** `smelt init` — no flags, no arguments, nothing but the word. */\nexport interface InitInvocation {\n readonly mode: 'init';\n readonly focus: readonly string[];\n readonly json: boolean;\n}\n\nexport const initCommand: Subcommand<InitInvocation, InitInvocation> = {\n name: 'init',\n flags: [],\n refusal: `init is interactive: it asks instead.`,\n usage: {\n synopsis: [],\n occasional: ['init'],\n section: {\n heading: 'CONFIG',\n body:\n ` ${CLI_NAME} init walks you through writing a smelt.config.json (and optional typed\\n` +\n ` stubs), one question at a time; nothing is written until a final confirm. Runs read\\n` +\n ` the nearest smelt.config.json, walking up from the working directory, for DEFAULTS\\n` +\n ` only — budget, strategy, store. An explicit flag always wins, and a malformed config\\n` +\n ` is a usage error, never silently ignored.`,\n },\n },\n\n parse(_values: FlagValues, positionals: readonly string[]): InitInvocation {\n // `smelt init` is a subcommand, so a file literally named `init` needs `./init`.\n if (positionals.length > 1) {\n throw new CliUsageError(`${CLI_NAME}: init takes no further arguments.`);\n }\n return { mode: 'init', focus: [], json: false };\n },\n\n /** Nothing to merge: every answer is asked for, and the config is read by the wizard. */\n resolve(invocation: InitInvocation): InitInvocation {\n return invocation;\n },\n\n async run(_resolved: InitInvocation, io: CliIo): Promise<number> {\n if (io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: init is interactive, and this invocation has no interactive ` +\n `input stream. Run \\`${CLI_NAME} init\\` from a terminal.`,\n );\n }\n return await runInit({\n input: io.initInput,\n output: (text) => io.stdout(colorize(text, io.color === true)),\n cwd: io.cwd ?? process.cwd(),\n });\n },\n};\n"]}
@@ -1,9 +1,12 @@
1
+ import type { AgentsInvocation } from './agents.ts';
2
+ import type { DoctorInvocation } from './doctor.ts';
1
3
  import type { FlagName, FlagValues } from './flags.ts';
2
4
  import type { HooksInvocation } from './hooks.ts';
3
5
  import type { InitInvocation } from './init.ts';
4
6
  import type { MapInvocation } from './map.ts';
5
7
  import type { RetrieveInvocation } from './retrieve.ts';
6
8
  import type { SmeltInvocation } from './smelt.ts';
9
+ import type { SetupInvocation } from './setup.ts';
7
10
  import type { StatsInvocation } from './stats.ts';
8
11
  import type { Subcommand, Verb } from './subcommand.ts';
9
12
  /**
@@ -21,7 +24,7 @@ import type { Subcommand, Verb } from './subcommand.ts';
21
24
  */
22
25
  export declare const SUBCOMMANDS: Readonly<Record<Verb, AnySubcommand>>;
23
26
  /** Everything `parseSmeltArgs` can return. Narrow on `mode`. */
24
- export type CliInvocation = SmeltInvocation | InitInvocation | MapInvocation | RetrieveInvocation | StatsInvocation | HooksInvocation;
27
+ export type CliInvocation = SmeltInvocation | InitInvocation | MapInvocation | RetrieveInvocation | StatsInvocation | HooksInvocation | AgentsInvocation | SetupInvocation | DoctorInvocation;
25
28
  /**
26
29
  * One registry entry, with its verb's own invocation and resolved types erased.
27
30
  *
@@ -1 +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"]}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/registry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AAEjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAExD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,CAU7D,CAAC;AAEF,gEAAgE;AAChE,MAAM,MAAM,aAAa,GACrB,eAAe,GACf,cAAc,GACd,aAAa,GACb,kBAAkB,GAClB,eAAe,GACf,eAAe,GACf,gBAAgB,GAChB,eAAe,GACf,gBAAgB,CAAC;AAErB;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;AAE/D,6EAA6E;AAC7E,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAA+B,CAAC;AAEpF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,IAAI,EAEtC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,aAAa,CAI3E;AAqBD,6DAA6D;AAC7D,wBAAgB,UAAU,CACxB,IAAI,EAAE,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,GACvD,aAAa,CAEf;AAYD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,aAAa,EAAE,CAEjE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAYnF","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { CLI_NAME } from '../shell.ts';\n\nimport { agentsCommand } from './agents.ts';\nimport type { AgentsInvocation } from './agents.ts';\nimport { doctorCommand } from './doctor.ts';\nimport type { DoctorInvocation } from './doctor.ts';\nimport { flagList, VERB_FLAGS } from './flags.ts';\nimport type { FlagName, FlagValues, VerbFlag } from './flags.ts';\nimport { hooksCommand } from './hooks.ts';\nimport type { HooksInvocation } from './hooks.ts';\nimport { initCommand } from './init.ts';\nimport type { InitInvocation } from './init.ts';\nimport { mapCommand } from './map.ts';\nimport type { MapInvocation } from './map.ts';\nimport { retrieveCommand } from './retrieve.ts';\nimport type { RetrieveInvocation } from './retrieve.ts';\nimport { smeltCommand } from './smelt.ts';\nimport type { SmeltInvocation } from './smelt.ts';\nimport { setupCommand } from './setup.ts';\nimport type { SetupInvocation } from './setup.ts';\nimport { statsCommand } from './stats.ts';\nimport type { StatsInvocation } from './stats.ts';\nimport { DEFAULT_VERB } from './subcommand.ts';\nimport type { Subcommand, Verb } from './subcommand.ts';\n\n/**\n * The registry — every verb `smelt` answers to, one {@link Subcommand} each.\n *\n * `Record<Verb, Subcommand>` on purpose, exactly like `LANGUAGE_PROFILES` and\n * `HARNESS_PROFILES`: adding a `Verb` in `subcommand.ts` without writing its file is a\n * compile error, so the verb list and the facts cannot drift. Every derived view — the\n * USAGE block, the help's sections, the `map only.` prefixes in OPTIONS, and above all\n * the flag-ownership refusal below — is computed from this object, never written twice.\n *\n * Key order is meaningful: it is the order every rendered list uses (USAGE, the help\n * sections, the owners named in a refusal), so keep it stable and append new verbs at\n * the end.\n */\nexport const SUBCOMMANDS: Readonly<Record<Verb, AnySubcommand>> = {\n smelt: smeltCommand,\n init: initCommand,\n map: mapCommand,\n retrieve: retrieveCommand,\n stats: statsCommand,\n hooks: hooksCommand,\n agents: agentsCommand,\n setup: setupCommand,\n doctor: doctorCommand,\n};\n\n/** Everything `parseSmeltArgs` can return. Narrow on `mode`. */\nexport type CliInvocation =\n | SmeltInvocation\n | InitInvocation\n | MapInvocation\n | RetrieveInvocation\n | StatsInvocation\n | HooksInvocation\n | AgentsInvocation\n | SetupInvocation\n | DoctorInvocation;\n\n/**\n * One registry entry, with its verb's own invocation and resolved types erased.\n *\n * The erasure is what lets six differently-typed commands live in one `Record`, and it\n * is safe because the two ends are never crossed: `runCli` hands a command exactly the\n * invocation that command's own `parse` produced (`verbFor` maps each `mode` back to\n * the verb that minted it), and the resolved value never leaves the pair of calls that\n * makes and consumes it.\n */\nexport type AnySubcommand = Subcommand<CliInvocation, unknown>;\n\n/** Every command, in registry order. The list every rendered block walks. */\nexport const SUBCOMMAND_LIST: readonly AnySubcommand[] = Object.values(SUBCOMMANDS);\n\n/**\n * The verbs with a word on the command line — everything but the default one.\n *\n * The default verb is excluded deliberately: `smelt smelt` is a file named `smelt`,\n * not a recursive invocation, the same way `smelt map` as a *file* needs `./map`.\n */\nexport const NAMED_VERBS: readonly Verb[] = SUBCOMMAND_LIST.map((command) => command.name).filter(\n (name) => name !== DEFAULT_VERB,\n);\n\n/**\n * Which command a command line selects: `positionals[0]` when it names a verb, the\n * default verb otherwise. The whole of subcommand dispatch, in one lookup.\n */\nexport function subcommandFor(positionals: readonly string[]): AnySubcommand {\n const first = positionals[0];\n const named = NAMED_VERBS.find((verb) => verb === first);\n return SUBCOMMANDS[named ?? DEFAULT_VERB];\n}\n\n/**\n * Which verb minted an invocation. Total over the modes `parseSmeltArgs` can return\n * for a verb, so a new mode without a home is a compile error — `'reconstruct'` maps\n * to the default verb because `--reconstruct` is that verb's second job, not a\n * seventh command.\n */\nconst VERB_BY_MODE = {\n smelt: 'smelt',\n reconstruct: 'smelt',\n init: 'init',\n map: 'map',\n retrieve: 'retrieve',\n stats: 'stats',\n hooks: 'hooks',\n agents: 'agents',\n setup: 'setup',\n doctor: 'doctor',\n} as const satisfies Record<Exclude<CliInvocation['mode'], 'help' | 'version'>, Verb>;\n\n/** The command that produced an invocation, for dispatch. */\nexport function commandFor(\n mode: Exclude<CliInvocation['mode'], 'help' | 'version'>,\n): AnySubcommand {\n return SUBCOMMANDS[VERB_BY_MODE[mode]];\n}\n\n/** How a command is named in prose. The default verb has no word to name it by. */\nfunction label(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : command.name;\n}\n\n/** How a command is named as the *owner* of a flag: the form you would type. */\nfunction ownerLabel(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : `\\`${CLI_NAME} ${command.name}\\``;\n}\n\n/**\n * The commands that own a flag, in registry order — empty for the two global flags,\n * which no verb owns. A flag with exactly one owner has an unambiguous home worth\n * naming in a refusal (and worth prefixing its OPTIONS entry with); a flag several\n * verbs share (`--json`, `--budget`, `--focus`) has none, and pointing at one of them\n * would be a guess.\n */\nexport function ownersOf(flag: FlagName): readonly AnySubcommand[] {\n return SUBCOMMAND_LIST.filter((command) => (command.flags as readonly string[]).includes(flag));\n}\n\n/**\n * Refuse every flag the chosen verb does not own — the one message that replaced five\n * hand-written ones.\n *\n * Before this existed, each verb refused each other verb's flags in prose, so the\n * refusals were O(verbs × flags) sentences kept in sync by hand and an eleventh flag\n * edited five of them. Now ownership is declared once per verb and the message is\n * generated from three derived facts, in the order a reader needs them:\n *\n * 1. **what this verb takes**, and what it got instead — the offending flag, named;\n * 2. **where the flag does belong**, when exactly one verb owns it, listed as that\n * owner's *exclusively* owned flags (so `--ignore` here still reads \"--ignore and\n * --cache belong to `smelt map`\", exactly as the hand-written message did);\n * 3. **why not here** — the verb's own `refusal` sentence, the half of the old\n * messages worth keeping, and the only half a verb still writes.\n *\n * Every refusal stays a {@link CliUsageError}, so every one of them still exits 2.\n *\n * @throws {CliUsageError} naming the offending flag, this verb, and the flag's owner.\n */\nexport function refuseForeignFlags(command: AnySubcommand, values: FlagValues): void {\n const foreign = VERB_FLAGS.filter(\n (flag) => values[flag] !== undefined && !command.flags.includes(flag),\n );\n if (foreign.length === 0) return;\n\n const takes =\n command.flags.length === 0\n ? `${label(command)} takes no flags (got ${flagList(foreign)}).`\n : `${label(command)} takes only ${flagList(command.flags)} (got ${flagList(foreign)}).`;\n\n throw new CliUsageError(`${CLI_NAME}: ${takes}${redirects(foreign)} ${command.refusal}`);\n}\n\n/**\n * ` --ignore and --cache belong to \\`smelt map\\`.` — one clause per single-owner verb.\n *\n * A clause names only the flags its owner owns **alone**, never the ones it shares.\n * `ownersOf(flag).length === 1` is the same test that decided the owner deserved a\n * clause at all, applied to the whole clause: a shared flag (`--budget`, `--focus`,\n * `--json`) has no single home, so naming it here would assert an ownership that the\n * OPTIONS block — which prefixes `map only.` by the same rule — correctly denies.\n */\nfunction redirects(foreign: readonly VerbFlag[]): string {\n const owners = SUBCOMMAND_LIST.filter((owner) =>\n foreign.some((flag) => {\n const claimants = ownersOf(flag);\n return claimants.length === 1 && claimants[0] === owner;\n }),\n );\n return owners\n .map((owner) => {\n const elsewhere = owner.flags.filter((flag) => ownersOf(flag).length === 1);\n const verb = elsewhere.length === 1 ? 'belongs' : 'belong';\n return ` ${flagList(elsewhere)} ${verb} to ${ownerLabel(owner)}.`;\n })\n .join('');\n}\n"]}
@@ -1,11 +1,14 @@
1
1
  import { CliUsageError } from '../../errors.js';
2
2
  import { CLI_NAME } from '../shell.js';
3
+ import { agentsCommand } from './agents.js';
4
+ import { doctorCommand } from './doctor.js';
3
5
  import { flagList, VERB_FLAGS } from './flags.js';
4
6
  import { hooksCommand } from './hooks.js';
5
7
  import { initCommand } from './init.js';
6
8
  import { mapCommand } from './map.js';
7
9
  import { retrieveCommand } from './retrieve.js';
8
10
  import { smeltCommand } from './smelt.js';
11
+ import { setupCommand } from './setup.js';
9
12
  import { statsCommand } from './stats.js';
10
13
  import { DEFAULT_VERB } from './subcommand.js';
11
14
  /**
@@ -28,6 +31,9 @@ export const SUBCOMMANDS = {
28
31
  retrieve: retrieveCommand,
29
32
  stats: statsCommand,
30
33
  hooks: hooksCommand,
34
+ agents: agentsCommand,
35
+ setup: setupCommand,
36
+ doctor: doctorCommand,
31
37
  };
32
38
  /** Every command, in registry order. The list every rendered block walks. */
33
39
  export const SUBCOMMAND_LIST = Object.values(SUBCOMMANDS);
@@ -61,6 +67,9 @@ const VERB_BY_MODE = {
61
67
  retrieve: 'retrieve',
62
68
  stats: 'stats',
63
69
  hooks: 'hooks',
70
+ agents: 'agents',
71
+ setup: 'setup',
72
+ doctor: 'doctor',
64
73
  };
65
74
  /** The command that produced an invocation, for dispatch. */
66
75
  export function commandFor(mode) {
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/cli/subcommands/registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { CLI_NAME } from '../shell.ts';\n\nimport { 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"]}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/cli/subcommands/registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { CLI_NAME } from '../shell.ts';\n\nimport { agentsCommand } from './agents.ts';\nimport type { AgentsInvocation } from './agents.ts';\nimport { doctorCommand } from './doctor.ts';\nimport type { DoctorInvocation } from './doctor.ts';\nimport { flagList, VERB_FLAGS } from './flags.ts';\nimport type { FlagName, FlagValues, VerbFlag } from './flags.ts';\nimport { hooksCommand } from './hooks.ts';\nimport type { HooksInvocation } from './hooks.ts';\nimport { initCommand } from './init.ts';\nimport type { InitInvocation } from './init.ts';\nimport { mapCommand } from './map.ts';\nimport type { MapInvocation } from './map.ts';\nimport { retrieveCommand } from './retrieve.ts';\nimport type { RetrieveInvocation } from './retrieve.ts';\nimport { smeltCommand } from './smelt.ts';\nimport type { SmeltInvocation } from './smelt.ts';\nimport { setupCommand } from './setup.ts';\nimport type { SetupInvocation } from './setup.ts';\nimport { statsCommand } from './stats.ts';\nimport type { StatsInvocation } from './stats.ts';\nimport { DEFAULT_VERB } from './subcommand.ts';\nimport type { Subcommand, Verb } from './subcommand.ts';\n\n/**\n * The registry — every verb `smelt` answers to, one {@link Subcommand} each.\n *\n * `Record<Verb, Subcommand>` on purpose, exactly like `LANGUAGE_PROFILES` and\n * `HARNESS_PROFILES`: adding a `Verb` in `subcommand.ts` without writing its file is a\n * compile error, so the verb list and the facts cannot drift. Every derived view — the\n * USAGE block, the help's sections, the `map only.` prefixes in OPTIONS, and above all\n * the flag-ownership refusal below — is computed from this object, never written twice.\n *\n * Key order is meaningful: it is the order every rendered list uses (USAGE, the help\n * sections, the owners named in a refusal), so keep it stable and append new verbs at\n * the end.\n */\nexport const SUBCOMMANDS: Readonly<Record<Verb, AnySubcommand>> = {\n smelt: smeltCommand,\n init: initCommand,\n map: mapCommand,\n retrieve: retrieveCommand,\n stats: statsCommand,\n hooks: hooksCommand,\n agents: agentsCommand,\n setup: setupCommand,\n doctor: doctorCommand,\n};\n\n/** Everything `parseSmeltArgs` can return. Narrow on `mode`. */\nexport type CliInvocation =\n | SmeltInvocation\n | InitInvocation\n | MapInvocation\n | RetrieveInvocation\n | StatsInvocation\n | HooksInvocation\n | AgentsInvocation\n | SetupInvocation\n | DoctorInvocation;\n\n/**\n * One registry entry, with its verb's own invocation and resolved types erased.\n *\n * The erasure is what lets six differently-typed commands live in one `Record`, and it\n * is safe because the two ends are never crossed: `runCli` hands a command exactly the\n * invocation that command's own `parse` produced (`verbFor` maps each `mode` back to\n * the verb that minted it), and the resolved value never leaves the pair of calls that\n * makes and consumes it.\n */\nexport type AnySubcommand = Subcommand<CliInvocation, unknown>;\n\n/** Every command, in registry order. The list every rendered block walks. */\nexport const SUBCOMMAND_LIST: readonly AnySubcommand[] = Object.values(SUBCOMMANDS);\n\n/**\n * The verbs with a word on the command line — everything but the default one.\n *\n * The default verb is excluded deliberately: `smelt smelt` is a file named `smelt`,\n * not a recursive invocation, the same way `smelt map` as a *file* needs `./map`.\n */\nexport const NAMED_VERBS: readonly Verb[] = SUBCOMMAND_LIST.map((command) => command.name).filter(\n (name) => name !== DEFAULT_VERB,\n);\n\n/**\n * Which command a command line selects: `positionals[0]` when it names a verb, the\n * default verb otherwise. The whole of subcommand dispatch, in one lookup.\n */\nexport function subcommandFor(positionals: readonly string[]): AnySubcommand {\n const first = positionals[0];\n const named = NAMED_VERBS.find((verb) => verb === first);\n return SUBCOMMANDS[named ?? DEFAULT_VERB];\n}\n\n/**\n * Which verb minted an invocation. Total over the modes `parseSmeltArgs` can return\n * for a verb, so a new mode without a home is a compile error — `'reconstruct'` maps\n * to the default verb because `--reconstruct` is that verb's second job, not a\n * seventh command.\n */\nconst VERB_BY_MODE = {\n smelt: 'smelt',\n reconstruct: 'smelt',\n init: 'init',\n map: 'map',\n retrieve: 'retrieve',\n stats: 'stats',\n hooks: 'hooks',\n agents: 'agents',\n setup: 'setup',\n doctor: 'doctor',\n} as const satisfies Record<Exclude<CliInvocation['mode'], 'help' | 'version'>, Verb>;\n\n/** The command that produced an invocation, for dispatch. */\nexport function commandFor(\n mode: Exclude<CliInvocation['mode'], 'help' | 'version'>,\n): AnySubcommand {\n return SUBCOMMANDS[VERB_BY_MODE[mode]];\n}\n\n/** How a command is named in prose. The default verb has no word to name it by. */\nfunction label(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : command.name;\n}\n\n/** How a command is named as the *owner* of a flag: the form you would type. */\nfunction ownerLabel(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : `\\`${CLI_NAME} ${command.name}\\``;\n}\n\n/**\n * The commands that own a flag, in registry order — empty for the two global flags,\n * which no verb owns. A flag with exactly one owner has an unambiguous home worth\n * naming in a refusal (and worth prefixing its OPTIONS entry with); a flag several\n * verbs share (`--json`, `--budget`, `--focus`) has none, and pointing at one of them\n * would be a guess.\n */\nexport function ownersOf(flag: FlagName): readonly AnySubcommand[] {\n return SUBCOMMAND_LIST.filter((command) => (command.flags as readonly string[]).includes(flag));\n}\n\n/**\n * Refuse every flag the chosen verb does not own — the one message that replaced five\n * hand-written ones.\n *\n * Before this existed, each verb refused each other verb's flags in prose, so the\n * refusals were O(verbs × flags) sentences kept in sync by hand and an eleventh flag\n * edited five of them. Now ownership is declared once per verb and the message is\n * generated from three derived facts, in the order a reader needs them:\n *\n * 1. **what this verb takes**, and what it got instead — the offending flag, named;\n * 2. **where the flag does belong**, when exactly one verb owns it, listed as that\n * owner's *exclusively* owned flags (so `--ignore` here still reads \"--ignore and\n * --cache belong to `smelt map`\", exactly as the hand-written message did);\n * 3. **why not here** — the verb's own `refusal` sentence, the half of the old\n * messages worth keeping, and the only half a verb still writes.\n *\n * Every refusal stays a {@link CliUsageError}, so every one of them still exits 2.\n *\n * @throws {CliUsageError} naming the offending flag, this verb, and the flag's owner.\n */\nexport function refuseForeignFlags(command: AnySubcommand, values: FlagValues): void {\n const foreign = VERB_FLAGS.filter(\n (flag) => values[flag] !== undefined && !command.flags.includes(flag),\n );\n if (foreign.length === 0) return;\n\n const takes =\n command.flags.length === 0\n ? `${label(command)} takes no flags (got ${flagList(foreign)}).`\n : `${label(command)} takes only ${flagList(command.flags)} (got ${flagList(foreign)}).`;\n\n throw new CliUsageError(`${CLI_NAME}: ${takes}${redirects(foreign)} ${command.refusal}`);\n}\n\n/**\n * ` --ignore and --cache belong to \\`smelt map\\`.` — one clause per single-owner verb.\n *\n * A clause names only the flags its owner owns **alone**, never the ones it shares.\n * `ownersOf(flag).length === 1` is the same test that decided the owner deserved a\n * clause at all, applied to the whole clause: a shared flag (`--budget`, `--focus`,\n * `--json`) has no single home, so naming it here would assert an ownership that the\n * OPTIONS block — which prefixes `map only.` by the same rule — correctly denies.\n */\nfunction redirects(foreign: readonly VerbFlag[]): string {\n const owners = SUBCOMMAND_LIST.filter((owner) =>\n foreign.some((flag) => {\n const claimants = ownersOf(flag);\n return claimants.length === 1 && claimants[0] === owner;\n }),\n );\n return owners\n .map((owner) => {\n const elsewhere = owner.flags.filter((flag) => ownersOf(flag).length === 1);\n const verb = elsewhere.length === 1 ? 'belongs' : 'belong';\n return ` ${flagList(elsewhere)} ${verb} to ${ownerLabel(owner)}.`;\n })\n .join('');\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import type { Subcommand } from './subcommand.ts';
2
+ /**
3
+ * `smelt setup` — the one-command front door for the whole recipe. The flow itself is
4
+ * `cli/setup.ts`, a pure function over an injected input/output pair; this file is
5
+ * only the verb.
6
+ *
7
+ * Interactive from a terminal, like `init` and `hooks`; for an agent, the flags are
8
+ * the whole interface — `--yes` answers everything from the recipe, `--harness`
9
+ * (repeatable) picks the harnesses, `--no-mcp` skips the registration step, and
10
+ * `--json` (with `--yes`) turns the run into a machine-readable receipt. The refusal
11
+ * below is how an agent with no TTY learns that interface, which is the same trick
12
+ * `init`'s refusal uses.
13
+ */
14
+ /** `smelt setup [--harness <id>]... [--yes] [--no-mcp] [--json]` — parsed. */
15
+ export interface SetupInvocation {
16
+ readonly mode: 'setup';
17
+ readonly harnessIds: readonly string[];
18
+ readonly yes: boolean;
19
+ readonly noMcp: boolean;
20
+ readonly json: boolean;
21
+ }
22
+ export declare const setupCommand: Subcommand<SetupInvocation, SetupInvocation>;
23
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/setup.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;;;GAWG;AAEH,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,eAAe,EAAE,eAAe,CA2FrE,CAAC","sourcesContent":["import process from 'node:process';\n\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESSES, harnessById } from '../../harness/registry.ts';\nimport { SETUP_RECIPE } from '../../setup/recipe.ts';\nimport { runSetup } from '../setup.ts';\nimport { colorize } from '../lava.ts';\nimport { CLI_NAME } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { Subcommand } from './subcommand.ts';\n\n/**\n * `smelt setup` — the one-command front door for the whole recipe. The flow itself is\n * `cli/setup.ts`, a pure function over an injected input/output pair; this file is\n * only the verb.\n *\n * Interactive from a terminal, like `init` and `hooks`; for an agent, the flags are\n * the whole interface — `--yes` answers everything from the recipe, `--harness`\n * (repeatable) picks the harnesses, `--no-mcp` skips the registration step, and\n * `--json` (with `--yes`) turns the run into a machine-readable receipt. The refusal\n * below is how an agent with no TTY learns that interface, which is the same trick\n * `init`'s refusal uses.\n */\n\n/** `smelt setup [--harness <id>]... [--yes] [--no-mcp] [--json]` — parsed. */\nexport interface SetupInvocation {\n readonly mode: 'setup';\n readonly harnessIds: readonly string[];\n readonly yes: boolean;\n readonly noMcp: boolean;\n readonly json: boolean;\n}\n\nexport const setupCommand: Subcommand<SetupInvocation, SetupInvocation> = {\n name: 'setup',\n flags: ['harness', 'yes', 'no-mcp', 'json'],\n refusal: `setup applies the recipe; answer it with --yes (and --harness, --no-mcp, --json) or let it ask.`,\n usage: {\n synopsis: [],\n occasional: ['setup [--harness <id>]... [--yes] [--no-mcp] [--json]'],\n section: {\n heading: 'SETUP',\n body:\n ` ${CLI_NAME} setup applies the whole recipe in one command: smelt.config.json, the\\n` +\n ` hooks preset for the harnesses you name, the MCP registration step, and a real\\n` +\n ` smelt → retrieve round trip to prove the loop. Interactive from a terminal; for\\n` +\n ` an agent, answer everything up front:\\n\\n` +\n ` ${CLI_NAME} setup --yes [--harness <id>]... [--no-mcp] [--json]\\n\\n` +\n ` The defaults are the recipe's: budget ${SETUP_RECIPE.recommendedBudgetBytes} bytes\\n` +\n ` (written only when the config carries none), a directory store at\\n` +\n ` ${SETUP_RECIPE.store.defaultDir} (only when the config carries none). Existing\\n` +\n ` files are never overwritten — they are skipped with a note; hooks install edits\\n` +\n ` them, and it asks per file. Re-running on a current machine writes nothing and\\n` +\n ` exits 0. --json prints a receipt: every file, every check, the exit's meaning.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): SetupInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: setup takes no further arguments, got ` +\n `${positionals.slice(1).join(', ')}.`,\n );\n }\n const harnessIds = (values.harness ?? []).map((id) => {\n if (harnessById(id) === undefined) {\n throw new CliUsageError(\n `${CLI_NAME} setup: unknown harness \"${id}\". ` +\n `Known: ${HARNESSES.map((harness) => harness.id).join(', ')}.`,\n );\n }\n return id;\n });\n const yes = values.yes === true;\n const json = values.json === true;\n if (json && !yes) {\n throw new CliUsageError(\n `${CLI_NAME}: --json prints a machine receipt — pair it with --yes. ` +\n `The interactive flow's output is for humans.`,\n );\n }\n return {\n mode: 'setup',\n harnessIds,\n yes,\n noMcp: values['no-mcp'] === true,\n json,\n };\n },\n\n /**\n * Nothing to merge: the flow reads `smelt.config.json` itself. A *missing* config\n * is the thing setup writes; a *malformed* one is a loud refusal — doctor names it,\n * the user fixes it, and setup never guesses around bytes it cannot parse.\n */\n resolve(invocation: SetupInvocation): SetupInvocation {\n return invocation;\n },\n\n /**\n * Interactive unless `--yes` answered everything, so it needs the wizard stream —\n * and the refusal below is the agent-facing interface documentation.\n */\n async run(resolved: SetupInvocation, io: CliIo): Promise<number> {\n if (!resolved.yes && io.initInput === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: setup is interactive unless you answer it up front. ` +\n `Non-interactive:\\n` +\n ` ${CLI_NAME} setup --yes [--harness <id>]... [--no-mcp] [--json]`,\n );\n }\n return await runSetup(resolved, {\n // `input` stays absent for `--yes` — exactOptionalPropertyTypes means \"absent\"\n // is a decision, not a field carrying undefined.\n ...(io.initInput === undefined ? {} : { input: io.initInput }),\n output: (text) =>\n io.stdout(colorize(text, io.color === true && !resolved.yes && !resolved.json)),\n cwd: io.cwd ?? process.cwd(),\n version: io.version,\n // The lava renderer is for the human at a terminal: --yes and --json are the\n // machine paths, and their bytes stay plain however pretty the screen is.\n ...(io.color === true && !resolved.yes && !resolved.json ? { color: true } : {}),\n });\n },\n};\n"]}