@indigoai-us/hq-cli 5.294.0 → 5.295.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/command-catalog.generated.d.ts +12 -9
  3. package/dist/command-catalog.generated.js +15 -12
  4. package/dist/commands/auth.d.ts +1 -0
  5. package/dist/commands/auth.js +76 -1
  6. package/dist/commands/doctor.js +0 -74
  7. package/dist/commands/lanes-message.js +23 -5
  8. package/dist/commands/lanes-resume.js +0 -3
  9. package/dist/commands/lanes-shared.d.ts +1 -2
  10. package/dist/commands/lanes.d.ts +1 -9
  11. package/dist/commands/lanes.js +177 -172
  12. package/dist/fast-scaffold.d.ts +0 -5
  13. package/dist/fast-scaffold.js +0 -10
  14. package/dist/index.js +1 -10
  15. package/dist/lib/core-utils/sentry-report.d.ts +0 -19
  16. package/dist/lib/core-utils/sentry-report.js +8 -61
  17. package/dist/lib/doctor/checks/claude-wiring.js +9 -0
  18. package/dist/lib/lanes/flag-keys.d.ts +2 -0
  19. package/dist/lib/lanes/flag-keys.js +2 -0
  20. package/dist/lib/lanes/manager-status-gate.d.ts +0 -17
  21. package/dist/lib/lanes/manager-status-gate.js +2 -43
  22. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -0
  23. package/dist/lib/lanes/outpost-dispatch.js +7 -1
  24. package/dist/lib/lanes/outpost-hire-cache.d.ts +0 -1
  25. package/dist/lib/lanes/outpost-hire-cache.js +0 -1
  26. package/dist/lib/lanes/outpost-snapshot-memo.d.ts +6 -0
  27. package/dist/lib/lanes/outpost-snapshot-memo.js +14 -0
  28. package/dist/lib/lanes/reconcile.d.ts +2 -3
  29. package/dist/lib/lanes/reconcile.js +44 -38
  30. package/dist/lib/lanes/spawn.d.ts +0 -2
  31. package/dist/lib/lanes/spawn.js +18 -5
  32. package/dist/lib/lanes/store.js +22 -0
  33. package/dist/lib/lanes/types.d.ts +18 -1
  34. package/dist/lib/lanes/types.js +1 -1
  35. package/dist/lib/lanes/watcher.d.ts +4 -3
  36. package/dist/lib/lanes/watcher.js +102 -34
  37. package/dist/lib/mesh/live/daemon/run.js +18 -1
  38. package/dist/utils/cognito-session.d.ts +6 -0
  39. package/dist/utils/cognito-session.js +9 -0
  40. package/dist/utils/self-update.js +31 -0
  41. package/dist/utils/version-gate.js +22 -3
  42. package/package.json +1 -1
  43. package/dist/fast-lanes-list-route.d.ts +0 -8
  44. package/dist/fast-lanes-list-route.js +0 -32
  45. package/dist/fast-lanes-list.d.ts +0 -3
  46. package/dist/fast-lanes-list.js +0 -224
  47. package/dist/lib/doctor/client-health-sentry-flag.d.ts +0 -23
  48. package/dist/lib/doctor/client-health-sentry-flag.js +0 -99
  49. package/dist/lib/doctor/client-health-sentry.d.ts +0 -71
  50. package/dist/lib/doctor/client-health-sentry.js +0 -286
  51. package/dist/lib/lanes/outpost-snapshot-cache.d.ts +0 -26
  52. package/dist/lib/lanes/outpost-snapshot-cache.js +0 -215
  53. package/dist/lib/lanes/outpost-snapshot-refresh.d.ts +0 -21
  54. package/dist/lib/lanes/outpost-snapshot-refresh.js +0 -329
@@ -1,286 +0,0 @@
1
- import { randomUUID as cryptoRandomUUID } from "node:crypto";
2
- import * as fs from "node:fs";
3
- import * as os from "node:os";
4
- import * as path from "node:path";
5
- import { CLI_VERSION } from "../../cli-version.js";
6
- import { readSyncVersion } from "../../utils/feedback-versions.js";
7
- import { readHqVersion } from "../../utils/pack-contributions.js";
8
- export const CLIENT_HEALTH_SENTRY_NOT_HANDLED_EXIT_CODE = 10;
9
- export const CLIENT_HEALTH_SENTRY_MAX_INPUT_BYTES = 2 * 1024 * 1024;
10
- export const CLIENT_HEALTH_SENTRY_MAX_FINDINGS = 250;
11
- export const CLIENT_HEALTH_INSTALL_ID_FILENAME = "install-id";
12
- export const CLIENT_HEALTH_LAST_RESULT_FILENAME = "last-result.json";
13
- const STATE_DIRECTORY = path.join("workspace", ".hq-client-health");
14
- const REASON_CODES = new Set([
15
- "core-version-unavailable",
16
- "core-update-available",
17
- "no-local-tree",
18
- "never-synced",
19
- "invalid-last-sync",
20
- "stale-threshold",
21
- "manifest-unavailable",
22
- "manifest-state-unavailable",
23
- "no-manifest-scopes",
24
- "never-uploaded",
25
- "invalid-upload-time",
26
- "unresolved-company-uid",
27
- "realtime-lag-stuck",
28
- "receiver-permission-denied",
29
- "runner-restart-loop",
30
- "device-limit-backoff",
31
- "repeated-conflicts",
32
- ]);
33
- const FIX_OUTCOMES = new Set(["not_auto_fixable", "fix_failed", "fix_timed_out"]);
34
- const INSTALL_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
35
- const VERSION = /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?(?:\+[A-Za-z0-9.-]+)?$/;
36
- const PLATFORMS = new Set([
37
- "aix", "android", "darwin", "freebsd", "haiku", "linux", "openbsd",
38
- "sunos", "win32", "cygwin",
39
- ]);
40
- const ARCHITECTURES = new Set([
41
- "arm", "arm64", "ia32", "loong64", "mips", "mipsel", "ppc", "ppc64",
42
- "riscv64", "s390x", "x64",
43
- ]);
44
- function isRecord(value) {
45
- return value !== null && typeof value === "object" && !Array.isArray(value);
46
- }
47
- function cleanVersion(value) {
48
- return value && VERSION.test(value) ? value : "unknown";
49
- }
50
- function cleanPlatform(value) {
51
- return PLATFORMS.has(value) ? value : "unknown";
52
- }
53
- function cleanArchitecture(value) {
54
- return ARCHITECTURES.has(value) ? value : "unknown";
55
- }
56
- /** Collapse company-specific doctor ids before any value can enter a Sentry event. */
57
- export function normalizeClientHealthCheckId(checkId) {
58
- if (checkId === "sync.update.core")
59
- return checkId;
60
- if (checkId === "sync.runtime" || checkId.startsWith("sync.runtime."))
61
- return "sync.runtime";
62
- if (checkId === "sync.journal.__hq_personal_vault__")
63
- return "sync.journal.personal_vault";
64
- if (checkId === "sync.manifest.__hq_personal_vault__")
65
- return "sync.manifest.personal_vault";
66
- const match = /^sync\.(journal|manifest)\.([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/.exec(checkId);
67
- if (!match)
68
- return "other";
69
- const family = match[1];
70
- const scope = match[2];
71
- if (scope === "personal")
72
- return `sync.${family}.personal`;
73
- if (family === "manifest" && scope.startsWith("unresolved."))
74
- return "other";
75
- return `sync.${family}.company`;
76
- }
77
- function companySlugForCheck(checkId, checkClass) {
78
- if (checkClass !== "sync.journal.company" && checkClass !== "sync.manifest.company") {
79
- return null;
80
- }
81
- const match = /^sync\.(?:journal|manifest)\.([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/.exec(checkId);
82
- return match?.[1] ?? null;
83
- }
84
- function safeReasonCode(value) {
85
- return typeof value === "string" && REASON_CODES.has(value) ? value : "none";
86
- }
87
- function safeFixOutcome(value) {
88
- return FIX_OUTCOMES.has(value) ? value : "not_auto_fixable";
89
- }
90
- /** Keep only the doctor fields needed for safe health reporting. */
91
- export function parseClientHealthDoctorFindings(input) {
92
- if (Buffer.byteLength(input, "utf8") > CLIENT_HEALTH_SENTRY_MAX_INPUT_BYTES) {
93
- throw new Error("Client health doctor input exceeds its size limit");
94
- }
95
- let parsed;
96
- try {
97
- parsed = JSON.parse(input);
98
- }
99
- catch {
100
- throw new Error("Client health doctor input is invalid JSON");
101
- }
102
- if (!isRecord(parsed) || !Array.isArray(parsed.results)) {
103
- throw new Error("Client health doctor input has no results array");
104
- }
105
- const findings = [];
106
- for (const value of parsed.results) {
107
- if (!isRecord(value) || value.family !== "sync")
108
- continue;
109
- if (value.status !== "FAIL" && value.status !== "WARN")
110
- continue;
111
- if (value.checkId === "sync.update.core" && value.status === "WARN")
112
- continue;
113
- const checkId = typeof value.checkId === "string" &&
114
- /^sync\.[A-Za-z0-9._-]{1,160}$/.test(value.checkId)
115
- ? value.checkId
116
- : "unknown";
117
- const checkClass = normalizeClientHealthCheckId(checkId);
118
- findings.push({
119
- checkClass,
120
- companySlug: companySlugForCheck(checkId, checkClass),
121
- reasonCode: safeReasonCode(value.reasonCode),
122
- status: value.status,
123
- });
124
- if (findings.length > CLIENT_HEALTH_SENTRY_MAX_FINDINGS) {
125
- throw new Error("Client health doctor input has too many sync findings");
126
- }
127
- }
128
- return findings;
129
- }
130
- function defaultVersions(hqRoot) {
131
- let core;
132
- try {
133
- core = readHqVersion(hqRoot);
134
- }
135
- catch {
136
- core = null;
137
- }
138
- let sync;
139
- try {
140
- sync = readSyncVersion();
141
- }
142
- catch {
143
- sync = null;
144
- }
145
- return { cli: CLI_VERSION, core, sync };
146
- }
147
- function stateDirectory(hqRoot) {
148
- const root = fs.realpathSync(hqRoot);
149
- const workspacePath = path.join(root, "workspace");
150
- fs.mkdirSync(workspacePath, { recursive: true, mode: 0o700 });
151
- const workspaceStat = fs.lstatSync(workspacePath);
152
- if (!workspaceStat.isDirectory() || workspaceStat.isSymbolicLink()) {
153
- throw new Error("Client health state workspace is not a local directory");
154
- }
155
- const directory = path.join(root, STATE_DIRECTORY);
156
- fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
157
- const directoryStat = fs.lstatSync(directory);
158
- if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) {
159
- throw new Error("Client health state directory is not a local directory");
160
- }
161
- fs.chmodSync(directory, 0o700);
162
- return directory;
163
- }
164
- function readInstallId(filePath) {
165
- try {
166
- const fileStat = fs.lstatSync(filePath);
167
- if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
168
- throw new Error("Client health install id is not a regular local file");
169
- }
170
- const value = fs.readFileSync(filePath, "utf8").trim();
171
- if (!INSTALL_ID.test(value))
172
- throw new Error("Client health install id is invalid");
173
- fs.chmodSync(filePath, 0o600);
174
- return value;
175
- }
176
- catch (error) {
177
- if (isRecord(error) && error.code === "ENOENT")
178
- return null;
179
- throw error;
180
- }
181
- }
182
- /** Store a random UUID once per local HQ install, without using host identity. */
183
- export function getOrCreateClientHealthInstallId(hqRoot, randomUUID = cryptoRandomUUID) {
184
- const directory = stateDirectory(hqRoot);
185
- const filePath = path.join(directory, CLIENT_HEALTH_INSTALL_ID_FILENAME);
186
- const current = readInstallId(filePath);
187
- if (current)
188
- return current;
189
- const candidate = randomUUID();
190
- if (!INSTALL_ID.test(candidate))
191
- throw new Error("Client health random id source returned an invalid id");
192
- try {
193
- fs.writeFileSync(filePath, `${candidate}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
194
- return candidate;
195
- }
196
- catch (error) {
197
- if (!isRecord(error) || error.code !== "EEXIST")
198
- throw error;
199
- const raced = readInstallId(filePath);
200
- if (!raced)
201
- throw new Error("Client health install id was not created");
202
- return raced;
203
- }
204
- }
205
- function nextStep(checkClass) {
206
- switch (checkClass) {
207
- case "sync.update.core":
208
- return "HQ couldn't update itself automatically. Run `hq update` or `/update-hq`, then restart this session.";
209
- case "sync.journal.personal":
210
- case "sync.journal.personal_vault":
211
- return "HQ hasn't synced your personal files in over 7 days and the automatic fix did not work. Run `hq sync` and check the result. If it fails, run `hq doctor` and send the output to support.";
212
- case "sync.journal.company":
213
- return "HQ hasn't synced this company's files in over 7 days and the automatic fix did not work. Run `hq sync` and check the result. If it fails, run `hq doctor` and send the output to support.";
214
- case "sync.manifest.personal":
215
- case "sync.manifest.personal_vault":
216
- return "HQ couldn't read your personal sync list. Run `hq doctor --fix`. If it still fails, run `hq login` again.";
217
- case "sync.manifest.company":
218
- return "HQ couldn't read a company sync manifest. Run `hq doctor --fix`. If it still fails, run `hq login` again.";
219
- default:
220
- return "HQ still reports a sync health issue after its automatic fix. Run `hq doctor` and send the output to support.";
221
- }
222
- }
223
- function writeLastResult(hqRoot, finding, outcome, randomUUID) {
224
- const directory = stateDirectory(hqRoot);
225
- const result = {
226
- check_class: finding.checkClass,
227
- outcome,
228
- next_step: nextStep(finding.checkClass),
229
- result_id: randomUUID(),
230
- };
231
- if (!INSTALL_ID.test(result.result_id))
232
- throw new Error("Client health result id source returned an invalid id");
233
- const filePath = path.join(directory, CLIENT_HEALTH_LAST_RESULT_FILENAME);
234
- const tempPath = path.join(directory, `${CLIENT_HEALTH_LAST_RESULT_FILENAME}.${result.result_id}.tmp`);
235
- fs.writeFileSync(tempPath, `${JSON.stringify(result, null, 2)}\n`, {
236
- encoding: "utf8",
237
- flag: "wx",
238
- mode: 0o600,
239
- });
240
- fs.renameSync(tempPath, filePath);
241
- return result;
242
- }
243
- function buildEvent(finding, installId, outcome, failingCount, versions, platform, arch) {
244
- const reasonCode = finding.reasonCode;
245
- return {
246
- message: `Client health: ${finding.checkClass} unresolved after hq doctor --fix`,
247
- level: "warning",
248
- fingerprint: ["client-health", finding.checkClass, reasonCode],
249
- tags: {
250
- check_class: finding.checkClass,
251
- reason_code: reasonCode,
252
- fix_outcome: outcome,
253
- status: finding.status,
254
- failing_check_count: String(failingCount),
255
- cli_version: cleanVersion(versions.cli),
256
- core_version: cleanVersion(versions.core),
257
- sync_version: cleanVersion(versions.sync),
258
- platform: cleanPlatform(platform),
259
- arch: cleanArchitecture(arch),
260
- },
261
- user: { id: installId, ip_address: null },
262
- };
263
- }
264
- /**
265
- * Report only after every finding's company has opted in. A mixed rollout
266
- * falls back as one batch so the old path cannot duplicate partially sent rows.
267
- */
268
- export async function handleClientHealthSentryReport(input, dependencies) {
269
- const findings = parseClientHealthDoctorFindings(input.doctorJson);
270
- if (findings.length === 0)
271
- return "handled";
272
- const outcome = safeFixOutcome(input.fixOutcome);
273
- const companySlugs = [...new Set(findings.map((finding) => finding.companySlug))];
274
- const enabled = await Promise.all(companySlugs.map((companySlug) => dependencies.resolveFlagForCompany(companySlug)));
275
- if (enabled.some((value) => value !== true))
276
- return "not_handled";
277
- const randomUUID = dependencies.randomUUID ?? cryptoRandomUUID;
278
- const installId = getOrCreateClientHealthInstallId(input.hqRoot, randomUUID);
279
- const versions = (dependencies.versions ?? defaultVersions)(input.hqRoot);
280
- const events = findings.map((finding) => buildEvent(finding, installId, outcome, findings.length, versions, cleanPlatform((dependencies.platform ?? (() => process.platform))()), cleanArchitecture((dependencies.arch ?? os.arch)())));
281
- writeLastResult(input.hqRoot, findings[0], outcome, randomUUID);
282
- if (!(await dependencies.sendEvents(events)))
283
- return "send_failed";
284
- return "handled";
285
- }
286
- //# sourceMappingURL=client-health-sentry.js.map
@@ -1,26 +0,0 @@
1
- export declare const OUTPOST_SNAPSHOT_CACHE_TTL_MS = 3000;
2
- export declare const OUTPOST_SNAPSHOT_CACHE_STALE_LOCK_MS = 45000;
3
- export interface OutpostSnapshotCachePolicy {
4
- ttlMs?: number;
5
- staleLockMs?: number;
6
- lockWaitMs?: number;
7
- pollMs?: number;
8
- now?: () => number;
9
- sleep?: (ms: number) => Promise<void>;
10
- acquireLock?: (lockPath: string, token: string, nowMs: number) => void;
11
- }
12
- /** Deduplicates identical remote snapshot queries within one command invocation. */
13
- export declare class OutpostSnapshotInvocationMemo {
14
- private readonly requests;
15
- getOrCreate<T>(outpostId: string, query: unknown, fetch: () => Promise<T>): Promise<T>;
16
- }
17
- export declare function outpostLaneListSnapshotKey(outpostId: string, normalizedQuery: unknown): string;
18
- export declare function outpostSnapshotCachePath(hqRoot: string, key: string): string;
19
- export declare function getOrFetchOutpostSnapshot<T>(input: {
20
- hqRoot: string;
21
- key: string;
22
- isValue: (value: unknown) => value is T;
23
- fetch: () => Promise<T>;
24
- policy?: OutpostSnapshotCachePolicy;
25
- }): Promise<T>;
26
- //# sourceMappingURL=outpost-snapshot-cache.d.ts.map
@@ -1,215 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { LanesError } from "./errors.js";
5
- export const OUTPOST_SNAPSHOT_CACHE_TTL_MS = 3_000;
6
- export const OUTPOST_SNAPSHOT_CACHE_STALE_LOCK_MS = 45_000;
7
- const CACHE_RELATIVE_PATH = path.join("workspace", "lanes-runs", ".cache", "outpost-snapshots");
8
- const CACHE_SCHEMA_VERSION = "hq-lanes-outpost-snapshot-cache.v1";
9
- const LOCK_POLL_MS = 20;
10
- const LOCK_WAIT_MS = 60_000;
11
- /** Deduplicates identical remote snapshot queries within one command invocation. */
12
- export class OutpostSnapshotInvocationMemo {
13
- requests = new Map();
14
- getOrCreate(outpostId, query, fetch) {
15
- const key = JSON.stringify([outpostId, query]);
16
- const existing = this.requests.get(key);
17
- if (existing)
18
- return existing;
19
- const request = fetch();
20
- this.requests.set(key, request);
21
- return request;
22
- }
23
- }
24
- export function outpostLaneListSnapshotKey(outpostId, normalizedQuery) {
25
- return JSON.stringify(["lane-list", outpostId, normalizedQuery]);
26
- }
27
- export function outpostSnapshotCachePath(hqRoot, key) {
28
- const digest = createKeyDigest(key);
29
- return path.join(hqRoot, CACHE_RELATIVE_PATH, `${digest}.json`);
30
- }
31
- export async function getOrFetchOutpostSnapshot(input) {
32
- const policy = input.policy ?? {};
33
- const now = policy.now ?? Date.now;
34
- const ttlMs = policy.ttlMs ?? OUTPOST_SNAPSHOT_CACHE_TTL_MS;
35
- const staleLockMs = policy.staleLockMs ?? OUTPOST_SNAPSHOT_CACHE_STALE_LOCK_MS;
36
- const lockWaitMs = policy.lockWaitMs ?? LOCK_WAIT_MS;
37
- const pollMs = policy.pollMs ?? LOCK_POLL_MS;
38
- const sleep = policy.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
39
- const filePath = outpostSnapshotCachePath(input.hqRoot, input.key);
40
- const cached = readFreshCache(filePath, input.key, now(), ttlMs, input.isValue);
41
- if (cached !== undefined)
42
- return cached;
43
- const lockPath = `${filePath}.lock`;
44
- const token = `${process.pid}:${randomBytes(12).toString("hex")}`;
45
- const startedAt = now();
46
- const acquireLock = policy.acquireLock ?? createCacheLock;
47
- while (true) {
48
- try {
49
- acquireLock(lockPath, token, now());
50
- break;
51
- }
52
- catch (err) {
53
- const code = err.code;
54
- if (code !== "EEXIST") {
55
- process.stderr.write(`${JSON.stringify({
56
- event: "lanes.outpost_snapshot_cache_lock_unavailable",
57
- error: typeof code === "string" ? code : "UnknownError",
58
- fallback: "uncached",
59
- })}\n`);
60
- const value = await input.fetch();
61
- if (!input.isValue(value)) {
62
- throw new LanesError("outpost_snapshot_cache_value_invalid", "Outpost snapshot fetch returned an invalid value", { key: input.key });
63
- }
64
- return value;
65
- }
66
- if (recoverStaleLock(lockPath, staleLockMs, now()))
67
- continue;
68
- const afterWait = readFreshCache(filePath, input.key, now(), ttlMs, input.isValue);
69
- if (afterWait !== undefined)
70
- return afterWait;
71
- if (now() - startedAt >= lockWaitMs) {
72
- throw new LanesError("outpost_snapshot_cache_lock_timeout", "Timed out waiting for an Outpost snapshot refresh", { path: lockPath });
73
- }
74
- await sleep(pollMs);
75
- }
76
- }
77
- try {
78
- const afterLock = readFreshCache(filePath, input.key, now(), ttlMs, input.isValue);
79
- if (afterLock !== undefined)
80
- return afterLock;
81
- const value = await input.fetch();
82
- if (!input.isValue(value)) {
83
- throw new LanesError("outpost_snapshot_cache_value_invalid", "Outpost snapshot fetch returned an invalid value", { key: input.key });
84
- }
85
- writeCache(filePath, input.key, value, now());
86
- return value;
87
- }
88
- finally {
89
- releaseLock(lockPath, token);
90
- }
91
- }
92
- function createCacheLock(lockPath, token, nowMs) {
93
- fs.mkdirSync(path.dirname(lockPath), { recursive: true });
94
- const lockFd = fs.openSync(lockPath, "wx", 0o600);
95
- try {
96
- fs.writeSync(lockFd, `${token}\n${nowMs}\n`);
97
- }
98
- catch (err) {
99
- try {
100
- fs.closeSync(lockFd);
101
- }
102
- catch (closeErr) {
103
- process.stderr.write(`lanes snapshot cache: lock close failed: ${errorText(closeErr)}\n`);
104
- }
105
- releaseLock(lockPath, token);
106
- throw err;
107
- }
108
- fs.closeSync(lockFd);
109
- }
110
- function createKeyDigest(key) {
111
- // A lane-list query can contain lane ids. Keep raw ids out of cache filenames.
112
- return createHash("sha256").update(key).digest("hex");
113
- }
114
- function readFreshCache(filePath, key, nowMs, ttlMs, isValue) {
115
- let contents;
116
- try {
117
- contents = fs.readFileSync(filePath, "utf8");
118
- }
119
- catch (err) {
120
- if (err.code === "ENOENT")
121
- return undefined;
122
- process.stderr.write(`lanes snapshot cache: read failed: ${errorText(err)}\n`);
123
- return undefined;
124
- }
125
- let raw;
126
- try {
127
- raw = JSON.parse(contents);
128
- }
129
- catch (err) {
130
- process.stderr.write(`lanes snapshot cache: ignoring malformed cache: ${errorText(err)}\n`);
131
- return undefined;
132
- }
133
- if (raw === null || typeof raw !== "object" || Array.isArray(raw))
134
- return undefined;
135
- const entry = raw;
136
- if (Object.keys(entry).length !== 4 ||
137
- entry.schema_version !== CACHE_SCHEMA_VERSION ||
138
- entry.key !== key ||
139
- !Number.isSafeInteger(entry.cached_at) ||
140
- entry.cached_at > nowMs ||
141
- nowMs - entry.cached_at >= ttlMs ||
142
- !isValue(entry.value))
143
- return undefined;
144
- return entry.value;
145
- }
146
- function writeCache(filePath, key, value, nowMs) {
147
- const temporaryPath = `${filePath}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
148
- try {
149
- const fd = fs.openSync(temporaryPath, "wx", 0o600);
150
- try {
151
- fs.writeFileSync(fd, `${JSON.stringify({
152
- schema_version: CACHE_SCHEMA_VERSION,
153
- key,
154
- cached_at: nowMs,
155
- value,
156
- })}\n`);
157
- fs.fsyncSync(fd);
158
- }
159
- finally {
160
- fs.closeSync(fd);
161
- }
162
- fs.renameSync(temporaryPath, filePath);
163
- }
164
- catch (err) {
165
- try {
166
- fs.unlinkSync(temporaryPath);
167
- }
168
- catch (cleanupErr) {
169
- if (cleanupErr.code !== "ENOENT") {
170
- process.stderr.write(`lanes snapshot cache: temp cleanup failed: ${errorText(cleanupErr)}\n`);
171
- }
172
- }
173
- // The cache is an optimization; a successful, validated remote result stays usable.
174
- process.stderr.write(`lanes snapshot cache: write failed: ${errorText(err)}\n`);
175
- }
176
- }
177
- function recoverStaleLock(lockPath, staleLockMs, nowMs) {
178
- let stat;
179
- try {
180
- stat = fs.statSync(lockPath);
181
- }
182
- catch (err) {
183
- return err.code === "ENOENT";
184
- }
185
- if (nowMs - stat.mtimeMs <= staleLockMs)
186
- return false;
187
- const stalePath = `${lockPath}.stale.${process.pid}.${randomBytes(6).toString("hex")}`;
188
- try {
189
- fs.renameSync(lockPath, stalePath);
190
- fs.unlinkSync(stalePath);
191
- return true;
192
- }
193
- catch (err) {
194
- const code = err.code;
195
- if (code === "ENOENT")
196
- return true;
197
- throw new LanesError("outpost_snapshot_cache_lock_recovery_failed", `Could not recover a stale Outpost snapshot lock: ${errorText(err)}`, { path: lockPath });
198
- }
199
- }
200
- function releaseLock(lockPath, token) {
201
- try {
202
- const contents = fs.readFileSync(lockPath, "utf8");
203
- if (contents.split("\n", 1)[0] === token)
204
- fs.unlinkSync(lockPath);
205
- }
206
- catch (err) {
207
- if (err.code !== "ENOENT") {
208
- process.stderr.write(`lanes snapshot cache: lock release failed: ${errorText(err)}\n`);
209
- }
210
- }
211
- }
212
- function errorText(err) {
213
- return err instanceof Error ? err.message : String(err);
214
- }
215
- //# sourceMappingURL=outpost-snapshot-cache.js.map
@@ -1,21 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- export declare const OUTPOST_SNAPSHOT_REFRESH_LOCK_STALE_AFTER_MS: number;
3
- export interface SnapshotRefreshChildRuntime {
4
- pid: number;
5
- once: (event: "exit", listener: () => void) => unknown;
6
- }
7
- export interface StartSnapshotRefreshDeps {
8
- spawn?: typeof spawn;
9
- now?: () => number;
10
- pid?: number;
11
- isProcessAlive?: (pid: number) => boolean;
12
- entryPath?: string;
13
- beforeStaleLockClaim?: () => void;
14
- writeError?: (message: string) => void;
15
- }
16
- export declare function outpostSnapshotRefreshLockPath(root: string, env?: NodeJS.ProcessEnv): string;
17
- /** Start the detached remote snapshot refresh unless this HQ root is refreshing already. */
18
- export declare function startOutpostLaneSnapshotRefresh(root: string, env?: NodeJS.ProcessEnv, deps?: StartSnapshotRefreshDeps): void;
19
- /** Adopt the launcher lock in the detached child and remove it on normal exit. */
20
- export declare function registerOutpostLaneSnapshotRefreshChild(env?: NodeJS.ProcessEnv, runtime?: SnapshotRefreshChildRuntime): boolean;
21
- //# sourceMappingURL=outpost-snapshot-refresh.d.ts.map