@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/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/cli.js +868 -0
- package/dist/cli.js.map +1 -0
- package/dist/db.js +70 -0
- package/dist/db.js.map +1 -0
- package/dist/errors.js +22 -0
- package/dist/errors.js.map +1 -0
- package/dist/id.js +19 -0
- package/dist/id.js.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/layer.js +20 -0
- package/dist/layer.js.map +1 -0
- package/dist/mcp/server.js +453 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/repo/dep-repo.js +104 -0
- package/dist/repo/dep-repo.js.map +1 -0
- package/dist/repo/task-repo.js +140 -0
- package/dist/repo/task-repo.js.map +1 -0
- package/dist/schema.js +37 -0
- package/dist/schema.js.map +1 -0
- package/dist/schemas/sync.js +55 -0
- package/dist/schemas/sync.js.map +1 -0
- package/dist/services/dep-service.js +34 -0
- package/dist/services/dep-service.js.map +1 -0
- package/dist/services/hierarchy-service.js +66 -0
- package/dist/services/hierarchy-service.js.map +1 -0
- package/dist/services/ready-service.js +70 -0
- package/dist/services/ready-service.js.map +1 -0
- package/dist/services/score-service.js +82 -0
- package/dist/services/score-service.js.map +1 -0
- package/dist/services/sync-service.js +244 -0
- package/dist/services/sync-service.js.map +1 -0
- package/dist/services/task-service.js +201 -0
- package/dist/services/task-service.js.map +1 -0
- package/migrations/001_initial.sql +57 -0
- package/package.json +52 -0
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
|