@indigoai-us/hq-cli 5.345.50 → 5.345.52

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 (33) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/commands/core-hq-session-native.js +2 -2
  3. package/dist/commands/core-resolve-company-native.js +2 -2
  4. package/dist/commands/lanes.js +52 -11
  5. package/dist/commands/secrets.js +2 -2
  6. package/dist/commands/workers.js +2 -2
  7. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +40293 -13770
  8. package/dist/hq-cloud-sync-runner/package.json +1 -1
  9. package/dist/lib/bot/daemon.d.ts +1 -1
  10. package/dist/lib/bot/daemon.js +35 -29
  11. package/dist/lib/daemon/install.js +34 -3
  12. package/dist/lib/doctor/checks/bots.js +3 -8
  13. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  14. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  15. package/dist/lib/lanes/outpost-dispatch.d.ts +1 -0
  16. package/dist/lib/lanes/outpost-dispatch.js +2 -0
  17. package/dist/lib/mesh/live/daemon/install.js +28 -15
  18. package/dist/lib/service-manager/index.d.ts +3 -0
  19. package/dist/lib/service-manager/index.js +22 -6
  20. package/dist/lib/service-manager/launchd.d.ts +40 -3
  21. package/dist/lib/service-manager/launchd.js +106 -5
  22. package/dist/lib/service-manager/migration-state.d.ts +12 -0
  23. package/dist/lib/service-manager/migration-state.js +46 -0
  24. package/dist/lib/service-manager/migration.d.ts +22 -0
  25. package/dist/lib/service-manager/migration.js +85 -0
  26. package/dist/lib/service-manager/startup-migration.d.ts +24 -0
  27. package/dist/lib/service-manager/startup-migration.js +27 -0
  28. package/dist/lib/service-manager/types.d.ts +3 -0
  29. package/dist/main.js +22 -0
  30. package/dist/native-fast-route.d.ts +4 -0
  31. package/dist/native-fast-route.js +20 -0
  32. package/dist/utils/resident-package-preload.js +2 -1
  33. package/package.json +2 -2
@@ -100,6 +100,7 @@ export declare function buildOutpostLaneListCommand(input?: {
100
100
  laneUlids?: readonly string[];
101
101
  active?: boolean;
102
102
  hierarchy?: string | readonly string[];
103
+ senior?: string;
103
104
  snapshot?: boolean;
104
105
  }): string;
105
106
  /**
@@ -93,6 +93,8 @@ export function buildOutpostLaneListCommand(input = {}) {
93
93
  ? [input.hierarchy]
94
94
  : input.hierarchy ?? [])
95
95
  args.push("--hierarchy", laneId);
96
+ if (input.senior !== undefined)
97
+ args.push("--senior", input.senior);
96
98
  if (input.snapshot)
97
99
  args.push("--snapshot");
98
100
  return withRemoteHqDir(joinCommandParts(args));
@@ -23,7 +23,7 @@ import { isInstallableHqRoot, readDaemonConfig, registerTrustedHqRoot, verifyHqR
23
23
  import { defaultPidLockDeps, pidLockStatus, readPidLock } from "./pid-lock.js";
24
24
  import { retireLegacyListen } from "./legacy-listen.js";
25
25
  import { readDaemonState } from "./state.js";
26
- import { activateLaunchd, defaultRunCommand, launchdIsLoaded, launchdServiceTarget, launchdPlistProgramArguments, launchdProgramArgumentsDiffer, parseLaunchctlPrintArguments, } from "../../../service-manager/launchd.js";
26
+ import { activateLaunchd, defaultRunCommand, discoverLaunchdIdentity, installLaunchdPlist, launchdProgramArguments, launchdIsLoaded, launchdServiceTarget, launchdPlistProgramArguments, parseLaunchdCommand, launchdProgramArgumentsDiffer, parseLaunchctlPrintArguments, } from "../../../service-manager/launchd.js";
27
27
  import { activateSystemd, systemdIsActive } from "../../../service-manager/systemd.js";
28
28
  import { nodeBinaryProblem, preferStableNodeBinary } from "../../../node-binary.js";
29
29
  /**
@@ -428,9 +428,7 @@ export function installedDaemonNodeBinary(paths, platform, readUnit = (file) =>
428
428
  const text = readUnit(paths.plistPath);
429
429
  if (!text)
430
430
  return null;
431
- const block = text.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
432
- const first = block?.[1]?.match(/<string>([^<]*)<\/string>/);
433
- return first?.[1] ?? null;
431
+ return parseLaunchdCommand(launchdPlistProgramArguments(text))?.node ?? null;
434
432
  }
435
433
  if (platform === "linux") {
436
434
  const text = readUnit(paths.systemdUnitPath);
@@ -544,19 +542,21 @@ function safeUsername() {
544
542
  }
545
543
  }
546
544
  export function renderLaunchdPlist(paths) {
545
+ const identity = discoverLaunchdIdentity(paths.home);
546
+ const args = launchdProgramArguments({
547
+ nodeBinary: paths.nodeBinary,
548
+ hqBinary: paths.hqBinary,
549
+ launchdLauncherBinary: identity.launcherBinary,
550
+ }, ["mesh", "daemon", "run"]);
547
551
  return `<?xml version="1.0" encoding="UTF-8"?>
548
552
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
549
553
  <plist version="1.0">
550
554
  <dict>
551
555
  <key>Label</key>
552
556
  <string>${LAUNCHD_LABEL}</string>
553
- <key>ProgramArguments</key>
557
+ ${identity.associatedBundleIdentifier ? ` <key>AssociatedBundleIdentifiers</key>\n <array>\n <string>${escapeXml(identity.associatedBundleIdentifier)}</string>\n </array>\n` : ""} <key>ProgramArguments</key>
554
558
  <array>
555
- <string>${escapeXml(paths.nodeBinary)}</string>
556
- <string>${escapeXml(paths.hqBinary)}</string>
557
- <string>mesh</string>
558
- <string>daemon</string>
559
- <string>run</string>
559
+ ${args.map((arg) => ` <string>${escapeXml(arg)}</string>`).join("\n")}
560
560
  </array>
561
561
  <key>WorkingDirectory</key>
562
562
  <string>${escapeXml(paths.daemonDir)}</string>
@@ -920,11 +920,24 @@ export function installDaemonService(opts) {
920
920
  if (platform === "darwin") {
921
921
  const rendered = renderLaunchdPlist(paths);
922
922
  mkdirSync(paths.launchAgentsDir, { recursive: true });
923
- replaceUnitFile(paths.plistPath, rendered, 0o600, () => {
924
- if (opts.run)
925
- activateLaunchd(paths.plistPath, opts.run, opts.uid, { sleep: opts.sleep });
926
- else
927
- opts.activate?.(platform, paths.plistPath);
923
+ installLaunchdPlist(paths.plistPath, LAUNCHD_LABEL, rendered, {
924
+ files: {
925
+ existsSync: fs.existsSync,
926
+ readFileSync: (file) => fs.readFileSync(file, "utf8"),
927
+ writeFileSync: fs.writeFileSync,
928
+ chmodSync: fs.chmodSync,
929
+ renameSync: fs.renameSync,
930
+ unlinkSync: fs.unlinkSync,
931
+ },
932
+ activate: Boolean(opts.run || opts.activate),
933
+ uid: opts.uid,
934
+ rollbackOnReloadError: true,
935
+ reload: () => {
936
+ if (opts.run)
937
+ activateLaunchd(paths.plistPath, opts.run, opts.uid, { sleep: opts.sleep });
938
+ else if (opts.activate)
939
+ opts.activate(platform, paths.plistPath);
940
+ },
928
941
  });
929
942
  return {
930
943
  platform,
@@ -15,9 +15,12 @@ export interface ServiceManagerDeps {
15
15
  platform?: ServicePlatform;
16
16
  run?: RunCommand;
17
17
  writeFileSync?: typeof fs.writeFileSync;
18
+ chmodSync?: typeof fs.chmodSync;
19
+ renameSync?: typeof fs.renameSync;
18
20
  mkdirSync?: typeof fs.mkdirSync;
19
21
  unlinkSync?: typeof fs.unlinkSync;
20
22
  existsSync?: typeof fs.existsSync;
23
+ readFileSync?: typeof fs.readFileSync;
21
24
  /** When false, render + write units but do not (de)activate them. */
22
25
  activate?: boolean;
23
26
  }
@@ -5,7 +5,7 @@
5
5
  * without touching the host's service manager.
6
6
  */
7
7
  import * as fs from "node:fs";
8
- import { activateLaunchd, deactivateLaunchd, defaultRunCommand, launchAgentsDir, launchdIsLoaded, launchdPlistPath, renderLaunchdPlist, } from "./launchd.js";
8
+ import { deactivateLaunchd, defaultRunCommand, launchAgentsDir, launchdIsLoaded, launchdPlistPath, installLaunchdPlist, renderLaunchdPlist, } from "./launchd.js";
9
9
  import { activateSystemd, deactivateSystemd, renderSystemdUserUnit, systemdIsActive, systemdUnitName, systemdUnitPath, systemdUserDir, } from "./systemd.js";
10
10
  import { detectServicePlatform, } from "./types.js";
11
11
  export { detectServicePlatform } from "./types.js";
@@ -23,6 +23,11 @@ export function installServices(specs, host, deps = {}) {
23
23
  const platform = deps.platform ?? detectServicePlatform();
24
24
  const run = deps.run ?? defaultRunCommand;
25
25
  const writeFileSync = deps.writeFileSync ?? fs.writeFileSync;
26
+ const chmodSync = deps.chmodSync ?? fs.chmodSync;
27
+ const renameSync = deps.renameSync ?? fs.renameSync;
28
+ const unlinkSync = deps.unlinkSync ?? fs.unlinkSync;
29
+ const existsSync = deps.existsSync ?? fs.existsSync;
30
+ const readFileSync = deps.readFileSync ?? fs.readFileSync;
26
31
  const mkdirSync = deps.mkdirSync ?? fs.mkdirSync;
27
32
  const activate = deps.activate ?? true;
28
33
  if (platform === "other") {
@@ -38,14 +43,25 @@ export function installServices(specs, host, deps = {}) {
38
43
  for (const spec of specs) {
39
44
  const unitPath = unitPathFor(platform, host.home, spec);
40
45
  const rendered = renderUnit(platform, spec, host);
41
- writeFileSync(unitPath, rendered, { mode: 0o600 });
46
+ if (platform === "darwin") {
47
+ const installed = installLaunchdPlist(unitPath, spec.label, rendered, {
48
+ run,
49
+ activate,
50
+ files: { existsSync, readFileSync: (file) => readFileSync(file, "utf8"), writeFileSync, chmodSync, renameSync, unlinkSync },
51
+ });
52
+ const unit = { name: spec.name, unitPath, rendered, installed: true };
53
+ if (activate)
54
+ unit.running = installed.loaded;
55
+ units.push(unit);
56
+ continue;
57
+ }
58
+ else {
59
+ writeFileSync(unitPath, rendered, { mode: 0o600 });
60
+ }
42
61
  const unit = { name: spec.name, unitPath, rendered, installed: true };
43
62
  if (activate) {
44
63
  try {
45
- if (platform === "darwin")
46
- activateLaunchd(unitPath, run);
47
- else
48
- activateSystemd(systemdUnitName(spec), run);
64
+ activateSystemd(systemdUnitName(spec), run);
49
65
  unit.running = true;
50
66
  }
51
67
  catch (err) {
@@ -2,11 +2,24 @@
2
2
  * macOS LaunchAgent rendering + activation for kit services.
3
3
  *
4
4
  * Units live at ~/Library/LaunchAgents/<label>.plist. Activation uses the
5
- * modern `launchctl bootstrap gui/<uid>` form (bootout first so a re-install
6
- * picks up a changed plist). Command execution is injected so tests never
7
- * touch launchctl.
5
+ * modern `launchctl bootstrap gui/<uid>` form only after a serialized plist
6
+ * changes. Command execution is injected so tests never touch launchctl.
8
7
  */
8
+ import * as fs from "node:fs";
9
9
  import { type ServiceHostPaths, type ServiceSpec } from "./types.js";
10
+ export interface LaunchdIdentity {
11
+ launcherBinary?: string;
12
+ associatedBundleIdentifier?: string;
13
+ }
14
+ /** Find the installed HQ bundle and read its identity from the generated plist. */
15
+ export declare function discoverLaunchdIdentity(home: string, io?: Pick<typeof fs, "existsSync" | "readFileSync">): LaunchdIdentity;
16
+ export declare function launchdProgramArguments(host: Pick<ServiceHostPaths, "nodeBinary" | "hqBinary" | "launchdLauncherBinary">, args: string[]): string[];
17
+ export declare function parseLaunchdCommand(args: string[]): {
18
+ node: string;
19
+ hq: string;
20
+ args: string[];
21
+ } | null;
22
+ export declare function launchdIdentityForHome(home: string): LaunchdIdentity;
10
23
  export declare function launchAgentsDir(home: string): string;
11
24
  export declare function launchdPlistPath(home: string, spec: Pick<ServiceSpec, "label">): string;
12
25
  export declare function renderLaunchdPlist(spec: ServiceSpec, host: ServiceHostPaths): string;
@@ -16,6 +29,30 @@ export type RunCommand = (cmd: string, args: string[]) => {
16
29
  stderr: string;
17
30
  };
18
31
  export type SleepFn = (ms: number) => void;
32
+ export interface LaunchdPlistFiles {
33
+ existsSync(file: string): boolean;
34
+ readFileSync(file: string, encoding: "utf8"): string;
35
+ writeFileSync(file: string, contents: string, options?: {
36
+ mode?: number;
37
+ }): unknown;
38
+ chmodSync?(file: string, mode: number): unknown;
39
+ unlinkSync?(file: string): unknown;
40
+ renameSync?(from: string, to: string): unknown;
41
+ }
42
+ /** Write and activate a LaunchAgent only when its serialized plist changes. */
43
+ export declare function installLaunchdPlist(plistPath: string, _label: string, rendered: string, deps: {
44
+ files: LaunchdPlistFiles;
45
+ run?: RunCommand;
46
+ uid?: number;
47
+ activate?: boolean;
48
+ reload?: () => unknown;
49
+ isLoaded?: () => boolean;
50
+ sleep?: SleepFn;
51
+ rollbackOnReloadError?: boolean;
52
+ }): {
53
+ changed: boolean;
54
+ loaded: boolean;
55
+ };
19
56
  /** How long to wait for a booted-out job to leave launchd before bootstrapping anyway. */
20
57
  export declare const LAUNCHD_UNLOAD_DEADLINE_MS = 10000;
21
58
  /** Interval between `launchctl print` polls while the old job unloads. */
@@ -2,15 +2,55 @@
2
2
  * macOS LaunchAgent rendering + activation for kit services.
3
3
  *
4
4
  * Units live at ~/Library/LaunchAgents/<label>.plist. Activation uses the
5
- * modern `launchctl bootstrap gui/<uid>` form (bootout first so a re-install
6
- * picks up a changed plist). Command execution is injected so tests never
7
- * touch launchctl.
5
+ * modern `launchctl bootstrap gui/<uid>` form only after a serialized plist
6
+ * changes. Command execution is injected so tests never touch launchctl.
8
7
  */
9
8
  import { spawnSync } from "node:child_process";
9
+ import * as fs from "node:fs";
10
10
  import * as os from "node:os";
11
11
  import * as path from "node:path";
12
12
  import { escapeXml, } from "./types.js";
13
13
  const LAUNCHD_PATH_ENV = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
14
+ const HQ_TASK_ARGUMENT = "--hq-launchd-task";
15
+ /** Find the installed HQ bundle and read its identity from the generated plist. */
16
+ export function discoverLaunchdIdentity(home, io = fs) {
17
+ const bundles = ["/Applications/HQ.app", path.join(home, "Applications", "HQ.app")];
18
+ for (const bundle of bundles) {
19
+ const infoPath = path.join(bundle, "Contents", "Info.plist");
20
+ const executable = path.join(bundle, "Contents", "MacOS", "hq-sync-menubar");
21
+ if (!io.existsSync(infoPath))
22
+ continue;
23
+ const xml = io.readFileSync(infoPath, "utf8");
24
+ const identifier = /<key>CFBundleIdentifier<\/key>\s*<string>([^<]+)<\/string>/.exec(xml)?.[1];
25
+ const version = /<key>HQLaunchdTaskVersion<\/key>\s*<integer>(\d+)<\/integer>/.exec(xml)?.[1];
26
+ if (!identifier)
27
+ throw new Error(`HQ.app Info.plist has no CFBundleIdentifier: ${infoPath}`);
28
+ if (version === "1" && !io.existsSync(executable)) {
29
+ throw new Error(`HQ.app advertises LaunchAgent launcher protocol 1 but its executable is missing: ${executable}`);
30
+ }
31
+ return {
32
+ associatedBundleIdentifier: identifier,
33
+ ...(version === "1" ? { launcherBinary: executable } : {}),
34
+ };
35
+ }
36
+ return {};
37
+ }
38
+ export function launchdProgramArguments(host, args) {
39
+ return host.launchdLauncherBinary
40
+ ? [host.launchdLauncherBinary, HQ_TASK_ARGUMENT, host.nodeBinary, host.hqBinary, ...args]
41
+ : [host.nodeBinary, host.hqBinary, ...args];
42
+ }
43
+ export function parseLaunchdCommand(args) {
44
+ const offset = args[1] === HQ_TASK_ARGUMENT ? 2 : 0;
45
+ const node = args[offset];
46
+ const hq = args[offset + 1];
47
+ if (!node || !hq)
48
+ return null;
49
+ return { node, hq, args: args.slice(offset + 2) };
50
+ }
51
+ export function launchdIdentityForHome(home) {
52
+ return discoverLaunchdIdentity(home);
53
+ }
14
54
  export function launchAgentsDir(home) {
15
55
  return path.join(home, "Library", "LaunchAgents");
16
56
  }
@@ -18,7 +58,10 @@ export function launchdPlistPath(home, spec) {
18
58
  return path.join(launchAgentsDir(home), `${spec.label}.plist`);
19
59
  }
20
60
  export function renderLaunchdPlist(spec, host) {
21
- const args = [host.nodeBinary, host.hqBinary, ...spec.args]
61
+ const discovered = discoverLaunchdIdentity(host.home);
62
+ const associatedBundleIdentifier = host.associatedBundleIdentifier ?? discovered.associatedBundleIdentifier;
63
+ const launcherBinary = host.launchdLauncherBinary ?? discovered.launcherBinary;
64
+ const args = launchdProgramArguments({ ...host, launchdLauncherBinary: launcherBinary }, spec.args)
22
65
  .map((a) => ` <string>${escapeXml(a)}</string>`)
23
66
  .join("\n");
24
67
  const env = {
@@ -36,7 +79,7 @@ export function renderLaunchdPlist(spec, host) {
36
79
  <dict>
37
80
  <key>Label</key>
38
81
  <string>${escapeXml(spec.label)}</string>
39
- <key>ProgramArguments</key>
82
+ ${associatedBundleIdentifier ? ` <key>AssociatedBundleIdentifiers</key>\n <array>\n <string>${escapeXml(associatedBundleIdentifier)}</string>\n </array>\n` : ""} <key>ProgramArguments</key>
40
83
  <array>
41
84
  ${args}
42
85
  </array>
@@ -60,6 +103,64 @@ ${envXml}
60
103
  </plist>
61
104
  `;
62
105
  }
106
+ /** Write and activate a LaunchAgent only when its serialized plist changes. */
107
+ export function installLaunchdPlist(plistPath, _label, rendered, deps) {
108
+ const { files } = deps;
109
+ const current = files.existsSync(plistPath) ? files.readFileSync(plistPath, "utf8") : null;
110
+ const isLoaded = () => deps.isLoaded?.() ?? (deps.run ? launchdIsLoaded(_label, deps.run, deps.uid) : false);
111
+ if (current === rendered) {
112
+ if (deps.activate === false)
113
+ return { changed: false, loaded: false };
114
+ if (isLoaded())
115
+ return { changed: false, loaded: true };
116
+ if (deps.reload)
117
+ deps.reload();
118
+ else if (deps.run)
119
+ activateLaunchd(plistPath, deps.run, deps.uid, { sleep: deps.sleep });
120
+ return { changed: false, loaded: isLoaded() };
121
+ }
122
+ const write = (contents) => {
123
+ if (!files.renameSync) {
124
+ files.writeFileSync(plistPath, contents, { mode: 0o600 });
125
+ files.chmodSync?.(plistPath, 0o600);
126
+ return;
127
+ }
128
+ const tempPath = `${plistPath}.${process.pid}.tmp`;
129
+ try {
130
+ files.writeFileSync(tempPath, contents, { mode: 0o600 });
131
+ files.chmodSync?.(tempPath, 0o600);
132
+ files.renameSync(tempPath, plistPath);
133
+ }
134
+ catch (error) {
135
+ try {
136
+ files.unlinkSync?.(tempPath);
137
+ }
138
+ catch (cleanupError) {
139
+ throw new AggregateError([error, cleanupError], `Could not clean temporary LaunchAgent plist ${tempPath}`);
140
+ }
141
+ throw error;
142
+ }
143
+ };
144
+ write(rendered);
145
+ if (deps.activate === false)
146
+ return { changed: true, loaded: false };
147
+ try {
148
+ if (deps.reload)
149
+ deps.reload();
150
+ else if (deps.run)
151
+ activateLaunchd(plistPath, deps.run, deps.uid, { sleep: deps.sleep });
152
+ }
153
+ catch (error) {
154
+ if (deps.rollbackOnReloadError) {
155
+ if (current === null)
156
+ files.unlinkSync?.(plistPath);
157
+ else
158
+ write(current);
159
+ }
160
+ throw error;
161
+ }
162
+ return { changed: true, loaded: isLoaded() };
163
+ }
63
164
  /** How long to wait for a booted-out job to leave launchd before bootstrapping anyway. */
64
165
  export const LAUNCHD_UNLOAD_DEADLINE_MS = 10_000;
65
166
  /** Interval between `launchctl print` polls while the old job unloads. */
@@ -0,0 +1,12 @@
1
+ export declare const LAUNCHD_MIGRATION_MARKER = ".launchd-node-migration-v1";
2
+ export declare const DEFERRED_MIGRATION_RETRY_MS: number;
3
+ export interface LaunchdMigrationMarker {
4
+ version: 1;
5
+ checkedAt: string;
6
+ deferredLabels: string[];
7
+ }
8
+ export declare function launchdMigrationMarkerPath(home: string): string;
9
+ /** Old timestamp-only markers mean the previous migration completed successfully. */
10
+ export declare function shouldRunLaunchdMigration(home: string, now?: number): boolean;
11
+ export declare function writeLaunchdMigrationMarker(home: string, deferredLabels: string[], now?: number): void;
12
+ //# sourceMappingURL=migration-state.d.ts.map
@@ -0,0 +1,46 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ export const LAUNCHD_MIGRATION_MARKER = ".launchd-node-migration-v1";
4
+ export const DEFERRED_MIGRATION_RETRY_MS = 24 * 60 * 60 * 1000;
5
+ export function launchdMigrationMarkerPath(home) {
6
+ return path.join(home, "Library", "Application Support", "Indigo HQ", LAUNCHD_MIGRATION_MARKER);
7
+ }
8
+ /** Old timestamp-only markers mean the previous migration completed successfully. */
9
+ export function shouldRunLaunchdMigration(home, now = Date.now()) {
10
+ const marker = launchdMigrationMarkerPath(home);
11
+ if (!fs.existsSync(marker))
12
+ return true;
13
+ try {
14
+ const raw = fs.readFileSync(marker, "utf8");
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(raw);
18
+ }
19
+ catch {
20
+ return Number.isNaN(Date.parse(raw.trim()));
21
+ }
22
+ if (!parsed || typeof parsed !== "object")
23
+ return true;
24
+ const state = parsed;
25
+ if (state.version !== 1 || typeof state.checkedAt !== "string")
26
+ return true;
27
+ if (!Array.isArray(state.deferredLabels) || state.deferredLabels.length === 0)
28
+ return false;
29
+ const checkedAt = Date.parse(state.checkedAt);
30
+ return !Number.isFinite(checkedAt) || now - checkedAt >= DEFERRED_MIGRATION_RETRY_MS;
31
+ }
32
+ catch {
33
+ return true;
34
+ }
35
+ }
36
+ export function writeLaunchdMigrationMarker(home, deferredLabels, now = Date.now()) {
37
+ const marker = launchdMigrationMarkerPath(home);
38
+ fs.mkdirSync(path.dirname(marker), { recursive: true, mode: 0o700 });
39
+ const state = {
40
+ version: 1,
41
+ checkedAt: new Date(now).toISOString(),
42
+ deferredLabels: [...new Set(deferredLabels)].sort(),
43
+ };
44
+ fs.writeFileSync(marker, `${JSON.stringify(state)}\n`, { mode: 0o600 });
45
+ }
46
+ //# sourceMappingURL=migration-state.js.map
@@ -0,0 +1,22 @@
1
+ import * as fs from "node:fs";
2
+ import { type RunCommand, type SleepFn } from "./launchd.js";
3
+ export interface LaunchdMigrationOptions {
4
+ home: string;
5
+ run: RunCommand;
6
+ uid: number;
7
+ launcherBinary?: string;
8
+ associatedBundleIdentifier?: string;
9
+ files?: Pick<typeof fs, "existsSync" | "readFileSync" | "writeFileSync" | "renameSync" | "unlinkSync" | "readdirSync" | "mkdirSync">;
10
+ processArgs?: string[];
11
+ xpcServiceName?: string;
12
+ sleep?: SleepFn;
13
+ }
14
+ export interface LaunchdMigrationResult {
15
+ migrated: string[];
16
+ backupsRemoved: string[];
17
+ deferred: string[];
18
+ alreadyDone: boolean;
19
+ }
20
+ /** First-CLI-run cleanup for HQ's old Node-first LaunchAgents and backup plists. */
21
+ export declare function migrateLegacyNodeLaunchAgents(options: LaunchdMigrationOptions): LaunchdMigrationResult;
22
+ //# sourceMappingURL=migration.d.ts.map
@@ -0,0 +1,85 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { launchAgentsDir, activateLaunchd, launchdPlistProgramArguments, } from "./launchd.js";
4
+ import { isVersionedNodePath, preferStableNodeBinary } from "../node-binary.js";
5
+ import { preferStableHqBinary } from "../mesh/live/daemon/install.js";
6
+ import { launchdMigrationMarkerPath, shouldRunLaunchdMigration, writeLaunchdMigrationMarker } from "./migration-state.js";
7
+ const HQ_LABEL_PREFIXES = ["ai.indigo.hq-", "ai.getindigo.hq-"];
8
+ function xml(value) {
9
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
10
+ }
11
+ function plistArgs(args) {
12
+ return ` <key>ProgramArguments</key>\n <array>\n${args.map((arg) => ` <string>${xml(arg)}</string>`).join("\n")}\n </array>`;
13
+ }
14
+ function isNodeProgram(program) {
15
+ return /(^|[\\/])node(?:\.exe)?$/i.test(program);
16
+ }
17
+ /** First-CLI-run cleanup for HQ's old Node-first LaunchAgents and backup plists. */
18
+ export function migrateLegacyNodeLaunchAgents(options) {
19
+ const files = options.files ?? fs;
20
+ const marker = launchdMigrationMarkerPath(options.home);
21
+ if (files.existsSync(marker) && !shouldRunLaunchdMigration(options.home)) {
22
+ return { migrated: [], backupsRemoved: [], deferred: [], alreadyDone: true };
23
+ }
24
+ const dir = launchAgentsDir(options.home);
25
+ const migrated = [];
26
+ const backupsRemoved = [];
27
+ const deferred = [];
28
+ if (!files.existsSync(dir))
29
+ files.mkdirSync(dir, { recursive: true });
30
+ for (const name of files.readdirSync(dir)) {
31
+ const fullPath = path.join(dir, name);
32
+ if (name.startsWith("ai.indigo.") && name.includes(".plist.bak-")) {
33
+ files.unlinkSync(fullPath);
34
+ backupsRemoved.push(name);
35
+ continue;
36
+ }
37
+ if (!name.endsWith(".plist") || !HQ_LABEL_PREFIXES.some((prefix) => name.startsWith(prefix)))
38
+ continue;
39
+ const source = files.readFileSync(fullPath, "utf8");
40
+ const args = launchdPlistProgramArguments(source);
41
+ if (args.length < 2 || !isNodeProgram(args[0]))
42
+ continue;
43
+ const label = name.slice(0, -".plist".length);
44
+ const currentProcessIsJob = options.xpcServiceName === label ||
45
+ (options.processArgs?.length === args.length && options.processArgs.every((arg, index) => arg === args[index]));
46
+ if (currentProcessIsJob) {
47
+ deferred.push(label);
48
+ continue;
49
+ }
50
+ if (!options.launcherBinary) {
51
+ deferred.push(name.slice(0, -".plist".length));
52
+ continue;
53
+ }
54
+ const nodeBinary = preferStableNodeBinary(args[0]);
55
+ if (isVersionedNodePath(nodeBinary)) {
56
+ deferred.push(name.slice(0, -".plist".length));
57
+ continue;
58
+ }
59
+ const hqBinary = preferStableHqBinary(args[1]);
60
+ const nextArgs = [options.launcherBinary, "--hq-launchd-task", nodeBinary, hqBinary, ...args.slice(2)];
61
+ const renderedArgs = plistArgs(nextArgs);
62
+ let updated = source.replace(/<key>ProgramArguments<\/key>\s*<array>[\s\S]*?<\/array>/, renderedArgs);
63
+ if (options.associatedBundleIdentifier && !/<key>AssociatedBundleIdentifiers<\/key>/.test(updated)) {
64
+ const attribution = ` <key>AssociatedBundleIdentifiers</key>\n <array>\n <string>${xml(options.associatedBundleIdentifier)}</string>\n </array>\n`;
65
+ updated = updated.replace(/(<key>Label<\/key>\s*<string>[^<]+<\/string>\s*)/, `$1${attribution}`);
66
+ }
67
+ if (updated === source)
68
+ throw new Error(`Could not safely rewrite ${fullPath}`);
69
+ const tmp = `${fullPath}.${process.pid}.migration.tmp`;
70
+ files.writeFileSync(tmp, updated, { mode: 0o600 });
71
+ files.renameSync(tmp, fullPath);
72
+ try {
73
+ activateLaunchd(fullPath, options.run, options.uid, { sleep: options.sleep });
74
+ }
75
+ catch (error) {
76
+ throw new Error(`Could not activate migrated HQ LaunchAgent ${name}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
77
+ }
78
+ migrated.push(label);
79
+ }
80
+ // A deferred/self/unsupported item is retried at most daily, rather than
81
+ // forcing every ordinary CLI invocation to rescan the LaunchAgents folder.
82
+ writeLaunchdMigrationMarker(options.home, deferred);
83
+ return { migrated, backupsRemoved, deferred, alreadyDone: false };
84
+ }
85
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1,24 @@
1
+ import type { RunCommand } from "./launchd.js";
2
+ export interface LaunchdStartupMigrationOptions {
3
+ home: string;
4
+ uid: number;
5
+ run?: RunCommand;
6
+ processArgs: string[];
7
+ xpcServiceName?: string;
8
+ }
9
+ export interface LaunchdStartupMigrationDependencies {
10
+ shouldRun?: (home: string) => boolean | Promise<boolean>;
11
+ discover?: (home: string) => {
12
+ launcherBinary?: string;
13
+ associatedBundleIdentifier?: string;
14
+ };
15
+ migrate?: (options: LaunchdStartupMigrationOptions & {
16
+ launcherBinary?: string;
17
+ associatedBundleIdentifier?: string;
18
+ }) => unknown;
19
+ markDeferred?: (home: string) => void;
20
+ warn?: (message: string) => void;
21
+ }
22
+ /** Best-effort migration; a compatibility cleanup never blocks the requested command. */
23
+ export declare function runLaunchdMigrationAtStartup(options: LaunchdStartupMigrationOptions, dependencies?: LaunchdStartupMigrationDependencies): Promise<void>;
24
+ //# sourceMappingURL=startup-migration.d.ts.map
@@ -0,0 +1,27 @@
1
+ /** Best-effort migration; a compatibility cleanup never blocks the requested command. */
2
+ export async function runLaunchdMigrationAtStartup(options, dependencies = {}) {
3
+ try {
4
+ const state = await import("./migration-state.js");
5
+ const shouldRun = dependencies.shouldRun ?? state.shouldRunLaunchdMigration;
6
+ if (!await shouldRun(options.home))
7
+ return;
8
+ const launchd = await import("./launchd.js");
9
+ const migration = await import("./migration.js");
10
+ const discover = dependencies.discover ?? launchd.discoverLaunchdIdentity;
11
+ const migrate = dependencies.migrate ?? migration.migrateLegacyNodeLaunchAgents;
12
+ migrate({ ...options, run: options.run ?? launchd.defaultRunCommand, ...discover(options.home) });
13
+ }
14
+ catch {
15
+ try {
16
+ if (dependencies.markDeferred)
17
+ dependencies.markDeferred(options.home);
18
+ else {
19
+ const state = await import("./migration-state.js");
20
+ state.writeLaunchdMigrationMarker(options.home, ["migration-error"]);
21
+ }
22
+ }
23
+ catch { /* Keep the requested command available even if state cannot be saved. */ }
24
+ (dependencies.warn ?? console.warn)("hq: legacy LaunchAgent migration failed; continuing");
25
+ }
26
+ }
27
+ //# sourceMappingURL=startup-migration.js.map
@@ -26,6 +26,9 @@ export interface ServiceHostPaths {
26
26
  home: string;
27
27
  nodeBinary: string;
28
28
  hqBinary: string;
29
+ /** Optional signed HQ.app launcher and bundle attribution for macOS. */
30
+ launchdLauncherBinary?: string;
31
+ associatedBundleIdentifier?: string;
29
32
  }
30
33
  export type ServicePlatform = "darwin" | "linux" | "other";
31
34
  export declare function detectServicePlatform(platform?: NodeJS.Platform): ServicePlatform;
package/dist/main.js CHANGED
@@ -166,6 +166,28 @@ program.hook("preAction", async () => {
166
166
  });
167
167
  });
168
168
  export async function runCli() {
169
+ const xpcServiceName = process.env.XPC_SERVICE_NAME ?? "";
170
+ const launchedByHqAgent = xpcServiceName.startsWith("ai.indigo.") || xpcServiceName.startsWith("ai.getindigo.");
171
+ const isDaemonEntryPoint = process.argv[2] === "daemon" && process.argv[3] === "run";
172
+ const isBotHotPath = process.argv[2] === "bot" && process.argv[3] === "run";
173
+ if (process.platform === "darwin" && typeof process.getuid === "function" &&
174
+ !launchedByHqAgent && !isDaemonEntryPoint && !isBotHotPath) {
175
+ const home = os.homedir();
176
+ // The startup helper checks its small marker before importing launchd or
177
+ // inspecting the app bundle. Linux never imports this Darwin-only path.
178
+ try {
179
+ const { runLaunchdMigrationAtStartup } = await import("./lib/service-manager/startup-migration.js");
180
+ await runLaunchdMigrationAtStartup({
181
+ home,
182
+ uid: process.getuid(),
183
+ processArgs: process.argv,
184
+ xpcServiceName,
185
+ });
186
+ }
187
+ catch {
188
+ console.warn("hq: legacy LaunchAgent migration failed; continuing");
189
+ }
190
+ }
169
191
  const foregroundNetworkBudget = createForegroundNetworkBudget();
170
192
  activeForegroundNetworkBudget = foregroundNetworkBudget;
171
193
  // Login must be usable before any identity or company is available, so do
@@ -4,4 +4,8 @@ export declare function nativeFastRouteEnabled(argv: readonly string[], env?: No
4
4
  export declare function fastFlagEnabled(flagKey: string, env?: NodeJS.ProcessEnv, options?: {
5
5
  numericEnvironmentOverride?: boolean;
6
6
  }): Promise<boolean>;
7
+ /** Reads a fail-safe flag that stays enabled unless a fresh value disables it. */
8
+ export declare function fastFlagEnabledDefaultOn(flagKey: string, env?: NodeJS.ProcessEnv, options?: {
9
+ numericEnvironmentOverride?: boolean;
10
+ }): Promise<boolean>;
7
11
  //# sourceMappingURL=native-fast-route.d.ts.map