@indigoai-us/hq-cli 5.77.13 → 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 (307) hide show
  1. package/CHANGELOG.md +34 -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 +51 -2
  33. package/dist/commands/pkg-install.js +43 -0
  34. package/dist/commands/run.js +8 -0
  35. package/dist/commands/secrets.js +125 -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 -2247
  144. package/src/commands/pack-update-cache.test.ts +0 -149
  145. package/src/commands/packs.test.ts +0 -88
  146. package/src/commands/packs.ts +0 -622
  147. package/src/commands/people.test.ts +0 -807
  148. package/src/commands/people.ts +0 -422
  149. package/src/commands/pkg-install.dispatch.test.ts +0 -71
  150. package/src/commands/pkg-install.ts +0 -215
  151. package/src/commands/pkg-list.test.ts +0 -79
  152. package/src/commands/pkg-list.ts +0 -13
  153. package/src/commands/pkg-remove.ts +0 -71
  154. package/src/commands/pkg-update.ts +0 -189
  155. package/src/commands/publish.test.ts +0 -650
  156. package/src/commands/publish.ts +0 -580
  157. package/src/commands/reindex.test.ts +0 -259
  158. package/src/commands/reindex.ts +0 -274
  159. package/src/commands/rescue.test.ts +0 -39
  160. package/src/commands/rescue.ts +0 -210
  161. package/src/commands/run.env-local.test.ts +0 -88
  162. package/src/commands/run.test.ts +0 -45
  163. package/src/commands/run.ts +0 -184
  164. package/src/commands/safe-extract.test.ts +0 -459
  165. package/src/commands/safe-extract.ts +0 -444
  166. package/src/commands/secrets-scope.test.ts +0 -56
  167. package/src/commands/secrets-scope.ts +0 -32
  168. package/src/commands/secrets.parse-destination.test.ts +0 -38
  169. package/src/commands/secrets.test.ts +0 -2037
  170. package/src/commands/secrets.ts +0 -2240
  171. package/src/commands/signals.ts +0 -351
  172. package/src/commands/skill.test.ts +0 -770
  173. package/src/commands/skill.ts +0 -796
  174. package/src/commands/sources.ts +0 -362
  175. package/src/commands/sync-mode.test.ts +0 -366
  176. package/src/commands/sync-mode.ts +0 -387
  177. package/src/commands/sync-narrow.test.ts +0 -633
  178. package/src/commands/sync-narrow.ts +0 -555
  179. package/src/commands/sync.ts +0 -165
  180. package/src/commands/team-sync.ts +0 -628
  181. package/src/commands/update.ts +0 -71
  182. package/src/commands/whoami.ts +0 -83
  183. package/src/commands/workers.test.ts +0 -158
  184. package/src/commands/workers.ts +0 -301
  185. package/src/index.test.ts +0 -32
  186. package/src/index.ts +0 -20
  187. package/src/lib/db/control-plane.test.ts +0 -59
  188. package/src/lib/db/control-plane.ts +0 -113
  189. package/src/lib/db/local.test.ts +0 -81
  190. package/src/lib/db/local.ts +0 -148
  191. package/src/lib/db/migrate.test.ts +0 -133
  192. package/src/lib/db/migrate.ts +0 -137
  193. package/src/lib/db/paths.test.ts +0 -112
  194. package/src/lib/db/paths.ts +0 -128
  195. package/src/lib/db/remote-engine.test.ts +0 -44
  196. package/src/lib/db/remote-engine.ts +0 -148
  197. package/src/lib/db/remote-sql.test.ts +0 -32
  198. package/src/lib/db/remote-sql.ts +0 -62
  199. package/src/lib/db/sql.test.ts +0 -106
  200. package/src/lib/db/sql.ts +0 -192
  201. package/src/lib/hq-cloud-dep.smoke.test.ts +0 -75
  202. package/src/lib/local-tree-diff.test.ts +0 -301
  203. package/src/lib/local-tree-diff.ts +0 -362
  204. package/src/lib/narrow-hint-banner.test.ts +0 -235
  205. package/src/lib/narrow-hint-banner.ts +0 -212
  206. package/src/lib/plan-limit-nag.test.ts +0 -317
  207. package/src/lib/plan-limit-nag.ts +0 -264
  208. package/src/main.ts +0 -371
  209. package/src/node-preflight.test.ts +0 -60
  210. package/src/node-preflight.ts +0 -67
  211. package/src/outpost/session-heartbeat-bounds.test.ts +0 -195
  212. package/src/outpost/session-heartbeat-guard.test.ts +0 -105
  213. package/src/outpost/session-heartbeat-publisher.test.ts +0 -178
  214. package/src/outpost/session-heartbeat-publisher.ts +0 -186
  215. package/src/outpost/session-heartbeat-retain-guard.test.ts +0 -126
  216. package/src/outpost/session-heartbeat.test.ts +0 -459
  217. package/src/outpost/session-heartbeat.ts +0 -877
  218. package/src/packaging.test.ts +0 -109
  219. package/src/run/__fixtures__/discover-schemas/example.env.schema +0 -4
  220. package/src/run/discover-schemas.test.ts +0 -153
  221. package/src/run/discover-schemas.ts +0 -79
  222. package/src/run/hq-plugin.test.ts +0 -371
  223. package/src/run/hq-plugin.ts +0 -277
  224. package/src/run/varlock-shape.test.ts +0 -57
  225. package/src/schemas/hq-package.schema.json +0 -155
  226. package/src/sentry-before-send.test.ts +0 -359
  227. package/src/sentry-before-send.ts +0 -221
  228. package/src/sentry-epipe.test.ts +0 -37
  229. package/src/sentry-release.test.ts +0 -54
  230. package/src/sentry.test.ts +0 -24
  231. package/src/sentry.ts +0 -54
  232. package/src/strategies/link.ts +0 -62
  233. package/src/strategies/merge.ts +0 -142
  234. package/src/types.ts +0 -146
  235. package/src/utils/__fixtures__/scan-packages.generated-block.sh +0 -23
  236. package/src/utils/auth-error.test.ts +0 -40
  237. package/src/utils/auth-error.ts +0 -42
  238. package/src/utils/billing-gate.test.ts +0 -95
  239. package/src/utils/billing-gate.ts +0 -228
  240. package/src/utils/breadcrumb-buffer.ts +0 -18
  241. package/src/utils/cli-telemetry.test.ts +0 -153
  242. package/src/utils/cli-telemetry.ts +0 -61
  243. package/src/utils/cognito-session.machine.test.ts +0 -109
  244. package/src/utils/cognito-session.refresh.test.ts +0 -84
  245. package/src/utils/cognito-session.test.ts +0 -328
  246. package/src/utils/cognito-session.ts +0 -448
  247. package/src/utils/company-selection-error.test.ts +0 -42
  248. package/src/utils/company-selection-error.ts +0 -45
  249. package/src/utils/contribution-table.ts +0 -83
  250. package/src/utils/environmental-error.test.ts +0 -45
  251. package/src/utils/environmental-error.ts +0 -39
  252. package/src/utils/epipe.test.ts +0 -28
  253. package/src/utils/epipe.ts +0 -29
  254. package/src/utils/expected-cli-error.test.ts +0 -28
  255. package/src/utils/expected-cli-error.ts +0 -39
  256. package/src/utils/feedback-diagnostics.test.ts +0 -183
  257. package/src/utils/feedback-diagnostics.ts +0 -123
  258. package/src/utils/feedback-screenshots.test.ts +0 -134
  259. package/src/utils/feedback-screenshots.ts +0 -124
  260. package/src/utils/feedback-versions.test.ts +0 -98
  261. package/src/utils/feedback-versions.ts +0 -68
  262. package/src/utils/git.ts +0 -74
  263. package/src/utils/integrity.ts +0 -59
  264. package/src/utils/intercepted-process-exit.test.ts +0 -37
  265. package/src/utils/intercepted-process-exit.ts +0 -36
  266. package/src/utils/login-provider.test.ts +0 -42
  267. package/src/utils/login-provider.ts +0 -30
  268. package/src/utils/manifest.test.ts +0 -167
  269. package/src/utils/manifest.ts +0 -131
  270. package/src/utils/pack-contributions.test.ts +0 -603
  271. package/src/utils/pack-contributions.ts +0 -450
  272. package/src/utils/people.ts +0 -215
  273. package/src/utils/registry-client.ts +0 -204
  274. package/src/utils/registry.ts +0 -90
  275. package/src/utils/resolve-vault-credential.test.ts +0 -69
  276. package/src/utils/resolve-vault-credential.ts +0 -60
  277. package/src/utils/sandbox-runner-client.test.ts +0 -279
  278. package/src/utils/sandbox-runner-client.ts +0 -260
  279. package/src/utils/secrets-cache.ts +0 -183
  280. package/src/utils/sentry-identity.ts +0 -45
  281. package/src/utils/settle-with-timeout.test.ts +0 -21
  282. package/src/utils/settle-with-timeout.ts +0 -22
  283. package/src/utils/user-yaml-error.test.ts +0 -24
  284. package/src/utils/user-yaml-error.ts +0 -30
  285. package/src/utils/vault-api.test.ts +0 -536
  286. package/src/utils/vault-api.ts +0 -382
  287. package/src/utils/version-check.test.ts +0 -176
  288. package/src/utils/version-check.ts +0 -155
  289. package/src/utils/version-gate.test.ts +0 -1024
  290. package/src/utils/version-gate.ts +0 -702
  291. package/test/commands/db-tenant-isolation.test.ts +0 -94
  292. package/test/commands/db.test.ts +0 -85
  293. package/test/commands/signals.test.ts +0 -207
  294. package/test/commands/sources.test.ts +0 -231
  295. package/test/e2e/smoke-install-mcp.sh +0 -113
  296. package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +0 -1
  297. package/test/fixtures/hq-pack-smoke-mcp/package.yaml +0 -11
  298. package/test/fixtures/signals/action_item/sample.md +0 -16
  299. package/test/fixtures/signals/summary/sample.md +0 -12
  300. package/test/fixtures/sources/meetings/sample.md +0 -25
  301. package/test/helpers/cli-runner.ts +0 -150
  302. package/test/helpers/s3-list-mock.ts +0 -79
  303. package/test/helpers/vault-service-mock.ts +0 -295
  304. package/test/sources-signals/smoke.test.ts +0 -228
  305. package/tsconfig.json +0 -20
  306. package/vitest.config.ts +0 -11
  307. package/vitest.e2e.config.ts +0 -7
@@ -1,603 +0,0 @@
1
- /**
2
- * pack-contributions tests — pins the content-pack lifecycle helpers that
3
- * commands/packs.ts (list / update / uninstall) depends on.
4
- *
5
- * The load-bearing invariant is the contributes->host-path mapping: it MUST
6
- * agree byte-for-byte with core/scripts/scan-packages.sh (the wiring
7
- * authority) and pack-install.ts:validateManifest. A drift there means
8
- * uninstall would miss symlinks (dangling links) or delete the wrong path.
9
- */
10
-
11
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
12
- import * as fs from 'fs';
13
- import * as os from 'os';
14
- import * as path from 'path';
15
- import { fileURLToPath } from 'node:url';
16
- import {
17
- contributionLinks,
18
- linkStatus,
19
- listInstalledPacks,
20
- findDependentPacks,
21
- unwirePack,
22
- unwirePackMcp,
23
- packagesDir,
24
- readHqVersion,
25
- type WiredLink,
26
- type InstalledPack,
27
- } from './pack-contributions.js';
28
- import { parse as parseToml } from 'smol-toml';
29
- import {
30
- registerClaudeServer,
31
- registerServer,
32
- claudeConfigPath,
33
- codexConfigPath,
34
- codexHome,
35
- } from '../commands/mcp-registration.js';
36
- import {
37
- CONTRIBUTION_TABLE,
38
- CONTRIBUTION_KEYS,
39
- SYMLINK_KEYS,
40
- payloadFor,
41
- type ContributionKey,
42
- } from './contribution-table.js';
43
- import type { PackContributeKey } from '../types.js';
44
-
45
- // The canonical mapping is DERIVED from the single-source CONTRIBUTION_TABLE
46
- // (US-003) -- the oracle is no longer a hand-restated copy. For each SYMLINK
47
- // key, src = the payload expanded for item "X"; dst = <host-dir>/<basename>.
48
- const EXPECTED: Record<ContributionKey, { src: string; dst: string }> =
49
- Object.fromEntries(
50
- SYMLINK_KEYS.map((k) => {
51
- const src = payloadFor(k, 'X');
52
- const dst = `${CONTRIBUTION_TABLE[k].host}/${src.split('/').pop()}`;
53
- return [k, { src, dst }];
54
- }),
55
- ) as Record<ContributionKey, { src: string; dst: string }>;
56
-
57
- function mkTmp(prefix: string): string {
58
- // realpathSync so macOS /tmp -> /private/tmp doesn't break path equality.
59
- return fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), prefix)));
60
- }
61
-
62
- describe('contributionLinks: mapping', () => {
63
- it('maps every SYMLINK contributes key to the canonical src/dst', () => {
64
- const hqRoot = '/hq';
65
- const packDir = '/hq/core/packages/hq-pack-x';
66
- for (const key of SYMLINK_KEYS) {
67
- const [link] = contributionLinks(hqRoot, packDir, { [key]: ['X'] });
68
- expect(link.key).toBe(key);
69
- expect(link.src).toBe(path.join(packDir, EXPECTED[key].src));
70
- expect(link.dst).toBe(path.join(hqRoot, EXPECTED[key].dst));
71
- }
72
- });
73
-
74
- it('produces NO symlink for wire:merge keys (mcp)', () => {
75
- // The mcp row is wire:merge — declared as data (US-003) but never wired as a
76
- // symlink here; its host effect is a config merge (US-004/US-005).
77
- const mergeKeys = CONTRIBUTION_KEYS.filter(
78
- (k) => CONTRIBUTION_TABLE[k].wire === 'merge',
79
- );
80
- expect(mergeKeys).toContain('mcp'); // guards the row exists
81
- for (const key of mergeKeys) {
82
- const links = contributionLinks('/hq', '/p', { [key]: ['X'] });
83
- expect(links).toEqual([]);
84
- }
85
- });
86
-
87
- it('ignores empty / non-array contributes subfields', () => {
88
- const links = contributionLinks('/hq', '/p', {
89
- skills: [],
90
- workers: ['a', 'b'],
91
- // @ts-expect-error — defensive: non-array should be skipped
92
- hooks: 'nope',
93
- });
94
- expect(links.map((l) => l.item)).toEqual(['a', 'b']);
95
- });
96
- });
97
-
98
- // ---------------------------------------------------------------------------
99
- // US-003 single-source parity: FULL key-set + payload-suffix + wire-mode
100
- // equivalence across every surface (table, derived union, validateManifest's
101
- // payload reader, and the generated scan-packages.sh block). Replaces the old
102
- // substring-only `toContain` check, which passed even if a key was MISSING, had
103
- // the WRONG payload suffix, or the WRONG wire mode.
104
- // ---------------------------------------------------------------------------
105
- describe('US-003 parity: contribution table is the single source', () => {
106
- // The bash block that `core/scripts/scan-packages.sh` reads is GENERATED from
107
- // the contribution table (scripts/generate-scan-packages-table.mjs) and lives
108
- // in hq-core — a separately-versioned repo this package never contains. We
109
- // vendor that generated block as a committed golden fixture and compare the
110
- // table against it, so this guard runs DETERMINISTICALLY in CI and from any
111
- // checkout. Regenerate after any table change: `pnpm gen:scan-golden`
112
- // (hq-core's own scan-packages.sh must be regenerated from the same table —
113
- // that cross-repo leg is enforced in the monorepo that holds both).
114
- //
115
- // Anchored to THIS module via import.meta.url — never process.cwd().
116
- // REGRESSION (do NOT reintroduce): the old resolver did
117
- // path.resolve(process.cwd(), '../../../core/scripts/scan-packages.sh')
118
- // which, from a checkout sitting 3 levels below an installed HQ (e.g.
119
- // HQ/workspace/worktrees/<name>), bound to that unrelated, differently-
120
- // versioned hq-core script and false-failed; from anywhere else it resolved
121
- // outside the repo and silently skipped, so the guard NEVER ran — not even in
122
- // CI. See the module-anchor regression test below.
123
- const goldenBlockPath = path.resolve(
124
- path.dirname(fileURLToPath(import.meta.url)),
125
- '__fixtures__',
126
- 'scan-packages.generated-block.sh',
127
- );
128
-
129
- // Parse the generated `contrib_row` case arms back into {key:{payload,host,wire}}.
130
- function parseBashTable(bash: string): Record<
131
- string,
132
- { payload: string; host: string; wire: string }
133
- > {
134
- const out: Record<string, { payload: string; host: string; wire: string }> = {};
135
- const re =
136
- /^\s*([a-z]+)\)\s*printf\s+'%s'\s+'([^|]+)\|([^|]+)\|([^']+)'\s*;;/gm;
137
- let m;
138
- while ((m = re.exec(bash)) !== null) {
139
- out[m[1]] = { payload: m[2], host: m[3], wire: m[4] };
140
- }
141
- return out;
142
- }
143
-
144
- it('the derived PackContributeKey union == the table keys (8 keys incl. mcp)', () => {
145
- // CONTRIBUTION_KEYS IS PackContributeKey's source (types.ts derives the
146
- // union from it). Assert the full expected set is present, no more, no less.
147
- expect([...CONTRIBUTION_KEYS].sort()).toEqual(
148
- [
149
- 'commands',
150
- 'hooks',
151
- 'knowledge',
152
- 'mcp',
153
- 'policies',
154
- 'scripts',
155
- 'skills',
156
- 'workers',
157
- ].sort(),
158
- );
159
- // Type-level: every key is assignable to PackContributeKey.
160
- const asUnion: PackContributeKey[] = [...CONTRIBUTION_KEYS];
161
- expect(asUnion.length).toBe(8);
162
- });
163
-
164
- it('every key has a wire mode of exactly symlink|merge, mcp is the merge row', () => {
165
- for (const k of CONTRIBUTION_KEYS) {
166
- expect(['symlink', 'merge']).toContain(CONTRIBUTION_TABLE[k].wire);
167
- }
168
- expect(CONTRIBUTION_TABLE.mcp.wire).toBe('merge');
169
- expect(CONTRIBUTION_TABLE.mcp.payload).toBe('mcp/{item}.json');
170
- expect(CONTRIBUTION_TABLE.mcp.host).toBe('merge:claude+codex');
171
- // All non-mcp rows are symlink in this milestone.
172
- for (const k of CONTRIBUTION_KEYS) {
173
- if (k !== 'mcp') expect(CONTRIBUTION_TABLE[k].wire).toBe('symlink');
174
- }
175
- });
176
-
177
- it('payload suffixes are pinned per key (suffix is load-bearing)', () => {
178
- const suffix = (p: string) => {
179
- const base = p.split('/').pop() as string;
180
- const dot = base.indexOf('.', base.indexOf('}'));
181
- return dot >= 0 ? base.slice(dot) : '';
182
- };
183
- expect(suffix(CONTRIBUTION_TABLE.commands.payload)).toBe('.md');
184
- expect(suffix(CONTRIBUTION_TABLE.policies.payload)).toBe('.md');
185
- expect(suffix(CONTRIBUTION_TABLE.hooks.payload)).toBe('.sh');
186
- expect(suffix(CONTRIBUTION_TABLE.mcp.payload)).toBe('.json');
187
- expect(suffix(CONTRIBUTION_TABLE.workers.payload)).toBe('');
188
- expect(suffix(CONTRIBUTION_TABLE.knowledge.payload)).toBe('');
189
- expect(suffix(CONTRIBUTION_TABLE.skills.payload)).toBe('');
190
- expect(suffix(CONTRIBUTION_TABLE.scripts.payload)).toBe('');
191
- });
192
-
193
- it('generated block (vendored golden) matches the table on key-set + payload + host + wire', () => {
194
- const bash = fs.readFileSync(goldenBlockPath, 'utf-8');
195
- const bashTable = parseBashTable(bash);
196
-
197
- // FULL key-set equivalence (fails if the golden is MISSING a key or has an
198
- // EXTRA one — not just a substring presence check). This doubles as the
199
- // drift guard: if the table changed but the golden wasn't regenerated, one
200
- // of these assertions fails with the exact key/field that diverged.
201
- expect(Object.keys(bashTable).sort()).toEqual([...CONTRIBUTION_KEYS].sort());
202
-
203
- // Per-key payload + host + wire-mode equivalence.
204
- for (const k of CONTRIBUTION_KEYS) {
205
- expect(
206
- bashTable[k],
207
- `golden block missing row for "${k}" — run \`pnpm gen:scan-golden\``,
208
- ).toBeDefined();
209
- expect(bashTable[k].payload).toBe(CONTRIBUTION_TABLE[k].payload);
210
- expect(bashTable[k].host).toBe(CONTRIBUTION_TABLE[k].host);
211
- expect(bashTable[k].wire).toBe(CONTRIBUTION_TABLE[k].wire);
212
- }
213
-
214
- // The CONTRIB_KEYS array in the block also lists every key.
215
- const keysLine = bash.match(/CONTRIB_KEYS=\(([^)]*)\)/);
216
- expect(keysLine).not.toBeNull();
217
- const bashKeys = (keysLine as RegExpMatchArray)[1].trim().split(/\s+/).sort();
218
- expect(bashKeys).toEqual([...CONTRIBUTION_KEYS].sort());
219
- });
220
-
221
- it('resolves the golden from the module, never from process.cwd() (regression: a co-located HQ must not bind)', () => {
222
- // Rebuild the exact historical trap: an installed-HQ-shaped dir carrying a
223
- // DIFFERENT scan-packages.sh, with a checkout nested 3 levels below it
224
- // (the HQ/workspace/worktrees/<name> layout that first surfaced this bug).
225
- const fakeHq = mkTmp('fake-hq-');
226
- fs.mkdirSync(path.join(fakeHq, 'core', 'scripts'), { recursive: true });
227
- fs.writeFileSync(
228
- path.join(fakeHq, 'core', 'scripts', 'scan-packages.sh'),
229
- '# unrelated, differently-versioned hq-core script\n',
230
- );
231
- const nested = path.join(fakeHq, 'workspace', 'worktrees', 'hq-cli-x');
232
- fs.mkdirSync(nested, { recursive: true });
233
-
234
- const orig = process.cwd();
235
- try {
236
- process.chdir(nested);
237
- // Module-anchored resolution is unaffected by cwd and points at the repo
238
- // golden — which really exists and is what the parity test reads.
239
- expect(fs.existsSync(goldenBlockPath)).toBe(true);
240
- expect(
241
- goldenBlockPath.endsWith(
242
- path.join('src', 'utils', '__fixtures__', 'scan-packages.generated-block.sh'),
243
- ),
244
- ).toBe(true);
245
- // The OLD cwd-relative resolver WOULD have latched onto the fake file.
246
- // Prove the trap is real, and that we no longer resolve to it.
247
- const oldCwdRelative = path.resolve(
248
- process.cwd(),
249
- '../../../core/scripts/scan-packages.sh',
250
- );
251
- expect(oldCwdRelative).toBe(
252
- path.join(fakeHq, 'core', 'scripts', 'scan-packages.sh'),
253
- );
254
- expect(oldCwdRelative).not.toBe(goldenBlockPath);
255
- } finally {
256
- process.chdir(orig);
257
- fs.rmSync(fakeHq, { recursive: true, force: true });
258
- }
259
- });
260
-
261
- it('validateManifest payload reader (payloadFor) agrees with the table for every key', () => {
262
- for (const k of CONTRIBUTION_KEYS) {
263
- // payloadFor is exactly what validateManifest now uses for the
264
- // payload-existence check, so this pins that surface to the table.
265
- expect(payloadFor(k, 'demo')).toBe(
266
- CONTRIBUTION_TABLE[k].payload.replace('{item}', 'demo'),
267
- );
268
- }
269
- });
270
- });
271
-
272
- describe('linkStatus + unwirePack', () => {
273
- let hqRoot: string;
274
- let packDir: string;
275
-
276
- beforeEach(() => {
277
- hqRoot = mkTmp('hq-pc-root-');
278
- packDir = path.join(packagesDir(hqRoot), 'hq-pack-demo');
279
- // A pack that contributes one skill and one command.
280
- fs.mkdirSync(path.join(packDir, 'skills', 'demo-skill'), { recursive: true });
281
- fs.writeFileSync(path.join(packDir, 'skills', 'demo-skill', 'SKILL.md'), '# demo');
282
- fs.mkdirSync(path.join(packDir, 'commands'), { recursive: true });
283
- fs.writeFileSync(path.join(packDir, 'commands', 'demo-cmd.md'), '# cmd');
284
- });
285
-
286
- afterEach(() => {
287
- fs.rmSync(hqRoot, { recursive: true, force: true });
288
- });
289
-
290
- const contributes = { skills: ['demo-skill'], commands: ['demo-cmd'] };
291
-
292
- function wire(link: WiredLink): void {
293
- fs.mkdirSync(path.dirname(link.dst), { recursive: true });
294
- fs.symlinkSync(link.src, link.dst);
295
- }
296
-
297
- it('reports missing before wiring, live after wiring', () => {
298
- const links = contributionLinks(hqRoot, packDir, contributes);
299
- for (const l of links) expect(linkStatus(l)).toBe('missing');
300
- for (const l of links) wire(l);
301
- for (const l of links) expect(linkStatus(l)).toBe('live');
302
- });
303
-
304
- it('reports broken when the pack payload is gone', () => {
305
- const links = contributionLinks(hqRoot, packDir, contributes);
306
- links.forEach(wire);
307
- fs.rmSync(packDir, { recursive: true, force: true });
308
- for (const l of links) expect(linkStatus(l)).toBe('broken');
309
- });
310
-
311
- it('reports foreign when the dst points elsewhere or is a real file', () => {
312
- const [skillLink, cmdLink] = contributionLinks(hqRoot, packDir, contributes);
313
- // foreign symlink: points at some other path
314
- fs.mkdirSync(path.dirname(skillLink.dst), { recursive: true });
315
- const other = path.join(hqRoot, 'somewhere-else');
316
- fs.mkdirSync(other, { recursive: true });
317
- fs.symlinkSync(other, skillLink.dst);
318
- expect(linkStatus(skillLink)).toBe('foreign');
319
- // foreign real file
320
- fs.mkdirSync(path.dirname(cmdLink.dst), { recursive: true });
321
- fs.writeFileSync(cmdLink.dst, 'host-owned content');
322
- expect(linkStatus(cmdLink)).toBe('foreign');
323
- });
324
-
325
- it('unwirePack removes ONLY our symlinks, leaving foreign ones', () => {
326
- const [skillLink, cmdLink] = contributionLinks(hqRoot, packDir, contributes);
327
- wire(skillLink); // our link
328
- // cmd slot occupied by a foreign symlink (another pack / host content)
329
- fs.mkdirSync(path.dirname(cmdLink.dst), { recursive: true });
330
- const other = path.join(hqRoot, 'foreign-target.md');
331
- fs.writeFileSync(other, 'x');
332
- fs.symlinkSync(other, cmdLink.dst);
333
-
334
- const result = unwirePack(hqRoot, packDir, contributes);
335
-
336
- expect(fs.existsSync(skillLink.dst)).toBe(false); // ours removed
337
- expect(fs.lstatSync(cmdLink.dst).isSymbolicLink()).toBe(true); // foreign kept
338
- expect(result.unlinked.map((u) => u.item)).toEqual(['demo-skill']);
339
- expect(result.skipped.map((s) => s.item)).toEqual(['demo-cmd']);
340
- });
341
-
342
- it('unwirePack removes broken (dangling) links that point into our pack', () => {
343
- const links = contributionLinks(hqRoot, packDir, contributes);
344
- links.forEach(wire);
345
- fs.rmSync(packDir, { recursive: true, force: true }); // now all broken
346
- const result = unwirePack(hqRoot, packDir, contributes);
347
- expect(result.unlinked).toHaveLength(2);
348
- for (const l of links) expect(fs.existsSync(l.dst)).toBe(false);
349
- for (const l of links) expect(() => fs.lstatSync(l.dst)).toThrow(); // gone
350
- });
351
- });
352
-
353
- describe('listInstalledPacks', () => {
354
- let hqRoot: string;
355
-
356
- beforeEach(() => {
357
- hqRoot = mkTmp('hq-pc-list-');
358
- });
359
- afterEach(() => {
360
- fs.rmSync(hqRoot, { recursive: true, force: true });
361
- });
362
-
363
- it('returns [] when core/packages is absent', () => {
364
- expect(listInstalledPacks(hqRoot)).toEqual([]);
365
- });
366
-
367
- it('walks packs, skips README + .archive, and parses the stamped source', () => {
368
- const dir = packagesDir(hqRoot);
369
- fs.mkdirSync(dir, { recursive: true });
370
- fs.writeFileSync(path.join(dir, 'README.md'), '# packages');
371
- fs.mkdirSync(path.join(dir, '.archive', 'old-pack-2026'), { recursive: true });
372
-
373
- const p = path.join(dir, 'hq-pack-one');
374
- fs.mkdirSync(p, { recursive: true });
375
- fs.writeFileSync(
376
- path.join(p, 'package.yaml'),
377
- 'source: "github:acme/repo#packages/hq-pack-one"\nname: hq-pack-one\nversion: 2.1.0\ncontributes:\n skills:\n - foo\n',
378
- );
379
-
380
- const packs = listInstalledPacks(hqRoot);
381
- expect(packs.map((x) => x.name)).toEqual(['hq-pack-one']);
382
- expect(packs[0].manifest?.version).toBe('2.1.0');
383
- expect(packs[0].manifest?.source).toBe('github:acme/repo#packages/hq-pack-one');
384
- });
385
-
386
- it('surfaces an unreadable manifest as error, not a throw', () => {
387
- const dir = packagesDir(hqRoot);
388
- const p = path.join(dir, 'hq-pack-bad');
389
- fs.mkdirSync(p, { recursive: true });
390
- fs.writeFileSync(path.join(p, 'package.yaml'), 'name: [unclosed\n');
391
- const packs = listInstalledPacks(hqRoot);
392
- expect(packs).toHaveLength(1);
393
- expect(packs[0].manifest).toBeNull();
394
- expect(packs[0].error).toMatch(/invalid/i);
395
- });
396
- });
397
-
398
- describe('readHqVersion: v15 layout awareness (feedback_57d7edcf, symptom b)', () => {
399
- let hqRoot: string;
400
-
401
- beforeEach(() => {
402
- hqRoot = mkTmp('hq-readhqversion-');
403
- });
404
-
405
- afterEach(() => {
406
- fs.rmSync(hqRoot, { recursive: true, force: true });
407
- });
408
-
409
- it('reads the CANONICAL core/core.yaml on a v14+/v15 layout', () => {
410
- fs.mkdirSync(path.join(hqRoot, 'core'), { recursive: true });
411
- fs.writeFileSync(
412
- path.join(hqRoot, 'core', 'core.yaml'),
413
- 'version: 1\nhqVersion: "15.0.11"\n',
414
- );
415
- expect(readHqVersion(hqRoot)).toBe('15.0.11');
416
- });
417
-
418
- it('PREFERS canonical core/core.yaml over a STALE leftover root core.yaml', () => {
419
- // The exact reported failure: post-v12→v15 reshape moved core.yaml under
420
- // core/, but a stale root copy (hqVersion 12.1.0) lingered. The version
421
- // read MUST report the canonical 15.x, not the stale root 12.1.0 — else
422
- // `hq install` wrongly rejects packs requiring hqCore >=15.
423
- fs.mkdirSync(path.join(hqRoot, 'core'), { recursive: true });
424
- fs.writeFileSync(
425
- path.join(hqRoot, 'core', 'core.yaml'),
426
- 'version: 1\nhqVersion: "15.0.11"\n',
427
- );
428
- fs.writeFileSync(
429
- path.join(hqRoot, 'core.yaml'),
430
- 'version: 1\nhqVersion: "12.1.0"\n',
431
- );
432
- expect(readHqVersion(hqRoot)).toBe('15.0.11');
433
- });
434
-
435
- it('falls back to the legacy root core.yaml when no nested core/core.yaml exists', () => {
436
- fs.writeFileSync(
437
- path.join(hqRoot, 'core.yaml'),
438
- 'version: 1\nhqVersion: "13.4.0"\n',
439
- );
440
- expect(readHqVersion(hqRoot)).toBe('13.4.0');
441
- });
442
-
443
- it('returns null when neither core/core.yaml nor root core.yaml exists', () => {
444
- expect(readHqVersion(hqRoot)).toBeNull();
445
- });
446
- });
447
-
448
- // ---------------------------------------------------------------------------
449
- // US-009: unwirePackMcp — the wire:merge (MCP) un-registration parallel to the
450
- // symlink unwirePack. Runs against an ISOLATED tmpdir home (env.home) so the
451
- // developer's real ~/.claude.json / ~/.codex are NEVER touched.
452
- // ---------------------------------------------------------------------------
453
-
454
- describe('US-009: unwirePackMcp (mcp un-registration parallel to symlink unwire)', () => {
455
- let mcpHome: string;
456
-
457
- beforeEach(() => {
458
- mcpHome = mkTmp('hq-unwire-mcp-');
459
- });
460
- afterEach(() => {
461
- fs.rmSync(mcpHome, { recursive: true, force: true });
462
- });
463
-
464
- it('un-registers ONLY this pack\'s provenance-stamped mcp servers from ~/.claude.json', () => {
465
- const env = { home: mcpHome };
466
- // The user owns `figma`; our pack installs `vyg`.
467
- fs.writeFileSync(
468
- claudeConfigPath(env),
469
- JSON.stringify({ mcpServers: { figma: { type: 'http', url: 'https://figma' } } }, null, 2) + '\n',
470
- );
471
- registerClaudeServer({ name: 'vyg', manifest: { type: 'http', url: 'https://vyg' }, pack: 'hq-pack-vyg', env });
472
-
473
- const result = unwirePackMcp('hq-pack-vyg', { mcp: ['vyg'] }, { env });
474
- expect(result.servers).toHaveLength(1);
475
- expect(result.servers[0]!.claude.outcome).toBe('removed');
476
-
477
- const after = JSON.parse(fs.readFileSync(claudeConfigPath(env), 'utf-8'));
478
- expect(after.mcpServers.vyg).toBeUndefined(); // ours removed
479
- expect(after.mcpServers.figma.url).toBe('https://figma'); // user sibling preserved
480
- });
481
-
482
- it('no-ops cleanly when the pack declares no mcp (merge) keys', () => {
483
- const result = unwirePackMcp('hq-pack-x', { commands: ['foo'], skills: ['bar'] }, { env: { home: mcpHome } });
484
- expect(result.servers).toEqual([]);
485
- });
486
-
487
- it('skip-and-warns (does not delete) a foreign/unstamped same-named entry', () => {
488
- const env = { home: mcpHome };
489
- // The user has their OWN `vyg` (no _hqPack stamp).
490
- fs.writeFileSync(
491
- claudeConfigPath(env),
492
- JSON.stringify({ mcpServers: { vyg: { type: 'http', url: 'https://user-vyg' } } }, null, 2) + '\n',
493
- );
494
- const before = fs.readFileSync(claudeConfigPath(env), 'utf-8');
495
-
496
- const result = unwirePackMcp('hq-pack-vyg', { mcp: ['vyg'] }, { env });
497
- expect(result.servers[0]!.claude.outcome).toBe('skipped-foreign');
498
- // The user's server is left byte-for-byte intact.
499
- expect(fs.readFileSync(claudeConfigPath(env), 'utf-8')).toBe(before);
500
- });
501
-
502
- it('is a clean no-op on a Codex-less host with no ~/.claude.json (never fabricates)', () => {
503
- const env = { home: mcpHome };
504
- const result = unwirePackMcp('hq-pack-vyg', { mcp: ['vyg'] }, { env });
505
- expect(result.servers[0]!.claude.outcome).toBe('absent');
506
- expect('skipped' in result.servers[0]!.codex && (result.servers[0]!.codex as any).skipped).toBe(true);
507
- expect(fs.existsSync(path.join(mcpHome, '.claude.json'))).toBe(false);
508
- expect(fs.existsSync(path.join(mcpHome, '.codex'))).toBe(false);
509
- });
510
-
511
- it('delegates to unregisterMcpServers across BOTH surfaces when ~/.codex exists', () => {
512
- // The unwire-hook must un-register from Claude AND Codex (not just Claude) — i.e.
513
- // it really delegates to unregisterMcpServers, which fans out over both surfaces.
514
- const env = { home: mcpHome };
515
- fs.mkdirSync(codexHome(env), { recursive: true }); // make Codex a real runtime
516
- // Install our vyg into BOTH surfaces, alongside a user-owned figma on Claude.
517
- fs.writeFileSync(
518
- claudeConfigPath(env),
519
- JSON.stringify({ mcpServers: { figma: { type: 'http', url: 'https://figma' } } }, null, 2) + '\n',
520
- );
521
- registerServer({ name: 'vyg', manifest: { type: 'http', url: 'https://vyg' }, pack: 'hq-pack-vyg', env });
522
- // Both surfaces carry vyg before uninstall.
523
- expect(JSON.parse(fs.readFileSync(claudeConfigPath(env), 'utf-8')).mcpServers.vyg).toBeDefined();
524
- expect((parseToml(fs.readFileSync(codexConfigPath(env), 'utf-8')) as any).mcp_servers.vyg).toBeDefined();
525
-
526
- const result = unwirePackMcp('hq-pack-vyg', { mcp: ['vyg'] }, { env });
527
- expect(result.servers).toHaveLength(1);
528
- expect(result.servers[0]!.claude.outcome).toBe('removed');
529
- expect('skipped' in result.servers[0]!.codex).toBe(false);
530
- expect((result.servers[0]!.codex as any).outcome).toBe('removed');
531
-
532
- // Both surfaces no longer carry vyg; the user's Claude figma sibling survives.
533
- expect(JSON.parse(fs.readFileSync(claudeConfigPath(env), 'utf-8')).mcpServers.vyg).toBeUndefined();
534
- expect(JSON.parse(fs.readFileSync(claudeConfigPath(env), 'utf-8')).mcpServers.figma).toBeDefined();
535
- expect((parseToml(fs.readFileSync(codexConfigPath(env), 'utf-8')) as any).mcp_servers.vyg).toBeUndefined();
536
- });
537
-
538
- it('skip-and-warns a foreign Codex table too (does not delete another pack\'s server)', () => {
539
- // The Codex parallel of the foreign skip: a vyg table stamped by ANOTHER pack must
540
- // be left in place when uninstalling OUR pack — provenance scope, not name match.
541
- const env = { home: mcpHome };
542
- fs.mkdirSync(codexHome(env), { recursive: true });
543
- const original =
544
- '# keep me\n[mcp_servers.vyg]\ntype = "http"\nurl = "https://vyg"\n_hqPack = "hq-pack-OTHER"\n';
545
- fs.writeFileSync(codexConfigPath(env), original);
546
-
547
- const result = unwirePackMcp('hq-pack-vyg', { mcp: ['vyg'] }, { env });
548
- expect((result.servers[0]!.codex as any).outcome).toBe('skipped-foreign');
549
- // Byte-for-byte preserved (comment intact — no write on a foreign skip).
550
- expect(fs.readFileSync(codexConfigPath(env), 'utf-8')).toBe(original);
551
- });
552
- });
553
-
554
- // ---------------------------------------------------------------------------
555
- // findDependentPacks — uninstall dependents guard (M0)
556
- // ---------------------------------------------------------------------------
557
-
558
- describe('findDependentPacks (M0)', () => {
559
- function ip(
560
- name: string,
561
- requiresPacks?: Array<{ name: string; version?: string }>,
562
- ): InstalledPack {
563
- return {
564
- name,
565
- dir: `/tmp/nope/${name}`,
566
- manifest: {
567
- name,
568
- version: '1.0.0',
569
- publisher: '@indigoai-us',
570
- access: 'public',
571
- requires: { hqCore: '>=15.0.0', packs: requiresPacks },
572
- contributes: { skills: ['x'] },
573
- },
574
- } as InstalledPack;
575
- }
576
-
577
- it('returns the packs that require the named pack', () => {
578
- const installed = [
579
- ip('hq-pack-crm'),
580
- ip('hq-pack-accounting', [{ name: 'hq-pack-crm', version: '>=1.0.0' }]),
581
- ip('hq-pack-unrelated'),
582
- ];
583
- const deps = findDependentPacks(installed, 'hq-pack-crm');
584
- expect(deps.map((p) => p.name)).toEqual(['hq-pack-accounting']);
585
- });
586
-
587
- it('returns empty when no installed pack requires it', () => {
588
- const installed = [ip('hq-pack-crm'), ip('hq-pack-unrelated')];
589
- expect(findDependentPacks(installed, 'hq-pack-crm')).toEqual([]);
590
- });
591
-
592
- it('never counts a pack that lists itself as its own dependent', () => {
593
- // Defensive: a self-referencing manifest (rejected at install) must not make
594
- // a pack un-removable.
595
- const installed = [ip('hq-pack-weird', [{ name: 'hq-pack-weird' }])];
596
- expect(findDependentPacks(installed, 'hq-pack-weird')).toEqual([]);
597
- });
598
-
599
- it('tolerates an unreadable manifest (manifest === null)', () => {
600
- const broken: InstalledPack = { name: 'hq-pack-broken', dir: '/tmp/nope', manifest: null };
601
- expect(findDependentPacks([broken], 'hq-pack-crm')).toEqual([]);
602
- });
603
- });