kanbo-cli 0.1.0

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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -0
  3. package/dist/better-sqlite3-fU3G4xnX.cjs +2254 -0
  4. package/dist/better-sqlite3-fU3G4xnX.cjs.map +1 -0
  5. package/dist/cli.cjs +42949 -0
  6. package/dist/cli.cjs.map +1 -0
  7. package/dist/dist-Cd6wg8pp.cjs +942 -0
  8. package/dist/dist-Cd6wg8pp.cjs.map +1 -0
  9. package/dist/drizzle-postgres/0000_board.sql +135 -0
  10. package/dist/drizzle-postgres/0001_history_order.sql +2 -0
  11. package/dist/drizzle-postgres/0002_issue_pull_requests.sql +16 -0
  12. package/dist/drizzle-postgres/0003_milestone_start_date.sql +1 -0
  13. package/dist/drizzle-postgres/0004_status_entry_rules.sql +1 -0
  14. package/dist/drizzle-postgres/0005_run_external_session_ref.sql +1 -0
  15. package/dist/drizzle-postgres/README.md +61 -0
  16. package/dist/drizzle-postgres/meta/0000_snapshot.json +1078 -0
  17. package/dist/drizzle-postgres/meta/0001_snapshot.json +1090 -0
  18. package/dist/drizzle-postgres/meta/0002_snapshot.json +1239 -0
  19. package/dist/drizzle-postgres/meta/0003_snapshot.json +1245 -0
  20. package/dist/drizzle-postgres/meta/0004_snapshot.json +1251 -0
  21. package/dist/drizzle-postgres/meta/0005_snapshot.json +1257 -0
  22. package/dist/drizzle-postgres/meta/_journal.json +48 -0
  23. package/dist/drizzle-sqlite/0000_board.sql +133 -0
  24. package/dist/drizzle-sqlite/0001_issue_pull_requests.sql +15 -0
  25. package/dist/drizzle-sqlite/0002_milestone_start_date.sql +1 -0
  26. package/dist/drizzle-sqlite/0003_status_entry_rules.sql +1 -0
  27. package/dist/drizzle-sqlite/0004_run_external_session_ref.sql +1 -0
  28. package/dist/drizzle-sqlite/README.md +67 -0
  29. package/dist/drizzle-sqlite/meta/0000_snapshot.json +946 -0
  30. package/dist/drizzle-sqlite/meta/0001_snapshot.json +1054 -0
  31. package/dist/drizzle-sqlite/meta/0002_snapshot.json +1061 -0
  32. package/dist/drizzle-sqlite/meta/0003_snapshot.json +1068 -0
  33. package/dist/drizzle-sqlite/meta/0004_snapshot.json +1075 -0
  34. package/dist/drizzle-sqlite/meta/_journal.json +41 -0
  35. package/dist/lib/board-store-B16YarvX.d.mts +281 -0
  36. package/dist/lib/board-store-BHHo_ZlR.d.cts +281 -0
  37. package/dist/lib/index.cjs +340 -0
  38. package/dist/lib/index.cjs.map +1 -0
  39. package/dist/lib/index.d.cts +1608 -0
  40. package/dist/lib/index.d.mts +1608 -0
  41. package/dist/lib/index.mjs +284 -0
  42. package/dist/lib/index.mjs.map +1 -0
  43. package/dist/lib/manifest-C0EcwwgO.mjs +7866 -0
  44. package/dist/lib/manifest-C0EcwwgO.mjs.map +1 -0
  45. package/dist/lib/manifest-CpZOcB_6.cjs +8272 -0
  46. package/dist/lib/manifest-CpZOcB_6.cjs.map +1 -0
  47. package/dist/lib/mcp/index.cjs +184 -0
  48. package/dist/lib/mcp/index.cjs.map +1 -0
  49. package/dist/lib/mcp/index.d.cts +346 -0
  50. package/dist/lib/mcp/index.d.mts +346 -0
  51. package/dist/lib/mcp/index.mjs +173 -0
  52. package/dist/lib/mcp/index.mjs.map +1 -0
  53. package/dist/lib/migrate-BhYi9cXw.mjs +656 -0
  54. package/dist/lib/migrate-BhYi9cXw.mjs.map +1 -0
  55. package/dist/lib/migrate-DnMjTCvu.cjs +709 -0
  56. package/dist/lib/migrate-DnMjTCvu.cjs.map +1 -0
  57. package/dist/lib/migrations-path-CZxVOgeD.cjs +109 -0
  58. package/dist/lib/migrations-path-CZxVOgeD.cjs.map +1 -0
  59. package/dist/lib/migrations-path-Ciwan537.mjs +80 -0
  60. package/dist/lib/migrations-path-Ciwan537.mjs.map +1 -0
  61. package/dist/lib/postgres/index.cjs +56 -0
  62. package/dist/lib/postgres/index.cjs.map +1 -0
  63. package/dist/lib/postgres/index.d.cts +3403 -0
  64. package/dist/lib/postgres/index.d.mts +3403 -0
  65. package/dist/lib/postgres/index.mjs +35 -0
  66. package/dist/lib/postgres/index.mjs.map +1 -0
  67. package/dist/lib/roles.sql-B9rt-x4o.cjs +1033 -0
  68. package/dist/lib/roles.sql-B9rt-x4o.cjs.map +1 -0
  69. package/dist/lib/roles.sql-CvyCGt38.mjs +920 -0
  70. package/dist/lib/roles.sql-CvyCGt38.mjs.map +1 -0
  71. package/dist/lib/schema-epoch-ZyumVL_A.d.cts +16 -0
  72. package/dist/lib/schema-epoch-ZyumVL_A.d.mts +16 -0
  73. package/dist/lib/schema-fsHqNAip.d.cts +3612 -0
  74. package/dist/lib/schema-fsHqNAip.d.mts +3612 -0
  75. package/dist/lib/sqlite/index.cjs +23 -0
  76. package/dist/lib/sqlite/index.d.cts +175 -0
  77. package/dist/lib/sqlite/index.d.mts +175 -0
  78. package/dist/lib/sqlite/index.mjs +4 -0
  79. package/dist/lib/sqlite/schema.cjs +266 -0
  80. package/dist/lib/sqlite/schema.cjs.map +1 -0
  81. package/dist/lib/sqlite/schema.d.cts +2 -0
  82. package/dist/lib/sqlite/schema.d.mts +2 -0
  83. package/dist/lib/sqlite/schema.mjs +256 -0
  84. package/dist/lib/sqlite/schema.mjs.map +1 -0
  85. package/dist/lib/tool-names-9cmCn0gP.d.cts +112 -0
  86. package/dist/lib/tool-names-Dxg1UTUl.d.mts +112 -0
  87. package/dist/page/board.css +477 -0
  88. package/dist/page/board.js +2 -0
  89. package/drizzle-postgres/0000_board.sql +135 -0
  90. package/drizzle-postgres/0001_history_order.sql +2 -0
  91. package/drizzle-postgres/0002_issue_pull_requests.sql +16 -0
  92. package/drizzle-postgres/0003_milestone_start_date.sql +1 -0
  93. package/drizzle-postgres/0004_status_entry_rules.sql +1 -0
  94. package/drizzle-postgres/0005_run_external_session_ref.sql +1 -0
  95. package/drizzle-postgres/README.md +61 -0
  96. package/drizzle-postgres/meta/0000_snapshot.json +1078 -0
  97. package/drizzle-postgres/meta/0001_snapshot.json +1090 -0
  98. package/drizzle-postgres/meta/0002_snapshot.json +1239 -0
  99. package/drizzle-postgres/meta/0003_snapshot.json +1245 -0
  100. package/drizzle-postgres/meta/0004_snapshot.json +1251 -0
  101. package/drizzle-postgres/meta/0005_snapshot.json +1257 -0
  102. package/drizzle-postgres/meta/_journal.json +48 -0
  103. package/drizzle-sqlite/0000_board.sql +133 -0
  104. package/drizzle-sqlite/0001_issue_pull_requests.sql +15 -0
  105. package/drizzle-sqlite/0002_milestone_start_date.sql +1 -0
  106. package/drizzle-sqlite/0003_status_entry_rules.sql +1 -0
  107. package/drizzle-sqlite/0004_run_external_session_ref.sql +1 -0
  108. package/drizzle-sqlite/README.md +67 -0
  109. package/drizzle-sqlite/meta/0000_snapshot.json +946 -0
  110. package/drizzle-sqlite/meta/0001_snapshot.json +1054 -0
  111. package/drizzle-sqlite/meta/0002_snapshot.json +1061 -0
  112. package/drizzle-sqlite/meta/0003_snapshot.json +1068 -0
  113. package/drizzle-sqlite/meta/0004_snapshot.json +1075 -0
  114. package/drizzle-sqlite/meta/_journal.json +41 -0
  115. package/package.json +147 -0
@@ -0,0 +1,709 @@
1
+ const require_migrations_path = require("./migrations-path-CZxVOgeD.cjs");
2
+ const require_sqlite_schema = require("./sqlite/schema.cjs");
3
+ let drizzle_orm = require("drizzle-orm");
4
+ let drizzle_orm_sqlite_core = require("drizzle-orm/sqlite-core");
5
+ let node_module = require("node:module");
6
+ let node_path = require("node:path");
7
+ let node_url = require("node:url");
8
+ let drizzle_orm_better_sqlite3_migrator = require("drizzle-orm/better-sqlite3/migrator");
9
+ //#region src/sqlite/transaction.ts
10
+ /**
11
+ * The better-sqlite3 handle Drizzle keeps beside the query builder. `drizzle()`
12
+ * attaches it as `$client` (`drizzle-orm/better-sqlite3/driver.js`), but the
13
+ * handle is declared as a `BetterSQLite3Database`, a class that does not
14
+ * declare `$client` — which is why the assertion below restates the shape by
15
+ * hand. Its `inTransaction` is the only honest answer to "is a
16
+ * transaction already open", including one this module did not open.
17
+ */
18
+ function readSqliteConnection(database) {
19
+ return database.$client;
20
+ }
21
+ /**
22
+ * One fixed name for every nested scope, the way better-sqlite3 names its own
23
+ * (`lib/methods/transaction.js`). `release` and `rollback to` address the
24
+ * innermost savepoint carrying the name, and these scopes are opened and closed
25
+ * in stack order, so each call reaches its own — a counter in the name would
26
+ * only repeat what SQLite already tracks, and would collide the moment two
27
+ * scopes nested through different code paths.
28
+ */
29
+ const SAVEPOINT_NAME = "kanbo_tx";
30
+ function readTransactionScope(connection, mode) {
31
+ if (!connection.inTransaction) return {
32
+ begin: mode === "immediate" ? "begin immediate" : "begin",
33
+ commit: "commit",
34
+ undo: "rollback",
35
+ releaseAfterUndo: null
36
+ };
37
+ return {
38
+ begin: `savepoint ${SAVEPOINT_NAME}`,
39
+ commit: `release ${SAVEPOINT_NAME}`,
40
+ undo: `rollback to ${SAVEPOINT_NAME}`,
41
+ releaseAfterUndo: `release ${SAVEPOINT_NAME}`
42
+ };
43
+ }
44
+ /**
45
+ * Run `fn` inside one SQLite transaction that is allowed to await.
46
+ *
47
+ * `database.transaction(...)` cannot serve here: it commits the moment its
48
+ * callback returns, and an async callback returns a pending promise, so the
49
+ * transaction would close before the work inside it ran. The statements are
50
+ * issued by hand instead, and the driver's own `inTransaction` flag picks which
51
+ * — the same branch better-sqlite3 takes. With nothing open: `begin`, then
52
+ * `commit`, or `rollback` when `fn` throws. With a transaction already open —
53
+ * another call to this helper, or one a module opened through
54
+ * `database.transaction` — a `savepoint` … `release` pair, undone by
55
+ * `rollback to` followed by its own `release`. A second `begin` would throw,
56
+ * which is why the flag decides and not a private counter. When SQLite has
57
+ * already aborted the transaction itself the undo is skipped: it would throw
58
+ * over the caller's error.
59
+ *
60
+ * **The callback must be the only database-touching async chain in flight —
61
+ * no `Promise.all` sibling, no lock hand-off, no fire-and-forget promise
62
+ * started before it — and inside it, only await operations that resolve
63
+ * without I/O: store methods, synchronous database writes.** Even a resolved
64
+ * promise yields to the microtask queue first, so a sibling chain already
65
+ * queued in the same macrotask still runs inside this open transaction; a
66
+ * chain started in a separate macrotask is unaffected. Awaiting anything that
67
+ * truly suspends — a timer, the filesystem, the network — hands the
68
+ * connection to whatever runs next, and its writes land in this transaction
69
+ * too.
70
+ */
71
+ async function runSqliteTransaction(database, fn, options) {
72
+ const connection = readSqliteConnection(database);
73
+ const scope = readTransactionScope(connection, options?.mode ?? "deferred");
74
+ database.run(drizzle_orm.sql.raw(scope.begin));
75
+ try {
76
+ const result = await fn();
77
+ database.run(drizzle_orm.sql.raw(scope.commit));
78
+ return result;
79
+ } catch (error) {
80
+ if (connection.inTransaction) {
81
+ database.run(drizzle_orm.sql.raw(scope.undo));
82
+ if (scope.releaseAfterUndo) database.run(drizzle_orm.sql.raw(scope.releaseAfterUndo));
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+ //#endregion
88
+ //#region src/sqlite/board-store.sqlite.ts
89
+ /**
90
+ * The order two runs of one card are in when they started in the same second.
91
+ *
92
+ * `started_at` counts whole seconds and a run id is a random UUID, so the column
93
+ * pair the attempt number is built on cannot separate two launches a second
94
+ * apart — the row the board wrote second has to be the second attempt. SQLite
95
+ * hands that out already: `rowid` rises with every insert. A store on another
96
+ * engine answers the same question with whatever its own insert order is.
97
+ */
98
+ const RUN_INSERTION_ORDER = drizzle_orm.sql`${require_sqlite_schema.issueRuns}.rowid asc`;
99
+ /**
100
+ * The same tie-break for the two tables that record what happened to a card.
101
+ *
102
+ * `created_at` counts whole seconds here too, and a person approving a card and
103
+ * an agent commenting on it inside the same second must come back in the order
104
+ * the board wrote them: `readApproval` takes the *last* comment to decide
105
+ * whether the card was approved or returned.
106
+ */
107
+ const COMMENT_INSERTION_ORDER = drizzle_orm.sql`${require_sqlite_schema.issueComments}.rowid asc`;
108
+ const FIELD_CHANGE_INSERTION_ORDER = drizzle_orm.sql`${require_sqlite_schema.issueFieldChanges}.rowid asc`;
109
+ /** And for a card's pull-request links, which list in the order they were made. */
110
+ const PULL_REQUEST_INSERTION_ORDER = drizzle_orm.sql`${require_sqlite_schema.issuePullRequests}.rowid asc`;
111
+ /**
112
+ * The run facts of every card in scope, in one statement.
113
+ *
114
+ * The window functions do the grouping the caller would otherwise do with a
115
+ * second query per card: `count(*)` counts the card's runs on every one of its
116
+ * rows, and `row_number()` ranks them so that a `running` run wins, then the
117
+ * newest start, then the row written last — the tie-break two runs started in
118
+ * the same second need. Reading only rank 1 therefore leaves one row per card, carrying
119
+ * the run the board should show — and `state` on that row says whether it is
120
+ * still going, which is what turns it into an `activeRun` or into `null`.
121
+ */
122
+ function selectRunProjection(database, scope) {
123
+ if ("issueIds" in scope && scope.issueIds.length === 0) return [];
124
+ const cards = "workspaceId" in scope ? drizzle_orm.sql`select ${require_sqlite_schema.issues.id} from ${require_sqlite_schema.issues} where ${require_sqlite_schema.issues.workspaceId} = ${scope.workspaceId}` : drizzle_orm.sql`values ${drizzle_orm.sql.join(scope.issueIds.map((issueId) => drizzle_orm.sql`(${issueId})`), drizzle_orm.sql`, `)}`;
125
+ return database.all(drizzle_orm.sql`
126
+ select
127
+ ranked.issue_id as issueId,
128
+ ranked.attempt_count as attemptCount,
129
+ ranked.id as id,
130
+ ranked.agent_id as agentId,
131
+ ranked.agent_name as agentName,
132
+ ranked.state as state,
133
+ ranked.branch as branch,
134
+ ranked.execution_mode as executionMode,
135
+ ranked.chat_session_id as chatSessionId,
136
+ ranked.started_at as startedAt
137
+ from (
138
+ select
139
+ run.*,
140
+ count(*) over (partition by run.issue_id) as attempt_count,
141
+ row_number() over (
142
+ partition by run.issue_id
143
+ order by (run.state = 'running') desc, run.started_at desc, run.rowid desc
144
+ ) as card_rank
145
+ from ${require_sqlite_schema.issueRuns} as run
146
+ where run.issue_id in (${cards})
147
+ ) as ranked
148
+ where ranked.card_rank = 1
149
+ `).map((row) => ({
150
+ issueId: row.issueId,
151
+ attemptCount: row.attemptCount,
152
+ activeRun: row.state === "running" ? {
153
+ id: row.id,
154
+ agentId: row.agentId,
155
+ agentName: row.agentName,
156
+ state: row.state,
157
+ branch: row.branch,
158
+ executionMode: row.executionMode,
159
+ chatSessionId: row.chatSessionId,
160
+ startedAt: row.startedAt
161
+ } : null
162
+ }));
163
+ }
164
+ /**
165
+ * Bind the store's statements to one SQLite handle. `resolveDatabase` is called
166
+ * per statement rather than captured, so the store follows a host that swaps its
167
+ * database out (a server that resets its infrastructure between tests, say).
168
+ */
169
+ function createSqliteBoardStoreCore(resolveDatabase) {
170
+ return {
171
+ statuses: {
172
+ listByWorkspace: (workspaceId) => resolveDatabase().select().from(require_sqlite_schema.issueStatuses).where((0, drizzle_orm.eq)(require_sqlite_schema.issueStatuses.workspaceId, workspaceId)).orderBy(require_sqlite_schema.issueStatuses.order).all(),
173
+ listAllInBoardOrder: () => resolveDatabase().select().from(require_sqlite_schema.issueStatuses).orderBy(require_sqlite_schema.issueStatuses.workspaceId, require_sqlite_schema.issueStatuses.order).all(),
174
+ listAll: () => resolveDatabase().select().from(require_sqlite_schema.issueStatuses).all(),
175
+ findById: (statusId) => resolveDatabase().select().from(require_sqlite_schema.issueStatuses).where((0, drizzle_orm.eq)(require_sqlite_schema.issueStatuses.id, statusId)).get() ?? null,
176
+ findInWorkspace: (workspaceId, statusId) => resolveDatabase().select().from(require_sqlite_schema.issueStatuses).where(drizzle_orm.sql`${require_sqlite_schema.issueStatuses.workspaceId} = ${workspaceId} AND ${require_sqlite_schema.issueStatuses.id} = ${statusId}`).get() ?? null,
177
+ countByWorkspace: (workspaceId) => {
178
+ return resolveDatabase().select({ count: drizzle_orm.sql`count(*)` }).from(require_sqlite_schema.issueStatuses).where((0, drizzle_orm.eq)(require_sqlite_schema.issueStatuses.workspaceId, workspaceId)).get()?.count ?? 0;
179
+ },
180
+ create: (values) => resolveDatabase().insert(require_sqlite_schema.issueStatuses).values(values).returning().get(),
181
+ update: (statusId, patch) => {
182
+ resolveDatabase().update(require_sqlite_schema.issueStatuses).set(patch).where((0, drizzle_orm.eq)(require_sqlite_schema.issueStatuses.id, statusId)).run();
183
+ },
184
+ delete: (statusId) => {
185
+ resolveDatabase().delete(require_sqlite_schema.issueStatuses).where((0, drizzle_orm.eq)(require_sqlite_schema.issueStatuses.id, statusId)).run();
186
+ }
187
+ },
188
+ milestones: {
189
+ listNewestFirst: (workspaceId) => {
190
+ const predicates = [workspaceId ? (0, drizzle_orm.eq)(require_sqlite_schema.issueMilestones.workspaceId, workspaceId) : void 0].filter((predicate) => predicate !== void 0);
191
+ return resolveDatabase().select().from(require_sqlite_schema.issueMilestones).where(predicates.length > 0 ? (0, drizzle_orm.and)(...predicates) : void 0).orderBy((0, drizzle_orm.desc)(require_sqlite_schema.issueMilestones.createdAt)).all();
192
+ },
193
+ listByWorkspace: (workspaceId) => resolveDatabase().select().from(require_sqlite_schema.issueMilestones).where((0, drizzle_orm.eq)(require_sqlite_schema.issueMilestones.workspaceId, workspaceId)).all(),
194
+ listAll: () => resolveDatabase().select().from(require_sqlite_schema.issueMilestones).all(),
195
+ findById: (milestoneId) => resolveDatabase().select().from(require_sqlite_schema.issueMilestones).where((0, drizzle_orm.eq)(require_sqlite_schema.issueMilestones.id, milestoneId)).get() ?? null,
196
+ findInWorkspace: (workspaceId, milestoneId) => resolveDatabase().select().from(require_sqlite_schema.issueMilestones).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issueMilestones.id, milestoneId), (0, drizzle_orm.eq)(require_sqlite_schema.issueMilestones.workspaceId, workspaceId))).get() ?? null,
197
+ create: (values) => resolveDatabase().insert(require_sqlite_schema.issueMilestones).values(values).returning().get(),
198
+ update: (milestoneId, patch) => {
199
+ resolveDatabase().update(require_sqlite_schema.issueMilestones).set(patch).where((0, drizzle_orm.eq)(require_sqlite_schema.issueMilestones.id, milestoneId)).run();
200
+ },
201
+ delete: (milestoneId) => {
202
+ resolveDatabase().delete(require_sqlite_schema.issueMilestones).where((0, drizzle_orm.eq)(require_sqlite_schema.issueMilestones.id, milestoneId)).run();
203
+ }
204
+ },
205
+ issues: {
206
+ listInBoardOrder: (workspaceId) => {
207
+ const predicates = [workspaceId ? (0, drizzle_orm.eq)(require_sqlite_schema.issues.workspaceId, workspaceId) : void 0].filter((predicate) => predicate !== void 0);
208
+ return resolveDatabase().select().from(require_sqlite_schema.issues).where(predicates.length > 0 ? (0, drizzle_orm.and)(...predicates) : void 0).orderBy(require_sqlite_schema.issues.order, (0, drizzle_orm.desc)(require_sqlite_schema.issues.createdAt)).all();
209
+ },
210
+ listNewestFirst: () => resolveDatabase().select().from(require_sqlite_schema.issues).orderBy((0, drizzle_orm.desc)(require_sqlite_schema.issues.createdAt)).all(),
211
+ listAll: () => resolveDatabase().select().from(require_sqlite_schema.issues).all(),
212
+ listByWorkspace: (workspaceId) => resolveDatabase().select().from(require_sqlite_schema.issues).where((0, drizzle_orm.eq)(require_sqlite_schema.issues.workspaceId, workspaceId)).all(),
213
+ listByIds: (issueIds) => resolveDatabase().select().from(require_sqlite_schema.issues).where((0, drizzle_orm.inArray)(require_sqlite_schema.issues.id, issueIds)).all(),
214
+ findById: (issueId) => resolveDatabase().select().from(require_sqlite_schema.issues).where((0, drizzle_orm.eq)(require_sqlite_schema.issues.id, issueId)).get() ?? null,
215
+ findInWorkspace: (workspaceId, issueId) => resolveDatabase().select().from(require_sqlite_schema.issues).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issues.id, issueId), (0, drizzle_orm.eq)(require_sqlite_schema.issues.workspaceId, workspaceId))).get() ?? null,
216
+ findByNumber: (workspaceId, number) => resolveDatabase().select().from(require_sqlite_schema.issues).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issues.workspaceId, workspaceId), (0, drizzle_orm.eq)(require_sqlite_schema.issues.number, number))).get() ?? null,
217
+ maxNumber: (workspaceId) => {
218
+ return resolveDatabase().select({ maxNum: drizzle_orm.sql`coalesce(max(${require_sqlite_schema.issues.number}), 0)` }).from(require_sqlite_schema.issues).where((0, drizzle_orm.eq)(require_sqlite_schema.issues.workspaceId, workspaceId)).get()?.maxNum ?? 0;
219
+ },
220
+ maxOrder: (workspaceId) => {
221
+ return resolveDatabase().select({ maxOrder: drizzle_orm.sql`coalesce(max(${require_sqlite_schema.issues.order}), 0)` }).from(require_sqlite_schema.issues).where((0, drizzle_orm.eq)(require_sqlite_schema.issues.workspaceId, workspaceId)).get()?.maxOrder ?? 0;
222
+ },
223
+ create: (values) => resolveDatabase().insert(require_sqlite_schema.issues).values(values).returning().get(),
224
+ update: (issueId, patch) => {
225
+ resolveDatabase().update(require_sqlite_schema.issues).set(patch).where((0, drizzle_orm.eq)(require_sqlite_schema.issues.id, issueId)).run();
226
+ },
227
+ updateMany: (issueIds, patch) => resolveDatabase().update(require_sqlite_schema.issues).set(patch).where(drizzle_orm.sql`${require_sqlite_schema.issues.id} IN (${drizzle_orm.sql.join(issueIds.map((issueId) => drizzle_orm.sql`${issueId}`), drizzle_orm.sql`, `)})`).run().changes,
228
+ clearMilestoneReferences: (milestoneId, updatedAt) => {
229
+ resolveDatabase().update(require_sqlite_schema.issues).set({
230
+ milestoneId: null,
231
+ updatedAt
232
+ }).where((0, drizzle_orm.eq)(require_sqlite_schema.issues.milestoneId, milestoneId)).run();
233
+ },
234
+ clearParentReferences: (parentIssueId, updatedAt) => {
235
+ resolveDatabase().update(require_sqlite_schema.issues).set({
236
+ parentIssueId: null,
237
+ updatedAt
238
+ }).where((0, drizzle_orm.eq)(require_sqlite_schema.issues.parentIssueId, parentIssueId)).run();
239
+ },
240
+ clearDelegateReferences: (delegate, updatedAt) => resolveDatabase().update(require_sqlite_schema.issues).set({
241
+ delegateAgentId: null,
242
+ delegateProviderTargetId: null,
243
+ updatedAt
244
+ }).where("agentId" in delegate ? (0, drizzle_orm.eq)(require_sqlite_schema.issues.delegateAgentId, delegate.agentId) : (0, drizzle_orm.eq)(require_sqlite_schema.issues.delegateProviderTargetId, delegate.providerTargetId)).run().changes,
245
+ delete: (issueId) => {
246
+ resolveDatabase().delete(require_sqlite_schema.issues).where((0, drizzle_orm.eq)(require_sqlite_schema.issues.id, issueId)).run();
247
+ }
248
+ },
249
+ comments: {
250
+ listByIssue: (issueId) => resolveDatabase().select().from(require_sqlite_schema.issueComments).where((0, drizzle_orm.eq)(require_sqlite_schema.issueComments.issueId, issueId)).orderBy((0, drizzle_orm.asc)(require_sqlite_schema.issueComments.createdAt), COMMENT_INSERTION_ORDER).all(),
251
+ findById: (commentId) => resolveDatabase().select().from(require_sqlite_schema.issueComments).where((0, drizzle_orm.eq)(require_sqlite_schema.issueComments.id, commentId)).get() ?? null,
252
+ findByDedupeKey: (issueId, dedupeKey) => resolveDatabase().select().from(require_sqlite_schema.issueComments).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issueComments.issueId, issueId), (0, drizzle_orm.eq)(require_sqlite_schema.issueComments.dedupeKey, dedupeKey))).get() ?? null,
253
+ listByDedupeKeyPrefix: (issueId, prefix) => resolveDatabase().select().from(require_sqlite_schema.issueComments).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issueComments.issueId, issueId), drizzle_orm.sql`substr(${require_sqlite_schema.issueComments.dedupeKey}, 1, ${prefix.length}) = ${prefix}`)).orderBy((0, drizzle_orm.asc)(require_sqlite_schema.issueComments.createdAt), COMMENT_INSERTION_ORDER).all(),
254
+ create: (values) => resolveDatabase().insert(require_sqlite_schema.issueComments).values(values).returning().get(),
255
+ createOnce: (values) => {
256
+ const database = resolveDatabase();
257
+ const inserted = database.insert(require_sqlite_schema.issueComments).values(values).onConflictDoNothing().returning().get();
258
+ if (inserted) return inserted;
259
+ const existing = database.select().from(require_sqlite_schema.issueComments).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issueComments.issueId, values.issueId), (0, drizzle_orm.eq)(require_sqlite_schema.issueComments.dedupeKey, values.dedupeKey))).get();
260
+ if (!existing) throw new Error(`issue comment for ${values.issueId} was refused, and no comment carries dedupe key ${values.dedupeKey}`);
261
+ return existing;
262
+ },
263
+ delete: (commentId) => {
264
+ resolveDatabase().delete(require_sqlite_schema.issueComments).where((0, drizzle_orm.eq)(require_sqlite_schema.issueComments.id, commentId)).run();
265
+ }
266
+ },
267
+ runs: {
268
+ listByIssue: (issueId) => resolveDatabase().select().from(require_sqlite_schema.issueRuns).where((0, drizzle_orm.eq)(require_sqlite_schema.issueRuns.issueId, issueId)).orderBy((0, drizzle_orm.asc)(require_sqlite_schema.issueRuns.startedAt), RUN_INSERTION_ORDER).all(),
269
+ listRunning: (issueId) => resolveDatabase().select().from(require_sqlite_schema.issueRuns).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issueRuns.issueId, issueId), (0, drizzle_orm.eq)(require_sqlite_schema.issueRuns.state, "running"))).orderBy((0, drizzle_orm.asc)(require_sqlite_schema.issueRuns.startedAt), RUN_INSERTION_ORDER).all(),
270
+ listRunningWithSession: () => resolveDatabase().select().from(require_sqlite_schema.issueRuns).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issueRuns.state, "running"), (0, drizzle_orm.isNotNull)(require_sqlite_schema.issueRuns.chatSessionId))).orderBy((0, drizzle_orm.asc)(require_sqlite_schema.issueRuns.startedAt), RUN_INSERTION_ORDER).all(),
271
+ findById: (runId) => resolveDatabase().select().from(require_sqlite_schema.issueRuns).where((0, drizzle_orm.eq)(require_sqlite_schema.issueRuns.id, runId)).get() ?? null,
272
+ findByChatSessionId: (chatSessionId) => resolveDatabase().select().from(require_sqlite_schema.issueRuns).where((0, drizzle_orm.eq)(require_sqlite_schema.issueRuns.chatSessionId, chatSessionId)).orderBy((0, drizzle_orm.desc)(require_sqlite_schema.issueRuns.startedAt), drizzle_orm.sql`${require_sqlite_schema.issueRuns}.rowid desc`).get() ?? null,
273
+ countByIssue: (issueId) => {
274
+ return resolveDatabase().select({ count: drizzle_orm.sql`count(*)` }).from(require_sqlite_schema.issueRuns).where((0, drizzle_orm.eq)(require_sqlite_schema.issueRuns.issueId, issueId)).get()?.count ?? 0;
275
+ },
276
+ create: (values) => resolveDatabase().insert(require_sqlite_schema.issueRuns).values(values).returning().get(),
277
+ update: (runId, patch) => {
278
+ resolveDatabase().update(require_sqlite_schema.issueRuns).set(patch).where((0, drizzle_orm.eq)(require_sqlite_schema.issueRuns.id, runId)).run();
279
+ },
280
+ project: (scope) => selectRunProjection(resolveDatabase(), scope)
281
+ },
282
+ relations: {
283
+ listByIssue: (issueId) => resolveDatabase().select().from(require_sqlite_schema.issueRelations).where((0, drizzle_orm.or)((0, drizzle_orm.eq)(require_sqlite_schema.issueRelations.sourceIssueId, issueId), (0, drizzle_orm.eq)(require_sqlite_schema.issueRelations.targetIssueId, issueId))).all(),
284
+ findById: (relationId) => resolveDatabase().select().from(require_sqlite_schema.issueRelations).where((0, drizzle_orm.eq)(require_sqlite_schema.issueRelations.id, relationId)).get() ?? null,
285
+ findMatching: (edge) => resolveDatabase().select().from(require_sqlite_schema.issueRelations).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issueRelations.type, edge.type), edge.type === "relates_to" ? (0, drizzle_orm.or)((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issueRelations.sourceIssueId, edge.sourceIssueId), (0, drizzle_orm.eq)(require_sqlite_schema.issueRelations.targetIssueId, edge.targetIssueId)), (0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issueRelations.sourceIssueId, edge.targetIssueId), (0, drizzle_orm.eq)(require_sqlite_schema.issueRelations.targetIssueId, edge.sourceIssueId))) : (0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issueRelations.sourceIssueId, edge.sourceIssueId), (0, drizzle_orm.eq)(require_sqlite_schema.issueRelations.targetIssueId, edge.targetIssueId)))).get() ?? null,
286
+ create: (values) => resolveDatabase().insert(require_sqlite_schema.issueRelations).values(values).returning().get(),
287
+ delete: (relationId) => {
288
+ resolveDatabase().delete(require_sqlite_schema.issueRelations).where((0, drizzle_orm.eq)(require_sqlite_schema.issueRelations.id, relationId)).run();
289
+ },
290
+ deleteByIssue: (issueId) => {
291
+ resolveDatabase().delete(require_sqlite_schema.issueRelations).where((0, drizzle_orm.or)((0, drizzle_orm.eq)(require_sqlite_schema.issueRelations.sourceIssueId, issueId), (0, drizzle_orm.eq)(require_sqlite_schema.issueRelations.targetIssueId, issueId))).run();
292
+ }
293
+ },
294
+ contextRefs: { replace: (issueId, contextRefs, updatedAt) => {
295
+ resolveDatabase().update(require_sqlite_schema.issues).set({
296
+ contextRefs,
297
+ updatedAt
298
+ }).where((0, drizzle_orm.eq)(require_sqlite_schema.issues.id, issueId)).run();
299
+ } },
300
+ fieldChanges: {
301
+ listByIssue: (issueId) => resolveDatabase().select().from(require_sqlite_schema.issueFieldChanges).where((0, drizzle_orm.eq)(require_sqlite_schema.issueFieldChanges.issueId, issueId)).orderBy((0, drizzle_orm.asc)(require_sqlite_schema.issueFieldChanges.createdAt), FIELD_CHANGE_INSERTION_ORDER).all(),
302
+ listByIssueField: (issueId, field) => resolveDatabase().select().from(require_sqlite_schema.issueFieldChanges).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issueFieldChanges.issueId, issueId), (0, drizzle_orm.eq)(require_sqlite_schema.issueFieldChanges.field, field))).orderBy((0, drizzle_orm.asc)(require_sqlite_schema.issueFieldChanges.createdAt), FIELD_CHANGE_INSERTION_ORDER).all(),
303
+ create: (values) => {
304
+ resolveDatabase().insert(require_sqlite_schema.issueFieldChanges).values(values).run();
305
+ }
306
+ },
307
+ pullRequests: {
308
+ listByIssue: (issueId) => resolveDatabase().select().from(require_sqlite_schema.issuePullRequests).where((0, drizzle_orm.eq)(require_sqlite_schema.issuePullRequests.issueId, issueId)).orderBy((0, drizzle_orm.asc)(require_sqlite_schema.issuePullRequests.createdAt), PULL_REQUEST_INSERTION_ORDER).all(),
309
+ listLinked: () => resolveDatabase().select({
310
+ ...(0, drizzle_orm.getTableColumns)(require_sqlite_schema.issuePullRequests),
311
+ workspaceId: require_sqlite_schema.issues.workspaceId
312
+ }).from(require_sqlite_schema.issuePullRequests).innerJoin(require_sqlite_schema.issues, (0, drizzle_orm.eq)(require_sqlite_schema.issues.id, require_sqlite_schema.issuePullRequests.issueId)).orderBy((0, drizzle_orm.asc)(require_sqlite_schema.issuePullRequests.createdAt), PULL_REQUEST_INSERTION_ORDER).all(),
313
+ link: (values) => {
314
+ const database = resolveDatabase();
315
+ const inserted = database.insert(require_sqlite_schema.issuePullRequests).values(values).onConflictDoNothing().returning().get();
316
+ if (inserted) return inserted;
317
+ const existing = database.select().from(require_sqlite_schema.issuePullRequests).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issuePullRequests.issueId, values.issueId), (0, drizzle_orm.eq)(require_sqlite_schema.issuePullRequests.owner, values.owner), (0, drizzle_orm.eq)(require_sqlite_schema.issuePullRequests.repo, values.repo), (0, drizzle_orm.eq)(require_sqlite_schema.issuePullRequests.number, values.number))).get();
318
+ if (!existing) throw new Error(`pull request link for ${values.issueId} was refused, and the card names no ${values.owner}/${values.repo}#${values.number}`);
319
+ return existing;
320
+ },
321
+ unlink: (issueId, linkId) => resolveDatabase().delete(require_sqlite_schema.issuePullRequests).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issuePullRequests.id, linkId), (0, drizzle_orm.eq)(require_sqlite_schema.issuePullRequests.issueId, issueId))).returning().get() ?? null
322
+ },
323
+ meta: {
324
+ read: (key) => resolveDatabase().select().from(require_sqlite_schema.kanbanMeta).where((0, drizzle_orm.eq)(require_sqlite_schema.kanbanMeta.key, key)).get() ?? null,
325
+ bumpRevision: (key, updatedAt) => {
326
+ resolveDatabase().insert(require_sqlite_schema.kanbanMeta).values({
327
+ key,
328
+ revision: 1,
329
+ updatedAt
330
+ }).onConflictDoUpdate({
331
+ target: require_sqlite_schema.kanbanMeta.key,
332
+ set: {
333
+ revision: drizzle_orm.sql`${require_sqlite_schema.kanbanMeta.revision} + 1`,
334
+ updatedAt
335
+ }
336
+ }).run();
337
+ }
338
+ }
339
+ };
340
+ }
341
+ /**
342
+ * `BoardStore` backed by SQLite: every method resolves immediately, because the
343
+ * core underneath it is synchronous.
344
+ *
345
+ * `database` is a function rather than a handle so the store follows whatever
346
+ * the host currently has open — a server passes its single connection
347
+ * accessor, a standalone reader passes its own.
348
+ */
349
+ function createSqliteBoardStore(options) {
350
+ const core = createSqliteBoardStoreCore(options.database);
351
+ const store = {
352
+ statuses: {
353
+ listByWorkspace: async (workspaceId) => core.statuses.listByWorkspace(workspaceId),
354
+ listAllInBoardOrder: async () => core.statuses.listAllInBoardOrder(),
355
+ listAll: async () => core.statuses.listAll(),
356
+ findById: async (statusId) => core.statuses.findById(statusId),
357
+ findInWorkspace: async (workspaceId, statusId) => core.statuses.findInWorkspace(workspaceId, statusId),
358
+ countByWorkspace: async (workspaceId) => core.statuses.countByWorkspace(workspaceId),
359
+ create: async (values) => core.statuses.create(values),
360
+ update: async (statusId, patch) => core.statuses.update(statusId, patch),
361
+ delete: async (statusId) => core.statuses.delete(statusId)
362
+ },
363
+ milestones: {
364
+ listNewestFirst: async (workspaceId) => core.milestones.listNewestFirst(workspaceId),
365
+ listByWorkspace: async (workspaceId) => core.milestones.listByWorkspace(workspaceId),
366
+ listAll: async () => core.milestones.listAll(),
367
+ findById: async (milestoneId) => core.milestones.findById(milestoneId),
368
+ findInWorkspace: async (workspaceId, milestoneId) => core.milestones.findInWorkspace(workspaceId, milestoneId),
369
+ create: async (values) => core.milestones.create(values),
370
+ update: async (milestoneId, patch) => core.milestones.update(milestoneId, patch),
371
+ delete: async (milestoneId) => core.milestones.delete(milestoneId)
372
+ },
373
+ issues: {
374
+ listInBoardOrder: async (workspaceId) => core.issues.listInBoardOrder(workspaceId),
375
+ listNewestFirst: async () => core.issues.listNewestFirst(),
376
+ listAll: async () => core.issues.listAll(),
377
+ listByWorkspace: async (workspaceId) => core.issues.listByWorkspace(workspaceId),
378
+ listByIds: async (issueIds) => core.issues.listByIds(issueIds),
379
+ findById: async (issueId) => core.issues.findById(issueId),
380
+ findInWorkspace: async (workspaceId, issueId) => core.issues.findInWorkspace(workspaceId, issueId),
381
+ findByNumber: async (workspaceId, number) => core.issues.findByNumber(workspaceId, number),
382
+ maxNumber: async (workspaceId) => core.issues.maxNumber(workspaceId),
383
+ maxOrder: async (workspaceId) => core.issues.maxOrder(workspaceId),
384
+ create: async (values) => core.issues.create(values),
385
+ update: async (issueId, patch) => core.issues.update(issueId, patch),
386
+ updateMany: async (issueIds, patch) => core.issues.updateMany(issueIds, patch),
387
+ clearMilestoneReferences: async (milestoneId, updatedAt) => core.issues.clearMilestoneReferences(milestoneId, updatedAt),
388
+ clearParentReferences: async (parentIssueId, updatedAt) => core.issues.clearParentReferences(parentIssueId, updatedAt),
389
+ clearDelegateReferences: async (delegate, updatedAt) => core.issues.clearDelegateReferences(delegate, updatedAt),
390
+ delete: async (issueId) => core.issues.delete(issueId)
391
+ },
392
+ comments: {
393
+ listByIssue: async (issueId) => core.comments.listByIssue(issueId),
394
+ findById: async (commentId) => core.comments.findById(commentId),
395
+ findByDedupeKey: async (issueId, dedupeKey) => core.comments.findByDedupeKey(issueId, dedupeKey),
396
+ listByDedupeKeyPrefix: async (issueId, prefix) => core.comments.listByDedupeKeyPrefix(issueId, prefix),
397
+ create: async (values) => core.comments.create(values),
398
+ createOnce: async (values) => core.comments.createOnce(values),
399
+ delete: async (commentId) => core.comments.delete(commentId)
400
+ },
401
+ runs: {
402
+ listByIssue: async (issueId) => core.runs.listByIssue(issueId),
403
+ listRunning: async (issueId) => core.runs.listRunning(issueId),
404
+ listRunningWithSession: async () => core.runs.listRunningWithSession(),
405
+ findById: async (runId) => core.runs.findById(runId),
406
+ findByChatSessionId: async (chatSessionId) => core.runs.findByChatSessionId(chatSessionId),
407
+ countByIssue: async (issueId) => core.runs.countByIssue(issueId),
408
+ create: async (values) => core.runs.create(values),
409
+ update: async (runId, patch) => core.runs.update(runId, patch),
410
+ project: async (scope) => core.runs.project(scope)
411
+ },
412
+ relations: {
413
+ listByIssue: async (issueId) => core.relations.listByIssue(issueId),
414
+ findById: async (relationId) => core.relations.findById(relationId),
415
+ findMatching: async (edge) => core.relations.findMatching(edge),
416
+ create: async (values) => core.relations.create(values),
417
+ delete: async (relationId) => core.relations.delete(relationId),
418
+ deleteByIssue: async (issueId) => core.relations.deleteByIssue(issueId)
419
+ },
420
+ contextRefs: { replace: async (issueId, contextRefs, updatedAt) => core.contextRefs.replace(issueId, contextRefs, updatedAt) },
421
+ fieldChanges: {
422
+ listByIssue: async (issueId) => core.fieldChanges.listByIssue(issueId),
423
+ listByIssueField: async (issueId, field) => core.fieldChanges.listByIssueField(issueId, field),
424
+ create: async (values) => core.fieldChanges.create(values)
425
+ },
426
+ pullRequests: {
427
+ listByIssue: async (issueId) => core.pullRequests.listByIssue(issueId),
428
+ listLinked: async () => core.pullRequests.listLinked(),
429
+ link: async (values) => core.pullRequests.link(values),
430
+ unlink: async (issueId, linkId) => core.pullRequests.unlink(issueId, linkId)
431
+ },
432
+ meta: {
433
+ read: async (key) => core.meta.read(key),
434
+ bumpRevision: async (key, updatedAt) => core.meta.bumpRevision(key, updatedAt)
435
+ },
436
+ /**
437
+ * Run `fn` inside one SQLite transaction, through the driver-level helper in
438
+ * `./transaction.ts`.
439
+ *
440
+ * The caller rule lives there in full; in short: `fn` must be the only
441
+ * database-touching async chain in flight, and inside it only await
442
+ * operations that resolve without I/O — store methods, synchronous database
443
+ * writes.
444
+ */
445
+ transaction: async (fn, transactionOptions) => {
446
+ return await runSqliteTransaction(options.database(), () => fn(store), transactionOptions);
447
+ }
448
+ };
449
+ return store;
450
+ }
451
+ //#endregion
452
+ //#region src/sqlite/open-database.ts
453
+ const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
454
+ /** The tables a board file must already carry for this package to read it. */
455
+ const REQUIRED_BOARD_TABLES = ["issue_runs", "kanban_meta"];
456
+ /** What a caller is told when the native module a board file needs is not installed. */
457
+ const BETTER_SQLITE3_MISSING_MESSAGE = "Install better-sqlite3 to use a board file";
458
+ /**
459
+ * `better-sqlite3` is an optional peer dependency (ruling 5): a Postgres-only
460
+ * or capabilities-only install carries no native module, and importing
461
+ * `kanbo/sqlite` for a type or for `runSqliteTransaction` should not
462
+ * demand one either. So the require happens here, the one place a board file
463
+ * is actually opened, rather than at the top of this module.
464
+ */
465
+ function loadBetterSqlite3() {
466
+ try {
467
+ return require$1("better-sqlite3");
468
+ } catch (error) {
469
+ throw new Error(BETTER_SQLITE3_MISSING_MESSAGE, { cause: error });
470
+ }
471
+ }
472
+ /**
473
+ * Drizzle's own better-sqlite3 driver, and why it is not imported at the top of
474
+ * this file like every other import in the package.
475
+ *
476
+ * `drizzle-orm/better-sqlite3` imports the native module eagerly — it has an
477
+ * overload where `drizzle()` constructs the connection itself — so a static
478
+ * import of it makes `better-sqlite3` required by everything that can reach
479
+ * this module, `kanbo capabilities` and `kanbo/mcp` included, and the
480
+ * friendly message above becomes unreachable. A dynamic import keeps the whole
481
+ * chain behind the one call that actually opens a file, which is what makes the
482
+ * optional peer honest. `createRequire` would be the smaller change and is the
483
+ * wrong one: it would load drizzle's CJS copy beside the ESM one this package
484
+ * already holds, and it would resolve from `node_modules`, which the packaged
485
+ * `dist/cli.cjs` deliberately does not have beside it.
486
+ *
487
+ * The module is remembered after the first load, so a command that opens two
488
+ * board files pays for it once.
489
+ */
490
+ let sqliteDriver = null;
491
+ async function loadSqliteDriver() {
492
+ sqliteDriver ??= await import("drizzle-orm/better-sqlite3");
493
+ return sqliteDriver;
494
+ }
495
+ /**
496
+ * Open a board file with the runtime pragmas a long-running server sets on its
497
+ * own database, so a board read from a terminal behaves the same as one read
498
+ * from inside an app. Migrations are not applied here: the file's owner applies
499
+ * them (`kanbo init --file`, `kanbo migrate`, or the app the database belongs to).
500
+ *
501
+ * Asynchronous because the driver beneath it is loaded on demand — see
502
+ * `loadSqliteDriver`. The native module is asked for first, so an install
503
+ * without it hears the sentence above rather than a module-resolution stack
504
+ * from inside drizzle.
505
+ */
506
+ async function openBoardDatabase(path) {
507
+ const Database = loadBetterSqlite3();
508
+ const { drizzle } = await loadSqliteDriver();
509
+ const connection = new Database(path);
510
+ connection.pragma("foreign_keys = ON");
511
+ connection.pragma("journal_mode = WAL");
512
+ connection.pragma("busy_timeout = 5000");
513
+ connection.pragma("synchronous = NORMAL");
514
+ return {
515
+ database: drizzle(connection, { schema: require_sqlite_schema.boardSqliteSchema }),
516
+ connection,
517
+ close: () => connection.close()
518
+ };
519
+ }
520
+ /**
521
+ * Every column this build expects to find, read off the schema itself — the
522
+ * same idea as the Postgres guard's, so a column a later migration added
523
+ * (`issue_milestones.start_date`) is looked for without being listed again.
524
+ */
525
+ function expectedColumns() {
526
+ return Object.values(require_sqlite_schema.boardSqliteSchema).flatMap((table) => {
527
+ const config = (0, drizzle_orm_sqlite_core.getTableConfig)(table);
528
+ return config.columns.map((column) => `${config.name}.${column.name}`);
529
+ });
530
+ }
531
+ /**
532
+ * Refuse a board file this build cannot speak for, before any statement runs
533
+ * against it: one with no board in it (`missingTables`), one whose board a
534
+ * later migration has since added a table to (`missingAddedTables`) or a
535
+ * column to (`missingColumns`), and one stamped with another epoch. An app
536
+ * that migrates its own database on start need not call this; it is the guard a
537
+ * standalone reader puts in front of a file it did not create.
538
+ */
539
+ function assertBoardSchema(database) {
540
+ const present = new Set(database.all(drizzle_orm.sql`select name from sqlite_master where type = 'table'`).map((row) => row.name));
541
+ const missing = REQUIRED_BOARD_TABLES.filter((table) => !present.has(table));
542
+ if (missing.length > 0) throw new require_migrations_path.BoardError("board_schema_outdated", { missingTables: missing });
543
+ const missingAddedTables = require_migrations_path.BOARD_TABLES_ADDED_WITHIN_EPOCH.filter((table) => !present.has(table));
544
+ if (missingAddedTables.length > 0) throw new require_migrations_path.BoardError("board_schema_outdated", { missingAddedTables });
545
+ const columns = new Set(database.all(drizzle_orm.sql`
546
+ select m.name as tableName, p.name as columnName
547
+ from sqlite_master m join pragma_table_info(m.name) p
548
+ where m.type = 'table'`).map((row) => `${row.tableName}.${row.columnName}`));
549
+ const missingColumns = expectedColumns().filter((column) => !columns.has(column));
550
+ if (missingColumns.length > 0) throw new require_migrations_path.BoardError("board_schema_outdated", { missingColumns });
551
+ const row = database.get(drizzle_orm.sql`select revision from kanban_meta where key = 'schema_epoch'`);
552
+ if (row?.revision !== 1) throw new require_migrations_path.BoardError("board_schema_outdated", {
553
+ expected: 1,
554
+ found: row?.revision ?? null
555
+ });
556
+ }
557
+ /**
558
+ * Which of the two ever wrote this file's board, going by the `kanban_meta`
559
+ * marker `kanbo init --file` leaves behind (ruling 5-4).
560
+ *
561
+ * Absent reads as a host database — every file this package has never
562
+ * touched, and a file this build cannot make sense of at all: `assertBoardSchema` is what says a board is missing or too old,
563
+ * not this, so a table or a row that is not there yet is read as "not this
564
+ * package's file" rather than an error of its own.
565
+ */
566
+ function readBoardFileOwner(database) {
567
+ try {
568
+ return database.get(drizzle_orm.sql`select key from kanban_meta where key = 'file_owner'`) ? "kanbo" : "host";
569
+ } catch {
570
+ return "host";
571
+ }
572
+ }
573
+ /**
574
+ * Leave the marker that tells every later reader this file is this package's
575
+ * own, not a host app's. `kanbo init --file` calls this once, right after
576
+ * migrating; `insert or ignore` is what makes calling it again on the same
577
+ * file a no-op rather than a conflict.
578
+ */
579
+ function markBoardFileOwnedByKanbo(database) {
580
+ database.run(drizzle_orm.sql`insert or ignore into kanban_meta (key, revision) values ('file_owner', 1)`);
581
+ }
582
+ /**
583
+ * Look at a file on a connection of its own and hand it back closed, so a
584
+ * caller can decide what to do with it before opening it for real.
585
+ *
586
+ * Two callers, two different decisions off the same two facts: the command line
587
+ * asks whose file it is to know whether a host app's `workspaces` row may be
588
+ * looked up in it, and a host app asks before it writes a board into it, because
589
+ * a file with tables that are not this package's is somebody else's database
590
+ * and migrating it would contaminate it (ruling 5-8).
591
+ *
592
+ * A path that is not a SQLite database at all — a README, a half-downloaded
593
+ * file, a directory — throws, and that is deliberately not the same answer as
594
+ * "a database with somebody else's tables in it": one is a mistake about which
595
+ * path to use and the other is a mistake about which file is a board. No pragma
596
+ * is set and nothing is written, so a file that turns out not to be ours is
597
+ * left exactly as it was found, journal mode included.
598
+ */
599
+ function readBoardFileContents(path) {
600
+ const connection = new (loadBetterSqlite3())(path, { fileMustExist: true });
601
+ try {
602
+ if (connection.prepare("select count(*) as count from sqlite_master where type = 'table'").get().count === 0) return {
603
+ empty: true,
604
+ owner: "host"
605
+ };
606
+ return {
607
+ empty: false,
608
+ owner: readMarkedOwner(connection)
609
+ };
610
+ } finally {
611
+ connection.close();
612
+ }
613
+ }
614
+ /** The marker, read straight off the driver: the same question `readBoardFileOwner` asks a drizzle handle. */
615
+ function readMarkedOwner(connection) {
616
+ try {
617
+ return connection.prepare("select key from kanban_meta where key = 'file_owner'").get() ? "kanbo" : "host";
618
+ } catch {
619
+ return "host";
620
+ }
621
+ }
622
+ //#endregion
623
+ //#region src/sqlite/migrate.ts
624
+ /** The folder's name, wherever it has been put. */
625
+ const MIGRATIONS_DIRECTORY = "drizzle-sqlite";
626
+ /**
627
+ * The board's SQLite migrations for the file that is running right now — the
628
+ * ones that build a board *file of this package's own*. The two layouts this
629
+ * has to survive are `src/migrations-path.ts`'s subject.
630
+ */
631
+ function getBoardSqliteMigrationsPath() {
632
+ const here = (0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
633
+ return require_migrations_path.resolveBoardMigrationsPath(here, MIGRATIONS_DIRECTORY, require_migrations_path.hasMigrationJournal);
634
+ }
635
+ /**
636
+ * Create or update a board file this package owns.
637
+ *
638
+ * A host app's own database is not this: there the board is nine tables among
639
+ * the app's own, and the app's migration runner migrates the whole file — which
640
+ * may take two passes, because a migration that rebuilds a table has to drop
641
+ * it with foreign keys off. Nothing here rebuilds anything — `drizzle-sqlite/` starts at the
642
+ * board as it is now and will only ever be appended to — so drizzle's own
643
+ * migrator is the whole of what this needs, and a file that has been migrated
644
+ * before is a no-op by `__drizzle_migrations`.
645
+ *
646
+ * What the file holds afterwards is the board and nothing else: no
647
+ * `workspaces`, no `agents`, no `provider_targets`. Every reference out of the
648
+ * board is a plain text column for exactly that reason (ruling 5-1).
649
+ */
650
+ function migrateBoardFile(database) {
651
+ (0, drizzle_orm_better_sqlite3_migrator.migrate)(database, { migrationsFolder: getBoardSqliteMigrationsPath() });
652
+ }
653
+ //#endregion
654
+ Object.defineProperty(exports, "assertBoardSchema", {
655
+ enumerable: true,
656
+ get: function() {
657
+ return assertBoardSchema;
658
+ }
659
+ });
660
+ Object.defineProperty(exports, "createSqliteBoardStore", {
661
+ enumerable: true,
662
+ get: function() {
663
+ return createSqliteBoardStore;
664
+ }
665
+ });
666
+ Object.defineProperty(exports, "getBoardSqliteMigrationsPath", {
667
+ enumerable: true,
668
+ get: function() {
669
+ return getBoardSqliteMigrationsPath;
670
+ }
671
+ });
672
+ Object.defineProperty(exports, "markBoardFileOwnedByKanbo", {
673
+ enumerable: true,
674
+ get: function() {
675
+ return markBoardFileOwnedByKanbo;
676
+ }
677
+ });
678
+ Object.defineProperty(exports, "migrateBoardFile", {
679
+ enumerable: true,
680
+ get: function() {
681
+ return migrateBoardFile;
682
+ }
683
+ });
684
+ Object.defineProperty(exports, "openBoardDatabase", {
685
+ enumerable: true,
686
+ get: function() {
687
+ return openBoardDatabase;
688
+ }
689
+ });
690
+ Object.defineProperty(exports, "readBoardFileContents", {
691
+ enumerable: true,
692
+ get: function() {
693
+ return readBoardFileContents;
694
+ }
695
+ });
696
+ Object.defineProperty(exports, "readBoardFileOwner", {
697
+ enumerable: true,
698
+ get: function() {
699
+ return readBoardFileOwner;
700
+ }
701
+ });
702
+ Object.defineProperty(exports, "runSqliteTransaction", {
703
+ enumerable: true,
704
+ get: function() {
705
+ return runSqliteTransaction;
706
+ }
707
+ });
708
+
709
+ //# sourceMappingURL=migrate-DnMjTCvu.cjs.map