@drewpayment/mink 0.11.0-beta.4 → 0.12.0-beta.1

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.
Files changed (71) hide show
  1. package/dashboard/out/404.html +1 -1
  2. package/dashboard/out/action-log.html +1 -1
  3. package/dashboard/out/action-log.txt +1 -1
  4. package/dashboard/out/activity.html +1 -1
  5. package/dashboard/out/activity.txt +1 -1
  6. package/dashboard/out/bugs.html +1 -1
  7. package/dashboard/out/bugs.txt +1 -1
  8. package/dashboard/out/capture.html +1 -1
  9. package/dashboard/out/capture.txt +1 -1
  10. package/dashboard/out/config.html +1 -1
  11. package/dashboard/out/config.txt +1 -1
  12. package/dashboard/out/daemon.html +1 -1
  13. package/dashboard/out/daemon.txt +1 -1
  14. package/dashboard/out/design.html +1 -1
  15. package/dashboard/out/design.txt +1 -1
  16. package/dashboard/out/discord.html +1 -1
  17. package/dashboard/out/discord.txt +1 -1
  18. package/dashboard/out/file-index.html +1 -1
  19. package/dashboard/out/file-index.txt +1 -1
  20. package/dashboard/out/index.html +1 -1
  21. package/dashboard/out/index.txt +1 -1
  22. package/dashboard/out/insights.html +1 -1
  23. package/dashboard/out/insights.txt +1 -1
  24. package/dashboard/out/learning.html +1 -1
  25. package/dashboard/out/learning.txt +1 -1
  26. package/dashboard/out/overview.html +1 -1
  27. package/dashboard/out/overview.txt +1 -1
  28. package/dashboard/out/scheduler.html +1 -1
  29. package/dashboard/out/scheduler.txt +1 -1
  30. package/dashboard/out/sync.html +1 -1
  31. package/dashboard/out/sync.txt +1 -1
  32. package/dashboard/out/tokens.html +1 -1
  33. package/dashboard/out/tokens.txt +1 -1
  34. package/dashboard/out/waste.html +1 -1
  35. package/dashboard/out/waste.txt +1 -1
  36. package/dashboard/out/wiki.html +1 -1
  37. package/dashboard/out/wiki.txt +1 -1
  38. package/dist/cli.bun.js +90615 -0
  39. package/dist/{cli.js → cli.node.js} +2227 -758
  40. package/package.json +14 -4
  41. package/scripts/build.mjs +47 -0
  42. package/src/commands/bug-search.ts +2 -4
  43. package/src/commands/detect-waste.ts +24 -32
  44. package/src/commands/post-read.ts +10 -11
  45. package/src/commands/post-write.ts +13 -19
  46. package/src/commands/pre-read.ts +19 -24
  47. package/src/commands/scan.ts +103 -40
  48. package/src/commands/status.ts +45 -26
  49. package/src/core/bug-memory.ts +32 -34
  50. package/src/core/dashboard-api.ts +44 -22
  51. package/src/core/index-store.ts +23 -0
  52. package/src/core/paths.ts +7 -0
  53. package/src/core/scanner.ts +8 -4
  54. package/src/core/state-aggregator.ts +64 -7
  55. package/src/core/state-counters.ts +11 -31
  56. package/src/core/sync-merge-drivers.ts +164 -1
  57. package/src/core/sync.ts +9 -0
  58. package/src/core/token-ledger.ts +50 -4
  59. package/src/repositories/bug-memory-repo.ts +268 -0
  60. package/src/repositories/counters-repo.ts +88 -0
  61. package/src/repositories/file-index-repo.ts +238 -0
  62. package/src/repositories/token-ledger-repo.ts +412 -0
  63. package/src/storage/db.ts +121 -0
  64. package/src/storage/driver.bun.ts +99 -0
  65. package/src/storage/driver.node.ts +107 -0
  66. package/src/storage/driver.ts +76 -0
  67. package/src/storage/migrate-json.ts +415 -0
  68. package/src/storage/schema.ts +207 -0
  69. package/src/types/file-index.ts +9 -0
  70. /package/dashboard/out/_next/static/{fci7mSuW5y3ri6IlmLojm → 9ElzGFcXpcjLq-QSQslWY}/_buildManifest.js +0 -0
  71. /package/dashboard/out/_next/static/{fci7mSuW5y3ri6IlmLojm → 9ElzGFcXpcjLq-QSQslWY}/_ssgManifest.js +0 -0
@@ -0,0 +1,121 @@
1
+ // Project database lifecycle. The handle is opened lazily and cached per
2
+ // process (hook commands are short-lived; their first call to a repository
3
+ // triggers the open, and the handle is closed via the registered exit hook).
4
+ //
5
+ // On first open for a project that has on-disk JSON state, the lazy JSON
6
+ // importer runs (see `migrate-json.ts`). The importer is idempotent — once
7
+ // `meta.migrated_from_json_at` is set, it returns immediately.
8
+
9
+ import { mkdirSync } from "fs";
10
+ import { dirname } from "path";
11
+ import { projectDbPath } from "../core/paths";
12
+ import { openDriver, type DbDriver } from "./driver";
13
+ import { applySchema } from "./schema";
14
+ import { migrateJsonIfNeeded } from "./migrate-json";
15
+
16
+ export { projectDbPath } from "../core/paths";
17
+
18
+ interface ConnectionEntry {
19
+ driver: DbDriver;
20
+ closed: boolean;
21
+ }
22
+
23
+ const handles = new Map<string, ConnectionEntry>();
24
+ let exitHookInstalled = false;
25
+
26
+ function installExitHook(): void {
27
+ if (exitHookInstalled) return;
28
+ exitHookInstalled = true;
29
+ const closeAll = (): void => {
30
+ for (const entry of handles.values()) {
31
+ if (entry.closed) continue;
32
+ try {
33
+ entry.driver.close();
34
+ } catch {
35
+ // best effort — process is shutting down
36
+ }
37
+ entry.closed = true;
38
+ }
39
+ };
40
+ process.on("exit", closeAll);
41
+ }
42
+
43
+ // Test-only — drop cached handles between tests that wipe MINK_ROOT_OVERRIDE.
44
+ // Production code never calls this; the exit hook handles real shutdown.
45
+ export function _resetDbCacheForTests(): void {
46
+ for (const entry of handles.values()) {
47
+ if (entry.closed) continue;
48
+ try {
49
+ entry.driver.close();
50
+ } catch {
51
+ // ignore
52
+ }
53
+ entry.closed = true;
54
+ }
55
+ handles.clear();
56
+ }
57
+
58
+ function applyPragmas(db: DbDriver): void {
59
+ // WAL: enables concurrent readers during a writer; survives crashes.
60
+ // synchronous=NORMAL: safe with WAL, ~2-5x faster than FULL.
61
+ // foreign_keys=ON: required for bug_tags / bug_related cascades.
62
+ // busy_timeout: matches the existing 5s hook safety timeout in
63
+ // src/core/runtime.ts — under contention SQLite will retry rather than
64
+ // throw SQLITE_BUSY immediately.
65
+ db.exec("PRAGMA journal_mode = WAL");
66
+ db.exec("PRAGMA synchronous = NORMAL");
67
+ db.exec("PRAGMA foreign_keys = ON");
68
+ db.exec("PRAGMA busy_timeout = 5000");
69
+ }
70
+
71
+ export function openProjectDb(cwd: string): DbDriver {
72
+ const path = projectDbPath(cwd);
73
+ const cached = handles.get(path);
74
+ if (cached && !cached.closed) return cached.driver;
75
+
76
+ mkdirSync(dirname(path), { recursive: true });
77
+ const driver = openDriver(path);
78
+ applyPragmas(driver);
79
+ applySchema(driver);
80
+
81
+ // Run migration AFTER applySchema so the importer can write into existing
82
+ // tables. The importer no-ops once `meta.migrated_from_json_at` is set.
83
+ try {
84
+ migrateJsonIfNeeded(driver, cwd);
85
+ } catch (err) {
86
+ // Migration failures should not block the process — log and continue
87
+ // with an empty DB. Phase 2 callers will fall back to legacy JSON reads.
88
+ // (We rethrow for tests via MINK_DB_STRICT_MIGRATE=1.)
89
+ if (process.env.MINK_DB_STRICT_MIGRATE === "1") throw err;
90
+ console.warn(
91
+ `[mink] JSON → SQLite migration failed for ${cwd}: ${
92
+ (err as Error).message
93
+ }`
94
+ );
95
+ }
96
+
97
+ installExitHook();
98
+ handles.set(path, { driver, closed: false });
99
+ return driver;
100
+ }
101
+
102
+ // Force a WAL checkpoint and close the handle for the given cwd. Used by
103
+ // `mink sync` before pushing so the .db is self-contained (the -wal/-shm
104
+ // sidecars are not synced).
105
+ export function checkpointAndClose(cwd: string): void {
106
+ const path = projectDbPath(cwd);
107
+ const entry = handles.get(path);
108
+ if (!entry || entry.closed) return;
109
+ try {
110
+ entry.driver.exec("PRAGMA wal_checkpoint(TRUNCATE)");
111
+ } catch {
112
+ // best effort
113
+ }
114
+ try {
115
+ entry.driver.close();
116
+ } catch {
117
+ // best effort
118
+ }
119
+ entry.closed = true;
120
+ handles.delete(path);
121
+ }
@@ -0,0 +1,99 @@
1
+ // bun:sqlite implementation of the DbDriver interface.
2
+ // Selected at build time when MINK_RUNTIME === "bun" and via `typeof Bun`
3
+ // detection when running unbundled.
4
+
5
+ import type { DbDriver, DriverModule, SqlParam, Statement } from "./driver";
6
+
7
+ // Use require() so the type-only import path doesn't trip Node when this
8
+ // file is loaded under the wrong runtime by mistake (the runtime dispatcher
9
+ // in driver.ts is supposed to prevent that).
10
+ const { Database } = require("bun:sqlite") as typeof import("bun:sqlite");
11
+
12
+ class BunStatement implements Statement {
13
+ constructor(private readonly stmt: import("bun:sqlite").Statement) {}
14
+
15
+ run(...params: SqlParam[]): { changes: number | bigint; lastInsertRowid: number | bigint } {
16
+ const r = this.stmt.run(...(params as never[]));
17
+ return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
18
+ }
19
+
20
+ get(...params: SqlParam[]) {
21
+ const row = this.stmt.get(...(params as never[]));
22
+ return (row ?? undefined) as Record<string, unknown> | undefined;
23
+ }
24
+
25
+ all(...params: SqlParam[]) {
26
+ return this.stmt.all(...(params as never[])) as Record<string, unknown>[];
27
+ }
28
+ }
29
+
30
+ class BunDriver implements DbDriver {
31
+ readonly filename: string;
32
+ private readonly db: import("bun:sqlite").Database;
33
+ private readonly txnDepth = { value: 0 };
34
+
35
+ constructor(filename: string) {
36
+ this.filename = filename;
37
+ this.db = new Database(filename, { create: true });
38
+ }
39
+
40
+ prepare(sql: string): Statement {
41
+ return new BunStatement(this.db.prepare(sql));
42
+ }
43
+
44
+ exec(sql: string): void {
45
+ this.db.exec(sql);
46
+ }
47
+
48
+ // We implement transactions manually (rather than using bun:sqlite's
49
+ // `db.transaction(fn)` wrapper) so semantics match node:sqlite exactly:
50
+ // synchronous, nestable via savepoints, IMMEDIATE locking to fail fast
51
+ // when another writer is mid-transaction.
52
+ transaction<T>(fn: () => T): T {
53
+ if (this.txnDepth.value > 0) {
54
+ const sp = `sp_${this.txnDepth.value}`;
55
+ this.db.exec(`SAVEPOINT ${sp}`);
56
+ this.txnDepth.value++;
57
+ try {
58
+ const result = fn();
59
+ this.db.exec(`RELEASE SAVEPOINT ${sp}`);
60
+ this.txnDepth.value--;
61
+ return result;
62
+ } catch (err) {
63
+ this.db.exec(`ROLLBACK TO SAVEPOINT ${sp}`);
64
+ this.db.exec(`RELEASE SAVEPOINT ${sp}`);
65
+ this.txnDepth.value--;
66
+ throw err;
67
+ }
68
+ }
69
+ this.db.exec("BEGIN IMMEDIATE");
70
+ this.txnDepth.value++;
71
+ try {
72
+ const result = fn();
73
+ this.db.exec("COMMIT");
74
+ this.txnDepth.value--;
75
+ return result;
76
+ } catch (err) {
77
+ this.db.exec("ROLLBACK");
78
+ this.txnDepth.value--;
79
+ throw err;
80
+ }
81
+ }
82
+
83
+ close(): void {
84
+ this.db.close();
85
+ }
86
+
87
+ pragma(stmt: string): unknown {
88
+ // bun:sqlite has no dedicated pragma() helper; route through exec/query.
89
+ // Pragmas that return a value (e.g. `journal_mode`) are SELECT-shaped.
90
+ if (/^[a-z_]+\s*=/i.test(stmt) || /^[a-z_]+\s*\([^)]*\)/i.test(stmt)) {
91
+ // Assignment or call form — no result expected, but the sqlite engine
92
+ // still returns the new value. Query so callers can read it.
93
+ return this.db.prepare(`PRAGMA ${stmt}`).all();
94
+ }
95
+ return this.db.prepare(`PRAGMA ${stmt}`).all();
96
+ }
97
+ }
98
+
99
+ export const open: DriverModule["open"] = (filename) => new BunDriver(filename);
@@ -0,0 +1,107 @@
1
+ // node:sqlite implementation of the DbDriver interface.
2
+ // Selected at build time when MINK_RUNTIME === "node" and via runtime
3
+ // detection when running unbundled under Node.
4
+
5
+ import type { DbDriver, DriverModule, SqlParam, Statement } from "./driver";
6
+
7
+ // Suppress Node 22's `ExperimentalWarning: SQLite is an experimental feature`
8
+ // the first (and only) time the module is required. We do it inline rather
9
+ // than via NODE_NO_WARNINGS so users don't lose warnings from other modules.
10
+ const originalEmit = process.emit;
11
+ process.emit = function patchedEmit(
12
+ this: NodeJS.Process,
13
+ event: string | symbol,
14
+ ...args: unknown[]
15
+ ): boolean {
16
+ if (
17
+ event === "warning" &&
18
+ args[0] instanceof Error &&
19
+ (args[0] as Error & { name: string }).name === "ExperimentalWarning" &&
20
+ /sqlite/i.test((args[0] as Error).message)
21
+ ) {
22
+ return false;
23
+ }
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ return (originalEmit as any).call(this, event, ...args);
26
+ } as typeof process.emit;
27
+
28
+ const { DatabaseSync } = require("node:sqlite") as typeof import("node:sqlite");
29
+
30
+ class NodeStatement implements Statement {
31
+ // node:sqlite uses StatementSync from its types
32
+ constructor(private readonly stmt: import("node:sqlite").StatementSync) {}
33
+
34
+ run(...params: SqlParam[]): { changes: number | bigint; lastInsertRowid: number | bigint } {
35
+ const r = this.stmt.run(...(params as never[]));
36
+ return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
37
+ }
38
+
39
+ get(...params: SqlParam[]) {
40
+ const row = this.stmt.get(...(params as never[]));
41
+ return (row ?? undefined) as Record<string, unknown> | undefined;
42
+ }
43
+
44
+ all(...params: SqlParam[]) {
45
+ return this.stmt.all(...(params as never[])) as Record<string, unknown>[];
46
+ }
47
+ }
48
+
49
+ class NodeDriver implements DbDriver {
50
+ readonly filename: string;
51
+ private readonly db: import("node:sqlite").DatabaseSync;
52
+ private readonly txnDepth = { value: 0 };
53
+
54
+ constructor(filename: string) {
55
+ this.filename = filename;
56
+ this.db = new DatabaseSync(filename);
57
+ }
58
+
59
+ prepare(sql: string): Statement {
60
+ return new NodeStatement(this.db.prepare(sql));
61
+ }
62
+
63
+ exec(sql: string): void {
64
+ this.db.exec(sql);
65
+ }
66
+
67
+ transaction<T>(fn: () => T): T {
68
+ if (this.txnDepth.value > 0) {
69
+ const sp = `sp_${this.txnDepth.value}`;
70
+ this.db.exec(`SAVEPOINT ${sp}`);
71
+ this.txnDepth.value++;
72
+ try {
73
+ const result = fn();
74
+ this.db.exec(`RELEASE SAVEPOINT ${sp}`);
75
+ this.txnDepth.value--;
76
+ return result;
77
+ } catch (err) {
78
+ this.db.exec(`ROLLBACK TO SAVEPOINT ${sp}`);
79
+ this.db.exec(`RELEASE SAVEPOINT ${sp}`);
80
+ this.txnDepth.value--;
81
+ throw err;
82
+ }
83
+ }
84
+ this.db.exec("BEGIN IMMEDIATE");
85
+ this.txnDepth.value++;
86
+ try {
87
+ const result = fn();
88
+ this.db.exec("COMMIT");
89
+ this.txnDepth.value--;
90
+ return result;
91
+ } catch (err) {
92
+ this.db.exec("ROLLBACK");
93
+ this.txnDepth.value--;
94
+ throw err;
95
+ }
96
+ }
97
+
98
+ close(): void {
99
+ this.db.close();
100
+ }
101
+
102
+ pragma(stmt: string): unknown {
103
+ return this.db.prepare(`PRAGMA ${stmt}`).all();
104
+ }
105
+ }
106
+
107
+ export const open: DriverModule["open"] = (filename) => new NodeDriver(filename);
@@ -0,0 +1,76 @@
1
+ // Build-time-selected SQLite driver adapter. The actual import of
2
+ // `bun:sqlite` or `node:sqlite` happens in the runtime-specific
3
+ // `driver.bun.ts` / `driver.node.ts` siblings; this file picks one based on
4
+ // the `MINK_RUNTIME` define injected by `bun build --define`. When neither
5
+ // define is set (e.g. running TypeScript directly under `bun test`), it
6
+ // falls back to `typeof Bun` detection.
7
+ //
8
+ // The adapter exposes a stable 5-method surface — `prepare`, `exec`,
9
+ // `transaction`, `close`, `pragma` — chosen as the minimal set that covers
10
+ // every call site in `src/repositories/`. Both backends' `Statement.run/get/all`
11
+ // methods are compatible enough that we pass them through unchanged.
12
+
13
+ declare const MINK_RUNTIME: string | undefined;
14
+
15
+ export type SqlParam = string | number | bigint | Uint8Array | null;
16
+ export type SqlRow = Record<string, unknown>;
17
+
18
+ export interface Statement {
19
+ run(...params: SqlParam[]): { changes: number | bigint; lastInsertRowid: number | bigint };
20
+ get(...params: SqlParam[]): SqlRow | undefined;
21
+ all(...params: SqlParam[]): SqlRow[];
22
+ iterate?(...params: SqlParam[]): IterableIterator<SqlRow>;
23
+ }
24
+
25
+ export interface DbDriver {
26
+ prepare(sql: string): Statement;
27
+ exec(sql: string): void;
28
+ transaction<T>(fn: () => T): T;
29
+ close(): void;
30
+ pragma(stmt: string): unknown;
31
+ readonly filename: string;
32
+ }
33
+
34
+ export interface DriverModule {
35
+ open(filename: string): DbDriver;
36
+ }
37
+
38
+ function pickRuntime(): "bun" | "node" {
39
+ // `MINK_RUNTIME` is replaced at bundle time. When running source directly
40
+ // (tests, `bun src/cli.ts`), the symbol is undefined and we fall back to
41
+ // feature detection.
42
+ try {
43
+ if (typeof MINK_RUNTIME !== "undefined") {
44
+ if (MINK_RUNTIME === "bun" || MINK_RUNTIME === "node") return MINK_RUNTIME;
45
+ }
46
+ } catch {
47
+ // ReferenceError when the symbol is not declared — proceed to detect.
48
+ }
49
+ return typeof Bun !== "undefined" ? "bun" : "node";
50
+ }
51
+
52
+ let cached: DriverModule | undefined;
53
+
54
+ function loadDriver(): DriverModule {
55
+ if (cached) return cached;
56
+ const runtime = pickRuntime();
57
+ // Conditional `require` keeps the unused branch out of the active bundle
58
+ // when `bun build` does constant-folding on `pickRuntime()`'s result via
59
+ // the `MINK_RUNTIME` define. At runtime, only the matching branch ever
60
+ // executes, so the other module's `import 'bun:sqlite'` /
61
+ // `import 'node:sqlite'` is never evaluated.
62
+ if (runtime === "bun") {
63
+ cached = require("./driver.bun") as DriverModule;
64
+ } else {
65
+ cached = require("./driver.node") as DriverModule;
66
+ }
67
+ return cached;
68
+ }
69
+
70
+ export function openDriver(filename: string): DbDriver {
71
+ return loadDriver().open(filename);
72
+ }
73
+
74
+ export function currentRuntime(): "bun" | "node" {
75
+ return pickRuntime();
76
+ }