@xaccefy/pi-casefile 0.2.0 → 0.2.1
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/package.json +1 -1
- package/src/index.ts +21 -1
- package/src/ledger.ts +162 -57
- package/src/poc-runner.ts +41 -8
- package/src/sqlite-compat/index.ts +9 -2
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -122,8 +122,15 @@ const ListSchema = Type.Object(
|
|
|
122
122
|
status: Type.Optional(CaseStatusSchema),
|
|
123
123
|
confidence: Type.Optional(CaseConfidenceSchema),
|
|
124
124
|
severity: Type.Optional(CaseSeveritySchema),
|
|
125
|
+
minSeverity: Type.Optional(CaseSeveritySchema),
|
|
125
126
|
priority: Type.Optional(CasePrioritySchema),
|
|
126
127
|
tag: Type.Optional(Type.String({ description: "Filter by tag" })),
|
|
128
|
+
since: Type.Optional(
|
|
129
|
+
Type.String({ description: "ISO timestamp; only cases created at/after this time" }),
|
|
130
|
+
),
|
|
131
|
+
until: Type.Optional(
|
|
132
|
+
Type.String({ description: "ISO timestamp; only cases created at/before this time" }),
|
|
133
|
+
),
|
|
127
134
|
limit: Type.Optional(Type.Number({ description: "Max results (default 50)" })),
|
|
128
135
|
offset: Type.Optional(Type.Number({ description: "Skip N results for pagination" })),
|
|
129
136
|
},
|
|
@@ -146,8 +153,15 @@ const SearchSchema = Type.Object(
|
|
|
146
153
|
status: Type.Optional(CaseStatusSchema),
|
|
147
154
|
confidence: Type.Optional(CaseConfidenceSchema),
|
|
148
155
|
severity: Type.Optional(CaseSeveritySchema),
|
|
156
|
+
minSeverity: Type.Optional(CaseSeveritySchema),
|
|
149
157
|
priority: Type.Optional(CasePrioritySchema),
|
|
150
|
-
tag: Type.Optional(Type.String()),
|
|
158
|
+
tag: Type.Optional(Type.String({ description: "Filter by tag" })),
|
|
159
|
+
since: Type.Optional(
|
|
160
|
+
Type.String({ description: "ISO timestamp; only cases created at/after this time" }),
|
|
161
|
+
),
|
|
162
|
+
until: Type.Optional(
|
|
163
|
+
Type.String({ description: "ISO timestamp; only cases created at/before this time" }),
|
|
164
|
+
),
|
|
151
165
|
limit: Type.Optional(Type.Number()),
|
|
152
166
|
offset: Type.Optional(Type.Number()),
|
|
153
167
|
},
|
|
@@ -725,8 +739,11 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
725
739
|
status: params.status as CaseStatus | undefined,
|
|
726
740
|
confidence: params.confidence as CaseConfidence | undefined,
|
|
727
741
|
severity: params.severity as CaseSeverity | undefined,
|
|
742
|
+
minSeverity: params.minSeverity as CaseSeverity | undefined,
|
|
728
743
|
priority: params.priority as CasePriority | undefined,
|
|
729
744
|
tag: params.tag,
|
|
745
|
+
since: params.since as string | undefined,
|
|
746
|
+
until: params.until as string | undefined,
|
|
730
747
|
limit: params.limit,
|
|
731
748
|
offset: params.offset,
|
|
732
749
|
});
|
|
@@ -772,8 +789,11 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
772
789
|
status: params.status as CaseStatus | undefined,
|
|
773
790
|
confidence: params.confidence as CaseConfidence | undefined,
|
|
774
791
|
severity: params.severity as CaseSeverity | undefined,
|
|
792
|
+
minSeverity: params.minSeverity as CaseSeverity | undefined,
|
|
775
793
|
priority: params.priority as CasePriority | undefined,
|
|
776
794
|
tag: params.tag,
|
|
795
|
+
since: params.since as string | undefined,
|
|
796
|
+
until: params.until as string | undefined,
|
|
777
797
|
limit: params.limit,
|
|
778
798
|
offset: params.offset,
|
|
779
799
|
});
|
package/src/ledger.ts
CHANGED
|
@@ -141,8 +141,14 @@ export type CaseSearchOptions = {
|
|
|
141
141
|
status?: CaseStatus;
|
|
142
142
|
confidence?: CaseConfidence;
|
|
143
143
|
severity?: CaseSeverity;
|
|
144
|
+
/** Return only cases at or above this severity (info < low < medium < high < critical). */
|
|
145
|
+
minSeverity?: CaseSeverity;
|
|
144
146
|
priority?: CasePriority;
|
|
145
147
|
tag?: string;
|
|
148
|
+
/** ISO timestamp; only cases created at/after this time. */
|
|
149
|
+
since?: string;
|
|
150
|
+
/** ISO timestamp; only cases created at/before this time. */
|
|
151
|
+
until?: string;
|
|
146
152
|
limit?: number;
|
|
147
153
|
offset?: number;
|
|
148
154
|
};
|
|
@@ -190,10 +196,15 @@ export function getCasefilePath(): string {
|
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
export function setCasefilePath(path: string | undefined): void {
|
|
193
|
-
ledgerPathOverride = path;
|
|
194
199
|
if (dbInstance) {
|
|
195
|
-
|
|
200
|
+
try {
|
|
201
|
+
dbInstance.close();
|
|
202
|
+
} catch {
|
|
203
|
+
// Best-effort close.
|
|
204
|
+
}
|
|
196
205
|
}
|
|
206
|
+
ledgerPathOverride = path;
|
|
207
|
+
dbInstance = undefined; // Force reconnection on next getDb
|
|
197
208
|
}
|
|
198
209
|
|
|
199
210
|
// ── SQLite Schema Init ────────────────────────────────────────────────
|
|
@@ -210,6 +221,9 @@ function getDb(): DatabaseSync {
|
|
|
210
221
|
}
|
|
211
222
|
|
|
212
223
|
const db = new DatabaseSync(dbPath);
|
|
224
|
+
// Enable foreign-key enforcement so ON DELETE CASCADE actually fires
|
|
225
|
+
// (SQLite keeps FK off by default; bun:sqlite in particular defaults it off).
|
|
226
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
213
227
|
|
|
214
228
|
// Create tables
|
|
215
229
|
db.exec(`
|
|
@@ -353,8 +367,13 @@ export function getCaseById(id: string): CaseRecord | undefined {
|
|
|
353
367
|
|
|
354
368
|
function validateCase(record: CaseRecord): void {
|
|
355
369
|
if (!record.title.trim()) throw new Error("Case title cannot be empty");
|
|
356
|
-
|
|
357
|
-
|
|
370
|
+
// Keep this gate in lockstep with promoteFindingResult: a case may only be
|
|
371
|
+
// CONFIRMED when it has evidence, a PoC, demonstrated impact, and a severity.
|
|
372
|
+
if (
|
|
373
|
+
record.status === "confirmed" &&
|
|
374
|
+
(!record.evidence || !record.poc || !record.impact || !record.severity)
|
|
375
|
+
) {
|
|
376
|
+
throw new Error("Confirmed cases require evidence, poc, impact, and severity");
|
|
358
377
|
}
|
|
359
378
|
if (record.status === "blocked" && (record.blockers ?? []).length === 0) {
|
|
360
379
|
throw new Error("Blocked cases require at least one blocker");
|
|
@@ -370,13 +389,10 @@ function validateCase(record: CaseRecord): void {
|
|
|
370
389
|
"Killed cases require evidence, next step, blockers, or assumptions explaining why",
|
|
371
390
|
);
|
|
372
391
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
(record.references ?? []).length === 0
|
|
378
|
-
) {
|
|
379
|
-
throw new Error("Reported cases require poc, remediation, or references");
|
|
392
|
+
// A case becomes REPORTED only via CaseReport, which records reportPath. Require it
|
|
393
|
+
// here so validation stays consistent with the confirmed→reported transition gate.
|
|
394
|
+
if (record.status === "reported" && !record.reportPath) {
|
|
395
|
+
throw new Error("Reported cases require a generated report (run CaseReport first)");
|
|
380
396
|
}
|
|
381
397
|
}
|
|
382
398
|
|
|
@@ -810,57 +826,138 @@ export function unlinkCasesResult(sourceId: string, targetId: string): CaseLinkR
|
|
|
810
826
|
|
|
811
827
|
// ── Search & Queries ─────────────────────────────────────────────────
|
|
812
828
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
829
|
+
// Searchable text columns (excludes ids/timestamps/JSON arrays for performance + signal).
|
|
830
|
+
const SEARCH_COLUMNS = [
|
|
831
|
+
"title",
|
|
832
|
+
"summary",
|
|
833
|
+
"evidence",
|
|
834
|
+
"impact",
|
|
835
|
+
"target",
|
|
836
|
+
"endpoint",
|
|
837
|
+
"bugClass",
|
|
838
|
+
"poc",
|
|
839
|
+
] as const;
|
|
840
|
+
|
|
841
|
+
const FIELD_COLUMN: Record<CaseSearchField, string> = {
|
|
842
|
+
title: "title",
|
|
843
|
+
summary: "summary",
|
|
844
|
+
evidence: "evidence",
|
|
845
|
+
impact: "impact",
|
|
846
|
+
target: "target",
|
|
847
|
+
endpoint: "endpoint",
|
|
848
|
+
bugClass: "bugClass",
|
|
849
|
+
poc: "poc",
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
function severityRank(s: CaseSeverity): number {
|
|
853
|
+
return SEVERITY_VALUES.indexOf(s);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Build a parameterized WHERE clause + params for case queries. Pushes all
|
|
858
|
+
* structured filters (and free-text) into SQL so we never load the whole ledger
|
|
859
|
+
* into memory just to filter it in JS. Also returns a stable ORDER BY that keeps
|
|
860
|
+
* the original status precedence (hypothesis first) with updated_at as tiebreak.
|
|
861
|
+
*/
|
|
862
|
+
function buildCaseWhere(options: CaseSearchOptions): {
|
|
863
|
+
whereSql: string;
|
|
864
|
+
orderSql: string;
|
|
865
|
+
params: unknown[];
|
|
866
|
+
} {
|
|
867
|
+
const where: string[] = [];
|
|
868
|
+
const params: unknown[] = [];
|
|
869
|
+
|
|
870
|
+
if (options.status) {
|
|
871
|
+
where.push("status = ?");
|
|
872
|
+
params.push(options.status);
|
|
818
873
|
}
|
|
819
|
-
|
|
820
|
-
.
|
|
821
|
-
.
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
.
|
|
874
|
+
if (options.confidence) {
|
|
875
|
+
where.push("confidence = ?");
|
|
876
|
+
params.push(options.confidence);
|
|
877
|
+
}
|
|
878
|
+
if (options.severity) {
|
|
879
|
+
where.push("severity = ?");
|
|
880
|
+
params.push(options.severity);
|
|
881
|
+
}
|
|
882
|
+
if (options.minSeverity) {
|
|
883
|
+
where.push(
|
|
884
|
+
"severity IS NOT NULL AND (CASE severity WHEN 'info' THEN 0 WHEN 'low' THEN 1 WHEN 'medium' THEN 2 WHEN 'high' THEN 3 WHEN 'critical' THEN 4 ELSE -1 END) >= ?",
|
|
885
|
+
);
|
|
886
|
+
params.push(severityRank(options.minSeverity));
|
|
887
|
+
}
|
|
888
|
+
if (options.priority) {
|
|
889
|
+
where.push("priority = ?");
|
|
890
|
+
params.push(options.priority);
|
|
891
|
+
}
|
|
892
|
+
if (options.tag) {
|
|
893
|
+
where.push("EXISTS (SELECT 1 FROM json_each(tags_json) WHERE lower(value) = ?)");
|
|
894
|
+
params.push(options.tag.trim().toLowerCase());
|
|
895
|
+
}
|
|
896
|
+
if (options.since) {
|
|
897
|
+
where.push("created_at >= ?");
|
|
898
|
+
params.push(options.since);
|
|
899
|
+
}
|
|
900
|
+
if (options.until) {
|
|
901
|
+
where.push("created_at <= ?");
|
|
902
|
+
params.push(options.until);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const query = options.query?.trim().toLowerCase();
|
|
906
|
+
if (query) {
|
|
907
|
+
const likeParam = `%${query}%`;
|
|
908
|
+
if (options.field) {
|
|
909
|
+
where.push(`lower(${FIELD_COLUMN[options.field]}) LIKE ?`);
|
|
910
|
+
params.push(likeParam);
|
|
911
|
+
} else {
|
|
912
|
+
const ors = SEARCH_COLUMNS.map((c) => `lower(${c}) LIKE ?`).join(" OR ");
|
|
913
|
+
where.push(`(${ors})`);
|
|
914
|
+
for (let i = 0; i < SEARCH_COLUMNS.length; i++) params.push(likeParam);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const orderSql =
|
|
919
|
+
"CASE status WHEN 'hypothesis' THEN 0 WHEN 'investigating' THEN 1 WHEN 'confirmed' THEN 2 " +
|
|
920
|
+
"WHEN 'blocked' THEN 3 WHEN 'killed' THEN 4 WHEN 'reported' THEN 5 ELSE 6 END, updated_at DESC";
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
whereSql: where.length ? `WHERE ${where.join(" AND ")}` : "",
|
|
924
|
+
orderSql,
|
|
925
|
+
params,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** Map DB rows to CaseRecords, attaching linkedCaseIds fetched in a single batch. */
|
|
930
|
+
function mapRowsWithLinks(db: DatabaseSync, rows: any[]): CaseRecord[] {
|
|
931
|
+
if (rows.length === 0) return [];
|
|
932
|
+
const ids = rows.map((r) => r.id);
|
|
933
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
934
|
+
const links = db
|
|
935
|
+
.prepare(`SELECT source_id, target_id FROM case_links WHERE source_id IN (${placeholders})`)
|
|
936
|
+
.all(...ids) as { source_id: string; target_id: string }[];
|
|
937
|
+
const linkMap = new Map<string, string[]>();
|
|
938
|
+
for (const l of links) {
|
|
939
|
+
if (!linkMap.has(l.source_id)) linkMap.set(l.source_id, []);
|
|
940
|
+
linkMap.get(l.source_id)!.push(l.target_id);
|
|
941
|
+
}
|
|
942
|
+
return rows.map((row) => mapRow(row, linkMap.get(row.id) ?? []));
|
|
825
943
|
}
|
|
826
944
|
|
|
827
945
|
export function searchCases(options: CaseSearchOptions = {}): {
|
|
828
946
|
cases: CaseRecord[];
|
|
829
947
|
total: number;
|
|
830
948
|
} {
|
|
831
|
-
const
|
|
832
|
-
const field = options.field;
|
|
833
|
-
const tag = options.tag?.trim().toLowerCase();
|
|
949
|
+
const db = getDb();
|
|
834
950
|
const limit = Math.max(1, Math.min(options.limit ?? 50, 200));
|
|
835
951
|
const offset = Math.max(0, options.offset ?? 0);
|
|
836
952
|
|
|
837
|
-
const
|
|
838
|
-
"hypothesis",
|
|
839
|
-
"investigating",
|
|
840
|
-
"confirmed",
|
|
841
|
-
"blocked",
|
|
842
|
-
"killed",
|
|
843
|
-
"reported",
|
|
844
|
-
];
|
|
845
|
-
|
|
846
|
-
const filtered = readCasefile()
|
|
847
|
-
.filter((r) => !options.status || r.status === options.status)
|
|
848
|
-
.filter((r) => !options.confidence || r.confidence === options.confidence)
|
|
849
|
-
.filter((r) => !options.severity || r.severity === options.severity)
|
|
850
|
-
.filter((r) => !options.priority || r.priority === options.priority)
|
|
851
|
-
.filter((r) => !tag || r.tags?.some((t) => t.toLowerCase() === tag))
|
|
852
|
-
.filter((r) => !query || caseHaystack(r, field).includes(query))
|
|
853
|
-
.sort((a, b) => {
|
|
854
|
-
const aStatus = STATUS_ORDER.indexOf(a.status);
|
|
855
|
-
const bStatus = STATUS_ORDER.indexOf(b.status);
|
|
856
|
-
if (aStatus !== bStatus) return aStatus - bStatus;
|
|
857
|
-
return b.updatedAt.localeCompare(a.updatedAt);
|
|
858
|
-
});
|
|
953
|
+
const { whereSql, orderSql, params } = buildCaseWhere(options);
|
|
859
954
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
955
|
+
const total = (db.prepare(`SELECT COUNT(*) as c FROM cases ${whereSql}`).get(...params) as any).c;
|
|
956
|
+
const rows = db
|
|
957
|
+
.prepare(`SELECT * FROM cases ${whereSql} ORDER BY ${orderSql} LIMIT ? OFFSET ?`)
|
|
958
|
+
.all(...params, limit, offset) as any[];
|
|
959
|
+
|
|
960
|
+
return { total, cases: mapRowsWithLinks(db, rows) };
|
|
864
961
|
}
|
|
865
962
|
|
|
866
963
|
export function countCases(): {
|
|
@@ -868,14 +965,22 @@ export function countCases(): {
|
|
|
868
965
|
byStatus: Record<string, number>;
|
|
869
966
|
bySeverity: Record<string, number>;
|
|
870
967
|
} {
|
|
871
|
-
const
|
|
968
|
+
const db = getDb();
|
|
969
|
+
const total = (db.prepare("SELECT COUNT(*) as c FROM cases").get() as any).c;
|
|
970
|
+
const statusRows = db
|
|
971
|
+
.prepare("SELECT status, COUNT(*) as n FROM cases GROUP BY status")
|
|
972
|
+
.all() as { status: string; n: number }[];
|
|
973
|
+
const severityRows = db
|
|
974
|
+
.prepare(
|
|
975
|
+
"SELECT severity, COUNT(*) as n FROM cases WHERE severity IS NOT NULL GROUP BY severity",
|
|
976
|
+
)
|
|
977
|
+
.all() as { severity: string; n: number }[];
|
|
978
|
+
|
|
872
979
|
const byStatus: Record<string, number> = {};
|
|
873
980
|
const bySeverity: Record<string, number> = {};
|
|
874
|
-
for (const r of
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
return { total: records.length, byStatus, bySeverity };
|
|
981
|
+
for (const r of statusRows) byStatus[r.status] = r.n;
|
|
982
|
+
for (const r of severityRows) bySeverity[r.severity] = r.n;
|
|
983
|
+
return { total, byStatus, bySeverity };
|
|
879
984
|
}
|
|
880
985
|
|
|
881
986
|
// ── Format helpers ───────────────────────────────────────────────────
|
package/src/poc-runner.ts
CHANGED
|
@@ -239,6 +239,12 @@ function buildDockerArgs(image: string, command: string, workspaceDir: string):
|
|
|
239
239
|
"no-new-privileges",
|
|
240
240
|
"--user",
|
|
241
241
|
"1000:1000",
|
|
242
|
+
"--memory",
|
|
243
|
+
"256m",
|
|
244
|
+
"--pids-limit",
|
|
245
|
+
"128",
|
|
246
|
+
"--cpus",
|
|
247
|
+
"1.0",
|
|
242
248
|
"-v",
|
|
243
249
|
`${workspaceDir}:/workspace:rw`,
|
|
244
250
|
image,
|
|
@@ -260,6 +266,27 @@ function renderCommand(template: string, pocPath: string, inSandbox: boolean): s
|
|
|
260
266
|
.replace(/{{class}}/g, className);
|
|
261
267
|
}
|
|
262
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Translate a spawnSync result into a robust exit code.
|
|
271
|
+
*
|
|
272
|
+
* `spawnSync` returns `status: null` AND `signal: null` when it cannot even start
|
|
273
|
+
* the child (e.g. the binary is missing → ENOENT, or the docker daemon is
|
|
274
|
+
* unavailable). The previous `result.status ?? (result.signal ? 1 : 0)` then
|
|
275
|
+
* collapsed to `0`, making a never-executed PoC look successful — which let
|
|
276
|
+
* PromoteFinding promote an investigating case to CONFIRMED without the PoC
|
|
277
|
+
* ever running. We fail closed: a spawn error or a missing status/signal is
|
|
278
|
+
* always a non-zero exit.
|
|
279
|
+
*/
|
|
280
|
+
function spawnExitCode(result: {
|
|
281
|
+
status: number | null;
|
|
282
|
+
signal: string | null;
|
|
283
|
+
error?: Error;
|
|
284
|
+
}): number {
|
|
285
|
+
if (result.error) return 127;
|
|
286
|
+
if (result.status !== null) return result.status;
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
|
|
263
290
|
function runSandboxed(pocPath: string, language: PocLanguage): PocRun {
|
|
264
291
|
const ranAt = new Date().toISOString();
|
|
265
292
|
const sourceName = basename(pocPath);
|
|
@@ -283,10 +310,11 @@ function runSandboxed(pocPath: string, language: PocLanguage): PocRun {
|
|
|
283
310
|
maxBuffer: MAX_BUFFER,
|
|
284
311
|
});
|
|
285
312
|
|
|
286
|
-
const
|
|
313
|
+
const spawnErr = result.error ? `\n[spawn error] ${result.error.message}` : "";
|
|
314
|
+
const output = sanitizeOutput((result.stdout ?? "") + (result.stderr ?? "") + spawnErr);
|
|
287
315
|
return {
|
|
288
316
|
path: pocPath,
|
|
289
|
-
exitCode:
|
|
317
|
+
exitCode: spawnExitCode(result),
|
|
290
318
|
output,
|
|
291
319
|
ranAt,
|
|
292
320
|
sandbox: true,
|
|
@@ -314,11 +342,15 @@ function runLocal(pocPath: string, language: PocLanguage): PocRun {
|
|
|
314
342
|
throw new Error("Language config has no run command");
|
|
315
343
|
}
|
|
316
344
|
|
|
317
|
-
//
|
|
345
|
+
// The run template is `<interpreter> <file>`. Preserve multi-arg run commands for
|
|
346
|
+
// normal paths, but keep a space-containing PoC path as a single argument (no shell
|
|
347
|
+
// is used, so args are passed verbatim).
|
|
318
348
|
const command = renderCommand(language.run, pocPath, false);
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
349
|
+
const trimmed = command.trim();
|
|
350
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
351
|
+
const interpreter = firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
|
|
352
|
+
const rest = firstSpace === -1 ? "" : trimmed.slice(firstSpace + 1);
|
|
353
|
+
const args = pocPath.includes(" ") ? (rest ? [rest] : []) : rest ? rest.split(" ") : [];
|
|
322
354
|
|
|
323
355
|
const result = spawnSync(interpreter, args, {
|
|
324
356
|
encoding: "utf8",
|
|
@@ -326,10 +358,11 @@ function runLocal(pocPath: string, language: PocLanguage): PocRun {
|
|
|
326
358
|
maxBuffer: MAX_BUFFER,
|
|
327
359
|
});
|
|
328
360
|
|
|
329
|
-
const
|
|
361
|
+
const spawnErr = result.error ? `\n[spawn error] ${result.error.message}` : "";
|
|
362
|
+
const output = sanitizeOutput((result.stdout ?? "") + (result.stderr ?? "") + spawnErr);
|
|
330
363
|
return {
|
|
331
364
|
path: pocPath,
|
|
332
|
-
exitCode:
|
|
365
|
+
exitCode: spawnExitCode(result),
|
|
333
366
|
output,
|
|
334
367
|
ranAt,
|
|
335
368
|
sandbox: false,
|
|
@@ -8,8 +8,15 @@ try {
|
|
|
8
8
|
// biome-ignore lint/suspicious/noExplicitAny: Runtime module swappability
|
|
9
9
|
DatabaseSyncConstructor = _require("bun:sqlite").Database as any;
|
|
10
10
|
} catch {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
try {
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: Runtime module swappability
|
|
13
|
+
DatabaseSyncConstructor = (_require("node:sqlite") as any).DatabaseSync;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"XPI requires bun:sqlite (Bun runtime) or node:sqlite (Node >= 22.5). " +
|
|
17
|
+
`Neither SQLite backend is available: ${(e as Error).message}`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
// biome-ignore lint/suspicious/noExplicitAny: Standard SQLite API returns any
|