@tomkapa/tayto 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.
package/dist/index.js ADDED
@@ -0,0 +1,1962 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DependencyType,
4
+ RANK_GAP,
5
+ TERMINAL_STATUSES,
6
+ TaskStatus,
7
+ TaskType,
8
+ UIDependencyType,
9
+ isTerminalStatus,
10
+ logger
11
+ } from "./chunk-6NQOFUIQ.js";
12
+
13
+ // src/config/index.ts
14
+ import { mkdirSync } from "fs";
15
+ import { homedir } from "os";
16
+ import { join } from "path";
17
+ function ensureDir(dir) {
18
+ mkdirSync(dir, { recursive: true });
19
+ }
20
+ function loadConfig() {
21
+ const dataDir = process.env["TASK_DATA_DIR"] ?? join(homedir(), ".task");
22
+ ensureDir(dataDir);
23
+ const logDir = process.env["TASK_LOG_DIR"] ?? join(dataDir, "logs");
24
+ ensureDir(logDir);
25
+ return {
26
+ dbPath: process.env["TASK_DB_PATH"] ?? join(dataDir, "data.db"),
27
+ logDir,
28
+ logLevel: process.env["TASK_LOG_LEVEL"] ?? "info",
29
+ otelEndpoint: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"]
30
+ };
31
+ }
32
+
33
+ // src/db/connection.ts
34
+ import Database from "better-sqlite3";
35
+ function createDatabase(dbPath) {
36
+ const db = new Database(dbPath);
37
+ db.pragma("journal_mode = WAL");
38
+ db.pragma("foreign_keys = ON");
39
+ return db;
40
+ }
41
+
42
+ // src/db/migrator.ts
43
+ import { readdirSync, readFileSync } from "fs";
44
+ import { dirname, join as join2 } from "path";
45
+ import { fileURLToPath } from "url";
46
+ var migrationsDir = join2(dirname(fileURLToPath(import.meta.url)), "migrations");
47
+ function loadMigrations() {
48
+ const files = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
49
+ return files.map((name) => ({
50
+ name,
51
+ sql: readFileSync(join2(migrationsDir, name), "utf-8")
52
+ }));
53
+ }
54
+ function runMigrations(db) {
55
+ db.exec(`
56
+ CREATE TABLE IF NOT EXISTS _migrations (
57
+ name TEXT PRIMARY KEY,
58
+ applied_at TEXT NOT NULL
59
+ );
60
+ `);
61
+ const applied = new Set(
62
+ db.prepare("SELECT name FROM _migrations").all().map(
63
+ (r) => r.name
64
+ )
65
+ );
66
+ const migrations = loadMigrations();
67
+ for (const migration of migrations) {
68
+ if (applied.has(migration.name)) continue;
69
+ logger.info(`Applying migration: ${migration.name}`);
70
+ const migrate = db.transaction(() => {
71
+ db.exec(migration.sql);
72
+ db.prepare("INSERT INTO _migrations (name, applied_at) VALUES (?, ?)").run(
73
+ migration.name,
74
+ (/* @__PURE__ */ new Date()).toISOString()
75
+ );
76
+ });
77
+ migrate();
78
+ logger.info(`Migration applied: ${migration.name}`);
79
+ }
80
+ }
81
+
82
+ // src/logging/telemetry.ts
83
+ import { NodeSDK } from "@opentelemetry/sdk-node";
84
+ import { BatchSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base";
85
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
86
+ var sdk;
87
+ function initTelemetry(config) {
88
+ const spanProcessor = config.otelEndpoint ? new BatchSpanProcessor(new OTLPTraceExporter({ url: config.otelEndpoint })) : config.logLevel === "debug" ? new BatchSpanProcessor(new ConsoleSpanExporter()) : void 0;
89
+ sdk = new NodeSDK({
90
+ serviceName: "task",
91
+ spanProcessors: spanProcessor ? [spanProcessor] : []
92
+ });
93
+ sdk.start();
94
+ }
95
+ async function shutdownTelemetry() {
96
+ if (sdk) {
97
+ await sdk.shutdown();
98
+ }
99
+ }
100
+
101
+ // src/types/common.ts
102
+ function ok(value) {
103
+ return { ok: true, value };
104
+ }
105
+ function err(error) {
106
+ return { ok: false, error };
107
+ }
108
+
109
+ // src/errors/app-error.ts
110
+ var AppError = class extends Error {
111
+ constructor(code, message, cause) {
112
+ super(message);
113
+ this.code = code;
114
+ this.cause = cause;
115
+ this.name = "AppError";
116
+ }
117
+ code;
118
+ cause;
119
+ };
120
+
121
+ // src/repository/project.repository.ts
122
+ import { ulid } from "ulid";
123
+
124
+ // src/repository/shared.ts
125
+ var NOT_DELETED = "deleted_at IS NULL";
126
+ function rowToTask(row) {
127
+ return {
128
+ id: row.id,
129
+ projectId: row.project_id,
130
+ parentId: row.parent_id,
131
+ name: row.name,
132
+ description: row.description,
133
+ type: row.type,
134
+ status: row.status,
135
+ rank: row.rank,
136
+ technicalNotes: row.technical_notes,
137
+ additionalRequirements: row.additional_requirements,
138
+ createdAt: row.created_at,
139
+ updatedAt: row.updated_at
140
+ };
141
+ }
142
+
143
+ // src/repository/project.repository.ts
144
+ function rowToProject(row) {
145
+ return {
146
+ id: row.id,
147
+ key: row.key,
148
+ name: row.name,
149
+ description: row.description,
150
+ isDefault: row.is_default === 1,
151
+ createdAt: row.created_at,
152
+ updatedAt: row.updated_at
153
+ };
154
+ }
155
+ var SqliteProjectRepository = class {
156
+ constructor(db) {
157
+ this.db = db;
158
+ }
159
+ db;
160
+ insert(input) {
161
+ return logger.startSpan("ProjectRepository.insert", () => {
162
+ try {
163
+ const now = (/* @__PURE__ */ new Date()).toISOString();
164
+ const id = ulid();
165
+ if (input.isDefault) {
166
+ this.db.prepare("UPDATE projects SET is_default = 0 WHERE is_default = 1").run();
167
+ }
168
+ this.db.prepare(
169
+ `INSERT INTO projects (id, key, name, description, is_default, created_at, updated_at)
170
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
171
+ ).run(
172
+ id,
173
+ input.key,
174
+ input.name,
175
+ input.description ?? "",
176
+ input.isDefault ? 1 : 0,
177
+ now,
178
+ now
179
+ );
180
+ const row = this.db.prepare("SELECT * FROM projects WHERE id = ?").get(id);
181
+ if (!row) {
182
+ return err(new AppError("DB_ERROR", "Failed to retrieve inserted project"));
183
+ }
184
+ return ok(rowToProject(row));
185
+ } catch (e) {
186
+ if (e instanceof Error && e.message.includes("UNIQUE constraint")) {
187
+ return err(new AppError("DUPLICATE", `Project name already exists: ${input.name}`, e));
188
+ }
189
+ return err(new AppError("DB_ERROR", "Failed to insert project", e));
190
+ }
191
+ });
192
+ }
193
+ findById(id) {
194
+ try {
195
+ const row = this.db.prepare(`SELECT * FROM projects WHERE id = ? AND ${NOT_DELETED}`).get(id);
196
+ return ok(row ? rowToProject(row) : null);
197
+ } catch (e) {
198
+ return err(new AppError("DB_ERROR", "Failed to find project by id", e));
199
+ }
200
+ }
201
+ findByKey(key) {
202
+ try {
203
+ const row = this.db.prepare(`SELECT * FROM projects WHERE key = ? AND ${NOT_DELETED}`).get(key);
204
+ return ok(row ? rowToProject(row) : null);
205
+ } catch (e) {
206
+ return err(new AppError("DB_ERROR", "Failed to find project by key", e));
207
+ }
208
+ }
209
+ findByName(name) {
210
+ try {
211
+ const row = this.db.prepare(`SELECT * FROM projects WHERE name = ? AND ${NOT_DELETED}`).get(name);
212
+ return ok(row ? rowToProject(row) : null);
213
+ } catch (e) {
214
+ return err(new AppError("DB_ERROR", "Failed to find project by name", e));
215
+ }
216
+ }
217
+ findDefault() {
218
+ try {
219
+ const row = this.db.prepare(`SELECT * FROM projects WHERE is_default = 1 AND ${NOT_DELETED}`).get();
220
+ return ok(row ? rowToProject(row) : null);
221
+ } catch (e) {
222
+ return err(new AppError("DB_ERROR", "Failed to find default project", e));
223
+ }
224
+ }
225
+ findAll() {
226
+ try {
227
+ const rows = this.db.prepare(`SELECT * FROM projects WHERE ${NOT_DELETED} ORDER BY created_at DESC`).all();
228
+ return ok(rows.map(rowToProject));
229
+ } catch (e) {
230
+ return err(new AppError("DB_ERROR", "Failed to list projects", e));
231
+ }
232
+ }
233
+ update(id, input) {
234
+ return logger.startSpan("ProjectRepository.update", () => {
235
+ try {
236
+ const existing = this.db.prepare(`SELECT * FROM projects WHERE id = ? AND ${NOT_DELETED}`).get(id);
237
+ if (!existing) {
238
+ return err(new AppError("NOT_FOUND", `Project not found: ${id}`));
239
+ }
240
+ const now = (/* @__PURE__ */ new Date()).toISOString();
241
+ if (input.isDefault) {
242
+ this.db.prepare("UPDATE projects SET is_default = 0 WHERE is_default = 1").run();
243
+ }
244
+ this.db.prepare(
245
+ `UPDATE projects SET
246
+ name = ?, description = ?, is_default = ?, updated_at = ?
247
+ WHERE id = ?`
248
+ ).run(
249
+ input.name ?? existing.name,
250
+ input.description ?? existing.description,
251
+ input.isDefault !== void 0 ? input.isDefault ? 1 : 0 : existing.is_default,
252
+ now,
253
+ id
254
+ );
255
+ const row = this.db.prepare("SELECT * FROM projects WHERE id = ?").get(id);
256
+ if (!row) {
257
+ return err(new AppError("DB_ERROR", "Failed to retrieve updated project"));
258
+ }
259
+ return ok(rowToProject(row));
260
+ } catch (e) {
261
+ if (e instanceof Error && e.message.includes("UNIQUE constraint")) {
262
+ return err(new AppError("DUPLICATE", `Project name already exists`, e));
263
+ }
264
+ return err(new AppError("DB_ERROR", "Failed to update project", e));
265
+ }
266
+ });
267
+ }
268
+ delete(id) {
269
+ return logger.startSpan("ProjectRepository.delete", () => {
270
+ try {
271
+ const existing = this.db.prepare(`SELECT * FROM projects WHERE id = ? AND ${NOT_DELETED}`).get(id);
272
+ if (!existing) {
273
+ return err(new AppError("NOT_FOUND", `Project not found: ${id}`));
274
+ }
275
+ const now = (/* @__PURE__ */ new Date()).toISOString();
276
+ this.db.prepare("UPDATE projects SET deleted_at = ?, updated_at = ? WHERE id = ?").run(now, now, id);
277
+ this.db.prepare(
278
+ "UPDATE tasks SET deleted_at = ?, updated_at = ? WHERE project_id = ? AND deleted_at IS NULL"
279
+ ).run(now, now, id);
280
+ return ok(void 0);
281
+ } catch (e) {
282
+ return err(new AppError("DB_ERROR", "Failed to delete project", e));
283
+ }
284
+ });
285
+ }
286
+ incrementTaskCounter(id) {
287
+ return logger.startSpan("ProjectRepository.incrementTaskCounter", () => {
288
+ try {
289
+ this.db.prepare(
290
+ `UPDATE projects SET task_counter = task_counter + 1 WHERE id = ? AND ${NOT_DELETED}`
291
+ ).run(id);
292
+ const row = this.db.prepare(`SELECT task_counter FROM projects WHERE id = ? AND ${NOT_DELETED}`).get(id);
293
+ if (!row) {
294
+ return err(new AppError("NOT_FOUND", `Project not found: ${id}`));
295
+ }
296
+ return ok(row.task_counter);
297
+ } catch (e) {
298
+ return err(new AppError("DB_ERROR", "Failed to increment task counter", e));
299
+ }
300
+ });
301
+ }
302
+ };
303
+
304
+ // src/repository/task.repository.ts
305
+ var TERMINAL_STATUS_ARRAY = [...TERMINAL_STATUSES];
306
+ var TERMINAL_PLACEHOLDERS = TERMINAL_STATUS_ARRAY.map(() => "?").join(", ");
307
+ var SqliteTaskRepository = class {
308
+ constructor(db) {
309
+ this.db = db;
310
+ }
311
+ db;
312
+ insert(id, input) {
313
+ return logger.startSpan("TaskRepository.insert", () => {
314
+ try {
315
+ const now = (/* @__PURE__ */ new Date()).toISOString();
316
+ const maxActiveResult = this.getMaxActiveRank(input.projectId);
317
+ if (!maxActiveResult.ok) return maxActiveResult;
318
+ const maxActiveRank = maxActiveResult.value;
319
+ const minTerminalResult = this.getMinTerminalRank(input.projectId);
320
+ if (!minTerminalResult.ok) return minTerminalResult;
321
+ const minTerminalRank = minTerminalResult.value;
322
+ let rank;
323
+ if (minTerminalRank !== null && minTerminalRank > maxActiveRank) {
324
+ rank = maxActiveRank > 0 ? (maxActiveRank + minTerminalRank) / 2 : minTerminalRank - RANK_GAP;
325
+ } else {
326
+ rank = maxActiveRank + RANK_GAP;
327
+ }
328
+ this.db.prepare(
329
+ `INSERT INTO tasks (id, project_id, parent_id, name, description, type, status, rank, technical_notes, additional_requirements, created_at, updated_at)
330
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
331
+ ).run(
332
+ id,
333
+ input.projectId,
334
+ input.parentId ?? null,
335
+ input.name,
336
+ input.description ?? "",
337
+ input.type,
338
+ input.status,
339
+ rank,
340
+ input.technicalNotes ?? "",
341
+ input.additionalRequirements ?? "",
342
+ now,
343
+ now
344
+ );
345
+ const row = this.db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
346
+ if (!row) {
347
+ return err(new AppError("DB_ERROR", "Failed to retrieve inserted task"));
348
+ }
349
+ return ok(rowToTask(row));
350
+ } catch (e) {
351
+ return err(new AppError("DB_ERROR", "Failed to insert task", e));
352
+ }
353
+ });
354
+ }
355
+ findById(id) {
356
+ try {
357
+ const row = this.db.prepare(`SELECT * FROM tasks WHERE id = ? AND ${NOT_DELETED}`).get(id);
358
+ return ok(row ? rowToTask(row) : null);
359
+ } catch (e) {
360
+ return err(new AppError("DB_ERROR", "Failed to find task by id", e));
361
+ }
362
+ }
363
+ findMany(filter) {
364
+ try {
365
+ const conditions = [NOT_DELETED];
366
+ const params = [];
367
+ if (filter.projectId) {
368
+ conditions.push("project_id = ?");
369
+ params.push(filter.projectId);
370
+ }
371
+ if (filter.status) {
372
+ conditions.push("status = ?");
373
+ params.push(filter.status);
374
+ }
375
+ if (filter.type) {
376
+ conditions.push("type = ?");
377
+ params.push(filter.type);
378
+ }
379
+ if (filter.parentId) {
380
+ conditions.push("parent_id = ?");
381
+ params.push(filter.parentId);
382
+ }
383
+ if (filter.search) {
384
+ const ftsQuery = filter.search.trim().split(/\s+/).map((term) => `"${term.replace(/"/g, '""')}"*`).join(" ");
385
+ conditions.push(`id IN (SELECT id FROM tasks_fts WHERE tasks_fts MATCH ?)`);
386
+ params.push(ftsQuery);
387
+ }
388
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
389
+ const sql = `SELECT * FROM tasks ${where} ORDER BY rank ASC`;
390
+ const rows = this.db.prepare(sql).all(...params);
391
+ return ok(rows.map(rowToTask));
392
+ } catch (e) {
393
+ return err(new AppError("DB_ERROR", "Failed to list tasks", e));
394
+ }
395
+ }
396
+ update(id, input) {
397
+ return logger.startSpan("TaskRepository.update", () => {
398
+ try {
399
+ const existing = this.db.prepare(`SELECT * FROM tasks WHERE id = ? AND ${NOT_DELETED}`).get(id);
400
+ if (!existing) {
401
+ return err(new AppError("NOT_FOUND", `Task not found: ${id}`));
402
+ }
403
+ const now = (/* @__PURE__ */ new Date()).toISOString();
404
+ let technicalNotes = input.technicalNotes ?? existing.technical_notes;
405
+ if (input.appendNotes) {
406
+ technicalNotes = existing.technical_notes + (existing.technical_notes ? `
407
+
408
+ ---
409
+ _${now}_
410
+
411
+ ` : "") + input.appendNotes;
412
+ }
413
+ let additionalRequirements = input.additionalRequirements ?? existing.additional_requirements;
414
+ if (input.appendRequirements) {
415
+ additionalRequirements = existing.additional_requirements + (existing.additional_requirements ? `
416
+
417
+ ---
418
+ _${now}_
419
+
420
+ ` : "") + input.appendRequirements;
421
+ }
422
+ this.db.prepare(
423
+ `UPDATE tasks SET
424
+ name = ?, description = ?, type = ?, status = ?,
425
+ parent_id = ?, technical_notes = ?, additional_requirements = ?, updated_at = ?
426
+ WHERE id = ?`
427
+ ).run(
428
+ input.name ?? existing.name,
429
+ input.description ?? existing.description,
430
+ input.type ?? existing.type,
431
+ input.status ?? existing.status,
432
+ input.parentId !== void 0 ? input.parentId : existing.parent_id,
433
+ technicalNotes,
434
+ additionalRequirements,
435
+ now,
436
+ id
437
+ );
438
+ const row = this.db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
439
+ if (!row) {
440
+ return err(new AppError("DB_ERROR", "Failed to retrieve updated task"));
441
+ }
442
+ return ok(rowToTask(row));
443
+ } catch (e) {
444
+ return err(new AppError("DB_ERROR", "Failed to update task", e));
445
+ }
446
+ });
447
+ }
448
+ delete(id) {
449
+ return logger.startSpan("TaskRepository.delete", () => {
450
+ try {
451
+ const existing = this.db.prepare(`SELECT * FROM tasks WHERE id = ? AND ${NOT_DELETED}`).get(id);
452
+ if (!existing) {
453
+ return err(new AppError("NOT_FOUND", `Task not found: ${id}`));
454
+ }
455
+ const now = (/* @__PURE__ */ new Date()).toISOString();
456
+ this.db.prepare("UPDATE tasks SET deleted_at = ?, updated_at = ? WHERE id = ?").run(now, now, id);
457
+ return ok(void 0);
458
+ } catch (e) {
459
+ return err(new AppError("DB_ERROR", "Failed to delete task", e));
460
+ }
461
+ });
462
+ }
463
+ rerank(taskId, newRank) {
464
+ return logger.startSpan("TaskRepository.rerank", () => {
465
+ try {
466
+ const now = (/* @__PURE__ */ new Date()).toISOString();
467
+ const existing = this.db.prepare(`SELECT * FROM tasks WHERE id = ? AND ${NOT_DELETED}`).get(taskId);
468
+ if (!existing) {
469
+ return err(new AppError("NOT_FOUND", `Task not found: ${taskId}`));
470
+ }
471
+ this.db.prepare("UPDATE tasks SET rank = ?, updated_at = ? WHERE id = ?").run(newRank, now, taskId);
472
+ const row = this.db.prepare("SELECT * FROM tasks WHERE id = ?").get(taskId);
473
+ if (!row) {
474
+ return err(new AppError("DB_ERROR", "Failed to retrieve reranked task"));
475
+ }
476
+ return ok(rowToTask(row));
477
+ } catch (e) {
478
+ return err(new AppError("DB_ERROR", "Failed to rerank task", e));
479
+ }
480
+ });
481
+ }
482
+ getMaxRank(projectId) {
483
+ try {
484
+ const row = this.db.prepare(`SELECT MAX(rank) as max_rank FROM tasks WHERE project_id = ? AND ${NOT_DELETED}`).get(projectId);
485
+ return ok(row?.max_rank ?? 0);
486
+ } catch (e) {
487
+ return err(new AppError("DB_ERROR", "Failed to get max rank", e));
488
+ }
489
+ }
490
+ getMaxActiveRank(projectId) {
491
+ try {
492
+ const row = this.db.prepare(
493
+ `SELECT MAX(rank) as max_rank FROM tasks WHERE project_id = ? AND ${NOT_DELETED} AND status NOT IN (${TERMINAL_PLACEHOLDERS})`
494
+ ).get(projectId, ...TERMINAL_STATUS_ARRAY);
495
+ return ok(row?.max_rank ?? 0);
496
+ } catch (e) {
497
+ return err(new AppError("DB_ERROR", "Failed to get max active rank", e));
498
+ }
499
+ }
500
+ getMinTerminalRank(projectId) {
501
+ try {
502
+ const row = this.db.prepare(
503
+ `SELECT MIN(rank) as min_rank FROM tasks WHERE project_id = ? AND ${NOT_DELETED} AND status IN (${TERMINAL_PLACEHOLDERS})`
504
+ ).get(projectId, ...TERMINAL_STATUS_ARRAY);
505
+ return ok(row?.min_rank ?? null);
506
+ } catch (e) {
507
+ return err(new AppError("DB_ERROR", "Failed to get min terminal rank", e));
508
+ }
509
+ }
510
+ getRankedTasks(projectId, status) {
511
+ try {
512
+ let sql;
513
+ let params;
514
+ if (status) {
515
+ sql = `SELECT * FROM tasks WHERE project_id = ? AND ${NOT_DELETED} AND status = ? ORDER BY rank ASC`;
516
+ params = [projectId, status];
517
+ } else {
518
+ sql = `SELECT * FROM tasks WHERE project_id = ? AND ${NOT_DELETED} ORDER BY rank ASC`;
519
+ params = [projectId];
520
+ }
521
+ const rows = this.db.prepare(sql).all(...params);
522
+ return ok(rows.map(rowToTask));
523
+ } catch (e) {
524
+ return err(new AppError("DB_ERROR", "Failed to get ranked tasks", e));
525
+ }
526
+ }
527
+ search(query, projectId) {
528
+ return logger.startSpan("TaskRepository.search", () => {
529
+ try {
530
+ const ftsQuery = query.trim().split(/\s+/).map((term) => `"${term.replace(/"/g, '""')}"*`).join(" ");
531
+ let sql;
532
+ let params;
533
+ if (projectId) {
534
+ sql = `SELECT t.*, bm25(tasks_fts) AS fts_rank
535
+ FROM tasks_fts f
536
+ JOIN tasks t ON t.id = f.id AND t.deleted_at IS NULL
537
+ WHERE tasks_fts MATCH ? AND t.project_id = ?
538
+ ORDER BY fts_rank ASC`;
539
+ params = [ftsQuery, projectId];
540
+ } else {
541
+ sql = `SELECT t.*, bm25(tasks_fts) AS fts_rank
542
+ FROM tasks_fts f
543
+ JOIN tasks t ON t.id = f.id AND t.deleted_at IS NULL
544
+ WHERE tasks_fts MATCH ?
545
+ ORDER BY fts_rank ASC`;
546
+ params = [ftsQuery];
547
+ }
548
+ const rows = this.db.prepare(sql).all(...params);
549
+ return ok(
550
+ rows.map((row) => ({
551
+ task: rowToTask(row),
552
+ rank: row.fts_rank
553
+ }))
554
+ );
555
+ } catch (e) {
556
+ return err(new AppError("DB_ERROR", "Full-text search failed", e));
557
+ }
558
+ });
559
+ }
560
+ };
561
+
562
+ // src/repository/dependency.repository.ts
563
+ function rowToDependency(row) {
564
+ return {
565
+ taskId: row.task_id,
566
+ dependsOnId: row.depends_on_id,
567
+ type: row.type,
568
+ createdAt: row.created_at
569
+ };
570
+ }
571
+ var SqliteDependencyRepository = class {
572
+ constructor(db) {
573
+ this.db = db;
574
+ }
575
+ db;
576
+ insert(taskId, dependsOnId, type) {
577
+ return logger.startSpan("DependencyRepository.insert", () => {
578
+ try {
579
+ const now = (/* @__PURE__ */ new Date()).toISOString();
580
+ this.db.prepare(
581
+ `INSERT INTO task_dependencies (task_id, depends_on_id, type, created_at)
582
+ VALUES (?, ?, ?, ?)`
583
+ ).run(taskId, dependsOnId, type, now);
584
+ const row = this.db.prepare("SELECT * FROM task_dependencies WHERE task_id = ? AND depends_on_id = ?").get(taskId, dependsOnId);
585
+ if (!row) {
586
+ return err(new AppError("DB_ERROR", "Failed to retrieve inserted dependency"));
587
+ }
588
+ return ok(rowToDependency(row));
589
+ } catch (e) {
590
+ if (e instanceof Error && e.message.includes("UNIQUE constraint")) {
591
+ return err(
592
+ new AppError("DUPLICATE", `Dependency already exists: ${taskId} -> ${dependsOnId}`, e)
593
+ );
594
+ }
595
+ if (e instanceof Error && e.message.includes("FOREIGN KEY constraint")) {
596
+ return err(new AppError("NOT_FOUND", "One or both tasks do not exist", e));
597
+ }
598
+ if (e instanceof Error && e.message.includes("CHECK constraint")) {
599
+ return err(new AppError("VALIDATION", "A task cannot depend on itself", e));
600
+ }
601
+ return err(new AppError("DB_ERROR", "Failed to insert dependency", e));
602
+ }
603
+ });
604
+ }
605
+ delete(taskId, dependsOnId) {
606
+ return logger.startSpan("DependencyRepository.delete", () => {
607
+ try {
608
+ const existing = this.db.prepare("SELECT * FROM task_dependencies WHERE task_id = ? AND depends_on_id = ?").get(taskId, dependsOnId);
609
+ if (!existing) {
610
+ return err(
611
+ new AppError("NOT_FOUND", `Dependency not found: ${taskId} -> ${dependsOnId}`)
612
+ );
613
+ }
614
+ this.db.prepare("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on_id = ?").run(taskId, dependsOnId);
615
+ return ok(void 0);
616
+ } catch (e) {
617
+ return err(new AppError("DB_ERROR", "Failed to delete dependency", e));
618
+ }
619
+ });
620
+ }
621
+ findByTask(taskId) {
622
+ try {
623
+ const rows = this.db.prepare("SELECT * FROM task_dependencies WHERE task_id = ? ORDER BY created_at ASC").all(taskId);
624
+ return ok(rows.map(rowToDependency));
625
+ } catch (e) {
626
+ return err(new AppError("DB_ERROR", "Failed to find dependencies for task", e));
627
+ }
628
+ }
629
+ findDependents(taskId) {
630
+ try {
631
+ const rows = this.db.prepare("SELECT * FROM task_dependencies WHERE depends_on_id = ? ORDER BY created_at ASC").all(taskId);
632
+ return ok(rows.map(rowToDependency));
633
+ } catch (e) {
634
+ return err(new AppError("DB_ERROR", "Failed to find dependents for task", e));
635
+ }
636
+ }
637
+ getBlockers(taskId) {
638
+ try {
639
+ const rows = this.db.prepare(
640
+ `SELECT t.* FROM tasks t
641
+ JOIN task_dependencies td ON t.id = td.depends_on_id
642
+ WHERE td.task_id = ? AND td.type = 'blocks' AND t.deleted_at IS NULL
643
+ ORDER BY t.rank ASC`
644
+ ).all(taskId);
645
+ return ok(rows.map(rowToTask));
646
+ } catch (e) {
647
+ return err(new AppError("DB_ERROR", "Failed to get blockers", e));
648
+ }
649
+ }
650
+ getDependents(taskId) {
651
+ try {
652
+ const rows = this.db.prepare(
653
+ `SELECT t.* FROM tasks t
654
+ JOIN task_dependencies td ON t.id = td.task_id
655
+ WHERE td.depends_on_id = ? AND td.type = 'blocks' AND t.deleted_at IS NULL
656
+ ORDER BY t.rank ASC`
657
+ ).all(taskId);
658
+ return ok(rows.map(rowToTask));
659
+ } catch (e) {
660
+ return err(new AppError("DB_ERROR", "Failed to get dependents", e));
661
+ }
662
+ }
663
+ getRelated(taskId) {
664
+ try {
665
+ const rows = this.db.prepare(
666
+ `SELECT DISTINCT t.* FROM tasks t
667
+ JOIN task_dependencies td ON (
668
+ (td.task_id = ? AND td.depends_on_id = t.id) OR
669
+ (td.depends_on_id = ? AND td.task_id = t.id)
670
+ )
671
+ WHERE td.type = 'relates-to' AND t.deleted_at IS NULL
672
+ ORDER BY t.rank ASC`
673
+ ).all(taskId, taskId);
674
+ return ok(rows.map(rowToTask));
675
+ } catch (e) {
676
+ return err(new AppError("DB_ERROR", "Failed to get related tasks", e));
677
+ }
678
+ }
679
+ getDuplicates(taskId) {
680
+ try {
681
+ const rows = this.db.prepare(
682
+ `SELECT DISTINCT t.* FROM tasks t
683
+ JOIN task_dependencies td ON (
684
+ (td.task_id = ? AND td.depends_on_id = t.id) OR
685
+ (td.depends_on_id = ? AND td.task_id = t.id)
686
+ )
687
+ WHERE td.type = 'duplicates' AND t.deleted_at IS NULL
688
+ ORDER BY t.rank ASC`
689
+ ).all(taskId, taskId);
690
+ return ok(rows.map(rowToTask));
691
+ } catch (e) {
692
+ return err(new AppError("DB_ERROR", "Failed to get duplicate tasks", e));
693
+ }
694
+ }
695
+ getTransitiveClosure(taskId) {
696
+ try {
697
+ const rows = this.db.prepare(
698
+ `WITH RECURSIVE transitive_deps(id) AS (
699
+ SELECT depends_on_id FROM task_dependencies WHERE task_id = ?
700
+ UNION
701
+ SELECT td.depends_on_id FROM task_dependencies td
702
+ JOIN transitive_deps d ON td.task_id = d.id
703
+ )
704
+ SELECT t.* FROM tasks t
705
+ WHERE t.id IN (SELECT id FROM transitive_deps) AND t.deleted_at IS NULL
706
+ ORDER BY t.rank ASC`
707
+ ).all(taskId);
708
+ return ok(rows.map(rowToTask));
709
+ } catch (e) {
710
+ return err(new AppError("DB_ERROR", "Failed to get transitive closure", e));
711
+ }
712
+ }
713
+ wouldCreateCycle(taskId, dependsOnId) {
714
+ try {
715
+ const row = this.db.prepare(
716
+ `WITH RECURSIVE reachable(id) AS (
717
+ SELECT depends_on_id FROM task_dependencies WHERE task_id = ?
718
+ UNION
719
+ SELECT td.depends_on_id FROM task_dependencies td
720
+ JOIN reachable r ON td.task_id = r.id
721
+ )
722
+ SELECT 1 AS found FROM reachable WHERE id = ? LIMIT 1`
723
+ ).get(dependsOnId, taskId);
724
+ return ok(row !== void 0);
725
+ } catch (e) {
726
+ return err(new AppError("DB_ERROR", "Failed to check for cycle", e));
727
+ }
728
+ }
729
+ };
730
+
731
+ // src/types/project.ts
732
+ import { z } from "zod/v4";
733
+ var CreateProjectSchema = z.object({
734
+ name: z.string().min(1, "Project name is required").max(255),
735
+ key: z.string().min(2, "Project key must be at least 2 characters").max(7, "Project key must be at most 7 characters").regex(/^[A-Za-z0-9]+$/, "Project key must contain only letters and digits").transform((v) => v.toUpperCase()).optional(),
736
+ description: z.string().max(5e3).optional(),
737
+ isDefault: z.boolean().optional()
738
+ });
739
+ var UpdateProjectSchema = z.object({
740
+ name: z.string().min(1).max(255).optional(),
741
+ description: z.string().max(5e3).optional(),
742
+ isDefault: z.boolean().optional()
743
+ });
744
+
745
+ // src/service/project.service.ts
746
+ var ProjectServiceImpl = class {
747
+ constructor(repo) {
748
+ this.repo = repo;
749
+ }
750
+ repo;
751
+ createProject(input) {
752
+ return logger.startSpan("ProjectService.createProject", () => {
753
+ const parsed = CreateProjectSchema.safeParse(input);
754
+ if (!parsed.success) {
755
+ return err(new AppError("VALIDATION", parsed.error.message));
756
+ }
757
+ const key = parsed.data.key ?? this.generateKey(parsed.data.name);
758
+ const keyError = this.validateKey(key);
759
+ if (keyError) {
760
+ return err(new AppError("VALIDATION", keyError));
761
+ }
762
+ const existingResult = this.repo.findByKey(key);
763
+ if (!existingResult.ok) return existingResult;
764
+ if (existingResult.value) {
765
+ return err(new AppError("DUPLICATE", `Project key already exists: ${key}`));
766
+ }
767
+ return this.repo.insert({ ...parsed.data, key });
768
+ });
769
+ }
770
+ generateKey(name) {
771
+ return name.replace(/[^A-Za-z0-9]/g, "").slice(0, 3).toUpperCase();
772
+ }
773
+ validateKey(key) {
774
+ if (key.length < 2 || key.length > 7) {
775
+ return `Project key must be 2-7 characters, got ${key.length}. Provide a --key explicitly.`;
776
+ }
777
+ if (!/^[A-Z0-9]+$/.test(key)) {
778
+ return "Project key must contain only uppercase letters and digits.";
779
+ }
780
+ return null;
781
+ }
782
+ listProjects() {
783
+ return this.repo.findAll();
784
+ }
785
+ getProject(id) {
786
+ return logger.startSpan("ProjectService.getProject", () => {
787
+ const result = this.repo.findById(id);
788
+ if (!result.ok) return result;
789
+ if (!result.value) {
790
+ return err(new AppError("NOT_FOUND", `Project not found: ${id}`));
791
+ }
792
+ return ok(result.value);
793
+ });
794
+ }
795
+ updateProject(id, input) {
796
+ return logger.startSpan("ProjectService.updateProject", () => {
797
+ const parsed = UpdateProjectSchema.safeParse(input);
798
+ if (!parsed.success) {
799
+ return err(new AppError("VALIDATION", parsed.error.message));
800
+ }
801
+ return this.repo.update(id, parsed.data);
802
+ });
803
+ }
804
+ deleteProject(id) {
805
+ return this.repo.delete(id);
806
+ }
807
+ resolveProject(idOrName) {
808
+ return logger.startSpan("ProjectService.resolveProject", () => {
809
+ if (idOrName) {
810
+ const byId = this.repo.findById(idOrName);
811
+ if (!byId.ok) return byId;
812
+ if (byId.value) return ok(byId.value);
813
+ const byKey = this.repo.findByKey(idOrName.toUpperCase());
814
+ if (!byKey.ok) return byKey;
815
+ if (byKey.value) return ok(byKey.value);
816
+ const byName = this.repo.findByName(idOrName);
817
+ if (!byName.ok) return byName;
818
+ if (byName.value) return ok(byName.value);
819
+ return err(new AppError("NOT_FOUND", `Project not found: ${idOrName}`));
820
+ }
821
+ const defaultProject = this.repo.findDefault();
822
+ if (!defaultProject.ok) return defaultProject;
823
+ if (defaultProject.value) return ok(defaultProject.value);
824
+ return err(
825
+ new AppError(
826
+ "NOT_FOUND",
827
+ "No project specified and no default project set. Create a project first."
828
+ )
829
+ );
830
+ });
831
+ }
832
+ nextTaskId(project) {
833
+ return logger.startSpan("ProjectService.nextTaskId", () => {
834
+ const counterResult = this.repo.incrementTaskCounter(project.id);
835
+ if (!counterResult.ok) return counterResult;
836
+ return ok(`${project.key}-${counterResult.value}`);
837
+ });
838
+ }
839
+ };
840
+
841
+ // src/types/task.ts
842
+ import { z as z2 } from "zod/v4";
843
+ var taskStatusValues = Object.values(TaskStatus);
844
+ var taskTypeValues = Object.values(TaskType);
845
+ var uiDepTypeValues = Object.values(UIDependencyType);
846
+ var DependencyEntrySchema = z2.object({
847
+ id: z2.string().min(1),
848
+ type: z2.enum(uiDepTypeValues).default(DependencyType.Blocks)
849
+ });
850
+ var CreateTaskSchema = z2.object({
851
+ name: z2.string().min(1, "Task name is required").max(500),
852
+ description: z2.string().max(1e4).optional(),
853
+ type: z2.enum(taskTypeValues).default(TaskType.Story),
854
+ status: z2.enum(taskStatusValues).default(TaskStatus.Backlog),
855
+ projectId: z2.string().optional(),
856
+ parentId: z2.string().optional(),
857
+ technicalNotes: z2.string().max(5e4).optional(),
858
+ additionalRequirements: z2.string().max(5e4).optional(),
859
+ dependsOn: z2.array(DependencyEntrySchema).optional()
860
+ });
861
+ var UpdateTaskSchema = z2.object({
862
+ name: z2.string().min(1).max(500).optional(),
863
+ description: z2.string().max(1e4).optional(),
864
+ type: z2.enum(taskTypeValues).optional(),
865
+ status: z2.enum(taskStatusValues).optional(),
866
+ parentId: z2.string().nullable().optional(),
867
+ technicalNotes: z2.string().max(5e4).optional(),
868
+ additionalRequirements: z2.string().max(5e4).optional(),
869
+ appendNotes: z2.string().max(5e4).optional(),
870
+ appendRequirements: z2.string().max(5e4).optional()
871
+ });
872
+ var TaskFilterSchema = z2.object({
873
+ projectId: z2.string().optional(),
874
+ status: z2.enum(taskStatusValues).optional(),
875
+ type: z2.enum(taskTypeValues).optional(),
876
+ parentId: z2.string().optional(),
877
+ search: z2.string().optional()
878
+ });
879
+ var RerankTaskSchema = z2.object({
880
+ taskId: z2.string().min(1, "Task id is required"),
881
+ afterId: z2.string().optional(),
882
+ beforeId: z2.string().optional(),
883
+ position: z2.number().int().min(1).optional()
884
+ });
885
+
886
+ // src/service/task.service.ts
887
+ var TaskServiceImpl = class {
888
+ constructor(repo, projectService, getDependencyService) {
889
+ this.repo = repo;
890
+ this.projectService = projectService;
891
+ this.getDependencyService = getDependencyService;
892
+ }
893
+ repo;
894
+ projectService;
895
+ getDependencyService;
896
+ createTask(input, projectIdOrName) {
897
+ return logger.startSpan("TaskService.createTask", () => {
898
+ const parsed = CreateTaskSchema.safeParse(input);
899
+ if (!parsed.success) {
900
+ return err(new AppError("VALIDATION", parsed.error.message));
901
+ }
902
+ const projectRef = parsed.data.projectId ?? projectIdOrName;
903
+ const projectResult = this.projectService.resolveProject(projectRef);
904
+ if (!projectResult.ok) return projectResult;
905
+ if (parsed.data.parentId) {
906
+ const parentResult = this.repo.findById(parsed.data.parentId);
907
+ if (!parentResult.ok) return parentResult;
908
+ if (!parentResult.value) {
909
+ return err(new AppError("NOT_FOUND", `Parent task not found: ${parsed.data.parentId}`));
910
+ }
911
+ }
912
+ const project = projectResult.value;
913
+ const taskIdResult = this.projectService.nextTaskId(project);
914
+ if (!taskIdResult.ok) return taskIdResult;
915
+ const insertResult = this.repo.insert(taskIdResult.value, {
916
+ ...parsed.data,
917
+ projectId: project.id
918
+ });
919
+ if (!insertResult.ok) return insertResult;
920
+ if (parsed.data.dependsOn && parsed.data.dependsOn.length > 0) {
921
+ for (const entry of parsed.data.dependsOn) {
922
+ const depResult = this.getDependencyService().addDependency({
923
+ taskId: insertResult.value.id,
924
+ dependsOnId: entry.id,
925
+ type: entry.type
926
+ });
927
+ if (!depResult.ok) return depResult;
928
+ }
929
+ }
930
+ return insertResult;
931
+ });
932
+ }
933
+ getTask(id) {
934
+ return logger.startSpan("TaskService.getTask", () => {
935
+ const result = this.repo.findById(id);
936
+ if (!result.ok) return result;
937
+ if (!result.value) {
938
+ return err(new AppError("NOT_FOUND", `Task not found: ${id}`));
939
+ }
940
+ return ok(result.value);
941
+ });
942
+ }
943
+ listTasks(filter) {
944
+ return logger.startSpan("TaskService.listTasks", () => {
945
+ const parsed = TaskFilterSchema.safeParse(filter);
946
+ if (!parsed.success) {
947
+ return err(new AppError("VALIDATION", parsed.error.message));
948
+ }
949
+ let resolvedFilter = parsed.data;
950
+ if (parsed.data.projectId) {
951
+ const projectResult = this.projectService.resolveProject(parsed.data.projectId);
952
+ if (!projectResult.ok) return projectResult;
953
+ resolvedFilter = { ...resolvedFilter, projectId: projectResult.value.id };
954
+ }
955
+ return this.repo.findMany(resolvedFilter);
956
+ });
957
+ }
958
+ updateTask(id, input) {
959
+ return logger.startSpan("TaskService.updateTask", () => {
960
+ const parsed = UpdateTaskSchema.safeParse(input);
961
+ if (!parsed.success) {
962
+ return err(new AppError("VALIDATION", parsed.error.message));
963
+ }
964
+ if (parsed.data.status === TaskStatus.InProgress) {
965
+ const blockersResult = this.getDependencyService().listBlockers(id);
966
+ if (!blockersResult.ok) return blockersResult;
967
+ const hasNonTerminalBlocker = blockersResult.value.some((b) => !isTerminalStatus(b.status));
968
+ if (hasNonTerminalBlocker) {
969
+ return err(
970
+ new AppError("VALIDATION", "Task is blocked by unfinished dependencies")
971
+ );
972
+ }
973
+ }
974
+ if (parsed.data.status && isTerminalStatus(parsed.data.status)) {
975
+ const existingResult = this.repo.findById(id);
976
+ if (!existingResult.ok) return existingResult;
977
+ if (!existingResult.value) {
978
+ return err(new AppError("NOT_FOUND", `Task not found: ${id}`));
979
+ }
980
+ const existing = existingResult.value;
981
+ const updateResult = this.repo.update(id, parsed.data);
982
+ if (!updateResult.ok) return updateResult;
983
+ if (!isTerminalStatus(existing.status)) {
984
+ const maxRankResult = this.repo.getMaxRank(existing.projectId);
985
+ if (!maxRankResult.ok) return maxRankResult;
986
+ return this.repo.rerank(id, maxRankResult.value + RANK_GAP);
987
+ }
988
+ return updateResult;
989
+ }
990
+ return this.repo.update(id, parsed.data);
991
+ });
992
+ }
993
+ deleteTask(id) {
994
+ return this.repo.delete(id);
995
+ }
996
+ breakdownTask(parentId, subtasks) {
997
+ return logger.startSpan("TaskService.breakdownTask", () => {
998
+ const parentResult = this.repo.findById(parentId);
999
+ if (!parentResult.ok) return parentResult;
1000
+ if (!parentResult.value) {
1001
+ return err(new AppError("NOT_FOUND", `Parent task not found: ${parentId}`));
1002
+ }
1003
+ const parent = parentResult.value;
1004
+ const projectResult = this.projectService.resolveProject(parent.projectId);
1005
+ if (!projectResult.ok) return projectResult;
1006
+ const project = projectResult.value;
1007
+ const created = [];
1008
+ for (const subtask of subtasks) {
1009
+ const parsed = CreateTaskSchema.safeParse(subtask);
1010
+ if (!parsed.success) {
1011
+ return err(new AppError("VALIDATION", `Invalid subtask: ${parsed.error.message}`));
1012
+ }
1013
+ const taskIdResult = this.projectService.nextTaskId(project);
1014
+ if (!taskIdResult.ok) return taskIdResult;
1015
+ const result = this.repo.insert(taskIdResult.value, {
1016
+ ...parsed.data,
1017
+ projectId: parent.projectId,
1018
+ parentId
1019
+ });
1020
+ if (!result.ok) return result;
1021
+ created.push(result.value);
1022
+ }
1023
+ return ok(created);
1024
+ });
1025
+ }
1026
+ /**
1027
+ * Re-rank a task using Jira-style ranking:
1028
+ * - afterId: place the task immediately after the given task
1029
+ * - beforeId: place the task immediately before the given task
1030
+ * - position: place the task at the given 1-based position in the backlog
1031
+ *
1032
+ * New rank is computed as the midpoint between neighbors.
1033
+ * If moving to the top, rank = first_rank - GAP.
1034
+ * If moving to the bottom, rank = last_rank + GAP.
1035
+ */
1036
+ rerankTask(input, projectIdOrName) {
1037
+ return logger.startSpan("TaskService.rerankTask", () => {
1038
+ const parsed = RerankTaskSchema.safeParse(input);
1039
+ if (!parsed.success) {
1040
+ return err(new AppError("VALIDATION", parsed.error.message));
1041
+ }
1042
+ const { taskId, afterId, beforeId, position } = parsed.data;
1043
+ const specifiedCount = [afterId, beforeId, position].filter((v) => v !== void 0).length;
1044
+ if (specifiedCount !== 1) {
1045
+ return err(
1046
+ new AppError(
1047
+ "VALIDATION",
1048
+ "Exactly one of --after, --before, or --position must be specified"
1049
+ )
1050
+ );
1051
+ }
1052
+ const taskResult = this.repo.findById(taskId);
1053
+ if (!taskResult.ok) return taskResult;
1054
+ if (!taskResult.value) {
1055
+ return err(new AppError("NOT_FOUND", `Task not found: ${taskId}`));
1056
+ }
1057
+ const task = taskResult.value;
1058
+ if (isTerminalStatus(task.status)) {
1059
+ return err(
1060
+ new AppError(
1061
+ "VALIDATION",
1062
+ `Cannot rerank a task with status '${task.status}'. Only active tasks can be reranked.`
1063
+ )
1064
+ );
1065
+ }
1066
+ const projectRef = projectIdOrName ?? task.projectId;
1067
+ const projectResult = this.projectService.resolveProject(projectRef);
1068
+ if (!projectResult.ok) return projectResult;
1069
+ const projectId = projectResult.value.id;
1070
+ const rankedResult = this.repo.getRankedTasks(projectId, TaskStatus.Backlog);
1071
+ if (!rankedResult.ok) return rankedResult;
1072
+ const ranked = rankedResult.value.filter((t) => t.id !== taskId);
1073
+ let newRank;
1074
+ if (afterId) {
1075
+ const anchor = ranked.find((t) => t.id === afterId);
1076
+ if (!anchor) {
1077
+ return err(new AppError("NOT_FOUND", `Anchor task not found in backlog: ${afterId}`));
1078
+ }
1079
+ const anchorIndex = ranked.indexOf(anchor);
1080
+ const next = ranked[anchorIndex + 1];
1081
+ newRank = next ? (anchor.rank + next.rank) / 2 : anchor.rank + RANK_GAP;
1082
+ } else if (beforeId) {
1083
+ const anchor = ranked.find((t) => t.id === beforeId);
1084
+ if (!anchor) {
1085
+ return err(new AppError("NOT_FOUND", `Anchor task not found in backlog: ${beforeId}`));
1086
+ }
1087
+ const anchorIndex = ranked.indexOf(anchor);
1088
+ const prev = ranked[anchorIndex - 1];
1089
+ newRank = prev ? (prev.rank + anchor.rank) / 2 : anchor.rank - RANK_GAP;
1090
+ } else {
1091
+ const pos = position;
1092
+ if (pos < 1) {
1093
+ return err(new AppError("VALIDATION", "Position must be >= 1"));
1094
+ }
1095
+ if (pos === 1) {
1096
+ const first = ranked[0];
1097
+ newRank = first ? first.rank - RANK_GAP : RANK_GAP;
1098
+ } else if (pos > ranked.length) {
1099
+ const last = ranked[ranked.length - 1];
1100
+ newRank = last ? last.rank + RANK_GAP : RANK_GAP;
1101
+ } else {
1102
+ const above = ranked[pos - 2];
1103
+ const below = ranked[pos - 1];
1104
+ if (!above || !below) {
1105
+ return err(new AppError("DB_ERROR", "Unexpected missing neighbor tasks"));
1106
+ }
1107
+ newRank = (above.rank + below.rank) / 2;
1108
+ }
1109
+ }
1110
+ const depService = this.getDependencyService();
1111
+ const blockersResult = depService.listBlockers(taskId);
1112
+ if (blockersResult.ok) {
1113
+ for (const blocker of blockersResult.value) {
1114
+ if (blocker.projectId === projectId && newRank < blocker.rank) {
1115
+ return err(
1116
+ new AppError(
1117
+ "VALIDATION",
1118
+ `Cannot rank above blocker "${blocker.id}" (${blocker.name}). Complete or remove the dependency first.`
1119
+ )
1120
+ );
1121
+ }
1122
+ }
1123
+ }
1124
+ const dependentsResult = depService.listDependents(taskId);
1125
+ if (dependentsResult.ok) {
1126
+ for (const dep of dependentsResult.value) {
1127
+ if (dep.projectId === projectId && newRank > dep.rank) {
1128
+ return err(
1129
+ new AppError(
1130
+ "VALIDATION",
1131
+ `Cannot rank below dependent "${dep.id}" (${dep.name}). Complete or remove the dependency first.`
1132
+ )
1133
+ );
1134
+ }
1135
+ }
1136
+ }
1137
+ return this.repo.rerank(taskId, newRank);
1138
+ });
1139
+ }
1140
+ searchTasks(query, projectIdOrName) {
1141
+ return logger.startSpan("TaskService.searchTasks", () => {
1142
+ if (!query.trim()) {
1143
+ return err(new AppError("VALIDATION", "Search query cannot be empty"));
1144
+ }
1145
+ let projectId;
1146
+ if (projectIdOrName) {
1147
+ const projectResult = this.projectService.resolveProject(projectIdOrName);
1148
+ if (!projectResult.ok) return projectResult;
1149
+ projectId = projectResult.value.id;
1150
+ }
1151
+ return this.repo.search(query, projectId);
1152
+ });
1153
+ }
1154
+ };
1155
+
1156
+ // src/types/dependency.ts
1157
+ import { z as z3 } from "zod/v4";
1158
+ var dbDepTypeValues = [
1159
+ DependencyType.Blocks,
1160
+ DependencyType.RelatesTo,
1161
+ DependencyType.Duplicates
1162
+ ];
1163
+ var AddDependencySchema = z3.object({
1164
+ taskId: z3.string().min(1, "Task id is required"),
1165
+ dependsOnId: z3.string().min(1, "Depends-on task id is required"),
1166
+ type: z3.enum(dbDepTypeValues).default(DependencyType.Blocks)
1167
+ });
1168
+ var RemoveDependencySchema = z3.object({
1169
+ taskId: z3.string().min(1, "Task id is required"),
1170
+ dependsOnId: z3.string().min(1, "Depends-on task id is required")
1171
+ });
1172
+
1173
+ // src/service/dependency.service.ts
1174
+ var DependencyServiceImpl = class {
1175
+ constructor(depRepo, taskRepo) {
1176
+ this.depRepo = depRepo;
1177
+ this.taskRepo = taskRepo;
1178
+ }
1179
+ depRepo;
1180
+ taskRepo;
1181
+ requireTask(taskId) {
1182
+ const result = this.taskRepo.findById(taskId);
1183
+ if (!result.ok) return result;
1184
+ if (!result.value) {
1185
+ return err(new AppError("NOT_FOUND", `Task not found: ${taskId}`));
1186
+ }
1187
+ return ok(result.value);
1188
+ }
1189
+ addDependency(input) {
1190
+ return logger.startSpan("DependencyService.addDependency", () => {
1191
+ const normalized = normalizeBlockedBy(input);
1192
+ const parsed = AddDependencySchema.safeParse(normalized);
1193
+ if (!parsed.success) {
1194
+ return err(new AppError("VALIDATION", parsed.error.message));
1195
+ }
1196
+ const { taskId, dependsOnId, type } = parsed.data;
1197
+ const taskResult = this.requireTask(taskId);
1198
+ if (!taskResult.ok) return taskResult;
1199
+ const depResult = this.requireTask(dependsOnId);
1200
+ if (!depResult.ok) return depResult;
1201
+ const cycleResult = this.depRepo.wouldCreateCycle(taskId, dependsOnId);
1202
+ if (!cycleResult.ok) return cycleResult;
1203
+ if (cycleResult.value) {
1204
+ return err(
1205
+ new AppError(
1206
+ "VALIDATION",
1207
+ `Adding this dependency would create a cycle: ${taskId} -> ${dependsOnId}`
1208
+ )
1209
+ );
1210
+ }
1211
+ return this.depRepo.insert(taskId, dependsOnId, type);
1212
+ });
1213
+ }
1214
+ removeDependency(input) {
1215
+ return logger.startSpan("DependencyService.removeDependency", () => {
1216
+ const parsed = RemoveDependencySchema.safeParse(input);
1217
+ if (!parsed.success) {
1218
+ return err(new AppError("VALIDATION", parsed.error.message));
1219
+ }
1220
+ return this.depRepo.delete(parsed.data.taskId, parsed.data.dependsOnId);
1221
+ });
1222
+ }
1223
+ removeDependencyBetween(taskId, otherId) {
1224
+ return logger.startSpan("DependencyService.removeDependencyBetween", () => {
1225
+ const forward = this.depRepo.delete(taskId, otherId);
1226
+ if (forward.ok) return forward;
1227
+ return this.depRepo.delete(otherId, taskId);
1228
+ });
1229
+ }
1230
+ listBlockers(taskId) {
1231
+ return this.depRepo.getBlockers(taskId);
1232
+ }
1233
+ listDependents(taskId) {
1234
+ return this.depRepo.getDependents(taskId);
1235
+ }
1236
+ listRelated(taskId) {
1237
+ return this.depRepo.getRelated(taskId);
1238
+ }
1239
+ listDuplicates(taskId) {
1240
+ return this.depRepo.getDuplicates(taskId);
1241
+ }
1242
+ listAllDeps(taskId) {
1243
+ return this.depRepo.findByTask(taskId);
1244
+ }
1245
+ getTransitiveDeps(taskId) {
1246
+ return this.depRepo.getTransitiveClosure(taskId);
1247
+ }
1248
+ buildGraph(taskId) {
1249
+ return logger.startSpan("DependencyService.buildGraph", () => {
1250
+ const taskResult = this.taskRepo.findById(taskId);
1251
+ if (!taskResult.ok) return taskResult;
1252
+ if (!taskResult.value) {
1253
+ return err(new AppError("NOT_FOUND", `Task not found: ${taskId}`));
1254
+ }
1255
+ const rootTask = taskResult.value;
1256
+ const visited = /* @__PURE__ */ new Map();
1257
+ const allEdges = [];
1258
+ const queue = [taskId];
1259
+ visited.set(taskId, rootTask);
1260
+ while (queue.length > 0) {
1261
+ const current = queue.shift();
1262
+ if (!current) break;
1263
+ const blockersResult = this.depRepo.findByTask(current);
1264
+ if (!blockersResult.ok) return blockersResult;
1265
+ for (const dep of blockersResult.value) {
1266
+ allEdges.push({ from: dep.taskId, to: dep.dependsOnId, type: dep.type });
1267
+ if (!visited.has(dep.dependsOnId)) {
1268
+ const t = this.taskRepo.findById(dep.dependsOnId);
1269
+ if (!t.ok) return t;
1270
+ if (t.value) {
1271
+ visited.set(dep.dependsOnId, t.value);
1272
+ queue.push(dep.dependsOnId);
1273
+ }
1274
+ }
1275
+ }
1276
+ const dependentsResult = this.depRepo.findDependents(current);
1277
+ if (!dependentsResult.ok) return dependentsResult;
1278
+ for (const dep of dependentsResult.value) {
1279
+ allEdges.push({ from: dep.taskId, to: dep.dependsOnId, type: dep.type });
1280
+ if (!visited.has(dep.taskId)) {
1281
+ const t = this.taskRepo.findById(dep.taskId);
1282
+ if (!t.ok) return t;
1283
+ if (t.value) {
1284
+ visited.set(dep.taskId, t.value);
1285
+ queue.push(dep.taskId);
1286
+ }
1287
+ }
1288
+ }
1289
+ }
1290
+ const edgeSet = /* @__PURE__ */ new Set();
1291
+ const uniqueEdges = [];
1292
+ for (const edge of allEdges) {
1293
+ const key = `${edge.from}->${edge.to}`;
1294
+ if (!edgeSet.has(key)) {
1295
+ edgeSet.add(key);
1296
+ uniqueEdges.push(edge);
1297
+ }
1298
+ }
1299
+ const nodes = Array.from(visited.values());
1300
+ const mermaid = this.toMermaid(nodes, uniqueEdges, taskId);
1301
+ return ok({ nodes, edges: uniqueEdges, mermaid });
1302
+ });
1303
+ }
1304
+ toMermaid(nodes, edges, highlightId) {
1305
+ const lines = ["graph LR"];
1306
+ for (const node of nodes) {
1307
+ const label = `${node.id}: ${node.name} [${node.status}]`;
1308
+ const escaped = label.replace(/"/g, "#quot;");
1309
+ if (node.id === highlightId) {
1310
+ lines.push(` ${node.id}("${escaped}"):::highlight`);
1311
+ } else {
1312
+ lines.push(` ${node.id}["${escaped}"]`);
1313
+ }
1314
+ }
1315
+ for (const edge of edges) {
1316
+ const label = edge.type === "blocks" ? "blocks" : edge.type;
1317
+ lines.push(` ${edge.from} -->|${label}| ${edge.to}`);
1318
+ }
1319
+ lines.push(" classDef highlight fill:#f9f,stroke:#333,stroke-width:2px");
1320
+ return lines.join("\n");
1321
+ }
1322
+ };
1323
+ function normalizeBlockedBy(input) {
1324
+ if (typeof input === "object" && input !== null && "type" in input && input.type === UIDependencyType.BlockedBy) {
1325
+ const { taskId, dependsOnId, ...rest } = input;
1326
+ return { ...rest, taskId: dependsOnId, dependsOnId: taskId, type: DependencyType.Blocks };
1327
+ }
1328
+ return input;
1329
+ }
1330
+
1331
+ // src/types/portability.ts
1332
+ import { z as z4 } from "zod/v4";
1333
+ var ImportFileSchema = z4.object({
1334
+ version: z4.number().optional(),
1335
+ tasks: z4.array(z4.record(z4.string(), z4.unknown())).min(1, "At least one task is required"),
1336
+ dependencies: z4.array(z4.record(z4.string(), z4.unknown())).optional().default([])
1337
+ });
1338
+ function parseFieldMapping(raw) {
1339
+ const mapping = /* @__PURE__ */ new Map();
1340
+ for (const pair of raw.split(",")) {
1341
+ const parts = pair.split(":").map((s) => s.trim());
1342
+ if (parts.length === 2 && parts[0] && parts[1]) {
1343
+ mapping.set(parts[0], parts[1]);
1344
+ }
1345
+ }
1346
+ return mapping;
1347
+ }
1348
+
1349
+ // src/service/portability.service.ts
1350
+ var PortabilityServiceImpl = class {
1351
+ constructor(taskService, depService, projectService) {
1352
+ this.taskService = taskService;
1353
+ this.depService = depService;
1354
+ this.projectService = projectService;
1355
+ }
1356
+ taskService;
1357
+ depService;
1358
+ projectService;
1359
+ exportTasks(projectIdOrName) {
1360
+ return logger.startSpan("PortabilityService.exportTasks", () => {
1361
+ const projectResult = this.projectService.resolveProject(projectIdOrName);
1362
+ if (!projectResult.ok) return projectResult;
1363
+ const project = projectResult.value;
1364
+ const tasksResult = this.taskService.listTasks({ projectId: project.id });
1365
+ if (!tasksResult.ok) return tasksResult;
1366
+ const tasks = tasksResult.value;
1367
+ const taskIds = new Set(tasks.map((t) => t.id));
1368
+ const exportTasks = tasks.map((t) => ({
1369
+ id: t.id,
1370
+ name: t.name,
1371
+ description: t.description,
1372
+ type: t.type,
1373
+ status: t.status,
1374
+ parentId: t.parentId,
1375
+ technicalNotes: t.technicalNotes,
1376
+ additionalRequirements: t.additionalRequirements,
1377
+ rank: t.rank
1378
+ }));
1379
+ const allDeps = [];
1380
+ for (const task of tasks) {
1381
+ const depsResult = this.depService.listAllDeps(task.id);
1382
+ if (!depsResult.ok) return depsResult;
1383
+ for (const dep of depsResult.value) {
1384
+ if (taskIds.has(dep.dependsOnId)) {
1385
+ allDeps.push({
1386
+ taskId: dep.taskId,
1387
+ dependsOnId: dep.dependsOnId,
1388
+ type: dep.type
1389
+ });
1390
+ }
1391
+ }
1392
+ }
1393
+ logger.info("Exported tasks", {
1394
+ project: project.key,
1395
+ tasks: tasks.length,
1396
+ dependencies: allDeps.length
1397
+ });
1398
+ return ok({
1399
+ version: 1,
1400
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1401
+ tasks: exportTasks,
1402
+ dependencies: allDeps
1403
+ });
1404
+ });
1405
+ }
1406
+ importTasks(fileData, projectIdOrName, fieldMapping) {
1407
+ return logger.startSpan("PortabilityService.importTasks", () => {
1408
+ const parsed = ImportFileSchema.safeParse(fileData);
1409
+ if (!parsed.success) {
1410
+ return err(new AppError("VALIDATION", parsed.error.message));
1411
+ }
1412
+ const reverseMap = this.buildReverseMap(fieldMapping);
1413
+ const mappedTasks = [];
1414
+ for (const sourceTask of parsed.data.tasks) {
1415
+ const mapped = this.mapTaskFields(sourceTask, reverseMap);
1416
+ if (!mapped.ok) return mapped;
1417
+ mappedTasks.push(mapped.value);
1418
+ }
1419
+ const mappedDeps = [];
1420
+ for (const sourceDep of parsed.data.dependencies) {
1421
+ const mapped = this.mapDependencyFields(sourceDep, reverseMap);
1422
+ if (!mapped.ok) return mapped;
1423
+ mappedDeps.push(mapped.value);
1424
+ }
1425
+ const sorted = this.topoSortByParent(mappedTasks);
1426
+ const idMap = /* @__PURE__ */ new Map();
1427
+ for (const task of sorted) {
1428
+ let parentId;
1429
+ if (task.parentId) {
1430
+ parentId = idMap.get(task.parentId) ?? task.parentId;
1431
+ }
1432
+ const createResult = this.taskService.createTask(
1433
+ {
1434
+ name: task.name,
1435
+ description: task.description || void 0,
1436
+ type: task.type || void 0,
1437
+ status: task.status || void 0,
1438
+ parentId,
1439
+ technicalNotes: task.technicalNotes || void 0,
1440
+ additionalRequirements: task.additionalRequirements || void 0
1441
+ },
1442
+ projectIdOrName
1443
+ );
1444
+ if (!createResult.ok) return createResult;
1445
+ idMap.set(task.sourceId, createResult.value.id);
1446
+ logger.info("Imported task", {
1447
+ sourceId: task.sourceId,
1448
+ newId: createResult.value.id
1449
+ });
1450
+ }
1451
+ let depCount = 0;
1452
+ for (const dep of mappedDeps) {
1453
+ const taskId = idMap.get(dep.sourceTaskId) ?? dep.sourceTaskId;
1454
+ const dependsOnId = idMap.get(dep.sourceDependsOnId) ?? dep.sourceDependsOnId;
1455
+ const depResult = this.depService.addDependency({
1456
+ taskId,
1457
+ dependsOnId,
1458
+ type: dep.type || void 0
1459
+ });
1460
+ if (!depResult.ok) return depResult;
1461
+ depCount++;
1462
+ logger.info("Imported dependency", { from: taskId, to: dependsOnId });
1463
+ }
1464
+ const idMapObj = {};
1465
+ for (const [k, v] of idMap) {
1466
+ idMapObj[k] = v;
1467
+ }
1468
+ logger.info("Import completed", {
1469
+ tasks: mappedTasks.length,
1470
+ dependencies: depCount
1471
+ });
1472
+ return ok({
1473
+ imported: mappedTasks.length,
1474
+ dependencies: depCount,
1475
+ idMap: idMapObj
1476
+ });
1477
+ });
1478
+ }
1479
+ // ── Private helpers ───────────────────────────────────────────────
1480
+ /** Invert the user-provided mapping (source→target) into (target→source). */
1481
+ buildReverseMap(fieldMapping) {
1482
+ const reverse = /* @__PURE__ */ new Map();
1483
+ if (!fieldMapping) return reverse;
1484
+ for (const [source, target] of fieldMapping) {
1485
+ reverse.set(target, source);
1486
+ }
1487
+ return reverse;
1488
+ }
1489
+ /** Read a field from a source object, checking reverse-mapped name first, then literal name. */
1490
+ getField(source, field, reverseMap) {
1491
+ const sourceField = reverseMap.get(field) ?? field;
1492
+ const value = source[sourceField];
1493
+ if (value == null) return "";
1494
+ if (typeof value === "string") return value;
1495
+ if (typeof value === "number" || typeof value === "boolean") return `${value}`;
1496
+ return JSON.stringify(value);
1497
+ }
1498
+ mapTaskFields(source, reverseMap) {
1499
+ const sourceId = this.getField(source, "id", reverseMap);
1500
+ const name = this.getField(source, "name", reverseMap);
1501
+ if (!sourceId) {
1502
+ return err(new AppError("VALIDATION", "Each imported task must have an id"));
1503
+ }
1504
+ if (!name) {
1505
+ return err(
1506
+ new AppError("VALIDATION", `Imported task '${sourceId}' is missing required field 'name'`)
1507
+ );
1508
+ }
1509
+ return ok({
1510
+ sourceId,
1511
+ name,
1512
+ description: this.getField(source, "description", reverseMap),
1513
+ type: this.getField(source, "type", reverseMap),
1514
+ status: this.getField(source, "status", reverseMap),
1515
+ parentId: this.getField(source, "parentId", reverseMap) || null,
1516
+ technicalNotes: this.getField(source, "technicalNotes", reverseMap),
1517
+ additionalRequirements: this.getField(source, "additionalRequirements", reverseMap)
1518
+ });
1519
+ }
1520
+ mapDependencyFields(source, reverseMap) {
1521
+ const taskId = this.getField(source, "taskId", reverseMap);
1522
+ const dependsOnId = this.getField(source, "dependsOnId", reverseMap);
1523
+ if (!taskId || !dependsOnId) {
1524
+ return err(new AppError("VALIDATION", "Each dependency must have taskId and dependsOnId"));
1525
+ }
1526
+ return ok({
1527
+ sourceTaskId: taskId,
1528
+ sourceDependsOnId: dependsOnId,
1529
+ type: this.getField(source, "type", reverseMap)
1530
+ });
1531
+ }
1532
+ /**
1533
+ * Sort tasks so that parents come before children within the import set.
1534
+ * Tasks whose parentId is outside the import set are treated as roots.
1535
+ */
1536
+ topoSortByParent(tasks) {
1537
+ const sourceIds = new Set(tasks.map((t) => t.sourceId));
1538
+ const sorted = [];
1539
+ const remaining = new Set(tasks);
1540
+ const created = /* @__PURE__ */ new Set();
1541
+ while (remaining.size > 0) {
1542
+ const batch = [];
1543
+ for (const task of remaining) {
1544
+ const parentInImport = task.parentId !== null && sourceIds.has(task.parentId);
1545
+ if (!parentInImport || task.parentId !== null && created.has(task.parentId)) {
1546
+ batch.push(task);
1547
+ }
1548
+ }
1549
+ if (batch.length === 0) {
1550
+ logger.warn("Circular parent references detected during import, forcing remaining tasks");
1551
+ for (const t of remaining) {
1552
+ sorted.push(t);
1553
+ }
1554
+ break;
1555
+ }
1556
+ for (const t of batch) {
1557
+ remaining.delete(t);
1558
+ created.add(t.sourceId);
1559
+ sorted.push(t);
1560
+ }
1561
+ }
1562
+ return sorted;
1563
+ }
1564
+ };
1565
+
1566
+ // src/cli/container.ts
1567
+ function createContainer(db) {
1568
+ const projectRepo = new SqliteProjectRepository(db);
1569
+ const taskRepo = new SqliteTaskRepository(db);
1570
+ const depRepo = new SqliteDependencyRepository(db);
1571
+ const projectService = new ProjectServiceImpl(projectRepo);
1572
+ const dependencyService = new DependencyServiceImpl(depRepo, taskRepo);
1573
+ const taskService = new TaskServiceImpl(taskRepo, projectService, () => dependencyService);
1574
+ const portabilityService = new PortabilityServiceImpl(
1575
+ taskService,
1576
+ dependencyService,
1577
+ projectService
1578
+ );
1579
+ return { projectService, taskService, dependencyService, portabilityService };
1580
+ }
1581
+
1582
+ // src/cli/index.ts
1583
+ import { Command } from "commander";
1584
+
1585
+ // src/cli/output.ts
1586
+ function printSuccess(data) {
1587
+ process.stdout.write(JSON.stringify({ ok: true, data }, null, 2) + "\n");
1588
+ }
1589
+ function printError(error) {
1590
+ process.stderr.write(
1591
+ JSON.stringify({ ok: false, error: { code: error.code, message: error.message } }, null, 2) + "\n"
1592
+ );
1593
+ process.exit(1);
1594
+ }
1595
+ function handleResult(result) {
1596
+ if (result.ok) {
1597
+ printSuccess(result.value);
1598
+ } else {
1599
+ printError(result.error);
1600
+ }
1601
+ }
1602
+
1603
+ // src/cli/commands/project/create.ts
1604
+ function registerProjectCreate(parent, container) {
1605
+ parent.command("create").description("Create a new project").requiredOption("-n, --name <name>", "Project name").option(
1606
+ "-k, --key <key>",
1607
+ "Project key (2-10 alphanumeric chars, defaults to first 3 chars of name)"
1608
+ ).option("-d, --description <description>", "Project description").option("--default", "Set as default project").action((opts) => {
1609
+ const result = container.projectService.createProject({
1610
+ name: opts.name,
1611
+ key: opts.key,
1612
+ description: opts.description,
1613
+ isDefault: opts.default
1614
+ });
1615
+ handleResult(result);
1616
+ });
1617
+ }
1618
+
1619
+ // src/cli/commands/project/list.ts
1620
+ function registerProjectList(parent, container) {
1621
+ parent.command("list").description("List all projects").action(() => {
1622
+ const result = container.projectService.listProjects();
1623
+ handleResult(result);
1624
+ });
1625
+ }
1626
+
1627
+ // src/cli/commands/project/update.ts
1628
+ function registerProjectUpdate(parent, container) {
1629
+ parent.command("update <idOrKeyOrName>").description("Update a project (lookup by id, key, or name)").option("-n, --name <name>", "Project name").option("-d, --description <description>", "Project description").option("--default", "Set as default project").action(
1630
+ (idOrKeyOrName, opts) => {
1631
+ const resolved = container.projectService.resolveProject(idOrKeyOrName);
1632
+ if (!resolved.ok) {
1633
+ handleResult(resolved);
1634
+ return;
1635
+ }
1636
+ const result = container.projectService.updateProject(resolved.value.id, {
1637
+ name: opts.name,
1638
+ description: opts.description,
1639
+ isDefault: opts.default
1640
+ });
1641
+ handleResult(result);
1642
+ }
1643
+ );
1644
+ }
1645
+
1646
+ // src/cli/commands/project/delete.ts
1647
+ function registerProjectDelete(parent, container) {
1648
+ parent.command("delete <idOrKeyOrName>").description("Delete a project (lookup by id, key, or name)").action((idOrKeyOrName) => {
1649
+ const resolved = container.projectService.resolveProject(idOrKeyOrName);
1650
+ if (!resolved.ok) {
1651
+ handleResult(resolved);
1652
+ return;
1653
+ }
1654
+ const result = container.projectService.deleteProject(resolved.value.id);
1655
+ handleResult(result);
1656
+ });
1657
+ }
1658
+
1659
+ // src/cli/commands/project/set-default.ts
1660
+ function registerProjectSetDefault(parent, container) {
1661
+ parent.command("set-default <idOrKeyOrName>").description("Set a project as the default (lookup by id, key, or name)").action((idOrKeyOrName) => {
1662
+ const resolved = container.projectService.resolveProject(idOrKeyOrName);
1663
+ if (!resolved.ok) {
1664
+ handleResult(resolved);
1665
+ return;
1666
+ }
1667
+ const result = container.projectService.updateProject(resolved.value.id, {
1668
+ isDefault: true
1669
+ });
1670
+ handleResult(result);
1671
+ });
1672
+ }
1673
+
1674
+ // src/cli/commands/task/create.ts
1675
+ function registerTaskCreate(parent, container) {
1676
+ parent.command("create").description("Create a new task (appended to bottom of backlog)").requiredOption("-n, --name <name>", "Task name").option("-p, --project <project>", "Project id or name").option("-d, --description <description>", "Task description").option("-t, --type <type>", "Task type: story, tech-debt, bug", "story").option("-s, --status <status>", "Task status", "backlog").option("--parent <parentId>", "Parent task id for subtask").option("--technical-notes <notes>", "Technical notes (markdown)").option("--additional-requirements <requirements>", "Additional requirements (markdown)").option("--depends-on <ids...>", "Task ids this task depends on (blocks relationship)").action(
1677
+ (opts) => {
1678
+ const result = container.taskService.createTask(
1679
+ {
1680
+ name: opts.name,
1681
+ description: opts.description,
1682
+ type: opts.type,
1683
+ status: opts.status,
1684
+ parentId: opts.parent,
1685
+ technicalNotes: opts.technicalNotes,
1686
+ additionalRequirements: opts.additionalRequirements,
1687
+ dependsOn: opts.dependsOn?.map((id) => ({ id }))
1688
+ },
1689
+ opts.project
1690
+ );
1691
+ handleResult(result);
1692
+ }
1693
+ );
1694
+ }
1695
+
1696
+ // src/cli/commands/task/list.ts
1697
+ function registerTaskList(parent, container) {
1698
+ parent.command("list").description("List tasks in rank order (defaults to backlog)").option("-p, --project <project>", "Filter by project id or name").option("-s, --status <status>", "Filter by status (default: backlog)").option("-t, --type <type>", "Filter by type").option("--parent <parentId>", "Filter by parent task id").option("--search <text>", "Search in name, description, and notes").action(
1699
+ (opts) => {
1700
+ const result = container.taskService.listTasks({
1701
+ projectId: opts.project,
1702
+ status: opts.status ?? "backlog",
1703
+ type: opts.type,
1704
+ parentId: opts.parent,
1705
+ search: opts.search
1706
+ });
1707
+ handleResult(result);
1708
+ }
1709
+ );
1710
+ }
1711
+
1712
+ // src/cli/commands/task/show.ts
1713
+ function registerTaskShow(parent, container) {
1714
+ parent.command("show <id>").description("Show task details").action((id) => {
1715
+ const result = container.taskService.getTask(id);
1716
+ handleResult(result);
1717
+ });
1718
+ }
1719
+
1720
+ // src/cli/commands/task/update.ts
1721
+ function registerTaskUpdate(parent, container) {
1722
+ parent.command("update <id>").description("Update a task").option("-n, --name <name>", "Task name").option("-d, --description <description>", "Task description").option("-t, --type <type>", "Task type: story, tech-debt, bug").option("-s, --status <status>", "Task status").option("--parent <parentId>", "Parent task id").option("--technical-notes <notes>", "Replace technical notes").option("--additional-requirements <requirements>", "Replace additional requirements").option("--append-notes <notes>", "Append to technical notes").option("--append-requirements <requirements>", "Append to additional requirements").action(
1723
+ (id, opts) => {
1724
+ const result = container.taskService.updateTask(id, {
1725
+ name: opts.name,
1726
+ description: opts.description,
1727
+ type: opts.type,
1728
+ status: opts.status,
1729
+ parentId: opts.parent,
1730
+ technicalNotes: opts.technicalNotes,
1731
+ additionalRequirements: opts.additionalRequirements,
1732
+ appendNotes: opts.appendNotes,
1733
+ appendRequirements: opts.appendRequirements
1734
+ });
1735
+ handleResult(result);
1736
+ }
1737
+ );
1738
+ }
1739
+
1740
+ // src/cli/commands/task/delete.ts
1741
+ function registerTaskDelete(parent, container) {
1742
+ parent.command("delete <id>").description("Delete a task").action((id) => {
1743
+ const result = container.taskService.deleteTask(id);
1744
+ handleResult(result);
1745
+ });
1746
+ }
1747
+
1748
+ // src/cli/commands/task/breakdown.ts
1749
+ import { readFileSync as readFileSync2 } from "fs";
1750
+ function registerTaskBreakdown(parent, container) {
1751
+ parent.command("breakdown <parentId>").description("Create subtasks from a JSON file").requiredOption("-f, --file <path>", "JSON file with array of subtask definitions").action((parentId, opts) => {
1752
+ let content;
1753
+ try {
1754
+ content = readFileSync2(opts.file, "utf-8");
1755
+ } catch (e) {
1756
+ return printError(
1757
+ new AppError("VALIDATION", `Failed to read subtasks file: ${opts.file}`, e)
1758
+ );
1759
+ }
1760
+ let parsed;
1761
+ try {
1762
+ parsed = JSON.parse(content);
1763
+ } catch (e) {
1764
+ return printError(
1765
+ new AppError("VALIDATION", `Invalid JSON in subtasks file: ${opts.file}`, e)
1766
+ );
1767
+ }
1768
+ if (!Array.isArray(parsed)) {
1769
+ return printError(new AppError("VALIDATION", "File must contain a JSON array of subtasks"));
1770
+ }
1771
+ handleResult(container.taskService.breakdownTask(parentId, parsed));
1772
+ });
1773
+ }
1774
+
1775
+ // src/cli/commands/task/rank.ts
1776
+ function registerTaskRank(parent, container) {
1777
+ parent.command("rank <id>").description("Re-rank a task in the backlog (Jira-style positioning)").option("--after <taskId>", "Place immediately after this task").option("--before <taskId>", "Place immediately before this task").option("--position <n>", "Place at 1-based position in backlog").option("-p, --project <project>", "Project id or name").action(
1778
+ (id, opts) => {
1779
+ const result = container.taskService.rerankTask(
1780
+ {
1781
+ taskId: id,
1782
+ afterId: opts.after,
1783
+ beforeId: opts.before,
1784
+ position: opts.position ? parseInt(opts.position, 10) : void 0
1785
+ },
1786
+ opts.project
1787
+ );
1788
+ handleResult(result);
1789
+ }
1790
+ );
1791
+ }
1792
+
1793
+ // src/cli/commands/task/search.ts
1794
+ function registerTaskSearch(parent, container) {
1795
+ parent.command("search <query>").description("Full-text search tasks with relevance ranking (FTS5)").option("-p, --project <project>", "Limit search to a project").action((query, opts) => {
1796
+ const result = container.taskService.searchTasks(query, opts.project);
1797
+ handleResult(result);
1798
+ });
1799
+ }
1800
+
1801
+ // src/cli/commands/task/export.ts
1802
+ import { writeFileSync } from "fs";
1803
+ function registerTaskExport(parent, container) {
1804
+ parent.command("export").description("Export tasks to JSON file").option("-p, --project <project>", "Project id or name").option("-o, --output <file>", "Output file path (defaults to stdout)").action((opts) => {
1805
+ const result = container.portabilityService.exportTasks(opts.project);
1806
+ if (!result.ok) {
1807
+ return printError(result.error);
1808
+ }
1809
+ if (opts.output) {
1810
+ try {
1811
+ writeFileSync(opts.output, JSON.stringify(result.value, null, 2) + "\n", "utf-8");
1812
+ } catch (e) {
1813
+ return printError(new AppError("UNKNOWN", `Failed to write file: ${opts.output}`, e));
1814
+ }
1815
+ printSuccess({
1816
+ file: opts.output,
1817
+ tasks: result.value.tasks.length,
1818
+ dependencies: result.value.dependencies.length
1819
+ });
1820
+ } else {
1821
+ printSuccess(result.value);
1822
+ }
1823
+ });
1824
+ }
1825
+
1826
+ // src/cli/commands/task/import.ts
1827
+ import { readFileSync as readFileSync3 } from "fs";
1828
+ function registerTaskImport(parent, container) {
1829
+ parent.command("import").description("Import tasks from JSON file").requiredOption("-f, --file <file>", "Input JSON file path").option("-p, --project <project>", "Target project id or name").option(
1830
+ "--map <mapping>",
1831
+ 'Field mapping as comma-separated source:target pairs (e.g. "title:name,summary:description")'
1832
+ ).action((opts) => {
1833
+ let fileData;
1834
+ try {
1835
+ const raw = readFileSync3(opts.file, "utf-8");
1836
+ fileData = JSON.parse(raw);
1837
+ } catch (e) {
1838
+ return printError(
1839
+ new AppError(
1840
+ "VALIDATION",
1841
+ `Failed to read or parse file: ${opts.file}${e instanceof Error ? ` - ${e.message}` : ""}`
1842
+ )
1843
+ );
1844
+ }
1845
+ const fieldMapping = opts.map ? parseFieldMapping(opts.map) : void 0;
1846
+ const result = container.portabilityService.importTasks(fileData, opts.project, fieldMapping);
1847
+ handleResult(result);
1848
+ });
1849
+ }
1850
+
1851
+ // src/cli/commands/dep/add.ts
1852
+ function registerDepAdd(parent, container) {
1853
+ parent.command("add <taskId> <dependsOnId>").description("Add a dependency (taskId depends on dependsOnId)").option(
1854
+ "-t, --type <type>",
1855
+ "Dependency type: blocks, relates-to, duplicates, blocked-by",
1856
+ "blocks"
1857
+ ).action((taskId, dependsOnId, opts) => {
1858
+ const result = container.dependencyService.addDependency({
1859
+ taskId,
1860
+ dependsOnId,
1861
+ type: opts.type
1862
+ });
1863
+ handleResult(result);
1864
+ });
1865
+ }
1866
+
1867
+ // src/cli/commands/dep/remove.ts
1868
+ function registerDepRemove(parent, container) {
1869
+ parent.command("remove <taskId> <dependsOnId>").description("Remove a dependency").action((taskId, dependsOnId) => {
1870
+ const result = container.dependencyService.removeDependency({
1871
+ taskId,
1872
+ dependsOnId
1873
+ });
1874
+ handleResult(result);
1875
+ });
1876
+ }
1877
+
1878
+ // src/cli/commands/dep/list.ts
1879
+ function registerDepList(parent, container) {
1880
+ parent.command("list <taskId>").description("List direct dependencies of a task").option("--blockers", "Show tasks that block this task").option("--dependents", "Show tasks that depend on this task").option("--transitive", "Show all transitive blockers (deep)").action(
1881
+ (taskId, opts) => {
1882
+ if (opts.transitive) {
1883
+ handleResult(container.dependencyService.getTransitiveDeps(taskId));
1884
+ } else if (opts.dependents) {
1885
+ handleResult(container.dependencyService.listDependents(taskId));
1886
+ } else if (opts.blockers) {
1887
+ handleResult(container.dependencyService.listBlockers(taskId));
1888
+ } else {
1889
+ handleResult(container.dependencyService.listAllDeps(taskId));
1890
+ }
1891
+ }
1892
+ );
1893
+ }
1894
+
1895
+ // src/cli/commands/dep/graph.ts
1896
+ function registerDepGraph(parent, container) {
1897
+ parent.command("graph <taskId>").description("Build full dependency graph centered on a task (outputs Mermaid)").action((taskId) => {
1898
+ const result = container.dependencyService.buildGraph(taskId);
1899
+ handleResult(result);
1900
+ });
1901
+ }
1902
+
1903
+ // src/cli/index.ts
1904
+ function buildCLI(container) {
1905
+ const program = new Command();
1906
+ program.name("tayto").description("CLI task management for solo devs and AI agents").version("0.1.0");
1907
+ const project = program.command("project").description("Manage projects");
1908
+ registerProjectCreate(project, container);
1909
+ registerProjectList(project, container);
1910
+ registerProjectUpdate(project, container);
1911
+ registerProjectDelete(project, container);
1912
+ registerProjectSetDefault(project, container);
1913
+ const task = program.command("task").description("Manage tasks");
1914
+ registerTaskCreate(task, container);
1915
+ registerTaskList(task, container);
1916
+ registerTaskShow(task, container);
1917
+ registerTaskUpdate(task, container);
1918
+ registerTaskDelete(task, container);
1919
+ registerTaskBreakdown(task, container);
1920
+ registerTaskRank(task, container);
1921
+ registerTaskSearch(task, container);
1922
+ registerTaskExport(task, container);
1923
+ registerTaskImport(task, container);
1924
+ const dep = program.command("dep").description("Manage task dependencies");
1925
+ registerDepAdd(dep, container);
1926
+ registerDepRemove(dep, container);
1927
+ registerDepList(dep, container);
1928
+ registerDepGraph(dep, container);
1929
+ program.command("tui").description("Launch interactive terminal UI").option("-p, --project <project>", "Start with specific project").action(async (opts) => {
1930
+ const { launchTUI } = await import("./tui-JNZRBEIQ.js");
1931
+ await launchTUI(container, opts.project);
1932
+ });
1933
+ return program;
1934
+ }
1935
+
1936
+ // src/index.ts
1937
+ async function main() {
1938
+ const config = loadConfig();
1939
+ logger.init(config.logDir);
1940
+ initTelemetry(config);
1941
+ const db = createDatabase(config.dbPath);
1942
+ runMigrations(db);
1943
+ const container = createContainer(db);
1944
+ const args = process.argv.slice(2);
1945
+ if (args.length === 0) {
1946
+ const { launchTUI } = await import("./tui-JNZRBEIQ.js");
1947
+ await launchTUI(container);
1948
+ } else {
1949
+ const program = buildCLI(container);
1950
+ await program.parseAsync(process.argv);
1951
+ }
1952
+ db.close();
1953
+ await shutdownTelemetry();
1954
+ }
1955
+ main().catch((e) => {
1956
+ const error = e instanceof AppError ? e : new AppError("UNKNOWN", e instanceof Error ? e.message : "Unknown error", e);
1957
+ process.stderr.write(
1958
+ JSON.stringify({ ok: false, error: { code: error.code, message: error.message } }, null, 2) + "\n"
1959
+ );
1960
+ process.exit(1);
1961
+ });
1962
+ //# sourceMappingURL=index.js.map