esedre 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esedre.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import fs3 from "node:fs";
|
|
|
9
9
|
import path3 from "node:path";
|
|
10
10
|
|
|
11
11
|
// src/types.ts
|
|
12
|
-
var CURRENT_ESEDRE_VERSION = "0.1.
|
|
12
|
+
var CURRENT_ESEDRE_VERSION = "0.1.8";
|
|
13
13
|
var EsedreConflictError = class extends Error {
|
|
14
14
|
constructor(ticketId, currentHash, lastHash) {
|
|
15
15
|
super(
|
|
@@ -30,8 +30,8 @@ import fs from "node:fs";
|
|
|
30
30
|
import path from "node:path";
|
|
31
31
|
import os from "node:os";
|
|
32
32
|
var DEFAULT_ESEDRE_PORT = 5674;
|
|
33
|
-
var MAX_PROJECT_CODE_LENGTH =
|
|
34
|
-
var PROJECT_CODE_REGEX = /^[a-zA-Z0-9]{1,
|
|
33
|
+
var MAX_PROJECT_CODE_LENGTH = 8;
|
|
34
|
+
var PROJECT_CODE_REGEX = /^[a-zA-Z0-9]{1,8}$/;
|
|
35
35
|
var MAX_PROJECT_NAME_LENGTH = 48;
|
|
36
36
|
function resolvePorts(config, overrides) {
|
|
37
37
|
const globalConfig = readGlobalConfig();
|
|
@@ -244,9 +244,11 @@ function removeLocationFromGlobalConfig(identifier) {
|
|
|
244
244
|
if (current.dataDir) {
|
|
245
245
|
const currentDataDirs = Array.isArray(current.dataDir) ? current.dataDir : [current.dataDir];
|
|
246
246
|
const initialLen = currentDataDirs.length;
|
|
247
|
-
const remaining = currentDataDirs.filter(
|
|
248
|
-
|
|
249
|
-
|
|
247
|
+
const remaining = currentDataDirs.filter((d) => {
|
|
248
|
+
const matchPath = path.resolve(expandHome(d)).toLowerCase() === path.resolve(expandHome(trimmed)).toLowerCase();
|
|
249
|
+
const matchBasename = path.basename(d).toLowerCase() === trimmed.toLowerCase();
|
|
250
|
+
return !matchPath && !matchBasename;
|
|
251
|
+
});
|
|
250
252
|
if (remaining.length !== initialLen) {
|
|
251
253
|
current.dataDir = remaining.length === 0 ? void 0 : remaining.length === 1 ? remaining[0] : remaining;
|
|
252
254
|
writeGlobalConfig(current);
|
|
@@ -537,12 +539,12 @@ async function generateProjectSnapshot(storage, projectCode, outputDir) {
|
|
|
537
539
|
}
|
|
538
540
|
|
|
539
541
|
// src/storage/filesystem.ts
|
|
540
|
-
function isProjectMatch(code,
|
|
542
|
+
function isProjectMatch(code, filter) {
|
|
543
|
+
if (!code || !filter) return false;
|
|
541
544
|
const f = filter.trim().toLowerCase();
|
|
542
545
|
const c = code.trim().toLowerCase();
|
|
543
|
-
|
|
544
|
-
if (
|
|
545
|
-
if ((f === "prof" || f === "core" || f === "pasrc") && (c === "prof" || c === "core")) return true;
|
|
546
|
+
if (c === f) return true;
|
|
547
|
+
if ((f === "profe" || f === "prof" || f === "core" || f === "pasrc") && (c === "profe" || c === "prof" || c === "core")) return true;
|
|
546
548
|
if ((f === "esedre" || f === "ese" || f === "docs") && (c === "esedre" || c === "docs")) return true;
|
|
547
549
|
if ((f === "alce" || f === "web") && (c === "alce" || c === "web")) return true;
|
|
548
550
|
return false;
|
|
@@ -570,17 +572,51 @@ function writeSafeFile(filePath, content) {
|
|
|
570
572
|
var FilesystemStorageAdapter = class {
|
|
571
573
|
workspaceRoot;
|
|
572
574
|
config;
|
|
575
|
+
isConfigExplicit = false;
|
|
576
|
+
lastConfigCheck = 0;
|
|
577
|
+
configTtlMs = 3e3;
|
|
578
|
+
duplicateProjectWarnings = [];
|
|
579
|
+
getDuplicateProjectWarnings() {
|
|
580
|
+
this.resolveProjectLocations();
|
|
581
|
+
return [...this.duplicateProjectWarnings];
|
|
582
|
+
}
|
|
573
583
|
constructor(workspaceRoot, config) {
|
|
574
584
|
this.workspaceRoot = workspaceRoot || this.resolveWorkspaceRoot();
|
|
575
585
|
if (config) {
|
|
576
586
|
this.config = config;
|
|
587
|
+
this.isConfigExplicit = true;
|
|
588
|
+
this.lastConfigCheck = Date.now();
|
|
577
589
|
} else {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
590
|
+
this.config = {};
|
|
591
|
+
this.refreshConfig();
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
refreshConfig() {
|
|
595
|
+
try {
|
|
596
|
+
const discovered = findEsedreConfig(this.workspaceRoot);
|
|
597
|
+
this.config = discovered.config || {};
|
|
598
|
+
} catch {
|
|
599
|
+
this.config = {};
|
|
600
|
+
}
|
|
601
|
+
this.lastConfigCheck = Date.now();
|
|
602
|
+
}
|
|
603
|
+
ensureFreshConfig() {
|
|
604
|
+
if (this.isConfigExplicit && this.config.allowedProjects?.includes("*")) {
|
|
605
|
+
if (Date.now() - this.lastConfigCheck > this.configTtlMs) {
|
|
606
|
+
try {
|
|
607
|
+
const discovered = findEsedreConfig(this.workspaceRoot, { fallbackToGlobal: true });
|
|
608
|
+
this.config = {
|
|
609
|
+
...discovered.config,
|
|
610
|
+
allowedProjects: ["*"]
|
|
611
|
+
};
|
|
612
|
+
} catch {
|
|
613
|
+
}
|
|
614
|
+
this.lastConfigCheck = Date.now();
|
|
583
615
|
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (!this.isConfigExplicit && Date.now() - this.lastConfigCheck > this.configTtlMs) {
|
|
619
|
+
this.refreshConfig();
|
|
584
620
|
}
|
|
585
621
|
}
|
|
586
622
|
resolveWorkspaceRoot() {
|
|
@@ -599,8 +635,11 @@ var FilesystemStorageAdapter = class {
|
|
|
599
635
|
return process.cwd();
|
|
600
636
|
}
|
|
601
637
|
resolveProjectLocations() {
|
|
638
|
+
this.ensureFreshConfig();
|
|
602
639
|
const locations = [];
|
|
603
640
|
const seenCodes = /* @__PURE__ */ new Set();
|
|
641
|
+
const codeToSource = /* @__PURE__ */ new Map();
|
|
642
|
+
this.duplicateProjectWarnings = [];
|
|
604
643
|
const rawDataDirs = Array.isArray(this.config.dataDir) ? [...this.config.dataDir] : this.config.dataDir ? [this.config.dataDir] : [];
|
|
605
644
|
if (rawDataDirs.length === 0) {
|
|
606
645
|
const candidateHubDirs = [
|
|
@@ -640,9 +679,6 @@ var FilesystemStorageAdapter = class {
|
|
|
640
679
|
if (fs3.existsSync(pJsonPath)) {
|
|
641
680
|
try {
|
|
642
681
|
desc = JSON.parse(fs3.readFileSync(pJsonPath, "utf-8"));
|
|
643
|
-
if (desc && !desc.slug) {
|
|
644
|
-
desc.slug = (desc.code || ent.name).toLowerCase();
|
|
645
|
-
}
|
|
646
682
|
} catch {
|
|
647
683
|
}
|
|
648
684
|
}
|
|
@@ -654,7 +690,7 @@ var FilesystemStorageAdapter = class {
|
|
|
654
690
|
try {
|
|
655
691
|
const hubProjs = JSON.parse(fs3.readFileSync(f, "utf-8"));
|
|
656
692
|
desc = hubProjs.find(
|
|
657
|
-
(p) => p.code.toLowerCase() === ent.name.toLowerCase()
|
|
693
|
+
(p) => p.code.toLowerCase() === ent.name.toLowerCase()
|
|
658
694
|
);
|
|
659
695
|
if (desc) break;
|
|
660
696
|
} catch {
|
|
@@ -666,20 +702,32 @@ var FilesystemStorageAdapter = class {
|
|
|
666
702
|
desc = {
|
|
667
703
|
id: locations.length + 1,
|
|
668
704
|
code: ent.name,
|
|
669
|
-
slug: ent.name.toLowerCase(),
|
|
670
705
|
name: ent.name,
|
|
671
706
|
description: `${ent.name} project`
|
|
672
707
|
};
|
|
673
708
|
}
|
|
674
709
|
const codeKey = desc.code.toLowerCase();
|
|
710
|
+
const sourceName = resolvedHub;
|
|
675
711
|
if (!seenCodes.has(codeKey)) {
|
|
676
712
|
seenCodes.add(codeKey);
|
|
713
|
+
codeToSource.set(codeKey, sourceName);
|
|
677
714
|
locations.push({
|
|
678
715
|
project: desc,
|
|
679
716
|
ticketsDir: projectTicketsDir,
|
|
680
717
|
sourceType: "hub",
|
|
681
718
|
hubDir: resolvedHub
|
|
682
719
|
});
|
|
720
|
+
} else {
|
|
721
|
+
const firstSource = codeToSource.get(codeKey) || "configured location";
|
|
722
|
+
if (firstSource.toLowerCase() !== sourceName.toLowerCase()) {
|
|
723
|
+
if (!this.duplicateProjectWarnings.some((w) => w.code.toLowerCase() === codeKey && w.duplicateHub.toLowerCase() === sourceName.toLowerCase())) {
|
|
724
|
+
this.duplicateProjectWarnings.push({
|
|
725
|
+
code: desc.code,
|
|
726
|
+
firstHub: firstSource,
|
|
727
|
+
duplicateHub: sourceName
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
683
731
|
}
|
|
684
732
|
}
|
|
685
733
|
} else {
|
|
@@ -705,7 +753,6 @@ var FilesystemStorageAdapter = class {
|
|
|
705
753
|
descs = [{
|
|
706
754
|
id: 1,
|
|
707
755
|
code: pCode,
|
|
708
|
-
slug: pCode.toLowerCase(),
|
|
709
756
|
name: pCode,
|
|
710
757
|
description: `${pCode} project`
|
|
711
758
|
}];
|
|
@@ -713,8 +760,10 @@ var FilesystemStorageAdapter = class {
|
|
|
713
760
|
const isShared = descs.length > 1;
|
|
714
761
|
for (const desc of descs) {
|
|
715
762
|
const codeKey = desc.code.toLowerCase();
|
|
763
|
+
const sourceName = resolvedHub;
|
|
716
764
|
if (!seenCodes.has(codeKey)) {
|
|
717
765
|
seenCodes.add(codeKey);
|
|
766
|
+
codeToSource.set(codeKey, sourceName);
|
|
718
767
|
locations.push({
|
|
719
768
|
project: desc,
|
|
720
769
|
ticketsDir: actualTicketsDir,
|
|
@@ -722,6 +771,17 @@ var FilesystemStorageAdapter = class {
|
|
|
722
771
|
hubDir: resolvedHub,
|
|
723
772
|
isSharedDir: isShared
|
|
724
773
|
});
|
|
774
|
+
} else {
|
|
775
|
+
const firstSource = codeToSource.get(codeKey) || "configured location";
|
|
776
|
+
if (firstSource.toLowerCase() !== sourceName.toLowerCase()) {
|
|
777
|
+
if (!this.duplicateProjectWarnings.some((w) => w.code.toLowerCase() === codeKey && w.duplicateHub.toLowerCase() === sourceName.toLowerCase())) {
|
|
778
|
+
this.duplicateProjectWarnings.push({
|
|
779
|
+
code: desc.code,
|
|
780
|
+
firstHub: firstSource,
|
|
781
|
+
duplicateHub: sourceName
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
725
785
|
}
|
|
726
786
|
}
|
|
727
787
|
}
|
|
@@ -747,9 +807,6 @@ var FilesystemStorageAdapter = class {
|
|
|
747
807
|
if (fs3.existsSync(cand)) {
|
|
748
808
|
try {
|
|
749
809
|
desc = JSON.parse(fs3.readFileSync(cand, "utf-8"));
|
|
750
|
-
if (desc && !desc.slug) {
|
|
751
|
-
desc.slug = (desc.code || code).toLowerCase();
|
|
752
|
-
}
|
|
753
810
|
break;
|
|
754
811
|
} catch {
|
|
755
812
|
}
|
|
@@ -759,19 +816,31 @@ var FilesystemStorageAdapter = class {
|
|
|
759
816
|
desc = {
|
|
760
817
|
id: locations.length + 1,
|
|
761
818
|
code,
|
|
762
|
-
slug: code.toLowerCase(),
|
|
763
819
|
name: code,
|
|
764
820
|
description: `${code} project`
|
|
765
821
|
};
|
|
766
822
|
}
|
|
767
823
|
const codeKey = desc.code.toLowerCase();
|
|
824
|
+
const sourceName = projectDir;
|
|
768
825
|
if (!seenCodes.has(codeKey)) {
|
|
769
826
|
seenCodes.add(codeKey);
|
|
827
|
+
codeToSource.set(codeKey, sourceName);
|
|
770
828
|
locations.push({
|
|
771
829
|
project: desc,
|
|
772
830
|
ticketsDir: actualTicketsDir,
|
|
773
831
|
sourceType: "federated"
|
|
774
832
|
});
|
|
833
|
+
} else {
|
|
834
|
+
const firstSource = codeToSource.get(codeKey) || "configured location";
|
|
835
|
+
if (firstSource.toLowerCase() !== sourceName.toLowerCase()) {
|
|
836
|
+
if (!this.duplicateProjectWarnings.some((w) => w.code.toLowerCase() === codeKey && w.duplicateHub.toLowerCase() === sourceName.toLowerCase())) {
|
|
837
|
+
this.duplicateProjectWarnings.push({
|
|
838
|
+
code: desc.code,
|
|
839
|
+
firstHub: firstSource,
|
|
840
|
+
duplicateHub: sourceName
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
775
844
|
}
|
|
776
845
|
}
|
|
777
846
|
}
|
|
@@ -814,7 +883,6 @@ var FilesystemStorageAdapter = class {
|
|
|
814
883
|
descs = [{
|
|
815
884
|
id: 1,
|
|
816
885
|
code: this.config.projectCode,
|
|
817
|
-
slug: this.config.projectCode.toLowerCase(),
|
|
818
886
|
name: pName,
|
|
819
887
|
description: `${pName} project`
|
|
820
888
|
}];
|
|
@@ -846,7 +914,7 @@ var FilesystemStorageAdapter = class {
|
|
|
846
914
|
const cleanCode = rawCode;
|
|
847
915
|
const cleanName = input.name?.trim() || cleanCode;
|
|
848
916
|
const cleanDesc = input.description?.trim() || `${cleanName} project`;
|
|
849
|
-
const
|
|
917
|
+
const cleanLower = cleanCode.toLowerCase();
|
|
850
918
|
const colors2 = input.colors || {
|
|
851
919
|
badge: "border-cyan-500/30 bg-cyan-500/10 text-cyan-300",
|
|
852
920
|
dot: "bg-cyan-400",
|
|
@@ -872,17 +940,91 @@ var FilesystemStorageAdapter = class {
|
|
|
872
940
|
}
|
|
873
941
|
}
|
|
874
942
|
}
|
|
875
|
-
|
|
943
|
+
const existingHubs = [];
|
|
876
944
|
for (const dDir of rawDataDirs) {
|
|
877
945
|
const resolved = path3.resolve(this.workspaceRoot, expandHome(dDir));
|
|
878
|
-
if (fs3.existsSync(resolved)) {
|
|
879
|
-
|
|
880
|
-
|
|
946
|
+
if (fs3.existsSync(resolved) && !existingHubs.includes(resolved)) {
|
|
947
|
+
existingHubs.push(resolved);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
let targetHub = null;
|
|
951
|
+
if (input.hub) {
|
|
952
|
+
const targetQuery = input.hub.trim();
|
|
953
|
+
const resolvedQuery = path3.resolve(this.workspaceRoot, expandHome(targetQuery)).toLowerCase();
|
|
954
|
+
const isPathLike = targetQuery.includes("/") || targetQuery.includes("\\");
|
|
955
|
+
const basenameMatches = existingHubs.filter(
|
|
956
|
+
(h) => path3.basename(h).toLowerCase() === targetQuery.toLowerCase()
|
|
957
|
+
);
|
|
958
|
+
if (!isPathLike && basenameMatches.length > 1) {
|
|
959
|
+
const list = basenameMatches.map((h) => ` \u2022 ${h}`).join("\n");
|
|
960
|
+
throw new Error(
|
|
961
|
+
`Hub name collision: Multiple configured data hubs share basename "${targetQuery}". Please specify by unambiguous path:
|
|
962
|
+
${list}`
|
|
963
|
+
);
|
|
964
|
+
} else if (basenameMatches.length === 1 && !isPathLike) {
|
|
965
|
+
targetHub = basenameMatches[0];
|
|
966
|
+
} else {
|
|
967
|
+
const exactPathMatch = existingHubs.find((h) => h.toLowerCase() === resolvedQuery);
|
|
968
|
+
if (exactPathMatch) {
|
|
969
|
+
targetHub = exactPathMatch;
|
|
970
|
+
} else if (basenameMatches.length === 1) {
|
|
971
|
+
targetHub = basenameMatches[0];
|
|
972
|
+
} else {
|
|
973
|
+
const directHubPath = path3.resolve(this.workspaceRoot, expandHome(targetQuery));
|
|
974
|
+
if (fs3.existsSync(directHubPath) && (fs3.existsSync(path3.join(directHubPath, "projects")) || fs3.existsSync(path3.join(directHubPath, "projects.json")))) {
|
|
975
|
+
targetHub = directHubPath;
|
|
976
|
+
} else {
|
|
977
|
+
const available = existingHubs.map((h) => `${path3.basename(h)} (${h})`).join(", ");
|
|
978
|
+
throw new Error(
|
|
979
|
+
`Hub "${targetQuery}" not found. Configured data hubs: ${available || "(none)"}`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
} else {
|
|
985
|
+
if (existingHubs.length === 1) {
|
|
986
|
+
targetHub = existingHubs[0];
|
|
987
|
+
} else if (existingHubs.length > 1) {
|
|
988
|
+
const basenames = existingHubs.map((h) => path3.basename(h));
|
|
989
|
+
const hasCollision = new Set(basenames.map((b) => b.toLowerCase())).size < basenames.length;
|
|
990
|
+
const formattedList = existingHubs.map((h) => ` \u2022 ${path3.basename(h)} (${h})`).join("\n");
|
|
991
|
+
throw new Error(
|
|
992
|
+
`Multiple data hubs configured. Please specify --hub <name${hasCollision ? "|path" : ""}>:
|
|
993
|
+
${formattedList}`
|
|
994
|
+
);
|
|
881
995
|
}
|
|
882
996
|
}
|
|
883
997
|
if (targetHub) {
|
|
998
|
+
const normalizedTargetHub = path3.resolve(targetHub).toLowerCase();
|
|
999
|
+
const existingLocations = this.resolveProjectLocations();
|
|
1000
|
+
const existingConflict = existingLocations.find((l) => {
|
|
1001
|
+
if (l.project.code.toLowerCase() !== cleanLower) return false;
|
|
1002
|
+
if (l.sourceType === "hub" && l.hubDir) {
|
|
1003
|
+
return path3.resolve(l.hubDir).toLowerCase() !== normalizedTargetHub;
|
|
1004
|
+
}
|
|
1005
|
+
return false;
|
|
1006
|
+
});
|
|
1007
|
+
if (existingConflict && existingConflict.hubDir) {
|
|
1008
|
+
const conflictLoc = path3.basename(existingConflict.hubDir);
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
`Project code "${cleanCode}" already exists in another data hub (${conflictLoc}). Duplicate project codes across hubs are not supported.`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
884
1013
|
const hubProjectsDir = path3.join(targetHub, "projects");
|
|
885
|
-
|
|
1014
|
+
let resolvedDirName = cleanCode;
|
|
1015
|
+
if (fs3.existsSync(hubProjectsDir)) {
|
|
1016
|
+
try {
|
|
1017
|
+
const entries = fs3.readdirSync(hubProjectsDir, { withFileTypes: true });
|
|
1018
|
+
const matchedDir = entries.find(
|
|
1019
|
+
(e) => e.isDirectory() && e.name.toLowerCase() === cleanLower
|
|
1020
|
+
);
|
|
1021
|
+
if (matchedDir) {
|
|
1022
|
+
resolvedDirName = matchedDir.name;
|
|
1023
|
+
}
|
|
1024
|
+
} catch {
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const projectDir = path3.join(hubProjectsDir, resolvedDirName);
|
|
886
1028
|
const ticketsDir2 = path3.join(projectDir, "tickets");
|
|
887
1029
|
if (!fs3.existsSync(ticketsDir2)) {
|
|
888
1030
|
fs3.mkdirSync(ticketsDir2, { recursive: true });
|
|
@@ -895,13 +1037,13 @@ var FilesystemStorageAdapter = class {
|
|
|
895
1037
|
} catch {
|
|
896
1038
|
}
|
|
897
1039
|
}
|
|
1040
|
+
const resolvedCode2 = existingPJson2?.code || resolvedDirName || cleanCode;
|
|
898
1041
|
const existingProjects = await this.getProjects();
|
|
899
1042
|
const maxId = existingProjects.reduce((max, p) => Math.max(max, p.id || 0), 0);
|
|
900
1043
|
const id2 = existingPJson2?.id || maxId + 1;
|
|
901
1044
|
const projectDesc2 = {
|
|
902
1045
|
id: id2,
|
|
903
|
-
code:
|
|
904
|
-
slug,
|
|
1046
|
+
code: resolvedCode2,
|
|
905
1047
|
name: cleanName,
|
|
906
1048
|
description: cleanDesc,
|
|
907
1049
|
colors: colors2
|
|
@@ -911,7 +1053,7 @@ var FilesystemStorageAdapter = class {
|
|
|
911
1053
|
if (fs3.existsSync(hubProjectsFile)) {
|
|
912
1054
|
try {
|
|
913
1055
|
const arr = JSON.parse(fs3.readFileSync(hubProjectsFile, "utf-8"));
|
|
914
|
-
const idx = arr.findIndex((p) => p.code.toLowerCase() ===
|
|
1056
|
+
const idx = arr.findIndex((p) => p.code.toLowerCase() === cleanLower);
|
|
915
1057
|
if (idx >= 0) {
|
|
916
1058
|
arr[idx] = projectDesc2;
|
|
917
1059
|
} else {
|
|
@@ -936,11 +1078,11 @@ var FilesystemStorageAdapter = class {
|
|
|
936
1078
|
} catch {
|
|
937
1079
|
}
|
|
938
1080
|
}
|
|
1081
|
+
const resolvedCode = existingPJson?.code && existingPJson.code.toLowerCase() === cleanLower ? existingPJson.code : cleanCode;
|
|
939
1082
|
const id = existingPJson?.id || 1;
|
|
940
1083
|
const projectDesc = {
|
|
941
1084
|
id,
|
|
942
|
-
code:
|
|
943
|
-
slug,
|
|
1085
|
+
code: resolvedCode,
|
|
944
1086
|
name: cleanName,
|
|
945
1087
|
description: cleanDesc,
|
|
946
1088
|
colors: colors2
|
|
@@ -950,7 +1092,7 @@ var FilesystemStorageAdapter = class {
|
|
|
950
1092
|
if (fs3.existsSync(projectsJsonPath)) {
|
|
951
1093
|
try {
|
|
952
1094
|
const arr = JSON.parse(fs3.readFileSync(projectsJsonPath, "utf-8"));
|
|
953
|
-
const idx = arr.findIndex((p) => p.code.toLowerCase() ===
|
|
1095
|
+
const idx = arr.findIndex((p) => p.code.toLowerCase() === cleanLower);
|
|
954
1096
|
if (idx >= 0) {
|
|
955
1097
|
arr[idx] = projectDesc;
|
|
956
1098
|
} else {
|
|
@@ -966,11 +1108,11 @@ var FilesystemStorageAdapter = class {
|
|
|
966
1108
|
const locations = this.resolveProjectLocations();
|
|
967
1109
|
if (locations.length === 0) return null;
|
|
968
1110
|
const strId = String(id).trim();
|
|
969
|
-
const prefixMatch = strId.match(/^([a-zA-Z0-9]{1,
|
|
1111
|
+
const prefixMatch = strId.match(/^([a-zA-Z0-9]{1,8})[-_:](\d+)$/i);
|
|
970
1112
|
if (prefixMatch) {
|
|
971
1113
|
const code = prefixMatch[1].toLowerCase();
|
|
972
1114
|
const num2 = parseInt(prefixMatch[2], 10);
|
|
973
|
-
const loc = locations.find((l) => isProjectMatch(l.project.code,
|
|
1115
|
+
const loc = locations.find((l) => isProjectMatch(l.project.code, code));
|
|
974
1116
|
if (!loc) return null;
|
|
975
1117
|
const ticketDir = path3.join(loc.ticketsDir, String(num2));
|
|
976
1118
|
const metaPath = path3.join(ticketDir, "meta.json");
|
|
@@ -982,7 +1124,7 @@ var FilesystemStorageAdapter = class {
|
|
|
982
1124
|
meta.type = meta.type || meta.category || "Feature";
|
|
983
1125
|
meta.category = meta.type;
|
|
984
1126
|
const mProj = (meta.project || "").toLowerCase();
|
|
985
|
-
if (mProj && mProj !== code && mProj !== loc.project.
|
|
1127
|
+
if (mProj && mProj !== code && mProj !== loc.project.code.toLowerCase()) {
|
|
986
1128
|
return null;
|
|
987
1129
|
}
|
|
988
1130
|
} catch {
|
|
@@ -1007,7 +1149,7 @@ var FilesystemStorageAdapter = class {
|
|
|
1007
1149
|
const metaRaw = fs3.readFileSync(metaPath, "utf-8");
|
|
1008
1150
|
const meta = JSON.parse(metaRaw);
|
|
1009
1151
|
const matchedLoc = locations.find(
|
|
1010
|
-
(l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project &&
|
|
1152
|
+
(l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && l.project.code.toLowerCase() === meta.project.toLowerCase()
|
|
1011
1153
|
) || loc;
|
|
1012
1154
|
matches.push({ loc: matchedLoc, ticketDir, id: num });
|
|
1013
1155
|
} catch {
|
|
@@ -1020,7 +1162,7 @@ var FilesystemStorageAdapter = class {
|
|
|
1020
1162
|
if (this.config.projectCode) {
|
|
1021
1163
|
const prefLower = this.config.projectCode.toLowerCase();
|
|
1022
1164
|
const preferredMatch = matches.find(
|
|
1023
|
-
(m) => m.loc.project.code.toLowerCase() === prefLower
|
|
1165
|
+
(m) => m.loc.project.code.toLowerCase() === prefLower
|
|
1024
1166
|
);
|
|
1025
1167
|
if (preferredMatch) return preferredMatch;
|
|
1026
1168
|
}
|
|
@@ -1042,7 +1184,7 @@ var FilesystemStorageAdapter = class {
|
|
|
1042
1184
|
for (const loc of locations) {
|
|
1043
1185
|
if (filter?.project && filter.project !== "all") {
|
|
1044
1186
|
const pLower = filter.project.toLowerCase();
|
|
1045
|
-
if (!isProjectMatch(loc.project.code,
|
|
1187
|
+
if (!isProjectMatch(loc.project.code, filter.project)) {
|
|
1046
1188
|
continue;
|
|
1047
1189
|
}
|
|
1048
1190
|
}
|
|
@@ -1074,7 +1216,7 @@ var FilesystemStorageAdapter = class {
|
|
|
1074
1216
|
let effectiveLoc = loc;
|
|
1075
1217
|
if (loc.isSharedDir) {
|
|
1076
1218
|
const found = locations.find(
|
|
1077
|
-
(l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project &&
|
|
1219
|
+
(l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && l.project.code.toLowerCase() === meta.project.toLowerCase()
|
|
1078
1220
|
);
|
|
1079
1221
|
if (found) effectiveLoc = found;
|
|
1080
1222
|
}
|
|
@@ -1082,7 +1224,7 @@ var FilesystemStorageAdapter = class {
|
|
|
1082
1224
|
meta.projectId = effectiveLoc.project.id;
|
|
1083
1225
|
if (filter?.project && filter.project !== "all") {
|
|
1084
1226
|
const pLower = filter.project.toLowerCase();
|
|
1085
|
-
if (!isProjectMatch(meta.project,
|
|
1227
|
+
if (!isProjectMatch(meta.project, filter.project)) {
|
|
1086
1228
|
continue;
|
|
1087
1229
|
}
|
|
1088
1230
|
}
|
|
@@ -1249,7 +1391,7 @@ var FilesystemStorageAdapter = class {
|
|
|
1249
1391
|
if (requestedIdentifier) {
|
|
1250
1392
|
const lower = String(requestedIdentifier).trim().toLowerCase();
|
|
1251
1393
|
targetLoc = locations.find(
|
|
1252
|
-
(l) => l.project.code?.toLowerCase() === lower ||
|
|
1394
|
+
(l) => l.project.code?.toLowerCase() === lower || String(l.project.id) === lower || l.project.name?.toLowerCase() === lower
|
|
1253
1395
|
);
|
|
1254
1396
|
if (!targetLoc) {
|
|
1255
1397
|
throw new Error(`Project '${requestedIdentifier}' is invalid or not registered in this workspace.`);
|
|
@@ -1333,6 +1475,7 @@ ${input.summary || "Summary to be defined."}
|
|
|
1333
1475
|
...existing.meta,
|
|
1334
1476
|
...updates,
|
|
1335
1477
|
id: locInfo.id,
|
|
1478
|
+
project: locInfo.loc.project.code,
|
|
1336
1479
|
updatedAt: now,
|
|
1337
1480
|
completedAt,
|
|
1338
1481
|
revision
|
|
@@ -1433,6 +1576,9 @@ var SecurityFilter = class {
|
|
|
1433
1576
|
}
|
|
1434
1577
|
target;
|
|
1435
1578
|
config;
|
|
1579
|
+
getDuplicateProjectWarnings() {
|
|
1580
|
+
return this.target.getDuplicateProjectWarnings ? this.target.getDuplicateProjectWarnings() : [];
|
|
1581
|
+
}
|
|
1436
1582
|
async getProjects() {
|
|
1437
1583
|
const projs = await this.target.getProjects();
|
|
1438
1584
|
if (!this.config.allowedProjects || this.config.allowedProjects.length === 0) {
|
|
@@ -2451,11 +2597,13 @@ async function startDaemon(options = {}) {
|
|
|
2451
2597
|
if (!pid) {
|
|
2452
2598
|
throw new Error("Failed to spawn Esedre daemon background process.");
|
|
2453
2599
|
}
|
|
2600
|
+
const timeoutMs = process.platform === "win32" ? 1e4 : 5e3;
|
|
2601
|
+
const pingTimeout = process.platform === "win32" ? 500 : 300;
|
|
2454
2602
|
const startTime = Date.now();
|
|
2455
2603
|
let ready = false;
|
|
2456
|
-
while (Date.now() - startTime <
|
|
2457
|
-
await new Promise((r) => setTimeout(r,
|
|
2458
|
-
const ping = await pingDaemon(port,
|
|
2604
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2605
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
2606
|
+
const ping = await pingDaemon(port, pingTimeout);
|
|
2459
2607
|
if (ping.responding && ping.isEsedre) {
|
|
2460
2608
|
ready = true;
|
|
2461
2609
|
break;
|
|
@@ -3056,7 +3204,10 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
3056
3204
|
}
|
|
3057
3205
|
}
|
|
3058
3206
|
let config;
|
|
3059
|
-
|
|
3207
|
+
let projectCode = options.projectCode || existingRaw?.projectCode;
|
|
3208
|
+
if (existingRaw?.projectCode && options.projectCode && existingRaw.projectCode.toLowerCase() === options.projectCode.toLowerCase()) {
|
|
3209
|
+
projectCode = existingRaw.projectCode;
|
|
3210
|
+
}
|
|
3060
3211
|
const projectName = options.projectName || existingRaw?.projectName || (projectCode ? projectCode : void 0);
|
|
3061
3212
|
const dataDir = options.dataDir !== void 0 ? options.dataDir : existingRaw?.dataDir;
|
|
3062
3213
|
if (existingRaw) {
|
|
@@ -3097,7 +3248,6 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
3097
3248
|
const projDesc = {
|
|
3098
3249
|
id: 1,
|
|
3099
3250
|
code: projectCode.toUpperCase(),
|
|
3100
|
-
slug: projectCode.toLowerCase(),
|
|
3101
3251
|
name: projectName || projectCode.toUpperCase(),
|
|
3102
3252
|
description: `${projectName || projectCode.toUpperCase()} project`,
|
|
3103
3253
|
colors: {
|
|
@@ -3270,16 +3420,30 @@ function sendJson(res, status, data) {
|
|
|
3270
3420
|
"Content-Type": "application/json; charset=utf-8",
|
|
3271
3421
|
"Access-Control-Allow-Origin": "*",
|
|
3272
3422
|
"Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
|
|
3273
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
3423
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, x-esedre-allowed-projects"
|
|
3274
3424
|
});
|
|
3275
3425
|
res.end(JSON.stringify(data));
|
|
3276
3426
|
}
|
|
3277
|
-
var cachedAllData =
|
|
3278
|
-
var cachedAllTimestamp = 0;
|
|
3427
|
+
var cachedAllData = /* @__PURE__ */ new Map();
|
|
3279
3428
|
var CACHE_TTL_MS = 5e3;
|
|
3280
3429
|
function invalidateApiCache() {
|
|
3281
|
-
cachedAllData
|
|
3282
|
-
|
|
3430
|
+
cachedAllData.clear();
|
|
3431
|
+
}
|
|
3432
|
+
function getRequestStorage(baseStorage, req, url) {
|
|
3433
|
+
const headerAllowed = req.headers["x-esedre-allowed-projects"];
|
|
3434
|
+
const queryAllowed = url.searchParams.get("allowedProjects");
|
|
3435
|
+
const rawAllowed = (typeof headerAllowed === "string" ? headerAllowed : Array.isArray(headerAllowed) ? headerAllowed[0] : null) || queryAllowed;
|
|
3436
|
+
if (rawAllowed && typeof rawAllowed === "string") {
|
|
3437
|
+
const list = rawAllowed.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3438
|
+
if (list.length > 0 && !list.includes("*")) {
|
|
3439
|
+
const sortedKey = [...list].sort().join(",");
|
|
3440
|
+
return {
|
|
3441
|
+
storage: new SecurityFilter(baseStorage, { allowedProjects: list }),
|
|
3442
|
+
cacheKey: sortedKey
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
return { storage: baseStorage, cacheKey: "*" };
|
|
3283
3447
|
}
|
|
3284
3448
|
function createApiHandler(storage, workspaceRoot) {
|
|
3285
3449
|
return async (req, res) => {
|
|
@@ -3305,6 +3469,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3305
3469
|
if (pathname.startsWith("/esedre/api")) {
|
|
3306
3470
|
pathname = pathname.slice("/esedre".length);
|
|
3307
3471
|
}
|
|
3472
|
+
const { storage: reqStorage, cacheKey } = getRequestStorage(storage, req, url);
|
|
3308
3473
|
try {
|
|
3309
3474
|
if (req.method === "GET") {
|
|
3310
3475
|
if (pathname === "/api/ping") {
|
|
@@ -3312,7 +3477,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3312
3477
|
return true;
|
|
3313
3478
|
}
|
|
3314
3479
|
if (pathname === "/api/planning/projects") {
|
|
3315
|
-
const projects = await
|
|
3480
|
+
const projects = await reqStorage.getProjects();
|
|
3316
3481
|
sendJson(res, 200, projects);
|
|
3317
3482
|
return true;
|
|
3318
3483
|
}
|
|
@@ -3322,7 +3487,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3322
3487
|
const type = url.searchParams.get("type") || url.searchParams.get("category") || void 0;
|
|
3323
3488
|
const category = type;
|
|
3324
3489
|
const search = url.searchParams.get("search") || void 0;
|
|
3325
|
-
const tickets = await
|
|
3490
|
+
const tickets = await reqStorage.listTickets({
|
|
3326
3491
|
project: project === "all" ? void 0 : project,
|
|
3327
3492
|
status,
|
|
3328
3493
|
type,
|
|
@@ -3334,11 +3499,11 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3334
3499
|
}
|
|
3335
3500
|
if (pathname.startsWith("/api/planning/ticket/")) {
|
|
3336
3501
|
const idStr = pathname.slice("/api/planning/ticket/".length);
|
|
3337
|
-
if (!idStr || !/^([a-zA-Z0-9]{1,
|
|
3502
|
+
if (!idStr || !/^([a-zA-Z0-9]{1,8}-)?\d+$/.test(idStr)) {
|
|
3338
3503
|
sendJson(res, 400, { error: "Invalid ticket ID" });
|
|
3339
3504
|
return true;
|
|
3340
3505
|
}
|
|
3341
|
-
const ticket = await
|
|
3506
|
+
const ticket = await reqStorage.getTicket(idStr);
|
|
3342
3507
|
if (!ticket) {
|
|
3343
3508
|
sendJson(res, 400, { error: "Ticket not found" });
|
|
3344
3509
|
return true;
|
|
@@ -3348,11 +3513,12 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3348
3513
|
}
|
|
3349
3514
|
if (pathname === "/api/planning/all") {
|
|
3350
3515
|
const now = Date.now();
|
|
3351
|
-
|
|
3352
|
-
|
|
3516
|
+
const cached = cachedAllData.get(cacheKey);
|
|
3517
|
+
if (cached && now - cached.timestamp < CACHE_TTL_MS) {
|
|
3518
|
+
sendJson(res, 200, cached.data);
|
|
3353
3519
|
return true;
|
|
3354
3520
|
}
|
|
3355
|
-
const tickets = await
|
|
3521
|
+
const tickets = await reqStorage.listTickets({});
|
|
3356
3522
|
const metasMap = {};
|
|
3357
3523
|
const detailsMap = {};
|
|
3358
3524
|
const plansMap = {};
|
|
@@ -3382,7 +3548,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3382
3548
|
}
|
|
3383
3549
|
}
|
|
3384
3550
|
}
|
|
3385
|
-
const projects = await
|
|
3551
|
+
const projects = await reqStorage.getProjects();
|
|
3386
3552
|
const responsePayload = {
|
|
3387
3553
|
success: true,
|
|
3388
3554
|
projects,
|
|
@@ -3395,13 +3561,12 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3395
3561
|
planHistory: {},
|
|
3396
3562
|
ticketHistory: {}
|
|
3397
3563
|
};
|
|
3398
|
-
cachedAllData
|
|
3399
|
-
cachedAllTimestamp = now;
|
|
3564
|
+
cachedAllData.set(cacheKey, { data: responsePayload, timestamp: now });
|
|
3400
3565
|
sendJson(res, 200, responsePayload);
|
|
3401
3566
|
return true;
|
|
3402
3567
|
}
|
|
3403
3568
|
if (pathname === "/api/planning/metas") {
|
|
3404
|
-
const tickets = await
|
|
3569
|
+
const tickets = await reqStorage.listTickets({});
|
|
3405
3570
|
const metasMap = {};
|
|
3406
3571
|
for (const t of tickets) {
|
|
3407
3572
|
metasMap[String(t.meta.id)] = t.meta;
|
|
@@ -3410,7 +3575,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3410
3575
|
return true;
|
|
3411
3576
|
}
|
|
3412
3577
|
if (pathname === "/api/planning/details") {
|
|
3413
|
-
const tickets = await
|
|
3578
|
+
const tickets = await reqStorage.listTickets({});
|
|
3414
3579
|
const detailsMap = {};
|
|
3415
3580
|
for (const t of tickets) {
|
|
3416
3581
|
if (t.detail?.raw) {
|
|
@@ -3421,7 +3586,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3421
3586
|
return true;
|
|
3422
3587
|
}
|
|
3423
3588
|
if (pathname === "/api/planning/plans") {
|
|
3424
|
-
const tickets = await
|
|
3589
|
+
const tickets = await reqStorage.listTickets({});
|
|
3425
3590
|
const plansMap = {};
|
|
3426
3591
|
for (const t of tickets) {
|
|
3427
3592
|
if (t.planMarkdown) {
|
|
@@ -3432,7 +3597,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3432
3597
|
return true;
|
|
3433
3598
|
}
|
|
3434
3599
|
if (pathname === "/api/planning/comments") {
|
|
3435
|
-
const tickets = await
|
|
3600
|
+
const tickets = await reqStorage.listTickets({});
|
|
3436
3601
|
const commentsMap = {};
|
|
3437
3602
|
for (const t of tickets) {
|
|
3438
3603
|
commentsMap[String(t.meta.id)] = t.comments || [];
|
|
@@ -3462,7 +3627,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3462
3627
|
sendJson(res, 400, { error: "Project query parameter is required for snapshot generation (no default fallback)." });
|
|
3463
3628
|
return true;
|
|
3464
3629
|
}
|
|
3465
|
-
const snapshot = await generateProjectSnapshot(
|
|
3630
|
+
const snapshot = await generateProjectSnapshot(reqStorage, projectCode, workspaceRoot);
|
|
3466
3631
|
sendJson(res, 200, snapshot);
|
|
3467
3632
|
return true;
|
|
3468
3633
|
}
|
|
@@ -3471,12 +3636,12 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3471
3636
|
const body = await readJsonBody(req);
|
|
3472
3637
|
if (pathname === "/api/planning/tickets") {
|
|
3473
3638
|
invalidateApiCache();
|
|
3474
|
-
const created = await
|
|
3639
|
+
const created = await reqStorage.createTicket(body);
|
|
3475
3640
|
sendJson(res, 201, { ticketId: String(created.meta.id), meta: created.meta });
|
|
3476
3641
|
return true;
|
|
3477
3642
|
}
|
|
3478
3643
|
if (pathname === "/api/planning/create-project") {
|
|
3479
|
-
const { code, name, description, colors: colors2 } = body;
|
|
3644
|
+
const { code, name, description, colors: colors2, hub } = body;
|
|
3480
3645
|
if (!code || !name) {
|
|
3481
3646
|
sendJson(res, 400, { error: "Both code and name are required to register a project." });
|
|
3482
3647
|
return true;
|
|
@@ -3492,11 +3657,12 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3492
3657
|
return true;
|
|
3493
3658
|
}
|
|
3494
3659
|
invalidateApiCache();
|
|
3495
|
-
const project = await
|
|
3660
|
+
const project = await reqStorage.registerProject({
|
|
3496
3661
|
code,
|
|
3497
3662
|
name,
|
|
3498
3663
|
description,
|
|
3499
|
-
colors: colors2
|
|
3664
|
+
colors: colors2,
|
|
3665
|
+
hub
|
|
3500
3666
|
});
|
|
3501
3667
|
sendJson(res, 201, project);
|
|
3502
3668
|
return true;
|
|
@@ -3504,27 +3670,27 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3504
3670
|
if (pathname === "/api/planning/plans") {
|
|
3505
3671
|
const { ticketId, planMarkdown, lastHash } = body;
|
|
3506
3672
|
invalidateApiCache();
|
|
3507
|
-
await
|
|
3673
|
+
await reqStorage.savePlan(ticketId, planMarkdown, lastHash);
|
|
3508
3674
|
sendJson(res, 200, { success: true });
|
|
3509
3675
|
return true;
|
|
3510
3676
|
}
|
|
3511
3677
|
if (pathname === "/api/planning/comments") {
|
|
3512
3678
|
const { ticketId, text, author } = body;
|
|
3513
3679
|
invalidateApiCache();
|
|
3514
|
-
await
|
|
3515
|
-
const ticket = await
|
|
3680
|
+
await reqStorage.addComment(ticketId, { author: author || "Developer", text });
|
|
3681
|
+
const ticket = await reqStorage.getTicket(ticketId);
|
|
3516
3682
|
sendJson(res, 200, { comments: ticket?.comments || [] });
|
|
3517
3683
|
return true;
|
|
3518
3684
|
}
|
|
3519
3685
|
if (pathname === "/api/planning/update-meta") {
|
|
3520
3686
|
const { ticketId, updates, lastHash } = body;
|
|
3521
|
-
const updated = await
|
|
3687
|
+
const updated = await reqStorage.updateTicket(ticketId, updates, lastHash);
|
|
3522
3688
|
sendJson(res, 200, { meta: updated.meta });
|
|
3523
3689
|
return true;
|
|
3524
3690
|
}
|
|
3525
3691
|
if (pathname === "/api/planning/toggle-flag") {
|
|
3526
3692
|
const { ticketId, flagged } = body;
|
|
3527
|
-
const updated = await
|
|
3693
|
+
const updated = await reqStorage.updateTicket(ticketId, {
|
|
3528
3694
|
featureFlag: flagged ? "chat_enhanced" : ""
|
|
3529
3695
|
});
|
|
3530
3696
|
sendJson(res, 200, { meta: updated.meta });
|
|
@@ -3533,7 +3699,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3533
3699
|
if (pathname === "/api/planning/details") {
|
|
3534
3700
|
const { ticketId, detailMarkdown, metaUpdates } = body;
|
|
3535
3701
|
if (metaUpdates) {
|
|
3536
|
-
await
|
|
3702
|
+
await reqStorage.updateTicket(ticketId, metaUpdates);
|
|
3537
3703
|
}
|
|
3538
3704
|
sendJson(res, 200, { success: true, detail: detailMarkdown });
|
|
3539
3705
|
return true;
|
|
@@ -3542,6 +3708,10 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3542
3708
|
sendJson(res, 404, { error: `Endpoint not found: ${pathname}` });
|
|
3543
3709
|
return true;
|
|
3544
3710
|
} catch (err) {
|
|
3711
|
+
if (err.name === "EsedreAuthorizationError" || err.message?.includes("outside this workspace's authorized scope") || err.message?.includes("Access Denied")) {
|
|
3712
|
+
sendJson(res, 403, { error: err.message });
|
|
3713
|
+
return true;
|
|
3714
|
+
}
|
|
3545
3715
|
console.error("[Esedre API Error]:", err);
|
|
3546
3716
|
sendJson(res, 500, { error: err.message });
|
|
3547
3717
|
return true;
|
|
@@ -3744,12 +3914,14 @@ ${colors.bold}SERVICE DAEMON COMMANDS:${colors.reset}
|
|
|
3744
3914
|
${colors.bold}mcp${colors.reset} Start the Model Context Protocol (MCP) JSON-RPC 2.0 stdio server for autonomous LLM coding agents.
|
|
3745
3915
|
|
|
3746
3916
|
${colors.bold}DEVELOPER ADMINISTRATION (Human Setup & Configuration):${colors.reset}
|
|
3747
|
-
${colors.bold}init${colors.reset} [<path>] [--project <code>] [-n|--name <name>] [--allow <c1,c2>] [--port <n>] [
|
|
3748
|
-
Bring a project repository online (.esedre footprint, wrappers, snapshot)
|
|
3917
|
+
${colors.bold}init${colors.reset} [<path>] [--project <code>] [-n|--name <name>] [--hub <name|path>] [--allow <c1,c2>] [--port <n>] [-y|--yes] [--json]
|
|
3918
|
+
Bring a project repository online (.esedre footprint, wrappers, snapshot), bootstrap a data hub (--hub), or register a project directly into an Esedre ticket data hub (--project <code>).
|
|
3919
|
+
\u2022 In-repo workspace: ese init (creates .esedre/esedre.json in current directory)
|
|
3920
|
+
\u2022 Data hub project: ese init --project <code> --name "<name>" (creates project in configured dataDir hub)
|
|
3749
3921
|
|
|
3750
3922
|
${colors.bold}configure${colors.reset} [add <path> | remove <code|path> | set <key> <val> | get <key>] [--json]
|
|
3751
3923
|
Inspect or mutate central Esedre configuration (~/.esedre/config.json).
|
|
3752
|
-
\u2022 add <path>
|
|
3924
|
+
\u2022 add <path> Link an external project repository (federated projects map) or register a data hub (dataDir)
|
|
3753
3925
|
\u2022 remove <target> Unlink a project or data hub
|
|
3754
3926
|
\u2022 set <key> <val> Update a central configuration setting
|
|
3755
3927
|
|
|
@@ -3829,6 +4001,15 @@ function parseArgs(rawArgs) {
|
|
|
3829
4001
|
if (flags["f"] && !flags["foreground"]) flags["foreground"] = flags["f"];
|
|
3830
4002
|
return { command, positionals, flags };
|
|
3831
4003
|
}
|
|
4004
|
+
function printDuplicateProjectWarnings(storage, isJson) {
|
|
4005
|
+
if (isJson) return;
|
|
4006
|
+
const warnings = storage.getDuplicateProjectWarnings ? storage.getDuplicateProjectWarnings() : [];
|
|
4007
|
+
for (const warn of warnings) {
|
|
4008
|
+
console.error(
|
|
4009
|
+
`${colors.yellow}\u26A0\uFE0F Warning: Duplicate project code '${warn.code}' detected across multiple data hubs (${path9.basename(warn.firstHub)} and ${path9.basename(warn.duplicateHub)}). Duplicate project codes across hubs are not supported; only '${path9.basename(warn.firstHub)}' is active.${colors.reset}`
|
|
4010
|
+
);
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
3832
4013
|
async function main() {
|
|
3833
4014
|
const rawArgs = process.argv.slice(2);
|
|
3834
4015
|
const { command, positionals, flags } = parseArgs(rawArgs);
|
|
@@ -3855,9 +4036,12 @@ async function main() {
|
|
|
3855
4036
|
try {
|
|
3856
4037
|
switch (command) {
|
|
3857
4038
|
case "init": {
|
|
3858
|
-
const
|
|
3859
|
-
const
|
|
3860
|
-
|
|
4039
|
+
const rawTarget = positionals[0];
|
|
4040
|
+
const targetDir = rawTarget ? path9.resolve(rawTarget) : discovered.workspaceRoot;
|
|
4041
|
+
const rawHubFlag = flags["hub"];
|
|
4042
|
+
const hubArg = typeof rawHubFlag === "string" ? rawHubFlag : void 0;
|
|
4043
|
+
const isHubRequested = Boolean(rawHubFlag);
|
|
4044
|
+
if (isHubRequested && !flags["project"] && !flags["p"]) {
|
|
3861
4045
|
const hubRes = initHub(targetDir);
|
|
3862
4046
|
try {
|
|
3863
4047
|
addLocationToGlobalConfig(targetDir);
|
|
@@ -3932,26 +4116,66 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
3932
4116
|
console.error(`${colors.red}Error: ${nameVal.error}${colors.reset}`);
|
|
3933
4117
|
process.exit(1);
|
|
3934
4118
|
}
|
|
3935
|
-
const
|
|
3936
|
-
const
|
|
3937
|
-
(p) => p.code.toLowerCase() === projectCode.toLowerCase()
|
|
3938
|
-
);
|
|
4119
|
+
const isTargetHub = fs9.existsSync(path9.join(targetDir, "projects.json")) || fs9.existsSync(path9.join(targetDir, "projects")) && !fs9.existsSync(path9.join(targetDir, ".esedre", "esedre.json"));
|
|
4120
|
+
const isWorkspaceless = isHubRequested || isTargetHub || Boolean(discovered.config?.projectCode) && !rawTarget && discovered.config?.projectCode?.toLowerCase() !== projectCode.toLowerCase();
|
|
3939
4121
|
let projectNewlyRegistered = false;
|
|
3940
4122
|
let registeredProj;
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
} else {
|
|
3948
|
-
registeredProj = existing;
|
|
3949
|
-
if (flags["name"] || flags["n"]) {
|
|
4123
|
+
try {
|
|
4124
|
+
const existingProjects = await storage.getProjects();
|
|
4125
|
+
const existing = existingProjects.find(
|
|
4126
|
+
(p) => p.code.toLowerCase() === projectCode.toLowerCase()
|
|
4127
|
+
);
|
|
4128
|
+
if (!existing) {
|
|
3950
4129
|
registeredProj = await rawStorage.registerProject({
|
|
3951
4130
|
code: projectCode,
|
|
3952
|
-
name: projectName
|
|
4131
|
+
name: projectName,
|
|
4132
|
+
hub: hubArg
|
|
3953
4133
|
});
|
|
4134
|
+
projectNewlyRegistered = true;
|
|
4135
|
+
} else {
|
|
4136
|
+
registeredProj = existing;
|
|
4137
|
+
if (flags["name"] || flags["n"]) {
|
|
4138
|
+
registeredProj = await rawStorage.registerProject({
|
|
4139
|
+
code: projectCode,
|
|
4140
|
+
name: projectName,
|
|
4141
|
+
hub: hubArg
|
|
4142
|
+
});
|
|
4143
|
+
}
|
|
3954
4144
|
}
|
|
4145
|
+
} catch (err) {
|
|
4146
|
+
console.error(`${colors.red}Error: ${err.message}${colors.reset}`);
|
|
4147
|
+
process.exit(1);
|
|
4148
|
+
}
|
|
4149
|
+
if (isWorkspaceless) {
|
|
4150
|
+
if (isJson) {
|
|
4151
|
+
console.log(JSON.stringify({
|
|
4152
|
+
project: registeredProj,
|
|
4153
|
+
projectNewlyRegistered,
|
|
4154
|
+
registeredInHub: true,
|
|
4155
|
+
commands: {
|
|
4156
|
+
create: `ese create --title "..." --project ${registeredProj.code}`,
|
|
4157
|
+
list: `ese list --project ${registeredProj.code}`
|
|
4158
|
+
}
|
|
4159
|
+
}, null, 2));
|
|
4160
|
+
} else {
|
|
4161
|
+
if (projectNewlyRegistered) {
|
|
4162
|
+
console.log(`${colors.bold}${colors.green}\u2714 Registered project '${registeredProj.code}' (${registeredProj.name}) in Esedre data hub!${colors.reset}`);
|
|
4163
|
+
} else {
|
|
4164
|
+
console.log(`${colors.bold}${colors.green}\u2714 Verified project '${registeredProj.code}' (${registeredProj.name}) in Esedre data hub!${colors.reset}`);
|
|
4165
|
+
}
|
|
4166
|
+
console.log(` \u2022 Code: ${registeredProj.code}`);
|
|
4167
|
+
console.log(` \u2022 Name: ${registeredProj.name}`);
|
|
4168
|
+
if (registeredProj.description) {
|
|
4169
|
+
console.log(` \u2022 Description: ${registeredProj.description}`);
|
|
4170
|
+
}
|
|
4171
|
+
console.log(`
|
|
4172
|
+
${colors.bold}Next Steps:${colors.reset}`);
|
|
4173
|
+
console.log(` 1. Create your first ticket:`);
|
|
4174
|
+
console.log(` ${colors.cyan}ese create --title "First feature" --type Feature --project ${registeredProj.code}${colors.reset}`);
|
|
4175
|
+
console.log(` 2. Inspect roadmap & tickets:`);
|
|
4176
|
+
console.log(` ${colors.cyan}ese list --project ${registeredProj.code}${colors.reset}`);
|
|
4177
|
+
}
|
|
4178
|
+
return;
|
|
3955
4179
|
}
|
|
3956
4180
|
const allowedProjects = allowArg ? allowArg.split(",").map((s) => s.trim()) : discovered.config?.allowedProjects || [projectCode];
|
|
3957
4181
|
let setupProxy = void 0;
|
|
@@ -4186,10 +4410,12 @@ ${colors.bold}Linked Projects (projects):${colors.reset}`);
|
|
|
4186
4410
|
}
|
|
4187
4411
|
console.log(`
|
|
4188
4412
|
${colors.bold}Commands:${colors.reset}`);
|
|
4189
|
-
console.log(` \u2022 Link
|
|
4190
|
-
console.log(` \u2022
|
|
4191
|
-
console.log(` \u2022
|
|
4192
|
-
console.log(` \u2022
|
|
4413
|
+
console.log(` \u2022 Link external repository: ${colors.cyan}ese configure add <repoPath>${colors.reset}`);
|
|
4414
|
+
console.log(` \u2022 Register a data hub: ${colors.cyan}ese configure add <hubPath>${colors.reset}`);
|
|
4415
|
+
console.log(` \u2022 Create hub project: ${colors.cyan}ese init --project <code> --name "<name>"${colors.reset}`);
|
|
4416
|
+
console.log(` \u2022 Unlink a project or hub: ${colors.cyan}ese configure remove <code|path>${colors.reset}`);
|
|
4417
|
+
console.log(` \u2022 Set setting value: ${colors.cyan}ese configure set <key> <value>${colors.reset}`);
|
|
4418
|
+
console.log(` \u2022 Bring new repo online: ${colors.cyan}ese init [path]${colors.reset}`);
|
|
4193
4419
|
return;
|
|
4194
4420
|
}
|
|
4195
4421
|
case "upgrade": {
|
|
@@ -4251,12 +4477,18 @@ ${colors.bold}Commands:${colors.reset}`);
|
|
|
4251
4477
|
const ports = resolvePorts(discovered.config, { port: flagPort });
|
|
4252
4478
|
const port = ports.gateway;
|
|
4253
4479
|
const isForeground = Boolean(flags["foreground"] || flags["f"]);
|
|
4480
|
+
const serverConfig = {
|
|
4481
|
+
...discovered.config,
|
|
4482
|
+
allowedProjects: ["*"]
|
|
4483
|
+
};
|
|
4484
|
+
const serverStorage = new FilesystemStorageAdapter(discovered.workspaceRoot, serverConfig);
|
|
4485
|
+
printDuplicateProjectWarnings(serverStorage, isJson);
|
|
4254
4486
|
if (isForeground) {
|
|
4255
4487
|
const cluster = startGatewayCluster({
|
|
4256
4488
|
gatewayPort: ports.gateway,
|
|
4257
4489
|
uiPort: ports.ui,
|
|
4258
4490
|
apiPort: ports.api,
|
|
4259
|
-
storage,
|
|
4491
|
+
storage: serverStorage,
|
|
4260
4492
|
workspaceRoot: discovered.workspaceRoot
|
|
4261
4493
|
});
|
|
4262
4494
|
console.log(`${colors.bold}${colors.green}\u2714 Esedre Server active on http://localhost:${ports.gateway}${colors.reset}`);
|
|
@@ -4291,6 +4523,7 @@ ${colors.dim}Shutting down Esedre cluster...${colors.reset}`);
|
|
|
4291
4523
|
return;
|
|
4292
4524
|
}
|
|
4293
4525
|
case "status": {
|
|
4526
|
+
printDuplicateProjectWarnings(storage, isJson);
|
|
4294
4527
|
const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
|
|
4295
4528
|
const port = resolvePorts(discovered.config, { port: flagPort }).gateway;
|
|
4296
4529
|
const status = await getDaemonStatus({ port, workspaceRoot: discovered.workspaceRoot });
|
|
@@ -4328,6 +4561,7 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
|
|
|
4328
4561
|
return;
|
|
4329
4562
|
}
|
|
4330
4563
|
case "projects": {
|
|
4564
|
+
printDuplicateProjectWarnings(storage, isJson);
|
|
4331
4565
|
const projects = await storage.getProjects();
|
|
4332
4566
|
if (isJson) {
|
|
4333
4567
|
console.log(JSON.stringify(projects, null, 2));
|
|
@@ -4340,6 +4574,7 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
|
|
|
4340
4574
|
return;
|
|
4341
4575
|
}
|
|
4342
4576
|
case "list": {
|
|
4577
|
+
printDuplicateProjectWarnings(storage, isJson);
|
|
4343
4578
|
const rawProject = flags["project"];
|
|
4344
4579
|
const project = rawProject === "all" ? void 0 : rawProject || discovered.config?.projectCode || void 0;
|
|
4345
4580
|
const status = flags["status"] || void 0;
|