@indigoai-us/hq-cli 5.345.1 → 5.345.2
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 +5 -0
- package/dist/commands/daemon.js +6 -2
- package/dist/commands/mcp-serve.js +15 -5
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +135416 -0
- package/dist/hq-cloud-sync-runner/package.json +1 -0
- package/dist/index.js +2 -1
- package/dist/lib/daemon/hq-child-bootstrap-stage.d.ts +22 -0
- package/dist/lib/daemon/hq-child-bootstrap-stage.js +127 -0
- package/dist/lib/daemon/hq-child-bootstrap.d.mts +3 -0
- package/dist/lib/daemon/hq-child-bootstrap.mjs +208 -0
- package/dist/lib/daemon/hq-cloud.d.ts +5 -0
- package/dist/lib/daemon/hq-cloud.js +14 -0
- package/dist/lib/daemon/run.d.ts +28 -2
- package/dist/lib/daemon/run.js +139 -10
- package/dist/lib/daemon/sync-runner-bundle.d.ts +40 -0
- package/dist/lib/daemon/sync-runner-bundle.js +199 -0
- package/dist/utils/package-use-lease.d.ts +6 -0
- package/dist/utils/package-use-lease.js +25 -2
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.33","bundleSha256":"ac95a726e29b0c0db65da6753d3c4e249f3f8a0cf7d56fe7579ac1804a14f34f","formatVersion":1}
|
package/dist/index.js
CHANGED
|
@@ -138,8 +138,9 @@ function packageUseErrorClass(error) {
|
|
|
138
138
|
return typeof value.name === "string" ? value.name : "unknown";
|
|
139
139
|
}
|
|
140
140
|
function dispatchWithPackageUseLease(entryUrl, dispatch, loadLease = async () => {
|
|
141
|
-
const { acquireCurrentPackageUseLease } = await import("./utils/package-use-lease.js");
|
|
141
|
+
const { acquireCurrentPackageUseLease, signalPackageUseLeaseBootstrapHandoff } = await import("./utils/package-use-lease.js");
|
|
142
142
|
acquireCurrentPackageUseLease();
|
|
143
|
+
signalPackageUseLeaseBootstrapHandoff();
|
|
143
144
|
}, updateRequestPending = async () => {
|
|
144
145
|
const { currentPackageUseUpdatePending } = await import("./utils/package-use-lease.js");
|
|
145
146
|
return currentPackageUseUpdatePending();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Atomic, content-addressed staging for the daemon's replace-independent hq launcher. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
export declare const HQ_CHILD_BOOTSTRAP_FILE = "hq-child-bootstrap.mjs";
|
|
4
|
+
export type HqChildBootstrapStageErrorCode = "SOURCE_MISSING" | "STAGE_INVALID" | "COPY_FAILED" | "HASH_MISMATCH" | "FSYNC_FAILED" | "RENAME_FAILED";
|
|
5
|
+
export declare class HqChildBootstrapStageError extends Error {
|
|
6
|
+
readonly code: HqChildBootstrapStageErrorCode;
|
|
7
|
+
constructor(code: HqChildBootstrapStageErrorCode, message: string, options?: ErrorOptions);
|
|
8
|
+
}
|
|
9
|
+
export interface StagedHqChildBootstrap {
|
|
10
|
+
directory: string;
|
|
11
|
+
bootstrapPath: string;
|
|
12
|
+
sha256: string;
|
|
13
|
+
}
|
|
14
|
+
export interface StageHqChildBootstrapOptions {
|
|
15
|
+
sourcePath?: string;
|
|
16
|
+
daemonRoot: string;
|
|
17
|
+
copyFile?: typeof fs.copyFileSync;
|
|
18
|
+
renameDirectory?: (from: string, to: string) => void;
|
|
19
|
+
}
|
|
20
|
+
/** Stage only a complete, content-verified bootstrap; temporary siblings are never selected. */
|
|
21
|
+
export declare function stageHqChildBootstrap(options: StageHqChildBootstrapOptions): StagedHqChildBootstrap;
|
|
22
|
+
//# sourceMappingURL=hq-child-bootstrap-stage.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** Atomic, content-addressed staging for the daemon's replace-independent hq launcher. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { ensurePrivateDir } from "./paths.js";
|
|
7
|
+
export const HQ_CHILD_BOOTSTRAP_FILE = "hq-child-bootstrap.mjs";
|
|
8
|
+
export class HqChildBootstrapStageError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code, message, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.name = "HqChildBootstrapStageError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function fail(code, message, cause) {
|
|
17
|
+
if (cause instanceof HqChildBootstrapStageError)
|
|
18
|
+
throw cause;
|
|
19
|
+
throw new HqChildBootstrapStageError(code, message, cause === undefined ? undefined : { cause });
|
|
20
|
+
}
|
|
21
|
+
function hashFile(file) {
|
|
22
|
+
return createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
23
|
+
}
|
|
24
|
+
function syncFile(file) {
|
|
25
|
+
const fd = fs.openSync(file, "r");
|
|
26
|
+
try {
|
|
27
|
+
fs.fsyncSync(fd);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
fs.closeSync(fd);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function syncDirectory(directory) {
|
|
34
|
+
const fd = fs.openSync(directory, fs.constants.O_RDONLY);
|
|
35
|
+
try {
|
|
36
|
+
fs.fsyncSync(fd);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
fs.closeSync(fd);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function readStage(directory, expectedHash) {
|
|
43
|
+
if (!fs.existsSync(directory))
|
|
44
|
+
return null;
|
|
45
|
+
const bootstrapPath = path.join(directory, HQ_CHILD_BOOTSTRAP_FILE);
|
|
46
|
+
try {
|
|
47
|
+
if (!fs.statSync(bootstrapPath).isFile() || hashFile(bootstrapPath) !== expectedHash) {
|
|
48
|
+
fail("STAGE_INVALID", "An existing daemon hq bootstrap stage is invalid; preserving it");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
fail("STAGE_INVALID", "Unable to validate the existing daemon hq bootstrap stage", error);
|
|
53
|
+
}
|
|
54
|
+
return { directory, bootstrapPath, sha256: expectedHash };
|
|
55
|
+
}
|
|
56
|
+
/** Stage only a complete, content-verified bootstrap; temporary siblings are never selected. */
|
|
57
|
+
export function stageHqChildBootstrap(options) {
|
|
58
|
+
const sourcePath = options.sourcePath ?? path.join(path.dirname(fileURLToPath(import.meta.url)), HQ_CHILD_BOOTSTRAP_FILE);
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.statSync(sourcePath).isFile())
|
|
61
|
+
fail("SOURCE_MISSING", "The daemon hq bootstrap source is missing");
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
fail("SOURCE_MISSING", "Unable to access the daemon hq bootstrap source", error);
|
|
65
|
+
}
|
|
66
|
+
let expectedHash;
|
|
67
|
+
try {
|
|
68
|
+
expectedHash = hashFile(sourcePath);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
fail("SOURCE_MISSING", "Unable to read the daemon hq bootstrap source", error);
|
|
72
|
+
}
|
|
73
|
+
const stageRoot = path.join(options.daemonRoot, "bundles", "hq-child-bootstrap");
|
|
74
|
+
const selectedDirectory = path.join(stageRoot, expectedHash);
|
|
75
|
+
const existing = readStage(selectedDirectory, expectedHash);
|
|
76
|
+
if (existing)
|
|
77
|
+
return existing;
|
|
78
|
+
let tempDirectory;
|
|
79
|
+
let failureCode = "COPY_FAILED";
|
|
80
|
+
try {
|
|
81
|
+
ensurePrivateDir(stageRoot);
|
|
82
|
+
tempDirectory = path.join(stageRoot, `.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`);
|
|
83
|
+
fs.mkdirSync(tempDirectory, { mode: 0o700 });
|
|
84
|
+
(options.copyFile ?? fs.copyFileSync)(sourcePath, path.join(tempDirectory, HQ_CHILD_BOOTSTRAP_FILE));
|
|
85
|
+
const stagedPath = path.join(tempDirectory, HQ_CHILD_BOOTSTRAP_FILE);
|
|
86
|
+
if (hashFile(stagedPath) !== expectedHash)
|
|
87
|
+
fail("HASH_MISMATCH", "The staged daemon hq bootstrap hash does not match");
|
|
88
|
+
failureCode = "FSYNC_FAILED";
|
|
89
|
+
syncFile(stagedPath);
|
|
90
|
+
syncDirectory(tempDirectory);
|
|
91
|
+
failureCode = "RENAME_FAILED";
|
|
92
|
+
try {
|
|
93
|
+
(options.renameDirectory ?? fs.renameSync)(tempDirectory, selectedDirectory);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error.code === "EEXIST" || error.code === "ENOTEMPTY") {
|
|
97
|
+
const raced = readStage(selectedDirectory, expectedHash);
|
|
98
|
+
if (raced) {
|
|
99
|
+
fs.rmSync(tempDirectory, { recursive: true, force: true });
|
|
100
|
+
return raced;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
fail("RENAME_FAILED", "Unable to atomically select the daemon hq bootstrap", error);
|
|
104
|
+
}
|
|
105
|
+
tempDirectory = undefined;
|
|
106
|
+
failureCode = "FSYNC_FAILED";
|
|
107
|
+
syncDirectory(stageRoot);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (tempDirectory) {
|
|
111
|
+
try {
|
|
112
|
+
fs.rmSync(tempDirectory, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
catch (cleanupError) {
|
|
115
|
+
fail("COPY_FAILED", "Unable to remove the temporary daemon hq bootstrap stage", new AggregateError([error, cleanupError]));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (error instanceof HqChildBootstrapStageError)
|
|
119
|
+
throw error;
|
|
120
|
+
fail(failureCode, "Unable to stage the daemon hq bootstrap", error);
|
|
121
|
+
}
|
|
122
|
+
const staged = readStage(selectedDirectory, expectedHash);
|
|
123
|
+
if (!staged)
|
|
124
|
+
fail("FSYNC_FAILED", "The completed daemon hq bootstrap failed validation");
|
|
125
|
+
return staged;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=hq-child-bootstrap-stage.js.map
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// This staged launcher intentionally imports Node built-ins only. It waits for
|
|
3
|
+
// the desktop update protocol and publishes its own lease before reading the
|
|
4
|
+
// replaceable CLI entry, then hands that lease off after the child publishes
|
|
5
|
+
// its normal package-use lease.
|
|
6
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { performance } from "node:perf_hooks";
|
|
12
|
+
const UPDATE_REQUEST = "update.pending.json";
|
|
13
|
+
const HANDOFF_ENV = "HQ_CLI_PACKAGE_USE_HANDOFF_TOKEN";
|
|
14
|
+
const LEASE_WAIT_MS = 30_000;
|
|
15
|
+
function packageUseStateDirectory() {
|
|
16
|
+
if (process.platform === "win32") {
|
|
17
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
18
|
+
if (!localAppData)
|
|
19
|
+
throw new Error("LOCALAPPDATA is unavailable for the HQ CLI package-use lease");
|
|
20
|
+
return path.join(localAppData, "hq-cli", "state", "package-use");
|
|
21
|
+
}
|
|
22
|
+
return path.join(process.env.XDG_STATE_HOME?.trim() || path.join(os.homedir(), ".local", "state"), "hq-cli", "package-use");
|
|
23
|
+
}
|
|
24
|
+
function normalizedPrefix(prefix) {
|
|
25
|
+
let normalized = fs.realpathSync.native(prefix).replace(/\\/g, "/");
|
|
26
|
+
normalized = normalized.replace(/^\\\\\?\\UNC\\/i, "//").replace(/^\\\\\?\\/, "");
|
|
27
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
28
|
+
}
|
|
29
|
+
function getLeaseDirectory(prefix) {
|
|
30
|
+
const digest = createHash("sha256").update(normalizedPrefix(prefix), "utf8").digest("hex");
|
|
31
|
+
return path.join(packageUseStateDirectory(), digest);
|
|
32
|
+
}
|
|
33
|
+
function updateRequestIsActive(requestPath) {
|
|
34
|
+
let request;
|
|
35
|
+
try {
|
|
36
|
+
request = JSON.parse(fs.readFileSync(requestPath, "utf8"));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code === "ENOENT")
|
|
40
|
+
return false;
|
|
41
|
+
throw new Error("Unable to read the HQ CLI update request");
|
|
42
|
+
}
|
|
43
|
+
const pid = request.pid;
|
|
44
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
45
|
+
throw new Error("The HQ CLI update request is invalid");
|
|
46
|
+
try {
|
|
47
|
+
process.kill(pid, 0);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code === "ESRCH") {
|
|
51
|
+
fs.rmSync(requestPath, { force: true });
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
let requestedSignal = null;
|
|
58
|
+
let child = null;
|
|
59
|
+
const forwardedSignals = process.platform === "win32"
|
|
60
|
+
? ["SIGINT", "SIGTERM"]
|
|
61
|
+
: ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
62
|
+
for (const signal of forwardedSignals) {
|
|
63
|
+
process.on(signal, () => {
|
|
64
|
+
requestedSignal = signal;
|
|
65
|
+
if (child?.pid && child.exitCode === null && child.signalCode === null)
|
|
66
|
+
child.kill(signal);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async function waitForRequestToClear(requestPath, started) {
|
|
70
|
+
while (updateRequestIsActive(requestPath)) {
|
|
71
|
+
if (requestedSignal)
|
|
72
|
+
throw new Error(`Interrupted by ${requestedSignal}`);
|
|
73
|
+
if (Date.now() - started >= LEASE_WAIT_MS)
|
|
74
|
+
throw new Error("The HQ CLI package is being updated; retry after it finishes");
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function acquireBootstrapLease(prefix, version) {
|
|
79
|
+
const directory = getLeaseDirectory(prefix);
|
|
80
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
81
|
+
const requestPath = path.join(directory, UPDATE_REQUEST);
|
|
82
|
+
const record = {
|
|
83
|
+
pid: process.pid,
|
|
84
|
+
start_time_ms: Math.floor(performance.timeOrigin),
|
|
85
|
+
hq_version: version,
|
|
86
|
+
};
|
|
87
|
+
const leasePath = path.join(directory, `${record.pid}-${record.start_time_ms}.json`);
|
|
88
|
+
const tempPath = `${leasePath}.${randomBytes(8).toString("hex")}.tmp`;
|
|
89
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
90
|
+
await waitForRequestToClear(requestPath, Date.now());
|
|
91
|
+
try {
|
|
92
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(record)}\n`, { flag: "wx", mode: 0o600 });
|
|
93
|
+
fs.renameSync(tempPath, leasePath);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
fs.rmSync(tempPath, { force: true });
|
|
97
|
+
throw new Error("Unable to publish the daemon hq bootstrap lease", { cause: error });
|
|
98
|
+
}
|
|
99
|
+
if (!updateRequestIsActive(requestPath))
|
|
100
|
+
return { directory, leasePath };
|
|
101
|
+
fs.rmSync(leasePath, { force: true });
|
|
102
|
+
}
|
|
103
|
+
throw new Error("An HQ CLI update is waiting for the package lease; retry when it finishes");
|
|
104
|
+
}
|
|
105
|
+
function resolveInstalledEntry(prefix) {
|
|
106
|
+
const packageName = path.join("@indigoai-us", "hq-cli");
|
|
107
|
+
const packageRoots = [
|
|
108
|
+
path.join(prefix, "lib", "node_modules", packageName),
|
|
109
|
+
path.join(prefix, "node_modules", packageName),
|
|
110
|
+
];
|
|
111
|
+
for (const packageRoot of packageRoots) {
|
|
112
|
+
let metadata;
|
|
113
|
+
try {
|
|
114
|
+
metadata = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
118
|
+
continue;
|
|
119
|
+
throw new Error("Unable to read the installed HQ CLI package metadata", { cause: error });
|
|
120
|
+
}
|
|
121
|
+
if (metadata.name !== "@indigoai-us/hq-cli" || typeof metadata.bin?.hq !== "string")
|
|
122
|
+
continue;
|
|
123
|
+
const entry = path.resolve(packageRoot, metadata.bin.hq);
|
|
124
|
+
const relative = path.relative(packageRoot, entry);
|
|
125
|
+
if (relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative)) {
|
|
126
|
+
throw new Error("The installed HQ CLI entry is outside its package directory");
|
|
127
|
+
}
|
|
128
|
+
if (fs.statSync(entry).isFile())
|
|
129
|
+
return entry;
|
|
130
|
+
}
|
|
131
|
+
throw new Error("The installed HQ CLI entry could not be resolved after the update request cleared");
|
|
132
|
+
}
|
|
133
|
+
function relaySignal(signal) {
|
|
134
|
+
for (const forwarded of forwardedSignals)
|
|
135
|
+
process.removeAllListeners(forwarded);
|
|
136
|
+
process.kill(process.pid, signal);
|
|
137
|
+
throw new Error(`Unable to relay ${signal}`);
|
|
138
|
+
}
|
|
139
|
+
async function run() {
|
|
140
|
+
const [prefix, version, ...args] = process.argv.slice(2);
|
|
141
|
+
if (!prefix || !version)
|
|
142
|
+
throw new Error("The daemon hq bootstrap arguments are invalid");
|
|
143
|
+
const lease = await acquireBootstrapLease(prefix, version);
|
|
144
|
+
const token = randomBytes(16).toString("hex");
|
|
145
|
+
const markerPathFor = (pid) => path.join(lease.directory, `handoff-${pid}-${token}.handoff`);
|
|
146
|
+
let handoffMarker = null;
|
|
147
|
+
let leaseReleased = false;
|
|
148
|
+
const releaseLease = () => {
|
|
149
|
+
if (leaseReleased)
|
|
150
|
+
return;
|
|
151
|
+
leaseReleased = true;
|
|
152
|
+
fs.rmSync(lease.leasePath, { force: true });
|
|
153
|
+
};
|
|
154
|
+
try {
|
|
155
|
+
if (requestedSignal)
|
|
156
|
+
return relaySignal(requestedSignal);
|
|
157
|
+
// This resolution is deliberately after the update request is clear and
|
|
158
|
+
// after the bootstrap lease record protects the package from replacement.
|
|
159
|
+
const entry = resolveInstalledEntry(prefix);
|
|
160
|
+
child = spawn(process.execPath, [entry, ...args], {
|
|
161
|
+
env: { ...process.env, [HANDOFF_ENV]: token },
|
|
162
|
+
stdio: "inherit",
|
|
163
|
+
windowsHide: true,
|
|
164
|
+
});
|
|
165
|
+
child.once("error", (error) => {
|
|
166
|
+
process.stderr.write(`hq daemon child bootstrap could not start the CLI (${error.code ?? error.name})\n`);
|
|
167
|
+
});
|
|
168
|
+
if (child.pid !== undefined)
|
|
169
|
+
handoffMarker = markerPathFor(child.pid);
|
|
170
|
+
const exit = await new Promise((resolve, reject) => {
|
|
171
|
+
if (!child)
|
|
172
|
+
return reject(new Error("The HQ CLI child did not start"));
|
|
173
|
+
child.once("error", reject);
|
|
174
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
175
|
+
const poll = setInterval(() => {
|
|
176
|
+
if (!handoffMarker || leaseReleased || !fs.existsSync(handoffMarker))
|
|
177
|
+
return;
|
|
178
|
+
fs.rmSync(handoffMarker, { force: true });
|
|
179
|
+
releaseLease();
|
|
180
|
+
}, 20);
|
|
181
|
+
child.once("exit", () => clearInterval(poll));
|
|
182
|
+
child.once("error", () => clearInterval(poll));
|
|
183
|
+
});
|
|
184
|
+
if (!leaseReleased)
|
|
185
|
+
releaseLease();
|
|
186
|
+
if (handoffMarker)
|
|
187
|
+
fs.rmSync(handoffMarker, { force: true });
|
|
188
|
+
if (requestedSignal)
|
|
189
|
+
return relaySignal(requestedSignal);
|
|
190
|
+
if (exit.signal)
|
|
191
|
+
return relaySignal(exit.signal);
|
|
192
|
+
return exit.code ?? 1;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
if (!leaseReleased)
|
|
196
|
+
releaseLease();
|
|
197
|
+
if (handoffMarker)
|
|
198
|
+
fs.rmSync(handoffMarker, { force: true });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
void run().then((code) => { process.exitCode = code; }, (error) => {
|
|
202
|
+
const signal = requestedSignal;
|
|
203
|
+
process.stderr.write(`hq daemon child bootstrap failed (${error instanceof Error ? error.name : "unknown"})\n`);
|
|
204
|
+
if (signal)
|
|
205
|
+
relaySignal(signal);
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
});
|
|
208
|
+
//# sourceMappingURL=hq-child-bootstrap.mjs.map
|
|
@@ -17,6 +17,9 @@ export interface PickedHqCloud extends HqCloudCopy {
|
|
|
17
17
|
/** Set when a version is pinned but that version is not the installed managed copy. */
|
|
18
18
|
pinnedVersionMissing?: string;
|
|
19
19
|
}
|
|
20
|
+
export type ManagedHqCloud = PickedHqCloud & {
|
|
21
|
+
source: "managed";
|
|
22
|
+
};
|
|
20
23
|
export declare function managedHqCloudDir(daemonRoot: string): string;
|
|
21
24
|
/** The package folder inside the updater's install prefix. */
|
|
22
25
|
export declare function managedHqCloudRoot(daemonRoot: string): string;
|
|
@@ -56,6 +59,8 @@ export declare function hqCloudInstallCommand(prefix: string, version: string, p
|
|
|
56
59
|
export declare function bundledHqCloud(): HqCloudCopy | null;
|
|
57
60
|
/** The hq-cloud the daemon should run right now. */
|
|
58
61
|
export declare function currentHqCloud(daemonRoot: string): PickedHqCloud | null;
|
|
62
|
+
/** After the CLI package lease is released, only daemon-owned package files are eligible. */
|
|
63
|
+
export declare function currentHqCloudAfterLeaseRelease(daemonRoot: string, expectedVersion?: string | null): ManagedHqCloud | null;
|
|
59
64
|
export interface HqCloudUpdateOutcome {
|
|
60
65
|
ok: boolean;
|
|
61
66
|
from: string | null;
|
|
@@ -132,6 +132,20 @@ export function currentHqCloud(daemonRoot) {
|
|
|
132
132
|
const pinnedVersion = readDaemonConfig(path.join(daemonRoot, "config.json")).hqCloudVersion;
|
|
133
133
|
return pickHqCloud(bundledHqCloud(), readHqCloudCopy(managedHqCloudRoot(daemonRoot)), (copy) => hqCloudRunners(copy.root).syncRunner !== null, pinnedVersion);
|
|
134
134
|
}
|
|
135
|
+
/** After the CLI package lease is released, only daemon-owned package files are eligible. */
|
|
136
|
+
export function currentHqCloudAfterLeaseRelease(daemonRoot, expectedVersion) {
|
|
137
|
+
const config = readDaemonConfig(path.join(daemonRoot, "config.json"));
|
|
138
|
+
const managed = readHqCloudCopy(managedHqCloudRoot(daemonRoot));
|
|
139
|
+
if (!managed || !hqCloudRunners(managed.root).syncRunner)
|
|
140
|
+
return null;
|
|
141
|
+
if (config.hqCloudVersion && config.hqCloudVersion !== managed.version)
|
|
142
|
+
return null;
|
|
143
|
+
const expected = parseVersion(config.hqCloudVersion ?? expectedVersion ?? "");
|
|
144
|
+
const actual = parseVersion(managed.version);
|
|
145
|
+
if (!expected || !actual || expected[0] !== actual[0])
|
|
146
|
+
return null;
|
|
147
|
+
return { ...managed, source: "managed" };
|
|
148
|
+
}
|
|
135
149
|
function runNpm(file, args, shell, env) {
|
|
136
150
|
return spawnSync(file, args, {
|
|
137
151
|
encoding: "utf8",
|
package/dist/lib/daemon/run.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { type UnitPlan } from "./catalog.js";
|
|
|
9
9
|
import { type LegacyUnit } from "./legacy.js";
|
|
10
10
|
import { type DaemonLockResult } from "./lock.js";
|
|
11
11
|
import { type MachineType } from "./machine-type.js";
|
|
12
|
+
import { type PickedHqCloud } from "./hq-cloud.js";
|
|
12
13
|
import { type HostApp } from "./supervisor.js";
|
|
13
14
|
export declare const LOG_ROTATE_BYTES: number;
|
|
14
15
|
/** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
|
|
@@ -34,9 +35,34 @@ export interface RunDaemonOptions {
|
|
|
34
35
|
env?: NodeJS.ProcessEnv;
|
|
35
36
|
nodeBinary: string;
|
|
36
37
|
hqBinary: string;
|
|
37
|
-
/** Called after
|
|
38
|
-
onStartupReadsComplete?: () => void;
|
|
38
|
+
/** Called after initial reads; returns true only when the lease was actually released. */
|
|
39
|
+
onStartupReadsComplete?: () => boolean | void;
|
|
40
|
+
/** Test seam; defaults to the payload emitted by the hq-cli package build. */
|
|
41
|
+
syncRunnerBundleDirectory?: string;
|
|
42
|
+
/** Test seam; the replaceable hq-cli package root. */
|
|
43
|
+
cliPackageRoot?: string | null;
|
|
44
|
+
/** Test seam; the npm prefix whose installed hq entry is protected by package-use leases. */
|
|
45
|
+
cliPackagePrefix?: string | null;
|
|
46
|
+
/** Test seam for atomic staging failure coverage. */
|
|
47
|
+
hqChildBootstrapSourcePath?: string;
|
|
39
48
|
}
|
|
49
|
+
export type SyncRunnerSource = {
|
|
50
|
+
kind: "staged-sync";
|
|
51
|
+
runnerPath: string;
|
|
52
|
+
} | {
|
|
53
|
+
kind: "managed-full-package";
|
|
54
|
+
runnerPath: string | null;
|
|
55
|
+
} | {
|
|
56
|
+
kind: "bundled-full-package";
|
|
57
|
+
runnerPath: string | null;
|
|
58
|
+
} | {
|
|
59
|
+
kind: "unavailable";
|
|
60
|
+
runnerPath: null;
|
|
61
|
+
};
|
|
62
|
+
export declare function syncRunnerSourceForPlan(stagedRunnerPath: string | null, hqCloud: PickedHqCloud | null, legacyBundledRunnerPath?: string | null): SyncRunnerSource;
|
|
63
|
+
/** Whether every executable in the first service plan remains usable after the CLI package is replaced. */
|
|
64
|
+
export declare function daemonPlanCanReleasePackageUseLease(plans: readonly UnitPlan[], replaceablePackageRoots: readonly string[], stagingFailed: boolean, hqBootstrapReady?: boolean): boolean;
|
|
65
|
+
export declare function daemonHqChildCommand(nodeBinary: string, hqBinary: string, args: readonly string[], bootstrapPath: string | null, packagePrefix: string | null): [string, string[]];
|
|
40
66
|
export declare function resolveSyncRunnerPath(): string | null;
|
|
41
67
|
/** The hosted desktop app uses this env value when planning daemon services. */
|
|
42
68
|
export declare function daemonInstantSyncEnabled(env: NodeJS.ProcessEnv, configured?: boolean): boolean;
|