@indigoai-us/hq-cli 5.345.135 → 5.345.137
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 +44 -0
- package/assets/scaffold/core/scripts/worktree-gc.sh +98 -8
- package/dist/command-catalog.generated.d.ts +17 -6
- package/dist/command-catalog.generated.js +23 -7
- package/dist/commands/core-checkpoint-native.d.ts +10 -0
- package/dist/commands/core-checkpoint-native.js +60 -15
- package/dist/commands/daemon.js +3 -1
- package/dist/commands/integrations-core.d.ts +22 -0
- package/dist/commands/integrations-core.js +25 -0
- package/dist/commands/integrations-manage.d.ts +24 -1
- package/dist/commands/integrations-manage.js +92 -17
- package/dist/commands/integrations.d.ts +11 -0
- package/dist/commands/integrations.js +100 -19
- package/dist/commands/lanes-loop.d.ts +9 -0
- package/dist/commands/lanes-loop.js +9 -1
- package/dist/commands/lanes.js +69 -54
- package/dist/commands/meetings.d.ts +2 -0
- package/dist/commands/meetings.js +57 -14
- package/dist/commands/mesh.d.ts +19 -1
- package/dist/commands/mesh.js +158 -70
- package/dist/commands/reindex.js +80 -5
- package/dist/commands/storage-offload.d.ts +93 -28
- package/dist/commands/storage-offload.js +290 -34
- package/dist/commands/storage.js +60 -7
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2550 -1748
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +4 -0
- package/dist/lib/capacity/route.js +24 -0
- package/dist/lib/core-utils/session-company.d.ts +11 -0
- package/dist/lib/core-utils/session-company.js +34 -0
- package/dist/lib/core-utils/worktree-resume.js +44 -4
- package/dist/lib/core-utils/worktree-session-owner.d.ts +27 -0
- package/dist/lib/core-utils/worktree-session-owner.js +157 -0
- package/dist/lib/daemon/catalog.d.ts +2 -0
- package/dist/lib/daemon/catalog.js +3 -3
- package/dist/lib/daemon/hq-child-bootstrap-stage.d.ts +5 -0
- package/dist/lib/daemon/hq-child-bootstrap-stage.js +3 -11
- package/dist/lib/daemon/server.js +10 -1
- package/dist/lib/daemon/supervisor.js +6 -0
- package/dist/lib/daemon/sync-runner-bundle.d.ts +5 -0
- package/dist/lib/daemon/sync-runner-bundle.js +4 -12
- package/dist/lib/doctor/checks/claude-wiring.js +2 -1
- package/dist/lib/doctor/fix/apply.d.ts +20 -0
- package/dist/lib/doctor/fix/apply.js +63 -3
- package/dist/lib/doctor/fix/remediation.d.ts +1 -1
- package/dist/lib/doctor/fix/remediation.js +16 -0
- package/dist/lib/doctor/shared.d.ts +11 -1
- package/dist/lib/doctor/shared.js +16 -2
- 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/monitor.js +14 -2
- package/dist/lib/mesh/live/daemon/install.d.ts +12 -0
- package/dist/lib/mesh/live/daemon/install.js +22 -0
- package/dist/lib/mesh/live/flush.js +106 -3
- package/dist/lib/mesh/live/session-events-client.d.ts +9 -0
- package/dist/lib/mesh/live/session-events-client.js +15 -0
- package/dist/lib/work-context/state.d.ts +10 -0
- package/dist/lib/work-context/state.js +5 -0
- package/dist/utils/fsync-directory.d.ts +17 -0
- package/dist/utils/fsync-directory.js +27 -0
- package/dist/utils/windows-hide-default.d.ts +42 -0
- package/dist/utils/windows-hide-default.js +98 -0
- package/package.json +2 -2
|
@@ -354,12 +354,24 @@ const WATCH_RUNTIME_EXECUTABLES = ["bun", "deno", "node", "nodejs", "ts-node", "
|
|
|
354
354
|
// on the raw line, then validate whatever precedes it as an hq invocation.
|
|
355
355
|
// A path form (absolute or ./, ../) is required so an unrelated leading
|
|
356
356
|
// command such as "vim /tmp/hq" is not absorbed into the path.
|
|
357
|
+
//
|
|
358
|
+
// Package managers that install the bin through a shell shim (pnpm, some
|
|
359
|
+
// volta/yarn setups) exec the runtime with the package entrypoint instead of
|
|
360
|
+
// the hq symlink, so the ps line reads
|
|
361
|
+
// "node .../node_modules/@indigoai-us/hq-cli/dist/index.js lanes watch ...".
|
|
362
|
+
// Accept that entrypoint path as an hq executable too.
|
|
363
|
+
const HQ_CLI_ENTRYPOINT_SUFFIX = "/hq-cli/dist/index.js";
|
|
357
364
|
function isHqExecutablePath(candidate) {
|
|
358
365
|
if (candidate === "hq")
|
|
359
366
|
return true;
|
|
360
|
-
if (!/^\.{0,2}\//u.test(candidate)
|
|
367
|
+
if (!/^\.{0,2}\//u.test(candidate))
|
|
368
|
+
return false;
|
|
369
|
+
const isHqBin = path.basename(candidate) === "hq";
|
|
370
|
+
const isHqEntrypoint = candidate
|
|
371
|
+
.replace(/\\/gu, "/")
|
|
372
|
+
.endsWith(HQ_CLI_ENTRYPOINT_SUFFIX);
|
|
373
|
+
if (!isHqBin && !isHqEntrypoint)
|
|
361
374
|
return false;
|
|
362
|
-
}
|
|
363
375
|
return !candidate.split(" ").some((piece) => piece.startsWith("-"));
|
|
364
376
|
}
|
|
365
377
|
function isHqInvocationPrefix(prefix) {
|
|
@@ -121,6 +121,18 @@ export declare function detectPlatform(platform?: NodeJS.Platform): DaemonInstal
|
|
|
121
121
|
* binary when both exist.
|
|
122
122
|
*/
|
|
123
123
|
export declare function preferStableHqBinary(entry: string, platform?: DaemonInstallPlatform, existsSync?: typeof fs.existsSync): string;
|
|
124
|
+
/**
|
|
125
|
+
* A path that `node <path>` can run. preferStableHqBinary maps the package
|
|
126
|
+
* entry to the npm `hq.cmd` shim on Windows, which is right for a task that
|
|
127
|
+
* launches through cmd.exe but wrong for every caller that runs
|
|
128
|
+
* `node <hqBinary>`: node parses the batch file as JavaScript and exits with
|
|
129
|
+
* "SyntaxError: Invalid or unexpected token" on `@ECHO off`
|
|
130
|
+
* (feedback_a60339e8, feedback_516e4427). Map the shim back to the package's
|
|
131
|
+
* JS entry, which sits beside it in the same npm prefix and so stays valid
|
|
132
|
+
* across `npm i -g` updates. Anything that is not a Windows command shim is
|
|
133
|
+
* returned unchanged.
|
|
134
|
+
*/
|
|
135
|
+
export declare function nodeRunnableHqEntry(hqBinary: string, platform?: DaemonInstallPlatform | NodeJS.Platform, existsSync?: typeof fs.existsSync): string;
|
|
124
136
|
/**
|
|
125
137
|
* Absolute path of the CLI entry that is executing this process.
|
|
126
138
|
* realpath of argv[1] only — never a PATH lookup of `hq`.
|
|
@@ -299,6 +299,28 @@ export function preferStableHqBinary(entry, platform = detectPlatform(), existsS
|
|
|
299
299
|
}
|
|
300
300
|
return entry;
|
|
301
301
|
}
|
|
302
|
+
/** JS entry of the hq-cli package, relative to an npm global prefix. */
|
|
303
|
+
const HQ_CLI_PACKAGE_ENTRY = ["node_modules", "@indigoai-us", "hq-cli", "dist", "index.js"];
|
|
304
|
+
/**
|
|
305
|
+
* A path that `node <path>` can run. preferStableHqBinary maps the package
|
|
306
|
+
* entry to the npm `hq.cmd` shim on Windows, which is right for a task that
|
|
307
|
+
* launches through cmd.exe but wrong for every caller that runs
|
|
308
|
+
* `node <hqBinary>`: node parses the batch file as JavaScript and exits with
|
|
309
|
+
* "SyntaxError: Invalid or unexpected token" on `@ECHO off`
|
|
310
|
+
* (feedback_a60339e8, feedback_516e4427). Map the shim back to the package's
|
|
311
|
+
* JS entry, which sits beside it in the same npm prefix and so stays valid
|
|
312
|
+
* across `npm i -g` updates. Anything that is not a Windows command shim is
|
|
313
|
+
* returned unchanged.
|
|
314
|
+
*/
|
|
315
|
+
export function nodeRunnableHqEntry(hqBinary, platform = detectPlatform(), existsSync = fs.existsSync) {
|
|
316
|
+
if (platform !== "win32")
|
|
317
|
+
return hqBinary;
|
|
318
|
+
const ext = path.win32.extname(hqBinary).toLowerCase();
|
|
319
|
+
if (ext !== ".cmd" && ext !== ".bat" && ext !== ".ps1")
|
|
320
|
+
return hqBinary;
|
|
321
|
+
const entry = path.win32.join(path.win32.dirname(hqBinary), ...HQ_CLI_PACKAGE_ENTRY);
|
|
322
|
+
return existsSync(entry) ? entry : hqBinary;
|
|
323
|
+
}
|
|
302
324
|
/**
|
|
303
325
|
* Absolute path of the CLI entry that is executing this process.
|
|
304
326
|
* realpath of argv[1] only — never a PATH lookup of `hq`.
|
|
@@ -22,7 +22,7 @@ import { defaultSleep, FLUSH_MAX_ATTEMPTS, fullJitterDelayMs, } from "./backoff.
|
|
|
22
22
|
import { LOCAL_ONLY_FIELDS, stripLocalOnlyFields } from "./format-spool-line.js";
|
|
23
23
|
import { enrichSessionEndEvent } from "./report-summary.js";
|
|
24
24
|
import { createWorkLocationResolver, withWorkLocation, } from "./work-location.js";
|
|
25
|
-
import { SESSION_EVENTS_BATCH_MAX, WRITABLE_CONTEXT_STATUSES, parseSessionEventsBatchAck, } from "./session-events-client.js";
|
|
25
|
+
import { SESSION_EVENTS_BATCH_MAX, WRITABLE_CONTEXT_STATUSES, isSessionOwnerMismatch, parseSessionEventsBatchAck, SESSION_OWNER_MISMATCH, } from "./session-events-client.js";
|
|
26
26
|
import { appendDeadLetterLine, appendDroppedReceipt, appendHeldLinesIfAbsent, appendSpoolLine, claimHeld, claimSpool, countDistinctHeldEvents, countJsonlLines, recoverOrphanClaims, removeClaimFile, } from "./spool.js";
|
|
27
27
|
import { workMeshHeldPath, workMeshRoot } from "./paths.js";
|
|
28
28
|
/** Bound a replayed session backlog so one flush cannot burst hundreds of events. */
|
|
@@ -306,7 +306,7 @@ export async function flushSessionEvents(inputDeps) {
|
|
|
306
306
|
v: 1,
|
|
307
307
|
sessionId,
|
|
308
308
|
dropped: remainingItems.length,
|
|
309
|
-
reason:
|
|
309
|
+
reason: decision.reason,
|
|
310
310
|
at: now().toISOString(),
|
|
311
311
|
}, deps.workMeshRoot);
|
|
312
312
|
summary.dropped += remainingItems.length;
|
|
@@ -353,6 +353,22 @@ export async function flushSessionEvents(inputDeps) {
|
|
|
353
353
|
accountBatchSuccess(result.body, originalChunk, deps.workMeshRoot, now, summary, markDisposed);
|
|
354
354
|
continue;
|
|
355
355
|
}
|
|
356
|
+
if (!result.retryable && isSessionOwnerMismatch(result.status, result.body)) {
|
|
357
|
+
// Another actor already reports this session (feedback_5b4683c2).
|
|
358
|
+
// Not an error on this machine: drop the session's remaining events
|
|
359
|
+
// with a receipt, remember the answer, and skip it from now on.
|
|
360
|
+
const unsent = remainingItems.filter((item) => pendingRestore.has(item.event));
|
|
361
|
+
dropOwnerMismatchedSession({
|
|
362
|
+
sessionId,
|
|
363
|
+
companyUid: decision.companyUid,
|
|
364
|
+
items: unsent,
|
|
365
|
+
deps,
|
|
366
|
+
now,
|
|
367
|
+
summary,
|
|
368
|
+
markDisposed,
|
|
369
|
+
});
|
|
370
|
+
continue sessionLoop;
|
|
371
|
+
}
|
|
356
372
|
if (!result.retryable && result.status === 404) {
|
|
357
373
|
const rejectedProjectId = decision.projectId;
|
|
358
374
|
updateSessionState(sessionId, deps.workContextRoot, (current) => {
|
|
@@ -560,15 +576,34 @@ function accountBatchSuccess(body, originalChunk, meshRoot, now, summary, markDi
|
|
|
560
576
|
}
|
|
561
577
|
const rejectedIds = new Set();
|
|
562
578
|
const at = now().toISOString();
|
|
579
|
+
let ownerMismatchDropped = 0;
|
|
563
580
|
for (const row of ack.rejected) {
|
|
564
581
|
const item = byId.get(row.eventId);
|
|
565
582
|
if (!item)
|
|
566
583
|
continue;
|
|
584
|
+
if (row.code === SESSION_OWNER_MISMATCH) {
|
|
585
|
+
ownerMismatchDropped += 1;
|
|
586
|
+
summary.dropped += 1;
|
|
587
|
+
rejectedIds.add(row.eventId);
|
|
588
|
+
markDisposed(item.event);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
567
591
|
appendDeadLetterLine(deadLetterEnvelope(item.event, row.code, row.code, at, row.code), meshRoot);
|
|
568
592
|
summary.deadLettered += 1;
|
|
569
593
|
rejectedIds.add(row.eventId);
|
|
570
594
|
markDisposed(item.event);
|
|
571
595
|
}
|
|
596
|
+
if (ownerMismatchDropped > 0) {
|
|
597
|
+
const sessionId = sessionIdOf(originalChunk[0].event);
|
|
598
|
+
appendDroppedReceipt({
|
|
599
|
+
v: 1,
|
|
600
|
+
sessionId,
|
|
601
|
+
dropped: ownerMismatchDropped,
|
|
602
|
+
reason: "owner_mismatch",
|
|
603
|
+
code: SESSION_OWNER_MISMATCH,
|
|
604
|
+
at,
|
|
605
|
+
}, meshRoot);
|
|
606
|
+
}
|
|
572
607
|
const remaining = originalChunk.filter((item) => {
|
|
573
608
|
const id = eventIdOf(item.event);
|
|
574
609
|
return !id || !rejectedIds.has(id);
|
|
@@ -767,6 +802,26 @@ async function postPresenceLeaveIfNeeded(input) {
|
|
|
767
802
|
});
|
|
768
803
|
if (!result.ok) {
|
|
769
804
|
const at = input.now().toISOString();
|
|
805
|
+
if (!result.retryable && isSessionOwnerMismatch(result.status, result.body)) {
|
|
806
|
+
// The old company says another actor owns this session, so there is no
|
|
807
|
+
// presence of ours to leave. Drop the leave events, clear the pending
|
|
808
|
+
// leave, and let the remaining events continue to the new binding.
|
|
809
|
+
dropOwnerMismatchedSession({
|
|
810
|
+
sessionId: input.sessionId,
|
|
811
|
+
companyUid: leave.companyUid,
|
|
812
|
+
items: leaveItems,
|
|
813
|
+
deps: input.deps,
|
|
814
|
+
now: input.now,
|
|
815
|
+
summary: input.summary,
|
|
816
|
+
markDisposed: input.markDisposed,
|
|
817
|
+
clearPresenceLeave: true,
|
|
818
|
+
});
|
|
819
|
+
return {
|
|
820
|
+
state: readSessionState(input.sessionId, input.deps.workContextRoot) ?? input.state,
|
|
821
|
+
items: remaining,
|
|
822
|
+
abort: false,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
770
825
|
if (!result.retryable) {
|
|
771
826
|
for (const item of input.items) {
|
|
772
827
|
appendDeadLetterLine(deadLetterEnvelope(item.event, `HTTP_${result.status}`, result.status, at), input.deps.workMeshRoot);
|
|
@@ -843,12 +898,60 @@ function rebindUnboundSession(sessionId, items, state, deps, nowMs) {
|
|
|
843
898
|
}
|
|
844
899
|
return readSessionState(sessionId, deps.workContextRoot) ?? state;
|
|
845
900
|
}
|
|
901
|
+
const loggedOwnerMismatch = new Set();
|
|
902
|
+
/**
|
|
903
|
+
* Drop a session's events after the server answered 403
|
|
904
|
+
* SESSION_OWNER_MISMATCH: write one dropped receipt, mark the session state so
|
|
905
|
+
* later flushes skip it without posting, and log once per session. These
|
|
906
|
+
* events never reach dead-letter, so daemon health stays clean.
|
|
907
|
+
*/
|
|
908
|
+
function dropOwnerMismatchedSession(input) {
|
|
909
|
+
const at = input.now().toISOString();
|
|
910
|
+
if (input.items.length > 0) {
|
|
911
|
+
appendDroppedReceipt({
|
|
912
|
+
v: 1,
|
|
913
|
+
sessionId: input.sessionId,
|
|
914
|
+
dropped: input.items.length,
|
|
915
|
+
reason: "owner_mismatch",
|
|
916
|
+
code: SESSION_OWNER_MISMATCH,
|
|
917
|
+
companyUid: input.companyUid,
|
|
918
|
+
at,
|
|
919
|
+
}, input.deps.workMeshRoot);
|
|
920
|
+
}
|
|
921
|
+
input.summary.dropped += input.items.length;
|
|
922
|
+
for (const item of input.items)
|
|
923
|
+
input.markDisposed(item.event);
|
|
924
|
+
updateSessionState(input.sessionId, input.deps.workContextRoot, (current) => {
|
|
925
|
+
if (!current)
|
|
926
|
+
return current;
|
|
927
|
+
if (input.clearPresenceLeave) {
|
|
928
|
+
const { presenceLeave: _leave, ...rest } = current;
|
|
929
|
+
return { ...rest, updatedAt: at };
|
|
930
|
+
}
|
|
931
|
+
return {
|
|
932
|
+
...current,
|
|
933
|
+
ownerMismatch: { companyUid: input.companyUid, at },
|
|
934
|
+
updatedAt: at,
|
|
935
|
+
};
|
|
936
|
+
});
|
|
937
|
+
const key = `${input.deps.workContextRoot}\0${input.sessionId}`;
|
|
938
|
+
if (!loggedOwnerMismatch.has(key)) {
|
|
939
|
+
loggedOwnerMismatch.add(key);
|
|
940
|
+
input.deps.log?.(`work-mesh: session ${input.sessionId} is reported by another actor (403 SESSION_OWNER_MISMATCH); dropped ${input.items.length} event(s), not dead-lettered`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
846
943
|
function classifySession(state) {
|
|
847
944
|
if (!state) {
|
|
848
945
|
return { action: "hold", heldReason: "STATE_ABSENT" };
|
|
849
946
|
}
|
|
850
947
|
if (state.contextStatus === "untracked") {
|
|
851
|
-
return { action: "drop" };
|
|
948
|
+
return { action: "drop", reason: "untracked" };
|
|
949
|
+
}
|
|
950
|
+
// The server already told us another actor owns this session in this
|
|
951
|
+
// company; posting again would only earn another 403.
|
|
952
|
+
if (state.ownerMismatch?.companyUid &&
|
|
953
|
+
state.companyUid === state.ownerMismatch.companyUid) {
|
|
954
|
+
return { action: "drop", reason: "owner_mismatch" };
|
|
852
955
|
}
|
|
853
956
|
if (state.contextStatus === "needs_company") {
|
|
854
957
|
return { action: "hold", heldReason: "NEEDS_COMPANY" };
|
|
@@ -41,6 +41,15 @@ export interface SessionEventsBatchAck {
|
|
|
41
41
|
*/
|
|
42
42
|
export declare function parseSessionEventsBatchAck(body: unknown): SessionEventsBatchAck | null;
|
|
43
43
|
export type SessionEventsPoster = (body: SessionEventsBatchBody) => Promise<SessionEventsPostResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Server code for a session that another actor in the same company already
|
|
46
|
+
* reports (for example a local bot's own headless turn, picked up by the
|
|
47
|
+
* person's daemon). The server will never accept this actor's events for that
|
|
48
|
+
* session, so the flush drops them with a receipt instead of dead-lettering.
|
|
49
|
+
*/
|
|
50
|
+
export declare const SESSION_OWNER_MISMATCH = "SESSION_OWNER_MISMATCH";
|
|
51
|
+
/** True for the 403 SESSION_OWNER_MISMATCH answer (feedback_5b4683c2). */
|
|
52
|
+
export declare function isSessionOwnerMismatch(status: number, body: unknown): boolean;
|
|
44
53
|
export declare function classifySessionEventsResponse(status: number, networkError?: boolean): Pick<SessionEventsPostResult, "ok" | "retryable" | "networkError">;
|
|
45
54
|
/**
|
|
46
55
|
* Build a poster that uses vaultApiFetch + Cognito token.
|
|
@@ -65,6 +65,21 @@ export function parseSessionEventsBatchAck(body) {
|
|
|
65
65
|
}
|
|
66
66
|
return { accepted, duplicates, rejected };
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Server code for a session that another actor in the same company already
|
|
70
|
+
* reports (for example a local bot's own headless turn, picked up by the
|
|
71
|
+
* person's daemon). The server will never accept this actor's events for that
|
|
72
|
+
* session, so the flush drops them with a receipt instead of dead-lettering.
|
|
73
|
+
*/
|
|
74
|
+
export const SESSION_OWNER_MISMATCH = "SESSION_OWNER_MISMATCH";
|
|
75
|
+
/** True for the 403 SESSION_OWNER_MISMATCH answer (feedback_5b4683c2). */
|
|
76
|
+
export function isSessionOwnerMismatch(status, body) {
|
|
77
|
+
if (status !== 403)
|
|
78
|
+
return false;
|
|
79
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
80
|
+
return false;
|
|
81
|
+
return body.code === SESSION_OWNER_MISMATCH;
|
|
82
|
+
}
|
|
68
83
|
export function classifySessionEventsResponse(status, networkError = false) {
|
|
69
84
|
if (networkError || status === 0) {
|
|
70
85
|
return { ok: false, retryable: true, networkError: true };
|
|
@@ -117,6 +117,16 @@ export interface SessionStateFile {
|
|
|
117
117
|
taskId?: string;
|
|
118
118
|
bindingEpisodeId?: string;
|
|
119
119
|
};
|
|
120
|
+
/**
|
|
121
|
+
* LOCAL-ONLY. Set when the server answered 403 SESSION_OWNER_MISMATCH for
|
|
122
|
+
* this session in `companyUid`: another actor already reports it (a local
|
|
123
|
+
* bot's own turn, a checkpoint helper). While the session stays bound to
|
|
124
|
+
* that company, flush drops its events locally instead of posting them.
|
|
125
|
+
*/
|
|
126
|
+
ownerMismatch?: {
|
|
127
|
+
companyUid: string;
|
|
128
|
+
at: string;
|
|
129
|
+
};
|
|
120
130
|
updatedAt: string;
|
|
121
131
|
}
|
|
122
132
|
/**
|
|
@@ -215,6 +215,8 @@ function projectSessionState(state) {
|
|
|
215
215
|
out.projectHint = state.projectHint;
|
|
216
216
|
if (state.presenceLeave?.companyUid)
|
|
217
217
|
out.presenceLeave = state.presenceLeave;
|
|
218
|
+
if (state.ownerMismatch?.companyUid)
|
|
219
|
+
out.ownerMismatch = state.ownerMismatch;
|
|
218
220
|
return out;
|
|
219
221
|
}
|
|
220
222
|
/**
|
|
@@ -282,6 +284,9 @@ export function mergeLocalSessionFields(next, prior, options = {}) {
|
|
|
282
284
|
if (!out.presenceLeave && prior.presenceLeave) {
|
|
283
285
|
out.presenceLeave = prior.presenceLeave;
|
|
284
286
|
}
|
|
287
|
+
if (!out.ownerMismatch && prior.ownerMismatch) {
|
|
288
|
+
out.ownerMismatch = prior.ownerMismatch;
|
|
289
|
+
}
|
|
285
290
|
if (out.projectId)
|
|
286
291
|
out.projectHint = undefined;
|
|
287
292
|
const sameBinding = prior.companyUid === out.companyUid &&
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Filesystem seam for {@link fsyncDirectory} tests. */
|
|
2
|
+
export interface DirectorySyncIo {
|
|
3
|
+
openSync: (file: string, flags: number) => number;
|
|
4
|
+
fsyncSync: (fd: number) => void;
|
|
5
|
+
closeSync: (fd: number) => void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Flush a directory entry (a rename or a new child) to disk.
|
|
9
|
+
*
|
|
10
|
+
* Windows has no directory fsync: FlushFileBuffers on a directory handle
|
|
11
|
+
* fails, and Node reports `EPERM: operation not permitted, fsync`. NTFS
|
|
12
|
+
* journals the rename itself, so there is nothing to flush and the call is
|
|
13
|
+
* skipped on win32 (feedback_516e4427). On every other platform a failure
|
|
14
|
+
* still throws, so a POSIX stage keeps its durability check.
|
|
15
|
+
*/
|
|
16
|
+
export declare function fsyncDirectory(directory: string, platform?: NodeJS.Platform, io?: DirectorySyncIo): void;
|
|
17
|
+
//# sourceMappingURL=fsync-directory.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
const defaultIo = {
|
|
3
|
+
openSync: (file, flags) => fs.openSync(file, flags),
|
|
4
|
+
fsyncSync: (fd) => fs.fsyncSync(fd),
|
|
5
|
+
closeSync: (fd) => fs.closeSync(fd),
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Flush a directory entry (a rename or a new child) to disk.
|
|
9
|
+
*
|
|
10
|
+
* Windows has no directory fsync: FlushFileBuffers on a directory handle
|
|
11
|
+
* fails, and Node reports `EPERM: operation not permitted, fsync`. NTFS
|
|
12
|
+
* journals the rename itself, so there is nothing to flush and the call is
|
|
13
|
+
* skipped on win32 (feedback_516e4427). On every other platform a failure
|
|
14
|
+
* still throws, so a POSIX stage keeps its durability check.
|
|
15
|
+
*/
|
|
16
|
+
export function fsyncDirectory(directory, platform = process.platform, io = defaultIo) {
|
|
17
|
+
if (platform === "win32")
|
|
18
|
+
return;
|
|
19
|
+
const fd = io.openSync(directory, fs.constants.O_RDONLY);
|
|
20
|
+
try {
|
|
21
|
+
io.fsyncSync(fd);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
io.closeSync(fd);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=fsync-directory.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `windowsHide: true` for child processes of a background hq on Windows.
|
|
3
|
+
*
|
|
4
|
+
* A console program (node, bash, git, cygpath, claude --version) started from
|
|
5
|
+
* a process that has no console of its own gets a new, visible console
|
|
6
|
+
* window. The HQ desktop app, the sync runner and hooks start hq with piped
|
|
7
|
+
* stdio and no console, so every child hq spawned flashed a window on screen,
|
|
8
|
+
* every minute or two, all day (feedback_351a40d5). Node only hides it when a
|
|
9
|
+
* call passes `windowsHide: true`, and hq-cli has a few hundred spawn sites.
|
|
10
|
+
*
|
|
11
|
+
* When this process is on Windows and none of its standard streams is a
|
|
12
|
+
* terminal, every child_process call that does not choose `windowsHide`
|
|
13
|
+
* itself gets `windowsHide: true`. An interactive hq (any standard stream is
|
|
14
|
+
* a TTY) is left alone, so a child that shares the user's console still does.
|
|
15
|
+
*/
|
|
16
|
+
type ChildProcessModule = typeof import("node:child_process");
|
|
17
|
+
/** child_process entry points whose options object accepts `windowsHide`. */
|
|
18
|
+
export declare const WINDOWS_HIDE_FUNCTIONS: readonly ["spawn", "spawnSync", "execFile", "execFileSync", "exec", "execSync", "fork"];
|
|
19
|
+
export type WindowsHideFunction = (typeof WINDOWS_HIDE_FUNCTIONS)[number];
|
|
20
|
+
/**
|
|
21
|
+
* Return `args` with `windowsHide: true` in the options object of a
|
|
22
|
+
* child_process call, unless the caller set `windowsHide` either way. The
|
|
23
|
+
* input array and the caller's options object are not mutated.
|
|
24
|
+
*/
|
|
25
|
+
export declare function applyWindowsHideDefault(fn: WindowsHideFunction, args: readonly unknown[]): unknown[];
|
|
26
|
+
/** True when none of stdin, stdout and stderr is a terminal. */
|
|
27
|
+
export declare function isBackgroundProcess(isTty?: (fd: number) => boolean): boolean;
|
|
28
|
+
export interface InstallWindowsHideDefaultOptions {
|
|
29
|
+
platform?: NodeJS.Platform;
|
|
30
|
+
background?: boolean;
|
|
31
|
+
/** child_process module to patch (tests pass a stand-in). */
|
|
32
|
+
childProcess?: ChildProcessModule;
|
|
33
|
+
/** Called after patching so ESM named imports see the wrapped functions. */
|
|
34
|
+
syncExports?: () => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Patch node:child_process once for this process. Returns true when the
|
|
38
|
+
* default is (now or already) in force.
|
|
39
|
+
*/
|
|
40
|
+
export declare function installWindowsHideDefault(options?: InstallWindowsHideDefaultOptions): boolean;
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=windows-hide-default.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `windowsHide: true` for child processes of a background hq on Windows.
|
|
3
|
+
*
|
|
4
|
+
* A console program (node, bash, git, cygpath, claude --version) started from
|
|
5
|
+
* a process that has no console of its own gets a new, visible console
|
|
6
|
+
* window. The HQ desktop app, the sync runner and hooks start hq with piped
|
|
7
|
+
* stdio and no console, so every child hq spawned flashed a window on screen,
|
|
8
|
+
* every minute or two, all day (feedback_351a40d5). Node only hides it when a
|
|
9
|
+
* call passes `windowsHide: true`, and hq-cli has a few hundred spawn sites.
|
|
10
|
+
*
|
|
11
|
+
* When this process is on Windows and none of its standard streams is a
|
|
12
|
+
* terminal, every child_process call that does not choose `windowsHide`
|
|
13
|
+
* itself gets `windowsHide: true`. An interactive hq (any standard stream is
|
|
14
|
+
* a TTY) is left alone, so a child that shares the user's console still does.
|
|
15
|
+
*/
|
|
16
|
+
// Only node:module is imported at load time (index.ts already loads it). This
|
|
17
|
+
// module is imported on every hq start, including the hook-time fast routes,
|
|
18
|
+
// so node:tty and node:util are loaded lazily and only on Windows.
|
|
19
|
+
import { createRequire, syncBuiltinESMExports } from "node:module";
|
|
20
|
+
/** child_process entry points whose options object accepts `windowsHide`. */
|
|
21
|
+
export const WINDOWS_HIDE_FUNCTIONS = ["spawn", "spawnSync", "execFile", "execFileSync", "exec", "execSync", "fork"];
|
|
22
|
+
const INSTALLED = Symbol.for("hq.windowsHideDefault.installed");
|
|
23
|
+
/** Same symbol as util.promisify.custom (a registered symbol). */
|
|
24
|
+
const PROMISIFY_CUSTOM = Symbol.for("nodejs.util.promisify.custom");
|
|
25
|
+
const requireBuiltin = createRequire(import.meta.url);
|
|
26
|
+
function defaultIsatty(fd) {
|
|
27
|
+
return requireBuiltin("node:tty").isatty(fd);
|
|
28
|
+
}
|
|
29
|
+
function isOptionsObject(value) {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Return `args` with `windowsHide: true` in the options object of a
|
|
34
|
+
* child_process call, unless the caller set `windowsHide` either way. The
|
|
35
|
+
* input array and the caller's options object are not mutated.
|
|
36
|
+
*/
|
|
37
|
+
export function applyWindowsHideDefault(fn, args) {
|
|
38
|
+
const out = [...args];
|
|
39
|
+
let index;
|
|
40
|
+
if (fn === "exec" || fn === "execSync") {
|
|
41
|
+
index = 1;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// (file, args?, options?, callback?): the args array is optional and may
|
|
45
|
+
// be passed as undefined/null ahead of an options object.
|
|
46
|
+
index = Array.isArray(out[1]) || (out[1] == null && out.length > 2) ? 2 : 1;
|
|
47
|
+
}
|
|
48
|
+
const current = out[index];
|
|
49
|
+
if (isOptionsObject(current)) {
|
|
50
|
+
if ("windowsHide" in current)
|
|
51
|
+
return out;
|
|
52
|
+
out[index] = { ...current, windowsHide: true };
|
|
53
|
+
}
|
|
54
|
+
else if (current == null) {
|
|
55
|
+
out[index] = { windowsHide: true };
|
|
56
|
+
}
|
|
57
|
+
else if (typeof current === "function") {
|
|
58
|
+
out.splice(index, 0, { windowsHide: true });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/** True when none of stdin, stdout and stderr is a terminal. */
|
|
63
|
+
export function isBackgroundProcess(isTty = defaultIsatty) {
|
|
64
|
+
return !isTty(0) && !isTty(1) && !isTty(2);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Patch node:child_process once for this process. Returns true when the
|
|
68
|
+
* default is (now or already) in force.
|
|
69
|
+
*/
|
|
70
|
+
export function installWindowsHideDefault(options = {}) {
|
|
71
|
+
const platform = options.platform ?? process.platform;
|
|
72
|
+
if (platform !== "win32")
|
|
73
|
+
return false;
|
|
74
|
+
if (!(options.background ?? isBackgroundProcess()))
|
|
75
|
+
return false;
|
|
76
|
+
const childProcess = options.childProcess ?? requireBuiltin("node:child_process");
|
|
77
|
+
const target = childProcess;
|
|
78
|
+
if (target[INSTALLED])
|
|
79
|
+
return true;
|
|
80
|
+
for (const name of WINDOWS_HIDE_FUNCTIONS) {
|
|
81
|
+
const original = target[name];
|
|
82
|
+
if (typeof original !== "function")
|
|
83
|
+
continue;
|
|
84
|
+
const originalFn = original;
|
|
85
|
+
const wrapped = function (...args) {
|
|
86
|
+
return originalFn.apply(this, applyWindowsHideDefault(name, args));
|
|
87
|
+
};
|
|
88
|
+
const custom = originalFn[PROMISIFY_CUSTOM];
|
|
89
|
+
if (typeof custom === "function") {
|
|
90
|
+
wrapped[PROMISIFY_CUSTOM] = (...args) => custom(...applyWindowsHideDefault(name, args));
|
|
91
|
+
}
|
|
92
|
+
target[name] = wrapped;
|
|
93
|
+
}
|
|
94
|
+
target[INSTALLED] = true;
|
|
95
|
+
(options.syncExports ?? syncBuiltinESMExports)();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=windows-hide-default.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.137",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
49
49
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
50
50
|
"@aws-sdk/dsql-signer": "^3.1146.0",
|
|
51
|
-
"@indigoai-us/hq-cloud": "~6.18.
|
|
51
|
+
"@indigoai-us/hq-cloud": "~6.18.76",
|
|
52
52
|
"@indigoai-us/hq-flags-client": "^0.1.2",
|
|
53
53
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
54
54
|
"@sentry/node": "^10.49.0",
|