esedre 0.1.4 → 0.1.6
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/LICENSE +327 -327
- package/README.md +263 -214
- package/dist/esedre.mjs +1239 -364
- package/dist/web/assets/{index-BqjKnasV.js → index-B-37KY54.js} +2 -2
- package/dist/web/assets/index-CGwIfCgR.css +2 -0
- package/dist/web/esedre-icon-inverted.svg +18 -18
- package/dist/web/esedre-icon.svg +18 -18
- package/dist/web/favicon.svg +18 -18
- package/dist/web/index.html +18 -18
- package/docs/assets/esedre-dashboard-dark.png +0 -0
- package/docs/assets/esedre-dashboard-light.png +0 -0
- package/package.json +66 -66
- package/dist/web/assets/index-CezKPkES.css +0 -2
package/dist/esedre.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/esedre.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs9 from "node:fs";
|
|
5
|
+
import path9 from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/storage/filesystem.ts
|
|
8
8
|
import fs3 from "node:fs";
|
|
9
9
|
import path3 from "node:path";
|
|
10
10
|
|
|
11
11
|
// src/types.ts
|
|
12
|
+
var CURRENT_ESEDRE_VERSION = "0.1.6";
|
|
12
13
|
var EsedreConflictError = class extends Error {
|
|
13
14
|
constructor(ticketId, currentHash, lastHash) {
|
|
14
15
|
super(
|
|
@@ -27,9 +28,18 @@ var EsedreConflictError = class extends Error {
|
|
|
27
28
|
// src/config.ts
|
|
28
29
|
import fs from "node:fs";
|
|
29
30
|
import path from "node:path";
|
|
31
|
+
import os from "node:os";
|
|
30
32
|
var DEFAULT_ESEDRE_PORT = 5674;
|
|
31
|
-
var MAX_PROJECT_CODE_LENGTH =
|
|
32
|
-
var PROJECT_CODE_REGEX = /^[a-zA-Z0-9]{1,
|
|
33
|
+
var MAX_PROJECT_CODE_LENGTH = 7;
|
|
34
|
+
var PROJECT_CODE_REGEX = /^[a-zA-Z0-9]{1,7}$/;
|
|
35
|
+
var MAX_PROJECT_NAME_LENGTH = 48;
|
|
36
|
+
function resolvePorts(config, overrides) {
|
|
37
|
+
const globalConfig = readGlobalConfig();
|
|
38
|
+
const gateway = overrides?.gatewayPort ?? overrides?.port ?? overrides?.ports?.gateway ?? config?.ports?.gateway ?? config?.port ?? globalConfig?.ports?.gateway ?? globalConfig?.port ?? DEFAULT_ESEDRE_PORT;
|
|
39
|
+
const ui = overrides?.uiPort ?? overrides?.ports?.ui ?? config?.ports?.ui ?? globalConfig?.ports?.ui ?? gateway + 1;
|
|
40
|
+
const api = overrides?.apiPort ?? overrides?.ports?.api ?? config?.ports?.api ?? globalConfig?.ports?.api ?? gateway + 2;
|
|
41
|
+
return { gateway, ui, api };
|
|
42
|
+
}
|
|
33
43
|
var EsedreAuthorizationError = class extends Error {
|
|
34
44
|
constructor(projectCode, message) {
|
|
35
45
|
super(message || `Access Denied: Project '${projectCode}' is outside this workspace's authorized scope.`);
|
|
@@ -46,7 +56,7 @@ function validateProjectCode(code, existingCodes) {
|
|
|
46
56
|
if (!PROJECT_CODE_REGEX.test(trimmed)) {
|
|
47
57
|
return {
|
|
48
58
|
valid: false,
|
|
49
|
-
error: `Project code '${trimmed}' is invalid. It must be 1 to ${MAX_PROJECT_CODE_LENGTH} alphanumeric characters (e.g. CORE,
|
|
59
|
+
error: `Project code '${trimmed}' is invalid. It must be 1 to ${MAX_PROJECT_CODE_LENGTH} alphanumeric characters (e.g. CORE, ALCE, WEB).`
|
|
50
60
|
};
|
|
51
61
|
}
|
|
52
62
|
if (existingCodes && existingCodes.length > 0) {
|
|
@@ -61,6 +71,22 @@ function validateProjectCode(code, existingCodes) {
|
|
|
61
71
|
}
|
|
62
72
|
return { valid: true };
|
|
63
73
|
}
|
|
74
|
+
function validateProjectName(name) {
|
|
75
|
+
if (!name || typeof name !== "string") {
|
|
76
|
+
return { valid: false, error: "Project name is required." };
|
|
77
|
+
}
|
|
78
|
+
const trimmed = name.trim();
|
|
79
|
+
if (trimmed.length === 0) {
|
|
80
|
+
return { valid: false, error: "Project name cannot be empty." };
|
|
81
|
+
}
|
|
82
|
+
if (trimmed.length > MAX_PROJECT_NAME_LENGTH) {
|
|
83
|
+
return {
|
|
84
|
+
valid: false,
|
|
85
|
+
error: `Project name '${trimmed}' exceeds maximum length of ${MAX_PROJECT_NAME_LENGTH} characters.`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { valid: true };
|
|
89
|
+
}
|
|
64
90
|
function isProjectAuthorized(projectCode, allowedProjects) {
|
|
65
91
|
if (!allowedProjects || allowedProjects.length === 0) {
|
|
66
92
|
return true;
|
|
@@ -72,35 +98,277 @@ function isProjectAuthorized(projectCode, allowedProjects) {
|
|
|
72
98
|
const normalized = projectCode.trim().toUpperCase();
|
|
73
99
|
return normalizedList.includes(normalized);
|
|
74
100
|
}
|
|
75
|
-
function
|
|
101
|
+
function getGlobalConfigDir() {
|
|
102
|
+
if (process.env.ESEDRE_GLOBAL_DIR) {
|
|
103
|
+
return process.env.ESEDRE_GLOBAL_DIR;
|
|
104
|
+
}
|
|
105
|
+
return path.join(os.homedir(), ".esedre");
|
|
106
|
+
}
|
|
107
|
+
function getGlobalConfigPath() {
|
|
108
|
+
return path.join(getGlobalConfigDir(), "config.json");
|
|
109
|
+
}
|
|
110
|
+
function expandHome(filePath) {
|
|
111
|
+
if (!filePath) return filePath;
|
|
112
|
+
if (filePath === "~" || filePath.startsWith("~/") || filePath.startsWith("~\\")) {
|
|
113
|
+
return path.join(os.homedir(), filePath.slice(1));
|
|
114
|
+
}
|
|
115
|
+
return filePath;
|
|
116
|
+
}
|
|
117
|
+
function readGlobalConfig() {
|
|
118
|
+
const configPath = getGlobalConfigPath();
|
|
119
|
+
if (!fs.existsSync(configPath)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
124
|
+
return JSON.parse(raw);
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function writeGlobalConfig(config) {
|
|
130
|
+
const dir = getGlobalConfigDir();
|
|
131
|
+
if (!fs.existsSync(dir)) {
|
|
132
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
const configPath = getGlobalConfigPath();
|
|
135
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
136
|
+
try {
|
|
137
|
+
writeGlobalVersion(config.version || CURRENT_ESEDRE_VERSION);
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function getGlobalVersionPath() {
|
|
142
|
+
return path.join(getGlobalConfigDir(), "version");
|
|
143
|
+
}
|
|
144
|
+
function writeGlobalVersion(version = CURRENT_ESEDRE_VERSION) {
|
|
145
|
+
const dir = getGlobalConfigDir();
|
|
146
|
+
if (!fs.existsSync(dir)) {
|
|
147
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
148
|
+
}
|
|
149
|
+
fs.writeFileSync(getGlobalVersionPath(), version.trim() + "\n", "utf-8");
|
|
150
|
+
}
|
|
151
|
+
function addLocationToGlobalConfig(targetPath) {
|
|
152
|
+
const resolved = path.resolve(expandHome(targetPath));
|
|
153
|
+
if (!fs.existsSync(resolved)) {
|
|
154
|
+
throw new Error(`Target path does not exist: "${targetPath}" (resolved: ${resolved})`);
|
|
155
|
+
}
|
|
156
|
+
const current = readGlobalConfig() || { version: CURRENT_ESEDRE_VERSION, port: DEFAULT_ESEDRE_PORT };
|
|
157
|
+
const hasProjectsDir = fs.existsSync(path.join(resolved, "projects"));
|
|
158
|
+
const hasProjectsJson = fs.existsSync(path.join(resolved, "projects.json"));
|
|
159
|
+
if (hasProjectsDir || hasProjectsJson) {
|
|
160
|
+
let projectCount = 0;
|
|
161
|
+
if (hasProjectsJson) {
|
|
162
|
+
try {
|
|
163
|
+
const pjs = JSON.parse(fs.readFileSync(path.join(resolved, "projects.json"), "utf-8"));
|
|
164
|
+
if (Array.isArray(pjs)) projectCount = pjs.length;
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
} else if (hasProjectsDir) {
|
|
168
|
+
try {
|
|
169
|
+
projectCount = fs.readdirSync(path.join(resolved, "projects")).filter((e) => {
|
|
170
|
+
return fs.statSync(path.join(resolved, "projects", e)).isDirectory();
|
|
171
|
+
}).length;
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const currentDataDirs = Array.isArray(current.dataDir) ? current.dataDir : current.dataDir ? [current.dataDir] : [];
|
|
176
|
+
const normalizedResolved = resolved.replace(/\\/g, "/");
|
|
177
|
+
const existingIdx = currentDataDirs.findIndex(
|
|
178
|
+
(d) => path.resolve(expandHome(d)).replace(/\\/g, "/") === normalizedResolved
|
|
179
|
+
);
|
|
180
|
+
if (existingIdx === -1) {
|
|
181
|
+
currentDataDirs.push(normalizedResolved);
|
|
182
|
+
}
|
|
183
|
+
current.dataDir = currentDataDirs.length === 1 ? currentDataDirs[0] : currentDataDirs;
|
|
184
|
+
writeGlobalConfig(current);
|
|
185
|
+
return { type: "hub", path: normalizedResolved, projectCount };
|
|
186
|
+
}
|
|
187
|
+
const inRepoConfig = path.join(resolved, ".esedre", "esedre.json");
|
|
188
|
+
const inRepoRootConfig = path.join(resolved, "esedre.json");
|
|
189
|
+
const inRepoProjectJson = path.join(resolved, ".esedre", "project.json");
|
|
190
|
+
const inRepoRootProjectJson = path.join(resolved, "project.json");
|
|
191
|
+
let code;
|
|
192
|
+
let name;
|
|
193
|
+
for (const cand of [inRepoConfig, inRepoRootConfig, inRepoProjectJson, inRepoRootProjectJson]) {
|
|
194
|
+
if (fs.existsSync(cand)) {
|
|
195
|
+
try {
|
|
196
|
+
const parsed = JSON.parse(fs.readFileSync(cand, "utf-8"));
|
|
197
|
+
if (parsed.projectCode) code = parsed.projectCode;
|
|
198
|
+
if (parsed.code && !code) code = parsed.code;
|
|
199
|
+
if (parsed.projectName) name = parsed.projectName;
|
|
200
|
+
if (parsed.name && !name) name = parsed.name;
|
|
201
|
+
if (code) break;
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (code) {
|
|
207
|
+
const normalizedCode = code.toUpperCase();
|
|
208
|
+
const normalizedResolved = resolved.replace(/\\/g, "/");
|
|
209
|
+
if (!current.projects) {
|
|
210
|
+
current.projects = {};
|
|
211
|
+
}
|
|
212
|
+
current.projects[normalizedCode] = normalizedResolved;
|
|
213
|
+
writeGlobalConfig(current);
|
|
214
|
+
return { type: "project", code: normalizedCode, name, path: normalizedResolved };
|
|
215
|
+
}
|
|
216
|
+
return { type: "uninitialized", path: resolved.replace(/\\/g, "/") };
|
|
217
|
+
}
|
|
218
|
+
function removeLocationFromGlobalConfig(identifier) {
|
|
219
|
+
const current = readGlobalConfig();
|
|
220
|
+
if (!current) return { removed: false };
|
|
221
|
+
const trimmed = identifier.trim();
|
|
222
|
+
const normalizedId = trimmed.toUpperCase();
|
|
223
|
+
if (current.projects && current.projects[normalizedId]) {
|
|
224
|
+
const target = current.projects[normalizedId];
|
|
225
|
+
delete current.projects[normalizedId];
|
|
226
|
+
if (Object.keys(current.projects).length === 0) {
|
|
227
|
+
delete current.projects;
|
|
228
|
+
}
|
|
229
|
+
writeGlobalConfig(current);
|
|
230
|
+
return { removed: true, type: "project", target };
|
|
231
|
+
}
|
|
232
|
+
if (current.projects) {
|
|
233
|
+
for (const [code, p] of Object.entries(current.projects)) {
|
|
234
|
+
if (path.resolve(expandHome(p)).toLowerCase() === path.resolve(expandHome(trimmed)).toLowerCase()) {
|
|
235
|
+
delete current.projects[code];
|
|
236
|
+
if (Object.keys(current.projects).length === 0) {
|
|
237
|
+
delete current.projects;
|
|
238
|
+
}
|
|
239
|
+
writeGlobalConfig(current);
|
|
240
|
+
return { removed: true, type: "project", target: p };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (current.dataDir) {
|
|
245
|
+
const currentDataDirs = Array.isArray(current.dataDir) ? current.dataDir : [current.dataDir];
|
|
246
|
+
const initialLen = currentDataDirs.length;
|
|
247
|
+
const remaining = currentDataDirs.filter(
|
|
248
|
+
(d) => path.resolve(expandHome(d)).toLowerCase() !== path.resolve(expandHome(trimmed)).toLowerCase()
|
|
249
|
+
);
|
|
250
|
+
if (remaining.length !== initialLen) {
|
|
251
|
+
current.dataDir = remaining.length === 0 ? void 0 : remaining.length === 1 ? remaining[0] : remaining;
|
|
252
|
+
writeGlobalConfig(current);
|
|
253
|
+
return { removed: true, type: "hub", target: trimmed };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return { removed: false };
|
|
257
|
+
}
|
|
258
|
+
function getGlobalConfigKey(key) {
|
|
259
|
+
const current = readGlobalConfig();
|
|
260
|
+
if (!current) return void 0;
|
|
261
|
+
if (key.includes(".")) {
|
|
262
|
+
const parts = key.split(".");
|
|
263
|
+
let val = current;
|
|
264
|
+
for (const part of parts) {
|
|
265
|
+
if (val === void 0 || val === null) return void 0;
|
|
266
|
+
val = val[part];
|
|
267
|
+
}
|
|
268
|
+
return val;
|
|
269
|
+
}
|
|
270
|
+
return current[key];
|
|
271
|
+
}
|
|
272
|
+
function setGlobalConfigKey(key, value) {
|
|
273
|
+
const current = readGlobalConfig() || { version: CURRENT_ESEDRE_VERSION, port: DEFAULT_ESEDRE_PORT };
|
|
274
|
+
let parsedValue = value;
|
|
275
|
+
if ((key === "port" || key.endsWith(".port") || key.startsWith("ports.")) && typeof value === "string") {
|
|
276
|
+
const num = parseInt(value, 10);
|
|
277
|
+
if (!isNaN(num)) parsedValue = num;
|
|
278
|
+
}
|
|
279
|
+
if (key.includes(".")) {
|
|
280
|
+
const parts = key.split(".");
|
|
281
|
+
let obj = current;
|
|
282
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
283
|
+
if (!obj[parts[i]] || typeof obj[parts[i]] !== "object") {
|
|
284
|
+
obj[parts[i]] = {};
|
|
285
|
+
}
|
|
286
|
+
obj = obj[parts[i]];
|
|
287
|
+
}
|
|
288
|
+
obj[parts[parts.length - 1]] = parsedValue;
|
|
289
|
+
} else {
|
|
290
|
+
current[key] = parsedValue;
|
|
291
|
+
}
|
|
292
|
+
writeGlobalConfig(current);
|
|
293
|
+
return { key, value: parsedValue };
|
|
294
|
+
}
|
|
295
|
+
function findEsedreConfig(startDir, options) {
|
|
76
296
|
let current = path.resolve(startDir || process.cwd());
|
|
77
297
|
const candidates = [
|
|
78
298
|
[".esedre", "esedre.json"],
|
|
79
299
|
["esedre.json"]
|
|
80
300
|
];
|
|
301
|
+
let localConfig = null;
|
|
302
|
+
let localConfigPath = null;
|
|
303
|
+
let localWorkspaceRoot = current;
|
|
81
304
|
while (true) {
|
|
82
305
|
for (const segs of candidates) {
|
|
83
306
|
const candidatePath = path.join(current, ...segs);
|
|
84
307
|
if (fs.existsSync(candidatePath)) {
|
|
85
308
|
try {
|
|
86
309
|
const raw = fs.readFileSync(candidatePath, "utf-8");
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
workspaceRoot: current
|
|
92
|
-
};
|
|
310
|
+
localConfig = JSON.parse(raw);
|
|
311
|
+
localConfigPath = candidatePath;
|
|
312
|
+
localWorkspaceRoot = current;
|
|
313
|
+
break;
|
|
93
314
|
} catch (err) {
|
|
94
315
|
throw new Error(`Failed to parse Esedre configuration at ${candidatePath}: ${err.message}`);
|
|
95
316
|
}
|
|
96
317
|
}
|
|
97
318
|
}
|
|
319
|
+
if (localConfig) break;
|
|
98
320
|
const parent = path.dirname(current);
|
|
99
321
|
if (parent === current) {
|
|
100
322
|
break;
|
|
101
323
|
}
|
|
102
324
|
current = parent;
|
|
103
325
|
}
|
|
326
|
+
const globalConfig = readGlobalConfig();
|
|
327
|
+
if (localConfig) {
|
|
328
|
+
const merged = {
|
|
329
|
+
...localConfig,
|
|
330
|
+
port: localConfig.port !== void 0 ? localConfig.port : globalConfig?.port || DEFAULT_ESEDRE_PORT,
|
|
331
|
+
ports: localConfig.ports || globalConfig?.ports
|
|
332
|
+
};
|
|
333
|
+
return {
|
|
334
|
+
config: merged,
|
|
335
|
+
configPath: localConfigPath,
|
|
336
|
+
workspaceRoot: localWorkspaceRoot
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (options?.fallbackToGlobal) {
|
|
340
|
+
let checkDir = path.resolve(startDir || process.cwd());
|
|
341
|
+
let hasInRepoMarkers = false;
|
|
342
|
+
let inRepoRoot = checkDir;
|
|
343
|
+
while (true) {
|
|
344
|
+
const candidateMarkers = [
|
|
345
|
+
path.join(checkDir, ".esedre", "tickets"),
|
|
346
|
+
path.join(checkDir, "tickets"),
|
|
347
|
+
path.join(checkDir, "src", "data", "planning", "tickets"),
|
|
348
|
+
path.join(checkDir, ".esedre", "projects.json"),
|
|
349
|
+
path.join(checkDir, ".esedre", "project.json"),
|
|
350
|
+
path.join(checkDir, "projects.json"),
|
|
351
|
+
path.join(checkDir, "project.json"),
|
|
352
|
+
path.join(checkDir, "src", "data", "planning", "projects.json"),
|
|
353
|
+
path.join(checkDir, "src", "data", "planning", "projects", "projects.json")
|
|
354
|
+
];
|
|
355
|
+
if (candidateMarkers.some((m) => fs.existsSync(m))) {
|
|
356
|
+
hasInRepoMarkers = true;
|
|
357
|
+
inRepoRoot = checkDir;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
const parent = path.dirname(checkDir);
|
|
361
|
+
if (parent === checkDir) break;
|
|
362
|
+
checkDir = parent;
|
|
363
|
+
}
|
|
364
|
+
if (!hasInRepoMarkers && globalConfig && (globalConfig.dataDir || globalConfig.projects || globalConfig.port || globalConfig.projectCode)) {
|
|
365
|
+
return {
|
|
366
|
+
config: globalConfig,
|
|
367
|
+
configPath: getGlobalConfigPath(),
|
|
368
|
+
workspaceRoot: process.cwd()
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
104
372
|
return {
|
|
105
373
|
config: {},
|
|
106
374
|
configPath: null,
|
|
@@ -150,14 +418,16 @@ function appendSnapshotToGitIgnore(workspaceRoot) {
|
|
|
150
418
|
return false;
|
|
151
419
|
}
|
|
152
420
|
}
|
|
153
|
-
function migrateEsedreConfig(raw, targetVersion =
|
|
421
|
+
function migrateEsedreConfig(raw, targetVersion = CURRENT_ESEDRE_VERSION) {
|
|
154
422
|
const migrated = {
|
|
155
423
|
version: targetVersion,
|
|
156
424
|
projectCode: raw?.projectCode || void 0,
|
|
425
|
+
projectName: raw?.projectName || void 0,
|
|
157
426
|
allowedProjects: Array.isArray(raw?.allowedProjects) ? raw.allowedProjects : void 0,
|
|
158
427
|
dataDir: Array.isArray(raw?.dataDir) ? raw.dataDir : typeof raw?.dataDir === "string" ? raw.dataDir : void 0,
|
|
159
428
|
serverUrl: raw?.serverUrl,
|
|
160
|
-
port: typeof raw?.port === "number" ? raw.port :
|
|
429
|
+
port: typeof raw?.port === "number" && raw.port !== DEFAULT_ESEDRE_PORT ? raw.port : void 0,
|
|
430
|
+
ports: typeof raw?.ports === "object" && raw?.ports ? raw.ports : void 0,
|
|
161
431
|
projects: typeof raw?.projects === "object" && raw?.projects ? raw.projects : void 0
|
|
162
432
|
};
|
|
163
433
|
return migrated;
|
|
@@ -239,7 +509,7 @@ async function generateProjectSnapshot(storage, projectCode, outputDir) {
|
|
|
239
509
|
});
|
|
240
510
|
}
|
|
241
511
|
const snapshot = {
|
|
242
|
-
version:
|
|
512
|
+
version: CURRENT_ESEDRE_VERSION,
|
|
243
513
|
projectCode,
|
|
244
514
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
245
515
|
totalTickets: entries.length,
|
|
@@ -331,9 +601,28 @@ var FilesystemStorageAdapter = class {
|
|
|
331
601
|
resolveProjectLocations() {
|
|
332
602
|
const locations = [];
|
|
333
603
|
const seenCodes = /* @__PURE__ */ new Set();
|
|
334
|
-
const rawDataDirs = Array.isArray(this.config.dataDir) ? this.config.dataDir : this.config.dataDir ? [this.config.dataDir] : [];
|
|
604
|
+
const rawDataDirs = Array.isArray(this.config.dataDir) ? [...this.config.dataDir] : this.config.dataDir ? [this.config.dataDir] : [];
|
|
605
|
+
if (rawDataDirs.length === 0) {
|
|
606
|
+
const candidateHubDirs = [
|
|
607
|
+
this.workspaceRoot,
|
|
608
|
+
path3.join(this.workspaceRoot, "data"),
|
|
609
|
+
path3.join(this.workspaceRoot, ".esedre")
|
|
610
|
+
];
|
|
611
|
+
for (const cand of candidateHubDirs) {
|
|
612
|
+
const pSub = path3.join(cand, "projects");
|
|
613
|
+
if (fs3.existsSync(pSub)) {
|
|
614
|
+
try {
|
|
615
|
+
if (fs3.statSync(pSub).isDirectory()) {
|
|
616
|
+
rawDataDirs.push(cand);
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
} catch {
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
335
624
|
for (const dDir of rawDataDirs) {
|
|
336
|
-
const resolvedHub = path3.resolve(this.workspaceRoot, dDir);
|
|
625
|
+
const resolvedHub = path3.resolve(this.workspaceRoot, expandHome(dDir));
|
|
337
626
|
if (!fs3.existsSync(resolvedHub)) continue;
|
|
338
627
|
const projectsSubDir = path3.join(resolvedHub, "projects");
|
|
339
628
|
if (fs3.existsSync(projectsSubDir)) {
|
|
@@ -351,6 +640,9 @@ var FilesystemStorageAdapter = class {
|
|
|
351
640
|
if (fs3.existsSync(pJsonPath)) {
|
|
352
641
|
try {
|
|
353
642
|
desc = JSON.parse(fs3.readFileSync(pJsonPath, "utf-8"));
|
|
643
|
+
if (desc && !desc.slug) {
|
|
644
|
+
desc.slug = (desc.code || ent.name).toLowerCase();
|
|
645
|
+
}
|
|
354
646
|
} catch {
|
|
355
647
|
}
|
|
356
648
|
}
|
|
@@ -436,7 +728,7 @@ var FilesystemStorageAdapter = class {
|
|
|
436
728
|
}
|
|
437
729
|
if (this.config.projects && typeof this.config.projects === "object") {
|
|
438
730
|
for (const [code, rawPath] of Object.entries(this.config.projects)) {
|
|
439
|
-
const projectDir = path3.resolve(this.workspaceRoot, rawPath);
|
|
731
|
+
const projectDir = path3.resolve(this.workspaceRoot, expandHome(rawPath));
|
|
440
732
|
if (!fs3.existsSync(projectDir)) continue;
|
|
441
733
|
let actualTicketsDir = path3.join(projectDir, "tickets");
|
|
442
734
|
if (fs3.existsSync(path3.join(projectDir, ".esedre", "tickets"))) {
|
|
@@ -455,6 +747,9 @@ var FilesystemStorageAdapter = class {
|
|
|
455
747
|
if (fs3.existsSync(cand)) {
|
|
456
748
|
try {
|
|
457
749
|
desc = JSON.parse(fs3.readFileSync(cand, "utf-8"));
|
|
750
|
+
if (desc && !desc.slug) {
|
|
751
|
+
desc.slug = (desc.code || code).toLowerCase();
|
|
752
|
+
}
|
|
458
753
|
break;
|
|
459
754
|
} catch {
|
|
460
755
|
}
|
|
@@ -486,7 +781,14 @@ var FilesystemStorageAdapter = class {
|
|
|
486
781
|
path3.join(this.workspaceRoot, "tickets"),
|
|
487
782
|
path3.join(this.workspaceRoot, "src", "data", "planning", "tickets")
|
|
488
783
|
];
|
|
489
|
-
const
|
|
784
|
+
const existingDirs = candidateDirs.filter((d) => fs3.existsSync(d));
|
|
785
|
+
const foundTicketsDir = existingDirs.find((d) => {
|
|
786
|
+
try {
|
|
787
|
+
return fs3.readdirSync(d).length > 0;
|
|
788
|
+
} catch {
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
}) || existingDirs[0] || path3.join(this.workspaceRoot, ".esedre", "tickets");
|
|
490
792
|
let descs = [];
|
|
491
793
|
const pJsonCandidates = [
|
|
492
794
|
path3.join(this.workspaceRoot, ".esedre", "projects.json"),
|
|
@@ -541,7 +843,7 @@ var FilesystemStorageAdapter = class {
|
|
|
541
843
|
}
|
|
542
844
|
async registerProject(input) {
|
|
543
845
|
const rawCode = input.code.trim();
|
|
544
|
-
const cleanCode = rawCode
|
|
846
|
+
const cleanCode = rawCode;
|
|
545
847
|
const cleanName = input.name?.trim() || cleanCode;
|
|
546
848
|
const cleanDesc = input.description?.trim() || `${cleanName} project`;
|
|
547
849
|
const slug = cleanCode.toLowerCase();
|
|
@@ -550,10 +852,29 @@ var FilesystemStorageAdapter = class {
|
|
|
550
852
|
dot: "bg-cyan-400",
|
|
551
853
|
border: "border-cyan-500/40"
|
|
552
854
|
};
|
|
553
|
-
const rawDataDirs = Array.isArray(this.config.dataDir) ? this.config.dataDir : this.config.dataDir ? [this.config.dataDir] : [];
|
|
855
|
+
const rawDataDirs = Array.isArray(this.config.dataDir) ? [...this.config.dataDir] : this.config.dataDir ? [this.config.dataDir] : [];
|
|
856
|
+
if (rawDataDirs.length === 0) {
|
|
857
|
+
const candidateHubDirs = [
|
|
858
|
+
this.workspaceRoot,
|
|
859
|
+
path3.join(this.workspaceRoot, "data"),
|
|
860
|
+
path3.join(this.workspaceRoot, ".esedre")
|
|
861
|
+
];
|
|
862
|
+
for (const cand of candidateHubDirs) {
|
|
863
|
+
const pSub = path3.join(cand, "projects");
|
|
864
|
+
if (fs3.existsSync(pSub)) {
|
|
865
|
+
try {
|
|
866
|
+
if (fs3.statSync(pSub).isDirectory()) {
|
|
867
|
+
rawDataDirs.push(cand);
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
} catch {
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
554
875
|
let targetHub = null;
|
|
555
876
|
for (const dDir of rawDataDirs) {
|
|
556
|
-
const resolved = path3.resolve(this.workspaceRoot, dDir);
|
|
877
|
+
const resolved = path3.resolve(this.workspaceRoot, expandHome(dDir));
|
|
557
878
|
if (fs3.existsSync(resolved)) {
|
|
558
879
|
targetHub = resolved;
|
|
559
880
|
break;
|
|
@@ -574,7 +895,9 @@ var FilesystemStorageAdapter = class {
|
|
|
574
895
|
} catch {
|
|
575
896
|
}
|
|
576
897
|
}
|
|
577
|
-
const
|
|
898
|
+
const existingProjects = await this.getProjects();
|
|
899
|
+
const maxId = existingProjects.reduce((max, p) => Math.max(max, p.id || 0), 0);
|
|
900
|
+
const id2 = existingPJson2?.id || maxId + 1;
|
|
578
901
|
const projectDesc2 = {
|
|
579
902
|
id: id2,
|
|
580
903
|
code: cleanCode,
|
|
@@ -643,7 +966,7 @@ var FilesystemStorageAdapter = class {
|
|
|
643
966
|
const locations = this.resolveProjectLocations();
|
|
644
967
|
if (locations.length === 0) return null;
|
|
645
968
|
const strId = String(id).trim();
|
|
646
|
-
const prefixMatch = strId.match(/^([a-zA-Z0-9]{1,
|
|
969
|
+
const prefixMatch = strId.match(/^([a-zA-Z0-9]{1,7})[-_:](\d+)$/i);
|
|
647
970
|
if (prefixMatch) {
|
|
648
971
|
const code = prefixMatch[1].toLowerCase();
|
|
649
972
|
const num2 = parseInt(prefixMatch[2], 10);
|
|
@@ -926,7 +1249,7 @@ var FilesystemStorageAdapter = class {
|
|
|
926
1249
|
if (requestedIdentifier) {
|
|
927
1250
|
const lower = String(requestedIdentifier).trim().toLowerCase();
|
|
928
1251
|
targetLoc = locations.find(
|
|
929
|
-
(l) => l.project.code
|
|
1252
|
+
(l) => l.project.code?.toLowerCase() === lower || l.project.slug?.toLowerCase() === lower || String(l.project.id) === lower || l.project.name?.toLowerCase() === lower
|
|
930
1253
|
);
|
|
931
1254
|
if (!targetLoc) {
|
|
932
1255
|
throw new Error(`Project '${requestedIdentifier}' is invalid or not registered in this workspace.`);
|
|
@@ -1222,22 +1545,22 @@ function formatTicketListTable(tickets) {
|
|
|
1222
1545
|
const rows = tickets.map((t) => {
|
|
1223
1546
|
const id = t.projectDescriptor?.code ? `${t.projectDescriptor.code}-${t.meta.id}` : `#${t.meta.id}`;
|
|
1224
1547
|
const project = t.projectDescriptor?.code || "CORE";
|
|
1225
|
-
const
|
|
1548
|
+
const type = t.meta.type || t.meta.category || "Feature";
|
|
1226
1549
|
const status = t.meta.status;
|
|
1227
1550
|
const title = t.meta.title;
|
|
1228
|
-
return { id, project,
|
|
1551
|
+
return { id, project, type, status, title };
|
|
1229
1552
|
});
|
|
1230
1553
|
const idWidth = Math.max(4, ...rows.map((r) => r.id.length));
|
|
1231
1554
|
const projWidth = Math.max(7, ...rows.map((r) => r.project.length));
|
|
1232
|
-
const
|
|
1555
|
+
const typeWidth = Math.max(8, ...rows.map((r) => r.type.length));
|
|
1233
1556
|
const statusWidth = Math.max(14, ...rows.map((r) => r.status.length));
|
|
1234
|
-
const header = `${colors.bold}${pad("ID", idWidth)} ${pad("Project", projWidth)} ${pad("
|
|
1235
|
-
const divider = `${colors.dim}${"-".repeat(idWidth)} ${"-".repeat(projWidth)} ${"-".repeat(
|
|
1557
|
+
const header = `${colors.bold}${pad("ID", idWidth)} ${pad("Project", projWidth)} ${pad("Type", typeWidth)} ${pad("Status", statusWidth)} Title${colors.reset}`;
|
|
1558
|
+
const divider = `${colors.dim}${"-".repeat(idWidth)} ${"-".repeat(projWidth)} ${"-".repeat(typeWidth)} ${"-".repeat(statusWidth)} ${"-".repeat(40)}${colors.reset}`;
|
|
1236
1559
|
const formattedRows = rows.map((r) => {
|
|
1237
1560
|
const statusColored = colorStatus(r.status);
|
|
1238
|
-
const
|
|
1561
|
+
const typeColored = colorType(r.type);
|
|
1239
1562
|
const projColored = `${colors.magenta}${r.project}${colors.reset}`;
|
|
1240
|
-
return `${colors.bold}${pad(r.id, idWidth)}${colors.reset} ${pad(projColored, projWidth + (isColorSupported ? colors.magenta.length + colors.reset.length : 0))} ${pad(
|
|
1563
|
+
return `${colors.bold}${pad(r.id, idWidth)}${colors.reset} ${pad(projColored, projWidth + (isColorSupported ? colors.magenta.length + colors.reset.length : 0))} ${pad(typeColored, typeWidth + (isColorSupported ? 9 : 0))} ${pad(statusColored, statusWidth + (isColorSupported ? 9 : 0))} ${r.title}`;
|
|
1241
1564
|
});
|
|
1242
1565
|
return [header, divider, ...formattedRows].join("\n");
|
|
1243
1566
|
}
|
|
@@ -1247,7 +1570,7 @@ function formatTicketDetail(ticket) {
|
|
|
1247
1570
|
lines.push(`${colors.bold}${colors.cyan}Ticket #${meta.project ? `${meta.project}-${meta.id}` : meta.id}: ${meta.title}${colors.reset}`);
|
|
1248
1571
|
lines.push(`${colors.dim}${"=".repeat(60)}${colors.reset}`);
|
|
1249
1572
|
lines.push(`${colors.bold}Project:${colors.reset} ${projectDescriptor ? `${projectDescriptor.code} - ${projectDescriptor.name}` : "Default"}`);
|
|
1250
|
-
lines.push(`${colors.bold}
|
|
1573
|
+
lines.push(`${colors.bold}Type:${colors.reset} ${colorType(meta.type || meta.category || "Feature")}`);
|
|
1251
1574
|
lines.push(`${colors.bold}Status:${colors.reset} ${colorStatus(meta.status)}`);
|
|
1252
1575
|
lines.push(`${colors.bold}Complexity:${colors.reset} ${meta.complexity || "Medium"}`);
|
|
1253
1576
|
lines.push(`${colors.bold}Effort:${colors.reset} ${meta.estimatedEffort || "N/A"}`);
|
|
@@ -1281,9 +1604,12 @@ function formatTicketDetail(ticket) {
|
|
|
1281
1604
|
lines.push(` \u2022 ${q}`);
|
|
1282
1605
|
}
|
|
1283
1606
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1607
|
+
if (planMarkdown) {
|
|
1608
|
+
lines.push("");
|
|
1609
|
+
lines.push(`${colors.bold}Implementation Plan:${colors.reset}`);
|
|
1610
|
+
lines.push(planMarkdown.trim());
|
|
1611
|
+
}
|
|
1612
|
+
if (comments && comments.length > 0) {
|
|
1287
1613
|
lines.push("");
|
|
1288
1614
|
lines.push(`${colors.bold}Comments (${comments.length}):${colors.reset}`);
|
|
1289
1615
|
for (const c of comments) {
|
|
@@ -1307,20 +1633,20 @@ function colorStatus(status) {
|
|
|
1307
1633
|
return `${colors.cyan}${status}${colors.reset}`;
|
|
1308
1634
|
}
|
|
1309
1635
|
}
|
|
1310
|
-
function
|
|
1311
|
-
switch (
|
|
1636
|
+
function colorType(type) {
|
|
1637
|
+
switch (type) {
|
|
1312
1638
|
case "Feature":
|
|
1313
|
-
return `${colors.blue}${
|
|
1639
|
+
return `${colors.blue}${type}${colors.reset}`;
|
|
1314
1640
|
case "Platform":
|
|
1315
|
-
return `${colors.magenta}${
|
|
1641
|
+
return `${colors.magenta}${type}${colors.reset}`;
|
|
1316
1642
|
case "Tools":
|
|
1317
|
-
return `${colors.yellow}${
|
|
1643
|
+
return `${colors.yellow}${type}${colors.reset}`;
|
|
1318
1644
|
case "Bug":
|
|
1319
|
-
return `${colors.red}${
|
|
1645
|
+
return `${colors.red}${type}${colors.reset}`;
|
|
1320
1646
|
case "Idea":
|
|
1321
|
-
return `${colors.green}${
|
|
1647
|
+
return `${colors.green}${type}${colors.reset}`;
|
|
1322
1648
|
default:
|
|
1323
|
-
return
|
|
1649
|
+
return type;
|
|
1324
1650
|
}
|
|
1325
1651
|
}
|
|
1326
1652
|
|
|
@@ -1522,17 +1848,26 @@ Status: ${ticket.meta.status}`;
|
|
|
1522
1848
|
}
|
|
1523
1849
|
}
|
|
1524
1850
|
}
|
|
1851
|
+
parseTicketId(val) {
|
|
1852
|
+
if (typeof val === "number") return val;
|
|
1853
|
+
const str = String(val).trim();
|
|
1854
|
+
if (/^\d+$/.test(str)) {
|
|
1855
|
+
return parseInt(str, 10);
|
|
1856
|
+
}
|
|
1857
|
+
return str;
|
|
1858
|
+
}
|
|
1525
1859
|
getToolDefinitions() {
|
|
1526
1860
|
return [
|
|
1527
1861
|
{
|
|
1528
1862
|
name: "esedre_list_tickets",
|
|
1529
|
-
description: "List roadmap tickets with optional project code, status, category, or search query.",
|
|
1863
|
+
description: "List roadmap tickets with optional project code, status, type, category, or search query.",
|
|
1530
1864
|
inputSchema: {
|
|
1531
1865
|
type: "object",
|
|
1532
1866
|
properties: {
|
|
1533
1867
|
project: { type: "string", description: "Project code (e.g. CORE, WEB, DOCS)" },
|
|
1534
1868
|
status: { type: "string", enum: ["Planned", "In Development", "Completed", "Rejected"], description: "Ticket status" },
|
|
1535
|
-
|
|
1869
|
+
type: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket type" },
|
|
1870
|
+
category: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket category (legacy alias for type)" },
|
|
1536
1871
|
search: { type: "string", description: "Search keywords in title or ID" }
|
|
1537
1872
|
}
|
|
1538
1873
|
}
|
|
@@ -1543,7 +1878,7 @@ Status: ${ticket.meta.status}`;
|
|
|
1543
1878
|
inputSchema: {
|
|
1544
1879
|
type: "object",
|
|
1545
1880
|
properties: {
|
|
1546
|
-
ticketId: { type: "integer", description: "Numeric ticket ID (e.g. 96, 101)" }
|
|
1881
|
+
ticketId: { type: ["integer", "string"], description: "Numeric ticket ID (e.g. 96, 101) or compound key (e.g. Profe-96)" }
|
|
1547
1882
|
},
|
|
1548
1883
|
required: ["ticketId"]
|
|
1549
1884
|
}
|
|
@@ -1554,7 +1889,7 @@ Status: ${ticket.meta.status}`;
|
|
|
1554
1889
|
inputSchema: {
|
|
1555
1890
|
type: "object",
|
|
1556
1891
|
properties: {
|
|
1557
|
-
ticketId: { type: "integer", description: "Numeric ticket ID" }
|
|
1892
|
+
ticketId: { type: ["integer", "string"], description: "Numeric ticket ID or compound key" }
|
|
1558
1893
|
},
|
|
1559
1894
|
required: ["ticketId"]
|
|
1560
1895
|
}
|
|
@@ -1565,7 +1900,7 @@ Status: ${ticket.meta.status}`;
|
|
|
1565
1900
|
inputSchema: {
|
|
1566
1901
|
type: "object",
|
|
1567
1902
|
properties: {
|
|
1568
|
-
ticketId: { type: "integer", description: "Numeric ticket ID" },
|
|
1903
|
+
ticketId: { type: ["integer", "string"], description: "Numeric ticket ID or compound key" },
|
|
1569
1904
|
planMarkdown: { type: "string", description: "Implementation plan markdown content" },
|
|
1570
1905
|
lastHash: { type: "string", description: "Optimistic concurrency control: last known sha1 hash of the ticket" }
|
|
1571
1906
|
},
|
|
@@ -1580,24 +1915,29 @@ Status: ${ticket.meta.status}`;
|
|
|
1580
1915
|
properties: {
|
|
1581
1916
|
title: { type: "string", description: "Ticket title (max 48 characters)" },
|
|
1582
1917
|
project: { type: "string", description: "Target project code (e.g. CORE, WEB, DOCS)" },
|
|
1918
|
+
type: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket type" },
|
|
1583
1919
|
category: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket category" },
|
|
1584
1920
|
complexity: { type: "string", description: "Complexity (e.g. Low, Medium, High)" },
|
|
1585
|
-
effort: { type: "string", description: "Estimated effort (e.g. 2.0
|
|
1921
|
+
effort: { type: "string", description: "Estimated effort (e.g. 2.0 - 4.0 hours)" },
|
|
1586
1922
|
summary: { type: "string", description: "Initial feature summary" },
|
|
1587
1923
|
author: { type: "string", description: "Submitting author name" }
|
|
1588
1924
|
},
|
|
1589
|
-
required: ["title"
|
|
1925
|
+
required: ["title"]
|
|
1590
1926
|
}
|
|
1591
1927
|
},
|
|
1592
1928
|
{
|
|
1593
1929
|
name: "esedre_update_ticket",
|
|
1594
|
-
description: "Update an existing ticket state (status, title, active planning flag, or feature flag).",
|
|
1930
|
+
description: "Update an existing ticket state (status, type, title, complexity, effort, active planning flag, or feature flag).",
|
|
1595
1931
|
inputSchema: {
|
|
1596
1932
|
type: "object",
|
|
1597
1933
|
properties: {
|
|
1598
|
-
ticketId: { type: "integer", description: "Numeric ticket ID" },
|
|
1934
|
+
ticketId: { type: ["integer", "string"], description: "Numeric ticket ID or compound key" },
|
|
1599
1935
|
status: { type: "string", enum: ["Planned", "In Development", "Completed", "Rejected"] },
|
|
1936
|
+
type: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"] },
|
|
1937
|
+
category: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"] },
|
|
1600
1938
|
title: { type: "string", description: "New title (max 48 characters)" },
|
|
1939
|
+
complexity: { type: "string", description: "Complexity (e.g. Low, Medium, High)" },
|
|
1940
|
+
effort: { type: "string", description: "Estimated effort (e.g. 2.0 - 4.0 hours)" },
|
|
1601
1941
|
inDevelopment: { type: "boolean", description: "Active development toggle" },
|
|
1602
1942
|
featureFlag: { type: "string", description: "Feature flag name" },
|
|
1603
1943
|
lastHash: { type: "string", description: "Optimistic concurrency control: last known sha1 hash of the ticket" }
|
|
@@ -1607,11 +1947,11 @@ Status: ${ticket.meta.status}`;
|
|
|
1607
1947
|
},
|
|
1608
1948
|
{
|
|
1609
1949
|
name: "esedre_add_comment",
|
|
1610
|
-
description: "Append a developer or
|
|
1950
|
+
description: "Append a developer or LLM agent comment to a ticket.",
|
|
1611
1951
|
inputSchema: {
|
|
1612
1952
|
type: "object",
|
|
1613
1953
|
properties: {
|
|
1614
|
-
ticketId: { type: "integer", description: "Numeric ticket ID" },
|
|
1954
|
+
ticketId: { type: ["integer", "string"], description: "Numeric ticket ID or compound key" },
|
|
1615
1955
|
text: { type: "string", description: "Comment message" },
|
|
1616
1956
|
author: { type: "string", description: "Author name (e.g. Antigravity, Developer)" }
|
|
1617
1957
|
},
|
|
@@ -1624,16 +1964,19 @@ Status: ${ticket.meta.status}`;
|
|
|
1624
1964
|
const canonical = name.replace(/^esedre_/, "");
|
|
1625
1965
|
switch (canonical) {
|
|
1626
1966
|
case "list_tickets": {
|
|
1967
|
+
const type = args.type || args.category;
|
|
1627
1968
|
const tickets = await this.storage.listTickets({
|
|
1628
1969
|
project: args.project,
|
|
1629
1970
|
status: args.status,
|
|
1630
|
-
|
|
1971
|
+
type,
|
|
1972
|
+
category: type,
|
|
1631
1973
|
search: args.search
|
|
1632
1974
|
});
|
|
1633
1975
|
return tickets.map((t) => ({
|
|
1634
1976
|
id: t.meta.id,
|
|
1635
1977
|
title: t.meta.title,
|
|
1636
|
-
|
|
1978
|
+
type: t.meta.type || t.meta.category,
|
|
1979
|
+
category: t.meta.type || t.meta.category,
|
|
1637
1980
|
status: t.meta.status,
|
|
1638
1981
|
complexity: t.meta.complexity,
|
|
1639
1982
|
effort: t.meta.estimatedEffort,
|
|
@@ -1642,26 +1985,27 @@ Status: ${ticket.meta.status}`;
|
|
|
1642
1985
|
}));
|
|
1643
1986
|
}
|
|
1644
1987
|
case "get_ticket": {
|
|
1645
|
-
const id =
|
|
1988
|
+
const id = this.parseTicketId(args.ticketId);
|
|
1646
1989
|
const ticket = await this.storage.getTicket(id);
|
|
1647
1990
|
if (!ticket) throw new Error(`Ticket #${id} not found`);
|
|
1648
1991
|
return ticket;
|
|
1649
1992
|
}
|
|
1650
1993
|
case "get_plan": {
|
|
1651
|
-
const id =
|
|
1994
|
+
const id = this.parseTicketId(args.ticketId);
|
|
1652
1995
|
const plan = await this.storage.getPlan(id);
|
|
1653
1996
|
return { ticketId: id, planMarkdown: plan || null };
|
|
1654
1997
|
}
|
|
1655
1998
|
case "save_plan": {
|
|
1656
|
-
const id =
|
|
1999
|
+
const id = this.parseTicketId(args.ticketId);
|
|
1657
2000
|
await this.storage.savePlan(id, args.planMarkdown, args.lastHash);
|
|
1658
2001
|
return { success: true, message: `Implementation plan saved for Ticket #${id}` };
|
|
1659
2002
|
}
|
|
1660
2003
|
case "create_ticket": {
|
|
2004
|
+
const type = args.type || args.category || "Feature";
|
|
1661
2005
|
const created = await this.storage.createTicket({
|
|
1662
2006
|
title: args.title,
|
|
1663
|
-
type
|
|
1664
|
-
category:
|
|
2007
|
+
type,
|
|
2008
|
+
category: type,
|
|
1665
2009
|
projectCode: args.project,
|
|
1666
2010
|
complexity: args.complexity,
|
|
1667
2011
|
estimatedEffort: args.effort,
|
|
@@ -1671,17 +2015,24 @@ Status: ${ticket.meta.status}`;
|
|
|
1671
2015
|
return created;
|
|
1672
2016
|
}
|
|
1673
2017
|
case "update_ticket": {
|
|
1674
|
-
const id =
|
|
2018
|
+
const id = this.parseTicketId(args.ticketId);
|
|
1675
2019
|
const updates = {};
|
|
2020
|
+
const type = args.type || args.category;
|
|
2021
|
+
if (type) {
|
|
2022
|
+
updates.type = type;
|
|
2023
|
+
updates.category = type;
|
|
2024
|
+
}
|
|
1676
2025
|
if (args.status) updates.status = args.status;
|
|
1677
2026
|
if (args.title) updates.title = args.title;
|
|
2027
|
+
if (args.complexity) updates.complexity = args.complexity;
|
|
2028
|
+
if (args.effort) updates.estimatedEffort = args.effort;
|
|
1678
2029
|
if (args.inDevelopment !== void 0) updates.isActivePlanning = Boolean(args.inDevelopment);
|
|
1679
2030
|
if (args.featureFlag) updates.featureFlag = args.featureFlag;
|
|
1680
2031
|
const updated = await this.storage.updateTicket(id, updates, args.lastHash);
|
|
1681
2032
|
return updated;
|
|
1682
2033
|
}
|
|
1683
2034
|
case "add_comment": {
|
|
1684
|
-
const id =
|
|
2035
|
+
const id = this.parseTicketId(args.ticketId);
|
|
1685
2036
|
const comment = await this.storage.addComment(id, {
|
|
1686
2037
|
text: args.text,
|
|
1687
2038
|
author: args.author || "Agent"
|
|
@@ -1694,47 +2045,235 @@ Status: ${ticket.meta.status}`;
|
|
|
1694
2045
|
}
|
|
1695
2046
|
};
|
|
1696
2047
|
|
|
2048
|
+
// src/utils/manifestDetector.ts
|
|
2049
|
+
import fs4 from "node:fs";
|
|
2050
|
+
import path4 from "node:path";
|
|
2051
|
+
function sanitizeCode(input) {
|
|
2052
|
+
const clean = input.replace(/[^a-zA-Z0-9]/g, "");
|
|
2053
|
+
return clean.slice(0, MAX_PROJECT_CODE_LENGTH) || "APP";
|
|
2054
|
+
}
|
|
2055
|
+
function detectProjectManifest(targetDir) {
|
|
2056
|
+
const dirBase = path4.basename(targetDir);
|
|
2057
|
+
for (const settingsFile of ["settings.gradle.kts", "settings.gradle"]) {
|
|
2058
|
+
const fullPath = path4.join(targetDir, settingsFile);
|
|
2059
|
+
if (fs4.existsSync(fullPath)) {
|
|
2060
|
+
try {
|
|
2061
|
+
const content = fs4.readFileSync(fullPath, "utf-8");
|
|
2062
|
+
const match = content.match(/rootProject\.name\s*=\s*["']([^"']+)["']/);
|
|
2063
|
+
if (match && match[1].trim()) {
|
|
2064
|
+
const rawName = match[1].trim();
|
|
2065
|
+
return {
|
|
2066
|
+
candidateCode: sanitizeCode(rawName),
|
|
2067
|
+
candidateName: rawName,
|
|
2068
|
+
manifestType: "gradle",
|
|
2069
|
+
manifestFile: settingsFile
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
} catch {
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
for (const buildFile of [
|
|
2077
|
+
path4.join("app", "build.gradle.kts"),
|
|
2078
|
+
"build.gradle.kts",
|
|
2079
|
+
path4.join("app", "build.gradle"),
|
|
2080
|
+
"build.gradle"
|
|
2081
|
+
]) {
|
|
2082
|
+
const fullPath = path4.join(targetDir, buildFile);
|
|
2083
|
+
if (fs4.existsSync(fullPath)) {
|
|
2084
|
+
try {
|
|
2085
|
+
const content = fs4.readFileSync(fullPath, "utf-8");
|
|
2086
|
+
const match = content.match(/(?:namespace|applicationId)\s*=\s*["']([^"']+)["']/);
|
|
2087
|
+
if (match && match[1].trim()) {
|
|
2088
|
+
const ns = match[1].trim();
|
|
2089
|
+
const lastSegment = ns.split(".").pop() || dirBase;
|
|
2090
|
+
const candidateName = lastSegment.charAt(0).toUpperCase() + lastSegment.slice(1);
|
|
2091
|
+
return {
|
|
2092
|
+
candidateCode: sanitizeCode(candidateName),
|
|
2093
|
+
candidateName,
|
|
2094
|
+
manifestType: "gradle",
|
|
2095
|
+
manifestFile: buildFile
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
} catch {
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
const pomPath = path4.join(targetDir, "pom.xml");
|
|
2103
|
+
if (fs4.existsSync(pomPath)) {
|
|
2104
|
+
try {
|
|
2105
|
+
const content = fs4.readFileSync(pomPath, "utf-8");
|
|
2106
|
+
const stripped = content.replace(/<!--[\s\S]*?-->/g, "").replace(/<parent>[\s\S]*?<\/parent>/g, "");
|
|
2107
|
+
const nameMatch = stripped.match(/<name>\s*([^<]+)\s*<\/name>/);
|
|
2108
|
+
const artifactMatch = stripped.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/) || content.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/);
|
|
2109
|
+
let rawName = nameMatch?.[1]?.trim();
|
|
2110
|
+
if (!rawName || rawName.startsWith("${")) {
|
|
2111
|
+
rawName = artifactMatch?.[1]?.trim();
|
|
2112
|
+
}
|
|
2113
|
+
if (rawName) {
|
|
2114
|
+
return {
|
|
2115
|
+
candidateCode: sanitizeCode(rawName),
|
|
2116
|
+
candidateName: rawName,
|
|
2117
|
+
manifestType: "maven",
|
|
2118
|
+
manifestFile: "pom.xml"
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
} catch {
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
const pkgPath = path4.join(targetDir, "package.json");
|
|
2125
|
+
if (fs4.existsSync(pkgPath)) {
|
|
2126
|
+
try {
|
|
2127
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
2128
|
+
if (pkg.name && typeof pkg.name === "string") {
|
|
2129
|
+
const baseName = pkg.name.replace(/^@[\w-]+\//, "");
|
|
2130
|
+
return {
|
|
2131
|
+
candidateCode: sanitizeCode(baseName),
|
|
2132
|
+
candidateName: baseName,
|
|
2133
|
+
manifestType: "node",
|
|
2134
|
+
manifestFile: "package.json"
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
} catch {
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
const cargoPath = path4.join(targetDir, "Cargo.toml");
|
|
2141
|
+
if (fs4.existsSync(cargoPath)) {
|
|
2142
|
+
try {
|
|
2143
|
+
const content = fs4.readFileSync(cargoPath, "utf-8");
|
|
2144
|
+
const match = content.match(/\[package\][\s\S]*?name\s*=\s*["']([^"']+)["']/);
|
|
2145
|
+
if (match && match[1].trim()) {
|
|
2146
|
+
const rawName = match[1].trim();
|
|
2147
|
+
return {
|
|
2148
|
+
candidateCode: sanitizeCode(rawName),
|
|
2149
|
+
candidateName: rawName,
|
|
2150
|
+
manifestType: "rust",
|
|
2151
|
+
manifestFile: "Cargo.toml"
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
} catch {
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
const pyprojectPath = path4.join(targetDir, "pyproject.toml");
|
|
2158
|
+
if (fs4.existsSync(pyprojectPath)) {
|
|
2159
|
+
try {
|
|
2160
|
+
const content = fs4.readFileSync(pyprojectPath, "utf-8");
|
|
2161
|
+
const match = content.match(/name\s*=\s*["']([^"']+)["']/);
|
|
2162
|
+
if (match && match[1].trim()) {
|
|
2163
|
+
const rawName = match[1].trim();
|
|
2164
|
+
return {
|
|
2165
|
+
candidateCode: sanitizeCode(rawName),
|
|
2166
|
+
candidateName: rawName,
|
|
2167
|
+
manifestType: "python",
|
|
2168
|
+
manifestFile: "pyproject.toml"
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
} catch {
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
const setupCfgPath = path4.join(targetDir, "setup.cfg");
|
|
2175
|
+
if (fs4.existsSync(setupCfgPath)) {
|
|
2176
|
+
try {
|
|
2177
|
+
const content = fs4.readFileSync(setupCfgPath, "utf-8");
|
|
2178
|
+
const match = content.match(/^name\s*=\s*([^\r\n]+)/m);
|
|
2179
|
+
if (match && match[1].trim()) {
|
|
2180
|
+
const rawName = match[1].trim();
|
|
2181
|
+
return {
|
|
2182
|
+
candidateCode: sanitizeCode(rawName),
|
|
2183
|
+
candidateName: rawName,
|
|
2184
|
+
manifestType: "python",
|
|
2185
|
+
manifestFile: "setup.cfg"
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
} catch {
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
const setupPyPath = path4.join(targetDir, "setup.py");
|
|
2192
|
+
if (fs4.existsSync(setupPyPath)) {
|
|
2193
|
+
try {
|
|
2194
|
+
const content = fs4.readFileSync(setupPyPath, "utf-8");
|
|
2195
|
+
const match = content.match(/name\s*=\s*["']([^"']+)["']/);
|
|
2196
|
+
if (match && match[1].trim()) {
|
|
2197
|
+
const rawName = match[1].trim();
|
|
2198
|
+
return {
|
|
2199
|
+
candidateCode: sanitizeCode(rawName),
|
|
2200
|
+
candidateName: rawName,
|
|
2201
|
+
manifestType: "python",
|
|
2202
|
+
manifestFile: "setup.py"
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
} catch {
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
const goModPath = path4.join(targetDir, "go.mod");
|
|
2209
|
+
if (fs4.existsSync(goModPath)) {
|
|
2210
|
+
try {
|
|
2211
|
+
const content = fs4.readFileSync(goModPath, "utf-8");
|
|
2212
|
+
const match = content.match(/^module\s+([^\r\n\s]+)/m);
|
|
2213
|
+
if (match && match[1].trim()) {
|
|
2214
|
+
const modPath = match[1].trim();
|
|
2215
|
+
const baseName = modPath.split("/").pop() || dirBase;
|
|
2216
|
+
return {
|
|
2217
|
+
candidateCode: sanitizeCode(baseName),
|
|
2218
|
+
candidateName: baseName,
|
|
2219
|
+
manifestType: "go",
|
|
2220
|
+
manifestFile: "go.mod"
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
} catch {
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
return {
|
|
2227
|
+
candidateCode: sanitizeCode(dirBase),
|
|
2228
|
+
candidateName: dirBase,
|
|
2229
|
+
manifestType: "directory"
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
|
|
1697
2233
|
// src/upgrade.ts
|
|
1698
2234
|
import crypto2 from "node:crypto";
|
|
1699
|
-
import
|
|
1700
|
-
import
|
|
2235
|
+
import fs6 from "node:fs";
|
|
2236
|
+
import path6 from "node:path";
|
|
1701
2237
|
|
|
1702
2238
|
// src/server/daemon.ts
|
|
1703
|
-
import
|
|
1704
|
-
import
|
|
1705
|
-
import
|
|
2239
|
+
import fs5 from "node:fs";
|
|
2240
|
+
import path5 from "node:path";
|
|
2241
|
+
import os2 from "node:os";
|
|
1706
2242
|
import http from "node:http";
|
|
1707
2243
|
import { spawn, spawnSync } from "node:child_process";
|
|
1708
2244
|
import { fileURLToPath } from "node:url";
|
|
1709
2245
|
var __filename = fileURLToPath(import.meta.url);
|
|
1710
|
-
var __dirname =
|
|
2246
|
+
var __dirname = path5.dirname(__filename);
|
|
1711
2247
|
function getGlobalEsedreDir() {
|
|
1712
|
-
|
|
2248
|
+
if (process.env.ESEDRE_GLOBAL_DIR) {
|
|
2249
|
+
return process.env.ESEDRE_GLOBAL_DIR;
|
|
2250
|
+
}
|
|
2251
|
+
return path5.join(os2.homedir(), ".esedre");
|
|
1713
2252
|
}
|
|
1714
2253
|
function ensureGlobalEsedreStore() {
|
|
1715
2254
|
const globalDir = getGlobalEsedreDir();
|
|
1716
|
-
const runDir =
|
|
1717
|
-
const logsDir =
|
|
2255
|
+
const runDir = path5.join(globalDir, "run");
|
|
2256
|
+
const logsDir = path5.join(globalDir, "logs");
|
|
1718
2257
|
try {
|
|
1719
|
-
|
|
1720
|
-
|
|
2258
|
+
fs5.mkdirSync(runDir, { recursive: true });
|
|
2259
|
+
fs5.mkdirSync(logsDir, { recursive: true });
|
|
1721
2260
|
try {
|
|
1722
|
-
const runEntries =
|
|
2261
|
+
const runEntries = fs5.readdirSync(runDir);
|
|
1723
2262
|
for (const entry of runEntries) {
|
|
1724
2263
|
if (entry.startsWith(".probe-")) {
|
|
1725
|
-
|
|
2264
|
+
fs5.unlinkSync(path5.join(runDir, entry));
|
|
1726
2265
|
}
|
|
1727
2266
|
}
|
|
1728
2267
|
} catch {
|
|
1729
2268
|
}
|
|
1730
|
-
const testProbe =
|
|
2269
|
+
const testProbe = path5.join(runDir, `.probe-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`);
|
|
1731
2270
|
try {
|
|
1732
|
-
|
|
2271
|
+
fs5.writeFileSync(testProbe, "ok", "utf-8");
|
|
1733
2272
|
return { ok: true, path: globalDir };
|
|
1734
2273
|
} finally {
|
|
1735
|
-
if (
|
|
2274
|
+
if (fs5.existsSync(testProbe)) {
|
|
1736
2275
|
try {
|
|
1737
|
-
|
|
2276
|
+
fs5.unlinkSync(testProbe);
|
|
1738
2277
|
} catch {
|
|
1739
2278
|
}
|
|
1740
2279
|
}
|
|
@@ -1749,10 +2288,10 @@ function resolveStoreDir(workspaceRoot) {
|
|
|
1749
2288
|
return globalStore.path;
|
|
1750
2289
|
}
|
|
1751
2290
|
if (workspaceRoot) {
|
|
1752
|
-
const localStore =
|
|
2291
|
+
const localStore = path5.join(workspaceRoot, ".esedre");
|
|
1753
2292
|
try {
|
|
1754
|
-
|
|
1755
|
-
|
|
2293
|
+
fs5.mkdirSync(path5.join(localStore, "run"), { recursive: true });
|
|
2294
|
+
fs5.mkdirSync(path5.join(localStore, "logs"), { recursive: true });
|
|
1756
2295
|
return localStore;
|
|
1757
2296
|
} catch {
|
|
1758
2297
|
}
|
|
@@ -1761,32 +2300,32 @@ function resolveStoreDir(workspaceRoot) {
|
|
|
1761
2300
|
}
|
|
1762
2301
|
function getDaemonStateFile(port, workspaceRoot) {
|
|
1763
2302
|
const store = resolveStoreDir(workspaceRoot);
|
|
1764
|
-
return
|
|
2303
|
+
return path5.join(store, "run", `daemon-${port}.json`);
|
|
1765
2304
|
}
|
|
1766
2305
|
function getDaemonLogFile(port, workspaceRoot) {
|
|
1767
2306
|
const store = resolveStoreDir(workspaceRoot);
|
|
1768
|
-
return
|
|
2307
|
+
return path5.join(store, "logs", `daemon-${port}.log`);
|
|
1769
2308
|
}
|
|
1770
2309
|
function getDaemonState(port, workspaceRoot) {
|
|
1771
2310
|
const stateFile = getDaemonStateFile(port, workspaceRoot);
|
|
1772
|
-
if (!
|
|
2311
|
+
if (!fs5.existsSync(stateFile)) {
|
|
1773
2312
|
return null;
|
|
1774
2313
|
}
|
|
1775
2314
|
try {
|
|
1776
|
-
const raw =
|
|
2315
|
+
const raw = fs5.readFileSync(stateFile, "utf-8").trim();
|
|
1777
2316
|
if (!raw) {
|
|
1778
|
-
|
|
2317
|
+
fs5.unlinkSync(stateFile);
|
|
1779
2318
|
return null;
|
|
1780
2319
|
}
|
|
1781
2320
|
const state = JSON.parse(raw);
|
|
1782
2321
|
if (!state || typeof state.pid !== "number" || typeof state.port !== "number") {
|
|
1783
|
-
|
|
2322
|
+
fs5.unlinkSync(stateFile);
|
|
1784
2323
|
return null;
|
|
1785
2324
|
}
|
|
1786
2325
|
return state;
|
|
1787
2326
|
} catch {
|
|
1788
2327
|
try {
|
|
1789
|
-
|
|
2328
|
+
fs5.unlinkSync(stateFile);
|
|
1790
2329
|
} catch {
|
|
1791
2330
|
}
|
|
1792
2331
|
return null;
|
|
@@ -1838,7 +2377,7 @@ function pingDaemon(port, timeoutMs = 200) {
|
|
|
1838
2377
|
});
|
|
1839
2378
|
}
|
|
1840
2379
|
async function startDaemon(options = {}) {
|
|
1841
|
-
const port = options.port
|
|
2380
|
+
const port = resolvePorts(void 0, { port: options.port }).gateway;
|
|
1842
2381
|
const workspaceRoot = options.workspaceRoot || process.cwd();
|
|
1843
2382
|
const stateFile = getDaemonStateFile(port, workspaceRoot);
|
|
1844
2383
|
const logFile = getDaemonLogFile(port, workspaceRoot);
|
|
@@ -1849,12 +2388,15 @@ async function startDaemon(options = {}) {
|
|
|
1849
2388
|
if (ping.responding && ping.isEsedre) {
|
|
1850
2389
|
if (!options.quiet) {
|
|
1851
2390
|
console.log(`\x1B[36m\u26A1 Esedre daemon already running on http://localhost:${port} (PID ${existingState.pid})\x1B[0m`);
|
|
2391
|
+
if (existingState.version && existingState.version !== CURRENT_ESEDRE_VERSION) {
|
|
2392
|
+
console.log(`\x1B[33m\u26A0\uFE0F Warning: Daemon is running v${existingState.version}, but v${CURRENT_ESEDRE_VERSION} is installed. Run 'ese stop && ese start' to reload.\x1B[0m`);
|
|
2393
|
+
}
|
|
1852
2394
|
}
|
|
1853
2395
|
return existingState;
|
|
1854
2396
|
}
|
|
1855
2397
|
}
|
|
1856
2398
|
try {
|
|
1857
|
-
|
|
2399
|
+
fs5.unlinkSync(stateFile);
|
|
1858
2400
|
} catch {
|
|
1859
2401
|
}
|
|
1860
2402
|
}
|
|
@@ -1867,33 +2409,33 @@ async function startDaemon(options = {}) {
|
|
|
1867
2409
|
pid: process.pid,
|
|
1868
2410
|
port,
|
|
1869
2411
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1870
|
-
version:
|
|
2412
|
+
version: CURRENT_ESEDRE_VERSION,
|
|
1871
2413
|
workspaceRoot,
|
|
1872
2414
|
logFile
|
|
1873
2415
|
};
|
|
1874
2416
|
}
|
|
1875
2417
|
resolveStoreDir(workspaceRoot);
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
const logFd =
|
|
1879
|
-
const isEsedreArgv1 = process.argv[1] && /esedre/i.test(
|
|
2418
|
+
fs5.mkdirSync(path5.dirname(logFile), { recursive: true });
|
|
2419
|
+
fs5.mkdirSync(path5.dirname(stateFile), { recursive: true });
|
|
2420
|
+
const logFd = fs5.openSync(logFile, "a");
|
|
2421
|
+
const isEsedreArgv1 = process.argv[1] && /esedre/i.test(path5.basename(process.argv[1]));
|
|
1880
2422
|
const cliCandidates = [
|
|
1881
|
-
isEsedreArgv1 ? process.argv[1] : void 0,
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2423
|
+
isEsedreArgv1 ? path5.resolve(process.argv[1]) : void 0,
|
|
2424
|
+
path5.resolve(workspaceRoot, "dist", "esedre.mjs"),
|
|
2425
|
+
path5.resolve(workspaceRoot, "../esedre/dist", "esedre.mjs"),
|
|
2426
|
+
path5.resolve(__dirname, "esedre.mjs"),
|
|
2427
|
+
path5.resolve(__dirname, "..", "dist", "esedre.mjs"),
|
|
2428
|
+
path5.resolve(__dirname, "..", "..", "dist", "esedre.mjs"),
|
|
2429
|
+
path5.resolve(__dirname, "..", "..", "bin", "esedre.js"),
|
|
2430
|
+
path5.resolve(__dirname, "..", "..", "bin", "esedre.ts"),
|
|
2431
|
+
path5.resolve(__dirname, "..", "esedre.mjs"),
|
|
2432
|
+
path5.resolve(__dirname, "../../../esedre/dist", "esedre.mjs")
|
|
1891
2433
|
].filter(Boolean);
|
|
1892
|
-
const cliPath = cliCandidates.find((c) =>
|
|
1893
|
-
if (!
|
|
2434
|
+
const cliPath = cliCandidates.find((c) => fs5.existsSync(c) && (c.endsWith(".mjs") || c.endsWith(".js") || c.endsWith(".ts"))) || cliCandidates[0];
|
|
2435
|
+
if (!fs5.existsSync(cliPath)) {
|
|
1894
2436
|
throw new Error(`Could not locate Esedre executable. Searched candidates: ${cliCandidates.join(", ")}`);
|
|
1895
2437
|
}
|
|
1896
|
-
const child = spawn(process.execPath, [cliPath, "
|
|
2438
|
+
const child = spawn(process.execPath, [cliPath, "start", "--foreground", "--port", String(port)], {
|
|
1897
2439
|
detached: true,
|
|
1898
2440
|
stdio: ["ignore", logFd, logFd],
|
|
1899
2441
|
cwd: workspaceRoot,
|
|
@@ -1901,6 +2443,10 @@ async function startDaemon(options = {}) {
|
|
|
1901
2443
|
windowsHide: true
|
|
1902
2444
|
});
|
|
1903
2445
|
child.unref();
|
|
2446
|
+
try {
|
|
2447
|
+
fs5.closeSync(logFd);
|
|
2448
|
+
} catch {
|
|
2449
|
+
}
|
|
1904
2450
|
const pid = child.pid;
|
|
1905
2451
|
if (!pid) {
|
|
1906
2452
|
throw new Error("Failed to spawn Esedre daemon background process.");
|
|
@@ -1932,18 +2478,18 @@ async function startDaemon(options = {}) {
|
|
|
1932
2478
|
uiPort: port + 1,
|
|
1933
2479
|
apiPort: port + 2,
|
|
1934
2480
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1935
|
-
version:
|
|
2481
|
+
version: CURRENT_ESEDRE_VERSION,
|
|
1936
2482
|
workspaceRoot,
|
|
1937
2483
|
logFile
|
|
1938
2484
|
};
|
|
1939
2485
|
const tmpState = `${stateFile}.tmp.${Date.now()}`;
|
|
1940
|
-
|
|
2486
|
+
fs5.writeFileSync(tmpState, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
1941
2487
|
try {
|
|
1942
|
-
|
|
2488
|
+
fs5.renameSync(tmpState, stateFile);
|
|
1943
2489
|
} catch {
|
|
1944
|
-
|
|
2490
|
+
fs5.writeFileSync(stateFile, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
1945
2491
|
try {
|
|
1946
|
-
|
|
2492
|
+
fs5.unlinkSync(tmpState);
|
|
1947
2493
|
} catch {
|
|
1948
2494
|
}
|
|
1949
2495
|
}
|
|
@@ -1955,7 +2501,7 @@ async function startDaemon(options = {}) {
|
|
|
1955
2501
|
return state;
|
|
1956
2502
|
}
|
|
1957
2503
|
async function stopDaemon(options = {}) {
|
|
1958
|
-
const port = options.port
|
|
2504
|
+
const port = resolvePorts(void 0, { port: options.port }).gateway;
|
|
1959
2505
|
const workspaceRoot = options.workspaceRoot || process.cwd();
|
|
1960
2506
|
const stateFile = getDaemonStateFile(port, workspaceRoot);
|
|
1961
2507
|
const state = getDaemonState(port, workspaceRoot);
|
|
@@ -1985,8 +2531,8 @@ async function stopDaemon(options = {}) {
|
|
|
1985
2531
|
}
|
|
1986
2532
|
}
|
|
1987
2533
|
try {
|
|
1988
|
-
if (
|
|
1989
|
-
|
|
2534
|
+
if (fs5.existsSync(stateFile)) {
|
|
2535
|
+
fs5.unlinkSync(stateFile);
|
|
1990
2536
|
}
|
|
1991
2537
|
} catch {
|
|
1992
2538
|
}
|
|
@@ -1996,7 +2542,7 @@ async function stopDaemon(options = {}) {
|
|
|
1996
2542
|
return true;
|
|
1997
2543
|
}
|
|
1998
2544
|
async function getDaemonStatus(options = {}) {
|
|
1999
|
-
const port = options.port
|
|
2545
|
+
const port = resolvePorts(void 0, { port: options.port }).gateway;
|
|
2000
2546
|
const workspaceRoot = options.workspaceRoot || process.cwd();
|
|
2001
2547
|
const state = getDaemonState(port, workspaceRoot);
|
|
2002
2548
|
const stateFile = getDaemonStateFile(port, workspaceRoot);
|
|
@@ -2005,6 +2551,8 @@ async function getDaemonStatus(options = {}) {
|
|
|
2005
2551
|
if (state && isProcessAlive(state.pid) && ping.responding && ping.isEsedre) {
|
|
2006
2552
|
const startedMs = Date.parse(state.startedAt) || Date.now();
|
|
2007
2553
|
const uptimeSeconds = Math.max(0, Math.floor((Date.now() - startedMs) / 1e3));
|
|
2554
|
+
const daemonVersion = state.version;
|
|
2555
|
+
const isStale = Boolean(daemonVersion && daemonVersion !== CURRENT_ESEDRE_VERSION);
|
|
2008
2556
|
return {
|
|
2009
2557
|
running: true,
|
|
2010
2558
|
pid: state.pid,
|
|
@@ -2013,12 +2561,15 @@ async function getDaemonStatus(options = {}) {
|
|
|
2013
2561
|
startedAt: state.startedAt,
|
|
2014
2562
|
workspaceRoot: state.workspaceRoot,
|
|
2015
2563
|
projects: ping.projects,
|
|
2016
|
-
logFile: state.logFile || logFile
|
|
2564
|
+
logFile: state.logFile || logFile,
|
|
2565
|
+
version: daemonVersion,
|
|
2566
|
+
staleVersion: isStale,
|
|
2567
|
+
installedVersion: CURRENT_ESEDRE_VERSION
|
|
2017
2568
|
};
|
|
2018
2569
|
}
|
|
2019
2570
|
if (state && !isProcessAlive(state.pid)) {
|
|
2020
2571
|
try {
|
|
2021
|
-
|
|
2572
|
+
fs5.unlinkSync(stateFile);
|
|
2022
2573
|
} catch {
|
|
2023
2574
|
}
|
|
2024
2575
|
}
|
|
@@ -2026,18 +2577,21 @@ async function getDaemonStatus(options = {}) {
|
|
|
2026
2577
|
running: ping.responding && ping.isEsedre,
|
|
2027
2578
|
port,
|
|
2028
2579
|
projects: ping.projects,
|
|
2029
|
-
logFile
|
|
2580
|
+
logFile,
|
|
2581
|
+
version: state?.version,
|
|
2582
|
+
staleVersion: Boolean(state?.version && state.version !== CURRENT_ESEDRE_VERSION),
|
|
2583
|
+
installedVersion: CURRENT_ESEDRE_VERSION
|
|
2030
2584
|
};
|
|
2031
2585
|
}
|
|
2032
2586
|
function printDaemonLogs(options = {}) {
|
|
2033
|
-
const port = options.port
|
|
2587
|
+
const port = resolvePorts(void 0, { port: options.port }).gateway;
|
|
2034
2588
|
const workspaceRoot = options.workspaceRoot || process.cwd();
|
|
2035
2589
|
const logFile = getDaemonLogFile(port, workspaceRoot);
|
|
2036
|
-
if (!
|
|
2590
|
+
if (!fs5.existsSync(logFile)) {
|
|
2037
2591
|
console.log(`\x1B[33mNo logs found at: ${logFile}\x1B[0m`);
|
|
2038
2592
|
return;
|
|
2039
2593
|
}
|
|
2040
|
-
const content =
|
|
2594
|
+
const content = fs5.readFileSync(logFile, "utf-8");
|
|
2041
2595
|
const count = options.lines || 40;
|
|
2042
2596
|
const allLines = content.split(/\r?\n/);
|
|
2043
2597
|
const tail = allLines.slice(-count).join("\n");
|
|
@@ -2047,7 +2601,6 @@ function printDaemonLogs(options = {}) {
|
|
|
2047
2601
|
}
|
|
2048
2602
|
|
|
2049
2603
|
// src/upgrade.ts
|
|
2050
|
-
var CURRENT_ESEDRE_VERSION = "0.1.4";
|
|
2051
2604
|
function computeNormalizedHash(content) {
|
|
2052
2605
|
const normalized = content.replace(/\r\n/g, "\n").trim();
|
|
2053
2606
|
return crypto2.createHash("sha1").update(normalized, "utf-8").digest("hex");
|
|
@@ -2175,10 +2728,129 @@ var ESE_WRAPPER_BASH = `#!/usr/bin/env bash
|
|
|
2175
2728
|
DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
|
2176
2729
|
exec "$DIR/esedre" "$@"
|
|
2177
2730
|
`;
|
|
2178
|
-
var
|
|
2731
|
+
var HISTORIC_SKILL_HASHES = [
|
|
2732
|
+
"8611116c276fdfb598b965c716b251ce7c77c7f7",
|
|
2733
|
+
"05f129fb6e06a18df00215af105567abcbf0bc67"
|
|
2734
|
+
];
|
|
2735
|
+
var ESEDRE_SKILL_TEMPLATE = `---
|
|
2736
|
+
name: esedre
|
|
2737
|
+
description: Tooling and workflow reference for interacting with the Esedre developer ticketing and roadmap system. Use this skill whenever inspecting planned work, viewing ticket specs, updating implementation plans, or posting developer notes.
|
|
2738
|
+
---
|
|
2739
|
+
|
|
2740
|
+
# Esedre Planning & Roadmap Workflow
|
|
2741
|
+
|
|
2742
|
+
Esedre is the developer ticketing and LLM coding partner coordination platform. The engine codebase resides in the standalone repository \`aellinsar/esedre\` (\`../esedre\`), and operates on decoupled ticket repositories (such as \`aellinsar/esedre-data\` configured in \`.esedre/esedre.json\`). Interact with tickets via in-repo shell wrappers in \`.esedre/\` (\`.esedre/ese\`), or the global \`ese\` CLI.
|
|
2743
|
+
|
|
2744
|
+
## 0. Instant Zero-Latency Context (\`.esedre/snapshot.json\`)
|
|
2745
|
+
|
|
2746
|
+
Before querying tickets over the network or CLI, check \`.esedre/snapshot.json\` in the workspace root. It provides a local read-only projection containing all active and completed tickets, summaries, implementation plans, revision numbers, completion dates (\`completedAt\`), and staleness metrics (\`daysSinceUpdate\`).
|
|
2747
|
+
|
|
2748
|
+
## 1. Model Context Protocol (MCP) Tools
|
|
2749
|
+
|
|
2750
|
+
When Esedre MCP is active in your agent session (\`.agents/mcp_config.json\`), use these tools directly:
|
|
2751
|
+
|
|
2752
|
+
| Tool | Purpose | Key Arguments |
|
|
2753
|
+
|---|---|---|
|
|
2754
|
+
| \`esedre_list_tickets\` | List roadmap tickets with filters | \`project\` (e.g. \`Profe\`, \`Esedre\`, \`Alce\`), \`status\`, \`type\`, \`search\` |
|
|
2755
|
+
| \`esedre_get_ticket\` | Full specification, summary, comments, revision & sha1 | \`ticketId\` (numeric e.g. \`96\` or compound e.g. \`Profe-96\`) |
|
|
2756
|
+
| \`esedre_get_plan\` | Active implementation plan markdown | \`ticketId\` (numeric or compound) |
|
|
2757
|
+
| \`esedre_save_plan\` | Save implementation plan markdown with OCC | \`ticketId\`, \`planMarkdown\`, \`lastHash\` |
|
|
2758
|
+
| \`esedre_create_ticket\` | Mint a new ticket with auto sequential ID | \`title\` (max 48 chars), \`type\`, \`project\`, \`effort\`, \`summary\` |
|
|
2759
|
+
| \`esedre_update_ticket\` | Update ticket attributes with OCC | \`ticketId\`, \`status\`, \`type\`, \`title\`, \`complexity\`, \`effort\`, \`inDevelopment\`, \`featureFlag\`, \`lastHash\` |
|
|
2760
|
+
| \`esedre_add_comment\` | Append developer or agent comment | \`ticketId\`, \`text\`, \`author\` |
|
|
2761
|
+
|
|
2762
|
+
## 2. Core CLI Commands (Fallback via \`run_command\`)
|
|
2763
|
+
|
|
2764
|
+
Both \`esedre\` and \`ese\` work interchangeably:
|
|
2765
|
+
|
|
2766
|
+
### List Tickets
|
|
2767
|
+
\`\`\`bash
|
|
2768
|
+
.esedre/ese list [-p|--project <code|all>] [-s|--status <status>] [-t|--type <type>] [-q|--search <q>] [--json]
|
|
2769
|
+
\`\`\`
|
|
2770
|
+
|
|
2771
|
+
### Read Ticket Details
|
|
2772
|
+
\`\`\`bash
|
|
2773
|
+
.esedre/ese get <ticketId> [--json]
|
|
2774
|
+
\`\`\`
|
|
2775
|
+
|
|
2776
|
+
### Read or Update Implementation Plans
|
|
2777
|
+
\`\`\`bash
|
|
2778
|
+
.esedre/ese plan <ticketId>
|
|
2779
|
+
.esedre/ese plan <ticketId> --file <planFilePath>
|
|
2780
|
+
.esedre/ese plan <ticketId> --set "<markdownContent>" [--last-hash <hash>]
|
|
2781
|
+
\`\`\`
|
|
2782
|
+
|
|
2783
|
+
### Create a Ticket
|
|
2784
|
+
\`\`\`bash
|
|
2785
|
+
.esedre/ese create --title "..." [-p|--project <code>] [-t|--type Feature] [--complexity Medium] [--effort "2.0 - 4.0 hours"] [--json]
|
|
2786
|
+
\`\`\`
|
|
2787
|
+
|
|
2788
|
+
### Update Ticket Status & Attributes
|
|
2789
|
+
\`\`\`bash
|
|
2790
|
+
.esedre/ese update <ticketId> --status "In Development"
|
|
2791
|
+
.esedre/ese update <ticketId> --status "Completed"
|
|
2792
|
+
.esedre/ese update <ticketId> --type Feature --complexity Medium --effort "2.0 - 4.0 hours"
|
|
2793
|
+
\`\`\`
|
|
2794
|
+
|
|
2795
|
+
### Append Comments or Notes
|
|
2796
|
+
\`\`\`bash
|
|
2797
|
+
.esedre/ese comment <ticketId> "Verified implementation." --author "Agent"
|
|
2798
|
+
.esedre/ese comment <ticketId> --text "Verified implementation." --author "Agent"
|
|
2799
|
+
\`\`\`
|
|
2800
|
+
|
|
2801
|
+
### Regenerate Snapshot
|
|
2802
|
+
\`\`\`bash
|
|
2803
|
+
.esedre/ese snapshot
|
|
2804
|
+
\`\`\`
|
|
2805
|
+
|
|
2806
|
+
### Server Lifecycle & Daemon Management
|
|
2807
|
+
\`\`\`bash
|
|
2808
|
+
# Start background server daemon on port 5674 (default)
|
|
2809
|
+
.esedre/ese start [--port 5674] [--quiet] [--json]
|
|
2810
|
+
|
|
2811
|
+
# Run foreground gateway server directly (Ctrl+C to stop)
|
|
2812
|
+
.esedre/ese start -f [--port 5674]
|
|
2813
|
+
|
|
2814
|
+
# Check daemon health and diagnostics
|
|
2815
|
+
.esedre/ese status [--port 5674] [--json]
|
|
2816
|
+
|
|
2817
|
+
# View recent daemon activity logs
|
|
2818
|
+
.esedre/ese logs [--port 5674] [--lines <n>]
|
|
2819
|
+
|
|
2820
|
+
# Stop background server daemon
|
|
2821
|
+
.esedre/ese stop [--port 5674] [--quiet] [--json]
|
|
2822
|
+
\`\`\`
|
|
2823
|
+
|
|
2824
|
+
## 3. Embedded View & Theme Token Contract
|
|
2825
|
+
|
|
2826
|
+
When embedding \`<PlannedWorkView />\` inside a host app (e.g. \`PlannedWorkModal.tsx\`):
|
|
2827
|
+
|
|
2828
|
+
### Zero-Effort Drop-in (Default Fallback)
|
|
2829
|
+
\`PlannedWorkView\` incorporates an internal CSS fallback bridge (\`.esedre-host-bridge\` / \`.esedre-theme-root\` with \`ESEDRE_THEME_FALLBACK_CSS\`).
|
|
2830
|
+
If an embedding application supplies **zero CSS variables**, the component automatically falls back to clean, high-contrast light or dark themes matching the user's OS preference (\`prefers-color-scheme: dark\`) or host \`.dark\` / \`[data-theme="night"]\` classes.
|
|
2831
|
+
|
|
2832
|
+
### Theme Token Customization
|
|
2833
|
+
If the host application declares any or all of the 12 core design tokens on \`:root\` or an ancestor container, \`PlannedWorkView\` automatically adopts them:
|
|
2834
|
+
- **Surface**: \`--bg-surface\`, \`--bg-surface-elevated\`, \`--bg-input\`
|
|
2835
|
+
- **Borders**: \`--border-subtle\`, \`--border-strong\`, \`--border-accent\`
|
|
2836
|
+
- **Text**: \`--text-primary\`, \`--text-secondary\`, \`--text-muted\`
|
|
2837
|
+
- **Accent**: \`--accent-primary\`, \`--accent-bg-subtle\`, \`--accent-border-subtle\`
|
|
2838
|
+
|
|
2839
|
+
## 4. Safety & Invariants
|
|
2840
|
+
- **Always use \`--json\`** for CLI programmatic inspection.
|
|
2841
|
+
- **Optimistic Concurrency**: Writes support \`--last-hash <hash>\` to prevent overwriting concurrent updates.
|
|
2842
|
+
- **Universal Type Naming**: Always use \`type\` (\`Feature\`, \`Platform\`, \`Tools\`, \`Idea\`, \`Bug\`). The legacy name \`category\` is deprecated.
|
|
2843
|
+
- **Compound IDs for Multi-Project Portfolios**: In \`project=all\`, ticket IDs are \`\${projectCode}-\${id}\` (e.g. \`Profe-1\`, \`Esedre-1\`). DOM anchors strictly use \`feature-card-\${projectCode}-\${id}\`.
|
|
2844
|
+
- **Never delete tickets** directly via filesystem.
|
|
2845
|
+
- **Workspace Scoping**: An LLM coding partner operates strictly within the authorized project scope declared in the local repository configuration. Central repository linking and system-level configuration are managed separately; focus roadmap work, implementation plans, and verification notes on this project.
|
|
2846
|
+
- **Secret Developer Hash Route (\`#/planner\`, \`#/plan\`, \`#planner\`, \`#plan\`)**:
|
|
2847
|
+
- In web applications embedding Esedre (such as Professor Arwam), \`#/planner\` and \`#/plan\` serve as direct secret developer entry routes.
|
|
2848
|
+
- **SEO Invariant**: These developer routes are private and MUST NEVER be exposed in \`sitemap.xml\`, \`robots.txt\`, \`llms.txt\`, or public navigation links.
|
|
2849
|
+
- The URL hash is preserved across hard page refreshes (F5) without falling back to \`#/chat\` or other views.
|
|
2850
|
+
`;
|
|
2179
2851
|
function plantWrappers(targetDir) {
|
|
2180
|
-
const esedreDir =
|
|
2181
|
-
|
|
2852
|
+
const esedreDir = path6.join(targetDir, ".esedre");
|
|
2853
|
+
fs6.mkdirSync(esedreDir, { recursive: true });
|
|
2182
2854
|
const wrappers = [
|
|
2183
2855
|
{ name: "esedre.cmd", content: WRAPPER_CMD, exec: false },
|
|
2184
2856
|
{ name: "esedre.ps1", content: WRAPPER_PS1, exec: false },
|
|
@@ -2188,24 +2860,24 @@ function plantWrappers(targetDir) {
|
|
|
2188
2860
|
{ name: "ese", content: ESE_WRAPPER_BASH, exec: true }
|
|
2189
2861
|
];
|
|
2190
2862
|
for (const w of wrappers) {
|
|
2191
|
-
const fullPath =
|
|
2192
|
-
|
|
2863
|
+
const fullPath = path6.join(esedreDir, w.name);
|
|
2864
|
+
fs6.writeFileSync(fullPath, w.content, "utf-8");
|
|
2193
2865
|
if (w.exec) {
|
|
2194
2866
|
try {
|
|
2195
|
-
|
|
2867
|
+
fs6.chmodSync(fullPath, 493);
|
|
2196
2868
|
} catch {
|
|
2197
2869
|
}
|
|
2198
2870
|
}
|
|
2199
2871
|
}
|
|
2200
2872
|
}
|
|
2201
2873
|
function setupMcpConfig(targetDir) {
|
|
2202
|
-
const agentsDir =
|
|
2203
|
-
|
|
2204
|
-
const mcpConfigPath =
|
|
2874
|
+
const agentsDir = path6.join(targetDir, ".agents");
|
|
2875
|
+
fs6.mkdirSync(agentsDir, { recursive: true });
|
|
2876
|
+
const mcpConfigPath = path6.join(agentsDir, "mcp_config.json");
|
|
2205
2877
|
let configObj = { mcpServers: {} };
|
|
2206
|
-
if (
|
|
2878
|
+
if (fs6.existsSync(mcpConfigPath)) {
|
|
2207
2879
|
try {
|
|
2208
|
-
configObj = JSON.parse(
|
|
2880
|
+
configObj = JSON.parse(fs6.readFileSync(mcpConfigPath, "utf-8"));
|
|
2209
2881
|
if (!configObj.mcpServers) configObj.mcpServers = {};
|
|
2210
2882
|
} catch {
|
|
2211
2883
|
}
|
|
@@ -2222,28 +2894,29 @@ function setupMcpConfig(targetDir) {
|
|
|
2222
2894
|
};
|
|
2223
2895
|
}
|
|
2224
2896
|
try {
|
|
2225
|
-
|
|
2897
|
+
fs6.writeFileSync(mcpConfigPath, JSON.stringify(configObj, null, 2) + "\n", "utf-8");
|
|
2226
2898
|
return true;
|
|
2227
2899
|
} catch {
|
|
2228
2900
|
return false;
|
|
2229
2901
|
}
|
|
2230
2902
|
}
|
|
2231
|
-
function setupSkill(targetDir) {
|
|
2232
|
-
const skillDir =
|
|
2233
|
-
|
|
2234
|
-
const skillFile =
|
|
2235
|
-
if (!
|
|
2236
|
-
|
|
2903
|
+
function setupSkill(targetDir, options = {}) {
|
|
2904
|
+
const skillDir = path6.join(targetDir, ".agents", "skills", "esedre");
|
|
2905
|
+
fs6.mkdirSync(skillDir, { recursive: true });
|
|
2906
|
+
const skillFile = path6.join(skillDir, "SKILL.md");
|
|
2907
|
+
if (!fs6.existsSync(skillFile)) {
|
|
2908
|
+
fs6.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
|
|
2237
2909
|
return { status: "MISSING", updated: true };
|
|
2238
2910
|
}
|
|
2239
|
-
const currentContent =
|
|
2240
|
-
const status = classifyContent(currentContent, ESEDRE_SKILL_TEMPLATE,
|
|
2241
|
-
if (status === "LATEST") {
|
|
2911
|
+
const currentContent = fs6.readFileSync(skillFile, "utf-8");
|
|
2912
|
+
const status = classifyContent(currentContent, ESEDRE_SKILL_TEMPLATE, HISTORIC_SKILL_HASHES);
|
|
2913
|
+
if (status === "LATEST" && !options.force) {
|
|
2242
2914
|
return { status, updated: false };
|
|
2243
2915
|
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2916
|
+
const hasBannedOrDeprecatedTerms = currentContent.includes("companion agent") || currentContent.includes("companion LLM") || currentContent.includes("tools/esedre.mjs");
|
|
2917
|
+
if (options.force || status === "HISTORIC_DEFAULT" || status === "MISSING" || hasBannedOrDeprecatedTerms) {
|
|
2918
|
+
fs6.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
|
|
2919
|
+
return { status: "LATEST", updated: true };
|
|
2247
2920
|
}
|
|
2248
2921
|
return { status, updated: false };
|
|
2249
2922
|
}
|
|
@@ -2256,8 +2929,8 @@ var VITE_CONFIG_FILES = [
|
|
|
2256
2929
|
];
|
|
2257
2930
|
function findViteConfig(targetDir) {
|
|
2258
2931
|
for (const name of VITE_CONFIG_FILES) {
|
|
2259
|
-
const fullPath =
|
|
2260
|
-
if (
|
|
2932
|
+
const fullPath = path6.join(targetDir, name);
|
|
2933
|
+
if (fs6.existsSync(fullPath)) {
|
|
2261
2934
|
return fullPath;
|
|
2262
2935
|
}
|
|
2263
2936
|
}
|
|
@@ -2321,10 +2994,10 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
|
|
|
2321
2994
|
recommendedProxySnippet: snippet
|
|
2322
2995
|
};
|
|
2323
2996
|
}
|
|
2324
|
-
const relativeName =
|
|
2997
|
+
const relativeName = path6.basename(configPath);
|
|
2325
2998
|
let content = "";
|
|
2326
2999
|
try {
|
|
2327
|
-
content =
|
|
3000
|
+
content = fs6.readFileSync(configPath, "utf-8");
|
|
2328
3001
|
} catch {
|
|
2329
3002
|
return {
|
|
2330
3003
|
status: "MANUAL_REQUIRED",
|
|
@@ -2355,7 +3028,7 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
|
|
|
2355
3028
|
};
|
|
2356
3029
|
}
|
|
2357
3030
|
try {
|
|
2358
|
-
|
|
3031
|
+
fs6.writeFileSync(configPath, injected, "utf-8");
|
|
2359
3032
|
return {
|
|
2360
3033
|
status: "CONFIGURED",
|
|
2361
3034
|
configFile: relativeName,
|
|
@@ -2370,25 +3043,27 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
|
|
|
2370
3043
|
}
|
|
2371
3044
|
}
|
|
2372
3045
|
function configureWorkspace(targetDir, options = {}) {
|
|
2373
|
-
const esedreDir =
|
|
2374
|
-
if (!
|
|
2375
|
-
|
|
3046
|
+
const esedreDir = path6.join(targetDir, ".esedre");
|
|
3047
|
+
if (!fs6.existsSync(esedreDir)) {
|
|
3048
|
+
fs6.mkdirSync(esedreDir, { recursive: true });
|
|
2376
3049
|
}
|
|
2377
|
-
const esedreJsonPath =
|
|
3050
|
+
const esedreJsonPath = path6.join(esedreDir, "esedre.json");
|
|
2378
3051
|
let existingRaw = null;
|
|
2379
|
-
if (
|
|
3052
|
+
if (fs6.existsSync(esedreJsonPath)) {
|
|
2380
3053
|
try {
|
|
2381
|
-
existingRaw = JSON.parse(
|
|
3054
|
+
existingRaw = JSON.parse(fs6.readFileSync(esedreJsonPath, "utf-8"));
|
|
2382
3055
|
} catch {
|
|
2383
3056
|
}
|
|
2384
3057
|
}
|
|
2385
3058
|
let config;
|
|
2386
3059
|
const projectCode = options.projectCode || existingRaw?.projectCode;
|
|
2387
3060
|
const projectName = options.projectName || existingRaw?.projectName || (projectCode ? projectCode : void 0);
|
|
3061
|
+
const dataDir = options.dataDir !== void 0 ? options.dataDir : existingRaw?.dataDir;
|
|
2388
3062
|
if (existingRaw) {
|
|
2389
3063
|
config = migrateEsedreConfig(existingRaw, CURRENT_ESEDRE_VERSION);
|
|
2390
3064
|
if (projectCode) config.projectCode = projectCode;
|
|
2391
3065
|
if (projectName) config.projectName = projectName;
|
|
3066
|
+
if (dataDir) config.dataDir = dataDir;
|
|
2392
3067
|
if (options.allowedProjects) {
|
|
2393
3068
|
config.allowedProjects = options.allowedProjects;
|
|
2394
3069
|
} else if (projectCode) {
|
|
@@ -2405,18 +3080,20 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
2405
3080
|
projectCode: projectCode || void 0,
|
|
2406
3081
|
projectName: projectName || void 0,
|
|
2407
3082
|
allowedProjects: options.allowedProjects || (projectCode ? [projectCode] : void 0),
|
|
2408
|
-
|
|
3083
|
+
dataDir: dataDir || void 0,
|
|
3084
|
+
port: options.port || void 0
|
|
2409
3085
|
};
|
|
2410
3086
|
}
|
|
2411
|
-
|
|
3087
|
+
fs6.writeFileSync(esedreJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2412
3088
|
let projectRegistered = false;
|
|
2413
3089
|
if (projectCode) {
|
|
2414
|
-
const
|
|
2415
|
-
|
|
2416
|
-
|
|
3090
|
+
const hasExistingTicketsDir = fs6.existsSync(path6.join(targetDir, "tickets")) || fs6.existsSync(path6.join(targetDir, "src", "data", "planning", "tickets"));
|
|
3091
|
+
const ticketsDir = path6.join(esedreDir, "tickets");
|
|
3092
|
+
if (!hasExistingTicketsDir && !fs6.existsSync(ticketsDir)) {
|
|
3093
|
+
fs6.mkdirSync(ticketsDir, { recursive: true });
|
|
2417
3094
|
}
|
|
2418
|
-
const inRepoPJson =
|
|
2419
|
-
if (!
|
|
3095
|
+
const inRepoPJson = path6.join(esedreDir, "project.json");
|
|
3096
|
+
if (!fs6.existsSync(inRepoPJson)) {
|
|
2420
3097
|
const projDesc = {
|
|
2421
3098
|
id: 1,
|
|
2422
3099
|
code: projectCode.toUpperCase(),
|
|
@@ -2429,14 +3106,14 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
2429
3106
|
border: "border-cyan-500/40"
|
|
2430
3107
|
}
|
|
2431
3108
|
};
|
|
2432
|
-
|
|
3109
|
+
fs6.writeFileSync(inRepoPJson, JSON.stringify(projDesc, null, 2) + "\n", "utf-8");
|
|
2433
3110
|
projectRegistered = true;
|
|
2434
3111
|
} else if (projectName) {
|
|
2435
3112
|
try {
|
|
2436
|
-
const existing = JSON.parse(
|
|
3113
|
+
const existing = JSON.parse(fs6.readFileSync(inRepoPJson, "utf-8"));
|
|
2437
3114
|
if (existing.name !== projectName) {
|
|
2438
3115
|
existing.name = projectName;
|
|
2439
|
-
|
|
3116
|
+
fs6.writeFileSync(inRepoPJson, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
2440
3117
|
}
|
|
2441
3118
|
} catch {
|
|
2442
3119
|
}
|
|
@@ -2448,8 +3125,9 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
2448
3125
|
if (options.setupMcp !== false) {
|
|
2449
3126
|
mcpConfigured = setupMcpConfig(targetDir);
|
|
2450
3127
|
}
|
|
2451
|
-
const skillRes = setupSkill(targetDir);
|
|
2452
|
-
const
|
|
3128
|
+
const skillRes = setupSkill(targetDir, { force: options.force });
|
|
3129
|
+
const targetPort = resolvePorts(config).gateway;
|
|
3130
|
+
const proxyRes = configureViteProxy(targetDir, targetPort, options.setupProxy);
|
|
2453
3131
|
const globalStoreRes = ensureGlobalEsedreStore();
|
|
2454
3132
|
if (!globalStoreRes.ok) {
|
|
2455
3133
|
console.warn(`\x1B[33m\u26A0\uFE0F Warning: Could not initialize global store at ${globalStoreRes.path}: ${globalStoreRes.error}\x1B[0m`);
|
|
@@ -2471,17 +3149,41 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
2471
3149
|
globalStoreError: globalStoreRes.error
|
|
2472
3150
|
};
|
|
2473
3151
|
}
|
|
3152
|
+
var initWorkspace = configureWorkspace;
|
|
3153
|
+
function initHub(targetDir) {
|
|
3154
|
+
const resolved = path6.resolve(targetDir);
|
|
3155
|
+
if (!fs6.existsSync(resolved)) {
|
|
3156
|
+
fs6.mkdirSync(resolved, { recursive: true });
|
|
3157
|
+
}
|
|
3158
|
+
const projectsDir = path6.join(resolved, "projects");
|
|
3159
|
+
let projectsDirCreated = false;
|
|
3160
|
+
if (!fs6.existsSync(projectsDir)) {
|
|
3161
|
+
fs6.mkdirSync(projectsDir, { recursive: true });
|
|
3162
|
+
projectsDirCreated = true;
|
|
3163
|
+
}
|
|
3164
|
+
const projectsJsonPath = path6.join(resolved, "projects.json");
|
|
3165
|
+
let projectsJsonCreated = false;
|
|
3166
|
+
if (!fs6.existsSync(projectsJsonPath)) {
|
|
3167
|
+
fs6.writeFileSync(projectsJsonPath, "[]\n", "utf-8");
|
|
3168
|
+
projectsJsonCreated = true;
|
|
3169
|
+
}
|
|
3170
|
+
return {
|
|
3171
|
+
projectsJsonCreated,
|
|
3172
|
+
projectsDirCreated,
|
|
3173
|
+
hubDir: resolved.replace(/\\/g, "/")
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
2474
3176
|
|
|
2475
3177
|
// src/server/gateway.ts
|
|
2476
|
-
import
|
|
3178
|
+
import fs8 from "node:fs";
|
|
2477
3179
|
import http4 from "node:http";
|
|
2478
|
-
import
|
|
3180
|
+
import path8 from "node:path";
|
|
2479
3181
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2480
3182
|
|
|
2481
3183
|
// src/server/uiServer.ts
|
|
2482
3184
|
import http2 from "node:http";
|
|
2483
|
-
import
|
|
2484
|
-
import
|
|
3185
|
+
import fs7 from "node:fs";
|
|
3186
|
+
import path7 from "node:path";
|
|
2485
3187
|
var MIME_TYPES = {
|
|
2486
3188
|
".html": "text/html; charset=utf-8",
|
|
2487
3189
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -2511,20 +3213,20 @@ function startUiServer(port, webDir) {
|
|
|
2511
3213
|
if (!reqPath || reqPath === "/") {
|
|
2512
3214
|
reqPath = "/index.html";
|
|
2513
3215
|
}
|
|
2514
|
-
const safePath =
|
|
2515
|
-
let filePath =
|
|
2516
|
-
if (!
|
|
2517
|
-
filePath =
|
|
3216
|
+
const safePath = path7.normalize(reqPath).replace(/^(\.\.[/\\])+/, "");
|
|
3217
|
+
let filePath = path7.join(webDir, safePath);
|
|
3218
|
+
if (!fs7.existsSync(filePath) || fs7.statSync(filePath).isDirectory()) {
|
|
3219
|
+
filePath = path7.join(webDir, "index.html");
|
|
2518
3220
|
}
|
|
2519
|
-
if (!
|
|
3221
|
+
if (!fs7.existsSync(filePath)) {
|
|
2520
3222
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
2521
3223
|
res.end("Esedre UI: index.html not found. Run npm run build first.");
|
|
2522
3224
|
return;
|
|
2523
3225
|
}
|
|
2524
|
-
const ext =
|
|
3226
|
+
const ext = path7.extname(filePath).toLowerCase();
|
|
2525
3227
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
2526
3228
|
try {
|
|
2527
|
-
const content =
|
|
3229
|
+
const content = fs7.readFileSync(filePath);
|
|
2528
3230
|
res.writeHead(200, { "Content-Type": contentType });
|
|
2529
3231
|
res.end(content);
|
|
2530
3232
|
} catch (err) {
|
|
@@ -2605,6 +3307,10 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
2605
3307
|
}
|
|
2606
3308
|
try {
|
|
2607
3309
|
if (req.method === "GET") {
|
|
3310
|
+
if (pathname === "/api/ping") {
|
|
3311
|
+
sendJson(res, 200, { status: "ok", esedre: true });
|
|
3312
|
+
return true;
|
|
3313
|
+
}
|
|
2608
3314
|
if (pathname === "/api/planning/projects") {
|
|
2609
3315
|
const projects = await storage.getProjects();
|
|
2610
3316
|
sendJson(res, 200, projects);
|
|
@@ -2628,7 +3334,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
2628
3334
|
}
|
|
2629
3335
|
if (pathname.startsWith("/api/planning/ticket/")) {
|
|
2630
3336
|
const idStr = pathname.slice("/api/planning/ticket/".length);
|
|
2631
|
-
if (!idStr || !/^([a-zA-Z0-9]{1,
|
|
3337
|
+
if (!idStr || !/^([a-zA-Z0-9]{1,7}-)?\d+$/.test(idStr)) {
|
|
2632
3338
|
sendJson(res, 400, { error: "Invalid ticket ID" });
|
|
2633
3339
|
return true;
|
|
2634
3340
|
}
|
|
@@ -2780,6 +3486,11 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
2780
3486
|
sendJson(res, 400, { error: codeVal.error });
|
|
2781
3487
|
return true;
|
|
2782
3488
|
}
|
|
3489
|
+
const nameVal = validateProjectName(name);
|
|
3490
|
+
if (!nameVal.valid) {
|
|
3491
|
+
sendJson(res, 400, { error: nameVal.error });
|
|
3492
|
+
return true;
|
|
3493
|
+
}
|
|
2783
3494
|
invalidateApiCache();
|
|
2784
3495
|
const project = await storage.registerProject({
|
|
2785
3496
|
code,
|
|
@@ -2859,35 +3570,35 @@ function startApiServer(port, storage, workspaceRoot) {
|
|
|
2859
3570
|
|
|
2860
3571
|
// src/server/gateway.ts
|
|
2861
3572
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2862
|
-
var __dirname2 =
|
|
3573
|
+
var __dirname2 = path8.dirname(__filename2);
|
|
2863
3574
|
function resolveWebDir(explicitWebDir, workspaceRoot) {
|
|
2864
|
-
if (explicitWebDir &&
|
|
3575
|
+
if (explicitWebDir && fs8.existsSync(path8.join(explicitWebDir, "index.html"))) {
|
|
2865
3576
|
return explicitWebDir;
|
|
2866
3577
|
}
|
|
2867
3578
|
const candidates = [
|
|
2868
3579
|
// 1. Packaged layout right next to executable (e.g. tools/web)
|
|
2869
|
-
|
|
3580
|
+
path8.resolve(__dirname2, "web"),
|
|
2870
3581
|
// 2. From workspace root: <workspaceRoot>/dist/web
|
|
2871
|
-
workspaceRoot ?
|
|
3582
|
+
workspaceRoot ? path8.resolve(workspaceRoot, "dist", "web") : "",
|
|
2872
3583
|
// 3. In-repo legacy subfolder: <workspaceRoot>/esedre/dist/web
|
|
2873
|
-
workspaceRoot ?
|
|
3584
|
+
workspaceRoot ? path8.resolve(workspaceRoot, "esedre", "dist", "web") : "",
|
|
2874
3585
|
// 4. Sibling standalone repository from workspace root: <workspaceRoot>/../esedre/dist/web
|
|
2875
|
-
workspaceRoot ?
|
|
3586
|
+
workspaceRoot ? path8.resolve(workspaceRoot, "..", "esedre", "dist", "web") : "",
|
|
2876
3587
|
// 5. Packaged layout inside dist/web
|
|
2877
|
-
|
|
3588
|
+
path8.resolve(__dirname2, "dist", "web"),
|
|
2878
3589
|
// 6. Sibling standalone repository from __dirname: <__dirname>/../../esedre/dist/web
|
|
2879
|
-
|
|
3590
|
+
path8.resolve(__dirname2, "..", "..", "esedre", "dist", "web"),
|
|
2880
3591
|
// 7. Source tree layout: esedre/src/server/ -> esedre/dist/web
|
|
2881
|
-
|
|
3592
|
+
path8.resolve(__dirname2, "..", "..", "dist", "web"),
|
|
2882
3593
|
// 8. Legacy layout from __dirname
|
|
2883
|
-
|
|
3594
|
+
path8.resolve(__dirname2, "..", "esedre", "dist", "web")
|
|
2884
3595
|
].filter(Boolean);
|
|
2885
3596
|
for (const candidate of candidates) {
|
|
2886
|
-
if (
|
|
3597
|
+
if (fs8.existsSync(path8.join(candidate, "index.html"))) {
|
|
2887
3598
|
return candidate;
|
|
2888
3599
|
}
|
|
2889
3600
|
}
|
|
2890
|
-
return candidates[0] ||
|
|
3601
|
+
return candidates[0] || path8.resolve(__dirname2, "web");
|
|
2891
3602
|
}
|
|
2892
3603
|
function proxyRequest(req, res, targetPort, rewritePath) {
|
|
2893
3604
|
const targetPath = rewritePath ? rewritePath(req.url || "/") : req.url || "/";
|
|
@@ -2917,7 +3628,12 @@ function proxyRequest(req, res, targetPort, rewritePath) {
|
|
|
2917
3628
|
req.pipe(proxyReq, { end: true });
|
|
2918
3629
|
}
|
|
2919
3630
|
function startGatewayCluster(options) {
|
|
2920
|
-
const
|
|
3631
|
+
const resolved = resolvePorts(void 0, {
|
|
3632
|
+
gatewayPort: options.gatewayPort,
|
|
3633
|
+
uiPort: options.uiPort,
|
|
3634
|
+
apiPort: options.apiPort
|
|
3635
|
+
});
|
|
3636
|
+
const gatewayPort = resolved.gateway;
|
|
2921
3637
|
const uiPort = options.uiPort !== void 0 ? options.uiPort : gatewayPort ? gatewayPort + 1 : 0;
|
|
2922
3638
|
const apiPort = options.apiPort !== void 0 ? options.apiPort : gatewayPort ? gatewayPort + 2 : 0;
|
|
2923
3639
|
const webDir = resolveWebDir(options.webDir, options.workspaceRoot);
|
|
@@ -2971,18 +3687,23 @@ function startGatewayCluster(options) {
|
|
|
2971
3687
|
}
|
|
2972
3688
|
|
|
2973
3689
|
// bin/esedre.ts
|
|
2974
|
-
function printHelp() {
|
|
3690
|
+
function printHelp(config) {
|
|
3691
|
+
const port = config?.port || DEFAULT_ESEDRE_PORT;
|
|
3692
|
+
const projectCode = config?.projectCode;
|
|
3693
|
+
const webUrl = `http://localhost:${port}/app${projectCode ? "?project=" + encodeURIComponent(projectCode) : "?project=all"}`;
|
|
2975
3694
|
console.log(`
|
|
2976
|
-
${colors.bold}${colors.cyan}Esedre CLI (/eh-seh-dreh/)${colors.reset}: Developer Roadmap &
|
|
3695
|
+
${colors.bold}${colors.cyan}Esedre CLI (/eh-seh-dreh/)${colors.reset}: Developer Roadmap & LLM Coding Partner Coordination Engine
|
|
3696
|
+
|
|
3697
|
+
${colors.bold}WEB UI:${colors.reset}
|
|
3698
|
+
Interactive Planner: ${colors.cyan}${webUrl}${colors.reset}
|
|
2977
3699
|
|
|
2978
3700
|
${colors.bold}USAGE:${colors.reset}
|
|
2979
3701
|
.esedre/esedre <command> [options]
|
|
2980
3702
|
.esedre/ese <command> [options]
|
|
2981
3703
|
esedre <command> [options]
|
|
2982
3704
|
ese <command> [options]
|
|
2983
|
-
node tools/esedre.mjs <command> [options]
|
|
2984
3705
|
|
|
2985
|
-
${colors.bold}COMMANDS:${colors.reset}
|
|
3706
|
+
${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
|
|
2986
3707
|
${colors.bold}list${colors.reset} [-p|--project <code|all>] [-s|--status <status>] [-t|--type <type>] [-q|--search <q>] [--json]
|
|
2987
3708
|
List roadmap tickets with optional filters (defaults to active project).
|
|
2988
3709
|
\u2022 Status values: 'Planned', 'In Development', 'Completed', 'Rejected'
|
|
@@ -2997,46 +3718,48 @@ ${colors.bold}COMMANDS:${colors.reset}
|
|
|
2997
3718
|
${colors.bold}create${colors.reset} --title "..." [-p|--project <code>] [-t|--type <type>] [--complexity <c>] [--effort "<e>"] [--summary "<s>"] [--json]
|
|
2998
3719
|
Mint a new roadmap ticket with sequential numeric ID.
|
|
2999
3720
|
|
|
3000
|
-
${colors.bold}update${colors.reset} <id> [-s|--status <status>] [--title "..."] [--
|
|
3001
|
-
Mutate ticket status, title, active state, or feature flag with OCC protection.
|
|
3721
|
+
${colors.bold}update${colors.reset} <id> [-s|--status <status>] [-t|--type <type>] [--title "..."] [--complexity <c>] [--effort "<e>"] [--in-dev] [--flag <name>] [--last-hash <sha1>] [--force] [--json]
|
|
3722
|
+
Mutate ticket status, type, title, complexity, effort, active state, or feature flag with OCC protection.
|
|
3002
3723
|
|
|
3003
|
-
${colors.bold}comment${colors.reset} <id> --text "..." [--author "..."] [--json]
|
|
3724
|
+
${colors.bold}comment${colors.reset} <id> ["<text>"] [--text "..."] [--author "..."] [--json]
|
|
3004
3725
|
Append a research finding, test verification, or note to ticket history.
|
|
3005
3726
|
|
|
3006
3727
|
${colors.bold}snapshot${colors.reset} [--project <code>] [--json]
|
|
3007
3728
|
Generate lean read-only projection snapshot (.esedre/snapshot.json) for zero-latency agent context.
|
|
3008
3729
|
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
${colors.bold}upgrade${colors.reset} [--json]
|
|
3015
|
-
Upgrade workspace configuration schema, wrappers, and agent skills. Refreshes snapshot.
|
|
3016
|
-
|
|
3017
|
-
${colors.bold}projects${colors.reset} [--json]
|
|
3018
|
-
List registered workspace projects, project codes (e.g. 'CORE', 'WEB', 'DOCS'), and descriptions.
|
|
3019
|
-
|
|
3020
|
-
${colors.bold}daemon${colors.reset} [start|stop|status|logs] [--port <n>] [--quiet] [--json]
|
|
3021
|
-
Manage background daemon process (start, stop, check status, or view logs).
|
|
3022
|
-
|
|
3023
|
-
${colors.bold}serve${colors.reset} [--port <n>]
|
|
3024
|
-
Run the unified gateway & web server in foreground (default 5674).
|
|
3025
|
-
|
|
3026
|
-
${colors.bold}start${colors.reset} [--port <n>] [--quiet] [--json]
|
|
3027
|
-
Alias for 'daemon start'. Start Esedre background daemon process.
|
|
3730
|
+
${colors.bold}SERVICE DAEMON COMMANDS:${colors.reset}
|
|
3731
|
+
${colors.bold}start${colors.reset} [--port <n>] [--foreground | -f] [--quiet] [--json]
|
|
3732
|
+
Start the Esedre background server and web UI daemon (default port ${DEFAULT_ESEDRE_PORT}).
|
|
3733
|
+
Use --foreground (-f) to run directly in terminal foreground (blocks terminal, Ctrl+C to stop).
|
|
3028
3734
|
|
|
3029
3735
|
${colors.bold}stop${colors.reset} [--port <n>] [--quiet] [--json]
|
|
3030
|
-
|
|
3736
|
+
Stop the running Esedre background server daemon.
|
|
3031
3737
|
|
|
3032
3738
|
${colors.bold}status${colors.reset} [--port <n>] [--json]
|
|
3033
|
-
|
|
3739
|
+
Check health, uptime, and diagnostics of the running Esedre server.
|
|
3034
3740
|
|
|
3035
3741
|
${colors.bold}logs${colors.reset} [--port <n>] [--lines <n>]
|
|
3036
|
-
Tail recent
|
|
3742
|
+
Tail recent logs from the Esedre background server.
|
|
3037
3743
|
|
|
3038
3744
|
${colors.bold}mcp${colors.reset} Start the Model Context Protocol (MCP) JSON-RPC 2.0 stdio server for autonomous LLM coding agents.
|
|
3039
3745
|
|
|
3746
|
+
${colors.bold}DEVELOPER ADMINISTRATION (Human Setup & Configuration):${colors.reset}
|
|
3747
|
+
${colors.bold}init${colors.reset} [<path>] [--project <code>] [-n|--name <name>] [--allow <c1,c2>] [--port <n>] [--hub] [--proxy] [--no-proxy] [-y|--yes] [--json]
|
|
3748
|
+
Bring a project repository online (.esedre footprint, wrappers, snapshot) or bootstrap a data hub (--hub).
|
|
3749
|
+
|
|
3750
|
+
${colors.bold}configure${colors.reset} [add <path> | remove <code|path> | set <key> <val> | get <key>] [--json]
|
|
3751
|
+
Inspect or mutate central Esedre configuration (~/.esedre/config.json).
|
|
3752
|
+
\u2022 add <path> Smart-detect and link a data hub or project repository
|
|
3753
|
+
\u2022 remove <target> Unlink a project or data hub
|
|
3754
|
+
\u2022 set <key> <val> Update a central configuration setting
|
|
3755
|
+
|
|
3756
|
+
${colors.bold}upgrade${colors.reset} [<path>] [--force|-f] [--json]
|
|
3757
|
+
Upgrade workspace configuration schema, wrappers, and agent skills. Refreshes snapshot.
|
|
3758
|
+
Pass --force to overwrite customized skills with the latest official template.
|
|
3759
|
+
|
|
3760
|
+
${colors.bold}projects${colors.reset} [--json]
|
|
3761
|
+
List registered workspace projects, project codes (e.g. 'CORE', 'WEB', 'DOCS'), and descriptions.
|
|
3762
|
+
|
|
3040
3763
|
${colors.bold}OPTIONS:${colors.reset}
|
|
3041
3764
|
-p, --project <code> Project code (e.g. CORE, ALCE, DOCS).
|
|
3042
3765
|
-n, --name <name> Project display name (e.g. "Alce Web Reader").
|
|
@@ -3046,6 +3769,7 @@ ${colors.bold}OPTIONS:${colors.reset}
|
|
|
3046
3769
|
--json Output raw machine-readable JSON (strongly recommended for autonomous LLM coding agents).
|
|
3047
3770
|
--last-hash <hash> Optimistic concurrency control: last known sha1 hash of the ticket from 'get'.
|
|
3048
3771
|
--force Bypass optimistic concurrency last-hash conflict checks on writes.
|
|
3772
|
+
-v, --version Show the current version number.
|
|
3049
3773
|
-h, --help Show this help reference.
|
|
3050
3774
|
|
|
3051
3775
|
${colors.bold}TYPICAL AGENT WORKFLOW:${colors.reset}
|
|
@@ -3102,50 +3826,97 @@ function parseArgs(rawArgs) {
|
|
|
3102
3826
|
if (flags["n"] && !flags["name"]) flags["name"] = flags["n"];
|
|
3103
3827
|
if (flags["s"] && !flags["status"]) flags["status"] = flags["s"];
|
|
3104
3828
|
if (flags["q"] && !flags["search"]) flags["search"] = flags["q"];
|
|
3829
|
+
if (flags["f"] && !flags["foreground"]) flags["foreground"] = flags["f"];
|
|
3105
3830
|
return { command, positionals, flags };
|
|
3106
3831
|
}
|
|
3107
3832
|
async function main() {
|
|
3108
3833
|
const rawArgs = process.argv.slice(2);
|
|
3109
3834
|
const { command, positionals, flags } = parseArgs(rawArgs);
|
|
3835
|
+
try {
|
|
3836
|
+
writeGlobalVersion();
|
|
3837
|
+
} catch {
|
|
3838
|
+
}
|
|
3839
|
+
const discovered = findEsedreConfig(process.cwd(), { fallbackToGlobal: true });
|
|
3840
|
+
const isJson = Boolean(flags["json"]);
|
|
3841
|
+
if (flags["version"] || flags["v"] || command === "version") {
|
|
3842
|
+
if (isJson) {
|
|
3843
|
+
console.log(JSON.stringify({ version: CURRENT_ESEDRE_VERSION }));
|
|
3844
|
+
} else {
|
|
3845
|
+
console.log(`esedre v${CURRENT_ESEDRE_VERSION}`);
|
|
3846
|
+
}
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3110
3849
|
if (!command || flags["help"] || flags["h"]) {
|
|
3111
|
-
printHelp();
|
|
3850
|
+
printHelp(discovered.config);
|
|
3112
3851
|
return;
|
|
3113
3852
|
}
|
|
3114
|
-
const isJson = Boolean(flags["json"]);
|
|
3115
|
-
const discovered = findEsedreConfig(process.cwd());
|
|
3116
3853
|
const rawStorage = new FilesystemStorageAdapter(discovered.workspaceRoot, discovered.config);
|
|
3117
3854
|
const storage = new SecurityFilter(rawStorage, discovered.config);
|
|
3118
3855
|
try {
|
|
3119
3856
|
switch (command) {
|
|
3120
|
-
case "
|
|
3857
|
+
case "init": {
|
|
3858
|
+
const targetDir = positionals[0] ? path9.resolve(positionals[0]) : discovered.workspaceRoot;
|
|
3859
|
+
const isHub = Boolean(flags["hub"]);
|
|
3860
|
+
if (isHub) {
|
|
3861
|
+
const hubRes = initHub(targetDir);
|
|
3862
|
+
try {
|
|
3863
|
+
addLocationToGlobalConfig(targetDir);
|
|
3864
|
+
} catch {
|
|
3865
|
+
}
|
|
3866
|
+
if (isJson) {
|
|
3867
|
+
console.log(JSON.stringify(hubRes, null, 2));
|
|
3868
|
+
} else {
|
|
3869
|
+
console.log(`${colors.bold}${colors.green}\u2714 Initialized Esedre ticket data hub!${colors.reset}`);
|
|
3870
|
+
console.log(` \u2022 Location: ${hubRes.hubDir}`);
|
|
3871
|
+
console.log(` \u2022 Projects file: ${hubRes.projectsJsonCreated ? "Created projects.json" : "Existing projects.json"}`);
|
|
3872
|
+
console.log(` \u2022 Projects dir: ${hubRes.projectsDirCreated ? "Created projects/ directory" : "Existing projects/ directory"}`);
|
|
3873
|
+
console.log(` \u2022 Global Config: Registered as dataDir in ~/.esedre/config.json`);
|
|
3874
|
+
console.log(`
|
|
3875
|
+
${colors.bold}Next Steps:${colors.reset}`);
|
|
3876
|
+
console.log(` 1. Link project repositories to this hub using:`);
|
|
3877
|
+
console.log(` ${colors.cyan}ese configure add <projectPath>${colors.reset}`);
|
|
3878
|
+
console.log(` 2. Start the Esedre background server:`);
|
|
3879
|
+
console.log(` ${colors.cyan}ese start${colors.reset}`);
|
|
3880
|
+
}
|
|
3881
|
+
return;
|
|
3882
|
+
}
|
|
3121
3883
|
let projectCode = flags["project"] || flags["p"] || discovered.config?.projectCode;
|
|
3122
3884
|
let projectName = flags["name"] || flags["n"] || discovered.config?.projectName;
|
|
3123
3885
|
const allowArg = flags["allow"];
|
|
3124
|
-
const port = flags["port"] ? parseInt(String(flags["port"]), 10) :
|
|
3886
|
+
const port = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
|
|
3125
3887
|
const setupMcp = flags["no-mcp"] ? false : true;
|
|
3126
3888
|
const isYes = Boolean(flags["yes"] || flags["y"]);
|
|
3127
|
-
|
|
3889
|
+
const detected = detectProjectManifest(targetDir);
|
|
3890
|
+
const candidateCode = detected.candidateCode;
|
|
3891
|
+
const candidateName = detected.candidateName;
|
|
3892
|
+
if (!isJson && detected.manifestFile && !projectCode) {
|
|
3893
|
+
console.log(`${colors.dim} \u2022 Detected ${detected.manifestType} project (${detected.manifestFile}): ${candidateName}${colors.reset}`);
|
|
3894
|
+
}
|
|
3895
|
+
if (isYes) {
|
|
3896
|
+
if (!projectCode) projectCode = candidateCode;
|
|
3897
|
+
if (!projectName) projectName = candidateName || projectCode;
|
|
3898
|
+
} else if (process.stdin.isTTY && (!projectCode || !projectName) && !isJson) {
|
|
3128
3899
|
const readline2 = await import("node:readline/promises");
|
|
3129
3900
|
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
3130
3901
|
try {
|
|
3131
3902
|
if (!projectCode) {
|
|
3132
|
-
const
|
|
3133
|
-
|
|
3134
|
-
);
|
|
3135
|
-
projectCode = codeAnswer.trim();
|
|
3903
|
+
const promptMsg = candidateCode ? `${colors.cyan}?${colors.reset} Project code (1-${MAX_PROJECT_CODE_LENGTH} alphanumeric characters) [${candidateCode}]: ` : `${colors.cyan}?${colors.reset} Project code (1-${MAX_PROJECT_CODE_LENGTH} alphanumeric characters, e.g. ALCE, DOCS): `;
|
|
3904
|
+
const codeAnswer = await rl.question(promptMsg);
|
|
3905
|
+
projectCode = codeAnswer.trim() || candidateCode;
|
|
3136
3906
|
}
|
|
3137
3907
|
if (!projectName && projectCode) {
|
|
3908
|
+
const defaultDisplayName = candidateName || `${projectCode} App`;
|
|
3138
3909
|
const nameAnswer = await rl.question(
|
|
3139
|
-
`${colors.cyan}?${colors.reset} Project display name
|
|
3910
|
+
`${colors.cyan}?${colors.reset} Project display name [${defaultDisplayName}]: `
|
|
3140
3911
|
);
|
|
3141
|
-
projectName = nameAnswer.trim() ||
|
|
3912
|
+
projectName = nameAnswer.trim() || defaultDisplayName;
|
|
3142
3913
|
}
|
|
3143
3914
|
} finally {
|
|
3144
3915
|
rl.close();
|
|
3145
3916
|
}
|
|
3146
3917
|
}
|
|
3147
3918
|
if (!projectCode) {
|
|
3148
|
-
console.error(`${colors.red}Error: Project code is required (--project <code>, -p <code>, or
|
|
3919
|
+
console.error(`${colors.red}Error: Project code is required (--project <code>, -p <code>, or in esedre.json).${colors.reset}`);
|
|
3149
3920
|
process.exit(1);
|
|
3150
3921
|
}
|
|
3151
3922
|
const codeVal = validateProjectCode(projectCode);
|
|
@@ -3156,6 +3927,11 @@ async function main() {
|
|
|
3156
3927
|
if (!projectName) {
|
|
3157
3928
|
projectName = projectCode;
|
|
3158
3929
|
}
|
|
3930
|
+
const nameVal = validateProjectName(projectName);
|
|
3931
|
+
if (!nameVal.valid) {
|
|
3932
|
+
console.error(`${colors.red}Error: ${nameVal.error}${colors.reset}`);
|
|
3933
|
+
process.exit(1);
|
|
3934
|
+
}
|
|
3159
3935
|
const existingProjects = await storage.getProjects();
|
|
3160
3936
|
const existing = existingProjects.find(
|
|
3161
3937
|
(p) => p.code.toLowerCase() === projectCode.toLowerCase()
|
|
@@ -3184,10 +3960,10 @@ async function main() {
|
|
|
3184
3960
|
} else if (flags["no-proxy"] !== void 0) {
|
|
3185
3961
|
setupProxy = false;
|
|
3186
3962
|
}
|
|
3187
|
-
const viteConfigPath = findViteConfig(
|
|
3963
|
+
const viteConfigPath = findViteConfig(targetDir);
|
|
3188
3964
|
if (viteConfigPath && setupProxy === void 0 && !isJson) {
|
|
3189
3965
|
try {
|
|
3190
|
-
const rawContent =
|
|
3966
|
+
const rawContent = fs9.readFileSync(viteConfigPath, "utf-8");
|
|
3191
3967
|
const alreadyConfigured = rawContent.includes("'/esedre'") || rawContent.includes('"/esedre"');
|
|
3192
3968
|
if (!alreadyConfigured) {
|
|
3193
3969
|
if (isYes) {
|
|
@@ -3197,7 +3973,7 @@ async function main() {
|
|
|
3197
3973
|
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
3198
3974
|
try {
|
|
3199
3975
|
const answer = await rl.question(
|
|
3200
|
-
`${colors.cyan}?${colors.reset} Detected ${colors.bold}${
|
|
3976
|
+
`${colors.cyan}?${colors.reset} Detected ${colors.bold}${path9.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] `
|
|
3201
3977
|
);
|
|
3202
3978
|
const trimmed = answer.trim().toLowerCase();
|
|
3203
3979
|
setupProxy = trimmed === "" || trimmed === "y" || trimmed === "yes";
|
|
@@ -3209,20 +3985,36 @@ async function main() {
|
|
|
3209
3985
|
} catch {
|
|
3210
3986
|
}
|
|
3211
3987
|
}
|
|
3212
|
-
const
|
|
3988
|
+
const rawHub = flags["data-dir"] || flags["hub"] || discovered.config?.dataDir;
|
|
3989
|
+
let hubRelPath = void 0;
|
|
3990
|
+
if (rawHub) {
|
|
3991
|
+
const singleHub = Array.isArray(rawHub) ? rawHub[0] : rawHub;
|
|
3992
|
+
const resolvedHub = path9.resolve(expandHome(singleHub));
|
|
3993
|
+
if (fs9.existsSync(resolvedHub)) {
|
|
3994
|
+
const rel = path9.relative(targetDir, resolvedHub).replace(/\\/g, "/");
|
|
3995
|
+
hubRelPath = rel.startsWith(".") ? rel : "./" + rel;
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
const res = initWorkspace(targetDir, {
|
|
3213
3999
|
projectCode,
|
|
3214
4000
|
projectName: registeredProj.name,
|
|
3215
4001
|
allowedProjects,
|
|
4002
|
+
dataDir: hubRelPath,
|
|
3216
4003
|
port,
|
|
3217
4004
|
setupMcp,
|
|
3218
4005
|
setupProxy
|
|
3219
4006
|
});
|
|
4007
|
+
try {
|
|
4008
|
+
addLocationToGlobalConfig(targetDir);
|
|
4009
|
+
} catch {
|
|
4010
|
+
}
|
|
3220
4011
|
let snapshotTickets = 0;
|
|
3221
4012
|
try {
|
|
3222
|
-
const snapshot = await generateProjectSnapshot(storage, projectCode,
|
|
4013
|
+
const snapshot = await generateProjectSnapshot(storage, projectCode, targetDir);
|
|
3223
4014
|
snapshotTickets = snapshot.totalTickets;
|
|
3224
4015
|
} catch {
|
|
3225
4016
|
}
|
|
4017
|
+
const effectivePort = resolvePorts(discovered.config, { port }).gateway;
|
|
3226
4018
|
if (isJson) {
|
|
3227
4019
|
console.log(JSON.stringify({
|
|
3228
4020
|
...res,
|
|
@@ -3230,9 +4022,9 @@ async function main() {
|
|
|
3230
4022
|
projectNewlyRegistered,
|
|
3231
4023
|
snapshotTickets,
|
|
3232
4024
|
urls: {
|
|
3233
|
-
ui: `http://localhost:${
|
|
3234
|
-
portfolio: `http://localhost:${
|
|
3235
|
-
api: `http://localhost:${
|
|
4025
|
+
ui: `http://localhost:${effectivePort}/app?project=${projectCode}`,
|
|
4026
|
+
portfolio: `http://localhost:${effectivePort}/app?project=all`,
|
|
4027
|
+
api: `http://localhost:${effectivePort}/api`
|
|
3236
4028
|
},
|
|
3237
4029
|
commands: {
|
|
3238
4030
|
start: "ese start",
|
|
@@ -3243,7 +4035,7 @@ async function main() {
|
|
|
3243
4035
|
}
|
|
3244
4036
|
}, null, 2));
|
|
3245
4037
|
} else {
|
|
3246
|
-
console.log(`${colors.bold}${colors.green}\u2714 Esedre workspace
|
|
4038
|
+
console.log(`${colors.bold}${colors.green}\u2714 Esedre workspace initialized successfully!${colors.reset}`);
|
|
3247
4039
|
console.log(` \u2022 Config: .esedre/esedre.json (Project: ${projectCode} (${registeredProj.name}), Version: ${CURRENT_ESEDRE_VERSION})`);
|
|
3248
4040
|
if (projectNewlyRegistered) {
|
|
3249
4041
|
console.log(` \u2022 Project: ${colors.green}Registered new project '${projectCode}' (${registeredProj.name}) in Esedre${colors.reset}`);
|
|
@@ -3255,13 +4047,9 @@ async function main() {
|
|
|
3255
4047
|
console.log(` \u2022 Snapshot: Generated .esedre/snapshot.json (${snapshotTickets} tickets projected)`);
|
|
3256
4048
|
console.log(` \u2022 MCP: ${res.mcpConfigured ? "Configured in .agents/mcp_config.json" : "Skipped"}`);
|
|
3257
4049
|
console.log(` \u2022 Skill: ${res.skillConfigured ? "Updated .agents/skills/esedre/SKILL.md" : `Retained (${res.skillStatus})`}`);
|
|
3258
|
-
|
|
3259
|
-
console.log(` \u2022 Global Store: ${res.globalStorePath} (verified write permissions)`);
|
|
3260
|
-
} else if (res.globalStoreStatus === "warning") {
|
|
3261
|
-
console.log(` \u2022 ${colors.yellow}Global Store: Fallback to ${res.globalStorePath} (${res.globalStoreError})${colors.reset}`);
|
|
3262
|
-
}
|
|
4050
|
+
console.log(` \u2022 Central Config: Linked into ~/.esedre/config.json`);
|
|
3263
4051
|
if (res.viteProxyStatus === "CONFIGURED") {
|
|
3264
|
-
console.log(` \u2022 ${colors.green}Vite Proxy: Added /esedre proxy to ${res.viteConfigFile} targeting http://127.0.0.1:${
|
|
4052
|
+
console.log(` \u2022 ${colors.green}Vite Proxy: Added /esedre proxy to ${res.viteConfigFile} targeting http://127.0.0.1:${effectivePort}${colors.reset}`);
|
|
3265
4053
|
} else if (res.viteProxyStatus === "ALREADY_CONFIGURED") {
|
|
3266
4054
|
console.log(` \u2022 Vite Proxy: /esedre is already configured in ${res.viteConfigFile}`);
|
|
3267
4055
|
} else if (res.viteProxyStatus === "SKIPPED") {
|
|
@@ -3271,7 +4059,7 @@ ${colors.bold}Recommendation:${colors.reset} To embed the dev planner within you
|
|
|
3271
4059
|
console.log(colors.dim + (res.recommendedProxySnippet?.split("\n").map((l) => " " + l).join("\n") || "") + colors.reset);
|
|
3272
4060
|
} else if (res.viteProxyStatus === "NOT_APPLICABLE") {
|
|
3273
4061
|
console.log(`
|
|
3274
|
-
${colors.bold}Recommendation:${colors.reset} If you wish to embed the dev planner within your project's UI, configure a reverse proxy for '/esedre' targeting http://127.0.0.1:${
|
|
4062
|
+
${colors.bold}Recommendation:${colors.reset} If you wish to embed the dev planner within your project's UI, configure a reverse proxy for '/esedre' targeting http://127.0.0.1:${effectivePort}:`);
|
|
3275
4063
|
console.log(colors.dim + (res.recommendedProxySnippet?.split("\n").map((l) => " " + l).join("\n") || "") + colors.reset);
|
|
3276
4064
|
} else if (res.viteProxyStatus === "MANUAL_REQUIRED") {
|
|
3277
4065
|
console.log(` \u2022 ${colors.yellow}Vite Proxy: Could not automatically inject proxy into ${res.viteConfigFile}.${colors.reset}`);
|
|
@@ -3281,11 +4069,11 @@ ${colors.bold}Recommendation:${colors.reset} Please manually add this proxy to y
|
|
|
3281
4069
|
}
|
|
3282
4070
|
console.log(`
|
|
3283
4071
|
${colors.bold}Next Steps:${colors.reset}`);
|
|
3284
|
-
console.log(` 1. ${colors.bold}Start the background
|
|
4072
|
+
console.log(` 1. ${colors.bold}Start the background server:${colors.reset}`);
|
|
3285
4073
|
console.log(` ${colors.cyan}ese start${colors.reset} (or: ${colors.dim}.esedre/ese start${colors.reset})`);
|
|
3286
4074
|
console.log(`
|
|
3287
4075
|
2. ${colors.bold}Open the visual Web UI:${colors.reset}`);
|
|
3288
|
-
console.log(` ${colors.cyan}http://localhost:${
|
|
4076
|
+
console.log(` ${colors.cyan}http://localhost:${effectivePort}/app?project=${projectCode}${colors.reset}`);
|
|
3289
4077
|
if (res.viteProxyStatus === "CONFIGURED" || res.viteProxyStatus === "ALREADY_CONFIGURED") {
|
|
3290
4078
|
console.log(` ${colors.dim}Embedded via host dev server:${colors.reset} http://localhost:5173/esedre/app?project=${projectCode}`);
|
|
3291
4079
|
}
|
|
@@ -3298,18 +4086,129 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
3298
4086
|
}
|
|
3299
4087
|
return;
|
|
3300
4088
|
}
|
|
4089
|
+
case "configure": {
|
|
4090
|
+
const subAction = positionals[0]?.toLowerCase();
|
|
4091
|
+
if (subAction === "add") {
|
|
4092
|
+
const targetPath = positionals[1] || ".";
|
|
4093
|
+
const addRes = addLocationToGlobalConfig(targetPath);
|
|
4094
|
+
if (isJson) {
|
|
4095
|
+
console.log(JSON.stringify(addRes, null, 2));
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
4098
|
+
if (addRes.type === "hub") {
|
|
4099
|
+
console.log(`${colors.bold}${colors.green}\u2714 Detected Esedre Data Hub (${addRes.projectCount || 0} projects)${colors.reset}`);
|
|
4100
|
+
console.log(` \u2022 Registered in ~/.esedre/config.json -> dataDir: ${addRes.path}`);
|
|
4101
|
+
} else if (addRes.type === "project") {
|
|
4102
|
+
console.log(`${colors.bold}${colors.green}\u2714 Detected Esedre project '${addRes.code}'${addRes.name ? ` (${addRes.name})` : ""}${colors.reset}`);
|
|
4103
|
+
console.log(` \u2022 Linked in ~/.esedre/config.json -> projects.${addRes.code}: ${addRes.path}`);
|
|
4104
|
+
} else {
|
|
4105
|
+
console.log(`${colors.yellow}\u26A0\uFE0F No Esedre project or data hub detected at "${addRes.path}"${colors.reset}`);
|
|
4106
|
+
console.log(`\u{1F449} Run 'ese init ${targetPath}' to bring this repository online first.`);
|
|
4107
|
+
}
|
|
4108
|
+
return;
|
|
4109
|
+
}
|
|
4110
|
+
if (subAction === "remove" || subAction === "rm") {
|
|
4111
|
+
const target = positionals[1];
|
|
4112
|
+
if (!target) {
|
|
4113
|
+
console.error(`${colors.red}Error: Project code or directory path required (e.g. ese configure remove ALCE)${colors.reset}`);
|
|
4114
|
+
process.exit(1);
|
|
4115
|
+
}
|
|
4116
|
+
const rmRes = removeLocationFromGlobalConfig(target);
|
|
4117
|
+
if (isJson) {
|
|
4118
|
+
console.log(JSON.stringify(rmRes, null, 2));
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
if (rmRes.removed) {
|
|
4122
|
+
console.log(`${colors.green}\u2714 Removed ${rmRes.type || "entry"} "${rmRes.target || target}" from central configuration (~/.esedre/config.json)${colors.reset}`);
|
|
4123
|
+
} else {
|
|
4124
|
+
console.log(`${colors.yellow}\u25CB "${target}" not found in central configuration.${colors.reset}`);
|
|
4125
|
+
}
|
|
4126
|
+
return;
|
|
4127
|
+
}
|
|
4128
|
+
if (subAction === "set") {
|
|
4129
|
+
const key = positionals[1];
|
|
4130
|
+
const val = positionals[2];
|
|
4131
|
+
if (!key || val === void 0) {
|
|
4132
|
+
console.error(`${colors.red}Error: Key and value required (e.g. ese configure set port 5674)${colors.reset}`);
|
|
4133
|
+
process.exit(1);
|
|
4134
|
+
}
|
|
4135
|
+
const parsedVal = /^\d+$/.test(val) ? parseInt(val, 10) : val;
|
|
4136
|
+
const setRes = setGlobalConfigKey(key, parsedVal);
|
|
4137
|
+
if (isJson) {
|
|
4138
|
+
console.log(JSON.stringify(setRes, null, 2));
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4141
|
+
console.log(`${colors.green}\u2714 Updated central configuration: ${key} = ${JSON.stringify(parsedVal)}${colors.reset}`);
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
if (subAction === "get") {
|
|
4145
|
+
const key = positionals[1];
|
|
4146
|
+
const cfg = readGlobalConfig() || {};
|
|
4147
|
+
const val = key ? getGlobalConfigKey(key) : cfg;
|
|
4148
|
+
if (isJson) {
|
|
4149
|
+
console.log(JSON.stringify({ key, value: val }, null, 2));
|
|
4150
|
+
} else {
|
|
4151
|
+
console.log(val !== void 0 ? typeof val === "object" ? JSON.stringify(val, null, 2) : String(val) : `${colors.dim}(not set)${colors.reset}`);
|
|
4152
|
+
}
|
|
4153
|
+
return;
|
|
4154
|
+
}
|
|
4155
|
+
const globalCfg = readGlobalConfig() || {};
|
|
4156
|
+
const configPath = getGlobalConfigPath();
|
|
4157
|
+
if (isJson) {
|
|
4158
|
+
console.log(JSON.stringify({
|
|
4159
|
+
configPath,
|
|
4160
|
+
config: globalCfg
|
|
4161
|
+
}, null, 2));
|
|
4162
|
+
return;
|
|
4163
|
+
}
|
|
4164
|
+
console.log(`
|
|
4165
|
+
${colors.bold}${colors.cyan}Esedre Central Configuration${colors.reset}`);
|
|
4166
|
+
console.log(` \u2022 Config File: ${configPath} ${fs9.existsSync(configPath) ? colors.green + "(active)" + colors.reset : colors.dim + "(default)" + colors.reset}`);
|
|
4167
|
+
console.log(` \u2022 Default Port: ${globalCfg.port || DEFAULT_ESEDRE_PORT}`);
|
|
4168
|
+
console.log(`
|
|
4169
|
+
${colors.bold}Data Hubs (dataDir):${colors.reset}`);
|
|
4170
|
+
if (globalCfg.dataDir) {
|
|
4171
|
+
const hubs = Array.isArray(globalCfg.dataDir) ? globalCfg.dataDir : [globalCfg.dataDir];
|
|
4172
|
+
for (const h of hubs) {
|
|
4173
|
+
console.log(` \u2022 ${colors.cyan}${h}${colors.reset}`);
|
|
4174
|
+
}
|
|
4175
|
+
} else {
|
|
4176
|
+
console.log(` ${colors.dim}(none configured: add with 'ese configure add <hubPath>')${colors.reset}`);
|
|
4177
|
+
}
|
|
4178
|
+
console.log(`
|
|
4179
|
+
${colors.bold}Linked Projects (projects):${colors.reset}`);
|
|
4180
|
+
if (globalCfg.projects && Object.keys(globalCfg.projects).length > 0) {
|
|
4181
|
+
for (const [code, p] of Object.entries(globalCfg.projects)) {
|
|
4182
|
+
console.log(` \u2022 ${colors.bold}${code}${colors.reset}: ${colors.cyan}${p}${colors.reset}`);
|
|
4183
|
+
}
|
|
4184
|
+
} else {
|
|
4185
|
+
console.log(` ${colors.dim}(none linked: link with 'ese configure add <projectPath>')${colors.reset}`);
|
|
4186
|
+
}
|
|
4187
|
+
console.log(`
|
|
4188
|
+
${colors.bold}Commands:${colors.reset}`);
|
|
4189
|
+
console.log(` \u2022 Link a hub or project: ${colors.cyan}ese configure add <path>${colors.reset}`);
|
|
4190
|
+
console.log(` \u2022 Unlink a project or hub: ${colors.cyan}ese configure remove <code|path>${colors.reset}`);
|
|
4191
|
+
console.log(` \u2022 Set setting value: ${colors.cyan}ese configure set <key> <value>${colors.reset}`);
|
|
4192
|
+
console.log(` \u2022 Bring new repo online: ${colors.cyan}ese init [path]${colors.reset}`);
|
|
4193
|
+
return;
|
|
4194
|
+
}
|
|
3301
4195
|
case "upgrade": {
|
|
3302
|
-
const
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
4196
|
+
const targetDir = positionals[0] ? path9.resolve(positionals[0]) : discovered.workspaceRoot;
|
|
4197
|
+
const targetDiscovered = targetDir === discovered.workspaceRoot ? discovered : findEsedreConfig(targetDir, { fallbackToGlobal: true });
|
|
4198
|
+
const isForce = Boolean(flags["force"] || flags["f"]);
|
|
4199
|
+
const res = configureWorkspace(targetDir, {
|
|
4200
|
+
projectCode: targetDiscovered.config?.projectCode,
|
|
4201
|
+
allowedProjects: targetDiscovered.config?.allowedProjects,
|
|
4202
|
+
port: targetDiscovered.config?.port,
|
|
4203
|
+
force: isForce
|
|
3306
4204
|
});
|
|
3307
|
-
const projectCode =
|
|
4205
|
+
const projectCode = targetDiscovered.config?.projectCode;
|
|
3308
4206
|
if (!projectCode) {
|
|
3309
4207
|
console.error(`${colors.red}Error: "projectCode" is required in esedre.json configuration (no default fallback).${colors.reset}`);
|
|
3310
4208
|
process.exit(1);
|
|
3311
4209
|
}
|
|
3312
|
-
const
|
|
4210
|
+
const targetStorage = targetDir === discovered.workspaceRoot ? storage : new SecurityFilter(new FilesystemStorageAdapter(targetDir, targetDiscovered.config), targetDiscovered.config);
|
|
4211
|
+
const snapshot = await generateProjectSnapshot(targetStorage, projectCode, targetDir);
|
|
3313
4212
|
if (isJson) {
|
|
3314
4213
|
console.log(JSON.stringify({ upgrade: res, snapshot: { totalTickets: snapshot.totalTickets } }, null, 2));
|
|
3315
4214
|
} else {
|
|
@@ -3317,8 +4216,10 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
3317
4216
|
console.log(` \u2022 Esedre Engine Version: ${CURRENT_ESEDRE_VERSION}`);
|
|
3318
4217
|
console.log(` \u2022 Wrappers updated in .esedre/`);
|
|
3319
4218
|
console.log(` \u2022 Refreshed .esedre/snapshot.json (${snapshot.totalTickets} tickets projected)`);
|
|
3320
|
-
if (res.
|
|
3321
|
-
console.log(` \u2022 ${colors.
|
|
4219
|
+
if (res.skillConfigured) {
|
|
4220
|
+
console.log(` \u2022 ${colors.green}Updated .agents/skills/esedre/SKILL.md to latest template${colors.reset}`);
|
|
4221
|
+
} else if (res.skillStatus === "CUSTOMIZED") {
|
|
4222
|
+
console.log(` \u2022 ${colors.yellow}Notice: .agents/skills/esedre/SKILL.md is customized; preserved user modifications (use --force to overwrite).${colors.reset}`);
|
|
3322
4223
|
}
|
|
3323
4224
|
}
|
|
3324
4225
|
return;
|
|
@@ -3346,7 +4247,32 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
3346
4247
|
return;
|
|
3347
4248
|
}
|
|
3348
4249
|
case "start": {
|
|
3349
|
-
const
|
|
4250
|
+
const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
|
|
4251
|
+
const ports = resolvePorts(discovered.config, { port: flagPort });
|
|
4252
|
+
const port = ports.gateway;
|
|
4253
|
+
const isForeground = Boolean(flags["foreground"] || flags["f"]);
|
|
4254
|
+
if (isForeground) {
|
|
4255
|
+
const cluster = startGatewayCluster({
|
|
4256
|
+
gatewayPort: ports.gateway,
|
|
4257
|
+
uiPort: ports.ui,
|
|
4258
|
+
apiPort: ports.api,
|
|
4259
|
+
storage,
|
|
4260
|
+
workspaceRoot: discovered.workspaceRoot
|
|
4261
|
+
});
|
|
4262
|
+
console.log(`${colors.bold}${colors.green}\u2714 Esedre Server active on http://localhost:${ports.gateway}${colors.reset}`);
|
|
4263
|
+
console.log(` \u2022 Web UI: http://localhost:${ports.gateway}/app (internal: ${ports.ui})`);
|
|
4264
|
+
console.log(` \u2022 REST API: http://localhost:${ports.gateway}/api (internal: ${ports.api})`);
|
|
4265
|
+
console.log(` \u2022 Rewrite /: http://localhost:${ports.gateway}/ -> http://localhost:${ports.gateway}/app/`);
|
|
4266
|
+
console.log(` \u2022 Portfolio: http://localhost:${ports.gateway}/app?project=all`);
|
|
4267
|
+
console.log(` \u2022 Press Ctrl+C to stop.`);
|
|
4268
|
+
process.on("SIGINT", async () => {
|
|
4269
|
+
console.log(`
|
|
4270
|
+
${colors.dim}Shutting down Esedre cluster...${colors.reset}`);
|
|
4271
|
+
await cluster.close();
|
|
4272
|
+
process.exit(0);
|
|
4273
|
+
});
|
|
4274
|
+
return;
|
|
4275
|
+
}
|
|
3350
4276
|
const quiet = Boolean(flags["quiet"] || isJson);
|
|
3351
4277
|
const state = await startDaemon({ port, quiet, workspaceRoot: discovered.workspaceRoot });
|
|
3352
4278
|
if (isJson) {
|
|
@@ -3355,7 +4281,8 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
3355
4281
|
return;
|
|
3356
4282
|
}
|
|
3357
4283
|
case "stop": {
|
|
3358
|
-
const
|
|
4284
|
+
const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
|
|
4285
|
+
const port = resolvePorts(discovered.config, { port: flagPort }).gateway;
|
|
3359
4286
|
const quiet = Boolean(flags["quiet"] || isJson);
|
|
3360
4287
|
const stopped = await stopDaemon({ port, quiet, workspaceRoot: discovered.workspaceRoot });
|
|
3361
4288
|
if (isJson) {
|
|
@@ -3364,7 +4291,8 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
3364
4291
|
return;
|
|
3365
4292
|
}
|
|
3366
4293
|
case "status": {
|
|
3367
|
-
const
|
|
4294
|
+
const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
|
|
4295
|
+
const port = resolvePorts(discovered.config, { port: flagPort }).gateway;
|
|
3368
4296
|
const status = await getDaemonStatus({ port, workspaceRoot: discovered.workspaceRoot });
|
|
3369
4297
|
if (isJson) {
|
|
3370
4298
|
console.log(JSON.stringify(status, null, 2));
|
|
@@ -3373,13 +4301,19 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
3373
4301
|
console.log(`${colors.bold}${colors.green}\u25CF Esedre daemon is RUNNING${colors.reset}`);
|
|
3374
4302
|
console.log(` \u2022 Port: ${status.port}`);
|
|
3375
4303
|
if (status.pid) console.log(` \u2022 PID: ${status.pid}`);
|
|
4304
|
+
if (status.version) console.log(` \u2022 Version: v${status.version}`);
|
|
3376
4305
|
if (status.uptimeSeconds !== void 0) console.log(` \u2022 Uptime: ${status.uptimeSeconds}s`);
|
|
3377
4306
|
if (status.startedAt) console.log(` \u2022 Started: ${status.startedAt}`);
|
|
3378
4307
|
if (status.projects?.length) console.log(` \u2022 Projects: ${status.projects.join(", ")}`);
|
|
3379
4308
|
if (status.logFile) console.log(` \u2022 Logs: ${status.logFile}`);
|
|
4309
|
+
if (status.staleVersion) {
|
|
4310
|
+
console.log(`
|
|
4311
|
+
${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but v${status.installedVersion} is installed.${colors.reset}`);
|
|
4312
|
+
console.log(` Restart to apply updates: ${colors.cyan}ese stop && ese start${colors.reset}`);
|
|
4313
|
+
}
|
|
3380
4314
|
} else {
|
|
3381
4315
|
console.log(`${colors.yellow}\u25CB Esedre daemon is STOPPED (port ${status.port})${colors.reset}`);
|
|
3382
|
-
if (status.logFile &&
|
|
4316
|
+
if (status.logFile && fs9.existsSync(status.logFile)) {
|
|
3383
4317
|
console.log(` \u2022 Recent Logs: ${status.logFile}`);
|
|
3384
4318
|
}
|
|
3385
4319
|
}
|
|
@@ -3387,80 +4321,12 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
3387
4321
|
return;
|
|
3388
4322
|
}
|
|
3389
4323
|
case "logs": {
|
|
3390
|
-
const
|
|
4324
|
+
const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
|
|
4325
|
+
const port = resolvePorts(discovered.config, { port: flagPort }).gateway;
|
|
3391
4326
|
const lines = flags["lines"] ? parseInt(String(flags["lines"]), 10) : 40;
|
|
3392
4327
|
printDaemonLogs({ port, lines, workspaceRoot: discovered.workspaceRoot });
|
|
3393
4328
|
return;
|
|
3394
4329
|
}
|
|
3395
|
-
case "serve": {
|
|
3396
|
-
const gatewayPort = flags["port"] ? parseInt(String(flags["port"]), 10) : discovered.config?.port || 5674;
|
|
3397
|
-
const cluster = startGatewayCluster({
|
|
3398
|
-
gatewayPort,
|
|
3399
|
-
uiPort: gatewayPort + 1,
|
|
3400
|
-
apiPort: gatewayPort + 2,
|
|
3401
|
-
storage,
|
|
3402
|
-
workspaceRoot: discovered.workspaceRoot
|
|
3403
|
-
});
|
|
3404
|
-
console.log(`${colors.bold}${colors.green}\u2714 Esedre Server active on http://localhost:${gatewayPort}${colors.reset}`);
|
|
3405
|
-
console.log(` \u2022 Web UI: http://localhost:${gatewayPort}/app (internal: ${gatewayPort + 1})`);
|
|
3406
|
-
console.log(` \u2022 REST API: http://localhost:${gatewayPort}/api (internal: ${gatewayPort + 2})`);
|
|
3407
|
-
console.log(` \u2022 Rewrite /: http://localhost:${gatewayPort}/ -> http://localhost:${gatewayPort}/app/`);
|
|
3408
|
-
console.log(` \u2022 Portfolio: http://localhost:${gatewayPort}/app?project=all`);
|
|
3409
|
-
console.log(` \u2022 Press Ctrl+C to stop.`);
|
|
3410
|
-
process.on("SIGINT", async () => {
|
|
3411
|
-
console.log(`
|
|
3412
|
-
${colors.dim}Shutting down Esedre cluster...${colors.reset}`);
|
|
3413
|
-
await cluster.close();
|
|
3414
|
-
process.exit(0);
|
|
3415
|
-
});
|
|
3416
|
-
return;
|
|
3417
|
-
}
|
|
3418
|
-
case "daemon": {
|
|
3419
|
-
const sub = positionals[0] || "status";
|
|
3420
|
-
const port = flags["port"] ? parseInt(String(flags["port"]), 10) : discovered.config?.port || 5674;
|
|
3421
|
-
const quiet = Boolean(flags["quiet"] || isJson);
|
|
3422
|
-
if (sub === "start") {
|
|
3423
|
-
const state = await startDaemon({ port, quiet, workspaceRoot: discovered.workspaceRoot });
|
|
3424
|
-
if (isJson) console.log(JSON.stringify(state, null, 2));
|
|
3425
|
-
return;
|
|
3426
|
-
} else if (sub === "stop") {
|
|
3427
|
-
const stopped = await stopDaemon({ port, quiet, workspaceRoot: discovered.workspaceRoot });
|
|
3428
|
-
if (isJson) console.log(JSON.stringify({ port, stopped }, null, 2));
|
|
3429
|
-
return;
|
|
3430
|
-
} else if (sub === "status") {
|
|
3431
|
-
const status = await getDaemonStatus({ port, workspaceRoot: discovered.workspaceRoot });
|
|
3432
|
-
if (isJson) {
|
|
3433
|
-
console.log(JSON.stringify(status, null, 2));
|
|
3434
|
-
} else {
|
|
3435
|
-
if (status.running) {
|
|
3436
|
-
console.log(`${colors.bold}${colors.green}\u25CF Esedre daemon is RUNNING${colors.reset}`);
|
|
3437
|
-
console.log(` \u2022 Port: ${status.port}`);
|
|
3438
|
-
if (status.pid) console.log(` \u2022 PID: ${status.pid}`);
|
|
3439
|
-
if (status.uptimeSeconds !== void 0) console.log(` \u2022 Uptime: ${status.uptimeSeconds}s`);
|
|
3440
|
-
if (status.startedAt) console.log(` \u2022 Started: ${status.startedAt}`);
|
|
3441
|
-
if (status.projects?.length) console.log(` \u2022 Projects: ${status.projects.join(", ")}`);
|
|
3442
|
-
if (status.logFile) console.log(` \u2022 Logs: ${status.logFile}`);
|
|
3443
|
-
} else {
|
|
3444
|
-
console.log(`${colors.yellow}\u25CB Esedre daemon is STOPPED (port ${status.port})${colors.reset}`);
|
|
3445
|
-
if (status.logFile && fs8.existsSync(status.logFile)) {
|
|
3446
|
-
console.log(` \u2022 Recent Logs: ${status.logFile}`);
|
|
3447
|
-
}
|
|
3448
|
-
}
|
|
3449
|
-
}
|
|
3450
|
-
return;
|
|
3451
|
-
} else if (sub === "logs") {
|
|
3452
|
-
const logFile = path8.resolve(discovered.workspaceRoot || process.cwd(), ".esedre", "daemon.log");
|
|
3453
|
-
if (fs8.existsSync(logFile)) {
|
|
3454
|
-
console.log(fs8.readFileSync(logFile, "utf-8"));
|
|
3455
|
-
} else {
|
|
3456
|
-
console.log(`${colors.dim}No daemon log file found at ${logFile}${colors.reset}`);
|
|
3457
|
-
}
|
|
3458
|
-
return;
|
|
3459
|
-
} else {
|
|
3460
|
-
console.error(`${colors.red}Unknown daemon subcommand: '${sub}'. Expected 'start', 'stop', 'status', or 'logs'.${colors.reset}`);
|
|
3461
|
-
process.exit(1);
|
|
3462
|
-
}
|
|
3463
|
-
}
|
|
3464
4330
|
case "projects": {
|
|
3465
4331
|
const projects = await storage.getProjects();
|
|
3466
4332
|
if (isJson) {
|
|
@@ -3494,9 +4360,22 @@ ${colors.dim}Shutting down Esedre cluster...${colors.reset}`);
|
|
|
3494
4360
|
sha1: t.sha1 || t.meta.sha1
|
|
3495
4361
|
})), null, 2));
|
|
3496
4362
|
} else {
|
|
3497
|
-
|
|
3498
|
-
|
|
4363
|
+
if (tickets.length === 0) {
|
|
4364
|
+
console.log(`${colors.dim}No tickets found matching criteria.${colors.reset}`);
|
|
4365
|
+
const projects = await storage.getProjects();
|
|
4366
|
+
if (discovered.config?.projectCode) {
|
|
4367
|
+
console.log(`${colors.dim}Tip: Create your first ticket with 'ese create --title "..." --type Feature'.${colors.reset}`);
|
|
4368
|
+
} else if (projects.length > 0) {
|
|
4369
|
+
const codes = projects.map((p) => p.code).join(", ");
|
|
4370
|
+
console.log(`${colors.dim}Tip: Create a ticket with 'ese create --title "..." --type Feature --project <${codes}>'.${colors.reset}`);
|
|
4371
|
+
} else {
|
|
4372
|
+
console.log(`${colors.dim}Tip: Run 'ese init' to initialize this repository.${colors.reset}`);
|
|
4373
|
+
}
|
|
4374
|
+
} else {
|
|
4375
|
+
console.log(formatTicketListTable(tickets));
|
|
4376
|
+
console.log(`
|
|
3499
4377
|
${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
4378
|
+
}
|
|
3500
4379
|
}
|
|
3501
4380
|
return;
|
|
3502
4381
|
}
|
|
@@ -3535,11 +4414,11 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
3535
4414
|
if (setPlan || filePath) {
|
|
3536
4415
|
let content = setPlan;
|
|
3537
4416
|
if (filePath) {
|
|
3538
|
-
if (!
|
|
4417
|
+
if (!fs9.existsSync(filePath)) {
|
|
3539
4418
|
console.error(`${colors.red}Error: Plan file "${filePath}" not found.${colors.reset}`);
|
|
3540
4419
|
process.exit(1);
|
|
3541
4420
|
}
|
|
3542
|
-
content =
|
|
4421
|
+
content = fs9.readFileSync(filePath, "utf-8");
|
|
3543
4422
|
}
|
|
3544
4423
|
await storage.savePlan(id, content, lastHash);
|
|
3545
4424
|
console.log(`${colors.green}\u2714 Implementation plan saved for Ticket #${id}${colors.reset}`);
|
|
@@ -3568,10 +4447,11 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
3568
4447
|
const projectCode = flags["project"] || discovered.config?.projectCode;
|
|
3569
4448
|
if (!projectCode) {
|
|
3570
4449
|
console.error(`${colors.red}Error: Project is required to create a ticket (--project <code> or configure projectCode in esedre.json).${colors.reset}`);
|
|
4450
|
+
console.error(`${colors.dim}Tip: Run 'ese init' to initialize this repository.${colors.reset}`);
|
|
3571
4451
|
process.exit(1);
|
|
3572
4452
|
}
|
|
3573
4453
|
const complexity = flags["complexity"] || "Medium";
|
|
3574
|
-
const estimatedEffort = flags["effort"] || "2.0
|
|
4454
|
+
const estimatedEffort = flags["effort"] || "2.0 - 4.0 hours";
|
|
3575
4455
|
const summary = flags["summary"] || void 0;
|
|
3576
4456
|
const submittedBy = flags["author"] || "Developer";
|
|
3577
4457
|
const val = validateProjectCode(projectCode);
|
|
@@ -3599,14 +4479,11 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
3599
4479
|
case "update": {
|
|
3600
4480
|
const idStr = positionals[0];
|
|
3601
4481
|
if (!idStr) {
|
|
3602
|
-
console.error(`${colors.red}Error: Ticket ID is required (e.g. ese update 96 --status "Completed")${colors.reset}`);
|
|
3603
|
-
process.exit(1);
|
|
3604
|
-
}
|
|
3605
|
-
const id = parseInt(idStr, 10);
|
|
3606
|
-
if (isNaN(id)) {
|
|
3607
|
-
console.error(`${colors.red}Error: Invalid Ticket ID "${idStr}"${colors.reset}`);
|
|
4482
|
+
console.error(`${colors.red}Error: Ticket ID is required (e.g. ese update 96 --status "Completed" or ese update Profe-96)${colors.reset}`);
|
|
3608
4483
|
process.exit(1);
|
|
3609
4484
|
}
|
|
4485
|
+
const projectFlag = flags["project"];
|
|
4486
|
+
const lookupKey = projectFlag && /^\d+$/.test(idStr) ? `${projectFlag}-${idStr}` : idStr;
|
|
3610
4487
|
const status = flags["status"];
|
|
3611
4488
|
const title = flags["title"];
|
|
3612
4489
|
const complexity = flags["complexity"];
|
|
@@ -3627,36 +4504,34 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
3627
4504
|
if (effort) updates.estimatedEffort = effort;
|
|
3628
4505
|
if (inDev !== void 0) updates.isActivePlanning = inDev;
|
|
3629
4506
|
if (flag) updates.featureFlag = flag;
|
|
3630
|
-
const updated = await storage.updateTicket(
|
|
4507
|
+
const updated = await storage.updateTicket(lookupKey, updates, lastHash);
|
|
3631
4508
|
if (isJson) {
|
|
3632
4509
|
console.log(JSON.stringify(updated, null, 2));
|
|
3633
4510
|
} else {
|
|
3634
|
-
console.log(`${colors.green}\u2714 Updated Ticket #${
|
|
4511
|
+
console.log(`${colors.green}\u2714 Updated Ticket #${lookupKey}: ${updated.meta.title} (Status: ${updated.meta.status}, SHA-1: ${updated.sha1?.slice(0, 8)})${colors.reset}`);
|
|
3635
4512
|
}
|
|
3636
4513
|
return;
|
|
3637
4514
|
}
|
|
3638
4515
|
case "comment": {
|
|
3639
4516
|
const idStr = positionals[0];
|
|
3640
4517
|
if (!idStr) {
|
|
3641
|
-
console.error(`${colors.red}Error: Ticket ID is required (e.g. ese comment 96 --text "...")`);
|
|
3642
|
-
process.exit(1);
|
|
3643
|
-
}
|
|
3644
|
-
const id = parseInt(idStr, 10);
|
|
3645
|
-
if (isNaN(id)) {
|
|
3646
|
-
console.error(`${colors.red}Error: Invalid Ticket ID "${idStr}"`);
|
|
4518
|
+
console.error(`${colors.red}Error: Ticket ID is required (e.g. ese comment 96 --text "..." or ese comment Profe-96)${colors.reset}`);
|
|
3647
4519
|
process.exit(1);
|
|
3648
4520
|
}
|
|
3649
|
-
const
|
|
4521
|
+
const projectFlag = flags["project"];
|
|
4522
|
+
const lookupKey = projectFlag && /^\d+$/.test(idStr) ? `${projectFlag}-${idStr}` : idStr;
|
|
4523
|
+
const positionalText = positionals.slice(1).join(" ").trim();
|
|
4524
|
+
const text = flags["text"] || (positionalText || void 0);
|
|
3650
4525
|
if (!text) {
|
|
3651
|
-
console.error(`${colors.red}Error:
|
|
4526
|
+
console.error(`${colors.red}Error: Comment text is required (e.g. ese comment 96 "Verified tests" or --text "...")${colors.reset}`);
|
|
3652
4527
|
process.exit(1);
|
|
3653
4528
|
}
|
|
3654
4529
|
const author = flags["author"] || "User";
|
|
3655
|
-
const comment = await storage.addComment(
|
|
4530
|
+
const comment = await storage.addComment(lookupKey, { author, text });
|
|
3656
4531
|
if (isJson) {
|
|
3657
4532
|
console.log(JSON.stringify(comment, null, 2));
|
|
3658
4533
|
} else {
|
|
3659
|
-
console.log(`${colors.green}\u2714 Added comment to Ticket #${
|
|
4534
|
+
console.log(`${colors.green}\u2714 Added comment to Ticket #${lookupKey} by ${comment.author}${colors.reset}`);
|
|
3660
4535
|
}
|
|
3661
4536
|
return;
|
|
3662
4537
|
}
|