@indigoai-us/hq-cli 5.171.0 → 5.172.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 +13 -0
- package/dist/commands/daemon.js +7 -3
- 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/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.172.0] — 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- The `hq daemon` updater keeps hq-cloud current as well. Every 6 hours it
|
|
10
|
+
installs the newest hq-cloud with the same major version into
|
|
11
|
+
`~/.hq/daemon/hq-cloud`, and it restarts sync and the outpost heartbeat
|
|
12
|
+
right after an update. The daemon runs sync, and the heartbeat when that
|
|
13
|
+
hq-cloud has `hq-outpost-heartbeat`, from whichever copy is newer: this one
|
|
14
|
+
or the one bundled with the CLI.
|
|
15
|
+
- `hq daemon` sync pushes local changes as they happen (`--event-push`), the
|
|
16
|
+
same as outpost sync does today.
|
|
17
|
+
|
|
5
18
|
## [5.171.0] — 2026-09-24
|
|
6
19
|
|
|
7
20
|
### Fixed
|
package/dist/commands/daemon.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
17
|
import * as os from "node:os";
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
-
import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices } from "../lib/daemon/catalog.js";
|
|
19
|
+
import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits } from "../lib/daemon/catalog.js";
|
|
20
20
|
import { applyServiceToggle, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
|
|
21
21
|
import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
|
|
22
22
|
import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
|
|
@@ -418,19 +418,23 @@ async function runMigrate(d, opts) {
|
|
|
418
418
|
d.out("Nothing was changed. `hq daemon migrate --apply` takes them over.");
|
|
419
419
|
}
|
|
420
420
|
async function runServiceSelfUpdate(d) {
|
|
421
|
-
const [{ daemonSelfUpdate }, { hostToolUpdater, runDaemonUpdater, updaterEnv }, { TOOL_NAMES }, { cliSelfUpdatable }] = await Promise.all([
|
|
421
|
+
const [{ daemonSelfUpdate }, { hostToolUpdater, runDaemonUpdater, updaterEnv }, { TOOL_NAMES }, { cliSelfUpdatable }, { updateManagedHqCloud },] = await Promise.all([
|
|
422
422
|
import("../utils/self-update.js"),
|
|
423
423
|
import("../lib/daemon/updater.js"),
|
|
424
424
|
import("../lib/daemon/tools.js"),
|
|
425
425
|
import("../lib/daemon/run.js"),
|
|
426
|
+
import("../lib/daemon/hq-cloud.js"),
|
|
426
427
|
]);
|
|
427
|
-
const { paths } = currentHost(d);
|
|
428
|
+
const { paths, detected } = currentHost(d);
|
|
428
429
|
const env = updaterEnv(d.env);
|
|
429
430
|
const result = await runDaemonUpdater({
|
|
430
431
|
// Same guard as every self-update: never replace a dev checkout or a project's copy with a global install.
|
|
431
432
|
updateCli: cliSelfUpdatable() ? () => daemonSelfUpdate({ env }) : null,
|
|
432
433
|
tools: TOOL_NAMES.map((name) => hostToolUpdater(name, env, d.home)),
|
|
434
|
+
updateHqCloud: async () => updateManagedHqCloud(paths.root, env),
|
|
435
|
+
hqCloudUnits: hqCloudUnits(detected.type),
|
|
433
436
|
requestDaemonRestart: () => writeControlRequest(paths.requestsDir, { action: "restart-daemon" }),
|
|
437
|
+
requestRestart: (unit) => writeControlRequest(paths.requestsDir, { action: "restart", unit }),
|
|
434
438
|
log: d.out,
|
|
435
439
|
});
|
|
436
440
|
if (!result.ok)
|
|
@@ -62,6 +62,8 @@ export interface PlanContext {
|
|
|
62
62
|
hqRoot: string | undefined;
|
|
63
63
|
/** hq-cloud's sync-runner.js, or null when it cannot be found. */
|
|
64
64
|
syncRunnerPath: string | null;
|
|
65
|
+
/** hq-cloud's `hq-outpost-heartbeat` bin, when the hq-cloud in use has one. */
|
|
66
|
+
heartbeatRunnerPath: string | null;
|
|
65
67
|
signedIn: boolean;
|
|
66
68
|
/** ~/.hq-agent/kit.json exists. */
|
|
67
69
|
kitConfigured: boolean;
|
|
@@ -86,6 +88,12 @@ export interface PlanContext {
|
|
|
86
88
|
/** The box's Codex remote-control runner script, when it has one. */
|
|
87
89
|
codexRunnerScript: string | null;
|
|
88
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The units that run hq-cloud's code on a machine type, restarted after the
|
|
93
|
+
* updater installs a new hq-cloud. Only outposts and self-hosted machines run
|
|
94
|
+
* the outpost heartbeat; agent machines run the agent kit's own.
|
|
95
|
+
*/
|
|
96
|
+
export declare function hqCloudUnits(machineType: MachineType): ServiceName[];
|
|
89
97
|
/** Written by outpost provisioning; the daemon runs it rather than re-implementing Codex login and relay setup. */
|
|
90
98
|
export declare const CODEX_RUNNER_SCRIPT = "/usr/local/bin/outpost-codex-runner.sh";
|
|
91
99
|
/** The outpost's Claude runner script, read for the session name it uses. */
|
|
@@ -96,6 +96,14 @@ function firstBlock(checks) {
|
|
|
96
96
|
const hit = checks.find(([blocked]) => blocked);
|
|
97
97
|
return hit ? { state: "waiting", reason: hit[1] } : undefined;
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* The units that run hq-cloud's code on a machine type, restarted after the
|
|
101
|
+
* updater installs a new hq-cloud. Only outposts and self-hosted machines run
|
|
102
|
+
* the outpost heartbeat; agent machines run the agent kit's own.
|
|
103
|
+
*/
|
|
104
|
+
export function hqCloudUnits(machineType) {
|
|
105
|
+
return machineType === "outpost" || machineType === "self-hosted" ? ["sync", "heartbeat"] : ["sync"];
|
|
106
|
+
}
|
|
99
107
|
/** Written by outpost provisioning; the daemon runs it rather than re-implementing Codex login and relay setup. */
|
|
100
108
|
export const CODEX_RUNNER_SCRIPT = "/usr/local/bin/outpost-codex-runner.sh";
|
|
101
109
|
/** The outpost's Claude runner script, read for the session name it uses. */
|
|
@@ -113,7 +121,7 @@ function planService(service, ctx) {
|
|
|
113
121
|
command: {
|
|
114
122
|
type: "node",
|
|
115
123
|
script: ctx.syncRunnerPath,
|
|
116
|
-
args: ["--companies", "--direction", "both", "--on-conflict", "keep", "--hq-root", ctx.hqRoot ?? "", "--watch"],
|
|
124
|
+
args: ["--companies", "--direction", "both", "--on-conflict", "keep", "--hq-root", ctx.hqRoot ?? "", "--watch", "--event-push"],
|
|
117
125
|
},
|
|
118
126
|
});
|
|
119
127
|
unit.blocked = firstBlock([
|
|
@@ -141,7 +149,10 @@ function planService(service, ctx) {
|
|
|
141
149
|
}
|
|
142
150
|
return [
|
|
143
151
|
base(service, {
|
|
144
|
-
|
|
152
|
+
// hq-cloud's own heartbeat picks up hq-cloud updates without a CLI release.
|
|
153
|
+
command: ctx.heartbeatRunnerPath
|
|
154
|
+
? { type: "node", script: ctx.heartbeatRunnerPath, args: ["--home", ctx.home] }
|
|
155
|
+
: { type: "hq", args: ["outposts", "heartbeat", "--home", ctx.home] },
|
|
145
156
|
blocked: firstBlock([[!ctx.signedIn, NOT_SIGNED_IN]]),
|
|
146
157
|
}),
|
|
147
158
|
];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which hq-cloud the daemon runs sync and the outpost heartbeat from.
|
|
3
|
+
*
|
|
4
|
+
* The hq CLI ships with a copy of hq-cloud, but hq-cloud releases more often
|
|
5
|
+
* than the CLI. The updater keeps a newer copy of the same major version in
|
|
6
|
+
* `<daemon dir>/hq-cloud`, and the daemon runs whichever of the two is newer.
|
|
7
|
+
* A different major version is never used: it may not match this CLI.
|
|
8
|
+
*/
|
|
9
|
+
export declare const HQ_CLOUD_PACKAGE = "@indigoai-us/hq-cloud";
|
|
10
|
+
export interface HqCloudCopy {
|
|
11
|
+
/** The package folder (the one holding its package.json). */
|
|
12
|
+
root: string;
|
|
13
|
+
version: string;
|
|
14
|
+
}
|
|
15
|
+
export interface PickedHqCloud extends HqCloudCopy {
|
|
16
|
+
source: "bundled" | "managed";
|
|
17
|
+
}
|
|
18
|
+
export declare function managedHqCloudDir(daemonRoot: string): string;
|
|
19
|
+
/** The package folder inside the updater's install prefix. */
|
|
20
|
+
export declare function managedHqCloudRoot(daemonRoot: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* `usable` says whether a copy has what the daemon runs; a newer managed copy
|
|
23
|
+
* that fails it (an interrupted or damaged install) is passed over.
|
|
24
|
+
*/
|
|
25
|
+
export declare function pickHqCloud(bundled: HqCloudCopy | null, managed: HqCloudCopy | null, usable?: (copy: HqCloudCopy) => boolean): PickedHqCloud | null;
|
|
26
|
+
export interface HqCloudRunners {
|
|
27
|
+
syncRunner: string | null;
|
|
28
|
+
/** `hq-outpost-heartbeat`, which older hq-cloud releases do not have. */
|
|
29
|
+
heartbeat: string | null;
|
|
30
|
+
}
|
|
31
|
+
export declare function hqCloudRunners(root: string, exists?: (p: string) => boolean): HqCloudRunners;
|
|
32
|
+
/** Install the newest release of the bundled major version, never older than the bundled copy. */
|
|
33
|
+
export declare function hqCloudInstallCommand(prefix: string, bundledVersion: string, platform?: NodeJS.Platform): {
|
|
34
|
+
file: string;
|
|
35
|
+
args: string[];
|
|
36
|
+
shell: boolean;
|
|
37
|
+
};
|
|
38
|
+
export declare function readHqCloudCopy(root: string): HqCloudCopy | null;
|
|
39
|
+
/** The copy bundled with this CLI. */
|
|
40
|
+
export declare function bundledHqCloud(): HqCloudCopy | null;
|
|
41
|
+
/** The hq-cloud the daemon should run right now. */
|
|
42
|
+
export declare function currentHqCloud(daemonRoot: string): PickedHqCloud | null;
|
|
43
|
+
export interface HqCloudUpdateOutcome {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
from: string | null;
|
|
46
|
+
to: string | null;
|
|
47
|
+
detail?: string;
|
|
48
|
+
}
|
|
49
|
+
/** Install or update the updater's copy, as this (the login) user. */
|
|
50
|
+
export declare function updateManagedHqCloud(daemonRoot: string, env: NodeJS.ProcessEnv): HqCloudUpdateOutcome;
|
|
51
|
+
//# sourceMappingURL=hq-cloud.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which hq-cloud the daemon runs sync and the outpost heartbeat from.
|
|
3
|
+
*
|
|
4
|
+
* The hq CLI ships with a copy of hq-cloud, but hq-cloud releases more often
|
|
5
|
+
* than the CLI. The updater keeps a newer copy of the same major version in
|
|
6
|
+
* `<daemon dir>/hq-cloud`, and the daemon runs whichever of the two is newer.
|
|
7
|
+
* A different major version is never used: it may not match this CLI.
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { buildSpawnPlan } from "../../utils/windows-spawn.js";
|
|
14
|
+
export const HQ_CLOUD_PACKAGE = "@indigoai-us/hq-cloud";
|
|
15
|
+
export function managedHqCloudDir(daemonRoot) {
|
|
16
|
+
return path.join(daemonRoot, "hq-cloud");
|
|
17
|
+
}
|
|
18
|
+
/** The package folder inside the updater's install prefix. */
|
|
19
|
+
export function managedHqCloudRoot(daemonRoot) {
|
|
20
|
+
return path.join(managedHqCloudDir(daemonRoot), "node_modules", ...HQ_CLOUD_PACKAGE.split("/"));
|
|
21
|
+
}
|
|
22
|
+
function parseVersion(v) {
|
|
23
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(v.trim());
|
|
24
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
25
|
+
}
|
|
26
|
+
function newer(a, b) {
|
|
27
|
+
for (let i = 0; i < 3; i++) {
|
|
28
|
+
if (a[i] !== b[i])
|
|
29
|
+
return a[i] > b[i];
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* `usable` says whether a copy has what the daemon runs; a newer managed copy
|
|
35
|
+
* that fails it (an interrupted or damaged install) is passed over.
|
|
36
|
+
*/
|
|
37
|
+
export function pickHqCloud(bundled, managed, usable = () => true) {
|
|
38
|
+
if (!bundled)
|
|
39
|
+
return null;
|
|
40
|
+
const b = parseVersion(bundled.version);
|
|
41
|
+
const m = managed ? parseVersion(managed.version) : null;
|
|
42
|
+
if (managed && b && m && m[0] === b[0] && newer(m, b) && usable(managed))
|
|
43
|
+
return { ...managed, source: "managed" };
|
|
44
|
+
return { ...bundled, source: "bundled" };
|
|
45
|
+
}
|
|
46
|
+
export function hqCloudRunners(root, exists = fs.existsSync) {
|
|
47
|
+
const bin = (name) => {
|
|
48
|
+
const p = path.join(root, "dist", "bin", name);
|
|
49
|
+
return exists(p) ? p : null;
|
|
50
|
+
};
|
|
51
|
+
return { syncRunner: bin("sync-runner.js"), heartbeat: bin("outpost-heartbeat.js") };
|
|
52
|
+
}
|
|
53
|
+
/** Install the newest release of the bundled major version, never older than the bundled copy. */
|
|
54
|
+
export function hqCloudInstallCommand(prefix, bundledVersion, platform = process.platform) {
|
|
55
|
+
// npm is a .cmd on Windows and needs the shell, so every argument is quoted for it.
|
|
56
|
+
const plan = buildSpawnPlan("npm", ["install", "--prefix", prefix, "--no-audit", "--no-fund", "--omit=dev", `${HQ_CLOUD_PACKAGE}@^${bundledVersion}`], platform);
|
|
57
|
+
return { file: plan.cmd, args: plan.args, shell: plan.shell };
|
|
58
|
+
}
|
|
59
|
+
export function readHqCloudCopy(root) {
|
|
60
|
+
try {
|
|
61
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
62
|
+
return typeof pkg.version === "string" ? { root, version: pkg.version } : null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** The copy bundled with this CLI. */
|
|
69
|
+
export function bundledHqCloud() {
|
|
70
|
+
try {
|
|
71
|
+
const require = createRequire(import.meta.url);
|
|
72
|
+
return readHqCloudCopy(path.dirname(require.resolve(`${HQ_CLOUD_PACKAGE}/package.json`)));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** The hq-cloud the daemon should run right now. */
|
|
79
|
+
export function currentHqCloud(daemonRoot) {
|
|
80
|
+
return pickHqCloud(bundledHqCloud(), readHqCloudCopy(managedHqCloudRoot(daemonRoot)), (copy) => hqCloudRunners(copy.root).syncRunner !== null);
|
|
81
|
+
}
|
|
82
|
+
/** Install or update the updater's copy, as this (the login) user. */
|
|
83
|
+
export function updateManagedHqCloud(daemonRoot, env) {
|
|
84
|
+
const bundled = bundledHqCloud();
|
|
85
|
+
const from = currentHqCloud(daemonRoot)?.version ?? null;
|
|
86
|
+
if (!bundled)
|
|
87
|
+
return { ok: false, from, to: null, detail: "this CLI install has no bundled hq-cloud to match" };
|
|
88
|
+
const prefix = managedHqCloudDir(daemonRoot);
|
|
89
|
+
try {
|
|
90
|
+
fs.mkdirSync(prefix, { recursive: true, mode: 0o700 });
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
return { ok: false, from, to: null, detail: `could not create ${prefix}: ${err instanceof Error ? err.message : String(err)}` };
|
|
94
|
+
}
|
|
95
|
+
const cmd = hqCloudInstallCommand(prefix, bundled.version);
|
|
96
|
+
const r = spawnSync(cmd.file, cmd.args, {
|
|
97
|
+
encoding: "utf8",
|
|
98
|
+
env,
|
|
99
|
+
timeout: 10 * 60_000,
|
|
100
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
101
|
+
windowsHide: true,
|
|
102
|
+
shell: cmd.shell,
|
|
103
|
+
});
|
|
104
|
+
if (r.status !== 0) {
|
|
105
|
+
const tail = `${r.stderr ?? ""}\n${r.stdout ?? ""}`.trim().split("\n").slice(-3).join(" ").slice(0, 400);
|
|
106
|
+
return { ok: false, from, to: null, detail: tail || (r.error ? r.error.message : `npm exited ${r.status ?? "by signal"}`) };
|
|
107
|
+
}
|
|
108
|
+
return { ok: true, from, to: currentHqCloud(daemonRoot)?.version ?? null };
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=hq-cloud.js.map
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -23,6 +23,7 @@ import { daemonRootRefusal } from "./install.js";
|
|
|
23
23
|
import { detectMachineType } from "./machine-type.js";
|
|
24
24
|
import { appendPrivate, daemonPaths, ensurePrivateDir, openPrivateAppend } from "./paths.js";
|
|
25
25
|
import { takeControlRequests } from "./requests.js";
|
|
26
|
+
import { currentHqCloud, hqCloudRunners } from "./hq-cloud.js";
|
|
26
27
|
import { runnerSessionName } from "./remote-control.js";
|
|
27
28
|
import { STOP_ESCALATION_MS, createSupervisor } from "./supervisor.js";
|
|
28
29
|
import { userToolDirs } from "./tools.js";
|
|
@@ -230,8 +231,7 @@ export async function runDaemon(opts) {
|
|
|
230
231
|
return 1;
|
|
231
232
|
}
|
|
232
233
|
log("info", `starting hq daemon (pid ${process.pid}) as ${machineType} (${detected.detail})`);
|
|
233
|
-
|
|
234
|
-
if (!syncRunnerPath)
|
|
234
|
+
if (!resolveSyncRunnerPath())
|
|
235
235
|
log("warn", "the hq-cloud sync runner was not found in this CLI install; sync is unavailable");
|
|
236
236
|
const updatable = cliSelfUpdatable();
|
|
237
237
|
const hostName = os.hostname().split(".")[0] || "hq-host";
|
|
@@ -247,12 +247,16 @@ export async function runDaemon(opts) {
|
|
|
247
247
|
const legacy = legacyUnits();
|
|
248
248
|
const configured = config.hqRoot ?? env.HQ_ROOT?.trim() ?? undefined;
|
|
249
249
|
const hqRoot = configured && fs.existsSync(configured) ? configured : undefined;
|
|
250
|
+
// Re-picked every reconcile, so a newer hq-cloud from the updater is used as soon as it lands.
|
|
251
|
+
const hqCloud = currentHqCloud(paths.root);
|
|
252
|
+
const runners = hqCloud ? hqCloudRunners(hqCloud.root) : { syncRunner: null, heartbeat: null };
|
|
250
253
|
return planUnits(enabledServices(machineType, config), {
|
|
251
254
|
machineType,
|
|
252
255
|
platform,
|
|
253
256
|
home,
|
|
254
257
|
hqRoot,
|
|
255
|
-
syncRunnerPath,
|
|
258
|
+
syncRunnerPath: runners.syncRunner,
|
|
259
|
+
heartbeatRunnerPath: runners.heartbeat,
|
|
256
260
|
signedIn: signedIn(env, home, log),
|
|
257
261
|
kitConfigured: fs.existsSync(agentKitPaths(home, env).kitConfigPath),
|
|
258
262
|
desktopAppManagesSync: machineType === "desktop" &&
|
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
* 2. Update the Claude, Codex and Grok CLIs the login user installed, each
|
|
9
9
|
* through its own update route. A copy this user cannot write (root's
|
|
10
10
|
* under /usr) is reported and left alone.
|
|
11
|
-
* 3.
|
|
11
|
+
* 3. Install or update the daemon's own hq-cloud (sync and the outpost
|
|
12
|
+
* heartbeat run from it when it is newer than the CLI's bundled copy), and
|
|
13
|
+
* restart sync and the heartbeat right after it changes.
|
|
14
|
+
* 4. After a CLI update, ask the daemon to restart itself so the supervisor and
|
|
12
15
|
* every service run the new version. The remote-control hosts notice an
|
|
13
16
|
* updated Claude or Codex on disk themselves and restart once idle.
|
|
14
17
|
*/
|
|
15
18
|
import type { SelfUpdateOutcome } from "../../utils/self-update.js";
|
|
19
|
+
import type { HqCloudUpdateOutcome } from "./hq-cloud.js";
|
|
16
20
|
import { type ToolInstall, type ToolName } from "./tools.js";
|
|
17
21
|
export type { ToolInstall } from "./tools.js";
|
|
18
22
|
export interface ToolUpdater {
|
|
@@ -27,7 +31,13 @@ export interface UpdaterDeps {
|
|
|
27
31
|
/** Null when this CLI is not a global install the updater may replace. */
|
|
28
32
|
updateCli: (() => Promise<Pick<SelfUpdateOutcome, "action" | "latest">>) | null;
|
|
29
33
|
tools: readonly ToolUpdater[];
|
|
34
|
+
/** Install or update the daemon's own hq-cloud; null where it has none. */
|
|
35
|
+
updateHqCloud: (() => Promise<HqCloudUpdateOutcome>) | null;
|
|
36
|
+
/** The units on this machine that run hq-cloud code, restarted after it updates. */
|
|
37
|
+
hqCloudUnits: readonly string[];
|
|
30
38
|
requestDaemonRestart: () => void;
|
|
39
|
+
/** Restart one unit (a restart request to the running daemon). */
|
|
40
|
+
requestRestart: (unit: string) => void;
|
|
31
41
|
log: (line: string) => void;
|
|
32
42
|
}
|
|
33
43
|
export interface UpdaterResult {
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* 2. Update the Claude, Codex and Grok CLIs the login user installed, each
|
|
9
9
|
* through its own update route. A copy this user cannot write (root's
|
|
10
10
|
* under /usr) is reported and left alone.
|
|
11
|
-
* 3.
|
|
11
|
+
* 3. Install or update the daemon's own hq-cloud (sync and the outpost
|
|
12
|
+
* heartbeat run from it when it is newer than the CLI's bundled copy), and
|
|
13
|
+
* restart sync and the heartbeat right after it changes.
|
|
14
|
+
* 4. After a CLI update, ask the daemon to restart itself so the supervisor and
|
|
12
15
|
* every service run the new version. The remote-control hosts notice an
|
|
13
16
|
* updated Claude or Codex on disk themselves and restart once idle.
|
|
14
17
|
*/
|
|
@@ -39,6 +42,26 @@ export async function runDaemonUpdater(deps) {
|
|
|
39
42
|
deps.log(`hq CLI: ${cli.action === "current" ? "up to date" : cli.action}`);
|
|
40
43
|
}
|
|
41
44
|
}
|
|
45
|
+
if (deps.updateHqCloud) {
|
|
46
|
+
const cloud = await deps.updateHqCloud().catch((err) => ({
|
|
47
|
+
ok: false,
|
|
48
|
+
from: null,
|
|
49
|
+
to: null,
|
|
50
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
51
|
+
}));
|
|
52
|
+
if (!cloud.ok) {
|
|
53
|
+
deps.log(`hq-cloud update failed: ${cloud.detail ?? "unknown error"}`);
|
|
54
|
+
ok = false;
|
|
55
|
+
}
|
|
56
|
+
else if (cloud.to && cloud.to !== cloud.from) {
|
|
57
|
+
deps.log(`hq-cloud updated ${cloud.from ?? "(none)"} -> ${cloud.to}; restarting ${deps.hqCloudUnits.join(" and ")} onto it`);
|
|
58
|
+
for (const unit of deps.hqCloudUnits)
|
|
59
|
+
deps.requestRestart(unit);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
deps.log(`hq-cloud ${cloud.to ?? cloud.from ?? "unknown version"}: up to date`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
42
65
|
const updated = [];
|
|
43
66
|
for (const tool of deps.tools) {
|
|
44
67
|
const outcome = updateTool(tool, deps.log);
|