esedre 0.1.7 → 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/README.md
CHANGED
|
@@ -88,6 +88,8 @@ ese start
|
|
|
88
88
|
# Open dashboard: http://localhost:5674/app
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
The server launches with unconstrained portfolio visibility across all registered data hubs and projects, allowing the main Esedre Web UI (as opposed to embedded component and LLM agent project-scoped behavior) to manage them seamlessly. Incoming agent requests enforce project scoping dynamically via the `x-esedre-allowed-projects` header or `?allowedProjects=...` query parameter.
|
|
92
|
+
|
|
91
93
|
### 4. Create Your First Ticket
|
|
92
94
|
|
|
93
95
|
```bash
|
|
@@ -260,6 +262,7 @@ Esedre enforces clean project isolation so each LLM agent is informed only of th
|
|
|
260
262
|
- Unauthorized requests throw `EsedreAuthorizationError`:
|
|
261
263
|
- **CLI**: Prints `Access Denied: ...` and exits with status code 1.
|
|
262
264
|
- **MCP**: Responds with standard JSON-RPC error `-32603`.
|
|
265
|
+
- **REST API**: Responds with HTTP status code `403 Forbidden`.
|
|
263
266
|
|
|
264
267
|
---
|
|
265
268
|
|
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(
|
|
@@ -539,12 +539,12 @@ async function generateProjectSnapshot(storage, projectCode, outputDir) {
|
|
|
539
539
|
}
|
|
540
540
|
|
|
541
541
|
// src/storage/filesystem.ts
|
|
542
|
-
function isProjectMatch(code,
|
|
542
|
+
function isProjectMatch(code, filter) {
|
|
543
|
+
if (!code || !filter) return false;
|
|
543
544
|
const f = filter.trim().toLowerCase();
|
|
544
545
|
const c = code.trim().toLowerCase();
|
|
545
|
-
|
|
546
|
-
if (
|
|
547
|
-
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;
|
|
548
548
|
if ((f === "esedre" || f === "ese" || f === "docs") && (c === "esedre" || c === "docs")) return true;
|
|
549
549
|
if ((f === "alce" || f === "web") && (c === "alce" || c === "web")) return true;
|
|
550
550
|
return false;
|
|
@@ -572,6 +572,9 @@ function writeSafeFile(filePath, content) {
|
|
|
572
572
|
var FilesystemStorageAdapter = class {
|
|
573
573
|
workspaceRoot;
|
|
574
574
|
config;
|
|
575
|
+
isConfigExplicit = false;
|
|
576
|
+
lastConfigCheck = 0;
|
|
577
|
+
configTtlMs = 3e3;
|
|
575
578
|
duplicateProjectWarnings = [];
|
|
576
579
|
getDuplicateProjectWarnings() {
|
|
577
580
|
this.resolveProjectLocations();
|
|
@@ -581,13 +584,39 @@ var FilesystemStorageAdapter = class {
|
|
|
581
584
|
this.workspaceRoot = workspaceRoot || this.resolveWorkspaceRoot();
|
|
582
585
|
if (config) {
|
|
583
586
|
this.config = config;
|
|
587
|
+
this.isConfigExplicit = true;
|
|
588
|
+
this.lastConfigCheck = Date.now();
|
|
584
589
|
} else {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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();
|
|
590
615
|
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (!this.isConfigExplicit && Date.now() - this.lastConfigCheck > this.configTtlMs) {
|
|
619
|
+
this.refreshConfig();
|
|
591
620
|
}
|
|
592
621
|
}
|
|
593
622
|
resolveWorkspaceRoot() {
|
|
@@ -606,6 +635,7 @@ var FilesystemStorageAdapter = class {
|
|
|
606
635
|
return process.cwd();
|
|
607
636
|
}
|
|
608
637
|
resolveProjectLocations() {
|
|
638
|
+
this.ensureFreshConfig();
|
|
609
639
|
const locations = [];
|
|
610
640
|
const seenCodes = /* @__PURE__ */ new Set();
|
|
611
641
|
const codeToSource = /* @__PURE__ */ new Map();
|
|
@@ -649,9 +679,6 @@ var FilesystemStorageAdapter = class {
|
|
|
649
679
|
if (fs3.existsSync(pJsonPath)) {
|
|
650
680
|
try {
|
|
651
681
|
desc = JSON.parse(fs3.readFileSync(pJsonPath, "utf-8"));
|
|
652
|
-
if (desc && !desc.slug) {
|
|
653
|
-
desc.slug = (desc.code || ent.name).toLowerCase();
|
|
654
|
-
}
|
|
655
682
|
} catch {
|
|
656
683
|
}
|
|
657
684
|
}
|
|
@@ -663,7 +690,7 @@ var FilesystemStorageAdapter = class {
|
|
|
663
690
|
try {
|
|
664
691
|
const hubProjs = JSON.parse(fs3.readFileSync(f, "utf-8"));
|
|
665
692
|
desc = hubProjs.find(
|
|
666
|
-
(p) => p.code.toLowerCase() === ent.name.toLowerCase()
|
|
693
|
+
(p) => p.code.toLowerCase() === ent.name.toLowerCase()
|
|
667
694
|
);
|
|
668
695
|
if (desc) break;
|
|
669
696
|
} catch {
|
|
@@ -675,7 +702,6 @@ var FilesystemStorageAdapter = class {
|
|
|
675
702
|
desc = {
|
|
676
703
|
id: locations.length + 1,
|
|
677
704
|
code: ent.name,
|
|
678
|
-
slug: ent.name.toLowerCase(),
|
|
679
705
|
name: ent.name,
|
|
680
706
|
description: `${ent.name} project`
|
|
681
707
|
};
|
|
@@ -727,7 +753,6 @@ var FilesystemStorageAdapter = class {
|
|
|
727
753
|
descs = [{
|
|
728
754
|
id: 1,
|
|
729
755
|
code: pCode,
|
|
730
|
-
slug: pCode.toLowerCase(),
|
|
731
756
|
name: pCode,
|
|
732
757
|
description: `${pCode} project`
|
|
733
758
|
}];
|
|
@@ -782,9 +807,6 @@ var FilesystemStorageAdapter = class {
|
|
|
782
807
|
if (fs3.existsSync(cand)) {
|
|
783
808
|
try {
|
|
784
809
|
desc = JSON.parse(fs3.readFileSync(cand, "utf-8"));
|
|
785
|
-
if (desc && !desc.slug) {
|
|
786
|
-
desc.slug = (desc.code || code).toLowerCase();
|
|
787
|
-
}
|
|
788
810
|
break;
|
|
789
811
|
} catch {
|
|
790
812
|
}
|
|
@@ -794,7 +816,6 @@ var FilesystemStorageAdapter = class {
|
|
|
794
816
|
desc = {
|
|
795
817
|
id: locations.length + 1,
|
|
796
818
|
code,
|
|
797
|
-
slug: code.toLowerCase(),
|
|
798
819
|
name: code,
|
|
799
820
|
description: `${code} project`
|
|
800
821
|
};
|
|
@@ -862,7 +883,6 @@ var FilesystemStorageAdapter = class {
|
|
|
862
883
|
descs = [{
|
|
863
884
|
id: 1,
|
|
864
885
|
code: this.config.projectCode,
|
|
865
|
-
slug: this.config.projectCode.toLowerCase(),
|
|
866
886
|
name: pName,
|
|
867
887
|
description: `${pName} project`
|
|
868
888
|
}];
|
|
@@ -894,7 +914,7 @@ var FilesystemStorageAdapter = class {
|
|
|
894
914
|
const cleanCode = rawCode;
|
|
895
915
|
const cleanName = input.name?.trim() || cleanCode;
|
|
896
916
|
const cleanDesc = input.description?.trim() || `${cleanName} project`;
|
|
897
|
-
const
|
|
917
|
+
const cleanLower = cleanCode.toLowerCase();
|
|
898
918
|
const colors2 = input.colors || {
|
|
899
919
|
badge: "border-cyan-500/30 bg-cyan-500/10 text-cyan-300",
|
|
900
920
|
dot: "bg-cyan-400",
|
|
@@ -978,7 +998,7 @@ ${formattedList}`
|
|
|
978
998
|
const normalizedTargetHub = path3.resolve(targetHub).toLowerCase();
|
|
979
999
|
const existingLocations = this.resolveProjectLocations();
|
|
980
1000
|
const existingConflict = existingLocations.find((l) => {
|
|
981
|
-
if (l.project.code.toLowerCase() !==
|
|
1001
|
+
if (l.project.code.toLowerCase() !== cleanLower) return false;
|
|
982
1002
|
if (l.sourceType === "hub" && l.hubDir) {
|
|
983
1003
|
return path3.resolve(l.hubDir).toLowerCase() !== normalizedTargetHub;
|
|
984
1004
|
}
|
|
@@ -996,7 +1016,7 @@ ${formattedList}`
|
|
|
996
1016
|
try {
|
|
997
1017
|
const entries = fs3.readdirSync(hubProjectsDir, { withFileTypes: true });
|
|
998
1018
|
const matchedDir = entries.find(
|
|
999
|
-
(e) => e.isDirectory() && e.name.toLowerCase() ===
|
|
1019
|
+
(e) => e.isDirectory() && e.name.toLowerCase() === cleanLower
|
|
1000
1020
|
);
|
|
1001
1021
|
if (matchedDir) {
|
|
1002
1022
|
resolvedDirName = matchedDir.name;
|
|
@@ -1024,7 +1044,6 @@ ${formattedList}`
|
|
|
1024
1044
|
const projectDesc2 = {
|
|
1025
1045
|
id: id2,
|
|
1026
1046
|
code: resolvedCode2,
|
|
1027
|
-
slug,
|
|
1028
1047
|
name: cleanName,
|
|
1029
1048
|
description: cleanDesc,
|
|
1030
1049
|
colors: colors2
|
|
@@ -1034,7 +1053,7 @@ ${formattedList}`
|
|
|
1034
1053
|
if (fs3.existsSync(hubProjectsFile)) {
|
|
1035
1054
|
try {
|
|
1036
1055
|
const arr = JSON.parse(fs3.readFileSync(hubProjectsFile, "utf-8"));
|
|
1037
|
-
const idx = arr.findIndex((p) => p.code.toLowerCase() ===
|
|
1056
|
+
const idx = arr.findIndex((p) => p.code.toLowerCase() === cleanLower);
|
|
1038
1057
|
if (idx >= 0) {
|
|
1039
1058
|
arr[idx] = projectDesc2;
|
|
1040
1059
|
} else {
|
|
@@ -1059,12 +1078,11 @@ ${formattedList}`
|
|
|
1059
1078
|
} catch {
|
|
1060
1079
|
}
|
|
1061
1080
|
}
|
|
1062
|
-
const resolvedCode = existingPJson?.code && existingPJson.code.toLowerCase() ===
|
|
1081
|
+
const resolvedCode = existingPJson?.code && existingPJson.code.toLowerCase() === cleanLower ? existingPJson.code : cleanCode;
|
|
1063
1082
|
const id = existingPJson?.id || 1;
|
|
1064
1083
|
const projectDesc = {
|
|
1065
1084
|
id,
|
|
1066
1085
|
code: resolvedCode,
|
|
1067
|
-
slug,
|
|
1068
1086
|
name: cleanName,
|
|
1069
1087
|
description: cleanDesc,
|
|
1070
1088
|
colors: colors2
|
|
@@ -1074,7 +1092,7 @@ ${formattedList}`
|
|
|
1074
1092
|
if (fs3.existsSync(projectsJsonPath)) {
|
|
1075
1093
|
try {
|
|
1076
1094
|
const arr = JSON.parse(fs3.readFileSync(projectsJsonPath, "utf-8"));
|
|
1077
|
-
const idx = arr.findIndex((p) => p.code.toLowerCase() ===
|
|
1095
|
+
const idx = arr.findIndex((p) => p.code.toLowerCase() === cleanLower);
|
|
1078
1096
|
if (idx >= 0) {
|
|
1079
1097
|
arr[idx] = projectDesc;
|
|
1080
1098
|
} else {
|
|
@@ -1094,7 +1112,7 @@ ${formattedList}`
|
|
|
1094
1112
|
if (prefixMatch) {
|
|
1095
1113
|
const code = prefixMatch[1].toLowerCase();
|
|
1096
1114
|
const num2 = parseInt(prefixMatch[2], 10);
|
|
1097
|
-
const loc = locations.find((l) => isProjectMatch(l.project.code,
|
|
1115
|
+
const loc = locations.find((l) => isProjectMatch(l.project.code, code));
|
|
1098
1116
|
if (!loc) return null;
|
|
1099
1117
|
const ticketDir = path3.join(loc.ticketsDir, String(num2));
|
|
1100
1118
|
const metaPath = path3.join(ticketDir, "meta.json");
|
|
@@ -1106,7 +1124,7 @@ ${formattedList}`
|
|
|
1106
1124
|
meta.type = meta.type || meta.category || "Feature";
|
|
1107
1125
|
meta.category = meta.type;
|
|
1108
1126
|
const mProj = (meta.project || "").toLowerCase();
|
|
1109
|
-
if (mProj && mProj !== code && mProj !== loc.project.
|
|
1127
|
+
if (mProj && mProj !== code && mProj !== loc.project.code.toLowerCase()) {
|
|
1110
1128
|
return null;
|
|
1111
1129
|
}
|
|
1112
1130
|
} catch {
|
|
@@ -1131,7 +1149,7 @@ ${formattedList}`
|
|
|
1131
1149
|
const metaRaw = fs3.readFileSync(metaPath, "utf-8");
|
|
1132
1150
|
const meta = JSON.parse(metaRaw);
|
|
1133
1151
|
const matchedLoc = locations.find(
|
|
1134
|
-
(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()
|
|
1135
1153
|
) || loc;
|
|
1136
1154
|
matches.push({ loc: matchedLoc, ticketDir, id: num });
|
|
1137
1155
|
} catch {
|
|
@@ -1144,7 +1162,7 @@ ${formattedList}`
|
|
|
1144
1162
|
if (this.config.projectCode) {
|
|
1145
1163
|
const prefLower = this.config.projectCode.toLowerCase();
|
|
1146
1164
|
const preferredMatch = matches.find(
|
|
1147
|
-
(m) => m.loc.project.code.toLowerCase() === prefLower
|
|
1165
|
+
(m) => m.loc.project.code.toLowerCase() === prefLower
|
|
1148
1166
|
);
|
|
1149
1167
|
if (preferredMatch) return preferredMatch;
|
|
1150
1168
|
}
|
|
@@ -1166,7 +1184,7 @@ ${formattedList}`
|
|
|
1166
1184
|
for (const loc of locations) {
|
|
1167
1185
|
if (filter?.project && filter.project !== "all") {
|
|
1168
1186
|
const pLower = filter.project.toLowerCase();
|
|
1169
|
-
if (!isProjectMatch(loc.project.code,
|
|
1187
|
+
if (!isProjectMatch(loc.project.code, filter.project)) {
|
|
1170
1188
|
continue;
|
|
1171
1189
|
}
|
|
1172
1190
|
}
|
|
@@ -1198,7 +1216,7 @@ ${formattedList}`
|
|
|
1198
1216
|
let effectiveLoc = loc;
|
|
1199
1217
|
if (loc.isSharedDir) {
|
|
1200
1218
|
const found = locations.find(
|
|
1201
|
-
(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()
|
|
1202
1220
|
);
|
|
1203
1221
|
if (found) effectiveLoc = found;
|
|
1204
1222
|
}
|
|
@@ -1206,7 +1224,7 @@ ${formattedList}`
|
|
|
1206
1224
|
meta.projectId = effectiveLoc.project.id;
|
|
1207
1225
|
if (filter?.project && filter.project !== "all") {
|
|
1208
1226
|
const pLower = filter.project.toLowerCase();
|
|
1209
|
-
if (!isProjectMatch(meta.project,
|
|
1227
|
+
if (!isProjectMatch(meta.project, filter.project)) {
|
|
1210
1228
|
continue;
|
|
1211
1229
|
}
|
|
1212
1230
|
}
|
|
@@ -1373,7 +1391,7 @@ ${formattedList}`
|
|
|
1373
1391
|
if (requestedIdentifier) {
|
|
1374
1392
|
const lower = String(requestedIdentifier).trim().toLowerCase();
|
|
1375
1393
|
targetLoc = locations.find(
|
|
1376
|
-
(l) => l.project.code?.toLowerCase() === lower ||
|
|
1394
|
+
(l) => l.project.code?.toLowerCase() === lower || String(l.project.id) === lower || l.project.name?.toLowerCase() === lower
|
|
1377
1395
|
);
|
|
1378
1396
|
if (!targetLoc) {
|
|
1379
1397
|
throw new Error(`Project '${requestedIdentifier}' is invalid or not registered in this workspace.`);
|
|
@@ -2579,11 +2597,13 @@ async function startDaemon(options = {}) {
|
|
|
2579
2597
|
if (!pid) {
|
|
2580
2598
|
throw new Error("Failed to spawn Esedre daemon background process.");
|
|
2581
2599
|
}
|
|
2600
|
+
const timeoutMs = process.platform === "win32" ? 1e4 : 5e3;
|
|
2601
|
+
const pingTimeout = process.platform === "win32" ? 500 : 300;
|
|
2582
2602
|
const startTime = Date.now();
|
|
2583
2603
|
let ready = false;
|
|
2584
|
-
while (Date.now() - startTime <
|
|
2585
|
-
await new Promise((r) => setTimeout(r,
|
|
2586
|
-
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);
|
|
2587
2607
|
if (ping.responding && ping.isEsedre) {
|
|
2588
2608
|
ready = true;
|
|
2589
2609
|
break;
|
|
@@ -3228,7 +3248,6 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
3228
3248
|
const projDesc = {
|
|
3229
3249
|
id: 1,
|
|
3230
3250
|
code: projectCode.toUpperCase(),
|
|
3231
|
-
slug: projectCode.toLowerCase(),
|
|
3232
3251
|
name: projectName || projectCode.toUpperCase(),
|
|
3233
3252
|
description: `${projectName || projectCode.toUpperCase()} project`,
|
|
3234
3253
|
colors: {
|
|
@@ -3401,16 +3420,30 @@ function sendJson(res, status, data) {
|
|
|
3401
3420
|
"Content-Type": "application/json; charset=utf-8",
|
|
3402
3421
|
"Access-Control-Allow-Origin": "*",
|
|
3403
3422
|
"Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
|
|
3404
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
3423
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, x-esedre-allowed-projects"
|
|
3405
3424
|
});
|
|
3406
3425
|
res.end(JSON.stringify(data));
|
|
3407
3426
|
}
|
|
3408
|
-
var cachedAllData =
|
|
3409
|
-
var cachedAllTimestamp = 0;
|
|
3427
|
+
var cachedAllData = /* @__PURE__ */ new Map();
|
|
3410
3428
|
var CACHE_TTL_MS = 5e3;
|
|
3411
3429
|
function invalidateApiCache() {
|
|
3412
|
-
cachedAllData
|
|
3413
|
-
|
|
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: "*" };
|
|
3414
3447
|
}
|
|
3415
3448
|
function createApiHandler(storage, workspaceRoot) {
|
|
3416
3449
|
return async (req, res) => {
|
|
@@ -3436,6 +3469,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3436
3469
|
if (pathname.startsWith("/esedre/api")) {
|
|
3437
3470
|
pathname = pathname.slice("/esedre".length);
|
|
3438
3471
|
}
|
|
3472
|
+
const { storage: reqStorage, cacheKey } = getRequestStorage(storage, req, url);
|
|
3439
3473
|
try {
|
|
3440
3474
|
if (req.method === "GET") {
|
|
3441
3475
|
if (pathname === "/api/ping") {
|
|
@@ -3443,7 +3477,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3443
3477
|
return true;
|
|
3444
3478
|
}
|
|
3445
3479
|
if (pathname === "/api/planning/projects") {
|
|
3446
|
-
const projects = await
|
|
3480
|
+
const projects = await reqStorage.getProjects();
|
|
3447
3481
|
sendJson(res, 200, projects);
|
|
3448
3482
|
return true;
|
|
3449
3483
|
}
|
|
@@ -3453,7 +3487,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3453
3487
|
const type = url.searchParams.get("type") || url.searchParams.get("category") || void 0;
|
|
3454
3488
|
const category = type;
|
|
3455
3489
|
const search = url.searchParams.get("search") || void 0;
|
|
3456
|
-
const tickets = await
|
|
3490
|
+
const tickets = await reqStorage.listTickets({
|
|
3457
3491
|
project: project === "all" ? void 0 : project,
|
|
3458
3492
|
status,
|
|
3459
3493
|
type,
|
|
@@ -3469,7 +3503,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3469
3503
|
sendJson(res, 400, { error: "Invalid ticket ID" });
|
|
3470
3504
|
return true;
|
|
3471
3505
|
}
|
|
3472
|
-
const ticket = await
|
|
3506
|
+
const ticket = await reqStorage.getTicket(idStr);
|
|
3473
3507
|
if (!ticket) {
|
|
3474
3508
|
sendJson(res, 400, { error: "Ticket not found" });
|
|
3475
3509
|
return true;
|
|
@@ -3479,11 +3513,12 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3479
3513
|
}
|
|
3480
3514
|
if (pathname === "/api/planning/all") {
|
|
3481
3515
|
const now = Date.now();
|
|
3482
|
-
|
|
3483
|
-
|
|
3516
|
+
const cached = cachedAllData.get(cacheKey);
|
|
3517
|
+
if (cached && now - cached.timestamp < CACHE_TTL_MS) {
|
|
3518
|
+
sendJson(res, 200, cached.data);
|
|
3484
3519
|
return true;
|
|
3485
3520
|
}
|
|
3486
|
-
const tickets = await
|
|
3521
|
+
const tickets = await reqStorage.listTickets({});
|
|
3487
3522
|
const metasMap = {};
|
|
3488
3523
|
const detailsMap = {};
|
|
3489
3524
|
const plansMap = {};
|
|
@@ -3513,7 +3548,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3513
3548
|
}
|
|
3514
3549
|
}
|
|
3515
3550
|
}
|
|
3516
|
-
const projects = await
|
|
3551
|
+
const projects = await reqStorage.getProjects();
|
|
3517
3552
|
const responsePayload = {
|
|
3518
3553
|
success: true,
|
|
3519
3554
|
projects,
|
|
@@ -3526,13 +3561,12 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3526
3561
|
planHistory: {},
|
|
3527
3562
|
ticketHistory: {}
|
|
3528
3563
|
};
|
|
3529
|
-
cachedAllData
|
|
3530
|
-
cachedAllTimestamp = now;
|
|
3564
|
+
cachedAllData.set(cacheKey, { data: responsePayload, timestamp: now });
|
|
3531
3565
|
sendJson(res, 200, responsePayload);
|
|
3532
3566
|
return true;
|
|
3533
3567
|
}
|
|
3534
3568
|
if (pathname === "/api/planning/metas") {
|
|
3535
|
-
const tickets = await
|
|
3569
|
+
const tickets = await reqStorage.listTickets({});
|
|
3536
3570
|
const metasMap = {};
|
|
3537
3571
|
for (const t of tickets) {
|
|
3538
3572
|
metasMap[String(t.meta.id)] = t.meta;
|
|
@@ -3541,7 +3575,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3541
3575
|
return true;
|
|
3542
3576
|
}
|
|
3543
3577
|
if (pathname === "/api/planning/details") {
|
|
3544
|
-
const tickets = await
|
|
3578
|
+
const tickets = await reqStorage.listTickets({});
|
|
3545
3579
|
const detailsMap = {};
|
|
3546
3580
|
for (const t of tickets) {
|
|
3547
3581
|
if (t.detail?.raw) {
|
|
@@ -3552,7 +3586,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3552
3586
|
return true;
|
|
3553
3587
|
}
|
|
3554
3588
|
if (pathname === "/api/planning/plans") {
|
|
3555
|
-
const tickets = await
|
|
3589
|
+
const tickets = await reqStorage.listTickets({});
|
|
3556
3590
|
const plansMap = {};
|
|
3557
3591
|
for (const t of tickets) {
|
|
3558
3592
|
if (t.planMarkdown) {
|
|
@@ -3563,7 +3597,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3563
3597
|
return true;
|
|
3564
3598
|
}
|
|
3565
3599
|
if (pathname === "/api/planning/comments") {
|
|
3566
|
-
const tickets = await
|
|
3600
|
+
const tickets = await reqStorage.listTickets({});
|
|
3567
3601
|
const commentsMap = {};
|
|
3568
3602
|
for (const t of tickets) {
|
|
3569
3603
|
commentsMap[String(t.meta.id)] = t.comments || [];
|
|
@@ -3593,7 +3627,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3593
3627
|
sendJson(res, 400, { error: "Project query parameter is required for snapshot generation (no default fallback)." });
|
|
3594
3628
|
return true;
|
|
3595
3629
|
}
|
|
3596
|
-
const snapshot = await generateProjectSnapshot(
|
|
3630
|
+
const snapshot = await generateProjectSnapshot(reqStorage, projectCode, workspaceRoot);
|
|
3597
3631
|
sendJson(res, 200, snapshot);
|
|
3598
3632
|
return true;
|
|
3599
3633
|
}
|
|
@@ -3602,7 +3636,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3602
3636
|
const body = await readJsonBody(req);
|
|
3603
3637
|
if (pathname === "/api/planning/tickets") {
|
|
3604
3638
|
invalidateApiCache();
|
|
3605
|
-
const created = await
|
|
3639
|
+
const created = await reqStorage.createTicket(body);
|
|
3606
3640
|
sendJson(res, 201, { ticketId: String(created.meta.id), meta: created.meta });
|
|
3607
3641
|
return true;
|
|
3608
3642
|
}
|
|
@@ -3623,7 +3657,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3623
3657
|
return true;
|
|
3624
3658
|
}
|
|
3625
3659
|
invalidateApiCache();
|
|
3626
|
-
const project = await
|
|
3660
|
+
const project = await reqStorage.registerProject({
|
|
3627
3661
|
code,
|
|
3628
3662
|
name,
|
|
3629
3663
|
description,
|
|
@@ -3636,27 +3670,27 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3636
3670
|
if (pathname === "/api/planning/plans") {
|
|
3637
3671
|
const { ticketId, planMarkdown, lastHash } = body;
|
|
3638
3672
|
invalidateApiCache();
|
|
3639
|
-
await
|
|
3673
|
+
await reqStorage.savePlan(ticketId, planMarkdown, lastHash);
|
|
3640
3674
|
sendJson(res, 200, { success: true });
|
|
3641
3675
|
return true;
|
|
3642
3676
|
}
|
|
3643
3677
|
if (pathname === "/api/planning/comments") {
|
|
3644
3678
|
const { ticketId, text, author } = body;
|
|
3645
3679
|
invalidateApiCache();
|
|
3646
|
-
await
|
|
3647
|
-
const ticket = await
|
|
3680
|
+
await reqStorage.addComment(ticketId, { author: author || "Developer", text });
|
|
3681
|
+
const ticket = await reqStorage.getTicket(ticketId);
|
|
3648
3682
|
sendJson(res, 200, { comments: ticket?.comments || [] });
|
|
3649
3683
|
return true;
|
|
3650
3684
|
}
|
|
3651
3685
|
if (pathname === "/api/planning/update-meta") {
|
|
3652
3686
|
const { ticketId, updates, lastHash } = body;
|
|
3653
|
-
const updated = await
|
|
3687
|
+
const updated = await reqStorage.updateTicket(ticketId, updates, lastHash);
|
|
3654
3688
|
sendJson(res, 200, { meta: updated.meta });
|
|
3655
3689
|
return true;
|
|
3656
3690
|
}
|
|
3657
3691
|
if (pathname === "/api/planning/toggle-flag") {
|
|
3658
3692
|
const { ticketId, flagged } = body;
|
|
3659
|
-
const updated = await
|
|
3693
|
+
const updated = await reqStorage.updateTicket(ticketId, {
|
|
3660
3694
|
featureFlag: flagged ? "chat_enhanced" : ""
|
|
3661
3695
|
});
|
|
3662
3696
|
sendJson(res, 200, { meta: updated.meta });
|
|
@@ -3665,7 +3699,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3665
3699
|
if (pathname === "/api/planning/details") {
|
|
3666
3700
|
const { ticketId, detailMarkdown, metaUpdates } = body;
|
|
3667
3701
|
if (metaUpdates) {
|
|
3668
|
-
await
|
|
3702
|
+
await reqStorage.updateTicket(ticketId, metaUpdates);
|
|
3669
3703
|
}
|
|
3670
3704
|
sendJson(res, 200, { success: true, detail: detailMarkdown });
|
|
3671
3705
|
return true;
|
|
@@ -3674,6 +3708,10 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3674
3708
|
sendJson(res, 404, { error: `Endpoint not found: ${pathname}` });
|
|
3675
3709
|
return true;
|
|
3676
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
|
+
}
|
|
3677
3715
|
console.error("[Esedre API Error]:", err);
|
|
3678
3716
|
sendJson(res, 500, { error: err.message });
|
|
3679
3717
|
return true;
|
|
@@ -4435,17 +4473,22 @@ ${colors.bold}Commands:${colors.reset}`);
|
|
|
4435
4473
|
return;
|
|
4436
4474
|
}
|
|
4437
4475
|
case "start": {
|
|
4438
|
-
printDuplicateProjectWarnings(storage, isJson);
|
|
4439
4476
|
const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
|
|
4440
4477
|
const ports = resolvePorts(discovered.config, { port: flagPort });
|
|
4441
4478
|
const port = ports.gateway;
|
|
4442
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);
|
|
4443
4486
|
if (isForeground) {
|
|
4444
4487
|
const cluster = startGatewayCluster({
|
|
4445
4488
|
gatewayPort: ports.gateway,
|
|
4446
4489
|
uiPort: ports.ui,
|
|
4447
4490
|
apiPort: ports.api,
|
|
4448
|
-
storage,
|
|
4491
|
+
storage: serverStorage,
|
|
4449
4492
|
workspaceRoot: discovered.workspaceRoot
|
|
4450
4493
|
});
|
|
4451
4494
|
console.log(`${colors.bold}${colors.green}\u2714 Esedre Server active on http://localhost:${ports.gateway}${colors.reset}`);
|