esedre 1.0.1 → 1.2.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 +48 -0
- package/README.md +14 -5
- package/dist/esedre.mjs +1333 -458
- 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 fs11 from "node:fs";
|
|
5
|
+
import path11 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.2.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) {
|
|
@@ -2672,6 +2884,7 @@ ${input.summary || "Summary to be defined."}
|
|
|
2672
2884
|
} catch {
|
|
2673
2885
|
}
|
|
2674
2886
|
}
|
|
2887
|
+
this.invalidateTicketCache(locInfo.ticketDir);
|
|
2675
2888
|
}
|
|
2676
2889
|
async addComment(id, comment) {
|
|
2677
2890
|
const ticket = await this.getTicket(id);
|
|
@@ -2698,8 +2911,151 @@ ${input.summary || "Summary to be defined."}
|
|
|
2698
2911
|
};
|
|
2699
2912
|
comments.push(newComment);
|
|
2700
2913
|
writeSafeFile(commentsPath, JSON.stringify(comments, null, 2) + "\n");
|
|
2914
|
+
this.invalidateTicketCache(locInfo.ticketDir);
|
|
2701
2915
|
return newComment;
|
|
2702
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)) {
|
|
2996
|
+
try {
|
|
2997
|
+
currentInlines = JSON.parse(fs3.readFileSync(inlinesPath, "utf-8"));
|
|
2998
|
+
} catch {
|
|
2999
|
+
}
|
|
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;
|
|
3012
|
+
}
|
|
3013
|
+
async saveAttachment(id, filename, buffer) {
|
|
3014
|
+
const locInfo = this.findTicketLocation(id);
|
|
3015
|
+
if (!locInfo) {
|
|
3016
|
+
throw new Error(`Ticket #${id} could not be located on disk`);
|
|
3017
|
+
}
|
|
3018
|
+
const attachmentsDir = path3.join(locInfo.ticketDir, "attachments");
|
|
3019
|
+
if (!fs3.existsSync(attachmentsDir)) {
|
|
3020
|
+
fs3.mkdirSync(attachmentsDir, { recursive: true });
|
|
3021
|
+
}
|
|
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;
|
|
3058
|
+
}
|
|
2703
3059
|
resolveTicketIdentifier(id, defaultProject) {
|
|
2704
3060
|
const str = String(id).trim();
|
|
2705
3061
|
if (!/^([a-zA-Z0-9]{1,8})[-_:](\d+)$/i.test(str) && defaultProject && defaultProject !== "all") {
|
|
@@ -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,9 +3501,86 @@ 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
|
|
3582
|
+
import fs4 from "node:fs";
|
|
3583
|
+
import path4 from "node:path";
|
|
3147
3584
|
import readline from "node:readline";
|
|
3148
3585
|
var EsedreMcpServer = class {
|
|
3149
3586
|
storage;
|
|
@@ -3365,7 +3802,8 @@ Status: ${ticket.meta.status}`;
|
|
|
3365
3802
|
milestone: { type: "string", description: "Filter tickets by milestone name or ID" },
|
|
3366
3803
|
search: { type: "string", description: "Search keywords in title or ID" },
|
|
3367
3804
|
isBlocked: { type: "boolean", description: "Filter tickets blocked by uncompleted tickets" },
|
|
3368
|
-
linkedTo: { type: "string", description: "Filter tickets linked to a specific ticket ID or key" }
|
|
3805
|
+
linkedTo: { type: "string", description: "Filter tickets linked to a specific ticket ID or key" },
|
|
3806
|
+
includePlan: { type: "boolean", description: "When true, includes implementation plan markdown (planMarkdown) for each ticket" }
|
|
3369
3807
|
}
|
|
3370
3808
|
}
|
|
3371
3809
|
},
|
|
@@ -3442,6 +3880,8 @@ Status: ${ticket.meta.status}`;
|
|
|
3442
3880
|
inDevelopment: { type: "boolean", description: "Active development toggle" },
|
|
3443
3881
|
featureFlag: { type: "string", description: "Feature flag name" },
|
|
3444
3882
|
milestone: { type: "string", description: 'Update ticket milestone ("none" to clear)' },
|
|
3883
|
+
detail: { type: "string", description: "Updated specification markdown for ticket detail" },
|
|
3884
|
+
detailMarkdown: { type: "string", description: "Updated specification markdown for ticket detail" },
|
|
3445
3885
|
lastHash: { type: "string", description: "Optimistic concurrency control: last known sha1 hash of the ticket" }
|
|
3446
3886
|
},
|
|
3447
3887
|
required: ["ticketId"]
|
|
@@ -3455,9 +3895,10 @@ Status: ${ticket.meta.status}`;
|
|
|
3455
3895
|
properties: {
|
|
3456
3896
|
ticketId: { type: ["integer", "string"], description: "Numeric ticket ID or compound key" },
|
|
3457
3897
|
text: { type: "string", description: "Comment message" },
|
|
3898
|
+
file: { type: "string", description: "Path to local file containing comment message markdown" },
|
|
3458
3899
|
author: { type: "string", description: "Author name (e.g. Antigravity, Developer)" }
|
|
3459
3900
|
},
|
|
3460
|
-
required: ["ticketId"
|
|
3901
|
+
required: ["ticketId"]
|
|
3461
3902
|
}
|
|
3462
3903
|
},
|
|
3463
3904
|
{
|
|
@@ -3554,6 +3995,7 @@ Status: ${ticket.meta.status}`;
|
|
|
3554
3995
|
switch (canonical) {
|
|
3555
3996
|
case "list_tickets": {
|
|
3556
3997
|
const type = args.type || args.category;
|
|
3998
|
+
const includePlan = Boolean(args.includePlan);
|
|
3557
3999
|
const tickets = await this.storage.listTickets({
|
|
3558
4000
|
project: args.project,
|
|
3559
4001
|
status: args.status,
|
|
@@ -3565,23 +4007,39 @@ Status: ${ticket.meta.status}`;
|
|
|
3565
4007
|
isBlocked: args.isBlocked !== void 0 ? Boolean(args.isBlocked) : void 0,
|
|
3566
4008
|
linkedTo: args.linkedTo
|
|
3567
4009
|
});
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
4010
|
+
const results = await Promise.all(
|
|
4011
|
+
tickets.map(async (t) => {
|
|
4012
|
+
let planMarkdown = t.planMarkdown;
|
|
4013
|
+
if (includePlan && planMarkdown === void 0) {
|
|
4014
|
+
const lookupKey = t.projectDescriptor?.code ? `${t.projectDescriptor.code}-${t.meta.id}` : t.meta.id;
|
|
4015
|
+
planMarkdown = await this.storage.getPlan(lookupKey) ?? null;
|
|
4016
|
+
}
|
|
4017
|
+
const hasPlan = Boolean(
|
|
4018
|
+
t.planMarkdown && t.planMarkdown.trim().length > 0 || planMarkdown && planMarkdown.trim().length > 0
|
|
4019
|
+
);
|
|
4020
|
+
return {
|
|
4021
|
+
id: t.meta.id,
|
|
4022
|
+
title: t.meta.title,
|
|
4023
|
+
type: t.meta.type || t.meta.category,
|
|
4024
|
+
category: t.meta.type || t.meta.category,
|
|
4025
|
+
status: t.meta.status,
|
|
4026
|
+
priority: t.meta.priority,
|
|
4027
|
+
complexity: t.meta.complexity,
|
|
4028
|
+
effort: t.meta.estimatedEffort,
|
|
4029
|
+
milestone: t.meta.milestone,
|
|
4030
|
+
featureFlag: t.meta.featureFlag,
|
|
4031
|
+
inheritedFeatureFlag: t.meta.inheritedFeatureFlag,
|
|
4032
|
+
project: t.projectDescriptor?.code || t.meta.project || "UNASSIGNED",
|
|
4033
|
+
summary: t.detail?.summary,
|
|
4034
|
+
hasPlan,
|
|
4035
|
+
...includePlan ? { planMarkdown: planMarkdown || null } : {},
|
|
4036
|
+
sha1: t.sha1 || t.meta.sha1,
|
|
4037
|
+
isBlocked: t.isBlocked,
|
|
4038
|
+
links: t.links
|
|
4039
|
+
};
|
|
4040
|
+
})
|
|
4041
|
+
);
|
|
4042
|
+
return results;
|
|
3585
4043
|
}
|
|
3586
4044
|
case "get_ticket": {
|
|
3587
4045
|
const id = this.parseTicketId(args.ticketId);
|
|
@@ -3633,13 +4091,33 @@ Status: ${ticket.meta.status}`;
|
|
|
3633
4091
|
if (args.inDevelopment !== void 0) updates.isActivePlanning = Boolean(args.inDevelopment);
|
|
3634
4092
|
if (args.featureFlag) updates.featureFlag = args.featureFlag;
|
|
3635
4093
|
if (args.milestone !== void 0) updates.milestone = args.milestone;
|
|
4094
|
+
if (args.detail !== void 0) updates.detail = args.detail;
|
|
4095
|
+
if (args.detailMarkdown !== void 0) updates.detailMarkdown = args.detailMarkdown;
|
|
3636
4096
|
const updated = await this.storage.updateTicket(id, updates, args.lastHash);
|
|
3637
4097
|
return updated;
|
|
3638
4098
|
}
|
|
3639
4099
|
case "add_comment": {
|
|
3640
4100
|
const id = this.parseTicketId(args.ticketId);
|
|
4101
|
+
let text = args.text;
|
|
4102
|
+
if (!text && args.file) {
|
|
4103
|
+
const resolvedPath = path4.resolve(process.cwd(), args.file);
|
|
4104
|
+
if (!fs4.existsSync(resolvedPath)) {
|
|
4105
|
+
throw new Error(`File not found: ${args.file}`);
|
|
4106
|
+
}
|
|
4107
|
+
if (!fs4.statSync(resolvedPath).isFile()) {
|
|
4108
|
+
throw new Error(`Path is not a file: ${args.file}`);
|
|
4109
|
+
}
|
|
4110
|
+
const fileContent = fs4.readFileSync(resolvedPath, "utf-8");
|
|
4111
|
+
if (!fileContent.trim()) {
|
|
4112
|
+
throw new Error(`Comment file is empty: ${args.file}`);
|
|
4113
|
+
}
|
|
4114
|
+
text = fileContent;
|
|
4115
|
+
}
|
|
4116
|
+
if (!text || !text.trim()) {
|
|
4117
|
+
throw new Error("Comment text or file is required");
|
|
4118
|
+
}
|
|
3641
4119
|
const comment = await this.storage.addComment(id, {
|
|
3642
|
-
text
|
|
4120
|
+
text,
|
|
3643
4121
|
author: args.author || "Agent"
|
|
3644
4122
|
});
|
|
3645
4123
|
return comment;
|
|
@@ -3708,19 +4186,19 @@ Status: ${ticket.meta.status}`;
|
|
|
3708
4186
|
};
|
|
3709
4187
|
|
|
3710
4188
|
// src/utils/manifestDetector.ts
|
|
3711
|
-
import
|
|
3712
|
-
import
|
|
4189
|
+
import fs5 from "node:fs";
|
|
4190
|
+
import path5 from "node:path";
|
|
3713
4191
|
function sanitizeCode(input) {
|
|
3714
4192
|
const clean = input.replace(/[^a-zA-Z0-9]/g, "");
|
|
3715
4193
|
return clean.slice(0, MAX_PROJECT_CODE_LENGTH) || "APP";
|
|
3716
4194
|
}
|
|
3717
4195
|
function detectProjectManifest(targetDir) {
|
|
3718
|
-
const dirBase =
|
|
4196
|
+
const dirBase = path5.basename(targetDir);
|
|
3719
4197
|
for (const settingsFile of ["settings.gradle.kts", "settings.gradle"]) {
|
|
3720
|
-
const fullPath =
|
|
3721
|
-
if (
|
|
4198
|
+
const fullPath = path5.join(targetDir, settingsFile);
|
|
4199
|
+
if (fs5.existsSync(fullPath)) {
|
|
3722
4200
|
try {
|
|
3723
|
-
const content =
|
|
4201
|
+
const content = fs5.readFileSync(fullPath, "utf-8");
|
|
3724
4202
|
const match = content.match(/rootProject\.name\s*=\s*["']([^"']+)["']/);
|
|
3725
4203
|
if (match && match[1].trim()) {
|
|
3726
4204
|
const rawName = match[1].trim();
|
|
@@ -3736,15 +4214,15 @@ function detectProjectManifest(targetDir) {
|
|
|
3736
4214
|
}
|
|
3737
4215
|
}
|
|
3738
4216
|
for (const buildFile of [
|
|
3739
|
-
|
|
4217
|
+
path5.join("app", "build.gradle.kts"),
|
|
3740
4218
|
"build.gradle.kts",
|
|
3741
|
-
|
|
4219
|
+
path5.join("app", "build.gradle"),
|
|
3742
4220
|
"build.gradle"
|
|
3743
4221
|
]) {
|
|
3744
|
-
const fullPath =
|
|
3745
|
-
if (
|
|
4222
|
+
const fullPath = path5.join(targetDir, buildFile);
|
|
4223
|
+
if (fs5.existsSync(fullPath)) {
|
|
3746
4224
|
try {
|
|
3747
|
-
const content =
|
|
4225
|
+
const content = fs5.readFileSync(fullPath, "utf-8");
|
|
3748
4226
|
const match = content.match(/(?:namespace|applicationId)\s*=\s*["']([^"']+)["']/);
|
|
3749
4227
|
if (match && match[1].trim()) {
|
|
3750
4228
|
const ns = match[1].trim();
|
|
@@ -3761,10 +4239,10 @@ function detectProjectManifest(targetDir) {
|
|
|
3761
4239
|
}
|
|
3762
4240
|
}
|
|
3763
4241
|
}
|
|
3764
|
-
const pomPath =
|
|
3765
|
-
if (
|
|
4242
|
+
const pomPath = path5.join(targetDir, "pom.xml");
|
|
4243
|
+
if (fs5.existsSync(pomPath)) {
|
|
3766
4244
|
try {
|
|
3767
|
-
const content =
|
|
4245
|
+
const content = fs5.readFileSync(pomPath, "utf-8");
|
|
3768
4246
|
const stripped = content.replace(/<!--[\s\S]*?-->/g, "").replace(/<parent>[\s\S]*?<\/parent>/g, "");
|
|
3769
4247
|
const nameMatch = stripped.match(/<name>\s*([^<]+)\s*<\/name>/);
|
|
3770
4248
|
const artifactMatch = stripped.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/) || content.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/);
|
|
@@ -3783,10 +4261,10 @@ function detectProjectManifest(targetDir) {
|
|
|
3783
4261
|
} catch {
|
|
3784
4262
|
}
|
|
3785
4263
|
}
|
|
3786
|
-
const pkgPath =
|
|
3787
|
-
if (
|
|
4264
|
+
const pkgPath = path5.join(targetDir, "package.json");
|
|
4265
|
+
if (fs5.existsSync(pkgPath)) {
|
|
3788
4266
|
try {
|
|
3789
|
-
const pkg = JSON.parse(
|
|
4267
|
+
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
3790
4268
|
if (pkg.name && typeof pkg.name === "string") {
|
|
3791
4269
|
const baseName = pkg.name.replace(/^@[\w-]+\//, "");
|
|
3792
4270
|
return {
|
|
@@ -3799,10 +4277,10 @@ function detectProjectManifest(targetDir) {
|
|
|
3799
4277
|
} catch {
|
|
3800
4278
|
}
|
|
3801
4279
|
}
|
|
3802
|
-
const cargoPath =
|
|
3803
|
-
if (
|
|
4280
|
+
const cargoPath = path5.join(targetDir, "Cargo.toml");
|
|
4281
|
+
if (fs5.existsSync(cargoPath)) {
|
|
3804
4282
|
try {
|
|
3805
|
-
const content =
|
|
4283
|
+
const content = fs5.readFileSync(cargoPath, "utf-8");
|
|
3806
4284
|
const match = content.match(/\[package\][\s\S]*?name\s*=\s*["']([^"']+)["']/);
|
|
3807
4285
|
if (match && match[1].trim()) {
|
|
3808
4286
|
const rawName = match[1].trim();
|
|
@@ -3816,10 +4294,10 @@ function detectProjectManifest(targetDir) {
|
|
|
3816
4294
|
} catch {
|
|
3817
4295
|
}
|
|
3818
4296
|
}
|
|
3819
|
-
const pyprojectPath =
|
|
3820
|
-
if (
|
|
4297
|
+
const pyprojectPath = path5.join(targetDir, "pyproject.toml");
|
|
4298
|
+
if (fs5.existsSync(pyprojectPath)) {
|
|
3821
4299
|
try {
|
|
3822
|
-
const content =
|
|
4300
|
+
const content = fs5.readFileSync(pyprojectPath, "utf-8");
|
|
3823
4301
|
const match = content.match(/name\s*=\s*["']([^"']+)["']/);
|
|
3824
4302
|
if (match && match[1].trim()) {
|
|
3825
4303
|
const rawName = match[1].trim();
|
|
@@ -3833,10 +4311,10 @@ function detectProjectManifest(targetDir) {
|
|
|
3833
4311
|
} catch {
|
|
3834
4312
|
}
|
|
3835
4313
|
}
|
|
3836
|
-
const setupCfgPath =
|
|
3837
|
-
if (
|
|
4314
|
+
const setupCfgPath = path5.join(targetDir, "setup.cfg");
|
|
4315
|
+
if (fs5.existsSync(setupCfgPath)) {
|
|
3838
4316
|
try {
|
|
3839
|
-
const content =
|
|
4317
|
+
const content = fs5.readFileSync(setupCfgPath, "utf-8");
|
|
3840
4318
|
const match = content.match(/^name\s*=\s*([^\r\n]+)/m);
|
|
3841
4319
|
if (match && match[1].trim()) {
|
|
3842
4320
|
const rawName = match[1].trim();
|
|
@@ -3850,10 +4328,10 @@ function detectProjectManifest(targetDir) {
|
|
|
3850
4328
|
} catch {
|
|
3851
4329
|
}
|
|
3852
4330
|
}
|
|
3853
|
-
const setupPyPath =
|
|
3854
|
-
if (
|
|
4331
|
+
const setupPyPath = path5.join(targetDir, "setup.py");
|
|
4332
|
+
if (fs5.existsSync(setupPyPath)) {
|
|
3855
4333
|
try {
|
|
3856
|
-
const content =
|
|
4334
|
+
const content = fs5.readFileSync(setupPyPath, "utf-8");
|
|
3857
4335
|
const match = content.match(/name\s*=\s*["']([^"']+)["']/);
|
|
3858
4336
|
if (match && match[1].trim()) {
|
|
3859
4337
|
const rawName = match[1].trim();
|
|
@@ -3867,10 +4345,10 @@ function detectProjectManifest(targetDir) {
|
|
|
3867
4345
|
} catch {
|
|
3868
4346
|
}
|
|
3869
4347
|
}
|
|
3870
|
-
const goModPath =
|
|
3871
|
-
if (
|
|
4348
|
+
const goModPath = path5.join(targetDir, "go.mod");
|
|
4349
|
+
if (fs5.existsSync(goModPath)) {
|
|
3872
4350
|
try {
|
|
3873
|
-
const content =
|
|
4351
|
+
const content = fs5.readFileSync(goModPath, "utf-8");
|
|
3874
4352
|
const match = content.match(/^module\s+([^\r\n\s]+)/m);
|
|
3875
4353
|
if (match && match[1].trim()) {
|
|
3876
4354
|
const modPath = match[1].trim();
|
|
@@ -3894,48 +4372,48 @@ function detectProjectManifest(targetDir) {
|
|
|
3894
4372
|
|
|
3895
4373
|
// src/upgrade.ts
|
|
3896
4374
|
import crypto2 from "node:crypto";
|
|
3897
|
-
import
|
|
3898
|
-
import
|
|
4375
|
+
import fs7 from "node:fs";
|
|
4376
|
+
import path7 from "node:path";
|
|
3899
4377
|
|
|
3900
4378
|
// src/server/daemon.ts
|
|
3901
|
-
import
|
|
3902
|
-
import
|
|
4379
|
+
import fs6 from "node:fs";
|
|
4380
|
+
import path6 from "node:path";
|
|
3903
4381
|
import os3 from "node:os";
|
|
3904
4382
|
import http from "node:http";
|
|
3905
4383
|
import { spawn, spawnSync } from "node:child_process";
|
|
3906
4384
|
import { fileURLToPath } from "node:url";
|
|
3907
4385
|
var __filename = fileURLToPath(import.meta.url);
|
|
3908
|
-
var __dirname =
|
|
4386
|
+
var __dirname = path6.dirname(__filename);
|
|
3909
4387
|
function getGlobalEsedreDir() {
|
|
3910
4388
|
if (process.env.ESEDRE_GLOBAL_DIR) {
|
|
3911
4389
|
return process.env.ESEDRE_GLOBAL_DIR;
|
|
3912
4390
|
}
|
|
3913
|
-
return
|
|
4391
|
+
return path6.join(os3.homedir(), ".esedre");
|
|
3914
4392
|
}
|
|
3915
4393
|
function ensureGlobalEsedreStore() {
|
|
3916
4394
|
const globalDir = getGlobalEsedreDir();
|
|
3917
|
-
const runDir =
|
|
3918
|
-
const logsDir =
|
|
4395
|
+
const runDir = path6.join(globalDir, "run");
|
|
4396
|
+
const logsDir = path6.join(globalDir, "logs");
|
|
3919
4397
|
try {
|
|
3920
|
-
|
|
3921
|
-
|
|
4398
|
+
fs6.mkdirSync(runDir, { recursive: true });
|
|
4399
|
+
fs6.mkdirSync(logsDir, { recursive: true });
|
|
3922
4400
|
try {
|
|
3923
|
-
const runEntries =
|
|
4401
|
+
const runEntries = fs6.readdirSync(runDir);
|
|
3924
4402
|
for (const entry of runEntries) {
|
|
3925
4403
|
if (entry.startsWith(".probe-")) {
|
|
3926
|
-
|
|
4404
|
+
fs6.unlinkSync(path6.join(runDir, entry));
|
|
3927
4405
|
}
|
|
3928
4406
|
}
|
|
3929
4407
|
} catch {
|
|
3930
4408
|
}
|
|
3931
|
-
const testProbe =
|
|
4409
|
+
const testProbe = path6.join(runDir, `.probe-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`);
|
|
3932
4410
|
try {
|
|
3933
|
-
|
|
4411
|
+
fs6.writeFileSync(testProbe, "ok", "utf-8");
|
|
3934
4412
|
return { ok: true, path: globalDir };
|
|
3935
4413
|
} finally {
|
|
3936
|
-
if (
|
|
4414
|
+
if (fs6.existsSync(testProbe)) {
|
|
3937
4415
|
try {
|
|
3938
|
-
|
|
4416
|
+
fs6.unlinkSync(testProbe);
|
|
3939
4417
|
} catch {
|
|
3940
4418
|
}
|
|
3941
4419
|
}
|
|
@@ -3950,10 +4428,10 @@ function resolveStoreDir(workspaceRoot) {
|
|
|
3950
4428
|
return globalStore.path;
|
|
3951
4429
|
}
|
|
3952
4430
|
if (workspaceRoot) {
|
|
3953
|
-
const localStore =
|
|
4431
|
+
const localStore = path6.join(workspaceRoot, ".esedre");
|
|
3954
4432
|
try {
|
|
3955
|
-
|
|
3956
|
-
|
|
4433
|
+
fs6.mkdirSync(path6.join(localStore, "run"), { recursive: true });
|
|
4434
|
+
fs6.mkdirSync(path6.join(localStore, "logs"), { recursive: true });
|
|
3957
4435
|
return localStore;
|
|
3958
4436
|
} catch {
|
|
3959
4437
|
}
|
|
@@ -3962,32 +4440,32 @@ function resolveStoreDir(workspaceRoot) {
|
|
|
3962
4440
|
}
|
|
3963
4441
|
function getDaemonStateFile(port, workspaceRoot) {
|
|
3964
4442
|
const store = resolveStoreDir(workspaceRoot);
|
|
3965
|
-
return
|
|
4443
|
+
return path6.join(store, "run", `daemon-${port}.json`);
|
|
3966
4444
|
}
|
|
3967
4445
|
function getDaemonLogFile(port, workspaceRoot) {
|
|
3968
4446
|
const store = resolveStoreDir(workspaceRoot);
|
|
3969
|
-
return
|
|
4447
|
+
return path6.join(store, "logs", `daemon-${port}.log`);
|
|
3970
4448
|
}
|
|
3971
4449
|
function getDaemonState(port, workspaceRoot) {
|
|
3972
4450
|
const stateFile = getDaemonStateFile(port, workspaceRoot);
|
|
3973
|
-
if (!
|
|
4451
|
+
if (!fs6.existsSync(stateFile)) {
|
|
3974
4452
|
return null;
|
|
3975
4453
|
}
|
|
3976
4454
|
try {
|
|
3977
|
-
const raw =
|
|
4455
|
+
const raw = fs6.readFileSync(stateFile, "utf-8").trim();
|
|
3978
4456
|
if (!raw) {
|
|
3979
|
-
|
|
4457
|
+
fs6.unlinkSync(stateFile);
|
|
3980
4458
|
return null;
|
|
3981
4459
|
}
|
|
3982
4460
|
const state = JSON.parse(raw);
|
|
3983
4461
|
if (!state || typeof state.pid !== "number" || typeof state.port !== "number") {
|
|
3984
|
-
|
|
4462
|
+
fs6.unlinkSync(stateFile);
|
|
3985
4463
|
return null;
|
|
3986
4464
|
}
|
|
3987
4465
|
return state;
|
|
3988
4466
|
} catch {
|
|
3989
4467
|
try {
|
|
3990
|
-
|
|
4468
|
+
fs6.unlinkSync(stateFile);
|
|
3991
4469
|
} catch {
|
|
3992
4470
|
}
|
|
3993
4471
|
return null;
|
|
@@ -4058,7 +4536,7 @@ async function startDaemon(options = {}) {
|
|
|
4058
4536
|
}
|
|
4059
4537
|
}
|
|
4060
4538
|
try {
|
|
4061
|
-
|
|
4539
|
+
fs6.unlinkSync(stateFile);
|
|
4062
4540
|
} catch {
|
|
4063
4541
|
}
|
|
4064
4542
|
}
|
|
@@ -4077,24 +4555,40 @@ async function startDaemon(options = {}) {
|
|
|
4077
4555
|
};
|
|
4078
4556
|
}
|
|
4079
4557
|
resolveStoreDir(workspaceRoot);
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
const
|
|
4083
|
-
|
|
4558
|
+
fs6.mkdirSync(path6.dirname(logFile), { recursive: true });
|
|
4559
|
+
fs6.mkdirSync(path6.dirname(stateFile), { recursive: true });
|
|
4560
|
+
const MAX_LOG_SIZE_BYTES = 10 * 1024 * 1024;
|
|
4561
|
+
try {
|
|
4562
|
+
if (fs6.existsSync(logFile)) {
|
|
4563
|
+
const stats = fs6.statSync(logFile);
|
|
4564
|
+
if (stats.size > MAX_LOG_SIZE_BYTES) {
|
|
4565
|
+
const oldLogFile = `${logFile}.old`;
|
|
4566
|
+
try {
|
|
4567
|
+
if (fs6.existsSync(oldLogFile)) fs6.unlinkSync(oldLogFile);
|
|
4568
|
+
fs6.renameSync(logFile, oldLogFile);
|
|
4569
|
+
} catch {
|
|
4570
|
+
fs6.truncateSync(logFile, 0);
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
} catch {
|
|
4575
|
+
}
|
|
4576
|
+
const logFd = fs6.openSync(logFile, "a");
|
|
4577
|
+
const isEsedreArgv1 = process.argv[1] && /esedre/i.test(path6.basename(process.argv[1]));
|
|
4084
4578
|
const cliCandidates = [
|
|
4085
|
-
isEsedreArgv1 ?
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4579
|
+
isEsedreArgv1 ? path6.resolve(process.argv[1]) : void 0,
|
|
4580
|
+
path6.resolve(workspaceRoot, "dist", "esedre.mjs"),
|
|
4581
|
+
path6.resolve(workspaceRoot, "../esedre/dist", "esedre.mjs"),
|
|
4582
|
+
path6.resolve(__dirname, "esedre.mjs"),
|
|
4583
|
+
path6.resolve(__dirname, "..", "dist", "esedre.mjs"),
|
|
4584
|
+
path6.resolve(__dirname, "..", "..", "dist", "esedre.mjs"),
|
|
4585
|
+
path6.resolve(__dirname, "..", "..", "bin", "esedre.js"),
|
|
4586
|
+
path6.resolve(__dirname, "..", "..", "bin", "esedre.ts"),
|
|
4587
|
+
path6.resolve(__dirname, "..", "esedre.mjs"),
|
|
4588
|
+
path6.resolve(__dirname, "../../../esedre/dist", "esedre.mjs")
|
|
4095
4589
|
].filter(Boolean);
|
|
4096
|
-
const cliPath = cliCandidates.find((c) =>
|
|
4097
|
-
if (!
|
|
4590
|
+
const cliPath = cliCandidates.find((c) => fs6.existsSync(c) && (c.endsWith(".mjs") || c.endsWith(".js") || c.endsWith(".ts"))) || cliCandidates[0];
|
|
4591
|
+
if (!fs6.existsSync(cliPath)) {
|
|
4098
4592
|
throw new Error(`Could not locate Esedre executable. Searched candidates: ${cliCandidates.join(", ")}`);
|
|
4099
4593
|
}
|
|
4100
4594
|
const child = spawn(process.execPath, [cliPath, "start", "--foreground", "--port", String(port)], {
|
|
@@ -4106,7 +4600,7 @@ async function startDaemon(options = {}) {
|
|
|
4106
4600
|
});
|
|
4107
4601
|
child.unref();
|
|
4108
4602
|
try {
|
|
4109
|
-
|
|
4603
|
+
fs6.closeSync(logFd);
|
|
4110
4604
|
} catch {
|
|
4111
4605
|
}
|
|
4112
4606
|
const pid = child.pid;
|
|
@@ -4147,13 +4641,13 @@ async function startDaemon(options = {}) {
|
|
|
4147
4641
|
logFile
|
|
4148
4642
|
};
|
|
4149
4643
|
const tmpState = `${stateFile}.tmp.${Date.now()}`;
|
|
4150
|
-
|
|
4644
|
+
fs6.writeFileSync(tmpState, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
4151
4645
|
try {
|
|
4152
|
-
|
|
4646
|
+
fs6.renameSync(tmpState, stateFile);
|
|
4153
4647
|
} catch {
|
|
4154
|
-
|
|
4648
|
+
fs6.writeFileSync(stateFile, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
4155
4649
|
try {
|
|
4156
|
-
|
|
4650
|
+
fs6.unlinkSync(tmpState);
|
|
4157
4651
|
} catch {
|
|
4158
4652
|
}
|
|
4159
4653
|
}
|
|
@@ -4216,8 +4710,8 @@ async function stopDaemon(options = {}) {
|
|
|
4216
4710
|
}
|
|
4217
4711
|
}
|
|
4218
4712
|
try {
|
|
4219
|
-
if (
|
|
4220
|
-
|
|
4713
|
+
if (fs6.existsSync(stateFile)) {
|
|
4714
|
+
fs6.unlinkSync(stateFile);
|
|
4221
4715
|
}
|
|
4222
4716
|
} catch {
|
|
4223
4717
|
}
|
|
@@ -4254,7 +4748,7 @@ async function getDaemonStatus(options = {}) {
|
|
|
4254
4748
|
}
|
|
4255
4749
|
if (state && !isProcessAlive(state.pid)) {
|
|
4256
4750
|
try {
|
|
4257
|
-
|
|
4751
|
+
fs6.unlinkSync(stateFile);
|
|
4258
4752
|
} catch {
|
|
4259
4753
|
}
|
|
4260
4754
|
}
|
|
@@ -4272,17 +4766,34 @@ function printDaemonLogs(options = {}) {
|
|
|
4272
4766
|
const port = resolvePorts(void 0, { port: options.port }).gateway;
|
|
4273
4767
|
const workspaceRoot = options.workspaceRoot || process.cwd();
|
|
4274
4768
|
const logFile = getDaemonLogFile(port, workspaceRoot);
|
|
4275
|
-
if (!
|
|
4769
|
+
if (!fs6.existsSync(logFile)) {
|
|
4276
4770
|
console.log(`\x1B[33mNo logs found at: ${logFile}\x1B[0m`);
|
|
4277
4771
|
return;
|
|
4278
4772
|
}
|
|
4279
|
-
const content = fs5.readFileSync(logFile, "utf-8");
|
|
4280
4773
|
const count = options.lines || 40;
|
|
4774
|
+
let content = "";
|
|
4775
|
+
try {
|
|
4776
|
+
const stats = fs6.statSync(logFile);
|
|
4777
|
+
const maxReadBytes = Math.min(stats.size, Math.max(256 * 1024, count * 2048));
|
|
4778
|
+
if (stats.size <= maxReadBytes) {
|
|
4779
|
+
content = fs6.readFileSync(logFile, "utf-8");
|
|
4780
|
+
} else {
|
|
4781
|
+
const fd = fs6.openSync(logFile, "r");
|
|
4782
|
+
const buffer = Buffer.alloc(maxReadBytes);
|
|
4783
|
+
fs6.readSync(fd, buffer, 0, maxReadBytes, stats.size - maxReadBytes);
|
|
4784
|
+
fs6.closeSync(fd);
|
|
4785
|
+
content = buffer.toString("utf-8");
|
|
4786
|
+
}
|
|
4787
|
+
} catch {
|
|
4788
|
+
content = fs6.readFileSync(logFile, "utf-8");
|
|
4789
|
+
}
|
|
4281
4790
|
const allLines = content.split(/\r?\n/);
|
|
4282
|
-
const
|
|
4791
|
+
const sanitizedLines = allLines.slice(-count).map((line) => {
|
|
4792
|
+
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, "");
|
|
4793
|
+
});
|
|
4283
4794
|
console.log(`\x1B[36m--- Esedre Daemon Logs (${logFile}) ---\x1B[0m
|
|
4284
4795
|
`);
|
|
4285
|
-
console.log(
|
|
4796
|
+
console.log(sanitizedLines.join("\n"));
|
|
4286
4797
|
}
|
|
4287
4798
|
|
|
4288
4799
|
// src/upgrade.ts
|
|
@@ -4489,13 +5000,13 @@ When Esedre MCP is active in your agent session (\`.agents/mcp_config.json\`), u
|
|
|
4489
5000
|
|
|
4490
5001
|
| Tool | Purpose | Key Arguments |
|
|
4491
5002
|
|---|---|---|
|
|
4492
|
-
| \`esedre_list_tickets\` | List roadmap tickets with filters | \`project\` (e.g. \`Profe\`, \`Esedre\`, \`Alce\`), \`status\`, \`type\`, \`search\` |
|
|
5003
|
+
| \`esedre_list_tickets\` | List roadmap tickets with filters | \`project\` (e.g. \`Profe\`, \`Esedre\`, \`Alce\`), \`status\`, \`type\`, \`priority\`, \`milestone\`, \`isBlocked\`, \`linkedTo\`, \`search\`, \`includePlan\` |
|
|
4493
5004
|
| \`esedre_get_ticket\` | Full specification, summary, comments, revision & sha1 | \`ticketId\` (numeric e.g. \`96\` or compound e.g. \`Profe-96\`) |
|
|
4494
5005
|
| \`esedre_get_plan\` | Active implementation plan markdown | \`ticketId\` (numeric or compound) |
|
|
4495
5006
|
| \`esedre_save_plan\` | Save implementation plan markdown with OCC | \`ticketId\`, \`planMarkdown\`, \`lastHash\` |
|
|
4496
5007
|
| \`esedre_create_ticket\` | Mint a new ticket with auto sequential ID | \`title\` (max 48 chars), \`type\`, \`project\`, \`effort\`, \`summary\`, \`detail\` |
|
|
4497
5008
|
| \`esedre_update_ticket\` | Update ticket attributes with OCC | \`ticketId\`, \`status\`, \`type\`, \`title\`, \`complexity\`, \`effort\`, \`inDevelopment\`, \`featureFlag\`, \`lastHash\` |
|
|
4498
|
-
| \`esedre_add_comment\` | Append developer or agent comment | \`ticketId\`, \`text\`, \`author\` |
|
|
5009
|
+
| \`esedre_add_comment\` | Append developer or agent comment | \`ticketId\`, \`text\`, \`file\`, \`author\` |
|
|
4499
5010
|
|
|
4500
5011
|
## 2. Core CLI Commands (Fallback via \`run_command\`)
|
|
4501
5012
|
|
|
@@ -4533,6 +5044,7 @@ Both \`esedre\` and \`ese\` work interchangeably:
|
|
|
4533
5044
|
### Append Comments or Notes
|
|
4534
5045
|
\`\`\`bash
|
|
4535
5046
|
.esedre/ese comment <ticketId> "Verified implementation." --author "Agent"
|
|
5047
|
+
.esedre/ese comment <ticketId> --file <commentFilePath> --author "Agent"
|
|
4536
5048
|
.esedre/ese comment <ticketId> --text "Verified implementation." --author "Agent"
|
|
4537
5049
|
\`\`\`
|
|
4538
5050
|
|
|
@@ -4588,8 +5100,8 @@ If the host application declares any or all of the 12 core design tokens on \`:r
|
|
|
4588
5100
|
- The URL hash is preserved across hard page refreshes (F5) without falling back to \`#/chat\` or other views.
|
|
4589
5101
|
`;
|
|
4590
5102
|
function plantWrappers(targetDir) {
|
|
4591
|
-
const esedreDir =
|
|
4592
|
-
|
|
5103
|
+
const esedreDir = path7.join(targetDir, ".esedre");
|
|
5104
|
+
fs7.mkdirSync(esedreDir, { recursive: true });
|
|
4593
5105
|
const wrappers = [
|
|
4594
5106
|
{ name: "esedre.cmd", content: WRAPPER_CMD, exec: false },
|
|
4595
5107
|
{ name: "esedre.ps1", content: WRAPPER_PS1, exec: false },
|
|
@@ -4599,24 +5111,24 @@ function plantWrappers(targetDir) {
|
|
|
4599
5111
|
{ name: "ese", content: ESE_WRAPPER_BASH, exec: true }
|
|
4600
5112
|
];
|
|
4601
5113
|
for (const w of wrappers) {
|
|
4602
|
-
const fullPath =
|
|
4603
|
-
|
|
5114
|
+
const fullPath = path7.join(esedreDir, w.name);
|
|
5115
|
+
fs7.writeFileSync(fullPath, w.content, "utf-8");
|
|
4604
5116
|
if (w.exec) {
|
|
4605
5117
|
try {
|
|
4606
|
-
|
|
5118
|
+
fs7.chmodSync(fullPath, 493);
|
|
4607
5119
|
} catch {
|
|
4608
5120
|
}
|
|
4609
5121
|
}
|
|
4610
5122
|
}
|
|
4611
5123
|
}
|
|
4612
5124
|
function setupMcpConfig(targetDir) {
|
|
4613
|
-
const agentsDir =
|
|
4614
|
-
|
|
4615
|
-
const mcpConfigPath =
|
|
5125
|
+
const agentsDir = path7.join(targetDir, ".agents");
|
|
5126
|
+
fs7.mkdirSync(agentsDir, { recursive: true });
|
|
5127
|
+
const mcpConfigPath = path7.join(agentsDir, "mcp_config.json");
|
|
4616
5128
|
let configObj = { mcpServers: {} };
|
|
4617
|
-
if (
|
|
5129
|
+
if (fs7.existsSync(mcpConfigPath)) {
|
|
4618
5130
|
try {
|
|
4619
|
-
configObj = JSON.parse(
|
|
5131
|
+
configObj = JSON.parse(fs7.readFileSync(mcpConfigPath, "utf-8"));
|
|
4620
5132
|
if (!configObj.mcpServers) configObj.mcpServers = {};
|
|
4621
5133
|
} catch {
|
|
4622
5134
|
}
|
|
@@ -4633,28 +5145,28 @@ function setupMcpConfig(targetDir) {
|
|
|
4633
5145
|
};
|
|
4634
5146
|
}
|
|
4635
5147
|
try {
|
|
4636
|
-
|
|
5148
|
+
fs7.writeFileSync(mcpConfigPath, JSON.stringify(configObj, null, 2) + "\n", "utf-8");
|
|
4637
5149
|
return true;
|
|
4638
5150
|
} catch {
|
|
4639
5151
|
return false;
|
|
4640
5152
|
}
|
|
4641
5153
|
}
|
|
4642
5154
|
function setupSkill(targetDir, options = {}) {
|
|
4643
|
-
const skillDir =
|
|
4644
|
-
|
|
4645
|
-
const skillFile =
|
|
4646
|
-
if (!
|
|
4647
|
-
|
|
5155
|
+
const skillDir = path7.join(targetDir, ".agents", "skills", "esedre");
|
|
5156
|
+
fs7.mkdirSync(skillDir, { recursive: true });
|
|
5157
|
+
const skillFile = path7.join(skillDir, "SKILL.md");
|
|
5158
|
+
if (!fs7.existsSync(skillFile)) {
|
|
5159
|
+
fs7.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
|
|
4648
5160
|
return { status: "MISSING", updated: true };
|
|
4649
5161
|
}
|
|
4650
|
-
const currentContent =
|
|
5162
|
+
const currentContent = fs7.readFileSync(skillFile, "utf-8");
|
|
4651
5163
|
const status = classifyContent(currentContent, ESEDRE_SKILL_TEMPLATE, HISTORIC_SKILL_HASHES);
|
|
4652
5164
|
if (status === "LATEST" && !options.force) {
|
|
4653
5165
|
return { status, updated: false };
|
|
4654
5166
|
}
|
|
4655
5167
|
const hasBannedOrDeprecatedTerms = currentContent.includes("companion agent") || currentContent.includes("companion LLM") || currentContent.includes("tools/esedre.mjs");
|
|
4656
5168
|
if (options.force || status === "HISTORIC_DEFAULT" || status === "MISSING" || hasBannedOrDeprecatedTerms) {
|
|
4657
|
-
|
|
5169
|
+
fs7.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
|
|
4658
5170
|
return { status: "LATEST", updated: true };
|
|
4659
5171
|
}
|
|
4660
5172
|
return { status, updated: false };
|
|
@@ -4668,8 +5180,8 @@ var VITE_CONFIG_FILES = [
|
|
|
4668
5180
|
];
|
|
4669
5181
|
function findViteConfig(targetDir) {
|
|
4670
5182
|
for (const name of VITE_CONFIG_FILES) {
|
|
4671
|
-
const fullPath =
|
|
4672
|
-
if (
|
|
5183
|
+
const fullPath = path7.join(targetDir, name);
|
|
5184
|
+
if (fs7.existsSync(fullPath)) {
|
|
4673
5185
|
return fullPath;
|
|
4674
5186
|
}
|
|
4675
5187
|
}
|
|
@@ -4733,10 +5245,10 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
|
|
|
4733
5245
|
recommendedProxySnippet: snippet
|
|
4734
5246
|
};
|
|
4735
5247
|
}
|
|
4736
|
-
const relativeName =
|
|
5248
|
+
const relativeName = path7.basename(configPath);
|
|
4737
5249
|
let content = "";
|
|
4738
5250
|
try {
|
|
4739
|
-
content =
|
|
5251
|
+
content = fs7.readFileSync(configPath, "utf-8");
|
|
4740
5252
|
} catch {
|
|
4741
5253
|
return {
|
|
4742
5254
|
status: "MANUAL_REQUIRED",
|
|
@@ -4767,7 +5279,7 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
|
|
|
4767
5279
|
};
|
|
4768
5280
|
}
|
|
4769
5281
|
try {
|
|
4770
|
-
|
|
5282
|
+
fs7.writeFileSync(configPath, injected, "utf-8");
|
|
4771
5283
|
return {
|
|
4772
5284
|
status: "CONFIGURED",
|
|
4773
5285
|
configFile: relativeName,
|
|
@@ -4782,15 +5294,15 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
|
|
|
4782
5294
|
}
|
|
4783
5295
|
}
|
|
4784
5296
|
function configureWorkspace(targetDir, options = {}) {
|
|
4785
|
-
const esedreDir =
|
|
4786
|
-
if (!
|
|
4787
|
-
|
|
5297
|
+
const esedreDir = path7.join(targetDir, ".esedre");
|
|
5298
|
+
if (!fs7.existsSync(esedreDir)) {
|
|
5299
|
+
fs7.mkdirSync(esedreDir, { recursive: true });
|
|
4788
5300
|
}
|
|
4789
|
-
const esedreJsonPath =
|
|
5301
|
+
const esedreJsonPath = path7.join(esedreDir, "esedre.json");
|
|
4790
5302
|
let existingRaw = null;
|
|
4791
|
-
if (
|
|
5303
|
+
if (fs7.existsSync(esedreJsonPath)) {
|
|
4792
5304
|
try {
|
|
4793
|
-
existingRaw = JSON.parse(
|
|
5305
|
+
existingRaw = JSON.parse(fs7.readFileSync(esedreJsonPath, "utf-8"));
|
|
4794
5306
|
} catch {
|
|
4795
5307
|
}
|
|
4796
5308
|
}
|
|
@@ -4826,16 +5338,16 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
4826
5338
|
port: options.port || void 0
|
|
4827
5339
|
};
|
|
4828
5340
|
}
|
|
4829
|
-
|
|
5341
|
+
fs7.writeFileSync(esedreJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
4830
5342
|
let projectRegistered = false;
|
|
4831
5343
|
if (projectCode) {
|
|
4832
|
-
const hasExistingTicketsDir =
|
|
4833
|
-
const ticketsDir =
|
|
4834
|
-
if (!hasExistingTicketsDir && !
|
|
4835
|
-
|
|
5344
|
+
const hasExistingTicketsDir = fs7.existsSync(path7.join(targetDir, "tickets")) || fs7.existsSync(path7.join(targetDir, "src", "data", "planning", "tickets"));
|
|
5345
|
+
const ticketsDir = path7.join(esedreDir, "tickets");
|
|
5346
|
+
if (!hasExistingTicketsDir && !fs7.existsSync(ticketsDir)) {
|
|
5347
|
+
fs7.mkdirSync(ticketsDir, { recursive: true });
|
|
4836
5348
|
}
|
|
4837
|
-
const inRepoPJson =
|
|
4838
|
-
if (!
|
|
5349
|
+
const inRepoPJson = path7.join(esedreDir, "project.json");
|
|
5350
|
+
if (!fs7.existsSync(inRepoPJson)) {
|
|
4839
5351
|
const projDesc = {
|
|
4840
5352
|
id: 1,
|
|
4841
5353
|
code: projectCode.toUpperCase(),
|
|
@@ -4847,14 +5359,14 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
4847
5359
|
border: "border-cyan-500/40"
|
|
4848
5360
|
}
|
|
4849
5361
|
};
|
|
4850
|
-
|
|
5362
|
+
fs7.writeFileSync(inRepoPJson, JSON.stringify(projDesc, null, 2) + "\n", "utf-8");
|
|
4851
5363
|
projectRegistered = true;
|
|
4852
5364
|
} else if (projectName) {
|
|
4853
5365
|
try {
|
|
4854
|
-
const existing = JSON.parse(
|
|
5366
|
+
const existing = JSON.parse(fs7.readFileSync(inRepoPJson, "utf-8"));
|
|
4855
5367
|
if (existing.name !== projectName) {
|
|
4856
5368
|
existing.name = projectName;
|
|
4857
|
-
|
|
5369
|
+
fs7.writeFileSync(inRepoPJson, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
4858
5370
|
}
|
|
4859
5371
|
} catch {
|
|
4860
5372
|
}
|
|
@@ -4904,26 +5416,26 @@ var ESEDRE_HUB_AGENTS_TEMPLATE = `# Agent Guidelines: esedre-data
|
|
|
4904
5416
|
When inspecting or managing tickets for any project (such as \`Profe\`, \`Esedre\`, or \`Alce\`), navigate to that project's workspace and utilize its local \`.esedre/snapshot.json\`, Esedre MCP tools, or \`.esedre/ese\` CLI wrapper.
|
|
4905
5417
|
`;
|
|
4906
5418
|
function initHub(targetDir) {
|
|
4907
|
-
const resolved =
|
|
4908
|
-
if (!
|
|
4909
|
-
|
|
5419
|
+
const resolved = path7.resolve(targetDir);
|
|
5420
|
+
if (!fs7.existsSync(resolved)) {
|
|
5421
|
+
fs7.mkdirSync(resolved, { recursive: true });
|
|
4910
5422
|
}
|
|
4911
|
-
const projectsDir =
|
|
5423
|
+
const projectsDir = path7.join(resolved, "projects");
|
|
4912
5424
|
let projectsDirCreated = false;
|
|
4913
|
-
if (!
|
|
4914
|
-
|
|
5425
|
+
if (!fs7.existsSync(projectsDir)) {
|
|
5426
|
+
fs7.mkdirSync(projectsDir, { recursive: true });
|
|
4915
5427
|
projectsDirCreated = true;
|
|
4916
5428
|
}
|
|
4917
|
-
const projectsJsonPath =
|
|
5429
|
+
const projectsJsonPath = path7.join(resolved, "projects.json");
|
|
4918
5430
|
let projectsJsonCreated = false;
|
|
4919
|
-
if (!
|
|
4920
|
-
|
|
5431
|
+
if (!fs7.existsSync(projectsJsonPath)) {
|
|
5432
|
+
fs7.writeFileSync(projectsJsonPath, "[]\n", "utf-8");
|
|
4921
5433
|
projectsJsonCreated = true;
|
|
4922
5434
|
}
|
|
4923
|
-
const agentsMdPath =
|
|
5435
|
+
const agentsMdPath = path7.join(resolved, "AGENTS.md");
|
|
4924
5436
|
let agentsMdCreated = false;
|
|
4925
|
-
if (!
|
|
4926
|
-
|
|
5437
|
+
if (!fs7.existsSync(agentsMdPath)) {
|
|
5438
|
+
fs7.writeFileSync(agentsMdPath, ESEDRE_HUB_AGENTS_TEMPLATE, "utf-8");
|
|
4927
5439
|
agentsMdCreated = true;
|
|
4928
5440
|
}
|
|
4929
5441
|
return {
|
|
@@ -4933,17 +5445,126 @@ function initHub(targetDir) {
|
|
|
4933
5445
|
hubDir: resolved.replace(/\\/g, "/")
|
|
4934
5446
|
};
|
|
4935
5447
|
}
|
|
5448
|
+
function isWrapperOutdated(esedreCmdContent) {
|
|
5449
|
+
if (!esedreCmdContent || typeof esedreCmdContent !== "string") {
|
|
5450
|
+
return true;
|
|
5451
|
+
}
|
|
5452
|
+
if (/goto\s+:?done\b/i.test(esedreCmdContent)) {
|
|
5453
|
+
return true;
|
|
5454
|
+
}
|
|
5455
|
+
if (/^:done\b/im.test(esedreCmdContent)) {
|
|
5456
|
+
return true;
|
|
5457
|
+
}
|
|
5458
|
+
if (!esedreCmdContent.includes("call :run %*") || !/^:run\b/im.test(esedreCmdContent)) {
|
|
5459
|
+
return true;
|
|
5460
|
+
}
|
|
5461
|
+
return false;
|
|
5462
|
+
}
|
|
5463
|
+
function findOutdatedWrappers(currentWorkspaceRoot, globalConfig) {
|
|
5464
|
+
const outdated = [];
|
|
5465
|
+
const checkedDirs = /* @__PURE__ */ new Set();
|
|
5466
|
+
const checkDir = (dir, name) => {
|
|
5467
|
+
const resolved = path7.resolve(expandHome(dir));
|
|
5468
|
+
if (checkedDirs.has(resolved.toLowerCase())) return;
|
|
5469
|
+
checkedDirs.add(resolved.toLowerCase());
|
|
5470
|
+
if (!fs7.existsSync(resolved)) return;
|
|
5471
|
+
const cmdPath = path7.join(resolved, ".esedre", "esedre.cmd");
|
|
5472
|
+
if (fs7.existsSync(cmdPath)) {
|
|
5473
|
+
try {
|
|
5474
|
+
const content = fs7.readFileSync(cmdPath, "utf-8");
|
|
5475
|
+
if (isWrapperOutdated(content)) {
|
|
5476
|
+
outdated.push({ name, dir: resolved, wrapperPath: cmdPath });
|
|
5477
|
+
}
|
|
5478
|
+
} catch {
|
|
5479
|
+
}
|
|
5480
|
+
}
|
|
5481
|
+
};
|
|
5482
|
+
if (currentWorkspaceRoot) {
|
|
5483
|
+
checkDir(currentWorkspaceRoot, path7.basename(currentWorkspaceRoot));
|
|
5484
|
+
}
|
|
5485
|
+
if (globalConfig?.projects) {
|
|
5486
|
+
for (const [code, p] of Object.entries(globalConfig.projects)) {
|
|
5487
|
+
checkDir(p, code);
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
return outdated;
|
|
5491
|
+
}
|
|
5492
|
+
async function upgradeAllWorkspaces(options = {}) {
|
|
5493
|
+
const globalCfg = options.globalConfig !== void 0 ? options.globalConfig : readGlobalConfig();
|
|
5494
|
+
const workspaceMap = /* @__PURE__ */ new Map();
|
|
5495
|
+
if (options.currentWorkspaceRoot && fs7.existsSync(options.currentWorkspaceRoot)) {
|
|
5496
|
+
const hasEsedre = fs7.existsSync(path7.join(options.currentWorkspaceRoot, ".esedre")) || fs7.existsSync(path7.join(options.currentWorkspaceRoot, "esedre.json"));
|
|
5497
|
+
if (hasEsedre) {
|
|
5498
|
+
const disc = findEsedreConfig(options.currentWorkspaceRoot, { fallbackToGlobal: true });
|
|
5499
|
+
workspaceMap.set(
|
|
5500
|
+
path7.resolve(options.currentWorkspaceRoot),
|
|
5501
|
+
disc.config?.projectCode || path7.basename(options.currentWorkspaceRoot)
|
|
5502
|
+
);
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
if (globalCfg?.projects) {
|
|
5506
|
+
for (const [code, p] of Object.entries(globalCfg.projects)) {
|
|
5507
|
+
const resolved = path7.resolve(expandHome(p));
|
|
5508
|
+
if (fs7.existsSync(resolved)) {
|
|
5509
|
+
if (!workspaceMap.has(resolved)) {
|
|
5510
|
+
workspaceMap.set(resolved, code);
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
const results = [];
|
|
5516
|
+
for (const [targetDir, name] of workspaceMap.entries()) {
|
|
5517
|
+
try {
|
|
5518
|
+
const targetDiscovered = findEsedreConfig(targetDir, { fallbackToGlobal: true });
|
|
5519
|
+
const projectCode = targetDiscovered.config?.projectCode;
|
|
5520
|
+
const res = configureWorkspace(targetDir, {
|
|
5521
|
+
projectCode,
|
|
5522
|
+
allowedProjects: targetDiscovered.config?.allowedProjects,
|
|
5523
|
+
port: targetDiscovered.config?.port,
|
|
5524
|
+
force: options.force
|
|
5525
|
+
});
|
|
5526
|
+
let totalTickets;
|
|
5527
|
+
if (projectCode) {
|
|
5528
|
+
try {
|
|
5529
|
+
const targetStorage = new SecurityFilter(
|
|
5530
|
+
new FilesystemStorageAdapter(targetDir, targetDiscovered.config),
|
|
5531
|
+
targetDiscovered.config
|
|
5532
|
+
);
|
|
5533
|
+
const snapshot = await generateProjectSnapshot(targetStorage, projectCode, targetDir);
|
|
5534
|
+
totalTickets = snapshot.totalTickets;
|
|
5535
|
+
} catch {
|
|
5536
|
+
}
|
|
5537
|
+
}
|
|
5538
|
+
results.push({
|
|
5539
|
+
dir: targetDir,
|
|
5540
|
+
name,
|
|
5541
|
+
projectCode,
|
|
5542
|
+
upgrade: res,
|
|
5543
|
+
totalTickets
|
|
5544
|
+
});
|
|
5545
|
+
} catch (err) {
|
|
5546
|
+
results.push({
|
|
5547
|
+
dir: targetDir,
|
|
5548
|
+
name,
|
|
5549
|
+
upgrade: {},
|
|
5550
|
+
error: err.message || String(err)
|
|
5551
|
+
});
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5554
|
+
const totalUpgraded = results.filter((r) => !r.error && r.upgrade.wrappersPlanted).length;
|
|
5555
|
+
return { workspaces: results, totalUpgraded };
|
|
5556
|
+
}
|
|
4936
5557
|
|
|
4937
5558
|
// src/server/gateway.ts
|
|
4938
|
-
import
|
|
5559
|
+
import fs10 from "node:fs";
|
|
4939
5560
|
import http4 from "node:http";
|
|
4940
|
-
import
|
|
5561
|
+
import path10 from "node:path";
|
|
4941
5562
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
4942
5563
|
|
|
4943
5564
|
// src/server/uiServer.ts
|
|
4944
5565
|
import http2 from "node:http";
|
|
4945
|
-
import
|
|
4946
|
-
import
|
|
5566
|
+
import fs8 from "node:fs";
|
|
5567
|
+
import path8 from "node:path";
|
|
4947
5568
|
var MIME_TYPES = {
|
|
4948
5569
|
".html": "text/html; charset=utf-8",
|
|
4949
5570
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -4973,20 +5594,27 @@ function startUiServer(port, webDir) {
|
|
|
4973
5594
|
if (!reqPath || reqPath === "/") {
|
|
4974
5595
|
reqPath = "/index.html";
|
|
4975
5596
|
}
|
|
4976
|
-
const safePath =
|
|
4977
|
-
let filePath =
|
|
4978
|
-
|
|
4979
|
-
|
|
5597
|
+
const safePath = path8.normalize(reqPath).replace(/^(\.\.[/\\])+/, "");
|
|
5598
|
+
let filePath = path8.join(webDir, safePath);
|
|
5599
|
+
const resolvedFilePath = path8.resolve(filePath);
|
|
5600
|
+
const resolvedWebDir = path8.resolve(webDir);
|
|
5601
|
+
if (!resolvedFilePath.startsWith(resolvedWebDir)) {
|
|
5602
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
5603
|
+
res.end("Access Denied: Path escapes web root.");
|
|
5604
|
+
return;
|
|
5605
|
+
}
|
|
5606
|
+
if (!fs8.existsSync(filePath) || fs8.statSync(filePath).isDirectory()) {
|
|
5607
|
+
filePath = path8.join(webDir, "index.html");
|
|
4980
5608
|
}
|
|
4981
|
-
if (!
|
|
5609
|
+
if (!fs8.existsSync(filePath)) {
|
|
4982
5610
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
4983
5611
|
res.end("Esedre UI: index.html not found. Run npm run build first.");
|
|
4984
5612
|
return;
|
|
4985
5613
|
}
|
|
4986
|
-
const ext =
|
|
5614
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
4987
5615
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
4988
5616
|
try {
|
|
4989
|
-
const content =
|
|
5617
|
+
const content = fs8.readFileSync(filePath);
|
|
4990
5618
|
res.writeHead(200, { "Content-Type": contentType });
|
|
4991
5619
|
res.end(content);
|
|
4992
5620
|
} catch (err) {
|
|
@@ -5008,6 +5636,8 @@ function startUiServer(port, webDir) {
|
|
|
5008
5636
|
|
|
5009
5637
|
// src/server/apiServer.ts
|
|
5010
5638
|
import http3 from "node:http";
|
|
5639
|
+
import fs9 from "node:fs";
|
|
5640
|
+
import path9 from "node:path";
|
|
5011
5641
|
var MAX_JSON_BODY_BYTES = 10 * 1024 * 1024;
|
|
5012
5642
|
function readJsonBody(req) {
|
|
5013
5643
|
return new Promise((resolve, reject) => {
|
|
@@ -5043,6 +5673,7 @@ function sendJson(res, status, data) {
|
|
|
5043
5673
|
}
|
|
5044
5674
|
var cachedAllData = /* @__PURE__ */ new Map();
|
|
5045
5675
|
var CACHE_TTL_MS = 5e3;
|
|
5676
|
+
var MAX_CACHE_ENTRIES = 30;
|
|
5046
5677
|
function invalidateApiCache() {
|
|
5047
5678
|
cachedAllData.clear();
|
|
5048
5679
|
}
|
|
@@ -5142,6 +5773,8 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5142
5773
|
const detailsMap = {};
|
|
5143
5774
|
const plansMap = {};
|
|
5144
5775
|
const commentsMap = {};
|
|
5776
|
+
const answersMap = {};
|
|
5777
|
+
const inlineCommentsMap = {};
|
|
5145
5778
|
for (const t of tickets) {
|
|
5146
5779
|
const numKey = String(t.meta.id);
|
|
5147
5780
|
const code = t.projectDescriptor?.code || t.meta.project || "Profe";
|
|
@@ -5167,6 +5800,18 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5167
5800
|
plansMap[numKey] = t.planMarkdown;
|
|
5168
5801
|
}
|
|
5169
5802
|
}
|
|
5803
|
+
if (t.answers && Object.keys(t.answers).length > 0) {
|
|
5804
|
+
answersMap[prefixedKey] = t.answers;
|
|
5805
|
+
if (!answersMap[numKey] || code.toUpperCase() === "PROF") {
|
|
5806
|
+
answersMap[numKey] = t.answers;
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
if (t.inlineComments && t.inlineComments.length > 0) {
|
|
5810
|
+
inlineCommentsMap[prefixedKey] = t.inlineComments;
|
|
5811
|
+
if (!inlineCommentsMap[numKey] || code.toUpperCase() === "PROF") {
|
|
5812
|
+
inlineCommentsMap[numKey] = t.inlineComments;
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5170
5815
|
}
|
|
5171
5816
|
const projects = await reqStorage.getProjects();
|
|
5172
5817
|
const responsePayload = {
|
|
@@ -5176,11 +5821,15 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5176
5821
|
details: detailsMap,
|
|
5177
5822
|
plans: plansMap,
|
|
5178
5823
|
comments: commentsMap,
|
|
5179
|
-
answers:
|
|
5180
|
-
inlineComments:
|
|
5824
|
+
answers: answersMap,
|
|
5825
|
+
inlineComments: inlineCommentsMap,
|
|
5181
5826
|
planHistory: {},
|
|
5182
5827
|
ticketHistory: {}
|
|
5183
5828
|
};
|
|
5829
|
+
if (cachedAllData.size >= MAX_CACHE_ENTRIES) {
|
|
5830
|
+
const oldestKey = cachedAllData.keys().next().value;
|
|
5831
|
+
if (oldestKey) cachedAllData.delete(oldestKey);
|
|
5832
|
+
}
|
|
5184
5833
|
cachedAllData.set(cacheKey, { data: responsePayload, timestamp: now });
|
|
5185
5834
|
sendJson(res, 200, responsePayload);
|
|
5186
5835
|
return true;
|
|
@@ -5230,11 +5879,88 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5230
5879
|
return true;
|
|
5231
5880
|
}
|
|
5232
5881
|
if (pathname === "/api/planning/inline-comments") {
|
|
5233
|
-
|
|
5882
|
+
const ticketParam = url.searchParams.get("ticketId") || url.searchParams.get("id");
|
|
5883
|
+
if (ticketParam) {
|
|
5884
|
+
const inlines = reqStorage.getInlineComments ? await reqStorage.getInlineComments(ticketParam) : [];
|
|
5885
|
+
sendJson(res, 200, inlines);
|
|
5886
|
+
return true;
|
|
5887
|
+
}
|
|
5888
|
+
const tickets = await reqStorage.listTickets({});
|
|
5889
|
+
const inlinesMap = {};
|
|
5890
|
+
for (const t of tickets) {
|
|
5891
|
+
if (t.inlineComments && t.inlineComments.length > 0) {
|
|
5892
|
+
inlinesMap[String(t.meta.id)] = t.inlineComments;
|
|
5893
|
+
const code = t.projectDescriptor?.code || t.meta.project;
|
|
5894
|
+
if (code) {
|
|
5895
|
+
inlinesMap[`${code}-${t.meta.id}`] = t.inlineComments;
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
sendJson(res, 200, inlinesMap);
|
|
5234
5900
|
return true;
|
|
5235
5901
|
}
|
|
5236
5902
|
if (pathname === "/api/planning/answers") {
|
|
5237
|
-
|
|
5903
|
+
const ticketParam = url.searchParams.get("ticketId") || url.searchParams.get("id");
|
|
5904
|
+
if (ticketParam) {
|
|
5905
|
+
const answers = await reqStorage.getAnswers(ticketParam);
|
|
5906
|
+
sendJson(res, 200, answers);
|
|
5907
|
+
return true;
|
|
5908
|
+
}
|
|
5909
|
+
const tickets = await reqStorage.listTickets({});
|
|
5910
|
+
const answersMap = {};
|
|
5911
|
+
for (const t of tickets) {
|
|
5912
|
+
if (t.answers && Object.keys(t.answers).length > 0) {
|
|
5913
|
+
answersMap[String(t.meta.id)] = t.answers;
|
|
5914
|
+
const code = t.projectDescriptor?.code || t.meta.project;
|
|
5915
|
+
if (code) {
|
|
5916
|
+
answersMap[`${code}-${t.meta.id}`] = t.answers;
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5920
|
+
sendJson(res, 200, answersMap);
|
|
5921
|
+
return true;
|
|
5922
|
+
}
|
|
5923
|
+
if (pathname === "/api/planning/attachment") {
|
|
5924
|
+
const ticketId = url.searchParams.get("ticketId") || url.searchParams.get("id");
|
|
5925
|
+
const file = url.searchParams.get("file");
|
|
5926
|
+
if (!ticketId || !file) {
|
|
5927
|
+
sendJson(res, 400, { error: "ticketId and file parameters are required." });
|
|
5928
|
+
return true;
|
|
5929
|
+
}
|
|
5930
|
+
const ticket = await reqStorage.getTicket(ticketId);
|
|
5931
|
+
if (!ticket) {
|
|
5932
|
+
sendJson(res, 404, { error: "Ticket not found." });
|
|
5933
|
+
return true;
|
|
5934
|
+
}
|
|
5935
|
+
const filePath = reqStorage.getAttachmentPath ? reqStorage.getAttachmentPath(ticketId, file) : null;
|
|
5936
|
+
if (!filePath || !fs9.existsSync(filePath)) {
|
|
5937
|
+
sendJson(res, 404, { error: "Attachment not found." });
|
|
5938
|
+
return true;
|
|
5939
|
+
}
|
|
5940
|
+
const stat = fs9.statSync(filePath);
|
|
5941
|
+
if (!stat.isFile()) {
|
|
5942
|
+
sendJson(res, 404, { error: "Attachment not found." });
|
|
5943
|
+
return true;
|
|
5944
|
+
}
|
|
5945
|
+
const ext = path9.extname(filePath).toLowerCase();
|
|
5946
|
+
const mimeTypes = {
|
|
5947
|
+
".png": "image/png",
|
|
5948
|
+
".jpg": "image/jpeg",
|
|
5949
|
+
".jpeg": "image/jpeg",
|
|
5950
|
+
".gif": "image/gif",
|
|
5951
|
+
".webp": "image/webp",
|
|
5952
|
+
".svg": "image/svg+xml",
|
|
5953
|
+
".mp4": "video/mp4",
|
|
5954
|
+
".webm": "video/webm",
|
|
5955
|
+
".mov": "video/quicktime"
|
|
5956
|
+
};
|
|
5957
|
+
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
5958
|
+
res.writeHead(200, {
|
|
5959
|
+
"Content-Type": contentType,
|
|
5960
|
+
"Content-Length": stat.size,
|
|
5961
|
+
"Cache-Control": "public, max-age=86400"
|
|
5962
|
+
});
|
|
5963
|
+
fs9.createReadStream(filePath).pipe(res);
|
|
5238
5964
|
return true;
|
|
5239
5965
|
}
|
|
5240
5966
|
if (pathname === "/api/planning/plan-history") {
|
|
@@ -5348,6 +6074,10 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5348
6074
|
}
|
|
5349
6075
|
if (pathname === "/api/planning/plans") {
|
|
5350
6076
|
const { ticketId, planMarkdown, lastHash } = body;
|
|
6077
|
+
if (!ticketId) {
|
|
6078
|
+
sendJson(res, 400, { error: "ticketId is required." });
|
|
6079
|
+
return true;
|
|
6080
|
+
}
|
|
5351
6081
|
invalidateApiCache();
|
|
5352
6082
|
await reqStorage.savePlan(ticketId, planMarkdown, lastHash);
|
|
5353
6083
|
sendJson(res, 200, { success: true });
|
|
@@ -5355,6 +6085,10 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5355
6085
|
}
|
|
5356
6086
|
if (pathname === "/api/planning/comments") {
|
|
5357
6087
|
const { ticketId, text, author } = body;
|
|
6088
|
+
if (!ticketId) {
|
|
6089
|
+
sendJson(res, 400, { error: "ticketId is required." });
|
|
6090
|
+
return true;
|
|
6091
|
+
}
|
|
5358
6092
|
invalidateApiCache();
|
|
5359
6093
|
await reqStorage.addComment(ticketId, { author: author || "Developer", text });
|
|
5360
6094
|
const ticket = await reqStorage.getTicket(ticketId);
|
|
@@ -5363,12 +6097,22 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5363
6097
|
}
|
|
5364
6098
|
if (pathname === "/api/planning/update-meta") {
|
|
5365
6099
|
const { ticketId, updates, lastHash } = body;
|
|
6100
|
+
if (!ticketId) {
|
|
6101
|
+
sendJson(res, 400, { error: "ticketId is required." });
|
|
6102
|
+
return true;
|
|
6103
|
+
}
|
|
6104
|
+
invalidateApiCache();
|
|
5366
6105
|
const updated = await reqStorage.updateTicket(ticketId, updates, lastHash);
|
|
5367
6106
|
sendJson(res, 200, { meta: updated.meta });
|
|
5368
6107
|
return true;
|
|
5369
6108
|
}
|
|
5370
6109
|
if (pathname === "/api/planning/toggle-flag") {
|
|
5371
6110
|
const { ticketId, flagged } = body;
|
|
6111
|
+
if (!ticketId) {
|
|
6112
|
+
sendJson(res, 400, { error: "ticketId is required." });
|
|
6113
|
+
return true;
|
|
6114
|
+
}
|
|
6115
|
+
invalidateApiCache();
|
|
5372
6116
|
const updated = await reqStorage.updateTicket(ticketId, {
|
|
5373
6117
|
featureFlag: flagged ? "chat_enhanced" : ""
|
|
5374
6118
|
});
|
|
@@ -5377,10 +6121,56 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5377
6121
|
}
|
|
5378
6122
|
if (pathname === "/api/planning/details") {
|
|
5379
6123
|
const { ticketId, detailMarkdown, metaUpdates } = body;
|
|
5380
|
-
if (
|
|
5381
|
-
|
|
6124
|
+
if (!ticketId) {
|
|
6125
|
+
sendJson(res, 400, { error: "ticketId is required." });
|
|
6126
|
+
return true;
|
|
6127
|
+
}
|
|
6128
|
+
invalidateApiCache();
|
|
6129
|
+
const result = await reqStorage.saveDetail(ticketId, detailMarkdown || "", metaUpdates);
|
|
6130
|
+
sendJson(res, 200, result);
|
|
6131
|
+
return true;
|
|
6132
|
+
}
|
|
6133
|
+
if (pathname === "/api/planning/answers") {
|
|
6134
|
+
const { ticketId, questionIndex, answer } = body;
|
|
6135
|
+
if (ticketId === void 0 || questionIndex === void 0 || answer === void 0) {
|
|
6136
|
+
sendJson(res, 400, { error: "ticketId, questionIndex, and answer are required." });
|
|
6137
|
+
return true;
|
|
6138
|
+
}
|
|
6139
|
+
invalidateApiCache();
|
|
6140
|
+
const updated = await reqStorage.saveAnswer(ticketId, Number(questionIndex), String(answer));
|
|
6141
|
+
sendJson(res, 200, updated);
|
|
6142
|
+
return true;
|
|
6143
|
+
}
|
|
6144
|
+
if (pathname === "/api/planning/save-inline-comment" || pathname === "/api/planning/inline-comments") {
|
|
6145
|
+
const { ticketId, selectedText, comment, author } = body;
|
|
6146
|
+
if (!ticketId || !selectedText || !comment) {
|
|
6147
|
+
sendJson(res, 400, { error: "ticketId, selectedText, and comment are required." });
|
|
6148
|
+
return true;
|
|
6149
|
+
}
|
|
6150
|
+
invalidateApiCache();
|
|
6151
|
+
const inlineComments = reqStorage.saveInlineComment ? await reqStorage.saveInlineComment(ticketId, selectedText, comment, author) : [];
|
|
6152
|
+
sendJson(res, 200, { inlineComments });
|
|
6153
|
+
return true;
|
|
6154
|
+
}
|
|
6155
|
+
if (pathname === "/api/planning/upload-attachment") {
|
|
6156
|
+
const { ticketId, filename, base64Data } = body;
|
|
6157
|
+
if (!ticketId || !filename || !base64Data) {
|
|
6158
|
+
sendJson(res, 400, { error: "ticketId, filename, and base64Data are required." });
|
|
6159
|
+
return true;
|
|
5382
6160
|
}
|
|
5383
|
-
|
|
6161
|
+
const base64Content = base64Data.replace(/^data:[^;]+;base64,/, "");
|
|
6162
|
+
const buffer = Buffer.from(base64Content, "base64");
|
|
6163
|
+
if (!reqStorage.saveAttachment) {
|
|
6164
|
+
sendJson(res, 500, { error: "Attachments are not supported by the storage engine." });
|
|
6165
|
+
return true;
|
|
6166
|
+
}
|
|
6167
|
+
invalidateApiCache();
|
|
6168
|
+
const saved = await reqStorage.saveAttachment(ticketId, filename, buffer);
|
|
6169
|
+
sendJson(res, 200, {
|
|
6170
|
+
success: true,
|
|
6171
|
+
filename: saved.filename,
|
|
6172
|
+
url: `/api/planning/attachment?ticketId=${encodeURIComponent(ticketId)}&file=${encodeURIComponent(saved.filename)}`
|
|
6173
|
+
});
|
|
5384
6174
|
return true;
|
|
5385
6175
|
}
|
|
5386
6176
|
if (pathname === "/api/planning/milestones") {
|
|
@@ -5461,35 +6251,35 @@ function startApiServer(port, storage, workspaceRoot) {
|
|
|
5461
6251
|
|
|
5462
6252
|
// src/server/gateway.ts
|
|
5463
6253
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
5464
|
-
var __dirname2 =
|
|
6254
|
+
var __dirname2 = path10.dirname(__filename2);
|
|
5465
6255
|
function resolveWebDir(explicitWebDir, workspaceRoot) {
|
|
5466
|
-
if (explicitWebDir &&
|
|
6256
|
+
if (explicitWebDir && fs10.existsSync(path10.join(explicitWebDir, "index.html"))) {
|
|
5467
6257
|
return explicitWebDir;
|
|
5468
6258
|
}
|
|
5469
6259
|
const candidates = [
|
|
5470
6260
|
// 1. Packaged layout right next to executable (e.g. tools/web)
|
|
5471
|
-
|
|
6261
|
+
path10.resolve(__dirname2, "web"),
|
|
5472
6262
|
// 2. From workspace root: <workspaceRoot>/dist/web
|
|
5473
|
-
workspaceRoot ?
|
|
6263
|
+
workspaceRoot ? path10.resolve(workspaceRoot, "dist", "web") : "",
|
|
5474
6264
|
// 3. In-repo legacy subfolder: <workspaceRoot>/esedre/dist/web
|
|
5475
|
-
workspaceRoot ?
|
|
6265
|
+
workspaceRoot ? path10.resolve(workspaceRoot, "esedre", "dist", "web") : "",
|
|
5476
6266
|
// 4. Sibling standalone repository from workspace root: <workspaceRoot>/../esedre/dist/web
|
|
5477
|
-
workspaceRoot ?
|
|
6267
|
+
workspaceRoot ? path10.resolve(workspaceRoot, "..", "esedre", "dist", "web") : "",
|
|
5478
6268
|
// 5. Packaged layout inside dist/web
|
|
5479
|
-
|
|
6269
|
+
path10.resolve(__dirname2, "dist", "web"),
|
|
5480
6270
|
// 6. Sibling standalone repository from __dirname: <__dirname>/../../esedre/dist/web
|
|
5481
|
-
|
|
6271
|
+
path10.resolve(__dirname2, "..", "..", "esedre", "dist", "web"),
|
|
5482
6272
|
// 7. Source tree layout: esedre/src/server/ -> esedre/dist/web
|
|
5483
|
-
|
|
6273
|
+
path10.resolve(__dirname2, "..", "..", "dist", "web"),
|
|
5484
6274
|
// 8. Legacy layout from __dirname
|
|
5485
|
-
|
|
6275
|
+
path10.resolve(__dirname2, "..", "esedre", "dist", "web")
|
|
5486
6276
|
].filter(Boolean);
|
|
5487
6277
|
for (const candidate of candidates) {
|
|
5488
|
-
if (
|
|
6278
|
+
if (fs10.existsSync(path10.join(candidate, "index.html"))) {
|
|
5489
6279
|
return candidate;
|
|
5490
6280
|
}
|
|
5491
6281
|
}
|
|
5492
|
-
return candidates[0] ||
|
|
6282
|
+
return candidates[0] || path10.resolve(__dirname2, "web");
|
|
5493
6283
|
}
|
|
5494
6284
|
function proxyRequest(req, res, targetPort, rewritePath) {
|
|
5495
6285
|
const targetPath = rewritePath ? rewritePath(req.url || "/") : req.url || "/";
|
|
@@ -5609,10 +6399,10 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
|
|
|
5609
6399
|
${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
6400
|
Mint a new roadmap ticket with sequential numeric ID.
|
|
5611
6401
|
|
|
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]
|
|
6402
|
+
${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
6403
|
Mutate ticket status, type, priority, title, complexity, effort, active state, or feature flag with OCC protection.
|
|
5614
6404
|
|
|
5615
|
-
${colors.bold}comment${colors.reset} <id> ["<text>"] [--text "..."] [--author "..."] [--json]
|
|
6405
|
+
${colors.bold}comment${colors.reset} <id> ["<text>"] [--file <path>] [--text "..."] [--author "..."] [--json]
|
|
5616
6406
|
Append a research finding, test verification, or note to ticket history.
|
|
5617
6407
|
|
|
5618
6408
|
${colors.bold}link${colors.reset} <sourceId> <relation> <targetId> [--author "..."] [--json]
|
|
@@ -5661,8 +6451,9 @@ ${colors.bold}DEVELOPER ADMINISTRATION (Human Setup & Configuration):${colors.re
|
|
|
5661
6451
|
\u2022 remove <target> Unlink a project or data hub
|
|
5662
6452
|
\u2022 set <key> <val> Update a central configuration setting
|
|
5663
6453
|
|
|
5664
|
-
${colors.bold}upgrade${colors.reset} [<path>] [--force|-f] [--json]
|
|
6454
|
+
${colors.bold}upgrade${colors.reset} [<path>] [--all|-a] [--force|-f] [--json]
|
|
5665
6455
|
Upgrade workspace configuration schema, wrappers, and agent skills. Refreshes snapshot.
|
|
6456
|
+
Pass --all (-a) to upgrade all registered projects in central ~/.esedre/config.json.
|
|
5666
6457
|
Pass --force to overwrite customized skills with the latest official template.
|
|
5667
6458
|
|
|
5668
6459
|
${colors.bold}projects${colors.reset} [--json]
|
|
@@ -5675,6 +6466,7 @@ ${colors.bold}DEVELOPER ADMINISTRATION (Human Setup & Configuration):${colors.re
|
|
|
5675
6466
|
Safely rename a project code on disk, updating hub folders, manifests, ticket references, and workspace configs.
|
|
5676
6467
|
|
|
5677
6468
|
${colors.bold}OPTIONS:${colors.reset}
|
|
6469
|
+
-a, --all Apply operation across all registered projects in central configuration.
|
|
5678
6470
|
-p, --project <code> Project code (e.g. CORE, ALCE, DOCS).
|
|
5679
6471
|
-n, --name <name> Project display name (e.g. "Alce Web Reader").
|
|
5680
6472
|
-t, --type <type> Ticket type ('Feature', 'Platform', 'Tools', 'Idea', 'Bug').
|
|
@@ -5686,8 +6478,8 @@ ${colors.bold}OPTIONS:${colors.reset}
|
|
|
5686
6478
|
--to <targetId> Target ticket ID for link/unlink commands.
|
|
5687
6479
|
--blocked Filter tickets that are blocked by uncompleted tickets.
|
|
5688
6480
|
--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)
|
|
6481
|
+
--detail "<md>" Specification markdown for ticket detail during create or update.
|
|
6482
|
+
--file <path> Path to markdown file for detail (create, update), implementation plan (plan), or comment (comment).
|
|
5691
6483
|
--json Output raw machine-readable JSON (strongly recommended for autonomous LLM coding agents).
|
|
5692
6484
|
--last-hash <hash> Optimistic concurrency control: last known sha1 hash of the ticket from 'get'.
|
|
5693
6485
|
--force Bypass optimistic concurrency last-hash conflict checks on writes.
|
|
@@ -5752,6 +6544,7 @@ function parseArgs(rawArgs) {
|
|
|
5752
6544
|
if (flags["q"] && !flags["search"]) flags["search"] = flags["q"];
|
|
5753
6545
|
if (flags["r"] && !flags["relation"]) flags["relation"] = flags["r"];
|
|
5754
6546
|
if (flags["f"] && !flags["foreground"]) flags["foreground"] = flags["f"];
|
|
6547
|
+
if (flags["a"] && !flags["all"]) flags["all"] = flags["a"];
|
|
5755
6548
|
return { command, positionals, flags };
|
|
5756
6549
|
}
|
|
5757
6550
|
function printDuplicateProjectWarnings(storage, isJson) {
|
|
@@ -5759,7 +6552,7 @@ function printDuplicateProjectWarnings(storage, isJson) {
|
|
|
5759
6552
|
const warnings = storage.getDuplicateProjectWarnings ? storage.getDuplicateProjectWarnings() : [];
|
|
5760
6553
|
for (const warn of warnings) {
|
|
5761
6554
|
console.error(
|
|
5762
|
-
`${colors.yellow}\u26A0\uFE0F Warning: Duplicate project code '${warn.code}' detected across multiple data hubs (${
|
|
6555
|
+
`${colors.yellow}\u26A0\uFE0F Warning: Duplicate project code '${warn.code}' detected across multiple data hubs (${path11.basename(warn.firstHub)} and ${path11.basename(warn.duplicateHub)}). Duplicate project codes across hubs are not supported; only '${path11.basename(warn.firstHub)}' is active.${colors.reset}`
|
|
5763
6556
|
);
|
|
5764
6557
|
}
|
|
5765
6558
|
}
|
|
@@ -5802,7 +6595,7 @@ async function main() {
|
|
|
5802
6595
|
switch (command) {
|
|
5803
6596
|
case "init": {
|
|
5804
6597
|
const rawTarget = positionals[0];
|
|
5805
|
-
const targetDir = rawTarget ?
|
|
6598
|
+
const targetDir = rawTarget ? path11.resolve(rawTarget) : discovered.workspaceRoot;
|
|
5806
6599
|
const rawHubFlag = flags["hub"];
|
|
5807
6600
|
const hubArg = typeof rawHubFlag === "string" ? rawHubFlag : void 0;
|
|
5808
6601
|
const isHubRequested = Boolean(rawHubFlag);
|
|
@@ -5881,7 +6674,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
5881
6674
|
console.error(`${colors.red}Error: ${nameVal.error}${colors.reset}`);
|
|
5882
6675
|
process.exit(1);
|
|
5883
6676
|
}
|
|
5884
|
-
const isTargetHub =
|
|
6677
|
+
const isTargetHub = fs11.existsSync(path11.join(targetDir, "projects.json")) || fs11.existsSync(path11.join(targetDir, "projects")) && !fs11.existsSync(path11.join(targetDir, ".esedre", "esedre.json"));
|
|
5885
6678
|
const isWorkspaceless = isHubRequested || isTargetHub || Boolean(discovered.config?.projectCode) && !rawTarget && discovered.config?.projectCode?.toLowerCase() !== projectCode.toLowerCase();
|
|
5886
6679
|
let projectNewlyRegistered = false;
|
|
5887
6680
|
let registeredProj;
|
|
@@ -5952,7 +6745,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
5952
6745
|
const viteConfigPath = findViteConfig(targetDir);
|
|
5953
6746
|
if (viteConfigPath && setupProxy === void 0 && !isJson) {
|
|
5954
6747
|
try {
|
|
5955
|
-
const rawContent =
|
|
6748
|
+
const rawContent = fs11.readFileSync(viteConfigPath, "utf-8");
|
|
5956
6749
|
const alreadyConfigured = rawContent.includes("'/esedre'") || rawContent.includes('"/esedre"');
|
|
5957
6750
|
if (!alreadyConfigured) {
|
|
5958
6751
|
if (isYes) {
|
|
@@ -5962,7 +6755,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
5962
6755
|
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
5963
6756
|
try {
|
|
5964
6757
|
const answer = await rl.question(
|
|
5965
|
-
`${colors.cyan}?${colors.reset} Detected ${colors.bold}${
|
|
6758
|
+
`${colors.cyan}?${colors.reset} Detected ${colors.bold}${path11.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
6759
|
);
|
|
5967
6760
|
const trimmed = answer.trim().toLowerCase();
|
|
5968
6761
|
setupProxy = trimmed === "" || trimmed === "y" || trimmed === "yes";
|
|
@@ -5978,10 +6771,13 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
5978
6771
|
let hubRelPath = void 0;
|
|
5979
6772
|
if (rawHub) {
|
|
5980
6773
|
const singleHub = Array.isArray(rawHub) ? rawHub[0] : rawHub;
|
|
5981
|
-
const resolvedHub =
|
|
5982
|
-
if (
|
|
5983
|
-
const rel =
|
|
5984
|
-
|
|
6774
|
+
const resolvedHub = path11.resolve(expandHome(singleHub));
|
|
6775
|
+
if (fs11.existsSync(resolvedHub)) {
|
|
6776
|
+
const rel = path11.relative(targetDir, resolvedHub).replace(/\\/g, "/");
|
|
6777
|
+
const isSubdir = !rel.startsWith("..") && !path11.isAbsolute(rel);
|
|
6778
|
+
if (isSubdir || Boolean(flags["data-dir"])) {
|
|
6779
|
+
hubRelPath = rel.startsWith(".") ? rel : "./" + rel;
|
|
6780
|
+
}
|
|
5985
6781
|
}
|
|
5986
6782
|
}
|
|
5987
6783
|
const res = initWorkspace(targetDir, {
|
|
@@ -6152,7 +6948,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
6152
6948
|
}
|
|
6153
6949
|
console.log(`
|
|
6154
6950
|
${colors.bold}${colors.cyan}Esedre Central Configuration${colors.reset}`);
|
|
6155
|
-
console.log(` \u2022 Config File: ${configPath} ${
|
|
6951
|
+
console.log(` \u2022 Config File: ${configPath} ${fs11.existsSync(configPath) ? colors.green + "(active)" + colors.reset : colors.dim + "(default)" + colors.reset}`);
|
|
6156
6952
|
console.log(` \u2022 Default Port: ${globalCfg.port || DEFAULT_ESEDRE_PORT}`);
|
|
6157
6953
|
console.log(`
|
|
6158
6954
|
${colors.bold}Data Hubs (dataDir):${colors.reset}`);
|
|
@@ -6184,9 +6980,46 @@ ${colors.bold}Commands:${colors.reset}`);
|
|
|
6184
6980
|
return;
|
|
6185
6981
|
}
|
|
6186
6982
|
case "upgrade": {
|
|
6187
|
-
const
|
|
6188
|
-
const targetDiscovered = targetDir === discovered.workspaceRoot ? discovered : findEsedreConfig(targetDir, { fallbackToGlobal: true });
|
|
6983
|
+
const isAll = Boolean(flags["all"] || flags["a"]);
|
|
6189
6984
|
const isForce = Boolean(flags["force"] || flags["f"]);
|
|
6985
|
+
if (isAll) {
|
|
6986
|
+
const globalCfg = readGlobalConfig();
|
|
6987
|
+
const allRes = await upgradeAllWorkspaces({
|
|
6988
|
+
currentWorkspaceRoot: discovered.workspaceRoot,
|
|
6989
|
+
globalConfig: globalCfg,
|
|
6990
|
+
force: isForce
|
|
6991
|
+
});
|
|
6992
|
+
if (isJson) {
|
|
6993
|
+
console.log(JSON.stringify(allRes, null, 2));
|
|
6994
|
+
return;
|
|
6995
|
+
}
|
|
6996
|
+
if (allRes.workspaces.length === 0) {
|
|
6997
|
+
console.log(`${colors.yellow}No registered workspaces found to upgrade.${colors.reset}`);
|
|
6998
|
+
return;
|
|
6999
|
+
}
|
|
7000
|
+
console.log(`
|
|
7001
|
+
${colors.bold}${colors.green}\u2714 Esedre multi-workspace upgrade completed across ${allRes.workspaces.length} workspace(s):${colors.reset}`);
|
|
7002
|
+
for (const w of allRes.workspaces) {
|
|
7003
|
+
if (w.error) {
|
|
7004
|
+
console.log(` \u2022 ${colors.red}\u2717 ${w.name} (${w.dir}): ${w.error}${colors.reset}`);
|
|
7005
|
+
} else {
|
|
7006
|
+
const snapStr = w.totalTickets !== void 0 ? ` (${w.totalTickets} tickets projected)` : "";
|
|
7007
|
+
console.log(` \u2022 ${colors.bold}${w.name}${colors.reset} (${w.dir}):`);
|
|
7008
|
+
console.log(` - Wrappers updated in .esedre/`);
|
|
7009
|
+
if (w.totalTickets !== void 0) {
|
|
7010
|
+
console.log(` - Refreshed .esedre/snapshot.json${snapStr}`);
|
|
7011
|
+
}
|
|
7012
|
+
if (w.upgrade.skillConfigured) {
|
|
7013
|
+
console.log(` - Updated .agents/skills/esedre/SKILL.md`);
|
|
7014
|
+
} else if (w.upgrade.skillStatus === "CUSTOMIZED") {
|
|
7015
|
+
console.log(` - Preserved customized .agents/skills/esedre/SKILL.md`);
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
}
|
|
7019
|
+
return;
|
|
7020
|
+
}
|
|
7021
|
+
const targetDir = positionals[0] ? path11.resolve(positionals[0]) : discovered.workspaceRoot;
|
|
7022
|
+
const targetDiscovered = targetDir === discovered.workspaceRoot ? discovered : findEsedreConfig(targetDir, { fallbackToGlobal: true });
|
|
6190
7023
|
const res = configureWorkspace(targetDir, {
|
|
6191
7024
|
projectCode: targetDiscovered.config?.projectCode,
|
|
6192
7025
|
allowedProjects: targetDiscovered.config?.allowedProjects,
|
|
@@ -6293,8 +7126,10 @@ ${colors.dim}Shutting down Esedre cluster...${colors.reset}`);
|
|
|
6293
7126
|
const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
|
|
6294
7127
|
const port = resolvePorts(discovered.config, { port: flagPort }).gateway;
|
|
6295
7128
|
const status = await getDaemonStatus({ port, workspaceRoot: discovered.workspaceRoot });
|
|
7129
|
+
const globalCfg = readGlobalConfig() || discovered.config;
|
|
7130
|
+
const outdatedWrappers = findOutdatedWrappers(discovered.workspaceRoot, globalCfg);
|
|
6296
7131
|
if (isJson) {
|
|
6297
|
-
console.log(JSON.stringify(status, null, 2));
|
|
7132
|
+
console.log(JSON.stringify({ ...status, outdatedWrappers }, null, 2));
|
|
6298
7133
|
} else {
|
|
6299
7134
|
if (status.running) {
|
|
6300
7135
|
console.log(`${colors.bold}${colors.green}\u25CF Esedre daemon is RUNNING${colors.reset}`);
|
|
@@ -6312,10 +7147,14 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
|
|
|
6312
7147
|
}
|
|
6313
7148
|
} else {
|
|
6314
7149
|
console.log(`${colors.yellow}\u25CB Esedre daemon is STOPPED (port ${status.port})${colors.reset}`);
|
|
6315
|
-
if (status.logFile &&
|
|
7150
|
+
if (status.logFile && fs11.existsSync(status.logFile)) {
|
|
6316
7151
|
console.log(` \u2022 Recent Logs: ${status.logFile}`);
|
|
6317
7152
|
}
|
|
6318
7153
|
}
|
|
7154
|
+
if (outdatedWrappers.length > 0) {
|
|
7155
|
+
console.log(`
|
|
7156
|
+
${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}`);
|
|
7157
|
+
}
|
|
6319
7158
|
}
|
|
6320
7159
|
return;
|
|
6321
7160
|
}
|
|
@@ -6513,11 +7352,11 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
6513
7352
|
if (setPlan || filePath) {
|
|
6514
7353
|
let content = setPlan;
|
|
6515
7354
|
if (filePath) {
|
|
6516
|
-
if (!
|
|
7355
|
+
if (!fs11.existsSync(filePath)) {
|
|
6517
7356
|
console.error(`${colors.red}Error: Plan file "${filePath}" not found.${colors.reset}`);
|
|
6518
7357
|
process.exit(1);
|
|
6519
7358
|
}
|
|
6520
|
-
content =
|
|
7359
|
+
content = fs11.readFileSync(filePath, "utf-8");
|
|
6521
7360
|
}
|
|
6522
7361
|
await storage.savePlan(id, content, lastHash);
|
|
6523
7362
|
if (isJson) {
|
|
@@ -6589,8 +7428,8 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
6589
7428
|
const milestone = flags["milestone"] || flags["m"] || void 0;
|
|
6590
7429
|
let detailMarkdown = detailArg;
|
|
6591
7430
|
if (fileArg) {
|
|
6592
|
-
const filePath =
|
|
6593
|
-
if (!
|
|
7431
|
+
const filePath = path11.resolve(fileArg);
|
|
7432
|
+
if (!fs11.existsSync(filePath)) {
|
|
6594
7433
|
if (isJson) {
|
|
6595
7434
|
console.error(JSON.stringify({ error: `Detail file "${fileArg}" not found` }));
|
|
6596
7435
|
} else {
|
|
@@ -6598,7 +7437,7 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
6598
7437
|
}
|
|
6599
7438
|
process.exit(1);
|
|
6600
7439
|
}
|
|
6601
|
-
detailMarkdown =
|
|
7440
|
+
detailMarkdown = fs11.readFileSync(filePath, "utf-8");
|
|
6602
7441
|
}
|
|
6603
7442
|
const created = await storage.createTicket({
|
|
6604
7443
|
title,
|
|
@@ -6664,6 +7503,22 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
6664
7503
|
if (inDev !== void 0) updates.isActivePlanning = inDev;
|
|
6665
7504
|
if (flag) updates.featureFlag = flag;
|
|
6666
7505
|
if (rawMilestone !== void 0) updates.milestone = rawMilestone;
|
|
7506
|
+
const detailArg = flags["detail"];
|
|
7507
|
+
const fileArg = flags["file"];
|
|
7508
|
+
let detailMarkdown = detailArg ? normalizeDashesAndMojibake(detailArg) : void 0;
|
|
7509
|
+
if (fileArg) {
|
|
7510
|
+
const filePath = path11.resolve(fileArg);
|
|
7511
|
+
if (!fs11.existsSync(filePath)) {
|
|
7512
|
+
if (isJson) {
|
|
7513
|
+
console.error(JSON.stringify({ error: `Detail file "${fileArg}" not found` }));
|
|
7514
|
+
} else {
|
|
7515
|
+
console.error(`${colors.red}Error: Detail file "${fileArg}" not found.${colors.reset}`);
|
|
7516
|
+
}
|
|
7517
|
+
process.exit(1);
|
|
7518
|
+
}
|
|
7519
|
+
detailMarkdown = normalizeDashesAndMojibake(fs11.readFileSync(filePath, "utf-8"));
|
|
7520
|
+
}
|
|
7521
|
+
if (detailMarkdown !== void 0) updates.detailMarkdown = detailMarkdown;
|
|
6667
7522
|
const updated = await storage.updateTicket(lookupKey, updates, lastHash);
|
|
6668
7523
|
if (isJson) {
|
|
6669
7524
|
console.log(JSON.stringify(updated, null, 2));
|
|
@@ -6819,10 +7674,30 @@ ${colors.bold}Tickets:${colors.reset}`);
|
|
|
6819
7674
|
}
|
|
6820
7675
|
const projectFlag = flags["project"];
|
|
6821
7676
|
const lookupKey = projectFlag && /^\d+$/.test(idStr) ? `${projectFlag}-${idStr}` : idStr;
|
|
6822
|
-
|
|
6823
|
-
const
|
|
7677
|
+
let text;
|
|
7678
|
+
const filePath = flags["file"];
|
|
7679
|
+
if (filePath) {
|
|
7680
|
+
const resolvedPath = path11.resolve(process.cwd(), filePath);
|
|
7681
|
+
if (!fs11.existsSync(resolvedPath)) {
|
|
7682
|
+
console.error(`${colors.red}Error: File not found: ${filePath}${colors.reset}`);
|
|
7683
|
+
process.exit(1);
|
|
7684
|
+
}
|
|
7685
|
+
if (!fs11.statSync(resolvedPath).isFile()) {
|
|
7686
|
+
console.error(`${colors.red}Error: Path is not a file: ${filePath}${colors.reset}`);
|
|
7687
|
+
process.exit(1);
|
|
7688
|
+
}
|
|
7689
|
+
const fileContent = fs11.readFileSync(resolvedPath, "utf-8");
|
|
7690
|
+
if (!fileContent.trim()) {
|
|
7691
|
+
console.error(`${colors.red}Error: Comment file is empty: ${filePath}${colors.reset}`);
|
|
7692
|
+
process.exit(1);
|
|
7693
|
+
}
|
|
7694
|
+
text = fileContent;
|
|
7695
|
+
} else {
|
|
7696
|
+
const positionalText = positionals.slice(1).join(" ").trim();
|
|
7697
|
+
text = flags["text"] || (positionalText || void 0);
|
|
7698
|
+
}
|
|
6824
7699
|
if (!text) {
|
|
6825
|
-
console.error(`${colors.red}Error: Comment text is required (e.g. ese comment 96
|
|
7700
|
+
console.error(`${colors.red}Error: Comment text or --file is required (e.g. ese comment 96 --file note.md or ese comment 96 "Verified tests")${colors.reset}`);
|
|
6826
7701
|
process.exit(1);
|
|
6827
7702
|
}
|
|
6828
7703
|
const author = flags["author"] || "User";
|