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