@stigmer/plugin-package 3.35.0 → 3.36.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/client/archive.d.ts.map +1 -1
- package/client/archive.js.map +1 -1
- package/client/builtin.d.ts.map +1 -1
- package/client/builtin.js.map +1 -1
- package/client/ignore/defaults.d.ts.map +1 -1
- package/client/ignore/defaults.js.map +1 -1
- package/client/ignore/match.d.ts.map +1 -1
- package/client/ignore/match.js.map +1 -1
- package/client/ignore/matcher.d.ts.map +1 -1
- package/client/ignore/matcher.js.map +1 -1
- package/client/ignore/pattern.d.ts.map +1 -1
- package/client/ignore/pattern.js.map +1 -1
- package/client/prepare.d.ts.map +1 -1
- package/client/prepare.js.map +1 -1
- package/client/presentation.d.ts.map +1 -1
- package/client/presentation.js.map +1 -1
- package/client/refs.d.ts.map +1 -1
- package/client/refs.js.map +1 -1
- package/client/release.d.ts.map +1 -1
- package/client/release.js.map +1 -1
- package/client/reroot.d.ts.map +1 -1
- package/client/reroot.js.map +1 -1
- package/client/select.d.ts.map +1 -1
- package/client/select.js.map +1 -1
- package/client/vocabulary.d.ts.map +1 -1
- package/client/vocabulary.js.map +1 -1
- package/client.d.ts.map +1 -1
- package/client.js.map +1 -1
- package/detect.d.ts.map +1 -1
- package/detect.js.map +1 -1
- package/dialects/claude.d.ts.map +1 -1
- package/dialects/claude.js.map +1 -1
- package/dialects/codex.d.ts.map +1 -1
- package/dialects/codex.js.map +1 -1
- package/dialects/cursor.d.ts.map +1 -1
- package/dialects/cursor.js.map +1 -1
- package/dialects/manifest.d.ts.map +1 -1
- package/dialects/manifest.js.map +1 -1
- package/dialects/open.d.ts.map +1 -1
- package/dialects/open.js.map +1 -1
- package/documents.d.ts.map +1 -1
- package/documents.js.map +1 -1
- package/files.d.ts.map +1 -1
- package/files.js.map +1 -1
- package/frontmatter.d.ts.map +1 -1
- package/frontmatter.js.map +1 -1
- package/index.d.ts.map +1 -1
- package/index.js.map +1 -1
- package/marketplace/messages.d.ts.map +1 -1
- package/marketplace/messages.js.map +1 -1
- package/marketplace/outcome.d.ts.map +1 -1
- package/marketplace/outcome.js.map +1 -1
- package/marketplace/read-marketplace.d.ts.map +1 -1
- package/marketplace/read-marketplace.js.map +1 -1
- package/messages.d.ts.map +1 -1
- package/messages.js.map +1 -1
- package/normalise/ignored.d.ts.map +1 -1
- package/normalise/ignored.js.map +1 -1
- package/normalise/mcp-servers.d.ts.map +1 -1
- package/normalise/mcp-servers.js.map +1 -1
- package/normalise/overlay.d.ts.map +1 -1
- package/normalise/overlay.js.map +1 -1
- package/normalise/skills.d.ts.map +1 -1
- package/normalise/skills.js.map +1 -1
- package/normalise/sub-agents.d.ts.map +1 -1
- package/normalise/sub-agents.js.map +1 -1
- package/normalise/variables.d.ts.map +1 -1
- package/normalise/variables.js.map +1 -1
- package/outcome.d.ts.map +1 -1
- package/outcome.js.map +1 -1
- package/package.json +1 -1
- package/placeholders.d.ts.map +1 -1
- package/placeholders.js.map +1 -1
- package/presentation.d.ts.map +1 -1
- package/presentation.js.map +1 -1
- package/read-plugin-package.d.ts.map +1 -1
- package/read-plugin-package.js.map +1 -1
- package/testing.d.ts.map +1 -1
- package/testing.js.map +1 -1
- package/types.d.ts.map +1 -1
- package/types.js.map +1 -1
- package/src/__test-utils__/directory-files.ts +0 -29
- package/src/__test-utils__/read.ts +0 -55
- package/src/__tests__/adversarial.test.ts +0 -434
- package/src/__tests__/client-ignore.test.ts +0 -133
- package/src/__tests__/client-prepare.test.ts +0 -163
- package/src/__tests__/client-refs.test.ts +0 -101
- package/src/__tests__/client-select-archive.test.ts +0 -150
- package/src/__tests__/detect.test.ts +0 -144
- package/src/__tests__/files.test.ts +0 -119
- package/src/__tests__/fixtures/cursor-plugins/.cursor-plugin/marketplace.json +0 -412
- package/src/__tests__/fixtures/cursor-plugins/NOTICE +0 -27
- package/src/__tests__/fixtures/cursor-plugins/advisor/.cursor-plugin/plugin.json +0 -33
- package/src/__tests__/fixtures/cursor-plugins/advisor/CHANGELOG.md +0 -8
- package/src/__tests__/fixtures/cursor-plugins/advisor/LICENSE +0 -21
- package/src/__tests__/fixtures/cursor-plugins/advisor/README.md +0 -87
- package/src/__tests__/fixtures/cursor-plugins/advisor/agents/advisor-subagent.md +0 -48
- package/src/__tests__/fixtures/cursor-plugins/advisor/assets/avatar.png +0 -0
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/capture-response.sh +0 -20
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/hooks.json +0 -27
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/lib.sh +0 -61
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/mark-pending.sh +0 -27
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/record-consult.sh +0 -41
- package/src/__tests__/fixtures/cursor-plugins/advisor/hooks/stop-hook.sh +0 -48
- package/src/__tests__/fixtures/cursor-plugins/advisor/skills/advisor/SKILL.md +0 -123
- package/src/__tests__/fixtures/cursor-plugins/advisor/skills/advisor/references/briefing-template.md +0 -44
- package/src/__tests__/fixtures/cursor-plugins/github/.cursor-plugin/plugin.json +0 -45
- package/src/__tests__/fixtures/cursor-plugins/github/CHANGELOG.md +0 -9
- package/src/__tests__/fixtures/cursor-plugins/github/LICENSE +0 -21
- package/src/__tests__/fixtures/cursor-plugins/github/README.md +0 -64
- package/src/__tests__/fixtures/cursor-plugins/github/assets/logo.svg +0 -0
- package/src/__tests__/fixtures/cursor-plugins/github/mcp.json +0 -11
- package/src/__tests__/fixtures/cursor-plugins/playwright/.cursor-plugin/plugin.json +0 -35
- package/src/__tests__/fixtures/cursor-plugins/playwright/CHANGELOG.md +0 -8
- package/src/__tests__/fixtures/cursor-plugins/playwright/LICENSE +0 -21
- package/src/__tests__/fixtures/cursor-plugins/playwright/README.md +0 -46
- package/src/__tests__/fixtures/cursor-plugins/playwright/assets/logo.svg +0 -0
- package/src/__tests__/fixtures/cursor-plugins/playwright/mcp.json +0 -8
- package/src/__tests__/fixtures/cursor-plugins/salesforce/.cursor-plugin/plugin.json +0 -48
- package/src/__tests__/fixtures/cursor-plugins/salesforce/CHANGELOG.md +0 -10
- package/src/__tests__/fixtures/cursor-plugins/salesforce/LICENSE +0 -21
- package/src/__tests__/fixtures/cursor-plugins/salesforce/README.md +0 -95
- package/src/__tests__/fixtures/cursor-plugins/salesforce/assets/logo.svg +0 -0
- package/src/__tests__/fixtures/cursor-plugins/salesforce/mcp.json +0 -12
- package/src/__tests__/fixtures/cursor-plugins/thermos/.cursor-plugin/plugin.json +0 -32
- package/src/__tests__/fixtures/cursor-plugins/thermos/CHANGELOG.md +0 -8
- package/src/__tests__/fixtures/cursor-plugins/thermos/LICENSE +0 -21
- package/src/__tests__/fixtures/cursor-plugins/thermos/README.md +0 -70
- package/src/__tests__/fixtures/cursor-plugins/thermos/agents/thermo-nuclear-code-quality-review-subagent.md +0 -23
- package/src/__tests__/fixtures/cursor-plugins/thermos/agents/thermo-nuclear-review-subagent.md +0 -28
- package/src/__tests__/fixtures/cursor-plugins/thermos/assets/logo.png +0 -0
- package/src/__tests__/fixtures/cursor-plugins/thermos/skills/thermo-nuclear-code-quality-review/SKILL.md +0 -192
- package/src/__tests__/fixtures/cursor-plugins/thermos/skills/thermo-nuclear-review/SKILL.md +0 -51
- package/src/__tests__/fixtures/cursor-plugins/thermos/skills/thermos/SKILL.md +0 -21
- package/src/__tests__/fixtures/cursor-plugins/xero/.cursor-plugin/plugin.json +0 -53
- package/src/__tests__/fixtures/cursor-plugins/xero/CHANGELOG.md +0 -9
- package/src/__tests__/fixtures/cursor-plugins/xero/LICENSE +0 -21
- package/src/__tests__/fixtures/cursor-plugins/xero/README.md +0 -79
- package/src/__tests__/fixtures/cursor-plugins/xero/assets/logo.png +0 -0
- package/src/__tests__/fixtures/cursor-plugins/xero/mcp.json +0 -16
- package/src/__tests__/fixtures.test.ts +0 -198
- package/src/__tests__/marketplace.test.ts +0 -378
- package/src/__tests__/mcp-servers.test.ts +0 -120
- package/src/__tests__/overlay-and-ignored.test.ts +0 -65
- package/src/__tests__/presentation.test.ts +0 -186
- package/src/__tests__/skills.test.ts +0 -89
- package/src/__tests__/sub-agents.test.ts +0 -111
- package/src/__tests__/variables.test.ts +0 -94
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The marketplace reader's suite: one accepted fixture per dialect pinning
|
|
3
|
-
* the normalised shape, the verbatim `cursor/plugins` catalogue read over a
|
|
4
|
-
* partial tree (the six vendored plugins at their source paths, the other
|
|
5
|
-
* seventy-three reported as directories the tree does not hold), and the
|
|
6
|
-
* adversarial contract: one fixture per finding kind, asserting the complete
|
|
7
|
-
* sorted list of kinds and the sentence of the kind under test. The two
|
|
8
|
-
* adversarial tables are `Record`s over the kind unions, so a kind added to
|
|
9
|
-
* `marketplace/outcome.ts` without a case here does not compile.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
|
-
|
|
14
|
-
import { describe, expect, it } from "vitest";
|
|
15
|
-
|
|
16
|
-
import { inMemoryPluginFiles, PLUGIN_DOCUMENT_LIMITS, type PluginFileEntry, type PluginFiles } from "../files.js";
|
|
17
|
-
import type { Marketplace, MarketplaceErrorKind, MarketplaceFinding, MarketplaceReadOutcome, MarketplaceWarningKind } from "../marketplace/outcome.js";
|
|
18
|
-
import { hasMarketplaceFile, readMarketplace, readMarketplaceFile } from "../marketplace/read-marketplace.js";
|
|
19
|
-
import { cursorPlugin, openPlugin, type PluginFixture } from "../testing.js";
|
|
20
|
-
import { directoryPluginFiles } from "../__test-utils__/directory-files.js";
|
|
21
|
-
import { findingOf, type Kinds } from "../__test-utils__/read.js";
|
|
22
|
-
|
|
23
|
-
const FIXTURES = fileURLToPath(new URL("./fixtures/cursor-plugins/", import.meta.url));
|
|
24
|
-
|
|
25
|
-
/** A marketplace tree from a map, with plugin fixtures mounted under their directories. */
|
|
26
|
-
function tree(marketplaceFile: string, content: unknown, plugins: Readonly<Record<string, PluginFixture>> = {}): PluginFiles {
|
|
27
|
-
const files = new Map<string, string | Uint8Array>();
|
|
28
|
-
files.set(marketplaceFile, typeof content === "string" ? content : JSON.stringify(content));
|
|
29
|
-
for (const [dir, fixture] of Object.entries(plugins)) {
|
|
30
|
-
for (const [path, bytes] of fixture) files.set(dir === "" ? path : `${dir}/${path}`, bytes);
|
|
31
|
-
}
|
|
32
|
-
return inMemoryPluginFiles(files);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function kindsOf(outcome: MarketplaceReadOutcome): Kinds {
|
|
36
|
-
const sorted = (findings: readonly MarketplaceFinding[]): string[] => findings.map((f) => f.kind).sort();
|
|
37
|
-
return outcome.ok
|
|
38
|
-
? { errors: [], warnings: sorted(outcome.warnings) }
|
|
39
|
-
: { errors: sorted(outcome.errors), warnings: sorted(outcome.warnings) };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function accepted(outcome: MarketplaceReadOutcome): Marketplace {
|
|
43
|
-
if (!outcome.ok) {
|
|
44
|
-
expect.fail(`expected an accepted marketplace, got refusals:\n${outcome.errors.map((f) => ` ${f.kind}: ${f.message}`).join("\n")}`);
|
|
45
|
-
}
|
|
46
|
-
return outcome.marketplace;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function refused(outcome: MarketplaceReadOutcome): readonly MarketplaceFinding[] {
|
|
50
|
-
if (outcome.ok) {
|
|
51
|
-
expect.fail(`expected a refusal, got an accepted marketplace '${outcome.marketplace.name}'`);
|
|
52
|
-
}
|
|
53
|
-
return outcome.errors;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const errors = (...kinds: MarketplaceErrorKind[]): Kinds => ({ errors: [...kinds].sort(), warnings: [] });
|
|
57
|
-
const warnings = (...kinds: MarketplaceWarningKind[]): Kinds => ({ errors: [], warnings: [...kinds].sort() });
|
|
58
|
-
|
|
59
|
-
const STIGMER_FILE = "marketplace.json";
|
|
60
|
-
const alpha = openPlugin({ name: "alpha" });
|
|
61
|
-
const beta = openPlugin({ name: "beta" });
|
|
62
|
-
|
|
63
|
-
describe("hasMarketplaceFile", () => {
|
|
64
|
-
it("routes on any of the four locations and nothing else", () => {
|
|
65
|
-
expect(hasMarketplaceFile(["marketplace.json"])).toBe(true);
|
|
66
|
-
expect(hasMarketplaceFile([".claude-plugin/marketplace.json"])).toBe(true);
|
|
67
|
-
expect(hasMarketplaceFile([".cursor-plugin/marketplace.json"])).toBe(true);
|
|
68
|
-
expect(hasMarketplaceFile([".agents/plugins/marketplace.json"])).toBe(true);
|
|
69
|
-
expect(hasMarketplaceFile(["plugin.json", "thermos/marketplace.json"])).toBe(false);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe("the Stigmer file (root marketplace.json)", () => {
|
|
74
|
-
const outcome = readMarketplace(
|
|
75
|
-
tree(
|
|
76
|
-
STIGMER_FILE,
|
|
77
|
-
{
|
|
78
|
-
name: "stigmer",
|
|
79
|
-
description: "The official catalogue.",
|
|
80
|
-
owner: { name: "Stigmer", url: "https://stigmer.ai" },
|
|
81
|
-
plugins: [
|
|
82
|
-
{ name: "alpha", source: "./alpha" },
|
|
83
|
-
{ name: "beta", source: "beta/", description: "Beta, described by the catalogue." },
|
|
84
|
-
],
|
|
85
|
-
},
|
|
86
|
-
{ alpha, beta },
|
|
87
|
-
),
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
it("is accepted with no warnings", () => {
|
|
91
|
-
expect(kindsOf(outcome)).toEqual({ errors: [], warnings: [] });
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("carries identity, dialect, path and entries in file order", () => {
|
|
95
|
-
const marketplace = accepted(outcome);
|
|
96
|
-
expect(marketplace.name).toBe("stigmer");
|
|
97
|
-
expect(marketplace.description).toBe("The official catalogue.");
|
|
98
|
-
expect(marketplace.owner).toEqual({ name: "Stigmer", url: "https://stigmer.ai" });
|
|
99
|
-
expect(marketplace.dialect).toBe("stigmer");
|
|
100
|
-
expect(marketplace.path).toBe(STIGMER_FILE);
|
|
101
|
-
expect(marketplace.plugins).toEqual([
|
|
102
|
-
{ name: "alpha", dir: "alpha" },
|
|
103
|
-
{ name: "beta", dir: "beta", description: "Beta, described by the catalogue." },
|
|
104
|
-
]);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("ignores a `defaults` list, the retired install-unasked field, whatever it holds", () => {
|
|
108
|
-
for (const defaults of [["alpha"], ["gamma"], 7]) {
|
|
109
|
-
const read = readMarketplace(tree(STIGMER_FILE, { name: "acme", plugins: [{ name: "alpha", source: "./alpha" }], defaults }, { alpha }));
|
|
110
|
-
expect(kindsOf(read)).toEqual({ errors: [], warnings: [] });
|
|
111
|
-
expect(accepted(read).plugins).toEqual([{ name: "alpha", dir: "alpha" }]);
|
|
112
|
-
expect(Object.keys(accepted(read))).not.toContain("defaults");
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("offers the tree root itself when the source is '.' (a single-plugin marketplace)", () => {
|
|
117
|
-
const single = readMarketplace(tree(STIGMER_FILE, { name: "solo", plugins: [{ name: "alpha", source: "." }] }, { "": alpha }));
|
|
118
|
-
expect(accepted(single).plugins).toEqual([{ name: "alpha", dir: "" }]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("takes precedence over a vendor file in the same tree", () => {
|
|
122
|
-
const files = tree(STIGMER_FILE, { name: "ours", plugins: [] });
|
|
123
|
-
const both = inMemoryPluginFiles(
|
|
124
|
-
new Map<string, string>([
|
|
125
|
-
...files.entries.map((e: PluginFileEntry) => [e.path, new TextDecoder().decode(files.read(e.path))] as const),
|
|
126
|
-
[".cursor-plugin/marketplace.json", JSON.stringify({ name: "theirs", plugins: [] })],
|
|
127
|
-
]),
|
|
128
|
-
);
|
|
129
|
-
expect(accepted(readMarketplace(both)).name).toBe("ours");
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe("the Claude Code file (.claude-plugin/marketplace.json)", () => {
|
|
134
|
-
const outcome = readMarketplace(
|
|
135
|
-
tree(
|
|
136
|
-
".claude-plugin/marketplace.json",
|
|
137
|
-
{
|
|
138
|
-
name: "acme-tools",
|
|
139
|
-
owner: { name: "Acme", email: "plugins@acme.example" },
|
|
140
|
-
metadata: { description: "Acme's plugins", version: "3", pluginRoot: "./plugins" },
|
|
141
|
-
plugins: [
|
|
142
|
-
{ name: "alpha", source: "./alpha", description: "Alpha." },
|
|
143
|
-
{ name: "remote", source: { source: "github", repo: "acme/remote" } },
|
|
144
|
-
{ name: "subdir", source: { source: "git-subdir", url: "acme/mono", path: "plugins/x" } },
|
|
145
|
-
],
|
|
146
|
-
},
|
|
147
|
-
{ "plugins/alpha": alpha },
|
|
148
|
-
),
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
it("reads metadata.description and metadata.pluginRoot, drops remote sources with one warning each", () => {
|
|
152
|
-
expect(kindsOf(outcome)).toEqual(warnings("entry-source-unsupported", "entry-source-unsupported"));
|
|
153
|
-
const marketplace = accepted(outcome);
|
|
154
|
-
expect(marketplace.dialect).toBe("claude");
|
|
155
|
-
expect(marketplace.description).toBe("Acme's plugins");
|
|
156
|
-
expect(marketplace.owner).toEqual({ name: "Acme", email: "plugins@acme.example" });
|
|
157
|
-
expect(marketplace.plugins).toEqual([{ name: "alpha", dir: "plugins/alpha", description: "Alpha." }]);
|
|
158
|
-
const messages = outcome.warnings.map((w) => w.message);
|
|
159
|
-
expect(messages).toContain(
|
|
160
|
-
"plugin 'remote' in '.claude-plugin/marketplace.json' has source 'github', a form Stigmer does not fetch (only a directory inside the marketplace); not offered",
|
|
161
|
-
);
|
|
162
|
-
expect(messages).toContain(
|
|
163
|
-
"plugin 'subdir' in '.claude-plugin/marketplace.json' has source 'git-subdir', a form Stigmer does not fetch (only a directory inside the marketplace); not offered",
|
|
164
|
-
);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe("the Codex file (.agents/plugins/marketplace.json)", () => {
|
|
169
|
-
const outcome = readMarketplace(
|
|
170
|
-
tree(
|
|
171
|
-
".agents/plugins/marketplace.json",
|
|
172
|
-
{
|
|
173
|
-
name: "local",
|
|
174
|
-
interface: { displayName: "Local Plugins" },
|
|
175
|
-
plugins: [
|
|
176
|
-
{ name: "alpha", source: { source: "local", path: "./plugins/alpha" }, policy: { installation: "INSTALLED_BY_DEFAULT", authentication: "ON_INSTALL" }, category: "Productivity" },
|
|
177
|
-
{ name: "beta", source: { source: "local", path: "./plugins/beta" }, policy: { installation: "AVAILABLE", authentication: "ON_USE" } },
|
|
178
|
-
],
|
|
179
|
-
},
|
|
180
|
-
{ "plugins/alpha": alpha, "plugins/beta": beta },
|
|
181
|
-
),
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
it("reads local sources and offers an INSTALLED_BY_DEFAULT entry like any other; the display name is not a description", () => {
|
|
185
|
-
expect(kindsOf(outcome)).toEqual({ errors: [], warnings: [] });
|
|
186
|
-
const marketplace = accepted(outcome);
|
|
187
|
-
expect(marketplace.dialect).toBe("codex");
|
|
188
|
-
expect(marketplace.description).toBeUndefined();
|
|
189
|
-
expect(marketplace.plugins.map((p) => p.dir)).toEqual(["plugins/alpha", "plugins/beta"]);
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
describe("the Cursor file, verbatim from cursor/plugins at c1c0a32, over a partial tree", () => {
|
|
194
|
-
// The six vendored plugin directories mounted where the catalogue names
|
|
195
|
-
// them; the marketplace file byte-for-byte. Cursor writes sources without
|
|
196
|
-
// a `./` prefix and nests third-party plugins under `third_party/`.
|
|
197
|
-
const mounts: Readonly<Record<string, string>> = {
|
|
198
|
-
thermos: "thermos",
|
|
199
|
-
advisor: "advisor",
|
|
200
|
-
"third_party/github": "github",
|
|
201
|
-
"third_party/xero": "xero",
|
|
202
|
-
"third_party/playwright": "playwright",
|
|
203
|
-
"third_party/salesforce": "salesforce",
|
|
204
|
-
};
|
|
205
|
-
const catalogue = directoryPluginFiles(`${FIXTURES}.cursor-plugin`);
|
|
206
|
-
const files = new Map<string, Uint8Array>();
|
|
207
|
-
files.set(".cursor-plugin/marketplace.json", catalogue.read("marketplace.json"));
|
|
208
|
-
for (const [dir, fixture] of Object.entries(mounts)) {
|
|
209
|
-
const plugin = directoryPluginFiles(`${FIXTURES}${fixture}`);
|
|
210
|
-
for (const entry of plugin.entries) files.set(`${dir}/${entry.path}`, plugin.read(entry.path));
|
|
211
|
-
}
|
|
212
|
-
const outcome = readMarketplace(inMemoryPluginFiles(files));
|
|
213
|
-
|
|
214
|
-
it("offers the six the tree holds and reports the seventy-three it does not, one warning each", () => {
|
|
215
|
-
const marketplace = accepted(outcome);
|
|
216
|
-
expect(marketplace.name).toBe("cursor-plugins");
|
|
217
|
-
expect(marketplace.dialect).toBe("cursor");
|
|
218
|
-
expect(marketplace.owner).toEqual({ name: "Cursor", email: "plugins@cursor.com" });
|
|
219
|
-
expect(marketplace.description).toBe("Official Cursor plugin marketplace: developer tools, framework rules, MCP integrations, and agent skills");
|
|
220
|
-
expect(marketplace.plugins.map((p) => p.name).sort()).toEqual(["advisor", "github", "playwright", "salesforce", "thermos", "xero"]);
|
|
221
|
-
expect(marketplace.plugins.find((p) => p.name === "github")).toEqual({
|
|
222
|
-
name: "github",
|
|
223
|
-
dir: "third_party/github",
|
|
224
|
-
description: "Manage repos, issues, pull requests, and Actions.",
|
|
225
|
-
});
|
|
226
|
-
expect(outcome.warnings).toHaveLength(73);
|
|
227
|
-
expect(new Set(outcome.warnings.map((w) => w.kind))).toEqual(new Set(["entry-directory-missing"]));
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("read as a file alone, declares all seventy-nine with no directory warnings: the CLI's bare-name peek", () => {
|
|
231
|
-
const fileOnly = readMarketplaceFile(
|
|
232
|
-
inMemoryPluginFiles(new Map([[".cursor-plugin/marketplace.json", files.get(".cursor-plugin/marketplace.json")!]])),
|
|
233
|
-
);
|
|
234
|
-
const declared = accepted(fileOnly);
|
|
235
|
-
expect(declared.plugins).toHaveLength(79);
|
|
236
|
-
expect(declared.plugins.map((p) => p.name)).toContain("thermos");
|
|
237
|
-
expect(fileOnly.warnings.filter((w) => w.kind === "entry-directory-missing")).toEqual([]);
|
|
238
|
-
// Still refused for what the file itself gets wrong, exactly as the tree read is.
|
|
239
|
-
expect(readMarketplaceFile(inMemoryPluginFiles(new Map([["README.md", "x"]]))).ok).toBe(false);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("offers every entry when the whole catalogue is present (the sources all resolve inside the root)", () => {
|
|
243
|
-
// Every source directory given one manifest, so the file's own shape is
|
|
244
|
-
// proven independently of which plugins the fixtures vendor.
|
|
245
|
-
const complete = new Map<string, Uint8Array | string>([[".cursor-plugin/marketplace.json", catalogue.read("marketplace.json")]]);
|
|
246
|
-
const parsed = JSON.parse(new TextDecoder().decode(catalogue.read("marketplace.json"))) as { plugins: { name: string; source: string }[] };
|
|
247
|
-
for (const entry of parsed.plugins) {
|
|
248
|
-
for (const [path, bytes] of cursorPlugin({ name: entry.name })) complete.set(`${entry.source}/${path}`, bytes);
|
|
249
|
-
}
|
|
250
|
-
const whole = readMarketplace(inMemoryPluginFiles(complete));
|
|
251
|
-
expect(kindsOf(whole)).toEqual({ errors: [], warnings: [] });
|
|
252
|
-
expect(accepted(whole).plugins).toHaveLength(79);
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
interface Case {
|
|
257
|
-
readonly files: PluginFiles;
|
|
258
|
-
/** Every kind the read produces, sorted. */
|
|
259
|
-
readonly kinds: Kinds;
|
|
260
|
-
/** The sentence of the kind under test (a string, or a regex source for a parser's own text). */
|
|
261
|
-
readonly message: string;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const ERROR_CASES: Record<MarketplaceErrorKind, Case> = {
|
|
265
|
-
"marketplace-not-found": {
|
|
266
|
-
files: inMemoryPluginFiles(new Map([["README.md", "no marketplace here"]])),
|
|
267
|
-
kinds: errors("marketplace-not-found"),
|
|
268
|
-
message:
|
|
269
|
-
"no marketplace file found: expected one of 'marketplace.json', '.claude-plugin/marketplace.json', '.cursor-plugin/marketplace.json', '.agents/plugins/marketplace.json'",
|
|
270
|
-
},
|
|
271
|
-
"marketplace-too-large": {
|
|
272
|
-
files: inMemoryPluginFiles(new Map([[STIGMER_FILE, new Uint8Array(PLUGIN_DOCUMENT_LIMITS.marketplace + 1)]])),
|
|
273
|
-
kinds: errors("marketplace-too-large"),
|
|
274
|
-
message: `'marketplace.json' is ${PLUGIN_DOCUMENT_LIMITS.marketplace + 1} bytes, above the ${PLUGIN_DOCUMENT_LIMITS.marketplace} byte cap for a marketplace file`,
|
|
275
|
-
},
|
|
276
|
-
"marketplace-unreadable": {
|
|
277
|
-
files: tree(STIGMER_FILE, "{ oops"),
|
|
278
|
-
kinds: errors("marketplace-unreadable"),
|
|
279
|
-
message: /^'marketplace.json' is not a readable marketplace file: /.source,
|
|
280
|
-
},
|
|
281
|
-
"marketplace-field-type": {
|
|
282
|
-
files: tree(STIGMER_FILE, { name: 7, plugins: [] }),
|
|
283
|
-
kinds: errors("marketplace-field-type"),
|
|
284
|
-
message: "'marketplace.json' field 'name' must be a string",
|
|
285
|
-
},
|
|
286
|
-
"marketplace-name-missing": {
|
|
287
|
-
files: tree(STIGMER_FILE, { plugins: [] }),
|
|
288
|
-
kinds: errors("marketplace-name-missing"),
|
|
289
|
-
message: "'marketplace.json' has no 'name'; a marketplace needs one so its plugins can be addressed as '<marketplace>/<plugin>'",
|
|
290
|
-
},
|
|
291
|
-
"marketplace-name-invalid": {
|
|
292
|
-
files: tree(STIGMER_FILE, { name: "Acme Tools", plugins: [] }),
|
|
293
|
-
kinds: errors("marketplace-name-invalid"),
|
|
294
|
-
message: "'marketplace.json' name 'Acme Tools' is invalid: 1 to 64 characters of a-z, 0-9, '-' and '.', starting and ending alphanumeric, no '--' or '..'",
|
|
295
|
-
},
|
|
296
|
-
"marketplace-plugins-missing": {
|
|
297
|
-
files: tree(STIGMER_FILE, { name: "acme" }),
|
|
298
|
-
kinds: errors("marketplace-plugins-missing"),
|
|
299
|
-
message: "'marketplace.json' has no 'plugins' list",
|
|
300
|
-
},
|
|
301
|
-
"entry-shape": {
|
|
302
|
-
files: tree(STIGMER_FILE, { name: "acme", plugins: ["alpha"] }),
|
|
303
|
-
kinds: errors("entry-shape"),
|
|
304
|
-
message: "'marketplace.json' plugins[0] must be an object with 'name' and 'source'",
|
|
305
|
-
},
|
|
306
|
-
"entry-name-missing": {
|
|
307
|
-
files: tree(STIGMER_FILE, { name: "acme", plugins: [{ source: "./alpha" }] }),
|
|
308
|
-
kinds: errors("entry-name-missing"),
|
|
309
|
-
message: "'marketplace.json' plugins[0] has no 'name'",
|
|
310
|
-
},
|
|
311
|
-
"entry-name-invalid": {
|
|
312
|
-
files: tree(STIGMER_FILE, { name: "acme", plugins: [{ name: "Alpha!", source: "./alpha" }] }),
|
|
313
|
-
kinds: errors("entry-name-invalid"),
|
|
314
|
-
message: "'marketplace.json' plugin 'Alpha!' has an invalid name: 1 to 64 characters of a-z, 0-9, '-' and '.', starting and ending alphanumeric, no '--' or '..'",
|
|
315
|
-
},
|
|
316
|
-
"entry-name-duplicate": {
|
|
317
|
-
files: tree(
|
|
318
|
-
STIGMER_FILE,
|
|
319
|
-
{ name: "acme", plugins: [{ name: "alpha", source: "./alpha" }, { name: "alpha", source: "./beta" }] },
|
|
320
|
-
{ alpha, beta },
|
|
321
|
-
),
|
|
322
|
-
kinds: errors("entry-name-duplicate"),
|
|
323
|
-
message: "'marketplace.json' lists plugin 'alpha' more than once; 'install alpha' would be ambiguous",
|
|
324
|
-
},
|
|
325
|
-
"entry-source-missing": {
|
|
326
|
-
files: tree(STIGMER_FILE, { name: "acme", plugins: [{ name: "alpha" }] }),
|
|
327
|
-
kinds: errors("entry-source-missing"),
|
|
328
|
-
message: "'marketplace.json' plugin 'alpha' has no 'source'",
|
|
329
|
-
},
|
|
330
|
-
"entry-source-escapes-root": {
|
|
331
|
-
files: tree(STIGMER_FILE, { name: "acme", plugins: [{ name: "alpha", source: "../alpha" }] }),
|
|
332
|
-
kinds: errors("entry-source-escapes-root"),
|
|
333
|
-
message: "'marketplace.json' plugin 'alpha' has source '../alpha' outside the marketplace root; a source is a directory inside the marketplace",
|
|
334
|
-
},
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
const WARNING_CASES: Record<MarketplaceWarningKind, Case> = {
|
|
338
|
-
"entry-source-unsupported": {
|
|
339
|
-
files: tree(STIGMER_FILE, { name: "acme", plugins: [{ name: "alpha", source: "https://example.com/alpha.zip" }] }),
|
|
340
|
-
kinds: warnings("entry-source-unsupported"),
|
|
341
|
-
message:
|
|
342
|
-
"plugin 'alpha' in 'marketplace.json' has source 'https://example.com/alpha.zip', a form Stigmer does not fetch (only a directory inside the marketplace); not offered",
|
|
343
|
-
},
|
|
344
|
-
"entry-directory-missing": {
|
|
345
|
-
files: tree(STIGMER_FILE, { name: "acme", plugins: [{ name: "alpha", source: "./alpha" }] }),
|
|
346
|
-
kinds: warnings("entry-directory-missing"),
|
|
347
|
-
message: "plugin 'alpha' in 'marketplace.json' names directory 'alpha', which the marketplace does not contain; not offered",
|
|
348
|
-
},
|
|
349
|
-
"entry-not-a-plugin": {
|
|
350
|
-
files: tree(STIGMER_FILE, { name: "acme", plugins: [{ name: "alpha", source: "./alpha" }] }, { alpha: new Map([["README.md", "not a plugin"]]) }),
|
|
351
|
-
kinds: warnings("entry-not-a-plugin"),
|
|
352
|
-
message: "plugin 'alpha' in 'marketplace.json' names directory 'alpha', which holds no plugin manifest; not offered",
|
|
353
|
-
},
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
describe("adversarial marketplaces: one fixture per error kind", () => {
|
|
357
|
-
for (const [kind, testCase] of Object.entries(ERROR_CASES) as [MarketplaceErrorKind, Case][]) {
|
|
358
|
-
it(`${kind}: refused with its sentence and nothing else`, () => {
|
|
359
|
-
const outcome = readMarketplace(testCase.files);
|
|
360
|
-
expect(kindsOf(outcome)).toEqual(testCase.kinds);
|
|
361
|
-
const finding = findingOf(refused(outcome), kind);
|
|
362
|
-
if (testCase.message.startsWith("^")) expect(finding.message).toMatch(new RegExp(testCase.message));
|
|
363
|
-
else expect(finding.message).toBe(testCase.message);
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
describe("adversarial marketplaces: one fixture per warning kind", () => {
|
|
369
|
-
for (const [kind, testCase] of Object.entries(WARNING_CASES) as [MarketplaceWarningKind, Case][]) {
|
|
370
|
-
it(`${kind}: the entry is dropped with its sentence and the marketplace is still offered`, () => {
|
|
371
|
-
const outcome = readMarketplace(testCase.files);
|
|
372
|
-
expect(kindsOf(outcome)).toEqual(testCase.kinds);
|
|
373
|
-
const marketplace = accepted(outcome);
|
|
374
|
-
expect(marketplace.plugins).toEqual([]);
|
|
375
|
-
expect(findingOf(outcome.warnings, kind).message).toBe(testCase.message);
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
});
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pins the MCP server normalisation on its accepting paths: the two
|
|
3
|
-
* transports in both the open and the vendor dialects, transport inference,
|
|
4
|
-
* `sse` mapped with its warning, the `${user_config.KEY}` rewrite, header
|
|
5
|
-
* and argument references collected onto `env`, the Cursor `auth` block
|
|
6
|
-
* warned and scanned, inline and multiple sources, and the URL rule's
|
|
7
|
-
* loopback exception. Refusals are pinned kind by kind in the adversarial
|
|
8
|
-
* suite.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, expect, it } from "vitest";
|
|
12
|
-
|
|
13
|
-
import { claudePlugin, cursorPlugin, openPlugin } from "../testing.js";
|
|
14
|
-
import { accepted, findingOf, kindsOf, read } from "../__test-utils__/read.js";
|
|
15
|
-
|
|
16
|
-
describe("open dialect", () => {
|
|
17
|
-
it("reads a stdio and a streamable-http server into the two spec shapes", () => {
|
|
18
|
-
const plugin = accepted(
|
|
19
|
-
read(
|
|
20
|
-
openPlugin({
|
|
21
|
-
mcpServers: {
|
|
22
|
-
files: { type: "stdio", command: "npx", args: ["-y", "@acme/files", "--token", "${TOKEN}"], env: { TOKEN: "${TOKEN}" } },
|
|
23
|
-
api: { type: "streamable-http", url: "https://api.example.com/mcp", headers: { Authorization: "Bearer ${API_KEY}", "X-Org": "acme" } },
|
|
24
|
-
},
|
|
25
|
-
}),
|
|
26
|
-
),
|
|
27
|
-
);
|
|
28
|
-
expect(plugin.mcpServers).toEqual([
|
|
29
|
-
{ name: "files", transport: "stdio", command: "npx", args: ["-y", "@acme/files", "--token", "${TOKEN}"], env: ["TOKEN"] },
|
|
30
|
-
{ name: "api", transport: "http", url: "https://api.example.com/mcp", headers: { Authorization: "Bearer ${API_KEY}", "X-Org": "acme" }, env: ["API_KEY"] },
|
|
31
|
-
]);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("maps the legacy sse transport to http with the fallback warning", () => {
|
|
35
|
-
const outcome = read(openPlugin({ mcpServers: { legacy: { type: "sse", url: "https://legacy.example.com/sse" } } }));
|
|
36
|
-
expect(kindsOf(outcome).warnings).toEqual(["mcp-server-sse-mapped"]);
|
|
37
|
-
expect(accepted(outcome).mcpServers[0]).toMatchObject({ transport: "http", url: "https://legacy.example.com/sse" });
|
|
38
|
-
expect(findingOf(outcome.warnings, "mcp-server-sse-mapped").message).toContain("falls back to SSE only when the server rejects it");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("allows plain http for loopback hosts only", () => {
|
|
42
|
-
const outcome = read(openPlugin({ mcpServers: { local: { type: "streamable-http", url: "http://localhost:8080/mcp" }, ip: { type: "streamable-http", url: "http://127.0.0.1/mcp" } } }));
|
|
43
|
-
expect(accepted(outcome).mcpServers).toHaveLength(2);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe("vendor dialects", () => {
|
|
48
|
-
it("infers the transport from command or url when type is absent", () => {
|
|
49
|
-
const plugin = accepted(
|
|
50
|
-
read(
|
|
51
|
-
cursorPlugin({
|
|
52
|
-
mcpServers: {
|
|
53
|
-
play: { command: "npx", args: ["-y", "@playwright/mcp@latest"] },
|
|
54
|
-
web: { url: "https://mcp.example.com/" },
|
|
55
|
-
explicit: { type: "http", url: "https://mcp.example.com/two" },
|
|
56
|
-
},
|
|
57
|
-
}),
|
|
58
|
-
),
|
|
59
|
-
);
|
|
60
|
-
expect(plugin.mcpServers.map((s) => [s.name, s.transport])).toEqual([
|
|
61
|
-
["play", "stdio"],
|
|
62
|
-
["web", "http"],
|
|
63
|
-
["explicit", "http"],
|
|
64
|
-
]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("rewrites Claude's ${user_config.KEY} to ${KEY} in headers, args and env", () => {
|
|
68
|
-
const plugin = accepted(
|
|
69
|
-
read(
|
|
70
|
-
claudePlugin({
|
|
71
|
-
userConfig: { API_KEY: { type: "string", sensitive: true, required: true }, DB: { type: "string" } },
|
|
72
|
-
mcpServers: {
|
|
73
|
-
api: { url: "https://api.example.com/mcp", headers: { Authorization: "Bearer ${user_config.API_KEY}" } },
|
|
74
|
-
db: { command: "uvx", args: ["db-mcp", "--name", "${user_config.DB}"], env: { DB: "${user_config.DB}" } },
|
|
75
|
-
},
|
|
76
|
-
}),
|
|
77
|
-
),
|
|
78
|
-
);
|
|
79
|
-
expect(plugin.mcpServers[0]).toMatchObject({ headers: { Authorization: "Bearer ${API_KEY}" }, env: ["API_KEY"] });
|
|
80
|
-
expect(plugin.mcpServers[1]).toMatchObject({ args: ["db-mcp", "--name", "${DB}"], env: ["DB"] });
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("warns on Cursor's auth block and still counts the variables inside it as references", () => {
|
|
84
|
-
const outcome = read(
|
|
85
|
-
cursorPlugin({
|
|
86
|
-
variables: { CLIENT_ID: { type: "string" } },
|
|
87
|
-
required: ["CLIENT_ID"],
|
|
88
|
-
mcpServers: { crm: { type: "http", url: "https://crm.example.com/mcp", auth: { CLIENT_ID: "${CLIENT_ID}", scopes: ["a"] } } },
|
|
89
|
-
}),
|
|
90
|
-
);
|
|
91
|
-
expect(kindsOf(outcome)).toEqual({ errors: [], warnings: ["mcp-server-auth-ignored"] });
|
|
92
|
-
expect(accepted(outcome).mcpServers[0]?.env).toEqual(["CLIENT_ID"]);
|
|
93
|
-
expect(findingOf(outcome.warnings, "mcp-server-auth-ignored").message).toContain("ai.stigmer/mcp-servers/crm.yaml");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("warns on a field Stigmer does not read instead of refusing", () => {
|
|
97
|
-
const outcome = read(cursorPlugin({ mcpServers: { s: { type: "http", url: "https://x.example.com/mcp", timeout: 30 } } }));
|
|
98
|
-
expect(kindsOf(outcome)).toEqual({ errors: [], warnings: ["mcp-server-field-ignored"] });
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("reads an inline mcpServers object and a declared file together", () => {
|
|
102
|
-
const files = cursorPlugin({ mcpServers: { fromFile: { type: "http", url: "https://a.example.com/mcp" } } });
|
|
103
|
-
const manifest = JSON.parse(files.get(".cursor-plugin/plugin.json") as string) as Record<string, unknown>;
|
|
104
|
-
manifest["mcpServers"] = ["./mcp.json", { mcpServers: { inline: { type: "http", url: "https://b.example.com/mcp" } } }];
|
|
105
|
-
files.set(".cursor-plugin/plugin.json", JSON.stringify(manifest));
|
|
106
|
-
const plugin = accepted(read(files));
|
|
107
|
-
expect(plugin.mcpServers.map((s) => s.name)).toEqual(["fromFile", "inline"]);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("reads Claude's default .mcp.json when the manifest declares no mcpServers", () => {
|
|
111
|
-
const plugin = accepted(read(claudePlugin({ mcpServers: { s: { command: "npx", args: ["x"] } } })));
|
|
112
|
-
expect(plugin.mcpServers.map((s) => s.name)).toEqual(["s"]);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("warns on a top-level .mcp.json field it does not read, where the open format would refuse", () => {
|
|
116
|
-
const files = claudePlugin({ mcpServers: { s: { command: "npx" } } });
|
|
117
|
-
files.set(".mcp.json", JSON.stringify({ mcpServers: { s: { command: "npx" } }, comment: "x" }));
|
|
118
|
-
expect(kindsOf(read(files))).toEqual({ errors: [], warnings: ["mcp-config-field-ignored"] });
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pins the `ai.stigmer/` overlay (the three document shapes located and
|
|
3
|
-
* handed over as bytes with the server check) and the ignored-component
|
|
4
|
-
* record (directories and files on disk, manifest fields, deduplicated,
|
|
5
|
-
* never per file, with hook scripts never read).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, expect, it } from "vitest";
|
|
9
|
-
|
|
10
|
-
import { cursorPlugin, openPlugin } from "../testing.js";
|
|
11
|
-
import { accepted, kindsOf, read } from "../__test-utils__/read.js";
|
|
12
|
-
|
|
13
|
-
describe("the ai.stigmer/ overlay", () => {
|
|
14
|
-
it("locates the agent, workflow and server documents and hands them over as bytes", () => {
|
|
15
|
-
const files = openPlugin({
|
|
16
|
-
mcpServers: { gh: { type: "streamable-http", url: "https://gh.example.com/mcp" } },
|
|
17
|
-
files: {
|
|
18
|
-
"ai.stigmer/agent.yaml": "kind: Agent\n",
|
|
19
|
-
"ai.stigmer/workflows/triage.yaml": "kind: Workflow\n",
|
|
20
|
-
"ai.stigmer/mcp-servers/gh.yaml": "spec:\n auth: {}\n",
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
const overlay = accepted(read(files)).overlay;
|
|
24
|
-
expect(overlay.agent?.path).toBe("ai.stigmer/agent.yaml");
|
|
25
|
-
expect(new TextDecoder().decode(overlay.agent?.bytes)).toBe("kind: Agent\n");
|
|
26
|
-
expect(overlay.workflows.map((w) => [w.name, w.path])).toEqual([["triage", "ai.stigmer/workflows/triage.yaml"]]);
|
|
27
|
-
expect(overlay.mcpServers.map((s) => [s.server, s.path])).toEqual([["gh", "ai.stigmer/mcp-servers/gh.yaml"]]);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("is empty when the folder is absent", () => {
|
|
31
|
-
expect(accepted(read(openPlugin())).overlay).toEqual({ workflows: [], mcpServers: [] });
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("ignored components", () => {
|
|
36
|
-
it("records each component directory and file once, and never reads inside them", () => {
|
|
37
|
-
const files = cursorPlugin({
|
|
38
|
-
manifest: { hooks: "./hooks/hooks.json", rules: "./rules/", logo: "assets/logo.png", minClientVersions: { cursor: "3.13.0" } },
|
|
39
|
-
files: {
|
|
40
|
-
"hooks/hooks.json": '{"hooks":{"stop":[{"command":"bash \\"${CURSOR_PLUGIN_ROOT}/hooks/stop.sh\\""}]}}',
|
|
41
|
-
"hooks/stop.sh": "exit 0",
|
|
42
|
-
"rules/one.mdc": "rule",
|
|
43
|
-
"rules/two.mdc": "rule",
|
|
44
|
-
"assets/logo.png": new Uint8Array([0x89, 0x50]),
|
|
45
|
-
".lsp.json": "{}",
|
|
46
|
-
"settings.json": "{}",
|
|
47
|
-
"README.md": "readme",
|
|
48
|
-
"LICENSE": "MIT",
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
const outcome = read(files);
|
|
52
|
-
expect(kindsOf(outcome)).toEqual({ errors: [], warnings: [] });
|
|
53
|
-
expect(accepted(outcome).ignored).toEqual([
|
|
54
|
-
{ kind: "assets", path: "assets/" },
|
|
55
|
-
{ kind: "hooks", path: "hooks/" },
|
|
56
|
-
{ kind: "rules", path: "rules/" },
|
|
57
|
-
{ kind: "lsp-servers", path: ".lsp.json" },
|
|
58
|
-
{ kind: "settings", path: "settings.json" },
|
|
59
|
-
{ kind: "hooks", path: ".cursor-plugin/plugin.json#hooks" },
|
|
60
|
-
{ kind: "rules", path: ".cursor-plugin/plugin.json#rules" },
|
|
61
|
-
{ kind: "logo", path: ".cursor-plugin/plugin.json#logo" },
|
|
62
|
-
{ kind: "min-client-versions", path: ".cursor-plugin/plugin.json#minClientVersions" },
|
|
63
|
-
]);
|
|
64
|
-
});
|
|
65
|
-
});
|