@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.
- package/CHANGELOG.md +34 -0
- package/assets/scaffold/core/scripts/archive-old-threads.sh +86 -0
- package/assets/scaffold/core/scripts/backfill-company-skill-mirrors.sh +73 -0
- package/assets/scaffold/core/scripts/backfill-workspace-mirror.sh +51 -0
- package/assets/scaffold/core/scripts/codex-skill-bridge.sh +550 -0
- package/assets/scaffold/core/scripts/detect-stale-core-policy-mirror.sh +185 -0
- package/assets/scaffold/core/scripts/hq-status-summary.sh +187 -0
- package/assets/scaffold/core/scripts/ontology-readme-drift.sh +264 -0
- package/assets/scaffold/core/scripts/qmd-reindex-after-sync.sh +93 -0
- package/assets/scaffold/core/scripts/rebuild-all-indexes.sh +69 -0
- package/assets/scaffold/core/scripts/rebuild-companies-index.sh +206 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +127 -0
- package/assets/scaffold/core/scripts/rebuild-orchestrator-index.sh +86 -0
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +105 -0
- package/assets/scaffold/core/scripts/rebuild-public-knowledge-index.sh +83 -0
- package/assets/scaffold/core/scripts/rebuild-reports-index.sh +114 -0
- package/assets/scaffold/core/scripts/rebuild-social-drafts-index.sh +135 -0
- package/assets/scaffold/core/scripts/rebuild-threads-index.sh +82 -0
- package/assets/scaffold/core/scripts/rebuild-workers-index.sh +97 -0
- package/assets/scaffold/core/scripts/resize-screenshot.sh +47 -0
- package/assets/scaffold/core/scripts/token-usage-report.sh +254 -0
- package/assets/scaffold/core/scripts/worktree.sh +136 -0
- package/dist/commands/agents.js +28 -12
- package/dist/commands/api-keys.js +100 -20
- package/dist/commands/core.d.ts +90 -0
- package/dist/commands/core.js +279 -0
- package/dist/commands/files-browse.d.ts +21 -0
- package/dist/commands/files-browse.js +145 -24
- package/dist/commands/files.js +33 -7
- package/dist/commands/members.js +16 -5
- package/dist/commands/pack-install.d.ts +14 -0
- package/dist/commands/pack-install.js +51 -2
- package/dist/commands/pkg-install.js +43 -0
- package/dist/commands/run.js +8 -0
- package/dist/commands/secrets.js +125 -65
- package/dist/main.js +6 -0
- package/dist/utils/api-key-command-gate.d.ts +45 -0
- package/dist/utils/api-key-command-gate.js +51 -0
- package/dist/utils/hq-roots.d.ts +91 -0
- package/dist/utils/hq-roots.js +212 -0
- package/dist/utils/resolve-vault-credential.d.ts +63 -3
- package/dist/utils/resolve-vault-credential.js +152 -6
- package/dist/utils/run-bundled-script.d.ts +66 -0
- package/dist/utils/run-bundled-script.js +80 -0
- package/dist/utils/vault-api.d.ts +21 -0
- package/dist/utils/vault-api.js +74 -3
- package/package.json +7 -1
- package/.claude/policies/hq-cli-interactive-reads-use-sts-vend.md +0 -49
- package/.github/workflows/ci.yml +0 -46
- package/.github/workflows/publish.yml +0 -36
- package/.github/workflows/scripts/smoke-test-pkg.sh +0 -97
- package/e2e/cli.test.ts +0 -128
- package/eslint.config.js +0 -37
- package/pnpm-workspace.yaml +0 -4
- package/scripts/chmod-bins.mjs +0 -49
- package/scripts/generate-dsn.mjs +0 -26
- package/scripts/generate-scan-packages-table.mjs +0 -113
- package/scripts/smoke-sources-signals.sh +0 -103
- package/src/bin/hq-auth-refresh.ts +0 -43
- package/src/cli-version.ts +0 -13
- package/src/commands/__fixtures__/make-tar.ts +0 -126
- package/src/commands/_patterns.ts +0 -10
- package/src/commands/add.ts +0 -75
- package/src/commands/agents.test.ts +0 -527
- package/src/commands/agents.ts +0 -923
- package/src/commands/api-keys.test.ts +0 -291
- package/src/commands/api-keys.ts +0 -382
- package/src/commands/artifact-verify.test.ts +0 -177
- package/src/commands/auth.test.ts +0 -82
- package/src/commands/auth.ts +0 -213
- package/src/commands/billing.test.ts +0 -158
- package/src/commands/billing.ts +0 -146
- package/src/commands/channels.test.ts +0 -74
- package/src/commands/channels.ts +0 -91
- package/src/commands/cloud-demote.test.ts +0 -401
- package/src/commands/cloud-demote.ts +0 -278
- package/src/commands/cloud-provision.test.ts +0 -1117
- package/src/commands/cloud-provision.ts +0 -942
- package/src/commands/cloud.pull-all.test.ts +0 -711
- package/src/commands/cloud.pull-per-company.test.ts +0 -188
- package/src/commands/cloud.push-all.test.ts +0 -485
- package/src/commands/cloud.scope-excluded-warning.test.ts +0 -22
- package/src/commands/cloud.selectors.test.ts +0 -208
- package/src/commands/cloud.status.test.ts +0 -160
- package/src/commands/cloud.test.ts +0 -41
- package/src/commands/cloud.ts +0 -2130
- package/src/commands/company.test.ts +0 -202
- package/src/commands/company.ts +0 -148
- package/src/commands/creators.test.ts +0 -60
- package/src/commands/creators.ts +0 -117
- package/src/commands/crm.test.ts +0 -179
- package/src/commands/crm.ts +0 -236
- package/src/commands/db-migrate.ts +0 -55
- package/src/commands/db-provision.ts +0 -114
- package/src/commands/db-sql.ts +0 -124
- package/src/commands/db-status.ts +0 -108
- package/src/commands/db.ts +0 -26
- package/src/commands/dm.test.ts +0 -748
- package/src/commands/dm.ts +0 -1226
- package/src/commands/feedback.test.ts +0 -413
- package/src/commands/feedback.ts +0 -169
- package/src/commands/files-browse.test.ts +0 -1249
- package/src/commands/files-browse.ts +0 -1506
- package/src/commands/files-delete.test.ts +0 -484
- package/src/commands/files-recovery.test.ts +0 -370
- package/src/commands/files.test.ts +0 -722
- package/src/commands/files.ts +0 -1446
- package/src/commands/group-grants.test.ts +0 -330
- package/src/commands/group-grants.ts +0 -455
- package/src/commands/groups.test.ts +0 -44
- package/src/commands/groups.ts +0 -368
- package/src/commands/integrations.test.ts +0 -633
- package/src/commands/integrations.ts +0 -507
- package/src/commands/list.ts +0 -85
- package/src/commands/login.ts +0 -52
- package/src/commands/logout.ts +0 -30
- package/src/commands/marketplace-install.test.ts +0 -543
- package/src/commands/marketplace-security.test.ts +0 -646
- package/src/commands/mcp-registration.test.ts +0 -2787
- package/src/commands/mcp-registration.ts +0 -2612
- package/src/commands/mcp-status.test.ts +0 -483
- package/src/commands/mcp-status.ts +0 -575
- package/src/commands/mcp-status.us011.test.ts +0 -243
- package/src/commands/meetings.test.ts +0 -522
- package/src/commands/meetings.ts +0 -789
- package/src/commands/members.test.ts +0 -1474
- package/src/commands/members.ts +0 -1181
- package/src/commands/onboard-identity-guard.test.ts +0 -99
- package/src/commands/onboard-identity-guard.ts +0 -103
- package/src/commands/onboard-join.test.ts +0 -96
- package/src/commands/onboard-join.ts +0 -101
- package/src/commands/onboard-warning.test.ts +0 -26
- package/src/commands/onboard-warning.ts +0 -12
- package/src/commands/onboard.ts +0 -267
- package/src/commands/outposts-heartbeat.test.ts +0 -299
- package/src/commands/outposts-heartbeat.ts +0 -310
- package/src/commands/outposts-replica-sync.test.ts +0 -272
- package/src/commands/outposts-self-deploy.test.ts +0 -243
- package/src/commands/outposts.test.ts +0 -1039
- package/src/commands/outposts.ts +0 -1935
- package/src/commands/pack-install-secret-authorization.test.ts +0 -115
- package/src/commands/pack-install.test.ts +0 -1469
- package/src/commands/pack-install.ts +0 -2247
- package/src/commands/pack-update-cache.test.ts +0 -149
- package/src/commands/packs.test.ts +0 -88
- package/src/commands/packs.ts +0 -622
- package/src/commands/people.test.ts +0 -807
- package/src/commands/people.ts +0 -422
- package/src/commands/pkg-install.dispatch.test.ts +0 -71
- package/src/commands/pkg-install.ts +0 -215
- package/src/commands/pkg-list.test.ts +0 -79
- package/src/commands/pkg-list.ts +0 -13
- package/src/commands/pkg-remove.ts +0 -71
- package/src/commands/pkg-update.ts +0 -189
- package/src/commands/publish.test.ts +0 -650
- package/src/commands/publish.ts +0 -580
- package/src/commands/reindex.test.ts +0 -259
- package/src/commands/reindex.ts +0 -274
- package/src/commands/rescue.test.ts +0 -39
- package/src/commands/rescue.ts +0 -210
- package/src/commands/run.env-local.test.ts +0 -88
- package/src/commands/run.test.ts +0 -45
- package/src/commands/run.ts +0 -184
- package/src/commands/safe-extract.test.ts +0 -459
- package/src/commands/safe-extract.ts +0 -444
- package/src/commands/secrets-scope.test.ts +0 -56
- package/src/commands/secrets-scope.ts +0 -32
- package/src/commands/secrets.parse-destination.test.ts +0 -38
- package/src/commands/secrets.test.ts +0 -2037
- package/src/commands/secrets.ts +0 -2240
- package/src/commands/signals.ts +0 -351
- package/src/commands/skill.test.ts +0 -770
- package/src/commands/skill.ts +0 -796
- package/src/commands/sources.ts +0 -362
- package/src/commands/sync-mode.test.ts +0 -366
- package/src/commands/sync-mode.ts +0 -387
- package/src/commands/sync-narrow.test.ts +0 -633
- package/src/commands/sync-narrow.ts +0 -555
- package/src/commands/sync.ts +0 -165
- package/src/commands/team-sync.ts +0 -628
- package/src/commands/update.ts +0 -71
- package/src/commands/whoami.ts +0 -83
- package/src/commands/workers.test.ts +0 -158
- package/src/commands/workers.ts +0 -301
- package/src/index.test.ts +0 -32
- package/src/index.ts +0 -20
- package/src/lib/db/control-plane.test.ts +0 -59
- package/src/lib/db/control-plane.ts +0 -113
- package/src/lib/db/local.test.ts +0 -81
- package/src/lib/db/local.ts +0 -148
- package/src/lib/db/migrate.test.ts +0 -133
- package/src/lib/db/migrate.ts +0 -137
- package/src/lib/db/paths.test.ts +0 -112
- package/src/lib/db/paths.ts +0 -128
- package/src/lib/db/remote-engine.test.ts +0 -44
- package/src/lib/db/remote-engine.ts +0 -148
- package/src/lib/db/remote-sql.test.ts +0 -32
- package/src/lib/db/remote-sql.ts +0 -62
- package/src/lib/db/sql.test.ts +0 -106
- package/src/lib/db/sql.ts +0 -192
- package/src/lib/hq-cloud-dep.smoke.test.ts +0 -75
- package/src/lib/local-tree-diff.test.ts +0 -301
- package/src/lib/local-tree-diff.ts +0 -362
- package/src/lib/narrow-hint-banner.test.ts +0 -235
- package/src/lib/narrow-hint-banner.ts +0 -212
- package/src/lib/plan-limit-nag.test.ts +0 -317
- package/src/lib/plan-limit-nag.ts +0 -264
- package/src/main.ts +0 -371
- package/src/node-preflight.test.ts +0 -60
- package/src/node-preflight.ts +0 -67
- package/src/outpost/session-heartbeat-bounds.test.ts +0 -195
- package/src/outpost/session-heartbeat-guard.test.ts +0 -105
- package/src/outpost/session-heartbeat-publisher.test.ts +0 -178
- package/src/outpost/session-heartbeat-publisher.ts +0 -186
- package/src/outpost/session-heartbeat-retain-guard.test.ts +0 -126
- package/src/outpost/session-heartbeat.test.ts +0 -459
- package/src/outpost/session-heartbeat.ts +0 -877
- package/src/packaging.test.ts +0 -109
- package/src/run/__fixtures__/discover-schemas/example.env.schema +0 -4
- package/src/run/discover-schemas.test.ts +0 -153
- package/src/run/discover-schemas.ts +0 -79
- package/src/run/hq-plugin.test.ts +0 -371
- package/src/run/hq-plugin.ts +0 -277
- package/src/run/varlock-shape.test.ts +0 -57
- package/src/schemas/hq-package.schema.json +0 -155
- package/src/sentry-before-send.test.ts +0 -359
- package/src/sentry-before-send.ts +0 -221
- package/src/sentry-epipe.test.ts +0 -37
- package/src/sentry-release.test.ts +0 -54
- package/src/sentry.test.ts +0 -24
- package/src/sentry.ts +0 -54
- package/src/strategies/link.ts +0 -62
- package/src/strategies/merge.ts +0 -142
- package/src/types.ts +0 -146
- package/src/utils/__fixtures__/scan-packages.generated-block.sh +0 -23
- package/src/utils/auth-error.test.ts +0 -40
- package/src/utils/auth-error.ts +0 -42
- package/src/utils/billing-gate.test.ts +0 -95
- package/src/utils/billing-gate.ts +0 -228
- package/src/utils/breadcrumb-buffer.ts +0 -18
- package/src/utils/cli-telemetry.test.ts +0 -153
- package/src/utils/cli-telemetry.ts +0 -61
- package/src/utils/cognito-session.machine.test.ts +0 -109
- package/src/utils/cognito-session.refresh.test.ts +0 -84
- package/src/utils/cognito-session.test.ts +0 -328
- package/src/utils/cognito-session.ts +0 -448
- package/src/utils/company-selection-error.test.ts +0 -42
- package/src/utils/company-selection-error.ts +0 -45
- package/src/utils/contribution-table.ts +0 -83
- package/src/utils/environmental-error.test.ts +0 -45
- package/src/utils/environmental-error.ts +0 -39
- package/src/utils/epipe.test.ts +0 -28
- package/src/utils/epipe.ts +0 -29
- package/src/utils/expected-cli-error.test.ts +0 -28
- package/src/utils/expected-cli-error.ts +0 -39
- package/src/utils/feedback-diagnostics.test.ts +0 -183
- package/src/utils/feedback-diagnostics.ts +0 -123
- package/src/utils/feedback-screenshots.test.ts +0 -134
- package/src/utils/feedback-screenshots.ts +0 -124
- package/src/utils/feedback-versions.test.ts +0 -98
- package/src/utils/feedback-versions.ts +0 -68
- package/src/utils/git.ts +0 -74
- package/src/utils/integrity.ts +0 -59
- package/src/utils/intercepted-process-exit.test.ts +0 -37
- package/src/utils/intercepted-process-exit.ts +0 -36
- package/src/utils/login-provider.test.ts +0 -42
- package/src/utils/login-provider.ts +0 -30
- package/src/utils/manifest.test.ts +0 -167
- package/src/utils/manifest.ts +0 -131
- package/src/utils/pack-contributions.test.ts +0 -603
- package/src/utils/pack-contributions.ts +0 -450
- package/src/utils/people.ts +0 -215
- package/src/utils/registry-client.ts +0 -204
- package/src/utils/registry.ts +0 -90
- package/src/utils/resolve-vault-credential.test.ts +0 -69
- package/src/utils/resolve-vault-credential.ts +0 -60
- package/src/utils/sandbox-runner-client.test.ts +0 -279
- package/src/utils/sandbox-runner-client.ts +0 -260
- package/src/utils/secrets-cache.ts +0 -183
- package/src/utils/sentry-identity.ts +0 -45
- package/src/utils/settle-with-timeout.test.ts +0 -21
- package/src/utils/settle-with-timeout.ts +0 -22
- package/src/utils/user-yaml-error.test.ts +0 -24
- package/src/utils/user-yaml-error.ts +0 -30
- package/src/utils/vault-api.test.ts +0 -536
- package/src/utils/vault-api.ts +0 -382
- package/src/utils/version-check.test.ts +0 -176
- package/src/utils/version-check.ts +0 -155
- package/src/utils/version-gate.test.ts +0 -1024
- package/src/utils/version-gate.ts +0 -702
- package/test/commands/db-tenant-isolation.test.ts +0 -94
- package/test/commands/db.test.ts +0 -85
- package/test/commands/signals.test.ts +0 -207
- package/test/commands/sources.test.ts +0 -231
- package/test/e2e/smoke-install-mcp.sh +0 -113
- package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +0 -1
- package/test/fixtures/hq-pack-smoke-mcp/package.yaml +0 -11
- package/test/fixtures/signals/action_item/sample.md +0 -16
- package/test/fixtures/signals/summary/sample.md +0 -12
- package/test/fixtures/sources/meetings/sample.md +0 -25
- package/test/helpers/cli-runner.ts +0 -150
- package/test/helpers/s3-list-mock.ts +0 -79
- package/test/helpers/vault-service-mock.ts +0 -295
- package/test/sources-signals/smoke.test.ts +0 -228
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -11
- 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
|
-
});
|