Skip to content

Commit 3dae2f0

Browse files
fix: four CLI improvements (embed --db, concurrent DB safety, prerelease update suppression, incremental build logging)
- Add missing --db flag to `embed` command, passing customDbPath to buildEmbeddings - Add busy_timeout pragma (5s) and advisory lockfile to openDb/closeDb for concurrent access safety - Suppress update-check notifications for prerelease/dev versions (contains '-') - Log changed/removed file names at debug level during incremental builds Impact: 8 functions changed, 14 affected
1 parent 2b6354d commit 3dae2f0

7 files changed

Lines changed: 106 additions & 7 deletions

File tree

src/builder.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fs from 'node:fs';
33
import path from 'node:path';
44
import { loadConfig } from './config.js';
55
import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
6-
import { getBuildMeta, initSchema, openDb, setBuildMeta } from './db.js';
6+
import { closeDb, getBuildMeta, initSchema, openDb, setBuildMeta } from './db.js';
77
import { readJournal, writeJournalHeader } from './journal.js';
88
import { debug, info, warn } from './logger.js';
99
import { getActiveEngine, parseFilesAuto } from './parser.js';
@@ -418,7 +418,7 @@ export async function buildGraph(rootDir, opts = {}) {
418418
}
419419
}
420420
info('No changes detected. Graph is up to date.');
421-
db.close();
421+
closeDb(db);
422422
writeJournalHeader(rootDir, Date.now());
423423
return;
424424
}
@@ -442,6 +442,8 @@ export async function buildGraph(rootDir, opts = {}) {
442442
);
443443
} else {
444444
info(`Incremental: ${parseChanges.length} changed, ${removed.length} removed`);
445+
if (parseChanges.length > 0) debug(`Changed files: ${parseChanges.map((c) => c.relPath).join(', ')}`);
446+
if (removed.length > 0) debug(`Removed files: ${removed.join(', ')}`);
445447
// Remove embeddings/metrics/edges/nodes for changed and removed files
446448
// Embeddings must be deleted BEFORE nodes (we need node IDs to find them)
447449
const deleteEmbeddingsForFile = hasEmbeddings
@@ -958,7 +960,7 @@ export async function buildGraph(rootDir, opts = {}) {
958960
debug(`Failed to write build metadata: ${err.message}`);
959961
}
960962

961-
db.close();
963+
closeDb(db);
962964

963965
// Write journal header after successful build
964966
writeJournalHeader(rootDir, Date.now());

src/cli.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ program
470470
`Embedding strategy: ${EMBEDDING_STRATEGIES.join(', ')}. "structured" uses graph context (callers/callees), "source" embeds raw code`,
471471
'structured',
472472
)
473+
.option('-d, --db <path>', 'Path to graph.db')
473474
.action(async (dir, opts) => {
474475
if (!EMBEDDING_STRATEGIES.includes(opts.strategy)) {
475476
console.error(
@@ -479,7 +480,7 @@ program
479480
}
480481
const root = path.resolve(dir || '.');
481482
const model = opts.model || config.embeddings?.model || DEFAULT_MODEL;
482-
await buildEmbeddings(root, model, undefined, { strategy: opts.strategy });
483+
await buildEmbeddings(root, model, opts.db, { strategy: opts.strategy });
483484
});
484485

485486
program

src/db.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
33
import Database from 'better-sqlite3';
4-
import { debug } from './logger.js';
4+
import { debug, warn } from './logger.js';
55

66
// ─── Schema Migrations ─────────────────────────────────────────────────
77
export const MIGRATIONS = [
@@ -134,11 +134,59 @@ export function setBuildMeta(db, entries) {
134134
export function openDb(dbPath) {
135135
const dir = path.dirname(dbPath);
136136
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
137+
acquireAdvisoryLock(dbPath);
137138
const db = new Database(dbPath);
138139
db.pragma('journal_mode = WAL');
140+
db.pragma('busy_timeout = 5000');
141+
db.__lockPath = `${dbPath}.lock`;
139142
return db;
140143
}
141144

145+
export function closeDb(db) {
146+
db.close();
147+
if (db.__lockPath) releaseAdvisoryLock(db.__lockPath);
148+
}
149+
150+
function isProcessAlive(pid) {
151+
try {
152+
process.kill(pid, 0);
153+
return true;
154+
} catch {
155+
return false;
156+
}
157+
}
158+
159+
function acquireAdvisoryLock(dbPath) {
160+
const lockPath = `${dbPath}.lock`;
161+
try {
162+
if (fs.existsSync(lockPath)) {
163+
const content = fs.readFileSync(lockPath, 'utf-8').trim();
164+
const pid = Number(content);
165+
if (pid && pid !== process.pid && isProcessAlive(pid)) {
166+
warn(`Another process (PID ${pid}) may be using this database. Proceeding with caution.`);
167+
}
168+
}
169+
} catch {
170+
/* ignore read errors */
171+
}
172+
try {
173+
fs.writeFileSync(lockPath, String(process.pid), 'utf-8');
174+
} catch {
175+
/* best-effort */
176+
}
177+
}
178+
179+
function releaseAdvisoryLock(lockPath) {
180+
try {
181+
const content = fs.readFileSync(lockPath, 'utf-8').trim();
182+
if (Number(content) === process.pid) {
183+
fs.unlinkSync(lockPath);
184+
}
185+
} catch {
186+
/* ignore */
187+
}
188+
}
189+
142190
export function initSchema(db) {
143191
db.exec(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL DEFAULT 0)`);
144192

src/update-check.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export async function checkForUpdates(currentVersion, options = {}) {
109109
if (process.env.CI) return null;
110110
if (process.env.NO_UPDATE_CHECK) return null;
111111
if (!process.stderr.isTTY) return null;
112+
if (currentVersion.includes('-')) return null;
112113

113114
const cachePath = options.cachePath || CACHE_PATH;
114115
const fetchFn = options._fetchLatest || fetchLatestVersion;

src/watcher.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from 'node:fs';
22
import path from 'node:path';
33
import { readFileSafe } from './builder.js';
44
import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
5-
import { initSchema, openDb } from './db.js';
5+
import { closeDb, initSchema, openDb } from './db.js';
66
import { appendJournalEntries } from './journal.js';
77
import { info, warn } from './logger.js';
88
import { createParseTreeCache, getActiveEngine, parseFileIncremental } from './parser.js';
@@ -261,7 +261,7 @@ export async function watchProject(rootDir, opts = {}) {
261261
}
262262
}
263263
if (cache) cache.clear();
264-
db.close();
264+
closeDb(db);
265265
process.exit(0);
266266
});
267267
}

tests/unit/db.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import path from 'node:path';
88
import Database from 'better-sqlite3';
99
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
1010
import {
11+
closeDb,
1112
findDbPath,
1213
getBuildMeta,
1314
initSchema,
@@ -91,6 +92,24 @@ describe('openDb', () => {
9192
expect(row.name).toBe('test');
9293
db.close();
9394
});
95+
96+
it('sets busy_timeout pragma to 5000', () => {
97+
const dbPath = path.join(tmpDir, 'busy-timeout.db');
98+
const db = openDb(dbPath);
99+
const timeout = db.pragma('busy_timeout', { simple: true });
100+
expect(timeout).toBe(5000);
101+
closeDb(db);
102+
});
103+
104+
it('creates lock file on open and removes on closeDb', () => {
105+
const dbPath = path.join(tmpDir, 'locktest.db');
106+
const lockPath = `${dbPath}.lock`;
107+
const db = openDb(dbPath);
108+
expect(fs.existsSync(lockPath)).toBe(true);
109+
expect(fs.readFileSync(lockPath, 'utf-8').trim()).toBe(String(process.pid));
110+
closeDb(db);
111+
expect(fs.existsSync(lockPath)).toBe(false);
112+
});
94113
});
95114

96115
describe('findDbPath', () => {

tests/unit/update-check.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,34 @@ describe('checkForUpdates', () => {
240240
}
241241
});
242242

243+
it('returns null for prerelease versions (e.g. beta)', async () => {
244+
const origIsTTY = process.stderr.isTTY;
245+
process.stderr.isTTY = true;
246+
try {
247+
const result = await checkForUpdates('2.0.0-beta.1', {
248+
cachePath,
249+
_fetchLatest: async () => '2.0.0',
250+
});
251+
expect(result).toBeNull();
252+
} finally {
253+
process.stderr.isTTY = origIsTTY;
254+
}
255+
});
256+
257+
it('returns null for dev versions (e.g. -dev)', async () => {
258+
const origIsTTY = process.stderr.isTTY;
259+
process.stderr.isTTY = true;
260+
try {
261+
const result = await checkForUpdates('1.5.0-dev', {
262+
cachePath,
263+
_fetchLatest: async () => '2.0.0',
264+
});
265+
expect(result).toBeNull();
266+
} finally {
267+
process.stderr.isTTY = origIsTTY;
268+
}
269+
});
270+
243271
it('returns null from fresh cache when version is current', async () => {
244272
const origIsTTY = process.stderr.isTTY;
245273
process.stderr.isTTY = true;

0 commit comments

Comments
 (0)