esedre 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +11 -2
- package/dist/esedre.mjs +1132 -317
- package/dist/web/assets/index-CqLRSVQd.js +117 -0
- package/dist/web/assets/index-hrxboO8S.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -3
- package/dist/web/assets/index-DDWeUKW-.css +0 -2
- package/dist/web/assets/index-DjaZk7Yp.js +0 -117
package/dist/esedre.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/esedre.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs10 from "node:fs";
|
|
5
|
+
import path10 from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/storage/filesystem.ts
|
|
8
8
|
import fs3 from "node:fs";
|
|
@@ -10,7 +10,7 @@ import path3 from "node:path";
|
|
|
10
10
|
import os2 from "node:os";
|
|
11
11
|
|
|
12
12
|
// src/types.ts
|
|
13
|
-
var CURRENT_ESEDRE_VERSION = "1.0
|
|
13
|
+
var CURRENT_ESEDRE_VERSION = "1.1.0";
|
|
14
14
|
var INVERSE_RELATIONS = {
|
|
15
15
|
"relates-to": "relates-to",
|
|
16
16
|
"blocks": "blocked-by",
|
|
@@ -112,6 +112,16 @@ function getGlobalConfigDir() {
|
|
|
112
112
|
if (process.env.ESEDRE_GLOBAL_DIR) {
|
|
113
113
|
return process.env.ESEDRE_GLOBAL_DIR;
|
|
114
114
|
}
|
|
115
|
+
if (process.env.VITEST) {
|
|
116
|
+
const testGlobalDir = path.join(os.tmpdir(), "esedre-vitest-global");
|
|
117
|
+
if (!fs.existsSync(testGlobalDir)) {
|
|
118
|
+
try {
|
|
119
|
+
fs.mkdirSync(testGlobalDir, { recursive: true });
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return testGlobalDir;
|
|
124
|
+
}
|
|
115
125
|
return path.join(os.homedir(), ".esedre");
|
|
116
126
|
}
|
|
117
127
|
function getGlobalConfigPath() {
|
|
@@ -158,6 +168,35 @@ function writeGlobalVersion(version = CURRENT_ESEDRE_VERSION) {
|
|
|
158
168
|
}
|
|
159
169
|
fs.writeFileSync(getGlobalVersionPath(), version.trim() + "\n", "utf-8");
|
|
160
170
|
}
|
|
171
|
+
function hubContainsProject(hubPath, projectCodes) {
|
|
172
|
+
if (!hubPath || !projectCodes || projectCodes.length === 0) return false;
|
|
173
|
+
const resolved = path.resolve(expandHome(hubPath));
|
|
174
|
+
const projectsDir = path.join(resolved, "projects");
|
|
175
|
+
if (fs.existsSync(projectsDir)) {
|
|
176
|
+
try {
|
|
177
|
+
const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
|
|
178
|
+
const dirNames = entries.filter((e) => e.isDirectory()).map((e) => e.name.toLowerCase());
|
|
179
|
+
if (projectCodes.some((code) => dirNames.includes(code.trim().toLowerCase()))) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const projectsJson = path.join(resolved, "projects.json");
|
|
186
|
+
if (fs.existsSync(projectsJson)) {
|
|
187
|
+
try {
|
|
188
|
+
const parsed = JSON.parse(fs.readFileSync(projectsJson, "utf-8"));
|
|
189
|
+
if (Array.isArray(parsed)) {
|
|
190
|
+
const pCodes = parsed.map((p) => (p.code || "").trim().toLowerCase());
|
|
191
|
+
if (projectCodes.some((code) => pCodes.includes(code.trim().toLowerCase()))) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
161
200
|
function addLocationToGlobalConfig(targetPath) {
|
|
162
201
|
const resolved = path.resolve(expandHome(targetPath));
|
|
163
202
|
if (!fs.existsSync(resolved)) {
|
|
@@ -337,8 +376,21 @@ function findEsedreConfig(startDir, options) {
|
|
|
337
376
|
}
|
|
338
377
|
const globalConfig = readGlobalConfig();
|
|
339
378
|
if (localConfig) {
|
|
379
|
+
let inheritedDataDir = localConfig.dataDir;
|
|
380
|
+
if (inheritedDataDir === void 0 && globalConfig?.dataDir) {
|
|
381
|
+
const targetCodes = [
|
|
382
|
+
...localConfig.projectCode ? [localConfig.projectCode] : [],
|
|
383
|
+
...Array.isArray(localConfig.allowedProjects) ? localConfig.allowedProjects : []
|
|
384
|
+
].filter((c) => c && c !== "*");
|
|
385
|
+
const globalDirs = Array.isArray(globalConfig.dataDir) ? globalConfig.dataDir : [globalConfig.dataDir];
|
|
386
|
+
const matchesGlobalHub = globalDirs.some((d) => hubContainsProject(d, targetCodes));
|
|
387
|
+
if (matchesGlobalHub) {
|
|
388
|
+
inheritedDataDir = globalConfig.dataDir;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
340
391
|
const merged = {
|
|
341
392
|
...localConfig,
|
|
393
|
+
dataDir: inheritedDataDir,
|
|
342
394
|
port: localConfig.port !== void 0 ? localConfig.port : globalConfig?.port || DEFAULT_ESEDRE_PORT,
|
|
343
395
|
ports: localConfig.ports || globalConfig?.ports
|
|
344
396
|
};
|
|
@@ -655,6 +707,18 @@ function normalizeTicketFields(ticket) {
|
|
|
655
707
|
if (c.author) c.author = normalizeDashesAndMojibake(c.author);
|
|
656
708
|
}
|
|
657
709
|
}
|
|
710
|
+
if (ticket.answers) {
|
|
711
|
+
for (const key of Object.keys(ticket.answers)) {
|
|
712
|
+
ticket.answers[key] = normalizeDashesAndMojibake(ticket.answers[key]);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (ticket.inlineComments) {
|
|
716
|
+
for (const ic of ticket.inlineComments) {
|
|
717
|
+
if (ic.selectedText) ic.selectedText = normalizeDashesAndMojibake(ic.selectedText);
|
|
718
|
+
if (ic.comment) ic.comment = normalizeDashesAndMojibake(ic.comment);
|
|
719
|
+
if (ic.author) ic.author = normalizeDashesAndMojibake(ic.author);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
658
722
|
return ticket;
|
|
659
723
|
}
|
|
660
724
|
function formatTicketListTable(tickets) {
|
|
@@ -759,9 +823,12 @@ function formatTicketDetail(ticket) {
|
|
|
759
823
|
if (detail?.openQuestions && detail.openQuestions.length > 0) {
|
|
760
824
|
lines.push("");
|
|
761
825
|
lines.push(`${colors.bold}Open Decisions & Questions:${colors.reset}`);
|
|
762
|
-
|
|
826
|
+
detail.openQuestions.forEach((q, idx) => {
|
|
763
827
|
lines.push(` \u2022 ${normalizeDashesAndMojibake(q)}`);
|
|
764
|
-
|
|
828
|
+
if (ticket.answers && ticket.answers[String(idx)]) {
|
|
829
|
+
lines.push(` ${colors.green}> Answer:${colors.reset} ${normalizeDashesAndMojibake(ticket.answers[String(idx)])}`);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
765
832
|
}
|
|
766
833
|
if (ticket.links && ticket.links.length > 0) {
|
|
767
834
|
lines.push("");
|
|
@@ -883,13 +950,23 @@ function writeSafeFile(filePath, content) {
|
|
|
883
950
|
if (!fs3.existsSync(dir)) {
|
|
884
951
|
fs3.mkdirSync(dir, { recursive: true });
|
|
885
952
|
}
|
|
886
|
-
const normalized = content.replace(/\n/g, "\n");
|
|
887
953
|
const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
888
954
|
try {
|
|
889
|
-
|
|
890
|
-
|
|
955
|
+
if (Buffer.isBuffer(content)) {
|
|
956
|
+
fs3.writeFileSync(tempPath, content);
|
|
957
|
+
fs3.renameSync(tempPath, filePath);
|
|
958
|
+
} else {
|
|
959
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
960
|
+
fs3.writeFileSync(tempPath, normalized, "utf-8");
|
|
961
|
+
fs3.renameSync(tempPath, filePath);
|
|
962
|
+
}
|
|
891
963
|
} catch {
|
|
892
|
-
|
|
964
|
+
if (Buffer.isBuffer(content)) {
|
|
965
|
+
fs3.writeFileSync(filePath, content);
|
|
966
|
+
} else {
|
|
967
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
968
|
+
fs3.writeFileSync(filePath, normalized, "utf-8");
|
|
969
|
+
}
|
|
893
970
|
if (fs3.existsSync(tempPath)) {
|
|
894
971
|
try {
|
|
895
972
|
fs3.unlinkSync(tempPath);
|
|
@@ -905,6 +982,23 @@ var FilesystemStorageAdapter = class {
|
|
|
905
982
|
lastConfigCheck = 0;
|
|
906
983
|
configTtlMs = 3e3;
|
|
907
984
|
duplicateProjectWarnings = [];
|
|
985
|
+
milestoneMemoryCache = /* @__PURE__ */ new Map();
|
|
986
|
+
ticketMemoryCache = /* @__PURE__ */ new Map();
|
|
987
|
+
invalidateTicketCache(ticketDir) {
|
|
988
|
+
if (ticketDir) {
|
|
989
|
+
this.ticketMemoryCache.delete(path3.resolve(ticketDir));
|
|
990
|
+
} else {
|
|
991
|
+
this.ticketMemoryCache.clear();
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
invalidateMilestoneCache(loc) {
|
|
995
|
+
if (loc) {
|
|
996
|
+
const mPath = path3.resolve(this.getMilestonesPath(loc));
|
|
997
|
+
this.milestoneMemoryCache.delete(mPath);
|
|
998
|
+
} else {
|
|
999
|
+
this.milestoneMemoryCache.clear();
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
908
1002
|
getDuplicateProjectWarnings() {
|
|
909
1003
|
this.resolveProjectLocations();
|
|
910
1004
|
return [...this.duplicateProjectWarnings];
|
|
@@ -1803,6 +1897,8 @@ ${formattedList}`
|
|
|
1803
1897
|
this.config.allowedProjects[idx] = cleanNew;
|
|
1804
1898
|
}
|
|
1805
1899
|
}
|
|
1900
|
+
this.invalidateTicketCache();
|
|
1901
|
+
this.invalidateMilestoneCache();
|
|
1806
1902
|
this.refreshConfig();
|
|
1807
1903
|
return {
|
|
1808
1904
|
success: true,
|
|
@@ -1820,11 +1916,26 @@ ${formattedList}`
|
|
|
1820
1916
|
}
|
|
1821
1917
|
readProjectMilestones(loc) {
|
|
1822
1918
|
const mPath = this.getMilestonesPath(loc);
|
|
1823
|
-
|
|
1919
|
+
const resolvedPath = path3.resolve(mPath);
|
|
1920
|
+
if (!fs3.existsSync(mPath)) {
|
|
1921
|
+
this.milestoneMemoryCache.delete(resolvedPath);
|
|
1922
|
+
return [];
|
|
1923
|
+
}
|
|
1824
1924
|
try {
|
|
1925
|
+
const stat = fs3.statSync(mPath);
|
|
1926
|
+
const cached = this.milestoneMemoryCache.get(resolvedPath);
|
|
1927
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
1928
|
+
return cached.milestones;
|
|
1929
|
+
}
|
|
1825
1930
|
const raw = fs3.readFileSync(mPath, "utf-8");
|
|
1826
1931
|
const parsed = JSON.parse(raw);
|
|
1827
|
-
|
|
1932
|
+
const milestones = Array.isArray(parsed) ? parsed : [];
|
|
1933
|
+
this.milestoneMemoryCache.set(resolvedPath, {
|
|
1934
|
+
milestones,
|
|
1935
|
+
mtimeMs: stat.mtimeMs,
|
|
1936
|
+
size: stat.size
|
|
1937
|
+
});
|
|
1938
|
+
return milestones;
|
|
1828
1939
|
} catch {
|
|
1829
1940
|
return [];
|
|
1830
1941
|
}
|
|
@@ -1832,6 +1943,8 @@ ${formattedList}`
|
|
|
1832
1943
|
writeProjectMilestones(loc, milestones) {
|
|
1833
1944
|
const mPath = this.getMilestonesPath(loc);
|
|
1834
1945
|
writeSafeFile(mPath, JSON.stringify(milestones, null, 2) + "\n");
|
|
1946
|
+
this.invalidateMilestoneCache(loc);
|
|
1947
|
+
this.invalidateTicketCache();
|
|
1835
1948
|
}
|
|
1836
1949
|
resolveMilestone(milestoneRef, localLoc) {
|
|
1837
1950
|
const cleanRef = normalizeDashesAndMojibake(milestoneRef).trim();
|
|
@@ -2081,13 +2194,162 @@ ${formattedList}`
|
|
|
2081
2194
|
}
|
|
2082
2195
|
return null;
|
|
2083
2196
|
}
|
|
2197
|
+
readTicketFromDir(ticketDir, num, loc, locations, codeToDescriptor, idToDescriptor) {
|
|
2198
|
+
const resolvedTicketDir = path3.resolve(ticketDir);
|
|
2199
|
+
const filesToCheck = [
|
|
2200
|
+
"meta.json",
|
|
2201
|
+
"detail.md",
|
|
2202
|
+
"implementation_plan.md",
|
|
2203
|
+
"comments.json",
|
|
2204
|
+
"answers.json",
|
|
2205
|
+
"inline-comments.json"
|
|
2206
|
+
];
|
|
2207
|
+
const currentStats = {};
|
|
2208
|
+
for (const f of filesToCheck) {
|
|
2209
|
+
const p = path3.join(ticketDir, f);
|
|
2210
|
+
try {
|
|
2211
|
+
const st = fs3.statSync(p);
|
|
2212
|
+
currentStats[f] = { mtimeMs: st.mtimeMs, size: st.size };
|
|
2213
|
+
} catch {
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
if (!currentStats["meta.json"]) return null;
|
|
2217
|
+
const cached = this.ticketMemoryCache.get(resolvedTicketDir);
|
|
2218
|
+
if (cached) {
|
|
2219
|
+
const cachedKeys = Object.keys(cached.stats);
|
|
2220
|
+
const currentKeys = Object.keys(currentStats);
|
|
2221
|
+
if (cachedKeys.length === currentKeys.length) {
|
|
2222
|
+
let match = true;
|
|
2223
|
+
for (const key of currentKeys) {
|
|
2224
|
+
const c = cached.stats[key];
|
|
2225
|
+
const s = currentStats[key];
|
|
2226
|
+
if (!c || c.mtimeMs !== s.mtimeMs || c.size !== s.size) {
|
|
2227
|
+
match = false;
|
|
2228
|
+
break;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
if (match) {
|
|
2232
|
+
let ticket = cached.ticket;
|
|
2233
|
+
if (ticket.meta.links && ticket.meta.links.length > 0) {
|
|
2234
|
+
ticket = {
|
|
2235
|
+
...ticket,
|
|
2236
|
+
meta: { ...ticket.meta, links: ticket.meta.links.map((l) => ({ ...l })) }
|
|
2237
|
+
};
|
|
2238
|
+
this.enrichTicketLinks(ticket);
|
|
2239
|
+
}
|
|
2240
|
+
return ticket;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
try {
|
|
2245
|
+
const metaPath = path3.join(ticketDir, "meta.json");
|
|
2246
|
+
const metaRaw = fs3.readFileSync(metaPath, "utf-8");
|
|
2247
|
+
const meta = JSON.parse(metaRaw);
|
|
2248
|
+
meta.id = meta.id ?? num;
|
|
2249
|
+
meta.type = meta.type || meta.category || "Feature";
|
|
2250
|
+
meta.category = meta.type;
|
|
2251
|
+
if (meta.priority) {
|
|
2252
|
+
meta.priority = normalizePriority(meta.priority);
|
|
2253
|
+
}
|
|
2254
|
+
let effectiveLoc = loc;
|
|
2255
|
+
if (loc.isSharedDir) {
|
|
2256
|
+
let found;
|
|
2257
|
+
if (idToDescriptor && codeToDescriptor) {
|
|
2258
|
+
const desc = (meta.projectId !== void 0 ? idToDescriptor.get(meta.projectId) : void 0) || (meta.project ? codeToDescriptor.get(meta.project.toLowerCase()) : void 0);
|
|
2259
|
+
if (desc) {
|
|
2260
|
+
found = locations.find((l) => l.project.code.toLowerCase() === desc.code.toLowerCase());
|
|
2261
|
+
}
|
|
2262
|
+
} else {
|
|
2263
|
+
found = locations.find(
|
|
2264
|
+
(l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && l.project.code.toLowerCase() === meta.project.toLowerCase()
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
if (found) effectiveLoc = found;
|
|
2268
|
+
}
|
|
2269
|
+
meta.project = effectiveLoc.project.code;
|
|
2270
|
+
meta.projectId = effectiveLoc.project.id;
|
|
2271
|
+
if (meta.milestone) {
|
|
2272
|
+
meta.milestone = normalizeDashesAndMojibake(meta.milestone);
|
|
2273
|
+
const mMatch = this.resolveMilestone(meta.milestone, effectiveLoc);
|
|
2274
|
+
if (mMatch?.featureFlag) {
|
|
2275
|
+
meta.inheritedFeatureFlag = mMatch.featureFlag;
|
|
2276
|
+
if (!meta.featureFlag) {
|
|
2277
|
+
meta.featureFlag = mMatch.featureFlag;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
let detail;
|
|
2282
|
+
const detailPath = path3.join(ticketDir, "detail.md");
|
|
2283
|
+
if (currentStats["detail.md"]) {
|
|
2284
|
+
const raw = fs3.readFileSync(detailPath, "utf-8");
|
|
2285
|
+
detail = this.parseDetailMarkdown(raw);
|
|
2286
|
+
}
|
|
2287
|
+
let planMarkdown;
|
|
2288
|
+
const planPath = path3.join(ticketDir, "implementation_plan.md");
|
|
2289
|
+
if (currentStats["implementation_plan.md"]) {
|
|
2290
|
+
planMarkdown = fs3.readFileSync(planPath, "utf-8");
|
|
2291
|
+
}
|
|
2292
|
+
let comments = [];
|
|
2293
|
+
const commentsPath = path3.join(ticketDir, "comments.json");
|
|
2294
|
+
if (currentStats["comments.json"]) {
|
|
2295
|
+
try {
|
|
2296
|
+
comments = JSON.parse(fs3.readFileSync(commentsPath, "utf-8"));
|
|
2297
|
+
} catch {
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
let answers;
|
|
2301
|
+
const answersPath = path3.join(ticketDir, "answers.json");
|
|
2302
|
+
if (currentStats["answers.json"]) {
|
|
2303
|
+
try {
|
|
2304
|
+
answers = JSON.parse(fs3.readFileSync(answersPath, "utf-8"));
|
|
2305
|
+
} catch {
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
let inlineComments;
|
|
2309
|
+
const inlinesPath = path3.join(ticketDir, "inline-comments.json");
|
|
2310
|
+
if (currentStats["inline-comments.json"]) {
|
|
2311
|
+
try {
|
|
2312
|
+
inlineComments = JSON.parse(fs3.readFileSync(inlinesPath, "utf-8"));
|
|
2313
|
+
} catch {
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
const ticketObj = {
|
|
2317
|
+
meta,
|
|
2318
|
+
detail,
|
|
2319
|
+
planMarkdown,
|
|
2320
|
+
comments,
|
|
2321
|
+
answers,
|
|
2322
|
+
inlineComments,
|
|
2323
|
+
projectDescriptor: effectiveLoc.project
|
|
2324
|
+
};
|
|
2325
|
+
normalizeTicketFields(ticketObj);
|
|
2326
|
+
this.enrichTicketLinks(ticketObj);
|
|
2327
|
+
ticketObj.sha1 = computeTicketHash(ticketObj);
|
|
2328
|
+
ticketObj.lastHash = ticketObj.sha1;
|
|
2329
|
+
this.ticketMemoryCache.set(resolvedTicketDir, {
|
|
2330
|
+
ticket: ticketObj,
|
|
2331
|
+
stats: currentStats
|
|
2332
|
+
});
|
|
2333
|
+
return ticketObj;
|
|
2334
|
+
} catch {
|
|
2335
|
+
return null;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2084
2338
|
async listTickets(filter) {
|
|
2085
2339
|
const locations = this.resolveProjectLocations();
|
|
2086
2340
|
const tickets = [];
|
|
2087
2341
|
const processedSharedTickets = /* @__PURE__ */ new Set();
|
|
2342
|
+
const codeToDescriptor = /* @__PURE__ */ new Map();
|
|
2343
|
+
const idToDescriptor = /* @__PURE__ */ new Map();
|
|
2344
|
+
for (const l of locations) {
|
|
2345
|
+
codeToDescriptor.set(l.project.code.toLowerCase(), l.project);
|
|
2346
|
+
if (l.project.id !== void 0) {
|
|
2347
|
+
idToDescriptor.set(l.project.id, l.project);
|
|
2348
|
+
idToDescriptor.set(String(l.project.id), l.project);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2088
2351
|
for (const loc of locations) {
|
|
2089
2352
|
if (filter?.project && filter.project !== "all") {
|
|
2090
|
-
const pLower = filter.project.toLowerCase();
|
|
2091
2353
|
if (!isProjectMatch(loc.project.code, filter.project)) {
|
|
2092
2354
|
continue;
|
|
2093
2355
|
}
|
|
@@ -2099,145 +2361,104 @@ ${formattedList}`
|
|
|
2099
2361
|
} catch {
|
|
2100
2362
|
continue;
|
|
2101
2363
|
}
|
|
2364
|
+
const activeDirNames = /* @__PURE__ */ new Set();
|
|
2102
2365
|
for (const entry of entries) {
|
|
2103
2366
|
if (!entry.isDirectory()) continue;
|
|
2104
2367
|
const num = parseInt(entry.name, 10);
|
|
2105
2368
|
if (isNaN(num)) continue;
|
|
2369
|
+
activeDirNames.add(entry.name);
|
|
2106
2370
|
const ticketDir = path3.join(loc.ticketsDir, entry.name);
|
|
2107
|
-
const metaPath = path3.join(ticketDir, "meta.json");
|
|
2108
|
-
if (!fs3.existsSync(metaPath)) continue;
|
|
2109
2371
|
if (loc.isSharedDir) {
|
|
2110
2372
|
const key = `${loc.ticketsDir}:${entry.name}`;
|
|
2111
2373
|
if (processedSharedTickets.has(key)) continue;
|
|
2112
2374
|
processedSharedTickets.add(key);
|
|
2113
2375
|
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
(l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && l.project.code.toLowerCase() === meta.project.toLowerCase()
|
|
2127
|
-
);
|
|
2128
|
-
if (found) effectiveLoc = found;
|
|
2129
|
-
}
|
|
2130
|
-
meta.project = effectiveLoc.project.code;
|
|
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
|
-
}
|
|
2143
|
-
if (filter?.project && filter.project !== "all") {
|
|
2144
|
-
const pLower = filter.project.toLowerCase();
|
|
2145
|
-
if (!isProjectMatch(meta.project, filter.project)) {
|
|
2146
|
-
continue;
|
|
2147
|
-
}
|
|
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
|
-
}
|
|
2169
|
-
if (filter?.status && meta.status !== filter.status) {
|
|
2376
|
+
const ticketObj = this.readTicketFromDir(
|
|
2377
|
+
ticketDir,
|
|
2378
|
+
num,
|
|
2379
|
+
loc,
|
|
2380
|
+
locations,
|
|
2381
|
+
codeToDescriptor,
|
|
2382
|
+
idToDescriptor
|
|
2383
|
+
);
|
|
2384
|
+
if (!ticketObj) continue;
|
|
2385
|
+
const meta = ticketObj.meta;
|
|
2386
|
+
if (filter?.project && filter.project !== "all") {
|
|
2387
|
+
if (!isProjectMatch(meta.project, filter.project)) {
|
|
2170
2388
|
continue;
|
|
2171
2389
|
}
|
|
2172
|
-
|
|
2390
|
+
}
|
|
2391
|
+
if (filter?.milestone) {
|
|
2392
|
+
const mFilter = filter.milestone.trim().toLowerCase();
|
|
2393
|
+
const targetLoc = meta.project && locations.find((l) => l.project.code.toLowerCase() === meta.project.toLowerCase()) || loc;
|
|
2394
|
+
const resolvedFilterMilestone = this.resolveMilestone(filter.milestone, targetLoc);
|
|
2395
|
+
const mMatch = meta.milestone ? this.resolveMilestone(meta.milestone, targetLoc) : null;
|
|
2396
|
+
const matchesDirect = Boolean(meta.milestone && meta.milestone.toLowerCase() === mFilter);
|
|
2397
|
+
const matchesResolvedTitle = Boolean(
|
|
2398
|
+
resolvedFilterMilestone && meta.milestone && meta.milestone.toLowerCase() === resolvedFilterMilestone.title.toLowerCase()
|
|
2399
|
+
);
|
|
2400
|
+
const matchesResolvedId = Boolean(
|
|
2401
|
+
resolvedFilterMilestone && meta.milestone && String(resolvedFilterMilestone.id).toLowerCase() === meta.milestone.toLowerCase()
|
|
2402
|
+
);
|
|
2403
|
+
const matchesCompound = Boolean(
|
|
2404
|
+
resolvedFilterMilestone && meta.milestone && `${resolvedFilterMilestone.project}:${resolvedFilterMilestone.title}`.toLowerCase() === meta.milestone.toLowerCase()
|
|
2405
|
+
);
|
|
2406
|
+
const matchesResolvedSelf = Boolean(
|
|
2407
|
+
mMatch && (mMatch.title.toLowerCase() === mFilter || String(mMatch.id).toLowerCase() === mFilter || `${mMatch.project}:${mMatch.title}`.toLowerCase() === mFilter || `${mMatch.project}:${mMatch.id}`.toLowerCase() === mFilter)
|
|
2408
|
+
);
|
|
2409
|
+
if (!matchesDirect && !matchesResolvedTitle && !matchesResolvedId && !matchesCompound && !matchesResolvedSelf) {
|
|
2173
2410
|
continue;
|
|
2174
2411
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2412
|
+
}
|
|
2413
|
+
if (filter?.status && meta.status !== filter.status) {
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
if (filter?.category && meta.category !== filter.category) {
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
if (filter?.priority !== void 0) {
|
|
2420
|
+
const fPrio = String(filter.priority).trim().toLowerCase();
|
|
2421
|
+
if (fPrio === "none" || fPrio === "null") {
|
|
2422
|
+
if (meta.priority) {
|
|
2423
|
+
continue;
|
|
2185
2424
|
}
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
const term = filter.search.toLowerCase();
|
|
2189
|
-
const matchesTitle = meta.title.toLowerCase().includes(term);
|
|
2190
|
-
const matchesNum = String(meta.id).includes(term) || `${meta.project}-${meta.id}`.toLowerCase().includes(term);
|
|
2191
|
-
if (!matchesTitle && !matchesNum) {
|
|
2425
|
+
} else {
|
|
2426
|
+
if (!meta.priority || meta.priority.toLowerCase() !== fPrio) {
|
|
2192
2427
|
continue;
|
|
2193
2428
|
}
|
|
2194
2429
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
const planPath = path3.join(ticketDir, "implementation_plan.md");
|
|
2203
|
-
if (fs3.existsSync(planPath)) {
|
|
2204
|
-
planMarkdown = fs3.readFileSync(planPath, "utf-8");
|
|
2205
|
-
}
|
|
2206
|
-
let comments = [];
|
|
2207
|
-
const commentsPath = path3.join(ticketDir, "comments.json");
|
|
2208
|
-
if (fs3.existsSync(commentsPath)) {
|
|
2209
|
-
try {
|
|
2210
|
-
comments = JSON.parse(fs3.readFileSync(commentsPath, "utf-8"));
|
|
2211
|
-
} catch {
|
|
2212
|
-
}
|
|
2430
|
+
}
|
|
2431
|
+
if (filter?.search) {
|
|
2432
|
+
const term = filter.search.toLowerCase();
|
|
2433
|
+
const matchesTitle = meta.title.toLowerCase().includes(term);
|
|
2434
|
+
const matchesNum = String(meta.id).includes(term) || `${meta.project}-${meta.id}`.toLowerCase().includes(term);
|
|
2435
|
+
if (!matchesTitle && !matchesNum) {
|
|
2436
|
+
continue;
|
|
2213
2437
|
}
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
if (
|
|
2438
|
+
}
|
|
2439
|
+
if (filter?.isBlocked !== void 0 && Boolean(ticketObj.isBlocked) !== Boolean(filter.isBlocked)) {
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
if (filter?.linkedTo) {
|
|
2443
|
+
const target = filter.linkedTo.trim().toLowerCase();
|
|
2444
|
+
const hasLink = ticketObj.meta.links?.some((l) => {
|
|
2445
|
+
const tk = l.targetKey.toLowerCase();
|
|
2446
|
+
const idStr = String(l.targetId);
|
|
2447
|
+
return tk === target || idStr === target;
|
|
2448
|
+
});
|
|
2449
|
+
if (!hasLink) {
|
|
2226
2450
|
continue;
|
|
2227
2451
|
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
}
|
|
2452
|
+
}
|
|
2453
|
+
tickets.push(ticketObj);
|
|
2454
|
+
}
|
|
2455
|
+
const locResolvedDir = path3.resolve(loc.ticketsDir);
|
|
2456
|
+
for (const cachedPath of this.ticketMemoryCache.keys()) {
|
|
2457
|
+
if (cachedPath.startsWith(locResolvedDir + path3.sep)) {
|
|
2458
|
+
const folderName = path3.basename(cachedPath);
|
|
2459
|
+
if (!activeDirNames.has(folderName)) {
|
|
2460
|
+
this.ticketMemoryCache.delete(cachedPath);
|
|
2238
2461
|
}
|
|
2239
|
-
tickets.push(ticketObj);
|
|
2240
|
-
} catch {
|
|
2241
2462
|
}
|
|
2242
2463
|
}
|
|
2243
2464
|
}
|
|
@@ -2251,61 +2472,8 @@ ${formattedList}`
|
|
|
2251
2472
|
async getTicket(id) {
|
|
2252
2473
|
const locInfo = this.findTicketLocation(id);
|
|
2253
2474
|
if (!locInfo) return null;
|
|
2254
|
-
const
|
|
2255
|
-
|
|
2256
|
-
try {
|
|
2257
|
-
const metaRaw = fs3.readFileSync(metaPath, "utf-8");
|
|
2258
|
-
const meta = JSON.parse(metaRaw);
|
|
2259
|
-
meta.id = meta.id ?? locInfo.id;
|
|
2260
|
-
meta.project = locInfo.loc.project.code;
|
|
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
|
-
}
|
|
2275
|
-
let detail;
|
|
2276
|
-
const detailPath = path3.join(locInfo.ticketDir, "detail.md");
|
|
2277
|
-
if (fs3.existsSync(detailPath)) {
|
|
2278
|
-
const raw = fs3.readFileSync(detailPath, "utf-8");
|
|
2279
|
-
detail = this.parseDetailMarkdown(raw);
|
|
2280
|
-
}
|
|
2281
|
-
let planMarkdown;
|
|
2282
|
-
const planPath = path3.join(locInfo.ticketDir, "implementation_plan.md");
|
|
2283
|
-
if (fs3.existsSync(planPath)) {
|
|
2284
|
-
planMarkdown = fs3.readFileSync(planPath, "utf-8");
|
|
2285
|
-
}
|
|
2286
|
-
let comments = [];
|
|
2287
|
-
const commentsPath = path3.join(locInfo.ticketDir, "comments.json");
|
|
2288
|
-
if (fs3.existsSync(commentsPath)) {
|
|
2289
|
-
try {
|
|
2290
|
-
comments = JSON.parse(fs3.readFileSync(commentsPath, "utf-8"));
|
|
2291
|
-
} catch {
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
const ticket = {
|
|
2295
|
-
meta,
|
|
2296
|
-
detail,
|
|
2297
|
-
planMarkdown,
|
|
2298
|
-
comments,
|
|
2299
|
-
projectDescriptor: locInfo.loc.project
|
|
2300
|
-
};
|
|
2301
|
-
normalizeTicketFields(ticket);
|
|
2302
|
-
this.enrichTicketLinks(ticket);
|
|
2303
|
-
ticket.sha1 = computeTicketHash(ticket);
|
|
2304
|
-
ticket.lastHash = ticket.sha1;
|
|
2305
|
-
return ticket;
|
|
2306
|
-
} catch {
|
|
2307
|
-
return null;
|
|
2308
|
-
}
|
|
2475
|
+
const locations = this.resolveProjectLocations();
|
|
2476
|
+
return this.readTicketFromDir(locInfo.ticketDir, locInfo.id, locInfo.loc, locations);
|
|
2309
2477
|
}
|
|
2310
2478
|
parseDetailMarkdown(raw) {
|
|
2311
2479
|
const titleMatch = raw.match(/^#\s*(?:Ticket\s*#?\d+\s*:\s*|\d+\s*:\s*)?([^\n]+)/im);
|
|
@@ -2365,6 +2533,63 @@ ${formattedList}`
|
|
|
2365
2533
|
raw
|
|
2366
2534
|
};
|
|
2367
2535
|
}
|
|
2536
|
+
formatTicketDetailMarkdown(rawDetail, meta, fallbackSummary) {
|
|
2537
|
+
const trimmed = rawDetail.trim();
|
|
2538
|
+
const priorityLine = meta.priority ? `**Priority**: ${meta.priority}
|
|
2539
|
+
` : "";
|
|
2540
|
+
const milestoneLine = meta.milestone ? `**Milestone**: ${meta.milestone}
|
|
2541
|
+
` : "";
|
|
2542
|
+
if (/^#\s+[^\n]+/m.test(trimmed)) {
|
|
2543
|
+
let processed = trimmed.replace(/^#\s+[^\n]+/m, `# Ticket #${meta.id}: ${meta.title}`);
|
|
2544
|
+
if (!/\*\*(?:Type|Category)\*\*:/i.test(processed)) {
|
|
2545
|
+
const metaBlock = `
|
|
2546
|
+
**Category**: ${meta.category}
|
|
2547
|
+
${priorityLine}${milestoneLine}**Complexity**: ${meta.complexity}
|
|
2548
|
+
**Estimated Effort**: ${meta.estimatedEffort}
|
|
2549
|
+
`;
|
|
2550
|
+
processed = processed.replace(/^(# Ticket[^\n]+\n)/m, `$1${metaBlock}`);
|
|
2551
|
+
}
|
|
2552
|
+
if (!/(?:##|###)\s*(?:Summary|Rationale)/i.test(processed)) {
|
|
2553
|
+
const summaryBlock = `
|
|
2554
|
+
### Summary
|
|
2555
|
+
${fallbackSummary || "Summary to be defined."}
|
|
2556
|
+
`;
|
|
2557
|
+
processed = processed.replace(/^((?:# Ticket[^\n]+\n)(?:\*\*[^\n]+\n)*)/m, `$1${summaryBlock}`);
|
|
2558
|
+
}
|
|
2559
|
+
return processed.endsWith("\n") ? processed : `${processed}
|
|
2560
|
+
`;
|
|
2561
|
+
}
|
|
2562
|
+
const hasSummary = /(?:##|###)\s*(?:Summary|Rationale)/i.test(trimmed);
|
|
2563
|
+
const hasBreakdown = /(?:##|###)\s*Feature Breakdown/i.test(trimmed);
|
|
2564
|
+
let body = "";
|
|
2565
|
+
if (!hasSummary) {
|
|
2566
|
+
body += `### Summary
|
|
2567
|
+
${fallbackSummary || "Summary to be defined."}
|
|
2568
|
+
|
|
2569
|
+
`;
|
|
2570
|
+
}
|
|
2571
|
+
if (!hasBreakdown && !trimmed.startsWith("#")) {
|
|
2572
|
+
body += `### Feature Breakdown
|
|
2573
|
+
${trimmed}
|
|
2574
|
+
|
|
2575
|
+
### Technical Details & Architecture
|
|
2576
|
+
- Architecture specifications to be documented.
|
|
2577
|
+
|
|
2578
|
+
### Open Questions & Decisions
|
|
2579
|
+
- None recorded at initialization.
|
|
2580
|
+
`;
|
|
2581
|
+
} else {
|
|
2582
|
+
body += `${trimmed}
|
|
2583
|
+
`;
|
|
2584
|
+
}
|
|
2585
|
+
return `# Ticket #${meta.id}: ${meta.title}
|
|
2586
|
+
**Category**: ${meta.category}
|
|
2587
|
+
${priorityLine}${milestoneLine}**Complexity**: ${meta.complexity}
|
|
2588
|
+
**Estimated Effort**: ${meta.estimatedEffort}
|
|
2589
|
+
|
|
2590
|
+
${body.trim()}
|
|
2591
|
+
`;
|
|
2592
|
+
}
|
|
2368
2593
|
async createTicket(input) {
|
|
2369
2594
|
const locations = this.resolveProjectLocations();
|
|
2370
2595
|
if (locations.length === 0) {
|
|
@@ -2440,64 +2665,14 @@ ${formattedList}`
|
|
|
2440
2665
|
project: targetLoc.project.code
|
|
2441
2666
|
};
|
|
2442
2667
|
const rawDetail = cleanDetailRaw?.trim();
|
|
2443
|
-
const priorityLine = meta.priority ? `**Priority**: ${meta.priority}
|
|
2444
|
-
` : "";
|
|
2445
|
-
const milestoneLine = meta.milestone ? `**Milestone**: ${meta.milestone}
|
|
2446
|
-
` : "";
|
|
2447
2668
|
let detailMd;
|
|
2448
2669
|
if (rawDetail) {
|
|
2449
|
-
|
|
2450
|
-
let processed = rawDetail.replace(/^#\s+[^\n]+/m, `# Ticket #${nextId}: ${meta.title}`);
|
|
2451
|
-
if (!/\*\*(?:Type|Category)\*\*:/i.test(processed)) {
|
|
2452
|
-
const metaBlock = `
|
|
2453
|
-
**Category**: ${meta.category}
|
|
2454
|
-
${priorityLine}${milestoneLine}**Complexity**: ${meta.complexity}
|
|
2455
|
-
**Estimated Effort**: ${meta.estimatedEffort}
|
|
2456
|
-
`;
|
|
2457
|
-
processed = processed.replace(/^(# Ticket[^\n]+\n)/m, `$1${metaBlock}`);
|
|
2458
|
-
}
|
|
2459
|
-
if (!/(?:##|###)\s*(?:Summary|Rationale)/i.test(processed)) {
|
|
2460
|
-
const summaryBlock = `
|
|
2461
|
-
### Summary
|
|
2462
|
-
${input.summary || "Summary to be defined."}
|
|
2463
|
-
`;
|
|
2464
|
-
processed = processed.replace(/^((?:# Ticket[^\n]+\n)(?:\*\*[^\n]+\n)*)/m, `$1${summaryBlock}`);
|
|
2465
|
-
}
|
|
2466
|
-
detailMd = processed.endsWith("\n") ? processed : `${processed}
|
|
2467
|
-
`;
|
|
2468
|
-
} else {
|
|
2469
|
-
const hasSummary = /(?:##|###)\s*(?:Summary|Rationale)/i.test(rawDetail);
|
|
2470
|
-
const hasBreakdown = /(?:##|###)\s*Feature Breakdown/i.test(rawDetail);
|
|
2471
|
-
let body = "";
|
|
2472
|
-
if (!hasSummary) {
|
|
2473
|
-
body += `### Summary
|
|
2474
|
-
${input.summary || "Summary to be defined."}
|
|
2475
|
-
|
|
2476
|
-
`;
|
|
2477
|
-
}
|
|
2478
|
-
if (!hasBreakdown && !rawDetail.startsWith("#")) {
|
|
2479
|
-
body += `### Feature Breakdown
|
|
2480
|
-
${rawDetail}
|
|
2481
|
-
|
|
2482
|
-
### Technical Details & Architecture
|
|
2483
|
-
- Architecture specifications to be documented.
|
|
2484
|
-
|
|
2485
|
-
### Open Questions & Decisions
|
|
2486
|
-
- None recorded at initialization.
|
|
2487
|
-
`;
|
|
2488
|
-
} else {
|
|
2489
|
-
body += `${rawDetail}
|
|
2490
|
-
`;
|
|
2491
|
-
}
|
|
2492
|
-
detailMd = `# Ticket #${nextId}: ${meta.title}
|
|
2493
|
-
**Category**: ${meta.category}
|
|
2494
|
-
${priorityLine}${milestoneLine}**Complexity**: ${meta.complexity}
|
|
2495
|
-
**Estimated Effort**: ${meta.estimatedEffort}
|
|
2496
|
-
|
|
2497
|
-
${body.trim()}
|
|
2498
|
-
`;
|
|
2499
|
-
}
|
|
2670
|
+
detailMd = this.formatTicketDetailMarkdown(rawDetail, meta, input.summary);
|
|
2500
2671
|
} else {
|
|
2672
|
+
const priorityLine = meta.priority ? `**Priority**: ${meta.priority}
|
|
2673
|
+
` : "";
|
|
2674
|
+
const milestoneLine = meta.milestone ? `**Milestone**: ${meta.milestone}
|
|
2675
|
+
` : "";
|
|
2501
2676
|
detailMd = `# Ticket #${nextId}: ${meta.title}
|
|
2502
2677
|
**Category**: ${meta.category}
|
|
2503
2678
|
${priorityLine}${milestoneLine}**Complexity**: ${meta.complexity}
|
|
@@ -2519,6 +2694,7 @@ ${input.summary || "Summary to be defined."}
|
|
|
2519
2694
|
}
|
|
2520
2695
|
writeSafeFile(path3.join(ticketDir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
2521
2696
|
writeSafeFile(path3.join(ticketDir, "detail.md"), detailMd);
|
|
2697
|
+
this.invalidateTicketCache(ticketDir);
|
|
2522
2698
|
const created = await this.getTicket(`${targetLoc.project.code}-${nextId}`);
|
|
2523
2699
|
return created;
|
|
2524
2700
|
}
|
|
@@ -2542,7 +2718,8 @@ ${input.summary || "Summary to be defined."}
|
|
|
2542
2718
|
const isNowCompleted = updates.status === "Completed";
|
|
2543
2719
|
const completedAt = isNowCompleted ? existing.meta.completedAt || now : updates.status && updates.status !== "Completed" ? void 0 : existing.meta.completedAt;
|
|
2544
2720
|
const revision = (existing.meta.revision || 1) + 1;
|
|
2545
|
-
const
|
|
2721
|
+
const { priority: _p, detail: _d, detailMarkdown: _dm, ...metaFields } = updates;
|
|
2722
|
+
const sanitizedUpdates = { ...metaFields };
|
|
2546
2723
|
if (sanitizedUpdates.title) sanitizedUpdates.title = normalizeDashesAndMojibake(sanitizedUpdates.title);
|
|
2547
2724
|
if (sanitizedUpdates.estimatedEffort) sanitizedUpdates.estimatedEffort = normalizeDashesAndMojibake(sanitizedUpdates.estimatedEffort);
|
|
2548
2725
|
if (sanitizedUpdates.complexity) sanitizedUpdates.complexity = normalizeDashesAndMojibake(sanitizedUpdates.complexity);
|
|
@@ -2596,7 +2773,41 @@ ${input.summary || "Summary to be defined."}
|
|
|
2596
2773
|
updatedMeta.type = newType;
|
|
2597
2774
|
updatedMeta.category = newType;
|
|
2598
2775
|
}
|
|
2599
|
-
|
|
2776
|
+
const rawDetailArg = updates.detailMarkdown !== void 0 ? updates.detailMarkdown : updates.detail;
|
|
2777
|
+
if (rawDetailArg !== void 0) {
|
|
2778
|
+
const cleanDetail = normalizeDashesAndMojibake(rawDetailArg);
|
|
2779
|
+
let content = this.formatTicketDetailMarkdown(cleanDetail, updatedMeta, existing.detail?.summary);
|
|
2780
|
+
if (updates.type || updates.category) {
|
|
2781
|
+
content = content.replace(/\*\*(?:Type|Category)\*\*:\s*[^\n]+/i, `**Type**: ${updatedMeta.type}`);
|
|
2782
|
+
}
|
|
2783
|
+
if (updatedMeta.priority) {
|
|
2784
|
+
if (/\*\*Priority\*\*:\s*[^\n]+/i.test(content)) {
|
|
2785
|
+
content = content.replace(/\*\*Priority\*\*:\s*[^\n]+/i, `**Priority**: ${updatedMeta.priority}`);
|
|
2786
|
+
} else if (/\*\*(?:Type|Category)\*\*:\s*[^\n]+/i.test(content)) {
|
|
2787
|
+
content = content.replace(/(\*\*(?:Type|Category)\*\*:\s*[^\n]+)/i, `$1
|
|
2788
|
+
**Priority**: ${updatedMeta.priority}`);
|
|
2789
|
+
}
|
|
2790
|
+
} else if ("priority" in updates) {
|
|
2791
|
+
content = content.replace(/\*\*Priority\*\*:\s*[^\n]+\n?/i, "");
|
|
2792
|
+
}
|
|
2793
|
+
if (updatedMeta.milestone) {
|
|
2794
|
+
if (/\*\*Milestone\*\*:\s*[^\n]+/i.test(content)) {
|
|
2795
|
+
content = content.replace(/\*\*Milestone\*\*:\s*[^\n]+/i, `**Milestone**: ${updatedMeta.milestone}`);
|
|
2796
|
+
} else if (/\*\*(?:Priority|Type|Category)\*\*:\s*[^\n]+/i.test(content)) {
|
|
2797
|
+
content = content.replace(/(\*\*(?:Priority|Type|Category)\*\*:\s*[^\n]+)/i, `$1
|
|
2798
|
+
**Milestone**: ${updatedMeta.milestone}`);
|
|
2799
|
+
}
|
|
2800
|
+
} else if ("milestone" in updates) {
|
|
2801
|
+
content = content.replace(/\*\*Milestone\*\*:\s*[^\n]+\n?/i, "");
|
|
2802
|
+
}
|
|
2803
|
+
if (updates.complexity) {
|
|
2804
|
+
content = content.replace(/\*\*Complexity\*\*:\s*[^\n]+/i, `**Complexity**: ${updatedMeta.complexity}`);
|
|
2805
|
+
}
|
|
2806
|
+
if (updates.estimatedEffort) {
|
|
2807
|
+
content = content.replace(/\*\*Estimated Effort\*\*:\s*[^\n]+/i, `**Estimated Effort**: ${updatedMeta.estimatedEffort}`);
|
|
2808
|
+
}
|
|
2809
|
+
writeSafeFile(path3.join(locInfo.ticketDir, "detail.md"), content);
|
|
2810
|
+
} else if (existing.detail && ("priority" in updates || "milestone" in updates || updates.title || updates.type || updates.category || updates.complexity || updates.estimatedEffort)) {
|
|
2600
2811
|
let content = existing.detail.raw;
|
|
2601
2812
|
if (updates.title) {
|
|
2602
2813
|
content = content.replace(/^#\s*(?:Ticket\s*#?\d+\s*:\s*|\d+\s*:\s*)?[^\n]+/im, `# Ticket #${locInfo.id}: ${updatedMeta.title}`);
|
|
@@ -2636,6 +2847,7 @@ ${input.summary || "Summary to be defined."}
|
|
|
2636
2847
|
}
|
|
2637
2848
|
writeSafeFile(path3.join(locInfo.ticketDir, "detail.md"), content);
|
|
2638
2849
|
}
|
|
2850
|
+
this.invalidateTicketCache(locInfo.ticketDir);
|
|
2639
2851
|
return await this.getTicket(`${locInfo.loc.project.code}-${locInfo.id}`);
|
|
2640
2852
|
}
|
|
2641
2853
|
async getPlan(id) {
|
|
@@ -2660,45 +2872,189 @@ ${input.summary || "Summary to be defined."}
|
|
|
2660
2872
|
if (!locInfo) {
|
|
2661
2873
|
throw new Error(`Ticket #${id} could not be located on disk`);
|
|
2662
2874
|
}
|
|
2663
|
-
const planPath = path3.join(locInfo.ticketDir, "implementation_plan.md");
|
|
2664
|
-
writeSafeFile(planPath, normalizeDashesAndMojibake(planMarkdown));
|
|
2665
|
-
const metaPath = path3.join(locInfo.ticketDir, "meta.json");
|
|
2666
|
-
if (fs3.existsSync(metaPath)) {
|
|
2875
|
+
const planPath = path3.join(locInfo.ticketDir, "implementation_plan.md");
|
|
2876
|
+
writeSafeFile(planPath, normalizeDashesAndMojibake(planMarkdown));
|
|
2877
|
+
const metaPath = path3.join(locInfo.ticketDir, "meta.json");
|
|
2878
|
+
if (fs3.existsSync(metaPath)) {
|
|
2879
|
+
try {
|
|
2880
|
+
const meta = JSON.parse(fs3.readFileSync(metaPath, "utf-8"));
|
|
2881
|
+
meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2882
|
+
meta.revision = (meta.revision || 1) + 1;
|
|
2883
|
+
writeSafeFile(metaPath, JSON.stringify(meta, null, 2) + "\n");
|
|
2884
|
+
} catch {
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
this.invalidateTicketCache(locInfo.ticketDir);
|
|
2888
|
+
}
|
|
2889
|
+
async addComment(id, comment) {
|
|
2890
|
+
const ticket = await this.getTicket(id);
|
|
2891
|
+
if (!ticket) {
|
|
2892
|
+
throw new Error(`Ticket #${id} does not exist`);
|
|
2893
|
+
}
|
|
2894
|
+
const locInfo = this.findTicketLocation(id);
|
|
2895
|
+
if (!locInfo) {
|
|
2896
|
+
throw new Error(`Ticket #${id} could not be located on disk`);
|
|
2897
|
+
}
|
|
2898
|
+
const commentsPath = path3.join(locInfo.ticketDir, "comments.json");
|
|
2899
|
+
let comments = [];
|
|
2900
|
+
if (fs3.existsSync(commentsPath)) {
|
|
2901
|
+
try {
|
|
2902
|
+
comments = JSON.parse(fs3.readFileSync(commentsPath, "utf-8"));
|
|
2903
|
+
} catch {
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
const newComment = {
|
|
2907
|
+
id: `${locInfo.id}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
2908
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2909
|
+
author: normalizeDashesAndMojibake(comment.author || "User"),
|
|
2910
|
+
text: normalizeDashesAndMojibake(comment.text.trim())
|
|
2911
|
+
};
|
|
2912
|
+
comments.push(newComment);
|
|
2913
|
+
writeSafeFile(commentsPath, JSON.stringify(comments, null, 2) + "\n");
|
|
2914
|
+
this.invalidateTicketCache(locInfo.ticketDir);
|
|
2915
|
+
return newComment;
|
|
2916
|
+
}
|
|
2917
|
+
async getAnswers(id) {
|
|
2918
|
+
const locInfo = this.findTicketLocation(id);
|
|
2919
|
+
if (!locInfo) return {};
|
|
2920
|
+
const answersPath = path3.join(locInfo.ticketDir, "answers.json");
|
|
2921
|
+
if (!fs3.existsSync(answersPath)) return {};
|
|
2922
|
+
try {
|
|
2923
|
+
return JSON.parse(fs3.readFileSync(answersPath, "utf-8"));
|
|
2924
|
+
} catch {
|
|
2925
|
+
return {};
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
async saveAnswer(id, questionIndex, answer) {
|
|
2929
|
+
const locInfo = this.findTicketLocation(id);
|
|
2930
|
+
if (!locInfo) {
|
|
2931
|
+
throw new Error(`Ticket #${id} could not be located on disk`);
|
|
2932
|
+
}
|
|
2933
|
+
const answersPath = path3.join(locInfo.ticketDir, "answers.json");
|
|
2934
|
+
let currentAnswers = {};
|
|
2935
|
+
if (fs3.existsSync(answersPath)) {
|
|
2936
|
+
try {
|
|
2937
|
+
currentAnswers = JSON.parse(fs3.readFileSync(answersPath, "utf-8"));
|
|
2938
|
+
} catch {
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
currentAnswers[String(questionIndex)] = normalizeDashesAndMojibake(answer);
|
|
2942
|
+
writeSafeFile(answersPath, JSON.stringify(currentAnswers, null, 2) + "\n");
|
|
2943
|
+
this.invalidateTicketCache(locInfo.ticketDir);
|
|
2944
|
+
return currentAnswers;
|
|
2945
|
+
}
|
|
2946
|
+
async saveDetail(id, detailMarkdown, metaUpdates) {
|
|
2947
|
+
const locInfo = this.findTicketLocation(id);
|
|
2948
|
+
if (!locInfo) {
|
|
2949
|
+
throw new Error(`Ticket #${id} could not be located on disk`);
|
|
2950
|
+
}
|
|
2951
|
+
let updatedMeta;
|
|
2952
|
+
if (metaUpdates && Object.keys(metaUpdates).length > 0) {
|
|
2953
|
+
const updatedTicket = await this.updateTicket(id, metaUpdates);
|
|
2954
|
+
updatedMeta = updatedTicket.meta;
|
|
2955
|
+
} else {
|
|
2956
|
+
const metaPath = path3.join(locInfo.ticketDir, "meta.json");
|
|
2957
|
+
if (fs3.existsSync(metaPath)) {
|
|
2958
|
+
try {
|
|
2959
|
+
const meta = JSON.parse(fs3.readFileSync(metaPath, "utf-8"));
|
|
2960
|
+
meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2961
|
+
meta.revision = (meta.revision || 1) + 1;
|
|
2962
|
+
writeSafeFile(metaPath, JSON.stringify(meta, null, 2) + "\n");
|
|
2963
|
+
} catch {
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
const detailPath = path3.join(locInfo.ticketDir, "detail.md");
|
|
2968
|
+
writeSafeFile(detailPath, normalizeDashesAndMojibake(detailMarkdown));
|
|
2969
|
+
this.invalidateTicketCache(locInfo.ticketDir);
|
|
2970
|
+
const finalTicket = await this.getTicket(`${locInfo.loc.project.code}-${locInfo.id}`);
|
|
2971
|
+
return {
|
|
2972
|
+
success: true,
|
|
2973
|
+
meta: finalTicket?.meta || updatedMeta,
|
|
2974
|
+
detail: detailMarkdown
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
async getInlineComments(id) {
|
|
2978
|
+
const locInfo = this.findTicketLocation(id);
|
|
2979
|
+
if (!locInfo) return [];
|
|
2980
|
+
const inlinesPath = path3.join(locInfo.ticketDir, "inline-comments.json");
|
|
2981
|
+
if (!fs3.existsSync(inlinesPath)) return [];
|
|
2982
|
+
try {
|
|
2983
|
+
return JSON.parse(fs3.readFileSync(inlinesPath, "utf-8"));
|
|
2984
|
+
} catch {
|
|
2985
|
+
return [];
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
async saveInlineComment(id, selectedText, comment, author) {
|
|
2989
|
+
const locInfo = this.findTicketLocation(id);
|
|
2990
|
+
if (!locInfo) {
|
|
2991
|
+
throw new Error(`Ticket #${id} could not be located on disk`);
|
|
2992
|
+
}
|
|
2993
|
+
const inlinesPath = path3.join(locInfo.ticketDir, "inline-comments.json");
|
|
2994
|
+
let currentInlines = [];
|
|
2995
|
+
if (fs3.existsSync(inlinesPath)) {
|
|
2667
2996
|
try {
|
|
2668
|
-
|
|
2669
|
-
meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2670
|
-
meta.revision = (meta.revision || 1) + 1;
|
|
2671
|
-
writeSafeFile(metaPath, JSON.stringify(meta, null, 2) + "\n");
|
|
2997
|
+
currentInlines = JSON.parse(fs3.readFileSync(inlinesPath, "utf-8"));
|
|
2672
2998
|
} catch {
|
|
2673
2999
|
}
|
|
2674
3000
|
}
|
|
3001
|
+
const newEntry = {
|
|
3002
|
+
id: `${locInfo.id}-ic-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
3003
|
+
selectedText: normalizeDashesAndMojibake(selectedText),
|
|
3004
|
+
comment: normalizeDashesAndMojibake(comment.trim()),
|
|
3005
|
+
author: normalizeDashesAndMojibake(author || "Developer"),
|
|
3006
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3007
|
+
};
|
|
3008
|
+
currentInlines.push(newEntry);
|
|
3009
|
+
writeSafeFile(inlinesPath, JSON.stringify(currentInlines, null, 2) + "\n");
|
|
3010
|
+
this.invalidateTicketCache(locInfo.ticketDir);
|
|
3011
|
+
return currentInlines;
|
|
2675
3012
|
}
|
|
2676
|
-
async
|
|
2677
|
-
const ticket = await this.getTicket(id);
|
|
2678
|
-
if (!ticket) {
|
|
2679
|
-
throw new Error(`Ticket #${id} does not exist`);
|
|
2680
|
-
}
|
|
3013
|
+
async saveAttachment(id, filename, buffer) {
|
|
2681
3014
|
const locInfo = this.findTicketLocation(id);
|
|
2682
3015
|
if (!locInfo) {
|
|
2683
3016
|
throw new Error(`Ticket #${id} could not be located on disk`);
|
|
2684
3017
|
}
|
|
2685
|
-
const
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
try {
|
|
2689
|
-
comments = JSON.parse(fs3.readFileSync(commentsPath, "utf-8"));
|
|
2690
|
-
} catch {
|
|
2691
|
-
}
|
|
3018
|
+
const attachmentsDir = path3.join(locInfo.ticketDir, "attachments");
|
|
3019
|
+
if (!fs3.existsSync(attachmentsDir)) {
|
|
3020
|
+
fs3.mkdirSync(attachmentsDir, { recursive: true });
|
|
2692
3021
|
}
|
|
2693
|
-
const
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
3022
|
+
const MAX_ATTACHMENT_SIZE_BYTES = 10 * 1024 * 1024;
|
|
3023
|
+
if (buffer.length > MAX_ATTACHMENT_SIZE_BYTES) {
|
|
3024
|
+
throw new Error(`Attachment exceeds maximum allowable size of 10MB (received ${(buffer.length / (1024 * 1024)).toFixed(1)}MB)`);
|
|
3025
|
+
}
|
|
3026
|
+
const MAX_TICKET_ATTACHMENTS = 30;
|
|
3027
|
+
const existingFiles = fs3.readdirSync(attachmentsDir);
|
|
3028
|
+
if (existingFiles.length >= MAX_TICKET_ATTACHMENTS) {
|
|
3029
|
+
throw new Error(`Ticket #${id} has reached the maximum allowed limit of ${MAX_TICKET_ATTACHMENTS} attachments`);
|
|
3030
|
+
}
|
|
3031
|
+
let safeName = path3.basename(filename).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3032
|
+
if (!safeName || safeName === "." || safeName === ".." || safeName.replace(/_/g, "") === "") {
|
|
3033
|
+
safeName = `attachment_${Date.now()}`;
|
|
3034
|
+
}
|
|
3035
|
+
let targetFile = path3.join(attachmentsDir, safeName);
|
|
3036
|
+
if (fs3.existsSync(targetFile)) {
|
|
3037
|
+
const ext = path3.extname(safeName);
|
|
3038
|
+
const nameWithoutExt = path3.basename(safeName, ext);
|
|
3039
|
+
safeName = `${nameWithoutExt}_${Date.now()}${ext}`;
|
|
3040
|
+
targetFile = path3.join(attachmentsDir, safeName);
|
|
3041
|
+
}
|
|
3042
|
+
writeSafeFile(targetFile, buffer);
|
|
3043
|
+
this.invalidateTicketCache(locInfo.ticketDir);
|
|
3044
|
+
return { filename: safeName, relativePath: `attachments/${safeName}` };
|
|
3045
|
+
}
|
|
3046
|
+
getAttachmentPath(id, filename) {
|
|
3047
|
+
const locInfo = this.findTicketLocation(id);
|
|
3048
|
+
if (!locInfo) return null;
|
|
3049
|
+
const safeName = path3.basename(filename).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3050
|
+
if (!safeName || safeName === "." || safeName === "..") return null;
|
|
3051
|
+
const attachmentsDir = path3.resolve(locInfo.ticketDir, "attachments");
|
|
3052
|
+
const target = path3.resolve(attachmentsDir, safeName);
|
|
3053
|
+
if (!target.startsWith(attachmentsDir)) return null;
|
|
3054
|
+
if (fs3.existsSync(target) && fs3.statSync(target).isFile()) {
|
|
3055
|
+
return target;
|
|
3056
|
+
}
|
|
3057
|
+
return null;
|
|
2702
3058
|
}
|
|
2703
3059
|
resolveTicketIdentifier(id, defaultProject) {
|
|
2704
3060
|
const str = String(id).trim();
|
|
@@ -2835,6 +3191,7 @@ ${input.summary || "Summary to be defined."}
|
|
|
2835
3191
|
sourceMeta.updatedAt = now;
|
|
2836
3192
|
sourceMeta.revision = (sourceMeta.revision || 1) + 1;
|
|
2837
3193
|
writeSafeFile(sourceMetaPath, JSON.stringify(sourceMeta, null, 2) + "\n");
|
|
3194
|
+
this.invalidateTicketCache(sourceInfo.ticketDir);
|
|
2838
3195
|
let updatedTarget;
|
|
2839
3196
|
const targetMetaPath = path3.join(targetInfo.ticketDir, "meta.json");
|
|
2840
3197
|
if (fs3.existsSync(targetMetaPath)) {
|
|
@@ -2862,6 +3219,7 @@ ${input.summary || "Summary to be defined."}
|
|
|
2862
3219
|
targetMeta.updatedAt = now;
|
|
2863
3220
|
targetMeta.revision = (targetMeta.revision || 1) + 1;
|
|
2864
3221
|
writeSafeFile(targetMetaPath, JSON.stringify(targetMeta, null, 2) + "\n");
|
|
3222
|
+
this.invalidateTicketCache(targetInfo.ticketDir);
|
|
2865
3223
|
updatedTarget = await this.getTicket(targetInfo.key) || void 0;
|
|
2866
3224
|
} catch {
|
|
2867
3225
|
}
|
|
@@ -2890,6 +3248,7 @@ ${input.summary || "Summary to be defined."}
|
|
|
2890
3248
|
sourceMeta.updatedAt = now;
|
|
2891
3249
|
sourceMeta.revision = (sourceMeta.revision || 1) + 1;
|
|
2892
3250
|
writeSafeFile(sourceMetaPath, JSON.stringify(sourceMeta, null, 2) + "\n");
|
|
3251
|
+
this.invalidateTicketCache(sourceInfo.ticketDir);
|
|
2893
3252
|
}
|
|
2894
3253
|
}
|
|
2895
3254
|
} catch {
|
|
@@ -2910,6 +3269,7 @@ ${input.summary || "Summary to be defined."}
|
|
|
2910
3269
|
targetMeta.updatedAt = now;
|
|
2911
3270
|
targetMeta.revision = (targetMeta.revision || 1) + 1;
|
|
2912
3271
|
writeSafeFile(targetMetaPath, JSON.stringify(targetMeta, null, 2) + "\n");
|
|
3272
|
+
this.invalidateTicketCache(targetInfo.ticketDir);
|
|
2913
3273
|
updatedTarget = await this.getTicket(targetInfo.key) || void 0;
|
|
2914
3274
|
}
|
|
2915
3275
|
}
|
|
@@ -3141,6 +3501,81 @@ var SecurityFilter = class {
|
|
|
3141
3501
|
target: result.target ? this.redactLinks(result.target) : void 0
|
|
3142
3502
|
};
|
|
3143
3503
|
}
|
|
3504
|
+
async getAnswers(id) {
|
|
3505
|
+
const ticket = await this.target.getTicket(id);
|
|
3506
|
+
if (ticket) {
|
|
3507
|
+
const code = ticket.projectDescriptor?.code || ticket.meta.project;
|
|
3508
|
+
if (code && !isProjectAuthorized(code, this.config.allowedProjects)) {
|
|
3509
|
+
throw new EsedreAuthorizationError(code);
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
return this.target.getAnswers(id);
|
|
3513
|
+
}
|
|
3514
|
+
async saveAnswer(id, questionIndex, answer) {
|
|
3515
|
+
const ticket = await this.target.getTicket(id);
|
|
3516
|
+
if (ticket) {
|
|
3517
|
+
const code = ticket.projectDescriptor?.code || ticket.meta.project;
|
|
3518
|
+
if (code && !isProjectAuthorized(code, this.config.allowedProjects)) {
|
|
3519
|
+
throw new EsedreAuthorizationError(code);
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
return this.target.saveAnswer(id, questionIndex, answer);
|
|
3523
|
+
}
|
|
3524
|
+
async saveDetail(id, detailMarkdown, metaUpdates) {
|
|
3525
|
+
const ticket = await this.target.getTicket(id);
|
|
3526
|
+
if (ticket) {
|
|
3527
|
+
const code = ticket.projectDescriptor?.code || ticket.meta.project;
|
|
3528
|
+
if (code && !isProjectAuthorized(code, this.config.allowedProjects)) {
|
|
3529
|
+
throw new EsedreAuthorizationError(code);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
return this.target.saveDetail(id, detailMarkdown, metaUpdates);
|
|
3533
|
+
}
|
|
3534
|
+
async getInlineComments(id) {
|
|
3535
|
+
const ticket = await this.target.getTicket(id);
|
|
3536
|
+
if (ticket) {
|
|
3537
|
+
const code = ticket.projectDescriptor?.code || ticket.meta.project;
|
|
3538
|
+
if (code && !isProjectAuthorized(code, this.config.allowedProjects)) {
|
|
3539
|
+
throw new EsedreAuthorizationError(code);
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
return this.target.getInlineComments ? this.target.getInlineComments(id) : [];
|
|
3543
|
+
}
|
|
3544
|
+
async saveInlineComment(id, selectedText, comment, author) {
|
|
3545
|
+
const ticket = await this.target.getTicket(id);
|
|
3546
|
+
if (ticket) {
|
|
3547
|
+
const code = ticket.projectDescriptor?.code || ticket.meta.project;
|
|
3548
|
+
if (code && !isProjectAuthorized(code, this.config.allowedProjects)) {
|
|
3549
|
+
throw new EsedreAuthorizationError(code);
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
return this.target.saveInlineComment ? this.target.saveInlineComment(id, selectedText, comment, author) : [];
|
|
3553
|
+
}
|
|
3554
|
+
async saveAttachment(id, filename, buffer) {
|
|
3555
|
+
const ticket = await this.target.getTicket(id);
|
|
3556
|
+
if (ticket) {
|
|
3557
|
+
const code = ticket.projectDescriptor?.code || ticket.meta.project;
|
|
3558
|
+
if (code && !isProjectAuthorized(code, this.config.allowedProjects)) {
|
|
3559
|
+
throw new EsedreAuthorizationError(code);
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
if (!this.target.saveAttachment) {
|
|
3563
|
+
throw new Error("Attachments are not supported by target storage adapter.");
|
|
3564
|
+
}
|
|
3565
|
+
return this.target.saveAttachment(id, filename, buffer);
|
|
3566
|
+
}
|
|
3567
|
+
getAttachmentPath(id, filename) {
|
|
3568
|
+
if (this.target.findTicketLocation) {
|
|
3569
|
+
const locInfo = this.target.findTicketLocation(id);
|
|
3570
|
+
if (locInfo) {
|
|
3571
|
+
const code = locInfo.loc?.project?.code;
|
|
3572
|
+
if (code && !isProjectAuthorized(code, this.config.allowedProjects)) {
|
|
3573
|
+
throw new EsedreAuthorizationError(code);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
return this.target.getAttachmentPath ? this.target.getAttachmentPath(id, filename) : null;
|
|
3578
|
+
}
|
|
3144
3579
|
};
|
|
3145
3580
|
|
|
3146
3581
|
// src/mcp/server.ts
|
|
@@ -3442,6 +3877,8 @@ Status: ${ticket.meta.status}`;
|
|
|
3442
3877
|
inDevelopment: { type: "boolean", description: "Active development toggle" },
|
|
3443
3878
|
featureFlag: { type: "string", description: "Feature flag name" },
|
|
3444
3879
|
milestone: { type: "string", description: 'Update ticket milestone ("none" to clear)' },
|
|
3880
|
+
detail: { type: "string", description: "Updated specification markdown for ticket detail" },
|
|
3881
|
+
detailMarkdown: { type: "string", description: "Updated specification markdown for ticket detail" },
|
|
3445
3882
|
lastHash: { type: "string", description: "Optimistic concurrency control: last known sha1 hash of the ticket" }
|
|
3446
3883
|
},
|
|
3447
3884
|
required: ["ticketId"]
|
|
@@ -3633,6 +4070,8 @@ Status: ${ticket.meta.status}`;
|
|
|
3633
4070
|
if (args.inDevelopment !== void 0) updates.isActivePlanning = Boolean(args.inDevelopment);
|
|
3634
4071
|
if (args.featureFlag) updates.featureFlag = args.featureFlag;
|
|
3635
4072
|
if (args.milestone !== void 0) updates.milestone = args.milestone;
|
|
4073
|
+
if (args.detail !== void 0) updates.detail = args.detail;
|
|
4074
|
+
if (args.detailMarkdown !== void 0) updates.detailMarkdown = args.detailMarkdown;
|
|
3636
4075
|
const updated = await this.storage.updateTicket(id, updates, args.lastHash);
|
|
3637
4076
|
return updated;
|
|
3638
4077
|
}
|
|
@@ -4079,6 +4518,22 @@ async function startDaemon(options = {}) {
|
|
|
4079
4518
|
resolveStoreDir(workspaceRoot);
|
|
4080
4519
|
fs5.mkdirSync(path5.dirname(logFile), { recursive: true });
|
|
4081
4520
|
fs5.mkdirSync(path5.dirname(stateFile), { recursive: true });
|
|
4521
|
+
const MAX_LOG_SIZE_BYTES = 10 * 1024 * 1024;
|
|
4522
|
+
try {
|
|
4523
|
+
if (fs5.existsSync(logFile)) {
|
|
4524
|
+
const stats = fs5.statSync(logFile);
|
|
4525
|
+
if (stats.size > MAX_LOG_SIZE_BYTES) {
|
|
4526
|
+
const oldLogFile = `${logFile}.old`;
|
|
4527
|
+
try {
|
|
4528
|
+
if (fs5.existsSync(oldLogFile)) fs5.unlinkSync(oldLogFile);
|
|
4529
|
+
fs5.renameSync(logFile, oldLogFile);
|
|
4530
|
+
} catch {
|
|
4531
|
+
fs5.truncateSync(logFile, 0);
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
} catch {
|
|
4536
|
+
}
|
|
4082
4537
|
const logFd = fs5.openSync(logFile, "a");
|
|
4083
4538
|
const isEsedreArgv1 = process.argv[1] && /esedre/i.test(path5.basename(process.argv[1]));
|
|
4084
4539
|
const cliCandidates = [
|
|
@@ -4276,13 +4731,30 @@ function printDaemonLogs(options = {}) {
|
|
|
4276
4731
|
console.log(`\x1B[33mNo logs found at: ${logFile}\x1B[0m`);
|
|
4277
4732
|
return;
|
|
4278
4733
|
}
|
|
4279
|
-
const content = fs5.readFileSync(logFile, "utf-8");
|
|
4280
4734
|
const count = options.lines || 40;
|
|
4735
|
+
let content = "";
|
|
4736
|
+
try {
|
|
4737
|
+
const stats = fs5.statSync(logFile);
|
|
4738
|
+
const maxReadBytes = Math.min(stats.size, Math.max(256 * 1024, count * 2048));
|
|
4739
|
+
if (stats.size <= maxReadBytes) {
|
|
4740
|
+
content = fs5.readFileSync(logFile, "utf-8");
|
|
4741
|
+
} else {
|
|
4742
|
+
const fd = fs5.openSync(logFile, "r");
|
|
4743
|
+
const buffer = Buffer.alloc(maxReadBytes);
|
|
4744
|
+
fs5.readSync(fd, buffer, 0, maxReadBytes, stats.size - maxReadBytes);
|
|
4745
|
+
fs5.closeSync(fd);
|
|
4746
|
+
content = buffer.toString("utf-8");
|
|
4747
|
+
}
|
|
4748
|
+
} catch {
|
|
4749
|
+
content = fs5.readFileSync(logFile, "utf-8");
|
|
4750
|
+
}
|
|
4281
4751
|
const allLines = content.split(/\r?\n/);
|
|
4282
|
-
const
|
|
4752
|
+
const sanitizedLines = allLines.slice(-count).map((line) => {
|
|
4753
|
+
return line.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\r(?!\n)/g, "").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
4754
|
+
});
|
|
4283
4755
|
console.log(`\x1B[36m--- Esedre Daemon Logs (${logFile}) ---\x1B[0m
|
|
4284
4756
|
`);
|
|
4285
|
-
console.log(
|
|
4757
|
+
console.log(sanitizedLines.join("\n"));
|
|
4286
4758
|
}
|
|
4287
4759
|
|
|
4288
4760
|
// src/upgrade.ts
|
|
@@ -4933,11 +5405,120 @@ function initHub(targetDir) {
|
|
|
4933
5405
|
hubDir: resolved.replace(/\\/g, "/")
|
|
4934
5406
|
};
|
|
4935
5407
|
}
|
|
5408
|
+
function isWrapperOutdated(esedreCmdContent) {
|
|
5409
|
+
if (!esedreCmdContent || typeof esedreCmdContent !== "string") {
|
|
5410
|
+
return true;
|
|
5411
|
+
}
|
|
5412
|
+
if (/goto\s+:?done\b/i.test(esedreCmdContent)) {
|
|
5413
|
+
return true;
|
|
5414
|
+
}
|
|
5415
|
+
if (/^:done\b/im.test(esedreCmdContent)) {
|
|
5416
|
+
return true;
|
|
5417
|
+
}
|
|
5418
|
+
if (!esedreCmdContent.includes("call :run %*") || !/^:run\b/im.test(esedreCmdContent)) {
|
|
5419
|
+
return true;
|
|
5420
|
+
}
|
|
5421
|
+
return false;
|
|
5422
|
+
}
|
|
5423
|
+
function findOutdatedWrappers(currentWorkspaceRoot, globalConfig) {
|
|
5424
|
+
const outdated = [];
|
|
5425
|
+
const checkedDirs = /* @__PURE__ */ new Set();
|
|
5426
|
+
const checkDir = (dir, name) => {
|
|
5427
|
+
const resolved = path6.resolve(expandHome(dir));
|
|
5428
|
+
if (checkedDirs.has(resolved.toLowerCase())) return;
|
|
5429
|
+
checkedDirs.add(resolved.toLowerCase());
|
|
5430
|
+
if (!fs6.existsSync(resolved)) return;
|
|
5431
|
+
const cmdPath = path6.join(resolved, ".esedre", "esedre.cmd");
|
|
5432
|
+
if (fs6.existsSync(cmdPath)) {
|
|
5433
|
+
try {
|
|
5434
|
+
const content = fs6.readFileSync(cmdPath, "utf-8");
|
|
5435
|
+
if (isWrapperOutdated(content)) {
|
|
5436
|
+
outdated.push({ name, dir: resolved, wrapperPath: cmdPath });
|
|
5437
|
+
}
|
|
5438
|
+
} catch {
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
};
|
|
5442
|
+
if (currentWorkspaceRoot) {
|
|
5443
|
+
checkDir(currentWorkspaceRoot, path6.basename(currentWorkspaceRoot));
|
|
5444
|
+
}
|
|
5445
|
+
if (globalConfig?.projects) {
|
|
5446
|
+
for (const [code, p] of Object.entries(globalConfig.projects)) {
|
|
5447
|
+
checkDir(p, code);
|
|
5448
|
+
}
|
|
5449
|
+
}
|
|
5450
|
+
return outdated;
|
|
5451
|
+
}
|
|
5452
|
+
async function upgradeAllWorkspaces(options = {}) {
|
|
5453
|
+
const globalCfg = options.globalConfig !== void 0 ? options.globalConfig : readGlobalConfig();
|
|
5454
|
+
const workspaceMap = /* @__PURE__ */ new Map();
|
|
5455
|
+
if (options.currentWorkspaceRoot && fs6.existsSync(options.currentWorkspaceRoot)) {
|
|
5456
|
+
const hasEsedre = fs6.existsSync(path6.join(options.currentWorkspaceRoot, ".esedre")) || fs6.existsSync(path6.join(options.currentWorkspaceRoot, "esedre.json"));
|
|
5457
|
+
if (hasEsedre) {
|
|
5458
|
+
const disc = findEsedreConfig(options.currentWorkspaceRoot, { fallbackToGlobal: true });
|
|
5459
|
+
workspaceMap.set(
|
|
5460
|
+
path6.resolve(options.currentWorkspaceRoot),
|
|
5461
|
+
disc.config?.projectCode || path6.basename(options.currentWorkspaceRoot)
|
|
5462
|
+
);
|
|
5463
|
+
}
|
|
5464
|
+
}
|
|
5465
|
+
if (globalCfg?.projects) {
|
|
5466
|
+
for (const [code, p] of Object.entries(globalCfg.projects)) {
|
|
5467
|
+
const resolved = path6.resolve(expandHome(p));
|
|
5468
|
+
if (fs6.existsSync(resolved)) {
|
|
5469
|
+
if (!workspaceMap.has(resolved)) {
|
|
5470
|
+
workspaceMap.set(resolved, code);
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5475
|
+
const results = [];
|
|
5476
|
+
for (const [targetDir, name] of workspaceMap.entries()) {
|
|
5477
|
+
try {
|
|
5478
|
+
const targetDiscovered = findEsedreConfig(targetDir, { fallbackToGlobal: true });
|
|
5479
|
+
const projectCode = targetDiscovered.config?.projectCode;
|
|
5480
|
+
const res = configureWorkspace(targetDir, {
|
|
5481
|
+
projectCode,
|
|
5482
|
+
allowedProjects: targetDiscovered.config?.allowedProjects,
|
|
5483
|
+
port: targetDiscovered.config?.port,
|
|
5484
|
+
force: options.force
|
|
5485
|
+
});
|
|
5486
|
+
let totalTickets;
|
|
5487
|
+
if (projectCode) {
|
|
5488
|
+
try {
|
|
5489
|
+
const targetStorage = new SecurityFilter(
|
|
5490
|
+
new FilesystemStorageAdapter(targetDir, targetDiscovered.config),
|
|
5491
|
+
targetDiscovered.config
|
|
5492
|
+
);
|
|
5493
|
+
const snapshot = await generateProjectSnapshot(targetStorage, projectCode, targetDir);
|
|
5494
|
+
totalTickets = snapshot.totalTickets;
|
|
5495
|
+
} catch {
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5498
|
+
results.push({
|
|
5499
|
+
dir: targetDir,
|
|
5500
|
+
name,
|
|
5501
|
+
projectCode,
|
|
5502
|
+
upgrade: res,
|
|
5503
|
+
totalTickets
|
|
5504
|
+
});
|
|
5505
|
+
} catch (err) {
|
|
5506
|
+
results.push({
|
|
5507
|
+
dir: targetDir,
|
|
5508
|
+
name,
|
|
5509
|
+
upgrade: {},
|
|
5510
|
+
error: err.message || String(err)
|
|
5511
|
+
});
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
const totalUpgraded = results.filter((r) => !r.error && r.upgrade.wrappersPlanted).length;
|
|
5515
|
+
return { workspaces: results, totalUpgraded };
|
|
5516
|
+
}
|
|
4936
5517
|
|
|
4937
5518
|
// src/server/gateway.ts
|
|
4938
|
-
import
|
|
5519
|
+
import fs9 from "node:fs";
|
|
4939
5520
|
import http4 from "node:http";
|
|
4940
|
-
import
|
|
5521
|
+
import path9 from "node:path";
|
|
4941
5522
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
4942
5523
|
|
|
4943
5524
|
// src/server/uiServer.ts
|
|
@@ -4975,6 +5556,13 @@ function startUiServer(port, webDir) {
|
|
|
4975
5556
|
}
|
|
4976
5557
|
const safePath = path7.normalize(reqPath).replace(/^(\.\.[/\\])+/, "");
|
|
4977
5558
|
let filePath = path7.join(webDir, safePath);
|
|
5559
|
+
const resolvedFilePath = path7.resolve(filePath);
|
|
5560
|
+
const resolvedWebDir = path7.resolve(webDir);
|
|
5561
|
+
if (!resolvedFilePath.startsWith(resolvedWebDir)) {
|
|
5562
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
5563
|
+
res.end("Access Denied: Path escapes web root.");
|
|
5564
|
+
return;
|
|
5565
|
+
}
|
|
4978
5566
|
if (!fs7.existsSync(filePath) || fs7.statSync(filePath).isDirectory()) {
|
|
4979
5567
|
filePath = path7.join(webDir, "index.html");
|
|
4980
5568
|
}
|
|
@@ -5008,6 +5596,8 @@ function startUiServer(port, webDir) {
|
|
|
5008
5596
|
|
|
5009
5597
|
// src/server/apiServer.ts
|
|
5010
5598
|
import http3 from "node:http";
|
|
5599
|
+
import fs8 from "node:fs";
|
|
5600
|
+
import path8 from "node:path";
|
|
5011
5601
|
var MAX_JSON_BODY_BYTES = 10 * 1024 * 1024;
|
|
5012
5602
|
function readJsonBody(req) {
|
|
5013
5603
|
return new Promise((resolve, reject) => {
|
|
@@ -5043,6 +5633,7 @@ function sendJson(res, status, data) {
|
|
|
5043
5633
|
}
|
|
5044
5634
|
var cachedAllData = /* @__PURE__ */ new Map();
|
|
5045
5635
|
var CACHE_TTL_MS = 5e3;
|
|
5636
|
+
var MAX_CACHE_ENTRIES = 30;
|
|
5046
5637
|
function invalidateApiCache() {
|
|
5047
5638
|
cachedAllData.clear();
|
|
5048
5639
|
}
|
|
@@ -5142,6 +5733,8 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5142
5733
|
const detailsMap = {};
|
|
5143
5734
|
const plansMap = {};
|
|
5144
5735
|
const commentsMap = {};
|
|
5736
|
+
const answersMap = {};
|
|
5737
|
+
const inlineCommentsMap = {};
|
|
5145
5738
|
for (const t of tickets) {
|
|
5146
5739
|
const numKey = String(t.meta.id);
|
|
5147
5740
|
const code = t.projectDescriptor?.code || t.meta.project || "Profe";
|
|
@@ -5167,6 +5760,18 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5167
5760
|
plansMap[numKey] = t.planMarkdown;
|
|
5168
5761
|
}
|
|
5169
5762
|
}
|
|
5763
|
+
if (t.answers && Object.keys(t.answers).length > 0) {
|
|
5764
|
+
answersMap[prefixedKey] = t.answers;
|
|
5765
|
+
if (!answersMap[numKey] || code.toUpperCase() === "PROF") {
|
|
5766
|
+
answersMap[numKey] = t.answers;
|
|
5767
|
+
}
|
|
5768
|
+
}
|
|
5769
|
+
if (t.inlineComments && t.inlineComments.length > 0) {
|
|
5770
|
+
inlineCommentsMap[prefixedKey] = t.inlineComments;
|
|
5771
|
+
if (!inlineCommentsMap[numKey] || code.toUpperCase() === "PROF") {
|
|
5772
|
+
inlineCommentsMap[numKey] = t.inlineComments;
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5170
5775
|
}
|
|
5171
5776
|
const projects = await reqStorage.getProjects();
|
|
5172
5777
|
const responsePayload = {
|
|
@@ -5176,11 +5781,15 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5176
5781
|
details: detailsMap,
|
|
5177
5782
|
plans: plansMap,
|
|
5178
5783
|
comments: commentsMap,
|
|
5179
|
-
answers:
|
|
5180
|
-
inlineComments:
|
|
5784
|
+
answers: answersMap,
|
|
5785
|
+
inlineComments: inlineCommentsMap,
|
|
5181
5786
|
planHistory: {},
|
|
5182
5787
|
ticketHistory: {}
|
|
5183
5788
|
};
|
|
5789
|
+
if (cachedAllData.size >= MAX_CACHE_ENTRIES) {
|
|
5790
|
+
const oldestKey = cachedAllData.keys().next().value;
|
|
5791
|
+
if (oldestKey) cachedAllData.delete(oldestKey);
|
|
5792
|
+
}
|
|
5184
5793
|
cachedAllData.set(cacheKey, { data: responsePayload, timestamp: now });
|
|
5185
5794
|
sendJson(res, 200, responsePayload);
|
|
5186
5795
|
return true;
|
|
@@ -5230,11 +5839,88 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5230
5839
|
return true;
|
|
5231
5840
|
}
|
|
5232
5841
|
if (pathname === "/api/planning/inline-comments") {
|
|
5233
|
-
|
|
5842
|
+
const ticketParam = url.searchParams.get("ticketId") || url.searchParams.get("id");
|
|
5843
|
+
if (ticketParam) {
|
|
5844
|
+
const inlines = reqStorage.getInlineComments ? await reqStorage.getInlineComments(ticketParam) : [];
|
|
5845
|
+
sendJson(res, 200, inlines);
|
|
5846
|
+
return true;
|
|
5847
|
+
}
|
|
5848
|
+
const tickets = await reqStorage.listTickets({});
|
|
5849
|
+
const inlinesMap = {};
|
|
5850
|
+
for (const t of tickets) {
|
|
5851
|
+
if (t.inlineComments && t.inlineComments.length > 0) {
|
|
5852
|
+
inlinesMap[String(t.meta.id)] = t.inlineComments;
|
|
5853
|
+
const code = t.projectDescriptor?.code || t.meta.project;
|
|
5854
|
+
if (code) {
|
|
5855
|
+
inlinesMap[`${code}-${t.meta.id}`] = t.inlineComments;
|
|
5856
|
+
}
|
|
5857
|
+
}
|
|
5858
|
+
}
|
|
5859
|
+
sendJson(res, 200, inlinesMap);
|
|
5234
5860
|
return true;
|
|
5235
5861
|
}
|
|
5236
5862
|
if (pathname === "/api/planning/answers") {
|
|
5237
|
-
|
|
5863
|
+
const ticketParam = url.searchParams.get("ticketId") || url.searchParams.get("id");
|
|
5864
|
+
if (ticketParam) {
|
|
5865
|
+
const answers = await reqStorage.getAnswers(ticketParam);
|
|
5866
|
+
sendJson(res, 200, answers);
|
|
5867
|
+
return true;
|
|
5868
|
+
}
|
|
5869
|
+
const tickets = await reqStorage.listTickets({});
|
|
5870
|
+
const answersMap = {};
|
|
5871
|
+
for (const t of tickets) {
|
|
5872
|
+
if (t.answers && Object.keys(t.answers).length > 0) {
|
|
5873
|
+
answersMap[String(t.meta.id)] = t.answers;
|
|
5874
|
+
const code = t.projectDescriptor?.code || t.meta.project;
|
|
5875
|
+
if (code) {
|
|
5876
|
+
answersMap[`${code}-${t.meta.id}`] = t.answers;
|
|
5877
|
+
}
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
sendJson(res, 200, answersMap);
|
|
5881
|
+
return true;
|
|
5882
|
+
}
|
|
5883
|
+
if (pathname === "/api/planning/attachment") {
|
|
5884
|
+
const ticketId = url.searchParams.get("ticketId") || url.searchParams.get("id");
|
|
5885
|
+
const file = url.searchParams.get("file");
|
|
5886
|
+
if (!ticketId || !file) {
|
|
5887
|
+
sendJson(res, 400, { error: "ticketId and file parameters are required." });
|
|
5888
|
+
return true;
|
|
5889
|
+
}
|
|
5890
|
+
const ticket = await reqStorage.getTicket(ticketId);
|
|
5891
|
+
if (!ticket) {
|
|
5892
|
+
sendJson(res, 404, { error: "Ticket not found." });
|
|
5893
|
+
return true;
|
|
5894
|
+
}
|
|
5895
|
+
const filePath = reqStorage.getAttachmentPath ? reqStorage.getAttachmentPath(ticketId, file) : null;
|
|
5896
|
+
if (!filePath || !fs8.existsSync(filePath)) {
|
|
5897
|
+
sendJson(res, 404, { error: "Attachment not found." });
|
|
5898
|
+
return true;
|
|
5899
|
+
}
|
|
5900
|
+
const stat = fs8.statSync(filePath);
|
|
5901
|
+
if (!stat.isFile()) {
|
|
5902
|
+
sendJson(res, 404, { error: "Attachment not found." });
|
|
5903
|
+
return true;
|
|
5904
|
+
}
|
|
5905
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
5906
|
+
const mimeTypes = {
|
|
5907
|
+
".png": "image/png",
|
|
5908
|
+
".jpg": "image/jpeg",
|
|
5909
|
+
".jpeg": "image/jpeg",
|
|
5910
|
+
".gif": "image/gif",
|
|
5911
|
+
".webp": "image/webp",
|
|
5912
|
+
".svg": "image/svg+xml",
|
|
5913
|
+
".mp4": "video/mp4",
|
|
5914
|
+
".webm": "video/webm",
|
|
5915
|
+
".mov": "video/quicktime"
|
|
5916
|
+
};
|
|
5917
|
+
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
5918
|
+
res.writeHead(200, {
|
|
5919
|
+
"Content-Type": contentType,
|
|
5920
|
+
"Content-Length": stat.size,
|
|
5921
|
+
"Cache-Control": "public, max-age=86400"
|
|
5922
|
+
});
|
|
5923
|
+
fs8.createReadStream(filePath).pipe(res);
|
|
5238
5924
|
return true;
|
|
5239
5925
|
}
|
|
5240
5926
|
if (pathname === "/api/planning/plan-history") {
|
|
@@ -5348,6 +6034,10 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5348
6034
|
}
|
|
5349
6035
|
if (pathname === "/api/planning/plans") {
|
|
5350
6036
|
const { ticketId, planMarkdown, lastHash } = body;
|
|
6037
|
+
if (!ticketId) {
|
|
6038
|
+
sendJson(res, 400, { error: "ticketId is required." });
|
|
6039
|
+
return true;
|
|
6040
|
+
}
|
|
5351
6041
|
invalidateApiCache();
|
|
5352
6042
|
await reqStorage.savePlan(ticketId, planMarkdown, lastHash);
|
|
5353
6043
|
sendJson(res, 200, { success: true });
|
|
@@ -5355,6 +6045,10 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5355
6045
|
}
|
|
5356
6046
|
if (pathname === "/api/planning/comments") {
|
|
5357
6047
|
const { ticketId, text, author } = body;
|
|
6048
|
+
if (!ticketId) {
|
|
6049
|
+
sendJson(res, 400, { error: "ticketId is required." });
|
|
6050
|
+
return true;
|
|
6051
|
+
}
|
|
5358
6052
|
invalidateApiCache();
|
|
5359
6053
|
await reqStorage.addComment(ticketId, { author: author || "Developer", text });
|
|
5360
6054
|
const ticket = await reqStorage.getTicket(ticketId);
|
|
@@ -5363,12 +6057,22 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5363
6057
|
}
|
|
5364
6058
|
if (pathname === "/api/planning/update-meta") {
|
|
5365
6059
|
const { ticketId, updates, lastHash } = body;
|
|
6060
|
+
if (!ticketId) {
|
|
6061
|
+
sendJson(res, 400, { error: "ticketId is required." });
|
|
6062
|
+
return true;
|
|
6063
|
+
}
|
|
6064
|
+
invalidateApiCache();
|
|
5366
6065
|
const updated = await reqStorage.updateTicket(ticketId, updates, lastHash);
|
|
5367
6066
|
sendJson(res, 200, { meta: updated.meta });
|
|
5368
6067
|
return true;
|
|
5369
6068
|
}
|
|
5370
6069
|
if (pathname === "/api/planning/toggle-flag") {
|
|
5371
6070
|
const { ticketId, flagged } = body;
|
|
6071
|
+
if (!ticketId) {
|
|
6072
|
+
sendJson(res, 400, { error: "ticketId is required." });
|
|
6073
|
+
return true;
|
|
6074
|
+
}
|
|
6075
|
+
invalidateApiCache();
|
|
5372
6076
|
const updated = await reqStorage.updateTicket(ticketId, {
|
|
5373
6077
|
featureFlag: flagged ? "chat_enhanced" : ""
|
|
5374
6078
|
});
|
|
@@ -5377,10 +6081,56 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5377
6081
|
}
|
|
5378
6082
|
if (pathname === "/api/planning/details") {
|
|
5379
6083
|
const { ticketId, detailMarkdown, metaUpdates } = body;
|
|
5380
|
-
if (
|
|
5381
|
-
|
|
6084
|
+
if (!ticketId) {
|
|
6085
|
+
sendJson(res, 400, { error: "ticketId is required." });
|
|
6086
|
+
return true;
|
|
6087
|
+
}
|
|
6088
|
+
invalidateApiCache();
|
|
6089
|
+
const result = await reqStorage.saveDetail(ticketId, detailMarkdown || "", metaUpdates);
|
|
6090
|
+
sendJson(res, 200, result);
|
|
6091
|
+
return true;
|
|
6092
|
+
}
|
|
6093
|
+
if (pathname === "/api/planning/answers") {
|
|
6094
|
+
const { ticketId, questionIndex, answer } = body;
|
|
6095
|
+
if (ticketId === void 0 || questionIndex === void 0 || answer === void 0) {
|
|
6096
|
+
sendJson(res, 400, { error: "ticketId, questionIndex, and answer are required." });
|
|
6097
|
+
return true;
|
|
6098
|
+
}
|
|
6099
|
+
invalidateApiCache();
|
|
6100
|
+
const updated = await reqStorage.saveAnswer(ticketId, Number(questionIndex), String(answer));
|
|
6101
|
+
sendJson(res, 200, updated);
|
|
6102
|
+
return true;
|
|
6103
|
+
}
|
|
6104
|
+
if (pathname === "/api/planning/save-inline-comment" || pathname === "/api/planning/inline-comments") {
|
|
6105
|
+
const { ticketId, selectedText, comment, author } = body;
|
|
6106
|
+
if (!ticketId || !selectedText || !comment) {
|
|
6107
|
+
sendJson(res, 400, { error: "ticketId, selectedText, and comment are required." });
|
|
6108
|
+
return true;
|
|
5382
6109
|
}
|
|
5383
|
-
|
|
6110
|
+
invalidateApiCache();
|
|
6111
|
+
const inlineComments = reqStorage.saveInlineComment ? await reqStorage.saveInlineComment(ticketId, selectedText, comment, author) : [];
|
|
6112
|
+
sendJson(res, 200, { inlineComments });
|
|
6113
|
+
return true;
|
|
6114
|
+
}
|
|
6115
|
+
if (pathname === "/api/planning/upload-attachment") {
|
|
6116
|
+
const { ticketId, filename, base64Data } = body;
|
|
6117
|
+
if (!ticketId || !filename || !base64Data) {
|
|
6118
|
+
sendJson(res, 400, { error: "ticketId, filename, and base64Data are required." });
|
|
6119
|
+
return true;
|
|
6120
|
+
}
|
|
6121
|
+
const base64Content = base64Data.replace(/^data:[^;]+;base64,/, "");
|
|
6122
|
+
const buffer = Buffer.from(base64Content, "base64");
|
|
6123
|
+
if (!reqStorage.saveAttachment) {
|
|
6124
|
+
sendJson(res, 500, { error: "Attachments are not supported by the storage engine." });
|
|
6125
|
+
return true;
|
|
6126
|
+
}
|
|
6127
|
+
invalidateApiCache();
|
|
6128
|
+
const saved = await reqStorage.saveAttachment(ticketId, filename, buffer);
|
|
6129
|
+
sendJson(res, 200, {
|
|
6130
|
+
success: true,
|
|
6131
|
+
filename: saved.filename,
|
|
6132
|
+
url: `/api/planning/attachment?ticketId=${encodeURIComponent(ticketId)}&file=${encodeURIComponent(saved.filename)}`
|
|
6133
|
+
});
|
|
5384
6134
|
return true;
|
|
5385
6135
|
}
|
|
5386
6136
|
if (pathname === "/api/planning/milestones") {
|
|
@@ -5461,35 +6211,35 @@ function startApiServer(port, storage, workspaceRoot) {
|
|
|
5461
6211
|
|
|
5462
6212
|
// src/server/gateway.ts
|
|
5463
6213
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
5464
|
-
var __dirname2 =
|
|
6214
|
+
var __dirname2 = path9.dirname(__filename2);
|
|
5465
6215
|
function resolveWebDir(explicitWebDir, workspaceRoot) {
|
|
5466
|
-
if (explicitWebDir &&
|
|
6216
|
+
if (explicitWebDir && fs9.existsSync(path9.join(explicitWebDir, "index.html"))) {
|
|
5467
6217
|
return explicitWebDir;
|
|
5468
6218
|
}
|
|
5469
6219
|
const candidates = [
|
|
5470
6220
|
// 1. Packaged layout right next to executable (e.g. tools/web)
|
|
5471
|
-
|
|
6221
|
+
path9.resolve(__dirname2, "web"),
|
|
5472
6222
|
// 2. From workspace root: <workspaceRoot>/dist/web
|
|
5473
|
-
workspaceRoot ?
|
|
6223
|
+
workspaceRoot ? path9.resolve(workspaceRoot, "dist", "web") : "",
|
|
5474
6224
|
// 3. In-repo legacy subfolder: <workspaceRoot>/esedre/dist/web
|
|
5475
|
-
workspaceRoot ?
|
|
6225
|
+
workspaceRoot ? path9.resolve(workspaceRoot, "esedre", "dist", "web") : "",
|
|
5476
6226
|
// 4. Sibling standalone repository from workspace root: <workspaceRoot>/../esedre/dist/web
|
|
5477
|
-
workspaceRoot ?
|
|
6227
|
+
workspaceRoot ? path9.resolve(workspaceRoot, "..", "esedre", "dist", "web") : "",
|
|
5478
6228
|
// 5. Packaged layout inside dist/web
|
|
5479
|
-
|
|
6229
|
+
path9.resolve(__dirname2, "dist", "web"),
|
|
5480
6230
|
// 6. Sibling standalone repository from __dirname: <__dirname>/../../esedre/dist/web
|
|
5481
|
-
|
|
6231
|
+
path9.resolve(__dirname2, "..", "..", "esedre", "dist", "web"),
|
|
5482
6232
|
// 7. Source tree layout: esedre/src/server/ -> esedre/dist/web
|
|
5483
|
-
|
|
6233
|
+
path9.resolve(__dirname2, "..", "..", "dist", "web"),
|
|
5484
6234
|
// 8. Legacy layout from __dirname
|
|
5485
|
-
|
|
6235
|
+
path9.resolve(__dirname2, "..", "esedre", "dist", "web")
|
|
5486
6236
|
].filter(Boolean);
|
|
5487
6237
|
for (const candidate of candidates) {
|
|
5488
|
-
if (
|
|
6238
|
+
if (fs9.existsSync(path9.join(candidate, "index.html"))) {
|
|
5489
6239
|
return candidate;
|
|
5490
6240
|
}
|
|
5491
6241
|
}
|
|
5492
|
-
return candidates[0] ||
|
|
6242
|
+
return candidates[0] || path9.resolve(__dirname2, "web");
|
|
5493
6243
|
}
|
|
5494
6244
|
function proxyRequest(req, res, targetPort, rewritePath) {
|
|
5495
6245
|
const targetPath = rewritePath ? rewritePath(req.url || "/") : req.url || "/";
|
|
@@ -5609,7 +6359,7 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
|
|
|
5609
6359
|
${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]
|
|
5610
6360
|
Mint a new roadmap ticket with sequential numeric ID.
|
|
5611
6361
|
|
|
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]
|
|
6362
|
+
${colors.bold}update${colors.reset} <id> [-s|--status <status>] [-t|--type <type>] [-P|--priority <prio>] [--title "..."] [--complexity <c>] [--effort "<e>"] [--detail "<md>"] [--file <path>] [--in-dev] [--flag <name>] [--last-hash <sha1>] [--force] [--json]
|
|
5613
6363
|
Mutate ticket status, type, priority, title, complexity, effort, active state, or feature flag with OCC protection.
|
|
5614
6364
|
|
|
5615
6365
|
${colors.bold}comment${colors.reset} <id> ["<text>"] [--text "..."] [--author "..."] [--json]
|
|
@@ -5661,8 +6411,9 @@ ${colors.bold}DEVELOPER ADMINISTRATION (Human Setup & Configuration):${colors.re
|
|
|
5661
6411
|
\u2022 remove <target> Unlink a project or data hub
|
|
5662
6412
|
\u2022 set <key> <val> Update a central configuration setting
|
|
5663
6413
|
|
|
5664
|
-
${colors.bold}upgrade${colors.reset} [<path>] [--force|-f] [--json]
|
|
6414
|
+
${colors.bold}upgrade${colors.reset} [<path>] [--all|-a] [--force|-f] [--json]
|
|
5665
6415
|
Upgrade workspace configuration schema, wrappers, and agent skills. Refreshes snapshot.
|
|
6416
|
+
Pass --all (-a) to upgrade all registered projects in central ~/.esedre/config.json.
|
|
5666
6417
|
Pass --force to overwrite customized skills with the latest official template.
|
|
5667
6418
|
|
|
5668
6419
|
${colors.bold}projects${colors.reset} [--json]
|
|
@@ -5675,6 +6426,7 @@ ${colors.bold}DEVELOPER ADMINISTRATION (Human Setup & Configuration):${colors.re
|
|
|
5675
6426
|
Safely rename a project code on disk, updating hub folders, manifests, ticket references, and workspace configs.
|
|
5676
6427
|
|
|
5677
6428
|
${colors.bold}OPTIONS:${colors.reset}
|
|
6429
|
+
-a, --all Apply operation across all registered projects in central configuration.
|
|
5678
6430
|
-p, --project <code> Project code (e.g. CORE, ALCE, DOCS).
|
|
5679
6431
|
-n, --name <name> Project display name (e.g. "Alce Web Reader").
|
|
5680
6432
|
-t, --type <type> Ticket type ('Feature', 'Platform', 'Tools', 'Idea', 'Bug').
|
|
@@ -5686,8 +6438,8 @@ ${colors.bold}OPTIONS:${colors.reset}
|
|
|
5686
6438
|
--to <targetId> Target ticket ID for link/unlink commands.
|
|
5687
6439
|
--blocked Filter tickets that are blocked by uncompleted tickets.
|
|
5688
6440
|
--linked-to <key> Filter tickets that are linked to a specific ticket.
|
|
5689
|
-
--detail "<md>" Specification markdown for ticket detail during create.
|
|
5690
|
-
--file <path> Path to markdown file for detail (create) or implementation plan (plan).
|
|
6441
|
+
--detail "<md>" Specification markdown for ticket detail during create or update.
|
|
6442
|
+
--file <path> Path to markdown file for detail (create, update) or implementation plan (plan).
|
|
5691
6443
|
--json Output raw machine-readable JSON (strongly recommended for autonomous LLM coding agents).
|
|
5692
6444
|
--last-hash <hash> Optimistic concurrency control: last known sha1 hash of the ticket from 'get'.
|
|
5693
6445
|
--force Bypass optimistic concurrency last-hash conflict checks on writes.
|
|
@@ -5752,6 +6504,7 @@ function parseArgs(rawArgs) {
|
|
|
5752
6504
|
if (flags["q"] && !flags["search"]) flags["search"] = flags["q"];
|
|
5753
6505
|
if (flags["r"] && !flags["relation"]) flags["relation"] = flags["r"];
|
|
5754
6506
|
if (flags["f"] && !flags["foreground"]) flags["foreground"] = flags["f"];
|
|
6507
|
+
if (flags["a"] && !flags["all"]) flags["all"] = flags["a"];
|
|
5755
6508
|
return { command, positionals, flags };
|
|
5756
6509
|
}
|
|
5757
6510
|
function printDuplicateProjectWarnings(storage, isJson) {
|
|
@@ -5759,7 +6512,7 @@ function printDuplicateProjectWarnings(storage, isJson) {
|
|
|
5759
6512
|
const warnings = storage.getDuplicateProjectWarnings ? storage.getDuplicateProjectWarnings() : [];
|
|
5760
6513
|
for (const warn of warnings) {
|
|
5761
6514
|
console.error(
|
|
5762
|
-
`${colors.yellow}\u26A0\uFE0F Warning: Duplicate project code '${warn.code}' detected across multiple data hubs (${
|
|
6515
|
+
`${colors.yellow}\u26A0\uFE0F Warning: Duplicate project code '${warn.code}' detected across multiple data hubs (${path10.basename(warn.firstHub)} and ${path10.basename(warn.duplicateHub)}). Duplicate project codes across hubs are not supported; only '${path10.basename(warn.firstHub)}' is active.${colors.reset}`
|
|
5763
6516
|
);
|
|
5764
6517
|
}
|
|
5765
6518
|
}
|
|
@@ -5802,7 +6555,7 @@ async function main() {
|
|
|
5802
6555
|
switch (command) {
|
|
5803
6556
|
case "init": {
|
|
5804
6557
|
const rawTarget = positionals[0];
|
|
5805
|
-
const targetDir = rawTarget ?
|
|
6558
|
+
const targetDir = rawTarget ? path10.resolve(rawTarget) : discovered.workspaceRoot;
|
|
5806
6559
|
const rawHubFlag = flags["hub"];
|
|
5807
6560
|
const hubArg = typeof rawHubFlag === "string" ? rawHubFlag : void 0;
|
|
5808
6561
|
const isHubRequested = Boolean(rawHubFlag);
|
|
@@ -5881,7 +6634,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
5881
6634
|
console.error(`${colors.red}Error: ${nameVal.error}${colors.reset}`);
|
|
5882
6635
|
process.exit(1);
|
|
5883
6636
|
}
|
|
5884
|
-
const isTargetHub =
|
|
6637
|
+
const isTargetHub = fs10.existsSync(path10.join(targetDir, "projects.json")) || fs10.existsSync(path10.join(targetDir, "projects")) && !fs10.existsSync(path10.join(targetDir, ".esedre", "esedre.json"));
|
|
5885
6638
|
const isWorkspaceless = isHubRequested || isTargetHub || Boolean(discovered.config?.projectCode) && !rawTarget && discovered.config?.projectCode?.toLowerCase() !== projectCode.toLowerCase();
|
|
5886
6639
|
let projectNewlyRegistered = false;
|
|
5887
6640
|
let registeredProj;
|
|
@@ -5952,7 +6705,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
5952
6705
|
const viteConfigPath = findViteConfig(targetDir);
|
|
5953
6706
|
if (viteConfigPath && setupProxy === void 0 && !isJson) {
|
|
5954
6707
|
try {
|
|
5955
|
-
const rawContent =
|
|
6708
|
+
const rawContent = fs10.readFileSync(viteConfigPath, "utf-8");
|
|
5956
6709
|
const alreadyConfigured = rawContent.includes("'/esedre'") || rawContent.includes('"/esedre"');
|
|
5957
6710
|
if (!alreadyConfigured) {
|
|
5958
6711
|
if (isYes) {
|
|
@@ -5962,7 +6715,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
5962
6715
|
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
5963
6716
|
try {
|
|
5964
6717
|
const answer = await rl.question(
|
|
5965
|
-
`${colors.cyan}?${colors.reset} Detected ${colors.bold}${
|
|
6718
|
+
`${colors.cyan}?${colors.reset} Detected ${colors.bold}${path10.basename(viteConfigPath)}${colors.reset}. Would you like to configure a reverse proxy for ${colors.bold}/esedre${colors.reset} targeting http://127.0.0.1:${port}? [Y/n] `
|
|
5966
6719
|
);
|
|
5967
6720
|
const trimmed = answer.trim().toLowerCase();
|
|
5968
6721
|
setupProxy = trimmed === "" || trimmed === "y" || trimmed === "yes";
|
|
@@ -5978,10 +6731,13 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
5978
6731
|
let hubRelPath = void 0;
|
|
5979
6732
|
if (rawHub) {
|
|
5980
6733
|
const singleHub = Array.isArray(rawHub) ? rawHub[0] : rawHub;
|
|
5981
|
-
const resolvedHub =
|
|
5982
|
-
if (
|
|
5983
|
-
const rel =
|
|
5984
|
-
|
|
6734
|
+
const resolvedHub = path10.resolve(expandHome(singleHub));
|
|
6735
|
+
if (fs10.existsSync(resolvedHub)) {
|
|
6736
|
+
const rel = path10.relative(targetDir, resolvedHub).replace(/\\/g, "/");
|
|
6737
|
+
const isSubdir = !rel.startsWith("..") && !path10.isAbsolute(rel);
|
|
6738
|
+
if (isSubdir || Boolean(flags["data-dir"])) {
|
|
6739
|
+
hubRelPath = rel.startsWith(".") ? rel : "./" + rel;
|
|
6740
|
+
}
|
|
5985
6741
|
}
|
|
5986
6742
|
}
|
|
5987
6743
|
const res = initWorkspace(targetDir, {
|
|
@@ -6152,7 +6908,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
6152
6908
|
}
|
|
6153
6909
|
console.log(`
|
|
6154
6910
|
${colors.bold}${colors.cyan}Esedre Central Configuration${colors.reset}`);
|
|
6155
|
-
console.log(` \u2022 Config File: ${configPath} ${
|
|
6911
|
+
console.log(` \u2022 Config File: ${configPath} ${fs10.existsSync(configPath) ? colors.green + "(active)" + colors.reset : colors.dim + "(default)" + colors.reset}`);
|
|
6156
6912
|
console.log(` \u2022 Default Port: ${globalCfg.port || DEFAULT_ESEDRE_PORT}`);
|
|
6157
6913
|
console.log(`
|
|
6158
6914
|
${colors.bold}Data Hubs (dataDir):${colors.reset}`);
|
|
@@ -6184,9 +6940,46 @@ ${colors.bold}Commands:${colors.reset}`);
|
|
|
6184
6940
|
return;
|
|
6185
6941
|
}
|
|
6186
6942
|
case "upgrade": {
|
|
6187
|
-
const
|
|
6188
|
-
const targetDiscovered = targetDir === discovered.workspaceRoot ? discovered : findEsedreConfig(targetDir, { fallbackToGlobal: true });
|
|
6943
|
+
const isAll = Boolean(flags["all"] || flags["a"]);
|
|
6189
6944
|
const isForce = Boolean(flags["force"] || flags["f"]);
|
|
6945
|
+
if (isAll) {
|
|
6946
|
+
const globalCfg = readGlobalConfig();
|
|
6947
|
+
const allRes = await upgradeAllWorkspaces({
|
|
6948
|
+
currentWorkspaceRoot: discovered.workspaceRoot,
|
|
6949
|
+
globalConfig: globalCfg,
|
|
6950
|
+
force: isForce
|
|
6951
|
+
});
|
|
6952
|
+
if (isJson) {
|
|
6953
|
+
console.log(JSON.stringify(allRes, null, 2));
|
|
6954
|
+
return;
|
|
6955
|
+
}
|
|
6956
|
+
if (allRes.workspaces.length === 0) {
|
|
6957
|
+
console.log(`${colors.yellow}No registered workspaces found to upgrade.${colors.reset}`);
|
|
6958
|
+
return;
|
|
6959
|
+
}
|
|
6960
|
+
console.log(`
|
|
6961
|
+
${colors.bold}${colors.green}\u2714 Esedre multi-workspace upgrade completed across ${allRes.workspaces.length} workspace(s):${colors.reset}`);
|
|
6962
|
+
for (const w of allRes.workspaces) {
|
|
6963
|
+
if (w.error) {
|
|
6964
|
+
console.log(` \u2022 ${colors.red}\u2717 ${w.name} (${w.dir}): ${w.error}${colors.reset}`);
|
|
6965
|
+
} else {
|
|
6966
|
+
const snapStr = w.totalTickets !== void 0 ? ` (${w.totalTickets} tickets projected)` : "";
|
|
6967
|
+
console.log(` \u2022 ${colors.bold}${w.name}${colors.reset} (${w.dir}):`);
|
|
6968
|
+
console.log(` - Wrappers updated in .esedre/`);
|
|
6969
|
+
if (w.totalTickets !== void 0) {
|
|
6970
|
+
console.log(` - Refreshed .esedre/snapshot.json${snapStr}`);
|
|
6971
|
+
}
|
|
6972
|
+
if (w.upgrade.skillConfigured) {
|
|
6973
|
+
console.log(` - Updated .agents/skills/esedre/SKILL.md`);
|
|
6974
|
+
} else if (w.upgrade.skillStatus === "CUSTOMIZED") {
|
|
6975
|
+
console.log(` - Preserved customized .agents/skills/esedre/SKILL.md`);
|
|
6976
|
+
}
|
|
6977
|
+
}
|
|
6978
|
+
}
|
|
6979
|
+
return;
|
|
6980
|
+
}
|
|
6981
|
+
const targetDir = positionals[0] ? path10.resolve(positionals[0]) : discovered.workspaceRoot;
|
|
6982
|
+
const targetDiscovered = targetDir === discovered.workspaceRoot ? discovered : findEsedreConfig(targetDir, { fallbackToGlobal: true });
|
|
6190
6983
|
const res = configureWorkspace(targetDir, {
|
|
6191
6984
|
projectCode: targetDiscovered.config?.projectCode,
|
|
6192
6985
|
allowedProjects: targetDiscovered.config?.allowedProjects,
|
|
@@ -6293,8 +7086,10 @@ ${colors.dim}Shutting down Esedre cluster...${colors.reset}`);
|
|
|
6293
7086
|
const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
|
|
6294
7087
|
const port = resolvePorts(discovered.config, { port: flagPort }).gateway;
|
|
6295
7088
|
const status = await getDaemonStatus({ port, workspaceRoot: discovered.workspaceRoot });
|
|
7089
|
+
const globalCfg = readGlobalConfig() || discovered.config;
|
|
7090
|
+
const outdatedWrappers = findOutdatedWrappers(discovered.workspaceRoot, globalCfg);
|
|
6296
7091
|
if (isJson) {
|
|
6297
|
-
console.log(JSON.stringify(status, null, 2));
|
|
7092
|
+
console.log(JSON.stringify({ ...status, outdatedWrappers }, null, 2));
|
|
6298
7093
|
} else {
|
|
6299
7094
|
if (status.running) {
|
|
6300
7095
|
console.log(`${colors.bold}${colors.green}\u25CF Esedre daemon is RUNNING${colors.reset}`);
|
|
@@ -6312,10 +7107,14 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
|
|
|
6312
7107
|
}
|
|
6313
7108
|
} else {
|
|
6314
7109
|
console.log(`${colors.yellow}\u25CB Esedre daemon is STOPPED (port ${status.port})${colors.reset}`);
|
|
6315
|
-
if (status.logFile &&
|
|
7110
|
+
if (status.logFile && fs10.existsSync(status.logFile)) {
|
|
6316
7111
|
console.log(` \u2022 Recent Logs: ${status.logFile}`);
|
|
6317
7112
|
}
|
|
6318
7113
|
}
|
|
7114
|
+
if (outdatedWrappers.length > 0) {
|
|
7115
|
+
console.log(`
|
|
7116
|
+
${colors.yellow}Notice: ${outdatedWrappers.length} workspace(s) have legacy CMD wrappers (${outdatedWrappers.map((o) => o.name).join(", ")}). Run 'ese upgrade --all' to update.${colors.reset}`);
|
|
7117
|
+
}
|
|
6319
7118
|
}
|
|
6320
7119
|
return;
|
|
6321
7120
|
}
|
|
@@ -6513,11 +7312,11 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
6513
7312
|
if (setPlan || filePath) {
|
|
6514
7313
|
let content = setPlan;
|
|
6515
7314
|
if (filePath) {
|
|
6516
|
-
if (!
|
|
7315
|
+
if (!fs10.existsSync(filePath)) {
|
|
6517
7316
|
console.error(`${colors.red}Error: Plan file "${filePath}" not found.${colors.reset}`);
|
|
6518
7317
|
process.exit(1);
|
|
6519
7318
|
}
|
|
6520
|
-
content =
|
|
7319
|
+
content = fs10.readFileSync(filePath, "utf-8");
|
|
6521
7320
|
}
|
|
6522
7321
|
await storage.savePlan(id, content, lastHash);
|
|
6523
7322
|
if (isJson) {
|
|
@@ -6589,8 +7388,8 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
6589
7388
|
const milestone = flags["milestone"] || flags["m"] || void 0;
|
|
6590
7389
|
let detailMarkdown = detailArg;
|
|
6591
7390
|
if (fileArg) {
|
|
6592
|
-
const filePath =
|
|
6593
|
-
if (!
|
|
7391
|
+
const filePath = path10.resolve(fileArg);
|
|
7392
|
+
if (!fs10.existsSync(filePath)) {
|
|
6594
7393
|
if (isJson) {
|
|
6595
7394
|
console.error(JSON.stringify({ error: `Detail file "${fileArg}" not found` }));
|
|
6596
7395
|
} else {
|
|
@@ -6598,7 +7397,7 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
6598
7397
|
}
|
|
6599
7398
|
process.exit(1);
|
|
6600
7399
|
}
|
|
6601
|
-
detailMarkdown =
|
|
7400
|
+
detailMarkdown = fs10.readFileSync(filePath, "utf-8");
|
|
6602
7401
|
}
|
|
6603
7402
|
const created = await storage.createTicket({
|
|
6604
7403
|
title,
|
|
@@ -6664,6 +7463,22 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
6664
7463
|
if (inDev !== void 0) updates.isActivePlanning = inDev;
|
|
6665
7464
|
if (flag) updates.featureFlag = flag;
|
|
6666
7465
|
if (rawMilestone !== void 0) updates.milestone = rawMilestone;
|
|
7466
|
+
const detailArg = flags["detail"];
|
|
7467
|
+
const fileArg = flags["file"];
|
|
7468
|
+
let detailMarkdown = detailArg ? normalizeDashesAndMojibake(detailArg) : void 0;
|
|
7469
|
+
if (fileArg) {
|
|
7470
|
+
const filePath = path10.resolve(fileArg);
|
|
7471
|
+
if (!fs10.existsSync(filePath)) {
|
|
7472
|
+
if (isJson) {
|
|
7473
|
+
console.error(JSON.stringify({ error: `Detail file "${fileArg}" not found` }));
|
|
7474
|
+
} else {
|
|
7475
|
+
console.error(`${colors.red}Error: Detail file "${fileArg}" not found.${colors.reset}`);
|
|
7476
|
+
}
|
|
7477
|
+
process.exit(1);
|
|
7478
|
+
}
|
|
7479
|
+
detailMarkdown = normalizeDashesAndMojibake(fs10.readFileSync(filePath, "utf-8"));
|
|
7480
|
+
}
|
|
7481
|
+
if (detailMarkdown !== void 0) updates.detailMarkdown = detailMarkdown;
|
|
6667
7482
|
const updated = await storage.updateTicket(lookupKey, updates, lastHash);
|
|
6668
7483
|
if (isJson) {
|
|
6669
7484
|
console.log(JSON.stringify(updated, null, 2));
|