@indigoai-us/hq-cli 5.101.2 → 5.101.3
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 +2 -0
- package/dist/lib/search-index/index.d.ts +31 -1
- package/dist/lib/search-index/index.js +59 -3
- package/dist/main.d.ts +5 -0
- package/dist/main.js +38 -10
- package/dist/sentry.js +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -63,6 +63,22 @@ export declare class QmdExitError extends Error {
|
|
|
63
63
|
export declare class QmdCollectionMissingError extends QmdExitError {
|
|
64
64
|
name: string;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* qmd's `collection add` exited non-zero because the requested name is ALREADY
|
|
68
|
+
* registered in qmd's YAML config under exactly that name — the idempotent,
|
|
69
|
+
* already-satisfied duplicate (qmd 2.5.3 prints `Collection '<name>' already
|
|
70
|
+
* exists.`). `collectionName` carries the parsed name so a caller can confirm
|
|
71
|
+
* the duplicate is the collection it was adding. Distinct from qmd's DIFFERENT
|
|
72
|
+
* path+pattern duplicate ("A collection already exists for this path and
|
|
73
|
+
* pattern:"), which names ANOTHER collection, carries no quoted-name form, and
|
|
74
|
+
* stays a plain {@link QmdExitError} so it keeps failing loud. HQ-CLI-Q
|
|
75
|
+
* (Sentry 7670823899).
|
|
76
|
+
*/
|
|
77
|
+
export declare class QmdCollectionExistsError extends QmdExitError {
|
|
78
|
+
readonly collectionName: string;
|
|
79
|
+
name: string;
|
|
80
|
+
constructor(message: string, args: string[], status: number | null, stdout: string, stderr: string, collectionName: string);
|
|
81
|
+
}
|
|
66
82
|
export type ResolveQmdBinOptions = {
|
|
67
83
|
env?: Record<string, string | undefined>;
|
|
68
84
|
isExecutable?: (candidate: string) => boolean;
|
|
@@ -241,7 +257,21 @@ export type ReconcileCollectionsOptions = {
|
|
|
241
257
|
bin?: string;
|
|
242
258
|
runner?: QmdProcessRunner;
|
|
243
259
|
};
|
|
244
|
-
/**
|
|
260
|
+
/**
|
|
261
|
+
* Register expected collections that qmd does not yet know about, returning the
|
|
262
|
+
* collections this call actually created (a `collection add` that exited 0).
|
|
263
|
+
*
|
|
264
|
+
* A same-name idempotent duplicate — qmd already owns the exact requested name
|
|
265
|
+
* in its YAML config (`Collection '<name>' already exists.`), while the
|
|
266
|
+
* SQLite-backed `collection list` we derive `missing` from had diverged — is
|
|
267
|
+
* NOT a failure. Reconciliation still issues the `context add`, whose qmd-side
|
|
268
|
+
* resyncConfig re-syncs YAML into SQLite and heals the divergence that provoked
|
|
269
|
+
* the retry, then continues with later collections; such a pre-existing
|
|
270
|
+
* collection is excluded from the returned list. Every other qmd failure still
|
|
271
|
+
* propagates: the DIFFERENT path+pattern duplicate, a same-name duplicate that
|
|
272
|
+
* named a different collection, a missing collection, a `context add` failure,
|
|
273
|
+
* a missing/unbuilt qmd binary. HQ-CLI-Q (Sentry 7670823899).
|
|
274
|
+
*/
|
|
245
275
|
export declare function reconcileCollections(hqRoot: string, options?: ReconcileCollectionsOptions): SearchCollection[];
|
|
246
276
|
export declare function listRegisteredCollections(hqRoot: string, options?: RunQmdOptions): Set<string>;
|
|
247
277
|
export {};
|
|
@@ -42,6 +42,25 @@ export class QmdExitError extends Error {
|
|
|
42
42
|
export class QmdCollectionMissingError extends QmdExitError {
|
|
43
43
|
name = 'QmdCollectionMissingError';
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* qmd's `collection add` exited non-zero because the requested name is ALREADY
|
|
47
|
+
* registered in qmd's YAML config under exactly that name — the idempotent,
|
|
48
|
+
* already-satisfied duplicate (qmd 2.5.3 prints `Collection '<name>' already
|
|
49
|
+
* exists.`). `collectionName` carries the parsed name so a caller can confirm
|
|
50
|
+
* the duplicate is the collection it was adding. Distinct from qmd's DIFFERENT
|
|
51
|
+
* path+pattern duplicate ("A collection already exists for this path and
|
|
52
|
+
* pattern:"), which names ANOTHER collection, carries no quoted-name form, and
|
|
53
|
+
* stays a plain {@link QmdExitError} so it keeps failing loud. HQ-CLI-Q
|
|
54
|
+
* (Sentry 7670823899).
|
|
55
|
+
*/
|
|
56
|
+
export class QmdCollectionExistsError extends QmdExitError {
|
|
57
|
+
collectionName;
|
|
58
|
+
name = 'QmdCollectionExistsError';
|
|
59
|
+
constructor(message, args, status, stdout, stderr, collectionName) {
|
|
60
|
+
super(message, args, status, stdout, stderr);
|
|
61
|
+
this.collectionName = collectionName;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
45
64
|
function isExecutable(candidate) {
|
|
46
65
|
try {
|
|
47
66
|
fs.accessSync(candidate, fs.constants.X_OK);
|
|
@@ -643,6 +662,18 @@ function finishRunQmd(result, bin, args) {
|
|
|
643
662
|
if (/(?:collection|qmd:\/\/).*(?:not found|does not exist|unknown)|(?:not found|does not exist).*collection/i.test(detail)) {
|
|
644
663
|
throw new QmdCollectionMissingError(message, args, normalized.status, normalized.stdout, normalized.stderr);
|
|
645
664
|
}
|
|
665
|
+
// qmd's same-name duplicate: `collection add` targeted a name YAML already
|
|
666
|
+
// owns (`Collection '<name>' already exists.`). Reconciliation treats this
|
|
667
|
+
// exact shape as already-satisfied (HQ-CLI-Q). The DIFFERENT path+pattern
|
|
668
|
+
// duplicate ("A collection already exists for this path and pattern:") names
|
|
669
|
+
// another collection and carries no quoted-name form, so it never matches and
|
|
670
|
+
// stays a plain QmdExitError. Colour is off under capture (qmd gates on
|
|
671
|
+
// isTTY) and any surrounding ANSI sits outside this substring, so the plain
|
|
672
|
+
// match is robust either way.
|
|
673
|
+
const alreadyExists = /Collection '([^']+)' already exists\./.exec(detail);
|
|
674
|
+
if (alreadyExists) {
|
|
675
|
+
throw new QmdCollectionExistsError(message, args, normalized.status, normalized.stdout, normalized.stderr, alreadyExists[1]);
|
|
676
|
+
}
|
|
646
677
|
throw new QmdExitError(message, args, normalized.status, normalized.stdout, normalized.stderr);
|
|
647
678
|
}
|
|
648
679
|
/** Run qmd with captured output and typed failures. */
|
|
@@ -741,16 +772,41 @@ export function deriveCollections(hqRoot) {
|
|
|
741
772
|
}
|
|
742
773
|
return collections;
|
|
743
774
|
}
|
|
744
|
-
/**
|
|
775
|
+
/**
|
|
776
|
+
* Register expected collections that qmd does not yet know about, returning the
|
|
777
|
+
* collections this call actually created (a `collection add` that exited 0).
|
|
778
|
+
*
|
|
779
|
+
* A same-name idempotent duplicate — qmd already owns the exact requested name
|
|
780
|
+
* in its YAML config (`Collection '<name>' already exists.`), while the
|
|
781
|
+
* SQLite-backed `collection list` we derive `missing` from had diverged — is
|
|
782
|
+
* NOT a failure. Reconciliation still issues the `context add`, whose qmd-side
|
|
783
|
+
* resyncConfig re-syncs YAML into SQLite and heals the divergence that provoked
|
|
784
|
+
* the retry, then continues with later collections; such a pre-existing
|
|
785
|
+
* collection is excluded from the returned list. Every other qmd failure still
|
|
786
|
+
* propagates: the DIFFERENT path+pattern duplicate, a same-name duplicate that
|
|
787
|
+
* named a different collection, a missing collection, a `context add` failure,
|
|
788
|
+
* a missing/unbuilt qmd binary. HQ-CLI-Q (Sentry 7670823899).
|
|
789
|
+
*/
|
|
745
790
|
export function reconcileCollections(hqRoot, options = {}) {
|
|
746
791
|
const runOptions = { bin: options.bin, runner: options.runner, cwd: hqRoot };
|
|
747
792
|
const registered = listRegisteredCollections(hqRoot, runOptions);
|
|
748
793
|
const missing = deriveCollections(hqRoot).filter((collection) => !registered.has(collection.name));
|
|
794
|
+
const created = [];
|
|
749
795
|
for (const collection of missing) {
|
|
750
|
-
|
|
796
|
+
try {
|
|
797
|
+
runQmd(['collection', 'add', collection.path, '--name', collection.name, '--mask', collection.mask], runOptions);
|
|
798
|
+
created.push(collection);
|
|
799
|
+
}
|
|
800
|
+
catch (error) {
|
|
801
|
+
// Only the exact same-name duplicate for THIS collection is already
|
|
802
|
+
// satisfied. Anything else — including a same-name duplicate that names a
|
|
803
|
+
// different collection — stays fatal.
|
|
804
|
+
if (!(error instanceof QmdCollectionExistsError) || error.collectionName !== collection.name)
|
|
805
|
+
throw error;
|
|
806
|
+
}
|
|
751
807
|
runQmd(['context', 'add', `qmd://${collection.name}`, collection.context], runOptions);
|
|
752
808
|
}
|
|
753
|
-
return
|
|
809
|
+
return created;
|
|
754
810
|
}
|
|
755
811
|
export function listRegisteredCollections(hqRoot, options = {}) {
|
|
756
812
|
const result = runQmd(['collection', 'list'], { ...options, cwd: options.cwd ?? hqRoot });
|
package/dist/main.d.ts
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
import "./node-preflight.js";
|
|
6
6
|
import "./node-network-compat.js";
|
|
7
7
|
import { Sentry } from "./sentry.js";
|
|
8
|
+
export type StreamErrorDependencies = {
|
|
9
|
+
stderr: Pick<typeof process.stderr, "write">;
|
|
10
|
+
exit: (code: number) => void;
|
|
11
|
+
};
|
|
12
|
+
export declare function handleStreamError(err: NodeJS.ErrnoException, deps?: StreamErrorDependencies): void;
|
|
8
13
|
export declare function runCli(): Promise<void>;
|
|
9
14
|
export type TopLevelErrorDependencies = {
|
|
10
15
|
sentry: Pick<typeof Sentry, "captureException">;
|
package/dist/main.js
CHANGED
|
@@ -83,19 +83,47 @@ import { isPackageRootResolutionError, packageRootCaptureContext, } from "./util
|
|
|
83
83
|
import { fallbackOperatorMessage, unexpectedCliErrorMessage } from "./utils/unexpected-cli-error.js";
|
|
84
84
|
/** Hard upper bound for non-user-visible release-health finalization. */
|
|
85
85
|
const RELEASE_HEALTH_SETTLE_TIMEOUT_MS = 3_000;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
|
|
86
|
+
const defaultStreamErrorDependencies = {
|
|
87
|
+
stderr: process.stderr,
|
|
88
|
+
exit: (code) => process.exit(code),
|
|
89
|
+
};
|
|
90
|
+
// Handle an 'error' event on process.stdout/stderr without turning a benign
|
|
91
|
+
// machine-state condition into a Sentry fatal.
|
|
92
|
+
// - EPIPE: a downstream reader closed the pipe early (`hq … | head`,
|
|
93
|
+
// `source <(hq …)`, a parent that exited) — a clean exit 0, no capture. The
|
|
94
|
+
// SYNCHRONOUS path (a `write EPIPE` thrown straight out of console.log) is
|
|
95
|
+
// handled in the top-level catch below; both share `isEpipe` (HQ-6B).
|
|
96
|
+
// - ENOSPC / EDQUOT / EROFS: a full disk, exhausted quota, or read-only mount
|
|
97
|
+
// (e.g. stdout redirected to a file that fills up) — the user's machine,
|
|
98
|
+
// not an hq-cli defect. Print one actionable line (best-effort; the same
|
|
99
|
+
// disk may reject it) and exit 1 WITHOUT rethrowing, so it never becomes an
|
|
100
|
+
// uncaughtException that ships a fatal (HQ-CLI-R). sentry.ts's
|
|
101
|
+
// path-independent beforeSend also drops it, covering the buffered-write
|
|
102
|
+
// route that never surfaces here.
|
|
103
|
+
// - Anything else: rethrow — an unknown stream failure must stay loud.
|
|
104
|
+
// Exported in dependency-injectable form (mirroring handleTopLevelError) so the
|
|
105
|
+
// process boundary stays unit-testable through the ./main.js import seam.
|
|
106
|
+
export function handleStreamError(err, deps = defaultStreamErrorDependencies) {
|
|
92
107
|
if (isEpipe(err)) {
|
|
93
|
-
|
|
108
|
+
deps.exit(0);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const envMessage = environmentalFsErrorMessage(err);
|
|
112
|
+
if (envMessage) {
|
|
113
|
+
try {
|
|
114
|
+
deps.stderr.write(`hq: ${envMessage}\n`);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// The same full disk / read-only mount can reject this write too; there
|
|
118
|
+
// is nothing more to surface, but we still exit non-zero below.
|
|
119
|
+
}
|
|
120
|
+
deps.exit(1);
|
|
121
|
+
return;
|
|
94
122
|
}
|
|
95
123
|
throw err;
|
|
96
|
-
}
|
|
97
|
-
process.stdout.on("error",
|
|
98
|
-
process.stderr.on("error",
|
|
124
|
+
}
|
|
125
|
+
process.stdout.on("error", (err) => handleStreamError(err));
|
|
126
|
+
process.stderr.on("error", (err) => handleStreamError(err));
|
|
99
127
|
initSentry();
|
|
100
128
|
const program = new Command();
|
|
101
129
|
program
|
package/dist/sentry.js
CHANGED
|
@@ -5,6 +5,7 @@ import { beforeBreadcrumb } from "./utils/breadcrumb-buffer.js";
|
|
|
5
5
|
import { CLI_VERSION } from "./cli-version.js";
|
|
6
6
|
import { getCachedSentryUser } from "./utils/sentry-identity.js";
|
|
7
7
|
import { isEpipe } from "./utils/epipe.js";
|
|
8
|
+
import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
|
|
8
9
|
/**
|
|
9
10
|
* Drop broken-pipe (EPIPE) crashes before scrubbing/send. A closed downstream
|
|
10
11
|
* reader (`hq … | head`, `source <(hq …)`, a parent that exited) is normal
|
|
@@ -18,6 +19,17 @@ import { isEpipe } from "./utils/epipe.js";
|
|
|
18
19
|
export function epipeAwareBeforeSend(event, hint) {
|
|
19
20
|
if (isEpipe(hint?.originalException))
|
|
20
21
|
return null;
|
|
22
|
+
// Path-independent belt for environmental filesystem failures (ENOSPC /
|
|
23
|
+
// EDQUOT / EROFS — a full disk, exhausted quota, or read-only mount, e.g.
|
|
24
|
+
// stdout redirected to a file that fills up). These are the user's machine,
|
|
25
|
+
// never an hq-cli defect, and can surface by several routes: a synchronous
|
|
26
|
+
// throw through the top-level boundary, a rethrow from the stdout/stderr
|
|
27
|
+
// 'error' listener, or a buffered-write uncaughtException that never reaches
|
|
28
|
+
// either. handleTopLevelError already prints an actionable message for the
|
|
29
|
+
// boundary route; dropping the event here suppresses the fatal regardless of
|
|
30
|
+
// route, while the CLI still exits non-zero. HQ-CLI-R (Sentry 7671416365).
|
|
31
|
+
if (environmentalFsErrorMessage(hint?.originalException))
|
|
32
|
+
return null;
|
|
21
33
|
return beforeSend(event, hint);
|
|
22
34
|
}
|
|
23
35
|
export function initSentry() {
|