@kuznai/inception-engine 0.20.0 → 0.22.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 +44 -4
- package/dist/src/config/agents.js +70 -0
- package/dist/src/core/adapters/hooks.d.ts +8 -0
- package/dist/src/core/adapters/hooks.js +86 -0
- package/dist/src/core/adapters/index.d.ts +4 -3
- package/dist/src/core/adapters/index.js +19 -22
- package/dist/src/core/adapters/mcp.js +15 -1
- package/dist/src/core/capabilities.d.ts +1 -1
- package/dist/src/core/capabilities.js +17 -2
- package/dist/src/core/deploy.js +18 -19
- package/dist/src/core/detect.js +2 -7
- package/dist/src/core/init.js +20 -11
- package/dist/src/core/preflight.js +35 -19
- package/dist/src/core/revert.js +1 -1
- package/dist/src/core/validation.d.ts +1 -0
- package/dist/src/core/validation.js +15 -0
- package/dist/src/errors.d.ts +3 -1
- package/dist/src/errors.js +2 -2
- package/dist/src/schemas/manifest.d.ts +27 -0
- package/dist/src/schemas/manifest.js +11 -1
- package/dist/src/types.d.ts +8 -2
- package/dist/test/os/windows/agentRules-integration.test.d.ts +1 -0
- package/dist/test/os/windows/agentRules-integration.test.js +245 -0
- package/dist/test/unit/adapters.test.js +86 -2
- package/dist/test/unit/deploy.test.js +5 -3
- package/dist/test/unit/init-fixture.test.d.ts +1 -0
- package/dist/test/unit/init-fixture.test.js +153 -0
- package/dist/test/unit/preflight.test.js +18 -0
- package/dist/test/unit/revert.test.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import { executeDeploy } from "../../../src/core/deploy.js";
|
|
6
|
+
import { lookupDeployment, registerDeployment, } from "../../../src/core/ownership.js";
|
|
7
|
+
import { executeRevert } from "../../../src/core/revert.js";
|
|
8
|
+
import { exists, makeTmpDir } from "../../helpers/fs.js";
|
|
9
|
+
describe("agentRules repo/workspace file-write (Windows)", {
|
|
10
|
+
skip: process.platform !== "win32",
|
|
11
|
+
}, () => {
|
|
12
|
+
it("deploys agentRules with scope repo and registers it", async () => {
|
|
13
|
+
const home = await makeTmpDir("ie-rules-home");
|
|
14
|
+
const repo = await makeTmpDir("ie-rules-repo");
|
|
15
|
+
const sourceDir = await makeTmpDir("ie-rules-source");
|
|
16
|
+
try {
|
|
17
|
+
const source = path.join(sourceDir, "CLAUDE.md");
|
|
18
|
+
await writeFile(source, "# My Rules\n");
|
|
19
|
+
const target = path.join(repo, "CLAUDE.md");
|
|
20
|
+
const action = {
|
|
21
|
+
kind: "file-write",
|
|
22
|
+
skill: "my-rules",
|
|
23
|
+
agent: "claude-code",
|
|
24
|
+
source,
|
|
25
|
+
target,
|
|
26
|
+
};
|
|
27
|
+
const { succeeded, failed } = await executeDeploy([action], false, false, home);
|
|
28
|
+
assert.equal(succeeded, 1);
|
|
29
|
+
assert.equal(failed.length, 0);
|
|
30
|
+
assert.ok(await exists(target));
|
|
31
|
+
const entry = await lookupDeployment(home, target);
|
|
32
|
+
assert.ok(entry !== null);
|
|
33
|
+
assert.equal(entry?.kind, "file-write");
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await rm(home, { recursive: true, force: true });
|
|
37
|
+
await rm(repo, { recursive: true, force: true });
|
|
38
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
it("reverts agentRules with scope repo and unregisters it", async () => {
|
|
42
|
+
const home = await makeTmpDir("ie-rules-home");
|
|
43
|
+
const repo = await makeTmpDir("ie-rules-repo");
|
|
44
|
+
try {
|
|
45
|
+
const source = path.join(repo, "source-CLAUDE.md");
|
|
46
|
+
const target = path.join(repo, "CLAUDE.md");
|
|
47
|
+
await writeFile(target, "# My Rules\n");
|
|
48
|
+
await registerDeployment(home, target, {
|
|
49
|
+
kind: "file-write",
|
|
50
|
+
source,
|
|
51
|
+
skill: "my-rules",
|
|
52
|
+
agent: "claude-code",
|
|
53
|
+
});
|
|
54
|
+
const action = {
|
|
55
|
+
kind: "file-write",
|
|
56
|
+
skill: "my-rules",
|
|
57
|
+
agent: "claude-code",
|
|
58
|
+
target,
|
|
59
|
+
};
|
|
60
|
+
const { succeeded, failed } = await executeRevert([action], false, false, home);
|
|
61
|
+
assert.equal(succeeded, 1);
|
|
62
|
+
assert.equal(failed.length, 0);
|
|
63
|
+
assert.ok(!(await exists(target)));
|
|
64
|
+
assert.equal(await lookupDeployment(home, target), null);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await rm(home, { recursive: true, force: true });
|
|
68
|
+
await rm(repo, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
it("deploys agentRules with scope workspace and registers it", async () => {
|
|
72
|
+
const home = await makeTmpDir("ie-rules-home");
|
|
73
|
+
const workspace = await makeTmpDir("ie-rules-workspace");
|
|
74
|
+
const sourceDir = await makeTmpDir("ie-rules-source");
|
|
75
|
+
try {
|
|
76
|
+
const source = path.join(sourceDir, "CLAUDE.md");
|
|
77
|
+
await writeFile(source, "# Workspace Rules\n");
|
|
78
|
+
const target = path.join(workspace, "CLAUDE.md");
|
|
79
|
+
const action = {
|
|
80
|
+
kind: "file-write",
|
|
81
|
+
skill: "my-rules",
|
|
82
|
+
agent: "claude-code",
|
|
83
|
+
source,
|
|
84
|
+
target,
|
|
85
|
+
};
|
|
86
|
+
const { succeeded, failed } = await executeDeploy([action], false, false, home);
|
|
87
|
+
assert.equal(succeeded, 1);
|
|
88
|
+
assert.equal(failed.length, 0);
|
|
89
|
+
assert.ok(await exists(target));
|
|
90
|
+
const entry = await lookupDeployment(home, target);
|
|
91
|
+
assert.ok(entry !== null);
|
|
92
|
+
assert.equal(entry?.kind, "file-write");
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await rm(home, { recursive: true, force: true });
|
|
96
|
+
await rm(workspace, { recursive: true, force: true });
|
|
97
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
it("reverts agentRules with scope workspace and unregisters it", async () => {
|
|
101
|
+
const home = await makeTmpDir("ie-rules-home");
|
|
102
|
+
const workspace = await makeTmpDir("ie-rules-workspace");
|
|
103
|
+
try {
|
|
104
|
+
const source = path.join(workspace, "source-CLAUDE.md");
|
|
105
|
+
const target = path.join(workspace, "CLAUDE.md");
|
|
106
|
+
await writeFile(target, "# Workspace Rules\n");
|
|
107
|
+
await registerDeployment(home, target, {
|
|
108
|
+
kind: "file-write",
|
|
109
|
+
source,
|
|
110
|
+
skill: "my-rules",
|
|
111
|
+
agent: "claude-code",
|
|
112
|
+
});
|
|
113
|
+
const action = {
|
|
114
|
+
kind: "file-write",
|
|
115
|
+
skill: "my-rules",
|
|
116
|
+
agent: "claude-code",
|
|
117
|
+
target,
|
|
118
|
+
};
|
|
119
|
+
const { succeeded, failed } = await executeRevert([action], false, false, home);
|
|
120
|
+
assert.equal(succeeded, 1);
|
|
121
|
+
assert.equal(failed.length, 0);
|
|
122
|
+
assert.ok(!(await exists(target)));
|
|
123
|
+
assert.equal(await lookupDeployment(home, target), null);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await rm(home, { recursive: true, force: true });
|
|
127
|
+
await rm(workspace, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe("Antigravity frontmatter-emit (Windows)", {
|
|
132
|
+
skip: process.platform !== "win32",
|
|
133
|
+
}, () => {
|
|
134
|
+
it("deploys frontmatter-emit for Antigravity and registers it", async () => {
|
|
135
|
+
const home = await makeTmpDir("ie-fm-home");
|
|
136
|
+
const repo = await makeTmpDir("ie-fm-repo");
|
|
137
|
+
try {
|
|
138
|
+
const target = path.join(repo, ".agents", "rules", "my-mcp.md");
|
|
139
|
+
const frontmatter = {
|
|
140
|
+
"mcp-servers": {
|
|
141
|
+
"my-mcp": { command: "npx", args: ["-y", "my-mcp-server"] },
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const action = {
|
|
145
|
+
kind: "frontmatter-emit",
|
|
146
|
+
skill: "my-mcp",
|
|
147
|
+
agent: "antigravity",
|
|
148
|
+
target,
|
|
149
|
+
frontmatter,
|
|
150
|
+
};
|
|
151
|
+
const { succeeded, failed } = await executeDeploy([action], false, false, home);
|
|
152
|
+
assert.equal(succeeded, 1);
|
|
153
|
+
assert.equal(failed.length, 0);
|
|
154
|
+
assert.ok(await exists(target));
|
|
155
|
+
const content = await readFile(target, "utf-8");
|
|
156
|
+
assert.ok(content.includes("mcp-servers:"));
|
|
157
|
+
const entry = await lookupDeployment(home, target);
|
|
158
|
+
assert.ok(entry !== null);
|
|
159
|
+
assert.equal(entry?.kind, "frontmatter-emit");
|
|
160
|
+
assert.equal(entry.created, true);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
await rm(home, { recursive: true, force: true });
|
|
164
|
+
await rm(repo, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
it("reverts frontmatter-emit for Antigravity and removes file when no body", async () => {
|
|
168
|
+
const home = await makeTmpDir("ie-fm-home");
|
|
169
|
+
const repo = await makeTmpDir("ie-fm-repo");
|
|
170
|
+
try {
|
|
171
|
+
const target = path.join(repo, ".agents", "rules", "my-mcp.md");
|
|
172
|
+
const frontmatter = {
|
|
173
|
+
"mcp-servers": {
|
|
174
|
+
"my-mcp": { command: "npx", args: ["-y", "my-mcp-server"] },
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
// Deploy first so the file and registry entry are created.
|
|
178
|
+
const deployAction = {
|
|
179
|
+
kind: "frontmatter-emit",
|
|
180
|
+
skill: "my-mcp",
|
|
181
|
+
agent: "antigravity",
|
|
182
|
+
target,
|
|
183
|
+
frontmatter,
|
|
184
|
+
};
|
|
185
|
+
await executeDeploy([deployAction], false, false, home);
|
|
186
|
+
const action = {
|
|
187
|
+
kind: "frontmatter-emit",
|
|
188
|
+
skill: "my-mcp",
|
|
189
|
+
agent: "antigravity",
|
|
190
|
+
target,
|
|
191
|
+
};
|
|
192
|
+
const { succeeded, failed } = await executeRevert([action], false, false, home);
|
|
193
|
+
assert.equal(succeeded, 1);
|
|
194
|
+
assert.equal(failed.length, 0);
|
|
195
|
+
assert.ok(!(await exists(target)));
|
|
196
|
+
assert.equal(await lookupDeployment(home, target), null);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await rm(home, { recursive: true, force: true });
|
|
200
|
+
await rm(repo, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
it("reverts frontmatter-emit for Antigravity preserving body when file had prior content", async () => {
|
|
204
|
+
const home = await makeTmpDir("ie-fm-home");
|
|
205
|
+
const repo = await makeTmpDir("ie-fm-repo");
|
|
206
|
+
try {
|
|
207
|
+
const target = path.join(repo, ".agents", "rules", "my-mcp.md");
|
|
208
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
209
|
+
await writeFile(target, "# My Rules\n");
|
|
210
|
+
const undoPatch = { "mcp-servers": null };
|
|
211
|
+
await registerDeployment(home, target, {
|
|
212
|
+
kind: "frontmatter-emit",
|
|
213
|
+
patch: {
|
|
214
|
+
"mcp-servers": {
|
|
215
|
+
"my-mcp": { command: "npx", args: ["-y", "my-mcp-server"] },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
undoPatch,
|
|
219
|
+
created: false,
|
|
220
|
+
hadFrontmatter: false,
|
|
221
|
+
skill: "my-mcp",
|
|
222
|
+
agent: "antigravity",
|
|
223
|
+
});
|
|
224
|
+
const action = {
|
|
225
|
+
kind: "frontmatter-emit",
|
|
226
|
+
skill: "my-mcp",
|
|
227
|
+
agent: "antigravity",
|
|
228
|
+
target,
|
|
229
|
+
};
|
|
230
|
+
const { succeeded, failed } = await executeRevert([action], false, false, home);
|
|
231
|
+
assert.equal(succeeded, 1);
|
|
232
|
+
assert.equal(failed.length, 0);
|
|
233
|
+
// File should still exist because it had body content.
|
|
234
|
+
assert.ok(await exists(target));
|
|
235
|
+
const content = await readFile(target, "utf-8");
|
|
236
|
+
assert.ok(!content.includes("mcp-servers:"));
|
|
237
|
+
assert.ok(content.includes("My Rules"));
|
|
238
|
+
assert.equal(await lookupDeployment(home, target), null);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
await rm(home, { recursive: true, force: true });
|
|
242
|
+
await rm(repo, { recursive: true, force: true });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -137,13 +137,13 @@ describe("compileMcpServerActions", () => {
|
|
|
137
137
|
servers: { "my-mcp": { command: "s" } },
|
|
138
138
|
});
|
|
139
139
|
});
|
|
140
|
-
it("returns a frontmatter-emit action for antigravity MCP", () => {
|
|
140
|
+
it("returns a frontmatter-emit action for antigravity MCP with scope: repo", () => {
|
|
141
141
|
const home = "/home/test";
|
|
142
142
|
const { actions, warnings } = compileMcpServerActions({
|
|
143
143
|
name: "my-mcp",
|
|
144
144
|
agents: ["antigravity"],
|
|
145
145
|
config: { command: "s" },
|
|
146
|
-
scope: "
|
|
146
|
+
scope: "repo",
|
|
147
147
|
}, ["antigravity"], home, "/repo/test");
|
|
148
148
|
assert.equal(actions.length, 1);
|
|
149
149
|
assert.equal(warnings.length, 0);
|
|
@@ -151,6 +151,20 @@ describe("compileMcpServerActions", () => {
|
|
|
151
151
|
assert.equal(actions[0]?.agent, "antigravity");
|
|
152
152
|
assertPathEndsWith(actions[0]?.target ?? "", ".agents/rules/my-mcp.md", `expected target to end with .agents/rules/my-mcp.md, got ${actions[0]?.target}`);
|
|
153
153
|
});
|
|
154
|
+
it("returns a config-patch action for antigravity MCP with scope: global", () => {
|
|
155
|
+
const home = "/home/test";
|
|
156
|
+
const { actions, warnings } = compileMcpServerActions({
|
|
157
|
+
name: "my-mcp",
|
|
158
|
+
agents: ["antigravity"],
|
|
159
|
+
config: { command: "s" },
|
|
160
|
+
scope: "global",
|
|
161
|
+
}, ["antigravity"], home);
|
|
162
|
+
assert.equal(actions.length, 1);
|
|
163
|
+
assert.equal(warnings.length, 0);
|
|
164
|
+
assert.equal(actions[0]?.kind, "config-patch");
|
|
165
|
+
assert.equal(actions[0]?.agent, "antigravity");
|
|
166
|
+
assertPathEndsWith(actions[0]?.target ?? "", ".gemini/antigravity/mcp_config.json", `expected target to end with .gemini/antigravity/mcp_config.json, got ${actions[0]?.target}`);
|
|
167
|
+
});
|
|
154
168
|
it("throws when a supported MCP target is missing both command and url", () => {
|
|
155
169
|
assert.throws(() => compileMcpServerActions({
|
|
156
170
|
name: "my-mcp",
|
|
@@ -910,6 +924,76 @@ describe("compileAgentDefinitionActions", () => {
|
|
|
910
924
|
await rm(dir, { recursive: true });
|
|
911
925
|
}
|
|
912
926
|
});
|
|
927
|
+
it("accepts github-copilot tools as empty array", async () => {
|
|
928
|
+
const compile = await getAdapter();
|
|
929
|
+
const dir = await makeTmpDir();
|
|
930
|
+
try {
|
|
931
|
+
await writeFile(path.join(dir, "agent.md"), "---\nname: test\ndescription: test\ntools: []\n---\n# Agent");
|
|
932
|
+
const realRoot = await realpath(dir);
|
|
933
|
+
const result = await compile({
|
|
934
|
+
name: "agent",
|
|
935
|
+
agents: ["github-copilot"],
|
|
936
|
+
path: "agent.md",
|
|
937
|
+
scope: "repo",
|
|
938
|
+
}, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test");
|
|
939
|
+
assert.ok(result.actions.length > 0, "expected at least one action");
|
|
940
|
+
}
|
|
941
|
+
finally {
|
|
942
|
+
await rm(dir, { recursive: true });
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
it("accepts github-copilot tools as array of strings", async () => {
|
|
946
|
+
const compile = await getAdapter();
|
|
947
|
+
const dir = await makeTmpDir();
|
|
948
|
+
try {
|
|
949
|
+
await writeFile(path.join(dir, "agent.md"), "---\nname: test\ndescription: test\ntools:\n - github\n - codebase\n---\n# Agent");
|
|
950
|
+
const realRoot = await realpath(dir);
|
|
951
|
+
const result = await compile({
|
|
952
|
+
name: "agent",
|
|
953
|
+
agents: ["github-copilot"],
|
|
954
|
+
path: "agent.md",
|
|
955
|
+
scope: "repo",
|
|
956
|
+
}, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test");
|
|
957
|
+
assert.ok(result.actions.length > 0, "expected at least one action");
|
|
958
|
+
}
|
|
959
|
+
finally {
|
|
960
|
+
await rm(dir, { recursive: true });
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
it("throws for github-copilot when tools is not an array", async () => {
|
|
964
|
+
const compile = await getAdapter();
|
|
965
|
+
const dir = await makeTmpDir();
|
|
966
|
+
try {
|
|
967
|
+
await writeFile(path.join(dir, "bad-tools.md"), "---\nname: test\ndescription: test\ntools: not-an-array\n---\n# Bad");
|
|
968
|
+
const realRoot = await realpath(dir);
|
|
969
|
+
await assert.rejects(compile({
|
|
970
|
+
name: "bad",
|
|
971
|
+
agents: ["github-copilot"],
|
|
972
|
+
path: "bad-tools.md",
|
|
973
|
+
scope: "repo",
|
|
974
|
+
}, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test"), /"tools" field that must be an array/);
|
|
975
|
+
}
|
|
976
|
+
finally {
|
|
977
|
+
await rm(dir, { recursive: true });
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
it("throws for github-copilot when tools contains a non-string entry", async () => {
|
|
981
|
+
const compile = await getAdapter();
|
|
982
|
+
const dir = await makeTmpDir();
|
|
983
|
+
try {
|
|
984
|
+
await writeFile(path.join(dir, "bad-tools.md"), "---\nname: test\ndescription: test\ntools:\n - github\n - 123\n---\n# Bad");
|
|
985
|
+
const realRoot = await realpath(dir);
|
|
986
|
+
await assert.rejects(compile({
|
|
987
|
+
name: "bad",
|
|
988
|
+
agents: ["github-copilot"],
|
|
989
|
+
path: "bad-tools.md",
|
|
990
|
+
scope: "repo",
|
|
991
|
+
}, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test"), /"tools" entry that must be a string/);
|
|
992
|
+
}
|
|
993
|
+
finally {
|
|
994
|
+
await rm(dir, { recursive: true });
|
|
995
|
+
}
|
|
996
|
+
});
|
|
913
997
|
it("throws for antigravity when mcp-servers is malformed", async () => {
|
|
914
998
|
const compile = await getAdapter();
|
|
915
999
|
const dir = await makeTmpDir();
|
|
@@ -30,6 +30,7 @@ const testManifest = {
|
|
|
30
30
|
agentRules: [],
|
|
31
31
|
permissions: [],
|
|
32
32
|
agentDefinitions: [],
|
|
33
|
+
hooks: [],
|
|
33
34
|
};
|
|
34
35
|
describe("planDeploy", () => {
|
|
35
36
|
it("creates actions for detected agents only", async () => {
|
|
@@ -647,8 +648,9 @@ describe("planDeploy", () => {
|
|
|
647
648
|
assert.ok(geminiAction, "expected a gemini-cli action");
|
|
648
649
|
assert.match(geminiAction.target, /[\\/]\.gemini[\\/]settings\.json$/);
|
|
649
650
|
assert.ok(antigravityAction, "expected an antigravity action");
|
|
650
|
-
// Antigravity's target for mcpServer is .
|
|
651
|
-
assert.
|
|
651
|
+
// Antigravity's target for global mcpServer is .gemini/antigravity/mcp_config.json
|
|
652
|
+
assert.equal(antigravityAction.kind, "config-patch");
|
|
653
|
+
assert.match(antigravityAction.target, /[\\/]\.gemini[\\/]antigravity[\\/]mcp_config\.json$/);
|
|
652
654
|
}
|
|
653
655
|
finally {
|
|
654
656
|
await rm(sourceDir, { recursive: true });
|
|
@@ -802,7 +804,7 @@ describe("planDeploy", () => {
|
|
|
802
804
|
name: "my-tool",
|
|
803
805
|
agents: ["antigravity"],
|
|
804
806
|
config: { command: "npx", args: ["-y", "my-tool"] },
|
|
805
|
-
scope: "
|
|
807
|
+
scope: "repo",
|
|
806
808
|
},
|
|
807
809
|
],
|
|
808
810
|
agentRules: [],
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { cp, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import { makeTmpDir } from "../helpers/fs.js";
|
|
7
|
+
import { assertPathEndsWith, normalizeSlashes } from "../helpers/path.js";
|
|
8
|
+
const PROJECT_ROOT = path.resolve(import.meta.dirname, "..", "..");
|
|
9
|
+
const FIXTURE_DIR = path.join(PROJECT_ROOT, "test", "fixtures", "readme-sample");
|
|
10
|
+
function run(args, env) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const child = spawn(process.execPath, [path.join(PROJECT_ROOT, "src", "index.ts"), ...args], {
|
|
13
|
+
cwd: PROJECT_ROOT,
|
|
14
|
+
env: { ...process.env, ...env },
|
|
15
|
+
});
|
|
16
|
+
let stdout = "";
|
|
17
|
+
let stderr = "";
|
|
18
|
+
child.stdout.on("data", (d) => {
|
|
19
|
+
stdout += d.toString();
|
|
20
|
+
});
|
|
21
|
+
child.stderr.on("data", (d) => {
|
|
22
|
+
stderr += d.toString();
|
|
23
|
+
});
|
|
24
|
+
child.on("close", (code) => {
|
|
25
|
+
resolve({ stdout, stderr, code: code ?? 1 });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extract the JSON manifest object from `init --plan` stdout.
|
|
31
|
+
* The output format is:
|
|
32
|
+
* [plan] Would write /path/inception.json with N skill(s), ...:
|
|
33
|
+
* <blank line>
|
|
34
|
+
* { ...json... }
|
|
35
|
+
* The [plan] prefix may include ANSI escape codes; the first `{` is
|
|
36
|
+
* reliably the start of the JSON object.
|
|
37
|
+
*/
|
|
38
|
+
function extractPlanJson(stdout) {
|
|
39
|
+
const jsonStart = stdout.indexOf("{");
|
|
40
|
+
assert.ok(jsonStart !== -1, `Could not find JSON object in --plan stdout:\n${stdout}`);
|
|
41
|
+
return JSON.parse(stdout.slice(jsonStart));
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Block 1: init --plan against the real limbo/ tree (read-only)
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
describe("init --plan against real limbo/ tree", () => {
|
|
47
|
+
it("emits the 4 limbo skills with all 5 agents and empty other sections", async () => {
|
|
48
|
+
const limboDir = path.join(PROJECT_ROOT, "limbo");
|
|
49
|
+
const { stdout, code } = await run(["init", limboDir, "--plan"]);
|
|
50
|
+
assert.equal(code, 0, `init --plan exited non-zero.\nstdout: ${stdout}`);
|
|
51
|
+
assert.ok(stdout.includes("[plan]"), `missing [plan] prefix:\n${stdout}`);
|
|
52
|
+
const manifest = extractPlanJson(stdout);
|
|
53
|
+
// Exactly 4 skills
|
|
54
|
+
assert.equal(manifest.skills.length, 4, `expected 4 skills, got ${manifest.skills.length}: ${JSON.stringify(manifest.skills.map((s) => s.name))}`);
|
|
55
|
+
// Names sorted alphabetically (sort before comparing - filesystem order not guaranteed)
|
|
56
|
+
const names = manifest.skills.map((s) => s.name).sort();
|
|
57
|
+
assert.deepEqual(names, [
|
|
58
|
+
"inception",
|
|
59
|
+
"interstellar",
|
|
60
|
+
"tenet",
|
|
61
|
+
"the-prestige",
|
|
62
|
+
]);
|
|
63
|
+
// Each skill has exactly the 5 portability agents (github-copilot excluded)
|
|
64
|
+
const expectedAgents = [
|
|
65
|
+
"antigravity",
|
|
66
|
+
"claude-code",
|
|
67
|
+
"codex",
|
|
68
|
+
"gemini-cli",
|
|
69
|
+
"opencode",
|
|
70
|
+
];
|
|
71
|
+
for (const skill of manifest.skills) {
|
|
72
|
+
assert.deepEqual(skill.agents.slice().sort(), expectedAgents, `skill "${skill.name}" agents mismatch: ${JSON.stringify(skill.agents)}`);
|
|
73
|
+
assertPathEndsWith(skill.path, `skills/${skill.name}`, `skill "${skill.name}" path should end with skills/${skill.name}`);
|
|
74
|
+
}
|
|
75
|
+
// All other sections are empty arrays
|
|
76
|
+
assert.deepEqual(manifest.mcpServers, [], "mcpServers should be []");
|
|
77
|
+
assert.deepEqual(manifest.agentRules, [], "agentRules should be []");
|
|
78
|
+
assert.deepEqual(manifest.agentDefinitions, [], "agentDefinitions should be []");
|
|
79
|
+
assert.deepEqual(manifest.files, [], "files should be []");
|
|
80
|
+
assert.deepEqual(manifest.configs, [], "configs should be []");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Block 2: init against README-shaped fixture (copied to temp dir)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
describe("init against readme-sample fixture", () => {
|
|
87
|
+
it("generates a manifest covering all README-documented init discovery paths", async () => {
|
|
88
|
+
const tmpDir = await makeTmpDir("ie-fixture-readme");
|
|
89
|
+
try {
|
|
90
|
+
// Copy the static fixture tree (including .claude/ hidden dir) into tmpDir
|
|
91
|
+
await cp(FIXTURE_DIR, tmpDir, { recursive: true });
|
|
92
|
+
const { stdout, code } = await run(["init", tmpDir]);
|
|
93
|
+
assert.equal(code, 0, `init exited non-zero.\nstdout: ${stdout}`);
|
|
94
|
+
const manifest = JSON.parse(await readFile(path.join(tmpDir, "inception.json"), "utf-8"));
|
|
95
|
+
// --- skills: 2 entries, github-copilot excluded per portability rules ---
|
|
96
|
+
assert.equal(manifest.skills.length, 2, `expected 2 skills, got ${manifest.skills.length}: ${JSON.stringify(manifest.skills.map((s) => s.name))}`);
|
|
97
|
+
const skillNames = manifest.skills.map((s) => s.name).sort();
|
|
98
|
+
assert.deepEqual(skillNames, ["my-skill", "other-skill"]);
|
|
99
|
+
const expectedSkillAgents = [
|
|
100
|
+
"antigravity",
|
|
101
|
+
"claude-code",
|
|
102
|
+
"codex",
|
|
103
|
+
"gemini-cli",
|
|
104
|
+
"opencode",
|
|
105
|
+
];
|
|
106
|
+
for (const skill of manifest.skills) {
|
|
107
|
+
assert.deepEqual(skill.agents.slice().sort(), expectedSkillAgents, `skill "${skill.name}" should have all 5 agents (not github-copilot): ${JSON.stringify(skill.agents)}`);
|
|
108
|
+
assertPathEndsWith(skill.path, `skills/${skill.name}`, `skill "${skill.name}" path should end with skills/${skill.name}`);
|
|
109
|
+
}
|
|
110
|
+
// --- agentRules: 3 entries for CLAUDE.md, AGENTS.md, GEMINI.md ---
|
|
111
|
+
assert.equal(manifest.agentRules.length, 3, `expected 3 agentRules, got ${manifest.agentRules.length}: ${JSON.stringify(manifest.agentRules.map((r) => r.name))}`);
|
|
112
|
+
const claudeRule = manifest.agentRules.find((r) => normalizeSlashes(r.path).endsWith("CLAUDE.md"));
|
|
113
|
+
assert.ok(claudeRule, "should have an agentRules entry for CLAUDE.md");
|
|
114
|
+
assert.deepEqual(claudeRule?.agents.slice().sort(), ["claude-code"], `CLAUDE.md agents: ${JSON.stringify(claudeRule?.agents)}`);
|
|
115
|
+
assert.equal(claudeRule?.scope, "global");
|
|
116
|
+
const agentsRule = manifest.agentRules.find((r) => normalizeSlashes(r.path).endsWith("AGENTS.md"));
|
|
117
|
+
assert.ok(agentsRule, "should have an agentRules entry for AGENTS.md");
|
|
118
|
+
assert.deepEqual(agentsRule?.agents.slice().sort(), ["codex", "opencode"], `AGENTS.md agents: ${JSON.stringify(agentsRule?.agents)}`);
|
|
119
|
+
assert.equal(agentsRule?.scope, "global");
|
|
120
|
+
const geminiRule = manifest.agentRules.find((r) => normalizeSlashes(r.path).endsWith("GEMINI.md"));
|
|
121
|
+
assert.ok(geminiRule, "should have an agentRules entry for GEMINI.md");
|
|
122
|
+
assert.deepEqual(geminiRule?.agents.slice().sort(), ["gemini-cli"],
|
|
123
|
+
// antigravity is shared-via gemini-cli and excluded from init defaults
|
|
124
|
+
`GEMINI.md agents should be [gemini-cli] only: ${JSON.stringify(geminiRule?.agents)}`);
|
|
125
|
+
assert.equal(geminiRule?.scope, "global");
|
|
126
|
+
// --- mcpServers: 1 entry round-tripped from mcp-servers.json sidecar ---
|
|
127
|
+
assert.equal(manifest.mcpServers.length, 1, `expected 1 mcpServer, got ${manifest.mcpServers.length}`);
|
|
128
|
+
const mcp = manifest.mcpServers[0];
|
|
129
|
+
assert.equal(mcp.name, "my-mcp-server");
|
|
130
|
+
assert.deepEqual(mcp.agents.slice().sort(), [
|
|
131
|
+
"claude-code",
|
|
132
|
+
"codex",
|
|
133
|
+
"gemini-cli",
|
|
134
|
+
"opencode",
|
|
135
|
+
]);
|
|
136
|
+
assert.equal(mcp.config.command, "npx");
|
|
137
|
+
assert.deepEqual(mcp.config.args, ["-y", "@example/mcp-server"]);
|
|
138
|
+
// --- agentDefinitions: 1 entry from .claude/agents/ -> claude-code ---
|
|
139
|
+
assert.equal(manifest.agentDefinitions.length, 1, `expected 1 agentDefinition, got ${manifest.agentDefinitions.length}`);
|
|
140
|
+
const def = manifest.agentDefinitions[0];
|
|
141
|
+
assert.equal(def.name, "code-reviewer");
|
|
142
|
+
assert.deepEqual(def.agents, ["claude-code"]);
|
|
143
|
+
assert.equal(def.scope, "repo");
|
|
144
|
+
assertPathEndsWith(def.path, ".claude/agents/code-reviewer.md", "code-reviewer path should end with .claude/agents/code-reviewer.md");
|
|
145
|
+
// --- files and configs stay empty (no sidecar manifests for them) ---
|
|
146
|
+
assert.deepEqual(manifest.files, [], "files should be []");
|
|
147
|
+
assert.deepEqual(manifest.configs, [], "configs should be []");
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
await rm(tmpDir, { recursive: true });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -125,6 +125,24 @@ describe("runPreflight", () => {
|
|
|
125
125
|
assert.equal(implementationOnlyWarning, undefined, `expected no implementation-only warning, got: ${implementationOnlyWarning?.message}`);
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
|
+
describe("github-copilot devcontainer support", () => {
|
|
129
|
+
it("emits no capability warning for devcontainer MCP when Copilot is detected and devcontainer scope is targeted", async () => {
|
|
130
|
+
const manifestWithMcp = {
|
|
131
|
+
...emptyManifest,
|
|
132
|
+
mcpServers: [
|
|
133
|
+
{
|
|
134
|
+
name: "test-mcp",
|
|
135
|
+
scope: "devcontainer",
|
|
136
|
+
agents: ["github-copilot"],
|
|
137
|
+
config: { command: "node", args: ["server.js"] },
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
const warnings = await runPreflight(baseOptions, manifestWithMcp, "/home/test", ["github-copilot"]);
|
|
142
|
+
const capabilityWarning = warnings.find((w) => w.kind === "info" && /mcpServers/.test(w.message));
|
|
143
|
+
assert.equal(capabilityWarning, undefined, `expected no MCP capability warning for devcontainer scope, got: ${capabilityWarning?.message}`);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
128
146
|
describe("instruction precedence warnings", () => {
|
|
129
147
|
it("emits duplicate-content precedence warning when same source used in both global and repo scope for an agent", async () => {
|
|
130
148
|
const sourceDir = await makeTmpDir();
|
|
@@ -46,6 +46,7 @@ describe("planRevertAll", () => {
|
|
|
46
46
|
agentRules: [],
|
|
47
47
|
permissions: [],
|
|
48
48
|
agentDefinitions: [],
|
|
49
|
+
hooks: [],
|
|
49
50
|
};
|
|
50
51
|
const actions = planRevertAll(multiAgentManifest, "/home/test");
|
|
51
52
|
assert.equal(actions.length, 3);
|
|
@@ -75,6 +76,7 @@ describe("planRevertAll", () => {
|
|
|
75
76
|
agentRules: [],
|
|
76
77
|
permissions: [],
|
|
77
78
|
agentDefinitions: [],
|
|
79
|
+
hooks: [],
|
|
78
80
|
};
|
|
79
81
|
const actions = planRevertAll(manifest, "/home/test");
|
|
80
82
|
assert.equal(actions.length, 5);
|
|
@@ -712,6 +714,7 @@ describe("planRevert — mcpServers and agentRules", () => {
|
|
|
712
714
|
agentRules: [],
|
|
713
715
|
permissions: [],
|
|
714
716
|
agentDefinitions: [],
|
|
717
|
+
hooks: [],
|
|
715
718
|
};
|
|
716
719
|
const actions = planRevert(manifest, ["claude-code"], home);
|
|
717
720
|
assert.equal(actions.length, 1);
|
|
@@ -735,6 +738,7 @@ describe("planRevert — mcpServers and agentRules", () => {
|
|
|
735
738
|
agentRules: [],
|
|
736
739
|
permissions: [],
|
|
737
740
|
agentDefinitions: [],
|
|
741
|
+
hooks: [],
|
|
738
742
|
};
|
|
739
743
|
const actions = planRevert(manifest, ["codex"], home);
|
|
740
744
|
assert.equal(actions.length, 0);
|
|
@@ -755,6 +759,7 @@ describe("planRevert — mcpServers and agentRules", () => {
|
|
|
755
759
|
],
|
|
756
760
|
permissions: [],
|
|
757
761
|
agentDefinitions: [],
|
|
762
|
+
hooks: [],
|
|
758
763
|
};
|
|
759
764
|
const actions = planRevert(manifest, ["claude-code"], home);
|
|
760
765
|
assert.equal(actions.length, 1);
|
|
@@ -778,6 +783,7 @@ describe("planRevert — mcpServers and agentRules", () => {
|
|
|
778
783
|
],
|
|
779
784
|
permissions: [],
|
|
780
785
|
agentDefinitions: [],
|
|
786
|
+
hooks: [],
|
|
781
787
|
};
|
|
782
788
|
const actions = planRevert(manifest, ["codex"], home);
|
|
783
789
|
assert.equal(actions.length, 0);
|
|
@@ -801,6 +807,7 @@ describe("planRevertAll — mcpServers and agentRules", () => {
|
|
|
801
807
|
agentRules: [],
|
|
802
808
|
permissions: [],
|
|
803
809
|
agentDefinitions: [],
|
|
810
|
+
hooks: [],
|
|
804
811
|
};
|
|
805
812
|
const actions = planRevertAll(manifest, home);
|
|
806
813
|
assert.equal(actions.length, 2);
|
|
@@ -824,6 +831,7 @@ describe("planRevertAll — mcpServers and agentRules", () => {
|
|
|
824
831
|
],
|
|
825
832
|
permissions: [],
|
|
826
833
|
agentDefinitions: [],
|
|
834
|
+
hooks: [],
|
|
827
835
|
};
|
|
828
836
|
const actions = planRevertAll(manifest, home);
|
|
829
837
|
assert.equal(actions.length, 2);
|