@indigoai-us/hq-cli 5.345.47 → 5.345.49
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 +15 -0
- package/README.md +2 -1
- package/assets/scaffold/core/scripts/hq-session.sh +113 -7
- package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +101 -0
- package/dist/command-catalog.generated.d.ts +12 -0
- package/dist/command-catalog.generated.js +15 -0
- package/dist/commands/anywhere-runtime-flag.js +5 -3
- package/dist/commands/core-hq-session-native.d.ts +2 -0
- package/dist/commands/core-hq-session-native.js +242 -52
- package/dist/commands/core-resolve-company-native.d.ts +1 -0
- package/dist/commands/core-resolve-company-native.js +41 -11
- package/dist/commands/feedback.d.ts +10 -0
- package/dist/commands/feedback.js +45 -0
- package/dist/commands/lanes-message.js +2 -2
- package/dist/commands/lanes.js +2 -2
- package/dist/commands/mcp-serve.js +17 -4
- package/dist/commands/secrets.d.ts +5 -0
- package/dist/commands/secrets.js +181 -2
- package/dist/commands/workers.d.ts +4 -1
- package/dist/commands/workers.js +57 -15
- package/dist/lib/anywhere-person-setting.d.ts +1 -0
- package/dist/lib/anywhere-person-setting.js +12 -0
- package/dist/lib/anywhere-runtime-fast-flag.js +5 -3
- package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -1
- package/dist/lib/anywhere-runtime-flag-key.js +6 -1
- package/dist/lib/cli-flag-keys.d.ts +1 -0
- package/dist/lib/cli-flag-keys.js +1 -0
- package/dist/lib/core-utils/session-id.d.ts +6 -0
- package/dist/lib/core-utils/session-id.js +35 -0
- package/dist/lib/daemon/catalog.js +1 -1
- package/dist/lib/daemon/flag.d.ts +1 -1
- package/dist/lib/daemon/flag.js +20 -9
- package/dist/lib/daemon/lanes-reconcile-guard.d.ts +6 -5
- package/dist/lib/daemon/lanes-reconcile-guard.js +26 -7
- package/dist/lib/daemon/run.d.ts +6 -0
- package/dist/lib/daemon/run.js +36 -9
- package/dist/lib/daemon/server.d.ts +1 -1
- package/dist/lib/daemon/supervisor.d.ts +7 -1
- package/dist/lib/daemon/supervisor.js +50 -4
- package/dist/lib/lanes/reconcile.js +6 -4
- package/dist/lib/lanes/registry-flag.d.ts +11 -1
- package/dist/lib/lanes/registry-flag.js +68 -29
- package/dist/lib/lanes/watcher.js +63 -2
- package/dist/utils/resident-package-preload.js +8 -1
- package/dist/utils/self-update.js +6 -5
- package/dist/utils/version-gate.d.ts +11 -4
- package/dist/utils/version-gate.js +132 -41
- package/docs/anywhere-runtime.md +5 -3
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ export function readLanesFlagLocalOverride(flagKey, env = process.env) {
|
|
|
8
8
|
}
|
|
9
9
|
const registryReads = new Map();
|
|
10
10
|
const reportedThrownFlagReads = new Set();
|
|
11
|
+
const reportedRegistryFailureFallbacks = new Set();
|
|
11
12
|
class MissingFlagTokenError extends Error {
|
|
12
13
|
constructor() {
|
|
13
14
|
super("No cached HQ identity token is available");
|
|
@@ -17,13 +18,13 @@ class MissingFlagTokenError extends Error {
|
|
|
17
18
|
function errorClass(error) {
|
|
18
19
|
return error instanceof Error ? error.name : "UnknownError";
|
|
19
20
|
}
|
|
20
|
-
function reportReadFailure(flagKey, error) {
|
|
21
|
+
function reportReadFailure(flagKey, error, fallback) {
|
|
21
22
|
const suffix = flagKey.replace(/^lanes\./, "").replaceAll(/[^a-zA-Z0-9]+/g, "_");
|
|
22
23
|
process.stderr.write(`${JSON.stringify({
|
|
23
24
|
event: `lanes.${suffix}.flag_read_failed`,
|
|
24
25
|
flag: flagKey,
|
|
25
26
|
error: errorClass(error),
|
|
26
|
-
fallback
|
|
27
|
+
fallback,
|
|
27
28
|
})}\n`);
|
|
28
29
|
}
|
|
29
30
|
function reportInvalidCompanyUid(flagKey, source) {
|
|
@@ -46,27 +47,42 @@ function cacheKey(flagKey, company, endpoint) {
|
|
|
46
47
|
}
|
|
47
48
|
/**
|
|
48
49
|
* Read a lanes flag once for each company identity in this CLI process. The
|
|
49
|
-
* cache contains only an in-flight or settled
|
|
50
|
+
* cache contains only an in-flight or settled decision result; it is never
|
|
50
51
|
* written to disk. A local override is checked on every call and wins first.
|
|
51
52
|
*/
|
|
52
53
|
export async function readLanesRegistryFlag(flagKey, options = {}) {
|
|
54
|
+
const result = await readLanesRegistryFlagResult(flagKey, options);
|
|
55
|
+
return result.state === "resolved" ? result.value : false;
|
|
56
|
+
}
|
|
57
|
+
/** Read a lanes flag while preserving registry failure as an explicit unknown state. */
|
|
58
|
+
export async function readLanesRegistryFlagResult(flagKey, options = {}) {
|
|
59
|
+
const outcome = await readRegistryFlagOutcome(flagKey, options, false);
|
|
60
|
+
return outcome.result;
|
|
61
|
+
}
|
|
62
|
+
async function readRegistryFlagOutcome(flagKey, options, failOpen) {
|
|
53
63
|
const env = options.env ?? process.env;
|
|
54
64
|
const localOverride = readLanesFlagLocalOverride(flagKey, env);
|
|
55
65
|
if (localOverride !== null)
|
|
56
|
-
return localOverride;
|
|
66
|
+
return { result: { state: "resolved", value: localOverride } };
|
|
57
67
|
const companyUid = options.company?.uid?.trim();
|
|
58
68
|
if (companyUid && !companyUid.startsWith("cmp_")) {
|
|
59
69
|
reportInvalidCompanyUid(flagKey, options.companyUidSource ?? "company.uid");
|
|
60
|
-
return false;
|
|
70
|
+
return { result: { state: "resolved", value: false } };
|
|
61
71
|
}
|
|
62
72
|
const endpoint = resolveFlagRegistryEndpoint(undefined, env);
|
|
63
73
|
const key = cacheKey(flagKey, options.company, endpoint);
|
|
64
74
|
const cached = registryReads.get(key);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
const pending = cached ?? readRegistryValue(flagKey, endpoint, options);
|
|
76
|
+
if (!cached)
|
|
77
|
+
registryReads.set(key, pending);
|
|
78
|
+
const outcome = await pending;
|
|
79
|
+
reportRegistryFailure(flagKey, key, outcome, options, failOpen);
|
|
80
|
+
return outcome;
|
|
81
|
+
}
|
|
82
|
+
/** Delivery and wake call sites opt in to availability when the registry is unreadable. */
|
|
83
|
+
export async function readLanesRegistryFlagFailOpen(flagKey, options = {}) {
|
|
84
|
+
const outcome = await readRegistryFlagOutcome(flagKey, options, true);
|
|
85
|
+
return outcome.result.state === "unknown" ? true : outcome.result.value;
|
|
70
86
|
}
|
|
71
87
|
/**
|
|
72
88
|
* Flag readers normally report their own failures and return the safe default.
|
|
@@ -97,15 +113,10 @@ async function readRegistryValue(flagKey, endpoint, options) {
|
|
|
97
113
|
const companyUid = company?.uid?.trim();
|
|
98
114
|
const companySlug = company?.slug?.trim();
|
|
99
115
|
let client;
|
|
100
|
-
let
|
|
101
|
-
let
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
failed = true;
|
|
105
|
-
if (reported)
|
|
106
|
-
return;
|
|
107
|
-
reported = true;
|
|
108
|
-
(options.reportFailure ?? ((cause) => reportReadFailure(flagKey, cause)))(error);
|
|
116
|
+
let resolution;
|
|
117
|
+
let failure;
|
|
118
|
+
const rememberFailure = (error) => {
|
|
119
|
+
failure ??= { error };
|
|
109
120
|
};
|
|
110
121
|
try {
|
|
111
122
|
const acquireToken = options.getToken ?? (async () => {
|
|
@@ -135,30 +146,58 @@ async function readRegistryValue(flagKey, endpoint, options) {
|
|
|
135
146
|
requestTimeoutMs: LANES_FLAG_READ_TIMEOUT_MS,
|
|
136
147
|
...(companyUid ? { companyUid } : {}),
|
|
137
148
|
...(companySlug ? { companyIdentifiers: [companySlug] } : {}),
|
|
138
|
-
onError:
|
|
149
|
+
onError: rememberFailure,
|
|
139
150
|
});
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
151
|
+
try {
|
|
152
|
+
await client.ready();
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
rememberFailure(error);
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
resolution = client.explain(flagKey, { fallback: false });
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
rememberFailure(error);
|
|
146
162
|
}
|
|
147
163
|
}
|
|
148
164
|
catch (error) {
|
|
149
|
-
|
|
165
|
+
rememberFailure(error);
|
|
150
166
|
}
|
|
151
167
|
finally {
|
|
152
168
|
try {
|
|
153
169
|
client?.close();
|
|
154
170
|
}
|
|
155
171
|
catch (error) {
|
|
156
|
-
|
|
172
|
+
rememberFailure(error);
|
|
157
173
|
}
|
|
158
174
|
}
|
|
159
|
-
|
|
175
|
+
if (resolution?.source === "snapshot" || resolution?.source === "local-override") {
|
|
176
|
+
return { result: { state: "resolved", value: resolution.value }, ...(failure ? { failure } : {}) };
|
|
177
|
+
}
|
|
178
|
+
if (failure)
|
|
179
|
+
return { result: { state: "unknown" }, failure };
|
|
180
|
+
return { result: { state: "resolved", value: resolution?.value ?? false } };
|
|
181
|
+
}
|
|
182
|
+
function reportRegistryFailure(flagKey, key, outcome, options, failOpen) {
|
|
183
|
+
if (!outcome.failure)
|
|
184
|
+
return;
|
|
185
|
+
const fallback = outcome.result.state === "resolved"
|
|
186
|
+
? outcome.result.value
|
|
187
|
+
: failOpen;
|
|
188
|
+
const reportKey = `${key}:${fallback}`;
|
|
189
|
+
if (reportedRegistryFailureFallbacks.has(reportKey))
|
|
190
|
+
return;
|
|
191
|
+
reportedRegistryFailureFallbacks.add(reportKey);
|
|
192
|
+
const reportFailure = options.reportFailure;
|
|
193
|
+
if (reportFailure) {
|
|
194
|
+
reportFailure(outcome.failure.error);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
reportReadFailure(flagKey, outcome.failure.error, fallback);
|
|
160
198
|
}
|
|
161
199
|
export function _resetLanesRegistryFlagCacheForTests() {
|
|
162
200
|
registryReads.clear();
|
|
201
|
+
reportedRegistryFailureFallbacks.clear();
|
|
163
202
|
}
|
|
164
203
|
//# sourceMappingURL=registry-flag.js.map
|
|
@@ -398,8 +398,69 @@ function applyTerminalToLaneWithResult(hqRoot, laneId, classification, opts = {}
|
|
|
398
398
|
current.state === "interrupted" ||
|
|
399
399
|
current.state === "cancelled" ||
|
|
400
400
|
current.state === "killed";
|
|
401
|
-
if (terminal)
|
|
402
|
-
|
|
401
|
+
if (terminal) {
|
|
402
|
+
// A terminal state may have been recorded before the worker's final
|
|
403
|
+
// report arrived. The report is durable output from work that already
|
|
404
|
+
// ran, so preserve it without reopening the lane or rechecking admission.
|
|
405
|
+
if (opts.envelope === undefined || opts.envelope === null)
|
|
406
|
+
return current;
|
|
407
|
+
const approvalGate = enforceRegistryApprovalGate(current, opts.envelope);
|
|
408
|
+
effectiveEnvelope = approvalGate.envelope;
|
|
409
|
+
effectiveClassification = approvalGate.violation
|
|
410
|
+
? { state: "blocked", reason: "approval_gate_violated" }
|
|
411
|
+
: terminalClassification;
|
|
412
|
+
const envelope = effectiveEnvelope;
|
|
413
|
+
const storySwitch = applyStorySwitchEnvelope(current, envelope, at, envelopeRef);
|
|
414
|
+
const next = {
|
|
415
|
+
...current,
|
|
416
|
+
last_envelope: {
|
|
417
|
+
ref: envelopeRef,
|
|
418
|
+
decision: envelope.decision,
|
|
419
|
+
at,
|
|
420
|
+
artifacts: envelope.artifacts,
|
|
421
|
+
...(envelope.blocked_reason !== undefined ? { blocked_reason: envelope.blocked_reason } : {}),
|
|
422
|
+
...(envelope.next_check_at !== undefined ? { next_check_at: envelope.next_check_at } : {}),
|
|
423
|
+
},
|
|
424
|
+
...(envelope.decision !== "blocked" ? { keep_alive_blocked_resume_streak: 0 } : {}),
|
|
425
|
+
envelope_validation_errors: opts.envelopeValidationErrors,
|
|
426
|
+
};
|
|
427
|
+
if (!approvalGate.violation && approvalGate.approvedIdsToConsume.length > 0) {
|
|
428
|
+
next.registry_worker_approval_history = approvalGate.approvedIdsToConsume.reduce((history, approvalId) => appendCappedHistory(history, {
|
|
429
|
+
event: "consumed",
|
|
430
|
+
approval_id: approvalId,
|
|
431
|
+
envelope_ref: envelopeRef,
|
|
432
|
+
at,
|
|
433
|
+
}), current.registry_worker_approval_history ?? []);
|
|
434
|
+
}
|
|
435
|
+
if (current.state === "cancelled" || current.state === "killed") {
|
|
436
|
+
return JSON.stringify(next) !== JSON.stringify(current) ? next : current;
|
|
437
|
+
}
|
|
438
|
+
const storyStatus = storySwitch.suppressCompletion
|
|
439
|
+
? "in_progress"
|
|
440
|
+
: terminalEnvelopeStoryStatus(current, envelope.decision, envelope.story_status);
|
|
441
|
+
if (storyStatus !== undefined) {
|
|
442
|
+
next.story_status = storySwitch.suppressCompletion ? "in_progress" : envelope.story_status;
|
|
443
|
+
next.story_complete = storySwitch.suppressCompletion ? false : envelope.story_complete;
|
|
444
|
+
next.story_complete_reason = storySwitch.suppressCompletion ? undefined : envelope.story_complete_reason;
|
|
445
|
+
next.stories = laneStories(current).map((story) => story.project_id === current.project_id && story.story_id === current.story_id
|
|
446
|
+
? {
|
|
447
|
+
...story,
|
|
448
|
+
status: storyStatus,
|
|
449
|
+
updated_at: at,
|
|
450
|
+
...(envelope.decision === "blocked"
|
|
451
|
+
? { blocked_reason: envelope.blocked_reason ?? "" }
|
|
452
|
+
: { blocked_reason: undefined }),
|
|
453
|
+
}
|
|
454
|
+
: story);
|
|
455
|
+
}
|
|
456
|
+
if (storySwitch.clearPending)
|
|
457
|
+
next.story_switch_pending = undefined;
|
|
458
|
+
if (storySwitch.storySwitchUnacknowledged !== undefined) {
|
|
459
|
+
next.story_switch_unacknowledged = storySwitch.storySwitchUnacknowledged;
|
|
460
|
+
next.attribution_history = storySwitch.attributionHistory ?? current.attribution_history;
|
|
461
|
+
}
|
|
462
|
+
return JSON.stringify(next) !== JSON.stringify(current) ? next : current;
|
|
463
|
+
}
|
|
403
464
|
if (current.state === "awaiting_input" || current.state === "blocked") {
|
|
404
465
|
if (current.state === effectiveClassification.state)
|
|
405
466
|
return current;
|
|
@@ -40,7 +40,7 @@ export const RESIDENT_PACKAGE_MODULES = {
|
|
|
40
40
|
"@indigoai-us/hq-cloud",
|
|
41
41
|
],
|
|
42
42
|
"bot-logs-follow": [],
|
|
43
|
-
mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud", "../sentry.js", "../lib/doctor/client-health-reason-codes-flag.js", "@indigoai-us/hq-flags-client"],
|
|
43
|
+
mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js", "../lib/anywhere-person-setting.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud", "../sentry.js", "../lib/doctor/client-health-reason-codes-flag.js", "@indigoai-us/hq-flags-client"],
|
|
44
44
|
childWrapper: ["../main.js"],
|
|
45
45
|
coreJobRun: ["../lib/flag-registry.js"],
|
|
46
46
|
"daemon-call": ["../lib/daemon/protocol.js"],
|
|
@@ -76,6 +76,7 @@ export const RESIDENT_PACKAGE_MODULES = {
|
|
|
76
76
|
"../utils/plan-gate-error.js",
|
|
77
77
|
"open",
|
|
78
78
|
"../commands/mcp-serve.js",
|
|
79
|
+
"../lib/anywhere-person-setting.js",
|
|
79
80
|
"../lib/daemon/mcp-oauth.js",
|
|
80
81
|
"../lib/mcp/tools.js",
|
|
81
82
|
"../lib/mcp/prompts.js",
|
|
@@ -83,6 +84,8 @@ export const RESIDENT_PACKAGE_MODULES = {
|
|
|
83
84
|
"../utils/cli-telemetry.js",
|
|
84
85
|
"../utils/cognito-session.js",
|
|
85
86
|
"../lib/first-sync-next-step.js",
|
|
87
|
+
"../lib/process-flag-company-context.js",
|
|
88
|
+
"../lib/flag-snapshot-file.js",
|
|
86
89
|
"@indigoai-us/hq-cloud",
|
|
87
90
|
],
|
|
88
91
|
};
|
|
@@ -185,6 +188,7 @@ const LOADERS = {
|
|
|
185
188
|
mcp: [
|
|
186
189
|
() => import("../lib/mcp/tools.js"),
|
|
187
190
|
() => import("../lib/mcp/prompts.js"),
|
|
191
|
+
() => import("../lib/anywhere-person-setting.js"),
|
|
188
192
|
() => import("../utils/cognito-session.js"),
|
|
189
193
|
() => import("@indigoai-us/hq-cloud"),
|
|
190
194
|
async () => {
|
|
@@ -345,6 +349,7 @@ const LOADERS = {
|
|
|
345
349
|
() => import("../utils/plan-gate-error.js"),
|
|
346
350
|
() => import("open"),
|
|
347
351
|
() => import("../commands/mcp-serve.js"),
|
|
352
|
+
() => import("../lib/anywhere-person-setting.js"),
|
|
348
353
|
() => import("../lib/daemon/mcp-oauth.js"),
|
|
349
354
|
() => import("../lib/mcp/tools.js"),
|
|
350
355
|
() => import("../lib/mcp/prompts.js"),
|
|
@@ -352,6 +357,8 @@ const LOADERS = {
|
|
|
352
357
|
() => import("../utils/cli-telemetry.js"),
|
|
353
358
|
() => import("../utils/cognito-session.js"),
|
|
354
359
|
() => import("../lib/first-sync-next-step.js"),
|
|
360
|
+
() => import("../lib/process-flag-company-context.js"),
|
|
361
|
+
() => import("../lib/flag-snapshot-file.js"),
|
|
355
362
|
() => import("@indigoai-us/hq-cloud"),
|
|
356
363
|
],
|
|
357
364
|
};
|
|
@@ -65,7 +65,7 @@ import chalk from "chalk";
|
|
|
65
65
|
import { Sentry } from "../sentry.js";
|
|
66
66
|
import { isCliTelemetryDisabled } from "./cli-telemetry.js";
|
|
67
67
|
import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
|
|
68
|
-
import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput,
|
|
68
|
+
import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput, resolvePnpmPathInstall, pnpmUpdateEnv, pnpmInstalledVersion, performPnpmUpdate, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, UPDATE_RECURSION_GUARD_ENV, } from "./version-gate.js";
|
|
69
69
|
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";
|
|
@@ -926,17 +926,18 @@ async function attemptUpdateAndReexec(argv, flavor, known, deps, env) {
|
|
|
926
926
|
// actually running would stay exactly as stale as it started while the CLI
|
|
927
927
|
// reported success and re-exec'd into the same build. That is the shape of
|
|
928
928
|
// the 2026-09-02 gate loop — see isLocalDependencyInstall in version-gate.ts.
|
|
929
|
-
|
|
929
|
+
let install = (deps.resolveInstall ?? resolveRunningInstall)();
|
|
930
930
|
if (isLocalDependencyInstall(install)) {
|
|
931
931
|
console.error(chalk.dim(`hq-cli ${latest} is available, but this copy is a local dependency (${install.packageRoot}) — update the project that owns it.`));
|
|
932
932
|
return { action: "skipped", latest };
|
|
933
933
|
}
|
|
934
934
|
if (install.manager === "pnpm") {
|
|
935
|
-
const
|
|
936
|
-
if (
|
|
937
|
-
console.error(chalk.yellow(`⚠ hq self-update refused: ${
|
|
935
|
+
const pathTarget = resolvePnpmPathInstall(install);
|
|
936
|
+
if (pathTarget.refusal) {
|
|
937
|
+
console.error(chalk.yellow(`⚠ hq self-update refused: ${pathTarget.refusal}`));
|
|
938
938
|
return { action: "skipped", latest };
|
|
939
939
|
}
|
|
940
|
+
install = pathTarget.install;
|
|
940
941
|
}
|
|
941
942
|
const releaseLock = flavor.lock ? (deps.acquireLock ?? acquireUpdateLock)() : () => { };
|
|
942
943
|
if (!releaseLock)
|
|
@@ -194,6 +194,8 @@ export declare function isPrefixWritable(prefix: string, access?: (target: strin
|
|
|
194
194
|
* existing ancestor: npm can create a child only if that ancestor is writable.
|
|
195
195
|
*/
|
|
196
196
|
export declare function canWriteNpmInstall(prefix: string, packageRoot: string, access?: (target: string, mode: number) => void, platform?: NodeJS.Platform): boolean;
|
|
197
|
+
/** Check whether pnpm can replace its global package and PATH shim. */
|
|
198
|
+
export declare function canWritePnpmInstall(install: RunningInstall, access?: (target: string, mode: number) => void): boolean;
|
|
197
199
|
/** Claim the one local warning/report for this installed version and prefix. */
|
|
198
200
|
export declare function claimNonWritableNpmPrefixNotice(install: Pick<RunningInstall, "manager" | "prefix" | "packageRoot">, fromVersion: string, homeDir?: string, kind?: "user" | "report"): boolean;
|
|
199
201
|
/** Emit one path-free warning event for a handled non-writable npm prefix. */
|
|
@@ -223,10 +225,12 @@ export declare function derivePnpmHome(install: RunningInstall): string | null;
|
|
|
223
225
|
* Environment for the pnpm self-update spawn. pnpm requires its configured
|
|
224
226
|
* global bin directory to be on PATH; pnpm 12.5.1 reports
|
|
225
227
|
* `ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH` otherwise. Minimal-environment parents
|
|
226
|
-
* may also lack PNPM_HOME, so derive it from the
|
|
228
|
+
* may also lack PNPM_HOME, so derive it from the selected install when needed.
|
|
227
229
|
*
|
|
228
|
-
* A no-op for npm/bun installs.
|
|
229
|
-
*
|
|
230
|
+
* A no-op for npm/bun installs. The selected install's home owns its global
|
|
231
|
+
* bin directory, so it takes precedence over an inherited PNPM_HOME after the
|
|
232
|
+
* updater retargets the install. Returns `undefined` only when the inherited
|
|
233
|
+
* environment already has the required values.
|
|
230
234
|
*/
|
|
231
235
|
export declare function pnpmUpdateEnv(install: RunningInstall, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv | undefined;
|
|
232
236
|
export declare function buildPrefixedInstallArgv(prefix: string): string[];
|
|
@@ -436,7 +440,10 @@ declare function probeCliVersion(bin: string): string | null;
|
|
|
436
440
|
* shims, so writing one home cannot change a different home that appears first
|
|
437
441
|
* on PATH.
|
|
438
442
|
*/
|
|
439
|
-
export declare function
|
|
443
|
+
export declare function resolvePnpmPathInstall(install: RunningInstall, canWrite?: (target: RunningInstall) => boolean): {
|
|
444
|
+
install: RunningInstall;
|
|
445
|
+
refusal: string | null;
|
|
446
|
+
};
|
|
440
447
|
/**
|
|
441
448
|
* Read-your-writes convergence check, run after an install reports success.
|
|
442
449
|
*
|
|
@@ -101,11 +101,11 @@ function findRunningPackageRoot() {
|
|
|
101
101
|
return findCliPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
|
|
102
102
|
}
|
|
103
103
|
/** Resolve an on-PATH executable back to the installed hq-cli package it loads. */
|
|
104
|
-
function
|
|
104
|
+
function resolvePackageRootForExecutable(bin) {
|
|
105
105
|
try {
|
|
106
106
|
const packageRoot = findCliPackageRoot(path.dirname(realpathSync(bin)));
|
|
107
107
|
if (packageRoot)
|
|
108
|
-
return packageRoot;
|
|
108
|
+
return { packageRoot, candidates: [packageRoot] };
|
|
109
109
|
}
|
|
110
110
|
catch {
|
|
111
111
|
// A missing or inaccessible executable may still be recoverable from the
|
|
@@ -115,25 +115,26 @@ function findPackageRootForExecutable(bin) {
|
|
|
115
115
|
// resolves symlinks, but a generated shim is a regular file, so walking up
|
|
116
116
|
// from its real path never reaches the package it invokes. Look up only the
|
|
117
117
|
// hq-cli package under the matching PNPM_HOME/global layout instead.
|
|
118
|
-
const
|
|
118
|
+
const shimBinDir = path.dirname(bin);
|
|
119
119
|
const configuredHome = process.env.PNPM_HOME;
|
|
120
|
-
const inferredHome = path.basename(
|
|
121
|
-
? path.dirname(
|
|
122
|
-
:
|
|
120
|
+
const inferredHome = path.basename(shimBinDir).toLowerCase() === "bin"
|
|
121
|
+
? path.dirname(shimBinDir)
|
|
122
|
+
: shimBinDir;
|
|
123
123
|
const candidateHomes = [configuredHome, inferredHome].filter((home, index, homes) => Boolean(home) && homes.findIndex((candidate) => candidate === home) === index);
|
|
124
|
-
const home = candidateHomes.find((candidate) => samePath(
|
|
125
|
-
samePath(
|
|
124
|
+
const home = candidateHomes.find((candidate) => samePath(shimBinDir, candidate) ||
|
|
125
|
+
samePath(shimBinDir, path.join(candidate, "bin")));
|
|
126
126
|
if (!home)
|
|
127
|
-
return null;
|
|
127
|
+
return { packageRoot: null, candidates: [] };
|
|
128
128
|
const globalRoot = path.join(home, "global");
|
|
129
129
|
let storeVersions;
|
|
130
130
|
try {
|
|
131
131
|
storeVersions = readdirSync(globalRoot);
|
|
132
132
|
}
|
|
133
133
|
catch {
|
|
134
|
-
return null;
|
|
134
|
+
return { packageRoot: null, candidates: [] };
|
|
135
135
|
}
|
|
136
136
|
const packageSegments = CLI_NAME.split("/");
|
|
137
|
+
const candidates = new Set();
|
|
137
138
|
for (const storeVersion of storeVersions) {
|
|
138
139
|
const storeRoot = path.join(globalRoot, storeVersion);
|
|
139
140
|
const packageRoots = [
|
|
@@ -156,11 +157,38 @@ function findPackageRootForExecutable(bin) {
|
|
|
156
157
|
if (packageRoot &&
|
|
157
158
|
samePath(packageRoot, candidate) &&
|
|
158
159
|
isPnpmManagedPackageDir(packageRoot)) {
|
|
159
|
-
|
|
160
|
+
candidates.add(packageRoot);
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
|
-
|
|
164
|
+
const roots = [...candidates];
|
|
165
|
+
if (roots.length <= 1)
|
|
166
|
+
return { packageRoot: roots[0] ?? null, candidates: roots };
|
|
167
|
+
// pnpm's generated shim identifies the package entry point it will execute.
|
|
168
|
+
// Use that target to select among v11 hash directories instead of letting
|
|
169
|
+
// directory enumeration order choose the global project and virtual store.
|
|
170
|
+
let shimContents;
|
|
171
|
+
try {
|
|
172
|
+
shimContents = readFileSync(bin, "utf8").replace(/\\/g, "/");
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return { packageRoot: null, candidates: roots };
|
|
176
|
+
}
|
|
177
|
+
const selected = roots.filter((root) => {
|
|
178
|
+
const entryPoint = installedHqEntryPoint({ manager: "pnpm", prefix: null, packageRoot: root });
|
|
179
|
+
if (!entryPoint)
|
|
180
|
+
return false;
|
|
181
|
+
const absolute = entryPoint.replace(/\\/g, "/");
|
|
182
|
+
const relative = path.relative(shimBinDir, entryPoint).replace(/\\/g, "/");
|
|
183
|
+
return shimContents.includes(absolute) || shimContents.includes(relative);
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
packageRoot: selected.length === 1 ? selected[0] : null,
|
|
187
|
+
candidates: roots,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function findPackageRootForExecutable(bin) {
|
|
191
|
+
return resolvePackageRootForExecutable(bin).packageRoot;
|
|
164
192
|
}
|
|
165
193
|
function samePath(left, right) {
|
|
166
194
|
const canonical = (value) => {
|
|
@@ -507,6 +535,39 @@ export function canWriteNpmInstall(prefix, packageRoot, access = (target, mode)
|
|
|
507
535
|
}
|
|
508
536
|
});
|
|
509
537
|
}
|
|
538
|
+
/** Check whether pnpm can replace its global package and PATH shim. */
|
|
539
|
+
export function canWritePnpmInstall(install, access = (target, mode) => accessSync(target, mode)) {
|
|
540
|
+
const home = derivePnpmHome(install);
|
|
541
|
+
if (!home || !install.packageRoot)
|
|
542
|
+
return false;
|
|
543
|
+
const mode = fsConstants.W_OK | fsConstants.X_OK;
|
|
544
|
+
const dirs = pnpmInstallDirs(install);
|
|
545
|
+
const targets = [
|
|
546
|
+
home,
|
|
547
|
+
path.join(home, "global"),
|
|
548
|
+
path.join(home, "store"),
|
|
549
|
+
...(dirs ? [dirs.globalDir, dirs.storeDir, dirs.virtualStoreDir] : []),
|
|
550
|
+
path.dirname(install.packageRoot),
|
|
551
|
+
];
|
|
552
|
+
return targets.every((target) => {
|
|
553
|
+
let candidate = path.resolve(target);
|
|
554
|
+
while (true) {
|
|
555
|
+
try {
|
|
556
|
+
access(candidate, mode);
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
if (error?.code !== "ENOENT")
|
|
561
|
+
return false;
|
|
562
|
+
// pnpm creates missing targets under the nearest existing writable ancestor.
|
|
563
|
+
const parent = path.dirname(candidate);
|
|
564
|
+
if (parent === candidate)
|
|
565
|
+
return false;
|
|
566
|
+
candidate = parent;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
510
571
|
const nonWritablePrefixNotices = new Set();
|
|
511
572
|
/** Claim the one local warning/report for this installed version and prefix. */
|
|
512
573
|
export function claimNonWritableNpmPrefixNotice(install, fromVersion, homeDir = os.homedir(), kind = "user") {
|
|
@@ -601,18 +662,22 @@ export function derivePnpmHome(install) {
|
|
|
601
662
|
* Environment for the pnpm self-update spawn. pnpm requires its configured
|
|
602
663
|
* global bin directory to be on PATH; pnpm 12.5.1 reports
|
|
603
664
|
* `ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH` otherwise. Minimal-environment parents
|
|
604
|
-
* may also lack PNPM_HOME, so derive it from the
|
|
665
|
+
* may also lack PNPM_HOME, so derive it from the selected install when needed.
|
|
605
666
|
*
|
|
606
|
-
* A no-op for npm/bun installs.
|
|
607
|
-
*
|
|
667
|
+
* A no-op for npm/bun installs. The selected install's home owns its global
|
|
668
|
+
* bin directory, so it takes precedence over an inherited PNPM_HOME after the
|
|
669
|
+
* updater retargets the install. Returns `undefined` only when the inherited
|
|
670
|
+
* environment already has the required values.
|
|
608
671
|
*/
|
|
609
672
|
export function pnpmUpdateEnv(install, base = process.env) {
|
|
610
673
|
if (install.manager !== "pnpm")
|
|
611
674
|
return undefined;
|
|
612
|
-
const home =
|
|
675
|
+
const home = derivePnpmHome(install) ?? base.PNPM_HOME;
|
|
613
676
|
if (!home)
|
|
614
677
|
return undefined;
|
|
615
|
-
|
|
678
|
+
// pnpm's own `bin -g` command reports PNPM_HOME itself as the global bin
|
|
679
|
+
// directory; it must be that directory, not a guessed `<home>/bin` child.
|
|
680
|
+
const binDir = home;
|
|
616
681
|
const pathKey = Object.keys(base).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
617
682
|
const currentPath = base[pathKey] ?? "";
|
|
618
683
|
const normalizedBinDir = path.resolve(binDir);
|
|
@@ -633,7 +698,7 @@ export function pnpmUpdateEnv(install, base = process.env) {
|
|
|
633
698
|
COREPACK_DEFAULT_TO_LATEST: "0",
|
|
634
699
|
COREPACK_ENABLE_AUTO_PIN: "0",
|
|
635
700
|
};
|
|
636
|
-
if (
|
|
701
|
+
if (base.PNPM_HOME !== home)
|
|
637
702
|
result.PNPM_HOME = home;
|
|
638
703
|
if (!alreadyOnPath)
|
|
639
704
|
result[pathKey] = [currentPath, binDir].filter(Boolean).join(path.delimiter);
|
|
@@ -1401,36 +1466,62 @@ function probeCliVersion(bin) {
|
|
|
1401
1466
|
* shims, so writing one home cannot change a different home that appears first
|
|
1402
1467
|
* on PATH.
|
|
1403
1468
|
*/
|
|
1404
|
-
export function
|
|
1469
|
+
export function resolvePnpmPathInstall(install, canWrite = canWritePnpmInstall) {
|
|
1405
1470
|
if (install.manager !== "pnpm" || !install.packageRoot)
|
|
1406
|
-
return null;
|
|
1471
|
+
return { install, refusal: null };
|
|
1407
1472
|
const bin = resolveHqOnPath();
|
|
1408
1473
|
if (!bin)
|
|
1409
|
-
return null;
|
|
1410
|
-
const pathPackageRoot = findPackageRootForExecutable(bin);
|
|
1411
|
-
if (!pathPackageRoot ||
|
|
1412
|
-
globalInstallManagerForPath(bin, pathPackageRoot) !== "pnpm") {
|
|
1413
|
-
return null;
|
|
1414
|
-
}
|
|
1415
|
-
// The fallback resolver scans every hash in a PNPM_HOME because generated
|
|
1416
|
-
// shims do not expose their target. Multiple installs in the SAME home can
|
|
1417
|
-
// therefore produce an arbitrary package root; only a different home means
|
|
1418
|
-
// that updating this install cannot change which global shim wins PATH.
|
|
1474
|
+
return { install, refusal: null };
|
|
1419
1475
|
const runningHome = derivePnpmHome(install);
|
|
1476
|
+
const packageRootLookup = resolvePackageRootForExecutable(bin);
|
|
1477
|
+
const pathPackageRoot = packageRootLookup.packageRoot;
|
|
1478
|
+
if (!pathPackageRoot) {
|
|
1479
|
+
if (packageRootLookup.candidates.length > 1) {
|
|
1480
|
+
return {
|
|
1481
|
+
install,
|
|
1482
|
+
refusal: `could not identify which pnpm package PATH binary ${bin} resolves to; matching global packages: ${packageRootLookup.candidates.join(", ")}`,
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
const binDirectory = path.dirname(bin);
|
|
1486
|
+
const binHome = path.basename(binDirectory).toLowerCase() === "bin"
|
|
1487
|
+
? path.dirname(binDirectory)
|
|
1488
|
+
: binDirectory;
|
|
1489
|
+
if (runningHome && binHome && samePath(runningHome, binHome)) {
|
|
1490
|
+
if (canWrite(install))
|
|
1491
|
+
return { install, refusal: null };
|
|
1492
|
+
return {
|
|
1493
|
+
install,
|
|
1494
|
+
refusal: `cannot update the pnpm install resolved by PATH at ${bin}: its pnpm home is not writable`,
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
return { install, refusal: `could not identify the pnpm-managed install resolved by PATH at ${bin}` };
|
|
1498
|
+
}
|
|
1499
|
+
const pathManager = globalInstallManagerForPath(bin, pathPackageRoot);
|
|
1500
|
+
if (pathManager !== "pnpm") {
|
|
1501
|
+
if (pathManager === "npm" || pathManager === "bun") {
|
|
1502
|
+
// Updating the running pnpm install still makes progress when a known
|
|
1503
|
+
// npm/Bun install shadows it; refusing would guarantee no convergence.
|
|
1504
|
+
return { install, refusal: null };
|
|
1505
|
+
}
|
|
1506
|
+
return { install, refusal: `PATH resolves ${bin} to an install that is not verified as pnpm-managed` };
|
|
1507
|
+
}
|
|
1508
|
+
// The resolver uses the generated shim's target when a pnpm v11 home has
|
|
1509
|
+
// multiple package hashes. Its selected package root must drive the global
|
|
1510
|
+
// directory and virtual store used by the update.
|
|
1420
1511
|
const pathHome = derivePnpmHome({ manager: "pnpm", prefix: null, packageRoot: pathPackageRoot });
|
|
1421
|
-
if (!runningHome || !pathHome
|
|
1422
|
-
return
|
|
1423
|
-
let resolvedBin = bin;
|
|
1424
|
-
try {
|
|
1425
|
-
resolvedBin = realpathSync(bin);
|
|
1512
|
+
if (!runningHome || !pathHome) {
|
|
1513
|
+
return { install, refusal: `could not identify the PNPM_HOME for PATH binary ${bin}` };
|
|
1426
1514
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1515
|
+
const targetInstall = samePath(runningHome, pathHome)
|
|
1516
|
+
? install
|
|
1517
|
+
: { manager: "pnpm", prefix: null, packageRoot: pathPackageRoot };
|
|
1518
|
+
if (!canWrite(targetInstall)) {
|
|
1519
|
+
return {
|
|
1520
|
+
install,
|
|
1521
|
+
refusal: `cannot update the pnpm install resolved by PATH at ${bin}: its pnpm home is not writable`,
|
|
1522
|
+
};
|
|
1429
1523
|
}
|
|
1430
|
-
|
|
1431
|
-
return (`PATH resolves ${resolvedBin} at version ${version} (manager: pnpm), ` +
|
|
1432
|
-
`but this process is running from a different pnpm install at ${install.packageRoot}. ` +
|
|
1433
|
-
"Refusing to update one pnpm home while another hq-cli install wins PATH resolution.");
|
|
1524
|
+
return { install: targetInstall, refusal: null };
|
|
1434
1525
|
}
|
|
1435
1526
|
/**
|
|
1436
1527
|
* Read-your-writes convergence check, run after an install reports success.
|
package/docs/anywhere-runtime.md
CHANGED
|
@@ -5,7 +5,9 @@ local hqd daemon on macOS, Linux, and Windows. On Windows, hqd uses a bundled
|
|
|
5
5
|
PowerShell pipe host that applies and verifies a user-only named-pipe DACL
|
|
6
6
|
before becoming ready. A per-user
|
|
7
7
|
registry links repositories to HQ companies. The `hq-anywhere-runtime` flag
|
|
8
|
-
is
|
|
8
|
+
is an operator kill switch that defaults on when its registry row is absent,
|
|
9
|
+
archived, or unreadable. An explicit registry false or the local
|
|
10
|
+
`HQ_FLAG_HQ_ANYWHERE_RUNTIME=false|0` override turns it off. Runtime access also
|
|
9
11
|
requires the current person to enable the `hq-anywhere` setting in HQ. If that
|
|
10
12
|
setting cannot be read, runtime access stays off.
|
|
11
13
|
|
|
@@ -34,8 +36,8 @@ stored in a repository or synced HQ folder.
|
|
|
34
36
|
Windows requires Windows PowerShell 5.1 or PowerShell 7 for the bundled pipe
|
|
35
37
|
host. If it is unavailable, the pipe cannot be created with a verified
|
|
36
38
|
current-user-only DACL, or another process already owns the pipe name, hqd
|
|
37
|
-
refuses to start. The
|
|
38
|
-
|
|
39
|
+
refuses to start. The flag's missing-row fallback is on across platforms; an
|
|
40
|
+
explicit false remains the operator kill switch.
|
|
39
41
|
|
|
40
42
|
On macOS and Linux, hqd checks the socket directory and socket path with
|
|
41
43
|
`lstat` before serving. It refuses symlinked, shared, or foreign-owned paths.
|