@indigoai-us/hq-cli 5.304.0 → 5.306.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 +16 -0
- package/dist/command-catalog.generated.d.ts +3 -3
- package/dist/command-catalog.generated.js +5 -3
- package/dist/command-registration-plan.js +8 -0
- package/dist/commands/daemon.js +24 -23
- package/dist/commands/lanes.js +13 -0
- package/dist/commands/mcp-status.js +1 -0
- package/dist/commands/members.js +0 -1
- package/dist/commands/people.d.ts +3 -0
- package/dist/commands/people.js +27 -3
- package/dist/index.js +6 -1
- package/dist/lazy-commands.js +5 -2
- package/dist/lib/aws-sdk-node-warning-output.d.ts +12 -0
- package/dist/lib/aws-sdk-node-warning-output.js +22 -0
- package/dist/lib/company-errors-sdk-warning-flag.d.ts +5 -0
- package/dist/lib/company-errors-sdk-warning-flag.js +8 -0
- package/dist/lib/daemon/catalog.js +6 -2
- package/dist/lib/daemon/remote-control.d.ts +19 -8
- package/dist/lib/daemon/remote-control.js +62 -12
- package/dist/lib/daemon/supervisor.d.ts +2 -0
- package/dist/lib/daemon/supervisor.js +11 -0
- package/dist/lib/doctor/checks/work-context.js +2 -2
- package/dist/lib/lanes/keep-alive.js +13 -2
- package/dist/lib/mesh/live/daemon/doctor.js +2 -2
- package/dist/lib/mesh/live/flush.d.ts +2 -2
- package/dist/lib/mesh/live/flush.js +49 -29
- package/dist/lib/mesh/live/spool.d.ts +6 -0
- package/dist/lib/mesh/live/spool.js +203 -4
- package/dist/lib/work-context/bind-best.js +13 -1
- package/dist/main.js +16 -3
- package/dist/startup-registration.js +9 -3
- package/dist/utils/install-tree-torn.d.ts +13 -6
- package/dist/utils/install-tree-torn.js +42 -16
- package/dist/utils/self-update.d.ts +4 -0
- package/dist/utils/self-update.js +46 -9
- package/dist/utils/sentry-fingerprint.js +1 -0
- package/dist/utils/version-check.d.ts +7 -0
- package/dist/utils/version-check.js +30 -1
- package/dist/utils/version-gate.d.ts +28 -10
- package/dist/utils/version-gate.js +159 -29
- package/dist/utils/version-report.d.ts +2 -1
- package/dist/utils/version-report.js +8 -1
- package/package.json +1 -1
- package/dist/lib/work-context/index.d.ts +0 -26
- package/dist/lib/work-context/index.js +0 -22
|
@@ -700,7 +700,7 @@ function retiredSiblingActivitySignature(packageRoot, fs) {
|
|
|
700
700
|
for (const sibling of siblings) {
|
|
701
701
|
visit(path.join(parent, sibling), sibling, 0);
|
|
702
702
|
}
|
|
703
|
-
return { value: JSON.stringify(snapshots), observed };
|
|
703
|
+
return { value: JSON.stringify(snapshots), observed, present: siblings.length > 0 };
|
|
704
704
|
}
|
|
705
705
|
/** Whether packageRoot's own manifest is healthy (`name === CLI_NAME`). */
|
|
706
706
|
function packageRootHealthy(packageRoot, fs) {
|
|
@@ -787,6 +787,8 @@ function installTreeActivitySignature(target, packageRoot, fs) {
|
|
|
787
787
|
const activity = installTreeActivityPaths(target, packageRoot);
|
|
788
788
|
const snapshots = [];
|
|
789
789
|
let observed = true;
|
|
790
|
+
let nonRetiredObserved = true;
|
|
791
|
+
let retiredSiblingPresent = null;
|
|
790
792
|
const errorCode = (err) => {
|
|
791
793
|
if (err !== null && typeof err === "object" && "code" in err && typeof err.code === "string") {
|
|
792
794
|
return err.code;
|
|
@@ -809,6 +811,7 @@ function installTreeActivitySignature(target, packageRoot, fs) {
|
|
|
809
811
|
}
|
|
810
812
|
catch (err) {
|
|
811
813
|
observed = false;
|
|
814
|
+
nonRetiredObserved = false;
|
|
812
815
|
snapshots.push([candidate, `unreadable:${errorCode(err)}`]);
|
|
813
816
|
}
|
|
814
817
|
}
|
|
@@ -820,6 +823,7 @@ function installTreeActivitySignature(target, packageRoot, fs) {
|
|
|
820
823
|
}
|
|
821
824
|
catch (err) {
|
|
822
825
|
observed = false;
|
|
826
|
+
nonRetiredObserved = false;
|
|
823
827
|
snapshots.push([directory, `entries-unreadable:${errorCode(err)}`]);
|
|
824
828
|
}
|
|
825
829
|
}
|
|
@@ -827,6 +831,7 @@ function installTreeActivitySignature(target, packageRoot, fs) {
|
|
|
827
831
|
try {
|
|
828
832
|
const retired = retiredSiblingActivitySignature(packageRoot, fs);
|
|
829
833
|
snapshots.push(["retired-siblings", retired.value]);
|
|
834
|
+
retiredSiblingPresent = retired.present;
|
|
830
835
|
if (!retired.observed)
|
|
831
836
|
observed = false;
|
|
832
837
|
}
|
|
@@ -835,7 +840,16 @@ function installTreeActivitySignature(target, packageRoot, fs) {
|
|
|
835
840
|
snapshots.push(["retired-siblings-unreadable", errorCode(err)]);
|
|
836
841
|
}
|
|
837
842
|
}
|
|
838
|
-
return { value: JSON.stringify(snapshots), observed };
|
|
843
|
+
return { value: JSON.stringify(snapshots), observed, nonRetiredObserved, retiredSiblingPresent };
|
|
844
|
+
}
|
|
845
|
+
function canProbeUnsettledRetiredSibling(snapshot) {
|
|
846
|
+
// A very large or unreadable retired npm tree can exceed the bounded recursive
|
|
847
|
+
// activity scan. Its stable partial signature is still useful when the live
|
|
848
|
+
// install paths are observable and npm's named retired sibling is present.
|
|
849
|
+
// This only selects the existing stale-tree repair outcome; it never removes
|
|
850
|
+
// files, and any observed signature change still suppresses stale classification.
|
|
851
|
+
return snapshot.observed ||
|
|
852
|
+
(snapshot.nonRetiredObserved && snapshot.retiredSiblingPresent === true);
|
|
839
853
|
}
|
|
840
854
|
export const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1_500;
|
|
841
855
|
/**
|
|
@@ -850,11 +864,14 @@ export const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1_500;
|
|
|
850
864
|
* app, the box's update timer, another `hq`) cannot make a legitimately-in-progress
|
|
851
865
|
* reinstall look stale:
|
|
852
866
|
* - UNLOCKED time — wall time observed with no fresh foreign lock — is bounded
|
|
853
|
-
* by `deadlineMs`. If the target is still missing and the
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
867
|
+
* by `deadlineMs`. If the target is still missing and the observable tree
|
|
868
|
+
* signature stays unchanged for `staleProbeMs`, the wait returns `staleTree`
|
|
869
|
+
* early. When only a named retired npm sibling's deep scan is incomplete,
|
|
870
|
+
* a stable partial signature ends the wait early as `retiredSiblingUnverified`
|
|
871
|
+
* instead. That outcome preserves uncertainty and tells the user to wait for
|
|
872
|
+
* any installer before attempting a repair. Any observed signature change
|
|
873
|
+
* keeps the ordinary settle deadline in force. `deadlineMs === 0` evaluates
|
|
874
|
+
* readiness once.
|
|
858
875
|
* - LOCKED time — wall time observed under a fresh foreign lock held by a live
|
|
859
876
|
* pid — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
|
|
860
877
|
* 10-minute staleness window) and does NOT consume the settle budget. When
|
|
@@ -882,7 +899,7 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
882
899
|
const staleProbeMs = Math.max(0, args.staleProbeMs ?? DEFAULT_INSTALL_TREE_STALE_PROBE_MS);
|
|
883
900
|
const start = now();
|
|
884
901
|
let lastActivitySnapshot = installTreeActivitySignature(args.target, args.packageRoot, fs);
|
|
885
|
-
let staleObservationStartedAt = lastActivitySnapshot
|
|
902
|
+
let staleObservationStartedAt = canProbeUnsettledRetiredSibling(lastActivitySnapshot) ? start : null;
|
|
886
903
|
let sawTreeChange = false;
|
|
887
904
|
let readySince = null;
|
|
888
905
|
let sawLock = false;
|
|
@@ -909,16 +926,18 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
909
926
|
}
|
|
910
927
|
if (!sawTreeChange) {
|
|
911
928
|
const activitySnapshot = installTreeActivitySignature(args.target, args.packageRoot, fs);
|
|
912
|
-
|
|
929
|
+
const previousCanProbe = canProbeUnsettledRetiredSibling(lastActivitySnapshot);
|
|
930
|
+
const currentCanProbe = canProbeUnsettledRetiredSibling(activitySnapshot);
|
|
931
|
+
if (previousCanProbe && currentCanProbe && activitySnapshot.value !== lastActivitySnapshot.value) {
|
|
932
|
+
sawTreeChange = true;
|
|
913
933
|
staleObservationStartedAt = null;
|
|
914
934
|
}
|
|
915
|
-
else if (!
|
|
916
|
-
staleObservationStartedAt = t;
|
|
917
|
-
}
|
|
918
|
-
else if (activitySnapshot.value !== lastActivitySnapshot.value) {
|
|
919
|
-
sawTreeChange = true;
|
|
935
|
+
else if (!currentCanProbe) {
|
|
920
936
|
staleObservationStartedAt = null;
|
|
921
937
|
}
|
|
938
|
+
else if (!previousCanProbe) {
|
|
939
|
+
staleObservationStartedAt = t;
|
|
940
|
+
}
|
|
922
941
|
lastActivitySnapshot = activitySnapshot;
|
|
923
942
|
}
|
|
924
943
|
let retiredPresent = null;
|
|
@@ -985,8 +1004,9 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
985
1004
|
lockedMs,
|
|
986
1005
|
lockTool,
|
|
987
1006
|
sawTreeChange,
|
|
988
|
-
staleTree:
|
|
1007
|
+
staleTree: lastActivitySnapshot.observed,
|
|
989
1008
|
retiredSiblingPresent: retiredPresent,
|
|
1009
|
+
retiredSiblingUnverified: !lastActivitySnapshot.observed && lastActivitySnapshot.retiredSiblingPresent === true,
|
|
990
1010
|
};
|
|
991
1011
|
}
|
|
992
1012
|
// Give up when the TREE has had a full unlocked budget with no writer visible,
|
|
@@ -1176,6 +1196,10 @@ export const INSTALL_TREE_STALE_REMEDY = "hq-cli's install tree is incomplete an
|
|
|
1176
1196
|
"Reinstall with `npm install -g @indigoai-us/hq-cli@latest` " +
|
|
1177
1197
|
"(or `pnpm add -g @indigoai-us/hq-cli@latest` / `bun add -g @indigoai-us/hq-cli@latest` " +
|
|
1178
1198
|
"when that manager owns your install), then run the command again.";
|
|
1199
|
+
/** Guidance when a retired package sibling's activity could not be verified. */
|
|
1200
|
+
export const INSTALL_TREE_RETIRED_UNVERIFIED_REMEDY = "hq-cli found a retired install sibling but could not verify whether an installer is still using it. Wait for any npm or desktop CLI update to finish, then run the command again. If no install is running, repair with `npm install -g @indigoai-us/hq-cli@latest` " +
|
|
1201
|
+
"(or `pnpm add -g @indigoai-us/hq-cli@latest` / `bun add -g @indigoai-us/hq-cli@latest` " +
|
|
1202
|
+
"when that manager owns your install).";
|
|
1179
1203
|
/**
|
|
1180
1204
|
* The fixed, input-free remedy for the 'lock-held' outcome: a cooperating writer
|
|
1181
1205
|
* held the shared update lock past its own staleness ceiling. Telling the user to
|
|
@@ -1198,7 +1222,9 @@ export function installTreeTornStderrLine(err) {
|
|
|
1198
1222
|
? INSTALL_TREE_LOCK_HELD_REMEDY
|
|
1199
1223
|
: err.diagnostics.outcome === "stale_tree"
|
|
1200
1224
|
? INSTALL_TREE_STALE_REMEDY
|
|
1201
|
-
:
|
|
1225
|
+
: err.diagnostics.outcome === "retired_unverified"
|
|
1226
|
+
? INSTALL_TREE_RETIRED_UNVERIFIED_REMEDY
|
|
1227
|
+
: INSTALL_TREE_TORN_REMEDY;
|
|
1202
1228
|
const specifier = err.diagnostics.specifier;
|
|
1203
1229
|
return specifier ? `${remedy} (missing: ${specifier})` : remedy;
|
|
1204
1230
|
}
|
|
@@ -137,6 +137,10 @@ export interface SelfUpdateDeps {
|
|
|
137
137
|
currentVersion?: string;
|
|
138
138
|
fetchLatest?: () => Promise<string | null>;
|
|
139
139
|
resolveInstall?: () => RunningInstall;
|
|
140
|
+
/** Permission preflight for an npm global prefix (tests inject a read-only result). */
|
|
141
|
+
canWriteNpmInstall?: (install: RunningInstall) => boolean;
|
|
142
|
+
/** Home directory used for the durable per-install permission notice marker. */
|
|
143
|
+
homeDir?: string;
|
|
140
144
|
/** Same-filesystem install rename, injectable for rollback tests. */
|
|
141
145
|
renamePath?: typeof fs.renameSync;
|
|
142
146
|
runner?: (cmd: string, args: string[], env?: NodeJS.ProcessEnv) => UpdateResult;
|
|
@@ -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, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir,
|
|
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";
|
|
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";
|
|
@@ -659,10 +659,30 @@ async function updateAndReexec(argv, flavor, known, deps) {
|
|
|
659
659
|
console.error(chalk.dim(`hq-cli ${latest} is available, but this copy is a local dependency (${install.packageRoot}) — update the project that owns it.`));
|
|
660
660
|
return { action: "skipped", latest };
|
|
661
661
|
}
|
|
662
|
-
const staged = canStageInstall(install) && (await readStagedSelfUpdateFlag(deps, env));
|
|
663
662
|
const releaseLock = flavor.lock ? (deps.acquireLock ?? acquireUpdateLock)() : () => { };
|
|
664
663
|
if (!releaseLock)
|
|
665
664
|
return { action: "skipped", latest };
|
|
665
|
+
if (install.manager === "npm" &&
|
|
666
|
+
install.prefix &&
|
|
667
|
+
install.packageRoot &&
|
|
668
|
+
!(deps.canWriteNpmInstall ?? ((resolved) => canWriteNpmInstall(resolved.prefix, resolved.packageRoot)))(install)) {
|
|
669
|
+
const claimed = flavor.noun === "daemon update"
|
|
670
|
+
? false
|
|
671
|
+
: claimNonWritableNpmPrefixNotice(install, current, deps.homeDir);
|
|
672
|
+
if (flavor.noun === "rescue" || claimed) {
|
|
673
|
+
console.error(chalk.yellow(` ${nonWritablePrefixNote(install.prefix)}`));
|
|
674
|
+
}
|
|
675
|
+
await captureNonWritableNpmPrefixNotice({
|
|
676
|
+
fromVersion: current,
|
|
677
|
+
targetVersion: latestValid,
|
|
678
|
+
install,
|
|
679
|
+
env,
|
|
680
|
+
homeDir: deps.homeDir,
|
|
681
|
+
});
|
|
682
|
+
releaseLock();
|
|
683
|
+
return { action: "update-failed", latest };
|
|
684
|
+
}
|
|
685
|
+
const staged = canStageInstall(install) && (await readStagedSelfUpdateFlag(deps, env));
|
|
666
686
|
const renamePath = deps.renamePath ?? fs.renameSync;
|
|
667
687
|
// The in-place update moves the package aside, and the installer's
|
|
668
688
|
// supervisor ships inside it, so run the installer through a copy.
|
|
@@ -732,12 +752,27 @@ async function updateAndReexec(argv, flavor, known, deps) {
|
|
|
732
752
|
console.error(chalk.yellow(`⚠ hq-cli ${latest} is available but the update failed` +
|
|
733
753
|
`${result.detail ? `: ${result.detail}` : ""}`));
|
|
734
754
|
console.error(chalk.dim(` Try manually: ${plan.cmd} ${plan.args.join(" ")}`));
|
|
735
|
-
// A
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
755
|
+
// A permission wall discovered after npm fails is handled as an expected
|
|
756
|
+
// install-state warning. The preflight normally catches it before spawn;
|
|
757
|
+
// this check covers ACL changes and other races after the preflight.
|
|
758
|
+
const npmPrefixNotWritable = install.manager === "npm" &&
|
|
759
|
+
install.prefix &&
|
|
760
|
+
install.packageRoot &&
|
|
761
|
+
!(deps.canWriteNpmInstall ?? ((resolved) => canWriteNpmInstall(resolved.prefix, resolved.packageRoot)))(install);
|
|
762
|
+
if (npmPrefixNotWritable) {
|
|
763
|
+
const prefixNoticeClaimed = flavor.noun === "daemon update"
|
|
764
|
+
? false
|
|
765
|
+
: claimNonWritableNpmPrefixNotice(install, current, deps.homeDir);
|
|
766
|
+
if (flavor.noun === "rescue" || prefixNoticeClaimed) {
|
|
767
|
+
console.error(chalk.yellow(` ${nonWritablePrefixNote(install.prefix)}`));
|
|
768
|
+
}
|
|
769
|
+
await captureNonWritableNpmPrefixNotice({
|
|
770
|
+
fromVersion: current,
|
|
771
|
+
targetVersion: latestValid,
|
|
772
|
+
install,
|
|
773
|
+
env,
|
|
774
|
+
homeDir: deps.homeDir,
|
|
775
|
+
});
|
|
741
776
|
}
|
|
742
777
|
// A manager-level failure often means the install layout itself is broken
|
|
743
778
|
// (e.g. a hand-rolled pnpm store nested inside the app's bin dir). The
|
|
@@ -748,7 +783,9 @@ async function updateAndReexec(argv, flavor, known, deps) {
|
|
|
748
783
|
console.error(chalk.dim(` Or repair the install: hq rescue`));
|
|
749
784
|
}
|
|
750
785
|
console.error(chalk.dim(` Continuing the ${flavor.noun} on ${current}.`));
|
|
751
|
-
|
|
786
|
+
if (!npmPrefixNotWritable) {
|
|
787
|
+
await captureSelfUpdateOutcome("update-failed", current, latestValid, install.manager, result, env, flavor.noun === "rescue");
|
|
788
|
+
}
|
|
752
789
|
return { action: "update-failed", latest };
|
|
753
790
|
}
|
|
754
791
|
if (installSnapshot)
|
|
@@ -189,6 +189,7 @@ const KNOWN_INSTALL_TREE_OUTCOMES = new Set([
|
|
|
189
189
|
"reexec-failed",
|
|
190
190
|
"lock-held",
|
|
191
191
|
"stale_tree",
|
|
192
|
+
"retired_unverified",
|
|
192
193
|
]);
|
|
193
194
|
function installTreeRecoveryOutcomeTag(value) {
|
|
194
195
|
return typeof value === "string" && KNOWN_INSTALL_TREE_OUTCOMES.has(value)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare const CLI_VERSION_NUDGE_FLAG = "cli.version-update-nudge";
|
|
1
2
|
declare function isKnownNoninteractiveStatusProbe(argv?: readonly string[]): boolean;
|
|
2
3
|
/**
|
|
3
4
|
* The cached npm `latest` when it is newer than the running version, else null.
|
|
@@ -21,6 +22,12 @@ export interface RefreshVersionCacheOptions {
|
|
|
21
22
|
timeoutMs?: number;
|
|
22
23
|
}
|
|
23
24
|
export declare function refreshVersionCache(options?: RefreshVersionCacheOptions): Promise<void>;
|
|
25
|
+
/** Write the optional update guidance from the existing npm version cache.
|
|
26
|
+
* This helper never performs a network request; a missing or stale cache means
|
|
27
|
+
* there is no nudge to show.
|
|
28
|
+
*/
|
|
29
|
+
export declare function printCachedVersionNudge(stderr?: Pick<NodeJS.WriteStream, "write">): Promise<void>;
|
|
30
|
+
export declare function printVersionAndNudge(stdout?: Pick<NodeJS.WriteStream, "write">, stderr?: Pick<NodeJS.WriteStream, "write">): Promise<void>;
|
|
24
31
|
export declare const __test__: {
|
|
25
32
|
CACHE_TTL_MS: number;
|
|
26
33
|
isKnownNoninteractiveStatusProbe: typeof isKnownNoninteractiveStatusProbe;
|
|
@@ -9,6 +9,7 @@ const CACHE_TTL_MS = 60 * 60 * 1000; // 1h — catch fresh releases within the h
|
|
|
9
9
|
const CACHE_TTL_JITTER_MS = 5 * 60 * 1000; // up to 5m early, to spread a fleet's refreshes off a single instant
|
|
10
10
|
const FETCH_TIMEOUT_MS = 3_000;
|
|
11
11
|
const REFRESH_LOCK_STALE_MS = 10 * 60 * 1000;
|
|
12
|
+
export const CLI_VERSION_NUDGE_FLAG = "cli.version-update-nudge";
|
|
12
13
|
/**
|
|
13
14
|
* How long to stop auto-retrying an `ineffectiveLatest` target. Long enough to
|
|
14
15
|
* break the per-invocation loop on a busy agent box, short enough that a box
|
|
@@ -67,7 +68,7 @@ function isKnownNoninteractiveStatusProbe(argv = process.argv) {
|
|
|
67
68
|
const json = args.includes("--json") || !process.stdout.isTTY;
|
|
68
69
|
if (!json)
|
|
69
70
|
return false;
|
|
70
|
-
if (positional[0] === "mcp" && positional[1] === "status")
|
|
71
|
+
if (positional[0] === "mcp" && (positional[1] === "status" || positional[1] === "list"))
|
|
71
72
|
return true;
|
|
72
73
|
if (positional[0] === "packs" && (positional[1] === "list" || positional[1] === "ls"))
|
|
73
74
|
return true;
|
|
@@ -199,6 +200,34 @@ export async function refreshVersionCache(options = {}) {
|
|
|
199
200
|
releaseLock();
|
|
200
201
|
}
|
|
201
202
|
}
|
|
203
|
+
/** Write the optional update guidance from the existing npm version cache.
|
|
204
|
+
* This helper never performs a network request; a missing or stale cache means
|
|
205
|
+
* there is no nudge to show.
|
|
206
|
+
*/
|
|
207
|
+
export async function printCachedVersionNudge(stderr = process.stderr) {
|
|
208
|
+
if (isOptedOut())
|
|
209
|
+
return;
|
|
210
|
+
let resolveProcessFlagGate;
|
|
211
|
+
try {
|
|
212
|
+
({ resolveProcessFlagGate } = await import("../lib/flag-registry.js"));
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
const errorClass = error instanceof Error ? error.name : "unknown";
|
|
216
|
+
stderr.write(`hq: Update nudge unavailable (${errorClass}).\n`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const enabled = resolveProcessFlagGate(CLI_VERSION_NUDGE_FLAG, { fallback: false }, () => false);
|
|
220
|
+
if (!enabled)
|
|
221
|
+
return;
|
|
222
|
+
const latest = staleAgainstCachedLatest();
|
|
223
|
+
if (latest) {
|
|
224
|
+
stderr.write(`Update available: ${latest}. Update with npm install -g @indigoai-us/hq-cli@latest, pnpm add -g @indigoai-us/hq-cli@latest, or bun add -g @indigoai-us/hq-cli@latest.\n`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export async function printVersionAndNudge(stdout = process.stdout, stderr = process.stderr) {
|
|
228
|
+
stdout.write(`${CLI_VERSION}\n`);
|
|
229
|
+
await printCachedVersionNudge(stderr);
|
|
230
|
+
}
|
|
202
231
|
export const __test__ = {
|
|
203
232
|
CACHE_TTL_MS,
|
|
204
233
|
isKnownNoninteractiveStatusProbe,
|
|
@@ -151,9 +151,9 @@ export declare function resolveRunningManager(): InstallManager;
|
|
|
151
151
|
/** Convenience view of {@link resolveRunningInstall} for callers needing one field. */
|
|
152
152
|
export declare function resolveRunningPrefix(): string | null;
|
|
153
153
|
/**
|
|
154
|
-
* Whether the current process can write the npm global
|
|
155
|
-
* an `npm install -g --prefix <prefix>` could actually replace the
|
|
156
|
-
* CLI, or would fail with EACCES.
|
|
154
|
+
* Whether the current process can write the npm global install targets — i.e.
|
|
155
|
+
* whether an `npm install -g --prefix <prefix>` could actually replace the
|
|
156
|
+
* installed CLI, or would fail with EACCES.
|
|
157
157
|
*
|
|
158
158
|
* The path npm rewrites is the prefix's `bin` dir (`<prefix>/bin/hq` on unix
|
|
159
159
|
* globals — the `rename /usr/bin/hq` EACCES the agent boxes hit), so that is
|
|
@@ -167,13 +167,27 @@ export declare function resolveRunningPrefix(): string | null;
|
|
|
167
167
|
*/
|
|
168
168
|
export declare function isPrefixWritable(prefix: string, access?: (target: string, mode: number) => void): boolean;
|
|
169
169
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
170
|
+
* Check the directories an npm global update must mutate before spawning npm.
|
|
171
|
+
* Npm replaces the package under node_modules and writes executable links
|
|
172
|
+
* under prefix/bin. The package's parent must be writable because npm renames
|
|
173
|
+
* the old package there.
|
|
174
|
+
*
|
|
175
|
+
* When one of those directories has not been created yet, check its nearest
|
|
176
|
+
* existing ancestor: npm can create a child only if that ancestor is writable.
|
|
175
177
|
*/
|
|
176
|
-
export declare function
|
|
178
|
+
export declare function canWriteNpmInstall(prefix: string, packageRoot: string, access?: (target: string, mode: number) => void): boolean;
|
|
179
|
+
/** Claim the one local warning/report for this installed version and prefix. */
|
|
180
|
+
export declare function claimNonWritableNpmPrefixNotice(install: Pick<RunningInstall, "manager" | "prefix" | "packageRoot">, fromVersion: string, homeDir?: string, kind?: "user" | "report"): boolean;
|
|
181
|
+
/** Emit one path-free warning event for a handled non-writable npm prefix. */
|
|
182
|
+
export declare function captureNonWritableNpmPrefixNotice(input: {
|
|
183
|
+
fromVersion: string;
|
|
184
|
+
targetVersion: string;
|
|
185
|
+
install: Pick<RunningInstall, "manager" | "prefix" | "packageRoot">;
|
|
186
|
+
env?: NodeJS.ProcessEnv;
|
|
187
|
+
homeDir?: string;
|
|
188
|
+
}): Promise<void>;
|
|
189
|
+
/** One-line repair guidance for a global npm install the current user cannot write. */
|
|
190
|
+
export declare function nonWritablePrefixNote(prefix: string, platform?: NodeJS.Platform): string;
|
|
177
191
|
/**
|
|
178
192
|
* Derive `PNPM_HOME` from a pnpm-managed install's own path. pnpm resolves its
|
|
179
193
|
* global bin directory from `PNPM_HOME` (or an explicit `global-bin-dir`), and
|
|
@@ -411,6 +425,8 @@ export declare function checkUpdateConvergence(targetVersion: string, deps?: {
|
|
|
411
425
|
interface EnforceUpdateDeps {
|
|
412
426
|
performUpdateString?: (command: string) => UpdateResult;
|
|
413
427
|
resolveInstall?: () => RunningInstall;
|
|
428
|
+
canWriteNpmInstall?: (install: RunningInstall) => boolean;
|
|
429
|
+
homeDir?: string;
|
|
414
430
|
runner?: UpdateRunner;
|
|
415
431
|
platform?: NodeJS.Platform;
|
|
416
432
|
cleanStale?: (prefix: string) => string[];
|
|
@@ -442,7 +458,7 @@ interface EnforceUpdateDeps {
|
|
|
442
458
|
* A timed-out updater is the exception: it has been forcibly reaped, so the
|
|
443
459
|
* gate returns and lets the user's command run on the current version.
|
|
444
460
|
*/
|
|
445
|
-
declare function enforceUpdateRequired(decision: VersionCheckResponse, deps?: EnforceUpdateDeps): void
|
|
461
|
+
declare function enforceUpdateRequired(decision: VersionCheckResponse, deps?: EnforceUpdateDeps): void | Promise<void>;
|
|
446
462
|
/**
|
|
447
463
|
* Public entry point. Call before commander parses argv. Blocks the CLI on
|
|
448
464
|
* network IO for up to FETCH_TIMEOUT_MS — acceptable because the alternative
|
|
@@ -484,6 +500,8 @@ export declare const __test__: {
|
|
|
484
500
|
buildPnpmInstallArgv: typeof buildPnpmInstallArgv;
|
|
485
501
|
buildPrefixedInstallArgv: typeof buildPrefixedInstallArgv;
|
|
486
502
|
buildSpawnPlan: typeof buildSpawnPlan;
|
|
503
|
+
canWriteNpmInstall: typeof canWriteNpmInstall;
|
|
504
|
+
claimNonWritableNpmPrefixNotice: typeof claimNonWritableNpmPrefixNotice;
|
|
487
505
|
cleanStalePartialInstall: typeof cleanStalePartialInstall;
|
|
488
506
|
derivePnpmHome: typeof derivePnpmHome;
|
|
489
507
|
enforceUpdateRequired: typeof enforceUpdateRequired;
|