argsbarg 4.0.4 → 4.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.
Files changed (147) hide show
  1. package/CHANGELOG.md +77 -1
  2. package/README.md +91 -85
  3. package/docs/README.md +6 -6
  4. package/docs/ai-skills.md +8 -5
  5. package/docs/bundled-docs.md +1 -1
  6. package/docs/cli-program.md +9 -7
  7. package/docs/config-schema.md +37 -13
  8. package/docs/developing.md +8 -8
  9. package/docs/distribution-homebrew.md +103 -0
  10. package/docs/install.md +143 -106
  11. package/docs/mcp.md +23 -12
  12. package/docs/output-schema.md +1 -1
  13. package/examples/full-example/Formula/.gitkeep +0 -0
  14. package/examples/full-example/README.md +98 -0
  15. package/examples/full-example/biome.json +22 -0
  16. package/examples/{consumer-app → full-example}/bun.lock +2 -0
  17. package/examples/full-example/justfile +134 -0
  18. package/examples/{consumer-app → full-example}/package.json +10 -3
  19. package/examples/{consumer-app → full-example}/schemas/generated/app-config.json +1 -1
  20. package/examples/{consumer-app → full-example}/schemas/generated/status.json +1 -1
  21. package/examples/full-example/scripts/create-identity.ts +11 -0
  22. package/examples/full-example/scripts/formula-shared.ts +73 -0
  23. package/examples/full-example/scripts/gen-dev-formula.ts +26 -0
  24. package/examples/full-example/scripts/print-identity.ts +27 -0
  25. package/examples/full-example/src/commands/echo/command.ts +21 -0
  26. package/examples/full-example/src/commands/status/command.test.ts +10 -0
  27. package/examples/full-example/src/commands/status/command.ts +36 -0
  28. package/examples/{consumer-app → full-example}/src/commands/status/types.ts +1 -1
  29. package/examples/full-example/src/index.ts +10 -0
  30. package/examples/full-example/src/program.ts +57 -0
  31. package/examples/{consumer-app → full-example}/src/types.ts +1 -1
  32. package/examples/mcp-test.ts +8 -24
  33. package/examples/nested.ts +1 -3
  34. package/index.d.ts +81 -65
  35. package/package.json +2 -2
  36. package/src/builtins/builtins.test.ts +37 -22
  37. package/src/builtins/completion-group.ts +17 -15
  38. package/src/builtins/config.test.ts +31 -25
  39. package/src/builtins/config.ts +4 -3
  40. package/src/builtins/dispatch.ts +25 -1
  41. package/src/builtins/install.ts +45 -82
  42. package/src/builtins/mcp.ts +1 -1
  43. package/src/builtins/registry.ts +2 -0
  44. package/src/builtins/uninstall.ts +80 -0
  45. package/src/capabilities.ts +5 -7
  46. package/src/cli-tool/cli-smoke.test.ts +19 -0
  47. package/src/cli-tool/create.test.ts +119 -0
  48. package/src/cli-tool/create.ts +380 -0
  49. package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +15 -14
  50. package/src/cli-tool/main.ts +8 -0
  51. package/src/cli-tool/post-create.ts +111 -0
  52. package/src/cli-tool/program.ts +82 -0
  53. package/src/cli-tool/prompt.ts +28 -0
  54. package/src/cli-tool/run-create.ts +149 -0
  55. package/src/config/bootstrap.ts +174 -66
  56. package/src/config/context.test.ts +22 -36
  57. package/src/config/context.ts +5 -4
  58. package/src/config/file.test.ts +66 -56
  59. package/src/config/file.ts +33 -25
  60. package/src/config/resolve.test.ts +192 -1
  61. package/src/config/resolve.ts +92 -13
  62. package/src/config.integration.test.ts +17 -10
  63. package/src/docs/api-guide.test.ts +4 -5
  64. package/src/docs/docs.test.ts +2 -1
  65. package/src/docs/mcp-guide.ts +7 -8
  66. package/src/hidden-mcpb.test.ts +41 -1
  67. package/src/index.ts +7 -10
  68. package/src/install/binary-placement.test.ts +101 -0
  69. package/src/install/binary-placement.ts +47 -0
  70. package/src/install/detect-installed.ts +2 -97
  71. package/src/install/index.ts +239 -168
  72. package/src/install/install-validate.test.ts +61 -0
  73. package/src/install/install.test.ts +170 -90
  74. package/src/install/mcp-openclaw.test.ts +40 -0
  75. package/src/install/mcp-openclaw.ts +106 -0
  76. package/src/install/normalize-uninstall.ts +11 -0
  77. package/src/install/normalize.ts +20 -0
  78. package/src/install/paths.ts +18 -26
  79. package/src/install/plan.ts +40 -261
  80. package/src/install/shell.ts +0 -14
  81. package/src/install/status.test.ts +85 -0
  82. package/src/install/status.ts +22 -15
  83. package/src/install/target-base.ts +93 -0
  84. package/src/install/target-detect.ts +20 -0
  85. package/src/install/target-effective.ts +129 -0
  86. package/src/install/target-mcp-cli.ts +149 -0
  87. package/src/install/target-mcp-json.ts +130 -0
  88. package/src/install/target-plan-build.ts +67 -0
  89. package/src/install/target-registry.ts +57 -0
  90. package/src/install/target-scope.ts +253 -0
  91. package/src/install/target-skill.ts +104 -0
  92. package/src/install/target-types.ts +129 -0
  93. package/src/install/targets/app.ts +60 -0
  94. package/src/install/targets/chatgpt-mcp.ts +12 -0
  95. package/src/install/targets/claude-code-mcp.ts +15 -0
  96. package/src/install/targets/claude-desktop-mcp.ts +12 -0
  97. package/src/install/targets/claude-skill.ts +16 -0
  98. package/src/install/targets/codex-mcp.ts +25 -0
  99. package/src/install/targets/codex-skill.ts +14 -0
  100. package/src/install/targets/configure.ts +63 -0
  101. package/src/install/targets/cursor-mcp.ts +15 -0
  102. package/src/install/targets/cursor-skill.ts +16 -0
  103. package/src/install/targets/index.ts +50 -0
  104. package/src/install/targets/openclaw-mcp.ts +25 -0
  105. package/src/install/targets/openclaw-skill.ts +17 -0
  106. package/src/install/targets/opencode-mcp.ts +101 -0
  107. package/src/install/targets/opencode-skill.ts +15 -0
  108. package/src/install/targets.test.ts +118 -0
  109. package/src/install/uninstall.ts +16 -152
  110. package/src/invoke.test.ts +7 -1
  111. package/src/mcp/bundle.ts +16 -4
  112. package/src/mcp/claude.test.ts +14 -1
  113. package/src/mcp/claude.ts +11 -4
  114. package/src/mcp/env.test.ts +92 -0
  115. package/src/mcp/env.ts +15 -14
  116. package/src/mcp/zip.test.ts +17 -0
  117. package/src/mcp/zip.ts +62 -9
  118. package/src/mcp.integration.test.ts +1 -1
  119. package/src/parse.test.ts +14 -2
  120. package/src/paths/host.ts +11 -11
  121. package/src/paths/remove-empty-dir.ts +13 -0
  122. package/src/prompt.ts +10 -0
  123. package/src/schema.ts +9 -1
  124. package/src/skill/generate.ts +18 -4
  125. package/src/skill/install.ts +33 -6
  126. package/src/skill/naming.ts +28 -0
  127. package/src/types.ts +86 -7
  128. package/src/validate.ts +73 -9
  129. package/docs/templates/cursor/rules/cli-program.mdc +0 -31
  130. package/examples/config-app/main.ts +0 -20
  131. package/examples/config-app/program.ts +0 -81
  132. package/examples/config-app/schema.ts +0 -37
  133. package/examples/config-app/types.ts +0 -19
  134. package/examples/consumer-app/README.md +0 -57
  135. package/examples/consumer-app/src/main.ts +0 -15
  136. package/examples/consumer-app/src/program.ts +0 -108
  137. package/src/install/binary.ts +0 -94
  138. package/src/install/completions.ts +0 -56
  139. package/src/install/update.test.ts +0 -108
  140. package/src/install/update.ts +0 -57
  141. /package/examples/{consumer-app → full-example}/schemas/configSchemas.ts +0 -0
  142. /package/examples/{consumer-app → full-example}/schemas/outputSchemas.ts +0 -0
  143. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts +0 -0
  144. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.ts +0 -0
  145. /package/examples/{consumer-app → full-example}/scripts/schemagen/naming.ts +0 -0
  146. /package/examples/{consumer-app → full-example}/scripts/schemagen.ts +0 -0
  147. /package/examples/{consumer-app → full-example}/tsconfig.json +0 -0
@@ -0,0 +1,63 @@
1
+ import { appConfigInstalled, displayAppConfigPath, uninstallAppConfig } from "../../config/file.ts";
2
+ import type { CliProgram, InstallAgentIntegration } from "../../types.ts";
3
+ import type { InstallPaths } from "../paths.ts";
4
+ import { InstallTarget } from "../target-base.ts";
5
+ import type {
6
+ DetectedSnapshot,
7
+ InstallAction,
8
+ InstalledArtifacts,
9
+ InstallStatus,
10
+ TargetPlanContext,
11
+ UninstallAction,
12
+ } from "../target-types.ts";
13
+
14
+ /** App config file (install --configure / wizard). */
15
+ class ConfigureInstallTarget extends InstallTarget {
16
+ readonly key = "configure" as const;
17
+ readonly actionKind = "configure" as const;
18
+ readonly category = "core" as const;
19
+
20
+ defaultIncludedInAll(_integration: InstallAgentIntegration): boolean {
21
+ return false;
22
+ }
23
+
24
+ isAvailable(root: CliProgram, _paths: InstallPaths): boolean {
25
+ return root.appConfig !== undefined;
26
+ }
27
+
28
+ isDetected(_paths: InstallPaths, root: CliProgram): boolean {
29
+ if (!root.appConfig) return false;
30
+ return appConfigInstalled(root);
31
+ }
32
+
33
+ applyDetected(_paths: InstallPaths, _root: CliProgram, _out: InstalledArtifacts): void {}
34
+
35
+ protected isDetectedFromSnapshot(detected: DetectedSnapshot): boolean {
36
+ return detected.appConfig ?? false;
37
+ }
38
+
39
+ protected formatStatusLine(_paths: InstallPaths, root: CliProgram): string {
40
+ return displayAppConfigPath(root);
41
+ }
42
+
43
+ protected assignStatusLine(_status: InstallStatus, _line: string): void {
44
+ // configure status shown via appConfigStatus in status.ts
45
+ }
46
+
47
+ protected buildInstallActions(_ctx: TargetPlanContext): InstallAction[] {
48
+ return [];
49
+ }
50
+
51
+ protected buildUninstallActions(ctx: TargetPlanContext): UninstallAction[] {
52
+ return [
53
+ {
54
+ kind: "configure",
55
+ summary: `app config: ${displayAppConfigPath(ctx.root)}`,
56
+ message: `Removing app config ${displayAppConfigPath(ctx.root)}`,
57
+ run: () => uninstallAppConfig(ctx.root, ctx.dry),
58
+ },
59
+ ];
60
+ }
61
+ }
62
+
63
+ export const configureTarget = new ConfigureInstallTarget();
@@ -0,0 +1,15 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { userHome } from "../paths.ts";
4
+ import { McpJsonInstallTarget } from "../target-mcp-json.ts";
5
+
6
+ export const cursorMcpTarget = new McpJsonInstallTarget({
7
+ key: "cursorMcp",
8
+ actionKind: "cursor-mcp",
9
+ label: "cursor mcp",
10
+ pairedSkillKey: "cursorSkill",
11
+ configPath: (p) => p.cursorMcpPath,
12
+ detectedKey: "cursorMcp",
13
+ statusField: "cursorMcp",
14
+ isAvailable: () => existsSync(join(userHome(), ".cursor")),
15
+ });
@@ -0,0 +1,16 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { userHome } from "../paths.ts";
4
+ import { SkillInstallTarget } from "../target-skill.ts";
5
+
6
+ export const cursorSkillTarget = new SkillInstallTarget({
7
+ key: "cursorSkill",
8
+ actionKind: "cursor-skill",
9
+ label: "Cursor",
10
+ uninstallPrefix: "cursor skill",
11
+ pairedMcpKey: "cursorMcp",
12
+ skillDir: (p) => p.cursorSkillDir,
13
+ detectedKey: "cursorSkill",
14
+ statusField: "cursorSkill",
15
+ isAvailable: () => existsSync(join(userHome(), ".cursor")),
16
+ });
@@ -0,0 +1,50 @@
1
+ import type { InstallTarget } from "../target-base.ts";
2
+ import { appTarget } from "./app.ts";
3
+ import { chatgptMcpTarget } from "./chatgpt-mcp.ts";
4
+ import { claudeCodeMcpTarget } from "./claude-code-mcp.ts";
5
+ import { claudeDesktopMcpTarget } from "./claude-desktop-mcp.ts";
6
+ import { claudeSkillTarget } from "./claude-skill.ts";
7
+ import { codexMcpTarget } from "./codex-mcp.ts";
8
+ import { codexSkillTarget } from "./codex-skill.ts";
9
+ import { configureTarget } from "./configure.ts";
10
+ import { cursorMcpTarget } from "./cursor-mcp.ts";
11
+ import { cursorSkillTarget } from "./cursor-skill.ts";
12
+ import { openclawMcpTarget } from "./openclaw-mcp.ts";
13
+ import { openclawSkillTarget } from "./openclaw-skill.ts";
14
+ import { opencodeMcpTarget } from "./opencode-mcp.ts";
15
+ import { opencodeSkillTarget } from "./opencode-skill.ts";
16
+
17
+ /** Ordered install targets (plan iteration order). */
18
+ export const INSTALL_TARGETS: InstallTarget[] = [
19
+ appTarget,
20
+ cursorSkillTarget,
21
+ claudeSkillTarget,
22
+ codexSkillTarget,
23
+ opencodeSkillTarget,
24
+ openclawSkillTarget,
25
+ cursorMcpTarget,
26
+ claudeCodeMcpTarget,
27
+ claudeDesktopMcpTarget,
28
+ opencodeMcpTarget,
29
+ codexMcpTarget,
30
+ openclawMcpTarget,
31
+ chatgptMcpTarget,
32
+ configureTarget,
33
+ ];
34
+
35
+ export {
36
+ appTarget,
37
+ chatgptMcpTarget,
38
+ claudeCodeMcpTarget,
39
+ claudeDesktopMcpTarget,
40
+ claudeSkillTarget,
41
+ codexMcpTarget,
42
+ codexSkillTarget,
43
+ configureTarget,
44
+ cursorMcpTarget,
45
+ cursorSkillTarget,
46
+ openclawMcpTarget,
47
+ openclawSkillTarget,
48
+ opencodeMcpTarget,
49
+ opencodeSkillTarget,
50
+ };
@@ -0,0 +1,25 @@
1
+ import { openclawOnPath } from "../mcp-openclaw.ts";
2
+ import {
3
+ checkOpenclawMcpConflict,
4
+ McpCliInstallTarget,
5
+ mergeOpenclawMcpConfig,
6
+ openclawMcpHasServer,
7
+ removeOpenclawMcpConfig,
8
+ } from "../target-mcp-cli.ts";
9
+
10
+ export const openclawMcpTarget = new McpCliInstallTarget({
11
+ key: "openclawMcp",
12
+ actionKind: "openclaw-mcp",
13
+ label: "openclaw mcp",
14
+ pairedSkillKey: "openclawSkill",
15
+ configPath: (p) => p.openclawConfigPath,
16
+ detectedKey: "openclawMcp",
17
+ statusField: "openclawMcp",
18
+ isAvailable: () => openclawOnPath(),
19
+ hasServer: openclawMcpHasServer,
20
+ installMessage: 'Registering MCP server "{name}" via openclaw mcp add',
21
+ uninstallMessage: 'Removing MCP server "{name}" from OpenClaw via openclaw mcp unset',
22
+ merge: mergeOpenclawMcpConfig,
23
+ remove: removeOpenclawMcpConfig,
24
+ preflight: checkOpenclawMcpConflict,
25
+ });
@@ -0,0 +1,17 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { openclawOnPath } from "../mcp-openclaw.ts";
4
+ import { userHome } from "../paths.ts";
5
+ import { SkillInstallTarget } from "../target-skill.ts";
6
+
7
+ export const openclawSkillTarget = new SkillInstallTarget({
8
+ key: "openclawSkill",
9
+ actionKind: "openclaw-skill",
10
+ label: "OpenClaw",
11
+ uninstallPrefix: "openclaw skill",
12
+ pairedMcpKey: "openclawMcp",
13
+ skillDir: (p) => p.openclawSkillDir,
14
+ detectedKey: "openclawSkill",
15
+ statusField: "openclawSkill",
16
+ isAvailable: () => existsSync(join(userHome(), ".openclaw")) || openclawOnPath(),
17
+ });
@@ -0,0 +1,101 @@
1
+ import type { CliProgram } from "../../types.ts";
2
+ import {
3
+ checkOpenCodeMcpConflict,
4
+ detectOpenCodeMcpConfigPath,
5
+ expectedOpenCodeMcpEntry,
6
+ mergeOpenCodeMcpConfig,
7
+ opencodePresent,
8
+ removeOpenCodeMcpConfig,
9
+ } from "../mcp-opencode.ts";
10
+ import { displayInstallPath, type InstallPaths, userHome } from "../paths.ts";
11
+ import { InstallTarget } from "../target-base.ts";
12
+ import type {
13
+ DetectedSnapshot,
14
+ InstallAction,
15
+ InstalledArtifacts,
16
+ InstallStatus,
17
+ TargetPlanContext,
18
+ UninstallAction,
19
+ } from "../target-types.ts";
20
+
21
+ function resolveConfigPath(paths: InstallPaths, home: string): string {
22
+ return detectOpenCodeMcpConfigPath(home, paths.mcpName) ?? paths.opencodeMcpPath;
23
+ }
24
+
25
+ /** OpenCode MCP (top-level `mcp` key, not `mcpServers`). */
26
+ class OpenCodeMcpInstallTarget extends InstallTarget {
27
+ readonly key = "opencodeMcp" as const;
28
+ readonly actionKind = "opencode-mcp" as const;
29
+ readonly category = "mcp" as const;
30
+ readonly pairedKey = "opencodeSkill" as const;
31
+
32
+ isAvailable(_root: CliProgram, _paths: InstallPaths): boolean {
33
+ return opencodePresent(userHome());
34
+ }
35
+
36
+ isDetected(paths: InstallPaths, _root: CliProgram): boolean {
37
+ return detectOpenCodeMcpConfigPath(userHome(), paths.mcpName) !== undefined;
38
+ }
39
+
40
+ applyDetected(paths: InstallPaths, root: CliProgram, out: InstalledArtifacts): void {
41
+ out.opencodeMcp = this.isDetected(paths, root);
42
+ }
43
+
44
+ protected isDetectedFromSnapshot(detected: DetectedSnapshot): boolean {
45
+ return detected.opencodeMcp;
46
+ }
47
+
48
+ protected formatStatusLine(paths: InstallPaths, _root: CliProgram): string {
49
+ const home = userHome();
50
+ const path = detectOpenCodeMcpConfigPath(home, paths.mcpName) ?? paths.opencodeMcpPath;
51
+ return `${displayInstallPath(path)} (server "${paths.mcpName}")`;
52
+ }
53
+
54
+ protected assignStatusLine(status: InstallStatus, line: string): void {
55
+ status.opencodeMcp = line;
56
+ }
57
+
58
+ preflight(ctx: TargetPlanContext): string | null {
59
+ const entry = expectedOpenCodeMcpEntry(ctx.root);
60
+ return checkOpenCodeMcpConflict(
61
+ ctx.paths.opencodeMcpPath,
62
+ ctx.paths.mcpName,
63
+ entry,
64
+ !!ctx.opts.yes,
65
+ );
66
+ }
67
+
68
+ protected buildInstallActions(ctx: TargetPlanContext): InstallAction[] {
69
+ const entry = expectedOpenCodeMcpEntry(ctx.root);
70
+ const configPath = ctx.paths.opencodeMcpPath;
71
+ return [
72
+ {
73
+ kind: this.actionKind,
74
+ summary: `opencode mcp: ${displayInstallPath(configPath)}`,
75
+ message: `Merging MCP server "${ctx.paths.mcpName}" into ${displayInstallPath(configPath)}`,
76
+ run: () => {
77
+ mergeOpenCodeMcpConfig(configPath, ctx.paths.mcpName, entry, ctx.dry);
78
+ return [configPath];
79
+ },
80
+ },
81
+ ];
82
+ }
83
+
84
+ protected buildUninstallActions(ctx: TargetPlanContext): UninstallAction[] {
85
+ const home = userHome();
86
+ const configPath = resolveConfigPath(ctx.paths, home);
87
+ return [
88
+ {
89
+ kind: this.actionKind,
90
+ summary: `opencode mcp: ${displayInstallPath(configPath)}`,
91
+ message: `Removing MCP server "${ctx.paths.mcpName}" from ${displayInstallPath(configPath)}`,
92
+ run: () => {
93
+ removeOpenCodeMcpConfig(configPath, ctx.paths.mcpName, ctx.dry);
94
+ return [configPath];
95
+ },
96
+ },
97
+ ];
98
+ }
99
+ }
100
+
101
+ export const opencodeMcpTarget = new OpenCodeMcpInstallTarget();
@@ -0,0 +1,15 @@
1
+ import { opencodePresent } from "../mcp-opencode.ts";
2
+ import { userHome } from "../paths.ts";
3
+ import { SkillInstallTarget } from "../target-skill.ts";
4
+
5
+ export const opencodeSkillTarget = new SkillInstallTarget({
6
+ key: "opencodeSkill",
7
+ actionKind: "opencode-skill",
8
+ label: "OpenCode",
9
+ uninstallPrefix: "opencode skill",
10
+ pairedMcpKey: "opencodeMcp",
11
+ skillDir: (p) => p.opencodeSkillDir,
12
+ detectedKey: "opencodeSkill",
13
+ statusField: "opencodeSkill",
14
+ isAvailable: () => opencodePresent(userHome()),
15
+ });
@@ -0,0 +1,118 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { CliProgram } from "../types.ts";
3
+ import { normalizeInstallRawOpts } from "./normalize.ts";
4
+ import { normalizeUninstallRawOpts } from "./normalize-uninstall.ts";
5
+ import { resolveAgentIntegration, resolveEffectiveInstallTargets } from "./target-effective.ts";
6
+ import { isArtifactInScope } from "./target-scope.ts";
7
+
8
+ describe("normalizeInstallRawOpts", () => {
9
+ test("bare install sets all", () => {
10
+ expect(normalizeInstallRawOpts({})).toEqual({ all: "1" });
11
+ });
12
+
13
+ test("bare uninstall sets all", () => {
14
+ expect(normalizeUninstallRawOpts({})).toEqual({
15
+ uninstall: "1",
16
+ all: "1",
17
+ });
18
+ });
19
+
20
+ test("configure-only install unchanged", () => {
21
+ expect(normalizeInstallRawOpts({ configure: "1" })).toEqual({ configure: "1" });
22
+ });
23
+ });
24
+
25
+ describe("resolveAgentIntegration", () => {
26
+ test("defaults to skill without MCP", () => {
27
+ expect(resolveAgentIntegration(undefined, false)).toBe("skill");
28
+ });
29
+
30
+ test("defaults to mcp when MCP enabled", () => {
31
+ expect(resolveAgentIntegration(undefined, true)).toBe("mcp");
32
+ });
33
+ });
34
+
35
+ describe("resolveEffectiveInstallTargets", () => {
36
+ test("defaults app and configure not in --all", () => {
37
+ const t = resolveEffectiveInstallTargets(undefined);
38
+ expect(t.app.includedInAll).toBe(false);
39
+ expect(t.configure.includedInAll).toBe(false);
40
+ });
41
+
42
+ test("skill mode includes skills in --all not MCP pairs", () => {
43
+ const program: Pick<CliProgram, "mcpServer"> = {};
44
+ const t = resolveEffectiveInstallTargets(undefined, program);
45
+ expect(t.cursorSkill.includedInAll).toBe(true);
46
+ expect(t.cursorMcp.includedInAll).toBe(false);
47
+ });
48
+
49
+ test("mcp mode includes MCP in --all not paired skills", () => {
50
+ const program: Pick<CliProgram, "mcpServer"> = { mcpServer: { enabled: true } };
51
+ const t = resolveEffectiveInstallTargets(undefined, program);
52
+ expect(t.cursorMcp.includedInAll).toBe(true);
53
+ expect(t.cursorSkill.includedInAll).toBe(false);
54
+ expect(t.claudeDesktopMcp.includedInAll).toBe(true);
55
+ });
56
+
57
+ test("both mode includes MCP and skills", () => {
58
+ const program: Pick<CliProgram, "mcpServer"> = { mcpServer: { enabled: true } };
59
+ const t = resolveEffectiveInstallTargets({ agentIntegration: "both" }, program);
60
+ expect(t.cursorMcp.includedInAll).toBe(true);
61
+ expect(t.cursorSkill.includedInAll).toBe(true);
62
+ });
63
+
64
+ test("per-key override disables one skill", () => {
65
+ const t = resolveEffectiveInstallTargets({
66
+ targets: { cursorSkill: false },
67
+ });
68
+ expect(t.claudeSkill.includedInAll).toBe(true);
69
+ expect(t.cursorSkill.enabled).toBe(false);
70
+ });
71
+
72
+ test("scoped --mcp uses effective targets not all MCP hosts", () => {
73
+ const program: CliProgram = {
74
+ key: "app",
75
+ version: "1",
76
+ description: "x",
77
+ mcpServer: { enabled: true },
78
+ install: { agentIntegration: "skill" },
79
+ handler: () => {},
80
+ };
81
+ const effective = resolveEffectiveInstallTargets(program.install, program);
82
+ const scope = { mcp: true };
83
+ expect(isArtifactInScope("cursorMcp", scope, effective, "install-scoped", program)).toBe(false);
84
+ expect(
85
+ isArtifactInScope(
86
+ "cursorMcp",
87
+ scope,
88
+ resolveEffectiveInstallTargets(
89
+ { targets: { cursorMcp: true }, agentIntegration: "both" },
90
+ program,
91
+ ),
92
+ "install-scoped",
93
+ program,
94
+ ),
95
+ ).toBe(true);
96
+ });
97
+
98
+ test("scoped --mcp --skill --configure includes skill and mcp not only configure", () => {
99
+ const program: CliProgram = {
100
+ key: "app",
101
+ version: "1",
102
+ description: "x",
103
+ mcpServer: { enabled: true },
104
+ install: { agentIntegration: "both" },
105
+ handler: () => {},
106
+ };
107
+ const effective = resolveEffectiveInstallTargets(program.install, program);
108
+ const scope = { mcp: true, skill: true, configure: true };
109
+ expect(isArtifactInScope("cursorSkill", scope, effective, "install-scoped", program)).toBe(
110
+ true,
111
+ );
112
+ expect(isArtifactInScope("cursorMcp", scope, effective, "install-scoped", program)).toBe(true);
113
+ expect(isArtifactInScope("configure", scope, effective, "install-scoped", program)).toBe(true);
114
+ expect(isArtifactInScope("claudeSkill", scope, effective, "install-scoped", program)).toBe(
115
+ true,
116
+ );
117
+ });
118
+ });
@@ -1,164 +1,19 @@
1
1
  import { existsSync, rmSync } from "node:fs";
2
- import { resolveAppConfigPath, uninstallAppConfig } from "../config/file.ts";
3
2
  import type { CliProgram } from "../types.ts";
4
- import { uninstallBinary } from "./binary.ts";
5
- import { uninstallCompletions } from "./completions.ts";
6
- import { detectInstalledArtifacts } from "./detect-installed.ts";
7
- import { removeCodexMcpConfig } from "./mcp-codex.ts";
8
- import { removeMcpConfig } from "./mcp-config.ts";
9
- import { detectOpenCodeMcpConfigPath, removeOpenCodeMcpConfig } from "./mcp-opencode.ts";
10
- import { type InstallPaths, userHome } from "./paths.ts";
11
- import {
12
- type InstallOpts,
13
- wantsInstallBin,
14
- wantsInstallCompletions,
15
- wantsInstallMcp,
16
- wantsInstallSkill,
17
- wantsUninstallConfig,
18
- } from "./plan.ts";
3
+ import type { InstallPaths } from "./paths.ts";
4
+ import { buildUninstallPlanFromTargets } from "./target-plan-build.ts";
5
+ import { skillTargetForUninstallSummary } from "./target-registry.ts";
6
+ import type { InstallOpts, UninstallAction } from "./target-types.ts";
19
7
 
20
- export interface UninstallAction {
21
- summary: string;
22
- message: string;
23
- run: () => string[];
24
- }
8
+ export type { InstallActionKind, InstallOpts, UninstallAction } from "./target-types.ts";
25
9
 
26
- /** Builds uninstall actions for scoped targets (--all mirrors install --all). */
10
+ /** Builds uninstall actions for scoped targets or --all (ignores install.targets on --all). */
27
11
  export function buildUninstallPlan(
28
12
  root: CliProgram,
29
13
  paths: InstallPaths,
30
14
  opts: InstallOpts,
31
15
  ): UninstallAction[] {
32
- const detected = detectInstalledArtifacts(paths);
33
- const dry = !!opts.dry;
34
- const actions: UninstallAction[] = [];
35
-
36
- if (wantsInstallBin(opts) && detected.binary) {
37
- actions.push({
38
- summary: `binary: ${paths.binaryPath}`,
39
- message: `Removing binary ${paths.binaryPath}`,
40
- run: () => uninstallBinary(root, paths, dry),
41
- });
42
- }
43
-
44
- if (
45
- wantsInstallCompletions(opts) &&
46
- (detected.bashCompletion || detected.zshCompletion || detected.fishCompletion)
47
- ) {
48
- if (detected.bashCompletion) {
49
- actions.push({
50
- summary: `bash completion: ${paths.bashCompletion}`,
51
- message: `Removing bash completion ${paths.bashCompletion}`,
52
- run: () => uninstallCompletions(paths, dry).filter((p) => p === paths.bashCompletion),
53
- });
54
- }
55
- if (detected.zshCompletion) {
56
- actions.push({
57
- summary: `zsh completion: ${paths.zshCompletion}`,
58
- message: `Removing zsh completion ${paths.zshCompletion}`,
59
- run: () => uninstallCompletions(paths, dry).filter((p) => p === paths.zshCompletion),
60
- });
61
- }
62
- if (detected.fishCompletion) {
63
- actions.push({
64
- summary: `fish completion: ${paths.fishCompletion}`,
65
- message: `Removing fish completion ${paths.fishCompletion}`,
66
- run: () => uninstallCompletions(paths, dry).filter((p) => p === paths.fishCompletion),
67
- });
68
- }
69
- }
70
-
71
- if (wantsInstallSkill(opts) && detected.cursorSkill) {
72
- actions.push({
73
- summary: `cursor skill: ${paths.cursorSkillDir}/`,
74
- message: `Removing Cursor skill ${paths.cursorSkillDir}/`,
75
- run: () => [],
76
- });
77
- }
78
-
79
- if (wantsInstallSkill(opts) && detected.claudeSkill) {
80
- actions.push({
81
- summary: `claude skill: ${paths.claudeSkillDir}/`,
82
- message: `Removing Claude Code skill ${paths.claudeSkillDir}/`,
83
- run: () => [],
84
- });
85
- }
86
-
87
- if (wantsInstallMcp(opts, root)) {
88
- if (detected.cursorMcp) {
89
- actions.push({
90
- summary: `cursor mcp: ${paths.cursorMcpPath}`,
91
- message: `Removing MCP server "${paths.mcpName}" from ${paths.cursorMcpPath}`,
92
- run: () => {
93
- removeMcpConfig(paths.cursorMcpPath, paths.mcpName, dry);
94
- return [paths.cursorMcpPath];
95
- },
96
- });
97
- }
98
- if (detected.claudeMcp) {
99
- actions.push({
100
- summary: `claude code mcp: ${paths.claudeMcpPath}`,
101
- message: `Removing MCP server "${paths.mcpName}" from ${paths.claudeMcpPath}`,
102
- run: () => {
103
- removeMcpConfig(paths.claudeMcpPath, paths.mcpName, dry);
104
- return [paths.claudeMcpPath];
105
- },
106
- });
107
- }
108
- if (detected.claudeDesktopMcp) {
109
- actions.push({
110
- summary: `claude desktop mcp: ${paths.claudeDesktopMcpPath}`,
111
- message: `Removing MCP server "${paths.mcpName}" from ${paths.claudeDesktopMcpPath}`,
112
- run: () => {
113
- removeMcpConfig(paths.claudeDesktopMcpPath, paths.mcpName, dry);
114
- return [paths.claudeDesktopMcpPath];
115
- },
116
- });
117
- }
118
- if (detected.opencodeMcp) {
119
- const openCodePath =
120
- detectOpenCodeMcpConfigPath(userHome(), paths.mcpName) ?? paths.opencodeMcpPath;
121
- actions.push({
122
- summary: `opencode mcp: ${openCodePath}`,
123
- message: `Removing MCP server "${paths.mcpName}" from ${openCodePath}`,
124
- run: () => {
125
- removeOpenCodeMcpConfig(openCodePath, paths.mcpName, dry);
126
- return [openCodePath];
127
- },
128
- });
129
- }
130
- if (detected.codexMcp) {
131
- actions.push({
132
- summary: `codex mcp: ${paths.codexConfigPath}`,
133
- message: `Removing MCP server "${paths.mcpName}" from Codex via codex mcp remove`,
134
- run: () => {
135
- removeCodexMcpConfig(userHome(), paths.mcpName, dry);
136
- return [paths.codexConfigPath];
137
- },
138
- });
139
- }
140
- if (detected.chatGptMcp) {
141
- actions.push({
142
- summary: `chatgpt desktop mcp: ${paths.chatGptMcpPath}`,
143
- message: `Removing MCP server "${paths.mcpName}" from ${paths.chatGptMcpPath}`,
144
- run: () => {
145
- removeMcpConfig(paths.chatGptMcpPath, paths.mcpName, dry);
146
- return [paths.chatGptMcpPath];
147
- },
148
- });
149
- }
150
- }
151
-
152
- if (wantsUninstallConfig(opts, root)) {
153
- const configPath = resolveAppConfigPath(root);
154
- actions.push({
155
- summary: `app config: ${configPath}`,
156
- message: `Removing app config ${configPath}`,
157
- run: () => (uninstallAppConfig(root, dry) ? [configPath] : []),
158
- });
159
- }
160
-
161
- return actions;
16
+ return buildUninstallPlanFromTargets(root, paths, opts);
162
17
  }
163
18
 
164
19
  /** Rimraf skill directories during uninstall. */
@@ -167,3 +22,12 @@ export function uninstallSkillDir(dir: string, dry: boolean): string[] {
167
22
  if (!dry) rmSync(dir, { recursive: true, force: true });
168
23
  return [`${dir}/`];
169
24
  }
25
+
26
+ /** Skill directory for uninstall action summary prefix. */
27
+ export function skillDirFromUninstallSummary(
28
+ summary: string,
29
+ paths: InstallPaths,
30
+ ): string | undefined {
31
+ const target = skillTargetForUninstallSummary(summary);
32
+ return target?.skillDir(paths);
33
+ }
@@ -16,7 +16,13 @@ test("ctx.invocation is cli via Cli.run", async () => {
16
16
  const indexPath = join(import.meta.dir, "index.ts");
17
17
  const { stdout } = await $`bun -e ${`
18
18
  import { Cli, CliProgram } from ${JSON.stringify(indexPath)};
19
- const program = { key: "t", description: "d", version: "0.0.0", handler: (ctx) => console.log(ctx.invocation) };
19
+ const program = {
20
+ key: "t",
21
+ description: "d",
22
+ version: "0.0.0",
23
+ install: { enabled: false },
24
+ handler: (ctx) => console.log(ctx.invocation),
25
+ };
20
26
  await new Cli(program).run([]);
21
27
  `}`.quiet();
22
28
  expect(stdout.toString().trim()).toBe("cli");
package/src/mcp/bundle.ts CHANGED
@@ -160,9 +160,21 @@ export function packMcpBundle(program: CliProgram, opts: PackMcpBundleOpts = {})
160
160
  }
161
161
  }
162
162
 
163
- /** Runs `mcp bundle`: writes `.mcpb` and Claude Code plugin zip; prints both paths. */
163
+ /** Runs `mcp bundle`: writes enabled dist artifacts; prints one path per line. */
164
164
  export function runMcpBundle(program: CliProgram): void {
165
- const mcpbPath = packMcpBundle(program);
166
- const pluginPath = packClaudePlugin(program);
167
- process.stdout.write(`${mcpbPath}\n${pluginPath}\n`);
165
+ const mcp = program.mcpServer;
166
+ if (!mcp?.mcpd && !mcp?.claudePlugin) {
167
+ throw new Error(
168
+ "mcp bundle: enable mcpServer.mcpd and/or mcpServer.claudePlugin on the program root.",
169
+ );
170
+ }
171
+
172
+ const lines: string[] = [];
173
+ if (mcp.mcpd) {
174
+ lines.push(packMcpBundle(program));
175
+ }
176
+ if (mcp.claudePlugin) {
177
+ lines.push(packClaudePlugin(program));
178
+ }
179
+ process.stdout.write(`${lines.join("\n")}\n`);
168
180
  }