@indigoai-us/hq-cli 5.77.14 → 5.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/assets/scaffold/core/scripts/archive-old-threads.sh +86 -0
  3. package/assets/scaffold/core/scripts/backfill-company-skill-mirrors.sh +73 -0
  4. package/assets/scaffold/core/scripts/backfill-workspace-mirror.sh +51 -0
  5. package/assets/scaffold/core/scripts/codex-skill-bridge.sh +550 -0
  6. package/assets/scaffold/core/scripts/detect-stale-core-policy-mirror.sh +185 -0
  7. package/assets/scaffold/core/scripts/hq-status-summary.sh +187 -0
  8. package/assets/scaffold/core/scripts/ontology-readme-drift.sh +264 -0
  9. package/assets/scaffold/core/scripts/qmd-reindex-after-sync.sh +93 -0
  10. package/assets/scaffold/core/scripts/rebuild-all-indexes.sh +69 -0
  11. package/assets/scaffold/core/scripts/rebuild-companies-index.sh +206 -0
  12. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +127 -0
  13. package/assets/scaffold/core/scripts/rebuild-orchestrator-index.sh +86 -0
  14. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +105 -0
  15. package/assets/scaffold/core/scripts/rebuild-public-knowledge-index.sh +83 -0
  16. package/assets/scaffold/core/scripts/rebuild-reports-index.sh +114 -0
  17. package/assets/scaffold/core/scripts/rebuild-social-drafts-index.sh +135 -0
  18. package/assets/scaffold/core/scripts/rebuild-threads-index.sh +82 -0
  19. package/assets/scaffold/core/scripts/rebuild-workers-index.sh +97 -0
  20. package/assets/scaffold/core/scripts/resize-screenshot.sh +47 -0
  21. package/assets/scaffold/core/scripts/token-usage-report.sh +254 -0
  22. package/assets/scaffold/core/scripts/worktree.sh +136 -0
  23. package/dist/commands/agents.js +28 -12
  24. package/dist/commands/api-keys.js +100 -20
  25. package/dist/commands/core.d.ts +90 -0
  26. package/dist/commands/core.js +279 -0
  27. package/dist/commands/files-browse.d.ts +21 -0
  28. package/dist/commands/files-browse.js +145 -24
  29. package/dist/commands/files.js +33 -7
  30. package/dist/commands/meetings.js +16 -9
  31. package/dist/commands/members.js +16 -5
  32. package/dist/commands/pack-install.d.ts +14 -0
  33. package/dist/commands/pack-install.js +42 -2
  34. package/dist/commands/pkg-install.js +39 -2
  35. package/dist/commands/run.js +6 -2
  36. package/dist/commands/secrets.js +120 -65
  37. package/dist/main.js +6 -0
  38. package/dist/utils/api-key-command-gate.d.ts +45 -0
  39. package/dist/utils/api-key-command-gate.js +51 -0
  40. package/dist/utils/hq-roots.d.ts +91 -0
  41. package/dist/utils/hq-roots.js +212 -0
  42. package/dist/utils/resolve-vault-credential.d.ts +63 -3
  43. package/dist/utils/resolve-vault-credential.js +152 -6
  44. package/dist/utils/run-bundled-script.d.ts +66 -0
  45. package/dist/utils/run-bundled-script.js +80 -0
  46. package/dist/utils/vault-api.d.ts +21 -0
  47. package/dist/utils/vault-api.js +74 -3
  48. package/package.json +7 -1
  49. package/.claude/policies/hq-cli-interactive-reads-use-sts-vend.md +0 -49
  50. package/.github/workflows/ci.yml +0 -46
  51. package/.github/workflows/publish.yml +0 -36
  52. package/.github/workflows/scripts/smoke-test-pkg.sh +0 -97
  53. package/e2e/cli.test.ts +0 -128
  54. package/eslint.config.js +0 -37
  55. package/pnpm-workspace.yaml +0 -4
  56. package/scripts/chmod-bins.mjs +0 -49
  57. package/scripts/generate-dsn.mjs +0 -26
  58. package/scripts/generate-scan-packages-table.mjs +0 -113
  59. package/scripts/smoke-sources-signals.sh +0 -103
  60. package/src/bin/hq-auth-refresh.ts +0 -43
  61. package/src/cli-version.ts +0 -13
  62. package/src/commands/__fixtures__/make-tar.ts +0 -126
  63. package/src/commands/_patterns.ts +0 -10
  64. package/src/commands/add.ts +0 -75
  65. package/src/commands/agents.test.ts +0 -527
  66. package/src/commands/agents.ts +0 -923
  67. package/src/commands/api-keys.test.ts +0 -291
  68. package/src/commands/api-keys.ts +0 -382
  69. package/src/commands/artifact-verify.test.ts +0 -177
  70. package/src/commands/auth.test.ts +0 -82
  71. package/src/commands/auth.ts +0 -213
  72. package/src/commands/billing.test.ts +0 -158
  73. package/src/commands/billing.ts +0 -146
  74. package/src/commands/channels.test.ts +0 -74
  75. package/src/commands/channels.ts +0 -91
  76. package/src/commands/cloud-demote.test.ts +0 -401
  77. package/src/commands/cloud-demote.ts +0 -278
  78. package/src/commands/cloud-provision.test.ts +0 -1117
  79. package/src/commands/cloud-provision.ts +0 -942
  80. package/src/commands/cloud.pull-all.test.ts +0 -711
  81. package/src/commands/cloud.pull-per-company.test.ts +0 -188
  82. package/src/commands/cloud.push-all.test.ts +0 -485
  83. package/src/commands/cloud.scope-excluded-warning.test.ts +0 -22
  84. package/src/commands/cloud.selectors.test.ts +0 -208
  85. package/src/commands/cloud.status.test.ts +0 -160
  86. package/src/commands/cloud.test.ts +0 -41
  87. package/src/commands/cloud.ts +0 -2130
  88. package/src/commands/company.test.ts +0 -202
  89. package/src/commands/company.ts +0 -148
  90. package/src/commands/creators.test.ts +0 -60
  91. package/src/commands/creators.ts +0 -117
  92. package/src/commands/crm.test.ts +0 -179
  93. package/src/commands/crm.ts +0 -236
  94. package/src/commands/db-migrate.ts +0 -55
  95. package/src/commands/db-provision.ts +0 -114
  96. package/src/commands/db-sql.ts +0 -124
  97. package/src/commands/db-status.ts +0 -108
  98. package/src/commands/db.ts +0 -26
  99. package/src/commands/dm.test.ts +0 -748
  100. package/src/commands/dm.ts +0 -1226
  101. package/src/commands/feedback.test.ts +0 -413
  102. package/src/commands/feedback.ts +0 -169
  103. package/src/commands/files-browse.test.ts +0 -1249
  104. package/src/commands/files-browse.ts +0 -1506
  105. package/src/commands/files-delete.test.ts +0 -484
  106. package/src/commands/files-recovery.test.ts +0 -370
  107. package/src/commands/files.test.ts +0 -722
  108. package/src/commands/files.ts +0 -1446
  109. package/src/commands/group-grants.test.ts +0 -330
  110. package/src/commands/group-grants.ts +0 -455
  111. package/src/commands/groups.test.ts +0 -44
  112. package/src/commands/groups.ts +0 -368
  113. package/src/commands/integrations.test.ts +0 -633
  114. package/src/commands/integrations.ts +0 -507
  115. package/src/commands/list.ts +0 -85
  116. package/src/commands/login.ts +0 -52
  117. package/src/commands/logout.ts +0 -30
  118. package/src/commands/marketplace-install.test.ts +0 -543
  119. package/src/commands/marketplace-security.test.ts +0 -646
  120. package/src/commands/mcp-registration.test.ts +0 -2787
  121. package/src/commands/mcp-registration.ts +0 -2612
  122. package/src/commands/mcp-status.test.ts +0 -483
  123. package/src/commands/mcp-status.ts +0 -575
  124. package/src/commands/mcp-status.us011.test.ts +0 -243
  125. package/src/commands/meetings.test.ts +0 -522
  126. package/src/commands/meetings.ts +0 -789
  127. package/src/commands/members.test.ts +0 -1474
  128. package/src/commands/members.ts +0 -1181
  129. package/src/commands/onboard-identity-guard.test.ts +0 -99
  130. package/src/commands/onboard-identity-guard.ts +0 -103
  131. package/src/commands/onboard-join.test.ts +0 -96
  132. package/src/commands/onboard-join.ts +0 -101
  133. package/src/commands/onboard-warning.test.ts +0 -26
  134. package/src/commands/onboard-warning.ts +0 -12
  135. package/src/commands/onboard.ts +0 -267
  136. package/src/commands/outposts-heartbeat.test.ts +0 -299
  137. package/src/commands/outposts-heartbeat.ts +0 -310
  138. package/src/commands/outposts-replica-sync.test.ts +0 -272
  139. package/src/commands/outposts-self-deploy.test.ts +0 -243
  140. package/src/commands/outposts.test.ts +0 -1039
  141. package/src/commands/outposts.ts +0 -1935
  142. package/src/commands/pack-install-secret-authorization.test.ts +0 -115
  143. package/src/commands/pack-install.test.ts +0 -1469
  144. package/src/commands/pack-install.ts +0 -2256
  145. package/src/commands/pack-update-cache.test.ts +0 -149
  146. package/src/commands/packs-update-api-key.test.ts +0 -105
  147. package/src/commands/packs.test.ts +0 -88
  148. package/src/commands/packs.ts +0 -622
  149. package/src/commands/people.test.ts +0 -807
  150. package/src/commands/people.ts +0 -422
  151. package/src/commands/pkg-install.dispatch.test.ts +0 -103
  152. package/src/commands/pkg-install.ts +0 -221
  153. package/src/commands/pkg-list.test.ts +0 -79
  154. package/src/commands/pkg-list.ts +0 -13
  155. package/src/commands/pkg-remove.ts +0 -71
  156. package/src/commands/pkg-update.ts +0 -189
  157. package/src/commands/publish.test.ts +0 -650
  158. package/src/commands/publish.ts +0 -580
  159. package/src/commands/reindex.test.ts +0 -259
  160. package/src/commands/reindex.ts +0 -274
  161. package/src/commands/rescue.test.ts +0 -39
  162. package/src/commands/rescue.ts +0 -210
  163. package/src/commands/run.env-local.test.ts +0 -88
  164. package/src/commands/run.test.ts +0 -45
  165. package/src/commands/run.ts +0 -190
  166. package/src/commands/safe-extract.test.ts +0 -459
  167. package/src/commands/safe-extract.ts +0 -444
  168. package/src/commands/secrets-scope.test.ts +0 -56
  169. package/src/commands/secrets-scope.ts +0 -32
  170. package/src/commands/secrets.parse-destination.test.ts +0 -38
  171. package/src/commands/secrets.test.ts +0 -2050
  172. package/src/commands/secrets.ts +0 -2249
  173. package/src/commands/signals.ts +0 -351
  174. package/src/commands/skill.test.ts +0 -770
  175. package/src/commands/skill.ts +0 -796
  176. package/src/commands/sources.ts +0 -362
  177. package/src/commands/sync-mode.test.ts +0 -366
  178. package/src/commands/sync-mode.ts +0 -387
  179. package/src/commands/sync-narrow.test.ts +0 -633
  180. package/src/commands/sync-narrow.ts +0 -555
  181. package/src/commands/sync.ts +0 -165
  182. package/src/commands/team-sync.ts +0 -628
  183. package/src/commands/update.ts +0 -71
  184. package/src/commands/whoami.ts +0 -83
  185. package/src/commands/workers.test.ts +0 -158
  186. package/src/commands/workers.ts +0 -301
  187. package/src/index.test.ts +0 -32
  188. package/src/index.ts +0 -20
  189. package/src/lib/db/control-plane.test.ts +0 -59
  190. package/src/lib/db/control-plane.ts +0 -113
  191. package/src/lib/db/local.test.ts +0 -81
  192. package/src/lib/db/local.ts +0 -148
  193. package/src/lib/db/migrate.test.ts +0 -133
  194. package/src/lib/db/migrate.ts +0 -137
  195. package/src/lib/db/paths.test.ts +0 -112
  196. package/src/lib/db/paths.ts +0 -128
  197. package/src/lib/db/remote-engine.test.ts +0 -44
  198. package/src/lib/db/remote-engine.ts +0 -148
  199. package/src/lib/db/remote-sql.test.ts +0 -32
  200. package/src/lib/db/remote-sql.ts +0 -62
  201. package/src/lib/db/sql.test.ts +0 -106
  202. package/src/lib/db/sql.ts +0 -192
  203. package/src/lib/hq-cloud-dep.smoke.test.ts +0 -75
  204. package/src/lib/local-tree-diff.test.ts +0 -301
  205. package/src/lib/local-tree-diff.ts +0 -362
  206. package/src/lib/narrow-hint-banner.test.ts +0 -235
  207. package/src/lib/narrow-hint-banner.ts +0 -212
  208. package/src/lib/plan-limit-nag.test.ts +0 -317
  209. package/src/lib/plan-limit-nag.ts +0 -264
  210. package/src/main.ts +0 -371
  211. package/src/node-preflight.test.ts +0 -60
  212. package/src/node-preflight.ts +0 -67
  213. package/src/outpost/session-heartbeat-bounds.test.ts +0 -195
  214. package/src/outpost/session-heartbeat-guard.test.ts +0 -105
  215. package/src/outpost/session-heartbeat-publisher.test.ts +0 -178
  216. package/src/outpost/session-heartbeat-publisher.ts +0 -186
  217. package/src/outpost/session-heartbeat-retain-guard.test.ts +0 -126
  218. package/src/outpost/session-heartbeat.test.ts +0 -459
  219. package/src/outpost/session-heartbeat.ts +0 -877
  220. package/src/packaging.test.ts +0 -109
  221. package/src/run/__fixtures__/discover-schemas/example.env.schema +0 -4
  222. package/src/run/discover-schemas.test.ts +0 -153
  223. package/src/run/discover-schemas.ts +0 -79
  224. package/src/run/hq-plugin.test.ts +0 -371
  225. package/src/run/hq-plugin.ts +0 -277
  226. package/src/run/varlock-shape.test.ts +0 -57
  227. package/src/schemas/hq-package.schema.json +0 -155
  228. package/src/sentry-before-send.test.ts +0 -359
  229. package/src/sentry-before-send.ts +0 -221
  230. package/src/sentry-epipe.test.ts +0 -37
  231. package/src/sentry-release.test.ts +0 -54
  232. package/src/sentry.test.ts +0 -24
  233. package/src/sentry.ts +0 -54
  234. package/src/strategies/link.ts +0 -62
  235. package/src/strategies/merge.ts +0 -142
  236. package/src/types.ts +0 -146
  237. package/src/utils/__fixtures__/scan-packages.generated-block.sh +0 -23
  238. package/src/utils/auth-error.test.ts +0 -40
  239. package/src/utils/auth-error.ts +0 -42
  240. package/src/utils/billing-gate.test.ts +0 -95
  241. package/src/utils/billing-gate.ts +0 -228
  242. package/src/utils/breadcrumb-buffer.ts +0 -18
  243. package/src/utils/cli-telemetry.test.ts +0 -153
  244. package/src/utils/cli-telemetry.ts +0 -61
  245. package/src/utils/cognito-session.machine.test.ts +0 -109
  246. package/src/utils/cognito-session.refresh.test.ts +0 -84
  247. package/src/utils/cognito-session.test.ts +0 -328
  248. package/src/utils/cognito-session.ts +0 -448
  249. package/src/utils/company-selection-error.test.ts +0 -42
  250. package/src/utils/company-selection-error.ts +0 -45
  251. package/src/utils/contribution-table.ts +0 -83
  252. package/src/utils/environmental-error.test.ts +0 -45
  253. package/src/utils/environmental-error.ts +0 -39
  254. package/src/utils/epipe.test.ts +0 -28
  255. package/src/utils/epipe.ts +0 -29
  256. package/src/utils/expected-cli-error.test.ts +0 -28
  257. package/src/utils/expected-cli-error.ts +0 -39
  258. package/src/utils/feedback-diagnostics.test.ts +0 -183
  259. package/src/utils/feedback-diagnostics.ts +0 -123
  260. package/src/utils/feedback-screenshots.test.ts +0 -134
  261. package/src/utils/feedback-screenshots.ts +0 -124
  262. package/src/utils/feedback-versions.test.ts +0 -98
  263. package/src/utils/feedback-versions.ts +0 -68
  264. package/src/utils/git.ts +0 -74
  265. package/src/utils/integrity.ts +0 -59
  266. package/src/utils/intercepted-process-exit.test.ts +0 -37
  267. package/src/utils/intercepted-process-exit.ts +0 -36
  268. package/src/utils/login-provider.test.ts +0 -42
  269. package/src/utils/login-provider.ts +0 -30
  270. package/src/utils/manifest.test.ts +0 -167
  271. package/src/utils/manifest.ts +0 -131
  272. package/src/utils/pack-contributions.test.ts +0 -603
  273. package/src/utils/pack-contributions.ts +0 -450
  274. package/src/utils/people.ts +0 -215
  275. package/src/utils/registry-client.ts +0 -204
  276. package/src/utils/registry.ts +0 -90
  277. package/src/utils/resolve-vault-credential.test.ts +0 -69
  278. package/src/utils/resolve-vault-credential.ts +0 -60
  279. package/src/utils/sandbox-runner-client.test.ts +0 -279
  280. package/src/utils/sandbox-runner-client.ts +0 -260
  281. package/src/utils/secrets-cache.ts +0 -183
  282. package/src/utils/sentry-identity.ts +0 -45
  283. package/src/utils/settle-with-timeout.test.ts +0 -21
  284. package/src/utils/settle-with-timeout.ts +0 -22
  285. package/src/utils/user-yaml-error.test.ts +0 -24
  286. package/src/utils/user-yaml-error.ts +0 -30
  287. package/src/utils/vault-api.test.ts +0 -536
  288. package/src/utils/vault-api.ts +0 -382
  289. package/src/utils/version-check.test.ts +0 -176
  290. package/src/utils/version-check.ts +0 -155
  291. package/src/utils/version-gate.test.ts +0 -1024
  292. package/src/utils/version-gate.ts +0 -702
  293. package/test/commands/db-tenant-isolation.test.ts +0 -94
  294. package/test/commands/db.test.ts +0 -85
  295. package/test/commands/signals.test.ts +0 -207
  296. package/test/commands/sources.test.ts +0 -231
  297. package/test/e2e/smoke-install-mcp.sh +0 -113
  298. package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +0 -1
  299. package/test/fixtures/hq-pack-smoke-mcp/package.yaml +0 -11
  300. package/test/fixtures/signals/action_item/sample.md +0 -16
  301. package/test/fixtures/signals/summary/sample.md +0 -12
  302. package/test/fixtures/sources/meetings/sample.md +0 -25
  303. package/test/helpers/cli-runner.ts +0 -150
  304. package/test/helpers/s3-list-mock.ts +0 -79
  305. package/test/helpers/vault-service-mock.ts +0 -295
  306. package/test/sources-signals/smoke.test.ts +0 -228
  307. package/tsconfig.json +0 -20
  308. package/vitest.config.ts +0 -11
  309. package/vitest.e2e.config.ts +0 -7
@@ -1,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
- });