@indigoai-us/hq-cli 5.77.14 → 5.78.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 (308) hide show
  1. package/CHANGELOG.md +23 -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/members.js +16 -5
  31. package/dist/commands/pack-install.d.ts +14 -0
  32. package/dist/commands/pack-install.js +42 -2
  33. package/dist/commands/pkg-install.js +39 -2
  34. package/dist/commands/run.js +6 -2
  35. package/dist/commands/secrets.js +120 -65
  36. package/dist/main.js +6 -0
  37. package/dist/utils/api-key-command-gate.d.ts +45 -0
  38. package/dist/utils/api-key-command-gate.js +51 -0
  39. package/dist/utils/hq-roots.d.ts +91 -0
  40. package/dist/utils/hq-roots.js +212 -0
  41. package/dist/utils/resolve-vault-credential.d.ts +63 -3
  42. package/dist/utils/resolve-vault-credential.js +152 -6
  43. package/dist/utils/run-bundled-script.d.ts +66 -0
  44. package/dist/utils/run-bundled-script.js +80 -0
  45. package/dist/utils/vault-api.d.ts +21 -0
  46. package/dist/utils/vault-api.js +74 -3
  47. package/package.json +7 -1
  48. package/.claude/policies/hq-cli-interactive-reads-use-sts-vend.md +0 -49
  49. package/.github/workflows/ci.yml +0 -46
  50. package/.github/workflows/publish.yml +0 -36
  51. package/.github/workflows/scripts/smoke-test-pkg.sh +0 -97
  52. package/e2e/cli.test.ts +0 -128
  53. package/eslint.config.js +0 -37
  54. package/pnpm-workspace.yaml +0 -4
  55. package/scripts/chmod-bins.mjs +0 -49
  56. package/scripts/generate-dsn.mjs +0 -26
  57. package/scripts/generate-scan-packages-table.mjs +0 -113
  58. package/scripts/smoke-sources-signals.sh +0 -103
  59. package/src/bin/hq-auth-refresh.ts +0 -43
  60. package/src/cli-version.ts +0 -13
  61. package/src/commands/__fixtures__/make-tar.ts +0 -126
  62. package/src/commands/_patterns.ts +0 -10
  63. package/src/commands/add.ts +0 -75
  64. package/src/commands/agents.test.ts +0 -527
  65. package/src/commands/agents.ts +0 -923
  66. package/src/commands/api-keys.test.ts +0 -291
  67. package/src/commands/api-keys.ts +0 -382
  68. package/src/commands/artifact-verify.test.ts +0 -177
  69. package/src/commands/auth.test.ts +0 -82
  70. package/src/commands/auth.ts +0 -213
  71. package/src/commands/billing.test.ts +0 -158
  72. package/src/commands/billing.ts +0 -146
  73. package/src/commands/channels.test.ts +0 -74
  74. package/src/commands/channels.ts +0 -91
  75. package/src/commands/cloud-demote.test.ts +0 -401
  76. package/src/commands/cloud-demote.ts +0 -278
  77. package/src/commands/cloud-provision.test.ts +0 -1117
  78. package/src/commands/cloud-provision.ts +0 -942
  79. package/src/commands/cloud.pull-all.test.ts +0 -711
  80. package/src/commands/cloud.pull-per-company.test.ts +0 -188
  81. package/src/commands/cloud.push-all.test.ts +0 -485
  82. package/src/commands/cloud.scope-excluded-warning.test.ts +0 -22
  83. package/src/commands/cloud.selectors.test.ts +0 -208
  84. package/src/commands/cloud.status.test.ts +0 -160
  85. package/src/commands/cloud.test.ts +0 -41
  86. package/src/commands/cloud.ts +0 -2130
  87. package/src/commands/company.test.ts +0 -202
  88. package/src/commands/company.ts +0 -148
  89. package/src/commands/creators.test.ts +0 -60
  90. package/src/commands/creators.ts +0 -117
  91. package/src/commands/crm.test.ts +0 -179
  92. package/src/commands/crm.ts +0 -236
  93. package/src/commands/db-migrate.ts +0 -55
  94. package/src/commands/db-provision.ts +0 -114
  95. package/src/commands/db-sql.ts +0 -124
  96. package/src/commands/db-status.ts +0 -108
  97. package/src/commands/db.ts +0 -26
  98. package/src/commands/dm.test.ts +0 -748
  99. package/src/commands/dm.ts +0 -1226
  100. package/src/commands/feedback.test.ts +0 -413
  101. package/src/commands/feedback.ts +0 -169
  102. package/src/commands/files-browse.test.ts +0 -1249
  103. package/src/commands/files-browse.ts +0 -1506
  104. package/src/commands/files-delete.test.ts +0 -484
  105. package/src/commands/files-recovery.test.ts +0 -370
  106. package/src/commands/files.test.ts +0 -722
  107. package/src/commands/files.ts +0 -1446
  108. package/src/commands/group-grants.test.ts +0 -330
  109. package/src/commands/group-grants.ts +0 -455
  110. package/src/commands/groups.test.ts +0 -44
  111. package/src/commands/groups.ts +0 -368
  112. package/src/commands/integrations.test.ts +0 -633
  113. package/src/commands/integrations.ts +0 -507
  114. package/src/commands/list.ts +0 -85
  115. package/src/commands/login.ts +0 -52
  116. package/src/commands/logout.ts +0 -30
  117. package/src/commands/marketplace-install.test.ts +0 -543
  118. package/src/commands/marketplace-security.test.ts +0 -646
  119. package/src/commands/mcp-registration.test.ts +0 -2787
  120. package/src/commands/mcp-registration.ts +0 -2612
  121. package/src/commands/mcp-status.test.ts +0 -483
  122. package/src/commands/mcp-status.ts +0 -575
  123. package/src/commands/mcp-status.us011.test.ts +0 -243
  124. package/src/commands/meetings.test.ts +0 -522
  125. package/src/commands/meetings.ts +0 -789
  126. package/src/commands/members.test.ts +0 -1474
  127. package/src/commands/members.ts +0 -1181
  128. package/src/commands/onboard-identity-guard.test.ts +0 -99
  129. package/src/commands/onboard-identity-guard.ts +0 -103
  130. package/src/commands/onboard-join.test.ts +0 -96
  131. package/src/commands/onboard-join.ts +0 -101
  132. package/src/commands/onboard-warning.test.ts +0 -26
  133. package/src/commands/onboard-warning.ts +0 -12
  134. package/src/commands/onboard.ts +0 -267
  135. package/src/commands/outposts-heartbeat.test.ts +0 -299
  136. package/src/commands/outposts-heartbeat.ts +0 -310
  137. package/src/commands/outposts-replica-sync.test.ts +0 -272
  138. package/src/commands/outposts-self-deploy.test.ts +0 -243
  139. package/src/commands/outposts.test.ts +0 -1039
  140. package/src/commands/outposts.ts +0 -1935
  141. package/src/commands/pack-install-secret-authorization.test.ts +0 -115
  142. package/src/commands/pack-install.test.ts +0 -1469
  143. package/src/commands/pack-install.ts +0 -2256
  144. package/src/commands/pack-update-cache.test.ts +0 -149
  145. package/src/commands/packs-update-api-key.test.ts +0 -105
  146. package/src/commands/packs.test.ts +0 -88
  147. package/src/commands/packs.ts +0 -622
  148. package/src/commands/people.test.ts +0 -807
  149. package/src/commands/people.ts +0 -422
  150. package/src/commands/pkg-install.dispatch.test.ts +0 -103
  151. package/src/commands/pkg-install.ts +0 -221
  152. package/src/commands/pkg-list.test.ts +0 -79
  153. package/src/commands/pkg-list.ts +0 -13
  154. package/src/commands/pkg-remove.ts +0 -71
  155. package/src/commands/pkg-update.ts +0 -189
  156. package/src/commands/publish.test.ts +0 -650
  157. package/src/commands/publish.ts +0 -580
  158. package/src/commands/reindex.test.ts +0 -259
  159. package/src/commands/reindex.ts +0 -274
  160. package/src/commands/rescue.test.ts +0 -39
  161. package/src/commands/rescue.ts +0 -210
  162. package/src/commands/run.env-local.test.ts +0 -88
  163. package/src/commands/run.test.ts +0 -45
  164. package/src/commands/run.ts +0 -190
  165. package/src/commands/safe-extract.test.ts +0 -459
  166. package/src/commands/safe-extract.ts +0 -444
  167. package/src/commands/secrets-scope.test.ts +0 -56
  168. package/src/commands/secrets-scope.ts +0 -32
  169. package/src/commands/secrets.parse-destination.test.ts +0 -38
  170. package/src/commands/secrets.test.ts +0 -2050
  171. package/src/commands/secrets.ts +0 -2249
  172. package/src/commands/signals.ts +0 -351
  173. package/src/commands/skill.test.ts +0 -770
  174. package/src/commands/skill.ts +0 -796
  175. package/src/commands/sources.ts +0 -362
  176. package/src/commands/sync-mode.test.ts +0 -366
  177. package/src/commands/sync-mode.ts +0 -387
  178. package/src/commands/sync-narrow.test.ts +0 -633
  179. package/src/commands/sync-narrow.ts +0 -555
  180. package/src/commands/sync.ts +0 -165
  181. package/src/commands/team-sync.ts +0 -628
  182. package/src/commands/update.ts +0 -71
  183. package/src/commands/whoami.ts +0 -83
  184. package/src/commands/workers.test.ts +0 -158
  185. package/src/commands/workers.ts +0 -301
  186. package/src/index.test.ts +0 -32
  187. package/src/index.ts +0 -20
  188. package/src/lib/db/control-plane.test.ts +0 -59
  189. package/src/lib/db/control-plane.ts +0 -113
  190. package/src/lib/db/local.test.ts +0 -81
  191. package/src/lib/db/local.ts +0 -148
  192. package/src/lib/db/migrate.test.ts +0 -133
  193. package/src/lib/db/migrate.ts +0 -137
  194. package/src/lib/db/paths.test.ts +0 -112
  195. package/src/lib/db/paths.ts +0 -128
  196. package/src/lib/db/remote-engine.test.ts +0 -44
  197. package/src/lib/db/remote-engine.ts +0 -148
  198. package/src/lib/db/remote-sql.test.ts +0 -32
  199. package/src/lib/db/remote-sql.ts +0 -62
  200. package/src/lib/db/sql.test.ts +0 -106
  201. package/src/lib/db/sql.ts +0 -192
  202. package/src/lib/hq-cloud-dep.smoke.test.ts +0 -75
  203. package/src/lib/local-tree-diff.test.ts +0 -301
  204. package/src/lib/local-tree-diff.ts +0 -362
  205. package/src/lib/narrow-hint-banner.test.ts +0 -235
  206. package/src/lib/narrow-hint-banner.ts +0 -212
  207. package/src/lib/plan-limit-nag.test.ts +0 -317
  208. package/src/lib/plan-limit-nag.ts +0 -264
  209. package/src/main.ts +0 -371
  210. package/src/node-preflight.test.ts +0 -60
  211. package/src/node-preflight.ts +0 -67
  212. package/src/outpost/session-heartbeat-bounds.test.ts +0 -195
  213. package/src/outpost/session-heartbeat-guard.test.ts +0 -105
  214. package/src/outpost/session-heartbeat-publisher.test.ts +0 -178
  215. package/src/outpost/session-heartbeat-publisher.ts +0 -186
  216. package/src/outpost/session-heartbeat-retain-guard.test.ts +0 -126
  217. package/src/outpost/session-heartbeat.test.ts +0 -459
  218. package/src/outpost/session-heartbeat.ts +0 -877
  219. package/src/packaging.test.ts +0 -109
  220. package/src/run/__fixtures__/discover-schemas/example.env.schema +0 -4
  221. package/src/run/discover-schemas.test.ts +0 -153
  222. package/src/run/discover-schemas.ts +0 -79
  223. package/src/run/hq-plugin.test.ts +0 -371
  224. package/src/run/hq-plugin.ts +0 -277
  225. package/src/run/varlock-shape.test.ts +0 -57
  226. package/src/schemas/hq-package.schema.json +0 -155
  227. package/src/sentry-before-send.test.ts +0 -359
  228. package/src/sentry-before-send.ts +0 -221
  229. package/src/sentry-epipe.test.ts +0 -37
  230. package/src/sentry-release.test.ts +0 -54
  231. package/src/sentry.test.ts +0 -24
  232. package/src/sentry.ts +0 -54
  233. package/src/strategies/link.ts +0 -62
  234. package/src/strategies/merge.ts +0 -142
  235. package/src/types.ts +0 -146
  236. package/src/utils/__fixtures__/scan-packages.generated-block.sh +0 -23
  237. package/src/utils/auth-error.test.ts +0 -40
  238. package/src/utils/auth-error.ts +0 -42
  239. package/src/utils/billing-gate.test.ts +0 -95
  240. package/src/utils/billing-gate.ts +0 -228
  241. package/src/utils/breadcrumb-buffer.ts +0 -18
  242. package/src/utils/cli-telemetry.test.ts +0 -153
  243. package/src/utils/cli-telemetry.ts +0 -61
  244. package/src/utils/cognito-session.machine.test.ts +0 -109
  245. package/src/utils/cognito-session.refresh.test.ts +0 -84
  246. package/src/utils/cognito-session.test.ts +0 -328
  247. package/src/utils/cognito-session.ts +0 -448
  248. package/src/utils/company-selection-error.test.ts +0 -42
  249. package/src/utils/company-selection-error.ts +0 -45
  250. package/src/utils/contribution-table.ts +0 -83
  251. package/src/utils/environmental-error.test.ts +0 -45
  252. package/src/utils/environmental-error.ts +0 -39
  253. package/src/utils/epipe.test.ts +0 -28
  254. package/src/utils/epipe.ts +0 -29
  255. package/src/utils/expected-cli-error.test.ts +0 -28
  256. package/src/utils/expected-cli-error.ts +0 -39
  257. package/src/utils/feedback-diagnostics.test.ts +0 -183
  258. package/src/utils/feedback-diagnostics.ts +0 -123
  259. package/src/utils/feedback-screenshots.test.ts +0 -134
  260. package/src/utils/feedback-screenshots.ts +0 -124
  261. package/src/utils/feedback-versions.test.ts +0 -98
  262. package/src/utils/feedback-versions.ts +0 -68
  263. package/src/utils/git.ts +0 -74
  264. package/src/utils/integrity.ts +0 -59
  265. package/src/utils/intercepted-process-exit.test.ts +0 -37
  266. package/src/utils/intercepted-process-exit.ts +0 -36
  267. package/src/utils/login-provider.test.ts +0 -42
  268. package/src/utils/login-provider.ts +0 -30
  269. package/src/utils/manifest.test.ts +0 -167
  270. package/src/utils/manifest.ts +0 -131
  271. package/src/utils/pack-contributions.test.ts +0 -603
  272. package/src/utils/pack-contributions.ts +0 -450
  273. package/src/utils/people.ts +0 -215
  274. package/src/utils/registry-client.ts +0 -204
  275. package/src/utils/registry.ts +0 -90
  276. package/src/utils/resolve-vault-credential.test.ts +0 -69
  277. package/src/utils/resolve-vault-credential.ts +0 -60
  278. package/src/utils/sandbox-runner-client.test.ts +0 -279
  279. package/src/utils/sandbox-runner-client.ts +0 -260
  280. package/src/utils/secrets-cache.ts +0 -183
  281. package/src/utils/sentry-identity.ts +0 -45
  282. package/src/utils/settle-with-timeout.test.ts +0 -21
  283. package/src/utils/settle-with-timeout.ts +0 -22
  284. package/src/utils/user-yaml-error.test.ts +0 -24
  285. package/src/utils/user-yaml-error.ts +0 -30
  286. package/src/utils/vault-api.test.ts +0 -536
  287. package/src/utils/vault-api.ts +0 -382
  288. package/src/utils/version-check.test.ts +0 -176
  289. package/src/utils/version-check.ts +0 -155
  290. package/src/utils/version-gate.test.ts +0 -1024
  291. package/src/utils/version-gate.ts +0 -702
  292. package/test/commands/db-tenant-isolation.test.ts +0 -94
  293. package/test/commands/db.test.ts +0 -85
  294. package/test/commands/signals.test.ts +0 -207
  295. package/test/commands/sources.test.ts +0 -231
  296. package/test/e2e/smoke-install-mcp.sh +0 -113
  297. package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +0 -1
  298. package/test/fixtures/hq-pack-smoke-mcp/package.yaml +0 -11
  299. package/test/fixtures/signals/action_item/sample.md +0 -16
  300. package/test/fixtures/signals/summary/sample.md +0 -12
  301. package/test/fixtures/sources/meetings/sample.md +0 -25
  302. package/test/helpers/cli-runner.ts +0 -150
  303. package/test/helpers/s3-list-mock.ts +0 -79
  304. package/test/helpers/vault-service-mock.ts +0 -295
  305. package/test/sources-signals/smoke.test.ts +0 -228
  306. package/tsconfig.json +0 -20
  307. package/vitest.config.ts +0 -11
  308. package/vitest.e2e.config.ts +0 -7
@@ -1,448 +0,0 @@
1
- /**
2
- * Shared Cognito session helpers for hq-cli commands.
3
- *
4
- * Consumed by:
5
- * - `hq onboard` and `hq sync push|pull` (need token + VaultServiceConfig)
6
- * - `hq auth refresh` and the standalone `hq-auth-refresh` bin invoked by
7
- * the deploy skill (.claude/skills/deploy/SKILL.md step 4)
8
- *
9
- * Defaults point at the shared vault-indigo-hq-prod Cognito pool (canonical
10
- * post-2026-04-25 cutover; hq-dev remains the staging fallback via env
11
- * overrides). They mirror tools/vlt-e2e/e2e-create-company-smoke.ts so the
12
- * CLI and the in-tree demo script stay drift-free. Override any of them via
13
- * env:
14
- *
15
- * AWS_REGION — e.g. us-east-1
16
- * HQ_COGNITO_DOMAIN — Cognito User Pool domain prefix
17
- * HQ_COGNITO_CLIENT_ID — App Client ID
18
- * HQ_COGNITO_CALLBACK_PORT — Loopback OAuth callback port
19
- * HQ_VAULT_API_URL — vault-service API Gateway URL
20
- */
21
-
22
- import * as fs from "fs";
23
- import * as os from "os";
24
- import * as path from "path";
25
- import * as yaml from "js-yaml";
26
- import chalk from "chalk";
27
- import {
28
- loadCachedTokens,
29
- isExpiring,
30
- refreshTokens,
31
- browserLogin,
32
- detectHqCoreVersion,
33
- isMachineIdentity,
34
- getValidMachineTokens,
35
- type CognitoAuthConfig,
36
- type ClientInfo,
37
- type VaultServiceConfig,
38
- } from "@indigoai-us/hq-cloud";
39
- import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
40
-
41
- // Re-export the cached-session primitives for callers that must deliberately
42
- // avoid the refresh/login behavior in the higher-level helpers below.
43
- export { isExpiring, isMachineIdentity, loadCachedTokens };
44
-
45
- export const DEFAULT_COGNITO: CognitoAuthConfig = {
46
- region: process.env.AWS_REGION ?? "us-east-1",
47
- userPoolDomain: process.env.HQ_COGNITO_DOMAIN ?? "vault-indigo-hq-prod",
48
- clientId: process.env.HQ_COGNITO_CLIENT_ID ?? "7acei2c8v870enheptb1j5foln",
49
- port: process.env.HQ_COGNITO_CALLBACK_PORT
50
- ? Number(process.env.HQ_COGNITO_CALLBACK_PORT)
51
- : 8765,
52
- // Skip Cognito's Hosted UI — go straight to Google OAuth. The Cognito
53
- // /oauth2/authorize endpoint honors `identity_provider` and performs an
54
- // internal redirect, so the user never sees the (un-styleable) Hosted UI.
55
- // Set HQ_COGNITO_IDENTITY_PROVIDER="" to fall back to the IdP picker for
56
- // accounts that use email+password (admin-created users without Google).
57
- identityProvider:
58
- process.env.HQ_COGNITO_IDENTITY_PROVIDER !== undefined
59
- ? process.env.HQ_COGNITO_IDENTITY_PROVIDER || undefined
60
- : "Google",
61
- };
62
-
63
- export const DEFAULT_VAULT_API_URL =
64
- process.env.HQ_VAULT_API_URL ?? "https://hqapi.hq.computer";
65
-
66
- /**
67
- * Resolve the HQ tree root for cloud-aware subcommands (`hq sync`, `hq onboard`,
68
- * `hq cloud …`, etc.).
69
- *
70
- * Resolution order — mirrors the menubar/installer resolver
71
- * (`hq-sync` `util/paths.rs::resolve_hq_folder`) so the CLI targets the SAME
72
- * root the AppBar syncs, regardless of cwd:
73
- * 1. `$HQ_ROOT` env var — explicit assertion by the caller; short-circuits all.
74
- * 2. Walk up from `process.cwd()` to the nearest HQ root (working-tree wins:
75
- * if you're inside an HQ tree, operate on THAT tree). See {@link isHqRoot}.
76
- * 3. `~/.hq/menubar.json` → `hqPath` — the canonical record written by the
77
- * installer (≥0.1.28) and the menubar's Settings re-tether. Same field HQ
78
- * Sync reads first.
79
- * 4. `~/.hq/config.json` → `hqFolderPath` — legacy installer record.
80
- * 5. Discovery: scan well-known locations (`~/HQ`, `~/hq`, `~/Documents/HQ`,
81
- * …) for a folder carrying a valid `core.yaml` signature. Safety net for
82
- * installs that never wrote the path back to menubar.json.
83
- * 6. Fall back to `~/hq` (the historical CLI default) — or throw, per
84
- * `opts.onMissing`.
85
- *
86
- * Why tiers 3–5 exist:
87
- * Before this, the CLI only knew `$HQ_ROOT` and the cwd walk-up. Run from any
88
- * cwd OUTSIDE the HQ tree with `$HQ_ROOT` unset, it fell straight to `~/hq` —
89
- * so a user whose HQ lives anywhere else (e.g. `~/Desktop/HQ`, as the AppBar
90
- * syncs it) silently got a brand-new `~/hq` stub and a full re-sync into it
91
- * (split-brain). Reading the installer/menubar records the way HQ Sync does
92
- * closes that gap (feedback_3967e294).
93
- *
94
- * Why both markers in the walk-up (tier 2)?
95
- * An HQ root has a `core.yaml` (canonical `core/core.yaml` since hq-core v14,
96
- * legacy `<root>/core.yaml` before) AND a sibling `companies/` directory. The
97
- * synced `core/` subtree ALSO contains a `core.yaml` (the template version
98
- * source-of-truth) but NO `companies/`. Requiring `companies/` as well stops
99
- * the walk from latching onto `<hqRoot>/core/` (Codex P2 on hq#146).
100
- *
101
- * `opts.onMissing` controls the final arm:
102
- * - `'fallback'` (default) — return `~/hq` if nothing resolves. Preserves the
103
- * module-load contract of `DEFAULT_HQ_ROOT`, pinned by commander.js
104
- * `.option()` callers at registration time.
105
- * - `'throw'` — throw a user-actionable error. Used by module-management
106
- * commands (pkg-*, team-sync) where a silent default-path miss would
107
- * target the wrong directory.
108
- *
109
- * Evaluated once at module load for `DEFAULT_HQ_ROOT`; the installer/menubar
110
- * records and cwd are both fixed at process start, so a one-shot read is sound.
111
- * Re-importable as a function for command-time resolution and tests.
112
- */
113
- export function resolveDefaultHqRoot(opts: {
114
- onMissing?: "throw" | "fallback";
115
- } = {}): string {
116
- // Tier 1: explicit override.
117
- if (process.env.HQ_ROOT) return path.resolve(process.env.HQ_ROOT);
118
-
119
- // Tier 2: working-tree walk-up — operate on the HQ tree you're inside.
120
- let cur = path.resolve(process.cwd());
121
- while (cur !== path.dirname(cur)) {
122
- if (isHqRoot(cur)) return cur;
123
- cur = path.dirname(cur);
124
- }
125
-
126
- // Tiers 3–4: the installer/menubar's canonical "where is HQ" records.
127
- const fromConfig = readHqRootFromHqConfig(path.join(os.homedir(), ".hq"));
128
- if (fromConfig) return path.resolve(fromConfig);
129
-
130
- // Tier 5: discover a valid HQ root in the well-known locations.
131
- const discovered = discoverHqRootViaCoreYaml();
132
- if (discovered) return discovered;
133
-
134
- // Tier 6: nothing resolved.
135
- if (opts.onMissing === "throw") {
136
- throw new Error(
137
- "Could not find HQ root. Run this command from within your HQ directory " +
138
- "(must contain core.yaml AND a companies/ subdirectory), set $HQ_ROOT, " +
139
- "or install HQ via the installer/menubar so ~/.hq/menubar.json records " +
140
- "its location.",
141
- );
142
- }
143
- return path.join(os.homedir(), "hq");
144
- }
145
-
146
- /**
147
- * Read the configured HQ root from the installer/menubar records under
148
- * `hqConfigDir` (`~/.hq`). Mirrors HQ Sync's precedence:
149
- * 1. `menubar.json` → `hqPath` (canonical; written by installer ≥0.1.28 and
150
- * the menubar Settings re-tether)
151
- * 2. `config.json` → `hqFolderPath` (legacy installer flows)
152
- * Returns the first non-empty string, or undefined. Best-effort: a missing or
153
- * malformed file is treated as "not configured" (never throws).
154
- */
155
- function readHqRootFromHqConfig(hqConfigDir: string): string | undefined {
156
- const fromMenubar = readJsonStringField(
157
- path.join(hqConfigDir, "menubar.json"),
158
- "hqPath",
159
- );
160
- if (fromMenubar) return fromMenubar;
161
- return readJsonStringField(
162
- path.join(hqConfigDir, "config.json"),
163
- "hqFolderPath",
164
- );
165
- }
166
-
167
- /** Read a top-level string field from a JSON file, or undefined on any miss. */
168
- function readJsonStringField(file: string, field: string): string | undefined {
169
- try {
170
- const parsed = JSON.parse(fs.readFileSync(file, "utf-8")) as Record<
171
- string,
172
- unknown
173
- >;
174
- const value = parsed[field];
175
- return typeof value === "string" && value.length > 0 ? value : undefined;
176
- } catch {
177
- return undefined;
178
- }
179
- }
180
-
181
- /** Well-known parent locations an HQ tree commonly lives in (mirrors hq-sync). */
182
- function hqDiscoveryCandidates(): string[] {
183
- const home = os.homedir();
184
- return [
185
- path.join(home, "HQ"),
186
- path.join(home, "hq"),
187
- path.join(home, "Documents", "HQ"),
188
- path.join(home, "Documents", "hq"),
189
- path.join(home, "Desktop", "HQ"),
190
- path.join(home, "Desktop", "hq"),
191
- ];
192
- }
193
-
194
- /**
195
- * Scan the well-known locations for a folder carrying a valid `core.yaml`
196
- * signature. First match wins; returns undefined if none qualify. Cheap — a
197
- * few stats plus one small YAML parse on a hit.
198
- */
199
- function discoverHqRootViaCoreYaml(): string | undefined {
200
- return hqDiscoveryCandidates().find(isValidHqRootSignature);
201
- }
202
-
203
- /**
204
- * True iff `dir` carries a valid hq-core `core.yaml` — canonical
205
- * (`<dir>/core/core.yaml`, hq-core ≥v14) or legacy (`<dir>/core.yaml`) — that
206
- * parses as YAML and has BOTH the `version` and `hqVersion` fields. The dual
207
- * field check (matching hq-sync's `is_valid_hq_root`) stops a stray `core.yaml`
208
- * from another tool from false-matching.
209
- */
210
- function isValidHqRootSignature(dir: string): boolean {
211
- const canonical = path.join(dir, "core", "core.yaml");
212
- const legacy = path.join(dir, "core.yaml");
213
- let file: string;
214
- if (fileExists(canonical)) file = canonical;
215
- else if (fileExists(legacy)) file = legacy;
216
- else return false;
217
- try {
218
- const parsed = yaml.load(fs.readFileSync(file, "utf-8")) as
219
- | Record<string, unknown>
220
- | null
221
- | undefined;
222
- return (
223
- !!parsed &&
224
- typeof parsed === "object" &&
225
- "version" in parsed &&
226
- "hqVersion" in parsed
227
- );
228
- } catch {
229
- return false;
230
- }
231
- }
232
-
233
- /** True iff `p` exists and is a regular file. */
234
- function fileExists(p: string): boolean {
235
- try {
236
- return fs.statSync(p).isFile();
237
- } catch {
238
- return false;
239
- }
240
- }
241
-
242
- /**
243
- * True iff `dir` is an HQ root for the cwd walk-up: it has a `core.yaml`
244
- * (canonical `core/core.yaml` since hq-core v14, or legacy `<dir>/core.yaml`)
245
- * AND a sibling `companies/` directory. The `companies/` discriminator keeps
246
- * the walk from latching onto the inner `core/` subtree, which carries its own
247
- * `core.yaml` but no `companies/`.
248
- */
249
- function isHqRoot(dir: string): boolean {
250
- const hasCoreYaml =
251
- fileExists(path.join(dir, "core", "core.yaml")) ||
252
- fileExists(path.join(dir, "core.yaml"));
253
- if (!hasCoreYaml) return false;
254
- try {
255
- return fs.statSync(path.join(dir, "companies")).isDirectory();
256
- } catch {
257
- return false;
258
- }
259
- }
260
-
261
- export const DEFAULT_HQ_ROOT = resolveDefaultHqRoot();
262
-
263
- /**
264
- * Return a non-expired Cognito access token, refreshing or browser-logging-in
265
- * as needed. Cache lives at ~/.hq/cognito-tokens.json.
266
- *
267
- * Pass `interactive: false` from automated contexts (e.g. the `hq-auth-refresh`
268
- * bin invoked by the deploy skill) where failing fast is better than opening
269
- * a browser.
270
- */
271
- export async function ensureCognitoToken(options: {
272
- interactive?: boolean;
273
- } = {}): Promise<string> {
274
- const interactive = options.interactive ?? true;
275
-
276
- // Machine identities (company agents) mint sessions on demand via
277
- // USER_PASSWORD_AUTH — no refresh-token dance, no browser. The vault API's
278
- // JWT authorizer accepts ID tokens, and the agent's identity claims
279
- // (custom:entityType=agent, custom:entityUid=agt_*) ride the ID token ONLY,
280
- // so vault-API calls from a machine identity send the ID token. The cached
281
- // token file keeps correct field semantics (real access token in
282
- // accessToken) for consumers that need token_use=access, e.g. the deploy
283
- // API via the deploy skill.
284
- if (isMachineIdentity()) {
285
- const machine = await getValidMachineTokens(DEFAULT_COGNITO);
286
- return machine.idToken;
287
- }
288
-
289
- const cached = loadCachedTokens();
290
-
291
- if (cached && !isExpiring(cached, 120)) {
292
- return cached.accessToken;
293
- }
294
-
295
- if (cached) {
296
- try {
297
- if (interactive) {
298
- console.error(chalk.dim(" Refreshing expiring HQ session..."));
299
- }
300
- const refreshed = await refreshTokens(DEFAULT_COGNITO, cached.refreshToken);
301
- return refreshed.accessToken;
302
- } catch (err) {
303
- if (interactive) {
304
- console.error(
305
- chalk.dim(
306
- ` Refresh failed (${err instanceof Error ? err.message : err}), falling back to browser login`,
307
- ),
308
- );
309
- }
310
- }
311
- }
312
-
313
- if (!interactive) {
314
- throw new Error(
315
- "No valid HQ session and interactive login is disabled. Run `hq login` first.",
316
- );
317
- }
318
-
319
- console.error(chalk.cyan(" No cached HQ session — launching browser sign-in..."));
320
- const tokens = await browserLogin(DEFAULT_COGNITO);
321
- return tokens.accessToken;
322
- }
323
-
324
- /**
325
- * Return a non-expired Cognito **ID** token (mirrors `ensureCognitoToken`'s
326
- * refresh/login flow but yields the id_token, not the access token).
327
- *
328
- * Staff-gated vault-API routes (`assertStaffCaller`) read the `email` claim,
329
- * which Cognito puts on the ID token ONLY — the access token has no `email`, so
330
- * sending it returns `403 NO_EMAIL_CLAIM`. Callers performing a staff action
331
- * (e.g. `hq publish --as <handle>`) MUST send the ID token. For machine
332
- * identities the vault API already authenticates with the ID token, so this is
333
- * identical to `ensureCognitoToken` there.
334
- */
335
- export async function ensureCognitoIdToken(options: {
336
- interactive?: boolean;
337
- } = {}): Promise<string> {
338
- const interactive = options.interactive ?? true;
339
-
340
- if (isMachineIdentity()) {
341
- const machine = await getValidMachineTokens(DEFAULT_COGNITO);
342
- return machine.idToken;
343
- }
344
-
345
- const cached = loadCachedTokens();
346
-
347
- if (cached && !isExpiring(cached, 120)) {
348
- return cached.idToken;
349
- }
350
-
351
- if (cached) {
352
- try {
353
- if (interactive) {
354
- console.error(chalk.dim(" Refreshing expiring HQ session..."));
355
- }
356
- const refreshed = await refreshTokens(DEFAULT_COGNITO, cached.refreshToken);
357
- return refreshed.idToken;
358
- } catch (err) {
359
- if (interactive) {
360
- console.error(
361
- chalk.dim(
362
- ` Refresh failed (${err instanceof Error ? err.message : err}), falling back to browser login`,
363
- ),
364
- );
365
- }
366
- }
367
- }
368
-
369
- if (!interactive) {
370
- throw new Error(
371
- "No valid HQ session and interactive login is disabled. Run `hq login` first.",
372
- );
373
- }
374
-
375
- console.error(chalk.cyan(" No cached HQ session — launching browser sign-in..."));
376
- const tokens = await browserLogin(DEFAULT_COGNITO);
377
- return tokens.idToken;
378
- }
379
-
380
- /**
381
- * The CLI's own ClientInfo. Resolved once at module load so every VaultClient
382
- * built in this process stamps the same name/version onto its requests.
383
- *
384
- * `hqCoreVersion` is filled in lazily by walking up from cwd to find
385
- * `core/core.yaml` — present only when the CLI is invoked from inside an
386
- * hq-core checkout, so the server can see scaffold-generation skew.
387
- */
388
- export const CLI_CLIENT_INFO: ClientInfo = {
389
- name: CLI_NAME,
390
- version: CLI_VERSION,
391
- hqCoreVersion: detectHqCoreVersion(),
392
- };
393
-
394
- /** Build a VaultServiceConfig with the given access token. */
395
- export function buildVaultConfig(authToken: string): VaultServiceConfig {
396
- return {
397
- apiUrl: DEFAULT_VAULT_API_URL,
398
- authToken,
399
- region: DEFAULT_COGNITO.region,
400
- clientInfo: CLI_CLIENT_INFO,
401
- };
402
- }
403
-
404
- /**
405
- * Ensure a valid cached Cognito session exists right now, non-interactively,
406
- * and return the result. Used by `hq auth refresh` and the `hq-auth-refresh`
407
- * bin. `refreshed: true` means a valid session is ensured now from a healthy
408
- * cache hit, a token refresh, or a machine-token mint. `refreshed: false`
409
- * means no valid session could be ensured non-interactively; the caller gets a
410
- * reason string and can decide what to do next. Never opens a browser.
411
- */
412
- export async function refreshCachedSession(): Promise<{
413
- refreshed: boolean;
414
- reason?: string;
415
- }> {
416
- // Machine identities have no refresh token; ensure a valid cached machine
417
- // session without forcing a re-mint when the cache is already healthy.
418
- if (isMachineIdentity()) {
419
- try {
420
- await getValidMachineTokens(DEFAULT_COGNITO);
421
- return { refreshed: true };
422
- } catch (err) {
423
- return {
424
- refreshed: false,
425
- reason: err instanceof Error ? err.message : String(err),
426
- };
427
- }
428
- }
429
-
430
- const cached = loadCachedTokens();
431
- if (!cached) {
432
- return { refreshed: false, reason: "no cached session" };
433
- }
434
-
435
- if (!isExpiring(cached, 120)) {
436
- return { refreshed: true };
437
- }
438
-
439
- try {
440
- await refreshTokens(DEFAULT_COGNITO, cached.refreshToken);
441
- return { refreshed: true };
442
- } catch (err) {
443
- return {
444
- refreshed: false,
445
- reason: err instanceof Error ? err.message : String(err),
446
- };
447
- }
448
- }
@@ -1,42 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import {
3
- CompanySelectionError,
4
- isCompanySelectionError,
5
- } from "./company-selection-error.js";
6
-
7
- describe("isCompanySelectionError", () => {
8
- // HQ-CLI-7: the exact error that flooded Sentry when a user with multiple
9
- // active memberships ran a command with no --company.
10
- it("classifies a CompanySelectionError as a selection prompt (skip Sentry)", () => {
11
- const err = new CompanySelectionError(
12
- "Multiple active companies found. Re-run with --company <slug-or-uid> to pick one:\n --company cmp_a\n --company cmp_b",
13
- );
14
- expect(isCompanySelectionError(err)).toBe(true);
15
- });
16
-
17
- it("preserves the user-facing message verbatim for the top-level handler", () => {
18
- const msg = "No active company memberships found. Use --company <slug> to specify.";
19
- expect(new CompanySelectionError(msg).message).toBe(msg);
20
- });
21
-
22
- it("keeps instanceof across the transpile target", () => {
23
- const err = new CompanySelectionError("pick one");
24
- expect(err).toBeInstanceOf(CompanySelectionError);
25
- expect(err).toBeInstanceOf(Error);
26
- expect(err.name).toBe("CompanySelectionError");
27
- });
28
-
29
- // A genuine defect must still reach Sentry — only the disambiguation class is
30
- // diverted, so real bugs are never silently swallowed.
31
- it("does NOT match a plain Error (so real faults still report)", () => {
32
- expect(isCompanySelectionError(new Error("Multiple active companies found."))).toBe(false);
33
- expect(isCompanySelectionError(new Error("boom"))).toBe(false);
34
- });
35
-
36
- it("does NOT match non-error values", () => {
37
- expect(isCompanySelectionError(null)).toBe(false);
38
- expect(isCompanySelectionError(undefined)).toBe(false);
39
- expect(isCompanySelectionError("Multiple active companies found.")).toBe(false);
40
- expect(isCompanySelectionError({ message: "pick one" })).toBe(false);
41
- });
42
- });
@@ -1,45 +0,0 @@
1
- // src/utils/company-selection-error.ts
2
- //
3
- // Classify the "the caller must pick a company with --company" conditions
4
- // (HQ-CLI-7). These are expected, user-actionable disambiguation prompts —
5
- // NOT hq-cli defects — so the top-level catch prints the message and exits
6
- // non-zero but SKIPS Sentry capture, mirroring the EPIPE (HQ-6B),
7
- // intercepted-process-exit (HQ-CLI-3), and environmental-FS (HQ-CLI-2)
8
- // carve-outs.
9
- //
10
- // HQ-CLI-7: a user with THREE active company memberships ran `hq integrations`
11
- // with no `--company`. `resolveCompanyFromMemberships` correctly threw
12
- // "Multiple active companies found. Re-run with --company <slug-or-uid>…" —
13
- // the message literally tells the user how to proceed — but it propagated to
14
- // the CLI's top-level handler as a plain Error and was shipped to Sentry as a
15
- // fatal. The command needs the human to disambiguate; the code cannot pick a
16
- // company for them, so this is normal usage, not a crash to triage.
17
-
18
- /**
19
- * Thrown when the CLI cannot resolve a single company on the user's behalf and
20
- * the user must re-run with `--company <slug-or-uid>`:
21
- * - they have multiple active memberships and passed no `--company`,
22
- * - they have no active membership and passed no `--company`, or
23
- * - a `--company` slug collides across companies, none in their namespace.
24
- *
25
- * The `message` is already user-facing and actionable — the top-level handler
26
- * prints it verbatim and skips Sentry capture.
27
- */
28
- export class CompanySelectionError extends Error {
29
- constructor(message: string) {
30
- super(message);
31
- this.name = "CompanySelectionError";
32
- // Preserve `instanceof` across the TS→ES5/ES2015 transpile target.
33
- Object.setPrototypeOf(this, CompanySelectionError.prototype);
34
- }
35
- }
36
-
37
- /**
38
- * True when `err` is a company-selection disambiguation prompt the user must
39
- * resolve with `--company`. Callers should print `err.message` and SKIP Sentry
40
- * capture (expected usage, no defect) while preserving a non-zero exit. Genuine
41
- * faults are plain `Error`s and return `false`, so real bugs still report.
42
- */
43
- export function isCompanySelectionError(err: unknown): boolean {
44
- return err instanceof CompanySelectionError;
45
- }
@@ -1,83 +0,0 @@
1
- /**
2
- * The contribution registry -- the SINGLE declarative source of truth (US-003)
3
- * for the `contributes.* -> host` mapping. Every other surface DERIVES from
4
- * this table:
5
- *
6
- * - `PackContributeKey` (types.ts) = `keyof typeof CONTRIBUTION_TABLE`
7
- * - `linkFor` / `contributionLinks` read `payload` + `host`
8
- * - `validateManifest`'s payload check reads `payload`
9
- * - `core/scripts/scan-packages.sh` reads a data block GENERATED
10
- * from this table
11
- * (scripts/generate-scan-packages-table.mjs)
12
- *
13
- * Adding a contribution type is ONE row here, not a five-site edit. The parity
14
- * test (`pack-contributions.test.ts`) asserts every surface agrees on the full
15
- * key-set, the payload suffix, and the wire mode.
16
- *
17
- * Row fields:
18
- * - `payload`: path INSIDE the pack, with the literal token `{item}` for the
19
- * declared name. The suffix after `{item}` (e.g. `.md`, `.json`, or none)
20
- * IS the load-bearing per-key shape.
21
- * - `host`: for `wire: 'symlink'`, the host DIRECTORY (relative to the HQ
22
- * root) the symlink is created under -- the symlink dst is
23
- * `<host>/<expanded-item-basename>`. For `wire: 'merge'`, a non-path
24
- * SENTINEL describing the merge target (e.g. `merge:claude+codex`); it is
25
- * NEVER used as a filesystem path.
26
- * - `wire`: `symlink` (a single `ln -s` into `host`) or `merge` (merged into
27
- * a shared host config; see US-004/US-005). Symlink-only readers MUST skip
28
- * `merge` rows.
29
- *
30
- * NOTE: the `mcp` row is DECLARED here as data only. US-003 does NOT wire any
31
- * MCP behavior -- the merge engine lands in US-004/US-005. Declaring it now
32
- * makes the table the single source so adding the merge wiring is a code change
33
- * against an already-present row, and the parity test guards it from day one.
34
- */
35
-
36
- export type WireMode = 'symlink' | 'merge';
37
-
38
- export interface ContributionRow {
39
- /** Path inside the pack, with `{item}` substituted for the declared name. */
40
- payload: string;
41
- /**
42
- * `wire: 'symlink'` -> host directory the symlink lives under (HQ-root
43
- * relative). `wire: 'merge'` -> a non-path sentinel for the merge target.
44
- */
45
- host: string;
46
- wire: WireMode;
47
- }
48
-
49
- /**
50
- * The 8-key contribution registry. `as const` so `keyof typeof` yields the
51
- * exact literal key union consumed by `PackContributeKey`.
52
- */
53
- export const CONTRIBUTION_TABLE = {
54
- workers: { payload: 'workers/{item}', host: 'core/workers/public', wire: 'symlink' },
55
- knowledge: { payload: 'knowledge/{item}', host: 'core/knowledge/public', wire: 'symlink' },
56
- skills: { payload: 'skills/{item}', host: '.claude/skills', wire: 'symlink' },
57
- commands: { payload: 'commands/{item}.md', host: '.claude/commands', wire: 'symlink' },
58
- hooks: { payload: 'hooks/{item}.sh', host: '.claude/hooks', wire: 'symlink' },
59
- policies: { payload: 'policies/{item}.md', host: 'core/policies', wire: 'symlink' },
60
- scripts: { payload: 'scripts/{item}', host: 'core/scripts', wire: 'symlink' },
61
- // wire:merge -- DECLARED as data (US-003); the merge engine ships in
62
- // US-004/US-005. Symlink readers skip this row.
63
- mcp: { payload: 'mcp/{item}.json', host: 'merge:claude+codex', wire: 'merge' },
64
- } as const satisfies Record<string, ContributionRow>;
65
-
66
- /** The declared contribution keys, derived once from the table. */
67
- export type ContributionKey = keyof typeof CONTRIBUTION_TABLE;
68
-
69
- /** All contribution keys, in declaration order. */
70
- export const CONTRIBUTION_KEYS = Object.keys(CONTRIBUTION_TABLE) as ContributionKey[];
71
-
72
- /**
73
- * Expand a row's `payload` template for a concrete item name. Returns the
74
- * pack-relative path of the contribution's payload (e.g. `commands/foo.md`).
75
- */
76
- export function payloadFor(key: ContributionKey, item: string): string {
77
- return CONTRIBUTION_TABLE[key].payload.replace('{item}', item);
78
- }
79
-
80
- /** Keys whose contributions are wired by a host symlink (skip `merge` rows). */
81
- export const SYMLINK_KEYS = CONTRIBUTION_KEYS.filter(
82
- (k) => CONTRIBUTION_TABLE[k].wire === 'symlink',
83
- );
@@ -1,45 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { environmentalFsErrorMessage } from "./environmental-error.js";
3
-
4
- /** Build a Node-style errno error with a `.code`, as fs.*Sync throws. */
5
- function errnoError(code: string, message: string): NodeJS.ErrnoException {
6
- const err = new Error(message) as NodeJS.ErrnoException;
7
- err.code = code;
8
- return err;
9
- }
10
-
11
- describe("environmentalFsErrorMessage", () => {
12
- // HQ-CLI-2: the exact error that crashed `hq reindex` and flooded Sentry.
13
- it("classifies ENOSPC (no space left on device) as environmental", () => {
14
- const err = errnoError("ENOSPC", "ENOSPC: no space left on device, open '/x'");
15
- const msg = environmentalFsErrorMessage(err);
16
- expect(msg).not.toBeNull();
17
- expect(msg).toMatch(/no space left on device/i);
18
- });
19
-
20
- it("classifies EDQUOT (quota exceeded) as environmental", () => {
21
- const msg = environmentalFsErrorMessage(errnoError("EDQUOT", "EDQUOT: disk quota exceeded"));
22
- expect(msg).toMatch(/quota/i);
23
- });
24
-
25
- it("classifies EROFS (read-only filesystem) as environmental", () => {
26
- const msg = environmentalFsErrorMessage(errnoError("EROFS", "EROFS: read-only file system"));
27
- expect(msg).toMatch(/read-only/i);
28
- });
29
-
30
- // A genuine code bug must still reach Sentry — only the disk-class codes are
31
- // diverted, so we never silently swallow real defects.
32
- it("returns null for a code-bug error (ENOENT) so it is still captured", () => {
33
- expect(environmentalFsErrorMessage(errnoError("ENOENT", "ENOENT: not found"))).toBeNull();
34
- });
35
-
36
- it("returns null for a plain Error with no code", () => {
37
- expect(environmentalFsErrorMessage(new Error("boom"))).toBeNull();
38
- });
39
-
40
- it("returns null for non-error values (null/undefined/string)", () => {
41
- expect(environmentalFsErrorMessage(null)).toBeNull();
42
- expect(environmentalFsErrorMessage(undefined)).toBeNull();
43
- expect(environmentalFsErrorMessage("ENOSPC")).toBeNull();
44
- });
45
- });