@smeltjs/core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- 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/args.d.ts +16 -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 +40 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +55 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/hooks.d.ts +33 -38
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +140 -485
- 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.map +1 -1
- package/dist/cli/report.js +2 -2
- package/dist/cli/report.js.map +1 -1
- package/dist/cli/run.d.ts +25 -88
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +32 -341
- 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/flags.d.ts +141 -0
- package/dist/cli/subcommands/flags.d.ts.map +1 -0
- package/dist/cli/subcommands/flags.js +208 -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 +64 -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 +80 -0
- package/dist/cli/subcommands/registry.d.ts.map +1 -0
- package/dist/cli/subcommands/registry.js +138 -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 +91 -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 +59 -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 +46 -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 +71 -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 +184 -0
- package/dist/harness/profile.d.ts.map +1 -0
- package/dist/harness/profile.js +26 -0
- package/dist/harness/profile.js.map +1 -0
- package/dist/harness/registry.d.ts +51 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +87 -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 +33 -76
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -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/types.d.ts +35 -3
- 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,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's edge, in one module: the name people type, where its bytes come from and
|
|
3
|
+
* go, and the codes it hands back to the shell.
|
|
4
|
+
*
|
|
5
|
+
* These three used to live in `args.ts` and `run.ts` — the two modules that now read
|
|
6
|
+
* the subcommand registry. A verb file needs all three, so leaving them there would
|
|
7
|
+
* make `args.ts → subcommands/* → args.ts` a cycle, which is exactly the shape that
|
|
8
|
+
* once forced the `--harness` help list to be hand-typed (see
|
|
9
|
+
* `src/harness/registry.ts`). **This file imports nothing**, so every module under
|
|
10
|
+
* `cli/` can read it and nothing has to be written twice to avoid a loop.
|
|
11
|
+
*/
|
|
12
|
+
/** The command people type. Independent of the package name. */
|
|
13
|
+
export declare const CLI_NAME = "smelt";
|
|
14
|
+
/**
|
|
15
|
+
* Where a wizard's answers come from: lines of text arriving over time, and nothing
|
|
16
|
+
* more. `process.stdin` is one, a scripted `Readable.from([...])` in a test is
|
|
17
|
+
* another, an async generator is a third.
|
|
18
|
+
*
|
|
19
|
+
* **Stated structurally, never as `NodeJS.ReadableStream`.** A `.d.ts` that names an
|
|
20
|
+
* ambient namespace only typechecks inside a compilation that pulled `@types/node`
|
|
21
|
+
* into its *global* scope, and a consumer building with `skipLibCheck: false` and a
|
|
22
|
+
* narrowed `types` array (or no `@types/node` at its own root — the ordinary case
|
|
23
|
+
* under pnpm) fails on smelt's declarations rather than on their own code. Naming the
|
|
24
|
+
* node type by import does not help: TypeScript resolves `node:stream` — and bare
|
|
25
|
+
* `stream` — only through the same globally-included `@types/node`. So the published
|
|
26
|
+
* surface describes the shape smelt actually consumes, which needs no node types at
|
|
27
|
+
* all, and {@link answerReader} is the one adapter that reads it.
|
|
28
|
+
* `test/guards/packaging.test.ts` holds the shipped declarations to it.
|
|
29
|
+
*/
|
|
30
|
+
export type AnswerStream = AsyncIterable<string | Uint8Array>;
|
|
31
|
+
/**
|
|
32
|
+
* One wizard's answers, one line at a time, plus the release that ends the process.
|
|
33
|
+
*
|
|
34
|
+
* `release` is not housekeeping — it is the difference between a wizard that exits and
|
|
35
|
+
* one the user has to Ctrl-C. See {@link answerReader}.
|
|
36
|
+
*/
|
|
37
|
+
export interface AnswerReader {
|
|
38
|
+
/** The next line, without its terminator, or `undefined` once input has ended. */
|
|
39
|
+
next(): Promise<string | undefined>;
|
|
40
|
+
/** Stop reading and let go of the source, so nothing it owns keeps the loop alive. */
|
|
41
|
+
release(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Read {@link AnswerStream} as lines — and, when the wizard is done, **let go of it**.
|
|
45
|
+
*
|
|
46
|
+
* This exists because the obvious adapter is a trap. Wrapping the answer stream in
|
|
47
|
+
* `Readable.from(...)` and handing that to `readline` reads the source through *its*
|
|
48
|
+
* async iterator, and closing the readline interface or destroying the wrapper ends
|
|
49
|
+
* only the wrapper: the source is left mid-`next()`, still subscribed, still holding
|
|
50
|
+
* its handle. On the real CLI that source is `process.stdin`, so `smelt init` wrote
|
|
51
|
+
* every file, printed `Done.` and then sat there forever — a hang only visible on an
|
|
52
|
+
* open pipe or a TTY, because EOF happens to end the iteration by itself, and EOF is
|
|
53
|
+
* what every scripted test hands it.
|
|
54
|
+
*
|
|
55
|
+
* So the source's own iterator is held here and nothing else touches it. `release`
|
|
56
|
+
* calls its `return()`, which is the contract an async iterable already has for "I am
|
|
57
|
+
* finished with you": `process.stdin`'s destroys the stream and unrefs the handle, an
|
|
58
|
+
* async generator runs its `finally`, and a plain array iterator does nothing at all.
|
|
59
|
+
* Crucially `release` is called between reads, never during one — an iterator awaiting
|
|
60
|
+
* `next()` cannot be returned out of, which is the very state the `Readable.from`
|
|
61
|
+
* wrapper left the source in.
|
|
62
|
+
*
|
|
63
|
+
* Bytes are decoded as UTF-8 across chunk boundaries, so a multi-byte character split
|
|
64
|
+
* across two reads survives; `\r\n` and a final line with no terminator both behave as
|
|
65
|
+
* readline did.
|
|
66
|
+
*/
|
|
67
|
+
export declare function answerReader(input: AnswerStream): AnswerReader;
|
|
68
|
+
/**
|
|
69
|
+
* Exit codes, and why there are five of them.
|
|
70
|
+
*
|
|
71
|
+
* A CLI that returns 0 whatever happens is the shell-level version of a stub that
|
|
72
|
+
* returns `[]`: the caller cannot tell success from failure, so a pipeline built on it
|
|
73
|
+
* fails silently. **`overBudget` is the load-bearing one.** A plan that did not fit is
|
|
74
|
+
* not an error — smelt refused to cut the regions the caller asked to keep, which is
|
|
75
|
+
* correct — but it is also not success, and a script must be able to see the
|
|
76
|
+
* difference without parsing prose.
|
|
77
|
+
*/
|
|
78
|
+
export declare const EXIT: {
|
|
79
|
+
readonly ok: 0;
|
|
80
|
+
readonly overBudget: 1;
|
|
81
|
+
readonly usage: 2;
|
|
82
|
+
readonly refused: 3;
|
|
83
|
+
readonly unexpected: 4;
|
|
84
|
+
};
|
|
85
|
+
/** Where the CLI's bytes come from and go. Injected so `runCli` is testable in-process. */
|
|
86
|
+
export interface CliIo {
|
|
87
|
+
readonly stdout: (text: string) => void;
|
|
88
|
+
readonly stderr: (text: string) => void;
|
|
89
|
+
/** All of stdin, as UTF-8. @throws {CliUsageError} when nothing is piped. */
|
|
90
|
+
readonly stdin: () => string;
|
|
91
|
+
/** The package version, for `--version`. */
|
|
92
|
+
readonly version: string;
|
|
93
|
+
/**
|
|
94
|
+
* Where `smelt.config.json` discovery starts, and where `init` writes. Defaults to
|
|
95
|
+
* the process working directory; tests pass a temp directory to stay hermetic.
|
|
96
|
+
*/
|
|
97
|
+
readonly cwd?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Interactive input for `smelt init` — the wizard reads answers line by line, which
|
|
100
|
+
* the one-shot `stdin()` above cannot provide. `bin.ts` passes the real stdin
|
|
101
|
+
* stream; tests pass a scripted one. Absent means `init` is a usage error.
|
|
102
|
+
* See {@link AnswerStream} for why the type is structural.
|
|
103
|
+
*/
|
|
104
|
+
readonly initInput?: AnswerStream;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=shell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/cli/shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,gEAAgE;AAChE,eAAO,MAAM,QAAQ,UAAU,CAAC;AAEhC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,kFAAkF;IAClF,IAAI,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACpC,sFAAsF;IACtF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CA2C9D;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI;aACf,EAAE,EAAE,CAAC;aACL,UAAU,EAAE,CAAC;aACb,KAAK,EAAE,CAAC;aACR,OAAO,EAAE,CAAC;aACV,UAAU,EAAE,CAAC;CACL,CAAC;AAEX,2FAA2F;AAC3F,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC;IAC7B,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,YAAY,CAAC;CACnC","sourcesContent":["/**\n * The CLI's edge, in one module: the name people type, where its bytes come from and\n * go, and the codes it hands back to the shell.\n *\n * These three used to live in `args.ts` and `run.ts` — the two modules that now read\n * the subcommand registry. A verb file needs all three, so leaving them there would\n * make `args.ts → subcommands/* → args.ts` a cycle, which is exactly the shape that\n * once forced the `--harness` help list to be hand-typed (see\n * `src/harness/registry.ts`). **This file imports nothing**, so every module under\n * `cli/` can read it and nothing has to be written twice to avoid a loop.\n */\n\n/** The command people type. Independent of the package name. */\nexport const CLI_NAME = 'smelt';\n\n/**\n * Where a wizard's answers come from: lines of text arriving over time, and nothing\n * more. `process.stdin` is one, a scripted `Readable.from([...])` in a test is\n * another, an async generator is a third.\n *\n * **Stated structurally, never as `NodeJS.ReadableStream`.** A `.d.ts` that names an\n * ambient namespace only typechecks inside a compilation that pulled `@types/node`\n * into its *global* scope, and a consumer building with `skipLibCheck: false` and a\n * narrowed `types` array (or no `@types/node` at its own root — the ordinary case\n * under pnpm) fails on smelt's declarations rather than on their own code. Naming the\n * node type by import does not help: TypeScript resolves `node:stream` — and bare\n * `stream` — only through the same globally-included `@types/node`. So the published\n * surface describes the shape smelt actually consumes, which needs no node types at\n * all, and {@link answerReader} is the one adapter that reads it.\n * `test/guards/packaging.test.ts` holds the shipped declarations to it.\n */\nexport type AnswerStream = AsyncIterable<string | Uint8Array>;\n\n/**\n * One wizard's answers, one line at a time, plus the release that ends the process.\n *\n * `release` is not housekeeping — it is the difference between a wizard that exits and\n * one the user has to Ctrl-C. See {@link answerReader}.\n */\nexport interface AnswerReader {\n /** The next line, without its terminator, or `undefined` once input has ended. */\n next(): Promise<string | undefined>;\n /** Stop reading and let go of the source, so nothing it owns keeps the loop alive. */\n release(): Promise<void>;\n}\n\n/**\n * Read {@link AnswerStream} as lines — and, when the wizard is done, **let go of it**.\n *\n * This exists because the obvious adapter is a trap. Wrapping the answer stream in\n * `Readable.from(...)` and handing that to `readline` reads the source through *its*\n * async iterator, and closing the readline interface or destroying the wrapper ends\n * only the wrapper: the source is left mid-`next()`, still subscribed, still holding\n * its handle. On the real CLI that source is `process.stdin`, so `smelt init` wrote\n * every file, printed `Done.` and then sat there forever — a hang only visible on an\n * open pipe or a TTY, because EOF happens to end the iteration by itself, and EOF is\n * what every scripted test hands it.\n *\n * So the source's own iterator is held here and nothing else touches it. `release`\n * calls its `return()`, which is the contract an async iterable already has for \"I am\n * finished with you\": `process.stdin`'s destroys the stream and unrefs the handle, an\n * async generator runs its `finally`, and a plain array iterator does nothing at all.\n * Crucially `release` is called between reads, never during one — an iterator awaiting\n * `next()` cannot be returned out of, which is the very state the `Readable.from`\n * wrapper left the source in.\n *\n * Bytes are decoded as UTF-8 across chunk boundaries, so a multi-byte character split\n * across two reads survives; `\\r\\n` and a final line with no terminator both behave as\n * readline did.\n */\nexport function answerReader(input: AnswerStream): AnswerReader {\n const iterator = input[Symbol.asyncIterator]();\n const decoder = new TextDecoder('utf-8');\n let pending = '';\n let ended = false;\n\n const takeLine = (): string | undefined => {\n const newline = pending.indexOf('\\n');\n if (newline === -1) return undefined;\n const line = pending.slice(0, newline);\n pending = pending.slice(newline + 1);\n return line.endsWith('\\r') ? line.slice(0, -1) : line;\n };\n\n return {\n async next(): Promise<string | undefined> {\n for (;;) {\n const line = takeLine();\n if (line !== undefined) return line;\n if (ended) {\n if (pending === '') return undefined;\n const last = pending;\n pending = '';\n return last;\n }\n const step = await iterator.next();\n if (step.done === true) {\n ended = true;\n pending += decoder.decode(); // flush a truncated multi-byte sequence\n continue;\n }\n pending +=\n typeof step.value === 'string'\n ? step.value\n : decoder.decode(step.value, { stream: true });\n }\n },\n async release(): Promise<void> {\n ended = true;\n pending = '';\n await iterator.return?.();\n },\n };\n}\n\n/**\n * Exit codes, and why there are five of them.\n *\n * A CLI that returns 0 whatever happens is the shell-level version of a stub that\n * returns `[]`: the caller cannot tell success from failure, so a pipeline built on it\n * fails silently. **`overBudget` is the load-bearing one.** A plan that did not fit is\n * not an error — smelt refused to cut the regions the caller asked to keep, which is\n * correct — but it is also not success, and a script must be able to see the\n * difference without parsing prose.\n */\nexport const EXIT = {\n ok: 0,\n overBudget: 1,\n usage: 2,\n refused: 3,\n unexpected: 4,\n} as const;\n\n/** Where the CLI's bytes come from and go. Injected so `runCli` is testable in-process. */\nexport interface CliIo {\n readonly stdout: (text: string) => void;\n readonly stderr: (text: string) => void;\n /** All of stdin, as UTF-8. @throws {CliUsageError} when nothing is piped. */\n readonly stdin: () => string;\n /** The package version, for `--version`. */\n readonly version: string;\n /**\n * Where `smelt.config.json` discovery starts, and where `init` writes. Defaults to\n * the process working directory; tests pass a temp directory to stay hermetic.\n */\n readonly cwd?: string;\n /**\n * Interactive input for `smelt init` — the wizard reads answers line by line, which\n * the one-shot `stdin()` above cannot provide. `bin.ts` passes the real stdin\n * stream; tests pass a scripted one. Absent means `init` is a usage error.\n * See {@link AnswerStream} for why the type is structural.\n */\n readonly initInput?: AnswerStream;\n}\n"]}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's edge, in one module: the name people type, where its bytes come from and
|
|
3
|
+
* go, and the codes it hands back to the shell.
|
|
4
|
+
*
|
|
5
|
+
* These three used to live in `args.ts` and `run.ts` — the two modules that now read
|
|
6
|
+
* the subcommand registry. A verb file needs all three, so leaving them there would
|
|
7
|
+
* make `args.ts → subcommands/* → args.ts` a cycle, which is exactly the shape that
|
|
8
|
+
* once forced the `--harness` help list to be hand-typed (see
|
|
9
|
+
* `src/harness/registry.ts`). **This file imports nothing**, so every module under
|
|
10
|
+
* `cli/` can read it and nothing has to be written twice to avoid a loop.
|
|
11
|
+
*/
|
|
12
|
+
/** The command people type. Independent of the package name. */
|
|
13
|
+
export const CLI_NAME = 'smelt';
|
|
14
|
+
/**
|
|
15
|
+
* Read {@link AnswerStream} as lines — and, when the wizard is done, **let go of it**.
|
|
16
|
+
*
|
|
17
|
+
* This exists because the obvious adapter is a trap. Wrapping the answer stream in
|
|
18
|
+
* `Readable.from(...)` and handing that to `readline` reads the source through *its*
|
|
19
|
+
* async iterator, and closing the readline interface or destroying the wrapper ends
|
|
20
|
+
* only the wrapper: the source is left mid-`next()`, still subscribed, still holding
|
|
21
|
+
* its handle. On the real CLI that source is `process.stdin`, so `smelt init` wrote
|
|
22
|
+
* every file, printed `Done.` and then sat there forever — a hang only visible on an
|
|
23
|
+
* open pipe or a TTY, because EOF happens to end the iteration by itself, and EOF is
|
|
24
|
+
* what every scripted test hands it.
|
|
25
|
+
*
|
|
26
|
+
* So the source's own iterator is held here and nothing else touches it. `release`
|
|
27
|
+
* calls its `return()`, which is the contract an async iterable already has for "I am
|
|
28
|
+
* finished with you": `process.stdin`'s destroys the stream and unrefs the handle, an
|
|
29
|
+
* async generator runs its `finally`, and a plain array iterator does nothing at all.
|
|
30
|
+
* Crucially `release` is called between reads, never during one — an iterator awaiting
|
|
31
|
+
* `next()` cannot be returned out of, which is the very state the `Readable.from`
|
|
32
|
+
* wrapper left the source in.
|
|
33
|
+
*
|
|
34
|
+
* Bytes are decoded as UTF-8 across chunk boundaries, so a multi-byte character split
|
|
35
|
+
* across two reads survives; `\r\n` and a final line with no terminator both behave as
|
|
36
|
+
* readline did.
|
|
37
|
+
*/
|
|
38
|
+
export function answerReader(input) {
|
|
39
|
+
const iterator = input[Symbol.asyncIterator]();
|
|
40
|
+
const decoder = new TextDecoder('utf-8');
|
|
41
|
+
let pending = '';
|
|
42
|
+
let ended = false;
|
|
43
|
+
const takeLine = () => {
|
|
44
|
+
const newline = pending.indexOf('\n');
|
|
45
|
+
if (newline === -1)
|
|
46
|
+
return undefined;
|
|
47
|
+
const line = pending.slice(0, newline);
|
|
48
|
+
pending = pending.slice(newline + 1);
|
|
49
|
+
return line.endsWith('\r') ? line.slice(0, -1) : line;
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
async next() {
|
|
53
|
+
for (;;) {
|
|
54
|
+
const line = takeLine();
|
|
55
|
+
if (line !== undefined)
|
|
56
|
+
return line;
|
|
57
|
+
if (ended) {
|
|
58
|
+
if (pending === '')
|
|
59
|
+
return undefined;
|
|
60
|
+
const last = pending;
|
|
61
|
+
pending = '';
|
|
62
|
+
return last;
|
|
63
|
+
}
|
|
64
|
+
const step = await iterator.next();
|
|
65
|
+
if (step.done === true) {
|
|
66
|
+
ended = true;
|
|
67
|
+
pending += decoder.decode(); // flush a truncated multi-byte sequence
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
pending +=
|
|
71
|
+
typeof step.value === 'string'
|
|
72
|
+
? step.value
|
|
73
|
+
: decoder.decode(step.value, { stream: true });
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
async release() {
|
|
77
|
+
ended = true;
|
|
78
|
+
pending = '';
|
|
79
|
+
await iterator.return?.();
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Exit codes, and why there are five of them.
|
|
85
|
+
*
|
|
86
|
+
* A CLI that returns 0 whatever happens is the shell-level version of a stub that
|
|
87
|
+
* returns `[]`: the caller cannot tell success from failure, so a pipeline built on it
|
|
88
|
+
* fails silently. **`overBudget` is the load-bearing one.** A plan that did not fit is
|
|
89
|
+
* not an error — smelt refused to cut the regions the caller asked to keep, which is
|
|
90
|
+
* correct — but it is also not success, and a script must be able to see the
|
|
91
|
+
* difference without parsing prose.
|
|
92
|
+
*/
|
|
93
|
+
export const EXIT = {
|
|
94
|
+
ok: 0,
|
|
95
|
+
overBudget: 1,
|
|
96
|
+
usage: 2,
|
|
97
|
+
refused: 3,
|
|
98
|
+
unexpected: 4,
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/cli/shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,gEAAgE;AAChE,MAAM,CAAC,MAAM,QAAQ,GAAG,OAAO,CAAC;AAiChC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,YAAY,CAAC,KAAmB;IAC9C,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC;IAC/C,MAAM,OAAO,GAAG,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,KAAK,GAAG,KAAK,CAAC;IAElB,MAAM,QAAQ,GAAG,GAAuB,EAAE;QACxC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,OAAO,KAAK,CAAC,CAAC;YAAE,OAAO,SAAS,CAAC;QACrC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QACvC,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;QACrC,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxD,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,CAAC,IAAI;YACR,SAAS,CAAC;gBACR,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;gBACxB,IAAI,IAAI,KAAK,SAAS;oBAAE,OAAO,IAAI,CAAC;gBACpC,IAAI,KAAK,EAAE,CAAC;oBACV,IAAI,OAAO,KAAK,EAAE;wBAAE,OAAO,SAAS,CAAC;oBACrC,MAAM,IAAI,GAAG,OAAO,CAAC;oBACrB,OAAO,GAAG,EAAE,CAAC;oBACb,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACnC,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;oBACvB,KAAK,GAAG,IAAI,CAAC;oBACb,OAAO,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,wCAAwC;oBACrE,SAAS;gBACX,CAAC;gBACD,OAAO;oBACL,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;wBAC5B,CAAC,CAAC,IAAI,CAAC,KAAK;wBACZ,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YACrD,CAAC;QACH,CAAC;QACD,KAAK,CAAC,OAAO;YACX,KAAK,GAAG,IAAI,CAAC;YACb,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;QAC5B,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,EAAE,EAAE,CAAC;IACL,UAAU,EAAE,CAAC;IACb,KAAK,EAAE,CAAC;IACR,OAAO,EAAE,CAAC;IACV,UAAU,EAAE,CAAC;CACL,CAAC","sourcesContent":["/**\n * The CLI's edge, in one module: the name people type, where its bytes come from and\n * go, and the codes it hands back to the shell.\n *\n * These three used to live in `args.ts` and `run.ts` — the two modules that now read\n * the subcommand registry. A verb file needs all three, so leaving them there would\n * make `args.ts → subcommands/* → args.ts` a cycle, which is exactly the shape that\n * once forced the `--harness` help list to be hand-typed (see\n * `src/harness/registry.ts`). **This file imports nothing**, so every module under\n * `cli/` can read it and nothing has to be written twice to avoid a loop.\n */\n\n/** The command people type. Independent of the package name. */\nexport const CLI_NAME = 'smelt';\n\n/**\n * Where a wizard's answers come from: lines of text arriving over time, and nothing\n * more. `process.stdin` is one, a scripted `Readable.from([...])` in a test is\n * another, an async generator is a third.\n *\n * **Stated structurally, never as `NodeJS.ReadableStream`.** A `.d.ts` that names an\n * ambient namespace only typechecks inside a compilation that pulled `@types/node`\n * into its *global* scope, and a consumer building with `skipLibCheck: false` and a\n * narrowed `types` array (or no `@types/node` at its own root — the ordinary case\n * under pnpm) fails on smelt's declarations rather than on their own code. Naming the\n * node type by import does not help: TypeScript resolves `node:stream` — and bare\n * `stream` — only through the same globally-included `@types/node`. So the published\n * surface describes the shape smelt actually consumes, which needs no node types at\n * all, and {@link answerReader} is the one adapter that reads it.\n * `test/guards/packaging.test.ts` holds the shipped declarations to it.\n */\nexport type AnswerStream = AsyncIterable<string | Uint8Array>;\n\n/**\n * One wizard's answers, one line at a time, plus the release that ends the process.\n *\n * `release` is not housekeeping — it is the difference between a wizard that exits and\n * one the user has to Ctrl-C. See {@link answerReader}.\n */\nexport interface AnswerReader {\n /** The next line, without its terminator, or `undefined` once input has ended. */\n next(): Promise<string | undefined>;\n /** Stop reading and let go of the source, so nothing it owns keeps the loop alive. */\n release(): Promise<void>;\n}\n\n/**\n * Read {@link AnswerStream} as lines — and, when the wizard is done, **let go of it**.\n *\n * This exists because the obvious adapter is a trap. Wrapping the answer stream in\n * `Readable.from(...)` and handing that to `readline` reads the source through *its*\n * async iterator, and closing the readline interface or destroying the wrapper ends\n * only the wrapper: the source is left mid-`next()`, still subscribed, still holding\n * its handle. On the real CLI that source is `process.stdin`, so `smelt init` wrote\n * every file, printed `Done.` and then sat there forever — a hang only visible on an\n * open pipe or a TTY, because EOF happens to end the iteration by itself, and EOF is\n * what every scripted test hands it.\n *\n * So the source's own iterator is held here and nothing else touches it. `release`\n * calls its `return()`, which is the contract an async iterable already has for \"I am\n * finished with you\": `process.stdin`'s destroys the stream and unrefs the handle, an\n * async generator runs its `finally`, and a plain array iterator does nothing at all.\n * Crucially `release` is called between reads, never during one — an iterator awaiting\n * `next()` cannot be returned out of, which is the very state the `Readable.from`\n * wrapper left the source in.\n *\n * Bytes are decoded as UTF-8 across chunk boundaries, so a multi-byte character split\n * across two reads survives; `\\r\\n` and a final line with no terminator both behave as\n * readline did.\n */\nexport function answerReader(input: AnswerStream): AnswerReader {\n const iterator = input[Symbol.asyncIterator]();\n const decoder = new TextDecoder('utf-8');\n let pending = '';\n let ended = false;\n\n const takeLine = (): string | undefined => {\n const newline = pending.indexOf('\\n');\n if (newline === -1) return undefined;\n const line = pending.slice(0, newline);\n pending = pending.slice(newline + 1);\n return line.endsWith('\\r') ? line.slice(0, -1) : line;\n };\n\n return {\n async next(): Promise<string | undefined> {\n for (;;) {\n const line = takeLine();\n if (line !== undefined) return line;\n if (ended) {\n if (pending === '') return undefined;\n const last = pending;\n pending = '';\n return last;\n }\n const step = await iterator.next();\n if (step.done === true) {\n ended = true;\n pending += decoder.decode(); // flush a truncated multi-byte sequence\n continue;\n }\n pending +=\n typeof step.value === 'string'\n ? step.value\n : decoder.decode(step.value, { stream: true });\n }\n },\n async release(): Promise<void> {\n ended = true;\n pending = '';\n await iterator.return?.();\n },\n };\n}\n\n/**\n * Exit codes, and why there are five of them.\n *\n * A CLI that returns 0 whatever happens is the shell-level version of a stub that\n * returns `[]`: the caller cannot tell success from failure, so a pipeline built on it\n * fails silently. **`overBudget` is the load-bearing one.** A plan that did not fit is\n * not an error — smelt refused to cut the regions the caller asked to keep, which is\n * correct — but it is also not success, and a script must be able to see the\n * difference without parsing prose.\n */\nexport const EXIT = {\n ok: 0,\n overBudget: 1,\n usage: 2,\n refused: 3,\n unexpected: 4,\n} as const;\n\n/** Where the CLI's bytes come from and go. Injected so `runCli` is testable in-process. */\nexport interface CliIo {\n readonly stdout: (text: string) => void;\n readonly stderr: (text: string) => void;\n /** All of stdin, as UTF-8. @throws {CliUsageError} when nothing is piped. */\n readonly stdin: () => string;\n /** The package version, for `--version`. */\n readonly version: string;\n /**\n * Where `smelt.config.json` discovery starts, and where `init` writes. Defaults to\n * the process working directory; tests pass a temp directory to stay hermetic.\n */\n readonly cwd?: string;\n /**\n * Interactive input for `smelt init` — the wizard reads answers line by line, which\n * the one-shot `stdin()` above cannot provide. `bin.ts` passes the real stdin\n * stream; tests pass a scripted one. Absent means `init` is a usage error.\n * See {@link AnswerStream} for why the type is structural.\n */\n readonly initInput?: AnswerStream;\n}\n"]}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every flag the CLI accepts, once.
|
|
3
|
+
*
|
|
4
|
+
* The table is the companion the {@link Subcommand} registry needs: a verb declares
|
|
5
|
+
* the flags it owns as `FlagName[]`, so the type of that list — and therefore whether
|
|
6
|
+
* a verb can claim a flag that does not exist — comes from here. It carries three
|
|
7
|
+
* things that used to be written in three places:
|
|
8
|
+
*
|
|
9
|
+
* 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a
|
|
10
|
+
* literal inside `parseSmeltArgs`.
|
|
11
|
+
* 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is
|
|
12
|
+
* spelled once and every verb's ownership list typechecks against it.
|
|
13
|
+
* 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`
|
|
14
|
+
* prefix, which is *not* stored here: it is generated from the registry's
|
|
15
|
+
* ownership (see `renderOptions` in `cli/usage.ts`), because "which verb owns
|
|
16
|
+
* this flag" is a fact the registry already holds and prose that restates it is
|
|
17
|
+
* prose that can go stale.
|
|
18
|
+
*
|
|
19
|
+
* Key order is meaningful: it is the order the OPTIONS block renders and the order a
|
|
20
|
+
* refusal lists flags in, so keep it stable and append new flags where they read best.
|
|
21
|
+
*/
|
|
22
|
+
export declare const CLI_FLAGS: {
|
|
23
|
+
readonly budget: {
|
|
24
|
+
readonly type: 'string';
|
|
25
|
+
};
|
|
26
|
+
readonly focus: {
|
|
27
|
+
readonly type: 'string';
|
|
28
|
+
readonly multiple: true;
|
|
29
|
+
};
|
|
30
|
+
readonly language: {
|
|
31
|
+
readonly type: 'string';
|
|
32
|
+
};
|
|
33
|
+
readonly strategy: {
|
|
34
|
+
readonly type: 'string';
|
|
35
|
+
};
|
|
36
|
+
readonly ignore: {
|
|
37
|
+
readonly type: 'string';
|
|
38
|
+
readonly multiple: true;
|
|
39
|
+
};
|
|
40
|
+
readonly cache: {
|
|
41
|
+
readonly type: 'string';
|
|
42
|
+
};
|
|
43
|
+
readonly harness: {
|
|
44
|
+
readonly type: 'string';
|
|
45
|
+
};
|
|
46
|
+
readonly json: {
|
|
47
|
+
readonly type: 'boolean';
|
|
48
|
+
};
|
|
49
|
+
readonly reconstruct: {
|
|
50
|
+
readonly type: 'boolean';
|
|
51
|
+
};
|
|
52
|
+
readonly help: {
|
|
53
|
+
readonly type: 'boolean';
|
|
54
|
+
readonly short: 'h';
|
|
55
|
+
};
|
|
56
|
+
readonly version: {
|
|
57
|
+
readonly type: 'boolean';
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/** Every flag name, as a type. A verb cannot claim a flag that is not in the table. */
|
|
61
|
+
export type FlagName = keyof typeof CLI_FLAGS;
|
|
62
|
+
/**
|
|
63
|
+
* The flags answered *before* any verb, so no verb owns them and no verb may refuse
|
|
64
|
+
* them: `smelt map --help` prints the help, exactly as it always has. Every other flag
|
|
65
|
+
* belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.
|
|
66
|
+
*/
|
|
67
|
+
export declare const GLOBAL_FLAGS: readonly ["help", "version"];
|
|
68
|
+
/** A flag a verb can own — everything but the two global ones. */
|
|
69
|
+
export type VerbFlag = Exclude<FlagName, (typeof GLOBAL_FLAGS)[number]>;
|
|
70
|
+
/** Every ownable flag, in table order: the list a refusal checks a verb against. */
|
|
71
|
+
export declare const VERB_FLAGS: readonly VerbFlag[];
|
|
72
|
+
/**
|
|
73
|
+
* `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.
|
|
74
|
+
*
|
|
75
|
+
* Every refusal that names more than one flag spells the list this way: the
|
|
76
|
+
* ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`
|
|
77
|
+
* refusal in `./smelt.ts`. It lives with the table because the *order* is the
|
|
78
|
+
* table's — a refusal that listed flags in the order the user happened to type them
|
|
79
|
+
* would read differently every time, and two refusals disagreeing about how to spell
|
|
80
|
+
* the same pair is the kind of drift this file exists to remove.
|
|
81
|
+
*/
|
|
82
|
+
export declare function flagList(flags: readonly VerbFlag[]): string;
|
|
83
|
+
/** What one flag's parsed value looks like, derived from how `parseArgs` was told to read it. */
|
|
84
|
+
type FlagValue<F> = F extends {
|
|
85
|
+
readonly type: 'boolean';
|
|
86
|
+
} ? boolean : F extends {
|
|
87
|
+
readonly multiple: true;
|
|
88
|
+
} ? readonly string[] : string;
|
|
89
|
+
/**
|
|
90
|
+
* The parsed flags, as every `Subcommand.parse` sees them: one optional field per
|
|
91
|
+
* flag, typed by the table above. Absent means the user did not type it — which is
|
|
92
|
+
* the only thing a refusal needs to know, and the only thing a verb may act on.
|
|
93
|
+
*/
|
|
94
|
+
export type FlagValues = {
|
|
95
|
+
readonly [K in FlagName]?: FlagValue<(typeof CLI_FLAGS)[K]>;
|
|
96
|
+
};
|
|
97
|
+
/** How one flag appears in the OPTIONS block of `--help`. */
|
|
98
|
+
export interface FlagHelp {
|
|
99
|
+
/** The left column, e.g. `--budget <bytes>` or `-h, --help`. */
|
|
100
|
+
readonly label: string;
|
|
101
|
+
/**
|
|
102
|
+
* The description, already wrapped to the OPTIONS column — a function because three
|
|
103
|
+
* entries render a registry (`SUPPORTED_LANGUAGES`, `STRATEGIES` +
|
|
104
|
+
* `STRUCTURAL_LANGUAGES`, `HARNESS_IDS`) rather than a hand-typed list. The
|
|
105
|
+
* ownership prefix is not here; it is generated.
|
|
106
|
+
*/
|
|
107
|
+
body(): readonly string[];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the
|
|
111
|
+
* hand-typed version wrapped. `--strategy` and `--language` render their registries on
|
|
112
|
+
* one line because they fit; the harness ids do not, and a list long enough to wrap is
|
|
113
|
+
* exactly the list nobody keeps in sync by hand.
|
|
114
|
+
*/
|
|
115
|
+
export declare function optionList(items: readonly string[], width: number): readonly string[];
|
|
116
|
+
/**
|
|
117
|
+
* The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag
|
|
118
|
+
* added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text
|
|
119
|
+
* cannot fall behind what the parser accepts.
|
|
120
|
+
*/
|
|
121
|
+
export declare const FLAG_HELP: Readonly<Record<FlagName, FlagHelp>>;
|
|
122
|
+
/**
|
|
123
|
+
* `--budget` has no built-in default, for the same reason `smelt()` has none: a budget
|
|
124
|
+
* smelt invented would be smelt deciding how much of the caller's context to throw
|
|
125
|
+
* away, silently, at a number nobody chose. A *missing* flag is not an error here,
|
|
126
|
+
* though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose
|
|
127
|
+
* explicitly, and the verb's `resolve` errors only when neither exists. A malformed
|
|
128
|
+
* value is always an error.
|
|
129
|
+
*
|
|
130
|
+
* It lives with the flag rather than with a verb because two verbs own `--budget`, and
|
|
131
|
+
* the two of them agreeing on what "4kb" means is not something to leave to chance.
|
|
132
|
+
*
|
|
133
|
+
* The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a
|
|
134
|
+
* leading `-` are answered by a digits-only test before anything numeric happens. The
|
|
135
|
+
* *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also
|
|
136
|
+
* where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`
|
|
137
|
+
* here and `"budgetBytes"` there.
|
|
138
|
+
*/
|
|
139
|
+
export declare function parseBudget(raw: string | undefined): number | undefined;
|
|
140
|
+
export {};
|
|
141
|
+
//# sourceMappingURL=flags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flags.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/flags.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS;aACpB,MAAM;iBAAI,IAAI,EAAE,QAAQ;;aACxB,KAAK;iBAAI,IAAI,EAAE,QAAQ;iBAAE,QAAQ;;aACjC,QAAQ;iBAAI,IAAI,EAAE,QAAQ;;aAC1B,QAAQ;iBAAI,IAAI,EAAE,QAAQ;;aAC1B,MAAM;iBAAI,IAAI,EAAE,QAAQ;iBAAE,QAAQ;;aAClC,KAAK;iBAAI,IAAI,EAAE,QAAQ;;aACvB,OAAO;iBAAI,IAAI,EAAE,QAAQ;;aACzB,IAAI;iBAAI,IAAI,EAAE,SAAS;;aACvB,WAAW;iBAAI,IAAI,EAAE,SAAS;;aAC9B,IAAI;iBAAI,IAAI,EAAE,SAAS;iBAAE,KAAK,EAAE,GAAG;;aACnC,OAAO;iBAAI,IAAI,EAAE,SAAS;;CAClB,CAAC;AAEX,uFAAuF;AACvF,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,SAAS,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,YAAY,8BAA6D,CAAC;AAEvF,kEAAkE;AAClE,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAExE,oFAAoF;AACpF,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAEzC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,CAI3D;AAED,iGAAiG;AACjG,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACtD,OAAO,GACP,CAAC,SAAS;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GACnC,SAAS,MAAM,EAAE,GACjB,MAAM,CAAC;AAEb;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,EAAE,CAAC,IAAI,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEzF,6DAA6D;AAC7D,MAAM,WAAW,QAAQ;IACvB,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,IAAI,IAAI,SAAS,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAerF;AAKD;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CA8E1D,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOvE","sourcesContent":["import { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESS_IDS } from '../../harness/registry.ts';\nimport { budgetFault, budgetMalformed } from '../../ops/inputs.ts';\nimport type { BudgetFault } from '../../ops/inputs.ts';\nimport { STRATEGIES, DEFAULT_STRATEGY } from '../../plan/planners.ts';\nimport { STRUCTURAL_LANGUAGES } from '../../plan/structural.ts';\nimport { DEFAULT_REPO_IGNORE } from '../../repomap/map.ts';\nimport { CLI_NAME } from '../shell.ts';\n\n/**\n * Every flag the CLI accepts, once.\n *\n * The table is the companion the {@link Subcommand} registry needs: a verb declares\n * the flags it owns as `FlagName[]`, so the type of that list — and therefore whether\n * a verb can claim a flag that does not exist — comes from here. It carries three\n * things that used to be written in three places:\n *\n * 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a\n * literal inside `parseSmeltArgs`.\n * 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is\n * spelled once and every verb's ownership list typechecks against it.\n * 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`\n * prefix, which is *not* stored here: it is generated from the registry's\n * ownership (see `renderOptions` in `cli/usage.ts`), because \"which verb owns\n * this flag\" is a fact the registry already holds and prose that restates it is\n * prose that can go stale.\n *\n * Key order is meaningful: it is the order the OPTIONS block renders and the order a\n * refusal lists flags in, so keep it stable and append new flags where they read best.\n */\nexport const CLI_FLAGS = {\n budget: { type: 'string' },\n focus: { type: 'string', multiple: true },\n language: { type: 'string' },\n strategy: { type: 'string' },\n ignore: { type: 'string', multiple: true },\n cache: { type: 'string' },\n harness: { type: 'string' },\n json: { type: 'boolean' },\n reconstruct: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean' },\n} as const;\n\n/** Every flag name, as a type. A verb cannot claim a flag that is not in the table. */\nexport type FlagName = keyof typeof CLI_FLAGS;\n\n/**\n * The flags answered *before* any verb, so no verb owns them and no verb may refuse\n * them: `smelt map --help` prints the help, exactly as it always has. Every other flag\n * belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.\n */\nexport const GLOBAL_FLAGS = ['help', 'version'] as const satisfies readonly FlagName[];\n\n/** A flag a verb can own — everything but the two global ones. */\nexport type VerbFlag = Exclude<FlagName, (typeof GLOBAL_FLAGS)[number]>;\n\n/** Every ownable flag, in table order: the list a refusal checks a verb against. */\nexport const VERB_FLAGS: readonly VerbFlag[] = Object.keys(CLI_FLAGS).filter(\n (name): name is VerbFlag => !(GLOBAL_FLAGS as readonly string[]).includes(name),\n);\n\n/**\n * `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.\n *\n * Every refusal that names more than one flag spells the list this way: the\n * ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`\n * refusal in `./smelt.ts`. It lives with the table because the *order* is the\n * table's — a refusal that listed flags in the order the user happened to type them\n * would read differently every time, and two refusals disagreeing about how to spell\n * the same pair is the kind of drift this file exists to remove.\n */\nexport function flagList(flags: readonly VerbFlag[]): string {\n const spelled = VERB_FLAGS.filter((flag) => flags.includes(flag)).map((flag) => `--${flag}`);\n if (spelled.length <= 1) return spelled.join('');\n return `${spelled.slice(0, -1).join(', ')} and ${spelled.at(-1) ?? ''}`;\n}\n\n/** What one flag's parsed value looks like, derived from how `parseArgs` was told to read it. */\ntype FlagValue<F> = F extends { readonly type: 'boolean' }\n ? boolean\n : F extends { readonly multiple: true }\n ? readonly string[]\n : string;\n\n/**\n * The parsed flags, as every `Subcommand.parse` sees them: one optional field per\n * flag, typed by the table above. Absent means the user did not type it — which is\n * the only thing a refusal needs to know, and the only thing a verb may act on.\n */\nexport type FlagValues = { readonly [K in FlagName]?: FlagValue<(typeof CLI_FLAGS)[K]> };\n\n/** How one flag appears in the OPTIONS block of `--help`. */\nexport interface FlagHelp {\n /** The left column, e.g. `--budget <bytes>` or `-h, --help`. */\n readonly label: string;\n /**\n * The description, already wrapped to the OPTIONS column — a function because three\n * entries render a registry (`SUPPORTED_LANGUAGES`, `STRATEGIES` +\n * `STRUCTURAL_LANGUAGES`, `HARNESS_IDS`) rather than a hand-typed list. The\n * ownership prefix is not here; it is generated.\n */\n body(): readonly string[];\n}\n\n/**\n * A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the\n * hand-typed version wrapped. `--strategy` and `--language` render their registries on\n * one line because they fit; the harness ids do not, and a list long enough to wrap is\n * exactly the list nobody keeps in sync by hand.\n */\nexport function optionList(items: readonly string[], width: number): readonly string[] {\n const lines: string[] = [];\n let line = '';\n items.forEach((item, index) => {\n const word = index === items.length - 1 ? `${item}.` : `${item},`;\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > width) {\n lines.push(line);\n line = word;\n } else {\n line = candidate;\n }\n });\n lines.push(line);\n return lines;\n}\n\n/** The width an OPTIONS description wraps within, once its 23-column indent is removed. */\nconst OPTION_BODY_WIDTH = 65;\n\n/**\n * The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag\n * added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text\n * cannot fall behind what the parser accepts.\n */\nexport const FLAG_HELP: Readonly<Record<FlagName, FlagHelp>> = {\n budget: {\n label: '--budget <bytes>',\n body: () => [\n 'Required, unless smelt.config.json sets defaultBudgetBytes.',\n 'Soft ceiling for the output, in UTF-8 bytes (for map: a hard',\n 'ceiling, met by construction). No built-in default: a budget',\n `${CLI_NAME} invented would decide for you.`,\n ],\n },\n focus: {\n label: '--focus <term>',\n body: () => [\n 'What you were looking for. Repeatable. Matching regions and',\n 'their context survive; the runs between them collapse. For',\n 'map: symbols matching a term (by name or path) are promoted',\n 'to the front of the fill order, ranks unchanged.',\n ],\n },\n language: {\n label: '--language <id>',\n body: () => [`Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.`],\n },\n strategy: {\n label: '--strategy <id>',\n body: () => [\n `${STRATEGIES.join(', ')}. Defaults to ${DEFAULT_STRATEGY}, unless`,\n 'smelt.config.json says otherwise. structural parses',\n `${STRUCTURAL_LANGUAGES.join(', ')};`,\n 'any other language is refused, never approximated. auto picks',\n 'structural for those languages and lexical for the rest, and',\n 'the report names whichever one actually ran.',\n ],\n },\n ignore: {\n label: '--ignore <entry>',\n body: () => [\n 'Repeatable. Replaces the default ignore list',\n // Read off DEFAULT_REPO_IGNORE, never re-typed: help text that lists a default\n // by hand is help text that will one day describe a different default.\n `(${DEFAULT_REPO_IGNORE.join(', ')}): a bare name matches any path segment,`,\n 'an entry containing / is a root-relative prefix.',\n ],\n },\n cache: {\n label: '--cache <dir>',\n body: () => [\n 'Directory for the tags cache, keyed by content',\n 'hash. Only when given does the map write to disk at all.',\n ],\n },\n harness: {\n label: '--harness <id>',\n body: () => [\n 'Skip harness detection and target one id:',\n ...optionList(HARNESS_IDS, OPTION_BODY_WIDTH),\n ],\n },\n json: {\n label: '--json',\n body: () => [\n 'Print a JSON envelope on stdout instead of the text:',\n '{ format, result, elided } for a smelt run — `result` is',\n 'the SmeltResult verbatim, `elided` carries the bytes, so',\n 'the envelope can be reconstructed; feed it back with',\n '--reconstruct. For map: { format, map }, the RepoMap',\n 'structure verbatim.',\n ],\n },\n reconstruct: {\n label: '--reconstruct',\n body: () => [\n 'Read a --json envelope and print the original text, byte for',\n 'byte. This is Law 3 you can run from a shell.',\n ],\n },\n help: { label: '-h, --help', body: () => ['This text.'] },\n version: { label: '--version', body: () => ['The package version.'] },\n};\n\n/**\n * `--budget` has no built-in default, for the same reason `smelt()` has none: a budget\n * smelt invented would be smelt deciding how much of the caller's context to throw\n * away, silently, at a number nobody chose. A *missing* flag is not an error here,\n * though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose\n * explicitly, and the verb's `resolve` errors only when neither exists. A malformed\n * value is always an error.\n *\n * It lives with the flag rather than with a verb because two verbs own `--budget`, and\n * the two of them agreeing on what \"4kb\" means is not something to leave to chance.\n *\n * The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a\n * leading `-` are answered by a digits-only test before anything numeric happens. The\n * *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also\n * where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`\n * here and `\"budgetBytes\"` there.\n */\nexport function parseBudget(raw: string | undefined): number | undefined {\n if (raw === undefined) return undefined;\n if (!/^\\d+$/.test(raw)) throw refuseBudget('not-an-integer', raw);\n const value = Number(raw);\n const fault = budgetFault(value);\n if (fault !== undefined) throw refuseBudget(fault, raw);\n return value;\n}\n\n/** The malformed-budget refusal, in the CLI's currency: prefixed, and exit 2. */\nfunction refuseBudget(fault: BudgetFault, raw: string): CliUsageError {\n return new CliUsageError(`${CLI_NAME}: ${budgetMalformed(fault, '--budget', raw)}`);\n}\n"]}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { SUPPORTED_LANGUAGES } from '../../detect.js';
|
|
2
|
+
import { CliUsageError } from '../../errors.js';
|
|
3
|
+
import { HARNESS_IDS } from '../../harness/registry.js';
|
|
4
|
+
import { budgetFault, budgetMalformed } from '../../ops/inputs.js';
|
|
5
|
+
import { STRATEGIES, DEFAULT_STRATEGY } from '../../plan/planners.js';
|
|
6
|
+
import { STRUCTURAL_LANGUAGES } from '../../plan/structural.js';
|
|
7
|
+
import { DEFAULT_REPO_IGNORE } from '../../repomap/map.js';
|
|
8
|
+
import { CLI_NAME } from '../shell.js';
|
|
9
|
+
/**
|
|
10
|
+
* Every flag the CLI accepts, once.
|
|
11
|
+
*
|
|
12
|
+
* The table is the companion the {@link Subcommand} registry needs: a verb declares
|
|
13
|
+
* the flags it owns as `FlagName[]`, so the type of that list — and therefore whether
|
|
14
|
+
* a verb can claim a flag that does not exist — comes from here. It carries three
|
|
15
|
+
* things that used to be written in three places:
|
|
16
|
+
*
|
|
17
|
+
* 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a
|
|
18
|
+
* literal inside `parseSmeltArgs`.
|
|
19
|
+
* 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is
|
|
20
|
+
* spelled once and every verb's ownership list typechecks against it.
|
|
21
|
+
* 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`
|
|
22
|
+
* prefix, which is *not* stored here: it is generated from the registry's
|
|
23
|
+
* ownership (see `renderOptions` in `cli/usage.ts`), because "which verb owns
|
|
24
|
+
* this flag" is a fact the registry already holds and prose that restates it is
|
|
25
|
+
* prose that can go stale.
|
|
26
|
+
*
|
|
27
|
+
* Key order is meaningful: it is the order the OPTIONS block renders and the order a
|
|
28
|
+
* refusal lists flags in, so keep it stable and append new flags where they read best.
|
|
29
|
+
*/
|
|
30
|
+
export const CLI_FLAGS = {
|
|
31
|
+
budget: { type: 'string' },
|
|
32
|
+
focus: { type: 'string', multiple: true },
|
|
33
|
+
language: { type: 'string' },
|
|
34
|
+
strategy: { type: 'string' },
|
|
35
|
+
ignore: { type: 'string', multiple: true },
|
|
36
|
+
cache: { type: 'string' },
|
|
37
|
+
harness: { type: 'string' },
|
|
38
|
+
json: { type: 'boolean' },
|
|
39
|
+
reconstruct: { type: 'boolean' },
|
|
40
|
+
help: { type: 'boolean', short: 'h' },
|
|
41
|
+
version: { type: 'boolean' },
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The flags answered *before* any verb, so no verb owns them and no verb may refuse
|
|
45
|
+
* them: `smelt map --help` prints the help, exactly as it always has. Every other flag
|
|
46
|
+
* belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.
|
|
47
|
+
*/
|
|
48
|
+
export const GLOBAL_FLAGS = ['help', 'version'];
|
|
49
|
+
/** Every ownable flag, in table order: the list a refusal checks a verb against. */
|
|
50
|
+
export const VERB_FLAGS = Object.keys(CLI_FLAGS).filter((name) => !GLOBAL_FLAGS.includes(name));
|
|
51
|
+
/**
|
|
52
|
+
* `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.
|
|
53
|
+
*
|
|
54
|
+
* Every refusal that names more than one flag spells the list this way: the
|
|
55
|
+
* ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`
|
|
56
|
+
* refusal in `./smelt.ts`. It lives with the table because the *order* is the
|
|
57
|
+
* table's — a refusal that listed flags in the order the user happened to type them
|
|
58
|
+
* would read differently every time, and two refusals disagreeing about how to spell
|
|
59
|
+
* the same pair is the kind of drift this file exists to remove.
|
|
60
|
+
*/
|
|
61
|
+
export function flagList(flags) {
|
|
62
|
+
const spelled = VERB_FLAGS.filter((flag) => flags.includes(flag)).map((flag) => `--${flag}`);
|
|
63
|
+
if (spelled.length <= 1)
|
|
64
|
+
return spelled.join('');
|
|
65
|
+
return `${spelled.slice(0, -1).join(', ')} and ${spelled.at(-1) ?? ''}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the
|
|
69
|
+
* hand-typed version wrapped. `--strategy` and `--language` render their registries on
|
|
70
|
+
* one line because they fit; the harness ids do not, and a list long enough to wrap is
|
|
71
|
+
* exactly the list nobody keeps in sync by hand.
|
|
72
|
+
*/
|
|
73
|
+
export function optionList(items, width) {
|
|
74
|
+
const lines = [];
|
|
75
|
+
let line = '';
|
|
76
|
+
items.forEach((item, index) => {
|
|
77
|
+
const word = index === items.length - 1 ? `${item}.` : `${item},`;
|
|
78
|
+
const candidate = line === '' ? word : `${line} ${word}`;
|
|
79
|
+
if (line !== '' && candidate.length > width) {
|
|
80
|
+
lines.push(line);
|
|
81
|
+
line = word;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
line = candidate;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
lines.push(line);
|
|
88
|
+
return lines;
|
|
89
|
+
}
|
|
90
|
+
/** The width an OPTIONS description wraps within, once its 23-column indent is removed. */
|
|
91
|
+
const OPTION_BODY_WIDTH = 65;
|
|
92
|
+
/**
|
|
93
|
+
* The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag
|
|
94
|
+
* added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text
|
|
95
|
+
* cannot fall behind what the parser accepts.
|
|
96
|
+
*/
|
|
97
|
+
export const FLAG_HELP = {
|
|
98
|
+
budget: {
|
|
99
|
+
label: '--budget <bytes>',
|
|
100
|
+
body: () => [
|
|
101
|
+
'Required, unless smelt.config.json sets defaultBudgetBytes.',
|
|
102
|
+
'Soft ceiling for the output, in UTF-8 bytes (for map: a hard',
|
|
103
|
+
'ceiling, met by construction). No built-in default: a budget',
|
|
104
|
+
`${CLI_NAME} invented would decide for you.`,
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
focus: {
|
|
108
|
+
label: '--focus <term>',
|
|
109
|
+
body: () => [
|
|
110
|
+
'What you were looking for. Repeatable. Matching regions and',
|
|
111
|
+
'their context survive; the runs between them collapse. For',
|
|
112
|
+
'map: symbols matching a term (by name or path) are promoted',
|
|
113
|
+
'to the front of the fill order, ranks unchanged.',
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
language: {
|
|
117
|
+
label: '--language <id>',
|
|
118
|
+
body: () => [`Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.`],
|
|
119
|
+
},
|
|
120
|
+
strategy: {
|
|
121
|
+
label: '--strategy <id>',
|
|
122
|
+
body: () => [
|
|
123
|
+
`${STRATEGIES.join(', ')}. Defaults to ${DEFAULT_STRATEGY}, unless`,
|
|
124
|
+
'smelt.config.json says otherwise. structural parses',
|
|
125
|
+
`${STRUCTURAL_LANGUAGES.join(', ')};`,
|
|
126
|
+
'any other language is refused, never approximated. auto picks',
|
|
127
|
+
'structural for those languages and lexical for the rest, and',
|
|
128
|
+
'the report names whichever one actually ran.',
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
ignore: {
|
|
132
|
+
label: '--ignore <entry>',
|
|
133
|
+
body: () => [
|
|
134
|
+
'Repeatable. Replaces the default ignore list',
|
|
135
|
+
// Read off DEFAULT_REPO_IGNORE, never re-typed: help text that lists a default
|
|
136
|
+
// by hand is help text that will one day describe a different default.
|
|
137
|
+
`(${DEFAULT_REPO_IGNORE.join(', ')}): a bare name matches any path segment,`,
|
|
138
|
+
'an entry containing / is a root-relative prefix.',
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
cache: {
|
|
142
|
+
label: '--cache <dir>',
|
|
143
|
+
body: () => [
|
|
144
|
+
'Directory for the tags cache, keyed by content',
|
|
145
|
+
'hash. Only when given does the map write to disk at all.',
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
harness: {
|
|
149
|
+
label: '--harness <id>',
|
|
150
|
+
body: () => [
|
|
151
|
+
'Skip harness detection and target one id:',
|
|
152
|
+
...optionList(HARNESS_IDS, OPTION_BODY_WIDTH),
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
json: {
|
|
156
|
+
label: '--json',
|
|
157
|
+
body: () => [
|
|
158
|
+
'Print a JSON envelope on stdout instead of the text:',
|
|
159
|
+
'{ format, result, elided } for a smelt run — `result` is',
|
|
160
|
+
'the SmeltResult verbatim, `elided` carries the bytes, so',
|
|
161
|
+
'the envelope can be reconstructed; feed it back with',
|
|
162
|
+
'--reconstruct. For map: { format, map }, the RepoMap',
|
|
163
|
+
'structure verbatim.',
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
reconstruct: {
|
|
167
|
+
label: '--reconstruct',
|
|
168
|
+
body: () => [
|
|
169
|
+
'Read a --json envelope and print the original text, byte for',
|
|
170
|
+
'byte. This is Law 3 you can run from a shell.',
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
help: { label: '-h, --help', body: () => ['This text.'] },
|
|
174
|
+
version: { label: '--version', body: () => ['The package version.'] },
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* `--budget` has no built-in default, for the same reason `smelt()` has none: a budget
|
|
178
|
+
* smelt invented would be smelt deciding how much of the caller's context to throw
|
|
179
|
+
* away, silently, at a number nobody chose. A *missing* flag is not an error here,
|
|
180
|
+
* though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose
|
|
181
|
+
* explicitly, and the verb's `resolve` errors only when neither exists. A malformed
|
|
182
|
+
* value is always an error.
|
|
183
|
+
*
|
|
184
|
+
* It lives with the flag rather than with a verb because two verbs own `--budget`, and
|
|
185
|
+
* the two of them agreeing on what "4kb" means is not something to leave to chance.
|
|
186
|
+
*
|
|
187
|
+
* The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a
|
|
188
|
+
* leading `-` are answered by a digits-only test before anything numeric happens. The
|
|
189
|
+
* *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also
|
|
190
|
+
* where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`
|
|
191
|
+
* here and `"budgetBytes"` there.
|
|
192
|
+
*/
|
|
193
|
+
export function parseBudget(raw) {
|
|
194
|
+
if (raw === undefined)
|
|
195
|
+
return undefined;
|
|
196
|
+
if (!/^\d+$/.test(raw))
|
|
197
|
+
throw refuseBudget('not-an-integer', raw);
|
|
198
|
+
const value = Number(raw);
|
|
199
|
+
const fault = budgetFault(value);
|
|
200
|
+
if (fault !== undefined)
|
|
201
|
+
throw refuseBudget(fault, raw);
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
/** The malformed-budget refusal, in the CLI's currency: prefixed, and exit 2. */
|
|
205
|
+
function refuseBudget(fault, raw) {
|
|
206
|
+
return new CliUsageError(`${CLI_NAME}: ${budgetMalformed(fault, '--budget', raw)}`);
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=flags.js.map
|