refactor(app): migrate main + preload to TypeScript with typed seams (Phase 5d-3c-ii)

Convert the app's main and preload source from .js to .ts (git mv preserves
history), adding types where they carry value: the core-consumption seam
(BuildIndexOptions/BuildIndexResult, FileInfo), the indexer service/worker
factories, and the preload IPC bridge. Module-to-module specifiers use the
real .ts extension (mirroring scripts/, since Node type-stripping does not
rewrite .js->.ts); the worker's runtime path stays indexer-worker.js because
that is the built output.

Toolchain:
- Add app/tsconfig.json: strict but noImplicitAny:false (the app orchestrates
  the already-strict core; annotating every SQLite-handle helper is low-value
  churn) + allowImportingTsExtensions (safe under noEmit).
- Add @types/better-sqlite3 for the injected binding.
- electron.vite.config.ts inputs -> .ts; refresh the stale CommonJS comment.
- typecheck script runs root + app projects. Root tsconfig excludes the
  app-importing tests (app-*.test.mjs, recap-capture-query.test.mjs) so the
  lenient app files are not dragged into the strict root program; the app
  source is covered by app/tsconfig.json instead. See docs/adr/0005.

Verified: npm run typecheck (root + app) clean; suite 121/121; electron-vite
build emits all 6 main entries + preload with no .ts/node:sqlite residue in
the bundles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tommy0103
2026-07-09 16:25:59 +08:00
co-authored by Claude Opus 4.8
parent 60d47a852e
commit 01a390fa10
21 changed files with 299 additions and 151 deletions
+10 -10
View File
@@ -2,22 +2,22 @@ import { resolve } from 'node:path';
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'; import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
// Kept CommonJS for the first electron-vite boot (no "type":"module" yet); the // The app main/preload/renderer are TypeScript + ESM. Each main-process module
// TS + ESM migration is a later stage. Each main-process module is its own input // is its own rollup input so it is emitted to out/main/<name>.js and the
// so it is emitted to out/main/<name>.js and the CommonJS require("./x") calls // relative imports between them (and `new Worker(__dirname/indexer-worker.js)`)
// between them (and `new Worker(__dirname/indexer-worker.js)`) resolve at runtime. // resolve to the built .js at runtime.
export default defineConfig({ export default defineConfig({
main: { main: {
plugins: [externalizeDepsPlugin()], plugins: [externalizeDepsPlugin()],
build: { build: {
rollupOptions: { rollupOptions: {
input: { input: {
index: resolve('src/main/index.js'), index: resolve('src/main/index.ts'),
indexer: resolve('src/main/indexer.js'), indexer: resolve('src/main/indexer.ts'),
'indexer-service': resolve('src/main/indexer-service.js'), 'indexer-service': resolve('src/main/indexer-service.ts'),
'indexer-worker': resolve('src/main/indexer-worker.js'), 'indexer-worker': resolve('src/main/indexer-worker.ts'),
'indexer-worker-client': resolve('src/main/indexer-worker-client.js'), 'indexer-worker-client': resolve('src/main/indexer-worker-client.ts'),
'recap-capture-query': resolve('src/main/recap-capture-query.js'), 'recap-capture-query': resolve('src/main/recap-capture-query.ts'),
}, },
}, },
}, },
+11
View File
@@ -12,6 +12,7 @@
"chokidar": "^4.0.3" "chokidar": "^4.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@vitejs/plugin-vue": "^5.0.0", "@vitejs/plugin-vue": "^5.0.0",
"electron": "^33.0.0", "electron": "^33.0.0",
"electron-builder": "^25.0.0", "electron-builder": "^25.0.0",
@@ -1848,6 +1849,16 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/@types/better-sqlite3": {
"version": "7.6.13",
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/cacheable-request": { "node_modules/@types/cacheable-request": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
+1
View File
@@ -60,6 +60,7 @@
"chokidar": "^4.0.3" "chokidar": "^4.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@vitejs/plugin-vue": "^5.0.0", "@vitejs/plugin-vue": "^5.0.0",
"electron": "^33.0.0", "electron": "^33.0.0",
"electron-builder": "^25.0.0", "electron-builder": "^25.0.0",
+16 -15
View File
@@ -5,10 +5,10 @@ import fs from 'node:fs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import Database from 'better-sqlite3'; import Database from 'better-sqlite3';
import chokidar from 'chokidar'; import chokidar from 'chokidar';
import { writeHeartbeat } from './indexer.js'; import { writeHeartbeat } from './indexer.ts';
import { createIndexerService } from './indexer-service.js'; import { createIndexerService } from './indexer-service.ts';
import { createWorkerBuildIndex } from './indexer-worker-client.js'; import { createWorkerBuildIndex } from './indexer-worker-client.ts';
import { buildRecapExportQuery } from './recap-capture-query.js'; import { buildRecapExportQuery } from './recap-capture-query.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -68,7 +68,7 @@ function migrateLegacyDbIfNeeded(paths = getPathsForClaudeDir()) {
fs.mkdirSync(path.dirname(paths.dbPath), { recursive: true }); fs.mkdirSync(path.dirname(paths.dbPath), { recursive: true });
fs.copyFileSync(legacyDbPath, paths.dbPath); fs.copyFileSync(legacyDbPath, paths.dbPath);
} catch (error) { } catch (error) {
console.warn?.(`Obelisk legacy DB migration skipped: ${error.message}`); console.warn?.(`Obelisk legacy DB migration skipped: ${(error as Error).message}`);
} }
} }
@@ -111,7 +111,7 @@ function resolveSchemaPath() {
path.join(__dirname, 'schema.sql'), path.join(__dirname, 'schema.sql'),
path.join(__dirname, '..', 'scripts', 'schema.sql'), path.join(__dirname, '..', 'scripts', 'schema.sql'),
process.resourcesPath ? path.join(process.resourcesPath, 'scripts', 'schema.sql') : null, process.resourcesPath ? path.join(process.resourcesPath, 'scripts', 'schema.sql') : null,
].filter(Boolean); ].filter((c): c is string => Boolean(c));
return candidates.find(p => fs.existsSync(p)); return candidates.find(p => fs.existsSync(p));
} }
@@ -160,7 +160,7 @@ function openDb(dbPath = getPathsForClaudeDir().dbPath) {
return db; return db;
} }
function notifyIndexUpdated(result = {}) { function notifyIndexUpdated(result: { affectedSessionIds?: unknown } = {}) {
const affectedSessionIds = Array.isArray(result.affectedSessionIds) const affectedSessionIds = Array.isArray(result.affectedSessionIds)
? [...new Set(result.affectedSessionIds.filter(Boolean))] ? [...new Set(result.affectedSessionIds.filter(Boolean))]
: []; : [];
@@ -173,7 +173,7 @@ function notifyIndexUpdated(result = {}) {
} }
} }
function sourceWhereClause(opts = {}, column = 'source') { function sourceWhereClause(opts: { includeCodex?: boolean; source?: string } = {}, column = "source"): { sql: string; params: unknown[] } {
if (opts.includeCodex || opts.source === 'all') return { sql: '', params: [] }; if (opts.includeCodex || opts.source === 'all') return { sql: '', params: [] };
if (opts.source) return { sql: `COALESCE(${column}, 'claude') = ?`, params: [opts.source] }; if (opts.source) return { sql: `COALESCE(${column}, 'claude') = ?`, params: [opts.source] };
return { sql: `COALESCE(${column}, 'claude') = 'claude'`, params: [] }; return { sql: `COALESCE(${column}, 'claude') = 'claude'`, params: [] };
@@ -283,7 +283,7 @@ function createWindow() {
const OBELISK_DIR = path.join(os.homedir(), '.obelisk'); const OBELISK_DIR = path.join(os.homedir(), '.obelisk');
const RECAP_DIR = path.join(OBELISK_DIR, 'recap'); const RECAP_DIR = path.join(OBELISK_DIR, 'recap');
let obeliskWatcher = null; let obeliskWatcher: import("chokidar").FSWatcher | null = null;
function startObeliskWatcher() { function startObeliskWatcher() {
if (obeliskWatcher) return obeliskWatcher; if (obeliskWatcher) return obeliskWatcher;
@@ -340,7 +340,7 @@ ipcMain.handle('db:getSessions', (_, opts = {}) => {
if (!db) return []; if (!db) return [];
const { project, limit = 200 } = opts; const { project, limit = 200 } = opts;
let sql = `SELECT id, title, project, project_path, started_at, ended_at, git_branch, version, message_count, jsonl_path, source FROM sessions`; let sql = `SELECT id, title, project, project_path, started_at, ended_at, git_branch, version, message_count, jsonl_path, source FROM sessions`;
const params = []; const params: unknown[] = [];
const sourceFilter = sourceWhereClause(opts); const sourceFilter = sourceWhereClause(opts);
if (sourceFilter.sql) { if (sourceFilter.sql) {
sql = appendWhere(sql, params, sourceFilter.sql); sql = appendWhere(sql, params, sourceFilter.sql);
@@ -437,7 +437,7 @@ ipcMain.handle('db:getMessageFullText', (_, uuid) => {
if (!match) return null; if (!match) return null;
const rawThreadId = match[1]; const rawThreadId = match[1];
const targetLine = Number(match[2]); const targetLine = Number(match[2]);
let jsonlPath = null; let jsonlPath: string | null = null;
if (!msg.agent_id) { if (!msg.agent_id) {
jsonlPath = db.prepare('SELECT jsonl_path FROM sessions WHERE id=?').get(msg.session_id)?.jsonl_path || null; jsonlPath = db.prepare('SELECT jsonl_path FROM sessions WHERE id=?').get(msg.session_id)?.jsonl_path || null;
} }
@@ -469,7 +469,7 @@ ipcMain.handle('db:getMessageFullText', (_, uuid) => {
} }
// Resolve JSONL path // Resolve JSONL path
let jsonlPath = null; let jsonlPath: string | null = null;
if (msg.agent_id) { if (msg.agent_id) {
const wa = db.prepare('SELECT agent_id, run_id, session_id FROM workflow_agents WHERE agent_id=?').get(msg.agent_id); const wa = db.prepare('SELECT agent_id, run_id, session_id FROM workflow_agents WHERE agent_id=?').get(msg.agent_id);
if (wa) { if (wa) {
@@ -501,7 +501,7 @@ ipcMain.handle('db:getMessageFullText', (_, uuid) => {
const content = obj.message?.content; const content = obj.message?.content;
if (typeof content === 'string') return content; if (typeof content === 'string') return content;
if (!Array.isArray(content)) return null; if (!Array.isArray(content)) return null;
const parts = []; const parts: string[] = [];
for (const b of content) { for (const b of content) {
if (b.type === 'text' && b.text) parts.push(b.text); if (b.type === 'text' && b.text) parts.push(b.text);
else if (b.type === 'thinking' && b.thinking) parts.push(b.thinking); else if (b.type === 'thinking' && b.thinking) parts.push(b.thinking);
@@ -614,7 +614,7 @@ async function createExportCapture(parentWin, query) {
nodeIntegration: false, nodeIntegration: false,
offscreen: true, offscreen: true,
deviceScaleFactor: 2, deviceScaleFactor: 2,
}, } as Electron.WebPreferences,
}); });
const isDev = process.argv.includes('--dev') || !!process.env.ELECTRON_RENDERER_URL; const isDev = process.argv.includes('--dev') || !!process.env.ELECTRON_RENDERER_URL;
@@ -802,6 +802,7 @@ ipcMain.handle('settings:set', async (_, key, value) => {
ipcMain.handle('settings:browseFolder', async (event) => { ipcMain.handle('settings:browseFolder', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender); const win = BrowserWindow.fromWebContents(event.sender);
if (!win) return null;
const { filePaths } = await dialog.showOpenDialog(win, { const { filePaths } = await dialog.showOpenDialog(win, {
properties: ['openDirectory'], properties: ['openDirectory'],
title: 'Select Claude Code data folder', title: 'Select Claude Code data folder',
@@ -851,7 +852,7 @@ ipcMain.handle('settings:rebuildIndex', async () => {
try { try {
openDb(paths.dbPath); openDb(paths.dbPath);
} catch (error) { } catch (error) {
console.warn?.(`Obelisk DB reopen after rebuild failed: ${error.message}`); console.warn?.(`Obelisk DB reopen after rebuild failed: ${(error as Error).message}`);
} }
} }
if (shouldRestartWatcher) startIndexerService({ buildOnStart: false }); if (shouldRestartWatcher) startIndexerService({ buildOnStart: false });
@@ -9,6 +9,34 @@ const DEFAULT_STABILITY_MS = 500;
const DEFAULT_HEARTBEAT_MS = 30000; const DEFAULT_HEARTBEAT_MS = 30000;
const DEFAULT_WATCH_RETRY_MS = 5000; const DEFAULT_WATCH_RETRY_MS = 5000;
type TimerHandle = ReturnType<typeof setTimeout>;
interface Timers {
setTimeout: (fn: () => void, ms?: number) => TimerHandle;
clearTimeout: (handle: TimerHandle) => void;
setInterval?: (fn: () => void, ms?: number) => TimerHandle;
clearInterval?: (handle: TimerHandle) => void;
}
interface Watcher {
close(): unknown;
}
interface IndexerServiceOptions {
projectsDir?: string;
watchDirs?: string | string[];
debounceMs?: number;
stabilityMs?: number;
heartbeatMs?: number;
watchRetryMs?: number;
buildIndex?: (args: { reason?: string; changedPaths?: string[] }) => unknown;
writeHeartbeat?: () => void;
watchProjects?: (onChange: (changedPath: string) => void) => Watcher | null;
chokidar?: any;
timers?: Timers;
logger?: { warn?: (msg: string) => void };
}
function createIndexerService({ function createIndexerService({
projectsDir = DEFAULT_PROJECTS_DIR, projectsDir = DEFAULT_PROJECTS_DIR,
watchDirs = [projectsDir], watchDirs = [projectsDir],
@@ -27,13 +55,13 @@ function createIndexerService({
clearInterval, clearInterval,
}, },
logger = console, logger = console,
} = {}) { }: IndexerServiceOptions = {}) {
if (typeof buildIndex !== 'function') throw new Error('createIndexerService() requires buildIndex'); if (typeof buildIndex !== 'function') throw new Error('createIndexerService() requires buildIndex');
const watch = watchProjects || ((onChange) => { const watch = watchProjects || ((onChange) => {
const roots = [...new Set((Array.isArray(watchDirs) ? watchDirs : [watchDirs]).filter(Boolean))]; const roots = [...new Set((Array.isArray(watchDirs) ? watchDirs : [watchDirs]).filter(Boolean))];
const existingRoots = roots.filter(root => fs.existsSync(root)); const existingRoots = roots.filter(root => fs.existsSync(root));
if (!existingRoots.length) return null; if (!existingRoots.length) return null;
const watchers = []; const watchers: any[] = [];
const onFileChange = (filename) => { const onFileChange = (filename) => {
const name = filename ? String(filename) : ''; const name = filename ? String(filename) : '';
if (!name || name.endsWith('.jsonl') || name.endsWith('.json')) onChange(name); if (!name || name.endsWith('.jsonl') || name.endsWith('.json')) onChange(name);
@@ -57,7 +85,7 @@ function createIndexerService({
.on('change', onFileChange) .on('change', onFileChange)
.on('unlink', onFileChange) .on('unlink', onFileChange)
.on('error', (error) => { .on('error', (error) => {
logger.warn?.(`Obelisk watcher failed: ${error.message}`); logger.warn?.(`Obelisk watcher failed: ${(error as Error).message}`);
}); });
watchers.push(watcher); watchers.push(watcher);
} }
@@ -68,19 +96,19 @@ function createIndexerService({
}; };
}); });
let buildTimer = null; let buildTimer: TimerHandle | null = null;
let stabilityTimer = null; let stabilityTimer: TimerHandle | null = null;
let heartbeatTimer = null; let heartbeatTimer: TimerHandle | null = null;
let watchRetryTimer = null; let watchRetryTimer: TimerHandle | null = null;
let watcher = null; let watcher: Watcher | null = null;
let stopped = false; let stopped = false;
let running = false; let running = false;
let pending = false; let pending = false;
let lastReason = null; let lastReason: string | null = null;
let changedPaths = new Set(); let changedPaths = new Set<string>();
let idlePromise = Promise.resolve(); let idlePromise = Promise.resolve();
const addChangedPath = (changedPath) => { const addChangedPath = (changedPath?: string | string[]) => {
if (Array.isArray(changedPath)) { if (Array.isArray(changedPath)) {
for (const p of changedPath) addChangedPath(p); for (const p of changedPath) addChangedPath(p);
return; return;
@@ -96,7 +124,7 @@ function createIndexerService({
return paths; return paths;
}; };
const runBuildNow = (reason = 'manual', paths = undefined) => { const runBuildNow = (reason = "manual", paths: string[] | undefined = undefined) => {
addChangedPath(paths); addChangedPath(paths);
if (stopped) return idlePromise; if (stopped) return idlePromise;
if (running) { if (running) {
@@ -113,7 +141,7 @@ function createIndexerService({
.catch((error) => { .catch((error) => {
// A build in flight when the service is stopped (e.g. a manual rebuild // A build in flight when the service is stopped (e.g. a manual rebuild
// tears down the worker) is a deliberate cancellation, not a failure. // tears down the worker) is a deliberate cancellation, not a failure.
if (!stopped) logger.warn?.(`Obelisk index build failed: ${error.message}`); if (!stopped) logger.warn?.(`Obelisk index build failed: ${(error as Error).message}`);
}) })
.finally(() => { .finally(() => {
running = false; running = false;
@@ -125,7 +153,7 @@ function createIndexerService({
return idlePromise; return idlePromise;
}; };
const scheduleBuild = (reason = 'change', changedPath = undefined) => { const scheduleBuild = (reason = "change", changedPath: string | undefined = undefined) => {
if (stopped) return; if (stopped) return;
addChangedPath(changedPath); addChangedPath(changedPath);
lastReason = reason; lastReason = reason;
@@ -165,7 +193,7 @@ function createIndexerService({
try { try {
writeHeartbeat(); writeHeartbeat();
} catch (error) { } catch (error) {
logger.warn?.(`Obelisk heartbeat failed: ${error.message}`); logger.warn?.(`Obelisk heartbeat failed: ${(error as Error).message}`);
} }
}, heartbeatMs); }, heartbeatMs);
} }
@@ -4,23 +4,41 @@ import { Worker } from 'node:worker_threads';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
interface WorkerMessage {
id: number;
result?: unknown;
error?: { message: string; stack?: string };
}
interface PendingBuild {
resolve: (value: unknown) => void;
reject: (error: Error) => void;
}
interface WorkerBuildIndexOptions {
workerPath?: string;
WorkerImpl?: typeof Worker;
}
function createWorkerBuildIndex({ function createWorkerBuildIndex({
// indexer-worker.js is the built worker output emitted next to this module.
workerPath = path.join(__dirname, 'indexer-worker.js'), workerPath = path.join(__dirname, 'indexer-worker.js'),
WorkerImpl = Worker, WorkerImpl = Worker,
} = {}) { }: WorkerBuildIndexOptions = {}) {
let worker = null; let worker: Worker | null = null;
let nextId = 1; let nextId = 1;
const pending = new Map(); const pending = new Map<number, PendingBuild>();
const rejectPending = (error) => { const rejectPending = (error: Error) => {
for (const { reject } of pending.values()) reject(error); for (const { reject } of pending.values()) reject(error);
pending.clear(); pending.clear();
}; };
const ensureWorker = () => { const ensureWorker = (): Worker => {
if (worker) return worker; if (worker) return worker;
worker = new WorkerImpl(workerPath, { type: 'module' }); const active = new WorkerImpl(workerPath, { type: 'module' } as ConstructorParameters<typeof Worker>[1]);
worker.on('message', (message) => { worker = active;
active.on('message', (message: WorkerMessage) => {
const current = pending.get(message.id); const current = pending.get(message.id);
if (!current) return; if (!current) return;
pending.delete(message.id); pending.delete(message.id);
@@ -32,18 +50,18 @@ function createWorkerBuildIndex({
current.resolve(message.result); current.resolve(message.result);
} }
}); });
worker.on('error', (error) => { active.on('error', (error: Error) => {
rejectPending(error); rejectPending(error);
worker = null; worker = null;
}); });
worker.on('exit', (code) => { active.on('exit', (code: number) => {
if (pending.size) rejectPending(new Error(`Indexer worker exited with code ${code}`)); if (pending.size) rejectPending(new Error(`Indexer worker exited with code ${code}`));
worker = null; worker = null;
}); });
return worker; return active;
}; };
const buildIndex = (args = {}) => new Promise((resolve, reject) => { const buildIndex = (args: Record<string, unknown> = {}) => new Promise((resolve, reject) => {
const id = nextId++; const id = nextId++;
pending.set(id, { resolve, reject }); pending.set(id, { resolve, reject });
ensureWorker().postMessage({ id, args }); ensureWorker().postMessage({ id, args });
-17
View File
@@ -1,17 +0,0 @@
import { parentPort } from 'node:worker_threads';
import { buildIndex } from './indexer.js';
parentPort.on('message', ({ id, args }) => {
try {
const result = buildIndex(args || {});
parentPort.postMessage({ id, result });
} catch (error) {
parentPort.postMessage({
id,
error: {
message: error.message,
stack: error.stack,
},
});
}
});
+20
View File
@@ -0,0 +1,20 @@
import { parentPort } from 'node:worker_threads';
import { buildIndex } from './indexer.ts';
if (!parentPort) throw new Error('indexer-worker must run as a worker thread');
const port = parentPort;
port.on('message', ({ id, args }: { id: number; args?: Record<string, unknown> }) => {
try {
const result = buildIndex(args || {});
port.postMessage({ id, result });
} catch (error) {
port.postMessage({
id,
error: {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
},
});
}
});
@@ -25,12 +25,22 @@ const DEFAULT_DB_PATH = path.join(DEFAULT_OBELISK_DIR, 'obelisk.sqlite');
const DEFAULT_PROJECTS_DIR = path.join(DEFAULT_CLAUDE_DIR, 'projects'); const DEFAULT_PROJECTS_DIR = path.join(DEFAULT_CLAUDE_DIR, 'projects');
const DEFAULT_HISTORY_PATH = path.join(DEFAULT_CLAUDE_DIR, 'history.jsonl'); const DEFAULT_HISTORY_PATH = path.join(DEFAULT_CLAUDE_DIR, 'history.jsonl');
interface FileInfo {
path: string;
sessionId?: string;
project?: string;
isSubagent?: boolean;
agentId?: string;
workflowRunId?: string;
source?: string;
}
function resolveSchemaPath() { function resolveSchemaPath() {
const candidates = [ const candidates = [
path.join(__dirname, 'schema.sql'), path.join(__dirname, 'schema.sql'),
path.join(__dirname, '..', '..', '..', 'scripts', 'schema.sql'), path.join(__dirname, '..', '..', '..', 'scripts', 'schema.sql'),
process.resourcesPath ? path.join(process.resourcesPath, 'scripts', 'schema.sql') : null, process.resourcesPath ? path.join(process.resourcesPath, 'scripts', 'schema.sql') : null,
].filter(Boolean); ].filter((c): c is string => Boolean(c));
const found = candidates.find(p => fs.existsSync(p)); const found = candidates.find(p => fs.existsSync(p));
if (!found) throw new Error('Obelisk schema.sql not found'); if (!found) throw new Error('Obelisk schema.sql not found');
return found; return found;
@@ -41,7 +51,7 @@ function installSchema(db, schemaPath = resolveSchemaPath()) {
migrateDb(db); migrateDb(db);
} }
function openIndexDb({ dbPath = DEFAULT_DB_PATH, schemaPath = resolveSchemaPath(), DatabaseImpl = Database } = {}) { function openIndexDb({ dbPath = DEFAULT_DB_PATH, schemaPath = resolveSchemaPath(), DatabaseImpl = Database }: { dbPath?: string; schemaPath?: string; DatabaseImpl?: new (dbPath: string) => any } = {}) {
fs.mkdirSync(path.dirname(dbPath), { recursive: true }); fs.mkdirSync(path.dirname(dbPath), { recursive: true });
const db = new DatabaseImpl(dbPath); const db = new DatabaseImpl(dbPath);
db.pragma('journal_mode = WAL'); db.pragma('journal_mode = WAL');
@@ -104,7 +114,7 @@ function copyMemoriesFromDb(db, sourceDbPath) {
} }
} }
function discoverJsonlFiles({ projectsDir = DEFAULT_PROJECTS_DIR, changedPaths = undefined } = {}) { function discoverJsonlFiles({ projectsDir = DEFAULT_PROJECTS_DIR, changedPaths = undefined }: { projectsDir?: string; changedPaths?: string[] } = {}) {
if (Array.isArray(changedPaths) && changedPaths.length) { if (Array.isArray(changedPaths) && changedPaths.length) {
const changedFiles = discoverJsonlFilesForChanges({ projectsDir, changedPaths }); const changedFiles = discoverJsonlFilesForChanges({ projectsDir, changedPaths });
if (changedFiles.length) return changedFiles; if (changedFiles.length) return changedFiles;
@@ -168,8 +178,8 @@ function dedupeFileInfos(files) {
return [...byPath.values()]; return [...byPath.values()];
} }
function discoverJsonlFilesForChanges({ projectsDir = DEFAULT_PROJECTS_DIR, changedPaths = [] } = {}) { function discoverJsonlFilesForChanges({ projectsDir = DEFAULT_PROJECTS_DIR, changedPaths = [] }: { projectsDir?: string; changedPaths?: string[] } = {}) {
const files = []; const files: FileInfo[] = [];
for (const changedPath of changedPaths) { for (const changedPath of changedPaths) {
const info = jsonlFileInfoFromPath(projectsDir, changedPath); const info = jsonlFileInfoFromPath(projectsDir, changedPath);
if (info) files.push(info); if (info) files.push(info);
@@ -178,7 +188,7 @@ function discoverJsonlFilesForChanges({ projectsDir = DEFAULT_PROJECTS_DIR, chan
} }
function discoverJsonlFilesFull({ projectsDir = DEFAULT_PROJECTS_DIR } = {}) { function discoverJsonlFilesFull({ projectsDir = DEFAULT_PROJECTS_DIR } = {}) {
const files = []; const files: FileInfo[] = [];
if (!fs.existsSync(projectsDir)) return files; if (!fs.existsSync(projectsDir)) return files;
let projects; let projects;
try { projects = fs.readdirSync(projectsDir); } catch { return files; } try { projects = fs.readdirSync(projectsDir); } catch { return files; }
@@ -219,7 +229,7 @@ function discoverJsonlFilesFull({ projectsDir = DEFAULT_PROJECTS_DIR } = {}) {
return files; return files;
} }
function discoverCodexJsonlFiles({ codexDir = DEFAULT_CODEX_DIR, changedPaths = undefined } = {}) { function discoverCodexJsonlFiles({ codexDir = DEFAULT_CODEX_DIR, changedPaths = undefined }: { codexDir?: string; changedPaths?: string[] } = {}) {
if (Array.isArray(changedPaths) && changedPaths.length) { if (Array.isArray(changedPaths) && changedPaths.length) {
const changedFiles = discoverCodexJsonlFilesForChanges({ codexDir, changedPaths }); const changedFiles = discoverCodexJsonlFilesForChanges({ codexDir, changedPaths });
if (changedFiles.length) return changedFiles; if (changedFiles.length) return changedFiles;
@@ -244,8 +254,8 @@ function isPathInside(rootDir, candidate) {
return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel); return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
} }
function discoverCodexJsonlFilesForChanges({ codexDir = DEFAULT_CODEX_DIR, changedPaths = [] } = {}) { function discoverCodexJsonlFilesForChanges({ codexDir = DEFAULT_CODEX_DIR, changedPaths = [] }: { codexDir?: string; changedPaths?: string[] } = {}) {
const files = []; const files: FileInfo[] = [];
const sessionsDir = codexSessionsDir(codexDir); const sessionsDir = codexSessionsDir(codexDir);
for (const changedPath of changedPaths) { for (const changedPath of changedPaths) {
const rootRelativePath = normalizeChangedPathForRoot(codexDir, changedPath); const rootRelativePath = normalizeChangedPathForRoot(codexDir, changedPath);
@@ -255,7 +265,7 @@ function discoverCodexJsonlFilesForChanges({ codexDir = DEFAULT_CODEX_DIR, chang
} }
const sessionRelativePath = normalizeChangedPathForRoot(sessionsDir, changedPath); const sessionRelativePath = normalizeChangedPathForRoot(sessionsDir, changedPath);
const fp = isPathInside(sessionsDir, rootRelativePath) ? rootRelativePath : sessionRelativePath; const fp = isPathInside(sessionsDir, rootRelativePath) ? rootRelativePath : sessionRelativePath;
if (!fp.endsWith('.jsonl') || !isPathInside(sessionsDir, fp)) continue; if (!fp || !fp.endsWith(".jsonl") || !isPathInside(sessionsDir, fp)) continue;
if (!fs.existsSync(fp)) continue; if (!fs.existsSync(fp)) continue;
files.push({ path: fp, source: 'codex' }); files.push({ path: fp, source: 'codex' });
} }
@@ -264,11 +274,11 @@ function discoverCodexJsonlFilesForChanges({ codexDir = DEFAULT_CODEX_DIR, chang
function discoverCodexJsonlFilesFull({ codexDir = DEFAULT_CODEX_DIR } = {}) { function discoverCodexJsonlFilesFull({ codexDir = DEFAULT_CODEX_DIR } = {}) {
const root = codexSessionsDir(codexDir); const root = codexSessionsDir(codexDir);
const files = []; const files: FileInfo[] = [];
if (!fs.existsSync(root)) return files; if (!fs.existsSync(root)) return files;
const stack = [root]; const stack = [root];
while (stack.length) { while (stack.length) {
const current = stack.pop(); const current = stack.pop()!;
let entries; let entries;
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; } try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
for (const entry of entries) { for (const entry of entries) {
@@ -308,7 +318,7 @@ function indexClaudeFile(db, file) {
} }
function codexSessionMeta(filePath) { function codexSessionMeta(filePath) {
let meta = null; let meta: any = null;
readLines(filePath, (line) => { readLines(filePath, (line) => {
let obj; let obj;
try { obj = JSON.parse(line); } catch { return; } try { obj = JSON.parse(line); } catch { return; }
@@ -328,7 +338,7 @@ function indexCodexFile(db, file) {
if (!needed) { if (!needed) {
if (guardian) { if (guardian) {
persist(db, { key: file.path, sessionId: '' }, (function* () { persist(db, { key: file.path, sessionId: '' }, (function* () {
yield { kind: 'delete-session', sessionId: codexDbId(guardian.threadRawId) }; yield { kind: "delete-session", sessionId: codexDbId(guardian.threadRawId) as string };
return null; return null;
})()); })());
} }
@@ -352,7 +362,7 @@ function indexCodexSessionIndex(db, { codexDir = DEFAULT_CODEX_DIR } = {}) {
db.prepare('UPDATE sessions SET title=COALESCE(title, ?), ended_at=COALESCE(ended_at, ?) WHERE id=? AND source=?') db.prepare('UPDATE sessions SET title=COALESCE(title, ?), ended_at=COALESCE(ended_at, ?) WHERE id=? AND source=?')
.run(item.thread_name, item.updated_at || null, codexDbId(item.id), 'codex'); .run(item.thread_name, item.updated_at || null, codexDbId(item.id), 'codex');
} catch (error) { } catch (error) {
console.warn(`Warning: malformed Codex session index line: ${error.message}`); console.warn(`Warning: malformed Codex session index line: ${(error as Error).message}`);
} }
}); });
} }
@@ -387,7 +397,7 @@ function indexSubagentMeta(db, fi) {
db.prepare('INSERT OR REPLACE INTO subagents VALUES(?,?,?,?,?,?,?)').run(fi.agentId, fi.sessionId, meta.toolUseId||null, meta.agentType||null, meta.description||null, dur, tok?.t||0); db.prepare('INSERT OR REPLACE INTO subagents VALUES(?,?,?,?,?,?,?)').run(fi.agentId, fi.sessionId, meta.toolUseId||null, meta.agentType||null, meta.description||null, dur, tok?.t||0);
} }
} catch (error) { } catch (error) {
console.warn(`Warning: failed to read subagent meta ${mp}: ${error.message}`); console.warn(`Warning: failed to read subagent meta ${mp}: ${(error as Error).message}`);
} }
} }
@@ -423,7 +433,7 @@ function indexWorkflows(db, { projectsDir = DEFAULT_PROJECTS_DIR } = {}) {
item.durationMs||null, item.tokens||null, item.toolCalls||null, 'agent-' + item.agentId); item.durationMs||null, item.tokens||null, item.toolCalls||null, 'agent-' + item.agentId);
} }
} catch (error) { } catch (error) {
console.warn(`Warning: failed to index workflow ${f}: ${error.message}`); console.warn(`Warning: failed to index workflow ${f}: ${(error as Error).message}`);
} }
} }
} }
@@ -437,7 +447,7 @@ function indexHistory(db, { historyPath = DEFAULT_HISTORY_PATH } = {}) {
const o = JSON.parse(line); const o = JSON.parse(line);
if (o.sessionId && o.title) db.prepare('UPDATE sessions SET title=? WHERE id=? AND title IS NULL').run(o.title, o.sessionId); if (o.sessionId && o.title) db.prepare('UPDATE sessions SET title=? WHERE id=? AND title IS NULL').run(o.title, o.sessionId);
} catch (error) { } catch (error) {
console.warn(`Warning: malformed history line: ${error.message}`); console.warn(`Warning: malformed history line: ${(error as Error).message}`);
} }
}); });
} }
@@ -488,6 +498,26 @@ function writeHeartbeat({ dbPath = DEFAULT_DB_PATH, DatabaseImpl = Database } =
} }
} }
interface BuildIndexOptions {
claudeDir?: string;
codexDir?: string;
projectsDir?: string;
historyPath?: string;
dbPath?: string;
schemaPath?: string;
DatabaseImpl?: new (dbPath: string) => any;
force?: boolean;
changedPaths?: string[];
preserveDbPath?: string | null;
}
interface BuildIndexResult {
files: number;
latestSourceMtime: number;
affectedSessionIds: string[];
ftsRebuilt: boolean;
}
function buildIndex({ function buildIndex({
claudeDir = DEFAULT_CLAUDE_DIR, claudeDir = DEFAULT_CLAUDE_DIR,
codexDir = path.join(path.dirname(claudeDir), '.codex'), codexDir = path.join(path.dirname(claudeDir), '.codex'),
@@ -499,7 +529,7 @@ function buildIndex({
force = false, force = false,
changedPaths = undefined, changedPaths = undefined,
preserveDbPath = null, preserveDbPath = null,
} = {}) { }: BuildIndexOptions = {}): BuildIndexResult {
const db = openIndexDb({ dbPath, schemaPath, DatabaseImpl }); const db = openIndexDb({ dbPath, schemaPath, DatabaseImpl });
let messageFtsTriggersDropped = false; let messageFtsTriggersDropped = false;
if (preserveDbPath && path.resolve(preserveDbPath) !== path.resolve(dbPath)) { if (preserveDbPath && path.resolve(preserveDbPath) !== path.resolve(dbPath)) {
@@ -531,7 +561,7 @@ function buildIndex({
db.prepare("DELETE FROM workflows").run(); db.prepare("DELETE FROM workflows").run();
db.prepare("DELETE FROM workflow_agents").run(); db.prepare("DELETE FROM workflow_agents").run();
} }
const affectedSessionIds = new Set(); const affectedSessionIds = new Set<string>();
if (Array.isArray(changedPaths)) { if (Array.isArray(changedPaths)) {
for (const changedPath of changedPaths) { for (const changedPath of changedPaths) {
const sessionId = sessionIdFromChangedPath(projectsDir, changedPath); const sessionId = sessionIdFromChangedPath(projectsDir, changedPath);
@@ -547,7 +577,7 @@ function buildIndex({
db.exec('COMMIT'); db.exec('COMMIT');
} catch (error) { } catch (error) {
db.exec('ROLLBACK'); db.exec('ROLLBACK');
console.warn(`Warning: failed to index ${file.path}: ${error.message}`); console.warn(`Warning: failed to index ${file.path}: ${(error as Error).message}`);
} }
} }
db.exec('BEGIN'); db.exec('BEGIN');
@@ -575,7 +605,7 @@ function buildIndex({
try { try {
installSchema(db, schemaPath); installSchema(db, schemaPath);
} catch (error) { } catch (error) {
console.warn(`Warning: failed to restore message FTS triggers: ${error.message}`); console.warn(`Warning: failed to restore message FTS triggers: ${(error as Error).message}`);
} }
} }
checkpointDb(db); checkpointDb(db);
@@ -1,11 +1,17 @@
import path from 'node:path'; import path from 'node:path';
function cleanRecapFilename(filename) { function cleanRecapFilename(filename?: string | null): string {
if (!filename) return ''; if (!filename) return '';
return path.basename(String(filename)); return path.basename(String(filename));
} }
function buildRecapExportQuery({ cardIdx = 0, archetype = '', filename = '' } = {}) { interface RecapExportQueryOptions {
cardIdx?: number | string;
archetype?: string;
filename?: string;
}
function buildRecapExportQuery({ cardIdx = 0, archetype = '', filename = '' }: RecapExportQueryOptions = {}): string {
const params = new URLSearchParams(); const params = new URLSearchParams();
const cardNumber = Number(cardIdx); const cardNumber = Number(cardIdx);
params.set('card', Number.isFinite(cardNumber) ? String(cardNumber) : '0'); params.set('card', Number.isFinite(cardNumber) ? String(cardNumber) : '0');
-46
View File
@@ -1,46 +0,0 @@
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('obelisk', {
getSessions: (opts) => ipcRenderer.invoke('db:getSessions', opts),
getSessionMessages: (id) => ipcRenderer.invoke('db:getSessionMessages', id),
getSessionToolCalls: (id) => ipcRenderer.invoke('db:getSessionToolCalls', id),
getSessionToolResults: (id) => ipcRenderer.invoke('db:getSessionToolResults', id),
getSessionSubagents: (id) => ipcRenderer.invoke('db:getSessionSubagents', id),
getSessionWorkflows: (id) => ipcRenderer.invoke('db:getSessionWorkflows', id),
getSubagentMessages: (agentId) => ipcRenderer.invoke('db:getSubagentMessages', agentId),
getSubagentToolCalls: (agentId) => ipcRenderer.invoke('db:getSubagentToolCalls', agentId),
getSubagentToolResults: (agentId) => ipcRenderer.invoke('db:getSubagentToolResults', agentId),
getSessionSummaries: (id) => ipcRenderer.invoke('db:getSessionSummaries', id),
getMessageFullText: (uuid) => ipcRenderer.invoke('db:getMessageFullText', uuid),
getMemories: () => ipcRenderer.invoke('db:getMemories'),
readMemoryFile: (path) => ipcRenderer.invoke('db:readMemoryFile', path),
archiveMemory: (id, reason) => ipcRenderer.invoke('db:archiveMemory', id, reason),
restoreMemory: (id) => ipcRenderer.invoke('db:restoreMemory', id),
getProjects: () => ipcRenderer.invoke('db:getProjects'),
getStats: () => ipcRenderer.invoke('db:getStats'),
getUsageStats: () => ipcRenderer.invoke('db:getUsageStats'),
onIndexUpdated: (callback) => {
const listener = (_, payload) => callback(payload);
ipcRenderer.on('obelisk:index-updated', listener);
return () => ipcRenderer.removeListener('obelisk:index-updated', listener);
},
onSessionUpdated: (callback) => {
const listener = (_, payload) => callback(payload);
ipcRenderer.on('obelisk:session-updated', listener);
return () => ipcRenderer.removeListener('obelisk:session-updated', listener);
},
captureExport: (opts) => ipcRenderer.invoke('capture:export', opts),
copyImage: (opts) => ipcRenderer.invoke('capture:copy', opts),
recapList: () => ipcRenderer.invoke('recap:list'),
recapRead: (filename) => ipcRenderer.invoke('recap:read', filename),
onRecapUpdated: (callback) => {
const listener = (_, filePath) => callback(filePath);
ipcRenderer.on('obelisk:recap-updated', listener);
return () => ipcRenderer.removeListener('obelisk:recap-updated', listener);
},
getSettings: () => ipcRenderer.invoke('settings:get'),
browseFolder: () => ipcRenderer.invoke('settings:browseFolder'),
setSetting: (key, value) => ipcRenderer.invoke('settings:set', key, value),
revealPath: (p) => ipcRenderer.invoke('settings:revealPath', p),
rebuildIndex: () => ipcRenderer.invoke('settings:rebuildIndex'),
});
+46
View File
@@ -0,0 +1,46 @@
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron';
contextBridge.exposeInMainWorld('obelisk', {
getSessions: (opts?: unknown) => ipcRenderer.invoke('db:getSessions', opts),
getSessionMessages: (id: string) => ipcRenderer.invoke('db:getSessionMessages', id),
getSessionToolCalls: (id: string) => ipcRenderer.invoke('db:getSessionToolCalls', id),
getSessionToolResults: (id: string) => ipcRenderer.invoke('db:getSessionToolResults', id),
getSessionSubagents: (id: string) => ipcRenderer.invoke('db:getSessionSubagents', id),
getSessionWorkflows: (id: string) => ipcRenderer.invoke('db:getSessionWorkflows', id),
getSubagentMessages: (agentId: string) => ipcRenderer.invoke('db:getSubagentMessages', agentId),
getSubagentToolCalls: (agentId: string) => ipcRenderer.invoke('db:getSubagentToolCalls', agentId),
getSubagentToolResults: (agentId: string) => ipcRenderer.invoke('db:getSubagentToolResults', agentId),
getSessionSummaries: (id: string) => ipcRenderer.invoke('db:getSessionSummaries', id),
getMessageFullText: (uuid: string) => ipcRenderer.invoke('db:getMessageFullText', uuid),
getMemories: () => ipcRenderer.invoke('db:getMemories'),
readMemoryFile: (path: string) => ipcRenderer.invoke('db:readMemoryFile', path),
archiveMemory: (id: string, reason?: string) => ipcRenderer.invoke('db:archiveMemory', id, reason),
restoreMemory: (id: string) => ipcRenderer.invoke('db:restoreMemory', id),
getProjects: () => ipcRenderer.invoke('db:getProjects'),
getStats: () => ipcRenderer.invoke('db:getStats'),
getUsageStats: () => ipcRenderer.invoke('db:getUsageStats'),
onIndexUpdated: (callback: (payload: unknown) => void) => {
const listener = (_: IpcRendererEvent, payload: unknown) => callback(payload);
ipcRenderer.on('obelisk:index-updated', listener);
return () => ipcRenderer.removeListener('obelisk:index-updated', listener);
},
onSessionUpdated: (callback: (payload: unknown) => void) => {
const listener = (_: IpcRendererEvent, payload: unknown) => callback(payload);
ipcRenderer.on('obelisk:session-updated', listener);
return () => ipcRenderer.removeListener('obelisk:session-updated', listener);
},
captureExport: (opts?: unknown) => ipcRenderer.invoke('capture:export', opts),
copyImage: (opts?: unknown) => ipcRenderer.invoke('capture:copy', opts),
recapList: () => ipcRenderer.invoke('recap:list'),
recapRead: (filename: string) => ipcRenderer.invoke('recap:read', filename),
onRecapUpdated: (callback: (filePath: unknown) => void) => {
const listener = (_: IpcRendererEvent, filePath: unknown) => callback(filePath);
ipcRenderer.on('obelisk:recap-updated', listener);
return () => ipcRenderer.removeListener('obelisk:recap-updated', listener);
},
getSettings: () => ipcRenderer.invoke('settings:get'),
browseFolder: () => ipcRenderer.invoke('settings:browseFolder'),
setSetting: (key: string, value: unknown) => ipcRenderer.invoke('settings:set', key, value),
revealPath: (p: string) => ipcRenderer.invoke('settings:revealPath', p),
rebuildIndex: () => ipcRenderer.invoke('settings:rebuildIndex'),
});
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2023"],
"types": ["node"],
"strict": true,
"noImplicitAny": false,
"noEmit": true,
"allowImportingTsExtensions": true,
"allowJs": true,
"checkJs": false,
"erasableSyntaxOnly": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/main/**/*", "src/preload/**/*"],
"exclude": ["node_modules", "out", "dist", "release", "src/renderer"]
}
+24 -3
View File
@@ -27,11 +27,32 @@ decisions within this:
(ADR-0003) remains for the skill artifact; the app does not need it. (ADR-0003) remains for the skill artifact; the app does not need it.
- **better-sqlite3 stays the app's binding**, externalized (not bundled) and - **better-sqlite3 stays the app's binding**, externalized (not bundled) and
unpacked from the asar. unpacked from the asar.
- **The app main + preload source is TypeScript with types at its seams**, but
under a *deliberately more lenient* project than the runtime core. `app/tsconfig.json`
keeps `strict` on yet sets `noImplicitAny: false`, because the app mostly
orchestrates the already-strictly-typed core (`scripts/`), and annotating every
internal SQLite-handle helper would be high-cost, low-value churn. Types are
added where they matter: the core-consumption seam (`BuildIndexOptions`/
`BuildIndexResult`, `FileInfo`), the service/worker factories, and the IPC
bridge. Module-to-module specifiers use the real `.ts` extension (mirroring
`scripts/`, since Node's type-stripping does not rewrite `.js``.ts`), which
needs `allowImportingTsExtensions` (safe under the project's `noEmit`); the
worker's *runtime* path stays `indexer-worker.js` because that is the built
output. `@types/better-sqlite3` is a devDependency for the injected binding.
**Two-tier typechecking.** `npm run typecheck` runs the root project (`scripts/` +
`tests/`, fully strict including `noImplicitAny`) and then the app project. The
root project **excludes the app-importing tests** (`tests/app-*.test.mjs`,
`tests/recap-capture-query.test.mjs`): those tests import app source, which would
otherwise drag the lenient app files into the strict root program and fail on
implicit `any`. The app source is instead covered by `app/tsconfig.json`, so
nothing loses type coverage — the strict core and the lenient app are checked by
the project that owns each, and never mixed.
**Consequences.** The app is restructured into `src/{main,preload,renderer}` with **Consequences.** The app is restructured into `src/{main,preload,renderer}` with
`electron.vite.config.ts`; each main module is a build input so CommonJS-style `electron.vite.config.ts`; each main module is a build input so relative imports
require resolution and the indexer worker (`{ type: 'module' }`) resolve at between them and the indexer worker (`{ type: 'module' }`) resolve at runtime.
runtime. `npm run dev` is `electron-vite dev`. Tests that loaded app modules moved `npm run dev` is `electron-vite dev`. Tests that loaded app modules moved
to ESM imports, and `app-main-settings` was rewritten from CJS `Module._load` to ESM imports, and `app-main-settings` was rewritten from CJS `Module._load`
mocking to `node:test` `mock.module` (needs `--experimental-test-module-mocks`). mocking to `node:test` `mock.module` (needs `--experimental-test-module-mocks`).
A future contributor may be tempted to make the preload ESM or disable the A future contributor may be tempted to make the preload ESM or disable the
+1 -1
View File
@@ -7,7 +7,7 @@
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"test": "node --experimental-test-module-mocks --test tests/*.test.mjs", "test": "node --experimental-test-module-mocks --test tests/*.test.mjs",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit && tsc --noEmit -p app/tsconfig.json",
"lint": "eslint .", "lint": "eslint .",
"build:core": "rm -rf dist && tsc -p tsconfig.build.json" "build:core": "rm -rf dist && tsc -p tsconfig.build.json"
}, },
+1 -1
View File
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
import { createIndexerService } from '../app/src/main/indexer-service.js'; import { createIndexerService } from '../app/src/main/indexer-service.ts';
function manualTimers() { function manualTimers() {
const timers = new Set(); const timers = new Set();
+1 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { createRequire } from 'node:module'; import { createRequire } from 'node:module';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
import { createWorkerBuildIndex } from '../app/src/main/indexer-worker-client.js'; import { createWorkerBuildIndex } from '../app/src/main/indexer-worker-client.ts';
test('worker build client resolves build results from a worker thread', async () => { test('worker build client resolves build results from a worker thread', async () => {
const instances = []; const instances = [];
+1 -1
View File
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
import { buildIndex } from '../app/src/main/indexer.js'; import { buildIndex } from '../app/src/main/indexer.ts';
const { DatabaseSync } = require('node:sqlite'); const { DatabaseSync } = require('node:sqlite');
class TestDatabase { class TestDatabase {
+4 -4
View File
@@ -22,7 +22,7 @@ const require = createRequire(import.meta.url);
// resolve each bare specifier exactly as the main module sees it (ESM resolution // resolve each bare specifier exactly as the main module sees it (ESM resolution
// relative to the main module's directory) and mock that URL. Relative deps are // relative to the main module's directory) and mock that URL. Relative deps are
// resolved against the main module URL directly. // resolved against the main module URL directly.
const mainUrl = new URL('../app/src/main/index.js', import.meta.url); const mainUrl = new URL('../app/src/main/index.ts', import.meta.url);
const mainPath = fileURLToPath(mainUrl); const mainPath = fileURLToPath(mainUrl);
const mainDir = fileURLToPath(new URL('.', mainUrl)); const mainDir = fileURLToPath(new URL('.', mainUrl));
@@ -37,9 +37,9 @@ function esmResolve(specifier) {
const ELECTRON_URL = esmResolve('electron'); const ELECTRON_URL = esmResolve('electron');
const DATABASE_URL = esmResolve('better-sqlite3'); const DATABASE_URL = esmResolve('better-sqlite3');
const CHOKIDAR_URL = esmResolve('chokidar'); const CHOKIDAR_URL = esmResolve('chokidar');
const INDEXER_URL = new URL('./indexer.js', mainUrl).href; const INDEXER_URL = new URL('./indexer.ts', mainUrl).href;
const INDEXER_SERVICE_URL = new URL('./indexer-service.js', mainUrl).href; const INDEXER_SERVICE_URL = new URL('./indexer-service.ts', mainUrl).href;
const INDEXER_WORKER_URL = new URL('./indexer-worker-client.js', mainUrl).href; const INDEXER_WORKER_URL = new URL('./indexer-worker-client.ts', mainUrl).href;
let importCounter = 0; let importCounter = 0;
+1 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { createRequire } from 'node:module'; import { createRequire } from 'node:module';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
import { buildRecapExportQuery, cleanRecapFilename } from '../app/src/main/recap-capture-query.js'; import { buildRecapExportQuery, cleanRecapFilename } from '../app/src/main/recap-capture-query.ts';
test('recap export query includes the selected recap filename', () => { test('recap export query includes the selected recap filename', () => {
const query = buildRecapExportQuery({ const query = buildRecapExportQuery({
+8 -1
View File
@@ -16,5 +16,12 @@
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true
}, },
"include": ["scripts/**/*", "tests/**/*"], "include": ["scripts/**/*", "tests/**/*"],
"exclude": ["node_modules", "app", "dist", "release"] "exclude": [
"node_modules",
"app",
"dist",
"release",
"tests/app-*.test.mjs",
"tests/recap-capture-query.test.mjs"
]
} }