@indigoai-us/hq-cli 5.318.1-beta.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 +10 -2
- package/assets/scaffold/core/scripts/repo-run-registry.sh +61 -17
- 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/command-catalog.generated.d.ts +22 -0
- package/dist/command-catalog.generated.js +29 -0
- package/dist/commands/cloud.js +0 -1
- package/dist/commands/core-hook-state-helpers-native.d.ts +1 -1
- package/dist/commands/core-hook-state-helpers-native.js +36 -7
- package/dist/commands/core-repo-run-registry-native.d.ts +1 -0
- package/dist/commands/core-repo-run-registry-native.js +10 -2
- package/dist/commands/core-rrr-worktree-scope-fix-flag.d.ts +4 -0
- package/dist/commands/core-rrr-worktree-scope-fix-flag.js +7 -0
- package/dist/commands/core-session-journal-native.js +31 -2
- package/dist/commands/lanes-checks.js +12 -0
- package/dist/commands/lanes-dropbox-repair.d.ts +14 -0
- package/dist/commands/lanes-dropbox-repair.js +33 -0
- package/dist/commands/lanes.js +44 -5
- package/dist/fast-core.js +6 -1
- 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/autopilot.js +9 -2
- package/dist/lib/lanes/flag-keys.d.ts +9 -0
- package/dist/lib/lanes/flag-keys.js +9 -0
- package/dist/lib/lanes/keep-alive-audit.d.ts +1 -1
- package/dist/lib/lanes/keep-alive-audit.js +69 -4
- package/dist/lib/lanes/orphaned-dropbox.d.ts +24 -0
- package/dist/lib/lanes/orphaned-dropbox.js +213 -0
- package/dist/lib/lanes/tracker-checks.d.ts +1 -1
- package/dist/utils/package-use-lease.d.ts +25 -0
- package/dist/utils/package-use-lease.js +213 -0
- package/package.json +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LaneCompanyRef } from "./types.js";
|
|
2
|
+
export interface OrphanedBareIdDropbox {
|
|
3
|
+
ulid: string;
|
|
4
|
+
canonical_lane_ids: string[];
|
|
5
|
+
message_count: number;
|
|
6
|
+
newest_message_at: string | null;
|
|
7
|
+
source_dir: string;
|
|
8
|
+
}
|
|
9
|
+
/** Finds bare-ULID run folders that have no bare lane record but do have a canonical lane. */
|
|
10
|
+
export declare function findOrphanedBareIdDropboxes(hqRoot: string, company?: LaneCompanyRef): OrphanedBareIdDropbox[];
|
|
11
|
+
export interface RepairOrphanedDropboxResult {
|
|
12
|
+
ulid: string;
|
|
13
|
+
canonical_lane_id: string;
|
|
14
|
+
message_count: number;
|
|
15
|
+
newest_message_at: string | null;
|
|
16
|
+
applied: boolean;
|
|
17
|
+
archived_dir?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function repairOrphanedBareIdDropbox(hqRoot: string, ulidInput: string, options: {
|
|
20
|
+
apply: boolean;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
now: Date;
|
|
23
|
+
}): RepairOrphanedDropboxResult;
|
|
24
|
+
//# sourceMappingURL=orphaned-dropbox.d.ts.map
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { LanesError } from "./errors.js";
|
|
4
|
+
import { LANES_RUNS_REL } from "./spawn.js";
|
|
5
|
+
import { readAllLanes, tryReadLane } from "./store.js";
|
|
6
|
+
import { resolveLaneCompany } from "./lane-company.js";
|
|
7
|
+
const ULID_PATTERN = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
|
|
8
|
+
function fail(error, message, details = {}) {
|
|
9
|
+
throw new LanesError(error, message, details);
|
|
10
|
+
}
|
|
11
|
+
function laneUlid(laneId) {
|
|
12
|
+
const prefix = laneId.slice(0, 26);
|
|
13
|
+
if (!ULID_PATTERN.test(prefix))
|
|
14
|
+
return null;
|
|
15
|
+
return laneId.length === 26 || laneId[26] === "_" ? prefix.toUpperCase() : null;
|
|
16
|
+
}
|
|
17
|
+
function checkedInbox(sourceDir) {
|
|
18
|
+
const dropboxDir = path.join(sourceDir, "dropbox");
|
|
19
|
+
const inbox = path.join(dropboxDir, "inbox.jsonl");
|
|
20
|
+
try {
|
|
21
|
+
const dropboxStat = fs.lstatSync(dropboxDir);
|
|
22
|
+
if (!dropboxStat.isDirectory() || dropboxStat.isSymbolicLink()) {
|
|
23
|
+
return fail("orphaned_dropbox_invalid", "Orphaned dropbox path must be a regular directory", { path: dropboxDir });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === "ENOENT")
|
|
28
|
+
return { inbox, messages: [] };
|
|
29
|
+
if (error instanceof LanesError)
|
|
30
|
+
throw error;
|
|
31
|
+
return fail("orphaned_dropbox_unreadable", `Could not inspect orphaned dropbox: ${error instanceof Error ? error.message : String(error)}`, { path: dropboxDir });
|
|
32
|
+
}
|
|
33
|
+
let stat;
|
|
34
|
+
try {
|
|
35
|
+
stat = fs.lstatSync(inbox);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code === "ENOENT")
|
|
39
|
+
return { inbox, messages: [] };
|
|
40
|
+
return fail("orphaned_dropbox_unreadable", `Could not inspect orphaned inbox: ${error instanceof Error ? error.message : String(error)}`, { path: inbox });
|
|
41
|
+
}
|
|
42
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
43
|
+
return fail("orphaned_dropbox_invalid", "Orphaned inbox must be a regular file", { path: inbox });
|
|
44
|
+
}
|
|
45
|
+
let text;
|
|
46
|
+
try {
|
|
47
|
+
text = fs.readFileSync(inbox, "utf8");
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return fail("orphaned_dropbox_unreadable", `Could not read orphaned inbox: ${error instanceof Error ? error.message : String(error)}`, { path: inbox });
|
|
51
|
+
}
|
|
52
|
+
const messages = [];
|
|
53
|
+
for (const [index, line] of text.split(/\r?\n/).entries()) {
|
|
54
|
+
if (line.trim() === "")
|
|
55
|
+
continue;
|
|
56
|
+
let value;
|
|
57
|
+
try {
|
|
58
|
+
value = JSON.parse(line);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return fail("orphaned_dropbox_message_invalid", "Orphaned inbox contains malformed JSONL", { path: inbox, line: index + 1 });
|
|
62
|
+
}
|
|
63
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || typeof value.text !== "string") {
|
|
64
|
+
return fail("orphaned_dropbox_message_invalid", "Orphaned inbox contains an invalid dropbox message", { path: inbox, line: index + 1 });
|
|
65
|
+
}
|
|
66
|
+
messages.push(value);
|
|
67
|
+
}
|
|
68
|
+
return { inbox, messages };
|
|
69
|
+
}
|
|
70
|
+
function canonicalLanesByUlid(hqRoot) {
|
|
71
|
+
const grouped = new Map();
|
|
72
|
+
for (const lane of readAllLanes(hqRoot)) {
|
|
73
|
+
const ulid = laneUlid(lane.lane_id);
|
|
74
|
+
if (!ulid)
|
|
75
|
+
continue;
|
|
76
|
+
grouped.set(ulid, [...(grouped.get(ulid) ?? []), lane]);
|
|
77
|
+
}
|
|
78
|
+
return grouped;
|
|
79
|
+
}
|
|
80
|
+
/** Finds bare-ULID run folders that have no bare lane record but do have a canonical lane. */
|
|
81
|
+
export function findOrphanedBareIdDropboxes(hqRoot, company) {
|
|
82
|
+
const runsRoot = path.join(hqRoot, LANES_RUNS_REL);
|
|
83
|
+
let entries;
|
|
84
|
+
try {
|
|
85
|
+
const stat = fs.lstatSync(runsRoot);
|
|
86
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
87
|
+
return fail("orphaned_dropbox_runs_invalid", "Lane runs path must be a regular directory", { path: runsRoot });
|
|
88
|
+
}
|
|
89
|
+
entries = fs.readdirSync(runsRoot, { withFileTypes: true });
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error.code === "ENOENT")
|
|
93
|
+
return [];
|
|
94
|
+
if (error instanceof LanesError)
|
|
95
|
+
throw error;
|
|
96
|
+
return fail("orphaned_dropbox_runs_unreadable", `Could not list lane runs: ${error instanceof Error ? error.message : String(error)}`, { path: runsRoot });
|
|
97
|
+
}
|
|
98
|
+
const lanesByUlid = canonicalLanesByUlid(hqRoot);
|
|
99
|
+
const findings = [];
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (!entry.isDirectory() || !ULID_PATTERN.test(entry.name))
|
|
102
|
+
continue;
|
|
103
|
+
const ulid = entry.name.toUpperCase();
|
|
104
|
+
if (tryReadLane(hqRoot, entry.name) !== null)
|
|
105
|
+
continue;
|
|
106
|
+
const canonicalLanes = lanesByUlid.get(ulid) ?? [];
|
|
107
|
+
const visibleLanes = company === undefined ? canonicalLanes : canonicalLanes.filter((lane) => {
|
|
108
|
+
const resolved = resolveLaneCompany(hqRoot, lane).company;
|
|
109
|
+
return resolved?.slug === company.slug && resolved.uid === company.uid;
|
|
110
|
+
});
|
|
111
|
+
const canonicalLaneIds = visibleLanes.map((lane) => lane.lane_id);
|
|
112
|
+
if (canonicalLaneIds.length === 0)
|
|
113
|
+
continue;
|
|
114
|
+
const sourceDir = path.join(runsRoot, entry.name);
|
|
115
|
+
const { messages } = checkedInbox(sourceDir);
|
|
116
|
+
const newest = messages.reduce((current, message) => {
|
|
117
|
+
if (typeof message.at !== "string")
|
|
118
|
+
return current;
|
|
119
|
+
const ms = Date.parse(message.at);
|
|
120
|
+
if (!Number.isFinite(ms) || (current !== null && current.ms >= ms))
|
|
121
|
+
return current;
|
|
122
|
+
return { at: message.at, ms };
|
|
123
|
+
}, null);
|
|
124
|
+
findings.push({
|
|
125
|
+
ulid,
|
|
126
|
+
canonical_lane_ids: canonicalLaneIds,
|
|
127
|
+
message_count: messages.length,
|
|
128
|
+
newest_message_at: newest?.at ?? null,
|
|
129
|
+
source_dir: sourceDir,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return findings.sort((a, b) => a.ulid.localeCompare(b.ulid));
|
|
133
|
+
}
|
|
134
|
+
export function repairOrphanedBareIdDropbox(hqRoot, ulidInput, options) {
|
|
135
|
+
const ulid = ulidInput.trim().toUpperCase();
|
|
136
|
+
if (!ULID_PATTERN.test(ulid))
|
|
137
|
+
return fail("orphaned_dropbox_ulid_invalid", "Expected a 26-character ULID", { ulid: ulidInput });
|
|
138
|
+
const finding = findOrphanedBareIdDropboxes(hqRoot).find((entry) => entry.ulid === ulid);
|
|
139
|
+
if (!finding)
|
|
140
|
+
return fail("orphaned_dropbox_not_found", `No orphaned bare-ID dropbox was found for ${ulid}`, { ulid });
|
|
141
|
+
if (finding.canonical_lane_ids.length !== 1) {
|
|
142
|
+
return fail("orphaned_dropbox_lane_ambiguous", `ULID ${ulid} matches more than one lane record`, { ulid, lane_ids: finding.canonical_lane_ids });
|
|
143
|
+
}
|
|
144
|
+
const canonicalLaneId = finding.canonical_lane_ids[0];
|
|
145
|
+
if (tryReadLane(hqRoot, canonicalLaneId) === null) {
|
|
146
|
+
return fail("orphaned_dropbox_lane_changed", `Canonical lane ${canonicalLaneId} disappeared before repair`, { ulid, lane_id: canonicalLaneId });
|
|
147
|
+
}
|
|
148
|
+
const { messages } = checkedInbox(finding.source_dir);
|
|
149
|
+
const newest = messages.reduce((current, message) => {
|
|
150
|
+
if (typeof message.at !== "string")
|
|
151
|
+
return current;
|
|
152
|
+
const ms = Date.parse(message.at);
|
|
153
|
+
if (!Number.isFinite(ms) || (current !== null && current.ms >= ms))
|
|
154
|
+
return current;
|
|
155
|
+
return { at: message.at, ms };
|
|
156
|
+
}, null);
|
|
157
|
+
const result = {
|
|
158
|
+
ulid,
|
|
159
|
+
canonical_lane_id: canonicalLaneId,
|
|
160
|
+
message_count: messages.length,
|
|
161
|
+
newest_message_at: newest?.at ?? null,
|
|
162
|
+
applied: false,
|
|
163
|
+
};
|
|
164
|
+
if (!options.apply)
|
|
165
|
+
return result;
|
|
166
|
+
if (!options.enabled)
|
|
167
|
+
return fail("orphaned_dropbox_repair_disabled", "Applying orphaned dropbox repairs requires the lanes.orphaned-bare-id-dropbox-repair flag", { ulid });
|
|
168
|
+
const archiveName = `${ulid}.orphaned-${options.now.toISOString().replaceAll(/[:.]/g, "-")}`;
|
|
169
|
+
const archivedDir = path.join(path.dirname(finding.source_dir), archiveName);
|
|
170
|
+
if (fs.existsSync(archivedDir))
|
|
171
|
+
return fail("orphaned_dropbox_archive_exists", "Orphaned dropbox archive path already exists", { ulid, path: archivedDir });
|
|
172
|
+
if (messages.length > 0) {
|
|
173
|
+
const targetRunDir = path.join(hqRoot, LANES_RUNS_REL, canonicalLaneId);
|
|
174
|
+
const targetDropboxDir = path.join(targetRunDir, "dropbox");
|
|
175
|
+
fs.mkdirSync(targetRunDir, { recursive: true });
|
|
176
|
+
for (const dir of [targetRunDir, targetDropboxDir]) {
|
|
177
|
+
try {
|
|
178
|
+
const stat = fs.lstatSync(dir);
|
|
179
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
180
|
+
return fail("orphaned_dropbox_target_invalid", "Canonical dropbox path must be a regular directory", { path: dir });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (error.code !== "ENOENT") {
|
|
185
|
+
return fail("orphaned_dropbox_target_unreadable", `Could not inspect canonical dropbox path: ${error instanceof Error ? error.message : String(error)}`, { path: dir });
|
|
186
|
+
}
|
|
187
|
+
fs.mkdirSync(dir);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const targetInbox = path.join(targetDropboxDir, "inbox.jsonl");
|
|
191
|
+
try {
|
|
192
|
+
const stat = fs.lstatSync(targetInbox);
|
|
193
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
194
|
+
return fail("orphaned_dropbox_target_invalid", "Canonical inbox must be a regular file", { path: targetInbox });
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error.code !== "ENOENT") {
|
|
198
|
+
return fail("orphaned_dropbox_target_unreadable", `Could not inspect canonical inbox: ${error instanceof Error ? error.message : String(error)}`, { path: targetInbox });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const redeliveredAt = options.now.toISOString();
|
|
202
|
+
const redelivered = messages.map((message) => ({
|
|
203
|
+
...message,
|
|
204
|
+
redelivered: true,
|
|
205
|
+
redelivered_from: ulid,
|
|
206
|
+
redelivered_at: redeliveredAt,
|
|
207
|
+
}));
|
|
208
|
+
fs.appendFileSync(targetInbox, `${redelivered.map((message) => JSON.stringify(message)).join("\n")}\n`, "utf8");
|
|
209
|
+
}
|
|
210
|
+
fs.renameSync(finding.source_dir, archivedDir);
|
|
211
|
+
return { ...result, applied: true, archived_dir: archivedDir };
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=orphaned-dropbox.js.map
|
|
@@ -69,7 +69,7 @@ export interface TrackerCheckFinding {
|
|
|
69
69
|
[key: string]: unknown;
|
|
70
70
|
}
|
|
71
71
|
export interface TrackerCheckResult {
|
|
72
|
-
id: "questions" | "lead_lanes" | "freshness" | "blockers" | "kpis" | "directives" | "orphaned_lane";
|
|
72
|
+
id: "questions" | "lead_lanes" | "freshness" | "blockers" | "kpis" | "directives" | "orphaned_lane" | "orphaned_bare_id_dropbox";
|
|
73
73
|
result: "ok" | "failing";
|
|
74
74
|
items: TrackerCheckFinding[];
|
|
75
75
|
}
|
|
@@ -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
|