@indigoai-us/hq-cli 5.345.32 → 5.345.34
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 +15 -0
- package/dist/command-catalog.generated.d.ts +92 -3
- package/dist/command-catalog.generated.js +111 -3
- package/dist/commands/cloud-provision.d.ts +2 -0
- package/dist/commands/db-dump.d.ts +8 -0
- package/dist/commands/db-dump.js +50 -0
- package/dist/commands/db-provision.d.ts +3 -10
- package/dist/commands/db-provision.js +26 -47
- package/dist/commands/db-remote-deps.d.ts +18 -0
- package/dist/commands/db-remote-deps.js +59 -0
- package/dist/commands/db-sql.d.ts +2 -1
- package/dist/commands/db-sql.js +30 -5
- package/dist/commands/db-usage.d.ts +7 -0
- package/dist/commands/db-usage.js +28 -0
- package/dist/commands/db.js +4 -0
- package/dist/commands/files.js +33 -5
- package/dist/commands/lanes-message.js +40 -8
- package/dist/commands/lanes-questions.d.ts +6 -1
- package/dist/commands/lanes-questions.js +19 -7
- package/dist/commands/lanes-resume.js +17 -6
- package/dist/commands/lanes-shared.d.ts +6 -1
- package/dist/commands/lanes.js +92 -15
- package/dist/commands/mesh.js +150 -26
- package/dist/commands/reindex.js +11 -8
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +68 -604
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/daemon/remote-control.js +62 -3
- package/dist/lib/db/control-plane.d.ts +32 -0
- package/dist/lib/db/control-plane.js +50 -0
- package/dist/lib/db/redact.d.ts +4 -0
- package/dist/lib/db/redact.js +17 -0
- package/dist/lib/db/remote-engine.d.ts +67 -0
- package/dist/lib/db/remote-engine.js +165 -0
- package/dist/lib/db/usage-format.d.ts +4 -0
- package/dist/lib/db/usage-format.js +41 -0
- package/dist/lib/index-render/shared.d.ts +3 -1
- package/dist/lib/index-render/shared.js +5 -2
- package/dist/lib/lanes/depth-cap.d.ts +7 -0
- package/dist/lib/lanes/depth-cap.js +41 -0
- package/dist/lib/lanes/keep-alive.d.ts +12 -0
- package/dist/lib/lanes/keep-alive.js +25 -6
- package/dist/lib/lanes/reconcile.d.ts +1 -1
- package/dist/lib/lanes/reconcile.js +16 -5
- package/dist/lib/mesh/api.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/install.d.ts +7 -4
- package/dist/lib/mesh/live/daemon/install.js +144 -47
- package/dist/lib/mesh/project-board.d.ts +17 -0
- package/dist/lib/mesh/project-board.js +28 -0
- package/dist/lib/mesh/story-status.d.ts +16 -0
- package/dist/lib/mesh/story-status.js +32 -7
- package/dist/utils/self-update.d.ts +2 -0
- package/dist/utils/self-update.js +61 -23
- package/dist/utils/version-check.d.ts +1 -1
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-gate.d.ts +21 -14
- package/dist/utils/version-gate.js +119 -28
- package/package.json +4 -1
|
@@ -5,27 +5,51 @@ const boardStoryStatusSet = new Set(BOARD_STORY_STATUSES);
|
|
|
5
5
|
* need attention, while `dropped` is terminal; the Board has only one terminal
|
|
6
6
|
* column (`done`).
|
|
7
7
|
*/
|
|
8
|
+
export const LEDGER_STATES = [
|
|
9
|
+
"todo", "in_progress", "review", "waiting", "blocked", "done", "dropped",
|
|
10
|
+
];
|
|
8
11
|
const LEDGER_STATE_TO_BOARD_STATUS = {
|
|
9
12
|
todo: "queued",
|
|
10
13
|
in_progress: "in_progress",
|
|
14
|
+
review: "review",
|
|
15
|
+
// The four-column Board cannot represent waiting separately. Preserve the
|
|
16
|
+
// ledger state on each story so consumers can distinguish it from blocked.
|
|
17
|
+
waiting: "queued",
|
|
11
18
|
in_review: "review",
|
|
12
19
|
verification: "review",
|
|
13
20
|
blocked: "review",
|
|
14
21
|
done: "done",
|
|
15
22
|
dropped: "done",
|
|
16
23
|
};
|
|
24
|
+
export const ACCEPTED_LEDGER_STATES = [
|
|
25
|
+
...LEDGER_STATES, "in_review", "verification",
|
|
26
|
+
];
|
|
27
|
+
export class UnknownProjectStoryStateError extends Error {
|
|
28
|
+
source;
|
|
29
|
+
storyId;
|
|
30
|
+
value;
|
|
31
|
+
code = "PROJECT_STORY_STATE_INVALID";
|
|
32
|
+
constructor(source, storyId, value) {
|
|
33
|
+
super(`${source}.state for story ${storyId || "<missing id>"} has unsupported value ${JSON.stringify(value)}; accepted: ${ACCEPTED_LEDGER_STATES.join("|")}.`);
|
|
34
|
+
this.source = source;
|
|
35
|
+
this.storyId = storyId;
|
|
36
|
+
this.value = value;
|
|
37
|
+
this.name = "UnknownProjectStoryStateError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
17
40
|
export function mapSourceStoryStatus(story, source) {
|
|
41
|
+
if (Object.hasOwn(story, "state")) {
|
|
42
|
+
if (typeof story.state !== "string" || !Object.hasOwn(LEDGER_STATE_TO_BOARD_STATUS, story.state.trim())) {
|
|
43
|
+
throw new UnknownProjectStoryStateError(source, "id" in story && typeof story.id === "string" ? story.id : "", story.state);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
18
46
|
if (story.status !== undefined && story.status !== null) {
|
|
19
47
|
if (typeof story.status !== "string" || !boardStoryStatusSet.has(story.status.trim())) {
|
|
20
48
|
throw new Error(`${source}.status must be queued|in_progress|review|done.`);
|
|
21
49
|
}
|
|
22
50
|
return story.status.trim();
|
|
23
51
|
}
|
|
24
|
-
if (
|
|
25
|
-
if (typeof story.state !== "string" ||
|
|
26
|
-
!Object.hasOwn(LEDGER_STATE_TO_BOARD_STATUS, story.state.trim())) {
|
|
27
|
-
throw new Error(`${source}.state must be todo|in_progress|in_review|verification|blocked|done|dropped.`);
|
|
28
|
-
}
|
|
52
|
+
if (typeof story.state === "string") {
|
|
29
53
|
return LEDGER_STATE_TO_BOARD_STATUS[story.state.trim()];
|
|
30
54
|
}
|
|
31
55
|
return undefined;
|
|
@@ -39,9 +63,10 @@ export function mapReaderStoryStatus(story) {
|
|
|
39
63
|
if (story.status !== undefined && story.status !== null) {
|
|
40
64
|
return typeof story.status === "string" ? story.status : undefined;
|
|
41
65
|
}
|
|
42
|
-
|
|
66
|
+
const stateValue = story.state ?? story.ledgerState;
|
|
67
|
+
if (typeof stateValue !== "string")
|
|
43
68
|
return undefined;
|
|
44
|
-
const state =
|
|
69
|
+
const state = stateValue.trim();
|
|
45
70
|
return Object.hasOwn(LEDGER_STATE_TO_BOARD_STATUS, state)
|
|
46
71
|
? LEDGER_STATE_TO_BOARD_STATUS[state]
|
|
47
72
|
: undefined;
|
|
@@ -159,6 +159,8 @@ export interface SelfUpdateDeps {
|
|
|
159
159
|
* the target we just installed. Defaults to {@link checkUpdateConvergence}.
|
|
160
160
|
*/
|
|
161
161
|
checkConvergence?: (targetVersion: string) => boolean;
|
|
162
|
+
/** Probe the previous command path after a failed install has been rolled back. */
|
|
163
|
+
checkRestoredInstall?: (targetVersion: string) => boolean;
|
|
162
164
|
/**
|
|
163
165
|
* Persist that `version` installed but never took effect, so the startup path
|
|
164
166
|
* stops auto-retrying it. Defaults to {@link markLatestIneffective}.
|
|
@@ -65,7 +65,7 @@ import chalk from "chalk";
|
|
|
65
65
|
import { Sentry } from "../sentry.js";
|
|
66
66
|
import { isCliTelemetryDisabled } from "./cli-telemetry.js";
|
|
67
67
|
import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
|
|
68
|
-
import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput, pnpmUpdateEnv, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, } from "./version-gate.js";
|
|
68
|
+
import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput, pnpmUpdateEnv, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, UPDATE_RECURSION_GUARD_ENV, } from "./version-gate.js";
|
|
69
69
|
import { canStageInstall, stagedNpmInstall } from "./staged-install.js";
|
|
70
70
|
import { acquireUpdateLock as acquireSharedUpdateLock } from "./update-lock.js";
|
|
71
71
|
import { markLatestIneffective } from "./version-check.js";
|
|
@@ -75,7 +75,7 @@ import { createCurrentPackageUseLeaseHandoff, PACKAGE_USE_LEASE_HANDOFF_ENV, rel
|
|
|
75
75
|
* Set on the re-exec'd child so it can never self-update (and re-exec) again.
|
|
76
76
|
* One update + one re-exec per user invocation, ever.
|
|
77
77
|
*/
|
|
78
|
-
export const REEXEC_GUARD_ENV =
|
|
78
|
+
export const REEXEC_GUARD_ENV = UPDATE_RECURSION_GUARD_ENV;
|
|
79
79
|
const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(CLI_NAME)}/latest`;
|
|
80
80
|
const FETCH_TIMEOUT_MS = 3_000;
|
|
81
81
|
/**
|
|
@@ -549,7 +549,7 @@ async function fetchLatestVersion() {
|
|
|
549
549
|
*/
|
|
550
550
|
export function buildSelfUpdatePlan(install) {
|
|
551
551
|
if (install.manager === "pnpm")
|
|
552
|
-
return { cmd: "pnpm", args: buildPnpmInstallArgv() };
|
|
552
|
+
return { cmd: "pnpm", args: buildPnpmInstallArgv(install) };
|
|
553
553
|
if (install.manager === "bun")
|
|
554
554
|
return { cmd: "bun", args: buildBunInstallArgv() };
|
|
555
555
|
if (install.prefix)
|
|
@@ -780,28 +780,43 @@ async function updateAndReexec(argv, flavor, known, deps) {
|
|
|
780
780
|
}
|
|
781
781
|
const staged = canStageInstall(install) && (await readStagedSelfUpdateFlag(deps, env));
|
|
782
782
|
const renamePath = deps.renamePath ?? fs.renameSync;
|
|
783
|
-
// The in-place update moves the package aside, and the installer's
|
|
784
|
-
// supervisor ships inside it, so run the installer through a copy.
|
|
785
|
-
const supervisor = staged ? null : (deps.detachSupervisor ?? detachUpdateSupervisor)();
|
|
786
783
|
// The staged install keeps the running package in place until the swap and
|
|
787
784
|
// restores it itself, so it takes no snapshot.
|
|
785
|
+
// The installer supervisor must be copied before snapshotInstall moves the
|
|
786
|
+
// active package that contains its source.
|
|
787
|
+
const supervisor = staged ? null : (deps.detachSupervisor ?? detachUpdateSupervisor)();
|
|
788
788
|
const installSnapshot = staged ? null : snapshotInstall(install, renamePath);
|
|
789
789
|
if (!staged && !installSnapshot) {
|
|
790
|
-
|
|
790
|
+
(deps.markIneffective ?? markLatestIneffective)(latest);
|
|
791
|
+
console.error(chalk.dim(` Update to hq-cli ${latest} skipped; the current install could not be backed up.`));
|
|
792
|
+
supervisor?.dispose();
|
|
793
|
+
releaseLock();
|
|
794
|
+
return { action: "update-failed", latest };
|
|
791
795
|
}
|
|
796
|
+
const checkRestoredInstall = deps.checkRestoredInstall ?? ((version) => checkUpdateConvergence(version, {
|
|
797
|
+
requireEvidence: true,
|
|
798
|
+
exactVersion: true,
|
|
799
|
+
currentInstall: () => install,
|
|
800
|
+
}));
|
|
792
801
|
let result;
|
|
793
802
|
const plan = buildSelfUpdatePlan(install);
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
803
|
+
// pnpm needs its global bin path on PATH. Corepack must also ignore a
|
|
804
|
+
// packageManager pin in the caller's working directory so this install uses
|
|
805
|
+
// the system-wide package manager without changing its selected version.
|
|
806
|
+
const updateEnv = install.manager === "pnpm"
|
|
807
|
+
? {
|
|
808
|
+
...env,
|
|
809
|
+
...(pnpmUpdateEnv(install, env) ?? {}),
|
|
810
|
+
// Package-manager lifecycle hooks can run hq while this install is still
|
|
811
|
+
// in progress. They must never start another update or manager lookup.
|
|
812
|
+
[REEXEC_GUARD_ENV]: "1",
|
|
813
|
+
}
|
|
814
|
+
: undefined;
|
|
802
815
|
let restoredAfterFailure = false;
|
|
803
816
|
try {
|
|
804
|
-
|
|
817
|
+
if (flavor.noun !== "command") {
|
|
818
|
+
console.error(chalk.dim(`Updating hq-cli ${current} → ${latest}…`));
|
|
819
|
+
}
|
|
805
820
|
const defaultRunner = flavor.verbose ? runUpdateCommand : runUpdateQuiet;
|
|
806
821
|
const runner = deps.runner ?? defaultRunner;
|
|
807
822
|
// Forward env only when PNPM_HOME had to be injected, so the common path
|
|
@@ -841,14 +856,21 @@ async function updateAndReexec(argv, flavor, known, deps) {
|
|
|
841
856
|
}
|
|
842
857
|
if (!result.ok && installSnapshot) {
|
|
843
858
|
restoredAfterFailure = restoreInstall(installSnapshot, renamePath);
|
|
844
|
-
if (restoredAfterFailure)
|
|
845
|
-
|
|
859
|
+
if (restoredAfterFailure) {
|
|
860
|
+
restoredAfterFailure = checkRestoredInstall(current);
|
|
861
|
+
if (restoredAfterFailure)
|
|
862
|
+
discardInstallSnapshot(installSnapshot);
|
|
863
|
+
}
|
|
846
864
|
}
|
|
847
865
|
}
|
|
848
866
|
catch (error) {
|
|
849
867
|
if (installSnapshot) {
|
|
850
|
-
if (restoreInstall(installSnapshot, renamePath))
|
|
868
|
+
if (restoreInstall(installSnapshot, renamePath) && checkRestoredInstall(current)) {
|
|
851
869
|
discardInstallSnapshot(installSnapshot);
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
console.error(chalk.red(" Could not restore and verify the previous hq-cli install after the update process failed."));
|
|
873
|
+
}
|
|
852
874
|
}
|
|
853
875
|
throw error;
|
|
854
876
|
}
|
|
@@ -857,6 +879,12 @@ async function updateAndReexec(argv, flavor, known, deps) {
|
|
|
857
879
|
releaseLock();
|
|
858
880
|
}
|
|
859
881
|
if (!result.ok) {
|
|
882
|
+
(deps.markIneffective ?? markLatestIneffective)(latest);
|
|
883
|
+
if (flavor.noun === "command") {
|
|
884
|
+
await captureSelfUpdateOutcome("update-failed", current, latestValid, install.manager, result, env, false);
|
|
885
|
+
console.error(chalk.yellow(`⚠ hq-cli update to ${latest} failed; continuing on ${current}.`));
|
|
886
|
+
return { action: "update-failed", latest };
|
|
887
|
+
}
|
|
860
888
|
if (restoredAfterFailure) {
|
|
861
889
|
console.error(chalk.dim(" The previous hq-cli install was restored."));
|
|
862
890
|
}
|
|
@@ -902,8 +930,6 @@ async function updateAndReexec(argv, flavor, known, deps) {
|
|
|
902
930
|
}
|
|
903
931
|
return { action: "update-failed", latest };
|
|
904
932
|
}
|
|
905
|
-
if (installSnapshot)
|
|
906
|
-
discardInstallSnapshot(installSnapshot);
|
|
907
933
|
// Read-your-writes: a "successful" `npm install -g` proves only that the
|
|
908
934
|
// prefix was rewritten, NOT that the `hq` this user's PATH resolves is the
|
|
909
935
|
// copy we just wrote. When a ghost/shadowing install keeps winning PATH
|
|
@@ -917,14 +943,26 @@ async function updateAndReexec(argv, flavor, known, deps) {
|
|
|
917
943
|
// So verify before announcing success. On non-convergence: mark the target
|
|
918
944
|
// ineffective (suppresses auto-retry for a cooldown), skip the pointless
|
|
919
945
|
// re-exec into a stale copy, and let the command run on the current version.
|
|
920
|
-
const checkConvergence = deps.checkConvergence ?? checkUpdateConvergence;
|
|
946
|
+
const checkConvergence = deps.checkConvergence ?? ((version) => checkUpdateConvergence(version, { requireEvidence: true, currentInstall: () => install }));
|
|
921
947
|
const converged = checkConvergence(latestValid);
|
|
922
948
|
if (converged === false) {
|
|
949
|
+
let restored = true;
|
|
950
|
+
if (installSnapshot) {
|
|
951
|
+
restored = restoreInstall(installSnapshot, renamePath);
|
|
952
|
+
if (restored)
|
|
953
|
+
restored = checkRestoredInstall(current);
|
|
954
|
+
if (restored)
|
|
955
|
+
discardInstallSnapshot(installSnapshot);
|
|
956
|
+
else
|
|
957
|
+
console.error(chalk.red(" Could not restore and verify the previous hq-cli install after verification failed."));
|
|
958
|
+
}
|
|
923
959
|
(deps.markIneffective ?? markLatestIneffective)(latest);
|
|
924
960
|
console.error(chalk.dim(` Continuing the ${flavor.noun} on ${current}.`));
|
|
925
961
|
await captureSelfUpdateOutcome("update-ineffective", current, latestValid, install.manager, undefined, env, flavor.noun === "rescue");
|
|
926
|
-
return { action: "update-ineffective", latest };
|
|
962
|
+
return { action: restored ? "update-ineffective" : "update-failed", latest };
|
|
927
963
|
}
|
|
964
|
+
if (installSnapshot)
|
|
965
|
+
discardInstallSnapshot(installSnapshot);
|
|
928
966
|
// A resident mesh daemon keeps the old JS loaded after a global install.
|
|
929
967
|
// Service restart is best-effort and cannot turn a successful update red.
|
|
930
968
|
try {
|
|
@@ -16,7 +16,7 @@ export declare function staleAgainstCachedLatest(now?: number): string | null;
|
|
|
16
16
|
/**
|
|
17
17
|
* Record that installing `version` did not move the on-PATH `hq` forward, so
|
|
18
18
|
* {@link staleAgainstCachedLatest} stops auto-retrying it for a cooldown. Called
|
|
19
|
-
* by the self-updater
|
|
19
|
+
* by the self-updater after an install failure or failed convergence check. Best-effort: a write failure only
|
|
20
20
|
* means the loop guard is skipped this once, never a broken CLI.
|
|
21
21
|
*/
|
|
22
22
|
export declare function markLatestIneffective(version: string, now?: number): void;
|
|
@@ -15,7 +15,7 @@ export const CLI_VERSION_NUDGE_FLAG = "cli.version-update-nudge";
|
|
|
15
15
|
* break the per-invocation loop on a busy agent box, short enough that a box
|
|
16
16
|
* whose PATH is later fixed recovers on its own without a manual `hq rescue`.
|
|
17
17
|
*/
|
|
18
|
-
const INEFFECTIVE_COOLDOWN_MS =
|
|
18
|
+
const INEFFECTIVE_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24h
|
|
19
19
|
function cachePath() {
|
|
20
20
|
return path.join(os.homedir(), ".hq", "version-check.json");
|
|
21
21
|
}
|
|
@@ -157,7 +157,7 @@ export function staleAgainstCachedLatest(now = Date.now()) {
|
|
|
157
157
|
/**
|
|
158
158
|
* Record that installing `version` did not move the on-PATH `hq` forward, so
|
|
159
159
|
* {@link staleAgainstCachedLatest} stops auto-retrying it for a cooldown. Called
|
|
160
|
-
* by the self-updater
|
|
160
|
+
* by the self-updater after an install failure or failed convergence check. Best-effort: a write failure only
|
|
161
161
|
* means the loop guard is skipped this once, never a broken CLI.
|
|
162
162
|
*/
|
|
163
163
|
export function markLatestIneffective(version, now = Date.now()) {
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { buildSpawnPlan, quoteForWindowsShell } from "./windows-spawn.js";
|
|
31
31
|
import { type UpdateLockHandle } from "./update-lock.js";
|
|
32
|
+
/** Prevent an update package manager's lifecycle child from starting another updater. */
|
|
33
|
+
export declare const UPDATE_RECURSION_GUARD_ENV = "HQ_RESCUE_SELF_UPDATED";
|
|
32
34
|
export type VersionGatePhase = "preflight" | "lock_acquired" | "installer_running" | "verifying" | "rollback" | "complete" | "failed" | "unknown";
|
|
33
35
|
/** The last closed version-gate phase observed in this process. */
|
|
34
36
|
export declare function getVersionGatePhase(): VersionGatePhase;
|
|
@@ -205,19 +207,13 @@ export declare function nonWritablePrefixNote(prefix: string, platform?: NodeJS.
|
|
|
205
207
|
*/
|
|
206
208
|
export declare function derivePnpmHome(install: RunningInstall): string | null;
|
|
207
209
|
/**
|
|
208
|
-
* Environment for the pnpm self-update spawn.
|
|
209
|
-
*
|
|
210
|
-
* `
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* `
|
|
215
|
-
* find the global bin dir it is about to rewrite.
|
|
216
|
-
*
|
|
217
|
-
* A no-op for npm/bun installs, and for any environment that already sets
|
|
218
|
-
* `PNPM_HOME` (never overridden — the caller's value wins). Returns `undefined`
|
|
219
|
-
* when nothing needs injecting, so callers can spawn with the inherited
|
|
220
|
-
* environment unchanged (and pass no `env` at all).
|
|
210
|
+
* Environment for the pnpm self-update spawn. pnpm requires its configured
|
|
211
|
+
* global bin directory to be on PATH; pnpm 12.5.1 reports
|
|
212
|
+
* `ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH` otherwise. Minimal-environment parents
|
|
213
|
+
* may also lack PNPM_HOME, so derive it from the running install when needed.
|
|
214
|
+
*
|
|
215
|
+
* A no-op for npm/bun installs. Never override a caller's PNPM_HOME. Returns
|
|
216
|
+
* `undefined` only when the inherited environment already has both values.
|
|
221
217
|
*/
|
|
222
218
|
export declare function pnpmUpdateEnv(install: RunningInstall, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv | undefined;
|
|
223
219
|
export declare function buildPrefixedInstallArgv(prefix: string): string[];
|
|
@@ -226,7 +222,16 @@ export declare function buildPrefixedInstallArgv(prefix: string): string[];
|
|
|
226
222
|
* PATH shim as part of the install, so the next invocation genuinely resolves
|
|
227
223
|
* the new version — which is the whole point of routing here instead of npm.
|
|
228
224
|
*/
|
|
229
|
-
export declare function buildPnpmInstallArgv(): string[];
|
|
225
|
+
export declare function buildPnpmInstallArgv(install?: RunningInstall): string[];
|
|
226
|
+
/**
|
|
227
|
+
* Read the pnpm global install's current store and virtual-store locations.
|
|
228
|
+
* Newer pnpm versions can change both defaults, so a self-update must carry
|
|
229
|
+
* forward the paths recorded for the install it is replacing.
|
|
230
|
+
*/
|
|
231
|
+
export declare function pnpmInstallDirs(install: RunningInstall): {
|
|
232
|
+
storeDir: string;
|
|
233
|
+
virtualStoreDir: string;
|
|
234
|
+
} | null;
|
|
230
235
|
/** Argv for updating a Bun-managed global install. */
|
|
231
236
|
export declare function buildBunInstallArgv(): string[];
|
|
232
237
|
/**
|
|
@@ -419,6 +424,8 @@ declare function probeCliVersion(bin: string): string | null;
|
|
|
419
424
|
* here may crash or block the CLI.
|
|
420
425
|
*/
|
|
421
426
|
export declare function checkUpdateConvergence(targetVersion: string, deps?: {
|
|
427
|
+
requireEvidence?: boolean;
|
|
428
|
+
exactVersion?: boolean;
|
|
422
429
|
resolveBin?: () => string | null;
|
|
423
430
|
probeVersion?: (bin: string) => string | null;
|
|
424
431
|
currentInstall?: () => RunningInstall;
|
|
@@ -35,6 +35,7 @@ import os from "node:os";
|
|
|
35
35
|
import path from "node:path";
|
|
36
36
|
import { fileURLToPath } from "node:url";
|
|
37
37
|
import chalk from "chalk";
|
|
38
|
+
import * as yaml from "js-yaml";
|
|
38
39
|
import semver from "semver";
|
|
39
40
|
import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
|
|
40
41
|
import { isVersionRequest } from "../version-request.js";
|
|
@@ -42,6 +43,8 @@ import { Sentry } from "../sentry.js";
|
|
|
42
43
|
import { isCliTelemetryDisabled } from "./cli-telemetry.js";
|
|
43
44
|
import { DEFAULT_VAULT_API_URL } from "./cognito-session.js";
|
|
44
45
|
import { acquireUpdateLock, } from "./update-lock.js";
|
|
46
|
+
/** Prevent an update package manager's lifecycle child from starting another updater. */
|
|
47
|
+
export const UPDATE_RECURSION_GUARD_ENV = "HQ_RESCUE_SELF_UPDATED";
|
|
45
48
|
const CLIENT_ID = "hq-cli";
|
|
46
49
|
const ENDPOINT_PATH = "/v1/client-version/check";
|
|
47
50
|
const FETCH_TIMEOUT_MS = 3_000;
|
|
@@ -572,27 +575,45 @@ export function derivePnpmHome(install) {
|
|
|
572
575
|
return home || null;
|
|
573
576
|
}
|
|
574
577
|
/**
|
|
575
|
-
* Environment for the pnpm self-update spawn.
|
|
576
|
-
*
|
|
577
|
-
* `
|
|
578
|
-
*
|
|
579
|
-
* cron, or any non-login shell that never sourced the profile exporting
|
|
580
|
-
* `PNPM_HOME`). When the running install is pnpm-managed and the parent lacks
|
|
581
|
-
* `PNPM_HOME`, inject one derived from the install's own path so the update can
|
|
582
|
-
* find the global bin dir it is about to rewrite.
|
|
578
|
+
* Environment for the pnpm self-update spawn. pnpm requires its configured
|
|
579
|
+
* global bin directory to be on PATH; pnpm 12.5.1 reports
|
|
580
|
+
* `ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH` otherwise. Minimal-environment parents
|
|
581
|
+
* may also lack PNPM_HOME, so derive it from the running install when needed.
|
|
583
582
|
*
|
|
584
|
-
* A no-op for npm/bun installs
|
|
585
|
-
* `
|
|
586
|
-
* when nothing needs injecting, so callers can spawn with the inherited
|
|
587
|
-
* environment unchanged (and pass no `env` at all).
|
|
583
|
+
* A no-op for npm/bun installs. Never override a caller's PNPM_HOME. Returns
|
|
584
|
+
* `undefined` only when the inherited environment already has both values.
|
|
588
585
|
*/
|
|
589
586
|
export function pnpmUpdateEnv(install, base = process.env) {
|
|
590
|
-
if (install.manager !== "pnpm"
|
|
587
|
+
if (install.manager !== "pnpm")
|
|
591
588
|
return undefined;
|
|
592
|
-
const home = derivePnpmHome(install);
|
|
589
|
+
const home = base.PNPM_HOME || derivePnpmHome(install);
|
|
593
590
|
if (!home)
|
|
594
591
|
return undefined;
|
|
595
|
-
|
|
592
|
+
const binDir = path.join(home, "bin");
|
|
593
|
+
const pathKey = Object.keys(base).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
594
|
+
const currentPath = base[pathKey] ?? "";
|
|
595
|
+
const normalizedBinDir = path.resolve(binDir);
|
|
596
|
+
const alreadyOnPath = currentPath.split(path.delimiter).some((entry) => {
|
|
597
|
+
if (!entry)
|
|
598
|
+
return false;
|
|
599
|
+
const normalizedEntry = path.resolve(entry);
|
|
600
|
+
return process.platform === "win32"
|
|
601
|
+
? normalizedEntry.toLowerCase() === normalizedBinDir.toLowerCase()
|
|
602
|
+
: normalizedEntry === normalizedBinDir;
|
|
603
|
+
});
|
|
604
|
+
const result = {
|
|
605
|
+
...base,
|
|
606
|
+
[UPDATE_RECURSION_GUARD_ENV]: "1",
|
|
607
|
+
// Corepack otherwise honors the packageManager in the user's current
|
|
608
|
+
// project, which can silently switch the update subprocess to that version.
|
|
609
|
+
COREPACK_ENABLE_PROJECT_SPEC: "0",
|
|
610
|
+
COREPACK_DEFAULT_TO_LATEST: "0",
|
|
611
|
+
};
|
|
612
|
+
if (!base.PNPM_HOME)
|
|
613
|
+
result.PNPM_HOME = home;
|
|
614
|
+
if (!alreadyOnPath)
|
|
615
|
+
result[pathKey] = [currentPath, binDir].filter(Boolean).join(path.delimiter);
|
|
616
|
+
return Object.keys(result).some((key) => result[key] !== base[key]) ? result : undefined;
|
|
596
617
|
}
|
|
597
618
|
export function buildPrefixedInstallArgv(prefix) {
|
|
598
619
|
return ["install", "-g", "--prefix", prefix, LATEST_PACKAGE_SPEC];
|
|
@@ -602,8 +623,51 @@ export function buildPrefixedInstallArgv(prefix) {
|
|
|
602
623
|
* PATH shim as part of the install, so the next invocation genuinely resolves
|
|
603
624
|
* the new version — which is the whole point of routing here instead of npm.
|
|
604
625
|
*/
|
|
605
|
-
export function buildPnpmInstallArgv() {
|
|
606
|
-
|
|
626
|
+
export function buildPnpmInstallArgv(install) {
|
|
627
|
+
const args = ["add", "-g", LATEST_PACKAGE_SPEC];
|
|
628
|
+
const dirs = install ? pnpmInstallDirs(install) : null;
|
|
629
|
+
if (dirs)
|
|
630
|
+
args.push("--store-dir", dirs.storeDir, "--virtual-store-dir", dirs.virtualStoreDir);
|
|
631
|
+
return args;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Read the pnpm global install's current store and virtual-store locations.
|
|
635
|
+
* Newer pnpm versions can change both defaults, so a self-update must carry
|
|
636
|
+
* forward the paths recorded for the install it is replacing.
|
|
637
|
+
*/
|
|
638
|
+
export function pnpmInstallDirs(install) {
|
|
639
|
+
if (install.manager !== "pnpm" || !install.packageRoot)
|
|
640
|
+
return null;
|
|
641
|
+
const normalized = install.packageRoot.replace(/\\/g, "/");
|
|
642
|
+
const global = normalized.match(/^(.*\/global\/\d+\/node_modules)(?:\/|$)|^(.*\/global\/v\d+\/[^/]+\/node_modules)(?:\/|$)/);
|
|
643
|
+
const globalNodeModules = global?.[1] ?? global?.[2];
|
|
644
|
+
if (!globalNodeModules)
|
|
645
|
+
return null;
|
|
646
|
+
const metadataPath = path.join(globalNodeModules, ".modules.yaml");
|
|
647
|
+
try {
|
|
648
|
+
const metadata = yaml.load(readFileSync(metadataPath, "utf8"));
|
|
649
|
+
const storeDir = typeof metadata?.storeDir === "string" && metadata.storeDir
|
|
650
|
+
? metadata.storeDir
|
|
651
|
+
: path.join(derivePnpmHome(install) ?? path.dirname(path.dirname(globalNodeModules)), "store", "v11");
|
|
652
|
+
const configuredVirtualStore = typeof metadata?.virtualStoreDir === "string"
|
|
653
|
+
? metadata.virtualStoreDir
|
|
654
|
+
: ".pnpm";
|
|
655
|
+
const virtualStoreDir = path.isAbsolute(configuredVirtualStore)
|
|
656
|
+
? configuredVirtualStore
|
|
657
|
+
: path.resolve(globalNodeModules, configuredVirtualStore);
|
|
658
|
+
return { storeDir, virtualStoreDir };
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
// The metadata file is absent in older pnpm global layouts. The global
|
|
662
|
+
// install root still determines its virtual store and default PNPM_HOME.
|
|
663
|
+
const pnpmHome = derivePnpmHome(install);
|
|
664
|
+
if (!pnpmHome)
|
|
665
|
+
return null;
|
|
666
|
+
return {
|
|
667
|
+
storeDir: path.join(pnpmHome, "store", "v11"),
|
|
668
|
+
virtualStoreDir: path.join(globalNodeModules, ".pnpm"),
|
|
669
|
+
};
|
|
670
|
+
}
|
|
607
671
|
}
|
|
608
672
|
/** Argv for updating a Bun-managed global install. */
|
|
609
673
|
export function buildBunInstallArgv() {
|
|
@@ -1078,7 +1142,7 @@ function performUpdate(command, runner = runUpdateCommand) {
|
|
|
1078
1142
|
*/
|
|
1079
1143
|
function manualUpdateCommand(install, decision) {
|
|
1080
1144
|
if (install.manager === "pnpm")
|
|
1081
|
-
return `pnpm ${buildPnpmInstallArgv().join(" ")}`;
|
|
1145
|
+
return `pnpm ${buildPnpmInstallArgv(install).join(" ")}`;
|
|
1082
1146
|
if (install.manager === "bun")
|
|
1083
1147
|
return `bun ${buildBunInstallArgv().join(" ")}`;
|
|
1084
1148
|
if (install.prefix)
|
|
@@ -1112,7 +1176,7 @@ function nudgeUpdateRecommended(decision, install = resolveRunningInstall()) {
|
|
|
1112
1176
|
return;
|
|
1113
1177
|
}
|
|
1114
1178
|
const command = install.manager === "pnpm"
|
|
1115
|
-
? `pnpm ${buildPnpmInstallArgv().join(" ")}`
|
|
1179
|
+
? `pnpm ${buildPnpmInstallArgv(install).join(" ")}`
|
|
1116
1180
|
: install.manager === "bun"
|
|
1117
1181
|
? `bun ${buildBunInstallArgv().join(" ")}`
|
|
1118
1182
|
: decision.updateCommand;
|
|
@@ -1193,7 +1257,8 @@ export function parseReportedCliVersion(stdout) {
|
|
|
1193
1257
|
/** `<bin> --version` output (trimmed), or null on any failure/timeout. */
|
|
1194
1258
|
function probeCliVersion(bin) {
|
|
1195
1259
|
try {
|
|
1196
|
-
const
|
|
1260
|
+
const isJavaScriptEntry = path.extname(bin).toLowerCase() === ".js";
|
|
1261
|
+
const result = spawnSync(isJavaScriptEntry ? process.execPath : bin, isJavaScriptEntry ? [bin, "--version"] : ["--version"], {
|
|
1197
1262
|
encoding: "utf-8",
|
|
1198
1263
|
timeout: CONVERGENCE_TIMEOUT_MS,
|
|
1199
1264
|
});
|
|
@@ -1224,19 +1289,30 @@ function probeCliVersion(bin) {
|
|
|
1224
1289
|
*/
|
|
1225
1290
|
export function checkUpdateConvergence(targetVersion, deps = {}) {
|
|
1226
1291
|
try {
|
|
1227
|
-
|
|
1292
|
+
let bin;
|
|
1293
|
+
try {
|
|
1294
|
+
bin = (deps.resolveBin ?? resolveHqOnPath)();
|
|
1295
|
+
}
|
|
1296
|
+
catch {
|
|
1297
|
+
bin = null;
|
|
1298
|
+
}
|
|
1228
1299
|
if (!bin) {
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1300
|
+
const install = (deps.currentInstall ?? resolveRunningInstall)();
|
|
1301
|
+
bin = installedHqEntryPoint(install);
|
|
1302
|
+
if (!bin) {
|
|
1303
|
+
console.error(chalk.yellow("⚠ Updated, but couldn't resolve `hq` on PATH or from the install to verify the new version took effect."));
|
|
1304
|
+
return !deps.requireEvidence;
|
|
1305
|
+
}
|
|
1232
1306
|
}
|
|
1233
1307
|
const reported = (deps.probeVersion ?? probeCliVersion)(bin);
|
|
1234
1308
|
if (!reported) {
|
|
1235
1309
|
console.error(chalk.yellow(`⚠ Updated, but \`${bin} --version\` did not respond — couldn't verify the new version took effect.`));
|
|
1236
|
-
return
|
|
1310
|
+
return !deps.requireEvidence; // unverifiable, see above
|
|
1237
1311
|
}
|
|
1238
1312
|
if (reported === targetVersion)
|
|
1239
1313
|
return true; // converged — the normal case
|
|
1314
|
+
if (deps.exactVersion)
|
|
1315
|
+
return false;
|
|
1240
1316
|
//
|
|
1241
1317
|
// The install runs `@latest`, not `@<targetVersion>`, so it can legitimately
|
|
1242
1318
|
// land a version NEWER than the `latestVersion` the gate was handed: the npm
|
|
@@ -1284,8 +1360,23 @@ export function checkUpdateConvergence(targetVersion, deps = {}) {
|
|
|
1284
1360
|
return false;
|
|
1285
1361
|
}
|
|
1286
1362
|
catch {
|
|
1287
|
-
//
|
|
1288
|
-
|
|
1363
|
+
// Keep the current fail-closed behavior for a thrown version probe: an
|
|
1364
|
+
// exception may mean the newly-written executable cannot be loaded.
|
|
1365
|
+
return !deps.requireEvidence;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function installedHqEntryPoint(install) {
|
|
1369
|
+
if (!install.packageRoot)
|
|
1370
|
+
return null;
|
|
1371
|
+
try {
|
|
1372
|
+
const manifest = JSON.parse(readFileSync(path.join(install.packageRoot, "package.json"), "utf8"));
|
|
1373
|
+
const entry = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.hq;
|
|
1374
|
+
return typeof entry === "string" && entry
|
|
1375
|
+
? path.resolve(install.packageRoot, entry)
|
|
1376
|
+
: null;
|
|
1377
|
+
}
|
|
1378
|
+
catch {
|
|
1379
|
+
return null;
|
|
1289
1380
|
}
|
|
1290
1381
|
}
|
|
1291
1382
|
/**
|
|
@@ -1404,7 +1495,7 @@ function attemptRequiredUpdate(decision, deps, install) {
|
|
|
1404
1495
|
let primaryArgs;
|
|
1405
1496
|
if (install.manager === "pnpm") {
|
|
1406
1497
|
primaryCmd = "pnpm";
|
|
1407
|
-
primaryArgs = buildPnpmInstallArgv();
|
|
1498
|
+
primaryArgs = buildPnpmInstallArgv(install);
|
|
1408
1499
|
}
|
|
1409
1500
|
else if (install.manager === "bun") {
|
|
1410
1501
|
primaryCmd = "bun";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.34",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
49
49
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
50
|
+
"@aws-sdk/dsql-signer": "^3.1146.0",
|
|
50
51
|
"@indigoai-us/hq-cloud": "~6.18.33",
|
|
51
52
|
"@indigoai-us/hq-flags-client": "^0.1.2",
|
|
52
53
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"js-yaml": "^4.1.0",
|
|
60
61
|
"mqtt": "5.15.2",
|
|
61
62
|
"open": "^10.1.0",
|
|
63
|
+
"pg": "^8.23.1",
|
|
62
64
|
"semver": "^7.8.5",
|
|
63
65
|
"simple-git": "^3.27.0",
|
|
64
66
|
"smol-toml": "1.6.1",
|
|
@@ -69,6 +71,7 @@
|
|
|
69
71
|
"@types/better-sqlite3": "^7.6.13",
|
|
70
72
|
"@types/js-yaml": "^4.0.9",
|
|
71
73
|
"@types/node": "^22.0.0",
|
|
74
|
+
"@types/pg": "^8.23.1",
|
|
72
75
|
"@types/semver": "^7.8.0",
|
|
73
76
|
"@vitest/coverage-v8": "4.1.6",
|
|
74
77
|
"aedes": "1.1.2",
|