esedre 0.1.12 → 1.0.1
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 +125 -79
- package/README.md +53 -10
- package/dist/esedre.mjs +2000 -90
- 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-CKb43WEo.js +0 -114
- package/dist/web/assets/index-Dhmj9uWs.css +0 -2
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.1
|
|
13
|
+
var CURRENT_ESEDRE_VERSION = "1.0.1";
|
|
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)) {
|
|
@@ -551,9 +597,18 @@ var colors = {
|
|
|
551
597
|
red: isColorSupported ? "\x1B[31m" : "",
|
|
552
598
|
blue: isColorSupported ? "\x1B[34m" : ""
|
|
553
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
|
+
}
|
|
554
609
|
function normalizeDashesAndMojibake(val) {
|
|
555
610
|
if (typeof val !== "string") return val;
|
|
556
|
-
return val.replace(/–|‗/g, "-").replace(/â€[˜™]/g, "'").replace(/â€[
|
|
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, "-");
|
|
557
612
|
}
|
|
558
613
|
function normalizeTicketFields(ticket) {
|
|
559
614
|
if (!ticket) return ticket;
|
|
@@ -561,11 +616,31 @@ function normalizeTicketFields(ticket) {
|
|
|
561
616
|
if (ticket.meta.title) ticket.meta.title = normalizeDashesAndMojibake(ticket.meta.title);
|
|
562
617
|
if (ticket.meta.estimatedEffort) ticket.meta.estimatedEffort = normalizeDashesAndMojibake(ticket.meta.estimatedEffort);
|
|
563
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
|
+
}
|
|
564
630
|
}
|
|
565
631
|
if (ticket.detail) {
|
|
566
632
|
if (ticket.detail.title) ticket.detail.title = normalizeDashesAndMojibake(ticket.detail.title);
|
|
567
633
|
if (ticket.detail.estimatedEffort) ticket.detail.estimatedEffort = normalizeDashesAndMojibake(ticket.detail.estimatedEffort);
|
|
568
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
|
+
}
|
|
569
644
|
if (ticket.detail.breakdown) ticket.detail.breakdown = ticket.detail.breakdown.map((b) => normalizeDashesAndMojibake(b));
|
|
570
645
|
if (ticket.detail.technicalDetails) ticket.detail.technicalDetails = ticket.detail.technicalDetails.map((t) => normalizeDashesAndMojibake(t));
|
|
571
646
|
if (ticket.detail.openQuestions) ticket.detail.openQuestions = ticket.detail.openQuestions.map((q) => normalizeDashesAndMojibake(q));
|
|
@@ -586,25 +661,55 @@ function formatTicketListTable(tickets) {
|
|
|
586
661
|
if (tickets.length === 0) {
|
|
587
662
|
return `${colors.dim}No tickets found matching criteria.${colors.reset}`;
|
|
588
663
|
}
|
|
664
|
+
const showPriority = tickets.some((t) => Boolean(t.meta.priority));
|
|
589
665
|
const rows = tickets.map((t) => {
|
|
590
666
|
const id = t.projectDescriptor?.code ? `${t.projectDescriptor.code}-${t.meta.id}` : `#${t.meta.id}`;
|
|
591
667
|
const project = t.projectDescriptor?.code || "CORE";
|
|
592
668
|
const type = t.meta.type || t.meta.category || "Feature";
|
|
669
|
+
const priority = t.meta.priority || "-";
|
|
593
670
|
const status = t.meta.status;
|
|
594
|
-
const title = normalizeDashesAndMojibake(t.meta.title);
|
|
595
|
-
return { id, project, type, status, title };
|
|
671
|
+
const title = normalizeDashesAndMojibake(t.meta.title) + (t.isBlocked ? ` ${colors.red}[BLOCKED]${colors.reset}` : "");
|
|
672
|
+
return { id, project, type, priority, status, title };
|
|
596
673
|
});
|
|
597
674
|
const idWidth = Math.max(4, ...rows.map((r) => r.id.length));
|
|
598
675
|
const projWidth = Math.max(7, ...rows.map((r) => r.project.length));
|
|
599
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;
|
|
600
678
|
const statusWidth = Math.max(14, ...rows.map((r) => r.status.length));
|
|
601
|
-
const
|
|
602
|
-
|
|
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}`;
|
|
603
695
|
const formattedRows = rows.map((r) => {
|
|
604
696
|
const statusColored = colorStatus(r.status);
|
|
605
697
|
const typeColored = colorType(r.type);
|
|
606
698
|
const projColored = `${colors.magenta}${r.project}${colors.reset}`;
|
|
607
|
-
|
|
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(" ");
|
|
608
713
|
});
|
|
609
714
|
return [header, divider, ...formattedRows].join("\n");
|
|
610
715
|
}
|
|
@@ -615,12 +720,22 @@ function formatTicketDetail(ticket) {
|
|
|
615
720
|
lines.push(`${colors.dim}${"=".repeat(60)}${colors.reset}`);
|
|
616
721
|
lines.push(`${colors.bold}Project:${colors.reset} ${projectDescriptor ? `${projectDescriptor.code} - ${projectDescriptor.name}` : "Default"}`);
|
|
617
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
|
+
}
|
|
618
726
|
lines.push(`${colors.bold}Status:${colors.reset} ${colorStatus(meta.status)}`);
|
|
619
727
|
lines.push(`${colors.bold}Complexity:${colors.reset} ${normalizeDashesAndMojibake(meta.complexity || "Medium")}`);
|
|
620
728
|
lines.push(`${colors.bold}Effort:${colors.reset} ${normalizeDashesAndMojibake(meta.estimatedEffort || "N/A")}`);
|
|
621
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
|
+
}
|
|
622
733
|
if (meta.featureFlag) {
|
|
623
|
-
|
|
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}`);
|
|
624
739
|
}
|
|
625
740
|
if (detail?.summary) {
|
|
626
741
|
lines.push("");
|
|
@@ -648,6 +763,17 @@ function formatTicketDetail(ticket) {
|
|
|
648
763
|
lines.push(` \u2022 ${normalizeDashesAndMojibake(q)}`);
|
|
649
764
|
}
|
|
650
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
|
+
}
|
|
651
777
|
if (planMarkdown) {
|
|
652
778
|
lines.push("");
|
|
653
779
|
lines.push(`${colors.bold}Implementation Plan:${colors.reset}`);
|
|
@@ -693,6 +819,53 @@ function colorType(type) {
|
|
|
693
819
|
return type;
|
|
694
820
|
}
|
|
695
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
|
+
}
|
|
696
869
|
|
|
697
870
|
// src/storage/filesystem.ts
|
|
698
871
|
function isProjectMatch(code, filter) {
|
|
@@ -748,6 +921,20 @@ var FilesystemStorageAdapter = class {
|
|
|
748
921
|
}
|
|
749
922
|
}
|
|
750
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
|
+
}
|
|
751
938
|
try {
|
|
752
939
|
const discovered = findEsedreConfig(this.workspaceRoot);
|
|
753
940
|
this.config = discovered.config || {};
|
|
@@ -1260,6 +1447,567 @@ ${formattedList}`
|
|
|
1260
1447
|
}
|
|
1261
1448
|
return projectDesc;
|
|
1262
1449
|
}
|
|
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
|
+
}
|
|
1459
|
+
const locations = this.resolveProjectLocations();
|
|
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)) {
|
|
1489
|
+
try {
|
|
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");
|
|
1495
|
+
}
|
|
1496
|
+
} catch {
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
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
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
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
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
const locations = this.resolveProjectLocations();
|
|
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
|
+
}
|
|
1263
2011
|
findTicketLocation(id) {
|
|
1264
2012
|
const locations = this.resolveProjectLocations();
|
|
1265
2013
|
if (locations.length === 0) return null;
|
|
@@ -1369,6 +2117,9 @@ ${formattedList}`
|
|
|
1369
2117
|
meta.id = meta.id ?? num;
|
|
1370
2118
|
meta.type = meta.type || meta.category || "Feature";
|
|
1371
2119
|
meta.category = meta.type;
|
|
2120
|
+
if (meta.priority) {
|
|
2121
|
+
meta.priority = normalizePriority(meta.priority);
|
|
2122
|
+
}
|
|
1372
2123
|
let effectiveLoc = loc;
|
|
1373
2124
|
if (loc.isSharedDir) {
|
|
1374
2125
|
const found = locations.find(
|
|
@@ -1378,18 +2129,61 @@ ${formattedList}`
|
|
|
1378
2129
|
}
|
|
1379
2130
|
meta.project = effectiveLoc.project.code;
|
|
1380
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
|
+
}
|
|
1381
2143
|
if (filter?.project && filter.project !== "all") {
|
|
1382
2144
|
const pLower = filter.project.toLowerCase();
|
|
1383
2145
|
if (!isProjectMatch(meta.project, filter.project)) {
|
|
1384
2146
|
continue;
|
|
1385
2147
|
}
|
|
1386
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
|
+
}
|
|
1387
2169
|
if (filter?.status && meta.status !== filter.status) {
|
|
1388
2170
|
continue;
|
|
1389
2171
|
}
|
|
1390
2172
|
if (filter?.category && meta.category !== filter.category) {
|
|
1391
2173
|
continue;
|
|
1392
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
|
+
}
|
|
1393
2187
|
if (filter?.search) {
|
|
1394
2188
|
const term = filter.search.toLowerCase();
|
|
1395
2189
|
const matchesTitle = meta.title.toLowerCase().includes(term);
|
|
@@ -1425,8 +2219,23 @@ ${formattedList}`
|
|
|
1425
2219
|
projectDescriptor: effectiveLoc.project
|
|
1426
2220
|
};
|
|
1427
2221
|
normalizeTicketFields(ticketObj);
|
|
2222
|
+
this.enrichTicketLinks(ticketObj);
|
|
1428
2223
|
ticketObj.sha1 = computeTicketHash(ticketObj);
|
|
1429
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
|
+
}
|
|
1430
2239
|
tickets.push(ticketObj);
|
|
1431
2240
|
} catch {
|
|
1432
2241
|
}
|
|
@@ -1450,6 +2259,19 @@ ${formattedList}`
|
|
|
1450
2259
|
meta.id = meta.id ?? locInfo.id;
|
|
1451
2260
|
meta.project = locInfo.loc.project.code;
|
|
1452
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
|
+
}
|
|
1453
2275
|
let detail;
|
|
1454
2276
|
const detailPath = path3.join(locInfo.ticketDir, "detail.md");
|
|
1455
2277
|
if (fs3.existsSync(detailPath)) {
|
|
@@ -1477,6 +2299,7 @@ ${formattedList}`
|
|
|
1477
2299
|
projectDescriptor: locInfo.loc.project
|
|
1478
2300
|
};
|
|
1479
2301
|
normalizeTicketFields(ticket);
|
|
2302
|
+
this.enrichTicketLinks(ticket);
|
|
1480
2303
|
ticket.sha1 = computeTicketHash(ticket);
|
|
1481
2304
|
ticket.lastHash = ticket.sha1;
|
|
1482
2305
|
return ticket;
|
|
@@ -1526,10 +2349,13 @@ ${formattedList}`
|
|
|
1526
2349
|
if (itemMatch) openQuestions.push(itemMatch[1].trim());
|
|
1527
2350
|
}
|
|
1528
2351
|
}
|
|
2352
|
+
const priorityMatch = raw.match(/\*\*Priority\*\*:\s*([^\n]+)/i);
|
|
2353
|
+
const priority = priorityMatch ? normalizePriority(priorityMatch[1].trim()) : void 0;
|
|
1529
2354
|
return {
|
|
1530
2355
|
title,
|
|
1531
2356
|
type,
|
|
1532
2357
|
category,
|
|
2358
|
+
priority,
|
|
1533
2359
|
complexity,
|
|
1534
2360
|
estimatedEffort,
|
|
1535
2361
|
summary,
|
|
@@ -1572,6 +2398,24 @@ ${formattedList}`
|
|
|
1572
2398
|
const cleanTitle = normalizeDashesAndMojibake(input.title).slice(0, 48).trim();
|
|
1573
2399
|
const cleanEffort = normalizeDashesAndMojibake(input.estimatedEffort || input.effort || "2.0 - 4.0 hours");
|
|
1574
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
|
+
}
|
|
1575
2419
|
const cleanSummary = input.summary ? normalizeDashesAndMojibake(input.summary) : void 0;
|
|
1576
2420
|
const cleanDetailRaw = input.detailMarkdown || input.detail ? normalizeDashesAndMojibake(input.detailMarkdown || input.detail) : void 0;
|
|
1577
2421
|
const meta = {
|
|
@@ -1579,6 +2423,10 @@ ${formattedList}`
|
|
|
1579
2423
|
title: cleanTitle,
|
|
1580
2424
|
type: effectiveType,
|
|
1581
2425
|
category: effectiveType,
|
|
2426
|
+
...cleanPriority ? { priority: cleanPriority } : {},
|
|
2427
|
+
...cleanMilestone ? { milestone: cleanMilestone } : {},
|
|
2428
|
+
...cleanFeatureFlag ? { featureFlag: cleanFeatureFlag } : {},
|
|
2429
|
+
...inheritedFlag ? { inheritedFeatureFlag: inheritedFlag } : {},
|
|
1582
2430
|
complexity: cleanComplexity,
|
|
1583
2431
|
estimatedEffort: cleanEffort,
|
|
1584
2432
|
submittedBy: input.submittedBy || "Developer",
|
|
@@ -1592,6 +2440,10 @@ ${formattedList}`
|
|
|
1592
2440
|
project: targetLoc.project.code
|
|
1593
2441
|
};
|
|
1594
2442
|
const rawDetail = cleanDetailRaw?.trim();
|
|
2443
|
+
const priorityLine = meta.priority ? `**Priority**: ${meta.priority}
|
|
2444
|
+
` : "";
|
|
2445
|
+
const milestoneLine = meta.milestone ? `**Milestone**: ${meta.milestone}
|
|
2446
|
+
` : "";
|
|
1595
2447
|
let detailMd;
|
|
1596
2448
|
if (rawDetail) {
|
|
1597
2449
|
if (/^#\s+[^\n]+/m.test(rawDetail)) {
|
|
@@ -1599,7 +2451,7 @@ ${formattedList}`
|
|
|
1599
2451
|
if (!/\*\*(?:Type|Category)\*\*:/i.test(processed)) {
|
|
1600
2452
|
const metaBlock = `
|
|
1601
2453
|
**Category**: ${meta.category}
|
|
1602
|
-
**Complexity**: ${meta.complexity}
|
|
2454
|
+
${priorityLine}${milestoneLine}**Complexity**: ${meta.complexity}
|
|
1603
2455
|
**Estimated Effort**: ${meta.estimatedEffort}
|
|
1604
2456
|
`;
|
|
1605
2457
|
processed = processed.replace(/^(# Ticket[^\n]+\n)/m, `$1${metaBlock}`);
|
|
@@ -1639,7 +2491,7 @@ ${rawDetail}
|
|
|
1639
2491
|
}
|
|
1640
2492
|
detailMd = `# Ticket #${nextId}: ${meta.title}
|
|
1641
2493
|
**Category**: ${meta.category}
|
|
1642
|
-
**Complexity**: ${meta.complexity}
|
|
2494
|
+
${priorityLine}${milestoneLine}**Complexity**: ${meta.complexity}
|
|
1643
2495
|
**Estimated Effort**: ${meta.estimatedEffort}
|
|
1644
2496
|
|
|
1645
2497
|
${body.trim()}
|
|
@@ -1648,7 +2500,7 @@ ${body.trim()}
|
|
|
1648
2500
|
} else {
|
|
1649
2501
|
detailMd = `# Ticket #${nextId}: ${meta.title}
|
|
1650
2502
|
**Category**: ${meta.category}
|
|
1651
|
-
**Complexity**: ${meta.complexity}
|
|
2503
|
+
${priorityLine}${milestoneLine}**Complexity**: ${meta.complexity}
|
|
1652
2504
|
**Estimated Effort**: ${meta.estimatedEffort}
|
|
1653
2505
|
|
|
1654
2506
|
### Summary
|
|
@@ -1703,13 +2555,48 @@ ${input.summary || "Summary to be defined."}
|
|
|
1703
2555
|
completedAt,
|
|
1704
2556
|
revision
|
|
1705
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
|
+
}
|
|
1706
2593
|
writeSafeFile(metaPath, JSON.stringify(updatedMeta, null, 2) + "\n");
|
|
1707
2594
|
const newType = updates.type || updates.category;
|
|
1708
2595
|
if (newType) {
|
|
1709
2596
|
updatedMeta.type = newType;
|
|
1710
2597
|
updatedMeta.category = newType;
|
|
1711
2598
|
}
|
|
1712
|
-
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)) {
|
|
1713
2600
|
let content = existing.detail.raw;
|
|
1714
2601
|
if (updates.title) {
|
|
1715
2602
|
content = content.replace(/^#\s*(?:Ticket\s*#?\d+\s*:\s*|\d+\s*:\s*)?[^\n]+/im, `# Ticket #${locInfo.id}: ${updatedMeta.title}`);
|
|
@@ -1717,6 +2604,30 @@ ${input.summary || "Summary to be defined."}
|
|
|
1717
2604
|
if (updates.type || updates.category) {
|
|
1718
2605
|
content = content.replace(/\*\*(?:Type|Category)\*\*:\s*[^\n]+/i, `**Type**: ${updatedMeta.type}`);
|
|
1719
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
|
+
}
|
|
1720
2631
|
if (updates.complexity) {
|
|
1721
2632
|
content = content.replace(/\*\*Complexity\*\*:\s*[^\n]+/i, `**Complexity**: ${updatedMeta.complexity}`);
|
|
1722
2633
|
}
|
|
@@ -1789,6 +2700,226 @@ ${input.summary || "Summary to be defined."}
|
|
|
1789
2700
|
writeSafeFile(commentsPath, JSON.stringify(comments, null, 2) + "\n");
|
|
1790
2701
|
return newComment;
|
|
1791
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);
|
|
2788
|
+
}
|
|
2789
|
+
ticket.links = enriched;
|
|
2790
|
+
ticket.isBlocked = isBlocked;
|
|
2791
|
+
}
|
|
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
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
const sourceInfo = this.resolveTicketIdentifier(sourceId, options?.project);
|
|
2799
|
+
if (!sourceInfo) {
|
|
2800
|
+
throw new Error(`Source ticket #${sourceId} could not be located.`);
|
|
2801
|
+
}
|
|
2802
|
+
const targetInfo = this.resolveTicketIdentifier(targetId, options?.project || sourceInfo.project);
|
|
2803
|
+
if (!targetInfo) {
|
|
2804
|
+
throw new Error(`Target ticket #${targetId} could not be located.`);
|
|
2805
|
+
}
|
|
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)) {
|
|
2841
|
+
try {
|
|
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;
|
|
2866
|
+
} catch {
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
const updatedSource = await this.getTicket(sourceInfo.key);
|
|
2870
|
+
return { source: updatedSource, target: updatedTarget };
|
|
2871
|
+
}
|
|
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.`);
|
|
2876
|
+
}
|
|
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)) {
|
|
2882
|
+
try {
|
|
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
|
+
}
|
|
2895
|
+
} catch {
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
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 };
|
|
2922
|
+
}
|
|
1792
2923
|
};
|
|
1793
2924
|
|
|
1794
2925
|
// src/securityFilter.ts
|
|
@@ -1819,19 +2950,56 @@ var SecurityFilter = class {
|
|
|
1819
2950
|
}
|
|
1820
2951
|
return proj;
|
|
1821
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
|
+
}
|
|
1822
2990
|
async listTickets(filter) {
|
|
1823
2991
|
if (filter?.project && !isProjectAuthorized(filter.project, this.config.allowedProjects)) {
|
|
1824
2992
|
throw new EsedreAuthorizationError(filter.project);
|
|
1825
2993
|
}
|
|
1826
2994
|
const tickets = await this.target.listTickets(filter);
|
|
1827
2995
|
if (!this.config.allowedProjects || this.config.allowedProjects.length === 0) {
|
|
1828
|
-
return tickets;
|
|
2996
|
+
return tickets.map((t) => this.redactLinks(t));
|
|
1829
2997
|
}
|
|
1830
2998
|
return tickets.filter((t) => {
|
|
1831
2999
|
const code = t.projectDescriptor?.code || t.meta.project;
|
|
1832
3000
|
if (!code) return true;
|
|
1833
3001
|
return isProjectAuthorized(code, this.config.allowedProjects);
|
|
1834
|
-
});
|
|
3002
|
+
}).map((t) => this.redactLinks(t));
|
|
1835
3003
|
}
|
|
1836
3004
|
async getTicket(id) {
|
|
1837
3005
|
const ticket = await this.target.getTicket(id);
|
|
@@ -1840,7 +3008,7 @@ var SecurityFilter = class {
|
|
|
1840
3008
|
if (code && !isProjectAuthorized(code, this.config.allowedProjects)) {
|
|
1841
3009
|
throw new EsedreAuthorizationError(code);
|
|
1842
3010
|
}
|
|
1843
|
-
return ticket;
|
|
3011
|
+
return this.redactLinks(ticket);
|
|
1844
3012
|
}
|
|
1845
3013
|
async createTicket(input) {
|
|
1846
3014
|
const targetCode = input.projectCode || this.config.projectCode;
|
|
@@ -1892,6 +3060,87 @@ var SecurityFilter = class {
|
|
|
1892
3060
|
}
|
|
1893
3061
|
return this.target.addComment(id, comment);
|
|
1894
3062
|
}
|
|
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;
|
|
3070
|
+
}
|
|
3071
|
+
if (this.config.allowedProjects.includes("*")) {
|
|
3072
|
+
return all;
|
|
3073
|
+
}
|
|
3074
|
+
return all.filter((m) => isProjectAuthorized(m.project, this.config.allowedProjects));
|
|
3075
|
+
}
|
|
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);
|
|
3084
|
+
}
|
|
3085
|
+
return milestone;
|
|
3086
|
+
}
|
|
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);
|
|
3091
|
+
}
|
|
3092
|
+
return this.target.createMilestone(input);
|
|
3093
|
+
}
|
|
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);
|
|
3102
|
+
}
|
|
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
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
return this.target.deleteMilestone(id, projectCode);
|
|
3111
|
+
}
|
|
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
|
+
};
|
|
3127
|
+
}
|
|
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
|
+
};
|
|
3143
|
+
}
|
|
1895
3144
|
};
|
|
1896
3145
|
|
|
1897
3146
|
// src/mcp/server.ts
|
|
@@ -1948,7 +3197,7 @@ var EsedreMcpServer = class {
|
|
|
1948
3197
|
},
|
|
1949
3198
|
serverInfo: {
|
|
1950
3199
|
name: "esedre",
|
|
1951
|
-
version:
|
|
3200
|
+
version: CURRENT_ESEDRE_VERSION
|
|
1952
3201
|
}
|
|
1953
3202
|
}
|
|
1954
3203
|
};
|
|
@@ -2112,7 +3361,11 @@ Status: ${ticket.meta.status}`;
|
|
|
2112
3361
|
status: { type: "string", enum: ["Planned", "In Development", "Completed", "Rejected"], description: "Ticket status" },
|
|
2113
3362
|
type: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket type" },
|
|
2114
3363
|
category: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket category (legacy alias for type)" },
|
|
2115
|
-
|
|
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" }
|
|
2116
3369
|
}
|
|
2117
3370
|
}
|
|
2118
3371
|
},
|
|
@@ -2161,10 +3414,12 @@ Status: ${ticket.meta.status}`;
|
|
|
2161
3414
|
project: { type: "string", description: "Target project code (e.g. CORE, WEB, DOCS)" },
|
|
2162
3415
|
type: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket type" },
|
|
2163
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" },
|
|
2164
3418
|
complexity: { type: "string", description: "Complexity (e.g. Low, Medium, High)" },
|
|
2165
3419
|
effort: { type: "string", description: "Estimated effort (e.g. 2.0 - 4.0 hours)" },
|
|
2166
3420
|
summary: { type: "string", description: "Initial feature summary" },
|
|
2167
3421
|
detail: { type: "string", description: "Initial specification, feature breakdown, or technical detail markdown" },
|
|
3422
|
+
milestone: { type: "string", description: "Assign ticket to milestone" },
|
|
2168
3423
|
author: { type: "string", description: "Submitting author name" }
|
|
2169
3424
|
},
|
|
2170
3425
|
required: ["title"]
|
|
@@ -2180,27 +3435,116 @@ Status: ${ticket.meta.status}`;
|
|
|
2180
3435
|
status: { type: "string", enum: ["Planned", "In Development", "Completed", "Rejected"] },
|
|
2181
3436
|
type: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"] },
|
|
2182
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)" },
|
|
2183
3439
|
title: { type: "string", description: "New title (max 48 characters)" },
|
|
2184
3440
|
complexity: { type: "string", description: "Complexity (e.g. Low, Medium, High)" },
|
|
2185
3441
|
effort: { type: "string", description: "Estimated effort (e.g. 2.0 - 4.0 hours)" },
|
|
2186
3442
|
inDevelopment: { type: "boolean", description: "Active development toggle" },
|
|
2187
3443
|
featureFlag: { type: "string", description: "Feature flag name" },
|
|
3444
|
+
milestone: { type: "string", description: 'Update ticket milestone ("none" to clear)' },
|
|
2188
3445
|
lastHash: { type: "string", description: "Optimistic concurrency control: last known sha1 hash of the ticket" }
|
|
2189
3446
|
},
|
|
2190
|
-
required: ["ticketId"]
|
|
3447
|
+
required: ["ticketId"]
|
|
3448
|
+
}
|
|
3449
|
+
},
|
|
3450
|
+
{
|
|
3451
|
+
name: "esedre_add_comment",
|
|
3452
|
+
description: "Append a developer or LLM agent comment to a ticket.",
|
|
3453
|
+
inputSchema: {
|
|
3454
|
+
type: "object",
|
|
3455
|
+
properties: {
|
|
3456
|
+
ticketId: { type: ["integer", "string"], description: "Numeric ticket ID or compound key" },
|
|
3457
|
+
text: { type: "string", description: "Comment message" },
|
|
3458
|
+
author: { type: "string", description: "Author name (e.g. Antigravity, Developer)" }
|
|
3459
|
+
},
|
|
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"]
|
|
2191
3535
|
}
|
|
2192
3536
|
},
|
|
2193
3537
|
{
|
|
2194
|
-
name: "
|
|
2195
|
-
description: "
|
|
3538
|
+
name: "esedre_unlink_ticket",
|
|
3539
|
+
description: "Remove a link between two tickets and its reciprocal link.",
|
|
2196
3540
|
inputSchema: {
|
|
2197
3541
|
type: "object",
|
|
2198
3542
|
properties: {
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
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" }
|
|
2202
3546
|
},
|
|
2203
|
-
required: ["
|
|
3547
|
+
required: ["sourceTicketId", "targetTicketId"]
|
|
2204
3548
|
}
|
|
2205
3549
|
}
|
|
2206
3550
|
];
|
|
@@ -2215,7 +3559,11 @@ Status: ${ticket.meta.status}`;
|
|
|
2215
3559
|
status: args.status,
|
|
2216
3560
|
type,
|
|
2217
3561
|
category: type,
|
|
2218
|
-
|
|
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
|
|
2219
3567
|
});
|
|
2220
3568
|
return tickets.map((t) => ({
|
|
2221
3569
|
id: t.meta.id,
|
|
@@ -2223,10 +3571,16 @@ Status: ${ticket.meta.status}`;
|
|
|
2223
3571
|
type: t.meta.type || t.meta.category,
|
|
2224
3572
|
category: t.meta.type || t.meta.category,
|
|
2225
3573
|
status: t.meta.status,
|
|
3574
|
+
priority: t.meta.priority,
|
|
2226
3575
|
complexity: t.meta.complexity,
|
|
2227
3576
|
effort: t.meta.estimatedEffort,
|
|
3577
|
+
milestone: t.meta.milestone,
|
|
3578
|
+
featureFlag: t.meta.featureFlag,
|
|
3579
|
+
inheritedFeatureFlag: t.meta.inheritedFeatureFlag,
|
|
2228
3580
|
project: t.projectDescriptor?.code || t.meta.project || "UNASSIGNED",
|
|
2229
|
-
sha1: t.sha1 || t.meta.sha1
|
|
3581
|
+
sha1: t.sha1 || t.meta.sha1,
|
|
3582
|
+
isBlocked: t.isBlocked,
|
|
3583
|
+
links: t.links
|
|
2230
3584
|
}));
|
|
2231
3585
|
}
|
|
2232
3586
|
case "get_ticket": {
|
|
@@ -2251,12 +3605,14 @@ Status: ${ticket.meta.status}`;
|
|
|
2251
3605
|
title: args.title,
|
|
2252
3606
|
type,
|
|
2253
3607
|
category: type,
|
|
3608
|
+
priority: args.priority,
|
|
2254
3609
|
projectCode: args.project,
|
|
2255
3610
|
complexity: args.complexity,
|
|
2256
3611
|
estimatedEffort: args.effort,
|
|
2257
3612
|
summary: args.summary,
|
|
2258
3613
|
detail: args.detail || args.detailMarkdown,
|
|
2259
3614
|
detailMarkdown: args.detailMarkdown || args.detail,
|
|
3615
|
+
milestone: args.milestone,
|
|
2260
3616
|
submittedBy: args.author || "Agent"
|
|
2261
3617
|
});
|
|
2262
3618
|
return created;
|
|
@@ -2270,11 +3626,13 @@ Status: ${ticket.meta.status}`;
|
|
|
2270
3626
|
updates.category = type;
|
|
2271
3627
|
}
|
|
2272
3628
|
if (args.status) updates.status = args.status;
|
|
3629
|
+
if (args.priority !== void 0) updates.priority = args.priority;
|
|
2273
3630
|
if (args.title) updates.title = args.title;
|
|
2274
3631
|
if (args.complexity) updates.complexity = args.complexity;
|
|
2275
3632
|
if (args.effort) updates.estimatedEffort = args.effort;
|
|
2276
3633
|
if (args.inDevelopment !== void 0) updates.isActivePlanning = Boolean(args.inDevelopment);
|
|
2277
3634
|
if (args.featureFlag) updates.featureFlag = args.featureFlag;
|
|
3635
|
+
if (args.milestone !== void 0) updates.milestone = args.milestone;
|
|
2278
3636
|
const updated = await this.storage.updateTicket(id, updates, args.lastHash);
|
|
2279
3637
|
return updated;
|
|
2280
3638
|
}
|
|
@@ -2286,6 +3644,63 @@ Status: ${ticket.meta.status}`;
|
|
|
2286
3644
|
});
|
|
2287
3645
|
return comment;
|
|
2288
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
|
+
}
|
|
2289
3704
|
default:
|
|
2290
3705
|
throw new Error(`Unknown tool: ${name}`);
|
|
2291
3706
|
}
|
|
@@ -2485,7 +3900,7 @@ import path6 from "node:path";
|
|
|
2485
3900
|
// src/server/daemon.ts
|
|
2486
3901
|
import fs5 from "node:fs";
|
|
2487
3902
|
import path5 from "node:path";
|
|
2488
|
-
import
|
|
3903
|
+
import os3 from "node:os";
|
|
2489
3904
|
import http from "node:http";
|
|
2490
3905
|
import { spawn, spawnSync } from "node:child_process";
|
|
2491
3906
|
import { fileURLToPath } from "node:url";
|
|
@@ -2495,7 +3910,7 @@ function getGlobalEsedreDir() {
|
|
|
2495
3910
|
if (process.env.ESEDRE_GLOBAL_DIR) {
|
|
2496
3911
|
return process.env.ESEDRE_GLOBAL_DIR;
|
|
2497
3912
|
}
|
|
2498
|
-
return path5.join(
|
|
3913
|
+
return path5.join(os3.homedir(), ".esedre");
|
|
2499
3914
|
}
|
|
2500
3915
|
function ensureGlobalEsedreStore() {
|
|
2501
3916
|
const globalDir = getGlobalEsedreDir();
|
|
@@ -2763,6 +4178,27 @@ async function stopDaemon(options = {}) {
|
|
|
2763
4178
|
}
|
|
2764
4179
|
return false;
|
|
2765
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
|
+
}
|
|
2766
4202
|
}
|
|
2767
4203
|
if (pid && isProcessAlive(pid)) {
|
|
2768
4204
|
try {
|
|
@@ -2870,49 +4306,49 @@ function classifyContent(currentContent, latestTemplate, historicHashes) {
|
|
|
2870
4306
|
}
|
|
2871
4307
|
var WRAPPER_CMD = `@echo off
|
|
2872
4308
|
REM Esedre Autonomous Ticketing & Project Planning Engine Wrapper (Windows CMD)
|
|
2873
|
-
|
|
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
|
+
)
|
|
2874
4337
|
|
|
2875
4338
|
where ese >nul 2>nul
|
|
2876
|
-
if %ERRORLEVEL% equ 0
|
|
4339
|
+
if %ERRORLEVEL% equ 0 (
|
|
4340
|
+
call ese %*
|
|
4341
|
+
goto :eof
|
|
4342
|
+
)
|
|
2877
4343
|
|
|
2878
4344
|
where esedre >nul 2>nul
|
|
2879
|
-
if %ERRORLEVEL% equ 0
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
if exist "%~dp0..\\node_modules\\.bin\\ese.cmd" goto use_node_modules_ese
|
|
2884
|
-
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
|
+
)
|
|
2885
4349
|
|
|
2886
4350
|
call npx --yes esedre %*
|
|
2887
|
-
goto
|
|
2888
|
-
|
|
2889
|
-
:use_ese
|
|
2890
|
-
call ese %*
|
|
2891
|
-
goto done
|
|
2892
|
-
|
|
2893
|
-
:use_esedre
|
|
2894
|
-
call esedre %*
|
|
2895
|
-
goto done
|
|
2896
|
-
|
|
2897
|
-
:use_sibling_dist
|
|
2898
|
-
node "%~dp0..\\..\\esedre\\dist\\esedre.mjs" %*
|
|
2899
|
-
goto done
|
|
2900
|
-
|
|
2901
|
-
:use_local_dist
|
|
2902
|
-
node "%~dp0..\\esedre\\dist\\esedre.mjs" %*
|
|
2903
|
-
goto done
|
|
2904
|
-
|
|
2905
|
-
:use_node_modules_ese
|
|
2906
|
-
call "%~dp0..\\node_modules\\.bin\\ese.cmd" %*
|
|
2907
|
-
goto done
|
|
2908
|
-
|
|
2909
|
-
:use_node_modules_esedre
|
|
2910
|
-
call "%~dp0..\\node_modules\\.bin\\esedre.cmd" %*
|
|
2911
|
-
goto done
|
|
2912
|
-
|
|
2913
|
-
:done
|
|
2914
|
-
endlocal
|
|
2915
|
-
exit /b %ERRORLEVEL%
|
|
4351
|
+
goto :eof
|
|
2916
4352
|
`;
|
|
2917
4353
|
var WRAPPER_PS1 = `# Esedre Autonomous Ticketing & Project Planning Engine Wrapper (PowerShell)
|
|
2918
4354
|
$ErrorActionPreference = "Stop"
|
|
@@ -2926,12 +4362,9 @@ try {
|
|
|
2926
4362
|
|
|
2927
4363
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
2928
4364
|
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
}
|
|
2933
|
-
if (Get-Command "esedre" -ErrorAction SilentlyContinue) {
|
|
2934
|
-
& esedre @args
|
|
4365
|
+
$inRepoEsedre = Join-Path $scriptDir "..\\dist\\esedre.mjs"
|
|
4366
|
+
if (Test-Path $inRepoEsedre) {
|
|
4367
|
+
& node $inRepoEsedre @args
|
|
2935
4368
|
exit $LASTEXITCODE
|
|
2936
4369
|
}
|
|
2937
4370
|
$siblingEsedre = Join-Path $scriptDir "..\\..\\esedre\\dist\\esedre.mjs"
|
|
@@ -2944,6 +4377,11 @@ if (Test-Path $distEsedre) {
|
|
|
2944
4377
|
& node $distEsedre @args
|
|
2945
4378
|
exit $LASTEXITCODE
|
|
2946
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
|
+
}
|
|
2947
4385
|
$localBin = Join-Path $scriptDir "..\\node_modules\\.bin\\ese.cmd"
|
|
2948
4386
|
if (Test-Path $localBin) {
|
|
2949
4387
|
& $localBin @args
|
|
@@ -2954,6 +4392,14 @@ if (Test-Path $localEsedreBin) {
|
|
|
2954
4392
|
& $localEsedreBin @args
|
|
2955
4393
|
exit $LASTEXITCODE
|
|
2956
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
|
+
}
|
|
2957
4403
|
& npx --yes esedre @args
|
|
2958
4404
|
exit $LASTEXITCODE
|
|
2959
4405
|
`;
|
|
@@ -2962,11 +4408,8 @@ var WRAPPER_BASH = `#!/usr/bin/env bash
|
|
|
2962
4408
|
set -e
|
|
2963
4409
|
DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
|
2964
4410
|
|
|
2965
|
-
if
|
|
2966
|
-
exec
|
|
2967
|
-
fi
|
|
2968
|
-
if command -v esedre >/dev/null 2>&1; then
|
|
2969
|
-
exec esedre "$@"
|
|
4411
|
+
if [ -f "$DIR/../dist/esedre.mjs" ]; then
|
|
4412
|
+
exec node "$DIR/../dist/esedre.mjs" "$@"
|
|
2970
4413
|
fi
|
|
2971
4414
|
if [ -f "$DIR/../../esedre/dist/esedre.mjs" ]; then
|
|
2972
4415
|
exec node "$DIR/../../esedre/dist/esedre.mjs" "$@"
|
|
@@ -2974,12 +4417,21 @@ fi
|
|
|
2974
4417
|
if [ -f "$DIR/../esedre/dist/esedre.mjs" ]; then
|
|
2975
4418
|
exec node "$DIR/../esedre/dist/esedre.mjs" "$@"
|
|
2976
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
|
|
2977
4423
|
if [ -f "$DIR/../node_modules/.bin/ese" ]; then
|
|
2978
4424
|
exec "$DIR/../node_modules/.bin/ese" "$@"
|
|
2979
4425
|
fi
|
|
2980
4426
|
if [ -f "$DIR/../node_modules/.bin/esedre" ]; then
|
|
2981
4427
|
exec "$DIR/../node_modules/.bin/esedre" "$@"
|
|
2982
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
|
|
2983
4435
|
exec npx --yes esedre "$@"
|
|
2984
4436
|
`;
|
|
2985
4437
|
var ESE_WRAPPER_CMD = `@echo off
|
|
@@ -3131,7 +4583,7 @@ If the host application declares any or all of the 12 core design tokens on \`:r
|
|
|
3131
4583
|
- **Never delete tickets** directly via filesystem.
|
|
3132
4584
|
- **Workspace Scoping**: An LLM coding partner operates strictly within the authorized project scope declared in the local repository configuration. Central repository linking and system-level configuration are managed separately; focus roadmap work, implementation plans, and verification notes on this project.
|
|
3133
4585
|
- **Secret Developer Hash Route (\`#/planner\`, \`#/plan\`, \`#planner\`, \`#plan\`)**:
|
|
3134
|
-
- In web applications embedding Esedre (such as
|
|
4586
|
+
- In web applications embedding Esedre (such as your host web app), \`#/planner\` and \`#/plan\` serve as direct secret developer entry routes.
|
|
3135
4587
|
- **SEO Invariant**: These developer routes are private and MUST NEVER be exposed in \`sitemap.xml\`, \`robots.txt\`, \`llms.txt\`, or public navigation links.
|
|
3136
4588
|
- The URL hash is preserved across hard page refreshes (F5) without falling back to \`#/chat\` or other views.
|
|
3137
4589
|
`;
|
|
@@ -3556,10 +5008,17 @@ function startUiServer(port, webDir) {
|
|
|
3556
5008
|
|
|
3557
5009
|
// src/server/apiServer.ts
|
|
3558
5010
|
import http3 from "node:http";
|
|
5011
|
+
var MAX_JSON_BODY_BYTES = 10 * 1024 * 1024;
|
|
3559
5012
|
function readJsonBody(req) {
|
|
3560
5013
|
return new Promise((resolve, reject) => {
|
|
3561
5014
|
let body = "";
|
|
5015
|
+
let bytesRead = 0;
|
|
3562
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
|
+
}
|
|
3563
5022
|
body += chunk;
|
|
3564
5023
|
});
|
|
3565
5024
|
req.on("end", () => {
|
|
@@ -3612,7 +5071,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3612
5071
|
res.writeHead(204, {
|
|
3613
5072
|
"Access-Control-Allow-Origin": "*",
|
|
3614
5073
|
"Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
|
|
3615
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
5074
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, x-esedre-allowed-projects"
|
|
3616
5075
|
});
|
|
3617
5076
|
res.end();
|
|
3618
5077
|
return true;
|
|
@@ -3644,12 +5103,14 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3644
5103
|
const status = url.searchParams.get("status") || void 0;
|
|
3645
5104
|
const type = url.searchParams.get("type") || url.searchParams.get("category") || void 0;
|
|
3646
5105
|
const category = type;
|
|
5106
|
+
const priority = url.searchParams.get("priority") || void 0;
|
|
3647
5107
|
const search = url.searchParams.get("search") || void 0;
|
|
3648
5108
|
const tickets = await reqStorage.listTickets({
|
|
3649
5109
|
project: project === "all" ? void 0 : project,
|
|
3650
5110
|
status,
|
|
3651
5111
|
type,
|
|
3652
5112
|
category,
|
|
5113
|
+
priority,
|
|
3653
5114
|
search
|
|
3654
5115
|
});
|
|
3655
5116
|
sendJson(res, 200, tickets);
|
|
@@ -3685,9 +5146,10 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3685
5146
|
const numKey = String(t.meta.id);
|
|
3686
5147
|
const code = t.projectDescriptor?.code || t.meta.project || "Profe";
|
|
3687
5148
|
const prefixedKey = `${code}-${t.meta.id}`;
|
|
3688
|
-
|
|
5149
|
+
const metaWithLinks = { ...t.meta, links: t.links || t.meta.links || [], isBlocked: t.isBlocked };
|
|
5150
|
+
metasMap[prefixedKey] = metaWithLinks;
|
|
3689
5151
|
if (!metasMap[numKey] || code.toUpperCase() === "PROF") {
|
|
3690
|
-
metasMap[numKey] =
|
|
5152
|
+
metasMap[numKey] = metaWithLinks;
|
|
3691
5153
|
}
|
|
3692
5154
|
commentsMap[prefixedKey] = t.comments || [];
|
|
3693
5155
|
if (!commentsMap[numKey] || code.toUpperCase() === "PROF") {
|
|
@@ -3789,6 +5251,12 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3789
5251
|
sendJson(res, 200, snapshot);
|
|
3790
5252
|
return true;
|
|
3791
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
|
+
}
|
|
3792
5260
|
}
|
|
3793
5261
|
if (req.method === "POST") {
|
|
3794
5262
|
const body = await readJsonBody(req);
|
|
@@ -3825,6 +5293,59 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3825
5293
|
sendJson(res, 201, project);
|
|
3826
5294
|
return true;
|
|
3827
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
|
+
}
|
|
3828
5349
|
if (pathname === "/api/planning/plans") {
|
|
3829
5350
|
const { ticketId, planMarkdown, lastHash } = body;
|
|
3830
5351
|
invalidateApiCache();
|
|
@@ -3862,6 +5383,48 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3862
5383
|
sendJson(res, 200, { success: true, detail: detailMarkdown });
|
|
3863
5384
|
return true;
|
|
3864
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
|
+
}
|
|
3865
5428
|
}
|
|
3866
5429
|
sendJson(res, 404, { error: `Endpoint not found: ${pathname}` });
|
|
3867
5430
|
return true;
|
|
@@ -4043,18 +5606,33 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
|
|
|
4043
5606
|
${colors.bold}plan${colors.reset} <id> [--file <path> | --set "<markdown>"] [--last-hash <sha1>] [--json]
|
|
4044
5607
|
View or update implementation plan with optimistic concurrency control.
|
|
4045
5608
|
|
|
4046
|
-
${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]
|
|
4047
5610
|
Mint a new roadmap ticket with sequential numeric ID.
|
|
4048
5611
|
|
|
4049
|
-
${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]
|
|
4050
|
-
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.
|
|
4051
5614
|
|
|
4052
5615
|
${colors.bold}comment${colors.reset} <id> ["<text>"] [--text "..."] [--author "..."] [--json]
|
|
4053
5616
|
Append a research finding, test verification, or note to ticket history.
|
|
4054
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
|
+
|
|
4055
5625
|
${colors.bold}snapshot, refresh${colors.reset} [--project <code>] [--json]
|
|
4056
5626
|
Generate lean read-only projection snapshot (.esedre/snapshot.json) for zero-latency agent context.
|
|
4057
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
|
+
|
|
4058
5636
|
${colors.bold}SERVICE DAEMON COMMANDS:${colors.reset}
|
|
4059
5637
|
${colors.bold}start${colors.reset} [--port <n>] [--foreground | -f] [--quiet] [--json]
|
|
4060
5638
|
Start the Esedre background server and web UI daemon (default port ${DEFAULT_ESEDRE_PORT}).
|
|
@@ -4090,12 +5668,24 @@ ${colors.bold}DEVELOPER ADMINISTRATION (Human Setup & Configuration):${colors.re
|
|
|
4090
5668
|
${colors.bold}projects${colors.reset} [--json]
|
|
4091
5669
|
List registered workspace projects, project codes (e.g. 'CORE', 'WEB', 'DOCS'), and descriptions.
|
|
4092
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
|
+
|
|
4093
5677
|
${colors.bold}OPTIONS:${colors.reset}
|
|
4094
5678
|
-p, --project <code> Project code (e.g. CORE, ALCE, DOCS).
|
|
4095
5679
|
-n, --name <name> Project display name (e.g. "Alce Web Reader").
|
|
4096
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.
|
|
4097
5683
|
-s, --status <stat> Ticket status ('Planned', 'In Development', 'Completed', 'Rejected').
|
|
4098
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.
|
|
4099
5689
|
--detail "<md>" Specification markdown for ticket detail during create.
|
|
4100
5690
|
--file <path> Path to markdown file for detail (create) or implementation plan (plan).
|
|
4101
5691
|
--json Output raw machine-readable JSON (strongly recommended for autonomous LLM coding agents).
|
|
@@ -4134,7 +5724,7 @@ function parseArgs(rawArgs) {
|
|
|
4134
5724
|
flags["help"] = true;
|
|
4135
5725
|
} else if (key === "y") {
|
|
4136
5726
|
flags["yes"] = true;
|
|
4137
|
-
} 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") {
|
|
4138
5728
|
const next = rawArgs[i + 1];
|
|
4139
5729
|
if (next !== void 0 && !next.startsWith("-")) {
|
|
4140
5730
|
flags[key] = next;
|
|
@@ -4155,9 +5745,12 @@ function parseArgs(rawArgs) {
|
|
|
4155
5745
|
}
|
|
4156
5746
|
if (flags["t"] && !flags["type"]) flags["type"] = flags["t"];
|
|
4157
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"];
|
|
4158
5750
|
if (flags["n"] && !flags["name"]) flags["name"] = flags["n"];
|
|
4159
5751
|
if (flags["s"] && !flags["status"]) flags["status"] = flags["s"];
|
|
4160
5752
|
if (flags["q"] && !flags["search"]) flags["search"] = flags["q"];
|
|
5753
|
+
if (flags["r"] && !flags["relation"]) flags["relation"] = flags["r"];
|
|
4161
5754
|
if (flags["f"] && !flags["foreground"]) flags["foreground"] = flags["f"];
|
|
4162
5755
|
return { command, positionals, flags };
|
|
4163
5756
|
}
|
|
@@ -4733,6 +6326,80 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
|
|
|
4733
6326
|
printDaemonLogs({ port, lines, workspaceRoot: discovered.workspaceRoot });
|
|
4734
6327
|
return;
|
|
4735
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
|
+
}
|
|
4736
6403
|
case "projects": {
|
|
4737
6404
|
printDuplicateProjectWarnings(storage, isJson);
|
|
4738
6405
|
const projects = await storage.getProjects();
|
|
@@ -4753,8 +6420,26 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
|
|
|
4753
6420
|
const status = flags["status"] || void 0;
|
|
4754
6421
|
const type = flags["type"] || flags["category"] || void 0;
|
|
4755
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
|
+
}
|
|
4756
6438
|
const search = flags["search"] || void 0;
|
|
4757
|
-
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 });
|
|
4758
6443
|
if (isJson) {
|
|
4759
6444
|
console.log(JSON.stringify(tickets.map((t) => ({
|
|
4760
6445
|
id: t.meta.id,
|
|
@@ -4762,10 +6447,16 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
|
|
|
4762
6447
|
type: t.meta.type || t.meta.category,
|
|
4763
6448
|
category: t.meta.type || t.meta.category,
|
|
4764
6449
|
status: t.meta.status,
|
|
6450
|
+
priority: t.meta.priority,
|
|
4765
6451
|
complexity: normalizeDashesAndMojibake(t.meta.complexity),
|
|
4766
6452
|
effort: normalizeDashesAndMojibake(t.meta.estimatedEffort),
|
|
6453
|
+
milestone: t.meta.milestone,
|
|
6454
|
+
featureFlag: t.meta.featureFlag,
|
|
6455
|
+
inheritedFeatureFlag: t.meta.inheritedFeatureFlag,
|
|
4767
6456
|
project: t.projectDescriptor?.code || t.meta.project || "UNASSIGNED",
|
|
4768
|
-
sha1: t.sha1 || t.meta.sha1
|
|
6457
|
+
sha1: t.sha1 || t.meta.sha1,
|
|
6458
|
+
isBlocked: t.isBlocked,
|
|
6459
|
+
links: t.links
|
|
4769
6460
|
})), null, 2));
|
|
4770
6461
|
} else {
|
|
4771
6462
|
if (tickets.length === 0) {
|
|
@@ -4868,6 +6559,16 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4868
6559
|
const title = normalizeDashesAndMojibake(rawTitle);
|
|
4869
6560
|
const type = flags["type"] || flags["category"] || "Feature";
|
|
4870
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
|
+
}
|
|
4871
6572
|
const projectCode = flags["project"] || discovered.config?.projectCode;
|
|
4872
6573
|
if (!projectCode) {
|
|
4873
6574
|
console.error(`${colors.red}Error: Project is required to create a ticket (--project <code> or configure projectCode in esedre.json).${colors.reset}`);
|
|
@@ -4885,6 +6586,7 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4885
6586
|
}
|
|
4886
6587
|
const detailArg = flags["detail"];
|
|
4887
6588
|
const fileArg = flags["file"];
|
|
6589
|
+
const milestone = flags["milestone"] || flags["m"] || void 0;
|
|
4888
6590
|
let detailMarkdown = detailArg;
|
|
4889
6591
|
if (fileArg) {
|
|
4890
6592
|
const filePath = path9.resolve(fileArg);
|
|
@@ -4902,18 +6604,20 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4902
6604
|
title,
|
|
4903
6605
|
type,
|
|
4904
6606
|
category,
|
|
6607
|
+
priority,
|
|
4905
6608
|
projectCode,
|
|
4906
6609
|
complexity,
|
|
4907
6610
|
estimatedEffort,
|
|
4908
6611
|
summary,
|
|
4909
6612
|
submittedBy,
|
|
4910
6613
|
detail: detailMarkdown,
|
|
4911
|
-
detailMarkdown
|
|
6614
|
+
detailMarkdown,
|
|
6615
|
+
milestone
|
|
4912
6616
|
});
|
|
4913
6617
|
if (isJson) {
|
|
4914
6618
|
console.log(JSON.stringify(created, null, 2));
|
|
4915
6619
|
} else {
|
|
4916
|
-
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)})`);
|
|
4917
6621
|
}
|
|
4918
6622
|
return;
|
|
4919
6623
|
}
|
|
@@ -4931,6 +6635,7 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4931
6635
|
const effort = flags["effort"] ? normalizeDashesAndMojibake(flags["effort"]) : void 0;
|
|
4932
6636
|
const inDev = flags["in-dev"] !== void 0 ? Boolean(flags["in-dev"]) : void 0;
|
|
4933
6637
|
const flag = flags["flag"];
|
|
6638
|
+
const rawMilestone = flags["milestone"] !== void 0 ? flags["milestone"] : flags["m"];
|
|
4934
6639
|
const isForce = Boolean(flags["force"]);
|
|
4935
6640
|
const lastHash = isForce ? void 0 : flags["last-hash"];
|
|
4936
6641
|
const updates = {};
|
|
@@ -4940,19 +6645,172 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4940
6645
|
updates.category = type;
|
|
4941
6646
|
}
|
|
4942
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
|
+
}
|
|
4943
6661
|
if (title) updates.title = title;
|
|
4944
6662
|
if (complexity) updates.complexity = complexity;
|
|
4945
6663
|
if (effort) updates.estimatedEffort = effort;
|
|
4946
6664
|
if (inDev !== void 0) updates.isActivePlanning = inDev;
|
|
4947
6665
|
if (flag) updates.featureFlag = flag;
|
|
6666
|
+
if (rawMilestone !== void 0) updates.milestone = rawMilestone;
|
|
4948
6667
|
const updated = await storage.updateTicket(lookupKey, updates, lastHash);
|
|
4949
6668
|
if (isJson) {
|
|
4950
6669
|
console.log(JSON.stringify(updated, null, 2));
|
|
4951
6670
|
} else {
|
|
4952
|
-
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}`);
|
|
4953
6672
|
}
|
|
4954
6673
|
return;
|
|
4955
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
|
+
}
|
|
4956
6814
|
case "comment": {
|
|
4957
6815
|
const idStr = positionals[0];
|
|
4958
6816
|
if (!idStr) {
|
|
@@ -4976,6 +6834,58 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4976
6834
|
}
|
|
4977
6835
|
return;
|
|
4978
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
|
+
}
|
|
4979
6889
|
default: {
|
|
4980
6890
|
console.error(`${colors.red}Error: Unknown command "${command}"${colors.reset}`);
|
|
4981
6891
|
printHelp();
|