@indigoai-us/hq-cli 5.171.0 → 5.173.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 +21 -0
- package/dist/commands/bot.d.ts +18 -0
- package/dist/commands/bot.js +93 -3
- package/dist/commands/daemon.js +7 -3
- package/dist/lib/bot/daemon.d.ts +69 -2
- package/dist/lib/bot/daemon.js +157 -7
- package/dist/lib/bot/run.js +1 -1
- package/dist/lib/bot/self-process.d.ts +19 -0
- package/dist/lib/bot/self-process.js +35 -0
- package/dist/lib/daemon/catalog.d.ts +8 -0
- package/dist/lib/daemon/catalog.js +13 -2
- package/dist/lib/daemon/hq-cloud.d.ts +51 -0
- package/dist/lib/daemon/hq-cloud.js +110 -0
- package/dist/lib/daemon/run.js +7 -3
- package/dist/lib/daemon/updater.d.ts +11 -1
- package/dist/lib/daemon/updater.js +24 -1
- package/dist/lib/doctor/checks/bots.d.ts +3 -0
- package/dist/lib/doctor/checks/bots.js +42 -5
- package/dist/lib/doctor/fix/apply.d.ts +14 -0
- package/dist/lib/doctor/fix/apply.js +38 -2
- package/dist/lib/doctor/fix/remediation.d.ts +1 -1
- package/dist/lib/doctor/fix/remediation.js +13 -0
- package/dist/lib/mesh/live/daemon/install.d.ts +16 -1
- package/dist/lib/mesh/live/daemon/install.js +53 -5
- package/dist/lib/node-binary.d.ts +50 -0
- package/dist/lib/node-binary.js +133 -0
- package/package.json +1 -1
|
@@ -24,6 +24,7 @@ import { retireLegacyListen } from "./legacy-listen.js";
|
|
|
24
24
|
import { readDaemonState } from "./state.js";
|
|
25
25
|
import { activateLaunchd, launchdPlistProgramArguments, launchdProgramArgumentsDiffer, parseLaunchctlPrintArguments, } from "../../../service-manager/launchd.js";
|
|
26
26
|
import { activateSystemd } from "../../../service-manager/systemd.js";
|
|
27
|
+
import { nodeBinaryProblem, preferStableNodeBinary } from "../../../node-binary.js";
|
|
27
28
|
/**
|
|
28
29
|
* `schtasks /End` terminates the task's root process only. The daemon is
|
|
29
30
|
* launched through `conhost.exe --headless`, so ending the task kills conhost
|
|
@@ -113,7 +114,7 @@ function migrateLegacyDaemonUnit(opts) {
|
|
|
113
114
|
env: opts.env,
|
|
114
115
|
hqRoot: verified,
|
|
115
116
|
hqBinary: "/usr/bin/hq",
|
|
116
|
-
nodeBinary: process.execPath
|
|
117
|
+
nodeBinary: process.execPath ? resolveNodeBinary(process.execPath, opts.env ?? process.env) : "/usr/bin/node",
|
|
117
118
|
});
|
|
118
119
|
try {
|
|
119
120
|
paths.hqBinary = resolveHqBinary(process.argv, opts.env ?? process.env);
|
|
@@ -264,18 +265,25 @@ export function resolveRunningCliEntry(argv = process.argv) {
|
|
|
264
265
|
throw new Error(`Cannot resolve hq binary: ${entry} is not a real path`);
|
|
265
266
|
}
|
|
266
267
|
}
|
|
267
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* Absolute node binary for a service unit: the realpath of process.execPath,
|
|
270
|
+
* then its upgrade-stable alias (/opt/homebrew/bin/node rather than
|
|
271
|
+
* /opt/homebrew/Cellar/node/<version>/bin/node) when one resolves to the same
|
|
272
|
+
* binary. No PATH lookup of `node` by name.
|
|
273
|
+
*/
|
|
268
274
|
export function resolveAbsoluteNodeBinary(execPath = process.execPath) {
|
|
269
275
|
if (!execPath)
|
|
270
276
|
throw new Error("Cannot resolve node binary: execPath is empty");
|
|
277
|
+
let real;
|
|
271
278
|
try {
|
|
272
|
-
|
|
279
|
+
real = fs.realpathSync(execPath);
|
|
273
280
|
}
|
|
274
281
|
catch {
|
|
275
282
|
if (path.isAbsolute(execPath))
|
|
276
283
|
return execPath;
|
|
277
284
|
throw new Error(`Cannot resolve node binary: ${execPath} is not a real path`);
|
|
278
285
|
}
|
|
286
|
+
return preferStableNodeBinary(real);
|
|
279
287
|
}
|
|
280
288
|
/** Resolve the hq binary the current shell uses (argv[1] or PATH). */
|
|
281
289
|
export function resolveHqBinary(argv = process.argv, env = process.env) {
|
|
@@ -312,7 +320,14 @@ export function isNewerCliVersion(current, recorded) {
|
|
|
312
320
|
*/
|
|
313
321
|
export function installDaemonIfStale(opts) {
|
|
314
322
|
const recorded = readDaemonState(opts.paths.daemonDir)?.cliVersion;
|
|
315
|
-
|
|
323
|
+
const platformForUnit = opts.platform ?? detectPlatform();
|
|
324
|
+
const installedNode = installedDaemonNodeBinary(opts.paths, platformForUnit, opts.readUnit);
|
|
325
|
+
// Only a rewrite that changes the node path helps: a unit whose node is gone
|
|
326
|
+
// or versioned, while this install resolves a different (stable) one.
|
|
327
|
+
const nodeProblem = installedNode && installedNode !== opts.paths.nodeBinary
|
|
328
|
+
? (opts.nodeProblem ?? nodeBinaryProblem)(installedNode)
|
|
329
|
+
: null;
|
|
330
|
+
if (!isNewerCliVersion(opts.currentVersion, recorded) && !nodeProblem) {
|
|
316
331
|
return {
|
|
317
332
|
reinstalled: false,
|
|
318
333
|
message: `daemon already records hq ${recorded}; not reinstalling`,
|
|
@@ -350,9 +365,42 @@ export function installDaemonIfStale(opts) {
|
|
|
350
365
|
message: `${installed.message} Restarted with ${opts.paths.nodeBinary} ${opts.paths.hqBinary}.`,
|
|
351
366
|
};
|
|
352
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* The node binary baked into the installed daemon unit (darwin plist or linux
|
|
370
|
+
* user unit), or null when there is no unit or it cannot be read. Used to
|
|
371
|
+
* reinstall a unit whose node was a versioned path an upgrade removed.
|
|
372
|
+
*/
|
|
373
|
+
export function installedDaemonNodeBinary(paths, platform, readUnit = (file) => {
|
|
374
|
+
try {
|
|
375
|
+
return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}) {
|
|
381
|
+
if (platform === "darwin") {
|
|
382
|
+
const text = readUnit(paths.plistPath);
|
|
383
|
+
if (!text)
|
|
384
|
+
return null;
|
|
385
|
+
const block = text.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
|
|
386
|
+
const first = block?.[1]?.match(/<string>([^<]*)<\/string>/);
|
|
387
|
+
return first?.[1] ?? null;
|
|
388
|
+
}
|
|
389
|
+
if (platform === "linux") {
|
|
390
|
+
const text = readUnit(paths.systemdUnitPath);
|
|
391
|
+
if (!text)
|
|
392
|
+
return null;
|
|
393
|
+
const m = text.match(/^ExecStart=('(?:[^']|'\\'')*'|\S+)/m);
|
|
394
|
+
if (!m)
|
|
395
|
+
return null;
|
|
396
|
+
const raw = m[1];
|
|
397
|
+
return raw.startsWith("'") ? raw.slice(1, -1).replace(/'\\''/g, "'") : raw;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
353
401
|
export function resolveNodeBinary(execPath = process.execPath, env = process.env) {
|
|
354
402
|
if (execPath && fs.existsSync(execPath))
|
|
355
|
-
return execPath;
|
|
403
|
+
return preferStableNodeBinary(execPath, { env });
|
|
356
404
|
const pathEnv = env.PATH ?? "";
|
|
357
405
|
for (const dir of pathEnv.split(path.delimiter)) {
|
|
358
406
|
const candidate = path.join(dir, "node");
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A node path that survives a node upgrade, for service units (LaunchAgents,
|
|
3
|
+
* systemd user units) that name node by absolute path.
|
|
4
|
+
*
|
|
5
|
+
* `process.execPath` is the resolved binary, e.g.
|
|
6
|
+
* /opt/homebrew/Cellar/node/26.0.0/bin/node. `brew upgrade node` deletes that
|
|
7
|
+
* directory, and every unit that baked it in stops launching (launchd cannot
|
|
8
|
+
* exec a missing program, so KeepAlive never brings the job back).
|
|
9
|
+
*
|
|
10
|
+
* The fix is to write the stable name the package manager keeps pointing at
|
|
11
|
+
* the current version (/opt/homebrew/bin/node, /opt/homebrew/opt/node/bin/node)
|
|
12
|
+
* instead of the versioned directory it points at today. A candidate is only
|
|
13
|
+
* used when it resolves to the SAME binary this process runs, so the unit
|
|
14
|
+
* keeps running the exact node it was installed with; a machine where no such
|
|
15
|
+
* alias exists (nvm without a `current` link, a hand-built node) keeps the
|
|
16
|
+
* execPath, as before.
|
|
17
|
+
*/
|
|
18
|
+
export interface NodeBinaryIo {
|
|
19
|
+
existsSync(p: string): boolean;
|
|
20
|
+
realpathSync(p: string): string;
|
|
21
|
+
}
|
|
22
|
+
/** True when `p` is a node binary inside a version-specific directory. */
|
|
23
|
+
export declare function isVersionedNodePath(p: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Stable names that may point at `execPath`, most specific first:
|
|
26
|
+
* the Homebrew prefix's bin/ and opt/<formula>/ links, a `current` link under
|
|
27
|
+
* nvm, then the usual global bins and every PATH entry.
|
|
28
|
+
*/
|
|
29
|
+
export declare function stableNodeCandidates(execPath: string, env?: NodeJS.ProcessEnv, home?: string): string[];
|
|
30
|
+
/**
|
|
31
|
+
* The stable name for `execPath` when it sits in a versioned directory and an
|
|
32
|
+
* alias resolving to the same binary exists; otherwise `execPath` unchanged.
|
|
33
|
+
*/
|
|
34
|
+
export declare function preferStableNodeBinary(execPath: string, opts?: {
|
|
35
|
+
env?: NodeJS.ProcessEnv;
|
|
36
|
+
home?: string;
|
|
37
|
+
io?: NodeBinaryIo;
|
|
38
|
+
}): string;
|
|
39
|
+
/**
|
|
40
|
+
* Why a unit's node path needs rewriting, or null when it is fine:
|
|
41
|
+
* "missing" — the binary is gone (launchd can no longer start the job);
|
|
42
|
+
* "versioned" — it still exists but the next upgrade will remove it, and a
|
|
43
|
+
* stable alias for the same binary exists.
|
|
44
|
+
*/
|
|
45
|
+
export declare function nodeBinaryProblem(nodePath: string, opts?: {
|
|
46
|
+
env?: NodeJS.ProcessEnv;
|
|
47
|
+
home?: string;
|
|
48
|
+
io?: NodeBinaryIo;
|
|
49
|
+
}): "missing" | "versioned" | null;
|
|
50
|
+
//# sourceMappingURL=node-binary.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A node path that survives a node upgrade, for service units (LaunchAgents,
|
|
3
|
+
* systemd user units) that name node by absolute path.
|
|
4
|
+
*
|
|
5
|
+
* `process.execPath` is the resolved binary, e.g.
|
|
6
|
+
* /opt/homebrew/Cellar/node/26.0.0/bin/node. `brew upgrade node` deletes that
|
|
7
|
+
* directory, and every unit that baked it in stops launching (launchd cannot
|
|
8
|
+
* exec a missing program, so KeepAlive never brings the job back).
|
|
9
|
+
*
|
|
10
|
+
* The fix is to write the stable name the package manager keeps pointing at
|
|
11
|
+
* the current version (/opt/homebrew/bin/node, /opt/homebrew/opt/node/bin/node)
|
|
12
|
+
* instead of the versioned directory it points at today. A candidate is only
|
|
13
|
+
* used when it resolves to the SAME binary this process runs, so the unit
|
|
14
|
+
* keeps running the exact node it was installed with; a machine where no such
|
|
15
|
+
* alias exists (nvm without a `current` link, a hand-built node) keeps the
|
|
16
|
+
* execPath, as before.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as os from "node:os";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
const defaultIo = {
|
|
22
|
+
existsSync: (p) => fs.existsSync(p),
|
|
23
|
+
realpathSync: (p) => fs.realpathSync(p),
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Install layouts whose node lives in a per-version directory that an upgrade
|
|
27
|
+
* or version switch removes or abandons.
|
|
28
|
+
*/
|
|
29
|
+
const VERSIONED_NODE_PATTERNS = [
|
|
30
|
+
// Homebrew: <prefix>/Cellar/node/26.0.0/bin/node, <prefix>/Cellar/node@22/22.9.0/bin/node
|
|
31
|
+
/\/Cellar\/node(?:@[^/]+)?\/[^/]+\/bin\/node$/,
|
|
32
|
+
// nvm
|
|
33
|
+
/\/\.nvm\/versions\/node\/[^/]+\/bin\/node$/,
|
|
34
|
+
// Volta
|
|
35
|
+
/\/\.volta\/tools\/image\/node\/[^/]+\/bin\/node$/,
|
|
36
|
+
// fnm (installed versions and the per-shell multishell links)
|
|
37
|
+
/\/fnm\/node-versions\/[^/]+\/installation\/bin\/node$/,
|
|
38
|
+
/\/fnm_multishells\/[^/]+\/bin\/node$/,
|
|
39
|
+
// asdf / mise
|
|
40
|
+
/\/\.asdf\/installs\/nodejs\/[^/]+\/bin\/node$/,
|
|
41
|
+
/\/mise\/installs\/node\/[^/]+\/bin\/node$/,
|
|
42
|
+
// n
|
|
43
|
+
/\/n\/versions\/node\/[^/]+\/bin\/node$/,
|
|
44
|
+
];
|
|
45
|
+
/** True when `p` is a node binary inside a version-specific directory. */
|
|
46
|
+
export function isVersionedNodePath(p) {
|
|
47
|
+
const normalized = p.split(path.sep).join("/");
|
|
48
|
+
return VERSIONED_NODE_PATTERNS.some((re) => re.test(normalized));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Stable names that may point at `execPath`, most specific first:
|
|
52
|
+
* the Homebrew prefix's bin/ and opt/<formula>/ links, a `current` link under
|
|
53
|
+
* nvm, then the usual global bins and every PATH entry.
|
|
54
|
+
*/
|
|
55
|
+
export function stableNodeCandidates(execPath, env = process.env, home = os.homedir()) {
|
|
56
|
+
const normalized = execPath.split(path.sep).join("/");
|
|
57
|
+
const out = [];
|
|
58
|
+
const brew = /^(.*)\/Cellar\/(node(?:@[^/]+)?)\/[^/]+\/bin\/node$/.exec(normalized);
|
|
59
|
+
if (brew) {
|
|
60
|
+
const prefix = brew[1];
|
|
61
|
+
out.push(`${prefix}/bin/node`, `${prefix}/opt/${brew[2]}/bin/node`);
|
|
62
|
+
}
|
|
63
|
+
const nvmDir = env.NVM_DIR?.trim() || path.join(home, ".nvm");
|
|
64
|
+
if (/\/\.nvm\/versions\/node\//.test(normalized))
|
|
65
|
+
out.push(path.join(nvmDir, "current", "bin", "node"));
|
|
66
|
+
out.push("/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node");
|
|
67
|
+
for (const dir of (env.PATH ?? "").split(path.delimiter)) {
|
|
68
|
+
if (dir && path.isAbsolute(dir))
|
|
69
|
+
out.push(path.join(dir, "node"));
|
|
70
|
+
}
|
|
71
|
+
return [...new Set(out)];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The stable name for `execPath` when it sits in a versioned directory and an
|
|
75
|
+
* alias resolving to the same binary exists; otherwise `execPath` unchanged.
|
|
76
|
+
*/
|
|
77
|
+
export function preferStableNodeBinary(execPath, opts = {}) {
|
|
78
|
+
if (!execPath || !isVersionedNodePath(execPath))
|
|
79
|
+
return execPath;
|
|
80
|
+
const io = opts.io ?? defaultIo;
|
|
81
|
+
let target;
|
|
82
|
+
try {
|
|
83
|
+
target = io.realpathSync(execPath);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// A versioned node that is already gone (a stale unit being repaired):
|
|
87
|
+
// there is no binary to match, so any working stable node is the answer.
|
|
88
|
+
return firstWorkingStableNode(execPath, opts, io) ?? execPath;
|
|
89
|
+
}
|
|
90
|
+
for (const candidate of stableNodeCandidates(execPath, opts.env ?? process.env, opts.home ?? os.homedir())) {
|
|
91
|
+
if (isVersionedNodePath(candidate))
|
|
92
|
+
continue;
|
|
93
|
+
try {
|
|
94
|
+
if (!io.existsSync(candidate))
|
|
95
|
+
continue;
|
|
96
|
+
if (io.realpathSync(candidate) === target)
|
|
97
|
+
return candidate;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
/* dangling link — not a candidate */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return execPath;
|
|
104
|
+
}
|
|
105
|
+
function firstWorkingStableNode(execPath, opts, io) {
|
|
106
|
+
for (const candidate of stableNodeCandidates(execPath, opts.env ?? process.env, opts.home ?? os.homedir())) {
|
|
107
|
+
if (isVersionedNodePath(candidate))
|
|
108
|
+
continue;
|
|
109
|
+
try {
|
|
110
|
+
if (io.existsSync(candidate) && io.existsSync(io.realpathSync(candidate)))
|
|
111
|
+
return candidate;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* dangling link */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Why a unit's node path needs rewriting, or null when it is fine:
|
|
121
|
+
* "missing" — the binary is gone (launchd can no longer start the job);
|
|
122
|
+
* "versioned" — it still exists but the next upgrade will remove it, and a
|
|
123
|
+
* stable alias for the same binary exists.
|
|
124
|
+
*/
|
|
125
|
+
export function nodeBinaryProblem(nodePath, opts = {}) {
|
|
126
|
+
const io = opts.io ?? defaultIo;
|
|
127
|
+
if (!nodePath)
|
|
128
|
+
return null;
|
|
129
|
+
if (!io.existsSync(nodePath))
|
|
130
|
+
return "missing";
|
|
131
|
+
return preferStableNodeBinary(nodePath, opts) !== nodePath ? "versioned" : null;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=node-binary.js.map
|