@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.
Files changed (105) hide show
  1. package/README.md +48 -22
  2. package/dist/{config → src/config}/agents.js +87 -14
  3. package/dist/{core → src/core}/adapters/agent-definitions.d.ts +0 -12
  4. package/dist/src/core/adapters/agent-definitions.js +162 -0
  5. package/dist/{core → src/core}/adapters/frontmatter.d.ts +10 -0
  6. package/dist/{core → src/core}/adapters/frontmatter.js +23 -6
  7. package/dist/{core → src/core}/adapters/mcp.js +17 -20
  8. package/dist/{core → src/core}/adapters/permissions.js +16 -20
  9. package/dist/{core → src/core}/adapters/rules.js +26 -50
  10. package/dist/src/core/capabilities.d.ts +47 -0
  11. package/dist/src/core/capabilities.js +280 -0
  12. package/dist/{core → src/core}/deploy.js +70 -41
  13. package/dist/{core → src/core}/init.js +18 -24
  14. package/dist/src/core/merge-patch.d.ts +4 -0
  15. package/dist/src/core/merge-patch.js +46 -0
  16. package/dist/{core → src/core}/ownership.js +13 -0
  17. package/dist/{core → src/core}/preflight.js +141 -29
  18. package/dist/{core → src/core}/revert.js +86 -26
  19. package/dist/{core → src/core}/validation.js +20 -0
  20. package/dist/{schemas → src/schemas}/manifest.d.ts +20 -0
  21. package/dist/{schemas → src/schemas}/manifest.js +11 -0
  22. package/dist/{schemas → src/schemas}/registry.d.ts +36 -0
  23. package/dist/{schemas → src/schemas}/registry.js +12 -4
  24. package/dist/{types.d.ts → src/types.d.ts} +13 -0
  25. package/dist/test/helpers/fs.d.ts +2 -0
  26. package/dist/test/helpers/fs.js +17 -0
  27. package/dist/test/helpers/path.d.ts +10 -0
  28. package/dist/test/helpers/path.js +15 -0
  29. package/dist/test/helpers/skill-dir-scenarios.d.ts +7 -0
  30. package/dist/test/helpers/skill-dir-scenarios.js +206 -0
  31. package/dist/test/helpers/skill-dir.d.ts +7 -0
  32. package/dist/test/helpers/skill-dir.js +46 -0
  33. package/dist/test/os/cross-platform/cross-platform.test.d.ts +1 -0
  34. package/dist/test/os/cross-platform/cross-platform.test.js +328 -0
  35. package/dist/test/os/posix/skill-dir.test.d.ts +1 -0
  36. package/dist/test/os/posix/skill-dir.test.js +19 -0
  37. package/dist/test/os/windows/revert-integration.test.d.ts +1 -0
  38. package/dist/test/os/windows/revert-integration.test.js +72 -0
  39. package/dist/test/os/windows/skill-dir.test.d.ts +1 -0
  40. package/dist/test/os/windows/skill-dir.test.js +19 -0
  41. package/dist/test/unit/adapters.test.d.ts +1 -0
  42. package/dist/test/unit/adapters.test.js +947 -0
  43. package/dist/test/unit/capabilities.test.d.ts +1 -0
  44. package/dist/test/unit/capabilities.test.js +71 -0
  45. package/dist/test/unit/cli.test.d.ts +1 -0
  46. package/dist/test/unit/cli.test.js +834 -0
  47. package/dist/test/unit/deploy.test.d.ts +1 -0
  48. package/dist/test/unit/deploy.test.js +2481 -0
  49. package/dist/test/unit/detect.test.d.ts +1 -0
  50. package/dist/test/unit/detect.test.js +192 -0
  51. package/dist/test/unit/enterprise.test.d.ts +1 -0
  52. package/dist/test/unit/enterprise.test.js +106 -0
  53. package/dist/test/unit/frontmatter.test.d.ts +1 -0
  54. package/dist/test/unit/frontmatter.test.js +102 -0
  55. package/dist/test/unit/gemini-north-star.test.d.ts +1 -0
  56. package/dist/test/unit/gemini-north-star.test.js +243 -0
  57. package/dist/test/unit/manifest.test.d.ts +1 -0
  58. package/dist/test/unit/manifest.test.js +737 -0
  59. package/dist/test/unit/migration-golden.test.d.ts +1 -0
  60. package/dist/test/unit/migration-golden.test.js +197 -0
  61. package/dist/test/unit/ownership.test.d.ts +1 -0
  62. package/dist/test/unit/ownership.test.js +587 -0
  63. package/dist/test/unit/preflight.test.d.ts +1 -0
  64. package/dist/test/unit/preflight.test.js +501 -0
  65. package/dist/test/unit/resolve.test.d.ts +1 -0
  66. package/dist/test/unit/resolve.test.js +119 -0
  67. package/dist/test/unit/revert.test.d.ts +1 -0
  68. package/dist/test/unit/revert.test.js +1004 -0
  69. package/dist/test/unit/toml.test.d.ts +1 -0
  70. package/dist/test/unit/toml.test.js +55 -0
  71. package/package.json +1 -1
  72. package/dist/core/adapters/agent-definitions.js +0 -91
  73. /package/dist/{config → src/config}/agents.d.ts +0 -0
  74. /package/dist/{config → src/config}/manifest.d.ts +0 -0
  75. /package/dist/{config → src/config}/manifest.js +0 -0
  76. /package/dist/{core → src/core}/adapters/index.d.ts +0 -0
  77. /package/dist/{core → src/core}/adapters/index.js +0 -0
  78. /package/dist/{core → src/core}/adapters/mcp.d.ts +0 -0
  79. /package/dist/{core → src/core}/adapters/permissions.d.ts +0 -0
  80. /package/dist/{core → src/core}/adapters/rules.d.ts +0 -0
  81. /package/dist/{core → src/core}/adapters/toml.d.ts +0 -0
  82. /package/dist/{core → src/core}/adapters/toml.js +0 -0
  83. /package/dist/{core → src/core}/deploy.d.ts +0 -0
  84. /package/dist/{core → src/core}/detect.d.ts +0 -0
  85. /package/dist/{core → src/core}/detect.js +0 -0
  86. /package/dist/{core → src/core}/init.d.ts +0 -0
  87. /package/dist/{core → src/core}/ownership.d.ts +0 -0
  88. /package/dist/{core → src/core}/preflight.d.ts +0 -0
  89. /package/dist/{core → src/core}/resolve.d.ts +0 -0
  90. /package/dist/{core → src/core}/resolve.js +0 -0
  91. /package/dist/{core → src/core}/revert.d.ts +0 -0
  92. /package/dist/{core → src/core}/runtime-paths.d.ts +0 -0
  93. /package/dist/{core → src/core}/runtime-paths.js +0 -0
  94. /package/dist/{core → src/core}/validation.d.ts +0 -0
  95. /package/dist/{errors.d.ts → src/errors.d.ts} +0 -0
  96. /package/dist/{errors.js → src/errors.js} +0 -0
  97. /package/dist/{formatters.d.ts → src/formatters.d.ts} +0 -0
  98. /package/dist/{formatters.js → src/formatters.js} +0 -0
  99. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  100. /package/dist/{index.js → src/index.js} +0 -0
  101. /package/dist/{logger.d.ts → src/logger.d.ts} +0 -0
  102. /package/dist/{logger.js → src/logger.js} +0 -0
  103. /package/dist/{schemas → src/schemas}/errors.d.ts +0 -0
  104. /package/dist/{schemas → src/schemas}/errors.js +0 -0
  105. /package/dist/{types.js → src/types.js} +0 -0
@@ -0,0 +1,206 @@
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 { it } from "node:test";
5
+ import { executeDeploy, planDeploy } from "../../src/core/deploy.js";
6
+ import { lookupDeployment, registerDeployment, } from "../../src/core/ownership.js";
7
+ import { executeRevert, planRevert } from "../../src/core/revert.js";
8
+ import { exists, makeTmpDir } from "./fs.js";
9
+ import { createFailingRegistryPersistence, createSkillSource, testSkillManifest, } from "./skill-dir.js";
10
+ const FIRST_SKILL_MD = "---\nname: test\ndescription: First skill\n---\n# First";
11
+ const SECOND_SKILL_MD = "---\nname: test\ndescription: Second skill\n---\n# Second";
12
+ export function registerSharedSkillDirDeployScenarios(options) {
13
+ it("first deploy succeeds", async () => {
14
+ const sourceDir = await makeTmpDir("ie-skill-dir-source");
15
+ const home = await makeTmpDir("ie-skill-dir-home");
16
+ try {
17
+ const source = await createSkillSource(sourceDir, "skills/test-skill", FIRST_SKILL_MD);
18
+ const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
19
+ const { succeeded, failed } = await executeDeploy(actions, false, false, home);
20
+ assert.equal(succeeded, 1);
21
+ assert.equal(failed.length, 0);
22
+ await options.assertManagedTarget(actions[0]?.target, source, FIRST_SKILL_MD);
23
+ const entry = await lookupDeployment(home, actions[0]?.target);
24
+ assert.ok(entry);
25
+ assert.equal(entry.kind, "skill-dir");
26
+ assert.equal(entry.method, options.method);
27
+ }
28
+ finally {
29
+ await rm(sourceDir, { recursive: true, force: true });
30
+ await rm(home, { recursive: true, force: true });
31
+ }
32
+ });
33
+ it("dry-run reports the planned skill-dir change only", async () => {
34
+ const sourceDir = await makeTmpDir("ie-skill-dir-source");
35
+ const home = await makeTmpDir("ie-skill-dir-home");
36
+ try {
37
+ await createSkillSource(sourceDir, "skills/test-skill", FIRST_SKILL_MD);
38
+ const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
39
+ const { succeeded, failed, planned } = await executeDeploy(actions, true, false, home);
40
+ assert.equal(succeeded, 1);
41
+ assert.equal(failed.length, 0);
42
+ assert.equal(planned.length, 1);
43
+ assert.equal(planned[0]?.verb, options.method === "symlink" ? "create-symlink" : "copy-dir");
44
+ assert.ok(!(await exists(actions[0]?.target)));
45
+ }
46
+ finally {
47
+ await rm(sourceDir, { recursive: true, force: true });
48
+ await rm(home, { recursive: true, force: true });
49
+ }
50
+ });
51
+ it("refuses to overwrite an unmanaged target", async () => {
52
+ const sourceDir = await makeTmpDir("ie-skill-dir-source");
53
+ const home = await makeTmpDir("ie-skill-dir-home");
54
+ try {
55
+ await createSkillSource(sourceDir, "skills/test-skill", FIRST_SKILL_MD);
56
+ const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
57
+ const target = actions[0]?.target;
58
+ await mkdir(target, { recursive: true });
59
+ await writeFile(path.join(target, "user.txt"), "user content");
60
+ const { succeeded, failed } = await executeDeploy(actions, false, false, home);
61
+ assert.equal(succeeded, 0);
62
+ assert.equal(failed.length, 1);
63
+ assert.match(failed[0]?.error, /not managed by inception-engine/);
64
+ assert.equal(await readFile(path.join(target, "user.txt"), "utf-8"), "user content");
65
+ }
66
+ finally {
67
+ await rm(sourceDir, { recursive: true, force: true });
68
+ await rm(home, { recursive: true, force: true });
69
+ }
70
+ });
71
+ it("refuses to overwrite a target with a mismatched registry entry", async () => {
72
+ const sourceDir = await makeTmpDir("ie-skill-dir-source");
73
+ const home = await makeTmpDir("ie-skill-dir-home");
74
+ try {
75
+ await createSkillSource(sourceDir, "skills/test-skill", FIRST_SKILL_MD);
76
+ const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
77
+ const target = actions[0]?.target;
78
+ await mkdir(target, { recursive: true });
79
+ await writeFile(path.join(target, "SKILL.md"), "other");
80
+ await registerDeployment(home, target, {
81
+ kind: "skill-dir",
82
+ source: "/other/source",
83
+ skill: "other-skill",
84
+ agent: "codex",
85
+ method: options.method,
86
+ });
87
+ const { succeeded, failed } = await executeDeploy(actions, false, false, home);
88
+ assert.equal(succeeded, 0);
89
+ assert.equal(failed.length, 1);
90
+ assert.match(failed[0]?.error, /not managed by inception-engine/);
91
+ assert.equal(await readFile(path.join(target, "SKILL.md"), "utf-8"), "other");
92
+ }
93
+ finally {
94
+ await rm(sourceDir, { recursive: true, force: true });
95
+ await rm(home, { recursive: true, force: true });
96
+ }
97
+ });
98
+ it("managed redeploy succeeds", async () => {
99
+ const sourceDir = await makeTmpDir("ie-skill-dir-source");
100
+ const home = await makeTmpDir("ie-skill-dir-home");
101
+ try {
102
+ const source = await createSkillSource(sourceDir, "skills/test-skill", FIRST_SKILL_MD);
103
+ const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
104
+ await executeDeploy(actions, false, false, home);
105
+ const { succeeded, failed } = await executeDeploy(actions, false, false, home);
106
+ assert.equal(succeeded, 1);
107
+ assert.equal(failed.length, 0);
108
+ await options.assertManagedTarget(actions[0]?.target, source, FIRST_SKILL_MD);
109
+ }
110
+ finally {
111
+ await rm(sourceDir, { recursive: true, force: true });
112
+ await rm(home, { recursive: true, force: true });
113
+ }
114
+ });
115
+ it("restores the prior managed target when registry persistence fails", async () => {
116
+ const sourceDir = await makeTmpDir("ie-skill-dir-source");
117
+ const home = await makeTmpDir("ie-skill-dir-home");
118
+ try {
119
+ const firstSource = await createSkillSource(sourceDir, "skills/test-skill", FIRST_SKILL_MD);
120
+ const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
121
+ const firstAction = actions[0];
122
+ await executeDeploy([firstAction], false, false, home);
123
+ await writeFile(path.join(firstSource, "SKILL.md"), SECOND_SKILL_MD);
124
+ const failAction = {
125
+ ...firstAction,
126
+ };
127
+ const { succeeded, failed } = await executeDeploy([failAction], false, false, home, {
128
+ registry: createFailingRegistryPersistence(),
129
+ });
130
+ assert.equal(succeeded, 0);
131
+ assert.equal(failed.length, 1);
132
+ assert.match(failed[0]?.error, /simulated registry persistence failure/);
133
+ await options.assertManagedTarget(firstAction.target, firstSource, FIRST_SKILL_MD);
134
+ assert.ok(!(await exists(`${firstAction.target}.inception-backup`)));
135
+ }
136
+ finally {
137
+ await rm(sourceDir, { recursive: true, force: true });
138
+ await rm(home, { recursive: true, force: true });
139
+ }
140
+ });
141
+ }
142
+ export function registerSharedSkillDirRevertScenarios(options) {
143
+ it("reverts a managed skill-dir target", async () => {
144
+ const sourceDir = await makeTmpDir("ie-skill-dir-source");
145
+ const home = await makeTmpDir("ie-skill-dir-home");
146
+ try {
147
+ await createSkillSource(sourceDir, "skills/test-skill", FIRST_SKILL_MD);
148
+ const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
149
+ await executeDeploy(actions, false, false, home);
150
+ const revertActions = planRevert(testSkillManifest, ["claude-code"], home);
151
+ const { succeeded, skipped, failed } = await executeRevert(revertActions, false, false, home);
152
+ assert.equal(succeeded, 1);
153
+ assert.equal(skipped, 0);
154
+ assert.equal(failed.length, 0);
155
+ assert.ok(!(await exists(actions[0]?.target)));
156
+ }
157
+ finally {
158
+ await rm(sourceDir, { recursive: true, force: true });
159
+ await rm(home, { recursive: true, force: true });
160
+ }
161
+ });
162
+ it("skips an unmanaged skill-dir target", async () => {
163
+ const home = await makeTmpDir("ie-skill-dir-home");
164
+ try {
165
+ const revertActions = planRevert(testSkillManifest, ["claude-code"], home);
166
+ const target = revertActions[0]?.target;
167
+ await mkdir(target, { recursive: true });
168
+ await writeFile(path.join(target, "SKILL.md"), FIRST_SKILL_MD);
169
+ const { succeeded, skipped, failed } = await executeRevert(revertActions, false, false, home);
170
+ assert.equal(succeeded, 0);
171
+ assert.equal(skipped, 1);
172
+ assert.equal(failed.length, 0);
173
+ assert.ok(await exists(target));
174
+ }
175
+ finally {
176
+ await rm(home, { recursive: true, force: true });
177
+ }
178
+ });
179
+ it("skips a target whose registry entry belongs to a different skill", async () => {
180
+ const sourceDir = await makeTmpDir("ie-skill-dir-source");
181
+ const home = await makeTmpDir("ie-skill-dir-home");
182
+ try {
183
+ const source = await createSkillSource(sourceDir, "skills/test-skill", FIRST_SKILL_MD);
184
+ const revertActions = planRevert(testSkillManifest, ["claude-code"], home);
185
+ const target = revertActions[0]?.target;
186
+ await mkdir(target, { recursive: true });
187
+ await writeFile(path.join(target, "SKILL.md"), FIRST_SKILL_MD);
188
+ await registerDeployment(home, target, {
189
+ kind: "skill-dir",
190
+ source,
191
+ skill: "different-skill",
192
+ agent: "codex",
193
+ method: options.method,
194
+ });
195
+ const { succeeded, skipped, failed } = await executeRevert(revertActions, false, false, home);
196
+ assert.equal(succeeded, 0);
197
+ assert.equal(skipped, 1);
198
+ assert.equal(failed.length, 0);
199
+ assert.ok(await exists(target));
200
+ }
201
+ finally {
202
+ await rm(sourceDir, { recursive: true, force: true });
203
+ await rm(home, { recursive: true, force: true });
204
+ }
205
+ });
206
+ }
@@ -0,0 +1,7 @@
1
+ import type { RegistryPersistence } from "../../src/core/ownership.ts";
2
+ import type { Manifest } from "../../src/types.ts";
3
+ export declare function createSkillSource(baseDir: string, skillPath?: string, skillBody?: string): Promise<string>;
4
+ export declare const testSkillManifest: Manifest;
5
+ export declare function createFailingRegistryPersistence(error?: Error): RegistryPersistence;
6
+ export declare function assertSymlinkTarget(target: string, source: string): Promise<void>;
7
+ export declare function assertCopyTarget(target: string, expectedSkillMd: string): Promise<void>;
@@ -0,0 +1,46 @@
1
+ import assert from "node:assert/strict";
2
+ import { lstat, mkdir, readFile, readlink, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { defaultRegistryPersistence } from "../../src/core/ownership.js";
5
+ import { exists } from "./fs.js";
6
+ export async function createSkillSource(baseDir, skillPath = "skills/test-skill", skillBody = "---\nname: test\ndescription: Test skill\n---\n# Test") {
7
+ const fullPath = path.join(baseDir, skillPath);
8
+ await mkdir(fullPath, { recursive: true });
9
+ await writeFile(path.join(fullPath, "SKILL.md"), skillBody);
10
+ return fullPath;
11
+ }
12
+ export const testSkillManifest = {
13
+ skills: [
14
+ {
15
+ name: "test-skill",
16
+ path: "skills/test-skill",
17
+ agents: ["claude-code"],
18
+ },
19
+ ],
20
+ files: [],
21
+ configs: [],
22
+ mcpServers: [],
23
+ agentRules: [],
24
+ permissions: [],
25
+ agentDefinitions: [],
26
+ };
27
+ export function createFailingRegistryPersistence(error = new Error("simulated registry persistence failure")) {
28
+ return {
29
+ load(home) {
30
+ return defaultRegistryPersistence.load(home);
31
+ },
32
+ async save() {
33
+ throw error;
34
+ },
35
+ };
36
+ }
37
+ export async function assertSymlinkTarget(target, source) {
38
+ assert.ok(await exists(target), "expected target to exist");
39
+ assert.ok((await lstat(target)).isSymbolicLink(), "expected a symlink target");
40
+ assert.equal(await readlink(target), source);
41
+ }
42
+ export async function assertCopyTarget(target, expectedSkillMd) {
43
+ assert.ok(await exists(target), "expected target to exist");
44
+ assert.ok(!(await lstat(target)).isSymbolicLink(), "expected a copied directory");
45
+ assert.equal(await readFile(path.join(target, "SKILL.md"), "utf-8"), expectedSkillMd);
46
+ }
@@ -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 {};