@indigoai-us/hq-cli 5.291.0 → 5.293.0
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 +53 -8
- package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
- package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
- package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
- package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
- package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
- package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
- package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
- package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
- package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
- package/dist/command-catalog.generated.d.ts +109 -1
- package/dist/command-catalog.generated.js +136 -1
- package/dist/commands/core-delegate-w1b.d.ts +8 -0
- package/dist/commands/core-delegate-w1b.js +1049 -0
- package/dist/commands/core-delegate.d.ts +28 -0
- package/dist/commands/core-delegate.js +15 -13
- package/dist/commands/core-handoff.js +152 -23
- package/dist/commands/core-jobs-native.d.ts +15 -0
- package/dist/commands/core-jobs-native.js +709 -2
- package/dist/commands/core-ontology.d.ts +22 -4
- package/dist/commands/core-ontology.js +90 -8
- package/dist/commands/core.js +10 -0
- package/dist/commands/lanes-move.js +54 -1
- package/dist/commands/lanes-questions.js +4 -0
- package/dist/commands/lanes.d.ts +2 -0
- package/dist/commands/lanes.js +119 -5
- package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
- package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
- package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
- package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
- package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
- package/dist/lib/core-utils/sentry-report.js +2 -2
- package/dist/lib/daemon/flag.d.ts +2 -0
- package/dist/lib/daemon/flag.js +2 -0
- package/dist/lib/daemon/run.d.ts +11 -0
- package/dist/lib/daemon/run.js +17 -1
- package/dist/lib/lanes/autopilot.d.ts +13 -1
- package/dist/lib/lanes/autopilot.js +271 -28
- package/dist/lib/lanes/grants.d.ts +1 -1
- package/dist/lib/lanes/grants.js +7 -5
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +5 -0
- package/dist/lib/lanes/keep-alive.js +9 -4
- package/dist/lib/lanes/questions.d.ts +2 -1
- package/dist/lib/lanes/questions.js +2 -0
- package/dist/lib/lanes/reconcile.js +186 -9
- package/dist/lib/lanes/spawn.js +4 -0
- package/dist/lib/lanes/store.js +21 -1
- package/dist/lib/lanes/types.d.ts +13 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +3 -1
- package/dist/utils/scaffold-sibling-references.js +4 -4
- package/dist/utils/self-update.d.ts +13 -0
- package/dist/utils/self-update.js +33 -7
- package/dist/utils/staged-install.d.ts +34 -0
- package/dist/utils/staged-install.js +296 -0
- package/dist/utils/version-gate.d.ts +15 -0
- package/dist/utils/version-gate.js +46 -2
- package/package.json +1 -1
- package/scripts/mesh-daemon-if-stale.mjs +4 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staged npm self-update: install the new hq-cli beside the running package,
|
|
3
|
+
* check that it starts, then swap it in with two renames.
|
|
4
|
+
*
|
|
5
|
+
* The in-place update moves the live package aside for the whole
|
|
6
|
+
* `npm install -g`, so anything that starts `hq` during the install finds no
|
|
7
|
+
* CLI on disk ("hq: command not found", MODULE_NOT_FOUND; outposts primary and
|
|
8
|
+
* 2, 5.290.0 -> 5.291.0). Here the live package stays in place until the new
|
|
9
|
+
* one is complete and has answered `--version`.
|
|
10
|
+
*
|
|
11
|
+
* Only a plain npm global layout is staged: `<prefix>/lib/node_modules/<pkg>`
|
|
12
|
+
* as a real directory, with bin links in `<prefix>/bin`. pnpm, Bun, Windows
|
|
13
|
+
* and linked checkouts keep the in-place update.
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { randomBytes } from "node:crypto";
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { CLI_NAME } from "../cli-version.js";
|
|
20
|
+
import { runUpdateCommand } from "./version-gate.js";
|
|
21
|
+
/** Set for the staging install; hq-cli's own postinstall skips itself on it. */
|
|
22
|
+
export const STAGED_INSTALL_ENV = "HQ_STAGED_INSTALL";
|
|
23
|
+
const PROBE_TIMEOUT_MS = 60_000;
|
|
24
|
+
const POSTINSTALL_TIMEOUT_MS = 120_000;
|
|
25
|
+
export function canStageInstall(install, platform = process.platform) {
|
|
26
|
+
if (platform === "win32" || install.manager !== "npm")
|
|
27
|
+
return false;
|
|
28
|
+
if (!install.prefix || !install.packageRoot)
|
|
29
|
+
return false;
|
|
30
|
+
if (path.resolve(install.packageRoot) !== path.resolve(install.prefix, "lib", "node_modules", CLI_NAME)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const entry = fs.lstatSync(install.packageRoot);
|
|
35
|
+
return entry.isDirectory() && !entry.isSymbolicLink();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function stagedNpmInstall(install, version, deps = {}) {
|
|
42
|
+
if (!canStageInstall(install, deps.platform)) {
|
|
43
|
+
return { ok: false, detail: "this hq-cli install is not a plain npm global install" };
|
|
44
|
+
}
|
|
45
|
+
const prefix = install.prefix;
|
|
46
|
+
const live = install.packageRoot;
|
|
47
|
+
const env = deps.env ?? process.env;
|
|
48
|
+
const run = deps.run ?? runUpdateCommand;
|
|
49
|
+
const rename = deps.rename ?? fs.renameSync;
|
|
50
|
+
const remove = deps.remove ?? ((target) => fs.rmSync(target, { recursive: true, force: true }));
|
|
51
|
+
const probe = deps.probeVersion ?? ((root) => probeHqVersion(root, env));
|
|
52
|
+
const postinstall = deps.runPostinstall ?? ((root) => runPackagePostinstall(root, env));
|
|
53
|
+
// Beside the live package, so both renames stay on one filesystem.
|
|
54
|
+
const tag = `${process.pid}-${randomBytes(8).toString("hex")}`;
|
|
55
|
+
const staging = path.join(path.dirname(live), `.hq-cli-staging-${tag}`);
|
|
56
|
+
const staged = path.join(staging, "lib", "node_modules", CLI_NAME);
|
|
57
|
+
const backup = path.join(path.dirname(live), `.hq-cli-backup-${tag}`);
|
|
58
|
+
let keepBackup = false;
|
|
59
|
+
try {
|
|
60
|
+
const result = run("npm", ["install", "-g", "--prefix", staging, `${CLI_NAME}@${version}`], { ...env, [STAGED_INSTALL_ENV]: "1" });
|
|
61
|
+
if (!result.ok)
|
|
62
|
+
return result;
|
|
63
|
+
const reported = probe(staged);
|
|
64
|
+
if (reported !== version) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
detail: `the staged hq-cli reported ${reported ?? "no version"} instead of ${version}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
rename(live, backup);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return { ok: false, detail: `could not move the running hq-cli aside: ${message(error)}` };
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
rename(staged, live);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
try {
|
|
81
|
+
rename(backup, live);
|
|
82
|
+
}
|
|
83
|
+
catch (restoreError) {
|
|
84
|
+
keepBackup = true;
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
detail: `could not move hq-cli ${version} into place (${message(error)}) or restore the previous install from ${backup} (${message(restoreError)})`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return { ok: false, detail: `could not move hq-cli ${version} into place: ${message(error)}` };
|
|
91
|
+
}
|
|
92
|
+
if (!linkBins(prefix, live, readBins(backup), tag)) {
|
|
93
|
+
// `hq` does not reach the new package, so put the old one back.
|
|
94
|
+
try {
|
|
95
|
+
rename(live, staged);
|
|
96
|
+
rename(backup, live);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
keepBackup = true;
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
detail: `could not link hq to hq-cli ${version} or restore the previous install from ${backup} (${message(error)})`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { ok: false, detail: `could not point ${path.join(prefix, "bin", "hq")} at hq-cli ${version}` };
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
postinstall(live);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// npm runs this script as `... || true`; it never fails an install.
|
|
112
|
+
}
|
|
113
|
+
return { ok: true };
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
// Leftovers are hidden siblings npm ignores; they never change the result.
|
|
117
|
+
try {
|
|
118
|
+
remove(staging);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// best effort
|
|
122
|
+
}
|
|
123
|
+
if (!keepBackup) {
|
|
124
|
+
try {
|
|
125
|
+
remove(backup);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// best effort
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Points `<prefix>/bin/<name>` at the live package for each bin it declares
|
|
135
|
+
* and removes hq-cli's links for bins the new version dropped. Existing npm
|
|
136
|
+
* links already point at the live path. A link into another package, or a
|
|
137
|
+
* regular file (someone's wrapper), is left alone.
|
|
138
|
+
*
|
|
139
|
+
* `hq` is settled first. If it does not resolve to a file, its link is put
|
|
140
|
+
* back as it was, no other bin is touched, and this returns false so the
|
|
141
|
+
* caller can restore the old package.
|
|
142
|
+
*/
|
|
143
|
+
function linkBins(prefix, live, previousBins, tag) {
|
|
144
|
+
const binDir = path.join(prefix, "bin");
|
|
145
|
+
const ownedByHqCli = (link) => {
|
|
146
|
+
try {
|
|
147
|
+
const target = path.resolve(binDir, fs.readlinkSync(link));
|
|
148
|
+
return target.startsWith(`${live}${path.sep}`);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const pointAt = (link, want) => {
|
|
155
|
+
const temporary = `${link}.hq-cli-${tag}`;
|
|
156
|
+
try {
|
|
157
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
158
|
+
fs.symlinkSync(want, temporary);
|
|
159
|
+
fs.renameSync(temporary, link);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
try {
|
|
163
|
+
fs.rmSync(temporary, { force: true });
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// A stray temporary link is harmless.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const linkBin = (name, target) => {
|
|
171
|
+
const link = path.join(binDir, name);
|
|
172
|
+
const want = path.relative(binDir, path.join(live, target));
|
|
173
|
+
try {
|
|
174
|
+
const existing = fs.lstatSync(link, { throwIfNoEntry: false });
|
|
175
|
+
if (existing && !existing.isSymbolicLink())
|
|
176
|
+
return;
|
|
177
|
+
if (existing && fs.readlinkSync(link) === want)
|
|
178
|
+
return;
|
|
179
|
+
if (existing && !ownedByHqCli(link))
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
pointAt(link, want);
|
|
186
|
+
};
|
|
187
|
+
const bins = readBins(live);
|
|
188
|
+
const hqLink = path.join(binDir, "hq");
|
|
189
|
+
const hqBefore = symlinkTarget(hqLink);
|
|
190
|
+
if (bins.hq)
|
|
191
|
+
linkBin("hq", bins.hq);
|
|
192
|
+
if (!resolvesToFile(hqLink)) {
|
|
193
|
+
const hqAfter = symlinkTarget(hqLink);
|
|
194
|
+
if (hqAfter !== hqBefore) {
|
|
195
|
+
if (hqBefore === null) {
|
|
196
|
+
try {
|
|
197
|
+
fs.unlinkSync(hqLink);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Nothing to undo.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
pointAt(hqLink, hqBefore);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
for (const [name, target] of Object.entries(bins)) {
|
|
210
|
+
if (name !== "hq")
|
|
211
|
+
linkBin(name, target);
|
|
212
|
+
}
|
|
213
|
+
for (const name of Object.keys(previousBins)) {
|
|
214
|
+
if (name in bins)
|
|
215
|
+
continue;
|
|
216
|
+
const link = path.join(binDir, name);
|
|
217
|
+
try {
|
|
218
|
+
if (fs.lstatSync(link).isSymbolicLink() && ownedByHqCli(link))
|
|
219
|
+
fs.unlinkSync(link);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Already gone, or not ours to remove.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
function symlinkTarget(link) {
|
|
228
|
+
try {
|
|
229
|
+
return fs.lstatSync(link).isSymbolicLink() ? fs.readlinkSync(link) : null;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function resolvesToFile(link) {
|
|
236
|
+
try {
|
|
237
|
+
return fs.statSync(link).isFile();
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function readBins(packageRoot) {
|
|
244
|
+
try {
|
|
245
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
246
|
+
if (typeof pkg.bin === "string")
|
|
247
|
+
return { hq: pkg.bin };
|
|
248
|
+
if (pkg.bin && typeof pkg.bin === "object") {
|
|
249
|
+
return Object.fromEntries(Object.entries(pkg.bin).filter(([, target]) => typeof target === "string"));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// An unreadable manifest declares no bins.
|
|
254
|
+
}
|
|
255
|
+
return {};
|
|
256
|
+
}
|
|
257
|
+
function probeHqVersion(packageRoot, env) {
|
|
258
|
+
const bin = readBins(packageRoot).hq ?? "dist/index.js";
|
|
259
|
+
const result = spawnSync(process.execPath, [path.join(packageRoot, bin), "--version"], {
|
|
260
|
+
encoding: "utf8",
|
|
261
|
+
env: { ...env, HQ_NO_UPDATE_CHECK: "1" },
|
|
262
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
263
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
264
|
+
windowsHide: true,
|
|
265
|
+
});
|
|
266
|
+
if (result.status !== 0)
|
|
267
|
+
return null;
|
|
268
|
+
const lines = (result.stdout ?? "").trim().split(/\r?\n/);
|
|
269
|
+
return lines[lines.length - 1]?.trim() || null;
|
|
270
|
+
}
|
|
271
|
+
function runPackagePostinstall(packageRoot, env) {
|
|
272
|
+
let script;
|
|
273
|
+
try {
|
|
274
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
275
|
+
script = pkg.scripts?.postinstall;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (typeof script !== "string" || script.trim() === "")
|
|
281
|
+
return;
|
|
282
|
+
const childEnv = { ...env };
|
|
283
|
+
delete childEnv[STAGED_INSTALL_ENV];
|
|
284
|
+
spawnSync(script, {
|
|
285
|
+
cwd: packageRoot,
|
|
286
|
+
env: childEnv,
|
|
287
|
+
shell: true,
|
|
288
|
+
stdio: "ignore",
|
|
289
|
+
timeout: POSTINSTALL_TIMEOUT_MS,
|
|
290
|
+
windowsHide: true,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function message(error) {
|
|
294
|
+
return error instanceof Error ? error.message : String(error);
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=staged-install.js.map
|
|
@@ -284,6 +284,21 @@ export { buildSpawnPlan, quoteForWindowsShell };
|
|
|
284
284
|
*/
|
|
285
285
|
export declare function inOwnProcessGroup<T extends object>(options: T): T;
|
|
286
286
|
declare function updateTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
287
|
+
/** The supervisor script the next install runs under. */
|
|
288
|
+
export declare function updateSupervisorPath(): string;
|
|
289
|
+
export interface DetachedUpdateSupervisor {
|
|
290
|
+
path: string;
|
|
291
|
+
dispose(): void;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Copies the supervisor to a temp dir and runs installs through the copy until
|
|
295
|
+
* disposed. The in-place self-update moves the whole hq-cli package aside
|
|
296
|
+
* before it starts the installer, and the supervisor ships inside that package
|
|
297
|
+
* (5.290.0-5.292.0 failed every npm global self-update with MODULE_NOT_FOUND).
|
|
298
|
+
* The script needs only Node builtins, so the copy runs on its own. Returns
|
|
299
|
+
* null when the copy cannot be made; the caller then keeps the bundled path.
|
|
300
|
+
*/
|
|
301
|
+
export declare function detachUpdateSupervisor(): DetachedUpdateSupervisor | null;
|
|
287
302
|
/**
|
|
288
303
|
* Where an install's output goes — the other half of surviving a killed parent.
|
|
289
304
|
*
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
import { spawnSync } from "node:child_process";
|
|
31
31
|
import { installPathCompatibilityEnabled } from "../lib/install-path-compatibility-flag.js";
|
|
32
32
|
import { buildSpawnPlan, quoteForWindowsShell } from "./windows-spawn.js";
|
|
33
|
-
import { accessSync, closeSync, constants as fsConstants, existsSync, mkdtempSync, openSync, readdirSync, realpathSync, readFileSync, rmSync, } from "node:fs";
|
|
33
|
+
import { accessSync, closeSync, constants as fsConstants, copyFileSync, existsSync, mkdtempSync, openSync, readdirSync, realpathSync, readFileSync, rmSync, } from "node:fs";
|
|
34
34
|
import os from "node:os";
|
|
35
35
|
import path from "node:path";
|
|
36
36
|
import { fileURLToPath } from "node:url";
|
|
@@ -674,6 +674,50 @@ function updateTimeoutMs(env = process.env) {
|
|
|
674
674
|
* compiled module for production and is directly runnable from source tests.
|
|
675
675
|
*/
|
|
676
676
|
const UPDATE_COMMAND_SUPERVISOR = fileURLToPath(new URL("./update-command-supervisor.cjs", import.meta.url));
|
|
677
|
+
/** Set while a self-update runs the installer through a copy of the supervisor. */
|
|
678
|
+
let detachedSupervisor = null;
|
|
679
|
+
/** The supervisor script the next install runs under. */
|
|
680
|
+
export function updateSupervisorPath() {
|
|
681
|
+
return detachedSupervisor ?? UPDATE_COMMAND_SUPERVISOR;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Copies the supervisor to a temp dir and runs installs through the copy until
|
|
685
|
+
* disposed. The in-place self-update moves the whole hq-cli package aside
|
|
686
|
+
* before it starts the installer, and the supervisor ships inside that package
|
|
687
|
+
* (5.290.0-5.292.0 failed every npm global self-update with MODULE_NOT_FOUND).
|
|
688
|
+
* The script needs only Node builtins, so the copy runs on its own. Returns
|
|
689
|
+
* null when the copy cannot be made; the caller then keeps the bundled path.
|
|
690
|
+
*/
|
|
691
|
+
export function detachUpdateSupervisor() {
|
|
692
|
+
if (detachedSupervisor)
|
|
693
|
+
return null;
|
|
694
|
+
let dir;
|
|
695
|
+
try {
|
|
696
|
+
dir = mkdtempSync(path.join(os.tmpdir(), "hq-cli-update-supervisor-"));
|
|
697
|
+
const copy = path.join(dir, "update-command-supervisor.cjs");
|
|
698
|
+
copyFileSync(UPDATE_COMMAND_SUPERVISOR, copy);
|
|
699
|
+
detachedSupervisor = copy;
|
|
700
|
+
const tempDir = dir;
|
|
701
|
+
return {
|
|
702
|
+
path: copy,
|
|
703
|
+
dispose: () => {
|
|
704
|
+
if (detachedSupervisor === copy)
|
|
705
|
+
detachedSupervisor = null;
|
|
706
|
+
try {
|
|
707
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
// Best-effort cleanup of a temp dir.
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
if (dir)
|
|
717
|
+
rmSync(dir, { recursive: true, force: true });
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
677
721
|
function readSupervisorSpawnError(errorMarker) {
|
|
678
722
|
try {
|
|
679
723
|
const value = JSON.parse(readFileSync(errorMarker, "utf-8"));
|
|
@@ -754,7 +798,7 @@ export function runSupervisedUpdateCommand(cmd, args, output, env) {
|
|
|
754
798
|
// waits briefly, sends KILL to survivors, and waits for its direct child so
|
|
755
799
|
// the completed result leaves no zombie behind.
|
|
756
800
|
const result = spawnSync(process.execPath, [
|
|
757
|
-
|
|
801
|
+
updateSupervisorPath(),
|
|
758
802
|
JSON.stringify({
|
|
759
803
|
cmd: plan.cmd,
|
|
760
804
|
args: plan.args,
|
package/package.json
CHANGED
|
@@ -7,6 +7,10 @@ import fs from "node:fs";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
|
+
// A staged self-update installs into a directory it is about to rename; it
|
|
11
|
+
// runs this script again from the live path after the swap.
|
|
12
|
+
if (process.env.HQ_STAGED_INSTALL === "1") process.exit(0);
|
|
13
|
+
|
|
10
14
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
15
|
const cli = path.join(root, "dist", "index.js");
|
|
12
16
|
if (!fs.existsSync(cli)) process.exit(0);
|