log-llm-config 1.3.21 → 1.3.23
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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { resolveSqlite3Binary } from '../runtime/sqlite_binary.js';
|
|
3
4
|
import { readFileCollectionVscdbContract, writeFileCollectionVscdbContract, } from '../runtime/management_storage.js';
|
|
4
5
|
/**
|
|
5
6
|
* ItemTable keys that store a bare JSON boolean; compliance paths use `${key}.${field}`.
|
|
@@ -11,10 +12,15 @@ export const CURSOR_SCALAR_ITEMTABLE_FIELDS = {
|
|
|
11
12
|
'cursor/autoOpenLocalhostUrls': 'autoOpenLocalhostUrls',
|
|
12
13
|
};
|
|
13
14
|
function querySqlite(dbPath, key) {
|
|
15
|
+
const bin = resolveSqlite3Binary();
|
|
16
|
+
if (!bin)
|
|
17
|
+
throw new Error('sqlite3 not found');
|
|
14
18
|
const safe = key.replace(/'/g, "''");
|
|
15
|
-
|
|
19
|
+
const script = `.timeout 60000\nSELECT value FROM ItemTable WHERE key='${safe}';\n`;
|
|
20
|
+
return execFileSync(bin, ['-noheader', dbPath], {
|
|
21
|
+
input: script,
|
|
16
22
|
encoding: 'utf8',
|
|
17
|
-
stdio: ['
|
|
23
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
18
24
|
}).trim();
|
|
19
25
|
}
|
|
20
26
|
/**
|
|
@@ -73,7 +79,8 @@ export function readVscdbItemTableJson(dbPath, itemKey) {
|
|
|
73
79
|
try {
|
|
74
80
|
if (!dbPath || !existsSync(dbPath))
|
|
75
81
|
return null;
|
|
76
|
-
|
|
82
|
+
if (!resolveSqlite3Binary())
|
|
83
|
+
return null;
|
|
77
84
|
}
|
|
78
85
|
catch {
|
|
79
86
|
return null;
|
|
@@ -218,10 +225,7 @@ export function readVSCDBState(dbPath, readQueries, mergeFromComposerStateKeys)
|
|
|
218
225
|
try {
|
|
219
226
|
if (!dbPath || !existsSync(dbPath))
|
|
220
227
|
return null;
|
|
221
|
-
|
|
222
|
-
execSync('which sqlite3', { stdio: 'ignore' });
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
228
|
+
if (!resolveSqlite3Binary()) {
|
|
225
229
|
console.warn('sqlite3 command not found; skipping vscdb reading');
|
|
226
230
|
return null;
|
|
227
231
|
}
|
|
@@ -52,7 +52,9 @@ export function canonicalCursorUserStateVscdbPath(filePath) {
|
|
|
52
52
|
function cursorStateVscdbAbsoluteBasePaths() {
|
|
53
53
|
const h = homedir();
|
|
54
54
|
if (process.platform === 'darwin') {
|
|
55
|
-
|
|
55
|
+
const support = join(h, 'Library', 'Application Support');
|
|
56
|
+
const variants = ['Cursor', 'Cursor - Insiders', 'Cursor Nightly', 'Cursor Next'];
|
|
57
|
+
return variants.map((name) => join(support, name, 'User', 'globalStorage', 'state.vscdb'));
|
|
56
58
|
}
|
|
57
59
|
if (process.platform === 'win32') {
|
|
58
60
|
const appData = process.env.APPDATA;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
2
|
+
import { delimiter, dirname } from 'node:path';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import { executeGet, executeBody } from '../../endpoint_client/http_transport.js';
|
|
5
5
|
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
@@ -12,6 +12,29 @@ import { CURSOR_SCALAR_ITEMTABLE_FIELDS, persistVscdbComposerContractFromPattern
|
|
|
12
12
|
import { sendConfigFile } from '../sender/batch_sender.js';
|
|
13
13
|
import { getFileCollectionPatterns } from '../../endpoint_client/registry_api.js';
|
|
14
14
|
import { resolveRemediationConfigPath } from './remediation_config_path.js';
|
|
15
|
+
import { resolveSqlite3Binary } from './sqlite_binary.js';
|
|
16
|
+
/** Best-effort detail from execFileSync failures (stderr, exit code, errno). */
|
|
17
|
+
function formatNodeChildException(err) {
|
|
18
|
+
if (!(err instanceof Error)) {
|
|
19
|
+
const s = String(err);
|
|
20
|
+
return { short: s, long: s };
|
|
21
|
+
}
|
|
22
|
+
const e = err;
|
|
23
|
+
const bits = [e.message];
|
|
24
|
+
if (e.code)
|
|
25
|
+
bits.push(`errno_code=${e.code}`);
|
|
26
|
+
if (typeof e.status === 'number')
|
|
27
|
+
bits.push(`exit_status=${e.status}`);
|
|
28
|
+
let stderr = '';
|
|
29
|
+
if (e.stderr != null) {
|
|
30
|
+
stderr = typeof e.stderr === 'string' ? e.stderr : e.stderr.toString('utf8');
|
|
31
|
+
const t = stderr.trim();
|
|
32
|
+
if (t)
|
|
33
|
+
bits.push(`stderr=${t.slice(0, 1200)}`);
|
|
34
|
+
}
|
|
35
|
+
const long = bits.join(' | ');
|
|
36
|
+
return { short: bits.slice(0, 2).join(' | '), long };
|
|
37
|
+
}
|
|
15
38
|
function reactiveStorageItemKeyFromContract() {
|
|
16
39
|
const k = readFileCollectionVscdbContract()?.reactive_storage_item_key;
|
|
17
40
|
return typeof k === 'string' && k.trim() !== '' ? k.trim() : undefined;
|
|
@@ -309,16 +332,19 @@ function mergePostApplyUploadIntoPayload(payload, hint) {
|
|
|
309
332
|
payload.post_apply_uploads.push({ file_path: hint.file_path, file_type: ft });
|
|
310
333
|
}
|
|
311
334
|
function assertSqlite3Available() {
|
|
312
|
-
|
|
313
|
-
|
|
335
|
+
const bin = resolveSqlite3Binary();
|
|
336
|
+
if (bin) {
|
|
337
|
+
complianceRunnerDiag(`sqlite_update: using sqlite3 binary ${bin}`);
|
|
314
338
|
return true;
|
|
315
339
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
340
|
+
const pathEnv = process.env.PATH ?? '';
|
|
341
|
+
const n = pathEnv ? pathEnv.split(delimiter).filter(Boolean).length : 0;
|
|
342
|
+
const line = 'sqlite_update: sqlite3 command not found';
|
|
343
|
+
hookRunLog(line);
|
|
344
|
+
hookRunLog(`sqlite_update: no sqlite3 resolved after checking env OPTIMUS_SQLITE3/SQLITE3_PATH, common paths, and PATH (${n} entries)`);
|
|
345
|
+
hookRunLog('sqlite_update: hint set OPTIMUS_SQLITE3 to the full path (e.g. /usr/bin/sqlite3 on macOS) if sqlite3 is outside PATH');
|
|
346
|
+
complianceRunnerDiag(`${line} PATH_entry_count=${n}`);
|
|
347
|
+
return false;
|
|
322
348
|
}
|
|
323
349
|
/**
|
|
324
350
|
* Unquoted SQLite identifiers must be [A-Za-z_][A-Za-z0-9_]* so values read from
|
|
@@ -476,25 +502,49 @@ function mergeSqliteOpIntoJson(currentJson, sqliteOp) {
|
|
|
476
502
|
const parentParts = parts.slice(0, -1);
|
|
477
503
|
let node = currentJson;
|
|
478
504
|
for (const p of parentParts) {
|
|
479
|
-
if (node == null || typeof node !== 'object')
|
|
505
|
+
if (node == null || typeof node !== 'object' || Array.isArray(node))
|
|
480
506
|
return;
|
|
481
|
-
|
|
507
|
+
const rec = node;
|
|
508
|
+
const child = rec[p];
|
|
509
|
+
if (child == null) {
|
|
510
|
+
rec[p] = {};
|
|
511
|
+
node = rec[p];
|
|
512
|
+
}
|
|
513
|
+
else if (typeof child === 'object' && !Array.isArray(child)) {
|
|
514
|
+
node = child;
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
482
519
|
}
|
|
483
520
|
if (node == null || typeof node !== 'object' || Array.isArray(node))
|
|
484
521
|
return;
|
|
485
522
|
const container = node;
|
|
486
|
-
const
|
|
487
|
-
|
|
523
|
+
const existing = container[arrayKey];
|
|
524
|
+
let arr;
|
|
525
|
+
if (Array.isArray(existing)) {
|
|
526
|
+
arr = existing;
|
|
527
|
+
}
|
|
528
|
+
else if (existing == null) {
|
|
529
|
+
arr = [];
|
|
530
|
+
container[arrayKey] = arr;
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
488
533
|
return;
|
|
534
|
+
}
|
|
489
535
|
const idx = arr.findIndex((item) => {
|
|
490
536
|
if (item === null || typeof item !== 'object')
|
|
491
537
|
return false;
|
|
492
538
|
const o = item;
|
|
493
539
|
return Object.entries(where).every(([k, v]) => o[k] === v);
|
|
494
540
|
});
|
|
495
|
-
if (idx
|
|
541
|
+
if (idx >= 0) {
|
|
542
|
+
Object.assign(arr[idx], sqliteOp.updates);
|
|
496
543
|
return;
|
|
497
|
-
|
|
544
|
+
}
|
|
545
|
+
const newItem = { ...where };
|
|
546
|
+
Object.assign(newItem, sqliteOp.updates);
|
|
547
|
+
arr.push(newItem);
|
|
498
548
|
return;
|
|
499
549
|
}
|
|
500
550
|
mergeJsonAtSqlitePath(currentJson, sqliteOp.json_path, sqliteOp.updates);
|
|
@@ -586,30 +636,68 @@ function sqliteSelectValueCell(dbPath, table, key_column, value_column, target_k
|
|
|
586
636
|
if (!assertSafeSqliteIdentifiersForItemTable(table, key_column, value_column)) {
|
|
587
637
|
return '';
|
|
588
638
|
}
|
|
639
|
+
const sqlite3 = resolveSqlite3Binary();
|
|
640
|
+
if (!sqlite3) {
|
|
641
|
+
const line = 'sqlite_update: sqliteSelectValueCell called with no resolved sqlite3 binary (logic error: assertSqlite3Available should run first)';
|
|
642
|
+
hookRunLog(line);
|
|
643
|
+
complianceRunnerDiag(line);
|
|
644
|
+
return '';
|
|
645
|
+
}
|
|
589
646
|
const safeName = target_key.replace(/'/g, "''");
|
|
590
647
|
const script = `.timeout 60000\nSELECT ${value_column} FROM ${table} WHERE ${key_column}='${safeName}';\n`;
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
648
|
+
try {
|
|
649
|
+
return execFileSync(sqlite3, ['-noheader', dbPath], {
|
|
650
|
+
input: script,
|
|
651
|
+
encoding: 'utf8',
|
|
652
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
653
|
+
}).trim();
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
const { short, long } = formatNodeChildException(err);
|
|
657
|
+
hookRunLog(`sqlite_update: SELECT failed binary=${sqlite3} db=${dbPath} table=${table} target_key=${target_key} ${long}`);
|
|
658
|
+
complianceRunnerDiag(`sqlite_update: SELECT failed target_key=${target_key} ${long.slice(0, 1800)}`);
|
|
659
|
+
throw new Error(`sqlite3 SELECT failed: ${short}`);
|
|
660
|
+
}
|
|
596
661
|
}
|
|
597
662
|
function sqliteExecWithTimeout(dbPath, sqlBody) {
|
|
663
|
+
const sqlite3 = resolveSqlite3Binary();
|
|
664
|
+
if (!sqlite3)
|
|
665
|
+
throw new Error('sqlite3 not found');
|
|
598
666
|
const script = `.timeout 60000\n${sqlBody}\n`;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
667
|
+
try {
|
|
668
|
+
execFileSync(sqlite3, [dbPath], {
|
|
669
|
+
input: script,
|
|
670
|
+
encoding: 'utf8',
|
|
671
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
const { long } = formatNodeChildException(err);
|
|
676
|
+
hookRunLog(`sqlite_update: UPDATE/exec failed binary=${sqlite3} db=${dbPath} ${long}`);
|
|
677
|
+
complianceRunnerDiag(`sqlite_update: UPDATE/exec failed ${long.slice(0, 1800)}`);
|
|
678
|
+
throw err;
|
|
679
|
+
}
|
|
604
680
|
}
|
|
605
681
|
/** Runs a single UPDATE (or other SQL) and returns sqlite `changes()` for the last statement. */
|
|
606
682
|
function sqliteRunUpdateReturningChanges(dbPath, updateSql) {
|
|
683
|
+
const sqlite3 = resolveSqlite3Binary();
|
|
684
|
+
if (!sqlite3)
|
|
685
|
+
throw new Error('sqlite3 not found');
|
|
607
686
|
const script = `.timeout 60000\n${updateSql}\nSELECT changes();\n`;
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
687
|
+
let out;
|
|
688
|
+
try {
|
|
689
|
+
out = execFileSync(sqlite3, [dbPath], {
|
|
690
|
+
input: script,
|
|
691
|
+
encoding: 'utf8',
|
|
692
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
693
|
+
}).trim();
|
|
694
|
+
}
|
|
695
|
+
catch (err) {
|
|
696
|
+
const { long } = formatNodeChildException(err);
|
|
697
|
+
hookRunLog(`sqlite_update: batch UPDATE/changes failed binary=${sqlite3} db=${dbPath} ${long}`);
|
|
698
|
+
complianceRunnerDiag(`sqlite_update: batch UPDATE failed ${long.slice(0, 1800)}`);
|
|
699
|
+
throw err;
|
|
700
|
+
}
|
|
613
701
|
const lines = out.split(/\r?\n/).filter((l) => l.length > 0);
|
|
614
702
|
const last = lines[lines.length - 1] ?? '0';
|
|
615
703
|
return parseInt(last, 10) || 0;
|
|
@@ -746,14 +834,24 @@ function assertSafeDeferredItemTableKey(targetKey) {
|
|
|
746
834
|
}
|
|
747
835
|
function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
748
836
|
const dbPath = configPath.split('#')[0];
|
|
837
|
+
hookRunLog(`sqlite_update: deferred_queue begin configPath=${configPath} dbPath=${dbPath} dbExists=${existsSync(dbPath)} sqliteOps=${sqliteOps.length}`);
|
|
838
|
+
complianceRunnerDiag(`sqlite_update: deferred_queue begin dbExists=${existsSync(dbPath)} ops=${sqliteOps.length}`);
|
|
749
839
|
if (!existsSync(dbPath)) {
|
|
750
840
|
const line = `sqlite_update: database not found at ${dbPath}`;
|
|
751
841
|
hookRunLog(line);
|
|
842
|
+
hookRunLog(`sqlite_update: parent dir exists=${existsSync(dirname(dbPath))} (if false, wrong Cursor profile path or never launched Cursor)`);
|
|
752
843
|
complianceRunnerDiag(line);
|
|
753
|
-
return
|
|
844
|
+
return {
|
|
845
|
+
ok: false,
|
|
846
|
+
reason: `state.vscdb not on disk at ${dbPath} (Cursor may not have created globalStorage yet — open Cursor once).`,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (!assertSqlite3Available()) {
|
|
850
|
+
return {
|
|
851
|
+
ok: false,
|
|
852
|
+
reason: 'sqlite3 CLI not found (IDE hooks often have a minimal PATH). On macOS try OPTIMUS_SQLITE3=/usr/bin/sqlite3 or install sqlite3 and fix PATH.',
|
|
853
|
+
};
|
|
754
854
|
}
|
|
755
|
-
if (!assertSqlite3Available())
|
|
756
|
-
return false;
|
|
757
855
|
const resolvedOps = sqliteOps.map((op) => resolveCursorComposerSqliteOp(dbPath, op));
|
|
758
856
|
const groups = new Map();
|
|
759
857
|
for (const op of resolvedOps) {
|
|
@@ -771,12 +869,14 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
|
771
869
|
const line = `sqlite_update: rejected unsafe or empty target_key for deferred queue (refusing to write state.vscdb)`;
|
|
772
870
|
hookRunLog(line);
|
|
773
871
|
complianceRunnerDiag(`${line} target_key_preview=${first.target_key.slice(0, 80)}`);
|
|
774
|
-
return false;
|
|
872
|
+
return { ok: false, reason: 'unsafe or empty ItemTable target_key (see hook_log).' };
|
|
775
873
|
}
|
|
776
874
|
complianceRunnerDiag(`sqlite_update: deferred merge db=${dbPath} target_key=${first.target_key} operations=${ops.length}`);
|
|
777
875
|
let currentJson = {};
|
|
876
|
+
let rawSelectLength = 0;
|
|
778
877
|
try {
|
|
779
878
|
const result = sqliteSelectValueCell(dbPath, first.table, first.key_column, first.value_column, first.target_key);
|
|
879
|
+
rawSelectLength = result.length;
|
|
780
880
|
if (result) {
|
|
781
881
|
const parsed = JSON.parse(result);
|
|
782
882
|
currentJson = coerceItemTableValueToObjectRoot(first.target_key, parsed);
|
|
@@ -786,7 +886,10 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
|
786
886
|
const line = `sqlite_update: error querying database: ${e instanceof Error ? e.message : String(e)}`;
|
|
787
887
|
hookRunLog(line);
|
|
788
888
|
complianceRunnerDiag(line);
|
|
789
|
-
return
|
|
889
|
+
return {
|
|
890
|
+
ok: false,
|
|
891
|
+
reason: `sqlite read/parse failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
892
|
+
};
|
|
790
893
|
}
|
|
791
894
|
for (const op of ops) {
|
|
792
895
|
mergeSqliteOpIntoJson(currentJson, op);
|
|
@@ -794,10 +897,17 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
|
794
897
|
repairComposerStateEmptySegmentBug(currentJson);
|
|
795
898
|
const updatedJson = serializeItemTableValueForWrite(first.target_key, currentJson);
|
|
796
899
|
if (updatedJson === '{}' && Object.keys(currentJson).length === 0) {
|
|
797
|
-
const
|
|
900
|
+
const opSummary = ops
|
|
901
|
+
.map((o) => `path=${o.json_path ?? ''} updates=${JSON.stringify(o.updates ?? {})}`)
|
|
902
|
+
.join(' || ')
|
|
903
|
+
.slice(0, 900);
|
|
904
|
+
const line = `sqlite_update: deferred merge produced empty JSON — refusing to queue (would wipe ItemTable row) target_key=${first.target_key} raw_cell_len=${rawSelectLength} ops=${opSummary}`;
|
|
798
905
|
hookRunLog(line);
|
|
799
|
-
complianceRunnerDiag(line);
|
|
800
|
-
return
|
|
906
|
+
complianceRunnerDiag(line.slice(0, 2000));
|
|
907
|
+
return {
|
|
908
|
+
ok: false,
|
|
909
|
+
reason: `merge produced empty JSON after ops (target_key=${first.target_key}, raw_cell_len=${rawSelectLength}). Check sqlite_update lines above for SELECT errors or bad sqlite_op merge.`,
|
|
910
|
+
};
|
|
801
911
|
}
|
|
802
912
|
queueDeferredVscdbItem({
|
|
803
913
|
dbPath,
|
|
@@ -811,13 +921,16 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
|
811
921
|
hookRunLog(okLine);
|
|
812
922
|
complianceRunnerDiag(okLine);
|
|
813
923
|
}
|
|
814
|
-
return true;
|
|
924
|
+
return { ok: true };
|
|
815
925
|
}
|
|
816
926
|
catch (err) {
|
|
817
927
|
const line = `sqlite_update: unexpected error: ${err instanceof Error ? err.message : String(err)}`;
|
|
818
928
|
hookRunLog(line);
|
|
819
929
|
complianceRunnerDiag(line);
|
|
820
|
-
return
|
|
930
|
+
return {
|
|
931
|
+
ok: false,
|
|
932
|
+
reason: `unexpected: ${err instanceof Error ? err.message : String(err)}`,
|
|
933
|
+
};
|
|
821
934
|
}
|
|
822
935
|
}
|
|
823
936
|
/**
|
|
@@ -932,11 +1045,16 @@ export function enforceRemediation(instruction) {
|
|
|
932
1045
|
const postApplyUpload = ft && inst.config_file_path.includes('#')
|
|
933
1046
|
? { file_path: inst.config_file_path, file_type: ft }
|
|
934
1047
|
: undefined;
|
|
935
|
-
const
|
|
936
|
-
if (!ok) {
|
|
937
|
-
|
|
1048
|
+
const q = queueDeferredSqliteOpsMerged(inst.config_file_path, ops, postApplyUpload);
|
|
1049
|
+
if (!q.ok) {
|
|
1050
|
+
const dbOnly = inst.config_file_path.split('#')[0];
|
|
1051
|
+
return fail(`deferred state.vscdb queue failed — ${q.reason} (see sqlite_update lines above in hook_log for SELECT/exec details)`, {
|
|
1052
|
+
config_file_path: inst.config_file_path,
|
|
1053
|
+
resolved_db_path: dbOnly,
|
|
1054
|
+
sqlite3_binary: resolveSqlite3Binary() ?? 'unresolved',
|
|
1055
|
+
});
|
|
938
1056
|
}
|
|
939
|
-
return { ok, deferredSqlite:
|
|
1057
|
+
return { ok: true, deferredSqlite: true };
|
|
940
1058
|
}
|
|
941
1059
|
let allSuccess = true;
|
|
942
1060
|
for (const check of sqliteOps) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
let cached;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the sqlite3 CLI for remediations and vscdb reads.
|
|
7
|
+
*
|
|
8
|
+
* Cursor/IDE-launched hooks often inherit a minimal PATH (no Homebrew), while macOS still ships
|
|
9
|
+
* `/usr/bin/sqlite3`. Prefer explicit paths before `which` / bare `sqlite3`.
|
|
10
|
+
*
|
|
11
|
+
* Override: `OPTIMUS_SQLITE3`, `SQLITE3_PATH`, or `SQLITE3_BIN` = absolute path to the binary.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveSqlite3Binary() {
|
|
14
|
+
if (cached !== undefined)
|
|
15
|
+
return cached;
|
|
16
|
+
for (const envName of ['OPTIMUS_SQLITE3', 'SQLITE3_PATH', 'SQLITE3_BIN']) {
|
|
17
|
+
const v = process.env[envName]?.trim();
|
|
18
|
+
if (v && existsSync(v)) {
|
|
19
|
+
cached = v;
|
|
20
|
+
return cached;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const candidates = [];
|
|
24
|
+
if (process.platform === 'darwin') {
|
|
25
|
+
candidates.push('/usr/bin/sqlite3', '/opt/homebrew/bin/sqlite3', '/usr/local/bin/sqlite3', '/opt/local/bin/sqlite3');
|
|
26
|
+
}
|
|
27
|
+
else if (process.platform === 'linux') {
|
|
28
|
+
candidates.push('/usr/bin/sqlite3', '/usr/local/bin/sqlite3');
|
|
29
|
+
}
|
|
30
|
+
else if (process.platform === 'win32') {
|
|
31
|
+
const pf = process.env.ProgramFiles;
|
|
32
|
+
if (pf) {
|
|
33
|
+
candidates.push(join(pf, 'Git', 'usr', 'bin', 'sqlite3.exe'));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const p of candidates) {
|
|
37
|
+
if (p && existsSync(p)) {
|
|
38
|
+
cached = p;
|
|
39
|
+
return cached;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
if (process.platform === 'win32') {
|
|
44
|
+
const out = execFileSync('where.exe', ['sqlite3'], {
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
47
|
+
}).trim();
|
|
48
|
+
const first = out.split(/\r?\n/).find((line) => {
|
|
49
|
+
const t = line.trim();
|
|
50
|
+
return t.length > 0 && existsSync(t);
|
|
51
|
+
});
|
|
52
|
+
if (first) {
|
|
53
|
+
cached = first.trim();
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
for (const whichBin of ['which', '/usr/bin/which']) {
|
|
59
|
+
try {
|
|
60
|
+
const out = execFileSync(whichBin, ['sqlite3'], {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
63
|
+
}).trim();
|
|
64
|
+
const first = out.split(/\n/)[0]?.trim();
|
|
65
|
+
if (first && existsSync(first)) {
|
|
66
|
+
cached = first;
|
|
67
|
+
return cached;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* try next */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* fall through */
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
execFileSync('sqlite3', ['--version'], { stdio: 'ignore' });
|
|
81
|
+
cached = 'sqlite3';
|
|
82
|
+
return cached;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
cached = null;
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Test-only: reset memoized path after env/path changes. */
|
|
90
|
+
export function clearSqlite3BinaryCacheForTests() {
|
|
91
|
+
cached = undefined;
|
|
92
|
+
}
|