argsbarg 7.0.11 → 7.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 +35 -1
- package/README.md +3 -2
- package/docs/ai-skills.md +2 -2
- package/docs/cli-program.md +3 -5
- package/docs/developing.md +3 -3
- package/docs/mcp.md +21 -8
- package/docs/output-schema.md +1 -1
- package/examples/full-example/AGENTS.md +20 -12
- package/examples/full-example/README.md +57 -12
- package/examples/full-example/biome.json +5 -4
- package/examples/full-example/bunfig.toml +3 -0
- package/examples/full-example/justfile +22 -27
- package/examples/full-example-json/AGENTS.md +14 -12
- package/examples/full-example-json/README.md +63 -15
- package/examples/full-example-json/biome.json +1 -3
- package/examples/full-example-json/bunfig.toml +3 -0
- package/examples/full-example-json/justfile +22 -27
- package/examples/mcp-plugin/.claude-plugin/marketplace.json +13 -0
- package/examples/mcp-plugin/.claude-plugin/plugin.json +10 -0
- package/examples/mcp-plugin/.cursor/hooks/run-tests-on-stop.ts +56 -0
- package/examples/mcp-plugin/.cursor/hooks.json +12 -0
- package/examples/mcp-plugin/.cursor-plugin/plugin.json +11 -0
- package/examples/mcp-plugin/.mcp.json +6 -0
- package/examples/mcp-plugin/AGENTS.md +90 -0
- package/examples/mcp-plugin/CLAUDE.md +1 -0
- package/examples/mcp-plugin/README.md +72 -0
- package/examples/mcp-plugin/biome.json +20 -0
- package/examples/mcp-plugin/bun.lock +48 -0
- package/examples/mcp-plugin/bunfig.toml +3 -0
- package/examples/mcp-plugin/docs/README.md +27 -0
- package/examples/mcp-plugin/docs/cli-schema.json +2085 -0
- package/examples/mcp-plugin/docs/cli.md +2026 -0
- package/examples/mcp-plugin/docs/http.md +92 -0
- package/examples/mcp-plugin/docs/mcp.md +116 -0
- package/examples/mcp-plugin/docs/openapi.json +1243 -0
- package/examples/mcp-plugin/justfile +89 -0
- package/examples/mcp-plugin/mcp.json +8 -0
- package/examples/mcp-plugin/package.json +23 -0
- package/examples/mcp-plugin/scripts/create-identity.ts +12 -0
- package/examples/mcp-plugin/scripts/mcp.mjs +11106 -0
- package/examples/mcp-plugin/scripts/release.ts +225 -0
- package/examples/mcp-plugin/skills/mcp-plugin/SKILL.md +59 -0
- package/examples/mcp-plugin/src/commands/echo/command.ts +26 -0
- package/examples/mcp-plugin/src/commands/render-json/__generated__/RenderJsonInputSchema.json +15 -0
- package/examples/mcp-plugin/src/commands/render-json/__generated__/index.ts +5 -0
- package/examples/mcp-plugin/src/commands/render-json/command.test.ts +46 -0
- package/examples/mcp-plugin/src/commands/render-json/command.ts +30 -0
- package/examples/mcp-plugin/src/commands/render-json/types.ts +9 -0
- package/examples/mcp-plugin/src/commands/status/__generated__/StatusJsonOutputSchema.json +15 -0
- package/examples/mcp-plugin/src/commands/status/__generated__/index.ts +5 -0
- package/examples/mcp-plugin/src/commands/status/command.test.ts +10 -0
- package/examples/mcp-plugin/src/commands/status/command.ts +28 -0
- package/examples/mcp-plugin/src/commands/status/types.ts +6 -0
- package/examples/mcp-plugin/src/commands/workspaces/__generated__/WorkspaceNameInputSchema.json +15 -0
- package/examples/mcp-plugin/src/commands/workspaces/__generated__/index.ts +5 -0
- package/examples/mcp-plugin/src/commands/workspaces/command.test.ts +58 -0
- package/examples/mcp-plugin/src/commands/workspaces/command.ts +94 -0
- package/examples/mcp-plugin/src/commands/workspaces/types.ts +6 -0
- package/examples/mcp-plugin/src/db/index.test.ts +86 -0
- package/examples/mcp-plugin/src/db/index.ts +77 -0
- package/examples/mcp-plugin/src/db/tables/workspaces.ts +58 -0
- package/examples/mcp-plugin/src/index.ts +10 -0
- package/examples/mcp-plugin/src/program.ts +32 -0
- package/examples/mcp-plugin/src/types/argsbarg.d.ts +11 -0
- package/examples/mcp-plugin/src/types/md.d.ts +4 -0
- package/examples/mcp-plugin/tsconfig.json +17 -0
- package/index.d.ts +12 -0
- package/package.json +1 -1
- package/src/builtins/mcp.ts +1 -1
- package/src/cli-tool/create.test.ts +5 -0
- package/src/cli-tool/create.ts +9 -1
- package/src/cli-tool/program.ts +1 -1
- package/src/config/manifest.ts +56 -0
- package/src/config/resolve.test.ts +12 -12
- package/src/configure/configure.test.ts +5 -3
- package/src/core/parse.test.ts +6 -5
- package/src/core/types.ts +12 -0
- package/src/exports/mcp.ts +12 -0
- package/src/headless/tool-call.test.ts +32 -0
- package/src/headless/tool-call.ts +19 -8
- package/src/mcp/bundle.ts +15 -5
- package/src/mcp/claude.test.ts +2 -7
- package/src/mcp/claude.ts +52 -68
- package/src/mcp/cursor.test.ts +151 -0
- package/src/mcp/cursor.ts +157 -0
- package/src/mcp/hidden-mcpb.test.ts +34 -1
- package/src/mcp/plugin-shared.ts +107 -0
- package/src/mcp/result.ts +5 -1
- package/src/mcp/server.ts +3 -2
- package/src/test/fixtures.ts +13 -0
- package/src/test/integration/mcp.test.ts +8 -7
- package/examples/full-example/scripts/print-identity.ts +0 -28
- package/examples/full-example-json/scripts/print-identity.ts +0 -28
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* Unit tests for shared headless error text (MCP and HTTP). */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import { type HeadlessToolCallFailure, headlessFailureMcpMessage, headlessFailureToHttpResponse } from "./tool-call.ts";
|
|
5
|
+
|
|
6
|
+
/** Builds a failed headless result with the given error message. */
|
|
7
|
+
function invokeFailure(
|
|
8
|
+
/** Full error text from the leaf. */
|
|
9
|
+
message: string,
|
|
10
|
+
): HeadlessToolCallFailure {
|
|
11
|
+
return {
|
|
12
|
+
exitCode: 1,
|
|
13
|
+
kind: "invoke",
|
|
14
|
+
message,
|
|
15
|
+
ok: false,
|
|
16
|
+
stderr: "",
|
|
17
|
+
stdout: "",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("headless error text", () => {
|
|
22
|
+
const multiline =
|
|
23
|
+
'cannot copy tab "Spec" losslessly (1 smart chip(s): "Ada"). Use force: true.\n' +
|
|
24
|
+
' • node h.x: contains 1 smart chip(s) ("Ada")\n\n' +
|
|
25
|
+
"Google Docs REST API has no native tab duplication endpoint.";
|
|
26
|
+
|
|
27
|
+
test("MCP and HTTP both keep the full multi-line error", async () => {
|
|
28
|
+
expect(headlessFailureMcpMessage(invokeFailure(multiline))).toBe(multiline);
|
|
29
|
+
const body = (await headlessFailureToHttpResponse(invokeFailure(multiline)).json()) as { error: string };
|
|
30
|
+
expect(body.error).toBe(multiline);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -6,7 +6,7 @@ import { bootstrapAppConfig } from "../config/bootstrap.ts";
|
|
|
6
6
|
import { formatMcpMissingConfigMessage, missingRequiredConfig } from "../config/resolve.ts";
|
|
7
7
|
import type { CliInvocation, CliProgram, InvokeFailureKind } from "../core/types.ts";
|
|
8
8
|
import { failureKindHttpStatus } from "../hooks/run.ts";
|
|
9
|
-
import { apiErrorResponse, apiSuccessResponse,
|
|
9
|
+
import { apiErrorResponse, apiSuccessResponse, stripAnsi } from "../http/result.ts";
|
|
10
10
|
import { type HttpRouteDef, httpRequestToArgv } from "../http/routes.ts";
|
|
11
11
|
import { obscureUnexpectedClientMessage } from "../log/emitter.ts";
|
|
12
12
|
import { buildToolCallSuccessFromResponse } from "../mcp/result.ts";
|
|
@@ -188,11 +188,7 @@ export function headlessSuccessToHttpResponse(
|
|
|
188
188
|
/** Maps a headless failure result to a JSON HTTP error Response. */
|
|
189
189
|
export function headlessFailureToHttpResponse(result: HeadlessToolCallFailure, obscureUnexpected = false): Response {
|
|
190
190
|
const status = resolveHttpErrorStatus(result);
|
|
191
|
-
|
|
192
|
-
if (obscureUnexpected && result.failureKind === "unexpected") {
|
|
193
|
-
message = obscureUnexpectedClientMessage();
|
|
194
|
-
}
|
|
195
|
-
return apiErrorResponse(status, { error: message });
|
|
191
|
+
return apiErrorResponse(status, { error: formatHeadlessError(result, obscureUnexpected) });
|
|
196
192
|
}
|
|
197
193
|
|
|
198
194
|
function resolveHttpErrorStatus(result: HeadlessToolCallFailure): number {
|
|
@@ -212,11 +208,26 @@ function resolveHttpErrorStatus(result: HeadlessToolCallFailure): number {
|
|
|
212
208
|
}
|
|
213
209
|
|
|
214
210
|
/** Maps invoke failure kind to MCP tools/call error text (respects obscureUnexpected). */
|
|
215
|
-
export function headlessFailureMcpMessage(
|
|
211
|
+
export function headlessFailureMcpMessage(
|
|
212
|
+
/** Failed headless tool invocation. */
|
|
213
|
+
result: HeadlessToolCallFailure,
|
|
214
|
+
/** When true, unexpected failures return a generic client message. */
|
|
215
|
+
obscureUnexpected = false,
|
|
216
|
+
): string {
|
|
217
|
+
return formatHeadlessError(result, obscureUnexpected);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Formats a headless failure for MCP text content and HTTP JSON `error` (full message, ANSI stripped). */
|
|
221
|
+
function formatHeadlessError(
|
|
222
|
+
/** Failed headless tool invocation. */
|
|
223
|
+
result: HeadlessToolCallFailure,
|
|
224
|
+
/** When true, unexpected failures return a generic client message. */
|
|
225
|
+
obscureUnexpected: boolean,
|
|
226
|
+
): string {
|
|
216
227
|
if (obscureUnexpected && result.failureKind === "unexpected") {
|
|
217
228
|
return obscureUnexpectedClientMessage();
|
|
218
229
|
}
|
|
219
|
-
return
|
|
230
|
+
return stripAnsi(result.message).trim();
|
|
220
231
|
}
|
|
221
232
|
|
|
222
233
|
/** Missing-config lookup failures as MCP/HTTP pre-invoke errors. */
|
package/src/mcp/bundle.ts
CHANGED
|
@@ -3,12 +3,13 @@ Packs a CLI program into an MCP Bundle (`.mcpb`) for Claude Desktop.
|
|
|
3
3
|
Expects `dist/<program.key>` as the compiled binary input.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { basename, join, resolve } from "node:path";
|
|
9
9
|
import { buildProgramUserConfig } from "../config/manifest.ts";
|
|
10
10
|
import type { CliMcpBundleConfig, CliProgram } from "../core/types.ts";
|
|
11
11
|
import { packClaudePlugin } from "./claude.ts";
|
|
12
|
+
import { packCursorPlugin } from "./cursor.ts";
|
|
12
13
|
import { collectMcpTools, mcpServerId } from "./tools.ts";
|
|
13
14
|
import { zipStore } from "./zip.ts";
|
|
14
15
|
|
|
@@ -118,7 +119,8 @@ export function packMcpBundle(program: CliProgram, opts: PackMcpBundleOpts = {})
|
|
|
118
119
|
const staging = mkdtempSync(join(tmpdir(), "mcpb-"));
|
|
119
120
|
try {
|
|
120
121
|
const stagedBinary = join(staging, binaryName);
|
|
121
|
-
cpSync(binaryPath, stagedBinary
|
|
122
|
+
cpSync(binaryPath, stagedBinary);
|
|
123
|
+
chmodSync(stagedBinary, 0o755);
|
|
122
124
|
|
|
123
125
|
const manifest = generateMcpManifest(program, binaryName);
|
|
124
126
|
const files: { name: string; data: Buffer }[] = [
|
|
@@ -148,10 +150,15 @@ export function packMcpBundle(program: CliProgram, opts: PackMcpBundleOpts = {})
|
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
/** Runs `mcp bundle`: writes enabled dist artifacts; prints one path per line. */
|
|
151
|
-
export function runMcpBundle(
|
|
153
|
+
export function runMcpBundle(
|
|
154
|
+
/** CLI program schema. */
|
|
155
|
+
program: CliProgram,
|
|
156
|
+
): void {
|
|
152
157
|
const mcp = program.mcpServer;
|
|
153
|
-
if (!mcp?.mcpd && !mcp?.claudePlugin) {
|
|
154
|
-
throw new Error(
|
|
158
|
+
if (!mcp?.mcpd && !mcp?.claudePlugin && !mcp?.cursorPlugin) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"mcp bundle: enable mcpServer.mcpd, mcpServer.claudePlugin, and/or mcpServer.cursorPlugin on the program root.",
|
|
161
|
+
);
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
const lines: string[] = [];
|
|
@@ -161,5 +168,8 @@ export function runMcpBundle(program: CliProgram): void {
|
|
|
161
168
|
if (mcp.claudePlugin) {
|
|
162
169
|
lines.push(packClaudePlugin(program));
|
|
163
170
|
}
|
|
171
|
+
if (mcp.cursorPlugin) {
|
|
172
|
+
lines.push(packCursorPlugin(program));
|
|
173
|
+
}
|
|
164
174
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
165
175
|
}
|
package/src/mcp/claude.test.ts
CHANGED
|
@@ -8,13 +8,8 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync }
|
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import type { CliProgram } from "../core/types.ts";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
generatePluginManifest,
|
|
14
|
-
generatePluginMcpJson,
|
|
15
|
-
packClaudePlugin,
|
|
16
|
-
pluginName,
|
|
17
|
-
} from "./claude.ts";
|
|
11
|
+
import { defaultClaudePluginPaths, generatePluginManifest, generatePluginMcpJson, packClaudePlugin } from "./claude.ts";
|
|
12
|
+
import { pluginName } from "./plugin-shared.ts";
|
|
18
13
|
|
|
19
14
|
const configFixture: CliProgram = {
|
|
20
15
|
key: "myapp",
|
package/src/mcp/claude.ts
CHANGED
|
@@ -3,50 +3,29 @@ Packs a Claude Code plugin zip from a compiled CLI binary.
|
|
|
3
3
|
Internal module — not exported from index.ts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
cpSync,
|
|
8
|
-
type Dirent,
|
|
9
|
-
existsSync,
|
|
10
|
-
mkdirSync,
|
|
11
|
-
mkdtempSync,
|
|
12
|
-
readdirSync,
|
|
13
|
-
readFileSync,
|
|
14
|
-
rmSync,
|
|
15
|
-
statSync,
|
|
16
|
-
writeFileSync,
|
|
17
|
-
} from "node:fs";
|
|
6
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
18
7
|
import { tmpdir } from "node:os";
|
|
19
|
-
import { basename, join,
|
|
8
|
+
import { basename, join, resolve } from "node:path";
|
|
20
9
|
import { buildPluginMcpEnvMapping, buildProgramUserConfig } from "../config/manifest.ts";
|
|
21
|
-
import type {
|
|
22
|
-
import { generatePluginSkillBundle } from "../skill/generate.ts";
|
|
23
|
-
import { applyPluginSkillHint } from "../skill/hint.ts";
|
|
10
|
+
import type { CliProgram } from "../core/types.ts";
|
|
24
11
|
import { defaultMcpBundlePaths, type PackMcpBundleOpts } from "./bundle.ts";
|
|
12
|
+
import { collectZipEntries, defaultAuthor, pluginName, stagePluginSkills } from "./plugin-shared.ts";
|
|
25
13
|
import { mcpServerId } from "./tools.ts";
|
|
26
|
-
import {
|
|
14
|
+
import { zipStore } from "./zip.ts";
|
|
27
15
|
|
|
16
|
+
/** Base distribution output directory relative to workspace root. */
|
|
28
17
|
const DIST_DIR = "dist";
|
|
29
|
-
const CLAUDE_PLUGIN_DIR = "claude-plugin";
|
|
30
|
-
|
|
31
|
-
/** Kebab-case plugin name for plugin.json (Claude Code requires kebab-case). */
|
|
32
|
-
export function pluginName(program: CliProgram): string {
|
|
33
|
-
return program.key
|
|
34
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
35
|
-
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
36
|
-
.replace(/^-+|-+$/g, "")
|
|
37
|
-
.toLowerCase();
|
|
38
|
-
}
|
|
39
18
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
email?: string;
|
|
43
|
-
url?: string;
|
|
44
|
-
} {
|
|
45
|
-
return bundle?.author ?? { name: "Unknown" };
|
|
46
|
-
}
|
|
19
|
+
/** Output subdirectory name under dist for Claude Code plugin archives. */
|
|
20
|
+
const CLAUDE_PLUGIN_DIR = "claude-plugin";
|
|
47
21
|
|
|
48
22
|
/** Default plugin zip output under cwd. */
|
|
49
|
-
export function defaultClaudePluginPaths(
|
|
23
|
+
export function defaultClaudePluginPaths(
|
|
24
|
+
/** CLI program schema. */
|
|
25
|
+
program: CliProgram,
|
|
26
|
+
/** Working directory (defaults to cwd). */
|
|
27
|
+
cwd = process.cwd(),
|
|
28
|
+
) {
|
|
50
29
|
const binaryName = program.key;
|
|
51
30
|
const dist = join(cwd, DIST_DIR);
|
|
52
31
|
const name = pluginName(program);
|
|
@@ -58,7 +37,12 @@ export function defaultClaudePluginPaths(program: CliProgram, cwd = process.cwd(
|
|
|
58
37
|
}
|
|
59
38
|
|
|
60
39
|
/** Generates `.claude-plugin/plugin.json` object. */
|
|
61
|
-
export function generatePluginManifest(
|
|
40
|
+
export function generatePluginManifest(
|
|
41
|
+
/** CLI program schema. */
|
|
42
|
+
program: CliProgram,
|
|
43
|
+
/** Staged executable name. */
|
|
44
|
+
_binaryName: string,
|
|
45
|
+
): Record<string, unknown> {
|
|
62
46
|
const bundle = program.mcpServer?.bundle;
|
|
63
47
|
const manifest: Record<string, unknown> = {
|
|
64
48
|
name: pluginName(program),
|
|
@@ -75,7 +59,12 @@ export function generatePluginManifest(program: CliProgram, _binaryName: string)
|
|
|
75
59
|
}
|
|
76
60
|
|
|
77
61
|
/** Generates plugin `.mcp.json` stdio server config. */
|
|
78
|
-
export function generatePluginMcpJson(
|
|
62
|
+
export function generatePluginMcpJson(
|
|
63
|
+
/** CLI program schema. */
|
|
64
|
+
program: CliProgram,
|
|
65
|
+
/** Staged executable name. */
|
|
66
|
+
binaryName: string,
|
|
67
|
+
): Record<string, unknown> {
|
|
79
68
|
const mcp: Record<string, unknown> = {
|
|
80
69
|
command: `\${CLAUDE_PLUGIN_ROOT}/bin/${binaryName}`,
|
|
81
70
|
args: ["mcp"],
|
|
@@ -89,35 +78,23 @@ export function generatePluginMcpJson(program: CliProgram, binaryName: string):
|
|
|
89
78
|
};
|
|
90
79
|
}
|
|
91
80
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
entry.unixMode = stMode;
|
|
108
|
-
}
|
|
109
|
-
entries.push(entry);
|
|
110
|
-
}
|
|
111
|
-
return entries;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function writePluginTree(pluginRoot: string, program: CliProgram, binaryPath: string, binaryName: string): void {
|
|
115
|
-
const bundle = generatePluginSkillBundle(program);
|
|
116
|
-
const skillMd = applyPluginSkillHint(program, bundle.skillMd);
|
|
117
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Stages directory tree for Claude Code plugin: manifest, .mcp.json, binary, and skills.
|
|
83
|
+
*/
|
|
84
|
+
function writePluginTree(
|
|
85
|
+
/** Staging root directory. */
|
|
86
|
+
pluginRoot: string,
|
|
87
|
+
/** CLI program schema. */
|
|
88
|
+
program: CliProgram,
|
|
89
|
+
/** Absolute path to compiled binary. */
|
|
90
|
+
binaryPath: string,
|
|
91
|
+
/** Executable filename. */
|
|
92
|
+
binaryName: string,
|
|
93
|
+
/** Working directory containing repository sources. */
|
|
94
|
+
cwd: string,
|
|
95
|
+
): void {
|
|
118
96
|
mkdirSync(join(pluginRoot, ".claude-plugin"), { recursive: true });
|
|
119
97
|
mkdirSync(join(pluginRoot, "bin"), { recursive: true });
|
|
120
|
-
mkdirSync(join(pluginRoot, "skills", bundle.dirName), { recursive: true });
|
|
121
98
|
|
|
122
99
|
writeFileSync(
|
|
123
100
|
join(pluginRoot, ".claude-plugin", "plugin.json"),
|
|
@@ -127,15 +104,22 @@ function writePluginTree(pluginRoot: string, program: CliProgram, binaryPath: st
|
|
|
127
104
|
join(pluginRoot, ".mcp.json"),
|
|
128
105
|
`${JSON.stringify(generatePluginMcpJson(program, binaryName), null, 2)}\n`,
|
|
129
106
|
);
|
|
130
|
-
|
|
131
|
-
|
|
107
|
+
const stagedBinary = join(pluginRoot, "bin", binaryName);
|
|
108
|
+
cpSync(binaryPath, stagedBinary);
|
|
109
|
+
chmodSync(stagedBinary, 0o755);
|
|
110
|
+
stagePluginSkills(pluginRoot, program, cwd);
|
|
132
111
|
}
|
|
133
112
|
|
|
134
113
|
/**
|
|
135
114
|
* Writes `dist/claude-plugin/<name>.zip` with manifests, binary, and skill bundle.
|
|
136
115
|
* Requires the compiled binary to exist.
|
|
137
116
|
*/
|
|
138
|
-
export function packClaudePlugin(
|
|
117
|
+
export function packClaudePlugin(
|
|
118
|
+
/** CLI program schema. */
|
|
119
|
+
program: CliProgram,
|
|
120
|
+
/** Packaging options. */
|
|
121
|
+
opts: PackMcpBundleOpts = {},
|
|
122
|
+
): string {
|
|
139
123
|
const cwd = opts.cwd ?? process.cwd();
|
|
140
124
|
const defaults = defaultClaudePluginPaths(program, cwd);
|
|
141
125
|
const mcpDefaults = defaultMcpBundlePaths(program, cwd);
|
|
@@ -149,7 +133,7 @@ export function packClaudePlugin(program: CliProgram, opts: PackMcpBundleOpts =
|
|
|
149
133
|
|
|
150
134
|
const staging = mkdtempSync(join(tmpdir(), "claude-plugin-"));
|
|
151
135
|
try {
|
|
152
|
-
writePluginTree(staging, program, binaryPath, binaryName);
|
|
136
|
+
writePluginTree(staging, program, binaryPath, binaryName, cwd);
|
|
153
137
|
const zip = zipStore(collectZipEntries(staging));
|
|
154
138
|
mkdirSync(join(pluginZipPath, ".."), { recursive: true });
|
|
155
139
|
writeFileSync(pluginZipPath, zip);
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tests for mcp/cursor module behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import type { CliProgram } from "../core/types.ts";
|
|
11
|
+
import {
|
|
12
|
+
defaultCursorPluginPaths,
|
|
13
|
+
generateCursorPluginManifest,
|
|
14
|
+
generateCursorPluginMcpJson,
|
|
15
|
+
packCursorPlugin,
|
|
16
|
+
} from "./cursor.ts";
|
|
17
|
+
import { pluginName } from "./plugin-shared.ts";
|
|
18
|
+
|
|
19
|
+
const configFixture: CliProgram = {
|
|
20
|
+
key: "myapp",
|
|
21
|
+
version: "1.0.0",
|
|
22
|
+
description: "Demo.",
|
|
23
|
+
mcpServer: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
bundle: {
|
|
26
|
+
author: { name: "Author", email: "author@example.com" },
|
|
27
|
+
displayName: "My Application",
|
|
28
|
+
homepage: "https://example.com",
|
|
29
|
+
license: "MIT",
|
|
30
|
+
repository: "https://github.com/example/myapp",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
appConfig: {
|
|
34
|
+
entries: {
|
|
35
|
+
apiToken: {
|
|
36
|
+
description: "Token from settings.",
|
|
37
|
+
env: "API_TOKEN",
|
|
38
|
+
required: true,
|
|
39
|
+
sensitive: true,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
commands: [{ key: "run", description: "Run.", handler: () => {} }],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Tests for cursor plugin. */
|
|
47
|
+
describe("cursor plugin", () => {
|
|
48
|
+
test("pluginName is kebab-case", () => {
|
|
49
|
+
expect(pluginName({ ...configFixture, key: "MyApp" })).toBe("my-app");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("generateCursorPluginManifest includes bundle metadata and variables", () => {
|
|
53
|
+
const manifest = generateCursorPluginManifest(configFixture, "myapp");
|
|
54
|
+
expect(manifest.name).toBe("myapp");
|
|
55
|
+
expect(manifest.displayName).toBe("My Application");
|
|
56
|
+
expect(manifest.homepage).toBe("https://example.com");
|
|
57
|
+
expect(manifest.repository).toBe("https://github.com/example/myapp");
|
|
58
|
+
expect(manifest.license).toBe("MIT");
|
|
59
|
+
const variables = manifest.variables as {
|
|
60
|
+
type: string;
|
|
61
|
+
properties: Record<string, { type: string; description: string }>;
|
|
62
|
+
required: string[];
|
|
63
|
+
};
|
|
64
|
+
expect(variables.type).toBe("object");
|
|
65
|
+
expect(variables.properties.API_TOKEN?.description).toBe("Token from settings.");
|
|
66
|
+
expect(variables.required).toContain("API_TOKEN");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("generateCursorPluginMcpJson uses CURSOR_PLUGIN_ROOT and mcpServers wrapper", () => {
|
|
70
|
+
const json = generateCursorPluginMcpJson(configFixture, "myapp");
|
|
71
|
+
const servers = json.mcpServers as Record<string, { command: string; args: string[]; env: Record<string, string> }>;
|
|
72
|
+
const entry = servers.myapp;
|
|
73
|
+
expect(entry).toBeDefined();
|
|
74
|
+
expect(entry?.command).toBe("${CURSOR_PLUGIN_ROOT}/bin/myapp");
|
|
75
|
+
expect(entry?.args).toEqual(["mcp"]);
|
|
76
|
+
expect(entry?.env?.API_TOKEN).toBe("${API_TOKEN}");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("defaultCursorPluginPaths", () => {
|
|
80
|
+
const cwd = "/tmp/work";
|
|
81
|
+
const paths = defaultCursorPluginPaths(configFixture, cwd);
|
|
82
|
+
expect(paths.pluginZipPath).toBe(join(cwd, "dist", "cursor-plugin", "myapp.zip"));
|
|
83
|
+
expect(paths.binaryPath).toBe(join(cwd, "dist", "myapp"));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("packCursorPlugin writes zip with manifests, binary, and generated skill", () => {
|
|
87
|
+
const work = mkdtempSync(join(tmpdir(), "cursor-plugin-test-"));
|
|
88
|
+
try {
|
|
89
|
+
const dist = join(work, "dist");
|
|
90
|
+
mkdirSync(dist, { recursive: true });
|
|
91
|
+
const binaryPath = join(dist, "myapp");
|
|
92
|
+
writeFileSync(binaryPath, "#!/bin/sh\n", { mode: 0o755 });
|
|
93
|
+
const paths = defaultCursorPluginPaths(configFixture, work);
|
|
94
|
+
packCursorPlugin(configFixture, { cwd: work, binaryPath });
|
|
95
|
+
const zip = readFileSync(paths.pluginZipPath);
|
|
96
|
+
expect(zip.length).toBeGreaterThan(0);
|
|
97
|
+
const zipText = zip.toString("utf8");
|
|
98
|
+
expect(zipText).toContain("skills/myapp/SKILL.md");
|
|
99
|
+
expect(zipText).toContain("MCP toolset");
|
|
100
|
+
|
|
101
|
+
writeFileSync(join(work, "out.zip"), zip);
|
|
102
|
+
const extract = join(work, "extract");
|
|
103
|
+
mkdirSync(extract, { recursive: true });
|
|
104
|
+
execSync("unzip -o -q ../out.zip", { cwd: extract });
|
|
105
|
+
const binMode = statSync(join(extract, "bin", "myapp")).mode & 0o777;
|
|
106
|
+
expect(binMode & 0o111).not.toBe(0);
|
|
107
|
+
|
|
108
|
+
const pluginJson = JSON.parse(readFileSync(join(extract, ".cursor-plugin", "plugin.json"), "utf8")) as {
|
|
109
|
+
name: string;
|
|
110
|
+
};
|
|
111
|
+
expect(pluginJson.name).toBe("myapp");
|
|
112
|
+
|
|
113
|
+
const mcpJson = JSON.parse(readFileSync(join(extract, "mcp.json"), "utf8")) as {
|
|
114
|
+
mcpServers: Record<string, { command: string }>;
|
|
115
|
+
};
|
|
116
|
+
expect(mcpJson.mcpServers.myapp?.command).toBe("${CURSOR_PLUGIN_ROOT}/bin/myapp");
|
|
117
|
+
} finally {
|
|
118
|
+
rmSync(work, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("packCursorPlugin preserves repository skills when present in cwd", () => {
|
|
123
|
+
const work = mkdtempSync(join(tmpdir(), "cursor-plugin-skill-"));
|
|
124
|
+
try {
|
|
125
|
+
const dist = join(work, "dist");
|
|
126
|
+
mkdirSync(dist, { recursive: true });
|
|
127
|
+
const binaryPath = join(dist, "myapp");
|
|
128
|
+
writeFileSync(binaryPath, "#!/bin/sh\n", { mode: 0o755 });
|
|
129
|
+
|
|
130
|
+
const repoSkillDir = join(work, "skills", "myapp");
|
|
131
|
+
mkdirSync(repoSkillDir, { recursive: true });
|
|
132
|
+
writeFileSync(join(repoSkillDir, "SKILL.md"), "# Custom Hand-Crafted Skill\nSpecial instructions here.");
|
|
133
|
+
writeFileSync(join(repoSkillDir, "reference.md"), "# Extra Reference Material");
|
|
134
|
+
|
|
135
|
+
const paths = defaultCursorPluginPaths(configFixture, work);
|
|
136
|
+
packCursorPlugin(configFixture, { cwd: work, binaryPath });
|
|
137
|
+
|
|
138
|
+
writeFileSync(join(work, "out.zip"), readFileSync(paths.pluginZipPath));
|
|
139
|
+
const extract = join(work, "extract");
|
|
140
|
+
mkdirSync(extract, { recursive: true });
|
|
141
|
+
execSync("unzip -o -q ../out.zip", { cwd: extract });
|
|
142
|
+
|
|
143
|
+
const skillContent = readFileSync(join(extract, "skills", "myapp", "SKILL.md"), "utf8");
|
|
144
|
+
expect(skillContent).toContain("Custom Hand-Crafted Skill");
|
|
145
|
+
const refContent = readFileSync(join(extract, "skills", "myapp", "reference.md"), "utf8");
|
|
146
|
+
expect(refContent).toContain("Extra Reference Material");
|
|
147
|
+
} finally {
|
|
148
|
+
rmSync(work, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Packs a Cursor plugin zip from a compiled CLI binary.
|
|
3
|
+
Internal module — not exported from index.ts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { basename, join, resolve } from "node:path";
|
|
9
|
+
import { buildCursorPluginMcpEnvMapping, buildCursorPluginVariables } from "../config/manifest.ts";
|
|
10
|
+
import type { CliProgram } from "../core/types.ts";
|
|
11
|
+
import { defaultMcpBundlePaths, type PackMcpBundleOpts } from "./bundle.ts";
|
|
12
|
+
import { collectZipEntries, defaultAuthor, pluginName, stagePluginSkills } from "./plugin-shared.ts";
|
|
13
|
+
import { mcpServerId } from "./tools.ts";
|
|
14
|
+
import { zipStore } from "./zip.ts";
|
|
15
|
+
|
|
16
|
+
/** Base distribution output directory relative to workspace root. */
|
|
17
|
+
const DIST_DIR = "dist";
|
|
18
|
+
|
|
19
|
+
/** Output subdirectory name under dist for Cursor plugin archives. */
|
|
20
|
+
const CURSOR_PLUGIN_DIR = "cursor-plugin";
|
|
21
|
+
|
|
22
|
+
/** Default Cursor plugin zip output under cwd. */
|
|
23
|
+
export function defaultCursorPluginPaths(
|
|
24
|
+
/** CLI program schema. */
|
|
25
|
+
program: CliProgram,
|
|
26
|
+
/** Working directory (defaults to cwd). */
|
|
27
|
+
cwd = process.cwd(),
|
|
28
|
+
) {
|
|
29
|
+
const binaryName = program.key;
|
|
30
|
+
const dist = join(cwd, DIST_DIR);
|
|
31
|
+
const name = pluginName(program);
|
|
32
|
+
return {
|
|
33
|
+
pluginZipPath: join(dist, CURSOR_PLUGIN_DIR, `${name}.zip`),
|
|
34
|
+
binaryPath: join(dist, binaryName),
|
|
35
|
+
binaryName,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Generates `.cursor-plugin/plugin.json` object. */
|
|
40
|
+
export function generateCursorPluginManifest(
|
|
41
|
+
/** CLI program schema. */
|
|
42
|
+
program: CliProgram,
|
|
43
|
+
/** Staged executable name. */
|
|
44
|
+
_binaryName: string,
|
|
45
|
+
): Record<string, unknown> {
|
|
46
|
+
const bundle = program.mcpServer?.bundle;
|
|
47
|
+
const manifest: Record<string, unknown> = {
|
|
48
|
+
name: pluginName(program),
|
|
49
|
+
version: program.version,
|
|
50
|
+
description: program.description,
|
|
51
|
+
author: defaultAuthor(bundle),
|
|
52
|
+
};
|
|
53
|
+
if (bundle?.displayName) {
|
|
54
|
+
manifest.displayName = bundle.displayName;
|
|
55
|
+
}
|
|
56
|
+
if (bundle?.homepage) {
|
|
57
|
+
manifest.homepage = bundle.homepage;
|
|
58
|
+
}
|
|
59
|
+
if (bundle?.repository) {
|
|
60
|
+
manifest.repository = bundle.repository;
|
|
61
|
+
}
|
|
62
|
+
if (bundle?.license) {
|
|
63
|
+
manifest.license = bundle.license;
|
|
64
|
+
}
|
|
65
|
+
const variables = buildCursorPluginVariables(program);
|
|
66
|
+
if (variables) {
|
|
67
|
+
manifest.variables = variables;
|
|
68
|
+
}
|
|
69
|
+
return manifest;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Generates plugin `mcp.json` stdio server config for Cursor. */
|
|
73
|
+
export function generateCursorPluginMcpJson(
|
|
74
|
+
/** CLI program schema. */
|
|
75
|
+
program: CliProgram,
|
|
76
|
+
/** Staged executable name. */
|
|
77
|
+
binaryName: string,
|
|
78
|
+
): Record<string, unknown> {
|
|
79
|
+
const mcp: Record<string, unknown> = {
|
|
80
|
+
command: `\${CURSOR_PLUGIN_ROOT}/bin/${binaryName}`,
|
|
81
|
+
args: ["mcp"],
|
|
82
|
+
};
|
|
83
|
+
const env = buildCursorPluginMcpEnvMapping(program);
|
|
84
|
+
if (env) {
|
|
85
|
+
mcp.env = env;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
mcpServers: {
|
|
89
|
+
[mcpServerId(program)]: mcp,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Stages directory tree for Cursor plugin: manifest, mcp.json, binary, and skills.
|
|
96
|
+
*/
|
|
97
|
+
function writePluginTree(
|
|
98
|
+
/** Staging root directory. */
|
|
99
|
+
pluginRoot: string,
|
|
100
|
+
/** CLI program schema. */
|
|
101
|
+
program: CliProgram,
|
|
102
|
+
/** Absolute path to compiled binary. */
|
|
103
|
+
binaryPath: string,
|
|
104
|
+
/** Executable filename. */
|
|
105
|
+
binaryName: string,
|
|
106
|
+
/** Working directory containing repository sources. */
|
|
107
|
+
cwd: string,
|
|
108
|
+
): void {
|
|
109
|
+
mkdirSync(join(pluginRoot, ".cursor-plugin"), { recursive: true });
|
|
110
|
+
mkdirSync(join(pluginRoot, "bin"), { recursive: true });
|
|
111
|
+
|
|
112
|
+
writeFileSync(
|
|
113
|
+
join(pluginRoot, ".cursor-plugin", "plugin.json"),
|
|
114
|
+
`${JSON.stringify(generateCursorPluginManifest(program, binaryName), null, 2)}\n`,
|
|
115
|
+
);
|
|
116
|
+
writeFileSync(
|
|
117
|
+
join(pluginRoot, "mcp.json"),
|
|
118
|
+
`${JSON.stringify(generateCursorPluginMcpJson(program, binaryName), null, 2)}\n`,
|
|
119
|
+
);
|
|
120
|
+
const stagedBinary = join(pluginRoot, "bin", binaryName);
|
|
121
|
+
cpSync(binaryPath, stagedBinary);
|
|
122
|
+
chmodSync(stagedBinary, 0o755);
|
|
123
|
+
stagePluginSkills(pluginRoot, program, cwd);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Writes `dist/cursor-plugin/<name>.zip` with manifests, binary, and skill bundle.
|
|
128
|
+
* Requires the compiled binary to exist.
|
|
129
|
+
*/
|
|
130
|
+
export function packCursorPlugin(
|
|
131
|
+
/** CLI program schema. */
|
|
132
|
+
program: CliProgram,
|
|
133
|
+
/** Packaging options. */
|
|
134
|
+
opts: PackMcpBundleOpts = {},
|
|
135
|
+
): string {
|
|
136
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
137
|
+
const defaults = defaultCursorPluginPaths(program, cwd);
|
|
138
|
+
const mcpDefaults = defaultMcpBundlePaths(program, cwd);
|
|
139
|
+
const binaryPath = resolve(cwd, opts.binaryPath ?? mcpDefaults.binaryPath);
|
|
140
|
+
const pluginZipPath = resolve(cwd, defaults.pluginZipPath);
|
|
141
|
+
const binaryName = basename(binaryPath);
|
|
142
|
+
|
|
143
|
+
if (!existsSync(binaryPath)) {
|
|
144
|
+
throw new Error(`Binary not found: ${binaryPath}. Build with compile first (expected dist/${program.key}).`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const staging = mkdtempSync(join(tmpdir(), "cursor-plugin-"));
|
|
148
|
+
try {
|
|
149
|
+
writePluginTree(staging, program, binaryPath, binaryName, cwd);
|
|
150
|
+
const zip = zipStore(collectZipEntries(staging));
|
|
151
|
+
mkdirSync(join(pluginZipPath, ".."), { recursive: true });
|
|
152
|
+
writeFileSync(pluginZipPath, zip);
|
|
153
|
+
return pluginZipPath;
|
|
154
|
+
} finally {
|
|
155
|
+
rmSync(staging, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
}
|