argsbarg 4.0.3 → 4.1.0
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 +42 -1
- package/README.md +6 -6
- package/docs/ai-skills.md +9 -4
- package/docs/bundled-docs.md +1 -0
- package/docs/cli-program.md +4 -4
- package/docs/config-schema.md +1 -4
- package/docs/developing.md +1 -1
- package/docs/install.md +160 -39
- package/docs/mcp.md +38 -11
- package/docs/templates/cursor/rules/cli-program.mdc +1 -1
- package/examples/config-app/program.ts +0 -3
- package/examples/consumer-app/README.md +1 -1
- package/examples/consumer-app/src/program.ts +5 -13
- package/examples/mcp-test.ts +14 -24
- package/index.d.ts +60 -5
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +20 -4
- package/src/builtins/config.test.ts +31 -25
- package/src/builtins/config.ts +4 -3
- package/src/builtins/install.ts +50 -57
- package/src/builtins/mcp.ts +1 -1
- package/src/capabilities.ts +4 -4
- package/src/cli.ts +2 -0
- package/src/config/bootstrap.ts +167 -66
- package/src/config/context.test.ts +22 -36
- package/src/config/context.ts +5 -4
- package/src/config/file.test.ts +66 -56
- package/src/config/file.ts +33 -25
- package/src/config/resolve.test.ts +25 -1
- package/src/config/resolve.ts +43 -8
- package/src/config.integration.test.ts +17 -10
- package/src/docs/api-guide.test.ts +1 -1
- package/src/docs/docs.test.ts +1 -1
- package/src/docs/mcp-guide.ts +15 -4
- package/src/docs/mcp-resources.test.ts +63 -0
- package/src/docs/mcp-resources.ts +68 -0
- package/src/hidden-mcpb.test.ts +41 -1
- package/src/index.ts +4 -0
- package/src/install/{binary.ts → app.ts} +13 -13
- package/src/install/bootstrap.ts +22 -0
- package/src/install/detect-installed.ts +2 -97
- package/src/install/index.ts +187 -110
- package/src/install/install-validate.test.ts +61 -0
- package/src/install/install.test.ts +138 -42
- package/src/install/mcp-openclaw.test.ts +40 -0
- package/src/install/mcp-openclaw.ts +106 -0
- package/src/install/normalize.ts +35 -0
- package/src/install/paths.ts +27 -13
- package/src/install/plan.ts +30 -259
- package/src/install/shell.ts +2 -2
- package/src/install/status.test.ts +85 -0
- package/src/install/status.ts +22 -9
- package/src/install/target-base.ts +93 -0
- package/src/install/target-detect.ts +20 -0
- package/src/install/target-effective.ts +131 -0
- package/src/install/target-mcp-cli.ts +149 -0
- package/src/install/target-mcp-json.ts +130 -0
- package/src/install/target-plan-build.ts +67 -0
- package/src/install/target-registry.ts +57 -0
- package/src/install/target-scope.ts +266 -0
- package/src/install/target-skill.ts +104 -0
- package/src/install/target-types.ts +145 -0
- package/src/install/targets/app.ts +69 -0
- package/src/install/targets/chatgpt-mcp.ts +12 -0
- package/src/install/targets/claude-code-mcp.ts +15 -0
- package/src/install/targets/claude-desktop-mcp.ts +12 -0
- package/src/install/targets/claude-skill.ts +16 -0
- package/src/install/targets/codex-mcp.ts +25 -0
- package/src/install/targets/codex-skill.ts +14 -0
- package/src/install/targets/completions.ts +133 -0
- package/src/install/targets/configure.ts +59 -0
- package/src/install/targets/cursor-mcp.ts +15 -0
- package/src/install/targets/cursor-skill.ts +16 -0
- package/src/install/targets/index.ts +53 -0
- package/src/install/targets/openclaw-mcp.ts +25 -0
- package/src/install/targets/openclaw-skill.ts +17 -0
- package/src/install/targets/opencode-mcp.ts +101 -0
- package/src/install/targets/opencode-skill.ts +15 -0
- package/src/install/targets.test.ts +136 -0
- package/src/install/uninstall.ts +16 -152
- package/src/install/update.test.ts +17 -2
- package/src/install/update.ts +2 -5
- package/src/invoke.test.ts +7 -1
- package/src/mcp/bundle.ts +16 -4
- package/src/mcp/claude.test.ts +23 -4
- package/src/mcp/claude.ts +18 -13
- package/src/mcp/tools.ts +4 -1
- package/src/mcp/zip.test.ts +17 -0
- package/src/mcp/zip.ts +62 -9
- package/src/mcp.integration.test.ts +18 -1
- package/src/parse.test.ts +57 -4
- package/src/paths/host.ts +11 -11
- package/src/paths/remove-empty-dir.ts +13 -0
- package/src/skill/generate.ts +89 -5
- package/src/skill/hint.ts +5 -0
- package/src/skill/install.ts +33 -6
- package/src/skill/naming.ts +28 -0
- package/src/types.ts +65 -4
- package/src/validate.ts +79 -4
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import type { CliProgram } from "../../types.ts";
|
|
3
|
+
import { installCompletions, uninstallCompletions } from "../completions.ts";
|
|
4
|
+
import { displayInstallPath, type InstallPaths } from "../paths.ts";
|
|
5
|
+
import { detectShells } from "../shell.ts";
|
|
6
|
+
import { InstallTarget } from "../target-base.ts";
|
|
7
|
+
import type {
|
|
8
|
+
DetectedSnapshot,
|
|
9
|
+
InstallAction,
|
|
10
|
+
InstalledArtifacts,
|
|
11
|
+
InstallStatus,
|
|
12
|
+
TargetPlanContext,
|
|
13
|
+
UninstallAction,
|
|
14
|
+
} from "../target-types.ts";
|
|
15
|
+
|
|
16
|
+
type ShellKind = "bash" | "zsh" | "fish";
|
|
17
|
+
|
|
18
|
+
interface ShellCompletionSpec {
|
|
19
|
+
shell: ShellKind;
|
|
20
|
+
label: string;
|
|
21
|
+
path: (paths: InstallPaths) => string;
|
|
22
|
+
detectedKey: "bashCompletion" | "zshCompletion" | "fishCompletion";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SHELLS: ShellCompletionSpec[] = [
|
|
26
|
+
{
|
|
27
|
+
shell: "bash",
|
|
28
|
+
label: "bash completion",
|
|
29
|
+
path: (p) => p.bashCompletion,
|
|
30
|
+
detectedKey: "bashCompletion",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
shell: "zsh",
|
|
34
|
+
label: "zsh completion",
|
|
35
|
+
path: (p) => p.zshCompletion,
|
|
36
|
+
detectedKey: "zshCompletion",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
shell: "fish",
|
|
40
|
+
label: "fish completion",
|
|
41
|
+
path: (p) => p.fishCompletion,
|
|
42
|
+
detectedKey: "fishCompletion",
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/** Shell tab-completion scripts (bash / zsh / fish). */
|
|
47
|
+
class CompletionsInstallTarget extends InstallTarget {
|
|
48
|
+
readonly key = "completions" as const;
|
|
49
|
+
readonly actionKind = "completions" as const;
|
|
50
|
+
readonly category = "core" as const;
|
|
51
|
+
|
|
52
|
+
isAvailable(_root: CliProgram, _paths: InstallPaths): boolean {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
isDetected(paths: InstallPaths, _root: CliProgram): boolean {
|
|
57
|
+
return SHELLS.some((s) => existsSync(s.path(paths)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
applyDetected(paths: InstallPaths, _root: CliProgram, out: InstalledArtifacts): void {
|
|
61
|
+
for (const s of SHELLS) {
|
|
62
|
+
out[s.detectedKey] = existsSync(s.path(paths));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected isDetectedFromSnapshot(detected: DetectedSnapshot): boolean {
|
|
67
|
+
return detected.bashCompletion || detected.zshCompletion || detected.fishCompletion;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected formatStatusLine(_paths: InstallPaths, _root: CliProgram): string {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected assignStatusLine(_status: InstallStatus, _line: string): void {}
|
|
75
|
+
|
|
76
|
+
contributeStatus(
|
|
77
|
+
paths: InstallPaths,
|
|
78
|
+
_root: CliProgram,
|
|
79
|
+
detected: InstalledArtifacts,
|
|
80
|
+
status: InstallStatus,
|
|
81
|
+
): void {
|
|
82
|
+
if (detected.bashCompletion) {
|
|
83
|
+
status.bashCompletion = displayInstallPath(paths.bashCompletion);
|
|
84
|
+
}
|
|
85
|
+
if (detected.zshCompletion) {
|
|
86
|
+
status.zshCompletion = displayInstallPath(paths.zshCompletion);
|
|
87
|
+
}
|
|
88
|
+
if (detected.fishCompletion) {
|
|
89
|
+
status.fishCompletion = displayInstallPath(paths.fishCompletion);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected buildInstallActions(ctx: TargetPlanContext): InstallAction[] {
|
|
94
|
+
const shells = detectShells();
|
|
95
|
+
const actions: InstallAction[] = [];
|
|
96
|
+
const refresh = ctx.mode === "refresh";
|
|
97
|
+
|
|
98
|
+
for (const s of SHELLS) {
|
|
99
|
+
const onPath = shells[s.shell];
|
|
100
|
+
const path = s.path(ctx.paths);
|
|
101
|
+
const wasInstalled = ctx.detected[s.detectedKey];
|
|
102
|
+
if (!onPath || (refresh && !wasInstalled)) continue;
|
|
103
|
+
|
|
104
|
+
actions.push({
|
|
105
|
+
kind: "completions",
|
|
106
|
+
summary: `${s.label}: ${displayInstallPath(path)}`,
|
|
107
|
+
message: `Writing ${s.label} to ${displayInstallPath(path)}`,
|
|
108
|
+
run: () => {
|
|
109
|
+
const all = installCompletions(ctx.root, ctx.paths, ctx.dry);
|
|
110
|
+
return all.filter((p) => p === path);
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return actions;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
protected buildUninstallActions(ctx: TargetPlanContext): UninstallAction[] {
|
|
118
|
+
const actions: UninstallAction[] = [];
|
|
119
|
+
for (const s of SHELLS) {
|
|
120
|
+
if (!ctx.detected[s.detectedKey]) continue;
|
|
121
|
+
const path = s.path(ctx.paths);
|
|
122
|
+
actions.push({
|
|
123
|
+
kind: "completions",
|
|
124
|
+
summary: `${s.label}: ${displayInstallPath(path)}`,
|
|
125
|
+
message: `Removing ${s.label} ${displayInstallPath(path)}`,
|
|
126
|
+
run: () => uninstallCompletions(ctx.paths, ctx.dry).filter((p) => p === path),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return actions;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const completionsTarget = new CompletionsInstallTarget();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { appConfigInstalled, displayAppConfigPath, uninstallAppConfig } from "../../config/file.ts";
|
|
2
|
+
import type { CliProgram } 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
|
+
isAvailable(root: CliProgram, _paths: InstallPaths): boolean {
|
|
21
|
+
return root.appConfig !== undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
isDetected(_paths: InstallPaths, root: CliProgram): boolean {
|
|
25
|
+
if (!root.appConfig) return false;
|
|
26
|
+
return appConfigInstalled(root);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
applyDetected(_paths: InstallPaths, _root: CliProgram, _out: InstalledArtifacts): void {}
|
|
30
|
+
|
|
31
|
+
protected isDetectedFromSnapshot(detected: DetectedSnapshot): boolean {
|
|
32
|
+
return detected.appConfig ?? false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected formatStatusLine(_paths: InstallPaths, root: CliProgram): string {
|
|
36
|
+
return displayAppConfigPath(root);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected assignStatusLine(_status: InstallStatus, _line: string): void {
|
|
40
|
+
// configure status shown via appConfigStatus in status.ts
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
protected buildInstallActions(_ctx: TargetPlanContext): InstallAction[] {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected buildUninstallActions(ctx: TargetPlanContext): UninstallAction[] {
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
kind: "configure",
|
|
51
|
+
summary: `app config: ${displayAppConfigPath(ctx.root)}`,
|
|
52
|
+
message: `Removing app config ${displayAppConfigPath(ctx.root)}`,
|
|
53
|
+
run: () => uninstallAppConfig(ctx.root, ctx.dry),
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
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,53 @@
|
|
|
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 { completionsTarget } from "./completions.ts";
|
|
10
|
+
import { configureTarget } from "./configure.ts";
|
|
11
|
+
import { cursorMcpTarget } from "./cursor-mcp.ts";
|
|
12
|
+
import { cursorSkillTarget } from "./cursor-skill.ts";
|
|
13
|
+
import { openclawMcpTarget } from "./openclaw-mcp.ts";
|
|
14
|
+
import { openclawSkillTarget } from "./openclaw-skill.ts";
|
|
15
|
+
import { opencodeMcpTarget } from "./opencode-mcp.ts";
|
|
16
|
+
import { opencodeSkillTarget } from "./opencode-skill.ts";
|
|
17
|
+
|
|
18
|
+
/** Ordered install targets (plan iteration order). */
|
|
19
|
+
export const INSTALL_TARGETS: InstallTarget[] = [
|
|
20
|
+
appTarget,
|
|
21
|
+
completionsTarget,
|
|
22
|
+
cursorSkillTarget,
|
|
23
|
+
claudeSkillTarget,
|
|
24
|
+
codexSkillTarget,
|
|
25
|
+
opencodeSkillTarget,
|
|
26
|
+
openclawSkillTarget,
|
|
27
|
+
cursorMcpTarget,
|
|
28
|
+
claudeCodeMcpTarget,
|
|
29
|
+
claudeDesktopMcpTarget,
|
|
30
|
+
opencodeMcpTarget,
|
|
31
|
+
codexMcpTarget,
|
|
32
|
+
openclawMcpTarget,
|
|
33
|
+
chatgptMcpTarget,
|
|
34
|
+
configureTarget,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
appTarget,
|
|
39
|
+
chatgptMcpTarget,
|
|
40
|
+
claudeCodeMcpTarget,
|
|
41
|
+
claudeDesktopMcpTarget,
|
|
42
|
+
claudeSkillTarget,
|
|
43
|
+
codexMcpTarget,
|
|
44
|
+
codexSkillTarget,
|
|
45
|
+
completionsTarget,
|
|
46
|
+
configureTarget,
|
|
47
|
+
cursorMcpTarget,
|
|
48
|
+
cursorSkillTarget,
|
|
49
|
+
openclawMcpTarget,
|
|
50
|
+
openclawSkillTarget,
|
|
51
|
+
opencodeMcpTarget,
|
|
52
|
+
opencodeSkillTarget,
|
|
53
|
+
};
|
|
@@ -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,136 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { CliProgram } from "../types.ts";
|
|
6
|
+
import { maybeBootstrapInstallArgv } from "./bootstrap.ts";
|
|
7
|
+
import { normalizeInstallRawOpts } from "./normalize.ts";
|
|
8
|
+
import { resolveInstallPaths } from "./paths.ts";
|
|
9
|
+
import { resolveAgentIntegration, resolveEffectiveInstallTargets } from "./target-effective.ts";
|
|
10
|
+
import { isArtifactInScope } from "./target-scope.ts";
|
|
11
|
+
|
|
12
|
+
describe("normalizeInstallRawOpts", () => {
|
|
13
|
+
test("bare install sets all", () => {
|
|
14
|
+
expect(normalizeInstallRawOpts({})).toEqual({ all: "1" });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("bare uninstall sets all", () => {
|
|
18
|
+
expect(normalizeInstallRawOpts({ uninstall: "1" })).toEqual({
|
|
19
|
+
uninstall: "1",
|
|
20
|
+
all: "1",
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("configure-only install unchanged", () => {
|
|
25
|
+
expect(normalizeInstallRawOpts({ configure: "1" })).toEqual({ configure: "1" });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("resolveAgentIntegration", () => {
|
|
30
|
+
test("defaults to skill without MCP", () => {
|
|
31
|
+
expect(resolveAgentIntegration(undefined, false)).toBe("skill");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("defaults to mcp when MCP enabled", () => {
|
|
35
|
+
expect(resolveAgentIntegration(undefined, true)).toBe("mcp");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("resolveEffectiveInstallTargets", () => {
|
|
40
|
+
test("defaults app completions configure includedInAll", () => {
|
|
41
|
+
const t = resolveEffectiveInstallTargets(undefined);
|
|
42
|
+
expect(t.app.includedInAll).toBe(true);
|
|
43
|
+
expect(t.completions.includedInAll).toBe(true);
|
|
44
|
+
expect(t.configure.includedInAll).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("skill mode includes skills in --all not MCP pairs", () => {
|
|
48
|
+
const program: Pick<CliProgram, "mcpServer"> = {};
|
|
49
|
+
const t = resolveEffectiveInstallTargets(undefined, program);
|
|
50
|
+
expect(t.cursorSkill.includedInAll).toBe(true);
|
|
51
|
+
expect(t.cursorMcp.includedInAll).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("mcp mode includes MCP in --all not paired skills", () => {
|
|
55
|
+
const program: Pick<CliProgram, "mcpServer"> = { mcpServer: { enabled: true } };
|
|
56
|
+
const t = resolveEffectiveInstallTargets(undefined, program);
|
|
57
|
+
expect(t.cursorMcp.includedInAll).toBe(true);
|
|
58
|
+
expect(t.cursorSkill.includedInAll).toBe(false);
|
|
59
|
+
expect(t.claudeDesktopMcp.includedInAll).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("both mode includes MCP and skills", () => {
|
|
63
|
+
const program: Pick<CliProgram, "mcpServer"> = { mcpServer: { enabled: true } };
|
|
64
|
+
const t = resolveEffectiveInstallTargets({ agentIntegration: "both" }, program);
|
|
65
|
+
expect(t.cursorMcp.includedInAll).toBe(true);
|
|
66
|
+
expect(t.cursorSkill.includedInAll).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("per-key override disables one skill", () => {
|
|
70
|
+
const t = resolveEffectiveInstallTargets({
|
|
71
|
+
targets: { cursorSkill: false },
|
|
72
|
+
});
|
|
73
|
+
expect(t.claudeSkill.includedInAll).toBe(true);
|
|
74
|
+
expect(t.cursorSkill.enabled).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("scoped --mcp uses effective targets not all MCP hosts", () => {
|
|
78
|
+
const program: CliProgram = {
|
|
79
|
+
key: "app",
|
|
80
|
+
version: "1",
|
|
81
|
+
description: "x",
|
|
82
|
+
mcpServer: { enabled: true },
|
|
83
|
+
install: { agentIntegration: "skill" },
|
|
84
|
+
handler: () => {},
|
|
85
|
+
};
|
|
86
|
+
const effective = resolveEffectiveInstallTargets(program.install, program);
|
|
87
|
+
const scope = { mcp: true };
|
|
88
|
+
expect(isArtifactInScope("cursorMcp", scope, effective, "install-scoped", program)).toBe(false);
|
|
89
|
+
expect(
|
|
90
|
+
isArtifactInScope(
|
|
91
|
+
"cursorMcp",
|
|
92
|
+
scope,
|
|
93
|
+
resolveEffectiveInstallTargets(
|
|
94
|
+
{ targets: { cursorMcp: true }, agentIntegration: "both" },
|
|
95
|
+
program,
|
|
96
|
+
),
|
|
97
|
+
"install-scoped",
|
|
98
|
+
program,
|
|
99
|
+
),
|
|
100
|
+
).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("maybeBootstrapInstallArgv", () => {
|
|
105
|
+
const program: CliProgram = {
|
|
106
|
+
key: "bootapp",
|
|
107
|
+
version: "1.0.0",
|
|
108
|
+
description: "Boot",
|
|
109
|
+
handler: () => {},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
test("rewrites empty argv when app missing and TTY", () => {
|
|
113
|
+
const prev = process.stdin.isTTY;
|
|
114
|
+
Object.defineProperty(process.stdin, "isTTY", { value: true, configurable: true });
|
|
115
|
+
try {
|
|
116
|
+
expect(maybeBootstrapInstallArgv([], program)).toEqual(["install"]);
|
|
117
|
+
} finally {
|
|
118
|
+
Object.defineProperty(process.stdin, "isTTY", { value: prev, configurable: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("does not rewrite when app exists", () => {
|
|
123
|
+
const home = mkdtempSync(join(tmpdir(), "boot-"));
|
|
124
|
+
const prevHome = process.env.HOME;
|
|
125
|
+
process.env.HOME = home;
|
|
126
|
+
const paths = resolveInstallPaths(program);
|
|
127
|
+
try {
|
|
128
|
+
mkdirSync(paths.appDir, { recursive: true });
|
|
129
|
+
writeFileSync(paths.appPath, "x", "utf8");
|
|
130
|
+
expect(maybeBootstrapInstallArgv([], program)).toEqual([]);
|
|
131
|
+
} finally {
|
|
132
|
+
process.env.HOME = prevHome;
|
|
133
|
+
rmSync(home, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|