esedre 0.1.11 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +116 -74
- package/README.md +53 -10
- package/dist/esedre.mjs +2290 -325
- package/dist/web/assets/index-DDWeUKW-.css +2 -0
- package/dist/web/assets/index-DjaZk7Yp.js +117 -0
- package/dist/web/index.html +2 -2
- package/docs/assets/esedre-milestones.png +0 -0
- package/package.json +2 -2
- package/dist/web/assets/index-CfQ8qeBs.css +0 -2
- package/dist/web/assets/index-xIsWBrsf.js +0 -114
package/dist/esedre.mjs
CHANGED
|
@@ -7,9 +7,19 @@ import path9 from "node:path";
|
|
|
7
7
|
// src/storage/filesystem.ts
|
|
8
8
|
import fs3 from "node:fs";
|
|
9
9
|
import path3 from "node:path";
|
|
10
|
+
import os2 from "node:os";
|
|
10
11
|
|
|
11
12
|
// src/types.ts
|
|
12
|
-
var CURRENT_ESEDRE_VERSION = "0.
|
|
13
|
+
var CURRENT_ESEDRE_VERSION = "1.0.0";
|
|
14
|
+
var INVERSE_RELATIONS = {
|
|
15
|
+
"relates-to": "relates-to",
|
|
16
|
+
"blocks": "blocked-by",
|
|
17
|
+
"blocked-by": "blocks",
|
|
18
|
+
"parent-of": "child-of",
|
|
19
|
+
"child-of": "parent-of",
|
|
20
|
+
"duplicates": "duplicated-by",
|
|
21
|
+
"duplicated-by": "duplicates"
|
|
22
|
+
};
|
|
13
23
|
var EsedreConflictError = class extends Error {
|
|
14
24
|
constructor(ticketId, currentHash, lastHash) {
|
|
15
25
|
super(
|
|
@@ -446,9 +456,13 @@ function computeTicketHash(ticket) {
|
|
|
446
456
|
title: (ticket.meta.title || "").trim(),
|
|
447
457
|
category: ticket.meta.category,
|
|
448
458
|
status: ticket.meta.status,
|
|
459
|
+
priority: ticket.meta.priority || "",
|
|
449
460
|
complexity: ticket.meta.complexity || "",
|
|
450
461
|
effort: ticket.meta.estimatedEffort || "",
|
|
451
462
|
project: ticket.meta.project || "",
|
|
463
|
+
milestone: ticket.meta.milestone || "",
|
|
464
|
+
featureFlag: ticket.meta.featureFlag || "",
|
|
465
|
+
links: ticket.meta.links || [],
|
|
452
466
|
revision: ticket.meta.revision || 1,
|
|
453
467
|
completedAt: ticket.meta.completedAt || "",
|
|
454
468
|
detailRaw: (ticket.detail?.raw || "").trim(),
|
|
@@ -495,11 +509,17 @@ async function generateProjectSnapshot(storage, projectCode, outputDir) {
|
|
|
495
509
|
title: ticketObj.meta.title,
|
|
496
510
|
type: ticketObj.meta.type || ticketObj.meta.category || "Feature",
|
|
497
511
|
category: ticketObj.meta.type || ticketObj.meta.category || "Feature",
|
|
512
|
+
priority: ticketObj.meta.priority,
|
|
498
513
|
status: ticketObj.meta.status,
|
|
499
514
|
complexity: ticketObj.meta.complexity,
|
|
500
515
|
estimatedEffort: ticketObj.meta.estimatedEffort,
|
|
501
516
|
project: pCode,
|
|
502
517
|
summary: ticketObj.detail?.summary,
|
|
518
|
+
milestone: ticketObj.meta.milestone,
|
|
519
|
+
featureFlag: ticketObj.meta.featureFlag,
|
|
520
|
+
inheritedFeatureFlag: ticketObj.meta.inheritedFeatureFlag,
|
|
521
|
+
links: ticketObj.meta.links || [],
|
|
522
|
+
isBlocked: ticketObj.isBlocked || false,
|
|
503
523
|
hasPlan: Boolean(plan && plan.trim().length > 0),
|
|
504
524
|
planMarkdown: plan && plan.trim().length > 0 ? plan : void 0,
|
|
505
525
|
createdAt,
|
|
@@ -510,12 +530,38 @@ async function generateProjectSnapshot(storage, projectCode, outputDir) {
|
|
|
510
530
|
sha1
|
|
511
531
|
});
|
|
512
532
|
}
|
|
533
|
+
let milestoneSnapshots;
|
|
534
|
+
try {
|
|
535
|
+
const rawMilestones = await storage.listMilestones(projectCode);
|
|
536
|
+
milestoneSnapshots = rawMilestones.map((m) => {
|
|
537
|
+
const mTickets = entries.filter(
|
|
538
|
+
(t) => t.milestone && (t.milestone.toLowerCase() === String(m.id).toLowerCase() || t.milestone.toLowerCase() === m.title.toLowerCase())
|
|
539
|
+
);
|
|
540
|
+
const completed = mTickets.filter((t) => t.status === "Completed").length;
|
|
541
|
+
const total = mTickets.length;
|
|
542
|
+
const progressPercent = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
543
|
+
return {
|
|
544
|
+
id: m.id,
|
|
545
|
+
project: m.project,
|
|
546
|
+
title: m.title,
|
|
547
|
+
description: m.description,
|
|
548
|
+
status: m.status,
|
|
549
|
+
featureFlag: m.featureFlag,
|
|
550
|
+
targetDate: m.targetDate,
|
|
551
|
+
totalTickets: total,
|
|
552
|
+
completedTickets: completed,
|
|
553
|
+
progressPercent
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
} catch {
|
|
557
|
+
}
|
|
513
558
|
const snapshot = {
|
|
514
559
|
version: CURRENT_ESEDRE_VERSION,
|
|
515
560
|
projectCode,
|
|
516
561
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
517
562
|
totalTickets: entries.length,
|
|
518
|
-
tickets: entries
|
|
563
|
+
tickets: entries,
|
|
564
|
+
...milestoneSnapshots && milestoneSnapshots.length > 0 ? { milestones: milestoneSnapshots } : {}
|
|
519
565
|
};
|
|
520
566
|
const esedreDir = path2.join(outputDir, ".esedre");
|
|
521
567
|
if (!fs2.existsSync(esedreDir)) {
|
|
@@ -538,6 +584,289 @@ async function generateProjectSnapshot(storage, projectCode, outputDir) {
|
|
|
538
584
|
return snapshot;
|
|
539
585
|
}
|
|
540
586
|
|
|
587
|
+
// src/utils/formatter.ts
|
|
588
|
+
var isColorSupported = !process.env.NO_COLOR && (process.stdout.isTTY || process.env.FORCE_COLOR);
|
|
589
|
+
var colors = {
|
|
590
|
+
reset: isColorSupported ? "\x1B[0m" : "",
|
|
591
|
+
bold: isColorSupported ? "\x1B[1m" : "",
|
|
592
|
+
dim: isColorSupported ? "\x1B[2m" : "",
|
|
593
|
+
cyan: isColorSupported ? "\x1B[36m" : "",
|
|
594
|
+
green: isColorSupported ? "\x1B[32m" : "",
|
|
595
|
+
yellow: isColorSupported ? "\x1B[33m" : "",
|
|
596
|
+
magenta: isColorSupported ? "\x1B[35m" : "",
|
|
597
|
+
red: isColorSupported ? "\x1B[31m" : "",
|
|
598
|
+
blue: isColorSupported ? "\x1B[34m" : ""
|
|
599
|
+
};
|
|
600
|
+
function normalizePriority(val) {
|
|
601
|
+
if (!val) return void 0;
|
|
602
|
+
const lower = String(val).trim().toLowerCase();
|
|
603
|
+
if (lower === "critical") return "Critical";
|
|
604
|
+
if (lower === "high") return "High";
|
|
605
|
+
if (lower === "medium") return "Medium";
|
|
606
|
+
if (lower === "low") return "Low";
|
|
607
|
+
return void 0;
|
|
608
|
+
}
|
|
609
|
+
function normalizeDashesAndMojibake(val) {
|
|
610
|
+
if (typeof val !== "string") return val;
|
|
611
|
+
return val.replace(/–|‗/g, "-").replace(/â€[˜™]/g, "'").replace(/â€[œ ]/g, '"').replace(/\u00e2\u20ac\u201c|\u00e2\u0080\u0093|–/g, "-").replace(/\u00e2\u20ac\u201d|\u00e2\u0080\u0094|—/g, "-").replace(/\u00e2\u20ac\u02dc|\u00e2\u20ac\u2122|‘|’/g, "'").replace(/\u00e2\u20ac\u0153|\u00e2\u20ac\u009d|\u00e2\u0080\u009d|“|â€\u009d|†/g, '"').replace(/\u00e2\u20ac\u00a6|…/g, "...").replace(/[\u2012\u2013\u2014\u2015\u2212]/g, "-");
|
|
612
|
+
}
|
|
613
|
+
function normalizeTicketFields(ticket) {
|
|
614
|
+
if (!ticket) return ticket;
|
|
615
|
+
if (ticket.meta) {
|
|
616
|
+
if (ticket.meta.title) ticket.meta.title = normalizeDashesAndMojibake(ticket.meta.title);
|
|
617
|
+
if (ticket.meta.estimatedEffort) ticket.meta.estimatedEffort = normalizeDashesAndMojibake(ticket.meta.estimatedEffort);
|
|
618
|
+
if (ticket.meta.complexity) ticket.meta.complexity = normalizeDashesAndMojibake(ticket.meta.complexity);
|
|
619
|
+
if (ticket.meta.milestone) ticket.meta.milestone = normalizeDashesAndMojibake(ticket.meta.milestone);
|
|
620
|
+
if (ticket.meta.featureFlag) ticket.meta.featureFlag = normalizeDashesAndMojibake(ticket.meta.featureFlag);
|
|
621
|
+
if (ticket.meta.inheritedFeatureFlag) ticket.meta.inheritedFeatureFlag = normalizeDashesAndMojibake(ticket.meta.inheritedFeatureFlag);
|
|
622
|
+
if (ticket.meta.priority) {
|
|
623
|
+
const norm = normalizePriority(ticket.meta.priority);
|
|
624
|
+
if (norm) {
|
|
625
|
+
ticket.meta.priority = norm;
|
|
626
|
+
} else {
|
|
627
|
+
delete ticket.meta.priority;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (ticket.detail) {
|
|
632
|
+
if (ticket.detail.title) ticket.detail.title = normalizeDashesAndMojibake(ticket.detail.title);
|
|
633
|
+
if (ticket.detail.estimatedEffort) ticket.detail.estimatedEffort = normalizeDashesAndMojibake(ticket.detail.estimatedEffort);
|
|
634
|
+
if (ticket.detail.summary) ticket.detail.summary = normalizeDashesAndMojibake(ticket.detail.summary);
|
|
635
|
+
if (ticket.detail.milestone) ticket.detail.milestone = normalizeDashesAndMojibake(ticket.detail.milestone);
|
|
636
|
+
if (ticket.detail.priority) {
|
|
637
|
+
const norm = normalizePriority(ticket.detail.priority);
|
|
638
|
+
if (norm) {
|
|
639
|
+
ticket.detail.priority = norm;
|
|
640
|
+
} else {
|
|
641
|
+
delete ticket.detail.priority;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (ticket.detail.breakdown) ticket.detail.breakdown = ticket.detail.breakdown.map((b) => normalizeDashesAndMojibake(b));
|
|
645
|
+
if (ticket.detail.technicalDetails) ticket.detail.technicalDetails = ticket.detail.technicalDetails.map((t) => normalizeDashesAndMojibake(t));
|
|
646
|
+
if (ticket.detail.openQuestions) ticket.detail.openQuestions = ticket.detail.openQuestions.map((q) => normalizeDashesAndMojibake(q));
|
|
647
|
+
if (ticket.detail.raw) ticket.detail.raw = normalizeDashesAndMojibake(ticket.detail.raw);
|
|
648
|
+
}
|
|
649
|
+
if (ticket.planMarkdown) {
|
|
650
|
+
ticket.planMarkdown = normalizeDashesAndMojibake(ticket.planMarkdown);
|
|
651
|
+
}
|
|
652
|
+
if (ticket.comments) {
|
|
653
|
+
for (const c of ticket.comments) {
|
|
654
|
+
if (c.text) c.text = normalizeDashesAndMojibake(c.text);
|
|
655
|
+
if (c.author) c.author = normalizeDashesAndMojibake(c.author);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return ticket;
|
|
659
|
+
}
|
|
660
|
+
function formatTicketListTable(tickets) {
|
|
661
|
+
if (tickets.length === 0) {
|
|
662
|
+
return `${colors.dim}No tickets found matching criteria.${colors.reset}`;
|
|
663
|
+
}
|
|
664
|
+
const showPriority = tickets.some((t) => Boolean(t.meta.priority));
|
|
665
|
+
const rows = tickets.map((t) => {
|
|
666
|
+
const id = t.projectDescriptor?.code ? `${t.projectDescriptor.code}-${t.meta.id}` : `#${t.meta.id}`;
|
|
667
|
+
const project = t.projectDescriptor?.code || "CORE";
|
|
668
|
+
const type = t.meta.type || t.meta.category || "Feature";
|
|
669
|
+
const priority = t.meta.priority || "-";
|
|
670
|
+
const status = t.meta.status;
|
|
671
|
+
const title = normalizeDashesAndMojibake(t.meta.title) + (t.isBlocked ? ` ${colors.red}[BLOCKED]${colors.reset}` : "");
|
|
672
|
+
return { id, project, type, priority, status, title };
|
|
673
|
+
});
|
|
674
|
+
const idWidth = Math.max(4, ...rows.map((r) => r.id.length));
|
|
675
|
+
const projWidth = Math.max(7, ...rows.map((r) => r.project.length));
|
|
676
|
+
const typeWidth = Math.max(8, ...rows.map((r) => r.type.length));
|
|
677
|
+
const prioWidth = showPriority ? Math.max(8, ...rows.map((r) => r.priority.length)) : 0;
|
|
678
|
+
const statusWidth = Math.max(14, ...rows.map((r) => r.status.length));
|
|
679
|
+
const headerParts = [
|
|
680
|
+
pad("ID", idWidth),
|
|
681
|
+
pad("Project", projWidth),
|
|
682
|
+
pad("Type", typeWidth)
|
|
683
|
+
];
|
|
684
|
+
if (showPriority) headerParts.push(pad("Priority", prioWidth));
|
|
685
|
+
headerParts.push(pad("Status", statusWidth), "Title");
|
|
686
|
+
const header = `${colors.bold}${headerParts.join(" ")}${colors.reset}`;
|
|
687
|
+
const dividerParts = [
|
|
688
|
+
"-".repeat(idWidth),
|
|
689
|
+
"-".repeat(projWidth),
|
|
690
|
+
"-".repeat(typeWidth)
|
|
691
|
+
];
|
|
692
|
+
if (showPriority) dividerParts.push("-".repeat(prioWidth));
|
|
693
|
+
dividerParts.push("-".repeat(statusWidth), "-".repeat(40));
|
|
694
|
+
const divider = `${colors.dim}${dividerParts.join(" ")}${colors.reset}`;
|
|
695
|
+
const formattedRows = rows.map((r) => {
|
|
696
|
+
const statusColored = colorStatus(r.status);
|
|
697
|
+
const typeColored = colorType(r.type);
|
|
698
|
+
const projColored = `${colors.magenta}${r.project}${colors.reset}`;
|
|
699
|
+
const rowParts = [
|
|
700
|
+
`${colors.bold}${pad(r.id, idWidth)}${colors.reset}`,
|
|
701
|
+
pad(projColored, projWidth + (isColorSupported ? colors.magenta.length + colors.reset.length : 0)),
|
|
702
|
+
pad(typeColored, typeWidth + (isColorSupported ? 9 : 0))
|
|
703
|
+
];
|
|
704
|
+
if (showPriority) {
|
|
705
|
+
const prioColored = r.priority === "-" ? `${colors.dim}-${colors.reset}` : colorPriority(r.priority);
|
|
706
|
+
rowParts.push(pad(prioColored, prioWidth + (isColorSupported && r.priority !== "-" ? 9 : isColorSupported ? colors.dim.length + colors.reset.length : 0)));
|
|
707
|
+
}
|
|
708
|
+
rowParts.push(
|
|
709
|
+
pad(statusColored, statusWidth + (isColorSupported ? 9 : 0)),
|
|
710
|
+
r.title
|
|
711
|
+
);
|
|
712
|
+
return rowParts.join(" ");
|
|
713
|
+
});
|
|
714
|
+
return [header, divider, ...formattedRows].join("\n");
|
|
715
|
+
}
|
|
716
|
+
function formatTicketDetail(ticket) {
|
|
717
|
+
const { meta, detail, comments, planMarkdown, projectDescriptor } = ticket;
|
|
718
|
+
const lines = [];
|
|
719
|
+
lines.push(`${colors.bold}${colors.cyan}Ticket #${meta.project ? `${meta.project}-${meta.id}` : meta.id}: ${normalizeDashesAndMojibake(meta.title)}${colors.reset}`);
|
|
720
|
+
lines.push(`${colors.dim}${"=".repeat(60)}${colors.reset}`);
|
|
721
|
+
lines.push(`${colors.bold}Project:${colors.reset} ${projectDescriptor ? `${projectDescriptor.code} - ${projectDescriptor.name}` : "Default"}`);
|
|
722
|
+
lines.push(`${colors.bold}Type:${colors.reset} ${colorType(meta.type || meta.category || "Feature")}`);
|
|
723
|
+
if (meta.priority) {
|
|
724
|
+
lines.push(`${colors.bold}Priority:${colors.reset} ${colorPriority(meta.priority)}`);
|
|
725
|
+
}
|
|
726
|
+
lines.push(`${colors.bold}Status:${colors.reset} ${colorStatus(meta.status)}`);
|
|
727
|
+
lines.push(`${colors.bold}Complexity:${colors.reset} ${normalizeDashesAndMojibake(meta.complexity || "Medium")}`);
|
|
728
|
+
lines.push(`${colors.bold}Effort:${colors.reset} ${normalizeDashesAndMojibake(meta.estimatedEffort || "N/A")}`);
|
|
729
|
+
lines.push(`${colors.bold}Submitted By:${colors.reset} ${meta.submittedBy || "Unknown"}`);
|
|
730
|
+
if (meta.milestone) {
|
|
731
|
+
lines.push(`${colors.bold}Milestone:${colors.reset} ${colors.magenta}${meta.milestone}${colors.reset}`);
|
|
732
|
+
}
|
|
733
|
+
if (meta.featureFlag) {
|
|
734
|
+
const isInherited = meta.inheritedFeatureFlag && meta.inheritedFeatureFlag === meta.featureFlag;
|
|
735
|
+
lines.push(`${colors.bold}Feature Flag:${colors.reset} ${colors.yellow}${meta.featureFlag}${colors.reset}${isInherited ? ` ${colors.dim}(inherited from Milestone)${colors.reset}` : ""}`);
|
|
736
|
+
}
|
|
737
|
+
if (ticket.isBlocked) {
|
|
738
|
+
lines.push(`${colors.bold}Dependency:${colors.reset} ${colors.red}${colors.bold}Blocked by uncompleted ticket${colors.reset}`);
|
|
739
|
+
}
|
|
740
|
+
if (detail?.summary) {
|
|
741
|
+
lines.push("");
|
|
742
|
+
lines.push(`${colors.bold}Summary:${colors.reset}`);
|
|
743
|
+
lines.push(normalizeDashesAndMojibake(detail.summary));
|
|
744
|
+
}
|
|
745
|
+
if (detail?.breakdown && detail.breakdown.length > 0) {
|
|
746
|
+
lines.push("");
|
|
747
|
+
lines.push(`${colors.bold}Feature Breakdown:${colors.reset}`);
|
|
748
|
+
for (const b of detail.breakdown) {
|
|
749
|
+
lines.push(` \u2022 ${normalizeDashesAndMojibake(b)}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (detail?.technicalDetails && detail.technicalDetails.length > 0) {
|
|
753
|
+
lines.push("");
|
|
754
|
+
lines.push(`${colors.bold}Technical Details:${colors.reset}`);
|
|
755
|
+
for (const t of detail.technicalDetails) {
|
|
756
|
+
lines.push(` \u2022 ${normalizeDashesAndMojibake(t)}`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (detail?.openQuestions && detail.openQuestions.length > 0) {
|
|
760
|
+
lines.push("");
|
|
761
|
+
lines.push(`${colors.bold}Open Decisions & Questions:${colors.reset}`);
|
|
762
|
+
for (const q of detail.openQuestions) {
|
|
763
|
+
lines.push(` \u2022 ${normalizeDashesAndMojibake(q)}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (ticket.links && ticket.links.length > 0) {
|
|
767
|
+
lines.push("");
|
|
768
|
+
lines.push(`${colors.bold}Linked Tickets (${ticket.links.length}):${colors.reset}`);
|
|
769
|
+
for (const l of ticket.links) {
|
|
770
|
+
const relFormatted = l.relation === "blocks" ? `${colors.red}Blocks${colors.reset}` : l.relation === "blocked-by" ? `${colors.red}Blocked by${colors.reset}` : l.relation === "parent-of" ? `${colors.magenta}Parent of${colors.reset}` : l.relation === "child-of" ? `${colors.magenta}Child of${colors.reset}` : l.relation === "duplicates" ? `${colors.yellow}Duplicates${colors.reset}` : l.relation === "duplicated-by" ? `${colors.yellow}Duplicated by${colors.reset}` : `${colors.cyan}Relates to${colors.reset}`;
|
|
771
|
+
const blockedTag = l.isBlockedByUncompleted ? ` ${colors.red}[BLOCKED]${colors.reset}` : "";
|
|
772
|
+
const titleSnippet = l.targetTitle ? ` : ${normalizeDashesAndMojibake(l.targetTitle)}` : "";
|
|
773
|
+
const statusSnippet = l.targetStatus ? ` [${l.targetStatus}]` : "";
|
|
774
|
+
lines.push(` \u2022 ${relFormatted} ${colors.bold}${l.targetKey}${colors.reset}${statusSnippet}${titleSnippet}${blockedTag}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (planMarkdown) {
|
|
778
|
+
lines.push("");
|
|
779
|
+
lines.push(`${colors.bold}Implementation Plan:${colors.reset}`);
|
|
780
|
+
lines.push(normalizeDashesAndMojibake(planMarkdown.trim()));
|
|
781
|
+
}
|
|
782
|
+
if (comments && comments.length > 0) {
|
|
783
|
+
lines.push("");
|
|
784
|
+
lines.push(`${colors.bold}Comments (${comments.length}):${colors.reset}`);
|
|
785
|
+
for (const c of comments) {
|
|
786
|
+
lines.push(` ${colors.dim}[${c.timestamp.slice(0, 10)}]${colors.reset} ${colors.bold}${normalizeDashesAndMojibake(c.author)}:${colors.reset} ${normalizeDashesAndMojibake(c.text)}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return lines.join("\n");
|
|
790
|
+
}
|
|
791
|
+
function pad(str, width) {
|
|
792
|
+
return str.padEnd(width, " ");
|
|
793
|
+
}
|
|
794
|
+
function colorStatus(status) {
|
|
795
|
+
switch (status) {
|
|
796
|
+
case "Completed":
|
|
797
|
+
return `${colors.green}${status}${colors.reset}`;
|
|
798
|
+
case "In Development":
|
|
799
|
+
return `${colors.green}${status}${colors.reset}`;
|
|
800
|
+
case "Rejected":
|
|
801
|
+
return `${colors.red}${status}${colors.reset}`;
|
|
802
|
+
default:
|
|
803
|
+
return `${colors.cyan}${status}${colors.reset}`;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function colorType(type) {
|
|
807
|
+
switch (type) {
|
|
808
|
+
case "Feature":
|
|
809
|
+
return `${colors.blue}${type}${colors.reset}`;
|
|
810
|
+
case "Platform":
|
|
811
|
+
return `${colors.magenta}${type}${colors.reset}`;
|
|
812
|
+
case "Tools":
|
|
813
|
+
return `${colors.yellow}${type}${colors.reset}`;
|
|
814
|
+
case "Bug":
|
|
815
|
+
return `${colors.red}${type}${colors.reset}`;
|
|
816
|
+
case "Idea":
|
|
817
|
+
return `${colors.green}${type}${colors.reset}`;
|
|
818
|
+
default:
|
|
819
|
+
return type;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function colorPriority(priority) {
|
|
823
|
+
if (!priority) return "";
|
|
824
|
+
switch (priority.toLowerCase()) {
|
|
825
|
+
case "critical":
|
|
826
|
+
return `${colors.red}${colors.bold}Critical${colors.reset}`;
|
|
827
|
+
case "high":
|
|
828
|
+
return `${colors.yellow}${colors.bold}High${colors.reset}`;
|
|
829
|
+
case "medium":
|
|
830
|
+
return `${colors.blue}Medium${colors.reset}`;
|
|
831
|
+
case "low":
|
|
832
|
+
return `${colors.dim}Low${colors.reset}`;
|
|
833
|
+
default:
|
|
834
|
+
return priority;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
function formatMilestoneListTable(milestones, tickets) {
|
|
838
|
+
if (milestones.length === 0) {
|
|
839
|
+
return `${colors.dim}No milestones found.${colors.reset}`;
|
|
840
|
+
}
|
|
841
|
+
const rows = milestones.map((m) => {
|
|
842
|
+
const id = String(m.id);
|
|
843
|
+
const project = m.project;
|
|
844
|
+
const title = m.title;
|
|
845
|
+
const status = m.status;
|
|
846
|
+
const flag = m.featureFlag || "-";
|
|
847
|
+
let progress = "-";
|
|
848
|
+
if (tickets) {
|
|
849
|
+
const mTickets = tickets.filter(
|
|
850
|
+
(t) => t.meta.milestone && (t.meta.milestone.toLowerCase() === String(m.id).toLowerCase() || t.meta.milestone.toLowerCase() === m.title.toLowerCase())
|
|
851
|
+
);
|
|
852
|
+
const completed = mTickets.filter((t) => t.meta.status === "Completed").length;
|
|
853
|
+
progress = `${completed}/${mTickets.length}`;
|
|
854
|
+
}
|
|
855
|
+
return { id, project, status, flag, progress, title };
|
|
856
|
+
});
|
|
857
|
+
const idWidth = Math.max(4, ...rows.map((r) => r.id.length));
|
|
858
|
+
const projWidth = Math.max(7, ...rows.map((r) => r.project.length));
|
|
859
|
+
const statusWidth = Math.max(10, ...rows.map((r) => r.status.length));
|
|
860
|
+
const flagWidth = Math.max(14, ...rows.map((r) => r.flag.length));
|
|
861
|
+
const progWidth = Math.max(8, ...rows.map((r) => r.progress.length));
|
|
862
|
+
const header = `${colors.bold}${pad("ID", idWidth)} ${pad("Project", projWidth)} ${pad("Status", statusWidth)} ${pad("Umbrella Flag", flagWidth)} ${pad("Progress", progWidth)} Title${colors.reset}`;
|
|
863
|
+
const divider = `${colors.dim}${"-".repeat(idWidth)} ${"-".repeat(projWidth)} ${"-".repeat(statusWidth)} ${"-".repeat(flagWidth)} ${"-".repeat(progWidth)} ${"-".repeat(30)}${colors.reset}`;
|
|
864
|
+
const formattedRows = rows.map((r) => {
|
|
865
|
+
return `${colors.bold}${pad(r.id, idWidth)}${colors.reset} ${pad(r.project, projWidth)} ${pad(r.status, statusWidth)} ${pad(r.flag, flagWidth)} ${pad(r.progress, progWidth)} ${r.title}`;
|
|
866
|
+
});
|
|
867
|
+
return [header, divider, ...formattedRows].join("\n");
|
|
868
|
+
}
|
|
869
|
+
|
|
541
870
|
// src/storage/filesystem.ts
|
|
542
871
|
function isProjectMatch(code, filter) {
|
|
543
872
|
if (!code || !filter) return false;
|
|
@@ -592,6 +921,20 @@ var FilesystemStorageAdapter = class {
|
|
|
592
921
|
}
|
|
593
922
|
}
|
|
594
923
|
refreshConfig() {
|
|
924
|
+
if (this.isConfigExplicit) {
|
|
925
|
+
try {
|
|
926
|
+
const discovered = findEsedreConfig(this.workspaceRoot);
|
|
927
|
+
if (discovered.config && Object.keys(discovered.config).length > 0) {
|
|
928
|
+
this.config = {
|
|
929
|
+
...this.config,
|
|
930
|
+
...discovered.config
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
this.lastConfigCheck = Date.now();
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
595
938
|
try {
|
|
596
939
|
const discovered = findEsedreConfig(this.workspaceRoot);
|
|
597
940
|
this.config = discovered.config || {};
|
|
@@ -1104,86 +1447,647 @@ ${formattedList}`
|
|
|
1104
1447
|
}
|
|
1105
1448
|
return projectDesc;
|
|
1106
1449
|
}
|
|
1107
|
-
|
|
1450
|
+
async updateProject(input) {
|
|
1451
|
+
const cleanCode = input.code.trim();
|
|
1452
|
+
const cleanLower = cleanCode.toLowerCase();
|
|
1453
|
+
if (input.name !== void 0) {
|
|
1454
|
+
const nameVal = validateProjectName(input.name);
|
|
1455
|
+
if (!nameVal.valid) {
|
|
1456
|
+
throw new Error(nameVal.error);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1108
1459
|
const locations = this.resolveProjectLocations();
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1460
|
+
const loc = locations.find((l) => l.project.code.toLowerCase() === cleanLower);
|
|
1461
|
+
if (!loc) {
|
|
1462
|
+
throw new Error(`Project "${cleanCode}" not found`);
|
|
1463
|
+
}
|
|
1464
|
+
const updatedDesc = {
|
|
1465
|
+
...loc.project,
|
|
1466
|
+
...input.name !== void 0 ? { name: input.name.trim() } : {},
|
|
1467
|
+
...input.description !== void 0 ? { description: input.description.trim() } : {},
|
|
1468
|
+
...input.colors !== void 0 ? { colors: input.colors } : {},
|
|
1469
|
+
...input.techStack !== void 0 ? { techStack: input.techStack } : {},
|
|
1470
|
+
...input.groundingRules !== void 0 ? { groundingRules: input.groundingRules } : {},
|
|
1471
|
+
...input.guidelinesRef !== void 0 ? { guidelinesRef: input.guidelinesRef } : {}
|
|
1472
|
+
};
|
|
1473
|
+
if (loc.sourceType === "hub" && loc.hubDir) {
|
|
1474
|
+
const projectDir = path3.dirname(loc.ticketsDir);
|
|
1475
|
+
const pJsonPath = path3.join(projectDir, "project.json");
|
|
1476
|
+
let existingPJson = {};
|
|
1477
|
+
if (fs3.existsSync(pJsonPath)) {
|
|
1478
|
+
try {
|
|
1479
|
+
existingPJson = JSON.parse(fs3.readFileSync(pJsonPath, "utf-8"));
|
|
1480
|
+
} catch {
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
const mergedPJson = { ...existingPJson, ...updatedDesc };
|
|
1484
|
+
writeSafeFile(pJsonPath, JSON.stringify(mergedPJson, null, 2) + "\n");
|
|
1485
|
+
const hubProjectsFile = path3.join(loc.hubDir, "projects.json");
|
|
1486
|
+
const subHubProjectsFile = path3.join(loc.hubDir, "projects", "projects.json");
|
|
1487
|
+
for (const f of [hubProjectsFile, subHubProjectsFile]) {
|
|
1488
|
+
if (fs3.existsSync(f)) {
|
|
1121
1489
|
try {
|
|
1122
|
-
const
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
if (mProj && mProj !== code && mProj !== loc.project.code.toLowerCase()) {
|
|
1128
|
-
return null;
|
|
1490
|
+
const arr = JSON.parse(fs3.readFileSync(f, "utf-8"));
|
|
1491
|
+
const idx = arr.findIndex((p) => p.code.toLowerCase() === cleanLower);
|
|
1492
|
+
if (idx >= 0) {
|
|
1493
|
+
arr[idx] = { ...arr[idx], ...updatedDesc };
|
|
1494
|
+
writeSafeFile(f, JSON.stringify(arr, null, 2) + "\n");
|
|
1129
1495
|
}
|
|
1130
1496
|
} catch {
|
|
1131
1497
|
}
|
|
1132
1498
|
}
|
|
1133
|
-
return { loc, ticketDir, id: num2 };
|
|
1134
1499
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1500
|
+
} else {
|
|
1501
|
+
const projectDir = loc.sourceType === "federated" ? path3.dirname(loc.ticketsDir) : path3.join(this.workspaceRoot, ".esedre");
|
|
1502
|
+
const candidates = [
|
|
1503
|
+
path3.join(projectDir, "project.json"),
|
|
1504
|
+
path3.join(this.workspaceRoot, ".esedre", "project.json"),
|
|
1505
|
+
path3.join(this.workspaceRoot, "project.json")
|
|
1506
|
+
];
|
|
1507
|
+
for (const pJsonPath of candidates) {
|
|
1508
|
+
if (fs3.existsSync(pJsonPath)) {
|
|
1509
|
+
try {
|
|
1510
|
+
const existing = JSON.parse(fs3.readFileSync(pJsonPath, "utf-8"));
|
|
1511
|
+
const merged = { ...existing, ...updatedDesc };
|
|
1512
|
+
writeSafeFile(pJsonPath, JSON.stringify(merged, null, 2) + "\n");
|
|
1513
|
+
} catch {
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
const pJsonListCandidates = [
|
|
1518
|
+
path3.join(projectDir, "projects.json"),
|
|
1519
|
+
path3.join(this.workspaceRoot, ".esedre", "projects.json"),
|
|
1520
|
+
path3.join(this.workspaceRoot, "projects.json")
|
|
1521
|
+
];
|
|
1522
|
+
for (const projectsJsonPath of pJsonListCandidates) {
|
|
1523
|
+
if (fs3.existsSync(projectsJsonPath)) {
|
|
1524
|
+
try {
|
|
1525
|
+
const arr = JSON.parse(fs3.readFileSync(projectsJsonPath, "utf-8"));
|
|
1526
|
+
const idx = arr.findIndex((p) => p.code.toLowerCase() === cleanLower);
|
|
1527
|
+
if (idx >= 0) {
|
|
1528
|
+
arr[idx] = { ...arr[idx], ...updatedDesc };
|
|
1529
|
+
writeSafeFile(projectsJsonPath, JSON.stringify(arr, null, 2) + "\n");
|
|
1530
|
+
}
|
|
1531
|
+
} catch {
|
|
1532
|
+
}
|
|
1157
1533
|
}
|
|
1158
|
-
} else {
|
|
1159
|
-
matches.push({ loc, ticketDir, id: num });
|
|
1160
1534
|
}
|
|
1161
1535
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
)
|
|
1536
|
+
this.refreshConfig();
|
|
1537
|
+
return updatedDesc;
|
|
1538
|
+
}
|
|
1539
|
+
async renameProjectCode(input) {
|
|
1540
|
+
const cleanOld = input.oldCode.trim();
|
|
1541
|
+
const cleanNew = input.newCode.trim();
|
|
1542
|
+
const oldLower = cleanOld.toLowerCase();
|
|
1543
|
+
const newLower = cleanNew.toLowerCase();
|
|
1544
|
+
const codeVal = validateProjectCode(cleanNew);
|
|
1545
|
+
if (!codeVal.valid) {
|
|
1546
|
+
throw new Error(codeVal.error);
|
|
1547
|
+
}
|
|
1548
|
+
if (input.newName !== void 0) {
|
|
1549
|
+
const nameVal = validateProjectName(input.newName);
|
|
1550
|
+
if (!nameVal.valid) {
|
|
1551
|
+
throw new Error(nameVal.error);
|
|
1552
|
+
}
|
|
1177
1553
|
}
|
|
1178
|
-
return null;
|
|
1179
|
-
}
|
|
1180
|
-
async listTickets(filter) {
|
|
1181
1554
|
const locations = this.resolveProjectLocations();
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1555
|
+
const loc = locations.find((l) => l.project.code.toLowerCase() === oldLower);
|
|
1556
|
+
if (!loc) {
|
|
1557
|
+
throw new Error(`Project "${cleanOld}" not found`);
|
|
1558
|
+
}
|
|
1559
|
+
if (oldLower !== newLower) {
|
|
1560
|
+
const collision = locations.find((l) => l.project.code.toLowerCase() === newLower);
|
|
1561
|
+
if (collision) {
|
|
1562
|
+
throw new Error(`Project code "${cleanNew}" collides with existing project "${collision.project.code}".`);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
let migratedTicketsCount = 0;
|
|
1566
|
+
const updatedHubs = [];
|
|
1567
|
+
const updatedConfigs = [];
|
|
1568
|
+
const updatedDesc = {
|
|
1569
|
+
...loc.project,
|
|
1570
|
+
code: cleanNew,
|
|
1571
|
+
...input.newName ? { name: input.newName.trim() } : {}
|
|
1572
|
+
};
|
|
1573
|
+
if (loc.sourceType === "hub" && loc.hubDir) {
|
|
1574
|
+
const hubDir = loc.hubDir;
|
|
1575
|
+
const oldProjectDir = path3.dirname(loc.ticketsDir);
|
|
1576
|
+
const hubProjectsDir = path3.dirname(oldProjectDir);
|
|
1577
|
+
const newProjectDir = path3.join(hubProjectsDir, cleanNew);
|
|
1578
|
+
if (oldProjectDir.toLowerCase() === newProjectDir.toLowerCase()) {
|
|
1579
|
+
if (path3.basename(oldProjectDir) !== cleanNew) {
|
|
1580
|
+
const tempHop = `${oldProjectDir}__rename_hop_${Date.now()}`;
|
|
1581
|
+
fs3.renameSync(oldProjectDir, tempHop);
|
|
1582
|
+
fs3.renameSync(tempHop, newProjectDir);
|
|
1583
|
+
}
|
|
1584
|
+
} else {
|
|
1585
|
+
if (fs3.existsSync(newProjectDir)) {
|
|
1586
|
+
throw new Error(`Target directory "${newProjectDir}" already exists.`);
|
|
1587
|
+
}
|
|
1588
|
+
fs3.renameSync(oldProjectDir, newProjectDir);
|
|
1589
|
+
}
|
|
1590
|
+
const pJsonPath = path3.join(newProjectDir, "project.json");
|
|
1591
|
+
let existingPJson = {};
|
|
1592
|
+
if (fs3.existsSync(pJsonPath)) {
|
|
1593
|
+
try {
|
|
1594
|
+
existingPJson = JSON.parse(fs3.readFileSync(pJsonPath, "utf-8"));
|
|
1595
|
+
} catch {
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
const mergedPJson = { ...existingPJson, ...updatedDesc, code: cleanNew };
|
|
1599
|
+
writeSafeFile(pJsonPath, JSON.stringify(mergedPJson, null, 2) + "\n");
|
|
1600
|
+
const hubProjectsFile = path3.join(hubDir, "projects.json");
|
|
1601
|
+
const subHubProjectsFile = path3.join(hubDir, "projects", "projects.json");
|
|
1602
|
+
for (const f of [hubProjectsFile, subHubProjectsFile]) {
|
|
1603
|
+
if (fs3.existsSync(f)) {
|
|
1604
|
+
try {
|
|
1605
|
+
const arr = JSON.parse(fs3.readFileSync(f, "utf-8"));
|
|
1606
|
+
const idx = arr.findIndex((p) => p.code.toLowerCase() === oldLower);
|
|
1607
|
+
if (idx >= 0) {
|
|
1608
|
+
arr[idx] = { ...arr[idx], ...updatedDesc, code: cleanNew };
|
|
1609
|
+
writeSafeFile(f, JSON.stringify(arr, null, 2) + "\n");
|
|
1610
|
+
if (!updatedHubs.includes(f)) updatedHubs.push(f);
|
|
1611
|
+
}
|
|
1612
|
+
} catch {
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
const newTicketsDir = path3.join(newProjectDir, "tickets");
|
|
1617
|
+
if (fs3.existsSync(newTicketsDir)) {
|
|
1618
|
+
try {
|
|
1619
|
+
const entries = fs3.readdirSync(newTicketsDir, { withFileTypes: true });
|
|
1620
|
+
for (const ent of entries) {
|
|
1621
|
+
if (!ent.isDirectory()) continue;
|
|
1622
|
+
const tMetaPath = path3.join(newTicketsDir, ent.name, "meta.json");
|
|
1623
|
+
if (fs3.existsSync(tMetaPath)) {
|
|
1624
|
+
try {
|
|
1625
|
+
const meta = JSON.parse(fs3.readFileSync(tMetaPath, "utf-8"));
|
|
1626
|
+
meta.project = cleanNew;
|
|
1627
|
+
if (Array.isArray(meta.links)) {
|
|
1628
|
+
for (const l of meta.links) {
|
|
1629
|
+
if (l.targetProject && l.targetProject.toLowerCase() === oldLower) {
|
|
1630
|
+
l.targetProject = cleanNew;
|
|
1631
|
+
l.targetKey = `${cleanNew}-${l.targetId}`;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
writeSafeFile(tMetaPath, JSON.stringify(meta, null, 2) + "\n");
|
|
1636
|
+
migratedTicketsCount++;
|
|
1637
|
+
} catch {
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
} catch {
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
for (const otherLoc of locations) {
|
|
1645
|
+
if (otherLoc.project.code.toLowerCase() === oldLower) continue;
|
|
1646
|
+
if (fs3.existsSync(otherLoc.ticketsDir)) {
|
|
1647
|
+
try {
|
|
1648
|
+
const entries = fs3.readdirSync(otherLoc.ticketsDir, { withFileTypes: true });
|
|
1649
|
+
for (const ent of entries) {
|
|
1650
|
+
if (!ent.isDirectory()) continue;
|
|
1651
|
+
const tMetaPath = path3.join(otherLoc.ticketsDir, ent.name, "meta.json");
|
|
1652
|
+
if (fs3.existsSync(tMetaPath)) {
|
|
1653
|
+
try {
|
|
1654
|
+
const meta = JSON.parse(fs3.readFileSync(tMetaPath, "utf-8"));
|
|
1655
|
+
if (Array.isArray(meta.links)) {
|
|
1656
|
+
let changed = false;
|
|
1657
|
+
for (const l of meta.links) {
|
|
1658
|
+
if (l.targetProject && l.targetProject.toLowerCase() === oldLower) {
|
|
1659
|
+
l.targetProject = cleanNew;
|
|
1660
|
+
l.targetKey = `${cleanNew}-${l.targetId}`;
|
|
1661
|
+
changed = true;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (changed) {
|
|
1665
|
+
writeSafeFile(tMetaPath, JSON.stringify(meta, null, 2) + "\n");
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
} catch {
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
} catch {
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
const newMilestonesPath = path3.join(newProjectDir, "milestones.json");
|
|
1677
|
+
if (fs3.existsSync(newMilestonesPath)) {
|
|
1678
|
+
try {
|
|
1679
|
+
const arr = JSON.parse(fs3.readFileSync(newMilestonesPath, "utf-8"));
|
|
1680
|
+
for (const m of arr) {
|
|
1681
|
+
m.project = cleanNew;
|
|
1682
|
+
}
|
|
1683
|
+
writeSafeFile(newMilestonesPath, JSON.stringify(arr, null, 2) + "\n");
|
|
1684
|
+
} catch {
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
} else {
|
|
1688
|
+
const esedreDir = path3.join(this.workspaceRoot, ".esedre");
|
|
1689
|
+
const pJsonPath = path3.join(esedreDir, "project.json");
|
|
1690
|
+
if (fs3.existsSync(pJsonPath)) {
|
|
1691
|
+
try {
|
|
1692
|
+
const existing = JSON.parse(fs3.readFileSync(pJsonPath, "utf-8"));
|
|
1693
|
+
writeSafeFile(pJsonPath, JSON.stringify({ ...existing, ...updatedDesc, code: cleanNew }, null, 2) + "\n");
|
|
1694
|
+
} catch {
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
const projectsJsonPath = path3.join(esedreDir, "projects.json");
|
|
1698
|
+
if (fs3.existsSync(projectsJsonPath)) {
|
|
1699
|
+
try {
|
|
1700
|
+
const arr = JSON.parse(fs3.readFileSync(projectsJsonPath, "utf-8"));
|
|
1701
|
+
const idx = arr.findIndex((p) => p.code.toLowerCase() === oldLower);
|
|
1702
|
+
if (idx >= 0) {
|
|
1703
|
+
arr[idx] = { ...arr[idx], ...updatedDesc, code: cleanNew };
|
|
1704
|
+
writeSafeFile(projectsJsonPath, JSON.stringify(arr, null, 2) + "\n");
|
|
1705
|
+
}
|
|
1706
|
+
} catch {
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
if (fs3.existsSync(loc.ticketsDir)) {
|
|
1710
|
+
try {
|
|
1711
|
+
const entries = fs3.readdirSync(loc.ticketsDir, { withFileTypes: true });
|
|
1712
|
+
for (const ent of entries) {
|
|
1713
|
+
if (!ent.isDirectory()) continue;
|
|
1714
|
+
const tMetaPath = path3.join(loc.ticketsDir, ent.name, "meta.json");
|
|
1715
|
+
if (fs3.existsSync(tMetaPath)) {
|
|
1716
|
+
try {
|
|
1717
|
+
const meta = JSON.parse(fs3.readFileSync(tMetaPath, "utf-8"));
|
|
1718
|
+
meta.project = cleanNew;
|
|
1719
|
+
if (Array.isArray(meta.links)) {
|
|
1720
|
+
for (const l of meta.links) {
|
|
1721
|
+
if (l.targetProject && l.targetProject.toLowerCase() === oldLower) {
|
|
1722
|
+
l.targetProject = cleanNew;
|
|
1723
|
+
l.targetKey = `${cleanNew}-${l.targetId}`;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
writeSafeFile(tMetaPath, JSON.stringify(meta, null, 2) + "\n");
|
|
1728
|
+
migratedTicketsCount++;
|
|
1729
|
+
} catch {
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
} catch {
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
const milestonesJsonPath = path3.join(esedreDir, "milestones.json");
|
|
1737
|
+
if (fs3.existsSync(milestonesJsonPath)) {
|
|
1738
|
+
try {
|
|
1739
|
+
const arr = JSON.parse(fs3.readFileSync(milestonesJsonPath, "utf-8"));
|
|
1740
|
+
for (const m of arr) {
|
|
1741
|
+
if (m.project.toLowerCase() === oldLower) {
|
|
1742
|
+
m.project = cleanNew;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
writeSafeFile(milestonesJsonPath, JSON.stringify(arr, null, 2) + "\n");
|
|
1746
|
+
} catch {
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
const workspaceConfigFile = path3.join(this.workspaceRoot, ".esedre", "esedre.json");
|
|
1751
|
+
if (fs3.existsSync(workspaceConfigFile)) {
|
|
1752
|
+
try {
|
|
1753
|
+
const wsCfg = JSON.parse(fs3.readFileSync(workspaceConfigFile, "utf-8"));
|
|
1754
|
+
let modified = false;
|
|
1755
|
+
if (wsCfg.projectCode && wsCfg.projectCode.toLowerCase() === oldLower) {
|
|
1756
|
+
wsCfg.projectCode = cleanNew;
|
|
1757
|
+
if (input.newName) wsCfg.projectName = input.newName.trim();
|
|
1758
|
+
modified = true;
|
|
1759
|
+
}
|
|
1760
|
+
if (Array.isArray(wsCfg.allowedProjects)) {
|
|
1761
|
+
const idx = wsCfg.allowedProjects.findIndex((p) => p.toLowerCase() === oldLower);
|
|
1762
|
+
if (idx >= 0) {
|
|
1763
|
+
wsCfg.allowedProjects[idx] = cleanNew;
|
|
1764
|
+
modified = true;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
if (modified) {
|
|
1768
|
+
writeSafeFile(workspaceConfigFile, JSON.stringify(wsCfg, null, 2) + "\n");
|
|
1769
|
+
updatedConfigs.push(workspaceConfigFile);
|
|
1770
|
+
}
|
|
1771
|
+
} catch {
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
const globalConfigFile = path3.join(os2.homedir(), ".esedre", "config.json");
|
|
1775
|
+
if (fs3.existsSync(globalConfigFile)) {
|
|
1776
|
+
try {
|
|
1777
|
+
const gCfg = JSON.parse(fs3.readFileSync(globalConfigFile, "utf-8"));
|
|
1778
|
+
let modified = false;
|
|
1779
|
+
if (gCfg.projects && typeof gCfg.projects === "object") {
|
|
1780
|
+
for (const [key, val] of Object.entries(gCfg.projects)) {
|
|
1781
|
+
if (key.toLowerCase() === oldLower) {
|
|
1782
|
+
delete gCfg.projects[key];
|
|
1783
|
+
gCfg.projects[cleanNew] = val;
|
|
1784
|
+
modified = true;
|
|
1785
|
+
break;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
if (modified) {
|
|
1790
|
+
writeSafeFile(globalConfigFile, JSON.stringify(gCfg, null, 2) + "\n");
|
|
1791
|
+
updatedConfigs.push(globalConfigFile);
|
|
1792
|
+
}
|
|
1793
|
+
} catch {
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
if (this.config.projectCode && this.config.projectCode.toLowerCase() === oldLower) {
|
|
1797
|
+
this.config.projectCode = cleanNew;
|
|
1798
|
+
if (input.newName) this.config.projectName = input.newName.trim();
|
|
1799
|
+
}
|
|
1800
|
+
if (Array.isArray(this.config.allowedProjects)) {
|
|
1801
|
+
const idx = this.config.allowedProjects.findIndex((p) => p.toLowerCase() === oldLower);
|
|
1802
|
+
if (idx >= 0) {
|
|
1803
|
+
this.config.allowedProjects[idx] = cleanNew;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
this.refreshConfig();
|
|
1807
|
+
return {
|
|
1808
|
+
success: true,
|
|
1809
|
+
oldCode: cleanOld,
|
|
1810
|
+
newCode: cleanNew,
|
|
1811
|
+
project: updatedDesc,
|
|
1812
|
+
migratedTicketsCount,
|
|
1813
|
+
updatedHubs,
|
|
1814
|
+
updatedConfigs
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
getMilestonesPath(loc) {
|
|
1818
|
+
const parent = path3.dirname(loc.ticketsDir);
|
|
1819
|
+
return path3.join(parent, "milestones.json");
|
|
1820
|
+
}
|
|
1821
|
+
readProjectMilestones(loc) {
|
|
1822
|
+
const mPath = this.getMilestonesPath(loc);
|
|
1823
|
+
if (!fs3.existsSync(mPath)) return [];
|
|
1824
|
+
try {
|
|
1825
|
+
const raw = fs3.readFileSync(mPath, "utf-8");
|
|
1826
|
+
const parsed = JSON.parse(raw);
|
|
1827
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1828
|
+
} catch {
|
|
1829
|
+
return [];
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
writeProjectMilestones(loc, milestones) {
|
|
1833
|
+
const mPath = this.getMilestonesPath(loc);
|
|
1834
|
+
writeSafeFile(mPath, JSON.stringify(milestones, null, 2) + "\n");
|
|
1835
|
+
}
|
|
1836
|
+
resolveMilestone(milestoneRef, localLoc) {
|
|
1837
|
+
const cleanRef = normalizeDashesAndMojibake(milestoneRef).trim();
|
|
1838
|
+
if (!cleanRef) return null;
|
|
1839
|
+
const locations = this.resolveProjectLocations();
|
|
1840
|
+
const colonIdx = cleanRef.indexOf(":");
|
|
1841
|
+
if (colonIdx > 0) {
|
|
1842
|
+
const projPrefix = cleanRef.substring(0, colonIdx).trim();
|
|
1843
|
+
const targetRef = cleanRef.substring(colonIdx + 1).trim();
|
|
1844
|
+
const targetLoc = locations.find((l) => isProjectMatch(l.project.code, projPrefix));
|
|
1845
|
+
if (targetLoc) {
|
|
1846
|
+
const pMilestones = this.readProjectMilestones(targetLoc);
|
|
1847
|
+
const match = pMilestones.find(
|
|
1848
|
+
(m) => String(m.id).toLowerCase() === targetRef.toLowerCase() || m.title.toLowerCase() === targetRef.toLowerCase()
|
|
1849
|
+
);
|
|
1850
|
+
if (match) return { ...match, project: targetLoc.project.code };
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
if (localLoc) {
|
|
1854
|
+
const localMilestones = this.readProjectMilestones(localLoc);
|
|
1855
|
+
const match = localMilestones.find(
|
|
1856
|
+
(m) => String(m.id).toLowerCase() === cleanRef.toLowerCase() || m.title.toLowerCase() === cleanRef.toLowerCase()
|
|
1857
|
+
);
|
|
1858
|
+
if (match) return { ...match, project: localLoc.project.code };
|
|
1859
|
+
}
|
|
1860
|
+
for (const loc of locations) {
|
|
1861
|
+
if (localLoc && isProjectMatch(loc.project.code, localLoc.project.code)) continue;
|
|
1862
|
+
const list = this.readProjectMilestones(loc);
|
|
1863
|
+
const match = list.find(
|
|
1864
|
+
(m) => String(m.id).toLowerCase() === cleanRef.toLowerCase() || m.title.toLowerCase() === cleanRef.toLowerCase()
|
|
1865
|
+
);
|
|
1866
|
+
if (match) return { ...match, project: loc.project.code };
|
|
1867
|
+
}
|
|
1868
|
+
return null;
|
|
1869
|
+
}
|
|
1870
|
+
async listMilestones(projectCode) {
|
|
1871
|
+
const locations = this.resolveProjectLocations();
|
|
1872
|
+
const milestones = [];
|
|
1873
|
+
for (const loc of locations) {
|
|
1874
|
+
if (projectCode && projectCode !== "all") {
|
|
1875
|
+
if (!isProjectMatch(loc.project.code, projectCode)) continue;
|
|
1876
|
+
}
|
|
1877
|
+
const list = this.readProjectMilestones(loc);
|
|
1878
|
+
for (const m of list) {
|
|
1879
|
+
milestones.push({
|
|
1880
|
+
...m,
|
|
1881
|
+
project: loc.project.code
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
return milestones.sort((a, b) => {
|
|
1886
|
+
if (a.project !== b.project) return a.project.localeCompare(b.project);
|
|
1887
|
+
return (Number(a.id) || 0) - (Number(b.id) || 0);
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
async getMilestone(id, projectCode) {
|
|
1891
|
+
const target = String(id).trim();
|
|
1892
|
+
const colonIdx = target.indexOf(":");
|
|
1893
|
+
let effectiveProject = projectCode;
|
|
1894
|
+
let effectiveId = target;
|
|
1895
|
+
if (colonIdx > 0 && (!projectCode || projectCode === "all")) {
|
|
1896
|
+
effectiveProject = target.substring(0, colonIdx).trim();
|
|
1897
|
+
effectiveId = target.substring(colonIdx + 1).trim();
|
|
1898
|
+
}
|
|
1899
|
+
const all = await this.listMilestones(effectiveProject);
|
|
1900
|
+
const targetLower = effectiveId.toLowerCase();
|
|
1901
|
+
const found = all.find(
|
|
1902
|
+
(m) => String(m.id).toLowerCase() === targetLower || m.title.toLowerCase() === targetLower
|
|
1903
|
+
);
|
|
1904
|
+
return found || null;
|
|
1905
|
+
}
|
|
1906
|
+
async createMilestone(input) {
|
|
1907
|
+
const locations = this.resolveProjectLocations();
|
|
1908
|
+
const projectCode = input.projectCode || this.config.projectCode;
|
|
1909
|
+
let targetLoc;
|
|
1910
|
+
if (projectCode) {
|
|
1911
|
+
targetLoc = locations.find((l) => isProjectMatch(l.project.code, projectCode));
|
|
1912
|
+
} else if (locations.length === 1) {
|
|
1913
|
+
targetLoc = locations[0];
|
|
1914
|
+
}
|
|
1915
|
+
if (!targetLoc) {
|
|
1916
|
+
throw new Error("Project is required to create a milestone.");
|
|
1917
|
+
}
|
|
1918
|
+
const existing = this.readProjectMilestones(targetLoc);
|
|
1919
|
+
const existingIds = existing.map((m) => typeof m.id === "number" ? m.id : parseInt(String(m.id), 10)).filter((n) => !isNaN(n));
|
|
1920
|
+
const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1;
|
|
1921
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1922
|
+
const title = normalizeDashesAndMojibake(input.title).trim();
|
|
1923
|
+
if (!title) {
|
|
1924
|
+
throw new Error("Milestone title is required.");
|
|
1925
|
+
}
|
|
1926
|
+
const milestone = {
|
|
1927
|
+
id: nextId,
|
|
1928
|
+
project: targetLoc.project.code,
|
|
1929
|
+
title,
|
|
1930
|
+
description: input.description ? normalizeDashesAndMojibake(input.description).trim() : void 0,
|
|
1931
|
+
status: input.status || "Planned",
|
|
1932
|
+
featureFlag: input.featureFlag ? normalizeDashesAndMojibake(input.featureFlag).trim() : void 0,
|
|
1933
|
+
targetDate: input.targetDate ? normalizeDashesAndMojibake(input.targetDate).trim() : void 0,
|
|
1934
|
+
createdAt: now,
|
|
1935
|
+
updatedAt: now
|
|
1936
|
+
};
|
|
1937
|
+
existing.push(milestone);
|
|
1938
|
+
this.writeProjectMilestones(targetLoc, existing);
|
|
1939
|
+
return milestone;
|
|
1940
|
+
}
|
|
1941
|
+
async updateMilestone(id, input, projectCode) {
|
|
1942
|
+
const locations = this.resolveProjectLocations();
|
|
1943
|
+
const targetId = String(id).trim().toLowerCase();
|
|
1944
|
+
let foundLoc;
|
|
1945
|
+
let foundIndex = -1;
|
|
1946
|
+
for (const loc of locations) {
|
|
1947
|
+
if (projectCode && projectCode !== "all" && !isProjectMatch(loc.project.code, projectCode)) continue;
|
|
1948
|
+
const list2 = this.readProjectMilestones(loc);
|
|
1949
|
+
const idx = list2.findIndex(
|
|
1950
|
+
(m2) => String(m2.id).toLowerCase() === targetId || m2.title.toLowerCase() === targetId
|
|
1951
|
+
);
|
|
1952
|
+
if (idx >= 0) {
|
|
1953
|
+
foundLoc = loc;
|
|
1954
|
+
foundIndex = idx;
|
|
1955
|
+
break;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
if (!foundLoc || foundIndex < 0) {
|
|
1959
|
+
throw new Error(`Milestone '${id}' not found.`);
|
|
1960
|
+
}
|
|
1961
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1962
|
+
const list = this.readProjectMilestones(foundLoc);
|
|
1963
|
+
const m = list[foundIndex];
|
|
1964
|
+
if (input.title !== void 0) m.title = normalizeDashesAndMojibake(input.title).trim();
|
|
1965
|
+
if (input.description !== void 0) {
|
|
1966
|
+
m.description = input.description ? normalizeDashesAndMojibake(input.description).trim() : void 0;
|
|
1967
|
+
}
|
|
1968
|
+
if (input.status !== void 0) {
|
|
1969
|
+
m.status = input.status;
|
|
1970
|
+
if (input.status === "Completed" && !m.completedAt) {
|
|
1971
|
+
m.completedAt = now;
|
|
1972
|
+
} else if (input.status !== "Completed") {
|
|
1973
|
+
delete m.completedAt;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
if (input.featureFlag !== void 0) {
|
|
1977
|
+
if (input.featureFlag === null || input.featureFlag === "" || input.featureFlag.toLowerCase() === "none") {
|
|
1978
|
+
delete m.featureFlag;
|
|
1979
|
+
} else {
|
|
1980
|
+
m.featureFlag = normalizeDashesAndMojibake(input.featureFlag).trim();
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
if (input.targetDate !== void 0) {
|
|
1984
|
+
if (input.targetDate === null || input.targetDate === "" || input.targetDate.toLowerCase() === "none") {
|
|
1985
|
+
delete m.targetDate;
|
|
1986
|
+
} else {
|
|
1987
|
+
m.targetDate = normalizeDashesAndMojibake(input.targetDate).trim();
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
m.updatedAt = now;
|
|
1991
|
+
m.project = foundLoc.project.code;
|
|
1992
|
+
this.writeProjectMilestones(foundLoc, list);
|
|
1993
|
+
return m;
|
|
1994
|
+
}
|
|
1995
|
+
async deleteMilestone(id, projectCode) {
|
|
1996
|
+
const locations = this.resolveProjectLocations();
|
|
1997
|
+
const targetId = String(id).trim().toLowerCase();
|
|
1998
|
+
for (const loc of locations) {
|
|
1999
|
+
if (projectCode && projectCode !== "all" && !isProjectMatch(loc.project.code, projectCode)) continue;
|
|
2000
|
+
const list = this.readProjectMilestones(loc);
|
|
2001
|
+
const filtered = list.filter(
|
|
2002
|
+
(m) => String(m.id).toLowerCase() !== targetId && m.title.toLowerCase() !== targetId
|
|
2003
|
+
);
|
|
2004
|
+
if (filtered.length !== list.length) {
|
|
2005
|
+
this.writeProjectMilestones(loc, filtered);
|
|
2006
|
+
return true;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
return false;
|
|
2010
|
+
}
|
|
2011
|
+
findTicketLocation(id) {
|
|
2012
|
+
const locations = this.resolveProjectLocations();
|
|
2013
|
+
if (locations.length === 0) return null;
|
|
2014
|
+
const strId = String(id).trim();
|
|
2015
|
+
const prefixMatch = strId.match(/^([a-zA-Z0-9]{1,8})[-_:](\d+)$/i);
|
|
2016
|
+
if (prefixMatch) {
|
|
2017
|
+
const code = prefixMatch[1].toLowerCase();
|
|
2018
|
+
const num2 = parseInt(prefixMatch[2], 10);
|
|
2019
|
+
const loc = locations.find((l) => isProjectMatch(l.project.code, code));
|
|
2020
|
+
if (!loc) return null;
|
|
2021
|
+
const ticketDir = path3.join(loc.ticketsDir, String(num2));
|
|
2022
|
+
const metaPath = path3.join(ticketDir, "meta.json");
|
|
2023
|
+
if (fs3.existsSync(metaPath)) {
|
|
2024
|
+
if (loc.isSharedDir) {
|
|
2025
|
+
try {
|
|
2026
|
+
const metaRaw = fs3.readFileSync(metaPath, "utf-8");
|
|
2027
|
+
const meta = JSON.parse(metaRaw);
|
|
2028
|
+
meta.type = meta.type || meta.category || "Feature";
|
|
2029
|
+
meta.category = meta.type;
|
|
2030
|
+
const mProj = (meta.project || "").toLowerCase();
|
|
2031
|
+
if (mProj && mProj !== code && mProj !== loc.project.code.toLowerCase()) {
|
|
2032
|
+
return null;
|
|
2033
|
+
}
|
|
2034
|
+
} catch {
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
return { loc, ticketDir, id: num2 };
|
|
2038
|
+
}
|
|
2039
|
+
return null;
|
|
2040
|
+
}
|
|
2041
|
+
const num = typeof id === "number" ? id : parseInt(strId, 10);
|
|
2042
|
+
if (isNaN(num)) return null;
|
|
2043
|
+
const matches = [];
|
|
2044
|
+
const checkedDirs = /* @__PURE__ */ new Set();
|
|
2045
|
+
for (const loc of locations) {
|
|
2046
|
+
const ticketDir = path3.join(loc.ticketsDir, String(num));
|
|
2047
|
+
const metaPath = path3.join(ticketDir, "meta.json");
|
|
2048
|
+
if (!fs3.existsSync(metaPath)) continue;
|
|
2049
|
+
if (loc.isSharedDir) {
|
|
2050
|
+
if (checkedDirs.has(ticketDir)) continue;
|
|
2051
|
+
checkedDirs.add(ticketDir);
|
|
2052
|
+
try {
|
|
2053
|
+
const metaRaw = fs3.readFileSync(metaPath, "utf-8");
|
|
2054
|
+
const meta = JSON.parse(metaRaw);
|
|
2055
|
+
const matchedLoc = locations.find(
|
|
2056
|
+
(l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && l.project.code.toLowerCase() === meta.project.toLowerCase()
|
|
2057
|
+
) || loc;
|
|
2058
|
+
matches.push({ loc: matchedLoc, ticketDir, id: num });
|
|
2059
|
+
} catch {
|
|
2060
|
+
matches.push({ loc, ticketDir, id: num });
|
|
2061
|
+
}
|
|
2062
|
+
} else {
|
|
2063
|
+
matches.push({ loc, ticketDir, id: num });
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
if (this.config.projectCode) {
|
|
2067
|
+
const prefLower = this.config.projectCode.toLowerCase();
|
|
2068
|
+
const preferredMatch = matches.find(
|
|
2069
|
+
(m) => m.loc.project.code.toLowerCase() === prefLower
|
|
2070
|
+
);
|
|
2071
|
+
if (preferredMatch) return preferredMatch;
|
|
2072
|
+
}
|
|
2073
|
+
if (matches.length === 1) {
|
|
2074
|
+
return matches[0];
|
|
2075
|
+
}
|
|
2076
|
+
if (matches.length > 1) {
|
|
2077
|
+
const codes = matches.map((m) => m.loc.project.code);
|
|
2078
|
+
throw new Error(
|
|
2079
|
+
`Ambiguous ticket #${num}: exists in multiple projects (${codes.join(", ")}). Please specify as <ProjectCode>-${num} (e.g. ${codes[0]}-${num}).`
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
return null;
|
|
2083
|
+
}
|
|
2084
|
+
async listTickets(filter) {
|
|
2085
|
+
const locations = this.resolveProjectLocations();
|
|
2086
|
+
const tickets = [];
|
|
2087
|
+
const processedSharedTickets = /* @__PURE__ */ new Set();
|
|
2088
|
+
for (const loc of locations) {
|
|
2089
|
+
if (filter?.project && filter.project !== "all") {
|
|
2090
|
+
const pLower = filter.project.toLowerCase();
|
|
1187
2091
|
if (!isProjectMatch(loc.project.code, filter.project)) {
|
|
1188
2092
|
continue;
|
|
1189
2093
|
}
|
|
@@ -1213,6 +2117,9 @@ ${formattedList}`
|
|
|
1213
2117
|
meta.id = meta.id ?? num;
|
|
1214
2118
|
meta.type = meta.type || meta.category || "Feature";
|
|
1215
2119
|
meta.category = meta.type;
|
|
2120
|
+
if (meta.priority) {
|
|
2121
|
+
meta.priority = normalizePriority(meta.priority);
|
|
2122
|
+
}
|
|
1216
2123
|
let effectiveLoc = loc;
|
|
1217
2124
|
if (loc.isSharedDir) {
|
|
1218
2125
|
const found = locations.find(
|
|
@@ -1222,18 +2129,61 @@ ${formattedList}`
|
|
|
1222
2129
|
}
|
|
1223
2130
|
meta.project = effectiveLoc.project.code;
|
|
1224
2131
|
meta.projectId = effectiveLoc.project.id;
|
|
2132
|
+
let mMatch = null;
|
|
2133
|
+
if (meta.milestone) {
|
|
2134
|
+
meta.milestone = normalizeDashesAndMojibake(meta.milestone);
|
|
2135
|
+
mMatch = this.resolveMilestone(meta.milestone, effectiveLoc);
|
|
2136
|
+
if (mMatch?.featureFlag) {
|
|
2137
|
+
meta.inheritedFeatureFlag = mMatch.featureFlag;
|
|
2138
|
+
if (!meta.featureFlag) {
|
|
2139
|
+
meta.featureFlag = mMatch.featureFlag;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
1225
2143
|
if (filter?.project && filter.project !== "all") {
|
|
1226
2144
|
const pLower = filter.project.toLowerCase();
|
|
1227
2145
|
if (!isProjectMatch(meta.project, filter.project)) {
|
|
1228
2146
|
continue;
|
|
1229
2147
|
}
|
|
1230
2148
|
}
|
|
2149
|
+
if (filter?.milestone) {
|
|
2150
|
+
const mFilter = filter.milestone.trim().toLowerCase();
|
|
2151
|
+
const resolvedFilterMilestone = this.resolveMilestone(filter.milestone, effectiveLoc);
|
|
2152
|
+
const matchesDirect = Boolean(meta.milestone && meta.milestone.toLowerCase() === mFilter);
|
|
2153
|
+
const matchesResolvedTitle = Boolean(
|
|
2154
|
+
resolvedFilterMilestone && meta.milestone && meta.milestone.toLowerCase() === resolvedFilterMilestone.title.toLowerCase()
|
|
2155
|
+
);
|
|
2156
|
+
const matchesResolvedId = Boolean(
|
|
2157
|
+
resolvedFilterMilestone && meta.milestone && String(resolvedFilterMilestone.id).toLowerCase() === meta.milestone.toLowerCase()
|
|
2158
|
+
);
|
|
2159
|
+
const matchesCompound = Boolean(
|
|
2160
|
+
resolvedFilterMilestone && meta.milestone && `${resolvedFilterMilestone.project}:${resolvedFilterMilestone.title}`.toLowerCase() === meta.milestone.toLowerCase()
|
|
2161
|
+
);
|
|
2162
|
+
const matchesResolvedSelf = Boolean(
|
|
2163
|
+
mMatch && (mMatch.title.toLowerCase() === mFilter || String(mMatch.id).toLowerCase() === mFilter || `${mMatch.project}:${mMatch.title}`.toLowerCase() === mFilter || `${mMatch.project}:${mMatch.id}`.toLowerCase() === mFilter)
|
|
2164
|
+
);
|
|
2165
|
+
if (!matchesDirect && !matchesResolvedTitle && !matchesResolvedId && !matchesCompound && !matchesResolvedSelf) {
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
1231
2169
|
if (filter?.status && meta.status !== filter.status) {
|
|
1232
2170
|
continue;
|
|
1233
2171
|
}
|
|
1234
2172
|
if (filter?.category && meta.category !== filter.category) {
|
|
1235
2173
|
continue;
|
|
1236
2174
|
}
|
|
2175
|
+
if (filter?.priority !== void 0) {
|
|
2176
|
+
const fPrio = String(filter.priority).trim().toLowerCase();
|
|
2177
|
+
if (fPrio === "none" || fPrio === "null") {
|
|
2178
|
+
if (meta.priority) {
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
} else {
|
|
2182
|
+
if (!meta.priority || meta.priority.toLowerCase() !== fPrio) {
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
1237
2187
|
if (filter?.search) {
|
|
1238
2188
|
const term = filter.search.toLowerCase();
|
|
1239
2189
|
const matchesTitle = meta.title.toLowerCase().includes(term);
|
|
@@ -1268,8 +2218,24 @@ ${formattedList}`
|
|
|
1268
2218
|
comments,
|
|
1269
2219
|
projectDescriptor: effectiveLoc.project
|
|
1270
2220
|
};
|
|
2221
|
+
normalizeTicketFields(ticketObj);
|
|
2222
|
+
this.enrichTicketLinks(ticketObj);
|
|
1271
2223
|
ticketObj.sha1 = computeTicketHash(ticketObj);
|
|
1272
2224
|
ticketObj.lastHash = ticketObj.sha1;
|
|
2225
|
+
if (filter?.isBlocked !== void 0 && Boolean(ticketObj.isBlocked) !== Boolean(filter.isBlocked)) {
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
if (filter?.linkedTo) {
|
|
2229
|
+
const target = filter.linkedTo.trim().toLowerCase();
|
|
2230
|
+
const hasLink = ticketObj.meta.links?.some((l) => {
|
|
2231
|
+
const tk = l.targetKey.toLowerCase();
|
|
2232
|
+
const idStr = String(l.targetId);
|
|
2233
|
+
return tk === target || idStr === target;
|
|
2234
|
+
});
|
|
2235
|
+
if (!hasLink) {
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
1273
2239
|
tickets.push(ticketObj);
|
|
1274
2240
|
} catch {
|
|
1275
2241
|
}
|
|
@@ -1293,6 +2259,19 @@ ${formattedList}`
|
|
|
1293
2259
|
meta.id = meta.id ?? locInfo.id;
|
|
1294
2260
|
meta.project = locInfo.loc.project.code;
|
|
1295
2261
|
meta.projectId = locInfo.loc.project.id;
|
|
2262
|
+
if (meta.priority) {
|
|
2263
|
+
meta.priority = normalizePriority(meta.priority);
|
|
2264
|
+
}
|
|
2265
|
+
if (meta.milestone) {
|
|
2266
|
+
meta.milestone = normalizeDashesAndMojibake(meta.milestone);
|
|
2267
|
+
const mMatch = this.resolveMilestone(meta.milestone, locInfo.loc);
|
|
2268
|
+
if (mMatch?.featureFlag) {
|
|
2269
|
+
meta.inheritedFeatureFlag = mMatch.featureFlag;
|
|
2270
|
+
if (!meta.featureFlag) {
|
|
2271
|
+
meta.featureFlag = mMatch.featureFlag;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
1296
2275
|
let detail;
|
|
1297
2276
|
const detailPath = path3.join(locInfo.ticketDir, "detail.md");
|
|
1298
2277
|
if (fs3.existsSync(detailPath)) {
|
|
@@ -1319,6 +2298,8 @@ ${formattedList}`
|
|
|
1319
2298
|
comments,
|
|
1320
2299
|
projectDescriptor: locInfo.loc.project
|
|
1321
2300
|
};
|
|
2301
|
+
normalizeTicketFields(ticket);
|
|
2302
|
+
this.enrichTicketLinks(ticket);
|
|
1322
2303
|
ticket.sha1 = computeTicketHash(ticket);
|
|
1323
2304
|
ticket.lastHash = ticket.sha1;
|
|
1324
2305
|
return ticket;
|
|
@@ -1368,10 +2349,13 @@ ${formattedList}`
|
|
|
1368
2349
|
if (itemMatch) openQuestions.push(itemMatch[1].trim());
|
|
1369
2350
|
}
|
|
1370
2351
|
}
|
|
2352
|
+
const priorityMatch = raw.match(/\*\*Priority\*\*:\s*([^\n]+)/i);
|
|
2353
|
+
const priority = priorityMatch ? normalizePriority(priorityMatch[1].trim()) : void 0;
|
|
1371
2354
|
return {
|
|
1372
2355
|
title,
|
|
1373
2356
|
type,
|
|
1374
2357
|
category,
|
|
2358
|
+
priority,
|
|
1375
2359
|
complexity,
|
|
1376
2360
|
estimatedEffort,
|
|
1377
2361
|
summary,
|
|
@@ -1411,13 +2395,40 @@ ${formattedList}`
|
|
|
1411
2395
|
fs3.mkdirSync(ticketDir, { recursive: true });
|
|
1412
2396
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1413
2397
|
const effectiveType = input.type || input.category || "Feature";
|
|
2398
|
+
const cleanTitle = normalizeDashesAndMojibake(input.title).slice(0, 48).trim();
|
|
2399
|
+
const cleanEffort = normalizeDashesAndMojibake(input.estimatedEffort || input.effort || "2.0 - 4.0 hours");
|
|
2400
|
+
const cleanComplexity = normalizeDashesAndMojibake(input.complexity || "Medium");
|
|
2401
|
+
const cleanPriority = normalizePriority(input.priority);
|
|
2402
|
+
let cleanMilestone = input.milestone ? normalizeDashesAndMojibake(String(input.milestone)).trim() : void 0;
|
|
2403
|
+
let inheritedFlag;
|
|
2404
|
+
let initialFlag;
|
|
2405
|
+
if (cleanMilestone) {
|
|
2406
|
+
const pMilestones = this.readProjectMilestones(targetLoc);
|
|
2407
|
+
const mMatch = pMilestones.find(
|
|
2408
|
+
(m) => String(m.id).toLowerCase() === cleanMilestone?.toLowerCase() || m.title.toLowerCase() === cleanMilestone?.toLowerCase()
|
|
2409
|
+
);
|
|
2410
|
+
if (mMatch?.featureFlag) {
|
|
2411
|
+
inheritedFlag = mMatch.featureFlag;
|
|
2412
|
+
initialFlag = mMatch.featureFlag;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
let cleanFeatureFlag = input.featureFlag ? normalizeDashesAndMojibake(input.featureFlag).trim() : void 0;
|
|
2416
|
+
if (!cleanFeatureFlag && initialFlag) {
|
|
2417
|
+
cleanFeatureFlag = initialFlag;
|
|
2418
|
+
}
|
|
2419
|
+
const cleanSummary = input.summary ? normalizeDashesAndMojibake(input.summary) : void 0;
|
|
2420
|
+
const cleanDetailRaw = input.detailMarkdown || input.detail ? normalizeDashesAndMojibake(input.detailMarkdown || input.detail) : void 0;
|
|
1414
2421
|
const meta = {
|
|
1415
2422
|
id: nextId,
|
|
1416
|
-
title:
|
|
2423
|
+
title: cleanTitle,
|
|
1417
2424
|
type: effectiveType,
|
|
1418
2425
|
category: effectiveType,
|
|
1419
|
-
|
|
1420
|
-
|
|
2426
|
+
...cleanPriority ? { priority: cleanPriority } : {},
|
|
2427
|
+
...cleanMilestone ? { milestone: cleanMilestone } : {},
|
|
2428
|
+
...cleanFeatureFlag ? { featureFlag: cleanFeatureFlag } : {},
|
|
2429
|
+
...inheritedFlag ? { inheritedFeatureFlag: inheritedFlag } : {},
|
|
2430
|
+
complexity: cleanComplexity,
|
|
2431
|
+
estimatedEffort: cleanEffort,
|
|
1421
2432
|
submittedBy: input.submittedBy || "Developer",
|
|
1422
2433
|
timestamp: now,
|
|
1423
2434
|
createdAt: now,
|
|
@@ -1428,7 +2439,11 @@ ${formattedList}`
|
|
|
1428
2439
|
projectId: targetLoc.project.id,
|
|
1429
2440
|
project: targetLoc.project.code
|
|
1430
2441
|
};
|
|
1431
|
-
const rawDetail =
|
|
2442
|
+
const rawDetail = cleanDetailRaw?.trim();
|
|
2443
|
+
const priorityLine = meta.priority ? `**Priority**: ${meta.priority}
|
|
2444
|
+
` : "";
|
|
2445
|
+
const milestoneLine = meta.milestone ? `**Milestone**: ${meta.milestone}
|
|
2446
|
+
` : "";
|
|
1432
2447
|
let detailMd;
|
|
1433
2448
|
if (rawDetail) {
|
|
1434
2449
|
if (/^#\s+[^\n]+/m.test(rawDetail)) {
|
|
@@ -1436,7 +2451,7 @@ ${formattedList}`
|
|
|
1436
2451
|
if (!/\*\*(?:Type|Category)\*\*:/i.test(processed)) {
|
|
1437
2452
|
const metaBlock = `
|
|
1438
2453
|
**Category**: ${meta.category}
|
|
1439
|
-
**Complexity**: ${meta.complexity}
|
|
2454
|
+
${priorityLine}${milestoneLine}**Complexity**: ${meta.complexity}
|
|
1440
2455
|
**Estimated Effort**: ${meta.estimatedEffort}
|
|
1441
2456
|
`;
|
|
1442
2457
|
processed = processed.replace(/^(# Ticket[^\n]+\n)/m, `$1${metaBlock}`);
|
|
@@ -1476,7 +2491,7 @@ ${rawDetail}
|
|
|
1476
2491
|
}
|
|
1477
2492
|
detailMd = `# Ticket #${nextId}: ${meta.title}
|
|
1478
2493
|
**Category**: ${meta.category}
|
|
1479
|
-
**Complexity**: ${meta.complexity}
|
|
2494
|
+
${priorityLine}${milestoneLine}**Complexity**: ${meta.complexity}
|
|
1480
2495
|
**Estimated Effort**: ${meta.estimatedEffort}
|
|
1481
2496
|
|
|
1482
2497
|
${body.trim()}
|
|
@@ -1485,7 +2500,7 @@ ${body.trim()}
|
|
|
1485
2500
|
} else {
|
|
1486
2501
|
detailMd = `# Ticket #${nextId}: ${meta.title}
|
|
1487
2502
|
**Category**: ${meta.category}
|
|
1488
|
-
**Complexity**: ${meta.complexity}
|
|
2503
|
+
${priorityLine}${milestoneLine}**Complexity**: ${meta.complexity}
|
|
1489
2504
|
**Estimated Effort**: ${meta.estimatedEffort}
|
|
1490
2505
|
|
|
1491
2506
|
### Summary
|
|
@@ -1527,22 +2542,61 @@ ${input.summary || "Summary to be defined."}
|
|
|
1527
2542
|
const isNowCompleted = updates.status === "Completed";
|
|
1528
2543
|
const completedAt = isNowCompleted ? existing.meta.completedAt || now : updates.status && updates.status !== "Completed" ? void 0 : existing.meta.completedAt;
|
|
1529
2544
|
const revision = (existing.meta.revision || 1) + 1;
|
|
2545
|
+
const sanitizedUpdates = { ...updates };
|
|
2546
|
+
if (sanitizedUpdates.title) sanitizedUpdates.title = normalizeDashesAndMojibake(sanitizedUpdates.title);
|
|
2547
|
+
if (sanitizedUpdates.estimatedEffort) sanitizedUpdates.estimatedEffort = normalizeDashesAndMojibake(sanitizedUpdates.estimatedEffort);
|
|
2548
|
+
if (sanitizedUpdates.complexity) sanitizedUpdates.complexity = normalizeDashesAndMojibake(sanitizedUpdates.complexity);
|
|
1530
2549
|
const updatedMeta = {
|
|
1531
2550
|
...existing.meta,
|
|
1532
|
-
...
|
|
2551
|
+
...sanitizedUpdates,
|
|
1533
2552
|
id: locInfo.id,
|
|
1534
2553
|
project: locInfo.loc.project.code,
|
|
1535
2554
|
updatedAt: now,
|
|
1536
2555
|
completedAt,
|
|
1537
2556
|
revision
|
|
1538
2557
|
};
|
|
2558
|
+
if ("priority" in updates || updates.priority !== void 0) {
|
|
2559
|
+
const rawPrio = updates.priority;
|
|
2560
|
+
if (rawPrio === null || rawPrio === "none" || rawPrio === "null" || rawPrio === "") {
|
|
2561
|
+
delete updatedMeta.priority;
|
|
2562
|
+
} else {
|
|
2563
|
+
const norm = normalizePriority(rawPrio);
|
|
2564
|
+
if (norm) {
|
|
2565
|
+
updatedMeta.priority = norm;
|
|
2566
|
+
} else {
|
|
2567
|
+
delete updatedMeta.priority;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
if ("milestone" in updates || updates.milestone !== void 0) {
|
|
2572
|
+
const rawMilestone = updates.milestone;
|
|
2573
|
+
if (rawMilestone === null || rawMilestone === "none" || rawMilestone === "null" || rawMilestone === "") {
|
|
2574
|
+
delete updatedMeta.milestone;
|
|
2575
|
+
if (updatedMeta.inheritedFeatureFlag && updatedMeta.featureFlag === updatedMeta.inheritedFeatureFlag) {
|
|
2576
|
+
delete updatedMeta.featureFlag;
|
|
2577
|
+
}
|
|
2578
|
+
delete updatedMeta.inheritedFeatureFlag;
|
|
2579
|
+
} else {
|
|
2580
|
+
updatedMeta.milestone = normalizeDashesAndMojibake(String(rawMilestone)).trim();
|
|
2581
|
+
const pMilestones = this.readProjectMilestones(locInfo.loc);
|
|
2582
|
+
const mMatch = pMilestones.find(
|
|
2583
|
+
(m) => String(m.id).toLowerCase() === updatedMeta.milestone?.toLowerCase() || m.title.toLowerCase() === updatedMeta.milestone?.toLowerCase()
|
|
2584
|
+
);
|
|
2585
|
+
if (mMatch?.featureFlag) {
|
|
2586
|
+
updatedMeta.inheritedFeatureFlag = mMatch.featureFlag;
|
|
2587
|
+
if (!updatedMeta.featureFlag) {
|
|
2588
|
+
updatedMeta.featureFlag = mMatch.featureFlag;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
1539
2593
|
writeSafeFile(metaPath, JSON.stringify(updatedMeta, null, 2) + "\n");
|
|
1540
2594
|
const newType = updates.type || updates.category;
|
|
1541
2595
|
if (newType) {
|
|
1542
2596
|
updatedMeta.type = newType;
|
|
1543
2597
|
updatedMeta.category = newType;
|
|
1544
2598
|
}
|
|
1545
|
-
if (existing.detail && (updates.title || updates.type || updates.category || updates.complexity || updates.estimatedEffort)) {
|
|
2599
|
+
if (existing.detail && ("priority" in updates || "milestone" in updates || updates.title || updates.type || updates.category || updates.complexity || updates.estimatedEffort)) {
|
|
1546
2600
|
let content = existing.detail.raw;
|
|
1547
2601
|
if (updates.title) {
|
|
1548
2602
|
content = content.replace(/^#\s*(?:Ticket\s*#?\d+\s*:\s*|\d+\s*:\s*)?[^\n]+/im, `# Ticket #${locInfo.id}: ${updatedMeta.title}`);
|
|
@@ -1550,77 +2604,321 @@ ${input.summary || "Summary to be defined."}
|
|
|
1550
2604
|
if (updates.type || updates.category) {
|
|
1551
2605
|
content = content.replace(/\*\*(?:Type|Category)\*\*:\s*[^\n]+/i, `**Type**: ${updatedMeta.type}`);
|
|
1552
2606
|
}
|
|
2607
|
+
if ("priority" in updates) {
|
|
2608
|
+
if (updatedMeta.priority) {
|
|
2609
|
+
if (/\*\*Priority\*\*:\s*[^\n]+/i.test(content)) {
|
|
2610
|
+
content = content.replace(/\*\*Priority\*\*:\s*[^\n]+/i, `**Priority**: ${updatedMeta.priority}`);
|
|
2611
|
+
} else if (/\*\*(?:Type|Category)\*\*:\s*[^\n]+/i.test(content)) {
|
|
2612
|
+
content = content.replace(/(\*\*(?:Type|Category)\*\*:\s*[^\n]+)/i, `$1
|
|
2613
|
+
**Priority**: ${updatedMeta.priority}`);
|
|
2614
|
+
}
|
|
2615
|
+
} else {
|
|
2616
|
+
content = content.replace(/\*\*Priority\*\*:\s*[^\n]+\n?/i, "");
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
if ("milestone" in updates) {
|
|
2620
|
+
if (updatedMeta.milestone) {
|
|
2621
|
+
if (/\*\*Milestone\*\*:\s*[^\n]+/i.test(content)) {
|
|
2622
|
+
content = content.replace(/\*\*Milestone\*\*:\s*[^\n]+/i, `**Milestone**: ${updatedMeta.milestone}`);
|
|
2623
|
+
} else if (/\*\*(?:Priority|Type|Category)\*\*:\s*[^\n]+/i.test(content)) {
|
|
2624
|
+
content = content.replace(/(\*\*(?:Priority|Type|Category)\*\*:\s*[^\n]+)/i, `$1
|
|
2625
|
+
**Milestone**: ${updatedMeta.milestone}`);
|
|
2626
|
+
}
|
|
2627
|
+
} else {
|
|
2628
|
+
content = content.replace(/\*\*Milestone\*\*:\s*[^\n]+\n?/i, "");
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
1553
2631
|
if (updates.complexity) {
|
|
1554
2632
|
content = content.replace(/\*\*Complexity\*\*:\s*[^\n]+/i, `**Complexity**: ${updatedMeta.complexity}`);
|
|
1555
2633
|
}
|
|
1556
2634
|
if (updates.estimatedEffort) {
|
|
1557
2635
|
content = content.replace(/\*\*Estimated Effort\*\*:\s*[^\n]+/i, `**Estimated Effort**: ${updatedMeta.estimatedEffort}`);
|
|
1558
2636
|
}
|
|
1559
|
-
writeSafeFile(path3.join(locInfo.ticketDir, "detail.md"), content);
|
|
2637
|
+
writeSafeFile(path3.join(locInfo.ticketDir, "detail.md"), content);
|
|
2638
|
+
}
|
|
2639
|
+
return await this.getTicket(`${locInfo.loc.project.code}-${locInfo.id}`);
|
|
2640
|
+
}
|
|
2641
|
+
async getPlan(id) {
|
|
2642
|
+
const locInfo = this.findTicketLocation(id);
|
|
2643
|
+
if (!locInfo) return null;
|
|
2644
|
+
const planPath = path3.join(locInfo.ticketDir, "implementation_plan.md");
|
|
2645
|
+
if (!fs3.existsSync(planPath)) return null;
|
|
2646
|
+
return normalizeDashesAndMojibake(fs3.readFileSync(planPath, "utf-8"));
|
|
2647
|
+
}
|
|
2648
|
+
async savePlan(id, planMarkdown, lastHash) {
|
|
2649
|
+
const ticket = await this.getTicket(id);
|
|
2650
|
+
if (!ticket) {
|
|
2651
|
+
throw new Error(`Ticket #${id} does not exist`);
|
|
2652
|
+
}
|
|
2653
|
+
if (lastHash !== void 0) {
|
|
2654
|
+
const currentHash = ticket.sha1 || computeTicketHash(ticket);
|
|
2655
|
+
if (!verifyTicketHash(currentHash, lastHash)) {
|
|
2656
|
+
throw new EsedreConflictError(id, currentHash, lastHash);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
const locInfo = this.findTicketLocation(id);
|
|
2660
|
+
if (!locInfo) {
|
|
2661
|
+
throw new Error(`Ticket #${id} could not be located on disk`);
|
|
2662
|
+
}
|
|
2663
|
+
const planPath = path3.join(locInfo.ticketDir, "implementation_plan.md");
|
|
2664
|
+
writeSafeFile(planPath, normalizeDashesAndMojibake(planMarkdown));
|
|
2665
|
+
const metaPath = path3.join(locInfo.ticketDir, "meta.json");
|
|
2666
|
+
if (fs3.existsSync(metaPath)) {
|
|
2667
|
+
try {
|
|
2668
|
+
const meta = JSON.parse(fs3.readFileSync(metaPath, "utf-8"));
|
|
2669
|
+
meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2670
|
+
meta.revision = (meta.revision || 1) + 1;
|
|
2671
|
+
writeSafeFile(metaPath, JSON.stringify(meta, null, 2) + "\n");
|
|
2672
|
+
} catch {
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
async addComment(id, comment) {
|
|
2677
|
+
const ticket = await this.getTicket(id);
|
|
2678
|
+
if (!ticket) {
|
|
2679
|
+
throw new Error(`Ticket #${id} does not exist`);
|
|
2680
|
+
}
|
|
2681
|
+
const locInfo = this.findTicketLocation(id);
|
|
2682
|
+
if (!locInfo) {
|
|
2683
|
+
throw new Error(`Ticket #${id} could not be located on disk`);
|
|
2684
|
+
}
|
|
2685
|
+
const commentsPath = path3.join(locInfo.ticketDir, "comments.json");
|
|
2686
|
+
let comments = [];
|
|
2687
|
+
if (fs3.existsSync(commentsPath)) {
|
|
2688
|
+
try {
|
|
2689
|
+
comments = JSON.parse(fs3.readFileSync(commentsPath, "utf-8"));
|
|
2690
|
+
} catch {
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
const newComment = {
|
|
2694
|
+
id: `${locInfo.id}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
2695
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2696
|
+
author: normalizeDashesAndMojibake(comment.author || "User"),
|
|
2697
|
+
text: normalizeDashesAndMojibake(comment.text.trim())
|
|
2698
|
+
};
|
|
2699
|
+
comments.push(newComment);
|
|
2700
|
+
writeSafeFile(commentsPath, JSON.stringify(comments, null, 2) + "\n");
|
|
2701
|
+
return newComment;
|
|
2702
|
+
}
|
|
2703
|
+
resolveTicketIdentifier(id, defaultProject) {
|
|
2704
|
+
const str = String(id).trim();
|
|
2705
|
+
if (!/^([a-zA-Z0-9]{1,8})[-_:](\d+)$/i.test(str) && defaultProject && defaultProject !== "all") {
|
|
2706
|
+
const candidate = this.findTicketLocation(`${defaultProject}-${str}`);
|
|
2707
|
+
if (candidate) {
|
|
2708
|
+
return {
|
|
2709
|
+
...candidate,
|
|
2710
|
+
key: `${candidate.loc.project.code}-${candidate.id}`,
|
|
2711
|
+
project: candidate.loc.project.code
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
const loc = this.findTicketLocation(id);
|
|
2716
|
+
if (!loc) return null;
|
|
2717
|
+
return {
|
|
2718
|
+
...loc,
|
|
2719
|
+
key: `${loc.loc.project.code}-${loc.id}`,
|
|
2720
|
+
project: loc.loc.project.code
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
wouldCreateCycle(sourceKey, targetKey, relation) {
|
|
2724
|
+
if (relation !== "blocks" && relation !== "parent-of") {
|
|
2725
|
+
return false;
|
|
2726
|
+
}
|
|
2727
|
+
const sLower = sourceKey.toLowerCase();
|
|
2728
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2729
|
+
const queue = [targetKey.toLowerCase()];
|
|
2730
|
+
while (queue.length > 0) {
|
|
2731
|
+
const current = queue.shift();
|
|
2732
|
+
if (current === sLower) {
|
|
2733
|
+
return true;
|
|
2734
|
+
}
|
|
2735
|
+
if (visited.has(current)) continue;
|
|
2736
|
+
visited.add(current);
|
|
2737
|
+
const loc = this.findTicketLocation(current);
|
|
2738
|
+
if (!loc) continue;
|
|
2739
|
+
const metaPath = path3.join(loc.ticketDir, "meta.json");
|
|
2740
|
+
if (!fs3.existsSync(metaPath)) continue;
|
|
2741
|
+
try {
|
|
2742
|
+
const meta = JSON.parse(fs3.readFileSync(metaPath, "utf-8"));
|
|
2743
|
+
if (Array.isArray(meta.links)) {
|
|
2744
|
+
for (const l of meta.links) {
|
|
2745
|
+
if (l.relation === relation) {
|
|
2746
|
+
queue.push(l.targetKey.toLowerCase());
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
} catch {
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
return false;
|
|
2754
|
+
}
|
|
2755
|
+
enrichTicketLinks(ticket) {
|
|
2756
|
+
if (!ticket.meta.links || ticket.meta.links.length === 0) {
|
|
2757
|
+
ticket.links = [];
|
|
2758
|
+
ticket.isBlocked = false;
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
const enriched = [];
|
|
2762
|
+
let isBlocked = false;
|
|
2763
|
+
for (const link of ticket.meta.links) {
|
|
2764
|
+
const enrichedLink = { ...link, isResolved: false };
|
|
2765
|
+
const targetLoc = this.findTicketLocation(link.targetKey);
|
|
2766
|
+
if (targetLoc) {
|
|
2767
|
+
const targetMetaPath = path3.join(targetLoc.ticketDir, "meta.json");
|
|
2768
|
+
if (fs3.existsSync(targetMetaPath)) {
|
|
2769
|
+
try {
|
|
2770
|
+
const targetMeta = JSON.parse(fs3.readFileSync(targetMetaPath, "utf-8"));
|
|
2771
|
+
enrichedLink.targetTitle = targetMeta.title;
|
|
2772
|
+
enrichedLink.targetType = targetMeta.type || targetMeta.category || "Feature";
|
|
2773
|
+
enrichedLink.targetStatus = targetMeta.status || "Planned";
|
|
2774
|
+
enrichedLink.isTargetCompleted = targetMeta.status === "Completed";
|
|
2775
|
+
if (targetMeta.priority) {
|
|
2776
|
+
enrichedLink.targetPriority = normalizePriority(targetMeta.priority);
|
|
2777
|
+
}
|
|
2778
|
+
enrichedLink.isResolved = true;
|
|
2779
|
+
if (link.relation === "blocked-by" && targetMeta.status !== "Completed") {
|
|
2780
|
+
enrichedLink.isBlockedByUncompleted = true;
|
|
2781
|
+
isBlocked = true;
|
|
2782
|
+
}
|
|
2783
|
+
} catch {
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
enriched.push(enrichedLink);
|
|
1560
2788
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
async getPlan(id) {
|
|
1564
|
-
const locInfo = this.findTicketLocation(id);
|
|
1565
|
-
if (!locInfo) return null;
|
|
1566
|
-
const planPath = path3.join(locInfo.ticketDir, "implementation_plan.md");
|
|
1567
|
-
if (!fs3.existsSync(planPath)) return null;
|
|
1568
|
-
return fs3.readFileSync(planPath, "utf-8");
|
|
2789
|
+
ticket.links = enriched;
|
|
2790
|
+
ticket.isBlocked = isBlocked;
|
|
1569
2791
|
}
|
|
1570
|
-
async
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
2792
|
+
async addTicketLink(sourceId, relation, targetId, options) {
|
|
2793
|
+
if (!INVERSE_RELATIONS[relation]) {
|
|
2794
|
+
throw new Error(
|
|
2795
|
+
`Invalid link relation: "${relation}". Valid relations: ${Object.keys(INVERSE_RELATIONS).join(", ")}`
|
|
2796
|
+
);
|
|
1574
2797
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
throw new EsedreConflictError(id, currentHash, lastHash);
|
|
1579
|
-
}
|
|
2798
|
+
const sourceInfo = this.resolveTicketIdentifier(sourceId, options?.project);
|
|
2799
|
+
if (!sourceInfo) {
|
|
2800
|
+
throw new Error(`Source ticket #${sourceId} could not be located.`);
|
|
1580
2801
|
}
|
|
1581
|
-
const
|
|
1582
|
-
if (!
|
|
1583
|
-
throw new Error(`
|
|
2802
|
+
const targetInfo = this.resolveTicketIdentifier(targetId, options?.project || sourceInfo.project);
|
|
2803
|
+
if (!targetInfo) {
|
|
2804
|
+
throw new Error(`Target ticket #${targetId} could not be located.`);
|
|
1584
2805
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
if (
|
|
2806
|
+
if (sourceInfo.key.toLowerCase() === targetInfo.key.toLowerCase()) {
|
|
2807
|
+
throw new Error(`Cannot link ticket ${sourceInfo.key} to itself.`);
|
|
2808
|
+
}
|
|
2809
|
+
if (this.wouldCreateCycle(sourceInfo.key, targetInfo.key, relation)) {
|
|
2810
|
+
throw new Error(
|
|
2811
|
+
`Circular dependency detected: linking ${sourceInfo.key} -> ${targetInfo.key} with "${relation}" forms a cycle.`
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2815
|
+
const sourceMetaPath = path3.join(sourceInfo.ticketDir, "meta.json");
|
|
2816
|
+
const sourceMeta = JSON.parse(fs3.readFileSync(sourceMetaPath, "utf-8"));
|
|
2817
|
+
sourceMeta.links = sourceMeta.links || [];
|
|
2818
|
+
const existingSourceLink = sourceMeta.links.find(
|
|
2819
|
+
(l) => l.targetKey.toLowerCase() === targetInfo.key.toLowerCase()
|
|
2820
|
+
);
|
|
2821
|
+
if (existingSourceLink) {
|
|
2822
|
+
existingSourceLink.relation = relation;
|
|
2823
|
+
existingSourceLink.targetProject = targetInfo.project;
|
|
2824
|
+
existingSourceLink.targetId = targetInfo.id;
|
|
2825
|
+
} else {
|
|
2826
|
+
sourceMeta.links.push({
|
|
2827
|
+
relation,
|
|
2828
|
+
targetKey: targetInfo.key,
|
|
2829
|
+
targetProject: targetInfo.project,
|
|
2830
|
+
targetId: targetInfo.id,
|
|
2831
|
+
createdAt: now,
|
|
2832
|
+
createdBy: options?.author ? normalizeDashesAndMojibake(options.author) : void 0
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
sourceMeta.updatedAt = now;
|
|
2836
|
+
sourceMeta.revision = (sourceMeta.revision || 1) + 1;
|
|
2837
|
+
writeSafeFile(sourceMetaPath, JSON.stringify(sourceMeta, null, 2) + "\n");
|
|
2838
|
+
let updatedTarget;
|
|
2839
|
+
const targetMetaPath = path3.join(targetInfo.ticketDir, "meta.json");
|
|
2840
|
+
if (fs3.existsSync(targetMetaPath)) {
|
|
1589
2841
|
try {
|
|
1590
|
-
const
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
2842
|
+
const targetMeta = JSON.parse(fs3.readFileSync(targetMetaPath, "utf-8"));
|
|
2843
|
+
targetMeta.links = targetMeta.links || [];
|
|
2844
|
+
const inverseRel = INVERSE_RELATIONS[relation];
|
|
2845
|
+
const existingTargetLink = targetMeta.links.find(
|
|
2846
|
+
(l) => l.targetKey.toLowerCase() === sourceInfo.key.toLowerCase()
|
|
2847
|
+
);
|
|
2848
|
+
if (existingTargetLink) {
|
|
2849
|
+
existingTargetLink.relation = inverseRel;
|
|
2850
|
+
existingTargetLink.targetProject = sourceInfo.project;
|
|
2851
|
+
existingTargetLink.targetId = sourceInfo.id;
|
|
2852
|
+
} else {
|
|
2853
|
+
targetMeta.links.push({
|
|
2854
|
+
relation: inverseRel,
|
|
2855
|
+
targetKey: sourceInfo.key,
|
|
2856
|
+
targetProject: sourceInfo.project,
|
|
2857
|
+
targetId: sourceInfo.id,
|
|
2858
|
+
createdAt: now,
|
|
2859
|
+
createdBy: options?.author ? normalizeDashesAndMojibake(options.author) : void 0
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
targetMeta.updatedAt = now;
|
|
2863
|
+
targetMeta.revision = (targetMeta.revision || 1) + 1;
|
|
2864
|
+
writeSafeFile(targetMetaPath, JSON.stringify(targetMeta, null, 2) + "\n");
|
|
2865
|
+
updatedTarget = await this.getTicket(targetInfo.key) || void 0;
|
|
1594
2866
|
} catch {
|
|
1595
2867
|
}
|
|
1596
2868
|
}
|
|
2869
|
+
const updatedSource = await this.getTicket(sourceInfo.key);
|
|
2870
|
+
return { source: updatedSource, target: updatedTarget };
|
|
1597
2871
|
}
|
|
1598
|
-
async
|
|
1599
|
-
const
|
|
1600
|
-
if (!
|
|
1601
|
-
throw new Error(`
|
|
1602
|
-
}
|
|
1603
|
-
const locInfo = this.findTicketLocation(id);
|
|
1604
|
-
if (!locInfo) {
|
|
1605
|
-
throw new Error(`Ticket #${id} could not be located on disk`);
|
|
2872
|
+
async removeTicketLink(sourceId, targetId, options) {
|
|
2873
|
+
const sourceInfo = this.resolveTicketIdentifier(sourceId, options?.project);
|
|
2874
|
+
if (!sourceInfo) {
|
|
2875
|
+
throw new Error(`Source ticket #${sourceId} could not be located.`);
|
|
1606
2876
|
}
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1609
|
-
|
|
2877
|
+
const targetInfo = this.resolveTicketIdentifier(targetId, options?.project || sourceInfo.project);
|
|
2878
|
+
const targetKey = targetInfo ? targetInfo.key : String(targetId).trim();
|
|
2879
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2880
|
+
const sourceMetaPath = path3.join(sourceInfo.ticketDir, "meta.json");
|
|
2881
|
+
if (fs3.existsSync(sourceMetaPath)) {
|
|
1610
2882
|
try {
|
|
1611
|
-
|
|
2883
|
+
const sourceMeta = JSON.parse(fs3.readFileSync(sourceMetaPath, "utf-8"));
|
|
2884
|
+
if (Array.isArray(sourceMeta.links)) {
|
|
2885
|
+
const originalLen = sourceMeta.links.length;
|
|
2886
|
+
sourceMeta.links = sourceMeta.links.filter(
|
|
2887
|
+
(l) => !(l.targetKey.toLowerCase() === targetKey.toLowerCase() && (!options?.relation || l.relation === options.relation))
|
|
2888
|
+
);
|
|
2889
|
+
if (sourceMeta.links.length !== originalLen) {
|
|
2890
|
+
sourceMeta.updatedAt = now;
|
|
2891
|
+
sourceMeta.revision = (sourceMeta.revision || 1) + 1;
|
|
2892
|
+
writeSafeFile(sourceMetaPath, JSON.stringify(sourceMeta, null, 2) + "\n");
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
1612
2895
|
} catch {
|
|
1613
2896
|
}
|
|
1614
2897
|
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
2898
|
+
let updatedTarget;
|
|
2899
|
+
if (targetInfo) {
|
|
2900
|
+
const targetMetaPath = path3.join(targetInfo.ticketDir, "meta.json");
|
|
2901
|
+
if (fs3.existsSync(targetMetaPath)) {
|
|
2902
|
+
try {
|
|
2903
|
+
const targetMeta = JSON.parse(fs3.readFileSync(targetMetaPath, "utf-8"));
|
|
2904
|
+
if (Array.isArray(targetMeta.links)) {
|
|
2905
|
+
const originalLen = targetMeta.links.length;
|
|
2906
|
+
targetMeta.links = targetMeta.links.filter(
|
|
2907
|
+
(l) => !(l.targetKey.toLowerCase() === sourceInfo.key.toLowerCase() && (!options?.relation || l.relation === INVERSE_RELATIONS[options.relation]))
|
|
2908
|
+
);
|
|
2909
|
+
if (targetMeta.links.length !== originalLen) {
|
|
2910
|
+
targetMeta.updatedAt = now;
|
|
2911
|
+
targetMeta.revision = (targetMeta.revision || 1) + 1;
|
|
2912
|
+
writeSafeFile(targetMetaPath, JSON.stringify(targetMeta, null, 2) + "\n");
|
|
2913
|
+
updatedTarget = await this.getTicket(targetInfo.key) || void 0;
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
} catch {
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
const updatedSource = await this.getTicket(sourceInfo.key);
|
|
2921
|
+
return { source: updatedSource, target: updatedTarget };
|
|
1624
2922
|
}
|
|
1625
2923
|
};
|
|
1626
2924
|
|
|
@@ -1652,19 +2950,56 @@ var SecurityFilter = class {
|
|
|
1652
2950
|
}
|
|
1653
2951
|
return proj;
|
|
1654
2952
|
}
|
|
2953
|
+
async updateProject(input) {
|
|
2954
|
+
if (!isProjectAuthorized(input.code, this.config.allowedProjects)) {
|
|
2955
|
+
throw new EsedreAuthorizationError(input.code);
|
|
2956
|
+
}
|
|
2957
|
+
return this.target.updateProject(input);
|
|
2958
|
+
}
|
|
2959
|
+
async renameProjectCode(input) {
|
|
2960
|
+
if (!isProjectAuthorized(input.oldCode, this.config.allowedProjects)) {
|
|
2961
|
+
throw new EsedreAuthorizationError(input.oldCode);
|
|
2962
|
+
}
|
|
2963
|
+
const result = await this.target.renameProjectCode(input);
|
|
2964
|
+
if (this.config.allowedProjects && !this.config.allowedProjects.includes("*")) {
|
|
2965
|
+
const oldUpper = input.oldCode.toUpperCase();
|
|
2966
|
+
const idx = this.config.allowedProjects.findIndex((p) => p.toUpperCase() === oldUpper);
|
|
2967
|
+
if (idx >= 0) {
|
|
2968
|
+
this.config.allowedProjects[idx] = result.newCode;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
return result;
|
|
2972
|
+
}
|
|
2973
|
+
redactLinks(ticket) {
|
|
2974
|
+
if (!ticket.links || ticket.links.length === 0) return ticket;
|
|
2975
|
+
if (!this.config.allowedProjects || this.config.allowedProjects.includes("*")) {
|
|
2976
|
+
return ticket;
|
|
2977
|
+
}
|
|
2978
|
+
for (const link of ticket.links) {
|
|
2979
|
+
if (link.targetProject && !isProjectAuthorized(link.targetProject, this.config.allowedProjects)) {
|
|
2980
|
+
link.targetTitle = "[Restricted Project]";
|
|
2981
|
+
delete link.targetType;
|
|
2982
|
+
delete link.targetStatus;
|
|
2983
|
+
delete link.targetPriority;
|
|
2984
|
+
delete link.isBlockedByUncompleted;
|
|
2985
|
+
link.isResolved = false;
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return ticket;
|
|
2989
|
+
}
|
|
1655
2990
|
async listTickets(filter) {
|
|
1656
2991
|
if (filter?.project && !isProjectAuthorized(filter.project, this.config.allowedProjects)) {
|
|
1657
2992
|
throw new EsedreAuthorizationError(filter.project);
|
|
1658
2993
|
}
|
|
1659
2994
|
const tickets = await this.target.listTickets(filter);
|
|
1660
2995
|
if (!this.config.allowedProjects || this.config.allowedProjects.length === 0) {
|
|
1661
|
-
return tickets;
|
|
2996
|
+
return tickets.map((t) => this.redactLinks(t));
|
|
1662
2997
|
}
|
|
1663
2998
|
return tickets.filter((t) => {
|
|
1664
2999
|
const code = t.projectDescriptor?.code || t.meta.project;
|
|
1665
3000
|
if (!code) return true;
|
|
1666
3001
|
return isProjectAuthorized(code, this.config.allowedProjects);
|
|
1667
|
-
});
|
|
3002
|
+
}).map((t) => this.redactLinks(t));
|
|
1668
3003
|
}
|
|
1669
3004
|
async getTicket(id) {
|
|
1670
3005
|
const ticket = await this.target.getTicket(id);
|
|
@@ -1673,7 +3008,7 @@ var SecurityFilter = class {
|
|
|
1673
3008
|
if (code && !isProjectAuthorized(code, this.config.allowedProjects)) {
|
|
1674
3009
|
throw new EsedreAuthorizationError(code);
|
|
1675
3010
|
}
|
|
1676
|
-
return ticket;
|
|
3011
|
+
return this.redactLinks(ticket);
|
|
1677
3012
|
}
|
|
1678
3013
|
async createTicket(input) {
|
|
1679
3014
|
const targetCode = input.projectCode || this.config.projectCode;
|
|
@@ -1725,132 +3060,88 @@ var SecurityFilter = class {
|
|
|
1725
3060
|
}
|
|
1726
3061
|
return this.target.addComment(id, comment);
|
|
1727
3062
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
dim: isColorSupported ? "\x1B[2m" : "",
|
|
1736
|
-
cyan: isColorSupported ? "\x1B[36m" : "",
|
|
1737
|
-
green: isColorSupported ? "\x1B[32m" : "",
|
|
1738
|
-
yellow: isColorSupported ? "\x1B[33m" : "",
|
|
1739
|
-
magenta: isColorSupported ? "\x1B[35m" : "",
|
|
1740
|
-
red: isColorSupported ? "\x1B[31m" : "",
|
|
1741
|
-
blue: isColorSupported ? "\x1B[34m" : ""
|
|
1742
|
-
};
|
|
1743
|
-
function formatTicketListTable(tickets) {
|
|
1744
|
-
if (tickets.length === 0) {
|
|
1745
|
-
return `${colors.dim}No tickets found matching criteria.${colors.reset}`;
|
|
1746
|
-
}
|
|
1747
|
-
const rows = tickets.map((t) => {
|
|
1748
|
-
const id = t.projectDescriptor?.code ? `${t.projectDescriptor.code}-${t.meta.id}` : `#${t.meta.id}`;
|
|
1749
|
-
const project = t.projectDescriptor?.code || "CORE";
|
|
1750
|
-
const type = t.meta.type || t.meta.category || "Feature";
|
|
1751
|
-
const status = t.meta.status;
|
|
1752
|
-
const title = t.meta.title;
|
|
1753
|
-
return { id, project, type, status, title };
|
|
1754
|
-
});
|
|
1755
|
-
const idWidth = Math.max(4, ...rows.map((r) => r.id.length));
|
|
1756
|
-
const projWidth = Math.max(7, ...rows.map((r) => r.project.length));
|
|
1757
|
-
const typeWidth = Math.max(8, ...rows.map((r) => r.type.length));
|
|
1758
|
-
const statusWidth = Math.max(14, ...rows.map((r) => r.status.length));
|
|
1759
|
-
const header = `${colors.bold}${pad("ID", idWidth)} ${pad("Project", projWidth)} ${pad("Type", typeWidth)} ${pad("Status", statusWidth)} Title${colors.reset}`;
|
|
1760
|
-
const divider = `${colors.dim}${"-".repeat(idWidth)} ${"-".repeat(projWidth)} ${"-".repeat(typeWidth)} ${"-".repeat(statusWidth)} ${"-".repeat(40)}${colors.reset}`;
|
|
1761
|
-
const formattedRows = rows.map((r) => {
|
|
1762
|
-
const statusColored = colorStatus(r.status);
|
|
1763
|
-
const typeColored = colorType(r.type);
|
|
1764
|
-
const projColored = `${colors.magenta}${r.project}${colors.reset}`;
|
|
1765
|
-
return `${colors.bold}${pad(r.id, idWidth)}${colors.reset} ${pad(projColored, projWidth + (isColorSupported ? colors.magenta.length + colors.reset.length : 0))} ${pad(typeColored, typeWidth + (isColorSupported ? 9 : 0))} ${pad(statusColored, statusWidth + (isColorSupported ? 9 : 0))} ${r.title}`;
|
|
1766
|
-
});
|
|
1767
|
-
return [header, divider, ...formattedRows].join("\n");
|
|
1768
|
-
}
|
|
1769
|
-
function formatTicketDetail(ticket) {
|
|
1770
|
-
const { meta, detail, comments, planMarkdown, projectDescriptor } = ticket;
|
|
1771
|
-
const lines = [];
|
|
1772
|
-
lines.push(`${colors.bold}${colors.cyan}Ticket #${meta.project ? `${meta.project}-${meta.id}` : meta.id}: ${meta.title}${colors.reset}`);
|
|
1773
|
-
lines.push(`${colors.dim}${"=".repeat(60)}${colors.reset}`);
|
|
1774
|
-
lines.push(`${colors.bold}Project:${colors.reset} ${projectDescriptor ? `${projectDescriptor.code} - ${projectDescriptor.name}` : "Default"}`);
|
|
1775
|
-
lines.push(`${colors.bold}Type:${colors.reset} ${colorType(meta.type || meta.category || "Feature")}`);
|
|
1776
|
-
lines.push(`${colors.bold}Status:${colors.reset} ${colorStatus(meta.status)}`);
|
|
1777
|
-
lines.push(`${colors.bold}Complexity:${colors.reset} ${meta.complexity || "Medium"}`);
|
|
1778
|
-
lines.push(`${colors.bold}Effort:${colors.reset} ${meta.estimatedEffort || "N/A"}`);
|
|
1779
|
-
lines.push(`${colors.bold}Submitted By:${colors.reset} ${meta.submittedBy || "Unknown"}`);
|
|
1780
|
-
if (meta.featureFlag) {
|
|
1781
|
-
lines.push(`${colors.bold}Feature Flag:${colors.reset} ${colors.yellow}${meta.featureFlag}${colors.reset}`);
|
|
1782
|
-
}
|
|
1783
|
-
if (detail?.summary) {
|
|
1784
|
-
lines.push("");
|
|
1785
|
-
lines.push(`${colors.bold}Summary:${colors.reset}`);
|
|
1786
|
-
lines.push(detail.summary);
|
|
1787
|
-
}
|
|
1788
|
-
if (detail?.breakdown && detail.breakdown.length > 0) {
|
|
1789
|
-
lines.push("");
|
|
1790
|
-
lines.push(`${colors.bold}Feature Breakdown:${colors.reset}`);
|
|
1791
|
-
for (const b of detail.breakdown) {
|
|
1792
|
-
lines.push(` \u2022 ${b}`);
|
|
3063
|
+
async listMilestones(projectCode) {
|
|
3064
|
+
if (projectCode && projectCode !== "all" && !isProjectAuthorized(projectCode, this.config.allowedProjects)) {
|
|
3065
|
+
throw new EsedreAuthorizationError(projectCode);
|
|
3066
|
+
}
|
|
3067
|
+
const all = await this.target.listMilestones(projectCode);
|
|
3068
|
+
if (!this.config.allowedProjects || this.config.allowedProjects.length === 0) {
|
|
3069
|
+
return all;
|
|
1793
3070
|
}
|
|
3071
|
+
if (this.config.allowedProjects.includes("*")) {
|
|
3072
|
+
return all;
|
|
3073
|
+
}
|
|
3074
|
+
return all.filter((m) => isProjectAuthorized(m.project, this.config.allowedProjects));
|
|
1794
3075
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
3076
|
+
async getMilestone(id, projectCode) {
|
|
3077
|
+
if (projectCode && projectCode !== "all" && !isProjectAuthorized(projectCode, this.config.allowedProjects)) {
|
|
3078
|
+
throw new EsedreAuthorizationError(projectCode);
|
|
3079
|
+
}
|
|
3080
|
+
const milestone = await this.target.getMilestone(id, projectCode);
|
|
3081
|
+
if (!milestone) return null;
|
|
3082
|
+
if (milestone.project && !isProjectAuthorized(milestone.project, this.config.allowedProjects)) {
|
|
3083
|
+
throw new EsedreAuthorizationError(milestone.project);
|
|
1800
3084
|
}
|
|
3085
|
+
return milestone;
|
|
1801
3086
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
lines.push(` \u2022 ${q}`);
|
|
3087
|
+
async createMilestone(input) {
|
|
3088
|
+
const targetCode = input.projectCode || this.config.projectCode;
|
|
3089
|
+
if (targetCode && !isProjectAuthorized(targetCode, this.config.allowedProjects)) {
|
|
3090
|
+
throw new EsedreAuthorizationError(targetCode);
|
|
1807
3091
|
}
|
|
3092
|
+
return this.target.createMilestone(input);
|
|
1808
3093
|
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
3094
|
+
async updateMilestone(id, input, projectCode) {
|
|
3095
|
+
const existing = await this.target.getMilestone(id, projectCode);
|
|
3096
|
+
if (existing) {
|
|
3097
|
+
if (existing.project && !isProjectAuthorized(existing.project, this.config.allowedProjects)) {
|
|
3098
|
+
throw new EsedreAuthorizationError(existing.project);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
return this.target.updateMilestone(id, input, projectCode);
|
|
1813
3102
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
3103
|
+
async deleteMilestone(id, projectCode) {
|
|
3104
|
+
const existing = await this.target.getMilestone(id, projectCode);
|
|
3105
|
+
if (existing) {
|
|
3106
|
+
if (existing.project && !isProjectAuthorized(existing.project, this.config.allowedProjects)) {
|
|
3107
|
+
throw new EsedreAuthorizationError(existing.project);
|
|
3108
|
+
}
|
|
1819
3109
|
}
|
|
3110
|
+
return this.target.deleteMilestone(id, projectCode);
|
|
1820
3111
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
3112
|
+
async addTicketLink(sourceId, relation, targetId, options) {
|
|
3113
|
+
const sourceTicket = await this.target.getTicket(sourceId);
|
|
3114
|
+
if (sourceTicket) {
|
|
3115
|
+
const code = sourceTicket.projectDescriptor?.code || sourceTicket.meta.project;
|
|
3116
|
+
if (code && !isProjectAuthorized(code, this.config.allowedProjects)) {
|
|
3117
|
+
throw new EsedreAuthorizationError(code);
|
|
3118
|
+
}
|
|
3119
|
+
} else if (options?.project && !isProjectAuthorized(options.project, this.config.allowedProjects)) {
|
|
3120
|
+
throw new EsedreAuthorizationError(options.project);
|
|
3121
|
+
}
|
|
3122
|
+
const result = await this.target.addTicketLink(sourceId, relation, targetId, options);
|
|
3123
|
+
return {
|
|
3124
|
+
source: this.redactLinks(result.source),
|
|
3125
|
+
target: result.target ? this.redactLinks(result.target) : void 0
|
|
3126
|
+
};
|
|
1836
3127
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
3128
|
+
async removeTicketLink(sourceId, targetId, options) {
|
|
3129
|
+
const sourceTicket = await this.target.getTicket(sourceId);
|
|
3130
|
+
if (sourceTicket) {
|
|
3131
|
+
const code = sourceTicket.projectDescriptor?.code || sourceTicket.meta.project;
|
|
3132
|
+
if (code && !isProjectAuthorized(code, this.config.allowedProjects)) {
|
|
3133
|
+
throw new EsedreAuthorizationError(code);
|
|
3134
|
+
}
|
|
3135
|
+
} else if (options?.project && !isProjectAuthorized(options.project, this.config.allowedProjects)) {
|
|
3136
|
+
throw new EsedreAuthorizationError(options.project);
|
|
3137
|
+
}
|
|
3138
|
+
const result = await this.target.removeTicketLink(sourceId, targetId, options);
|
|
3139
|
+
return {
|
|
3140
|
+
source: this.redactLinks(result.source),
|
|
3141
|
+
target: result.target ? this.redactLinks(result.target) : void 0
|
|
3142
|
+
};
|
|
1852
3143
|
}
|
|
1853
|
-
}
|
|
3144
|
+
};
|
|
1854
3145
|
|
|
1855
3146
|
// src/mcp/server.ts
|
|
1856
3147
|
import readline from "node:readline";
|
|
@@ -1906,7 +3197,7 @@ var EsedreMcpServer = class {
|
|
|
1906
3197
|
},
|
|
1907
3198
|
serverInfo: {
|
|
1908
3199
|
name: "esedre",
|
|
1909
|
-
version:
|
|
3200
|
+
version: CURRENT_ESEDRE_VERSION
|
|
1910
3201
|
}
|
|
1911
3202
|
}
|
|
1912
3203
|
};
|
|
@@ -2070,7 +3361,11 @@ Status: ${ticket.meta.status}`;
|
|
|
2070
3361
|
status: { type: "string", enum: ["Planned", "In Development", "Completed", "Rejected"], description: "Ticket status" },
|
|
2071
3362
|
type: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket type" },
|
|
2072
3363
|
category: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket category (legacy alias for type)" },
|
|
2073
|
-
|
|
3364
|
+
priority: { type: "string", enum: ["Critical", "High", "Medium", "Low", "none"], description: "Ticket priority filter" },
|
|
3365
|
+
milestone: { type: "string", description: "Filter tickets by milestone name or ID" },
|
|
3366
|
+
search: { type: "string", description: "Search keywords in title or ID" },
|
|
3367
|
+
isBlocked: { type: "boolean", description: "Filter tickets blocked by uncompleted tickets" },
|
|
3368
|
+
linkedTo: { type: "string", description: "Filter tickets linked to a specific ticket ID or key" }
|
|
2074
3369
|
}
|
|
2075
3370
|
}
|
|
2076
3371
|
},
|
|
@@ -2119,10 +3414,12 @@ Status: ${ticket.meta.status}`;
|
|
|
2119
3414
|
project: { type: "string", description: "Target project code (e.g. CORE, WEB, DOCS)" },
|
|
2120
3415
|
type: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket type" },
|
|
2121
3416
|
category: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket category" },
|
|
3417
|
+
priority: { type: "string", enum: ["Critical", "High", "Medium", "Low"], description: "Ticket priority" },
|
|
2122
3418
|
complexity: { type: "string", description: "Complexity (e.g. Low, Medium, High)" },
|
|
2123
3419
|
effort: { type: "string", description: "Estimated effort (e.g. 2.0 - 4.0 hours)" },
|
|
2124
3420
|
summary: { type: "string", description: "Initial feature summary" },
|
|
2125
3421
|
detail: { type: "string", description: "Initial specification, feature breakdown, or technical detail markdown" },
|
|
3422
|
+
milestone: { type: "string", description: "Assign ticket to milestone" },
|
|
2126
3423
|
author: { type: "string", description: "Submitting author name" }
|
|
2127
3424
|
},
|
|
2128
3425
|
required: ["title"]
|
|
@@ -2138,11 +3435,13 @@ Status: ${ticket.meta.status}`;
|
|
|
2138
3435
|
status: { type: "string", enum: ["Planned", "In Development", "Completed", "Rejected"] },
|
|
2139
3436
|
type: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"] },
|
|
2140
3437
|
category: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"] },
|
|
3438
|
+
priority: { type: "string", enum: ["Critical", "High", "Medium", "Low", "none"], description: "Ticket priority (pass none or null to clear)" },
|
|
2141
3439
|
title: { type: "string", description: "New title (max 48 characters)" },
|
|
2142
3440
|
complexity: { type: "string", description: "Complexity (e.g. Low, Medium, High)" },
|
|
2143
3441
|
effort: { type: "string", description: "Estimated effort (e.g. 2.0 - 4.0 hours)" },
|
|
2144
3442
|
inDevelopment: { type: "boolean", description: "Active development toggle" },
|
|
2145
3443
|
featureFlag: { type: "string", description: "Feature flag name" },
|
|
3444
|
+
milestone: { type: "string", description: 'Update ticket milestone ("none" to clear)' },
|
|
2146
3445
|
lastHash: { type: "string", description: "Optimistic concurrency control: last known sha1 hash of the ticket" }
|
|
2147
3446
|
},
|
|
2148
3447
|
required: ["ticketId"]
|
|
@@ -2158,7 +3457,94 @@ Status: ${ticket.meta.status}`;
|
|
|
2158
3457
|
text: { type: "string", description: "Comment message" },
|
|
2159
3458
|
author: { type: "string", description: "Author name (e.g. Antigravity, Developer)" }
|
|
2160
3459
|
},
|
|
2161
|
-
required: ["ticketId", "text"]
|
|
3460
|
+
required: ["ticketId", "text"]
|
|
3461
|
+
}
|
|
3462
|
+
},
|
|
3463
|
+
{
|
|
3464
|
+
name: "esedre_list_milestones",
|
|
3465
|
+
description: "List roadmap milestones with optional project filter.",
|
|
3466
|
+
inputSchema: {
|
|
3467
|
+
type: "object",
|
|
3468
|
+
properties: {
|
|
3469
|
+
project: { type: "string", description: "Project code (e.g. Esedre, Alce)" }
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
},
|
|
3473
|
+
{
|
|
3474
|
+
name: "esedre_get_milestone",
|
|
3475
|
+
description: "Retrieve milestone details by ID or title.",
|
|
3476
|
+
inputSchema: {
|
|
3477
|
+
type: "object",
|
|
3478
|
+
properties: {
|
|
3479
|
+
id: { type: ["integer", "string"], description: "Milestone ID or title" },
|
|
3480
|
+
project: { type: "string", description: "Project code" }
|
|
3481
|
+
},
|
|
3482
|
+
required: ["id"]
|
|
3483
|
+
}
|
|
3484
|
+
},
|
|
3485
|
+
{
|
|
3486
|
+
name: "esedre_create_milestone",
|
|
3487
|
+
description: "Create a new milestone with optional umbrella feature flag.",
|
|
3488
|
+
inputSchema: {
|
|
3489
|
+
type: "object",
|
|
3490
|
+
properties: {
|
|
3491
|
+
title: { type: "string", description: "Milestone title" },
|
|
3492
|
+
project: { type: "string", description: "Target project code" },
|
|
3493
|
+
description: { type: "string", description: "Milestone goal and description" },
|
|
3494
|
+
featureFlag: { type: "string", description: "Optional umbrella feature flag inherited by member tickets" },
|
|
3495
|
+
targetDate: { type: "string", description: "Optional target completion date" },
|
|
3496
|
+
status: { type: "string", enum: ["Planned", "Active", "Completed", "Closed"], description: "Milestone status" }
|
|
3497
|
+
},
|
|
3498
|
+
required: ["title"]
|
|
3499
|
+
}
|
|
3500
|
+
},
|
|
3501
|
+
{
|
|
3502
|
+
name: "esedre_update_milestone",
|
|
3503
|
+
description: "Update milestone state, umbrella feature flag, status, or target date.",
|
|
3504
|
+
inputSchema: {
|
|
3505
|
+
type: "object",
|
|
3506
|
+
properties: {
|
|
3507
|
+
id: { type: ["integer", "string"], description: "Milestone ID or title" },
|
|
3508
|
+
project: { type: "string", description: "Project code" },
|
|
3509
|
+
title: { type: "string", description: "New milestone title" },
|
|
3510
|
+
description: { type: "string", description: "Milestone goal and description" },
|
|
3511
|
+
featureFlag: { type: "string", description: 'Umbrella feature flag ("none" to clear)' },
|
|
3512
|
+
targetDate: { type: "string", description: 'Target date ("none" to clear)' },
|
|
3513
|
+
status: { type: "string", enum: ["Planned", "Active", "Completed", "Closed"], description: "Milestone status" }
|
|
3514
|
+
},
|
|
3515
|
+
required: ["id"]
|
|
3516
|
+
}
|
|
3517
|
+
},
|
|
3518
|
+
{
|
|
3519
|
+
name: "esedre_link_ticket",
|
|
3520
|
+
description: "Establish a bi-directional link between two tickets (within or across projects) with cycle detection.",
|
|
3521
|
+
inputSchema: {
|
|
3522
|
+
type: "object",
|
|
3523
|
+
properties: {
|
|
3524
|
+
sourceTicketId: { type: ["integer", "string"], description: "Source ticket ID or compound key" },
|
|
3525
|
+
relation: {
|
|
3526
|
+
type: "string",
|
|
3527
|
+
enum: ["relates-to", "blocks", "blocked-by", "parent-of", "child-of", "duplicates", "duplicated-by"],
|
|
3528
|
+
description: "Link relation type"
|
|
3529
|
+
},
|
|
3530
|
+
targetTicketId: { type: ["integer", "string"], description: "Target ticket ID or compound key" },
|
|
3531
|
+
author: { type: "string", description: "Author creating the link" },
|
|
3532
|
+
project: { type: "string", description: "Project code context for unqualified source ticket ID" }
|
|
3533
|
+
},
|
|
3534
|
+
required: ["sourceTicketId", "relation", "targetTicketId"]
|
|
3535
|
+
}
|
|
3536
|
+
},
|
|
3537
|
+
{
|
|
3538
|
+
name: "esedre_unlink_ticket",
|
|
3539
|
+
description: "Remove a link between two tickets and its reciprocal link.",
|
|
3540
|
+
inputSchema: {
|
|
3541
|
+
type: "object",
|
|
3542
|
+
properties: {
|
|
3543
|
+
sourceTicketId: { type: ["integer", "string"], description: "Source ticket ID or compound key" },
|
|
3544
|
+
targetTicketId: { type: ["integer", "string"], description: "Target ticket ID or compound key" },
|
|
3545
|
+
project: { type: "string", description: "Project code context for unqualified source ticket ID" }
|
|
3546
|
+
},
|
|
3547
|
+
required: ["sourceTicketId", "targetTicketId"]
|
|
2162
3548
|
}
|
|
2163
3549
|
}
|
|
2164
3550
|
];
|
|
@@ -2173,7 +3559,11 @@ Status: ${ticket.meta.status}`;
|
|
|
2173
3559
|
status: args.status,
|
|
2174
3560
|
type,
|
|
2175
3561
|
category: type,
|
|
2176
|
-
|
|
3562
|
+
priority: args.priority,
|
|
3563
|
+
milestone: args.milestone,
|
|
3564
|
+
search: args.search,
|
|
3565
|
+
isBlocked: args.isBlocked !== void 0 ? Boolean(args.isBlocked) : void 0,
|
|
3566
|
+
linkedTo: args.linkedTo
|
|
2177
3567
|
});
|
|
2178
3568
|
return tickets.map((t) => ({
|
|
2179
3569
|
id: t.meta.id,
|
|
@@ -2181,10 +3571,16 @@ Status: ${ticket.meta.status}`;
|
|
|
2181
3571
|
type: t.meta.type || t.meta.category,
|
|
2182
3572
|
category: t.meta.type || t.meta.category,
|
|
2183
3573
|
status: t.meta.status,
|
|
3574
|
+
priority: t.meta.priority,
|
|
2184
3575
|
complexity: t.meta.complexity,
|
|
2185
3576
|
effort: t.meta.estimatedEffort,
|
|
3577
|
+
milestone: t.meta.milestone,
|
|
3578
|
+
featureFlag: t.meta.featureFlag,
|
|
3579
|
+
inheritedFeatureFlag: t.meta.inheritedFeatureFlag,
|
|
2186
3580
|
project: t.projectDescriptor?.code || t.meta.project || "UNASSIGNED",
|
|
2187
|
-
sha1: t.sha1 || t.meta.sha1
|
|
3581
|
+
sha1: t.sha1 || t.meta.sha1,
|
|
3582
|
+
isBlocked: t.isBlocked,
|
|
3583
|
+
links: t.links
|
|
2188
3584
|
}));
|
|
2189
3585
|
}
|
|
2190
3586
|
case "get_ticket": {
|
|
@@ -2209,12 +3605,14 @@ Status: ${ticket.meta.status}`;
|
|
|
2209
3605
|
title: args.title,
|
|
2210
3606
|
type,
|
|
2211
3607
|
category: type,
|
|
3608
|
+
priority: args.priority,
|
|
2212
3609
|
projectCode: args.project,
|
|
2213
3610
|
complexity: args.complexity,
|
|
2214
3611
|
estimatedEffort: args.effort,
|
|
2215
3612
|
summary: args.summary,
|
|
2216
3613
|
detail: args.detail || args.detailMarkdown,
|
|
2217
3614
|
detailMarkdown: args.detailMarkdown || args.detail,
|
|
3615
|
+
milestone: args.milestone,
|
|
2218
3616
|
submittedBy: args.author || "Agent"
|
|
2219
3617
|
});
|
|
2220
3618
|
return created;
|
|
@@ -2228,11 +3626,13 @@ Status: ${ticket.meta.status}`;
|
|
|
2228
3626
|
updates.category = type;
|
|
2229
3627
|
}
|
|
2230
3628
|
if (args.status) updates.status = args.status;
|
|
3629
|
+
if (args.priority !== void 0) updates.priority = args.priority;
|
|
2231
3630
|
if (args.title) updates.title = args.title;
|
|
2232
3631
|
if (args.complexity) updates.complexity = args.complexity;
|
|
2233
3632
|
if (args.effort) updates.estimatedEffort = args.effort;
|
|
2234
3633
|
if (args.inDevelopment !== void 0) updates.isActivePlanning = Boolean(args.inDevelopment);
|
|
2235
3634
|
if (args.featureFlag) updates.featureFlag = args.featureFlag;
|
|
3635
|
+
if (args.milestone !== void 0) updates.milestone = args.milestone;
|
|
2236
3636
|
const updated = await this.storage.updateTicket(id, updates, args.lastHash);
|
|
2237
3637
|
return updated;
|
|
2238
3638
|
}
|
|
@@ -2244,6 +3644,63 @@ Status: ${ticket.meta.status}`;
|
|
|
2244
3644
|
});
|
|
2245
3645
|
return comment;
|
|
2246
3646
|
}
|
|
3647
|
+
case "list_milestones": {
|
|
3648
|
+
return this.storage.listMilestones(args.project);
|
|
3649
|
+
}
|
|
3650
|
+
case "get_milestone": {
|
|
3651
|
+
const m = await this.storage.getMilestone(args.id, args.project);
|
|
3652
|
+
if (!m) throw new Error(`Milestone '${args.id}' not found`);
|
|
3653
|
+
const tickets = await this.storage.listTickets({ project: args.project || m.project, milestone: String(m.id) });
|
|
3654
|
+
return {
|
|
3655
|
+
...m,
|
|
3656
|
+
tickets
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
case "create_milestone": {
|
|
3660
|
+
return this.storage.createMilestone({
|
|
3661
|
+
title: args.title,
|
|
3662
|
+
projectCode: args.project,
|
|
3663
|
+
description: args.description,
|
|
3664
|
+
featureFlag: args.featureFlag,
|
|
3665
|
+
targetDate: args.targetDate,
|
|
3666
|
+
status: args.status
|
|
3667
|
+
});
|
|
3668
|
+
}
|
|
3669
|
+
case "update_milestone": {
|
|
3670
|
+
return this.storage.updateMilestone(args.id, {
|
|
3671
|
+
title: args.title,
|
|
3672
|
+
description: args.description,
|
|
3673
|
+
featureFlag: args.featureFlag,
|
|
3674
|
+
targetDate: args.targetDate,
|
|
3675
|
+
status: args.status
|
|
3676
|
+
}, args.project);
|
|
3677
|
+
}
|
|
3678
|
+
case "link_ticket": {
|
|
3679
|
+
const sourceId = this.parseTicketId(args.sourceTicketId);
|
|
3680
|
+
const targetId = this.parseTicketId(args.targetTicketId);
|
|
3681
|
+
const relation = String(args.relation).toLowerCase().replace(/_/g, "-");
|
|
3682
|
+
const result = await this.storage.addTicketLink(sourceId, relation, targetId, {
|
|
3683
|
+
author: args.author || "Agent",
|
|
3684
|
+
project: args.project
|
|
3685
|
+
});
|
|
3686
|
+
return {
|
|
3687
|
+
success: true,
|
|
3688
|
+
source: result.source,
|
|
3689
|
+
target: result.target
|
|
3690
|
+
};
|
|
3691
|
+
}
|
|
3692
|
+
case "unlink_ticket": {
|
|
3693
|
+
const sourceId = this.parseTicketId(args.sourceTicketId);
|
|
3694
|
+
const targetId = this.parseTicketId(args.targetTicketId);
|
|
3695
|
+
const result = await this.storage.removeTicketLink(sourceId, targetId, {
|
|
3696
|
+
project: args.project
|
|
3697
|
+
});
|
|
3698
|
+
return {
|
|
3699
|
+
success: true,
|
|
3700
|
+
source: result.source,
|
|
3701
|
+
target: result.target
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
2247
3704
|
default:
|
|
2248
3705
|
throw new Error(`Unknown tool: ${name}`);
|
|
2249
3706
|
}
|
|
@@ -2443,7 +3900,7 @@ import path6 from "node:path";
|
|
|
2443
3900
|
// src/server/daemon.ts
|
|
2444
3901
|
import fs5 from "node:fs";
|
|
2445
3902
|
import path5 from "node:path";
|
|
2446
|
-
import
|
|
3903
|
+
import os3 from "node:os";
|
|
2447
3904
|
import http from "node:http";
|
|
2448
3905
|
import { spawn, spawnSync } from "node:child_process";
|
|
2449
3906
|
import { fileURLToPath } from "node:url";
|
|
@@ -2453,7 +3910,7 @@ function getGlobalEsedreDir() {
|
|
|
2453
3910
|
if (process.env.ESEDRE_GLOBAL_DIR) {
|
|
2454
3911
|
return process.env.ESEDRE_GLOBAL_DIR;
|
|
2455
3912
|
}
|
|
2456
|
-
return path5.join(
|
|
3913
|
+
return path5.join(os3.homedir(), ".esedre");
|
|
2457
3914
|
}
|
|
2458
3915
|
function ensureGlobalEsedreStore() {
|
|
2459
3916
|
const globalDir = getGlobalEsedreDir();
|
|
@@ -2721,6 +4178,27 @@ async function stopDaemon(options = {}) {
|
|
|
2721
4178
|
}
|
|
2722
4179
|
return false;
|
|
2723
4180
|
}
|
|
4181
|
+
try {
|
|
4182
|
+
if (process.platform === "win32") {
|
|
4183
|
+
const out = spawnSync("netstat", ["-ano", "-p", "tcp"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).stdout || "";
|
|
4184
|
+
const lines = out.split("\n");
|
|
4185
|
+
for (const line of lines) {
|
|
4186
|
+
if (line.includes(`:${port}`) && line.includes("LISTENING")) {
|
|
4187
|
+
const parts = line.trim().split(/\s+/);
|
|
4188
|
+
const foundPid = parseInt(parts[parts.length - 1], 10);
|
|
4189
|
+
if (!isNaN(foundPid) && foundPid > 0) {
|
|
4190
|
+
pid = foundPid;
|
|
4191
|
+
break;
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
} else {
|
|
4196
|
+
const out = spawnSync("lsof", ["-ti", `tcp:${port}`], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).stdout || "";
|
|
4197
|
+
const foundPid = parseInt(out.trim().split("\n")[0], 10);
|
|
4198
|
+
if (!isNaN(foundPid) && foundPid > 0) pid = foundPid;
|
|
4199
|
+
}
|
|
4200
|
+
} catch {
|
|
4201
|
+
}
|
|
2724
4202
|
}
|
|
2725
4203
|
if (pid && isProcessAlive(pid)) {
|
|
2726
4204
|
try {
|
|
@@ -2828,49 +4306,49 @@ function classifyContent(currentContent, latestTemplate, historicHashes) {
|
|
|
2828
4306
|
}
|
|
2829
4307
|
var WRAPPER_CMD = `@echo off
|
|
2830
4308
|
REM Esedre Autonomous Ticketing & Project Planning Engine Wrapper (Windows CMD)
|
|
2831
|
-
|
|
4309
|
+
call :run %*
|
|
4310
|
+
exit /b %ERRORLEVEL%
|
|
4311
|
+
|
|
4312
|
+
:run
|
|
4313
|
+
if exist "%~dp0..\\dist\\esedre.mjs" (
|
|
4314
|
+
node "%~dp0..\\dist\\esedre.mjs" %*
|
|
4315
|
+
goto :eof
|
|
4316
|
+
)
|
|
4317
|
+
if exist "%~dp0..\\..\\esedre\\dist\\esedre.mjs" (
|
|
4318
|
+
node "%~dp0..\\..\\esedre\\dist\\esedre.mjs" %*
|
|
4319
|
+
goto :eof
|
|
4320
|
+
)
|
|
4321
|
+
if exist "%~dp0..\\esedre\\dist\\esedre.mjs" (
|
|
4322
|
+
node "%~dp0..\\esedre\\dist\\esedre.mjs" %*
|
|
4323
|
+
goto :eof
|
|
4324
|
+
)
|
|
4325
|
+
if exist "%~dp0..\\node_modules\\esedre\\dist\\esedre.mjs" (
|
|
4326
|
+
node "%~dp0..\\node_modules\\esedre\\dist\\esedre.mjs" %*
|
|
4327
|
+
goto :eof
|
|
4328
|
+
)
|
|
4329
|
+
if exist "%~dp0..\\node_modules\\.bin\\ese.cmd" (
|
|
4330
|
+
call "%~dp0..\\node_modules\\.bin\\ese.cmd" %*
|
|
4331
|
+
goto :eof
|
|
4332
|
+
)
|
|
4333
|
+
if exist "%~dp0..\\node_modules\\.bin\\esedre.cmd" (
|
|
4334
|
+
call "%~dp0..\\node_modules\\.bin\\esedre.cmd" %*
|
|
4335
|
+
goto :eof
|
|
4336
|
+
)
|
|
2832
4337
|
|
|
2833
4338
|
where ese >nul 2>nul
|
|
2834
|
-
if %ERRORLEVEL% equ 0
|
|
4339
|
+
if %ERRORLEVEL% equ 0 (
|
|
4340
|
+
call ese %*
|
|
4341
|
+
goto :eof
|
|
4342
|
+
)
|
|
2835
4343
|
|
|
2836
4344
|
where esedre >nul 2>nul
|
|
2837
|
-
if %ERRORLEVEL% equ 0
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
if exist "%~dp0..\\node_modules\\.bin\\ese.cmd" goto use_node_modules_ese
|
|
2842
|
-
if exist "%~dp0..\\node_modules\\.bin\\esedre.cmd" goto use_node_modules_esedre
|
|
4345
|
+
if %ERRORLEVEL% equ 0 (
|
|
4346
|
+
call esedre %*
|
|
4347
|
+
goto :eof
|
|
4348
|
+
)
|
|
2843
4349
|
|
|
2844
4350
|
call npx --yes esedre %*
|
|
2845
|
-
goto
|
|
2846
|
-
|
|
2847
|
-
:use_ese
|
|
2848
|
-
call ese %*
|
|
2849
|
-
goto done
|
|
2850
|
-
|
|
2851
|
-
:use_esedre
|
|
2852
|
-
call esedre %*
|
|
2853
|
-
goto done
|
|
2854
|
-
|
|
2855
|
-
:use_sibling_dist
|
|
2856
|
-
node "%~dp0..\\..\\esedre\\dist\\esedre.mjs" %*
|
|
2857
|
-
goto done
|
|
2858
|
-
|
|
2859
|
-
:use_local_dist
|
|
2860
|
-
node "%~dp0..\\esedre\\dist\\esedre.mjs" %*
|
|
2861
|
-
goto done
|
|
2862
|
-
|
|
2863
|
-
:use_node_modules_ese
|
|
2864
|
-
call "%~dp0..\\node_modules\\.bin\\ese.cmd" %*
|
|
2865
|
-
goto done
|
|
2866
|
-
|
|
2867
|
-
:use_node_modules_esedre
|
|
2868
|
-
call "%~dp0..\\node_modules\\.bin\\esedre.cmd" %*
|
|
2869
|
-
goto done
|
|
2870
|
-
|
|
2871
|
-
:done
|
|
2872
|
-
endlocal
|
|
2873
|
-
exit /b %ERRORLEVEL%
|
|
4351
|
+
goto :eof
|
|
2874
4352
|
`;
|
|
2875
4353
|
var WRAPPER_PS1 = `# Esedre Autonomous Ticketing & Project Planning Engine Wrapper (PowerShell)
|
|
2876
4354
|
$ErrorActionPreference = "Stop"
|
|
@@ -2884,12 +4362,9 @@ try {
|
|
|
2884
4362
|
|
|
2885
4363
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
2886
4364
|
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
}
|
|
2891
|
-
if (Get-Command "esedre" -ErrorAction SilentlyContinue) {
|
|
2892
|
-
& esedre @args
|
|
4365
|
+
$inRepoEsedre = Join-Path $scriptDir "..\\dist\\esedre.mjs"
|
|
4366
|
+
if (Test-Path $inRepoEsedre) {
|
|
4367
|
+
& node $inRepoEsedre @args
|
|
2893
4368
|
exit $LASTEXITCODE
|
|
2894
4369
|
}
|
|
2895
4370
|
$siblingEsedre = Join-Path $scriptDir "..\\..\\esedre\\dist\\esedre.mjs"
|
|
@@ -2902,6 +4377,11 @@ if (Test-Path $distEsedre) {
|
|
|
2902
4377
|
& node $distEsedre @args
|
|
2903
4378
|
exit $LASTEXITCODE
|
|
2904
4379
|
}
|
|
4380
|
+
$nodeModulesEsedre = Join-Path $scriptDir "..\\node_modules\\esedre\\dist\\esedre.mjs"
|
|
4381
|
+
if (Test-Path $nodeModulesEsedre) {
|
|
4382
|
+
& node $nodeModulesEsedre @args
|
|
4383
|
+
exit $LASTEXITCODE
|
|
4384
|
+
}
|
|
2905
4385
|
$localBin = Join-Path $scriptDir "..\\node_modules\\.bin\\ese.cmd"
|
|
2906
4386
|
if (Test-Path $localBin) {
|
|
2907
4387
|
& $localBin @args
|
|
@@ -2912,6 +4392,14 @@ if (Test-Path $localEsedreBin) {
|
|
|
2912
4392
|
& $localEsedreBin @args
|
|
2913
4393
|
exit $LASTEXITCODE
|
|
2914
4394
|
}
|
|
4395
|
+
if (Get-Command "ese" -ErrorAction SilentlyContinue) {
|
|
4396
|
+
& ese @args
|
|
4397
|
+
exit $LASTEXITCODE
|
|
4398
|
+
}
|
|
4399
|
+
if (Get-Command "esedre" -ErrorAction SilentlyContinue) {
|
|
4400
|
+
& esedre @args
|
|
4401
|
+
exit $LASTEXITCODE
|
|
4402
|
+
}
|
|
2915
4403
|
& npx --yes esedre @args
|
|
2916
4404
|
exit $LASTEXITCODE
|
|
2917
4405
|
`;
|
|
@@ -2920,11 +4408,8 @@ var WRAPPER_BASH = `#!/usr/bin/env bash
|
|
|
2920
4408
|
set -e
|
|
2921
4409
|
DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
|
2922
4410
|
|
|
2923
|
-
if
|
|
2924
|
-
exec
|
|
2925
|
-
fi
|
|
2926
|
-
if command -v esedre >/dev/null 2>&1; then
|
|
2927
|
-
exec esedre "$@"
|
|
4411
|
+
if [ -f "$DIR/../dist/esedre.mjs" ]; then
|
|
4412
|
+
exec node "$DIR/../dist/esedre.mjs" "$@"
|
|
2928
4413
|
fi
|
|
2929
4414
|
if [ -f "$DIR/../../esedre/dist/esedre.mjs" ]; then
|
|
2930
4415
|
exec node "$DIR/../../esedre/dist/esedre.mjs" "$@"
|
|
@@ -2932,12 +4417,21 @@ fi
|
|
|
2932
4417
|
if [ -f "$DIR/../esedre/dist/esedre.mjs" ]; then
|
|
2933
4418
|
exec node "$DIR/../esedre/dist/esedre.mjs" "$@"
|
|
2934
4419
|
fi
|
|
4420
|
+
if [ -f "$DIR/../node_modules/esedre/dist/esedre.mjs" ]; then
|
|
4421
|
+
exec node "$DIR/../node_modules/esedre/dist/esedre.mjs" "$@"
|
|
4422
|
+
fi
|
|
2935
4423
|
if [ -f "$DIR/../node_modules/.bin/ese" ]; then
|
|
2936
4424
|
exec "$DIR/../node_modules/.bin/ese" "$@"
|
|
2937
4425
|
fi
|
|
2938
4426
|
if [ -f "$DIR/../node_modules/.bin/esedre" ]; then
|
|
2939
4427
|
exec "$DIR/../node_modules/.bin/esedre" "$@"
|
|
2940
4428
|
fi
|
|
4429
|
+
if command -v ese >/dev/null 2>&1; then
|
|
4430
|
+
exec ese "$@"
|
|
4431
|
+
fi
|
|
4432
|
+
if command -v esedre >/dev/null 2>&1; then
|
|
4433
|
+
exec esedre "$@"
|
|
4434
|
+
fi
|
|
2941
4435
|
exec npx --yes esedre "$@"
|
|
2942
4436
|
`;
|
|
2943
4437
|
var ESE_WRAPPER_CMD = `@echo off
|
|
@@ -3514,10 +5008,17 @@ function startUiServer(port, webDir) {
|
|
|
3514
5008
|
|
|
3515
5009
|
// src/server/apiServer.ts
|
|
3516
5010
|
import http3 from "node:http";
|
|
5011
|
+
var MAX_JSON_BODY_BYTES = 10 * 1024 * 1024;
|
|
3517
5012
|
function readJsonBody(req) {
|
|
3518
5013
|
return new Promise((resolve, reject) => {
|
|
3519
5014
|
let body = "";
|
|
5015
|
+
let bytesRead = 0;
|
|
3520
5016
|
req.on("data", (chunk) => {
|
|
5017
|
+
bytesRead += chunk.length;
|
|
5018
|
+
if (bytesRead > MAX_JSON_BODY_BYTES) {
|
|
5019
|
+
req.destroy(new Error("Payload Too Large: JSON body exceeds 10MB limit"));
|
|
5020
|
+
return;
|
|
5021
|
+
}
|
|
3521
5022
|
body += chunk;
|
|
3522
5023
|
});
|
|
3523
5024
|
req.on("end", () => {
|
|
@@ -3570,7 +5071,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3570
5071
|
res.writeHead(204, {
|
|
3571
5072
|
"Access-Control-Allow-Origin": "*",
|
|
3572
5073
|
"Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
|
|
3573
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
5074
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, x-esedre-allowed-projects"
|
|
3574
5075
|
});
|
|
3575
5076
|
res.end();
|
|
3576
5077
|
return true;
|
|
@@ -3602,12 +5103,14 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3602
5103
|
const status = url.searchParams.get("status") || void 0;
|
|
3603
5104
|
const type = url.searchParams.get("type") || url.searchParams.get("category") || void 0;
|
|
3604
5105
|
const category = type;
|
|
5106
|
+
const priority = url.searchParams.get("priority") || void 0;
|
|
3605
5107
|
const search = url.searchParams.get("search") || void 0;
|
|
3606
5108
|
const tickets = await reqStorage.listTickets({
|
|
3607
5109
|
project: project === "all" ? void 0 : project,
|
|
3608
5110
|
status,
|
|
3609
5111
|
type,
|
|
3610
5112
|
category,
|
|
5113
|
+
priority,
|
|
3611
5114
|
search
|
|
3612
5115
|
});
|
|
3613
5116
|
sendJson(res, 200, tickets);
|
|
@@ -3643,9 +5146,10 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3643
5146
|
const numKey = String(t.meta.id);
|
|
3644
5147
|
const code = t.projectDescriptor?.code || t.meta.project || "Profe";
|
|
3645
5148
|
const prefixedKey = `${code}-${t.meta.id}`;
|
|
3646
|
-
|
|
5149
|
+
const metaWithLinks = { ...t.meta, links: t.links || t.meta.links || [], isBlocked: t.isBlocked };
|
|
5150
|
+
metasMap[prefixedKey] = metaWithLinks;
|
|
3647
5151
|
if (!metasMap[numKey] || code.toUpperCase() === "PROF") {
|
|
3648
|
-
metasMap[numKey] =
|
|
5152
|
+
metasMap[numKey] = metaWithLinks;
|
|
3649
5153
|
}
|
|
3650
5154
|
commentsMap[prefixedKey] = t.comments || [];
|
|
3651
5155
|
if (!commentsMap[numKey] || code.toUpperCase() === "PROF") {
|
|
@@ -3747,6 +5251,12 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3747
5251
|
sendJson(res, 200, snapshot);
|
|
3748
5252
|
return true;
|
|
3749
5253
|
}
|
|
5254
|
+
if (pathname === "/api/planning/milestones") {
|
|
5255
|
+
const project = url.searchParams.get("project") || void 0;
|
|
5256
|
+
const milestones = await reqStorage.listMilestones(project);
|
|
5257
|
+
sendJson(res, 200, { milestones });
|
|
5258
|
+
return true;
|
|
5259
|
+
}
|
|
3750
5260
|
}
|
|
3751
5261
|
if (req.method === "POST") {
|
|
3752
5262
|
const body = await readJsonBody(req);
|
|
@@ -3783,6 +5293,59 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3783
5293
|
sendJson(res, 201, project);
|
|
3784
5294
|
return true;
|
|
3785
5295
|
}
|
|
5296
|
+
if (pathname === "/api/planning/update-project") {
|
|
5297
|
+
const { code, name, description, colors: colors2, techStack, groundingRules, guidelinesRef } = body;
|
|
5298
|
+
if (!code) {
|
|
5299
|
+
sendJson(res, 400, { error: "Project code is required." });
|
|
5300
|
+
return true;
|
|
5301
|
+
}
|
|
5302
|
+
if (name !== void 0) {
|
|
5303
|
+
const nameVal = validateProjectName(name);
|
|
5304
|
+
if (!nameVal.valid) {
|
|
5305
|
+
sendJson(res, 400, { error: nameVal.error });
|
|
5306
|
+
return true;
|
|
5307
|
+
}
|
|
5308
|
+
}
|
|
5309
|
+
invalidateApiCache();
|
|
5310
|
+
const updated = await reqStorage.updateProject({
|
|
5311
|
+
code,
|
|
5312
|
+
name,
|
|
5313
|
+
description,
|
|
5314
|
+
colors: colors2,
|
|
5315
|
+
techStack,
|
|
5316
|
+
groundingRules,
|
|
5317
|
+
guidelinesRef
|
|
5318
|
+
});
|
|
5319
|
+
sendJson(res, 200, updated);
|
|
5320
|
+
return true;
|
|
5321
|
+
}
|
|
5322
|
+
if (pathname === "/api/planning/rename-project") {
|
|
5323
|
+
const { oldCode, newCode, newName } = body;
|
|
5324
|
+
if (!oldCode || !newCode) {
|
|
5325
|
+
sendJson(res, 400, { error: "Both oldCode and newCode are required to rename a project." });
|
|
5326
|
+
return true;
|
|
5327
|
+
}
|
|
5328
|
+
const codeVal = validateProjectCode(newCode);
|
|
5329
|
+
if (!codeVal.valid) {
|
|
5330
|
+
sendJson(res, 400, { error: codeVal.error });
|
|
5331
|
+
return true;
|
|
5332
|
+
}
|
|
5333
|
+
if (newName !== void 0) {
|
|
5334
|
+
const nameVal = validateProjectName(newName);
|
|
5335
|
+
if (!nameVal.valid) {
|
|
5336
|
+
sendJson(res, 400, { error: nameVal.error });
|
|
5337
|
+
return true;
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
invalidateApiCache();
|
|
5341
|
+
const result = await reqStorage.renameProjectCode({
|
|
5342
|
+
oldCode,
|
|
5343
|
+
newCode,
|
|
5344
|
+
newName
|
|
5345
|
+
});
|
|
5346
|
+
sendJson(res, 200, result);
|
|
5347
|
+
return true;
|
|
5348
|
+
}
|
|
3786
5349
|
if (pathname === "/api/planning/plans") {
|
|
3787
5350
|
const { ticketId, planMarkdown, lastHash } = body;
|
|
3788
5351
|
invalidateApiCache();
|
|
@@ -3820,6 +5383,48 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3820
5383
|
sendJson(res, 200, { success: true, detail: detailMarkdown });
|
|
3821
5384
|
return true;
|
|
3822
5385
|
}
|
|
5386
|
+
if (pathname === "/api/planning/milestones") {
|
|
5387
|
+
invalidateApiCache();
|
|
5388
|
+
const created = await reqStorage.createMilestone(body);
|
|
5389
|
+
sendJson(res, 201, { milestone: created });
|
|
5390
|
+
return true;
|
|
5391
|
+
}
|
|
5392
|
+
if (pathname === "/api/planning/update-milestone") {
|
|
5393
|
+
invalidateApiCache();
|
|
5394
|
+
const { id, projectCode, ...updates } = body;
|
|
5395
|
+
const updated = await reqStorage.updateMilestone(id, updates, projectCode);
|
|
5396
|
+
sendJson(res, 200, { milestone: updated });
|
|
5397
|
+
return true;
|
|
5398
|
+
}
|
|
5399
|
+
if (pathname === "/api/planning/delete-milestone") {
|
|
5400
|
+
invalidateApiCache();
|
|
5401
|
+
const { id, projectCode } = body;
|
|
5402
|
+
const success = await reqStorage.deleteMilestone(id, projectCode);
|
|
5403
|
+
sendJson(res, 200, { success });
|
|
5404
|
+
return true;
|
|
5405
|
+
}
|
|
5406
|
+
if (pathname === "/api/planning/link") {
|
|
5407
|
+
invalidateApiCache();
|
|
5408
|
+
const { sourceId, relation, targetId, author, project } = body;
|
|
5409
|
+
if (!sourceId || !relation || !targetId) {
|
|
5410
|
+
sendJson(res, 400, { error: "sourceId, relation, and targetId are required." });
|
|
5411
|
+
return true;
|
|
5412
|
+
}
|
|
5413
|
+
const result = await reqStorage.addTicketLink(sourceId, relation, targetId, { author, project });
|
|
5414
|
+
sendJson(res, 200, { success: true, source: result.source, target: result.target });
|
|
5415
|
+
return true;
|
|
5416
|
+
}
|
|
5417
|
+
if (pathname === "/api/planning/unlink") {
|
|
5418
|
+
invalidateApiCache();
|
|
5419
|
+
const { sourceId, targetId, relation, project } = body;
|
|
5420
|
+
if (!sourceId || !targetId) {
|
|
5421
|
+
sendJson(res, 400, { error: "sourceId and targetId are required." });
|
|
5422
|
+
return true;
|
|
5423
|
+
}
|
|
5424
|
+
const result = await reqStorage.removeTicketLink(sourceId, targetId, { relation, project });
|
|
5425
|
+
sendJson(res, 200, { success: true, source: result.source, target: result.target });
|
|
5426
|
+
return true;
|
|
5427
|
+
}
|
|
3823
5428
|
}
|
|
3824
5429
|
sendJson(res, 404, { error: `Endpoint not found: ${pathname}` });
|
|
3825
5430
|
return true;
|
|
@@ -4001,18 +5606,33 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
|
|
|
4001
5606
|
${colors.bold}plan${colors.reset} <id> [--file <path> | --set "<markdown>"] [--last-hash <sha1>] [--json]
|
|
4002
5607
|
View or update implementation plan with optimistic concurrency control.
|
|
4003
5608
|
|
|
4004
|
-
${colors.bold}create${colors.reset} --title "..." [-p|--project <code>] [-t|--type <type>] [--complexity <c>] [--effort "<e>"] [--summary "<s>"] [--detail "<md>"] [--file <path>] [--json]
|
|
5609
|
+
${colors.bold}create${colors.reset} --title "..." [-p|--project <code>] [-t|--type <type>] [-P|--priority <prio>] [--complexity <c>] [--effort "<e>"] [--summary "<s>"] [--detail "<md>"] [--file <path>] [--json]
|
|
4005
5610
|
Mint a new roadmap ticket with sequential numeric ID.
|
|
4006
5611
|
|
|
4007
|
-
${colors.bold}update${colors.reset} <id> [-s|--status <status>] [-t|--type <type>] [--title "..."] [--complexity <c>] [--effort "<e>"] [--in-dev] [--flag <name>] [--last-hash <sha1>] [--force] [--json]
|
|
4008
|
-
Mutate ticket status, type, title, complexity, effort, active state, or feature flag with OCC protection.
|
|
5612
|
+
${colors.bold}update${colors.reset} <id> [-s|--status <status>] [-t|--type <type>] [-P|--priority <prio>] [--title "..."] [--complexity <c>] [--effort "<e>"] [--in-dev] [--flag <name>] [--last-hash <sha1>] [--force] [--json]
|
|
5613
|
+
Mutate ticket status, type, priority, title, complexity, effort, active state, or feature flag with OCC protection.
|
|
4009
5614
|
|
|
4010
5615
|
${colors.bold}comment${colors.reset} <id> ["<text>"] [--text "..."] [--author "..."] [--json]
|
|
4011
5616
|
Append a research finding, test verification, or note to ticket history.
|
|
4012
5617
|
|
|
5618
|
+
${colors.bold}link${colors.reset} <sourceId> <relation> <targetId> [--author "..."] [--json]
|
|
5619
|
+
Establish a bi-directional link between two tickets (within or across projects).
|
|
5620
|
+
\u2022 Relations: 'relates-to', 'blocks', 'blocked-by', 'parent-of', 'child-of', 'duplicates', 'duplicated-by'
|
|
5621
|
+
|
|
5622
|
+
${colors.bold}unlink${colors.reset} <sourceId> <targetId> [--json]
|
|
5623
|
+
Remove a link between two tickets and its reciprocal link.
|
|
5624
|
+
|
|
4013
5625
|
${colors.bold}snapshot, refresh${colors.reset} [--project <code>] [--json]
|
|
4014
5626
|
Generate lean read-only projection snapshot (.esedre/snapshot.json) for zero-latency agent context.
|
|
4015
5627
|
|
|
5628
|
+
${colors.bold}milestone${colors.reset} [list | get <id> | create | update <id> | delete <id>] [options] [--json]
|
|
5629
|
+
Manage project milestones with optional umbrella feature flags.
|
|
5630
|
+
\u2022 list: ese milestone list [-p <project>]
|
|
5631
|
+
\u2022 get: ese milestone get <id> [-p <project>]
|
|
5632
|
+
\u2022 create: ese milestone create --title "..." [-p <project>] [--flag <name>] [--status <status>] [--target-date <date>] [--desc "..."]
|
|
5633
|
+
\u2022 update: ese milestone update <id> [--title "..."] [--flag <name>] [--status <status>] [--target-date <date>] [--desc "..."]
|
|
5634
|
+
\u2022 delete: ese milestone delete <id> [-p <project>]
|
|
5635
|
+
|
|
4016
5636
|
${colors.bold}SERVICE DAEMON COMMANDS:${colors.reset}
|
|
4017
5637
|
${colors.bold}start${colors.reset} [--port <n>] [--foreground | -f] [--quiet] [--json]
|
|
4018
5638
|
Start the Esedre background server and web UI daemon (default port ${DEFAULT_ESEDRE_PORT}).
|
|
@@ -4048,12 +5668,24 @@ ${colors.bold}DEVELOPER ADMINISTRATION (Human Setup & Configuration):${colors.re
|
|
|
4048
5668
|
${colors.bold}projects${colors.reset} [--json]
|
|
4049
5669
|
List registered workspace projects, project codes (e.g. 'CORE', 'WEB', 'DOCS'), and descriptions.
|
|
4050
5670
|
|
|
5671
|
+
${colors.bold}project${colors.reset} [set <code> [-n|--name <name>] [-d|--desc <text>] | rename <old> <new>] [--json]
|
|
5672
|
+
Inspect, update metadata, or rename workspace projects.
|
|
5673
|
+
|
|
5674
|
+
${colors.bold}rename-project${colors.reset} <oldCode> <newCode> [-n|--name <name>] [--json]
|
|
5675
|
+
Safely rename a project code on disk, updating hub folders, manifests, ticket references, and workspace configs.
|
|
5676
|
+
|
|
4051
5677
|
${colors.bold}OPTIONS:${colors.reset}
|
|
4052
5678
|
-p, --project <code> Project code (e.g. CORE, ALCE, DOCS).
|
|
4053
5679
|
-n, --name <name> Project display name (e.g. "Alce Web Reader").
|
|
4054
5680
|
-t, --type <type> Ticket type ('Feature', 'Platform', 'Tools', 'Idea', 'Bug').
|
|
5681
|
+
-P, --priority <prio> Ticket priority ('Critical', 'High', 'Medium', 'Low', 'none').
|
|
5682
|
+
-m, --milestone <name|id> Ticket milestone association.
|
|
4055
5683
|
-s, --status <stat> Ticket status ('Planned', 'In Development', 'Completed', 'Rejected').
|
|
4056
5684
|
-q, --search <query> Case-insensitive substring search query.
|
|
5685
|
+
-r, --relation <rel> Link relation type ('relates-to', 'blocks', 'blocked-by', etc.).
|
|
5686
|
+
--to <targetId> Target ticket ID for link/unlink commands.
|
|
5687
|
+
--blocked Filter tickets that are blocked by uncompleted tickets.
|
|
5688
|
+
--linked-to <key> Filter tickets that are linked to a specific ticket.
|
|
4057
5689
|
--detail "<md>" Specification markdown for ticket detail during create.
|
|
4058
5690
|
--file <path> Path to markdown file for detail (create) or implementation plan (plan).
|
|
4059
5691
|
--json Output raw machine-readable JSON (strongly recommended for autonomous LLM coding agents).
|
|
@@ -4092,7 +5724,7 @@ function parseArgs(rawArgs) {
|
|
|
4092
5724
|
flags["help"] = true;
|
|
4093
5725
|
} else if (key === "y") {
|
|
4094
5726
|
flags["yes"] = true;
|
|
4095
|
-
} else if (key === "t" || key === "p" || key === "s" || key === "q" || key === "n") {
|
|
5727
|
+
} else if (key === "t" || key === "p" || key === "s" || key === "q" || key === "n" || key === "P" || key === "m" || key === "r") {
|
|
4096
5728
|
const next = rawArgs[i + 1];
|
|
4097
5729
|
if (next !== void 0 && !next.startsWith("-")) {
|
|
4098
5730
|
flags[key] = next;
|
|
@@ -4113,9 +5745,12 @@ function parseArgs(rawArgs) {
|
|
|
4113
5745
|
}
|
|
4114
5746
|
if (flags["t"] && !flags["type"]) flags["type"] = flags["t"];
|
|
4115
5747
|
if (flags["p"] && !flags["project"]) flags["project"] = flags["p"];
|
|
5748
|
+
if (flags["P"] && !flags["priority"]) flags["priority"] = flags["P"];
|
|
5749
|
+
if (flags["m"] && !flags["milestone"]) flags["milestone"] = flags["m"];
|
|
4116
5750
|
if (flags["n"] && !flags["name"]) flags["name"] = flags["n"];
|
|
4117
5751
|
if (flags["s"] && !flags["status"]) flags["status"] = flags["s"];
|
|
4118
5752
|
if (flags["q"] && !flags["search"]) flags["search"] = flags["q"];
|
|
5753
|
+
if (flags["r"] && !flags["relation"]) flags["relation"] = flags["r"];
|
|
4119
5754
|
if (flags["f"] && !flags["foreground"]) flags["foreground"] = flags["f"];
|
|
4120
5755
|
return { command, positionals, flags };
|
|
4121
5756
|
}
|
|
@@ -4129,6 +5764,18 @@ function printDuplicateProjectWarnings(storage, isJson) {
|
|
|
4129
5764
|
}
|
|
4130
5765
|
}
|
|
4131
5766
|
async function main() {
|
|
5767
|
+
if (process.stdout && typeof process.stdout.setDefaultEncoding === "function") {
|
|
5768
|
+
try {
|
|
5769
|
+
process.stdout.setDefaultEncoding("utf-8");
|
|
5770
|
+
} catch {
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5773
|
+
if (process.stderr && typeof process.stderr.setDefaultEncoding === "function") {
|
|
5774
|
+
try {
|
|
5775
|
+
process.stderr.setDefaultEncoding("utf-8");
|
|
5776
|
+
} catch {
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
4132
5779
|
const rawArgs = process.argv.slice(2);
|
|
4133
5780
|
const { command, positionals, flags } = parseArgs(rawArgs);
|
|
4134
5781
|
try {
|
|
@@ -4679,6 +6326,80 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
|
|
|
4679
6326
|
printDaemonLogs({ port, lines, workspaceRoot: discovered.workspaceRoot });
|
|
4680
6327
|
return;
|
|
4681
6328
|
}
|
|
6329
|
+
case "rename-project": {
|
|
6330
|
+
const oldCode = positionals[0];
|
|
6331
|
+
const newCode = positionals[1];
|
|
6332
|
+
if (!oldCode || !newCode) {
|
|
6333
|
+
console.error(`${colors.red}Error: Both oldCode and newCode are required (e.g. ese rename-project Profe Lab151)${colors.reset}`);
|
|
6334
|
+
process.exit(1);
|
|
6335
|
+
}
|
|
6336
|
+
const newName = flags["name"] || flags["n"];
|
|
6337
|
+
const result = await storage.renameProjectCode({ oldCode, newCode, newName });
|
|
6338
|
+
if (isJson) {
|
|
6339
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6340
|
+
} else {
|
|
6341
|
+
console.log(`${colors.green}\u2714 Successfully renamed project "${result.oldCode}" to "${result.newCode}"${colors.reset}`);
|
|
6342
|
+
console.log(` \u2022 Display Name: ${result.project.name}`);
|
|
6343
|
+
console.log(` \u2022 Migrated Tickets: ${result.migratedTicketsCount}`);
|
|
6344
|
+
if (result.updatedHubs.length > 0) {
|
|
6345
|
+
console.log(` \u2022 Updated Data Hubs: ${result.updatedHubs.length}`);
|
|
6346
|
+
}
|
|
6347
|
+
if (result.updatedConfigs.length > 0) {
|
|
6348
|
+
console.log(` \u2022 Updated Configs: ${result.updatedConfigs.join(", ")}`);
|
|
6349
|
+
}
|
|
6350
|
+
}
|
|
6351
|
+
return;
|
|
6352
|
+
}
|
|
6353
|
+
case "project": {
|
|
6354
|
+
const sub = positionals[0]?.toLowerCase();
|
|
6355
|
+
if (sub === "rename") {
|
|
6356
|
+
const oldCode = positionals[1];
|
|
6357
|
+
const newCode = positionals[2];
|
|
6358
|
+
if (!oldCode || !newCode) {
|
|
6359
|
+
console.error(`${colors.red}Error: Both oldCode and newCode are required (e.g. ese project rename Profe Lab151)${colors.reset}`);
|
|
6360
|
+
process.exit(1);
|
|
6361
|
+
}
|
|
6362
|
+
const newName = flags["name"] || flags["n"];
|
|
6363
|
+
const result = await storage.renameProjectCode({ oldCode, newCode, newName });
|
|
6364
|
+
if (isJson) {
|
|
6365
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6366
|
+
} else {
|
|
6367
|
+
console.log(`${colors.green}\u2714 Successfully renamed project "${result.oldCode}" to "${result.newCode}"${colors.reset}`);
|
|
6368
|
+
console.log(` \u2022 Display Name: ${result.project.name}`);
|
|
6369
|
+
console.log(` \u2022 Migrated Tickets: ${result.migratedTicketsCount}`);
|
|
6370
|
+
}
|
|
6371
|
+
return;
|
|
6372
|
+
}
|
|
6373
|
+
if (sub === "set") {
|
|
6374
|
+
const code = positionals[1] || flags["project"] || flags["p"];
|
|
6375
|
+
if (!code) {
|
|
6376
|
+
console.error(`${colors.red}Error: Project code is required (e.g. ese project set Lab151 --name "Lab 151")${colors.reset}`);
|
|
6377
|
+
process.exit(1);
|
|
6378
|
+
}
|
|
6379
|
+
const name = flags["name"] || flags["n"];
|
|
6380
|
+
const description = flags["description"] || flags["desc"] || flags["d"];
|
|
6381
|
+
const updated = await storage.updateProject({ code, name, description });
|
|
6382
|
+
if (isJson) {
|
|
6383
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
6384
|
+
} else {
|
|
6385
|
+
console.log(`${colors.green}\u2714 Updated project "${updated.code}"${colors.reset}`);
|
|
6386
|
+
console.log(` \u2022 Name: ${updated.name}`);
|
|
6387
|
+
console.log(` \u2022 Description: ${updated.description}`);
|
|
6388
|
+
}
|
|
6389
|
+
return;
|
|
6390
|
+
}
|
|
6391
|
+
printDuplicateProjectWarnings(storage, isJson);
|
|
6392
|
+
const projects = await storage.getProjects();
|
|
6393
|
+
if (isJson) {
|
|
6394
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
6395
|
+
} else {
|
|
6396
|
+
console.log(`${colors.bold}Registered Projects:${colors.reset}`);
|
|
6397
|
+
for (const p of projects) {
|
|
6398
|
+
console.log(` \u2022 ${colors.bold}${p.code}${colors.reset} (#${p.id}): ${p.name}: ${colors.dim}${p.description}${colors.reset}`);
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
return;
|
|
6402
|
+
}
|
|
4682
6403
|
case "projects": {
|
|
4683
6404
|
printDuplicateProjectWarnings(storage, isJson);
|
|
4684
6405
|
const projects = await storage.getProjects();
|
|
@@ -4699,19 +6420,43 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
|
|
|
4699
6420
|
const status = flags["status"] || void 0;
|
|
4700
6421
|
const type = flags["type"] || flags["category"] || void 0;
|
|
4701
6422
|
const category = type;
|
|
6423
|
+
const rawPriority = flags["priority"];
|
|
6424
|
+
let priority;
|
|
6425
|
+
if (rawPriority) {
|
|
6426
|
+
const lower = rawPriority.trim().toLowerCase();
|
|
6427
|
+
if (lower === "none" || lower === "null") {
|
|
6428
|
+
priority = "none";
|
|
6429
|
+
} else {
|
|
6430
|
+
const norm = normalizePriority(rawPriority);
|
|
6431
|
+
if (!norm) {
|
|
6432
|
+
console.error(`${colors.red}Error: Invalid priority filter '${rawPriority}'. Valid options: Critical, High, Medium, Low, none${colors.reset}`);
|
|
6433
|
+
process.exit(1);
|
|
6434
|
+
}
|
|
6435
|
+
priority = norm;
|
|
6436
|
+
}
|
|
6437
|
+
}
|
|
4702
6438
|
const search = flags["search"] || void 0;
|
|
4703
|
-
const
|
|
6439
|
+
const milestone = flags["milestone"] || flags["m"] || void 0;
|
|
6440
|
+
const isBlocked = flags["blocked"] ? true : void 0;
|
|
6441
|
+
const linkedTo = flags["linked-to"] || flags["linkedTo"] || void 0;
|
|
6442
|
+
const tickets = await storage.listTickets({ project, status, type, category, priority, milestone, search, isBlocked, linkedTo });
|
|
4704
6443
|
if (isJson) {
|
|
4705
6444
|
console.log(JSON.stringify(tickets.map((t) => ({
|
|
4706
6445
|
id: t.meta.id,
|
|
4707
|
-
title: t.meta.title,
|
|
6446
|
+
title: normalizeDashesAndMojibake(t.meta.title),
|
|
4708
6447
|
type: t.meta.type || t.meta.category,
|
|
4709
6448
|
category: t.meta.type || t.meta.category,
|
|
4710
6449
|
status: t.meta.status,
|
|
4711
|
-
|
|
4712
|
-
|
|
6450
|
+
priority: t.meta.priority,
|
|
6451
|
+
complexity: normalizeDashesAndMojibake(t.meta.complexity),
|
|
6452
|
+
effort: normalizeDashesAndMojibake(t.meta.estimatedEffort),
|
|
6453
|
+
milestone: t.meta.milestone,
|
|
6454
|
+
featureFlag: t.meta.featureFlag,
|
|
6455
|
+
inheritedFeatureFlag: t.meta.inheritedFeatureFlag,
|
|
4713
6456
|
project: t.projectDescriptor?.code || t.meta.project || "UNASSIGNED",
|
|
4714
|
-
sha1: t.sha1 || t.meta.sha1
|
|
6457
|
+
sha1: t.sha1 || t.meta.sha1,
|
|
6458
|
+
isBlocked: t.isBlocked,
|
|
6459
|
+
links: t.links
|
|
4715
6460
|
})), null, 2));
|
|
4716
6461
|
} else {
|
|
4717
6462
|
if (tickets.length === 0) {
|
|
@@ -4806,22 +6551,33 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4806
6551
|
return;
|
|
4807
6552
|
}
|
|
4808
6553
|
case "create": {
|
|
4809
|
-
const
|
|
4810
|
-
if (!
|
|
6554
|
+
const rawTitle = flags["title"];
|
|
6555
|
+
if (!rawTitle) {
|
|
4811
6556
|
console.error(`${colors.red}Error: --title is required${colors.reset}`);
|
|
4812
6557
|
process.exit(1);
|
|
4813
6558
|
}
|
|
6559
|
+
const title = normalizeDashesAndMojibake(rawTitle);
|
|
4814
6560
|
const type = flags["type"] || flags["category"] || "Feature";
|
|
4815
6561
|
const category = type;
|
|
6562
|
+
const rawPriority = flags["priority"];
|
|
6563
|
+
let priority;
|
|
6564
|
+
if (rawPriority) {
|
|
6565
|
+
const norm = normalizePriority(rawPriority);
|
|
6566
|
+
if (!norm) {
|
|
6567
|
+
console.error(`${colors.red}Error: Invalid priority '${rawPriority}'. Valid options: Critical, High, Medium, Low${colors.reset}`);
|
|
6568
|
+
process.exit(1);
|
|
6569
|
+
}
|
|
6570
|
+
priority = norm;
|
|
6571
|
+
}
|
|
4816
6572
|
const projectCode = flags["project"] || discovered.config?.projectCode;
|
|
4817
6573
|
if (!projectCode) {
|
|
4818
6574
|
console.error(`${colors.red}Error: Project is required to create a ticket (--project <code> or configure projectCode in esedre.json).${colors.reset}`);
|
|
4819
6575
|
console.error(`${colors.dim}Tip: Run 'ese init' to initialize this repository.${colors.reset}`);
|
|
4820
6576
|
process.exit(1);
|
|
4821
6577
|
}
|
|
4822
|
-
const complexity = flags["complexity"] || "Medium";
|
|
4823
|
-
const estimatedEffort = flags["effort"] || "2.0 - 4.0 hours";
|
|
4824
|
-
const summary = flags["summary"]
|
|
6578
|
+
const complexity = normalizeDashesAndMojibake(flags["complexity"] || "Medium");
|
|
6579
|
+
const estimatedEffort = normalizeDashesAndMojibake(flags["effort"] || "2.0 - 4.0 hours");
|
|
6580
|
+
const summary = flags["summary"] ? normalizeDashesAndMojibake(flags["summary"]) : void 0;
|
|
4825
6581
|
const submittedBy = flags["author"] || "Developer";
|
|
4826
6582
|
const val = validateProjectCode(projectCode);
|
|
4827
6583
|
if (!val.valid) {
|
|
@@ -4830,6 +6586,7 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4830
6586
|
}
|
|
4831
6587
|
const detailArg = flags["detail"];
|
|
4832
6588
|
const fileArg = flags["file"];
|
|
6589
|
+
const milestone = flags["milestone"] || flags["m"] || void 0;
|
|
4833
6590
|
let detailMarkdown = detailArg;
|
|
4834
6591
|
if (fileArg) {
|
|
4835
6592
|
const filePath = path9.resolve(fileArg);
|
|
@@ -4847,18 +6604,20 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4847
6604
|
title,
|
|
4848
6605
|
type,
|
|
4849
6606
|
category,
|
|
6607
|
+
priority,
|
|
4850
6608
|
projectCode,
|
|
4851
6609
|
complexity,
|
|
4852
6610
|
estimatedEffort,
|
|
4853
6611
|
summary,
|
|
4854
6612
|
submittedBy,
|
|
4855
6613
|
detail: detailMarkdown,
|
|
4856
|
-
detailMarkdown
|
|
6614
|
+
detailMarkdown,
|
|
6615
|
+
milestone
|
|
4857
6616
|
});
|
|
4858
6617
|
if (isJson) {
|
|
4859
6618
|
console.log(JSON.stringify(created, null, 2));
|
|
4860
6619
|
} else {
|
|
4861
|
-
console.log(`${colors.green}\u2714 Created Ticket #${created.meta.id}: ${created.meta.title}${colors.reset} [${created.projectDescriptor?.code || created.meta.project || "UNASSIGNED"}] (SHA-1: ${created.sha1?.slice(0, 8)})`);
|
|
6620
|
+
console.log(`${colors.green}\u2714 Created Ticket #${created.meta.id}: ${created.meta.title}${colors.reset} [${created.projectDescriptor?.code || created.meta.project || "UNASSIGNED"}]${created.meta.priority ? ` [${created.meta.priority}]` : ""} (SHA-1: ${created.sha1?.slice(0, 8)})`);
|
|
4862
6621
|
}
|
|
4863
6622
|
return;
|
|
4864
6623
|
}
|
|
@@ -4871,11 +6630,12 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4871
6630
|
const projectFlag = flags["project"];
|
|
4872
6631
|
const lookupKey = projectFlag && /^\d+$/.test(idStr) ? `${projectFlag}-${idStr}` : idStr;
|
|
4873
6632
|
const status = flags["status"];
|
|
4874
|
-
const title = flags["title"];
|
|
4875
|
-
const complexity = flags["complexity"];
|
|
4876
|
-
const effort = flags["effort"];
|
|
6633
|
+
const title = flags["title"] ? normalizeDashesAndMojibake(flags["title"]) : void 0;
|
|
6634
|
+
const complexity = flags["complexity"] ? normalizeDashesAndMojibake(flags["complexity"]) : void 0;
|
|
6635
|
+
const effort = flags["effort"] ? normalizeDashesAndMojibake(flags["effort"]) : void 0;
|
|
4877
6636
|
const inDev = flags["in-dev"] !== void 0 ? Boolean(flags["in-dev"]) : void 0;
|
|
4878
6637
|
const flag = flags["flag"];
|
|
6638
|
+
const rawMilestone = flags["milestone"] !== void 0 ? flags["milestone"] : flags["m"];
|
|
4879
6639
|
const isForce = Boolean(flags["force"]);
|
|
4880
6640
|
const lastHash = isForce ? void 0 : flags["last-hash"];
|
|
4881
6641
|
const updates = {};
|
|
@@ -4885,19 +6645,172 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4885
6645
|
updates.category = type;
|
|
4886
6646
|
}
|
|
4887
6647
|
if (status) updates.status = status;
|
|
6648
|
+
if (flags["priority"] !== void 0) {
|
|
6649
|
+
const raw = String(flags["priority"]).trim();
|
|
6650
|
+
if (raw.toLowerCase() === "none" || raw.toLowerCase() === "clear" || raw.toLowerCase() === "null") {
|
|
6651
|
+
updates.priority = "none";
|
|
6652
|
+
} else {
|
|
6653
|
+
const norm = normalizePriority(raw);
|
|
6654
|
+
if (!norm) {
|
|
6655
|
+
console.error(`${colors.red}Error: Invalid priority '${raw}'. Valid options: Critical, High, Medium, Low, none${colors.reset}`);
|
|
6656
|
+
process.exit(1);
|
|
6657
|
+
}
|
|
6658
|
+
updates.priority = norm;
|
|
6659
|
+
}
|
|
6660
|
+
}
|
|
4888
6661
|
if (title) updates.title = title;
|
|
4889
6662
|
if (complexity) updates.complexity = complexity;
|
|
4890
6663
|
if (effort) updates.estimatedEffort = effort;
|
|
4891
6664
|
if (inDev !== void 0) updates.isActivePlanning = inDev;
|
|
4892
6665
|
if (flag) updates.featureFlag = flag;
|
|
6666
|
+
if (rawMilestone !== void 0) updates.milestone = rawMilestone;
|
|
4893
6667
|
const updated = await storage.updateTicket(lookupKey, updates, lastHash);
|
|
4894
6668
|
if (isJson) {
|
|
4895
6669
|
console.log(JSON.stringify(updated, null, 2));
|
|
4896
6670
|
} else {
|
|
4897
|
-
console.log(`${colors.green}\u2714 Updated Ticket #${lookupKey}: ${updated.meta.title} (Status: ${updated.meta.status}, SHA-1: ${updated.sha1?.slice(0, 8)})${colors.reset}`);
|
|
6671
|
+
console.log(`${colors.green}\u2714 Updated Ticket #${lookupKey}: ${updated.meta.title} (Status: ${updated.meta.status}${updated.meta.priority ? `, Priority: ${updated.meta.priority}` : ""}${updated.meta.milestone ? `, Milestone: ${updated.meta.milestone}` : ""}, SHA-1: ${updated.sha1?.slice(0, 8)})${colors.reset}`);
|
|
4898
6672
|
}
|
|
4899
6673
|
return;
|
|
4900
6674
|
}
|
|
6675
|
+
case "milestone":
|
|
6676
|
+
case "milestones": {
|
|
6677
|
+
const subAction = positionals[0] || "list";
|
|
6678
|
+
const project = flags["project"] || flags["p"] || discovered.config?.projectCode;
|
|
6679
|
+
if (subAction === "list") {
|
|
6680
|
+
const milestones = await storage.listMilestones(project);
|
|
6681
|
+
const allTickets = await storage.listTickets({ project });
|
|
6682
|
+
if (isJson) {
|
|
6683
|
+
console.log(JSON.stringify(milestones, null, 2));
|
|
6684
|
+
} else {
|
|
6685
|
+
console.log(formatMilestoneListTable(milestones, allTickets));
|
|
6686
|
+
if (milestones.length > 0) {
|
|
6687
|
+
console.log(`
|
|
6688
|
+
${colors.dim}Total: ${milestones.length} milestones${colors.reset}`);
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6691
|
+
return;
|
|
6692
|
+
}
|
|
6693
|
+
if (subAction === "get") {
|
|
6694
|
+
const idStr = positionals[1];
|
|
6695
|
+
if (!idStr) {
|
|
6696
|
+
console.error(`${colors.red}Error: Milestone ID or title is required (e.g. ese milestone get 1 or ese milestone get "v0.2.0")${colors.reset}`);
|
|
6697
|
+
process.exit(1);
|
|
6698
|
+
}
|
|
6699
|
+
const milestone = await storage.getMilestone(idStr, project);
|
|
6700
|
+
if (!milestone) {
|
|
6701
|
+
console.error(`${colors.red}Error: Milestone "${idStr}" not found.${colors.reset}`);
|
|
6702
|
+
process.exit(1);
|
|
6703
|
+
}
|
|
6704
|
+
const tickets = await storage.listTickets({ project, milestone: String(milestone.id) });
|
|
6705
|
+
if (isJson) {
|
|
6706
|
+
console.log(JSON.stringify({ ...milestone, tickets }, null, 2));
|
|
6707
|
+
} else {
|
|
6708
|
+
console.log(`${colors.bold}${colors.cyan}Milestone #${milestone.id}: ${milestone.title}${colors.reset}`);
|
|
6709
|
+
console.log(`${colors.dim}${"=".repeat(60)}${colors.reset}`);
|
|
6710
|
+
console.log(`${colors.bold}Project:${colors.reset} ${milestone.project}`);
|
|
6711
|
+
console.log(`${colors.bold}Status:${colors.reset} ${milestone.status}`);
|
|
6712
|
+
if (milestone.featureFlag) {
|
|
6713
|
+
console.log(`${colors.bold}Umbrella Flag:${colors.reset} ${colors.yellow}${milestone.featureFlag}${colors.reset} ${colors.dim}(inherited by all tickets in milestone)${colors.reset}`);
|
|
6714
|
+
}
|
|
6715
|
+
if (milestone.targetDate) {
|
|
6716
|
+
console.log(`${colors.bold}Target Date:${colors.reset} ${milestone.targetDate}`);
|
|
6717
|
+
}
|
|
6718
|
+
if (milestone.description) {
|
|
6719
|
+
console.log(`
|
|
6720
|
+
${colors.bold}Description:${colors.reset}
|
|
6721
|
+
${milestone.description}`);
|
|
6722
|
+
}
|
|
6723
|
+
const completedCount = tickets.filter((t) => t.meta.status === "Completed").length;
|
|
6724
|
+
const progress = tickets.length > 0 ? Math.round(completedCount / tickets.length * 100) : 0;
|
|
6725
|
+
console.log(`
|
|
6726
|
+
${colors.bold}Progress:${colors.reset} ${completedCount} / ${tickets.length} completed (${progress}%)`);
|
|
6727
|
+
if (tickets.length > 0) {
|
|
6728
|
+
console.log(`
|
|
6729
|
+
${colors.bold}Tickets:${colors.reset}`);
|
|
6730
|
+
for (const t of tickets) {
|
|
6731
|
+
console.log(` \u2022 #${t.meta.id} [${t.meta.status}] [${t.meta.type}]: ${t.meta.title}${t.meta.priority ? ` (${t.meta.priority})` : ""}`);
|
|
6732
|
+
}
|
|
6733
|
+
}
|
|
6734
|
+
}
|
|
6735
|
+
return;
|
|
6736
|
+
}
|
|
6737
|
+
if (subAction === "create") {
|
|
6738
|
+
const rawTitle = flags["title"] || positionals.slice(1).join(" ").trim();
|
|
6739
|
+
if (!rawTitle) {
|
|
6740
|
+
console.error(`${colors.red}Error: --title is required to create a milestone (e.g. ese milestone create --title "v0.2.0")${colors.reset}`);
|
|
6741
|
+
process.exit(1);
|
|
6742
|
+
}
|
|
6743
|
+
const title = normalizeDashesAndMojibake(rawTitle);
|
|
6744
|
+
const projectCode = project;
|
|
6745
|
+
if (!projectCode) {
|
|
6746
|
+
console.error(`${colors.red}Error: Project is required to create a milestone (--project <code> or configure projectCode in esedre.json).${colors.reset}`);
|
|
6747
|
+
process.exit(1);
|
|
6748
|
+
}
|
|
6749
|
+
const description = flags["desc"] || flags["description"];
|
|
6750
|
+
const featureFlag = flags["flag"] || flags["feature-flag"];
|
|
6751
|
+
const targetDate = flags["target-date"] || flags["date"];
|
|
6752
|
+
const status = flags["status"] || "Planned";
|
|
6753
|
+
const created = await storage.createMilestone({
|
|
6754
|
+
projectCode,
|
|
6755
|
+
title,
|
|
6756
|
+
description,
|
|
6757
|
+
featureFlag,
|
|
6758
|
+
targetDate,
|
|
6759
|
+
status
|
|
6760
|
+
});
|
|
6761
|
+
if (isJson) {
|
|
6762
|
+
console.log(JSON.stringify(created, null, 2));
|
|
6763
|
+
} else {
|
|
6764
|
+
console.log(`${colors.green}\u2714 Created Milestone #${created.id}: ${created.title} [${created.project}]${created.featureFlag ? ` (Umbrella Flag: ${created.featureFlag})` : ""}${colors.reset}`);
|
|
6765
|
+
}
|
|
6766
|
+
return;
|
|
6767
|
+
}
|
|
6768
|
+
if (subAction === "update") {
|
|
6769
|
+
const idStr = positionals[1];
|
|
6770
|
+
if (!idStr) {
|
|
6771
|
+
console.error(`${colors.red}Error: Milestone ID is required (e.g. ese milestone update 1 --status Active)${colors.reset}`);
|
|
6772
|
+
process.exit(1);
|
|
6773
|
+
}
|
|
6774
|
+
const updates = {};
|
|
6775
|
+
if (flags["title"]) updates.title = flags["title"];
|
|
6776
|
+
if (flags["desc"] || flags["description"]) updates.description = flags["desc"] || flags["description"];
|
|
6777
|
+
if (flags["status"]) updates.status = flags["status"];
|
|
6778
|
+
if (flags["flag"] !== void 0 || flags["feature-flag"] !== void 0) {
|
|
6779
|
+
updates.featureFlag = flags["flag"] !== void 0 ? flags["flag"] : flags["feature-flag"];
|
|
6780
|
+
}
|
|
6781
|
+
if (flags["target-date"] !== void 0 || flags["date"] !== void 0) {
|
|
6782
|
+
updates.targetDate = flags["target-date"] !== void 0 ? flags["target-date"] : flags["date"];
|
|
6783
|
+
}
|
|
6784
|
+
const updated = await storage.updateMilestone(idStr, updates, project);
|
|
6785
|
+
if (isJson) {
|
|
6786
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
6787
|
+
} else {
|
|
6788
|
+
console.log(`${colors.green}\u2714 Updated Milestone #${updated.id}: ${updated.title} (Status: ${updated.status}${updated.featureFlag ? `, Flag: ${updated.featureFlag}` : ""})${colors.reset}`);
|
|
6789
|
+
}
|
|
6790
|
+
return;
|
|
6791
|
+
}
|
|
6792
|
+
if (subAction === "delete") {
|
|
6793
|
+
const idStr = positionals[1];
|
|
6794
|
+
if (!idStr) {
|
|
6795
|
+
console.error(`${colors.red}Error: Milestone ID is required (e.g. ese milestone delete 1)${colors.reset}`);
|
|
6796
|
+
process.exit(1);
|
|
6797
|
+
}
|
|
6798
|
+
const success = await storage.deleteMilestone(idStr, project);
|
|
6799
|
+
if (isJson) {
|
|
6800
|
+
console.log(JSON.stringify({ success, id: idStr }, null, 2));
|
|
6801
|
+
} else {
|
|
6802
|
+
if (success) {
|
|
6803
|
+
console.log(`${colors.green}\u2714 Deleted Milestone #${idStr}${colors.reset}`);
|
|
6804
|
+
} else {
|
|
6805
|
+
console.error(`${colors.red}Error: Milestone "${idStr}" not found.${colors.reset}`);
|
|
6806
|
+
process.exit(1);
|
|
6807
|
+
}
|
|
6808
|
+
}
|
|
6809
|
+
return;
|
|
6810
|
+
}
|
|
6811
|
+
console.error(`${colors.red}Error: Unknown milestone action "${subAction}". Valid actions: list, get, create, update, delete${colors.reset}`);
|
|
6812
|
+
process.exit(1);
|
|
6813
|
+
}
|
|
4901
6814
|
case "comment": {
|
|
4902
6815
|
const idStr = positionals[0];
|
|
4903
6816
|
if (!idStr) {
|
|
@@ -4921,6 +6834,58 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4921
6834
|
}
|
|
4922
6835
|
return;
|
|
4923
6836
|
}
|
|
6837
|
+
case "link": {
|
|
6838
|
+
let sourceId = positionals[0];
|
|
6839
|
+
let relationArg = flags["relation"];
|
|
6840
|
+
let targetId = flags["to"] || positionals[1];
|
|
6841
|
+
if (positionals.length >= 3) {
|
|
6842
|
+
sourceId = positionals[0];
|
|
6843
|
+
relationArg = positionals[1];
|
|
6844
|
+
targetId = positionals[2];
|
|
6845
|
+
} else if (positionals.length === 2 && !flags["relation"]) {
|
|
6846
|
+
const normRel = positionals[1].toLowerCase().replace(/_/g, "-");
|
|
6847
|
+
if (["blocks", "blocked-by", "relates-to", "parent-of", "child-of", "duplicates", "duplicated-by"].includes(normRel)) {
|
|
6848
|
+
relationArg = normRel;
|
|
6849
|
+
targetId = flags["to"] || positionals[2];
|
|
6850
|
+
} else {
|
|
6851
|
+
targetId = positionals[1];
|
|
6852
|
+
relationArg = "relates-to";
|
|
6853
|
+
}
|
|
6854
|
+
}
|
|
6855
|
+
if (!sourceId || !targetId) {
|
|
6856
|
+
console.error(`${colors.red}Error: Both source and target ticket IDs are required.${colors.reset}`);
|
|
6857
|
+
console.error(`${colors.dim}Usage: ese link <sourceId> <relation> <targetId> [options]${colors.reset}`);
|
|
6858
|
+
console.error(`${colors.dim}Relations: blocks, blocked-by, relates-to, parent-of, child-of, duplicates, duplicated-by${colors.reset}`);
|
|
6859
|
+
process.exit(1);
|
|
6860
|
+
}
|
|
6861
|
+
const relation = (relationArg || "relates-to").toLowerCase().replace(/_/g, "-");
|
|
6862
|
+
const project = flags["project"] || discovered.config?.projectCode;
|
|
6863
|
+
const author = flags["author"] || "User";
|
|
6864
|
+
const result = await storage.addTicketLink(sourceId, relation, targetId, { author, project });
|
|
6865
|
+
if (isJson) {
|
|
6866
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6867
|
+
} else {
|
|
6868
|
+
console.log(`${colors.green}\u2714 Linked #${result.source.meta.id} (${result.source.meta.project || "Project"}) -> ${relation} -> #${targetId}${colors.reset}`);
|
|
6869
|
+
}
|
|
6870
|
+
return;
|
|
6871
|
+
}
|
|
6872
|
+
case "unlink": {
|
|
6873
|
+
const sourceId = positionals[0];
|
|
6874
|
+
const targetId = flags["to"] || positionals[1];
|
|
6875
|
+
if (!sourceId || !targetId) {
|
|
6876
|
+
console.error(`${colors.red}Error: Both source and target ticket IDs are required.${colors.reset}`);
|
|
6877
|
+
console.error(`${colors.dim}Usage: ese unlink <sourceId> <targetId> [options]${colors.reset}`);
|
|
6878
|
+
process.exit(1);
|
|
6879
|
+
}
|
|
6880
|
+
const project = flags["project"] || discovered.config?.projectCode;
|
|
6881
|
+
const result = await storage.removeTicketLink(sourceId, targetId, { project });
|
|
6882
|
+
if (isJson) {
|
|
6883
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6884
|
+
} else {
|
|
6885
|
+
console.log(`${colors.green}\u2714 Unlinked #${result.source.meta.id} from #${targetId}${colors.reset}`);
|
|
6886
|
+
}
|
|
6887
|
+
return;
|
|
6888
|
+
}
|
|
4924
6889
|
default: {
|
|
4925
6890
|
console.error(`${colors.red}Error: Unknown command "${command}"${colors.reset}`);
|
|
4926
6891
|
printHelp();
|