Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
446 changes: 2 additions & 444 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions packages/cli/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ const buildOptions = {
entryPoints: [join(__dirname, 'src/index.ts')],
bundle: true,
platform: 'node' as const,
target: 'node18',
target: 'node22',
format: 'esm' as const,
outfile: join(distDir, 'index.js'),
banner: {
js: '#!/usr/bin/env node',
},
external: [
'better-sqlite3',
'commander',
'open',
'picocolors',
Expand Down
4 changes: 1 addition & 3 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"dev:watch": "tsx build.ts --watch"
},
"dependencies": {
"better-sqlite3": "^12.8.0",
"commander": "^14.0.3",
"dayjs": "^1.11.20",
"open": "^11.0.0",
Expand Down Expand Up @@ -40,10 +39,9 @@
"url": "https://github.com/kamranahmedse/diffity/issues"
},
"engines": {
"node": ">=18"
"node": ">=22.13"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^25.5.0",
"esbuild": "^0.27.0",
"tsx": "^4.21.0",
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ export function registerDoctorCommand(program: Command, version: string) {

process.stdout.write(' sqlite ');
try {
require('better-sqlite3');
console.log(pc.green('✓ better-sqlite3 loaded'));
require('node:sqlite');
console.log(pc.green('✓ node:sqlite available'));
} catch {
console.log(pc.red('✗ better-sqlite3 failed to load (native module issue)'));
console.log(pc.red(`✗ node:sqlite unavailable (needs Node >= 22.13, running ${process.version})`));
ok = false;
}

Expand Down
39 changes: 33 additions & 6 deletions packages/cli/src/db.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,50 @@
import Database from 'better-sqlite3';
import { createRequire } from 'node:module';
import { join } from 'node:path';
import type { DatabaseSync, SQLInputValue } from 'node:sqlite';
import { getDiffityDir } from '@diffity/git';

let db: Database.Database | null = null;
const require = createRequire(import.meta.url);

export function getDb(): Database.Database {
let db: DatabaseSync | null = null;

// Loaded lazily: `node:sqlite` only exists on Node >= 22.13, and a static import
// would abort the whole CLI at startup instead of showing this hint.
function loadSqlite(): { DatabaseSync: new (path: string) => DatabaseSync } {
try {
return require('node:sqlite');
} catch {
throw new Error(
`diffity needs Node's built-in sqlite module, which requires Node >= 22.13 (running ${process.version}).`,
);
}
}

export function getDb(): DatabaseSync {
if (db) {
return db;
}

const { DatabaseSync: Database } = loadSqlite();
const dbPath = join(getDiffityDir(), 'reviews.db');
db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA foreign_keys = ON');
migrateDb(db);
return db;
}

function migrateDb(db: Database.Database): void {
// node:sqlite types every row as `Record<string, SQLOutputValue>`, so the shape a
// query returns has to be asserted. These helpers keep that assertion in one place
// rather than at every call site.
export function queryAll<T>(sql: string, ...params: SQLInputValue[]): T[] {
return getDb().prepare(sql).all(...params) as T[];
}

export function queryOne<T>(sql: string, ...params: SQLInputValue[]): T | undefined {
return getDb().prepare(sql).get(...params) as T | undefined;
}

function migrateDb(db: DatabaseSync): void {
db.exec(`
CREATE TABLE IF NOT EXISTS review_sessions (
id TEXT PRIMARY KEY,
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
import { readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { getHeadHash, getDiffityDir } from '@diffity/git';
import { getDb } from './db.js';
import { getDb, queryOne } from './db.js';

export interface Session {
id: string;
Expand All @@ -18,9 +18,11 @@ export function findOrCreateSession(ref: string): Session {
const db = getDb();
const headHash = getHeadHash();

const existing = db.prepare(
'SELECT id, ref, head_hash FROM review_sessions WHERE ref = ? AND head_hash = ?'
).get(ref, headHash) as { id: string; ref: string; head_hash: string } | undefined;
const existing = queryOne<{ id: string; ref: string; head_hash: string }>(
'SELECT id, ref, head_hash FROM review_sessions WHERE ref = ? AND head_hash = ?',
ref,
headHash,
);

if (existing) {
const session: Session = { id: existing.id, ref: existing.ref, headHash: existing.head_hash };
Expand Down
26 changes: 12 additions & 14 deletions packages/cli/src/threads.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { randomUUID } from 'node:crypto';
import { getDb } from './db.js';
import { getDb, queryAll, queryOne } from './db.js';
import { unescapeMarkdown } from './unescape.js';

export interface ThreadAuthor {
Expand Down Expand Up @@ -81,11 +81,11 @@ function getCommentsForThreads(threadIds: string[]): Map<string, ThreadComment[]
if (threadIds.length === 0) {
return new Map();
}
const db = getDb();
const placeholders = threadIds.map(() => '?').join(', ');
const rows = db.prepare(
`SELECT * FROM comments WHERE thread_id IN (${placeholders}) ORDER BY created_at ASC`
).all(...threadIds) as CommentRow[];
const rows = queryAll<CommentRow>(
`SELECT * FROM comments WHERE thread_id IN (${placeholders}) ORDER BY created_at ASC`,
...threadIds,
);

const map = new Map<string, ThreadComment[]>();
for (const row of rows) {
Expand Down Expand Up @@ -155,21 +155,20 @@ interface JoinedRow extends ThreadRow {
}

export function getThreadsForSession(sessionId: string, status?: ThreadStatus): Thread[] {
const db = getDb();
const where = status
? 'WHERE t.session_id = ? AND t.status = ?'
: 'WHERE t.session_id = ?';
const params = status ? [sessionId, status] : [sessionId];

const rows = db.prepare(`
const rows = queryAll<JoinedRow>(`
SELECT t.*,
c.id AS c_id, c.author_name AS c_author_name, c.author_type AS c_author_type,
c.body AS c_body, c.created_at AS c_created_at
FROM comment_threads t
LEFT JOIN comments c ON c.thread_id = t.id
${where}
ORDER BY t.created_at ASC, c.created_at ASC
`).all(...params) as JoinedRow[];
`, ...params);

const threads = new Map<string, Thread>();
for (const row of rows) {
Expand All @@ -191,11 +190,10 @@ export function getThreadsForSession(sessionId: string, status?: ThreadStatus):
}

export function getThread(idOrPrefix: string): Thread | null {
const db = getDb();
let row = db.prepare('SELECT * FROM comment_threads WHERE id = ?').get(idOrPrefix) as ThreadRow | undefined;
let row = queryOne<ThreadRow>('SELECT * FROM comment_threads WHERE id = ?', idOrPrefix);

if (!row && idOrPrefix.length >= 8) {
row = db.prepare('SELECT * FROM comment_threads WHERE id LIKE ?').get(idOrPrefix + '%') as ThreadRow | undefined;
row = queryOne<ThreadRow>('SELECT * FROM comment_threads WHERE id LIKE ?', idOrPrefix + '%');
}

if (!row) {
Expand Down Expand Up @@ -267,15 +265,15 @@ export function editComment(commentId: string, body: string): void {

export function deleteComment(commentId: string): void {
const db = getDb();
const comment = db.prepare('SELECT thread_id FROM comments WHERE id = ?').get(commentId) as { thread_id: string } | undefined;
const comment = queryOne<{ thread_id: string }>('SELECT thread_id FROM comments WHERE id = ?', commentId);
if (!comment) {
return;
}

db.prepare('DELETE FROM comments WHERE id = ?').run(commentId);

const remaining = db.prepare('SELECT COUNT(*) as count FROM comments WHERE thread_id = ?').get(comment.thread_id) as { count: number };
if (remaining.count === 0) {
const remaining = queryOne<{ count: number }>('SELECT COUNT(*) as count FROM comments WHERE thread_id = ?', comment.thread_id);
if (remaining?.count === 0) {
db.prepare('DELETE FROM comment_threads WHERE id = ?').run(comment.thread_id);
}
}
27 changes: 13 additions & 14 deletions packages/cli/src/tours.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { randomUUID } from 'node:crypto';
import { getDb } from './db.js';
import { getDb, queryAll, queryOne } from './db.js';
import { unescapeMarkdown } from './unescape.js';

export type TourStatus = 'building' | 'ready';
Expand Down Expand Up @@ -96,23 +96,21 @@ export function createTour(sessionId: string, topic: string, body: string): Tour
}

export function getTour(id: string): Tour | null {
const db = getDb();
const row = db.prepare('SELECT * FROM tours WHERE id = ?').get(id) as TourRow | undefined;
const row = queryOne<TourRow>('SELECT * FROM tours WHERE id = ?', id);

if (!row) {
return null;
}

const stepRows = db.prepare(
'SELECT * FROM tour_steps WHERE tour_id = ? ORDER BY sort_order ASC'
).all(id) as TourStepRow[];
const stepRows = queryAll<TourStepRow>(
'SELECT * FROM tour_steps WHERE tour_id = ? ORDER BY sort_order ASC',
id,
);

return rowToTour(row, stepRows.map(rowToTourStep));
}

export function getToursForSession(sessionId: string): Tour[] {
const db = getDb();

interface JoinedRow extends TourRow {
s_id: string | null;
s_sort_order: number | null;
Expand All @@ -124,7 +122,7 @@ export function getToursForSession(sessionId: string): Tour[] {
s_created_at: string | null;
}

const rows = db.prepare(`
const rows = queryAll<JoinedRow>(`
SELECT t.*,
s.id AS s_id, s.sort_order AS s_sort_order, s.file_path AS s_file_path,
s.start_line AS s_start_line, s.end_line AS s_end_line,
Expand All @@ -133,7 +131,7 @@ export function getToursForSession(sessionId: string): Tour[] {
LEFT JOIN tour_steps s ON s.tour_id = t.id
WHERE t.session_id = ?
ORDER BY t.created_at ASC, s.sort_order ASC
`).all(sessionId) as JoinedRow[];
`, sessionId);

const tours = new Map<string, Tour>();
for (const row of rows) {
Expand Down Expand Up @@ -172,10 +170,11 @@ export function addTourStep(
const id = randomUUID();
const now = new Date().toISOString();

const maxRow = db.prepare(
'SELECT COALESCE(MAX(sort_order), 0) AS max_order FROM tour_steps WHERE tour_id = ?'
).get(tourId) as { max_order: number };
const sortOrder = maxRow.max_order + 1;
const maxRow = queryOne<{ max_order: number }>(
'SELECT COALESCE(MAX(sort_order), 0) AS max_order FROM tour_steps WHERE tour_id = ?',
tourId,
);
const sortOrder = (maxRow?.max_order ?? 0) + 1;

const cleanBody = unescapeMarkdown(body);
const cleanAnnotation = unescapeMarkdown(annotation);
Expand Down