@kuznai/inception-engine 0.23.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.
@@ -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";
@@ -587,13 +587,13 @@ describe("planDeploy", () => {
587
587
  {
588
588
  name: "codex-file",
589
589
  path: "missing.txt",
590
- target: "{home}/missing.txt",
590
+ target: "{home}/.codex/AGENTS.md",
591
591
  agents: ["codex"],
592
592
  },
593
593
  {
594
594
  name: "claude-file",
595
595
  path: "present.txt",
596
- target: "{home}/present.txt",
596
+ target: "{home}/.claude/CLAUDE.md",
597
597
  agents: ["claude-code"],
598
598
  },
599
599
  ],
@@ -613,6 +613,65 @@ describe("planDeploy", () => {
613
613
  await rm(sourceDir, { recursive: true });
614
614
  }
615
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
+ });
616
675
  it("ignores invalid agentRule sources for agents that are not being deployed", async () => {
617
676
  const sourceDir = await makeTmpDir();
618
677
  const manifest = {
@@ -897,418 +956,6 @@ describe("planDeploy", () => {
897
956
  }
898
957
  });
899
958
  });
900
- describe("executeDeploy", { skip: process.platform === "win32" }, () => {
901
- it("creates symlinks on POSIX", async () => {
902
- const sourceDir = await makeTmpDir();
903
- const home = await makeTmpDir();
904
- try {
905
- await createSkillSource(sourceDir, "skills/test-skill");
906
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
907
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
908
- assert.equal(succeeded, 1);
909
- assert.equal(failed.length, 0);
910
- const target = actions[0]?.target;
911
- assert.ok(await exists(target));
912
- assert.ok((await lstat(target)).isSymbolicLink());
913
- const action = actions[0];
914
- if (action?.kind !== "skill-dir") {
915
- assert.fail("Expected skill-dir action");
916
- }
917
- assert.equal(await readlink(target), action.source);
918
- }
919
- finally {
920
- await rm(sourceDir, { recursive: true });
921
- await rm(home, { recursive: true, force: true });
922
- }
923
- });
924
- it("registers deployment in registry on POSIX symlink deploy", async () => {
925
- const sourceDir = await makeTmpDir();
926
- const home = await makeTmpDir();
927
- try {
928
- const skillSource = await createSkillSource(sourceDir, "skills/test-skill");
929
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
930
- await executeDeploy(actions, false, false, home);
931
- const target = actions[0]?.target;
932
- // Registry should have the entry
933
- const entry = await lookupDeployment(home, target);
934
- assert.ok(entry);
935
- if (entry.kind !== "skill-dir")
936
- assert.fail("Expected skill-dir");
937
- assert.equal(entry.skill, "test-skill");
938
- assert.equal(entry.agent, "claude-code");
939
- assert.equal(entry.source, skillSource);
940
- assert.equal(entry.method, "symlink");
941
- // No .inception-totem should exist in source
942
- assert.ok(!(await exists(path.join(skillSource, ".inception-totem"))), "no .inception-totem should be written to source directory");
943
- }
944
- finally {
945
- await rm(sourceDir, { recursive: true });
946
- await rm(home, { recursive: true, force: true });
947
- }
948
- });
949
- it("does not create symlinks in dry-run mode", async () => {
950
- const sourceDir = await makeTmpDir();
951
- const home = await makeTmpDir();
952
- try {
953
- await createSkillSource(sourceDir, "skills/test-skill");
954
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
955
- const { succeeded, failed } = await executeDeploy(actions, true, false, home);
956
- assert.equal(succeeded, 1);
957
- assert.equal(failed.length, 0);
958
- const target = actions[0]?.target;
959
- assert.ok(!(await exists(target)));
960
- }
961
- finally {
962
- await rm(sourceDir, { recursive: true });
963
- await rm(home, { recursive: true, force: true });
964
- }
965
- });
966
- it("overwrites existing symlink (with registry entry)", async () => {
967
- const sourceDir = await makeTmpDir();
968
- const home = await makeTmpDir();
969
- try {
970
- await createSkillSource(sourceDir, "skills/test-skill");
971
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
972
- // Deploy twice - second should overwrite (first creates registry entry)
973
- await executeDeploy(actions, false, false, home);
974
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
975
- assert.equal(succeeded, 1);
976
- assert.equal(failed.length, 0);
977
- }
978
- finally {
979
- await rm(sourceDir, { recursive: true });
980
- await rm(home, { recursive: true, force: true });
981
- }
982
- });
983
- it("reports error for missing source (caught at planning)", async () => {
984
- const home = await makeTmpDir();
985
- try {
986
- await assert.rejects(planDeploy(testManifest, "/nonexistent/source", ["claude-code"], home), (err) => {
987
- assert.ok(err instanceof UserError);
988
- assert.equal(err.code, "DEPLOY_FAILED");
989
- assert.match(err.message, /source not found/);
990
- return true;
991
- });
992
- }
993
- finally {
994
- await rm(home, { recursive: true, force: true });
995
- }
996
- });
997
- it("reports permission denied when source is unreadable (caught at planning)", async () => {
998
- const sourceDir = await makeTmpDir();
999
- const home = await makeTmpDir();
1000
- // Chmod the parent so traversal into the skill source dir fails with EACCES
1001
- const skillsDir = path.join(sourceDir, "skills");
1002
- try {
1003
- await createSkillSource(sourceDir, "skills/test-skill");
1004
- await chmod(skillsDir, 0o000);
1005
- await assert.rejects(planDeploy(testManifest, sourceDir, ["claude-code"], home), (err) => {
1006
- assert.ok(err instanceof UserError);
1007
- assert.equal(err.code, "DEPLOY_FAILED");
1008
- assert.match(err.message, /Permission denied/);
1009
- return true;
1010
- });
1011
- }
1012
- finally {
1013
- await chmod(skillsDir, 0o755);
1014
- await rm(sourceDir, { recursive: true });
1015
- await rm(home, { recursive: true, force: true });
1016
- }
1017
- });
1018
- it("reports permission denied when skill directory is not readable (execute-only)", {
1019
- skip: process.platform === "win32" || process.getuid?.() === 0,
1020
- }, async () => {
1021
- const sourceDir = await makeTmpDir();
1022
- const home = await makeTmpDir();
1023
- const skillDir = path.join(sourceDir, "skills", "test-skill");
1024
- try {
1025
- await createSkillSource(sourceDir, "skills/test-skill");
1026
- // 0o111 = --x--x--x: directory exists and is traversable but not readable
1027
- await chmod(skillDir, 0o111);
1028
- await assert.rejects(planDeploy(testManifest, sourceDir, ["claude-code"], home), (err) => {
1029
- assert.ok(err instanceof UserError);
1030
- assert.equal(err.code, "DEPLOY_FAILED");
1031
- assert.match(err.message, /Permission denied reading skill directory/);
1032
- return true;
1033
- });
1034
- }
1035
- finally {
1036
- try {
1037
- await chmod(skillDir, 0o755);
1038
- }
1039
- catch {
1040
- /* best effort */
1041
- }
1042
- await rm(sourceDir, { recursive: true });
1043
- await rm(home, { recursive: true, force: true });
1044
- }
1045
- });
1046
- it("reports permission denied when SKILL.md is not readable", {
1047
- skip: process.platform === "win32" || process.getuid?.() === 0,
1048
- }, async () => {
1049
- const sourceDir = await makeTmpDir();
1050
- const home = await makeTmpDir();
1051
- const skillMdPath = path.join(sourceDir, "skills", "test-skill", "SKILL.md");
1052
- try {
1053
- await createSkillSource(sourceDir, "skills/test-skill");
1054
- await chmod(skillMdPath, 0o000);
1055
- await assert.rejects(planDeploy(testManifest, sourceDir, ["claude-code"], home), (err) => {
1056
- assert.ok(err instanceof UserError);
1057
- assert.equal(err.code, "DEPLOY_FAILED");
1058
- assert.match(err.message, /Permission denied reading SKILL\.md/);
1059
- return true;
1060
- });
1061
- }
1062
- finally {
1063
- try {
1064
- await chmod(skillMdPath, 0o644);
1065
- }
1066
- catch {
1067
- /* best effort */
1068
- }
1069
- await rm(sourceDir, { recursive: true });
1070
- await rm(home, { recursive: true, force: true });
1071
- }
1072
- });
1073
- it("refuses to overwrite unmanaged target", async () => {
1074
- const sourceDir = await makeTmpDir();
1075
- const home = await makeTmpDir();
1076
- try {
1077
- await createSkillSource(sourceDir, "skills/test-skill");
1078
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1079
- const target = actions[0]?.target;
1080
- // Create an unmanaged directory at the target (no registry entry)
1081
- await mkdir(target, { recursive: true });
1082
- await writeFile(path.join(target, "something.txt"), "user content");
1083
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1084
- assert.equal(succeeded, 0);
1085
- assert.equal(failed.length, 1);
1086
- assert.ok(failed[0]?.error.includes("not managed by inception-engine"));
1087
- // Original content should still be there
1088
- assert.ok(await exists(path.join(target, "something.txt")));
1089
- }
1090
- finally {
1091
- await rm(sourceDir, { recursive: true });
1092
- await rm(home, { recursive: true, force: true });
1093
- }
1094
- });
1095
- it("refuses to overwrite target with mismatched registry entry", async () => {
1096
- const sourceDir = await makeTmpDir();
1097
- const home = await makeTmpDir();
1098
- try {
1099
- await createSkillSource(sourceDir, "skills/test-skill");
1100
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1101
- const target = actions[0]?.target;
1102
- // Create a directory at the target and register it under a different source/skill
1103
- await mkdir(target, { recursive: true });
1104
- await writeFile(path.join(target, "SKILL.md"), "other content");
1105
- await registerDeployment(home, target, {
1106
- kind: "skill-dir",
1107
- source: "/completely/different/source",
1108
- skill: "different-skill",
1109
- agent: "codex",
1110
- method: "symlink",
1111
- });
1112
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1113
- assert.equal(succeeded, 0);
1114
- assert.equal(failed.length, 1);
1115
- assert.ok(failed[0]?.error.includes("not managed by inception-engine"));
1116
- // Original content should still be there
1117
- assert.ok(await exists(path.join(target, "SKILL.md")));
1118
- }
1119
- finally {
1120
- await rm(sourceDir, { recursive: true });
1121
- await rm(home, { recursive: true, force: true });
1122
- }
1123
- });
1124
- it("backup is removed after successful redeploy", async () => {
1125
- const sourceDir = await makeTmpDir();
1126
- const home = await makeTmpDir();
1127
- try {
1128
- await createSkillSource(sourceDir, "skills/test-skill");
1129
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1130
- const target = actions[0]?.target;
1131
- const backupPath = `${target}.inception-backup`;
1132
- await executeDeploy(actions, false, false, home);
1133
- await executeDeploy(actions, false, false, home);
1134
- assert.ok(!(await exists(backupPath)), "backup should not exist after successful redeploy");
1135
- assert.ok(await exists(target), "target should exist after redeploy");
1136
- }
1137
- finally {
1138
- await rm(sourceDir, { recursive: true });
1139
- await rm(home, { recursive: true, force: true });
1140
- }
1141
- });
1142
- });
1143
- describe("atomic redeploy behavior", {
1144
- skip: process.platform === "win32",
1145
- }, () => {
1146
- it("cleans up stale .inception-backup from a previous failed attempt", async () => {
1147
- const sourceDir = await makeTmpDir();
1148
- const home = await makeTmpDir();
1149
- try {
1150
- await createSkillSource(sourceDir, "skills/test-skill");
1151
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1152
- const target = actions[0]?.target;
1153
- const backupPath = `${target}.inception-backup`;
1154
- // First deploy to establish managed target
1155
- await executeDeploy(actions, false, false, home);
1156
- // Simulate a stale backup left by a previous crash
1157
- await mkdir(backupPath, { recursive: true });
1158
- await writeFile(path.join(backupPath, "stale.txt"), "leftover");
1159
- // Redeploy should clean up the stale backup and succeed
1160
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1161
- assert.equal(succeeded, 1);
1162
- assert.equal(failed.length, 0);
1163
- assert.ok(!(await exists(backupPath)), "stale backup should be cleaned up");
1164
- assert.ok(await exists(target), "target should exist after redeploy");
1165
- }
1166
- finally {
1167
- await rm(sourceDir, { recursive: true });
1168
- await rm(home, { recursive: true, force: true });
1169
- }
1170
- });
1171
- it("no backup is created on first deploy (no prior target)", async () => {
1172
- const sourceDir = await makeTmpDir();
1173
- const home = await makeTmpDir();
1174
- try {
1175
- await createSkillSource(sourceDir, "skills/test-skill");
1176
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1177
- const target = actions[0]?.target;
1178
- const backupPath = `${target}.inception-backup`;
1179
- const { succeeded } = await executeDeploy(actions, false, false, home);
1180
- assert.equal(succeeded, 1);
1181
- assert.ok(await exists(target));
1182
- assert.ok(!(await exists(backupPath)), "no backup should be created when there was no prior target");
1183
- }
1184
- finally {
1185
- await rm(sourceDir, { recursive: true });
1186
- await rm(home, { recursive: true, force: true });
1187
- }
1188
- });
1189
- it("restores backup when registry write fails (registry file read-only)", async () => {
1190
- const sourceDir = await makeTmpDir();
1191
- const home = await makeTmpDir();
1192
- const registryFile = path.join(home, ".inception-engine", "registry.json");
1193
- try {
1194
- await createSkillSource(sourceDir, "skills/test-skill");
1195
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1196
- const target = actions[0]?.target;
1197
- const backupPath = `${target}.inception-backup`;
1198
- // First deploy: establishes managed symlink and registry entry
1199
- const { succeeded: firstSucceeded } = await executeDeploy(actions, false, false, home);
1200
- assert.equal(firstSucceeded, 1);
1201
- const originalLink = await readlink(target);
1202
- // Make registry file read-only so registerDeployment fails on the next attempt.
1203
- // The lookupDeployment (read) still works; only the write will throw EACCES.
1204
- await chmod(registryFile, 0o444);
1205
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1206
- // Deploy must report failure
1207
- assert.equal(succeeded, 0);
1208
- assert.equal(failed.length, 1);
1209
- // Backup must be cleaned up (restored back to target)
1210
- assert.ok(!(await exists(backupPath)), "backup should be gone after rollback");
1211
- // Original managed symlink must be restored at the target path
1212
- assert.ok(await exists(target), "original symlink must be restored");
1213
- assert.ok((await lstat(target)).isSymbolicLink(), "restored target must be a symlink");
1214
- assert.equal(await readlink(target), originalLink, "restored symlink must point to original source");
1215
- }
1216
- finally {
1217
- try {
1218
- await chmod(registryFile, 0o644);
1219
- }
1220
- catch {
1221
- /* best effort */
1222
- }
1223
- await rm(sourceDir, { recursive: true, force: true });
1224
- await rm(home, { recursive: true, force: true });
1225
- }
1226
- });
1227
- it("restores backup when cp fails (source not readable)", async () => {
1228
- const sourceDir = await makeTmpDir();
1229
- const home = await makeTmpDir();
1230
- try {
1231
- // First deploy using copy method to establish a managed target directory
1232
- const skillSource = await createSkillSource(sourceDir, "skills/test-skill");
1233
- const target = path.join(home, ".claude", "skills", "test-skill");
1234
- await mkdir(path.dirname(target), { recursive: true });
1235
- const firstAction = {
1236
- kind: "skill-dir",
1237
- skill: "test-skill",
1238
- agent: "claude-code",
1239
- source: skillSource,
1240
- target,
1241
- method: "copy",
1242
- confidence: "documented",
1243
- };
1244
- const { succeeded: firstSucceeded } = await executeDeploy([firstAction], false, false, home);
1245
- assert.equal(firstSucceeded, 1);
1246
- assert.ok(await exists(target));
1247
- // Create an unreadable source directory for the next deploy attempt
1248
- // access() (F_OK) passes — the dir exists — but cp() fails reading its contents
1249
- const unreadableSource = path.join(sourceDir, "unreadable-source");
1250
- await mkdir(unreadableSource, { recursive: true });
1251
- await writeFile(path.join(unreadableSource, "SKILL.md"), "---");
1252
- await chmod(unreadableSource, 0o000);
1253
- const backupPath = `${target}.inception-backup`;
1254
- const failAction = {
1255
- kind: "skill-dir",
1256
- skill: "test-skill",
1257
- agent: "claude-code",
1258
- source: unreadableSource,
1259
- target,
1260
- method: "copy",
1261
- confidence: "documented",
1262
- };
1263
- const { succeeded, failed } = await executeDeploy([failAction], false, false, home);
1264
- // Deploy must report failure
1265
- assert.equal(succeeded, 0);
1266
- assert.equal(failed.length, 1);
1267
- // Backup must be cleaned up (restored back to target)
1268
- assert.ok(!(await exists(backupPath)), "backup should be gone after rollback");
1269
- // Original managed directory must be restored with its content intact
1270
- assert.ok(await exists(target), "original target must be restored");
1271
- assert.ok(await exists(path.join(target, "SKILL.md")), "restored target must have original content");
1272
- }
1273
- finally {
1274
- try {
1275
- await chmod(path.join(sourceDir, "unreadable-source"), 0o755);
1276
- }
1277
- catch {
1278
- /* best effort */
1279
- }
1280
- await rm(sourceDir, { recursive: true, force: true });
1281
- await rm(home, { recursive: true, force: true });
1282
- }
1283
- });
1284
- it("rename atomically replaces stale .inception-backup — no stale content leaks into target", async () => {
1285
- const sourceDir = await makeTmpDir();
1286
- const home = await makeTmpDir();
1287
- try {
1288
- await createSkillSource(sourceDir, "skills/test-skill");
1289
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1290
- const target = actions[0]?.target;
1291
- const backupPath = `${target}.inception-backup`;
1292
- // First deploy to establish a managed target
1293
- await executeDeploy(actions, false, false, home);
1294
- // Simulate a stale backup containing sentinel content
1295
- await mkdir(backupPath, { recursive: true });
1296
- await writeFile(path.join(backupPath, "stale.txt"), "leftover content");
1297
- // Redeploy: rename(target, backupPath) atomically replaces the stale dir
1298
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1299
- assert.equal(succeeded, 1);
1300
- assert.equal(failed.length, 0);
1301
- assert.ok(await exists(target), "target must exist after redeploy");
1302
- assert.ok(!(await exists(backupPath)), "stale backup must be cleaned up");
1303
- // The stale sentinel file must not appear in the new target
1304
- assert.ok(!(await exists(path.join(target, "stale.txt"))), "stale content must not leak into the new target");
1305
- }
1306
- finally {
1307
- await rm(sourceDir, { recursive: true });
1308
- await rm(home, { recursive: true, force: true });
1309
- }
1310
- });
1311
- });
1312
959
  describe("planDeploy path traversal", () => {
1313
960
  it("throws when skill.path resolves to the repository root itself (.)", async () => {
1314
961
  const sourceDir = await makeTmpDir();
@@ -1484,238 +1131,6 @@ describe("source directory immutability", () => {
1484
1131
  }
1485
1132
  });
1486
1133
  });
1487
- describe("executeDeploy (Windows)", {
1488
- skip: process.platform !== "win32",
1489
- }, () => {
1490
- it("creates copies on Windows", async () => {
1491
- const sourceDir = await makeTmpDir();
1492
- const home = await makeTmpDir();
1493
- try {
1494
- await createSkillSource(sourceDir, "skills/test-skill");
1495
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1496
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1497
- assert.equal(succeeded, 1);
1498
- assert.equal(failed.length, 0);
1499
- const target = actions[0]?.target;
1500
- assert.ok(await exists(target));
1501
- assert.ok(!(await lstat(target)).isSymbolicLink());
1502
- assert.ok(await exists(path.join(target, "SKILL.md")));
1503
- }
1504
- finally {
1505
- await rm(sourceDir, { recursive: true, force: true });
1506
- await rm(home, { recursive: true, force: true });
1507
- }
1508
- });
1509
- it("registers deployment in registry on Windows copy deploy", async () => {
1510
- const sourceDir = await makeTmpDir();
1511
- const home = await makeTmpDir();
1512
- try {
1513
- const skillSource = await createSkillSource(sourceDir, "skills/test-skill");
1514
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1515
- await executeDeploy(actions, false, false, home);
1516
- const target = actions[0]?.target;
1517
- const entry = await lookupDeployment(home, target);
1518
- assert.ok(entry);
1519
- if (entry.kind !== "skill-dir")
1520
- assert.fail("Expected skill-dir");
1521
- assert.equal(entry.skill, "test-skill");
1522
- assert.equal(entry.agent, "claude-code");
1523
- assert.equal(entry.source, skillSource);
1524
- assert.equal(entry.method, "copy");
1525
- assert.ok(!(await exists(path.join(skillSource, ".inception-totem"))));
1526
- }
1527
- finally {
1528
- await rm(sourceDir, { recursive: true, force: true });
1529
- await rm(home, { recursive: true, force: true });
1530
- }
1531
- });
1532
- it("does not create copies in dry-run mode on Windows", async () => {
1533
- const sourceDir = await makeTmpDir();
1534
- const home = await makeTmpDir();
1535
- try {
1536
- await createSkillSource(sourceDir, "skills/test-skill");
1537
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1538
- const { succeeded, failed } = await executeDeploy(actions, true, false, home);
1539
- assert.equal(succeeded, 1);
1540
- assert.equal(failed.length, 0);
1541
- const target = actions[0]?.target;
1542
- assert.ok(!(await exists(target)));
1543
- }
1544
- finally {
1545
- await rm(sourceDir, { recursive: true, force: true });
1546
- await rm(home, { recursive: true, force: true });
1547
- }
1548
- });
1549
- it("overwrites existing copy (with registry entry) on Windows", async () => {
1550
- const sourceDir = await makeTmpDir();
1551
- const home = await makeTmpDir();
1552
- try {
1553
- await createSkillSource(sourceDir, "skills/test-skill");
1554
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1555
- await executeDeploy(actions, false, false, home);
1556
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1557
- assert.equal(succeeded, 1);
1558
- assert.equal(failed.length, 0);
1559
- }
1560
- finally {
1561
- await rm(sourceDir, { recursive: true, force: true });
1562
- await rm(home, { recursive: true, force: true });
1563
- }
1564
- });
1565
- it("reports error for missing source (caught at planning)", async () => {
1566
- const home = await makeTmpDir();
1567
- try {
1568
- await assert.rejects(planDeploy(testManifest, "/nonexistent/source", ["claude-code"], home), (err) => {
1569
- assert.ok(err instanceof UserError);
1570
- assert.equal(err.code, "DEPLOY_FAILED");
1571
- assert.match(err.message, /source not found/);
1572
- return true;
1573
- });
1574
- }
1575
- finally {
1576
- await rm(home, { recursive: true, force: true });
1577
- }
1578
- });
1579
- it("refuses to overwrite unmanaged target on Windows", async () => {
1580
- const sourceDir = await makeTmpDir();
1581
- const home = await makeTmpDir();
1582
- try {
1583
- await createSkillSource(sourceDir, "skills/test-skill");
1584
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1585
- const target = actions[0]?.target;
1586
- await mkdir(target, { recursive: true });
1587
- await writeFile(path.join(target, "something.txt"), "user content");
1588
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1589
- assert.equal(succeeded, 0);
1590
- assert.equal(failed.length, 1);
1591
- assert.ok(failed[0]?.error.includes("not managed by inception-engine"));
1592
- assert.ok(await exists(path.join(target, "something.txt")));
1593
- }
1594
- finally {
1595
- await rm(sourceDir, { recursive: true, force: true });
1596
- await rm(home, { recursive: true, force: true });
1597
- }
1598
- });
1599
- it("refuses to overwrite target with mismatched registry entry on Windows", async () => {
1600
- const sourceDir = await makeTmpDir();
1601
- const home = await makeTmpDir();
1602
- try {
1603
- await createSkillSource(sourceDir, "skills/test-skill");
1604
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1605
- const target = actions[0]?.target;
1606
- await mkdir(target, { recursive: true });
1607
- await writeFile(path.join(target, "SKILL.md"), "other content");
1608
- await registerDeployment(home, target, {
1609
- kind: "skill-dir",
1610
- source: "/completely/different/source",
1611
- skill: "different-skill",
1612
- agent: "codex",
1613
- method: "copy",
1614
- });
1615
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1616
- assert.equal(succeeded, 0);
1617
- assert.equal(failed.length, 1);
1618
- assert.ok(failed[0]?.error.includes("not managed by inception-engine"));
1619
- assert.ok(await exists(path.join(target, "SKILL.md")));
1620
- }
1621
- finally {
1622
- await rm(sourceDir, { recursive: true, force: true });
1623
- await rm(home, { recursive: true, force: true });
1624
- }
1625
- });
1626
- it("backup is removed after successful redeploy on Windows", async () => {
1627
- const sourceDir = await makeTmpDir();
1628
- const home = await makeTmpDir();
1629
- try {
1630
- await createSkillSource(sourceDir, "skills/test-skill");
1631
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1632
- const target = actions[0]?.target;
1633
- const backupPath = `${target}.inception-backup`;
1634
- await executeDeploy(actions, false, false, home);
1635
- await executeDeploy(actions, false, false, home);
1636
- assert.ok(!(await exists(backupPath)), "backup should not exist after successful redeploy");
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
- });
1645
- describe("atomic redeploy behavior (Windows)", {
1646
- skip: process.platform !== "win32",
1647
- }, () => {
1648
- it("cleans up stale .inception-backup from a previous failed attempt on Windows", async () => {
1649
- const sourceDir = await makeTmpDir();
1650
- const home = await makeTmpDir();
1651
- try {
1652
- await createSkillSource(sourceDir, "skills/test-skill");
1653
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1654
- const target = actions[0]?.target;
1655
- const backupPath = `${target}.inception-backup`;
1656
- await executeDeploy(actions, false, false, home);
1657
- await mkdir(backupPath, { recursive: true });
1658
- await writeFile(path.join(backupPath, "stale.txt"), "leftover");
1659
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1660
- assert.equal(succeeded, 1);
1661
- assert.equal(failed.length, 0);
1662
- assert.ok(!(await exists(backupPath)), "stale backup should be cleaned up");
1663
- assert.ok(await exists(target), "target should exist after redeploy");
1664
- }
1665
- finally {
1666
- await rm(sourceDir, { recursive: true, force: true });
1667
- await rm(home, { recursive: true, force: true });
1668
- }
1669
- });
1670
- it("no backup is created on first deploy (no prior target) on Windows", async () => {
1671
- const sourceDir = await makeTmpDir();
1672
- const home = await makeTmpDir();
1673
- try {
1674
- await createSkillSource(sourceDir, "skills/test-skill");
1675
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1676
- const target = actions[0]?.target;
1677
- const backupPath = `${target}.inception-backup`;
1678
- const { succeeded } = await executeDeploy(actions, false, false, home);
1679
- assert.equal(succeeded, 1);
1680
- assert.ok(await exists(target));
1681
- assert.ok(!(await exists(backupPath)), "no backup should be created when there was no prior target");
1682
- }
1683
- finally {
1684
- await rm(sourceDir, { recursive: true, force: true });
1685
- await rm(home, { recursive: true, force: true });
1686
- }
1687
- });
1688
- it("restores backup when deploy fails (registry not writable) on Windows", async () => {
1689
- const sourceDir = await makeTmpDir();
1690
- const home = await makeTmpDir();
1691
- try {
1692
- await createSkillSource(sourceDir, "skills/test-skill");
1693
- const { actions } = await planDeploy(testManifest, sourceDir, ["claude-code"], home);
1694
- const target = actions[0]?.target;
1695
- const backupPath = `${target}.inception-backup`;
1696
- const { succeeded: firstSucceeded } = await executeDeploy(actions, false, false, home);
1697
- assert.equal(firstSucceeded, 1);
1698
- const registryFile = path.join(home, ".inception-engine", "registry.json");
1699
- await chmod(registryFile, 0o444); // trigger EPERM on Windows
1700
- const { succeeded, failed } = await executeDeploy(actions, false, false, home);
1701
- assert.equal(succeeded, 0);
1702
- assert.equal(failed.length, 1);
1703
- assert.ok(!(await exists(backupPath)), "backup should be gone after rollback");
1704
- assert.ok(await exists(target), "original target must be restored");
1705
- assert.ok(await exists(path.join(target, "SKILL.md")), "original content must be present");
1706
- }
1707
- finally {
1708
- try {
1709
- await chmod(path.join(home, ".inception-engine", "registry.json"), 0o666);
1710
- }
1711
- catch {
1712
- /* best effort */
1713
- }
1714
- await rm(sourceDir, { recursive: true, force: true });
1715
- await rm(home, { recursive: true, force: true });
1716
- }
1717
- });
1718
- });
1719
1134
  describe("executeDeploy — file-write", () => {
1720
1135
  it("copies source file to target", async () => {
1721
1136
  const sourceDir = await makeTmpDir();