@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +26 -0
  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 +7 -0
  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/mesh/live/backoff.d.ts +2 -2
  46. package/dist/lib/mesh/live/backoff.js +16 -3
  47. package/dist/lib/mesh/live/daemon/run.d.ts +2 -2
  48. package/dist/lib/mesh/live/daemon/run.js +5 -2
  49. package/dist/lib/mesh/live/flush.d.ts +3 -2
  50. package/dist/lib/mesh/live/flush.js +34 -7
  51. package/dist/lib/mesh/live/session-events-client.d.ts +2 -0
  52. package/dist/lib/mesh/live/session-events-client.js +25 -0
  53. package/dist/lib/service-manager/systemd.d.ts +2 -2
  54. package/dist/lib/service-manager/systemd.js +1 -1
  55. package/dist/lib/service-manager/types.d.ts +2 -0
  56. package/dist/main.d.ts +6 -0
  57. package/dist/main.js +53 -13
  58. package/dist/sentry.js +5 -1
  59. package/dist/startup-registration.d.ts +3 -1
  60. package/dist/startup-registration.js +25 -4
  61. package/dist/utils/cognito-session.d.ts +4 -0
  62. package/dist/utils/cognito-session.js +11 -10
  63. package/dist/utils/install-tree-torn.d.ts +29 -10
  64. package/dist/utils/install-tree-torn.js +262 -29
  65. package/dist/utils/plan-gate-error.d.ts +27 -13
  66. package/dist/utils/plan-gate-error.js +133 -58
  67. package/dist/utils/self-update.d.ts +9 -0
  68. package/dist/utils/self-update.js +15 -2
  69. package/dist/utils/sentry-fingerprint.d.ts +7 -1
  70. package/dist/utils/sentry-fingerprint.js +95 -15
  71. package/package.json +1 -1
@@ -52,6 +52,7 @@ import { resolveCompany } from "../lib/work-context/company.js";
52
52
  import { deriveRemoteOwnerSlug, deriveRepoIdentityKey, } from "../lib/work-context/repo-remote.js";
53
53
  import { promptRepoCompany, } from "../lib/work-context/repo-prompt.js";
54
54
  import { requeueQuarantinedOutbox } from "../lib/work-context/outbox.js";
55
+ import { handOffInstallHere } from "../lib/daemon/handoff.js";
55
56
  export function formatCheckLines(threads, company, projectId) {
56
57
  if (threads.length === 0) {
57
58
  return ["Work mesh: no active project threads found."];
@@ -2047,6 +2048,14 @@ async function runDaemonRun(opts = {}) {
2047
2048
  });
2048
2049
  }
2049
2050
  async function runDaemonInstall(opts) {
2051
+ // `hq daemon` runs mesh on this machine: installing a second unit would run it twice.
2052
+ // An explicit --hq-root is verified the same way as below and saved for the daemon.
2053
+ const explicitRoot = opts.hqRoot ? resolveInstallHqRoot({ explicit: opts.hqRoot }).root : undefined;
2054
+ const handedOff = opts.systemUser ? null : handOffInstallHere(["mesh"], process.env, { hqRoot: explicitRoot });
2055
+ if (handedOff) {
2056
+ console.log(opts.json ? JSON.stringify({ ok: true, action: "hq-daemon", reinstalled: false, message: handedOff }) : handedOff);
2057
+ return;
2058
+ }
2050
2059
  // Verified root only: a repo's own .claude/ must never become the daemon's
2051
2060
  // HQ root (that path is persisted as trusted and used for attribution).
2052
2061
  const hqRoot = resolveInstallHqRoot({ explicit: opts.hqRoot });
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The `hq daemon` service catalog: every background service HQ runs, the
3
+ * default list for each machine type, and the concrete unit each service
4
+ * becomes on this host.
5
+ *
6
+ * Services run today's existing entrypoints (`hq mesh daemon run`,
7
+ * `hq bot run <name>`, `hq-sync-runner --watch`, ...) as child processes, so
8
+ * moving a service under the daemon does not change what it does. Services
9
+ * with no entrypoint yet stay in the catalog as `unavailable` so status shows
10
+ * them instead of silently dropping them.
11
+ */
12
+ import type { MachineType } from "./machine-type.js";
13
+ export declare const SERVICE_NAMES: readonly ["sync", "mesh", "heartbeat", "inbox", "bots", "remote-control", "session-host", "lanes-reconcile", "search-index", "jobs-reconcile", "updater", "housekeeping"];
14
+ export type ServiceName = (typeof SERVICE_NAMES)[number];
15
+ export declare function isServiceName(value: unknown): value is ServiceName;
16
+ export declare const SERVICE_DESCRIPTIONS: Record<ServiceName, string>;
17
+ /** The services each machine type runs without being asked. */
18
+ export declare const DEFAULT_SERVICES: Record<MachineType, readonly ServiceName[]>;
19
+ /** Defaults plus this host's overrides, in catalog order. Unknown names are ignored. */
20
+ export declare function enabledServices(machineType: MachineType, overrides: {
21
+ enable: readonly string[];
22
+ disable: readonly string[];
23
+ }): ServiceName[];
24
+ export type UnitCommand = {
25
+ type: "hq";
26
+ args: string[];
27
+ } | {
28
+ type: "node";
29
+ script: string;
30
+ args: string[];
31
+ } | {
32
+ type: "exec";
33
+ file: string;
34
+ args: string[];
35
+ } | {
36
+ type: "internal";
37
+ task: "rotate-logs";
38
+ };
39
+ export interface UnitBlock {
40
+ /** `waiting` clears on its own once the requirement appears; `unavailable` needs a newer CLI. */
41
+ state: "waiting" | "unavailable";
42
+ reason: string;
43
+ }
44
+ export interface UnitPlan {
45
+ /** Service name, or `<service>:<instance>` for per-instance services (bots). */
46
+ id: string;
47
+ service: ServiceName;
48
+ description: string;
49
+ kind: "long-running" | "scheduled";
50
+ /** Long-running units: restart after any exit, or only after a failure. */
51
+ restart: "always" | "on-failure";
52
+ intervalSec?: number;
53
+ initialDelaySec?: number;
54
+ command: UnitCommand | null;
55
+ env: Record<string, string>;
56
+ blocked?: UnitBlock;
57
+ }
58
+ export interface PlanContext {
59
+ machineType: MachineType;
60
+ platform: NodeJS.Platform;
61
+ home: string;
62
+ hqRoot: string | undefined;
63
+ /** hq-cloud's sync-runner.js, or null when it cannot be found. */
64
+ syncRunnerPath: string | null;
65
+ signedIn: boolean;
66
+ /** ~/.hq-agent/kit.json exists. */
67
+ kitConfigured: boolean;
68
+ /** The HQ desktop app runs its own sync on this machine. */
69
+ desktopAppManagesSync: boolean;
70
+ bots: readonly string[];
71
+ /** Name of a pre-daemon unit that still runs this unit id, or null. */
72
+ legacyOwner: (unitId: string) => string | null;
73
+ /** This CLI is a global install the updater can replace (not a project dependency). */
74
+ cliSelfUpdatable: boolean;
75
+ /** `<hqRoot>/core/scripts/outpost-jobs-reconcile.sh` when it exists. */
76
+ jobsReconcileScript: string | null;
77
+ /** Absolute paths of the claude and tmux binaries, or null when not installed. */
78
+ claudePath: string | null;
79
+ tmuxPath: string | null;
80
+ /** Short host name, used to name this machine's remote-control session. */
81
+ hostName: string;
82
+ }
83
+ /** Turn enabled services into concrete units for this host. */
84
+ export declare function planUnits(services: readonly ServiceName[], ctx: PlanContext): UnitPlan[];
85
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1,274 @@
1
+ /**
2
+ * The `hq daemon` service catalog: every background service HQ runs, the
3
+ * default list for each machine type, and the concrete unit each service
4
+ * becomes on this host.
5
+ *
6
+ * Services run today's existing entrypoints (`hq mesh daemon run`,
7
+ * `hq bot run <name>`, `hq-sync-runner --watch`, ...) as child processes, so
8
+ * moving a service under the daemon does not change what it does. Services
9
+ * with no entrypoint yet stay in the catalog as `unavailable` so status shows
10
+ * them instead of silently dropping them.
11
+ */
12
+ export const SERVICE_NAMES = [
13
+ "sync",
14
+ "mesh",
15
+ "heartbeat",
16
+ "inbox",
17
+ "bots",
18
+ "remote-control",
19
+ "session-host",
20
+ "lanes-reconcile",
21
+ "search-index",
22
+ "jobs-reconcile",
23
+ "updater",
24
+ "housekeeping",
25
+ ];
26
+ export function isServiceName(value) {
27
+ return typeof value === "string" && SERVICE_NAMES.includes(value);
28
+ }
29
+ export const SERVICE_DESCRIPTIONS = {
30
+ sync: "keeps company vaults in sync with the cloud",
31
+ mesh: "Work Mesh Live presence",
32
+ heartbeat: "reports this machine's status to HQ",
33
+ inbox: "picks up the agent's incoming messages and tasks",
34
+ bots: "runs local bots",
35
+ "remote-control": "keeps Claude remote control running",
36
+ "session-host": "lets the desktop app open this machine's Claude sessions",
37
+ "lanes-reconcile": "cleans up stalled background lanes",
38
+ "search-index": "keeps local search up to date",
39
+ "jobs-reconcile": "keeps scheduled jobs matched to the cloud",
40
+ updater: "keeps the hq CLI and Codex up to date",
41
+ housekeeping: "rotates daemon logs",
42
+ };
43
+ /** The services each machine type runs without being asked. */
44
+ export const DEFAULT_SERVICES = {
45
+ desktop: ["sync", "mesh", "heartbeat", "bots", "lanes-reconcile", "search-index", "housekeeping"],
46
+ "self-hosted": [
47
+ "sync",
48
+ "mesh",
49
+ "heartbeat",
50
+ "bots",
51
+ "session-host",
52
+ "lanes-reconcile",
53
+ "search-index",
54
+ "updater",
55
+ "housekeeping",
56
+ ],
57
+ outpost: [
58
+ "sync",
59
+ "mesh",
60
+ "heartbeat",
61
+ "remote-control",
62
+ "session-host",
63
+ "lanes-reconcile",
64
+ "search-index",
65
+ "jobs-reconcile",
66
+ "updater",
67
+ "housekeeping",
68
+ ],
69
+ "agent-box": ["mesh", "heartbeat", "inbox", "search-index", "updater", "housekeeping"],
70
+ "agent-kit": ["heartbeat", "inbox", "updater", "housekeeping"],
71
+ };
72
+ /** Defaults plus this host's overrides, in catalog order. Unknown names are ignored. */
73
+ export function enabledServices(machineType, overrides) {
74
+ const defaults = DEFAULT_SERVICES[machineType];
75
+ return SERVICE_NAMES.filter((s) => (defaults.includes(s) || overrides.enable.includes(s)) && !overrides.disable.includes(s));
76
+ }
77
+ function base(service, over = {}) {
78
+ return {
79
+ id: service,
80
+ service,
81
+ description: SERVICE_DESCRIPTIONS[service],
82
+ kind: "long-running",
83
+ restart: "always",
84
+ command: null,
85
+ env: {},
86
+ ...over,
87
+ };
88
+ }
89
+ function unavailable(service, reason, over = {}) {
90
+ return base(service, { ...over, command: null, blocked: { state: "unavailable", reason } });
91
+ }
92
+ function firstBlock(checks) {
93
+ const hit = checks.find(([blocked]) => blocked);
94
+ return hit ? { state: "waiting", reason: hit[1] } : undefined;
95
+ }
96
+ const NO_HQ_ROOT = "no HQ folder configured (run `hq daemon install --hq-root <path>`)";
97
+ const NOT_SIGNED_IN = "not signed in (run `hq login`)";
98
+ const KIT_NOT_SET_UP = "the agent kit is not set up on this machine (run `hq agent kit install`)";
99
+ function planService(service, ctx) {
100
+ const agentMachine = ctx.machineType === "agent-kit" || ctx.machineType === "agent-box";
101
+ switch (service) {
102
+ case "sync": {
103
+ if (!ctx.syncRunnerPath)
104
+ return [unavailable(service, "the hq-cloud sync runner is missing from this CLI install")];
105
+ const unit = base(service, {
106
+ command: {
107
+ type: "node",
108
+ script: ctx.syncRunnerPath,
109
+ args: ["--companies", "--direction", "both", "--on-conflict", "keep", "--hq-root", ctx.hqRoot ?? "", "--watch"],
110
+ },
111
+ });
112
+ unit.blocked = firstBlock([
113
+ [ctx.desktopAppManagesSync, "the HQ desktop app is handling sync on this machine"],
114
+ [!ctx.hqRoot, NO_HQ_ROOT],
115
+ [!ctx.signedIn, NOT_SIGNED_IN],
116
+ ]);
117
+ return [unit];
118
+ }
119
+ case "mesh": {
120
+ const args = ["mesh", "daemon", "run", ...(ctx.hqRoot ? ["--hq-root", ctx.hqRoot] : [])];
121
+ return [base(service, { command: { type: "hq", args }, blocked: firstBlock([[!ctx.signedIn, NOT_SIGNED_IN]]) })];
122
+ }
123
+ case "heartbeat": {
124
+ if (agentMachine) {
125
+ return [
126
+ base(service, {
127
+ command: { type: "hq", args: ["agent", "kit", "run", "heartbeat"] },
128
+ blocked: firstBlock([[!ctx.kitConfigured, KIT_NOT_SET_UP]]),
129
+ }),
130
+ ];
131
+ }
132
+ if (ctx.machineType === "desktop") {
133
+ return [unavailable(service, "not available on desktop machines yet")];
134
+ }
135
+ return [
136
+ base(service, {
137
+ command: { type: "hq", args: ["outposts", "heartbeat", "--home", ctx.home] },
138
+ blocked: firstBlock([[!ctx.signedIn, NOT_SIGNED_IN]]),
139
+ }),
140
+ ];
141
+ }
142
+ case "inbox": {
143
+ if (!agentMachine)
144
+ return [unavailable(service, "only runs on agent machines")];
145
+ return [
146
+ base(service, {
147
+ command: { type: "hq", args: ["agent", "kit", "run", "inbox"] },
148
+ blocked: firstBlock([[!ctx.kitConfigured, KIT_NOT_SET_UP]]),
149
+ }),
150
+ ];
151
+ }
152
+ case "bots": {
153
+ if (ctx.bots.length === 0) {
154
+ return [base(service, { blocked: { state: "waiting", reason: "no local bots" } })];
155
+ }
156
+ return ctx.bots.map((name) => base(service, {
157
+ id: `bots:${name}`,
158
+ description: `runs the local bot ${name}`,
159
+ restart: "on-failure",
160
+ command: { type: "hq", args: ["bot", "run", name] },
161
+ }));
162
+ }
163
+ case "session-host":
164
+ return [
165
+ base(service, {
166
+ command: { type: "hq", args: ["outposts", "session-host"] },
167
+ env: ctx.hqRoot ? { HQ_ROOT: ctx.hqRoot } : {},
168
+ blocked: firstBlock([[!ctx.signedIn, NOT_SIGNED_IN]]),
169
+ }),
170
+ ];
171
+ case "lanes-reconcile":
172
+ return [
173
+ base(service, {
174
+ kind: "scheduled",
175
+ intervalSec: 60,
176
+ initialDelaySec: 30,
177
+ command: { type: "hq", args: ["lanes", "reconcile"] },
178
+ env: ctx.hqRoot ? { HQ_ROOT: ctx.hqRoot } : {},
179
+ blocked: firstBlock([[!ctx.hqRoot, NO_HQ_ROOT]]),
180
+ }),
181
+ ];
182
+ case "search-index":
183
+ return [
184
+ base(service, {
185
+ kind: "scheduled",
186
+ intervalSec: 900,
187
+ initialDelaySec: 120,
188
+ command: { type: "hq", args: ["index", "sync", "--hq-root", ctx.hqRoot ?? ""] },
189
+ blocked: firstBlock([[!ctx.hqRoot, NO_HQ_ROOT]]),
190
+ }),
191
+ ];
192
+ case "housekeeping":
193
+ return [
194
+ base(service, {
195
+ kind: "scheduled",
196
+ intervalSec: 3600,
197
+ initialDelaySec: 60,
198
+ command: { type: "internal", task: "rotate-logs" },
199
+ }),
200
+ ];
201
+ case "updater":
202
+ return [
203
+ base(service, {
204
+ kind: "scheduled",
205
+ intervalSec: 6 * 3600,
206
+ initialDelaySec: 900,
207
+ command: { type: "hq", args: ["daemon", "service", "self-update"] },
208
+ blocked: firstBlock([
209
+ [!ctx.cliSelfUpdatable, "this hq CLI is not a global install, so it is updated with the project or app that owns it"],
210
+ ]),
211
+ }),
212
+ ];
213
+ case "jobs-reconcile": {
214
+ if (ctx.platform === "win32")
215
+ return [unavailable(service, "needs bash; not supported on Windows")];
216
+ const script = ctx.jobsReconcileScript;
217
+ return [
218
+ base(service, {
219
+ kind: "scheduled",
220
+ intervalSec: 600,
221
+ initialDelaySec: 600,
222
+ command: {
223
+ type: "exec",
224
+ file: "bash",
225
+ args: [script ?? "", "--hq-root", ctx.hqRoot ?? "", "--ensure-hook", "--after-sync"],
226
+ },
227
+ env: ctx.hqRoot ? { HQ_ROOT: ctx.hqRoot } : {},
228
+ blocked: firstBlock([
229
+ [!ctx.hqRoot, NO_HQ_ROOT],
230
+ [!script, "this HQ folder has no core/scripts/outpost-jobs-reconcile.sh"],
231
+ ]),
232
+ }),
233
+ ];
234
+ }
235
+ case "remote-control": {
236
+ if (ctx.platform === "win32")
237
+ return [unavailable(service, "not supported on Windows yet")];
238
+ // Outposts keep the runner's session name and permission mode.
239
+ const outpost = ctx.machineType === "outpost";
240
+ const args = ["daemon", "service", "remote-control", "--claude", ctx.claudePath ?? "", "--name", outpost ? "outpost-1" : ctx.hostName];
241
+ if (outpost)
242
+ args.push("--permission-mode", "bypassPermissions");
243
+ return [
244
+ base(service, {
245
+ command: { type: "hq", args },
246
+ env: ctx.hqRoot ? { HQ_ROOT: ctx.hqRoot } : {},
247
+ blocked: firstBlock([
248
+ [!ctx.claudePath, "the claude CLI is not installed"],
249
+ [!ctx.tmuxPath, "tmux is not installed (remote control runs Claude inside tmux)"],
250
+ [!ctx.hqRoot, NO_HQ_ROOT],
251
+ ]),
252
+ }),
253
+ ];
254
+ }
255
+ }
256
+ }
257
+ /** Turn enabled services into concrete units for this host. */
258
+ export function planUnits(services, ctx) {
259
+ const units = [];
260
+ for (const service of services) {
261
+ for (const unit of planService(service, ctx)) {
262
+ const owner = unit.command ? ctx.legacyOwner(unit.id) : null;
263
+ if (owner && unit.blocked?.state !== "unavailable") {
264
+ unit.blocked = {
265
+ state: "waiting",
266
+ reason: `already run by ${owner}; \`hq daemon migrate\` will take it over`,
267
+ };
268
+ }
269
+ units.push(unit);
270
+ }
271
+ }
272
+ return units;
273
+ }
274
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * ~/.hq/daemon/config.json — the machine type saved at install, the HQ folder
3
+ * the services work against, and per-host overrides. The default service list
4
+ * is NOT stored here: it lives in the catalog, so a release that adds a
5
+ * default service reaches every host of that machine type.
6
+ */
7
+ import { type ServiceName } from "./catalog.js";
8
+ import { type MachineType } from "./machine-type.js";
9
+ export interface DaemonConfig {
10
+ version: 1;
11
+ machineType?: MachineType;
12
+ hqRoot?: string;
13
+ /** Services turned on here beyond the machine type's defaults. */
14
+ enable: string[];
15
+ /** Default services turned off here. */
16
+ disable: string[];
17
+ }
18
+ export declare function emptyDaemonConfig(): DaemonConfig;
19
+ export declare function readDaemonConfig(file: string): DaemonConfig;
20
+ export declare function writeDaemonConfig(file: string, config: DaemonConfig): void;
21
+ /**
22
+ * Turn services on or off for this host, relative to the machine type's
23
+ * defaults: only differences from the defaults are stored.
24
+ */
25
+ export declare function applyServiceToggle(config: DaemonConfig, action: "enable" | "disable", services: readonly string[], defaults: readonly ServiceName[]): DaemonConfig;
26
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * ~/.hq/daemon/config.json — the machine type saved at install, the HQ folder
3
+ * the services work against, and per-host overrides. The default service list
4
+ * is NOT stored here: it lives in the catalog, so a release that adds a
5
+ * default service reaches every host of that machine type.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import { isServiceName } from "./catalog.js";
10
+ import { isMachineType } from "./machine-type.js";
11
+ import { ensurePrivateDir } from "./paths.js";
12
+ export function emptyDaemonConfig() {
13
+ return { version: 1, enable: [], disable: [] };
14
+ }
15
+ function stringList(value) {
16
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
17
+ }
18
+ export function readDaemonConfig(file) {
19
+ let text;
20
+ try {
21
+ text = fs.readFileSync(file, "utf8");
22
+ }
23
+ catch (err) {
24
+ if (err.code === "ENOENT")
25
+ return emptyDaemonConfig();
26
+ throw err;
27
+ }
28
+ let raw;
29
+ try {
30
+ raw = JSON.parse(text);
31
+ }
32
+ catch (err) {
33
+ throw new Error(`hq daemon config at ${file} is not valid JSON: ${err.message}`);
34
+ }
35
+ if (!raw || typeof raw !== "object")
36
+ throw new Error(`hq daemon config at ${file} is not a JSON object`);
37
+ const obj = raw;
38
+ const config = { version: 1, enable: stringList(obj.enable), disable: stringList(obj.disable) };
39
+ if (isMachineType(obj.machineType))
40
+ config.machineType = obj.machineType;
41
+ if (typeof obj.hqRoot === "string" && obj.hqRoot.trim())
42
+ config.hqRoot = obj.hqRoot;
43
+ return config;
44
+ }
45
+ export function writeDaemonConfig(file, config) {
46
+ ensurePrivateDir(path.dirname(file));
47
+ const tmp = `${file}.${process.pid}.tmp`;
48
+ const ordered = { version: 1 };
49
+ if (config.machineType)
50
+ ordered.machineType = config.machineType;
51
+ if (config.hqRoot)
52
+ ordered.hqRoot = config.hqRoot;
53
+ ordered.enable = config.enable;
54
+ ordered.disable = config.disable;
55
+ fs.writeFileSync(tmp, `${JSON.stringify(ordered, null, 2)}\n`, { mode: 0o600 });
56
+ fs.renameSync(tmp, file);
57
+ fs.chmodSync(file, 0o600);
58
+ }
59
+ /**
60
+ * Turn services on or off for this host, relative to the machine type's
61
+ * defaults: only differences from the defaults are stored.
62
+ */
63
+ export function applyServiceToggle(config, action, services, defaults) {
64
+ for (const s of services) {
65
+ if (!isServiceName(s))
66
+ throw new Error(`Unknown service "${s}".`);
67
+ }
68
+ const enable = new Set(config.enable);
69
+ const disable = new Set(config.disable);
70
+ for (const s of services) {
71
+ const isDefault = defaults.includes(s);
72
+ if (action === "enable") {
73
+ disable.delete(s);
74
+ if (!isDefault)
75
+ enable.add(s);
76
+ }
77
+ else {
78
+ enable.delete(s);
79
+ if (isDefault)
80
+ disable.add(s);
81
+ }
82
+ }
83
+ return { ...config, enable: [...enable], disable: [...disable] };
84
+ }
85
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Lets commands that manage one service on their own (`hq bot start`, and the
3
+ * older per-service installers such as `hq mesh daemon install`) hand the job
4
+ * to the daemon when the daemon runs that service, so a second copy is never
5
+ * started or installed beside it.
6
+ */
7
+ import { type ServiceName } from "./catalog.js";
8
+ import { type MachineType } from "./machine-type.js";
9
+ import { type DaemonPaths } from "./paths.js";
10
+ /** A live daemon runs `unitId` (or held it after it was stopped from outside). */
11
+ export declare function daemonSupervises(paths: DaemonPaths, unitId: string, alive?: (pid: number) => boolean): boolean;
12
+ /**
13
+ * A live daemon runs the service `unitId` belongs to (`bots` for
14
+ * `bots:<name>`), so it will run this instance once its plan includes it. For
15
+ * a bot created since the daemon last read its plan; the caller must first
16
+ * rule out an older unit that runs the instance itself.
17
+ */
18
+ export declare function daemonWillSupervise(paths: DaemonPaths, unitId: string, alive?: (pid: number) => boolean): boolean;
19
+ /** The live daemon's child for `unitId` is `pid` (a bot started before the daemon is not). */
20
+ export declare function daemonOwnsProcess(paths: DaemonPaths, unitId: string, pid: number, alive?: (pid: number) => boolean): boolean;
21
+ /** A live daemon has `service` enabled for its saved machine type, whether or not it has planned a unit for it yet. */
22
+ export declare function daemonRunsService(paths: DaemonPaths, service: ServiceName, alive?: (pid: number) => boolean): boolean;
23
+ /** Ask the running daemon to (re)start a unit it supervises. */
24
+ export declare function askDaemonToStart(paths: DaemonPaths, unitId: string): void;
25
+ /** Ask the running daemon to stop a unit and keep it down until it is started again. */
26
+ export declare function askDaemonToStop(paths: DaemonPaths, unitId: string): void;
27
+ export interface DaemonUnitHost {
28
+ platform: NodeJS.Platform;
29
+ home: string;
30
+ systemUnitDir?: string;
31
+ windowsTaskExists: (task: string) => boolean;
32
+ }
33
+ /** The `hq daemon` OS unit is installed on this machine (for this user, or as the box's system unit). */
34
+ export declare function daemonUnitInstalled(host: DaemonUnitHost): boolean;
35
+ /**
36
+ * For an older per-service install command: when hq daemon is installed here,
37
+ * make sure it runs `services` and return what to tell the user instead of
38
+ * installing a separate unit. Returns null when hq daemon is not installed, so
39
+ * the older install runs exactly as before.
40
+ */
41
+ export declare function handOffInstall(opts: {
42
+ paths: DaemonPaths;
43
+ installed: boolean;
44
+ machineType: MachineType;
45
+ services: readonly ServiceName[];
46
+ /** An HQ folder the older command was explicitly given; the daemon runs its services against it. */
47
+ hqRoot?: string;
48
+ }): string | null;
49
+ /** handOffInstall for this machine and user, for the older install commands. */
50
+ export declare function handOffInstallHere(services: readonly ServiceName[], env?: NodeJS.ProcessEnv, opts?: {
51
+ hqRoot?: string;
52
+ }): string | null;
53
+ //# sourceMappingURL=handoff.d.ts.map