task-memory 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/index.js +2708 -0
  4. package/package.json +39 -0
package/dist/index.js ADDED
@@ -0,0 +1,2708 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/store.ts
4
+ import { join, resolve, dirname } from "path";
5
+ import { homedir } from "os";
6
+ import { existsSync, readFileSync, writeFileSync, statSync } from "fs";
7
+
8
+ // src/utils/orderUtils.ts
9
+ function parseOrder(order) {
10
+ if (!order || order === "") {
11
+ return [];
12
+ }
13
+ return order.split("-").map((s) => parseFloat(s));
14
+ }
15
+ function formatOrder(parts) {
16
+ if (parts.length === 0) {
17
+ return "";
18
+ }
19
+ return parts.map((n) => String(n)).join("-");
20
+ }
21
+ function compareOrders(a, b) {
22
+ if (a == null && b == null)
23
+ return 0;
24
+ if (a == null)
25
+ return 1;
26
+ if (b == null)
27
+ return -1;
28
+ const partsA = parseOrder(a);
29
+ const partsB = parseOrder(b);
30
+ const maxLen = Math.max(partsA.length, partsB.length);
31
+ for (let i = 0;i < maxLen; i++) {
32
+ if (i >= partsA.length)
33
+ return -1;
34
+ if (i >= partsB.length)
35
+ return 1;
36
+ if (partsA[i] !== partsB[i]) {
37
+ return partsA[i] - partsB[i];
38
+ }
39
+ }
40
+ return 0;
41
+ }
42
+ function normalizeOrders(orders, tiebreakers) {
43
+ const indexed = orders.map((order, index) => ({ order, index }));
44
+ const nonNull = indexed.filter((item) => item.order != null && item.order !== "");
45
+ if (nonNull.length === 0) {
46
+ return orders.map((o) => o == null ? null : o);
47
+ }
48
+ const parsed = nonNull.map((item) => ({
49
+ index: item.index,
50
+ parts: parseOrder(item.order),
51
+ originalOrder: item.order
52
+ }));
53
+ parsed.sort((a, b) => {
54
+ const orderCmp = compareOrders(a.originalOrder, b.originalOrder);
55
+ if (orderCmp !== 0)
56
+ return orderCmp;
57
+ if (tiebreakers) {
58
+ const prioA = tiebreakers[a.index] ?? 0;
59
+ const prioB = tiebreakers[b.index] ?? 0;
60
+ return prioB - prioA;
61
+ }
62
+ return 0;
63
+ });
64
+ const explicitCountBySlot = new Map;
65
+ const implicitOnlySlots = new Set;
66
+ for (const item of parsed) {
67
+ const parentKey = item.parts.length === 1 ? "" : formatOrder(item.parts.slice(0, -1));
68
+ const childNum = item.parts[item.parts.length - 1];
69
+ const slotKey = `${parentKey}::${childNum}`;
70
+ explicitCountBySlot.set(slotKey, (explicitCountBySlot.get(slotKey) ?? 0) + 1);
71
+ for (let len = 1;len < item.parts.length; len++) {
72
+ const pKey = len === 1 ? "" : formatOrder(item.parts.slice(0, len - 1));
73
+ const cNum = item.parts[len - 1];
74
+ const implicitKey = `${pKey}::${cNum}`;
75
+ if (!explicitCountBySlot.has(implicitKey)) {
76
+ implicitOnlySlots.add(implicitKey);
77
+ }
78
+ }
79
+ }
80
+ const slotsByParent = new Map;
81
+ for (const [slotKey, count] of explicitCountBySlot.entries()) {
82
+ const sep = slotKey.lastIndexOf("::");
83
+ const pKey = slotKey.slice(0, sep);
84
+ const cNum = parseFloat(slotKey.slice(sep + 2));
85
+ if (!slotsByParent.has(pKey))
86
+ slotsByParent.set(pKey, new Map);
87
+ slotsByParent.get(pKey).set(cNum, count);
88
+ }
89
+ for (const slotKey of implicitOnlySlots) {
90
+ const sep = slotKey.lastIndexOf("::");
91
+ const pKey = slotKey.slice(0, sep);
92
+ const cNum = parseFloat(slotKey.slice(sep + 2));
93
+ if (!slotsByParent.has(pKey))
94
+ slotsByParent.set(pKey, new Map);
95
+ if (!slotsByParent.get(pKey).has(cNum)) {
96
+ slotsByParent.get(pKey).set(cNum, 0);
97
+ }
98
+ }
99
+ const renumberMap = new Map;
100
+ for (const [pKey, slots] of slotsByParent.entries()) {
101
+ const sorted = Array.from(slots.keys()).sort((a, b) => a - b);
102
+ const mapping = new Map;
103
+ let newNum = 1;
104
+ for (const oldNum of sorted) {
105
+ const count = slots.get(oldNum);
106
+ const newNums = [];
107
+ const occurrences = count === 0 ? 1 : count;
108
+ for (let i = 0;i < occurrences; i++) {
109
+ newNums.push(newNum++);
110
+ }
111
+ mapping.set(oldNum, newNums);
112
+ }
113
+ renumberMap.set(pKey, mapping);
114
+ }
115
+ const normalizedMap = new Map;
116
+ const occurrenceTracker = new Map;
117
+ for (const item of parsed) {
118
+ const newParts = [];
119
+ for (let i = 0;i < item.parts.length; i++) {
120
+ const originalParentKey = i === 0 ? "" : formatOrder(item.parts.slice(0, i));
121
+ const oldNum = item.parts[i];
122
+ const mapping = renumberMap.get(originalParentKey);
123
+ if (!mapping) {
124
+ throw new Error(`Internal error: no renumbering map for parent ${originalParentKey}`);
125
+ }
126
+ const newNums = mapping.get(oldNum);
127
+ if (newNums === undefined || newNums.length === 0) {
128
+ throw new Error(`Internal error: no new number for ${oldNum} in parent ${originalParentKey}`);
129
+ }
130
+ const isLeaf = i === item.parts.length - 1;
131
+ if (isLeaf && newNums.length > 1) {
132
+ const trackerKey = `${originalParentKey}::${oldNum}`;
133
+ const occIdx = occurrenceTracker.get(trackerKey) ?? 0;
134
+ occurrenceTracker.set(trackerKey, occIdx + 1);
135
+ newParts.push(newNums[occIdx] ?? newNums[newNums.length - 1]);
136
+ } else {
137
+ newParts.push(newNums[0]);
138
+ }
139
+ }
140
+ normalizedMap.set(item.index, formatOrder(newParts));
141
+ }
142
+ return orders.map((o, index) => {
143
+ if (o == null)
144
+ return null;
145
+ return normalizedMap.get(index) ?? o;
146
+ });
147
+ }
148
+ function sortByOrder(items, getOrder, getId) {
149
+ return [...items].sort((a, b) => {
150
+ const orderA = getOrder(a);
151
+ const orderB = getOrder(b);
152
+ const cmp = compareOrders(orderA, orderB);
153
+ if (cmp !== 0)
154
+ return cmp;
155
+ const idA = getId(a);
156
+ const idB = getId(b);
157
+ const numA = parseInt(idA.replace(/\D/g, ""), 10) || 0;
158
+ const numB = parseInt(idB.replace(/\D/g, ""), 10) || 0;
159
+ return numA - numB;
160
+ });
161
+ }
162
+
163
+ // src/store.ts
164
+ function findGitPath(startDir) {
165
+ let currentDir = startDir;
166
+ const home = homedir();
167
+ while (true) {
168
+ const gitPath = join(currentDir, ".git");
169
+ if (existsSync(gitPath)) {
170
+ return gitPath;
171
+ }
172
+ if (currentDir === home) {
173
+ return null;
174
+ }
175
+ const parentDir = dirname(currentDir);
176
+ if (parentDir === currentDir) {
177
+ return null;
178
+ }
179
+ currentDir = parentDir;
180
+ }
181
+ }
182
+ function getDbPath() {
183
+ if (process.env.TASK_MEMORY_PATH) {
184
+ return resolve(process.cwd(), process.env.TASK_MEMORY_PATH);
185
+ }
186
+ const gitPath = findGitPath(process.cwd());
187
+ if (gitPath) {
188
+ let isGitDir = false;
189
+ try {
190
+ isGitDir = statSync(gitPath).isDirectory();
191
+ } catch {}
192
+ if (isGitDir) {
193
+ const gitTaskMemory = join(gitPath, "task-memory.json");
194
+ if (existsSync(gitTaskMemory)) {
195
+ return gitTaskMemory;
196
+ }
197
+ }
198
+ const projectDir = dirname(gitPath);
199
+ const taskMemoryPath = join(projectDir, "task-memory.json");
200
+ if (existsSync(taskMemoryPath)) {
201
+ return taskMemoryPath;
202
+ }
203
+ const hiddenPath = join(projectDir, ".task-memory.json");
204
+ if (existsSync(hiddenPath)) {
205
+ return hiddenPath;
206
+ }
207
+ if (isGitDir) {
208
+ return join(gitPath, "task-memory.json");
209
+ }
210
+ return taskMemoryPath;
211
+ }
212
+ return join(homedir(), ".task-memory.json");
213
+ }
214
+ var DB_PATH = getDbPath();
215
+ var cachedStore = null;
216
+ var afterSaveCallback = null;
217
+ function setAfterSaveCallback(callback) {
218
+ afterSaveCallback = callback;
219
+ }
220
+ function loadStore() {
221
+ if (!existsSync(DB_PATH)) {
222
+ return { tasks: [] };
223
+ }
224
+ try {
225
+ const data = readFileSync(DB_PATH, "utf-8");
226
+ const parsed = JSON.parse(data);
227
+ if (Array.isArray(parsed)) {
228
+ return { tasks: parsed };
229
+ }
230
+ return parsed;
231
+ } catch (e) {
232
+ console.error(`Error loading store from ${DB_PATH}:`, e);
233
+ return { tasks: [] };
234
+ }
235
+ }
236
+ function saveStore(store) {
237
+ try {
238
+ writeFileSync(DB_PATH, JSON.stringify(store, null, 2), "utf-8");
239
+ cachedStore = store;
240
+ if (afterSaveCallback) {
241
+ afterSaveCallback(store);
242
+ }
243
+ } catch (e) {
244
+ console.error(`Error saving store to ${DB_PATH}:`, e);
245
+ }
246
+ }
247
+ function loadTasks() {
248
+ const store = loadStore();
249
+ cachedStore = store;
250
+ return store.tasks;
251
+ }
252
+ function normalizeTaskOrders(tasks) {
253
+ const activeIndices = [];
254
+ const activeOrders = [];
255
+ const activeTiebreakers = [];
256
+ tasks.forEach((task, index) => {
257
+ if (task.status === "todo" || task.status === "wip") {
258
+ activeIndices.push(index);
259
+ activeOrders.push(task.order ?? null);
260
+ activeTiebreakers.push(new Date(task.updated_at || 0).getTime());
261
+ }
262
+ });
263
+ const normalizedOrders = normalizeOrders(activeOrders, activeTiebreakers);
264
+ const result = tasks.map((task, index) => {
265
+ if (task.status === "todo" || task.status === "wip") {
266
+ const activeIndex = activeIndices.indexOf(index);
267
+ if (activeIndex !== -1) {
268
+ const newOrder = normalizedOrders[activeIndex];
269
+ if (task.order !== newOrder) {
270
+ return { ...task, order: newOrder };
271
+ }
272
+ }
273
+ return task;
274
+ } else {
275
+ if (task.order != null) {
276
+ return { ...task, order: null };
277
+ }
278
+ return task;
279
+ }
280
+ });
281
+ return result;
282
+ }
283
+ function saveTasks(tasks) {
284
+ const store = cachedStore || loadStore();
285
+ store.tasks = normalizeTaskOrders(tasks);
286
+ saveStore(store);
287
+ }
288
+ function loadSyncConfig() {
289
+ const store = cachedStore || loadStore();
290
+ return store.sync;
291
+ }
292
+ function saveSyncConfig(sync) {
293
+ const store = cachedStore || loadStore();
294
+ store.sync = sync;
295
+ saveStore(store);
296
+ }
297
+ function getTaskById(tasks, idOrIndex) {
298
+ if (typeof idOrIndex === "number") {
299
+ const targetId = `TASK-${idOrIndex}`;
300
+ return tasks.find((t) => t.id === targetId);
301
+ }
302
+ const idStr = idOrIndex.toString();
303
+ if (idStr.match(/^\d+$/)) {
304
+ return tasks.find((t) => t.id === `TASK-${idStr}`);
305
+ }
306
+ return tasks.find((t) => t.id === idStr);
307
+ }
308
+ function getNextId(tasks) {
309
+ let max = 0;
310
+ for (const task of tasks) {
311
+ const match = task.id.match(/^TASK-(\d+)$/);
312
+ if (match) {
313
+ const num = parseInt(match[1], 10);
314
+ if (num > max)
315
+ max = num;
316
+ }
317
+ }
318
+ return `TASK-${max + 1}`;
319
+ }
320
+
321
+ // src/utils/taskBuilder.ts
322
+ function parseTaskArgs(args) {
323
+ const summaryParts = [];
324
+ let status = "todo";
325
+ let priority;
326
+ let goal;
327
+ let order;
328
+ const bodies = [];
329
+ const addFiles = [];
330
+ const readFiles = [];
331
+ for (let i = 0;i < args.length; i++) {
332
+ const arg = args[i];
333
+ if (!arg)
334
+ continue;
335
+ if (arg.startsWith("-")) {
336
+ switch (arg) {
337
+ case "--status":
338
+ case "-s":
339
+ const s = args[i + 1];
340
+ if (s && !s.startsWith("-")) {
341
+ if (["todo", "wip", "done", "pending", "long", "closed"].includes(s)) {
342
+ status = s;
343
+ i++;
344
+ } else {
345
+ throw new Error(`Invalid status '${s}'. Allowed: todo, wip, done, pending, long, closed.`);
346
+ }
347
+ } else {
348
+ throw new Error("--status requires a value.");
349
+ }
350
+ break;
351
+ case "--goal":
352
+ case "-g":
353
+ const g = args[i + 1];
354
+ if (g && !g.startsWith("-")) {
355
+ goal = g;
356
+ i++;
357
+ } else {
358
+ throw new Error("--goal requires a value.");
359
+ }
360
+ break;
361
+ case "--priority":
362
+ case "-p":
363
+ const p = args[i + 1];
364
+ if (p && !p.startsWith("-")) {
365
+ priority = p;
366
+ i++;
367
+ } else {
368
+ throw new Error("--priority requires a value.");
369
+ }
370
+ break;
371
+ case "--order":
372
+ case "-o":
373
+ const o = args[i + 1];
374
+ if (o && !o.startsWith("-")) {
375
+ order = o === "null" ? null : o;
376
+ i++;
377
+ } else {
378
+ throw new Error("--order requires a value.");
379
+ }
380
+ break;
381
+ case "--body":
382
+ case "-b":
383
+ const b = args[i + 1];
384
+ if (b && !b.startsWith("-")) {
385
+ bodies.push(b);
386
+ i++;
387
+ } else {
388
+ throw new Error("--body requires a value.");
389
+ }
390
+ break;
391
+ case "--add-file":
392
+ case "-a":
393
+ const af = args[i + 1];
394
+ if (af && !af.startsWith("-")) {
395
+ addFiles.push(af);
396
+ i++;
397
+ } else {
398
+ throw new Error("--add-file requires a path.");
399
+ }
400
+ break;
401
+ case "--read-file":
402
+ case "-r":
403
+ const rf = args[i + 1];
404
+ if (rf && !rf.startsWith("-")) {
405
+ readFiles.push(rf);
406
+ i++;
407
+ } else {
408
+ throw new Error("--read-file requires a path.");
409
+ }
410
+ break;
411
+ default:
412
+ throw new Error(`Unknown option '${arg}'.`);
413
+ }
414
+ } else {
415
+ summaryParts.push(arg);
416
+ }
417
+ }
418
+ return {
419
+ summary: summaryParts.join(" ") || undefined,
420
+ status,
421
+ priority,
422
+ goal,
423
+ order,
424
+ bodies,
425
+ addFiles,
426
+ readFiles
427
+ };
428
+ }
429
+ function buildTask(id, options) {
430
+ const now = new Date().toISOString();
431
+ const status = options.status || "todo";
432
+ const order = status === "todo" || status === "wip" ? options.order ?? null : null;
433
+ return {
434
+ id,
435
+ status,
436
+ priority: options.priority,
437
+ version: options.version || "tbd",
438
+ goal: options.goal,
439
+ order,
440
+ summary: options.summary || "",
441
+ bodies: (options.bodies || []).map((text) => ({ text, created_at: now })),
442
+ files: {
443
+ read: options.readFiles || [],
444
+ edit: options.addFiles || []
445
+ },
446
+ created_at: now,
447
+ updated_at: now
448
+ };
449
+ }
450
+
451
+ // src/commands/new.ts
452
+ function newCommand(args) {
453
+ if (args.includes("--help") || args.includes("-h")) {
454
+ console.log(`
455
+ Usage: tm new <summary> [options]
456
+
457
+ Options:
458
+ --status, -s <status> Set initial status (todo, wip, done, pending, long, closed)
459
+ --priority, -p <value> Set priority
460
+ --goal, -g <text> Set completion goal
461
+ --order, -o <value> Set progress order (e.g., 1, 1-1, 2-3)
462
+ --body, -b <text> Add initial body text
463
+ --add-file, -a <path> Add editable file
464
+ --read-file, -r <path> Add read-only file
465
+ `);
466
+ return;
467
+ }
468
+ let options;
469
+ try {
470
+ options = parseTaskArgs(args);
471
+ } catch (error) {
472
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
473
+ return;
474
+ }
475
+ if (!options.summary) {
476
+ console.error("Error: Task summary is required. Usage: tm new <summary> [options]");
477
+ return;
478
+ }
479
+ const tasks = loadTasks();
480
+ const id = getNextId(tasks);
481
+ const newTask = buildTask(id, options);
482
+ tasks.push(newTask);
483
+ saveTasks(tasks);
484
+ console.log(`${id} ${options.summary}`);
485
+ }
486
+
487
+ // src/reviewStore.ts
488
+ import { join as join2, dirname as dirname2 } from "path";
489
+ import { homedir as homedir2 } from "os";
490
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, statSync as statSync2 } from "fs";
491
+ function getReviewDbPath() {
492
+ if (process.env.REVIEW_MEMORY_PATH) {
493
+ return process.env.REVIEW_MEMORY_PATH;
494
+ }
495
+ const gitPath = findGitPath(process.cwd());
496
+ if (gitPath) {
497
+ let isGitDir = false;
498
+ try {
499
+ isGitDir = statSync2(gitPath).isDirectory();
500
+ } catch {}
501
+ if (isGitDir) {
502
+ return join2(gitPath, "review-memory.json");
503
+ }
504
+ return join2(dirname2(gitPath), "review-memory.json");
505
+ }
506
+ return join2(homedir2(), ".review-memory.json");
507
+ }
508
+ var DB_PATH2 = getReviewDbPath();
509
+ function loadReviews() {
510
+ if (!existsSync2(DB_PATH2)) {
511
+ return [];
512
+ }
513
+ try {
514
+ const data = readFileSync2(DB_PATH2, "utf-8");
515
+ const store = JSON.parse(data);
516
+ if (Array.isArray(store)) {
517
+ return store;
518
+ }
519
+ return store.reviews || [];
520
+ } catch (e) {
521
+ console.error(`Error loading reviews from ${DB_PATH2}:`, e);
522
+ return [];
523
+ }
524
+ }
525
+ function saveReviews(reviews) {
526
+ try {
527
+ writeFileSync2(DB_PATH2, JSON.stringify(reviews, null, 2), "utf-8");
528
+ } catch (e) {
529
+ console.error(`Error saving reviews to ${DB_PATH2}:`, e);
530
+ }
531
+ }
532
+ function getReviewById(reviews, idOrIndex) {
533
+ if (typeof idOrIndex === "number") {
534
+ const targetId = `REVIEW-${idOrIndex}`;
535
+ return reviews.find((r) => r.id === targetId);
536
+ }
537
+ const idStr = idOrIndex.toString();
538
+ if (idStr.match(/^\d+$/)) {
539
+ return reviews.find((r) => r.id === `REVIEW-${idStr}`);
540
+ }
541
+ return reviews.find((r) => r.id === idStr);
542
+ }
543
+ function getNextReviewId(reviews) {
544
+ let max = 0;
545
+ for (const review of reviews) {
546
+ const match = review.id.match(/^REVIEW-(\d+)$/);
547
+ if (match) {
548
+ const num = parseInt(match[1], 10);
549
+ if (num > max)
550
+ max = num;
551
+ }
552
+ }
553
+ return `REVIEW-${max + 1}`;
554
+ }
555
+
556
+ // src/commands/list.ts
557
+ function listCommand(args = []) {
558
+ if (args.includes("--help") || args.includes("-h")) {
559
+ console.log(`
560
+ Usage: tm list [options]
561
+
562
+ Options:
563
+ --status-all, -a Show all tasks (including done/closed)
564
+ --open Show all open tasks (todo, wip, pending, long)
565
+ --priority <p> Filter by priority
566
+ --status, -s <s> Filter by status
567
+ --version <v> Filter by version
568
+ --tbd Filter by version 'tbd' (includes closed/done)
569
+ --released Filter by released tasks (non-tbd version, includes closed/done)
570
+ --sort <key> Sort by: order (default), id, created
571
+ --head [N] Show first N tasks (default: 10)
572
+ --tail [N] Show last N tasks (default: 10)
573
+ `);
574
+ return;
575
+ }
576
+ function parseNumericOption(optionName, defaultValue) {
577
+ const index = args.indexOf(optionName);
578
+ if (index === -1)
579
+ return null;
580
+ const nextArg = args[index + 1];
581
+ if (nextArg && !nextArg.startsWith("-")) {
582
+ const parsed = parseInt(nextArg, 10);
583
+ return isNaN(parsed) ? defaultValue : parsed;
584
+ }
585
+ return defaultValue;
586
+ }
587
+ let showAll = false;
588
+ let showOpen = false;
589
+ let filterPriority = null;
590
+ let filterStatus = null;
591
+ let filterVersion = null;
592
+ let released = false;
593
+ let sortBy = "order";
594
+ for (let i = 0;i < args.length; i++) {
595
+ const arg = args[i];
596
+ switch (arg) {
597
+ case "--status-all":
598
+ case "-a":
599
+ showAll = true;
600
+ break;
601
+ case "--open":
602
+ showOpen = true;
603
+ break;
604
+ case "--priority":
605
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
606
+ filterPriority = args[++i];
607
+ } else {
608
+ console.error("Error: --priority requires a value.");
609
+ return;
610
+ }
611
+ break;
612
+ case "--status":
613
+ case "-s":
614
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
615
+ filterStatus = args[++i];
616
+ } else {
617
+ console.error("Error: --status requires a value.");
618
+ return;
619
+ }
620
+ break;
621
+ case "--version":
622
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
623
+ filterVersion = args[++i];
624
+ } else {
625
+ console.error("Error: --version requires a value.");
626
+ return;
627
+ }
628
+ break;
629
+ case "--tbd":
630
+ filterVersion = "tbd";
631
+ showAll = true;
632
+ break;
633
+ case "--released":
634
+ released = true;
635
+ showAll = true;
636
+ break;
637
+ case "--sort":
638
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
639
+ const sortValue = args[++i];
640
+ if (sortValue === "order" || sortValue === "id" || sortValue === "created") {
641
+ sortBy = sortValue;
642
+ } else {
643
+ console.error(`Error: Invalid sort key '${sortValue}'. Allowed: order, id, created.`);
644
+ return;
645
+ }
646
+ } else {
647
+ console.error("Error: --sort requires a value.");
648
+ return;
649
+ }
650
+ break;
651
+ case "--head":
652
+ case "--tail":
653
+ const nextArg = args[i + 1];
654
+ if (nextArg && !nextArg.startsWith("-")) {
655
+ i++;
656
+ }
657
+ break;
658
+ default:
659
+ if (arg.startsWith("--")) {
660
+ console.error(`Error: Unknown option '${arg}'.`);
661
+ return;
662
+ } else if (!arg.match(/^\d+$/)) {
663
+ console.error(`Error: Unknown argument '${arg}'.`);
664
+ return;
665
+ }
666
+ break;
667
+ }
668
+ }
669
+ const headCount = parseNumericOption("--head", 10);
670
+ const tailCount = parseNumericOption("--tail", 10);
671
+ const tasks = loadTasks();
672
+ const activeTasks = tasks.filter((t) => {
673
+ if (filterPriority && t.priority !== filterPriority)
674
+ return false;
675
+ if (filterStatus && t.status !== filterStatus)
676
+ return false;
677
+ if (filterVersion && t.version !== filterVersion)
678
+ return false;
679
+ if (released) {
680
+ if (!t.version || t.version === "tbd" || t.version === "")
681
+ return false;
682
+ }
683
+ if (filterStatus)
684
+ return true;
685
+ if (showAll)
686
+ return true;
687
+ if (t.status === "done" || t.status === "closed")
688
+ return false;
689
+ if (!showOpen && (t.status === "pending" || t.status === "long"))
690
+ return false;
691
+ return true;
692
+ });
693
+ let sortedTasks = activeTasks;
694
+ switch (sortBy) {
695
+ case "order":
696
+ sortedTasks = sortByOrder(activeTasks, (t) => t.order ?? null, (t) => t.id);
697
+ break;
698
+ case "id":
699
+ sortedTasks = [...activeTasks].sort((a, b) => {
700
+ const numA = parseInt(a.id.replace(/\D/g, ""), 10) || 0;
701
+ const numB = parseInt(b.id.replace(/\D/g, ""), 10) || 0;
702
+ return numA - numB;
703
+ });
704
+ break;
705
+ case "created":
706
+ sortedTasks = [...activeTasks].sort((a, b) => {
707
+ return a.created_at.localeCompare(b.created_at);
708
+ });
709
+ break;
710
+ }
711
+ const reviews = loadReviews();
712
+ const checkingReviews = reviews.filter((r) => r.status === "checking");
713
+ let displayTasks = sortedTasks;
714
+ let displayReviews = checkingReviews;
715
+ if (headCount !== null) {
716
+ displayTasks = sortedTasks.slice(0, headCount);
717
+ const remaining = headCount - displayTasks.length;
718
+ displayReviews = remaining > 0 ? checkingReviews.slice(0, remaining) : [];
719
+ } else if (tailCount !== null) {
720
+ const totalLength = sortedTasks.length + checkingReviews.length;
721
+ const startIndex = Math.max(0, totalLength - tailCount);
722
+ const tasksToSkip = Math.min(startIndex, sortedTasks.length);
723
+ displayTasks = sortedTasks.slice(tasksToSkip);
724
+ const reviewsToSkip = Math.max(0, startIndex - sortedTasks.length);
725
+ displayReviews = checkingReviews.slice(reviewsToSkip);
726
+ }
727
+ if (displayTasks.length === 0 && displayReviews.length === 0) {
728
+ return;
729
+ }
730
+ displayTasks.forEach((task) => {
731
+ const match = task.id.match(/^TASK-(\d+)$/);
732
+ const displayId = match ? match[1] : task.id;
733
+ const priorityStr = task.priority ? ` (Priority: ${task.priority})` : "";
734
+ let versionStr = "";
735
+ if (task.version && task.version !== "tbd") {
736
+ versionStr = ` [v:${task.version}]`;
737
+ }
738
+ console.log(`${displayId}: ${task.summary} [${task.status}]${priorityStr}${versionStr}`);
739
+ });
740
+ displayReviews.forEach((review) => {
741
+ console.log(`${review.id}: ${review.title} [${review.status}]`);
742
+ });
743
+ }
744
+
745
+ // src/commands/get.ts
746
+ function getCommand(args) {
747
+ if (args.includes("--help") || args.includes("-h")) {
748
+ console.log(`
749
+ Usage: tm get <id...> [options]
750
+
751
+ Options:
752
+ --all, -a, --history Show full history of bodies
753
+ `);
754
+ return;
755
+ }
756
+ const showAllHistory = args.includes("--all") || args.includes("-a") || args.includes("--history");
757
+ const ids = [];
758
+ for (const arg of args) {
759
+ if (arg.startsWith("-")) {
760
+ if (arg !== "--all" && arg !== "-a" && arg !== "--history") {
761
+ console.error(`Error: Unknown option '${arg}'.`);
762
+ return;
763
+ }
764
+ } else {
765
+ ids.push(arg);
766
+ }
767
+ }
768
+ if (ids.length === 0) {
769
+ console.error("Error: Task ID is required. Usage: tm get <id...> [options]");
770
+ return;
771
+ }
772
+ const tasks = loadTasks();
773
+ const result = [];
774
+ for (const id of ids) {
775
+ const task = getTaskById(tasks, id);
776
+ if (task) {
777
+ const taskOutput = { ...task };
778
+ if (!showAllHistory && task.bodies.length > 0) {
779
+ const lastBody = task.bodies[task.bodies.length - 1];
780
+ if (lastBody) {
781
+ taskOutput.bodies = [lastBody];
782
+ }
783
+ }
784
+ result.push(taskOutput);
785
+ } else {
786
+ console.error(`Error: ID '${id}' not found.`);
787
+ }
788
+ }
789
+ console.log(JSON.stringify(result, null, 2));
790
+ }
791
+
792
+ // src/commands/finish.ts
793
+ function finishCommand(args) {
794
+ if (args.includes("--help") || args.includes("-h")) {
795
+ console.log(`
796
+ Usage: tm finish <id...> [options]
797
+
798
+ Options:
799
+ --body <body> Add a closing comment
800
+ `);
801
+ return;
802
+ }
803
+ if (args.length === 0) {
804
+ console.error("Error: Task ID is required. Usage: tm finish <id...> [--body <body>]");
805
+ return;
806
+ }
807
+ const tasks = loadTasks();
808
+ let currentTargetIds = [];
809
+ let updated = false;
810
+ let lastActionWasOption = false;
811
+ for (let i = 0;i < args.length; i++) {
812
+ const arg = args[i];
813
+ if (!arg)
814
+ continue;
815
+ if (arg.startsWith("--")) {
816
+ lastActionWasOption = true;
817
+ if (arg === "--body") {
818
+ if (currentTargetIds.length === 0) {
819
+ console.error("Error: --body must be preceded by at least one task ID.");
820
+ return;
821
+ }
822
+ const bodyText = args[++i];
823
+ if (bodyText) {
824
+ const now = new Date().toISOString();
825
+ for (const id of currentTargetIds) {
826
+ const task = getTaskById(tasks, id);
827
+ if (task) {
828
+ task.bodies.push({ text: bodyText, created_at: now });
829
+ task.updated_at = now;
830
+ }
831
+ }
832
+ } else {
833
+ console.error("Error: --body requires a text argument.");
834
+ }
835
+ } else {
836
+ console.error(`Error: Unknown option '${arg}'.`);
837
+ return;
838
+ }
839
+ } else {
840
+ if (lastActionWasOption) {
841
+ currentTargetIds = [];
842
+ lastActionWasOption = false;
843
+ }
844
+ const id = arg;
845
+ const task = getTaskById(tasks, id);
846
+ if (task) {
847
+ if (task.status !== "done") {
848
+ task.status = "done";
849
+ task.updated_at = new Date().toISOString();
850
+ console.log(`Task ${task.id} marked as done.`);
851
+ updated = true;
852
+ } else {
853
+ console.log(`Task ${task.id} is already done.`);
854
+ }
855
+ currentTargetIds.push(id);
856
+ } else {
857
+ console.error(`Error: ID '${id}' not found.`);
858
+ }
859
+ }
860
+ }
861
+ if (updated) {
862
+ saveTasks(tasks);
863
+ }
864
+ }
865
+
866
+ // src/commands/update.ts
867
+ function updateCommand(args) {
868
+ if (args.includes("--help") || args.includes("-h")) {
869
+ console.log(`
870
+ Usage: tm update <id...> [options]
871
+
872
+ Options:
873
+ --status, -s <status> Update status (todo, wip, done, pending, long, closed)
874
+ --priority, -p <value> Update priority
875
+ --version, -v <value> Update version
876
+ --goal, -g <text> Update completion goal
877
+ --order, -o <value> Update progress order (use 'null' to clear)
878
+ --body, -b <text> Append body text
879
+ --add-file, -a <path> Add editable file
880
+ --rm-file, -d <path> Remove editable file
881
+ --read-file, -r <path> Add read-only file
882
+ `);
883
+ return;
884
+ }
885
+ const tasks = loadTasks();
886
+ let currentTargetIds = [];
887
+ let updated = false;
888
+ let lastActionWasOption = false;
889
+ const applyUpdate = (action) => {
890
+ if (currentTargetIds.length === 0) {
891
+ console.error("Error: No task ID specified for update. Usage: tm update <id> [options] ...");
892
+ return;
893
+ }
894
+ for (const id of currentTargetIds) {
895
+ const task = getTaskById(tasks, id);
896
+ if (task) {
897
+ action(task);
898
+ task.updated_at = new Date().toISOString();
899
+ updated = true;
900
+ } else {
901
+ console.error(`Error: ID '${id}' not found.`);
902
+ }
903
+ }
904
+ };
905
+ for (let i = 0;i < args.length; i++) {
906
+ const arg = args[i];
907
+ if (!arg)
908
+ continue;
909
+ if (arg.startsWith("-")) {
910
+ lastActionWasOption = true;
911
+ switch (arg) {
912
+ case "--status":
913
+ case "-s":
914
+ const status = args[++i];
915
+ if (status && ["todo", "wip", "done", "pending", "long", "closed"].includes(status)) {
916
+ applyUpdate((t) => {
917
+ t.status = status;
918
+ if (status !== "todo" && status !== "wip") {
919
+ t.order = null;
920
+ }
921
+ });
922
+ } else {
923
+ console.error(`Error: Invalid status '${status}'. Allowed: todo, wip, done, pending, long, closed.`);
924
+ }
925
+ break;
926
+ case "--priority":
927
+ case "-p":
928
+ const priority = args[++i];
929
+ if (priority) {
930
+ applyUpdate((t) => t.priority = priority);
931
+ } else {
932
+ console.error("Error: --priority requires a value.");
933
+ }
934
+ break;
935
+ case "--version":
936
+ case "-v":
937
+ const version = args[++i];
938
+ if (version) {
939
+ applyUpdate((t) => t.version = version);
940
+ } else {
941
+ console.error("Error: --version requires a value.");
942
+ }
943
+ break;
944
+ case "--goal":
945
+ case "-g":
946
+ const goal = args[++i];
947
+ if (goal) {
948
+ applyUpdate((t) => t.goal = goal);
949
+ } else {
950
+ console.error("Error: --goal requires a value.");
951
+ }
952
+ break;
953
+ case "--order":
954
+ case "-o":
955
+ const order = args[++i];
956
+ if (order) {
957
+ applyUpdate((t) => {
958
+ if (t.status === "todo" || t.status === "wip") {
959
+ t.order = order === "null" ? null : order;
960
+ } else {
961
+ console.error(`Error: Cannot set order for task with status '${t.status}'. Only todo/wip allowed.`);
962
+ }
963
+ });
964
+ } else {
965
+ console.error("Error: --order requires a value.");
966
+ }
967
+ break;
968
+ case "--body":
969
+ case "-b":
970
+ const bodyText = args[++i];
971
+ if (bodyText) {
972
+ applyUpdate((t) => t.bodies.push({
973
+ text: bodyText,
974
+ created_at: new Date().toISOString()
975
+ }));
976
+ } else {
977
+ console.error("Error: --body requires a text argument.");
978
+ }
979
+ break;
980
+ case "--add-file":
981
+ case "-a":
982
+ const addPath = args[++i];
983
+ if (addPath) {
984
+ applyUpdate((t) => {
985
+ if (!t.files.edit.includes(addPath)) {
986
+ t.files.edit.push(addPath);
987
+ }
988
+ });
989
+ } else {
990
+ console.error("Error: --add-file requires a path argument.");
991
+ }
992
+ break;
993
+ case "--rm-file":
994
+ case "-d":
995
+ const rmPath = args[++i];
996
+ if (rmPath) {
997
+ applyUpdate((t) => {
998
+ t.files.edit = t.files.edit.filter((p) => p !== rmPath);
999
+ });
1000
+ } else {
1001
+ console.error("Error: --rm-file requires a path argument.");
1002
+ }
1003
+ break;
1004
+ case "--read-file":
1005
+ case "-r":
1006
+ const readPath = args[++i];
1007
+ if (readPath) {
1008
+ applyUpdate((t) => {
1009
+ if (!t.files.read.includes(readPath)) {
1010
+ t.files.read.push(readPath);
1011
+ }
1012
+ });
1013
+ } else {
1014
+ console.error("Error: --read-file requires a path argument.");
1015
+ }
1016
+ break;
1017
+ default:
1018
+ console.error(`Error: Unknown option '${arg}'.`);
1019
+ return;
1020
+ }
1021
+ } else {
1022
+ if (lastActionWasOption) {
1023
+ currentTargetIds = [];
1024
+ lastActionWasOption = false;
1025
+ }
1026
+ currentTargetIds.push(arg);
1027
+ }
1028
+ }
1029
+ if (updated) {
1030
+ saveTasks(tasks);
1031
+ console.log("Tasks updated.");
1032
+ }
1033
+ }
1034
+
1035
+ // src/commands/env.ts
1036
+ function envCommand(args = []) {
1037
+ if (args.includes("--help") || args.includes("-h")) {
1038
+ console.log(`
1039
+ Usage: tm env
1040
+
1041
+ Description:
1042
+ Show the current task data file path.
1043
+ `);
1044
+ return;
1045
+ }
1046
+ if (args.length > 0) {
1047
+ console.error(`Error: env command doesn't accept arguments.`);
1048
+ return;
1049
+ }
1050
+ console.log(getDbPath());
1051
+ }
1052
+
1053
+ // src/commands/review.ts
1054
+ function reviewCommand(args) {
1055
+ if (args.includes("--help") || args.includes("-h")) {
1056
+ console.log(`
1057
+ Usage: tm review <command> [args]
1058
+
1059
+ Commands:
1060
+ new <title> --body <body>
1061
+ list
1062
+ get <id> [--history]
1063
+ update <id> [--status <status> --body <body>]
1064
+ return <id> [--status <status> --body <body>]
1065
+ accept <id> [--new <summary>...]
1066
+ reject <id>
1067
+ `);
1068
+ return;
1069
+ }
1070
+ const subcommand = args[0];
1071
+ const subArgs = args.slice(1);
1072
+ switch (subcommand) {
1073
+ case "new":
1074
+ handleNew(subArgs);
1075
+ break;
1076
+ case "list":
1077
+ handleList();
1078
+ break;
1079
+ case "get":
1080
+ handleGet(subArgs);
1081
+ break;
1082
+ case "update":
1083
+ handleUpdate(subArgs);
1084
+ break;
1085
+ case "return":
1086
+ handleReturn(subArgs);
1087
+ break;
1088
+ case "accept":
1089
+ handleAccept(subArgs);
1090
+ break;
1091
+ case "reject":
1092
+ handleReject(subArgs);
1093
+ break;
1094
+ default:
1095
+ console.error(`Unknown review subcommand: ${subcommand}`);
1096
+ console.log(`
1097
+ Usage: tm review <command> [args]
1098
+
1099
+ Commands:
1100
+ new <title> --body <body>
1101
+ list
1102
+ get <id> [--history]
1103
+ update <id> [--status <status> --body <body>]
1104
+ return <id> [--status <status> --body <body>]
1105
+ accept <id> [--new <summary>...]
1106
+ reject <id>
1107
+ `);
1108
+ process.exit(1);
1109
+ }
1110
+ }
1111
+ function handleNew(args) {
1112
+ let title = "";
1113
+ let body = "";
1114
+ const titleParts = [];
1115
+ let i = 0;
1116
+ while (i < args.length) {
1117
+ if (args[i].startsWith("--"))
1118
+ break;
1119
+ titleParts.push(args[i]);
1120
+ i++;
1121
+ }
1122
+ title = titleParts.join(" ");
1123
+ while (i < args.length) {
1124
+ if (args[i] === "--body") {
1125
+ body = args[i + 1] || "";
1126
+ i += 2;
1127
+ } else if (args[i].startsWith("--")) {
1128
+ console.error(`Error: Unknown option '${args[i]}'.`);
1129
+ return;
1130
+ } else {
1131
+ console.error(`Error: Unknown argument '${args[i]}'.`);
1132
+ return;
1133
+ }
1134
+ }
1135
+ if (!title) {
1136
+ console.error("Error: Title is required");
1137
+ return;
1138
+ }
1139
+ const reviews = loadReviews();
1140
+ const newId = getNextReviewId(reviews);
1141
+ const now = new Date().toISOString();
1142
+ const newReview = {
1143
+ id: newId,
1144
+ title,
1145
+ bodies: [{ text: body, created_at: now }],
1146
+ status: "todo",
1147
+ created_at: now,
1148
+ updated_at: now
1149
+ };
1150
+ reviews.push(newReview);
1151
+ saveReviews(reviews);
1152
+ console.log(`Created review ${newId}`);
1153
+ }
1154
+ function handleList() {
1155
+ const reviews = loadReviews();
1156
+ const activeReviews = reviews.filter((r) => r.status !== "closed" && r.status !== "done");
1157
+ if (activeReviews.length === 0) {
1158
+ console.log("No active reviews.");
1159
+ return;
1160
+ }
1161
+ console.log("ID\tStatus\tTitle");
1162
+ console.log("--\t------\t-----");
1163
+ activeReviews.forEach((r) => {
1164
+ console.log(`${r.id} ${r.status} ${r.title}`);
1165
+ });
1166
+ }
1167
+ function handleGet(args) {
1168
+ const id = args[0];
1169
+ const showHistory = args.includes("--history") || args.includes("--all");
1170
+ if (!id) {
1171
+ console.error("Error: Review ID is required");
1172
+ process.exit(1);
1173
+ }
1174
+ const reviews = loadReviews();
1175
+ const review = getReviewById(reviews, id);
1176
+ if (!review) {
1177
+ console.error(`Error: Review ${id} not found`);
1178
+ process.exit(1);
1179
+ }
1180
+ if (showHistory) {
1181
+ console.log(JSON.stringify(review, null, 2));
1182
+ return;
1183
+ }
1184
+ console.log(`Review: ${review.title}`);
1185
+ console.log(`ID: ${review.id}`);
1186
+ console.log(`Status: ${review.status}`);
1187
+ console.log("---");
1188
+ console.log("Description:");
1189
+ const description = review.bodies[0]?.text || "";
1190
+ console.log(description);
1191
+ if (review.bodies.length > 1) {
1192
+ console.log(`
1193
+ Answers:`);
1194
+ review.bodies.slice(1).forEach((b, i) => {
1195
+ console.log(`
1196
+ [${i + 1}] ${b.created_at}`);
1197
+ console.log(b.text);
1198
+ });
1199
+ }
1200
+ console.log(`
1201
+ To reply to this review, run:
1202
+ tm review return ${id} --body "Your reply here"`);
1203
+ }
1204
+ function handleUpdate(args) {
1205
+ const id = args[0];
1206
+ if (!id || id.startsWith("--")) {
1207
+ console.error("Error: Review ID is required");
1208
+ process.exit(1);
1209
+ }
1210
+ const reviews = loadReviews();
1211
+ const review = getReviewById(reviews, id);
1212
+ if (!review) {
1213
+ console.error(`Error: Review ${id} not found`);
1214
+ process.exit(1);
1215
+ }
1216
+ let status;
1217
+ let body;
1218
+ let i = 1;
1219
+ while (i < args.length) {
1220
+ if (args[i] === "--status") {
1221
+ const newStatus = args[i + 1];
1222
+ const validStatuses = ["todo", "wip", "checking", "closed", "done", "pending"];
1223
+ if (!validStatuses.includes(newStatus)) {
1224
+ console.error(`Error: Invalid status '${newStatus}'. Valid statuses are: ${validStatuses.join(", ")}`);
1225
+ process.exit(1);
1226
+ }
1227
+ status = newStatus;
1228
+ i += 2;
1229
+ } else if (args[i] === "--body") {
1230
+ body = args[i + 1];
1231
+ i += 2;
1232
+ } else if (args[i].startsWith("--")) {
1233
+ console.error(`Error: Unknown option '${args[i]}'.`);
1234
+ return;
1235
+ } else {
1236
+ console.error(`Error: Unknown argument '${args[i]}'.`);
1237
+ return;
1238
+ }
1239
+ }
1240
+ const now = new Date().toISOString();
1241
+ let updated = false;
1242
+ if (status) {
1243
+ review.status = status;
1244
+ updated = true;
1245
+ }
1246
+ if (body) {
1247
+ review.bodies.push({ text: body, created_at: now });
1248
+ updated = true;
1249
+ }
1250
+ if (updated) {
1251
+ review.updated_at = now;
1252
+ saveReviews(reviews);
1253
+ console.log(`Updated review ${review.id}`);
1254
+ } else {
1255
+ console.log("No changes made.");
1256
+ }
1257
+ }
1258
+ function handleReturn(args) {
1259
+ if (!args.includes("--status")) {
1260
+ args.push("--status", "checking");
1261
+ }
1262
+ handleUpdate(args);
1263
+ }
1264
+ function handleAccept(args) {
1265
+ const id = args[0];
1266
+ if (!id || id.startsWith("--")) {
1267
+ console.error("Error: Review ID is required");
1268
+ process.exit(1);
1269
+ }
1270
+ const reviews = loadReviews();
1271
+ const review = getReviewById(reviews, id);
1272
+ if (!review) {
1273
+ console.error(`Error: Review ${id} not found`);
1274
+ process.exit(1);
1275
+ }
1276
+ const newTasksArgs = [];
1277
+ let currentNewArgs = [];
1278
+ let collectingNew = false;
1279
+ for (let i = 1;i < args.length; i++) {
1280
+ if (args[i] === "--new") {
1281
+ if (collectingNew && currentNewArgs.length > 0) {
1282
+ newTasksArgs.push([...currentNewArgs]);
1283
+ }
1284
+ currentNewArgs = [];
1285
+ collectingNew = true;
1286
+ } else if (collectingNew) {
1287
+ if (args[i] === "--add") {
1288
+ if (collectingNew && currentNewArgs.length > 0) {
1289
+ newTasksArgs.push([...currentNewArgs]);
1290
+ }
1291
+ collectingNew = false;
1292
+ } else {
1293
+ currentNewArgs.push(args[i]);
1294
+ }
1295
+ }
1296
+ }
1297
+ if (collectingNew && currentNewArgs.length > 0) {
1298
+ newTasksArgs.push([...currentNewArgs]);
1299
+ }
1300
+ const tasks = loadTasks();
1301
+ const createdTaskIds = [];
1302
+ const now = new Date().toISOString();
1303
+ for (const taskArgs of newTasksArgs) {
1304
+ try {
1305
+ const options = parseTaskArgs(taskArgs);
1306
+ if (options.summary) {
1307
+ const newTaskId = getNextId(tasks);
1308
+ const newTask = buildTask(newTaskId, options);
1309
+ tasks.push(newTask);
1310
+ createdTaskIds.push(newTaskId);
1311
+ console.log(`Created task ${newTaskId} from review`);
1312
+ }
1313
+ } catch (error) {
1314
+ console.error(`Error parsing task args: ${error instanceof Error ? error.message : String(error)}`);
1315
+ return;
1316
+ }
1317
+ }
1318
+ if (createdTaskIds.length > 0) {
1319
+ saveTasks(tasks);
1320
+ review.related_task_ids = [...review.related_task_ids || [], ...createdTaskIds];
1321
+ }
1322
+ review.status = "done";
1323
+ review.updated_at = now;
1324
+ saveReviews(reviews);
1325
+ console.log(`Review ${id} accepted and marked as done.`);
1326
+ }
1327
+ function handleReject(args) {
1328
+ const id = args[0];
1329
+ if (!id) {
1330
+ console.error("Error: Review ID is required");
1331
+ process.exit(1);
1332
+ }
1333
+ const reviews = loadReviews();
1334
+ const review = getReviewById(reviews, id);
1335
+ if (!review) {
1336
+ console.error(`Error: Review ${id} not found`);
1337
+ process.exit(1);
1338
+ }
1339
+ review.status = "closed";
1340
+ review.updated_at = new Date().toISOString();
1341
+ saveReviews(reviews);
1342
+ console.log(`Review ${id} rejected and marked as closed.`);
1343
+ }
1344
+
1345
+ // src/commands/release.ts
1346
+ function releaseCommand(args) {
1347
+ if (args.includes("--help") || args.includes("-h")) {
1348
+ console.log(`
1349
+ Usage: tm release <id...> --version <v>
1350
+
1351
+ Options:
1352
+ --version <v> Set version for task(s)
1353
+ `);
1354
+ return;
1355
+ }
1356
+ let version = null;
1357
+ const ids = [];
1358
+ for (let i = 0;i < args.length; i++) {
1359
+ const arg = args[i];
1360
+ if (arg === "--version") {
1361
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
1362
+ version = args[++i];
1363
+ } else {
1364
+ console.error("Error: --version requires a value.");
1365
+ return;
1366
+ }
1367
+ } else if (arg.startsWith("--")) {
1368
+ console.error(`Error: Unknown option '${arg}'.`);
1369
+ return;
1370
+ } else {
1371
+ ids.push(arg);
1372
+ }
1373
+ }
1374
+ if (!version) {
1375
+ console.error("Error: --version <v> is required.");
1376
+ return;
1377
+ }
1378
+ if (ids.length === 0) {
1379
+ console.error("Error: No task IDs specified.");
1380
+ return;
1381
+ }
1382
+ const tasks = loadTasks();
1383
+ let updated = false;
1384
+ for (const id of ids) {
1385
+ const task = getTaskById(tasks, id);
1386
+ if (task) {
1387
+ task.version = version;
1388
+ task.updated_at = new Date().toISOString();
1389
+ updated = true;
1390
+ console.log(`Task ${id} version set to ${version}`);
1391
+ } else {
1392
+ console.error(`Error: Task ${id} not found.`);
1393
+ }
1394
+ }
1395
+ if (updated) {
1396
+ saveTasks(tasks);
1397
+ }
1398
+ }
1399
+
1400
+ // src/commands/close.ts
1401
+ function closeCommand(args) {
1402
+ if (args.includes("--help") || args.includes("-h")) {
1403
+ console.log(`
1404
+ Usage: tm close <id...> [options]
1405
+
1406
+ Options:
1407
+ --body <body> Add a closing comment
1408
+ `);
1409
+ return;
1410
+ }
1411
+ const ids = [];
1412
+ let bodyText = null;
1413
+ for (let i = 0;i < args.length; i++) {
1414
+ const arg = args[i];
1415
+ if (arg === "--body") {
1416
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
1417
+ bodyText = args[++i];
1418
+ } else {
1419
+ console.error("Error: --body requires a text argument.");
1420
+ return;
1421
+ }
1422
+ } else if (arg.startsWith("--")) {
1423
+ console.error(`Error: Unknown option '${arg}'.`);
1424
+ return;
1425
+ } else {
1426
+ ids.push(arg);
1427
+ }
1428
+ }
1429
+ if (ids.length === 0) {
1430
+ console.error("Error: No task IDs provided.");
1431
+ return;
1432
+ }
1433
+ const tasks = loadTasks();
1434
+ let updatedCount = 0;
1435
+ ids.forEach((id) => {
1436
+ const task = getTaskById(tasks, id);
1437
+ if (task) {
1438
+ task.status = "closed";
1439
+ task.updated_at = new Date().toISOString();
1440
+ if (task.version === "tbd") {
1441
+ task.version = "";
1442
+ }
1443
+ if (bodyText) {
1444
+ const newBody = {
1445
+ text: bodyText,
1446
+ created_at: new Date().toISOString()
1447
+ };
1448
+ task.bodies.push(newBody);
1449
+ }
1450
+ updatedCount++;
1451
+ console.log(`Task ${task.id} closed.`);
1452
+ } else {
1453
+ console.warn(`Task ${id} not found.`);
1454
+ }
1455
+ });
1456
+ if (updatedCount > 0) {
1457
+ saveTasks(tasks);
1458
+ }
1459
+ }
1460
+
1461
+ // src/syncStore.ts
1462
+ import { join as join3 } from "path";
1463
+ import { homedir as homedir3 } from "os";
1464
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync3, readdirSync } from "fs";
1465
+ import { spawnSync } from "child_process";
1466
+ var SYNC_DIR = join3(homedir3(), ".local", "task-memory");
1467
+ var PROJECTS_DIR = join3(SYNC_DIR, "projects");
1468
+ var CONFIG_FILE = join3(SYNC_DIR, "config.json");
1469
+ function getSyncDir() {
1470
+ return SYNC_DIR;
1471
+ }
1472
+ function isSyncInitialized() {
1473
+ return existsSync3(SYNC_DIR) && existsSync3(join3(SYNC_DIR, ".git"));
1474
+ }
1475
+ function initSyncRepo() {
1476
+ if (!existsSync3(SYNC_DIR)) {
1477
+ mkdirSync(SYNC_DIR, { recursive: true });
1478
+ }
1479
+ if (!existsSync3(PROJECTS_DIR)) {
1480
+ mkdirSync(PROJECTS_DIR, { recursive: true });
1481
+ }
1482
+ if (!existsSync3(join3(SYNC_DIR, ".git"))) {
1483
+ const result = spawnSync("git", ["init"], { cwd: SYNC_DIR, stdio: "inherit" });
1484
+ if (result.status !== 0) {
1485
+ console.error("Failed to initialize git repository");
1486
+ return false;
1487
+ }
1488
+ }
1489
+ if (!existsSync3(CONFIG_FILE)) {
1490
+ const config = { defaultAuto: false };
1491
+ writeFileSync3(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
1492
+ }
1493
+ const gitignorePath = join3(SYNC_DIR, ".gitignore");
1494
+ if (!existsSync3(gitignorePath)) {
1495
+ writeFileSync3(gitignorePath, `# Add patterns to ignore
1496
+ `, "utf-8");
1497
+ }
1498
+ return true;
1499
+ }
1500
+ function loadGlobalConfig() {
1501
+ if (!existsSync3(CONFIG_FILE)) {
1502
+ return { defaultAuto: false };
1503
+ }
1504
+ try {
1505
+ const data = readFileSync3(CONFIG_FILE, "utf-8");
1506
+ return JSON.parse(data);
1507
+ } catch (e) {
1508
+ return { defaultAuto: false };
1509
+ }
1510
+ }
1511
+ function saveGlobalConfig(config) {
1512
+ writeFileSync3(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
1513
+ }
1514
+ function getProjectFilePath(syncId) {
1515
+ return join3(PROJECTS_DIR, `${syncId}.json`);
1516
+ }
1517
+ function getEncryptedProjectFilePath(syncId) {
1518
+ return join3(PROJECTS_DIR, `${syncId}.json.age`);
1519
+ }
1520
+ function getPublicKeyFromIdentityFile(keyFile) {
1521
+ try {
1522
+ const content = readFileSync3(keyFile, "utf-8");
1523
+ const match = content.match(/^# public key: (.+)$/m);
1524
+ return match ? match[1].trim() : null;
1525
+ } catch {
1526
+ return null;
1527
+ }
1528
+ }
1529
+ function encryptData(data, recipient) {
1530
+ const result = spawnSync("age", ["-e", "-r", recipient, "-a"], {
1531
+ input: data,
1532
+ encoding: "utf-8"
1533
+ });
1534
+ if (result.error) {
1535
+ console.error(`Encryption failed: ${result.error.message}. Is 'age' command installed and in your PATH?`);
1536
+ return null;
1537
+ }
1538
+ if (result.status !== 0) {
1539
+ console.error(`Encryption failed: ${result.stderr}`);
1540
+ return null;
1541
+ }
1542
+ return result.stdout;
1543
+ }
1544
+ function decryptData(ciphertext, keyFile) {
1545
+ const result = spawnSync("age", ["-d", "-i", keyFile], {
1546
+ input: ciphertext,
1547
+ encoding: "utf-8"
1548
+ });
1549
+ if (result.error) {
1550
+ console.error(`Decryption failed: ${result.error.message}. Is 'age' command installed and in your PATH?`);
1551
+ return null;
1552
+ }
1553
+ if (result.status !== 0) {
1554
+ console.error(`Decryption failed: ${result.stderr}`);
1555
+ return null;
1556
+ }
1557
+ return result.stdout;
1558
+ }
1559
+ function generateAgeKey(outputPath) {
1560
+ const result = spawnSync("age-keygen", ["-o", outputPath], {
1561
+ encoding: "utf-8"
1562
+ });
1563
+ if (result.error) {
1564
+ console.error(`Key generation failed: ${result.error.message}. Is 'age-keygen' command installed and in your PATH?`);
1565
+ return false;
1566
+ }
1567
+ return result.status === 0;
1568
+ }
1569
+ function pushToSync(syncId, store, encryptRecipient) {
1570
+ if (!isSyncInitialized()) {
1571
+ console.error('Sync repository not initialized. Run "tm sync add" first.');
1572
+ return false;
1573
+ }
1574
+ const projectFile = encryptRecipient ? getEncryptedProjectFilePath(syncId) : getProjectFilePath(syncId);
1575
+ try {
1576
+ const jsonData = JSON.stringify(store, null, 2);
1577
+ if (encryptRecipient) {
1578
+ const encrypted = encryptData(jsonData, encryptRecipient);
1579
+ if (!encrypted)
1580
+ return false;
1581
+ writeFileSync3(projectFile, encrypted, "utf-8");
1582
+ } else {
1583
+ writeFileSync3(projectFile, jsonData, "utf-8");
1584
+ }
1585
+ return true;
1586
+ } catch (e) {
1587
+ console.error(`Failed to push to sync: ${e}`);
1588
+ return false;
1589
+ }
1590
+ }
1591
+ function tryAutoSync(syncConfig, store, encryptRecipient) {
1592
+ if (!syncConfig?.enabled || !syncConfig.auto) {
1593
+ return;
1594
+ }
1595
+ if (!isSyncInitialized()) {
1596
+ return;
1597
+ }
1598
+ pushToSync(syncConfig.id, store, encryptRecipient);
1599
+ }
1600
+ function pullFromSync(syncId, encryptIdentityFile) {
1601
+ if (!isSyncInitialized()) {
1602
+ console.error('Sync repository not initialized. Run "tm sync add" first.');
1603
+ return null;
1604
+ }
1605
+ const ageFile = getEncryptedProjectFilePath(syncId);
1606
+ const jsonFile = getProjectFilePath(syncId);
1607
+ try {
1608
+ if (encryptIdentityFile && existsSync3(ageFile)) {
1609
+ const ciphertext = readFileSync3(ageFile, "utf-8");
1610
+ const decrypted = decryptData(ciphertext, encryptIdentityFile);
1611
+ if (!decrypted)
1612
+ return null;
1613
+ return JSON.parse(decrypted);
1614
+ }
1615
+ if (existsSync3(jsonFile)) {
1616
+ const data = readFileSync3(jsonFile, "utf-8");
1617
+ return JSON.parse(data);
1618
+ }
1619
+ console.error(`Project "${syncId}" not found in sync repository.`);
1620
+ return null;
1621
+ } catch (e) {
1622
+ console.error(`Failed to pull from sync: ${e}`);
1623
+ return null;
1624
+ }
1625
+ }
1626
+ function listSyncedProjects() {
1627
+ if (!existsSync3(PROJECTS_DIR)) {
1628
+ return [];
1629
+ }
1630
+ const files = readdirSync(PROJECTS_DIR);
1631
+ return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
1632
+ }
1633
+ function runGitCommand(args) {
1634
+ if (!isSyncInitialized()) {
1635
+ console.error('Sync repository not initialized. Run "tm sync add" first.');
1636
+ return 1;
1637
+ }
1638
+ const result = spawnSync("git", args, { cwd: SYNC_DIR, stdio: "inherit" });
1639
+ return result.status ?? 1;
1640
+ }
1641
+ function normalizeRemoteUrl(url) {
1642
+ let s = url.trim();
1643
+ s = s.replace(/^https?:\/\//, "");
1644
+ s = s.replace(/^git:\/\//, "");
1645
+ s = s.replace(/^[^@]+@/, "");
1646
+ s = s.replace(":", "/");
1647
+ s = s.replace(/\.git$/, "");
1648
+ s = s.replace(/\//g, "-");
1649
+ s = s.replace(/[^a-zA-Z0-9._-]/g, "-");
1650
+ s = s.replace(/^-+|-+$/g, "");
1651
+ return s || "unknown";
1652
+ }
1653
+ function generateSyncId() {
1654
+ const originResult = spawnSync("git", ["remote", "get-url", "origin"], {
1655
+ cwd: process.cwd(),
1656
+ encoding: "utf-8"
1657
+ });
1658
+ if (originResult.status === 0 && originResult.stdout) {
1659
+ return normalizeRemoteUrl(originResult.stdout.trim());
1660
+ }
1661
+ const toplevelResult = spawnSync("git", ["rev-parse", "--show-toplevel"], {
1662
+ cwd: process.cwd(),
1663
+ encoding: "utf-8"
1664
+ });
1665
+ if (toplevelResult.status === 0 && toplevelResult.stdout) {
1666
+ return toplevelResult.stdout.trim().split("/").pop() || "unknown";
1667
+ }
1668
+ return process.cwd().split("/").pop() || "unknown";
1669
+ }
1670
+
1671
+ // src/commands/sync.ts
1672
+ import { existsSync as existsSync4 } from "fs";
1673
+ import { join as join4 } from "path";
1674
+ import { spawnSync as spawnSync2 } from "child_process";
1675
+ function mergeTasks(localTasks, remoteTasks) {
1676
+ const merged = [...localTasks];
1677
+ let idCollisions = 0;
1678
+ let conflictsResolved = 0;
1679
+ for (const remoteTask of remoteTasks) {
1680
+ const sameTaskIndex = merged.findIndex((t) => t.id === remoteTask.id && t.created_at === remoteTask.created_at);
1681
+ if (sameTaskIndex >= 0) {
1682
+ const existing = merged[sameTaskIndex];
1683
+ if (existing && new Date(remoteTask.updated_at) > new Date(existing.updated_at)) {
1684
+ merged[sameTaskIndex] = remoteTask;
1685
+ conflictsResolved++;
1686
+ }
1687
+ } else {
1688
+ const hasIdCollision = merged.some((t) => t.id === remoteTask.id);
1689
+ if (hasIdCollision) {
1690
+ const newId = getNextId(merged);
1691
+ merged.push({ ...remoteTask, id: newId });
1692
+ idCollisions++;
1693
+ } else {
1694
+ merged.push(remoteTask);
1695
+ }
1696
+ }
1697
+ }
1698
+ return { tasks: merged, idCollisions, conflictsResolved };
1699
+ }
1700
+ function parseArgs(args) {
1701
+ const subcommand = args[0] || "";
1702
+ const options = {};
1703
+ const positional = [];
1704
+ let i = 1;
1705
+ while (i < args.length) {
1706
+ const arg = args[i];
1707
+ if (arg?.startsWith("--")) {
1708
+ const key = arg.slice(2);
1709
+ const nextArg = args[i + 1];
1710
+ if (nextArg && !nextArg.startsWith("--")) {
1711
+ options[key] = nextArg;
1712
+ i += 2;
1713
+ } else {
1714
+ options[key] = true;
1715
+ i++;
1716
+ }
1717
+ } else {
1718
+ positional.push(arg);
1719
+ i++;
1720
+ }
1721
+ }
1722
+ return { subcommand, options, positional };
1723
+ }
1724
+ function ensureInitialized() {
1725
+ if (isSyncInitialized()) {
1726
+ return true;
1727
+ }
1728
+ return initSyncRepo();
1729
+ }
1730
+ function handleAdd(options) {
1731
+ if (!ensureInitialized()) {
1732
+ console.error("Failed to initialize sync repository");
1733
+ process.exit(1);
1734
+ }
1735
+ const existingConfig = loadSyncConfig();
1736
+ if (existingConfig?.enabled) {
1737
+ console.log(`Already added to sync with id: ${existingConfig.id}`);
1738
+ return;
1739
+ }
1740
+ const syncId = (typeof options.id === "string" ? options.id : null) || generateSyncId();
1741
+ const syncConfig = {
1742
+ id: syncId,
1743
+ enabled: true,
1744
+ auto: false
1745
+ };
1746
+ saveSyncConfig(syncConfig);
1747
+ console.log(`Added to sync with id: ${syncId}`);
1748
+ if (options.save) {
1749
+ const globalConfig = loadGlobalConfig();
1750
+ const { recipient } = resolveEncryptSettings(syncConfig, globalConfig);
1751
+ const store = loadStore();
1752
+ if (pushToSync(syncId, store, recipient)) {
1753
+ console.log("Saved to sync directory.");
1754
+ }
1755
+ }
1756
+ }
1757
+ function handleRemove() {
1758
+ const existingConfig = loadSyncConfig();
1759
+ if (!existingConfig?.enabled) {
1760
+ console.log("Not currently synced.");
1761
+ return;
1762
+ }
1763
+ const syncConfig = {
1764
+ ...existingConfig,
1765
+ enabled: false
1766
+ };
1767
+ saveSyncConfig(syncConfig);
1768
+ console.log(`Removed from sync. (id was: ${existingConfig.id})`);
1769
+ }
1770
+ function resolveEncryptSettings(syncConfig, globalConfig) {
1771
+ const enabled = syncConfig.encryptEnabled ?? globalConfig.defaultEncryptEnabled ?? false;
1772
+ if (!enabled)
1773
+ return { enabled: false, recipient: undefined, identityFile: undefined };
1774
+ return {
1775
+ enabled: true,
1776
+ recipient: syncConfig.encryptRecipient ?? globalConfig.defaultEncryptRecipient,
1777
+ identityFile: syncConfig.encryptIdentityFile ?? globalConfig.defaultEncryptIdentityFile
1778
+ };
1779
+ }
1780
+ function handleSave() {
1781
+ const syncConfig = loadSyncConfig();
1782
+ if (!syncConfig?.enabled) {
1783
+ console.error('Not synced. Run "tm sync add" first.');
1784
+ process.exit(1);
1785
+ }
1786
+ const globalConfig = loadGlobalConfig();
1787
+ const { recipient } = resolveEncryptSettings(syncConfig, globalConfig);
1788
+ const store = loadStore();
1789
+ if (pushToSync(syncConfig.id, store, recipient)) {
1790
+ console.log(`Saved to sync directory. (id: ${syncConfig.id})`);
1791
+ } else {
1792
+ process.exit(1);
1793
+ }
1794
+ }
1795
+ function handleLoad(options) {
1796
+ const syncConfig = loadSyncConfig();
1797
+ if (!syncConfig?.enabled) {
1798
+ console.error('Not synced. Run "tm sync add" first.');
1799
+ process.exit(1);
1800
+ }
1801
+ const globalConfig = loadGlobalConfig();
1802
+ const { identityFile } = resolveEncryptSettings(syncConfig, globalConfig);
1803
+ const remoteStore = pullFromSync(syncConfig.id, identityFile);
1804
+ if (!remoteStore) {
1805
+ process.exit(1);
1806
+ }
1807
+ const currentStore = loadStore();
1808
+ const merge = options.merge === true;
1809
+ if (merge) {
1810
+ const result = mergeTasks(currentStore.tasks, remoteStore.tasks);
1811
+ currentStore.tasks = result.tasks;
1812
+ saveStore(currentStore);
1813
+ let msg = `Merged from sync directory. (${remoteStore.tasks.length} tasks)`;
1814
+ if (result.idCollisions > 0)
1815
+ msg += ` ID collisions resolved: ${result.idCollisions}.`;
1816
+ if (result.conflictsResolved > 0)
1817
+ msg += ` Conflicts resolved: ${result.conflictsResolved}.`;
1818
+ console.log(msg);
1819
+ } else {
1820
+ currentStore.tasks = remoteStore.tasks;
1821
+ saveStore(currentStore);
1822
+ console.log(`Loaded from sync directory. (${remoteStore.tasks.length} tasks)`);
1823
+ }
1824
+ }
1825
+ function handleSetKey(positional, options) {
1826
+ const isGlobal = options.global === true;
1827
+ const isGenerate = options.generate === true;
1828
+ const pathArg = positional[1] ?? null;
1829
+ if (!isGenerate && !pathArg) {
1830
+ console.error("Usage: tm sync set key <path> [--global]");
1831
+ console.error(" tm sync set key --generate [--global]");
1832
+ process.exit(1);
1833
+ }
1834
+ let identityFile;
1835
+ if (isGenerate) {
1836
+ identityFile = join4(getSyncDir(), "age.key");
1837
+ if (!existsSync4(identityFile)) {
1838
+ if (!generateAgeKey(identityFile)) {
1839
+ console.error("Failed to generate age key.");
1840
+ process.exit(1);
1841
+ }
1842
+ console.log(`Generated key file: ${identityFile}`);
1843
+ } else {
1844
+ console.log(`Using existing key file: ${identityFile}`);
1845
+ }
1846
+ } else {
1847
+ identityFile = pathArg;
1848
+ if (!existsSync4(identityFile)) {
1849
+ console.error(`Key file not found: ${identityFile}`);
1850
+ process.exit(1);
1851
+ }
1852
+ }
1853
+ const pubkey = getPublicKeyFromIdentityFile(identityFile);
1854
+ if (!pubkey) {
1855
+ console.error(`Could not extract public key from: ${identityFile}`);
1856
+ process.exit(1);
1857
+ }
1858
+ if (isGlobal) {
1859
+ const globalConfig = loadGlobalConfig();
1860
+ globalConfig.defaultEncryptIdentityFile = identityFile;
1861
+ globalConfig.defaultEncryptRecipient = pubkey;
1862
+ saveGlobalConfig(globalConfig);
1863
+ console.log(`Global key file set: ${identityFile}`);
1864
+ console.log(`Global recipient set to: ${pubkey}`);
1865
+ } else {
1866
+ const syncConfig = loadSyncConfig();
1867
+ if (!syncConfig?.enabled) {
1868
+ console.error('Not synced. Run "tm sync add" first.');
1869
+ process.exit(1);
1870
+ }
1871
+ syncConfig.encryptIdentityFile = identityFile;
1872
+ syncConfig.encryptRecipient = pubkey;
1873
+ saveSyncConfig(syncConfig);
1874
+ console.log(`Key file set: ${identityFile}`);
1875
+ console.log(`Recipient set to: ${pubkey}`);
1876
+ }
1877
+ }
1878
+ function handleSetEncrypt(positional, options) {
1879
+ const isGlobal = options.global === true;
1880
+ const value = positional[1];
1881
+ if (value !== "on" && value !== "off") {
1882
+ console.error("Usage: tm sync set encrypt <on|off> [--global]");
1883
+ process.exit(1);
1884
+ }
1885
+ const enabled = value === "on";
1886
+ if (isGlobal) {
1887
+ const globalConfig = loadGlobalConfig();
1888
+ globalConfig.defaultEncryptEnabled = enabled;
1889
+ saveGlobalConfig(globalConfig);
1890
+ console.log(`Global encryption ${enabled ? "enabled" : "disabled"}.`);
1891
+ } else {
1892
+ const syncConfig = loadSyncConfig();
1893
+ if (!syncConfig?.enabled) {
1894
+ console.error('Not synced. Run "tm sync add" first.');
1895
+ process.exit(1);
1896
+ }
1897
+ syncConfig.encryptEnabled = enabled;
1898
+ saveSyncConfig(syncConfig);
1899
+ console.log(`Encryption ${enabled ? "enabled" : "disabled"}.`);
1900
+ }
1901
+ }
1902
+ function handleSet(positional, options) {
1903
+ const mode = positional[0];
1904
+ if (mode === "key") {
1905
+ handleSetKey(positional, options);
1906
+ return;
1907
+ }
1908
+ if (mode === "encrypt") {
1909
+ handleSetEncrypt(positional, options);
1910
+ return;
1911
+ }
1912
+ if (mode !== "auto" && mode !== "manual") {
1913
+ console.error("Usage: tm sync set <auto|manual|encrypt|key>");
1914
+ process.exit(1);
1915
+ }
1916
+ const syncConfig = loadSyncConfig();
1917
+ if (!syncConfig?.enabled) {
1918
+ console.error('Not synced. Run "tm sync add" first.');
1919
+ process.exit(1);
1920
+ }
1921
+ syncConfig.auto = mode === "auto";
1922
+ saveSyncConfig(syncConfig);
1923
+ console.log(`Sync mode set to: ${mode}`);
1924
+ }
1925
+ function handleStatus() {
1926
+ const syncConfig = loadSyncConfig();
1927
+ console.log("=== Sync Status ===");
1928
+ console.log(`Sync Directory: ${getSyncDir()}`);
1929
+ console.log(`Initialized: ${isSyncInitialized() ? "Yes" : "No"}`);
1930
+ console.log("");
1931
+ const globalConfig = loadGlobalConfig();
1932
+ if (globalConfig.defaultEncryptEnabled !== undefined || globalConfig.defaultEncryptIdentityFile || globalConfig.defaultEncryptRecipient) {
1933
+ console.log("=== Global Defaults ===");
1934
+ if (globalConfig.defaultEncryptEnabled !== undefined) {
1935
+ console.log(`Encrypt: ${globalConfig.defaultEncryptEnabled ? "on" : "off"}`);
1936
+ }
1937
+ if (globalConfig.defaultEncryptIdentityFile) {
1938
+ console.log(`Key: ${globalConfig.defaultEncryptIdentityFile}`);
1939
+ }
1940
+ if (globalConfig.defaultEncryptRecipient) {
1941
+ console.log(`Recipient: ${globalConfig.defaultEncryptRecipient}`);
1942
+ }
1943
+ console.log("");
1944
+ }
1945
+ if (syncConfig) {
1946
+ console.log("=== Current Project ===");
1947
+ console.log(`ID: ${syncConfig.id}`);
1948
+ console.log(`Enabled: ${syncConfig.enabled ? "Yes" : "No"}`);
1949
+ console.log(`Auto: ${syncConfig.auto ? "Yes" : "No"}`);
1950
+ const { enabled: encryptEnabled, identityFile: identity, recipient } = resolveEncryptSettings(syncConfig, globalConfig);
1951
+ console.log(`Encrypt: ${encryptEnabled ? "on" : "off"}${syncConfig.encryptEnabled === undefined ? " (global)" : ""}`);
1952
+ if (encryptEnabled) {
1953
+ if (identity)
1954
+ console.log(`Key: ${identity}${syncConfig.encryptIdentityFile === undefined ? " (global)" : ""}`);
1955
+ if (recipient)
1956
+ console.log(`Recipient: ${recipient}${syncConfig.encryptRecipient === undefined ? " (global)" : ""}`);
1957
+ }
1958
+ } else {
1959
+ console.log("Current project is not synced.");
1960
+ }
1961
+ console.log("");
1962
+ const projects = listSyncedProjects();
1963
+ if (projects.length > 0) {
1964
+ console.log("=== Synced Projects ===");
1965
+ for (const p of projects) {
1966
+ console.log(` - ${p}`);
1967
+ }
1968
+ }
1969
+ }
1970
+ function checkGhAuth() {
1971
+ const ghCheck = spawnSync2("gh", ["auth", "status"], { encoding: "utf-8" });
1972
+ if (ghCheck.error || ghCheck.status !== 0) {
1973
+ console.error('gh command is not available or not authenticated. Run "gh auth login" first.');
1974
+ process.exit(1);
1975
+ }
1976
+ }
1977
+ function handleUpload(options) {
1978
+ const syncConfig = loadSyncConfig();
1979
+ if (!syncConfig?.enabled) {
1980
+ console.error('Not synced. Run "tm sync add" first.');
1981
+ process.exit(1);
1982
+ }
1983
+ const globalConfig = loadGlobalConfig();
1984
+ const { recipient } = resolveEncryptSettings(syncConfig, globalConfig);
1985
+ const store = loadStore();
1986
+ if (!pushToSync(syncConfig.id, store, recipient)) {
1987
+ process.exit(1);
1988
+ }
1989
+ console.log(`Saved. (id: ${syncConfig.id})`);
1990
+ runGitCommand(["add", "."]);
1991
+ const now = new Date;
1992
+ const pad = (n) => String(n).padStart(2, "0");
1993
+ const defaultMessage = `sync: ${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
1994
+ const message = typeof options.message === "string" ? options.message : defaultMessage;
1995
+ const commitResult = spawnSync2("git", ["commit", "-m", message], {
1996
+ cwd: getSyncDir(),
1997
+ encoding: "utf-8",
1998
+ stdio: "pipe"
1999
+ });
2000
+ if (commitResult.status !== 0) {
2001
+ if (commitResult.stdout.includes("nothing to commit")) {
2002
+ console.log("Nothing to commit.");
2003
+ } else {
2004
+ console.error("Failed to commit.");
2005
+ console.error(commitResult.stderr);
2006
+ process.exit(1);
2007
+ }
2008
+ } else {
2009
+ console.log(`Committed: ${message}`);
2010
+ }
2011
+ const pushResult = spawnSync2("git", ["push", "--set-upstream", "origin", "HEAD"], {
2012
+ cwd: getSyncDir(),
2013
+ stdio: "inherit"
2014
+ });
2015
+ if (pushResult.status !== 0) {
2016
+ console.error("Failed to push.");
2017
+ process.exit(1);
2018
+ }
2019
+ }
2020
+ function handleRepoCreate(options) {
2021
+ checkGhAuth();
2022
+ if (!ensureInitialized()) {
2023
+ console.error("Failed to initialize sync repository");
2024
+ process.exit(1);
2025
+ }
2026
+ const isPublic = options.public === true;
2027
+ const repoName = typeof options.name === "string" ? options.name : "sync-task-memory";
2028
+ const visibility = isPublic ? "--public" : "--private";
2029
+ console.log(`Creating ${isPublic ? "public" : "private"} repository: ${repoName}`);
2030
+ const createResult = spawnSync2("gh", ["repo", "create", repoName, visibility], {
2031
+ encoding: "utf-8",
2032
+ stdio: "pipe"
2033
+ });
2034
+ if (createResult.status !== 0) {
2035
+ console.error("Failed to create repository.");
2036
+ console.error(createResult.stderr);
2037
+ process.exit(1);
2038
+ }
2039
+ const repoUrl = createResult.stdout.trim();
2040
+ console.log(`Repository created: ${repoUrl}`);
2041
+ spawnSync2("git", ["remote", "remove", "origin"], { cwd: getSyncDir(), encoding: "utf-8" });
2042
+ spawnSync2("git", ["remote", "add", "origin", repoUrl], { cwd: getSyncDir(), stdio: "inherit" });
2043
+ console.log(`Remote set. Use "tm git push" to push your data.`);
2044
+ }
2045
+ function handleRepoSet(positional) {
2046
+ const url = positional[1];
2047
+ if (!url) {
2048
+ console.error("Usage: tm sync repo set <url>");
2049
+ process.exit(1);
2050
+ }
2051
+ if (!ensureInitialized()) {
2052
+ console.error("Failed to initialize sync repository");
2053
+ process.exit(1);
2054
+ }
2055
+ spawnSync2("git", ["remote", "remove", "origin"], { cwd: getSyncDir(), encoding: "utf-8" });
2056
+ const addResult = spawnSync2("git", ["remote", "add", "origin", url], {
2057
+ cwd: getSyncDir(),
2058
+ stdio: "inherit"
2059
+ });
2060
+ if (addResult.status !== 0) {
2061
+ console.error("Failed to set remote.");
2062
+ process.exit(1);
2063
+ }
2064
+ console.log(`Remote set: ${url}`);
2065
+ }
2066
+ function handleRepoShow() {
2067
+ const result = spawnSync2("git", ["remote", "-v"], {
2068
+ cwd: getSyncDir(),
2069
+ encoding: "utf-8"
2070
+ });
2071
+ if (result.status !== 0 || !result.stdout.trim()) {
2072
+ console.log('No remote configured. Use "tm sync repo create" or "tm sync repo set <url>".');
2073
+ return;
2074
+ }
2075
+ console.log(result.stdout.trim());
2076
+ }
2077
+ function handleRepo(positional, options) {
2078
+ const subcommand = positional[0];
2079
+ switch (subcommand) {
2080
+ case "create":
2081
+ handleRepoCreate(options);
2082
+ break;
2083
+ case "set":
2084
+ handleRepoSet(positional);
2085
+ break;
2086
+ case "show":
2087
+ handleRepoShow();
2088
+ break;
2089
+ default:
2090
+ console.error("Usage: tm sync repo <create|set|show>");
2091
+ process.exit(1);
2092
+ }
2093
+ }
2094
+ function handleList2() {
2095
+ const projects = listSyncedProjects();
2096
+ if (projects.length === 0) {
2097
+ console.log("No projects synced.");
2098
+ return;
2099
+ }
2100
+ console.log("Synced projects:");
2101
+ for (const p of projects) {
2102
+ console.log(` - ${p}`);
2103
+ }
2104
+ }
2105
+ function showHelp() {
2106
+ console.log(`
2107
+ Usage: tm sync <subcommand> [options]
2108
+
2109
+ Subcommands:
2110
+ add [--id <name>] [--save]
2111
+ Add current project to sync
2112
+ remove Remove current project from sync
2113
+ save Save tasks to sync directory
2114
+ upload [--message <msg>]
2115
+ Save, git commit, and git push
2116
+ load [--merge] Load tasks from sync directory
2117
+ set <auto|manual> Set sync mode
2118
+ set encrypt <on|off> Enable/disable encryption for current project
2119
+ set encrypt <on|off> --global
2120
+ Enable/disable encryption globally (used as fallback)
2121
+ set key <path> Set key file (private key); recipient auto-extracted
2122
+ set key --generate Generate a new key file; recipient auto-extracted
2123
+ set key [--generate] --global
2124
+ Apply setting globally (used as fallback)
2125
+ repo create [--name <name>] [--public]
2126
+ Create GitHub repository and set as remote (requires gh)
2127
+ Default name: sync-task-memory
2128
+ repo set <url> Set existing repository as remote
2129
+ repo show Show current remote
2130
+ status Show sync status
2131
+ list List synced projects
2132
+
2133
+ Examples:
2134
+ tm sync add --id my-project --save
2135
+ tm sync set key --generate --global # age鍵を生成しグローバルに設定
2136
+ tm sync set key ~/.age.key --global # 既存の鍵をグローバルに設定
2137
+ tm sync set encrypt on --global # 暗号化をグローバルでデフォルト有効化
2138
+ tm sync set encrypt off # 現プロジェクトだけ暗号化を無効化
2139
+ tm sync repo create # privateリポジトリを作成
2140
+ tm sync repo create --public # publicリポジトリを作成
2141
+ tm sync repo create --name my-tasks # リポジトリ名を指定
2142
+ tm sync save
2143
+ tm sync load --merge
2144
+ tm sync set auto
2145
+ `);
2146
+ }
2147
+ function syncCommand(args) {
2148
+ const { subcommand, options, positional } = parseArgs(args);
2149
+ switch (subcommand) {
2150
+ case "add":
2151
+ handleAdd(options);
2152
+ break;
2153
+ case "remove":
2154
+ case "rm":
2155
+ handleRemove();
2156
+ break;
2157
+ case "save":
2158
+ handleSave();
2159
+ break;
2160
+ case "upload":
2161
+ handleUpload(options);
2162
+ break;
2163
+ case "load":
2164
+ handleLoad(options);
2165
+ break;
2166
+ case "set":
2167
+ handleSet(positional, options);
2168
+ break;
2169
+ case "repo":
2170
+ handleRepo(positional, options);
2171
+ break;
2172
+ case "status":
2173
+ handleStatus();
2174
+ break;
2175
+ case "list":
2176
+ case "ls":
2177
+ handleList2();
2178
+ break;
2179
+ case "help":
2180
+ case "--help":
2181
+ case "-h":
2182
+ showHelp();
2183
+ break;
2184
+ default:
2185
+ if (!subcommand) {
2186
+ showHelp();
2187
+ } else {
2188
+ console.error(`Unknown subcommand: ${subcommand}`);
2189
+ showHelp();
2190
+ process.exit(1);
2191
+ }
2192
+ }
2193
+ }
2194
+
2195
+ // src/commands/git.ts
2196
+ function gitCommand(args) {
2197
+ if (!isSyncInitialized()) {
2198
+ console.error('Sync repository not initialized. Run "tm sync add" first.');
2199
+ process.exit(1);
2200
+ }
2201
+ if (args.length === 0) {
2202
+ console.log(`Git repository at: ${getSyncDir()}`);
2203
+ console.log("Usage: tm git <git-command> [args]");
2204
+ console.log("");
2205
+ console.log("Examples:");
2206
+ console.log(" tm git status");
2207
+ console.log(" tm git remote add origin git@github.com:user/task-memory.git");
2208
+ console.log(" tm git push");
2209
+ console.log(" tm git pull");
2210
+ return;
2211
+ }
2212
+ const exitCode = runGitCommand(args);
2213
+ process.exit(exitCode);
2214
+ }
2215
+
2216
+ // docs/usage/index.md
2217
+ var usage_default = `# ユーザーガイド
2218
+
2219
+ \`task-memory\` (tm) は、AIエージェントや開発者がタスクの状態を管理し、コンテキストの喪失を防ぐためのツールです。
2220
+
2221
+ このドキュメントは \`tm docs\` コマンドでいつでも参照できます。
2222
+
2223
+ ## インストール方法
2224
+
2225
+ ### ローカルインストール
2226
+
2227
+ このリポジトリをクローンし、ローカルコマンドとしてリンクします。
2228
+
2229
+ \`\`\`bash
2230
+ git clone <repository-url> task-memory
2231
+ cd task-memory
2232
+ bun install
2233
+ bun link
2234
+ \`\`\`
2235
+
2236
+ これで \`tm\` コマンドがグローバルに使用可能になります(Bunのbinパスが通っている必要があります)。
2237
+
2238
+ または、エイリアスを設定して使用することも可能です。
2239
+
2240
+ \`\`\`bash
2241
+ alias tm="bun run /path/to/task-memory/src/index.ts"
2242
+ \`\`\`
2243
+
2244
+ ## 基本的な使い方
2245
+
2246
+ ### 1. タスクの作成 (\`tm new\`)
2247
+
2248
+ 新しいタスクを開始する際に使用します。
2249
+
2250
+ \`\`\`bash
2251
+ tm new "認証機能のリファクタリング"
2252
+ # 出力: TASK-1 認証機能のリファクタリング
2253
+ \`\`\`
2254
+
2255
+ #### 進行順序の設定
2256
+
2257
+ タスクに進行順序(order)を設定することで、作業の優先順位を階層的に管理できます。
2258
+
2259
+ **基本的な使い方:**
2260
+ \`\`\`bash
2261
+ # 順番に並べる
2262
+ tm new "Task 1" --order 1
2263
+ tm new "Task 2" --order 2
2264
+ tm new "Task 3" --order 3
2265
+ \`\`\`
2266
+
2267
+ **階層的な順序:**
2268
+ \`\`\`bash
2269
+ # 親タスク
2270
+ tm new "Database Migration" --order 1
2271
+
2272
+ # 子タスク(1の下位タスク)
2273
+ tm new "Backup current DB" --order 1-1
2274
+ tm new "Run migration script" --order 1-2
2275
+ tm new "Verify data integrity" --order 1-3
2276
+
2277
+ # 孫タスク(1-2の下位タスク)
2278
+ tm new "Test migration locally" --order 1-2-1
2279
+ tm new "Deploy to staging" --order 1-2-2
2280
+
2281
+ # 別の親タスク
2282
+ tm new "Documentation" --order 2
2283
+ \`\`\`
2284
+
2285
+ **小数での挿入:**
2286
+ \`\`\`bash
2287
+ # すでに 1, 2 がある場合、間に挿入
2288
+ tm new "Insert between 1 and 2" --order 1.5
2289
+ # 保存時に自動的に正規化されて 1, 2, 3 になります
2290
+ \`\`\`
2291
+
2292
+ **order の特徴:**
2293
+ - \`todo\`, \`wip\` ステータスのみ order を保持
2294
+ - \`done\`, \`closed\`, \`pending\`, \`long\` に変更すると order は自動的に解除
2295
+ - 欠番は自動的に詰められる(1, 3, 5 → 1, 2, 3)
2296
+ - 孫タスクがある場合、その親番号は確保される
2297
+ - 既存の order と同じ値を設定すると、新しいタスクが優先され、既存タスクは後ろにシフトされる
2298
+
2299
+ **既存 order への挿入(インサートセマンティクス):**
2300
+ \`\`\`bash
2301
+ # order=1 が A, B, C に割り当てられている場合に新タスクへ order=1 を設定すると
2302
+ # → 新タスク=1, A=2, B=3, C=4 と全体がシフトする
2303
+ tm update <新タスクID> --order 1
2304
+ \`\`\`
2305
+
2306
+ ### 2. タスク一覧の表示 (\`tm list\`)
2307
+
2308
+ 現在進行中(\`todo\`, \`wip\`)のタスクを表示します。
2309
+
2310
+ \`\`\`bash
2311
+ tm list
2312
+ # または短縮形
2313
+ tm ls
2314
+ tm l
2315
+ # 出力: 1: 認証機能のリファクタリング [todo]
2316
+ \`\`\`
2317
+
2318
+ #### フィルタリングオプション
2319
+
2320
+ タスク一覧は様々な条件でフィルタリングできます。
2321
+
2322
+ **ステータスによるフィルタリング:**
2323
+ \`\`\`bash
2324
+ # すべてのタスク(done/closed含む)を表示
2325
+ tm list --status-all
2326
+ tm ls -a
2327
+
2328
+ # オープンなタスク(todo, wip, pending, long)を表示
2329
+ tm list --open
2330
+
2331
+ # 特定のステータスのタスクのみ表示
2332
+ tm list --status pending
2333
+ tm ls -s wip
2334
+ tm ls -s done
2335
+ \`\`\`
2336
+
2337
+ **優先度によるフィルタリング:**
2338
+ \`\`\`bash
2339
+ tm list --priority high
2340
+ tm list --priority medium
2341
+ \`\`\`
2342
+
2343
+ **バージョンによるフィルタリング:**
2344
+ \`\`\`bash
2345
+ # 特定のバージョン
2346
+ tm list --version 1.0.0
2347
+
2348
+ # TBD(未リリース)のタスク
2349
+ tm list --tbd
2350
+
2351
+ # リリース済みのタスク(version が tbd 以外)
2352
+ tm list --released
2353
+ \`\`\`
2354
+
2355
+ **表示件数の制限:**
2356
+ \`\`\`bash
2357
+ # 最初の5件のみ表示
2358
+ tm list --head 5
2359
+
2360
+ # 最後の10件のみ表示
2361
+ tm list --tail 10
2362
+
2363
+ # デフォルト値(10件)で制限
2364
+ tm list --head
2365
+ tm list --tail
2366
+ \`\`\`
2367
+
2368
+ **組み合わせ:**
2369
+ \`\`\`bash
2370
+ # リリース済みタスクの最初の3件
2371
+ tm list --released --head 3
2372
+
2373
+ # 高優先度のpendingタスク
2374
+ tm list --priority high --status pending
2375
+ \`\`\`
2376
+
2377
+ #### ソート順の指定
2378
+
2379
+ デフォルトでは進行順序(order)でソートされます。
2380
+
2381
+ \`\`\`bash
2382
+ # 進行順序でソート(デフォルト)
2383
+ tm list --sort order
2384
+ # order が未設定のタスクは後ろに表示されます
2385
+ # 1 < 1-1 < 1-2 < 2 < 2-1 の順
2386
+
2387
+ # タスクID順
2388
+ tm list --sort id
2389
+
2390
+ # 作成日時順
2391
+ tm list --sort created
2392
+ \`\`\`
2393
+
2394
+ ### 3. タスクの更新 (\`tm update\`)
2395
+
2396
+ タスクの状態を更新したり、作業ログ(body)を追記したりします。
2397
+ 複数のタスクを一度に更新することも可能です。
2398
+
2399
+ **ステータスの変更:**
2400
+
2401
+ \`\`\`bash
2402
+ tm update 1 --status wip
2403
+ \`\`\`
2404
+
2405
+ **作業ログの追記:**
2406
+
2407
+ \`\`\`bash
2408
+ tm update 1 --body "JWTの実装を開始"
2409
+ \`\`\`
2410
+
2411
+ **関連ファイルの追加(AIエージェント向け):**
2412
+
2413
+ \`\`\`bash
2414
+ tm update 1 --add-file src/auth.ts
2415
+ \`\`\`
2416
+
2417
+ **進行順序の更新:**
2418
+
2419
+ \`\`\`bash
2420
+ # order を設定
2421
+ tm update 1 --order 1-2
2422
+
2423
+ # order を解除
2424
+ tm update 1 --order null
2425
+ \`\`\`
2426
+
2427
+ **複数タスクの同時更新(コンテキストスイッチ):**
2428
+
2429
+ \`\`\`bash
2430
+ tm update 1 --status done 2 --status wip --body "バグ調査中"
2431
+ # タスク1を完了にし、タスク2をWIPにしてログを追記
2432
+ \`\`\`
2433
+
2434
+ ### 4. タスク詳細の確認 (\`tm get\`)
2435
+
2436
+ タスクの詳細情報(JSON形式)を取得します。AIエージェントがコンテキストを復元するのに役立ちます。
2437
+
2438
+ \`\`\`bash
2439
+ tm get 1
2440
+ \`\`\`
2441
+
2442
+ 履歴をすべて表示するには \`--history\` または \`--all\` オプションを使用します。
2443
+
2444
+ \`\`\`bash
2445
+ tm get 1 --history
2446
+ \`\`\`
2447
+
2448
+ ### 5. タスクの完了 (\`tm finish\`)
2449
+
2450
+ タスクを完了状態(\`done\`)にします。
2451
+
2452
+ \`\`\`bash
2453
+ tm finish 1
2454
+ \`\`\`
2455
+
2456
+ ## データの保存場所
2457
+
2458
+ 以下の優先順位でデータの保存先が決定されます。
2459
+
2460
+ | 優先度 | 条件 | 保存先 |
2461
+ |--------|------|--------|
2462
+ | 1 | 環境変数 \`TASK_MEMORY_PATH\` が設定されている | 環境変数で指定したパス |
2463
+ | 2 | \`.git\` ディレクトリが存在し、かつ \`.git/task-memory.json\` が存在する | \`.git/task-memory.json\` |
2464
+ | 3 | \`.git\`(ファイル・ディレクトリ問わず)と同じ階層に \`task-memory.json\` が存在する | \`<プロジェクトルート>/task-memory.json\` |
2465
+ | 4 | \`.git\` と同じ階層に \`.task-memory.json\` が存在する | \`<プロジェクトルート>/.task-memory.json\` |
2466
+ | 5 | \`.git\` が存在するが保存ファイルがない(新規) | \`.git/task-memory.json\`(\`.git\` がディレクトリの場合)または \`<プロジェクトルート>/task-memory.json\` |
2467
+ | 6 | \`.git\` が存在しない | \`~/.task-memory.json\` |
2468
+
2469
+ ### git worktree での利用
2470
+
2471
+ \`git worktree\` 使用時は \`.git\` がファイルになりますが、そのファイルと同じ階層の \`task-memory.json\` または \`.task-memory.json\` が自動的に参照されます。これにより、worktree ごとに独立したタスク管理が可能です。
2472
+
2473
+ ## 同期機能 (\`tm sync\`)
2474
+
2475
+ タスクデータを \`~/.local/task-memory/\` 経由で複数環境間で同期します。
2476
+
2477
+ ### セットアップ
2478
+
2479
+ \`\`\`bash
2480
+ # 同期を有効化(sync ID を自動生成)
2481
+ tm sync add
2482
+
2483
+ # 同期 ID を指定(複数マシン間で同じIDを使う場合)
2484
+ tm sync add --id my-project-sync
2485
+ \`\`\`
2486
+
2487
+ ### 基本操作
2488
+
2489
+ \`\`\`bash
2490
+ # タスクを同期ディレクトリに保存
2491
+ tm sync save
2492
+
2493
+ # 同期ディレクトリからタスクを読み込み(上書き)
2494
+ tm sync load
2495
+
2496
+ # 読み込み時にローカルとマージ
2497
+ tm sync load --merge
2498
+
2499
+ # 保存・コミット・プッシュを一括実行
2500
+ tm sync upload
2501
+
2502
+ # コミットメッセージを指定
2503
+ tm sync upload --message "作業完了"
2504
+
2505
+ # 同期状態を確認
2506
+ tm sync status
2507
+
2508
+ # 同期を無効化
2509
+ tm sync remove
2510
+ \`\`\`
2511
+
2512
+ ### GitHubリポジトリとの連携
2513
+
2514
+ \`\`\`bash
2515
+ # GitHubにprivateリポジトリを作成してremoteに設定(gh コマンド必要)
2516
+ tm sync repo create
2517
+
2518
+ # リポジトリ名を指定
2519
+ tm sync repo create --name my-sync-repo
2520
+
2521
+ # publicリポジトリとして作成
2522
+ tm sync repo create --public
2523
+
2524
+ # 既存のリポジトリをremoteに設定
2525
+ tm sync repo set git@github.com:user/repo.git
2526
+
2527
+ # 現在のremoteを確認
2528
+ tm sync repo show
2529
+ \`\`\`
2530
+
2531
+ リポジトリ設定後は \`tm git push\` / \`tm git pull\` で同期できます。
2532
+
2533
+ ### マージ動作
2534
+
2535
+ \`--merge\` オプションでは以下のルールでマージされます:
2536
+
2537
+ - **同一タスク**(ID と作成日時が一致): \`updated_at\` が新しい方を優先
2538
+ - **ID 衝突**(同じ ID だが別のタスク): リモートのタスクに新しい ID を割り当てて追加
2539
+ - **新規タスク**: そのまま追加
2540
+
2541
+ ### 暗号化
2542
+
2543
+ [age](https://age-encryption.org/) による非対称暗号化をサポートします。
2544
+
2545
+ \`\`\`bash
2546
+ # identity ファイルを生成してグローバルに設定(公開鍵も自動設定)
2547
+ tm sync set key --generate --global
2548
+
2549
+ # 既存の identity ファイルをグローバルに設定
2550
+ tm sync set key ~/.age.key --global
2551
+
2552
+ # 暗号化を有効化(グローバル)
2553
+ tm sync set encrypt on --global
2554
+
2555
+ # 特定プロジェクトのみ暗号化を無効化
2556
+ tm sync set encrypt off
2557
+ \`\`\`
2558
+
2559
+ - 暗号化時: \`identity\` ファイルから公開鍵を抽出して暗号化(push 側)
2560
+ - 復号時: \`identity\` ファイルの秘密鍵で復号(pull 側)
2561
+ - 同期ファイルは \`.json.age\` として保存される
2562
+
2563
+ **注意**: identity ファイルを紛失するとデータを復元できません。安全な場所にバックアップしてください。
2564
+ `;
2565
+
2566
+ // src/index.ts
2567
+ setAfterSaveCallback((store) => {
2568
+ tryAutoSync(store.sync, store);
2569
+ });
2570
+ var args = process.argv.slice(2);
2571
+ var command = args[0];
2572
+ var commandArgs = args.slice(1);
2573
+ switch (command) {
2574
+ case "new":
2575
+ newCommand(commandArgs);
2576
+ break;
2577
+ case "list":
2578
+ case "ls":
2579
+ case "l":
2580
+ listCommand(commandArgs);
2581
+ break;
2582
+ case "get":
2583
+ case "g":
2584
+ getCommand(commandArgs);
2585
+ break;
2586
+ case "finish":
2587
+ case "fin":
2588
+ case "f":
2589
+ finishCommand(commandArgs);
2590
+ break;
2591
+ case "update":
2592
+ case "up":
2593
+ case "u":
2594
+ updateCommand(commandArgs);
2595
+ break;
2596
+ case "env":
2597
+ envCommand(commandArgs);
2598
+ break;
2599
+ case "review":
2600
+ case "rev":
2601
+ case "tmr":
2602
+ reviewCommand(commandArgs);
2603
+ break;
2604
+ case "release":
2605
+ releaseCommand(commandArgs);
2606
+ break;
2607
+ case "close":
2608
+ closeCommand(commandArgs);
2609
+ break;
2610
+ case "sync":
2611
+ syncCommand(commandArgs);
2612
+ break;
2613
+ case "git":
2614
+ gitCommand(commandArgs);
2615
+ break;
2616
+ case "docs":
2617
+ console.log(usage_default);
2618
+ break;
2619
+ case "help":
2620
+ case "--help":
2621
+ case "-h":
2622
+ console.log(`
2623
+ Usage: tm <command> [args]
2624
+
2625
+ Commands:
2626
+ new <summary> [options]
2627
+ Create a new task.
2628
+ Options:
2629
+ --status, -s <status> Set initial status (todo, wip, done, pending, long, closed)
2630
+ --priority, -p <value> Set priority
2631
+ --goal, -g <text> Set completion goal
2632
+ --body, -b <text> Add initial body text
2633
+ --add-file, -a <path> Add editable file
2634
+ --read-file, -r <path> Add read-only file
2635
+
2636
+ list (ls, l) [options]
2637
+ List active tasks (todo, wip).
2638
+ Options:
2639
+ --status-all, -a Show all tasks (including done/closed)
2640
+ --open Show all open tasks (todo, wip, pending, long)
2641
+ --priority <p> Filter by priority
2642
+ --status, -s <status> Filter by status
2643
+ --version <v> Filter by version
2644
+ --tbd Filter by version 'tbd' (includes closed/done)
2645
+ --released Filter by released tasks (non-tbd version)
2646
+ --head [N] Show first N tasks (default: 10)
2647
+ --tail [N] Show last N tasks (default: 10)
2648
+
2649
+ get (g) <id...> [options]
2650
+ Get task details (JSON).
2651
+ Options:
2652
+ --all, -a, --history, -h Show full history of bodies
2653
+
2654
+ finish (fin, f) <id...>
2655
+ Mark task(s) as done.
2656
+
2657
+ update (up, u) <id...> [options]
2658
+ Update task(s). Supports context switching.
2659
+ Options:
2660
+ --status, -s <status> Update status (todo, wip, done, pending, long, closed)
2661
+ --priority, -p <value> Update priority
2662
+ --goal, -g <text> Update completion goal
2663
+ --body, -b <text> Append body text
2664
+ --add-file, -a <path> Add editable file
2665
+ --rm-file, -d <path> Remove editable file
2666
+ --read-file, -r <path> Add read-only file
2667
+
2668
+ env
2669
+ Show the current task data file path.
2670
+
2671
+ review (rev, tmr) <subcommand> [args]
2672
+ Manage reviews.
2673
+ Subcommands: new, list, get, update, return, accept, reject
2674
+
2675
+ release <id...> --version <v>
2676
+ Set version for task(s).
2677
+
2678
+ close <id...> [--body <text>]
2679
+ Close task(s). Alias for update --status closed.
2680
+
2681
+ sync <subcommand> [options]
2682
+ Sync tasks to ~/.local/task-memory/ repository.
2683
+ Subcommands: add, remove, push, pull, set, status, list
2684
+
2685
+ git <git-command> [args]
2686
+ Run git commands in ~/.local/task-memory/ repository.
2687
+
2688
+ docs
2689
+ Show usage documentation.
2690
+
2691
+ Examples:
2692
+ tm new "Refactor auth" --status wip --body "Starting now" --priority high
2693
+ tm update 1 --status done 2 --status wip --body "Fixing bug"
2694
+ tm get 1 --history
2695
+ `);
2696
+ break;
2697
+ default:
2698
+ if (!command) {
2699
+ console.log(`
2700
+ Usage: tm <command> [args]
2701
+
2702
+ Run 'tm help' for detailed usage and examples.
2703
+ `);
2704
+ } else {
2705
+ console.error(`Error: Unknown command '${command}'. Run 'tm help' for usage.`);
2706
+ process.exit(1);
2707
+ }
2708
+ }