@industry-theme/backlogmd-kanban-panel 1.2.9 → 1.2.11
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/panels.bundle.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __defNormalProp = (obj, key2, value) => key2 in obj ? __defProp(obj, key2, { enumerable: true, configurable: true, writable: true, value }) : obj[key2] = value;
|
|
3
|
-
var __publicField = (obj, key2, value) => __defNormalProp(obj, typeof key2 !== "symbol" ? key2 + "" : key2, value);
|
|
4
1
|
import * as React2 from "react";
|
|
5
2
|
import React2__default, { useLayoutEffect, useEffect, useRef, useMemo, useCallback, useState, createContext, memo, useReducer, useContext, forwardRef, cloneElement, createElement, useImperativeHandle } from "react";
|
|
6
3
|
import { unstable_batchedUpdates, createPortal } from "react-dom";
|
|
7
4
|
import { jsxs, Fragment, jsx } from "react/jsx-runtime";
|
|
8
|
-
import {
|
|
5
|
+
import { DEFAULT_TASK_STATUSES, Core } from "@backlog-md/core";
|
|
6
|
+
import { trace, context, SpanStatusCode } from "@opentelemetry/api";
|
|
9
7
|
const canUseDOM = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
|
|
10
8
|
function isWindow(element2) {
|
|
11
9
|
const elementString = Object.prototype.toString.call(element2);
|
|
@@ -6338,2152 +6336,7 @@ function gt(o, e, t, r2) {
|
|
|
6338
6336
|
};
|
|
6339
6337
|
}, [o, e, t, r2]);
|
|
6340
6338
|
}
|
|
6341
|
-
const
|
|
6342
|
-
TODO: "To Do",
|
|
6343
|
-
IN_PROGRESS: "In Progress",
|
|
6344
|
-
DONE: "Done"
|
|
6345
|
-
};
|
|
6346
|
-
function parseTaskMarkdown(content2, filePath) {
|
|
6347
|
-
const { frontmatter, title, rawContent, acceptanceCriteria, description } = parseMarkdownContent(content2);
|
|
6348
|
-
const id = frontmatter.id || extractIdFromPath(filePath);
|
|
6349
|
-
const taskTitle = frontmatter.title || title || `Task ${id}`;
|
|
6350
|
-
return {
|
|
6351
|
-
id,
|
|
6352
|
-
title: taskTitle,
|
|
6353
|
-
status: frontmatter.status || "backlog",
|
|
6354
|
-
priority: frontmatter.priority,
|
|
6355
|
-
assignee: frontmatter.assignee || [],
|
|
6356
|
-
reporter: frontmatter.reporter,
|
|
6357
|
-
createdDate: frontmatter.createdDate || (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
6358
|
-
updatedDate: frontmatter.updatedDate,
|
|
6359
|
-
labels: frontmatter.labels || [],
|
|
6360
|
-
milestone: frontmatter.milestone,
|
|
6361
|
-
dependencies: frontmatter.dependencies || [],
|
|
6362
|
-
references: frontmatter.references || [],
|
|
6363
|
-
parentTaskId: frontmatter.parentTaskId,
|
|
6364
|
-
subtasks: frontmatter.subtasks,
|
|
6365
|
-
branch: frontmatter.branch,
|
|
6366
|
-
ordinal: frontmatter.ordinal,
|
|
6367
|
-
rawContent,
|
|
6368
|
-
description,
|
|
6369
|
-
acceptanceCriteriaItems: acceptanceCriteria,
|
|
6370
|
-
filePath
|
|
6371
|
-
};
|
|
6372
|
-
}
|
|
6373
|
-
function serializeTaskMarkdown(task) {
|
|
6374
|
-
const lines = [];
|
|
6375
|
-
lines.push("---");
|
|
6376
|
-
lines.push(`status: ${task.status}`);
|
|
6377
|
-
if (task.priority) {
|
|
6378
|
-
lines.push(`priority: ${task.priority}`);
|
|
6379
|
-
}
|
|
6380
|
-
if (task.assignee && task.assignee.length > 0) {
|
|
6381
|
-
lines.push(`assignee: [${task.assignee.join(", ")}]`);
|
|
6382
|
-
}
|
|
6383
|
-
if (task.reporter) {
|
|
6384
|
-
lines.push(`reporter: ${task.reporter}`);
|
|
6385
|
-
}
|
|
6386
|
-
if (task.labels && task.labels.length > 0) {
|
|
6387
|
-
lines.push(`labels: [${task.labels.join(", ")}]`);
|
|
6388
|
-
}
|
|
6389
|
-
if (task.milestone) {
|
|
6390
|
-
lines.push(`milestone: ${task.milestone}`);
|
|
6391
|
-
}
|
|
6392
|
-
if (task.dependencies && task.dependencies.length > 0) {
|
|
6393
|
-
lines.push(`dependencies: [${task.dependencies.join(", ")}]`);
|
|
6394
|
-
}
|
|
6395
|
-
if (task.references && task.references.length > 0) {
|
|
6396
|
-
lines.push(`references: [${task.references.join(", ")}]`);
|
|
6397
|
-
}
|
|
6398
|
-
if (task.parentTaskId) {
|
|
6399
|
-
lines.push(`parentTaskId: ${task.parentTaskId}`);
|
|
6400
|
-
}
|
|
6401
|
-
if (task.subtasks && task.subtasks.length > 0) {
|
|
6402
|
-
lines.push(`subtasks: [${task.subtasks.join(", ")}]`);
|
|
6403
|
-
}
|
|
6404
|
-
if (task.branch) {
|
|
6405
|
-
lines.push(`branch: ${task.branch}`);
|
|
6406
|
-
}
|
|
6407
|
-
if (task.ordinal !== void 0) {
|
|
6408
|
-
lines.push(`ordinal: ${task.ordinal}`);
|
|
6409
|
-
}
|
|
6410
|
-
if (task.createdDate) {
|
|
6411
|
-
lines.push(`createdDate: ${task.createdDate}`);
|
|
6412
|
-
}
|
|
6413
|
-
if (task.updatedDate) {
|
|
6414
|
-
lines.push(`updatedDate: ${task.updatedDate}`);
|
|
6415
|
-
}
|
|
6416
|
-
lines.push("---");
|
|
6417
|
-
lines.push("");
|
|
6418
|
-
lines.push(`# ${task.title}`);
|
|
6419
|
-
lines.push("");
|
|
6420
|
-
if (task.description) {
|
|
6421
|
-
lines.push(task.description);
|
|
6422
|
-
lines.push("");
|
|
6423
|
-
}
|
|
6424
|
-
if (task.acceptanceCriteriaItems && task.acceptanceCriteriaItems.length > 0) {
|
|
6425
|
-
lines.push("## Acceptance Criteria");
|
|
6426
|
-
lines.push("");
|
|
6427
|
-
for (const criterion of task.acceptanceCriteriaItems) {
|
|
6428
|
-
const checkbox = criterion.checked ? "[x]" : "[ ]";
|
|
6429
|
-
lines.push(`- ${checkbox} ${criterion.text}`);
|
|
6430
|
-
}
|
|
6431
|
-
lines.push("");
|
|
6432
|
-
}
|
|
6433
|
-
if (task.implementationPlan) {
|
|
6434
|
-
lines.push("## Implementation Plan");
|
|
6435
|
-
lines.push("");
|
|
6436
|
-
lines.push(task.implementationPlan);
|
|
6437
|
-
lines.push("");
|
|
6438
|
-
}
|
|
6439
|
-
if (task.implementationNotes) {
|
|
6440
|
-
lines.push("## Implementation Notes");
|
|
6441
|
-
lines.push("");
|
|
6442
|
-
lines.push(task.implementationNotes);
|
|
6443
|
-
lines.push("");
|
|
6444
|
-
}
|
|
6445
|
-
return lines.join("\n");
|
|
6446
|
-
}
|
|
6447
|
-
function parseMarkdownContent(content2) {
|
|
6448
|
-
let frontmatter = {};
|
|
6449
|
-
let remaining = content2;
|
|
6450
|
-
const frontmatterMatch = content2.match(/^---\n([\s\S]*?)\n---\n/);
|
|
6451
|
-
if (frontmatterMatch) {
|
|
6452
|
-
frontmatter = parseFrontmatter(frontmatterMatch[1]);
|
|
6453
|
-
remaining = content2.slice(frontmatterMatch[0].length);
|
|
6454
|
-
}
|
|
6455
|
-
let title = "";
|
|
6456
|
-
const titleMatch = remaining.match(/^#\s+(.+)$/m);
|
|
6457
|
-
if (titleMatch) {
|
|
6458
|
-
title = titleMatch[1].trim();
|
|
6459
|
-
}
|
|
6460
|
-
const acceptanceCriteria = parseAcceptanceCriteria(remaining);
|
|
6461
|
-
const rawContent = remaining.trim();
|
|
6462
|
-
const description = extractDescription(remaining, title);
|
|
6463
|
-
return { frontmatter, title, rawContent, description, acceptanceCriteria };
|
|
6464
|
-
}
|
|
6465
|
-
function parseFrontmatter(raw2) {
|
|
6466
|
-
const result = {};
|
|
6467
|
-
for (const line of raw2.split("\n")) {
|
|
6468
|
-
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
6469
|
-
if (!match)
|
|
6470
|
-
continue;
|
|
6471
|
-
const [, key2, value] = match;
|
|
6472
|
-
let cleanValue = value.trim();
|
|
6473
|
-
if (cleanValue.startsWith('"') && cleanValue.endsWith('"') || cleanValue.startsWith("'") && cleanValue.endsWith("'")) {
|
|
6474
|
-
cleanValue = cleanValue.slice(1, -1);
|
|
6475
|
-
}
|
|
6476
|
-
switch (key2) {
|
|
6477
|
-
case "id":
|
|
6478
|
-
case "title":
|
|
6479
|
-
result[key2] = cleanValue;
|
|
6480
|
-
break;
|
|
6481
|
-
case "status":
|
|
6482
|
-
result.status = cleanValue;
|
|
6483
|
-
break;
|
|
6484
|
-
case "priority":
|
|
6485
|
-
result.priority = cleanValue;
|
|
6486
|
-
break;
|
|
6487
|
-
case "ordinal":
|
|
6488
|
-
result.ordinal = parseInt(cleanValue, 10);
|
|
6489
|
-
break;
|
|
6490
|
-
case "reporter":
|
|
6491
|
-
case "milestone":
|
|
6492
|
-
case "parentTaskId":
|
|
6493
|
-
case "branch":
|
|
6494
|
-
case "createdDate":
|
|
6495
|
-
case "updatedDate":
|
|
6496
|
-
result[key2] = cleanValue;
|
|
6497
|
-
break;
|
|
6498
|
-
case "assignee":
|
|
6499
|
-
case "labels":
|
|
6500
|
-
case "dependencies":
|
|
6501
|
-
case "references":
|
|
6502
|
-
case "subtasks":
|
|
6503
|
-
result[key2] = parseArrayValue(value);
|
|
6504
|
-
break;
|
|
6505
|
-
}
|
|
6506
|
-
}
|
|
6507
|
-
return result;
|
|
6508
|
-
}
|
|
6509
|
-
function parseArrayValue(value) {
|
|
6510
|
-
const cleaned = value.replace(/^\[|\]$/g, "").trim();
|
|
6511
|
-
if (!cleaned)
|
|
6512
|
-
return [];
|
|
6513
|
-
return cleaned.split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
6514
|
-
}
|
|
6515
|
-
function parseAcceptanceCriteria(content2) {
|
|
6516
|
-
const criteria = [];
|
|
6517
|
-
const checkboxPattern = /^-\s*\[([ xX])\]\s*(.+)$/gm;
|
|
6518
|
-
const matches = content2.matchAll(checkboxPattern);
|
|
6519
|
-
let index2 = 1;
|
|
6520
|
-
for (const match of matches) {
|
|
6521
|
-
criteria.push({
|
|
6522
|
-
index: index2++,
|
|
6523
|
-
checked: match[1].toLowerCase() === "x",
|
|
6524
|
-
text: match[2].trim()
|
|
6525
|
-
});
|
|
6526
|
-
}
|
|
6527
|
-
return criteria;
|
|
6528
|
-
}
|
|
6529
|
-
function extractDescription(content2, title) {
|
|
6530
|
-
const descriptionSection = extractSection(content2, "Description");
|
|
6531
|
-
if (descriptionSection) {
|
|
6532
|
-
return stripHtmlComments(descriptionSection).trim();
|
|
6533
|
-
}
|
|
6534
|
-
let body = content2;
|
|
6535
|
-
if (title) {
|
|
6536
|
-
body = body.replace(new RegExp(`^#\\s+${escapeRegex(title)}\\s*\\n?`, "m"), "");
|
|
6537
|
-
}
|
|
6538
|
-
const firstSectionMatch = body.match(/^##\s+/m);
|
|
6539
|
-
if (firstSectionMatch && firstSectionMatch.index !== void 0) {
|
|
6540
|
-
body = body.slice(0, firstSectionMatch.index);
|
|
6541
|
-
}
|
|
6542
|
-
body = stripHtmlComments(body);
|
|
6543
|
-
return body.trim();
|
|
6544
|
-
}
|
|
6545
|
-
function stripHtmlComments(content2) {
|
|
6546
|
-
return content2.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
6547
|
-
}
|
|
6548
|
-
function extractIdFromPath(filePath) {
|
|
6549
|
-
const filename = filePath.split("/").pop() || "";
|
|
6550
|
-
const match = filename.match(/^(?:task-)?(\d+(?:\.\d+)?)\s*-/);
|
|
6551
|
-
if (match) {
|
|
6552
|
-
return match[1];
|
|
6553
|
-
}
|
|
6554
|
-
return filename.replace(/\.md$/, "");
|
|
6555
|
-
}
|
|
6556
|
-
function extractTitleFromPath(filePath) {
|
|
6557
|
-
const filename = filePath.split("/").pop() || "";
|
|
6558
|
-
const match = filename.match(/^(?:task-)?\d+(?:\.\d+)?\s*-\s*(.+)\.md$/);
|
|
6559
|
-
if (match) {
|
|
6560
|
-
return match[1].trim();
|
|
6561
|
-
}
|
|
6562
|
-
return filename.replace(/\.md$/, "");
|
|
6563
|
-
}
|
|
6564
|
-
function extractSourceFromPath(filePath) {
|
|
6565
|
-
if (filePath.includes("/completed/") || filePath.includes("\\completed\\")) {
|
|
6566
|
-
return "completed";
|
|
6567
|
-
}
|
|
6568
|
-
return "tasks";
|
|
6569
|
-
}
|
|
6570
|
-
function extractTaskIndexFromPath(filePath) {
|
|
6571
|
-
return {
|
|
6572
|
-
id: extractIdFromPath(filePath),
|
|
6573
|
-
filePath,
|
|
6574
|
-
title: extractTitleFromPath(filePath),
|
|
6575
|
-
source: extractSourceFromPath(filePath)
|
|
6576
|
-
};
|
|
6577
|
-
}
|
|
6578
|
-
function escapeRegex(str) {
|
|
6579
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6580
|
-
}
|
|
6581
|
-
function extractSection(content2, sectionTitle) {
|
|
6582
|
-
var _a;
|
|
6583
|
-
const src = content2.replace(/\r\n/g, "\n");
|
|
6584
|
-
const regex2 = new RegExp(`## ${sectionTitle}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`, "i");
|
|
6585
|
-
const match = src.match(regex2);
|
|
6586
|
-
return (_a = match == null ? void 0 : match[1]) == null ? void 0 : _a.trim();
|
|
6587
|
-
}
|
|
6588
|
-
function parseMilestoneFrontmatter(raw2) {
|
|
6589
|
-
const result = {};
|
|
6590
|
-
for (const line of raw2.split("\n")) {
|
|
6591
|
-
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
6592
|
-
if (!match)
|
|
6593
|
-
continue;
|
|
6594
|
-
const [, key2, value] = match;
|
|
6595
|
-
let cleanValue = value.trim();
|
|
6596
|
-
if (cleanValue.startsWith('"') && cleanValue.endsWith('"') || cleanValue.startsWith("'") && cleanValue.endsWith("'")) {
|
|
6597
|
-
cleanValue = cleanValue.slice(1, -1);
|
|
6598
|
-
}
|
|
6599
|
-
switch (key2) {
|
|
6600
|
-
case "id":
|
|
6601
|
-
case "title":
|
|
6602
|
-
result[key2] = cleanValue;
|
|
6603
|
-
break;
|
|
6604
|
-
case "tasks":
|
|
6605
|
-
if (cleanValue.startsWith("[") && cleanValue.endsWith("]")) {
|
|
6606
|
-
result.tasks = cleanValue.slice(1, -1).split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
6607
|
-
}
|
|
6608
|
-
break;
|
|
6609
|
-
}
|
|
6610
|
-
}
|
|
6611
|
-
return result;
|
|
6612
|
-
}
|
|
6613
|
-
function parseMilestoneMarkdown(content2) {
|
|
6614
|
-
let frontmatter = {};
|
|
6615
|
-
let remaining = content2;
|
|
6616
|
-
const frontmatterMatch = content2.match(/^---\n([\s\S]*?)\n---\n/);
|
|
6617
|
-
if (frontmatterMatch) {
|
|
6618
|
-
frontmatter = parseMilestoneFrontmatter(frontmatterMatch[1]);
|
|
6619
|
-
remaining = content2.slice(frontmatterMatch[0].length);
|
|
6620
|
-
}
|
|
6621
|
-
const rawContent = remaining.trim();
|
|
6622
|
-
const description = extractSection(rawContent, "Description") || "";
|
|
6623
|
-
return {
|
|
6624
|
-
id: frontmatter.id || "",
|
|
6625
|
-
title: frontmatter.title || "",
|
|
6626
|
-
description,
|
|
6627
|
-
rawContent,
|
|
6628
|
-
tasks: frontmatter.tasks || []
|
|
6629
|
-
};
|
|
6630
|
-
}
|
|
6631
|
-
function serializeMilestoneMarkdown(milestone) {
|
|
6632
|
-
const lines = [];
|
|
6633
|
-
lines.push("---");
|
|
6634
|
-
lines.push(`id: ${milestone.id}`);
|
|
6635
|
-
const escapedTitle = milestone.title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
6636
|
-
lines.push(`title: "${escapedTitle}"`);
|
|
6637
|
-
lines.push(`tasks: [${milestone.tasks.join(", ")}]`);
|
|
6638
|
-
lines.push("---");
|
|
6639
|
-
lines.push("");
|
|
6640
|
-
lines.push("## Description");
|
|
6641
|
-
lines.push("");
|
|
6642
|
-
lines.push(milestone.description || `Milestone: ${milestone.title}`);
|
|
6643
|
-
lines.push("");
|
|
6644
|
-
return lines.join("\n");
|
|
6645
|
-
}
|
|
6646
|
-
function getMilestoneFilename(id, title) {
|
|
6647
|
-
const safeTitle = title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, "-").toLowerCase().slice(0, 50);
|
|
6648
|
-
return `${id} - ${safeTitle}.md`;
|
|
6649
|
-
}
|
|
6650
|
-
function extractMilestoneIdFromFilename(filename) {
|
|
6651
|
-
const match = filename.match(/^(m-\d+)/);
|
|
6652
|
-
return match ? match[1] : null;
|
|
6653
|
-
}
|
|
6654
|
-
const version$1 = "0.3.13";
|
|
6655
|
-
const packageJson$1 = {
|
|
6656
|
-
version: version$1
|
|
6657
|
-
};
|
|
6658
|
-
const TRACER_NAME$1 = "@backlog-md/core";
|
|
6659
|
-
const TRACER_VERSION$1 = packageJson$1.version;
|
|
6660
|
-
function getTracer$1() {
|
|
6661
|
-
return trace.getTracer(TRACER_NAME$1, TRACER_VERSION$1);
|
|
6662
|
-
}
|
|
6663
|
-
const NO_MILESTONE_KEY = "__none";
|
|
6664
|
-
function normalizeMilestoneName(name2) {
|
|
6665
|
-
return name2.trim();
|
|
6666
|
-
}
|
|
6667
|
-
function milestoneKey(name2) {
|
|
6668
|
-
return normalizeMilestoneName(name2 ?? "").toLowerCase();
|
|
6669
|
-
}
|
|
6670
|
-
function isDoneStatus(status) {
|
|
6671
|
-
const normalized = (status ?? "").toLowerCase();
|
|
6672
|
-
return normalized.includes("done") || normalized.includes("complete");
|
|
6673
|
-
}
|
|
6674
|
-
function getMilestoneLabel(milestoneId, milestoneEntities) {
|
|
6675
|
-
if (!milestoneId) {
|
|
6676
|
-
return "Tasks without milestone";
|
|
6677
|
-
}
|
|
6678
|
-
const entity = milestoneEntities.find((m) => milestoneKey(m.id) === milestoneKey(milestoneId));
|
|
6679
|
-
return (entity == null ? void 0 : entity.title) || milestoneId;
|
|
6680
|
-
}
|
|
6681
|
-
function collectMilestoneIds(tasks, milestoneEntities) {
|
|
6682
|
-
const merged = [];
|
|
6683
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6684
|
-
const addMilestone = (value) => {
|
|
6685
|
-
const normalized = normalizeMilestoneName(value);
|
|
6686
|
-
if (!normalized)
|
|
6687
|
-
return;
|
|
6688
|
-
const key2 = milestoneKey(normalized);
|
|
6689
|
-
if (seen.has(key2))
|
|
6690
|
-
return;
|
|
6691
|
-
seen.add(key2);
|
|
6692
|
-
merged.push(normalized);
|
|
6693
|
-
};
|
|
6694
|
-
for (const entity of milestoneEntities) {
|
|
6695
|
-
addMilestone(entity.id);
|
|
6696
|
-
}
|
|
6697
|
-
for (const task of tasks) {
|
|
6698
|
-
addMilestone(task.milestone ?? "");
|
|
6699
|
-
}
|
|
6700
|
-
return merged;
|
|
6701
|
-
}
|
|
6702
|
-
function collectMilestones(tasks, configMilestones) {
|
|
6703
|
-
const merged = [];
|
|
6704
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6705
|
-
const addMilestone = (value) => {
|
|
6706
|
-
const normalized = normalizeMilestoneName(value);
|
|
6707
|
-
if (!normalized)
|
|
6708
|
-
return;
|
|
6709
|
-
const key2 = milestoneKey(normalized);
|
|
6710
|
-
if (seen.has(key2))
|
|
6711
|
-
return;
|
|
6712
|
-
seen.add(key2);
|
|
6713
|
-
merged.push(normalized);
|
|
6714
|
-
};
|
|
6715
|
-
for (const m of configMilestones) {
|
|
6716
|
-
addMilestone(m);
|
|
6717
|
-
}
|
|
6718
|
-
for (const task of tasks) {
|
|
6719
|
-
addMilestone(task.milestone ?? "");
|
|
6720
|
-
}
|
|
6721
|
-
return merged;
|
|
6722
|
-
}
|
|
6723
|
-
function createBucket(milestoneId, tasks, statuses, milestoneEntities, isNoMilestone) {
|
|
6724
|
-
const bucketMilestoneKey = milestoneKey(milestoneId);
|
|
6725
|
-
const bucketTasks = tasks.filter((task) => {
|
|
6726
|
-
const taskMilestoneKey = milestoneKey(task.milestone);
|
|
6727
|
-
return bucketMilestoneKey ? taskMilestoneKey === bucketMilestoneKey : !taskMilestoneKey;
|
|
6728
|
-
});
|
|
6729
|
-
const counts = {};
|
|
6730
|
-
for (const status of statuses) {
|
|
6731
|
-
counts[status] = 0;
|
|
6732
|
-
}
|
|
6733
|
-
for (const task of bucketTasks) {
|
|
6734
|
-
const status = task.status ?? "";
|
|
6735
|
-
counts[status] = (counts[status] ?? 0) + 1;
|
|
6736
|
-
}
|
|
6737
|
-
const doneCount = bucketTasks.filter((t) => isDoneStatus(t.status)).length;
|
|
6738
|
-
const progress = bucketTasks.length > 0 ? Math.round(doneCount / bucketTasks.length * 100) : 0;
|
|
6739
|
-
const key2 = bucketMilestoneKey ? bucketMilestoneKey : NO_MILESTONE_KEY;
|
|
6740
|
-
const label = getMilestoneLabel(milestoneId, milestoneEntities);
|
|
6741
|
-
return {
|
|
6742
|
-
key: key2,
|
|
6743
|
-
label,
|
|
6744
|
-
milestone: milestoneId,
|
|
6745
|
-
isNoMilestone,
|
|
6746
|
-
tasks: bucketTasks,
|
|
6747
|
-
statusCounts: counts,
|
|
6748
|
-
total: bucketTasks.length,
|
|
6749
|
-
doneCount,
|
|
6750
|
-
progress
|
|
6751
|
-
};
|
|
6752
|
-
}
|
|
6753
|
-
function buildMilestoneBuckets(tasks, milestoneEntities, statuses) {
|
|
6754
|
-
const allMilestoneIds = collectMilestoneIds(tasks, milestoneEntities);
|
|
6755
|
-
const buckets = [
|
|
6756
|
-
// "No milestone" bucket first
|
|
6757
|
-
createBucket(void 0, tasks, statuses, milestoneEntities, true),
|
|
6758
|
-
// Then each milestone bucket
|
|
6759
|
-
...allMilestoneIds.map((m) => createBucket(m, tasks, statuses, milestoneEntities, false))
|
|
6760
|
-
];
|
|
6761
|
-
return buckets;
|
|
6762
|
-
}
|
|
6763
|
-
function buildMilestoneBucketsFromConfig(tasks, configMilestones, statuses) {
|
|
6764
|
-
const milestoneEntities = configMilestones.map((id) => ({
|
|
6765
|
-
id,
|
|
6766
|
-
title: id,
|
|
6767
|
-
description: "",
|
|
6768
|
-
rawContent: "",
|
|
6769
|
-
tasks: []
|
|
6770
|
-
}));
|
|
6771
|
-
return buildMilestoneBuckets(tasks, milestoneEntities, statuses);
|
|
6772
|
-
}
|
|
6773
|
-
function groupTasksByMilestone(tasks, configMilestones, statuses) {
|
|
6774
|
-
const milestones = collectMilestones(tasks, configMilestones);
|
|
6775
|
-
const buckets = buildMilestoneBucketsFromConfig(tasks, configMilestones, statuses);
|
|
6776
|
-
return {
|
|
6777
|
-
milestones,
|
|
6778
|
-
buckets
|
|
6779
|
-
};
|
|
6780
|
-
}
|
|
6781
|
-
const PRIORITY_ORDER = {
|
|
6782
|
-
high: 0,
|
|
6783
|
-
medium: 1,
|
|
6784
|
-
low: 2
|
|
6785
|
-
};
|
|
6786
|
-
function sortTasksByTitle(tasks, direction = "asc") {
|
|
6787
|
-
return [...tasks].sort((a, b) => {
|
|
6788
|
-
const cmp = a.title.localeCompare(b.title);
|
|
6789
|
-
return direction === "asc" ? cmp : -cmp;
|
|
6790
|
-
});
|
|
6791
|
-
}
|
|
6792
|
-
function sortTasksBy(tasks, sortBy = "title", direction = "asc") {
|
|
6793
|
-
switch (sortBy) {
|
|
6794
|
-
case "title":
|
|
6795
|
-
return sortTasksByTitle(tasks, direction);
|
|
6796
|
-
case "createdDate":
|
|
6797
|
-
return [...tasks].sort((a, b) => {
|
|
6798
|
-
const cmp = a.createdDate.localeCompare(b.createdDate);
|
|
6799
|
-
return direction === "asc" ? cmp : -cmp;
|
|
6800
|
-
});
|
|
6801
|
-
default: {
|
|
6802
|
-
const sorted = sortTasks(tasks);
|
|
6803
|
-
return direction === "desc" ? sorted.reverse() : sorted;
|
|
6804
|
-
}
|
|
6805
|
-
}
|
|
6806
|
-
}
|
|
6807
|
-
function sortTasks(tasks) {
|
|
6808
|
-
return [...tasks].sort((a, b) => {
|
|
6809
|
-
if (a.ordinal !== void 0 && b.ordinal !== void 0) {
|
|
6810
|
-
return a.ordinal - b.ordinal;
|
|
6811
|
-
}
|
|
6812
|
-
if (a.ordinal !== void 0)
|
|
6813
|
-
return -1;
|
|
6814
|
-
if (b.ordinal !== void 0)
|
|
6815
|
-
return 1;
|
|
6816
|
-
const aPri = a.priority ? PRIORITY_ORDER[a.priority] : 3;
|
|
6817
|
-
const bPri = b.priority ? PRIORITY_ORDER[b.priority] : 3;
|
|
6818
|
-
if (aPri !== bPri)
|
|
6819
|
-
return aPri - bPri;
|
|
6820
|
-
return b.createdDate.localeCompare(a.createdDate);
|
|
6821
|
-
});
|
|
6822
|
-
}
|
|
6823
|
-
function groupTasksByStatus(tasks, statuses) {
|
|
6824
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
6825
|
-
for (const status of statuses) {
|
|
6826
|
-
grouped.set(status, []);
|
|
6827
|
-
}
|
|
6828
|
-
for (const task of tasks) {
|
|
6829
|
-
const list2 = grouped.get(task.status);
|
|
6830
|
-
if (list2) {
|
|
6831
|
-
list2.push(task);
|
|
6832
|
-
} else {
|
|
6833
|
-
grouped.set(task.status, [task]);
|
|
6834
|
-
}
|
|
6835
|
-
}
|
|
6836
|
-
for (const [status, statusTasks] of grouped) {
|
|
6837
|
-
grouped.set(status, sortTasks(statusTasks));
|
|
6838
|
-
}
|
|
6839
|
-
return grouped;
|
|
6840
|
-
}
|
|
6841
|
-
const DEFAULT_STATUSES = ["To Do", "In Progress", "Done"];
|
|
6842
|
-
function parseBacklogConfig(content2) {
|
|
6843
|
-
const config = {};
|
|
6844
|
-
const lines = content2.split("\n");
|
|
6845
|
-
for (const line of lines) {
|
|
6846
|
-
const trimmed = line.trim();
|
|
6847
|
-
if (!trimmed || trimmed.startsWith("#"))
|
|
6848
|
-
continue;
|
|
6849
|
-
const colonIndex = trimmed.indexOf(":");
|
|
6850
|
-
if (colonIndex === -1)
|
|
6851
|
-
continue;
|
|
6852
|
-
const key2 = trimmed.substring(0, colonIndex).trim();
|
|
6853
|
-
const value = trimmed.substring(colonIndex + 1).trim();
|
|
6854
|
-
switch (key2) {
|
|
6855
|
-
case "project_name":
|
|
6856
|
-
config.projectName = value.replace(/['"]/g, "");
|
|
6857
|
-
break;
|
|
6858
|
-
case "default_assignee":
|
|
6859
|
-
config.defaultAssignee = value.replace(/['"]/g, "");
|
|
6860
|
-
break;
|
|
6861
|
-
case "default_reporter":
|
|
6862
|
-
config.defaultReporter = value.replace(/['"]/g, "");
|
|
6863
|
-
break;
|
|
6864
|
-
case "default_status":
|
|
6865
|
-
config.defaultStatus = value.replace(/['"]/g, "");
|
|
6866
|
-
break;
|
|
6867
|
-
case "statuses":
|
|
6868
|
-
case "labels":
|
|
6869
|
-
case "milestones":
|
|
6870
|
-
if (value.startsWith("[") && value.endsWith("]")) {
|
|
6871
|
-
const arrayContent = value.slice(1, -1);
|
|
6872
|
-
config[key2] = arrayContent.split(",").map((item) => item.trim().replace(/['"]/g, "")).filter(Boolean);
|
|
6873
|
-
}
|
|
6874
|
-
break;
|
|
6875
|
-
case "date_format":
|
|
6876
|
-
config.dateFormat = value.replace(/['"]/g, "");
|
|
6877
|
-
break;
|
|
6878
|
-
case "max_column_width":
|
|
6879
|
-
config.maxColumnWidth = parseInt(value, 10);
|
|
6880
|
-
break;
|
|
6881
|
-
case "task_resolution_strategy":
|
|
6882
|
-
config.taskResolutionStrategy = value.replace(/['"]/g, "");
|
|
6883
|
-
break;
|
|
6884
|
-
case "default_editor":
|
|
6885
|
-
config.defaultEditor = value.replace(/["']/g, "");
|
|
6886
|
-
break;
|
|
6887
|
-
case "auto_open_browser":
|
|
6888
|
-
config.autoOpenBrowser = value.toLowerCase() === "true";
|
|
6889
|
-
break;
|
|
6890
|
-
case "default_port":
|
|
6891
|
-
config.defaultPort = parseInt(value, 10);
|
|
6892
|
-
break;
|
|
6893
|
-
case "remote_operations":
|
|
6894
|
-
config.remoteOperations = value.toLowerCase() === "true";
|
|
6895
|
-
break;
|
|
6896
|
-
case "auto_commit":
|
|
6897
|
-
config.autoCommit = value.toLowerCase() === "true";
|
|
6898
|
-
break;
|
|
6899
|
-
case "zero_padded_ids":
|
|
6900
|
-
config.zeroPaddedIds = parseInt(value, 10);
|
|
6901
|
-
break;
|
|
6902
|
-
case "timezone_preference":
|
|
6903
|
-
config.timezonePreference = value.replace(/['"]/g, "");
|
|
6904
|
-
break;
|
|
6905
|
-
case "include_date_time_in_dates":
|
|
6906
|
-
config.includeDateTimeInDates = value.toLowerCase() === "true";
|
|
6907
|
-
break;
|
|
6908
|
-
case "bypass_git_hooks":
|
|
6909
|
-
config.bypassGitHooks = value.toLowerCase() === "true";
|
|
6910
|
-
break;
|
|
6911
|
-
case "check_active_branches":
|
|
6912
|
-
config.checkActiveBranches = value.toLowerCase() === "true";
|
|
6913
|
-
break;
|
|
6914
|
-
case "active_branch_days":
|
|
6915
|
-
config.activeBranchDays = parseInt(value, 10);
|
|
6916
|
-
break;
|
|
6917
|
-
}
|
|
6918
|
-
}
|
|
6919
|
-
return {
|
|
6920
|
-
projectName: config.projectName || "Backlog",
|
|
6921
|
-
statuses: config.statuses || [...DEFAULT_STATUSES],
|
|
6922
|
-
labels: config.labels || [],
|
|
6923
|
-
milestones: config.milestones || [],
|
|
6924
|
-
defaultStatus: config.defaultStatus || DEFAULT_STATUSES[0],
|
|
6925
|
-
dateFormat: config.dateFormat || "YYYY-MM-DD",
|
|
6926
|
-
defaultAssignee: config.defaultAssignee,
|
|
6927
|
-
defaultReporter: config.defaultReporter,
|
|
6928
|
-
maxColumnWidth: config.maxColumnWidth,
|
|
6929
|
-
taskResolutionStrategy: config.taskResolutionStrategy,
|
|
6930
|
-
defaultEditor: config.defaultEditor,
|
|
6931
|
-
autoOpenBrowser: config.autoOpenBrowser,
|
|
6932
|
-
defaultPort: config.defaultPort,
|
|
6933
|
-
remoteOperations: config.remoteOperations,
|
|
6934
|
-
autoCommit: config.autoCommit,
|
|
6935
|
-
zeroPaddedIds: config.zeroPaddedIds,
|
|
6936
|
-
timezonePreference: config.timezonePreference,
|
|
6937
|
-
includeDateTimeInDates: config.includeDateTimeInDates,
|
|
6938
|
-
bypassGitHooks: config.bypassGitHooks,
|
|
6939
|
-
checkActiveBranches: config.checkActiveBranches,
|
|
6940
|
-
activeBranchDays: config.activeBranchDays
|
|
6941
|
-
};
|
|
6942
|
-
}
|
|
6943
|
-
function serializeBacklogConfig(config) {
|
|
6944
|
-
const lines = [];
|
|
6945
|
-
lines.push(`project_name: "${config.projectName}"`);
|
|
6946
|
-
if (config.defaultStatus) {
|
|
6947
|
-
lines.push(`default_status: "${config.defaultStatus}"`);
|
|
6948
|
-
}
|
|
6949
|
-
lines.push(`statuses: [${config.statuses.map((s2) => `"${s2}"`).join(", ")}]`);
|
|
6950
|
-
lines.push(`labels: [${(config.labels || []).map((l) => `"${l}"`).join(", ")}]`);
|
|
6951
|
-
lines.push(`milestones: [${(config.milestones || []).map((m) => `"${m}"`).join(", ")}]`);
|
|
6952
|
-
if (config.dateFormat) {
|
|
6953
|
-
lines.push(`date_format: "${config.dateFormat}"`);
|
|
6954
|
-
}
|
|
6955
|
-
if (config.defaultAssignee) {
|
|
6956
|
-
lines.push(`default_assignee: "${config.defaultAssignee}"`);
|
|
6957
|
-
}
|
|
6958
|
-
if (config.defaultReporter) {
|
|
6959
|
-
lines.push(`default_reporter: "${config.defaultReporter}"`);
|
|
6960
|
-
}
|
|
6961
|
-
if (config.defaultEditor) {
|
|
6962
|
-
lines.push(`default_editor: "${config.defaultEditor}"`);
|
|
6963
|
-
}
|
|
6964
|
-
if (typeof config.autoCommit === "boolean") {
|
|
6965
|
-
lines.push(`auto_commit: ${config.autoCommit}`);
|
|
6966
|
-
}
|
|
6967
|
-
if (typeof config.zeroPaddedIds === "number") {
|
|
6968
|
-
lines.push(`zero_padded_ids: ${config.zeroPaddedIds}`);
|
|
6969
|
-
}
|
|
6970
|
-
if (typeof config.autoOpenBrowser === "boolean") {
|
|
6971
|
-
lines.push(`auto_open_browser: ${config.autoOpenBrowser}`);
|
|
6972
|
-
}
|
|
6973
|
-
if (typeof config.defaultPort === "number") {
|
|
6974
|
-
lines.push(`default_port: ${config.defaultPort}`);
|
|
6975
|
-
}
|
|
6976
|
-
if (typeof config.remoteOperations === "boolean") {
|
|
6977
|
-
lines.push(`remote_operations: ${config.remoteOperations}`);
|
|
6978
|
-
}
|
|
6979
|
-
if (typeof config.bypassGitHooks === "boolean") {
|
|
6980
|
-
lines.push(`bypass_git_hooks: ${config.bypassGitHooks}`);
|
|
6981
|
-
}
|
|
6982
|
-
if (typeof config.checkActiveBranches === "boolean") {
|
|
6983
|
-
lines.push(`check_active_branches: ${config.checkActiveBranches}`);
|
|
6984
|
-
}
|
|
6985
|
-
if (typeof config.activeBranchDays === "number") {
|
|
6986
|
-
lines.push(`active_branch_days: ${config.activeBranchDays}`);
|
|
6987
|
-
}
|
|
6988
|
-
return `${lines.join("\n")}
|
|
6989
|
-
`;
|
|
6990
|
-
}
|
|
6991
|
-
class Core {
|
|
6992
|
-
constructor(options) {
|
|
6993
|
-
__publicField(this, "projectRoot");
|
|
6994
|
-
__publicField(this, "fs");
|
|
6995
|
-
__publicField(this, "tracer");
|
|
6996
|
-
__publicField(this, "config", null);
|
|
6997
|
-
__publicField(this, "tasks", /* @__PURE__ */ new Map());
|
|
6998
|
-
__publicField(this, "initialized", false);
|
|
6999
|
-
/** Lightweight task index for lazy loading (no file reads) */
|
|
7000
|
-
__publicField(this, "taskIndex", /* @__PURE__ */ new Map());
|
|
7001
|
-
__publicField(this, "lazyInitialized", false);
|
|
7002
|
-
this.projectRoot = options.projectRoot;
|
|
7003
|
-
this.fs = options.adapters.fs;
|
|
7004
|
-
this.tracer = getTracer$1();
|
|
7005
|
-
}
|
|
7006
|
-
/**
|
|
7007
|
-
* Check if projectRoot contains a valid Backlog.md project
|
|
7008
|
-
*/
|
|
7009
|
-
async isBacklogProject() {
|
|
7010
|
-
const configPath = this.fs.join(this.projectRoot, "backlog", "config.yml");
|
|
7011
|
-
return this.fs.exists(configPath);
|
|
7012
|
-
}
|
|
7013
|
-
/**
|
|
7014
|
-
* Initialize a new Backlog.md project in the projectRoot directory
|
|
7015
|
-
*
|
|
7016
|
-
* Creates the backlog/ directory and config.yml file.
|
|
7017
|
-
* Task directories (tasks/, completed/) are created lazily when needed.
|
|
7018
|
-
*
|
|
7019
|
-
* @param options - Optional configuration for the new project
|
|
7020
|
-
* @throws Error if project already exists
|
|
7021
|
-
*
|
|
7022
|
-
* @example
|
|
7023
|
-
* ```typescript
|
|
7024
|
-
* const core = new Core({ projectRoot: '/path/to/project', adapters: { fs } });
|
|
7025
|
-
*
|
|
7026
|
-
* // Initialize with defaults
|
|
7027
|
-
* await core.initProject();
|
|
7028
|
-
*
|
|
7029
|
-
* // Or with custom options
|
|
7030
|
-
* await core.initProject({
|
|
7031
|
-
* projectName: 'My Project',
|
|
7032
|
-
* statuses: ['Backlog', 'In Progress', 'Review', 'Done'],
|
|
7033
|
-
* labels: ['bug', 'feature', 'docs']
|
|
7034
|
-
* });
|
|
7035
|
-
* ```
|
|
7036
|
-
*/
|
|
7037
|
-
async initProject(options = {}) {
|
|
7038
|
-
if (await this.isBacklogProject()) {
|
|
7039
|
-
throw new Error(`Already a Backlog.md project: config.yml exists at ${this.fs.join(this.projectRoot, "backlog", "config.yml")}`);
|
|
7040
|
-
}
|
|
7041
|
-
const dirName = this.projectRoot.split("/").pop() || "Backlog";
|
|
7042
|
-
const projectName = options.projectName || dirName;
|
|
7043
|
-
const statuses = options.statuses || [
|
|
7044
|
-
DEFAULT_TASK_STATUSES.TODO,
|
|
7045
|
-
DEFAULT_TASK_STATUSES.IN_PROGRESS,
|
|
7046
|
-
DEFAULT_TASK_STATUSES.DONE
|
|
7047
|
-
];
|
|
7048
|
-
const config = {
|
|
7049
|
-
projectName,
|
|
7050
|
-
statuses,
|
|
7051
|
-
labels: options.labels || [],
|
|
7052
|
-
milestones: [],
|
|
7053
|
-
defaultStatus: options.defaultStatus || statuses[0],
|
|
7054
|
-
dateFormat: "YYYY-MM-DD"
|
|
7055
|
-
};
|
|
7056
|
-
const backlogDir = this.fs.join(this.projectRoot, "backlog");
|
|
7057
|
-
await this.fs.createDir(backlogDir, { recursive: true });
|
|
7058
|
-
const configPath = this.fs.join(backlogDir, "config.yml");
|
|
7059
|
-
const configContent = serializeBacklogConfig(config);
|
|
7060
|
-
await this.fs.writeFile(configPath, configContent);
|
|
7061
|
-
}
|
|
7062
|
-
/**
|
|
7063
|
-
* Initialize the Core instance
|
|
7064
|
-
*
|
|
7065
|
-
* Loads configuration and discovers all tasks.
|
|
7066
|
-
* Must be called before using other methods.
|
|
7067
|
-
*/
|
|
7068
|
-
async initialize() {
|
|
7069
|
-
if (this.initialized)
|
|
7070
|
-
return;
|
|
7071
|
-
return this.tracer.startActiveSpan("core.init", {
|
|
7072
|
-
attributes: {
|
|
7073
|
-
projectRoot: this.projectRoot,
|
|
7074
|
-
mode: "full"
|
|
7075
|
-
}
|
|
7076
|
-
}, async (span) => {
|
|
7077
|
-
var _a;
|
|
7078
|
-
const startTime = Date.now();
|
|
7079
|
-
try {
|
|
7080
|
-
span.addEvent("core.init.started", {
|
|
7081
|
-
projectRoot: this.projectRoot,
|
|
7082
|
-
mode: "full"
|
|
7083
|
-
});
|
|
7084
|
-
const configPath = this.fs.join(this.projectRoot, "backlog", "config.yml");
|
|
7085
|
-
const configExists = await this.fs.exists(configPath);
|
|
7086
|
-
if (!configExists) {
|
|
7087
|
-
const error = new Error(`Not a Backlog.md project: config.yml not found at ${configPath}`);
|
|
7088
|
-
span.addEvent("core.init.error", {
|
|
7089
|
-
"error.type": "ConfigNotFoundError",
|
|
7090
|
-
"error.message": error.message,
|
|
7091
|
-
stage: "config"
|
|
7092
|
-
});
|
|
7093
|
-
span.recordException(error);
|
|
7094
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
|
7095
|
-
throw error;
|
|
7096
|
-
}
|
|
7097
|
-
const configContent = await this.fs.readFile(configPath);
|
|
7098
|
-
this.config = parseBacklogConfig(configContent);
|
|
7099
|
-
span.addEvent("core.init.config.loaded", {
|
|
7100
|
-
projectName: this.config.projectName,
|
|
7101
|
-
statusCount: this.config.statuses.length,
|
|
7102
|
-
labelCount: ((_a = this.config.labels) == null ? void 0 : _a.length) ?? 0
|
|
7103
|
-
});
|
|
7104
|
-
let localTaskCount = 0;
|
|
7105
|
-
const tasksDir = this.fs.join(this.projectRoot, "backlog", "tasks");
|
|
7106
|
-
if (await this.fs.exists(tasksDir)) {
|
|
7107
|
-
await this.loadTasksFromDirectory(tasksDir, "local");
|
|
7108
|
-
localTaskCount = Array.from(this.tasks.values()).filter((t) => t.source === "local").length;
|
|
7109
|
-
}
|
|
7110
|
-
let completedTaskCount = 0;
|
|
7111
|
-
const completedDir = this.fs.join(this.projectRoot, "backlog", "completed");
|
|
7112
|
-
if (await this.fs.exists(completedDir)) {
|
|
7113
|
-
await this.loadTasksFromDirectory(completedDir, "completed");
|
|
7114
|
-
completedTaskCount = Array.from(this.tasks.values()).filter((t) => t.source === "completed").length;
|
|
7115
|
-
}
|
|
7116
|
-
const totalTaskCount = this.tasks.size;
|
|
7117
|
-
span.addEvent("core.init.tasks.loaded", {
|
|
7118
|
-
localTaskCount,
|
|
7119
|
-
completedTaskCount,
|
|
7120
|
-
totalTaskCount
|
|
7121
|
-
});
|
|
7122
|
-
this.initialized = true;
|
|
7123
|
-
const duration = Date.now() - startTime;
|
|
7124
|
-
span.addEvent("core.init.complete", {
|
|
7125
|
-
success: true,
|
|
7126
|
-
"duration.ms": duration,
|
|
7127
|
-
taskCount: totalTaskCount
|
|
7128
|
-
});
|
|
7129
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
7130
|
-
} catch (error) {
|
|
7131
|
-
if (error instanceof Error) {
|
|
7132
|
-
span.recordException(error);
|
|
7133
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
|
7134
|
-
}
|
|
7135
|
-
throw error;
|
|
7136
|
-
} finally {
|
|
7137
|
-
span.end();
|
|
7138
|
-
}
|
|
7139
|
-
});
|
|
7140
|
-
}
|
|
7141
|
-
/**
|
|
7142
|
-
* Initialize with lazy loading (no file content reads)
|
|
7143
|
-
*
|
|
7144
|
-
* Only loads config and builds task index from file paths.
|
|
7145
|
-
* Task content is loaded on-demand via loadTask().
|
|
7146
|
-
* Use this for web/panel contexts where file reads are expensive.
|
|
7147
|
-
*
|
|
7148
|
-
* @param filePaths - Array of all file paths in the project
|
|
7149
|
-
*/
|
|
7150
|
-
async initializeLazy(filePaths) {
|
|
7151
|
-
if (this.lazyInitialized)
|
|
7152
|
-
return;
|
|
7153
|
-
return this.tracer.startActiveSpan("core.init", {
|
|
7154
|
-
attributes: {
|
|
7155
|
-
projectRoot: this.projectRoot,
|
|
7156
|
-
mode: "lazy",
|
|
7157
|
-
"input.filePathCount": filePaths.length
|
|
7158
|
-
}
|
|
7159
|
-
}, async (span) => {
|
|
7160
|
-
var _a;
|
|
7161
|
-
const startTime = Date.now();
|
|
7162
|
-
try {
|
|
7163
|
-
span.addEvent("core.init.started", {
|
|
7164
|
-
projectRoot: this.projectRoot,
|
|
7165
|
-
mode: "lazy"
|
|
7166
|
-
});
|
|
7167
|
-
const configPath = this.fs.join(this.projectRoot, "backlog", "config.yml");
|
|
7168
|
-
const configExists = await this.fs.exists(configPath);
|
|
7169
|
-
if (!configExists) {
|
|
7170
|
-
const error = new Error(`Not a Backlog.md project: config.yml not found at ${configPath}`);
|
|
7171
|
-
span.addEvent("core.init.error", {
|
|
7172
|
-
"error.type": "ConfigNotFoundError",
|
|
7173
|
-
"error.message": error.message,
|
|
7174
|
-
stage: "config"
|
|
7175
|
-
});
|
|
7176
|
-
span.recordException(error);
|
|
7177
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
|
7178
|
-
throw error;
|
|
7179
|
-
}
|
|
7180
|
-
const configContent = await this.fs.readFile(configPath);
|
|
7181
|
-
this.config = parseBacklogConfig(configContent);
|
|
7182
|
-
span.addEvent("core.init.config.loaded", {
|
|
7183
|
-
projectName: this.config.projectName,
|
|
7184
|
-
statusCount: this.config.statuses.length,
|
|
7185
|
-
labelCount: ((_a = this.config.labels) == null ? void 0 : _a.length) ?? 0
|
|
7186
|
-
});
|
|
7187
|
-
this.taskIndex.clear();
|
|
7188
|
-
let tasksIndexed = 0;
|
|
7189
|
-
let completedIndexed = 0;
|
|
7190
|
-
for (const filePath of filePaths) {
|
|
7191
|
-
if (!filePath.endsWith(".md"))
|
|
7192
|
-
continue;
|
|
7193
|
-
const isTaskFile = filePath.includes("backlog/tasks/") || filePath.includes("backlog\\tasks\\");
|
|
7194
|
-
const isCompletedFile = filePath.includes("backlog/completed/") || filePath.includes("backlog\\completed\\");
|
|
7195
|
-
if (!isTaskFile && !isCompletedFile)
|
|
7196
|
-
continue;
|
|
7197
|
-
if (filePath.endsWith("config.yml"))
|
|
7198
|
-
continue;
|
|
7199
|
-
const indexEntry = extractTaskIndexFromPath(filePath);
|
|
7200
|
-
this.taskIndex.set(indexEntry.id, indexEntry);
|
|
7201
|
-
if (isTaskFile)
|
|
7202
|
-
tasksIndexed++;
|
|
7203
|
-
if (isCompletedFile)
|
|
7204
|
-
completedIndexed++;
|
|
7205
|
-
}
|
|
7206
|
-
this.lazyInitialized = true;
|
|
7207
|
-
const duration = Date.now() - startTime;
|
|
7208
|
-
span.addEvent("core.init.complete", {
|
|
7209
|
-
success: true,
|
|
7210
|
-
"duration.ms": duration,
|
|
7211
|
-
tasksIndexed,
|
|
7212
|
-
completedIndexed,
|
|
7213
|
-
totalIndexed: this.taskIndex.size
|
|
7214
|
-
});
|
|
7215
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
7216
|
-
} catch (error) {
|
|
7217
|
-
if (error instanceof Error) {
|
|
7218
|
-
span.recordException(error);
|
|
7219
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
|
7220
|
-
}
|
|
7221
|
-
throw error;
|
|
7222
|
-
} finally {
|
|
7223
|
-
span.end();
|
|
7224
|
-
}
|
|
7225
|
-
});
|
|
7226
|
-
}
|
|
7227
|
-
/**
|
|
7228
|
-
* Check if lazy initialization is complete
|
|
7229
|
-
*/
|
|
7230
|
-
isLazyInitialized() {
|
|
7231
|
-
return this.lazyInitialized;
|
|
7232
|
-
}
|
|
7233
|
-
/**
|
|
7234
|
-
* Get the task index (lightweight entries)
|
|
7235
|
-
*/
|
|
7236
|
-
getTaskIndex() {
|
|
7237
|
-
if (!this.lazyInitialized) {
|
|
7238
|
-
throw new Error("Core not lazy initialized. Call initializeLazy() first.");
|
|
7239
|
-
}
|
|
7240
|
-
return this.taskIndex;
|
|
7241
|
-
}
|
|
7242
|
-
/**
|
|
7243
|
-
* Load a single task by ID (on-demand loading)
|
|
7244
|
-
*
|
|
7245
|
-
* @param id - Task ID
|
|
7246
|
-
* @returns Task or undefined if not found
|
|
7247
|
-
*/
|
|
7248
|
-
async loadTask(id) {
|
|
7249
|
-
if (this.tasks.has(id)) {
|
|
7250
|
-
return this.tasks.get(id);
|
|
7251
|
-
}
|
|
7252
|
-
const indexEntry = this.taskIndex.get(id);
|
|
7253
|
-
if (!indexEntry) {
|
|
7254
|
-
return void 0;
|
|
7255
|
-
}
|
|
7256
|
-
try {
|
|
7257
|
-
const content2 = await this.fs.readFile(indexEntry.filePath);
|
|
7258
|
-
const task = parseTaskMarkdown(content2, indexEntry.filePath);
|
|
7259
|
-
task.source = indexEntry.source === "completed" ? "completed" : "local";
|
|
7260
|
-
this.tasks.set(task.id, task);
|
|
7261
|
-
return task;
|
|
7262
|
-
} catch (error) {
|
|
7263
|
-
console.warn(`Failed to load task ${id} from ${indexEntry.filePath}:`, error);
|
|
7264
|
-
return void 0;
|
|
7265
|
-
}
|
|
7266
|
-
}
|
|
7267
|
-
/**
|
|
7268
|
-
* Load multiple tasks by ID (on-demand loading)
|
|
7269
|
-
*
|
|
7270
|
-
* @param ids - Array of task IDs to load
|
|
7271
|
-
* @returns Array of loaded tasks (undefined entries filtered out)
|
|
7272
|
-
*/
|
|
7273
|
-
async loadTasks(ids2) {
|
|
7274
|
-
const tasks = await Promise.all(ids2.map((id) => this.loadTask(id)));
|
|
7275
|
-
return tasks.filter((t) => t !== void 0);
|
|
7276
|
-
}
|
|
7277
|
-
/**
|
|
7278
|
-
* Get tasks by source (tasks/completed) with pagination
|
|
7279
|
-
*
|
|
7280
|
-
* This is a lazy-loading alternative to getTasksByStatusPaginated().
|
|
7281
|
-
* Groups by directory (tasks/ or completed/) instead of status.
|
|
7282
|
-
* Only loads task content for items in the requested page.
|
|
7283
|
-
*
|
|
7284
|
-
* @param options - Per-source pagination options
|
|
7285
|
-
* @returns Paginated tasks grouped by source
|
|
7286
|
-
*/
|
|
7287
|
-
async getTasksBySourcePaginated(options) {
|
|
7288
|
-
if (!this.lazyInitialized) {
|
|
7289
|
-
throw new Error("Core not lazy initialized. Call initializeLazy() first.");
|
|
7290
|
-
}
|
|
7291
|
-
const tasksLimit = (options == null ? void 0 : options.tasksLimit) ?? 10;
|
|
7292
|
-
const completedLimit = (options == null ? void 0 : options.completedLimit) ?? 10;
|
|
7293
|
-
const offset = (options == null ? void 0 : options.offset) ?? 0;
|
|
7294
|
-
const tasksSortDirection = (options == null ? void 0 : options.tasksSortDirection) ?? "asc";
|
|
7295
|
-
const completedSortByIdDesc = (options == null ? void 0 : options.completedSortByIdDesc) ?? true;
|
|
7296
|
-
const sources = ["tasks", "completed"];
|
|
7297
|
-
const bySource = /* @__PURE__ */ new Map();
|
|
7298
|
-
for (const source2 of sources) {
|
|
7299
|
-
let entries = Array.from(this.taskIndex.values()).filter((e) => e.source === source2);
|
|
7300
|
-
const limit = source2 === "tasks" ? tasksLimit : completedLimit;
|
|
7301
|
-
entries = entries.sort((a, b) => {
|
|
7302
|
-
const aNum = parseInt(a.id.replace(/\D/g, ""), 10) || 0;
|
|
7303
|
-
const bNum = parseInt(b.id.replace(/\D/g, ""), 10) || 0;
|
|
7304
|
-
if (source2 === "completed") {
|
|
7305
|
-
return completedSortByIdDesc ? bNum - aNum : aNum - bNum;
|
|
7306
|
-
} else {
|
|
7307
|
-
return tasksSortDirection === "desc" ? bNum - aNum : aNum - bNum;
|
|
7308
|
-
}
|
|
7309
|
-
});
|
|
7310
|
-
const total = entries.length;
|
|
7311
|
-
const pageEntries = entries.slice(offset, offset + limit);
|
|
7312
|
-
const items = await this.loadTasks(pageEntries.map((e) => e.id));
|
|
7313
|
-
bySource.set(source2, {
|
|
7314
|
-
items,
|
|
7315
|
-
total,
|
|
7316
|
-
hasMore: offset + limit < total,
|
|
7317
|
-
offset,
|
|
7318
|
-
limit
|
|
7319
|
-
});
|
|
7320
|
-
}
|
|
7321
|
-
return {
|
|
7322
|
-
bySource,
|
|
7323
|
-
sources
|
|
7324
|
-
};
|
|
7325
|
-
}
|
|
7326
|
-
/**
|
|
7327
|
-
* Load more tasks for a specific source (lazy loading)
|
|
7328
|
-
*
|
|
7329
|
-
* @param source - Source to load more from ("tasks" or "completed")
|
|
7330
|
-
* @param currentOffset - Current offset (items already loaded)
|
|
7331
|
-
* @param options - Pagination options
|
|
7332
|
-
* @returns Paginated result for the source
|
|
7333
|
-
*/
|
|
7334
|
-
async loadMoreForSource(source2, currentOffset, options) {
|
|
7335
|
-
if (!this.lazyInitialized) {
|
|
7336
|
-
throw new Error("Core not lazy initialized. Call initializeLazy() first.");
|
|
7337
|
-
}
|
|
7338
|
-
const limit = (options == null ? void 0 : options.limit) ?? 10;
|
|
7339
|
-
const sortDirection = (options == null ? void 0 : options.sortDirection) ?? "asc";
|
|
7340
|
-
const completedSortByIdDesc = (options == null ? void 0 : options.completedSortByIdDesc) ?? true;
|
|
7341
|
-
let entries = Array.from(this.taskIndex.values()).filter((e) => e.source === source2);
|
|
7342
|
-
entries = entries.sort((a, b) => {
|
|
7343
|
-
const aNum = parseInt(a.id.replace(/\D/g, ""), 10) || 0;
|
|
7344
|
-
const bNum = parseInt(b.id.replace(/\D/g, ""), 10) || 0;
|
|
7345
|
-
if (source2 === "completed") {
|
|
7346
|
-
return completedSortByIdDesc ? bNum - aNum : aNum - bNum;
|
|
7347
|
-
} else {
|
|
7348
|
-
return sortDirection === "desc" ? bNum - aNum : aNum - bNum;
|
|
7349
|
-
}
|
|
7350
|
-
});
|
|
7351
|
-
const total = entries.length;
|
|
7352
|
-
const pageEntries = entries.slice(currentOffset, currentOffset + limit);
|
|
7353
|
-
const items = await this.loadTasks(pageEntries.map((e) => e.id));
|
|
7354
|
-
return {
|
|
7355
|
-
items,
|
|
7356
|
-
total,
|
|
7357
|
-
hasMore: currentOffset + limit < total,
|
|
7358
|
-
offset: currentOffset,
|
|
7359
|
-
limit
|
|
7360
|
-
};
|
|
7361
|
-
}
|
|
7362
|
-
/**
|
|
7363
|
-
* Get the loaded configuration
|
|
7364
|
-
*
|
|
7365
|
-
* @throws Error if not initialized
|
|
7366
|
-
*/
|
|
7367
|
-
getConfig() {
|
|
7368
|
-
this.ensureInitialized();
|
|
7369
|
-
return this.safeConfig;
|
|
7370
|
-
}
|
|
7371
|
-
/**
|
|
7372
|
-
* List all tasks, optionally filtered
|
|
7373
|
-
*
|
|
7374
|
-
* @param filter - Optional filter criteria
|
|
7375
|
-
* @returns Sorted array of tasks
|
|
7376
|
-
*/
|
|
7377
|
-
listTasks(filter) {
|
|
7378
|
-
this.ensureInitialized();
|
|
7379
|
-
let tasks = Array.from(this.tasks.values());
|
|
7380
|
-
if (filter == null ? void 0 : filter.status) {
|
|
7381
|
-
tasks = tasks.filter((t) => t.status === filter.status);
|
|
7382
|
-
}
|
|
7383
|
-
if (filter == null ? void 0 : filter.assignee) {
|
|
7384
|
-
const assignee = filter.assignee;
|
|
7385
|
-
tasks = tasks.filter((t) => t.assignee.includes(assignee));
|
|
7386
|
-
}
|
|
7387
|
-
if (filter == null ? void 0 : filter.priority) {
|
|
7388
|
-
tasks = tasks.filter((t) => t.priority === filter.priority);
|
|
7389
|
-
}
|
|
7390
|
-
if (filter == null ? void 0 : filter.milestone) {
|
|
7391
|
-
const filterKey = milestoneKey(filter.milestone);
|
|
7392
|
-
tasks = tasks.filter((t) => milestoneKey(t.milestone) === filterKey);
|
|
7393
|
-
}
|
|
7394
|
-
if ((filter == null ? void 0 : filter.labels) && filter.labels.length > 0) {
|
|
7395
|
-
tasks = tasks.filter((t) => {
|
|
7396
|
-
var _a;
|
|
7397
|
-
return (_a = filter.labels) == null ? void 0 : _a.some((label) => t.labels.includes(label));
|
|
7398
|
-
});
|
|
7399
|
-
}
|
|
7400
|
-
if (filter == null ? void 0 : filter.parentTaskId) {
|
|
7401
|
-
tasks = tasks.filter((t) => t.parentTaskId === filter.parentTaskId);
|
|
7402
|
-
}
|
|
7403
|
-
return sortTasks(tasks);
|
|
7404
|
-
}
|
|
7405
|
-
/**
|
|
7406
|
-
* Get tasks grouped by status
|
|
7407
|
-
*
|
|
7408
|
-
* This is the primary method for kanban-style displays.
|
|
7409
|
-
* Returns a Map with status as key and sorted tasks as value.
|
|
7410
|
-
* The Map preserves the order of statuses from config.
|
|
7411
|
-
*/
|
|
7412
|
-
getTasksByStatus() {
|
|
7413
|
-
this.ensureInitialized();
|
|
7414
|
-
const tasks = Array.from(this.tasks.values());
|
|
7415
|
-
return groupTasksByStatus(tasks, this.safeConfig.statuses);
|
|
7416
|
-
}
|
|
7417
|
-
/**
|
|
7418
|
-
* Get tasks grouped by milestone
|
|
7419
|
-
*
|
|
7420
|
-
* Returns a MilestoneSummary with:
|
|
7421
|
-
* - milestones: List of milestone IDs in display order
|
|
7422
|
-
* - buckets: Array of MilestoneBucket with tasks, progress, status counts
|
|
7423
|
-
*
|
|
7424
|
-
* The first bucket is always "Tasks without milestone".
|
|
7425
|
-
* Each bucket includes progress percentage based on done status.
|
|
7426
|
-
*
|
|
7427
|
-
* @example
|
|
7428
|
-
* ```typescript
|
|
7429
|
-
* const summary = core.getTasksByMilestone();
|
|
7430
|
-
* for (const bucket of summary.buckets) {
|
|
7431
|
-
* console.log(`${bucket.label}: ${bucket.progress}% complete`);
|
|
7432
|
-
* console.log(` ${bucket.doneCount}/${bucket.total} tasks done`);
|
|
7433
|
-
* }
|
|
7434
|
-
* ```
|
|
7435
|
-
*/
|
|
7436
|
-
getTasksByMilestone() {
|
|
7437
|
-
this.ensureInitialized();
|
|
7438
|
-
const tasks = Array.from(this.tasks.values());
|
|
7439
|
-
return groupTasksByMilestone(tasks, this.safeConfig.milestones, this.safeConfig.statuses);
|
|
7440
|
-
}
|
|
7441
|
-
// =========================================================================
|
|
7442
|
-
// Milestone CRUD Operations
|
|
7443
|
-
// =========================================================================
|
|
7444
|
-
/**
|
|
7445
|
-
* Get the milestones directory path
|
|
7446
|
-
*/
|
|
7447
|
-
getMilestonesDir() {
|
|
7448
|
-
return this.fs.join(this.projectRoot, "backlog", "milestones");
|
|
7449
|
-
}
|
|
7450
|
-
/**
|
|
7451
|
-
* List all milestones from the milestones directory
|
|
7452
|
-
*
|
|
7453
|
-
* @returns Array of Milestone objects sorted by ID
|
|
7454
|
-
*/
|
|
7455
|
-
async listMilestones() {
|
|
7456
|
-
const milestonesDir = this.getMilestonesDir();
|
|
7457
|
-
if (!await this.fs.exists(milestonesDir)) {
|
|
7458
|
-
return [];
|
|
7459
|
-
}
|
|
7460
|
-
const entries = await this.fs.readDir(milestonesDir);
|
|
7461
|
-
const milestones = [];
|
|
7462
|
-
for (const entry of entries) {
|
|
7463
|
-
if (!entry.endsWith(".md"))
|
|
7464
|
-
continue;
|
|
7465
|
-
if (entry.toLowerCase() === "readme.md")
|
|
7466
|
-
continue;
|
|
7467
|
-
const milestoneId = extractMilestoneIdFromFilename(entry);
|
|
7468
|
-
if (!milestoneId)
|
|
7469
|
-
continue;
|
|
7470
|
-
const filepath = this.fs.join(milestonesDir, entry);
|
|
7471
|
-
if (await this.fs.isDirectory(filepath))
|
|
7472
|
-
continue;
|
|
7473
|
-
try {
|
|
7474
|
-
const content2 = await this.fs.readFile(filepath);
|
|
7475
|
-
milestones.push(parseMilestoneMarkdown(content2));
|
|
7476
|
-
} catch (error) {
|
|
7477
|
-
console.warn(`Failed to parse milestone file ${filepath}:`, error);
|
|
7478
|
-
}
|
|
7479
|
-
}
|
|
7480
|
-
return milestones.sort((a, b) => a.id.localeCompare(b.id, void 0, { numeric: true }));
|
|
7481
|
-
}
|
|
7482
|
-
/**
|
|
7483
|
-
* Load a single milestone by ID
|
|
7484
|
-
*
|
|
7485
|
-
* @param id - Milestone ID (e.g., "m-0")
|
|
7486
|
-
* @returns Milestone or null if not found
|
|
7487
|
-
*/
|
|
7488
|
-
async loadMilestone(id) {
|
|
7489
|
-
const milestonesDir = this.getMilestonesDir();
|
|
7490
|
-
if (!await this.fs.exists(milestonesDir)) {
|
|
7491
|
-
return null;
|
|
7492
|
-
}
|
|
7493
|
-
const entries = await this.fs.readDir(milestonesDir);
|
|
7494
|
-
const milestoneFile = entries.find((entry) => {
|
|
7495
|
-
const fileId = extractMilestoneIdFromFilename(entry);
|
|
7496
|
-
return fileId === id;
|
|
7497
|
-
});
|
|
7498
|
-
if (!milestoneFile) {
|
|
7499
|
-
return null;
|
|
7500
|
-
}
|
|
7501
|
-
const filepath = this.fs.join(milestonesDir, milestoneFile);
|
|
7502
|
-
try {
|
|
7503
|
-
const content2 = await this.fs.readFile(filepath);
|
|
7504
|
-
return parseMilestoneMarkdown(content2);
|
|
7505
|
-
} catch {
|
|
7506
|
-
return null;
|
|
7507
|
-
}
|
|
7508
|
-
}
|
|
7509
|
-
/**
|
|
7510
|
-
* Create a new milestone
|
|
7511
|
-
*
|
|
7512
|
-
* @param input - Milestone creation input
|
|
7513
|
-
* @returns Created milestone
|
|
7514
|
-
*/
|
|
7515
|
-
async createMilestone(input) {
|
|
7516
|
-
const startTime = Date.now();
|
|
7517
|
-
const span = this.tracer.startSpan("milestone.create", {
|
|
7518
|
-
attributes: {
|
|
7519
|
-
"input.title": input.title,
|
|
7520
|
-
"input.hasDescription": input.description !== void 0
|
|
7521
|
-
}
|
|
7522
|
-
});
|
|
7523
|
-
return await context.with(trace.setSpan(context.active(), span), async () => {
|
|
7524
|
-
try {
|
|
7525
|
-
span.addEvent("milestone.create.started", {
|
|
7526
|
-
"input.title": input.title,
|
|
7527
|
-
"input.hasDescription": input.description !== void 0
|
|
7528
|
-
});
|
|
7529
|
-
const milestonesDir = this.getMilestonesDir();
|
|
7530
|
-
await this.fs.createDir(milestonesDir, { recursive: true });
|
|
7531
|
-
const entries = await this.fs.readDir(milestonesDir).catch(() => []);
|
|
7532
|
-
const existingIds = entries.map((f) => {
|
|
7533
|
-
const match = f.match(/^m-(\d+)/);
|
|
7534
|
-
return (match == null ? void 0 : match[1]) ? parseInt(match[1], 10) : -1;
|
|
7535
|
-
}).filter((id2) => id2 >= 0);
|
|
7536
|
-
const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 0;
|
|
7537
|
-
const id = `m-${nextId}`;
|
|
7538
|
-
const description = input.description || `Milestone: ${input.title}`;
|
|
7539
|
-
const tempMilestone = {
|
|
7540
|
-
id,
|
|
7541
|
-
title: input.title,
|
|
7542
|
-
description,
|
|
7543
|
-
rawContent: "",
|
|
7544
|
-
tasks: []
|
|
7545
|
-
};
|
|
7546
|
-
const content2 = serializeMilestoneMarkdown(tempMilestone);
|
|
7547
|
-
const milestone = {
|
|
7548
|
-
id,
|
|
7549
|
-
title: input.title,
|
|
7550
|
-
description,
|
|
7551
|
-
rawContent: content2,
|
|
7552
|
-
tasks: []
|
|
7553
|
-
};
|
|
7554
|
-
const filename = getMilestoneFilename(id, input.title);
|
|
7555
|
-
const filepath = this.fs.join(milestonesDir, filename);
|
|
7556
|
-
await this.fs.writeFile(filepath, content2);
|
|
7557
|
-
span.addEvent("milestone.create.complete", {
|
|
7558
|
-
"output.milestoneId": id,
|
|
7559
|
-
"duration.ms": Date.now() - startTime
|
|
7560
|
-
});
|
|
7561
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
7562
|
-
span.end();
|
|
7563
|
-
return milestone;
|
|
7564
|
-
} catch (error) {
|
|
7565
|
-
span.addEvent("milestone.create.error", {
|
|
7566
|
-
"error.type": error instanceof Error ? error.name : "UnknownError",
|
|
7567
|
-
"error.message": error instanceof Error ? error.message : String(error)
|
|
7568
|
-
});
|
|
7569
|
-
span.setStatus({
|
|
7570
|
-
code: SpanStatusCode.ERROR,
|
|
7571
|
-
message: error instanceof Error ? error.message : String(error)
|
|
7572
|
-
});
|
|
7573
|
-
span.end();
|
|
7574
|
-
throw error;
|
|
7575
|
-
}
|
|
7576
|
-
});
|
|
7577
|
-
}
|
|
7578
|
-
/**
|
|
7579
|
-
* Update an existing milestone
|
|
7580
|
-
*
|
|
7581
|
-
* @param id - Milestone ID to update
|
|
7582
|
-
* @param input - Fields to update
|
|
7583
|
-
* @returns Updated milestone or null if not found
|
|
7584
|
-
*/
|
|
7585
|
-
async updateMilestone(id, input) {
|
|
7586
|
-
const startTime = Date.now();
|
|
7587
|
-
const span = this.tracer.startSpan("milestone.update", {
|
|
7588
|
-
attributes: {
|
|
7589
|
-
"input.milestoneId": id,
|
|
7590
|
-
"input.hasTitle": input.title !== void 0,
|
|
7591
|
-
"input.hasDescription": input.description !== void 0
|
|
7592
|
-
}
|
|
7593
|
-
});
|
|
7594
|
-
return await context.with(trace.setSpan(context.active(), span), async () => {
|
|
7595
|
-
try {
|
|
7596
|
-
span.addEvent("milestone.update.started", {
|
|
7597
|
-
"input.milestoneId": id,
|
|
7598
|
-
"input.hasTitle": input.title !== void 0,
|
|
7599
|
-
"input.hasDescription": input.description !== void 0
|
|
7600
|
-
});
|
|
7601
|
-
const existing = await this.loadMilestone(id);
|
|
7602
|
-
if (!existing) {
|
|
7603
|
-
span.addEvent("milestone.update.error", {
|
|
7604
|
-
"error.type": "NotFoundError",
|
|
7605
|
-
"error.message": "Milestone not found",
|
|
7606
|
-
"input.milestoneId": id
|
|
7607
|
-
});
|
|
7608
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
7609
|
-
span.end();
|
|
7610
|
-
return null;
|
|
7611
|
-
}
|
|
7612
|
-
const milestonesDir = this.getMilestonesDir();
|
|
7613
|
-
const entries = await this.fs.readDir(milestonesDir);
|
|
7614
|
-
const currentFile = entries.find((entry) => {
|
|
7615
|
-
const fileId = extractMilestoneIdFromFilename(entry);
|
|
7616
|
-
return fileId === id;
|
|
7617
|
-
});
|
|
7618
|
-
if (!currentFile) {
|
|
7619
|
-
span.addEvent("milestone.update.error", {
|
|
7620
|
-
"error.type": "NotFoundError",
|
|
7621
|
-
"error.message": "Milestone file not found",
|
|
7622
|
-
"input.milestoneId": id
|
|
7623
|
-
});
|
|
7624
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
7625
|
-
span.end();
|
|
7626
|
-
return null;
|
|
7627
|
-
}
|
|
7628
|
-
const newTitle = input.title ?? existing.title;
|
|
7629
|
-
const newDescription = input.description ?? existing.description;
|
|
7630
|
-
const titleChanged = input.title !== void 0 && input.title !== existing.title;
|
|
7631
|
-
const tempMilestone = {
|
|
7632
|
-
id: existing.id,
|
|
7633
|
-
title: newTitle,
|
|
7634
|
-
description: newDescription,
|
|
7635
|
-
rawContent: "",
|
|
7636
|
-
tasks: existing.tasks
|
|
7637
|
-
};
|
|
7638
|
-
const content2 = serializeMilestoneMarkdown(tempMilestone);
|
|
7639
|
-
const updated = {
|
|
7640
|
-
id: existing.id,
|
|
7641
|
-
title: newTitle,
|
|
7642
|
-
description: newDescription,
|
|
7643
|
-
rawContent: content2,
|
|
7644
|
-
tasks: existing.tasks
|
|
7645
|
-
};
|
|
7646
|
-
const oldPath = this.fs.join(milestonesDir, currentFile);
|
|
7647
|
-
await this.fs.deleteFile(oldPath);
|
|
7648
|
-
const newFilename = getMilestoneFilename(id, updated.title);
|
|
7649
|
-
const newPath = this.fs.join(milestonesDir, newFilename);
|
|
7650
|
-
await this.fs.writeFile(newPath, content2);
|
|
7651
|
-
span.addEvent("milestone.update.complete", {
|
|
7652
|
-
"output.milestoneId": id,
|
|
7653
|
-
"output.titleChanged": titleChanged,
|
|
7654
|
-
"duration.ms": Date.now() - startTime
|
|
7655
|
-
});
|
|
7656
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
7657
|
-
span.end();
|
|
7658
|
-
return updated;
|
|
7659
|
-
} catch (error) {
|
|
7660
|
-
span.addEvent("milestone.update.error", {
|
|
7661
|
-
"error.type": error instanceof Error ? error.name : "UnknownError",
|
|
7662
|
-
"error.message": error instanceof Error ? error.message : String(error),
|
|
7663
|
-
"input.milestoneId": id
|
|
7664
|
-
});
|
|
7665
|
-
span.setStatus({
|
|
7666
|
-
code: SpanStatusCode.ERROR,
|
|
7667
|
-
message: error instanceof Error ? error.message : String(error)
|
|
7668
|
-
});
|
|
7669
|
-
span.end();
|
|
7670
|
-
throw error;
|
|
7671
|
-
}
|
|
7672
|
-
});
|
|
7673
|
-
}
|
|
7674
|
-
/**
|
|
7675
|
-
* Delete a milestone
|
|
7676
|
-
*
|
|
7677
|
-
* @param id - Milestone ID to delete
|
|
7678
|
-
* @returns true if deleted, false if not found
|
|
7679
|
-
*/
|
|
7680
|
-
async deleteMilestone(id) {
|
|
7681
|
-
const startTime = Date.now();
|
|
7682
|
-
const span = this.tracer.startSpan("milestone.delete", {
|
|
7683
|
-
attributes: {
|
|
7684
|
-
"input.milestoneId": id
|
|
7685
|
-
}
|
|
7686
|
-
});
|
|
7687
|
-
return await context.with(trace.setSpan(context.active(), span), async () => {
|
|
7688
|
-
try {
|
|
7689
|
-
span.addEvent("milestone.delete.started", {
|
|
7690
|
-
"input.milestoneId": id
|
|
7691
|
-
});
|
|
7692
|
-
const milestonesDir = this.getMilestonesDir();
|
|
7693
|
-
if (!await this.fs.exists(milestonesDir)) {
|
|
7694
|
-
span.addEvent("milestone.delete.error", {
|
|
7695
|
-
"error.type": "NotFoundError",
|
|
7696
|
-
"error.message": "Milestones directory not found",
|
|
7697
|
-
"input.milestoneId": id
|
|
7698
|
-
});
|
|
7699
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
7700
|
-
span.end();
|
|
7701
|
-
return false;
|
|
7702
|
-
}
|
|
7703
|
-
const entries = await this.fs.readDir(milestonesDir);
|
|
7704
|
-
const milestoneFile = entries.find((entry) => {
|
|
7705
|
-
const fileId = extractMilestoneIdFromFilename(entry);
|
|
7706
|
-
return fileId === id;
|
|
7707
|
-
});
|
|
7708
|
-
if (!milestoneFile) {
|
|
7709
|
-
span.addEvent("milestone.delete.error", {
|
|
7710
|
-
"error.type": "NotFoundError",
|
|
7711
|
-
"error.message": "Milestone not found",
|
|
7712
|
-
"input.milestoneId": id
|
|
7713
|
-
});
|
|
7714
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
7715
|
-
span.end();
|
|
7716
|
-
return false;
|
|
7717
|
-
}
|
|
7718
|
-
const filepath = this.fs.join(milestonesDir, milestoneFile);
|
|
7719
|
-
await this.fs.deleteFile(filepath);
|
|
7720
|
-
span.addEvent("milestone.delete.complete", {
|
|
7721
|
-
"output.milestoneId": id,
|
|
7722
|
-
"output.deleted": true,
|
|
7723
|
-
"duration.ms": Date.now() - startTime
|
|
7724
|
-
});
|
|
7725
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
7726
|
-
span.end();
|
|
7727
|
-
return true;
|
|
7728
|
-
} catch (error) {
|
|
7729
|
-
span.addEvent("milestone.delete.error", {
|
|
7730
|
-
"error.type": error instanceof Error ? error.name : "UnknownError",
|
|
7731
|
-
"error.message": error instanceof Error ? error.message : String(error),
|
|
7732
|
-
"input.milestoneId": id
|
|
7733
|
-
});
|
|
7734
|
-
span.setStatus({
|
|
7735
|
-
code: SpanStatusCode.ERROR,
|
|
7736
|
-
message: error instanceof Error ? error.message : String(error)
|
|
7737
|
-
});
|
|
7738
|
-
span.end();
|
|
7739
|
-
throw error;
|
|
7740
|
-
}
|
|
7741
|
-
});
|
|
7742
|
-
}
|
|
7743
|
-
/**
|
|
7744
|
-
* Get a single task by ID
|
|
7745
|
-
*
|
|
7746
|
-
* @param id - Task ID
|
|
7747
|
-
* @returns Task or undefined if not found
|
|
7748
|
-
*/
|
|
7749
|
-
getTask(id) {
|
|
7750
|
-
this.ensureInitialized();
|
|
7751
|
-
return this.tasks.get(id);
|
|
7752
|
-
}
|
|
7753
|
-
/**
|
|
7754
|
-
* List tasks with pagination
|
|
7755
|
-
*
|
|
7756
|
-
* @param filter - Filter and pagination options
|
|
7757
|
-
* @returns Paginated result with tasks
|
|
7758
|
-
*/
|
|
7759
|
-
listTasksPaginated(filter) {
|
|
7760
|
-
this.ensureInitialized();
|
|
7761
|
-
let tasks = this.applyFilters(Array.from(this.tasks.values()), filter);
|
|
7762
|
-
const pagination = (filter == null ? void 0 : filter.pagination) ?? {};
|
|
7763
|
-
const sortBy = pagination.sortBy ?? "title";
|
|
7764
|
-
const sortDirection = pagination.sortDirection ?? "asc";
|
|
7765
|
-
tasks = sortTasksBy(tasks, sortBy, sortDirection);
|
|
7766
|
-
const limit = pagination.limit ?? 10;
|
|
7767
|
-
const offset = pagination.offset ?? 0;
|
|
7768
|
-
const total = tasks.length;
|
|
7769
|
-
const items = tasks.slice(offset, offset + limit);
|
|
7770
|
-
return {
|
|
7771
|
-
items,
|
|
7772
|
-
total,
|
|
7773
|
-
hasMore: offset + limit < total,
|
|
7774
|
-
offset,
|
|
7775
|
-
limit
|
|
7776
|
-
};
|
|
7777
|
-
}
|
|
7778
|
-
/**
|
|
7779
|
-
* Get tasks by status with pagination per column
|
|
7780
|
-
*
|
|
7781
|
-
* @param pagination - Pagination options (applied per status)
|
|
7782
|
-
* @returns Paginated tasks grouped by status
|
|
7783
|
-
*/
|
|
7784
|
-
getTasksByStatusPaginated(pagination) {
|
|
7785
|
-
this.ensureInitialized();
|
|
7786
|
-
const limit = (pagination == null ? void 0 : pagination.limit) ?? 10;
|
|
7787
|
-
const offset = (pagination == null ? void 0 : pagination.offset) ?? 0;
|
|
7788
|
-
const sortBy = (pagination == null ? void 0 : pagination.sortBy) ?? "title";
|
|
7789
|
-
const sortDirection = (pagination == null ? void 0 : pagination.sortDirection) ?? "asc";
|
|
7790
|
-
const byStatus = /* @__PURE__ */ new Map();
|
|
7791
|
-
const allGrouped = /* @__PURE__ */ new Map();
|
|
7792
|
-
for (const status of this.safeConfig.statuses) {
|
|
7793
|
-
allGrouped.set(status, []);
|
|
7794
|
-
}
|
|
7795
|
-
for (const task of this.tasks.values()) {
|
|
7796
|
-
const list2 = allGrouped.get(task.status);
|
|
7797
|
-
if (list2) {
|
|
7798
|
-
list2.push(task);
|
|
7799
|
-
} else {
|
|
7800
|
-
allGrouped.set(task.status, [task]);
|
|
7801
|
-
}
|
|
7802
|
-
}
|
|
7803
|
-
for (const status of this.safeConfig.statuses) {
|
|
7804
|
-
let tasks = allGrouped.get(status) ?? [];
|
|
7805
|
-
tasks = sortTasksBy(tasks, sortBy, sortDirection);
|
|
7806
|
-
const total = tasks.length;
|
|
7807
|
-
const items = tasks.slice(offset, offset + limit);
|
|
7808
|
-
byStatus.set(status, {
|
|
7809
|
-
items,
|
|
7810
|
-
total,
|
|
7811
|
-
hasMore: offset + limit < total,
|
|
7812
|
-
offset,
|
|
7813
|
-
limit
|
|
7814
|
-
});
|
|
7815
|
-
}
|
|
7816
|
-
return {
|
|
7817
|
-
byStatus,
|
|
7818
|
-
statuses: this.safeConfig.statuses
|
|
7819
|
-
};
|
|
7820
|
-
}
|
|
7821
|
-
/**
|
|
7822
|
-
* Load more tasks for a specific status
|
|
7823
|
-
*
|
|
7824
|
-
* @param status - Status column to load more from
|
|
7825
|
-
* @param currentOffset - Current offset (items already loaded)
|
|
7826
|
-
* @param pagination - Pagination options (limit, sortBy, sortDirection)
|
|
7827
|
-
* @returns Paginated result for the status
|
|
7828
|
-
*/
|
|
7829
|
-
loadMoreForStatus(status, currentOffset, pagination) {
|
|
7830
|
-
this.ensureInitialized();
|
|
7831
|
-
const limit = (pagination == null ? void 0 : pagination.limit) ?? 10;
|
|
7832
|
-
const sortBy = (pagination == null ? void 0 : pagination.sortBy) ?? "title";
|
|
7833
|
-
const sortDirection = (pagination == null ? void 0 : pagination.sortDirection) ?? "asc";
|
|
7834
|
-
let tasks = Array.from(this.tasks.values()).filter((t) => t.status === status);
|
|
7835
|
-
tasks = sortTasksBy(tasks, sortBy, sortDirection);
|
|
7836
|
-
const total = tasks.length;
|
|
7837
|
-
const items = tasks.slice(currentOffset, currentOffset + limit);
|
|
7838
|
-
return {
|
|
7839
|
-
items,
|
|
7840
|
-
total,
|
|
7841
|
-
hasMore: currentOffset + limit < total,
|
|
7842
|
-
offset: currentOffset,
|
|
7843
|
-
limit
|
|
7844
|
-
};
|
|
7845
|
-
}
|
|
7846
|
-
/**
|
|
7847
|
-
* Reload all tasks from disk
|
|
7848
|
-
*
|
|
7849
|
-
* Useful after external changes to task files.
|
|
7850
|
-
*/
|
|
7851
|
-
async reload() {
|
|
7852
|
-
this.tasks.clear();
|
|
7853
|
-
this.initialized = false;
|
|
7854
|
-
await this.initialize();
|
|
7855
|
-
}
|
|
7856
|
-
// --- Private methods ---
|
|
7857
|
-
ensureInitialized() {
|
|
7858
|
-
if (!this.initialized && !this.lazyInitialized || !this.config) {
|
|
7859
|
-
throw new Error("Core not initialized. Call initialize() or initializeLazy() first.");
|
|
7860
|
-
}
|
|
7861
|
-
}
|
|
7862
|
-
/**
|
|
7863
|
-
* Get config with type safety (use after ensureInitialized)
|
|
7864
|
-
*/
|
|
7865
|
-
get safeConfig() {
|
|
7866
|
-
return this.config;
|
|
7867
|
-
}
|
|
7868
|
-
applyFilters(tasks, filter) {
|
|
7869
|
-
if (!filter)
|
|
7870
|
-
return tasks;
|
|
7871
|
-
let result = tasks;
|
|
7872
|
-
if (filter.status) {
|
|
7873
|
-
result = result.filter((t) => t.status === filter.status);
|
|
7874
|
-
}
|
|
7875
|
-
if (filter.assignee) {
|
|
7876
|
-
const assignee = filter.assignee;
|
|
7877
|
-
result = result.filter((t) => t.assignee.includes(assignee));
|
|
7878
|
-
}
|
|
7879
|
-
if (filter.priority) {
|
|
7880
|
-
result = result.filter((t) => t.priority === filter.priority);
|
|
7881
|
-
}
|
|
7882
|
-
if (filter.milestone) {
|
|
7883
|
-
const filterKey = milestoneKey(filter.milestone);
|
|
7884
|
-
result = result.filter((t) => milestoneKey(t.milestone) === filterKey);
|
|
7885
|
-
}
|
|
7886
|
-
if (filter.labels && filter.labels.length > 0) {
|
|
7887
|
-
result = result.filter((t) => {
|
|
7888
|
-
var _a;
|
|
7889
|
-
return (_a = filter.labels) == null ? void 0 : _a.some((label) => t.labels.includes(label));
|
|
7890
|
-
});
|
|
7891
|
-
}
|
|
7892
|
-
if (filter.parentTaskId) {
|
|
7893
|
-
result = result.filter((t) => t.parentTaskId === filter.parentTaskId);
|
|
7894
|
-
}
|
|
7895
|
-
return result;
|
|
7896
|
-
}
|
|
7897
|
-
async loadTasksFromDirectory(dir, source2) {
|
|
7898
|
-
const entries = await this.fs.readDir(dir);
|
|
7899
|
-
for (const entry of entries) {
|
|
7900
|
-
const fullPath = this.fs.join(dir, entry);
|
|
7901
|
-
if (await this.fs.isDirectory(fullPath)) {
|
|
7902
|
-
continue;
|
|
7903
|
-
}
|
|
7904
|
-
if (!entry.endsWith(".md")) {
|
|
7905
|
-
continue;
|
|
7906
|
-
}
|
|
7907
|
-
try {
|
|
7908
|
-
const content2 = await this.fs.readFile(fullPath);
|
|
7909
|
-
const task = parseTaskMarkdown(content2, fullPath);
|
|
7910
|
-
task.source = source2;
|
|
7911
|
-
this.tasks.set(task.id, task);
|
|
7912
|
-
} catch (error) {
|
|
7913
|
-
console.warn(`Failed to parse task file ${fullPath}:`, error);
|
|
7914
|
-
}
|
|
7915
|
-
}
|
|
7916
|
-
}
|
|
7917
|
-
// =========================================================================
|
|
7918
|
-
// Milestone-Task Sync Helpers
|
|
7919
|
-
// =========================================================================
|
|
7920
|
-
/**
|
|
7921
|
-
* Add a task ID to a milestone's tasks array
|
|
7922
|
-
*
|
|
7923
|
-
* @param taskId - Task ID to add
|
|
7924
|
-
* @param milestoneId - Milestone ID to update
|
|
7925
|
-
*/
|
|
7926
|
-
async addTaskToMilestone(taskId, milestoneId) {
|
|
7927
|
-
const milestone = await this.loadMilestone(milestoneId);
|
|
7928
|
-
if (!milestone) {
|
|
7929
|
-
console.warn(`Milestone ${milestoneId} not found when adding task ${taskId}`);
|
|
7930
|
-
return;
|
|
7931
|
-
}
|
|
7932
|
-
if (milestone.tasks.includes(taskId)) {
|
|
7933
|
-
return;
|
|
7934
|
-
}
|
|
7935
|
-
const updatedMilestone = {
|
|
7936
|
-
...milestone,
|
|
7937
|
-
tasks: [...milestone.tasks, taskId]
|
|
7938
|
-
};
|
|
7939
|
-
await this.writeMilestoneFile(updatedMilestone);
|
|
7940
|
-
}
|
|
7941
|
-
/**
|
|
7942
|
-
* Remove a task ID from a milestone's tasks array
|
|
7943
|
-
*
|
|
7944
|
-
* @param taskId - Task ID to remove
|
|
7945
|
-
* @param milestoneId - Milestone ID to update
|
|
7946
|
-
*/
|
|
7947
|
-
async removeTaskFromMilestone(taskId, milestoneId) {
|
|
7948
|
-
const milestone = await this.loadMilestone(milestoneId);
|
|
7949
|
-
if (!milestone) {
|
|
7950
|
-
return;
|
|
7951
|
-
}
|
|
7952
|
-
if (!milestone.tasks.includes(taskId)) {
|
|
7953
|
-
return;
|
|
7954
|
-
}
|
|
7955
|
-
const updatedMilestone = {
|
|
7956
|
-
...milestone,
|
|
7957
|
-
tasks: milestone.tasks.filter((id) => id !== taskId)
|
|
7958
|
-
};
|
|
7959
|
-
await this.writeMilestoneFile(updatedMilestone);
|
|
7960
|
-
}
|
|
7961
|
-
/**
|
|
7962
|
-
* Write a milestone to disk
|
|
7963
|
-
*/
|
|
7964
|
-
async writeMilestoneFile(milestone) {
|
|
7965
|
-
const milestonesDir = this.getMilestonesDir();
|
|
7966
|
-
const entries = await this.fs.readDir(milestonesDir).catch(() => []);
|
|
7967
|
-
const currentFile = entries.find((entry) => {
|
|
7968
|
-
const fileId = extractMilestoneIdFromFilename(entry);
|
|
7969
|
-
return fileId === milestone.id;
|
|
7970
|
-
});
|
|
7971
|
-
if (currentFile) {
|
|
7972
|
-
const oldPath = this.fs.join(milestonesDir, currentFile);
|
|
7973
|
-
await this.fs.deleteFile(oldPath).catch(() => {
|
|
7974
|
-
});
|
|
7975
|
-
}
|
|
7976
|
-
const content2 = serializeMilestoneMarkdown(milestone);
|
|
7977
|
-
const filename = getMilestoneFilename(milestone.id, milestone.title);
|
|
7978
|
-
const filepath = this.fs.join(milestonesDir, filename);
|
|
7979
|
-
await this.fs.writeFile(filepath, content2);
|
|
7980
|
-
}
|
|
7981
|
-
/**
|
|
7982
|
-
* Get the tasks directory path
|
|
7983
|
-
*/
|
|
7984
|
-
getTasksDir() {
|
|
7985
|
-
return this.fs.join(this.projectRoot, "backlog", "tasks");
|
|
7986
|
-
}
|
|
7987
|
-
/**
|
|
7988
|
-
* Get the completed directory path
|
|
7989
|
-
*/
|
|
7990
|
-
getCompletedDir() {
|
|
7991
|
-
return this.fs.join(this.projectRoot, "backlog", "completed");
|
|
7992
|
-
}
|
|
7993
|
-
// =========================================================================
|
|
7994
|
-
// Task CRUD Operations
|
|
7995
|
-
// =========================================================================
|
|
7996
|
-
/**
|
|
7997
|
-
* Create a new task
|
|
7998
|
-
*
|
|
7999
|
-
* @param input - Task creation input
|
|
8000
|
-
* @returns Created task
|
|
8001
|
-
*/
|
|
8002
|
-
async createTask(input) {
|
|
8003
|
-
this.ensureInitialized();
|
|
8004
|
-
const startTime = Date.now();
|
|
8005
|
-
const span = this.tracer.startSpan("task.create", {
|
|
8006
|
-
attributes: {
|
|
8007
|
-
"input.title": input.title,
|
|
8008
|
-
"input.status": input.status,
|
|
8009
|
-
"input.milestoneId": input.milestone
|
|
8010
|
-
}
|
|
8011
|
-
});
|
|
8012
|
-
return await context.with(trace.setSpan(context.active(), span), async () => {
|
|
8013
|
-
var _a, _b, _c, _d, _e2;
|
|
8014
|
-
try {
|
|
8015
|
-
span.addEvent("task.create.started", {
|
|
8016
|
-
"input.title": input.title,
|
|
8017
|
-
"input.status": input.status,
|
|
8018
|
-
"input.milestoneId": input.milestone
|
|
8019
|
-
});
|
|
8020
|
-
const tasksDir = this.getTasksDir();
|
|
8021
|
-
await this.fs.createDir(tasksDir, { recursive: true });
|
|
8022
|
-
const existingIds = Array.from(this.lazyInitialized ? this.taskIndex.keys() : this.tasks.keys()).map((id) => parseInt(id.replace(/\D/g, ""), 10)).filter((n) => !Number.isNaN(n));
|
|
8023
|
-
const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1;
|
|
8024
|
-
const taskId = String(nextId);
|
|
8025
|
-
const configStatuses = ((_a = this.config) == null ? void 0 : _a.statuses) || [
|
|
8026
|
-
DEFAULT_TASK_STATUSES.TODO,
|
|
8027
|
-
DEFAULT_TASK_STATUSES.IN_PROGRESS,
|
|
8028
|
-
DEFAULT_TASK_STATUSES.DONE
|
|
8029
|
-
];
|
|
8030
|
-
let status = input.status || ((_b = this.config) == null ? void 0 : _b.defaultStatus) || DEFAULT_TASK_STATUSES.TODO;
|
|
8031
|
-
if (!configStatuses.includes(status)) {
|
|
8032
|
-
console.warn(`Warning: Status "${status}" is not in configured statuses [${configStatuses.join(", ")}]. Using default status "${((_c = this.config) == null ? void 0 : _c.defaultStatus) || DEFAULT_TASK_STATUSES.TODO}" instead.`);
|
|
8033
|
-
status = ((_d = this.config) == null ? void 0 : _d.defaultStatus) || DEFAULT_TASK_STATUSES.TODO;
|
|
8034
|
-
}
|
|
8035
|
-
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
8036
|
-
const task = {
|
|
8037
|
-
id: taskId,
|
|
8038
|
-
title: input.title,
|
|
8039
|
-
status,
|
|
8040
|
-
priority: input.priority,
|
|
8041
|
-
assignee: input.assignee || [],
|
|
8042
|
-
createdDate: now,
|
|
8043
|
-
labels: input.labels || [],
|
|
8044
|
-
milestone: input.milestone,
|
|
8045
|
-
dependencies: input.dependencies || [],
|
|
8046
|
-
references: input.references || [],
|
|
8047
|
-
parentTaskId: input.parentTaskId,
|
|
8048
|
-
description: input.description,
|
|
8049
|
-
implementationPlan: input.implementationPlan,
|
|
8050
|
-
implementationNotes: input.implementationNotes,
|
|
8051
|
-
acceptanceCriteriaItems: (_e2 = input.acceptanceCriteria) == null ? void 0 : _e2.map((ac, i) => ({
|
|
8052
|
-
index: i + 1,
|
|
8053
|
-
text: ac.text,
|
|
8054
|
-
checked: ac.checked || false
|
|
8055
|
-
})),
|
|
8056
|
-
rawContent: input.rawContent,
|
|
8057
|
-
source: "local"
|
|
8058
|
-
};
|
|
8059
|
-
const content2 = serializeTaskMarkdown(task);
|
|
8060
|
-
const safeTitle = input.title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").slice(0, 50);
|
|
8061
|
-
const filename = `${taskId} - ${safeTitle}.md`;
|
|
8062
|
-
const filepath = this.fs.join(tasksDir, filename);
|
|
8063
|
-
await this.fs.writeFile(filepath, content2);
|
|
8064
|
-
task.filePath = filepath;
|
|
8065
|
-
this.tasks.set(taskId, task);
|
|
8066
|
-
if (this.lazyInitialized) {
|
|
8067
|
-
const relativePath = filepath.replace(`${this.projectRoot}/`, "");
|
|
8068
|
-
this.taskIndex.set(taskId, {
|
|
8069
|
-
id: taskId,
|
|
8070
|
-
filePath: relativePath,
|
|
8071
|
-
title: task.title,
|
|
8072
|
-
source: "tasks"
|
|
8073
|
-
});
|
|
8074
|
-
}
|
|
8075
|
-
if (input.milestone) {
|
|
8076
|
-
await this.addTaskToMilestone(taskId, input.milestone);
|
|
8077
|
-
}
|
|
8078
|
-
span.addEvent("task.create.complete", {
|
|
8079
|
-
"output.taskId": taskId,
|
|
8080
|
-
"output.taskIndex": nextId,
|
|
8081
|
-
"duration.ms": Date.now() - startTime
|
|
8082
|
-
});
|
|
8083
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
8084
|
-
span.end();
|
|
8085
|
-
return task;
|
|
8086
|
-
} catch (error) {
|
|
8087
|
-
span.addEvent("task.create.error", {
|
|
8088
|
-
"error.type": error instanceof Error ? error.name : "UnknownError",
|
|
8089
|
-
"error.message": error instanceof Error ? error.message : String(error)
|
|
8090
|
-
});
|
|
8091
|
-
span.setStatus({
|
|
8092
|
-
code: SpanStatusCode.ERROR,
|
|
8093
|
-
message: error instanceof Error ? error.message : String(error)
|
|
8094
|
-
});
|
|
8095
|
-
span.end();
|
|
8096
|
-
throw error;
|
|
8097
|
-
}
|
|
8098
|
-
});
|
|
8099
|
-
}
|
|
8100
|
-
/**
|
|
8101
|
-
* Update an existing task
|
|
8102
|
-
*
|
|
8103
|
-
* @param id - Task ID to update
|
|
8104
|
-
* @param input - Fields to update
|
|
8105
|
-
* @returns Updated task or null if not found
|
|
8106
|
-
*/
|
|
8107
|
-
async updateTask(id, input) {
|
|
8108
|
-
this.ensureInitialized();
|
|
8109
|
-
const startTime = Date.now();
|
|
8110
|
-
const span = this.tracer.startSpan("task.update", {
|
|
8111
|
-
attributes: {
|
|
8112
|
-
"input.taskId": id,
|
|
8113
|
-
"input.hasTitle": input.title !== void 0,
|
|
8114
|
-
"input.hasStatus": input.status !== void 0,
|
|
8115
|
-
"input.hasMilestone": input.milestone !== void 0
|
|
8116
|
-
}
|
|
8117
|
-
});
|
|
8118
|
-
return await context.with(trace.setSpan(context.active(), span), async () => {
|
|
8119
|
-
try {
|
|
8120
|
-
span.addEvent("task.update.started", {
|
|
8121
|
-
"input.taskId": id,
|
|
8122
|
-
"input.hasTitle": input.title !== void 0,
|
|
8123
|
-
"input.hasStatus": input.status !== void 0,
|
|
8124
|
-
"input.hasMilestone": input.milestone !== void 0
|
|
8125
|
-
});
|
|
8126
|
-
const existing = this.tasks.get(id);
|
|
8127
|
-
if (!existing) {
|
|
8128
|
-
span.addEvent("task.update.error", {
|
|
8129
|
-
"error.type": "NotFoundError",
|
|
8130
|
-
"error.message": "Task not found",
|
|
8131
|
-
"input.taskId": id
|
|
8132
|
-
});
|
|
8133
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
8134
|
-
span.end();
|
|
8135
|
-
return null;
|
|
8136
|
-
}
|
|
8137
|
-
const oldMilestone = existing.milestone;
|
|
8138
|
-
const newMilestone = input.milestone === null ? void 0 : input.milestone !== void 0 ? input.milestone : oldMilestone;
|
|
8139
|
-
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
8140
|
-
const updated = {
|
|
8141
|
-
...existing,
|
|
8142
|
-
title: input.title ?? existing.title,
|
|
8143
|
-
status: input.status ?? existing.status,
|
|
8144
|
-
priority: input.priority ?? existing.priority,
|
|
8145
|
-
milestone: newMilestone,
|
|
8146
|
-
updatedDate: now,
|
|
8147
|
-
description: input.description ?? existing.description,
|
|
8148
|
-
implementationPlan: input.clearImplementationPlan ? void 0 : input.implementationPlan ?? existing.implementationPlan,
|
|
8149
|
-
implementationNotes: input.clearImplementationNotes ? void 0 : input.implementationNotes ?? existing.implementationNotes,
|
|
8150
|
-
ordinal: input.ordinal ?? existing.ordinal,
|
|
8151
|
-
dependencies: input.dependencies ?? existing.dependencies,
|
|
8152
|
-
references: input.references ?? existing.references ?? []
|
|
8153
|
-
};
|
|
8154
|
-
if (input.labels) {
|
|
8155
|
-
updated.labels = input.labels;
|
|
8156
|
-
} else {
|
|
8157
|
-
if (input.addLabels) {
|
|
8158
|
-
updated.labels = [.../* @__PURE__ */ new Set([...updated.labels, ...input.addLabels])];
|
|
8159
|
-
}
|
|
8160
|
-
if (input.removeLabels) {
|
|
8161
|
-
updated.labels = updated.labels.filter((l) => {
|
|
8162
|
-
var _a;
|
|
8163
|
-
return !((_a = input.removeLabels) == null ? void 0 : _a.includes(l));
|
|
8164
|
-
});
|
|
8165
|
-
}
|
|
8166
|
-
}
|
|
8167
|
-
if (input.assignee) {
|
|
8168
|
-
updated.assignee = input.assignee;
|
|
8169
|
-
}
|
|
8170
|
-
if (input.addDependencies) {
|
|
8171
|
-
updated.dependencies = [.../* @__PURE__ */ new Set([...updated.dependencies, ...input.addDependencies])];
|
|
8172
|
-
}
|
|
8173
|
-
if (input.removeDependencies) {
|
|
8174
|
-
updated.dependencies = updated.dependencies.filter((d) => {
|
|
8175
|
-
var _a;
|
|
8176
|
-
return !((_a = input.removeDependencies) == null ? void 0 : _a.includes(d));
|
|
8177
|
-
});
|
|
8178
|
-
}
|
|
8179
|
-
if (input.addReferences) {
|
|
8180
|
-
updated.references = [
|
|
8181
|
-
.../* @__PURE__ */ new Set([...updated.references || [], ...input.addReferences])
|
|
8182
|
-
];
|
|
8183
|
-
}
|
|
8184
|
-
if (input.removeReferences) {
|
|
8185
|
-
updated.references = (updated.references || []).filter((r2) => {
|
|
8186
|
-
var _a;
|
|
8187
|
-
return !((_a = input.removeReferences) == null ? void 0 : _a.includes(r2));
|
|
8188
|
-
});
|
|
8189
|
-
}
|
|
8190
|
-
if (input.acceptanceCriteria) {
|
|
8191
|
-
updated.acceptanceCriteriaItems = input.acceptanceCriteria.map((ac, i) => ({
|
|
8192
|
-
index: i + 1,
|
|
8193
|
-
text: ac.text,
|
|
8194
|
-
checked: ac.checked || false
|
|
8195
|
-
}));
|
|
8196
|
-
}
|
|
8197
|
-
const content2 = serializeTaskMarkdown(updated);
|
|
8198
|
-
if (existing.filePath) {
|
|
8199
|
-
await this.fs.deleteFile(existing.filePath).catch(() => {
|
|
8200
|
-
});
|
|
8201
|
-
}
|
|
8202
|
-
const tasksDir = this.getTasksDir();
|
|
8203
|
-
const safeTitle = updated.title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").slice(0, 50);
|
|
8204
|
-
const filename = `${id} - ${safeTitle}.md`;
|
|
8205
|
-
const filepath = this.fs.join(tasksDir, filename);
|
|
8206
|
-
await this.fs.writeFile(filepath, content2);
|
|
8207
|
-
updated.filePath = filepath;
|
|
8208
|
-
this.tasks.set(id, updated);
|
|
8209
|
-
const milestoneChanged = milestoneKey(oldMilestone) !== milestoneKey(newMilestone);
|
|
8210
|
-
if (milestoneChanged) {
|
|
8211
|
-
if (oldMilestone) {
|
|
8212
|
-
await this.removeTaskFromMilestone(id, oldMilestone);
|
|
8213
|
-
}
|
|
8214
|
-
if (newMilestone) {
|
|
8215
|
-
await this.addTaskToMilestone(id, newMilestone);
|
|
8216
|
-
}
|
|
8217
|
-
}
|
|
8218
|
-
span.addEvent("task.update.complete", {
|
|
8219
|
-
"output.taskId": id,
|
|
8220
|
-
"output.statusChanged": input.status !== void 0,
|
|
8221
|
-
"duration.ms": Date.now() - startTime
|
|
8222
|
-
});
|
|
8223
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
8224
|
-
span.end();
|
|
8225
|
-
return updated;
|
|
8226
|
-
} catch (error) {
|
|
8227
|
-
span.addEvent("task.update.error", {
|
|
8228
|
-
"error.type": error instanceof Error ? error.name : "UnknownError",
|
|
8229
|
-
"error.message": error instanceof Error ? error.message : String(error),
|
|
8230
|
-
"input.taskId": id
|
|
8231
|
-
});
|
|
8232
|
-
span.setStatus({
|
|
8233
|
-
code: SpanStatusCode.ERROR,
|
|
8234
|
-
message: error instanceof Error ? error.message : String(error)
|
|
8235
|
-
});
|
|
8236
|
-
span.end();
|
|
8237
|
-
throw error;
|
|
8238
|
-
}
|
|
8239
|
-
});
|
|
8240
|
-
}
|
|
8241
|
-
/**
|
|
8242
|
-
* Delete a task
|
|
8243
|
-
*
|
|
8244
|
-
* @param id - Task ID to delete
|
|
8245
|
-
* @returns true if deleted, false if not found
|
|
8246
|
-
*/
|
|
8247
|
-
async deleteTask(id) {
|
|
8248
|
-
this.ensureInitialized();
|
|
8249
|
-
const startTime = Date.now();
|
|
8250
|
-
const span = this.tracer.startSpan("task.delete", {
|
|
8251
|
-
attributes: { "input.taskId": id }
|
|
8252
|
-
});
|
|
8253
|
-
return await context.with(trace.setSpan(context.active(), span), async () => {
|
|
8254
|
-
try {
|
|
8255
|
-
span.addEvent("task.delete.started", { "input.taskId": id });
|
|
8256
|
-
const task = this.tasks.get(id);
|
|
8257
|
-
if (!task) {
|
|
8258
|
-
span.addEvent("task.delete.error", {
|
|
8259
|
-
"error.type": "NotFoundError",
|
|
8260
|
-
"error.message": "Task not found",
|
|
8261
|
-
"input.taskId": id
|
|
8262
|
-
});
|
|
8263
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
8264
|
-
span.end();
|
|
8265
|
-
return false;
|
|
8266
|
-
}
|
|
8267
|
-
if (task.milestone) {
|
|
8268
|
-
await this.removeTaskFromMilestone(id, task.milestone);
|
|
8269
|
-
}
|
|
8270
|
-
if (task.filePath) {
|
|
8271
|
-
try {
|
|
8272
|
-
await this.fs.deleteFile(task.filePath);
|
|
8273
|
-
} catch {
|
|
8274
|
-
}
|
|
8275
|
-
}
|
|
8276
|
-
this.tasks.delete(id);
|
|
8277
|
-
span.addEvent("task.delete.complete", {
|
|
8278
|
-
"output.taskId": id,
|
|
8279
|
-
"output.deleted": true,
|
|
8280
|
-
"duration.ms": Date.now() - startTime
|
|
8281
|
-
});
|
|
8282
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
8283
|
-
span.end();
|
|
8284
|
-
return true;
|
|
8285
|
-
} catch (error) {
|
|
8286
|
-
span.addEvent("task.delete.error", {
|
|
8287
|
-
"error.type": error instanceof Error ? error.name : "UnknownError",
|
|
8288
|
-
"error.message": error instanceof Error ? error.message : String(error),
|
|
8289
|
-
"input.taskId": id
|
|
8290
|
-
});
|
|
8291
|
-
span.setStatus({
|
|
8292
|
-
code: SpanStatusCode.ERROR,
|
|
8293
|
-
message: error instanceof Error ? error.message : String(error)
|
|
8294
|
-
});
|
|
8295
|
-
span.end();
|
|
8296
|
-
throw error;
|
|
8297
|
-
}
|
|
8298
|
-
});
|
|
8299
|
-
}
|
|
8300
|
-
/**
|
|
8301
|
-
* Archive a task (move from tasks/ to completed/)
|
|
8302
|
-
*
|
|
8303
|
-
* @param id - Task ID to archive
|
|
8304
|
-
* @returns Archived task or null if not found or already archived
|
|
8305
|
-
*/
|
|
8306
|
-
async archiveTask(id) {
|
|
8307
|
-
this.ensureInitialized();
|
|
8308
|
-
const startTime = Date.now();
|
|
8309
|
-
const span = this.tracer.startSpan("task.archive", {
|
|
8310
|
-
attributes: { "input.taskId": id }
|
|
8311
|
-
});
|
|
8312
|
-
return await context.with(trace.setSpan(context.active(), span), async () => {
|
|
8313
|
-
try {
|
|
8314
|
-
span.addEvent("task.archive.started", { "input.taskId": id });
|
|
8315
|
-
const task = this.tasks.get(id);
|
|
8316
|
-
if (!task) {
|
|
8317
|
-
span.addEvent("task.archive.error", {
|
|
8318
|
-
"error.type": "NotFoundError",
|
|
8319
|
-
"error.message": "Task not found",
|
|
8320
|
-
"input.taskId": id
|
|
8321
|
-
});
|
|
8322
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
8323
|
-
span.end();
|
|
8324
|
-
return null;
|
|
8325
|
-
}
|
|
8326
|
-
if (task.source === "completed") {
|
|
8327
|
-
span.addEvent("task.archive.error", {
|
|
8328
|
-
"error.type": "InvalidStateError",
|
|
8329
|
-
"error.message": "Task already archived",
|
|
8330
|
-
"input.taskId": id
|
|
8331
|
-
});
|
|
8332
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
8333
|
-
span.end();
|
|
8334
|
-
return null;
|
|
8335
|
-
}
|
|
8336
|
-
const completedDir = this.getCompletedDir();
|
|
8337
|
-
await this.fs.createDir(completedDir, { recursive: true });
|
|
8338
|
-
const safeTitle = task.title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").slice(0, 50);
|
|
8339
|
-
const filename = `${id} - ${safeTitle}.md`;
|
|
8340
|
-
const newFilepath = this.fs.join(completedDir, filename);
|
|
8341
|
-
if (task.filePath) {
|
|
8342
|
-
try {
|
|
8343
|
-
await this.fs.deleteFile(task.filePath);
|
|
8344
|
-
} catch {
|
|
8345
|
-
}
|
|
8346
|
-
}
|
|
8347
|
-
const archived = {
|
|
8348
|
-
...task,
|
|
8349
|
-
source: "completed",
|
|
8350
|
-
filePath: newFilepath
|
|
8351
|
-
};
|
|
8352
|
-
const content2 = serializeTaskMarkdown(archived);
|
|
8353
|
-
await this.fs.writeFile(newFilepath, content2);
|
|
8354
|
-
this.tasks.set(id, archived);
|
|
8355
|
-
if (this.lazyInitialized) {
|
|
8356
|
-
const entry = this.taskIndex.get(id);
|
|
8357
|
-
if (entry) {
|
|
8358
|
-
entry.source = "completed";
|
|
8359
|
-
entry.filePath = newFilepath;
|
|
8360
|
-
}
|
|
8361
|
-
}
|
|
8362
|
-
span.addEvent("task.archive.complete", {
|
|
8363
|
-
"output.taskId": id,
|
|
8364
|
-
"output.newPath": newFilepath,
|
|
8365
|
-
"duration.ms": Date.now() - startTime
|
|
8366
|
-
});
|
|
8367
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
8368
|
-
span.end();
|
|
8369
|
-
return archived;
|
|
8370
|
-
} catch (error) {
|
|
8371
|
-
span.addEvent("task.archive.error", {
|
|
8372
|
-
"error.type": error instanceof Error ? error.name : "UnknownError",
|
|
8373
|
-
"error.message": error instanceof Error ? error.message : String(error),
|
|
8374
|
-
"input.taskId": id
|
|
8375
|
-
});
|
|
8376
|
-
span.setStatus({
|
|
8377
|
-
code: SpanStatusCode.ERROR,
|
|
8378
|
-
message: error instanceof Error ? error.message : String(error)
|
|
8379
|
-
});
|
|
8380
|
-
span.end();
|
|
8381
|
-
throw error;
|
|
8382
|
-
}
|
|
8383
|
-
});
|
|
8384
|
-
}
|
|
8385
|
-
/**
|
|
8386
|
-
* Restore a task (move from completed/ to tasks/)
|
|
8387
|
-
*
|
|
8388
|
-
* @param id - Task ID to restore
|
|
8389
|
-
* @returns Restored task or null if not found or not archived
|
|
8390
|
-
*/
|
|
8391
|
-
async restoreTask(id) {
|
|
8392
|
-
this.ensureInitialized();
|
|
8393
|
-
const startTime = Date.now();
|
|
8394
|
-
const span = this.tracer.startSpan("task.restore", {
|
|
8395
|
-
attributes: { "input.taskId": id }
|
|
8396
|
-
});
|
|
8397
|
-
return await context.with(trace.setSpan(context.active(), span), async () => {
|
|
8398
|
-
try {
|
|
8399
|
-
span.addEvent("task.restore.started", { "input.taskId": id });
|
|
8400
|
-
const task = this.tasks.get(id);
|
|
8401
|
-
if (!task) {
|
|
8402
|
-
span.addEvent("task.restore.error", {
|
|
8403
|
-
"error.type": "NotFoundError",
|
|
8404
|
-
"error.message": "Task not found",
|
|
8405
|
-
"input.taskId": id
|
|
8406
|
-
});
|
|
8407
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
8408
|
-
span.end();
|
|
8409
|
-
return null;
|
|
8410
|
-
}
|
|
8411
|
-
if (task.source !== "completed") {
|
|
8412
|
-
span.addEvent("task.restore.error", {
|
|
8413
|
-
"error.type": "InvalidStateError",
|
|
8414
|
-
"error.message": "Task not in completed",
|
|
8415
|
-
"input.taskId": id
|
|
8416
|
-
});
|
|
8417
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
8418
|
-
span.end();
|
|
8419
|
-
return null;
|
|
8420
|
-
}
|
|
8421
|
-
const tasksDir = this.getTasksDir();
|
|
8422
|
-
await this.fs.createDir(tasksDir, { recursive: true });
|
|
8423
|
-
const safeTitle = task.title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").slice(0, 50);
|
|
8424
|
-
const filename = `${id} - ${safeTitle}.md`;
|
|
8425
|
-
const newFilepath = this.fs.join(tasksDir, filename);
|
|
8426
|
-
if (task.filePath) {
|
|
8427
|
-
try {
|
|
8428
|
-
await this.fs.deleteFile(task.filePath);
|
|
8429
|
-
} catch {
|
|
8430
|
-
}
|
|
8431
|
-
}
|
|
8432
|
-
const restored = {
|
|
8433
|
-
...task,
|
|
8434
|
-
source: "local",
|
|
8435
|
-
filePath: newFilepath
|
|
8436
|
-
};
|
|
8437
|
-
const content2 = serializeTaskMarkdown(restored);
|
|
8438
|
-
await this.fs.writeFile(newFilepath, content2);
|
|
8439
|
-
this.tasks.set(id, restored);
|
|
8440
|
-
if (this.lazyInitialized) {
|
|
8441
|
-
const entry = this.taskIndex.get(id);
|
|
8442
|
-
if (entry) {
|
|
8443
|
-
entry.source = "tasks";
|
|
8444
|
-
entry.filePath = newFilepath;
|
|
8445
|
-
}
|
|
8446
|
-
}
|
|
8447
|
-
span.addEvent("task.restore.complete", {
|
|
8448
|
-
"output.taskId": id,
|
|
8449
|
-
"output.newPath": newFilepath,
|
|
8450
|
-
"duration.ms": Date.now() - startTime
|
|
8451
|
-
});
|
|
8452
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
8453
|
-
span.end();
|
|
8454
|
-
return restored;
|
|
8455
|
-
} catch (error) {
|
|
8456
|
-
span.addEvent("task.restore.error", {
|
|
8457
|
-
"error.type": error instanceof Error ? error.name : "UnknownError",
|
|
8458
|
-
"error.message": error instanceof Error ? error.message : String(error),
|
|
8459
|
-
"input.taskId": id
|
|
8460
|
-
});
|
|
8461
|
-
span.setStatus({
|
|
8462
|
-
code: SpanStatusCode.ERROR,
|
|
8463
|
-
message: error instanceof Error ? error.message : String(error)
|
|
8464
|
-
});
|
|
8465
|
-
span.end();
|
|
8466
|
-
throw error;
|
|
8467
|
-
}
|
|
8468
|
-
});
|
|
8469
|
-
}
|
|
8470
|
-
/**
|
|
8471
|
-
* Load specific tasks by their IDs (for lazy loading milestone tasks)
|
|
8472
|
-
*
|
|
8473
|
-
* @param ids - Array of task IDs to load
|
|
8474
|
-
* @returns Array of loaded tasks (missing tasks excluded)
|
|
8475
|
-
*/
|
|
8476
|
-
async loadTasksByIds(ids2) {
|
|
8477
|
-
if (this.initialized) {
|
|
8478
|
-
return ids2.map((id) => this.tasks.get(id)).filter((t) => t !== void 0);
|
|
8479
|
-
}
|
|
8480
|
-
if (this.lazyInitialized) {
|
|
8481
|
-
return this.loadTasks(ids2);
|
|
8482
|
-
}
|
|
8483
|
-
throw new Error("Core not initialized. Call initialize() or initializeLazy() first.");
|
|
8484
|
-
}
|
|
8485
|
-
}
|
|
8486
|
-
const version = "1.2.9";
|
|
6339
|
+
const version = "1.2.10";
|
|
8487
6340
|
const packageJson = {
|
|
8488
6341
|
version
|
|
8489
6342
|
};
|
|
@@ -11118,10 +8971,6 @@ const KanbanPanel = ({
|
|
|
11118
8971
|
"task.id": task.id,
|
|
11119
8972
|
"task.status": task.status || "unknown"
|
|
11120
8973
|
});
|
|
11121
|
-
span.setStatus({
|
|
11122
|
-
code: SpanStatusCode.OK
|
|
11123
|
-
});
|
|
11124
|
-
span.end();
|
|
11125
8974
|
if (events2) {
|
|
11126
8975
|
events2.emit({
|
|
11127
8976
|
type: "task:selected",
|
|
@@ -11132,7 +8981,14 @@ const KanbanPanel = ({
|
|
|
11132
8981
|
task
|
|
11133
8982
|
}
|
|
11134
8983
|
});
|
|
8984
|
+
span.addEvent("task.selected.emitted", {
|
|
8985
|
+
"task.id": task.id
|
|
8986
|
+
});
|
|
11135
8987
|
}
|
|
8988
|
+
span.setStatus({
|
|
8989
|
+
code: SpanStatusCode.OK
|
|
8990
|
+
});
|
|
8991
|
+
span.end();
|
|
11136
8992
|
}, [events2]);
|
|
11137
8993
|
useEffect(() => {
|
|
11138
8994
|
if (!events2) return;
|
|
@@ -56251,6 +54107,7 @@ const TaskDetailPanel = ({
|
|
|
56251
54107
|
status: "idle"
|
|
56252
54108
|
});
|
|
56253
54109
|
const panelRef = useRef(null);
|
|
54110
|
+
const deleteSpanRef = useRef(null);
|
|
56254
54111
|
const {
|
|
56255
54112
|
editable = false
|
|
56256
54113
|
} = config ?? {};
|
|
@@ -56304,27 +54161,42 @@ const TaskDetailPanel = ({
|
|
|
56304
54161
|
const handleOpenDeleteModal = useCallback(() => {
|
|
56305
54162
|
if (!selectedTask) return;
|
|
56306
54163
|
const tracer = getTracer();
|
|
56307
|
-
const span = tracer.startSpan("
|
|
54164
|
+
const span = tracer.startSpan("task.delete", {
|
|
56308
54165
|
attributes: {
|
|
56309
54166
|
"task.id": selectedTask.id
|
|
56310
54167
|
}
|
|
56311
54168
|
});
|
|
54169
|
+
deleteSpanRef.current = span;
|
|
56312
54170
|
span.addEvent("delete.modal.opened", {
|
|
56313
54171
|
"task.id": selectedTask.id
|
|
56314
54172
|
});
|
|
56315
|
-
span.setStatus({
|
|
56316
|
-
code: SpanStatusCode.OK
|
|
56317
|
-
});
|
|
56318
|
-
span.end();
|
|
56319
54173
|
setIsDeleteModalOpen(true);
|
|
56320
54174
|
}, [selectedTask]);
|
|
56321
54175
|
const handleDeleteConfirm = useCallback(async () => {
|
|
56322
54176
|
if (!selectedTask) return;
|
|
54177
|
+
const span = deleteSpanRef.current;
|
|
56323
54178
|
if (!(actions == null ? void 0 : actions.deleteTask)) {
|
|
56324
54179
|
setDeleteState({
|
|
56325
54180
|
status: "error",
|
|
56326
54181
|
error: "Delete action not available"
|
|
56327
54182
|
});
|
|
54183
|
+
if (span) {
|
|
54184
|
+
span.addEvent("task.deleted", {
|
|
54185
|
+
"task.id": selectedTask.id
|
|
54186
|
+
});
|
|
54187
|
+
span.addEvent("task.save.error", {
|
|
54188
|
+
"task.id": selectedTask.id,
|
|
54189
|
+
"operation": "delete",
|
|
54190
|
+
"error.type": "ActionNotAvailable",
|
|
54191
|
+
"error.message": "Delete action not available"
|
|
54192
|
+
});
|
|
54193
|
+
span.setStatus({
|
|
54194
|
+
code: SpanStatusCode.ERROR,
|
|
54195
|
+
message: "Delete action not available"
|
|
54196
|
+
});
|
|
54197
|
+
span.end();
|
|
54198
|
+
deleteSpanRef.current = null;
|
|
54199
|
+
}
|
|
56328
54200
|
return;
|
|
56329
54201
|
}
|
|
56330
54202
|
setDeleteState({
|
|
@@ -56332,19 +54204,16 @@ const TaskDetailPanel = ({
|
|
|
56332
54204
|
});
|
|
56333
54205
|
try {
|
|
56334
54206
|
await actions.deleteTask(selectedTask.id);
|
|
56335
|
-
|
|
56336
|
-
|
|
56337
|
-
attributes: {
|
|
54207
|
+
if (span) {
|
|
54208
|
+
span.addEvent("task.deleted", {
|
|
56338
54209
|
"task.id": selectedTask.id
|
|
56339
|
-
}
|
|
56340
|
-
|
|
56341
|
-
|
|
56342
|
-
|
|
56343
|
-
|
|
56344
|
-
|
|
56345
|
-
|
|
56346
|
-
});
|
|
56347
|
-
span.end();
|
|
54210
|
+
});
|
|
54211
|
+
span.setStatus({
|
|
54212
|
+
code: SpanStatusCode.OK
|
|
54213
|
+
});
|
|
54214
|
+
span.end();
|
|
54215
|
+
deleteSpanRef.current = null;
|
|
54216
|
+
}
|
|
56348
54217
|
setDeleteState({
|
|
56349
54218
|
status: "success"
|
|
56350
54219
|
});
|
|
@@ -56369,8 +54238,39 @@ const TaskDetailPanel = ({
|
|
|
56369
54238
|
status: "error",
|
|
56370
54239
|
error: errorMessage
|
|
56371
54240
|
});
|
|
54241
|
+
if (span) {
|
|
54242
|
+
span.addEvent("task.deleted", {
|
|
54243
|
+
"task.id": selectedTask.id
|
|
54244
|
+
});
|
|
54245
|
+
span.addEvent("task.save.error", {
|
|
54246
|
+
"task.id": selectedTask.id,
|
|
54247
|
+
"operation": "delete",
|
|
54248
|
+
"error.type": error instanceof Error ? error.name : "Unknown",
|
|
54249
|
+
"error.message": errorMessage
|
|
54250
|
+
});
|
|
54251
|
+
span.setStatus({
|
|
54252
|
+
code: SpanStatusCode.ERROR,
|
|
54253
|
+
message: errorMessage
|
|
54254
|
+
});
|
|
54255
|
+
span.end();
|
|
54256
|
+
deleteSpanRef.current = null;
|
|
54257
|
+
}
|
|
56372
54258
|
}
|
|
56373
54259
|
}, [actions, selectedTask, events2]);
|
|
54260
|
+
const handleDeleteCancel = useCallback(() => {
|
|
54261
|
+
const span = deleteSpanRef.current;
|
|
54262
|
+
if (span) {
|
|
54263
|
+
span.addEvent("delete.modal.cancelled", {
|
|
54264
|
+
"task.id": selectedTask == null ? void 0 : selectedTask.id
|
|
54265
|
+
});
|
|
54266
|
+
span.setStatus({
|
|
54267
|
+
code: SpanStatusCode.OK
|
|
54268
|
+
});
|
|
54269
|
+
span.end();
|
|
54270
|
+
deleteSpanRef.current = null;
|
|
54271
|
+
}
|
|
54272
|
+
setIsDeleteModalOpen(false);
|
|
54273
|
+
}, [selectedTask]);
|
|
56374
54274
|
useEffect(() => {
|
|
56375
54275
|
if (!events2) return;
|
|
56376
54276
|
const handleTaskSelected = (event) => {
|
|
@@ -56439,6 +54339,17 @@ const TaskDetailPanel = ({
|
|
|
56439
54339
|
};
|
|
56440
54340
|
}, [events2, selectedTask, isDeleteModalOpen, handleOpenDeleteModal, handleDeleteConfirm]);
|
|
56441
54341
|
const handleBack = useCallback(() => {
|
|
54342
|
+
const deleteSpan = deleteSpanRef.current;
|
|
54343
|
+
if (deleteSpan) {
|
|
54344
|
+
deleteSpan.addEvent("delete.modal.cancelled", {
|
|
54345
|
+
"task.id": selectedTask == null ? void 0 : selectedTask.id
|
|
54346
|
+
});
|
|
54347
|
+
deleteSpan.setStatus({
|
|
54348
|
+
code: SpanStatusCode.OK
|
|
54349
|
+
});
|
|
54350
|
+
deleteSpan.end();
|
|
54351
|
+
deleteSpanRef.current = null;
|
|
54352
|
+
}
|
|
56442
54353
|
if (selectedTask) {
|
|
56443
54354
|
const tracer = getTracer();
|
|
56444
54355
|
const span = tracer.startSpan("detail.interaction", {
|
|
@@ -56491,6 +54402,7 @@ const TaskDetailPanel = ({
|
|
|
56491
54402
|
children: [/* @__PURE__ */ jsx("h3", {
|
|
56492
54403
|
style: {
|
|
56493
54404
|
margin: "0 0 8px 0",
|
|
54405
|
+
fontFamily: theme2.fonts.body,
|
|
56494
54406
|
fontSize: theme2.fontSizes[3],
|
|
56495
54407
|
color: theme2.colors.text,
|
|
56496
54408
|
fontWeight: theme2.fontWeights.semibold
|
|
@@ -56499,6 +54411,7 @@ const TaskDetailPanel = ({
|
|
|
56499
54411
|
}), /* @__PURE__ */ jsx("p", {
|
|
56500
54412
|
style: {
|
|
56501
54413
|
margin: 0,
|
|
54414
|
+
fontFamily: theme2.fonts.body,
|
|
56502
54415
|
fontSize: theme2.fontSizes[1],
|
|
56503
54416
|
color: theme2.colors.textSecondary
|
|
56504
54417
|
},
|
|
@@ -56839,7 +54752,7 @@ const TaskDetailPanel = ({
|
|
|
56839
54752
|
padding: "16px"
|
|
56840
54753
|
},
|
|
56841
54754
|
children: [/* @__PURE__ */ jsx("div", {
|
|
56842
|
-
onClick:
|
|
54755
|
+
onClick: handleDeleteCancel,
|
|
56843
54756
|
style: {
|
|
56844
54757
|
position: "absolute",
|
|
56845
54758
|
inset: 0,
|
|
@@ -56866,13 +54779,14 @@ const TaskDetailPanel = ({
|
|
|
56866
54779
|
children: [/* @__PURE__ */ jsx("h2", {
|
|
56867
54780
|
style: {
|
|
56868
54781
|
margin: 0,
|
|
54782
|
+
fontFamily: theme2.fonts.body,
|
|
56869
54783
|
fontSize: theme2.fontSizes[4],
|
|
56870
|
-
fontWeight:
|
|
56871
|
-
color: theme2.colors.
|
|
54784
|
+
fontWeight: theme2.fontWeights.semibold,
|
|
54785
|
+
color: theme2.colors.warning
|
|
56872
54786
|
},
|
|
56873
54787
|
children: "Delete Task?"
|
|
56874
54788
|
}), /* @__PURE__ */ jsx("button", {
|
|
56875
|
-
onClick:
|
|
54789
|
+
onClick: handleDeleteCancel,
|
|
56876
54790
|
style: {
|
|
56877
54791
|
background: "none",
|
|
56878
54792
|
border: "none",
|
|
@@ -56895,11 +54809,15 @@ const TaskDetailPanel = ({
|
|
|
56895
54809
|
children: [/* @__PURE__ */ jsxs("p", {
|
|
56896
54810
|
style: {
|
|
56897
54811
|
margin: 0,
|
|
54812
|
+
fontFamily: theme2.fonts.body,
|
|
56898
54813
|
fontSize: theme2.fontSizes[2],
|
|
56899
54814
|
color: theme2.colors.text,
|
|
56900
54815
|
lineHeight: 1.5
|
|
56901
54816
|
},
|
|
56902
54817
|
children: ["Are you sure you want to delete ", /* @__PURE__ */ jsxs("strong", {
|
|
54818
|
+
style: {
|
|
54819
|
+
color: theme2.colors.primary
|
|
54820
|
+
},
|
|
56903
54821
|
children: ['"', selectedTask.title, '"']
|
|
56904
54822
|
}), "? This action cannot be undone."]
|
|
56905
54823
|
}), deleteState.status === "error" && /* @__PURE__ */ jsx("div", {
|
|
@@ -56909,8 +54827,9 @@ const TaskDetailPanel = ({
|
|
|
56909
54827
|
backgroundColor: `${theme2.colors.error}15`,
|
|
56910
54828
|
border: `1px solid ${theme2.colors.error}`,
|
|
56911
54829
|
borderRadius: theme2.radii[2],
|
|
56912
|
-
|
|
56913
|
-
fontSize: theme2.fontSizes[1]
|
|
54830
|
+
fontFamily: theme2.fonts.body,
|
|
54831
|
+
fontSize: theme2.fontSizes[1],
|
|
54832
|
+
color: theme2.colors.error
|
|
56914
54833
|
},
|
|
56915
54834
|
children: deleteState.error || "Failed to delete task"
|
|
56916
54835
|
})]
|
|
@@ -56924,12 +54843,13 @@ const TaskDetailPanel = ({
|
|
|
56924
54843
|
},
|
|
56925
54844
|
children: [/* @__PURE__ */ jsx("button", {
|
|
56926
54845
|
type: "button",
|
|
56927
|
-
onClick:
|
|
54846
|
+
onClick: handleDeleteCancel,
|
|
56928
54847
|
disabled: deleteState.status === "loading",
|
|
56929
54848
|
style: {
|
|
56930
54849
|
padding: "10px 20px",
|
|
54850
|
+
fontFamily: theme2.fonts.body,
|
|
56931
54851
|
fontSize: theme2.fontSizes[2],
|
|
56932
|
-
fontWeight:
|
|
54852
|
+
fontWeight: theme2.fontWeights.medium,
|
|
56933
54853
|
border: `1px solid ${theme2.colors.border}`,
|
|
56934
54854
|
borderRadius: theme2.radii[2],
|
|
56935
54855
|
backgroundColor: "transparent",
|
|
@@ -56947,8 +54867,9 @@ const TaskDetailPanel = ({
|
|
|
56947
54867
|
alignItems: "center",
|
|
56948
54868
|
gap: "8px",
|
|
56949
54869
|
padding: "10px 20px",
|
|
54870
|
+
fontFamily: theme2.fonts.body,
|
|
56950
54871
|
fontSize: theme2.fontSizes[2],
|
|
56951
|
-
fontWeight:
|
|
54872
|
+
fontWeight: theme2.fontWeights.medium,
|
|
56952
54873
|
border: "none",
|
|
56953
54874
|
borderRadius: theme2.radii[2],
|
|
56954
54875
|
backgroundColor: theme2.colors.error,
|