@indigoai-us/hq-cli 5.342.11 → 5.342.13
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 +19 -0
- package/dist/command-catalog.generated.d.ts +1 -1
- package/dist/command-catalog.generated.js +1 -1
- package/dist/commands/agent-kit.d.ts +2 -0
- package/dist/commands/agent-kit.js +23 -17
- package/dist/commands/agent-mcp.d.ts +2 -0
- package/dist/commands/agent-mcp.js +21 -15
- package/dist/commands/lanes-keep-alive-audit.js +1 -1
- package/dist/commands/lanes-message.d.ts +20 -2
- package/dist/commands/lanes-message.js +99 -38
- package/dist/commands/lanes-shared.d.ts +1 -0
- package/dist/commands/lanes.d.ts +4 -0
- package/dist/commands/lanes.js +24 -30
- package/dist/commands/mesh.js +2 -1
- package/dist/commands/monitor.d.ts +14 -1
- package/dist/commands/monitor.js +43 -14
- package/dist/lib/doctor/checks/anywhere.js +11 -6
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/keep-alive-audit.d.ts +5 -1
- package/dist/lib/lanes/keep-alive-audit.js +132 -19
- package/dist/lib/lanes/reconcile.js +46 -3
- package/dist/lib/lanes/spawn.js +79 -0
- package/dist/lib/lanes/types.d.ts +2 -1
- package/dist/utils/package-use-lease.d.ts +15 -0
- package/dist/utils/package-use-lease.js +94 -1
- package/dist/utils/resident-package-preload.d.ts +8 -1
- package/dist/utils/resident-package-preload.js +68 -0
- package/dist/utils/self-update.d.ts +5 -0
- package/dist/utils/self-update.js +100 -12
- package/docs/anywhere-runtime.md +3 -1
- package/package.json +4 -4
- package/dist/lib/db/remote-engine.d.ts +0 -58
- package/dist/lib/db/remote-engine.js +0 -87
- package/dist/lib/db/remote-sql.d.ts +0 -22
- package/dist/lib/db/remote-sql.js +0 -36
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
@@ -6,6 +6,8 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { performance } from "node:perf_hooks";
|
|
7
7
|
const CLI_NAME = "@indigoai-us/hq-cli";
|
|
8
8
|
const UPDATE_REQUEST = "update.pending.json";
|
|
9
|
+
export const PACKAGE_USE_LEASE_HANDOFF_ENV = "HQ_CLI_PACKAGE_USE_LEASE_HANDOFF";
|
|
10
|
+
const HANDOFF_TIMEOUT_MS = 2_000;
|
|
9
11
|
export class PackageUseUpdatePendingError extends Error {
|
|
10
12
|
constructor(message) {
|
|
11
13
|
super(message);
|
|
@@ -172,6 +174,93 @@ export function acquirePackageUseLease(prefix, hqVersion, stateDirectory) {
|
|
|
172
174
|
}
|
|
173
175
|
throw new PackageUseUpdatePendingError("An HQ CLI update is waiting for the package lease; retry when it finishes");
|
|
174
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Create a bounded, per-reexec rendezvous in the current lease directory.
|
|
179
|
+
* The child confirms readiness only after publishing its own atomic lease.
|
|
180
|
+
*/
|
|
181
|
+
export function createPackageUseLeaseHandoff(lease, timeoutMs = HANDOFF_TIMEOUT_MS) {
|
|
182
|
+
const directory = path.dirname(lease.leasePath);
|
|
183
|
+
const markerPath = path.join(directory, `.reexec-${process.pid}-${randomBytes(12).toString("hex")}.handoff`);
|
|
184
|
+
let cleaned = false;
|
|
185
|
+
const cleanup = () => {
|
|
186
|
+
if (cleaned)
|
|
187
|
+
return;
|
|
188
|
+
cleaned = true;
|
|
189
|
+
process.removeListener("exit", cleanup);
|
|
190
|
+
try {
|
|
191
|
+
rmSync(markerPath, { force: true });
|
|
192
|
+
rmSync(`${markerPath}.tmp`, { force: true });
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
process.stderr.write(`hq-cli package-use lease handoff cleanup failed (${errorLabel(error)})\n`);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
process.once("exit", cleanup);
|
|
199
|
+
return {
|
|
200
|
+
envValue: markerPath,
|
|
201
|
+
async waitForChild(pid, childAlive) {
|
|
202
|
+
const deadline = Date.now() + timeoutMs;
|
|
203
|
+
while (childAlive() && Date.now() <= deadline) {
|
|
204
|
+
try {
|
|
205
|
+
const marker = JSON.parse(readFileSync(markerPath, "utf8"));
|
|
206
|
+
if (marker.pid === pid && Number.isSafeInteger(marker.start_time_ms) && typeof marker.lease_name === "string") {
|
|
207
|
+
const leasePath = path.join(directory, marker.lease_name);
|
|
208
|
+
if (path.basename(leasePath) === marker.lease_name) {
|
|
209
|
+
const record = JSON.parse(readFileSync(leasePath, "utf8"));
|
|
210
|
+
if (record.pid === pid && record.start_time_ms === marker.start_time_ms)
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (error.code !== "ENOENT" && !(error instanceof SyntaxError)) {
|
|
217
|
+
process.stderr.write(`hq-cli package-use lease handoff check failed (${errorLabel(error)})\n`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
},
|
|
224
|
+
cleanup,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
export function createCurrentPackageUseLeaseHandoff(timeoutMs = HANDOFF_TIMEOUT_MS) {
|
|
228
|
+
return currentPackageUseLease ? createPackageUseLeaseHandoff(currentPackageUseLease, timeoutMs) : null;
|
|
229
|
+
}
|
|
230
|
+
/** Publish this process's lease identity to a parent waiting in the same prefix. */
|
|
231
|
+
export function publishPackageUseLeaseHandoff(lease, env = process.env) {
|
|
232
|
+
const markerPath = env[PACKAGE_USE_LEASE_HANDOFF_ENV];
|
|
233
|
+
if (!markerPath)
|
|
234
|
+
return false;
|
|
235
|
+
const directory = path.dirname(lease.leasePath);
|
|
236
|
+
if (path.dirname(path.resolve(markerPath)) !== path.resolve(directory)) {
|
|
237
|
+
delete env[PACKAGE_USE_LEASE_HANDOFF_ENV];
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const match = /^(\d+)-(\d+)\.json$/.exec(path.basename(lease.leasePath));
|
|
241
|
+
if (!match || Number(match[1]) !== process.pid) {
|
|
242
|
+
delete env[PACKAGE_USE_LEASE_HANDOFF_ENV];
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
const record = {
|
|
246
|
+
pid: process.pid,
|
|
247
|
+
start_time_ms: Number(match[2]),
|
|
248
|
+
lease_name: path.basename(lease.leasePath),
|
|
249
|
+
};
|
|
250
|
+
const tempPath = `${markerPath}.tmp`;
|
|
251
|
+
try {
|
|
252
|
+
writeFileSync(tempPath, `${JSON.stringify(record)}\n`, { flag: "wx", mode: 0o600 });
|
|
253
|
+
renameSync(tempPath, markerPath);
|
|
254
|
+
delete env[PACKAGE_USE_LEASE_HANDOFF_ENV];
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
rmSync(tempPath, { force: true });
|
|
259
|
+
delete env[PACKAGE_USE_LEASE_HANDOFF_ENV];
|
|
260
|
+
process.stderr.write(`hq-cli package-use lease handoff publish failed (${errorLabel(error)})\n`);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
175
264
|
function exists(file) {
|
|
176
265
|
try {
|
|
177
266
|
readFileSync(file);
|
|
@@ -198,6 +287,7 @@ export function acquireCurrentPackageUseLease() {
|
|
|
198
287
|
}
|
|
199
288
|
const lease = acquirePackageUseLease(prefix, pkg.version);
|
|
200
289
|
currentPackageUseLease = lease;
|
|
290
|
+
publishPackageUseLeaseHandoff(lease);
|
|
201
291
|
return lease;
|
|
202
292
|
}
|
|
203
293
|
/** Release this process's package-use lease after a long-running command has loaded its package modules. */
|
|
@@ -260,5 +350,8 @@ export function currentPackageUseUpdateRequested() {
|
|
|
260
350
|
}
|
|
261
351
|
export const __test__ = {
|
|
262
352
|
waitForUpdateRequest,
|
|
353
|
+
setCurrentPackageUseLease(lease) {
|
|
354
|
+
currentPackageUseLease = lease;
|
|
355
|
+
},
|
|
263
356
|
};
|
|
264
357
|
//# sourceMappingURL=package-use-lease.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ResidentPackagePath = "auth-login" | "bot" | "childWrapper" | "coreJobRun" | "login" | "mcp" | "daemon";
|
|
1
|
+
export type ResidentPackagePath = "agent-kit" | "agent-mcp" | "auth-login" | "bot" | "childWrapper" | "coreJobRun" | "login" | "mcp" | "daemon" | "mesh-daemon" | "lanes-watch" | "monitor-wait" | "monitor-run";
|
|
2
2
|
/** Package imports reachable lazily from each resident command's runtime graph. */
|
|
3
3
|
export declare const RESIDENT_PACKAGE_MODULES: Record<ResidentPackagePath, readonly string[]>;
|
|
4
4
|
export declare class ResidentPackagePreloadError extends Error {
|
|
@@ -10,4 +10,11 @@ export declare class ResidentPackagePreloadError extends Error {
|
|
|
10
10
|
export declare function reportResidentPackagePreloadFailure(error: unknown): void;
|
|
11
11
|
/** Resolve every lazy package module before a resident command releases its lease. */
|
|
12
12
|
export declare function preloadResidentPackageModules(residentPath: ResidentPackagePath, load?: () => Promise<unknown>): Promise<void>;
|
|
13
|
+
export interface ResidentCommandLeaseOptions {
|
|
14
|
+
preload?: () => Promise<void>;
|
|
15
|
+
reportFailure?: (error: unknown) => void;
|
|
16
|
+
release?: () => void;
|
|
17
|
+
}
|
|
18
|
+
/** Start a resident loop only after its declared lazy package imports are loaded. */
|
|
19
|
+
export declare function runResidentCommandWithPackageLease<T>(residentPath: ResidentPackagePath, run: () => Promise<T>, options?: ResidentCommandLeaseOptions): Promise<T>;
|
|
13
20
|
//# sourceMappingURL=resident-package-preload.d.ts.map
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload, } from "./package-use-lease.js";
|
|
1
2
|
/** Package imports reachable lazily from each resident command's runtime graph. */
|
|
2
3
|
export const RESIDENT_PACKAGE_MODULES = {
|
|
4
|
+
"agent-kit": ["../sentry.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "open"],
|
|
5
|
+
"agent-mcp": ["../sentry.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "../commands/access.js", "open"],
|
|
3
6
|
login: [
|
|
4
7
|
"../lib/mesh/api.js",
|
|
5
8
|
"../lib/work-context/config.js",
|
|
@@ -40,8 +43,31 @@ export const RESIDENT_PACKAGE_MODULES = {
|
|
|
40
43
|
"../utils/staged-npm-install.js",
|
|
41
44
|
"open",
|
|
42
45
|
],
|
|
46
|
+
"mesh-daemon": ["../sentry.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "open"],
|
|
47
|
+
"lanes-watch": ["../sentry.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "open"],
|
|
48
|
+
"monitor-wait": ["../sentry.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "open"],
|
|
49
|
+
"monitor-run": ["../commands/lanes.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "../sentry.js", "open"],
|
|
43
50
|
};
|
|
44
51
|
const LOADERS = {
|
|
52
|
+
"agent-kit": [
|
|
53
|
+
async () => {
|
|
54
|
+
const { preloadSentrySdk } = await import("../sentry.js");
|
|
55
|
+
preloadSentrySdk();
|
|
56
|
+
},
|
|
57
|
+
() => import("../lib/plan-limit-nag.js"),
|
|
58
|
+
() => import("../utils/plan-gate-error.js"),
|
|
59
|
+
() => import("open"),
|
|
60
|
+
],
|
|
61
|
+
"agent-mcp": [
|
|
62
|
+
async () => {
|
|
63
|
+
const { preloadSentrySdk } = await import("../sentry.js");
|
|
64
|
+
preloadSentrySdk();
|
|
65
|
+
},
|
|
66
|
+
() => import("../lib/plan-limit-nag.js"),
|
|
67
|
+
() => import("../utils/plan-gate-error.js"),
|
|
68
|
+
() => import("../commands/access.js"),
|
|
69
|
+
() => import("open"),
|
|
70
|
+
],
|
|
45
71
|
login: [
|
|
46
72
|
() => import("../lib/mesh/api.js"),
|
|
47
73
|
() => import("../lib/work-context/config.js"),
|
|
@@ -101,6 +127,43 @@ const LOADERS = {
|
|
|
101
127
|
() => import("../utils/staged-npm-install.js"),
|
|
102
128
|
() => import("open"),
|
|
103
129
|
],
|
|
130
|
+
"mesh-daemon": [
|
|
131
|
+
async () => {
|
|
132
|
+
const { preloadSentrySdk } = await import("../sentry.js");
|
|
133
|
+
preloadSentrySdk();
|
|
134
|
+
},
|
|
135
|
+
() => import("../lib/plan-limit-nag.js"),
|
|
136
|
+
() => import("../utils/plan-gate-error.js"),
|
|
137
|
+
() => import("open"),
|
|
138
|
+
],
|
|
139
|
+
"lanes-watch": [
|
|
140
|
+
async () => {
|
|
141
|
+
const { preloadSentrySdk } = await import("../sentry.js");
|
|
142
|
+
preloadSentrySdk();
|
|
143
|
+
},
|
|
144
|
+
() => import("../lib/plan-limit-nag.js"),
|
|
145
|
+
() => import("../utils/plan-gate-error.js"),
|
|
146
|
+
() => import("open"),
|
|
147
|
+
],
|
|
148
|
+
"monitor-wait": [
|
|
149
|
+
async () => {
|
|
150
|
+
const { preloadSentrySdk } = await import("../sentry.js");
|
|
151
|
+
preloadSentrySdk();
|
|
152
|
+
},
|
|
153
|
+
() => import("../lib/plan-limit-nag.js"),
|
|
154
|
+
() => import("../utils/plan-gate-error.js"),
|
|
155
|
+
() => import("open"),
|
|
156
|
+
],
|
|
157
|
+
"monitor-run": [
|
|
158
|
+
() => import("../commands/lanes.js"),
|
|
159
|
+
() => import("../lib/plan-limit-nag.js"),
|
|
160
|
+
() => import("../utils/plan-gate-error.js"),
|
|
161
|
+
async () => {
|
|
162
|
+
const { preloadSentrySdk } = await import("../sentry.js");
|
|
163
|
+
preloadSentrySdk();
|
|
164
|
+
},
|
|
165
|
+
() => import("open"),
|
|
166
|
+
],
|
|
104
167
|
};
|
|
105
168
|
export class ResidentPackagePreloadError extends Error {
|
|
106
169
|
moduleSpecifier;
|
|
@@ -141,4 +204,9 @@ export async function preloadResidentPackageModules(residentPath, load) {
|
|
|
141
204
|
}
|
|
142
205
|
}));
|
|
143
206
|
}
|
|
207
|
+
/** Start a resident loop only after its declared lazy package imports are loaded. */
|
|
208
|
+
export async function runResidentCommandWithPackageLease(residentPath, run, options = {}) {
|
|
209
|
+
await releasePackageUseLeaseAfterResidentPreload(options.preload ?? (() => preloadResidentPackageModules(residentPath)), options.reportFailure ?? reportResidentPackagePreloadFailure, options.release ?? releaseCurrentPackageUseLease);
|
|
210
|
+
return run();
|
|
211
|
+
}
|
|
144
212
|
//# sourceMappingURL=resident-package-preload.js.map
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
* Opt-outs: `HQ_NO_UPDATE_CHECK=1` (the shared knob that also silences
|
|
57
57
|
* version-check), `hq rescue --no-self-update`, and the re-exec guard env.
|
|
58
58
|
*/
|
|
59
|
+
import { spawn } from "node:child_process";
|
|
59
60
|
import * as fs from "node:fs";
|
|
60
61
|
import { type RunningInstall, type UpdateResult } from "./version-gate.js";
|
|
61
62
|
import { type RestartDaemonResult } from "../lib/mesh/live/daemon/install.js";
|
|
@@ -145,6 +146,10 @@ export interface SelfUpdateDeps {
|
|
|
145
146
|
renamePath?: typeof fs.renameSync;
|
|
146
147
|
runner?: (cmd: string, args: string[], env?: NodeJS.ProcessEnv) => UpdateResult;
|
|
147
148
|
reexec?: (argv: string[], env: NodeJS.ProcessEnv) => number | null;
|
|
149
|
+
/** Async process-spawn seam for lease handoff tests. */
|
|
150
|
+
spawnReexec?: typeof spawn;
|
|
151
|
+
/** stdin TTY probe for re-exec signal forwarding tests. */
|
|
152
|
+
stdinIsTTY?: () => boolean;
|
|
148
153
|
acquireLock?: () => (() => void) | null;
|
|
149
154
|
/**
|
|
150
155
|
* Read-your-writes convergence check run after a "successful" install:
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
* Opt-outs: `HQ_NO_UPDATE_CHECK=1` (the shared knob that also silences
|
|
57
57
|
* version-check), `hq rescue --no-self-update`, and the re-exec guard env.
|
|
58
58
|
*/
|
|
59
|
-
import {
|
|
59
|
+
import { spawn } from "node:child_process";
|
|
60
60
|
import { randomBytes } from "node:crypto";
|
|
61
61
|
import * as fs from "node:fs";
|
|
62
62
|
import * as path from "node:path";
|
|
@@ -70,6 +70,7 @@ import { canStageInstall, stagedNpmInstall } from "./staged-install.js";
|
|
|
70
70
|
import { acquireUpdateLock as acquireSharedUpdateLock } from "./update-lock.js";
|
|
71
71
|
import { markLatestIneffective } from "./version-check.js";
|
|
72
72
|
import { restartDaemonServiceIfInstalled } from "../lib/mesh/live/daemon/install.js";
|
|
73
|
+
import { createCurrentPackageUseLeaseHandoff, PACKAGE_USE_LEASE_HANDOFF_ENV, releaseCurrentPackageUseLease, } from "./package-use-lease.js";
|
|
73
74
|
/**
|
|
74
75
|
* Set on the re-exec'd child so it can never self-update (and re-exec) again.
|
|
75
76
|
* One update + one re-exec per user invocation, ever.
|
|
@@ -596,18 +597,101 @@ export function acquireUpdateLock(now = Date.now()) {
|
|
|
596
597
|
* command. Returns the child's exit status, or null when the child could not be
|
|
597
598
|
* started at all (e.g. `hq` not on PATH in a minimal-PATH parent).
|
|
598
599
|
*/
|
|
599
|
-
function reexecHq(argv, env) {
|
|
600
|
+
async function reexecHq(argv, env, spawnProcess = spawn, stdinIsTTY = () => process.stdin.isTTY === true) {
|
|
600
601
|
const plan = buildSpawnPlan("hq", argv);
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
env
|
|
605
|
-
|
|
606
|
-
|
|
602
|
+
const handoff = createCurrentPackageUseLeaseHandoff();
|
|
603
|
+
const childEnv = handoff
|
|
604
|
+
? { ...env, [PACKAGE_USE_LEASE_HANDOFF_ENV]: handoff.envValue }
|
|
605
|
+
: env;
|
|
606
|
+
let childForSignals;
|
|
607
|
+
let childSpawned = false;
|
|
608
|
+
let childClosed = false;
|
|
609
|
+
let spawnedChild;
|
|
610
|
+
const pendingSignals = [];
|
|
611
|
+
const reexecSignals = process.platform === "win32"
|
|
612
|
+
? ["SIGINT", "SIGTERM"]
|
|
613
|
+
: ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
614
|
+
const forwardSignal = (signal) => {
|
|
615
|
+
if (signal === "SIGINT" && stdinIsTTY())
|
|
616
|
+
return;
|
|
617
|
+
if (childClosed)
|
|
618
|
+
return;
|
|
619
|
+
if (!childForSignals || !childSpawned) {
|
|
620
|
+
pendingSignals.push(signal);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (childForSignals.exitCode === null && childForSignals.signalCode === null) {
|
|
624
|
+
childForSignals.kill(signal);
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
const signalHandlers = new Map(reexecSignals.map((signal) => [signal, () => forwardSignal(signal)]));
|
|
628
|
+
for (const [signal, handler] of signalHandlers)
|
|
629
|
+
process.on(signal, handler);
|
|
630
|
+
const removeSignalHandlers = () => {
|
|
631
|
+
for (const [signal, handler] of signalHandlers)
|
|
632
|
+
process.removeListener(signal, handler);
|
|
633
|
+
};
|
|
634
|
+
// spawnSync left default parent signal handling in place, so a parent-only
|
|
635
|
+
// SIGTERM or SIGHUP could terminate it. Forward stop signals to the async
|
|
636
|
+
// child while keeping this process waiting for the child's exit status.
|
|
637
|
+
try {
|
|
638
|
+
spawnedChild = spawnProcess(plan.cmd, plan.args, {
|
|
639
|
+
stdio: "inherit",
|
|
640
|
+
shell: plan.shell,
|
|
641
|
+
env: childEnv,
|
|
642
|
+
});
|
|
643
|
+
childForSignals = spawnedChild;
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
removeSignalHandlers();
|
|
647
|
+
handoff?.cleanup();
|
|
648
|
+
process.stderr.write(`hq-cli re-exec spawn failed (${reexecErrorClass(error)})\n`);
|
|
607
649
|
return null;
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
650
|
+
}
|
|
651
|
+
return new Promise((resolve) => {
|
|
652
|
+
let alive = true;
|
|
653
|
+
let spawnFailed = false;
|
|
654
|
+
spawnedChild.once("error", (error) => {
|
|
655
|
+
spawnFailed = true;
|
|
656
|
+
process.stderr.write(`hq-cli re-exec spawn failed (${reexecErrorClass(error)})\n`);
|
|
657
|
+
});
|
|
658
|
+
spawnedChild.once("spawn", () => {
|
|
659
|
+
childSpawned = true;
|
|
660
|
+
for (const signal of pendingSignals.splice(0)) {
|
|
661
|
+
if (spawnedChild.exitCode === null && spawnedChild.signalCode === null)
|
|
662
|
+
spawnedChild.kill(signal);
|
|
663
|
+
}
|
|
664
|
+
if (!handoff || spawnedChild.pid === undefined)
|
|
665
|
+
return;
|
|
666
|
+
void handoff.waitForChild(spawnedChild.pid, () => alive).then((matched) => {
|
|
667
|
+
if (matched && alive) {
|
|
668
|
+
releaseCurrentPackageUseLease();
|
|
669
|
+
handoff.cleanup();
|
|
670
|
+
}
|
|
671
|
+
}).catch((error) => {
|
|
672
|
+
process.stderr.write(`hq-cli package-use lease handoff wait failed (${error instanceof Error ? error.name : "unknown"})\n`);
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
spawnedChild.once("close", (code, _signal) => {
|
|
676
|
+
alive = false;
|
|
677
|
+
childClosed = true;
|
|
678
|
+
pendingSignals.length = 0;
|
|
679
|
+
removeSignalHandlers();
|
|
680
|
+
handoff?.cleanup();
|
|
681
|
+
// The former spawnSync path returned null on spawn failure and 1 for a signal.
|
|
682
|
+
resolve(spawnFailed ? null : code ?? 1);
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
function reexecErrorClass(error) {
|
|
687
|
+
if (error && typeof error === "object") {
|
|
688
|
+
const code = error.code;
|
|
689
|
+
if (typeof code === "string" && /^[A-Z0-9_]{1,32}$/.test(code))
|
|
690
|
+
return code;
|
|
691
|
+
if (error instanceof Error)
|
|
692
|
+
return error.name;
|
|
693
|
+
}
|
|
694
|
+
return "unknown";
|
|
611
695
|
}
|
|
612
696
|
async function updateAndReexec(argv, flavor, known, deps) {
|
|
613
697
|
const env = deps.env ?? process.env;
|
|
@@ -829,7 +913,11 @@ async function updateAndReexec(argv, flavor, known, deps) {
|
|
|
829
913
|
return { action: "updated", latest };
|
|
830
914
|
}
|
|
831
915
|
const childEnv = { ...env, [REEXEC_GUARD_ENV]: "1" };
|
|
832
|
-
const status =
|
|
916
|
+
const status = deps.spawnReexec
|
|
917
|
+
? await reexecHq([...argv.slice(2)], childEnv, deps.spawnReexec, deps.stdinIsTTY)
|
|
918
|
+
: deps.reexec
|
|
919
|
+
? deps.reexec([...argv.slice(2)], childEnv)
|
|
920
|
+
: await reexecHq([...argv.slice(2)], childEnv, spawn, deps.stdinIsTTY);
|
|
833
921
|
if (status === null) {
|
|
834
922
|
console.error(chalk.yellow(`⚠ Updated hq-cli to ${latest}, but couldn't re-launch \`hq\` from PATH; ` +
|
|
835
923
|
`continuing the ${flavor.noun} on ${current}.`));
|
package/docs/anywhere-runtime.md
CHANGED
|
@@ -73,7 +73,9 @@ the named-pipe transport without Unix mode or owner fields. Use `hq doctor
|
|
|
73
73
|
--json` for the same fields as structured details. The hqd health operation is
|
|
74
74
|
read-only and available while the runtime flag is off; it reports only the
|
|
75
75
|
current directory's binding and never returns authentication tokens. A Unix
|
|
76
|
-
socket that cannot be verified as user-only produces a warning.
|
|
76
|
+
socket that cannot be verified as user-only produces a warning. When a Unix
|
|
77
|
+
socket is unreachable, doctor recommends installing the daemon service if it is
|
|
78
|
+
absent or restarting it if it is installed. If an older
|
|
77
79
|
resident daemon does not support the health operation, doctor falls back to
|
|
78
80
|
`ping` for reachability, version and uptime, then warns that a restart is needed
|
|
79
81
|
for full diagnostics.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.342.
|
|
3
|
+
"version": "5.342.13",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"js-yaml": "^4.1.0",
|
|
60
60
|
"mqtt": "5.15.2",
|
|
61
61
|
"open": "^10.1.0",
|
|
62
|
-
"semver": "^7.
|
|
62
|
+
"semver": "^7.8.5",
|
|
63
63
|
"simple-git": "^3.27.0",
|
|
64
64
|
"smol-toml": "1.6.1",
|
|
65
65
|
"varlock": "1.21.1"
|
|
@@ -69,11 +69,11 @@
|
|
|
69
69
|
"@types/better-sqlite3": "^7.6.13",
|
|
70
70
|
"@types/js-yaml": "^4.0.9",
|
|
71
71
|
"@types/node": "^22.0.0",
|
|
72
|
-
"@types/semver": "^7.
|
|
72
|
+
"@types/semver": "^7.8.0",
|
|
73
73
|
"@vitest/coverage-v8": "4.1.6",
|
|
74
74
|
"aedes": "1.1.2",
|
|
75
75
|
"aws-sdk-client-mock": "^4.1.0",
|
|
76
|
-
"esbuild": "0.
|
|
76
|
+
"esbuild": "0.28.2",
|
|
77
77
|
"eslint": "^10.5.0",
|
|
78
78
|
"husky": "^9.1.7",
|
|
79
79
|
"lint-staged": "^15.5.2",
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pluggable remote vault DB engine interface (vault-databases US-007).
|
|
3
|
-
*
|
|
4
|
-
* Default shipped adapter id: aurora-dsql.
|
|
5
|
-
* This module is interface + registry only — no production AWS provision here.
|
|
6
|
-
*/
|
|
7
|
-
/** Connection material written only to secrets stores — never CLI stdout. */
|
|
8
|
-
export interface RemoteConnectionSecretPayload {
|
|
9
|
-
/** Opaque secret body (e.g. JSON with host/user/password or IAM token recipe). */
|
|
10
|
-
payload: Record<string, unknown>;
|
|
11
|
-
/** Suggested vault/Secrets Manager key suffix (namespaced by company elsewhere). */
|
|
12
|
-
secretNameHint: string;
|
|
13
|
-
}
|
|
14
|
-
export interface RemoteProvisionRequest {
|
|
15
|
-
companyUid: string;
|
|
16
|
-
companySlug: string;
|
|
17
|
-
region?: string;
|
|
18
|
-
/** Engine-specific options (cluster params, etc.). */
|
|
19
|
-
options?: Record<string, unknown>;
|
|
20
|
-
}
|
|
21
|
-
export interface RemoteProvisionResult {
|
|
22
|
-
engineId: string;
|
|
23
|
-
resourceArn: string;
|
|
24
|
-
region: string;
|
|
25
|
-
status: "ready" | "provisioning" | "failed" | "existing";
|
|
26
|
-
/** Secret payload to store — callers must not print. */
|
|
27
|
-
connectionSecret: RemoteConnectionSecretPayload;
|
|
28
|
-
}
|
|
29
|
-
export interface RemoteHealth {
|
|
30
|
-
engineId: string;
|
|
31
|
-
resourceArn: string;
|
|
32
|
-
healthy: boolean;
|
|
33
|
-
status: string;
|
|
34
|
-
checkedAt: string;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Remote Postgres-class engine adapter.
|
|
38
|
-
* Implementations live in control plane (hq-pro) for real AWS; CLI may mock.
|
|
39
|
-
*/
|
|
40
|
-
export interface RemoteDbEngine {
|
|
41
|
-
readonly id: string;
|
|
42
|
-
provision(req: RemoteProvisionRequest): Promise<RemoteProvisionResult>;
|
|
43
|
-
deprovision(companyUid: string, resourceArn: string): Promise<void>;
|
|
44
|
-
connectionSecretPayload(companyUid: string, resourceArn: string): Promise<RemoteConnectionSecretPayload>;
|
|
45
|
-
healthCheck(resourceArn: string): Promise<RemoteHealth>;
|
|
46
|
-
}
|
|
47
|
-
/** Default engine selection (PRD decision). */
|
|
48
|
-
export declare const DEFAULT_REMOTE_ENGINE_ID = "aurora-dsql";
|
|
49
|
-
export declare function registerRemoteEngine(engine: RemoteDbEngine): void;
|
|
50
|
-
export declare function getRemoteEngine(id?: string): RemoteDbEngine;
|
|
51
|
-
export declare function listRemoteEngineIds(): string[];
|
|
52
|
-
/** Clear registry (tests only). */
|
|
53
|
-
export declare function _resetRemoteEngineRegistryForTests(): void;
|
|
54
|
-
/**
|
|
55
|
-
* In-memory mock engine for unit tests — no AWS.
|
|
56
|
-
*/
|
|
57
|
-
export declare function createMockRemoteEngine(id?: string): RemoteDbEngine;
|
|
58
|
-
//# sourceMappingURL=remote-engine.d.ts.map
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pluggable remote vault DB engine interface (vault-databases US-007).
|
|
3
|
-
*
|
|
4
|
-
* Default shipped adapter id: aurora-dsql.
|
|
5
|
-
* This module is interface + registry only — no production AWS provision here.
|
|
6
|
-
*/
|
|
7
|
-
/** Default engine selection (PRD decision). */
|
|
8
|
-
export const DEFAULT_REMOTE_ENGINE_ID = "aurora-dsql";
|
|
9
|
-
const registry = new Map();
|
|
10
|
-
export function registerRemoteEngine(engine) {
|
|
11
|
-
registry.set(engine.id, engine);
|
|
12
|
-
}
|
|
13
|
-
export function getRemoteEngine(id = DEFAULT_REMOTE_ENGINE_ID) {
|
|
14
|
-
const engine = registry.get(id);
|
|
15
|
-
if (!engine) {
|
|
16
|
-
throw new Error(`remote DB engine not registered: ${id} (available: ${[...registry.keys()].join(", ") || "none"})`);
|
|
17
|
-
}
|
|
18
|
-
return engine;
|
|
19
|
-
}
|
|
20
|
-
export function listRemoteEngineIds() {
|
|
21
|
-
return [...registry.keys()];
|
|
22
|
-
}
|
|
23
|
-
/** Clear registry (tests only). */
|
|
24
|
-
export function _resetRemoteEngineRegistryForTests() {
|
|
25
|
-
registry.clear();
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* In-memory mock engine for unit tests — no AWS.
|
|
29
|
-
*/
|
|
30
|
-
export function createMockRemoteEngine(id = "mock") {
|
|
31
|
-
const resources = new Map();
|
|
32
|
-
return {
|
|
33
|
-
id,
|
|
34
|
-
async provision(req) {
|
|
35
|
-
const existing = [...resources.entries()].find(([, v]) => v.companyUid === req.companyUid);
|
|
36
|
-
if (existing) {
|
|
37
|
-
const [arn, meta] = existing;
|
|
38
|
-
return {
|
|
39
|
-
engineId: id,
|
|
40
|
-
resourceArn: arn,
|
|
41
|
-
region: meta.region,
|
|
42
|
-
status: "existing",
|
|
43
|
-
connectionSecret: {
|
|
44
|
-
payload: { mock: true, companyUid: req.companyUid },
|
|
45
|
-
secretNameHint: `db/remote/${req.companySlug}`,
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
const arn = `arn:mock:db:${req.companyUid}`;
|
|
50
|
-
const region = req.region ?? "us-east-1";
|
|
51
|
-
resources.set(arn, { companyUid: req.companyUid, region });
|
|
52
|
-
return {
|
|
53
|
-
engineId: id,
|
|
54
|
-
resourceArn: arn,
|
|
55
|
-
region,
|
|
56
|
-
status: "ready",
|
|
57
|
-
connectionSecret: {
|
|
58
|
-
payload: { mock: true, companyUid: req.companyUid },
|
|
59
|
-
secretNameHint: `db/remote/${req.companySlug}`,
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
},
|
|
63
|
-
async deprovision(companyUid, resourceArn) {
|
|
64
|
-
const meta = resources.get(resourceArn);
|
|
65
|
-
if (meta && meta.companyUid === companyUid) {
|
|
66
|
-
resources.delete(resourceArn);
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
async connectionSecretPayload(companyUid, resourceArn) {
|
|
70
|
-
return {
|
|
71
|
-
payload: { mock: true, companyUid, resourceArn },
|
|
72
|
-
secretNameHint: `db/remote/${companyUid}`,
|
|
73
|
-
};
|
|
74
|
-
},
|
|
75
|
-
async healthCheck(resourceArn) {
|
|
76
|
-
const ok = resources.has(resourceArn);
|
|
77
|
-
return {
|
|
78
|
-
engineId: id,
|
|
79
|
-
resourceArn,
|
|
80
|
-
healthy: ok,
|
|
81
|
-
status: ok ? "ready" : "missing",
|
|
82
|
-
checkedAt: new Date().toISOString(),
|
|
83
|
-
};
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
//# sourceMappingURL=remote-engine.js.map
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remote SQL helper with injected credentials and executor (US-010).
|
|
3
|
-
*
|
|
4
|
-
* Connection string never appears in argv, logs, or stdout.
|
|
5
|
-
* The CLI command does not currently wire this helper to a remote executor.
|
|
6
|
-
*/
|
|
7
|
-
export interface RemoteSqlOptions {
|
|
8
|
-
company: string;
|
|
9
|
-
sql: string;
|
|
10
|
-
/** Injected secret material — never log. */
|
|
11
|
-
getConnectionConfig: () => Promise<Record<string, unknown>>;
|
|
12
|
-
/** Execute against remote; tests inject mock. */
|
|
13
|
-
execute?: (config: Record<string, unknown>, sql: string) => Promise<Record<string, unknown>[]>;
|
|
14
|
-
}
|
|
15
|
-
export interface RemoteSqlResult {
|
|
16
|
-
rows: Record<string, unknown>[];
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Run SQL on remote tier using vault-bound credentials.
|
|
20
|
-
*/
|
|
21
|
-
export declare function runRemoteSql(opts: RemoteSqlOptions): Promise<RemoteSqlResult>;
|
|
22
|
-
//# sourceMappingURL=remote-sql.d.ts.map
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remote SQL helper with injected credentials and executor (US-010).
|
|
3
|
-
*
|
|
4
|
-
* Connection string never appears in argv, logs, or stdout.
|
|
5
|
-
* The CLI command does not currently wire this helper to a remote executor.
|
|
6
|
-
*/
|
|
7
|
-
function assertNoLeak(label, value) {
|
|
8
|
-
const s = typeof value === "string" ? value : JSON.stringify(value);
|
|
9
|
-
if (/postgres:\/\//i.test(s) || /postgresql:\/\//i.test(s)) {
|
|
10
|
-
throw new Error(`${label}: connection string leak blocked`);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Run SQL on remote tier using vault-bound credentials.
|
|
15
|
-
*/
|
|
16
|
-
export async function runRemoteSql(opts) {
|
|
17
|
-
if (!opts.sql?.trim()) {
|
|
18
|
-
throw new Error("SQL statement is required");
|
|
19
|
-
}
|
|
20
|
-
let config;
|
|
21
|
-
try {
|
|
22
|
-
config = await opts.getConnectionConfig();
|
|
23
|
-
}
|
|
24
|
-
catch (e) {
|
|
25
|
-
const msg = e instanceof Error ? e.message : "no remote binding";
|
|
26
|
-
throw new Error(`remote SQL unavailable: ${msg}`);
|
|
27
|
-
}
|
|
28
|
-
assertNoLeak("connection config", config);
|
|
29
|
-
if (!opts.execute) {
|
|
30
|
-
throw new Error("remote SQL executor not configured in this build (adapter pending live DSQL wire-up)");
|
|
31
|
-
}
|
|
32
|
-
const rows = await opts.execute(config, opts.sql);
|
|
33
|
-
assertNoLeak("remote sql rows", rows);
|
|
34
|
-
return { rows };
|
|
35
|
-
}
|
|
36
|
-
//# sourceMappingURL=remote-sql.js.map
|