@kuznai/inception-engine 0.19.0 → 0.20.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/README.md +48 -22
- package/dist/{config → src/config}/agents.js +87 -14
- package/dist/{core → src/core}/adapters/agent-definitions.d.ts +0 -12
- package/dist/src/core/adapters/agent-definitions.js +162 -0
- package/dist/{core → src/core}/adapters/frontmatter.d.ts +10 -0
- package/dist/{core → src/core}/adapters/frontmatter.js +23 -6
- package/dist/{core → src/core}/adapters/mcp.js +17 -20
- package/dist/{core → src/core}/adapters/permissions.js +16 -20
- package/dist/{core → src/core}/adapters/rules.js +26 -50
- package/dist/src/core/capabilities.d.ts +47 -0
- package/dist/src/core/capabilities.js +280 -0
- package/dist/{core → src/core}/deploy.js +70 -41
- package/dist/{core → src/core}/init.js +18 -24
- package/dist/src/core/merge-patch.d.ts +4 -0
- package/dist/src/core/merge-patch.js +46 -0
- package/dist/{core → src/core}/ownership.js +13 -0
- package/dist/{core → src/core}/preflight.js +141 -29
- package/dist/{core → src/core}/revert.js +86 -26
- package/dist/{core → src/core}/validation.js +20 -0
- package/dist/{schemas → src/schemas}/manifest.d.ts +20 -0
- package/dist/{schemas → src/schemas}/manifest.js +11 -0
- package/dist/{schemas → src/schemas}/registry.d.ts +36 -0
- package/dist/{schemas → src/schemas}/registry.js +12 -4
- package/dist/{types.d.ts → src/types.d.ts} +13 -0
- package/dist/test/helpers/fs.d.ts +2 -0
- package/dist/test/helpers/fs.js +17 -0
- package/dist/test/helpers/path.d.ts +10 -0
- package/dist/test/helpers/path.js +15 -0
- package/dist/test/helpers/skill-dir-scenarios.d.ts +7 -0
- package/dist/test/helpers/skill-dir-scenarios.js +206 -0
- package/dist/test/helpers/skill-dir.d.ts +7 -0
- package/dist/test/helpers/skill-dir.js +46 -0
- package/dist/test/os/cross-platform/cross-platform.test.d.ts +1 -0
- package/dist/test/os/cross-platform/cross-platform.test.js +328 -0
- package/dist/test/os/posix/skill-dir.test.d.ts +1 -0
- package/dist/test/os/posix/skill-dir.test.js +19 -0
- package/dist/test/os/windows/revert-integration.test.d.ts +1 -0
- package/dist/test/os/windows/revert-integration.test.js +72 -0
- package/dist/test/os/windows/skill-dir.test.d.ts +1 -0
- package/dist/test/os/windows/skill-dir.test.js +19 -0
- package/dist/test/unit/adapters.test.d.ts +1 -0
- package/dist/test/unit/adapters.test.js +947 -0
- package/dist/test/unit/capabilities.test.d.ts +1 -0
- package/dist/test/unit/capabilities.test.js +71 -0
- package/dist/test/unit/cli.test.d.ts +1 -0
- package/dist/test/unit/cli.test.js +834 -0
- package/dist/test/unit/deploy.test.d.ts +1 -0
- package/dist/test/unit/deploy.test.js +2481 -0
- package/dist/test/unit/detect.test.d.ts +1 -0
- package/dist/test/unit/detect.test.js +192 -0
- package/dist/test/unit/enterprise.test.d.ts +1 -0
- package/dist/test/unit/enterprise.test.js +106 -0
- package/dist/test/unit/frontmatter.test.d.ts +1 -0
- package/dist/test/unit/frontmatter.test.js +102 -0
- package/dist/test/unit/gemini-north-star.test.d.ts +1 -0
- package/dist/test/unit/gemini-north-star.test.js +243 -0
- package/dist/test/unit/manifest.test.d.ts +1 -0
- package/dist/test/unit/manifest.test.js +737 -0
- package/dist/test/unit/migration-golden.test.d.ts +1 -0
- package/dist/test/unit/migration-golden.test.js +197 -0
- package/dist/test/unit/ownership.test.d.ts +1 -0
- package/dist/test/unit/ownership.test.js +587 -0
- package/dist/test/unit/preflight.test.d.ts +1 -0
- package/dist/test/unit/preflight.test.js +501 -0
- package/dist/test/unit/resolve.test.d.ts +1 -0
- package/dist/test/unit/resolve.test.js +119 -0
- package/dist/test/unit/revert.test.d.ts +1 -0
- package/dist/test/unit/revert.test.js +1004 -0
- package/dist/test/unit/toml.test.d.ts +1 -0
- package/dist/test/unit/toml.test.js +55 -0
- package/package.json +1 -1
- package/dist/core/adapters/agent-definitions.js +0 -91
- /package/dist/{config → src/config}/agents.d.ts +0 -0
- /package/dist/{config → src/config}/manifest.d.ts +0 -0
- /package/dist/{config → src/config}/manifest.js +0 -0
- /package/dist/{core → src/core}/adapters/index.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/index.js +0 -0
- /package/dist/{core → src/core}/adapters/mcp.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/permissions.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/rules.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/toml.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/toml.js +0 -0
- /package/dist/{core → src/core}/deploy.d.ts +0 -0
- /package/dist/{core → src/core}/detect.d.ts +0 -0
- /package/dist/{core → src/core}/detect.js +0 -0
- /package/dist/{core → src/core}/init.d.ts +0 -0
- /package/dist/{core → src/core}/ownership.d.ts +0 -0
- /package/dist/{core → src/core}/preflight.d.ts +0 -0
- /package/dist/{core → src/core}/resolve.d.ts +0 -0
- /package/dist/{core → src/core}/resolve.js +0 -0
- /package/dist/{core → src/core}/revert.d.ts +0 -0
- /package/dist/{core → src/core}/runtime-paths.d.ts +0 -0
- /package/dist/{core → src/core}/runtime-paths.js +0 -0
- /package/dist/{core → src/core}/validation.d.ts +0 -0
- /package/dist/{errors.d.ts → src/errors.d.ts} +0 -0
- /package/dist/{errors.js → src/errors.js} +0 -0
- /package/dist/{formatters.d.ts → src/formatters.d.ts} +0 -0
- /package/dist/{formatters.js → src/formatters.js} +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{index.js → src/index.js} +0 -0
- /package/dist/{logger.d.ts → src/logger.d.ts} +0 -0
- /package/dist/{logger.js → src/logger.js} +0 -0
- /package/dist/{schemas → src/schemas}/errors.d.ts +0 -0
- /package/dist/{schemas → src/schemas}/errors.js +0 -0
- /package/dist/{types.js → src/types.js} +0 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { chmod, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import { loadManifest } from "../../src/config/manifest.js";
|
|
6
|
+
import { UserError } from "../../src/errors.js";
|
|
7
|
+
import { makeTmpDir } from "../helpers/fs.js";
|
|
8
|
+
describe("loadManifest", () => {
|
|
9
|
+
it("parses a valid manifest", async () => {
|
|
10
|
+
const dir = await makeTmpDir();
|
|
11
|
+
try {
|
|
12
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
13
|
+
skills: [
|
|
14
|
+
{
|
|
15
|
+
name: "test-skill",
|
|
16
|
+
path: "skills/test-skill",
|
|
17
|
+
agents: ["claude-code"],
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
}));
|
|
21
|
+
const manifest = await loadManifest(dir);
|
|
22
|
+
assert.equal(manifest.skills.length, 1);
|
|
23
|
+
assert.equal(manifest.skills[0]?.name, "test-skill");
|
|
24
|
+
assert.deepEqual(manifest.skills[0]?.agents, ["claude-code"]);
|
|
25
|
+
assert.deepEqual(manifest.mcpServers, []);
|
|
26
|
+
assert.deepEqual(manifest.agentRules, []);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await rm(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
it("throws on missing file", async () => {
|
|
33
|
+
await assert.rejects(loadManifest("/nonexistent/path"), (err) => {
|
|
34
|
+
assert.ok(err instanceof UserError);
|
|
35
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
36
|
+
assert.match(err.message, /No inception\.json found/);
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it("throws permission error when manifest is unreadable", {
|
|
41
|
+
skip: process.platform === "win32",
|
|
42
|
+
}, async () => {
|
|
43
|
+
const dir = await makeTmpDir();
|
|
44
|
+
const manifestPath = path.join(dir, "inception.json");
|
|
45
|
+
try {
|
|
46
|
+
await writeFile(manifestPath, JSON.stringify({ skills: [] }));
|
|
47
|
+
await chmod(manifestPath, 0o000);
|
|
48
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
49
|
+
assert.ok(err instanceof UserError);
|
|
50
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
51
|
+
assert.match(err.message, /Permission denied reading/);
|
|
52
|
+
return true;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
await chmod(manifestPath, 0o644);
|
|
57
|
+
await rm(dir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
it("throws on invalid JSON", async () => {
|
|
61
|
+
const dir = await makeTmpDir();
|
|
62
|
+
try {
|
|
63
|
+
await writeFile(path.join(dir, "inception.json"), "not json{");
|
|
64
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
65
|
+
assert.ok(err instanceof UserError);
|
|
66
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
67
|
+
assert.match(err.message, /Invalid JSON/);
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await rm(dir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
it("throws on missing skills array", async () => {
|
|
76
|
+
const dir = await makeTmpDir();
|
|
77
|
+
try {
|
|
78
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({}));
|
|
79
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
80
|
+
assert.ok(err instanceof UserError);
|
|
81
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
82
|
+
assert.match(err.message, /"skills" must be an array/);
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await rm(dir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
it("throws on unknown agent ID", async () => {
|
|
91
|
+
const dir = await makeTmpDir();
|
|
92
|
+
try {
|
|
93
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
94
|
+
skills: [{ name: "s", path: "p", agents: ["unknown-agent"] }],
|
|
95
|
+
}));
|
|
96
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
97
|
+
assert.ok(err instanceof UserError);
|
|
98
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
99
|
+
assert.match(err.message, /unknown agent "unknown-agent"/);
|
|
100
|
+
return true;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
await rm(dir, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
it("throws on missing skill name", async () => {
|
|
108
|
+
const dir = await makeTmpDir();
|
|
109
|
+
try {
|
|
110
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
111
|
+
skills: [{ path: "p", agents: ["claude-code"] }],
|
|
112
|
+
}));
|
|
113
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
114
|
+
assert.ok(err instanceof UserError);
|
|
115
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
116
|
+
assert.match(err.message, /name must be a non-empty string/);
|
|
117
|
+
return true;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await rm(dir, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
it("throws on skill.name with path traversal (../../.ssh)", async () => {
|
|
125
|
+
const dir = await makeTmpDir();
|
|
126
|
+
try {
|
|
127
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
128
|
+
skills: [
|
|
129
|
+
{ name: "../../.ssh", path: "skills/s", agents: ["claude-code"] },
|
|
130
|
+
],
|
|
131
|
+
}));
|
|
132
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
133
|
+
assert.ok(err instanceof UserError);
|
|
134
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
135
|
+
assert.match(err.message, /name must contain only/);
|
|
136
|
+
return true;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await rm(dir, { recursive: true });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
it("throws on skill.name with path separator (../escape)", async () => {
|
|
144
|
+
const dir = await makeTmpDir();
|
|
145
|
+
try {
|
|
146
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
147
|
+
skills: [
|
|
148
|
+
{ name: "../escape", path: "skills/s", agents: ["claude-code"] },
|
|
149
|
+
],
|
|
150
|
+
}));
|
|
151
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
152
|
+
assert.ok(err instanceof UserError);
|
|
153
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
154
|
+
assert.match(err.message, /name must contain only/);
|
|
155
|
+
return true;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
await rm(dir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
it("accepts a skill.name with dots, hyphens, and digits (valid-name_1.0)", async () => {
|
|
163
|
+
const dir = await makeTmpDir();
|
|
164
|
+
try {
|
|
165
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
166
|
+
skills: [
|
|
167
|
+
{
|
|
168
|
+
name: "valid-name_1.0",
|
|
169
|
+
path: "skills/s",
|
|
170
|
+
agents: ["claude-code"],
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
}));
|
|
174
|
+
const manifest = await loadManifest(dir);
|
|
175
|
+
assert.equal(manifest.skills[0]?.name, "valid-name_1.0");
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
await rm(dir, { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
it("throws on skill.path with path traversal (../../secret)", async () => {
|
|
182
|
+
const dir = await makeTmpDir();
|
|
183
|
+
try {
|
|
184
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
185
|
+
skills: [
|
|
186
|
+
{ name: "s", path: "../../secret", agents: ["claude-code"] },
|
|
187
|
+
],
|
|
188
|
+
}));
|
|
189
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
190
|
+
assert.ok(err instanceof UserError);
|
|
191
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
192
|
+
assert.match(err.message, /must not escape the repository root/);
|
|
193
|
+
return true;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
await rm(dir, { recursive: true });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
it("throws on skill.path that is absolute (/etc/passwd)", async () => {
|
|
201
|
+
const dir = await makeTmpDir();
|
|
202
|
+
try {
|
|
203
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
204
|
+
skills: [{ name: "s", path: "/etc/passwd", agents: ["claude-code"] }],
|
|
205
|
+
}));
|
|
206
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
207
|
+
assert.ok(err instanceof UserError);
|
|
208
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
209
|
+
assert.match(err.message, /must be a relative path/);
|
|
210
|
+
return true;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
await rm(dir, { recursive: true });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
it("throws when mcpServers is not an array", async () => {
|
|
218
|
+
const dir = await makeTmpDir();
|
|
219
|
+
try {
|
|
220
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({ skills: [], mcpServers: "not-an-array" }));
|
|
221
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
222
|
+
assert.ok(err instanceof UserError);
|
|
223
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
224
|
+
assert.match(err.message, /"mcpServers" must be an array/);
|
|
225
|
+
return true;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
await rm(dir, { recursive: true });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
it("throws when agentRules is not an array", async () => {
|
|
233
|
+
const dir = await makeTmpDir();
|
|
234
|
+
try {
|
|
235
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({ skills: [], agentRules: 42 }));
|
|
236
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
237
|
+
assert.ok(err instanceof UserError);
|
|
238
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
239
|
+
assert.match(err.message, /"agentRules" must be an array/);
|
|
240
|
+
return true;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
await rm(dir, { recursive: true });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
it("throws when mcpServers entry is missing 'name'", async () => {
|
|
248
|
+
const dir = await makeTmpDir();
|
|
249
|
+
try {
|
|
250
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
251
|
+
skills: [],
|
|
252
|
+
mcpServers: [
|
|
253
|
+
{ agents: ["claude-code"], config: { command: "my-server" } },
|
|
254
|
+
],
|
|
255
|
+
}));
|
|
256
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
257
|
+
assert.ok(err instanceof UserError);
|
|
258
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
259
|
+
assert.match(err.message, /name must be a non-empty string/);
|
|
260
|
+
return true;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
await rm(dir, { recursive: true });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
it("throws when mcpServers entry has an empty 'name'", async () => {
|
|
268
|
+
const dir = await makeTmpDir();
|
|
269
|
+
try {
|
|
270
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
271
|
+
skills: [],
|
|
272
|
+
mcpServers: [
|
|
273
|
+
{ name: "", agents: ["claude-code"], config: { command: "s" } },
|
|
274
|
+
],
|
|
275
|
+
}));
|
|
276
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
277
|
+
assert.ok(err instanceof UserError);
|
|
278
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
279
|
+
assert.match(err.message, /name must be a non-empty string/);
|
|
280
|
+
return true;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
await rm(dir, { recursive: true });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
it("throws when mcpServers entry is missing 'agents'", async () => {
|
|
288
|
+
const dir = await makeTmpDir();
|
|
289
|
+
try {
|
|
290
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
291
|
+
skills: [],
|
|
292
|
+
mcpServers: [{ name: "my-mcp", config: { command: "s" } }],
|
|
293
|
+
}));
|
|
294
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
295
|
+
assert.ok(err instanceof UserError);
|
|
296
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
297
|
+
assert.match(err.message, /agents must be a non-empty array/);
|
|
298
|
+
return true;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
await rm(dir, { recursive: true });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
it("throws when mcpServers entry is missing 'config'", async () => {
|
|
306
|
+
const dir = await makeTmpDir();
|
|
307
|
+
try {
|
|
308
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
309
|
+
skills: [],
|
|
310
|
+
mcpServers: [{ name: "my-mcp", agents: ["claude-code"] }],
|
|
311
|
+
}));
|
|
312
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
313
|
+
assert.ok(err instanceof UserError);
|
|
314
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
315
|
+
assert.match(err.message, /mcpServers\[0\]/);
|
|
316
|
+
return true;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
await rm(dir, { recursive: true });
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
it("accepts a valid mcpServers entry", async () => {
|
|
324
|
+
const dir = await makeTmpDir();
|
|
325
|
+
try {
|
|
326
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
327
|
+
skills: [],
|
|
328
|
+
mcpServers: [
|
|
329
|
+
{
|
|
330
|
+
name: "my-mcp",
|
|
331
|
+
agents: ["claude-code"],
|
|
332
|
+
config: { command: "my-server", args: ["--flag"] },
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
}));
|
|
336
|
+
const manifest = await loadManifest(dir);
|
|
337
|
+
assert.equal(manifest.mcpServers.length, 1);
|
|
338
|
+
assert.equal(manifest.mcpServers[0]?.name, "my-mcp");
|
|
339
|
+
assert.deepEqual(manifest.mcpServers[0]?.agents, ["claude-code"]);
|
|
340
|
+
assert.deepEqual(manifest.mcpServers[0]?.config, {
|
|
341
|
+
command: "my-server",
|
|
342
|
+
args: ["--flag"],
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
await rm(dir, { recursive: true });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
it("throws when agentRules entry is not an object", async () => {
|
|
350
|
+
const dir = await makeTmpDir();
|
|
351
|
+
try {
|
|
352
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({ skills: [], agentRules: ["not-an-object"] }));
|
|
353
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
354
|
+
assert.ok(err instanceof UserError);
|
|
355
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
356
|
+
assert.match(err.message, /agentRules\[0\]/);
|
|
357
|
+
return true;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
await rm(dir, { recursive: true });
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
it("throws when agentRules entry has an empty 'name'", async () => {
|
|
365
|
+
const dir = await makeTmpDir();
|
|
366
|
+
try {
|
|
367
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
368
|
+
skills: [],
|
|
369
|
+
agentRules: [
|
|
370
|
+
{ name: "", agents: ["claude-code"], path: "rules/CLAUDE.md" },
|
|
371
|
+
],
|
|
372
|
+
}));
|
|
373
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
374
|
+
assert.ok(err instanceof UserError);
|
|
375
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
376
|
+
assert.match(err.message, /name must be a non-empty string/);
|
|
377
|
+
return true;
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
finally {
|
|
381
|
+
await rm(dir, { recursive: true });
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
it("throws when agentRules entry is missing 'agents'", async () => {
|
|
385
|
+
const dir = await makeTmpDir();
|
|
386
|
+
try {
|
|
387
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
388
|
+
skills: [],
|
|
389
|
+
agentRules: [{ name: "my-rule", path: "rules/AGENTS.md" }],
|
|
390
|
+
}));
|
|
391
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
392
|
+
assert.ok(err instanceof UserError);
|
|
393
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
394
|
+
assert.match(err.message, /agents must be a non-empty array/);
|
|
395
|
+
return true;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
await rm(dir, { recursive: true });
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
it("throws when agentRules entry is missing 'path'", async () => {
|
|
403
|
+
const dir = await makeTmpDir();
|
|
404
|
+
try {
|
|
405
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
406
|
+
skills: [],
|
|
407
|
+
agentRules: [{ name: "my-rule", agents: ["claude-code"] }],
|
|
408
|
+
}));
|
|
409
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
410
|
+
assert.ok(err instanceof UserError);
|
|
411
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
412
|
+
assert.match(err.message, /agentRules\[0\]/);
|
|
413
|
+
return true;
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
await rm(dir, { recursive: true });
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
it("throws when agentRules entry has an escaping 'path'", async () => {
|
|
421
|
+
const dir = await makeTmpDir();
|
|
422
|
+
try {
|
|
423
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
424
|
+
skills: [],
|
|
425
|
+
agentRules: [
|
|
426
|
+
{
|
|
427
|
+
name: "my-rule",
|
|
428
|
+
agents: ["claude-code"],
|
|
429
|
+
path: "../../../etc/passwd",
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
}));
|
|
433
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
434
|
+
assert.ok(err instanceof UserError);
|
|
435
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
436
|
+
assert.match(err.message, /must not escape the repository root/);
|
|
437
|
+
return true;
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
await rm(dir, { recursive: true });
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
it("throws when file target escapes its placeholder root", async () => {
|
|
445
|
+
const dir = await makeTmpDir();
|
|
446
|
+
try {
|
|
447
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
448
|
+
skills: [],
|
|
449
|
+
files: [
|
|
450
|
+
{
|
|
451
|
+
name: "bad-target",
|
|
452
|
+
path: "rules.md",
|
|
453
|
+
target: "{home}/../.ssh/config",
|
|
454
|
+
agents: ["claude-code"],
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
}));
|
|
458
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
459
|
+
assert.ok(err instanceof UserError);
|
|
460
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
461
|
+
assert.match(err.message, /target must not escape its placeholder root/);
|
|
462
|
+
return true;
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
await rm(dir, { recursive: true });
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
it("accepts a valid agentRules entry", async () => {
|
|
470
|
+
const dir = await makeTmpDir();
|
|
471
|
+
try {
|
|
472
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
473
|
+
skills: [],
|
|
474
|
+
agentRules: [
|
|
475
|
+
{
|
|
476
|
+
name: "my-rule",
|
|
477
|
+
agents: ["claude-code"],
|
|
478
|
+
path: "rules/CLAUDE.md",
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
}));
|
|
482
|
+
const manifest = await loadManifest(dir);
|
|
483
|
+
assert.equal(manifest.agentRules.length, 1);
|
|
484
|
+
assert.equal(manifest.agentRules[0]?.name, "my-rule");
|
|
485
|
+
assert.deepEqual(manifest.agentRules[0]?.agents, ["claude-code"]);
|
|
486
|
+
assert.equal(manifest.agentRules[0]?.path, "rules/CLAUDE.md");
|
|
487
|
+
}
|
|
488
|
+
finally {
|
|
489
|
+
await rm(dir, { recursive: true });
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
it("agentRules scope defaults to 'global' when omitted", async () => {
|
|
493
|
+
const dir = await makeTmpDir();
|
|
494
|
+
try {
|
|
495
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
496
|
+
skills: [],
|
|
497
|
+
agentRules: [
|
|
498
|
+
{
|
|
499
|
+
name: "my-rule",
|
|
500
|
+
agents: ["claude-code"],
|
|
501
|
+
path: "rules/CLAUDE.md",
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
}));
|
|
505
|
+
const manifest = await loadManifest(dir);
|
|
506
|
+
assert.equal(manifest.agentRules[0]?.scope, "global");
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
await rm(dir, { recursive: true });
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
it("agentRules accepts scope: 'repo'", async () => {
|
|
513
|
+
const dir = await makeTmpDir();
|
|
514
|
+
try {
|
|
515
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
516
|
+
skills: [],
|
|
517
|
+
agentRules: [
|
|
518
|
+
{
|
|
519
|
+
name: "my-rule",
|
|
520
|
+
agents: ["claude-code"],
|
|
521
|
+
path: "rules/CLAUDE.md",
|
|
522
|
+
scope: "repo",
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
}));
|
|
526
|
+
const manifest = await loadManifest(dir);
|
|
527
|
+
assert.equal(manifest.agentRules[0]?.scope, "repo");
|
|
528
|
+
}
|
|
529
|
+
finally {
|
|
530
|
+
await rm(dir, { recursive: true });
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
it("agentRules rejects an invalid scope value", async () => {
|
|
534
|
+
const dir = await makeTmpDir();
|
|
535
|
+
try {
|
|
536
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
537
|
+
skills: [],
|
|
538
|
+
agentRules: [
|
|
539
|
+
{
|
|
540
|
+
name: "my-rule",
|
|
541
|
+
agents: ["claude-code"],
|
|
542
|
+
path: "rules/CLAUDE.md",
|
|
543
|
+
scope: "invalid-scope",
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
}));
|
|
547
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
548
|
+
assert.ok(err instanceof Error);
|
|
549
|
+
assert.match(err.message, /agentRules\[0\]/);
|
|
550
|
+
return true;
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
finally {
|
|
554
|
+
await rm(dir, { recursive: true });
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
it("defaults mcpServers and agentRules to [] when omitted", async () => {
|
|
558
|
+
const dir = await makeTmpDir();
|
|
559
|
+
try {
|
|
560
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({ skills: [] }));
|
|
561
|
+
const manifest = await loadManifest(dir);
|
|
562
|
+
assert.deepEqual(manifest.mcpServers, []);
|
|
563
|
+
assert.deepEqual(manifest.agentRules, []);
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
await rm(dir, { recursive: true });
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
it("throws on duplicate skill names", async () => {
|
|
570
|
+
const dir = await makeTmpDir();
|
|
571
|
+
try {
|
|
572
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
573
|
+
skills: [
|
|
574
|
+
{ name: "my-skill", path: "skills/a", agents: ["claude-code"] },
|
|
575
|
+
{ name: "my-skill", path: "skills/b", agents: ["codex"] },
|
|
576
|
+
],
|
|
577
|
+
}));
|
|
578
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
579
|
+
assert.ok(err instanceof UserError);
|
|
580
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
581
|
+
assert.match(err.message, /duplicate skill name "my-skill"/);
|
|
582
|
+
return true;
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
await rm(dir, { recursive: true });
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
it("deduplicates agent IDs within a skill", async () => {
|
|
590
|
+
const dir = await makeTmpDir();
|
|
591
|
+
try {
|
|
592
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
593
|
+
skills: [
|
|
594
|
+
{
|
|
595
|
+
name: "my-skill",
|
|
596
|
+
path: "skills/s",
|
|
597
|
+
agents: ["claude-code", "claude-code", "codex"],
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
}));
|
|
601
|
+
const manifest = await loadManifest(dir);
|
|
602
|
+
assert.deepEqual(manifest.skills[0]?.agents, ["claude-code", "codex"]);
|
|
603
|
+
}
|
|
604
|
+
finally {
|
|
605
|
+
await rm(dir, { recursive: true });
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
it("throws when agentDefinitions is not an array", async () => {
|
|
609
|
+
const dir = await makeTmpDir();
|
|
610
|
+
try {
|
|
611
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({ skills: [], agentDefinitions: 42 }));
|
|
612
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
613
|
+
assert.ok(err instanceof UserError);
|
|
614
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
615
|
+
assert.match(err.message, /"agentDefinitions" must be an array/);
|
|
616
|
+
return true;
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
finally {
|
|
620
|
+
await rm(dir, { recursive: true });
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
it("accepts a valid agentDefinitions entry", async () => {
|
|
624
|
+
const dir = await makeTmpDir();
|
|
625
|
+
try {
|
|
626
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
627
|
+
skills: [],
|
|
628
|
+
agentDefinitions: [
|
|
629
|
+
{
|
|
630
|
+
name: "my-agent",
|
|
631
|
+
agents: ["claude-code"],
|
|
632
|
+
path: "agents/my-agent.md",
|
|
633
|
+
},
|
|
634
|
+
],
|
|
635
|
+
}));
|
|
636
|
+
const manifest = await loadManifest(dir);
|
|
637
|
+
assert.equal(manifest.agentDefinitions.length, 1);
|
|
638
|
+
assert.equal(manifest.agentDefinitions[0]?.name, "my-agent");
|
|
639
|
+
assert.deepEqual(manifest.agentDefinitions[0]?.agents, ["claude-code"]);
|
|
640
|
+
assert.equal(manifest.agentDefinitions[0]?.path, "agents/my-agent.md");
|
|
641
|
+
}
|
|
642
|
+
finally {
|
|
643
|
+
await rm(dir, { recursive: true });
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
it("defaults agentDefinitions to [] when omitted", async () => {
|
|
647
|
+
const dir = await makeTmpDir();
|
|
648
|
+
try {
|
|
649
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({ skills: [] }));
|
|
650
|
+
const manifest = await loadManifest(dir);
|
|
651
|
+
assert.deepEqual(manifest.agentDefinitions, []);
|
|
652
|
+
}
|
|
653
|
+
finally {
|
|
654
|
+
await rm(dir, { recursive: true });
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
it("throws when agentDefinitions entry is missing 'name'", async () => {
|
|
658
|
+
const dir = await makeTmpDir();
|
|
659
|
+
try {
|
|
660
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
661
|
+
skills: [],
|
|
662
|
+
agentDefinitions: [
|
|
663
|
+
{ agents: ["claude-code"], path: "agents/my-agent.md" },
|
|
664
|
+
],
|
|
665
|
+
}));
|
|
666
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
667
|
+
assert.ok(err instanceof UserError);
|
|
668
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
669
|
+
assert.match(err.message, /name must be a non-empty string/);
|
|
670
|
+
return true;
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
finally {
|
|
674
|
+
await rm(dir, { recursive: true });
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
it("throws when agentDefinitions entry is missing 'path'", async () => {
|
|
678
|
+
const dir = await makeTmpDir();
|
|
679
|
+
try {
|
|
680
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
681
|
+
skills: [],
|
|
682
|
+
agentDefinitions: [{ name: "my-agent", agents: ["claude-code"] }],
|
|
683
|
+
}));
|
|
684
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
685
|
+
assert.ok(err instanceof UserError);
|
|
686
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
687
|
+
assert.match(err.message, /agentDefinitions\[0\]/);
|
|
688
|
+
return true;
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
finally {
|
|
692
|
+
await rm(dir, { recursive: true });
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
it("throws when agentDefinitions entry has an escaping 'path'", async () => {
|
|
696
|
+
const dir = await makeTmpDir();
|
|
697
|
+
try {
|
|
698
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
699
|
+
skills: [],
|
|
700
|
+
agentDefinitions: [
|
|
701
|
+
{
|
|
702
|
+
name: "my-agent",
|
|
703
|
+
agents: ["claude-code"],
|
|
704
|
+
path: "../../../etc/passwd",
|
|
705
|
+
},
|
|
706
|
+
],
|
|
707
|
+
}));
|
|
708
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
709
|
+
assert.ok(err instanceof UserError);
|
|
710
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
711
|
+
assert.match(err.message, /must not escape the repository root/);
|
|
712
|
+
return true;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
finally {
|
|
716
|
+
await rm(dir, { recursive: true });
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
it("throws when agentDefinitions entry is missing 'agents'", async () => {
|
|
720
|
+
const dir = await makeTmpDir();
|
|
721
|
+
try {
|
|
722
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({
|
|
723
|
+
skills: [],
|
|
724
|
+
agentDefinitions: [{ name: "my-agent", path: "agents/my-agent.md" }],
|
|
725
|
+
}));
|
|
726
|
+
await assert.rejects(loadManifest(dir), (err) => {
|
|
727
|
+
assert.ok(err instanceof UserError);
|
|
728
|
+
assert.equal(err.code, "MANIFEST_INVALID");
|
|
729
|
+
assert.match(err.message, /agents must be a non-empty array/);
|
|
730
|
+
return true;
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
finally {
|
|
734
|
+
await rm(dir, { recursive: true });
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
});
|