@jamesaphoenix/tx 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/cli.js ADDED
@@ -0,0 +1,868 @@
1
+ #!/usr/bin/env node
2
+ import { Effect } from "effect";
3
+ import { resolve } from "path";
4
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
5
+ import { makeAppLayer, SyncService } from "./layer.js";
6
+ import { TaskService } from "./services/task-service.js";
7
+ import { DependencyService } from "./services/dep-service.js";
8
+ import { ReadyService } from "./services/ready-service.js";
9
+ import { startMcpServer } from "./mcp/server.js";
10
+ // --- Help text constant ---
11
+ const HELP_TEXT = `tx v0.1.0 - Task management for AI agents and humans
12
+
13
+ Usage: tx <command> [arguments] [options]
14
+
15
+ Commands:
16
+ init Initialize task database
17
+ add <title> Create a new task
18
+ list List tasks
19
+ ready List ready tasks (no blockers)
20
+ show <id> Show task details
21
+ update <id> Update task
22
+ done <id> Mark task complete
23
+ delete <id> Delete task
24
+ block <id> <blocker> Add blocking dependency
25
+ unblock <id> <blocker> Remove blocking dependency
26
+ children <id> List child tasks
27
+ tree <id> Show task subtree
28
+ sync export Export tasks to JSONL file
29
+ sync import Import tasks from JSONL file
30
+ sync status Show sync status
31
+ mcp-server Start MCP server (JSON-RPC over stdio)
32
+
33
+ Global Options:
34
+ --json Output as JSON
35
+ --db <path> Database path (default: .tx/tasks.db)
36
+ --help Show help
37
+ --version Show version
38
+
39
+ Run 'tx help <command>' or 'tx <command> --help' for command-specific help.
40
+
41
+ Examples:
42
+ tx init
43
+ tx add "Implement auth" --score 800
44
+ tx add "Login page" --parent tx-a1b2c3d4 --score 600
45
+ tx list --status backlog,ready
46
+ tx ready --json
47
+ tx block <task-id> <blocker-id>
48
+ tx done <task-id>`;
49
+ // --- Argv parsing helpers ---
50
+ function parseArgs(argv) {
51
+ const args = argv.slice(2);
52
+ const positional = [];
53
+ const flags = {};
54
+ // Parse a flag at index idx, using valueCheckPrefix to determine if next arg is a value
55
+ // Returns number of args consumed (1 for boolean flag, 2 for flag with value)
56
+ function consumeFlag(idx, valueCheckPrefix) {
57
+ const arg = args[idx];
58
+ const key = arg.startsWith("--") ? arg.slice(2) : arg.slice(1);
59
+ const next = args[idx + 1];
60
+ if (next && !next.startsWith(valueCheckPrefix)) {
61
+ flags[key] = next;
62
+ return 2;
63
+ }
64
+ flags[key] = true;
65
+ return 1;
66
+ }
67
+ // Find the command (first non-flag argument), parsing any leading flags
68
+ let command = "help";
69
+ let startIdx = 0;
70
+ for (let i = 0; i < args.length; i++) {
71
+ if (args[i].startsWith("-")) {
72
+ i += consumeFlag(i, "-") - 1;
73
+ }
74
+ else {
75
+ command = args[i];
76
+ startIdx = i + 1;
77
+ break;
78
+ }
79
+ }
80
+ // Parse remaining args: positional arguments and flags after command
81
+ for (let i = startIdx; i < args.length; i++) {
82
+ const arg = args[i];
83
+ if (arg.startsWith("--")) {
84
+ i += consumeFlag(i, "--") - 1;
85
+ }
86
+ else if (arg.startsWith("-")) {
87
+ i += consumeFlag(i, "-") - 1;
88
+ }
89
+ else {
90
+ positional.push(arg);
91
+ }
92
+ }
93
+ return { command, positional, flags };
94
+ }
95
+ function flag(flags, ...names) {
96
+ return names.some(n => flags[n] === true);
97
+ }
98
+ function opt(flags, ...names) {
99
+ for (const n of names) {
100
+ const v = flags[n];
101
+ if (typeof v === "string")
102
+ return v;
103
+ }
104
+ return undefined;
105
+ }
106
+ // --- Formatters ---
107
+ function formatTaskWithDeps(t) {
108
+ const lines = [
109
+ `Task: ${t.id}`,
110
+ ` Title: ${t.title}`,
111
+ ` Status: ${t.status}`,
112
+ ` Score: ${t.score}`,
113
+ ` Ready: ${t.isReady ? "yes" : "no"}`,
114
+ ];
115
+ if (t.description)
116
+ lines.push(` Description: ${t.description}`);
117
+ if (t.parentId)
118
+ lines.push(` Parent: ${t.parentId}`);
119
+ lines.push(` Blocked by: ${t.blockedBy.length > 0 ? t.blockedBy.join(", ") : "(none)"}`);
120
+ lines.push(` Blocks: ${t.blocks.length > 0 ? t.blocks.join(", ") : "(none)"}`);
121
+ lines.push(` Children: ${t.children.length > 0 ? t.children.join(", ") : "(none)"}`);
122
+ lines.push(` Created: ${t.createdAt.toISOString()}`);
123
+ lines.push(` Updated: ${t.updatedAt.toISOString()}`);
124
+ if (t.completedAt)
125
+ lines.push(` Completed: ${t.completedAt.toISOString()}`);
126
+ return lines.join("\n");
127
+ }
128
+ // --- JSON serializer (handles Date objects) ---
129
+ function jsonReplacer(_key, value) {
130
+ if (value instanceof Date)
131
+ return value.toISOString();
132
+ return value;
133
+ }
134
+ function toJson(data) {
135
+ return JSON.stringify(data, jsonReplacer, 2);
136
+ }
137
+ // --- Command Help ---
138
+ const commandHelp = {
139
+ init: `tx init - Initialize task database
140
+
141
+ Usage: tx init [--db <path>]
142
+
143
+ Initializes the tx database and required tables. Creates .tx/tasks.db
144
+ by default. Safe to run multiple times (idempotent).
145
+
146
+ Options:
147
+ --db <path> Database path (default: .tx/tasks.db)
148
+ --help Show this help
149
+
150
+ Examples:
151
+ tx init # Initialize in .tx/tasks.db
152
+ tx init --db ~/my-tasks.db # Use custom path`,
153
+ add: `tx add - Create a new task
154
+
155
+ Usage: tx add <title> [options]
156
+
157
+ Creates a new task with the given title. Tasks start with status "backlog"
158
+ and default score 500.
159
+
160
+ Arguments:
161
+ <title> Required. The task title (use quotes for multi-word titles)
162
+
163
+ Options:
164
+ --parent, -p <id> Parent task ID (for subtasks)
165
+ --score, -s <n> Priority score 0-1000 (default: 500, higher = more important)
166
+ --description, -d <text> Task description
167
+ --json Output as JSON
168
+ --help Show this help
169
+
170
+ Examples:
171
+ tx add "Implement auth"
172
+ tx add "Login page" --parent tx-a1b2c3d4 --score 600
173
+ tx add "Fix bug" -s 800 -d "Urgent fix for login"`,
174
+ list: `tx list - List tasks
175
+
176
+ Usage: tx list [options]
177
+
178
+ Lists all tasks, optionally filtered by status. Shows task ID, status,
179
+ score, title, and ready indicator (+).
180
+
181
+ Options:
182
+ --status <s> Filter by status (comma-separated: backlog,ready,active,done)
183
+ --limit, -n <n> Maximum tasks to show
184
+ --json Output as JSON
185
+ --help Show this help
186
+
187
+ Examples:
188
+ tx list # List all tasks
189
+ tx list --status backlog,ready # Only backlog and ready tasks
190
+ tx list -n 10 --json # Top 10 as JSON`,
191
+ ready: `tx ready - List ready tasks
192
+
193
+ Usage: tx ready [options]
194
+
195
+ Lists tasks that are ready to work on (status is workable and all blockers
196
+ are done). Sorted by score, highest first.
197
+
198
+ Options:
199
+ --limit, -n <n> Maximum tasks to show (default: 10)
200
+ --json Output as JSON
201
+ --help Show this help
202
+
203
+ Examples:
204
+ tx ready # Top 10 ready tasks
205
+ tx ready -n 5 # Top 5 ready tasks
206
+ tx ready --json # Output as JSON for scripting`,
207
+ show: `tx show - Show task details
208
+
209
+ Usage: tx show <id> [options]
210
+
211
+ Shows full details for a single task including title, status, score,
212
+ description, parent, blockers, blocks, children, and timestamps.
213
+
214
+ Arguments:
215
+ <id> Required. Task ID (e.g., tx-a1b2c3d4)
216
+
217
+ Options:
218
+ --json Output as JSON
219
+ --help Show this help
220
+
221
+ Examples:
222
+ tx show tx-a1b2c3d4
223
+ tx show tx-a1b2c3d4 --json`,
224
+ update: `tx update - Update a task
225
+
226
+ Usage: tx update <id> [options]
227
+
228
+ Updates one or more fields on an existing task.
229
+
230
+ Arguments:
231
+ <id> Required. Task ID (e.g., tx-a1b2c3d4)
232
+
233
+ Options:
234
+ --status <s> New status (backlog|ready|planning|active|blocked|review|human_needs_to_review|done)
235
+ --title <t> New title
236
+ --score <n> New score (0-1000)
237
+ --description, -d <text> New description
238
+ --parent, -p <id> New parent task ID
239
+ --json Output as JSON
240
+ --help Show this help
241
+
242
+ Examples:
243
+ tx update tx-a1b2c3d4 --status active
244
+ tx update tx-a1b2c3d4 --score 900 --title "High priority bug"`,
245
+ done: `tx done - Mark task complete
246
+
247
+ Usage: tx done <id> [options]
248
+
249
+ Marks a task as complete (status = done). Also reports any tasks
250
+ that become unblocked as a result.
251
+
252
+ Arguments:
253
+ <id> Required. Task ID (e.g., tx-a1b2c3d4)
254
+
255
+ Options:
256
+ --json Output as JSON (includes task and newly unblocked task IDs)
257
+ --help Show this help
258
+
259
+ Examples:
260
+ tx done tx-a1b2c3d4
261
+ tx done tx-a1b2c3d4 --json`,
262
+ delete: `tx delete - Delete a task
263
+
264
+ Usage: tx delete <id> [options]
265
+
266
+ Permanently deletes a task. Also removes any dependencies involving
267
+ this task.
268
+
269
+ Arguments:
270
+ <id> Required. Task ID (e.g., tx-a1b2c3d4)
271
+
272
+ Options:
273
+ --json Output as JSON
274
+ --help Show this help
275
+
276
+ Examples:
277
+ tx delete tx-a1b2c3d4`,
278
+ block: `tx block - Add blocking dependency
279
+
280
+ Usage: tx block <task-id> <blocker-id> [options]
281
+
282
+ Makes one task block another. The blocked task cannot be ready until
283
+ the blocker is marked done. Circular dependencies are not allowed.
284
+
285
+ Arguments:
286
+ <task-id> Required. The task that will be blocked
287
+ <blocker-id> Required. The task that blocks it
288
+
289
+ Options:
290
+ --json Output as JSON
291
+ --help Show this help
292
+
293
+ Examples:
294
+ tx block tx-abc123 tx-def456 # tx-def456 blocks tx-abc123`,
295
+ unblock: `tx unblock - Remove blocking dependency
296
+
297
+ Usage: tx unblock <task-id> <blocker-id> [options]
298
+
299
+ Removes a blocking dependency between two tasks.
300
+
301
+ Arguments:
302
+ <task-id> Required. The task that was blocked
303
+ <blocker-id> Required. The task that was blocking it
304
+
305
+ Options:
306
+ --json Output as JSON
307
+ --help Show this help
308
+
309
+ Examples:
310
+ tx unblock tx-abc123 tx-def456`,
311
+ children: `tx children - List child tasks
312
+
313
+ Usage: tx children <id> [options]
314
+
315
+ Lists all direct children of a task (tasks with this task as parent).
316
+ Shows task ID, status, score, title, and ready indicator (+).
317
+
318
+ Arguments:
319
+ <id> Required. Parent task ID (e.g., tx-a1b2c3d4)
320
+
321
+ Options:
322
+ --json Output as JSON
323
+ --help Show this help
324
+
325
+ Examples:
326
+ tx children tx-a1b2c3d4
327
+ tx children tx-a1b2c3d4 --json`,
328
+ tree: `tx tree - Show task subtree
329
+
330
+ Usage: tx tree <id> [options]
331
+
332
+ Shows a task and all its descendants in a tree view. Useful for
333
+ visualizing task hierarchy.
334
+
335
+ Arguments:
336
+ <id> Required. Root task ID (e.g., tx-a1b2c3d4)
337
+
338
+ Options:
339
+ --json Output as JSON (nested structure with childTasks array)
340
+ --help Show this help
341
+
342
+ Examples:
343
+ tx tree tx-a1b2c3d4
344
+ tx tree tx-a1b2c3d4 --json`,
345
+ "mcp-server": `tx mcp-server - Start MCP server
346
+
347
+ Usage: tx mcp-server [options]
348
+
349
+ Starts the Model Context Protocol (MCP) server for integration with
350
+ AI agents. Communicates via JSON-RPC over stdio.
351
+
352
+ Options:
353
+ --db <path> Database path (default: .tx/tasks.db)
354
+ --help Show this help
355
+
356
+ Examples:
357
+ tx mcp-server
358
+ tx mcp-server --db ~/project/.tx/tasks.db`,
359
+ sync: `tx sync - Manage JSONL sync for git-based task sharing
360
+
361
+ Usage: tx sync <subcommand> [options]
362
+
363
+ Subcommands:
364
+ export Export all tasks and dependencies to JSONL file
365
+ import Import tasks from JSONL file (timestamp-based merge)
366
+ status Show sync status and whether database has unexported changes
367
+
368
+ Run 'tx sync <subcommand> --help' for subcommand-specific help.
369
+
370
+ Examples:
371
+ tx sync export # Export to .tx/tasks.jsonl
372
+ tx sync import # Import from .tx/tasks.jsonl
373
+ tx sync status # Show sync status`,
374
+ "sync export": `tx sync export - Export tasks to JSONL
375
+
376
+ Usage: tx sync export [--path <path>] [--json]
377
+
378
+ Exports all tasks and dependencies from the database to a JSONL file.
379
+ The file can be committed to git for sharing tasks across machines.
380
+
381
+ Options:
382
+ --path <p> Output file path (default: .tx/tasks.jsonl)
383
+ --json Output result as JSON
384
+ --help Show this help
385
+
386
+ Examples:
387
+ tx sync export # Export to default path
388
+ tx sync export --path ~/backup.jsonl # Export to custom path
389
+ tx sync export --json # JSON output for scripting`,
390
+ "sync import": `tx sync import - Import tasks from JSONL
391
+
392
+ Usage: tx sync import [--path <path>] [--json]
393
+
394
+ Imports tasks from a JSONL file into the database. Uses timestamp-based
395
+ conflict resolution: newer records win. Safe to run multiple times.
396
+
397
+ Options:
398
+ --path <p> Input file path (default: .tx/tasks.jsonl)
399
+ --json Output result as JSON
400
+ --help Show this help
401
+
402
+ Examples:
403
+ tx sync import # Import from default path
404
+ tx sync import --path ~/shared.jsonl # Import from custom path
405
+ tx sync import --json # JSON output for scripting`,
406
+ "sync status": `tx sync status - Show sync status
407
+
408
+ Usage: tx sync status [--json]
409
+
410
+ Shows the current sync status including:
411
+ - Number of tasks in database
412
+ - Number of operations in JSONL file
413
+ - Whether database has unexported changes (dirty)
414
+
415
+ Options:
416
+ --json Output as JSON
417
+ --help Show this help
418
+
419
+ Examples:
420
+ tx sync status
421
+ tx sync status --json`,
422
+ help: `tx help - Show help
423
+
424
+ Usage: tx help [command]
425
+ tx --help
426
+ tx <command> --help
427
+
428
+ Shows general help or help for a specific command.
429
+
430
+ Examples:
431
+ tx help # General help
432
+ tx help add # Help for 'add' command
433
+ tx add --help # Same as above`
434
+ };
435
+ // --- Commands ---
436
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
437
+ const commands = {
438
+ init: (_pos, _flags) => Effect.gen(function* () {
439
+ // Layer construction already creates db + runs migrations
440
+ // Just confirm it exists
441
+ console.log("Initialized tx database");
442
+ console.log(" Tables: tasks, task_dependencies, compaction_log, schema_version");
443
+ }),
444
+ add: (pos, flags) => Effect.gen(function* () {
445
+ const title = pos[0];
446
+ if (!title) {
447
+ console.error("Usage: tx add <title> [--parent/-p <id>] [--score/-s <n>] [--description/-d <text>] [--json]");
448
+ process.exit(1);
449
+ }
450
+ const svc = yield* TaskService;
451
+ const task = yield* svc.create({
452
+ title,
453
+ description: opt(flags, "description", "d"),
454
+ parentId: opt(flags, "parent", "p"),
455
+ score: opt(flags, "score", "s") ? parseInt(opt(flags, "score", "s"), 10) : undefined,
456
+ metadata: {}
457
+ });
458
+ if (flag(flags, "json")) {
459
+ const full = yield* svc.getWithDeps(task.id);
460
+ console.log(toJson(full));
461
+ }
462
+ else {
463
+ console.log(`Created task: ${task.id}`);
464
+ console.log(` Title: ${task.title}`);
465
+ console.log(` Score: ${task.score}`);
466
+ if (task.parentId)
467
+ console.log(` Parent: ${task.parentId}`);
468
+ }
469
+ }),
470
+ list: (_pos, flags) => Effect.gen(function* () {
471
+ const svc = yield* TaskService;
472
+ const statusFilter = opt(flags, "status");
473
+ const limit = opt(flags, "limit", "n") ? parseInt(opt(flags, "limit", "n"), 10) : undefined;
474
+ const tasks = yield* svc.listWithDeps({
475
+ status: statusFilter ? statusFilter.split(",") : undefined,
476
+ limit
477
+ });
478
+ if (flag(flags, "json")) {
479
+ console.log(toJson(tasks));
480
+ }
481
+ else {
482
+ if (tasks.length === 0) {
483
+ console.log("No tasks found");
484
+ }
485
+ else {
486
+ console.log(`${tasks.length} task(s):`);
487
+ for (const t of tasks) {
488
+ const readyMark = t.isReady ? "+" : " ";
489
+ console.log(` ${readyMark} ${t.id} [${t.status}] [${t.score}] ${t.title}`);
490
+ }
491
+ }
492
+ }
493
+ }),
494
+ ready: (_pos, flags) => Effect.gen(function* () {
495
+ const svc = yield* ReadyService;
496
+ const limit = opt(flags, "limit", "n") ? parseInt(opt(flags, "limit", "n"), 10) : 10;
497
+ const tasks = yield* svc.getReady(limit);
498
+ if (flag(flags, "json")) {
499
+ console.log(toJson(tasks));
500
+ }
501
+ else {
502
+ if (tasks.length === 0) {
503
+ console.log("No ready tasks");
504
+ }
505
+ else {
506
+ console.log(`${tasks.length} ready task(s):`);
507
+ for (const t of tasks) {
508
+ const blocksInfo = t.blocks.length > 0 ? ` (unblocks ${t.blocks.length})` : "";
509
+ console.log(` ${t.id} [${t.score}] ${t.title}${blocksInfo}`);
510
+ }
511
+ }
512
+ }
513
+ }),
514
+ show: (pos, flags) => Effect.gen(function* () {
515
+ const id = pos[0];
516
+ if (!id) {
517
+ console.error("Usage: tx show <id> [--json]");
518
+ process.exit(1);
519
+ }
520
+ const svc = yield* TaskService;
521
+ const task = yield* svc.getWithDeps(id);
522
+ if (flag(flags, "json")) {
523
+ console.log(toJson(task));
524
+ }
525
+ else {
526
+ console.log(formatTaskWithDeps(task));
527
+ }
528
+ }),
529
+ update: (pos, flags) => Effect.gen(function* () {
530
+ const id = pos[0];
531
+ if (!id) {
532
+ console.error("Usage: tx update <id> [--status <s>] [--title <t>] [--score <n>] [--description <d>] [--parent <p>] [--json]");
533
+ process.exit(1);
534
+ }
535
+ const svc = yield* TaskService;
536
+ const input = {};
537
+ if (opt(flags, "status"))
538
+ input.status = opt(flags, "status");
539
+ if (opt(flags, "title"))
540
+ input.title = opt(flags, "title");
541
+ if (opt(flags, "score"))
542
+ input.score = parseInt(opt(flags, "score"), 10);
543
+ if (opt(flags, "description", "d"))
544
+ input.description = opt(flags, "description", "d");
545
+ if (opt(flags, "parent", "p"))
546
+ input.parentId = opt(flags, "parent", "p");
547
+ yield* svc.update(id, input);
548
+ const task = yield* svc.getWithDeps(id);
549
+ if (flag(flags, "json")) {
550
+ console.log(toJson(task));
551
+ }
552
+ else {
553
+ console.log(`Updated: ${task.id}`);
554
+ console.log(` Status: ${task.status}`);
555
+ console.log(` Score: ${task.score}`);
556
+ }
557
+ }),
558
+ done: (pos, flags) => Effect.gen(function* () {
559
+ const id = pos[0];
560
+ if (!id) {
561
+ console.error("Usage: tx done <id> [--json]");
562
+ process.exit(1);
563
+ }
564
+ const taskSvc = yield* TaskService;
565
+ const readySvc = yield* ReadyService;
566
+ // Get tasks blocked by this one BEFORE marking complete
567
+ const blocking = yield* readySvc.getBlocking(id);
568
+ yield* taskSvc.update(id, { status: "done" });
569
+ const task = yield* taskSvc.getWithDeps(id);
570
+ // Find newly unblocked tasks using batch query
571
+ // Filter to workable statuses and get their full deps info in one batch
572
+ const candidateIds = blocking
573
+ .filter(t => ["backlog", "ready", "planning"].includes(t.status))
574
+ .map(t => t.id);
575
+ const candidatesWithDeps = yield* taskSvc.getWithDepsBatch(candidateIds);
576
+ const nowReady = candidatesWithDeps.filter(t => t.isReady).map(t => t.id);
577
+ if (flag(flags, "json")) {
578
+ console.log(toJson({ task, nowReady }));
579
+ }
580
+ else {
581
+ console.log(`Completed: ${task.id} - ${task.title}`);
582
+ if (nowReady.length > 0) {
583
+ console.log(`Now unblocked: ${nowReady.join(", ")}`);
584
+ }
585
+ }
586
+ }),
587
+ delete: (pos, flags) => Effect.gen(function* () {
588
+ const id = pos[0];
589
+ if (!id) {
590
+ console.error("Usage: tx delete <id> [--json]");
591
+ process.exit(1);
592
+ }
593
+ const svc = yield* TaskService;
594
+ const task = yield* svc.get(id);
595
+ yield* svc.remove(id);
596
+ if (flag(flags, "json")) {
597
+ console.log(toJson({ deleted: true, id: task.id, title: task.title }));
598
+ }
599
+ else {
600
+ console.log(`Deleted: ${task.id} - ${task.title}`);
601
+ }
602
+ }),
603
+ block: (pos, flags) => Effect.gen(function* () {
604
+ const id = pos[0];
605
+ const blocker = pos[1];
606
+ if (!id || !blocker) {
607
+ console.error("Usage: tx block <task-id> <blocker-id> [--json]");
608
+ process.exit(1);
609
+ }
610
+ const depSvc = yield* DependencyService;
611
+ const taskSvc = yield* TaskService;
612
+ yield* depSvc.addBlocker(id, blocker);
613
+ const task = yield* taskSvc.getWithDeps(id);
614
+ if (flag(flags, "json")) {
615
+ console.log(toJson({ success: true, task }));
616
+ }
617
+ else {
618
+ console.log(`${blocker} now blocks ${id}`);
619
+ console.log(` ${id} blocked by: ${task.blockedBy.join(", ")}`);
620
+ }
621
+ }),
622
+ unblock: (pos, flags) => Effect.gen(function* () {
623
+ const id = pos[0];
624
+ const blocker = pos[1];
625
+ if (!id || !blocker) {
626
+ console.error("Usage: tx unblock <task-id> <blocker-id> [--json]");
627
+ process.exit(1);
628
+ }
629
+ const depSvc = yield* DependencyService;
630
+ const taskSvc = yield* TaskService;
631
+ yield* depSvc.removeBlocker(id, blocker);
632
+ const task = yield* taskSvc.getWithDeps(id);
633
+ if (flag(flags, "json")) {
634
+ console.log(toJson({ success: true, task }));
635
+ }
636
+ else {
637
+ console.log(`${blocker} no longer blocks ${id}`);
638
+ console.log(` ${id} blocked by: ${task.blockedBy.length > 0 ? task.blockedBy.join(", ") : "(none)"}`);
639
+ }
640
+ }),
641
+ children: (pos, flags) => Effect.gen(function* () {
642
+ const id = pos[0];
643
+ if (!id) {
644
+ console.error("Usage: tx children <id> [--json]");
645
+ process.exit(1);
646
+ }
647
+ const svc = yield* TaskService;
648
+ const parent = yield* svc.getWithDeps(id);
649
+ const children = yield* svc.listWithDeps({ parentId: id });
650
+ if (flag(flags, "json")) {
651
+ console.log(toJson(children));
652
+ }
653
+ else {
654
+ if (children.length === 0) {
655
+ console.log(`No children for ${parent.id} - ${parent.title}`);
656
+ }
657
+ else {
658
+ console.log(`${children.length} child(ren) of ${parent.id} - ${parent.title}:`);
659
+ for (const c of children) {
660
+ const readyMark = c.isReady ? "+" : " ";
661
+ console.log(` ${readyMark} ${c.id} [${c.status}] [${c.score}] ${c.title}`);
662
+ }
663
+ }
664
+ }
665
+ }),
666
+ tree: (pos, flags) => Effect.gen(function* () {
667
+ const id = pos[0];
668
+ if (!id) {
669
+ console.error("Usage: tx tree <id> [--json]");
670
+ process.exit(1);
671
+ }
672
+ const svc = yield* TaskService;
673
+ // Recursive tree builder
674
+ const buildTree = (taskId, depth) => Effect.gen(function* () {
675
+ const task = yield* svc.getWithDeps(taskId);
676
+ const indent = " ".repeat(depth);
677
+ const readyMark = task.isReady ? "+" : " ";
678
+ if (flag(flags, "json") && depth === 0) {
679
+ // For JSON, collect the full tree
680
+ const collectTree = (t) => Effect.gen(function* () {
681
+ const childTasks = yield* svc.listWithDeps({ parentId: t.id });
682
+ const childTrees = [];
683
+ for (const c of childTasks) {
684
+ childTrees.push(yield* collectTree(c));
685
+ }
686
+ return { ...t, childTasks: childTrees };
687
+ });
688
+ const tree = yield* collectTree(task);
689
+ console.log(toJson(tree));
690
+ return;
691
+ }
692
+ console.log(`${indent}${readyMark} ${task.id} [${task.status}] [${task.score}] ${task.title}`);
693
+ const children = yield* svc.listWithDeps({ parentId: taskId });
694
+ for (const child of children) {
695
+ yield* buildTree(child.id, depth + 1);
696
+ }
697
+ });
698
+ yield* buildTree(id, 0);
699
+ }),
700
+ help: (pos) => Effect.sync(() => {
701
+ const subcommand = pos[0];
702
+ if (subcommand && commandHelp[subcommand]) {
703
+ console.log(commandHelp[subcommand]);
704
+ return;
705
+ }
706
+ console.log(HELP_TEXT);
707
+ }),
708
+ sync: (pos, flags) => Effect.gen(function* () {
709
+ const subcommand = pos[0];
710
+ if (!subcommand || subcommand === "help") {
711
+ console.log(commandHelp["sync"]);
712
+ return;
713
+ }
714
+ // Check for --help on subcommand
715
+ if (flag(flags, "help", "h")) {
716
+ const helpKey = `sync ${subcommand}`;
717
+ if (commandHelp[helpKey]) {
718
+ console.log(commandHelp[helpKey]);
719
+ return;
720
+ }
721
+ }
722
+ const syncSvc = yield* SyncService;
723
+ if (subcommand === "export") {
724
+ const path = opt(flags, "path");
725
+ const result = yield* syncSvc.export(path);
726
+ if (flag(flags, "json")) {
727
+ console.log(toJson(result));
728
+ }
729
+ else {
730
+ console.log(`Exported ${result.opCount} operation(s) to ${result.path}`);
731
+ }
732
+ }
733
+ else if (subcommand === "import") {
734
+ const path = opt(flags, "path");
735
+ const result = yield* syncSvc.import(path);
736
+ if (flag(flags, "json")) {
737
+ console.log(toJson(result));
738
+ }
739
+ else {
740
+ console.log(`Imported: ${result.imported}, Skipped: ${result.skipped}, Conflicts: ${result.conflicts}`);
741
+ }
742
+ }
743
+ else if (subcommand === "status") {
744
+ const status = yield* syncSvc.status();
745
+ if (flag(flags, "json")) {
746
+ console.log(toJson(status));
747
+ }
748
+ else {
749
+ console.log(`Sync Status:`);
750
+ console.log(` Tasks in database: ${status.dbTaskCount}`);
751
+ console.log(` Operations in JSONL: ${status.jsonlOpCount}`);
752
+ console.log(` Last export: ${status.lastExport ? status.lastExport.toISOString() : "(never)"}`);
753
+ console.log(` Dirty (unexported changes): ${status.isDirty ? "yes" : "no"}`);
754
+ }
755
+ }
756
+ else {
757
+ console.error(`Unknown sync subcommand: ${subcommand}`);
758
+ console.error(`Run 'tx sync --help' for usage information`);
759
+ process.exit(1);
760
+ }
761
+ })
762
+ };
763
+ // --- Main ---
764
+ const { command, positional, flags: parsedFlags } = parseArgs(process.argv);
765
+ // Handle --version early, before any command processing
766
+ if (flag(parsedFlags, "version") || flag(parsedFlags, "v")) {
767
+ console.log("tx v0.1.0");
768
+ process.exit(0);
769
+ }
770
+ // Handle --help for specific command (tx add --help) or help command (tx help / tx help add)
771
+ if (flag(parsedFlags, "help") || flag(parsedFlags, "h")) {
772
+ // Check for subcommand help (e.g., tx sync export --help)
773
+ if (command === "sync" && positional[0]) {
774
+ const subcommandKey = `sync ${positional[0]}`;
775
+ if (commandHelp[subcommandKey]) {
776
+ console.log(commandHelp[subcommandKey]);
777
+ process.exit(0);
778
+ }
779
+ }
780
+ // Check if we have a command with specific help
781
+ if (command !== "help" && commandHelp[command]) {
782
+ console.log(commandHelp[command]);
783
+ process.exit(0);
784
+ }
785
+ // Fall through to general help
786
+ console.log(HELP_TEXT);
787
+ process.exit(0);
788
+ }
789
+ // Handle 'tx help' and 'tx help <command>'
790
+ if (command === "help") {
791
+ const subcommand = positional[0];
792
+ // Check for compound command help (e.g., tx help sync export)
793
+ if (subcommand === "sync" && positional[1]) {
794
+ const subcommandKey = `sync ${positional[1]}`;
795
+ if (commandHelp[subcommandKey]) {
796
+ console.log(commandHelp[subcommandKey]);
797
+ process.exit(0);
798
+ }
799
+ }
800
+ if (subcommand && commandHelp[subcommand]) {
801
+ console.log(commandHelp[subcommand]);
802
+ }
803
+ else {
804
+ console.log(HELP_TEXT);
805
+ }
806
+ process.exit(0);
807
+ }
808
+ // Handle mcp-server separately (it manages its own runtime)
809
+ if (command === "mcp-server") {
810
+ const dbPath = typeof parsedFlags.db === "string"
811
+ ? resolve(parsedFlags.db)
812
+ : resolve(process.cwd(), ".tx", "tasks.db");
813
+ startMcpServer(dbPath).catch((err) => {
814
+ console.error(`MCP server error: ${err}`);
815
+ process.exit(1);
816
+ });
817
+ }
818
+ else {
819
+ const handler = commands[command];
820
+ if (!handler) {
821
+ console.error(`Unknown command: ${command}`);
822
+ console.error(`Run 'tx help' for usage information`);
823
+ process.exit(1);
824
+ }
825
+ const dbPath = typeof parsedFlags.db === "string"
826
+ ? resolve(parsedFlags.db)
827
+ : resolve(process.cwd(), ".tx", "tasks.db");
828
+ // For init, ensure directory exists
829
+ if (command === "init") {
830
+ const dir = resolve(process.cwd(), ".tx");
831
+ if (!existsSync(dir)) {
832
+ mkdirSync(dir, { recursive: true });
833
+ }
834
+ // Create .gitignore for .tx directory
835
+ const gitignorePath = resolve(dir, ".gitignore");
836
+ if (!existsSync(gitignorePath)) {
837
+ writeFileSync(gitignorePath, "tasks.db\ntasks.db-wal\ntasks.db-shm\n");
838
+ }
839
+ }
840
+ const layer = makeAppLayer(dbPath);
841
+ const program = handler(positional, parsedFlags);
842
+ const runnable = Effect.provide(program, layer);
843
+ Effect.runPromise(Effect.catchAll(runnable, (error) => {
844
+ const err = error;
845
+ if (err._tag === "TaskNotFoundError") {
846
+ console.error(err.message ?? `Task not found`);
847
+ process.exit(2);
848
+ }
849
+ if (err._tag === "ValidationError") {
850
+ console.error(err.message ?? `Validation error`);
851
+ process.exit(1);
852
+ }
853
+ if (err._tag === "CircularDependencyError") {
854
+ console.error(err.message ?? `Circular dependency detected`);
855
+ process.exit(1);
856
+ }
857
+ if (err._tag === "DatabaseError") {
858
+ console.error(err.message ?? `Database error`);
859
+ process.exit(1);
860
+ }
861
+ console.error(`Error: ${err.message ?? String(error)}`);
862
+ return Effect.sync(() => process.exit(1));
863
+ })).catch((err) => {
864
+ console.error(`Fatal: ${err}`);
865
+ process.exit(1);
866
+ });
867
+ }
868
+ //# sourceMappingURL=cli.js.map