codex-cleaner 0.0.1 → 0.0.2

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/dist/cleaner.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import Database from "better-sqlite3";
2
+ import { DatabaseSync } from "node:sqlite";
3
3
  import type { BlockingProcess, CleanerOptions, CompactWhere } from "./types.js";
4
4
  type CodexSpawnCommand = {
5
5
  args: string[];
@@ -30,21 +30,21 @@ export declare function compactWhere(args: {
30
30
  protectRecent: boolean;
31
31
  protectedIds: Set<string>;
32
32
  }): CompactWhere;
33
- export declare function collectCompactCandidateStats(db: Database.Database, args: {
33
+ export declare function collectCompactCandidateStats(db: DatabaseSync, args: {
34
34
  archivedOnly: boolean;
35
35
  cutoffMs: number;
36
36
  maxChars: number;
37
37
  protectRecent: boolean;
38
38
  protectedIds: Set<string>;
39
39
  }): Record<string, unknown>;
40
- export declare function collectStaleArchiveCandidateStats(db: Database.Database, codexHome: string, args: {
40
+ export declare function collectStaleArchiveCandidateStats(db: DatabaseSync, codexHome: string, args: {
41
41
  cutoffMs: number;
42
42
  protectedIds: Set<string>;
43
43
  statRollouts?: boolean;
44
44
  }): Record<string, unknown>;
45
- export declare function collectLogCleanupStats(db: Database.Database, options: CleanerOptions): Record<string, unknown>;
45
+ export declare function collectLogCleanupStats(db: DatabaseSync, options: CleanerOptions): Record<string, unknown>;
46
46
  export declare function collectTuiLogCleanupStats(logPath: string, keepMib: number): Record<string, unknown>;
47
- export declare function collectOrphanRolloutArchiveStats(db: Database.Database, codexHome: string, args: {
47
+ export declare function collectOrphanRolloutArchiveStats(db: DatabaseSync, codexHome: string, args: {
48
48
  cutoffMs: number;
49
49
  }): Record<string, unknown>;
50
50
  export declare function nextBackupPath(dbPath: string, backupDir: string, now?: Date): string;
package/dist/cleaner.js CHANGED
@@ -2,8 +2,8 @@ import { execFile, spawn, spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { backup as sqliteBackup, DatabaseSync } from "node:sqlite";
5
6
  import { promisify } from "node:util";
6
- import Database from "better-sqlite3";
7
7
  const execFileAsync = promisify(execFile);
8
8
  const THREAD_COLUMNS_TO_CAP = ["title", "preview", "first_user_message"];
9
9
  const APP_SERVER_REQUEST_TIMEOUT_MS = 60_000;
@@ -438,7 +438,7 @@ export async function compactMetadata(options) {
438
438
  throw new Error("--apply requires --confirm-lossy-metadata");
439
439
  }
440
440
  if (options.apply && Number(before.rows) > 0) {
441
- backupPath = await backupSqliteDatabase(stateDb, resolveBackupDir(options, codexHome));
441
+ backupPath = await backupOpenSqliteDatabase(db, stateDb, resolveBackupDir(options, codexHome));
442
442
  const where = compactWhere({
443
443
  archivedOnly: options.archivedOnly,
444
444
  cutoffMs,
@@ -447,9 +447,8 @@ export async function compactMetadata(options) {
447
447
  protectedIds,
448
448
  });
449
449
  const assignments = THREAD_COLUMNS_TO_CAP.map((column) => `${column} = CASE WHEN length(${column}) > @maxChars THEN substr(${column}, 1, @maxChars) ELSE ${column} END`).join(", ");
450
- const update = db.prepare(`UPDATE threads SET ${assignments} WHERE ${where.sql}`);
451
- const tx = db.transaction(() => update.run(where.params));
452
- changedRows = Number(tx().changes);
450
+ const update = prepareStatement(db, `UPDATE threads SET ${assignments} WHERE ${where.sql}`);
451
+ changedRows = Number(runTransaction(db, () => update.run(where.params).changes));
453
452
  }
454
453
  const after = collectCompactCandidateStats(db, {
455
454
  archivedOnly: options.archivedOnly,
@@ -749,23 +748,20 @@ export async function cleanLogs(options) {
749
748
  const hasRowChanges = Number(before.cap_rows) > 0 || Number(before.delete_rows) > 0;
750
749
  const shouldVacuum = Number(beforeSpace.freelist_count) > 0 || hasRowChanges;
751
750
  if (options.apply && shouldVacuum) {
752
- backupPath = await backupSqliteDatabase(logsDb, resolveBackupDir(options, codexHome));
751
+ backupPath = await backupOpenSqliteDatabase(db, logsDb, resolveBackupDir(options, codexHome));
753
752
  const cutoffSeconds = logCutoffSeconds(options.keepLogDays);
754
753
  if (hasRowChanges) {
755
- const tx = db.transaction(() => {
756
- deletedRows = Number(db.prepare("DELETE FROM logs WHERE ts < @cutoffSeconds").run({ cutoffSeconds }).changes);
757
- cappedRows = Number(db
758
- .prepare(`
754
+ runTransaction(db, () => {
755
+ deletedRows = Number(prepareStatement(db, "DELETE FROM logs WHERE ts < @cutoffSeconds").run({ cutoffSeconds }).changes);
756
+ cappedRows = Number(prepareStatement(db, `
759
757
  UPDATE logs
760
758
  SET estimated_bytes = max(0, estimated_bytes - (length(feedback_log_body) - @maxLogBodyChars)),
761
759
  feedback_log_body = substr(feedback_log_body, 1, @maxLogBodyChars)
762
760
  WHERE ts >= @cutoffSeconds
763
761
  AND feedback_log_body IS NOT NULL
764
762
  AND length(feedback_log_body) > @maxLogBodyChars
765
- `)
766
- .run({ cutoffSeconds, maxLogBodyChars: options.maxLogBodyChars }).changes);
763
+ `).run({ cutoffSeconds, maxLogBodyChars: options.maxLogBodyChars }).changes);
767
764
  });
768
- tx();
769
765
  }
770
766
  db.exec("VACUUM");
771
767
  queryAll(db, "PRAGMA wal_checkpoint(TRUNCATE)");
@@ -912,9 +908,7 @@ export async function scheduleBackupPrune(options) {
912
908
  };
913
909
  }
914
910
  export function compactWhere(args) {
915
- const clauses = [
916
- `(${THREAD_COLUMNS_TO_CAP.map((column) => `length(${column}) > @maxChars`).join(" OR ")})`,
917
- ];
911
+ const clauses = [`(${THREAD_COLUMNS_TO_CAP.map((column) => `length(${column}) > @maxChars`).join(" OR ")})`];
918
912
  const params = {
919
913
  cutoffMs: args.cutoffMs,
920
914
  maxChars: args.maxChars,
@@ -1368,15 +1362,18 @@ function backupFileStats(files) {
1368
1362
  };
1369
1363
  }
1370
1364
  async function backupSqliteDatabase(dbPath, backupDir) {
1371
- fs.mkdirSync(backupDir, { recursive: true });
1372
- const backupPath = nextBackupPath(dbPath, backupDir);
1373
1365
  const db = openWritableDb(dbPath);
1374
1366
  try {
1375
- await db.backup(backupPath);
1367
+ return await backupOpenSqliteDatabase(db, dbPath, backupDir);
1376
1368
  }
1377
1369
  finally {
1378
1370
  db.close();
1379
1371
  }
1372
+ }
1373
+ async function backupOpenSqliteDatabase(db, dbPath, backupDir) {
1374
+ fs.mkdirSync(backupDir, { recursive: true });
1375
+ const backupPath = nextBackupPath(dbPath, backupDir);
1376
+ await sqliteBackup(db, backupPath);
1380
1377
  return backupPath;
1381
1378
  }
1382
1379
  function backupRegularFile(filePath, backupDir) {
@@ -1393,7 +1390,7 @@ async function vacuumSqliteDatabase(args) {
1393
1390
  const beforeSpace = collectSqliteSpaceStats(db);
1394
1391
  if (args.apply && Number(beforeSpace.freelist_count) > 0) {
1395
1392
  if (args.backupBeforeVacuum) {
1396
- backupPath = await backupSqliteDatabase(args.dbPath, args.backupDir);
1393
+ backupPath = await backupOpenSqliteDatabase(db, args.dbPath, args.backupDir);
1397
1394
  }
1398
1395
  db.exec("VACUUM");
1399
1396
  queryAll(db, "PRAGMA wal_checkpoint(TRUNCATE)");
@@ -1424,10 +1421,7 @@ function nextOrphanRolloutManifestPath(backupDir, now = new Date()) {
1424
1421
  return nextTimestampedPath(backupDir, "orphan-rollouts", ".manifest.bak", now);
1425
1422
  }
1426
1423
  function nextTimestampedPath(backupDir, name, suffix, now) {
1427
- const stamp = now
1428
- .toISOString()
1429
- .replace(/[-:]/g, "")
1430
- .replace(".", "_");
1424
+ const stamp = now.toISOString().replace(/[-:]/g, "").replace(".", "_");
1431
1425
  const base = `${name}.${stamp}`;
1432
1426
  let candidate = path.join(backupDir, `${base}${suffix}`);
1433
1427
  let collision = 2;
@@ -1604,21 +1598,38 @@ function timestampForName(date) {
1604
1598
  function openReadonlyDb(file) {
1605
1599
  if (!fs.existsSync(file))
1606
1600
  throw new Error(`SQLite database not found: ${file}`);
1607
- return new Database(file, { fileMustExist: true, readonly: true });
1601
+ return new DatabaseSync(file, { readOnly: true });
1608
1602
  }
1609
1603
  function openWritableDb(file) {
1610
1604
  if (!fs.existsSync(file))
1611
1605
  throw new Error(`SQLite database not found: ${file}`);
1612
- const db = new Database(file, { fileMustExist: true });
1613
- db.pragma("busy_timeout = 5000");
1614
- db.pragma("foreign_keys = ON");
1606
+ const db = new DatabaseSync(file);
1607
+ db.exec("PRAGMA busy_timeout = 5000");
1608
+ db.exec("PRAGMA foreign_keys = ON");
1615
1609
  return db;
1616
1610
  }
1617
1611
  function queryAll(db, sql, params) {
1618
- return db.prepare(sql).all(params ?? {});
1612
+ return prepareStatement(db, sql).all(params ?? {});
1619
1613
  }
1620
1614
  function queryOne(db, sql, params) {
1621
- return db.prepare(sql).get(params ?? {}) ?? {};
1615
+ return prepareStatement(db, sql).get(params ?? {}) ?? {};
1616
+ }
1617
+ function prepareStatement(db, sql) {
1618
+ const statement = db.prepare(sql);
1619
+ statement.setAllowUnknownNamedParameters(true);
1620
+ return statement;
1621
+ }
1622
+ function runTransaction(db, fn) {
1623
+ db.exec("BEGIN IMMEDIATE");
1624
+ try {
1625
+ const result = fn();
1626
+ db.exec("COMMIT");
1627
+ return result;
1628
+ }
1629
+ catch (error) {
1630
+ db.exec("ROLLBACK");
1631
+ throw error;
1632
+ }
1622
1633
  }
1623
1634
  function tryQueryAll(db, sql) {
1624
1635
  try {
package/dist/cli.js CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
2
3
  import { parseArgs } from "node:util";
3
- import { buildScanReport, archiveOrphanRollouts, checkpointWal, cleanCodex, compactMetadata, emitReport, pruneBackups, requireStoppedOrReadonlyAllowed, scanBackups, scheduleBackupPrune, } from "./cleaner.js";
4
- import { runWizard } from "./wizard.js";
5
4
  const USAGE = `
6
5
  codex-cleaner [command] [options]
7
6
 
@@ -131,7 +130,12 @@ async function main(argv = process.argv.slice(2)) {
131
130
  pruneLogs: Boolean(parsed.values["prune-logs"]),
132
131
  pruneTuiLog: Boolean(parsed.values["prune-tui-log"]),
133
132
  };
133
+ const reexecCode = reexecWithSqliteWarningDisabled(argv);
134
+ if (reexecCode != null)
135
+ return reexecCode;
136
+ const { archiveOrphanRollouts, buildScanReport, checkpointWal, cleanCodex, compactMetadata, emitReport, pruneBackups, requireStoppedOrReadonlyAllowed, scanBackups, scheduleBackupPrune, } = await import("./cleaner.js");
134
137
  if (!command) {
138
+ const { runWizard } = await import("./wizard.js");
135
139
  return runWizard(options);
136
140
  }
137
141
  if (command !== "backups") {
@@ -178,6 +182,24 @@ function isMutating(command, options) {
178
182
  function allowsRunningFileOnlyMutation(command, options) {
179
183
  return command === "archive-orphan-rollouts" && options.apply && options.allowRunningOrphanRolloutArchive;
180
184
  }
185
+ function reexecWithSqliteWarningDisabled(argv) {
186
+ const alreadyDisabled = process.env.NODE_NO_WARNINGS === "1" ||
187
+ process.execArgv.includes("--no-warnings") ||
188
+ process.execArgv.includes("--disable-warning=ExperimentalWarning");
189
+ if (alreadyDisabled)
190
+ return null;
191
+ if (!process.allowedNodeEnvironmentFlags.has("--disable-warning=ExperimentalWarning"))
192
+ return null;
193
+ if (!process.argv[1])
194
+ return null;
195
+ const result = spawnSync(process.execPath, [...process.execArgv, "--disable-warning=ExperimentalWarning", process.argv[1], ...argv], {
196
+ stdio: "inherit",
197
+ windowsHide: true,
198
+ });
199
+ if (result.error)
200
+ throw result.error;
201
+ return result.status ?? 1;
202
+ }
181
203
  function parsePositiveInt(raw, name) {
182
204
  const value = Number(raw);
183
205
  if (!Number.isInteger(value) || value <= 0) {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "codex-cleaner",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Guarded cleanup and compaction utility for local Codex state.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "packageManager": "npm@11.11.0",
7
+ "packageManager": "npm@11.15.0",
8
8
  "bin": {
9
9
  "codex-cleaner": "dist/cli.js"
10
10
  },
@@ -13,7 +13,7 @@
13
13
  "README.md"
14
14
  ],
15
15
  "engines": {
16
- "node": ">=22.12"
16
+ "node": ">=22.16"
17
17
  },
18
18
  "scripts": {
19
19
  "build": "tsc -p tsconfig.json",
@@ -21,16 +21,14 @@
21
21
  "format": "prettier --check .",
22
22
  "lint": "eslint .",
23
23
  "prepack": "npm run build",
24
- "test": "vitest run"
24
+ "test": "node scripts/run-vitest.js"
25
25
  },
26
26
  "dependencies": {
27
27
  "@inquirer/prompts": "^8.4.3",
28
- "better-sqlite3": "^12.10.0",
29
28
  "picocolors": "^1.1.1"
30
29
  },
31
30
  "devDependencies": {
32
31
  "@eslint/js": "^10.0.1",
33
- "@types/better-sqlite3": "^7.6.13",
34
32
  "@types/node": "^25.9.1",
35
33
  "eslint": "^10.4.0",
36
34
  "globals": "^17.6.0",