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 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.0.3...HEAD
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
@@ -412,11 +412,11 @@ await cli.run();
412
412
 
413
413
  | Field | Default | Purpose |
414
414
  | --- | --- | --- |
415
- | `description` | *(required)* | Shown in prompts, `config get`, and bundle manifests |
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, `config get`, and status |
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 `config get`/`set` skip this exit.
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`, `docs`, or `config` at the root — ArgsBarg injects these when configured.
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
 
@@ -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 `config get` / `config set` | Read/write resolved values (opt-out via `commands: false`) |
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 reserved `config` command.
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 `config` command
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
- | `config get [key]` | Resolved value(s); `--json`; `--json --pretty` |
201
- | `config set <key> <value>` | One key; full document re-validated after merge |
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
- `config get`/`set` skip required-config exit and TTY prompts. Sensitive values redact on `get` (`REDACTED` / `{ "set": true }` with `--json`).
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 `config set`.
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 `config get`/`set` |
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 config get apiToken --json
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 `config get` / `config set` when `program.appConfig.commands` is enabled (default). Hosts inject `user_config` → env at spawn; they never write the argsbarg config file.
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`, `install`, `docs`, `mcp`, `config get`/`set` |
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 config get apiToken --json
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 `config get`.
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 `config get` / `config set`. Default: enabled when `appConfig` is set. */
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "5.0.3",
3
+ "version": "5.1.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3"
@@ -154,8 +154,8 @@ describe("completion emitters", () => {
154
154
 
155
155
  /** Tests for schema export builtins. */
156
156
  describe("schema export builtins", () => {
157
- /** ExportPresentationBuiltins includes config when appConfig set. */
158
- test("exportPresentationBuiltins includes config when appConfig set", () => {
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 += emitConsumeLongZsh(ident, scopes);
268
- out += emitConsumeShortZsh(ident, scopes);
269
- out += emitMatchChildZsh(ident, scopes, pathIndex);
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(["config", "get", "apiToken"]);
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(["config", "get", "apiToken", "--json"]);
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(["config", "set", "port", "9090"]);
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(["config", "get", "port"]);
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;