@indigoai-us/hq-cli 5.319.0 → 5.320.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/assets/scaffold/core/scripts/session-journal.sh +40 -2
- package/dist/bin/hq-auth-refresh.d.ts +3 -1
- package/dist/bin/hq-auth-refresh.js +41 -6
- package/dist/commands/core-session-journal-native.js +31 -2
- package/dist/commands/lanes.js +28 -5
- package/dist/index.d.ts +9 -0
- package/dist/index.js +135 -63
- package/dist/lib/daemon/run.d.ts +2 -0
- package/dist/lib/daemon/run.js +10 -1
- package/dist/lib/lanes/flag-keys.d.ts +3 -0
- package/dist/lib/lanes/flag-keys.js +3 -0
- package/dist/utils/package-use-lease.d.ts +25 -0
- package/dist/utils/package-use-lease.js +213 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.320.0] — 2026-10-02
|
|
6
|
+
- Write session-journal titles as escaped YAML strings so free-form titles remain parseable.
|
|
7
|
+
- Added a default-off hq-flags gate that lets lane list pollers skip remote owner refreshes to reduce poll latency.
|
|
8
|
+
- Publish per-process package-use leases so Desktop updates can wait for active hq-cli work without native locks.
|
|
9
|
+
|
|
5
10
|
## [5.319.0] — 2026-10-02
|
|
6
11
|
- Let `hq access` use ACL-checked company presigned reads when STS vend rejects a truncated write-policy scope.
|
|
7
12
|
- Lead autopilot on an Outpost can wake leads that were created on that Outpost, behind the default-off `lanes.autopilot-outpost-native-leads` flag. Before, those leads were skipped as a host mismatch and got no wakes.
|
|
@@ -43,6 +43,41 @@ slugify() {
|
|
|
43
43
|
| cut -c1-40
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
yaml_double_quoted() {
|
|
47
|
+
local value="$1" output='"' character next codepoint hex
|
|
48
|
+
local LC_ALL=C index
|
|
49
|
+
for ((index = 0; index < ${#value}; index++)); do
|
|
50
|
+
character="${value:index:1}"
|
|
51
|
+
if [[ "$character" == $'\xc2' ]] && (( index + 1 < ${#value} )); then
|
|
52
|
+
next="${value:index+1:1}"
|
|
53
|
+
printf -v codepoint '%d' "'${next}"
|
|
54
|
+
if (( codepoint >= 128 && codepoint <= 159 )); then
|
|
55
|
+
printf -v hex '%04x' "$codepoint"
|
|
56
|
+
output+="\\u$hex"
|
|
57
|
+
index=$((index + 1))
|
|
58
|
+
continue
|
|
59
|
+
fi
|
|
60
|
+
fi
|
|
61
|
+
case "$character" in
|
|
62
|
+
\\) output+='\\' ;;
|
|
63
|
+
'"') output+='\"' ;;
|
|
64
|
+
$'\n') output+='\n' ;;
|
|
65
|
+
$'\r') output+='\r' ;;
|
|
66
|
+
$'\t') output+='\t' ;;
|
|
67
|
+
*)
|
|
68
|
+
if [[ "$character" == [[:cntrl:]] ]]; then
|
|
69
|
+
printf -v codepoint '%d' "'${character}"
|
|
70
|
+
printf -v hex '%02x' "$codepoint"
|
|
71
|
+
output+="\\x$hex"
|
|
72
|
+
else
|
|
73
|
+
output+="$character"
|
|
74
|
+
fi
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
done
|
|
78
|
+
printf '%s"' "$output"
|
|
79
|
+
}
|
|
80
|
+
|
|
46
81
|
# Counters are per-session. A date-global counter lets concurrent Claude and
|
|
47
82
|
# Codex tasks trip (and reset) each other's journal reminders.
|
|
48
83
|
counter_key() {
|
|
@@ -140,6 +175,9 @@ case "$cmd" in
|
|
|
140
175
|
|
|
141
176
|
slug="$(slugify "$title")"
|
|
142
177
|
[ -z "$slug" ] && slug="entry"
|
|
178
|
+
yaml_title="$(yaml_double_quoted "$title")"
|
|
179
|
+
yaml_slug="$slug"
|
|
180
|
+
[[ "$slug" != *$'\n'* ]] || yaml_slug="$(yaml_double_quoted "$slug")"
|
|
143
181
|
JOURNAL_WRITE_LOCK=""
|
|
144
182
|
acquire_journal_write_lock "$dir" || { warn "write: failed to acquire journal lock"; exit 0; }
|
|
145
183
|
trap release_journal_write_lock EXIT
|
|
@@ -161,8 +199,8 @@ case "$cmd" in
|
|
|
161
199
|
{
|
|
162
200
|
printf -- '---\n'
|
|
163
201
|
printf 'ts: %s\n' "$(now_iso_z)"
|
|
164
|
-
printf 'title: %s\n' "$
|
|
165
|
-
printf 'slug: %s\n' "$
|
|
202
|
+
printf 'title: %s\n' "$yaml_title"
|
|
203
|
+
printf 'slug: %s\n' "$yaml_slug"
|
|
166
204
|
if [ -n "$files_csv" ]; then
|
|
167
205
|
printf 'files:\n'
|
|
168
206
|
old_ifs="$IFS"
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* non-interactively.
|
|
12
12
|
*/
|
|
13
13
|
import "../node-preflight.js";
|
|
14
|
+
import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
|
|
14
15
|
import "../node-network-compat.js";
|
|
15
|
-
import { refreshCachedSession } from "../utils/cognito-session.js";
|
|
16
|
+
import type { refreshCachedSession } from "../utils/cognito-session.js";
|
|
16
17
|
export declare function runAuthRefresh(refreshSession?: typeof refreshCachedSession): Promise<number>;
|
|
18
|
+
export declare function startAuthRefresh(acquireLease?: typeof acquireCurrentPackageUseLease): Promise<number>;
|
|
17
19
|
//# sourceMappingURL=hq-auth-refresh.d.ts.map
|
|
@@ -13,15 +13,16 @@
|
|
|
13
13
|
// MUST be first: guard the Node version before any dependency that needs a
|
|
14
14
|
// Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
|
|
15
15
|
import "../node-preflight.js";
|
|
16
|
+
import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
|
|
16
17
|
import "../node-network-compat.js";
|
|
17
18
|
import { fileURLToPath } from "node:url";
|
|
18
19
|
import path from "node:path";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
export async function runAuthRefresh(refreshSession) {
|
|
21
|
+
const { finishSentrySession, Sentry } = await import("../sentry.js");
|
|
22
|
+
const { refreshCachedSession: refresh } = await import("../utils/cognito-session.js");
|
|
22
23
|
let exitCode = 1;
|
|
23
24
|
try {
|
|
24
|
-
const result = await refreshSession();
|
|
25
|
+
const result = await (refreshSession ?? refresh)();
|
|
25
26
|
if (result.refreshed) {
|
|
26
27
|
exitCode = 0;
|
|
27
28
|
}
|
|
@@ -41,8 +42,42 @@ export async function runAuthRefresh(refreshSession = refreshCachedSession) {
|
|
|
41
42
|
}
|
|
42
43
|
return exitCode;
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
function isPackageUseUpdatePendingError(error) {
|
|
46
|
+
return Boolean(error &&
|
|
47
|
+
typeof error === "object" &&
|
|
48
|
+
error.name === "PackageUseUpdatePendingError");
|
|
49
|
+
}
|
|
50
|
+
function packageUseErrorClass(error) {
|
|
51
|
+
if (!error || typeof error !== "object")
|
|
52
|
+
return "unknown";
|
|
53
|
+
const value = error;
|
|
54
|
+
if (typeof value.code === "string")
|
|
55
|
+
return value.code;
|
|
56
|
+
if (value.cause && typeof value.cause === "object") {
|
|
57
|
+
const cause = value.cause;
|
|
58
|
+
if (typeof cause.code === "string")
|
|
59
|
+
return cause.code;
|
|
60
|
+
if (typeof cause.name === "string")
|
|
61
|
+
return cause.name;
|
|
62
|
+
}
|
|
63
|
+
return typeof value.name === "string" ? value.name : "unknown";
|
|
64
|
+
}
|
|
65
|
+
export async function startAuthRefresh(acquireLease = acquireCurrentPackageUseLease) {
|
|
66
|
+
try {
|
|
67
|
+
acquireLease();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (isPackageUseUpdatePendingError(error)) {
|
|
71
|
+
process.stderr.write(`hq-auth-refresh: ${error instanceof Error ? error.message : "The HQ CLI package is being updated"}\n`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
process.stderr.write(`hq-auth-refresh: package-use lease unavailable (${packageUseErrorClass(error)}); continuing without lease\n`);
|
|
75
|
+
}
|
|
76
|
+
const { initSentry } = await import("../sentry.js");
|
|
45
77
|
initSentry();
|
|
46
|
-
|
|
78
|
+
return runAuthRefresh();
|
|
79
|
+
}
|
|
80
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
81
|
+
void startAuthRefresh().then((exitCode) => process.exit(exitCode));
|
|
47
82
|
}
|
|
48
83
|
//# sourceMappingURL=hq-auth-refresh.js.map
|
|
@@ -24,7 +24,35 @@ const counterKey = (value) => {
|
|
|
24
24
|
return key.length > 96 ? key.slice(0, 96) : key;
|
|
25
25
|
};
|
|
26
26
|
const slugify = (value) => value.toLowerCase()
|
|
27
|
-
.
|
|
27
|
+
.split("\n")
|
|
28
|
+
.map((line) => line.replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 40))
|
|
29
|
+
.join("\n");
|
|
30
|
+
function yamlDoubleQuoted(value) {
|
|
31
|
+
let escaped = "";
|
|
32
|
+
for (const character of value) {
|
|
33
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
34
|
+
if (character === "\\")
|
|
35
|
+
escaped += "\\\\";
|
|
36
|
+
else if (character === '"')
|
|
37
|
+
escaped += '\\"';
|
|
38
|
+
else if (character === "\n")
|
|
39
|
+
escaped += "\\n";
|
|
40
|
+
else if (character === "\r")
|
|
41
|
+
escaped += "\\r";
|
|
42
|
+
else if (character === "\t")
|
|
43
|
+
escaped += "\\t";
|
|
44
|
+
else if (codePoint < 0x20 || codePoint === 0x7f) {
|
|
45
|
+
escaped += `\\x${codePoint.toString(16).padStart(2, "0")}`;
|
|
46
|
+
}
|
|
47
|
+
else if (codePoint >= 0x80 && codePoint <= 0x9f) {
|
|
48
|
+
escaped += `\\u${codePoint.toString(16).padStart(4, "0")}`;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
escaped += character;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return `"${escaped}"`;
|
|
55
|
+
}
|
|
28
56
|
const warning = (message) => { process.stderr.write(`session-journal: ${message}\n`); };
|
|
29
57
|
const journalDir = (root, date) => path.join(root, "workspace/threads/journal", date);
|
|
30
58
|
const hasFile = (file) => {
|
|
@@ -211,6 +239,7 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
|
|
|
211
239
|
return 0;
|
|
212
240
|
}
|
|
213
241
|
const slug = slugify(title) || "entry";
|
|
242
|
+
const yamlSlug = slug.includes("\n") ? yamlDoubleQuoted(slug) : slug;
|
|
214
243
|
const body = bodyFile && hasFile(bodyFile)
|
|
215
244
|
? fs.readFileSync(bodyFile, "utf8")
|
|
216
245
|
: "## Goal\n\n## Findings\n\n## Decisions\n\n## Next\n";
|
|
@@ -226,7 +255,7 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
|
|
|
226
255
|
for (let attempt = 0; attempt < 10_000; attempt += 1) {
|
|
227
256
|
seq = nextSequence(dir);
|
|
228
257
|
output = path.join(dir, `${seq}-${slug}.md`);
|
|
229
|
-
const lines = ["---\n", `ts: ${nowIso()}\n`, `title: ${title}\n`, `slug: ${
|
|
258
|
+
const lines = ["---\n", `ts: ${nowIso()}\n`, `title: ${yamlDoubleQuoted(title)}\n`, `slug: ${yamlSlug}\n`];
|
|
230
259
|
if (files) {
|
|
231
260
|
lines.push("files:\n");
|
|
232
261
|
for (const file of files.split(","))
|
package/dist/commands/lanes.js
CHANGED
|
@@ -35,7 +35,7 @@ import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
|
35
35
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
36
36
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
37
37
|
import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
|
|
38
|
-
import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG } from "../lib/lanes/flag-keys.js";
|
|
38
|
+
import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
39
39
|
import { CODEX_ROLLOUT_RETENTION_FLAG, configuredCodexRolloutHomes, lowDiskGuardEnabledForLane, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
|
|
40
40
|
import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
|
|
41
41
|
import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
|
|
@@ -1337,6 +1337,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
|
|
|
1337
1337
|
updated: new Map(),
|
|
1338
1338
|
unverifiedRemote: new Map(),
|
|
1339
1339
|
missingOnOwner: new Map(),
|
|
1340
|
+
skippedRemoteRefresh: new Set(),
|
|
1340
1341
|
};
|
|
1341
1342
|
const now = ctx.now();
|
|
1342
1343
|
const nowMs = now.getTime();
|
|
@@ -1378,6 +1379,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
|
|
|
1378
1379
|
if (candidates.length === 0)
|
|
1379
1380
|
return result;
|
|
1380
1381
|
const enabledByCompany = new Map();
|
|
1382
|
+
const skipRefreshByCompany = new Map();
|
|
1381
1383
|
const enabledCandidates = [];
|
|
1382
1384
|
for (const lane of candidates) {
|
|
1383
1385
|
const company = resolveLaneCompany(hqRoot, lane).company;
|
|
@@ -1392,8 +1394,20 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
|
|
|
1392
1394
|
});
|
|
1393
1395
|
enabledByCompany.set(companyKey, enabled);
|
|
1394
1396
|
}
|
|
1395
|
-
if (enabled)
|
|
1396
|
-
|
|
1397
|
+
if (!enabled)
|
|
1398
|
+
continue;
|
|
1399
|
+
if (options.skipRemoteRefreshForCompany) {
|
|
1400
|
+
let skipRefresh = skipRefreshByCompany.get(companyKey);
|
|
1401
|
+
if (skipRefresh === undefined) {
|
|
1402
|
+
skipRefresh = await options.skipRemoteRefreshForCompany(company);
|
|
1403
|
+
skipRefreshByCompany.set(companyKey, skipRefresh);
|
|
1404
|
+
}
|
|
1405
|
+
if (skipRefresh) {
|
|
1406
|
+
result.skippedRemoteRefresh.add(lane.lane_id);
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
enabledCandidates.push(lane);
|
|
1397
1411
|
}
|
|
1398
1412
|
if (enabledCandidates.length === 0)
|
|
1399
1413
|
return result;
|
|
@@ -4950,7 +4964,12 @@ async function runList(ctx, opts) {
|
|
|
4950
4964
|
}
|
|
4951
4965
|
const remoteRead = ctx.env[REMOTE_LANE_READ_ENV] === "1";
|
|
4952
4966
|
const remoteRefresh = remoteRead
|
|
4953
|
-
? {
|
|
4967
|
+
? {
|
|
4968
|
+
updated: new Map(),
|
|
4969
|
+
unverifiedRemote: new Map(),
|
|
4970
|
+
missingOnOwner: new Map(),
|
|
4971
|
+
skippedRemoteRefresh: new Set(),
|
|
4972
|
+
}
|
|
4954
4973
|
: await refreshRemoteLaneMirrors(ctx, hqRoot, laneSource, new Set(laneSource
|
|
4955
4974
|
.filter((lane) => matchesLaneFilters(lane, false))
|
|
4956
4975
|
.map((lane) => lane.lane_id)), {
|
|
@@ -4958,6 +4977,7 @@ async function runList(ctx, opts) {
|
|
|
4958
4977
|
laneIds: new Set(laneSource
|
|
4959
4978
|
.filter((lane) => matchesLaneFilters(lane, false))
|
|
4960
4979
|
.map((lane) => lane.lane_id)),
|
|
4980
|
+
skipRemoteRefreshForCompany: (company) => readLanesRegistryFlagFailOff(LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, { company, env: ctx.env }),
|
|
4961
4981
|
});
|
|
4962
4982
|
for (const [laneId, updated] of remoteRefresh.updated) {
|
|
4963
4983
|
const index = laneSource.findIndex((lane) => lane.lane_id === laneId);
|
|
@@ -4995,6 +5015,7 @@ async function runList(ctx, opts) {
|
|
|
4995
5015
|
const ownerEligibleLaneIds = new Set();
|
|
4996
5016
|
for (const lane of selectedLanes) {
|
|
4997
5017
|
if (remoteRead ||
|
|
5018
|
+
remoteRefresh.skippedRemoteRefresh.has(lane.lane_id) ||
|
|
4998
5019
|
lane.host.kind !== "outpost" ||
|
|
4999
5020
|
isTerminalLaneState(lane.state) ||
|
|
5000
5021
|
laneRunsHere(lane, host)) {
|
|
@@ -5034,6 +5055,7 @@ async function runList(ctx, opts) {
|
|
|
5034
5055
|
const livenessBySession = new Map();
|
|
5035
5056
|
let livenessDeps;
|
|
5036
5057
|
const lanes = selectedLanes.map((lane) => {
|
|
5058
|
+
const overdue = Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, now);
|
|
5037
5059
|
const remoteQueued = lane.state === "queued" &&
|
|
5038
5060
|
lane.host.kind === "outpost" &&
|
|
5039
5061
|
!laneRunsHere(lane, host);
|
|
@@ -5052,7 +5074,7 @@ async function runList(ctx, opts) {
|
|
|
5052
5074
|
queued_details: lane.queued_reason === "low_disk" ? lane.queued_details : undefined,
|
|
5053
5075
|
}
|
|
5054
5076
|
: {}),
|
|
5055
|
-
overdue
|
|
5077
|
+
overdue,
|
|
5056
5078
|
stalled: Boolean(lane.stalled),
|
|
5057
5079
|
// Listing is observational. Reconcile owns durable liveness transitions.
|
|
5058
5080
|
senior_lost: lane.state === "running" && lane.senior.kind === "session" && lane.senior.id
|
|
@@ -7873,6 +7895,7 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
7873
7895
|
updated: new Map(),
|
|
7874
7896
|
unverifiedRemote: new Map(),
|
|
7875
7897
|
missingOnOwner: new Map(),
|
|
7898
|
+
skippedRemoteRefresh: new Set(),
|
|
7876
7899
|
}
|
|
7877
7900
|
: await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot));
|
|
7878
7901
|
const remoteLost = [];
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,17 @@ import "./node-preflight.js";
|
|
|
3
3
|
import "./node-network-compat-fast.js";
|
|
4
4
|
import { isVersionRequest } from "./version-request.js";
|
|
5
5
|
import { isFastCoreRequest } from "./fast-scaffold.js";
|
|
6
|
+
type PackageUseLeaseLoader = () => Promise<void>;
|
|
7
|
+
declare function isNpmPackageEntry(entryUrl: string): boolean;
|
|
8
|
+
declare function dispatchWithPackageUseLease(entryUrl: string, dispatch: () => void, loadLease?: PackageUseLeaseLoader): void | Promise<void>;
|
|
9
|
+
declare function runCliEntry(entryUrl: string, dispatch: () => void, loadLease?: PackageUseLeaseLoader): void | Promise<void>;
|
|
6
10
|
export declare const __test__: {
|
|
7
11
|
isVersionRequest: typeof isVersionRequest;
|
|
8
12
|
isFastCoreRequest: typeof isFastCoreRequest;
|
|
13
|
+
dispatchWithPackageUseLease: typeof dispatchWithPackageUseLease;
|
|
14
|
+
runCliEntry: typeof runCliEntry;
|
|
15
|
+
isNpmPackageEntry: typeof isNpmPackageEntry;
|
|
16
|
+
acquirePackageUseLease: (prefix: string, version: string, stateDirectory?: string) => Promise<import("./utils/package-use-lease.js").PackageUseLease>;
|
|
9
17
|
};
|
|
18
|
+
export {};
|
|
10
19
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -17,77 +17,149 @@ import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.
|
|
|
17
17
|
// command never run (HQ-CLI-W). Sets process.exitCode only, never process.exit,
|
|
18
18
|
// so runCli's finally still finalizes release health and flushes Sentry.
|
|
19
19
|
installProcessRejectionBoundary();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
function dispatchCliEntry() {
|
|
21
|
+
if (isVersionRequest(process.argv)) {
|
|
22
|
+
// Bare `--version` is the CLI semver only (the contract field updaters
|
|
23
|
+
// compare). `hq version` and `--json` print the labelled HQ/CLI report.
|
|
24
|
+
// Lazy so this path still skips the command graph.
|
|
25
|
+
if (wantsLabelledVersionReport(process.argv)) {
|
|
26
|
+
void import("./lib/flag-registry.js").then(({ kickFlagRegistryReadiness }) => {
|
|
27
|
+
kickFlagRegistryReadiness();
|
|
28
|
+
return import("./utils/version-report.js").then(({ runVersionCommand }) => runVersionCommand(process.argv));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Keep the benchmarked updater fast route limited to the installed semver.
|
|
33
|
+
// Registry checks and flag resolution belong to the regular command path.
|
|
34
|
+
void import("./cli-version.js").then(({ CLI_VERSION }) => {
|
|
35
|
+
process.stdout.write(`${CLI_VERSION}\n`);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (parseFastCore(process.argv)?.entry.kind === "native" &&
|
|
40
|
+
(process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "true" ||
|
|
41
|
+
process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "1")) {
|
|
42
|
+
// An explicit enabled override has already decided the global gate. Skip the
|
|
43
|
+
// snapshot reader import on hot native plumbing paths; runFastCore still
|
|
44
|
+
// checks the command-family flag before choosing native or shell rollback.
|
|
45
|
+
void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
|
|
46
|
+
}
|
|
47
|
+
else if (parseFastCore(process.argv)?.entry.kind === "native") {
|
|
48
|
+
// Native routes are default-off. Resolve the company-scoped persisted flag
|
|
49
|
+
// without loading the command graph; a miss or stale snapshot takes the
|
|
50
|
+
// existing full-graph route.
|
|
51
|
+
void import("./native-fast-route.js").then(async ({ nativeFastRouteEnabled }) => {
|
|
52
|
+
if (await nativeFastRouteEnabled(process.argv)) {
|
|
53
|
+
const { runFastCore } = await import("./fast-core.js");
|
|
54
|
+
await runFastCore(process.argv);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const { runCli } = await import("./main.js");
|
|
58
|
+
await runCli();
|
|
59
|
+
}
|
|
28
60
|
});
|
|
29
61
|
}
|
|
30
|
-
else {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
62
|
+
else if (parseSecretsEnvFastRequest(process.argv)) {
|
|
63
|
+
const request = parseSecretsEnvFastRequest(process.argv);
|
|
64
|
+
void import("./secrets-env-fast-route.js").then(async (route) => {
|
|
65
|
+
await route.dispatchSecretsEnvFastRequest(request, {
|
|
66
|
+
isEnabled: () => route.secretsEnvFastRouteEnabled(request),
|
|
67
|
+
runFast: async () => {
|
|
68
|
+
const { runSecretsEnvFastPath } = await import("./secrets-env-fast-path.js");
|
|
69
|
+
return runSecretsEnvFastPath(request);
|
|
70
|
+
},
|
|
71
|
+
runCurrent: async () => {
|
|
72
|
+
const { runCli } = await import("./main.js");
|
|
73
|
+
await runCli();
|
|
74
|
+
},
|
|
75
|
+
});
|
|
35
76
|
});
|
|
36
77
|
}
|
|
78
|
+
else if (isFastCoreRequest(process.argv)) {
|
|
79
|
+
// Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
|
|
80
|
+
// dispatches straight to the bundled-script runner, so it never pays for the
|
|
81
|
+
// ~60-module command graph main.ts imports. See src/fast-scaffold.ts.
|
|
82
|
+
void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Keep the command lifecycle detached from module evaluation, as it was
|
|
86
|
+
// before the fast --version split. Some best-effort teardown work uses
|
|
87
|
+
// unref'd handles; top-level-awaiting runCli() makes Node turn an otherwise
|
|
88
|
+
// successful command into exit 13 when that teardown promise remains pending.
|
|
89
|
+
// A floated rejection here no longer exits 0 silently: the boundary installed
|
|
90
|
+
// above turns it into a non-zero exit (HQ-CLI-W).
|
|
91
|
+
void import("./main.js").then(({ runCli }) => runCli());
|
|
92
|
+
}
|
|
37
93
|
}
|
|
38
|
-
|
|
39
|
-
(
|
|
40
|
-
process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "1")) {
|
|
41
|
-
// An explicit enabled override has already decided the global gate. Skip the
|
|
42
|
-
// snapshot reader import on hot native plumbing paths; runFastCore still
|
|
43
|
-
// checks the command-family flag before choosing native or shell rollback.
|
|
44
|
-
void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
|
|
94
|
+
function isNpmPackageEntry(entryUrl) {
|
|
95
|
+
return /\/node_modules\/@indigoai-us\/hq-cli\/dist\/index\.js$/i.test(entryUrl);
|
|
45
96
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
void import("./native-fast-route.js").then(async ({ nativeFastRouteEnabled }) => {
|
|
51
|
-
if (await nativeFastRouteEnabled(process.argv)) {
|
|
52
|
-
const { runFastCore } = await import("./fast-core.js");
|
|
53
|
-
await runFastCore(process.argv);
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
const { runCli } = await import("./main.js");
|
|
57
|
-
await runCli();
|
|
58
|
-
}
|
|
59
|
-
});
|
|
97
|
+
function isPackageUseUpdatePendingError(error) {
|
|
98
|
+
return Boolean(error &&
|
|
99
|
+
typeof error === "object" &&
|
|
100
|
+
error.name === "PackageUseUpdatePendingError");
|
|
60
101
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
102
|
+
function packageUseErrorClass(error) {
|
|
103
|
+
if (!error || typeof error !== "object")
|
|
104
|
+
return "unknown";
|
|
105
|
+
const value = error;
|
|
106
|
+
if (typeof value.code === "string")
|
|
107
|
+
return value.code;
|
|
108
|
+
if (value.cause && typeof value.cause === "object") {
|
|
109
|
+
const cause = value.cause;
|
|
110
|
+
if (typeof cause.code === "string")
|
|
111
|
+
return cause.code;
|
|
112
|
+
if (typeof cause.name === "string")
|
|
113
|
+
return cause.name;
|
|
114
|
+
}
|
|
115
|
+
return typeof value.name === "string" ? value.name : "unknown";
|
|
76
116
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
117
|
+
function dispatchWithPackageUseLease(entryUrl, dispatch, loadLease = async () => {
|
|
118
|
+
const { acquireCurrentPackageUseLease } = await import("./utils/package-use-lease.js");
|
|
119
|
+
acquireCurrentPackageUseLease();
|
|
120
|
+
}) {
|
|
121
|
+
if (!isNpmPackageEntry(entryUrl)) {
|
|
122
|
+
dispatch();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
return loadLease().then(dispatch, (error) => {
|
|
126
|
+
if (isPackageUseUpdatePendingError(error))
|
|
127
|
+
throw error;
|
|
128
|
+
process.stderr.write(`hq-cli package-use lease unavailable (${packageUseErrorClass(error)}); continuing without lease\n`);
|
|
129
|
+
dispatch();
|
|
130
|
+
});
|
|
82
131
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
132
|
+
function runCliEntry(entryUrl, dispatch, loadLease) {
|
|
133
|
+
const handlePendingUpdate = (error) => {
|
|
134
|
+
if (!isPackageUseUpdatePendingError(error))
|
|
135
|
+
throw error;
|
|
136
|
+
const message = error instanceof Error
|
|
137
|
+
? error.message
|
|
138
|
+
: "The HQ CLI package is being updated";
|
|
139
|
+
process.stderr.write(`hq-cli: ${message}\n`);
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
};
|
|
142
|
+
try {
|
|
143
|
+
const result = dispatchWithPackageUseLease(entryUrl, dispatch, loadLease);
|
|
144
|
+
return result?.catch(handlePendingUpdate);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
handlePendingUpdate(error);
|
|
148
|
+
}
|
|
91
149
|
}
|
|
92
|
-
|
|
150
|
+
// Local builds and benchmark fixtures are not installed in an npm prefix.
|
|
151
|
+
// Keep their fast route free of package-use filesystem work; npm installs still
|
|
152
|
+
// publish a lease before dispatch.
|
|
153
|
+
void runCliEntry(import.meta.url, dispatchCliEntry);
|
|
154
|
+
export const __test__ = {
|
|
155
|
+
isVersionRequest,
|
|
156
|
+
isFastCoreRequest,
|
|
157
|
+
dispatchWithPackageUseLease,
|
|
158
|
+
runCliEntry,
|
|
159
|
+
isNpmPackageEntry,
|
|
160
|
+
acquirePackageUseLease: async (prefix, version, stateDirectory) => {
|
|
161
|
+
const lease = await import("./utils/package-use-lease.js");
|
|
162
|
+
return lease.acquirePackageUseLease(prefix, version, stateDirectory);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
93
165
|
//# sourceMappingURL=index.js.map
|
package/dist/lib/daemon/run.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { type HostApp } from "./supervisor.js";
|
|
|
12
12
|
export declare const LOG_ROTATE_BYTES: number;
|
|
13
13
|
/** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
|
|
14
14
|
export declare const RESTART_EXIT_CODE = 75;
|
|
15
|
+
/** Route an updater request through the daemon's existing graceful handoff. */
|
|
16
|
+
export declare function handoffForPackageUpdateRequest(requested: boolean, restartDaemon: () => void): void;
|
|
15
17
|
export interface RunDaemonOptions {
|
|
16
18
|
foreground?: boolean;
|
|
17
19
|
/** Started by the OS unit, which relaunches the daemon whenever it exits. */
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -12,6 +12,7 @@ import * as os from "node:os";
|
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
14
14
|
import { CLI_VERSION } from "../../cli-version.js";
|
|
15
|
+
import { currentPackageUseUpdateRequested } from "../../utils/package-use-lease.js";
|
|
15
16
|
import { isLocalDependencyInstall, resolveRunningInstall } from "../../utils/version-gate.js";
|
|
16
17
|
import { agentKitPaths } from "../agent-kit/paths.js";
|
|
17
18
|
import { readBotConfig } from "../bot/config.js";
|
|
@@ -39,6 +40,11 @@ import { userToolDirs } from "./tools.js";
|
|
|
39
40
|
export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
|
|
40
41
|
/** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
|
|
41
42
|
export const RESTART_EXIT_CODE = 75;
|
|
43
|
+
/** Route an updater request through the daemon's existing graceful handoff. */
|
|
44
|
+
export function handoffForPackageUpdateRequest(requested, restartDaemon) {
|
|
45
|
+
if (requested)
|
|
46
|
+
restartDaemon();
|
|
47
|
+
}
|
|
42
48
|
const LEGACY_PROBE_TTL_MS = 60_000;
|
|
43
49
|
/** A flag read that has not answered by then counts as off. */
|
|
44
50
|
const HANDOFF_FLAG_TIMEOUT_MS = 10_000;
|
|
@@ -603,7 +609,10 @@ export async function runDaemon(opts) {
|
|
|
603
609
|
log("info", `rotated ${n} log file(s)`);
|
|
604
610
|
return true;
|
|
605
611
|
},
|
|
606
|
-
takeRequests: () =>
|
|
612
|
+
takeRequests: () => {
|
|
613
|
+
handoffForPackageUpdateRequest(currentPackageUseUpdateRequested(), restartDaemon);
|
|
614
|
+
return takeControlRequests(paths.requestsDir);
|
|
615
|
+
},
|
|
607
616
|
restartDaemon: () => restartDaemon(),
|
|
608
617
|
writeState: (state) => writeJsonAtomic(paths.statePath, state),
|
|
609
618
|
log,
|
|
@@ -4,6 +4,9 @@ export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-bala
|
|
|
4
4
|
export declare const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
|
|
5
5
|
/** Count only children with live work against their senior's hire grant. */
|
|
6
6
|
export declare const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-count-live-only";
|
|
7
|
+
/** Skips owner refreshes during ordinary lane list polling when explicitly enabled. */
|
|
8
|
+
export declare const LANE_LIST_SKIP_REMOTE_REFRESH_FLAG = "lanes.list-skip-remote-refresh";
|
|
9
|
+
export declare const LANE_LIST_SKIP_REMOTE_REFRESH_DEFAULT_ENABLED = false;
|
|
7
10
|
/** Enables missed-deadline lead nudges and second-miss operator escalation. */
|
|
8
11
|
export declare const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
|
|
9
12
|
export declare const LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG = "lanes.autopilot-nudge-dedupe";
|
|
@@ -4,6 +4,9 @@ export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
|
4
4
|
export const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
|
|
5
5
|
/** Count only children with live work against their senior's hire grant. */
|
|
6
6
|
export const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-count-live-only";
|
|
7
|
+
/** Skips owner refreshes during ordinary lane list polling when explicitly enabled. */
|
|
8
|
+
export const LANE_LIST_SKIP_REMOTE_REFRESH_FLAG = "lanes.list-skip-remote-refresh";
|
|
9
|
+
export const LANE_LIST_SKIP_REMOTE_REFRESH_DEFAULT_ENABLED = false;
|
|
7
10
|
/** Enables missed-deadline lead nudges and second-miss operator escalation. */
|
|
8
11
|
export const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
|
|
9
12
|
export const LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG = "lanes.autopilot-nudge-dedupe";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class PackageUseUpdatePendingError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export interface PackageUseLease {
|
|
5
|
+
readonly leasePath: string;
|
|
6
|
+
release(): void;
|
|
7
|
+
}
|
|
8
|
+
export interface PackageUseLeaseRecord {
|
|
9
|
+
pid: number;
|
|
10
|
+
start_time_ms: number;
|
|
11
|
+
hq_version: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function npmPrefix(root: string, platform?: NodeJS.Platform): string | null;
|
|
14
|
+
export declare function packageUseLeaseDirectory(prefix: string, stateDirectory?: string): string;
|
|
15
|
+
export declare function packageUseUpdateRequestPath(prefix: string, stateDirectory?: string): string;
|
|
16
|
+
declare function waitForUpdateRequest(requestPath: string, timeoutMs: number): void;
|
|
17
|
+
export declare function acquirePackageUseLease(prefix: string, hqVersion: string, stateDirectory?: string): PackageUseLease;
|
|
18
|
+
export declare function acquireCurrentPackageUseLease(): PackageUseLease | null;
|
|
19
|
+
/** True while the desktop updater owns the per-prefix update request. */
|
|
20
|
+
export declare function currentPackageUseUpdateRequested(): boolean;
|
|
21
|
+
export declare const __test__: {
|
|
22
|
+
waitForUpdateRequest: typeof waitForUpdateRequest;
|
|
23
|
+
};
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=package-use-lease.d.ts.map
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { performance } from "node:perf_hooks";
|
|
7
|
+
const CLI_NAME = "@indigoai-us/hq-cli";
|
|
8
|
+
const UPDATE_REQUEST = "update.pending.json";
|
|
9
|
+
export class PackageUseUpdatePendingError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "PackageUseUpdatePendingError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function errorLabel(error) {
|
|
16
|
+
if (error && typeof error === "object") {
|
|
17
|
+
const value = error;
|
|
18
|
+
if (typeof value.code === "string")
|
|
19
|
+
return value.code;
|
|
20
|
+
if (typeof value.name === "string")
|
|
21
|
+
return value.name;
|
|
22
|
+
}
|
|
23
|
+
return "unknown error";
|
|
24
|
+
}
|
|
25
|
+
function readPackage(directory) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(path.join(directory, "package.json"), "utf8"));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const code = error.code;
|
|
31
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
32
|
+
return null;
|
|
33
|
+
throw new Error(`Unable to inspect the HQ CLI install (${errorLabel(error)})`, { cause: error });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function findCliPackage(directory) {
|
|
37
|
+
while (true) {
|
|
38
|
+
const pkg = readPackage(directory);
|
|
39
|
+
if (pkg?.name === CLI_NAME) {
|
|
40
|
+
return { root: directory, version: typeof pkg.version === "string" ? pkg.version : "unknown" };
|
|
41
|
+
}
|
|
42
|
+
const parent = path.dirname(directory);
|
|
43
|
+
if (parent === directory)
|
|
44
|
+
return null;
|
|
45
|
+
directory = parent;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function isPnpmGlobal(root) {
|
|
49
|
+
const normalized = root.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
50
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
51
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
52
|
+
if (parts[i] === "global" && (/^\d+$/.test(parts[i + 1]) || /^v\d+$/.test(parts[i + 1])))
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
const home = process.env.PNPM_HOME?.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
56
|
+
const globalRoot = home ? `${home}/global` : null;
|
|
57
|
+
const left = process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
58
|
+
const right = globalRoot && process.platform === "win32" ? globalRoot.toLowerCase() : globalRoot;
|
|
59
|
+
return Boolean(right && (left === right || left.startsWith(`${right}/`)));
|
|
60
|
+
}
|
|
61
|
+
function isBunGlobal(root) {
|
|
62
|
+
const parts = root.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
63
|
+
return parts.some((part, i) => part === "install" && parts[i + 1] === "global" && parts[i + 2] === "node_modules");
|
|
64
|
+
}
|
|
65
|
+
export function npmPrefix(root, platform = process.platform) {
|
|
66
|
+
const normalized = root.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
67
|
+
const parts = normalized.split("/");
|
|
68
|
+
const index = parts.lastIndexOf("node_modules");
|
|
69
|
+
if (index < 0)
|
|
70
|
+
return null;
|
|
71
|
+
const end = parts[index - 1] === "lib" ? index - 1 : index;
|
|
72
|
+
const prefix = parts.slice(0, end).join("/");
|
|
73
|
+
if (platform === "win32" && /^[a-z]:$/i.test(prefix))
|
|
74
|
+
return `${prefix}/`;
|
|
75
|
+
if (!prefix && normalized.startsWith("/"))
|
|
76
|
+
return "/";
|
|
77
|
+
return prefix || null;
|
|
78
|
+
}
|
|
79
|
+
function packageUseStateDirectory() {
|
|
80
|
+
if (process.platform === "win32") {
|
|
81
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
82
|
+
if (!localAppData)
|
|
83
|
+
throw new Error("LOCALAPPDATA is unavailable for the HQ CLI package-use lease");
|
|
84
|
+
return path.join(localAppData, "hq-cli", "state", "package-use");
|
|
85
|
+
}
|
|
86
|
+
return path.join(process.env.XDG_STATE_HOME?.trim() || path.join(os.homedir(), ".local", "state"), "hq-cli", "package-use");
|
|
87
|
+
}
|
|
88
|
+
function normalizedPrefix(prefix) {
|
|
89
|
+
let canonical = realpathSync.native(prefix).replace(/\\/g, "/");
|
|
90
|
+
canonical = canonical.replace(/^\\\\\?\\UNC\\/i, "//").replace(/^\\\\\?\\/, "");
|
|
91
|
+
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
92
|
+
}
|
|
93
|
+
export function packageUseLeaseDirectory(prefix, stateDirectory = packageUseStateDirectory()) {
|
|
94
|
+
const digest = createHash("sha256").update(normalizedPrefix(prefix), "utf8").digest("hex");
|
|
95
|
+
return path.join(stateDirectory, digest);
|
|
96
|
+
}
|
|
97
|
+
export function packageUseUpdateRequestPath(prefix, stateDirectory) {
|
|
98
|
+
return path.join(packageUseLeaseDirectory(prefix, stateDirectory), UPDATE_REQUEST);
|
|
99
|
+
}
|
|
100
|
+
function waitForUpdateRequest(requestPath, timeoutMs) {
|
|
101
|
+
const started = Date.now();
|
|
102
|
+
const sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
103
|
+
while (true) {
|
|
104
|
+
try {
|
|
105
|
+
const request = JSON.parse(readFileSync(requestPath, "utf8"));
|
|
106
|
+
const pid = request.pid;
|
|
107
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
|
|
108
|
+
throw new Error("invalid update request");
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
process.kill(pid, 0);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code === "ESRCH") {
|
|
115
|
+
rmSync(requestPath, { force: true });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error.code === "ENOENT")
|
|
122
|
+
return;
|
|
123
|
+
throw new Error(`Unable to read the HQ CLI update request (${errorLabel(error)})`, { cause: error });
|
|
124
|
+
}
|
|
125
|
+
if (Date.now() - started >= timeoutMs) {
|
|
126
|
+
throw new PackageUseUpdatePendingError("The HQ CLI package is being updated; retry after the update finishes");
|
|
127
|
+
}
|
|
128
|
+
Atomics.wait(sleeper, 0, 0, 100);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function acquirePackageUseLease(prefix, hqVersion, stateDirectory) {
|
|
132
|
+
const directory = packageUseLeaseDirectory(prefix, stateDirectory);
|
|
133
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
134
|
+
const requestPath = path.join(directory, UPDATE_REQUEST);
|
|
135
|
+
const record = {
|
|
136
|
+
pid: process.pid,
|
|
137
|
+
start_time_ms: Math.floor(performance.timeOrigin),
|
|
138
|
+
hq_version: hqVersion,
|
|
139
|
+
};
|
|
140
|
+
const leasePath = path.join(directory, `${record.pid}-${record.start_time_ms}.json`);
|
|
141
|
+
const tempPath = `${leasePath}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
142
|
+
let released = false;
|
|
143
|
+
const release = () => {
|
|
144
|
+
if (released)
|
|
145
|
+
return;
|
|
146
|
+
released = true;
|
|
147
|
+
process.removeListener("exit", releaseOnExit);
|
|
148
|
+
try {
|
|
149
|
+
rmSync(leasePath, { force: true });
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
process.stderr.write(`hq-cli package-use lease release failed (${errorLabel(error)})\n`);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const releaseOnExit = () => release();
|
|
156
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
157
|
+
waitForUpdateRequest(requestPath, 30_000);
|
|
158
|
+
try {
|
|
159
|
+
writeFileSync(tempPath, `${JSON.stringify(record)}\n`, { flag: "wx", mode: 0o600 });
|
|
160
|
+
renameSync(tempPath, leasePath);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
rmSync(tempPath, { force: true });
|
|
164
|
+
throw new Error(`Unable to publish the HQ CLI package-use lease (${errorLabel(error)})`, { cause: error });
|
|
165
|
+
}
|
|
166
|
+
if (!exists(requestPath)) {
|
|
167
|
+
process.once("exit", releaseOnExit);
|
|
168
|
+
return { leasePath, release };
|
|
169
|
+
}
|
|
170
|
+
rmSync(leasePath, { force: true });
|
|
171
|
+
}
|
|
172
|
+
throw new PackageUseUpdatePendingError("An HQ CLI update is waiting for the package lease; retry when it finishes");
|
|
173
|
+
}
|
|
174
|
+
function exists(file) {
|
|
175
|
+
try {
|
|
176
|
+
readFileSync(file);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error.code === "ENOENT")
|
|
181
|
+
return false;
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export function acquireCurrentPackageUseLease() {
|
|
186
|
+
const pkg = findCliPackage(path.dirname(fileURLToPath(import.meta.url)));
|
|
187
|
+
if (!pkg || isPnpmGlobal(pkg.root) || isBunGlobal(pkg.root))
|
|
188
|
+
return null;
|
|
189
|
+
const prefix = npmPrefix(pkg.root);
|
|
190
|
+
if (!prefix)
|
|
191
|
+
return null;
|
|
192
|
+
if (process.platform !== "win32") {
|
|
193
|
+
const parts = pkg.root.replace(/\\/g, "/").replace(/\/+$/, "").split("/").filter(Boolean);
|
|
194
|
+
const nodeModules = parts.lastIndexOf("node_modules");
|
|
195
|
+
if (nodeModules > 0 && parts[nodeModules - 1] !== "lib")
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return acquirePackageUseLease(prefix, pkg.version);
|
|
199
|
+
}
|
|
200
|
+
/** True while the desktop updater owns the per-prefix update request. */
|
|
201
|
+
export function currentPackageUseUpdateRequested() {
|
|
202
|
+
const pkg = findCliPackage(path.dirname(fileURLToPath(import.meta.url)));
|
|
203
|
+
if (!pkg || isPnpmGlobal(pkg.root) || isBunGlobal(pkg.root))
|
|
204
|
+
return false;
|
|
205
|
+
const prefix = npmPrefix(pkg.root);
|
|
206
|
+
if (!prefix)
|
|
207
|
+
return false;
|
|
208
|
+
return exists(packageUseUpdateRequestPath(prefix));
|
|
209
|
+
}
|
|
210
|
+
export const __test__ = {
|
|
211
|
+
waitForUpdateRequest,
|
|
212
|
+
};
|
|
213
|
+
//# sourceMappingURL=package-use-lease.js.map
|