@indigoai-us/hq-cli 5.342.2 → 5.342.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/commands/bot.js +3 -0
- package/dist/commands/daemon.d.ts +4 -0
- package/dist/commands/daemon.js +6 -1
- package/dist/commands/mcp-serve.d.ts +12 -0
- package/dist/commands/mcp-serve.js +33 -10
- package/dist/commands/secrets-env-exit.d.ts +6 -0
- package/dist/commands/secrets-env-exit.js +28 -0
- package/dist/lib/flag-registry-refresh-wait.d.ts +7 -0
- package/dist/lib/flag-registry-refresh-wait.js +10 -0
- 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/flag-registry-worker.js +6 -1
- package/dist/lib/flag-registry.d.ts +12 -1
- package/dist/lib/flag-registry.js +125 -5
- package/dist/main.js +10 -2
- package/dist/secrets-env-action.js +4 -4
- package/dist/utils/package-use-lease.d.ts +6 -0
- package/dist/utils/package-use-lease.js +24 -1
- package/dist/utils/resident-package-preload.d.ts +13 -0
- package/dist/utils/resident-package-preload.js +78 -0
- package/package.json +1 -1
|
@@ -20,7 +20,9 @@ async function refresh() {
|
|
|
20
20
|
endpoint,
|
|
21
21
|
...(companyUid ? { companyUid } : {}),
|
|
22
22
|
...(companyIdentifiers.length ? { companyIdentifiers } : {}),
|
|
23
|
-
getToken: () =>
|
|
23
|
+
getToken: () => typeof data.idToken === "string" && data.idToken.length > 0
|
|
24
|
+
? data.idToken
|
|
25
|
+
: loadCachedTokens()?.idToken ?? "",
|
|
24
26
|
refreshIntervalMs: 0,
|
|
25
27
|
onError: () => { },
|
|
26
28
|
});
|
|
@@ -28,9 +30,12 @@ async function refresh() {
|
|
|
28
30
|
const snapshot = client.snapshot();
|
|
29
31
|
if (snapshot)
|
|
30
32
|
parentPort.postMessage(snapshot);
|
|
33
|
+
else
|
|
34
|
+
parentPort.postMessage({ status: "snapshot_unavailable" });
|
|
31
35
|
}
|
|
32
36
|
catch {
|
|
33
37
|
// The parent is deliberately fail-open and has the legacy fallback.
|
|
38
|
+
parentPort?.postMessage({ status: "snapshot_unavailable" });
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
void refresh();
|
|
@@ -15,11 +15,18 @@ export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
|
|
|
15
15
|
export type FlagReader = Pick<FlagClient, "isEnabled">;
|
|
16
16
|
interface WorkerLike {
|
|
17
17
|
on(event: "message", listener: (value: unknown) => void): unknown;
|
|
18
|
-
on(event: "error", listener: () => void): unknown;
|
|
18
|
+
on(event: "error", listener: (error: Error) => void): unknown;
|
|
19
|
+
ref?(): unknown;
|
|
19
20
|
unref(): void;
|
|
20
21
|
}
|
|
21
22
|
export interface FlagRegistryDependencies {
|
|
22
23
|
endpoint?: string;
|
|
24
|
+
/** An explicit `--company` scope, which takes precedence over ambient binding. */
|
|
25
|
+
companyScope?: string;
|
|
26
|
+
/** CLI argv used to resolve explicit company scope before the command parser runs. */
|
|
27
|
+
argv?: readonly string[];
|
|
28
|
+
/** Optional machine ID token already cached by the CLI, never logged or persisted. */
|
|
29
|
+
idToken?: string;
|
|
23
30
|
createWorker?: (filename: string | URL, options: WorkerOptions) => WorkerLike;
|
|
24
31
|
sentry?: FlagRegistrySentry;
|
|
25
32
|
/** Test seam for exercising the no-source fail-open path. */
|
|
@@ -31,6 +38,10 @@ export interface CompanyFlagGateDependencies {
|
|
|
31
38
|
requestTimeoutMs?: number;
|
|
32
39
|
createClient?: typeof createFlagClient;
|
|
33
40
|
}
|
|
41
|
+
/** Extract only a CLI-level --company option; values after `--` belong to child commands. */
|
|
42
|
+
export declare function explicitCompanyScopeFromArgv(argv: readonly string[]): string | undefined;
|
|
43
|
+
/** Wait briefly for the current company refresh to persist or report completion. */
|
|
44
|
+
export declare function waitForFlagRegistryRefresh(timeoutMs?: number, companyUid?: string): Promise<void>;
|
|
34
45
|
/** Create the same snapshot-backed reader used by process gates. */
|
|
35
46
|
export declare function createProcessSnapshotReader(companyIdentifiers: string[], getSnapshot?: () => FlagSnapshot | null, env?: NodeJS.ProcessEnv): FlagReader;
|
|
36
47
|
/**
|
|
@@ -10,6 +10,7 @@ import { createFlagClient, isFlagSnapshot, readLocalOverride, resolveFlag, } fro
|
|
|
10
10
|
import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
|
|
11
11
|
import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
|
|
12
12
|
import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
|
|
13
|
+
import { registerFlagRegistryRefreshWaiter } from "./flag-registry-refresh-wait.js";
|
|
13
14
|
import { writeFlagSnapshotFile } from "./flag-snapshot-file.js";
|
|
14
15
|
function captureFlagRegistryException(error, tags, sentry) {
|
|
15
16
|
if (!sentry)
|
|
@@ -19,12 +20,74 @@ function captureFlagRegistryException(error, tags, sentry) {
|
|
|
19
20
|
export { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
|
|
20
21
|
export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
|
|
21
22
|
const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
|
|
23
|
+
/** Extract only a CLI-level --company option; values after `--` belong to child commands. */
|
|
24
|
+
export function explicitCompanyScopeFromArgv(argv) {
|
|
25
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
26
|
+
const argument = argv[index];
|
|
27
|
+
if (argument === "--")
|
|
28
|
+
return undefined;
|
|
29
|
+
if (argument === "--company") {
|
|
30
|
+
const value = argv[index + 1]?.trim();
|
|
31
|
+
return value && !value.startsWith("-") ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
if (argument.startsWith("--company=")) {
|
|
34
|
+
const value = argument.slice("--company=".length).trim();
|
|
35
|
+
return value || undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
22
40
|
function normalizedCompanyUid(value) {
|
|
23
41
|
const candidate = value?.trim() ?? "";
|
|
24
42
|
return COMPANY_UID_RE.test(candidate) ? candidate : undefined;
|
|
25
43
|
}
|
|
26
44
|
let heldSnapshot = null;
|
|
27
45
|
let processReader = null;
|
|
46
|
+
let refreshCompletion = Promise.resolve();
|
|
47
|
+
let completeRefresh = null;
|
|
48
|
+
let refreshWorker = null;
|
|
49
|
+
let refreshCompanyUid;
|
|
50
|
+
let refreshPendingSnapshot = null;
|
|
51
|
+
let refreshSentry;
|
|
52
|
+
/** Wait briefly for the current company refresh to persist or report completion. */
|
|
53
|
+
export async function waitForFlagRegistryRefresh(timeoutMs = 500, companyUid) {
|
|
54
|
+
const resolvedCompanyUid = normalizedCompanyUid(companyUid);
|
|
55
|
+
if (resolvedCompanyUid)
|
|
56
|
+
refreshCompanyUid = resolvedCompanyUid;
|
|
57
|
+
if (refreshPendingSnapshot && refreshCompanyUid) {
|
|
58
|
+
try {
|
|
59
|
+
writeFlagSnapshotFile(refreshCompanyUid, refreshPendingSnapshot.flags);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
captureFlagRegistryException(error instanceof Error ? error : new Error(String(error)), {
|
|
63
|
+
component: "flag-snapshot-cache-write",
|
|
64
|
+
company_uid_present: "true",
|
|
65
|
+
}, refreshSentry);
|
|
66
|
+
}
|
|
67
|
+
refreshPendingSnapshot = null;
|
|
68
|
+
completeRefresh?.();
|
|
69
|
+
completeRefresh = null;
|
|
70
|
+
}
|
|
71
|
+
if (!completeRefresh)
|
|
72
|
+
return;
|
|
73
|
+
const worker = refreshWorker;
|
|
74
|
+
worker?.ref?.();
|
|
75
|
+
let timer;
|
|
76
|
+
try {
|
|
77
|
+
await Promise.race([
|
|
78
|
+
refreshCompletion,
|
|
79
|
+
new Promise((resolve) => {
|
|
80
|
+
timer = setTimeout(resolve, Math.max(0, timeoutMs));
|
|
81
|
+
}),
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
if (timer)
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
worker?.unref();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
registerFlagRegistryRefreshWaiter(waitForFlagRegistryRefresh);
|
|
28
91
|
function processLocalOverride(flagKey, env = process.env) {
|
|
29
92
|
return readLocalOverride(flagKey, env);
|
|
30
93
|
}
|
|
@@ -61,10 +124,37 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
61
124
|
if (processReader)
|
|
62
125
|
return processReader;
|
|
63
126
|
const endpoint = resolveFlagRegistryEndpoint(dependencies.endpoint, process.env);
|
|
64
|
-
const
|
|
127
|
+
const companyScope = dependencies.companyScope ?? explicitCompanyScopeFromArgv(dependencies.argv ?? []);
|
|
128
|
+
const companyEnv = companyScope
|
|
129
|
+
? {
|
|
130
|
+
...process.env,
|
|
131
|
+
HQ_COMPANY_UID: COMPANY_UID_RE.test(companyScope)
|
|
132
|
+
? companyScope
|
|
133
|
+
: "",
|
|
134
|
+
HQ_COMPANY_SLUG: COMPANY_UID_RE.test(companyScope)
|
|
135
|
+
? ""
|
|
136
|
+
: companyScope,
|
|
137
|
+
}
|
|
138
|
+
: process.env;
|
|
139
|
+
const company = resolveProcessFlagCompanyContext(companyEnv);
|
|
65
140
|
const sentry = dependencies.sentry;
|
|
66
141
|
const companyUid = company.companyUid;
|
|
67
142
|
const companyIdentifiers = [companyUid, company.companySlug].filter((value) => Boolean(value));
|
|
143
|
+
refreshCompletion = Promise.resolve();
|
|
144
|
+
completeRefresh = null;
|
|
145
|
+
refreshWorker = null;
|
|
146
|
+
refreshCompanyUid = companyUid;
|
|
147
|
+
refreshPendingSnapshot = null;
|
|
148
|
+
refreshSentry = sentry;
|
|
149
|
+
if (companyUid || company.companySlug) {
|
|
150
|
+
refreshCompletion = new Promise((resolve) => {
|
|
151
|
+
completeRefresh = resolve;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const finishRefresh = () => {
|
|
155
|
+
completeRefresh?.();
|
|
156
|
+
completeRefresh = null;
|
|
157
|
+
};
|
|
68
158
|
processReader = processSnapshotReader(companyIdentifiers);
|
|
69
159
|
try {
|
|
70
160
|
const embeddedSource = dependencies.workerSource ?? FLAG_REGISTRY_WORKER_SOURCE;
|
|
@@ -76,6 +166,7 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
76
166
|
worker: "flag-registry",
|
|
77
167
|
flag_worker_failure: "embedded_source_unavailable",
|
|
78
168
|
});
|
|
169
|
+
finishRefresh();
|
|
79
170
|
return processReader;
|
|
80
171
|
}
|
|
81
172
|
// This source is embedded at build time, so it remains available while an
|
|
@@ -85,6 +176,9 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
85
176
|
endpoint,
|
|
86
177
|
...(companyUid ? { companyUid } : {}),
|
|
87
178
|
...(company.companySlug ? { companyIdentifiers: [company.companySlug] } : {}),
|
|
179
|
+
...((companyUid || company.companySlug) && dependencies.idToken
|
|
180
|
+
? { idToken: dependencies.idToken }
|
|
181
|
+
: {}),
|
|
88
182
|
},
|
|
89
183
|
eval: true,
|
|
90
184
|
};
|
|
@@ -100,12 +194,22 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
100
194
|
return createWorker(embeddedSource, workerOptions);
|
|
101
195
|
})
|
|
102
196
|
: createWorker(embeddedSource, workerOptions);
|
|
197
|
+
refreshWorker = worker;
|
|
198
|
+
let released = false;
|
|
199
|
+
const releaseWorker = () => {
|
|
200
|
+
if (released)
|
|
201
|
+
return;
|
|
202
|
+
released = true;
|
|
203
|
+
worker.unref();
|
|
204
|
+
};
|
|
103
205
|
worker.on("message", (value) => {
|
|
104
206
|
if (isFlagSnapshot(value)) {
|
|
105
207
|
heldSnapshot = value;
|
|
106
|
-
if (
|
|
208
|
+
if (completeRefresh)
|
|
209
|
+
refreshPendingSnapshot = value;
|
|
210
|
+
if (refreshCompanyUid && refreshPendingSnapshot) {
|
|
107
211
|
try {
|
|
108
|
-
writeFlagSnapshotFile(
|
|
212
|
+
writeFlagSnapshotFile(refreshCompanyUid, value.flags);
|
|
109
213
|
}
|
|
110
214
|
catch (error) {
|
|
111
215
|
captureFlagRegistryException(error instanceof Error ? error : new Error(String(error)), {
|
|
@@ -113,18 +217,28 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
113
217
|
company_uid_present: "true",
|
|
114
218
|
}, sentry);
|
|
115
219
|
}
|
|
220
|
+
refreshPendingSnapshot = null;
|
|
221
|
+
finishRefresh();
|
|
116
222
|
}
|
|
117
223
|
}
|
|
224
|
+
else {
|
|
225
|
+
finishRefresh();
|
|
226
|
+
}
|
|
227
|
+
releaseWorker();
|
|
118
228
|
});
|
|
119
229
|
// A failed background refresh must stay invisible to an offline CLI.
|
|
120
|
-
worker.on("error", () => {
|
|
230
|
+
worker.on("error", () => {
|
|
231
|
+
finishRefresh();
|
|
232
|
+
releaseWorker();
|
|
233
|
+
});
|
|
121
234
|
// Attach listeners first: Worker.on() re-refs the worker, so unref must be
|
|
122
235
|
// last for the parent process to remain free to exit immediately.
|
|
123
|
-
|
|
236
|
+
releaseWorker();
|
|
124
237
|
}
|
|
125
238
|
catch {
|
|
126
239
|
// Construction can fail in restricted Node runtimes. The reader is still
|
|
127
240
|
// safe: it has no snapshot, so it resolves through the legacy vocabulary.
|
|
241
|
+
finishRefresh();
|
|
128
242
|
}
|
|
129
243
|
return processReader;
|
|
130
244
|
}
|
|
@@ -198,5 +312,11 @@ export function currentFlagSnapshot() {
|
|
|
198
312
|
export function _resetFlagRegistryForTests() {
|
|
199
313
|
heldSnapshot = null;
|
|
200
314
|
processReader = null;
|
|
315
|
+
refreshCompletion = Promise.resolve();
|
|
316
|
+
completeRefresh = null;
|
|
317
|
+
refreshWorker = null;
|
|
318
|
+
refreshCompanyUid = undefined;
|
|
319
|
+
refreshPendingSnapshot = null;
|
|
320
|
+
refreshSentry = undefined;
|
|
201
321
|
}
|
|
202
322
|
//# sourceMappingURL=flag-registry.js.map
|
package/dist/main.js
CHANGED
|
@@ -37,7 +37,7 @@ import { isInterceptedProcessExit } from "./utils/intercepted-process-exit.js";
|
|
|
37
37
|
import { isAuthError } from "./utils/auth-error.js";
|
|
38
38
|
import { browserLoginAbandonedMessage } from "./utils/browser-login-abandoned.js";
|
|
39
39
|
import { callbackPortBusyMessage, DEFAULT_CALLBACK_PORT } from "./utils/callback-port-busy.js";
|
|
40
|
-
import { DEFAULT_COGNITO } from "./utils/cognito-session.js";
|
|
40
|
+
import { DEFAULT_COGNITO, isMachineIdentity, loadMachineCachedTokens, } from "./utils/cognito-session.js";
|
|
41
41
|
import { isCompanySelectionError } from "./utils/company-selection-error.js";
|
|
42
42
|
import { canOfferTeamUpgrade, formatPlanGateError, offerTeamUpgrade, offerUpgradeLink, planGateErrorFromVaultError, } from "./utils/plan-gate-error.js";
|
|
43
43
|
import { upgradeToTeam } from "./utils/team-upgrade.js";
|
|
@@ -53,6 +53,7 @@ import { emitPlanLimitNag } from "./lib/plan-limit-nag.js";
|
|
|
53
53
|
import { kickFlagRegistryReadiness } from "./lib/flag-registry.js";
|
|
54
54
|
import { isPackageRootResolutionError, packageRootCaptureContext, } from "./utils/package-root-diagnostics.js";
|
|
55
55
|
import { registerCommandsWithRecovery } from "./startup-registration.js";
|
|
56
|
+
import { releasePackageUseLeaseAfterCommandLoad } from "./utils/package-use-lease.js";
|
|
56
57
|
import { installTreeTornCaptureContext, installTreeTornStderrLine, isInstallTreeTornError, } from "./utils/install-tree-torn.js";
|
|
57
58
|
import { isVaultAccessDeniedError, vaultAccessDeniedMessage, } from "./utils/vault-access-denied-error.js";
|
|
58
59
|
import { fallbackOperatorMessage, unexpectedCliErrorMessage } from "./utils/unexpected-cli-error.js";
|
|
@@ -170,7 +171,13 @@ export async function runCli() {
|
|
|
170
171
|
const isAuthLogin = process.argv[2] === "auth" && process.argv[3] === "login";
|
|
171
172
|
if (!isAuthLogin) {
|
|
172
173
|
runGlobalNetworkLifecycle(process.argv, () => {
|
|
173
|
-
void kickFlagRegistryReadiness({
|
|
174
|
+
void kickFlagRegistryReadiness({
|
|
175
|
+
sentry: Sentry,
|
|
176
|
+
argv: process.argv,
|
|
177
|
+
...(isMachineIdentity()
|
|
178
|
+
? { idToken: loadMachineCachedTokens()?.idToken }
|
|
179
|
+
: {}),
|
|
180
|
+
});
|
|
174
181
|
});
|
|
175
182
|
}
|
|
176
183
|
// Set when a self-update re-exec'd this command on a newer CLI: the child
|
|
@@ -255,6 +262,7 @@ export async function runCli() {
|
|
|
255
262
|
reexecStatus = registration.reexecStatus;
|
|
256
263
|
return;
|
|
257
264
|
}
|
|
265
|
+
await releasePackageUseLeaseAfterCommandLoad(process.argv);
|
|
258
266
|
await program.parseAsync();
|
|
259
267
|
maybeShowFirstSyncNextStep();
|
|
260
268
|
}
|
|
@@ -6,6 +6,7 @@ import { isAuthError } from "./utils/auth-error.js";
|
|
|
6
6
|
import { DEFAULT_SECRETS_CACHE_TTL_MS, hasCacheEntry, readCache, removeCacheEntry, writeCache, } from "./utils/secrets-cache.js";
|
|
7
7
|
import { computeSha256 } from "./utils/integrity.js";
|
|
8
8
|
import { SECRET_NAME_PATTERN } from "./commands/_patterns.js";
|
|
9
|
+
import { finishEnvExportsAndExit } from "./commands/secrets-env-exit.js";
|
|
9
10
|
import { getEntityUid, vaultApiFetch } from "./utils/vault-api.js";
|
|
10
11
|
import { HQ_API_KEY_PREFIX, resolveVaultCredentialForCapability, } from "./utils/resolve-vault-credential.js";
|
|
11
12
|
import { CACHED_SECRETS_SERVER_UNAVAILABLE_MESSAGE, isSecretLoadUnavailable, isUnavailableSecretLoadHttpStatus, SecretLoadUnavailableError, } from "./utils/secret-load-availability.js";
|
|
@@ -436,10 +437,9 @@ export async function runSecretsEnvCommand(opts, parentScope, options = {}) {
|
|
|
436
437
|
const out = redact ? "[REDACTED]" : value;
|
|
437
438
|
payload += `export ${key}=${shellSingleQuote(out)}\n`;
|
|
438
439
|
}
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
emitEnvExportsAndExit(payload);
|
|
440
|
+
// Persist an already-running company snapshot before the synchronous
|
|
441
|
+
// export write exits the process. Personal exports do not wait.
|
|
442
|
+
await finishEnvExportsAndExit(payload, undefined, process.exit, undefined, !parentScope.personal, companyUid);
|
|
443
443
|
return "handled";
|
|
444
444
|
}
|
|
445
445
|
catch (err) {
|
|
@@ -16,6 +16,12 @@ export declare function packageUseUpdateRequestPath(prefix: string, stateDirecto
|
|
|
16
16
|
declare function waitForUpdateRequest(requestPath: string, timeoutMs: number): void;
|
|
17
17
|
export declare function acquirePackageUseLease(prefix: string, hqVersion: string, stateDirectory?: string): PackageUseLease;
|
|
18
18
|
export declare function acquireCurrentPackageUseLease(): PackageUseLease | null;
|
|
19
|
+
/** Release this process's package-use lease after a long-running command has loaded its package modules. */
|
|
20
|
+
export declare function releaseCurrentPackageUseLease(): void;
|
|
21
|
+
/** The lease remains held through command execution and is released on process exit. */
|
|
22
|
+
export declare function releasePackageUseLeaseAfterCommandLoad(_argv?: readonly string[], _release?: () => void): void;
|
|
23
|
+
/** Release only after a command-specific preload succeeds; failures are reported and keep the lease. */
|
|
24
|
+
export declare function releasePackageUseLeaseAfterResidentPreload(preload: () => Promise<void>, reportFailure: (error: unknown) => void, release?: () => void): Promise<void>;
|
|
19
25
|
/** True only when a valid update request belongs to a live updater process. */
|
|
20
26
|
export declare function packageUseUpdatePending(prefix: string, stateDirectory?: string): boolean;
|
|
21
27
|
/** True while the desktop updater owns the per-prefix update request. */
|
|
@@ -12,6 +12,7 @@ export class PackageUseUpdatePendingError extends Error {
|
|
|
12
12
|
this.name = "PackageUseUpdatePendingError";
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
let currentPackageUseLease = null;
|
|
15
16
|
function errorLabel(error) {
|
|
16
17
|
if (error && typeof error === "object") {
|
|
17
18
|
const value = error;
|
|
@@ -195,7 +196,29 @@ export function acquireCurrentPackageUseLease() {
|
|
|
195
196
|
if (nodeModules > 0 && parts[nodeModules - 1] !== "lib")
|
|
196
197
|
return null;
|
|
197
198
|
}
|
|
198
|
-
|
|
199
|
+
const lease = acquirePackageUseLease(prefix, pkg.version);
|
|
200
|
+
currentPackageUseLease = lease;
|
|
201
|
+
return lease;
|
|
202
|
+
}
|
|
203
|
+
/** Release this process's package-use lease after a long-running command has loaded its package modules. */
|
|
204
|
+
export function releaseCurrentPackageUseLease() {
|
|
205
|
+
currentPackageUseLease?.release();
|
|
206
|
+
currentPackageUseLease = null;
|
|
207
|
+
}
|
|
208
|
+
/** The lease remains held through command execution and is released on process exit. */
|
|
209
|
+
export function releasePackageUseLeaseAfterCommandLoad(_argv = process.argv, _release = releaseCurrentPackageUseLease) {
|
|
210
|
+
// The package-use lease's process-exit listener owns its normal release.
|
|
211
|
+
}
|
|
212
|
+
/** Release only after a command-specific preload succeeds; failures are reported and keep the lease. */
|
|
213
|
+
export async function releasePackageUseLeaseAfterResidentPreload(preload, reportFailure, release = releaseCurrentPackageUseLease) {
|
|
214
|
+
try {
|
|
215
|
+
await preload();
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
reportFailure(error);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
release();
|
|
199
222
|
}
|
|
200
223
|
/** True only when a valid update request belongs to a live updater process. */
|
|
201
224
|
export function packageUseUpdatePending(prefix, stateDirectory) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ResidentPackagePath = "bot" | "mcp";
|
|
2
|
+
/** Package imports reachable lazily from each resident command's runtime graph. */
|
|
3
|
+
export declare const RESIDENT_PACKAGE_MODULES: Record<ResidentPackagePath, readonly string[]>;
|
|
4
|
+
export declare class ResidentPackagePreloadError extends Error {
|
|
5
|
+
readonly moduleSpecifier: string;
|
|
6
|
+
readonly errorClass: string;
|
|
7
|
+
constructor(moduleSpecifier: string, errorClass: string);
|
|
8
|
+
}
|
|
9
|
+
/** Report only bounded runtime metadata; never include an exception message or path. */
|
|
10
|
+
export declare function reportResidentPackagePreloadFailure(error: unknown): void;
|
|
11
|
+
/** Resolve every lazy package module before a resident command releases its lease. */
|
|
12
|
+
export declare function preloadResidentPackageModules(residentPath: ResidentPackagePath, load?: () => Promise<unknown>): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=resident-package-preload.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Package imports reachable lazily from each resident command's runtime graph. */
|
|
2
|
+
export const RESIDENT_PACKAGE_MODULES = {
|
|
3
|
+
bot: ["../lib/flag-registry.js", "../commands/access.js", "../sentry.js", "open"],
|
|
4
|
+
mcp: [
|
|
5
|
+
"../lib/mcp/tools.js",
|
|
6
|
+
"../lib/mcp/prompts.js",
|
|
7
|
+
"../utils/cognito-session.js",
|
|
8
|
+
"../lib/daemon/mcp-oauth.js",
|
|
9
|
+
"../commands/access.js",
|
|
10
|
+
"../lib/flag-registry.js",
|
|
11
|
+
"@modelcontextprotocol/sdk/client/index.js",
|
|
12
|
+
"@modelcontextprotocol/sdk/client/streamableHttp.js",
|
|
13
|
+
"../sentry.js",
|
|
14
|
+
"open",
|
|
15
|
+
"./staged-npm-install.js",
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
const LOADERS = {
|
|
19
|
+
bot: [
|
|
20
|
+
() => import("../lib/flag-registry.js"),
|
|
21
|
+
() => import("../commands/access.js"),
|
|
22
|
+
() => import("../sentry.js"),
|
|
23
|
+
() => import("open"),
|
|
24
|
+
],
|
|
25
|
+
mcp: [
|
|
26
|
+
() => import("../lib/mcp/tools.js"),
|
|
27
|
+
() => import("../lib/mcp/prompts.js"),
|
|
28
|
+
() => import("../utils/cognito-session.js"),
|
|
29
|
+
() => import("../lib/daemon/mcp-oauth.js"),
|
|
30
|
+
() => import("../commands/access.js"),
|
|
31
|
+
() => import("../lib/flag-registry.js"),
|
|
32
|
+
() => import("@modelcontextprotocol/sdk/client/index.js"),
|
|
33
|
+
() => import("@modelcontextprotocol/sdk/client/streamableHttp.js"),
|
|
34
|
+
() => import("../sentry.js"),
|
|
35
|
+
() => import("open"),
|
|
36
|
+
() => import("./staged-npm-install.js"),
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
export class ResidentPackagePreloadError extends Error {
|
|
40
|
+
moduleSpecifier;
|
|
41
|
+
errorClass;
|
|
42
|
+
constructor(moduleSpecifier, errorClass) {
|
|
43
|
+
super("Resident package module preload failed");
|
|
44
|
+
this.moduleSpecifier = moduleSpecifier;
|
|
45
|
+
this.errorClass = errorClass;
|
|
46
|
+
this.name = "ResidentPackagePreloadError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function boundedErrorClass(error) {
|
|
50
|
+
const value = error && typeof error === "object"
|
|
51
|
+
? error
|
|
52
|
+
: {};
|
|
53
|
+
const label = typeof value.code === "string" ? value.code : typeof value.name === "string" ? value.name : "Error";
|
|
54
|
+
return /^[A-Za-z][A-Za-z0-9_]{0,39}$/.test(label) ? label : "Error";
|
|
55
|
+
}
|
|
56
|
+
/** Report only bounded runtime metadata; never include an exception message or path. */
|
|
57
|
+
export function reportResidentPackagePreloadFailure(error) {
|
|
58
|
+
const failure = error instanceof ResidentPackagePreloadError
|
|
59
|
+
? error
|
|
60
|
+
: new ResidentPackagePreloadError("unknown", boundedErrorClass(error));
|
|
61
|
+
process.stderr.write(`hq-cli resident package preload failed (class=${failure.errorClass}, module=${failure.moduleSpecifier})\n`);
|
|
62
|
+
}
|
|
63
|
+
/** Resolve every lazy package module before a resident command releases its lease. */
|
|
64
|
+
export async function preloadResidentPackageModules(residentPath, load) {
|
|
65
|
+
if (load) {
|
|
66
|
+
await load();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
await Promise.all(LOADERS[residentPath].map(async (loader, index) => {
|
|
70
|
+
try {
|
|
71
|
+
await loader();
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new ResidentPackagePreloadError(RESIDENT_PACKAGE_MODULES[residentPath][index] ?? "unknown", boundedErrorClass(error));
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=resident-package-preload.js.map
|