@smeltjs/core 0.2.0 → 0.4.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.
- package/README.md +3 -3
- package/dist/agents/guide.d.ts +68 -0
- package/dist/agents/guide.d.ts.map +1 -0
- package/dist/agents/guide.js +72 -0
- package/dist/agents/guide.js.map +1 -0
- package/dist/agents/instructions.d.ts +133 -0
- package/dist/agents/instructions.d.ts.map +1 -0
- package/dist/agents/instructions.js +212 -0
- package/dist/agents/instructions.js.map +1 -0
- package/dist/agents/lint.d.ts +154 -0
- package/dist/agents/lint.d.ts.map +1 -0
- package/dist/agents/lint.js +761 -0
- package/dist/agents/lint.js.map +1 -0
- package/dist/agents/split.d.ts +113 -0
- package/dist/agents/split.d.ts.map +1 -0
- package/dist/agents/split.js +234 -0
- package/dist/agents/split.js.map +1 -0
- package/dist/apply.d.ts.map +1 -1
- package/dist/apply.js.map +1 -1
- package/dist/cache/prefix.d.ts.map +1 -1
- package/dist/cache/prefix.js.map +1 -1
- package/dist/cli/agents.d.ts +39 -0
- package/dist/cli/agents.d.ts.map +1 -0
- package/dist/cli/agents.js +121 -0
- package/dist/cli/agents.js.map +1 -0
- package/dist/cli/args.d.ts +17 -70
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +18 -338
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/bin.d.ts.map +1 -1
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/config.d.ts +60 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +89 -2
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/hooks.d.ts +55 -51
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +206 -658
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/init.d.ts +30 -15
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +167 -38
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/report.d.ts +23 -0
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +142 -4
- package/dist/cli/report.js.map +1 -1
- package/dist/cli/run.d.ts +29 -90
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +34 -342
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/shell.d.ts +106 -0
- package/dist/cli/shell.d.ts.map +1 -0
- package/dist/cli/shell.js +100 -0
- package/dist/cli/shell.js.map +1 -0
- package/dist/cli/subcommands/agents.d.ts +72 -0
- package/dist/cli/subcommands/agents.d.ts.map +1 -0
- package/dist/cli/subcommands/agents.js +162 -0
- package/dist/cli/subcommands/agents.js.map +1 -0
- package/dist/cli/subcommands/flags.d.ts +144 -0
- package/dist/cli/subcommands/flags.d.ts.map +1 -0
- package/dist/cli/subcommands/flags.js +220 -0
- package/dist/cli/subcommands/flags.js.map +1 -0
- package/dist/cli/subcommands/hooks.d.ts +18 -0
- package/dist/cli/subcommands/hooks.d.ts.map +1 -0
- package/dist/cli/subcommands/hooks.js +80 -0
- package/dist/cli/subcommands/hooks.js.map +1 -0
- package/dist/cli/subcommands/init.d.ts +19 -0
- package/dist/cli/subcommands/init.d.ts.map +1 -0
- package/dist/cli/subcommands/init.js +44 -0
- package/dist/cli/subcommands/init.js.map +1 -0
- package/dist/cli/subcommands/map.d.ts +75 -0
- package/dist/cli/subcommands/map.d.ts.map +1 -0
- package/dist/cli/subcommands/map.js +132 -0
- package/dist/cli/subcommands/map.js.map +1 -0
- package/dist/cli/subcommands/registry.d.ts +81 -0
- package/dist/cli/subcommands/registry.d.ts.map +1 -0
- package/dist/cli/subcommands/registry.js +141 -0
- package/dist/cli/subcommands/registry.js.map +1 -0
- package/dist/cli/subcommands/retrieve.d.ts +74 -0
- package/dist/cli/subcommands/retrieve.d.ts.map +1 -0
- package/dist/cli/subcommands/retrieve.js +84 -0
- package/dist/cli/subcommands/retrieve.js.map +1 -0
- package/dist/cli/subcommands/smelt.d.ts +107 -0
- package/dist/cli/subcommands/smelt.d.ts.map +1 -0
- package/dist/cli/subcommands/smelt.js +304 -0
- package/dist/cli/subcommands/smelt.js.map +1 -0
- package/dist/cli/subcommands/stats.d.ts +43 -0
- package/dist/cli/subcommands/stats.d.ts.map +1 -0
- package/dist/cli/subcommands/stats.js +62 -0
- package/dist/cli/subcommands/stats.js.map +1 -0
- package/dist/cli/subcommands/subcommand.d.ts +113 -0
- package/dist/cli/subcommands/subcommand.d.ts.map +1 -0
- package/dist/cli/subcommands/subcommand.js +3 -0
- package/dist/cli/subcommands/subcommand.js.map +1 -0
- package/dist/cli/usage.d.ts +3 -0
- package/dist/cli/usage.d.ts.map +1 -0
- package/dist/cli/usage.js +95 -0
- package/dist/cli/usage.js.map +1 -0
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js.map +1 -1
- package/dist/errors.d.ts +21 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +33 -1
- package/dist/errors.js.map +1 -1
- package/dist/harness/aider.d.ts +10 -0
- package/dist/harness/aider.d.ts.map +1 -0
- package/dist/harness/aider.js +21 -0
- package/dist/harness/aider.js.map +1 -0
- package/dist/harness/claude-code.d.ts +3 -0
- package/dist/harness/claude-code.d.ts.map +1 -0
- package/dist/harness/claude-code.js +68 -0
- package/dist/harness/claude-code.js.map +1 -0
- package/dist/harness/cline.d.ts +3 -0
- package/dist/harness/cline.d.ts.map +1 -0
- package/dist/harness/cline.js +54 -0
- package/dist/harness/cline.js.map +1 -0
- package/dist/harness/codex.d.ts +3 -0
- package/dist/harness/codex.d.ts.map +1 -0
- package/dist/harness/codex.js +92 -0
- package/dist/harness/codex.js.map +1 -0
- package/dist/harness/cursor.d.ts +3 -0
- package/dist/harness/cursor.d.ts.map +1 -0
- package/dist/harness/cursor.js +54 -0
- package/dist/harness/cursor.js.map +1 -0
- package/dist/harness/gemini.d.ts +3 -0
- package/dist/harness/gemini.d.ts.map +1 -0
- package/dist/harness/gemini.js +60 -0
- package/dist/harness/gemini.js.map +1 -0
- package/dist/harness/grok.d.ts +3 -0
- package/dist/harness/grok.d.ts.map +1 -0
- package/dist/harness/grok.js +47 -0
- package/dist/harness/grok.js.map +1 -0
- package/dist/harness/hermes.d.ts +3 -0
- package/dist/harness/hermes.d.ts.map +1 -0
- package/dist/harness/hermes.js +72 -0
- package/dist/harness/hermes.js.map +1 -0
- package/dist/harness/kilocode.d.ts +3 -0
- package/dist/harness/kilocode.d.ts.map +1 -0
- package/dist/harness/kilocode.js +35 -0
- package/dist/harness/kilocode.js.map +1 -0
- package/dist/harness/opencode.d.ts +3 -0
- package/dist/harness/opencode.d.ts.map +1 -0
- package/dist/harness/opencode.js +97 -0
- package/dist/harness/opencode.js.map +1 -0
- package/dist/harness/paths.d.ts +17 -0
- package/dist/harness/paths.d.ts.map +1 -0
- package/dist/harness/paths.js +45 -0
- package/dist/harness/paths.js.map +1 -0
- package/dist/harness/profile.d.ts +209 -0
- package/dist/harness/profile.d.ts.map +1 -0
- package/dist/harness/profile.js +41 -0
- package/dist/harness/profile.js.map +1 -0
- package/dist/harness/registry.d.ts +100 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +144 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/snippet.d.ts +19 -0
- package/dist/harness/snippet.d.ts.map +1 -0
- package/dist/harness/snippet.js +41 -0
- package/dist/harness/snippet.js.map +1 -0
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js.map +1 -1
- package/dist/hooks/guard-core.d.ts +14 -14
- package/dist/hooks/guard-core.d.ts.map +1 -1
- package/dist/hooks/guard-core.js +15 -37
- package/dist/hooks/guard-core.js.map +1 -1
- package/dist/hooks/shim.d.ts +95 -4
- package/dist/hooks/shim.d.ts.map +1 -1
- package/dist/hooks/shim.js +87 -2
- package/dist/hooks/shim.js.map +1 -1
- package/dist/hooks/shims/claude-code.d.ts +5 -18
- package/dist/hooks/shims/claude-code.d.ts.map +1 -1
- package/dist/hooks/shims/claude-code.js +9 -56
- package/dist/hooks/shims/claude-code.js.map +1 -1
- package/dist/hooks/shims/cline.d.ts +5 -12
- package/dist/hooks/shims/cline.d.ts.map +1 -1
- package/dist/hooks/shims/cline.js +9 -34
- package/dist/hooks/shims/cline.js.map +1 -1
- package/dist/hooks/shims/codex.d.ts +5 -18
- package/dist/hooks/shims/codex.d.ts.map +1 -1
- package/dist/hooks/shims/codex.js +9 -51
- package/dist/hooks/shims/codex.js.map +1 -1
- package/dist/hooks/shims/cursor.d.ts +5 -14
- package/dist/hooks/shims/cursor.d.ts.map +1 -1
- package/dist/hooks/shims/cursor.js +9 -42
- package/dist/hooks/shims/cursor.js.map +1 -1
- package/dist/hooks/shims/gemini.d.ts +5 -18
- package/dist/hooks/shims/gemini.d.ts.map +1 -1
- package/dist/hooks/shims/gemini.js +9 -48
- package/dist/hooks/shims/gemini.js.map +1 -1
- package/dist/hooks/shims/grok.d.ts +5 -13
- package/dist/hooks/shims/grok.d.ts.map +1 -1
- package/dist/hooks/shims/grok.js +9 -32
- package/dist/hooks/shims/grok.js.map +1 -1
- package/dist/hooks/shims/hermes.d.ts +5 -17
- package/dist/hooks/shims/hermes.d.ts.map +1 -1
- package/dist/hooks/shims/hermes.js +9 -45
- package/dist/hooks/shims/hermes.js.map +1 -1
- package/dist/index.d.ts +43 -76
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -67
- package/dist/index.js.map +1 -1
- package/dist/lang/bash.d.ts.map +1 -1
- package/dist/lang/bash.js.map +1 -1
- package/dist/lang/c.d.ts.map +1 -1
- package/dist/lang/c.js.map +1 -1
- package/dist/lang/c_sharp.d.ts.map +1 -1
- package/dist/lang/c_sharp.js.map +1 -1
- package/dist/lang/cpp.d.ts.map +1 -1
- package/dist/lang/cpp.js.map +1 -1
- package/dist/lang/go.d.ts.map +1 -1
- package/dist/lang/go.js.map +1 -1
- package/dist/lang/java.d.ts.map +1 -1
- package/dist/lang/java.js.map +1 -1
- package/dist/lang/javascript.d.ts.map +1 -1
- package/dist/lang/javascript.js.map +1 -1
- package/dist/lang/kotlin.d.ts.map +1 -1
- package/dist/lang/kotlin.js.map +1 -1
- package/dist/lang/php.d.ts.map +1 -1
- package/dist/lang/php.js.map +1 -1
- package/dist/lang/profile.d.ts.map +1 -1
- package/dist/lang/profile.js.map +1 -1
- package/dist/lang/python.d.ts.map +1 -1
- package/dist/lang/python.js.map +1 -1
- package/dist/lang/registry.d.ts.map +1 -1
- package/dist/lang/registry.js.map +1 -1
- package/dist/lang/ruby.d.ts.map +1 -1
- package/dist/lang/ruby.js.map +1 -1
- package/dist/lang/rust.d.ts.map +1 -1
- package/dist/lang/rust.js.map +1 -1
- package/dist/lang/swift.d.ts.map +1 -1
- package/dist/lang/swift.js.map +1 -1
- package/dist/lang/tsx.d.ts.map +1 -1
- package/dist/lang/tsx.js.map +1 -1
- package/dist/lang/typescript.d.ts.map +1 -1
- package/dist/lang/typescript.js.map +1 -1
- package/dist/net/policy.d.ts +18 -1
- package/dist/net/policy.d.ts.map +1 -1
- package/dist/net/policy.js +6 -4
- package/dist/net/policy.js.map +1 -1
- package/dist/ops/index.d.ts +39 -0
- package/dist/ops/index.d.ts.map +1 -0
- package/dist/ops/index.js +37 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/ops/inputs.d.ts +170 -0
- package/dist/ops/inputs.d.ts.map +1 -0
- package/dist/ops/inputs.js +142 -0
- package/dist/ops/inputs.js.map +1 -0
- package/dist/ops/verbs.d.ts +136 -0
- package/dist/ops/verbs.d.ts.map +1 -0
- package/dist/ops/verbs.js +76 -0
- package/dist/ops/verbs.js.map +1 -0
- package/dist/plan/auto.d.ts +69 -0
- package/dist/plan/auto.d.ts.map +1 -0
- package/dist/plan/auto.js +70 -0
- package/dist/plan/auto.js.map +1 -0
- package/dist/plan/budget.d.ts +21 -0
- package/dist/plan/budget.d.ts.map +1 -0
- package/dist/plan/budget.js +26 -0
- package/dist/plan/budget.js.map +1 -0
- package/dist/plan/grammar.d.ts +14 -0
- package/dist/plan/grammar.d.ts.map +1 -1
- package/dist/plan/grammar.js +47 -6
- package/dist/plan/grammar.js.map +1 -1
- package/dist/plan/lexical.d.ts.map +1 -1
- package/dist/plan/lexical.js +1 -7
- package/dist/plan/lexical.js.map +1 -1
- package/dist/plan/planners.d.ts +35 -2
- package/dist/plan/planners.d.ts.map +1 -1
- package/dist/plan/planners.js +35 -2
- package/dist/plan/planners.js.map +1 -1
- package/dist/plan/structural.d.ts +19 -5
- package/dist/plan/structural.d.ts.map +1 -1
- package/dist/plan/structural.js +180 -33
- package/dist/plan/structural.js.map +1 -1
- package/dist/repomap/cache.d.ts +48 -0
- package/dist/repomap/cache.d.ts.map +1 -1
- package/dist/repomap/cache.js +112 -13
- package/dist/repomap/cache.js.map +1 -1
- package/dist/repomap/io.d.ts +24 -0
- package/dist/repomap/io.d.ts.map +1 -0
- package/dist/repomap/io.js +34 -0
- package/dist/repomap/io.js.map +1 -0
- package/dist/repomap/map.d.ts +69 -6
- package/dist/repomap/map.d.ts.map +1 -1
- package/dist/repomap/map.js +104 -22
- package/dist/repomap/map.js.map +1 -1
- package/dist/repomap/rank.d.ts +26 -2
- package/dist/repomap/rank.d.ts.map +1 -1
- package/dist/repomap/rank.js +20 -0
- package/dist/repomap/rank.js.map +1 -1
- package/dist/repomap/reader.d.ts +73 -0
- package/dist/repomap/reader.d.ts.map +1 -0
- package/dist/repomap/reader.js +30 -0
- package/dist/repomap/reader.js.map +1 -0
- package/dist/repomap/tags.d.ts.map +1 -1
- package/dist/repomap/tags.js.map +1 -1
- package/dist/retrieve.d.ts +8 -0
- package/dist/retrieve.d.ts.map +1 -1
- package/dist/retrieve.js +12 -1
- package/dist/retrieve.js.map +1 -1
- package/dist/smelter.d.ts +79 -0
- package/dist/smelter.d.ts.map +1 -0
- package/dist/smelter.js +65 -0
- package/dist/smelter.js.map +1 -0
- package/dist/stages.d.ts.map +1 -1
- package/dist/stages.js +2 -2
- package/dist/stages.js.map +1 -1
- package/dist/stats.d.ts +2 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +2 -1
- package/dist/stats.js.map +1 -1
- package/dist/store-dir.d.ts +43 -4
- package/dist/store-dir.d.ts.map +1 -1
- package/dist/store-dir.js +48 -5
- package/dist/store-dir.js.map +1 -1
- package/dist/store.d.ts +6 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +6 -0
- package/dist/store.js.map +1 -1
- package/dist/text/json-edit.d.ts +71 -0
- package/dist/text/json-edit.d.ts.map +1 -0
- package/dist/text/json-edit.js +219 -0
- package/dist/text/json-edit.js.map +1 -0
- package/dist/types.d.ts +35 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +11 -11
- package/dist/cli/resolve.d.ts +0 -103
- package/dist/cli/resolve.d.ts.map +0 -1
- package/dist/cli/resolve.js +0 -106
- package/dist/cli/resolve.js.map +0 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { CliUsageError } from '../../errors.js';
|
|
2
|
+
import { budgetRequired, readTree } from '../../ops/inputs.js';
|
|
3
|
+
import { mapTree } from '../../ops/verbs.js';
|
|
4
|
+
import { CONFIG_FILE_NAME } from '../config.js';
|
|
5
|
+
import { formatMapReport } from '../report.js';
|
|
6
|
+
import { CLI_NAME, EXIT } from '../shell.js';
|
|
7
|
+
import { parseBudget } from './flags.js';
|
|
8
|
+
/**
|
|
9
|
+
* The `smelt map --json` envelope format. Its own version line, because the two
|
|
10
|
+
* envelopes carry different structures and must be able to move independently —
|
|
11
|
+
* a map envelope has no elided bytes to carry, since a map elides nothing.
|
|
12
|
+
*/
|
|
13
|
+
export const CLI_MAP_JSON_FORMAT = 'smelt-map-cli/v1';
|
|
14
|
+
export const mapCommand = {
|
|
15
|
+
name: 'map',
|
|
16
|
+
flags: ['budget', 'focus', 'ignore', 'cache', 'json'],
|
|
17
|
+
refusal: `map reads a whole tree, detects each file's language itself, and is not a planner ` +
|
|
18
|
+
`strategy — it returns a map, not an elision plan, and elides nothing, so there is ` +
|
|
19
|
+
`nothing to put back.`,
|
|
20
|
+
usage: {
|
|
21
|
+
synopsis: [
|
|
22
|
+
'map <dir> --budget <bytes> [--focus <term>]... [--ignore <entry>]... [--cache <dir>]',
|
|
23
|
+
],
|
|
24
|
+
section: {
|
|
25
|
+
heading: 'MAP',
|
|
26
|
+
body: ` ${CLI_NAME} map <dir> renders a ranked symbol map of a whole repository — modelled\n` +
|
|
27
|
+
` on Aider's repo-map (aider.chat/docs/repomap.html, design by Paul Gauthier) — to\n` +
|
|
28
|
+
` stdout, with a short report on stderr. Local files only: symlinks are never\n` +
|
|
29
|
+
` followed, binary files are skipped, and the map writes nothing to disk unless\n` +
|
|
30
|
+
` --cache names a directory. Every included symbol carries a receipt: its\n` +
|
|
31
|
+
` definition site and the measured reference counts that ranked it. Unlike a\n` +
|
|
32
|
+
` smelt run, map never exits\n` +
|
|
33
|
+
` 1: a plan can come back over budget because ${CLI_NAME} refuses to cut regions you\n` +
|
|
34
|
+
` asked to keep, but the map fits itself to the budget by construction — symbols\n` +
|
|
35
|
+
` are appended in rank order until the next line would not fit.`,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
/**
|
|
39
|
+
* Exactly one directory, and the same budget rules as everywhere else — a missing
|
|
40
|
+
* `--budget` is not an error *here* (the config may carry `defaultBudgetBytes`), a
|
|
41
|
+
* malformed one always is.
|
|
42
|
+
*/
|
|
43
|
+
parse(values, positionals) {
|
|
44
|
+
if (positionals.length < 2) {
|
|
45
|
+
throw new CliUsageError(`${CLI_NAME}: map needs the directory to read.\n` +
|
|
46
|
+
` ${CLI_NAME} map <dir> --budget <bytes> [--focus <term>]...`);
|
|
47
|
+
}
|
|
48
|
+
if (positionals.length > 2) {
|
|
49
|
+
throw new CliUsageError(`${CLI_NAME}: map takes exactly one directory, got ` +
|
|
50
|
+
`${String(positionals.length - 1)} (${positionals.slice(1).join(', ')}).`);
|
|
51
|
+
}
|
|
52
|
+
const budgetBytes = parseBudget(values.budget);
|
|
53
|
+
return {
|
|
54
|
+
mode: 'map',
|
|
55
|
+
dir: positionals[1],
|
|
56
|
+
...(budgetBytes === undefined ? {} : { budgetBytes }),
|
|
57
|
+
focus: values.focus ?? [],
|
|
58
|
+
ignore: values.ignore ?? [],
|
|
59
|
+
...(values.cache === undefined ? {} : { cacheDir: values.cache }),
|
|
60
|
+
json: values.json === true,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
resolve(invocation, config) {
|
|
64
|
+
return resolveMapRun(invocation, config());
|
|
65
|
+
},
|
|
66
|
+
run(resolved, io) {
|
|
67
|
+
return runMap(resolved, io);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Merge one `'map'`-mode invocation with the loaded config and the built-ins. The
|
|
72
|
+
* config contributes exactly what it contributes to a smelt run — `defaultBudgetBytes`,
|
|
73
|
+
* a default the user chose explicitly — and nothing else: the store and strategy legs
|
|
74
|
+
* are single-blob concerns, and the map ignores them rather than reinterpreting them.
|
|
75
|
+
*
|
|
76
|
+
* @throws {CliUsageError} when neither `--budget` nor the config names a budget.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveMapRun(invocation, config) {
|
|
79
|
+
const budgetBytes = invocation.budgetBytes ?? config?.config.defaultBudgetBytes;
|
|
80
|
+
if (budgetBytes === undefined) {
|
|
81
|
+
throw new CliUsageError(`${CLI_NAME}: ` +
|
|
82
|
+
budgetRequired({
|
|
83
|
+
knob: '--budget',
|
|
84
|
+
stake: 'the map to leave out',
|
|
85
|
+
advice: `Pass --budget, or set defaultBudgetBytes in ${CONFIG_FILE_NAME} ` +
|
|
86
|
+
`(\`${CLI_NAME} init\` writes one).\n` +
|
|
87
|
+
` ${CLI_NAME} map src --budget 4000 --focus handleRequest`,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
budgetBytes,
|
|
92
|
+
budgetSource: invocation.budgetBytes !== undefined ? 'flag' : 'config',
|
|
93
|
+
dir: invocation.dir,
|
|
94
|
+
focus: invocation.focus,
|
|
95
|
+
...(invocation.ignore.length === 0 ? {} : { ignore: invocation.ignore }),
|
|
96
|
+
...(invocation.cacheDir === undefined ? {} : { cacheDir: invocation.cacheDir }),
|
|
97
|
+
json: invocation.json,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* One `smelt map` run: prove the target is a tree, hand it to {@link mapTree}, render.
|
|
102
|
+
*
|
|
103
|
+
* The merge, including the budget-required refusal, lives in {@link resolveMapRun} —
|
|
104
|
+
* the same arrangement the smelt verb uses over its {@link ResolvedRun}. The walk
|
|
105
|
+
* itself belongs to no front door: the `repo_map` tool calls the same op, and refuses
|
|
106
|
+
* a file with the same sentence in its own vocabulary.
|
|
107
|
+
*/
|
|
108
|
+
async function runMap(run, io) {
|
|
109
|
+
const tree = readTree(run.dir, run.dir, {
|
|
110
|
+
tree: 'map',
|
|
111
|
+
file: `\`${CLI_NAME} <file>\``,
|
|
112
|
+
});
|
|
113
|
+
if (!tree.ok)
|
|
114
|
+
throw new CliUsageError(`${CLI_NAME}: ${tree.refusal}`);
|
|
115
|
+
const map = await mapTree({
|
|
116
|
+
root: tree.value,
|
|
117
|
+
budgetBytes: run.budgetBytes,
|
|
118
|
+
focus: run.focus,
|
|
119
|
+
...(run.ignore === undefined ? {} : { ignore: run.ignore }),
|
|
120
|
+
...(run.cacheDir === undefined ? {} : { cacheDir: run.cacheDir }),
|
|
121
|
+
});
|
|
122
|
+
if (run.json) {
|
|
123
|
+
const mapEnvelope = { format: CLI_MAP_JSON_FORMAT, map };
|
|
124
|
+
io.stdout(`${JSON.stringify(mapEnvelope, null, 2)}\n`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
io.stdout(map.text);
|
|
128
|
+
}
|
|
129
|
+
io.stderr(formatMapReport({ map, source: run.dir, budgetSource: run.budgetSource }));
|
|
130
|
+
return EXIT.ok;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=map.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map.js","sourceRoot":"","sources":["../../../src/cli/subcommands/map.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { budgetRequired, readTree } from '../../ops/inputs.ts';\nimport { mapTree } from '../../ops/verbs.ts';\nimport type { RepoMap } from '../../repomap/map.ts';\nimport { CONFIG_FILE_NAME } from '../config.ts';\nimport type { LoadedConfig } from '../config.ts';\nimport { formatMapReport } from '../report.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport { parseBudget } from './flags.ts';\nimport type { FlagValues } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt map <dir>` — the repo map's front door.\n *\n * Deliberately **not** a `--strategy` name: `buildRepoMap` returns a {@link RepoMap},\n * not an `ElisionPlan` — nothing is elided, stored, or reversible — so it gets its own\n * verb and its own envelope instead of a strategy name that would lie about what comes\n * back. And it never exits {@link EXIT.overBudget}: a smelt plan may refuse to cut kept\n * regions and come back too big, while the map fits itself to the budget by\n * construction, so no exit code pretends an over-budget map can happen.\n */\n\n/**\n * `smelt map <dir>` — the repo-map subcommand, parsed. A separate shape rather than\n * more optional fields on `SmeltInvocation`, because the two commands share\n * almost nothing: a map has a directory instead of a file/stdin, an ignore list and\n * a cache directory instead of a language and a strategy.\n */\nexport interface MapInvocation {\n readonly mode: 'map';\n /** The repository root to map. Always present — `map` without a directory is a usage error. */\n readonly dir: string;\n /** `undefined` means the flag was not given — the config default may apply. */\n readonly budgetBytes?: number;\n readonly focus: readonly string[];\n /** `--ignore` entries, replacing the built-in default list when non-empty. */\n readonly ignore: readonly string[];\n /** `--cache <dir>`: only when given does the map write to disk. */\n readonly cacheDir?: string;\n readonly json: boolean;\n}\n\n/**\n * Everything one `smelt map` run needs, fully merged — {@link ResolvedRun}'s sibling,\n * not a contortion of it. The two commands share exactly one merged value (the\n * budget), so they share the *seam* that owns precedence — every verb resolving its\n * own flags against the same config — not a struct whose fields would mostly be lies\n * for one of them: a map has no store, no strategy, no stdin, and its ignore/cache\n * legs mean nothing to a single-blob run.\n */\nexport interface ResolvedMapRun {\n readonly budgetBytes: number;\n /** Where the budget came from. A missing budget never gets here — it throws. */\n readonly budgetSource: 'flag' | 'config';\n readonly dir: string;\n readonly focus: readonly string[];\n /** `undefined` means \"use the library's default ignore list\". Flags only. */\n readonly ignore?: readonly string[];\n /** Only when present does the map write to disk. Flags only; the config has no say. */\n readonly cacheDir?: string;\n readonly json: boolean;\n}\n\n/**\n * The `smelt map --json` envelope format. Its own version line, because the two\n * envelopes carry different structures and must be able to move independently —\n * a map envelope has no elided bytes to carry, since a map elides nothing.\n */\nexport const CLI_MAP_JSON_FORMAT = 'smelt-map-cli/v1';\n\n/** What `smelt map --json` prints: the {@link RepoMap} verbatim, versioned. */\nexport interface CliMapJsonEnvelope {\n readonly format: string;\n /** The {@link RepoMap} exactly as `buildRepoMap` returned it. */\n readonly map: RepoMap;\n}\n\nexport const mapCommand: Subcommand<MapInvocation, ResolvedMapRun> = {\n name: 'map',\n flags: ['budget', 'focus', 'ignore', 'cache', 'json'],\n refusal:\n `map reads a whole tree, detects each file's language itself, and is not a planner ` +\n `strategy — it returns a map, not an elision plan, and elides nothing, so there is ` +\n `nothing to put back.`,\n usage: {\n synopsis: [\n 'map <dir> --budget <bytes> [--focus <term>]... [--ignore <entry>]... [--cache <dir>]',\n ],\n section: {\n heading: 'MAP',\n body:\n ` ${CLI_NAME} map <dir> renders a ranked symbol map of a whole repository — modelled\\n` +\n ` on Aider's repo-map (aider.chat/docs/repomap.html, design by Paul Gauthier) — to\\n` +\n ` stdout, with a short report on stderr. Local files only: symlinks are never\\n` +\n ` followed, binary files are skipped, and the map writes nothing to disk unless\\n` +\n ` --cache names a directory. Every included symbol carries a receipt: its\\n` +\n ` definition site and the measured reference counts that ranked it. Unlike a\\n` +\n ` smelt run, map never exits\\n` +\n ` 1: a plan can come back over budget because ${CLI_NAME} refuses to cut regions you\\n` +\n ` asked to keep, but the map fits itself to the budget by construction — symbols\\n` +\n ` are appended in rank order until the next line would not fit.`,\n },\n },\n\n /**\n * Exactly one directory, and the same budget rules as everywhere else — a missing\n * `--budget` is not an error *here* (the config may carry `defaultBudgetBytes`), a\n * malformed one always is.\n */\n parse(values: FlagValues, positionals: readonly string[]): MapInvocation {\n if (positionals.length < 2) {\n throw new CliUsageError(\n `${CLI_NAME}: map needs the directory to read.\\n` +\n ` ${CLI_NAME} map <dir> --budget <bytes> [--focus <term>]...`,\n );\n }\n if (positionals.length > 2) {\n throw new CliUsageError(\n `${CLI_NAME}: map takes exactly one directory, got ` +\n `${String(positionals.length - 1)} (${positionals.slice(1).join(', ')}).`,\n );\n }\n const budgetBytes = parseBudget(values.budget);\n return {\n mode: 'map',\n dir: positionals[1]!,\n ...(budgetBytes === undefined ? {} : { budgetBytes }),\n focus: values.focus ?? [],\n ignore: values.ignore ?? [],\n ...(values.cache === undefined ? {} : { cacheDir: values.cache }),\n json: values.json === true,\n };\n },\n\n resolve(invocation: MapInvocation, config: ConfigSource): ResolvedMapRun {\n return resolveMapRun(invocation, config());\n },\n\n run(resolved: ResolvedMapRun, io: CliIo): Promise<number> {\n return runMap(resolved, io);\n },\n};\n\n/**\n * Merge one `'map'`-mode invocation with the loaded config and the built-ins. The\n * config contributes exactly what it contributes to a smelt run — `defaultBudgetBytes`,\n * a default the user chose explicitly — and nothing else: the store and strategy legs\n * are single-blob concerns, and the map ignores them rather than reinterpreting them.\n *\n * @throws {CliUsageError} when neither `--budget` nor the config names a budget.\n */\nexport function resolveMapRun(\n invocation: MapInvocation,\n config: LoadedConfig | undefined,\n): ResolvedMapRun {\n const budgetBytes = invocation.budgetBytes ?? config?.config.defaultBudgetBytes;\n if (budgetBytes === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: ` +\n budgetRequired({\n knob: '--budget',\n stake: 'the map to leave out',\n advice:\n `Pass --budget, or set defaultBudgetBytes in ${CONFIG_FILE_NAME} ` +\n `(\\`${CLI_NAME} init\\` writes one).\\n` +\n ` ${CLI_NAME} map src --budget 4000 --focus handleRequest`,\n }),\n );\n }\n\n return {\n budgetBytes,\n budgetSource: invocation.budgetBytes !== undefined ? 'flag' : 'config',\n dir: invocation.dir,\n focus: invocation.focus,\n ...(invocation.ignore.length === 0 ? {} : { ignore: invocation.ignore }),\n ...(invocation.cacheDir === undefined ? {} : { cacheDir: invocation.cacheDir }),\n json: invocation.json,\n };\n}\n\n/**\n * One `smelt map` run: prove the target is a tree, hand it to {@link mapTree}, render.\n *\n * The merge, including the budget-required refusal, lives in {@link resolveMapRun} —\n * the same arrangement the smelt verb uses over its {@link ResolvedRun}. The walk\n * itself belongs to no front door: the `repo_map` tool calls the same op, and refuses\n * a file with the same sentence in its own vocabulary.\n */\nasync function runMap(run: ResolvedMapRun, io: CliIo): Promise<number> {\n const tree = readTree(run.dir, run.dir, {\n tree: 'map',\n file: `\\`${CLI_NAME} <file>\\``,\n });\n if (!tree.ok) throw new CliUsageError(`${CLI_NAME}: ${tree.refusal}`);\n\n const map = await mapTree({\n root: tree.value,\n budgetBytes: run.budgetBytes,\n focus: run.focus,\n ...(run.ignore === undefined ? {} : { ignore: run.ignore }),\n ...(run.cacheDir === undefined ? {} : { cacheDir: run.cacheDir }),\n });\n\n if (run.json) {\n const mapEnvelope: CliMapJsonEnvelope = { format: CLI_MAP_JSON_FORMAT, map };\n io.stdout(`${JSON.stringify(mapEnvelope, null, 2)}\\n`);\n } else {\n io.stdout(map.text);\n }\n io.stderr(formatMapReport({ map, source: run.dir, budgetSource: run.budgetSource }));\n\n return EXIT.ok;\n}\n"]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { AgentsInvocation } from './agents.ts';
|
|
2
|
+
import type { FlagName, FlagValues } from './flags.ts';
|
|
3
|
+
import type { HooksInvocation } from './hooks.ts';
|
|
4
|
+
import type { InitInvocation } from './init.ts';
|
|
5
|
+
import type { MapInvocation } from './map.ts';
|
|
6
|
+
import type { RetrieveInvocation } from './retrieve.ts';
|
|
7
|
+
import type { SmeltInvocation } from './smelt.ts';
|
|
8
|
+
import type { StatsInvocation } from './stats.ts';
|
|
9
|
+
import type { Subcommand, Verb } from './subcommand.ts';
|
|
10
|
+
/**
|
|
11
|
+
* The registry — every verb `smelt` answers to, one {@link Subcommand} each.
|
|
12
|
+
*
|
|
13
|
+
* `Record<Verb, Subcommand>` on purpose, exactly like `LANGUAGE_PROFILES` and
|
|
14
|
+
* `HARNESS_PROFILES`: adding a `Verb` in `subcommand.ts` without writing its file is a
|
|
15
|
+
* compile error, so the verb list and the facts cannot drift. Every derived view — the
|
|
16
|
+
* USAGE block, the help's sections, the `map only.` prefixes in OPTIONS, and above all
|
|
17
|
+
* the flag-ownership refusal below — is computed from this object, never written twice.
|
|
18
|
+
*
|
|
19
|
+
* Key order is meaningful: it is the order every rendered list uses (USAGE, the help
|
|
20
|
+
* sections, the owners named in a refusal), so keep it stable and append new verbs at
|
|
21
|
+
* the end.
|
|
22
|
+
*/
|
|
23
|
+
export declare const SUBCOMMANDS: Readonly<Record<Verb, AnySubcommand>>;
|
|
24
|
+
/** Everything `parseSmeltArgs` can return. Narrow on `mode`. */
|
|
25
|
+
export type CliInvocation = SmeltInvocation | InitInvocation | MapInvocation | RetrieveInvocation | StatsInvocation | HooksInvocation | AgentsInvocation;
|
|
26
|
+
/**
|
|
27
|
+
* One registry entry, with its verb's own invocation and resolved types erased.
|
|
28
|
+
*
|
|
29
|
+
* The erasure is what lets six differently-typed commands live in one `Record`, and it
|
|
30
|
+
* is safe because the two ends are never crossed: `runCli` hands a command exactly the
|
|
31
|
+
* invocation that command's own `parse` produced (`verbFor` maps each `mode` back to
|
|
32
|
+
* the verb that minted it), and the resolved value never leaves the pair of calls that
|
|
33
|
+
* makes and consumes it.
|
|
34
|
+
*/
|
|
35
|
+
export type AnySubcommand = Subcommand<CliInvocation, unknown>;
|
|
36
|
+
/** Every command, in registry order. The list every rendered block walks. */
|
|
37
|
+
export declare const SUBCOMMAND_LIST: readonly AnySubcommand[];
|
|
38
|
+
/**
|
|
39
|
+
* The verbs with a word on the command line — everything but the default one.
|
|
40
|
+
*
|
|
41
|
+
* The default verb is excluded deliberately: `smelt smelt` is a file named `smelt`,
|
|
42
|
+
* not a recursive invocation, the same way `smelt map` as a *file* needs `./map`.
|
|
43
|
+
*/
|
|
44
|
+
export declare const NAMED_VERBS: readonly Verb[];
|
|
45
|
+
/**
|
|
46
|
+
* Which command a command line selects: `positionals[0]` when it names a verb, the
|
|
47
|
+
* default verb otherwise. The whole of subcommand dispatch, in one lookup.
|
|
48
|
+
*/
|
|
49
|
+
export declare function subcommandFor(positionals: readonly string[]): AnySubcommand;
|
|
50
|
+
/** The command that produced an invocation, for dispatch. */
|
|
51
|
+
export declare function commandFor(mode: Exclude<CliInvocation['mode'], 'help' | 'version'>): AnySubcommand;
|
|
52
|
+
/**
|
|
53
|
+
* The commands that own a flag, in registry order — empty for the two global flags,
|
|
54
|
+
* which no verb owns. A flag with exactly one owner has an unambiguous home worth
|
|
55
|
+
* naming in a refusal (and worth prefixing its OPTIONS entry with); a flag several
|
|
56
|
+
* verbs share (`--json`, `--budget`, `--focus`) has none, and pointing at one of them
|
|
57
|
+
* would be a guess.
|
|
58
|
+
*/
|
|
59
|
+
export declare function ownersOf(flag: FlagName): readonly AnySubcommand[];
|
|
60
|
+
/**
|
|
61
|
+
* Refuse every flag the chosen verb does not own — the one message that replaced five
|
|
62
|
+
* hand-written ones.
|
|
63
|
+
*
|
|
64
|
+
* Before this existed, each verb refused each other verb's flags in prose, so the
|
|
65
|
+
* refusals were O(verbs × flags) sentences kept in sync by hand and an eleventh flag
|
|
66
|
+
* edited five of them. Now ownership is declared once per verb and the message is
|
|
67
|
+
* generated from three derived facts, in the order a reader needs them:
|
|
68
|
+
*
|
|
69
|
+
* 1. **what this verb takes**, and what it got instead — the offending flag, named;
|
|
70
|
+
* 2. **where the flag does belong**, when exactly one verb owns it, listed as that
|
|
71
|
+
* owner's *exclusively* owned flags (so `--ignore` here still reads "--ignore and
|
|
72
|
+
* --cache belong to `smelt map`", exactly as the hand-written message did);
|
|
73
|
+
* 3. **why not here** — the verb's own `refusal` sentence, the half of the old
|
|
74
|
+
* messages worth keeping, and the only half a verb still writes.
|
|
75
|
+
*
|
|
76
|
+
* Every refusal stays a {@link CliUsageError}, so every one of them still exits 2.
|
|
77
|
+
*
|
|
78
|
+
* @throws {CliUsageError} naming the offending flag, this verb, and the flag's owner.
|
|
79
|
+
*/
|
|
80
|
+
export declare function refuseForeignFlags(command: AnySubcommand, values: FlagValues): void;
|
|
81
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/registry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,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,UAAU,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAExD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,CAQ7D,CAAC;AAEF,gEAAgE;AAChE,MAAM,MAAM,aAAa,GACrB,eAAe,GACf,cAAc,GACd,aAAa,GACb,kBAAkB,GAClB,eAAe,GACf,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;AAmBD,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 { 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 agents: agentsCommand,\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\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} 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,141 @@
|
|
|
1
|
+
import { CliUsageError } from '../../errors.js';
|
|
2
|
+
import { CLI_NAME } from '../shell.js';
|
|
3
|
+
import { agentsCommand } from './agents.js';
|
|
4
|
+
import { flagList, VERB_FLAGS } from './flags.js';
|
|
5
|
+
import { hooksCommand } from './hooks.js';
|
|
6
|
+
import { initCommand } from './init.js';
|
|
7
|
+
import { mapCommand } from './map.js';
|
|
8
|
+
import { retrieveCommand } from './retrieve.js';
|
|
9
|
+
import { smeltCommand } from './smelt.js';
|
|
10
|
+
import { statsCommand } from './stats.js';
|
|
11
|
+
import { DEFAULT_VERB } from './subcommand.js';
|
|
12
|
+
/**
|
|
13
|
+
* The registry — every verb `smelt` answers to, one {@link Subcommand} each.
|
|
14
|
+
*
|
|
15
|
+
* `Record<Verb, Subcommand>` on purpose, exactly like `LANGUAGE_PROFILES` and
|
|
16
|
+
* `HARNESS_PROFILES`: adding a `Verb` in `subcommand.ts` without writing its file is a
|
|
17
|
+
* compile error, so the verb list and the facts cannot drift. Every derived view — the
|
|
18
|
+
* USAGE block, the help's sections, the `map only.` prefixes in OPTIONS, and above all
|
|
19
|
+
* the flag-ownership refusal below — is computed from this object, never written twice.
|
|
20
|
+
*
|
|
21
|
+
* Key order is meaningful: it is the order every rendered list uses (USAGE, the help
|
|
22
|
+
* sections, the owners named in a refusal), so keep it stable and append new verbs at
|
|
23
|
+
* the end.
|
|
24
|
+
*/
|
|
25
|
+
export const SUBCOMMANDS = {
|
|
26
|
+
smelt: smeltCommand,
|
|
27
|
+
init: initCommand,
|
|
28
|
+
map: mapCommand,
|
|
29
|
+
retrieve: retrieveCommand,
|
|
30
|
+
stats: statsCommand,
|
|
31
|
+
hooks: hooksCommand,
|
|
32
|
+
agents: agentsCommand,
|
|
33
|
+
};
|
|
34
|
+
/** Every command, in registry order. The list every rendered block walks. */
|
|
35
|
+
export const SUBCOMMAND_LIST = Object.values(SUBCOMMANDS);
|
|
36
|
+
/**
|
|
37
|
+
* The verbs with a word on the command line — everything but the default one.
|
|
38
|
+
*
|
|
39
|
+
* The default verb is excluded deliberately: `smelt smelt` is a file named `smelt`,
|
|
40
|
+
* not a recursive invocation, the same way `smelt map` as a *file* needs `./map`.
|
|
41
|
+
*/
|
|
42
|
+
export const NAMED_VERBS = SUBCOMMAND_LIST.map((command) => command.name).filter((name) => name !== DEFAULT_VERB);
|
|
43
|
+
/**
|
|
44
|
+
* Which command a command line selects: `positionals[0]` when it names a verb, the
|
|
45
|
+
* default verb otherwise. The whole of subcommand dispatch, in one lookup.
|
|
46
|
+
*/
|
|
47
|
+
export function subcommandFor(positionals) {
|
|
48
|
+
const first = positionals[0];
|
|
49
|
+
const named = NAMED_VERBS.find((verb) => verb === first);
|
|
50
|
+
return SUBCOMMANDS[named ?? DEFAULT_VERB];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Which verb minted an invocation. Total over the modes `parseSmeltArgs` can return
|
|
54
|
+
* for a verb, so a new mode without a home is a compile error — `'reconstruct'` maps
|
|
55
|
+
* to the default verb because `--reconstruct` is that verb's second job, not a
|
|
56
|
+
* seventh command.
|
|
57
|
+
*/
|
|
58
|
+
const VERB_BY_MODE = {
|
|
59
|
+
smelt: 'smelt',
|
|
60
|
+
reconstruct: 'smelt',
|
|
61
|
+
init: 'init',
|
|
62
|
+
map: 'map',
|
|
63
|
+
retrieve: 'retrieve',
|
|
64
|
+
stats: 'stats',
|
|
65
|
+
hooks: 'hooks',
|
|
66
|
+
agents: 'agents',
|
|
67
|
+
};
|
|
68
|
+
/** The command that produced an invocation, for dispatch. */
|
|
69
|
+
export function commandFor(mode) {
|
|
70
|
+
return SUBCOMMANDS[VERB_BY_MODE[mode]];
|
|
71
|
+
}
|
|
72
|
+
/** How a command is named in prose. The default verb has no word to name it by. */
|
|
73
|
+
function label(command) {
|
|
74
|
+
return command.name === DEFAULT_VERB ? 'a single-blob run' : command.name;
|
|
75
|
+
}
|
|
76
|
+
/** How a command is named as the *owner* of a flag: the form you would type. */
|
|
77
|
+
function ownerLabel(command) {
|
|
78
|
+
return command.name === DEFAULT_VERB ? 'a single-blob run' : `\`${CLI_NAME} ${command.name}\``;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The commands that own a flag, in registry order — empty for the two global flags,
|
|
82
|
+
* which no verb owns. A flag with exactly one owner has an unambiguous home worth
|
|
83
|
+
* naming in a refusal (and worth prefixing its OPTIONS entry with); a flag several
|
|
84
|
+
* verbs share (`--json`, `--budget`, `--focus`) has none, and pointing at one of them
|
|
85
|
+
* would be a guess.
|
|
86
|
+
*/
|
|
87
|
+
export function ownersOf(flag) {
|
|
88
|
+
return SUBCOMMAND_LIST.filter((command) => command.flags.includes(flag));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Refuse every flag the chosen verb does not own — the one message that replaced five
|
|
92
|
+
* hand-written ones.
|
|
93
|
+
*
|
|
94
|
+
* Before this existed, each verb refused each other verb's flags in prose, so the
|
|
95
|
+
* refusals were O(verbs × flags) sentences kept in sync by hand and an eleventh flag
|
|
96
|
+
* edited five of them. Now ownership is declared once per verb and the message is
|
|
97
|
+
* generated from three derived facts, in the order a reader needs them:
|
|
98
|
+
*
|
|
99
|
+
* 1. **what this verb takes**, and what it got instead — the offending flag, named;
|
|
100
|
+
* 2. **where the flag does belong**, when exactly one verb owns it, listed as that
|
|
101
|
+
* owner's *exclusively* owned flags (so `--ignore` here still reads "--ignore and
|
|
102
|
+
* --cache belong to `smelt map`", exactly as the hand-written message did);
|
|
103
|
+
* 3. **why not here** — the verb's own `refusal` sentence, the half of the old
|
|
104
|
+
* messages worth keeping, and the only half a verb still writes.
|
|
105
|
+
*
|
|
106
|
+
* Every refusal stays a {@link CliUsageError}, so every one of them still exits 2.
|
|
107
|
+
*
|
|
108
|
+
* @throws {CliUsageError} naming the offending flag, this verb, and the flag's owner.
|
|
109
|
+
*/
|
|
110
|
+
export function refuseForeignFlags(command, values) {
|
|
111
|
+
const foreign = VERB_FLAGS.filter((flag) => values[flag] !== undefined && !command.flags.includes(flag));
|
|
112
|
+
if (foreign.length === 0)
|
|
113
|
+
return;
|
|
114
|
+
const takes = command.flags.length === 0
|
|
115
|
+
? `${label(command)} takes no flags (got ${flagList(foreign)}).`
|
|
116
|
+
: `${label(command)} takes only ${flagList(command.flags)} (got ${flagList(foreign)}).`;
|
|
117
|
+
throw new CliUsageError(`${CLI_NAME}: ${takes}${redirects(foreign)} ${command.refusal}`);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* ` --ignore and --cache belong to \`smelt map\`.` — one clause per single-owner verb.
|
|
121
|
+
*
|
|
122
|
+
* A clause names only the flags its owner owns **alone**, never the ones it shares.
|
|
123
|
+
* `ownersOf(flag).length === 1` is the same test that decided the owner deserved a
|
|
124
|
+
* clause at all, applied to the whole clause: a shared flag (`--budget`, `--focus`,
|
|
125
|
+
* `--json`) has no single home, so naming it here would assert an ownership that the
|
|
126
|
+
* OPTIONS block — which prefixes `map only.` by the same rule — correctly denies.
|
|
127
|
+
*/
|
|
128
|
+
function redirects(foreign) {
|
|
129
|
+
const owners = SUBCOMMAND_LIST.filter((owner) => foreign.some((flag) => {
|
|
130
|
+
const claimants = ownersOf(flag);
|
|
131
|
+
return claimants.length === 1 && claimants[0] === owner;
|
|
132
|
+
}));
|
|
133
|
+
return owners
|
|
134
|
+
.map((owner) => {
|
|
135
|
+
const elsewhere = owner.flags.filter((flag) => ownersOf(flag).length === 1);
|
|
136
|
+
const verb = elsewhere.length === 1 ? 'belongs' : 'belong';
|
|
137
|
+
return ` ${flagList(elsewhere)} ${verb} to ${ownerLabel(owner)}.`;
|
|
138
|
+
})
|
|
139
|
+
.join('');
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +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 { agentsCommand } from './agents.ts';\nimport type { AgentsInvocation } from './agents.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 { 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};\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\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} 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,74 @@
|
|
|
1
|
+
import type { LoadedConfig } from '../config.ts';
|
|
2
|
+
import type { Subcommand } from './subcommand.ts';
|
|
3
|
+
/**
|
|
4
|
+
* `smelt retrieve <hash>` — the marker's `retrieve("hash")`, as a real command.
|
|
5
|
+
*
|
|
6
|
+
* The exact original bytes go to stdout and **nothing else does**: no report, no
|
|
7
|
+
* trailing newline the store did not hold, no re-encoding. Trailing-newline fidelity
|
|
8
|
+
* is not pedantry — the retrieved bytes get spliced back into reasoning about the
|
|
9
|
+
* original, and an almost-right blob is the silent wrong answer this project refuses.
|
|
10
|
+
*
|
|
11
|
+
* The retrieval IS counted — that is the point. `store.retrieve()` journals the hit,
|
|
12
|
+
* so an agent working from pure shell moves the same `expansionRate` a tool-calling
|
|
13
|
+
* consumer moves, and over-pruning stays visible whichever loop is in use. Errors are
|
|
14
|
+
* the store's own, verbatim: `UnknownHashError` for a hash never elided,
|
|
15
|
+
* `StoreCorruptionError` for bytes that no longer hash to their name — distinct
|
|
16
|
+
* texts, both exiting {@link EXIT.refused}.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* `smelt retrieve <hash>` — parsed. A sibling shape, not a field on somebody else's:
|
|
20
|
+
* nothing but the hash, because the command's whole contract is "hash in, exact bytes
|
|
21
|
+
* out" — the same contract as the `smelt_retrieve` tool, reachable from a shell.
|
|
22
|
+
*/
|
|
23
|
+
export interface RetrieveInvocation {
|
|
24
|
+
readonly mode: 'retrieve';
|
|
25
|
+
/** The hash exactly as the marker printed it. Validated by the store, not here. */
|
|
26
|
+
readonly hash: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Everything `smelt retrieve` and `smelt stats` need from the config, fully merged:
|
|
30
|
+
* the persistent store's directory, already resolved against the config file. The
|
|
31
|
+
* third `Resolved*Run`, and the only one two verbs share — because they share the
|
|
32
|
+
* whole of it: no budget, no strategy, no file leg at all, only the store leg, which
|
|
33
|
+
* is config-only. `stats` resolves through {@link resolveStoreRun} rather than
|
|
34
|
+
* restating it.
|
|
35
|
+
*/
|
|
36
|
+
export interface ResolvedStoreRun {
|
|
37
|
+
/** Absolute path of the directory store, resolved against the config file. */
|
|
38
|
+
readonly storePath: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* What `retrieve` runs on: the shared store leg, plus the hash this invocation named.
|
|
42
|
+
* A composition rather than a wider {@link ResolvedStoreRun}, because `stats` shares
|
|
43
|
+
* the store leg and has no hash — a field that is meaningless for half its users is
|
|
44
|
+
* how a shared struct starts lying.
|
|
45
|
+
*/
|
|
46
|
+
export interface ResolvedRetrieveRun {
|
|
47
|
+
readonly store: ResolvedStoreRun;
|
|
48
|
+
readonly hash: string;
|
|
49
|
+
}
|
|
50
|
+
export declare const retrieveCommand: Subcommand<RetrieveInvocation, ResolvedRetrieveRun>;
|
|
51
|
+
/**
|
|
52
|
+
* The store leg alone, for the two commands whose entire job is the store between
|
|
53
|
+
* runs. The refusal is the point: `retrieve` exists so the marker's
|
|
54
|
+
* `retrieve("hash")` works from a later shell — cross-run retrieval — and a memory
|
|
55
|
+
* store dies with the process that filled it, so with a memory store (or no config
|
|
56
|
+
* at all) there is nothing those commands could honestly read. Answering with
|
|
57
|
+
* `UnknownHashError` or all-zero stats instead would be the quiet wrong answer this
|
|
58
|
+
* project refuses everywhere: the hash *was* elided, the counters *did* move — in a
|
|
59
|
+
* store that no longer exists.
|
|
60
|
+
*
|
|
61
|
+
* **This is a policy, not a law, which is why it stays here and is not exported.** The
|
|
62
|
+
* shared half — a config's store decision (`configuredStore`) and opening it
|
|
63
|
+
* (`openStore` in `ops/inputs.ts`) — is what every consumer needs and now what every
|
|
64
|
+
* consumer imports. This function is the CLI's ruling *on top of* that decision, and
|
|
65
|
+
* the MCP server deliberately rules the other way: it accepts a memory store, serves
|
|
66
|
+
* the whole session from it, and says how to get persistence at the moment an unknown
|
|
67
|
+
* hash makes the difference visible. Exporting this would offer that server the CLI's
|
|
68
|
+
* refusal wearing the name of a shared law — the fork this seam exists to end, running
|
|
69
|
+
* in the other direction.
|
|
70
|
+
*
|
|
71
|
+
* @throws {CliUsageError} when no config exists, or the configured store is memory.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveStoreRun(command: 'retrieve' | 'stats', config: LoadedConfig | undefined): ResolvedStoreRun;
|
|
74
|
+
//# sourceMappingURL=retrieve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retrieve.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/retrieve.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAKjD,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;;;;;;;GAcG;AAEH;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,eAAe,EAAE,UAAU,CAAC,kBAAkB,EAAE,mBAAmB,CAgD/E,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,UAAU,GAAG,OAAO,EAC7B,MAAM,EAAE,YAAY,GAAG,SAAS,GAC/B,gBAAgB,CAgBlB","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { openStore } from '../../ops/inputs.ts';\nimport { retrieveBytes } from '../../ops/verbs.ts';\nimport { CONFIG_FILE_NAME, configuredStore } from '../config.ts';\nimport type { LoadedConfig } from '../config.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt retrieve <hash>` — the marker's `retrieve(\"hash\")`, as a real command.\n *\n * The exact original bytes go to stdout and **nothing else does**: no report, no\n * trailing newline the store did not hold, no re-encoding. Trailing-newline fidelity\n * is not pedantry — the retrieved bytes get spliced back into reasoning about the\n * original, and an almost-right blob is the silent wrong answer this project refuses.\n *\n * The retrieval IS counted — that is the point. `store.retrieve()` journals the hit,\n * so an agent working from pure shell moves the same `expansionRate` a tool-calling\n * consumer moves, and over-pruning stays visible whichever loop is in use. Errors are\n * the store's own, verbatim: `UnknownHashError` for a hash never elided,\n * `StoreCorruptionError` for bytes that no longer hash to their name — distinct\n * texts, both exiting {@link EXIT.refused}.\n */\n\n/**\n * `smelt retrieve <hash>` — parsed. A sibling shape, not a field on somebody else's:\n * nothing but the hash, because the command's whole contract is \"hash in, exact bytes\n * out\" — the same contract as the `smelt_retrieve` tool, reachable from a shell.\n */\nexport interface RetrieveInvocation {\n readonly mode: 'retrieve';\n /** The hash exactly as the marker printed it. Validated by the store, not here. */\n readonly hash: string;\n}\n\n/**\n * Everything `smelt retrieve` and `smelt stats` need from the config, fully merged:\n * the persistent store's directory, already resolved against the config file. The\n * third `Resolved*Run`, and the only one two verbs share — because they share the\n * whole of it: no budget, no strategy, no file leg at all, only the store leg, which\n * is config-only. `stats` resolves through {@link resolveStoreRun} rather than\n * restating it.\n */\nexport interface ResolvedStoreRun {\n /** Absolute path of the directory store, resolved against the config file. */\n readonly storePath: string;\n}\n\n/**\n * What `retrieve` runs on: the shared store leg, plus the hash this invocation named.\n * A composition rather than a wider {@link ResolvedStoreRun}, because `stats` shares\n * the store leg and has no hash — a field that is meaningless for half its users is\n * how a shared struct starts lying.\n */\nexport interface ResolvedRetrieveRun {\n readonly store: ResolvedStoreRun;\n readonly hash: string;\n}\n\nexport const retrieveCommand: Subcommand<RetrieveInvocation, ResolvedRetrieveRun> = {\n name: 'retrieve',\n flags: [],\n refusal:\n `retrieve prints the exact original bytes for one hash, nothing else — even ` +\n `--json would wrap what must come back verbatim.`,\n usage: {\n synopsis: ['retrieve <hash>'],\n section: {\n heading: 'RETRIEVE & STATS',\n body:\n ` Every marker carries the hash of the bytes it replaced — <<smelt/v1: … —\\n` +\n ` retrieve(\"hash\")>> — and the marker's retrieve(\"hash\") is this command:\\n` +\n ` ${CLI_NAME} retrieve <hash> prints the exact original bytes on stdout, byte for\\n` +\n ` byte, nothing else. That closes the loop from pure shell: an agent that got a\\n` +\n ` marker asks for the bytes back with a command instead of a tool call, and the\\n` +\n ` retrieval is counted — asking for material back is exactly what the expansion\\n` +\n ` rate measures. An unknown hash and damaged bytes are distinct refusals (exit 3):\\n` +\n ` \"never elided\" and \"the store was corrupted\" call for different responses.`,\n },\n },\n\n /**\n * The hash and nothing else. Every flag is refused by the registry's one generated\n * message rather than ignored — the command prints the exact original bytes on\n * stdout and nothing more, so a flag that changed the output would break the one\n * contract it has, and a flag silently dropped would be a setting the user believed\n * was in force.\n */\n parse(_values: FlagValues, positionals: readonly string[]): RetrieveInvocation {\n if (positionals.length !== 2) {\n throw new CliUsageError(\n `${CLI_NAME}: retrieve needs exactly one hash — the one a marker printed.\\n` +\n ` ${CLI_NAME} retrieve 84998967370f38bc`,\n );\n }\n return { mode: 'retrieve', hash: positionals[1]! };\n },\n\n resolve(invocation: RetrieveInvocation, config: ConfigSource): ResolvedRetrieveRun {\n return { store: resolveStoreRun('retrieve', config()), hash: invocation.hash };\n },\n\n run(resolved: ResolvedRetrieveRun, io: CliIo): number {\n const store = openStore({ kind: 'directory', path: resolved.store.storePath });\n io.stdout(retrieveBytes({ store, hash: resolved.hash }));\n return EXIT.ok;\n },\n};\n\n/**\n * The store leg alone, for the two commands whose entire job is the store between\n * runs. The refusal is the point: `retrieve` exists so the marker's\n * `retrieve(\"hash\")` works from a later shell — cross-run retrieval — and a memory\n * store dies with the process that filled it, so with a memory store (or no config\n * at all) there is nothing those commands could honestly read. Answering with\n * `UnknownHashError` or all-zero stats instead would be the quiet wrong answer this\n * project refuses everywhere: the hash *was* elided, the counters *did* move — in a\n * store that no longer exists.\n *\n * **This is a policy, not a law, which is why it stays here and is not exported.** The\n * shared half — a config's store decision (`configuredStore`) and opening it\n * (`openStore` in `ops/inputs.ts`) — is what every consumer needs and now what every\n * consumer imports. This function is the CLI's ruling *on top of* that decision, and\n * the MCP server deliberately rules the other way: it accepts a memory store, serves\n * the whole session from it, and says how to get persistence at the moment an unknown\n * hash makes the difference visible. Exporting this would offer that server the CLI's\n * refusal wearing the name of a shared law — the fork this seam exists to end, running\n * in the other direction.\n *\n * @throws {CliUsageError} when no config exists, or the configured store is memory.\n */\nexport function resolveStoreRun(\n command: 'retrieve' | 'stats',\n config: LoadedConfig | undefined,\n): ResolvedStoreRun {\n const store = configuredStore(config);\n if (store.kind !== 'directory') {\n const state =\n config === undefined\n ? `there is no ${CONFIG_FILE_NAME} here`\n : `the ${CONFIG_FILE_NAME} at ${config.path} uses a memory store`;\n throw new CliUsageError(\n `${CLI_NAME}: ${command} needs a persistent store, and ${state}. Cross-run ` +\n `retrieval is the point of the store: a memory store dies with the process ` +\n `that made it, so a marker's hash from an earlier run names bytes this run ` +\n `never held. Configure {\"store\": {\"kind\": \"directory\", \"path\": …}} in ` +\n `${CONFIG_FILE_NAME} — \\`${CLI_NAME} init\\` writes one.`,\n );\n }\n return { storePath: store.path };\n}\n"]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { CliUsageError } from '../../errors.js';
|
|
2
|
+
import { openStore } from '../../ops/inputs.js';
|
|
3
|
+
import { retrieveBytes } from '../../ops/verbs.js';
|
|
4
|
+
import { CONFIG_FILE_NAME, configuredStore } from '../config.js';
|
|
5
|
+
import { CLI_NAME, EXIT } from '../shell.js';
|
|
6
|
+
export const retrieveCommand = {
|
|
7
|
+
name: 'retrieve',
|
|
8
|
+
flags: [],
|
|
9
|
+
refusal: `retrieve prints the exact original bytes for one hash, nothing else — even ` +
|
|
10
|
+
`--json would wrap what must come back verbatim.`,
|
|
11
|
+
usage: {
|
|
12
|
+
synopsis: ['retrieve <hash>'],
|
|
13
|
+
section: {
|
|
14
|
+
heading: 'RETRIEVE & STATS',
|
|
15
|
+
body: ` Every marker carries the hash of the bytes it replaced — <<smelt/v1: … —\n` +
|
|
16
|
+
` retrieve("hash")>> — and the marker's retrieve("hash") is this command:\n` +
|
|
17
|
+
` ${CLI_NAME} retrieve <hash> prints the exact original bytes on stdout, byte for\n` +
|
|
18
|
+
` byte, nothing else. That closes the loop from pure shell: an agent that got a\n` +
|
|
19
|
+
` marker asks for the bytes back with a command instead of a tool call, and the\n` +
|
|
20
|
+
` retrieval is counted — asking for material back is exactly what the expansion\n` +
|
|
21
|
+
` rate measures. An unknown hash and damaged bytes are distinct refusals (exit 3):\n` +
|
|
22
|
+
` "never elided" and "the store was corrupted" call for different responses.`,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
/**
|
|
26
|
+
* The hash and nothing else. Every flag is refused by the registry's one generated
|
|
27
|
+
* message rather than ignored — the command prints the exact original bytes on
|
|
28
|
+
* stdout and nothing more, so a flag that changed the output would break the one
|
|
29
|
+
* contract it has, and a flag silently dropped would be a setting the user believed
|
|
30
|
+
* was in force.
|
|
31
|
+
*/
|
|
32
|
+
parse(_values, positionals) {
|
|
33
|
+
if (positionals.length !== 2) {
|
|
34
|
+
throw new CliUsageError(`${CLI_NAME}: retrieve needs exactly one hash — the one a marker printed.\n` +
|
|
35
|
+
` ${CLI_NAME} retrieve 84998967370f38bc`);
|
|
36
|
+
}
|
|
37
|
+
return { mode: 'retrieve', hash: positionals[1] };
|
|
38
|
+
},
|
|
39
|
+
resolve(invocation, config) {
|
|
40
|
+
return { store: resolveStoreRun('retrieve', config()), hash: invocation.hash };
|
|
41
|
+
},
|
|
42
|
+
run(resolved, io) {
|
|
43
|
+
const store = openStore({ kind: 'directory', path: resolved.store.storePath });
|
|
44
|
+
io.stdout(retrieveBytes({ store, hash: resolved.hash }));
|
|
45
|
+
return EXIT.ok;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The store leg alone, for the two commands whose entire job is the store between
|
|
50
|
+
* runs. The refusal is the point: `retrieve` exists so the marker's
|
|
51
|
+
* `retrieve("hash")` works from a later shell — cross-run retrieval — and a memory
|
|
52
|
+
* store dies with the process that filled it, so with a memory store (or no config
|
|
53
|
+
* at all) there is nothing those commands could honestly read. Answering with
|
|
54
|
+
* `UnknownHashError` or all-zero stats instead would be the quiet wrong answer this
|
|
55
|
+
* project refuses everywhere: the hash *was* elided, the counters *did* move — in a
|
|
56
|
+
* store that no longer exists.
|
|
57
|
+
*
|
|
58
|
+
* **This is a policy, not a law, which is why it stays here and is not exported.** The
|
|
59
|
+
* shared half — a config's store decision (`configuredStore`) and opening it
|
|
60
|
+
* (`openStore` in `ops/inputs.ts`) — is what every consumer needs and now what every
|
|
61
|
+
* consumer imports. This function is the CLI's ruling *on top of* that decision, and
|
|
62
|
+
* the MCP server deliberately rules the other way: it accepts a memory store, serves
|
|
63
|
+
* the whole session from it, and says how to get persistence at the moment an unknown
|
|
64
|
+
* hash makes the difference visible. Exporting this would offer that server the CLI's
|
|
65
|
+
* refusal wearing the name of a shared law — the fork this seam exists to end, running
|
|
66
|
+
* in the other direction.
|
|
67
|
+
*
|
|
68
|
+
* @throws {CliUsageError} when no config exists, or the configured store is memory.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveStoreRun(command, config) {
|
|
71
|
+
const store = configuredStore(config);
|
|
72
|
+
if (store.kind !== 'directory') {
|
|
73
|
+
const state = config === undefined
|
|
74
|
+
? `there is no ${CONFIG_FILE_NAME} here`
|
|
75
|
+
: `the ${CONFIG_FILE_NAME} at ${config.path} uses a memory store`;
|
|
76
|
+
throw new CliUsageError(`${CLI_NAME}: ${command} needs a persistent store, and ${state}. Cross-run ` +
|
|
77
|
+
`retrieval is the point of the store: a memory store dies with the process ` +
|
|
78
|
+
`that made it, so a marker's hash from an earlier run names bytes this run ` +
|
|
79
|
+
`never held. Configure {"store": {"kind": "directory", "path": …}} in ` +
|
|
80
|
+
`${CONFIG_FILE_NAME} — \`${CLI_NAME} init\` writes one.`);
|
|
81
|
+
}
|
|
82
|
+
return { storePath: store.path };
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=retrieve.js.map
|