argsbarg 5.1.0 → 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,16 @@ 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
+
10
20
  ## [5.1.0] - 2026-07-04
11
21
 
12
22
  ### Changed
@@ -585,7 +595,8 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
585
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`).
586
596
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
587
597
 
588
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v5.1.0...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
589
600
  [5.1.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.0
590
601
  [5.0.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.0.3
591
602
  [5.0.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.0.2
package/docs/configure.md CHANGED
@@ -64,7 +64,7 @@ Non-interactive / CI: pass **`--yes`** (or **`--json`**, **`--sync`**, **`--remo
64
64
  | Cursor skill | Y/n prompt | `~/.cursor/skills/<dir>/` when `~/.cursor` exists |
65
65
  | Claude skill | Y/n prompt | `~/.claude/skills/<dir>/` when `~/.claude` exists |
66
66
  | Codex / OpenCode / OpenClaw skills | Y/n prompt | Agent-specific dirs when available |
67
- | 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 |
68
68
  | App config | auto-runs wizard | Interactive wizard writes `~/.local/lib/<key>/config.json` |
69
69
 
70
70
  ### Externally managed binary (Homebrew)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "5.1.0",
3
+ "version": "5.1.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3"
@@ -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);
@@ -2,8 +2,8 @@
2
2
  TTY prompts for per-target install, skip, or uninstall during interactive `configure`.
3
3
  */
4
4
 
5
- import { readSync } from "node:fs";
6
5
  import type { CliInstallArtifactKey } from "../install/target-types.ts";
6
+ import { readPromptLine } from "../prompt.ts";
7
7
 
8
8
  /** Human-readable labels for each install artifact key in interactive prompts. */
9
9
  const LABELS: Record<CliInstallArtifactKey, string> = {
@@ -35,9 +35,7 @@ export type TargetPromptAction = "install" | "skip" | "uninstall";
35
35
  export function promptTargetAction(label: string, installed: boolean): TargetPromptAction | null {
36
36
  const hint = installed ? `${label} [y/N]: ` : `${label} [Y/n]: `;
37
37
  process.stderr.write(hint);
38
- const buf = Buffer.alloc(256);
39
- const n = readSync(0, buf, { length: 256 });
40
- const ans = buf.toString("utf8", 0, n).trim().toLowerCase();
38
+ const ans = readPromptLine().trim().toLowerCase();
41
39
  if (installed) {
42
40
  if (ans === "n") return "uninstall";
43
41
  return "skip";
@@ -63,4 +63,15 @@ describe("validateConfigureConfig", () => {
63
63
  };
64
64
  expect(() => cliValidateProgram(program)).not.toThrow();
65
65
  });
66
+
67
+ test("rejects explicit MCP target without mcpServer", () => {
68
+ const program: CliProgram = {
69
+ ...base,
70
+ configure: {
71
+ agentIntegration: "both",
72
+ targets: { cursorMcp: true },
73
+ },
74
+ };
75
+ expect(() => cliValidateProgram(program)).toThrow(/mcpServer\.enabled/);
76
+ });
66
77
  });
@@ -7,7 +7,12 @@ import type {
7
7
  InstallTargetSpec,
8
8
  } from "../types.ts";
9
9
  import type { InstallPaths } from "./paths.ts";
10
- import { AGENT_PAIRS, INSTALL_ARTIFACT_KEYS, installTargetForKey } from "./target-registry.ts";
10
+ import {
11
+ AGENT_PAIRS,
12
+ INSTALL_ARTIFACT_KEYS,
13
+ installTargetForKey,
14
+ mcpServerRequiredForArtifact,
15
+ } from "./target-registry.ts";
11
16
  import type { CliInstallArtifactKey, InstallPlanMode } from "./target-types.ts";
12
17
 
13
18
  export type { InstallTargetSpec, ResolvedInstallTarget } from "../types.ts";
@@ -44,9 +49,13 @@ export function resolveInstallTargetSpec(
44
49
  function artifactDefaults(
45
50
  key: CliInstallArtifactKey,
46
51
  integration: InstallAgentIntegration,
52
+ mcpServerEnabled: boolean,
47
53
  ): { enabled: boolean; includedInAll: boolean } {
48
54
  const target = installTargetForKey(key);
49
55
  const includedInAll = target?.defaultIncludedInAll(integration) ?? false;
56
+ if (!mcpServerRequiredForArtifact(key, mcpServerEnabled)) {
57
+ return { enabled: false, includedInAll: false };
58
+ }
50
59
  return { enabled: true, includedInAll };
51
60
  }
52
61
 
@@ -86,7 +95,10 @@ export function resolveEffectiveInstallTargets(
86
95
  const user = configure?.targets;
87
96
  const out = {} as Record<CliInstallArtifactKey, { enabled: boolean; includedInAll: boolean }>;
88
97
  for (const key of INSTALL_ARTIFACT_KEYS) {
89
- out[key] = resolveInstallTargetSpec(user?.[key], artifactDefaults(key, integration));
98
+ out[key] = resolveInstallTargetSpec(
99
+ user?.[key],
100
+ artifactDefaults(key, integration, mcpEnabled),
101
+ );
90
102
  }
91
103
  applyAgentPairDedupe(out, user, integration);
92
104
  return out;
@@ -37,6 +37,23 @@ export function installTargetForKey(key: CliInstallArtifactKey): InstallTarget |
37
37
  return targetByKey.get(key);
38
38
  }
39
39
 
40
+ /** True when the artifact key is an MCP install target (requires mcpServer.enabled). */
41
+ export function isMcpArtifactKey(key: CliInstallArtifactKey): boolean {
42
+ return installTargetForKey(key)?.category === "mcp";
43
+ }
44
+
45
+ /**
46
+ * MCP install targets are inactive without mcpServer.enabled.
47
+ * Defaults: artifactDefaults in target-effective.ts.
48
+ * Explicit spec rejection: validateConfigureConfig in validate.ts.
49
+ */
50
+ export function mcpServerRequiredForArtifact(
51
+ key: CliInstallArtifactKey,
52
+ mcpServerEnabled: boolean,
53
+ ): boolean {
54
+ return !isMcpArtifactKey(key) || mcpServerEnabled;
55
+ }
56
+
40
57
  /** Lookup by plan action kind. */
41
58
  export function installTargetForActionKind(kind: InstallActionKind): InstallTarget | undefined {
42
59
  return INSTALL_TARGETS.find((t) => t.actionKind === kind);
@@ -50,7 +50,9 @@ describe("resolveEffectiveInstallTargets", () => {
50
50
  const program: Pick<CliProgram, "mcpServer"> = {};
51
51
  const t = resolveEffectiveInstallTargets(undefined, program);
52
52
  expect(t.cursorSkill.includedInAll).toBe(true);
53
+ expect(t.cursorSkill.enabled).toBe(true);
53
54
  expect(t.cursorMcp.includedInAll).toBe(false);
55
+ expect(t.cursorMcp.enabled).toBe(false);
54
56
  });
55
57
 
56
58
  test("mcp mode includes MCP in --all not paired skills", () => {
package/src/parse.ts CHANGED
@@ -249,21 +249,6 @@ function finishLeaf(
249
249
  optionDefs: CliOption[],
250
250
  forcePositionalsIn: boolean,
251
251
  ): ParseResult {
252
- /** Builds a parse error for positional consumption failures. */
253
- function errorResult(msg: string): ParseResult {
254
- const pr: ParseResult = {
255
- kind: ParseKind.Error,
256
- path: [],
257
- opts: {},
258
- args: [],
259
- helpExplicit: false,
260
- helpPath: [],
261
- errorMsg: msg,
262
- errorHelpPath: path,
263
- };
264
- return pr;
265
- }
266
-
267
252
  let idx = startIdx;
268
253
  const args: string[] = [];
269
254
  let forcePositionals = forcePositionalsIn;
@@ -273,7 +258,7 @@ function finishLeaf(
273
258
  if (argMax === 1) {
274
259
  if (argMin >= 1) {
275
260
  if (idx >= argv.length) {
276
- return errorResult(`Missing positional argument: ${p.name}`);
261
+ return errorResult(`Missing positional argument: ${p.name}`, path, []);
277
262
  }
278
263
  args.push(argv[idx]);
279
264
  idx += 1;
@@ -308,7 +293,7 @@ function finishLeaf(
308
293
  // MUST be false — lenient mode swallows unknown flags as positionals silently
309
294
  const tailRep = consumeOptions(optionDefs, false, argv, idx, opts);
310
295
  if (tailRep.report.err) {
311
- return errorResult(tailRep.report.err);
296
+ return errorResult(tailRep.report.err, path, []);
312
297
  }
313
298
  if (tailRep.report.sawDoubleDash) {
314
299
  forcePositionals = true;
@@ -317,7 +302,7 @@ function finishLeaf(
317
302
  idx = tailRep.nextIndex;
318
303
  continue;
319
304
  }
320
- return errorResult(`Unexpected option token: ${tok}`);
305
+ return errorResult(`Unexpected option token: ${tok}`, path, []);
321
306
  }
322
307
 
323
308
  args.push(tok);
@@ -332,13 +317,17 @@ function finishLeaf(
332
317
  }
333
318
  }
334
319
  if (count < argMin) {
335
- return errorResult(`Expected at least ${argMin} argument(s) for ${p.name}, got ${count}`);
320
+ return errorResult(
321
+ `Expected at least ${argMin} argument(s) for ${p.name}, got ${count}`,
322
+ path,
323
+ [],
324
+ );
336
325
  }
337
326
  }
338
327
 
339
328
  if (idx < argv.length) {
340
329
  if (forcePositionals) {
341
- return errorResult("Unexpected extra arguments");
330
+ return errorResult("Unexpected extra arguments", path, []);
342
331
  }
343
332
 
344
333
  if (isHelpTok(argv[idx])) {
@@ -347,12 +336,12 @@ function finishLeaf(
347
336
 
348
337
  const tailRep = consumeOptions(optionDefs, false, argv, idx, opts);
349
338
  if (tailRep.report.err) {
350
- return errorResult(tailRep.report.err);
339
+ return errorResult(tailRep.report.err, path, []);
351
340
  }
352
341
  idx = tailRep.nextIndex;
353
342
 
354
343
  if (idx < argv.length) {
355
- return errorResult("Unexpected extra arguments");
344
+ return errorResult("Unexpected extra arguments", path, []);
356
345
  }
357
346
  }
358
347
 
@@ -370,6 +359,24 @@ function finishLeaf(
370
359
 
371
360
  // ── Main Parser ───────────────────────────────────────────────────────────────
372
361
 
362
+ /** Builds a user-error parse result; `path` defaults to `errorHelpPath`. */
363
+ function errorResult(
364
+ errorMsg: string,
365
+ errorHelpPath: string[] = [],
366
+ path: string[] = errorHelpPath,
367
+ ): ParseResult {
368
+ return {
369
+ kind: ParseKind.Error,
370
+ path,
371
+ opts: {},
372
+ args: [],
373
+ helpExplicit: false,
374
+ helpPath: [],
375
+ errorMsg,
376
+ errorHelpPath,
377
+ };
378
+ }
379
+
373
380
  /** Builds a help-request result for the current routing path. */
374
381
  function helpResult(p: string[], explicit: boolean): ParseResult {
375
382
  return {
@@ -401,16 +408,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
401
408
  // Consume root-level options first
402
409
  const rootRep = consumeOptions(root.options ?? [], rootLenient, argv, i, opts);
403
410
  if (rootRep.report.err) {
404
- return {
405
- kind: ParseKind.Error,
406
- path: [],
407
- opts: {},
408
- args: [],
409
- helpExplicit: false,
410
- helpPath: [],
411
- errorMsg: rootRep.report.err,
412
- errorHelpPath: [],
413
- };
411
+ return errorResult(rootRep.report.err);
414
412
  }
415
413
  i = rootRep.nextIndex;
416
414
  let forcePositionals = rootRep.report.sawDoubleDash;
@@ -436,16 +434,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
436
434
  cmdName = root.fallbackCommand;
437
435
  node = findChild(root.commands, cmdName);
438
436
  if (!node) {
439
- return {
440
- kind: ParseKind.Error,
441
- path: [],
442
- opts: {},
443
- args: [],
444
- helpExplicit: false,
445
- helpPath: [],
446
- errorMsg: `Unknown command: ${cmdName}`,
447
- errorHelpPath: path,
448
- };
437
+ return errorResult(`Unknown command: ${cmdName}`, path, []);
449
438
  }
450
439
  } else {
451
440
  return helpResult([], false);
@@ -469,34 +458,20 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
469
458
  cmdName = fallbackCommand;
470
459
  node = findChild(root.commands, cmdName);
471
460
  if (!node) {
472
- return {
473
- kind: ParseKind.Error,
474
- path: [],
475
- opts: {},
476
- args: [],
477
- helpExplicit: false,
478
- helpPath: [],
479
- errorMsg: `Unknown command: ${cmdName}`,
480
- errorHelpPath: path,
481
- };
461
+ return errorResult(`Unknown command: ${cmdName}`, path, []);
482
462
  }
483
463
  } else {
484
464
  cmdName = peek;
485
465
  if (!forcePositionals) i += 1;
486
466
  node = findChild(root.commands, cmdName);
487
467
  if (!node) {
488
- return {
489
- kind: ParseKind.Error,
490
- path: [],
491
- opts: {},
492
- args: [],
493
- helpExplicit: false,
494
- helpPath: [],
495
- errorMsg: forcePositionals
468
+ return errorResult(
469
+ forcePositionals
496
470
  ? `Expected subcommand but got positional: ${cmdName}`
497
471
  : `Unknown command: ${cmdName}`,
498
- errorHelpPath: path,
499
- };
472
+ path,
473
+ [],
474
+ );
500
475
  }
501
476
  }
502
477
  }
@@ -504,16 +479,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
504
479
 
505
480
  path.push(cmdName);
506
481
  if (!node) {
507
- return {
508
- kind: ParseKind.Error,
509
- path,
510
- opts: {},
511
- args: [],
512
- helpExplicit: false,
513
- helpPath: [],
514
- errorMsg: `Unknown command: ${cmdName}`,
515
- errorHelpPath: path,
516
- };
482
+ return errorResult(`Unknown command: ${cmdName}`, path);
517
483
  }
518
484
  let current = node;
519
485
 
@@ -522,16 +488,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
522
488
  if (!forcePositionals) {
523
489
  const orep = consumeOptions(collectOptionDefs(root, path), false, argv, i, opts);
524
490
  if (orep.report.err) {
525
- return {
526
- kind: ParseKind.Error,
527
- path,
528
- opts: {},
529
- args: [],
530
- helpExplicit: false,
531
- helpPath: [],
532
- errorMsg: orep.report.err,
533
- errorHelpPath: path,
534
- };
491
+ return errorResult(orep.report.err, path);
535
492
  }
536
493
  i = orep.nextIndex;
537
494
  if (orep.report.sawDoubleDash) {
@@ -576,16 +533,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
576
533
 
577
534
  const tok = argv[i];
578
535
  if (!forcePositionals && tok.startsWith("-")) {
579
- return {
580
- kind: ParseKind.Error,
581
- path,
582
- opts: {},
583
- args: [],
584
- helpExplicit: false,
585
- helpPath: [],
586
- errorMsg: `Unexpected option token: ${tok}`,
587
- errorHelpPath: path,
588
- };
536
+ return errorResult(`Unexpected option token: ${tok}`, path);
589
537
  }
590
538
 
591
539
  if (!forcePositionals && isCliRouter(current)) {
@@ -614,18 +562,12 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
614
562
  }
615
563
  }
616
564
 
617
- return {
618
- kind: ParseKind.Error,
619
- path,
620
- opts: {},
621
- args: [],
622
- helpExplicit: false,
623
- helpPath: [],
624
- errorMsg: forcePositionals
565
+ return errorResult(
566
+ forcePositionals
625
567
  ? `Expected subcommand but got positional: ${tok}`
626
568
  : `Unknown subcommand: ${tok}`,
627
- errorHelpPath: path,
628
- };
569
+ path,
570
+ );
629
571
  }
630
572
 
631
573
  if (!isCliLeaf(current)) {
@@ -656,29 +598,11 @@ export function postParseValidate(root: CliNode, pr: ParseResult): ParseResult {
656
598
 
657
599
  for (const seg of pr.path) {
658
600
  if (!isCliRouter(node)) {
659
- return {
660
- kind: ParseKind.Error,
661
- path: pr.path,
662
- opts: {},
663
- args: [],
664
- helpExplicit: false,
665
- helpPath: [],
666
- errorMsg: "Internal path error",
667
- errorHelpPath: pr.path,
668
- };
601
+ return errorResult("Internal path error", pr.path);
669
602
  }
670
603
  const ch = findChild(node.commands, seg);
671
604
  if (!ch) {
672
- return {
673
- kind: ParseKind.Error,
674
- path: pr.path,
675
- opts: {},
676
- args: [],
677
- helpExplicit: false,
678
- helpPath: [],
679
- errorMsg: "Internal path error",
680
- errorHelpPath: pr.path,
681
- };
605
+ return errorResult("Internal path error", pr.path);
682
606
  }
683
607
  defs.push(...(ch.options ?? []));
684
608
  node = ch;
@@ -693,60 +617,24 @@ export function postParseValidate(root: CliNode, pr: ParseResult): ParseResult {
693
617
 
694
618
  for (const d of defs) {
695
619
  if (d.required && !(d.name in opts)) {
696
- return {
697
- kind: ParseKind.Error,
698
- path: pr.path,
699
- opts: {},
700
- args: [],
701
- helpExplicit: false,
702
- helpPath: [],
703
- errorMsg: `Missing required option: --${d.name}`,
704
- errorHelpPath: pr.path,
705
- };
620
+ return errorResult(`Missing required option: --${d.name}`, pr.path);
706
621
  }
707
622
  }
708
623
 
709
624
  for (const [k, v] of Object.entries(opts)) {
710
625
  const d = findOptionByName(defs, k);
711
626
  if (!d) {
712
- return {
713
- kind: ParseKind.Error,
714
- path: pr.path,
715
- opts: {},
716
- args: [],
717
- helpExplicit: false,
718
- helpPath: [],
719
- errorMsg: `Unknown option key: ${k}`,
720
- errorHelpPath: pr.path,
721
- };
627
+ return errorResult(`Unknown option key: ${k}`, pr.path);
722
628
  }
723
629
  if (d.kind === CliOptionKind.Number) {
724
630
  if (!fullStringIsDouble(v)) {
725
- return {
726
- kind: ParseKind.Error,
727
- path: pr.path,
728
- opts: {},
729
- args: [],
730
- helpExplicit: false,
731
- helpPath: [],
732
- errorMsg: `Invalid number for option --${k}: ${v}`,
733
- errorHelpPath: pr.path,
734
- };
631
+ return errorResult(`Invalid number for option --${k}: ${v}`, pr.path);
735
632
  }
736
633
  }
737
634
  if (d.kind === CliOptionKind.Enum) {
738
635
  const choices = d.choices ?? [];
739
636
  if (!choices.includes(v)) {
740
- return {
741
- kind: ParseKind.Error,
742
- path: pr.path,
743
- opts: {},
744
- args: [],
745
- helpExplicit: false,
746
- helpPath: [],
747
- errorMsg: `Option --${k}: '${v}' is not one of: ${choices.join(", ")}`,
748
- errorHelpPath: pr.path,
749
- };
637
+ return errorResult(`Option --${k}: '${v}' is not one of: ${choices.join(", ")}`, pr.path);
750
638
  }
751
639
  }
752
640
  if (d.kind === CliOptionKind.String && (d.format !== undefined || d.pattern !== undefined)) {
@@ -759,16 +647,7 @@ export function postParseValidate(root: CliNode, pr: ParseResult): ParseResult {
759
647
  : err instanceof Error
760
648
  ? err.message
761
649
  : String(err);
762
- return {
763
- kind: ParseKind.Error,
764
- path: pr.path,
765
- opts: {},
766
- args: [],
767
- helpExplicit: false,
768
- helpPath: [],
769
- errorMsg: `Invalid value for option --${k}: ${msg}`,
770
- errorHelpPath: pr.path,
771
- };
650
+ return errorResult(`Invalid value for option --${k}: ${msg}`, pr.path);
772
651
  }
773
652
  }
774
653
  }
package/src/validate.ts CHANGED
@@ -6,7 +6,7 @@ import { reservedCommandNames, resolveCapabilities } from "./capabilities.ts";
6
6
  import { reservedDocsTopicResourceUris } from "./docs/mcp-resources.ts";
7
7
  import { DOCS_BUILTIN_TOPIC_KEYS } from "./docs/resolve.ts";
8
8
  import { validateFormatValue } from "./formats.ts";
9
- import { AGENT_PAIRS } from "./install/target-registry.ts";
9
+ import { AGENT_PAIRS, MCP_KEYS, mcpServerRequiredForArtifact } from "./install/target-registry.ts";
10
10
  import { resolveMcpSchemaUri } from "./mcp/tools.ts";
11
11
  import {
12
12
  type CliLeaf,
@@ -150,6 +150,8 @@ function validateConfigureConfig(program: CliProgram): void {
150
150
  const integration: InstallAgentIntegration =
151
151
  configure.agentIntegration ?? (program.mcpServer?.enabled === true ? "mcp" : "skill");
152
152
 
153
+ const mcpEnabled = program.mcpServer?.enabled === true;
154
+
153
155
  for (const [mcpKey, skillKey] of AGENT_PAIRS) {
154
156
  const mcpSpec = targets[mcpKey];
155
157
  const skillSpec = targets[skillKey];
@@ -174,6 +176,15 @@ function validateConfigureConfig(program: CliProgram): void {
174
176
  );
175
177
  }
176
178
  }
179
+
180
+ for (const mcpKey of MCP_KEYS) {
181
+ if (
182
+ !mcpServerRequiredForArtifact(mcpKey, mcpEnabled) &&
183
+ installTargetExplicitTruthy(targets[mcpKey])
184
+ ) {
185
+ throw new CliSchemaValidationError(`configure.targets.${mcpKey} requires mcpServer.enabled`);
186
+ }
187
+ }
177
188
  }
178
189
 
179
190
  /** Validates a program schema. */