argsbarg 5.0.3 → 5.1.1
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/CHANGELOG.md +19 -1
- package/docs/cli-program.md +4 -4
- package/docs/config-schema.md +9 -9
- package/docs/configure.md +5 -1
- package/docs/mcp.md +1 -1
- package/examples/full-example/README.md +2 -2
- package/examples/full-example/src/program.ts +0 -3
- package/index.d.ts +2 -2
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +9 -3
- package/src/builtins/completion-bash.ts +2 -100
- package/src/builtins/completion-simulate-shared.ts +120 -0
- package/src/builtins/completion-zsh.ts +4 -102
- package/src/builtins/config.test.ts +4 -4
- package/src/builtins/config.ts +19 -22
- package/src/builtins/configure.ts +35 -4
- package/src/builtins/dispatch.ts +10 -15
- package/src/builtins/registry.ts +0 -2
- package/src/capabilities.ts +6 -12
- package/src/config/entry.ts +2 -2
- package/src/config/resolve.ts +2 -2
- package/src/configure/prompt.ts +2 -4
- package/src/docs/docs.test.ts +1 -1
- package/src/install/install-validate.test.ts +11 -0
- package/src/install/target-effective.ts +14 -2
- package/src/install/target-registry.ts +17 -0
- package/src/install/targets.test.ts +2 -0
- package/src/parse.ts +51 -172
- package/src/schema.ts +1 -1
- package/src/types.ts +2 -2
- package/src/validate.ts +12 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.1.1] - 2026-07-04
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Configure without MCP** — skill-only apps (`mcpServer` unset) no longer prompt for MCP host config during interactive `configure`; MCP install targets default to disabled unless `mcpServer.enabled` is true.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Internal DRY refactors** — shared bash/zsh completion simulate emitters, module-level `errorResult()` in the parser, unified configure stdin prompt via `readPromptLine`, and a shared `mcpServerRequiredForArtifact` predicate linking install defaults with schema validation.
|
|
19
|
+
|
|
20
|
+
## [5.1.0] - 2026-07-04
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Breaking: `config get`/`set` → `configure get`/`set`** — top-level `config` removed; app config introspection lives under the `configure` built-in.
|
|
25
|
+
|
|
10
26
|
## [5.0.3] - 2026-07-04
|
|
11
27
|
|
|
12
28
|
### Changed
|
|
@@ -579,7 +595,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
|
|
|
579
595
|
- Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
|
|
580
596
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
581
597
|
|
|
582
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v5.
|
|
598
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v5.1.1...HEAD
|
|
599
|
+
[5.1.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.1
|
|
600
|
+
[5.1.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.0
|
|
583
601
|
[5.0.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.0.3
|
|
584
602
|
[5.0.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.0.2
|
|
585
603
|
[5.0.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.0.1
|
package/docs/cli-program.md
CHANGED
|
@@ -412,11 +412,11 @@ await cli.run();
|
|
|
412
412
|
|
|
413
413
|
| Field | Default | Purpose |
|
|
414
414
|
| --- | --- | --- |
|
|
415
|
-
| `description` | *(required)* | Shown in prompts, `
|
|
415
|
+
| `description` | *(required)* | Shown in prompts, `configure get`, and bundle manifests |
|
|
416
416
|
| `title` | config key | Short label in interactive `configure` |
|
|
417
417
|
| `default` | — | Used when `jsonSchema` omitted (all-string mode) |
|
|
418
418
|
| `required` | `true` | When `false`, optional unless required by `jsonSchema` |
|
|
419
|
-
| `sensitive` | name heuristic (`token`, `secret`, …) | Redact in prompts, `
|
|
419
|
+
| `sensitive` | name heuristic (`token`, `secret`, …) | Redact in prompts, `configure get`, and status |
|
|
420
420
|
| `env` | — | When set: non-empty host env overrides file; consulted again after `resolve` when `resolve` returns `undefined`; exported to `process.env` after resolve |
|
|
421
421
|
| `resolve` | — | Optional fallback after file; return `undefined` to fall back to `env` (if set) and defaults |
|
|
422
422
|
|
|
@@ -425,7 +425,7 @@ await cli.run();
|
|
|
425
425
|
- Default: `$XDG_CONFIG_HOME/<sanitized-key>/config` or `%APPDATA%/<key>/config`.
|
|
426
426
|
- JSON: flat object keyed by schema names — `{ "apiToken": "…", "maxRetries": 5 }`.
|
|
427
427
|
- **Strict:** unknown keys rejected on load.
|
|
428
|
-
- **CLI:** missing required config exits 1 before the leaf handler (TTY prompt when interactive). Built-in `docs` and `
|
|
428
|
+
- **CLI:** missing required config exits 1 before the leaf handler (TTY prompt when interactive). Built-in `docs` and `configure get`/`set` skip this exit.
|
|
429
429
|
- **MCP:** server stays up; missing config returns `isError: true` at `tools/call`.
|
|
430
430
|
- **Configure:** interactive `configure` runs the app config wizard; **`configure --sync`** refreshes agent artifacts.
|
|
431
431
|
- **Agent integration:** `configure.agentIntegration` (`mcp` | `skill` | `both`) sets default sync targets; see [configure.md](configure.md#configuretargets).
|
|
@@ -436,7 +436,7 @@ See [config-schema.md](config-schema.md) for codegen, [configure.md](configure.m
|
|
|
436
436
|
|
|
437
437
|
## Reserved names
|
|
438
438
|
|
|
439
|
-
Do not declare user commands named `completion`, `configure`, `mcp`, `version`,
|
|
439
|
+
Do not declare user commands named `completion`, `configure`, `mcp`, `version`, or `docs` at the root — ArgsBarg injects these when configured. App config uses `configure get` / `configure set` subcommands (not a top-level `config` command).
|
|
440
440
|
|
|
441
441
|
## Cursor rule for consumer repos
|
|
442
442
|
|
package/docs/config-schema.md
CHANGED
|
@@ -40,7 +40,7 @@ await cli.run();
|
|
|
40
40
|
| --- | --- |
|
|
41
41
|
| Config file | Flat JSON keyed by schema names; strict load (unknown keys rejected) |
|
|
42
42
|
| Interactive `configure` / `--status` | Auto-runs config wizard when `entries` is non-empty; `--status` for read-only inventory |
|
|
43
|
-
| Built-in `
|
|
43
|
+
| Built-in `configure get` / `configure set` | Read/write resolved values (opt-out via `commands: false`) |
|
|
44
44
|
| MCP bundle / Claude plugin | `userConfig` for entries with `env` set |
|
|
45
45
|
| `ctx.appConfig` in handlers | `get`, `require`, `set`, `read`, `path`, `dir` — prefer over `process.env` |
|
|
46
46
|
|
|
@@ -48,7 +48,7 @@ await cli.run();
|
|
|
48
48
|
|
|
49
49
|
**No public config I/O exports** — consumers use `program.appConfig` for authoring and `ctx.appConfig` in handlers.
|
|
50
50
|
|
|
51
|
-
See [cli-program.md — Configuration](cli-program.md#configuration-programappconfig) for resolution order, bootstrap timing, and
|
|
51
|
+
See [cli-program.md — Configuration](cli-program.md#configuration-programappconfig) for resolution order, bootstrap timing, and `configure get`/`set`.
|
|
52
52
|
|
|
53
53
|
## `CliAppConfig` and `CliAppConfigEntry`
|
|
54
54
|
|
|
@@ -191,26 +191,26 @@ appConfig: {
|
|
|
191
191
|
|
|
192
192
|
All file values are strings. Defaults come from `entry.default`.
|
|
193
193
|
|
|
194
|
-
## Built-in `
|
|
194
|
+
## Built-in `configure get` / `configure set`
|
|
195
195
|
|
|
196
196
|
When `program.appConfig` is set and `commands !== false`:
|
|
197
197
|
|
|
198
198
|
| Subcommand | Purpose |
|
|
199
199
|
| --- | --- |
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
200
|
+
| `configure get [key]` | Resolved value(s); `--json`; `--json --pretty` |
|
|
201
|
+
| `configure set <key> <value>` | One key; full document re-validated after merge |
|
|
202
202
|
|
|
203
|
-
`
|
|
203
|
+
`configure get`/`set` skip required-config exit and TTY prompts. Sensitive values redact on `get` (`REDACTED` / `{ "set": true }` with `--json`).
|
|
204
204
|
|
|
205
|
-
Object/array/`$ref` properties require `--json` on `
|
|
205
|
+
Object/array/`$ref` properties require `--json` on `configure set`.
|
|
206
206
|
|
|
207
207
|
## Example in this repo
|
|
208
208
|
|
|
209
209
|
| Example | Role |
|
|
210
210
|
| --- | --- |
|
|
211
|
-
| [`examples/full-example/`](../examples/full-example/) | **Copy template** — schemagen discovery, `APP_CONFIG_JSON_SCHEMA` bridge, `program.appConfig`, built-in `
|
|
211
|
+
| [`examples/full-example/`](../examples/full-example/) | **Copy template** — schemagen discovery, `APP_CONFIG_JSON_SCHEMA` bridge, `program.appConfig`, built-in `configure get`/`set` |
|
|
212
212
|
|
|
213
213
|
```bash
|
|
214
214
|
cd examples/full-example && just setup && just schemagen
|
|
215
|
-
FULL_EXAMPLE_API_TOKEN=dev just run
|
|
215
|
+
FULL_EXAMPLE_API_TOKEN=dev just run configure get apiToken --json
|
|
216
216
|
```
|
package/docs/configure.md
CHANGED
|
@@ -47,6 +47,10 @@ Dev flow matches release: formula `install` copies the binary and generates comp
|
|
|
47
47
|
|
|
48
48
|
# Remove app config only (not skills/MCP)
|
|
49
49
|
<key> configure --remove-config --yes
|
|
50
|
+
|
|
51
|
+
# Read or write app config (non-interactive; when program.appConfig is set)
|
|
52
|
+
<key> configure get [key] [--json] [--pretty]
|
|
53
|
+
<key> configure set <key> <value> [--json]
|
|
50
54
|
```
|
|
51
55
|
|
|
52
56
|
Non-interactive / CI: pass **`--yes`** (or **`--json`**, **`--sync`**, **`--remove-all`**, **`--remove-config`**) — see [Confirmation](#confirmation).
|
|
@@ -60,7 +64,7 @@ Non-interactive / CI: pass **`--yes`** (or **`--json`**, **`--sync`**, **`--remo
|
|
|
60
64
|
| Cursor skill | Y/n prompt | `~/.cursor/skills/<dir>/` when `~/.cursor` exists |
|
|
61
65
|
| Claude skill | Y/n prompt | `~/.claude/skills/<dir>/` when `~/.claude` exists |
|
|
62
66
|
| Codex / OpenCode / OpenClaw skills | Y/n prompt | Agent-specific dirs when available |
|
|
63
|
-
| MCP config | Y/n prompt | Cursor, Claude Code/Desktop, OpenCode, Codex, OpenClaw, ChatGPT desktop |
|
|
67
|
+
| MCP config | Y/n prompt when `mcpServer.enabled` | Cursor, Claude Code/Desktop, OpenCode, Codex, OpenClaw, ChatGPT desktop |
|
|
64
68
|
| App config | auto-runs wizard | Interactive wizard writes `~/.local/lib/<key>/config.json` |
|
|
65
69
|
|
|
66
70
|
### Externally managed binary (Homebrew)
|
package/docs/mcp.md
CHANGED
|
@@ -327,7 +327,7 @@ At server start (`Cli.serveMcp()`), before the NDJSON loop:
|
|
|
327
327
|
- Loaded at MCP startup; host `process.env` wins for mapped env vars already set.
|
|
328
328
|
- Missing required config does **not** exit the MCP server — enforced at `tools/call` with a helpful error.
|
|
329
329
|
- Configure interactively: `myapp configure` (see [configure.md](configure.md)).
|
|
330
|
-
- Built-in `
|
|
330
|
+
- Built-in `configure get` / `configure set` when `program.appConfig.commands` is enabled (default). Hosts inject `user_config` → env at spawn; they never write the argsbarg config file.
|
|
331
331
|
|
|
332
332
|
Example:
|
|
333
333
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
| Area | Files / wiring |
|
|
8
8
|
| --- | --- |
|
|
9
|
-
| All builtins | `completion`, `version`, `
|
|
9
|
+
| All builtins | `completion`, `version`, `configure`, `docs`, `mcp`, `configure get`/`set` |
|
|
10
10
|
| `program.appConfig` | `src/types.ts` (`AppConfig`) → `schemas/configSchemas.ts` |
|
|
11
11
|
| `outputSchema` | `src/commands/status/types.ts` (`StatusJsonOutput`) → `schemas/outputSchemas.ts` |
|
|
12
12
|
| Schemagen | `scripts/schemagen.ts` + `scripts/schemagen/discover-schema-roots.ts` |
|
|
@@ -47,7 +47,7 @@ cd examples/full-example
|
|
|
47
47
|
just setup
|
|
48
48
|
just schemagen # after changing src/**/types.ts
|
|
49
49
|
FULL_EXAMPLE_API_TOKEN=dev just run status --json
|
|
50
|
-
FULL_EXAMPLE_API_TOKEN=dev just run
|
|
50
|
+
FULL_EXAMPLE_API_TOKEN=dev just run configure get apiToken --json
|
|
51
51
|
FULL_EXAMPLE_API_TOKEN=dev just run docs readme
|
|
52
52
|
```
|
|
53
53
|
|
|
@@ -50,8 +50,5 @@ export const program = {
|
|
|
50
50
|
mcpd: true,
|
|
51
51
|
claudePlugin: true,
|
|
52
52
|
},
|
|
53
|
-
configure: {
|
|
54
|
-
// Defaults: agentIntegration picks skill vs MCP for sync; app config is opt-in in interactive configure.
|
|
55
|
-
},
|
|
56
53
|
commands: [echoCommand, statusCommand],
|
|
57
54
|
} satisfies CliProgram;
|
package/index.d.ts
CHANGED
|
@@ -281,7 +281,7 @@ export interface CliAppConfigEntry {
|
|
|
281
281
|
/** When `false`, optional for bootstrap and MCP enforcement. Default: `true`. */
|
|
282
282
|
required?: boolean;
|
|
283
283
|
/**
|
|
284
|
-
* Mask stdin during prompts and redact on `
|
|
284
|
+
* Mask stdin during prompts and redact on `configure get`.
|
|
285
285
|
* Default: `/key|token|secret|password/i.test(name)`.
|
|
286
286
|
*/
|
|
287
287
|
sensitive?: boolean;
|
|
@@ -297,7 +297,7 @@ export interface CliAppConfigEntry {
|
|
|
297
297
|
* App configuration block on the program root ({@link CliProgram.appConfig}).
|
|
298
298
|
*/
|
|
299
299
|
export interface CliAppConfig {
|
|
300
|
-
/** Built-in `
|
|
300
|
+
/** Built-in `configure get` / `configure set`. Default: enabled when `appConfig` is set. */
|
|
301
301
|
commands?: boolean | {
|
|
302
302
|
enabled?: boolean;
|
|
303
303
|
mcpSet?: boolean;
|
package/package.json
CHANGED
|
@@ -154,8 +154,8 @@ describe("completion emitters", () => {
|
|
|
154
154
|
|
|
155
155
|
/** Tests for schema export builtins. */
|
|
156
156
|
describe("schema export builtins", () => {
|
|
157
|
-
/** ExportPresentationBuiltins
|
|
158
|
-
test("exportPresentationBuiltins
|
|
157
|
+
/** ExportPresentationBuiltins nests configure get/set when appConfig set. */
|
|
158
|
+
test("exportPresentationBuiltins nests configure get/set when appConfig set", () => {
|
|
159
159
|
const withConfig: CliProgram = {
|
|
160
160
|
...fixture,
|
|
161
161
|
appConfig: {
|
|
@@ -165,8 +165,14 @@ describe("schema export builtins", () => {
|
|
|
165
165
|
},
|
|
166
166
|
};
|
|
167
167
|
const builtins = exportPresentationBuiltins(withConfig);
|
|
168
|
-
expect(builtins.map((b) => b.key)).toContain("config");
|
|
169
168
|
expect(builtins.map((b) => b.key)).toContain("configure");
|
|
169
|
+
expect(builtins.map((b) => b.key)).not.toContain("config");
|
|
170
|
+
const configureNode = builtins.find((b) => b.key === "configure");
|
|
171
|
+
expect(configureNode && "commands" in configureNode).toBe(true);
|
|
172
|
+
if (configureNode && "commands" in configureNode) {
|
|
173
|
+
const keys = configureNode.commands?.map((c) => c.key) ?? [];
|
|
174
|
+
expect(keys).toEqual(expect.arrayContaining(["get", "set"]));
|
|
175
|
+
}
|
|
170
176
|
});
|
|
171
177
|
|
|
172
178
|
test("exportPresentationBuiltins omits hidden completion", () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliOptionKind, type CliRouter } from "../types.ts";
|
|
2
|
+
import { emitConsumeLong, emitConsumeShort, emitMatchChild } from "./completion-simulate-shared.ts";
|
|
2
3
|
import { collectScopes, type ScopeRec } from "./scopes.ts";
|
|
3
4
|
import {
|
|
4
5
|
escShellSingleQuoted,
|
|
@@ -8,105 +9,6 @@ import {
|
|
|
8
9
|
mainName,
|
|
9
10
|
} from "./shell-helpers.ts";
|
|
10
11
|
|
|
11
|
-
function emitConsumeLong(ident: string, scopes: ScopeRec[]): string {
|
|
12
|
-
let o = "_${ident}_nac_consume_long() {\n".replace("${ident}", ident);
|
|
13
|
-
o += ' local sid="$1" w="$2" nw="$3"\n';
|
|
14
|
-
o += " case $sid in\n";
|
|
15
|
-
for (const [i, sc] of scopes.entries()) {
|
|
16
|
-
o += ` ${i})\n`;
|
|
17
|
-
o += " case $w in\n";
|
|
18
|
-
o +=
|
|
19
|
-
" " +
|
|
20
|
-
kHelpLong +
|
|
21
|
-
"|${kHelpLong}=*|${kHelpShort}) echo 1 ;;\n"
|
|
22
|
-
.replace(/\$\{kHelpLong\}/g, kHelpLong)
|
|
23
|
-
.replace(/\$\{kHelpShort\}/g, kHelpShort);
|
|
24
|
-
for (const op of sc.opts) {
|
|
25
|
-
const base = `--${op.name}`;
|
|
26
|
-
if (op.kind === "presence") {
|
|
27
|
-
o += ` ${base}${"|${base}=*) echo 1 ;;\n".replace(/\$\{base\}/g, base)}`;
|
|
28
|
-
} else {
|
|
29
|
-
o += ` ${base}=*) echo 1 ;;\n`;
|
|
30
|
-
o += ` ${base}) echo 2 ;;\n`;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
o += " *) echo 0 ;;\n";
|
|
34
|
-
o += " esac\n";
|
|
35
|
-
o += " ;;\n";
|
|
36
|
-
}
|
|
37
|
-
o += " *) echo 0 ;;\n";
|
|
38
|
-
o += " esac\n";
|
|
39
|
-
o += "}\n";
|
|
40
|
-
return o;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function emitConsumeShort(ident: string, scopes: ScopeRec[]): string {
|
|
44
|
-
let o = "_${ident}_nac_consume_short() {\n".replace("${ident}", ident);
|
|
45
|
-
o += ' local sid="$1" w="$2"\n';
|
|
46
|
-
o += " case $sid in\n";
|
|
47
|
-
for (const [i, sc] of scopes.entries()) {
|
|
48
|
-
o += ` ${i})\n`;
|
|
49
|
-
o += " local rest=${w#-}\n";
|
|
50
|
-
o += " local ch\n";
|
|
51
|
-
o += " local saw=0\n";
|
|
52
|
-
o += " while [[ -n $rest ]]; do\n";
|
|
53
|
-
o += " ch=${rest:0:1}\n";
|
|
54
|
-
o += " rest=${rest:1}\n";
|
|
55
|
-
o += " case $ch in\n";
|
|
56
|
-
let boolChars = "";
|
|
57
|
-
for (const op of sc.opts) {
|
|
58
|
-
if (!op.shortName) continue;
|
|
59
|
-
if (op.kind === "presence") {
|
|
60
|
-
boolChars += `${op.shortName}|`;
|
|
61
|
-
} else {
|
|
62
|
-
o += ` ${op.shortName})\n`;
|
|
63
|
-
o += " if [[ $saw -ne 0 || -n $rest ]]; then echo 0; return; fi\n";
|
|
64
|
-
o += " echo 2; return ;;\n";
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (boolChars.length > 0) {
|
|
68
|
-
boolChars = boolChars.slice(0, -1);
|
|
69
|
-
o += ` ${boolChars}) ;;\n`;
|
|
70
|
-
}
|
|
71
|
-
o += " *) echo 0; return ;;\n";
|
|
72
|
-
o += " esac\n";
|
|
73
|
-
o += " saw=1\n";
|
|
74
|
-
o += " done\n";
|
|
75
|
-
o += " echo 1\n";
|
|
76
|
-
o += " ;;\n";
|
|
77
|
-
}
|
|
78
|
-
o += " *) echo 0 ;;\n";
|
|
79
|
-
o += " esac\n";
|
|
80
|
-
o += "}\n";
|
|
81
|
-
return o;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function emitMatchChild(
|
|
85
|
-
ident: string,
|
|
86
|
-
scopes: ScopeRec[],
|
|
87
|
-
pathIndex: Record<string, number>,
|
|
88
|
-
): string {
|
|
89
|
-
let o = "_${ident}_nac_match_child() {\n".replace("${ident}", ident);
|
|
90
|
-
o += ' local sid="$1" w="$2"\n';
|
|
91
|
-
o += " case $sid in\n";
|
|
92
|
-
for (const [sid, sc] of scopes.entries()) {
|
|
93
|
-
if (sc.kids.length === 0) continue;
|
|
94
|
-
o += ` ${sid})\n`;
|
|
95
|
-
o += " case $w in\n";
|
|
96
|
-
for (const ch of sc.kids) {
|
|
97
|
-
const childPath = sc.path === "" ? ch.key : `${sc.path}/${ch.key}`;
|
|
98
|
-
const cid = pathIndex[childPath] ?? 0;
|
|
99
|
-
o += ` ${ch.key}) echo ${cid}; return 0 ;;\n`;
|
|
100
|
-
}
|
|
101
|
-
o += " esac\n";
|
|
102
|
-
o += " ;;\n";
|
|
103
|
-
}
|
|
104
|
-
o += " esac\n";
|
|
105
|
-
o += " return 1\n";
|
|
106
|
-
o += "}\n";
|
|
107
|
-
return o;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
12
|
function emitSimulate(ident: string): string {
|
|
111
13
|
let o = "_${ident}_nac_simulate() {\n".replace("${ident}", ident);
|
|
112
14
|
o += " local i=1 sid=0 w steps next\n";
|
|
@@ -247,7 +149,7 @@ export function completionBashScript(schema: CliRouter): string {
|
|
|
247
149
|
}
|
|
248
150
|
|
|
249
151
|
out += emitConsumeLong(ident, scopes);
|
|
250
|
-
out += emitConsumeShort(ident, scopes);
|
|
152
|
+
out += emitConsumeShort(ident, scopes, "bash");
|
|
251
153
|
out += emitMatchChild(ident, scopes, pathIndex);
|
|
252
154
|
out += emitSimulate(ident);
|
|
253
155
|
out += emitEnumReplyBash(ident, scopes);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Shared shell completion emitters for bash and zsh simulate helpers.
|
|
3
|
+
Generates _nac_consume_long, _nac_consume_short, and _nac_match_child functions
|
|
4
|
+
used while walking argv to determine completion scope.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ScopeRec } from "./scopes.ts";
|
|
8
|
+
import { kHelpLong, kHelpShort } from "./shell-helpers.ts";
|
|
9
|
+
|
|
10
|
+
/** Which shell dialect to emit for bundled short-flag parsing. */
|
|
11
|
+
export type CompletionShellDialect = "bash" | "zsh";
|
|
12
|
+
|
|
13
|
+
/** Emits `_<ident>_nac_consume_long` — identical for bash and zsh. */
|
|
14
|
+
export function emitConsumeLong(ident: string, scopes: ScopeRec[]): string {
|
|
15
|
+
let o = "_${ident}_nac_consume_long() {\n".replace("${ident}", ident);
|
|
16
|
+
o += ' local sid="$1" w="$2" nw="$3"\n';
|
|
17
|
+
o += " case $sid in\n";
|
|
18
|
+
for (const [i, sc] of scopes.entries()) {
|
|
19
|
+
o += ` ${i})\n`;
|
|
20
|
+
o += " case $w in\n";
|
|
21
|
+
o +=
|
|
22
|
+
" " +
|
|
23
|
+
kHelpLong +
|
|
24
|
+
"|${kHelpLong}=*|${kHelpShort}) echo 1 ;;\n"
|
|
25
|
+
.replace(/\$\{kHelpLong\}/g, kHelpLong)
|
|
26
|
+
.replace(/\$\{kHelpShort\}/g, kHelpShort);
|
|
27
|
+
for (const op of sc.opts) {
|
|
28
|
+
const base = `--${op.name}`;
|
|
29
|
+
if (op.kind === "presence") {
|
|
30
|
+
o += ` ${base}${"|${base}=*) echo 1 ;;\n".replace(/\$\{base\}/g, base)}`;
|
|
31
|
+
} else {
|
|
32
|
+
o += ` ${base}=*) echo 1 ;;\n`;
|
|
33
|
+
o += ` ${base}) echo 2 ;;\n`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
o += " *) echo 0 ;;\n";
|
|
37
|
+
o += " esac\n";
|
|
38
|
+
o += " ;;\n";
|
|
39
|
+
}
|
|
40
|
+
o += " *) echo 0 ;;\n";
|
|
41
|
+
o += " esac\n";
|
|
42
|
+
o += "}\n";
|
|
43
|
+
return o;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Emits `_<ident>_nac_consume_short`; dialect selects substring syntax. */
|
|
47
|
+
export function emitConsumeShort(
|
|
48
|
+
ident: string,
|
|
49
|
+
scopes: ScopeRec[],
|
|
50
|
+
dialect: CompletionShellDialect,
|
|
51
|
+
): string {
|
|
52
|
+
const firstChar = dialect === "bash" ? "ch=${rest:0:1}" : "ch=${rest[1,1]}";
|
|
53
|
+
const restAdvance = dialect === "bash" ? "rest=${rest:1}" : "rest=${rest[2,-1]}";
|
|
54
|
+
|
|
55
|
+
let o = "_${ident}_nac_consume_short() {\n".replace("${ident}", ident);
|
|
56
|
+
o += ' local sid="$1" w="$2"\n';
|
|
57
|
+
o += " case $sid in\n";
|
|
58
|
+
for (const [i, sc] of scopes.entries()) {
|
|
59
|
+
o += ` ${i})\n`;
|
|
60
|
+
o += " local rest=${w#-}\n";
|
|
61
|
+
o += " local ch\n";
|
|
62
|
+
o += " local saw=0\n";
|
|
63
|
+
o += " while [[ -n $rest ]]; do\n";
|
|
64
|
+
o += ` ${firstChar}\n`;
|
|
65
|
+
o += ` ${restAdvance}\n`;
|
|
66
|
+
o += " case $ch in\n";
|
|
67
|
+
let boolChars = "";
|
|
68
|
+
for (const op of sc.opts) {
|
|
69
|
+
if (!op.shortName) continue;
|
|
70
|
+
if (op.kind === "presence") {
|
|
71
|
+
boolChars += `${op.shortName}|`;
|
|
72
|
+
} else {
|
|
73
|
+
o += ` ${op.shortName})\n`;
|
|
74
|
+
o += " if [[ $saw -ne 0 || -n $rest ]]; then echo 0; return; fi\n";
|
|
75
|
+
o += " echo 2; return ;;\n";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (boolChars.length > 0) {
|
|
79
|
+
boolChars = boolChars.slice(0, -1);
|
|
80
|
+
o += ` ${boolChars}) ;;\n`;
|
|
81
|
+
}
|
|
82
|
+
o += " *) echo 0; return ;;\n";
|
|
83
|
+
o += " esac\n";
|
|
84
|
+
o += " saw=1\n";
|
|
85
|
+
o += " done\n";
|
|
86
|
+
o += " echo 1\n";
|
|
87
|
+
o += " ;;\n";
|
|
88
|
+
}
|
|
89
|
+
o += " *) echo 0 ;;\n";
|
|
90
|
+
o += " esac\n";
|
|
91
|
+
o += "}\n";
|
|
92
|
+
return o;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Emits `_<ident>_nac_match_child` — identical for bash and zsh. */
|
|
96
|
+
export function emitMatchChild(
|
|
97
|
+
ident: string,
|
|
98
|
+
scopes: ScopeRec[],
|
|
99
|
+
pathIndex: Record<string, number>,
|
|
100
|
+
): string {
|
|
101
|
+
let o = "_${ident}_nac_match_child() {\n".replace("${ident}", ident);
|
|
102
|
+
o += ' local sid="$1" w="$2"\n';
|
|
103
|
+
o += " case $sid in\n";
|
|
104
|
+
for (const [sid, sc] of scopes.entries()) {
|
|
105
|
+
if (sc.kids.length === 0) continue;
|
|
106
|
+
o += ` ${sid})\n`;
|
|
107
|
+
o += " case $w in\n";
|
|
108
|
+
for (const ch of sc.kids) {
|
|
109
|
+
const childPath = sc.path === "" ? ch.key : `${sc.path}/${ch.key}`;
|
|
110
|
+
const cid = pathIndex[childPath] ?? 0;
|
|
111
|
+
o += ` ${ch.key}) echo ${cid}; return 0 ;;\n`;
|
|
112
|
+
}
|
|
113
|
+
o += " esac\n";
|
|
114
|
+
o += " ;;\n";
|
|
115
|
+
}
|
|
116
|
+
o += " esac\n";
|
|
117
|
+
o += " return 1\n";
|
|
118
|
+
o += "}\n";
|
|
119
|
+
return o;
|
|
120
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliOptionKind, type CliRouter } from "../types.ts";
|
|
2
|
+
import { emitConsumeLong, emitConsumeShort, emitMatchChild } from "./completion-simulate-shared.ts";
|
|
2
3
|
import { collectScopes, type ScopeRec } from "./scopes.ts";
|
|
3
4
|
import {
|
|
4
5
|
escShellSingleQuoted,
|
|
@@ -53,105 +54,6 @@ function emitScopeArraysZsh(ident: string, scopes: ScopeRec[]): string {
|
|
|
53
54
|
return out;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
function emitConsumeLongZsh(ident: string, scopes: ScopeRec[]): string {
|
|
57
|
-
let o = "_${ident}_nac_consume_long() {\n".replace("${ident}", ident);
|
|
58
|
-
o += ' local sid="$1" w="$2" nw="$3"\n';
|
|
59
|
-
o += " case $sid in\n";
|
|
60
|
-
for (const [i, sc] of scopes.entries()) {
|
|
61
|
-
o += ` ${i})\n`;
|
|
62
|
-
o += " case $w in\n";
|
|
63
|
-
o +=
|
|
64
|
-
" " +
|
|
65
|
-
kHelpLong +
|
|
66
|
-
"|${kHelpLong}=*|${kHelpShort}) echo 1 ;;\n"
|
|
67
|
-
.replace(/\$\{kHelpLong\}/g, kHelpLong)
|
|
68
|
-
.replace(/\$\{kHelpShort\}/g, kHelpShort);
|
|
69
|
-
for (const op of sc.opts) {
|
|
70
|
-
const base = `--${op.name}`;
|
|
71
|
-
if (op.kind === "presence") {
|
|
72
|
-
o += ` ${base}${"|${base}=*) echo 1 ;;\n".replace(/\$\{base\}/g, base)}`;
|
|
73
|
-
} else {
|
|
74
|
-
o += ` ${base}=*) echo 1 ;;\n`;
|
|
75
|
-
o += ` ${base}) echo 2 ;;\n`;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
o += " *) echo 0 ;;\n";
|
|
79
|
-
o += " esac\n";
|
|
80
|
-
o += " ;;\n";
|
|
81
|
-
}
|
|
82
|
-
o += " *) echo 0 ;;\n";
|
|
83
|
-
o += " esac\n";
|
|
84
|
-
o += "}\n";
|
|
85
|
-
return o;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function emitConsumeShortZsh(ident: string, scopes: ScopeRec[]): string {
|
|
89
|
-
let o = "_${ident}_nac_consume_short() {\n".replace("${ident}", ident);
|
|
90
|
-
o += ' local sid="$1" w="$2"\n';
|
|
91
|
-
o += " case $sid in\n";
|
|
92
|
-
for (const [i, sc] of scopes.entries()) {
|
|
93
|
-
o += ` ${i})\n`;
|
|
94
|
-
o += " local rest=${w#-}\n";
|
|
95
|
-
o += " local ch\n";
|
|
96
|
-
o += " local saw=0\n";
|
|
97
|
-
o += " while [[ -n $rest ]]; do\n";
|
|
98
|
-
o += " ch=${rest[1,1]}\n";
|
|
99
|
-
o += " rest=${rest[2,-1]}\n";
|
|
100
|
-
o += " case $ch in\n";
|
|
101
|
-
let boolChars = "";
|
|
102
|
-
for (const op of sc.opts) {
|
|
103
|
-
if (!op.shortName) continue;
|
|
104
|
-
if (op.kind === "presence") {
|
|
105
|
-
boolChars += `${op.shortName}|`;
|
|
106
|
-
} else {
|
|
107
|
-
o += ` ${op.shortName})\n`;
|
|
108
|
-
o += " if [[ $saw -ne 0 || -n $rest ]]; then echo 0; return; fi\n";
|
|
109
|
-
o += " echo 2; return ;;\n";
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (boolChars.length > 0) {
|
|
113
|
-
boolChars = boolChars.slice(0, -1);
|
|
114
|
-
o += ` ${boolChars}) ;;\n`;
|
|
115
|
-
}
|
|
116
|
-
o += " *) echo 0; return ;;\n";
|
|
117
|
-
o += " esac\n";
|
|
118
|
-
o += " saw=1\n";
|
|
119
|
-
o += " done\n";
|
|
120
|
-
o += " echo 1\n";
|
|
121
|
-
o += " ;;\n";
|
|
122
|
-
}
|
|
123
|
-
o += " *) echo 0 ;;\n";
|
|
124
|
-
o += " esac\n";
|
|
125
|
-
o += "}\n";
|
|
126
|
-
return o;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function emitMatchChildZsh(
|
|
130
|
-
ident: string,
|
|
131
|
-
scopes: ScopeRec[],
|
|
132
|
-
pathIndex: Record<string, number>,
|
|
133
|
-
): string {
|
|
134
|
-
let o = "_${ident}_nac_match_child() {\n".replace("${ident}", ident);
|
|
135
|
-
o += ' local sid="$1" w="$2"\n';
|
|
136
|
-
o += " case $sid in\n";
|
|
137
|
-
for (const [sid, sc] of scopes.entries()) {
|
|
138
|
-
if (sc.kids.length === 0) continue;
|
|
139
|
-
o += ` ${sid})\n`;
|
|
140
|
-
o += " case $w in\n";
|
|
141
|
-
for (const ch of sc.kids) {
|
|
142
|
-
const childPath = sc.path === "" ? ch.key : `${sc.path}/${ch.key}`;
|
|
143
|
-
const cid = pathIndex[childPath] ?? 0;
|
|
144
|
-
o += ` ${ch.key}) echo ${cid}; return 0 ;;\n`;
|
|
145
|
-
}
|
|
146
|
-
o += " esac\n";
|
|
147
|
-
o += " ;;\n";
|
|
148
|
-
}
|
|
149
|
-
o += " esac\n";
|
|
150
|
-
o += " return 1\n";
|
|
151
|
-
o += "}\n";
|
|
152
|
-
return o;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
57
|
function emitSimulateZsh(ident: string): string {
|
|
156
58
|
let o = "_${ident}_nac_simulate() {\n".replace("${ident}", ident);
|
|
157
59
|
o += " local i=2 sid=0 w steps next\n";
|
|
@@ -264,9 +166,9 @@ export function completionZshScript(schema: CliRouter): string {
|
|
|
264
166
|
|
|
265
167
|
let out = `#compdef ${schema.key}\n\n`;
|
|
266
168
|
out += emitScopeArraysZsh(ident, scopes);
|
|
267
|
-
out +=
|
|
268
|
-
out +=
|
|
269
|
-
out +=
|
|
169
|
+
out += emitConsumeLong(ident, scopes);
|
|
170
|
+
out += emitConsumeShort(ident, scopes, "zsh");
|
|
171
|
+
out += emitMatchChild(ident, scopes, pathIndex);
|
|
270
172
|
out += emitSimulateZsh(ident);
|
|
271
173
|
out += emitEnumReplyZsh(ident, scopes);
|
|
272
174
|
out += emitMainBodyZsh(schema, ident);
|
|
@@ -38,7 +38,7 @@ describe("builtins/config", () => {
|
|
|
38
38
|
const configPath = resolveAppConfigPath(program);
|
|
39
39
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
40
40
|
writeFileSync(configPath, `${JSON.stringify({ apiToken: "secret" })}\n`);
|
|
41
|
-
const result = await new Cli(program).invoke(["
|
|
41
|
+
const result = await new Cli(program).invoke(["configure", "get", "apiToken"]);
|
|
42
42
|
expect(result.exitCode).toBe(0);
|
|
43
43
|
expect(result.stdout.trim()).toBe("REDACTED");
|
|
44
44
|
} finally {
|
|
@@ -61,7 +61,7 @@ describe("builtins/config", () => {
|
|
|
61
61
|
const configPath = resolveAppConfigPath(program);
|
|
62
62
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
63
63
|
writeFileSync(configPath, `${JSON.stringify({ apiToken: "secret" })}\n`);
|
|
64
|
-
const result = await new Cli(program).invoke(["
|
|
64
|
+
const result = await new Cli(program).invoke(["configure", "get", "apiToken", "--json"]);
|
|
65
65
|
expect(result.exitCode).toBe(0);
|
|
66
66
|
expect(JSON.parse(result.stdout)).toEqual({ set: true });
|
|
67
67
|
} finally {
|
|
@@ -82,9 +82,9 @@ describe("builtins/config", () => {
|
|
|
82
82
|
try {
|
|
83
83
|
const program = configFixture();
|
|
84
84
|
writeAppConfigFile(program, { apiToken: "seed" });
|
|
85
|
-
const result = await new Cli(program).invoke(["
|
|
85
|
+
const result = await new Cli(program).invoke(["configure", "set", "port", "9090"]);
|
|
86
86
|
expect(result.exitCode).toBe(0);
|
|
87
|
-
const get = await new Cli(program).invoke(["
|
|
87
|
+
const get = await new Cli(program).invoke(["configure", "get", "port"]);
|
|
88
88
|
expect(get.stdout.trim()).toBe("9090");
|
|
89
89
|
} finally {
|
|
90
90
|
if (prevHome === undefined) delete process.env.HOME;
|