@indigoai-us/hq-cli 5.108.2 → 5.108.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/commands/index-cmd.js +13 -0
- package/dist/lib/search-index/index.d.ts +53 -0
- package/dist/lib/search-index/index.js +98 -2
- package/dist/main.js +56 -4
- package/dist/utils/qmd-workdir-missing-error.d.ts +18 -0
- package/dist/utils/qmd-workdir-missing-error.js +78 -0
- package/dist/utils/sentry-fingerprint.js +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,7 @@ import { findHqRoot } from '../utils/manifest.js';
|
|
|
5
5
|
import { QMD_NATIVE_BINDING_REMEDY, isQmdNativeBindingError, } from '../utils/qmd-native-binding-error.js';
|
|
6
6
|
import { isQmdStoreMissingError, qmdStoreMissingMessage, } from '../utils/qmd-store-missing-error.js';
|
|
7
7
|
import { isQmdStoreUnopenableError, qmdStoreUnopenableMessage, } from '../utils/qmd-store-unopenable-error.js';
|
|
8
|
+
import { isQmdWorkdirMissingError, qmdWorkdirMissingMessage, } from '../utils/qmd-workdir-missing-error.js';
|
|
8
9
|
const defaults = {
|
|
9
10
|
reconcileCollections,
|
|
10
11
|
deriveCollections,
|
|
@@ -147,6 +148,18 @@ export function registerIndexCommand(program, dependencies = defaults) {
|
|
|
147
148
|
process.stderr.write(`qmd: unusable — ${remedy}\n`);
|
|
148
149
|
process.exitCode = 1;
|
|
149
150
|
}
|
|
151
|
+
else if (isQmdWorkdirMissingError(error)) {
|
|
152
|
+
// The working directory hq handed qmd (the resolved hq root) does not
|
|
153
|
+
// exist, so the spawn failed with ENOENT (HQ-CLI-1A). Like the
|
|
154
|
+
// store-missing/unopenable cases, this diagnostic command should
|
|
155
|
+
// DESCRIBE the broken working directory, not crash on it or send the
|
|
156
|
+
// user to reinstall qmd: print the classified reason + remedy naming
|
|
157
|
+
// the directory and exit 1 without rethrowing. Every OTHER qmd failure
|
|
158
|
+
// keeps propagating.
|
|
159
|
+
const remedy = qmdWorkdirMissingMessage(error) ?? 'its working directory does not exist';
|
|
160
|
+
process.stderr.write(`qmd: unusable — ${remedy}\n`);
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
}
|
|
150
163
|
else {
|
|
151
164
|
throw error;
|
|
152
165
|
}
|
|
@@ -38,6 +38,25 @@ export type QmdInvocation = {
|
|
|
38
38
|
prefixArgs: string[];
|
|
39
39
|
execDir?: string;
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Bounded, hq-DERIVED evidence stamped onto a CAPTURED qmd spawn-level failure
|
|
43
|
+
* (the interpreter-missing and residual QmdBinaryMissingError cases). The
|
|
44
|
+
* reporting host (HQ-CLI-1A) shipped no platform log, so spawnSync's ENOENT
|
|
45
|
+
* could not be pinned to a missing command vs. a missing working directory. The
|
|
46
|
+
* boundary forwards this into a bounded Sentry context so the NEXT occurrence
|
|
47
|
+
* carries the evidence this one lacked. Every field is hq-derived — the errno
|
|
48
|
+
* code, the resolved `cwd`, `process.execPath`, the bundled launcher, and their
|
|
49
|
+
* existence at failure time — never caller argv or query text.
|
|
50
|
+
*/
|
|
51
|
+
export type QmdSpawnDiagnostics = {
|
|
52
|
+
errnoCode: string | null;
|
|
53
|
+
cwd: string | null;
|
|
54
|
+
cwdExists: boolean | null;
|
|
55
|
+
command: string;
|
|
56
|
+
commandExists: boolean;
|
|
57
|
+
launcher: string;
|
|
58
|
+
launcherExists: boolean;
|
|
59
|
+
};
|
|
41
60
|
/** Low-level spawn seam (defaults to `spawnSync`); injected in tests. */
|
|
42
61
|
export type QmdSpawn = (cmd: string, args: string[], options: {
|
|
43
62
|
cwd?: string;
|
|
@@ -59,6 +78,34 @@ export type QmdSpawn = (cmd: string, args: string[], options: {
|
|
|
59
78
|
export declare function withNodeDirOnPath(env: NodeJS.ProcessEnv | undefined, execDir: string | undefined, platform?: NodeJS.Platform): NodeJS.ProcessEnv | undefined;
|
|
60
79
|
export declare class QmdBinaryMissingError extends Error {
|
|
61
80
|
name: string;
|
|
81
|
+
/**
|
|
82
|
+
* Present only when the failure came through the real spawn path (attribution
|
|
83
|
+
* available): the errno code and per-component existence booleans captured at
|
|
84
|
+
* failure time, forwarded to a bounded Sentry context by the boundary so the
|
|
85
|
+
* next occurrence carries the evidence the reported one lacked (HQ-CLI-1A).
|
|
86
|
+
* Absent on the legacy injected-runner path and the resolver's opaque throws,
|
|
87
|
+
* so their existing shapes are unchanged.
|
|
88
|
+
*/
|
|
89
|
+
readonly spawnDiagnostics?: QmdSpawnDiagnostics;
|
|
90
|
+
constructor(message: string, spawnDiagnostics?: QmdSpawnDiagnostics);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* qmd could not be SPAWNED because its working directory does not exist:
|
|
94
|
+
* spawnSync returned ENOENT while chdir-ing into `cwd` (the resolved hq root at
|
|
95
|
+
* every index call site) before it could exec the command. On Linux that ENOENT
|
|
96
|
+
* is byte-identical to a missing-command ENOENT — Node sets `error.path` to the
|
|
97
|
+
* COMMAND either way — so finishRunQmd disambiguates by existence and raises
|
|
98
|
+
* THIS typed carrier only when the working directory itself is absent. Carries
|
|
99
|
+
* the hq-DERIVED `workdir`, the `command` that was spawned, and the bounded
|
|
100
|
+
* `errnoCode`; a missing working directory is the caller's filesystem, so the
|
|
101
|
+
* boundary prints qmdWorkdirMissingMessage and SKIPS capture (HQ-CLI-1A).
|
|
102
|
+
*/
|
|
103
|
+
export declare class QmdWorkdirMissingError extends Error {
|
|
104
|
+
readonly workdir: string;
|
|
105
|
+
readonly command: string;
|
|
106
|
+
readonly errnoCode: string;
|
|
107
|
+
name: string;
|
|
108
|
+
constructor(message: string, workdir: string, command: string, errnoCode: string);
|
|
62
109
|
}
|
|
63
110
|
export declare class QmdExitError extends Error {
|
|
64
111
|
readonly args: string[];
|
|
@@ -469,6 +516,12 @@ export type RunQmdOptions = {
|
|
|
469
516
|
execPath?: string;
|
|
470
517
|
/** Invocation resolver seam (default {@link resolveQmdInvocation}). */
|
|
471
518
|
resolveInvocation?: (options: ResolveQmdInvocationOptions) => QmdInvocation;
|
|
519
|
+
/**
|
|
520
|
+
* Existence-check seam threaded into finishRunQmd's spawn-failure
|
|
521
|
+
* disambiguation (default `fs.existsSync`); injected in tests so the missing
|
|
522
|
+
* cwd / interpreter branches are provable without touching the real disk.
|
|
523
|
+
*/
|
|
524
|
+
exists?: (candidate: string) => boolean;
|
|
472
525
|
};
|
|
473
526
|
/** Run qmd with captured output and typed failures. */
|
|
474
527
|
export declare function runQmd(args: string[], options?: RunQmdOptions): QmdProcessResult;
|
|
@@ -30,6 +30,43 @@ export function withNodeDirOnPath(env, execDir, platform = process.platform) {
|
|
|
30
30
|
}
|
|
31
31
|
export class QmdBinaryMissingError extends Error {
|
|
32
32
|
name = 'QmdBinaryMissingError';
|
|
33
|
+
/**
|
|
34
|
+
* Present only when the failure came through the real spawn path (attribution
|
|
35
|
+
* available): the errno code and per-component existence booleans captured at
|
|
36
|
+
* failure time, forwarded to a bounded Sentry context by the boundary so the
|
|
37
|
+
* next occurrence carries the evidence the reported one lacked (HQ-CLI-1A).
|
|
38
|
+
* Absent on the legacy injected-runner path and the resolver's opaque throws,
|
|
39
|
+
* so their existing shapes are unchanged.
|
|
40
|
+
*/
|
|
41
|
+
spawnDiagnostics;
|
|
42
|
+
constructor(message, spawnDiagnostics) {
|
|
43
|
+
super(message);
|
|
44
|
+
if (spawnDiagnostics)
|
|
45
|
+
this.spawnDiagnostics = spawnDiagnostics;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* qmd could not be SPAWNED because its working directory does not exist:
|
|
50
|
+
* spawnSync returned ENOENT while chdir-ing into `cwd` (the resolved hq root at
|
|
51
|
+
* every index call site) before it could exec the command. On Linux that ENOENT
|
|
52
|
+
* is byte-identical to a missing-command ENOENT — Node sets `error.path` to the
|
|
53
|
+
* COMMAND either way — so finishRunQmd disambiguates by existence and raises
|
|
54
|
+
* THIS typed carrier only when the working directory itself is absent. Carries
|
|
55
|
+
* the hq-DERIVED `workdir`, the `command` that was spawned, and the bounded
|
|
56
|
+
* `errnoCode`; a missing working directory is the caller's filesystem, so the
|
|
57
|
+
* boundary prints qmdWorkdirMissingMessage and SKIPS capture (HQ-CLI-1A).
|
|
58
|
+
*/
|
|
59
|
+
export class QmdWorkdirMissingError extends Error {
|
|
60
|
+
workdir;
|
|
61
|
+
command;
|
|
62
|
+
errnoCode;
|
|
63
|
+
name = 'QmdWorkdirMissingError';
|
|
64
|
+
constructor(message, workdir, command, errnoCode) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.workdir = workdir;
|
|
67
|
+
this.command = command;
|
|
68
|
+
this.errnoCode = errnoCode;
|
|
69
|
+
}
|
|
33
70
|
}
|
|
34
71
|
export class QmdExitError extends Error {
|
|
35
72
|
args;
|
|
@@ -966,7 +1003,7 @@ function describeQmdTermination(stdout, stderr) {
|
|
|
966
1003
|
return '';
|
|
967
1004
|
}
|
|
968
1005
|
/** Normalise a spawn result and raise the typed qmd failures. */
|
|
969
|
-
function finishRunQmd(result, bin, args) {
|
|
1006
|
+
function finishRunQmd(result, bin, args, attribution) {
|
|
970
1007
|
const normalized = {
|
|
971
1008
|
status: result.status,
|
|
972
1009
|
stdout: result.stdout ?? '',
|
|
@@ -975,6 +1012,54 @@ function finishRunQmd(result, bin, args) {
|
|
|
975
1012
|
signal: result.signal,
|
|
976
1013
|
};
|
|
977
1014
|
if (normalized.error) {
|
|
1015
|
+
// A spawn-LEVEL failure: the child could not even be exec'd. When the real
|
|
1016
|
+
// spawn path supplied attribution we can attribute it to the right
|
|
1017
|
+
// component; the legacy injected-runner path passes none and keeps today's
|
|
1018
|
+
// exact class and message.
|
|
1019
|
+
if (attribution) {
|
|
1020
|
+
const spawnError = normalized.error;
|
|
1021
|
+
const errnoCode = typeof spawnError.code === 'string' ? spawnError.code : null;
|
|
1022
|
+
const exists = attribution.exists ?? ((candidate) => fs.existsSync(candidate));
|
|
1023
|
+
const cwd = attribution.cwd ?? null;
|
|
1024
|
+
const cwdExists = cwd !== null ? exists(cwd) : null;
|
|
1025
|
+
const commandExists = exists(attribution.command);
|
|
1026
|
+
const launcherExists = exists(attribution.launcher);
|
|
1027
|
+
// (a) MISSING WORKING DIRECTORY. On Linux spawnSync reports ENOENT with
|
|
1028
|
+
// `error.path` set to the COMMAND both when the command is absent AND when
|
|
1029
|
+
// chdir into `cwd` fails, so this cwd check MUST precede the command check
|
|
1030
|
+
// below — a path-based test alone would keep blaming the interpreter for a
|
|
1031
|
+
// directory that is simply gone (HQ-CLI-1A). The directory is the caller's
|
|
1032
|
+
// filesystem, so it is raised as its own carrier and the boundary skips
|
|
1033
|
+
// capture.
|
|
1034
|
+
if (errnoCode === 'ENOENT' && cwd !== null && cwdExists === false) {
|
|
1035
|
+
throw new QmdWorkdirMissingError(`qmd could not run: its working directory (${cwd}) does not exist`, cwd, attribution.command, errnoCode);
|
|
1036
|
+
}
|
|
1037
|
+
const diagnostics = {
|
|
1038
|
+
errnoCode,
|
|
1039
|
+
cwd,
|
|
1040
|
+
cwdExists,
|
|
1041
|
+
command: attribution.command,
|
|
1042
|
+
commandExists,
|
|
1043
|
+
launcher: attribution.launcher,
|
|
1044
|
+
launcherExists,
|
|
1045
|
+
};
|
|
1046
|
+
// (b) MISSING INTERPRETER. The bundled qmd is launched as `<node> <qmd
|
|
1047
|
+
// launcher>`; when the interpreter (`command`, distinct from the launcher)
|
|
1048
|
+
// is what could not be executed, name IT rather than the launcher the
|
|
1049
|
+
// reported message wrongly blamed. Still captured — an absent interpreter
|
|
1050
|
+
// is a genuine defect — but now correct and self-describing.
|
|
1051
|
+
if (errnoCode === 'ENOENT' &&
|
|
1052
|
+
attribution.command !== attribution.launcher &&
|
|
1053
|
+
commandExists === false) {
|
|
1054
|
+
throw new QmdBinaryMissingError(`Unable to execute qmd: its Node interpreter (${attribution.command}) could not be executed: ${normalized.error.message}`, diagnostics);
|
|
1055
|
+
}
|
|
1056
|
+
// (c) RESIDUAL: an ENOENT the fix cannot attribute (a genuinely missing
|
|
1057
|
+
// qmd, or a race where the directory/interpreter reappeared) or any other
|
|
1058
|
+
// spawn errno (EACCES/EINVAL/…). Keep today's class and message byte-for-
|
|
1059
|
+
// byte, and attach the bounded diagnostics so the next occurrence carries
|
|
1060
|
+
// the per-component existence evidence.
|
|
1061
|
+
throw new QmdBinaryMissingError(`Unable to execute qmd at ${bin}: ${normalized.error.message}`, diagnostics);
|
|
1062
|
+
}
|
|
978
1063
|
throw new QmdBinaryMissingError(`Unable to execute qmd at ${bin}: ${normalized.error.message}`);
|
|
979
1064
|
}
|
|
980
1065
|
if (normalized.status === 0)
|
|
@@ -1157,13 +1242,24 @@ export function runQmd(args, options = {}) {
|
|
|
1157
1242
|
// idempotent; only the real spawn path needs it (the injected-runner path
|
|
1158
1243
|
// above never opens a store).
|
|
1159
1244
|
ensureQmdStoreDir(options.env ?? process.env, { cwd: options.cwd });
|
|
1245
|
+
const launcher = invocationBin(invocation);
|
|
1160
1246
|
const result = spawnQmd(invocation, args, {
|
|
1161
1247
|
cwd: options.cwd,
|
|
1162
1248
|
env: options.env ?? process.env,
|
|
1163
1249
|
platform: options.platform,
|
|
1164
1250
|
spawn: options.spawn,
|
|
1165
1251
|
});
|
|
1166
|
-
|
|
1252
|
+
// Thread attribution so finishRunQmd can attribute a spawn-level ENOENT to the
|
|
1253
|
+
// right component — the working directory, the Node interpreter, or a
|
|
1254
|
+
// genuinely missing qmd — instead of blaming the launcher for all three
|
|
1255
|
+
// (HQ-CLI-1A). `command` is the process actually spawned; `launcher` is qmd's
|
|
1256
|
+
// file identity.
|
|
1257
|
+
return finishRunQmd(result, launcher, args, {
|
|
1258
|
+
command: invocation.command,
|
|
1259
|
+
launcher,
|
|
1260
|
+
cwd: options.cwd,
|
|
1261
|
+
exists: options.exists,
|
|
1262
|
+
});
|
|
1167
1263
|
}
|
|
1168
1264
|
function containsIndexedMarkdown(directory) {
|
|
1169
1265
|
if (!fs.existsSync(directory))
|
package/dist/main.js
CHANGED
|
@@ -75,6 +75,7 @@ import { qmdStoreMissingMessage } from "./utils/qmd-store-missing-error.js";
|
|
|
75
75
|
import { qmdStoreUnopenableMessage } from "./utils/qmd-store-unopenable-error.js";
|
|
76
76
|
import { qmdQueryDocumentMessage } from "./utils/qmd-query-document-error.js";
|
|
77
77
|
import { qmdModelDownloadMessage } from "./utils/qmd-model-download-error.js";
|
|
78
|
+
import { qmdWorkdirMissingMessage } from "./utils/qmd-workdir-missing-error.js";
|
|
78
79
|
import { hqStateWriteErrorMessage } from "./utils/hq-state-write-error.js";
|
|
79
80
|
import { isExpectedUserError } from "./utils/expected-cli-error.js";
|
|
80
81
|
import { isVarlockEnvError } from "./run/env-graph-guard.js";
|
|
@@ -379,6 +380,26 @@ const defaultTopLevelErrorDependencies = {
|
|
|
379
380
|
process.exitCode = code;
|
|
380
381
|
},
|
|
381
382
|
};
|
|
383
|
+
/**
|
|
384
|
+
* Bounded Sentry context for a CAPTURED qmd spawn-level failure (the
|
|
385
|
+
* interpreter-missing and residual QmdBinaryMissingError cases). finishRunQmd
|
|
386
|
+
* stamps the errno code and per-component existence booleans onto the error at
|
|
387
|
+
* failure time; because the reporting host (HQ-CLI-1A) shipped no platform log,
|
|
388
|
+
* this is the evidence the next occurrence carries. Read STRUCTURALLY off the
|
|
389
|
+
* error's own `spawnDiagnostics` field (the boundary reconstructs typed carriers
|
|
390
|
+
* structurally in tests). Only hq-DERIVED paths (the resolved cwd,
|
|
391
|
+
* process.execPath, the bundled launcher) and a bounded errno code appear —
|
|
392
|
+
* never caller argv or query text. Returns undefined when the error carries no
|
|
393
|
+
* such diagnostics, so the generic capture stays a bare captureException(err).
|
|
394
|
+
*/
|
|
395
|
+
function qmdSpawnFailureCaptureContext(err) {
|
|
396
|
+
if (err === null || typeof err !== "object")
|
|
397
|
+
return undefined;
|
|
398
|
+
const diagnostics = err.spawnDiagnostics;
|
|
399
|
+
if (diagnostics === null || typeof diagnostics !== "object")
|
|
400
|
+
return undefined;
|
|
401
|
+
return { contexts: { qmd_spawn_failure: diagnostics } };
|
|
402
|
+
}
|
|
382
403
|
/** Classify a top-level failure without making the CLI process boundary opaque to tests. */
|
|
383
404
|
export async function handleTopLevelError(err, deps = defaultTopLevelErrorDependencies) {
|
|
384
405
|
// A broken pipe (EPIPE) means the reader of `hq`'s output closed it early
|
|
@@ -628,6 +649,23 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
|
|
|
628
649
|
const modelDownloadMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || queryDocumentMsg
|
|
629
650
|
? null
|
|
630
651
|
: qmdModelDownloadMessage(err);
|
|
652
|
+
// A qmd spawn-level failure whose ENOENT was a MISSING WORKING DIRECTORY
|
|
653
|
+
// (the resolved hq root the qmd child was handed as `cwd` does not exist).
|
|
654
|
+
// finishRunQmd types it QmdWorkdirMissingError, carrying the hq-derived
|
|
655
|
+
// directory; the boundary prints a self-describing remedy naming that
|
|
656
|
+
// directory and skips capture — a working directory that does not exist is
|
|
657
|
+
// the caller's filesystem, the same disposition as the store-missing /
|
|
658
|
+
// store-unopenable branches. Before this branch it was MISATTRIBUTED to a
|
|
659
|
+
// missing qmd (QmdBinaryMissingError, remedy `Install @tobilu/qmd`) and,
|
|
660
|
+
// with no boundary branch for it, captured as a crash (HQ-CLI-1A, Sentry
|
|
661
|
+
// 7705642434). Evaluated immediately AFTER the store-unopenable /
|
|
662
|
+
// query-document / model-download checks and BEFORE the environmental /
|
|
663
|
+
// transport / generic branches; the carrier's name is disjoint from every
|
|
664
|
+
// neighbour and it carries no `.code` environmentalFsErrorMessage could
|
|
665
|
+
// read, so ordering changes no existing branch.
|
|
666
|
+
const workdirMissingMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || queryDocumentMsg || modelDownloadMsg
|
|
667
|
+
? null
|
|
668
|
+
: qmdWorkdirMissingMessage(err);
|
|
631
669
|
// An hq STATE-TREE write that failed for an environmental errno
|
|
632
670
|
// (permission/quota/read-only/parent-gone) is typed at the write site as an
|
|
633
671
|
// HqStateWriteError carrying the exact hq-DERIVED path it tried to write.
|
|
@@ -637,10 +675,10 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
|
|
|
637
675
|
// remedy and skip capture. Placed with the environmental-fs family — BEFORE
|
|
638
676
|
// the generic envMsg computation — so this typed carrier wins even though
|
|
639
677
|
// it also carries an errno `code` environmentalFsErrorMessage could read.
|
|
640
|
-
const stateWriteMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || queryDocumentMsg || modelDownloadMsg
|
|
678
|
+
const stateWriteMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || queryDocumentMsg || modelDownloadMsg || workdirMissingMsg
|
|
641
679
|
? null
|
|
642
680
|
: hqStateWriteErrorMessage(err);
|
|
643
|
-
const envMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || queryDocumentMsg || modelDownloadMsg || stateWriteMsg
|
|
681
|
+
const envMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || queryDocumentMsg || modelDownloadMsg || workdirMissingMsg || stateWriteMsg
|
|
644
682
|
? null
|
|
645
683
|
: environmentalFsErrorMessage(err);
|
|
646
684
|
// A LOCAL sync-state lock failure (@indigoai-us/hq-cloud's
|
|
@@ -657,7 +695,7 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
|
|
|
657
695
|
// environmental-fs check, before network-transport — is pinned by tests.
|
|
658
696
|
// The `in-process-async-holder` reason is deliberately NOT suppressed here
|
|
659
697
|
// (see sync-state-lock-error.ts); it stays captured.
|
|
660
|
-
const lockMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || queryDocumentMsg || modelDownloadMsg || stateWriteMsg || envMsg
|
|
698
|
+
const lockMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || queryDocumentMsg || modelDownloadMsg || workdirMissingMsg || stateWriteMsg || envMsg
|
|
661
699
|
? null
|
|
662
700
|
: syncStateLockMessage(err);
|
|
663
701
|
// A raw network transport failure (undici's `TypeError: fetch failed`
|
|
@@ -679,6 +717,7 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
|
|
|
679
717
|
storeUnopenableMsg ||
|
|
680
718
|
queryDocumentMsg ||
|
|
681
719
|
modelDownloadMsg ||
|
|
720
|
+
workdirMissingMsg ||
|
|
682
721
|
stateWriteMsg ||
|
|
683
722
|
envMsg ||
|
|
684
723
|
lockMsg
|
|
@@ -711,6 +750,9 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
|
|
|
711
750
|
else if (modelDownloadMsg) {
|
|
712
751
|
deps.stderr.write(`hq: ${modelDownloadMsg}\n`);
|
|
713
752
|
}
|
|
753
|
+
else if (workdirMissingMsg) {
|
|
754
|
+
deps.stderr.write(`hq: ${workdirMissingMsg}\n`);
|
|
755
|
+
}
|
|
714
756
|
else if (stateWriteMsg) {
|
|
715
757
|
deps.stderr.write(`hq: ${stateWriteMsg}\n`);
|
|
716
758
|
}
|
|
@@ -724,7 +766,17 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
|
|
|
724
766
|
deps.stderr.write(`hq: ${transportMsg}\n`);
|
|
725
767
|
}
|
|
726
768
|
else {
|
|
727
|
-
|
|
769
|
+
// A genuinely unclassified fault is still captured exactly once. When it
|
|
770
|
+
// is a qmd spawn-level failure the fix could not attribute, attach the
|
|
771
|
+
// bounded existence context so the next occurrence carries the evidence
|
|
772
|
+
// the reported HQ-CLI-1A event lacked; every other error captures bare.
|
|
773
|
+
const spawnContext = qmdSpawnFailureCaptureContext(err);
|
|
774
|
+
if (spawnContext) {
|
|
775
|
+
deps.sentry.captureException(err, spawnContext);
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
deps.sentry.captureException(err);
|
|
779
|
+
}
|
|
728
780
|
// Always emit something. Printing only when unexpectedCliErrorMessage()
|
|
729
781
|
// returned a value meant every error class except IntegrationsCliError
|
|
730
782
|
// exited 1 with zero bytes on stdout AND stderr — a silent failure the
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when `err` is a qmd working-directory-missing failure: it carries the
|
|
3
|
+
* QmdWorkdirMissingError name and an hq-populated `workdir`. Accepts either the
|
|
4
|
+
* thrown error or a bare `{ name, workdir }` probe object. A true result means
|
|
5
|
+
* the caller should print the classified remedy, exit non-zero, and SKIP Sentry
|
|
6
|
+
* capture.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isQmdWorkdirMissingError(err: unknown): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* If `err` is a qmd working-directory-missing failure, return the actionable
|
|
11
|
+
* remedy; otherwise return `null`. Mirrors qmdStoreMissingMessage /
|
|
12
|
+
* qmdStoreUnopenableMessage so the top-level handler can branch on it the same
|
|
13
|
+
* way: a non-null result means print-and-skip-Sentry, null means "handle as
|
|
14
|
+
* usual". The working directory is read STRUCTURALLY from the error's own
|
|
15
|
+
* hq-populated `workdir` field, NEVER from qmd's output.
|
|
16
|
+
*/
|
|
17
|
+
export declare function qmdWorkdirMissingMessage(err: unknown): string | null;
|
|
18
|
+
//# sourceMappingURL=qmd-workdir-missing-error.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/utils/qmd-workdir-missing-error.ts
|
|
2
|
+
//
|
|
3
|
+
// Classify a qmd spawn failure caused by a MISSING WORKING DIRECTORY — the
|
|
4
|
+
// directory the qmd child was told to run in (`cwd`, always the resolved hq
|
|
5
|
+
// root at the qmd call sites) does not exist — rather than an hq-cli code
|
|
6
|
+
// defect or a genuinely missing qmd. This is the caller's LOCAL filesystem (a
|
|
7
|
+
// deleted tree, a `--hq-root` pointing at a directory that is not there), not a
|
|
8
|
+
// bug HQ can fix in code, so the CLI surfaces an actionable remedy and SKIPS
|
|
9
|
+
// Sentry capture. Sibling of qmd-store-missing-error.ts (HQ-CLI-16, a missing
|
|
10
|
+
// STORE directory) and qmd-store-unopenable-error.ts (HQ-CLI-18): a failure
|
|
11
|
+
// that is NOT an hq-cli defect is printed with an actionable message and never
|
|
12
|
+
// filed as a crash.
|
|
13
|
+
//
|
|
14
|
+
// HQ-CLI-1A (Sentry indigo-d0/hq-cli 7705642434): `hq index status --hq-root
|
|
15
|
+
// /home/ec2-user/hq` spawned the bundled qmd as `<node interpreter> <qmd
|
|
16
|
+
// launcher>`, and spawnSync failed with `spawnSync /usr/bin/node ENOENT`. On
|
|
17
|
+
// Linux, spawnSync reports ENOENT with `path`/`syscall` set to the COMMAND both
|
|
18
|
+
// when the command is missing AND when chdir into `cwd` fails — two distinct
|
|
19
|
+
// causes with byte-identical text. finishRunQmd was handed only the qmd
|
|
20
|
+
// launcher path, so it blamed a THIRD path Node never faulted
|
|
21
|
+
// (`QmdBinaryMissingError: Unable to execute qmd at …/@tobilu/qmd/bin/qmd`) with
|
|
22
|
+
// a remedy (`Install @tobilu/qmd`) that cannot fix either real cause, and — with
|
|
23
|
+
// no boundary branch for it — the misattributed error was captured as a crash.
|
|
24
|
+
// finishRunQmd now disambiguates at the failure site: when the ENOENT is a
|
|
25
|
+
// missing working directory it raises the typed carrier this module classifies.
|
|
26
|
+
//
|
|
27
|
+
// Classification is STRUCTURAL — the typed carrier's own `name` plus its
|
|
28
|
+
// hq-populated `workdir` field, NEVER a text match on qmd's output or the
|
|
29
|
+
// caller's argv — so a search query or a document body can never trip it, and
|
|
30
|
+
// the class need not be imported here (avoiding a cycle with the module that
|
|
31
|
+
// defines it). Reading `name`/`workdir` off the error object also lets the
|
|
32
|
+
// top-level boundary, which reconstructs typed carriers structurally, branch on
|
|
33
|
+
// it the same way it does the sibling qmd errors.
|
|
34
|
+
/**
|
|
35
|
+
* True when `err` is a qmd working-directory-missing failure: it carries the
|
|
36
|
+
* QmdWorkdirMissingError name and an hq-populated `workdir`. Accepts either the
|
|
37
|
+
* thrown error or a bare `{ name, workdir }` probe object. A true result means
|
|
38
|
+
* the caller should print the classified remedy, exit non-zero, and SKIP Sentry
|
|
39
|
+
* capture.
|
|
40
|
+
*/
|
|
41
|
+
export function isQmdWorkdirMissingError(err) {
|
|
42
|
+
if (err === null || typeof err !== "object")
|
|
43
|
+
return false;
|
|
44
|
+
const record = err;
|
|
45
|
+
return (record.name === "QmdWorkdirMissingError" &&
|
|
46
|
+
typeof record.workdir === "string" &&
|
|
47
|
+
record.workdir.length > 0);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The actionable remedy, naming the working directory that does not exist. The
|
|
51
|
+
* directory is an hq-DERIVED value — the resolved `cwd` (the hq root) the qmd
|
|
52
|
+
* child was handed — never caller argv/query or upstream free text, so the line
|
|
53
|
+
* stays input-free. Deliberately does NOT suggest reinstalling qmd (the reported
|
|
54
|
+
* misattribution's dead-end remedy) or `hq index sync` (reconciliation runs the
|
|
55
|
+
* same spawn and would hit the identical failure, a dead-end loop): the only fix
|
|
56
|
+
* is to make the working directory exist.
|
|
57
|
+
*/
|
|
58
|
+
function remedyMessage(workdir) {
|
|
59
|
+
return (`hq's working directory (${workdir}) does not exist, so its local search index ` +
|
|
60
|
+
"could not run there. This is your machine's filesystem, not an hq bug: " +
|
|
61
|
+
"re-create that directory, or re-run with --hq-root pointing at a directory " +
|
|
62
|
+
"that exists.");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* If `err` is a qmd working-directory-missing failure, return the actionable
|
|
66
|
+
* remedy; otherwise return `null`. Mirrors qmdStoreMissingMessage /
|
|
67
|
+
* qmdStoreUnopenableMessage so the top-level handler can branch on it the same
|
|
68
|
+
* way: a non-null result means print-and-skip-Sentry, null means "handle as
|
|
69
|
+
* usual". The working directory is read STRUCTURALLY from the error's own
|
|
70
|
+
* hq-populated `workdir` field, NEVER from qmd's output.
|
|
71
|
+
*/
|
|
72
|
+
export function qmdWorkdirMissingMessage(err) {
|
|
73
|
+
if (!isQmdWorkdirMissingError(err))
|
|
74
|
+
return null;
|
|
75
|
+
const record = err;
|
|
76
|
+
return remedyMessage(record.workdir);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=qmd-workdir-missing-error.js.map
|
|
@@ -86,6 +86,11 @@ const KNOWN_ERROR_NAMES = new Set([
|
|
|
86
86
|
"QmdStoreUnopenableError",
|
|
87
87
|
"QmdQueryDocumentError",
|
|
88
88
|
"QmdModelDownloadError",
|
|
89
|
+
// Shares the `Qmd` prefix the qmd branch keys on. The boundary now SUPPRESSES
|
|
90
|
+
// capture for this class, so it should never reach a capture route — allow-
|
|
91
|
+
// listed defensively so a future capture path cannot mint an unbounded group
|
|
92
|
+
// (HQ-CLI-1A).
|
|
93
|
+
"QmdWorkdirMissingError",
|
|
89
94
|
]);
|
|
90
95
|
/** Fixed bucket for any error name outside the closed allowlist. */
|
|
91
96
|
const FALLBACK_ERROR_NAME = "other";
|