@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,702 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hard version-gate: ask hq-pro whether the current CLI is below the minimum
|
|
3
|
-
* acceptable version and, if so, run `npm install -g …@latest` synchronously
|
|
4
|
-
* before any commander parsing happens. Distinct from the existing
|
|
5
|
-
* `version-check.ts` which is a passive (cached, opt-in) stderr nag against
|
|
6
|
-
* the npm registry.
|
|
7
|
-
*
|
|
8
|
-
* Why both?
|
|
9
|
-
* - `version-check.ts` answers "is there something newer?" by polling npm
|
|
10
|
-
* directly. It's a soft hint, lives on a 24h cache, and never blocks.
|
|
11
|
-
* - `version-gate.ts` answers "is the team currently allowing your version
|
|
12
|
-
* to run?" via an authoritative hq-pro endpoint. The server can yank a
|
|
13
|
-
* known-bad release without waiting for the npm `latest` dist-tag move.
|
|
14
|
-
*
|
|
15
|
-
* The endpoint is reusable across clients (hq-sync, hq-installer, create-hq).
|
|
16
|
-
* See `apps/hq-pro/src/vault-service/handlers/client-version-check.ts` for the
|
|
17
|
-
* source-of-truth table.
|
|
18
|
-
*
|
|
19
|
-
* Trust model: anonymous. The CLI may be running pre-login (e.g. fresh
|
|
20
|
-
* install) so we never send credentials. The endpoint identifies the client
|
|
21
|
-
* by `clientId` + `currentVersion`.
|
|
22
|
-
*
|
|
23
|
-
* Failure mode: silent. Network down, hq-pro returning 5xx, malformed body —
|
|
24
|
-
* the gate must never break the CLI for a user who's otherwise fine. We log
|
|
25
|
-
* to Sentry as a breadcrumb (best-effort) and return.
|
|
26
|
-
*
|
|
27
|
-
* Opt-out: `HQ_NO_UPDATE_CHECK=1` (same env as `version-check.ts` — one knob
|
|
28
|
-
* to silence both check + gate).
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { spawnSync } from "node:child_process";
|
|
32
|
-
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
33
|
-
import path from "node:path";
|
|
34
|
-
import { fileURLToPath } from "node:url";
|
|
35
|
-
import chalk from "chalk";
|
|
36
|
-
import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
|
|
37
|
-
import { DEFAULT_VAULT_API_URL } from "./cognito-session.js";
|
|
38
|
-
|
|
39
|
-
const CLIENT_ID = "hq-cli";
|
|
40
|
-
const ENDPOINT_PATH = "/v1/client-version/check";
|
|
41
|
-
const FETCH_TIMEOUT_MS = 3_000;
|
|
42
|
-
const LATEST_PACKAGE_SPEC = `${CLI_NAME}@latest`;
|
|
43
|
-
|
|
44
|
-
/** Which package manager owns the running global install. */
|
|
45
|
-
export type InstallManager = "npm" | "pnpm";
|
|
46
|
-
|
|
47
|
-
interface VersionCheckResponse {
|
|
48
|
-
clientId: string;
|
|
49
|
-
currentVersion: string;
|
|
50
|
-
minVersion: string;
|
|
51
|
-
latestVersion: string;
|
|
52
|
-
updateRequired: boolean;
|
|
53
|
-
updateRecommended: boolean;
|
|
54
|
-
updateCommand?: string;
|
|
55
|
-
downloadUrl?: string;
|
|
56
|
-
message?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function isOptedOut(): boolean {
|
|
60
|
-
return process.env.HQ_NO_UPDATE_CHECK === "1";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function findRunningPackageRoot(): string | null {
|
|
64
|
-
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
65
|
-
while (true) {
|
|
66
|
-
try {
|
|
67
|
-
const pkg = JSON.parse(
|
|
68
|
-
readFileSync(path.join(dir, "package.json"), "utf-8"),
|
|
69
|
-
) as { name?: unknown };
|
|
70
|
-
if (pkg.name === CLI_NAME) return dir;
|
|
71
|
-
} catch {
|
|
72
|
-
// Keep walking; compiled installs usually start under dist/.
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const parent = path.dirname(dir);
|
|
76
|
-
if (parent === dir) return null;
|
|
77
|
-
dir = parent;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function npmPrefixFromPackageDir(pkgDir: string): string | null {
|
|
82
|
-
const normalized = pkgDir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
83
|
-
const segments = normalized.split("/");
|
|
84
|
-
const nodeModulesIndex = segments.lastIndexOf("node_modules");
|
|
85
|
-
if (nodeModulesIndex === -1) return null;
|
|
86
|
-
|
|
87
|
-
const prefixEnd =
|
|
88
|
-
segments[nodeModulesIndex - 1] === "lib"
|
|
89
|
-
? nodeModulesIndex - 1
|
|
90
|
-
: nodeModulesIndex;
|
|
91
|
-
const prefix = segments.slice(0, prefixEnd).join("/");
|
|
92
|
-
if (prefix === "" && normalized.startsWith("/")) return "/";
|
|
93
|
-
return prefix || null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** `$PNPM_HOME/global`, normalised to forward slashes, when PNPM_HOME is set. */
|
|
97
|
-
function pnpmHomeGlobalRoot(): string | null {
|
|
98
|
-
const home = process.env.PNPM_HOME;
|
|
99
|
-
if (!home) return null;
|
|
100
|
-
const normalized = home.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
101
|
-
return normalized ? `${normalized}/global` : null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Whether the running package lives inside a pnpm-managed **global** install.
|
|
106
|
-
*
|
|
107
|
-
* pnpm does not use npm's `<prefix>/lib/node_modules` layout. A global
|
|
108
|
-
* `pnpm add -g` puts the package in a versioned content store under the
|
|
109
|
-
* `global/<store-layout-version>` root and exposes it through a generated shim
|
|
110
|
-
* on PATH:
|
|
111
|
-
*
|
|
112
|
-
* $PNPM_HOME/hq <- shim on PATH
|
|
113
|
-
* $PNPM_HOME/global/5/node_modules/@indigoai-us/hq-cli <- symlink
|
|
114
|
-
* $PNPM_HOME/global/5/.pnpm/@indigoai-us+hq-cli@5.61.0/node_modules/…
|
|
115
|
-
*
|
|
116
|
-
* Both the symlinked and the resolved (`.pnpm`) form are recognised, because
|
|
117
|
-
* whether `import.meta.url` reports the link or its target depends on how node
|
|
118
|
-
* resolved the entrypoint.
|
|
119
|
-
*
|
|
120
|
-
* A bare `.pnpm` segment is deliberately NOT enough. It also appears in a local
|
|
121
|
-
* project dependency (`<proj>/node_modules/.pnpm/@indigoai-us+hq-cli@…`) and in
|
|
122
|
-
* a `pnpm dlx` cache. Neither of those is what `pnpm add -g` updates, so
|
|
123
|
-
* treating them as global would mutate the user's global install as a side
|
|
124
|
-
* effect of a local invocation while the copy actually running stayed stale —
|
|
125
|
-
* i.e. the gate would re-fire and re-install globally on every subsequent run.
|
|
126
|
-
* Those layouts fall through to the ordinary npm-prefix/manual handling.
|
|
127
|
-
*/
|
|
128
|
-
export function isPnpmManagedPackageDir(pkgDir: string): boolean {
|
|
129
|
-
const normalized = pkgDir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
130
|
-
const segments = normalized.split("/").filter(Boolean);
|
|
131
|
-
|
|
132
|
-
// `global/<digits>` — pnpm's global root, in either the symlinked or the
|
|
133
|
-
// resolved (`.pnpm`) form, and independent of where PNPM_HOME points.
|
|
134
|
-
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
135
|
-
if (segments[i] === "global" && /^\d+$/.test(segments[i + 1]!)) return true;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Belt-and-braces for a future pnpm layout whose store dir is not numeric:
|
|
139
|
-
// anything under `$PNPM_HOME/global` is global by construction. Still keyed
|
|
140
|
-
// on the `global` segment so `$PNPM_HOME/store/**` (dlx caches) stays out.
|
|
141
|
-
const globalRoot = pnpmHomeGlobalRoot();
|
|
142
|
-
if (globalRoot) {
|
|
143
|
-
if (normalized === globalRoot || normalized.startsWith(`${globalRoot}/`)) {
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Where the running CLI is installed and who owns it. Resolved in ONE pass so
|
|
153
|
-
* the package-root walk (which reads and parses a `package.json` per directory
|
|
154
|
-
* level) happens once per invocation, and so the manager and the prefix can
|
|
155
|
-
* never disagree because the filesystem shifted between two separate walks.
|
|
156
|
-
*
|
|
157
|
-
* `prefix` is `null` for a pnpm-managed install even though a prefix-shaped
|
|
158
|
-
* string *can* be derived from those paths: `npmPrefixFromPackageDir` would
|
|
159
|
-
* happily hand back the pnpm store directory, and `npm install -g --prefix
|
|
160
|
-
* <store>` then unpacks a fresh copy into `<store>/lib/node_modules`, which
|
|
161
|
-
* pnpm's shim never reads. npm exits 0, the gate reports success, and the next
|
|
162
|
-
* `hq` invocation still runs the old version — the reported loop where a stale
|
|
163
|
-
* pnpm shim kept resolving 5.61.0 after every "successful" update.
|
|
164
|
-
*
|
|
165
|
-
* Defaults to npm with no prefix when the layout can't be determined — that is
|
|
166
|
-
* the historical behaviour and the common case.
|
|
167
|
-
*/
|
|
168
|
-
export interface RunningInstall {
|
|
169
|
-
manager: InstallManager;
|
|
170
|
-
prefix: string | null;
|
|
171
|
-
packageRoot: string | null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function resolveRunningInstall(): RunningInstall {
|
|
175
|
-
try {
|
|
176
|
-
const packageRoot = findRunningPackageRoot();
|
|
177
|
-
if (!packageRoot) return { manager: "npm", prefix: null, packageRoot: null };
|
|
178
|
-
if (isPnpmManagedPackageDir(packageRoot)) {
|
|
179
|
-
return { manager: "pnpm", prefix: null, packageRoot };
|
|
180
|
-
}
|
|
181
|
-
return {
|
|
182
|
-
manager: "npm",
|
|
183
|
-
prefix: npmPrefixFromPackageDir(packageRoot),
|
|
184
|
-
packageRoot,
|
|
185
|
-
};
|
|
186
|
-
} catch {
|
|
187
|
-
return { manager: "npm", prefix: null, packageRoot: null };
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** Convenience view of {@link resolveRunningInstall} for callers needing one field. */
|
|
192
|
-
export function resolveRunningManager(): InstallManager {
|
|
193
|
-
return resolveRunningInstall().manager;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** Convenience view of {@link resolveRunningInstall} for callers needing one field. */
|
|
197
|
-
export function resolveRunningPrefix(): string | null {
|
|
198
|
-
return resolveRunningInstall().prefix;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function buildPrefixedInstallArgv(prefix: string): string[] {
|
|
202
|
-
return ["install", "-g", "--prefix", prefix, LATEST_PACKAGE_SPEC];
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Argv for updating a pnpm-managed global install. `pnpm add -g` rewrites the
|
|
207
|
-
* PATH shim as part of the install, so the next invocation genuinely resolves
|
|
208
|
-
* the new version — which is the whole point of routing here instead of npm.
|
|
209
|
-
*/
|
|
210
|
-
export function buildPnpmInstallArgv(): string[] {
|
|
211
|
-
return ["add", "-g", LATEST_PACKAGE_SPEC];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Filesystem surface used by {@link cleanStalePartialInstall}. Injected so the
|
|
216
|
-
* cleanup logic is unit-testable without touching a real global prefix.
|
|
217
|
-
*/
|
|
218
|
-
export interface StaleInstallFs {
|
|
219
|
-
readdirSync: (dir: string) => string[];
|
|
220
|
-
existsSync: (target: string) => boolean;
|
|
221
|
-
readFileSync: (target: string, encoding: "utf-8") => string;
|
|
222
|
-
rmSync: (target: string, options: { recursive: boolean; force: boolean }) => void;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const nodeStaleInstallFs: StaleInstallFs = {
|
|
226
|
-
readdirSync: (dir) => readdirSync(dir),
|
|
227
|
-
existsSync,
|
|
228
|
-
readFileSync: (target, encoding) => readFileSync(target, encoding),
|
|
229
|
-
rmSync,
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
function isHealthyPackageDir(pkgDir: string, fs: StaleInstallFs): boolean {
|
|
233
|
-
try {
|
|
234
|
-
const pkg = JSON.parse(
|
|
235
|
-
fs.readFileSync(path.join(pkgDir, "package.json"), "utf-8"),
|
|
236
|
-
) as { name?: unknown };
|
|
237
|
-
return pkg.name === CLI_NAME;
|
|
238
|
-
} catch {
|
|
239
|
-
return false; // missing / unreadable / malformed package.json ⇒ partial
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Remove leftover artifacts from an interrupted `npm install -g` so a retry can
|
|
245
|
-
* succeed. npm unpacks a package into a `.<pkg>-<rand>` staging dir alongside
|
|
246
|
-
* the final location and then renames it into place; if a previous run was
|
|
247
|
-
* killed mid-rename (or a half-written package dir survives), every subsequent
|
|
248
|
-
* install fails with `ENOTEMPTY` because npm cannot atomically rename over the
|
|
249
|
-
* non-empty leftover. npm does not self-heal this — the stale dir must be
|
|
250
|
-
* removed first.
|
|
251
|
-
*
|
|
252
|
-
* To stay safe we only ever delete:
|
|
253
|
-
* - dot-prefixed npm staging dirs for THIS package (`.hq-cli-*`), and
|
|
254
|
-
* - a package dir whose `package.json` is missing/unreadable or whose `name`
|
|
255
|
-
* is not exactly {@link CLI_NAME} (i.e. a genuinely partial/foreign dir).
|
|
256
|
-
*
|
|
257
|
-
* A healthy install (valid `package.json`, `name === CLI_NAME`) is never
|
|
258
|
-
* touched, so an ordinary version bump still flows through npm untouched.
|
|
259
|
-
*
|
|
260
|
-
* Returns the list of removed paths — empty when there was nothing to clean, so
|
|
261
|
-
* callers can gate a reinstall retry on `removed.length > 0`.
|
|
262
|
-
*/
|
|
263
|
-
export function cleanStalePartialInstall(
|
|
264
|
-
prefix: string,
|
|
265
|
-
fs: StaleInstallFs = nodeStaleInstallFs,
|
|
266
|
-
): string[] {
|
|
267
|
-
const removed: string[] = [];
|
|
268
|
-
const slash = CLI_NAME.indexOf("/");
|
|
269
|
-
const scope = slash === -1 ? null : CLI_NAME.slice(0, slash);
|
|
270
|
-
const leaf = slash === -1 ? CLI_NAME : CLI_NAME.slice(slash + 1);
|
|
271
|
-
const stagingPrefix = `.${leaf}-`;
|
|
272
|
-
|
|
273
|
-
// Global npm keeps packages under `<prefix>/lib/node_modules` (unix) while a
|
|
274
|
-
// bare `--prefix` dir (windows / some sandboxes) uses `<prefix>/node_modules`.
|
|
275
|
-
const nmRoots = [
|
|
276
|
-
path.join(prefix, "lib", "node_modules"),
|
|
277
|
-
path.join(prefix, "node_modules"),
|
|
278
|
-
];
|
|
279
|
-
|
|
280
|
-
for (const nmRoot of nmRoots) {
|
|
281
|
-
// For a scoped package the staging dir + final dir both live inside the
|
|
282
|
-
// scope dir (`.../@indigoai-us/.hq-cli-<rand>`, `.../@indigoai-us/hq-cli`).
|
|
283
|
-
const parentDir = scope ? path.join(nmRoot, scope) : nmRoot;
|
|
284
|
-
let entries: string[];
|
|
285
|
-
try {
|
|
286
|
-
entries = fs.readdirSync(parentDir);
|
|
287
|
-
} catch {
|
|
288
|
-
continue; // this node_modules / scope dir doesn't exist here
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
for (const entry of entries) {
|
|
292
|
-
if (!entry.startsWith(stagingPrefix)) continue;
|
|
293
|
-
const target = path.join(parentDir, entry);
|
|
294
|
-
try {
|
|
295
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
296
|
-
removed.push(target);
|
|
297
|
-
} catch {
|
|
298
|
-
// best-effort: a dir we can't remove (perms) just means the retry
|
|
299
|
-
// still fails and we fall through to the sudo / manual path.
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const pkgDir = path.join(parentDir, leaf);
|
|
304
|
-
if (fs.existsSync(pkgDir) && !isHealthyPackageDir(pkgDir, fs)) {
|
|
305
|
-
try {
|
|
306
|
-
fs.rmSync(pkgDir, { recursive: true, force: true });
|
|
307
|
-
removed.push(pkgDir);
|
|
308
|
-
} catch {
|
|
309
|
-
// best-effort (see above)
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return removed;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Hit POST /v1/client-version/check. Returns the parsed body on 200, or
|
|
319
|
-
* `null` on any failure (caller treats as "no gate"). Tight 3s timeout —
|
|
320
|
-
* a hung server must not delay CLI startup.
|
|
321
|
-
*/
|
|
322
|
-
async function fetchVersionDecision(): Promise<VersionCheckResponse | null> {
|
|
323
|
-
try {
|
|
324
|
-
const url = `${DEFAULT_VAULT_API_URL}${ENDPOINT_PATH}`;
|
|
325
|
-
const res = await fetch(url, {
|
|
326
|
-
method: "POST",
|
|
327
|
-
headers: {
|
|
328
|
-
"Content-Type": "application/json",
|
|
329
|
-
Accept: "application/json",
|
|
330
|
-
},
|
|
331
|
-
body: JSON.stringify({
|
|
332
|
-
clientId: CLIENT_ID,
|
|
333
|
-
currentVersion: CLI_VERSION,
|
|
334
|
-
platform: `${process.platform}-${process.arch}`,
|
|
335
|
-
}),
|
|
336
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
337
|
-
});
|
|
338
|
-
if (!res.ok) return null;
|
|
339
|
-
const body = (await res.json()) as Partial<VersionCheckResponse>;
|
|
340
|
-
if (
|
|
341
|
-
typeof body.minVersion !== "string" ||
|
|
342
|
-
typeof body.latestVersion !== "string" ||
|
|
343
|
-
typeof body.updateRequired !== "boolean"
|
|
344
|
-
) {
|
|
345
|
-
return null;
|
|
346
|
-
}
|
|
347
|
-
return body as VersionCheckResponse;
|
|
348
|
-
} catch {
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Run the upgrade command in a blocking subprocess. Inherits stdio so the
|
|
355
|
-
* user sees the npm progress. We do NOT auto-rerun the CLI on completion —
|
|
356
|
-
* forcing a re-invocation would run twice on the same process and feel
|
|
357
|
-
* janky; instead we print a clear "rerun your command" message and exit.
|
|
358
|
-
*/
|
|
359
|
-
type UpdateResult = { ok: boolean; detail?: string; code?: string };
|
|
360
|
-
type UpdateRunner = (cmd: string, args: string[]) => UpdateResult;
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Quote an argv entry for a Windows `cmd.exe` invocation. Needed because Node
|
|
364
|
-
* does NOT quote argv when spawning with `shell: true` on Windows — it joins
|
|
365
|
-
* the array with spaces — so an npm prefix like `C:\Program Files\…` would be
|
|
366
|
-
* split into two arguments.
|
|
367
|
-
*/
|
|
368
|
-
export function quoteForWindowsShell(arg: string): string {
|
|
369
|
-
if (arg === "") return '""';
|
|
370
|
-
if (!/[\s"^&|<>()]/.test(arg)) return arg;
|
|
371
|
-
return `"${arg.replace(/"/g, '\\"')}"`;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* How to hand `<cmd> <args…>` to `spawnSync` on this platform.
|
|
376
|
-
*
|
|
377
|
-
* On Windows both `npm` and `pnpm` are `.cmd` shims, and since the
|
|
378
|
-
* CVE-2024-27980 hardening Node refuses to spawn a `.cmd`/`.bat` file without
|
|
379
|
-
* a shell. Without this the update would fail with EINVAL/ENOENT on every
|
|
380
|
-
* Windows install — including the pnpm layouts this gate claims to detect.
|
|
381
|
-
*/
|
|
382
|
-
export function buildSpawnPlan(
|
|
383
|
-
cmd: string,
|
|
384
|
-
args: readonly string[],
|
|
385
|
-
platform: NodeJS.Platform = process.platform,
|
|
386
|
-
): { cmd: string; args: string[]; shell: boolean } {
|
|
387
|
-
if (platform !== "win32") return { cmd, args: [...args], shell: false };
|
|
388
|
-
return { cmd, args: args.map(quoteForWindowsShell), shell: true };
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function runUpdateCommand(cmd: string, args: string[]): UpdateResult {
|
|
392
|
-
try {
|
|
393
|
-
const plan = buildSpawnPlan(cmd, args);
|
|
394
|
-
const result = spawnSync(plan.cmd, plan.args, {
|
|
395
|
-
stdio: "inherit",
|
|
396
|
-
shell: plan.shell,
|
|
397
|
-
});
|
|
398
|
-
// spawnSync reports a missing executable via `error`, not a throw.
|
|
399
|
-
if (result.error) {
|
|
400
|
-
const code = (result.error as NodeJS.ErrnoException).code;
|
|
401
|
-
return { ok: false, code, detail: result.error.message };
|
|
402
|
-
}
|
|
403
|
-
if (result.status !== 0) {
|
|
404
|
-
return {
|
|
405
|
-
ok: false,
|
|
406
|
-
detail: `exit ${result.status ?? "signal"}`,
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
return { ok: true };
|
|
410
|
-
} catch (err) {
|
|
411
|
-
return {
|
|
412
|
-
ok: false,
|
|
413
|
-
code: (err as NodeJS.ErrnoException | undefined)?.code,
|
|
414
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function performUpdateCommand(
|
|
420
|
-
cmd: string,
|
|
421
|
-
args: string[],
|
|
422
|
-
runner: UpdateRunner = runUpdateCommand,
|
|
423
|
-
): UpdateResult {
|
|
424
|
-
return runner(cmd, args);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function performUpdate(
|
|
428
|
-
command: string,
|
|
429
|
-
runner: UpdateRunner = runUpdateCommand,
|
|
430
|
-
): UpdateResult {
|
|
431
|
-
const parts = command.split(/\s+/).filter(Boolean);
|
|
432
|
-
if (parts.length === 0) return { ok: false, detail: "empty command" };
|
|
433
|
-
const cmd = parts[0]!;
|
|
434
|
-
const args = parts.slice(1);
|
|
435
|
-
return performUpdateCommand(cmd, args, runner);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* The command a user should run by hand for this install layout. hq-pro's
|
|
440
|
-
* `updateCommand` is npm-shaped for every client, so a pnpm-managed install
|
|
441
|
-
* must never be told to run it: `npm install -g …` drops a fresh copy under the
|
|
442
|
-
* npm global prefix while pnpm's PATH shim keeps resolving the old build. That
|
|
443
|
-
* is verbatim the reported symptom (a stale shim still reporting 5.61.0 after a
|
|
444
|
-
* "successful" update), so both the hard gate and the soft nudge below have to
|
|
445
|
-
* speak the running manager's language.
|
|
446
|
-
*/
|
|
447
|
-
function manualUpdateCommand(
|
|
448
|
-
install: RunningInstall,
|
|
449
|
-
decision: VersionCheckResponse,
|
|
450
|
-
): string | undefined {
|
|
451
|
-
if (install.manager === "pnpm") return `pnpm ${buildPnpmInstallArgv().join(" ")}`;
|
|
452
|
-
if (install.prefix) return `npm ${buildPrefixedInstallArgv(install.prefix).join(" ")}`;
|
|
453
|
-
return decision.updateCommand;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Soft notify when the server says we're below `latestVersion` but still ≥
|
|
458
|
-
* `minVersion`. Single chalk-yellow line on stderr; never blocks.
|
|
459
|
-
*
|
|
460
|
-
* This path fires for EVERY version below latest (the hard gate only fires
|
|
461
|
-
* below `minVersion`), so it is the far more frequently seen of the two and
|
|
462
|
-
* must be manager-aware for the same reason the gate is.
|
|
463
|
-
*/
|
|
464
|
-
function nudgeUpdateRecommended(
|
|
465
|
-
decision: VersionCheckResponse,
|
|
466
|
-
install: RunningInstall = resolveRunningInstall(),
|
|
467
|
-
): void {
|
|
468
|
-
const msg = chalk.yellow(
|
|
469
|
-
`⚠ A new version of hq-cli is available: ${decision.latestVersion} (current: ${decision.currentVersion}).`,
|
|
470
|
-
);
|
|
471
|
-
console.error(msg);
|
|
472
|
-
const command =
|
|
473
|
-
install.manager === "pnpm"
|
|
474
|
-
? `pnpm ${buildPnpmInstallArgv().join(" ")}`
|
|
475
|
-
: decision.updateCommand;
|
|
476
|
-
if (command) {
|
|
477
|
-
console.error(chalk.dim(` Update: ${command}`));
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Hard enforcement when the server says we're below `minVersion`. Print a
|
|
483
|
-
* red banner, attempt the update, then exit so the user reruns against the
|
|
484
|
-
* fresh binary. Sequence chosen so a user with a broken `npm` global prefix
|
|
485
|
-
* still gets a clear error rather than an opaque silent failure.
|
|
486
|
-
*
|
|
487
|
-
* Exit codes:
|
|
488
|
-
* 0 — update succeeded; user must rerun their command
|
|
489
|
-
* 75 — update failed (EX_TEMPFAIL; common for sudo/EACCES on system npm)
|
|
490
|
-
*/
|
|
491
|
-
function enforceUpdateRequired(
|
|
492
|
-
decision: VersionCheckResponse,
|
|
493
|
-
deps: {
|
|
494
|
-
performUpdateString?: (command: string) => UpdateResult;
|
|
495
|
-
resolveInstall?: () => RunningInstall;
|
|
496
|
-
runner?: UpdateRunner;
|
|
497
|
-
cleanStale?: (prefix: string) => string[];
|
|
498
|
-
} = {},
|
|
499
|
-
): never {
|
|
500
|
-
const banner = chalk.red.bold(
|
|
501
|
-
`✗ hq-cli ${decision.currentVersion} is below the minimum required version (${decision.minVersion}).`,
|
|
502
|
-
);
|
|
503
|
-
console.error(banner);
|
|
504
|
-
if (decision.message) console.error(chalk.dim(` ${decision.message}`));
|
|
505
|
-
|
|
506
|
-
const command = decision.updateCommand;
|
|
507
|
-
// ONE package-root walk decides both the manager and the prefix. A
|
|
508
|
-
// pnpm-managed install must be updated by pnpm; the server's `updateCommand`
|
|
509
|
-
// and any npm prefix are both wrong for that layout, so `resolveRunningInstall`
|
|
510
|
-
// already reports `prefix: null` there and neither is consulted below.
|
|
511
|
-
const install = (deps.resolveInstall ?? resolveRunningInstall)();
|
|
512
|
-
const isPnpm = install.manager === "pnpm";
|
|
513
|
-
const prefix = install.prefix;
|
|
514
|
-
if (!isPnpm && !command && !prefix) {
|
|
515
|
-
console.error(
|
|
516
|
-
chalk.red(
|
|
517
|
-
" No updateCommand provided by hq-pro — see https://hq.indigo.ai/docs/cli-update for manual steps.",
|
|
518
|
-
),
|
|
519
|
-
);
|
|
520
|
-
if (decision.downloadUrl) {
|
|
521
|
-
console.error(chalk.dim(` Download: ${decision.downloadUrl}`));
|
|
522
|
-
}
|
|
523
|
-
process.exit(75);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const runner = deps.runner ?? runUpdateCommand;
|
|
527
|
-
// Resolve the concrete install argv once so the sudo fallback below can re-run
|
|
528
|
-
// the EXACT same command under elevation.
|
|
529
|
-
let primaryCmd: string;
|
|
530
|
-
let primaryArgs: string[];
|
|
531
|
-
if (isPnpm) {
|
|
532
|
-
primaryCmd = "pnpm";
|
|
533
|
-
primaryArgs = buildPnpmInstallArgv();
|
|
534
|
-
} else if (prefix) {
|
|
535
|
-
primaryCmd = "npm";
|
|
536
|
-
primaryArgs = buildPrefixedInstallArgv(prefix);
|
|
537
|
-
} else {
|
|
538
|
-
const parts = (command ?? "").split(/\s+/).filter(Boolean);
|
|
539
|
-
primaryCmd = parts[0] ?? "";
|
|
540
|
-
primaryArgs = parts.slice(1);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
let result: UpdateResult;
|
|
544
|
-
if (isPnpm) {
|
|
545
|
-
console.error(
|
|
546
|
-
chalk.dim(" Detected a pnpm-managed global install; updating with pnpm"),
|
|
547
|
-
);
|
|
548
|
-
console.error(chalk.dim(` Running: pnpm ${primaryArgs.join(" ")}`));
|
|
549
|
-
result = performUpdateCommand("pnpm", primaryArgs, runner);
|
|
550
|
-
} else if (prefix) {
|
|
551
|
-
console.error(chalk.dim(` Installing into npm prefix: ${prefix}`));
|
|
552
|
-
console.error(chalk.dim(` Running: npm ${primaryArgs.join(" ")}`));
|
|
553
|
-
result = performUpdateCommand("npm", primaryArgs, runner);
|
|
554
|
-
} else {
|
|
555
|
-
console.error(chalk.dim(` Running: ${command}`));
|
|
556
|
-
result = deps.performUpdateString
|
|
557
|
-
? deps.performUpdateString(command!)
|
|
558
|
-
: performUpdate(command!, runner);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// A partial/corrupt global install leaves npm unable to atomically rename its
|
|
562
|
-
// freshly-unpacked package over a leftover directory, so the install above
|
|
563
|
-
// fails with ENOTEMPTY (e.g. a prior interrupted `npm install -g` left a
|
|
564
|
-
// half-written `hq-cli` package dir or a `.hq-cli-<rand>` staging dir under
|
|
565
|
-
// the prefix's node_modules). npm cannot self-heal this. Remove the stale
|
|
566
|
-
// artifacts and retry the install ONCE. Guarded on `cleaned.length > 0` so a
|
|
567
|
-
// plain EACCES on an otherwise-healthy prefix falls straight through to the
|
|
568
|
-
// sudo retry below without a redundant reinstall attempt.
|
|
569
|
-
if (!result.ok && prefix) {
|
|
570
|
-
const cleaner = deps.cleanStale ?? cleanStalePartialInstall;
|
|
571
|
-
const cleaned = cleaner(prefix);
|
|
572
|
-
if (cleaned.length > 0) {
|
|
573
|
-
console.error(
|
|
574
|
-
chalk.dim(
|
|
575
|
-
` Removing stale partial install artifacts and retrying: ${cleaned.join(", ")}`,
|
|
576
|
-
),
|
|
577
|
-
);
|
|
578
|
-
result = performUpdateCommand("npm", primaryArgs, runner);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// A root-owned global install (e.g. a system `/usr` install where the CLI runs
|
|
583
|
-
// unprivileged — the outpost agent boxes) can't rewrite the prefix's bin dir,
|
|
584
|
-
// so the install above fails with EACCES (`rename /usr/bin/hq`). Retry ONCE
|
|
585
|
-
// under non-interactive sudo: `sudo -n` fails fast WITHOUT a password prompt
|
|
586
|
-
// when passwordless sudo isn't configured, so interactive installs (e.g.
|
|
587
|
-
// Homebrew on macOS, where the first attempt already succeeded anyway) fall
|
|
588
|
-
// through to the manual path unchanged, while headless boxes with passwordless
|
|
589
|
-
// sudo self-update cleanly.
|
|
590
|
-
//
|
|
591
|
-
// Never for pnpm: `sudo -n pnpm add -g` installs into ROOT's PNPM_HOME, which
|
|
592
|
-
// leaves the user's shim untouched while reporting success — the same silent
|
|
593
|
-
// no-op this fix exists to remove. A failed pnpm update must surface instead.
|
|
594
|
-
if (!result.ok && primaryCmd && !isPnpm) {
|
|
595
|
-
console.error(
|
|
596
|
-
chalk.dim(
|
|
597
|
-
` Update failed unprivileged; retrying with: sudo -n ${primaryCmd} ${primaryArgs.join(" ")}`,
|
|
598
|
-
),
|
|
599
|
-
);
|
|
600
|
-
const sudoResult = performUpdateCommand(
|
|
601
|
-
"sudo",
|
|
602
|
-
["-n", primaryCmd, ...primaryArgs],
|
|
603
|
-
runner,
|
|
604
|
-
);
|
|
605
|
-
if (sudoResult.ok) result = sudoResult;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
if (!result.ok) {
|
|
609
|
-
console.error(
|
|
610
|
-
chalk.red(`✗ Update failed${result.detail ? `: ${result.detail}` : ""}.`),
|
|
611
|
-
);
|
|
612
|
-
// The package manager itself is missing from this environment — the usual
|
|
613
|
-
// cause is a minimal-PATH parent (launchd, cron, a bare systemd unit) that
|
|
614
|
-
// never sourced the shell profile which puts PNPM_HOME (or nvm's npm) on
|
|
615
|
-
// PATH. Say so, because "exit 75" alone reads as a permissions problem.
|
|
616
|
-
if (result.code === "ENOENT") {
|
|
617
|
-
console.error(
|
|
618
|
-
chalk.red(
|
|
619
|
-
` \`${primaryCmd}\` was not found on PATH in this environment (launchd, cron and other minimal-PATH parents commonly lack it).`,
|
|
620
|
-
),
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
const manual = manualUpdateCommand(install, decision) ?? command!;
|
|
624
|
-
console.error(chalk.dim(` Try manually: ${manual}`));
|
|
625
|
-
// Only informational, and only when we could not run the right manager at
|
|
626
|
-
// all: hq-pro's command is npm-shaped, so following it on a pnpm layout is
|
|
627
|
-
// what produced the stale-shim loop in the first place.
|
|
628
|
-
if (isPnpm && result.code === "ENOENT" && command) {
|
|
629
|
-
console.error(
|
|
630
|
-
chalk.dim(
|
|
631
|
-
` (hq-pro suggests \`${command}\` — that is for npm-managed installs; use it only if you have switched this install to npm.)`,
|
|
632
|
-
),
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
process.exit(75);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
console.error(
|
|
639
|
-
chalk.green(
|
|
640
|
-
`✓ Updated to hq-cli ${decision.latestVersion}. Rerun your command.`,
|
|
641
|
-
),
|
|
642
|
-
);
|
|
643
|
-
process.exit(0);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* Public entry point. Call before commander parses argv. Blocks the CLI on
|
|
648
|
-
* network IO for up to FETCH_TIMEOUT_MS — acceptable because the alternative
|
|
649
|
-
* (a fire-and-forget background check) gives the user no chance to bail out
|
|
650
|
-
* of a known-bad version before it does damage.
|
|
651
|
-
*
|
|
652
|
-
* `--version` / `-v` callers MUST skip the gate (the user is debugging a
|
|
653
|
-
* broken install and shouldn't be force-upgraded mid-investigation). Caller
|
|
654
|
-
* is responsible for checking argv before invoking us — see index.ts.
|
|
655
|
-
*/
|
|
656
|
-
export async function enforceVersionGate(): Promise<void> {
|
|
657
|
-
if (isOptedOut()) return;
|
|
658
|
-
const decision = await fetchVersionDecision();
|
|
659
|
-
if (!decision) return; // best-effort: silent on any failure
|
|
660
|
-
if (!decision.updateRequired && !decision.updateRecommended) return;
|
|
661
|
-
// Resolved once, here, so the up-to-date case never pays for the walk and
|
|
662
|
-
// neither downstream path repeats it.
|
|
663
|
-
const install = resolveRunningInstall();
|
|
664
|
-
if (decision.updateRequired) {
|
|
665
|
-
enforceUpdateRequired(decision, { resolveInstall: () => install }); // exits process
|
|
666
|
-
}
|
|
667
|
-
if (decision.updateRecommended) {
|
|
668
|
-
nudgeUpdateRecommended(decision, install);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Cheap argv pre-check: skip the gate for `--version` / `-V` so users
|
|
674
|
-
* inspecting a broken install can still see what they have without being
|
|
675
|
-
* force-upgraded.
|
|
676
|
-
*/
|
|
677
|
-
export function shouldSkipGate(argv: readonly string[]): boolean {
|
|
678
|
-
return argv.some(
|
|
679
|
-
(a) => a === "--version" || a === "-V" || a === "-v" || a === "--help" || a === "-h",
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
export const __test__ = {
|
|
684
|
-
CLIENT_ID,
|
|
685
|
-
ENDPOINT_PATH,
|
|
686
|
-
FETCH_TIMEOUT_MS,
|
|
687
|
-
buildPnpmInstallArgv,
|
|
688
|
-
buildPrefixedInstallArgv,
|
|
689
|
-
buildSpawnPlan,
|
|
690
|
-
cleanStalePartialInstall,
|
|
691
|
-
enforceUpdateRequired,
|
|
692
|
-
isPnpmManagedPackageDir,
|
|
693
|
-
npmPrefixFromPackageDir,
|
|
694
|
-
nudgeUpdateRecommended,
|
|
695
|
-
performUpdate,
|
|
696
|
-
performUpdateCommand,
|
|
697
|
-
quoteForWindowsShell,
|
|
698
|
-
runUpdateCommand,
|
|
699
|
-
resolveRunningInstall,
|
|
700
|
-
resolveRunningManager,
|
|
701
|
-
resolveRunningPrefix,
|
|
702
|
-
};
|