@indigoai-us/hq-cli 5.77.14 → 5.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/assets/scaffold/core/scripts/archive-old-threads.sh +86 -0
  3. package/assets/scaffold/core/scripts/backfill-company-skill-mirrors.sh +73 -0
  4. package/assets/scaffold/core/scripts/backfill-workspace-mirror.sh +51 -0
  5. package/assets/scaffold/core/scripts/codex-skill-bridge.sh +550 -0
  6. package/assets/scaffold/core/scripts/detect-stale-core-policy-mirror.sh +185 -0
  7. package/assets/scaffold/core/scripts/hq-status-summary.sh +187 -0
  8. package/assets/scaffold/core/scripts/ontology-readme-drift.sh +264 -0
  9. package/assets/scaffold/core/scripts/qmd-reindex-after-sync.sh +93 -0
  10. package/assets/scaffold/core/scripts/rebuild-all-indexes.sh +69 -0
  11. package/assets/scaffold/core/scripts/rebuild-companies-index.sh +206 -0
  12. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +127 -0
  13. package/assets/scaffold/core/scripts/rebuild-orchestrator-index.sh +86 -0
  14. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +105 -0
  15. package/assets/scaffold/core/scripts/rebuild-public-knowledge-index.sh +83 -0
  16. package/assets/scaffold/core/scripts/rebuild-reports-index.sh +114 -0
  17. package/assets/scaffold/core/scripts/rebuild-social-drafts-index.sh +135 -0
  18. package/assets/scaffold/core/scripts/rebuild-threads-index.sh +82 -0
  19. package/assets/scaffold/core/scripts/rebuild-workers-index.sh +97 -0
  20. package/assets/scaffold/core/scripts/resize-screenshot.sh +47 -0
  21. package/assets/scaffold/core/scripts/token-usage-report.sh +254 -0
  22. package/assets/scaffold/core/scripts/worktree.sh +136 -0
  23. package/dist/commands/agents.js +28 -12
  24. package/dist/commands/api-keys.js +100 -20
  25. package/dist/commands/core.d.ts +90 -0
  26. package/dist/commands/core.js +279 -0
  27. package/dist/commands/files-browse.d.ts +21 -0
  28. package/dist/commands/files-browse.js +145 -24
  29. package/dist/commands/files.js +33 -7
  30. package/dist/commands/meetings.js +16 -9
  31. package/dist/commands/members.js +16 -5
  32. package/dist/commands/pack-install.d.ts +14 -0
  33. package/dist/commands/pack-install.js +42 -2
  34. package/dist/commands/pkg-install.js +39 -2
  35. package/dist/commands/run.js +6 -2
  36. package/dist/commands/secrets.js +120 -65
  37. package/dist/main.js +6 -0
  38. package/dist/utils/api-key-command-gate.d.ts +45 -0
  39. package/dist/utils/api-key-command-gate.js +51 -0
  40. package/dist/utils/hq-roots.d.ts +91 -0
  41. package/dist/utils/hq-roots.js +212 -0
  42. package/dist/utils/resolve-vault-credential.d.ts +63 -3
  43. package/dist/utils/resolve-vault-credential.js +152 -6
  44. package/dist/utils/run-bundled-script.d.ts +66 -0
  45. package/dist/utils/run-bundled-script.js +80 -0
  46. package/dist/utils/vault-api.d.ts +21 -0
  47. package/dist/utils/vault-api.js +74 -3
  48. package/package.json +7 -1
  49. package/.claude/policies/hq-cli-interactive-reads-use-sts-vend.md +0 -49
  50. package/.github/workflows/ci.yml +0 -46
  51. package/.github/workflows/publish.yml +0 -36
  52. package/.github/workflows/scripts/smoke-test-pkg.sh +0 -97
  53. package/e2e/cli.test.ts +0 -128
  54. package/eslint.config.js +0 -37
  55. package/pnpm-workspace.yaml +0 -4
  56. package/scripts/chmod-bins.mjs +0 -49
  57. package/scripts/generate-dsn.mjs +0 -26
  58. package/scripts/generate-scan-packages-table.mjs +0 -113
  59. package/scripts/smoke-sources-signals.sh +0 -103
  60. package/src/bin/hq-auth-refresh.ts +0 -43
  61. package/src/cli-version.ts +0 -13
  62. package/src/commands/__fixtures__/make-tar.ts +0 -126
  63. package/src/commands/_patterns.ts +0 -10
  64. package/src/commands/add.ts +0 -75
  65. package/src/commands/agents.test.ts +0 -527
  66. package/src/commands/agents.ts +0 -923
  67. package/src/commands/api-keys.test.ts +0 -291
  68. package/src/commands/api-keys.ts +0 -382
  69. package/src/commands/artifact-verify.test.ts +0 -177
  70. package/src/commands/auth.test.ts +0 -82
  71. package/src/commands/auth.ts +0 -213
  72. package/src/commands/billing.test.ts +0 -158
  73. package/src/commands/billing.ts +0 -146
  74. package/src/commands/channels.test.ts +0 -74
  75. package/src/commands/channels.ts +0 -91
  76. package/src/commands/cloud-demote.test.ts +0 -401
  77. package/src/commands/cloud-demote.ts +0 -278
  78. package/src/commands/cloud-provision.test.ts +0 -1117
  79. package/src/commands/cloud-provision.ts +0 -942
  80. package/src/commands/cloud.pull-all.test.ts +0 -711
  81. package/src/commands/cloud.pull-per-company.test.ts +0 -188
  82. package/src/commands/cloud.push-all.test.ts +0 -485
  83. package/src/commands/cloud.scope-excluded-warning.test.ts +0 -22
  84. package/src/commands/cloud.selectors.test.ts +0 -208
  85. package/src/commands/cloud.status.test.ts +0 -160
  86. package/src/commands/cloud.test.ts +0 -41
  87. package/src/commands/cloud.ts +0 -2130
  88. package/src/commands/company.test.ts +0 -202
  89. package/src/commands/company.ts +0 -148
  90. package/src/commands/creators.test.ts +0 -60
  91. package/src/commands/creators.ts +0 -117
  92. package/src/commands/crm.test.ts +0 -179
  93. package/src/commands/crm.ts +0 -236
  94. package/src/commands/db-migrate.ts +0 -55
  95. package/src/commands/db-provision.ts +0 -114
  96. package/src/commands/db-sql.ts +0 -124
  97. package/src/commands/db-status.ts +0 -108
  98. package/src/commands/db.ts +0 -26
  99. package/src/commands/dm.test.ts +0 -748
  100. package/src/commands/dm.ts +0 -1226
  101. package/src/commands/feedback.test.ts +0 -413
  102. package/src/commands/feedback.ts +0 -169
  103. package/src/commands/files-browse.test.ts +0 -1249
  104. package/src/commands/files-browse.ts +0 -1506
  105. package/src/commands/files-delete.test.ts +0 -484
  106. package/src/commands/files-recovery.test.ts +0 -370
  107. package/src/commands/files.test.ts +0 -722
  108. package/src/commands/files.ts +0 -1446
  109. package/src/commands/group-grants.test.ts +0 -330
  110. package/src/commands/group-grants.ts +0 -455
  111. package/src/commands/groups.test.ts +0 -44
  112. package/src/commands/groups.ts +0 -368
  113. package/src/commands/integrations.test.ts +0 -633
  114. package/src/commands/integrations.ts +0 -507
  115. package/src/commands/list.ts +0 -85
  116. package/src/commands/login.ts +0 -52
  117. package/src/commands/logout.ts +0 -30
  118. package/src/commands/marketplace-install.test.ts +0 -543
  119. package/src/commands/marketplace-security.test.ts +0 -646
  120. package/src/commands/mcp-registration.test.ts +0 -2787
  121. package/src/commands/mcp-registration.ts +0 -2612
  122. package/src/commands/mcp-status.test.ts +0 -483
  123. package/src/commands/mcp-status.ts +0 -575
  124. package/src/commands/mcp-status.us011.test.ts +0 -243
  125. package/src/commands/meetings.test.ts +0 -522
  126. package/src/commands/meetings.ts +0 -789
  127. package/src/commands/members.test.ts +0 -1474
  128. package/src/commands/members.ts +0 -1181
  129. package/src/commands/onboard-identity-guard.test.ts +0 -99
  130. package/src/commands/onboard-identity-guard.ts +0 -103
  131. package/src/commands/onboard-join.test.ts +0 -96
  132. package/src/commands/onboard-join.ts +0 -101
  133. package/src/commands/onboard-warning.test.ts +0 -26
  134. package/src/commands/onboard-warning.ts +0 -12
  135. package/src/commands/onboard.ts +0 -267
  136. package/src/commands/outposts-heartbeat.test.ts +0 -299
  137. package/src/commands/outposts-heartbeat.ts +0 -310
  138. package/src/commands/outposts-replica-sync.test.ts +0 -272
  139. package/src/commands/outposts-self-deploy.test.ts +0 -243
  140. package/src/commands/outposts.test.ts +0 -1039
  141. package/src/commands/outposts.ts +0 -1935
  142. package/src/commands/pack-install-secret-authorization.test.ts +0 -115
  143. package/src/commands/pack-install.test.ts +0 -1469
  144. package/src/commands/pack-install.ts +0 -2256
  145. package/src/commands/pack-update-cache.test.ts +0 -149
  146. package/src/commands/packs-update-api-key.test.ts +0 -105
  147. package/src/commands/packs.test.ts +0 -88
  148. package/src/commands/packs.ts +0 -622
  149. package/src/commands/people.test.ts +0 -807
  150. package/src/commands/people.ts +0 -422
  151. package/src/commands/pkg-install.dispatch.test.ts +0 -103
  152. package/src/commands/pkg-install.ts +0 -221
  153. package/src/commands/pkg-list.test.ts +0 -79
  154. package/src/commands/pkg-list.ts +0 -13
  155. package/src/commands/pkg-remove.ts +0 -71
  156. package/src/commands/pkg-update.ts +0 -189
  157. package/src/commands/publish.test.ts +0 -650
  158. package/src/commands/publish.ts +0 -580
  159. package/src/commands/reindex.test.ts +0 -259
  160. package/src/commands/reindex.ts +0 -274
  161. package/src/commands/rescue.test.ts +0 -39
  162. package/src/commands/rescue.ts +0 -210
  163. package/src/commands/run.env-local.test.ts +0 -88
  164. package/src/commands/run.test.ts +0 -45
  165. package/src/commands/run.ts +0 -190
  166. package/src/commands/safe-extract.test.ts +0 -459
  167. package/src/commands/safe-extract.ts +0 -444
  168. package/src/commands/secrets-scope.test.ts +0 -56
  169. package/src/commands/secrets-scope.ts +0 -32
  170. package/src/commands/secrets.parse-destination.test.ts +0 -38
  171. package/src/commands/secrets.test.ts +0 -2050
  172. package/src/commands/secrets.ts +0 -2249
  173. package/src/commands/signals.ts +0 -351
  174. package/src/commands/skill.test.ts +0 -770
  175. package/src/commands/skill.ts +0 -796
  176. package/src/commands/sources.ts +0 -362
  177. package/src/commands/sync-mode.test.ts +0 -366
  178. package/src/commands/sync-mode.ts +0 -387
  179. package/src/commands/sync-narrow.test.ts +0 -633
  180. package/src/commands/sync-narrow.ts +0 -555
  181. package/src/commands/sync.ts +0 -165
  182. package/src/commands/team-sync.ts +0 -628
  183. package/src/commands/update.ts +0 -71
  184. package/src/commands/whoami.ts +0 -83
  185. package/src/commands/workers.test.ts +0 -158
  186. package/src/commands/workers.ts +0 -301
  187. package/src/index.test.ts +0 -32
  188. package/src/index.ts +0 -20
  189. package/src/lib/db/control-plane.test.ts +0 -59
  190. package/src/lib/db/control-plane.ts +0 -113
  191. package/src/lib/db/local.test.ts +0 -81
  192. package/src/lib/db/local.ts +0 -148
  193. package/src/lib/db/migrate.test.ts +0 -133
  194. package/src/lib/db/migrate.ts +0 -137
  195. package/src/lib/db/paths.test.ts +0 -112
  196. package/src/lib/db/paths.ts +0 -128
  197. package/src/lib/db/remote-engine.test.ts +0 -44
  198. package/src/lib/db/remote-engine.ts +0 -148
  199. package/src/lib/db/remote-sql.test.ts +0 -32
  200. package/src/lib/db/remote-sql.ts +0 -62
  201. package/src/lib/db/sql.test.ts +0 -106
  202. package/src/lib/db/sql.ts +0 -192
  203. package/src/lib/hq-cloud-dep.smoke.test.ts +0 -75
  204. package/src/lib/local-tree-diff.test.ts +0 -301
  205. package/src/lib/local-tree-diff.ts +0 -362
  206. package/src/lib/narrow-hint-banner.test.ts +0 -235
  207. package/src/lib/narrow-hint-banner.ts +0 -212
  208. package/src/lib/plan-limit-nag.test.ts +0 -317
  209. package/src/lib/plan-limit-nag.ts +0 -264
  210. package/src/main.ts +0 -371
  211. package/src/node-preflight.test.ts +0 -60
  212. package/src/node-preflight.ts +0 -67
  213. package/src/outpost/session-heartbeat-bounds.test.ts +0 -195
  214. package/src/outpost/session-heartbeat-guard.test.ts +0 -105
  215. package/src/outpost/session-heartbeat-publisher.test.ts +0 -178
  216. package/src/outpost/session-heartbeat-publisher.ts +0 -186
  217. package/src/outpost/session-heartbeat-retain-guard.test.ts +0 -126
  218. package/src/outpost/session-heartbeat.test.ts +0 -459
  219. package/src/outpost/session-heartbeat.ts +0 -877
  220. package/src/packaging.test.ts +0 -109
  221. package/src/run/__fixtures__/discover-schemas/example.env.schema +0 -4
  222. package/src/run/discover-schemas.test.ts +0 -153
  223. package/src/run/discover-schemas.ts +0 -79
  224. package/src/run/hq-plugin.test.ts +0 -371
  225. package/src/run/hq-plugin.ts +0 -277
  226. package/src/run/varlock-shape.test.ts +0 -57
  227. package/src/schemas/hq-package.schema.json +0 -155
  228. package/src/sentry-before-send.test.ts +0 -359
  229. package/src/sentry-before-send.ts +0 -221
  230. package/src/sentry-epipe.test.ts +0 -37
  231. package/src/sentry-release.test.ts +0 -54
  232. package/src/sentry.test.ts +0 -24
  233. package/src/sentry.ts +0 -54
  234. package/src/strategies/link.ts +0 -62
  235. package/src/strategies/merge.ts +0 -142
  236. package/src/types.ts +0 -146
  237. package/src/utils/__fixtures__/scan-packages.generated-block.sh +0 -23
  238. package/src/utils/auth-error.test.ts +0 -40
  239. package/src/utils/auth-error.ts +0 -42
  240. package/src/utils/billing-gate.test.ts +0 -95
  241. package/src/utils/billing-gate.ts +0 -228
  242. package/src/utils/breadcrumb-buffer.ts +0 -18
  243. package/src/utils/cli-telemetry.test.ts +0 -153
  244. package/src/utils/cli-telemetry.ts +0 -61
  245. package/src/utils/cognito-session.machine.test.ts +0 -109
  246. package/src/utils/cognito-session.refresh.test.ts +0 -84
  247. package/src/utils/cognito-session.test.ts +0 -328
  248. package/src/utils/cognito-session.ts +0 -448
  249. package/src/utils/company-selection-error.test.ts +0 -42
  250. package/src/utils/company-selection-error.ts +0 -45
  251. package/src/utils/contribution-table.ts +0 -83
  252. package/src/utils/environmental-error.test.ts +0 -45
  253. package/src/utils/environmental-error.ts +0 -39
  254. package/src/utils/epipe.test.ts +0 -28
  255. package/src/utils/epipe.ts +0 -29
  256. package/src/utils/expected-cli-error.test.ts +0 -28
  257. package/src/utils/expected-cli-error.ts +0 -39
  258. package/src/utils/feedback-diagnostics.test.ts +0 -183
  259. package/src/utils/feedback-diagnostics.ts +0 -123
  260. package/src/utils/feedback-screenshots.test.ts +0 -134
  261. package/src/utils/feedback-screenshots.ts +0 -124
  262. package/src/utils/feedback-versions.test.ts +0 -98
  263. package/src/utils/feedback-versions.ts +0 -68
  264. package/src/utils/git.ts +0 -74
  265. package/src/utils/integrity.ts +0 -59
  266. package/src/utils/intercepted-process-exit.test.ts +0 -37
  267. package/src/utils/intercepted-process-exit.ts +0 -36
  268. package/src/utils/login-provider.test.ts +0 -42
  269. package/src/utils/login-provider.ts +0 -30
  270. package/src/utils/manifest.test.ts +0 -167
  271. package/src/utils/manifest.ts +0 -131
  272. package/src/utils/pack-contributions.test.ts +0 -603
  273. package/src/utils/pack-contributions.ts +0 -450
  274. package/src/utils/people.ts +0 -215
  275. package/src/utils/registry-client.ts +0 -204
  276. package/src/utils/registry.ts +0 -90
  277. package/src/utils/resolve-vault-credential.test.ts +0 -69
  278. package/src/utils/resolve-vault-credential.ts +0 -60
  279. package/src/utils/sandbox-runner-client.test.ts +0 -279
  280. package/src/utils/sandbox-runner-client.ts +0 -260
  281. package/src/utils/secrets-cache.ts +0 -183
  282. package/src/utils/sentry-identity.ts +0 -45
  283. package/src/utils/settle-with-timeout.test.ts +0 -21
  284. package/src/utils/settle-with-timeout.ts +0 -22
  285. package/src/utils/user-yaml-error.test.ts +0 -24
  286. package/src/utils/user-yaml-error.ts +0 -30
  287. package/src/utils/vault-api.test.ts +0 -536
  288. package/src/utils/vault-api.ts +0 -382
  289. package/src/utils/version-check.test.ts +0 -176
  290. package/src/utils/version-check.ts +0 -155
  291. package/src/utils/version-gate.test.ts +0 -1024
  292. package/src/utils/version-gate.ts +0 -702
  293. package/test/commands/db-tenant-isolation.test.ts +0 -94
  294. package/test/commands/db.test.ts +0 -85
  295. package/test/commands/signals.test.ts +0 -207
  296. package/test/commands/sources.test.ts +0 -231
  297. package/test/e2e/smoke-install-mcp.sh +0 -113
  298. package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +0 -1
  299. package/test/fixtures/hq-pack-smoke-mcp/package.yaml +0 -11
  300. package/test/fixtures/signals/action_item/sample.md +0 -16
  301. package/test/fixtures/signals/summary/sample.md +0 -12
  302. package/test/fixtures/sources/meetings/sample.md +0 -25
  303. package/test/helpers/cli-runner.ts +0 -150
  304. package/test/helpers/s3-list-mock.ts +0 -79
  305. package/test/helpers/vault-service-mock.ts +0 -295
  306. package/test/sources-signals/smoke.test.ts +0 -228
  307. package/tsconfig.json +0 -20
  308. package/vitest.config.ts +0 -11
  309. package/vitest.e2e.config.ts +0 -7
@@ -1,362 +0,0 @@
1
- /**
2
- * Pure helper for `hq sync narrow` (US-007).
3
- *
4
- * Walks a local directory tree under `<hqRoot>/companies/{slug}/`, classifies
5
- * each file against a prospective `shared`-mode prefix set, and groups the
6
- * results into three buckets:
7
- *
8
- * - **staying** — file is covered by the prospective prefix set; survives.
9
- * - **clean** — file is NOT covered (orphan) AND is provably unchanged
10
- * since the last journal sync (safe to delete).
11
- * - **dirty** — file is NOT covered (orphan) AND has been locally
12
- * modified since the last sync (sacred — abort without
13
- * `--force`).
14
- *
15
- * The clean-vs-dirty rule mirrors hq-cloud's implicit-shrink path
16
- * (`scope-shrink.ts::classifyOrphan`) verbatim so the two narrowing paths —
17
- * implicit (next pull after sync-mode flip) and explicit (`hq sync narrow
18
- * --apply`) — agree on what "dirty" means. See US-000 Task 3 in
19
- * `companies/indigo/projects/hq-sync-browse-vs-sync/references.md`.
20
- *
21
- * Pure: no network. Reads the journal off `SyncJournal` (caller passes it
22
- * in), stats + hashes files on disk — no journal mutations and no remote
23
- * calls happen here. The CLI orchestrator (`sync-narrow.ts`) is responsible
24
- * for the destructive side effects (delete, tombstone, PUT sync-config).
25
- */
26
-
27
- import * as fs from "node:fs";
28
- import * as path from "node:path";
29
-
30
- import {
31
- hashFile,
32
- isCoveredByAny,
33
- type JournalEntry,
34
- type SyncJournal,
35
- } from "@indigoai-us/hq-cloud";
36
-
37
- // ── Types ───────────────────────────────────────────────────────────────────
38
-
39
- export type DirtyReason =
40
- | "modified-after-sync"
41
- | "hash-mismatch"
42
- | "not-in-journal"
43
- | "stat-error";
44
-
45
- export interface NarrowFile {
46
- /**
47
- * COMPANY-RELATIVE path (e.g. `meetings/a.md`) — the canonical namespace
48
- * shared by the per-company journal keys, the vault S3 keys, and the
49
- * server's explicit-grant paths. NOT hq-root-relative.
50
- */
51
- relPath: string;
52
- /** Absolute path on disk (convenience for the CLI delete loop). */
53
- absPath: string;
54
- /** Size in bytes from the local stat. 0 for symlinks (lstat size is the link, not target). */
55
- bytes: number;
56
- }
57
-
58
- export interface NarrowDirtyFile extends NarrowFile {
59
- reason: DirtyReason;
60
- }
61
-
62
- export interface NarrowPlan {
63
- /** Files covered by `prospectivePrefixSet` — survive the narrow. */
64
- staying: NarrowFile[];
65
- /** Orphan files that are journal-clean — safe to delete on `--apply`. */
66
- clean: NarrowFile[];
67
- /** Orphan files that are locally dirty — block `--apply` unless `--force`. */
68
- dirty: NarrowDirtyFile[];
69
-
70
- // Convenience aggregates for the CLI summary table.
71
- totalStayingCount: number;
72
- totalCleanCount: number;
73
- totalCleanBytes: number;
74
- totalDirtyCount: number;
75
- totalDirtyBytes: number;
76
- }
77
-
78
- export interface BuildNarrowPlanInput {
79
- /** Absolute HQ root path (the dir containing `companies/`). */
80
- hqRoot: string;
81
- /** Company slug — used to derive the walk root `<hqRoot>/companies/<slug>/`. */
82
- companySlug: string;
83
- /**
84
- * Coalesced prospective `shared`-mode prefix set (the result of running
85
- * the caller's explicit grants through `coalescePrefixes`). Prefixes are
86
- * COMPANY-RELATIVE (e.g. `meetings/`) — the namespace the grants endpoint
87
- * returns and the namespace `relPath` is now computed in.
88
- */
89
- prospectivePrefixSet: readonly string[];
90
- /**
91
- * Active per-company journal. Hash + mtime comparisons key off
92
- * `journal.files[relPath]`.
93
- */
94
- journal: SyncJournal;
95
- }
96
-
97
- // ── Pure entry point ────────────────────────────────────────────────────────
98
-
99
- /**
100
- * Build a NarrowPlan by walking `<hqRoot>/companies/<slug>/` and classifying
101
- * each regular file against `prospectivePrefixSet` + `journal`.
102
- *
103
- * Walk semantics:
104
- * - Recursive `fs.readdirSync(... withFileTypes)`.
105
- * - Symlinks are RECORDED but never followed (`lstat`, not `stat`), matching
106
- * the `share()`/sync engine's contract.
107
- * - Hidden files + `.DS_Store` + `node_modules` are walked as-is — the
108
- * caller is expected to point this at HQ content, where `.hqignore`
109
- * filtering happens upstream of the journal. The journal already
110
- * reflects what's been pushed/pulled, so `not-in-journal` is the
111
- * classifier signal for "we didn't put this here".
112
- * - If the walk root is missing entirely, returns an empty plan (rather
113
- * than throwing) — a freshly-flipped membership may not have synced any
114
- * files yet.
115
- */
116
- export function buildNarrowPlan(input: BuildNarrowPlanInput): NarrowPlan {
117
- const { hqRoot, companySlug, prospectivePrefixSet, journal } = input;
118
- const walkRoot = path.join(hqRoot, "companies", companySlug);
119
-
120
- const staying: NarrowFile[] = [];
121
- const clean: NarrowFile[] = [];
122
- const dirty: NarrowDirtyFile[] = [];
123
-
124
- if (!fs.existsSync(walkRoot)) {
125
- return emptyPlan();
126
- }
127
-
128
- // Walk with `walkRoot` as the rel-root so each file's `relPath` is
129
- // COMPANY-RELATIVE (e.g. `meetings/a.md`) — the same namespace as the
130
- // server's explicit-grant paths, the per-company journal keys, and the
131
- // hq-cloud sync engine's `RemoteFile.key`. Computing it relative to `hqRoot`
132
- // (the old behavior) produced `companies/<slug>/meetings/a.md`, which
133
- // matched neither the company-relative grants nor the journal keys — so
134
- // every file fell out of `prospectivePrefixSet` AND missed its journal
135
- // entry, making narrow flag the entire tree as dirty orphans. See the
136
- // namespace contract in hq-cloud `scope-shrink.ts` / `prefix-coalesce.ts`.
137
- walkLocal(walkRoot, walkRoot, (file) => {
138
- if (isCoveredByAny(file.relPath, prospectivePrefixSet)) {
139
- staying.push(file);
140
- return;
141
- }
142
- const verdict = classifyAgainstJournal(file, journal);
143
- if (verdict.clean) {
144
- clean.push(file);
145
- } else {
146
- dirty.push({ ...file, reason: verdict.reason });
147
- }
148
- });
149
-
150
- return {
151
- staying,
152
- clean,
153
- dirty,
154
- totalStayingCount: staying.length,
155
- totalCleanCount: clean.length,
156
- totalCleanBytes: clean.reduce((sum, f) => sum + f.bytes, 0),
157
- totalDirtyCount: dirty.length,
158
- totalDirtyBytes: dirty.reduce((sum, f) => sum + f.bytes, 0),
159
- };
160
- }
161
-
162
- // ── Internal: walk + classify ───────────────────────────────────────────────
163
-
164
- function emptyPlan(): NarrowPlan {
165
- return {
166
- staying: [],
167
- clean: [],
168
- dirty: [],
169
- totalStayingCount: 0,
170
- totalCleanCount: 0,
171
- totalCleanBytes: 0,
172
- totalDirtyCount: 0,
173
- totalDirtyBytes: 0,
174
- };
175
- }
176
-
177
- /**
178
- * Recursive readdir walk yielding regular files (and dangling-target
179
- * symlinks — those are recorded as files for narrow-plan purposes; the
180
- * delete path uses `fs.unlinkSync` which handles symlinks correctly).
181
- *
182
- * Uses `lstat` rather than `stat` so a symlink's size doesn't follow the
183
- * target chain (matches the share-engine convention).
184
- */
185
- /**
186
- * Normalize OS-native path separators to POSIX "/" for vault keys.
187
- *
188
- * `path.relative()` yields "\\"-separated paths on Windows, but vault S3 keys
189
- * are always "/"-separated — the server splits keys on "/" to rebuild the tree
190
- * and the journal + grants endpoint use that same namespace. Emit POSIX keys
191
- * regardless of client OS so a Windows narrow-plan lines up with the
192
- * forward-slash keys everywhere else. Mirrors the same normalization on
193
- * @indigoai-us/hq-cloud's upload path. Converting "\\" explicitly (rather than
194
- * only path.sep) keeps the result correct — and the regression test meaningful
195
- * — on a POSIX CI too.
196
- */
197
- export function toPosixKey(p: string): string {
198
- return p.split("\\").join("/");
199
- }
200
-
201
- function walkLocal(
202
- dir: string,
203
- // Rel-root for `relPath` — the company walk root (`<hqRoot>/companies/<slug>`),
204
- // so emitted `relPath`s are company-relative. Fixed across recursion.
205
- relRoot: string,
206
- emit: (file: NarrowFile) => void,
207
- ): void {
208
- let entries: fs.Dirent[];
209
- try {
210
- entries = fs.readdirSync(dir, { withFileTypes: true });
211
- } catch (err) {
212
- // Permission-denied or ENOENT mid-walk — skip silently; the CLI
213
- // surfaces the higher-level "company folder missing" message earlier.
214
- const code = (err as NodeJS.ErrnoException).code;
215
- if (code === "ENOENT" || code === "EACCES") return;
216
- throw err;
217
- }
218
-
219
- for (const entry of entries) {
220
- const absPath = path.join(dir, entry.name);
221
- const relPath = toPosixKey(path.relative(relRoot, absPath));
222
-
223
- if (entry.isSymbolicLink()) {
224
- // Record the link as a file-like entry. Don't descend — narrow is
225
- // about pruning files that the LOCAL tree has materialized here; a
226
- // symlink to a knowledge repo is one such "file" from the
227
- // walk's POV.
228
- let bytes = 0;
229
- try {
230
- bytes = fs.lstatSync(absPath).size;
231
- } catch {
232
- // pass — dangling link still gets emitted with size 0
233
- }
234
- emit({ relPath, absPath, bytes });
235
- continue;
236
- }
237
-
238
- if (entry.isDirectory()) {
239
- walkLocal(absPath, relRoot, emit);
240
- continue;
241
- }
242
-
243
- if (entry.isFile()) {
244
- let bytes = 0;
245
- try {
246
- bytes = fs.lstatSync(absPath).size;
247
- } catch {
248
- // pass
249
- }
250
- emit({ relPath, absPath, bytes });
251
- }
252
- }
253
- }
254
-
255
- interface JournalVerdict {
256
- clean: boolean;
257
- reason: DirtyReason;
258
- }
259
-
260
- /**
261
- * Mirror of `scope-shrink.ts::classifyOrphan`:
262
- *
263
- * clean = (entry exists AND hash(local) === entry.hash AND mtime <= syncedAt + 1s)
264
- * OR (local file missing)
265
- *
266
- * Differences from the implicit path:
267
- * - If there is NO journal entry at all, classify as **dirty**
268
- * (`not-in-journal`). The implicit path never sees those files (it
269
- * iterates the journal); here we walk the local tree so an
270
- * un-journaled file is a real risk — it could be an unsynced local
271
- * draft.
272
- * - We don't honor `direction: "up"` differently. For an explicit
273
- * narrow migration the user is asking "wipe everything outside the
274
- * shared scope" — push-only authorship deserves the same dirty-file
275
- * protection as pulled content (otherwise `--apply` could nuke local
276
- * drafts).
277
- */
278
- function classifyAgainstJournal(
279
- file: NarrowFile,
280
- journal: SyncJournal,
281
- ): JournalVerdict {
282
- const entry: JournalEntry | undefined = journal.files[file.relPath];
283
- if (!entry) {
284
- return { clean: false, reason: "not-in-journal" };
285
- }
286
- // Tombstoned entries — the file shouldn't be here at all; treat as
287
- // not-in-journal for safety so `--apply` doesn't silently delete it.
288
- if (entry.removedAt) {
289
- return { clean: false, reason: "not-in-journal" };
290
- }
291
-
292
- let stat: fs.Stats;
293
- try {
294
- stat = fs.lstatSync(file.absPath);
295
- } catch (err) {
296
- const code = (err as NodeJS.ErrnoException).code;
297
- if (code === "ENOENT") {
298
- // Already gone — clean (the apply path will skip ENOENT on unlink).
299
- return { clean: true, reason: "stat-error" };
300
- }
301
- return { clean: false, reason: "stat-error" };
302
- }
303
-
304
- // mtime guard with 1s grace for filesystem clock jitter, matching
305
- // hq-cloud `scope-shrink.ts`.
306
- const syncedAtMs = Date.parse(entry.syncedAt);
307
- if (!Number.isNaN(syncedAtMs) && stat.mtimeMs > syncedAtMs + 1000) {
308
- return { clean: false, reason: "modified-after-sync" };
309
- }
310
-
311
- // Hash check — final word. Symlinks: hashFile reads contents which is
312
- // wrong for symlinks (it would follow the target), so on a symlink we
313
- // skip the hash check and trust mtime alone. This matches the
314
- // engine's behavior in the symlink-orphan case.
315
- if (stat.isSymbolicLink()) {
316
- return { clean: true, reason: "stat-error" };
317
- }
318
-
319
- let actualHash: string;
320
- try {
321
- actualHash = hashFile(file.absPath);
322
- } catch {
323
- return { clean: false, reason: "stat-error" };
324
- }
325
- if (actualHash !== entry.hash) {
326
- return { clean: false, reason: "hash-mismatch" };
327
- }
328
- return { clean: true, reason: "stat-error" };
329
- }
330
-
331
- // ── Formatting helper (separable for tests) ────────────────────────────────
332
-
333
- /**
334
- * Render the dry-run summary as plain text (no chalk — keep it pure). The
335
- * CLI wrapper can colorize lines afterwards if desired.
336
- */
337
- export function formatNarrowPlanSummary(plan: NarrowPlan): string {
338
- const lines: string[] = [];
339
- lines.push("Narrow plan summary:");
340
- lines.push(` files staying: ${plan.totalStayingCount}`);
341
- lines.push(
342
- ` clean orphans: ${plan.totalCleanCount} (${formatBytes(plan.totalCleanBytes)})`,
343
- );
344
- lines.push(
345
- ` dirty orphans: ${plan.totalDirtyCount} (${formatBytes(plan.totalDirtyBytes)})`,
346
- );
347
- return lines.join("\n");
348
- }
349
-
350
- /** Human-readable byte counts with a fixed unit ladder. */
351
- export function formatBytes(n: number): string {
352
- if (!Number.isFinite(n) || n < 0) return "0 B";
353
- if (n < 1024) return `${n} B`;
354
- const units = ["KB", "MB", "GB", "TB"];
355
- let v = n / 1024;
356
- let i = 0;
357
- while (v >= 1024 && i < units.length - 1) {
358
- v /= 1024;
359
- i++;
360
- }
361
- return `${v.toFixed(2)} ${units[i]}`;
362
- }
@@ -1,235 +0,0 @@
1
- /**
2
- * Unit tests for `narrow-hint-banner` (US-011).
3
- *
4
- * Coverage:
5
- * 1. Emits a hint when sync mode is 'all' and not suppressed.
6
- * 2. Skips emission for shared/custom memberships.
7
- * 3. Dedupes per {companyUid, level} for the process lifetime.
8
- * 4. Suppression via HQ_SYNC_NARROW_HINT=off (env).
9
- * 5. Suppression via <hqRoot>/.hq/config.json `syncNarrowHint: 'off'`.
10
- * 6. Warning + strict level messages.
11
- * 7. `isStrictRefusal` only fires for strict + 'all'.
12
- * 8. `resolveBannerLevel` parses env values and defaults to 'hint'.
13
- */
14
-
15
- import { beforeEach, describe, expect, it } from "vitest";
16
-
17
- import {
18
- _resetShownForTests,
19
- emitNarrowHint,
20
- isStrictRefusal,
21
- resolveBannerLevel,
22
- shouldShowBanner,
23
- } from "./narrow-hint-banner.js";
24
-
25
- function makeSink(): { lines: string[]; write: (s: string) => void } {
26
- const lines: string[] = [];
27
- return {
28
- lines,
29
- write: (s: string) => {
30
- lines.push(s);
31
- },
32
- };
33
- }
34
-
35
- beforeEach(() => {
36
- _resetShownForTests();
37
- });
38
-
39
- describe("emitNarrowHint", () => {
40
- it("emits a hint message when sync mode is 'all' and not suppressed", () => {
41
- const sink = makeSink();
42
- emitNarrowHint(
43
- { companyUid: "cmp_acme", syncMode: "all", level: "hint" },
44
- { write: sink.write, showOverride: true },
45
- );
46
- expect(sink.lines).toHaveLength(1);
47
- expect(sink.lines[0]).toMatch(/switch to shared-mode sync/i);
48
- expect(sink.lines[0]).toMatch(/hq sync narrow --dry-run/);
49
- });
50
-
51
- it("skips emission when sync mode is 'shared'", () => {
52
- const sink = makeSink();
53
- emitNarrowHint(
54
- { companyUid: "cmp_acme", syncMode: "shared", level: "hint" },
55
- { write: sink.write, showOverride: true },
56
- );
57
- expect(sink.lines).toHaveLength(0);
58
- });
59
-
60
- it("skips emission when sync mode is 'custom'", () => {
61
- const sink = makeSink();
62
- emitNarrowHint(
63
- { companyUid: "cmp_acme", syncMode: "custom", level: "hint" },
64
- { write: sink.write, showOverride: true },
65
- );
66
- expect(sink.lines).toHaveLength(0);
67
- });
68
-
69
- it("dedupes per {companyUid, level} across calls", () => {
70
- const sink = makeSink();
71
- const input = {
72
- companyUid: "cmp_acme",
73
- syncMode: "all",
74
- level: "hint",
75
- } as const;
76
- emitNarrowHint(input, { write: sink.write, showOverride: true });
77
- emitNarrowHint(input, { write: sink.write, showOverride: true });
78
- emitNarrowHint(input, { write: sink.write, showOverride: true });
79
- expect(sink.lines).toHaveLength(1);
80
- });
81
-
82
- it("emits separately for different companies", () => {
83
- const sink = makeSink();
84
- emitNarrowHint(
85
- { companyUid: "cmp_acme", syncMode: "all", level: "hint" },
86
- { write: sink.write, showOverride: true },
87
- );
88
- emitNarrowHint(
89
- { companyUid: "cmp_globex", syncMode: "all", level: "hint" },
90
- { write: sink.write, showOverride: true },
91
- );
92
- expect(sink.lines).toHaveLength(2);
93
- });
94
-
95
- it("emits separately for the same company at a different level", () => {
96
- const sink = makeSink();
97
- emitNarrowHint(
98
- { companyUid: "cmp_acme", syncMode: "all", level: "hint" },
99
- { write: sink.write, showOverride: true },
100
- );
101
- emitNarrowHint(
102
- { companyUid: "cmp_acme", syncMode: "all", level: "warning" },
103
- { write: sink.write, showOverride: true },
104
- );
105
- expect(sink.lines).toHaveLength(2);
106
- });
107
-
108
- it("suppresses emission when showOverride=false (suppression path)", () => {
109
- const sink = makeSink();
110
- emitNarrowHint(
111
- { companyUid: "cmp_acme", syncMode: "all", level: "hint" },
112
- { write: sink.write, showOverride: false },
113
- );
114
- expect(sink.lines).toHaveLength(0);
115
- });
116
-
117
- it("emits the warning-level message", () => {
118
- const sink = makeSink();
119
- emitNarrowHint(
120
- { companyUid: "cmp_acme", syncMode: "all", level: "warning" },
121
- { write: sink.write, showOverride: true },
122
- );
123
- expect(sink.lines).toHaveLength(1);
124
- expect(sink.lines[0]).toMatch(/shared-mode sync is the new default/i);
125
- });
126
-
127
- it("emits the strict-level message (does NOT throw or exit)", () => {
128
- const sink = makeSink();
129
- expect(() =>
130
- emitNarrowHint(
131
- { companyUid: "cmp_acme", syncMode: "all", level: "strict" },
132
- { write: sink.write, showOverride: true },
133
- ),
134
- ).not.toThrow();
135
- expect(sink.lines).toHaveLength(1);
136
- expect(sink.lines[0]).toMatch(/--mode-all/);
137
- });
138
- });
139
-
140
- describe("shouldShowBanner", () => {
141
- it("returns true with no overrides", () => {
142
- expect(shouldShowBanner({ envValue: undefined })).toBe(true);
143
- });
144
-
145
- it("returns false when env value is 'off' (case-insensitive)", () => {
146
- expect(shouldShowBanner({ envValue: "off" })).toBe(false);
147
- expect(shouldShowBanner({ envValue: "OFF" })).toBe(false);
148
- });
149
-
150
- it("returns true for env values other than 'off'", () => {
151
- expect(shouldShowBanner({ envValue: "on" })).toBe(true);
152
- expect(shouldShowBanner({ envValue: "" })).toBe(true);
153
- });
154
-
155
- it("returns false when .hq/config.json sets syncNarrowHint='off'", () => {
156
- const exists = (_p: string) => true;
157
- const read = (_p: string) => JSON.stringify({ syncNarrowHint: "off" });
158
- expect(
159
- shouldShowBanner({
160
- envValue: undefined,
161
- hqRoot: "/tmp/hq",
162
- existsFile: exists,
163
- readFile: read,
164
- }),
165
- ).toBe(false);
166
- });
167
-
168
- it("returns true when .hq/config.json has no syncNarrowHint key", () => {
169
- const exists = (_p: string) => true;
170
- const read = (_p: string) =>
171
- JSON.stringify({ activeCompany: "acme" });
172
- expect(
173
- shouldShowBanner({
174
- envValue: undefined,
175
- hqRoot: "/tmp/hq",
176
- existsFile: exists,
177
- readFile: read,
178
- }),
179
- ).toBe(true);
180
- });
181
-
182
- it("tolerates malformed config.json (returns true)", () => {
183
- const exists = (_p: string) => true;
184
- const read = (_p: string) => "{not json";
185
- expect(
186
- shouldShowBanner({
187
- envValue: undefined,
188
- hqRoot: "/tmp/hq",
189
- existsFile: exists,
190
- readFile: read,
191
- }),
192
- ).toBe(true);
193
- });
194
-
195
- it("returns true when config file does not exist", () => {
196
- expect(
197
- shouldShowBanner({
198
- envValue: undefined,
199
- hqRoot: "/tmp/hq",
200
- existsFile: () => false,
201
- readFile: () => {
202
- throw new Error("should not be called");
203
- },
204
- }),
205
- ).toBe(true);
206
- });
207
- });
208
-
209
- describe("isStrictRefusal", () => {
210
- it("returns true only for level=strict + syncMode=all", () => {
211
- expect(isStrictRefusal("all", "strict")).toBe(true);
212
- expect(isStrictRefusal("shared", "strict")).toBe(false);
213
- expect(isStrictRefusal("custom", "strict")).toBe(false);
214
- expect(isStrictRefusal("all", "warning")).toBe(false);
215
- expect(isStrictRefusal("all", "hint")).toBe(false);
216
- });
217
- });
218
-
219
- describe("resolveBannerLevel", () => {
220
- it("defaults to 'hint' when env is undefined", () => {
221
- expect(resolveBannerLevel(undefined)).toBe("hint");
222
- });
223
-
224
- it("parses recognized values (case-insensitive)", () => {
225
- expect(resolveBannerLevel("hint")).toBe("hint");
226
- expect(resolveBannerLevel("warning")).toBe("warning");
227
- expect(resolveBannerLevel("strict")).toBe("strict");
228
- expect(resolveBannerLevel("WARNING")).toBe("warning");
229
- });
230
-
231
- it("falls back to 'hint' for unknown values", () => {
232
- expect(resolveBannerLevel("loud")).toBe("hint");
233
- expect(resolveBannerLevel("")).toBe("hint");
234
- });
235
- });