@indigoai-us/hq-cli 5.345.49 → 5.345.51
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 +15 -0
- package/dist/command-catalog.generated.d.ts +56 -11
- package/dist/command-catalog.generated.js +67 -11
- package/dist/commands/core-hq-session-native.js +2 -2
- package/dist/commands/core-resolve-company-native.js +2 -2
- package/dist/commands/db-destroy.js +13 -11
- package/dist/commands/db-dump.js +3 -3
- package/dist/commands/db-remote-deps.d.ts +3 -0
- package/dist/commands/db-remote-deps.js +13 -0
- package/dist/commands/db-sql.js +4 -3
- package/dist/commands/db-usage.js +4 -3
- package/dist/commands/dm-forward-flag.d.ts +4 -0
- package/dist/commands/dm-forward-flag.js +7 -0
- package/dist/commands/dm.d.ts +8 -0
- package/dist/commands/dm.js +219 -27
- package/dist/commands/lanes.js +52 -11
- package/dist/commands/secrets.js +2 -2
- package/dist/commands/workers.js +2 -2
- package/dist/lib/bot/daemon.d.ts +1 -1
- package/dist/lib/bot/daemon.js +35 -29
- package/dist/lib/daemon/hq-cloud.d.ts +2 -2
- package/dist/lib/daemon/hq-cloud.js +38 -9
- package/dist/lib/daemon/install.js +34 -3
- package/dist/lib/daemon/run.js +45 -19
- package/dist/lib/db/app-ref.d.ts +21 -0
- package/dist/lib/db/app-ref.js +45 -0
- package/dist/lib/doctor/checks/bots.js +3 -8
- package/dist/lib/lanes/keep-alive-audit.d.ts +5 -1
- package/dist/lib/lanes/keep-alive-audit.js +85 -3
- 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 +1 -1
|
@@ -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
|
|
@@ -28,6 +28,26 @@ export async function fastFlagEnabled(flagKey, env = process.env, options = {})
|
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/** Reads a fail-safe flag that stays enabled unless a fresh value disables it. */
|
|
32
|
+
export async function fastFlagEnabledDefaultOn(flagKey, env = process.env, options = {}) {
|
|
33
|
+
try {
|
|
34
|
+
const override = nativeRouteOverride(flagKey, env, options.numericEnvironmentOverride !== false);
|
|
35
|
+
if (override !== null)
|
|
36
|
+
return override;
|
|
37
|
+
const [{ resolveProcessFlagCompanyContext }, { readFlagSnapshotFile }] = await Promise.all([
|
|
38
|
+
import("./lib/process-flag-company-context.js"),
|
|
39
|
+
import("./lib/flag-snapshot-file.js"),
|
|
40
|
+
]);
|
|
41
|
+
const { companyUid } = resolveProcessFlagCompanyContext(env);
|
|
42
|
+
if (!companyUid)
|
|
43
|
+
return true;
|
|
44
|
+
return readFlagSnapshotFile(companyUid, { env })?.flags[flagKey] !== false;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// This flag is a kill switch: missing scope or cache data must leave the feature enabled.
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
31
51
|
/** The route's documented CLI override also accepts the conventional 1/0. */
|
|
32
52
|
function nativeRouteOverride(flagKey, env, numericEnvironmentOverride) {
|
|
33
53
|
const override = readLocalOverride(flagKey, env);
|
|
@@ -41,7 +41,7 @@ export const RESIDENT_PACKAGE_MODULES = {
|
|
|
41
41
|
],
|
|
42
42
|
"bot-logs-follow": [],
|
|
43
43
|
mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js", "../lib/anywhere-person-setting.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud", "../sentry.js", "../lib/doctor/client-health-reason-codes-flag.js", "@indigoai-us/hq-flags-client"],
|
|
44
|
-
childWrapper: ["../main.js"],
|
|
44
|
+
childWrapper: ["../main.js", "../lib/service-manager/startup-migration.js"],
|
|
45
45
|
coreJobRun: ["../lib/flag-registry.js"],
|
|
46
46
|
"daemon-call": ["../lib/daemon/protocol.js"],
|
|
47
47
|
daemon: [
|
|
@@ -200,6 +200,7 @@ const LOADERS = {
|
|
|
200
200
|
],
|
|
201
201
|
childWrapper: [
|
|
202
202
|
() => import("../main.js"),
|
|
203
|
+
() => import("../lib/service-manager/startup-migration.js"),
|
|
203
204
|
],
|
|
204
205
|
coreJobRun: [
|
|
205
206
|
() => import("../lib/flag-registry.js"),
|