@indigoai-us/hq-cloud 6.14.9 → 6.14.11

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 (45) hide show
  1. package/dist/cli/share.d.ts.map +1 -1
  2. package/dist/cli/share.js +89 -14
  3. package/dist/cli/share.js.map +1 -1
  4. package/dist/cli/share.test.js +131 -3
  5. package/dist/cli/share.test.js.map +1 -1
  6. package/dist/cli/sync.d.ts +1 -1
  7. package/dist/cli/sync.d.ts.map +1 -1
  8. package/dist/cli/sync.js +138 -41
  9. package/dist/cli/sync.js.map +1 -1
  10. package/dist/cli/sync.test.js +271 -25
  11. package/dist/cli/sync.test.js.map +1 -1
  12. package/dist/journal.d.ts +7 -1
  13. package/dist/journal.d.ts.map +1 -1
  14. package/dist/journal.js +22 -1
  15. package/dist/journal.js.map +1 -1
  16. package/dist/personal-vault-exclusions.d.ts +20 -4
  17. package/dist/personal-vault-exclusions.d.ts.map +1 -1
  18. package/dist/personal-vault-exclusions.js +37 -3
  19. package/dist/personal-vault-exclusions.js.map +1 -1
  20. package/dist/personal-vault-exclusions.test.js +19 -0
  21. package/dist/personal-vault-exclusions.test.js.map +1 -1
  22. package/dist/personal-vault.d.ts +6 -0
  23. package/dist/personal-vault.d.ts.map +1 -1
  24. package/dist/personal-vault.js +12 -0
  25. package/dist/personal-vault.js.map +1 -1
  26. package/dist/s3.d.ts +23 -1
  27. package/dist/s3.d.ts.map +1 -1
  28. package/dist/s3.js +74 -3
  29. package/dist/s3.js.map +1 -1
  30. package/dist/s3.test.js +71 -1
  31. package/dist/s3.test.js.map +1 -1
  32. package/dist/types.d.ts +13 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/cli/share.test.ts +176 -3
  36. package/src/cli/share.ts +114 -15
  37. package/src/cli/sync.test.ts +330 -24
  38. package/src/cli/sync.ts +184 -50
  39. package/src/journal.ts +28 -0
  40. package/src/personal-vault-exclusions.test.ts +23 -0
  41. package/src/personal-vault-exclusions.ts +39 -3
  42. package/src/personal-vault.ts +15 -0
  43. package/src/s3.test.ts +91 -0
  44. package/src/s3.ts +88 -2
  45. package/src/types.ts +13 -0
@@ -62,6 +62,14 @@ function makeEntityContext(overrides = {}) {
62
62
  ...overrides,
63
63
  };
64
64
  }
65
+ function deleteIntent(remoteEtag, localHash, localKind = "file") {
66
+ return {
67
+ version: 1,
68
+ remoteEtag,
69
+ localHash,
70
+ localKind,
71
+ };
72
+ }
65
73
  describe("wrapFilterWithIgnoreVisibility", () => {
66
74
  it("records noteworthy base-ignore exclusions while counting all rejections", () => {
67
75
  const hqRoot = path.join("/tmp", "hqroot");
@@ -1613,6 +1621,8 @@ describe("share", () => {
1613
1621
  syncedAt: new Date().toISOString(),
1614
1622
  direction: "up",
1615
1623
  remoteEtag: "knowledge-etag",
1624
+ kind: "symlink",
1625
+ localDeleteIntent: deleteIntent("knowledge-etag", "irrelevant-not-checked", "symlink"),
1616
1626
  },
1617
1627
  },
1618
1628
  }));
@@ -1744,6 +1754,32 @@ describe("share", () => {
1744
1754
  delete process.env.HQ_STATE_DIR;
1745
1755
  }
1746
1756
  });
1757
+ it("personalMode excludes generated namespaced skill wrappers while preserving local skills", async () => {
1758
+ const generatedSource = path.join(tmpDir, "companies", "acme", "skills", "demo", "scripts");
1759
+ const generatedWrapper = path.join(tmpDir, ".claude", "skills", "acme:demo");
1760
+ const localSkill = path.join(tmpDir, ".claude", "skills", "local-skill");
1761
+ fs.mkdirSync(generatedSource, { recursive: true });
1762
+ fs.writeFileSync(path.join(generatedSource, "run.sh"), "#!/bin/sh\n");
1763
+ fs.mkdirSync(generatedWrapper, { recursive: true });
1764
+ fs.symlinkSync(generatedSource, path.join(generatedWrapper, "scripts"), "dir");
1765
+ fs.mkdirSync(localSkill, { recursive: true });
1766
+ fs.writeFileSync(path.join(localSkill, "SKILL.md"), "# local\n");
1767
+ const result = await share({
1768
+ paths: [path.join(tmpDir, ".claude")],
1769
+ entityContext: makeEntityContext({
1770
+ uid: "prs_01PERSONALTEST",
1771
+ slug: "__hq_personal_vault__",
1772
+ bucketName: "hq-vault-prs-01PERSONALTEST",
1773
+ }),
1774
+ hqRoot: tmpDir,
1775
+ personalMode: true,
1776
+ journalSlug: "personal-generated-wrapper",
1777
+ });
1778
+ expect(result.filesUploaded).toBe(1);
1779
+ expect(result.filesExcludedByPolicy).toBeGreaterThanOrEqual(1);
1780
+ expect(uploadSymlink).not.toHaveBeenCalled();
1781
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), path.join(localSkill, "SKILL.md"), ".claude/skills/local-skill/SKILL.md", undefined, expect.anything());
1782
+ });
1747
1783
  it("personal-vault override does NOT apply to company vaults: `owned-only` on cmp_ entity still " +
1748
1784
  "refuses to tombstone a direction:'down' entry (multi-user curation intact)", async () => {
1749
1785
  // Sibling guard for the previous test: the override is scoped strictly
@@ -1808,6 +1844,8 @@ describe("share", () => {
1808
1844
  syncedAt: new Date().toISOString(),
1809
1845
  direction: "up",
1810
1846
  remoteEtag: "gone-etag",
1847
+ kind: "file",
1848
+ localDeleteIntent: deleteIntent("gone-etag", "irrelevant-not-checked-here"),
1811
1849
  },
1812
1850
  },
1813
1851
  }));
@@ -1842,6 +1880,9 @@ describe("share", () => {
1842
1880
  size: 42,
1843
1881
  syncedAt: new Date().toISOString(),
1844
1882
  direction: "up",
1883
+ remoteEtag: "removed-etag",
1884
+ kind: "file",
1885
+ localDeleteIntent: deleteIntent("removed-etag", "h"),
1845
1886
  },
1846
1887
  },
1847
1888
  }));
@@ -1912,6 +1953,9 @@ describe("share", () => {
1912
1953
  size: 1,
1913
1954
  syncedAt: new Date().toISOString(),
1914
1955
  direction: "up",
1956
+ remoteEtag: "in-scope-etag",
1957
+ kind: "file",
1958
+ localDeleteIntent: deleteIntent("in-scope-etag", "h"),
1915
1959
  },
1916
1960
  "other/also-gone.md": {
1917
1961
  hash: "h",
@@ -1954,6 +1998,9 @@ describe("share", () => {
1954
1998
  size: 5,
1955
1999
  syncedAt: new Date().toISOString(),
1956
2000
  direction: "up",
2001
+ remoteEtag: "flaky-etag",
2002
+ kind: "file",
2003
+ localDeleteIntent: deleteIntent("flaky-etag", "h"),
1957
2004
  },
1958
2005
  },
1959
2006
  }));
@@ -2020,6 +2067,9 @@ describe("share", () => {
2020
2067
  "i-uploaded.md": {
2021
2068
  hash: "h", size: 1, syncedAt: new Date().toISOString(),
2022
2069
  direction: "up",
2070
+ remoteEtag: "uploaded-etag",
2071
+ kind: "file",
2072
+ localDeleteIntent: deleteIntent("uploaded-etag", "h"),
2023
2073
  },
2024
2074
  },
2025
2075
  }));
@@ -2050,6 +2100,9 @@ describe("share", () => {
2050
2100
  "pulled.md": {
2051
2101
  hash: "h", size: 1, syncedAt: new Date().toISOString(),
2052
2102
  direction: "down",
2103
+ remoteEtag: "pulled-etag",
2104
+ kind: "file",
2105
+ localDeleteIntent: deleteIntent("pulled-etag", "h"),
2053
2106
  },
2054
2107
  },
2055
2108
  }));
@@ -2084,6 +2137,9 @@ describe("share", () => {
2084
2137
  "active/current.md": {
2085
2138
  hash: "h", size: 1, syncedAt: new Date().toISOString(),
2086
2139
  direction: "up",
2140
+ remoteEtag: "active-etag",
2141
+ kind: "file",
2142
+ localDeleteIntent: deleteIntent("active-etag", "h"),
2087
2143
  },
2088
2144
  },
2089
2145
  }));
@@ -2137,6 +2193,8 @@ describe("share", () => {
2137
2193
  hash: "h", size: 100, syncedAt: new Date().toISOString(),
2138
2194
  direction: "down",
2139
2195
  remoteEtag: "abc123",
2196
+ kind: "file",
2197
+ localDeleteIntent: deleteIntent("abc123", "h"),
2140
2198
  },
2141
2199
  },
2142
2200
  }));
@@ -2178,6 +2236,8 @@ describe("share", () => {
2178
2236
  syncedAt: new Date().toISOString(),
2179
2237
  direction: "down",
2180
2238
  remoteEtag: "abc123",
2239
+ kind: "file",
2240
+ localDeleteIntent: deleteIntent("abc123", "h"),
2181
2241
  removedAt: new Date().toISOString(),
2182
2242
  removedReason: "local-delete",
2183
2243
  },
@@ -2219,6 +2279,8 @@ describe("share", () => {
2219
2279
  hash: "h", size: 50, syncedAt: new Date().toISOString(),
2220
2280
  direction: "down",
2221
2281
  remoteEtag: "stale-etag",
2282
+ kind: "file",
2283
+ localDeleteIntent: deleteIntent("stale-etag", "h"),
2222
2284
  },
2223
2285
  },
2224
2286
  }));
@@ -2258,6 +2320,55 @@ describe("share", () => {
2258
2320
  expect(result.filesRefusedStale).toBe(1);
2259
2321
  expect(result.filesTombstoned).toBe(0);
2260
2322
  });
2323
+ it("currency-gated: preserves a file recreated after the delete plan", async () => {
2324
+ const companyRoot = path.join(tmpDir, "companies", "acme");
2325
+ fs.mkdirSync(companyRoot, { recursive: true });
2326
+ const recreatedPath = path.join(companyRoot, "racy.md");
2327
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
2328
+ fs.writeFileSync(journalPath, JSON.stringify({
2329
+ version: "2",
2330
+ lastSync: new Date().toISOString(),
2331
+ files: {
2332
+ "racy.md": {
2333
+ hash: "synced-hash",
2334
+ size: 10,
2335
+ syncedAt: new Date().toISOString(),
2336
+ direction: "up",
2337
+ remoteEtag: "racy-etag",
2338
+ kind: "file",
2339
+ localDeleteIntent: deleteIntent("racy-etag", "synced-hash"),
2340
+ },
2341
+ },
2342
+ pulls: [],
2343
+ }));
2344
+ // The currency HEAD establishes the remote revision, then a local writer
2345
+ // recreates the file before the planned DeleteObject can run.
2346
+ vi.mocked(headRemoteFile).mockImplementationOnce(async () => {
2347
+ fs.writeFileSync(recreatedPath, "recreated after planning");
2348
+ return { lastModified: new Date(), etag: '"racy-etag"', size: 10 };
2349
+ });
2350
+ const events = [];
2351
+ const result = await share({
2352
+ paths: [companyRoot],
2353
+ company: "acme",
2354
+ vaultConfig: mockConfig,
2355
+ hqRoot: tmpDir,
2356
+ skipUnchanged: true,
2357
+ propagateDeletes: true,
2358
+ propagateDeletePolicy: "currency-gated",
2359
+ onEvent: (e) => events.push(e),
2360
+ });
2361
+ expect(result.filesDeleted).toBe(0);
2362
+ expect(deleteRemoteFile).not.toHaveBeenCalled();
2363
+ expect(fs.readFileSync(recreatedPath, "utf-8")).toBe("recreated after planning");
2364
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2365
+ expect(journal.files["racy.md"]).toBeDefined();
2366
+ expect(events).toContainEqual(expect.objectContaining({
2367
+ type: "delete-refused-stale-etag",
2368
+ path: "racy.md",
2369
+ reason: "intent-changed",
2370
+ }));
2371
+ });
2261
2372
  it("currency-gated: refuses delete for a divergent-local entry (journal-honesty — never delete a remote the local never genuinely matched)", async () => {
2262
2373
  const companyRoot = path.join(tmpDir, "companies", "acme");
2263
2374
  fs.mkdirSync(companyRoot, { recursive: true });
@@ -2328,6 +2439,8 @@ describe("share", () => {
2328
2439
  hash: "h", size: 25, syncedAt: new Date().toISOString(),
2329
2440
  direction: "down",
2330
2441
  remoteEtag: "doesnt-matter",
2442
+ kind: "file",
2443
+ localDeleteIntent: deleteIntent("doesnt-matter", "h"),
2331
2444
  },
2332
2445
  },
2333
2446
  }));
@@ -2396,12 +2509,13 @@ describe("share", () => {
2396
2509
  });
2397
2510
  expect(result.filesDeleted).toBe(0);
2398
2511
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2399
- // HEAD must NOT be called — we short-circuit on the missing journal etag.
2512
+ // HEAD must NOT be called — the explicit-intent gate rejects legacy
2513
+ // entries before any remote operation.
2400
2514
  expect(headRemoteFile).not.toHaveBeenCalled();
2401
2515
  const refused = events.find((e) => e.type === "delete-refused-stale-etag");
2402
2516
  expect(refused).toMatchObject({
2403
- journalEtag: "<legacy-no-etag>",
2404
- reason: "legacy-no-etag",
2517
+ journalEtag: "<missing-delete-intent>",
2518
+ reason: "missing-delete-intent",
2405
2519
  });
2406
2520
  expect(result.filesRefusedStale).toBe(1);
2407
2521
  });
@@ -2424,6 +2538,8 @@ describe("share", () => {
2424
2538
  hash: "h", size: 200, syncedAt: new Date().toISOString(),
2425
2539
  direction: "down",
2426
2540
  remoteEtag: "upstream-etag",
2541
+ kind: "file",
2542
+ localDeleteIntent: deleteIntent("upstream-etag", "h"),
2427
2543
  },
2428
2544
  },
2429
2545
  }));
@@ -2525,6 +2641,8 @@ describe("share", () => {
2525
2641
  hash: "h", size: 5, syncedAt: new Date().toISOString(),
2526
2642
  direction: "up",
2527
2643
  remoteEtag: "regular-etag",
2644
+ kind: "file",
2645
+ localDeleteIntent: deleteIntent("regular-etag", "h"),
2528
2646
  },
2529
2647
  },
2530
2648
  }));
@@ -2796,6 +2914,8 @@ describe("share", () => {
2796
2914
  hash: "h", size: 1, syncedAt: new Date().toISOString(),
2797
2915
  direction: "up",
2798
2916
  remoteEtag: "abc",
2917
+ kind: "file",
2918
+ localDeleteIntent: deleteIntent("abc", "h"),
2799
2919
  },
2800
2920
  },
2801
2921
  }));
@@ -3190,6 +3310,8 @@ describe("share", () => {
3190
3310
  syncedAt: new Date().toISOString(),
3191
3311
  direction,
3192
3312
  remoteEtag: `etag-${i}`,
3313
+ kind: "file",
3314
+ localDeleteIntent: deleteIntent(`etag-${i}`, "h"),
3193
3315
  };
3194
3316
  if (i >= missing) {
3195
3317
  fs.writeFileSync(path.join(companyRoot, key), "hi");
@@ -3394,11 +3516,15 @@ describe("share", () => {
3394
3516
  files[`d-${i.toString().padStart(4, "0")}.md`] = {
3395
3517
  hash: "h", size: 5, syncedAt: now,
3396
3518
  direction: "down", remoteEtag: `down-etag-${i}`,
3519
+ kind: "file",
3520
+ localDeleteIntent: deleteIntent(`down-etag-${i}`, "h"),
3397
3521
  };
3398
3522
  }
3399
3523
  files["only-up.md"] = {
3400
3524
  hash: "h", size: 5, syncedAt: now,
3401
3525
  direction: "up", remoteEtag: "up-etag",
3526
+ kind: "file",
3527
+ localDeleteIntent: deleteIntent("up-etag", "h"),
3402
3528
  };
3403
3529
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3404
3530
  fs.writeFileSync(journalPath, JSON.stringify({ version: "1", lastSync: now, files }));
@@ -3534,6 +3660,8 @@ describe("currency-gated: journal version 2 fixtures", () => {
3534
3660
  syncedAt: new Date().toISOString(),
3535
3661
  direction: "down",
3536
3662
  remoteEtag: "v2-etag",
3663
+ kind: "file",
3664
+ localDeleteIntent: deleteIntent("v2-etag", "h"),
3537
3665
  },
3538
3666
  },
3539
3667
  pulls: [],