argsbarg 7.0.10 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -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 +20 -7
- 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/plugin.json +11 -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 +12 -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 +70 -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/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/help.test.ts +36 -0
- package/src/help.ts +32 -19
- package/src/mcp/bundle.ts +12 -3
- package/src/mcp/claude.test.ts +2 -7
- package/src/mcp/claude.ts +49 -67
- package/src/mcp/cursor.test.ts +151 -0
- package/src/mcp/cursor.ts +155 -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
package/src/help.test.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
cliPositionalLabel,
|
|
13
13
|
cliResolveNotes,
|
|
14
14
|
schemaToYamlLines,
|
|
15
|
+
visibleWidth,
|
|
15
16
|
} from "./help.ts";
|
|
16
17
|
import { testProgram } from "./test/fixtures.ts";
|
|
17
18
|
|
|
@@ -214,6 +215,41 @@ describe("cliHelpRender", () => {
|
|
|
214
215
|
expect(help).not.toContain("Output Schema");
|
|
215
216
|
});
|
|
216
217
|
|
|
218
|
+
/** Tests that TTY help table boxes constrain line lengths to terminal width without wrapping border characters. */
|
|
219
|
+
test("TTY help table boxes fit terminal width without overflow", () => {
|
|
220
|
+
const origColumns = process.stdout.columns;
|
|
221
|
+
try {
|
|
222
|
+
process.stdout.columns = 120;
|
|
223
|
+
const root = testProgram({
|
|
224
|
+
key: "doc",
|
|
225
|
+
version: "1.0.0",
|
|
226
|
+
description: "Test application.",
|
|
227
|
+
commands: [
|
|
228
|
+
{
|
|
229
|
+
key: "query",
|
|
230
|
+
description:
|
|
231
|
+
"Query the body tape. Prints apply-shaped YAML `{ documentId, tabs: [{ tabId, ops: [], nodes }] }` (or JSON with --json). Fill ops and pipe to apply.",
|
|
232
|
+
handler: () => {},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
key: "markdown-insert",
|
|
236
|
+
description:
|
|
237
|
+
"Insert markdown into a Google Doc tab as native DOM elements (headings, lists, code, tables).",
|
|
238
|
+
handler: () => {},
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
});
|
|
242
|
+
const help = cliHelpRender(cliPresentationRoot(root), [], false, { isTTY: true });
|
|
243
|
+
const boxLines = help.split("\n").filter((l) => l.includes("╭") || l.includes("│") || l.includes("╰"));
|
|
244
|
+
expect(boxLines.length).toBeGreaterThan(0);
|
|
245
|
+
for (const line of boxLines) {
|
|
246
|
+
expect(visibleWidth(line)).toBe(120);
|
|
247
|
+
}
|
|
248
|
+
} finally {
|
|
249
|
+
process.stdout.columns = origColumns;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
217
253
|
/** Tests that non-TTY help automatically includes output schema in YAML by default. */
|
|
218
254
|
test("non-TTY help automatically includes output schema in YAML by default", () => {
|
|
219
255
|
const root = testProgram({
|
package/src/help.ts
CHANGED
|
@@ -82,7 +82,7 @@ function isOutputTTY(useStderr: boolean): boolean {
|
|
|
82
82
|
// ── Width Helpers ─────────────────────────────────────────────────────────────
|
|
83
83
|
|
|
84
84
|
/** Counts display columns, skipping ANSI SGR sequences. */
|
|
85
|
-
function visibleWidth(s: string): number {
|
|
85
|
+
export function visibleWidth(s: string): number {
|
|
86
86
|
let w = 0;
|
|
87
87
|
let i = 0;
|
|
88
88
|
while (i < s.length) {
|
|
@@ -230,19 +230,26 @@ interface HelpRow {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
/** Renders a free-text or notes box with a Unicode border and `title` header. */
|
|
233
|
-
function renderTextBox(
|
|
233
|
+
function renderTextBox(
|
|
234
|
+
/** Section title to render in the top border. */
|
|
235
|
+
title: string,
|
|
236
|
+
/** Lines of text to display inside the box. */
|
|
237
|
+
lines: string[],
|
|
238
|
+
/** Available terminal column width. */
|
|
239
|
+
hw: number,
|
|
240
|
+
/** Whether ANSI color styling is enabled. */
|
|
241
|
+
color: boolean,
|
|
242
|
+
): string[] {
|
|
234
243
|
if (lines.length === 0) return [];
|
|
235
244
|
|
|
236
245
|
const titleLead = color
|
|
237
246
|
? style.gray(`${kBoxH} `) + style.grayBoldTitle(title) + style.gray(" ")
|
|
238
247
|
: `${kBoxH} ${title} `;
|
|
239
248
|
|
|
240
|
-
let contentWidth = visibleWidth(titleLead) + 1;
|
|
249
|
+
let contentWidth = Math.max(visibleWidth(titleLead) + 1, hw - 4);
|
|
241
250
|
for (const line of lines) {
|
|
242
251
|
contentWidth = Math.max(contentWidth, visibleWidth(line));
|
|
243
252
|
}
|
|
244
|
-
contentWidth = Math.max(hw - 2, contentWidth);
|
|
245
|
-
contentWidth = Math.min(contentWidth, hw - 4);
|
|
246
253
|
|
|
247
254
|
const borderWidth = contentWidth + 2;
|
|
248
255
|
const headerFill = Math.max(1, borderWidth - visibleWidth(titleLead));
|
|
@@ -265,7 +272,16 @@ function renderTextBox(title: string, lines: string[], hw: number, color: boolea
|
|
|
265
272
|
}
|
|
266
273
|
|
|
267
274
|
/** Renders a two-column label/description table in a box (options, subcommands, positionals). */
|
|
268
|
-
function renderTableBox(
|
|
275
|
+
function renderTableBox(
|
|
276
|
+
/** Section title to render in the top border. */
|
|
277
|
+
title: string,
|
|
278
|
+
/** Table rows containing labels and descriptions to format. */
|
|
279
|
+
rows: HelpRow[],
|
|
280
|
+
/** Available terminal column width. */
|
|
281
|
+
hw: number,
|
|
282
|
+
/** Whether ANSI color styling is enabled. */
|
|
283
|
+
color: boolean,
|
|
284
|
+
): string[] {
|
|
269
285
|
if (rows.length === 0) return [];
|
|
270
286
|
|
|
271
287
|
let labelWidth = 0;
|
|
@@ -273,10 +289,15 @@ function renderTableBox(title: string, rows: HelpRow[], hw: number, color: boole
|
|
|
273
289
|
labelWidth = Math.max(labelWidth, visibleWidth(row.label));
|
|
274
290
|
}
|
|
275
291
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
292
|
+
let titleLead: string;
|
|
293
|
+
if (color) {
|
|
294
|
+
titleLead = style.gray(`${kBoxH} `) + style.grayBoldTitle(title) + style.gray(" ");
|
|
295
|
+
} else {
|
|
296
|
+
titleLead = `${kBoxH} ${title} `;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const targetContentWidth = Math.max(visibleWidth(titleLead) + 1, hw - 4);
|
|
300
|
+
const descWidth = Math.max(1, targetContentWidth - labelWidth - 2);
|
|
280
301
|
|
|
281
302
|
const bodyLines: string[] = [];
|
|
282
303
|
for (const row of rows) {
|
|
@@ -289,18 +310,10 @@ function renderTableBox(title: string, rows: HelpRow[], hw: number, color: boole
|
|
|
289
310
|
}
|
|
290
311
|
}
|
|
291
312
|
|
|
292
|
-
let
|
|
293
|
-
if (color) {
|
|
294
|
-
titleLead = style.gray(`${kBoxH} `) + style.grayBoldTitle(title) + style.gray(" ");
|
|
295
|
-
} else {
|
|
296
|
-
titleLead = `${kBoxH} ${title} `;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
contentWidth = Math.max(contentWidth, visibleWidth(titleLead) + 1);
|
|
313
|
+
let contentWidth = targetContentWidth;
|
|
300
314
|
for (const line of bodyLines) {
|
|
301
315
|
contentWidth = Math.max(contentWidth, visibleWidth(line));
|
|
302
316
|
}
|
|
303
|
-
contentWidth = Math.min(contentWidth, hw - 4);
|
|
304
317
|
|
|
305
318
|
const borderWidth = contentWidth + 2;
|
|
306
319
|
const headerFill = Math.max(1, borderWidth - visibleWidth(titleLead));
|
package/src/mcp/bundle.ts
CHANGED
|
@@ -9,6 +9,7 @@ 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
|
|
|
@@ -148,10 +149,15 @@ export function packMcpBundle(program: CliProgram, opts: PackMcpBundleOpts = {})
|
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
/** Runs `mcp bundle`: writes enabled dist artifacts; prints one path per line. */
|
|
151
|
-
export function runMcpBundle(
|
|
152
|
+
export function runMcpBundle(
|
|
153
|
+
/** CLI program schema. */
|
|
154
|
+
program: CliProgram,
|
|
155
|
+
): void {
|
|
152
156
|
const mcp = program.mcpServer;
|
|
153
|
-
if (!mcp?.mcpd && !mcp?.claudePlugin) {
|
|
154
|
-
throw new Error(
|
|
157
|
+
if (!mcp?.mcpd && !mcp?.claudePlugin && !mcp?.cursorPlugin) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
"mcp bundle: enable mcpServer.mcpd, mcpServer.claudePlugin, and/or mcpServer.cursorPlugin on the program root.",
|
|
160
|
+
);
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
const lines: string[] = [];
|
|
@@ -161,5 +167,8 @@ export function runMcpBundle(program: CliProgram): void {
|
|
|
161
167
|
if (mcp.claudePlugin) {
|
|
162
168
|
lines.push(packClaudePlugin(program));
|
|
163
169
|
}
|
|
170
|
+
if (mcp.cursorPlugin) {
|
|
171
|
+
lines.push(packCursorPlugin(program));
|
|
172
|
+
}
|
|
164
173
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
165
174
|
}
|
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 { 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"),
|
|
@@ -128,14 +105,19 @@ function writePluginTree(pluginRoot: string, program: CliProgram, binaryPath: st
|
|
|
128
105
|
`${JSON.stringify(generatePluginMcpJson(program, binaryName), null, 2)}\n`,
|
|
129
106
|
);
|
|
130
107
|
cpSync(binaryPath, join(pluginRoot, "bin", binaryName), { mode: 0o755 });
|
|
131
|
-
|
|
108
|
+
stagePluginSkills(pluginRoot, program, cwd);
|
|
132
109
|
}
|
|
133
110
|
|
|
134
111
|
/**
|
|
135
112
|
* Writes `dist/claude-plugin/<name>.zip` with manifests, binary, and skill bundle.
|
|
136
113
|
* Requires the compiled binary to exist.
|
|
137
114
|
*/
|
|
138
|
-
export function packClaudePlugin(
|
|
115
|
+
export function packClaudePlugin(
|
|
116
|
+
/** CLI program schema. */
|
|
117
|
+
program: CliProgram,
|
|
118
|
+
/** Packaging options. */
|
|
119
|
+
opts: PackMcpBundleOpts = {},
|
|
120
|
+
): string {
|
|
139
121
|
const cwd = opts.cwd ?? process.cwd();
|
|
140
122
|
const defaults = defaultClaudePluginPaths(program, cwd);
|
|
141
123
|
const mcpDefaults = defaultMcpBundlePaths(program, cwd);
|
|
@@ -149,7 +131,7 @@ export function packClaudePlugin(program: CliProgram, opts: PackMcpBundleOpts =
|
|
|
149
131
|
|
|
150
132
|
const staging = mkdtempSync(join(tmpdir(), "claude-plugin-"));
|
|
151
133
|
try {
|
|
152
|
-
writePluginTree(staging, program, binaryPath, binaryName);
|
|
134
|
+
writePluginTree(staging, program, binaryPath, binaryName, cwd);
|
|
153
135
|
const zip = zipStore(collectZipEntries(staging));
|
|
154
136
|
mkdirSync(join(pluginZipPath, ".."), { recursive: true });
|
|
155
137
|
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,155 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Packs a Cursor plugin zip from a compiled CLI binary.
|
|
3
|
+
Internal module — not exported from index.ts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { 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
|
+
cpSync(binaryPath, join(pluginRoot, "bin", binaryName), { mode: 0o755 });
|
|
121
|
+
stagePluginSkills(pluginRoot, program, cwd);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Writes `dist/cursor-plugin/<name>.zip` with manifests, binary, and skill bundle.
|
|
126
|
+
* Requires the compiled binary to exist.
|
|
127
|
+
*/
|
|
128
|
+
export function packCursorPlugin(
|
|
129
|
+
/** CLI program schema. */
|
|
130
|
+
program: CliProgram,
|
|
131
|
+
/** Packaging options. */
|
|
132
|
+
opts: PackMcpBundleOpts = {},
|
|
133
|
+
): string {
|
|
134
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
135
|
+
const defaults = defaultCursorPluginPaths(program, cwd);
|
|
136
|
+
const mcpDefaults = defaultMcpBundlePaths(program, cwd);
|
|
137
|
+
const binaryPath = resolve(cwd, opts.binaryPath ?? mcpDefaults.binaryPath);
|
|
138
|
+
const pluginZipPath = resolve(cwd, defaults.pluginZipPath);
|
|
139
|
+
const binaryName = basename(binaryPath);
|
|
140
|
+
|
|
141
|
+
if (!existsSync(binaryPath)) {
|
|
142
|
+
throw new Error(`Binary not found: ${binaryPath}. Build with compile first (expected dist/${program.key}).`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const staging = mkdtempSync(join(tmpdir(), "cursor-plugin-"));
|
|
146
|
+
try {
|
|
147
|
+
writePluginTree(staging, program, binaryPath, binaryName, cwd);
|
|
148
|
+
const zip = zipStore(collectZipEntries(staging));
|
|
149
|
+
mkdirSync(join(pluginZipPath, ".."), { recursive: true });
|
|
150
|
+
writeFileSync(pluginZipPath, zip);
|
|
151
|
+
return pluginZipPath;
|
|
152
|
+
} finally {
|
|
153
|
+
rmSync(staging, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
}
|