@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,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
- });