@kuznai/inception-engine 0.22.0 → 0.24.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 (43) hide show
  1. package/README.md +50 -9
  2. package/dist/src/config/agents.js +39 -0
  3. package/dist/src/core/adapters/execution-config.d.ts +14 -0
  4. package/dist/src/core/adapters/execution-config.js +60 -0
  5. package/dist/src/core/adapters/index.d.ts +4 -3
  6. package/dist/src/core/adapters/index.js +7 -5
  7. package/dist/src/core/adapters/rules.js +50 -19
  8. package/dist/src/core/capabilities.d.ts +1 -1
  9. package/dist/src/core/capabilities.js +18 -2
  10. package/dist/src/core/deploy.js +66 -2
  11. package/dist/src/core/init.js +69 -13
  12. package/dist/src/core/ownership.d.ts +1 -0
  13. package/dist/src/core/ownership.js +34 -4
  14. package/dist/src/core/preflight.js +28 -0
  15. package/dist/src/core/revert.js +86 -18
  16. package/dist/src/core/validation.d.ts +1 -1
  17. package/dist/src/core/validation.js +56 -3
  18. package/dist/src/schemas/manifest.d.ts +31 -0
  19. package/dist/src/schemas/manifest.js +39 -3
  20. package/dist/src/types.d.ts +6 -2
  21. package/dist/test/helpers/path.d.ts +2 -2
  22. package/dist/test/helpers/path.js +3 -3
  23. package/dist/test/os/posix/deploy.test.js +395 -0
  24. package/dist/test/os/posix/revert.test.js +188 -0
  25. package/dist/test/os/windows/deploy.test.d.ts +1 -0
  26. package/dist/test/os/windows/deploy.test.js +238 -0
  27. package/dist/test/unit/adapters.test.js +402 -2
  28. package/dist/test/unit/capabilities.test.js +44 -1
  29. package/dist/test/unit/cli.test.js +162 -0
  30. package/dist/test/unit/deploy.test.js +88 -647
  31. package/dist/test/unit/formatters.test.d.ts +1 -0
  32. package/dist/test/unit/formatters.test.js +74 -0
  33. package/dist/test/unit/init-fixture.test.js +85 -1
  34. package/dist/test/unit/manifest.test.js +72 -0
  35. package/dist/test/unit/ownership.test.js +61 -4
  36. package/dist/test/unit/preflight.test.js +145 -0
  37. package/dist/test/unit/resolve.test.js +67 -1
  38. package/dist/test/unit/revert.test.js +81 -182
  39. package/package.json +1 -1
  40. package/dist/test/os/windows/agentRules-integration.test.js +0 -245
  41. package/dist/test/os/windows/revert-integration.test.js +0 -72
  42. /package/dist/test/os/{windows/agentRules-integration.test.d.ts → posix/deploy.test.d.ts} +0 -0
  43. /package/dist/test/os/{windows/revert-integration.test.d.ts → posix/revert.test.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { chmod, copyFile, lstat, mkdir, readdir, readFile, readlink, rename, rm, symlink, writeFile, } from "node:fs/promises";
2
+ import { copyFile, mkdir, readdir, readFile, rename, rm, symlink, writeFile, } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { describe, it } from "node:test";
5
5
  import { executeDeploy, planDeploy } from "../../src/core/deploy.js";
@@ -68,6 +68,32 @@ describe("planDeploy", () => {
68
68
  await rm(sourceDir, { recursive: true });
69
69
  }
70
70
  });
71
+ it("includes executionConfigs actions for supported detected agents", async () => {
72
+ const sourceDir = await makeTmpDir();
73
+ try {
74
+ const manifest = {
75
+ ...testManifest,
76
+ executionConfigs: [
77
+ {
78
+ name: "gemini-safety",
79
+ agents: ["gemini-cli"],
80
+ config: { safeMode: true },
81
+ },
82
+ ],
83
+ };
84
+ await createSkillSource(sourceDir, "skills/test-skill");
85
+ const { actions } = await planDeploy(manifest, sourceDir, ["gemini-cli"], "/home/test");
86
+ const executionAction = actions.find((action) => action.kind === "config-patch" &&
87
+ action.agent === "gemini-cli" &&
88
+ action.skill === "gemini-safety");
89
+ assert.ok(executionAction, "expected planDeploy to include the gemini executionConfigs patch");
90
+ assert.deepEqual(executionAction.patch, { safeMode: true });
91
+ assertPathEndsWith(executionAction.target, ".gemini/settings.json", `expected Gemini execution config target under .gemini/settings.json, got: ${executionAction.target}`);
92
+ }
93
+ finally {
94
+ await rm(sourceDir, { recursive: true });
95
+ }
96
+ });
71
97
  it("throws when skill source path does not exist", async () => {
72
98
  const sourceDir = await makeTmpDir();
73
99
  try {
@@ -561,13 +587,13 @@ describe("planDeploy", () => {
561
587
  {
562
588
  name: "codex-file",
563
589
  path: "missing.txt",
564
- target: "{home}/missing.txt",
590
+ target: "{home}/.codex/AGENTS.md",
565
591
  agents: ["codex"],
566
592
  },
567
593
  {
568
594
  name: "claude-file",
569
595
  path: "present.txt",
570
- target: "{home}/present.txt",
596
+ target: "{home}/.claude/CLAUDE.md",
571
597
  agents: ["claude-code"],
572
598
  },
573
599
  ],
@@ -587,6 +613,65 @@ describe("planDeploy", () => {
587
613
  await rm(sourceDir, { recursive: true });
588
614
  }
589
615
  });
616
+ it("rejects generic home-scoped file targets outside approved agent surfaces", async () => {
617
+ const sourceDir = await makeTmpDir();
618
+ const manifest = {
619
+ skills: [],
620
+ files: [
621
+ {
622
+ name: "unsafe-file",
623
+ path: "present.txt",
624
+ target: "{home}/.ssh/config",
625
+ agents: ["claude-code"],
626
+ },
627
+ ],
628
+ configs: [],
629
+ mcpServers: [],
630
+ agentRules: [],
631
+ permissions: [],
632
+ agentDefinitions: [],
633
+ };
634
+ try {
635
+ await writeFile(path.join(sourceDir, "present.txt"), "present");
636
+ await assert.rejects(planDeploy(manifest, sourceDir, ["claude-code"], "/home/test"), (err) => {
637
+ assert.ok(err instanceof UserError);
638
+ assert.equal(err.code, "DEPLOY_FAILED");
639
+ assert.match(err.message, /not an approved managed surface/);
640
+ return true;
641
+ });
642
+ }
643
+ finally {
644
+ await rm(sourceDir, { recursive: true });
645
+ }
646
+ });
647
+ it("allows generic repo-scoped file targets", async () => {
648
+ const sourceDir = await makeTmpDir();
649
+ const manifest = {
650
+ skills: [],
651
+ files: [
652
+ {
653
+ name: "repo-file",
654
+ path: "present.txt",
655
+ target: "{repo}/.agents/rules/example.md",
656
+ agents: ["claude-code"],
657
+ },
658
+ ],
659
+ configs: [],
660
+ mcpServers: [],
661
+ agentRules: [],
662
+ permissions: [],
663
+ agentDefinitions: [],
664
+ };
665
+ try {
666
+ await writeFile(path.join(sourceDir, "present.txt"), "present");
667
+ const { actions } = await planDeploy(manifest, sourceDir, ["claude-code"], "/home/test");
668
+ assert.equal(actions.length, 1);
669
+ assertPathEndsWith(actions[0]?.target ?? "", path.join(".agents", "rules", "example.md"));
670
+ }
671
+ finally {
672
+ await rm(sourceDir, { recursive: true });
673
+ }
674
+ });
590
675
  it("ignores invalid agentRule sources for agents that are not being deployed", async () => {
591
676
  const sourceDir = await makeTmpDir();
592
677
  const manifest = {
@@ -871,418 +956,6 @@ describe("planDeploy", () => {
871
956
  }
872
957
  });
873
958
  });
874
- describe("executeDeploy", { skip: process.platform === "win32" }, () => {
875
- it("creates symlinks on POSIX", async () => {
876
- const sourceDir = await makeTmpDir();
877
- const home = await makeTmpDir();
878
- try {
879
- await createSkillSource(sourceDir, "skills/test-skill");
880
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
881
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
882
- assert.equal(succeeded, 1);
883
- assert.equal(failed.length, 0);
884
- const target = actions[0]?.target;
885
- assert.ok(await exists(target));
886
- assert.ok((await lstat(target)).isSymbolicLink());
887
- const action = actions[0];
888
- if (action?.kind !== "skill-dir") {
889
- assert.fail("Expected skill-dir action");
890
- }
891
- assert.equal(await readlink(target), action.source);
892
- }
893
- finally {
894
- await rm(sourceDir, { recursive: true });
895
- await rm(home, { recursive: true, force: true });
896
- }
897
- });
898
- it("registers deployment in registry on POSIX symlink deploy", async () => {
899
- const sourceDir = await makeTmpDir();
900
- const home = await makeTmpDir();
901
- try {
902
- const skillSource = await createSkillSource(sourceDir, "skills/test-skill");
903
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
904
- await executeDeploy(actions, false, false, home);
905
- const target = actions[0]?.target;
906
- // Registry should have the entry
907
- const entry = await lookupDeployment(home, target);
908
- assert.ok(entry);
909
- if (entry.kind !== "skill-dir")
910
- assert.fail("Expected skill-dir");
911
- assert.equal(entry.skill, "test-skill");
912
- assert.equal(entry.agent, "claude-code");
913
- assert.equal(entry.source, skillSource);
914
- assert.equal(entry.method, "symlink");
915
- // No .inception-totem should exist in source
916
- assert.ok(!(await exists(path.join(skillSource, ".inception-totem"))), "no .inception-totem should be written to source directory");
917
- }
918
- finally {
919
- await rm(sourceDir, { recursive: true });
920
- await rm(home, { recursive: true, force: true });
921
- }
922
- });
923
- it("does not create symlinks in dry-run mode", async () => {
924
- const sourceDir = await makeTmpDir();
925
- const home = await makeTmpDir();
926
- try {
927
- await createSkillSource(sourceDir, "skills/test-skill");
928
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
929
- const { succeeded, failed } = await executeDeploy(actions, true, false, home);
930
- assert.equal(succeeded, 1);
931
- assert.equal(failed.length, 0);
932
- const target = actions[0]?.target;
933
- assert.ok(!(await exists(target)));
934
- }
935
- finally {
936
- await rm(sourceDir, { recursive: true });
937
- await rm(home, { recursive: true, force: true });
938
- }
939
- });
940
- it("overwrites existing symlink (with registry entry)", async () => {
941
- const sourceDir = await makeTmpDir();
942
- const home = await makeTmpDir();
943
- try {
944
- await createSkillSource(sourceDir, "skills/test-skill");
945
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
946
- // Deploy twice - second should overwrite (first creates registry entry)
947
- await executeDeploy(actions, false, false, home);
948
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
949
- assert.equal(succeeded, 1);
950
- assert.equal(failed.length, 0);
951
- }
952
- finally {
953
- await rm(sourceDir, { recursive: true });
954
- await rm(home, { recursive: true, force: true });
955
- }
956
- });
957
- it("reports error for missing source (caught at planning)", async () => {
958
- const home = await makeTmpDir();
959
- try {
960
- await assert.rejects(planDeploy(testManifest, "/nonexistent/source", ["claude-code"], home), (err) => {
961
- assert.ok(err instanceof UserError);
962
- assert.equal(err.code, "DEPLOY_FAILED");
963
- assert.match(err.message, /source not found/);
964
- return true;
965
- });
966
- }
967
- finally {
968
- await rm(home, { recursive: true, force: true });
969
- }
970
- });
971
- it("reports permission denied when source is unreadable (caught at planning)", async () => {
972
- const sourceDir = await makeTmpDir();
973
- const home = await makeTmpDir();
974
- // Chmod the parent so traversal into the skill source dir fails with EACCES
975
- const skillsDir = path.join(sourceDir, "skills");
976
- try {
977
- await createSkillSource(sourceDir, "skills/test-skill");
978
- await chmod(skillsDir, 0o000);
979
- await assert.rejects(planDeploy(testManifest, sourceDir, ["claude-code"], home), (err) => {
980
- assert.ok(err instanceof UserError);
981
- assert.equal(err.code, "DEPLOY_FAILED");
982
- assert.match(err.message, /Permission denied/);
983
- return true;
984
- });
985
- }
986
- finally {
987
- await chmod(skillsDir, 0o755);
988
- await rm(sourceDir, { recursive: true });
989
- await rm(home, { recursive: true, force: true });
990
- }
991
- });
992
- it("reports permission denied when skill directory is not readable (execute-only)", {
993
- skip: process.platform === "win32" || process.getuid?.() === 0,
994
- }, async () => {
995
- const sourceDir = await makeTmpDir();
996
- const home = await makeTmpDir();
997
- const skillDir = path.join(sourceDir, "skills", "test-skill");
998
- try {
999
- await createSkillSource(sourceDir, "skills/test-skill");
1000
- // 0o111 = --x--x--x: directory exists and is traversable but not readable
1001
- await chmod(skillDir, 0o111);
1002
- await assert.rejects(planDeploy(testManifest, sourceDir, ["claude-code"], home), (err) => {
1003
- assert.ok(err instanceof UserError);
1004
- assert.equal(err.code, "DEPLOY_FAILED");
1005
- assert.match(err.message, /Permission denied reading skill directory/);
1006
- return true;
1007
- });
1008
- }
1009
- finally {
1010
- try {
1011
- await chmod(skillDir, 0o755);
1012
- }
1013
- catch {
1014
- /* best effort */
1015
- }
1016
- await rm(sourceDir, { recursive: true });
1017
- await rm(home, { recursive: true, force: true });
1018
- }
1019
- });
1020
- it("reports permission denied when SKILL.md is not readable", {
1021
- skip: process.platform === "win32" || process.getuid?.() === 0,
1022
- }, async () => {
1023
- const sourceDir = await makeTmpDir();
1024
- const home = await makeTmpDir();
1025
- const skillMdPath = path.join(sourceDir, "skills", "test-skill", "SKILL.md");
1026
- try {
1027
- await createSkillSource(sourceDir, "skills/test-skill");
1028
- await chmod(skillMdPath, 0o000);
1029
- await assert.rejects(planDeploy(testManifest, sourceDir, ["claude-code"], home), (err) => {
1030
- assert.ok(err instanceof UserError);
1031
- assert.equal(err.code, "DEPLOY_FAILED");
1032
- assert.match(err.message, /Permission denied reading SKILL\.md/);
1033
- return true;
1034
- });
1035
- }
1036
- finally {
1037
- try {
1038
- await chmod(skillMdPath, 0o644);
1039
- }
1040
- catch {
1041
- /* best effort */
1042
- }
1043
- await rm(sourceDir, { recursive: true });
1044
- await rm(home, { recursive: true, force: true });
1045
- }
1046
- });
1047
- it("refuses to overwrite unmanaged target", async () => {
1048
- const sourceDir = await makeTmpDir();
1049
- const home = await makeTmpDir();
1050
- try {
1051
- await createSkillSource(sourceDir, "skills/test-skill");
1052
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1053
- const target = actions[0]?.target;
1054
- // Create an unmanaged directory at the target (no registry entry)
1055
- await mkdir(target, { recursive: true });
1056
- await writeFile(path.join(target, "something.txt"), "user content");
1057
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1058
- assert.equal(succeeded, 0);
1059
- assert.equal(failed.length, 1);
1060
- assert.ok(failed[0]?.error.includes("not managed by inception-engine"));
1061
- // Original content should still be there
1062
- assert.ok(await exists(path.join(target, "something.txt")));
1063
- }
1064
- finally {
1065
- await rm(sourceDir, { recursive: true });
1066
- await rm(home, { recursive: true, force: true });
1067
- }
1068
- });
1069
- it("refuses to overwrite target with mismatched registry entry", async () => {
1070
- const sourceDir = await makeTmpDir();
1071
- const home = await makeTmpDir();
1072
- try {
1073
- await createSkillSource(sourceDir, "skills/test-skill");
1074
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1075
- const target = actions[0]?.target;
1076
- // Create a directory at the target and register it under a different source/skill
1077
- await mkdir(target, { recursive: true });
1078
- await writeFile(path.join(target, "SKILL.md"), "other content");
1079
- await registerDeployment(home, target, {
1080
- kind: "skill-dir",
1081
- source: "/completely/different/source",
1082
- skill: "different-skill",
1083
- agent: "codex",
1084
- method: "symlink",
1085
- });
1086
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1087
- assert.equal(succeeded, 0);
1088
- assert.equal(failed.length, 1);
1089
- assert.ok(failed[0]?.error.includes("not managed by inception-engine"));
1090
- // Original content should still be there
1091
- assert.ok(await exists(path.join(target, "SKILL.md")));
1092
- }
1093
- finally {
1094
- await rm(sourceDir, { recursive: true });
1095
- await rm(home, { recursive: true, force: true });
1096
- }
1097
- });
1098
- it("backup is removed after successful redeploy", async () => {
1099
- const sourceDir = await makeTmpDir();
1100
- const home = await makeTmpDir();
1101
- try {
1102
- await createSkillSource(sourceDir, "skills/test-skill");
1103
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1104
- const target = actions[0]?.target;
1105
- const backupPath = `${target}.inception-backup`;
1106
- await executeDeploy(actions, false, false, home);
1107
- await executeDeploy(actions, false, false, home);
1108
- assert.ok(!(await exists(backupPath)), "backup should not exist after successful redeploy");
1109
- assert.ok(await exists(target), "target should exist after redeploy");
1110
- }
1111
- finally {
1112
- await rm(sourceDir, { recursive: true });
1113
- await rm(home, { recursive: true, force: true });
1114
- }
1115
- });
1116
- });
1117
- describe("atomic redeploy behavior", {
1118
- skip: process.platform === "win32",
1119
- }, () => {
1120
- it("cleans up stale .inception-backup from a previous failed attempt", async () => {
1121
- const sourceDir = await makeTmpDir();
1122
- const home = await makeTmpDir();
1123
- try {
1124
- await createSkillSource(sourceDir, "skills/test-skill");
1125
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1126
- const target = actions[0]?.target;
1127
- const backupPath = `${target}.inception-backup`;
1128
- // First deploy to establish managed target
1129
- await executeDeploy(actions, false, false, home);
1130
- // Simulate a stale backup left by a previous crash
1131
- await mkdir(backupPath, { recursive: true });
1132
- await writeFile(path.join(backupPath, "stale.txt"), "leftover");
1133
- // Redeploy should clean up the stale backup and succeed
1134
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1135
- assert.equal(succeeded, 1);
1136
- assert.equal(failed.length, 0);
1137
- assert.ok(!(await exists(backupPath)), "stale backup should be cleaned up");
1138
- assert.ok(await exists(target), "target should exist after redeploy");
1139
- }
1140
- finally {
1141
- await rm(sourceDir, { recursive: true });
1142
- await rm(home, { recursive: true, force: true });
1143
- }
1144
- });
1145
- it("no backup is created on first deploy (no prior target)", async () => {
1146
- const sourceDir = await makeTmpDir();
1147
- const home = await makeTmpDir();
1148
- try {
1149
- await createSkillSource(sourceDir, "skills/test-skill");
1150
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1151
- const target = actions[0]?.target;
1152
- const backupPath = `${target}.inception-backup`;
1153
- const { succeeded } = await executeDeploy(actions, false, false, home);
1154
- assert.equal(succeeded, 1);
1155
- assert.ok(await exists(target));
1156
- assert.ok(!(await exists(backupPath)), "no backup should be created when there was no prior target");
1157
- }
1158
- finally {
1159
- await rm(sourceDir, { recursive: true });
1160
- await rm(home, { recursive: true, force: true });
1161
- }
1162
- });
1163
- it("restores backup when registry write fails (registry file read-only)", async () => {
1164
- const sourceDir = await makeTmpDir();
1165
- const home = await makeTmpDir();
1166
- const registryFile = path.join(home, ".inception-engine", "registry.json");
1167
- try {
1168
- await createSkillSource(sourceDir, "skills/test-skill");
1169
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1170
- const target = actions[0]?.target;
1171
- const backupPath = `${target}.inception-backup`;
1172
- // First deploy: establishes managed symlink and registry entry
1173
- const { succeeded: firstSucceeded } = await executeDeploy(actions, false, false, home);
1174
- assert.equal(firstSucceeded, 1);
1175
- const originalLink = await readlink(target);
1176
- // Make registry file read-only so registerDeployment fails on the next attempt.
1177
- // The lookupDeployment (read) still works; only the write will throw EACCES.
1178
- await chmod(registryFile, 0o444);
1179
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1180
- // Deploy must report failure
1181
- assert.equal(succeeded, 0);
1182
- assert.equal(failed.length, 1);
1183
- // Backup must be cleaned up (restored back to target)
1184
- assert.ok(!(await exists(backupPath)), "backup should be gone after rollback");
1185
- // Original managed symlink must be restored at the target path
1186
- assert.ok(await exists(target), "original symlink must be restored");
1187
- assert.ok((await lstat(target)).isSymbolicLink(), "restored target must be a symlink");
1188
- assert.equal(await readlink(target), originalLink, "restored symlink must point to original source");
1189
- }
1190
- finally {
1191
- try {
1192
- await chmod(registryFile, 0o644);
1193
- }
1194
- catch {
1195
- /* best effort */
1196
- }
1197
- await rm(sourceDir, { recursive: true, force: true });
1198
- await rm(home, { recursive: true, force: true });
1199
- }
1200
- });
1201
- it("restores backup when cp fails (source not readable)", async () => {
1202
- const sourceDir = await makeTmpDir();
1203
- const home = await makeTmpDir();
1204
- try {
1205
- // First deploy using copy method to establish a managed target directory
1206
- const skillSource = await createSkillSource(sourceDir, "skills/test-skill");
1207
- const target = path.join(home, ".claude", "skills", "test-skill");
1208
- await mkdir(path.dirname(target), { recursive: true });
1209
- const firstAction = {
1210
- kind: "skill-dir",
1211
- skill: "test-skill",
1212
- agent: "claude-code",
1213
- source: skillSource,
1214
- target,
1215
- method: "copy",
1216
- confidence: "documented",
1217
- };
1218
- const { succeeded: firstSucceeded } = await executeDeploy([firstAction], false, false, home);
1219
- assert.equal(firstSucceeded, 1);
1220
- assert.ok(await exists(target));
1221
- // Create an unreadable source directory for the next deploy attempt
1222
- // access() (F_OK) passes — the dir exists — but cp() fails reading its contents
1223
- const unreadableSource = path.join(sourceDir, "unreadable-source");
1224
- await mkdir(unreadableSource, { recursive: true });
1225
- await writeFile(path.join(unreadableSource, "SKILL.md"), "---");
1226
- await chmod(unreadableSource, 0o000);
1227
- const backupPath = `${target}.inception-backup`;
1228
- const failAction = {
1229
- kind: "skill-dir",
1230
- skill: "test-skill",
1231
- agent: "claude-code",
1232
- source: unreadableSource,
1233
- target,
1234
- method: "copy",
1235
- confidence: "documented",
1236
- };
1237
- const { succeeded, failed } = await executeDeploy([failAction], false, false, home);
1238
- // Deploy must report failure
1239
- assert.equal(succeeded, 0);
1240
- assert.equal(failed.length, 1);
1241
- // Backup must be cleaned up (restored back to target)
1242
- assert.ok(!(await exists(backupPath)), "backup should be gone after rollback");
1243
- // Original managed directory must be restored with its content intact
1244
- assert.ok(await exists(target), "original target must be restored");
1245
- assert.ok(await exists(path.join(target, "SKILL.md")), "restored target must have original content");
1246
- }
1247
- finally {
1248
- try {
1249
- await chmod(path.join(sourceDir, "unreadable-source"), 0o755);
1250
- }
1251
- catch {
1252
- /* best effort */
1253
- }
1254
- await rm(sourceDir, { recursive: true, force: true });
1255
- await rm(home, { recursive: true, force: true });
1256
- }
1257
- });
1258
- it("rename atomically replaces stale .inception-backup — no stale content leaks into target", async () => {
1259
- const sourceDir = await makeTmpDir();
1260
- const home = await makeTmpDir();
1261
- try {
1262
- await createSkillSource(sourceDir, "skills/test-skill");
1263
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1264
- const target = actions[0]?.target;
1265
- const backupPath = `${target}.inception-backup`;
1266
- // First deploy to establish a managed target
1267
- await executeDeploy(actions, false, false, home);
1268
- // Simulate a stale backup containing sentinel content
1269
- await mkdir(backupPath, { recursive: true });
1270
- await writeFile(path.join(backupPath, "stale.txt"), "leftover content");
1271
- // Redeploy: rename(target, backupPath) atomically replaces the stale dir
1272
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1273
- assert.equal(succeeded, 1);
1274
- assert.equal(failed.length, 0);
1275
- assert.ok(await exists(target), "target must exist after redeploy");
1276
- assert.ok(!(await exists(backupPath)), "stale backup must be cleaned up");
1277
- // The stale sentinel file must not appear in the new target
1278
- assert.ok(!(await exists(path.join(target, "stale.txt"))), "stale content must not leak into the new target");
1279
- }
1280
- finally {
1281
- await rm(sourceDir, { recursive: true });
1282
- await rm(home, { recursive: true, force: true });
1283
- }
1284
- });
1285
- });
1286
959
  describe("planDeploy path traversal", () => {
1287
960
  it("throws when skill.path resolves to the repository root itself (.)", async () => {
1288
961
  const sourceDir = await makeTmpDir();
@@ -1458,238 +1131,6 @@ describe("source directory immutability", () => {
1458
1131
  }
1459
1132
  });
1460
1133
  });
1461
- describe("executeDeploy (Windows)", {
1462
- skip: process.platform !== "win32",
1463
- }, () => {
1464
- it("creates copies on Windows", async () => {
1465
- const sourceDir = await makeTmpDir();
1466
- const home = await makeTmpDir();
1467
- try {
1468
- await createSkillSource(sourceDir, "skills/test-skill");
1469
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1470
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1471
- assert.equal(succeeded, 1);
1472
- assert.equal(failed.length, 0);
1473
- const target = actions[0]?.target;
1474
- assert.ok(await exists(target));
1475
- assert.ok(!(await lstat(target)).isSymbolicLink());
1476
- assert.ok(await exists(path.join(target, "SKILL.md")));
1477
- }
1478
- finally {
1479
- await rm(sourceDir, { recursive: true, force: true });
1480
- await rm(home, { recursive: true, force: true });
1481
- }
1482
- });
1483
- it("registers deployment in registry on Windows copy deploy", async () => {
1484
- const sourceDir = await makeTmpDir();
1485
- const home = await makeTmpDir();
1486
- try {
1487
- const skillSource = await createSkillSource(sourceDir, "skills/test-skill");
1488
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1489
- await executeDeploy(actions, false, false, home);
1490
- const target = actions[0]?.target;
1491
- const entry = await lookupDeployment(home, target);
1492
- assert.ok(entry);
1493
- if (entry.kind !== "skill-dir")
1494
- assert.fail("Expected skill-dir");
1495
- assert.equal(entry.skill, "test-skill");
1496
- assert.equal(entry.agent, "claude-code");
1497
- assert.equal(entry.source, skillSource);
1498
- assert.equal(entry.method, "copy");
1499
- assert.ok(!(await exists(path.join(skillSource, ".inception-totem"))));
1500
- }
1501
- finally {
1502
- await rm(sourceDir, { recursive: true, force: true });
1503
- await rm(home, { recursive: true, force: true });
1504
- }
1505
- });
1506
- it("does not create copies in dry-run mode on Windows", async () => {
1507
- const sourceDir = await makeTmpDir();
1508
- const home = await makeTmpDir();
1509
- try {
1510
- await createSkillSource(sourceDir, "skills/test-skill");
1511
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1512
- const { succeeded, failed } = await executeDeploy(actions, true, false, home);
1513
- assert.equal(succeeded, 1);
1514
- assert.equal(failed.length, 0);
1515
- const target = actions[0]?.target;
1516
- assert.ok(!(await exists(target)));
1517
- }
1518
- finally {
1519
- await rm(sourceDir, { recursive: true, force: true });
1520
- await rm(home, { recursive: true, force: true });
1521
- }
1522
- });
1523
- it("overwrites existing copy (with registry entry) on Windows", async () => {
1524
- const sourceDir = await makeTmpDir();
1525
- const home = await makeTmpDir();
1526
- try {
1527
- await createSkillSource(sourceDir, "skills/test-skill");
1528
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1529
- await executeDeploy(actions, false, false, home);
1530
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1531
- assert.equal(succeeded, 1);
1532
- assert.equal(failed.length, 0);
1533
- }
1534
- finally {
1535
- await rm(sourceDir, { recursive: true, force: true });
1536
- await rm(home, { recursive: true, force: true });
1537
- }
1538
- });
1539
- it("reports error for missing source (caught at planning)", async () => {
1540
- const home = await makeTmpDir();
1541
- try {
1542
- await assert.rejects(planDeploy(testManifest, "/nonexistent/source", ["claude-code"], home), (err) => {
1543
- assert.ok(err instanceof UserError);
1544
- assert.equal(err.code, "DEPLOY_FAILED");
1545
- assert.match(err.message, /source not found/);
1546
- return true;
1547
- });
1548
- }
1549
- finally {
1550
- await rm(home, { recursive: true, force: true });
1551
- }
1552
- });
1553
- it("refuses to overwrite unmanaged target on Windows", async () => {
1554
- const sourceDir = await makeTmpDir();
1555
- const home = await makeTmpDir();
1556
- try {
1557
- await createSkillSource(sourceDir, "skills/test-skill");
1558
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1559
- const target = actions[0]?.target;
1560
- await mkdir(target, { recursive: true });
1561
- await writeFile(path.join(target, "something.txt"), "user content");
1562
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1563
- assert.equal(succeeded, 0);
1564
- assert.equal(failed.length, 1);
1565
- assert.ok(failed[0]?.error.includes("not managed by inception-engine"));
1566
- assert.ok(await exists(path.join(target, "something.txt")));
1567
- }
1568
- finally {
1569
- await rm(sourceDir, { recursive: true, force: true });
1570
- await rm(home, { recursive: true, force: true });
1571
- }
1572
- });
1573
- it("refuses to overwrite target with mismatched registry entry on Windows", async () => {
1574
- const sourceDir = await makeTmpDir();
1575
- const home = await makeTmpDir();
1576
- try {
1577
- await createSkillSource(sourceDir, "skills/test-skill");
1578
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1579
- const target = actions[0]?.target;
1580
- await mkdir(target, { recursive: true });
1581
- await writeFile(path.join(target, "SKILL.md"), "other content");
1582
- await registerDeployment(home, target, {
1583
- kind: "skill-dir",
1584
- source: "/completely/different/source",
1585
- skill: "different-skill",
1586
- agent: "codex",
1587
- method: "copy",
1588
- });
1589
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1590
- assert.equal(succeeded, 0);
1591
- assert.equal(failed.length, 1);
1592
- assert.ok(failed[0]?.error.includes("not managed by inception-engine"));
1593
- assert.ok(await exists(path.join(target, "SKILL.md")));
1594
- }
1595
- finally {
1596
- await rm(sourceDir, { recursive: true, force: true });
1597
- await rm(home, { recursive: true, force: true });
1598
- }
1599
- });
1600
- it("backup is removed after successful redeploy on Windows", async () => {
1601
- const sourceDir = await makeTmpDir();
1602
- const home = await makeTmpDir();
1603
- try {
1604
- await createSkillSource(sourceDir, "skills/test-skill");
1605
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1606
- const target = actions[0]?.target;
1607
- const backupPath = `${target}.inception-backup`;
1608
- await executeDeploy(actions, false, false, home);
1609
- await executeDeploy(actions, false, false, home);
1610
- assert.ok(!(await exists(backupPath)), "backup should not exist after successful redeploy");
1611
- assert.ok(await exists(target), "target should exist after redeploy");
1612
- }
1613
- finally {
1614
- await rm(sourceDir, { recursive: true, force: true });
1615
- await rm(home, { recursive: true, force: true });
1616
- }
1617
- });
1618
- });
1619
- describe("atomic redeploy behavior (Windows)", {
1620
- skip: process.platform !== "win32",
1621
- }, () => {
1622
- it("cleans up stale .inception-backup from a previous failed attempt on Windows", async () => {
1623
- const sourceDir = await makeTmpDir();
1624
- const home = await makeTmpDir();
1625
- try {
1626
- await createSkillSource(sourceDir, "skills/test-skill");
1627
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1628
- const target = actions[0]?.target;
1629
- const backupPath = `${target}.inception-backup`;
1630
- await executeDeploy(actions, false, false, home);
1631
- await mkdir(backupPath, { recursive: true });
1632
- await writeFile(path.join(backupPath, "stale.txt"), "leftover");
1633
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1634
- assert.equal(succeeded, 1);
1635
- assert.equal(failed.length, 0);
1636
- assert.ok(!(await exists(backupPath)), "stale backup should be cleaned up");
1637
- assert.ok(await exists(target), "target should exist after redeploy");
1638
- }
1639
- finally {
1640
- await rm(sourceDir, { recursive: true, force: true });
1641
- await rm(home, { recursive: true, force: true });
1642
- }
1643
- });
1644
- it("no backup is created on first deploy (no prior target) on Windows", async () => {
1645
- const sourceDir = await makeTmpDir();
1646
- const home = await makeTmpDir();
1647
- try {
1648
- await createSkillSource(sourceDir, "skills/test-skill");
1649
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1650
- const target = actions[0]?.target;
1651
- const backupPath = `${target}.inception-backup`;
1652
- const { succeeded } = await executeDeploy(actions, false, false, home);
1653
- assert.equal(succeeded, 1);
1654
- assert.ok(await exists(target));
1655
- assert.ok(!(await exists(backupPath)), "no backup should be created when there was no prior target");
1656
- }
1657
- finally {
1658
- await rm(sourceDir, { recursive: true, force: true });
1659
- await rm(home, { recursive: true, force: true });
1660
- }
1661
- });
1662
- it("restores backup when deploy fails (registry not writable) on Windows", async () => {
1663
- const sourceDir = await makeTmpDir();
1664
- const home = await makeTmpDir();
1665
- try {
1666
- await createSkillSource(sourceDir, "skills/test-skill");
1667
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1668
- const target = actions[0]?.target;
1669
- const backupPath = `${target}.inception-backup`;
1670
- const { succeeded: firstSucceeded } = await executeDeploy(actions, false, false, home);
1671
- assert.equal(firstSucceeded, 1);
1672
- const registryFile = path.join(home, ".inception-engine", "registry.json");
1673
- await chmod(registryFile, 0o444); // trigger EPERM on Windows
1674
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1675
- assert.equal(succeeded, 0);
1676
- assert.equal(failed.length, 1);
1677
- assert.ok(!(await exists(backupPath)), "backup should be gone after rollback");
1678
- assert.ok(await exists(target), "original target must be restored");
1679
- assert.ok(await exists(path.join(target, "SKILL.md")), "original content must be present");
1680
- }
1681
- finally {
1682
- try {
1683
- await chmod(path.join(home, ".inception-engine", "registry.json"), 0o666);
1684
- }
1685
- catch {
1686
- /* best effort */
1687
- }
1688
- await rm(sourceDir, { recursive: true, force: true });
1689
- await rm(home, { recursive: true, force: true });
1690
- }
1691
- });
1692
- });
1693
1134
  describe("executeDeploy — file-write", () => {
1694
1135
  it("copies source file to target", async () => {
1695
1136
  const sourceDir = await makeTmpDir();