@backlog-md/core 0.2.1 → 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.
- package/dist/core/Core.d.ts +130 -1
- package/dist/core/Core.d.ts.map +1 -1
- package/dist/core/Core.js +541 -35
- package/dist/core/Core.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/markdown/index.d.ts +56 -1
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +174 -0
- package/dist/markdown/index.js.map +1 -1
- package/dist/types/index.d.ts +52 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/milestones.d.ts +65 -0
- package/dist/utils/milestones.d.ts.map +1 -0
- package/dist/utils/milestones.js +191 -0
- package/dist/utils/milestones.js.map +1 -0
- package/package.json +1 -1
package/dist/core/Core.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* by accepting adapter implementations for I/O operations.
|
|
6
6
|
*/
|
|
7
7
|
import { parseBacklogConfig, serializeBacklogConfig } from "./config-parser";
|
|
8
|
-
import { parseTaskMarkdown, extractTaskIndexFromPath } from "../markdown";
|
|
9
|
-
import { sortTasks, sortTasksBy, groupTasksByStatus } from "../utils";
|
|
8
|
+
import { parseTaskMarkdown, serializeTaskMarkdown, extractTaskIndexFromPath, parseMilestoneMarkdown, serializeMilestoneMarkdown, getMilestoneFilename, extractMilestoneIdFromFilename, } from "../markdown";
|
|
9
|
+
import { sortTasks, sortTasksBy, groupTasksByStatus, groupTasksByMilestone, milestoneKey, } from "../utils";
|
|
10
10
|
/**
|
|
11
11
|
* Core class for Backlog.md operations
|
|
12
12
|
*
|
|
@@ -239,23 +239,15 @@ export class Core {
|
|
|
239
239
|
let entries = Array.from(this.taskIndex.values()).filter((e) => e.source === source);
|
|
240
240
|
// Per-source limit
|
|
241
241
|
const limit = source === "tasks" ? tasksLimit : completedLimit;
|
|
242
|
-
// Sort
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
// Sort by title
|
|
254
|
-
entries = entries.sort((a, b) => {
|
|
255
|
-
const cmp = a.title.localeCompare(b.title);
|
|
256
|
-
return tasksSortDirection === "asc" ? cmp : -cmp;
|
|
257
|
-
});
|
|
258
|
-
}
|
|
242
|
+
// Sort by ID (numeric)
|
|
243
|
+
entries = entries.sort((a, b) => {
|
|
244
|
+
const aNum = parseInt(a.id.replace(/\D/g, ""), 10) || 0;
|
|
245
|
+
const bNum = parseInt(b.id.replace(/\D/g, ""), 10) || 0;
|
|
246
|
+
// Completed: descending (most recent first), Active: ascending
|
|
247
|
+
return source === "completed" && completedSortByIdDesc
|
|
248
|
+
? bNum - aNum
|
|
249
|
+
: aNum - bNum;
|
|
250
|
+
});
|
|
259
251
|
const total = entries.length;
|
|
260
252
|
const pageEntries = entries.slice(offset, offset + limit);
|
|
261
253
|
// Load only the tasks for this page
|
|
@@ -290,22 +282,15 @@ export class Core {
|
|
|
290
282
|
const completedSortByIdDesc = options?.completedSortByIdDesc ?? true;
|
|
291
283
|
// Get index entries for this source
|
|
292
284
|
let entries = Array.from(this.taskIndex.values()).filter((e) => e.source === source);
|
|
293
|
-
// Sort
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
// Sort by title
|
|
304
|
-
entries = entries.sort((a, b) => {
|
|
305
|
-
const cmp = a.title.localeCompare(b.title);
|
|
306
|
-
return sortDirection === "asc" ? cmp : -cmp;
|
|
307
|
-
});
|
|
308
|
-
}
|
|
285
|
+
// Sort by ID (numeric)
|
|
286
|
+
entries = entries.sort((a, b) => {
|
|
287
|
+
const aNum = parseInt(a.id.replace(/\D/g, ""), 10) || 0;
|
|
288
|
+
const bNum = parseInt(b.id.replace(/\D/g, ""), 10) || 0;
|
|
289
|
+
// Completed: descending (most recent first), Active: ascending
|
|
290
|
+
return source === "completed" && completedSortByIdDesc
|
|
291
|
+
? bNum - aNum
|
|
292
|
+
: aNum - bNum;
|
|
293
|
+
});
|
|
309
294
|
const total = entries.length;
|
|
310
295
|
const pageEntries = entries.slice(currentOffset, currentOffset + limit);
|
|
311
296
|
// Load only the tasks for this page
|
|
@@ -345,6 +330,10 @@ export class Core {
|
|
|
345
330
|
if (filter?.priority) {
|
|
346
331
|
tasks = tasks.filter((t) => t.priority === filter.priority);
|
|
347
332
|
}
|
|
333
|
+
if (filter?.milestone) {
|
|
334
|
+
const filterKey = milestoneKey(filter.milestone);
|
|
335
|
+
tasks = tasks.filter((t) => milestoneKey(t.milestone) === filterKey);
|
|
336
|
+
}
|
|
348
337
|
if (filter?.labels && filter.labels.length > 0) {
|
|
349
338
|
tasks = tasks.filter((t) => filter.labels.some((label) => t.labels.includes(label)));
|
|
350
339
|
}
|
|
@@ -365,6 +354,231 @@ export class Core {
|
|
|
365
354
|
const tasks = Array.from(this.tasks.values());
|
|
366
355
|
return groupTasksByStatus(tasks, this.config.statuses);
|
|
367
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Get tasks grouped by milestone
|
|
359
|
+
*
|
|
360
|
+
* Returns a MilestoneSummary with:
|
|
361
|
+
* - milestones: List of milestone IDs in display order
|
|
362
|
+
* - buckets: Array of MilestoneBucket with tasks, progress, status counts
|
|
363
|
+
*
|
|
364
|
+
* The first bucket is always "Tasks without milestone".
|
|
365
|
+
* Each bucket includes progress percentage based on done status.
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```typescript
|
|
369
|
+
* const summary = core.getTasksByMilestone();
|
|
370
|
+
* for (const bucket of summary.buckets) {
|
|
371
|
+
* console.log(`${bucket.label}: ${bucket.progress}% complete`);
|
|
372
|
+
* console.log(` ${bucket.doneCount}/${bucket.total} tasks done`);
|
|
373
|
+
* }
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
376
|
+
getTasksByMilestone() {
|
|
377
|
+
this.ensureInitialized();
|
|
378
|
+
const tasks = Array.from(this.tasks.values());
|
|
379
|
+
return groupTasksByMilestone(tasks, this.config.milestones, this.config.statuses);
|
|
380
|
+
}
|
|
381
|
+
// =========================================================================
|
|
382
|
+
// Milestone CRUD Operations
|
|
383
|
+
// =========================================================================
|
|
384
|
+
/**
|
|
385
|
+
* Get the milestones directory path
|
|
386
|
+
*/
|
|
387
|
+
getMilestonesDir() {
|
|
388
|
+
return this.fs.join(this.projectRoot, "backlog", "milestones");
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* List all milestones from the milestones directory
|
|
392
|
+
*
|
|
393
|
+
* @returns Array of Milestone objects sorted by ID
|
|
394
|
+
*/
|
|
395
|
+
async listMilestones() {
|
|
396
|
+
const milestonesDir = this.getMilestonesDir();
|
|
397
|
+
// Check if directory exists
|
|
398
|
+
if (!(await this.fs.exists(milestonesDir))) {
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
const entries = await this.fs.readDir(milestonesDir);
|
|
402
|
+
const milestones = [];
|
|
403
|
+
for (const entry of entries) {
|
|
404
|
+
// Skip non-milestone files
|
|
405
|
+
if (!entry.endsWith(".md"))
|
|
406
|
+
continue;
|
|
407
|
+
if (entry.toLowerCase() === "readme.md")
|
|
408
|
+
continue;
|
|
409
|
+
const milestoneId = extractMilestoneIdFromFilename(entry);
|
|
410
|
+
if (!milestoneId)
|
|
411
|
+
continue;
|
|
412
|
+
const filepath = this.fs.join(milestonesDir, entry);
|
|
413
|
+
// Skip directories
|
|
414
|
+
if (await this.fs.isDirectory(filepath))
|
|
415
|
+
continue;
|
|
416
|
+
try {
|
|
417
|
+
const content = await this.fs.readFile(filepath);
|
|
418
|
+
milestones.push(parseMilestoneMarkdown(content));
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
console.warn(`Failed to parse milestone file ${filepath}:`, error);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Sort by ID for consistent ordering
|
|
425
|
+
return milestones.sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true }));
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Load a single milestone by ID
|
|
429
|
+
*
|
|
430
|
+
* @param id - Milestone ID (e.g., "m-0")
|
|
431
|
+
* @returns Milestone or null if not found
|
|
432
|
+
*/
|
|
433
|
+
async loadMilestone(id) {
|
|
434
|
+
const milestonesDir = this.getMilestonesDir();
|
|
435
|
+
if (!(await this.fs.exists(milestonesDir))) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
const entries = await this.fs.readDir(milestonesDir);
|
|
439
|
+
// Find file matching the ID
|
|
440
|
+
const milestoneFile = entries.find((entry) => {
|
|
441
|
+
const fileId = extractMilestoneIdFromFilename(entry);
|
|
442
|
+
return fileId === id;
|
|
443
|
+
});
|
|
444
|
+
if (!milestoneFile) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
const filepath = this.fs.join(milestonesDir, milestoneFile);
|
|
448
|
+
try {
|
|
449
|
+
const content = await this.fs.readFile(filepath);
|
|
450
|
+
return parseMilestoneMarkdown(content);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Create a new milestone
|
|
458
|
+
*
|
|
459
|
+
* @param input - Milestone creation input
|
|
460
|
+
* @returns Created milestone
|
|
461
|
+
*/
|
|
462
|
+
async createMilestone(input) {
|
|
463
|
+
const milestonesDir = this.getMilestonesDir();
|
|
464
|
+
// Ensure milestones directory exists
|
|
465
|
+
await this.fs.createDir(milestonesDir, { recursive: true });
|
|
466
|
+
// Find next available milestone ID
|
|
467
|
+
const entries = await this.fs.readDir(milestonesDir).catch(() => []);
|
|
468
|
+
const existingIds = entries
|
|
469
|
+
.map((f) => {
|
|
470
|
+
const match = f.match(/^m-(\d+)/);
|
|
471
|
+
return match?.[1] ? parseInt(match[1], 10) : -1;
|
|
472
|
+
})
|
|
473
|
+
.filter((id) => id >= 0);
|
|
474
|
+
const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 0;
|
|
475
|
+
const id = `m-${nextId}`;
|
|
476
|
+
const description = input.description || `Milestone: ${input.title}`;
|
|
477
|
+
// Create a temporary milestone to generate content
|
|
478
|
+
const tempMilestone = {
|
|
479
|
+
id,
|
|
480
|
+
title: input.title,
|
|
481
|
+
description,
|
|
482
|
+
rawContent: "",
|
|
483
|
+
tasks: [],
|
|
484
|
+
};
|
|
485
|
+
// Generate content
|
|
486
|
+
const content = serializeMilestoneMarkdown(tempMilestone);
|
|
487
|
+
// Create the final milestone with correct rawContent
|
|
488
|
+
const milestone = {
|
|
489
|
+
id,
|
|
490
|
+
title: input.title,
|
|
491
|
+
description,
|
|
492
|
+
rawContent: content,
|
|
493
|
+
tasks: [],
|
|
494
|
+
};
|
|
495
|
+
// Write file
|
|
496
|
+
const filename = getMilestoneFilename(id, input.title);
|
|
497
|
+
const filepath = this.fs.join(milestonesDir, filename);
|
|
498
|
+
await this.fs.writeFile(filepath, content);
|
|
499
|
+
return milestone;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Update an existing milestone
|
|
503
|
+
*
|
|
504
|
+
* @param id - Milestone ID to update
|
|
505
|
+
* @param input - Fields to update
|
|
506
|
+
* @returns Updated milestone or null if not found
|
|
507
|
+
*/
|
|
508
|
+
async updateMilestone(id, input) {
|
|
509
|
+
const existing = await this.loadMilestone(id);
|
|
510
|
+
if (!existing) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const milestonesDir = this.getMilestonesDir();
|
|
514
|
+
const entries = await this.fs.readDir(milestonesDir);
|
|
515
|
+
// Find the current file
|
|
516
|
+
const currentFile = entries.find((entry) => {
|
|
517
|
+
const fileId = extractMilestoneIdFromFilename(entry);
|
|
518
|
+
return fileId === id;
|
|
519
|
+
});
|
|
520
|
+
if (!currentFile) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
// Build updated values
|
|
524
|
+
const newTitle = input.title ?? existing.title;
|
|
525
|
+
const newDescription = input.description ?? existing.description;
|
|
526
|
+
// Create a temporary milestone to generate content
|
|
527
|
+
const tempMilestone = {
|
|
528
|
+
id: existing.id,
|
|
529
|
+
title: newTitle,
|
|
530
|
+
description: newDescription,
|
|
531
|
+
rawContent: "",
|
|
532
|
+
tasks: existing.tasks,
|
|
533
|
+
};
|
|
534
|
+
// Generate new content
|
|
535
|
+
const content = serializeMilestoneMarkdown(tempMilestone);
|
|
536
|
+
// Create the final updated milestone
|
|
537
|
+
const updated = {
|
|
538
|
+
id: existing.id,
|
|
539
|
+
title: newTitle,
|
|
540
|
+
description: newDescription,
|
|
541
|
+
rawContent: content,
|
|
542
|
+
tasks: existing.tasks,
|
|
543
|
+
};
|
|
544
|
+
// Delete old file
|
|
545
|
+
const oldPath = this.fs.join(milestonesDir, currentFile);
|
|
546
|
+
await this.fs.deleteFile(oldPath);
|
|
547
|
+
// Write new file (with potentially new filename if title changed)
|
|
548
|
+
const newFilename = getMilestoneFilename(id, updated.title);
|
|
549
|
+
const newPath = this.fs.join(milestonesDir, newFilename);
|
|
550
|
+
await this.fs.writeFile(newPath, content);
|
|
551
|
+
return updated;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Delete a milestone
|
|
555
|
+
*
|
|
556
|
+
* @param id - Milestone ID to delete
|
|
557
|
+
* @returns true if deleted, false if not found
|
|
558
|
+
*/
|
|
559
|
+
async deleteMilestone(id) {
|
|
560
|
+
const milestonesDir = this.getMilestonesDir();
|
|
561
|
+
if (!(await this.fs.exists(milestonesDir))) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
const entries = await this.fs.readDir(milestonesDir);
|
|
565
|
+
// Find file matching the ID
|
|
566
|
+
const milestoneFile = entries.find((entry) => {
|
|
567
|
+
const fileId = extractMilestoneIdFromFilename(entry);
|
|
568
|
+
return fileId === id;
|
|
569
|
+
});
|
|
570
|
+
if (!milestoneFile) {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
const filepath = this.fs.join(milestonesDir, milestoneFile);
|
|
574
|
+
try {
|
|
575
|
+
await this.fs.deleteFile(filepath);
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
368
582
|
/**
|
|
369
583
|
* Get a single task by ID
|
|
370
584
|
*
|
|
@@ -503,6 +717,10 @@ export class Core {
|
|
|
503
717
|
if (filter.priority) {
|
|
504
718
|
result = result.filter((t) => t.priority === filter.priority);
|
|
505
719
|
}
|
|
720
|
+
if (filter.milestone) {
|
|
721
|
+
const filterKey = milestoneKey(filter.milestone);
|
|
722
|
+
result = result.filter((t) => milestoneKey(t.milestone) === filterKey);
|
|
723
|
+
}
|
|
506
724
|
if (filter.labels && filter.labels.length > 0) {
|
|
507
725
|
result = result.filter((t) => filter.labels.some((label) => t.labels.includes(label)));
|
|
508
726
|
}
|
|
@@ -536,5 +754,293 @@ export class Core {
|
|
|
536
754
|
}
|
|
537
755
|
}
|
|
538
756
|
}
|
|
757
|
+
// =========================================================================
|
|
758
|
+
// Milestone-Task Sync Helpers
|
|
759
|
+
// =========================================================================
|
|
760
|
+
/**
|
|
761
|
+
* Add a task ID to a milestone's tasks array
|
|
762
|
+
*
|
|
763
|
+
* @param taskId - Task ID to add
|
|
764
|
+
* @param milestoneId - Milestone ID to update
|
|
765
|
+
*/
|
|
766
|
+
async addTaskToMilestone(taskId, milestoneId) {
|
|
767
|
+
const milestone = await this.loadMilestone(milestoneId);
|
|
768
|
+
if (!milestone) {
|
|
769
|
+
console.warn(`Milestone ${milestoneId} not found when adding task ${taskId}`);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
// Check if task already in milestone
|
|
773
|
+
if (milestone.tasks.includes(taskId)) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
// Update milestone with new task
|
|
777
|
+
const updatedMilestone = {
|
|
778
|
+
...milestone,
|
|
779
|
+
tasks: [...milestone.tasks, taskId],
|
|
780
|
+
};
|
|
781
|
+
await this.writeMilestoneFile(updatedMilestone);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Remove a task ID from a milestone's tasks array
|
|
785
|
+
*
|
|
786
|
+
* @param taskId - Task ID to remove
|
|
787
|
+
* @param milestoneId - Milestone ID to update
|
|
788
|
+
*/
|
|
789
|
+
async removeTaskFromMilestone(taskId, milestoneId) {
|
|
790
|
+
const milestone = await this.loadMilestone(milestoneId);
|
|
791
|
+
if (!milestone) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
// Check if task is in milestone
|
|
795
|
+
if (!milestone.tasks.includes(taskId)) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
// Update milestone without the task
|
|
799
|
+
const updatedMilestone = {
|
|
800
|
+
...milestone,
|
|
801
|
+
tasks: milestone.tasks.filter((id) => id !== taskId),
|
|
802
|
+
};
|
|
803
|
+
await this.writeMilestoneFile(updatedMilestone);
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Write a milestone to disk
|
|
807
|
+
*/
|
|
808
|
+
async writeMilestoneFile(milestone) {
|
|
809
|
+
const milestonesDir = this.getMilestonesDir();
|
|
810
|
+
const entries = await this.fs.readDir(milestonesDir).catch(() => []);
|
|
811
|
+
// Find and delete the current file
|
|
812
|
+
const currentFile = entries.find((entry) => {
|
|
813
|
+
const fileId = extractMilestoneIdFromFilename(entry);
|
|
814
|
+
return fileId === milestone.id;
|
|
815
|
+
});
|
|
816
|
+
if (currentFile) {
|
|
817
|
+
const oldPath = this.fs.join(milestonesDir, currentFile);
|
|
818
|
+
await this.fs.deleteFile(oldPath).catch(() => { });
|
|
819
|
+
}
|
|
820
|
+
// Write new file
|
|
821
|
+
const content = serializeMilestoneMarkdown(milestone);
|
|
822
|
+
const filename = getMilestoneFilename(milestone.id, milestone.title);
|
|
823
|
+
const filepath = this.fs.join(milestonesDir, filename);
|
|
824
|
+
await this.fs.writeFile(filepath, content);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Get the tasks directory path
|
|
828
|
+
*/
|
|
829
|
+
getTasksDir() {
|
|
830
|
+
return this.fs.join(this.projectRoot, "backlog", "tasks");
|
|
831
|
+
}
|
|
832
|
+
// =========================================================================
|
|
833
|
+
// Task CRUD Operations
|
|
834
|
+
// =========================================================================
|
|
835
|
+
/**
|
|
836
|
+
* Create a new task
|
|
837
|
+
*
|
|
838
|
+
* @param input - Task creation input
|
|
839
|
+
* @returns Created task
|
|
840
|
+
*/
|
|
841
|
+
async createTask(input) {
|
|
842
|
+
this.ensureInitialized();
|
|
843
|
+
const tasksDir = this.getTasksDir();
|
|
844
|
+
// Ensure tasks directory exists
|
|
845
|
+
await this.fs.createDir(tasksDir, { recursive: true });
|
|
846
|
+
// Generate next task ID
|
|
847
|
+
const existingIds = Array.from(this.tasks.keys())
|
|
848
|
+
.map((id) => parseInt(id.replace(/\D/g, ""), 10))
|
|
849
|
+
.filter((n) => !isNaN(n));
|
|
850
|
+
const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1;
|
|
851
|
+
const taskId = String(nextId);
|
|
852
|
+
// Build task object
|
|
853
|
+
const now = new Date().toISOString().split("T")[0];
|
|
854
|
+
const task = {
|
|
855
|
+
id: taskId,
|
|
856
|
+
title: input.title,
|
|
857
|
+
status: input.status || this.config.defaultStatus || "To Do",
|
|
858
|
+
priority: input.priority,
|
|
859
|
+
assignee: input.assignee || [],
|
|
860
|
+
createdDate: now,
|
|
861
|
+
labels: input.labels || [],
|
|
862
|
+
milestone: input.milestone,
|
|
863
|
+
dependencies: input.dependencies || [],
|
|
864
|
+
parentTaskId: input.parentTaskId,
|
|
865
|
+
description: input.description,
|
|
866
|
+
implementationPlan: input.implementationPlan,
|
|
867
|
+
implementationNotes: input.implementationNotes,
|
|
868
|
+
acceptanceCriteriaItems: input.acceptanceCriteria?.map((ac, i) => ({
|
|
869
|
+
index: i + 1,
|
|
870
|
+
text: ac.text,
|
|
871
|
+
checked: ac.checked || false,
|
|
872
|
+
})),
|
|
873
|
+
rawContent: input.rawContent,
|
|
874
|
+
source: "local",
|
|
875
|
+
};
|
|
876
|
+
// Serialize and write file
|
|
877
|
+
const content = serializeTaskMarkdown(task);
|
|
878
|
+
const safeTitle = input.title
|
|
879
|
+
.replace(/[<>:"/\\|?*]/g, "")
|
|
880
|
+
.replace(/\s+/g, " ")
|
|
881
|
+
.slice(0, 50);
|
|
882
|
+
const filename = `${taskId} - ${safeTitle}.md`;
|
|
883
|
+
const filepath = this.fs.join(tasksDir, filename);
|
|
884
|
+
await this.fs.writeFile(filepath, content);
|
|
885
|
+
// Update in-memory cache
|
|
886
|
+
task.filePath = filepath;
|
|
887
|
+
this.tasks.set(taskId, task);
|
|
888
|
+
// Sync milestone if specified
|
|
889
|
+
if (input.milestone) {
|
|
890
|
+
await this.addTaskToMilestone(taskId, input.milestone);
|
|
891
|
+
}
|
|
892
|
+
return task;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Update an existing task
|
|
896
|
+
*
|
|
897
|
+
* @param id - Task ID to update
|
|
898
|
+
* @param input - Fields to update
|
|
899
|
+
* @returns Updated task or null if not found
|
|
900
|
+
*/
|
|
901
|
+
async updateTask(id, input) {
|
|
902
|
+
this.ensureInitialized();
|
|
903
|
+
const existing = this.tasks.get(id);
|
|
904
|
+
if (!existing) {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
const oldMilestone = existing.milestone;
|
|
908
|
+
const newMilestone = input.milestone === null
|
|
909
|
+
? undefined
|
|
910
|
+
: input.milestone !== undefined
|
|
911
|
+
? input.milestone
|
|
912
|
+
: oldMilestone;
|
|
913
|
+
// Build updated task
|
|
914
|
+
const now = new Date().toISOString().split("T")[0];
|
|
915
|
+
const updated = {
|
|
916
|
+
...existing,
|
|
917
|
+
title: input.title ?? existing.title,
|
|
918
|
+
status: input.status ?? existing.status,
|
|
919
|
+
priority: input.priority ?? existing.priority,
|
|
920
|
+
milestone: newMilestone,
|
|
921
|
+
updatedDate: now,
|
|
922
|
+
description: input.description ?? existing.description,
|
|
923
|
+
implementationPlan: input.clearImplementationPlan
|
|
924
|
+
? undefined
|
|
925
|
+
: input.implementationPlan ?? existing.implementationPlan,
|
|
926
|
+
implementationNotes: input.clearImplementationNotes
|
|
927
|
+
? undefined
|
|
928
|
+
: input.implementationNotes ?? existing.implementationNotes,
|
|
929
|
+
ordinal: input.ordinal ?? existing.ordinal,
|
|
930
|
+
dependencies: input.dependencies ?? existing.dependencies,
|
|
931
|
+
};
|
|
932
|
+
// Handle label operations
|
|
933
|
+
if (input.labels) {
|
|
934
|
+
updated.labels = input.labels;
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
if (input.addLabels) {
|
|
938
|
+
updated.labels = [...new Set([...updated.labels, ...input.addLabels])];
|
|
939
|
+
}
|
|
940
|
+
if (input.removeLabels) {
|
|
941
|
+
updated.labels = updated.labels.filter((l) => !input.removeLabels.includes(l));
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// Handle assignee
|
|
945
|
+
if (input.assignee) {
|
|
946
|
+
updated.assignee = input.assignee;
|
|
947
|
+
}
|
|
948
|
+
// Handle dependency operations
|
|
949
|
+
if (input.addDependencies) {
|
|
950
|
+
updated.dependencies = [
|
|
951
|
+
...new Set([...updated.dependencies, ...input.addDependencies]),
|
|
952
|
+
];
|
|
953
|
+
}
|
|
954
|
+
if (input.removeDependencies) {
|
|
955
|
+
updated.dependencies = updated.dependencies.filter((d) => !input.removeDependencies.includes(d));
|
|
956
|
+
}
|
|
957
|
+
// Handle acceptance criteria
|
|
958
|
+
if (input.acceptanceCriteria) {
|
|
959
|
+
updated.acceptanceCriteriaItems = input.acceptanceCriteria.map((ac, i) => ({
|
|
960
|
+
index: i + 1,
|
|
961
|
+
text: ac.text,
|
|
962
|
+
checked: ac.checked || false,
|
|
963
|
+
}));
|
|
964
|
+
}
|
|
965
|
+
// Serialize and write file
|
|
966
|
+
const content = serializeTaskMarkdown(updated);
|
|
967
|
+
// Delete old file if exists
|
|
968
|
+
if (existing.filePath) {
|
|
969
|
+
await this.fs.deleteFile(existing.filePath).catch(() => { });
|
|
970
|
+
}
|
|
971
|
+
// Write new file
|
|
972
|
+
const tasksDir = this.getTasksDir();
|
|
973
|
+
const safeTitle = updated.title
|
|
974
|
+
.replace(/[<>:"/\\|?*]/g, "")
|
|
975
|
+
.replace(/\s+/g, " ")
|
|
976
|
+
.slice(0, 50);
|
|
977
|
+
const filename = `${id} - ${safeTitle}.md`;
|
|
978
|
+
const filepath = this.fs.join(tasksDir, filename);
|
|
979
|
+
await this.fs.writeFile(filepath, content);
|
|
980
|
+
// Update in-memory cache
|
|
981
|
+
updated.filePath = filepath;
|
|
982
|
+
this.tasks.set(id, updated);
|
|
983
|
+
// Handle milestone sync
|
|
984
|
+
const milestoneChanged = milestoneKey(oldMilestone) !== milestoneKey(newMilestone);
|
|
985
|
+
if (milestoneChanged) {
|
|
986
|
+
// Remove from old milestone
|
|
987
|
+
if (oldMilestone) {
|
|
988
|
+
await this.removeTaskFromMilestone(id, oldMilestone);
|
|
989
|
+
}
|
|
990
|
+
// Add to new milestone
|
|
991
|
+
if (newMilestone) {
|
|
992
|
+
await this.addTaskToMilestone(id, newMilestone);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return updated;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Delete a task
|
|
999
|
+
*
|
|
1000
|
+
* @param id - Task ID to delete
|
|
1001
|
+
* @returns true if deleted, false if not found
|
|
1002
|
+
*/
|
|
1003
|
+
async deleteTask(id) {
|
|
1004
|
+
this.ensureInitialized();
|
|
1005
|
+
const task = this.tasks.get(id);
|
|
1006
|
+
if (!task) {
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
1009
|
+
// Remove from milestone if assigned
|
|
1010
|
+
if (task.milestone) {
|
|
1011
|
+
await this.removeTaskFromMilestone(id, task.milestone);
|
|
1012
|
+
}
|
|
1013
|
+
// Delete file
|
|
1014
|
+
if (task.filePath) {
|
|
1015
|
+
try {
|
|
1016
|
+
await this.fs.deleteFile(task.filePath);
|
|
1017
|
+
}
|
|
1018
|
+
catch {
|
|
1019
|
+
// File may already be deleted
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
// Remove from in-memory cache
|
|
1023
|
+
this.tasks.delete(id);
|
|
1024
|
+
return true;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Load specific tasks by their IDs (for lazy loading milestone tasks)
|
|
1028
|
+
*
|
|
1029
|
+
* @param ids - Array of task IDs to load
|
|
1030
|
+
* @returns Array of loaded tasks (missing tasks excluded)
|
|
1031
|
+
*/
|
|
1032
|
+
async loadTasksByIds(ids) {
|
|
1033
|
+
// If fully initialized, return from cache
|
|
1034
|
+
if (this.initialized) {
|
|
1035
|
+
return ids
|
|
1036
|
+
.map((id) => this.tasks.get(id))
|
|
1037
|
+
.filter((t) => t !== undefined);
|
|
1038
|
+
}
|
|
1039
|
+
// If lazy initialized, load on demand
|
|
1040
|
+
if (this.lazyInitialized) {
|
|
1041
|
+
return this.loadTasks(ids);
|
|
1042
|
+
}
|
|
1043
|
+
throw new Error("Core not initialized. Call initialize() or initializeLazy() first.");
|
|
1044
|
+
}
|
|
539
1045
|
}
|
|
540
1046
|
//# sourceMappingURL=Core.js.map
|