@stigmer/plugin-package 3.35.0 → 3.37.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,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A test-only `PluginFiles` over a directory on disk, for the vendored
|
|
3
|
-
* fixtures. Sorted, symlinks skipped, sizes from `stat`, the contract the
|
|
4
|
-
* CLI's walker honours; kept here (not in the library) because the
|
|
5
|
-
* library's main entry imports no `node:*` module and the CLI owns the
|
|
6
|
-
* real walker with its ignore rules.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
|
|
12
|
-
import { comparePaths, type PluginFileEntry, type PluginFiles } from "../files.js";
|
|
13
|
-
|
|
14
|
-
export function directoryPluginFiles(root: string): PluginFiles {
|
|
15
|
-
const entries: PluginFileEntry[] = [];
|
|
16
|
-
const walk = (dir: string, prefix: string): void => {
|
|
17
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
18
|
-
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
19
|
-
if (entry.isDirectory()) walk(join(dir, entry.name), path);
|
|
20
|
-
else if (entry.isFile()) entries.push({ path, size: statSync(join(dir, entry.name)).size });
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
walk(root, "");
|
|
24
|
-
entries.sort((a, b) => comparePaths(a.path, b.path));
|
|
25
|
-
return {
|
|
26
|
-
entries,
|
|
27
|
-
read: (path) => new Uint8Array(readFileSync(join(root, ...path.split("/")))),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test helpers: read a fixture map, name the finding kinds an outcome
|
|
3
|
-
* carries, and unwrap the arms with a diagnostic when the other arm came
|
|
4
|
-
* back. Kinds are returned SORTED and complete (errors and warnings both),
|
|
5
|
-
* so an adversarial test asserts everything the reader said and a warning
|
|
6
|
-
* that appears beside the error under test is named, never tolerated by
|
|
7
|
-
* omission.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { expect } from "vitest";
|
|
11
|
-
|
|
12
|
-
import { inMemoryPluginFiles } from "../files.js";
|
|
13
|
-
import type { Finding, PluginFinding, PluginReadOutcome } from "../outcome.js";
|
|
14
|
-
import { readPluginPackage } from "../read-plugin-package.js";
|
|
15
|
-
import type { PluginFixture } from "../testing.js";
|
|
16
|
-
import type { PluginPackage } from "../types.js";
|
|
17
|
-
|
|
18
|
-
export function read(files: PluginFixture): PluginReadOutcome {
|
|
19
|
-
return readPluginPackage(inMemoryPluginFiles(files));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface Kinds {
|
|
23
|
-
readonly errors: readonly string[];
|
|
24
|
-
readonly warnings: readonly string[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function kindsOf(outcome: PluginReadOutcome): Kinds {
|
|
28
|
-
const sorted = (findings: readonly PluginFinding[]): string[] => findings.map((f) => f.kind).sort();
|
|
29
|
-
return outcome.ok
|
|
30
|
-
? { errors: [], warnings: sorted(outcome.warnings) }
|
|
31
|
-
: { errors: sorted(outcome.errors), warnings: sorted(outcome.warnings) };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** The plugin, failing with every finding's sentence when the read refused. */
|
|
35
|
-
export function accepted(outcome: PluginReadOutcome): PluginPackage {
|
|
36
|
-
if (!outcome.ok) {
|
|
37
|
-
expect.fail(`expected an accepted plugin, got refusals:\n${outcome.errors.map((f) => ` ${f.kind}: ${f.message}`).join("\n")}`);
|
|
38
|
-
}
|
|
39
|
-
return outcome.plugin;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** The errors, failing when the read accepted. */
|
|
43
|
-
export function refused(outcome: PluginReadOutcome): readonly PluginFinding[] {
|
|
44
|
-
if (outcome.ok) {
|
|
45
|
-
expect.fail(`expected a refusal, got an accepted plugin '${outcome.plugin.name}'`);
|
|
46
|
-
}
|
|
47
|
-
return outcome.errors;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** The one finding of `kind`, failing when it is absent or repeated; over either vocabulary. */
|
|
51
|
-
export function findingOf<K extends string>(findings: readonly Finding<K>[], kind: string): Finding<K> {
|
|
52
|
-
const matches = findings.filter((f) => f.kind === kind);
|
|
53
|
-
expect(matches, `exactly one '${kind}' finding`).toHaveLength(1);
|
|
54
|
-
return matches[0] as Finding<K>;
|
|
55
|
-
}
|
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The adversarial suite: one fixture per finding kind, each a valid plugin
|
|
3
|
-
* with exactly one thing broken, asserting the COMPLETE sorted list of
|
|
4
|
-
* kinds the reader produced (errors and warnings both) and the sentence of
|
|
5
|
-
* the kind under test. A warning that legitimately accompanies an error is
|
|
6
|
-
* named in its case, never tolerated by omission.
|
|
7
|
-
*
|
|
8
|
-
* The two tables are `Record`s over the kind unions, so a kind added to
|
|
9
|
-
* `outcome.ts` without a case here does not compile, and a case for a kind
|
|
10
|
-
* that no longer exists does not either. Together with `messages.ts` this
|
|
11
|
-
* is the contract: every kind, one sentence, one fixture.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, expect, it } from "vitest";
|
|
15
|
-
|
|
16
|
-
import { PLUGIN_DOCUMENT_LIMITS } from "../files.js";
|
|
17
|
-
import type { PluginErrorKind, PluginWarningKind } from "../outcome.js";
|
|
18
|
-
import { claudePlugin, cursorPlugin, openPlugin, type PluginFixture, withFile, withManifestField, withoutFile } from "../testing.js";
|
|
19
|
-
import { findingOf, type Kinds, kindsOf, read } from "../__test-utils__/read.js";
|
|
20
|
-
|
|
21
|
-
interface Case {
|
|
22
|
-
readonly files: PluginFixture;
|
|
23
|
-
/** Every kind the read produces, sorted. */
|
|
24
|
-
readonly kinds: Kinds;
|
|
25
|
-
/** The sentence of the kind under test. */
|
|
26
|
-
readonly message: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const OPEN_MANIFEST_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
|
30
|
-
const OPEN_MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
|
|
31
|
-
const HTTP = { type: "streamable-http", url: "https://mcp.example.com/mcp" } as const;
|
|
32
|
-
const CURSOR_HTTP = { type: "http", url: "https://mcp.example.com/mcp" } as const;
|
|
33
|
-
|
|
34
|
-
const errors = (...kinds: PluginErrorKind[]): Kinds => ({ errors: [...kinds].sort(), warnings: [] });
|
|
35
|
-
const withWarnings = (kinds: Kinds, ...warnings: PluginWarningKind[]): Kinds => ({ errors: kinds.errors, warnings: [...warnings].sort() });
|
|
36
|
-
|
|
37
|
-
const ERROR_CASES: Record<PluginErrorKind, Case> = {
|
|
38
|
-
"no-manifest": {
|
|
39
|
-
files: new Map([["README.md", "no manifest here"]]),
|
|
40
|
-
kinds: errors("no-manifest"),
|
|
41
|
-
message: "no plugin manifest found: expected one of 'plugin.json', '.claude-plugin/plugin.json', '.cursor-plugin/plugin.json', '.codex-plugin/plugin.json'",
|
|
42
|
-
},
|
|
43
|
-
"manifest-unreadable": {
|
|
44
|
-
files: new Map([["plugin.json", "{ oops"]]),
|
|
45
|
-
kinds: errors("manifest-unreadable"),
|
|
46
|
-
message: /^manifest 'plugin.json' is not valid JSON: /.source,
|
|
47
|
-
},
|
|
48
|
-
"manifest-schema-missing": {
|
|
49
|
-
files: openPlugin({ schema: null }),
|
|
50
|
-
kinds: errors("manifest-schema-missing"),
|
|
51
|
-
message: `manifest 'plugin.json' is missing the required '$schema' field; an Agent Plugins manifest declares '${OPEN_MANIFEST_SCHEMA}'`,
|
|
52
|
-
},
|
|
53
|
-
"manifest-schema-unsupported": {
|
|
54
|
-
files: openPlugin({ schema: "https://agent-plugins.org/schemas/9.0.0/plugin.schema.json" }),
|
|
55
|
-
kinds: errors("manifest-schema-unsupported"),
|
|
56
|
-
message: `manifest 'plugin.json' declares an unsupported '$schema' 'https://agent-plugins.org/schemas/9.0.0/plugin.schema.json'; this reader supports '${OPEN_MANIFEST_SCHEMA}'`,
|
|
57
|
-
},
|
|
58
|
-
"manifest-name-missing": {
|
|
59
|
-
files: withManifestField(openPlugin(), "plugin.json", "name", undefined),
|
|
60
|
-
kinds: errors("manifest-name-missing"),
|
|
61
|
-
message: "manifest 'plugin.json' is missing the required 'name' field",
|
|
62
|
-
},
|
|
63
|
-
"manifest-name-invalid": {
|
|
64
|
-
files: openPlugin({ name: "Has--Double" }),
|
|
65
|
-
kinds: errors("manifest-name-invalid"),
|
|
66
|
-
message: "plugin name 'Has--Double' in 'plugin.json' is invalid: 1 to 64 characters of lowercase letters, digits, hyphens and periods, starting and ending alphanumeric, with no '--' or '..'",
|
|
67
|
-
},
|
|
68
|
-
"manifest-name-conflict": {
|
|
69
|
-
files: new Map([...openPlugin({ name: "one" }), ...claudePlugin({ name: "two" })]),
|
|
70
|
-
kinds: errors("manifest-name-conflict"),
|
|
71
|
-
message: "manifests disagree on the plugin name: 'one' in 'plugin.json' versus 'two' in '.claude-plugin/plugin.json'",
|
|
72
|
-
},
|
|
73
|
-
"manifest-field-type": {
|
|
74
|
-
files: openPlugin({ manifest: { version: 3 } }),
|
|
75
|
-
kinds: errors("manifest-field-type"),
|
|
76
|
-
message: "field 'version' in 'plugin.json' has the wrong type: expected a string",
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
"path-not-relative": {
|
|
80
|
-
files: cursorPlugin({ manifest: { skills: "skills/" } }),
|
|
81
|
-
kinds: errors("path-not-relative"),
|
|
82
|
-
message: "path 'skills/' in '.cursor-plugin/plugin.json' must be plugin-relative and begin with './'",
|
|
83
|
-
},
|
|
84
|
-
"path-escapes-root": {
|
|
85
|
-
files: cursorPlugin({ manifest: { agents: "./../shared/agents/" } }),
|
|
86
|
-
kinds: errors("path-escapes-root"),
|
|
87
|
-
message: "path './../shared/agents/' in '.cursor-plugin/plugin.json' escapes the plugin root",
|
|
88
|
-
},
|
|
89
|
-
"path-glob-unsupported": {
|
|
90
|
-
files: cursorPlugin({ manifest: { skills: "./skills/**" } }),
|
|
91
|
-
kinds: errors("path-glob-unsupported"),
|
|
92
|
-
message: "path './skills/**' in '.cursor-plugin/plugin.json' is a glob pattern; declare directories and files by path",
|
|
93
|
-
},
|
|
94
|
-
"path-uncontained": {
|
|
95
|
-
files: withFile(openPlugin(), "/etc/passwd", "root"),
|
|
96
|
-
kinds: errors("path-uncontained"),
|
|
97
|
-
message: "the reader listed a path outside the plugin root: '/etc/passwd'",
|
|
98
|
-
},
|
|
99
|
-
"document-too-large": {
|
|
100
|
-
files: withFile(openPlugin(), "skills/big/SKILL.md", `---\nname: big\ndescription: d\n---\n${"x".repeat(PLUGIN_DOCUMENT_LIMITS.skillMd)}`),
|
|
101
|
-
kinds: errors("document-too-large"),
|
|
102
|
-
message: `'skills/big/SKILL.md' is ${PLUGIN_DOCUMENT_LIMITS.skillMd + "---\nname: big\ndescription: d\n---\n".length} bytes, over the ${PLUGIN_DOCUMENT_LIMITS.skillMd}-byte limit for this kind of document`,
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
"skill-frontmatter-missing": {
|
|
106
|
-
files: openPlugin({ skills: [{ name: "bare", frontmatter: null }] }),
|
|
107
|
-
kinds: errors("skill-frontmatter-missing"),
|
|
108
|
-
message: "'skills/bare/SKILL.md' must start with YAML frontmatter ('---')",
|
|
109
|
-
},
|
|
110
|
-
"skill-frontmatter-unclosed": {
|
|
111
|
-
files: withFile(openPlugin(), "skills/open/SKILL.md", "---\nname: open\ndescription: d\nbody without a closing fence"),
|
|
112
|
-
kinds: errors("skill-frontmatter-unclosed"),
|
|
113
|
-
message: "'skills/open/SKILL.md' frontmatter is not closed (missing the closing '---')",
|
|
114
|
-
},
|
|
115
|
-
"skill-frontmatter-unreadable": {
|
|
116
|
-
files: withFile(openPlugin(), "skills/broken/SKILL.md", "---\nname: [unclosed\n---\nbody"),
|
|
117
|
-
kinds: errors("skill-frontmatter-unreadable"),
|
|
118
|
-
message: /^'skills\/broken\/SKILL.md' frontmatter is not valid YAML: /.source,
|
|
119
|
-
},
|
|
120
|
-
"skill-name-invalid": {
|
|
121
|
-
files: openPlugin({ skills: [{ name: "Bad_Name", dir: "bad", description: "d" }] }),
|
|
122
|
-
kinds: errors("skill-name-invalid"),
|
|
123
|
-
message: "skill name 'Bad_Name' in 'skills/bad/SKILL.md' is invalid: lowercase letters, digits and hyphens, optionally dot-scoped, with every segment alphanumeric",
|
|
124
|
-
},
|
|
125
|
-
"skill-name-duplicate": {
|
|
126
|
-
files: openPlugin({ skills: [{ name: "same", dir: "a", description: "d" }, { name: "same", dir: "b", description: "d" }] }),
|
|
127
|
-
kinds: withWarnings(errors("skill-name-duplicate"), "skill-name-differs-from-directory", "skill-name-differs-from-directory"),
|
|
128
|
-
message: "skill name 'same' appears more than once (again in 'skills/b/SKILL.md')",
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
"mcp-config-unreadable": {
|
|
132
|
-
files: withFile(openPlugin(), "mcp.json", "not json"),
|
|
133
|
-
kinds: errors("mcp-config-unreadable"),
|
|
134
|
-
message: /^MCP configuration 'mcp.json' is not valid JSON: /.source,
|
|
135
|
-
},
|
|
136
|
-
"mcp-config-shape": {
|
|
137
|
-
files: withFile(openPlugin(), "mcp.json", JSON.stringify({ $schema: OPEN_MCP_SCHEMA, mcpServers: [] })),
|
|
138
|
-
kinds: errors("mcp-config-shape"),
|
|
139
|
-
message: "MCP configuration 'mcp.json' must be an object with an 'mcpServers' object",
|
|
140
|
-
},
|
|
141
|
-
"mcp-config-schema-missing": {
|
|
142
|
-
files: openPlugin({ mcpServers: { s: HTTP }, mcpSchema: null }),
|
|
143
|
-
kinds: errors("mcp-config-schema-missing"),
|
|
144
|
-
message: `MCP configuration 'mcp.json' is missing the required '$schema' field; an Agent Plugins configuration declares '${OPEN_MCP_SCHEMA}'`,
|
|
145
|
-
},
|
|
146
|
-
"mcp-config-schema-unsupported": {
|
|
147
|
-
files: openPlugin({ mcpServers: { s: HTTP }, mcpSchema: "https://agent-plugins.org/schemas/9.0.0/mcp.schema.json" }),
|
|
148
|
-
kinds: errors("mcp-config-schema-unsupported"),
|
|
149
|
-
message: `MCP configuration 'mcp.json' declares an unsupported '$schema' 'https://agent-plugins.org/schemas/9.0.0/mcp.schema.json'; this reader supports '${OPEN_MCP_SCHEMA}'`,
|
|
150
|
-
},
|
|
151
|
-
"mcp-config-field-unknown": {
|
|
152
|
-
files: openPlugin({ mcpServers: { s: HTTP }, mcpConfig: { comment: "x" } }),
|
|
153
|
-
kinds: errors("mcp-config-field-unknown"),
|
|
154
|
-
message: "MCP configuration 'mcp.json' has an unexpected top-level field 'comment'; only '$schema' and 'mcpServers' are allowed",
|
|
155
|
-
},
|
|
156
|
-
|
|
157
|
-
"mcp-server-shape": {
|
|
158
|
-
files: openPlugin({ mcpServers: { s: "not-an-object" } }),
|
|
159
|
-
kinds: errors("mcp-server-shape"),
|
|
160
|
-
message: "MCP server 's' in 'mcp.json' must be an object",
|
|
161
|
-
},
|
|
162
|
-
"mcp-server-type-missing": {
|
|
163
|
-
files: openPlugin({ mcpServers: { s: { url: "https://mcp.example.com/mcp" } } }),
|
|
164
|
-
kinds: errors("mcp-server-type-missing"),
|
|
165
|
-
message: "MCP server 's' in 'mcp.json' is missing the 'type' field the Agent Plugins format requires ('stdio', 'streamable-http' or 'sse')",
|
|
166
|
-
},
|
|
167
|
-
"mcp-server-transport-unknown": {
|
|
168
|
-
files: cursorPlugin({ mcpServers: { s: { args: ["x"] } } }),
|
|
169
|
-
kinds: errors("mcp-server-transport-unknown"),
|
|
170
|
-
message: "MCP server 's' in 'mcp.json' has no 'type' and neither a 'command' nor a 'url' to infer it from",
|
|
171
|
-
},
|
|
172
|
-
"mcp-server-type-ambiguous": {
|
|
173
|
-
files: cursorPlugin({ mcpServers: { s: { command: "npx", url: "https://mcp.example.com/mcp" } } }),
|
|
174
|
-
kinds: errors("mcp-server-type-ambiguous"),
|
|
175
|
-
message: "MCP server 's' in 'mcp.json' declares both 'command' and 'url'; a server is either stdio or HTTP",
|
|
176
|
-
},
|
|
177
|
-
"mcp-server-type-unknown": {
|
|
178
|
-
files: cursorPlugin({ mcpServers: { s: { type: "websocket", url: "wss://x" } } }),
|
|
179
|
-
kinds: errors("mcp-server-type-unknown"),
|
|
180
|
-
message: "MCP server 's' in 'mcp.json' has an unknown 'type' 'websocket'; expected 'stdio', 'http', 'streamable-http' or 'sse'",
|
|
181
|
-
},
|
|
182
|
-
"mcp-server-field-unknown": {
|
|
183
|
-
files: openPlugin({ mcpServers: { s: { ...HTTP, env: { A: "${A}" } } } }),
|
|
184
|
-
kinds: errors("mcp-server-field-unknown"),
|
|
185
|
-
message: "MCP server 's' in 'mcp.json' has a field 'env' that its transport does not define",
|
|
186
|
-
},
|
|
187
|
-
"mcp-server-field-type": {
|
|
188
|
-
files: openPlugin({ mcpServers: { s: { type: "stdio", command: "npx", args: "-y" } } }),
|
|
189
|
-
kinds: errors("mcp-server-field-type"),
|
|
190
|
-
message: "MCP server 's' in 'mcp.json' field 'args' has the wrong type",
|
|
191
|
-
},
|
|
192
|
-
"mcp-server-url-missing": {
|
|
193
|
-
files: openPlugin({ mcpServers: { s: { type: "streamable-http" } } }),
|
|
194
|
-
kinds: errors("mcp-server-url-missing"),
|
|
195
|
-
message: "MCP server 's' in 'mcp.json' is missing the 'url' its HTTP transport requires",
|
|
196
|
-
},
|
|
197
|
-
"mcp-server-url-invalid": {
|
|
198
|
-
files: openPlugin({ mcpServers: { s: { type: "streamable-http", url: "http://mcp.example.com/mcp" } } }),
|
|
199
|
-
kinds: errors("mcp-server-url-invalid"),
|
|
200
|
-
message: "MCP server 's' in 'mcp.json' has an invalid 'url' 'http://mcp.example.com/mcp': expected an absolute HTTPS URL (HTTP only for localhost) with no user information or fragment",
|
|
201
|
-
},
|
|
202
|
-
"mcp-server-url-variable": {
|
|
203
|
-
files: cursorPlugin({ variables: { URL: { type: "string" } }, required: ["URL"], mcpServers: { s: { type: "http", url: "${URL}" } } }),
|
|
204
|
-
kinds: errors("mcp-server-url-variable"),
|
|
205
|
-
message: "MCP server 's' in 'mcp.json' has a variable in its 'url'; Stigmer sends the URL as written, so write the URL out and put variables in 'headers'",
|
|
206
|
-
},
|
|
207
|
-
"mcp-server-command-missing": {
|
|
208
|
-
files: openPlugin({ mcpServers: { s: { type: "stdio", args: ["x"] } } }),
|
|
209
|
-
kinds: errors("mcp-server-command-missing"),
|
|
210
|
-
message: "MCP server 's' in 'mcp.json' is missing the 'command' its stdio transport requires",
|
|
211
|
-
},
|
|
212
|
-
"mcp-server-command-invalid": {
|
|
213
|
-
files: openPlugin({ mcpServers: { s: { type: "stdio", command: "npx -y @acme/server" } } }),
|
|
214
|
-
kinds: errors("mcp-server-command-invalid"),
|
|
215
|
-
message: "MCP server 's' in 'mcp.json' has a 'command' that is not a single executable name; put arguments in 'args'",
|
|
216
|
-
},
|
|
217
|
-
"mcp-server-command-relative": {
|
|
218
|
-
files: openPlugin({ mcpServers: { s: { type: "stdio", command: "./bin/server" } } }),
|
|
219
|
-
kinds: errors("mcp-server-command-relative"),
|
|
220
|
-
message: "MCP server 's' in 'mcp.json' runs a command bundled in the plugin './bin/server'; Stigmer runs only commands on the runner's PATH (for example 'npx' or 'uvx')",
|
|
221
|
-
},
|
|
222
|
-
"mcp-server-plugin-root-reference": {
|
|
223
|
-
files: claudePlugin({ mcpServers: { s: { command: "npx", args: ["--config", "${CLAUDE_PLUGIN_ROOT}/config.json"] } } }),
|
|
224
|
-
kinds: errors("mcp-server-plugin-root-reference"),
|
|
225
|
-
message: "MCP server 's' in '.mcp.json' references the plugin's own files through '${CLAUDE_PLUGIN_ROOT}'; Stigmer does not mount plugin files into the runner",
|
|
226
|
-
},
|
|
227
|
-
"mcp-server-cwd-unsupported": {
|
|
228
|
-
files: openPlugin({ mcpServers: { s: { type: "stdio", command: "npx", cwd: "./data" } } }),
|
|
229
|
-
kinds: errors("mcp-server-cwd-unsupported"),
|
|
230
|
-
message: "MCP server 's' in 'mcp.json' sets a working directory; Stigmer does not mount plugin files, so a bundled directory cannot be reached",
|
|
231
|
-
},
|
|
232
|
-
"mcp-server-env-literal": {
|
|
233
|
-
files: openPlugin({ mcpServers: { s: { type: "stdio", command: "npx", env: { LOG_LEVEL: "debug" } } } }),
|
|
234
|
-
kinds: errors("mcp-server-env-literal"),
|
|
235
|
-
message: `MCP server 's' in 'mcp.json' sets environment variable 'LOG_LEVEL' to a literal value; Stigmer passes declared variables by name, so write 'LOG_LEVEL: "\${LOG_LEVEL}"' and declare the variable`,
|
|
236
|
-
},
|
|
237
|
-
"mcp-server-env-rename": {
|
|
238
|
-
files: openPlugin({ mcpServers: { s: { type: "stdio", command: "npx", env: { API_KEY: "${MY_KEY}" } } } }),
|
|
239
|
-
kinds: errors("mcp-server-env-rename"),
|
|
240
|
-
message: "MCP server 's' in 'mcp.json' maps environment variable 'API_KEY' to a differently named variable; Stigmer passes declared variables by name, so use the same name on both sides",
|
|
241
|
-
},
|
|
242
|
-
"mcp-server-name-duplicate": {
|
|
243
|
-
files: new Map([
|
|
244
|
-
...openPlugin({ name: "dup", mcpServers: { s: HTTP } }),
|
|
245
|
-
...claudePlugin({ name: "dup", mcpServers: { s: { url: "https://other.example.com/mcp" } } }),
|
|
246
|
-
]),
|
|
247
|
-
kinds: errors("mcp-server-name-duplicate"),
|
|
248
|
-
message: "MCP server name 's' appears more than once (again in '.mcp.json')",
|
|
249
|
-
},
|
|
250
|
-
"mcp-server-header-duplicate": {
|
|
251
|
-
files: openPlugin({ mcpServers: { s: { ...HTTP, headers: { Authorization: "a", authorization: "b" } } } }),
|
|
252
|
-
kinds: errors("mcp-server-header-duplicate"),
|
|
253
|
-
message: "MCP server 's' in 'mcp.json' declares header 'authorization' more than once (header names are case-insensitive)",
|
|
254
|
-
},
|
|
255
|
-
"mcp-server-header-invalid": {
|
|
256
|
-
files: openPlugin({ mcpServers: { s: { ...HTTP, headers: { "X Bad Header": "a" } } } }),
|
|
257
|
-
kinds: errors("mcp-server-header-invalid"),
|
|
258
|
-
message: "MCP server 's' in 'mcp.json' has an invalid header name 'X Bad Header'",
|
|
259
|
-
},
|
|
260
|
-
|
|
261
|
-
"sub-agent-frontmatter-unreadable": {
|
|
262
|
-
files: withFile(cursorPlugin({ agents: [] }), "agents/broken.md", "---\nname: [unclosed\n---\nA perfectly good prompt body."),
|
|
263
|
-
kinds: errors("sub-agent-frontmatter-unreadable"),
|
|
264
|
-
message: /^'agents\/broken.md' frontmatter is not valid YAML: /.source,
|
|
265
|
-
},
|
|
266
|
-
"sub-agent-instructions-short": {
|
|
267
|
-
files: cursorPlugin({ agents: [{ file: "terse", frontmatter: { name: "terse" }, body: "Go.\n" }] }),
|
|
268
|
-
kinds: errors("sub-agent-instructions-short"),
|
|
269
|
-
message: "sub-agent 'terse' in 'agents/terse.md' has instructions under 10 characters; the body of the file is the sub-agent's prompt",
|
|
270
|
-
},
|
|
271
|
-
"sub-agent-name-duplicate": {
|
|
272
|
-
files: cursorPlugin({ agents: [{ file: "a", frontmatter: { name: "same" } }, { file: "b", frontmatter: { name: "same" } }] }),
|
|
273
|
-
kinds: errors("sub-agent-name-duplicate"),
|
|
274
|
-
message: "sub-agent name 'same' appears more than once (again in 'agents/b.md')",
|
|
275
|
-
},
|
|
276
|
-
|
|
277
|
-
"variable-name-invalid": {
|
|
278
|
-
files: cursorPlugin({ variables: { "my-token": { type: "string" } } }),
|
|
279
|
-
kinds: errors("variable-name-invalid"),
|
|
280
|
-
message: "variable name 'my-token' in '.cursor-plugin/plugin.json' is invalid: an environment variable name is letters, digits and underscores, not starting with a digit",
|
|
281
|
-
},
|
|
282
|
-
|
|
283
|
-
"overlay-server-unknown": {
|
|
284
|
-
files: openPlugin({ mcpServers: { s: HTTP }, files: { "ai.stigmer/mcp-servers/other.yaml": "spec: {}\n" } }),
|
|
285
|
-
kinds: errors("overlay-server-unknown"),
|
|
286
|
-
message: "'ai.stigmer/mcp-servers/other.yaml' overlays MCP server 'other', which the plugin does not declare",
|
|
287
|
-
},
|
|
288
|
-
"overlay-document-unknown": {
|
|
289
|
-
files: openPlugin({ files: { "ai.stigmer/agent.yml": "kind: Agent\n" } }),
|
|
290
|
-
kinds: errors("overlay-document-unknown"),
|
|
291
|
-
message: "'ai.stigmer/agent.yml' is not a document Stigmer reads; the 'ai.stigmer/' folder holds 'agent.yaml', 'workflows/<name>.yaml' and 'mcp-servers/<server>.yaml'",
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
const warnings = (...kinds: PluginWarningKind[]): Kinds => ({ errors: [], warnings: [...kinds].sort() });
|
|
296
|
-
|
|
297
|
-
const WARNING_CASES: Record<PluginWarningKind, Case> = {
|
|
298
|
-
"manifest-field-unknown": {
|
|
299
|
-
files: openPlugin({ manifest: { colour: "blue" } }),
|
|
300
|
-
kinds: warnings("manifest-field-unknown"),
|
|
301
|
-
message: "manifest 'plugin.json' has an unknown field 'colour', ignored",
|
|
302
|
-
},
|
|
303
|
-
"manifest-extensions-invalid": {
|
|
304
|
-
files: openPlugin({ manifest: { extensions: ["nope"] } }),
|
|
305
|
-
kinds: warnings("manifest-extensions-invalid"),
|
|
306
|
-
message: "manifest 'plugin.json' has an 'extensions' field that is not an object, ignored",
|
|
307
|
-
},
|
|
308
|
-
"path-missing": {
|
|
309
|
-
files: cursorPlugin({ manifest: { agents: "./agents/" } }),
|
|
310
|
-
kinds: warnings("path-missing"),
|
|
311
|
-
message: "path './agents' declared in '.cursor-plugin/plugin.json' does not exist in the plugin",
|
|
312
|
-
},
|
|
313
|
-
"skill-name-defaulted": {
|
|
314
|
-
files: withFile(openPlugin(), "skills/from-dir/SKILL.md", "---\ndescription: d\n---\nbody"),
|
|
315
|
-
kinds: warnings("skill-name-defaulted"),
|
|
316
|
-
message: "'skills/from-dir/SKILL.md' has no 'name' in its frontmatter; the skill is named after its directory, 'from-dir'",
|
|
317
|
-
},
|
|
318
|
-
"skill-name-differs-from-directory": {
|
|
319
|
-
files: openPlugin({ skills: [{ name: "real", dir: "folder", description: "d" }] }),
|
|
320
|
-
kinds: warnings("skill-name-differs-from-directory"),
|
|
321
|
-
message: "skill 'real' in 'skills/folder/SKILL.md' is named differently from its directory 'folder'",
|
|
322
|
-
},
|
|
323
|
-
"skill-description-missing": {
|
|
324
|
-
files: openPlugin({ skills: [{ name: "quiet" }] }),
|
|
325
|
-
kinds: warnings("skill-description-missing"),
|
|
326
|
-
message: "skill 'quiet' in 'skills/quiet/SKILL.md' has no 'description'",
|
|
327
|
-
},
|
|
328
|
-
"mcp-config-field-ignored": {
|
|
329
|
-
files: withFile(claudePlugin(), ".mcp.json", JSON.stringify({ mcpServers: {}, comment: "x" })),
|
|
330
|
-
kinds: warnings("mcp-config-field-ignored"),
|
|
331
|
-
message: "MCP configuration '.mcp.json' has a top-level field 'comment' Stigmer does not read, ignored",
|
|
332
|
-
},
|
|
333
|
-
"mcp-server-sse-mapped": {
|
|
334
|
-
files: openPlugin({ mcpServers: { legacy: { type: "sse", url: "https://legacy.example.com/sse" } } }),
|
|
335
|
-
kinds: warnings("mcp-server-sse-mapped"),
|
|
336
|
-
message: "MCP server 'legacy' in 'mcp.json' declares the legacy 'sse' transport; Stigmer connects over Streamable HTTP and falls back to SSE only when the server rejects it",
|
|
337
|
-
},
|
|
338
|
-
"mcp-server-field-ignored": {
|
|
339
|
-
files: cursorPlugin({ mcpServers: { s: { ...CURSOR_HTTP, timeout: 30 } } }),
|
|
340
|
-
kinds: warnings("mcp-server-field-ignored"),
|
|
341
|
-
message: "MCP server 's' in 'mcp.json' has a field 'timeout' Stigmer does not read, ignored",
|
|
342
|
-
},
|
|
343
|
-
"mcp-server-auth-ignored": {
|
|
344
|
-
files: cursorPlugin({ mcpServers: { s: { ...CURSOR_HTTP, auth: { scopes: ["read"] } } } }),
|
|
345
|
-
kinds: warnings("mcp-server-auth-ignored"),
|
|
346
|
-
message: "MCP server 's' in 'mcp.json' has an 'auth' block Stigmer does not read; OAuth for a server is declared in 'ai.stigmer/mcp-servers/s.yaml'",
|
|
347
|
-
},
|
|
348
|
-
"variable-inferred": {
|
|
349
|
-
files: cursorPlugin({ mcpServers: { s: { ...CURSOR_HTTP, headers: { Authorization: "Bearer ${TOKEN}" } } } }),
|
|
350
|
-
kinds: warnings("variable-inferred"),
|
|
351
|
-
message: "variable 'TOKEN' is referenced by MCP server 's' but not declared; it is declared as a required secret",
|
|
352
|
-
},
|
|
353
|
-
"variable-unreferenced": {
|
|
354
|
-
files: cursorPlugin({ variables: { UNUSED: { type: "string" } } }),
|
|
355
|
-
kinds: warnings("variable-unreferenced"),
|
|
356
|
-
message: "variable 'UNUSED' in '.cursor-plugin/plugin.json' is declared but no MCP server references it",
|
|
357
|
-
},
|
|
358
|
-
"variable-default-dropped": {
|
|
359
|
-
files: claudePlugin({ userConfig: { MODE: { type: "string", default: "fast" } }, mcpServers: { s: { command: "npx", args: ["${MODE}"] } } }),
|
|
360
|
-
kinds: warnings("variable-default-dropped"),
|
|
361
|
-
message: "variable 'MODE' in '.claude-plugin/plugin.json' has a default value, which Stigmer does not carry; the user supplies the value",
|
|
362
|
-
},
|
|
363
|
-
"variable-type-narrowed": {
|
|
364
|
-
files: claudePlugin({ userConfig: { DIR: { type: "directory" } }, mcpServers: { s: { command: "npx", args: ["${DIR}"] } } }),
|
|
365
|
-
kinds: warnings("variable-type-narrowed"),
|
|
366
|
-
message: "variable 'DIR' in '.claude-plugin/plugin.json' is typed 'directory'; Stigmer variables are strings",
|
|
367
|
-
},
|
|
368
|
-
"variable-option-dropped": {
|
|
369
|
-
files: claudePlugin({ userConfig: { MODE: { type: "string", options: ["a", "b"] } }, mcpServers: { s: { command: "npx", args: ["${MODE}"] } } }),
|
|
370
|
-
kinds: warnings("variable-option-dropped"),
|
|
371
|
-
message: "variable 'MODE' in '.claude-plugin/plugin.json' has a 'options' constraint, which Stigmer does not carry",
|
|
372
|
-
},
|
|
373
|
-
"sub-agent-name-defaulted": {
|
|
374
|
-
files: cursorPlugin({ agents: [{ file: "helper", frontmatter: null, body: "You help with everything, patiently.\n" }] }),
|
|
375
|
-
kinds: warnings("sub-agent-name-defaulted"),
|
|
376
|
-
message: "'agents/helper.md' has no 'name' in its frontmatter; the sub-agent is named after the file, 'helper'",
|
|
377
|
-
},
|
|
378
|
-
"sub-agent-skill-unknown": {
|
|
379
|
-
files: claudePlugin({ agents: [{ file: "a", frontmatter: { skills: ["missing"] } }] }),
|
|
380
|
-
kinds: warnings("sub-agent-skill-unknown"),
|
|
381
|
-
message: "sub-agent 'a' in 'agents/a.md' asks for skill 'missing', which the plugin does not ship",
|
|
382
|
-
},
|
|
383
|
-
"sub-agent-model-unknown": {
|
|
384
|
-
files: cursorPlugin({ agents: [{ file: "a", frontmatter: { model: "grok-4.6[effort=xhigh]" } }] }),
|
|
385
|
-
kinds: warnings("sub-agent-model-unknown"),
|
|
386
|
-
message: "sub-agent 'a' in 'agents/a.md' names model 'grok-4.6[effort=xhigh]', which Stigmer cannot map; the sub-agent runs on the session's model",
|
|
387
|
-
},
|
|
388
|
-
"sub-agent-field-ignored": {
|
|
389
|
-
files: cursorPlugin({ agents: [{ file: "a", frontmatter: { readonly: true } }] }),
|
|
390
|
-
kinds: warnings("sub-agent-field-ignored"),
|
|
391
|
-
message: "sub-agent 'a' in 'agents/a.md' has a field 'readonly' Stigmer does not read, ignored",
|
|
392
|
-
},
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
function expectSentence(actual: string, expected: string): void {
|
|
396
|
-
// A case whose sentence ends in a parser's own message pins its prefix.
|
|
397
|
-
if (expected.startsWith("^")) expect(actual).toMatch(new RegExp(expected));
|
|
398
|
-
else expect(actual).toBe(expected);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
describe("every error kind refuses with its own sentence and nothing unexpected", () => {
|
|
402
|
-
it.each(Object.entries(ERROR_CASES))("%s", (kind, testCase) => {
|
|
403
|
-
const outcome = read(testCase.files);
|
|
404
|
-
expect(kindsOf(outcome)).toEqual(testCase.kinds);
|
|
405
|
-
expect(outcome.ok).toBe(false);
|
|
406
|
-
if (!outcome.ok) expectSentence(findingOf(outcome.errors, kind).message, testCase.message);
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
describe("every warning kind warns with its own sentence and the plugin is accepted", () => {
|
|
411
|
-
it.each(Object.entries(WARNING_CASES))("%s", (kind, testCase) => {
|
|
412
|
-
const outcome = read(testCase.files);
|
|
413
|
-
expect(kindsOf(outcome)).toEqual(testCase.kinds);
|
|
414
|
-
expect(outcome.ok).toBe(true);
|
|
415
|
-
expectSentence(findingOf(outcome.warnings, kind).message, testCase.message);
|
|
416
|
-
});
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
describe("a valid plugin in every dialect produces no findings at all", () => {
|
|
420
|
-
it.each([
|
|
421
|
-
["open", openPlugin({ skills: [{ name: "a", description: "d" }], mcpServers: { s: HTTP } })],
|
|
422
|
-
["claude", claudePlugin({ skills: [{ name: "a", description: "d" }], agents: [{ file: "b", frontmatter: { description: "x" } }] })],
|
|
423
|
-
["cursor", cursorPlugin({ skills: [{ name: "a", description: "d" }], agents: [{ file: "b" }], mcpServers: { s: CURSOR_HTTP } })],
|
|
424
|
-
])("%s", (_dialect, files) => {
|
|
425
|
-
expect(kindsOf(read(files))).toEqual({ errors: [], warnings: [] });
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it("removing the one broken thing from an adversarial case yields a clean plugin", () => {
|
|
429
|
-
// The env-literal case minus its env block is the open stdio server every other stdio case starts from.
|
|
430
|
-
const files = withFile(ERROR_CASES["mcp-server-env-literal"].files, "mcp.json", JSON.stringify({ $schema: OPEN_MCP_SCHEMA, mcpServers: { s: { type: "stdio", command: "npx" } } }));
|
|
431
|
-
expect(kindsOf(read(files))).toEqual({ errors: [], warnings: [] });
|
|
432
|
-
expect(withoutFile(files, "mcp.json").has("mcp.json")).toBe(false);
|
|
433
|
-
});
|
|
434
|
-
});
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The ignore engine's table tests: the filepath.Match port, the Pattern
|
|
3
|
-
* matcher (scoping, dir-only, globs, negation) and the composed matcher's
|
|
4
|
-
* source precedence with last-match-wins. Moved here with the engine from
|
|
5
|
-
* the CLI; the matcher is now built from the TEXT of the ignore files, so
|
|
6
|
-
* the precedence cases hand it text where they once wrote a temp directory.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, expect, it } from "vitest";
|
|
10
|
-
|
|
11
|
-
import { matchName } from "../client/ignore/match.js";
|
|
12
|
-
import { buildMatcher } from "../client/ignore/matcher.js";
|
|
13
|
-
import { MatchResult, parsePattern } from "../client/ignore/pattern.js";
|
|
14
|
-
|
|
15
|
-
describe("matchName (filepath.Match port)", () => {
|
|
16
|
-
const cases: [string, string, boolean][] = [
|
|
17
|
-
["*.go", "main.go", true],
|
|
18
|
-
["*.go", "main.rs", false],
|
|
19
|
-
["*", "anything", true],
|
|
20
|
-
["a?c", "abc", true],
|
|
21
|
-
["a?c", "ac", false],
|
|
22
|
-
["[abc]", "b", true],
|
|
23
|
-
["[abc]", "d", false],
|
|
24
|
-
["[a-c]", "b", true],
|
|
25
|
-
["[a-c]", "z", false],
|
|
26
|
-
["[^a]", "b", true],
|
|
27
|
-
["[^a]", "a", false],
|
|
28
|
-
["foo*", "foobar", true],
|
|
29
|
-
["*bar", "foobar", true],
|
|
30
|
-
["h[a-z]llo", "hello", true],
|
|
31
|
-
["node_modules", "node_modules", true],
|
|
32
|
-
["\\*.go", "*.go", true],
|
|
33
|
-
// Malformed patterns: Go returns ErrBadPattern → callers treat as no match.
|
|
34
|
-
["[", "a", false],
|
|
35
|
-
["[a-", "a", false],
|
|
36
|
-
];
|
|
37
|
-
for (const [pattern, name, expected] of cases) {
|
|
38
|
-
it(`${pattern} ~ ${name} => ${expected}`, () => {
|
|
39
|
-
expect(matchName(pattern, name)).toBe(expected);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe("parsePattern / Pattern.match", () => {
|
|
45
|
-
it("directory-only patterns only match directories (or ancestors)", () => {
|
|
46
|
-
const p = parsePattern("node_modules/", []);
|
|
47
|
-
expect(p.match(["node_modules"], true)).toBe(MatchResult.Exclude);
|
|
48
|
-
expect(p.match(["node_modules"], false)).toBe(MatchResult.NoMatch);
|
|
49
|
-
expect(p.match(["src", "node_modules"], true)).toBe(MatchResult.Exclude);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("simple name patterns match at any depth", () => {
|
|
53
|
-
const p = parsePattern("*.log", []);
|
|
54
|
-
expect(p.match(["app.log"], false)).toBe(MatchResult.Exclude);
|
|
55
|
-
expect(p.match(["sub", "app.log"], false)).toBe(MatchResult.Exclude);
|
|
56
|
-
expect(p.match(["app.txt"], false)).toBe(MatchResult.NoMatch);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("leading slash anchors to the domain root", () => {
|
|
60
|
-
const p = parsePattern("/build", []);
|
|
61
|
-
expect(p.match(["build"], true)).toBe(MatchResult.Exclude);
|
|
62
|
-
expect(p.match(["src", "build"], true)).toBe(MatchResult.NoMatch);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("** matches zero-to-many directories", () => {
|
|
66
|
-
const p = parsePattern("**/foo", []);
|
|
67
|
-
expect(p.match(["foo"], false)).toBe(MatchResult.Exclude);
|
|
68
|
-
expect(p.match(["a", "b", "foo"], false)).toBe(MatchResult.Exclude);
|
|
69
|
-
expect(p.match(["a", "bar"], false)).toBe(MatchResult.NoMatch);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("negation patterns produce Include", () => {
|
|
73
|
-
const p = parsePattern("!keep.txt", []);
|
|
74
|
-
expect(p.inclusion).toBe(true);
|
|
75
|
-
expect(p.match(["keep.txt"], false)).toBe(MatchResult.Include);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("domain scopes matching below a subtree", () => {
|
|
79
|
-
const p = parsePattern("*.txt", ["sub"]);
|
|
80
|
-
expect(p.match(["sub", "a.txt"], false)).toBe(MatchResult.Exclude);
|
|
81
|
-
expect(p.match(["a.txt"], false)).toBe(MatchResult.NoMatch);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("buildMatcher (source precedence, last-match-wins)", () => {
|
|
86
|
-
it("applies security defaults including the .env.example negation", () => {
|
|
87
|
-
const m = buildMatcher({ includeDefaults: true });
|
|
88
|
-
expect(m.match(".env", false)).toBe(true);
|
|
89
|
-
expect(m.match(".env.example", false)).toBe(false);
|
|
90
|
-
expect(m.match("id_rsa", false)).toBe(true);
|
|
91
|
-
expect(m.match("node_modules", true)).toBe(true);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("honors .gitignore with negation (last match wins)", () => {
|
|
95
|
-
const m = buildMatcher({ includeDefaults: true, gitignore: "*.draft\n!keep.draft\n" });
|
|
96
|
-
expect(m.match("notes.draft", false)).toBe(true);
|
|
97
|
-
expect(m.match("keep.draft", false)).toBe(false);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("an absent .gitignore contributes nothing", () => {
|
|
101
|
-
const m = buildMatcher({ includeDefaults: true });
|
|
102
|
-
expect(m.match("notes.draft", false)).toBe(false);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it(".stigmerignore outranks .gitignore", () => {
|
|
106
|
-
const m = buildMatcher({ includeDefaults: true, gitignore: "!shared.draft\n", stigmerignore: "*.draft\n" });
|
|
107
|
-
expect(m.match("shared.draft", false)).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("caller extra ignore/include override file sources", () => {
|
|
111
|
-
const m = buildMatcher({
|
|
112
|
-
includeDefaults: true,
|
|
113
|
-
gitignore: "*.draft\n",
|
|
114
|
-
extraIgnore: ["*.tmp"],
|
|
115
|
-
extraInclude: ["important.draft"],
|
|
116
|
-
});
|
|
117
|
-
expect(m.match("scratch.tmp", false)).toBe(true);
|
|
118
|
-
expect(m.match("important.draft", false)).toBe(false);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("reports the matching source and reason", () => {
|
|
122
|
-
const m = buildMatcher({ includeDefaults: true });
|
|
123
|
-
const reason = m.matchWithReason(".env", false);
|
|
124
|
-
expect(reason.ignored).toBe(true);
|
|
125
|
-
expect(reason.source).toBe("defaults");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("CRLF ignore files parse line by line", () => {
|
|
129
|
-
const m = buildMatcher({ includeDefaults: false, gitignore: "*.a\r\n*.b\r\n" });
|
|
130
|
-
expect(m.match("x.a", false)).toBe(true);
|
|
131
|
-
expect(m.match("x.b", false)).toBe(true);
|
|
132
|
-
});
|
|
133
|
-
});
|