@kaban-board/core 0.2.11 → 0.2.13
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/db/bun-adapter.d.ts +3 -0
- package/dist/db/bun-adapter.d.ts.map +1 -0
- package/dist/db/bun-only.d.ts +7 -0
- package/dist/db/bun-only.d.ts.map +1 -0
- package/dist/db/index.d.ts +2 -27
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/libsql-adapter.d.ts +3 -0
- package/dist/db/libsql-adapter.d.ts.map +1 -0
- package/dist/db/migrate.d.ts +1 -1
- package/dist/db/migrator.d.ts +1 -1
- package/dist/db/types.d.ts +17 -0
- package/dist/db/types.d.ts.map +1 -0
- package/dist/db/utils.d.ts +3 -0
- package/dist/db/utils.d.ts.map +1 -0
- package/dist/index-bun.d.ts +8 -0
- package/dist/index-bun.d.ts.map +1 -0
- package/dist/index-bun.js +1207 -0
- package/dist/index.js +215 -152
- package/dist/services/board.d.ts +1 -1
- package/dist/services/board.d.ts.map +1 -1
- package/dist/services/task.d.ts +1 -1
- package/dist/services/task.d.ts.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,1207 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __export = (target, all) => {
|
|
19
|
+
for (var name in all)
|
|
20
|
+
__defProp(target, name, {
|
|
21
|
+
get: all[name],
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
set: (newValue) => all[name] = () => newValue
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
28
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
29
|
+
|
|
30
|
+
// src/types.ts
|
|
31
|
+
var DEFAULT_CONFIG, KabanError, ExitCode;
|
|
32
|
+
var init_types = __esm(() => {
|
|
33
|
+
DEFAULT_CONFIG = {
|
|
34
|
+
board: {
|
|
35
|
+
name: "Kaban Board"
|
|
36
|
+
},
|
|
37
|
+
columns: [
|
|
38
|
+
{ id: "backlog", name: "Backlog" },
|
|
39
|
+
{ id: "todo", name: "Todo" },
|
|
40
|
+
{ id: "in_progress", name: "In Progress", wipLimit: 3 },
|
|
41
|
+
{ id: "review", name: "Review", wipLimit: 2 },
|
|
42
|
+
{ id: "done", name: "Done", isTerminal: true }
|
|
43
|
+
],
|
|
44
|
+
defaults: {
|
|
45
|
+
column: "todo",
|
|
46
|
+
agent: "user"
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
KabanError = class KabanError extends Error {
|
|
50
|
+
code;
|
|
51
|
+
constructor(message, code) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.code = code;
|
|
54
|
+
this.name = "KabanError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
ExitCode = {
|
|
58
|
+
SUCCESS: 0,
|
|
59
|
+
GENERAL_ERROR: 1,
|
|
60
|
+
NOT_FOUND: 2,
|
|
61
|
+
CONFLICT: 3,
|
|
62
|
+
VALIDATION: 4
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// src/db/utils.ts
|
|
67
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
68
|
+
import { dirname } from "node:path";
|
|
69
|
+
import { fileURLToPath } from "node:url";
|
|
70
|
+
function fileUrlToPath(urlOrPath) {
|
|
71
|
+
if (!urlOrPath.startsWith("file:"))
|
|
72
|
+
return urlOrPath;
|
|
73
|
+
if (urlOrPath.startsWith("file:///") || urlOrPath.startsWith("file://localhost/")) {
|
|
74
|
+
return fileURLToPath(urlOrPath);
|
|
75
|
+
}
|
|
76
|
+
return urlOrPath.replace(/^file:/, "");
|
|
77
|
+
}
|
|
78
|
+
function ensureDbDir(filePath) {
|
|
79
|
+
if (filePath === ":memory:" || filePath.trim() === "")
|
|
80
|
+
return;
|
|
81
|
+
const dir = dirname(filePath);
|
|
82
|
+
if (!existsSync(dir)) {
|
|
83
|
+
mkdirSync(dir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
var init_utils = () => {};
|
|
87
|
+
|
|
88
|
+
// src/db/schema.ts
|
|
89
|
+
var exports_schema = {};
|
|
90
|
+
__export(exports_schema, {
|
|
91
|
+
undoLog: () => undoLog,
|
|
92
|
+
tasks: () => tasks,
|
|
93
|
+
columns: () => columns,
|
|
94
|
+
boards: () => boards
|
|
95
|
+
});
|
|
96
|
+
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
97
|
+
var boards, columns, tasks, undoLog;
|
|
98
|
+
var init_schema = __esm(() => {
|
|
99
|
+
boards = sqliteTable("boards", {
|
|
100
|
+
id: text("id").primaryKey(),
|
|
101
|
+
name: text("name").notNull(),
|
|
102
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
103
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull()
|
|
104
|
+
});
|
|
105
|
+
columns = sqliteTable("columns", {
|
|
106
|
+
id: text("id").primaryKey(),
|
|
107
|
+
boardId: text("board_id").notNull().references(() => boards.id),
|
|
108
|
+
name: text("name").notNull(),
|
|
109
|
+
position: integer("position").notNull(),
|
|
110
|
+
wipLimit: integer("wip_limit"),
|
|
111
|
+
isTerminal: integer("is_terminal", { mode: "boolean" }).notNull().default(false)
|
|
112
|
+
});
|
|
113
|
+
tasks = sqliteTable("tasks", {
|
|
114
|
+
id: text("id").primaryKey(),
|
|
115
|
+
title: text("title").notNull(),
|
|
116
|
+
description: text("description"),
|
|
117
|
+
columnId: text("column_id").notNull().references(() => columns.id),
|
|
118
|
+
position: integer("position").notNull(),
|
|
119
|
+
createdBy: text("created_by").notNull(),
|
|
120
|
+
assignedTo: text("assigned_to"),
|
|
121
|
+
parentId: text("parent_id").references(() => tasks.id),
|
|
122
|
+
dependsOn: text("depends_on", { mode: "json" }).$type().notNull().default([]),
|
|
123
|
+
files: text("files", { mode: "json" }).$type().notNull().default([]),
|
|
124
|
+
labels: text("labels", { mode: "json" }).$type().notNull().default([]),
|
|
125
|
+
blockedReason: text("blocked_reason"),
|
|
126
|
+
version: integer("version").notNull().default(1),
|
|
127
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
128
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
129
|
+
startedAt: integer("started_at", { mode: "timestamp" }),
|
|
130
|
+
completedAt: integer("completed_at", { mode: "timestamp" }),
|
|
131
|
+
archived: integer("archived", { mode: "boolean" }).notNull().default(false),
|
|
132
|
+
archivedAt: integer("archived_at", { mode: "timestamp" })
|
|
133
|
+
});
|
|
134
|
+
undoLog = sqliteTable("undo_log", {
|
|
135
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
136
|
+
operation: text("operation").notNull(),
|
|
137
|
+
data: text("data", { mode: "json" }).notNull(),
|
|
138
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull()
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// drizzle/meta/_journal.json
|
|
143
|
+
var _journal_default;
|
|
144
|
+
var init__journal = __esm(() => {
|
|
145
|
+
_journal_default = {
|
|
146
|
+
version: "7",
|
|
147
|
+
dialect: "sqlite",
|
|
148
|
+
entries: [
|
|
149
|
+
{
|
|
150
|
+
idx: 0,
|
|
151
|
+
version: "6",
|
|
152
|
+
when: 1769351194943,
|
|
153
|
+
tag: "0000_init",
|
|
154
|
+
breakpoints: true
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
idx: 1,
|
|
158
|
+
version: "6",
|
|
159
|
+
when: 1769351198000,
|
|
160
|
+
tag: "0001_add_archived",
|
|
161
|
+
breakpoints: true
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
idx: 2,
|
|
165
|
+
version: "6",
|
|
166
|
+
when: 1769351200000,
|
|
167
|
+
tag: "0002_add_fts5",
|
|
168
|
+
breakpoints: true
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// drizzle/0000_init.sql
|
|
175
|
+
var _0000_init_default = "CREATE TABLE `boards` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`name` text NOT NULL,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE TABLE `columns` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`board_id` text NOT NULL,\n\t`name` text NOT NULL,\n\t`position` integer NOT NULL,\n\t`wip_limit` integer,\n\t`is_terminal` integer DEFAULT false NOT NULL,\n\tFOREIGN KEY (`board_id`) REFERENCES `boards`(`id`) ON UPDATE no action ON DELETE no action\n);\n--> statement-breakpoint\nCREATE TABLE `tasks` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`title` text NOT NULL,\n\t`description` text,\n\t`column_id` text NOT NULL,\n\t`position` integer NOT NULL,\n\t`created_by` text NOT NULL,\n\t`assigned_to` text,\n\t`parent_id` text,\n\t`depends_on` text DEFAULT '[]' NOT NULL,\n\t`files` text DEFAULT '[]' NOT NULL,\n\t`labels` text DEFAULT '[]' NOT NULL,\n\t`blocked_reason` text,\n\t`version` integer DEFAULT 1 NOT NULL,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\t`started_at` integer,\n\t`completed_at` integer,\n\t`archived` integer DEFAULT false NOT NULL,\n\t`archived_at` integer,\n\tFOREIGN KEY (`column_id`) REFERENCES `columns`(`id`) ON UPDATE no action ON DELETE no action,\n\tFOREIGN KEY (`parent_id`) REFERENCES `tasks`(`id`) ON UPDATE no action ON DELETE no action\n);\n--> statement-breakpoint\nCREATE TABLE `undo_log` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,\n\t`operation` text NOT NULL,\n\t`data` text NOT NULL,\n\t`created_at` integer NOT NULL\n);\n";
|
|
176
|
+
var init_0000_init = () => {};
|
|
177
|
+
|
|
178
|
+
// drizzle/0001_add_archived.sql
|
|
179
|
+
var _0001_add_archived_default = `-- Add archived columns for task archiving feature
|
|
180
|
+
-- SQLite doesn't support ADD COLUMN IF NOT EXISTS, so we check manually
|
|
181
|
+
-- These columns may already exist in newer databases
|
|
182
|
+
|
|
183
|
+
ALTER TABLE tasks ADD COLUMN archived INTEGER DEFAULT false NOT NULL;
|
|
184
|
+
--> statement-breakpoint
|
|
185
|
+
ALTER TABLE tasks ADD COLUMN archived_at INTEGER;
|
|
186
|
+
`;
|
|
187
|
+
var init_0001_add_archived = () => {};
|
|
188
|
+
|
|
189
|
+
// drizzle/0002_add_fts5.sql
|
|
190
|
+
var _0002_add_fts5_default = `-- Add index for archived column
|
|
191
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_archived ON tasks(archived);
|
|
192
|
+
--> statement-breakpoint
|
|
193
|
+
-- Create FTS5 virtual table for full-text search
|
|
194
|
+
-- Uses unicode61 tokenizer with remove_diacritics for proper Russian language support
|
|
195
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
|
|
196
|
+
id, title, description,
|
|
197
|
+
content='tasks',
|
|
198
|
+
content_rowid='rowid',
|
|
199
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
200
|
+
);
|
|
201
|
+
--> statement-breakpoint
|
|
202
|
+
-- Create trigger to keep FTS in sync on INSERT
|
|
203
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_insert AFTER INSERT ON tasks BEGIN
|
|
204
|
+
INSERT INTO tasks_fts(rowid, id, title, description)
|
|
205
|
+
VALUES (NEW.rowid, NEW.id, NEW.title, COALESCE(NEW.description, ''));
|
|
206
|
+
END;
|
|
207
|
+
--> statement-breakpoint
|
|
208
|
+
-- Create trigger to keep FTS in sync on DELETE
|
|
209
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_delete AFTER DELETE ON tasks BEGIN
|
|
210
|
+
INSERT INTO tasks_fts(tasks_fts, rowid, id, title, description)
|
|
211
|
+
VALUES('delete', OLD.rowid, OLD.id, OLD.title, COALESCE(OLD.description, ''));
|
|
212
|
+
END;
|
|
213
|
+
--> statement-breakpoint
|
|
214
|
+
-- Create trigger to keep FTS in sync on UPDATE
|
|
215
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_update AFTER UPDATE ON tasks BEGIN
|
|
216
|
+
INSERT INTO tasks_fts(tasks_fts, rowid, id, title, description)
|
|
217
|
+
VALUES('delete', OLD.rowid, OLD.id, OLD.title, COALESCE(OLD.description, ''));
|
|
218
|
+
INSERT INTO tasks_fts(rowid, id, title, description)
|
|
219
|
+
VALUES (NEW.rowid, NEW.id, NEW.title, COALESCE(NEW.description, ''));
|
|
220
|
+
END;
|
|
221
|
+
--> statement-breakpoint
|
|
222
|
+
-- Populate FTS with existing data (only if FTS table is empty)
|
|
223
|
+
INSERT INTO tasks_fts(rowid, id, title, description)
|
|
224
|
+
SELECT rowid, id, title, COALESCE(description, '') FROM tasks
|
|
225
|
+
WHERE NOT EXISTS (SELECT 1 FROM tasks_fts LIMIT 1);
|
|
226
|
+
`;
|
|
227
|
+
var init_0002_add_fts5 = () => {};
|
|
228
|
+
|
|
229
|
+
// src/db/migrator.ts
|
|
230
|
+
var exports_migrator = {};
|
|
231
|
+
__export(exports_migrator, {
|
|
232
|
+
runMigrations: () => runMigrations
|
|
233
|
+
});
|
|
234
|
+
import { readFileSync } from "node:fs";
|
|
235
|
+
import { dirname as dirname2, isAbsolute, join } from "node:path";
|
|
236
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
237
|
+
function resolveSqlContent(sqlOrPath) {
|
|
238
|
+
if (sqlOrPath.includes("CREATE") || sqlOrPath.includes("INSERT") || sqlOrPath.includes("--")) {
|
|
239
|
+
return sqlOrPath;
|
|
240
|
+
}
|
|
241
|
+
let filePath = sqlOrPath;
|
|
242
|
+
if (filePath.startsWith("./")) {
|
|
243
|
+
filePath = join(drizzleDir, filePath.replace("./", "").replace(/-[a-z0-9]+\.sql$/, ".sql"));
|
|
244
|
+
}
|
|
245
|
+
if (!isAbsolute(filePath)) {
|
|
246
|
+
filePath = join(drizzleDir, filePath);
|
|
247
|
+
}
|
|
248
|
+
return readFileSync(filePath, "utf-8");
|
|
249
|
+
}
|
|
250
|
+
function getMigrations() {
|
|
251
|
+
const j = _journal_default;
|
|
252
|
+
return j.entries.map((entry) => ({
|
|
253
|
+
tag: entry.tag,
|
|
254
|
+
sql: resolveSqlContent(migrationSql[entry.tag] ?? "")
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
async function getAppliedMigrations(db) {
|
|
258
|
+
try {
|
|
259
|
+
const results = [];
|
|
260
|
+
const client = db.$client;
|
|
261
|
+
if (client && typeof client.prepare === "function") {
|
|
262
|
+
const bunClient = client;
|
|
263
|
+
const rows = bunClient.prepare("SELECT tag FROM __drizzle_migrations").all();
|
|
264
|
+
return new Set(rows.map((r) => r.tag));
|
|
265
|
+
}
|
|
266
|
+
if (client && typeof client.execute === "function") {
|
|
267
|
+
const libsqlClient = client;
|
|
268
|
+
const res = await libsqlClient.execute("SELECT tag FROM __drizzle_migrations");
|
|
269
|
+
return new Set(res.rows.map((r) => String(r.tag)));
|
|
270
|
+
}
|
|
271
|
+
return new Set(results.map((r) => r.tag));
|
|
272
|
+
} catch {
|
|
273
|
+
return new Set;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function createMigrationsTable(db) {
|
|
277
|
+
await db.$runRaw(`
|
|
278
|
+
CREATE TABLE IF NOT EXISTS __drizzle_migrations (
|
|
279
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
280
|
+
tag TEXT NOT NULL UNIQUE,
|
|
281
|
+
created_at INTEGER NOT NULL
|
|
282
|
+
)
|
|
283
|
+
`);
|
|
284
|
+
}
|
|
285
|
+
async function recordMigration(db, tag) {
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
await db.$runRaw(`INSERT INTO __drizzle_migrations (tag, created_at) VALUES ('${tag}', ${now})`);
|
|
288
|
+
}
|
|
289
|
+
async function isLegacyDatabase(db) {
|
|
290
|
+
try {
|
|
291
|
+
const client = db.$client;
|
|
292
|
+
if (client && typeof client.prepare === "function") {
|
|
293
|
+
const bunClient = client;
|
|
294
|
+
const tables = bunClient.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='boards'").all();
|
|
295
|
+
return tables.length > 0;
|
|
296
|
+
}
|
|
297
|
+
if (client && typeof client.execute === "function") {
|
|
298
|
+
const libsqlClient = client;
|
|
299
|
+
const res = await libsqlClient.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='boards'");
|
|
300
|
+
return res.rows.length > 0;
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function runMigrations(db) {
|
|
308
|
+
const isLegacy = await isLegacyDatabase(db);
|
|
309
|
+
await createMigrationsTable(db);
|
|
310
|
+
const applied = await getAppliedMigrations(db);
|
|
311
|
+
if (isLegacy && applied.size === 0) {
|
|
312
|
+
await recordMigration(db, "0000_init");
|
|
313
|
+
applied.add("0000_init");
|
|
314
|
+
}
|
|
315
|
+
const migrations = getMigrations();
|
|
316
|
+
const toApply = migrations.filter((m) => !applied.has(m.tag));
|
|
317
|
+
const newlyApplied = [];
|
|
318
|
+
for (const migration of toApply) {
|
|
319
|
+
if (!migration.sql) {
|
|
320
|
+
throw new Error(`Migration SQL not found for: ${migration.tag}`);
|
|
321
|
+
}
|
|
322
|
+
const statements = migration.sql.split("--> statement-breakpoint").map((s) => s.trim()).filter(Boolean);
|
|
323
|
+
for (const stmt of statements) {
|
|
324
|
+
try {
|
|
325
|
+
await db.$runRaw(stmt);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
const msg = String(err);
|
|
328
|
+
if (stmt.includes("ADD COLUMN") && msg.includes("duplicate column name")) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
await recordMigration(db, migration.tag);
|
|
335
|
+
newlyApplied.push(migration.tag);
|
|
336
|
+
}
|
|
337
|
+
return { applied: newlyApplied };
|
|
338
|
+
}
|
|
339
|
+
var migrationSql, __dirname2, drizzleDir;
|
|
340
|
+
var init_migrator = __esm(() => {
|
|
341
|
+
init__journal();
|
|
342
|
+
init_0000_init();
|
|
343
|
+
init_0001_add_archived();
|
|
344
|
+
init_0002_add_fts5();
|
|
345
|
+
migrationSql = {
|
|
346
|
+
"0000_init": _0000_init_default,
|
|
347
|
+
"0001_add_archived": _0001_add_archived_default,
|
|
348
|
+
"0002_add_fts5": _0002_add_fts5_default
|
|
349
|
+
};
|
|
350
|
+
__dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
351
|
+
drizzleDir = join(__dirname2, "..", "..", "drizzle");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// src/db/bun-adapter.ts
|
|
355
|
+
var exports_bun_adapter = {};
|
|
356
|
+
__export(exports_bun_adapter, {
|
|
357
|
+
createBunDb: () => createBunDb
|
|
358
|
+
});
|
|
359
|
+
async function createBunDb(filePath) {
|
|
360
|
+
let sqlite;
|
|
361
|
+
try {
|
|
362
|
+
const { Database } = await import("bun:sqlite");
|
|
363
|
+
const { drizzle } = await import("drizzle-orm/bun-sqlite");
|
|
364
|
+
ensureDbDir(filePath);
|
|
365
|
+
sqlite = new Database(filePath);
|
|
366
|
+
const db = drizzle({ client: sqlite, schema: exports_schema });
|
|
367
|
+
let closed = false;
|
|
368
|
+
const sqliteRef = sqlite;
|
|
369
|
+
return Object.assign(db, {
|
|
370
|
+
$client: sqliteRef,
|
|
371
|
+
$runRaw: async (sql) => {
|
|
372
|
+
try {
|
|
373
|
+
if (typeof sqliteRef.exec === "function") {
|
|
374
|
+
sqliteRef.exec(sql);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const statements = sql.split(";").filter((s) => s.trim());
|
|
378
|
+
for (const stmt of statements) {
|
|
379
|
+
sqliteRef.run(stmt);
|
|
380
|
+
}
|
|
381
|
+
} catch (error) {
|
|
382
|
+
throw new KabanError(`SQL execution failed: ${error instanceof Error ? error.message : String(error)}`, ExitCode.GENERAL_ERROR);
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
$close: async () => {
|
|
386
|
+
if (closed)
|
|
387
|
+
return;
|
|
388
|
+
closed = true;
|
|
389
|
+
try {
|
|
390
|
+
sqliteRef.close();
|
|
391
|
+
} catch {}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
} catch (error) {
|
|
395
|
+
try {
|
|
396
|
+
sqlite?.close?.();
|
|
397
|
+
} catch {}
|
|
398
|
+
if (error instanceof KabanError)
|
|
399
|
+
throw error;
|
|
400
|
+
throw new KabanError(`Failed to create Bun database: ${error instanceof Error ? error.message : String(error)}`, ExitCode.GENERAL_ERROR);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
var init_bun_adapter = __esm(() => {
|
|
404
|
+
init_types();
|
|
405
|
+
init_schema();
|
|
406
|
+
init_utils();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// src/db/bun-only.ts
|
|
410
|
+
init_types();
|
|
411
|
+
init_utils();
|
|
412
|
+
init_migrator();
|
|
413
|
+
init_schema();
|
|
414
|
+
async function createDb(config, options = {}) {
|
|
415
|
+
const { migrate = true } = options;
|
|
416
|
+
try {
|
|
417
|
+
const driver = process.env.KABAN_DB_DRIVER;
|
|
418
|
+
if (driver === "libsql") {
|
|
419
|
+
throw new KabanError("LibSQL driver requested but this build only supports bun:sqlite", ExitCode.GENERAL_ERROR);
|
|
420
|
+
}
|
|
421
|
+
const { createBunDb: createBunDb2 } = await Promise.resolve().then(() => (init_bun_adapter(), exports_bun_adapter));
|
|
422
|
+
let db;
|
|
423
|
+
if (typeof config === "string") {
|
|
424
|
+
db = await createBunDb2(config);
|
|
425
|
+
} else {
|
|
426
|
+
db = await createBunDb2(fileUrlToPath(config.url));
|
|
427
|
+
}
|
|
428
|
+
if (migrate) {
|
|
429
|
+
const { runMigrations: runMigrations2 } = await Promise.resolve().then(() => (init_migrator(), exports_migrator));
|
|
430
|
+
await runMigrations2(db);
|
|
431
|
+
}
|
|
432
|
+
return db;
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (error instanceof KabanError)
|
|
435
|
+
throw error;
|
|
436
|
+
throw new KabanError(`Failed to create database: ${error instanceof Error ? error.message : String(error)}`, ExitCode.GENERAL_ERROR);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async function initializeSchema(db) {
|
|
440
|
+
const SCHEMA_SQL = `
|
|
441
|
+
PRAGMA journal_mode = WAL;
|
|
442
|
+
|
|
443
|
+
CREATE TABLE IF NOT EXISTS boards (
|
|
444
|
+
id TEXT PRIMARY KEY,
|
|
445
|
+
name TEXT NOT NULL,
|
|
446
|
+
created_at INTEGER NOT NULL,
|
|
447
|
+
updated_at INTEGER NOT NULL
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
CREATE TABLE IF NOT EXISTS columns (
|
|
451
|
+
id TEXT PRIMARY KEY,
|
|
452
|
+
board_id TEXT NOT NULL REFERENCES boards(id),
|
|
453
|
+
name TEXT NOT NULL,
|
|
454
|
+
position INTEGER NOT NULL,
|
|
455
|
+
wip_limit INTEGER,
|
|
456
|
+
is_terminal INTEGER NOT NULL DEFAULT 0
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
460
|
+
id TEXT PRIMARY KEY,
|
|
461
|
+
title TEXT NOT NULL,
|
|
462
|
+
description TEXT,
|
|
463
|
+
column_id TEXT NOT NULL REFERENCES columns(id),
|
|
464
|
+
position INTEGER NOT NULL,
|
|
465
|
+
created_by TEXT NOT NULL,
|
|
466
|
+
assigned_to TEXT,
|
|
467
|
+
parent_id TEXT REFERENCES tasks(id),
|
|
468
|
+
depends_on TEXT NOT NULL DEFAULT '[]',
|
|
469
|
+
files TEXT NOT NULL DEFAULT '[]',
|
|
470
|
+
labels TEXT NOT NULL DEFAULT '[]',
|
|
471
|
+
blocked_reason TEXT,
|
|
472
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
473
|
+
created_at INTEGER NOT NULL,
|
|
474
|
+
updated_at INTEGER NOT NULL,
|
|
475
|
+
started_at INTEGER,
|
|
476
|
+
completed_at INTEGER,
|
|
477
|
+
archived INTEGER NOT NULL DEFAULT 0,
|
|
478
|
+
archived_at INTEGER
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
CREATE TABLE IF NOT EXISTS undo_log (
|
|
482
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
483
|
+
operation TEXT NOT NULL,
|
|
484
|
+
data TEXT NOT NULL,
|
|
485
|
+
created_at INTEGER NOT NULL
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_column ON tasks(column_id);
|
|
489
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id);
|
|
490
|
+
`;
|
|
491
|
+
await db.$runRaw(SCHEMA_SQL);
|
|
492
|
+
}
|
|
493
|
+
// src/schemas.ts
|
|
494
|
+
import { z } from "zod/v4";
|
|
495
|
+
var ULID_REGEX = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
496
|
+
var UlidSchema = z.string().regex(ULID_REGEX, "Invalid ULID format");
|
|
497
|
+
var TitleBaseSchema = z.string().min(1, "Title cannot be empty").max(200, "Title cannot exceed 200 characters");
|
|
498
|
+
var TitleSchema = TitleBaseSchema.transform((s) => s.trim());
|
|
499
|
+
var AgentNameBaseSchema = z.string().min(1, "Agent name cannot be empty").max(50, "Agent name cannot exceed 50 characters").regex(/^[a-zA-Z][a-zA-Z0-9_-]*$/, "Agent name must start with letter, then alphanumeric with _ or -");
|
|
500
|
+
var AgentNameSchema = AgentNameBaseSchema.transform((s) => s.toLowerCase());
|
|
501
|
+
var ColumnIdSchema = z.string().min(1, "Column ID cannot be empty").max(50, "Column ID cannot exceed 50 characters").regex(/^[a-z0-9_]+$/, "Column ID must be lowercase alphanumeric with underscores");
|
|
502
|
+
var TaskSchema = z.object({
|
|
503
|
+
id: UlidSchema,
|
|
504
|
+
title: z.string(),
|
|
505
|
+
description: z.string().nullable(),
|
|
506
|
+
columnId: z.string(),
|
|
507
|
+
position: z.number().int().nonnegative(),
|
|
508
|
+
createdBy: z.string(),
|
|
509
|
+
assignedTo: z.string().nullable(),
|
|
510
|
+
parentId: UlidSchema.nullable(),
|
|
511
|
+
dependsOn: z.array(UlidSchema),
|
|
512
|
+
files: z.array(z.string()),
|
|
513
|
+
labels: z.array(z.string()),
|
|
514
|
+
blockedReason: z.string().nullable(),
|
|
515
|
+
version: z.number().int().positive(),
|
|
516
|
+
createdAt: z.date(),
|
|
517
|
+
updatedAt: z.date(),
|
|
518
|
+
startedAt: z.date().nullable(),
|
|
519
|
+
completedAt: z.date().nullable(),
|
|
520
|
+
archived: z.boolean().default(false),
|
|
521
|
+
archivedAt: z.date().nullable().optional()
|
|
522
|
+
});
|
|
523
|
+
var ColumnSchema = z.object({
|
|
524
|
+
id: z.string(),
|
|
525
|
+
name: z.string(),
|
|
526
|
+
position: z.number().int().nonnegative(),
|
|
527
|
+
wipLimit: z.number().int().positive().nullable(),
|
|
528
|
+
isTerminal: z.boolean()
|
|
529
|
+
});
|
|
530
|
+
var BoardSchema = z.object({
|
|
531
|
+
id: UlidSchema,
|
|
532
|
+
name: z.string(),
|
|
533
|
+
createdAt: z.date(),
|
|
534
|
+
updatedAt: z.date()
|
|
535
|
+
});
|
|
536
|
+
var BoardResponseSchema = z.object({
|
|
537
|
+
id: UlidSchema,
|
|
538
|
+
name: z.string(),
|
|
539
|
+
createdAt: z.string().datetime(),
|
|
540
|
+
updatedAt: z.string().datetime()
|
|
541
|
+
});
|
|
542
|
+
var ColumnConfigSchema = z.object({
|
|
543
|
+
id: ColumnIdSchema,
|
|
544
|
+
name: z.string().min(1).max(50),
|
|
545
|
+
wipLimit: z.number().int().positive().optional(),
|
|
546
|
+
isTerminal: z.boolean().optional()
|
|
547
|
+
});
|
|
548
|
+
var ConfigSchema = z.object({
|
|
549
|
+
board: z.object({
|
|
550
|
+
name: z.string().min(1).max(100)
|
|
551
|
+
}),
|
|
552
|
+
columns: z.array(ColumnConfigSchema).min(1),
|
|
553
|
+
defaults: z.object({
|
|
554
|
+
column: ColumnIdSchema,
|
|
555
|
+
agent: AgentNameSchema
|
|
556
|
+
})
|
|
557
|
+
});
|
|
558
|
+
var AddTaskInputSchema = z.object({
|
|
559
|
+
title: TitleSchema,
|
|
560
|
+
description: z.string().max(5000).optional(),
|
|
561
|
+
columnId: ColumnIdSchema.optional(),
|
|
562
|
+
createdBy: AgentNameSchema.optional(),
|
|
563
|
+
agent: AgentNameSchema.optional(),
|
|
564
|
+
assignedTo: AgentNameSchema.optional(),
|
|
565
|
+
dependsOn: z.array(UlidSchema).optional(),
|
|
566
|
+
files: z.array(z.string()).optional(),
|
|
567
|
+
labels: z.array(z.string().max(50)).optional()
|
|
568
|
+
});
|
|
569
|
+
var UpdateTaskInputSchema = z.object({
|
|
570
|
+
title: TitleSchema.optional(),
|
|
571
|
+
description: z.string().max(5000).nullable().optional(),
|
|
572
|
+
assignedTo: AgentNameSchema.nullable().optional(),
|
|
573
|
+
files: z.array(z.string()).optional(),
|
|
574
|
+
labels: z.array(z.string().max(50)).optional()
|
|
575
|
+
});
|
|
576
|
+
var MoveTaskInputSchema = z.object({
|
|
577
|
+
id: UlidSchema,
|
|
578
|
+
columnId: ColumnIdSchema,
|
|
579
|
+
force: z.boolean().optional()
|
|
580
|
+
});
|
|
581
|
+
var ListTasksFilterSchema = z.object({
|
|
582
|
+
columnId: ColumnIdSchema.optional(),
|
|
583
|
+
createdBy: AgentNameSchema.optional(),
|
|
584
|
+
agent: AgentNameSchema.optional(),
|
|
585
|
+
assignee: AgentNameSchema.optional(),
|
|
586
|
+
blocked: z.boolean().optional(),
|
|
587
|
+
includeArchived: z.boolean().optional()
|
|
588
|
+
});
|
|
589
|
+
var GetTaskInputSchema = z.object({
|
|
590
|
+
id: UlidSchema
|
|
591
|
+
});
|
|
592
|
+
var DeleteTaskInputSchema = z.object({
|
|
593
|
+
id: UlidSchema
|
|
594
|
+
});
|
|
595
|
+
var TaskResponseSchema = TaskSchema.extend({
|
|
596
|
+
createdAt: z.string().datetime(),
|
|
597
|
+
updatedAt: z.string().datetime(),
|
|
598
|
+
startedAt: z.string().datetime().nullable(),
|
|
599
|
+
completedAt: z.string().datetime().nullable(),
|
|
600
|
+
archivedAt: z.string().datetime().nullable().optional()
|
|
601
|
+
});
|
|
602
|
+
var BoardStatusSchema = z.object({
|
|
603
|
+
board: BoardResponseSchema,
|
|
604
|
+
columns: z.array(ColumnSchema.extend({
|
|
605
|
+
taskCount: z.number().int().nonnegative()
|
|
606
|
+
})),
|
|
607
|
+
totalTasks: z.number().int().nonnegative()
|
|
608
|
+
});
|
|
609
|
+
var ApiResponseSchema = (dataSchema) => z.object({
|
|
610
|
+
success: z.boolean(),
|
|
611
|
+
data: dataSchema.optional(),
|
|
612
|
+
error: z.object({
|
|
613
|
+
message: z.string(),
|
|
614
|
+
code: z.number()
|
|
615
|
+
}).optional()
|
|
616
|
+
});
|
|
617
|
+
var AddTaskInputJsonSchema = z.object({
|
|
618
|
+
title: TitleBaseSchema,
|
|
619
|
+
description: z.string().max(5000).optional(),
|
|
620
|
+
columnId: ColumnIdSchema.optional(),
|
|
621
|
+
createdBy: AgentNameBaseSchema.optional(),
|
|
622
|
+
agent: AgentNameBaseSchema.optional(),
|
|
623
|
+
assignedTo: AgentNameBaseSchema.optional(),
|
|
624
|
+
dependsOn: z.array(UlidSchema).optional(),
|
|
625
|
+
files: z.array(z.string()).optional(),
|
|
626
|
+
labels: z.array(z.string().max(50)).optional()
|
|
627
|
+
});
|
|
628
|
+
var UpdateTaskInputJsonSchema = z.object({
|
|
629
|
+
title: TitleBaseSchema.optional(),
|
|
630
|
+
description: z.string().max(5000).nullable().optional(),
|
|
631
|
+
assignedTo: AgentNameBaseSchema.nullable().optional(),
|
|
632
|
+
files: z.array(z.string()).optional(),
|
|
633
|
+
labels: z.array(z.string().max(50)).optional()
|
|
634
|
+
});
|
|
635
|
+
var ListTasksFilterJsonSchema = z.object({
|
|
636
|
+
columnId: ColumnIdSchema.optional(),
|
|
637
|
+
createdBy: AgentNameBaseSchema.optional(),
|
|
638
|
+
agent: AgentNameBaseSchema.optional(),
|
|
639
|
+
assignee: AgentNameBaseSchema.optional(),
|
|
640
|
+
blocked: z.boolean().optional(),
|
|
641
|
+
includeArchived: z.boolean().optional()
|
|
642
|
+
});
|
|
643
|
+
var ColumnConfigJsonSchema = z.object({
|
|
644
|
+
id: ColumnIdSchema,
|
|
645
|
+
name: z.string().min(1).max(50),
|
|
646
|
+
wipLimit: z.number().int().positive().optional(),
|
|
647
|
+
isTerminal: z.boolean().optional()
|
|
648
|
+
});
|
|
649
|
+
var ConfigJsonSchema = z.object({
|
|
650
|
+
board: z.object({
|
|
651
|
+
name: z.string().min(1).max(100)
|
|
652
|
+
}),
|
|
653
|
+
columns: z.array(ColumnConfigJsonSchema).min(1),
|
|
654
|
+
defaults: z.object({
|
|
655
|
+
column: ColumnIdSchema,
|
|
656
|
+
agent: AgentNameBaseSchema
|
|
657
|
+
})
|
|
658
|
+
});
|
|
659
|
+
var jsonSchemas = {
|
|
660
|
+
Task: z.toJSONSchema(TaskResponseSchema),
|
|
661
|
+
Column: z.toJSONSchema(ColumnSchema),
|
|
662
|
+
Board: z.toJSONSchema(BoardResponseSchema),
|
|
663
|
+
Config: z.toJSONSchema(ConfigJsonSchema),
|
|
664
|
+
AddTaskInput: z.toJSONSchema(AddTaskInputJsonSchema),
|
|
665
|
+
UpdateTaskInput: z.toJSONSchema(UpdateTaskInputJsonSchema),
|
|
666
|
+
MoveTaskInput: z.toJSONSchema(MoveTaskInputSchema),
|
|
667
|
+
ListTasksFilter: z.toJSONSchema(ListTasksFilterJsonSchema),
|
|
668
|
+
BoardStatus: z.toJSONSchema(BoardStatusSchema)
|
|
669
|
+
};
|
|
670
|
+
function getJsonSchema(name) {
|
|
671
|
+
return jsonSchemas[name];
|
|
672
|
+
}
|
|
673
|
+
// src/services/board.ts
|
|
674
|
+
init_schema();
|
|
675
|
+
import { eq } from "drizzle-orm";
|
|
676
|
+
import { ulid } from "ulid";
|
|
677
|
+
|
|
678
|
+
class BoardService {
|
|
679
|
+
db;
|
|
680
|
+
constructor(db) {
|
|
681
|
+
this.db = db;
|
|
682
|
+
}
|
|
683
|
+
async initializeBoard(config) {
|
|
684
|
+
const now = new Date;
|
|
685
|
+
const boardId = ulid();
|
|
686
|
+
await this.db.insert(boards).values({
|
|
687
|
+
id: boardId,
|
|
688
|
+
name: config.board.name,
|
|
689
|
+
createdAt: now,
|
|
690
|
+
updatedAt: now
|
|
691
|
+
});
|
|
692
|
+
for (let i = 0;i < config.columns.length; i++) {
|
|
693
|
+
const col = config.columns[i];
|
|
694
|
+
await this.db.insert(columns).values({
|
|
695
|
+
id: col.id,
|
|
696
|
+
boardId,
|
|
697
|
+
name: col.name,
|
|
698
|
+
position: i,
|
|
699
|
+
wipLimit: col.wipLimit ?? null,
|
|
700
|
+
isTerminal: col.isTerminal ?? false
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
id: boardId,
|
|
705
|
+
name: config.board.name,
|
|
706
|
+
createdAt: now,
|
|
707
|
+
updatedAt: now
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
async getBoard() {
|
|
711
|
+
const rows = await this.db.select().from(boards).limit(1);
|
|
712
|
+
return rows[0] ?? null;
|
|
713
|
+
}
|
|
714
|
+
async getColumns() {
|
|
715
|
+
return this.db.select().from(columns).orderBy(columns.position);
|
|
716
|
+
}
|
|
717
|
+
async getColumn(id) {
|
|
718
|
+
const rows = await this.db.select().from(columns).where(eq(columns.id, id));
|
|
719
|
+
return rows[0] ?? null;
|
|
720
|
+
}
|
|
721
|
+
async getTerminalColumn() {
|
|
722
|
+
const rows = await this.db.select().from(columns).where(eq(columns.isTerminal, true));
|
|
723
|
+
return rows[0] ?? null;
|
|
724
|
+
}
|
|
725
|
+
async getTerminalColumns() {
|
|
726
|
+
return this.db.select().from(columns).where(eq(columns.isTerminal, true)).orderBy(columns.position);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// src/services/task.ts
|
|
730
|
+
init_schema();
|
|
731
|
+
init_types();
|
|
732
|
+
import { and, eq as eq2, inArray, lt, sql } from "drizzle-orm";
|
|
733
|
+
import { ulid as ulid2 } from "ulid";
|
|
734
|
+
|
|
735
|
+
// src/utils/similarity.ts
|
|
736
|
+
function tokenize(text2) {
|
|
737
|
+
const words = text2.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((word) => word.length > 0);
|
|
738
|
+
return new Set(words);
|
|
739
|
+
}
|
|
740
|
+
function jaccardSimilarity(text1, text2) {
|
|
741
|
+
const set1 = tokenize(text1);
|
|
742
|
+
const set2 = tokenize(text2);
|
|
743
|
+
if (set1.size === 0 || set2.size === 0) {
|
|
744
|
+
return 0;
|
|
745
|
+
}
|
|
746
|
+
const intersection = new Set([...set1].filter((word) => set2.has(word)));
|
|
747
|
+
const union = new Set([...set1, ...set2]);
|
|
748
|
+
return intersection.size / union.size;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/validation.ts
|
|
752
|
+
import { ZodError } from "zod";
|
|
753
|
+
init_types();
|
|
754
|
+
function wrapZodError(fn, fieldName) {
|
|
755
|
+
try {
|
|
756
|
+
return fn();
|
|
757
|
+
} catch (error) {
|
|
758
|
+
if (error instanceof ZodError) {
|
|
759
|
+
const message = error.issues?.[0]?.message ?? `Invalid ${fieldName}`;
|
|
760
|
+
throw new KabanError(message, ExitCode.VALIDATION);
|
|
761
|
+
}
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function validateTitle(title) {
|
|
766
|
+
return wrapZodError(() => TitleSchema.parse(title), "title");
|
|
767
|
+
}
|
|
768
|
+
function validateAgentName(name) {
|
|
769
|
+
return wrapZodError(() => AgentNameSchema.parse(name), "agent name");
|
|
770
|
+
}
|
|
771
|
+
function validateColumnId(id) {
|
|
772
|
+
return wrapZodError(() => ColumnIdSchema.parse(id), "column ID");
|
|
773
|
+
}
|
|
774
|
+
function isValidUlid(id) {
|
|
775
|
+
return UlidSchema.safeParse(id).success;
|
|
776
|
+
}
|
|
777
|
+
function validateTaskId(id) {
|
|
778
|
+
return wrapZodError(() => UlidSchema.parse(id), "task ID");
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/services/task.ts
|
|
782
|
+
class TaskService {
|
|
783
|
+
db;
|
|
784
|
+
boardService;
|
|
785
|
+
duplicateConfig = {
|
|
786
|
+
enabled: true,
|
|
787
|
+
threshold: 0.5,
|
|
788
|
+
warnThreshold: 0.5
|
|
789
|
+
};
|
|
790
|
+
constructor(db, boardService) {
|
|
791
|
+
this.db = db;
|
|
792
|
+
this.boardService = boardService;
|
|
793
|
+
}
|
|
794
|
+
async getTaskOrThrow(id) {
|
|
795
|
+
const task = await this.getTask(id);
|
|
796
|
+
if (!task) {
|
|
797
|
+
throw new KabanError(`Task '${id}' not found`, ExitCode.NOT_FOUND);
|
|
798
|
+
}
|
|
799
|
+
return task;
|
|
800
|
+
}
|
|
801
|
+
async addTask(input) {
|
|
802
|
+
const title = validateTitle(input.title);
|
|
803
|
+
const createdBy = input.createdBy ? validateAgentName(input.createdBy) : input.agent ? validateAgentName(input.agent) : "user";
|
|
804
|
+
const columnId = input.columnId ? validateColumnId(input.columnId) : "todo";
|
|
805
|
+
const column = await this.boardService.getColumn(columnId);
|
|
806
|
+
if (!column) {
|
|
807
|
+
throw new KabanError(`Column '${columnId}' does not exist`, ExitCode.VALIDATION);
|
|
808
|
+
}
|
|
809
|
+
const now = new Date;
|
|
810
|
+
const id = ulid2();
|
|
811
|
+
const maxPositionResult = await this.db.select({ max: sql`COALESCE(MAX(position), -1)` }).from(tasks).where(eq2(tasks.columnId, columnId));
|
|
812
|
+
const position = (maxPositionResult[0]?.max ?? -1) + 1;
|
|
813
|
+
await this.db.insert(tasks).values({
|
|
814
|
+
id,
|
|
815
|
+
title,
|
|
816
|
+
description: input.description ?? null,
|
|
817
|
+
columnId,
|
|
818
|
+
position,
|
|
819
|
+
createdBy,
|
|
820
|
+
assignedTo: null,
|
|
821
|
+
parentId: null,
|
|
822
|
+
dependsOn: input.dependsOn ?? [],
|
|
823
|
+
files: input.files ?? [],
|
|
824
|
+
labels: input.labels ?? [],
|
|
825
|
+
blockedReason: null,
|
|
826
|
+
version: 1,
|
|
827
|
+
createdAt: now,
|
|
828
|
+
updatedAt: now,
|
|
829
|
+
startedAt: null,
|
|
830
|
+
completedAt: null,
|
|
831
|
+
archived: false,
|
|
832
|
+
archivedAt: null
|
|
833
|
+
});
|
|
834
|
+
return this.getTaskOrThrow(id);
|
|
835
|
+
}
|
|
836
|
+
async getTask(id) {
|
|
837
|
+
const rows = await this.db.select().from(tasks).where(eq2(tasks.id, id));
|
|
838
|
+
return rows[0] ?? null;
|
|
839
|
+
}
|
|
840
|
+
async listTasks(filter) {
|
|
841
|
+
const conditions = [];
|
|
842
|
+
if (!filter?.includeArchived) {
|
|
843
|
+
conditions.push(eq2(tasks.archived, false));
|
|
844
|
+
}
|
|
845
|
+
if (filter?.columnId) {
|
|
846
|
+
conditions.push(eq2(tasks.columnId, filter.columnId));
|
|
847
|
+
}
|
|
848
|
+
const creatorFilter = filter?.createdBy ?? filter?.agent;
|
|
849
|
+
if (creatorFilter) {
|
|
850
|
+
conditions.push(eq2(tasks.createdBy, creatorFilter));
|
|
851
|
+
}
|
|
852
|
+
if (filter?.assignee) {
|
|
853
|
+
conditions.push(eq2(tasks.assignedTo, filter.assignee));
|
|
854
|
+
}
|
|
855
|
+
if (filter?.blocked === true) {
|
|
856
|
+
conditions.push(sql`${tasks.blockedReason} IS NOT NULL`);
|
|
857
|
+
}
|
|
858
|
+
if (conditions.length > 0) {
|
|
859
|
+
return this.db.select().from(tasks).where(and(...conditions)).orderBy(tasks.columnId, tasks.position);
|
|
860
|
+
}
|
|
861
|
+
return this.db.select().from(tasks).orderBy(tasks.columnId, tasks.position);
|
|
862
|
+
}
|
|
863
|
+
async deleteTask(id) {
|
|
864
|
+
const task = await this.getTask(id);
|
|
865
|
+
if (!task) {
|
|
866
|
+
throw new KabanError(`Task '${id}' not found`, ExitCode.NOT_FOUND);
|
|
867
|
+
}
|
|
868
|
+
await this.db.delete(tasks).where(eq2(tasks.id, id));
|
|
869
|
+
}
|
|
870
|
+
async moveTask(id, columnId, options) {
|
|
871
|
+
const task = await this.getTask(id);
|
|
872
|
+
if (!task) {
|
|
873
|
+
throw new KabanError(`Task '${id}' not found`, ExitCode.NOT_FOUND);
|
|
874
|
+
}
|
|
875
|
+
validateColumnId(columnId);
|
|
876
|
+
const column = await this.boardService.getColumn(columnId);
|
|
877
|
+
if (!column) {
|
|
878
|
+
throw new KabanError(`Column '${columnId}' does not exist`, ExitCode.VALIDATION);
|
|
879
|
+
}
|
|
880
|
+
if (column.wipLimit && !options?.force) {
|
|
881
|
+
const count = await this.getTaskCountInColumn(columnId);
|
|
882
|
+
if (count >= column.wipLimit) {
|
|
883
|
+
throw new KabanError(`Column '${column.name}' at WIP limit (${count}/${column.wipLimit}). Move a task out first.`, ExitCode.VALIDATION);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (column.isTerminal && options?.validateDeps) {
|
|
887
|
+
const depResult = await this.validateDependencies(id);
|
|
888
|
+
if (!depResult.valid) {
|
|
889
|
+
throw new KabanError(`Task '${id}' is blocked by incomplete dependencies: ${depResult.blockedBy.join(", ")}`, ExitCode.VALIDATION);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const now = new Date;
|
|
893
|
+
const isTerminal = column.isTerminal;
|
|
894
|
+
const maxPositionResult = await this.db.select({ max: sql`COALESCE(MAX(position), -1)` }).from(tasks).where(eq2(tasks.columnId, columnId));
|
|
895
|
+
const newPosition = (maxPositionResult[0]?.max ?? -1) + 1;
|
|
896
|
+
await this.db.update(tasks).set({
|
|
897
|
+
columnId,
|
|
898
|
+
position: newPosition,
|
|
899
|
+
version: task.version + 1,
|
|
900
|
+
updatedAt: now,
|
|
901
|
+
completedAt: isTerminal ? now : task.completedAt,
|
|
902
|
+
startedAt: columnId === "in_progress" && !task.startedAt ? now : task.startedAt
|
|
903
|
+
}).where(eq2(tasks.id, id));
|
|
904
|
+
return this.getTaskOrThrow(id);
|
|
905
|
+
}
|
|
906
|
+
async updateTask(id, input, expectedVersion) {
|
|
907
|
+
const task = await this.getTask(id);
|
|
908
|
+
if (!task) {
|
|
909
|
+
throw new KabanError(`Task '${id}' not found`, ExitCode.NOT_FOUND);
|
|
910
|
+
}
|
|
911
|
+
if (expectedVersion !== undefined && task.version !== expectedVersion) {
|
|
912
|
+
throw new KabanError(`Task modified by another agent, re-read required. Current version: ${task.version}`, ExitCode.CONFLICT);
|
|
913
|
+
}
|
|
914
|
+
const updates = {
|
|
915
|
+
version: task.version + 1,
|
|
916
|
+
updatedAt: new Date
|
|
917
|
+
};
|
|
918
|
+
if (input.title !== undefined) {
|
|
919
|
+
updates.title = validateTitle(input.title);
|
|
920
|
+
}
|
|
921
|
+
if (input.description !== undefined) {
|
|
922
|
+
updates.description = input.description;
|
|
923
|
+
}
|
|
924
|
+
if (input.assignedTo !== undefined) {
|
|
925
|
+
updates.assignedTo = input.assignedTo ? validateAgentName(input.assignedTo) : null;
|
|
926
|
+
}
|
|
927
|
+
if (input.files !== undefined) {
|
|
928
|
+
updates.files = input.files;
|
|
929
|
+
}
|
|
930
|
+
if (input.labels !== undefined) {
|
|
931
|
+
updates.labels = input.labels;
|
|
932
|
+
}
|
|
933
|
+
await this.db.update(tasks).set(updates).where(expectedVersion !== undefined ? and(eq2(tasks.id, id), eq2(tasks.version, expectedVersion)) : eq2(tasks.id, id));
|
|
934
|
+
return this.getTaskOrThrow(id);
|
|
935
|
+
}
|
|
936
|
+
async getTaskCountInColumn(columnId) {
|
|
937
|
+
const result = await this.db.select({ count: sql`COUNT(*)` }).from(tasks).where(eq2(tasks.columnId, columnId));
|
|
938
|
+
return result[0]?.count ?? 0;
|
|
939
|
+
}
|
|
940
|
+
async archiveTasks(criteria) {
|
|
941
|
+
const hasCriteria = criteria.status || criteria.olderThan || criteria.taskIds?.length;
|
|
942
|
+
if (!hasCriteria) {
|
|
943
|
+
throw new KabanError("At least one criteria must be provided", ExitCode.VALIDATION);
|
|
944
|
+
}
|
|
945
|
+
const conditions = [eq2(tasks.archived, false)];
|
|
946
|
+
if (criteria.status) {
|
|
947
|
+
conditions.push(eq2(tasks.columnId, criteria.status));
|
|
948
|
+
}
|
|
949
|
+
if (criteria.olderThan) {
|
|
950
|
+
conditions.push(lt(tasks.createdAt, criteria.olderThan));
|
|
951
|
+
}
|
|
952
|
+
if (criteria.taskIds?.length) {
|
|
953
|
+
conditions.push(inArray(tasks.id, criteria.taskIds));
|
|
954
|
+
}
|
|
955
|
+
const matchingTasks = await this.db.select({ id: tasks.id }).from(tasks).where(and(...conditions));
|
|
956
|
+
if (matchingTasks.length === 0) {
|
|
957
|
+
return { archivedCount: 0, taskIds: [] };
|
|
958
|
+
}
|
|
959
|
+
const taskIds = matchingTasks.map((t) => t.id);
|
|
960
|
+
const now = new Date;
|
|
961
|
+
await this.db.update(tasks).set({
|
|
962
|
+
archived: true,
|
|
963
|
+
archivedAt: now,
|
|
964
|
+
updatedAt: now
|
|
965
|
+
}).where(inArray(tasks.id, taskIds));
|
|
966
|
+
return {
|
|
967
|
+
archivedCount: taskIds.length,
|
|
968
|
+
taskIds
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
async restoreTask(taskId, targetColumnId) {
|
|
972
|
+
const task = await this.getTask(taskId);
|
|
973
|
+
if (!task) {
|
|
974
|
+
throw new KabanError(`Task '${taskId}' not found`, ExitCode.NOT_FOUND);
|
|
975
|
+
}
|
|
976
|
+
if (!task.archived) {
|
|
977
|
+
throw new KabanError(`Task '${taskId}' is not archived`, ExitCode.VALIDATION);
|
|
978
|
+
}
|
|
979
|
+
const columnId = targetColumnId ?? task.columnId;
|
|
980
|
+
if (targetColumnId) {
|
|
981
|
+
validateColumnId(targetColumnId);
|
|
982
|
+
const column = await this.boardService.getColumn(targetColumnId);
|
|
983
|
+
if (!column) {
|
|
984
|
+
throw new KabanError(`Column '${targetColumnId}' does not exist`, ExitCode.VALIDATION);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const now = new Date;
|
|
988
|
+
await this.db.update(tasks).set({
|
|
989
|
+
archived: false,
|
|
990
|
+
archivedAt: null,
|
|
991
|
+
columnId,
|
|
992
|
+
version: task.version + 1,
|
|
993
|
+
updatedAt: now
|
|
994
|
+
}).where(eq2(tasks.id, taskId));
|
|
995
|
+
return this.getTaskOrThrow(taskId);
|
|
996
|
+
}
|
|
997
|
+
async searchArchive(query, options) {
|
|
998
|
+
const limit = options?.limit ?? 50;
|
|
999
|
+
const offset = options?.offset ?? 0;
|
|
1000
|
+
if (!query.trim()) {
|
|
1001
|
+
const countResult = await this.db.select({ count: sql`COUNT(*)` }).from(tasks).where(eq2(tasks.archived, true));
|
|
1002
|
+
const total2 = countResult[0]?.count ?? 0;
|
|
1003
|
+
const archivedTasks = await this.db.select().from(tasks).where(eq2(tasks.archived, true)).orderBy(tasks.archivedAt).limit(limit).offset(offset);
|
|
1004
|
+
return { tasks: archivedTasks, total: total2 };
|
|
1005
|
+
}
|
|
1006
|
+
const ftsQuery = query.trim().split(/\s+/).map((term) => `"${term}"`).join(" ");
|
|
1007
|
+
const client = this.db.$client;
|
|
1008
|
+
const countRow = client.prepare(`SELECT COUNT(*) as count FROM tasks t
|
|
1009
|
+
JOIN tasks_fts fts ON t.id = fts.id
|
|
1010
|
+
WHERE tasks_fts MATCH ?
|
|
1011
|
+
AND t.archived = 1`).get(ftsQuery);
|
|
1012
|
+
const total = countRow?.count ?? 0;
|
|
1013
|
+
const rows = client.prepare(`SELECT t.* FROM tasks t
|
|
1014
|
+
JOIN tasks_fts fts ON t.id = fts.id
|
|
1015
|
+
WHERE tasks_fts MATCH ?
|
|
1016
|
+
AND t.archived = 1
|
|
1017
|
+
ORDER BY rank
|
|
1018
|
+
LIMIT ? OFFSET ?`).all(ftsQuery, limit, offset);
|
|
1019
|
+
const searchTasks = rows.map((row) => ({
|
|
1020
|
+
id: row.id,
|
|
1021
|
+
title: row.title,
|
|
1022
|
+
description: row.description,
|
|
1023
|
+
columnId: row.column_id,
|
|
1024
|
+
position: row.position,
|
|
1025
|
+
createdBy: row.created_by,
|
|
1026
|
+
assignedTo: row.assigned_to,
|
|
1027
|
+
parentId: row.parent_id,
|
|
1028
|
+
dependsOn: JSON.parse(row.depends_on || "[]"),
|
|
1029
|
+
files: JSON.parse(row.files || "[]"),
|
|
1030
|
+
labels: JSON.parse(row.labels || "[]"),
|
|
1031
|
+
blockedReason: row.blocked_reason,
|
|
1032
|
+
version: row.version,
|
|
1033
|
+
createdAt: new Date(row.created_at),
|
|
1034
|
+
updatedAt: new Date(row.updated_at),
|
|
1035
|
+
startedAt: row.started_at ? new Date(row.started_at) : null,
|
|
1036
|
+
completedAt: row.completed_at ? new Date(row.completed_at) : null,
|
|
1037
|
+
archived: Boolean(row.archived),
|
|
1038
|
+
archivedAt: row.archived_at ? new Date(row.archived_at) : null
|
|
1039
|
+
}));
|
|
1040
|
+
return { tasks: searchTasks, total };
|
|
1041
|
+
}
|
|
1042
|
+
async purgeArchive(criteria) {
|
|
1043
|
+
const conditions = [eq2(tasks.archived, true)];
|
|
1044
|
+
if (criteria?.olderThan) {
|
|
1045
|
+
conditions.push(lt(tasks.archivedAt, criteria.olderThan));
|
|
1046
|
+
}
|
|
1047
|
+
const matchingTasks = await this.db.select({ id: tasks.id }).from(tasks).where(and(...conditions));
|
|
1048
|
+
if (matchingTasks.length === 0) {
|
|
1049
|
+
return { deletedCount: 0 };
|
|
1050
|
+
}
|
|
1051
|
+
const taskIds = matchingTasks.map((t) => t.id);
|
|
1052
|
+
await this.db.delete(tasks).where(inArray(tasks.id, taskIds));
|
|
1053
|
+
return { deletedCount: taskIds.length };
|
|
1054
|
+
}
|
|
1055
|
+
async resetBoard() {
|
|
1056
|
+
const allTasks = await this.db.select({ id: tasks.id }).from(tasks);
|
|
1057
|
+
if (allTasks.length === 0) {
|
|
1058
|
+
return { deletedCount: 0 };
|
|
1059
|
+
}
|
|
1060
|
+
await this.db.delete(tasks).where(sql`1=1`);
|
|
1061
|
+
return { deletedCount: allTasks.length };
|
|
1062
|
+
}
|
|
1063
|
+
async addDependency(taskId, dependsOnId) {
|
|
1064
|
+
if (taskId === dependsOnId) {
|
|
1065
|
+
throw new KabanError("Task cannot depend on itself", ExitCode.VALIDATION);
|
|
1066
|
+
}
|
|
1067
|
+
const task = await this.getTaskOrThrow(taskId);
|
|
1068
|
+
await this.getTaskOrThrow(dependsOnId);
|
|
1069
|
+
if (task.dependsOn.includes(dependsOnId)) {
|
|
1070
|
+
return task;
|
|
1071
|
+
}
|
|
1072
|
+
const updatedDependsOn = [...task.dependsOn, dependsOnId];
|
|
1073
|
+
const now = new Date;
|
|
1074
|
+
await this.db.update(tasks).set({
|
|
1075
|
+
dependsOn: updatedDependsOn,
|
|
1076
|
+
version: task.version + 1,
|
|
1077
|
+
updatedAt: now
|
|
1078
|
+
}).where(eq2(tasks.id, taskId));
|
|
1079
|
+
return this.getTaskOrThrow(taskId);
|
|
1080
|
+
}
|
|
1081
|
+
async removeDependency(taskId, dependsOnId) {
|
|
1082
|
+
const task = await this.getTaskOrThrow(taskId);
|
|
1083
|
+
if (!task.dependsOn.includes(dependsOnId)) {
|
|
1084
|
+
return task;
|
|
1085
|
+
}
|
|
1086
|
+
const updatedDependsOn = task.dependsOn.filter((id) => id !== dependsOnId);
|
|
1087
|
+
const now = new Date;
|
|
1088
|
+
await this.db.update(tasks).set({
|
|
1089
|
+
dependsOn: updatedDependsOn,
|
|
1090
|
+
version: task.version + 1,
|
|
1091
|
+
updatedAt: now
|
|
1092
|
+
}).where(eq2(tasks.id, taskId));
|
|
1093
|
+
return this.getTaskOrThrow(taskId);
|
|
1094
|
+
}
|
|
1095
|
+
async validateDependencies(taskId) {
|
|
1096
|
+
const task = await this.getTask(taskId);
|
|
1097
|
+
if (!task) {
|
|
1098
|
+
throw new KabanError(`Task '${taskId}' not found`, ExitCode.NOT_FOUND);
|
|
1099
|
+
}
|
|
1100
|
+
if (!task.dependsOn || task.dependsOn.length === 0) {
|
|
1101
|
+
return { valid: true, blockedBy: [] };
|
|
1102
|
+
}
|
|
1103
|
+
const terminalColumn = await this.boardService.getTerminalColumn();
|
|
1104
|
+
if (!terminalColumn) {
|
|
1105
|
+
return { valid: true, blockedBy: [] };
|
|
1106
|
+
}
|
|
1107
|
+
const blockedBy = [];
|
|
1108
|
+
for (const depId of task.dependsOn) {
|
|
1109
|
+
const depTask = await this.getTask(depId);
|
|
1110
|
+
if (depTask && depTask.columnId !== terminalColumn.id) {
|
|
1111
|
+
blockedBy.push(depId);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return {
|
|
1115
|
+
valid: blockedBy.length === 0,
|
|
1116
|
+
blockedBy
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
async findSimilarTasks(title, threshold = 0.5) {
|
|
1120
|
+
const activeTasks = await this.listTasks({ includeArchived: false });
|
|
1121
|
+
return activeTasks.map((task) => ({
|
|
1122
|
+
task,
|
|
1123
|
+
similarity: jaccardSimilarity(title, task.title)
|
|
1124
|
+
})).filter((result) => result.similarity >= threshold).sort((a, b) => b.similarity - a.similarity).slice(0, 5);
|
|
1125
|
+
}
|
|
1126
|
+
async addTaskChecked(input, options) {
|
|
1127
|
+
const similarTasks = await this.findSimilarTasks(input.title, this.duplicateConfig.threshold);
|
|
1128
|
+
const highSimilarity = similarTasks.filter((s) => s.similarity >= this.duplicateConfig.warnThreshold);
|
|
1129
|
+
if (highSimilarity.length > 0 && !options?.force) {
|
|
1130
|
+
const similar = highSimilarity.map((s) => `"${s.task.title}" (${Math.round(s.similarity * 100)}%)`).join(", ");
|
|
1131
|
+
return {
|
|
1132
|
+
task: null,
|
|
1133
|
+
created: false,
|
|
1134
|
+
similarTasks,
|
|
1135
|
+
rejected: true,
|
|
1136
|
+
rejectionReason: `Found ${highSimilarity.length} very similar task(s): ${similar}. Use force=true to create anyway.`
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
const task = await this.addTask(input);
|
|
1140
|
+
return {
|
|
1141
|
+
task,
|
|
1142
|
+
created: true,
|
|
1143
|
+
similarTasks,
|
|
1144
|
+
rejected: false
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
async getArchiveStats() {
|
|
1148
|
+
const archived = await this.db.select().from(tasks).where(eq2(tasks.archived, true));
|
|
1149
|
+
const byColumn = {};
|
|
1150
|
+
let oldestArchivedAt = null;
|
|
1151
|
+
for (const task of archived) {
|
|
1152
|
+
byColumn[task.columnId] = (byColumn[task.columnId] ?? 0) + 1;
|
|
1153
|
+
if (task.archivedAt && (!oldestArchivedAt || task.archivedAt < oldestArchivedAt)) {
|
|
1154
|
+
oldestArchivedAt = task.archivedAt;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
totalArchived: archived.length,
|
|
1159
|
+
byColumn,
|
|
1160
|
+
oldestArchivedAt
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/index-bun.ts
|
|
1166
|
+
init_types();
|
|
1167
|
+
export {
|
|
1168
|
+
validateTitle,
|
|
1169
|
+
validateTaskId,
|
|
1170
|
+
validateColumnId,
|
|
1171
|
+
validateAgentName,
|
|
1172
|
+
undoLog,
|
|
1173
|
+
tokenize,
|
|
1174
|
+
tasks,
|
|
1175
|
+
runMigrations,
|
|
1176
|
+
jsonSchemas,
|
|
1177
|
+
jaccardSimilarity,
|
|
1178
|
+
isValidUlid,
|
|
1179
|
+
initializeSchema,
|
|
1180
|
+
getJsonSchema,
|
|
1181
|
+
createDb,
|
|
1182
|
+
columns,
|
|
1183
|
+
boards,
|
|
1184
|
+
UpdateTaskInputSchema,
|
|
1185
|
+
UlidSchema,
|
|
1186
|
+
TitleSchema,
|
|
1187
|
+
TaskService,
|
|
1188
|
+
TaskSchema,
|
|
1189
|
+
TaskResponseSchema,
|
|
1190
|
+
MoveTaskInputSchema,
|
|
1191
|
+
ListTasksFilterSchema,
|
|
1192
|
+
KabanError,
|
|
1193
|
+
GetTaskInputSchema,
|
|
1194
|
+
ExitCode,
|
|
1195
|
+
DeleteTaskInputSchema,
|
|
1196
|
+
DEFAULT_CONFIG,
|
|
1197
|
+
ConfigSchema,
|
|
1198
|
+
ColumnSchema,
|
|
1199
|
+
ColumnIdSchema,
|
|
1200
|
+
ColumnConfigSchema,
|
|
1201
|
+
BoardStatusSchema,
|
|
1202
|
+
BoardService,
|
|
1203
|
+
BoardSchema,
|
|
1204
|
+
ApiResponseSchema,
|
|
1205
|
+
AgentNameSchema,
|
|
1206
|
+
AddTaskInputSchema
|
|
1207
|
+
};
|