@kuznai/inception-engine 0.19.0 → 0.21.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 +50 -22
- package/dist/{config → src/config}/agents.js +113 -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/src/core/adapters/index.js +25 -0
- 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 +82 -54
- package/dist/{core → src/core}/detect.js +2 -7
- package/dist/{core → src/core}/init.js +20 -25
- 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/src/core/preflight.js +296 -0
- package/dist/{core → src/core}/revert.js +86 -26
- package/dist/{core → src/core}/validation.js +31 -0
- package/dist/{errors.d.ts → src/errors.d.ts} +3 -1
- package/dist/{errors.js → src/errors.js} +2 -2
- 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} +15 -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/agentRules-integration.test.d.ts +1 -0
- package/dist/test/os/windows/agentRules-integration.test.js +245 -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 +1017 -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/init-fixture.test.d.ts +1 -0
- package/dist/test/unit/init-fixture.test.js +153 -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 +521 -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/core/adapters/index.js +0 -30
- package/dist/core/preflight.js +0 -168
- /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/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}/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/{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,328 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import { AGENT_REGISTRY } from "../../../src/config/agents.js";
|
|
6
|
+
import { getDeployMethod, lookupHomeForUserWith, resolveAgentDetectPathFor, resolveAgentSkillPathFor, } from "../../../src/core/resolve.js";
|
|
7
|
+
import { resolveTargetTemplate } from "../../../src/core/runtime-paths.js";
|
|
8
|
+
import { UserError } from "../../../src/errors.js";
|
|
9
|
+
const HOME = "/home/u";
|
|
10
|
+
const SKILL = "s";
|
|
11
|
+
const posixJoin = path.posix.join;
|
|
12
|
+
const joinLikeRoot = (root, ...segments) => (root.includes("\\") || /^[a-zA-Z]:/.test(root)
|
|
13
|
+
? path.win32
|
|
14
|
+
: path.posix).join(root, ...segments);
|
|
15
|
+
function getAgent(id) {
|
|
16
|
+
const agent = AGENT_REGISTRY.find((a) => a.id === id);
|
|
17
|
+
assert.ok(agent, `agent "${id}" not found in AGENT_REGISTRY`);
|
|
18
|
+
return agent;
|
|
19
|
+
}
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// resolveAgentSkillPathFor — POSIX
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
describe("resolveAgentSkillPathFor — posix", () => {
|
|
24
|
+
const p = "posix";
|
|
25
|
+
it("claude-code", () => {
|
|
26
|
+
const agent = getAgent("claude-code");
|
|
27
|
+
assert.equal(resolveAgentSkillPathFor(agent, SKILL, HOME, p), posixJoin(HOME, ".claude", "skills", SKILL));
|
|
28
|
+
});
|
|
29
|
+
it("codex", () => {
|
|
30
|
+
const agent = getAgent("codex");
|
|
31
|
+
assert.equal(resolveAgentSkillPathFor(agent, SKILL, HOME, p), posixJoin(HOME, ".codex", "skills", SKILL));
|
|
32
|
+
});
|
|
33
|
+
it("gemini-cli", () => {
|
|
34
|
+
const agent = getAgent("gemini-cli");
|
|
35
|
+
assert.equal(resolveAgentSkillPathFor(agent, SKILL, HOME, p), posixJoin(HOME, ".gemini", "skills", SKILL));
|
|
36
|
+
});
|
|
37
|
+
it("antigravity", () => {
|
|
38
|
+
const agent = getAgent("antigravity");
|
|
39
|
+
assert.equal(resolveAgentSkillPathFor(agent, SKILL, HOME, p), posixJoin(HOME, ".gemini", "antigravity", "skills", SKILL));
|
|
40
|
+
});
|
|
41
|
+
it("opencode defaults to home/.config when XDG_CONFIG_HOME is unset", () => {
|
|
42
|
+
const agent = getAgent("opencode");
|
|
43
|
+
const saved = process.env.XDG_CONFIG_HOME;
|
|
44
|
+
try {
|
|
45
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
46
|
+
assert.equal(resolveAgentSkillPathFor(agent, SKILL, HOME, p), posixJoin(HOME, ".config", "opencode", "skills", SKILL));
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
if (saved === undefined)
|
|
50
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
51
|
+
else
|
|
52
|
+
process.env.XDG_CONFIG_HOME = saved;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
it("opencode uses XDG_CONFIG_HOME when set to an absolute path", () => {
|
|
56
|
+
const agent = getAgent("opencode");
|
|
57
|
+
const saved = process.env.XDG_CONFIG_HOME;
|
|
58
|
+
try {
|
|
59
|
+
process.env.XDG_CONFIG_HOME = "/custom/config";
|
|
60
|
+
assert.equal(resolveAgentSkillPathFor(agent, SKILL, HOME, p), posixJoin("/custom/config", "opencode", "skills", SKILL));
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
if (saved === undefined)
|
|
64
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
65
|
+
else
|
|
66
|
+
process.env.XDG_CONFIG_HOME = saved;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
it("opencode ignores relative XDG_CONFIG_HOME and falls back to home/.config", () => {
|
|
70
|
+
const agent = getAgent("opencode");
|
|
71
|
+
const saved = process.env.XDG_CONFIG_HOME;
|
|
72
|
+
try {
|
|
73
|
+
process.env.XDG_CONFIG_HOME = "relative/path";
|
|
74
|
+
assert.equal(resolveAgentSkillPathFor(agent, SKILL, HOME, p), posixJoin(HOME, ".config", "opencode", "skills", SKILL));
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
if (saved === undefined)
|
|
78
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
79
|
+
else
|
|
80
|
+
process.env.XDG_CONFIG_HOME = saved;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// resolveAgentSkillPathFor — Windows
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
describe("resolveAgentSkillPathFor — windows", () => {
|
|
88
|
+
const p = "windows";
|
|
89
|
+
it("claude-code uses home (same template as posix)", () => {
|
|
90
|
+
const agent = getAgent("claude-code");
|
|
91
|
+
assert.equal(resolveAgentSkillPathFor(agent, SKILL, HOME, p), posixJoin(HOME, ".claude", "skills", SKILL));
|
|
92
|
+
});
|
|
93
|
+
it("opencode uses APPDATA when set", () => {
|
|
94
|
+
const agent = getAgent("opencode");
|
|
95
|
+
const appdata = "C:\\Users\\u\\AppData\\Roaming";
|
|
96
|
+
const saved = process.env.APPDATA;
|
|
97
|
+
try {
|
|
98
|
+
process.env.APPDATA = appdata;
|
|
99
|
+
assert.equal(resolveAgentSkillPathFor(agent, SKILL, HOME, p), joinLikeRoot(appdata, "opencode", "skills", SKILL));
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
if (saved === undefined)
|
|
103
|
+
delete process.env.APPDATA;
|
|
104
|
+
else
|
|
105
|
+
process.env.APPDATA = saved;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
it("opencode falls back to home/AppData/Roaming when APPDATA is unset", () => {
|
|
109
|
+
const agent = getAgent("opencode");
|
|
110
|
+
const saved = process.env.APPDATA;
|
|
111
|
+
try {
|
|
112
|
+
delete process.env.APPDATA;
|
|
113
|
+
assert.equal(resolveAgentSkillPathFor(agent, SKILL, HOME, p), posixJoin(HOME, "AppData", "Roaming", "opencode", "skills", SKILL));
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
if (saved === undefined)
|
|
117
|
+
delete process.env.APPDATA;
|
|
118
|
+
else
|
|
119
|
+
process.env.APPDATA = saved;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
it("antigravity uses home (same template as posix)", () => {
|
|
123
|
+
const agent = getAgent("antigravity");
|
|
124
|
+
assert.equal(resolveAgentSkillPathFor(agent, SKILL, HOME, p), posixJoin(HOME, ".gemini", "antigravity", "skills", SKILL));
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// resolveAgentDetectPathFor — POSIX
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
describe("resolveAgentDetectPathFor — posix", () => {
|
|
131
|
+
const p = "posix";
|
|
132
|
+
it("claude-code", () => {
|
|
133
|
+
const agent = getAgent("claude-code");
|
|
134
|
+
assert.equal(resolveAgentDetectPathFor(agent, HOME, p), posixJoin(HOME, ".claude"));
|
|
135
|
+
});
|
|
136
|
+
it("opencode detect path defaults to home/.config when XDG_CONFIG_HOME is unset", () => {
|
|
137
|
+
const agent = getAgent("opencode");
|
|
138
|
+
const saved = process.env.XDG_CONFIG_HOME;
|
|
139
|
+
try {
|
|
140
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
141
|
+
assert.equal(resolveAgentDetectPathFor(agent, HOME, p), posixJoin(HOME, ".config", "opencode"));
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
if (saved === undefined)
|
|
145
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
146
|
+
else
|
|
147
|
+
process.env.XDG_CONFIG_HOME = saved;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
it("opencode detect path uses XDG_CONFIG_HOME when set to an absolute path", () => {
|
|
151
|
+
const agent = getAgent("opencode");
|
|
152
|
+
const saved = process.env.XDG_CONFIG_HOME;
|
|
153
|
+
try {
|
|
154
|
+
process.env.XDG_CONFIG_HOME = "/custom/config";
|
|
155
|
+
assert.equal(resolveAgentDetectPathFor(agent, HOME, p), posixJoin("/custom/config", "opencode"));
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
if (saved === undefined)
|
|
159
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
160
|
+
else
|
|
161
|
+
process.env.XDG_CONFIG_HOME = saved;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
it("antigravity has nested detect path", () => {
|
|
165
|
+
const agent = getAgent("antigravity");
|
|
166
|
+
assert.equal(resolveAgentDetectPathFor(agent, HOME, p), posixJoin(HOME, ".gemini", "antigravity"));
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// resolveAgentDetectPathFor — Windows
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
describe("resolveAgentDetectPathFor — windows", () => {
|
|
173
|
+
const p = "windows";
|
|
174
|
+
it("opencode uses APPDATA when set", () => {
|
|
175
|
+
const agent = getAgent("opencode");
|
|
176
|
+
const appdata = "C:\\Users\\u\\AppData\\Roaming";
|
|
177
|
+
const saved = process.env.APPDATA;
|
|
178
|
+
try {
|
|
179
|
+
process.env.APPDATA = appdata;
|
|
180
|
+
assert.equal(resolveAgentDetectPathFor(agent, HOME, p), joinLikeRoot(appdata, "opencode"));
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
if (saved === undefined)
|
|
184
|
+
delete process.env.APPDATA;
|
|
185
|
+
else
|
|
186
|
+
process.env.APPDATA = saved;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
it("claude-code uses home (same template as posix)", () => {
|
|
190
|
+
const agent = getAgent("claude-code");
|
|
191
|
+
assert.equal(resolveAgentDetectPathFor(agent, HOME, p), posixJoin(HOME, ".claude"));
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// lookupHomeForUserWith — getent path (linux)
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
describe("lookupHomeForUserWith — getent (linux)", () => {
|
|
198
|
+
const platform = "linux";
|
|
199
|
+
const noopRead = readFileSync; // won't be called in success cases
|
|
200
|
+
it("returns home from valid getent output", () => {
|
|
201
|
+
const execFileFn = ((_cmd, _args, _opts) => "alice:x:1000:1000::/home/alice:/bin/bash\n");
|
|
202
|
+
assert.equal(lookupHomeForUserWith("alice", platform, execFileFn, noopRead), "/home/alice");
|
|
203
|
+
});
|
|
204
|
+
it("falls through on getent failure and reads /etc/passwd", () => {
|
|
205
|
+
const execFileFn = ((_cmd, _args, _opts) => {
|
|
206
|
+
throw new Error("getent: command not found");
|
|
207
|
+
});
|
|
208
|
+
const readFileFn = ((_p, _enc) => "root:x:0:0:root:/root:/bin/bash\nalice:x:1000:1000::/home/alice:/bin/bash\n");
|
|
209
|
+
assert.equal(lookupHomeForUserWith("alice", platform, execFileFn, readFileFn), "/home/alice");
|
|
210
|
+
});
|
|
211
|
+
it("falls through when getent returns malformed output (no 6th field)", () => {
|
|
212
|
+
const execFileFn = ((_cmd, _args, _opts) => "badinput");
|
|
213
|
+
const readFileFn = ((_p, _enc) => "alice:x:1000:1000::/home/alice:/bin/bash\n");
|
|
214
|
+
assert.equal(lookupHomeForUserWith("alice", platform, execFileFn, readFileFn), "/home/alice");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// lookupHomeForUserWith — dscl path (darwin)
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
describe("lookupHomeForUserWith — dscl (darwin)", () => {
|
|
221
|
+
const platform = "darwin";
|
|
222
|
+
it("returns home from valid dscl output", () => {
|
|
223
|
+
const execFileFn = ((_cmd, _args, _opts) => "NFSHomeDirectory: /Users/alice\n");
|
|
224
|
+
assert.equal(lookupHomeForUserWith("alice", platform, execFileFn, readFileSync), "/Users/alice");
|
|
225
|
+
});
|
|
226
|
+
it("falls through on dscl failure and reads /etc/passwd", () => {
|
|
227
|
+
const execFileFn = ((_cmd, _args, _opts) => {
|
|
228
|
+
throw new Error("dscl: record not found");
|
|
229
|
+
});
|
|
230
|
+
const readFileFn = ((_p, _enc) => "alice:x:1000:1000::/Users/alice:/bin/zsh\n");
|
|
231
|
+
assert.equal(lookupHomeForUserWith("alice", platform, execFileFn, readFileFn), "/Users/alice");
|
|
232
|
+
});
|
|
233
|
+
it("falls through when dscl returns non-absolute home", () => {
|
|
234
|
+
const execFileFn = ((_cmd, _args, _opts) => "NFSHomeDirectory: \n");
|
|
235
|
+
const readFileFn = ((_p, _enc) => "alice:x:1000:1000::/Users/alice:/bin/zsh\n");
|
|
236
|
+
assert.equal(lookupHomeForUserWith("alice", platform, execFileFn, readFileFn), "/Users/alice");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// lookupHomeForUserWith — /etc/passwd fallback
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
describe("lookupHomeForUserWith — /etc/passwd fallback", () => {
|
|
243
|
+
const platform = "linux";
|
|
244
|
+
const throwExec = ((_cmd, _args, _opts) => {
|
|
245
|
+
throw new Error("getent absent");
|
|
246
|
+
});
|
|
247
|
+
it("finds user among multiple users", () => {
|
|
248
|
+
const readFileFn = ((_p, _enc) => "root:x:0:0:root:/root:/bin/bash\nalice:x:1000:1000::/home/alice:/bin/bash\nbob:x:1001:1001::/home/bob:/bin/bash\n");
|
|
249
|
+
assert.equal(lookupHomeForUserWith("bob", platform, throwExec, readFileFn), "/home/bob");
|
|
250
|
+
});
|
|
251
|
+
it("ignores lines with wrong username", () => {
|
|
252
|
+
const readFileFn = ((_p, _enc) => "alice:x:1000:1000::/home/alice:/bin/bash\nbob:x:1001:1001::/home/bob:/bin/bash\n");
|
|
253
|
+
// looking up "alice", should not return bob's home
|
|
254
|
+
assert.equal(lookupHomeForUserWith("alice", platform, throwExec, readFileFn), "/home/alice");
|
|
255
|
+
});
|
|
256
|
+
it("ignores passwd lines with non-absolute home field", () => {
|
|
257
|
+
// alice's home field is empty — should fall through to error
|
|
258
|
+
const readFileFn = ((_p, _enc) => "alice:x:1000:1000:::");
|
|
259
|
+
assert.throws(() => lookupHomeForUserWith("alice", platform, throwExec, readFileFn), (err) => err instanceof UserError && err.code === "RESOLVE_FAILED");
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// lookupHomeForUserWith — all methods fail
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
describe("lookupHomeForUserWith — all methods fail", () => {
|
|
266
|
+
it("throws UserError with code RESOLVE_FAILED", () => {
|
|
267
|
+
const throwExec = ((_cmd, _args, _opts) => {
|
|
268
|
+
throw new Error("no exec");
|
|
269
|
+
});
|
|
270
|
+
const throwRead = ((_p, _enc) => {
|
|
271
|
+
throw new Error("no read");
|
|
272
|
+
});
|
|
273
|
+
assert.throws(() => lookupHomeForUserWith("alice", "linux", throwExec, throwRead), (err) => {
|
|
274
|
+
assert.ok(err instanceof UserError);
|
|
275
|
+
assert.equal(err.code, "RESOLVE_FAILED");
|
|
276
|
+
assert.match(err.message, /Cannot determine home directory/);
|
|
277
|
+
return true;
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
describe("resolveTargetTemplate", () => {
|
|
282
|
+
it("uses APPDATA for Windows-style target templates", () => {
|
|
283
|
+
const saved = process.env.APPDATA;
|
|
284
|
+
try {
|
|
285
|
+
process.env.APPDATA = "C:\\Users\\u\\AppData\\Roaming";
|
|
286
|
+
assert.equal(resolveTargetTemplate("{appdata}\\opencode\\AGENTS.md", "/unused/home"), "C:\\Users\\u\\AppData\\Roaming\\opencode\\AGENTS.md");
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
if (saved === undefined)
|
|
290
|
+
delete process.env.APPDATA;
|
|
291
|
+
else
|
|
292
|
+
process.env.APPDATA = saved;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
it("falls back to home/AppData/Roaming when APPDATA is unset", () => {
|
|
296
|
+
const saved = process.env.APPDATA;
|
|
297
|
+
try {
|
|
298
|
+
delete process.env.APPDATA;
|
|
299
|
+
assert.equal(resolveTargetTemplate("{appdata}/opencode/AGENTS.md", "/home/u"), `${posixJoin("/home/u", "AppData", "Roaming")}/opencode/AGENTS.md`);
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
if (saved === undefined)
|
|
303
|
+
delete process.env.APPDATA;
|
|
304
|
+
else
|
|
305
|
+
process.env.APPDATA = saved;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
it("uses absolute XDG_CONFIG_HOME and ignores relative values", () => {
|
|
309
|
+
const saved = process.env.XDG_CONFIG_HOME;
|
|
310
|
+
try {
|
|
311
|
+
process.env.XDG_CONFIG_HOME = "/custom/config";
|
|
312
|
+
assert.equal(resolveTargetTemplate("{xdg_config}/opencode/AGENTS.md", "/home/u"), "/custom/config/opencode/AGENTS.md");
|
|
313
|
+
process.env.XDG_CONFIG_HOME = "relative/path";
|
|
314
|
+
assert.equal(resolveTargetTemplate("{xdg_config}/opencode/AGENTS.md", "/home/u"), `${posixJoin("/home/u", ".config")}/opencode/AGENTS.md`);
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
if (saved === undefined)
|
|
318
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
319
|
+
else
|
|
320
|
+
process.env.XDG_CONFIG_HOME = saved;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
describe("getDeployMethod", () => {
|
|
325
|
+
it("returns a host-appropriate skill-dir deployment method", () => {
|
|
326
|
+
assert.equal(getDeployMethod(), process.platform === "win32" ? "copy" : "symlink");
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe } from "node:test";
|
|
2
|
+
import { assertSymlinkTarget } from "../../helpers/skill-dir.js";
|
|
3
|
+
import { registerSharedSkillDirDeployScenarios, registerSharedSkillDirRevertScenarios, } from "../../helpers/skill-dir-scenarios.js";
|
|
4
|
+
describe("skill-dir deploy and revert (POSIX)", {
|
|
5
|
+
skip: process.platform === "win32",
|
|
6
|
+
}, () => {
|
|
7
|
+
registerSharedSkillDirDeployScenarios({
|
|
8
|
+
method: "symlink",
|
|
9
|
+
async assertManagedTarget(target, source) {
|
|
10
|
+
await assertSymlinkTarget(target, source);
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
registerSharedSkillDirRevertScenarios({
|
|
14
|
+
method: "symlink",
|
|
15
|
+
async assertManagedTarget(target, source) {
|
|
16
|
+
await assertSymlinkTarget(target, source);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
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 { lookupDeployment, registerDeployment, } from "../../../src/core/ownership.js";
|
|
6
|
+
import { executeRevert } from "../../../src/core/revert.js";
|
|
7
|
+
import { exists, makeTmpDir } from "../../helpers/fs.js";
|
|
8
|
+
describe("revert integration (Windows)", {
|
|
9
|
+
skip: process.platform !== "win32",
|
|
10
|
+
}, () => {
|
|
11
|
+
it("reverts a deployed mcpServer config-patch and unregisters it", async () => {
|
|
12
|
+
const home = await makeTmpDir("ie-revert-home");
|
|
13
|
+
try {
|
|
14
|
+
const configFile = path.join(home, ".claude.json");
|
|
15
|
+
await writeFile(configFile, JSON.stringify({
|
|
16
|
+
other: "value",
|
|
17
|
+
mcpServers: { "my-mcp": { command: "npx" } },
|
|
18
|
+
}));
|
|
19
|
+
await registerDeployment(home, configFile, {
|
|
20
|
+
kind: "config-patch",
|
|
21
|
+
patch: { mcpServers: { "my-mcp": { command: "npx" } } },
|
|
22
|
+
undoPatch: { mcpServers: null },
|
|
23
|
+
skill: "my-mcp",
|
|
24
|
+
agent: "claude-code",
|
|
25
|
+
});
|
|
26
|
+
const action = {
|
|
27
|
+
kind: "config-patch",
|
|
28
|
+
skill: "my-mcp",
|
|
29
|
+
agent: "claude-code",
|
|
30
|
+
target: configFile,
|
|
31
|
+
};
|
|
32
|
+
const { succeeded, failed } = await executeRevert([action], false, false, home);
|
|
33
|
+
assert.equal(succeeded, 1);
|
|
34
|
+
assert.equal(failed.length, 0);
|
|
35
|
+
assert.deepEqual(JSON.parse(await readFile(configFile, "utf-8")), {
|
|
36
|
+
other: "value",
|
|
37
|
+
});
|
|
38
|
+
assert.equal(await lookupDeployment(home, configFile), null);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await rm(home, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
it("reverts a deployed agentRule file-write and unregisters it", async () => {
|
|
45
|
+
const home = await makeTmpDir("ie-revert-home");
|
|
46
|
+
try {
|
|
47
|
+
const rulesFile = path.join(home, ".claude", "CLAUDE.md");
|
|
48
|
+
await mkdir(path.dirname(rulesFile), { recursive: true });
|
|
49
|
+
await writeFile(rulesFile, "# My Rules\n");
|
|
50
|
+
await registerDeployment(home, rulesFile, {
|
|
51
|
+
kind: "file-write",
|
|
52
|
+
source: "/some/source/CLAUDE.md",
|
|
53
|
+
skill: "my-rules",
|
|
54
|
+
agent: "claude-code",
|
|
55
|
+
});
|
|
56
|
+
const action = {
|
|
57
|
+
kind: "file-write",
|
|
58
|
+
skill: "my-rules",
|
|
59
|
+
agent: "claude-code",
|
|
60
|
+
target: rulesFile,
|
|
61
|
+
};
|
|
62
|
+
const { succeeded, failed } = await executeRevert([action], false, false, home);
|
|
63
|
+
assert.equal(succeeded, 1);
|
|
64
|
+
assert.equal(failed.length, 0);
|
|
65
|
+
assert.ok(!(await exists(rulesFile)));
|
|
66
|
+
assert.equal(await lookupDeployment(home, rulesFile), null);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await rm(home, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|