@indigoai-us/hq-cli 5.162.0 → 5.164.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 +26 -0
- package/dist/command-catalog.generated.d.ts +188 -0
- package/dist/command-catalog.generated.js +235 -0
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +6 -1
- package/dist/commands/agent-enroll.js +30 -3
- package/dist/commands/agents.js +15 -22
- package/dist/commands/bot.js +56 -6
- package/dist/commands/daemon.d.ts +53 -0
- package/dist/commands/daemon.js +554 -0
- package/dist/commands/lanes.js +7 -0
- package/dist/commands/meetings.js +18 -16
- package/dist/commands/mesh.js +9 -0
- package/dist/lib/daemon/catalog.d.ts +85 -0
- package/dist/lib/daemon/catalog.js +274 -0
- package/dist/lib/daemon/config.d.ts +26 -0
- package/dist/lib/daemon/config.js +85 -0
- package/dist/lib/daemon/handoff.d.ts +53 -0
- package/dist/lib/daemon/handoff.js +136 -0
- package/dist/lib/daemon/install.d.ts +76 -0
- package/dist/lib/daemon/install.js +299 -0
- package/dist/lib/daemon/legacy.d.ts +35 -0
- package/dist/lib/daemon/legacy.js +120 -0
- package/dist/lib/daemon/lock.d.ts +47 -0
- package/dist/lib/daemon/lock.js +138 -0
- package/dist/lib/daemon/machine-type.d.ts +48 -0
- package/dist/lib/daemon/machine-type.js +139 -0
- package/dist/lib/daemon/migrate.d.ts +61 -0
- package/dist/lib/daemon/migrate.js +238 -0
- package/dist/lib/daemon/paths.d.ts +22 -0
- package/dist/lib/daemon/paths.js +42 -0
- package/dist/lib/daemon/remote-control.d.ts +35 -0
- package/dist/lib/daemon/remote-control.js +47 -0
- package/dist/lib/daemon/requests.d.ts +24 -0
- package/dist/lib/daemon/requests.js +63 -0
- package/dist/lib/daemon/run.d.ts +41 -0
- package/dist/lib/daemon/run.js +319 -0
- package/dist/lib/daemon/supervisor.d.ts +87 -0
- package/dist/lib/daemon/supervisor.js +523 -0
- package/dist/lib/daemon/updater.d.ts +39 -0
- package/dist/lib/daemon/updater.js +103 -0
- package/dist/lib/flag-registry.d.ts +7 -1
- package/dist/lib/flag-registry.js +8 -0
- package/dist/lib/mesh/live/backoff.d.ts +2 -2
- package/dist/lib/mesh/live/backoff.js +16 -3
- package/dist/lib/mesh/live/daemon/run.d.ts +2 -2
- package/dist/lib/mesh/live/daemon/run.js +5 -2
- package/dist/lib/mesh/live/flush.d.ts +3 -2
- package/dist/lib/mesh/live/flush.js +34 -7
- package/dist/lib/mesh/live/session-events-client.d.ts +2 -0
- package/dist/lib/mesh/live/session-events-client.js +25 -0
- package/dist/lib/service-manager/systemd.d.ts +2 -2
- package/dist/lib/service-manager/systemd.js +1 -1
- package/dist/lib/service-manager/types.d.ts +2 -0
- package/dist/main.d.ts +6 -0
- package/dist/main.js +53 -13
- package/dist/sentry.js +5 -1
- package/dist/startup-registration.d.ts +3 -1
- package/dist/startup-registration.js +25 -4
- package/dist/utils/cognito-session.d.ts +4 -0
- package/dist/utils/cognito-session.js +11 -10
- package/dist/utils/install-tree-torn.d.ts +29 -10
- package/dist/utils/install-tree-torn.js +262 -29
- package/dist/utils/plan-gate-error.d.ts +27 -13
- package/dist/utils/plan-gate-error.js +133 -58
- package/dist/utils/self-update.d.ts +9 -0
- package/dist/utils/self-update.js +15 -2
- package/dist/utils/sentry-fingerprint.d.ts +7 -1
- package/dist/utils/sentry-fingerprint.js +95 -15
- package/package.json +1 -1
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* are only READ (their exported symbols), never modified.
|
|
31
31
|
*/
|
|
32
32
|
import * as nodeFs from "node:fs";
|
|
33
|
+
import { homedir } from "node:os";
|
|
33
34
|
import * as path from "node:path";
|
|
34
35
|
import { fileURLToPath } from "node:url";
|
|
35
36
|
import { CLI_NAME } from "../cli-version.js";
|
|
@@ -637,6 +638,125 @@ function packageRootHealthy(packageRoot, fs) {
|
|
|
637
638
|
const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8"));
|
|
638
639
|
return manifest.name === CLI_NAME;
|
|
639
640
|
}
|
|
641
|
+
function isWithinPath(candidate, root) {
|
|
642
|
+
const relative = path.relative(root, candidate);
|
|
643
|
+
return relative === "" ||
|
|
644
|
+
(!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`));
|
|
645
|
+
}
|
|
646
|
+
/** Paths whose presence, metadata, or immediate entries show activity for this import. */
|
|
647
|
+
function installTreeActivityPaths(target, packageRoot) {
|
|
648
|
+
const root = packageRoot ? path.resolve(packageRoot) : null;
|
|
649
|
+
const paths = new Set();
|
|
650
|
+
const directories = new Set();
|
|
651
|
+
const addPath = (candidate) => {
|
|
652
|
+
const resolved = path.resolve(candidate);
|
|
653
|
+
paths.add(resolved);
|
|
654
|
+
directories.add(path.dirname(resolved));
|
|
655
|
+
if (!root || !isWithinPath(resolved, root))
|
|
656
|
+
return;
|
|
657
|
+
let current = path.dirname(resolved);
|
|
658
|
+
for (;;) {
|
|
659
|
+
paths.add(current);
|
|
660
|
+
directories.add(current);
|
|
661
|
+
if (current === root)
|
|
662
|
+
break;
|
|
663
|
+
const parent = path.dirname(current);
|
|
664
|
+
if (parent === current || !isWithinPath(parent, root))
|
|
665
|
+
break;
|
|
666
|
+
current = parent;
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
if (root) {
|
|
670
|
+
for (const candidate of [
|
|
671
|
+
root,
|
|
672
|
+
path.join(root, "package.json"),
|
|
673
|
+
path.join(root, "dist"),
|
|
674
|
+
path.join(root, "node_modules"),
|
|
675
|
+
]) {
|
|
676
|
+
addPath(candidate);
|
|
677
|
+
directories.add(candidate);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (target.kind === "path" || target.kind === "exists") {
|
|
681
|
+
addPath(target.path);
|
|
682
|
+
}
|
|
683
|
+
else if (target.kind === "relative") {
|
|
684
|
+
const base = path.resolve(target.from, target.specifier);
|
|
685
|
+
const candidates = [
|
|
686
|
+
base,
|
|
687
|
+
...CJS_FILE_EXTENSIONS.map((ext) => base + ext),
|
|
688
|
+
...CJS_FILE_EXTENSIONS.map((ext) => path.join(base, `index${ext}`)),
|
|
689
|
+
];
|
|
690
|
+
for (const candidate of candidates)
|
|
691
|
+
addPath(candidate);
|
|
692
|
+
}
|
|
693
|
+
else if (target.kind === "package") {
|
|
694
|
+
let from = path.resolve(target.from);
|
|
695
|
+
let ancestorCount = 0;
|
|
696
|
+
for (;;) {
|
|
697
|
+
if (root && !isWithinPath(from, root))
|
|
698
|
+
break;
|
|
699
|
+
addPath(path.join(from, "node_modules", target.name));
|
|
700
|
+
if (from === root)
|
|
701
|
+
break;
|
|
702
|
+
ancestorCount += 1;
|
|
703
|
+
if (!root && ancestorCount >= 12)
|
|
704
|
+
break;
|
|
705
|
+
const parent = path.dirname(from);
|
|
706
|
+
if (parent === from)
|
|
707
|
+
break;
|
|
708
|
+
from = parent;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return {
|
|
712
|
+
paths: [...paths].sort(),
|
|
713
|
+
directories: [...directories].sort(),
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
/** A local-only snapshot of the missing target and directories an installer may change. */
|
|
717
|
+
function installTreeActivitySignature(target, packageRoot, fs) {
|
|
718
|
+
const activity = installTreeActivityPaths(target, packageRoot);
|
|
719
|
+
const snapshots = [];
|
|
720
|
+
let observed = true;
|
|
721
|
+
const errorCode = (err) => {
|
|
722
|
+
if (err !== null && typeof err === "object" && "code" in err && typeof err.code === "string") {
|
|
723
|
+
return err.code;
|
|
724
|
+
}
|
|
725
|
+
return "error";
|
|
726
|
+
};
|
|
727
|
+
for (const candidate of activity.paths) {
|
|
728
|
+
try {
|
|
729
|
+
if (!fs.existsSync(candidate)) {
|
|
730
|
+
snapshots.push([candidate, "missing"]);
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const stat = fs.statSync(candidate);
|
|
734
|
+
snapshots.push([
|
|
735
|
+
candidate,
|
|
736
|
+
stat.isDirectory() ? "directory" : "file",
|
|
737
|
+
stat.size ?? null,
|
|
738
|
+
stat.mtimeMs ?? null,
|
|
739
|
+
]);
|
|
740
|
+
}
|
|
741
|
+
catch (err) {
|
|
742
|
+
observed = false;
|
|
743
|
+
snapshots.push([candidate, `unreadable:${errorCode(err)}`]);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (const directory of activity.directories) {
|
|
747
|
+
try {
|
|
748
|
+
if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory())
|
|
749
|
+
continue;
|
|
750
|
+
snapshots.push([directory, "entries", fs.readdirSync(directory).sort()]);
|
|
751
|
+
}
|
|
752
|
+
catch (err) {
|
|
753
|
+
observed = false;
|
|
754
|
+
snapshots.push([directory, `entries-unreadable:${errorCode(err)}`]);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return { value: JSON.stringify(snapshots), observed };
|
|
758
|
+
}
|
|
759
|
+
export const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1_500;
|
|
640
760
|
/**
|
|
641
761
|
* Poll until the install tree has been continuously READY for `quietMs`, or the
|
|
642
762
|
* wait exhausts its bounds. READY means: (i) no FRESH update lock held by another
|
|
@@ -645,14 +765,14 @@ function packageRootHealthy(packageRoot, fs) {
|
|
|
645
765
|
* `.<leaf>-<rand>` retired sibling remains beside it. Every filesystem error makes
|
|
646
766
|
* its condition "not ready" rather than throwing.
|
|
647
767
|
*
|
|
648
|
-
* The wait is LOCK-AWARE and bounded
|
|
649
|
-
*
|
|
650
|
-
*
|
|
768
|
+
* The wait is LOCK-AWARE and bounded, so a live cooperating writer (the desktop
|
|
769
|
+
* app, the box's update timer, another `hq`) cannot make a legitimately-in-progress
|
|
770
|
+
* reinstall look stale:
|
|
651
771
|
* - UNLOCKED time — wall time observed with no fresh foreign lock — is bounded
|
|
652
|
-
* by `deadlineMs`.
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
772
|
+
* by `deadlineMs`. If the target is still missing, no lock or retired sibling
|
|
773
|
+
* is present, and the observed tree stays unchanged for `staleProbeMs`, the
|
|
774
|
+
* wait returns `staleTree` early. Any observed tree change keeps the ordinary
|
|
775
|
+
* settle deadline in force. `deadlineMs === 0` evaluates readiness once.
|
|
656
776
|
* - LOCKED time — wall time observed under a fresh foreign lock held by a live
|
|
657
777
|
* pid — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
|
|
658
778
|
* 10-minute staleness window) and does NOT consume the settle budget. When
|
|
@@ -665,7 +785,8 @@ function packageRootHealthy(packageRoot, fs) {
|
|
|
665
785
|
* began, so a starved event loop only makes the wait coarser, never unbounded. A
|
|
666
786
|
* stale lock (dead pid, older than the staleness window, unparseable) or our own
|
|
667
787
|
* pid never extends anything. The whole wait is bounded (≤ `deadlineMs +
|
|
668
|
-
* lockWaitMs`),
|
|
788
|
+
* lockWaitMs`), and the unchanged-tree path normally returns after the much
|
|
789
|
+
* shorter `staleProbeMs` observation window.
|
|
669
790
|
*/
|
|
670
791
|
export async function waitForInstallTreeSettled(args) {
|
|
671
792
|
const now = args.now ?? Date.now;
|
|
@@ -676,7 +797,11 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
676
797
|
const quietMs = args.quietMs ?? 1500;
|
|
677
798
|
const deadlineMs = args.deadlineMs;
|
|
678
799
|
const lockWaitMs = args.lockWaitMs ?? DEFAULT_INSTALL_SETTLE_LOCK_WAIT_MS;
|
|
800
|
+
const staleProbeMs = Math.max(0, args.staleProbeMs ?? DEFAULT_INSTALL_TREE_STALE_PROBE_MS);
|
|
679
801
|
const start = now();
|
|
802
|
+
let lastActivitySnapshot = installTreeActivitySignature(args.target, args.packageRoot, fs);
|
|
803
|
+
let staleObservationStartedAt = lastActivitySnapshot.observed ? start : null;
|
|
804
|
+
let sawTreeChange = false;
|
|
680
805
|
let readySince = null;
|
|
681
806
|
let sawLock = false;
|
|
682
807
|
let sawRetired = false;
|
|
@@ -700,6 +825,32 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
700
825
|
sawLock = true;
|
|
701
826
|
lockTool = boundedDiagnosticValue(lock.tool, LOCK_TOOL_BYTES);
|
|
702
827
|
}
|
|
828
|
+
if (!sawTreeChange) {
|
|
829
|
+
const activitySnapshot = installTreeActivitySignature(args.target, args.packageRoot, fs);
|
|
830
|
+
if (!activitySnapshot.observed) {
|
|
831
|
+
staleObservationStartedAt = null;
|
|
832
|
+
}
|
|
833
|
+
else if (!lastActivitySnapshot.observed) {
|
|
834
|
+
staleObservationStartedAt = t;
|
|
835
|
+
}
|
|
836
|
+
else if (activitySnapshot.value !== lastActivitySnapshot.value) {
|
|
837
|
+
sawTreeChange = true;
|
|
838
|
+
staleObservationStartedAt = null;
|
|
839
|
+
}
|
|
840
|
+
lastActivitySnapshot = activitySnapshot;
|
|
841
|
+
}
|
|
842
|
+
let retiredPresent = false;
|
|
843
|
+
let retiredCheckFailed = false;
|
|
844
|
+
if (args.packageRoot) {
|
|
845
|
+
try {
|
|
846
|
+
retiredPresent = retiredSiblingPresent(args.packageRoot, fs);
|
|
847
|
+
if (retiredPresent)
|
|
848
|
+
sawRetired = true;
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
retiredCheckFailed = true;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
703
854
|
let ready = !foreignLockHeld && installTargetPresent(args.target, fs);
|
|
704
855
|
if (ready && args.packageRoot) {
|
|
705
856
|
try {
|
|
@@ -709,17 +860,8 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
709
860
|
catch {
|
|
710
861
|
ready = false;
|
|
711
862
|
}
|
|
712
|
-
if (ready)
|
|
713
|
-
|
|
714
|
-
if (retiredSiblingPresent(args.packageRoot, fs)) {
|
|
715
|
-
sawRetired = true;
|
|
716
|
-
ready = false;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
catch {
|
|
720
|
-
ready = false;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
863
|
+
if (ready && (retiredPresent || retiredCheckFailed))
|
|
864
|
+
ready = false;
|
|
723
865
|
}
|
|
724
866
|
if (ready) {
|
|
725
867
|
if (readySince === null)
|
|
@@ -731,7 +873,38 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
731
873
|
const waitedMs = t - start;
|
|
732
874
|
if (ready && (deadlineMs === 0 || t - readySince >= quietMs)) {
|
|
733
875
|
// Settling requires no foreign lock, so it is never held at the end here.
|
|
734
|
-
return {
|
|
876
|
+
return {
|
|
877
|
+
settled: true,
|
|
878
|
+
waitedMs,
|
|
879
|
+
sawLock,
|
|
880
|
+
sawRetired,
|
|
881
|
+
lockHeldAtEnd: false,
|
|
882
|
+
lockedMs,
|
|
883
|
+
lockTool,
|
|
884
|
+
sawTreeChange,
|
|
885
|
+
staleTree: false,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
if (!ready &&
|
|
889
|
+
!foreignLockHeld &&
|
|
890
|
+
!retiredPresent &&
|
|
891
|
+
!retiredCheckFailed &&
|
|
892
|
+
!sawLock &&
|
|
893
|
+
!sawRetired &&
|
|
894
|
+
!sawTreeChange &&
|
|
895
|
+
staleObservationStartedAt !== null &&
|
|
896
|
+
t - staleObservationStartedAt >= staleProbeMs) {
|
|
897
|
+
return {
|
|
898
|
+
settled: false,
|
|
899
|
+
waitedMs,
|
|
900
|
+
sawLock,
|
|
901
|
+
sawRetired,
|
|
902
|
+
lockHeldAtEnd: false,
|
|
903
|
+
lockedMs,
|
|
904
|
+
lockTool,
|
|
905
|
+
sawTreeChange,
|
|
906
|
+
staleTree: true,
|
|
907
|
+
};
|
|
735
908
|
}
|
|
736
909
|
// Give up when the TREE has had a full unlocked budget with no writer visible,
|
|
737
910
|
// OR a writer is STILL holding the lock and has held it past its own staleness
|
|
@@ -742,7 +915,17 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
742
915
|
// `deadlineMs` 0 makes the unlocked test fire on the first poll (evaluate-once),
|
|
743
916
|
// matching the pre-lock-aware contract; `lockHeldAtEnd` distinguishes the causes.
|
|
744
917
|
if (unlockedMs >= deadlineMs || (foreignLockHeld && lockedMs >= lockWaitMs)) {
|
|
745
|
-
return {
|
|
918
|
+
return {
|
|
919
|
+
settled: false,
|
|
920
|
+
waitedMs,
|
|
921
|
+
sawLock,
|
|
922
|
+
sawRetired,
|
|
923
|
+
lockHeldAtEnd: foreignLockHeld,
|
|
924
|
+
lockedMs,
|
|
925
|
+
lockTool,
|
|
926
|
+
sawTreeChange,
|
|
927
|
+
staleTree: false,
|
|
928
|
+
};
|
|
746
929
|
}
|
|
747
930
|
// The first poll at which the wait has reached `deadlineMs` while a lock is
|
|
748
931
|
// still held is exactly where the pre-lock-aware wait gave up — announce the
|
|
@@ -808,7 +991,7 @@ export function installRewriteInProgress(args) {
|
|
|
808
991
|
}
|
|
809
992
|
}
|
|
810
993
|
/** The fixed, input-free carrier message (grouping cardinality stays bounded). */
|
|
811
|
-
const INSTALL_TREE_TORN_MESSAGE = "hq-cli install tree
|
|
994
|
+
const INSTALL_TREE_TORN_MESSAGE = "hq-cli could not load a module from its install tree and automatic recovery did not complete";
|
|
812
995
|
/**
|
|
813
996
|
* A torn-install failure that stays VISIBLE in Sentry with bounded, hq-derived
|
|
814
997
|
* diagnostics. Fixed `name`/`message` (no argv, path, or free text in either),
|
|
@@ -822,6 +1005,8 @@ export class InstallTreeTornError extends Error {
|
|
|
822
1005
|
super(INSTALL_TREE_TORN_MESSAGE);
|
|
823
1006
|
this.name = "InstallTreeTornError";
|
|
824
1007
|
this.cause = init.cause;
|
|
1008
|
+
const sawTreeChange = init.sawTreeChange ?? false;
|
|
1009
|
+
const treeState = init.treeState ?? (init.outcome === "stale_tree" ? "stale" : "in_flight");
|
|
825
1010
|
this.diagnostics = {
|
|
826
1011
|
dialect: init.classified.dialect,
|
|
827
1012
|
code: init.classified.code,
|
|
@@ -829,10 +1014,12 @@ export class InstallTreeTornError extends Error {
|
|
|
829
1014
|
importer: boundedDiagnosticValue(init.classified.importer, IMPORTER_BYTES),
|
|
830
1015
|
packageRoot: boundedDiagnosticValue(init.packageRoot ?? "", PACKAGE_ROOT_BYTES),
|
|
831
1016
|
outcome: init.outcome,
|
|
1017
|
+
treeState,
|
|
832
1018
|
attempt: init.attempt,
|
|
833
1019
|
waitedMs: init.waitedMs,
|
|
834
1020
|
sawLock: init.sawLock,
|
|
835
1021
|
sawRetired: init.sawRetired,
|
|
1022
|
+
sawTreeChange,
|
|
836
1023
|
lockHeldAtEnd: init.lockHeldAtEnd ?? false,
|
|
837
1024
|
lockedMs: init.lockedMs ?? 0,
|
|
838
1025
|
lockTool: boundedDiagnosticValue(init.lockTool ?? "", LOCK_TOOL_BYTES),
|
|
@@ -846,8 +1033,45 @@ export function isInstallTreeTornError(err) {
|
|
|
846
1033
|
return err instanceof InstallTreeTornError;
|
|
847
1034
|
}
|
|
848
1035
|
/** The exact Sentry `contexts` payload for a captured torn-install failure. */
|
|
849
|
-
export function installTreeTornCaptureContext(err) {
|
|
850
|
-
|
|
1036
|
+
export function installTreeTornCaptureContext(err, homeDirectory = homedir()) {
|
|
1037
|
+
const diagnostics = err.diagnostics;
|
|
1038
|
+
return {
|
|
1039
|
+
contexts: {
|
|
1040
|
+
install_tree_torn: {
|
|
1041
|
+
...diagnostics,
|
|
1042
|
+
specifier: scrubHomeDirectoryPrefix(diagnostics.specifier, homeDirectory),
|
|
1043
|
+
importer: scrubHomeDirectoryPrefix(diagnostics.importer, homeDirectory),
|
|
1044
|
+
packageRoot: scrubHomeDirectoryPrefix(diagnostics.packageRoot, homeDirectory),
|
|
1045
|
+
},
|
|
1046
|
+
},
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
function looksLikeWindowsPath(value) {
|
|
1050
|
+
return /^[a-zA-Z]:[\\/]/.test(value) || /^\\\\/.test(value);
|
|
1051
|
+
}
|
|
1052
|
+
/** Replace a home-directory prefix without matching similarly-named sibling folders. */
|
|
1053
|
+
export function scrubHomeDirectoryPrefix(value, homeDirectory = homedir()) {
|
|
1054
|
+
if (!value || !homeDirectory)
|
|
1055
|
+
return value;
|
|
1056
|
+
if (looksLikeWindowsPath(value) || looksLikeWindowsPath(homeDirectory)) {
|
|
1057
|
+
const home = path.win32.normalize(homeDirectory).replace(/[\\/]+$/, "");
|
|
1058
|
+
const normalized = path.win32.normalize(value);
|
|
1059
|
+
const foldedHome = home.toLowerCase();
|
|
1060
|
+
const foldedValue = normalized.toLowerCase();
|
|
1061
|
+
if (foldedValue === foldedHome)
|
|
1062
|
+
return "~";
|
|
1063
|
+
const prefix = `${foldedHome}\\`;
|
|
1064
|
+
if (foldedValue.startsWith(prefix))
|
|
1065
|
+
return `~\\${normalized.slice(home.length + 1)}`;
|
|
1066
|
+
return value;
|
|
1067
|
+
}
|
|
1068
|
+
const home = path.posix.normalize(homeDirectory).replace(/\/+$/, "");
|
|
1069
|
+
const normalized = path.posix.normalize(value);
|
|
1070
|
+
if (normalized === home)
|
|
1071
|
+
return "~";
|
|
1072
|
+
if (normalized.startsWith(`${home}/`))
|
|
1073
|
+
return `~/${normalized.slice(home.length + 1)}`;
|
|
1074
|
+
return value;
|
|
851
1075
|
}
|
|
852
1076
|
/**
|
|
853
1077
|
* The fixed, input-free operator remedy. It names the likely writers (so a
|
|
@@ -855,10 +1079,15 @@ export function installTreeTornCaptureContext(err) {
|
|
|
855
1079
|
* and interpolates NOTHING — the only per-event value is the bounded specifier
|
|
856
1080
|
* appended by {@link installTreeTornStderrLine}.
|
|
857
1081
|
*/
|
|
858
|
-
export const INSTALL_TREE_TORN_REMEDY = "
|
|
859
|
-
"
|
|
860
|
-
"
|
|
861
|
-
"
|
|
1082
|
+
export const INSTALL_TREE_TORN_REMEDY = "hq-cli could not finish automatic install-tree recovery. Re-run your command; " +
|
|
1083
|
+
"if it still fails, reinstall with `npm install -g @indigoai-us/hq-cli@latest` " +
|
|
1084
|
+
"(or `pnpm add -g @indigoai-us/hq-cli@latest` / `bun add -g @indigoai-us/hq-cli@latest` " +
|
|
1085
|
+
"when that manager owns your install).";
|
|
1086
|
+
/** Fixed repair guidance for an unchanged tree with no active updater signal. */
|
|
1087
|
+
export const INSTALL_TREE_STALE_REMEDY = "hq-cli's install tree is incomplete and no active updater was detected. " +
|
|
1088
|
+
"Reinstall with `npm install -g @indigoai-us/hq-cli@latest` " +
|
|
1089
|
+
"(or `pnpm add -g @indigoai-us/hq-cli@latest` / `bun add -g @indigoai-us/hq-cli@latest` " +
|
|
1090
|
+
"when that manager owns your install), then run the command again.";
|
|
862
1091
|
/**
|
|
863
1092
|
* The fixed, input-free remedy for the 'lock-held' outcome: a cooperating writer
|
|
864
1093
|
* held the shared update lock past its own staleness ceiling. Telling the user to
|
|
@@ -877,7 +1106,11 @@ export const INSTALL_TREE_LOCK_HELD_REMEDY = "another hq updater (the desktop ap
|
|
|
877
1106
|
* {@link INSTALL_TREE_TORN_REMEDY} byte-for-byte.
|
|
878
1107
|
*/
|
|
879
1108
|
export function installTreeTornStderrLine(err) {
|
|
880
|
-
const remedy = err.diagnostics.outcome === "lock-held"
|
|
1109
|
+
const remedy = err.diagnostics.outcome === "lock-held"
|
|
1110
|
+
? INSTALL_TREE_LOCK_HELD_REMEDY
|
|
1111
|
+
: err.diagnostics.outcome === "stale_tree"
|
|
1112
|
+
? INSTALL_TREE_STALE_REMEDY
|
|
1113
|
+
: INSTALL_TREE_TORN_REMEDY;
|
|
881
1114
|
const specifier = err.diagnostics.specifier;
|
|
882
1115
|
return specifier ? `${remedy} (missing: ${specifier})` : remedy;
|
|
883
1116
|
}
|
|
@@ -1,30 +1,44 @@
|
|
|
1
|
-
export type PlanGateCode = "PLAN_LIMIT_EXCEEDED" | "PLAN_REQUIRED"
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* code rather than a reuse of `PLAN_REQUIRED` with company copy.
|
|
7
|
-
*/
|
|
8
|
-
| "PERSONAL_PLAN_REQUIRED";
|
|
1
|
+
export type PlanGateCode = "PLAN_LIMIT_EXCEEDED" | "PLAN_REQUIRED" | "MEETING_PLAN_REQUIRED"
|
|
2
|
+
/** A PERSONAL scope asked for a feature the paid Individual plan includes. */
|
|
3
|
+
| "PERSONAL_PLAN_REQUIRED"
|
|
4
|
+
/** External agent enrollment was denied by the company plan. */
|
|
5
|
+
| "BILLING_REQUIRED";
|
|
9
6
|
export interface PlanGateDetails {
|
|
10
7
|
resource?: string;
|
|
11
|
-
/** The refused feature, on a `PERSONAL_PLAN_REQUIRED` denial. */
|
|
12
8
|
feature?: string;
|
|
13
9
|
used?: number;
|
|
14
10
|
limit?: number;
|
|
15
11
|
upgradeUrl?: string;
|
|
16
|
-
|
|
12
|
+
reason?: string;
|
|
13
|
+
/** Company target from request context or the server's UID-keyed billing URL. */
|
|
17
14
|
companyUid?: string;
|
|
18
15
|
}
|
|
16
|
+
/** One renderer for server-selected upgrade links across CLI plan refusals. */
|
|
17
|
+
export declare function formatUpgradeRefusal(reason: string, upgradeUrl: string | undefined, options?: {
|
|
18
|
+
urlOnly?: boolean;
|
|
19
|
+
}): string;
|
|
20
|
+
/** Recover a company UID only from the server's UID-keyed billing callback. */
|
|
21
|
+
export declare function companyUidFromPlanUpgradeUrl(value: unknown): string | undefined;
|
|
19
22
|
export declare class PlanGateError extends Error {
|
|
20
23
|
readonly code: PlanGateCode;
|
|
21
24
|
readonly details: PlanGateDetails;
|
|
22
25
|
constructor(code: PlanGateCode, details: PlanGateDetails);
|
|
23
26
|
}
|
|
24
27
|
export declare function isPlanGateError(err: unknown): err is PlanGateError;
|
|
25
|
-
|
|
28
|
+
/** Plain, stable CLI copy for plan-gated resource creation. */
|
|
29
|
+
export declare function formatPlanGateError(err: PlanGateError, options?: {
|
|
30
|
+
urlOnly?: boolean;
|
|
31
|
+
}): string;
|
|
26
32
|
export declare function canOfferTeamUpgrade(argv?: readonly string[]): boolean;
|
|
27
|
-
|
|
33
|
+
export interface UpgradeLinkOfferDependencies {
|
|
34
|
+
ask?: (question: string) => Promise<string>;
|
|
35
|
+
openUrl?: (url: string) => Promise<void>;
|
|
36
|
+
argv?: readonly string[];
|
|
37
|
+
interactive?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** Ask before opening the exact URL returned by the server. */
|
|
40
|
+
export declare function offerUpgradeLink(url: string, deps?: UpgradeLinkOfferDependencies): Promise<boolean>;
|
|
41
|
+
/** Legacy checkout callback retained for callers outside the shared refusal paths. */
|
|
28
42
|
export declare function offerTeamUpgrade(onConfirm: () => Promise<void>, deps?: {
|
|
29
43
|
ask?: (question: string) => Promise<string>;
|
|
30
44
|
argv?: readonly string[];
|
|
@@ -33,7 +47,7 @@ export declare function offerTeamUpgrade(onConfirm: () => Promise<void>, deps?:
|
|
|
33
47
|
/** Decode an already-read JSON envelope from either shared HTTP client. */
|
|
34
48
|
export declare function planGateErrorFromPayload(status: number, body: unknown, companyUid?: string): PlanGateError | null;
|
|
35
49
|
/**
|
|
36
|
-
* Decode
|
|
50
|
+
* Decode the supported plan-gate envelopes. Malformed, unrelated,
|
|
37
51
|
* or non-402 responses retain their existing command-specific handling.
|
|
38
52
|
*/
|
|
39
53
|
export declare function planGateErrorFromResponse(response: Response, companyUid?: string): Promise<{
|