@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.
- package/CHANGELOG.md +11 -0
- package/dist/commands/core-hq-session-native.js +2 -2
- package/dist/commands/core-resolve-company-native.js +2 -2
- package/dist/commands/lanes.js +52 -11
- package/dist/commands/secrets.js +2 -2
- package/dist/commands/workers.js +2 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +40293 -13770
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/daemon.d.ts +1 -1
- package/dist/lib/bot/daemon.js +35 -29
- package/dist/lib/daemon/install.js +34 -3
- package/dist/lib/doctor/checks/bots.js +3 -8
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -0
- package/dist/lib/lanes/outpost-dispatch.js +2 -0
- package/dist/lib/mesh/live/daemon/install.js +28 -15
- package/dist/lib/service-manager/index.d.ts +3 -0
- package/dist/lib/service-manager/index.js +22 -6
- package/dist/lib/service-manager/launchd.d.ts +40 -3
- package/dist/lib/service-manager/launchd.js +106 -5
- package/dist/lib/service-manager/migration-state.d.ts +12 -0
- package/dist/lib/service-manager/migration-state.js +46 -0
- package/dist/lib/service-manager/migration.d.ts +22 -0
- package/dist/lib/service-manager/migration.js +85 -0
- package/dist/lib/service-manager/startup-migration.d.ts +24 -0
- package/dist/lib/service-manager/startup-migration.js +27 -0
- package/dist/lib/service-manager/types.d.ts +3 -0
- package/dist/main.js +22 -0
- package/dist/native-fast-route.d.ts +4 -0
- package/dist/native-fast-route.js +20 -0
- package/dist/utils/resident-package-preload.js +2 -1
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6
|
-
*
|
|
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
|
|
6
|
-
*
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|