@indigoai-us/hq-cli 5.200.1 → 5.201.1
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 +20 -0
- package/dist/command-catalog.generated.d.ts +16 -0
- package/dist/command-catalog.generated.js +21 -0
- package/dist/commands/daemon.js +34 -0
- package/dist/commands/lanes.js +32 -6
- package/dist/lib/daemon/catalog.d.ts +2 -0
- package/dist/lib/daemon/catalog.js +1 -0
- package/dist/lib/daemon/config.d.ts +2 -0
- package/dist/lib/daemon/config.js +6 -0
- package/dist/lib/daemon/hq-cloud.d.ts +2 -2
- package/dist/lib/daemon/hq-cloud.js +22 -5
- package/dist/lib/daemon/legacy-sync-env.d.ts +10 -0
- package/dist/lib/daemon/legacy-sync-env.js +58 -0
- package/dist/lib/daemon/run.js +2 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.201.1] — 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Outpost grant checks now include child lanes from the owning Outpost. Owner
|
|
10
|
+
records take precedence over duplicate primary records, and each grant
|
|
11
|
+
command reads the owner lane list once.
|
|
12
|
+
- Outpost grant checks re-read owner lanes under the hire lock and use the
|
|
13
|
+
validated Outpost CLI override for the lane-list probe.
|
|
14
|
+
|
|
15
|
+
## [5.201.0] — 2026-09-25
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `hq daemon pin-hq-cloud <version>` pins the daemon-managed hq-cloud version to an exact semver within the bundled major, including prereleases; `--clear` removes the pin.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- The daemon carries `Environment=` assignments from Linux `outpost-sync.service` drop-ins into its sync unit.
|
|
24
|
+
|
|
5
25
|
## [5.200.1] — 2026-09-25
|
|
6
26
|
|
|
7
27
|
### Fixed
|
|
@@ -8444,6 +8444,22 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8444
8444
|
}];
|
|
8445
8445
|
readonly options: readonly [];
|
|
8446
8446
|
readonly subcommands: readonly [];
|
|
8447
|
+
}, {
|
|
8448
|
+
readonly name: "pin-hq-cloud";
|
|
8449
|
+
readonly description: "Pin hq-cloud to an exact version, or clear the pin";
|
|
8450
|
+
readonly aliases: readonly [];
|
|
8451
|
+
readonly hidden: false;
|
|
8452
|
+
readonly usage: "[options] [version]";
|
|
8453
|
+
readonly arguments: readonly [{
|
|
8454
|
+
readonly name: "version";
|
|
8455
|
+
readonly required: false;
|
|
8456
|
+
readonly variadic: false;
|
|
8457
|
+
}];
|
|
8458
|
+
readonly options: readonly [{
|
|
8459
|
+
readonly flags: "--clear";
|
|
8460
|
+
readonly description: "Clear the saved hq-cloud version pin";
|
|
8461
|
+
}];
|
|
8462
|
+
readonly subcommands: readonly [];
|
|
8447
8463
|
}, {
|
|
8448
8464
|
readonly name: "migrate";
|
|
8449
8465
|
readonly description: "List the older HQ background units the daemon would take over; --apply takes them over";
|
|
@@ -10918,6 +10918,27 @@ export const COMMAND_CATALOG = [
|
|
|
10918
10918
|
"options": [],
|
|
10919
10919
|
"subcommands": []
|
|
10920
10920
|
},
|
|
10921
|
+
{
|
|
10922
|
+
"name": "pin-hq-cloud",
|
|
10923
|
+
"description": "Pin hq-cloud to an exact version, or clear the pin",
|
|
10924
|
+
"aliases": [],
|
|
10925
|
+
"hidden": false,
|
|
10926
|
+
"usage": "[options] [version]",
|
|
10927
|
+
"arguments": [
|
|
10928
|
+
{
|
|
10929
|
+
"name": "version",
|
|
10930
|
+
"required": false,
|
|
10931
|
+
"variadic": false
|
|
10932
|
+
}
|
|
10933
|
+
],
|
|
10934
|
+
"options": [
|
|
10935
|
+
{
|
|
10936
|
+
"flags": "--clear",
|
|
10937
|
+
"description": "Clear the saved hq-cloud version pin"
|
|
10938
|
+
}
|
|
10939
|
+
],
|
|
10940
|
+
"subcommands": []
|
|
10941
|
+
},
|
|
10921
10942
|
{
|
|
10922
10943
|
"name": "migrate",
|
|
10923
10944
|
"description": "List the older HQ background units the daemon would take over; --apply takes them over",
|
package/dist/commands/daemon.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
17
|
import * as os from "node:os";
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
+
import { major as semverMajor, valid as validSemver } from "semver";
|
|
19
20
|
import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits } from "../lib/daemon/catalog.js";
|
|
20
21
|
import { applyServiceToggle, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
|
|
21
22
|
import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
|
|
@@ -23,6 +24,7 @@ import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/d
|
|
|
23
24
|
import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
|
|
24
25
|
import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
|
|
25
26
|
import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
|
|
27
|
+
import { bundledHqCloud } from "../lib/daemon/hq-cloud.js";
|
|
26
28
|
import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "../lib/daemon/machine-type.js";
|
|
27
29
|
import { daemonPaths } from "../lib/daemon/paths.js";
|
|
28
30
|
import { writeControlRequest } from "../lib/daemon/requests.js";
|
|
@@ -375,6 +377,33 @@ async function runToggle(d, action, services) {
|
|
|
375
377
|
d.out(line);
|
|
376
378
|
d.out("A running daemon picks this up within 15 seconds.");
|
|
377
379
|
}
|
|
380
|
+
async function runPinHqCloud(d, version, opts) {
|
|
381
|
+
if (opts.clear === Boolean(version)) {
|
|
382
|
+
throw new DaemonCommandError("Pass one exact semver version, or use --clear.");
|
|
383
|
+
}
|
|
384
|
+
const host = currentHost(d);
|
|
385
|
+
const { paths } = host;
|
|
386
|
+
const config = readDaemonConfig(paths.configPath);
|
|
387
|
+
if (opts.clear) {
|
|
388
|
+
delete config.hqCloudVersion;
|
|
389
|
+
writeDaemonConfig(paths.configPath, config);
|
|
390
|
+
handOver(d, host);
|
|
391
|
+
d.out("Cleared the hq-cloud version pin.");
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
if (!version || validSemver(version) !== version) {
|
|
395
|
+
throw new DaemonCommandError(`Invalid hq-cloud version "${version ?? ""}". Pass an exact semver version, including prereleases if needed.`);
|
|
396
|
+
}
|
|
397
|
+
const bundled = bundledHqCloud();
|
|
398
|
+
if (!bundled || semverMajor(bundled.version) !== semverMajor(version)) {
|
|
399
|
+
throw new DaemonCommandError(`The hq-cloud pin must use the same major version as the bundled hq-cloud${bundled ? ` (${semverMajor(bundled.version)})` : ""}.`);
|
|
400
|
+
}
|
|
401
|
+
writeDaemonConfig(paths.configPath, { ...config, hqCloudVersion: version });
|
|
402
|
+
handOver(d, host);
|
|
403
|
+
d.out(`Pinned hq-cloud to ${version}.`);
|
|
404
|
+
}
|
|
405
|
+
d.out("Run `hq daemon trigger updater`; wait for the hq-cloud update or current-version message in `hq daemon logs updater`, then restart `hq daemon restart sync`, `hq daemon restart heartbeat`, and `hq daemon restart session-host`.");
|
|
406
|
+
}
|
|
378
407
|
async function runMigrate(d, opts) {
|
|
379
408
|
const units = d.legacyUnits();
|
|
380
409
|
if (units.length === 0) {
|
|
@@ -585,6 +614,11 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
585
614
|
.command("disable <services...>")
|
|
586
615
|
.description("Turn default services off for this machine")
|
|
587
616
|
.action(wrap((d, services) => runToggle(d, "disable", services)));
|
|
617
|
+
daemon
|
|
618
|
+
.command("pin-hq-cloud [version]")
|
|
619
|
+
.description("Pin hq-cloud to an exact version, or clear the pin")
|
|
620
|
+
.option("--clear", "Clear the saved hq-cloud version pin")
|
|
621
|
+
.action(wrap((d, version, opts) => runPinHqCloud(d, version, opts)));
|
|
588
622
|
daemon
|
|
589
623
|
.command("migrate")
|
|
590
624
|
.description("List the older HQ background units the daemon would take over; --apply takes them over")
|
package/dist/commands/lanes.js
CHANGED
|
@@ -479,6 +479,14 @@ function laneHireFields(lane, lanes) {
|
|
|
479
479
|
hire_summary: hireSummary,
|
|
480
480
|
};
|
|
481
481
|
}
|
|
482
|
+
function mergeOwnerLaneHierarchy(supplied, ownerLanes) {
|
|
483
|
+
const byId = new Map();
|
|
484
|
+
for (const lane of supplied)
|
|
485
|
+
byId.set(lane.lane_id, lane);
|
|
486
|
+
for (const lane of ownerLanes)
|
|
487
|
+
byId.set(lane.lane_id, lane);
|
|
488
|
+
return [...byId.values()];
|
|
489
|
+
}
|
|
482
490
|
function outpostCliOverride(ctx, outpostId) {
|
|
483
491
|
const envKey = `HQ_LANES_OUTPOST_CLI_${outpostId.toUpperCase().replace(/[^A-Z0-9_]/g, "_")}`;
|
|
484
492
|
const configured = ctx.env[envKey];
|
|
@@ -1031,11 +1039,12 @@ async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(h
|
|
|
1031
1039
|
};
|
|
1032
1040
|
}
|
|
1033
1041
|
async function readOutpostLaneRecords(ctx, outpostId) {
|
|
1042
|
+
const hqBinary = outpostCliOverride(ctx, outpostId);
|
|
1034
1043
|
let result;
|
|
1035
1044
|
try {
|
|
1036
1045
|
result = await ctx.dispatchOutpostExec({
|
|
1037
1046
|
outpostId,
|
|
1038
|
-
command: buildOutpostLaneListCommand(),
|
|
1047
|
+
command: buildOutpostLaneListCommand({ hqBinary }),
|
|
1039
1048
|
timeoutSeconds: CAPACITY_PROBE_TIMEOUT_MS / 1000,
|
|
1040
1049
|
waitMs: CAPACITY_PROBE_TIMEOUT_MS,
|
|
1041
1050
|
});
|
|
@@ -3312,9 +3321,20 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3312
3321
|
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch to`, { lane_id: lane.lane_id });
|
|
3313
3322
|
}
|
|
3314
3323
|
const outpostId = lane.host.id;
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3324
|
+
let grantContext;
|
|
3325
|
+
let grantHierarchy;
|
|
3326
|
+
if (action === "grant") {
|
|
3327
|
+
const enforcementEnabled = await ctx.laneHireGrantsEnabled(lane.company);
|
|
3328
|
+
let hierarchy = loadAllLanes(ctx.hqRoot());
|
|
3329
|
+
const ownerLanes = await readOutpostLaneRecords(ctx, outpostId);
|
|
3330
|
+
const byId = new Map(hierarchy.map((record) => [record.lane_id, record]));
|
|
3331
|
+
for (const ownerLane of ownerLanes) {
|
|
3332
|
+
byId.set(ownerLane.lane_id, ownerLane);
|
|
3333
|
+
}
|
|
3334
|
+
hierarchy = [...byId.values()];
|
|
3335
|
+
grantHierarchy = hierarchy;
|
|
3336
|
+
grantContext = createOutpostHireGrantContext(lane, hierarchy, enforcementEnabled);
|
|
3337
|
+
}
|
|
3318
3338
|
let dispatched;
|
|
3319
3339
|
try {
|
|
3320
3340
|
dispatched = await ctx.dispatchOutpostExec({
|
|
@@ -3407,6 +3427,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3407
3427
|
...remote,
|
|
3408
3428
|
lane_id: ownerLane.lane_id,
|
|
3409
3429
|
lane: ownerLane,
|
|
3430
|
+
...laneHireFields(ownerLane, grantHierarchy ?? []),
|
|
3410
3431
|
outpost_id: outpostId,
|
|
3411
3432
|
outpost_command_id: dispatched.commandId,
|
|
3412
3433
|
outpost_transport: "outposts_exec",
|
|
@@ -4518,7 +4539,11 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
|
|
|
4518
4539
|
(!grantContext || !sameHireGrantSenior(current.senior, grantContext.expected_senior))) {
|
|
4519
4540
|
throw new LanesError("hire_grant_senior_changed", `Lane ${laneId}'s senior changed before the Outpost applied the grant update`, { lane_id: laneId, current_senior: current.senior });
|
|
4520
4541
|
}
|
|
4521
|
-
const hierarchy = grantContext
|
|
4542
|
+
const hierarchy = grantContext
|
|
4543
|
+
? authorization === "ssm-owner-dispatch"
|
|
4544
|
+
? mergeOwnerLaneHierarchy(grantContext.hierarchy, loadAllLanes(hqRoot))
|
|
4545
|
+
: grantContext.hierarchy
|
|
4546
|
+
: loadAllLanes(hqRoot);
|
|
4522
4547
|
if (enforce && current.senior.kind === "lane") {
|
|
4523
4548
|
if (!current.senior.id) {
|
|
4524
4549
|
throw new LanesError("hire_grant_senior_invalid", `Lane ${laneId} has a lane senior without an id`, {
|
|
@@ -4604,7 +4629,8 @@ async function runGrantOwnerTarget(ctx, laneId, opts) {
|
|
|
4604
4629
|
const hireGrant = parseHireGrant(opts.hire);
|
|
4605
4630
|
const grantContext = parseOutpostHireGrantContext(opts.grantContext, laneId);
|
|
4606
4631
|
const hqRoot = ctx.hqRoot();
|
|
4607
|
-
|
|
4632
|
+
const updated = await applyHireGrant(ctx, laneId, hireGrant, "ssm-owner-dispatch", grantContext);
|
|
4633
|
+
printGrantResult(hqRoot, updated, mergeOwnerLaneHierarchy(grantContext.hierarchy, loadAllLanes(hqRoot)));
|
|
4608
4634
|
}
|
|
4609
4635
|
async function runBackfillGrants(ctx, opts) {
|
|
4610
4636
|
if (opts.apply && opts.dryRun) {
|
|
@@ -66,6 +66,8 @@ export interface PlanContext {
|
|
|
66
66
|
heartbeatRunnerPath: string | null;
|
|
67
67
|
/** hq-cloud's `hq-outpost-session-host` bin, when the hq-cloud in use has one. */
|
|
68
68
|
sessionHostRunnerPath: string | null;
|
|
69
|
+
/** Environment retained from legacy outpost sync systemd drop-ins. */
|
|
70
|
+
legacySyncEnv: Record<string, string>;
|
|
69
71
|
signedIn: boolean;
|
|
70
72
|
/** ~/.hq-agent/kit.json exists. */
|
|
71
73
|
kitConfigured: boolean;
|
|
@@ -129,6 +129,7 @@ function planService(service, ctx) {
|
|
|
129
129
|
script: ctx.syncRunnerPath,
|
|
130
130
|
args: ["--companies", "--direction", "both", "--on-conflict", "keep", "--hq-root", ctx.hqRoot ?? "", "--watch", "--event-push"],
|
|
131
131
|
},
|
|
132
|
+
env: { ...ctx.legacySyncEnv },
|
|
132
133
|
});
|
|
133
134
|
unit.blocked = firstBlock([
|
|
134
135
|
[ctx.desktopAppManagesSync, "the HQ desktop app is handling sync on this machine"],
|
|
@@ -10,6 +10,8 @@ export interface DaemonConfig {
|
|
|
10
10
|
version: 1;
|
|
11
11
|
machineType?: MachineType;
|
|
12
12
|
hqRoot?: string;
|
|
13
|
+
/** Exact hq-cloud package version preferred by this daemon, including prereleases. */
|
|
14
|
+
hqCloudVersion?: string;
|
|
13
15
|
/** Services turned on here beyond the machine type's defaults. */
|
|
14
16
|
enable: string[];
|
|
15
17
|
/** Default services turned off here. */
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import { valid as validSemver } from "semver";
|
|
9
10
|
import { isServiceName } from "./catalog.js";
|
|
10
11
|
import { isMachineType } from "./machine-type.js";
|
|
11
12
|
import { ensurePrivateDir } from "./paths.js";
|
|
@@ -40,6 +41,9 @@ export function readDaemonConfig(file) {
|
|
|
40
41
|
config.machineType = obj.machineType;
|
|
41
42
|
if (typeof obj.hqRoot === "string" && obj.hqRoot.trim())
|
|
42
43
|
config.hqRoot = obj.hqRoot;
|
|
44
|
+
if (typeof obj.hqCloudVersion === "string" && validSemver(obj.hqCloudVersion) === obj.hqCloudVersion) {
|
|
45
|
+
config.hqCloudVersion = obj.hqCloudVersion;
|
|
46
|
+
}
|
|
43
47
|
return config;
|
|
44
48
|
}
|
|
45
49
|
export function writeDaemonConfig(file, config) {
|
|
@@ -50,6 +54,8 @@ export function writeDaemonConfig(file, config) {
|
|
|
50
54
|
ordered.machineType = config.machineType;
|
|
51
55
|
if (config.hqRoot)
|
|
52
56
|
ordered.hqRoot = config.hqRoot;
|
|
57
|
+
if (config.hqCloudVersion)
|
|
58
|
+
ordered.hqCloudVersion = config.hqCloudVersion;
|
|
53
59
|
ordered.enable = config.enable;
|
|
54
60
|
ordered.disable = config.disable;
|
|
55
61
|
fs.writeFileSync(tmp, `${JSON.stringify(ordered, null, 2)}\n`, { mode: 0o600 });
|
|
@@ -22,7 +22,7 @@ export declare function managedHqCloudRoot(daemonRoot: string): string;
|
|
|
22
22
|
* `usable` says whether a copy has what the daemon runs; a newer managed copy
|
|
23
23
|
* that fails it (an interrupted or damaged install) is passed over.
|
|
24
24
|
*/
|
|
25
|
-
export declare function pickHqCloud(bundled: HqCloudCopy | null, managed: HqCloudCopy | null, usable?: (copy: HqCloudCopy) => boolean): PickedHqCloud | null;
|
|
25
|
+
export declare function pickHqCloud(bundled: HqCloudCopy | null, managed: HqCloudCopy | null, usable?: (copy: HqCloudCopy) => boolean, pinnedVersion?: string): PickedHqCloud | null;
|
|
26
26
|
export interface HqCloudRunners {
|
|
27
27
|
syncRunner: string | null;
|
|
28
28
|
/** `hq-outpost-heartbeat`, which older hq-cloud releases do not have. */
|
|
@@ -32,7 +32,7 @@ export interface HqCloudRunners {
|
|
|
32
32
|
}
|
|
33
33
|
export declare function hqCloudRunners(root: string, exists?: (p: string) => boolean): HqCloudRunners;
|
|
34
34
|
/** Install the newest release of the bundled major version, never older than the bundled copy. */
|
|
35
|
-
export declare function hqCloudInstallCommand(prefix: string, bundledVersion: string, platform?: NodeJS.Platform): {
|
|
35
|
+
export declare function hqCloudInstallCommand(prefix: string, bundledVersion: string, platform?: NodeJS.Platform, pinnedVersion?: string): {
|
|
36
36
|
file: string;
|
|
37
37
|
args: string[];
|
|
38
38
|
shell: boolean;
|
|
@@ -10,6 +10,7 @@ import { spawnSync } from "node:child_process";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { readDaemonConfig } from "./config.js";
|
|
13
14
|
import { buildSpawnPlan } from "../../utils/windows-spawn.js";
|
|
14
15
|
export const HQ_CLOUD_PACKAGE = "@indigoai-us/hq-cloud";
|
|
15
16
|
export function managedHqCloudDir(daemonRoot) {
|
|
@@ -34,11 +35,17 @@ function newer(a, b) {
|
|
|
34
35
|
* `usable` says whether a copy has what the daemon runs; a newer managed copy
|
|
35
36
|
* that fails it (an interrupted or damaged install) is passed over.
|
|
36
37
|
*/
|
|
37
|
-
export function pickHqCloud(bundled, managed, usable = () => true) {
|
|
38
|
+
export function pickHqCloud(bundled, managed, usable = () => true, pinnedVersion) {
|
|
38
39
|
if (!bundled)
|
|
39
40
|
return null;
|
|
40
41
|
const b = parseVersion(bundled.version);
|
|
41
42
|
const m = managed ? parseVersion(managed.version) : null;
|
|
43
|
+
if (pinnedVersion) {
|
|
44
|
+
if (managed && managed.version === pinnedVersion && b && m && m[0] === b[0] && usable(managed)) {
|
|
45
|
+
return { ...managed, source: "managed" };
|
|
46
|
+
}
|
|
47
|
+
return { ...bundled, source: "bundled" };
|
|
48
|
+
}
|
|
42
49
|
if (managed && b && m && m[0] === b[0] && newer(m, b) && usable(managed))
|
|
43
50
|
return { ...managed, source: "managed" };
|
|
44
51
|
return { ...bundled, source: "bundled" };
|
|
@@ -55,9 +62,17 @@ export function hqCloudRunners(root, exists = fs.existsSync) {
|
|
|
55
62
|
};
|
|
56
63
|
}
|
|
57
64
|
/** Install the newest release of the bundled major version, never older than the bundled copy. */
|
|
58
|
-
export function hqCloudInstallCommand(prefix, bundledVersion, platform = process.platform) {
|
|
65
|
+
export function hqCloudInstallCommand(prefix, bundledVersion, platform = process.platform, pinnedVersion) {
|
|
59
66
|
// npm is a .cmd on Windows and needs the shell, so every argument is quoted for it.
|
|
60
|
-
const plan = buildSpawnPlan("npm", [
|
|
67
|
+
const plan = buildSpawnPlan("npm", [
|
|
68
|
+
"install",
|
|
69
|
+
"--prefix",
|
|
70
|
+
prefix,
|
|
71
|
+
"--no-audit",
|
|
72
|
+
"--no-fund",
|
|
73
|
+
"--omit=dev",
|
|
74
|
+
`${HQ_CLOUD_PACKAGE}@${pinnedVersion ?? `^${bundledVersion}`}`,
|
|
75
|
+
], platform);
|
|
61
76
|
return { file: plan.cmd, args: plan.args, shell: plan.shell };
|
|
62
77
|
}
|
|
63
78
|
export function readHqCloudCopy(root) {
|
|
@@ -81,10 +96,12 @@ export function bundledHqCloud() {
|
|
|
81
96
|
}
|
|
82
97
|
/** The hq-cloud the daemon should run right now. */
|
|
83
98
|
export function currentHqCloud(daemonRoot) {
|
|
84
|
-
|
|
99
|
+
const pinnedVersion = readDaemonConfig(path.join(daemonRoot, "config.json")).hqCloudVersion;
|
|
100
|
+
return pickHqCloud(bundledHqCloud(), readHqCloudCopy(managedHqCloudRoot(daemonRoot)), (copy) => hqCloudRunners(copy.root).syncRunner !== null, pinnedVersion);
|
|
85
101
|
}
|
|
86
102
|
/** Install or update the updater's copy, as this (the login) user. */
|
|
87
103
|
export function updateManagedHqCloud(daemonRoot, env) {
|
|
104
|
+
const pinnedVersion = readDaemonConfig(path.join(daemonRoot, "config.json")).hqCloudVersion;
|
|
88
105
|
const bundled = bundledHqCloud();
|
|
89
106
|
const from = currentHqCloud(daemonRoot)?.version ?? null;
|
|
90
107
|
if (!bundled)
|
|
@@ -96,7 +113,7 @@ export function updateManagedHqCloud(daemonRoot, env) {
|
|
|
96
113
|
catch (err) {
|
|
97
114
|
return { ok: false, from, to: null, detail: `could not create ${prefix}: ${err instanceof Error ? err.message : String(err)}` };
|
|
98
115
|
}
|
|
99
|
-
const cmd = hqCloudInstallCommand(prefix, bundled.version);
|
|
116
|
+
const cmd = hqCloudInstallCommand(prefix, bundled.version, process.platform, pinnedVersion);
|
|
100
117
|
const r = spawnSync(cmd.file, cmd.args, {
|
|
101
118
|
encoding: "utf8",
|
|
102
119
|
env,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment assignments kept in the legacy outpost sync service drop-ins.
|
|
3
|
+
* The daemon reads these so taking over outpost-sync.service keeps its tuned
|
|
4
|
+
* sync limits. This module is deliberately tolerant: a bad or missing drop-in
|
|
5
|
+
* must never stop daemon planning.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LEGACY_SYNC_DROPIN_DIR = "/etc/systemd/system/outpost-sync.service.d";
|
|
8
|
+
/** Read simple KEY=VALUE and quoted "KEY=VALUE" Environment directives. */
|
|
9
|
+
export declare function readLegacySyncEnv(platform: NodeJS.Platform, directory?: string): Record<string, string>;
|
|
10
|
+
//# sourceMappingURL=legacy-sync-env.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment assignments kept in the legacy outpost sync service drop-ins.
|
|
3
|
+
* The daemon reads these so taking over outpost-sync.service keeps its tuned
|
|
4
|
+
* sync limits. This module is deliberately tolerant: a bad or missing drop-in
|
|
5
|
+
* must never stop daemon planning.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
export const LEGACY_SYNC_DROPIN_DIR = "/etc/systemd/system/outpost-sync.service.d";
|
|
10
|
+
function assignments(text, into) {
|
|
11
|
+
let inService = false;
|
|
12
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
13
|
+
const line = rawLine.trim();
|
|
14
|
+
if (!line || line.startsWith("#"))
|
|
15
|
+
continue;
|
|
16
|
+
const section = /^\[([^\]]+)\]$/.exec(line);
|
|
17
|
+
if (section) {
|
|
18
|
+
inService = section[1] === "Service";
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (!inService)
|
|
22
|
+
continue;
|
|
23
|
+
const match = /^Environment=(?:"([^"]*)"|([A-Za-z_][A-Za-z0-9_]*=[^\s"]*))$/.exec(line);
|
|
24
|
+
const assignment = match?.[1] ?? match?.[2];
|
|
25
|
+
if (assignment === undefined)
|
|
26
|
+
continue;
|
|
27
|
+
const equals = assignment.indexOf("=");
|
|
28
|
+
if (equals <= 0)
|
|
29
|
+
continue;
|
|
30
|
+
const key = assignment.slice(0, equals);
|
|
31
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
32
|
+
continue;
|
|
33
|
+
into[key] = assignment.slice(equals + 1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Read simple KEY=VALUE and quoted "KEY=VALUE" Environment directives. */
|
|
37
|
+
export function readLegacySyncEnv(platform, directory = LEGACY_SYNC_DROPIN_DIR) {
|
|
38
|
+
if (platform !== "linux")
|
|
39
|
+
return {};
|
|
40
|
+
let files;
|
|
41
|
+
try {
|
|
42
|
+
files = fs.readdirSync(directory).filter((name) => name.endsWith(".conf")).sort();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
const env = {};
|
|
48
|
+
for (const file of files) {
|
|
49
|
+
try {
|
|
50
|
+
assignments(fs.readFileSync(path.join(directory, file), "utf8"), env);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// A single unreadable drop-in cannot make daemon planning fail.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return env;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=legacy-sync-env.js.map
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -18,6 +18,7 @@ import { botsRoot } from "../bot/paths.js";
|
|
|
18
18
|
import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits } from "./catalog.js";
|
|
19
19
|
import { readDaemonConfig } from "./config.js";
|
|
20
20
|
import { findLegacyUnits, legacyOwnerLookup } from "./legacy.js";
|
|
21
|
+
import { readLegacySyncEnv } from "./legacy-sync-env.js";
|
|
21
22
|
import { acquireDaemonLock, releaseDaemonLock } from "./lock.js";
|
|
22
23
|
import { daemonRootRefusal } from "./install.js";
|
|
23
24
|
import { detectMachineType } from "./machine-type.js";
|
|
@@ -258,6 +259,7 @@ export async function runDaemon(opts) {
|
|
|
258
259
|
syncRunnerPath: runners.syncRunner,
|
|
259
260
|
heartbeatRunnerPath: runners.heartbeat,
|
|
260
261
|
sessionHostRunnerPath: runners.sessionHost,
|
|
262
|
+
legacySyncEnv: readLegacySyncEnv(platform),
|
|
261
263
|
signedIn: signedIn(env, home, log),
|
|
262
264
|
kitConfigured: fs.existsSync(agentKitPaths(home, env).kitConfigPath),
|
|
263
265
|
desktopAppManagesSync: machineType === "desktop" &&
|
|
@@ -72,7 +72,7 @@ export declare function parseOutpostPickupPayload(raw: unknown): OutpostPickupPa
|
|
|
72
72
|
export declare function buildOutpostPickupCommand(payloadUrl: string): string;
|
|
73
73
|
/** Bounded, read-only capacity query executed by the target Outpost CLI. */
|
|
74
74
|
export declare function buildOutpostCapacityCommand(): string;
|
|
75
|
-
/** Read
|
|
75
|
+
/** Read lane records owned by an Outpost, using its configured HQ binary when set. */
|
|
76
76
|
export declare function buildOutpostLaneListCommand(input?: {
|
|
77
77
|
hqBinary?: string;
|
|
78
78
|
}): string;
|
|
@@ -65,7 +65,7 @@ export function buildOutpostPickupCommand(payloadUrl) {
|
|
|
65
65
|
export function buildOutpostCapacityCommand() {
|
|
66
66
|
return withRemoteHqDir(joinCommandParts(["hq", "lanes", "capacity", "--json"]));
|
|
67
67
|
}
|
|
68
|
-
/** Read
|
|
68
|
+
/** Read lane records owned by an Outpost, using its configured HQ binary when set. */
|
|
69
69
|
export function buildOutpostLaneListCommand(input = {}) {
|
|
70
70
|
return withRemoteHqDir(joinCommandParts([
|
|
71
71
|
input.hqBinary?.trim() || "hq",
|