@indigoai-us/hq-cli 5.167.0 → 5.168.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/command-catalog.generated.d.ts +88 -1
- package/dist/command-catalog.generated.js +112 -1
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +79 -22
- package/dist/commands/lanes.js +2 -0
- package/dist/commands/mesh.js +13 -19
- package/dist/commands/outpost-pruner.d.ts +3 -0
- package/dist/commands/outpost-pruner.js +125 -0
- package/dist/lib/daemon/catalog.d.ts +11 -1
- package/dist/lib/daemon/catalog.js +31 -6
- package/dist/lib/daemon/install.d.ts +7 -0
- package/dist/lib/daemon/install.js +18 -2
- package/dist/lib/daemon/legacy.js +6 -1
- package/dist/lib/daemon/machine-type.d.ts +4 -1
- package/dist/lib/daemon/machine-type.js +29 -9
- package/dist/lib/daemon/migrate.js +2 -2
- package/dist/lib/daemon/remote-control.d.ts +32 -9
- package/dist/lib/daemon/remote-control.js +55 -11
- package/dist/lib/daemon/run.d.ts +6 -0
- package/dist/lib/daemon/run.js +43 -2
- package/dist/lib/daemon/tools.d.ts +52 -0
- package/dist/lib/daemon/tools.js +95 -0
- package/dist/lib/daemon/updater.d.ts +23 -15
- package/dist/lib/daemon/updater.js +97 -60
- package/dist/lib/lanes/outpost-pruner-install.d.ts +37 -0
- package/dist/lib/lanes/outpost-pruner-install.js +226 -0
- package/dist/lib/lanes/outpost-worktree-pruner-flag.d.ts +19 -0
- package/dist/lib/lanes/outpost-worktree-pruner-flag.js +76 -0
- package/dist/lib/lanes/outpost-worktree-pruner.d.ts +60 -0
- package/dist/lib/lanes/outpost-worktree-pruner.js +1135 -0
- package/dist/lib/mesh/live/session-events-client.js +3 -2
- package/dist/lib/mesh/project-board.d.ts +7 -0
- package/dist/lib/mesh/project-board.js +29 -1
- package/package.json +1 -1
package/dist/lib/daemon/run.js
CHANGED
|
@@ -15,14 +15,17 @@ import { isLocalDependencyInstall, resolveRunningInstall } from "../../utils/ver
|
|
|
15
15
|
import { agentKitPaths } from "../agent-kit/paths.js";
|
|
16
16
|
import { readBotConfig } from "../bot/config.js";
|
|
17
17
|
import { botsRoot } from "../bot/paths.js";
|
|
18
|
-
import { enabledServices, planUnits } from "./catalog.js";
|
|
18
|
+
import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits } from "./catalog.js";
|
|
19
19
|
import { readDaemonConfig } from "./config.js";
|
|
20
20
|
import { findLegacyUnits, legacyOwnerLookup } from "./legacy.js";
|
|
21
21
|
import { acquireDaemonLock, releaseDaemonLock } from "./lock.js";
|
|
22
|
+
import { daemonRootRefusal } from "./install.js";
|
|
22
23
|
import { detectMachineType } from "./machine-type.js";
|
|
23
24
|
import { appendPrivate, daemonPaths, ensurePrivateDir, openPrivateAppend } from "./paths.js";
|
|
24
25
|
import { takeControlRequests } from "./requests.js";
|
|
26
|
+
import { runnerSessionName } from "./remote-control.js";
|
|
25
27
|
import { STOP_ESCALATION_MS, createSupervisor } from "./supervisor.js";
|
|
28
|
+
import { userToolDirs } from "./tools.js";
|
|
26
29
|
export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
|
|
27
30
|
/** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
|
|
28
31
|
export const RESTART_EXIT_CODE = 75;
|
|
@@ -79,6 +82,35 @@ export function findOnPath(name, env = process.env) {
|
|
|
79
82
|
}
|
|
80
83
|
return null;
|
|
81
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* A tool the login user installed (~/.local/bin, ~/.npm-global/bin,
|
|
87
|
+
* ~/.grok/bin) before anything on PATH, so a box runs and updates the user's
|
|
88
|
+
* own copy instead of one root installed under /usr.
|
|
89
|
+
*/
|
|
90
|
+
export function findUserBinary(name, env, home) {
|
|
91
|
+
if (process.platform !== "win32") {
|
|
92
|
+
for (const dir of userToolDirs(home)) {
|
|
93
|
+
const candidate = path.join(dir, name);
|
|
94
|
+
try {
|
|
95
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
96
|
+
if (fs.statSync(candidate).isFile())
|
|
97
|
+
return candidate;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// not here
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return findOnPath(name, env);
|
|
105
|
+
}
|
|
106
|
+
function readIfExists(file) {
|
|
107
|
+
try {
|
|
108
|
+
return fs.readFileSync(file, "utf8");
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
82
114
|
/** This CLI is a global package install the updater can replace (not a dev checkout or a project dependency). */
|
|
83
115
|
export function cliSelfUpdatable() {
|
|
84
116
|
const install = resolveRunningInstall();
|
|
@@ -191,6 +223,12 @@ export async function runDaemon(opts) {
|
|
|
191
223
|
const machineType = detected.type;
|
|
192
224
|
for (const w of detected.warnings ?? [])
|
|
193
225
|
log("warn", w);
|
|
226
|
+
const refusal = daemonRootRefusal(typeof process.getuid === "function" && process.getuid() === 0, machineType);
|
|
227
|
+
if (refusal) {
|
|
228
|
+
log("error", refusal);
|
|
229
|
+
releaseDaemonLock(paths.lockPath);
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
194
232
|
log("info", `starting hq daemon (pid ${process.pid}) as ${machineType} (${detected.detail})`);
|
|
195
233
|
const syncRunnerPath = resolveSyncRunnerPath();
|
|
196
234
|
if (!syncRunnerPath)
|
|
@@ -223,7 +261,10 @@ export async function runDaemon(opts) {
|
|
|
223
261
|
legacyOwner: legacyOwnerLookup(legacy),
|
|
224
262
|
cliSelfUpdatable: updatable,
|
|
225
263
|
jobsReconcileScript: jobsReconcileScript(hqRoot),
|
|
226
|
-
claudePath:
|
|
264
|
+
claudePath: findUserBinary("claude", env, home),
|
|
265
|
+
codexPath: findUserBinary("codex", env, home),
|
|
266
|
+
codexRunnerScript: platform === "linux" && fs.existsSync(CODEX_RUNNER_SCRIPT) ? CODEX_RUNNER_SCRIPT : null,
|
|
267
|
+
remoteControlName: runnerSessionName(readIfExists(CLAUDE_RUNNER_SCRIPT) ?? ""),
|
|
227
268
|
tmuxPath: findOnPath("tmux", env),
|
|
228
269
|
hostName,
|
|
229
270
|
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AI CLIs the daemon keeps current (Claude, Codex, Grok): where the login
|
|
3
|
+
* user's copy is, which version it is, and how to update it.
|
|
4
|
+
*
|
|
5
|
+
* Boxes can carry two copies of a tool: one root installed under /usr during
|
|
6
|
+
* provisioning, and the login user's own under ~/.npm-global or ~/.local. The
|
|
7
|
+
* daemon runs as the login user, so it always prefers that user's copy (which
|
|
8
|
+
* it can update) and never updates a copy it cannot write.
|
|
9
|
+
*/
|
|
10
|
+
export declare const TOOL_NAMES: readonly ["claude", "codex", "grok"];
|
|
11
|
+
export type ToolName = (typeof TOOL_NAMES)[number];
|
|
12
|
+
export interface ToolProbeDeps {
|
|
13
|
+
home: string;
|
|
14
|
+
/** Defaults to this process's platform. */
|
|
15
|
+
platform?: NodeJS.Platform;
|
|
16
|
+
env: NodeJS.ProcessEnv;
|
|
17
|
+
exists: (p: string) => boolean;
|
|
18
|
+
realpath: (p: string) => string;
|
|
19
|
+
/** Whether this user can write the directory. */
|
|
20
|
+
writable: (dir: string) => boolean;
|
|
21
|
+
/** `<bin> --version`. */
|
|
22
|
+
version: (bin: string) => {
|
|
23
|
+
status: number | null;
|
|
24
|
+
stdout: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface ToolInstall {
|
|
28
|
+
/** The command as found (for example ~/.npm-global/bin/claude). */
|
|
29
|
+
path: string;
|
|
30
|
+
/** Where that resolves to on disk. */
|
|
31
|
+
realPath: string;
|
|
32
|
+
version: string | null;
|
|
33
|
+
/** False for a copy this user cannot update (for example root's under /usr). */
|
|
34
|
+
writable: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** The login user's own install folders, searched before PATH. */
|
|
37
|
+
export declare function userToolDirs(home: string): string[];
|
|
38
|
+
export declare function findUserTool(name: ToolName, deps: ToolProbeDeps): string | null;
|
|
39
|
+
export declare function parseToolVersion(output: string): string | null;
|
|
40
|
+
export declare function locateTool(name: ToolName, deps: ToolProbeDeps): ToolInstall | null;
|
|
41
|
+
export interface ToolUpdateCommand {
|
|
42
|
+
file: string;
|
|
43
|
+
args: string[];
|
|
44
|
+
env: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* An npm install is updated through npm, into the prefix it was found in (the
|
|
48
|
+
* npm on PATH may be configured for another one); anything else through the
|
|
49
|
+
* CLI's own `update`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function toolUpdateCommand(name: ToolName, install: Pick<ToolInstall, "path" | "realPath">, platform?: NodeJS.Platform): ToolUpdateCommand;
|
|
52
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AI CLIs the daemon keeps current (Claude, Codex, Grok): where the login
|
|
3
|
+
* user's copy is, which version it is, and how to update it.
|
|
4
|
+
*
|
|
5
|
+
* Boxes can carry two copies of a tool: one root installed under /usr during
|
|
6
|
+
* provisioning, and the login user's own under ~/.npm-global or ~/.local. The
|
|
7
|
+
* daemon runs as the login user, so it always prefers that user's copy (which
|
|
8
|
+
* it can update) and never updates a copy it cannot write.
|
|
9
|
+
*/
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
export const TOOL_NAMES = ["claude", "codex", "grok"];
|
|
12
|
+
/** npm package for tools that can be installed with npm. */
|
|
13
|
+
const NPM_PACKAGES = {
|
|
14
|
+
claude: "@anthropic-ai/claude-code",
|
|
15
|
+
codex: "@openai/codex",
|
|
16
|
+
grok: "@vibe-kit/grok-cli",
|
|
17
|
+
};
|
|
18
|
+
/** Environment each tool's own `update` command needs to run unattended. */
|
|
19
|
+
const UPDATE_ENV = {
|
|
20
|
+
codex: { CODEX_NON_INTERACTIVE: "1" },
|
|
21
|
+
};
|
|
22
|
+
/** The login user's own install folders, searched before PATH. */
|
|
23
|
+
export function userToolDirs(home) {
|
|
24
|
+
return [
|
|
25
|
+
path.posix.join(home, ".local", "bin"),
|
|
26
|
+
path.posix.join(home, ".npm-global", "bin"),
|
|
27
|
+
path.posix.join(home, ".grok", "bin"),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
export function findUserTool(name, deps) {
|
|
31
|
+
const windows = (deps.platform ?? process.platform) === "win32";
|
|
32
|
+
if (!windows) {
|
|
33
|
+
for (const dir of userToolDirs(deps.home)) {
|
|
34
|
+
const candidate = path.posix.join(dir, name);
|
|
35
|
+
if (deps.exists(candidate))
|
|
36
|
+
return candidate;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const p = windows ? path.win32 : path.posix;
|
|
40
|
+
// Windows installs are claude.exe, codex.cmd and so on.
|
|
41
|
+
const exts = windows ? (deps.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
42
|
+
for (const dir of (deps.env.PATH ?? "").split(p.delimiter)) {
|
|
43
|
+
if (!dir)
|
|
44
|
+
continue;
|
|
45
|
+
for (const ext of exts) {
|
|
46
|
+
const candidate = p.join(dir, `${name}${ext.toLowerCase()}`);
|
|
47
|
+
if (deps.exists(candidate))
|
|
48
|
+
return candidate;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
export function parseToolVersion(output) {
|
|
54
|
+
const m = /\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)\b/.exec(output);
|
|
55
|
+
return m ? m[1] : null;
|
|
56
|
+
}
|
|
57
|
+
export function locateTool(name, deps) {
|
|
58
|
+
const found = findUserTool(name, deps);
|
|
59
|
+
if (!found)
|
|
60
|
+
return null;
|
|
61
|
+
const realPath = deps.realpath(found);
|
|
62
|
+
const r = deps.version(found);
|
|
63
|
+
return {
|
|
64
|
+
path: found,
|
|
65
|
+
realPath,
|
|
66
|
+
version: r.status === 0 ? parseToolVersion(r.stdout) : null,
|
|
67
|
+
writable: deps.writable(path.dirname(realPath)),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The npm global prefix an install lives in: `<prefix>/lib/node_modules/<pkg>`
|
|
72
|
+
* on Unix, `<prefix>\\node_modules\\<pkg>` on Windows. Null when the real path
|
|
73
|
+
* is not inside that package.
|
|
74
|
+
*/
|
|
75
|
+
function npmPrefixOf(realPath, pkg, platform) {
|
|
76
|
+
const windows = platform === "win32";
|
|
77
|
+
const p = windows ? path.win32 : path.posix;
|
|
78
|
+
const marker = windows ? `${p.sep}node_modules${p.sep}${pkg.split("/").join(p.sep)}${p.sep}` : `/lib/node_modules/${pkg}/`;
|
|
79
|
+
const at = realPath.indexOf(marker);
|
|
80
|
+
return at > 0 ? realPath.slice(0, at) : null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* An npm install is updated through npm, into the prefix it was found in (the
|
|
84
|
+
* npm on PATH may be configured for another one); anything else through the
|
|
85
|
+
* CLI's own `update`.
|
|
86
|
+
*/
|
|
87
|
+
export function toolUpdateCommand(name, install, platform = process.platform) {
|
|
88
|
+
const pkg = NPM_PACKAGES[name];
|
|
89
|
+
const prefix = pkg ? npmPrefixOf(install.realPath, pkg, platform) : null;
|
|
90
|
+
if (pkg && prefix) {
|
|
91
|
+
return { file: "npm", args: ["install", "-g", "--prefix", prefix, `${pkg}@latest`], env: {} };
|
|
92
|
+
}
|
|
93
|
+
return { file: install.path, args: ["update"], env: { ...(UPDATE_ENV[name] ?? {}) } };
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -3,37 +3,45 @@
|
|
|
3
3
|
* replaces the boxes' `hq-cli-update.timer` and `*-codex-update.timer`:
|
|
4
4
|
*
|
|
5
5
|
* 1. Update the hq CLI through the shared self-update path (same install
|
|
6
|
-
* routing, lock and convergence check as every other CLI update)
|
|
7
|
-
*
|
|
6
|
+
* routing, lock and convergence check as every other CLI update), when it
|
|
7
|
+
* is a global install this process may replace.
|
|
8
|
+
* 2. Update the Claude, Codex and Grok CLIs the login user installed, each
|
|
9
|
+
* through its own update route. A copy this user cannot write (root's
|
|
10
|
+
* under /usr) is reported and left alone.
|
|
8
11
|
* 3. After a CLI update, ask the daemon to restart itself so the supervisor and
|
|
9
|
-
* every service run the new version.
|
|
12
|
+
* every service run the new version. The remote-control hosts notice an
|
|
13
|
+
* updated Claude or Codex on disk themselves and restart once idle.
|
|
10
14
|
*/
|
|
11
15
|
import type { SelfUpdateOutcome } from "../../utils/self-update.js";
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
install:
|
|
16
|
+
import { type ToolInstall, type ToolName } from "./tools.js";
|
|
17
|
+
export type { ToolInstall } from "./tools.js";
|
|
18
|
+
export interface ToolUpdater {
|
|
19
|
+
name: ToolName;
|
|
20
|
+
locate: () => ToolInstall | null;
|
|
21
|
+
update: (install: ToolInstall) => {
|
|
18
22
|
ok: boolean;
|
|
19
23
|
detail?: string;
|
|
20
24
|
};
|
|
21
25
|
}
|
|
22
26
|
export interface UpdaterDeps {
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
/** Null when this CLI is not a global install the updater may replace. */
|
|
28
|
+
updateCli: (() => Promise<Pick<SelfUpdateOutcome, "action" | "latest">>) | null;
|
|
29
|
+
tools: readonly ToolUpdater[];
|
|
25
30
|
requestDaemonRestart: () => void;
|
|
26
31
|
log: (line: string) => void;
|
|
27
32
|
}
|
|
28
|
-
export
|
|
33
|
+
export interface UpdaterResult {
|
|
29
34
|
ok: boolean;
|
|
30
|
-
|
|
35
|
+
/** Tools whose version changed in this run. */
|
|
36
|
+
updated: ToolName[];
|
|
37
|
+
}
|
|
38
|
+
export declare function runDaemonUpdater(deps: UpdaterDeps): Promise<UpdaterResult>;
|
|
31
39
|
/**
|
|
32
40
|
* The variables for the updater's own CLI update. Every supervised service
|
|
33
41
|
* gets HQ_NO_UPDATE_CHECK=1 so it never updates itself; the updater is the one
|
|
34
42
|
* process that must, so it drops that opt-out.
|
|
35
43
|
*/
|
|
36
44
|
export declare function updaterEnv(vars: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
37
|
-
/**
|
|
38
|
-
export declare function
|
|
45
|
+
/** The real updater for one tool, run as this (the login) user. */
|
|
46
|
+
export declare function hostToolUpdater(name: ToolName, env: NodeJS.ProcessEnv, home: string): ToolUpdater;
|
|
39
47
|
//# sourceMappingURL=updater.d.ts.map
|
|
@@ -3,56 +3,77 @@
|
|
|
3
3
|
* replaces the boxes' `hq-cli-update.timer` and `*-codex-update.timer`:
|
|
4
4
|
*
|
|
5
5
|
* 1. Update the hq CLI through the shared self-update path (same install
|
|
6
|
-
* routing, lock and convergence check as every other CLI update)
|
|
7
|
-
*
|
|
6
|
+
* routing, lock and convergence check as every other CLI update), when it
|
|
7
|
+
* is a global install this process may replace.
|
|
8
|
+
* 2. Update the Claude, Codex and Grok CLIs the login user installed, each
|
|
9
|
+
* through its own update route. A copy this user cannot write (root's
|
|
10
|
+
* under /usr) is reported and left alone.
|
|
8
11
|
* 3. After a CLI update, ask the daemon to restart itself so the supervisor and
|
|
9
|
-
* every service run the new version.
|
|
12
|
+
* every service run the new version. The remote-control hosts notice an
|
|
13
|
+
* updated Claude or Codex on disk themselves and restart once idle.
|
|
10
14
|
*/
|
|
11
15
|
import { spawnSync } from "node:child_process";
|
|
12
|
-
import
|
|
13
|
-
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import { locateTool, toolUpdateCommand } from "./tools.js";
|
|
14
18
|
export async function runDaemonUpdater(deps) {
|
|
15
19
|
let ok = true;
|
|
16
|
-
const cli = await deps.updateCli();
|
|
17
20
|
// The restart stops this process too, so it is requested only once all the
|
|
18
|
-
// updater's work (
|
|
21
|
+
// updater's work (every tool install) is done.
|
|
19
22
|
let restart = false;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
if (deps.updateCli === null) {
|
|
24
|
+
deps.log("hq CLI: not a global install, so it is updated with the project or app that owns it");
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const cli = await deps.updateCli();
|
|
28
|
+
switch (cli.action) {
|
|
29
|
+
case "updated":
|
|
30
|
+
deps.log(`hq CLI updated to ${cli.latest ?? "the latest version"}; the daemon restarts onto it once the tools are checked`);
|
|
31
|
+
restart = true;
|
|
32
|
+
break;
|
|
33
|
+
case "update-failed":
|
|
34
|
+
case "update-ineffective":
|
|
35
|
+
deps.log(`hq CLI update to ${cli.latest ?? "latest"} did not take effect (${cli.action})`);
|
|
36
|
+
ok = false;
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
deps.log(`hq CLI: ${cli.action === "current" ? "up to date" : cli.action}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const updated = [];
|
|
43
|
+
for (const tool of deps.tools) {
|
|
44
|
+
const outcome = updateTool(tool, deps.log);
|
|
45
|
+
if (outcome === "failed")
|
|
28
46
|
ok = false;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
deps.log(`hq CLI: ${cli.action === "current" ? "up to date" : cli.action}`);
|
|
47
|
+
if (outcome === "updated")
|
|
48
|
+
updated.push(tool.name);
|
|
32
49
|
}
|
|
33
|
-
if (!updateCodex(deps))
|
|
34
|
-
ok = false;
|
|
35
50
|
if (restart)
|
|
36
51
|
deps.requestDaemonRestart();
|
|
37
|
-
return { ok };
|
|
52
|
+
return { ok, updated };
|
|
38
53
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
return
|
|
54
|
+
function updateTool(tool, log) {
|
|
55
|
+
const before = tool.locate();
|
|
56
|
+
if (!before) {
|
|
57
|
+
log(`${tool.name}: not installed`);
|
|
58
|
+
return "skipped";
|
|
59
|
+
}
|
|
60
|
+
if (!before.writable) {
|
|
61
|
+
log(`${tool.name} at ${before.path} belongs to another account, so it is not updated as this user`);
|
|
62
|
+
return "skipped";
|
|
63
|
+
}
|
|
64
|
+
const result = tool.update(before);
|
|
65
|
+
if (!result.ok) {
|
|
66
|
+
log(`${tool.name} update failed: ${result.detail ?? "unknown error"}`);
|
|
67
|
+
return "failed";
|
|
48
68
|
}
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
const after = tool.locate();
|
|
70
|
+
const from = before.version ?? "unknown version";
|
|
71
|
+
if (after?.version && after.version !== before.version) {
|
|
72
|
+
log(`${tool.name} updated ${from} -> ${after.version}`);
|
|
73
|
+
return "updated";
|
|
53
74
|
}
|
|
54
|
-
|
|
55
|
-
return
|
|
75
|
+
log(`${tool.name} ${from}: up to date`);
|
|
76
|
+
return "current";
|
|
56
77
|
}
|
|
57
78
|
/**
|
|
58
79
|
* The variables for the updater's own CLI update. Every supervised service
|
|
@@ -64,39 +85,55 @@ export function updaterEnv(vars) {
|
|
|
64
85
|
delete copy.HQ_NO_UPDATE_CHECK;
|
|
65
86
|
return copy;
|
|
66
87
|
}
|
|
67
|
-
function
|
|
68
|
-
const r = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", args, {
|
|
69
|
-
encoding: "utf8",
|
|
70
|
-
timeout: 10 * 60_000,
|
|
71
|
-
windowsHide: true,
|
|
72
|
-
shell: process.platform === "win32",
|
|
73
|
-
});
|
|
74
|
-
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
75
|
-
}
|
|
76
|
-
/** Codex through the global npm install, the way the boxes install it. */
|
|
77
|
-
export function npmCodexUpdater() {
|
|
88
|
+
function hostProbe(env, home) {
|
|
78
89
|
return {
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
home,
|
|
91
|
+
env,
|
|
92
|
+
exists: (p) => {
|
|
81
93
|
try {
|
|
82
|
-
|
|
83
|
-
return
|
|
94
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
95
|
+
return true;
|
|
84
96
|
}
|
|
85
97
|
catch {
|
|
86
|
-
return
|
|
98
|
+
return false;
|
|
87
99
|
}
|
|
88
100
|
},
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
realpath: (p) => fs.realpathSync(p),
|
|
102
|
+
writable: (dir) => {
|
|
103
|
+
try {
|
|
104
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
version: (bin) => {
|
|
112
|
+
const r = spawnSync(bin, ["--version"], { encoding: "utf8", env, timeout: 30_000, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
113
|
+
return { status: r.status, stdout: r.stdout ?? "" };
|
|
93
114
|
},
|
|
94
|
-
|
|
95
|
-
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** The real updater for one tool, run as this (the login) user. */
|
|
118
|
+
export function hostToolUpdater(name, env, home) {
|
|
119
|
+
const probe = hostProbe(env, home);
|
|
120
|
+
return {
|
|
121
|
+
name,
|
|
122
|
+
locate: () => locateTool(name, probe),
|
|
123
|
+
update: (install) => {
|
|
124
|
+
const cmd = toolUpdateCommand(name, install);
|
|
125
|
+
const r = spawnSync(cmd.file, cmd.args, {
|
|
126
|
+
encoding: "utf8",
|
|
127
|
+
env: { ...env, ...cmd.env },
|
|
128
|
+
timeout: 10 * 60_000,
|
|
129
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
130
|
+
windowsHide: true,
|
|
131
|
+
shell: process.platform === "win32" && cmd.file === "npm",
|
|
132
|
+
});
|
|
96
133
|
if (r.status === 0)
|
|
97
134
|
return { ok: true };
|
|
98
|
-
const tail = r.stderr.trim().split("\n").slice(-3).join(" ").slice(0, 400);
|
|
99
|
-
return { ok: false, detail: tail ||
|
|
135
|
+
const tail = `${r.stderr ?? ""}\n${r.stdout ?? ""}`.trim().split("\n").slice(-3).join(" ").slice(0, 400);
|
|
136
|
+
return { ok: false, detail: tail || (r.error ? r.error.message : `${cmd.file} exited ${r.status ?? "by signal"}`) };
|
|
100
137
|
},
|
|
101
138
|
};
|
|
102
139
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const OUTPOST_PRUNER_PRESSURE_SERVICE = "hq-outpost-worktree-pruner-pressure.service";
|
|
2
|
+
export declare const OUTPOST_PRUNER_PRESSURE_TIMER = "hq-outpost-worktree-pruner-pressure.timer";
|
|
3
|
+
export declare const OUTPOST_PRUNER_DAILY_SERVICE = "hq-outpost-worktree-pruner-daily.service";
|
|
4
|
+
export declare const OUTPOST_PRUNER_DAILY_TIMER = "hq-outpost-worktree-pruner-daily.timer";
|
|
5
|
+
export declare const OUTPOST_PRUNER_MIN_AGE_DAYS = 14;
|
|
6
|
+
export interface OutpostPrunerInstallOptions {
|
|
7
|
+
systemdRoot: string;
|
|
8
|
+
hqRoot: string;
|
|
9
|
+
nodeBinary?: string;
|
|
10
|
+
hqBinary?: string;
|
|
11
|
+
home?: string;
|
|
12
|
+
flagApiUrl?: string;
|
|
13
|
+
companyUid?: string;
|
|
14
|
+
enable?: boolean;
|
|
15
|
+
runCommand?: (command: string, args: string[]) => {
|
|
16
|
+
status: number;
|
|
17
|
+
stdout?: string;
|
|
18
|
+
stderr?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface OutpostPrunerInstallResult {
|
|
22
|
+
ok: true;
|
|
23
|
+
action: "outpost-pruner.install" | "outpost-pruner.uninstall";
|
|
24
|
+
systemd_root: string;
|
|
25
|
+
units: string[];
|
|
26
|
+
written?: boolean;
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
removed?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function renderOutpostPrunerPressureTimer(): string;
|
|
31
|
+
export declare function renderOutpostPrunerDailyTimer(): string;
|
|
32
|
+
export declare function defaultOutpostPrunerSystemdDir(home?: string): string;
|
|
33
|
+
export declare function installOutpostPrunerTimers(options: OutpostPrunerInstallOptions): OutpostPrunerInstallResult;
|
|
34
|
+
export declare function uninstallOutpostPrunerTimers(options: Pick<OutpostPrunerInstallOptions, "systemdRoot" | "runCommand"> & {
|
|
35
|
+
disable?: boolean;
|
|
36
|
+
}): OutpostPrunerInstallResult;
|
|
37
|
+
//# sourceMappingURL=outpost-pruner-install.d.ts.map
|