@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,834 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import { makeTmpDir } from "../helpers/fs.js";
|
|
7
|
+
import { normalizeSlashes } from "../helpers/path.js";
|
|
8
|
+
const PROJECT_ROOT = path.resolve(import.meta.dirname, "..", "..");
|
|
9
|
+
function run(args, env) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
const child = spawn(process.execPath, [path.join(PROJECT_ROOT, "src", "index.ts"), ...args], {
|
|
12
|
+
cwd: PROJECT_ROOT,
|
|
13
|
+
env: { ...process.env, ...env },
|
|
14
|
+
});
|
|
15
|
+
let stdout = "";
|
|
16
|
+
let stderr = "";
|
|
17
|
+
child.stdout.on("data", (d) => {
|
|
18
|
+
stdout += d.toString();
|
|
19
|
+
});
|
|
20
|
+
child.stderr.on("data", (d) => {
|
|
21
|
+
stderr += d.toString();
|
|
22
|
+
});
|
|
23
|
+
child.on("close", (code) => {
|
|
24
|
+
resolve({ stdout, stderr, code: code ?? 1 });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function makeValidRepo(dir, skillName = "test-skill", agents = ["claude-code"]) {
|
|
29
|
+
const manifest = {
|
|
30
|
+
skills: [{ name: skillName, path: `skills/${skillName}`, agents }],
|
|
31
|
+
files: [],
|
|
32
|
+
configs: [],
|
|
33
|
+
mcpServers: [],
|
|
34
|
+
agentRules: [],
|
|
35
|
+
};
|
|
36
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify(manifest, null, 2));
|
|
37
|
+
const skillDir = path.join(dir, "skills", skillName);
|
|
38
|
+
await mkdir(skillDir, { recursive: true });
|
|
39
|
+
await writeFile(path.join(skillDir, "SKILL.md"), "---\nname: test-skill\ndescription: A test skill\n---\n# Test\n");
|
|
40
|
+
}
|
|
41
|
+
describe("CLI exit codes and output", () => {
|
|
42
|
+
it("--help exits 0 and prints usage", async () => {
|
|
43
|
+
const { stdout, code } = await run(["--help"]);
|
|
44
|
+
assert.equal(code, 0);
|
|
45
|
+
assert.ok(stdout.includes("inception-engine"), "stdout should include tool name");
|
|
46
|
+
assert.ok(stdout.includes("--plan"), "stdout should include options");
|
|
47
|
+
});
|
|
48
|
+
it("no args exits 0 and prints usage", async () => {
|
|
49
|
+
const { stdout, code } = await run([]);
|
|
50
|
+
assert.equal(code, 0);
|
|
51
|
+
assert.ok(stdout.includes("inception-engine"), "stdout should include tool name");
|
|
52
|
+
});
|
|
53
|
+
it("unknown --agents value exits 2", async () => {
|
|
54
|
+
const dir = await makeTmpDir();
|
|
55
|
+
try {
|
|
56
|
+
await makeValidRepo(dir);
|
|
57
|
+
const { stderr, code } = await run([
|
|
58
|
+
dir,
|
|
59
|
+
"--agents",
|
|
60
|
+
"invalid-xyz-agent",
|
|
61
|
+
]);
|
|
62
|
+
assert.equal(code, 2);
|
|
63
|
+
assert.ok(stderr.includes("Error:") || stderr.includes("invalid"), `stderr should mention error: ${stderr}`);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await rm(dir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
it("non-existent directory exits 3", async () => {
|
|
70
|
+
const { stderr, code } = await run(["/absolutely/nonexistent/path/xyz"]);
|
|
71
|
+
assert.equal(code, 3);
|
|
72
|
+
assert.ok(stderr.includes("Error:"), `stderr should mention error: ${stderr}`);
|
|
73
|
+
});
|
|
74
|
+
it("directory with malformed JSON exits 3", async () => {
|
|
75
|
+
const dir = await makeTmpDir();
|
|
76
|
+
try {
|
|
77
|
+
await writeFile(path.join(dir, "inception.json"), "{not valid json}");
|
|
78
|
+
const { stderr, code } = await run([dir]);
|
|
79
|
+
assert.equal(code, 3);
|
|
80
|
+
assert.ok(stderr.includes("Error:"), `stderr should mention error: ${stderr}`);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await rm(dir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
it("directory with schema-invalid manifest exits 3", async () => {
|
|
87
|
+
const dir = await makeTmpDir();
|
|
88
|
+
try {
|
|
89
|
+
await writeFile(path.join(dir, "inception.json"), JSON.stringify({ skills: "not-an-array" }));
|
|
90
|
+
const { stderr, code } = await run([dir]);
|
|
91
|
+
assert.equal(code, 3);
|
|
92
|
+
assert.ok(stderr.includes("Error:"), `stderr should mention error: ${stderr}`);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await rm(dir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
it("valid manifest with --plan exits 0 and shows plan", async () => {
|
|
99
|
+
const dir = await makeTmpDir();
|
|
100
|
+
try {
|
|
101
|
+
await makeValidRepo(dir);
|
|
102
|
+
const { stdout, code } = await run([
|
|
103
|
+
dir,
|
|
104
|
+
"--plan",
|
|
105
|
+
"--agents",
|
|
106
|
+
"claude-code",
|
|
107
|
+
]);
|
|
108
|
+
assert.equal(code, 0);
|
|
109
|
+
assert.ok(stdout.includes("[plan]"), `stdout should include [plan]: ${stdout}`);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await rm(dir, { recursive: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
it("revert --plan with valid manifest exits 0", async () => {
|
|
116
|
+
const dir = await makeTmpDir();
|
|
117
|
+
try {
|
|
118
|
+
await makeValidRepo(dir);
|
|
119
|
+
const { code } = await run([
|
|
120
|
+
"revert",
|
|
121
|
+
dir,
|
|
122
|
+
"--plan",
|
|
123
|
+
"--agents",
|
|
124
|
+
"claude-code",
|
|
125
|
+
]);
|
|
126
|
+
assert.equal(code, 0);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await rm(dir, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
it("missing <directory> argument exits 2", async () => {
|
|
133
|
+
// Passing only a valid option but no positional directory
|
|
134
|
+
const { stderr, code } = await run(["--plan"]);
|
|
135
|
+
assert.equal(code, 2);
|
|
136
|
+
assert.ok(stderr.includes("Error:"), `stderr should mention error: ${stderr}`);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe("init command", () => {
|
|
140
|
+
it("generates inception.json from a directory with skill folders", async () => {
|
|
141
|
+
const dir = await makeTmpDir();
|
|
142
|
+
try {
|
|
143
|
+
// Create two skill directories
|
|
144
|
+
for (const name of ["alpha", "beta"]) {
|
|
145
|
+
await mkdir(path.join(dir, "skills", name), { recursive: true });
|
|
146
|
+
await writeFile(path.join(dir, "skills", name, "SKILL.md"), `---\nname: ${name}\ndescription: A skill\n---\n`);
|
|
147
|
+
}
|
|
148
|
+
const { stdout, code } = await run(["init", dir]);
|
|
149
|
+
assert.equal(code, 0, `stdout: ${stdout}`);
|
|
150
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
151
|
+
assert.equal(manifest.skills.length, 2);
|
|
152
|
+
const names = manifest.skills.map((s) => s.name).sort();
|
|
153
|
+
assert.deepEqual(names, ["alpha", "beta"]);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
await rm(dir, { recursive: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
it("--plan does not write inception.json", async () => {
|
|
160
|
+
const dir = await makeTmpDir();
|
|
161
|
+
try {
|
|
162
|
+
await mkdir(path.join(dir, "my-skill"), { recursive: true });
|
|
163
|
+
await writeFile(path.join(dir, "my-skill", "SKILL.md"), "---\nname: my-skill\ndescription: test\n---\n");
|
|
164
|
+
const { stdout, code } = await run(["init", dir, "--plan"]);
|
|
165
|
+
assert.equal(code, 0, `stdout: ${stdout}`);
|
|
166
|
+
assert.ok(stdout.includes("[plan]"), `stdout should include [plan]: ${stdout}`);
|
|
167
|
+
// File should NOT have been written
|
|
168
|
+
const { access } = await import("node:fs/promises");
|
|
169
|
+
let exists = true;
|
|
170
|
+
try {
|
|
171
|
+
await access(path.join(dir, "inception.json"));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
exists = false;
|
|
175
|
+
}
|
|
176
|
+
assert.ok(!exists, "inception.json should not exist after plan");
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
await rm(dir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
it("refuses to overwrite existing inception.json without --force", async () => {
|
|
183
|
+
const dir = await makeTmpDir();
|
|
184
|
+
try {
|
|
185
|
+
await mkdir(path.join(dir, "my-skill"), { recursive: true });
|
|
186
|
+
await writeFile(path.join(dir, "my-skill", "SKILL.md"), "---\nname: my-skill\ndescription: test\n---\n");
|
|
187
|
+
await writeFile(path.join(dir, "inception.json"), "{}");
|
|
188
|
+
const { code } = await run(["init", dir]);
|
|
189
|
+
assert.equal(code, 2);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
await rm(dir, { recursive: true });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
it("--force overwrites existing inception.json", async () => {
|
|
196
|
+
const dir = await makeTmpDir();
|
|
197
|
+
try {
|
|
198
|
+
await mkdir(path.join(dir, "my-skill"), { recursive: true });
|
|
199
|
+
await writeFile(path.join(dir, "my-skill", "SKILL.md"), "---\nname: my-skill\ndescription: test\n---\n");
|
|
200
|
+
await writeFile(path.join(dir, "inception.json"), "{}");
|
|
201
|
+
const { code } = await run(["init", dir, "--force"]);
|
|
202
|
+
assert.equal(code, 0);
|
|
203
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
204
|
+
assert.equal(manifest.skills.length, 1);
|
|
205
|
+
assert.equal(manifest.skills[0].name, "my-skill");
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
await rm(dir, { recursive: true });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
it("--agents restricts agent list in generated manifest", async () => {
|
|
212
|
+
const dir = await makeTmpDir();
|
|
213
|
+
try {
|
|
214
|
+
await mkdir(path.join(dir, "my-skill"), { recursive: true });
|
|
215
|
+
await writeFile(path.join(dir, "my-skill", "SKILL.md"), "---\nname: my-skill\ndescription: test\n---\n");
|
|
216
|
+
const { code } = await run([
|
|
217
|
+
"init",
|
|
218
|
+
dir,
|
|
219
|
+
"--agents",
|
|
220
|
+
"claude-code,codex",
|
|
221
|
+
]);
|
|
222
|
+
assert.equal(code, 0);
|
|
223
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
224
|
+
assert.deepEqual(manifest.skills[0].agents.sort(), [
|
|
225
|
+
"claude-code",
|
|
226
|
+
"codex",
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
await rm(dir, { recursive: true });
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
it("empty directory with no skills exits 0 with info message and writes manifest", async () => {
|
|
234
|
+
const dir = await makeTmpDir();
|
|
235
|
+
try {
|
|
236
|
+
const { stdout, code } = await run(["init", dir]);
|
|
237
|
+
assert.equal(code, 0);
|
|
238
|
+
assert.ok(stdout.includes("No skill"), `stdout should mention no skills: ${stdout}`);
|
|
239
|
+
// manifest is still written even with no skills
|
|
240
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
241
|
+
assert.ok(Array.isArray(manifest.skills), "manifest.skills should be an array");
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
await rm(dir, { recursive: true });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
it("generated manifest always includes all five section keys", async () => {
|
|
248
|
+
const dir = await makeTmpDir();
|
|
249
|
+
try {
|
|
250
|
+
await mkdir(path.join(dir, "my-skill"), { recursive: true });
|
|
251
|
+
await writeFile(path.join(dir, "my-skill", "SKILL.md"), "---\nname: my-skill\ndescription: test\n---\n");
|
|
252
|
+
const { code } = await run(["init", dir]);
|
|
253
|
+
assert.equal(code, 0);
|
|
254
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
255
|
+
assert.ok(Array.isArray(manifest.skills), "manifest.skills should be an array");
|
|
256
|
+
assert.ok(Array.isArray(manifest.files), "manifest.files should be an array");
|
|
257
|
+
assert.ok(Array.isArray(manifest.configs), "manifest.configs should be an array");
|
|
258
|
+
assert.ok(Array.isArray(manifest.mcpServers), "manifest.mcpServers should be an array");
|
|
259
|
+
assert.ok(Array.isArray(manifest.agentRules), "manifest.agentRules should be an array");
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
await rm(dir, { recursive: true });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
it("discovers CLAUDE.md as agentRules entry for claude-code", async () => {
|
|
266
|
+
const dir = await makeTmpDir();
|
|
267
|
+
try {
|
|
268
|
+
await mkdir(path.join(dir, "my-skill"), { recursive: true });
|
|
269
|
+
await writeFile(path.join(dir, "my-skill", "SKILL.md"), "---\nname: my-skill\ndescription: test\n---\n");
|
|
270
|
+
await writeFile(path.join(dir, "CLAUDE.md"), "# rules\n");
|
|
271
|
+
const { code } = await run(["init", dir]);
|
|
272
|
+
assert.equal(code, 0);
|
|
273
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
274
|
+
assert.ok(manifest.agentRules.length > 0, "should have at least one agentRules entry");
|
|
275
|
+
const entry = manifest.agentRules.find((r) => r.path.endsWith("CLAUDE.md"));
|
|
276
|
+
assert.ok(entry, "should have an entry for CLAUDE.md");
|
|
277
|
+
assert.ok(entry?.agents.includes("claude-code"), `agents should include claude-code, got: ${JSON.stringify(entry?.agents)}`);
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
await rm(dir, { recursive: true });
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
it("discovers AGENTS.md as agentRules entry for codex and opencode", async () => {
|
|
284
|
+
const dir = await makeTmpDir();
|
|
285
|
+
try {
|
|
286
|
+
await mkdir(path.join(dir, "my-skill"), { recursive: true });
|
|
287
|
+
await writeFile(path.join(dir, "my-skill", "SKILL.md"), "---\nname: my-skill\ndescription: test\n---\n");
|
|
288
|
+
await writeFile(path.join(dir, "AGENTS.md"), "# rules\n");
|
|
289
|
+
const { code } = await run(["init", dir]);
|
|
290
|
+
assert.equal(code, 0);
|
|
291
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
292
|
+
const entry = manifest.agentRules.find((r) => r.path.endsWith("AGENTS.md"));
|
|
293
|
+
assert.ok(entry, "should have an entry for AGENTS.md");
|
|
294
|
+
assert.deepEqual(entry?.agents.slice().sort(), ["codex", "opencode"], `expected codex+opencode, got: ${JSON.stringify(entry?.agents)}`);
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
await rm(dir, { recursive: true });
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
it("discovers GEMINI.md as agentRules entry for gemini-cli only", async () => {
|
|
301
|
+
const dir = await makeTmpDir();
|
|
302
|
+
try {
|
|
303
|
+
await mkdir(path.join(dir, "my-skill"), { recursive: true });
|
|
304
|
+
await writeFile(path.join(dir, "my-skill", "SKILL.md"), "---\nname: my-skill\ndescription: test\n---\n");
|
|
305
|
+
await writeFile(path.join(dir, "GEMINI.md"), "# rules\n");
|
|
306
|
+
const { code } = await run(["init", dir]);
|
|
307
|
+
assert.equal(code, 0);
|
|
308
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
309
|
+
const entry = manifest.agentRules.find((r) => r.path.endsWith("GEMINI.md"));
|
|
310
|
+
assert.ok(entry, "should have an entry for GEMINI.md");
|
|
311
|
+
assert.deepEqual(entry?.agents.slice().sort(), ["gemini-cli"], `expected gemini-cli only, got: ${JSON.stringify(entry?.agents)}`);
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
await rm(dir, { recursive: true });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
it("--agents filter is applied to discovered agentRules entries", async () => {
|
|
318
|
+
const dir = await makeTmpDir();
|
|
319
|
+
try {
|
|
320
|
+
await mkdir(path.join(dir, "my-skill"), { recursive: true });
|
|
321
|
+
await writeFile(path.join(dir, "my-skill", "SKILL.md"), "---\nname: my-skill\ndescription: test\n---\n");
|
|
322
|
+
// CLAUDE.md normally maps to claude-code only; with --agents codex it should
|
|
323
|
+
// fall back to the active agents list since intersection would be empty
|
|
324
|
+
await writeFile(path.join(dir, "CLAUDE.md"), "# rules\n");
|
|
325
|
+
const { code } = await run(["init", dir, "--agents", "codex"]);
|
|
326
|
+
assert.equal(code, 0);
|
|
327
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
328
|
+
const entry = manifest.agentRules.find((r) => r.path.endsWith("CLAUDE.md"));
|
|
329
|
+
assert.ok(entry, "should have an entry for CLAUDE.md");
|
|
330
|
+
// intersection of [claude-code] and [codex] is empty → falls back to activeAgents [codex]
|
|
331
|
+
assert.deepEqual(entry?.agents, ["codex"], `expected [codex] fallback, got: ${JSON.stringify(entry?.agents)}`);
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
await rm(dir, { recursive: true });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
it("directory with no .md files produces agentRules: []", async () => {
|
|
338
|
+
const dir = await makeTmpDir();
|
|
339
|
+
try {
|
|
340
|
+
await mkdir(path.join(dir, "my-skill"), { recursive: true });
|
|
341
|
+
await writeFile(path.join(dir, "my-skill", "SKILL.md"), "---\nname: my-skill\ndescription: test\n---\n");
|
|
342
|
+
const { code } = await run(["init", dir]);
|
|
343
|
+
assert.equal(code, 0);
|
|
344
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
345
|
+
assert.deepEqual(manifest.agentRules, []);
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
await rm(dir, { recursive: true });
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
it("agentRules name collision with skill gets -rules suffix", async () => {
|
|
352
|
+
const dir = await makeTmpDir();
|
|
353
|
+
try {
|
|
354
|
+
// Skill named "my-rules", and a file "my-rules.md" → name collision
|
|
355
|
+
await mkdir(path.join(dir, "my-rules"), { recursive: true });
|
|
356
|
+
await writeFile(path.join(dir, "my-rules", "SKILL.md"), "---\nname: my-rules\ndescription: test\n---\n");
|
|
357
|
+
await writeFile(path.join(dir, "my-rules.md"), "# rules\n");
|
|
358
|
+
const { code } = await run(["init", dir]);
|
|
359
|
+
assert.equal(code, 0);
|
|
360
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
361
|
+
assert.ok(manifest.skills.some((s) => s.name === "my-rules"), "skill name should be my-rules");
|
|
362
|
+
const rulesEntry = manifest.agentRules.find((r) => r.path.endsWith("my-rules.md"));
|
|
363
|
+
assert.ok(rulesEntry, "should have agentRules entry for my-rules.md");
|
|
364
|
+
assert.equal(rulesEntry?.name, "my-rules-rules", `expected my-rules-rules, got: ${rulesEntry?.name}`);
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
await rm(dir, { recursive: true });
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
// --- portability reconciliation ---
|
|
371
|
+
it("copilot-instructions.md maps to claude-code, not github-copilot", async () => {
|
|
372
|
+
const dir = await makeTmpDir();
|
|
373
|
+
try {
|
|
374
|
+
await writeFile(path.join(dir, "copilot-instructions.md"), "# rules\n");
|
|
375
|
+
const { code } = await run(["init", dir]);
|
|
376
|
+
assert.equal(code, 0);
|
|
377
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
378
|
+
const entry = manifest.agentRules.find((r) => r.path.includes("copilot-instructions"));
|
|
379
|
+
assert.ok(entry, "should have an entry for copilot-instructions.md");
|
|
380
|
+
assert.ok(entry?.agents.includes("claude-code"), `agents should include claude-code, got: ${JSON.stringify(entry?.agents)}`);
|
|
381
|
+
assert.ok(!entry?.agents.includes("github-copilot"), `agents should not include github-copilot, got: ${JSON.stringify(entry?.agents)}`);
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
await rm(dir, { recursive: true });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
it("unrecognized .md fallback excludes github-copilot", async () => {
|
|
388
|
+
const dir = await makeTmpDir();
|
|
389
|
+
try {
|
|
390
|
+
await writeFile(path.join(dir, "my-custom-rules.md"), "# rules\n");
|
|
391
|
+
const { code } = await run(["init", dir]);
|
|
392
|
+
assert.equal(code, 0);
|
|
393
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
394
|
+
const entry = manifest.agentRules.find((r) => r.path.includes("my-custom-rules"));
|
|
395
|
+
assert.ok(entry, "should have an entry for my-custom-rules.md");
|
|
396
|
+
assert.ok(!entry?.agents.includes("github-copilot"), `fallback agents should not include github-copilot, got: ${JSON.stringify(entry?.agents)}`);
|
|
397
|
+
assert.ok(!entry?.agents.includes("antigravity"), `fallback agents should not include antigravity rider surfaces, got: ${JSON.stringify(entry?.agents)}`);
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
await rm(dir, { recursive: true });
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
it("--agents github-copilot produces agentRules: []", async () => {
|
|
404
|
+
const dir = await makeTmpDir();
|
|
405
|
+
try {
|
|
406
|
+
await writeFile(path.join(dir, "my-custom-rules.md"), "# rules\n");
|
|
407
|
+
const { code } = await run(["init", dir, "--agents", "github-copilot"]);
|
|
408
|
+
assert.equal(code, 0);
|
|
409
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
410
|
+
assert.deepEqual(manifest.agentRules, [], "github-copilot cannot receive agentRules, so agentRules should be []");
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
413
|
+
await rm(dir, { recursive: true });
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
it("generated skills exclude github-copilot because skills are shared via claude-code", async () => {
|
|
417
|
+
const dir = await makeTmpDir();
|
|
418
|
+
try {
|
|
419
|
+
await mkdir(path.join(dir, "my-skill"), { recursive: true });
|
|
420
|
+
await writeFile(path.join(dir, "my-skill", "SKILL.md"), "---\nname: my-skill\ndescription: test\n---\n");
|
|
421
|
+
const { code } = await run(["init", dir]);
|
|
422
|
+
assert.equal(code, 0);
|
|
423
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
424
|
+
assert.ok(manifest.skills[0], "expected one generated skill");
|
|
425
|
+
assert.ok(!manifest.skills[0].agents.includes("github-copilot"), `skills should not include github-copilot, got: ${JSON.stringify(manifest.skills[0].agents)}`);
|
|
426
|
+
}
|
|
427
|
+
finally {
|
|
428
|
+
await rm(dir, { recursive: true });
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
it("--agents antigravity alone produces agentRules: [] because shared surfaces default to the primary agent", async () => {
|
|
432
|
+
const dir = await makeTmpDir();
|
|
433
|
+
try {
|
|
434
|
+
await writeFile(path.join(dir, "my-custom-rules.md").toString(), "# rules\n");
|
|
435
|
+
const { code } = await run(["init", dir, "--agents", "antigravity"]);
|
|
436
|
+
assert.equal(code, 0);
|
|
437
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
438
|
+
assert.deepEqual(manifest.agentRules, []);
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
await rm(dir, { recursive: true });
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
// --- mcp-servers.json discovery ---
|
|
445
|
+
it("valid mcp-servers.json populates mcpServers", async () => {
|
|
446
|
+
const dir = await makeTmpDir();
|
|
447
|
+
try {
|
|
448
|
+
await writeFile(path.join(dir, "mcp-servers.json"), JSON.stringify([
|
|
449
|
+
{
|
|
450
|
+
name: "my-server",
|
|
451
|
+
agents: ["claude-code"],
|
|
452
|
+
config: { command: "npx", args: ["-y", "my-pkg"] },
|
|
453
|
+
},
|
|
454
|
+
]));
|
|
455
|
+
const { code } = await run(["init", dir]);
|
|
456
|
+
assert.equal(code, 0);
|
|
457
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
458
|
+
assert.equal(manifest.mcpServers.length, 1);
|
|
459
|
+
assert.equal(manifest.mcpServers[0].name, "my-server");
|
|
460
|
+
assert.deepEqual(manifest.mcpServers[0].agents, ["claude-code"]);
|
|
461
|
+
assert.equal(manifest.mcpServers[0].config.command, "npx");
|
|
462
|
+
}
|
|
463
|
+
finally {
|
|
464
|
+
await rm(dir, { recursive: true });
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
it("invalid JSON in mcp-servers.json produces mcpServers: [] and a warning", async () => {
|
|
468
|
+
const dir = await makeTmpDir();
|
|
469
|
+
try {
|
|
470
|
+
await writeFile(path.join(dir, "mcp-servers.json"), "{not json}");
|
|
471
|
+
const { code, stdout } = await run(["init", dir]);
|
|
472
|
+
assert.equal(code, 0);
|
|
473
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
474
|
+
assert.deepEqual(manifest.mcpServers, []);
|
|
475
|
+
assert.ok(stdout.includes("mcp-servers.json"), "stdout should mention mcp-servers.json warning");
|
|
476
|
+
}
|
|
477
|
+
finally {
|
|
478
|
+
await rm(dir, { recursive: true });
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
it("non-array mcp-servers.json produces mcpServers: [] and a warning", async () => {
|
|
482
|
+
const dir = await makeTmpDir();
|
|
483
|
+
try {
|
|
484
|
+
await writeFile(path.join(dir, "mcp-servers.json"), JSON.stringify({ name: "not-an-array" }));
|
|
485
|
+
const { code, stdout } = await run(["init", dir]);
|
|
486
|
+
assert.equal(code, 0);
|
|
487
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
488
|
+
assert.deepEqual(manifest.mcpServers, []);
|
|
489
|
+
assert.ok(stdout.includes("mcp-servers.json"), "stdout should mention mcp-servers.json warning");
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
await rm(dir, { recursive: true });
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
it("mcp-servers.json with mixed valid/invalid entries only keeps valid ones", async () => {
|
|
496
|
+
const dir = await makeTmpDir();
|
|
497
|
+
try {
|
|
498
|
+
await writeFile(path.join(dir, "mcp-servers.json"), JSON.stringify([
|
|
499
|
+
{
|
|
500
|
+
name: "valid-server",
|
|
501
|
+
agents: ["claude-code"],
|
|
502
|
+
config: { command: "npx" },
|
|
503
|
+
},
|
|
504
|
+
{ name: "missing-config-and-agents" },
|
|
505
|
+
]));
|
|
506
|
+
const { code } = await run(["init", dir]);
|
|
507
|
+
assert.equal(code, 0);
|
|
508
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
509
|
+
assert.equal(manifest.mcpServers.length, 1);
|
|
510
|
+
assert.equal(manifest.mcpServers[0].name, "valid-server");
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
await rm(dir, { recursive: true });
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
it("missing mcp-servers.json produces mcpServers: [] with no warning", async () => {
|
|
517
|
+
const dir = await makeTmpDir();
|
|
518
|
+
try {
|
|
519
|
+
const { code, stdout } = await run(["init", dir]);
|
|
520
|
+
assert.equal(code, 0);
|
|
521
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
522
|
+
assert.deepEqual(manifest.mcpServers, []);
|
|
523
|
+
assert.ok(!stdout.includes("mcp-servers.json"), "stdout should not mention mcp-servers.json when file is absent");
|
|
524
|
+
}
|
|
525
|
+
finally {
|
|
526
|
+
await rm(dir, { recursive: true });
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
// --- files/ and configs/ directory hints ---
|
|
530
|
+
it("files/ directory produces a hint in stdout and files remains []", async () => {
|
|
531
|
+
const dir = await makeTmpDir();
|
|
532
|
+
try {
|
|
533
|
+
await mkdir(path.join(dir, "files"), { recursive: true });
|
|
534
|
+
await writeFile(path.join(dir, "files", "settings.json"), "{}");
|
|
535
|
+
const { code, stdout } = await run(["init", dir]);
|
|
536
|
+
assert.equal(code, 0);
|
|
537
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
538
|
+
assert.deepEqual(manifest.files, []);
|
|
539
|
+
assert.ok(stdout.includes("files/"), "stdout should mention the files/ directory");
|
|
540
|
+
}
|
|
541
|
+
finally {
|
|
542
|
+
await rm(dir, { recursive: true });
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
it("configs/ directory produces a hint in stdout and configs remains []", async () => {
|
|
546
|
+
const dir = await makeTmpDir();
|
|
547
|
+
try {
|
|
548
|
+
await mkdir(path.join(dir, "configs"), { recursive: true });
|
|
549
|
+
await writeFile(path.join(dir, "configs", "patch.json"), "{}");
|
|
550
|
+
const { code, stdout } = await run(["init", dir]);
|
|
551
|
+
assert.equal(code, 0);
|
|
552
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
553
|
+
assert.deepEqual(manifest.configs, []);
|
|
554
|
+
assert.ok(stdout.includes("configs/"), "stdout should mention the configs/ directory");
|
|
555
|
+
}
|
|
556
|
+
finally {
|
|
557
|
+
await rm(dir, { recursive: true });
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
// --- files-manifest.json discovery ---
|
|
561
|
+
it("valid files-manifest.json populates files", async () => {
|
|
562
|
+
const dir = await makeTmpDir();
|
|
563
|
+
try {
|
|
564
|
+
await writeFile(path.join(dir, "source.txt"), "hello");
|
|
565
|
+
await writeFile(path.join(dir, "files-manifest.json"), JSON.stringify([
|
|
566
|
+
{
|
|
567
|
+
name: "my-file",
|
|
568
|
+
path: "source.txt",
|
|
569
|
+
target: "{home}/.myconfig",
|
|
570
|
+
agents: ["claude-code"],
|
|
571
|
+
},
|
|
572
|
+
]));
|
|
573
|
+
const { code } = await run(["init", dir]);
|
|
574
|
+
assert.equal(code, 0);
|
|
575
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
576
|
+
assert.equal(manifest.files.length, 1);
|
|
577
|
+
assert.equal(manifest.files[0].name, "my-file");
|
|
578
|
+
assert.equal(manifest.files[0].target, "{home}/.myconfig");
|
|
579
|
+
assert.deepEqual(manifest.files[0].agents, ["claude-code"]);
|
|
580
|
+
}
|
|
581
|
+
finally {
|
|
582
|
+
await rm(dir, { recursive: true });
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
it("invalid JSON in files-manifest.json produces files: [] and a warning", async () => {
|
|
586
|
+
const dir = await makeTmpDir();
|
|
587
|
+
try {
|
|
588
|
+
await writeFile(path.join(dir, "files-manifest.json"), "{not json}");
|
|
589
|
+
const { code, stdout } = await run(["init", dir]);
|
|
590
|
+
assert.equal(code, 0);
|
|
591
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
592
|
+
assert.deepEqual(manifest.files, []);
|
|
593
|
+
assert.ok(stdout.includes("files-manifest.json"), "stdout should mention files-manifest.json warning");
|
|
594
|
+
}
|
|
595
|
+
finally {
|
|
596
|
+
await rm(dir, { recursive: true });
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
it("non-array files-manifest.json produces files: [] and a warning", async () => {
|
|
600
|
+
const dir = await makeTmpDir();
|
|
601
|
+
try {
|
|
602
|
+
await writeFile(path.join(dir, "files-manifest.json"), JSON.stringify({ name: "not-an-array" }));
|
|
603
|
+
const { code, stdout } = await run(["init", dir]);
|
|
604
|
+
assert.equal(code, 0);
|
|
605
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
606
|
+
assert.deepEqual(manifest.files, []);
|
|
607
|
+
assert.ok(stdout.includes("files-manifest.json"), "stdout should mention files-manifest.json warning");
|
|
608
|
+
}
|
|
609
|
+
finally {
|
|
610
|
+
await rm(dir, { recursive: true });
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
it("files-manifest.json with mixed valid/invalid entries only keeps valid ones", async () => {
|
|
614
|
+
const dir = await makeTmpDir();
|
|
615
|
+
try {
|
|
616
|
+
await writeFile(path.join(dir, "files-manifest.json"), JSON.stringify([
|
|
617
|
+
{
|
|
618
|
+
name: "valid-file",
|
|
619
|
+
path: "source.txt",
|
|
620
|
+
target: "{home}/.myconfig",
|
|
621
|
+
agents: ["claude-code"],
|
|
622
|
+
},
|
|
623
|
+
{ name: "missing-required-fields" },
|
|
624
|
+
]));
|
|
625
|
+
const { code } = await run(["init", dir]);
|
|
626
|
+
assert.equal(code, 0);
|
|
627
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
628
|
+
assert.equal(manifest.files.length, 1);
|
|
629
|
+
assert.equal(manifest.files[0].name, "valid-file");
|
|
630
|
+
}
|
|
631
|
+
finally {
|
|
632
|
+
await rm(dir, { recursive: true });
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
it("files/ dir with valid files-manifest.json suppresses the hint", async () => {
|
|
636
|
+
const dir = await makeTmpDir();
|
|
637
|
+
try {
|
|
638
|
+
await mkdir(path.join(dir, "files"), { recursive: true });
|
|
639
|
+
await writeFile(path.join(dir, "files", "settings.json"), "{}");
|
|
640
|
+
await writeFile(path.join(dir, "files-manifest.json"), JSON.stringify([
|
|
641
|
+
{
|
|
642
|
+
name: "my-settings",
|
|
643
|
+
path: "files/settings.json",
|
|
644
|
+
target: "{home}/.claude/settings.json",
|
|
645
|
+
agents: ["claude-code"],
|
|
646
|
+
},
|
|
647
|
+
]));
|
|
648
|
+
const { code, stdout } = await run(["init", dir]);
|
|
649
|
+
assert.equal(code, 0);
|
|
650
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
651
|
+
assert.equal(manifest.files.length, 1);
|
|
652
|
+
assert.ok(!stdout.includes("create files-manifest.json"), "hint should not appear when files were loaded from files-manifest.json");
|
|
653
|
+
}
|
|
654
|
+
finally {
|
|
655
|
+
await rm(dir, { recursive: true });
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
// --- configs-manifest.json discovery ---
|
|
659
|
+
it("valid configs-manifest.json populates configs", async () => {
|
|
660
|
+
const dir = await makeTmpDir();
|
|
661
|
+
try {
|
|
662
|
+
await writeFile(path.join(dir, "configs-manifest.json"), JSON.stringify([
|
|
663
|
+
{
|
|
664
|
+
name: "enable-feature",
|
|
665
|
+
target: "{home}/.claude/settings.json",
|
|
666
|
+
patch: { someFeature: true },
|
|
667
|
+
agents: ["claude-code"],
|
|
668
|
+
},
|
|
669
|
+
]));
|
|
670
|
+
const { code } = await run(["init", dir]);
|
|
671
|
+
assert.equal(code, 0);
|
|
672
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
673
|
+
assert.equal(manifest.configs.length, 1);
|
|
674
|
+
assert.equal(manifest.configs[0].name, "enable-feature");
|
|
675
|
+
assert.equal(manifest.configs[0].target, "{home}/.claude/settings.json");
|
|
676
|
+
assert.deepEqual(manifest.configs[0].patch, { someFeature: true });
|
|
677
|
+
}
|
|
678
|
+
finally {
|
|
679
|
+
await rm(dir, { recursive: true });
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
it("invalid JSON in configs-manifest.json produces configs: [] and a warning", async () => {
|
|
683
|
+
const dir = await makeTmpDir();
|
|
684
|
+
try {
|
|
685
|
+
await writeFile(path.join(dir, "configs-manifest.json"), "{not json}");
|
|
686
|
+
const { code, stdout } = await run(["init", dir]);
|
|
687
|
+
assert.equal(code, 0);
|
|
688
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
689
|
+
assert.deepEqual(manifest.configs, []);
|
|
690
|
+
assert.ok(stdout.includes("configs-manifest.json"), "stdout should mention configs-manifest.json warning");
|
|
691
|
+
}
|
|
692
|
+
finally {
|
|
693
|
+
await rm(dir, { recursive: true });
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
it("non-array configs-manifest.json produces configs: [] and a warning", async () => {
|
|
697
|
+
const dir = await makeTmpDir();
|
|
698
|
+
try {
|
|
699
|
+
await writeFile(path.join(dir, "configs-manifest.json"), JSON.stringify({ name: "not-an-array" }));
|
|
700
|
+
const { code, stdout } = await run(["init", dir]);
|
|
701
|
+
assert.equal(code, 0);
|
|
702
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
703
|
+
assert.deepEqual(manifest.configs, []);
|
|
704
|
+
assert.ok(stdout.includes("configs-manifest.json"), "stdout should mention configs-manifest.json warning");
|
|
705
|
+
}
|
|
706
|
+
finally {
|
|
707
|
+
await rm(dir, { recursive: true });
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
it("configs-manifest.json with mixed valid/invalid entries only keeps valid ones", async () => {
|
|
711
|
+
const dir = await makeTmpDir();
|
|
712
|
+
try {
|
|
713
|
+
await writeFile(path.join(dir, "configs-manifest.json"), JSON.stringify([
|
|
714
|
+
{
|
|
715
|
+
name: "valid-config",
|
|
716
|
+
target: "{home}/.claude/settings.json",
|
|
717
|
+
patch: { key: "value" },
|
|
718
|
+
agents: ["claude-code"],
|
|
719
|
+
},
|
|
720
|
+
{ name: "missing-required-fields" },
|
|
721
|
+
]));
|
|
722
|
+
const { code } = await run(["init", dir]);
|
|
723
|
+
assert.equal(code, 0);
|
|
724
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
725
|
+
assert.equal(manifest.configs.length, 1);
|
|
726
|
+
assert.equal(manifest.configs[0].name, "valid-config");
|
|
727
|
+
}
|
|
728
|
+
finally {
|
|
729
|
+
await rm(dir, { recursive: true });
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
it("configs/ dir with valid configs-manifest.json suppresses the hint", async () => {
|
|
733
|
+
const dir = await makeTmpDir();
|
|
734
|
+
try {
|
|
735
|
+
await mkdir(path.join(dir, "configs"), { recursive: true });
|
|
736
|
+
await writeFile(path.join(dir, "configs-manifest.json"), JSON.stringify([
|
|
737
|
+
{
|
|
738
|
+
name: "my-patch",
|
|
739
|
+
target: "{home}/.claude/settings.json",
|
|
740
|
+
patch: { feature: true },
|
|
741
|
+
agents: ["claude-code"],
|
|
742
|
+
},
|
|
743
|
+
]));
|
|
744
|
+
const { code, stdout } = await run(["init", dir]);
|
|
745
|
+
assert.equal(code, 0);
|
|
746
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
747
|
+
assert.equal(manifest.configs.length, 1);
|
|
748
|
+
assert.ok(!stdout.includes("create configs-manifest.json"), "hint should not appear when configs were loaded from configs-manifest.json");
|
|
749
|
+
}
|
|
750
|
+
finally {
|
|
751
|
+
await rm(dir, { recursive: true });
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
it("init skips .agents/rules/ file with mcp-servers frontmatter from agentDefinitions", async () => {
|
|
755
|
+
const dir = await makeTmpDir();
|
|
756
|
+
try {
|
|
757
|
+
await mkdir(path.join(dir, ".agents", "rules"), { recursive: true });
|
|
758
|
+
await writeFile(path.join(dir, ".agents", "rules", "my-server.md"), "---\nmcp-servers:\n my-server:\n command: npx\n---\n# MCP Server\n");
|
|
759
|
+
const { code } = await run(["init", dir]);
|
|
760
|
+
assert.equal(code, 0);
|
|
761
|
+
const manifest = JSON.parse(await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8")));
|
|
762
|
+
const found = manifest.agentDefinitions.find((d) => d.name === "my-server");
|
|
763
|
+
assert.equal(found, undefined, "file with mcp-servers frontmatter should not appear in agentDefinitions");
|
|
764
|
+
}
|
|
765
|
+
finally {
|
|
766
|
+
await rm(dir, { recursive: true });
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
it("init includes .agents/rules/ file without mcp-servers frontmatter as agentDefinitions", async () => {
|
|
770
|
+
const dir = await makeTmpDir();
|
|
771
|
+
try {
|
|
772
|
+
await mkdir(path.join(dir, ".agents", "rules"), { recursive: true });
|
|
773
|
+
await writeFile(path.join(dir, ".agents", "rules", "my-agent.md"), "---\nname: my-agent\ndescription: A persona\n---\n# Agent\n");
|
|
774
|
+
const { code } = await run(["init", dir]);
|
|
775
|
+
assert.equal(code, 0);
|
|
776
|
+
const manifest = JSON.parse(await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8")));
|
|
777
|
+
const found = manifest.agentDefinitions.find((d) => d.name === "my-agent");
|
|
778
|
+
assert.ok(found, "agent-persona file should appear in agentDefinitions");
|
|
779
|
+
assert.ok(normalizeSlashes(found?.path ?? "").includes(".agents/rules/my-agent.md"), "path should point into .agents/rules/");
|
|
780
|
+
}
|
|
781
|
+
finally {
|
|
782
|
+
await rm(dir, { recursive: true });
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
it("init warns when skipping .agents/rules/ file with mcp-servers frontmatter", async () => {
|
|
786
|
+
const dir = await makeTmpDir();
|
|
787
|
+
try {
|
|
788
|
+
await mkdir(path.join(dir, ".agents", "rules"), { recursive: true });
|
|
789
|
+
await writeFile(path.join(dir, ".agents", "rules", "srv.md"), "---\nmcp-servers:\n srv:\n command: npx\n---\n");
|
|
790
|
+
const { code, stdout } = await run(["init", dir]);
|
|
791
|
+
assert.equal(code, 0);
|
|
792
|
+
assert.ok(stdout.includes("mcp-servers") || stdout.includes("srv.md"), "stdout should mention the skipped MCP-frontmatter file");
|
|
793
|
+
}
|
|
794
|
+
finally {
|
|
795
|
+
await rm(dir, { recursive: true });
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
it("init discovers legacy github-copilot agent definitions in .github/agents/", async () => {
|
|
799
|
+
const dir = await makeTmpDir();
|
|
800
|
+
try {
|
|
801
|
+
await mkdir(path.join(dir, ".github", "agents"), { recursive: true });
|
|
802
|
+
await writeFile(path.join(dir, ".github", "agents", "legacy.agent.md"), "---\nname: legacy\ndescription: legacy agent\n---\n# Legacy Agent\n");
|
|
803
|
+
const { code } = await run(["init", dir]);
|
|
804
|
+
assert.equal(code, 0);
|
|
805
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
806
|
+
const found = manifest.agentDefinitions.find((d) => d.name === "legacy");
|
|
807
|
+
assert.ok(found, "legacy agent-persona file should appear in agentDefinitions");
|
|
808
|
+
assert.ok(normalizeSlashes(found?.path ?? "").includes(".github/agents/legacy.agent.md"), "path should point into .github/agents/");
|
|
809
|
+
assert.deepEqual(found?.agents, ["github-copilot"]);
|
|
810
|
+
}
|
|
811
|
+
finally {
|
|
812
|
+
await rm(dir, { recursive: true });
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
it("init discovers new github-copilot agent definitions in .github/copilot/agents/", async () => {
|
|
816
|
+
const dir = await makeTmpDir();
|
|
817
|
+
try {
|
|
818
|
+
await mkdir(path.join(dir, ".github", "copilot", "agents"), {
|
|
819
|
+
recursive: true,
|
|
820
|
+
});
|
|
821
|
+
await writeFile(path.join(dir, ".github", "copilot", "agents", "new.md"), "---\nname: new\ndescription: new agent\n---\n# New Agent\n");
|
|
822
|
+
const { code } = await run(["init", dir]);
|
|
823
|
+
assert.equal(code, 0);
|
|
824
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
825
|
+
const found = manifest.agentDefinitions.find((d) => d.name === "new");
|
|
826
|
+
assert.ok(found, "new agent-persona file should appear in agentDefinitions");
|
|
827
|
+
assert.ok(normalizeSlashes(found?.path ?? "").includes(".github/copilot/agents/new.md"), "path should point into .github/copilot/agents/");
|
|
828
|
+
assert.deepEqual(found?.agents, ["github-copilot"]);
|
|
829
|
+
}
|
|
830
|
+
finally {
|
|
831
|
+
await rm(dir, { recursive: true });
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
});
|