@open-code-review/cli 2.0.0 → 2.1.0

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/README.md CHANGED
@@ -4,6 +4,8 @@ Command-line interface for [Open Code Review](https://github.com/spencermarx/ope
4
4
 
5
5
  ## Quick Start
6
6
 
7
+ **Requires Node.js >= 22.5** — OCR's storage engine is Node's built-in SQLite (`node:sqlite`), so there's no native module to compile and it installs cleanly under any package manager (npm, **pnpm 10+**, yarn).
8
+
7
9
  ```bash
8
10
  # 1. Install globally
9
11
  npm install -g @open-code-review/cli
@@ -30510,37 +30510,98 @@ function resolveOcrDir(startDir) {
30510
30510
  }
30511
30511
 
30512
30512
  // ../cli/src/lib/db/index.ts
30513
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, copyFileSync, statSync } from "node:fs";
30513
+ import {
30514
+ existsSync as existsSync4,
30515
+ mkdirSync as mkdirSync2,
30516
+ copyFileSync,
30517
+ statSync,
30518
+ mkdtempSync,
30519
+ rmSync
30520
+ } from "node:fs";
30514
30521
  import { dirname as dirname4, join as join4 } from "node:path";
30515
30522
 
30516
30523
  // ../cli/src/lib/db/engine.ts
30517
- import BetterSqlite3 from "better-sqlite3";
30524
+ import { createRequire } from "node:module";
30525
+
30526
+ // ../cli/src/lib/runtime-checks.ts
30527
+ var NODE_FLOOR = { major: 22, minor: 5 };
30528
+ function isSupportedNode(version) {
30529
+ const [major = 0, minor = 0] = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
30530
+ return major > NODE_FLOOR.major || major === NODE_FLOOR.major && minor >= NODE_FLOOR.minor;
30531
+ }
30532
+ function nodeVersionGuardMessage(version) {
30533
+ return `
30534
+ Open Code Review requires Node.js >= ${NODE_FLOOR.major}.${NODE_FLOOR.minor} (it uses Node's built-in SQLite, \`node:sqlite\`).
30535
+ You have Node ${version}. Upgrade Node (e.g. \`nvm install 22 && nvm use 22\`) and re-run.
30536
+
30537
+ `;
30538
+ }
30539
+ function isSuppressibleSqliteWarning(warning) {
30540
+ const message = typeof warning === "string" ? warning : warning?.message;
30541
+ return typeof message === "string" && message.includes("SQLite is an experimental feature");
30542
+ }
30543
+
30544
+ // ../cli/src/lib/db/engine.ts
30545
+ var SQLITE_BUSY = 5;
30546
+ var SQLITE_BUSY_SNAPSHOT = 261;
30518
30547
  var BUSY_RETRY_ATTEMPTS = 5;
30519
30548
  var BUSY_RETRY_BACKOFF_MS = 50;
30520
- function isBusyError(e) {
30521
- if (e instanceof BetterSqlite3.SqliteError) {
30522
- return e.code === "SQLITE_BUSY" || e.code === "SQLITE_BUSY_SNAPSHOT";
30549
+ var savepointName = (depth) => `ocr_sp_${depth}`;
30550
+ var nodeRequire = createRequire(import.meta.url);
30551
+ var _preconditionsApplied = false;
30552
+ function applyEnginePreconditions() {
30553
+ if (_preconditionsApplied) return;
30554
+ _preconditionsApplied = true;
30555
+ const originalEmitWarning = process.emitWarning.bind(process);
30556
+ process.emitWarning = (warning, ...args) => {
30557
+ if (isSuppressibleSqliteWarning(warning)) return;
30558
+ originalEmitWarning(warning, ...args);
30559
+ };
30560
+ }
30561
+ var _DatabaseSyncCtor;
30562
+ function newDatabase(path2) {
30563
+ if (!_DatabaseSyncCtor) {
30564
+ applyEnginePreconditions();
30565
+ try {
30566
+ _DatabaseSyncCtor = nodeRequire("node:sqlite").DatabaseSync;
30567
+ } catch (e) {
30568
+ if (!isSupportedNode(process.versions.node)) {
30569
+ throw new Error(nodeVersionGuardMessage(process.versions.node).trim());
30570
+ }
30571
+ throw e;
30572
+ }
30523
30573
  }
30524
- const code = e?.code;
30525
- return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_SNAPSHOT";
30574
+ return new _DatabaseSyncCtor(path2);
30526
30575
  }
30576
+ function isBusyError(e) {
30577
+ const errcode = e?.errcode;
30578
+ return errcode === SQLITE_BUSY || errcode === SQLITE_BUSY_SNAPSHOT;
30579
+ }
30580
+ var SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
30527
30581
  function sleepSync(ms) {
30528
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
30582
+ Atomics.wait(SLEEP_BUF, 0, 0, ms);
30529
30583
  }
30530
- var BetterSqliteAdapter = class {
30584
+ var NodeSqliteAdapter = class {
30531
30585
  raw;
30586
+ /**
30587
+ * Transaction nesting depth. `node:sqlite` has no transaction helper, so we
30588
+ * drive `BEGIN IMMEDIATE` ourselves and use SAVEPOINTs for nested calls
30589
+ * (better-sqlite3 did this automatically). 0 = no transaction open.
30590
+ */
30591
+ txnDepth = 0;
30532
30592
  constructor(db) {
30533
30593
  this.raw = db;
30534
30594
  }
30535
30595
  exec(sql, params) {
30536
30596
  const stmt = this.raw.prepare(sql);
30537
- if (!stmt.reader) {
30597
+ const cols = stmt.columns();
30598
+ if (cols.length === 0) {
30538
30599
  stmt.run(...params ?? []);
30539
30600
  return [];
30540
30601
  }
30541
- const columns = stmt.columns().map((c) => c.name);
30542
- const values = stmt.raw().all(...params ?? []);
30543
- return values.length > 0 ? [{ columns, values }] : [];
30602
+ stmt.setReturnArrays(true);
30603
+ const values = stmt.all(...params ?? []);
30604
+ return values.length > 0 ? [{ columns: cols.map((c) => c.name), values }] : [];
30544
30605
  }
30545
30606
  run(sql, params) {
30546
30607
  if (params !== void 0) {
@@ -30553,34 +30614,93 @@ var BetterSqliteAdapter = class {
30553
30614
  return this.raw.prepare(sql);
30554
30615
  }
30555
30616
  transaction(fn) {
30556
- const tx = this.raw.transaction(fn);
30557
- for (let attempt = 0; ; attempt++) {
30617
+ return this.txnDepth > 0 ? this.runNested(fn) : this.runOuter(fn);
30618
+ }
30619
+ /**
30620
+ * Nested call: a SAVEPOINT within the outer transaction's write lock. No
30621
+ * busy-retry — the outer transaction already holds the lock. The savepoint
30622
+ * lets the inner block roll back independently while the outer continues.
30623
+ */
30624
+ runNested(fn) {
30625
+ const name = savepointName(this.txnDepth);
30626
+ this.raw.exec(`SAVEPOINT ${name}`);
30627
+ this.txnDepth++;
30628
+ try {
30629
+ const result = fn();
30630
+ this.raw.exec(`RELEASE ${name}`);
30631
+ return result;
30632
+ } catch (e) {
30633
+ try {
30634
+ this.raw.exec(`ROLLBACK TO ${name}`);
30635
+ this.raw.exec(`RELEASE ${name}`);
30636
+ } catch {
30637
+ }
30638
+ throw e;
30639
+ } finally {
30640
+ this.txnDepth--;
30641
+ }
30642
+ }
30643
+ /**
30644
+ * Outer transaction: `BEGIN IMMEDIATE` acquires the write lock up front so
30645
+ * cross-process writers serialize cleanly under WAL instead of failing late
30646
+ * on upgrade. `busy_timeout` covers most contention; a bounded synchronous
30647
+ * retry absorbs the residual SQLITE_BUSY (another connection holds the lock
30648
+ * past the timeout, or BUSY_SNAPSHOT). Non-busy errors and the final attempt
30649
+ * re-throw so genuine failures propagate.
30650
+ */
30651
+ runOuter(fn) {
30652
+ for (let attempt = 0; attempt < BUSY_RETRY_ATTEMPTS; attempt++) {
30558
30653
  try {
30559
- return tx.immediate();
30654
+ return this.runOnce(fn);
30560
30655
  } catch (e) {
30561
- if (!isBusyError(e) || attempt >= BUSY_RETRY_ATTEMPTS - 1) throw e;
30656
+ if (!isBusyError(e) || attempt === BUSY_RETRY_ATTEMPTS - 1) throw e;
30562
30657
  sleepSync(BUSY_RETRY_BACKOFF_MS);
30563
30658
  }
30564
30659
  }
30660
+ throw new Error("transaction retry budget exhausted");
30661
+ }
30662
+ /** One `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK` lifecycle. */
30663
+ runOnce(fn) {
30664
+ this.raw.exec("BEGIN IMMEDIATE");
30665
+ this.txnDepth = 1;
30666
+ try {
30667
+ const result = fn();
30668
+ this.raw.exec("COMMIT");
30669
+ return result;
30670
+ } catch (e) {
30671
+ try {
30672
+ this.raw.exec("ROLLBACK");
30673
+ } catch {
30674
+ }
30675
+ throw e;
30676
+ } finally {
30677
+ this.txnDepth = 0;
30678
+ }
30565
30679
  }
30566
30680
  pragma(source) {
30567
- return this.raw.pragma(source);
30681
+ this.raw.exec(`PRAGMA ${source}`);
30682
+ return void 0;
30568
30683
  }
30569
30684
  close() {
30570
30685
  try {
30571
- this.raw.pragma("wal_checkpoint(TRUNCATE)");
30686
+ this.raw.exec("PRAGMA wal_checkpoint(TRUNCATE)");
30572
30687
  } catch {
30573
30688
  }
30574
- this.raw.close();
30689
+ try {
30690
+ this.raw.close();
30691
+ } catch (e) {
30692
+ const message = e?.message ?? "";
30693
+ if (!/database is not open/i.test(message)) throw e;
30694
+ }
30575
30695
  }
30576
30696
  };
30577
30697
  function openEngine(dbPath) {
30578
- const native = new BetterSqlite3(dbPath);
30579
- native.pragma("journal_mode = WAL");
30580
- native.pragma("foreign_keys = ON");
30581
- native.pragma("busy_timeout = 5000");
30582
- native.pragma("synchronous = NORMAL");
30583
- return new BetterSqliteAdapter(native);
30698
+ const native = newDatabase(dbPath);
30699
+ native.exec("PRAGMA journal_mode = WAL");
30700
+ native.exec("PRAGMA foreign_keys = ON");
30701
+ native.exec("PRAGMA busy_timeout = 5000");
30702
+ native.exec("PRAGMA synchronous = NORMAL");
30703
+ return new NodeSqliteAdapter(native);
30584
30704
  }
30585
30705
 
30586
30706
  // ../cli/src/lib/db/migrations.ts
@@ -31760,7 +31880,7 @@ function walCheckpointTruncate(dbPath) {
31760
31880
  return "failed";
31761
31881
  } finally {
31762
31882
  try {
31763
- transient?.raw.close();
31883
+ transient?.close();
31764
31884
  } catch {
31765
31885
  }
31766
31886
  }
package/dist/index.js CHANGED
@@ -39,6 +39,30 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
39
39
  mod
40
40
  ));
41
41
 
42
+ // src/lib/runtime-checks.ts
43
+ function isSupportedNode(version) {
44
+ const [major = 0, minor = 0] = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
45
+ return major > NODE_FLOOR.major || major === NODE_FLOOR.major && minor >= NODE_FLOOR.minor;
46
+ }
47
+ function nodeVersionGuardMessage(version) {
48
+ return `
49
+ Open Code Review requires Node.js >= ${NODE_FLOOR.major}.${NODE_FLOOR.minor} (it uses Node's built-in SQLite, \`node:sqlite\`).
50
+ You have Node ${version}. Upgrade Node (e.g. \`nvm install 22 && nvm use 22\`) and re-run.
51
+
52
+ `;
53
+ }
54
+ function isSuppressibleSqliteWarning(warning) {
55
+ const message = typeof warning === "string" ? warning : warning?.message;
56
+ return typeof message === "string" && message.includes("SQLite is an experimental feature");
57
+ }
58
+ var NODE_FLOOR;
59
+ var init_runtime_checks = __esm({
60
+ "src/lib/runtime-checks.ts"() {
61
+ "use strict";
62
+ NODE_FLOOR = { major: 22, minor: 5 };
63
+ }
64
+ });
65
+
42
66
  // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/error.js
43
67
  var require_error = __commonJS({
44
68
  "../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/error.js"(exports) {
@@ -23320,21 +23344,41 @@ var init_result_mapper = __esm({
23320
23344
  });
23321
23345
 
23322
23346
  // src/lib/db/engine.ts
23323
- import BetterSqlite3 from "better-sqlite3";
23324
- function isBusyError(e) {
23325
- if (e instanceof BetterSqlite3.SqliteError) {
23326
- return e.code === "SQLITE_BUSY" || e.code === "SQLITE_BUSY_SNAPSHOT";
23347
+ import { createRequire as createRequire2 } from "node:module";
23348
+ function applyEnginePreconditions() {
23349
+ if (_preconditionsApplied) return;
23350
+ _preconditionsApplied = true;
23351
+ const originalEmitWarning = process.emitWarning.bind(process);
23352
+ process.emitWarning = (warning, ...args) => {
23353
+ if (isSuppressibleSqliteWarning(warning)) return;
23354
+ originalEmitWarning(warning, ...args);
23355
+ };
23356
+ }
23357
+ function newDatabase(path2) {
23358
+ if (!_DatabaseSyncCtor) {
23359
+ applyEnginePreconditions();
23360
+ try {
23361
+ _DatabaseSyncCtor = nodeRequire("node:sqlite").DatabaseSync;
23362
+ } catch (e) {
23363
+ if (!isSupportedNode(process.versions.node)) {
23364
+ throw new Error(nodeVersionGuardMessage(process.versions.node).trim());
23365
+ }
23366
+ throw e;
23367
+ }
23327
23368
  }
23328
- const code = e?.code;
23329
- return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_SNAPSHOT";
23369
+ return new _DatabaseSyncCtor(path2);
23370
+ }
23371
+ function isBusyError(e) {
23372
+ const errcode = e?.errcode;
23373
+ return errcode === SQLITE_BUSY || errcode === SQLITE_BUSY_SNAPSHOT;
23330
23374
  }
23331
23375
  function sleepSync(ms) {
23332
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
23376
+ Atomics.wait(SLEEP_BUF, 0, 0, ms);
23333
23377
  }
23334
23378
  function probeEngine() {
23335
23379
  try {
23336
- const db = new BetterSqlite3(":memory:");
23337
- db.pragma("journal_mode = WAL");
23380
+ const db = newDatabase(":memory:");
23381
+ db.exec("PRAGMA journal_mode = WAL");
23338
23382
  db.exec("CREATE TABLE _probe(x); INSERT INTO _probe VALUES (1);");
23339
23383
  const row = db.prepare("SELECT sqlite_version() AS v").get();
23340
23384
  db.close();
@@ -23344,33 +23388,47 @@ function probeEngine() {
23344
23388
  }
23345
23389
  }
23346
23390
  function openEngine(dbPath) {
23347
- const native = new BetterSqlite3(dbPath);
23348
- native.pragma("journal_mode = WAL");
23349
- native.pragma("foreign_keys = ON");
23350
- native.pragma("busy_timeout = 5000");
23351
- native.pragma("synchronous = NORMAL");
23352
- return new BetterSqliteAdapter(native);
23353
- }
23354
- var BUSY_RETRY_ATTEMPTS, BUSY_RETRY_BACKOFF_MS, BetterSqliteAdapter;
23391
+ const native = newDatabase(dbPath);
23392
+ native.exec("PRAGMA journal_mode = WAL");
23393
+ native.exec("PRAGMA foreign_keys = ON");
23394
+ native.exec("PRAGMA busy_timeout = 5000");
23395
+ native.exec("PRAGMA synchronous = NORMAL");
23396
+ return new NodeSqliteAdapter(native);
23397
+ }
23398
+ var SQLITE_BUSY, SQLITE_BUSY_SNAPSHOT, BUSY_RETRY_ATTEMPTS, BUSY_RETRY_BACKOFF_MS, savepointName, nodeRequire, _preconditionsApplied, _DatabaseSyncCtor, SLEEP_BUF, NodeSqliteAdapter;
23355
23399
  var init_engine = __esm({
23356
23400
  "src/lib/db/engine.ts"() {
23357
23401
  "use strict";
23402
+ init_runtime_checks();
23403
+ SQLITE_BUSY = 5;
23404
+ SQLITE_BUSY_SNAPSHOT = 261;
23358
23405
  BUSY_RETRY_ATTEMPTS = 5;
23359
23406
  BUSY_RETRY_BACKOFF_MS = 50;
23360
- BetterSqliteAdapter = class {
23407
+ savepointName = (depth) => `ocr_sp_${depth}`;
23408
+ nodeRequire = createRequire2(import.meta.url);
23409
+ _preconditionsApplied = false;
23410
+ SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
23411
+ NodeSqliteAdapter = class {
23361
23412
  raw;
23413
+ /**
23414
+ * Transaction nesting depth. `node:sqlite` has no transaction helper, so we
23415
+ * drive `BEGIN IMMEDIATE` ourselves and use SAVEPOINTs for nested calls
23416
+ * (better-sqlite3 did this automatically). 0 = no transaction open.
23417
+ */
23418
+ txnDepth = 0;
23362
23419
  constructor(db) {
23363
23420
  this.raw = db;
23364
23421
  }
23365
23422
  exec(sql, params) {
23366
23423
  const stmt = this.raw.prepare(sql);
23367
- if (!stmt.reader) {
23424
+ const cols = stmt.columns();
23425
+ if (cols.length === 0) {
23368
23426
  stmt.run(...params ?? []);
23369
23427
  return [];
23370
23428
  }
23371
- const columns = stmt.columns().map((c) => c.name);
23372
- const values = stmt.raw().all(...params ?? []);
23373
- return values.length > 0 ? [{ columns, values }] : [];
23429
+ stmt.setReturnArrays(true);
23430
+ const values = stmt.all(...params ?? []);
23431
+ return values.length > 0 ? [{ columns: cols.map((c) => c.name), values }] : [];
23374
23432
  }
23375
23433
  run(sql, params) {
23376
23434
  if (params !== void 0) {
@@ -23383,25 +23441,84 @@ var init_engine = __esm({
23383
23441
  return this.raw.prepare(sql);
23384
23442
  }
23385
23443
  transaction(fn) {
23386
- const tx = this.raw.transaction(fn);
23387
- for (let attempt = 0; ; attempt++) {
23444
+ return this.txnDepth > 0 ? this.runNested(fn) : this.runOuter(fn);
23445
+ }
23446
+ /**
23447
+ * Nested call: a SAVEPOINT within the outer transaction's write lock. No
23448
+ * busy-retry — the outer transaction already holds the lock. The savepoint
23449
+ * lets the inner block roll back independently while the outer continues.
23450
+ */
23451
+ runNested(fn) {
23452
+ const name = savepointName(this.txnDepth);
23453
+ this.raw.exec(`SAVEPOINT ${name}`);
23454
+ this.txnDepth++;
23455
+ try {
23456
+ const result = fn();
23457
+ this.raw.exec(`RELEASE ${name}`);
23458
+ return result;
23459
+ } catch (e) {
23388
23460
  try {
23389
- return tx.immediate();
23461
+ this.raw.exec(`ROLLBACK TO ${name}`);
23462
+ this.raw.exec(`RELEASE ${name}`);
23463
+ } catch {
23464
+ }
23465
+ throw e;
23466
+ } finally {
23467
+ this.txnDepth--;
23468
+ }
23469
+ }
23470
+ /**
23471
+ * Outer transaction: `BEGIN IMMEDIATE` acquires the write lock up front so
23472
+ * cross-process writers serialize cleanly under WAL instead of failing late
23473
+ * on upgrade. `busy_timeout` covers most contention; a bounded synchronous
23474
+ * retry absorbs the residual SQLITE_BUSY (another connection holds the lock
23475
+ * past the timeout, or BUSY_SNAPSHOT). Non-busy errors and the final attempt
23476
+ * re-throw so genuine failures propagate.
23477
+ */
23478
+ runOuter(fn) {
23479
+ for (let attempt = 0; attempt < BUSY_RETRY_ATTEMPTS; attempt++) {
23480
+ try {
23481
+ return this.runOnce(fn);
23390
23482
  } catch (e) {
23391
- if (!isBusyError(e) || attempt >= BUSY_RETRY_ATTEMPTS - 1) throw e;
23483
+ if (!isBusyError(e) || attempt === BUSY_RETRY_ATTEMPTS - 1) throw e;
23392
23484
  sleepSync(BUSY_RETRY_BACKOFF_MS);
23393
23485
  }
23394
23486
  }
23487
+ throw new Error("transaction retry budget exhausted");
23488
+ }
23489
+ /** One `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK` lifecycle. */
23490
+ runOnce(fn) {
23491
+ this.raw.exec("BEGIN IMMEDIATE");
23492
+ this.txnDepth = 1;
23493
+ try {
23494
+ const result = fn();
23495
+ this.raw.exec("COMMIT");
23496
+ return result;
23497
+ } catch (e) {
23498
+ try {
23499
+ this.raw.exec("ROLLBACK");
23500
+ } catch {
23501
+ }
23502
+ throw e;
23503
+ } finally {
23504
+ this.txnDepth = 0;
23505
+ }
23395
23506
  }
23396
23507
  pragma(source) {
23397
- return this.raw.pragma(source);
23508
+ this.raw.exec(`PRAGMA ${source}`);
23509
+ return void 0;
23398
23510
  }
23399
23511
  close() {
23400
23512
  try {
23401
- this.raw.pragma("wal_checkpoint(TRUNCATE)");
23513
+ this.raw.exec("PRAGMA wal_checkpoint(TRUNCATE)");
23402
23514
  } catch {
23403
23515
  }
23404
- this.raw.close();
23516
+ try {
23517
+ this.raw.close();
23518
+ } catch (e) {
23519
+ const message = e?.message ?? "";
23520
+ if (!/database is not open/i.test(message)) throw e;
23521
+ }
23405
23522
  }
23406
23523
  };
23407
23524
  }
@@ -24764,6 +24881,7 @@ __export(db_exports, {
24764
24881
  listAgentSessionsForWorkflow: () => listAgentSessionsForWorkflow,
24765
24882
  openDatabase: () => openDatabase,
24766
24883
  probeEngine: () => probeEngine,
24884
+ probeWrite: () => probeWrite,
24767
24885
  readCommandLog: () => readCommandLog,
24768
24886
  reconcileLegacyState: () => reconcileLegacyState,
24769
24887
  recordVendorSessionIdForExecution: () => recordVendorSessionIdForExecution,
@@ -24781,7 +24899,15 @@ __export(db_exports, {
24781
24899
  updateSession: () => updateSession,
24782
24900
  walCheckpointTruncate: () => walCheckpointTruncate
24783
24901
  });
24784
- import { existsSync as existsSync12, mkdirSync as mkdirSync4, copyFileSync, statSync } from "node:fs";
24902
+ import {
24903
+ existsSync as existsSync12,
24904
+ mkdirSync as mkdirSync4,
24905
+ copyFileSync,
24906
+ statSync,
24907
+ mkdtempSync,
24908
+ rmSync
24909
+ } from "node:fs";
24910
+ import { tmpdir } from "node:os";
24785
24911
  import { dirname as dirname6, join as join14 } from "node:path";
24786
24912
  function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
24787
24913
  if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
@@ -24889,7 +25015,7 @@ function walCheckpointTruncate(dbPath) {
24889
25015
  return "failed";
24890
25016
  } finally {
24891
25017
  try {
24892
- transient?.raw.close();
25018
+ transient?.close();
24893
25019
  } catch {
24894
25020
  }
24895
25021
  }
@@ -24907,6 +25033,41 @@ function closeAllDatabases() {
24907
25033
  connections.delete(path2);
24908
25034
  }
24909
25035
  }
25036
+ function probeWrite() {
25037
+ let dir;
25038
+ try {
25039
+ dir = mkdtempSync(join14(tmpdir(), "ocr-probe-"));
25040
+ const db = openEngine(join14(dir, "probe.db"));
25041
+ try {
25042
+ db.run("CREATE TABLE _probe_write (id INTEGER PRIMARY KEY, v TEXT)");
25043
+ db.transaction(() => {
25044
+ db.run("INSERT INTO _probe_write (v) VALUES (?)", ["written-in-txn"]);
25045
+ });
25046
+ const value = db.exec("SELECT v FROM _probe_write")[0]?.values[0]?.[0];
25047
+ if (value !== "written-in-txn") {
25048
+ return { ok: false, error: `unexpected probe value: ${String(value)}` };
25049
+ }
25050
+ return { ok: true };
25051
+ } finally {
25052
+ db.close();
25053
+ }
25054
+ } catch (e) {
25055
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
25056
+ } finally {
25057
+ if (dir) rmDirBestEffort(dir);
25058
+ }
25059
+ }
25060
+ function rmDirBestEffort(dir) {
25061
+ for (let attempt = 0; attempt < 3; attempt++) {
25062
+ try {
25063
+ rmSync(dir, { recursive: true, force: true });
25064
+ return;
25065
+ } catch {
25066
+ if (attempt === 2) return;
25067
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
25068
+ }
25069
+ }
25070
+ }
24910
25071
  var V2_SCHEMA_VERSION, connections;
24911
25072
  var init_db = __esm({
24912
25073
  "src/lib/db/index.ts"() {
@@ -24929,6 +25090,13 @@ var init_db = __esm({
24929
25090
  }
24930
25091
  });
24931
25092
 
25093
+ // src/lib/runtime-guard.ts
25094
+ init_runtime_checks();
25095
+ if (!isSupportedNode(process.versions.node)) {
25096
+ process.stderr.write(nodeVersionGuardMessage(process.versions.node));
25097
+ process.exit(1);
25098
+ }
25099
+
24932
25100
  // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/esm.mjs
24933
25101
  var import_index = __toESM(require_commander(), 1);
24934
25102
  var {
@@ -29333,7 +29501,7 @@ ${hint}
29333
29501
  }
29334
29502
 
29335
29503
  // src/lib/version.ts
29336
- var CLI_VERSION = true ? "2.0.0" : createRequire(import.meta.url)("../../package.json").version;
29504
+ var CLI_VERSION = true ? "2.1.0" : createRequire(import.meta.url)("../../package.json").version;
29337
29505
 
29338
29506
  // ../shared/platform/src/index.ts
29339
29507
  import { pathToFileURL } from "node:url";
@@ -35412,8 +35580,50 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
35412
35580
  import { existsSync as existsSync20 } from "node:fs";
35413
35581
  import { join as join24 } from "node:path";
35414
35582
  init_db();
35415
- var doctorCommand = new Command("doctor").description("Check OCR installation and verify all dependencies").action(() => {
35583
+ function printStorageEngine(probeWriteEnabled) {
35584
+ console.log();
35585
+ console.log(source_default.bold(" Storage Engine"));
35586
+ console.log();
35587
+ const engine = probeEngine();
35588
+ if (!engine.ok) {
35589
+ console.log(` ${source_default.red("\u2717")} node:sqlite unavailable`);
35590
+ console.log(` ${source_default.dim(engine.error)}`);
35591
+ console.log(
35592
+ ` ${source_default.dim(
35593
+ "OCR requires Node >= 22.5 (node:sqlite). Upgrade Node, then re-run `ocr doctor`."
35594
+ )}`
35595
+ );
35596
+ return false;
35597
+ }
35598
+ console.log(
35599
+ ` ${source_default.green("\u2713")} node:sqlite (SQLite ${engine.version}, WAL)`
35600
+ );
35601
+ if (probeWriteEnabled) {
35602
+ const write = probeWrite();
35603
+ if (!write.ok) {
35604
+ console.log(` ${source_default.red("\u2717")} write probe failed`);
35605
+ console.log(` ${source_default.dim(write.error)}`);
35606
+ return false;
35607
+ }
35608
+ console.log(
35609
+ ` ${source_default.green("\u2713")} write probe (on-disk WAL transaction round-trip)`
35610
+ );
35611
+ }
35612
+ return true;
35613
+ }
35614
+ var doctorCommand = new Command("doctor").description("Check OCR installation and verify all dependencies").option(
35615
+ "--probe-write",
35616
+ "additionally exercise an on-disk WAL transaction round-trip (used by the release install gate)"
35617
+ ).option(
35618
+ "--engine-only",
35619
+ "check ONLY the storage engine and exit on its result \u2014 skips project/tool checks (used by the release install gate, which runs from a non-initialized dir with no AI tools)"
35620
+ ).action((options) => {
35416
35621
  printHeader();
35622
+ if (options.engineOnly) {
35623
+ const ok = printStorageEngine(options.probeWrite ?? false);
35624
+ console.log();
35625
+ process.exit(ok ? 0 : 1);
35626
+ }
35417
35627
  const targetDir = process.cwd();
35418
35628
  let hasIssues = false;
35419
35629
  const depResult = checkDependencies();
@@ -35450,25 +35660,8 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
35450
35660
  if (!ocrStatus.valid) {
35451
35661
  hasIssues = true;
35452
35662
  }
35453
- console.log();
35454
- console.log(source_default.bold(" Storage Engine"));
35455
- console.log();
35456
- const engine = probeEngine();
35457
- if (engine.ok) {
35458
- console.log(
35459
- ` ${source_default.green("\u2713")} better-sqlite3 (SQLite ${engine.version}, WAL)`
35460
- );
35461
- } else {
35663
+ if (!printStorageEngine(options.probeWrite ?? false)) {
35462
35664
  hasIssues = true;
35463
- console.log(
35464
- ` ${source_default.red("\u2717")} better-sqlite3 failed to load`
35465
- );
35466
- console.log(` ${source_default.dim(engine.error)}`);
35467
- console.log(
35468
- ` ${source_default.dim(
35469
- "Reinstall the CLI; if it persists your platform may need build tools (python3 + a C++ toolchain) or lacks a prebuilt binary."
35470
- )}`
35471
- );
35472
35665
  }
35473
35666
  console.log();
35474
35667
  printCapabilities(depResult);
@@ -1,35 +1,97 @@
1
1
  // src/lib/db/index.ts
2
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, copyFileSync, statSync } from "node:fs";
2
+ import {
3
+ existsSync as existsSync3,
4
+ mkdirSync as mkdirSync2,
5
+ copyFileSync,
6
+ statSync,
7
+ mkdtempSync,
8
+ rmSync
9
+ } from "node:fs";
10
+ import { tmpdir } from "node:os";
3
11
  import { dirname as dirname3, join as join3 } from "node:path";
4
12
 
5
13
  // src/lib/db/engine.ts
6
- import BetterSqlite3 from "better-sqlite3";
14
+ import { createRequire } from "node:module";
15
+
16
+ // src/lib/runtime-checks.ts
17
+ var NODE_FLOOR = { major: 22, minor: 5 };
18
+ function isSupportedNode(version) {
19
+ const [major = 0, minor = 0] = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
20
+ return major > NODE_FLOOR.major || major === NODE_FLOOR.major && minor >= NODE_FLOOR.minor;
21
+ }
22
+ function nodeVersionGuardMessage(version) {
23
+ return `
24
+ Open Code Review requires Node.js >= ${NODE_FLOOR.major}.${NODE_FLOOR.minor} (it uses Node's built-in SQLite, \`node:sqlite\`).
25
+ You have Node ${version}. Upgrade Node (e.g. \`nvm install 22 && nvm use 22\`) and re-run.
26
+
27
+ `;
28
+ }
29
+ function isSuppressibleSqliteWarning(warning) {
30
+ const message = typeof warning === "string" ? warning : warning?.message;
31
+ return typeof message === "string" && message.includes("SQLite is an experimental feature");
32
+ }
33
+
34
+ // src/lib/db/engine.ts
35
+ var SQLITE_BUSY = 5;
36
+ var SQLITE_BUSY_SNAPSHOT = 261;
7
37
  var BUSY_RETRY_ATTEMPTS = 5;
8
38
  var BUSY_RETRY_BACKOFF_MS = 50;
9
- function isBusyError(e) {
10
- if (e instanceof BetterSqlite3.SqliteError) {
11
- return e.code === "SQLITE_BUSY" || e.code === "SQLITE_BUSY_SNAPSHOT";
39
+ var savepointName = (depth) => `ocr_sp_${depth}`;
40
+ var nodeRequire = createRequire(import.meta.url);
41
+ var _preconditionsApplied = false;
42
+ function applyEnginePreconditions() {
43
+ if (_preconditionsApplied) return;
44
+ _preconditionsApplied = true;
45
+ const originalEmitWarning = process.emitWarning.bind(process);
46
+ process.emitWarning = (warning, ...args) => {
47
+ if (isSuppressibleSqliteWarning(warning)) return;
48
+ originalEmitWarning(warning, ...args);
49
+ };
50
+ }
51
+ var _DatabaseSyncCtor;
52
+ function newDatabase(path) {
53
+ if (!_DatabaseSyncCtor) {
54
+ applyEnginePreconditions();
55
+ try {
56
+ _DatabaseSyncCtor = nodeRequire("node:sqlite").DatabaseSync;
57
+ } catch (e) {
58
+ if (!isSupportedNode(process.versions.node)) {
59
+ throw new Error(nodeVersionGuardMessage(process.versions.node).trim());
60
+ }
61
+ throw e;
62
+ }
12
63
  }
13
- const code = e?.code;
14
- return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_SNAPSHOT";
64
+ return new _DatabaseSyncCtor(path);
65
+ }
66
+ function isBusyError(e) {
67
+ const errcode = e?.errcode;
68
+ return errcode === SQLITE_BUSY || errcode === SQLITE_BUSY_SNAPSHOT;
15
69
  }
70
+ var SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
16
71
  function sleepSync(ms) {
17
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
72
+ Atomics.wait(SLEEP_BUF, 0, 0, ms);
18
73
  }
19
- var BetterSqliteAdapter = class {
74
+ var NodeSqliteAdapter = class {
20
75
  raw;
76
+ /**
77
+ * Transaction nesting depth. `node:sqlite` has no transaction helper, so we
78
+ * drive `BEGIN IMMEDIATE` ourselves and use SAVEPOINTs for nested calls
79
+ * (better-sqlite3 did this automatically). 0 = no transaction open.
80
+ */
81
+ txnDepth = 0;
21
82
  constructor(db) {
22
83
  this.raw = db;
23
84
  }
24
85
  exec(sql, params) {
25
86
  const stmt = this.raw.prepare(sql);
26
- if (!stmt.reader) {
87
+ const cols = stmt.columns();
88
+ if (cols.length === 0) {
27
89
  stmt.run(...params ?? []);
28
90
  return [];
29
91
  }
30
- const columns = stmt.columns().map((c) => c.name);
31
- const values = stmt.raw().all(...params ?? []);
32
- return values.length > 0 ? [{ columns, values }] : [];
92
+ stmt.setReturnArrays(true);
93
+ const values = stmt.all(...params ?? []);
94
+ return values.length > 0 ? [{ columns: cols.map((c) => c.name), values }] : [];
33
95
  }
34
96
  run(sql, params) {
35
97
  if (params !== void 0) {
@@ -42,31 +104,90 @@ var BetterSqliteAdapter = class {
42
104
  return this.raw.prepare(sql);
43
105
  }
44
106
  transaction(fn) {
45
- const tx = this.raw.transaction(fn);
46
- for (let attempt = 0; ; attempt++) {
107
+ return this.txnDepth > 0 ? this.runNested(fn) : this.runOuter(fn);
108
+ }
109
+ /**
110
+ * Nested call: a SAVEPOINT within the outer transaction's write lock. No
111
+ * busy-retry — the outer transaction already holds the lock. The savepoint
112
+ * lets the inner block roll back independently while the outer continues.
113
+ */
114
+ runNested(fn) {
115
+ const name = savepointName(this.txnDepth);
116
+ this.raw.exec(`SAVEPOINT ${name}`);
117
+ this.txnDepth++;
118
+ try {
119
+ const result = fn();
120
+ this.raw.exec(`RELEASE ${name}`);
121
+ return result;
122
+ } catch (e) {
47
123
  try {
48
- return tx.immediate();
124
+ this.raw.exec(`ROLLBACK TO ${name}`);
125
+ this.raw.exec(`RELEASE ${name}`);
126
+ } catch {
127
+ }
128
+ throw e;
129
+ } finally {
130
+ this.txnDepth--;
131
+ }
132
+ }
133
+ /**
134
+ * Outer transaction: `BEGIN IMMEDIATE` acquires the write lock up front so
135
+ * cross-process writers serialize cleanly under WAL instead of failing late
136
+ * on upgrade. `busy_timeout` covers most contention; a bounded synchronous
137
+ * retry absorbs the residual SQLITE_BUSY (another connection holds the lock
138
+ * past the timeout, or BUSY_SNAPSHOT). Non-busy errors and the final attempt
139
+ * re-throw so genuine failures propagate.
140
+ */
141
+ runOuter(fn) {
142
+ for (let attempt = 0; attempt < BUSY_RETRY_ATTEMPTS; attempt++) {
143
+ try {
144
+ return this.runOnce(fn);
49
145
  } catch (e) {
50
- if (!isBusyError(e) || attempt >= BUSY_RETRY_ATTEMPTS - 1) throw e;
146
+ if (!isBusyError(e) || attempt === BUSY_RETRY_ATTEMPTS - 1) throw e;
51
147
  sleepSync(BUSY_RETRY_BACKOFF_MS);
52
148
  }
53
149
  }
150
+ throw new Error("transaction retry budget exhausted");
151
+ }
152
+ /** One `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK` lifecycle. */
153
+ runOnce(fn) {
154
+ this.raw.exec("BEGIN IMMEDIATE");
155
+ this.txnDepth = 1;
156
+ try {
157
+ const result = fn();
158
+ this.raw.exec("COMMIT");
159
+ return result;
160
+ } catch (e) {
161
+ try {
162
+ this.raw.exec("ROLLBACK");
163
+ } catch {
164
+ }
165
+ throw e;
166
+ } finally {
167
+ this.txnDepth = 0;
168
+ }
54
169
  }
55
170
  pragma(source) {
56
- return this.raw.pragma(source);
171
+ this.raw.exec(`PRAGMA ${source}`);
172
+ return void 0;
57
173
  }
58
174
  close() {
59
175
  try {
60
- this.raw.pragma("wal_checkpoint(TRUNCATE)");
176
+ this.raw.exec("PRAGMA wal_checkpoint(TRUNCATE)");
61
177
  } catch {
62
178
  }
63
- this.raw.close();
179
+ try {
180
+ this.raw.close();
181
+ } catch (e) {
182
+ const message = e?.message ?? "";
183
+ if (!/database is not open/i.test(message)) throw e;
184
+ }
64
185
  }
65
186
  };
66
187
  function probeEngine() {
67
188
  try {
68
- const db = new BetterSqlite3(":memory:");
69
- db.pragma("journal_mode = WAL");
189
+ const db = newDatabase(":memory:");
190
+ db.exec("PRAGMA journal_mode = WAL");
70
191
  db.exec("CREATE TABLE _probe(x); INSERT INTO _probe VALUES (1);");
71
192
  const row = db.prepare("SELECT sqlite_version() AS v").get();
72
193
  db.close();
@@ -76,12 +197,12 @@ function probeEngine() {
76
197
  }
77
198
  }
78
199
  function openEngine(dbPath) {
79
- const native = new BetterSqlite3(dbPath);
80
- native.pragma("journal_mode = WAL");
81
- native.pragma("foreign_keys = ON");
82
- native.pragma("busy_timeout = 5000");
83
- native.pragma("synchronous = NORMAL");
84
- return new BetterSqliteAdapter(native);
200
+ const native = newDatabase(dbPath);
201
+ native.exec("PRAGMA journal_mode = WAL");
202
+ native.exec("PRAGMA foreign_keys = ON");
203
+ native.exec("PRAGMA busy_timeout = 5000");
204
+ native.exec("PRAGMA synchronous = NORMAL");
205
+ return new NodeSqliteAdapter(native);
85
206
  }
86
207
 
87
208
  // src/lib/db/migrations.ts
@@ -1484,7 +1605,7 @@ function walCheckpointTruncate(dbPath) {
1484
1605
  return "failed";
1485
1606
  } finally {
1486
1607
  try {
1487
- transient?.raw.close();
1608
+ transient?.close();
1488
1609
  } catch {
1489
1610
  }
1490
1611
  }
@@ -1502,6 +1623,41 @@ function closeAllDatabases() {
1502
1623
  connections.delete(path);
1503
1624
  }
1504
1625
  }
1626
+ function probeWrite() {
1627
+ let dir;
1628
+ try {
1629
+ dir = mkdtempSync(join3(tmpdir(), "ocr-probe-"));
1630
+ const db = openEngine(join3(dir, "probe.db"));
1631
+ try {
1632
+ db.run("CREATE TABLE _probe_write (id INTEGER PRIMARY KEY, v TEXT)");
1633
+ db.transaction(() => {
1634
+ db.run("INSERT INTO _probe_write (v) VALUES (?)", ["written-in-txn"]);
1635
+ });
1636
+ const value = db.exec("SELECT v FROM _probe_write")[0]?.values[0]?.[0];
1637
+ if (value !== "written-in-txn") {
1638
+ return { ok: false, error: `unexpected probe value: ${String(value)}` };
1639
+ }
1640
+ return { ok: true };
1641
+ } finally {
1642
+ db.close();
1643
+ }
1644
+ } catch (e) {
1645
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
1646
+ } finally {
1647
+ if (dir) rmDirBestEffort(dir);
1648
+ }
1649
+ }
1650
+ function rmDirBestEffort(dir) {
1651
+ for (let attempt = 0; attempt < 3; attempt++) {
1652
+ try {
1653
+ rmSync(dir, { recursive: true, force: true });
1654
+ return;
1655
+ } catch {
1656
+ if (attempt === 2) return;
1657
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
1658
+ }
1659
+ }
1660
+ }
1505
1661
  export {
1506
1662
  CANCELLED_EXIT_CODE,
1507
1663
  CASCADE_CLOSE_EXIT_CODE,
@@ -1540,6 +1696,7 @@ export {
1540
1696
  listAgentSessionsForWorkflow,
1541
1697
  openDatabase,
1542
1698
  probeEngine,
1699
+ probeWrite,
1543
1700
  readCommandLog,
1544
1701
  reconcileLegacyState,
1545
1702
  recordVendorSessionIdForExecution,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-code-review/cli",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "CLI for Open Code Review - Multi-environment setup and progress tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -63,7 +63,7 @@
63
63
  "license": "Apache-2.0",
64
64
  "author": "Spencer Marx",
65
65
  "engines": {
66
- "node": ">=20.0.0"
66
+ "node": ">=22.5.0"
67
67
  },
68
68
  "dependencies": {
69
69
  "@inquirer/prompts": "^7.2.0",
@@ -73,15 +73,13 @@
73
73
  "log-update": "^7.0.2",
74
74
  "ora": "^8.1.1",
75
75
  "socket.io": "^4.8",
76
- "better-sqlite3": "^11.8.1",
77
76
  "yaml": "^2.8.3",
78
- "@open-code-review/agents": "2.0.0"
77
+ "@open-code-review/agents": "2.1.0"
79
78
  },
80
79
  "publishConfig": {
81
80
  "access": "public"
82
81
  },
83
82
  "devDependencies": {
84
- "@types/better-sqlite3": "^7.6.12",
85
83
  "@open-code-review/platform": "0.0.0"
86
84
  }
87
85
  }