argsbarg 7.0.11 → 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.
Files changed (89) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +3 -2
  3. package/docs/ai-skills.md +2 -2
  4. package/docs/cli-program.md +3 -5
  5. package/docs/developing.md +3 -3
  6. package/docs/mcp.md +20 -7
  7. package/docs/output-schema.md +1 -1
  8. package/examples/full-example/AGENTS.md +20 -12
  9. package/examples/full-example/README.md +57 -12
  10. package/examples/full-example/biome.json +5 -4
  11. package/examples/full-example/bunfig.toml +3 -0
  12. package/examples/full-example/justfile +22 -27
  13. package/examples/full-example-json/AGENTS.md +14 -12
  14. package/examples/full-example-json/README.md +63 -15
  15. package/examples/full-example-json/biome.json +1 -3
  16. package/examples/full-example-json/bunfig.toml +3 -0
  17. package/examples/full-example-json/justfile +22 -27
  18. package/examples/mcp-plugin/.claude-plugin/plugin.json +11 -0
  19. package/examples/mcp-plugin/.cursor/hooks/run-tests-on-stop.ts +56 -0
  20. package/examples/mcp-plugin/.cursor/hooks.json +12 -0
  21. package/examples/mcp-plugin/.cursor-plugin/plugin.json +12 -0
  22. package/examples/mcp-plugin/.mcp.json +6 -0
  23. package/examples/mcp-plugin/AGENTS.md +90 -0
  24. package/examples/mcp-plugin/CLAUDE.md +1 -0
  25. package/examples/mcp-plugin/README.md +70 -0
  26. package/examples/mcp-plugin/biome.json +20 -0
  27. package/examples/mcp-plugin/bun.lock +48 -0
  28. package/examples/mcp-plugin/bunfig.toml +3 -0
  29. package/examples/mcp-plugin/docs/README.md +27 -0
  30. package/examples/mcp-plugin/docs/cli-schema.json +2085 -0
  31. package/examples/mcp-plugin/docs/cli.md +2026 -0
  32. package/examples/mcp-plugin/docs/http.md +92 -0
  33. package/examples/mcp-plugin/docs/mcp.md +116 -0
  34. package/examples/mcp-plugin/docs/openapi.json +1243 -0
  35. package/examples/mcp-plugin/justfile +89 -0
  36. package/examples/mcp-plugin/mcp.json +8 -0
  37. package/examples/mcp-plugin/package.json +23 -0
  38. package/examples/mcp-plugin/scripts/create-identity.ts +12 -0
  39. package/examples/mcp-plugin/scripts/mcp.mjs +11106 -0
  40. package/examples/mcp-plugin/scripts/release.ts +225 -0
  41. package/examples/mcp-plugin/skills/mcp-plugin/SKILL.md +59 -0
  42. package/examples/mcp-plugin/src/commands/echo/command.ts +26 -0
  43. package/examples/mcp-plugin/src/commands/render-json/__generated__/RenderJsonInputSchema.json +15 -0
  44. package/examples/mcp-plugin/src/commands/render-json/__generated__/index.ts +5 -0
  45. package/examples/mcp-plugin/src/commands/render-json/command.test.ts +46 -0
  46. package/examples/mcp-plugin/src/commands/render-json/command.ts +30 -0
  47. package/examples/mcp-plugin/src/commands/render-json/types.ts +9 -0
  48. package/examples/mcp-plugin/src/commands/status/__generated__/StatusJsonOutputSchema.json +15 -0
  49. package/examples/mcp-plugin/src/commands/status/__generated__/index.ts +5 -0
  50. package/examples/mcp-plugin/src/commands/status/command.test.ts +10 -0
  51. package/examples/mcp-plugin/src/commands/status/command.ts +28 -0
  52. package/examples/mcp-plugin/src/commands/status/types.ts +6 -0
  53. package/examples/mcp-plugin/src/commands/workspaces/__generated__/WorkspaceNameInputSchema.json +15 -0
  54. package/examples/mcp-plugin/src/commands/workspaces/__generated__/index.ts +5 -0
  55. package/examples/mcp-plugin/src/commands/workspaces/command.test.ts +58 -0
  56. package/examples/mcp-plugin/src/commands/workspaces/command.ts +94 -0
  57. package/examples/mcp-plugin/src/commands/workspaces/types.ts +6 -0
  58. package/examples/mcp-plugin/src/db/index.test.ts +86 -0
  59. package/examples/mcp-plugin/src/db/index.ts +77 -0
  60. package/examples/mcp-plugin/src/db/tables/workspaces.ts +58 -0
  61. package/examples/mcp-plugin/src/index.ts +10 -0
  62. package/examples/mcp-plugin/src/program.ts +32 -0
  63. package/examples/mcp-plugin/src/types/argsbarg.d.ts +11 -0
  64. package/examples/mcp-plugin/src/types/md.d.ts +4 -0
  65. package/examples/mcp-plugin/tsconfig.json +17 -0
  66. package/index.d.ts +12 -0
  67. package/package.json +1 -1
  68. package/src/builtins/mcp.ts +1 -1
  69. package/src/cli-tool/create.test.ts +5 -0
  70. package/src/cli-tool/create.ts +9 -1
  71. package/src/config/manifest.ts +56 -0
  72. package/src/config/resolve.test.ts +12 -12
  73. package/src/configure/configure.test.ts +5 -3
  74. package/src/core/parse.test.ts +6 -5
  75. package/src/core/types.ts +12 -0
  76. package/src/exports/mcp.ts +12 -0
  77. package/src/mcp/bundle.ts +12 -3
  78. package/src/mcp/claude.test.ts +2 -7
  79. package/src/mcp/claude.ts +49 -67
  80. package/src/mcp/cursor.test.ts +151 -0
  81. package/src/mcp/cursor.ts +155 -0
  82. package/src/mcp/hidden-mcpb.test.ts +34 -1
  83. package/src/mcp/plugin-shared.ts +107 -0
  84. package/src/mcp/result.ts +5 -1
  85. package/src/mcp/server.ts +3 -2
  86. package/src/test/fixtures.ts +13 -0
  87. package/src/test/integration/mcp.test.ts +8 -7
  88. package/examples/full-example/scripts/print-identity.ts +0 -28
  89. package/examples/full-example-json/scripts/print-identity.ts +0 -28
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(program: CliProgram): void {
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("mcp bundle: enable mcpServer.mcpd and/or mcpServer.claudePlugin on the program root.");
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
  }
@@ -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
- defaultClaudePluginPaths,
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, relative, resolve } from "node:path";
8
+ import { basename, join, resolve } from "node:path";
20
9
  import { buildPluginMcpEnvMapping, buildProgramUserConfig } from "../config/manifest.ts";
21
- import type { CliMcpBundleConfig, CliProgram } from "../core/types.ts";
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 { type ZipFileEntry, zipStore } from "./zip.ts";
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
- function defaultAuthor(bundle?: CliMcpBundleConfig): {
41
- name: string;
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(program: CliProgram, cwd = process.cwd()) {
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(program: CliProgram, _binaryName: string): Record<string, unknown> {
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(program: CliProgram, binaryName: string): Record<string, unknown> {
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
- function collectZipEntries(rootDir: string, dir = rootDir): ZipFileEntry[] {
93
- const entries: ZipFileEntry[] = [];
94
- for (const ent of readdirSync(dir, { withFileTypes: true }) as Dirent[]) {
95
- const full = join(dir, ent.name);
96
- if (ent.isDirectory()) {
97
- entries.push(...collectZipEntries(rootDir, full));
98
- continue;
99
- }
100
- if (!ent.isFile()) {
101
- continue;
102
- }
103
- const rel = relative(rootDir, full).split("\\").join("/");
104
- const stMode = statSync(full).mode;
105
- const entry: ZipFileEntry = { name: rel, data: readFileSync(full) };
106
- if (stMode & 0o111) {
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
- writeFileSync(join(pluginRoot, "skills", bundle.dirName, "SKILL.md"), skillMd);
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(program: CliProgram, opts: PackMcpBundleOpts = {}): string {
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
+ }
@@ -222,11 +222,44 @@ describe("mcp bundle", () => {
222
222
  }
223
223
  });
224
224
 
225
+ /** RunMcpBundle with cursorPlugin only prints plugin path. */
226
+ test("runMcpBundle with cursorPlugin only prints plugin path", () => {
227
+ const work = mkdtempSync(join(tmpdir(), "mcpb-run-cursor-"));
228
+ const stdout: string[] = [];
229
+ const orig = process.stdout.write.bind(process.stdout);
230
+ process.stdout.write = ((chunk: string | Uint8Array) => {
231
+ stdout.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
232
+ return true;
233
+ }) as typeof process.stdout.write;
234
+ const fixture: CliProgram = {
235
+ ...hiddenFixture,
236
+ mcpServer: { enabled: true, cursorPlugin: true },
237
+ };
238
+ try {
239
+ const dist = join(work, "dist");
240
+ mkdirSync(dist, { recursive: true });
241
+ writeFileSync(join(dist, "myapp"), "#!/bin/sh\n", { mode: 0o755 });
242
+ const prevCwd = process.cwd();
243
+ process.chdir(work);
244
+ try {
245
+ runMcpBundle(fixture);
246
+ } finally {
247
+ process.chdir(prevCwd);
248
+ }
249
+ const lines = stdout.join("").trim().split("\n");
250
+ expect(lines).toHaveLength(1);
251
+ expect(lines[0]).toContain("cursor-plugin");
252
+ } finally {
253
+ process.stdout.write = orig;
254
+ rmSync(work, { recursive: true, force: true });
255
+ }
256
+ });
257
+
225
258
  test("runMcpBundle errors when no bundle flags enabled", () => {
226
259
  const fixture: CliProgram = {
227
260
  ...hiddenFixture,
228
261
  mcpServer: { enabled: true },
229
262
  };
230
- expect(() => runMcpBundle(fixture)).toThrow(/mcpd/);
263
+ expect(() => runMcpBundle(fixture)).toThrow(/cursorPlugin/);
231
264
  });
232
265
  });
@@ -0,0 +1,107 @@
1
+ /*
2
+ Shared utilities and staging helpers for Claude Code and Cursor plugin packages.
3
+ Internal module — not exported from index.ts.
4
+ */
5
+
6
+ import {
7
+ cpSync,
8
+ type Dirent,
9
+ existsSync,
10
+ mkdirSync,
11
+ readdirSync,
12
+ readFileSync,
13
+ statSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import { join, relative, resolve } from "node:path";
17
+ import type { CliMcpBundleConfig, CliProgram } from "../core/types.ts";
18
+ import { generatePluginSkillBundle } from "../skill/generate.ts";
19
+ import { applyPluginSkillHint } from "../skill/hint.ts";
20
+ import { sanitizeToolSegment } from "./tools.ts";
21
+ import type { ZipFileEntry } from "./zip.ts";
22
+
23
+ /**
24
+ * Traverses a staging directory recursively and gathers ZIP entries preserving executable permissions.
25
+ */
26
+ export function collectZipEntries(
27
+ /** Directory containing the staged files. */
28
+ rootDir: string,
29
+ /** Subdirectory currently being traversed (defaults to rootDir). */
30
+ dir = rootDir,
31
+ ): ZipFileEntry[] {
32
+ const entries: ZipFileEntry[] = [];
33
+ for (const ent of readdirSync(dir, { withFileTypes: true }) as Dirent[]) {
34
+ const full = join(dir, ent.name);
35
+ if (ent.isDirectory()) {
36
+ entries.push(...collectZipEntries(rootDir, full));
37
+ continue;
38
+ }
39
+ if (!ent.isFile()) {
40
+ continue;
41
+ }
42
+ const rel = relative(rootDir, full).split("\\").join("/");
43
+ const stMode = statSync(full).mode;
44
+ const entry: ZipFileEntry = { name: rel, data: readFileSync(full) };
45
+ if (stMode & 0o111) {
46
+ entry.unixMode = stMode;
47
+ }
48
+ entries.push(entry);
49
+ }
50
+ return entries;
51
+ }
52
+
53
+ /**
54
+ * Returns author metadata from bundle config or fallback defaults.
55
+ */
56
+ export function defaultAuthor(
57
+ /** Optional bundle configuration. */
58
+ bundle?: CliMcpBundleConfig,
59
+ ): {
60
+ name: string;
61
+ email?: string;
62
+ url?: string;
63
+ } {
64
+ return bundle?.author ?? { name: "Unknown" };
65
+ }
66
+
67
+ /**
68
+ * Generates kebab-case plugin name for plugin manifests.
69
+ */
70
+ export function pluginName(
71
+ /** CLI program schema. */
72
+ program: CliProgram,
73
+ ): string {
74
+ return program.key
75
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
76
+ .replace(/[^a-zA-Z0-9]+/g, "-")
77
+ .replace(/^-+|-+$/g, "")
78
+ .toLowerCase();
79
+ }
80
+
81
+ /**
82
+ * Stages repository skill directory if present, or writes a generated MCP routing skill.
83
+ */
84
+ export function stagePluginSkills(
85
+ /** Plugin staging root directory. */
86
+ pluginRoot: string,
87
+ /** CLI program schema. */
88
+ program: CliProgram,
89
+ /** Working directory containing repository sources. */
90
+ cwd: string,
91
+ ): void {
92
+ const dirName = sanitizeToolSegment(program.key);
93
+ const repoSkillDir = program.mcpServer?.bundle?.skillsDir
94
+ ? resolve(cwd, program.mcpServer.bundle.skillsDir)
95
+ : join(cwd, "skills", dirName);
96
+
97
+ if (existsSync(repoSkillDir) && statSync(repoSkillDir).isDirectory()) {
98
+ const targetDir = join(pluginRoot, "skills", dirName);
99
+ mkdirSync(join(targetDir, ".."), { recursive: true });
100
+ cpSync(repoSkillDir, targetDir, { recursive: true });
101
+ } else {
102
+ const bundle = generatePluginSkillBundle(program);
103
+ const skillMd = applyPluginSkillHint(program, bundle.skillMd);
104
+ mkdirSync(join(pluginRoot, "skills", bundle.dirName), { recursive: true });
105
+ writeFileSync(join(pluginRoot, "skills", bundle.dirName, "SKILL.md"), skillMd);
106
+ }
107
+ }