@indigoai-us/hq-cli 5.345.160 → 5.345.161
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 +24 -0
- package/assets/scaffold/core/scripts/session-journal.sh +116 -24
- package/dist/command-catalog.generated.d.ts +218 -5
- package/dist/command-catalog.generated.js +269 -5
- package/dist/commands/auth.js +6 -5
- package/dist/commands/core-session-helper-runtime.d.ts +1 -0
- package/dist/commands/core-session-journal-native.d.ts +1 -0
- package/dist/commands/core-session-journal-native.js +158 -34
- package/dist/commands/feedback.d.ts +12 -0
- package/dist/commands/feedback.js +52 -0
- package/dist/commands/files-browse.d.ts +8 -0
- package/dist/commands/files-browse.js +83 -1
- package/dist/commands/install-global.d.ts +2 -0
- package/dist/commands/install-global.js +24 -1
- package/dist/commands/integrations-connect.js +1 -0
- package/dist/commands/integrations-manage.d.ts +2 -0
- package/dist/commands/integrations-manage.js +6 -4
- package/dist/commands/lanes-link.js +5 -3
- package/dist/commands/lanes-session-start.d.ts +12 -0
- package/dist/commands/lanes-session-start.js +93 -12
- package/dist/commands/meetings.d.ts +11 -0
- package/dist/commands/meetings.js +41 -11
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2486 -1794
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +21 -0
- package/dist/lanes-session-start-argv.d.ts +7 -0
- package/dist/lanes-session-start-argv.js +9 -0
- package/dist/lib/bot/log.d.ts +3 -2
- package/dist/lib/bot/progress.d.ts +23 -1
- package/dist/lib/bot/progress.js +43 -5
- package/dist/lib/bot/run.js +2 -0
- package/dist/lib/bot/runtime/claude.d.ts +12 -0
- package/dist/lib/bot/runtime/claude.js +20 -0
- package/dist/lib/bot/runtime/index.d.ts +10 -1
- package/dist/lib/bot/runtime/index.js +14 -2
- package/dist/lib/bot/runtime/types.d.ts +8 -0
- package/dist/lib/doctor/checks/ps1-shims.d.ts +21 -0
- package/dist/lib/doctor/checks/ps1-shims.js +82 -0
- package/dist/lib/doctor/fix/apply.d.ts +13 -1
- package/dist/lib/doctor/fix/apply.js +38 -3
- package/dist/lib/doctor/fix/remediation.d.ts +9 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/doctor/registry.js +2 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/links.d.ts +2 -1
- package/dist/lib/lanes/links.js +235 -17
- package/dist/lib/lanes/registry-flag.d.ts +24 -0
- package/dist/lib/lanes/registry-flag.js +72 -6
- package/dist/lib/lanes/store.d.ts +4 -2
- package/dist/lib/lanes/store.js +8 -8
- package/dist/lib/windows/ps1-shims.d.ts +60 -0
- package/dist/lib/windows/ps1-shims.js +152 -0
- package/dist/utils/self-update.d.ts +2 -0
- package/dist/utils/self-update.js +8 -0
- package/dist/utils/staged-npm-install.js +13 -2
- package/dist/utils/token-keychain.d.ts +11 -8
- package/dist/utils/token-keychain.js +36 -15
- package/package.json +8 -8
- package/scripts/install-hook-engine.mjs +12 -0
|
@@ -20,6 +20,8 @@ export interface LinkOptions {
|
|
|
20
20
|
timeout?: number;
|
|
21
21
|
event?: string;
|
|
22
22
|
input?: string;
|
|
23
|
+
ackMode?: string;
|
|
24
|
+
claims?: string;
|
|
23
25
|
now?: () => Date;
|
|
24
26
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
25
27
|
}
|
|
@@ -31,7 +33,6 @@ export declare function resolveLinkSessionId(root: string, explicit: string | un
|
|
|
31
33
|
/** Cheap hook guard used before remote flag lookup or queue access. */
|
|
32
34
|
export declare function linkedSessionIdFromHookInput(input: string): string;
|
|
33
35
|
export declare function isLinkedSession(root: string, sid: string): boolean;
|
|
34
|
-
/** Claims pending files by renaming the whole queue, matching conduct-inbox's no-loss drain. */
|
|
35
36
|
export declare function drainLinkQueue(inbox: string): string;
|
|
36
37
|
export declare function runLaneLink(subcommand: string, options: LinkOptions): Promise<LinkResult>;
|
|
37
38
|
/** Apply the default-on feature gate contract at the command boundary. */
|
package/dist/lib/lanes/links.js
CHANGED
|
@@ -2,6 +2,8 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { appendDropboxMessageAtPath } from "./dropbox.js";
|
|
5
|
+
import { withLaneRecordFileLock } from "./store.js";
|
|
6
|
+
import { LanesError } from "./errors.js";
|
|
5
7
|
export const LANE_SESSION_LINKS_FLAG = "lanes.session-links";
|
|
6
8
|
const ID_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
7
9
|
export class LaneLinkError extends Error {
|
|
@@ -132,6 +134,17 @@ function resolvePointer(root, sid, role, options) {
|
|
|
132
134
|
function queueDir(root, link, child) {
|
|
133
135
|
return path.join(rootDir(root), link, child ? "children" : "up", ...(child ? [child] : []), "inbox");
|
|
134
136
|
}
|
|
137
|
+
function withLinkQueueLock(inbox, fn) {
|
|
138
|
+
try {
|
|
139
|
+
return withLaneRecordFileLock(path.join(inbox, "queue"), fn, { reclaimByAge: false });
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error instanceof LanesError && error.error === "store_lock_timeout") {
|
|
143
|
+
throw new LaneLinkError("queue_busy", "Conductor queue busy; instructions retained");
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
135
148
|
function sendQueue(root, link, child, text) {
|
|
136
149
|
const inbox = queueDir(root, link, child);
|
|
137
150
|
const pending = path.join(inbox, "pending");
|
|
@@ -139,10 +152,12 @@ function sendQueue(root, link, child, text) {
|
|
|
139
152
|
// Reuse hq monitor's session-inbox append primitive; the pending directory is
|
|
140
153
|
// still claimed as a whole below, keeping conduct-inbox's no-loss handoff.
|
|
141
154
|
const cleanText = text.replaceAll("\0", "").replaceAll("\u000b", "").replaceAll("\u000c", "").replaceAll("\r", "");
|
|
142
|
-
|
|
155
|
+
withLinkQueueLock(inbox, () => {
|
|
156
|
+
appendDropboxMessageAtPath(pending, { at: new Date().toISOString(), text: cleanText });
|
|
157
|
+
});
|
|
143
158
|
}
|
|
144
159
|
/** Claims pending files by renaming the whole queue, matching conduct-inbox's no-loss drain. */
|
|
145
|
-
|
|
160
|
+
function drainLinkQueueUnlocked(inbox) {
|
|
146
161
|
const pending = path.join(inbox, "pending");
|
|
147
162
|
const claimedRoot = path.join(inbox, "claimed");
|
|
148
163
|
// Recover claims interrupted before the delivery result was committed. This is
|
|
@@ -190,16 +205,197 @@ export function drainLinkQueue(inbox) {
|
|
|
190
205
|
fs.writeFileSync(path.join(claim, ".consumed"), "delivered\n", { mode: 0o600 });
|
|
191
206
|
return messages;
|
|
192
207
|
}
|
|
208
|
+
export function drainLinkQueue(inbox) {
|
|
209
|
+
return withLinkQueueLock(inbox, () => drainLinkQueueUnlocked(inbox));
|
|
210
|
+
}
|
|
211
|
+
/** Claims remain replayable until the recipient explicitly accepts instructions.
|
|
212
|
+
* This is at-least-once transport, not proof of a native turn or execution.
|
|
213
|
+
*/
|
|
214
|
+
function claimLinkQueueUnlocked(inbox) {
|
|
215
|
+
const pending = path.join(inbox, "pending");
|
|
216
|
+
const claimed = path.join(inbox, "unaccepted");
|
|
217
|
+
fs.mkdirSync(claimed, { recursive: true });
|
|
218
|
+
if (fs.existsSync(path.join(pending, "inbox.jsonl"))) {
|
|
219
|
+
// Allocate while holding the queue lock. Wall time alone can repeat or
|
|
220
|
+
// move backward, and pid/random suffixes do not preserve claim order.
|
|
221
|
+
let serial = 0n;
|
|
222
|
+
for (const previous of fs.readdirSync(claimed)) {
|
|
223
|
+
const prefix = /^(\d{1,16})-/.exec(previous)?.[1];
|
|
224
|
+
if (prefix && BigInt(prefix) > serial)
|
|
225
|
+
serial = BigInt(prefix);
|
|
226
|
+
}
|
|
227
|
+
const wall = Date.now();
|
|
228
|
+
if (!Number.isSafeInteger(wall) || wall < 0)
|
|
229
|
+
throw new LaneLinkError("claim_clock_invalid", "Cannot allocate a safe receipt serial");
|
|
230
|
+
serial = BigInt(wall) > serial ? BigInt(wall) : serial + 1n;
|
|
231
|
+
if (serial > 9999999999999999n)
|
|
232
|
+
throw new LaneLinkError("claim_serial_exhausted", "Receipt serial exhausted; instructions retained");
|
|
233
|
+
const id = `${serial.toString().padStart(16, "0")}-${process.pid}-${randomBytes(8).toString("hex")}`;
|
|
234
|
+
const dir = path.join(claimed, id);
|
|
235
|
+
fs.mkdirSync(dir);
|
|
236
|
+
// The shared queue lock joins the producer's open/write/close before this
|
|
237
|
+
// immutable snapshot is read. Keep the producer directory stable too.
|
|
238
|
+
try {
|
|
239
|
+
fs.renameSync(path.join(pending, "inbox.jsonl"), path.join(dir, "inbox.jsonl"));
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (error.code !== "ENOENT")
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const claims = [];
|
|
247
|
+
const messages = [];
|
|
248
|
+
const ordered = fs.readdirSync(claimed).sort((a, b) => {
|
|
249
|
+
const left = /^(\d{1,16})-/.exec(a)?.[1];
|
|
250
|
+
const right = /^(\d{1,16})-/.exec(b)?.[1];
|
|
251
|
+
if (left && right && BigInt(left) !== BigInt(right))
|
|
252
|
+
return BigInt(left) < BigInt(right) ? -1 : 1;
|
|
253
|
+
return a.localeCompare(b);
|
|
254
|
+
});
|
|
255
|
+
for (const id of ordered) {
|
|
256
|
+
if (!safeId(id))
|
|
257
|
+
continue;
|
|
258
|
+
const dir = path.join(claimed, id);
|
|
259
|
+
if (fs.existsSync(path.join(dir, ".consumed")))
|
|
260
|
+
continue;
|
|
261
|
+
let text;
|
|
262
|
+
try {
|
|
263
|
+
text = fs.readFileSync(path.join(dir, "inbox.jsonl"), "utf8");
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
if (error.code === "ENOENT")
|
|
267
|
+
continue;
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
const rows = [];
|
|
271
|
+
const invalidRows = [];
|
|
272
|
+
text.split(/\r?\n/).forEach((line, index) => {
|
|
273
|
+
if (!line)
|
|
274
|
+
return;
|
|
275
|
+
try {
|
|
276
|
+
const row = JSON.parse(line);
|
|
277
|
+
if (!row || typeof row !== "object" || typeof row.text !== "string")
|
|
278
|
+
throw new Error("invalid row");
|
|
279
|
+
rows.push(row.text);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
invalidRows.push(index + 1);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
if (invalidRows.length) {
|
|
286
|
+
// The immutable original is the quarantine evidence. Record positions,
|
|
287
|
+
// never malformed contents, and retain both even after valid rows' ACK.
|
|
288
|
+
const audit = path.join(dir, "invalid-rows.json");
|
|
289
|
+
const expectedAudit = JSON.stringify({ version: 1, rows: invalidRows }) + "\n";
|
|
290
|
+
let previousAudit;
|
|
291
|
+
try {
|
|
292
|
+
previousAudit = fs.readFileSync(audit, "utf8");
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (error.code !== "ENOENT")
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
if (previousAudit !== expectedAudit) {
|
|
299
|
+
const temporary = `${audit}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
300
|
+
try {
|
|
301
|
+
const fd = fs.openSync(temporary, "wx", 0o600);
|
|
302
|
+
try {
|
|
303
|
+
fs.writeFileSync(fd, expectedAudit);
|
|
304
|
+
fs.fsyncSync(fd);
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
fs.closeSync(fd);
|
|
308
|
+
}
|
|
309
|
+
fs.renameSync(temporary, audit);
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
fs.rmSync(temporary, { force: true });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Retry all durability barriers even if a previous attempt wrote the
|
|
316
|
+
// audit but failed before publishing its directory entry durably.
|
|
317
|
+
for (const file of [path.join(dir, "inbox.jsonl"), audit]) {
|
|
318
|
+
const fd = fs.openSync(file, "r");
|
|
319
|
+
try {
|
|
320
|
+
fs.fsyncSync(fd);
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
fs.closeSync(fd);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
for (const directory of [dir, claimed]) {
|
|
327
|
+
try {
|
|
328
|
+
const fd = fs.openSync(directory, "r");
|
|
329
|
+
try {
|
|
330
|
+
fs.fsyncSync(fd);
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
fs.closeSync(fd);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
// Same known unsupported directory-flush codes as lanes store.
|
|
338
|
+
// File flush errors and unknown directory errors remain fatal.
|
|
339
|
+
if (!["EISDIR", "EINVAL", "ENOTSUP"].includes(error.code ?? ""))
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (!rows.length)
|
|
345
|
+
continue;
|
|
346
|
+
claims.push(id);
|
|
347
|
+
messages.push(...rows);
|
|
348
|
+
}
|
|
349
|
+
return { messages: messages.join("\n"), claims };
|
|
350
|
+
}
|
|
351
|
+
function claimLinkQueue(inbox) {
|
|
352
|
+
return withLinkQueueLock(inbox, () => claimLinkQueueUnlocked(inbox));
|
|
353
|
+
}
|
|
354
|
+
function acknowledgeLinkClaims(inbox, claims) {
|
|
355
|
+
return withLinkQueueLock(inbox, () => acknowledgeLinkClaimsUnlocked(inbox, claims));
|
|
356
|
+
}
|
|
357
|
+
function acknowledgeLinkClaimsUnlocked(inbox, claims) {
|
|
358
|
+
if (!Array.isArray(claims) || !claims.length || claims.some(id => typeof id !== "string" || !safeId(id))) {
|
|
359
|
+
throw new LaneLinkError("claim_invalid", "An exact nonempty claim receipt is required");
|
|
360
|
+
}
|
|
361
|
+
// Validate the entire receipt before changing any claim. Never resolve paths
|
|
362
|
+
// outside the current session's authoritative queue.
|
|
363
|
+
for (const id of claims) {
|
|
364
|
+
if (!fs.existsSync(path.join(inbox, "unaccepted", id, "inbox.jsonl"))) {
|
|
365
|
+
throw new LaneLinkError("claim_not_found", "Claim does not belong to this queue");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const id of claims) {
|
|
369
|
+
fs.writeFileSync(path.join(inbox, "unaccepted", id, ".consumed"), "recipient-accepted\n", { mode: 0o600 });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function countQuarantined(inbox) {
|
|
373
|
+
const claims = path.join(inbox, "unaccepted");
|
|
374
|
+
if (!fs.existsSync(claims))
|
|
375
|
+
return 0;
|
|
376
|
+
return fs.readdirSync(claims).filter(id => safeId(id) && fs.existsSync(path.join(claims, id, "invalid-rows.json"))).length;
|
|
377
|
+
}
|
|
193
378
|
function countPending(inbox) {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
379
|
+
const paths = [path.join(inbox, "pending", "inbox.jsonl")];
|
|
380
|
+
for (const name of ["claimed", "unaccepted"]) {
|
|
381
|
+
const claimed = path.join(inbox, name);
|
|
382
|
+
if (fs.existsSync(claimed))
|
|
383
|
+
for (const id of fs.readdirSync(claimed)) {
|
|
384
|
+
if (!fs.existsSync(path.join(claimed, id, ".consumed")))
|
|
385
|
+
paths.push(path.join(claimed, id, "inbox.jsonl"));
|
|
386
|
+
}
|
|
197
387
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
388
|
+
let count = 0;
|
|
389
|
+
for (const file of paths) {
|
|
390
|
+
try {
|
|
391
|
+
count += fs.readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean).length;
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (error.code !== "ENOENT")
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
202
397
|
}
|
|
398
|
+
return count;
|
|
203
399
|
}
|
|
204
400
|
function readText(options) {
|
|
205
401
|
if (options.textFile) {
|
|
@@ -244,7 +440,7 @@ function listChildren(root, dir) {
|
|
|
244
440
|
if (!fs.existsSync(metaPath))
|
|
245
441
|
return [];
|
|
246
442
|
const unread = countPending(path.join(cdir, "inbox"));
|
|
247
|
-
return [{ ...readJson(metaPath), undelivered: unread }];
|
|
443
|
+
return [{ ...readJson(metaPath), undelivered: unread, quarantined_claims: countQuarantined(path.join(cdir, "inbox")) }];
|
|
248
444
|
});
|
|
249
445
|
}
|
|
250
446
|
async function waitForReport(dir, timeoutSeconds, sleep) {
|
|
@@ -288,6 +484,7 @@ export async function runLaneLink(subcommand, options) {
|
|
|
288
484
|
const root = options.hqRoot;
|
|
289
485
|
let sid = "";
|
|
290
486
|
let childContext = false;
|
|
487
|
+
let stopHookActive = false;
|
|
291
488
|
if (subcommand === "_deliver") {
|
|
292
489
|
let payload = {};
|
|
293
490
|
try {
|
|
@@ -299,6 +496,7 @@ export async function runLaneLink(subcommand, options) {
|
|
|
299
496
|
const value = payload.session_id ?? payload.sessionId;
|
|
300
497
|
sid = typeof value === "string" && safeId(value) ? value : "";
|
|
301
498
|
childContext = isChildHookPayload(payload);
|
|
499
|
+
stopHookActive = payload.stop_hook_active === true || payload.stopHookActive === true;
|
|
302
500
|
}
|
|
303
501
|
const requireSid = () => sid || (sid = resolveLinkSessionId(root, options.sessionId, env));
|
|
304
502
|
const links = rootDir(root);
|
|
@@ -307,21 +505,41 @@ export async function runLaneLink(subcommand, options) {
|
|
|
307
505
|
// drain the root's queue into the subagent, so leave the message queued.
|
|
308
506
|
if (!sid || childContext || !["PostToolUse", "Stop"].includes(options.event ?? ""))
|
|
309
507
|
return {};
|
|
310
|
-
if (options.event === "Stop" && env.HQ_STOP_DECISION_DELIVERABLE === "0")
|
|
508
|
+
if (options.event === "Stop" && (stopHookActive || env.HQ_STOP_DECISION_DELIVERABLE === "0"))
|
|
311
509
|
return {};
|
|
312
510
|
const pointer = readPointer(root, sid);
|
|
313
511
|
if (!pointer)
|
|
314
512
|
return {};
|
|
315
513
|
const target = hookMessage(root, pointer);
|
|
316
|
-
|
|
514
|
+
if (options.ackMode !== undefined && options.ackMode !== "explicit")
|
|
515
|
+
throw new LaneLinkError("ack_mode_invalid", "Unsupported acknowledgement mode");
|
|
516
|
+
const claim = options.ackMode === "explicit" ? claimLinkQueue(target.inbox) : undefined;
|
|
517
|
+
const messages = claim ? claim.messages : drainLinkQueue(target.inbox);
|
|
317
518
|
if (!messages)
|
|
318
519
|
return {};
|
|
319
|
-
const
|
|
520
|
+
const receipt = claim ? { _hqLinkDelivery: { version: 1, sessionId: sid, link: pointer.link, child: pointer.name ?? null, claims: claim.claims } } : {};
|
|
521
|
+
const acceptance = claim ? `\n\n[conduct receipt] Stable instruction receipt IDs: ${claim.claims.join(",")}. Treat replayed IDs as the same instruction, not new execution authority. After accepting these instructions, acknowledge receipt once with: hq lanes link ack --session-id ${sid} --link ${pointer.link}${pointer.name ? ` --child ${pointer.name}` : ""} --claims ${claim.claims.join(",")}. Receipt acceptance does not approve actions, answer owner pickers, or mean work is completed.` : "";
|
|
522
|
+
const body = (target.role === "conductor"
|
|
320
523
|
? `[conduct] Sessions you are conducting reported while you were working.\nEach report names the session it came from. Relay what the operator needs to know, and answer a session with: hq lanes link send --child <name> --text '...'\n\n${messages}`
|
|
321
|
-
: `[conduct] The session conducting you sent this while you were working.\nTreat it as a new instruction from the operator, taking precedence over your current task where the two conflict. When you have something to tell the conductor - a result, a blocker, a question - send it with: hq lanes link report --state <working|blocked|idle|done> --text '...'\n\n${messages}
|
|
524
|
+
: `[conduct] The session conducting you sent this while you were working.\nTreat it as a new instruction from the operator, taking precedence over your current task where the two conflict. When you have something to tell the conductor - a result, a blocker, a question - send it with: hq lanes link report --state <working|blocked|idle|done> --text '...'\n\n${messages}`) + acceptance;
|
|
322
525
|
return options.event === "PostToolUse"
|
|
323
|
-
? { output: { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: body } } }
|
|
324
|
-
: { output: { decision: "block", reason: body } };
|
|
526
|
+
? { output: { ...receipt, hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: body } } }
|
|
527
|
+
: { output: { ...receipt, decision: "block", reason: body } };
|
|
528
|
+
}
|
|
529
|
+
if (subcommand === "capabilities") {
|
|
530
|
+
return { output: { protocol: "conduct-recipient-acceptance/1", explicitClaim: true, recipientAck: true, idleWake: false } };
|
|
531
|
+
}
|
|
532
|
+
if (subcommand === "ack") {
|
|
533
|
+
const currentSid = requireSid();
|
|
534
|
+
const pointer = readPointer(root, currentSid);
|
|
535
|
+
if (!pointer)
|
|
536
|
+
throw new LaneLinkError("session_unlinked", "Session is not linked");
|
|
537
|
+
if (!options.link || options.link !== pointer.link || (options.child ?? null) !== (pointer.name ?? null)) {
|
|
538
|
+
throw new LaneLinkError("claim_owner_invalid", "Receipt does not match the current link/child binding");
|
|
539
|
+
}
|
|
540
|
+
const claims = options.claims?.split(",");
|
|
541
|
+
acknowledgeLinkClaims(hookMessage(root, pointer).inbox, claims);
|
|
542
|
+
return { output: { acknowledged: true, acceptance: "recipient_instruction_only" } };
|
|
325
543
|
}
|
|
326
544
|
if (subcommand === "open") {
|
|
327
545
|
const sessionId = requireSid();
|
|
@@ -438,7 +656,7 @@ export async function runLaneLink(subcommand, options) {
|
|
|
438
656
|
const dir = linkPath(root, link);
|
|
439
657
|
const meta = readJson(path.join(dir, "meta.json"));
|
|
440
658
|
const unread = countPending(path.join(dir, "up", "inbox"));
|
|
441
|
-
return { output: { ...meta, unread_reports: unread, children: listChildren(root, dir) } };
|
|
659
|
+
return { output: { ...meta, unread_reports: unread, quarantined_reports: countQuarantined(path.join(dir, "up", "inbox")), children: listChildren(root, dir) } };
|
|
442
660
|
}
|
|
443
661
|
if (subcommand === "leave") {
|
|
444
662
|
const sessionId = requireSid();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type FlagClientOptions, type FlagResolution } from "@indigoai-us/hq-flags-client";
|
|
2
2
|
import type { LaneCompanyRef } from "./types.js";
|
|
3
|
+
import { type PersistedPersonFlagSnapshot } from "../flag-snapshot-file.js";
|
|
3
4
|
export declare const LANES_FLAG_API_DEFAULT_ENDPOINT = "https://hqapi.hq.computer";
|
|
4
5
|
export declare const LANES_FLAG_READ_TIMEOUT_MS = 5000;
|
|
5
6
|
export interface LanesFlagClient {
|
|
@@ -64,5 +65,28 @@ export declare function readLanesRegistryFlagFailOpen(flagKey: string, options?:
|
|
|
64
65
|
*/
|
|
65
66
|
export declare function readLanesRegistryFlagFailOff(flagKey: string, options?: ReadLanesFlagOptions, reader?: typeof readLanesRegistryFlag): Promise<boolean>;
|
|
66
67
|
export declare function _resetThrownFlagReadWarningsForTests(): void;
|
|
68
|
+
/** Offline value for a company-less lanes flag, with how long ago the registry confirmed it. */
|
|
69
|
+
export interface OfflineLanesFlagRead {
|
|
70
|
+
value: boolean;
|
|
71
|
+
ageMs: number;
|
|
72
|
+
source: "local-override" | "lanes-cache" | "person-snapshot";
|
|
73
|
+
}
|
|
74
|
+
export interface ReadLanesFlagOfflineOptions {
|
|
75
|
+
defaultValue?: boolean;
|
|
76
|
+
env?: NodeJS.ProcessEnv;
|
|
77
|
+
/** Last-known-good cache directory; null disables it. Defaults to the HQ state dir. */
|
|
78
|
+
cacheDir?: string | null;
|
|
79
|
+
now?: number;
|
|
80
|
+
/** Person-scope flag snapshot reader. Defaults to the snapshot under the env's cache home. */
|
|
81
|
+
readPersonSnapshot?: () => PersistedPersonFlagSnapshot | null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read a company-less lanes flag without any network request. Order: local
|
|
85
|
+
* override, then the lanes last-known-good cache, then the person flag
|
|
86
|
+
* snapshot other hq commands keep (default registry endpoint only). Returns
|
|
87
|
+
* null when no value was confirmed within LANES_FLAG_CACHE_MAX_AGE_MS; the
|
|
88
|
+
* caller decides the fallback and whether to refresh in the background.
|
|
89
|
+
*/
|
|
90
|
+
export declare function readLanesRegistryFlagOffline(flagKey: string, options?: ReadLanesFlagOfflineOptions): OfflineLanesFlagRead | null;
|
|
67
91
|
export declare function _resetLanesRegistryFlagCacheForTests(): void;
|
|
68
92
|
//# sourceMappingURL=registry-flag.d.ts.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createFlagClient, readLocalOverride, } from "@indigoai-us/hq-flags-client";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { ensureCognitoIdToken, loadCachedTokens, refreshActivePersonTokens, wantsMachineCognitoTokens, } from "../../utils/cognito-session.js";
|
|
5
4
|
import { FLAG_REGISTRY_DEFAULT_ENDPOINT, resolveFlagRegistryEndpoint, } from "../flag-registry-endpoint.js";
|
|
5
|
+
import { readPersonFlagSnapshotFile } from "../flag-snapshot-file.js";
|
|
6
6
|
export const LANES_FLAG_API_DEFAULT_ENDPOINT = FLAG_REGISTRY_DEFAULT_ENDPOINT;
|
|
7
7
|
export const LANES_FLAG_READ_TIMEOUT_MS = 5_000;
|
|
8
8
|
export function readLanesFlagLocalOverride(flagKey, env = process.env) {
|
|
@@ -55,7 +55,7 @@ function readFlagCacheFile(dir) {
|
|
|
55
55
|
return {};
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
function
|
|
58
|
+
function readCachedFlagEntry(dir, key, now = Date.now()) {
|
|
59
59
|
const entry = readFlagCacheFile(dir)[key];
|
|
60
60
|
if (!entry || typeof entry.value !== "boolean" || typeof entry.resolvedAt !== "string")
|
|
61
61
|
return null;
|
|
@@ -66,9 +66,15 @@ function readCachedFlagValue(dir, key, now = Date.now()) {
|
|
|
66
66
|
// A future timestamp (clock skew or a hand-edited file) is not trusted either.
|
|
67
67
|
if (age < 0 || age > LANES_FLAG_CACHE_MAX_AGE_MS)
|
|
68
68
|
return null;
|
|
69
|
-
return entry.value;
|
|
69
|
+
return { value: entry.value, missing: entry.missing === true, ageMs: age };
|
|
70
70
|
}
|
|
71
|
-
function
|
|
71
|
+
function readCachedFlagValue(dir, key, defaultValue) {
|
|
72
|
+
const entry = readCachedFlagEntry(dir, key);
|
|
73
|
+
if (!entry)
|
|
74
|
+
return null;
|
|
75
|
+
return entry.missing ? defaultValue : entry.value;
|
|
76
|
+
}
|
|
77
|
+
function writeCachedFlagValue(dir, key, value, missing = false) {
|
|
72
78
|
let tmp;
|
|
73
79
|
try {
|
|
74
80
|
const entries = readFlagCacheFile(dir);
|
|
@@ -76,7 +82,7 @@ function writeCachedFlagValue(dir, key, value) {
|
|
|
76
82
|
// measures time since the registry last confirmed it. Delete first so the
|
|
77
83
|
// key moves to the end and eviction drops the least recently confirmed.
|
|
78
84
|
delete entries[key];
|
|
79
|
-
entries[key] = { value, resolvedAt: new Date().toISOString() };
|
|
85
|
+
entries[key] = { value, resolvedAt: new Date().toISOString(), ...(missing ? { missing: true } : {}) };
|
|
80
86
|
const keys = Object.keys(entries);
|
|
81
87
|
for (const stale of keys.slice(0, Math.max(0, keys.length - FLAG_CACHE_MAX_ENTRIES)))
|
|
82
88
|
delete entries[stale];
|
|
@@ -237,7 +243,13 @@ function classifyAttempt(attempt) {
|
|
|
237
243
|
return "transient";
|
|
238
244
|
return "permanent";
|
|
239
245
|
}
|
|
246
|
+
// Loaded on first token use: the module costs ~350 ms to import, and offline
|
|
247
|
+
// readers such as the SessionStart hook never need a token.
|
|
248
|
+
function loadCognitoSession() {
|
|
249
|
+
return import("../../utils/cognito-session.js");
|
|
250
|
+
}
|
|
240
251
|
async function defaultRefreshToken() {
|
|
252
|
+
const { ensureCognitoIdToken, loadCachedTokens, refreshActivePersonTokens, wantsMachineCognitoTokens } = await loadCognitoSession();
|
|
241
253
|
if (wantsMachineCognitoTokens()) {
|
|
242
254
|
return ensureCognitoIdToken({ interactive: false });
|
|
243
255
|
}
|
|
@@ -250,6 +262,7 @@ async function defaultRefreshToken() {
|
|
|
250
262
|
function defaultAcquireToken() {
|
|
251
263
|
return async () => {
|
|
252
264
|
try {
|
|
265
|
+
const { ensureCognitoIdToken } = await loadCognitoSession();
|
|
253
266
|
return await ensureCognitoIdToken({ interactive: false });
|
|
254
267
|
}
|
|
255
268
|
catch (error) {
|
|
@@ -388,7 +401,7 @@ async function readRegistryValue(flagKey, endpoint, key, options) {
|
|
|
388
401
|
// 4xx, and a missing token mean the registry (or the session) refused the
|
|
389
402
|
// read, so it fails closed to the caller's default instead.
|
|
390
403
|
const transient = classifyAttempt(attempt) === "transient";
|
|
391
|
-
const cachedValue = transient && cacheDir ? readCachedFlagValue(cacheDir, key) : null;
|
|
404
|
+
const cachedValue = transient && cacheDir ? readCachedFlagValue(cacheDir, key, options.defaultValue ?? false) : null;
|
|
392
405
|
if (cachedValue !== null) {
|
|
393
406
|
return {
|
|
394
407
|
result: { state: "resolved", value: cachedValue },
|
|
@@ -397,8 +410,61 @@ async function readRegistryValue(flagKey, endpoint, key, options) {
|
|
|
397
410
|
}
|
|
398
411
|
return { result: { state: "unknown" }, failure: { ...attempt.failure, diagnostics } };
|
|
399
412
|
}
|
|
413
|
+
// A clean read that fell back means the registry has no row for this flag.
|
|
414
|
+
// Remember that, so offline readers can apply their default without a request.
|
|
415
|
+
if (attempt.resolution?.source === "fallback" && cacheDir)
|
|
416
|
+
writeCachedFlagValue(cacheDir, key, attempt.resolution.value, true);
|
|
400
417
|
return { result: { state: "resolved", value: attempt.resolution?.value ?? false } };
|
|
401
418
|
}
|
|
419
|
+
function defaultPersonSnapshotReader(env, now) {
|
|
420
|
+
return () => {
|
|
421
|
+
// Use only a cache home the env names, so a hermetic env never reads the real home.
|
|
422
|
+
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
|
|
423
|
+
const cacheHome = env.XDG_CACHE_HOME?.trim() || (home ? path.join(home, ".cache") : "");
|
|
424
|
+
if (!cacheHome)
|
|
425
|
+
return null;
|
|
426
|
+
return readPersonFlagSnapshotFile({ env: { ...env, XDG_CACHE_HOME: cacheHome }, now, maxAgeMs: LANES_FLAG_CACHE_MAX_AGE_MS });
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Read a company-less lanes flag without any network request. Order: local
|
|
431
|
+
* override, then the lanes last-known-good cache, then the person flag
|
|
432
|
+
* snapshot other hq commands keep (default registry endpoint only). Returns
|
|
433
|
+
* null when no value was confirmed within LANES_FLAG_CACHE_MAX_AGE_MS; the
|
|
434
|
+
* caller decides the fallback and whether to refresh in the background.
|
|
435
|
+
*/
|
|
436
|
+
export function readLanesRegistryFlagOffline(flagKey, options = {}) {
|
|
437
|
+
const env = options.env ?? process.env;
|
|
438
|
+
const defaultValue = options.defaultValue ?? false;
|
|
439
|
+
const now = options.now ?? Date.now();
|
|
440
|
+
const localOverride = readLanesFlagLocalOverride(flagKey, env);
|
|
441
|
+
if (localOverride !== null)
|
|
442
|
+
return { value: localOverride, ageMs: 0, source: "local-override" };
|
|
443
|
+
const endpoint = resolveFlagRegistryEndpoint(undefined, env);
|
|
444
|
+
const cacheDir = flagCacheDir({ env, ...(options.cacheDir !== undefined ? { cacheDir: options.cacheDir } : {}) });
|
|
445
|
+
const cached = cacheDir ? readCachedFlagEntry(cacheDir, cacheKey(flagKey, null, endpoint), now) : null;
|
|
446
|
+
let snapshot = null;
|
|
447
|
+
if (endpoint === FLAG_REGISTRY_DEFAULT_ENDPOINT) {
|
|
448
|
+
try {
|
|
449
|
+
snapshot = (options.readPersonSnapshot ?? defaultPersonSnapshotReader(env, now))();
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
snapshot = null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const snapshotAge = snapshot ? now - Date.parse(snapshot.fetched_at) : Number.NaN;
|
|
456
|
+
const snapshotUsable = snapshot !== null && Number.isFinite(snapshotAge) && snapshotAge >= 0 &&
|
|
457
|
+
snapshotAge <= LANES_FLAG_CACHE_MAX_AGE_MS;
|
|
458
|
+
// Prefer whichever source the registry confirmed most recently.
|
|
459
|
+
if (cached && (!snapshotUsable || cached.ageMs <= snapshotAge)) {
|
|
460
|
+
return { value: cached.missing ? defaultValue : cached.value, ageMs: cached.ageMs, source: "lanes-cache" };
|
|
461
|
+
}
|
|
462
|
+
if (snapshotUsable) {
|
|
463
|
+
const value = snapshot.flags[flagKey];
|
|
464
|
+
return { value: typeof value === "boolean" ? value : defaultValue, ageMs: snapshotAge, source: "person-snapshot" };
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
402
468
|
function reportRegistryFailure(flagKey, key, outcome, options, failOpen) {
|
|
403
469
|
if (!outcome.failure)
|
|
404
470
|
return;
|
|
@@ -40,9 +40,11 @@ export declare function mintLaneId(deps?: Pick<StoreDeps, "now" | "hostname"> &
|
|
|
40
40
|
export declare function mintQuestionId(deps?: Pick<StoreDeps, "now" | "hostname">): string;
|
|
41
41
|
export declare function appendCappedHistory<T>(history: readonly T[], entry: T, cap?: number): T[];
|
|
42
42
|
/** Test seam: run withRecordLock with overridden deps and a shorter timeout. */
|
|
43
|
-
export declare function withRecordLockForTests<T>(recordPath: string, overrides: Partial<StoreDeps>, fn: () => T, timeoutMs?: number): T;
|
|
43
|
+
export declare function withRecordLockForTests<T>(recordPath: string, overrides: Partial<StoreDeps>, fn: () => T, timeoutMs?: number, reclaimByAge?: boolean): T;
|
|
44
44
|
/** Run a lane-owned file read/modify/write transaction under its sidecar lock. */
|
|
45
|
-
export declare function withLaneRecordFileLock<T>(recordPath: string, fn: () => T
|
|
45
|
+
export declare function withLaneRecordFileLock<T>(recordPath: string, fn: () => T, options?: {
|
|
46
|
+
reclaimByAge?: boolean;
|
|
47
|
+
}): T;
|
|
46
48
|
export declare function parseLaneRecord(raw: unknown, filePath?: string): LaneRecord;
|
|
47
49
|
export declare function parseQuestionRecord(raw: unknown, filePath?: string): QuestionRecord;
|
|
48
50
|
/**
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -183,7 +183,7 @@ function sameFile(a, b) {
|
|
|
183
183
|
* (another process reclaimed it and took a fresh lock in between), the fresh
|
|
184
184
|
* lock is linked back into place and nothing is reclaimed.
|
|
185
185
|
*/
|
|
186
|
-
function recordLockIsStale(lockPath, deps) {
|
|
186
|
+
function recordLockIsStale(lockPath, deps, reclaimByAge = true) {
|
|
187
187
|
const statSync = deps.statSync ?? ((p) => fs.statSync(p));
|
|
188
188
|
const linkSync = deps.linkSync ?? fs.linkSync.bind(fs);
|
|
189
189
|
const before = statSync(lockPath);
|
|
@@ -199,7 +199,7 @@ function recordLockIsStale(lockPath, deps) {
|
|
|
199
199
|
const mtimeAge = now - before.mtimeMs;
|
|
200
200
|
const age = Number.isFinite(ts) ? Math.max(now - ts, mtimeAge) : mtimeAge;
|
|
201
201
|
const stale = (pidKnown && sameHost && pid !== process.pid && !lockOwnerAlive(pid)) ||
|
|
202
|
-
age > lockStaleMs();
|
|
202
|
+
(reclaimByAge && age > lockStaleMs());
|
|
203
203
|
if (!stale)
|
|
204
204
|
return false;
|
|
205
205
|
const parked = `${lockPath}.stale.${process.pid}.${randomBytes(6).toString("hex")}`;
|
|
@@ -243,7 +243,7 @@ function recordLockIsStale(lockPath, deps) {
|
|
|
243
243
|
removeParked();
|
|
244
244
|
return reclaimed;
|
|
245
245
|
}
|
|
246
|
-
function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
246
|
+
function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS, reclaimByAge = true) {
|
|
247
247
|
const lockPath = lockPathFor(recordPath);
|
|
248
248
|
const statSync = deps.statSync ?? ((p) => fs.statSync(p));
|
|
249
249
|
const fstatSync = deps.fstatSync ?? ((fd) => fs.fstatSync(fd));
|
|
@@ -295,7 +295,7 @@ function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
295
295
|
throw new LanesError("store_lock_timeout", `Timed out waiting for lock ${lockPath}`, { path: lockPath });
|
|
296
296
|
}
|
|
297
297
|
try {
|
|
298
|
-
if (recordLockIsStale(lockPath, deps))
|
|
298
|
+
if (recordLockIsStale(lockPath, deps, reclaimByAge))
|
|
299
299
|
continue;
|
|
300
300
|
}
|
|
301
301
|
catch (statErr) {
|
|
@@ -309,12 +309,12 @@ function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
311
|
/** Test seam: run withRecordLock with overridden deps and a shorter timeout. */
|
|
312
|
-
export function withRecordLockForTests(recordPath, overrides, fn, timeoutMs) {
|
|
313
|
-
return withRecordLock(recordPath, { ...defaultStoreDeps(), ...overrides }, fn, timeoutMs);
|
|
312
|
+
export function withRecordLockForTests(recordPath, overrides, fn, timeoutMs, reclaimByAge = true) {
|
|
313
|
+
return withRecordLock(recordPath, { ...defaultStoreDeps(), ...overrides }, fn, timeoutMs, reclaimByAge);
|
|
314
314
|
}
|
|
315
315
|
/** Run a lane-owned file read/modify/write transaction under its sidecar lock. */
|
|
316
|
-
export function withLaneRecordFileLock(recordPath, fn) {
|
|
317
|
-
return withRecordLock(recordPath, defaultStoreDeps(), fn);
|
|
316
|
+
export function withLaneRecordFileLock(recordPath, fn, options = {}) {
|
|
317
|
+
return withRecordLock(recordPath, defaultStoreDeps(), fn, LOCK_TIMEOUT_MS, options.reclaimByAge ?? true);
|
|
318
318
|
}
|
|
319
319
|
function readJsonFile(filePath, deps) {
|
|
320
320
|
let text;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** hq-cli's own bins and the package file each npm launcher runs. */
|
|
2
|
+
export declare const HQ_CLI_BINS: Readonly<Record<string, string>>;
|
|
3
|
+
/**
|
|
4
|
+
* True when `content` is npm's (cmd-shim) PowerShell launcher for the hq-cli
|
|
5
|
+
* bin `bin`. The launcher resolves its own folder, runs node on
|
|
6
|
+
* `$basedir/node_modules/@indigoai-us/hq-cli/<entry>` and exits with node's
|
|
7
|
+
* code. Either slash direction is accepted.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isNpmGeneratedHqCliPs1Shim(content: string, bin: string): boolean;
|
|
10
|
+
export interface Ps1ShimFs {
|
|
11
|
+
readFile: (file: string) => string;
|
|
12
|
+
remove: (file: string) => void;
|
|
13
|
+
}
|
|
14
|
+
/** The npm-generated hq-cli `.ps1` launchers present in `binDir`. */
|
|
15
|
+
export declare function findHqCliPs1Shims(binDir: string, io?: Ps1ShimFs): string[];
|
|
16
|
+
export interface RemovePs1ShimsResult {
|
|
17
|
+
/** Launchers removed by this call. */
|
|
18
|
+
removed: string[];
|
|
19
|
+
/** `.ps1` files with an hq-cli bin name that are not npm's launcher. */
|
|
20
|
+
kept: string[];
|
|
21
|
+
/** Launchers that could not be removed, with the reason. */
|
|
22
|
+
failed: Array<{
|
|
23
|
+
file: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Remove the npm-generated `.ps1` launchers for hq-cli's bins from `binDir`
|
|
29
|
+
* (the npm global prefix on Windows). Safe to run again: a missing file is
|
|
30
|
+
* not an error. Never touches another package's `.ps1` or a hand-written one.
|
|
31
|
+
*/
|
|
32
|
+
export declare function removeHqCliPs1Shims(binDir: string, io?: Ps1ShimFs): RemovePs1ShimsResult;
|
|
33
|
+
/**
|
|
34
|
+
* True when the effective execution policy blocks npm's unsigned launcher.
|
|
35
|
+
* `Undefined` as the effective policy means no scope set one, which Windows
|
|
36
|
+
* client editions treat as `Restricted`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function policyBlocksUnsignedScripts(policy: string): boolean;
|
|
39
|
+
export type ExecutionPolicyRun = () => {
|
|
40
|
+
status: number | null;
|
|
41
|
+
stdout: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Read the current user's effective PowerShell execution policy. Read-only:
|
|
45
|
+
* it runs `Get-ExecutionPolicy` and never sets anything. Null when
|
|
46
|
+
* PowerShell could not be run or printed nothing usable.
|
|
47
|
+
*/
|
|
48
|
+
export declare function readExecutionPolicy(run?: ExecutionPolicyRun): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Postinstall entry: after a global npm install on Windows, remove hq-cli's
|
|
51
|
+
* `.ps1` launchers from the prefix holding `packageDir`. Acts only when the
|
|
52
|
+
* package sits at `<prefix>/node_modules/@indigoai-us/hq-cli`, npm's Windows
|
|
53
|
+
* global layout. Null when it did nothing. npm 11.16+ may skip lifecycle
|
|
54
|
+
* scripts, so `hq doctor` reports any launcher left behind.
|
|
55
|
+
*/
|
|
56
|
+
export declare function removePs1ShimsAfterNpmInstall(packageDir: string, opts: {
|
|
57
|
+
platform: NodeJS.Platform;
|
|
58
|
+
global: boolean;
|
|
59
|
+
}, io?: Ps1ShimFs): RemovePs1ShimsResult | null;
|
|
60
|
+
//# sourceMappingURL=ps1-shims.d.ts.map
|