@indigoai-us/hq-cli 5.150.0 → 5.152.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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.152.0] — 2026-09-23
6
+
7
+ ### Fixed
8
+
9
+ - `hq mesh daemon install` (plain and `--if-stale`) now bootouts, bootstraps, and kickstarts the LaunchAgent after writing the plist. Bootstrap retries launchd `5: Input/output error` up to five times, one second apart. After a CLI upgrade, a loaded job whose program no longer matches the plist is reloaded the same way.
10
+
11
+ ## [5.151.0] — 2026-09-23
12
+
13
+ - `hq report publish` no longer fails when another app it cannot see holds the report's address. It publishes at the next free address (`<slug>-2`, then `-3`) and saves that address on the report.
14
+
5
15
  ## [5.150.0] — 2026-09-23
6
16
 
7
17
  ### Fixed
@@ -36,6 +36,7 @@ import { readLegacyBacklog } from "../lib/mesh/live/emit-replay.js";
36
36
  import { resolveMeshEmitMode, writeMeshConfigMode, } from "../lib/mesh/live/daemon/mode.js";
37
37
  import { workMeshRoot } from "../lib/mesh/live/paths.js";
38
38
  import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, findAncestorHqRoot, resolveInstallHqRoot, detectPlatform, formatCliIdentityLines, formatDaemonDoctor, installDaemonIfStale, installDaemonService, readDaemonState, resolveAbsoluteNodeBinary, resolveRunningCliEntry, runMeshDaemon, uninstallDaemonService, daemonDir, } from "../lib/mesh/live/daemon/index.js";
39
+ import { defaultRunCommand } from "../lib/service-manager/launchd.js";
39
40
  import { workContextRoot } from "../lib/work-context/paths.js";
40
41
  import { formatMigrateConfirmation, submitSessionMigration, } from "../lib/work-context/migrate.js";
41
42
  import { formatOrganizeList, prepareOrganizeDecision, settleOrganizeAskWithoutBind, submitOrganizeDecision, } from "../lib/work-context/organize.js";
@@ -2050,6 +2051,7 @@ async function runDaemonInstall(opts) {
2050
2051
  platform: detectPlatform(),
2051
2052
  paths,
2052
2053
  systemUser: opts.systemUser,
2054
+ run: defaultRunCommand,
2053
2055
  });
2054
2056
  if (opts.json) {
2055
2057
  console.log(JSON.stringify({
@@ -1,5 +1,5 @@
1
1
  import { Command } from "commander";
2
- import { buildReportTarball, deployOrgContext, type ChannelReport, type DeployAccessPolicy, type ReportDeployment, type ReportPatch } from "../lib/report-publish.js";
2
+ import { buildReportTarball, deployOrgContext, type ChannelReport, type DeployAccessPolicy, type ReportDeployment, type ReportPatch, ReportPublishError } from "../lib/report-publish.js";
3
3
  type ChannelRow = {
4
4
  channelId?: string;
5
5
  scope?: string;
@@ -17,7 +17,21 @@ export declare function withChannelCompany(report: ChannelReport, channels: Chan
17
17
  /** Explicit context has priority; discovery is the authenticated fallback for normal CLI use. */
18
18
  export declare function resolveReport(token: string, reportId: string, requestedChannelId?: string): Promise<ChannelReport>;
19
19
  export declare function patchReport(token: string, report: ChannelReport, patch: ReportPatchInput): Promise<ChannelReport>;
20
+ /** hq-deploy refused the subdomain: another app, not visible to this caller, holds it. */
21
+ export declare class SubdomainTakenError extends ReportPublishError {
22
+ constructor();
23
+ }
24
+ /**
25
+ * Subdomains to try, in order. The first is the report's own slug; the rest
26
+ * are used only when an app the caller cannot see already holds it (for
27
+ * example, one left by an earlier publish under a different account).
28
+ */
29
+ export declare function candidateSlugs(slug: string, attempts?: number): string[];
20
30
  export declare function resolveCompanySlug(token: string, companyUid: string): Promise<string>;
31
+ export declare function findOrCreateApp(token: string, slug: string, org: ReturnType<typeof deployOrgContext>): Promise<{
32
+ id: string;
33
+ subdomain: string;
34
+ }>;
21
35
  export declare function deployReportStatic(token: string, input: {
22
36
  slug: string;
23
37
  files: Parameters<typeof buildReportTarball>[0];
@@ -232,10 +232,28 @@ async function deployJson(token, route, method, body, extraHeaders = {}) {
232
232
  catch {
233
233
  throw new ReportPublishError("Could not reach hq-deploy.");
234
234
  }
235
- if (!res.ok)
235
+ if (!res.ok) {
236
+ const failure = await res.json().catch(() => ({}));
237
+ if (failure?.error?.code === "SUBDOMAIN_DUPLICATE")
238
+ throw new SubdomainTakenError();
236
239
  throw new ReportPublishError("hq-deploy rejected the report deployment.");
240
+ }
237
241
  return await res.json();
238
242
  }
243
+ /** hq-deploy refused the subdomain: another app, not visible to this caller, holds it. */
244
+ export class SubdomainTakenError extends ReportPublishError {
245
+ constructor() {
246
+ super("hq-deploy rejected the report deployment.");
247
+ }
248
+ }
249
+ /**
250
+ * Subdomains to try, in order. The first is the report's own slug; the rest
251
+ * are used only when an app the caller cannot see already holds it (for
252
+ * example, one left by an earlier publish under a different account).
253
+ */
254
+ export function candidateSlugs(slug, attempts = 3) {
255
+ return [slug, ...Array.from({ length: attempts }, (_, i) => `${slug}-${i + 2}`)];
256
+ }
239
257
  export async function resolveCompanySlug(token, companyUid) {
240
258
  const res = await vaultApiFetch({ token, path: `/entity/${encodeURIComponent(companyUid)}` });
241
259
  if (!res.ok)
@@ -250,12 +268,29 @@ async function orgFor(token, report) {
250
268
  const slug = report.companyUid ? await resolveCompanySlug(token, report.companyUid) : undefined;
251
269
  return deployOrgContext(report, slug);
252
270
  }
253
- async function findOrCreateApp(token, slug, org) {
271
+ export async function findOrCreateApp(token, slug, org) {
254
272
  const listed = await deployJson(token, "/api/apps", "GET", undefined, org.headers);
255
- const existing = Array.isArray(listed.apps)
256
- ? listed.apps.find((app) => !!app && typeof app === "object" && (app.subdomain === slug || app.name === slug))
257
- : undefined;
258
- const app = existing ?? await deployJson(token, "/api/apps", "POST", { name: slug, type: "static" }, org.headers);
273
+ const apps = Array.isArray(listed.apps) ? listed.apps : [];
274
+ const mine = (name) => apps.find((app) => !!app && typeof app === "object" && (app.subdomain === name || app.name === name));
275
+ let app;
276
+ for (const name of candidateSlugs(slug)) {
277
+ const existing = mine(name);
278
+ if (existing) {
279
+ app = existing;
280
+ break;
281
+ }
282
+ try {
283
+ app = await deployJson(token, "/api/apps", "POST", { name, type: "static" }, org.headers);
284
+ break;
285
+ }
286
+ catch (error) {
287
+ if (error instanceof SubdomainTakenError)
288
+ continue;
289
+ throw error;
290
+ }
291
+ }
292
+ if (!app)
293
+ throw new ReportPublishError("Every address for this report is taken on hq-deploy.");
259
294
  const id = typeof app.id === "string" ? app.id : undefined;
260
295
  const subdomain = typeof app.subdomain === "string" ? app.subdomain : undefined;
261
296
  if (!id || !subdomain)
@@ -12,7 +12,7 @@
12
12
  * Tests must not actually install services — use a temp HOME/prefix.
13
13
  */
14
14
  import * as fs from "node:fs";
15
- import { type RunCommand } from "../../../service-manager/launchd.js";
15
+ import { type RunCommand, type SleepFn } from "../../../service-manager/launchd.js";
16
16
  export type DaemonInstallPlatform = "darwin" | "linux" | "win32" | "other";
17
17
  /** Result of one schtasks.exe invocation (tests inject; never the real binary). */
18
18
  export interface SchtasksResult {
@@ -93,11 +93,6 @@ export interface RestartDaemonResult {
93
93
  restarted: boolean;
94
94
  message?: string;
95
95
  }
96
- /**
97
- * Restart the installed user daemon after a CLI upgrade. This is deliberately
98
- * best-effort: an update must remain successful if a user has no service,
99
- * launchctl/systemd is unavailable, or the user manager is not running.
100
- */
101
96
  export declare function restartDaemonServiceIfInstalled(opts?: {
102
97
  platform?: DaemonInstallPlatform;
103
98
  home?: string;
@@ -110,7 +105,10 @@ export declare function restartDaemonServiceIfInstalled(opts?: {
110
105
  writeFileSync?: typeof fs.writeFileSync;
111
106
  run?: (command: string, args: string[]) => {
112
107
  status: number | null;
108
+ stdout?: string;
109
+ stderr?: string;
113
110
  };
111
+ sleep?: SleepFn;
114
112
  /** Windows: terminate the recorded daemon pid after `schtasks /End` (tests inject). */
115
113
  kill?: (pid: number) => void;
116
114
  }): RestartDaemonResult;
@@ -149,6 +147,7 @@ export declare function installDaemonIfStale(opts: {
149
147
  systemUser?: string;
150
148
  run?: RunCommand;
151
149
  uid?: number;
150
+ sleep?: SleepFn;
152
151
  writeFileSync?: typeof fs.writeFileSync;
153
152
  mkdirSync?: typeof fs.mkdirSync;
154
153
  reloadSystemd?: () => {
@@ -282,6 +281,10 @@ export declare function installDaemonService(opts: {
282
281
  stderr: string;
283
282
  };
284
283
  windowsLegacyTaskPresent?: boolean;
284
+ /** launchctl/systemctl seam — when set on darwin, bootout/bootstrap/kickstart after writing the plist. */
285
+ run?: RunCommand;
286
+ uid?: number;
287
+ sleep?: SleepFn;
285
288
  }): InstallResult;
286
289
  export declare function uninstallDaemonService(opts: {
287
290
  platform?: DaemonInstallPlatform;
@@ -22,7 +22,7 @@ import { isInstallableHqRoot, readDaemonConfig, registerTrustedHqRoot, verifyHqR
22
22
  import { defaultPidLockDeps, pidLockStatus, readPidLock } from "./pid-lock.js";
23
23
  import { retireLegacyListen } from "./legacy-listen.js";
24
24
  import { readDaemonState } from "./state.js";
25
- import { activateLaunchd } from "../../../service-manager/launchd.js";
25
+ import { activateLaunchd, launchdPlistProgramArguments, launchdProgramArgumentsDiffer, parseLaunchctlPrintArguments, } from "../../../service-manager/launchd.js";
26
26
  import { activateSystemd } from "../../../service-manager/systemd.js";
27
27
  /**
28
28
  * `schtasks /End` terminates the task's root process only. The daemon is
@@ -139,13 +139,30 @@ function migrateLegacyDaemonUnit(opts) {
139
139
  * best-effort: an update must remain successful if a user has no service,
140
140
  * launchctl/systemd is unavailable, or the user manager is not running.
141
141
  */
142
+ function asRunCommand(run) {
143
+ return (command, args) => {
144
+ const result = run(command, args);
145
+ return {
146
+ status: result.status,
147
+ stdout: result.stdout ?? "",
148
+ stderr: result.stderr ?? "",
149
+ };
150
+ };
151
+ }
142
152
  export function restartDaemonServiceIfInstalled(opts = {}) {
143
153
  const platform = opts.platform ?? detectPlatform();
144
154
  const home = opts.home ?? os.homedir();
145
155
  const existsSync = opts.existsSync ?? fs.existsSync;
146
156
  const readFileSync = opts.readFileSync ?? fs.readFileSync;
147
157
  const writeFileSync = opts.writeFileSync ?? fs.writeFileSync;
148
- const run = opts.run ?? ((command, args) => spawnSync(command, args, { stdio: "ignore" }));
158
+ const run = opts.run ?? ((command, args) => {
159
+ const result = spawnSync(command, args, { encoding: "utf8" });
160
+ return {
161
+ status: result.status,
162
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
163
+ stderr: result.error ? result.error.message : (typeof result.stderr === "string" ? result.stderr : ""),
164
+ };
165
+ });
149
166
  try {
150
167
  migrateLegacyDaemonUnit({ platform, home, hqRoot: opts.hqRoot, env: opts.env, existsSync, readFileSync, writeFileSync, run });
151
168
  if (platform === "darwin") {
@@ -155,7 +172,22 @@ export function restartDaemonServiceIfInstalled(opts = {}) {
155
172
  const uid = opts.uid ?? process.getuid?.();
156
173
  if (typeof uid !== "number")
157
174
  return { attempted: false, restarted: false };
158
- const result = run("launchctl", ["kickstart", "-k", `gui/${uid}/${LAUNCHD_LABEL}`]);
175
+ const target = `gui/${uid}/${LAUNCHD_LABEL}`;
176
+ const printed = run("launchctl", ["print", target]);
177
+ const loadedArgs = parseLaunchctlPrintArguments(printed.stdout ?? "");
178
+ const rawPlist = readFileSync(plist);
179
+ const plistText = Buffer.isBuffer(rawPlist) ? rawPlist.toString("utf8") : String(rawPlist);
180
+ const plistArgs = launchdPlistProgramArguments(plistText);
181
+ if (launchdProgramArgumentsDiffer(loadedArgs, plistArgs)) {
182
+ try {
183
+ activateLaunchd(plist, asRunCommand(run), uid, { sleep: opts.sleep });
184
+ return { attempted: true, restarted: true };
185
+ }
186
+ catch {
187
+ return { attempted: true, restarted: false };
188
+ }
189
+ }
190
+ const result = run("launchctl", ["kickstart", "-k", target]);
159
191
  return { attempted: true, restarted: result.status === 0 };
160
192
  }
161
193
  if (platform === "linux") {
@@ -306,11 +338,11 @@ export function installDaemonIfStale(opts) {
306
338
  kill: opts.kill,
307
339
  runCommand: opts.runCommand,
308
340
  windowsLegacyTaskPresent: opts.windowsLegacyTaskPresent,
341
+ run,
342
+ uid: opts.uid,
343
+ sleep: opts.sleep,
309
344
  });
310
- if (platform === "darwin") {
311
- activateLaunchd(opts.paths.plistPath, run, opts.uid);
312
- }
313
- else if (platform === "linux" && !opts.systemUser && !opts.paths.systemUser) {
345
+ if (platform === "linux" && !opts.systemUser && !opts.paths.systemUser) {
314
346
  activateSystemd(SYSTEMD_UNIT_NAME, run);
315
347
  }
316
348
  return {
@@ -753,7 +785,12 @@ export function installDaemonService(opts) {
753
785
  const rendered = renderLaunchdPlist(paths);
754
786
  mkdirSync(paths.launchAgentsDir, { recursive: true });
755
787
  writeFileSync(paths.plistPath, rendered, { mode: 0o600 });
756
- opts.activate?.(platform, paths.plistPath);
788
+ if (opts.run) {
789
+ activateLaunchd(paths.plistPath, opts.run, opts.uid, { sleep: opts.sleep });
790
+ }
791
+ else {
792
+ opts.activate?.(platform, paths.plistPath);
793
+ }
757
794
  return {
758
795
  platform,
759
796
  action: "install",
@@ -9,9 +9,8 @@ import { type RunCommand } from "./launchd.js";
9
9
  import { type ServiceHostPaths, type ServicePlatform, type ServiceSpec } from "./types.js";
10
10
  export type { ServiceSpec, ServiceHostPaths, ServicePlatform } from "./types.js";
11
11
  export { detectServicePlatform } from "./types.js";
12
- export { renderLaunchdPlist } from "./launchd.js";
12
+ export { defaultRunCommand, renderLaunchdPlist } from "./launchd.js";
13
13
  export { renderSystemdUserUnit, systemdUnitName } from "./systemd.js";
14
- export declare const defaultRunCommand: RunCommand;
15
14
  export interface ServiceManagerDeps {
16
15
  platform?: ServicePlatform;
17
16
  run?: RunCommand;
@@ -4,22 +4,13 @@
4
4
  * injectable so the kit tests exercise the real rendering and dispatch
5
5
  * without touching the host's service manager.
6
6
  */
7
- import { spawnSync } from "node:child_process";
8
7
  import * as fs from "node:fs";
9
- import { activateLaunchd, deactivateLaunchd, launchAgentsDir, launchdIsLoaded, launchdPlistPath, renderLaunchdPlist, } from "./launchd.js";
8
+ import { activateLaunchd, deactivateLaunchd, defaultRunCommand, launchAgentsDir, launchdIsLoaded, launchdPlistPath, renderLaunchdPlist, } from "./launchd.js";
10
9
  import { activateSystemd, deactivateSystemd, renderSystemdUserUnit, systemdIsActive, systemdUnitName, systemdUnitPath, systemdUserDir, } from "./systemd.js";
11
10
  import { detectServicePlatform, } from "./types.js";
12
11
  export { detectServicePlatform } from "./types.js";
13
- export { renderLaunchdPlist } from "./launchd.js";
12
+ export { defaultRunCommand, renderLaunchdPlist } from "./launchd.js";
14
13
  export { renderSystemdUserUnit, systemdUnitName } from "./systemd.js";
15
- export const defaultRunCommand = (cmd, args) => {
16
- const r = spawnSync(cmd, args, { encoding: "utf8" });
17
- return {
18
- status: r.status,
19
- stdout: r.stdout ?? "",
20
- stderr: r.error ? r.error.message : (r.stderr ?? ""),
21
- };
22
- };
23
14
  function unitPathFor(platform, home, spec) {
24
15
  return platform === "darwin" ? launchdPlistPath(home, spec) : systemdUnitPath(home, spec);
25
16
  }
@@ -16,8 +16,36 @@ export type RunCommand = (cmd: string, args: string[]) => {
16
16
  stdout: string;
17
17
  stderr: string;
18
18
  };
19
- /** (Re)load a plist: bootout is best-effort (not loaded yet is fine). */
20
- export declare function activateLaunchd(plistPath: string, run: RunCommand, uid?: number): void;
19
+ export type SleepFn = (ms: number) => void;
20
+ /** Bootstrap attempts after bootout (first try + EIO retries). */
21
+ export declare const LAUNCHD_BOOTSTRAP_ATTEMPTS = 5;
22
+ /** Delay between bootstrap retries while the old job is still exiting. */
23
+ export declare const LAUNCHD_BOOTSTRAP_RETRY_MS = 1000;
24
+ export declare const defaultRunCommand: RunCommand;
25
+ export declare function launchdServiceTarget(plistPath: string, uid?: number): string;
26
+ /** launchd returns EIO (5) when bootstrap races the previous job's exit. */
27
+ export declare function isRetryableLaunchdBootstrap(r: {
28
+ status: number | null;
29
+ stdout: string;
30
+ stderr: string;
31
+ }): boolean;
32
+ /** ProgramArguments from a LaunchAgent plist. */
33
+ export declare function launchdPlistProgramArguments(plist: string): string[];
34
+ /**
35
+ * Arguments of a loaded job from `launchctl print gui/<uid>/<label>`.
36
+ * Prefers the `arguments = { ... }` block; falls back to `program =`.
37
+ */
38
+ export declare function parseLaunchctlPrintArguments(stdout: string): string[];
39
+ export declare function launchdProgramArgumentsDiffer(loaded: string[], plist: string[]): boolean;
40
+ /**
41
+ * (Re)load a plist so launchd picks up a rewritten ProgramArguments.
42
+ * bootout is best-effort (not loaded yet is fine). bootstrap retries EIO a
43
+ * bounded number of times — bootout returns before the old job has actually
44
+ * exited. kickstart then starts the newly loaded program.
45
+ */
46
+ export declare function activateLaunchd(plistPath: string, run: RunCommand, uid?: number, opts?: {
47
+ sleep?: SleepFn;
48
+ }): void;
21
49
  export declare function deactivateLaunchd(plistPath: string, run: RunCommand, uid?: number): void;
22
50
  export declare function launchdIsLoaded(label: string, run: RunCommand, uid?: number): boolean;
23
51
  //# sourceMappingURL=launchd.d.ts.map
@@ -6,6 +6,7 @@
6
6
  * picks up a changed plist). Command execution is injected so tests never
7
7
  * touch launchctl.
8
8
  */
9
+ import { spawnSync } from "node:child_process";
9
10
  import * as os from "node:os";
10
11
  import * as path from "node:path";
11
12
  import { escapeXml, } from "./types.js";
@@ -59,17 +60,92 @@ ${envXml}
59
60
  </plist>
60
61
  `;
61
62
  }
63
+ /** Bootstrap attempts after bootout (first try + EIO retries). */
64
+ export const LAUNCHD_BOOTSTRAP_ATTEMPTS = 5;
65
+ /** Delay between bootstrap retries while the old job is still exiting. */
66
+ export const LAUNCHD_BOOTSTRAP_RETRY_MS = 1000;
67
+ export const defaultRunCommand = (cmd, args) => {
68
+ const r = spawnSync(cmd, args, { encoding: "utf8" });
69
+ return {
70
+ status: r.status,
71
+ stdout: typeof r.stdout === "string" ? r.stdout : "",
72
+ stderr: r.error ? r.error.message : (typeof r.stderr === "string" ? r.stderr : ""),
73
+ };
74
+ };
75
+ function blockingSleep(ms) {
76
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
77
+ }
62
78
  function guiDomain(uid = os.userInfo().uid) {
63
79
  return `gui/${uid}`;
64
80
  }
65
- /** (Re)load a plist: bootout is best-effort (not loaded yet is fine). */
66
- export function activateLaunchd(plistPath, run, uid) {
81
+ export function launchdServiceTarget(plistPath, uid) {
82
+ return `${guiDomain(uid)}/${path.basename(plistPath, ".plist")}`;
83
+ }
84
+ function bootstrapErrorText(r) {
85
+ return `${r.stderr} ${r.stdout} exit ${r.status ?? ""}`;
86
+ }
87
+ /** launchd returns EIO (5) when bootstrap races the previous job's exit. */
88
+ export function isRetryableLaunchdBootstrap(r) {
89
+ if (r.status === 0)
90
+ return false;
91
+ return /Input\/output error|\bEIO\b|Bootstrap failed: 5|Boot-strap failed: 5/i.test(bootstrapErrorText(r));
92
+ }
93
+ function unescapeXml(value) {
94
+ return value
95
+ .replace(/&quot;/g, '"')
96
+ .replace(/&lt;/g, "<")
97
+ .replace(/&gt;/g, ">")
98
+ .replace(/&amp;/g, "&");
99
+ }
100
+ /** ProgramArguments from a LaunchAgent plist. */
101
+ export function launchdPlistProgramArguments(plist) {
102
+ const block = plist.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
103
+ if (!block)
104
+ return [];
105
+ return [...block[1].matchAll(/<string>([^<]*)<\/string>/g)].map((m) => unescapeXml(m[1]));
106
+ }
107
+ /**
108
+ * Arguments of a loaded job from `launchctl print gui/<uid>/<label>`.
109
+ * Prefers the `arguments = { ... }` block; falls back to `program =`.
110
+ */
111
+ export function parseLaunchctlPrintArguments(stdout) {
112
+ const block = /(?:program\s+)?arguments\s*=\s*\{([^}]*)\}/i.exec(stdout);
113
+ if (block) {
114
+ return block[1]
115
+ .split(/\r?\n/)
116
+ .map((line) => line.trim().replace(/;+$/, ""))
117
+ .filter((line) => line.length > 0);
118
+ }
119
+ const program = /^\s*program\s*=\s*(.+?);?\s*$/m.exec(stdout);
120
+ return program ? [program[1].trim()] : [];
121
+ }
122
+ export function launchdProgramArgumentsDiffer(loaded, plist) {
123
+ if (loaded.length === 0 || plist.length === 0)
124
+ return loaded.length !== plist.length;
125
+ if (loaded.length !== plist.length)
126
+ return true;
127
+ return loaded.some((value, i) => value !== plist[i]);
128
+ }
129
+ /**
130
+ * (Re)load a plist so launchd picks up a rewritten ProgramArguments.
131
+ * bootout is best-effort (not loaded yet is fine). bootstrap retries EIO a
132
+ * bounded number of times — bootout returns before the old job has actually
133
+ * exited. kickstart then starts the newly loaded program.
134
+ */
135
+ export function activateLaunchd(plistPath, run, uid, opts) {
67
136
  const domain = guiDomain(uid);
68
- run("launchctl", ["bootout", domain, plistPath]);
69
- const r = run("launchctl", ["bootstrap", domain, plistPath]);
70
- if (r.status !== 0) {
71
- throw new Error(`launchctl bootstrap failed for ${plistPath}: ${r.stderr.trim() || r.stdout.trim() || `exit ${r.status}`}`);
137
+ const target = launchdServiceTarget(plistPath, uid);
138
+ run("launchctl", ["bootout", target]);
139
+ const sleep = opts?.sleep ?? blockingSleep;
140
+ let last = run("launchctl", ["bootstrap", domain, plistPath]);
141
+ for (let attempt = 1; attempt < LAUNCHD_BOOTSTRAP_ATTEMPTS && last.status !== 0 && isRetryableLaunchdBootstrap(last); attempt++) {
142
+ sleep(LAUNCHD_BOOTSTRAP_RETRY_MS);
143
+ last = run("launchctl", ["bootstrap", domain, plistPath]);
144
+ }
145
+ if (last.status !== 0) {
146
+ throw new Error(`launchctl bootstrap failed for ${plistPath}: ${last.stderr.trim() || last.stdout.trim() || `exit ${last.status}`}`);
72
147
  }
148
+ run("launchctl", ["kickstart", "-k", target]);
73
149
  }
74
150
  export function deactivateLaunchd(plistPath, run, uid) {
75
151
  run("launchctl", ["bootout", guiDomain(uid), plistPath]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.150.0",
3
+ "version": "5.152.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {