argsbarg 5.1.0 → 5.1.2

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.2] - 2026-07-04
11
+
12
+ ### Changed
13
+
14
+ - **Private GitHub release installs** — release formulae use `GitHubPrivateReleaseDownloadStrategy` (GitHub API + `GitHub::API.credentials` / `HOMEBREW_GITHUB_API_TOKEN`); `just install-production` and `just test-release` export the token (with `gh auth token` fallback). Documented in `docs/distribution-homebrew.md`.
15
+
16
+ ## [5.1.1] - 2026-07-04
17
+
18
+ ### Fixed
19
+
20
+ - **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.
21
+
22
+ ### Changed
23
+
24
+ - **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.
25
+
10
26
  ## [5.1.0] - 2026-07-04
11
27
 
12
28
  ### Changed
@@ -585,7 +601,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
585
601
  - 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
602
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
587
603
 
588
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v5.1.0...HEAD
604
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v5.1.2...HEAD
605
+ [5.1.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.2
606
+ [5.1.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.1
589
607
  [5.1.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.0
590
608
  [5.0.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.0.3
591
609
  [5.0.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.0.2
package/docs/configure.md CHANGED
@@ -14,6 +14,8 @@ brew install <tap>/<key>
14
14
 
15
15
  Upgrade with `brew upgrade <key>`. Shell completions are installed by Homebrew during `brew install`. Users must configure their shell per [Homebrew Shell Completion](https://docs.brew.sh/Shell-Completion).
16
16
 
17
+ Private GitHub release assets require `HOMEBREW_GITHUB_API_TOKEN` at install time — see [distribution-homebrew.md](distribution-homebrew.md#end-user-install).
18
+
17
19
  **Uninstall the binary:** remove agent artifacts first (while the CLI is still on PATH), then `brew uninstall`:
18
20
 
19
21
  ```bash
@@ -64,7 +66,7 @@ Non-interactive / CI: pass **`--yes`** (or **`--json`**, **`--sync`**, **`--remo
64
66
  | Cursor skill | Y/n prompt | `~/.cursor/skills/<dir>/` when `~/.cursor` exists |
65
67
  | Claude skill | Y/n prompt | `~/.claude/skills/<dir>/` when `~/.claude` exists |
66
68
  | 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 |
69
+ | MCP config | Y/n prompt when `mcpServer.enabled` | Cursor, Claude Code/Desktop, OpenCode, Codex, OpenClaw, ChatGPT desktop |
68
70
  | App config | auto-runs wizard | Interactive wizard writes `~/.local/lib/<key>/config.json` |
69
71
 
70
72
  ### Externally managed binary (Homebrew)
@@ -20,6 +20,16 @@ brew install <tap>/{key}
20
20
  {key} configure # when app config is required
21
21
  ```
22
22
 
23
+ For **private or internal GitHub repos**, release formulae attach `HOMEBREW_GITHUB_API_TOKEN` when downloading the binary. Export a PAT with `repo` scope (or rely on `gh auth login` — `just install-production` and `just test-release` fall back to `gh auth token`):
24
+
25
+ ```bash
26
+ export HOMEBREW_GITHUB_API_TOKEN="$(gh auth token)"
27
+ brew tap <org>/<repo> git@github.com:<org>/<repo>.git
28
+ brew install <tap>/{key}
29
+ ```
30
+
31
+ Local dev installs (`just install-local`) use a `file://` staging URL and do not need the token.
32
+
23
33
  ### Developer install
24
34
 
25
35
  ```bash
@@ -56,6 +66,15 @@ def post_install
56
66
  end
57
67
  ```
58
68
 
69
+ Release formulae generated by `scripts/formula-shared.ts` embed a `GitHubPrivateReleaseDownloadStrategy` that resolves the release asset through the GitHub API at download time and authenticates with `GitHub::API.credentials` (backed by `HOMEBREW_GITHUB_API_TOKEN`, `gh auth`, or other Homebrew GitHub credential sources):
70
+
71
+ ```ruby
72
+ url "https://github.com/<org>/<repo>/releases/download/vX.Y.Z/{key}",
73
+ using: GitHubPrivateReleaseDownloadStrategy
74
+ ```
75
+
76
+ Local dev formulae (`just install-local`) use a plain `file://` URL and do not need the token.
77
+
59
78
  Completions require users to configure their shell per [Homebrew Shell Completion](https://docs.brew.sh/Shell-Completion).
60
79
 
61
80
  **Why configure is separate from `post_install`:** the wizard is interactive (TTY + prompts for secrets). Formula `post_install` runs non-interactively during `brew install` and in CI (`brew test`). Apps with `appConfig` print a one-line configure hint in formula `caveats` instead.
@@ -43,7 +43,7 @@ Production CLIs with several JSON commands tend to use **codegen** so types, han
43
43
 
44
44
  ## Recommended pipeline (copy per repo)
45
45
 
46
- No shared npm package — each app copies the same **contract**. Reference implementations: **sqsp-qa-tools**, **idp-trees**, **sqsp-i18n-tools** (see each repo’s `docs/architecture.md` for which commands use which schema root).
46
+ No shared npm package — each app copies the same **contract**. Reference implementations: **sqsp-qa-manager**, **idp-trees**, **poc-sqsp-i18n-tools** (see each repo’s `docs/architecture.md` for which commands use which schema root).
47
47
 
48
48
  ```mermaid
49
49
  flowchart LR
@@ -59,8 +59,8 @@ Requires [Homebrew](https://brew.sh) and a compiled binary at `dist/full-example
59
59
  just build
60
60
  just install-local # first-time dev formula (`just install` is an alias)
61
61
  just reinstall-local # fast binary swap during development
62
- just install-production # uninstall local dev, install from GitHub tap
63
- just test-release
62
+ just install-production # uninstall local dev, install from GitHub tap (needs HOMEBREW_GITHUB_API_TOKEN for private repos)
63
+ just test-release # same token requirement when formula uses a GitHub release URL
64
64
  ```
65
65
 
66
66
  Undo a local dev install:
@@ -8,6 +8,8 @@ cli_key := `bun scripts/print-identity.ts key`
8
8
  tap_org := `bun scripts/print-identity.ts tapOrg`
9
9
  tap_repo := `bun scripts/print-identity.ts tapRepo`
10
10
  tap := `bun scripts/print-identity.ts tap`
11
+ release_repo := `bun scripts/print-identity.ts releaseRepo`
12
+ tap_git_url := "git@github.com:" + release_repo + ".git"
11
13
  brew_prefix := `brew --prefix`
12
14
  tap_parent := brew_prefix + "/Library/Taps/" + tap_org
13
15
  tap_path := tap_parent + "/homebrew-" + tap_repo
@@ -59,12 +61,15 @@ install-local: build
59
61
  @echo ""
60
62
  @echo "Next: {{cli_key}} configure"
61
63
 
62
- # Remove local dev install, then install from GitHub tap
64
+ # Remove local dev install, then install from GitHub tap (private release assets need HOMEBREW_GITHUB_API_TOKEN)
63
65
  install-production: uninstall
64
- brew tap {{tap}}
65
- brew install --formula {{tap}}/{{cli_key}}
66
- @echo ""
67
- @echo "Next: {{cli_key}} configure"
66
+ #!/usr/bin/env bash
67
+ set -eu -o pipefail
68
+ source scripts/ensure-brew-github-token.sh
69
+ brew tap {{release_repo}} {{tap_git_url}}
70
+ brew install --formula {{release_repo}}/{{cli_key}}
71
+ echo ""
72
+ echo "Next: {{cli_key}} configure"
68
73
 
69
74
  # Alias for backward compatibility
70
75
  reinstall: reinstall-local
@@ -96,10 +101,13 @@ test: check
96
101
 
97
102
  # Install release formula from tap and run formula test
98
103
  test-release:
99
- @brew untap {{tap}} 2>/dev/null || true
104
+ #!/usr/bin/env bash
105
+ set -eu -o pipefail
106
+ source scripts/ensure-brew-github-token.sh
107
+ brew untap {{tap}} 2>/dev/null || true
100
108
  mkdir -p {{tap_parent}}
101
109
  ln -sfn '{{justfile_directory()}}' {{tap_path}}
102
- @brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
110
+ brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
103
111
  brew install --formula {{tap}}/{{cli_key}}
104
112
  brew test {{cli_key}}
105
113
 
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # Ensure HOMEBREW_GITHUB_API_TOKEN is set for private GitHub release downloads.
3
+ : "${HOMEBREW_GITHUB_API_TOKEN:=$(gh auth token 2>/dev/null || true)}"
4
+ if [[ -z "${HOMEBREW_GITHUB_API_TOKEN}" ]]; then
5
+ echo "HOMEBREW_GITHUB_API_TOKEN is required to download private GitHub release assets." >&2
6
+ echo "Export a GitHub PAT with repo scope, or run: gh auth login" >&2
7
+ exit 1
8
+ fi
9
+ export HOMEBREW_GITHUB_API_TOKEN
@@ -30,8 +30,51 @@ export const formulaTestRuby = `test do
30
30
 
31
31
  export interface FormulaCoords {
32
32
  url: string;
33
+ urlStanza: string;
33
34
  version: string;
34
35
  sha256: string;
36
+ /** When true, embed {@link githubPrivateReleaseDownloadStrategyRuby} in the formula class. */
37
+ privateRelease?: boolean;
38
+ }
39
+
40
+ /** Resolves private GitHub release assets via the API at download time. */
41
+ export const githubPrivateReleaseDownloadStrategyRuby = ` class GitHubPrivateReleaseDownloadStrategy < CurlDownloadStrategy
42
+ def initialize(url, name, version, **meta)
43
+ super
44
+ pattern = %r{https://github\\.com/([^/]+)/([^/]+)/releases/download/([^/]+)/(\\S+)}
45
+ match = url.match(pattern)
46
+ raise CurlDownloadStrategyError, "Invalid GitHub release URL: #{url}" unless match
47
+ @owner, @repo, @tag, @filename = match.captures
48
+ end
49
+
50
+ private
51
+
52
+ def _fetch(url:, resolved_url: resolved_download_url, timeout:)
53
+ curl_download resolved_download_url,
54
+ "--header", "Accept: application/octet-stream",
55
+ "--header", "Authorization: Bearer #{GitHub::API.credentials}",
56
+ to: temporary_path
57
+ end
58
+
59
+ def resolved_download_url
60
+ @resolved_download_url ||= begin
61
+ asset = GitHub.get_release(@owner, @repo, @tag).fetch("assets")
62
+ .find { |a| a["name"] == @filename }
63
+ raise CurlDownloadStrategyError, "Release asset not found: #{@filename}" unless asset
64
+ asset.fetch("url")
65
+ end
66
+ end
67
+ end`;
68
+
69
+ /** Homebrew `url` for local dev staging paths. */
70
+ export function devUrlStanza(url: string): string {
71
+ return `url "${url}"`;
72
+ }
73
+
74
+ /** Homebrew `url` for GitHub release assets (uses {@link githubPrivateReleaseDownloadStrategyRuby}). */
75
+ export function releaseUrlStanza(url: string): string {
76
+ return `url "${url}",
77
+ using: GitHubPrivateReleaseDownloadStrategy`;
35
78
  }
36
79
 
37
80
  export function releaseFormulaUrl(version: string): string {
@@ -39,10 +82,11 @@ export function releaseFormulaUrl(version: string): string {
39
82
  }
40
83
 
41
84
  export function renderFormula(coords: FormulaCoords): string {
85
+ const strategy = coords.privateRelease ? `${githubPrivateReleaseDownloadStrategyRuby}\n\n ` : "";
42
86
  return `class ${className} < Formula
43
- desc "${desc}"
87
+ ${strategy}desc "${desc}"
44
88
  homepage "${homepage}"
45
- url "${coords.url}"
89
+ ${coords.urlStanza}
46
90
  version "${coords.version}"
47
91
  sha256 "${coords.sha256}"
48
92
  {dependsOnBlock}
@@ -58,16 +102,21 @@ end
58
102
  }
59
103
 
60
104
  export function renderReleaseFormula(version: string, sha256: string): string {
105
+ const url = releaseFormulaUrl(version);
61
106
  return renderFormula({
62
- url: releaseFormulaUrl(version),
107
+ url,
108
+ urlStanza: releaseUrlStanza(url),
63
109
  version,
64
110
  sha256,
111
+ privateRelease: true,
65
112
  });
66
113
  }
67
114
 
68
115
  export function renderDevFormula(stagingPath: string, version: string, sha256: string): string {
116
+ const url = `file://${stagingPath}`;
69
117
  return renderFormula({
70
- url: `file://${stagingPath}`,
118
+ url,
119
+ urlStanza: devUrlStanza(url),
71
120
  version,
72
121
  sha256,
73
122
  });
@@ -5,7 +5,7 @@ import { createIdentity } from "./create-identity.ts";
5
5
 
6
6
  const field = process.argv[2];
7
7
  if (!field) {
8
- console.error("Usage: bun scripts/print-identity.ts <key|className|tap|tapOrg|tapRepo|envPrefix>");
8
+ console.error("Usage: bun scripts/print-identity.ts <key|className|tap|tapOrg|tapRepo|releaseRepo|envPrefix>");
9
9
  process.exit(1);
10
10
  }
11
11
 
@@ -17,6 +17,7 @@ const values: Record<string, string> = {
17
17
  tapOrg: tapOrg ?? "",
18
18
  tapRepo: tapRepo ?? "",
19
19
  envPrefix: createIdentity.envPrefix,
20
+ releaseRepo: createIdentity.releaseRepo,
20
21
  };
21
22
 
22
23
  const value = values[field];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "5.1.0",
3
+ "version": "5.1.2",
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. */