@indigoai-us/hq-cli 5.162.0 → 5.164.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/command-catalog.generated.d.ts +188 -0
- package/dist/command-catalog.generated.js +235 -0
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +6 -1
- package/dist/commands/agent-enroll.js +30 -3
- package/dist/commands/agents.js +15 -22
- package/dist/commands/bot.js +56 -6
- package/dist/commands/daemon.d.ts +53 -0
- package/dist/commands/daemon.js +554 -0
- package/dist/commands/lanes.js +7 -0
- package/dist/commands/meetings.js +18 -16
- package/dist/commands/mesh.js +9 -0
- package/dist/lib/daemon/catalog.d.ts +85 -0
- package/dist/lib/daemon/catalog.js +274 -0
- package/dist/lib/daemon/config.d.ts +26 -0
- package/dist/lib/daemon/config.js +85 -0
- package/dist/lib/daemon/handoff.d.ts +53 -0
- package/dist/lib/daemon/handoff.js +136 -0
- package/dist/lib/daemon/install.d.ts +76 -0
- package/dist/lib/daemon/install.js +299 -0
- package/dist/lib/daemon/legacy.d.ts +35 -0
- package/dist/lib/daemon/legacy.js +120 -0
- package/dist/lib/daemon/lock.d.ts +47 -0
- package/dist/lib/daemon/lock.js +138 -0
- package/dist/lib/daemon/machine-type.d.ts +48 -0
- package/dist/lib/daemon/machine-type.js +139 -0
- package/dist/lib/daemon/migrate.d.ts +61 -0
- package/dist/lib/daemon/migrate.js +238 -0
- package/dist/lib/daemon/paths.d.ts +22 -0
- package/dist/lib/daemon/paths.js +42 -0
- package/dist/lib/daemon/remote-control.d.ts +35 -0
- package/dist/lib/daemon/remote-control.js +47 -0
- package/dist/lib/daemon/requests.d.ts +24 -0
- package/dist/lib/daemon/requests.js +63 -0
- package/dist/lib/daemon/run.d.ts +41 -0
- package/dist/lib/daemon/run.js +319 -0
- package/dist/lib/daemon/supervisor.d.ts +87 -0
- package/dist/lib/daemon/supervisor.js +523 -0
- package/dist/lib/daemon/updater.d.ts +39 -0
- package/dist/lib/daemon/updater.js +103 -0
- package/dist/lib/flag-registry.d.ts +7 -1
- package/dist/lib/flag-registry.js +8 -0
- package/dist/lib/mesh/live/backoff.d.ts +2 -2
- package/dist/lib/mesh/live/backoff.js +16 -3
- package/dist/lib/mesh/live/daemon/run.d.ts +2 -2
- package/dist/lib/mesh/live/daemon/run.js +5 -2
- package/dist/lib/mesh/live/flush.d.ts +3 -2
- package/dist/lib/mesh/live/flush.js +34 -7
- package/dist/lib/mesh/live/session-events-client.d.ts +2 -0
- package/dist/lib/mesh/live/session-events-client.js +25 -0
- package/dist/lib/service-manager/systemd.d.ts +2 -2
- package/dist/lib/service-manager/systemd.js +1 -1
- package/dist/lib/service-manager/types.d.ts +2 -0
- package/dist/main.d.ts +6 -0
- package/dist/main.js +53 -13
- package/dist/sentry.js +5 -1
- package/dist/startup-registration.d.ts +3 -1
- package/dist/startup-registration.js +25 -4
- package/dist/utils/cognito-session.d.ts +4 -0
- package/dist/utils/cognito-session.js +11 -10
- package/dist/utils/install-tree-torn.d.ts +29 -10
- package/dist/utils/install-tree-torn.js +262 -29
- package/dist/utils/plan-gate-error.d.ts +27 -13
- package/dist/utils/plan-gate-error.js +133 -58
- package/dist/utils/self-update.d.ts +9 -0
- package/dist/utils/self-update.js +15 -2
- package/dist/utils/sentry-fingerprint.d.ts +7 -1
- package/dist/utils/sentry-fingerprint.js +95 -15
- package/package.json +1 -1
|
@@ -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
|
package/dist/commands/lanes.js
CHANGED
|
@@ -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 };
|
|
@@ -5082,6 +5083,12 @@ async function runReconcile(ctx, opts) {
|
|
|
5082
5083
|
}
|
|
5083
5084
|
}
|
|
5084
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
|
+
}
|
|
5085
5092
|
const callerCwd = ctx.callerCwd();
|
|
5086
5093
|
const systemdRootRaw = opts.systemdRoot ?? ctx.systemdRoot ?? defaultSystemdUserDir(ctx.homedir());
|
|
5087
5094
|
const systemdRoot = opts.systemdRoot !== undefined
|
|
@@ -2,6 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
4
4
|
import { vaultApiFetch, getCompanyUid, resolveCallerPersonUid, } from "../utils/vault-api.js";
|
|
5
|
+
import { isPlanGateError, planGateErrorFromPayload, } from "../utils/plan-gate-error.js";
|
|
5
6
|
function formatDuration(seconds) {
|
|
6
7
|
const h = Math.floor(seconds / 3600);
|
|
7
8
|
const m = Math.floor((seconds % 3600) / 60);
|
|
@@ -98,6 +99,9 @@ async function resolveShortId(token, prefix, query) {
|
|
|
98
99
|
}
|
|
99
100
|
async function handleApiError(res, json = false) {
|
|
100
101
|
const body = (await res.json().catch(() => ({})));
|
|
102
|
+
const planGate = planGateErrorFromPayload(res.status, body);
|
|
103
|
+
if (planGate)
|
|
104
|
+
throw planGate;
|
|
101
105
|
if (json) {
|
|
102
106
|
console.log(JSON.stringify(body, null, 2));
|
|
103
107
|
}
|
|
@@ -119,6 +123,12 @@ async function handleApiError(res, json = false) {
|
|
|
119
123
|
}
|
|
120
124
|
process.exit(1);
|
|
121
125
|
}
|
|
126
|
+
function handleMeetingsCommandError(err) {
|
|
127
|
+
if (isPlanGateError(err))
|
|
128
|
+
throw err;
|
|
129
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
122
132
|
function printMeetingTable(meetings) {
|
|
123
133
|
if (meetings.length === 0) {
|
|
124
134
|
console.log(chalk.dim(" No meetings found."));
|
|
@@ -212,8 +222,7 @@ export function registerMeetingsCommand(program) {
|
|
|
212
222
|
console.log();
|
|
213
223
|
}
|
|
214
224
|
catch (err) {
|
|
215
|
-
|
|
216
|
-
process.exit(1);
|
|
225
|
+
handleMeetingsCommandError(err);
|
|
217
226
|
}
|
|
218
227
|
});
|
|
219
228
|
// ── hq meetings invite <meeting-url> ──────────────────────────────
|
|
@@ -266,8 +275,7 @@ export function registerMeetingsCommand(program) {
|
|
|
266
275
|
console.log();
|
|
267
276
|
}
|
|
268
277
|
catch (err) {
|
|
269
|
-
|
|
270
|
-
process.exit(1);
|
|
278
|
+
handleMeetingsCommandError(err);
|
|
271
279
|
}
|
|
272
280
|
});
|
|
273
281
|
meetings
|
|
@@ -302,8 +310,7 @@ export function registerMeetingsCommand(program) {
|
|
|
302
310
|
console.log();
|
|
303
311
|
}
|
|
304
312
|
catch (err) {
|
|
305
|
-
|
|
306
|
-
process.exit(1);
|
|
313
|
+
handleMeetingsCommandError(err);
|
|
307
314
|
}
|
|
308
315
|
});
|
|
309
316
|
// ── hq meetings get <id> ──────────────────────────────────────────
|
|
@@ -374,8 +381,7 @@ export function registerMeetingsCommand(program) {
|
|
|
374
381
|
console.log();
|
|
375
382
|
}
|
|
376
383
|
catch (err) {
|
|
377
|
-
|
|
378
|
-
process.exit(1);
|
|
384
|
+
handleMeetingsCommandError(err);
|
|
379
385
|
}
|
|
380
386
|
});
|
|
381
387
|
// ── hq meetings set-company <id> ──────────────────────────────────
|
|
@@ -429,8 +435,7 @@ export function registerMeetingsCommand(program) {
|
|
|
429
435
|
console.log();
|
|
430
436
|
}
|
|
431
437
|
catch (err) {
|
|
432
|
-
|
|
433
|
-
process.exit(1);
|
|
438
|
+
handleMeetingsCommandError(err);
|
|
434
439
|
}
|
|
435
440
|
});
|
|
436
441
|
// ── hq meetings search <query> ─────────────────────────────────────
|
|
@@ -461,8 +466,7 @@ export function registerMeetingsCommand(program) {
|
|
|
461
466
|
console.log();
|
|
462
467
|
}
|
|
463
468
|
catch (err) {
|
|
464
|
-
|
|
465
|
-
process.exit(1);
|
|
469
|
+
handleMeetingsCommandError(err);
|
|
466
470
|
}
|
|
467
471
|
});
|
|
468
472
|
// ── hq meetings transcript <id> ────────────────────────────────────
|
|
@@ -531,8 +535,7 @@ export function registerMeetingsCommand(program) {
|
|
|
531
535
|
}
|
|
532
536
|
}
|
|
533
537
|
catch (err) {
|
|
534
|
-
|
|
535
|
-
process.exit(1);
|
|
538
|
+
handleMeetingsCommandError(err);
|
|
536
539
|
}
|
|
537
540
|
});
|
|
538
541
|
// ── hq meetings notes <id> ─────────────────────────────────────────
|
|
@@ -640,8 +643,7 @@ export function registerMeetingsCommand(program) {
|
|
|
640
643
|
console.log();
|
|
641
644
|
}
|
|
642
645
|
catch (err) {
|
|
643
|
-
|
|
644
|
-
process.exit(1);
|
|
646
|
+
handleMeetingsCommandError(err);
|
|
645
647
|
}
|
|
646
648
|
});
|
|
647
649
|
}
|