@indigoai-us/hq-cli 5.161.0 → 5.163.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +27 -2
  2. package/dist/command-catalog.generated.d.ts +188 -0
  3. package/dist/command-catalog.generated.js +235 -0
  4. package/dist/command-registration-plan.d.ts +7 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/agent-enroll.d.ts +6 -1
  7. package/dist/commands/agent-enroll.js +30 -3
  8. package/dist/commands/agents.js +15 -22
  9. package/dist/commands/bot.js +56 -6
  10. package/dist/commands/daemon.d.ts +53 -0
  11. package/dist/commands/daemon.js +554 -0
  12. package/dist/commands/lanes.js +10 -1
  13. package/dist/commands/meetings.js +18 -16
  14. package/dist/commands/mesh.js +9 -0
  15. package/dist/lib/daemon/catalog.d.ts +85 -0
  16. package/dist/lib/daemon/catalog.js +274 -0
  17. package/dist/lib/daemon/config.d.ts +26 -0
  18. package/dist/lib/daemon/config.js +85 -0
  19. package/dist/lib/daemon/handoff.d.ts +53 -0
  20. package/dist/lib/daemon/handoff.js +136 -0
  21. package/dist/lib/daemon/install.d.ts +76 -0
  22. package/dist/lib/daemon/install.js +299 -0
  23. package/dist/lib/daemon/legacy.d.ts +35 -0
  24. package/dist/lib/daemon/legacy.js +120 -0
  25. package/dist/lib/daemon/lock.d.ts +47 -0
  26. package/dist/lib/daemon/lock.js +138 -0
  27. package/dist/lib/daemon/machine-type.d.ts +48 -0
  28. package/dist/lib/daemon/machine-type.js +139 -0
  29. package/dist/lib/daemon/migrate.d.ts +61 -0
  30. package/dist/lib/daemon/migrate.js +238 -0
  31. package/dist/lib/daemon/paths.d.ts +22 -0
  32. package/dist/lib/daemon/paths.js +42 -0
  33. package/dist/lib/daemon/remote-control.d.ts +35 -0
  34. package/dist/lib/daemon/remote-control.js +47 -0
  35. package/dist/lib/daemon/requests.d.ts +24 -0
  36. package/dist/lib/daemon/requests.js +63 -0
  37. package/dist/lib/daemon/run.d.ts +41 -0
  38. package/dist/lib/daemon/run.js +319 -0
  39. package/dist/lib/daemon/supervisor.d.ts +87 -0
  40. package/dist/lib/daemon/supervisor.js +523 -0
  41. package/dist/lib/daemon/updater.d.ts +39 -0
  42. package/dist/lib/daemon/updater.js +103 -0
  43. package/dist/lib/flag-registry.d.ts +7 -1
  44. package/dist/lib/flag-registry.js +8 -0
  45. package/dist/lib/lanes/delivery.d.ts +5 -3
  46. package/dist/lib/lanes/delivery.js +14 -6
  47. package/dist/lib/lanes/spawn.d.ts +5 -0
  48. package/dist/lib/lanes/spawn.js +25 -3
  49. package/dist/lib/lanes/types.d.ts +11 -2
  50. package/dist/lib/lanes/types.js +11 -0
  51. package/dist/lib/lanes/watcher.d.ts +2 -2
  52. package/dist/lib/lanes/watcher.js +2 -2
  53. package/dist/lib/mesh/live/backoff.d.ts +2 -2
  54. package/dist/lib/mesh/live/backoff.js +16 -3
  55. package/dist/lib/mesh/live/daemon/run.d.ts +2 -2
  56. package/dist/lib/mesh/live/daemon/run.js +5 -2
  57. package/dist/lib/mesh/live/flush.d.ts +3 -2
  58. package/dist/lib/mesh/live/flush.js +34 -7
  59. package/dist/lib/mesh/live/session-events-client.d.ts +2 -0
  60. package/dist/lib/mesh/live/session-events-client.js +25 -0
  61. package/dist/lib/service-manager/systemd.d.ts +2 -2
  62. package/dist/lib/service-manager/systemd.js +1 -1
  63. package/dist/lib/service-manager/types.d.ts +2 -0
  64. package/dist/main.d.ts +6 -0
  65. package/dist/main.js +53 -13
  66. package/dist/utils/cognito-session.d.ts +4 -0
  67. package/dist/utils/cognito-session.js +11 -10
  68. package/dist/utils/plan-gate-error.d.ts +27 -13
  69. package/dist/utils/plan-gate-error.js +133 -58
  70. package/dist/utils/self-update.d.ts +9 -0
  71. package/dist/utils/self-update.js +15 -2
  72. package/package.json +1 -1
@@ -0,0 +1,554 @@
1
+ /**
2
+ * `hq daemon` — one supervisor for every HQ background service on a machine.
3
+ *
4
+ * hq daemon status (same as `hq daemon status`)
5
+ * hq daemon install detect the machine type, install the one OS unit
6
+ * hq daemon run what the OS unit runs; starts the default services
7
+ * hq daemon status [--json]
8
+ * hq daemon logs [unit] [--follow] [--lines N]
9
+ * hq daemon restart [unit] | trigger <unit>
10
+ * hq daemon enable|disable <service...> per-machine overrides
11
+ * hq daemon migrate list the older units it would take over (read-only)
12
+ * hq daemon uninstall [--purge]
13
+ *
14
+ * Hidden from `hq --help` while it rolls out.
15
+ */
16
+ import * as fs from "node:fs";
17
+ import * as os from "node:os";
18
+ import * as path from "node:path";
19
+ import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices } from "../lib/daemon/catalog.js";
20
+ import { applyServiceToggle, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
21
+ import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
22
+ import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
23
+ import { SYSTEM_UNIT_DIR, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
24
+ import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
25
+ import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
26
+ import { detectMachineType, homeFromPasswd } from "../lib/daemon/machine-type.js";
27
+ import { daemonPaths } from "../lib/daemon/paths.js";
28
+ import { writeControlRequest } from "../lib/daemon/requests.js";
29
+ import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
30
+ class DaemonCommandError extends Error {
31
+ }
32
+ /**
33
+ * The HQ folder install records: --hq-root, else the CLI's shared resolution
34
+ * (HQ_ROOT, the tree around the current folder, the folder the installer or
35
+ * menubar recorded, the well-known locations). Undefined when none resolves.
36
+ */
37
+ export function resolveDaemonHqRoot(flag, where) {
38
+ if (flag)
39
+ return path.resolve(flag);
40
+ try {
41
+ return resolveDefaultHqRoot({ onMissing: "throw", env: where.env, cwd: where.cwd, home: where.home });
42
+ }
43
+ catch {
44
+ // resolveDefaultHqRoot throws only when nothing resolves; install then
45
+ // says the folder is not set.
46
+ return undefined;
47
+ }
48
+ }
49
+ async function loadDefaults(over) {
50
+ const env = over.env ?? process.env;
51
+ const home = over.home ?? os.homedir();
52
+ const platform = over.platform ?? process.platform;
53
+ const needsBinaries = over.nodeBinary === undefined || over.hqBinary === undefined;
54
+ const mesh = needsBinaries || over.windowsUser === undefined ? await import("../lib/mesh/live/daemon/install.js") : null;
55
+ const serviceManager = over.run ? null : await import("../lib/service-manager/index.js");
56
+ const run = over.run ?? serviceManager.defaultRunCommand;
57
+ return {
58
+ env,
59
+ home,
60
+ platform,
61
+ out: over.out ?? ((l) => console.log(l)),
62
+ err: over.err ?? ((l) => console.error(l)),
63
+ run,
64
+ nodeBinary: over.nodeBinary ?? mesh.resolveAbsoluteNodeBinary(),
65
+ hqBinary: over.hqBinary ?? mesh.resolveHqBinary(),
66
+ uid: over.uid ?? (typeof process.getuid === "function" ? process.getuid() : 0),
67
+ isRoot: over.isRoot ?? (typeof process.getuid === "function" && process.getuid() === 0),
68
+ fileExists: over.fileExists ?? fs.existsSync,
69
+ readFile: over.readFile ?? ((p) => fs.readFileSync(p, "utf8")),
70
+ legacyUnits: over.legacyUnits ??
71
+ (() => findLegacyUnits({
72
+ platform,
73
+ home,
74
+ existsSync: fs.existsSync,
75
+ listDir: (dir) => fs.readdirSync(dir),
76
+ windowsTaskExists: (task) => run("schtasks.exe", ["/Query", "/TN", task]).status === 0,
77
+ })),
78
+ resolveHqRoot: over.resolveHqRoot ?? ((flag) => resolveDaemonHqRoot(flag, { env, cwd: process.cwd(), home })),
79
+ userHome: over.userHome ?? ((user) => loginHome(user, run)),
80
+ daemonAlive: over.daemonAlive ?? ((pid) => isDaemonProcess(pid)),
81
+ windowsUser: over.windowsUser ?? (platform === "win32" ? mesh.resolveWindowsUser(env) : ""),
82
+ windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
83
+ systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
84
+ };
85
+ }
86
+ function detect(d, flag, config) {
87
+ return detectMachineType(flag, {
88
+ platform: d.platform,
89
+ env: d.env,
90
+ home: d.home,
91
+ savedType: config.machineType,
92
+ existsSync: d.fileExists,
93
+ readFileSync: d.readFile,
94
+ });
95
+ }
96
+ function describeServices(machineType, config) {
97
+ return enabledServices(machineType, config).map((s) => ` ${s.padEnd(16)} ${SERVICE_DESCRIPTIONS[s]}`);
98
+ }
99
+ function hostFor(d, paths, pathEnv) {
100
+ const env = {};
101
+ if (pathEnv && d.env.PATH)
102
+ env.PATH = d.env.PATH;
103
+ if (d.env.HQ_DAEMON_DIR)
104
+ env.HQ_DAEMON_DIR = d.env.HQ_DAEMON_DIR;
105
+ return {
106
+ platform: d.platform,
107
+ home: d.home,
108
+ nodeBinary: d.nodeBinary,
109
+ hqBinary: d.hqBinary,
110
+ daemonDir: paths.root,
111
+ uid: d.uid,
112
+ isRoot: d.isRoot,
113
+ windowsUser: d.windowsUser ?? "",
114
+ windowsHeadlessLauncher: d.windowsHeadlessLauncher,
115
+ systemUnitDir: d.systemUnitDir ?? SYSTEM_UNIT_DIR,
116
+ env,
117
+ };
118
+ }
119
+ /**
120
+ * The login user's home from the account database: `getent` covers LDAP and
121
+ * other NSS sources, /etc/passwd covers hosts without it. Accounts are not
122
+ * required to live under /home (root, service accounts under /var/lib).
123
+ */
124
+ function loginHome(user, run) {
125
+ const entry = run("getent", ["passwd", user]);
126
+ const fromGetent = entry.status === 0 ? homeFromPasswd(entry.stdout, user) : null;
127
+ if (fromGetent)
128
+ return fromGetent;
129
+ let passwd;
130
+ try {
131
+ passwd = fs.readFileSync("/etc/passwd", "utf8");
132
+ }
133
+ catch (err) {
134
+ throw new DaemonCommandError(`Could not look up the home directory of ${user}: ${err.message}`);
135
+ }
136
+ const fromFile = homeFromPasswd(passwd, user);
137
+ if (!fromFile)
138
+ throw new DaemonCommandError(`The login user ${user} from /etc/hq/machine-type does not exist on this machine.`);
139
+ return fromFile;
140
+ }
141
+ /**
142
+ * Where the daemon's files are for this machine. On boxes the system unit runs
143
+ * as the login user, so its files live in that user's home, not root's.
144
+ */
145
+ function hostPaths(d, flag) {
146
+ const own = daemonPaths(d.home, d.env);
147
+ const detected = detect(d, flag, readDaemonConfig(own.configPath));
148
+ if (installScopeFor(detected.type) !== "system")
149
+ return { paths: own, detected };
150
+ const runAsUser = detected.runAsUser;
151
+ if (!runAsUser) {
152
+ throw new DaemonCommandError("A box install needs the login user the daemon runs as. Provisioning writes it to /etc/hq/machine-type (user=<name>).");
153
+ }
154
+ const paths = daemonPaths(d.userHome(runAsUser), d.env);
155
+ if (flag !== undefined)
156
+ return { paths, detected, runAsUser };
157
+ // Root normally has no saved config; the login user's saved type is the real one.
158
+ const saved = readDaemonConfig(paths.configPath);
159
+ const again = saved.machineType ? detect(d, undefined, saved) : detected;
160
+ return { paths, detected: again, runAsUser: again.runAsUser ?? runAsUser };
161
+ }
162
+ /** hostPaths for the everyday verbs: a box without a known login user falls back to this user's own files. */
163
+ function currentHost(d) {
164
+ try {
165
+ return hostPaths(d, undefined);
166
+ }
167
+ catch (err) {
168
+ if (!(err instanceof DaemonCommandError))
169
+ throw err;
170
+ const paths = daemonPaths(d.home, d.env);
171
+ return { paths, detected: detect(d, undefined, readDaemonConfig(paths.configPath)) };
172
+ }
173
+ }
174
+ /** Files root writes for a box's daemon must stay readable by the login user it runs as. */
175
+ function handOver(d, host) {
176
+ if (host.runAsUser && d.isRoot)
177
+ d.run("chown", ["-R", `${host.runAsUser}:`, host.paths.root]);
178
+ }
179
+ async function runInstall(d, opts) {
180
+ const { paths, detected, runAsUser } = hostPaths(d, opts.machineType);
181
+ const scope = installScopeFor(detected.type);
182
+ const config = {
183
+ ...readDaemonConfig(paths.configPath),
184
+ machineType: detected.type,
185
+ };
186
+ const hqRoot = d.resolveHqRoot(opts.hqRoot) ?? config.hqRoot;
187
+ if (hqRoot)
188
+ config.hqRoot = hqRoot;
189
+ writeDaemonConfig(paths.configPath, config);
190
+ for (const w of detected.warnings ?? [])
191
+ d.err(`warning: ${w}`);
192
+ d.out(`Machine type: ${detected.type} (${detected.detail})`);
193
+ d.out(hqRoot ? `HQ folder: ${hqRoot}` : "HQ folder: not set (sync, lanes and search wait until `hq daemon install --hq-root <path>`)");
194
+ d.out("Services that start automatically:");
195
+ for (const line of describeServices(detected.type, config))
196
+ d.out(line);
197
+ const migratable = d.legacyUnits().filter((u) => u.migratable);
198
+ for (const u of migratable) {
199
+ d.out(`Note: ${describeLegacyUnit(u)} still runs ${u.unitId}. The daemon leaves it alone until \`hq daemon migrate\` takes it over.`);
200
+ }
201
+ const runAsHome = runAsUser ? d.userHome(runAsUser) : undefined;
202
+ const result = installDaemonUnit(hostFor(d, paths, scope === "user"), { scope, runAsUser, runAsHome }, { run: d.run });
203
+ handOver(d, { paths, detected, runAsUser });
204
+ if (result.kind === "manual") {
205
+ d.out(`This platform has no supported service manager. Start the daemon yourself with:\n ${result.manualCommand}`);
206
+ return;
207
+ }
208
+ if (!result.running) {
209
+ throw new DaemonCommandError(`Installed ${result.unitPath}, but it did not start: ${result.error ?? "unknown error"}`);
210
+ }
211
+ d.out(`Installed and started: ${result.unitPath}`);
212
+ }
213
+ function migrateDeps(d, paths) {
214
+ return {
215
+ run: d.run,
216
+ uid: d.uid,
217
+ isRoot: d.isRoot,
218
+ adoptedDir: path.join(paths.root, "adopted"),
219
+ recordPath: path.join(paths.root, "adopted.json"),
220
+ // Box system units: root-owned, outside the login user's daemon folder (which handOver chowns to that user).
221
+ systemAdoptedDir: path.join(SYSTEM_ADOPTED_ROOT, "adopted"),
222
+ systemRecordPath: path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
223
+ systemUnitDir: d.systemUnitDir ?? SYSTEM_UNIT_DIR,
224
+ };
225
+ }
226
+ const SYSTEM_ADOPTED_ROOT = "/var/lib/hq/daemon";
227
+ /** Every unit the daemon took over that has not been given back (the system store only when root can read it). */
228
+ function unrestoredUnits(d, migrate) {
229
+ const own = readAdoptedUnits(migrate.recordPath);
230
+ return d.isRoot ? [...own, ...readAdoptedUnits(migrate.systemRecordPath)] : own;
231
+ }
232
+ async function runUninstall(d, opts) {
233
+ const host = currentHost(d);
234
+ const { paths, detected } = host;
235
+ const migrate = migrateDeps(d, paths);
236
+ const adopted = unrestoredUnits(d, migrate);
237
+ if (opts.purge && !opts.restoreLegacy && adopted.length > 0) {
238
+ throw new DaemonCommandError(`hq daemon took over ${adopted.map((u) => u.unitId).join(", ")}, and their original units are kept in ${migrate.adoptedDir}. ` +
239
+ "Run `hq daemon uninstall --restore-legacy --purge` to give them back first, so --purge does not delete them.");
240
+ }
241
+ const result = uninstallDaemonUnit(hostFor(d, paths, false), { scope: installScopeFor(detected.type) }, { run: d.run });
242
+ d.out(result.removed ? `Removed ${result.unitPath}` : "hq daemon was not installed.");
243
+ if (opts.restoreLegacy) {
244
+ const restored = restoreLegacyUnits(migrate);
245
+ for (const u of restored.restored)
246
+ d.out(`Gave ${u.unitId} back to ${describeLegacyUnit(u)}`);
247
+ for (const f of restored.failed)
248
+ d.err(`Could not give ${f.unit.unitId} back to ${describeLegacyUnit(f.unit)}: ${f.error}`);
249
+ if (restored.restored.length === 0 && restored.failed.length === 0)
250
+ d.out("hq daemon had not taken over any older units.");
251
+ if (restored.failed.length > 0) {
252
+ handOver(d, host);
253
+ throw new DaemonCommandError(`Some units were not restored; they are still listed in ${migrate.recordPath}.`);
254
+ }
255
+ }
256
+ if (opts.purge) {
257
+ fs.rmSync(paths.root, { recursive: true, force: true });
258
+ d.out(`Deleted ${paths.root}`);
259
+ }
260
+ else {
261
+ d.out(`Kept settings in ${paths.configPath}`);
262
+ }
263
+ }
264
+ function readState(paths) {
265
+ try {
266
+ return JSON.parse(fs.readFileSync(paths.statePath, "utf8"));
267
+ }
268
+ catch (err) {
269
+ if (err.code === "ENOENT")
270
+ return null;
271
+ throw new DaemonCommandError(`could not read ${paths.statePath}: ${err.message}`);
272
+ }
273
+ }
274
+ function formatUnit(u) {
275
+ const detail = u.status === "running" && u.pid
276
+ ? `pid ${u.pid}`
277
+ : u.status === "idle" && u.lastResult
278
+ ? `last run ${u.lastResult}`
279
+ : (u.reason ?? "");
280
+ const restarts = u.restarts > 0 ? ` (restarted ${u.restarts}x)` : "";
281
+ return ` ${u.id.padEnd(22)} ${u.status.padEnd(12)} ${detail}${restarts}`;
282
+ }
283
+ async function runStatus(d, opts) {
284
+ const host = currentHost(d);
285
+ const { paths } = host;
286
+ const pid = runningDaemonPid(paths.lockPath, d.daemonAlive);
287
+ const state = pid ? readState(paths) : null;
288
+ const config = readDaemonConfig(paths.configPath);
289
+ if (opts.json) {
290
+ d.out(JSON.stringify({ running: pid !== null, pid, machineType: state?.machineType ?? config.machineType ?? null, units: state?.units ?? [] }, null, 2));
291
+ return;
292
+ }
293
+ if (!pid || !state) {
294
+ const { detected } = host;
295
+ d.out(`hq daemon is not running. Machine type: ${detected.type} (${detected.detail}).`);
296
+ d.out("Default services for this machine:");
297
+ for (const line of describeServices(detected.type, config))
298
+ d.out(line);
299
+ return;
300
+ }
301
+ d.out(`hq daemon is running (pid ${pid}) on a ${state.machineType} machine.`);
302
+ for (const u of state.units)
303
+ d.out(formatUnit(u));
304
+ }
305
+ function tailLines(file, lines) {
306
+ const text = fs.readFileSync(file, "utf8");
307
+ return text.split("\n").filter((l, i, all) => i < all.length - 1 || l !== "").slice(-lines);
308
+ }
309
+ async function runLogs(d, unit, opts) {
310
+ const { paths } = currentHost(d);
311
+ const file = unit ? paths.unitLogPath(unit) : paths.supervisorLogPath;
312
+ if (!fs.existsSync(file))
313
+ throw new DaemonCommandError(`No log yet at ${file}`);
314
+ const n = Number(opts.lines ?? 50);
315
+ for (const line of tailLines(file, Number.isFinite(n) && n > 0 ? n : 50))
316
+ d.out(line);
317
+ if (!opts.follow)
318
+ return;
319
+ let offset = fs.statSync(file).size;
320
+ await new Promise(() => {
321
+ setInterval(() => {
322
+ const size = fs.statSync(file).size;
323
+ if (size < offset)
324
+ offset = 0;
325
+ if (size === offset)
326
+ return;
327
+ const fd = fs.openSync(file, "r");
328
+ const buf = Buffer.alloc(size - offset);
329
+ fs.readSync(fd, buf, 0, buf.length, offset);
330
+ fs.closeSync(fd);
331
+ offset = size;
332
+ process.stdout.write(buf.toString("utf8"));
333
+ }, 500);
334
+ });
335
+ }
336
+ function queueRequest(d, request, what) {
337
+ const host = currentHost(d);
338
+ const { paths } = host;
339
+ writeControlRequest(paths.requestsDir, request);
340
+ handOver(d, host);
341
+ const running = runningDaemonPid(paths.lockPath, d.daemonAlive) !== null;
342
+ d.out(running ? `Asked the daemon to ${what}.` : `Queued "${what}"; the daemon is not running, so it applies when it starts.`);
343
+ }
344
+ async function runToggle(d, action, services) {
345
+ const host = currentHost(d);
346
+ const { paths } = host;
347
+ const config = readDaemonConfig(paths.configPath);
348
+ const machineType = host.detected.type;
349
+ const next = applyServiceToggle(config, action, services, DEFAULT_SERVICES[machineType]);
350
+ writeDaemonConfig(paths.configPath, next);
351
+ handOver(d, host);
352
+ d.out(`Services for this ${machineType} machine:`);
353
+ for (const line of describeServices(machineType, next))
354
+ d.out(line);
355
+ d.out("A running daemon picks this up within 15 seconds.");
356
+ }
357
+ async function runMigrate(d, opts) {
358
+ const units = d.legacyUnits();
359
+ if (units.length === 0) {
360
+ d.out("No older HQ background units found. Nothing to migrate.");
361
+ return;
362
+ }
363
+ if (opts.apply) {
364
+ const host = currentHost(d);
365
+ // Stopping the older units with nothing installed to replace them would
366
+ // leave their services off.
367
+ const installed = daemonUnitInstalled({
368
+ platform: d.platform,
369
+ home: d.home,
370
+ systemUnitDir: d.systemUnitDir,
371
+ windowsTaskExists: (task) => d.run("schtasks.exe", ["/Query", "/TN", task]).status === 0,
372
+ });
373
+ if (!installed) {
374
+ throw new DaemonCommandError("hq daemon is not installed here yet. Run `hq daemon install` first, then `hq daemon migrate --apply`.");
375
+ }
376
+ const result = adoptLegacyUnits(units, migrateDeps(d, host.paths));
377
+ handOver(d, host);
378
+ for (const a of result.adopted)
379
+ d.out(`Took over ${a.unitId} from ${describeLegacyUnit(a)}`);
380
+ for (const s of result.skipped)
381
+ d.out(`Left ${s.unit.unitId} to ${describeLegacyUnit(s.unit)}: ${s.reason}`);
382
+ for (const f of result.failed)
383
+ d.err(`Could not take over ${f.unit.unitId} from ${describeLegacyUnit(f.unit)}: ${f.error}`);
384
+ if (result.adopted.length > 0) {
385
+ d.out("The daemon starts these services within a minute. `hq daemon uninstall --restore-legacy` puts the old units back.");
386
+ }
387
+ if (result.failed.length > 0)
388
+ throw new DaemonCommandError("Some units could not be taken over; they are still running as before.");
389
+ return;
390
+ }
391
+ for (const u of units) {
392
+ if (u.migratable)
393
+ d.out(`Would take over ${u.unitId} from ${describeLegacyUnit(u)}${u.path ? ` (${u.path})` : ""}`);
394
+ else
395
+ d.out(`Leaving ${u.unitId} to ${describeLegacyUnit(u)} (owned by the HQ desktop app)`);
396
+ }
397
+ d.out("Nothing was changed. `hq daemon migrate --apply` takes them over.");
398
+ }
399
+ async function runServiceSelfUpdate(d) {
400
+ const [{ daemonSelfUpdate }, { npmCodexUpdater, runDaemonUpdater, updaterEnv }, { cliSelfUpdatable }] = await Promise.all([
401
+ import("../utils/self-update.js"),
402
+ import("../lib/daemon/updater.js"),
403
+ import("../lib/daemon/run.js"),
404
+ ]);
405
+ // Same guard as the plan: never replace a dev checkout or a project's copy with a global install.
406
+ if (!cliSelfUpdatable()) {
407
+ d.out("This hq CLI is not a global install; nothing to update here.");
408
+ return;
409
+ }
410
+ const { paths } = currentHost(d);
411
+ const result = await runDaemonUpdater({
412
+ updateCli: () => daemonSelfUpdate({ env: updaterEnv(d.env) }),
413
+ codex: npmCodexUpdater(),
414
+ requestDaemonRestart: () => writeControlRequest(paths.requestsDir, { action: "restart-daemon" }),
415
+ log: d.out,
416
+ });
417
+ if (!result.ok)
418
+ process.exitCode = 1;
419
+ }
420
+ async function runServiceRemoteControl(d, opts) {
421
+ const { runRemoteControlHost } = await import("../lib/daemon/remote-control.js");
422
+ const { findOnPath } = await import("../lib/daemon/run.js");
423
+ const hqRoot = d.env.HQ_ROOT?.trim();
424
+ if (!hqRoot)
425
+ throw new DaemonCommandError("remote-control needs HQ_ROOT (the daemon sets it from the saved HQ folder)");
426
+ const tmux = findOnPath("tmux", d.env);
427
+ if (!tmux)
428
+ throw new DaemonCommandError("tmux is not installed");
429
+ const controller = new AbortController();
430
+ const stop = () => controller.abort();
431
+ process.once("SIGTERM", stop);
432
+ process.once("SIGINT", stop);
433
+ try {
434
+ process.exitCode = await runRemoteControlHost({
435
+ tmux,
436
+ claude: opts.claude,
437
+ hqRoot,
438
+ name: opts.name,
439
+ permissionMode: opts.permissionMode,
440
+ run: d.run,
441
+ pollMs: 5_000,
442
+ signal: controller.signal,
443
+ sleep: (ms) => new Promise((resolve) => {
444
+ const timer = setTimeout(resolve, ms);
445
+ controller.signal.addEventListener("abort", () => {
446
+ clearTimeout(timer);
447
+ resolve();
448
+ }, { once: true });
449
+ }),
450
+ log: d.out,
451
+ });
452
+ }
453
+ finally {
454
+ process.off("SIGTERM", stop);
455
+ process.off("SIGINT", stop);
456
+ }
457
+ }
458
+ export function registerDaemonCommand(program, over = {}) {
459
+ let resolved = null;
460
+ const deps = () => (resolved ??= loadDefaults(over));
461
+ const wrap = (fn) => async (...args) => {
462
+ const d = await deps();
463
+ try {
464
+ await fn(d, ...args);
465
+ }
466
+ catch (err) {
467
+ d.err(err instanceof Error ? err.message : String(err));
468
+ process.exitCode = 1;
469
+ }
470
+ };
471
+ const daemon = program
472
+ .command("daemon", { hidden: true })
473
+ .description("Run all of this machine's HQ background services under one supervisor")
474
+ .action(wrap((d) => runStatus(d, {})));
475
+ daemon
476
+ .command("install")
477
+ .description("Detect this machine's type, install the daemon, and start its default services")
478
+ .option("--machine-type <type>", "desktop | self-hosted | outpost | agent-box | agent-kit (skips detection)")
479
+ .option("--hq-root <path>", "HQ folder the services work against")
480
+ .action(wrap((d, opts) => runInstall(d, opts)));
481
+ daemon
482
+ .command("uninstall")
483
+ .description("Stop the daemon and remove its OS unit")
484
+ .option("--purge", "Also delete ~/.hq/daemon (settings, state and logs)")
485
+ .option("--restore-legacy", "Give the services `hq daemon migrate --apply` took over back to their original units")
486
+ .action(wrap((d, opts) => runUninstall(d, opts)));
487
+ daemon
488
+ .command("run")
489
+ .description("Run the supervisor (this is what the OS unit starts)")
490
+ .option("--foreground", "Also print the supervisor log to the terminal")
491
+ .option("--daemon-dir <path>", "Use this folder instead of ~/.hq/daemon (how the Windows task passes HQ_DAEMON_DIR)")
492
+ .option("--managed", "Started by the OS unit, which starts the daemon again whenever it exits")
493
+ .action(wrap(async (d, opts) => {
494
+ const { runDaemon } = await import("../lib/daemon/run.js");
495
+ // Services inherit this environment, so they see the same folder.
496
+ const env = opts.daemonDir ? { ...d.env, HQ_DAEMON_DIR: opts.daemonDir } : d.env;
497
+ process.exitCode = await runDaemon({
498
+ foreground: opts.foreground,
499
+ managed: opts.managed,
500
+ home: d.home,
501
+ env,
502
+ nodeBinary: d.nodeBinary,
503
+ hqBinary: d.hqBinary,
504
+ });
505
+ }));
506
+ daemon
507
+ .command("status")
508
+ .description("Show the machine type and each service's state")
509
+ .option("--json", "Print machine-readable JSON")
510
+ .action(wrap((d, opts) => runStatus(d, opts)));
511
+ daemon
512
+ .command("logs [unit]")
513
+ .description("Print the supervisor log, or one service's log")
514
+ .option("-f, --follow", "Keep printing new lines")
515
+ .option("-n, --lines <n>", "How many lines to print first", "50")
516
+ .action(wrap((d, unit, opts) => runLogs(d, unit, opts)));
517
+ daemon
518
+ .command("restart [unit]")
519
+ .description("Restart one service, or every running service")
520
+ .action(wrap(async (d, unit) => queueRequest(d, unit ? { action: "restart", unit } : { action: "restart" }, unit ? `restart ${unit}` : "restart all services")));
521
+ daemon
522
+ .command("trigger <unit>")
523
+ .description("Run a scheduled service now (for example search-index)")
524
+ .action(wrap(async (d, unit) => queueRequest(d, { action: "trigger", unit }, `run ${unit} now`)));
525
+ daemon
526
+ .command("enable <services...>")
527
+ .description("Turn services on for this machine, beyond its defaults")
528
+ .action(wrap((d, services) => runToggle(d, "enable", services)));
529
+ daemon
530
+ .command("disable <services...>")
531
+ .description("Turn default services off for this machine")
532
+ .action(wrap((d, services) => runToggle(d, "disable", services)));
533
+ daemon
534
+ .command("migrate")
535
+ .description("List the older HQ background units the daemon would take over; --apply takes them over")
536
+ .option("--apply", "Stop the older units and let the daemon run their services")
537
+ .action(wrap((d, opts) => runMigrate(d, opts)));
538
+ const service = daemon
539
+ .command("service", { hidden: true })
540
+ .description("Entry points the daemon runs for services that have no command of their own");
541
+ service
542
+ .command("self-update")
543
+ .description("Update the hq CLI (and Codex), then restart the daemon onto the new version")
544
+ .action(wrap((d) => runServiceSelfUpdate(d)));
545
+ service
546
+ .command("remote-control")
547
+ .description("Keep `claude remote-control` running in a tmux session in the HQ folder")
548
+ .requiredOption("--claude <path>", "claude binary")
549
+ .requiredOption("--name <name>", "remote-control session name")
550
+ .option("--permission-mode <mode>", "claude --permission-mode")
551
+ .action(wrap((d, opts) => runServiceRemoteControl(d, opts)));
552
+ return daemon;
553
+ }
554
+ //# sourceMappingURL=daemon.js.map
@@ -35,6 +35,7 @@ import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
35
35
  import { ensureSessionHome } from "../lib/lanes/session-relink.js";
36
36
  import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
37
37
  import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
38
+ import { handOffInstallHere } from "../lib/daemon/handoff.js";
38
39
  export { spawnLaneWorker };
39
40
  export { questionBodyAbsPath };
40
41
  export { MAX_LANE_DEPTH };
@@ -3830,7 +3831,9 @@ function laneWithRecoveredProviderSession(hqRoot, lane) {
3830
3831
  }
3831
3832
  /**
3832
3833
  * `hq lanes resume <lane-id> [--text <msg>]`: the explicit opt-in that
3833
- * continues a lane in `interrupted`, `done` or `blocked` on its recorded
3834
+ * continues a lane in `interrupted`, `done` or `blocked`, or a failed lane
3835
+ * whose only terminal reason is an invalid envelope after its repair turn,
3836
+ * on its recorded
3834
3837
  * provider session. `hq lanes message` keeps refusing `interrupted`; this
3835
3838
  * command is the path its refusal names. The launch is the same
3836
3839
  * `spawnLaneWorker` a message resume uses, so the resume-time compaction
@@ -5080,6 +5083,12 @@ async function runReconcile(ctx, opts) {
5080
5083
  }
5081
5084
  }
5082
5085
  async function runReconcileInstall(ctx, opts) {
5086
+ // `hq daemon` runs lanes-reconcile on this machine; a separate timer would run it twice.
5087
+ const handedOff = opts.systemdRoot ? null : handOffInstallHere(["lanes-reconcile"]);
5088
+ if (handedOff) {
5089
+ printJson({ ok: true, action: "hq-daemon", message: handedOff });
5090
+ return;
5091
+ }
5083
5092
  const callerCwd = ctx.callerCwd();
5084
5093
  const systemdRootRaw = opts.systemdRoot ?? ctx.systemdRoot ?? defaultSystemdUserDir(ctx.homedir());
5085
5094
  const systemdRoot = opts.systemdRoot !== undefined