argsbarg 3.4.1 → 3.5.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 +26 -1
- package/biome.json +29 -6
- package/bun.lock +22 -0
- package/docs/install.md +1 -1
- package/docs/mcp.md +63 -3
- package/index.d.ts +51 -51
- package/justfile +27 -6
- package/package.json +4 -2
- package/scripts/release.ts +26 -9
- package/src/builtins/builtins.test.ts +9 -4
- package/src/builtins/completion-bash.ts +74 -50
- package/src/builtins/completion-fish.ts +3 -8
- package/src/builtins/completion-group.ts +1 -1
- package/src/builtins/completion-zsh.ts +80 -42
- package/src/builtins/dispatch.ts +20 -16
- package/src/builtins/export.ts +19 -10
- package/src/builtins/index.ts +9 -4
- package/src/builtins/install.ts +10 -10
- package/src/builtins/mcp.ts +3 -3
- package/src/builtins/presentation.ts +8 -8
- package/src/builtins/scopes.ts +1 -1
- package/src/builtins/version.ts +1 -1
- package/src/completion.ts +4 -4
- package/src/docs/api-guide.test.ts +2 -2
- package/src/docs/api-guide.ts +2 -2
- package/src/docs/builtin.ts +27 -8
- package/src/docs/docs.test.ts +23 -12
- package/src/docs/mcp-guide.ts +112 -11
- package/src/docs/resolve.ts +10 -3
- package/src/docs/save.ts +11 -3
- package/src/headless.test.ts +8 -16
- package/src/help.ts +73 -43
- package/src/hidden-mcpb.test.ts +8 -10
- package/src/hidden.ts +2 -2
- package/src/index.test.ts +113 -89
- package/src/index.ts +24 -24
- package/src/install/binary.ts +12 -5
- package/src/install/completions.ts +7 -3
- package/src/install/detect-installed.ts +35 -4
- package/src/install/gh-release-update.ts +31 -23
- package/src/install/index.ts +69 -19
- package/src/install/install.test.ts +57 -8
- package/src/install/mcp-codex.test.ts +57 -0
- package/src/install/mcp-codex.ts +125 -0
- package/src/install/mcp-config.ts +12 -5
- package/src/install/mcp-opencode.test.ts +98 -0
- package/src/install/mcp-opencode.ts +149 -0
- package/src/install/paths.ts +51 -3
- package/src/install/plan.ts +96 -7
- package/src/install/shell.ts +1 -4
- package/src/install/status.ts +15 -7
- package/src/install/uninstall.ts +49 -5
- package/src/install/update.test.ts +2 -2
- package/src/install/update.ts +3 -1
- package/src/invoke.ts +12 -9
- package/src/mcp/bundle.ts +38 -14
- package/src/mcp/env.ts +7 -13
- package/src/mcp/server.ts +12 -6
- package/src/mcp/tools.ts +20 -4
- package/src/mcp.ts +3 -3
- package/src/parse.ts +96 -24
- package/src/runtime.ts +22 -12
- package/src/schema.ts +11 -5
- package/src/skill/generate.ts +4 -4
- package/src/skill/install.ts +6 -2
- package/src/validate.ts +21 -16
package/src/index.ts
CHANGED
|
@@ -7,28 +7,8 @@ It gives consumers one stable import path without forcing them to know the inter
|
|
|
7
7
|
module layout.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
export { cliInvoke } from "./invoke.ts";
|
|
11
|
-
export type { CliInvokeKind, CliInvokeResult } from "./invoke.ts";
|
|
12
10
|
export { CliContext } from "./context.ts";
|
|
13
|
-
export {
|
|
14
|
-
export { CliFallbackMode, CliOptionKind, CliSchemaValidationError } from "./types.ts";
|
|
15
|
-
export type {
|
|
16
|
-
CliProgram,
|
|
17
|
-
CliHandler,
|
|
18
|
-
CliInvocation,
|
|
19
|
-
CliMcpResource,
|
|
20
|
-
CliMcpServerConfig,
|
|
21
|
-
CliMcpBundleConfig,
|
|
22
|
-
CliMcpToolConfig,
|
|
23
|
-
CliInstallConfig,
|
|
24
|
-
CliUpdateArtifact,
|
|
25
|
-
CliUpdateGetLatest,
|
|
26
|
-
CliDocsConfig,
|
|
27
|
-
CliDocsTopic,
|
|
28
|
-
CliOption,
|
|
29
|
-
CliPositional,
|
|
30
|
-
} from "./types.ts";
|
|
31
|
-
export { isInteractiveTty } from "./utils.ts";
|
|
11
|
+
export type { HeadlessContext } from "./headless.ts";
|
|
32
12
|
export {
|
|
33
13
|
formatDryRunMessage,
|
|
34
14
|
requireYesInNonTty,
|
|
@@ -37,7 +17,7 @@ export {
|
|
|
37
17
|
shouldRunHeadlessWithYes,
|
|
38
18
|
wantsExplicitJson,
|
|
39
19
|
} from "./headless.ts";
|
|
40
|
-
export type {
|
|
20
|
+
export type { GhReleaseUpdateConfig, GhVersionCheckConfig } from "./install/gh-release-update.ts";
|
|
41
21
|
export {
|
|
42
22
|
createGhFetchLatest,
|
|
43
23
|
createGhVersionCheck,
|
|
@@ -45,6 +25,26 @@ export {
|
|
|
45
25
|
isAlreadyCurrent,
|
|
46
26
|
parseReleaseTag,
|
|
47
27
|
} from "./install/gh-release-update.ts";
|
|
48
|
-
export type {
|
|
49
|
-
export {
|
|
28
|
+
export type { CliInvokeKind, CliInvokeResult } from "./invoke.ts";
|
|
29
|
+
export { cliInvoke } from "./invoke.ts";
|
|
50
30
|
export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
|
|
31
|
+
export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
|
|
32
|
+
export { cliErrWithHelp, cliRun } from "./runtime";
|
|
33
|
+
export type {
|
|
34
|
+
CliDocsConfig,
|
|
35
|
+
CliDocsTopic,
|
|
36
|
+
CliHandler,
|
|
37
|
+
CliInstallConfig,
|
|
38
|
+
CliInvocation,
|
|
39
|
+
CliMcpBundleConfig,
|
|
40
|
+
CliMcpResource,
|
|
41
|
+
CliMcpServerConfig,
|
|
42
|
+
CliMcpToolConfig,
|
|
43
|
+
CliOption,
|
|
44
|
+
CliPositional,
|
|
45
|
+
CliProgram,
|
|
46
|
+
CliUpdateArtifact,
|
|
47
|
+
CliUpdateGetLatest,
|
|
48
|
+
} from "./types.ts";
|
|
49
|
+
export { CliFallbackMode, CliOptionKind, CliSchemaValidationError } from "./types.ts";
|
|
50
|
+
export { isInteractiveTty } from "./utils.ts";
|
package/src/install/binary.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
copyFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
2
9
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { CliProgram } from "../types.ts";
|
|
4
|
-
import { InstallPaths } from "./paths.ts";
|
|
10
|
+
import type { CliProgram } from "../types.ts";
|
|
11
|
+
import type { InstallPaths } from "./paths.ts";
|
|
5
12
|
import {
|
|
6
13
|
buildPathRcBlock,
|
|
7
14
|
buildZshFpathRcBlock,
|
|
@@ -39,7 +46,7 @@ export function installBinary(
|
|
|
39
46
|
let content = existsSync(paths.bashRc) ? readFileSync(paths.bashRc, "utf8") : "";
|
|
40
47
|
if (!hasRcBlock(content, root.key, "path")) {
|
|
41
48
|
if (!content.endsWith("\n") && content.length > 0) content += "\n";
|
|
42
|
-
content += block
|
|
49
|
+
content += `${block}\n`;
|
|
43
50
|
if (!dry) writeFileSync(paths.bashRc, content, "utf8");
|
|
44
51
|
changed.push(paths.bashRc);
|
|
45
52
|
patchedBashRc = true;
|
|
@@ -52,7 +59,7 @@ export function installBinary(
|
|
|
52
59
|
let content = existsSync(paths.zshRc) ? readFileSync(paths.zshRc, "utf8") : "";
|
|
53
60
|
if (!hasRcBlock(content, root.key, "fpath")) {
|
|
54
61
|
if (!content.endsWith("\n") && content.length > 0) content += "\n";
|
|
55
|
-
content += block
|
|
62
|
+
content += `${block}\n`;
|
|
56
63
|
if (!dry) writeFileSync(paths.zshRc, content, "utf8");
|
|
57
64
|
changed.push(paths.zshRc);
|
|
58
65
|
patchedZshRc = true;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
completionBashScript,
|
|
5
|
+
completionFishScript,
|
|
6
|
+
completionZshScript,
|
|
7
|
+
} from "../builtins/index.ts";
|
|
5
8
|
import { cliPresentationRoot } from "../builtins/presentation.ts";
|
|
6
|
-
import {
|
|
9
|
+
import type { CliProgram } from "../types.ts";
|
|
10
|
+
import type { InstallPaths } from "./paths.ts";
|
|
7
11
|
import { detectShells } from "./shell.ts";
|
|
8
12
|
|
|
9
13
|
/** Writes shell completion scripts for detected shells. */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { codexMcpHasServer } from "./mcp-codex.ts";
|
|
3
|
+
import { detectOpenCodeMcpConfigPath } from "./mcp-opencode.ts";
|
|
4
|
+
import { type InstallPaths, userHome } from "./paths.ts";
|
|
3
5
|
|
|
4
6
|
export interface InstalledArtifacts {
|
|
5
7
|
binary: boolean;
|
|
@@ -10,6 +12,10 @@ export interface InstalledArtifacts {
|
|
|
10
12
|
claudeSkill: boolean;
|
|
11
13
|
cursorMcp: boolean;
|
|
12
14
|
claudeMcp: boolean;
|
|
15
|
+
claudeDesktopMcp: boolean;
|
|
16
|
+
opencodeMcp: boolean;
|
|
17
|
+
codexMcp: boolean;
|
|
18
|
+
chatGptMcp: boolean;
|
|
13
19
|
bashRcPath: boolean;
|
|
14
20
|
zshRcFpath: boolean;
|
|
15
21
|
}
|
|
@@ -35,6 +41,10 @@ export function detectInstalledArtifacts(paths: InstallPaths): InstalledArtifact
|
|
|
35
41
|
claudeSkill: existsSync(paths.claudeSkillDir),
|
|
36
42
|
cursorMcp: mcpConfigHasServer(paths.cursorMcpPath, paths.mcpName),
|
|
37
43
|
claudeMcp: mcpConfigHasServer(paths.claudeMcpPath, paths.mcpName),
|
|
44
|
+
claudeDesktopMcp: mcpConfigHasServer(paths.claudeDesktopMcpPath, paths.mcpName),
|
|
45
|
+
opencodeMcp: detectOpenCodeMcpConfigPath(userHome(), paths.mcpName) !== undefined,
|
|
46
|
+
codexMcp: codexMcpHasServer(userHome(), paths.mcpName),
|
|
47
|
+
chatGptMcp: mcpConfigHasServer(paths.chatGptMcpPath, paths.mcpName),
|
|
38
48
|
bashRcPath: false,
|
|
39
49
|
zshRcFpath: false,
|
|
40
50
|
};
|
|
@@ -49,18 +59,39 @@ export interface InstallStatus {
|
|
|
49
59
|
claudeSkill?: string;
|
|
50
60
|
cursorMcp?: string;
|
|
51
61
|
claudeMcp?: string;
|
|
62
|
+
claudeDesktopMcp?: string;
|
|
63
|
+
opencodeMcp?: string;
|
|
64
|
+
codexMcp?: string;
|
|
65
|
+
chatGptMcp?: string;
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
/** Builds a status inventory from detected artifacts. */
|
|
55
|
-
export function buildInstallStatus(
|
|
69
|
+
export function buildInstallStatus(
|
|
70
|
+
paths: InstallPaths,
|
|
71
|
+
detected: InstalledArtifacts,
|
|
72
|
+
): InstallStatus {
|
|
56
73
|
const status: InstallStatus = {};
|
|
57
74
|
if (detected.binary) status.binary = paths.binaryPath;
|
|
58
75
|
if (detected.bashCompletion) status.bashCompletion = paths.bashCompletion;
|
|
59
76
|
if (detected.zshCompletion) status.zshCompletion = paths.zshCompletion;
|
|
60
77
|
if (detected.fishCompletion) status.fishCompletion = paths.fishCompletion;
|
|
61
|
-
if (detected.cursorSkill) status.cursorSkill = paths.cursorSkillDir
|
|
62
|
-
if (detected.claudeSkill) status.claudeSkill = paths.claudeSkillDir
|
|
78
|
+
if (detected.cursorSkill) status.cursorSkill = `${paths.cursorSkillDir}/`;
|
|
79
|
+
if (detected.claudeSkill) status.claudeSkill = `${paths.claudeSkillDir}/`;
|
|
63
80
|
if (detected.cursorMcp) status.cursorMcp = `${paths.cursorMcpPath} (server "${paths.mcpName}")`;
|
|
64
81
|
if (detected.claudeMcp) status.claudeMcp = `${paths.claudeMcpPath} (server "${paths.mcpName}")`;
|
|
82
|
+
if (detected.claudeDesktopMcp) {
|
|
83
|
+
status.claudeDesktopMcp = `${paths.claudeDesktopMcpPath} (server "${paths.mcpName}")`;
|
|
84
|
+
}
|
|
85
|
+
if (detected.opencodeMcp) {
|
|
86
|
+
const openCodePath =
|
|
87
|
+
detectOpenCodeMcpConfigPath(userHome(), paths.mcpName) ?? paths.opencodeMcpPath;
|
|
88
|
+
status.opencodeMcp = `${openCodePath} (server "${paths.mcpName}")`;
|
|
89
|
+
}
|
|
90
|
+
if (detected.codexMcp) {
|
|
91
|
+
status.codexMcp = `${paths.codexConfigPath} (server "${paths.mcpName}")`;
|
|
92
|
+
}
|
|
93
|
+
if (detected.chatGptMcp) {
|
|
94
|
+
status.chatGptMcp = `${paths.chatGptMcpPath} (server "${paths.mcpName}")`;
|
|
95
|
+
}
|
|
65
96
|
return status;
|
|
66
97
|
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
2
10
|
import { tmpdir } from "node:os";
|
|
3
11
|
import { dirname, join } from "node:path";
|
|
4
12
|
import type { CliUpdateGetLatest } from "../types.ts";
|
|
@@ -113,7 +121,8 @@ export function createGhVersionCheck(config: GhVersionCheckConfig): {
|
|
|
113
121
|
return;
|
|
114
122
|
}
|
|
115
123
|
|
|
116
|
-
void config
|
|
124
|
+
void config
|
|
125
|
+
.fetchLatest()
|
|
117
126
|
.then((latest) => {
|
|
118
127
|
writeVersionCheckCache(config.cachePath, { fetchedAt: Date.now(), latest });
|
|
119
128
|
})
|
|
@@ -125,26 +134,22 @@ export function createGhVersionCheck(config: GhVersionCheckConfig): {
|
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
/** Shared `gh release view` fetcher for hooks and version-check refresh. */
|
|
128
|
-
export function createGhFetchLatest(
|
|
137
|
+
export function createGhFetchLatest(
|
|
138
|
+
config: Pick<GhReleaseUpdateConfig, "repo" | "repoEnvHint">,
|
|
139
|
+
): () => Promise<string> {
|
|
129
140
|
return async () => {
|
|
130
|
-
const result = await runGh([
|
|
131
|
-
"release",
|
|
132
|
-
"view",
|
|
133
|
-
"--repo",
|
|
134
|
-
config.repo,
|
|
135
|
-
"--json",
|
|
136
|
-
"tagName",
|
|
137
|
-
]);
|
|
141
|
+
const result = await runGh(["release", "view", "--repo", config.repo, "--json", "tagName"]);
|
|
138
142
|
|
|
139
143
|
if (result.exitCode !== 0) {
|
|
140
144
|
const detail = result.stderr.trim() || result.stdout.trim();
|
|
141
|
-
const hint =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
?
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
const hint =
|
|
146
|
+
detail.includes("auth") || detail.includes("401")
|
|
147
|
+
? " Run `gh auth login` and try again."
|
|
148
|
+
: detail.includes("release not found") || detail.includes("Not Found")
|
|
149
|
+
? config.repoEnvHint
|
|
150
|
+
? ` ${config.repoEnvHint}`
|
|
151
|
+
: ` No releases found for ${config.repo}.`
|
|
152
|
+
: "";
|
|
148
153
|
throw new Error(
|
|
149
154
|
`Failed to fetch latest release from ${config.repo}: ${detail || "unknown error"}.${hint}`,
|
|
150
155
|
);
|
|
@@ -173,7 +178,10 @@ function ensureGhAvailable(): void {
|
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
|
|
176
|
-
async function downloadReleaseAsset(
|
|
181
|
+
async function downloadReleaseAsset(
|
|
182
|
+
config: GhReleaseUpdateConfig,
|
|
183
|
+
tempDir: string,
|
|
184
|
+
): Promise<string> {
|
|
177
185
|
const result = await runGh([
|
|
178
186
|
"release",
|
|
179
187
|
"download",
|
|
@@ -187,9 +195,7 @@ async function downloadReleaseAsset(config: GhReleaseUpdateConfig, tempDir: stri
|
|
|
187
195
|
|
|
188
196
|
if (result.exitCode !== 0) {
|
|
189
197
|
const detail = result.stderr.trim() || result.stdout.trim();
|
|
190
|
-
throw new Error(
|
|
191
|
-
`Failed to download release from ${config.repo}: ${detail || "unknown error"}`,
|
|
192
|
-
);
|
|
198
|
+
throw new Error(`Failed to download release from ${config.repo}: ${detail || "unknown error"}`);
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
const downloadedPath = join(tempDir, config.asset);
|
|
@@ -200,7 +206,9 @@ async function downloadReleaseAsset(config: GhReleaseUpdateConfig, tempDir: stri
|
|
|
200
206
|
return downloadedPath;
|
|
201
207
|
}
|
|
202
208
|
|
|
203
|
-
async function runGh(
|
|
209
|
+
async function runGh(
|
|
210
|
+
args: string[],
|
|
211
|
+
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
204
212
|
const proc = Bun.spawn(["gh", ...args], { stdout: "pipe", stderr: "pipe" });
|
|
205
213
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
206
214
|
proc.exited,
|
package/src/install/index.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { readSync } from "node:fs";
|
|
2
2
|
import { resolveCapabilities } from "../capabilities.ts";
|
|
3
|
-
import { CliProgram } from "../types.ts";
|
|
4
3
|
import { cliSkillInstall } from "../skill/install.ts";
|
|
4
|
+
import type { CliProgram } from "../types.ts";
|
|
5
|
+
import { checkCodexMcpConflict } from "./mcp-codex.ts";
|
|
5
6
|
import { checkMcpConflict, expectedMcpEntry } from "./mcp-config.ts";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type InstallAction,
|
|
10
|
-
type InstallOpts,
|
|
11
|
-
} from "./plan.ts";
|
|
12
|
-
import { resolveInstallPaths } from "./paths.ts";
|
|
7
|
+
import { checkOpenCodeMcpConflict, expectedOpenCodeMcpEntry } from "./mcp-opencode.ts";
|
|
8
|
+
import { resolveInstallPaths, userHome } from "./paths.ts";
|
|
9
|
+
import { buildInstallPlan, buildUpdatePlan, type InstallAction, type InstallOpts } from "./plan.ts";
|
|
13
10
|
import { installErr, installInfo, installOut, printInstallStatus } from "./status.ts";
|
|
14
|
-
import { buildUninstallPlan,
|
|
11
|
+
import { buildUninstallPlan, type UninstallAction, uninstallSkillDir } from "./uninstall.ts";
|
|
15
12
|
import { cliUpdate } from "./update.ts";
|
|
16
13
|
|
|
17
14
|
export function parseInstallOpts(raw: Record<string, string>): InstallOpts {
|
|
@@ -54,19 +51,39 @@ export function validateInstallOpts(opts: InstallOpts): string | null {
|
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
const mutationFlags =
|
|
57
|
-
opts.all ||
|
|
54
|
+
opts.all ||
|
|
55
|
+
opts.bin ||
|
|
56
|
+
opts.completions ||
|
|
57
|
+
opts.skill ||
|
|
58
|
+
opts.mcp ||
|
|
59
|
+
opts.reinstall ||
|
|
60
|
+
opts.update ||
|
|
61
|
+
opts.uninstall;
|
|
58
62
|
if (opts.status && mutationFlags) {
|
|
59
63
|
return "--status is mutually exclusive with install/reinstall/uninstall targets.";
|
|
60
64
|
}
|
|
61
65
|
if (
|
|
62
66
|
opts.reinstall &&
|
|
63
|
-
(opts.all ||
|
|
67
|
+
(opts.all ||
|
|
68
|
+
opts.completions ||
|
|
69
|
+
opts.skill ||
|
|
70
|
+
opts.mcp ||
|
|
71
|
+
opts.uninstall ||
|
|
72
|
+
opts.status ||
|
|
73
|
+
opts.update)
|
|
64
74
|
) {
|
|
65
75
|
return "--reinstall cannot be combined with other target flags.";
|
|
66
76
|
}
|
|
67
77
|
if (
|
|
68
78
|
opts.update &&
|
|
69
|
-
(opts.all ||
|
|
79
|
+
(opts.all ||
|
|
80
|
+
opts.bin ||
|
|
81
|
+
opts.completions ||
|
|
82
|
+
opts.skill ||
|
|
83
|
+
opts.mcp ||
|
|
84
|
+
opts.uninstall ||
|
|
85
|
+
opts.status ||
|
|
86
|
+
opts.reinstall)
|
|
70
87
|
) {
|
|
71
88
|
return "--update cannot be combined with other target flags.";
|
|
72
89
|
}
|
|
@@ -134,7 +151,11 @@ function executePlan(
|
|
|
134
151
|
export async function runInstallMutation(
|
|
135
152
|
root: CliProgram,
|
|
136
153
|
rawOpts: Record<string, string>,
|
|
137
|
-
): Promise<{
|
|
154
|
+
): Promise<{
|
|
155
|
+
changed: string[];
|
|
156
|
+
opts: InstallOpts;
|
|
157
|
+
paths: ReturnType<typeof resolveInstallPaths>;
|
|
158
|
+
}> {
|
|
138
159
|
const opts = parseInstallOpts(rawOpts);
|
|
139
160
|
const err = validateInstallOpts(opts);
|
|
140
161
|
if (err) {
|
|
@@ -150,13 +171,32 @@ export async function runInstallMutation(
|
|
|
150
171
|
|
|
151
172
|
if (!opts.uninstall && resolveCapabilities(root).mcp && (opts.all || opts.mcp)) {
|
|
152
173
|
const entry = expectedMcpEntry(root);
|
|
174
|
+
const openCodeEntry = expectedOpenCodeMcpEntry(root);
|
|
153
175
|
const yes = !!opts.yes;
|
|
154
|
-
for (const p of [
|
|
176
|
+
for (const p of [
|
|
177
|
+
paths.cursorMcpPath,
|
|
178
|
+
paths.claudeMcpPath,
|
|
179
|
+
paths.claudeDesktopMcpPath,
|
|
180
|
+
paths.chatGptMcpPath,
|
|
181
|
+
]) {
|
|
155
182
|
const conflict = checkMcpConflict(p, paths.mcpName, entry, yes);
|
|
156
183
|
if (conflict) {
|
|
157
184
|
throw new Error(conflict);
|
|
158
185
|
}
|
|
159
186
|
}
|
|
187
|
+
const openCodeConflict = checkOpenCodeMcpConflict(
|
|
188
|
+
paths.opencodeMcpPath,
|
|
189
|
+
paths.mcpName,
|
|
190
|
+
openCodeEntry,
|
|
191
|
+
yes,
|
|
192
|
+
);
|
|
193
|
+
if (openCodeConflict) {
|
|
194
|
+
throw new Error(openCodeConflict);
|
|
195
|
+
}
|
|
196
|
+
const codexConflict = checkCodexMcpConflict(userHome(), paths.mcpName, entry, yes);
|
|
197
|
+
if (codexConflict) {
|
|
198
|
+
throw new Error(codexConflict);
|
|
199
|
+
}
|
|
160
200
|
}
|
|
161
201
|
|
|
162
202
|
let actions: Array<InstallAction | UninstallAction>;
|
|
@@ -173,9 +213,12 @@ export async function runInstallMutation(
|
|
|
173
213
|
}
|
|
174
214
|
|
|
175
215
|
if (!opts.quiet && !opts.json) {
|
|
176
|
-
installOut(
|
|
216
|
+
installOut(
|
|
217
|
+
`About to ${opts.uninstall ? "remove" : opts.reinstall ? "reinstall" : "install"}:`,
|
|
218
|
+
opts,
|
|
219
|
+
);
|
|
177
220
|
for (const a of actions) {
|
|
178
|
-
installOut(
|
|
221
|
+
installOut(` - ${a.summary}`, opts);
|
|
179
222
|
}
|
|
180
223
|
}
|
|
181
224
|
|
|
@@ -194,7 +237,10 @@ export async function runInstallMutation(
|
|
|
194
237
|
}
|
|
195
238
|
|
|
196
239
|
/** Main install command orchestrator. */
|
|
197
|
-
export async function cliInstall(
|
|
240
|
+
export async function cliInstall(
|
|
241
|
+
root: CliProgram,
|
|
242
|
+
rawOpts: Record<string, string>,
|
|
243
|
+
): Promise<never> {
|
|
198
244
|
const opts = parseInstallOpts(rawOpts);
|
|
199
245
|
const err = validateInstallOpts(opts);
|
|
200
246
|
if (err) {
|
|
@@ -225,12 +271,16 @@ export async function cliInstall(root: CliProgram, rawOpts: Record<string, strin
|
|
|
225
271
|
}
|
|
226
272
|
|
|
227
273
|
if (mutationOpts.json) {
|
|
228
|
-
process.stdout.write(JSON.stringify(changed, null, 2)
|
|
274
|
+
process.stdout.write(`${JSON.stringify(changed, null, 2)}\n`);
|
|
229
275
|
process.exit(0);
|
|
230
276
|
}
|
|
231
277
|
|
|
232
278
|
if (!mutationOpts.quiet && changed.length > 0) {
|
|
233
|
-
const verb = mutationOpts.uninstall
|
|
279
|
+
const verb = mutationOpts.uninstall
|
|
280
|
+
? "Removed"
|
|
281
|
+
: mutationOpts.reinstall
|
|
282
|
+
? "Reinstalled"
|
|
283
|
+
: "Installed";
|
|
234
284
|
installOut(`${verb} ${changed.length} file(s).`, mutationOpts);
|
|
235
285
|
if (
|
|
236
286
|
!mutationOpts.uninstall &&
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync,
|
|
3
|
-
import { join } from "node:path";
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { tmpdir } from "node:os";
|
|
5
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { CliProgram } from "../types.ts";
|
|
6
6
|
import { detectInstalledArtifacts } from "./detect-installed.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { parseInstallOpts, runInstallMutation, validateInstallOpts } from "./index.ts";
|
|
8
|
+
import { opencodeConfigDir } from "./mcp-opencode.ts";
|
|
9
|
+
import { resolveClaudeDesktopMcpPath, resolveInstallPaths } from "./paths.ts";
|
|
8
10
|
import { buildInstallPlan } from "./plan.ts";
|
|
9
|
-
import { buildUninstallPlan } from "./uninstall.ts";
|
|
10
11
|
import { printInstallStatus } from "./status.ts";
|
|
11
|
-
import {
|
|
12
|
+
import { buildUninstallPlan } from "./uninstall.ts";
|
|
12
13
|
|
|
13
14
|
const fixture: CliProgram = {
|
|
14
15
|
key: "testapp",
|
|
@@ -21,15 +22,21 @@ const fixture: CliProgram = {
|
|
|
21
22
|
let home: string;
|
|
22
23
|
let prevHome: string | undefined;
|
|
23
24
|
|
|
25
|
+
let prevXdg: string | undefined;
|
|
26
|
+
|
|
24
27
|
beforeEach(() => {
|
|
25
28
|
home = mkdtempSync(join(tmpdir(), "argsbarg-install-"));
|
|
26
29
|
prevHome = process.env.HOME;
|
|
30
|
+
prevXdg = process.env.XDG_CONFIG_HOME;
|
|
27
31
|
process.env.HOME = home;
|
|
32
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
28
33
|
});
|
|
29
34
|
|
|
30
35
|
afterEach(() => {
|
|
31
36
|
if (prevHome === undefined) delete process.env.HOME;
|
|
32
37
|
else process.env.HOME = prevHome;
|
|
38
|
+
if (prevXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
39
|
+
else process.env.XDG_CONFIG_HOME = prevXdg;
|
|
33
40
|
rmSync(home, { recursive: true, force: true });
|
|
34
41
|
});
|
|
35
42
|
|
|
@@ -45,6 +52,18 @@ describe("install paths", () => {
|
|
|
45
52
|
const paths = resolveInstallPaths(fixture, {});
|
|
46
53
|
expect(paths.fishCompletion).toBe(join(xdg, "fish", "completions", "testapp.fish"));
|
|
47
54
|
});
|
|
55
|
+
|
|
56
|
+
test("claude desktop mcp path on darwin", () => {
|
|
57
|
+
const prev = process.platform;
|
|
58
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
59
|
+
try {
|
|
60
|
+
expect(resolveClaudeDesktopMcpPath(home)).toBe(
|
|
61
|
+
join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
62
|
+
);
|
|
63
|
+
} finally {
|
|
64
|
+
Object.defineProperty(process, "platform", { value: prev });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
48
67
|
});
|
|
49
68
|
|
|
50
69
|
describe("detect installed", () => {
|
|
@@ -68,6 +87,36 @@ describe("install plan", () => {
|
|
|
68
87
|
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
|
|
69
88
|
expect(plan.some((a) => a.kind === "binary")).toBe(true);
|
|
70
89
|
});
|
|
90
|
+
|
|
91
|
+
test("buildInstallPlan --mcp includes claude desktop when app data exists", () => {
|
|
92
|
+
const paths = resolveInstallPaths(fixture, {});
|
|
93
|
+
mkdirSync(join(home, "Library", "Application Support", "Claude"), { recursive: true });
|
|
94
|
+
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
|
|
95
|
+
expect(plan.some((a) => a.kind === "claude-desktop-mcp")).toBe(process.platform === "darwin");
|
|
96
|
+
expect(plan.some((a) => a.kind === "claude-mcp")).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("buildInstallPlan --mcp skips claude desktop without app data", () => {
|
|
100
|
+
const paths = resolveInstallPaths(fixture, {});
|
|
101
|
+
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
|
|
102
|
+
expect(plan.some((a) => a.kind === "claude-desktop-mcp")).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("buildInstallPlan --mcp includes opencode when config dir exists", () => {
|
|
106
|
+
const paths = resolveInstallPaths(fixture, {});
|
|
107
|
+
mkdirSync(opencodeConfigDir(home), { recursive: true });
|
|
108
|
+
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
|
|
109
|
+
expect(plan.some((a) => a.kind === "opencode-mcp")).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("buildInstallPlan --mcp includes chatgpt when app data exists", () => {
|
|
113
|
+
const paths = resolveInstallPaths(fixture, {});
|
|
114
|
+
if (process.platform === "darwin") {
|
|
115
|
+
mkdirSync(join(home, "Library", "Application Support", "ChatGPT"), { recursive: true });
|
|
116
|
+
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
|
|
117
|
+
expect(plan.some((a) => a.kind === "chatgpt-desktop-mcp")).toBe(true);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
71
120
|
});
|
|
72
121
|
|
|
73
122
|
describe("install status", () => {
|
|
@@ -183,6 +232,6 @@ describe("uninstall plan", () => {
|
|
|
183
232
|
|
|
184
233
|
const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", bin: "1" }));
|
|
185
234
|
expect(plan).toHaveLength(1);
|
|
186
|
-
expect(plan[0]
|
|
235
|
+
expect(plan[0]?.summary.startsWith("binary:")).toBe(true);
|
|
187
236
|
});
|
|
188
237
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { afterEach, beforeEach, 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 { codexMcpHasServer, readCodexMcpEntry, resolveCodexConfigPath } from "./mcp-codex.ts";
|
|
6
|
+
|
|
7
|
+
let home: string;
|
|
8
|
+
let prevHome: string | undefined;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
home = mkdtempSync(join(tmpdir(), "argsbarg-codex-"));
|
|
12
|
+
prevHome = process.env.HOME;
|
|
13
|
+
process.env.HOME = home;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (prevHome === undefined) delete process.env.HOME;
|
|
18
|
+
else process.env.HOME = prevHome;
|
|
19
|
+
rmSync(home, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("codex mcp config", () => {
|
|
23
|
+
test("readCodexMcpEntry parses simple command/args", () => {
|
|
24
|
+
const path = resolveCodexConfigPath(home);
|
|
25
|
+
mkdirSync(join(home, ".codex"), { recursive: true });
|
|
26
|
+
writeFileSync(
|
|
27
|
+
path,
|
|
28
|
+
`[mcp_servers.testapp]
|
|
29
|
+
command = "testapp"
|
|
30
|
+
args = ["mcp"]
|
|
31
|
+
`,
|
|
32
|
+
"utf8",
|
|
33
|
+
);
|
|
34
|
+
expect(readCodexMcpEntry(path, "testapp")).toEqual({
|
|
35
|
+
command: "testapp",
|
|
36
|
+
args: ["mcp"],
|
|
37
|
+
});
|
|
38
|
+
expect(codexMcpHasServer(home, "testapp")).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("readCodexMcpEntry parses transport stdio block", () => {
|
|
42
|
+
const path = resolveCodexConfigPath(home);
|
|
43
|
+
mkdirSync(join(home, ".codex"), { recursive: true });
|
|
44
|
+
writeFileSync(
|
|
45
|
+
path,
|
|
46
|
+
`[mcp_servers.testapp]
|
|
47
|
+
enabled = true
|
|
48
|
+
transport = { type = "stdio", command = "testapp", args = ["mcp"] }
|
|
49
|
+
`,
|
|
50
|
+
"utf8",
|
|
51
|
+
);
|
|
52
|
+
expect(readCodexMcpEntry(path, "testapp")).toEqual({
|
|
53
|
+
command: "testapp",
|
|
54
|
+
args: ["mcp"],
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|