@indigoai-us/hq-cloud 6.15.0 → 6.15.2

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 (329) hide show
  1. package/dist/bin/sync-mutation.d.ts +16 -0
  2. package/dist/bin/sync-mutation.d.ts.map +1 -0
  3. package/dist/bin/sync-mutation.js +60 -0
  4. package/dist/bin/sync-mutation.js.map +1 -0
  5. package/dist/bin/sync-mutation.test.d.ts +2 -0
  6. package/dist/bin/sync-mutation.test.d.ts.map +1 -0
  7. package/dist/bin/sync-mutation.test.js +165 -0
  8. package/dist/bin/sync-mutation.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts +8 -0
  10. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  11. package/dist/bin/sync-runner-company.js +16 -0
  12. package/dist/bin/sync-runner-company.js.map +1 -1
  13. package/dist/bin/sync-runner-company.test.d.ts +2 -0
  14. package/dist/bin/sync-runner-company.test.d.ts.map +1 -0
  15. package/dist/bin/sync-runner-company.test.js +36 -0
  16. package/dist/bin/sync-runner-company.test.js.map +1 -0
  17. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  18. package/dist/bin/sync-runner-watch-loop.js +98 -8
  19. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  20. package/dist/bin/sync-runner.d.ts +17 -0
  21. package/dist/bin/sync-runner.d.ts.map +1 -1
  22. package/dist/bin/sync-runner.js.map +1 -1
  23. package/dist/bin/sync-runner.test.js +109 -0
  24. package/dist/bin/sync-runner.test.js.map +1 -1
  25. package/dist/cli/conflict-recovery.test.d.ts +2 -0
  26. package/dist/cli/conflict-recovery.test.d.ts.map +1 -0
  27. package/dist/cli/conflict-recovery.test.js +201 -0
  28. package/dist/cli/conflict-recovery.test.js.map +1 -0
  29. package/dist/cli/conflict.d.ts +60 -0
  30. package/dist/cli/conflict.d.ts.map +1 -1
  31. package/dist/cli/conflict.js +333 -0
  32. package/dist/cli/conflict.js.map +1 -1
  33. package/dist/cli/sync.d.ts +35 -0
  34. package/dist/cli/sync.d.ts.map +1 -1
  35. package/dist/cli/sync.js +100 -0
  36. package/dist/cli/sync.js.map +1 -1
  37. package/dist/cli/sync.test.js +85 -1
  38. package/dist/cli/sync.test.js.map +1 -1
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/skill-telemetry.d.ts +6 -0
  44. package/dist/skill-telemetry.d.ts.map +1 -1
  45. package/dist/skill-telemetry.js +14 -2
  46. package/dist/skill-telemetry.js.map +1 -1
  47. package/dist/skill-telemetry.test.js +79 -0
  48. package/dist/skill-telemetry.test.js.map +1 -1
  49. package/dist/sync/candidate-uploader.d.ts +88 -0
  50. package/dist/sync/candidate-uploader.d.ts.map +1 -0
  51. package/dist/sync/candidate-uploader.js +212 -0
  52. package/dist/sync/candidate-uploader.js.map +1 -0
  53. package/dist/sync/candidate-uploader.test.d.ts +2 -0
  54. package/dist/sync/candidate-uploader.test.d.ts.map +1 -0
  55. package/dist/sync/candidate-uploader.test.js +132 -0
  56. package/dist/sync/candidate-uploader.test.js.map +1 -0
  57. package/dist/sync/delta-client.d.ts +73 -0
  58. package/dist/sync/delta-client.d.ts.map +1 -0
  59. package/dist/sync/delta-client.js +201 -0
  60. package/dist/sync/delta-client.js.map +1 -0
  61. package/dist/sync/delta-client.test.d.ts +2 -0
  62. package/dist/sync/delta-client.test.d.ts.map +1 -0
  63. package/dist/sync/delta-client.test.js +97 -0
  64. package/dist/sync/delta-client.test.js.map +1 -0
  65. package/dist/sync/durable-apply.d.ts +76 -0
  66. package/dist/sync/durable-apply.d.ts.map +1 -0
  67. package/dist/sync/durable-apply.js +530 -0
  68. package/dist/sync/durable-apply.js.map +1 -0
  69. package/dist/sync/durable-apply.test.d.ts +2 -0
  70. package/dist/sync/durable-apply.test.d.ts.map +1 -0
  71. package/dist/sync/durable-apply.test.js +180 -0
  72. package/dist/sync/durable-apply.test.js.map +1 -0
  73. package/dist/sync/event-sync.d.ts +33 -1
  74. package/dist/sync/event-sync.d.ts.map +1 -1
  75. package/dist/sync/event-sync.js +149 -1
  76. package/dist/sync/event-sync.js.map +1 -1
  77. package/dist/sync/event-sync.test.js +142 -1
  78. package/dist/sync/event-sync.test.js.map +1 -1
  79. package/dist/sync/index.d.ts +2 -0
  80. package/dist/sync/index.d.ts.map +1 -1
  81. package/dist/sync/index.js +1 -0
  82. package/dist/sync/index.js.map +1 -1
  83. package/dist/sync/multipart-uploader.d.ts +99 -0
  84. package/dist/sync/multipart-uploader.d.ts.map +1 -0
  85. package/dist/sync/multipart-uploader.js +447 -0
  86. package/dist/sync/multipart-uploader.js.map +1 -0
  87. package/dist/sync/multipart-uploader.test.d.ts +2 -0
  88. package/dist/sync/multipart-uploader.test.d.ts.map +1 -0
  89. package/dist/sync/multipart-uploader.test.js +119 -0
  90. package/dist/sync/multipart-uploader.test.js.map +1 -0
  91. package/dist/sync/mutation-client.d.ts +85 -0
  92. package/dist/sync/mutation-client.d.ts.map +1 -0
  93. package/dist/sync/mutation-client.js +245 -0
  94. package/dist/sync/mutation-client.js.map +1 -0
  95. package/dist/sync/mutation-client.test.d.ts +2 -0
  96. package/dist/sync/mutation-client.test.d.ts.map +1 -0
  97. package/dist/sync/mutation-client.test.js +51 -0
  98. package/dist/sync/mutation-client.test.js.map +1 -0
  99. package/dist/sync/push-receiver.d.ts +45 -0
  100. package/dist/sync/push-receiver.d.ts.map +1 -1
  101. package/dist/sync/push-receiver.js +101 -0
  102. package/dist/sync/push-receiver.js.map +1 -1
  103. package/dist/sync/push-receiver.test.js +54 -2
  104. package/dist/sync/push-receiver.test.js.map +1 -1
  105. package/dist/sync/scope-inventory-client.d.ts +69 -0
  106. package/dist/sync/scope-inventory-client.d.ts.map +1 -0
  107. package/dist/sync/scope-inventory-client.js +210 -0
  108. package/dist/sync/scope-inventory-client.js.map +1 -0
  109. package/dist/sync/scope-inventory-client.test.d.ts +2 -0
  110. package/dist/sync/scope-inventory-client.test.d.ts.map +1 -0
  111. package/dist/sync/scope-inventory-client.test.js +94 -0
  112. package/dist/sync/scope-inventory-client.test.js.map +1 -0
  113. package/dist/sync/snapshot-client.d.ts +98 -0
  114. package/dist/sync/snapshot-client.d.ts.map +1 -0
  115. package/dist/sync/snapshot-client.js +402 -0
  116. package/dist/sync/snapshot-client.js.map +1 -0
  117. package/dist/sync/snapshot-client.test.d.ts +2 -0
  118. package/dist/sync/snapshot-client.test.d.ts.map +1 -0
  119. package/dist/sync/snapshot-client.test.js +169 -0
  120. package/dist/sync/snapshot-client.test.js.map +1 -0
  121. package/dist/sync/uploader-finalization.d.ts +97 -0
  122. package/dist/sync/uploader-finalization.d.ts.map +1 -0
  123. package/dist/sync/uploader-finalization.js +273 -0
  124. package/dist/sync/uploader-finalization.js.map +1 -0
  125. package/dist/sync/uploader-finalization.test.d.ts +2 -0
  126. package/dist/sync/uploader-finalization.test.d.ts.map +1 -0
  127. package/dist/sync/uploader-finalization.test.js +92 -0
  128. package/dist/sync/uploader-finalization.test.js.map +1 -0
  129. package/dist/telemetry.d.ts +11 -1
  130. package/dist/telemetry.d.ts.map +1 -1
  131. package/dist/telemetry.js +21 -2
  132. package/dist/telemetry.js.map +1 -1
  133. package/dist/telemetry.test.js +80 -0
  134. package/dist/telemetry.test.js.map +1 -1
  135. package/package.json +6 -1
  136. package/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +0 -30
  137. package/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +0 -22
  138. package/.github/workflows/ci.yml +0 -84
  139. package/.github/workflows/publish.yml +0 -56
  140. package/.github/workflows/unreleased-commits-nag.yml +0 -256
  141. package/eslint.config.js +0 -67
  142. package/pnpm-workspace.yaml +0 -2
  143. package/scripts/presign-transport-e2e.mjs +0 -250
  144. package/scripts/vault-rebaseline.sh +0 -323
  145. package/scripts/vault-rescue.sh +0 -332
  146. package/src/active-company.test.ts +0 -188
  147. package/src/active-company.ts +0 -168
  148. package/src/agent-codex-instructions.test.ts +0 -332
  149. package/src/agent-codex-instructions.ts +0 -309
  150. package/src/auth.ts +0 -146
  151. package/src/backup-prune.test.ts +0 -98
  152. package/src/backup-prune.ts +0 -182
  153. package/src/bin/backup-prune-runner.ts +0 -33
  154. package/src/bin/rescue-runner.ts +0 -25
  155. package/src/bin/sync-runner-company.ts +0 -695
  156. package/src/bin/sync-runner-events.test.ts +0 -143
  157. package/src/bin/sync-runner-events.ts +0 -55
  158. package/src/bin/sync-runner-planning.test.ts +0 -311
  159. package/src/bin/sync-runner-planning.ts +0 -258
  160. package/src/bin/sync-runner-rollup.test.ts +0 -37
  161. package/src/bin/sync-runner-rollup.ts +0 -97
  162. package/src/bin/sync-runner-telemetry.ts +0 -15
  163. package/src/bin/sync-runner-watch-loop.ts +0 -1235
  164. package/src/bin/sync-runner-watch-routes.test.ts +0 -71
  165. package/src/bin/sync-runner-watch-routes.ts +0 -184
  166. package/src/bin/sync-runner.test.ts +0 -8767
  167. package/src/bin/sync-runner.ts +0 -2190
  168. package/src/cli/accept.ts +0 -124
  169. package/src/cli/conflict.ts +0 -119
  170. package/src/cli/doctor.test.ts +0 -581
  171. package/src/cli/doctor.ts +0 -642
  172. package/src/cli/index.ts +0 -49
  173. package/src/cli/invite.test.ts +0 -250
  174. package/src/cli/invite.ts +0 -214
  175. package/src/cli/promote.ts +0 -157
  176. package/src/cli/reindex-knowledge.test.ts +0 -307
  177. package/src/cli/reindex-knowledge.ts +0 -450
  178. package/src/cli/reindex.test.ts +0 -957
  179. package/src/cli/reindex.ts +0 -979
  180. package/src/cli/rescue-classify-ordering.test.ts +0 -548
  181. package/src/cli/rescue-clone-diagnostics.test.ts +0 -120
  182. package/src/cli/rescue-core.ts +0 -3011
  183. package/src/cli/rescue-drift-reconcile.test.ts +0 -179
  184. package/src/cli/rescue-drop-dir-symlink.test.ts +0 -224
  185. package/src/cli/rescue-exec-bit-preserve.test.ts +0 -187
  186. package/src/cli/rescue-hq-root-guard.test.ts +0 -232
  187. package/src/cli/rescue-journal-reconcile.test.ts +0 -215
  188. package/src/cli/rescue-mtime-preserve.test.ts +0 -203
  189. package/src/cli/rescue-settings-reconcile.test.ts +0 -637
  190. package/src/cli/rescue-snapshot.test.ts +0 -57
  191. package/src/cli/rescue-snapshot.ts +0 -51
  192. package/src/cli/rescue.reindex.test.ts +0 -63
  193. package/src/cli/rescue.test.ts +0 -131
  194. package/src/cli/rescue.ts +0 -182
  195. package/src/cli/share.test.ts +0 -7843
  196. package/src/cli/share.ts +0 -3663
  197. package/src/cli/sync-scope.test.ts +0 -652
  198. package/src/cli/sync.test.ts +0 -5207
  199. package/src/cli/sync.ts +0 -3470
  200. package/src/cli/tombstones.ts +0 -106
  201. package/src/cli/watch-event-push-conflict.test.ts +0 -234
  202. package/src/client-info.test.ts +0 -214
  203. package/src/client-info.ts +0 -121
  204. package/src/cognito-auth.test.ts +0 -712
  205. package/src/cognito-auth.ts +0 -1422
  206. package/src/company-resolver.test.ts +0 -618
  207. package/src/company-resolver.ts +0 -521
  208. package/src/context.test.ts +0 -583
  209. package/src/context.ts +0 -378
  210. package/src/daemon-worker.ts +0 -26
  211. package/src/daemon.ts +0 -99
  212. package/src/entity-resolver.test.ts +0 -315
  213. package/src/entity-resolver.ts +0 -180
  214. package/src/ignore.test.ts +0 -466
  215. package/src/ignore.ts +0 -469
  216. package/src/index.ts +0 -439
  217. package/src/journal.test.ts +0 -968
  218. package/src/journal.ts +0 -765
  219. package/src/lib/cloud-authoritative.test.ts +0 -45
  220. package/src/lib/cloud-authoritative.ts +0 -59
  221. package/src/lib/conflict-file.ts +0 -86
  222. package/src/lib/conflict-index.ts +0 -289
  223. package/src/lib/conflict.test.ts +0 -348
  224. package/src/lib/describe-error.test.ts +0 -100
  225. package/src/lib/describe-error.ts +0 -58
  226. package/src/lib/exit-codes.ts +0 -24
  227. package/src/lib/machine-id.test.ts +0 -231
  228. package/src/lib/machine-id.ts +0 -175
  229. package/src/lib/net-errors.test.ts +0 -65
  230. package/src/lib/net-errors.ts +0 -86
  231. package/src/lib/readlink-safe.test.ts +0 -43
  232. package/src/lib/readlink-safe.ts +0 -29
  233. package/src/local-path-codec.test.ts +0 -138
  234. package/src/local-path-codec.ts +0 -161
  235. package/src/machine-auth.test.ts +0 -1323
  236. package/src/manifest-reconcile.test.ts +0 -1123
  237. package/src/manifest-reconcile.ts +0 -518
  238. package/src/object-io.test.ts +0 -1221
  239. package/src/object-io.ts +0 -1306
  240. package/src/operation-lock.test.ts +0 -484
  241. package/src/operation-lock.ts +0 -680
  242. package/src/outcome-telemetry.test.ts +0 -498
  243. package/src/outcome-telemetry.ts +0 -639
  244. package/src/personal-vault-exclusions.test.ts +0 -308
  245. package/src/personal-vault-exclusions.ts +0 -354
  246. package/src/personal-vault.test.ts +0 -756
  247. package/src/personal-vault.ts +0 -496
  248. package/src/prefix-coalesce.test.ts +0 -240
  249. package/src/prefix-coalesce.ts +0 -273
  250. package/src/public-surface.test.ts +0 -117
  251. package/src/qmd-reindex.test.ts +0 -877
  252. package/src/qmd-reindex.ts +0 -842
  253. package/src/read-only-state-dir.test.ts +0 -188
  254. package/src/remote-pull.test.ts +0 -1130
  255. package/src/remote-pull.ts +0 -618
  256. package/src/s3.symlink-materialize.test.ts +0 -492
  257. package/src/s3.test.ts +0 -1789
  258. package/src/s3.ts +0 -1532
  259. package/src/schemas/signal-types.test.ts +0 -82
  260. package/src/schemas/signal-types.ts +0 -38
  261. package/src/schemas/source-channels.test.ts +0 -82
  262. package/src/schemas/source-channels.ts +0 -53
  263. package/src/scope-shrink.test.ts +0 -633
  264. package/src/scope-shrink.ts +0 -481
  265. package/src/signals/get.test.ts +0 -310
  266. package/src/signals/get.ts +0 -75
  267. package/src/signals/internals.ts +0 -195
  268. package/src/signals/list.test.ts +0 -420
  269. package/src/signals/list.ts +0 -79
  270. package/src/signals/parse.ts +0 -8
  271. package/src/signals/types.ts +0 -91
  272. package/src/skill-telemetry.test.ts +0 -1825
  273. package/src/skill-telemetry.ts +0 -1439
  274. package/src/sources/get.test.ts +0 -293
  275. package/src/sources/get.ts +0 -66
  276. package/src/sources/internals.ts +0 -198
  277. package/src/sources/list.test.ts +0 -402
  278. package/src/sources/list.ts +0 -84
  279. package/src/sources/parse.ts +0 -43
  280. package/src/sources/types.ts +0 -84
  281. package/src/sync/event-sync.test.ts +0 -594
  282. package/src/sync/event-sync.ts +0 -545
  283. package/src/sync/feature-flags.test.ts +0 -378
  284. package/src/sync/feature-flags.ts +0 -62
  285. package/src/sync/index.ts +0 -76
  286. package/src/sync/lease-client.test.ts +0 -128
  287. package/src/sync/lease-client.ts +0 -207
  288. package/src/sync/logger.test.ts +0 -242
  289. package/src/sync/logger.ts +0 -79
  290. package/src/sync/metrics.test.ts +0 -462
  291. package/src/sync/metrics.ts +0 -213
  292. package/src/sync/pull-scope.ts +0 -265
  293. package/src/sync/push-event.test.ts +0 -266
  294. package/src/sync/push-event.ts +0 -224
  295. package/src/sync/push-receiver.test.ts +0 -566
  296. package/src/sync/push-receiver.ts +0 -1048
  297. package/src/sync/push-transport.ts +0 -231
  298. package/src/sync/realtime-rollout.test.ts +0 -86
  299. package/src/sync/realtime-rollout.ts +0 -262
  300. package/src/sync/state-store.test.ts +0 -194
  301. package/src/sync/state-store.ts +0 -727
  302. package/src/sync-core.ts +0 -58
  303. package/src/sync-progress.test.ts +0 -94
  304. package/src/sync-progress.ts +0 -140
  305. package/src/telemetry-events.test.ts +0 -88
  306. package/src/telemetry-events.ts +0 -205
  307. package/src/telemetry.test.ts +0 -1280
  308. package/src/telemetry.ts +0 -1109
  309. package/src/types.ts +0 -314
  310. package/src/vault-client.test.ts +0 -1380
  311. package/src/vault-client.ts +0 -1694
  312. package/src/version.ts +0 -24
  313. package/src/watch-roots.test.ts +0 -278
  314. package/src/watch-roots.ts +0 -162
  315. package/src/watcher-event-gate.test.ts +0 -212
  316. package/src/watcher.test.ts +0 -1079
  317. package/src/watcher.ts +0 -1741
  318. package/test/e2e/sync/cross-tenant-isolation.test.ts +0 -630
  319. package/test/e2e/sync/skill-telemetry-oversized-transcript.test.ts +0 -124
  320. package/test/e2e/sync/transient-company-leg.test.ts +0 -384
  321. package/test/e2e/sync/windows-unreadable-link-leg.test.ts +0 -191
  322. package/test/e2e/watcher-real-chokidar.test.ts +0 -165
  323. package/test/e2e/watcher-recursive-backend.test.ts +0 -181
  324. package/test/e2e/watcher-scoped-coverage.test.ts +0 -381
  325. package/test/invite-flow.integration.test.ts +0 -244
  326. package/test/joiner-manifest-reconcile.integration.test.ts +0 -322
  327. package/test/share-sync.integration.test.ts +0 -213
  328. package/tsconfig.json +0 -19
  329. package/vitest.config.ts +0 -22
@@ -1,106 +0,0 @@
1
- /**
2
- * Shared FILE_TOMBSTONE fetch used by BOTH the pull planner (`sync.ts`) and the
3
- * push planner (`share.ts`).
4
- *
5
- * A FILE_TOMBSTONE is the authoritative record of an intentional, scoped delete
6
- * (`hq files delete <prefix>` → `POST /v1/files/delete` → `recordDeletions`).
7
- * Both sync legs consult it so a deleted key is neither re-downloaded (pull) nor
8
- * re-uploaded (push) and therefore cannot be resurrected by a behind peer.
9
- *
10
- * Lives in its own module so `share.ts` can use the fetch without importing from
11
- * `sync.ts` — `sync.ts` already imports values from `share.ts`
12
- * (`isEphemeralPath`, `isMalformedVaultKey`), so the reverse value-import would
13
- * create a runtime ESM cycle.
14
- */
15
-
16
- import type { VaultServiceConfig } from "../types.js";
17
- import { toPosixKey } from "../s3.js";
18
-
19
- /** Timeout for the best-effort FILE_TOMBSTONE fetch (GET /v1/files/tombstones). */
20
- export const FETCH_TOMBSTONES_TIMEOUT_MS = 5000;
21
-
22
- /**
23
- * A FILE_TOMBSTONE as the planners need it: the deleted key + when it was
24
- * deleted. The `deletedAt` timestamp is the decisive precedence signal on the
25
- * pull side — a remote object newer than it is a genuine re-create (sync it), an
26
- * object at or older than it is a stale resurrection of a deleted key (suppress
27
- * it).
28
- */
29
- export interface CompanyTombstone {
30
- deletedAt: string;
31
- }
32
-
33
- /**
34
- * Fetch the company's FILE_TOMBSTONE rows from hq-pro (GET /v1/files/tombstones)
35
- * and return them as a POSIX-keyed map the planners consult to avoid
36
- * resurrecting an intentionally-deleted object (delete-resync). The endpoint is
37
- * ACL-filtered server-side, so the map only ever contains keys this caller can
38
- * read — exactly the keys that can appear in the (STS-scoped) remote LIST.
39
- *
40
- * Best-effort and bounded by a 5s timeout: a tombstone read that fails, times
41
- * out, or returns non-2xx degrades to an EMPTY map — i.e. to the pre-fix
42
- * behavior (no suppression). That is the safe failure direction: a missed
43
- * tombstone re-pulls/re-pushes a deleted file (a known, recoverable annoyance),
44
- * whereas a spurious tombstone would HIDE a file the user wants. The failure is
45
- * logged (never silently swallowed) so a persistently-degraded read is visible.
46
- */
47
- export async function fetchCompanyTombstones(
48
- vaultConfig: VaultServiceConfig,
49
- companyUid: string,
50
- ): Promise<Map<string, CompanyTombstone>> {
51
- const out = new Map<string, CompanyTombstone>();
52
- try {
53
- const token =
54
- typeof vaultConfig.authToken === "function"
55
- ? await vaultConfig.authToken()
56
- : vaultConfig.authToken;
57
- const base = vaultConfig.apiUrl.replace(/\/+$/, "");
58
- const url = `${base}/v1/files/tombstones?company=${encodeURIComponent(
59
- companyUid,
60
- )}`;
61
- const controller = new AbortController();
62
- const timer = setTimeout(
63
- () => controller.abort(),
64
- FETCH_TOMBSTONES_TIMEOUT_MS,
65
- );
66
- try {
67
- const res = await fetch(url, {
68
- method: "GET",
69
- headers: { Authorization: `Bearer ${token}` },
70
- signal: controller.signal,
71
- });
72
- if (!res.ok) {
73
- // Non-2xx is non-fatal: log and degrade to no-suppression. A 404 means
74
- // the endpoint is not deployed yet (hq-pro release lag) — the sync
75
- // proceeds with the legacy behavior until it lands.
76
- console.error(
77
- `[hq-sync] tombstone fetch returned ${res.status} (degrading to no-suppression)`,
78
- );
79
- return out;
80
- }
81
- const body = (await res.json()) as {
82
- tombstones?: Array<{ key?: string; deletedAt?: string }>;
83
- };
84
- for (const t of body.tombstones ?? []) {
85
- if (typeof t.key === "string" && typeof t.deletedAt === "string") {
86
- out.set(toPosixKey(t.key), { deletedAt: t.deletedAt });
87
- }
88
- }
89
- } finally {
90
- clearTimeout(timer);
91
- }
92
- } catch (err) {
93
- // Best-effort: a failed tombstone read must never break the sync. Log
94
- // (policy: never silently swallow) and degrade to no-suppression.
95
- try {
96
- console.error(
97
- `[hq-sync] tombstone fetch failed (non-fatal, degrading to no-suppression): ${
98
- err instanceof Error ? err.message : String(err)
99
- }`,
100
- );
101
- } catch {
102
- // swallow — logging must never break sync
103
- }
104
- }
105
- return out;
106
- }
@@ -1,234 +0,0 @@
1
- /**
2
- * Regression guard: the watch + event-push path must NOT mint phantom
3
- * `.conflict-*` mirrors for an ordinary single-writer local edit.
4
- *
5
- * Background (feedback_ef2b7c8c): a user editing files under
6
- * `companies/{slug}/` saw the menubar's watch/event-push runner mint a
7
- * `.conflict-<ts>-<machine>` mirror on EVERY ordinary local Edit/Write —
8
- * thousands of phantom files in a single session. The watch/event-push runner
9
- * reacts to a local change by running a targeted `--direction push` pass, i.e.
10
- * `share()` over the changed company subtree, so the conflict-minting logic is
11
- * share()'s push-side conflict gate. The original gate flagged a conflict on
12
- * `localChanged` alone (`journalEntry.hash !== localHash`), which fires for
13
- * every edit of any already-synced file. The gate now requires
14
- * `(localChanged && remoteChanged) || isFreshCollision`: a single writer never
15
- * has `remoteChanged` (the live S3 ETag still equals the journal's recorded
16
- * baseline), so an ordinary edit is uploaded, not mirrored.
17
- *
18
- * This test pins that contract end-to-end through `share()` using the same
19
- * mocked-S3 harness as `share.test.ts`, modelling a single-writer remote whose
20
- * ETag is always exactly what THIS device last uploaded. A positive control
21
- * (a genuine out-of-band peer write) proves the guard is not trivially
22
- * always-zero: real divergence is still detected.
23
- */
24
-
25
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
26
- import * as fs from "fs";
27
- import * as path from "path";
28
- import * as os from "os";
29
- import { clearContextCache } from "../context.js";
30
- import type { VaultServiceConfig } from "../types.js";
31
-
32
- vi.mock("../s3.js", () => ({
33
- toPosixKey: (key: string) => key.split("\\").join("/"),
34
- uploadFile: vi.fn(),
35
- uploadSymlink: vi.fn().mockResolvedValue({ etag: '"upload-symlink-etag"' }),
36
- downloadFile: vi.fn().mockResolvedValue(undefined),
37
- listRemoteFiles: vi.fn().mockResolvedValue([]),
38
- deleteRemoteFile: vi.fn().mockResolvedValue(undefined),
39
- headRemoteFile: vi.fn(),
40
- primeObjectTransport: vi.fn().mockResolvedValue(undefined),
41
- primeUploads: vi.fn().mockResolvedValue(undefined),
42
- }));
43
-
44
- vi.mock("readline", () => ({
45
- createInterface: vi.fn(() => ({ question: vi.fn(), close: vi.fn() })),
46
- }));
47
-
48
- import { share } from "./share.js";
49
- import { downloadFile, headRemoteFile, uploadFile } from "../s3.js";
50
-
51
- const mockConfig: VaultServiceConfig = {
52
- apiUrl: "https://vault-api.test",
53
- authToken: "test-jwt-token",
54
- region: "us-east-1",
55
- };
56
-
57
- const mockEntity = {
58
- uid: "cmp_01ABCDEF",
59
- slug: "acme",
60
- bucketName: "hq-vault-acme-123",
61
- status: "active",
62
- };
63
-
64
- function setupFetchMock() {
65
- vi.stubGlobal(
66
- "fetch",
67
- vi.fn().mockImplementation(async (url: string) => {
68
- const u = String(url);
69
- if (u.includes("/entity/check-slug/me"))
70
- return {
71
- ok: true,
72
- status: 200,
73
- json: async () => ({ available: false, conflictingCompanyUid: mockEntity.uid }),
74
- text: async () => "",
75
- };
76
- if (u.includes("/entity/by-slug/") || /\/entity\/cmp_/.test(u))
77
- return { ok: true, status: 200, json: async () => ({ entity: mockEntity }), text: async () => "" };
78
- if (u.includes("/sts/vend"))
79
- return {
80
- ok: true,
81
- status: 200,
82
- json: async () => ({
83
- credentials: {
84
- accessKeyId: "ASIA_TEST",
85
- secretAccessKey: "s",
86
- sessionToken: "t",
87
- expiration: new Date(Date.now() + 9e5).toISOString(),
88
- },
89
- expiresAt: new Date(Date.now() + 9e5).toISOString(),
90
- }),
91
- text: async () => "",
92
- };
93
- return { ok: false, status: 404, text: async () => "Not found" };
94
- }),
95
- );
96
- }
97
-
98
- /** Every `.conflict-<ts>-*` mirror file anywhere under `root`. */
99
- function countConflictMirrors(root: string): string[] {
100
- const out: string[] = [];
101
- const walk = (d: string) => {
102
- for (const e of fs.readdirSync(d, { withFileTypes: true })) {
103
- const p = path.join(d, e.name);
104
- if (e.isDirectory()) walk(p);
105
- else if (/\.conflict-/.test(e.name)) out.push(p);
106
- }
107
- };
108
- walk(root);
109
- return out;
110
- }
111
-
112
- /**
113
- * In-memory single-writer S3: the object's ETag is always exactly what THIS
114
- * device last uploaded (no peer ever writes). `lastModified` advances on each
115
- * PUT to model S3 stamping it server-side. The S3-module signatures are
116
- * `uploadFile(ctx, localPath, key)`, `headRemoteFile(ctx, key)`,
117
- * `downloadFile(ctx, key, dest)` — wire the mocks to match exactly.
118
- */
119
- function wireSingleWriterRemote() {
120
- const remote = new Map<string, { etag: string; lastModified: Date; size: number }>();
121
- let clock = Date.now();
122
- let seq = 0;
123
- vi.mocked(uploadFile).mockImplementation(async (_ctx: unknown, localPath: string, key: string) => {
124
- const etag = `"etag-${seq++}"`;
125
- clock += 1000;
126
- remote.set(key, { etag, lastModified: new Date(clock), size: fs.statSync(localPath).size });
127
- return { etag };
128
- });
129
- vi.mocked(headRemoteFile).mockImplementation(async (_ctx: unknown, key: string) => {
130
- const r = remote.get(key);
131
- return r ? { lastModified: r.lastModified, etag: r.etag, size: r.size } : null;
132
- });
133
- vi.mocked(downloadFile).mockImplementation(async (_ctx: unknown, _key: string, dest: string) => {
134
- fs.mkdirSync(path.dirname(dest), { recursive: true });
135
- fs.writeFileSync(dest, "remote-bytes\n");
136
- return {};
137
- });
138
- return remote;
139
- }
140
-
141
- describe("watch + event-push conflict regression (feedback_ef2b7c8c)", () => {
142
- let tmpDir: string;
143
- let stateDir: string;
144
-
145
- beforeEach(() => {
146
- clearContextCache();
147
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-watch-conflict-"));
148
- stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-watch-state-"));
149
- process.env.HQ_STATE_DIR = stateDir;
150
- setupFetchMock();
151
- });
152
-
153
- afterEach(() => {
154
- vi.unstubAllGlobals();
155
- vi.clearAllMocks();
156
- fs.rmSync(tmpDir, { recursive: true, force: true });
157
- fs.rmSync(stateDir, { recursive: true, force: true });
158
- delete process.env.HQ_STATE_DIR;
159
- });
160
-
161
- it("a single writer's repeated local edits mint ZERO .conflict-* files", async () => {
162
- const companyRoot = path.join(tmpDir, "companies", "acme");
163
- fs.mkdirSync(companyRoot, { recursive: true });
164
- const files = ["notes.md", "plan.md", "ideas.md"].map((n) => path.join(companyRoot, n));
165
- for (const f of files) fs.writeFileSync(f, "v0\n");
166
-
167
- wireSingleWriterRemote();
168
-
169
- const conflictPathsSeen: string[] = [];
170
- // The event-push runner pushes the changed company subtree. Mirror that:
171
- // share() over the company root on every settled change.
172
- const eventPush = async () => {
173
- const r = await share({
174
- paths: [companyRoot],
175
- company: "acme",
176
- vaultConfig: mockConfig,
177
- hqRoot: tmpDir,
178
- onConflict: "keep",
179
- onEvent: () => {},
180
- });
181
- conflictPathsSeen.push(...r.conflictPaths);
182
- };
183
-
184
- // Initial sync establishes the journal baseline (first upload of each file).
185
- await eventPush();
186
-
187
- // An editing session: 30 ordinary local edits, round-robin across the
188
- // files, each followed by its targeted event-push.
189
- for (let i = 0; i < 30; i++) {
190
- fs.writeFileSync(files[i % files.length], `v${i + 1}\n`);
191
- await eventPush();
192
- }
193
-
194
- expect(countConflictMirrors(tmpDir)).toEqual([]);
195
- expect(conflictPathsSeen).toEqual([]);
196
- });
197
-
198
- it("positive control: a genuine peer write is still detected as a conflict", async () => {
199
- const companyRoot = path.join(tmpDir, "companies", "acme");
200
- fs.mkdirSync(companyRoot, { recursive: true });
201
- const file = path.join(companyRoot, "contested.md");
202
- fs.writeFileSync(file, "v0\n");
203
-
204
- const remote = wireSingleWriterRemote();
205
-
206
- const conflictPathsSeen: string[] = [];
207
- const eventPush = async () => {
208
- const r = await share({
209
- paths: [companyRoot],
210
- company: "acme",
211
- vaultConfig: mockConfig,
212
- hqRoot: tmpDir,
213
- onConflict: "keep",
214
- onEvent: () => {},
215
- });
216
- conflictPathsSeen.push(...r.conflictPaths);
217
- };
218
-
219
- await eventPush(); // baseline
220
-
221
- // A peer advances the remote object out-of-band to DIFFERENT bytes. Journal
222
- // keys are company-relative, so the key is "contested.md".
223
- remote.set("contested.md", { etag: '"peer-etag"', lastModified: new Date(Date.now() + 60_000), size: 99 });
224
- // ...and we also edit locally → both sides moved → a genuine conflict.
225
- fs.writeFileSync(file, "v1-local\n");
226
- await eventPush();
227
-
228
- // Existing-entry push conflicts surface via conflictPaths (the inspection
229
- // mirror is written by the pull leg). The contract under test: a REAL
230
- // divergence is still flagged — the single-writer guard above did not
231
- // over-correct into swallowing true conflicts.
232
- expect(conflictPathsSeen).toContain("contested.md");
233
- });
234
- });
@@ -1,214 +0,0 @@
1
- /**
2
- * Tests for the client-info helpers and header injection.
3
- *
4
- * Two layers are exercised here:
5
- * 1. The pure functions in `client-info.ts` — buildClientHeaders,
6
- * clientInfoFromPackage, detectHqCoreVersion.
7
- * 2. End-to-end injection into VaultClient.request, since "the headers
8
- * actually land on outbound fetch" is the property consumers care about.
9
- */
10
-
11
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
12
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
13
- import { tmpdir } from "node:os";
14
- import { join } from "node:path";
15
- import {
16
- buildClientHeaders,
17
- clientInfoFromPackage,
18
- detectHqCoreVersion,
19
- HEADER_CLIENT_NAME,
20
- HEADER_CLIENT_VERSION,
21
- HEADER_HQ_CORE_VERSION,
22
- } from "./client-info.js";
23
- import { VaultClient } from "./vault-client.js";
24
-
25
- describe("buildClientHeaders", () => {
26
- it("returns empty object when info is undefined", () => {
27
- expect(buildClientHeaders(undefined)).toEqual({});
28
- });
29
-
30
- it("emits User-Agent + x-hq-client-{name,version} from name/version", () => {
31
- const headers = buildClientHeaders({
32
- name: "@indigoai-us/hq-cli",
33
- version: "5.15.0",
34
- });
35
- expect(headers["User-Agent"]).toBe("@indigoai-us/hq-cli/5.15.0");
36
- expect(headers[HEADER_CLIENT_NAME]).toBe("@indigoai-us/hq-cli");
37
- expect(headers[HEADER_CLIENT_VERSION]).toBe("5.15.0");
38
- expect(headers[HEADER_HQ_CORE_VERSION]).toBeUndefined();
39
- });
40
-
41
- it("includes x-hq-core-version only when hqCoreVersion is set", () => {
42
- const headers = buildClientHeaders({
43
- name: "@indigoai-us/hq-cli",
44
- version: "5.15.0",
45
- hqCoreVersion: "14.1.0",
46
- });
47
- expect(headers[HEADER_HQ_CORE_VERSION]).toBe("14.1.0");
48
- });
49
-
50
- it("emits arbitrary extra fields as x-hq-client-<key> headers", () => {
51
- const headers = buildClientHeaders({
52
- name: "x",
53
- version: "1.0.0",
54
- extra: { machine: "ec2-bot-7", channel: "stable" },
55
- });
56
- expect(headers["x-hq-client-machine"]).toBe("ec2-bot-7");
57
- expect(headers["x-hq-client-channel"]).toBe("stable");
58
- });
59
- });
60
-
61
- describe("clientInfoFromPackage", () => {
62
- it("extracts name and version from a parsed package.json", () => {
63
- expect(clientInfoFromPackage({ name: "foo", version: "1.2.3" })).toEqual({
64
- name: "foo",
65
- version: "1.2.3",
66
- });
67
- });
68
-
69
- it("throws when name is missing", () => {
70
- expect(() => clientInfoFromPackage({ version: "1.0.0" })).toThrow(/name/);
71
- });
72
-
73
- it("throws when version is missing", () => {
74
- expect(() => clientInfoFromPackage({ name: "foo" })).toThrow(/version/);
75
- });
76
-
77
- it("throws on non-string fields", () => {
78
- expect(() =>
79
- clientInfoFromPackage({ name: 42 as unknown as string, version: "1.0.0" }),
80
- ).toThrow();
81
- });
82
- });
83
-
84
- describe("detectHqCoreVersion", () => {
85
- let tmpRoot: string;
86
- const origHqHome = process.env.HQ_HOME;
87
- const origHqRoot = process.env.HQ_ROOT;
88
-
89
- beforeEach(() => {
90
- tmpRoot = mkdtempSync(join(tmpdir(), "hq-core-detect-"));
91
- delete process.env.HQ_HOME;
92
- delete process.env.HQ_ROOT;
93
- });
94
-
95
- afterEach(() => {
96
- rmSync(tmpRoot, { recursive: true, force: true });
97
- if (origHqHome !== undefined) process.env.HQ_HOME = origHqHome;
98
- if (origHqRoot !== undefined) process.env.HQ_ROOT = origHqRoot;
99
- });
100
-
101
- function seedHqCore(root: string, version: string): void {
102
- mkdirSync(join(root, "core"), { recursive: true });
103
- mkdirSync(join(root, "companies"), { recursive: true });
104
- writeFileSync(
105
- join(root, "core", "core.yaml"),
106
- `version: 1\nhqVersion: "${version}"\nupdatedAt: "2026-05-13T00:00:00Z"\n`,
107
- );
108
- }
109
-
110
- it("returns undefined when nothing on the walk-up has core.yaml", () => {
111
- expect(detectHqCoreVersion(tmpRoot)).toBeUndefined();
112
- });
113
-
114
- it("returns hqVersion when startDir is the hq-core root", () => {
115
- seedHqCore(tmpRoot, "14.1.0");
116
- expect(detectHqCoreVersion(tmpRoot)).toBe("14.1.0");
117
- });
118
-
119
- it("walks upward to find core.yaml from a nested cwd", () => {
120
- seedHqCore(tmpRoot, "14.2.0");
121
- const nested = join(tmpRoot, "companies", "acme", "projects", "p1");
122
- mkdirSync(nested, { recursive: true });
123
- expect(detectHqCoreVersion(nested)).toBe("14.2.0");
124
- });
125
-
126
- it("ignores directories that have core.yaml but no companies/ — disambiguates fixtures", () => {
127
- mkdirSync(join(tmpRoot, "core"), { recursive: true });
128
- writeFileSync(
129
- join(tmpRoot, "core", "core.yaml"),
130
- `version: 1\nhqVersion: "99.0.0"\n`,
131
- );
132
- // No companies/ at this level → not an hq-core root.
133
- expect(detectHqCoreVersion(tmpRoot)).toBeUndefined();
134
- });
135
-
136
- it("honors HQ_HOME env override before walking cwd", () => {
137
- seedHqCore(tmpRoot, "14.3.0");
138
- process.env.HQ_HOME = tmpRoot;
139
- // startDir intentionally points elsewhere — env should win.
140
- const elsewhere = mkdtempSync(join(tmpdir(), "elsewhere-"));
141
- try {
142
- expect(detectHqCoreVersion(elsewhere)).toBe("14.3.0");
143
- } finally {
144
- rmSync(elsewhere, { recursive: true, force: true });
145
- }
146
- });
147
-
148
- it("parses unquoted hqVersion values too", () => {
149
- mkdirSync(join(tmpRoot, "core"), { recursive: true });
150
- mkdirSync(join(tmpRoot, "companies"), { recursive: true });
151
- writeFileSync(
152
- join(tmpRoot, "core", "core.yaml"),
153
- `version: 1\nhqVersion: 14.4.0\n`,
154
- );
155
- expect(detectHqCoreVersion(tmpRoot)).toBe("14.4.0");
156
- });
157
- });
158
-
159
- describe("VaultClient stamps client headers on outbound requests", () => {
160
- afterEach(() => {
161
- vi.restoreAllMocks();
162
- });
163
-
164
- it("includes x-hq-client-name + x-hq-client-version when clientInfo is set", async () => {
165
- const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
166
- new Response(JSON.stringify({ memberships: [] }), {
167
- status: 200,
168
- headers: { "Content-Type": "application/json" },
169
- }),
170
- );
171
-
172
- const client = new VaultClient({
173
- apiUrl: "https://vault.test.example.com",
174
- authToken: "tok",
175
- clientInfo: {
176
- name: "@indigoai-us/hq-cli",
177
- version: "5.15.0",
178
- hqCoreVersion: "14.1.0",
179
- },
180
- });
181
-
182
- await client.listMyMemberships();
183
-
184
- expect(fetchSpy).toHaveBeenCalledOnce();
185
- const init = fetchSpy.mock.calls[0]?.[1] as RequestInit;
186
- const headers = init.headers as Record<string, string>;
187
- expect(headers[HEADER_CLIENT_NAME]).toBe("@indigoai-us/hq-cli");
188
- expect(headers[HEADER_CLIENT_VERSION]).toBe("5.15.0");
189
- expect(headers[HEADER_HQ_CORE_VERSION]).toBe("14.1.0");
190
- expect(headers["User-Agent"]).toBe("@indigoai-us/hq-cli/5.15.0");
191
- });
192
-
193
- it("omits client headers when clientInfo is not set — back-compat", async () => {
194
- const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
195
- new Response(JSON.stringify({ memberships: [] }), {
196
- status: 200,
197
- headers: { "Content-Type": "application/json" },
198
- }),
199
- );
200
-
201
- const client = new VaultClient({
202
- apiUrl: "https://vault.test.example.com",
203
- authToken: "tok",
204
- });
205
-
206
- await client.listMyMemberships();
207
-
208
- const headers = (fetchSpy.mock.calls[0]?.[1] as RequestInit)
209
- .headers as Record<string, string>;
210
- expect(headers[HEADER_CLIENT_NAME]).toBeUndefined();
211
- expect(headers[HEADER_CLIENT_VERSION]).toBeUndefined();
212
- expect(headers["User-Agent"]).toBeUndefined();
213
- });
214
- });
@@ -1,121 +0,0 @@
1
- /**
2
- * Client identification — every HQ client that talks to hq-cloud-api should
3
- * stamp its name and version on outbound requests so the server can attribute
4
- * traffic, gate on minimum versions, and surface deprecation warnings.
5
- *
6
- * The CLI in particular sends a third header (`x-hq-core-version`) when it's
7
- * invoked from inside an hq-core checkout, so the server sees which scaffold
8
- * generation the user is running against.
9
- */
10
-
11
- import { readFileSync } from "node:fs";
12
- import { dirname, join, resolve } from "node:path";
13
- import type { ClientInfo } from "./types.js";
14
-
15
- export const HEADER_CLIENT_NAME = "x-hq-client-name";
16
- export const HEADER_CLIENT_VERSION = "x-hq-client-version";
17
- export const HEADER_HQ_CORE_VERSION = "x-hq-core-version";
18
-
19
- /**
20
- * Build the set of `x-hq-*` headers (plus a derived `User-Agent`) for a
21
- * ClientInfo. Returns an empty object when info is undefined so callers can
22
- * spread the result unconditionally.
23
- */
24
- export function buildClientHeaders(
25
- info: ClientInfo | undefined,
26
- ): Record<string, string> {
27
- if (!info) return {};
28
-
29
- const headers: Record<string, string> = {
30
- "User-Agent": `${info.name}/${info.version}`,
31
- [HEADER_CLIENT_NAME]: info.name,
32
- [HEADER_CLIENT_VERSION]: info.version,
33
- };
34
-
35
- if (info.hqCoreVersion) {
36
- headers[HEADER_HQ_CORE_VERSION] = info.hqCoreVersion;
37
- }
38
-
39
- if (info.extra) {
40
- for (const [k, v] of Object.entries(info.extra)) {
41
- headers[`x-hq-client-${k}`] = v;
42
- }
43
- }
44
-
45
- return headers;
46
- }
47
-
48
- /**
49
- * Build a ClientInfo from a parsed package.json. Most consumers will call this
50
- * once at startup and pass the result into every VaultServiceConfig.
51
- */
52
- export function clientInfoFromPackage(pkg: {
53
- name?: unknown;
54
- version?: unknown;
55
- }): ClientInfo {
56
- if (typeof pkg.name !== "string" || pkg.name.length === 0) {
57
- throw new Error("clientInfoFromPackage: package.json is missing a name");
58
- }
59
- if (typeof pkg.version !== "string" || pkg.version.length === 0) {
60
- throw new Error("clientInfoFromPackage: package.json is missing a version");
61
- }
62
- return { name: pkg.name, version: pkg.version };
63
- }
64
-
65
- /**
66
- * Walk upward from `startDir` looking for `core/core.yaml`. When found, parse
67
- * out the `hqVersion` field and return it. Returns undefined if we never find
68
- * an hq-core checkout — i.e. the caller is running from a plain user dir.
69
- *
70
- * Honors `HQ_HOME` env var as an explicit override so multi-checkout setups
71
- * (e.g. CI bots, the menubar app pointing at a non-cwd HQ root) can pin the
72
- * detection without relying on cwd.
73
- *
74
- * Why a regex instead of a YAML parser: `core.yaml` lives at the very top of
75
- * the file and the field has a stable shape — adding a YAML dep just to read
76
- * one string would balloon every consumer's bundle. The current shape is
77
- * `hqVersion: "X.Y.Z"` (quoted) per the canonical seed; we tolerate unquoted
78
- * too.
79
- */
80
- export function detectHqCoreVersion(startDir?: string): string | undefined {
81
- const fromEnv = process.env.HQ_HOME ?? process.env.HQ_ROOT;
82
- if (fromEnv) {
83
- const v = readCoreVersionAt(fromEnv);
84
- if (v) return v;
85
- }
86
-
87
- let dir = resolve(startDir ?? process.cwd());
88
- while (true) {
89
- const v = readCoreVersionAt(dir);
90
- if (v) return v;
91
- const parent = dirname(dir);
92
- if (parent === dir) return undefined;
93
- dir = parent;
94
- }
95
- }
96
-
97
- function readCoreVersionAt(hqRoot: string): string | undefined {
98
- // hq-core identity requires BOTH core/core.yaml AND companies/ — matches the
99
- // CLI's own detection (commit bc827d0). Without this guard, any directory
100
- // containing a stray `core/core.yaml` (e.g. a test fixture) would be
101
- // misidentified as an hq-core root.
102
- const yamlPath = join(hqRoot, "core", "core.yaml");
103
- const companiesPath = join(hqRoot, "companies");
104
- let content: string;
105
- try {
106
- content = readFileSync(yamlPath, "utf8");
107
- } catch {
108
- return undefined;
109
- }
110
- try {
111
- // statSync would be marginally cleaner, but readdirSync of a nonexistent
112
- // path throws synchronously which is what we want.
113
- readFileSync(companiesPath, { flag: "r" });
114
- } catch (err) {
115
- const e = err as NodeJS.ErrnoException;
116
- // EISDIR is the success case — companies/ exists and is a directory.
117
- if (e.code !== "EISDIR") return undefined;
118
- }
119
- const match = /^hqVersion:\s*["']?([^"'\s]+)/m.exec(content);
120
- return match ? match[1] : undefined;
121
- }