argsbarg 5.1.8 → 5.1.10

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/docs/cli-program.md +3 -1
  3. package/docs/config-schema.md +5 -3
  4. package/docs/configure.md +13 -7
  5. package/docs/distribution-homebrew.md +17 -3
  6. package/examples/full-example/justfile +6 -5
  7. package/examples/full-example/scripts/formula-shared.test.ts +68 -0
  8. package/examples/full-example/scripts/formula-shared.ts +39 -1
  9. package/examples/full-example/scripts/release.ts +212 -0
  10. package/index.d.ts +10 -1
  11. package/package.json +2 -1
  12. package/src/builtins/builtins.test.ts +1 -3
  13. package/src/builtins/completion-bash.ts +6 -27
  14. package/src/builtins/completion-group.ts +1 -2
  15. package/src/builtins/completion-simulate-shared.ts +2 -10
  16. package/src/builtins/completion-zsh.ts +7 -33
  17. package/src/builtins/config.test.ts +28 -0
  18. package/src/builtins/config.ts +40 -10
  19. package/src/builtins/configure-copy.ts +3 -6
  20. package/src/builtins/dispatch.ts +2 -10
  21. package/src/builtins/export.ts +1 -3
  22. package/src/builtins/presentation.ts +2 -7
  23. package/src/builtins/registry.ts +1 -5
  24. package/src/cli-tool/create.test.ts +3 -6
  25. package/src/cli-tool/create.ts +5 -22
  26. package/src/cli-tool/post-create.ts +1 -3
  27. package/src/cli-tool/program.ts +1 -2
  28. package/src/cli-tool/run-create.ts +2 -8
  29. package/src/cli.ts +6 -27
  30. package/src/config/bindings.test.ts +72 -0
  31. package/src/config/bindings.ts +115 -0
  32. package/src/config/bootstrap.ts +80 -65
  33. package/src/config/context.test.ts +45 -0
  34. package/src/config/context.ts +74 -14
  35. package/src/config/entry.ts +1 -3
  36. package/src/config/file.test.ts +46 -3
  37. package/src/config/file.ts +59 -19
  38. package/src/config/resolve.ts +1 -4
  39. package/src/config/schema.ts +1 -3
  40. package/src/config/validate.test.ts +3 -11
  41. package/src/config/validate.ts +30 -30
  42. package/src/config.integration.test.ts +1 -5
  43. package/src/configure/configure.test.ts +19 -5
  44. package/src/configure/index.ts +17 -15
  45. package/src/docs/api-guide.ts +1 -6
  46. package/src/docs/builtin.ts +1 -5
  47. package/src/docs/docs.test.ts +2 -6
  48. package/src/docs/mcp-guide.ts +3 -13
  49. package/src/docs/mcp-resources.test.ts +1 -3
  50. package/src/docs/mcp-resources.ts +1 -6
  51. package/src/docs/save.ts +2 -10
  52. package/src/formats.test.ts +1 -7
  53. package/src/formats.ts +1 -5
  54. package/src/headless.test.ts +5 -17
  55. package/src/help.ts +9 -51
  56. package/src/hidden-mcpb.test.ts +3 -12
  57. package/src/hidden.ts +1 -3
  58. package/src/install/binary-placement.test.ts +2 -6
  59. package/src/install/binary-placement.ts +1 -4
  60. package/src/install/gh-release-update.ts +5 -22
  61. package/src/install/mcp-codex.ts +1 -2
  62. package/src/install/mcp-config.ts +2 -12
  63. package/src/install/mcp-opencode.test.ts +1 -3
  64. package/src/install/mcp-opencode.ts +3 -15
  65. package/src/install/plan.ts +2 -10
  66. package/src/install/status.ts +3 -5
  67. package/src/install/target-base.ts +2 -11
  68. package/src/install/target-detect.ts +1 -5
  69. package/src/install/target-effective.ts +3 -13
  70. package/src/install/target-mcp-cli.ts +4 -26
  71. package/src/install/target-mcp-json.ts +3 -14
  72. package/src/install/target-plan-build.ts +1 -5
  73. package/src/install/target-registry.ts +13 -21
  74. package/src/install/target-scope.ts +8 -25
  75. package/src/install/target-types.ts +1 -6
  76. package/src/install/targets/app.ts +2 -2
  77. package/src/install/targets/opencode-mcp.ts +1 -6
  78. package/src/install/targets.test.ts +3 -10
  79. package/src/install/uninstall.ts +2 -9
  80. package/src/invoke.test.ts +1 -3
  81. package/src/mcp/bundle.ts +4 -19
  82. package/src/mcp/claude.test.ts +3 -3
  83. package/src/mcp/claude.ts +4 -17
  84. package/src/mcp/env.ts +1 -3
  85. package/src/mcp/server.ts +3 -12
  86. package/src/mcp/tools.ts +2 -11
  87. package/src/mcp.integration.test.ts +4 -10
  88. package/src/parse.test.ts +9 -34
  89. package/src/parse.ts +10 -44
  90. package/src/schema.ts +2 -10
  91. package/src/skill/hint.ts +1 -5
  92. package/src/skill/install.ts +1 -5
  93. package/src/test-fixtures.ts +1 -3
  94. package/src/validate.ts +29 -90
package/src/docs/save.ts CHANGED
@@ -21,20 +21,12 @@ export function docsSaveGeneratedHint(program: CliProgram, topic: string): strin
21
21
  }
22
22
 
23
23
  /** Inserts save hint without breaking YAML frontmatter (`docs skill`). */
24
- export function applySaveGeneratedHint(
25
- program: CliProgram,
26
- topic: string,
27
- content: string,
28
- ): string {
24
+ export function applySaveGeneratedHint(program: CliProgram, topic: string, content: string): string {
29
25
  if (!docsTopicIsGeneratedByArgsbarg(topic)) {
30
26
  return content;
31
27
  }
32
28
  const hint = docsSaveGeneratedHint(program, topic);
33
- return insertGeneratedHint(
34
- content,
35
- hint,
36
- topic === "skill" ? { afterFrontmatter: true } : undefined,
37
- );
29
+ return insertGeneratedHint(content, hint, topic === "skill" ? { afterFrontmatter: true } : undefined);
38
30
  }
39
31
 
40
32
  /** File body for `--save` (hint on argsbarg-generated markdown only). */
@@ -3,13 +3,7 @@ Tests for formats module behavior.
3
3
  */
4
4
 
5
5
  import { expect, test } from "bun:test";
6
- import {
7
- parseCommaList,
8
- parseDate,
9
- parseDateTime,
10
- parseDurationMs,
11
- validateFormatValue,
12
- } from "./formats.ts";
6
+ import { parseCommaList, parseDate, parseDateTime, parseDurationMs, validateFormatValue } from "./formats.ts";
13
7
  import { CliValueFormat } from "./types.ts";
14
8
 
15
9
  test("parseDurationMs parses minutes and hours", () => {
package/src/formats.ts CHANGED
@@ -108,11 +108,7 @@ export function formatValidationError(format: CliValueFormat, value: string): st
108
108
  }
109
109
 
110
110
  /** Validates a string value against format and/or pattern metadata. */
111
- export function validateFormatValue(
112
- value: string,
113
- format?: CliValueFormat,
114
- pattern?: string,
115
- ): void {
111
+ export function validateFormatValue(value: string, format?: CliValueFormat, pattern?: string): void {
116
112
  if (format !== undefined) {
117
113
  switch (format) {
118
114
  case CliValueFormat.Duration:
@@ -26,28 +26,16 @@ test("shouldRunHeadless is true for MCP and json", () => {
26
26
  });
27
27
 
28
28
  test("shouldRunHeadlessWithPositionals requires positionals in non-tty", () => {
29
- expect(shouldRunHeadlessWithPositionals({ invocation: "cli" }, false, [], false, false)).toBe(
30
- false,
31
- );
32
- expect(shouldRunHeadlessWithPositionals({ invocation: "cli" }, false, ["a"], false, false)).toBe(
33
- true,
34
- );
29
+ expect(shouldRunHeadlessWithPositionals({ invocation: "cli" }, false, [], false, false)).toBe(false);
30
+ expect(shouldRunHeadlessWithPositionals({ invocation: "cli" }, false, ["a"], false, false)).toBe(true);
35
31
  });
36
32
 
37
33
  /** Tests that shouldRunHeadlessWithYes requires yes in non-tty. */
38
34
  test("shouldRunHeadlessWithYes requires yes in non-tty", () => {
35
+ expect(shouldRunHeadlessWithYes({ invocation: "cli" }, { yes: true, hasRequiredArgs: true }, false)).toBe(true);
36
+ expect(shouldRunHeadlessWithYes({ invocation: "cli" }, { yes: false, hasRequiredArgs: true }, false)).toBe(false);
39
37
  expect(
40
- shouldRunHeadlessWithYes({ invocation: "cli" }, { yes: true, hasRequiredArgs: true }, false),
41
- ).toBe(true);
42
- expect(
43
- shouldRunHeadlessWithYes({ invocation: "cli" }, { yes: false, hasRequiredArgs: true }, false),
44
- ).toBe(false);
45
- expect(
46
- shouldRunHeadlessWithYes(
47
- { invocation: "cli" },
48
- { yes: false, hasRequiredArgs: true, dryRun: true },
49
- false,
50
- ),
38
+ shouldRunHeadlessWithYes({ invocation: "cli" }, { yes: false, hasRequiredArgs: true, dryRun: true }, false),
51
39
  ).toBe(true);
52
40
  });
53
41
 
package/src/help.ts CHANGED
@@ -252,20 +252,10 @@ function renderTextBox(title: string, lines: string[], hw: number, color: boolea
252
252
 
253
253
  for (const line of lines) {
254
254
  const padded = padVisible(line, contentWidth);
255
- out.push(
256
- (color ? style.gray(kBoxV) : kBoxV) +
257
- " " +
258
- padded +
259
- " " +
260
- (color ? style.gray(kBoxV) : kBoxV),
261
- );
255
+ out.push(`${color ? style.gray(kBoxV) : kBoxV} ${padded} ${color ? style.gray(kBoxV) : kBoxV}`);
262
256
  }
263
257
 
264
- out.push(
265
- color
266
- ? style.gray(kBoxBL + repeatBoxH(borderWidth) + kBoxBR)
267
- : kBoxBL + repeatBoxH(borderWidth) + kBoxBR,
268
- );
258
+ out.push(color ? style.gray(kBoxBL + repeatBoxH(borderWidth) + kBoxBR) : kBoxBL + repeatBoxH(borderWidth) + kBoxBR);
269
259
 
270
260
  return out;
271
261
  }
@@ -287,11 +277,7 @@ function renderTableBox(title: string, rows: HelpRow[], hw: number, color: boole
287
277
  const bodyLines: string[] = [];
288
278
  for (const row of rows) {
289
279
  const wrapped = wrapText(row.description, descWidth);
290
- const first =
291
- row.label +
292
- spaces(labelWidth - visibleWidth(row.label)) +
293
- " " +
294
- (color ? style.white(wrapped[0]) : wrapped[0]);
280
+ const first = `${row.label + spaces(labelWidth - visibleWidth(row.label))} ${color ? style.white(wrapped[0]) : wrapped[0]}`;
295
281
  bodyLines.push(first);
296
282
  for (let idx = 1; idx < wrapped.length; idx++) {
297
283
  const pad = color ? style.gray(spaces(labelWidth)) : spaces(labelWidth);
@@ -324,20 +310,10 @@ function renderTableBox(title: string, rows: HelpRow[], hw: number, color: boole
324
310
 
325
311
  for (const line of bodyLines) {
326
312
  const padded = padVisible(line, contentWidth);
327
- out.push(
328
- (color ? style.gray(kBoxV) : kBoxV) +
329
- " " +
330
- padded +
331
- " " +
332
- (color ? style.gray(kBoxV) : kBoxV),
333
- );
313
+ out.push(`${color ? style.gray(kBoxV) : kBoxV} ${padded} ${color ? style.gray(kBoxV) : kBoxV}`);
334
314
  }
335
315
 
336
- out.push(
337
- color
338
- ? style.gray(kBoxBL + repeatBoxH(borderWidth) + kBoxBR)
339
- : kBoxBL + repeatBoxH(borderWidth) + kBoxBR,
340
- );
316
+ out.push(color ? style.gray(kBoxBL + repeatBoxH(borderWidth) + kBoxBR) : kBoxBL + repeatBoxH(borderWidth) + kBoxBR);
341
317
 
342
318
  return out;
343
319
  }
@@ -402,13 +378,7 @@ function rowsForSubcommands(cmds: CliNode[]): HelpRow[] {
402
378
 
403
379
  // ── Main Help Render ──────────────────────────────────────────────────────────
404
380
 
405
- function appendNotesBox(
406
- lines: string[],
407
- notes: string | undefined,
408
- appKey: string,
409
- hw: number,
410
- color: boolean,
411
- ): void {
381
+ function appendNotesBox(lines: string[], notes: string | undefined, appKey: string, hw: number, color: boolean): void {
412
382
  if ((notes ?? "").length === 0) {
413
383
  return;
414
384
  }
@@ -441,21 +411,14 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
441
411
  ).join("\n"),
442
412
  );
443
413
 
444
- const optBox = renderTableBox(
445
- "Options",
446
- rowsForOptions(visibleOptions(schema.options), color),
447
- hw,
448
- color,
449
- );
414
+ const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(schema.options), color), hw, color);
450
415
  if (optBox.length > 0) {
451
416
  lines.push("");
452
417
  lines.push(optBox.join("\n"));
453
418
  }
454
419
  if ((schema.commands ?? []).length > 0) {
455
420
  lines.push("");
456
- lines.push(
457
- renderTableBox("Commands", rowsForSubcommands(schema.commands ?? []), hw, color).join("\n"),
458
- );
421
+ lines.push(renderTableBox("Commands", rowsForSubcommands(schema.commands ?? []), hw, color).join("\n"));
459
422
  }
460
423
  appendNotesBox(lines, schema.notes, schema.key, hw, color);
461
424
  return `${lines.join("\n")}\n\n`;
@@ -496,12 +459,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
496
459
  ).join("\n"),
497
460
  );
498
461
 
499
- const optBox = renderTableBox(
500
- "Options",
501
- rowsForOptions(visibleOptions(node.options), color),
502
- hw,
503
- color,
504
- );
462
+ const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(node.options), color), hw, color);
505
463
  if (optBox.length > 0) {
506
464
  lines.push("");
507
465
  lines.push(optBox.join("\n"));
@@ -9,12 +9,7 @@ import { join } from "node:path";
9
9
  import { exportPresentationBuiltins } from "./builtins/export.ts";
10
10
  import { cliParseRoot, cliPresentationRoot } from "./builtins/presentation.ts";
11
11
  import { cliHelpRender } from "./help.ts";
12
- import {
13
- defaultMcpBundlePaths,
14
- generateMcpManifest,
15
- packMcpBundle,
16
- runMcpBundle,
17
- } from "./mcp/bundle.ts";
12
+ import { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle, runMcpBundle } from "./mcp/bundle.ts";
18
13
  import { collectMcpTools } from "./mcp/tools.ts";
19
14
  import { cliSchemaExport } from "./schema.ts";
20
15
  import { CliOptionKind, type CliProgram } from "./types.ts";
@@ -128,8 +123,7 @@ describe("mcp bundle", () => {
128
123
  expect(manifest.manifest_version).toBe("0.3");
129
124
  expect((manifest.server as { type: string }).type).toBe("binary");
130
125
  expect((manifest.server as { entry_point: string }).entry_point).toBe("myapp");
131
- const mcpConfig = (manifest.server as { mcp_config: { command: string; args: string[] } })
132
- .mcp_config;
126
+ const mcpConfig = (manifest.server as { mcp_config: { command: string; args: string[] } }).mcp_config;
133
127
  expect(mcpConfig.command).toBe("${__dirname}/myapp");
134
128
  expect(mcpConfig.args).toEqual(["mcp"]);
135
129
  expect((manifest.compatibility as { platforms: string[] }).platforms).toEqual(["darwin"]);
@@ -185,10 +179,7 @@ describe("mcp bundle", () => {
185
179
  }
186
180
  const lines = stdout.join("").trim().split("\n");
187
181
  const norm = (p: string) => realpathSync.native(p);
188
- expect(lines.map(norm)).toEqual([
189
- norm(join(dist, "myapp.mcpb")),
190
- norm(join(dist, "claude-plugin", "myapp.zip")),
191
- ]);
182
+ expect(lines.map(norm)).toEqual([norm(join(dist, "myapp.mcpb")), norm(join(dist, "claude-plugin", "myapp.zip"))]);
192
183
  const zip = readFileSync(join(dist, "claude-plugin", "myapp.zip"));
193
184
  expect(zip.indexOf(Buffer.from(".mcp.json"))).toBeGreaterThanOrEqual(0);
194
185
  } finally {
package/src/hidden.ts CHANGED
@@ -18,9 +18,7 @@ export function presentationNode(node: CliNode): CliNode | null {
18
18
  }
19
19
  const options = visibleOptions(node.options);
20
20
  if (isCliRouter(node)) {
21
- const commands = node.commands
22
- .map((ch) => presentationNode(ch))
23
- .filter((ch): ch is CliNode => ch !== null);
21
+ const commands = node.commands.map((ch) => presentationNode(ch)).filter((ch): ch is CliNode => ch !== null);
24
22
  return { ...node, options, commands };
25
23
  }
26
24
  return { ...node, options };
@@ -3,15 +3,11 @@ Tests for install/binary-placement module behavior.
3
3
  */
4
4
 
5
5
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6
- import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
6
+ import { chmodSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import type { CliProgram } from "../types.ts";
10
- import {
11
- isAppInstalled,
12
- isExternallyManagedBinary,
13
- resolvePathCommand,
14
- } from "./binary-placement.ts";
10
+ import { isAppInstalled, isExternallyManagedBinary, resolvePathCommand } from "./binary-placement.ts";
15
11
 
16
12
  const program: CliProgram = {
17
13
  key: "placementapp",
@@ -32,10 +32,7 @@ function realpathOrSelf(path: string): string {
32
32
  }
33
33
 
34
34
  /** True when PATH resolves to the running executable (e.g. Homebrew Cellar). */
35
- export function isExternallyManagedBinary(
36
- key: string,
37
- execPath: string = process.execPath,
38
- ): boolean {
35
+ export function isExternallyManagedBinary(key: string, execPath: string = process.execPath): boolean {
39
36
  const resolved = resolvePathCommand(key);
40
37
  if (!resolved) return false;
41
38
  return resolved === realpathOrSelf(execPath);
@@ -1,12 +1,4 @@
1
- import {
2
- chmodSync,
3
- existsSync,
4
- mkdirSync,
5
- mkdtempSync,
6
- readFileSync,
7
- rmSync,
8
- writeFileSync,
9
- } from "node:fs";
1
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
2
  import { tmpdir } from "node:os";
11
3
  import { dirname, join } from "node:path";
12
4
  import type { CliUpdateGetLatest } from "../types.ts";
@@ -150,9 +142,7 @@ export function createGhFetchLatest(
150
142
  ? ` ${config.repoEnvHint}`
151
143
  : ` No releases found for ${config.repo}.`
152
144
  : "";
153
- throw new Error(
154
- `Failed to fetch latest release from ${config.repo}: ${detail || "unknown error"}.${hint}`,
155
- );
145
+ throw new Error(`Failed to fetch latest release from ${config.repo}: ${detail || "unknown error"}.${hint}`);
156
146
  }
157
147
 
158
148
  let parsed: { tagName?: string };
@@ -172,16 +162,11 @@ export function createGhFetchLatest(
172
162
 
173
163
  function ensureGhAvailable(): void {
174
164
  if (Bun.which("gh") === null) {
175
- throw new Error(
176
- "GitHub CLI (gh) is required. Install from https://cli.github.com/ and run `gh auth login`.",
177
- );
165
+ throw new Error("GitHub CLI (gh) is required. Install from https://cli.github.com/ and run `gh auth login`.");
178
166
  }
179
167
  }
180
168
 
181
- async function downloadReleaseAsset(
182
- config: GhReleaseUpdateConfig,
183
- tempDir: string,
184
- ): Promise<string> {
169
+ async function downloadReleaseAsset(config: GhReleaseUpdateConfig, tempDir: string): Promise<string> {
185
170
  const result = await runGh([
186
171
  "release",
187
172
  "download",
@@ -206,9 +191,7 @@ async function downloadReleaseAsset(
206
191
  return downloadedPath;
207
192
  }
208
193
 
209
- async function runGh(
210
- args: string[],
211
- ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
194
+ async function runGh(args: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
212
195
  const proc = Bun.spawn(["gh", ...args], { stdout: "pipe", stderr: "pipe" });
213
196
  const [exitCode, stdout, stderr] = await Promise.all([
214
197
  proc.exited,
@@ -45,8 +45,7 @@ export function readCodexMcpEntry(path: string, name: string): McpServerEntry |
45
45
  const transport = block.match(/transport\s*=\s*\{[^}]*type\s*=\s*"stdio"[^}]*\}/s)?.[0];
46
46
  const search = transport ?? block;
47
47
 
48
- const cmd =
49
- search.match(/command\s*=\s*"([^"]*)"/)?.[1] ?? search.match(/command\s*=\s*'([^']*)'/)?.[1];
48
+ const cmd = search.match(/command\s*=\s*"([^"]*)"/)?.[1] ?? search.match(/command\s*=\s*'([^']*)'/)?.[1];
50
49
  if (!cmd) return undefined;
51
50
 
52
51
  const argsMatch = search.match(/args\s*=\s*\[([^\]]*)\]/s);
@@ -29,12 +29,7 @@ export function readMcpServerEntry(path: string, name: string): McpServerEntry |
29
29
  }
30
30
 
31
31
  /** Returns an error message when existing entry conflicts, or null if safe to merge. */
32
- export function checkMcpConflict(
33
- path: string,
34
- name: string,
35
- expected: McpServerEntry,
36
- yes: boolean,
37
- ): string | null {
32
+ export function checkMcpConflict(path: string, name: string, expected: McpServerEntry, yes: boolean): string | null {
38
33
  const existing = readMcpServerEntry(path, name);
39
34
  if (existing && !entriesEqual(existing, expected) && !yes) {
40
35
  return (
@@ -48,12 +43,7 @@ export function checkMcpConflict(
48
43
  }
49
44
 
50
45
  /** Merges MCP server entry into config file. */
51
- export function mergeMcpConfig(
52
- path: string,
53
- name: string,
54
- entry: McpServerEntry,
55
- dry: boolean,
56
- ): void {
46
+ export function mergeMcpConfig(path: string, name: string, entry: McpServerEntry, dry: boolean): void {
57
47
  if (dry) return;
58
48
  let data: Record<string, unknown> = {};
59
49
  if (existsSync(path)) {
@@ -57,9 +57,7 @@ describe("opencode mcp config", () => {
57
57
 
58
58
  test("resolveOpenCodeConfigPathForInstall defaults to config.json", () => {
59
59
  mkdirSync(opencodeConfigDir(home), { recursive: true });
60
- expect(resolveOpenCodeConfigPathForInstall(home)).toBe(
61
- join(opencodeConfigDir(home), "config.json"),
62
- );
60
+ expect(resolveOpenCodeConfigPathForInstall(home)).toBe(join(opencodeConfigDir(home), "config.json"));
63
61
  });
64
62
 
65
63
  test("mergeOpenCodeMcpConfig writes mcp block", () => {
@@ -6,11 +6,7 @@ import { expectedMcpEntry } from "./mcp-config.ts";
6
6
  export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json";
7
7
 
8
8
  /** Global OpenCode config filenames, in precedence order (matches OpenCode). */
9
- export const OPENCODE_CONFIG_FILENAMES = [
10
- "opencode.jsonc",
11
- "opencode.json",
12
- "config.json",
13
- ] as const;
9
+ export const OPENCODE_CONFIG_FILENAMES = ["opencode.jsonc", "opencode.json", "config.json"] as const;
14
10
 
15
11
  export interface OpenCodeLocalMcpEntry {
16
12
  type: "local";
@@ -71,10 +67,7 @@ function readConfig(path: string): Record<string, unknown> {
71
67
  }
72
68
 
73
69
  /** Reads `mcp[name]` when it is a local stdio server. */
74
- export function readOpenCodeMcpEntry(
75
- path: string,
76
- name: string,
77
- ): OpenCodeLocalMcpEntry | undefined {
70
+ export function readOpenCodeMcpEntry(path: string, name: string): OpenCodeLocalMcpEntry | undefined {
78
71
  if (!existsSync(path)) return undefined;
79
72
  try {
80
73
  const data = readConfig(path);
@@ -122,12 +115,7 @@ export function checkOpenCodeMcpConflict(
122
115
  }
123
116
 
124
117
  /** Merges a local MCP entry into OpenCode `mcp` config. */
125
- export function mergeOpenCodeMcpConfig(
126
- path: string,
127
- name: string,
128
- entry: OpenCodeLocalMcpEntry,
129
- dry: boolean,
130
- ): void {
118
+ export function mergeOpenCodeMcpConfig(path: string, name: string, entry: OpenCodeLocalMcpEntry, dry: boolean): void {
131
119
  if (dry) return;
132
120
  const data = readConfig(path);
133
121
  if (!data.$schema) data.$schema = OPENCODE_CONFIG_SCHEMA;
@@ -7,20 +7,12 @@ import type { InstallAction, InstallOpts } from "./target-types.ts";
7
7
  export type { InstallAction, InstallActionKind, InstallOpts } from "./target-types.ts";
8
8
 
9
9
  /** Builds install actions for normal mode (--all / scoped targets). */
10
- export function buildInstallPlan(
11
- root: CliProgram,
12
- paths: InstallPaths,
13
- opts: InstallOpts,
14
- ): InstallAction[] {
10
+ export function buildInstallPlan(root: CliProgram, paths: InstallPaths, opts: InstallOpts): InstallAction[] {
15
11
  return buildInstallPlanFromTargets(root, paths, opts);
16
12
  }
17
13
 
18
14
  /** Builds update/reinstall actions; greenfield fallback when nothing detected. */
19
- export function buildUpdatePlan(
20
- root: CliProgram,
21
- paths: InstallPaths,
22
- opts: InstallOpts,
23
- ): InstallAction[] {
15
+ export function buildUpdatePlan(root: CliProgram, paths: InstallPaths, opts: InstallOpts): InstallAction[] {
24
16
  const refresh = buildInstallPlanFromTargets(root, paths, {
25
17
  ...opts,
26
18
  reinstall: true,
@@ -79,12 +79,10 @@ export function printInstallStatus(root: CliProgram, opts: InstallOpts): void {
79
79
 
80
80
  const configStatus = appConfigStatus(root);
81
81
  if (configStatus) {
82
- installOut(
83
- ` app config: ${configStatus.path}${configStatus.exists ? "" : " (missing)"}`,
84
- opts,
85
- );
82
+ installOut(` app config: ${configStatus.path}${configStatus.exists ? "" : " (missing)"}`, opts);
86
83
  for (const req of configStatus.required) {
87
- installOut(` ${req.key}: ${req.set ? "set" : "missing"}`, opts);
84
+ const bindingHint = req.binding && req.binding !== "missing" && req.set ? ` (${req.binding})` : "";
85
+ installOut(` ${req.key}: ${req.set ? "set" : "missing"}${bindingHint}`, opts);
88
86
  }
89
87
  }
90
88
  }
@@ -46,11 +46,7 @@ export abstract class InstallTarget {
46
46
 
47
47
  protected abstract isDetectedFromSnapshot(detected: DetectedSnapshot): boolean;
48
48
 
49
- statusLine(
50
- paths: InstallPaths,
51
- root: CliProgram,
52
- detected: InstalledArtifacts,
53
- ): string | undefined {
49
+ statusLine(paths: InstallPaths, root: CliProgram, detected: InstalledArtifacts): string | undefined {
54
50
  if (!this.isDetectedFromInstalled(detected)) return undefined;
55
51
  return this.formatStatusLine(paths, root);
56
52
  }
@@ -78,12 +74,7 @@ export abstract class InstallTarget {
78
74
 
79
75
  protected abstract buildUninstallActions(ctx: TargetPlanContext): UninstallAction[];
80
76
 
81
- contributeStatus(
82
- paths: InstallPaths,
83
- root: CliProgram,
84
- detected: InstalledArtifacts,
85
- status: InstallStatus,
86
- ): void {
77
+ contributeStatus(paths: InstallPaths, root: CliProgram, detected: InstalledArtifacts, status: InstallStatus): void {
87
78
  const line = this.statusLine(paths, root, detected);
88
79
  if (!line) return;
89
80
  this.assignStatusLine(status, line);
@@ -7,11 +7,7 @@ export { buildDetectedSnapshot, detectInstalledArtifacts } from "./target-scope.
7
7
  export type { InstalledArtifacts, InstallStatus } from "./target-types.ts";
8
8
 
9
9
  /** Builds a status inventory from detected artifacts. */
10
- export function buildInstallStatus(
11
- paths: InstallPaths,
12
- detected: InstalledArtifacts,
13
- root: CliProgram,
14
- ): InstallStatus {
10
+ export function buildInstallStatus(paths: InstallPaths, detected: InstalledArtifacts, root: CliProgram): InstallStatus {
15
11
  const status: InstallStatus = {};
16
12
  for (const target of INSTALL_TARGETS) {
17
13
  target.contributeStatus(paths, root, detected, status);
@@ -19,10 +19,7 @@ export type { InstallTargetSpec, ResolvedInstallTarget } from "../types.ts";
19
19
 
20
20
  export type { InstallPlanMode, InstallScope } from "./target-types.ts";
21
21
 
22
- export function resolveAgentIntegration(
23
- configure?: CliConfigureConfig,
24
- mcpEnabled = false,
25
- ): InstallAgentIntegration {
22
+ export function resolveAgentIntegration(configure?: CliConfigureConfig, mcpEnabled = false): InstallAgentIntegration {
26
23
  return configure?.agentIntegration ?? (mcpEnabled ? "mcp" : "skill");
27
24
  }
28
25
 
@@ -95,10 +92,7 @@ export function resolveEffectiveInstallTargets(
95
92
  const user = configure?.targets;
96
93
  const out = {} as Record<CliInstallArtifactKey, { enabled: boolean; includedInAll: boolean }>;
97
94
  for (const key of INSTALL_ARTIFACT_KEYS) {
98
- out[key] = resolveInstallTargetSpec(
99
- user?.[key],
100
- artifactDefaults(key, integration, mcpEnabled),
101
- );
95
+ out[key] = resolveInstallTargetSpec(user?.[key], artifactDefaults(key, integration, mcpEnabled));
102
96
  }
103
97
  applyAgentPairDedupe(out, user, integration);
104
98
  return out;
@@ -132,10 +126,6 @@ export function resolveInstallPlanMode(opts: {
132
126
  }
133
127
 
134
128
  /** @deprecated Use target registry. */
135
- export function isInstallTargetAvailable(
136
- key: CliInstallArtifactKey,
137
- root: CliProgram,
138
- paths: InstallPaths,
139
- ): boolean {
129
+ export function isInstallTargetAvailable(key: CliInstallArtifactKey, root: CliProgram, paths: InstallPaths): boolean {
140
130
  return installTargetForKey(key)?.isAvailable(root, paths) ?? false;
141
131
  }
@@ -1,10 +1,5 @@
1
1
  import type { CliProgram } from "../types.ts";
2
- import {
3
- checkCodexMcpConflict,
4
- codexMcpHasServer,
5
- mergeCodexMcpConfig,
6
- removeCodexMcpConfig,
7
- } from "./mcp-codex.ts";
2
+ import { checkCodexMcpConflict, codexMcpHasServer, mergeCodexMcpConfig, removeCodexMcpConfig } from "./mcp-codex.ts";
8
3
  import { expectedMcpEntry } from "./mcp-config.ts";
9
4
  import {
10
5
  checkOpenclawMcpConflict,
@@ -37,20 +32,9 @@ export interface McpCliHostSpec {
37
32
  hasServer: (home: string, name: string) => boolean;
38
33
  installMessage: string;
39
34
  uninstallMessage: string;
40
- merge: (
41
- home: string,
42
- name: string,
43
- entry: ReturnType<typeof expectedMcpEntry>,
44
- dry: boolean,
45
- yes: boolean,
46
- ) => string;
35
+ merge: (home: string, name: string, entry: ReturnType<typeof expectedMcpEntry>, dry: boolean, yes: boolean) => string;
47
36
  remove: (home: string, name: string, dry: boolean) => void;
48
- preflight: (
49
- home: string,
50
- name: string,
51
- entry: ReturnType<typeof expectedMcpEntry>,
52
- yes: boolean,
53
- ) => string | null;
37
+ preflight: (home: string, name: string, entry: ReturnType<typeof expectedMcpEntry>, yes: boolean) => string | null;
54
38
  }
55
39
 
56
40
  /** MCP host registered via an external CLI (codex, openclaw). */
@@ -108,13 +92,7 @@ export class McpCliInstallTarget extends InstallTarget {
108
92
  summary: `${this.spec.label}: ${displayInstallPath(configPath)}`,
109
93
  message: this.spec.installMessage.replace("{name}", ctx.paths.mcpName),
110
94
  run: () => {
111
- const written = this.spec.merge(
112
- userHome(),
113
- ctx.paths.mcpName,
114
- entry,
115
- ctx.dry,
116
- !!ctx.opts.yes,
117
- );
95
+ const written = this.spec.merge(userHome(), ctx.paths.mcpName, entry, ctx.dry, !!ctx.opts.yes);
118
96
  return [written];
119
97
  },
120
98
  },
@@ -25,14 +25,8 @@ export interface McpJsonHostSpec {
25
25
  label: string;
26
26
  pairedSkillKey?: CliInstallArtifactKey;
27
27
  configPath: (paths: InstallPaths) => string;
28
- detectedKey: keyof Pick<
29
- InstalledArtifacts,
30
- "cursorMcp" | "claudeMcp" | "claudeDesktopMcp" | "chatGptMcp"
31
- >;
32
- statusField: keyof Pick<
33
- InstallStatus,
34
- "cursorMcp" | "claudeMcp" | "claudeDesktopMcp" | "chatGptMcp"
35
- >;
28
+ detectedKey: keyof Pick<InstalledArtifacts, "cursorMcp" | "claudeMcp" | "claudeDesktopMcp" | "chatGptMcp">;
29
+ statusField: keyof Pick<InstallStatus, "cursorMcp" | "claudeMcp" | "claudeDesktopMcp" | "chatGptMcp">;
36
30
  isAvailable: (root: CliProgram, paths: InstallPaths) => boolean;
37
31
  /** Append server name to status line (default false). */
38
32
  statusIncludesServer?: boolean;
@@ -89,12 +83,7 @@ export class McpJsonInstallTarget extends InstallTarget {
89
83
 
90
84
  preflight(ctx: TargetPlanContext): string | null {
91
85
  const entry = expectedMcpEntry(ctx.root);
92
- return checkMcpConflict(
93
- this.spec.configPath(ctx.paths),
94
- ctx.paths.mcpName,
95
- entry,
96
- !!ctx.opts.yes,
97
- );
86
+ return checkMcpConflict(this.spec.configPath(ctx.paths), ctx.paths.mcpName, entry, !!ctx.opts.yes);
98
87
  }
99
88
 
100
89
  protected buildInstallActions(ctx: TargetPlanContext): InstallAction[] {
@@ -7,11 +7,7 @@ import { buildDetectedSnapshot, buildTargetPlanContext } from "./target-scope.ts
7
7
  import type { InstallAction, InstallOpts, UninstallAction } from "./target-types.ts";
8
8
 
9
9
  /** Builds install actions for normal mode (--all / scoped targets). */
10
- export function buildInstallPlanFromTargets(
11
- root: CliProgram,
12
- paths: InstallPaths,
13
- opts: InstallOpts,
14
- ): InstallAction[] {
10
+ export function buildInstallPlanFromTargets(root: CliProgram, paths: InstallPaths, opts: InstallOpts): InstallAction[] {
15
11
  const detected = buildDetectedSnapshot(root, paths);
16
12
  const ctx = buildTargetPlanContext(root, paths, opts, detected);
17
13