@indigoai-us/hq-cloud 6.15.80 → 6.15.82

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 (125) hide show
  1. package/dist/backup-prune.d.ts +18 -0
  2. package/dist/backup-prune.d.ts.map +1 -1
  3. package/dist/backup-prune.js +20 -0
  4. package/dist/backup-prune.js.map +1 -1
  5. package/dist/backup-prune.test.js +30 -1
  6. package/dist/backup-prune.test.js.map +1 -1
  7. package/dist/bin/backup-prune-runner.d.ts +4 -1
  8. package/dist/bin/backup-prune-runner.d.ts.map +1 -1
  9. package/dist/bin/backup-prune-runner.js +61 -38
  10. package/dist/bin/backup-prune-runner.js.map +1 -1
  11. package/dist/bin/backup-prune-runner.test.d.ts +2 -0
  12. package/dist/bin/backup-prune-runner.test.d.ts.map +1 -0
  13. package/dist/bin/backup-prune-runner.test.js +22 -0
  14. package/dist/bin/backup-prune-runner.test.js.map +1 -0
  15. package/dist/bin/layout-compatibility.d.ts +12 -0
  16. package/dist/bin/layout-compatibility.d.ts.map +1 -0
  17. package/dist/bin/layout-compatibility.js +50 -0
  18. package/dist/bin/layout-compatibility.js.map +1 -0
  19. package/dist/bin/layout-compatibility.test.d.ts +2 -0
  20. package/dist/bin/layout-compatibility.test.d.ts.map +1 -0
  21. package/dist/bin/layout-compatibility.test.js +87 -0
  22. package/dist/bin/layout-compatibility.test.js.map +1 -0
  23. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  24. package/dist/bin/sync-runner-company.js +15 -0
  25. package/dist/bin/sync-runner-company.js.map +1 -1
  26. package/dist/bin/sync-runner-company.test.js +65 -0
  27. package/dist/bin/sync-runner-company.test.js.map +1 -1
  28. package/dist/bin/sync-runner-events.test.js +20 -0
  29. package/dist/bin/sync-runner-events.test.js.map +1 -1
  30. package/dist/bin/sync-runner-watch-loop.d.ts +9 -0
  31. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  32. package/dist/bin/sync-runner-watch-loop.js +369 -21
  33. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  34. package/dist/bin/sync-runner.d.ts +5 -0
  35. package/dist/bin/sync-runner.d.ts.map +1 -1
  36. package/dist/bin/sync-runner.js +11 -1
  37. package/dist/bin/sync-runner.js.map +1 -1
  38. package/dist/bin/sync-runner.test.js +165 -2
  39. package/dist/bin/sync-runner.test.js.map +1 -1
  40. package/dist/cli/share.d.ts.map +1 -1
  41. package/dist/cli/share.js +29 -5
  42. package/dist/cli/share.js.map +1 -1
  43. package/dist/cli/share.test.js +131 -24
  44. package/dist/cli/share.test.js.map +1 -1
  45. package/dist/cli/sync.d.ts +23 -0
  46. package/dist/cli/sync.d.ts.map +1 -1
  47. package/dist/cli/sync.js +72 -7
  48. package/dist/cli/sync.js.map +1 -1
  49. package/dist/cli/sync.test.js +199 -0
  50. package/dist/cli/sync.test.js.map +1 -1
  51. package/dist/cli/watch-event-push-conflict.test.js +22 -11
  52. package/dist/cli/watch-event-push-conflict.test.js.map +1 -1
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +1 -1
  56. package/dist/index.js.map +1 -1
  57. package/dist/journal.d.ts +18 -0
  58. package/dist/journal.d.ts.map +1 -1
  59. package/dist/journal.js +41 -17
  60. package/dist/journal.js.map +1 -1
  61. package/dist/journal.test.js +8 -1
  62. package/dist/journal.test.js.map +1 -1
  63. package/dist/outposts/self-deploy.d.ts.map +1 -1
  64. package/dist/outposts/self-deploy.js +10 -1
  65. package/dist/outposts/self-deploy.js.map +1 -1
  66. package/dist/outposts/self-deploy.test.js +4 -1
  67. package/dist/outposts/self-deploy.test.js.map +1 -1
  68. package/dist/s3.d.ts +62 -0
  69. package/dist/s3.d.ts.map +1 -1
  70. package/dist/s3.js +91 -1
  71. package/dist/s3.js.map +1 -1
  72. package/dist/s3.rename-retry.test.d.ts +2 -0
  73. package/dist/s3.rename-retry.test.d.ts.map +1 -0
  74. package/dist/s3.rename-retry.test.js +153 -0
  75. package/dist/s3.rename-retry.test.js.map +1 -0
  76. package/dist/sync/area-ledger-migration.d.ts +8 -1
  77. package/dist/sync/area-ledger-migration.d.ts.map +1 -1
  78. package/dist/sync/area-ledger-migration.js +23 -0
  79. package/dist/sync/area-ledger-migration.js.map +1 -1
  80. package/dist/sync/area-ledger-migration.test.js +20 -0
  81. package/dist/sync/area-ledger-migration.test.js.map +1 -1
  82. package/dist/sync/area-ledger.d.ts +10 -0
  83. package/dist/sync/area-ledger.d.ts.map +1 -1
  84. package/dist/sync/area-ledger.js +11 -0
  85. package/dist/sync/area-ledger.js.map +1 -1
  86. package/dist/sync/feature-flags.d.ts +29 -0
  87. package/dist/sync/feature-flags.d.ts.map +1 -1
  88. package/dist/sync/feature-flags.js +44 -0
  89. package/dist/sync/feature-flags.js.map +1 -1
  90. package/dist/sync/feature-flags.test.js +38 -1
  91. package/dist/sync/feature-flags.test.js.map +1 -1
  92. package/dist/sync/index.d.ts +6 -0
  93. package/dist/sync/index.d.ts.map +1 -1
  94. package/dist/sync/index.js +3 -0
  95. package/dist/sync/index.js.map +1 -1
  96. package/dist/sync/layout-compatibility.d.ts +17 -0
  97. package/dist/sync/layout-compatibility.d.ts.map +1 -0
  98. package/dist/sync/layout-compatibility.js +29 -0
  99. package/dist/sync/layout-compatibility.js.map +1 -0
  100. package/dist/sync/parallel-sync-coordinator.d.ts.map +1 -1
  101. package/dist/sync/parallel-sync-coordinator.js +0 -3
  102. package/dist/sync/parallel-sync-coordinator.js.map +1 -1
  103. package/dist/sync/parallel-sync-coordinator.test.js +14 -0
  104. package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
  105. package/dist/sync/per-area-maintenance.d.ts +45 -0
  106. package/dist/sync/per-area-maintenance.d.ts.map +1 -0
  107. package/dist/sync/per-area-maintenance.js +131 -0
  108. package/dist/sync/per-area-maintenance.js.map +1 -0
  109. package/dist/sync/per-area-maintenance.test.d.ts +2 -0
  110. package/dist/sync/per-area-maintenance.test.d.ts.map +1 -0
  111. package/dist/sync/per-area-maintenance.test.js +100 -0
  112. package/dist/sync/per-area-maintenance.test.js.map +1 -0
  113. package/dist/sync/realtime-telemetry.d.ts +49 -0
  114. package/dist/sync/realtime-telemetry.d.ts.map +1 -0
  115. package/dist/sync/realtime-telemetry.js +101 -0
  116. package/dist/sync/realtime-telemetry.js.map +1 -0
  117. package/dist/sync/realtime-telemetry.test.d.ts +2 -0
  118. package/dist/sync/realtime-telemetry.test.d.ts.map +1 -0
  119. package/dist/sync/realtime-telemetry.test.js +64 -0
  120. package/dist/sync/realtime-telemetry.test.js.map +1 -0
  121. package/dist/sync/state-store.d.ts +10 -0
  122. package/dist/sync/state-store.d.ts.map +1 -1
  123. package/dist/sync/state-store.js +18 -0
  124. package/dist/sync/state-store.js.map +1 -1
  125. package/package.json +3 -2
@@ -691,6 +691,95 @@ describe("sync", () => {
691
691
  expect(errs[0].path).toBe("docs/x.md");
692
692
  expect(events.some((e) => e.type === "skip-symlink-privilege")).toBe(false);
693
693
  });
694
+ it("HQ-DESKTOP-54 (second path): a Windows rename refusal on the install is a deliberate skip (never type:error) and is left unjournaled", async () => {
695
+ // The reopening path: an EPERM/EBUSY rename that outlasts the bounded win32
696
+ // retry surfaces from s3 as a typed WindowsRenameBlockedError. downloadOne
697
+ // must downgrade it to skip-rename-blocked — NOT type:"error" — so the
698
+ // runner never counts it toward errors[] and never exits 2. The refused key
699
+ // must also stay unjournaled so a later, unblocked pass retries it.
700
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
701
+ { key: "projects/demo/note.md", size: 10, lastModified: new Date(), etag: '"note"' },
702
+ ]);
703
+ vi.mocked(s3Module.downloadFile).mockRejectedValueOnce(new s3Module.WindowsRenameBlockedError("dest", "EPERM"));
704
+ const events = [];
705
+ const result = await sync({
706
+ company: "acme",
707
+ vaultConfig: mockConfig,
708
+ hqRoot: tmpDir,
709
+ skipReindex: true,
710
+ onEvent: (e) => events.push(e),
711
+ });
712
+ const skip = events.find((e) => e.type === "skip-rename-blocked");
713
+ expect(skip).toMatchObject({ type: "skip-rename-blocked", path: "projects/demo/note.md" });
714
+ expect(skip.remedy).toMatch(/holding this file open/i);
715
+ expect(events.some((e) => e.type === "error")).toBe(false);
716
+ expect(result.filesSkipped).toBeGreaterThanOrEqual(1);
717
+ expect(result.filesDownloaded).toBe(0);
718
+ expect(getEntry(readJournal("acme"), "projects/demo/note.md")).toBeUndefined();
719
+ });
720
+ it("HQ-DESKTOP-54 (second path) convergence: after a refused pass, the next pass that CAN install journals it (no manual reset)", async () => {
721
+ const remote = [
722
+ { key: "projects/demo/note.md", size: 10, lastModified: new Date(), etag: '"note"' },
723
+ ];
724
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce(remote);
725
+ vi.mocked(s3Module.downloadFile).mockRejectedValueOnce(new s3Module.WindowsRenameBlockedError("dest", "EPERM"));
726
+ await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir, skipReindex: true });
727
+ expect(getEntry(readJournal("acme"), "projects/demo/note.md")).toBeUndefined();
728
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce(remote);
729
+ vi.mocked(s3Module.downloadFile).mockImplementationOnce(async (_ctx, _key, localPath) => {
730
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
731
+ fs.writeFileSync(localPath, "remote body");
732
+ return { metadata: {}, contentHash: "h", contentSize: 11 };
733
+ });
734
+ const result = await sync({
735
+ company: "acme",
736
+ vaultConfig: mockConfig,
737
+ hqRoot: tmpDir,
738
+ skipReindex: true,
739
+ });
740
+ expect(result.filesDownloaded).toBe(1);
741
+ expect(getEntry(readJournal("acme"), "projects/demo/note.md")).toBeDefined();
742
+ });
743
+ it("an EACCES install rename is NOT downgraded — it stays a type:error (only the transient EPERM/EBUSY family is a skip)", async () => {
744
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
745
+ { key: "projects/demo/note.md", size: 10, lastModified: new Date(), etag: '"note"' },
746
+ ]);
747
+ // s3 propagates a raw EACCES (an ACL-denied destination) unchanged — it is
748
+ // never wrapped as WindowsRenameBlockedError — so downloadOne errors it.
749
+ vi.mocked(s3Module.downloadFile).mockRejectedValueOnce(errnoError("EACCES"));
750
+ const events = [];
751
+ await sync({
752
+ company: "acme",
753
+ vaultConfig: mockConfig,
754
+ hqRoot: tmpDir,
755
+ skipReindex: true,
756
+ onEvent: (e) => events.push(e),
757
+ });
758
+ expect(events.some((e) => e.type === "skip-rename-blocked")).toBe(false);
759
+ const errs = events.filter((e) => e.type === "error");
760
+ expect(errs).toHaveLength(1);
761
+ expect(errs[0].path).toBe("projects/demo/note.md");
762
+ });
763
+ it("the default console logger renders the rename-blocked skip and its remedy (a plain `hq sync` is not silent)", async () => {
764
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
765
+ { key: "projects/demo/note.md", size: 10, lastModified: new Date(), etag: '"note"' },
766
+ ]);
767
+ vi.mocked(s3Module.downloadFile).mockRejectedValueOnce(new s3Module.WindowsRenameBlockedError("dest", "EPERM"));
768
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
769
+ const log = vi.spyOn(console, "log").mockImplementation(() => { });
770
+ let rendered = [];
771
+ try {
772
+ // No onEvent → defaultConsoleLogger handles rendering.
773
+ await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir, skipReindex: true });
774
+ // Capture BEFORE mockRestore(), which clears recorded calls.
775
+ rendered = warn.mock.calls.map((c) => String(c[0]));
776
+ }
777
+ finally {
778
+ warn.mockRestore();
779
+ log.mockRestore();
780
+ }
781
+ expect(rendered.some((line) => /projects\/demo\/note\.md skipped/.test(line) && /holding the file open/i.test(line))).toBe(true);
782
+ });
694
783
  it("HQ-DESKTOP-54: a privilege refusal during the conflict-convergence probe is a skip, not an error or a counted conflict", async () => {
695
784
  // The conflict path also materializes the remote object (to compare it
696
785
  // against the diverged local copy). On an unprivileged Windows host that
@@ -1086,6 +1175,116 @@ describe("sync", () => {
1086
1175
  // here would re-create the deadlock this test exists to prevent.
1087
1176
  expect(entry.localDiverges).toBeFalsy();
1088
1177
  });
1178
+ it("HQ-DESKTOP-54 (second path): a blocked conflict-keep preserve rename is a skip that does NOT strand the file (no holdLocal, no localDiverges)", async () => {
1179
+ const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
1180
+ fs.mkdirSync(companyDocs, { recursive: true });
1181
+ const originalPath = path.join(companyDocs, "handoff.md");
1182
+ fs.writeFileSync(originalPath, "local version");
1183
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
1184
+ { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
1185
+ ]);
1186
+ fs.writeFileSync(journalPath, JSON.stringify({
1187
+ version: "1",
1188
+ lastSync: new Date().toISOString(),
1189
+ files: {
1190
+ "docs/handoff.md": {
1191
+ hash: "old-hash-from-last-sync",
1192
+ size: 20,
1193
+ syncedAt: new Date(Date.now() - 3600000).toISOString(),
1194
+ direction: "down",
1195
+ },
1196
+ },
1197
+ }));
1198
+ // A held local file: the move-aside EPERMs. On win32 s3 raises the typed
1199
+ // WindowsRenameBlockedError immediately (no retry).
1200
+ const realRenameSync = fs.renameSync;
1201
+ const renameSpy = vi.spyOn(fs, "renameSync").mockImplementation((from, to) => {
1202
+ if (String(from) === originalPath && String(to).includes(".conflict-")) {
1203
+ throw errnoError("EPERM");
1204
+ }
1205
+ return realRenameSync(from, to);
1206
+ });
1207
+ const realPlatform = process.platform;
1208
+ Object.defineProperty(process, "platform", { value: "win32", configurable: true });
1209
+ const events = [];
1210
+ try {
1211
+ await sync({
1212
+ company: "acme",
1213
+ onConflict: "keep",
1214
+ vaultConfig: mockConfig,
1215
+ hqRoot: tmpDir,
1216
+ onEvent: (e) => events.push(e),
1217
+ });
1218
+ }
1219
+ finally {
1220
+ Object.defineProperty(process, "platform", { value: realPlatform, configurable: true });
1221
+ renameSpy.mockRestore();
1222
+ }
1223
+ // Deliberate skip, never an error → runner stays exit 0.
1224
+ expect(events.some((e) => e.type === "skip-rename-blocked")).toBe(true);
1225
+ expect(events.some((e) => e.type === "error")).toBe(false);
1226
+ // The operator's file is untouched (nothing moved, remote not adopted).
1227
+ expect(fs.readFileSync(originalPath, "utf-8")).toBe("local version");
1228
+ // NOT stranded: holdLocal did not run, so no remote etag was stamped and
1229
+ // localDiverges was not armed — the conflict simply re-fires next pass.
1230
+ const entry = journalAt(journalPath).files["docs/handoff.md"];
1231
+ expect(entry.remoteEtag).toBeFalsy();
1232
+ expect(entry.localDiverges).toBeFalsy();
1233
+ });
1234
+ it("conflict-keep preserve: a non-downgradable EACCES stays the existing type:error, whose tail now carries the code= token (instrumentation guard)", async () => {
1235
+ const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
1236
+ fs.mkdirSync(companyDocs, { recursive: true });
1237
+ const originalPath = path.join(companyDocs, "handoff.md");
1238
+ fs.writeFileSync(originalPath, "local version");
1239
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
1240
+ { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
1241
+ ]);
1242
+ fs.writeFileSync(journalPath, JSON.stringify({
1243
+ version: "1",
1244
+ lastSync: new Date().toISOString(),
1245
+ files: {
1246
+ "docs/handoff.md": {
1247
+ hash: "old-hash-from-last-sync",
1248
+ size: 20,
1249
+ syncedAt: new Date(Date.now() - 3600000).toISOString(),
1250
+ direction: "down",
1251
+ },
1252
+ },
1253
+ }));
1254
+ const realRenameSync = fs.renameSync;
1255
+ const renameSpy = vi.spyOn(fs, "renameSync").mockImplementation((from, to) => {
1256
+ if (String(from) === originalPath && String(to).includes(".conflict-")) {
1257
+ throw errnoError("EACCES");
1258
+ }
1259
+ return realRenameSync(from, to);
1260
+ });
1261
+ const realPlatform = process.platform;
1262
+ Object.defineProperty(process, "platform", { value: "win32", configurable: true });
1263
+ const events = [];
1264
+ try {
1265
+ await sync({
1266
+ company: "acme",
1267
+ onConflict: "keep",
1268
+ vaultConfig: mockConfig,
1269
+ hqRoot: tmpDir,
1270
+ onEvent: (e) => events.push(e),
1271
+ });
1272
+ }
1273
+ finally {
1274
+ Object.defineProperty(process, "platform", { value: realPlatform, configurable: true });
1275
+ renameSpy.mockRestore();
1276
+ }
1277
+ expect(events.some((e) => e.type === "skip-rename-blocked")).toBe(false);
1278
+ const err = events.find((e) => e.type === "error");
1279
+ expect(err).toBeDefined();
1280
+ // Literal prefix byte-identical (desktop shape vocabulary keeps matching);
1281
+ // the tail now carries the parseable cause token.
1282
+ expect(err.message.startsWith("conflict keep could not preserve the local copy: ")).toBe(true);
1283
+ expect(err.message).toContain("code=EACCES");
1284
+ // The existing holdLocal path still fires for a non-downgradable errno.
1285
+ const entry = journalAt(journalPath).files["docs/handoff.md"];
1286
+ expect(entry.localDiverges).toBe(true);
1287
+ });
1089
1288
  it("conflict keep records the REMOTE body's hash, so the entry is pushable instead of permanently divergent", async () => {
1090
1289
  // This is the deadlock in one assertion. The old `keep` wrote the LOCAL
1091
1290
  // hash next to the REMOTE etag, which is a self-contradictory entry: the