@indigoai-us/hq-cli 5.345.50 → 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 +8 -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/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/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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.51] — 2026-10-08
|
|
6
|
+
|
|
7
|
+
- Multi-company session locking now defaults on across native CLI readers; an explicit false flag value remains the kill switch.
|
|
8
|
+
|
|
9
|
+
- The Codex skill bridge regression suite now checks that the repo `.agents/skills` mirror exposes every skill from the Claude listing source once.
|
|
10
|
+
- `hq lanes message`, `resume` and `grant` now reach an Outpost-hosted lane that has no local record even when the owner's lane store is too large for one hierarchy read. The lane, its senior, its siblings and its children are read in bounded parts instead of the command failing with `lane_not_found`.
|
|
11
|
+
- HQ CLI compares LaunchAgent plists before writing or reloading them, attributes jobs to an installed HQ.app, uses its signed launcher when available, and migrates legacy Node-first agents once.
|
|
12
|
+
|
|
5
13
|
## [5.345.50] — 2026-10-08
|
|
6
14
|
- The lane keep-alive audit now admits every `done` stop envelope and splits the unrestarted ones by keep-alive eligibility: a stop keep-alive is specified not to wake is reported as `finished_story` or `idle_no_queued_work` instead of being counted as a liveness failure. `stopped_and_not_restarted_on_their_own` keeps its name and its value on the stops the audit already reported.
|
|
7
15
|
- A finished standing lane that receives queued work long after it stopped now gets the normal grace period measured from when that work appeared, so a normal exit from `idle_no_queued_work` is no longer reported as a keep-alive liveness failure.
|
|
@@ -781,11 +781,11 @@ export async function runHqSessionFast(argv, _stdin, cwd, hqRoot) {
|
|
|
781
781
|
|| (commandArgs[0] === "get" && commandArgs[1] === "company_slugs");
|
|
782
782
|
let enabled = false;
|
|
783
783
|
if (needsMultiCompanyFlag) {
|
|
784
|
-
const [{
|
|
784
|
+
const [{ fastFlagEnabledDefaultOn }, { MULTI_COMPANY_SESSION_LOCK_FLAG }] = await Promise.all([
|
|
785
785
|
import("../native-fast-route.js"),
|
|
786
786
|
import("../lib/cli-flag-keys.js"),
|
|
787
787
|
]);
|
|
788
|
-
enabled = await
|
|
788
|
+
enabled = await fastFlagEnabledDefaultOn(MULTI_COMPANY_SESSION_LOCK_FLAG);
|
|
789
789
|
}
|
|
790
790
|
const result = executeHqSession(argv, { cwd, hqRoot }, { multiCompanyLockEnabled: enabled });
|
|
791
791
|
if (result.stdout)
|
|
@@ -4,7 +4,7 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { awkCannotOpenFileDiagnostic } from "../lib/core-utils/awk-diagnostics.js";
|
|
6
6
|
import { buildCmdExeSpawnPlan, isWindowsBatchFile } from "../utils/windows-spawn.js";
|
|
7
|
-
import {
|
|
7
|
+
import { fastFlagEnabledDefaultOn } from "../native-fast-route.js";
|
|
8
8
|
export const RESOLVE_COMPANY_ASSET = "core/scripts/resolve-company.sh";
|
|
9
9
|
export const RESOLVE_COMPANY_HELP = [
|
|
10
10
|
"hq-core: public",
|
|
@@ -603,7 +603,7 @@ export async function runResolveCompanyFast(args, stdin, cwd, hqRoot, _commandNa
|
|
|
603
603
|
hqRoot,
|
|
604
604
|
stdin: actualStdin,
|
|
605
605
|
stdinIsTty: process.stdin.isTTY === true,
|
|
606
|
-
multiCompanyLockEnabled: await
|
|
606
|
+
multiCompanyLockEnabled: await fastFlagEnabledDefaultOn("hooks.multi-company-session-lock"),
|
|
607
607
|
});
|
|
608
608
|
process.stdout.write(result.stdout);
|
|
609
609
|
if (result.stderr.length)
|
package/dist/commands/lanes.js
CHANGED
|
@@ -947,6 +947,7 @@ function laneListQueryFlags(query) {
|
|
|
947
947
|
...(query.laneUlids?.length ? ["--lane-ulid"] : []),
|
|
948
948
|
...(query.active ? ["--active"] : []),
|
|
949
949
|
...(query.hierarchy?.length ? ["--hierarchy"] : []),
|
|
950
|
+
...(query.senior !== undefined ? ["--senior"] : []),
|
|
950
951
|
...(query.snapshot ? ["--snapshot"] : []),
|
|
951
952
|
];
|
|
952
953
|
}
|
|
@@ -1404,6 +1405,7 @@ function normalizedLaneListQuery(query) {
|
|
|
1404
1405
|
...(laneUlids === undefined ? {} : { laneUlids }),
|
|
1405
1406
|
...(query.active ? { active: true } : {}),
|
|
1406
1407
|
...(hierarchy === undefined ? {} : { hierarchy }),
|
|
1408
|
+
...(query.senior === undefined ? {} : { senior: query.senior }),
|
|
1407
1409
|
...(query.snapshot ? { snapshot: true } : {}),
|
|
1408
1410
|
...(query.strictSnapshot ? { strictSnapshot: true } : {}),
|
|
1409
1411
|
...(query.includeForeignHostRows ? { includeForeignHostRows: true } : {}),
|
|
@@ -1503,6 +1505,48 @@ async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}, dispa
|
|
|
1503
1505
|
}
|
|
1504
1506
|
return invocationMemo.getOrCreate(outpostId, query, () => readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOptions));
|
|
1505
1507
|
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Read the owner rows the hire-grant context for `laneId` is built from.
|
|
1510
|
+
*
|
|
1511
|
+
* The owner's `--hierarchy` selector also returns every lane that shares the
|
|
1512
|
+
* target's company and project, so on a store with hundreds of lanes in one
|
|
1513
|
+
* project the response reaches the Outpost exec output limit and the read
|
|
1514
|
+
* fails before a single row is parsed. When that happens, and the target has a
|
|
1515
|
+
* lane senior, assemble the rows the context builder needs from bounded reads
|
|
1516
|
+
* every owner CLI supports: the target and its senior by id, the senior's
|
|
1517
|
+
* direct children (the target's siblings) and the target's own children by
|
|
1518
|
+
* `--senior`. The owner rejects `--senior` together with `--snapshot`, so the
|
|
1519
|
+
* two `--senior` reads use the plain JSON list; the dispatched command sets
|
|
1520
|
+
* HQ_LANES_REMOTE_READ=1, which keeps that list from refreshing remote
|
|
1521
|
+
* mirrors, and the view-only fields it adds are stripped before parsing. A
|
|
1522
|
+
* session-senior target needs same-company profile rows that no bounded
|
|
1523
|
+
* filter selects, so its oversized read stays an error.
|
|
1524
|
+
*/
|
|
1525
|
+
async function readOutpostLaneHierarchy(ctx, outpostId, laneId) {
|
|
1526
|
+
let tooLarge;
|
|
1527
|
+
try {
|
|
1528
|
+
return await readOutpostLaneRecords(ctx, outpostId, { hierarchy: [laneId], snapshot: true });
|
|
1529
|
+
}
|
|
1530
|
+
catch (err) {
|
|
1531
|
+
if (!isLanesError(err) || err.error !== "outpost_lane_list_too_large")
|
|
1532
|
+
throw err;
|
|
1533
|
+
tooLarge = err;
|
|
1534
|
+
}
|
|
1535
|
+
const targets = await readOutpostLaneRecords(ctx, outpostId, { laneIds: [laneId], snapshot: true });
|
|
1536
|
+
const target = targets.find((lane) => lane.lane_id === laneId);
|
|
1537
|
+
if (target === undefined)
|
|
1538
|
+
return targets;
|
|
1539
|
+
if (target.senior.kind !== "lane" || !target.senior.id) {
|
|
1540
|
+
throw new LanesError(tooLarge.error, `${tooLarge.message}; lane ${laneId} has a ${target.senior.kind} senior, whose hierarchy cannot be read in bounded parts`, { ...tooLarge.details, lane_id: laneId, senior: target.senior });
|
|
1541
|
+
}
|
|
1542
|
+
const seniorId = target.senior.id;
|
|
1543
|
+
const [seniorRows, siblings, children] = await Promise.all([
|
|
1544
|
+
readOutpostLaneRecords(ctx, outpostId, { laneIds: [seniorId], snapshot: true }),
|
|
1545
|
+
readOutpostLaneRecords(ctx, outpostId, { senior: `lane:${seniorId}` }),
|
|
1546
|
+
readOutpostLaneRecords(ctx, outpostId, { senior: `lane:${laneId}` }),
|
|
1547
|
+
]);
|
|
1548
|
+
return mergeOwnerLaneRecords([...targets, ...seniorRows, ...siblings], children);
|
|
1549
|
+
}
|
|
1506
1550
|
/** Refresh this host's selected Outpost mirrors in one bounded owner query per Outpost. */
|
|
1507
1551
|
async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, options = {}) {
|
|
1508
1552
|
const result = {
|
|
@@ -1829,9 +1873,12 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1829
1873
|
for (const outpostId of lookupOutpostIds) {
|
|
1830
1874
|
let ownerLanes;
|
|
1831
1875
|
try {
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1876
|
+
// The hierarchy read is shared with the grant context built after
|
|
1877
|
+
// resolution, so one owner round trip serves both whenever the Outpost
|
|
1878
|
+
// exec output limit can carry the owner's response.
|
|
1879
|
+
ownerLanes = hostlessUlidLookup
|
|
1880
|
+
? await readOutpostLaneRecords(ctx, outpostId, { laneUlids: [requestedUlid], snapshot: true })
|
|
1881
|
+
: await readOutpostLaneHierarchy(ctx, outpostId, laneId);
|
|
1835
1882
|
probedOutpostIds.push(outpostId);
|
|
1836
1883
|
}
|
|
1837
1884
|
catch (err) {
|
|
@@ -4665,20 +4712,14 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4665
4712
|
let grantContext;
|
|
4666
4713
|
let grantHierarchy;
|
|
4667
4714
|
if (action === "grant") {
|
|
4668
|
-
const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await
|
|
4669
|
-
hierarchy: [lane.lane_id],
|
|
4670
|
-
snapshot: true,
|
|
4671
|
-
}));
|
|
4715
|
+
const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneHierarchy(ctx, outpostId, lane.lane_id));
|
|
4672
4716
|
const hierarchy = await hierarchyWithVerifiedRemoteSenior(ctx, ctx.hqRoot(), lane, ownerHierarchy);
|
|
4673
4717
|
grantHierarchy = hierarchy;
|
|
4674
4718
|
grantContext = createOutpostHireGrantContext(lane, hierarchy);
|
|
4675
4719
|
}
|
|
4676
4720
|
else if ((action === "resume" && lane.senior.kind === "lane") ||
|
|
4677
4721
|
(action === "message" && (lane.senior.kind === "lane" || lane.senior.kind === "session"))) {
|
|
4678
|
-
const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await
|
|
4679
|
-
hierarchy: [lane.lane_id],
|
|
4680
|
-
snapshot: true,
|
|
4681
|
-
}));
|
|
4722
|
+
const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneHierarchy(ctx, outpostId, lane.lane_id));
|
|
4682
4723
|
const hierarchy = await hierarchyWithVerifiedRemoteSenior(ctx, ctx.hqRoot(), lane, ownerHierarchy);
|
|
4683
4724
|
grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined, action === "message" ? options.supersededMessageSessionId : undefined);
|
|
4684
4725
|
}
|
package/dist/commands/secrets.js
CHANGED
|
@@ -145,7 +145,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
145
145
|
import { removeCacheEntry, clearAllCache } from "../utils/secrets-cache.js";
|
|
146
146
|
import { isHqCliChild, releaseChildWrapperPackageUseLeaseAfterStart } from "../utils/child-wrapper-lease.js";
|
|
147
147
|
import { resolveDefaultHqRoot } from "../utils/hq-root-resolver.js";
|
|
148
|
-
import {
|
|
148
|
+
import { fastFlagEnabledDefaultOn } from "../native-fast-route.js";
|
|
149
149
|
import { MULTI_COMPANY_SESSION_LOCK_FLAG } from "../lib/cli-flag-keys.js";
|
|
150
150
|
import { resolveSessionId } from "../lib/core-utils/session-id.js";
|
|
151
151
|
import { secretsExecChildExitCode } from "../utils/child-wrapper-exit.js";
|
|
@@ -1249,7 +1249,7 @@ export function registerSecretsCommand(program) {
|
|
|
1249
1249
|
process.exit(1);
|
|
1250
1250
|
}
|
|
1251
1251
|
const keys = parseSecretNameList(_opts.only);
|
|
1252
|
-
const multiCompanyLockEnabled = await
|
|
1252
|
+
const multiCompanyLockEnabled = await fastFlagEnabledDefaultOn(MULTI_COMPANY_SESSION_LOCK_FLAG);
|
|
1253
1253
|
const sessionLock = multiCompanyLockEnabled ? readSessionCompanyLock(process.env) : undefined;
|
|
1254
1254
|
const parentScope = secrets.opts();
|
|
1255
1255
|
const inheritedCompany = parentScope.company;
|
package/dist/commands/workers.js
CHANGED
|
@@ -7,7 +7,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
7
7
|
import { vaultApiFetch, getCompanyUid } from "./secrets.js";
|
|
8
8
|
import { GROUP_ID_PATTERN, EMAIL_PATTERN, toSharedFolderGlob } from "./_patterns.js";
|
|
9
9
|
import { findHqRoot } from "../utils/manifest.js";
|
|
10
|
-
import {
|
|
10
|
+
import { fastFlagEnabledDefaultOn } from "../native-fast-route.js";
|
|
11
11
|
import { MULTI_COMPANY_SESSION_LOCK_FLAG } from "../lib/cli-flag-keys.js";
|
|
12
12
|
import { resolveSessionId } from "../lib/core-utils/session-id.js";
|
|
13
13
|
export { readWorkerRegistry } from "../lib/workers-registry/read.js";
|
|
@@ -198,7 +198,7 @@ async function runWorkersShare(workerId, opts) {
|
|
|
198
198
|
}
|
|
199
199
|
async function runWorkersList(opts) {
|
|
200
200
|
const hqRoot = findHqRoot();
|
|
201
|
-
const enabled = await
|
|
201
|
+
const enabled = await fastFlagEnabledDefaultOn(MULTI_COMPANY_SESSION_LOCK_FLAG);
|
|
202
202
|
const lockedCompanies = resolveActiveCompanies(hqRoot, enabled);
|
|
203
203
|
if (enabled && opts.company && denyWorkerCompanyOutsideLock(opts.company, lockedCompanies))
|
|
204
204
|
return;
|
package/dist/lib/bot/daemon.d.ts
CHANGED
|
@@ -64,7 +64,7 @@ export interface BotDaemonDeps {
|
|
|
64
64
|
platform?: BotDaemonPlatform;
|
|
65
65
|
launchctl?: LaunchctlExec;
|
|
66
66
|
uid?: number;
|
|
67
|
-
fsImpl?: Pick<typeof fs, "existsSync" | "writeFileSync" | "mkdirSync" | "unlinkSync"
|
|
67
|
+
fsImpl?: Pick<typeof fs, "existsSync" | "readFileSync" | "writeFileSync" | "mkdirSync" | "unlinkSync"> & Partial<Pick<typeof fs, "chmodSync" | "renameSync">>;
|
|
68
68
|
/** Blocking sleep, injectable for tests. */
|
|
69
69
|
sleepMs?: (ms: number) => void;
|
|
70
70
|
/**
|
package/dist/lib/bot/daemon.js
CHANGED
|
@@ -15,7 +15,7 @@ import * as path from "node:path";
|
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
16
|
import { resolveHqBinary, resolveNodeBinary, preferStableHqBinary } from "../mesh/live/daemon/install.js";
|
|
17
17
|
import { botLogPath, launchdLabel } from "./paths.js";
|
|
18
|
-
import { reloadLaunchd } from "../service-manager/launchd.js";
|
|
18
|
+
import { discoverLaunchdIdentity, installLaunchdPlist, launchdProgramArguments, launchdPlistProgramArguments as parsePlistArguments, parseLaunchdCommand, reloadLaunchd } from "../service-manager/launchd.js";
|
|
19
19
|
import { nodeBinaryProblem, preferStableNodeBinary } from "../node-binary.js";
|
|
20
20
|
export function detectBotPlatform(platform = process.platform) {
|
|
21
21
|
if (platform === "darwin")
|
|
@@ -66,6 +66,12 @@ function escapeXml(value) {
|
|
|
66
66
|
.replace(/"/g, """);
|
|
67
67
|
}
|
|
68
68
|
export function renderBotLaunchdPlist(p) {
|
|
69
|
+
const identity = discoverLaunchdIdentity(p.home);
|
|
70
|
+
const args = launchdProgramArguments({
|
|
71
|
+
nodeBinary: p.nodeBinary,
|
|
72
|
+
hqBinary: p.hqBinary,
|
|
73
|
+
launchdLauncherBinary: identity.launcherBinary,
|
|
74
|
+
}, ["bot", "run", p.name]);
|
|
69
75
|
const envRows = [
|
|
70
76
|
["HOME", p.home],
|
|
71
77
|
["PATH", p.pathEntries.join(":")],
|
|
@@ -80,13 +86,9 @@ export function renderBotLaunchdPlist(p) {
|
|
|
80
86
|
<dict>
|
|
81
87
|
<key>Label</key>
|
|
82
88
|
<string>${escapeXml(p.label)}</string>
|
|
83
|
-
<key>ProgramArguments</key>
|
|
89
|
+
${identity.associatedBundleIdentifier ? ` <key>AssociatedBundleIdentifiers</key>\n <array>\n <string>${escapeXml(identity.associatedBundleIdentifier)}</string>\n </array>\n` : ""} <key>ProgramArguments</key>
|
|
84
90
|
<array>
|
|
85
|
-
<string>${escapeXml(
|
|
86
|
-
<string>${escapeXml(p.hqBinary)}</string>
|
|
87
|
-
<string>bot</string>
|
|
88
|
-
<string>run</string>
|
|
89
|
-
<string>${escapeXml(p.name)}</string>
|
|
91
|
+
${args.map((arg) => ` <string>${escapeXml(arg)}</string>`).join("\n")}
|
|
90
92
|
</array>
|
|
91
93
|
<key>WorkingDirectory</key>
|
|
92
94
|
<string>${escapeXml(p.hqRoot ?? p.botDir)}</string>
|
|
@@ -190,16 +192,13 @@ function launchdLoaded(p, deps) {
|
|
|
190
192
|
return deps.launchctl(["print", `${launchdDomain(deps.uid)}/${p.label}`]).ok;
|
|
191
193
|
}
|
|
192
194
|
export function launchdPlistProgramArguments(plist) {
|
|
193
|
-
|
|
194
|
-
if (!block)
|
|
195
|
-
return [];
|
|
196
|
-
return [...block[1].matchAll(/<string>([^<]*)<\/string>/g)].map((m) => m[1]);
|
|
195
|
+
return parsePlistArguments(plist);
|
|
197
196
|
}
|
|
198
197
|
/** The hq script path baked into an installed unit, or null if none. */
|
|
199
198
|
export function installedDaemonHqBinary(p, io = fs) {
|
|
200
199
|
if (io.existsSync(p.plistPath)) {
|
|
201
200
|
const args = launchdPlistProgramArguments(io.readFileSync(p.plistPath, "utf8"));
|
|
202
|
-
return args
|
|
201
|
+
return parseLaunchdCommand(args)?.hq ?? null;
|
|
203
202
|
}
|
|
204
203
|
if (io.existsSync(p.systemdUnitPath)) {
|
|
205
204
|
const unit = io.readFileSync(p.systemdUnitPath, "utf8");
|
|
@@ -212,7 +211,7 @@ export function installedDaemonHqBinary(p, io = fs) {
|
|
|
212
211
|
export function installedDaemonNodeBinary(p, io = fs) {
|
|
213
212
|
if (io.existsSync(p.plistPath)) {
|
|
214
213
|
const args = launchdPlistProgramArguments(io.readFileSync(p.plistPath, "utf8"));
|
|
215
|
-
return args
|
|
214
|
+
return parseLaunchdCommand(args)?.node ?? null;
|
|
216
215
|
}
|
|
217
216
|
if (io.existsSync(p.systemdUnitPath)) {
|
|
218
217
|
const unit = io.readFileSync(p.systemdUnitPath, "utf8");
|
|
@@ -383,19 +382,27 @@ export function installBotDaemon(p, deps = {}) {
|
|
|
383
382
|
io.mkdirSync(path.dirname(p.logPath), { recursive: true, mode: 0o700 });
|
|
384
383
|
if (platform === "darwin") {
|
|
385
384
|
io.mkdirSync(path.dirname(p.plistPath), { recursive: true });
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
385
|
+
const reloadState = { result: null };
|
|
386
|
+
const written = installLaunchdPlist(p.plistPath, p.label, renderBotLaunchdPlist(p), {
|
|
387
|
+
files: {
|
|
388
|
+
existsSync: io.existsSync,
|
|
389
|
+
readFileSync: (file) => io.readFileSync(file, "utf8"),
|
|
390
|
+
writeFileSync: io.writeFileSync,
|
|
391
|
+
...(io.chmodSync ? { chmodSync: io.chmodSync } : {}),
|
|
392
|
+
...(io.renameSync ? { renameSync: io.renameSync } : {}),
|
|
393
|
+
unlinkSync: io.unlinkSync,
|
|
394
|
+
},
|
|
395
|
+
activate: Boolean(deps.launchctl),
|
|
396
|
+
isLoaded: () => launchdLoaded(p, deps),
|
|
397
|
+
...(deps.launchctl ? { reload: () => { reloadState.result = rebootstrapLaunchd(p, deps); return reloadState.result; } } : {}),
|
|
398
|
+
});
|
|
399
|
+
let loaded = written.loaded;
|
|
400
|
+
if (deps.launchctl && reloadState.result) {
|
|
401
|
+
const r = reloadState.result;
|
|
402
|
+
loaded = r.loaded;
|
|
403
|
+
if (!r?.ok) {
|
|
404
|
+
const detail = `launchctl bootstrap failed after ${r?.bootstrapAttempts ?? 0} attempts: ${(r?.error ?? "").slice(0, 200)}`;
|
|
405
|
+
if (r?.loaded) {
|
|
399
406
|
// The previous definition survived the bootout; keep it running.
|
|
400
407
|
deps.launchctl(["kickstart", "-k", `${launchdDomain(deps.uid)}/${p.label}`]);
|
|
401
408
|
}
|
|
@@ -403,14 +410,13 @@ export function installBotDaemon(p, deps = {}) {
|
|
|
403
410
|
platform,
|
|
404
411
|
action: "install",
|
|
405
412
|
installed: true,
|
|
406
|
-
loaded: r
|
|
413
|
+
loaded: r?.loaded ?? false,
|
|
407
414
|
dest: p.plistPath,
|
|
408
|
-
message: r
|
|
415
|
+
message: r?.loaded
|
|
409
416
|
? `Wrote ${p.plistPath} but ${detail}. The previous definition of ${p.label} is still loaded and was restarted; the rewritten plist is not active yet.`
|
|
410
417
|
: `Wrote ${p.plistPath} but ${detail}. ${p.label} is NOT loaded. Load it with: launchctl bootstrap ${launchdDomain(deps.uid)} ${p.plistPath}`,
|
|
411
418
|
};
|
|
412
419
|
}
|
|
413
|
-
loaded = true;
|
|
414
420
|
}
|
|
415
421
|
return {
|
|
416
422
|
platform,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import * as fs from "node:fs";
|
|
19
19
|
import * as path from "node:path";
|
|
20
|
-
import { activateLaunchd, deactivateLaunchd, launchdPlistPath, renderLaunchdPlist, } from "../service-manager/launchd.js";
|
|
20
|
+
import { activateLaunchd, deactivateLaunchd, defaultRunCommand, installLaunchdPlist, launchdIsLoaded, launchdPlistPath, renderLaunchdPlist, } from "../service-manager/launchd.js";
|
|
21
21
|
import { activateSystemd, deactivateSystemd, renderSystemdUserUnit, systemdUnitPath, } from "../service-manager/systemd.js";
|
|
22
22
|
import { escapeXml, shellQuote } from "../service-manager/types.js";
|
|
23
23
|
import { readPidLock } from "../mesh/live/daemon/pid-lock.js";
|
|
@@ -251,12 +251,43 @@ export function installDaemonUnit(host, options, deps) {
|
|
|
251
251
|
fs.mkdirSync(path.dirname(plan.unitPath), { recursive: true });
|
|
252
252
|
if (plan.kind !== "systemd-system")
|
|
253
253
|
fs.mkdirSync(path.join(host.daemonDir, "logs"), { recursive: true });
|
|
254
|
-
if (plan.kind === "
|
|
254
|
+
if (plan.kind === "launchd") {
|
|
255
|
+
let plist;
|
|
256
|
+
try {
|
|
257
|
+
plist = installLaunchdPlist(plan.unitPath, DAEMON_LAUNCHD_LABEL, plan.rendered, {
|
|
258
|
+
run: deps.run ?? defaultRunCommand,
|
|
259
|
+
uid: host.uid,
|
|
260
|
+
sleep: deps.sleep,
|
|
261
|
+
isLoaded: () => launchdIsLoaded(DAEMON_LAUNCHD_LABEL, deps.run ?? defaultRunCommand, host.uid),
|
|
262
|
+
files: {
|
|
263
|
+
existsSync: fs.existsSync,
|
|
264
|
+
readFileSync: (file) => fs.readFileSync(file, "utf8"),
|
|
265
|
+
writeFileSync: fs.writeFileSync,
|
|
266
|
+
chmodSync: fs.chmodSync,
|
|
267
|
+
renameSync: fs.renameSync,
|
|
268
|
+
unlinkSync: fs.unlinkSync,
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
return {
|
|
274
|
+
kind: plan.kind,
|
|
275
|
+
unitPath: plan.unitPath,
|
|
276
|
+
installed: fs.existsSync(plan.unitPath),
|
|
277
|
+
running: launchdIsLoaded(DAEMON_LAUNCHD_LABEL, deps.run ?? defaultRunCommand, host.uid),
|
|
278
|
+
error: err instanceof Error ? err.message : String(err),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (!plist.loaded)
|
|
282
|
+
return { kind: plan.kind, unitPath: plan.unitPath, installed: true, running: false };
|
|
283
|
+
}
|
|
284
|
+
else if (plan.kind === "schtasks")
|
|
255
285
|
fs.writeFileSync(plan.unitPath, encodeWindowsTaskXml(plan.rendered));
|
|
256
286
|
else
|
|
257
287
|
fs.writeFileSync(plan.unitPath, plan.rendered, { mode: plan.kind === "systemd-system" ? 0o644 : 0o600 });
|
|
258
288
|
try {
|
|
259
|
-
|
|
289
|
+
if (plan.kind !== "launchd")
|
|
290
|
+
activate(plan, host, deps.run, deps.sleep);
|
|
260
291
|
return { kind: plan.kind, unitPath: plan.unitPath, installed: true, running: true };
|
|
261
292
|
}
|
|
262
293
|
catch (err) {
|
|
@@ -10,6 +10,7 @@ import { botsRoot, botDir, launchdLabel } from "../../bot/paths.js";
|
|
|
10
10
|
import { readBotConfig } from "../../bot/config.js";
|
|
11
11
|
import { isPidAlive, readBotStatus } from "../../bot/status.js";
|
|
12
12
|
import { nodeBinaryProblem } from "../../node-binary.js";
|
|
13
|
+
import { launchdPlistProgramArguments, parseLaunchdCommand } from "../../service-manager/launchd.js";
|
|
13
14
|
export const BOTS_FAMILY_ID = "bots";
|
|
14
15
|
export const BOTS_FAMILY_TITLE = "Local bots";
|
|
15
16
|
const DEFAULT_DEPS = {
|
|
@@ -35,14 +36,8 @@ export function checkBots(_context, deps = DEFAULT_DEPS) {
|
|
|
35
36
|
];
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
function launchdPlistArgs(plist) {
|
|
39
|
-
const block = plist.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
|
|
40
|
-
if (!block)
|
|
41
|
-
return [];
|
|
42
|
-
return [...block[1].matchAll(/<string>([^<]*)<\/string>/g)].map((m) => m[1]);
|
|
43
|
-
}
|
|
44
39
|
function launchdPlistHqBinary(plist) {
|
|
45
|
-
return
|
|
40
|
+
return parseLaunchdCommand(launchdPlistProgramArguments(plist))?.hq ?? null;
|
|
46
41
|
}
|
|
47
42
|
function runChecks(deps) {
|
|
48
43
|
const home = deps.home();
|
|
@@ -122,7 +117,7 @@ function runChecks(deps) {
|
|
|
122
117
|
try {
|
|
123
118
|
const text = fs.readFileSync(plistPath, "utf8");
|
|
124
119
|
hqPath = launchdPlistHqBinary(text);
|
|
125
|
-
nodePath =
|
|
120
|
+
nodePath = parseLaunchdCommand(launchdPlistProgramArguments(text))?.node ?? null;
|
|
126
121
|
}
|
|
127
122
|
catch {
|
|
128
123
|
hqPath = null;
|
|
@@ -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
|
|
@@ -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"),
|