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.
- package/CHANGELOG.md +77 -1
- package/README.md +91 -85
- package/docs/README.md +6 -6
- package/docs/ai-skills.md +8 -5
- package/docs/bundled-docs.md +1 -1
- package/docs/cli-program.md +9 -7
- package/docs/config-schema.md +37 -13
- package/docs/developing.md +8 -8
- package/docs/distribution-homebrew.md +103 -0
- package/docs/install.md +143 -106
- package/docs/mcp.md +23 -12
- package/docs/output-schema.md +1 -1
- package/examples/full-example/Formula/.gitkeep +0 -0
- package/examples/full-example/README.md +98 -0
- package/examples/full-example/biome.json +22 -0
- package/examples/{consumer-app → full-example}/bun.lock +2 -0
- package/examples/full-example/justfile +134 -0
- package/examples/{consumer-app → full-example}/package.json +10 -3
- package/examples/{consumer-app → full-example}/schemas/generated/app-config.json +1 -1
- package/examples/{consumer-app → full-example}/schemas/generated/status.json +1 -1
- package/examples/full-example/scripts/create-identity.ts +11 -0
- package/examples/full-example/scripts/formula-shared.ts +73 -0
- package/examples/full-example/scripts/gen-dev-formula.ts +26 -0
- package/examples/full-example/scripts/print-identity.ts +27 -0
- package/examples/full-example/src/commands/echo/command.ts +21 -0
- package/examples/full-example/src/commands/status/command.test.ts +10 -0
- package/examples/full-example/src/commands/status/command.ts +36 -0
- package/examples/{consumer-app → full-example}/src/commands/status/types.ts +1 -1
- package/examples/full-example/src/index.ts +10 -0
- package/examples/full-example/src/program.ts +57 -0
- package/examples/{consumer-app → full-example}/src/types.ts +1 -1
- package/examples/mcp-test.ts +8 -24
- package/examples/nested.ts +1 -3
- package/index.d.ts +81 -65
- package/package.json +2 -2
- package/src/builtins/builtins.test.ts +37 -22
- package/src/builtins/completion-group.ts +17 -15
- package/src/builtins/config.test.ts +31 -25
- package/src/builtins/config.ts +4 -3
- package/src/builtins/dispatch.ts +25 -1
- package/src/builtins/install.ts +45 -82
- package/src/builtins/mcp.ts +1 -1
- package/src/builtins/registry.ts +2 -0
- package/src/builtins/uninstall.ts +80 -0
- package/src/capabilities.ts +5 -7
- package/src/cli-tool/cli-smoke.test.ts +19 -0
- package/src/cli-tool/create.test.ts +119 -0
- package/src/cli-tool/create.ts +380 -0
- package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +15 -14
- package/src/cli-tool/main.ts +8 -0
- package/src/cli-tool/post-create.ts +111 -0
- package/src/cli-tool/program.ts +82 -0
- package/src/cli-tool/prompt.ts +28 -0
- package/src/cli-tool/run-create.ts +149 -0
- package/src/config/bootstrap.ts +174 -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 +192 -1
- package/src/config/resolve.ts +92 -13
- package/src/config.integration.test.ts +17 -10
- package/src/docs/api-guide.test.ts +4 -5
- package/src/docs/docs.test.ts +2 -1
- package/src/docs/mcp-guide.ts +7 -8
- package/src/hidden-mcpb.test.ts +41 -1
- package/src/index.ts +7 -10
- package/src/install/binary-placement.test.ts +101 -0
- package/src/install/binary-placement.ts +47 -0
- package/src/install/detect-installed.ts +2 -97
- package/src/install/index.ts +239 -168
- package/src/install/install-validate.test.ts +61 -0
- package/src/install/install.test.ts +170 -90
- package/src/install/mcp-openclaw.test.ts +40 -0
- package/src/install/mcp-openclaw.ts +106 -0
- package/src/install/normalize-uninstall.ts +11 -0
- package/src/install/normalize.ts +20 -0
- package/src/install/paths.ts +18 -26
- package/src/install/plan.ts +40 -261
- package/src/install/shell.ts +0 -14
- package/src/install/status.test.ts +85 -0
- package/src/install/status.ts +22 -15
- package/src/install/target-base.ts +93 -0
- package/src/install/target-detect.ts +20 -0
- package/src/install/target-effective.ts +129 -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 +253 -0
- package/src/install/target-skill.ts +104 -0
- package/src/install/target-types.ts +129 -0
- package/src/install/targets/app.ts +60 -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/configure.ts +63 -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 +50 -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 +118 -0
- package/src/install/uninstall.ts +16 -152
- package/src/invoke.test.ts +7 -1
- package/src/mcp/bundle.ts +16 -4
- package/src/mcp/claude.test.ts +14 -1
- package/src/mcp/claude.ts +11 -4
- package/src/mcp/env.test.ts +92 -0
- package/src/mcp/env.ts +15 -14
- package/src/mcp/zip.test.ts +17 -0
- package/src/mcp/zip.ts +62 -9
- package/src/mcp.integration.test.ts +1 -1
- package/src/parse.test.ts +14 -2
- package/src/paths/host.ts +11 -11
- package/src/paths/remove-empty-dir.ts +13 -0
- package/src/prompt.ts +10 -0
- package/src/schema.ts +9 -1
- package/src/skill/generate.ts +18 -4
- package/src/skill/install.ts +33 -6
- package/src/skill/naming.ts +28 -0
- package/src/types.ts +86 -7
- package/src/validate.ts +73 -9
- package/docs/templates/cursor/rules/cli-program.mdc +0 -31
- package/examples/config-app/main.ts +0 -20
- package/examples/config-app/program.ts +0 -81
- package/examples/config-app/schema.ts +0 -37
- package/examples/config-app/types.ts +0 -19
- package/examples/consumer-app/README.md +0 -57
- package/examples/consumer-app/src/main.ts +0 -15
- package/examples/consumer-app/src/program.ts +0 -108
- package/src/install/binary.ts +0 -94
- package/src/install/completions.ts +0 -56
- package/src/install/update.test.ts +0 -108
- package/src/install/update.ts +0 -57
- /package/examples/{consumer-app → full-example}/schemas/configSchemas.ts +0 -0
- /package/examples/{consumer-app → full-example}/schemas/outputSchemas.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/naming.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen.ts +0 -0
- /package/examples/{consumer-app → full-example}/tsconfig.json +0 -0
|
@@ -3,16 +3,17 @@ App config bootstrap and MCP config enforcement regressions.
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { expect, test } from "bun:test";
|
|
6
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
|
-
import { join } from "node:path";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
9
|
import { bootstrapAppConfig } from "./config/bootstrap.ts";
|
|
10
|
+
import { resolveAppConfigPath } from "./config/file.ts";
|
|
10
11
|
import { mcpRequest, testProgram } from "./test-fixtures.ts";
|
|
11
12
|
|
|
12
13
|
test("bootstrapAppConfig prefers host env over config file", () => {
|
|
13
14
|
const dir = mkdtempSync(join(tmpdir(), "argsbarg-env-"));
|
|
14
|
-
const
|
|
15
|
-
|
|
15
|
+
const prevHome = process.env.HOME;
|
|
16
|
+
process.env.HOME = dir;
|
|
16
17
|
process.env.FOO = "original";
|
|
17
18
|
try {
|
|
18
19
|
const p = testProgram({
|
|
@@ -20,7 +21,6 @@ test("bootstrapAppConfig prefers host env over config file", () => {
|
|
|
20
21
|
version: "0",
|
|
21
22
|
description: "",
|
|
22
23
|
appConfig: {
|
|
23
|
-
path: configFile,
|
|
24
24
|
entries: {
|
|
25
25
|
foo: { description: "x", env: "FOO" },
|
|
26
26
|
bar: { description: "y", env: "BAR" },
|
|
@@ -28,10 +28,15 @@ test("bootstrapAppConfig prefers host env over config file", () => {
|
|
|
28
28
|
},
|
|
29
29
|
handler: () => {},
|
|
30
30
|
});
|
|
31
|
+
const configFile = resolveAppConfigPath(p);
|
|
32
|
+
mkdirSync(dirname(configFile), { recursive: true });
|
|
33
|
+
writeFileSync(configFile, `${JSON.stringify({ foo: "fromfile", bar: "bar" })}\n`, "utf8");
|
|
31
34
|
bootstrapAppConfig(p, { validateFile: true });
|
|
32
35
|
expect(process.env.FOO).toBe("original");
|
|
33
36
|
expect(process.env.BAR).toBe("bar");
|
|
34
37
|
} finally {
|
|
38
|
+
if (prevHome === undefined) delete process.env.HOME;
|
|
39
|
+
else process.env.HOME = prevHome;
|
|
35
40
|
delete process.env.FOO;
|
|
36
41
|
delete process.env.BAR;
|
|
37
42
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -74,7 +79,8 @@ test("MCP program.appConfig succeeds when env present", async () => {
|
|
|
74
79
|
|
|
75
80
|
test("MCP config file loads and exports vars for tool handlers", async () => {
|
|
76
81
|
const dir = mkdtempSync(join(tmpdir(), "argsbarg-mcp-"));
|
|
77
|
-
const configFile = join(dir, "config");
|
|
82
|
+
const configFile = join(dir, ".local", "lib", "mcp_test", "config");
|
|
83
|
+
mkdirSync(dirname(configFile), { recursive: true });
|
|
78
84
|
writeFileSync(
|
|
79
85
|
configFile,
|
|
80
86
|
`${JSON.stringify({ argsTestSecret: "file-value" }, null, 2)}\n`,
|
|
@@ -91,17 +97,19 @@ test("MCP config file loads and exports vars for tool handlers", async () => {
|
|
|
91
97
|
],
|
|
92
98
|
{
|
|
93
99
|
script: "examples/mcp-test.ts",
|
|
94
|
-
env: {
|
|
100
|
+
env: { HOME: dir, ARGS_TEST_SECRET: "present" },
|
|
95
101
|
},
|
|
96
102
|
);
|
|
97
103
|
const res = responses.get(15) as { result: { isError: boolean; content: { text: string }[] } };
|
|
98
104
|
expect(res.result.isError).toBe(false);
|
|
99
105
|
expect(res.result.content[0]?.text.trim()).toBe("present");
|
|
106
|
+
rmSync(dir, { recursive: true, force: true });
|
|
100
107
|
});
|
|
101
108
|
|
|
102
109
|
test("Cli.run docs api skips required appConfig exit", async () => {
|
|
103
110
|
const dir = mkdtempSync(join(tmpdir(), "argsbarg-docs-skip-"));
|
|
104
|
-
const configFile = join(dir, "config");
|
|
111
|
+
const configFile = join(dir, ".local", "lib", "docs_skip_test", "config");
|
|
112
|
+
mkdirSync(dirname(configFile), { recursive: true });
|
|
105
113
|
writeFileSync(configFile, "{}\n");
|
|
106
114
|
const entry = join(import.meta.dir, "index.ts");
|
|
107
115
|
const mainPath = join(dir, "run-docs.ts");
|
|
@@ -114,7 +122,6 @@ const program = {
|
|
|
114
122
|
description: "test",
|
|
115
123
|
docs: { enabled: true, topics: { readme: { text: "# readme\\n" } } },
|
|
116
124
|
appConfig: {
|
|
117
|
-
path: ${JSON.stringify(configFile)},
|
|
118
125
|
entries: { token: { description: "Token.", env: "DOCS_SKIP_RUN_TOKEN" } },
|
|
119
126
|
},
|
|
120
127
|
handler: () => {},
|
|
@@ -122,7 +129,7 @@ const program = {
|
|
|
122
129
|
await new Cli(program).run(process.argv.slice(2));
|
|
123
130
|
`,
|
|
124
131
|
);
|
|
125
|
-
const env = { ...process.env }
|
|
132
|
+
const env = { ...process.env, HOME: dir } as Record<string, string | undefined>;
|
|
126
133
|
delete env.DOCS_SKIP_RUN_TOKEN;
|
|
127
134
|
try {
|
|
128
135
|
const proc = Bun.spawn(["bun", "run", mainPath, "docs", "api"], {
|
|
@@ -71,21 +71,20 @@ test("generateApiGuide resolves program key in install notes", () => {
|
|
|
71
71
|
};
|
|
72
72
|
const md = generateApiGuide(fixture);
|
|
73
73
|
expect(md).not.toContain("{argsbarg:program}");
|
|
74
|
-
expect(md).toContain("
|
|
74
|
+
expect(md).toContain("brew install");
|
|
75
75
|
expect(md).not.toContain("Upgrade to latest release");
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
test("generateApiGuide
|
|
78
|
+
test("generateApiGuide mentions Homebrew upgrade", () => {
|
|
79
79
|
const fixture: CliProgram = {
|
|
80
80
|
key: "myapp",
|
|
81
81
|
version: "1.0.0",
|
|
82
82
|
description: "Demo app.",
|
|
83
|
-
install: { updateGetLatest: async () => ({ path: process.execPath }) },
|
|
84
83
|
commands: [{ key: "run", description: "Run.", handler: () => {} }],
|
|
85
84
|
};
|
|
86
85
|
const md = generateApiGuide(fixture);
|
|
87
|
-
expect(md).toContain("
|
|
88
|
-
expect(md).toContain("
|
|
86
|
+
expect(md).toContain("brew upgrade");
|
|
87
|
+
expect(md).not.toContain("install --update");
|
|
89
88
|
});
|
|
90
89
|
|
|
91
90
|
test("generateApiGuide resolves {argsbarg:program} in consumer notes", () => {
|
package/src/docs/docs.test.ts
CHANGED
|
@@ -217,7 +217,8 @@ test("generateMcpGuide includes schema URI and install targets", () => {
|
|
|
217
217
|
expect(guide).toContain("claude_desktop_config.json");
|
|
218
218
|
expect(guide).toContain("## Installation");
|
|
219
219
|
expect(guide).toContain("## Running directly");
|
|
220
|
-
expect(guide).toContain("install --
|
|
220
|
+
expect(guide).toContain("install --mcp");
|
|
221
|
+
expect(guide).toContain("brew install myapp");
|
|
221
222
|
expect(guide).toContain("OpenAI Codex");
|
|
222
223
|
expect(guide).toContain("ChatGPT");
|
|
223
224
|
});
|
package/src/docs/mcp-guide.ts
CHANGED
|
@@ -111,7 +111,7 @@ export function generateMcpGuide(root: CliProgram): string {
|
|
|
111
111
|
|
|
112
112
|
if (caps.install) {
|
|
113
113
|
lines.push(
|
|
114
|
-
`Install the CLI first so \`${root.key}\` is on your PATH (e.g.
|
|
114
|
+
`Install the CLI first so \`${root.key}\` is on your PATH (e.g. \`brew install ${root.key}\`). Host configs reference the app by name.`,
|
|
115
115
|
"",
|
|
116
116
|
);
|
|
117
117
|
} else {
|
|
@@ -167,13 +167,12 @@ export function generateMcpGuide(root: CliProgram): string {
|
|
|
167
167
|
"",
|
|
168
168
|
);
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
170
|
+
lines.push(
|
|
171
|
+
"## Environment",
|
|
172
|
+
"",
|
|
173
|
+
"- **`shellEnv`** — on by default; captures login-shell environment at MCP startup (PATH, toolchain shims, exports). Opt out with `shellEnv: false`.",
|
|
174
|
+
"",
|
|
175
|
+
);
|
|
177
176
|
|
|
178
177
|
if (root.appConfig?.entries && Object.keys(root.appConfig.entries).length > 0) {
|
|
179
178
|
lines.push("## Configuration", "");
|
package/src/hidden-mcpb.test.ts
CHANGED
|
@@ -19,7 +19,7 @@ const hiddenFixture: CliProgram = {
|
|
|
19
19
|
key: "myapp",
|
|
20
20
|
version: "1.0.0",
|
|
21
21
|
description: "Hidden demo.",
|
|
22
|
-
mcpServer: { enabled: true },
|
|
22
|
+
mcpServer: { enabled: true, mcpd: true, claudePlugin: true },
|
|
23
23
|
commands: [
|
|
24
24
|
{
|
|
25
25
|
key: "public",
|
|
@@ -187,4 +187,44 @@ describe("mcp bundle", () => {
|
|
|
187
187
|
rmSync(work, { recursive: true, force: true });
|
|
188
188
|
}
|
|
189
189
|
});
|
|
190
|
+
|
|
191
|
+
test("runMcpBundle with claudePlugin only prints plugin path", () => {
|
|
192
|
+
const work = mkdtempSync(join(tmpdir(), "mcpb-run-"));
|
|
193
|
+
const stdout: string[] = [];
|
|
194
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
195
|
+
process.stdout.write = ((chunk: string | Uint8Array) => {
|
|
196
|
+
stdout.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
197
|
+
return true;
|
|
198
|
+
}) as typeof process.stdout.write;
|
|
199
|
+
const fixture: CliProgram = {
|
|
200
|
+
...hiddenFixture,
|
|
201
|
+
mcpServer: { enabled: true, claudePlugin: true },
|
|
202
|
+
};
|
|
203
|
+
try {
|
|
204
|
+
const dist = join(work, "dist");
|
|
205
|
+
mkdirSync(dist, { recursive: true });
|
|
206
|
+
writeFileSync(join(dist, "myapp"), "#!/bin/sh\n", { mode: 0o755 });
|
|
207
|
+
const prevCwd = process.cwd();
|
|
208
|
+
process.chdir(work);
|
|
209
|
+
try {
|
|
210
|
+
runMcpBundle(fixture);
|
|
211
|
+
} finally {
|
|
212
|
+
process.chdir(prevCwd);
|
|
213
|
+
}
|
|
214
|
+
const lines = stdout.join("").trim().split("\n");
|
|
215
|
+
expect(lines).toHaveLength(1);
|
|
216
|
+
expect(lines[0]).toContain("claude-plugin");
|
|
217
|
+
} finally {
|
|
218
|
+
process.stdout.write = orig;
|
|
219
|
+
rmSync(work, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("runMcpBundle errors when no bundle flags enabled", () => {
|
|
224
|
+
const fixture: CliProgram = {
|
|
225
|
+
...hiddenFixture,
|
|
226
|
+
mcpServer: { enabled: true },
|
|
227
|
+
};
|
|
228
|
+
expect(() => runMcpBundle(fixture)).toThrow(/mcpd/);
|
|
229
|
+
});
|
|
190
230
|
});
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ module layout.
|
|
|
9
9
|
|
|
10
10
|
export { Cli, type CliInvokeKind, type CliInvokeResult } from "./cli.ts";
|
|
11
11
|
export { cliErrWithHelp } from "./cli-errors.ts";
|
|
12
|
+
export { displayAppConfigPath, resolveAppConfigPath } from "./config/file.ts";
|
|
12
13
|
export type { CliLeafInputs } from "./context.ts";
|
|
13
14
|
export { CliContext } from "./context.ts";
|
|
14
15
|
export {
|
|
@@ -26,23 +27,18 @@ export {
|
|
|
26
27
|
shouldRunHeadlessWithYes,
|
|
27
28
|
wantsExplicitJson,
|
|
28
29
|
} from "./headless.ts";
|
|
29
|
-
export type { GhReleaseUpdateConfig, GhVersionCheckConfig } from "./install/gh-release-update.ts";
|
|
30
|
-
export {
|
|
31
|
-
createGhFetchLatest,
|
|
32
|
-
createGhVersionCheck,
|
|
33
|
-
ghReleaseUpdateGetLatest,
|
|
34
|
-
isAlreadyCurrent,
|
|
35
|
-
parseReleaseTag,
|
|
36
|
-
} from "./install/gh-release-update.ts";
|
|
37
30
|
export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
|
|
38
31
|
export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
|
|
39
32
|
export type {
|
|
40
33
|
CliAppConfig,
|
|
41
34
|
CliAppConfigEntry,
|
|
35
|
+
CliAppConfigResolveContext,
|
|
36
|
+
CliAppConfigResolveFn,
|
|
42
37
|
CliDocsConfig,
|
|
43
38
|
CliDocsTopic,
|
|
44
39
|
CliHandler,
|
|
45
40
|
CliInstallConfig,
|
|
41
|
+
CliInstallTargets,
|
|
46
42
|
CliInvocation,
|
|
47
43
|
CliMcpBundleConfig,
|
|
48
44
|
CliMcpResource,
|
|
@@ -51,8 +47,9 @@ export type {
|
|
|
51
47
|
CliOption,
|
|
52
48
|
CliPositional,
|
|
53
49
|
CliProgram,
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
InstallAgentIntegration,
|
|
51
|
+
InstallTargetSpec,
|
|
52
|
+
ResolvedInstallTarget,
|
|
56
53
|
} from "./types.ts";
|
|
57
54
|
export {
|
|
58
55
|
CliFallbackMode,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, 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 {
|
|
7
|
+
isAppInstalled,
|
|
8
|
+
isExternallyManagedBinary,
|
|
9
|
+
resolvePathCommand,
|
|
10
|
+
} from "./binary-placement.ts";
|
|
11
|
+
|
|
12
|
+
const program: CliProgram = {
|
|
13
|
+
key: "placementapp",
|
|
14
|
+
version: "1.0.0",
|
|
15
|
+
description: "x",
|
|
16
|
+
handler: () => {},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let tmp: string;
|
|
20
|
+
let prevPath: string | undefined;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
tmp = mkdtempSync(join(tmpdir(), "argsbarg-placement-"));
|
|
24
|
+
prevPath = process.env.PATH;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
if (prevPath === undefined) delete process.env.PATH;
|
|
29
|
+
else process.env.PATH = prevPath;
|
|
30
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("isExternallyManagedBinary", () => {
|
|
34
|
+
test("false when command is not on PATH", () => {
|
|
35
|
+
process.env.PATH = tmp;
|
|
36
|
+
expect(isExternallyManagedBinary("placementapp-not-on-path")).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("true when PATH resolves to execPath", () => {
|
|
40
|
+
const bin = join(tmp, "placementapp");
|
|
41
|
+
writeFileSync(bin, "#!/bin/sh\n", "utf8");
|
|
42
|
+
chmodSync(bin, 0o755);
|
|
43
|
+
process.env.PATH = tmp;
|
|
44
|
+
expect(isExternallyManagedBinary("placementapp", bin)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("true when PATH entry is a symlink to execPath", () => {
|
|
48
|
+
const bin = join(tmp, "placementapp");
|
|
49
|
+
const target = join(tmp, "real-binary");
|
|
50
|
+
writeFileSync(target, "fake", "utf8");
|
|
51
|
+
symlinkSync(target, bin);
|
|
52
|
+
process.env.PATH = tmp;
|
|
53
|
+
expect(isExternallyManagedBinary("placementapp", target)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("false when PATH points at a different binary", () => {
|
|
57
|
+
const bin = join(tmp, "placementapp");
|
|
58
|
+
const other = join(tmp, "other");
|
|
59
|
+
writeFileSync(bin, "a", "utf8");
|
|
60
|
+
writeFileSync(other, "b", "utf8");
|
|
61
|
+
process.env.PATH = tmp;
|
|
62
|
+
expect(isExternallyManagedBinary("placementapp", other)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("isAppInstalled", () => {
|
|
67
|
+
test("true when externally managed", () => {
|
|
68
|
+
const bin = join(tmp, program.key);
|
|
69
|
+
const prevExec = process.execPath;
|
|
70
|
+
writeFileSync(bin, "x", "utf8");
|
|
71
|
+
chmodSync(bin, 0o755);
|
|
72
|
+
process.env.PATH = tmp;
|
|
73
|
+
process.execPath = bin;
|
|
74
|
+
try {
|
|
75
|
+
expect(isAppInstalled(program)).toBe(true);
|
|
76
|
+
} finally {
|
|
77
|
+
process.execPath = prevExec;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("false when not on PATH and no local copy", () => {
|
|
82
|
+
const home = mkdtempSync(join(tmpdir(), "argsbarg-placement-home-"));
|
|
83
|
+
const prevHome = process.env.HOME;
|
|
84
|
+
process.env.HOME = home;
|
|
85
|
+
process.env.PATH = tmp;
|
|
86
|
+
try {
|
|
87
|
+
expect(isAppInstalled(program)).toBe(false);
|
|
88
|
+
} finally {
|
|
89
|
+
if (prevHome === undefined) delete process.env.HOME;
|
|
90
|
+
else process.env.HOME = prevHome;
|
|
91
|
+
rmSync(home, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("resolvePathCommand", () => {
|
|
97
|
+
test("returns undefined when missing", () => {
|
|
98
|
+
process.env.PATH = tmp;
|
|
99
|
+
expect(resolvePathCommand("missing-cmd")).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { accessSync, constants, realpathSync } from "node:fs";
|
|
2
|
+
import { delimiter, join } from "node:path";
|
|
3
|
+
import type { CliProgram } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
/** Resolves a command name on PATH to its real path, if present. */
|
|
6
|
+
export function resolvePathCommand(key: string): string | undefined {
|
|
7
|
+
const pathEnv = process.env.PATH ?? "";
|
|
8
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
9
|
+
if (!dir) continue;
|
|
10
|
+
const candidate = join(dir, key);
|
|
11
|
+
try {
|
|
12
|
+
accessSync(candidate, constants.F_OK);
|
|
13
|
+
return realpathSync(candidate);
|
|
14
|
+
} catch {}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const found = Bun.which(key);
|
|
18
|
+
if (found === null) return undefined;
|
|
19
|
+
try {
|
|
20
|
+
return realpathSync(found);
|
|
21
|
+
} catch {
|
|
22
|
+
return found;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function realpathOrSelf(path: string): string {
|
|
27
|
+
try {
|
|
28
|
+
return realpathSync(path);
|
|
29
|
+
} catch {
|
|
30
|
+
return path;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
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 {
|
|
39
|
+
const resolved = resolvePathCommand(key);
|
|
40
|
+
if (!resolved) return false;
|
|
41
|
+
return resolved === realpathOrSelf(execPath);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** True when the app binary is available on PATH (e.g. Homebrew). */
|
|
45
|
+
export function isAppInstalled(program: CliProgram): boolean {
|
|
46
|
+
return isExternallyManagedBinary(program.key);
|
|
47
|
+
}
|
|
@@ -1,97 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { detectOpenCodeMcpConfigPath } from "./mcp-opencode.ts";
|
|
4
|
-
import { type InstallPaths, userHome } from "./paths.ts";
|
|
5
|
-
|
|
6
|
-
export interface InstalledArtifacts {
|
|
7
|
-
binary: boolean;
|
|
8
|
-
bashCompletion: boolean;
|
|
9
|
-
zshCompletion: boolean;
|
|
10
|
-
fishCompletion: boolean;
|
|
11
|
-
cursorSkill: boolean;
|
|
12
|
-
claudeSkill: boolean;
|
|
13
|
-
cursorMcp: boolean;
|
|
14
|
-
claudeMcp: boolean;
|
|
15
|
-
claudeDesktopMcp: boolean;
|
|
16
|
-
opencodeMcp: boolean;
|
|
17
|
-
codexMcp: boolean;
|
|
18
|
-
chatGptMcp: boolean;
|
|
19
|
-
bashRcPath: boolean;
|
|
20
|
-
zshRcFpath: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function mcpConfigHasServer(path: string, name: string): boolean {
|
|
24
|
-
if (!existsSync(path)) return false;
|
|
25
|
-
try {
|
|
26
|
-
const data = JSON.parse(readFileSync(path, "utf8")) as { mcpServers?: Record<string, unknown> };
|
|
27
|
-
return data.mcpServers?.[name] !== undefined;
|
|
28
|
-
} catch {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Detects which install artifacts are currently present. */
|
|
34
|
-
export function detectInstalledArtifacts(paths: InstallPaths): InstalledArtifacts {
|
|
35
|
-
return {
|
|
36
|
-
binary: existsSync(paths.binaryPath),
|
|
37
|
-
bashCompletion: existsSync(paths.bashCompletion),
|
|
38
|
-
zshCompletion: existsSync(paths.zshCompletion),
|
|
39
|
-
fishCompletion: existsSync(paths.fishCompletion),
|
|
40
|
-
cursorSkill: existsSync(paths.cursorSkillDir),
|
|
41
|
-
claudeSkill: existsSync(paths.claudeSkillDir),
|
|
42
|
-
cursorMcp: mcpConfigHasServer(paths.cursorMcpPath, paths.mcpName),
|
|
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),
|
|
48
|
-
bashRcPath: false,
|
|
49
|
-
zshRcFpath: false,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface InstallStatus {
|
|
54
|
-
binary?: string;
|
|
55
|
-
bashCompletion?: string;
|
|
56
|
-
zshCompletion?: string;
|
|
57
|
-
fishCompletion?: string;
|
|
58
|
-
cursorSkill?: string;
|
|
59
|
-
claudeSkill?: string;
|
|
60
|
-
cursorMcp?: string;
|
|
61
|
-
claudeMcp?: string;
|
|
62
|
-
claudeDesktopMcp?: string;
|
|
63
|
-
opencodeMcp?: string;
|
|
64
|
-
codexMcp?: string;
|
|
65
|
-
chatGptMcp?: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Builds a status inventory from detected artifacts. */
|
|
69
|
-
export function buildInstallStatus(
|
|
70
|
-
paths: InstallPaths,
|
|
71
|
-
detected: InstalledArtifacts,
|
|
72
|
-
): InstallStatus {
|
|
73
|
-
const status: InstallStatus = {};
|
|
74
|
-
if (detected.binary) status.binary = paths.binaryPath;
|
|
75
|
-
if (detected.bashCompletion) status.bashCompletion = paths.bashCompletion;
|
|
76
|
-
if (detected.zshCompletion) status.zshCompletion = paths.zshCompletion;
|
|
77
|
-
if (detected.fishCompletion) status.fishCompletion = paths.fishCompletion;
|
|
78
|
-
if (detected.cursorSkill) status.cursorSkill = `${paths.cursorSkillDir}/`;
|
|
79
|
-
if (detected.claudeSkill) status.claudeSkill = `${paths.claudeSkillDir}/`;
|
|
80
|
-
if (detected.cursorMcp) status.cursorMcp = `${paths.cursorMcpPath} (server "${paths.mcpName}")`;
|
|
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
|
-
}
|
|
96
|
-
return status;
|
|
97
|
-
}
|
|
1
|
+
export { buildInstallStatus, detectInstalledArtifacts } from "./target-detect.ts";
|
|
2
|
+
export type { InstalledArtifacts, InstallStatus } from "./target-types.ts";
|