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
package/src/mcp/claude.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import type { CliProgram } from "../types.ts";
|
|
@@ -36,6 +37,7 @@ describe("claude plugin", () => {
|
|
|
36
37
|
test("generatePluginManifest includes userConfig from program.appConfig", () => {
|
|
37
38
|
const manifest = generatePluginManifest(configFixture, "myapp");
|
|
38
39
|
expect(manifest.name).toBe("myapp");
|
|
40
|
+
expect(manifest.mcpServers).toBe(".mcp.json");
|
|
39
41
|
const userConfig = manifest.userConfig as Record<string, Record<string, unknown>>;
|
|
40
42
|
expect(userConfig.api_token?.description).toBe("Token from settings.");
|
|
41
43
|
expect(userConfig.api_token?.sensitive).toBe(true);
|
|
@@ -72,6 +74,17 @@ describe("claude plugin", () => {
|
|
|
72
74
|
expect(zipText).toContain("Server id: `myapp`");
|
|
73
75
|
expect(zipText).not.toContain("skills/myapp/reference.md");
|
|
74
76
|
expect(zipText).not.toContain("Invoke via shell");
|
|
77
|
+
|
|
78
|
+
writeFileSync(join(work, "out.zip"), zip);
|
|
79
|
+
const extract = join(work, "extract");
|
|
80
|
+
mkdirSync(extract, { recursive: true });
|
|
81
|
+
execSync("unzip -o -q ../out.zip", { cwd: extract });
|
|
82
|
+
const binMode = statSync(join(extract, "bin", "myapp")).mode & 0o777;
|
|
83
|
+
expect(binMode & 0o111).not.toBe(0);
|
|
84
|
+
const pluginJson = JSON.parse(
|
|
85
|
+
readFileSync(join(extract, ".claude-plugin", "plugin.json"), "utf8"),
|
|
86
|
+
) as { mcpServers: string };
|
|
87
|
+
expect(pluginJson.mcpServers).toBe(".mcp.json");
|
|
75
88
|
} finally {
|
|
76
89
|
rmSync(work, { recursive: true, force: true });
|
|
77
90
|
}
|
package/src/mcp/claude.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
readdirSync,
|
|
13
13
|
readFileSync,
|
|
14
14
|
rmSync,
|
|
15
|
+
statSync,
|
|
15
16
|
writeFileSync,
|
|
16
17
|
} from "node:fs";
|
|
17
18
|
import { tmpdir } from "node:os";
|
|
@@ -22,7 +23,7 @@ import { applyPluginSkillHint } from "../skill/hint.ts";
|
|
|
22
23
|
import type { CliMcpBundleConfig, CliProgram } from "../types.ts";
|
|
23
24
|
import { defaultMcpBundlePaths, type PackMcpBundleOpts } from "./bundle.ts";
|
|
24
25
|
import { mcpServerId } from "./tools.ts";
|
|
25
|
-
import { zipStore } from "./zip.ts";
|
|
26
|
+
import { type ZipFileEntry, zipStore } from "./zip.ts";
|
|
26
27
|
|
|
27
28
|
const DIST_DIR = "dist";
|
|
28
29
|
const CLAUDE_PLUGIN_DIR = "claude-plugin";
|
|
@@ -67,6 +68,7 @@ export function generatePluginManifest(
|
|
|
67
68
|
version: program.version,
|
|
68
69
|
description: program.description,
|
|
69
70
|
author: defaultAuthor(bundle),
|
|
71
|
+
mcpServers: ".mcp.json",
|
|
70
72
|
};
|
|
71
73
|
const userConfig = buildProgramUserConfig(program);
|
|
72
74
|
if (userConfig) {
|
|
@@ -93,8 +95,8 @@ export function generatePluginMcpJson(
|
|
|
93
95
|
};
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
function collectZipEntries(rootDir: string, dir = rootDir):
|
|
97
|
-
const entries:
|
|
98
|
+
function collectZipEntries(rootDir: string, dir = rootDir): ZipFileEntry[] {
|
|
99
|
+
const entries: ZipFileEntry[] = [];
|
|
98
100
|
for (const ent of readdirSync(dir, { withFileTypes: true }) as Dirent[]) {
|
|
99
101
|
const full = join(dir, ent.name);
|
|
100
102
|
if (ent.isDirectory()) {
|
|
@@ -105,7 +107,12 @@ function collectZipEntries(rootDir: string, dir = rootDir): { name: string; data
|
|
|
105
107
|
continue;
|
|
106
108
|
}
|
|
107
109
|
const rel = relative(rootDir, full).split("\\").join("/");
|
|
108
|
-
|
|
110
|
+
const stMode = statSync(full).mode;
|
|
111
|
+
const entry: ZipFileEntry = { name: rel, data: readFileSync(full) };
|
|
112
|
+
if (stMode & 0o111) {
|
|
113
|
+
entry.unixMode = stMode;
|
|
114
|
+
}
|
|
115
|
+
entries.push(entry);
|
|
109
116
|
}
|
|
110
117
|
return entries;
|
|
111
118
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { chmodSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { applyShellEnv, bootstrapMcpEnv } from "./env.ts";
|
|
6
|
+
|
|
7
|
+
const TEST_VAR = "ARGS_BARG_SHELL_ENV_TEST";
|
|
8
|
+
|
|
9
|
+
describe("mcp/env", () => {
|
|
10
|
+
test("applyShellEnv merges PATH and fills missing vars", () => {
|
|
11
|
+
const prevPath = process.env.PATH;
|
|
12
|
+
const prevTest = process.env[TEST_VAR];
|
|
13
|
+
delete process.env[TEST_VAR];
|
|
14
|
+
try {
|
|
15
|
+
process.env.PATH = "/host/bin";
|
|
16
|
+
applyShellEnv({
|
|
17
|
+
PATH: "/shell/bin:/host/bin",
|
|
18
|
+
[TEST_VAR]: "from-shell",
|
|
19
|
+
});
|
|
20
|
+
expect(process.env.PATH).toBe("/shell/bin:/host/bin");
|
|
21
|
+
expect(process.env[TEST_VAR]).toBe("from-shell");
|
|
22
|
+
} finally {
|
|
23
|
+
if (prevPath === undefined) delete process.env.PATH;
|
|
24
|
+
else process.env.PATH = prevPath;
|
|
25
|
+
if (prevTest === undefined) delete process.env[TEST_VAR];
|
|
26
|
+
else process.env[TEST_VAR] = prevTest;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("applyShellEnv does not overwrite host vars except PATH merge", () => {
|
|
31
|
+
const prev = process.env.HOME;
|
|
32
|
+
process.env.HOME = "/host/home";
|
|
33
|
+
try {
|
|
34
|
+
applyShellEnv({ HOME: "/shell/home" });
|
|
35
|
+
expect(process.env.HOME).toBe("/host/home");
|
|
36
|
+
} finally {
|
|
37
|
+
if (prev === undefined) delete process.env.HOME;
|
|
38
|
+
else process.env.HOME = prev;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("bootstrapMcpEnv", () => {
|
|
44
|
+
let fakeShell: string;
|
|
45
|
+
let prevShell: string | undefined;
|
|
46
|
+
let prevTest: string | undefined;
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
const dir = mkdtempSync(join(tmpdir(), "argsbarg-shell-env-"));
|
|
50
|
+
fakeShell = join(dir, "fake-shell.sh");
|
|
51
|
+
writeFileSync(
|
|
52
|
+
fakeShell,
|
|
53
|
+
`#!/bin/sh
|
|
54
|
+
if [ "$1" = "-l" ] && [ "$2" = "-c" ] && [ "$3" = "env" ]; then
|
|
55
|
+
echo "${TEST_VAR}=from_fake_shell"
|
|
56
|
+
fi
|
|
57
|
+
`,
|
|
58
|
+
);
|
|
59
|
+
chmodSync(fakeShell, 0o755);
|
|
60
|
+
prevShell = process.env.SHELL;
|
|
61
|
+
prevTest = process.env[TEST_VAR];
|
|
62
|
+
delete process.env[TEST_VAR];
|
|
63
|
+
process.env.SHELL = fakeShell;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
if (prevShell === undefined) delete process.env.SHELL;
|
|
68
|
+
else process.env.SHELL = prevShell;
|
|
69
|
+
if (prevTest === undefined) delete process.env[TEST_VAR];
|
|
70
|
+
else process.env[TEST_VAR] = prevTest;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("defaults on when shellEnv is undefined", () => {
|
|
74
|
+
bootstrapMcpEnv({});
|
|
75
|
+
expect(process.env[TEST_VAR]).toBe("from_fake_shell");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("runs when shellEnv is true", () => {
|
|
79
|
+
bootstrapMcpEnv({ shellEnv: true });
|
|
80
|
+
expect(process.env[TEST_VAR]).toBe("from_fake_shell");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("uses explicit shell path when shellEnv is a string", () => {
|
|
84
|
+
bootstrapMcpEnv({ shellEnv: fakeShell });
|
|
85
|
+
expect(process.env[TEST_VAR]).toBe("from_fake_shell");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("skips capture when shellEnv is false", () => {
|
|
89
|
+
bootstrapMcpEnv({ shellEnv: false });
|
|
90
|
+
expect(process.env[TEST_VAR]).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
});
|
package/src/mcp/env.ts
CHANGED
|
@@ -39,21 +39,22 @@ export function applyShellEnv(env: Record<string, string>): void {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/** Applies mcpServer shellEnv bootstrap. */
|
|
42
|
+
/** Applies mcpServer shellEnv bootstrap (default on; opt out with `shellEnv: false`). */
|
|
43
43
|
export function bootstrapMcpEnv(config: { shellEnv?: boolean | string }): void {
|
|
44
|
+
if (config.shellEnv === false) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
44
47
|
const shellEnvCfg = config.shellEnv;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
applyShellEnv(captured);
|
|
57
|
-
}
|
|
48
|
+
const shell =
|
|
49
|
+
typeof shellEnvCfg === "string"
|
|
50
|
+
? shellEnvCfg
|
|
51
|
+
: (process.env.SHELL ?? (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash"));
|
|
52
|
+
const captured = captureShellEnv(shell);
|
|
53
|
+
if (Object.keys(captured).length === 0) {
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
`[argsbarg] shellEnv: failed to capture shell environment from ${shell}\n`,
|
|
56
|
+
);
|
|
57
|
+
} else {
|
|
58
|
+
applyShellEnv(captured);
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { zipStore } from "./zip.ts";
|
|
7
|
+
|
|
8
|
+
test("zipStore preserves unix executable mode on extract", () => {
|
|
9
|
+
const work = mkdtempSync(join(tmpdir(), "zip-exec-"));
|
|
10
|
+
const data = Buffer.from("#!/bin/sh\necho hi\n");
|
|
11
|
+
const zip = zipStore([{ name: "bin/tool", data, unixMode: 0o100755 }]);
|
|
12
|
+
writeFileSync(join(work, "plugin.zip"), zip);
|
|
13
|
+
execSync("unzip -o -q plugin.zip", { cwd: work });
|
|
14
|
+
const mode = statSync(join(work, "bin", "tool")).mode & 0o777;
|
|
15
|
+
expect(mode & 0o111).not.toBe(0);
|
|
16
|
+
expect(readFileSync(join(work, "bin", "tool"), "utf8")).toBe("#!/bin/sh\necho hi\n");
|
|
17
|
+
});
|
package/src/mcp/zip.ts
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
Minimal ZIP writer (store method, no compression) for MCPB and Claude plugin bundles.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/** Info-ZIP Unix UID/GID extra field (`0x7875`) for permission restore on extract. */
|
|
6
|
+
function unixUxExtraField(uid: number, gid: number): Buffer {
|
|
7
|
+
const uidBuf = Buffer.alloc(4);
|
|
8
|
+
uidBuf.writeUInt32LE(uid >>> 0, 0);
|
|
9
|
+
const gidBuf = Buffer.alloc(4);
|
|
10
|
+
gidBuf.writeUInt32LE(gid >>> 0, 0);
|
|
11
|
+
const payload = Buffer.concat([Buffer.from([1, 4]), uidBuf, Buffer.from([4]), gidBuf]);
|
|
12
|
+
const header = Buffer.alloc(4);
|
|
13
|
+
header.writeUInt16LE(0x7875, 0);
|
|
14
|
+
header.writeUInt16LE(payload.length, 2);
|
|
15
|
+
return Buffer.concat([header, payload]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Info-ZIP Unix permission extra field (`0x7855`). */
|
|
19
|
+
function unixUpExtraField(unixMode: number): Buffer {
|
|
20
|
+
const payload = Buffer.alloc(5);
|
|
21
|
+
payload.writeUInt8(1, 0);
|
|
22
|
+
payload.writeUInt32LE(unixMode >>> 0, 1);
|
|
23
|
+
const header = Buffer.alloc(4);
|
|
24
|
+
header.writeUInt16LE(0x7855, 0);
|
|
25
|
+
header.writeUInt16LE(payload.length, 2);
|
|
26
|
+
return Buffer.concat([header, payload]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function unixExtraFields(unixMode: number): Buffer {
|
|
30
|
+
const { uid, gid } = defaultUnixIds();
|
|
31
|
+
return Buffer.concat([unixUpExtraField(unixMode), unixUxExtraField(uid, gid)]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function defaultUnixIds(): { uid: number; gid: number } {
|
|
35
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 501;
|
|
36
|
+
const gid = typeof process.getgid === "function" ? process.getgid() : 20;
|
|
37
|
+
return { uid, gid };
|
|
38
|
+
}
|
|
39
|
+
|
|
5
40
|
/** CRC-32 for ZIP local headers. */
|
|
6
41
|
function crc32(data: Buffer): number {
|
|
7
42
|
let crc = 0xffffffff;
|
|
@@ -18,8 +53,16 @@ function crc32(data: Buffer): number {
|
|
|
18
53
|
return (crc ^ 0xffffffff) >>> 0;
|
|
19
54
|
}
|
|
20
55
|
|
|
56
|
+
/** One file entry for {@link zipStore}. */
|
|
57
|
+
export interface ZipFileEntry {
|
|
58
|
+
name: string;
|
|
59
|
+
data: Buffer;
|
|
60
|
+
/** Unix permission bits (e.g. `0o755`). Encoded in ZIP external attributes when set. */
|
|
61
|
+
unixMode?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
21
64
|
/** Writes a minimal ZIP (store, no compression) with one or more files. */
|
|
22
|
-
export function zipStore(files:
|
|
65
|
+
export function zipStore(files: ZipFileEntry[]): Buffer {
|
|
23
66
|
const parts: Buffer[] = [];
|
|
24
67
|
const central: Buffer[] = [];
|
|
25
68
|
let offset = 0;
|
|
@@ -27,9 +70,11 @@ export function zipStore(files: { name: string; data: Buffer }[]): Buffer {
|
|
|
27
70
|
for (const file of files) {
|
|
28
71
|
const nameBuf = Buffer.from(file.name, "utf8");
|
|
29
72
|
const crc = crc32(file.data);
|
|
30
|
-
const
|
|
73
|
+
const unixMode = file.unixMode;
|
|
74
|
+
const extra = unixMode !== undefined ? unixExtraFields(unixMode) : Buffer.alloc(0);
|
|
75
|
+
const local = Buffer.alloc(30 + nameBuf.length + extra.length);
|
|
31
76
|
local.writeUInt32LE(0x04034b50, 0);
|
|
32
|
-
local.writeUInt16LE(20, 4);
|
|
77
|
+
local.writeUInt16LE(unixMode !== undefined ? 10 : 20, 4);
|
|
33
78
|
local.writeUInt16LE(0, 6);
|
|
34
79
|
local.writeUInt16LE(0, 8);
|
|
35
80
|
local.writeUInt16LE(0, 10);
|
|
@@ -38,13 +83,15 @@ export function zipStore(files: { name: string; data: Buffer }[]): Buffer {
|
|
|
38
83
|
local.writeUInt32LE(file.data.length, 18);
|
|
39
84
|
local.writeUInt32LE(file.data.length, 22);
|
|
40
85
|
local.writeUInt32LE(nameBuf.length, 26);
|
|
41
|
-
local.writeUInt16LE(
|
|
86
|
+
local.writeUInt16LE(extra.length, 28);
|
|
42
87
|
nameBuf.copy(local, 30);
|
|
88
|
+
extra.copy(local, 30 + nameBuf.length);
|
|
43
89
|
|
|
44
|
-
const centralHdr = Buffer.alloc(46 + nameBuf.length);
|
|
90
|
+
const centralHdr = Buffer.alloc(46 + nameBuf.length + extra.length);
|
|
45
91
|
centralHdr.writeUInt32LE(0x02014b50, 0);
|
|
46
|
-
|
|
47
|
-
centralHdr.writeUInt16LE(20,
|
|
92
|
+
// macOS Info-ZIP stores version made by before version needed (non-APPNOTE layout).
|
|
93
|
+
centralHdr.writeUInt16LE(unixMode !== undefined ? 0x031e : 20, 4);
|
|
94
|
+
centralHdr.writeUInt16LE(unixMode !== undefined ? 10 : 20, 6);
|
|
48
95
|
centralHdr.writeUInt16LE(0, 8);
|
|
49
96
|
centralHdr.writeUInt16LE(0, 10);
|
|
50
97
|
centralHdr.writeUInt16LE(0, 12);
|
|
@@ -53,13 +100,19 @@ export function zipStore(files: { name: string; data: Buffer }[]): Buffer {
|
|
|
53
100
|
centralHdr.writeUInt32LE(file.data.length, 20);
|
|
54
101
|
centralHdr.writeUInt32LE(file.data.length, 24);
|
|
55
102
|
centralHdr.writeUInt32LE(nameBuf.length, 28);
|
|
56
|
-
centralHdr.writeUInt16LE(
|
|
103
|
+
centralHdr.writeUInt16LE(extra.length, 30);
|
|
57
104
|
centralHdr.writeUInt16LE(0, 32);
|
|
58
105
|
centralHdr.writeUInt16LE(0, 34);
|
|
59
106
|
centralHdr.writeUInt16LE(0, 36);
|
|
60
|
-
|
|
107
|
+
if (unixMode !== undefined) {
|
|
108
|
+
const externalAttr = (unixMode << 16) >>> 0;
|
|
109
|
+
centralHdr.writeUInt32LE(externalAttr, 38);
|
|
110
|
+
} else {
|
|
111
|
+
centralHdr.writeUInt32LE(0, 38);
|
|
112
|
+
}
|
|
61
113
|
centralHdr.writeUInt32LE(offset, 42);
|
|
62
114
|
nameBuf.copy(centralHdr, 46);
|
|
115
|
+
extra.copy(centralHdr, 46 + nameBuf.length);
|
|
63
116
|
|
|
64
117
|
parts.push(local, file.data);
|
|
65
118
|
central.push(centralHdr);
|
|
@@ -469,7 +469,7 @@ test("MCP ping returns empty result", async () => {
|
|
|
469
469
|
test("minimal.ts mcp without opt-in fails", async () => {
|
|
470
470
|
const { stderr, exitCode } = await $`bun run examples/minimal.ts mcp`.nothrow().quiet();
|
|
471
471
|
expect(exitCode).toBe(1);
|
|
472
|
-
expect(stderr.toString()).toContain("MCP is not
|
|
472
|
+
expect(stderr.toString()).toContain("MCP is not available");
|
|
473
473
|
});
|
|
474
474
|
|
|
475
475
|
test("MCP resources/list includes custom resource", async () => {
|
package/src/parse.test.ts
CHANGED
|
@@ -495,7 +495,7 @@ test("leaf completion help prints correctly", async () => {
|
|
|
495
495
|
const out = stdout.toString();
|
|
496
496
|
expect(exitCode).toBe(0);
|
|
497
497
|
expect(out).toContain("Show help for this command.");
|
|
498
|
-
expect(out).toContain("
|
|
498
|
+
expect(out).toContain("Homebrew");
|
|
499
499
|
expect(stderr.toString()).toBe("");
|
|
500
500
|
});
|
|
501
501
|
|
|
@@ -529,6 +529,7 @@ test("docs schema exports JSON for leaf roots", async () => {
|
|
|
529
529
|
"completion",
|
|
530
530
|
"version",
|
|
531
531
|
"install",
|
|
532
|
+
"uninstall",
|
|
532
533
|
"docs",
|
|
533
534
|
]);
|
|
534
535
|
});
|
|
@@ -614,7 +615,7 @@ test("cliSchemaExport resolves program key in install notes", () => {
|
|
|
614
615
|
|
|
615
616
|
const json = cliSchemaJson(root);
|
|
616
617
|
expect(json).not.toContain("{argsbarg:program}");
|
|
617
|
-
expect(json).toContain("
|
|
618
|
+
expect(json).toContain("brew install");
|
|
618
619
|
});
|
|
619
620
|
|
|
620
621
|
test("cliSchemaExport resolves {argsbarg:program} in consumer notes", () => {
|
|
@@ -1124,6 +1125,17 @@ test("install config on non-root node is rejected", () => {
|
|
|
1124
1125
|
expect(() => cliValidateProgram(root)).toThrow(/install is only supported on the program root/);
|
|
1125
1126
|
});
|
|
1126
1127
|
|
|
1128
|
+
test("install.prefix is rejected", () => {
|
|
1129
|
+
const root = {
|
|
1130
|
+
key: "app",
|
|
1131
|
+
version: "0.0.0",
|
|
1132
|
+
description: "",
|
|
1133
|
+
install: { prefix: "/opt/bin" },
|
|
1134
|
+
handler: () => {},
|
|
1135
|
+
} as unknown as CliProgram;
|
|
1136
|
+
expect(() => cliValidateProgram(root)).toThrow(/install\.prefix removed/);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1127
1139
|
test("generateSkillBundle includes frontmatter and compact command index", () => {
|
|
1128
1140
|
const bundle = generateSkillBundle(nestedMcpFixture, "cursor");
|
|
1129
1141
|
expect(bundle.dirName).toBe("nested_ts");
|
package/src/paths/host.ts
CHANGED
|
@@ -21,20 +21,20 @@ export function expandTilde(path: string): string {
|
|
|
21
21
|
return path;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** Display path using `~/` when under the user home directory. */
|
|
25
|
+
export function displayHomePath(absolutePath: string, home = userHome()): string {
|
|
26
|
+
if (home.length > 0 && absolutePath.startsWith(home)) {
|
|
27
|
+
return `~${absolutePath.slice(home.length)}`;
|
|
28
|
+
}
|
|
29
|
+
return absolutePath;
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
/** XDG config base directory (`$XDG_CONFIG_HOME` or `~/.config`). */
|
|
25
33
|
export function xdgConfigHome(home = userHome()): string {
|
|
26
34
|
return process.env.XDG_CONFIG_HOME ?? join(home, ".config");
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
/**
|
|
30
|
-
export function
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** OS-appropriate base directory for app config files. */
|
|
35
|
-
export function appConfigHome(home = userHome()): string {
|
|
36
|
-
if (process.platform === "win32") {
|
|
37
|
-
return appDataHome(home);
|
|
38
|
-
}
|
|
39
|
-
return xdgConfigHome(home);
|
|
37
|
+
/** App config library root (`~/.local/lib`). */
|
|
38
|
+
export function appConfigLibHome(home = userHome()): string {
|
|
39
|
+
return join(home, ".local", "lib");
|
|
40
40
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { existsSync, readdirSync, rmdirSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
/** Removes a directory when it exists and is empty. Returns true when removed. */
|
|
4
|
+
export function removeEmptyDir(path: string): boolean {
|
|
5
|
+
if (!existsSync(path)) return false;
|
|
6
|
+
try {
|
|
7
|
+
if (readdirSync(path).length > 0) return false;
|
|
8
|
+
rmdirSync(path);
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Shared terminal prompt helpers. */
|
|
2
|
+
|
|
3
|
+
import { readSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
/** Read one line from stdin (no masking). */
|
|
6
|
+
export function readPromptLine(): string {
|
|
7
|
+
const buf = Buffer.alloc(4096);
|
|
8
|
+
const n = readSync(0, buf, { length: 4096 });
|
|
9
|
+
return buf.toString("utf8", 0, n).replace(/\r?\n$/, "");
|
|
10
|
+
}
|
package/src/schema.ts
CHANGED
|
@@ -13,7 +13,15 @@ import {
|
|
|
13
13
|
leafOutputSchema,
|
|
14
14
|
} from "./types.ts";
|
|
15
15
|
|
|
16
|
-
const RESERVED = new Set([
|
|
16
|
+
const RESERVED = new Set([
|
|
17
|
+
"completion",
|
|
18
|
+
"install",
|
|
19
|
+
"uninstall",
|
|
20
|
+
"docs",
|
|
21
|
+
"mcp",
|
|
22
|
+
"version",
|
|
23
|
+
"config",
|
|
24
|
+
]);
|
|
17
25
|
|
|
18
26
|
function exportCommand(cmd: CliNode, root: CliProgram): CliSchemaExport | null {
|
|
19
27
|
if (cmd.hidden) {
|
package/src/skill/generate.ts
CHANGED
|
@@ -13,8 +13,10 @@ import {
|
|
|
13
13
|
} from "../mcp/tools.ts";
|
|
14
14
|
import { collectOptionDefs } from "../parse.ts";
|
|
15
15
|
import { CliOptionKind, type CliProgram } from "../types.ts";
|
|
16
|
+
import type { SkillTarget } from "./naming.ts";
|
|
17
|
+
import { skillDirNameForTarget, skillFrontmatterName } from "./naming.ts";
|
|
16
18
|
|
|
17
|
-
export type SkillTarget
|
|
19
|
+
export type { SkillTarget } from "./naming.ts";
|
|
18
20
|
|
|
19
21
|
export interface SkillBundle {
|
|
20
22
|
dirName: string;
|
|
@@ -103,7 +105,7 @@ function buildConfigurationSection(root: CliProgram): string[] {
|
|
|
103
105
|
|
|
104
106
|
/** Builds SKILL.md body for the given target. */
|
|
105
107
|
function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): string {
|
|
106
|
-
const name =
|
|
108
|
+
const name = skillFrontmatterName(root.key, target);
|
|
107
109
|
const description = skillDescription(root);
|
|
108
110
|
const tools = collectMcpTools(root);
|
|
109
111
|
|
|
@@ -161,7 +163,7 @@ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): s
|
|
|
161
163
|
"Do not install under `~/.cursor/skills-cursor/` (reserved for Cursor built-ins).",
|
|
162
164
|
"",
|
|
163
165
|
);
|
|
164
|
-
} else {
|
|
166
|
+
} else if (target === "claude") {
|
|
165
167
|
lines.push(
|
|
166
168
|
"## Claude Code",
|
|
167
169
|
"",
|
|
@@ -171,6 +173,18 @@ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): s
|
|
|
171
173
|
`- Bundled files in this directory are available via \`\${CLAUDE_SKILL_DIR}\` when the skill runs.`,
|
|
172
174
|
"",
|
|
173
175
|
);
|
|
176
|
+
} else if (target === "codex") {
|
|
177
|
+
lines.push(
|
|
178
|
+
"## Codex",
|
|
179
|
+
"",
|
|
180
|
+
`- Global: \`~/.codex/skills/${dirName}/\``,
|
|
181
|
+
"- Enable skills in `~/.codex/config.toml` if required.",
|
|
182
|
+
"",
|
|
183
|
+
);
|
|
184
|
+
} else if (target === "opencode") {
|
|
185
|
+
lines.push("## OpenCode", "", `- Global: \`~/.config/opencode/skills/${dirName}/\``, "");
|
|
186
|
+
} else {
|
|
187
|
+
lines.push("## OpenClaw", "", `- Global: \`~/.openclaw/skills/${dirName}/\``, "");
|
|
174
188
|
}
|
|
175
189
|
|
|
176
190
|
return lines.join("\n");
|
|
@@ -231,7 +245,7 @@ export function generatePluginSkillBundle(root: CliProgram): PluginSkillBundle {
|
|
|
231
245
|
|
|
232
246
|
/** Generates SKILL.md and reference.md for Cursor or Claude Code. */
|
|
233
247
|
export function generateSkillBundle(root: CliProgram, target: SkillTarget): SkillBundle {
|
|
234
|
-
const dirName =
|
|
248
|
+
const dirName = skillDirNameForTarget(root.key, target);
|
|
235
249
|
return {
|
|
236
250
|
dirName,
|
|
237
251
|
skillMd: buildSkillMd(root, target, dirName),
|
package/src/skill/install.ts
CHANGED
|
@@ -5,19 +5,28 @@ import type { CliProgram } from "../types.ts";
|
|
|
5
5
|
import { generateSkillBundle, type SkillTarget } from "./generate.ts";
|
|
6
6
|
import { applySkillInstallHints } from "./hint.ts";
|
|
7
7
|
|
|
8
|
+
export { skillDirNameForTarget, skillFrontmatterName, skillSlug } from "./naming.ts";
|
|
9
|
+
|
|
8
10
|
export interface SkillInstallOpts {
|
|
9
11
|
global?: boolean;
|
|
10
|
-
/** When true, remove an existing skill directory before writing. */
|
|
11
12
|
rimraf?: boolean;
|
|
12
|
-
/** When true, skip writes but return paths that would change. */
|
|
13
13
|
dry?: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function resolveSkillDir(target: SkillTarget, dirName: string, global: boolean): string {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const home = userHome();
|
|
18
|
+
switch (target) {
|
|
19
|
+
case "cursor":
|
|
20
|
+
return join(global ? home : process.cwd(), ".cursor", "skills", dirName);
|
|
21
|
+
case "claude":
|
|
22
|
+
return join(global ? home : process.cwd(), ".claude", "skills", dirName);
|
|
23
|
+
case "codex":
|
|
24
|
+
return join(global ? home : process.cwd(), ".codex", "skills", dirName);
|
|
25
|
+
case "opencode":
|
|
26
|
+
return join(global ? home : process.cwd(), ".config", "opencode", "skills", dirName);
|
|
27
|
+
case "openclaw":
|
|
28
|
+
return join(global ? home : process.cwd(), ".openclaw", "skills", dirName);
|
|
29
|
+
}
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
/** Writes SKILL.md and reference.md; returns changed file paths. */
|
|
@@ -47,3 +56,21 @@ export function cliSkillInstall(
|
|
|
47
56
|
changed.push(skillPath, refPath);
|
|
48
57
|
return changed;
|
|
49
58
|
}
|
|
59
|
+
|
|
60
|
+
/** Maps install action kind to skill target. */
|
|
61
|
+
export function skillTargetFromActionKind(kind: string): SkillTarget | undefined {
|
|
62
|
+
switch (kind) {
|
|
63
|
+
case "cursor-skill":
|
|
64
|
+
return "cursor";
|
|
65
|
+
case "claude-skill":
|
|
66
|
+
return "claude";
|
|
67
|
+
case "codex-skill":
|
|
68
|
+
return "codex";
|
|
69
|
+
case "opencode-skill":
|
|
70
|
+
return "opencode";
|
|
71
|
+
case "openclaw-skill":
|
|
72
|
+
return "openclaw";
|
|
73
|
+
default:
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Agent skill install targets. */
|
|
2
|
+
export type SkillTarget = "cursor" | "claude" | "codex" | "opencode" | "openclaw";
|
|
3
|
+
|
|
4
|
+
import { sanitizeToolSegment } from "../mcp/tools.ts";
|
|
5
|
+
|
|
6
|
+
/** Kebab-case skill folder + frontmatter name for agents that require hyphen slugs. */
|
|
7
|
+
export function skillSlug(programKey: string): string {
|
|
8
|
+
return programKey
|
|
9
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
10
|
+
.replace(/^-+|-+$/g, "")
|
|
11
|
+
.toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Directory name for a skill target (underscore vs kebab conventions). */
|
|
15
|
+
export function skillDirNameForTarget(programKey: string, target: SkillTarget): string {
|
|
16
|
+
if (target === "cursor" || target === "claude") {
|
|
17
|
+
return sanitizeToolSegment(programKey);
|
|
18
|
+
}
|
|
19
|
+
return skillSlug(programKey);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Frontmatter `name` for SKILL.md by target. */
|
|
23
|
+
export function skillFrontmatterName(programKey: string, target: SkillTarget): string {
|
|
24
|
+
if (target === "cursor" || target === "claude") {
|
|
25
|
+
return sanitizeToolSegment(programKey);
|
|
26
|
+
}
|
|
27
|
+
return skillSlug(programKey);
|
|
28
|
+
}
|