agent-enderun 0.8.0 → 0.8.1

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.
Files changed (2) hide show
  1. package/package.json +2 -1
  2. package/src/cli/index.ts +2154 -0
@@ -0,0 +1,2154 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ import crypto from "crypto";
7
+ import { execSync } from "child_process";
8
+ import cp from "child_process";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const sourceDir = path.join(__dirname, "..", "..");
13
+ const targetDir = process.cwd();
14
+
15
+ // --- CONSTANTS ---
16
+ const FRAMEWORK_VERSION = getPackageVersion();
17
+
18
+ // --- HELPER FUNCTIONS ---
19
+
20
+ function getPackageVersion() {
21
+ const pkg = JSON.parse(fs.readFileSync(path.join(sourceDir, "package.json"), "utf8"));
22
+ return pkg.version;
23
+ }
24
+
25
+ function getFrameworkDir() {
26
+ return ".gemini";
27
+ }
28
+
29
+ function getMemoryPath() {
30
+ return path.join(targetDir, getFrameworkDir(), "PROJECT_MEMORY.md");
31
+ }
32
+
33
+ export function generateULID(seedTime = Date.now(), seed?: number) {
34
+ const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
35
+ const ENCODING_LEN = ENCODING.length;
36
+ let time = seedTime;
37
+ const timeChars = new Array(10);
38
+ for (let i = 9; i >= 0; i--) {
39
+ timeChars[i] = ENCODING.charAt(time % ENCODING_LEN);
40
+ time = Math.floor(time / ENCODING_LEN);
41
+ }
42
+ const randomChars = new Array(16);
43
+ if (seed) {
44
+ let pseudoRandom = seed;
45
+ for (let i = 0; i < 16; i++) {
46
+ pseudoRandom = (pseudoRandom * 16807) % 2147483647;
47
+ randomChars[i] = ENCODING.charAt(pseudoRandom % ENCODING_LEN);
48
+ }
49
+ } else {
50
+ for (let i = 0; i < 16; i++) {
51
+ randomChars[i] = ENCODING.charAt(Math.floor(Math.random() * ENCODING_LEN));
52
+ }
53
+ }
54
+ return timeChars.join("") + randomChars.join("");
55
+ }
56
+
57
+ function sleep(ms: number): void {
58
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
59
+ }
60
+
61
+ function acquireMemoryLock(lockPath: string, maxRetries = 5): boolean {
62
+ for (let attempt = 0; attempt < maxRetries; attempt += 1) {
63
+ try {
64
+ const fd = fs.openSync(lockPath, "wx");
65
+ fs.closeSync(fd);
66
+ return true;
67
+ } catch (error) {
68
+ if (error?.code !== "EEXIST") throw error;
69
+ if (attempt < maxRetries - 1) sleep(1000);
70
+ }
71
+ }
72
+ return false;
73
+ }
74
+
75
+ function releaseMemoryLock(lockPath: string): void {
76
+ if (fs.existsSync(lockPath)) fs.unlinkSync(lockPath);
77
+ }
78
+
79
+ function insertTaskRow(memoryContent: string, row: string): string | null {
80
+ const sectionHeader = "## ACTIVE TASKS";
81
+ const tableDivider = "| :--- | :--- | :--- | :--- | :--- |";
82
+ const sectionIndex = memoryContent.indexOf(sectionHeader);
83
+ if (sectionIndex === -1) return null;
84
+ const dividerIndex = memoryContent.indexOf(tableDivider, sectionIndex);
85
+ if (dividerIndex === -1) return null;
86
+ const dividerLineEnd = memoryContent.indexOf("\n", dividerIndex);
87
+ if (dividerLineEnd === -1) return null;
88
+
89
+ return (
90
+ memoryContent.slice(0, dividerLineEnd + 1) +
91
+ `${row}\n` +
92
+ memoryContent.slice(dividerLineEnd + 1)
93
+ );
94
+ }
95
+
96
+ export function sanitizeTableCell(value: unknown): string {
97
+ return String(value).replace(/\|/g, "\\|").replace(/\r?\n/g, " ").trim();
98
+ }
99
+
100
+ export function normalizeAgentName(agent?: unknown): string {
101
+ return String(agent || "manager").replace(/^@+/, "").trim() || "manager";
102
+ }
103
+
104
+ export function normalizePriority(priority?: unknown): string {
105
+ const normalized = String(priority || "P2").toUpperCase().trim();
106
+ return /^P[0-3]$/.test(normalized) ? normalized : "P2";
107
+ }
108
+
109
+ function mergePackageJson(targetPath: string, sourcePath: string): void {
110
+ let targetPkg = {};
111
+ if (fs.existsSync(targetPath)) {
112
+ try {
113
+ targetPkg = JSON.parse(fs.readFileSync(targetPath, "utf8"));
114
+ } catch {
115
+ console.warn("⚠️ Could not parse existing package.json, creating a new one.");
116
+ }
117
+ }
118
+
119
+ const sourcePkg = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
120
+
121
+ type PackageMap = Record<string, string>;
122
+ const sanitizeDeps = (deps: PackageMap | Record<string, unknown> | undefined): Record<string, string> | undefined => {
123
+ if (!deps) return deps as undefined;
124
+ const cleaned: Record<string, string> = {};
125
+ for (const [name, version] of Object.entries(deps as Record<string, unknown>)) {
126
+ cleaned[name] = (typeof version === "string" && version.startsWith("workspace:")) ? "*" : String(version || "");
127
+ }
128
+ return cleaned;
129
+ };
130
+
131
+ interface PackageJson {
132
+ name?: string;
133
+ version?: string;
134
+ type?: string;
135
+ workspaces?: string[];
136
+ dependencies?: Record<string, string>;
137
+ devDependencies?: Record<string, string>;
138
+ peerDependencies?: Record<string, string>;
139
+ optionalDependencies?: Record<string, string>;
140
+ scripts?: Record<string, string>;
141
+ enderun?: Record<string, unknown>;
142
+ }
143
+
144
+ const actualTargetScope = (targetPkg as PackageJson)?.name && (targetPkg as PackageJson).name!.startsWith("@")
145
+ ? (targetPkg as PackageJson).name!.split("/")[0]
146
+ : ((targetPkg as PackageJson).name ? `@${(targetPkg as PackageJson).name}` : "");
147
+
148
+ // Cleanup potential leftovers from previous bugged runs where agent-enderun was renamed to target name
149
+ if (actualTargetScope) {
150
+ const scopeName = actualTargetScope.startsWith("@") ? actualTargetScope.slice(1) : actualTargetScope;
151
+ const cleanup = (obj: Record<string, unknown> | undefined): void => {
152
+ if (!obj) return;
153
+ delete obj[scopeName];
154
+ delete obj[actualTargetScope];
155
+ delete obj["agent-enderun"]; // Will be re-added correctly
156
+ };
157
+ cleanup((targetPkg as PackageJson).devDependencies);
158
+ cleanup((targetPkg as PackageJson).dependencies);
159
+ }
160
+
161
+ targetPkg.dependencies = sanitizeDeps({
162
+ ...targetPkg.dependencies,
163
+ ...sourcePkg.dependencies
164
+ });
165
+
166
+ // Merge scripts
167
+ const pkgMgr = getPackageManager();
168
+ const runCmd = pkgMgr === "yarn" ? "yarn" : (pkgMgr === "pnpm" ? "pnpm" : "npm run");
169
+
170
+ targetPkg.scripts = {
171
+ ...targetPkg.scripts,
172
+ "enderun:status": "agent-enderun status",
173
+ "enderun:trace": "agent-enderun trace:new",
174
+ "enderun:verify": "agent-enderun verify-contract",
175
+ "enderun:check": "agent-enderun check",
176
+ "enderun:test": "vitest run",
177
+ "enderun:test:watch": "vitest",
178
+ "enderun:build": `${runCmd} build --prefix framework-mcp`,
179
+ };
180
+
181
+ const sourceDevDeps = sourcePkg.devDependencies || {};
182
+ targetPkg.devDependencies = sanitizeDeps({
183
+ ...targetPkg.devDependencies,
184
+ "agent-enderun": `^${sourcePkg.version}`,
185
+ ...(sourceDevDeps["@modelcontextprotocol/sdk"] ? {"@modelcontextprotocol/sdk": sourceDevDeps["@modelcontextprotocol/sdk"]} : {}),
186
+ ...(sourceDevDeps["zod"] ? {"zod": sourceDevDeps["zod"]} : {}),
187
+ ...(sourceDevDeps["ts-morph"] ? {"ts-morph": sourceDevDeps["ts-morph"]} : {}),
188
+ ...(sourceDevDeps["typescript"] ? {"typescript": sourceDevDeps["typescript"]} : {}),
189
+ ...(sourceDevDeps["@types/node"] ? {"@types/node": sourceDevDeps["@types/node"]} : {}),
190
+ ...(sourceDevDeps["tsx"] ? {"tsx": sourceDevDeps["tsx"]} : {}),
191
+ ...(sourceDevDeps["vitest"] ? {"vitest": sourceDevDeps["vitest"]} : {}),
192
+ "@pandacss/dev": "^0.53.0"
193
+ });
194
+
195
+ if ((targetPkg as PackageJson).peerDependencies) {
196
+ (targetPkg as PackageJson).peerDependencies = sanitizeDeps((targetPkg as PackageJson).peerDependencies) as Record<string,string> | undefined;
197
+ }
198
+ if ((targetPkg as PackageJson).optionalDependencies) {
199
+ (targetPkg as PackageJson).optionalDependencies = sanitizeDeps((targetPkg as PackageJson).optionalDependencies) as Record<string,string> | undefined;
200
+ }
201
+
202
+ // Ensure basic fields
203
+ if (!targetPkg.name) targetPkg.name = path.basename(process.cwd());
204
+ if (!targetPkg.version) targetPkg.version = "0.1.0";
205
+ if (!targetPkg.type) targetPkg.type = "module";
206
+ if (!targetPkg.workspaces) targetPkg.workspaces = ["apps/*", "framework-mcp"];
207
+
208
+ // Add metadata
209
+ targetPkg.enderun = {
210
+ version: sourcePkg.version,
211
+ initializedAt: new Date().toISOString(),
212
+ };
213
+
214
+ fs.writeFileSync(targetPath, JSON.stringify(targetPkg, null, 2));
215
+ console.warn("✅ package.json updated with Enderun scripts and dependencies.");
216
+ }
217
+
218
+ function updateGitIgnore(targetPath, frameworkDir = ".gemini") {
219
+ const IGNORE_LINES = [
220
+ "# AI-Enderun",
221
+ `${frameworkDir}/logs/*.json`,
222
+ `${frameworkDir}/*.lock`,
223
+ ".env",
224
+ ".DS_Store"
225
+ ];
226
+
227
+ let content = "";
228
+ if (fs.existsSync(targetPath)) {
229
+ content = fs.readFileSync(targetPath, "utf8");
230
+ }
231
+
232
+ const lines = content.split("\n").map((l) => l.trim());
233
+ let added = false;
234
+
235
+ for (const line of IGNORE_LINES) {
236
+ if (!lines.includes(line)) {
237
+ content += (content.endsWith("\n") || content === "" ? "" : "\n") + line + "\n";
238
+ added = true;
239
+ }
240
+ }
241
+
242
+ if (added) {
243
+ fs.writeFileSync(targetPath, content);
244
+ console.warn("✅ .gitignore updated.");
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Finds the Claude config file path (if it exists).
250
+ */
251
+ function findClaudeConfigPath() {
252
+ const home = process.env.HOME || process.env.USERPROFILE;
253
+ if (!home) return null;
254
+
255
+ const possiblePaths = [
256
+ path.join(home, ".config", "claude", "config.json"),
257
+ path.join(home, ".claude", "config.json"),
258
+ path.join(home, "Library", "Application Support", "Claude", "config.json"), // macOS Claude Desktop
259
+ path.join(home, "Library", "Application Support", "Claude Code", "config.json"), // macOS Claude Code
260
+ path.join(home, ".config", "Claude", "config.json"), // some Linux setups
261
+ ];
262
+
263
+ for (const p of possiblePaths) {
264
+ if (fs.existsSync(p)) {
265
+ return p;
266
+ }
267
+ }
268
+ return null;
269
+ }
270
+
271
+ /**
272
+ * Adds or updates an MCP server entry in Claude's config file.
273
+ */
274
+ function addMcpServerToClaude(configPath, serverName, serverConfig) {
275
+ try {
276
+ let config = { mcpServers: {} };
277
+
278
+ if (fs.existsSync(configPath)) {
279
+ const content = fs.readFileSync(configPath, "utf8");
280
+ config = JSON.parse(content);
281
+ }
282
+
283
+ if (!config.mcpServers) {
284
+ config.mcpServers = {};
285
+ }
286
+
287
+ config.mcpServers[serverName] = serverConfig;
288
+
289
+ // Ensure parent directory exists
290
+ const dir = path.dirname(configPath);
291
+ if (!fs.existsSync(dir)) {
292
+ fs.mkdirSync(dir, { recursive: true });
293
+ }
294
+
295
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
296
+ return true;
297
+ } catch {
298
+ return false;
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Create initial PROJECT_MEMORY.md if missing.
304
+ */
305
+ function initializeMemory(memoryPath, targetBase) {
306
+ if (fs.existsSync(memoryPath)) return;
307
+
308
+ const traceId = generateULID();
309
+ const date = new Date().toISOString().split("T")[0];
310
+ const template = `# PROJECT MEMORY — Agent Enderun
311
+
312
+ This file is the Single Source of Truth (SSOT) and the persistent memory of the project.
313
+
314
+ ## CURRENT STATUS
315
+
316
+ | Active Phase | Profile | Last Update | Active Trace ID | Blockers |
317
+ | :----------- | :------ | :---------- | :-------------- | :------- |
318
+ | PHASE_0 | Lightweight | ${date} | ${traceId} | NONE |
319
+
320
+ ## PROJECT DEFINITION
321
+
322
+ | Field | Value |
323
+ | :--- | :--- |
324
+ | Project Name | ${path.basename(process.cwd())} |
325
+ | Platform | Not defined |
326
+ | Frontend | React 19 + Vite + Panda CSS |
327
+ | Backend | Node.js 20+ + Fastify |
328
+ | DB | PostgreSQL |
329
+
330
+ ## DOD STATUS
331
+
332
+ | Phase | Status | Note |
333
+ | :--- | :--- | :--- |
334
+ | PHASE_0 | IN_PROGRESS | Initializing project structure |
335
+ | PHASE_1 | PENDING | |
336
+ | PHASE_2 | PENDING | |
337
+ | PHASE_3 | PENDING | |
338
+ | PHASE_4 | PENDING | |
339
+
340
+ ## CRITICAL DECISIONS
341
+
342
+ | Date | Decision | Rationale | Agent |
343
+ | :--- | :--- | :--- | :--- |
344
+ | ${date} | Project Initialized | Framework setup via CLI | @manager |
345
+
346
+ ## DELIVERABLES
347
+
348
+ | Module | Status | Agent | Date |
349
+ | :--- | :--- | :--- | :--- |
350
+
351
+ ## ACTIVE TASKS
352
+
353
+ | Trace ID | Task | Agent | Priority | Status |
354
+ | :--- | :--- | :--- | :--- | :--- |
355
+ | ${traceId} | Framework setup and architecture alignment | @manager | P1 | IN_PROGRESS |
356
+
357
+ ## HISTORY (Persistent Memory)
358
+
359
+ ### ${date} — Framework Initialization
360
+
361
+ - **Agent:** @manager
362
+ - **Trace ID:** ${traceId}
363
+ - **Action:** Initialized Agent Enderun framework and project structure.
364
+ `;
365
+
366
+ const finalTemplate = template.replace(/\{\{FRAMEWORK_DIR\}\}/g, targetBase);
367
+ fs.writeFileSync(memoryPath, finalTemplate);
368
+ console.warn(`✅ PROJECT_MEMORY.md initialized in ${targetBase}`);
369
+ }
370
+
371
+ // --- COMMANDS ---
372
+
373
+ function getPackageManager() {
374
+ const override = process.env.AGENT_ENDERUN_PACKAGE_MANAGER || process.env.AGENT_ENDERUN_PM || process.env.AI_ENDERUN_PACKAGE_MANAGER || process.env.AI_ENDERUN_PM;
375
+ if (override) return override.toLowerCase();
376
+
377
+ const userAgent = process.env.npm_config_user_agent || "";
378
+ const npmExecPath = process.env.npm_execpath || "";
379
+
380
+ if (userAgent.includes("pnpm") || npmExecPath.includes("pnpm")) return "pnpm";
381
+ if (userAgent.includes("yarn") || npmExecPath.includes("yarn")) return "yarn";
382
+
383
+ // Check for lockfiles in target directory
384
+ if (fs.existsSync(path.join(process.cwd(), "pnpm-lock.yaml")) || fs.existsSync(path.join(process.cwd(), "pnpm-workspace.yaml"))) return "pnpm";
385
+ if (fs.existsSync(path.join(process.cwd(), "yarn.lock"))) return "yarn";
386
+
387
+ return "npm";
388
+ }
389
+
390
+ /**
391
+ * Scaffold the framework into the target project.
392
+ */
393
+ async function initCommand(adapter = "gemini") {
394
+ // Supports gemini, claude, and cursor adapters
395
+ const selectedAdapter = ["gemini", "claude", "cursor"].includes(adapter.toLowerCase()) ? adapter.toLowerCase() : "gemini";
396
+ const targetBase = `.${selectedAdapter}`;
397
+ const targetFrameworkDir = path.join(targetDir, targetBase);
398
+ // ... (CORE_FILES and DIRS_TO_CREATE remain the same)
399
+ const CORE_FILES = [
400
+ ".enderun", // source template folder (remapped to targetBase: .gemini)
401
+
402
+ "mcp.json",
403
+ ".env.example",
404
+ "ENDERUN.md",
405
+ "package.json",
406
+ "tsconfig.json",
407
+ "panda.config.ts",
408
+ "vitest.config.ts",
409
+ "framework-mcp",
410
+ "docs",
411
+ ];
412
+
413
+ const DIRS_TO_CREATE = [
414
+ targetBase,
415
+ `${targetBase}/agents`,
416
+ `${targetBase}/agents/schema`,
417
+ `${targetBase}/skills`,
418
+ `${targetBase}/knowledge`,
419
+ `${targetBase}/benchmarks`,
420
+ `${targetBase}/monitoring`,
421
+ `${targetBase}/logs`,
422
+ `${targetBase}/messages`,
423
+ `${targetBase}/memory-graph`,
424
+ `${targetBase}/memory-graph/agent-contexts`,
425
+ `${targetBase}/queue`,
426
+ `${targetBase}/queue/pending`,
427
+ `${targetBase}/queue/processing`,
428
+ `${targetBase}/queue/completed`,
429
+ `${targetBase}/queue/failed`,
430
+ `${targetBase}/queue/pipelines`,
431
+ "apps/web",
432
+ "apps/backend",
433
+ "docs",
434
+ "framework-mcp",
435
+ "tests",
436
+ ];
437
+
438
+ console.warn(`🚀 Installing Agent Enderun (Adapter: ${selectedAdapter})...`);
439
+
440
+ // Ensure target framework base exists
441
+ if (!fs.existsSync(targetFrameworkDir)) {
442
+ fs.mkdirSync(targetFrameworkDir, { recursive: true });
443
+ }
444
+
445
+ // Create subdirectories
446
+ for (const dir of DIRS_TO_CREATE) {
447
+ const fullPath = path.join(targetDir, dir);
448
+ if (!fs.existsSync(fullPath)) {
449
+ fs.mkdirSync(fullPath, { recursive: true });
450
+ console.warn(`📂 Created directory: ${dir}`);
451
+ }
452
+ }
453
+
454
+ // Process core files and the chosen adapter pointer file
455
+ const filesToProcess = [...CORE_FILES, `${selectedAdapter}.md`].filter(f => fs.existsSync(path.join(sourceDir, f)) || CORE_FILES.includes(f) || f === `${selectedAdapter}.md`);
456
+
457
+ // Ensure apps/backend structure for contract file
458
+ ensureDir(path.join(targetDir, "apps/backend"));
459
+ const initContractPath = path.join(targetDir, "apps/backend/contract.version.json");
460
+ if (!fs.existsSync(initContractPath)) {
461
+ writeJsonFile(initContractPath, {
462
+ "contract_hash": "initial_hash_placeholder",
463
+ "last_updated": new Date().toISOString()
464
+ });
465
+ }
466
+
467
+ // Create MCP config for Gemini adapter
468
+ const mcpConfigPath = path.join(targetFrameworkDir, "mcp_config.json");
469
+ if (!fs.existsSync(mcpConfigPath)) {
470
+ writeJsonFile(mcpConfigPath, {
471
+ "mcpServers": {}
472
+ });
473
+ console.warn(`📄 Created ${targetBase}/mcp_config.json (MCP format)`);
474
+ }
475
+
476
+ // Detect target project scope
477
+ let targetPkg = {};
478
+ try {
479
+ const targetPkgPath = path.join(targetDir, "package.json");
480
+ if (fs.existsSync(targetPkgPath)) {
481
+ targetPkg = JSON.parse(fs.readFileSync(targetPkgPath, "utf8"));
482
+ }
483
+ } catch {
484
+ // ignore parse errors when reading existing package.json
485
+ }
486
+
487
+ const targetScope = (targetPkg && targetPkg.name)
488
+ ? (targetPkg.name.startsWith("@") ? targetPkg.name.split("/")[0] : `@${targetPkg.name}`)
489
+ : `@${path.basename(targetDir)}`;
490
+
491
+
492
+ for (const item of filesToProcess) {
493
+ const src = path.join(sourceDir, item);
494
+ let dest = path.join(targetDir, item);
495
+
496
+ // FORCED CLEANUP: Delete existing framework engine directory to ensure clean update
497
+ if (["framework-mcp"].includes(item)) {
498
+ if (fs.existsSync(dest)) {
499
+ try {
500
+ fs.rmSync(dest, { recursive: true, force: true });
501
+ } catch {
502
+ // ignore
503
+ }
504
+ }
505
+ }
506
+
507
+ // Remap core framework files to targetBase
508
+ if (item === ".enderun" || item.startsWith(".enderun/")) {
509
+ dest = path.join(targetDir, item.replace(".enderun", targetBase));
510
+ }
511
+ if (item === "ENDERUN.md") dest = path.join(targetFrameworkDir, "ENDERUN.md");
512
+
513
+ // Adapter pointer check
514
+ const isAdapterPointer = item === `${selectedAdapter}.md` || item === "gemini.md" || item === "claude.md" || item === "cursor.md";
515
+ if (isAdapterPointer) {
516
+ dest = path.join(targetDir, item);
517
+ }
518
+
519
+ if (fs.existsSync(src)) {
520
+ if (fs.lstatSync(src).isDirectory()) {
521
+ const skipFiles = (item === ".enderun") ? ["logs", "PROJECT_MEMORY.md", "PROJECT_MEMORY.lock"] : [];
522
+ const nonDestructive = ["docs", ".enderun"].includes(item); // source name during copy
523
+ copyDir(src, dest, new Set(skipFiles), nonDestructive, targetBase, targetScope);
524
+ } else {
525
+ if (item === "package.json") continue; // We merge it later
526
+
527
+ if (fs.existsSync(dest) && !isAdapterPointer) {
528
+ console.warn(`ℹ️ Skipping existing file: ${item}`);
529
+ continue;
530
+ }
531
+
532
+ const ext = path.extname(item);
533
+ const textExtensions = [".md", ".json", ".js", ".ts", ".txt", ""];
534
+ if (textExtensions.includes(ext)) {
535
+ let content = fs.readFileSync(src, "utf8");
536
+ // Use selected adapter for placeholder replacement
537
+ const currentAdapter = selectedAdapter;
538
+
539
+ content = content.replace(/\{\{FRAMEWORK_DIR\}\}/g, targetBase);
540
+ content = content.replace(/\{\{ADAPTER\}\}/g, currentAdapter);
541
+ // Fallback: replace any residual hardcoded .enderun/ paths (with or without trailing slash)
542
+ content = content.replace(/\.enderun\//g, `${targetBase}/`);
543
+ content = content.replace(/`\.enderun`/g, `\`${targetBase}\``);
544
+ content = content.replace(/\.enderun(?![\w/-])/g, targetBase); // bare .enderun not followed by word char or / -
545
+
546
+ if (ext === ".json") {
547
+ try {
548
+ const json = JSON.parse(content);
549
+ content = JSON.stringify(sanitizeJson(json, targetScope), null, 2);
550
+ // Ensure variable replacement even inside JSON strings if any
551
+ content = content.replace(/\{\{FRAMEWORK_DIR\}\}/g, targetBase);
552
+ content = content.replace(/\{\{ADAPTER\}\}/g, currentAdapter);
553
+ } catch {
554
+ content = content.replace(/workspace:[^"'\s]*/g, "*");
555
+ }
556
+ }
557
+
558
+ fs.writeFileSync(dest, content);
559
+ } else {
560
+ fs.copyFileSync(src, dest);
561
+ }
562
+ }
563
+ console.warn(`✅ ${item} processed -> ${path.relative(targetDir, dest)}`);
564
+ }
565
+ }
566
+
567
+ // Merge Package JSON
568
+ mergePackageJson(path.join(targetDir, "package.json"), path.join(sourceDir, "package.json"));
569
+ updateGitIgnore(path.join(targetDir, ".gitignore"), targetBase);
570
+
571
+ const finalMemoryPath = path.join(targetDir, targetBase, "PROJECT_MEMORY.md");
572
+ initializeMemory(finalMemoryPath, targetBase);
573
+
574
+ deepCleanProtocols(targetDir, targetScope);
575
+
576
+ // Create initial sample test
577
+ const sampleTestPath = path.join(targetDir, "tests/initial.test.ts");
578
+ if (!fs.existsSync(sampleTestPath)) {
579
+ fs.writeFileSync(sampleTestPath, `import { describe, it, expect } from 'vitest';
580
+
581
+ describe('Initial Setup', () => {
582
+ it('should verify the testing environment is active', () => {
583
+ expect(true).toBe(true);
584
+ });
585
+ });
586
+ `);
587
+ }
588
+
589
+ if (!fs.existsSync(path.join(targetDir, ".git"))) {
590
+ try {
591
+ const { execSync } = await import("child_process");
592
+ execSync("git init", { cwd: targetDir, stdio: "ignore" });
593
+ console.warn("✅ Git repository initialized.");
594
+ } catch { /* ignore */ }
595
+ }
596
+
597
+ console.warn(`\n🛠️ Running ${selectedAdapter} adapter configuration...`);
598
+
599
+ console.warn(`♊ ${selectedAdapter.toUpperCase()} CLI: Setup complete in ${targetBase}/ folder.`);
600
+ console.warn(` • Root entrypoint: ${selectedAdapter}.md`);
601
+ console.warn(` • All framework files and governance live under ${targetBase}/`);
602
+
603
+ const pkgMgr = getPackageManager();
604
+ const installCmd = pkgMgr === "npm" ? "npm install" : `${pkgMgr} install`;
605
+ const buildCmd = pkgMgr === "npm" ? "npm run enderun:build" : `${pkgMgr} run enderun:build`;
606
+
607
+ console.warn(`\n✨ Framework scaffolded! (v${FRAMEWORK_VERSION})`);
608
+
609
+ // Allow skipping install in test/CI environments
610
+ if (process.env.ENDERUN_SKIP_INSTALL === "1") {
611
+ console.warn("\n⏭️ Skipping install steps (ENDERUN_SKIP_INSTALL=1).");
612
+ return;
613
+ }
614
+
615
+ try {
616
+ const { execSync } = await import("child_process");
617
+ console.warn(`\n📦 Step 1/3: Running '${installCmd}'...`);
618
+ execSync(installCmd, { stdio: "inherit" });
619
+ console.warn(`\n🏗️ Step 2/3: Running '${buildCmd}'...`);
620
+ execSync(buildCmd, { stdio: "inherit" });
621
+ console.warn(`\n🔍 Step 3/3: Running 'agent-enderun check'...`);
622
+ checkCommand();
623
+ console.warn("\n🚀 Agent Enderun is fully installed and verified!");
624
+ } catch {
625
+ console.error("\n❌ Automatic installation failed. Run manually:");
626
+ console.warn(`👉 ${installCmd} && ${buildCmd}`);
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Recursively scans a directory for package.json files and removes workspace: protocols.
632
+ */
633
+ function deepCleanProtocols(dir, targetScope = "") {
634
+ if (!fs.existsSync(dir)) return;
635
+
636
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
637
+ for (const entry of entries) {
638
+ const fullPath = path.join(dir, entry.name);
639
+ if (entry.isDirectory()) {
640
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
641
+ deepCleanProtocols(fullPath, targetScope);
642
+ } else if (entry.name === "package.json") {
643
+ try {
644
+ const content = fs.readFileSync(fullPath, "utf8");
645
+ const json = JSON.parse(content);
646
+ const cleaned = JSON.stringify(sanitizeJson(json, targetScope), null, 2);
647
+ fs.writeFileSync(fullPath, cleaned);
648
+ } catch {
649
+ // ignore malformed json
650
+ }
651
+ } else if (entry.name === "package-lock.json") {
652
+ fs.unlinkSync(fullPath);
653
+ }
654
+ }
655
+ }
656
+
657
+ /**
658
+ * Check framework health and MCP status.
659
+ */
660
+ function checkCommand() {
661
+ console.warn(`🔍 Checking Agent Enderun Health (v${FRAMEWORK_VERSION})...`);
662
+ let issues = 0;
663
+
664
+ const frameworkDir = getFrameworkDir();
665
+
666
+ const constitutionPath = fs.existsSync(path.join(process.cwd(), "ENDERUN.md"))
667
+ ? "ENDERUN.md"
668
+ : path.join(frameworkDir, "ENDERUN.md");
669
+
670
+ const checks = [
671
+ { name: "Constitution (ENDERUN.md)", path: constitutionPath },
672
+ { name: "Memory (PROJECT_MEMORY.md)", path: path.join(frameworkDir, "PROJECT_MEMORY.md") },
673
+ { name: "Command Map (cli-commands.json)", path: path.join(frameworkDir, "cli-commands.json") },
674
+ { name: "Framework Config (config.json)", path: path.join(frameworkDir, "config.json") },
675
+ { name: "Agent Status (STATUS.md)", path: path.join(frameworkDir, "STATUS.md") },
676
+ { name: "MCP Config (mcp.json)", path: "mcp.json" },
677
+ { name: "Backend Contract", path: "apps/backend/contract.version.json" },
678
+ { name: "MCP Server", path: "framework-mcp/package.json" },
679
+ { name: "Panda CSS Config", path: "panda.config.ts" },
680
+ { name: "Brain Dashboard", path: path.join(frameworkDir, "BRAIN_DASHBOARD.md") },
681
+ { name: "Project Architectural Portal (docs/README.md)", path: "docs/README.md" },
682
+ { name: "System Getting Started Guide (docs/getting-started.md)", path: "docs/getting-started.md" },
683
+ { name: "Enterprise Approval Flows Protocol", path: "docs/architecture/approval-flows.md" },
684
+ { name: "Professional Error Handling Pattern", path: "docs/backend/error-handling.md" },
685
+ { name: "Atomic Component Standards", path: "docs/frontend/component-patterns.md" },
686
+ ];
687
+
688
+ for (const check of checks) {
689
+ if (fs.existsSync(path.join(process.cwd(), check.path))) {
690
+ console.warn(`✅ ${check.name} found.`);
691
+ } else {
692
+ console.warn(`❌ ${check.name} MISSING! (${check.path})`);
693
+ issues++;
694
+ }
695
+ }
696
+
697
+ // Dependency Check
698
+ const mcpNodeModules = path.join(process.cwd(), "framework-mcp/node_modules");
699
+ const rootNodeModules = path.join(process.cwd(), "node_modules");
700
+ if (!fs.existsSync(mcpNodeModules) && !fs.existsSync(rootNodeModules)) {
701
+ console.warn("❌ Dependencies MISSING! (Run 'npm install')");
702
+ issues++;
703
+ } else {
704
+ console.warn("✅ Dependencies found.");
705
+ }
706
+
707
+ // MCP Build Check
708
+ const mcpPath = path.join(process.cwd(), "framework-mcp/dist/index.js");
709
+ if (!fs.existsSync(mcpPath)) {
710
+ console.warn("❌ MCP Build MISSING! (Run 'npm run enderun:build')");
711
+ issues++;
712
+ } else {
713
+ console.warn("✅ MCP Build found.");
714
+ console.warn("⏳ Testing MCP Server syntax...");
715
+ try {
716
+ execSync(`node --check ${mcpPath}`, { stdio: "pipe" });
717
+ console.warn("✅ MCP Server syntax valid.");
718
+ } catch {
719
+ // If --check fails on ESM, we might skip it or use a better check
720
+ console.warn("⚠️ MCP Syntax check skipped (ESM/Environment).");
721
+ }
722
+ }
723
+
724
+ if (issues === 0) {
725
+ console.warn("\n🚀 All systems green! Agent Enderun is ready for orchestration.");
726
+ } else {
727
+ console.warn(`\n⚠️ Found ${issues} issues. Please fix them before starting.`);
728
+ process.exit(1);
729
+ }
730
+ }
731
+
732
+ function sanitizeJson(obj, targetScope = "") {
733
+ if (typeof obj !== "object" || obj === null) return obj;
734
+ if (Array.isArray(obj)) return obj.map(item => sanitizeJson(item, targetScope));
735
+ const cleaned = {};
736
+ for (const [key, value] of Object.entries(obj)) {
737
+ let finalKey = key;
738
+ let finalValue = value;
739
+
740
+ // Remove UnoCSS related keys or values
741
+ if (typeof key === "string" && key.includes("unocss")) continue;
742
+ if (typeof value === "string" && value.includes("unocss")) {
743
+ continue; // Skip this script/field
744
+ }
745
+
746
+ // Replace scope if needed
747
+ if (targetScope) {
748
+ const scopeName = targetScope.startsWith("@") ? targetScope.slice(1) : targetScope;
749
+
750
+ // Handle scoped: @ai-enderun/foo -> @target/foo
751
+ if (typeof key === "string" && key.startsWith("@ai-enderun/")) {
752
+ finalKey = key.replace("@ai-enderun/", `${targetScope}/`);
753
+ }
754
+ if (typeof value === "string" && value.startsWith("@ai-enderun/")) {
755
+ finalValue = value.replace("@ai-enderun/", `${targetScope}/`);
756
+ }
757
+
758
+ // Handle unscoped: ai-enderun-foo -> target-foo
759
+ if (typeof key === "string" && key.startsWith("ai-enderun-")) {
760
+ finalKey = key.replace("ai-enderun-", `${scopeName}-`);
761
+ }
762
+ if (typeof value === "string" && value.startsWith("ai-enderun-")) {
763
+ finalValue = value.replace("ai-enderun-", `${scopeName}-`);
764
+ }
765
+
766
+ // Handle agent-enderun -> target (ONLY for the package name)
767
+ if (key === "name" && value === "agent-enderun") {
768
+ finalValue = scopeName;
769
+ }
770
+
771
+ // Preserve agent-enderun in dependencies and bin
772
+ // (No action needed as finalKey/finalValue default to original)
773
+
774
+ if (typeof value === "string" && value.startsWith("workspace:")) {
775
+ finalValue = "*";
776
+ }
777
+ } else if (typeof value === "string" && value.startsWith("workspace:")) {
778
+ finalValue = "*";
779
+ }
780
+
781
+ cleaned[finalKey] = (typeof finalValue === "object") ? sanitizeJson(finalValue, targetScope) : finalValue;
782
+ }
783
+ return cleaned;
784
+ }
785
+
786
+ function copyDir(src, dest, skipSet = new Set(), nonDestructive = false, frameworkDir = ".gemini", targetScope = "") {
787
+ const DEFAULT_SKIP = new Set(["node_modules", ".git", ".DS_Store", "package-lock.json"]);
788
+ const actualSkip = new Set([...DEFAULT_SKIP, ...skipSet]);
789
+
790
+ if (!fs.existsSync(dest)) {
791
+ fs.mkdirSync(dest, { recursive: true });
792
+ }
793
+
794
+ fs.readdirSync(src, { withFileTypes: true }).forEach(entry => {
795
+ if (actualSkip.has(entry.name)) return;
796
+
797
+ const srcPath = path.join(src, entry.name);
798
+ const destPath = path.join(dest, entry.name);
799
+
800
+ if (entry.isDirectory()) {
801
+ copyDir(srcPath, destPath, skipSet, nonDestructive, frameworkDir, targetScope);
802
+ } else {
803
+ if (nonDestructive && fs.existsSync(destPath)) {
804
+ return;
805
+ }
806
+
807
+ const ext = path.extname(entry.name);
808
+ const textExtensions = [".md", ".json", ".js", ".ts", ".txt", ""];
809
+
810
+ if (textExtensions.includes(ext)) {
811
+ let content = fs.readFileSync(srcPath, "utf8");
812
+ content = content.replace(/\{\{FRAMEWORK_DIR\}\}/g, frameworkDir);
813
+ // Also replace any residual hardcoded .enderun/ paths left in source files (with or without trailing slash)
814
+ content = content.replace(/\.enderun\//g, `${frameworkDir}/`);
815
+ content = content.replace(/`\.enderun`/g, `\`${frameworkDir}\``);
816
+ content = content.replace(/\.enderun(?![\w/-])/g, frameworkDir);
817
+
818
+ // Sanitize workspace: protocol
819
+ if (ext === ".json") {
820
+ try {
821
+ const json = JSON.parse(content);
822
+ content = JSON.stringify(sanitizeJson(json, targetScope), null, 2);
823
+ } catch {
824
+ content = content.replace(/workspace:[^"'\s]*/g, "*");
825
+ }
826
+ } else {
827
+ content = content.replace(/workspace:[^"'\s]*/g, "*");
828
+ }
829
+
830
+ const frameworkBase = frameworkDir.startsWith(".") ? frameworkDir.slice(1) : frameworkDir;
831
+ let currentAdapter = frameworkBase;
832
+ if (entry.name.endsWith(".md") && ["gemini", "claude", "cursor", "codex"].some(a => entry.name.startsWith(a))) {
833
+ currentAdapter = ["gemini", "claude", "cursor", "codex"].find(a => entry.name.startsWith(a));
834
+ }
835
+
836
+ content = content.replace(/\{\{FRAMEWORK_DIR\}\}/g, frameworkDir);
837
+ content = content.replace(/\{\{ADAPTER\}\}/g, currentAdapter);
838
+ // Fallback: replace any residual hardcoded .enderun/ paths (with or without trailing slash)
839
+ content = content.replace(/\.enderun\//g, `${frameworkDir}/`);
840
+ content = content.replace(/`\.enderun`/g, `\`${frameworkDir}\``);
841
+ content = content.replace(/\.enderun(?![\w/-])/g, frameworkDir);
842
+
843
+ fs.writeFileSync(destPath, content);
844
+ } else {
845
+ fs.copyFileSync(srcPath, destPath);
846
+ }
847
+ }
848
+ });
849
+ }
850
+
851
+ /**
852
+ * Print the current framework status.
853
+ */
854
+ function statusCommand() {
855
+ const memoryPath = getMemoryPath();
856
+ const frameworkDir = getFrameworkDir();
857
+ if (!fs.existsSync(memoryPath)) {
858
+ console.error(`❌ Error: ${frameworkDir}/PROJECT_MEMORY.md not found. Please run 'init' first.`);
859
+ return;
860
+ }
861
+
862
+ const content = fs.readFileSync(memoryPath, "utf8");
863
+ const statusMatch = content.match(/\| Active Phase \| Profile \| Last Update \| Active Trace ID \| Blockers \|\n\| :----------- \| :------ \| :---------- \| :-------------- \| :------- \|\n\| (.*?) \| (.*?) \| (.*?) \| (.*?) \| (.*?) \|/);
864
+
865
+ console.warn("\n📊 --- PROJECT STATUS ---");
866
+ if (statusMatch) {
867
+ console.warn(`🔹 Phase: ${statusMatch[1].trim()}`);
868
+ console.warn(`🧭 Profile: ${statusMatch[2].trim()}`);
869
+ console.warn(`📅 Update: ${statusMatch[3].trim()}`);
870
+ console.warn(`🆔 Trace ID: ${statusMatch[4].trim()}`);
871
+ console.warn(`⛔ Blockers: ${statusMatch[5].trim()}`);
872
+ }
873
+
874
+ const tasksSection = content.match(/## ACTIVE TASKS\n\n([\s\S]*?)\n\n##/);
875
+ if (tasksSection) {
876
+ console.warn("\n📋 Active Tasks:");
877
+ console.warn(tasksSection[1].trim());
878
+ }
879
+
880
+ console.warn("\n-----------------------\n");
881
+ }
882
+
883
+ /**
884
+ * Generate a new Trace ID and add it to project memory.
885
+ */
886
+ function traceNewCommand(description, agent = "manager", priority = "P2") {
887
+ const memoryPath = getMemoryPath();
888
+ if (!fs.existsSync(memoryPath)) {
889
+ console.error("❌ Error: PROJECT_MEMORY.md not found.");
890
+ return;
891
+ }
892
+
893
+ const traceId = generateULID();
894
+ const safeDescription = sanitizeTableCell(description);
895
+ const safeAgent = normalizeAgentName(agent);
896
+ const safePriority = normalizePriority(priority);
897
+ const newTask = `| ${traceId} | ${safeDescription} | @${safeAgent} | ${safePriority} | IN_PROGRESS |`;
898
+ const lockPath = `${memoryPath}.lock`;
899
+
900
+ if (!acquireMemoryLock(lockPath)) {
901
+ console.error("❌ Error: Memory lock timeout (5 retries).");
902
+ return;
903
+ }
904
+
905
+ try {
906
+ const content = fs.readFileSync(memoryPath, "utf8");
907
+ const updated = insertTaskRow(content, newTask);
908
+ if (!updated) {
909
+ console.error("❌ Error: ACTIVE TASKS table not found, task could not be added.");
910
+ return;
911
+ }
912
+
913
+ fs.writeFileSync(memoryPath, updated);
914
+ console.warn(`\n✅ New Trace ID created: ${traceId}`);
915
+ console.warn(`📝 Added to task list: ${description}\n`);
916
+ return traceId;
917
+ } finally {
918
+ releaseMemoryLock(lockPath);
919
+ }
920
+ }
921
+
922
+ /**
923
+ * Verify the shared-types contract hash.
924
+ */
925
+ function verifyContractCommand() {
926
+ const sharedDir = path.join(targetDir, "apps/backend/src/types");
927
+ const contractPath = path.join(targetDir, "apps/backend/contract.version.json");
928
+
929
+ if (!fs.existsSync(sharedDir) || !fs.existsSync(contractPath)) {
930
+ console.error("❌ Error: Backend types or contract.version.json not found.");
931
+ return;
932
+ }
933
+
934
+ const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).flatMap((e) => {
935
+ const p = path.join(d, e.name);
936
+ return e.isDirectory() ? walk(p) : (p.endsWith(".ts") ? [p] : []);
937
+ });
938
+
939
+ const files = walk(sharedDir).sort();
940
+ const h = crypto.createHash("sha256");
941
+ for (const f of files) {
942
+ h.update(path.relative(targetDir, f));
943
+ h.update("\0");
944
+ h.update(fs.readFileSync(f));
945
+ h.update("\0");
946
+ }
947
+ const currentHash = h.digest("hex");
948
+
949
+ try {
950
+ const stored = JSON.parse(fs.readFileSync(contractPath, "utf8")).contract_hash;
951
+ if (currentHash === stored) {
952
+ console.warn("✅ Contract hash verified! (MATCH)");
953
+ } else {
954
+ console.error(`❌ HASH MISMATCH!\nExpected: ${stored}\nActual: ${currentHash}`);
955
+ process.exit(1);
956
+ }
957
+ } catch {
958
+ console.error("❌ Error reading contract.version.json");
959
+ process.exit(1);
960
+ }
961
+ }
962
+
963
+ function securityAuditCommand(targetPath) {
964
+ console.warn(`🔍 Running Advanced Security Audit on: ${targetPath}...`);
965
+ const scanRules = [
966
+ { pattern: /sql`/, message: "Potential Raw SQL usage detected", severity: "HIGH" },
967
+ { pattern: /(password|secret|api_?key)\s*[:=]\s*['"][^'"]+['"]/i, message: "Potential hardcoded secret detected", severity: "CRITICAL" },
968
+ { pattern: /:\s*any(?!\w)/, message: "Usage of 'any' type detected", severity: "MEDIUM" },
969
+ { pattern: /\.innerHTML\s*=/, message: "Unsafe innerHTML assignment detected", severity: "MEDIUM" },
970
+ ];
971
+ const issues = [];
972
+ const files = collectFiles(path.join(targetDir, targetPath), [".ts", ".tsx", ".js", ".jsx"]);
973
+ files.forEach(f => {
974
+ const content = fs.readFileSync(f, "utf8");
975
+ const lines = content.split("\n");
976
+ lines.forEach((line, i) => {
977
+ scanRules.forEach(rule => {
978
+ if (rule.pattern.test(line)) {
979
+ issues.push(`[${rule.severity}] ${rule.message} in ${path.relative(targetDir, f)}:${i+1}`);
980
+ }
981
+ });
982
+ });
983
+ });
984
+ if (issues.length === 0) {
985
+ console.warn("✅ No security issues detected.");
986
+ } else {
987
+ issues.forEach(issue => console.warn(`⚠️ ${issue}`));
988
+ }
989
+ }
990
+
991
+ function complianceCheckCommand(targetPath) {
992
+ console.warn(`📜 Checking Constitution Compliance: ${targetPath}...`);
993
+ const violations = [];
994
+ const forbidden = ["@shadcn", "mui", "@chakra-ui", "tailwindcss"];
995
+ const files = collectFiles(path.join(targetDir, targetPath), [".ts", ".tsx", ".js", ".jsx", ".md"]);
996
+ files.forEach(f => {
997
+ const content = fs.readFileSync(f, "utf8");
998
+ forbidden.forEach(lib => {
999
+ if (content.includes(lib)) violations.push(`${path.relative(targetDir, f)}: Forbidden library '${lib}' found.`);
1000
+ });
1001
+ if (f.endsWith(".ts") && content.includes("interface") && content.match(/ID\s*:\s*string/)) {
1002
+ violations.push(`${path.relative(targetDir, f)}: Potential Branded Types violation (ID typed as string).`);
1003
+ }
1004
+ });
1005
+ if (violations.length === 0) {
1006
+ console.warn("✅ All systems compliant with ENDERUN.md.");
1007
+ } else {
1008
+ violations.forEach(v => console.warn(`❌ ${v}`));
1009
+ }
1010
+ }
1011
+
1012
+ function gitCommitCommand(traceId) {
1013
+ try {
1014
+ const diff = cp.execSync("git diff --staged", { encoding: "utf8" });
1015
+ if (!diff) {
1016
+ console.warn("ℹ️ No staged changes found. Use 'git add' first.");
1017
+ return;
1018
+ }
1019
+ const files = cp.execSync("git diff --staged --name-only", { encoding: "utf8" }).split("\n").filter(Boolean);
1020
+ let type = "feat"; const scope = "code";
1021
+ if (files.some(f => f.includes(".md"))) type = "docs";
1022
+ else if (files.some(f => f.includes("apps/backend/src/types"))) type = "arch";
1023
+ else if (files.some(f => f.includes("bin/cli.js"))) type = "fix";
1024
+
1025
+ const summary = files.length === 1 ? `update ${path.basename(files[0])}` : `update ${files.length} files`;
1026
+ console.warn(`\n### SUGGESTED COMMIT MESSAGE ###\n\n[${traceId}] ${type}(${scope}): ${summary}\n`);
1027
+ } catch {
1028
+ console.warn("❌ Git command failed.");
1029
+ }
1030
+ }
1031
+
1032
+ function explorerGraphCommand(targetPath) {
1033
+ console.warn(`🗺️ Generating Dependency Graph for: ${targetPath}...`);
1034
+ const files = collectFiles(path.join(targetDir, targetPath), [".ts", ".tsx"]);
1035
+ const edges = [];
1036
+ files.forEach(f => {
1037
+ const content = fs.readFileSync(f, "utf8");
1038
+ const name = path.basename(f, path.extname(f));
1039
+ const imports = content.match(/from\s+['"]\.\.?\/[^'"]+['"]/g) || [];
1040
+ imports.forEach(imp => {
1041
+ const target = path.basename(imp.split(/['"]/)[1]);
1042
+ edges.push(`${name} --> ${target}`);
1043
+ });
1044
+ });
1045
+ if (edges.length === 0) {
1046
+ console.warn("ℹ️ No internal dependencies found.");
1047
+ } else {
1048
+ console.warn("\n```mermaid\ngraph TD\n" + Array.from(new Set(edges)).join("\n") + "\n```\n");
1049
+ }
1050
+ }
1051
+
1052
+ function explorerAuditCommand(targetPath) {
1053
+ console.warn(`🧠 Codebase Intelligence Scan: ${targetPath}...`);
1054
+ const files = collectFiles(path.join(targetDir, targetPath), [".ts", ".tsx"]);
1055
+ const complexity = [];
1056
+ files.forEach(f => {
1057
+ const content = fs.readFileSync(f, "utf8");
1058
+ const lines = content.split("\n").length;
1059
+ if (lines > 300) complexity.push(`${path.relative(targetDir, f)} (${lines} lines)`);
1060
+ });
1061
+ if (complexity.length > 0) {
1062
+ console.warn("\n⚠️ Complexity Spikes:");
1063
+ complexity.forEach(c => console.warn(`- ${c}`));
1064
+ } else {
1065
+ console.warn("✅ Codebase structure looks clean.");
1066
+ }
1067
+ }
1068
+
1069
+ function collectFiles(dir, extensions) {
1070
+ let results = [];
1071
+ if (!fs.existsSync(dir)) return results;
1072
+ const list = fs.readdirSync(dir);
1073
+ list.forEach(file => {
1074
+ file = path.join(dir, file);
1075
+ const stat = fs.statSync(file);
1076
+ if (stat && stat.isDirectory()) {
1077
+ if (!file.includes("node_modules") && !file.includes(".git")) {
1078
+ results = results.concat(collectFiles(file, extensions));
1079
+ }
1080
+ } else {
1081
+ if (extensions.includes(path.extname(file))) {
1082
+ results.push(file);
1083
+ }
1084
+ }
1085
+ });
1086
+ return results;
1087
+ }
1088
+
1089
+ function runScriptCommand(script, projectPath) {
1090
+ const fullPath = path.join(targetDir, projectPath);
1091
+ if (!fs.existsSync(fullPath)) {
1092
+ console.warn(`❌ Project path not found: ${projectPath}`);
1093
+ return;
1094
+ }
1095
+ console.warn(`🚀 Running 'npm run ${script}' in ${projectPath}...`);
1096
+ try {
1097
+ cp.spawnSync("npm", ["run", script], { cwd: fullPath, stdio: "inherit", shell: true });
1098
+ } catch {
1099
+ console.warn(`❌ Failed to run script: ${e.message}`);
1100
+ }
1101
+ }
1102
+
1103
+ function gitSyncCommand() {
1104
+ console.warn("🔄 Syncing with remote repository...");
1105
+ try {
1106
+ cp.execSync("git pull origin main --rebase", { stdio: "inherit" });
1107
+ console.warn("✅ Successfully synced and rebased.");
1108
+ } catch {
1109
+ console.warn("❌ Sync failed. Please resolve conflicts manually.");
1110
+ }
1111
+ }
1112
+
1113
+ function logAgentActionCommand(data) {
1114
+ const frameworkDir = getFrameworkDir();
1115
+ const logsDir = path.join(targetDir, frameworkDir, "logs");
1116
+ if (!fs.existsSync(logsDir)) {
1117
+ fs.mkdirSync(logsDir, { recursive: true });
1118
+ }
1119
+
1120
+ const agent = normalizeAgentName(data.agent);
1121
+ const logPath = path.join(logsDir, `${agent}.json`);
1122
+ let logs = [];
1123
+
1124
+ if (fs.existsSync(logPath)) {
1125
+ try {
1126
+ logs = JSON.parse(fs.readFileSync(logPath, "utf8"));
1127
+ if (!Array.isArray(logs)) logs = [];
1128
+ } catch {
1129
+ logs = [];
1130
+ }
1131
+ }
1132
+
1133
+ const newEntry = {
1134
+ timestamp: new Date().toISOString(),
1135
+ ...data,
1136
+ };
1137
+
1138
+ logs.push(newEntry);
1139
+ fs.writeFileSync(logPath, JSON.stringify(logs, null, 2));
1140
+ console.warn(`✅ Logged action to ${frameworkDir}/logs/${agent}.json`);
1141
+ }
1142
+
1143
+ function updateProjectMemoryCommand(section, content) {
1144
+ const memoryPath = getMemoryPath();
1145
+ if (!fs.existsSync(memoryPath)) {
1146
+ console.error("❌ Error: PROJECT_MEMORY.md not found.");
1147
+ return;
1148
+ }
1149
+
1150
+ const lockPath = `${memoryPath}.lock`;
1151
+ if (!acquireMemoryLock(lockPath)) {
1152
+ console.error("❌ Error: Memory lock timeout.");
1153
+ return;
1154
+ }
1155
+
1156
+ try {
1157
+ let memoryContent = fs.readFileSync(memoryPath, "utf8");
1158
+
1159
+ if (section === "HISTORY") {
1160
+ const headers = ["## HISTORY (Persistent Memory)", "## HISTORY"];
1161
+ let sectionIndex = -1;
1162
+ for (const h of headers) {
1163
+ sectionIndex = memoryContent.indexOf(h);
1164
+ if (sectionIndex !== -1) {
1165
+ break;
1166
+ }
1167
+ }
1168
+
1169
+ if (sectionIndex === -1) {
1170
+ console.error("❌ Error: HISTORY section not found.");
1171
+ return;
1172
+ }
1173
+ const headerEnd = memoryContent.indexOf("\n", sectionIndex) + 1;
1174
+ memoryContent = memoryContent.slice(0, headerEnd) + "\n" + content.trim() + "\n" + memoryContent.slice(headerEnd);
1175
+ } else {
1176
+ const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1177
+ const sectionRegex = new RegExp(`## ${escaped}[\\s\\S]*?(?=\\n## |$)`, "m");
1178
+ if (!sectionRegex.test(memoryContent)) {
1179
+ console.error(`❌ Error: Section not found: ${section}`);
1180
+ return;
1181
+ }
1182
+ memoryContent = memoryContent.replace(sectionRegex, `## ${section}\n\n${content.trim()}\n`);
1183
+ }
1184
+
1185
+ fs.writeFileSync(memoryPath, memoryContent);
1186
+ console.warn(`✅ Section ${section} updated in PROJECT_MEMORY.md`);
1187
+ } finally {
1188
+ releaseMemoryLock(lockPath);
1189
+ }
1190
+ }
1191
+
1192
+ function ensureDir(dirPath) {
1193
+ if (!fs.existsSync(dirPath)) {
1194
+ fs.mkdirSync(dirPath, { recursive: true });
1195
+ }
1196
+ }
1197
+
1198
+ function writeTextFile(filePath, content) {
1199
+ ensureDir(path.dirname(filePath));
1200
+ fs.writeFileSync(filePath, content.endsWith("\n") ? content : `${content}\n`);
1201
+ }
1202
+
1203
+ function writeJsonFile(filePath, value) {
1204
+ writeTextFile(filePath, JSON.stringify(value, null, 2));
1205
+ }
1206
+
1207
+ function slugifyName(value) {
1208
+ const slug = String(value || "enderun-app")
1209
+ .toLowerCase()
1210
+ .replace(/[^a-z0-9]+/g, "-")
1211
+ .replace(/^-+|-+$/g, "");
1212
+ return slug || "enderun-app";
1213
+ }
1214
+
1215
+ function titleCase(value) {
1216
+ return String(value || "Enderun App")
1217
+ .replace(/[-_]+/g, " ")
1218
+ .replace(/\s+/g, " ")
1219
+ .trim()
1220
+ .replace(/\b\w/g, (char) => char.toUpperCase());
1221
+ }
1222
+
1223
+ function inferAppSpec(description) {
1224
+ const normalized = String(description || "").toLowerCase();
1225
+ const isCrm = /\bcrm\b|customer|musteri|müşteri/.test(normalized);
1226
+ const hasAuth = /auth|login|giris|giriş|signin|sign in|user|kullanici|kullanıcı|role|rol/.test(normalized);
1227
+ const hasRoles = /role|rol|permission|yetki|admin/.test(normalized);
1228
+ const hasReports = /report|rapor|analytics|dashboard|chart|metric/.test(normalized);
1229
+ const appName = isCrm ? "crm-dashboard" : slugifyName(description).split("-").slice(0, 4).join("-");
1230
+
1231
+ return {
1232
+ rawDescription: description,
1233
+ appName,
1234
+ title: isCrm ? "CRM Dashboard" : titleCase(appName),
1235
+ domain: isCrm ? "CRM" : "Business",
1236
+ modules: {
1237
+ auth: hasAuth || isCrm,
1238
+ users: hasAuth || hasRoles || isCrm,
1239
+ roles: hasRoles || isCrm,
1240
+ reports: hasReports || isCrm,
1241
+ },
1242
+ };
1243
+ }
1244
+
1245
+ function buildSharedTypesContent(existingContent) {
1246
+ const marker = "// --- Generated Application Contract ---";
1247
+ const generated = [
1248
+ marker,
1249
+ 'export type RoleID = Brand<string, "RoleID">;',
1250
+ 'export type ReportID = Brand<string, "ReportID">;',
1251
+ 'export type CustomerID = Brand<string, "CustomerID">;',
1252
+ "",
1253
+ "export interface AuthSession {",
1254
+ " user: User;",
1255
+ " token: string;",
1256
+ " expiresAt: string;",
1257
+ "}",
1258
+ "",
1259
+ "export interface Role {",
1260
+ " id: RoleID;",
1261
+ " name: string;",
1262
+ " permissions: string[];",
1263
+ "}",
1264
+ "",
1265
+ "export interface Customer {",
1266
+ " id: CustomerID;",
1267
+ " name: string;",
1268
+ " ownerId: UserID;",
1269
+ " status: \"LEAD\" | \"ACTIVE\" | \"AT_RISK\";",
1270
+ " annualValue: number;",
1271
+ " createdAt: string;",
1272
+ "}",
1273
+ "",
1274
+ "export interface ReportMetric {",
1275
+ " id: ReportID;",
1276
+ " label: string;",
1277
+ " value: number;",
1278
+ " trend: \"UP\" | \"DOWN\" | \"FLAT\";",
1279
+ "}",
1280
+ "",
1281
+ "export interface DashboardSummary {",
1282
+ " customers: Customer[];",
1283
+ " users: User[];",
1284
+ " roles: Role[];",
1285
+ " reports: ReportMetric[];",
1286
+ "}",
1287
+ ].join("\n");
1288
+
1289
+ if (existingContent.includes(marker)) return existingContent;
1290
+ return `${existingContent.trim()}\n\n${generated}\n`;
1291
+ }
1292
+
1293
+ function updateContractHashFile() {
1294
+ const sharedDir = path.join(targetDir, "apps/backend/src/types");
1295
+ const contractPath = path.join(targetDir, "apps/backend/contract.version.json");
1296
+ if (!fs.existsSync(sharedDir) || !fs.existsSync(contractPath)) return;
1297
+
1298
+ const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
1299
+ const fullPath = path.join(dir, entry.name);
1300
+ return entry.isDirectory() ? walk(fullPath) : (entry.name.endsWith(".ts") ? [fullPath] : []);
1301
+ });
1302
+
1303
+ const hash = crypto.createHash("sha256");
1304
+ for (const filePath of walk(sharedDir).sort()) {
1305
+ hash.update(path.relative(targetDir, filePath));
1306
+ hash.update("\0");
1307
+ hash.update(fs.readFileSync(filePath));
1308
+ hash.update("\0");
1309
+ }
1310
+
1311
+ const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
1312
+ contract.contract_hash = hash.digest("hex");
1313
+ contract.last_updated = new Date().toISOString();
1314
+ fs.writeFileSync(contractPath, JSON.stringify(contract, null, 2));
1315
+ }
1316
+
1317
+ function createBaseTypeFiles(baseDir) {
1318
+ const typesDir = path.join(baseDir, "types");
1319
+ ensureDir(typesDir);
1320
+
1321
+ writeTextFile(path.join(typesDir, "api.ts"), `import { TraceID } from "./brands.js";
1322
+
1323
+ /**
1324
+ * API Response Wrappers
1325
+ */
1326
+ export interface ApiResponse<T> {
1327
+ data: T;
1328
+ meta?: {
1329
+ requestId: TraceID;
1330
+ timestamp: string;
1331
+ };
1332
+ }
1333
+
1334
+ export interface ApiError {
1335
+ error: {
1336
+ code: string;
1337
+ message: string;
1338
+ details?: unknown;
1339
+ };
1340
+ }
1341
+ `);
1342
+
1343
+ writeTextFile(path.join(typesDir, "brands.ts"), `/**
1344
+ * Branded Type Utility
1345
+ */
1346
+ export type Brand<K, T> = K & { __brand: T };
1347
+
1348
+ /**
1349
+ * Entity IDs (Branded)
1350
+ */
1351
+ export type TraceID = Brand<string, "TraceID">;
1352
+ export type AgentID = Brand<string, "AgentID">;
1353
+ export type ProjectID = Brand<string, "ProjectID">;
1354
+ export type UserID = Brand<string, "UserID">;
1355
+ `);
1356
+
1357
+ writeTextFile(path.join(typesDir, "constants.ts"), `import { TraceID } from "./brands.js";
1358
+
1359
+ /**
1360
+ * Project Phases
1361
+ */
1362
+ export const PROJECT_PHASES = ["PHASE_0", "PHASE_1", "PHASE_2", "PHASE_3", "PHASE_4"] as const;
1363
+ export type ProjectPhase = (typeof PROJECT_PHASES)[number];
1364
+
1365
+ /**
1366
+ * Execution Profiles
1367
+ */
1368
+ export const EXECUTION_PROFILES = ["Lightweight", "Full"] as const;
1369
+ export type ExecutionProfile = (typeof EXECUTION_PROFILES)[number];
1370
+
1371
+ /**
1372
+ * Task Priorities
1373
+ */
1374
+ export const TASK_PRIORITIES = ["P0", "P1", "P2", "P3"] as const;
1375
+ export type TaskPriority = (typeof TASK_PRIORITIES)[number];
1376
+
1377
+ /**
1378
+ * Task Statuses
1379
+ */
1380
+ export const TASK_STATUSES = ["PENDING", "IN_PROGRESS", "BLOCKED", "COMPLETED", "FAILED"] as const;
1381
+ export type TaskStatus = (typeof TASK_STATUSES)[number];
1382
+
1383
+ /**
1384
+ * Action Types & Status
1385
+ */
1386
+ export const ACTION_TYPES = ["CREATE", "MODIFY", "DELETE", "RESEARCH", "ORCHESTRATE"] as const;
1387
+ export type ActionType = (typeof ACTION_TYPES)[number];
1388
+
1389
+ export const ACTION_STATUSES = ["SUCCESS", "FAILURE", "WAITING"] as const;
1390
+ export type ActionStatus = (typeof ACTION_STATUSES)[number];
1391
+ `);
1392
+
1393
+ writeTextFile(path.join(typesDir, "index.ts"), `/**
1394
+ * Agent Enderun — App-Local Types (Modular)
1395
+ */
1396
+
1397
+ export * from "./brands.js";
1398
+ export * from "./constants.js";
1399
+ export * from "./models.js";
1400
+ export * from "./api.js";
1401
+ export * from "./logs.js";
1402
+ `);
1403
+
1404
+ writeTextFile(path.join(typesDir, "logs.ts"), `import { TraceID, AgentID } from "./brands.js";
1405
+ import { ActionType, ActionStatus } from "./constants.js";
1406
+
1407
+ /**
1408
+ * Audit & Agent Logging Types
1409
+ */
1410
+ export interface AgentActionLog {
1411
+ timestamp: string;
1412
+ agent: AgentID;
1413
+ action: ActionType;
1414
+ requestId: TraceID | "—";
1415
+ status: ActionStatus;
1416
+ summary: string;
1417
+ files?: string[];
1418
+ details?: Record<string, unknown>;
1419
+ }
1420
+ `);
1421
+
1422
+ writeTextFile(path.join(typesDir, "models.ts"), `import { UserID, TraceID, AgentID } from "./brands.js";
1423
+ import { ProjectPhase, ExecutionProfile, TaskPriority, TaskStatus } from "./constants.js";
1424
+
1425
+ /**
1426
+ * Base Entity Fields
1427
+ */
1428
+ export interface BaseEntity {
1429
+ id: string; // Usually ULID
1430
+ createdAt: string;
1431
+ updatedAt: string;
1432
+ deletedAt?: string | null;
1433
+ }
1434
+
1435
+ /**
1436
+ * Audit Log Model
1437
+ */
1438
+ export interface AuditLog extends BaseEntity {
1439
+ entityName: string;
1440
+ entityId: string;
1441
+ action: "CREATE" | "UPDATE" | "DELETE" | "RESTORE";
1442
+ userId: UserID;
1443
+ previousState?: Record<string, unknown> | null;
1444
+ newState?: Record<string, unknown> | null;
1445
+ traceId: TraceID;
1446
+ }
1447
+
1448
+ /**
1449
+ * User Model
1450
+ */
1451
+ export interface User extends BaseEntity {
1452
+ id: UserID;
1453
+ email: string;
1454
+ fullName: string;
1455
+ role: "ADMIN" | "DEVELOPER" | "VIEWER";
1456
+ }
1457
+
1458
+ export interface UserProfile extends User {
1459
+ avatarUrl?: string;
1460
+ bio?: string;
1461
+ preferences: Record<string, unknown>;
1462
+ }
1463
+
1464
+ /**
1465
+ * Project Status
1466
+ */
1467
+ export interface ProjectStatus {
1468
+ phase: ProjectPhase;
1469
+ profile: ExecutionProfile;
1470
+ lastUpdate: string;
1471
+ activeTraceId: TraceID | null;
1472
+ blockers: string[];
1473
+ }
1474
+
1475
+ /**
1476
+ * Task Model
1477
+ */
1478
+ export interface Task {
1479
+ id: TraceID;
1480
+ description: string;
1481
+ agent: AgentID;
1482
+ priority: TaskPriority;
1483
+ status: TaskStatus;
1484
+ createdAt: string;
1485
+ updatedAt: string;
1486
+ }
1487
+ `);
1488
+ console.warn(`✅ Base types created in ${path.relative(targetDir, typesDir)}`);
1489
+ }
1490
+
1491
+ function createBackendFiles(spec) {
1492
+ createBaseTypeFiles(path.join(targetDir, "apps/backend/src"));
1493
+ writeJsonFile(path.join(targetDir, "apps/backend/contract.version.json"), {
1494
+ "contract_hash": "initial_hash_placeholder",
1495
+ "last_updated": new Date().toISOString()
1496
+ });
1497
+ writeJsonFile(path.join(targetDir, "apps/backend/package.json"), {
1498
+ name: "@agent-enderun/backend",
1499
+ version: "0.1.0",
1500
+ private: true,
1501
+ type: "module",
1502
+ scripts: {
1503
+ dev: "tsx src/server.ts",
1504
+ build: "tsc -p tsconfig.json",
1505
+ start: "node dist/server.js",
1506
+ test: "vitest run",
1507
+ },
1508
+ dependencies: {
1509
+ "@fastify/cors": "^11.0.0",
1510
+ fastify: "^5.0.0",
1511
+ zod: "^3.24.2",
1512
+ },
1513
+ devDependencies: {
1514
+ "@types/node": "^22.13.4",
1515
+ tsx: "^4.19.4",
1516
+ typescript: "^5.9.3",
1517
+ vitest: "^3.0.5",
1518
+ },
1519
+ });
1520
+
1521
+ writeJsonFile(path.join(targetDir, "apps/backend/tsconfig.json"), {
1522
+ extends: "../../tsconfig.json",
1523
+ compilerOptions: {
1524
+ outDir: "dist",
1525
+ rootDir: "src",
1526
+ module: "NodeNext",
1527
+ moduleResolution: "NodeNext",
1528
+ target: "ES2022",
1529
+ strict: true,
1530
+ skipLibCheck: true,
1531
+ },
1532
+ include: ["src/**/*.ts"],
1533
+ });
1534
+
1535
+ writeTextFile(path.join(targetDir, "apps/backend/src/data.ts"), [
1536
+ 'import type { Customer, DashboardSummary, ReportMetric, Role, User } from "./types/index.js";',
1537
+ "",
1538
+ 'const now = new Date().toISOString();',
1539
+ "",
1540
+ "export const roles: Role[] = [",
1541
+ ' { id: "role_admin" as Role["id"], name: "Admin", permissions: ["users:manage", "reports:view", "customers:manage"] },',
1542
+ ' { id: "role_manager" as Role["id"], name: "Manager", permissions: ["reports:view", "customers:manage"] },',
1543
+ ' { id: "role_viewer" as Role["id"], name: "Viewer", permissions: ["reports:view"] },',
1544
+ "];",
1545
+ "",
1546
+ "export const users: User[] = [",
1547
+ ' { id: "user_1" as User["id"], email: "admin@example.com", fullName: "Admin User", role: "ADMIN", createdAt: now },',
1548
+ ' { id: "user_2" as User["id"], email: "manager@example.com", fullName: "Sales Manager", role: "DEVELOPER", createdAt: now },',
1549
+ "];",
1550
+ "",
1551
+ "export const customers: Customer[] = [",
1552
+ ' { id: "customer_1" as Customer["id"], name: "Northwind", ownerId: users[1].id, status: "ACTIVE", annualValue: 125000, createdAt: now },',
1553
+ ' { id: "customer_2" as Customer["id"], name: "Acme Corp", ownerId: users[1].id, status: "LEAD", annualValue: 82000, createdAt: now },',
1554
+ ' { id: "customer_3" as Customer["id"], name: "Globex", ownerId: users[0].id, status: "AT_RISK", annualValue: 54000, createdAt: now },',
1555
+ "];",
1556
+ "",
1557
+ "export const reports: ReportMetric[] = [",
1558
+ ' { id: "report_pipeline" as ReportMetric["id"], label: "Pipeline", value: 261000, trend: "UP" },',
1559
+ ' { id: "report_active_customers" as ReportMetric["id"], label: "Active Customers", value: 1, trend: "FLAT" },',
1560
+ ' { id: "report_risk" as ReportMetric["id"], label: "At Risk", value: 1, trend: "DOWN" },',
1561
+ "];",
1562
+ "",
1563
+ "export function getDashboardSummary(): DashboardSummary {",
1564
+ " return { customers, users, roles, reports };",
1565
+ "}",
1566
+ ].join("\n"));
1567
+
1568
+ writeTextFile(path.join(targetDir, "apps/backend/src/server.ts"), [
1569
+ 'import Fastify from "fastify";',
1570
+ 'import cors from "@fastify/cors";',
1571
+ 'import { z } from "zod";',
1572
+ 'import { customers, getDashboardSummary, reports, roles, users } from "./data.js";',
1573
+ "",
1574
+ "const app = Fastify({ logger: true });",
1575
+ "await app.register(cors, { origin: true });",
1576
+ "",
1577
+ 'app.get("/health", async () => ({ ok: true, service: "agent-enderun-backend" }));',
1578
+ 'app.get("/api/v1/dashboard", async () => ({ data: getDashboardSummary() }));',
1579
+ 'app.get("/api/v1/users", async () => ({ data: users }));',
1580
+ 'app.get("/api/v1/roles", async () => ({ data: roles }));',
1581
+ 'app.get("/api/v1/customers", async () => ({ data: customers }));',
1582
+ 'app.get("/api/v1/reports", async () => ({ data: reports }));',
1583
+ "",
1584
+ 'app.post("/api/v1/auth/login", async (request, reply) => {',
1585
+ " const body = z.object({ email: z.string().email(), password: z.string().min(1) }).safeParse(request.body);",
1586
+ " if (!body.success) return reply.code(400).send({ error: { code: \"VALIDATION_ERROR\", message: \"Invalid login payload\" } });",
1587
+ "",
1588
+ " const user = users.find((item) => item.email === body.data.email) || users[0];",
1589
+ " return { data: { user, token: \"demo-token\", expiresAt: new Date(Date.now() + 3600000).toISOString() } };",
1590
+ "});",
1591
+ "",
1592
+ "const port = Number(process.env.PORT || 4000);",
1593
+ "await app.listen({ port, host: \"0.0.0.0\" });",
1594
+ ].join("\n"));
1595
+
1596
+ writeTextFile(path.join(targetDir, "apps/backend/README.md"), [
1597
+ `# ${spec.title} Backend`,
1598
+ "",
1599
+ "Fastify API generated by Agent Enderun.",
1600
+ "",
1601
+ "## Commands",
1602
+ "",
1603
+ "- `npm run dev`",
1604
+ "- `npm run build`",
1605
+ "- `npm run test`",
1606
+ ].join("\n"));
1607
+ }
1608
+
1609
+ function createWebFiles(spec) {
1610
+ createBaseTypeFiles(path.join(targetDir, "apps/web/src"));
1611
+ writeJsonFile(path.join(targetDir, "apps/web/package.json"), {
1612
+ name: "@agent-enderun/web",
1613
+ version: "0.1.0",
1614
+ private: true,
1615
+ type: "module",
1616
+ scripts: {
1617
+ dev: "vite --host 0.0.0.0",
1618
+ build: "tsc -p tsconfig.json && vite build",
1619
+ preview: "vite preview",
1620
+ test: "vitest run",
1621
+ },
1622
+ dependencies: {
1623
+ "@vitejs/plugin-react": "^5.0.0",
1624
+ vite: "^7.0.0",
1625
+ react: "^19.0.0",
1626
+ "react-dom": "^19.0.0",
1627
+ "lucide-react": "^0.468.0",
1628
+ },
1629
+ devDependencies: {
1630
+ "@types/react": "^19.0.0",
1631
+ "@types/react-dom": "^19.0.0",
1632
+ typescript: "^5.9.3",
1633
+ vitest: "^3.0.5",
1634
+ },
1635
+ });
1636
+
1637
+ writeJsonFile(path.join(targetDir, "apps/web/tsconfig.json"), {
1638
+ extends: "../../tsconfig.json",
1639
+ compilerOptions: {
1640
+ jsx: "react-jsx",
1641
+ module: "NodeNext",
1642
+ moduleResolution: "NodeNext",
1643
+ target: "ES2022",
1644
+ strict: true,
1645
+ skipLibCheck: true,
1646
+ },
1647
+ include: ["src/**/*.ts", "src/**/*.tsx"],
1648
+ });
1649
+
1650
+ writeTextFile(path.join(targetDir, "apps/web/index.html"), [
1651
+ '<div id="root"></div>',
1652
+ '<script type="module" src="/src/main.tsx"></script>',
1653
+ ].join("\n"));
1654
+
1655
+ writeTextFile(path.join(targetDir, "apps/web/src/main.tsx"), [
1656
+ 'import React from "react";',
1657
+ 'import { createRoot } from "react-dom/client";',
1658
+ 'import { App } from "./App.js";',
1659
+ 'import "./styles.css";',
1660
+ "",
1661
+ 'createRoot(document.getElementById("root") as HTMLElement).render(',
1662
+ " <React.StrictMode>",
1663
+ " <App />",
1664
+ " </React.StrictMode>,",
1665
+ ");",
1666
+ ].join("\n"));
1667
+
1668
+ writeTextFile(path.join(targetDir, "apps/web/src/App.tsx"), [
1669
+ 'import { BarChart3, ShieldCheck, UsersRound } from "lucide-react";',
1670
+ "",
1671
+ "const metrics = [",
1672
+ ' { label: "Pipeline", value: "$261K", tone: "green" },',
1673
+ ' { label: "Active customers", value: "18", tone: "blue" },',
1674
+ ' { label: "At risk", value: "3", tone: "red" },',
1675
+ "];",
1676
+ "",
1677
+ "const customers = [",
1678
+ ' { name: "Northwind", status: "Active", owner: "Sales Manager", value: "$125K" },',
1679
+ ' { name: "Acme Corp", status: "Lead", owner: "Sales Manager", value: "$82K" },',
1680
+ ' { name: "Globex", status: "At risk", owner: "Admin User", value: "$54K" },',
1681
+ "];",
1682
+ "",
1683
+ "export function App() {",
1684
+ " return (",
1685
+ ' <main className="shell">',
1686
+ ' <aside className="sidebar" aria-label="Primary navigation">',
1687
+ ' <div className="brand">AE</div>',
1688
+ ' <nav>',
1689
+ ' <a className="active" href="#dashboard"><BarChart3 size={18} /> Dashboard</a>',
1690
+ ' <a href="#users"><UsersRound size={18} /> Users</a>',
1691
+ ' <a href="#roles"><ShieldCheck size={18} /> Roles</a>',
1692
+ " </nav>",
1693
+ " </aside>",
1694
+ "",
1695
+ ' <section className="workspace">',
1696
+ ' <header className="topbar">',
1697
+ " <div>",
1698
+ ` <p>${spec.domain}</p>`,
1699
+ ` <h1>${spec.title}</h1>`,
1700
+ " </div>",
1701
+ ' <button type="button">New customer</button>',
1702
+ " </header>",
1703
+ "",
1704
+ ' <section className="metrics" aria-label="Report metrics">',
1705
+ " {metrics.map((metric) => (",
1706
+ ' <article className={`metric ${metric.tone}`} key={metric.label}>',
1707
+ " <span>{metric.label}</span>",
1708
+ " <strong>{metric.value}</strong>",
1709
+ " </article>",
1710
+ " ))}",
1711
+ " </section>",
1712
+ "",
1713
+ ' <section className="panel">',
1714
+ " <div>",
1715
+ " <h2>Customers</h2>",
1716
+ " <p>Ownership, value and status at a glance.</p>",
1717
+ " </div>",
1718
+ ' <div className="table">',
1719
+ " {customers.map((customer) => (",
1720
+ ' <div className="row" key={customer.name}>',
1721
+ " <strong>{customer.name}</strong>",
1722
+ " <span>{customer.status}</span>",
1723
+ " <span>{customer.owner}</span>",
1724
+ " <b>{customer.value}</b>",
1725
+ " </div>",
1726
+ " ))}",
1727
+ " </div>",
1728
+ " </section>",
1729
+ " </section>",
1730
+ " </main>",
1731
+ " );",
1732
+ "}",
1733
+ ].join("\n"));
1734
+
1735
+ writeTextFile(path.join(targetDir, "apps/web/src/styles.css"), [
1736
+ ":root {",
1737
+ " color: #172026;",
1738
+ " background: #f4f7f6;",
1739
+ " font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;",
1740
+ "}",
1741
+ "",
1742
+ "* { box-sizing: border-box; }",
1743
+ "body { margin: 0; }",
1744
+ "button { font: inherit; }",
1745
+ "",
1746
+ ".shell {",
1747
+ " min-height: 100vh;",
1748
+ " display: grid;",
1749
+ " grid-template-columns: 240px 1fr;",
1750
+ "}",
1751
+ "",
1752
+ ".sidebar {",
1753
+ " background: #102022;",
1754
+ " color: #eef6f2;",
1755
+ " padding: 24px;",
1756
+ "}",
1757
+ "",
1758
+ ".brand {",
1759
+ " width: 40px;",
1760
+ " height: 40px;",
1761
+ " display: grid;",
1762
+ " place-items: center;",
1763
+ " background: #d8f36a;",
1764
+ " color: #102022;",
1765
+ " font-weight: 800;",
1766
+ " border-radius: 8px;",
1767
+ " margin-bottom: 32px;",
1768
+ "}",
1769
+ "",
1770
+ "nav { display: grid; gap: 8px; }",
1771
+ "nav a {",
1772
+ " color: inherit;",
1773
+ " text-decoration: none;",
1774
+ " display: flex;",
1775
+ " gap: 10px;",
1776
+ " align-items: center;",
1777
+ " padding: 10px 12px;",
1778
+ " border-radius: 8px;",
1779
+ "}",
1780
+ "nav a.active, nav a:hover { background: rgba(255,255,255,0.12); }",
1781
+ "",
1782
+ ".workspace { padding: 32px; }",
1783
+ ".topbar {",
1784
+ " display: flex;",
1785
+ " justify-content: space-between;",
1786
+ " align-items: center;",
1787
+ " gap: 24px;",
1788
+ " margin-bottom: 24px;",
1789
+ "}",
1790
+ ".topbar p { margin: 0 0 4px; color: #58666a; font-size: 14px; }",
1791
+ ".topbar h1 { margin: 0; font-size: 32px; letter-spacing: 0; }",
1792
+ ".topbar button {",
1793
+ " border: 0;",
1794
+ " border-radius: 8px;",
1795
+ " background: #176b5d;",
1796
+ " color: white;",
1797
+ " padding: 10px 14px;",
1798
+ "}",
1799
+ "",
1800
+ ".metrics {",
1801
+ " display: grid;",
1802
+ " grid-template-columns: repeat(3, minmax(0, 1fr));",
1803
+ " gap: 16px;",
1804
+ " margin-bottom: 24px;",
1805
+ "}",
1806
+ ".metric, .panel {",
1807
+ " background: white;",
1808
+ " border: 1px solid #d9e3e0;",
1809
+ " border-radius: 8px;",
1810
+ "}",
1811
+ ".metric { padding: 18px; }",
1812
+ ".metric span { display: block; color: #58666a; margin-bottom: 8px; }",
1813
+ ".metric strong { font-size: 28px; }",
1814
+ ".metric.green { border-top: 4px solid #49a078; }",
1815
+ ".metric.blue { border-top: 4px solid #3f7cac; }",
1816
+ ".metric.red { border-top: 4px solid #d95d39; }",
1817
+ "",
1818
+ ".panel { padding: 20px; }",
1819
+ ".panel h2 { margin: 0 0 4px; font-size: 20px; }",
1820
+ ".panel p { margin: 0 0 18px; color: #58666a; }",
1821
+ ".table { display: grid; gap: 8px; }",
1822
+ ".row {",
1823
+ " display: grid;",
1824
+ " grid-template-columns: 1.4fr 0.8fr 1fr 0.6fr;",
1825
+ " gap: 16px;",
1826
+ " align-items: center;",
1827
+ " padding: 12px;",
1828
+ " border-radius: 8px;",
1829
+ " background: #f7faf9;",
1830
+ "}",
1831
+ "",
1832
+ "@media (max-width: 760px) {",
1833
+ " .shell { grid-template-columns: 1fr; }",
1834
+ " .sidebar { position: static; }",
1835
+ " .metrics { grid-template-columns: 1fr; }",
1836
+ " .topbar { align-items: flex-start; flex-direction: column; }",
1837
+ " .row { grid-template-columns: 1fr; }",
1838
+ "}",
1839
+ ].join("\n"));
1840
+
1841
+ writeTextFile(path.join(targetDir, "apps/web/README.md"), [
1842
+ `# ${spec.title} Web`,
1843
+ "",
1844
+ "React dashboard generated by Agent Enderun.",
1845
+ "",
1846
+ "## Commands",
1847
+ "",
1848
+ "- `npm run dev`",
1849
+ "- `npm run build`",
1850
+ "- `npm run test`",
1851
+ ].join("\n"));
1852
+ }
1853
+
1854
+ function updateProjectDocs(spec) {
1855
+ const frameworkDir = getFrameworkDir();
1856
+ const docsDir = path.join(targetDir, frameworkDir, "docs");
1857
+ const apiDir = path.join(docsDir, "api");
1858
+ ensureDir(apiDir);
1859
+
1860
+ writeTextFile(path.join(docsDir, "project-docs.md"), [
1861
+ `# ${spec.title} Requirements`,
1862
+ "",
1863
+ "## Request",
1864
+ "",
1865
+ spec.rawDescription,
1866
+ "",
1867
+ "## Generated Scope",
1868
+ "",
1869
+ `- Domain: ${spec.domain}`,
1870
+ `- Auth: ${spec.modules.auth ? "yes" : "no"}`,
1871
+ `- Users: ${spec.modules.users ? "yes" : "no"}`,
1872
+ `- Roles: ${spec.modules.roles ? "yes" : "no"}`,
1873
+ `- Reports: ${spec.modules.reports ? "yes" : "no"}`,
1874
+ "",
1875
+ "## Architecture",
1876
+ "",
1877
+ "- `apps/backend`: Fastify API",
1878
+ "- `apps/web`: React dashboard",
1879
+ "- `apps/backend/src/types`: Contract-first backend TypeScript types",
1880
+ ].join("\n"));
1881
+
1882
+ writeTextFile(path.join(apiDir, "README.md"), [
1883
+ "# API Registry",
1884
+ "",
1885
+ "- `POST /api/v1/auth/login`",
1886
+ "- `GET /api/v1/dashboard`",
1887
+ "- `GET /api/v1/users`",
1888
+ "- `GET /api/v1/roles`",
1889
+ "- `GET /api/v1/customers`",
1890
+ "- `GET /api/v1/reports`",
1891
+ ].join("\n"));
1892
+ }
1893
+
1894
+ function updateMemoryForGeneratedApp(spec, traceId) {
1895
+ const memoryPath = getMemoryPath();
1896
+ if (!fs.existsSync(memoryPath)) return;
1897
+
1898
+ const today = new Date().toISOString().split("T")[0];
1899
+ const history = [
1900
+ `### ${today} — Generated ${spec.title}`,
1901
+ "",
1902
+ "- **Agent:** @manager",
1903
+ `- **Trace ID:** ${traceId}`,
1904
+ "- **Action:** Created full-stack starter from natural language request.",
1905
+ "- **Files:** apps/backend, apps/web, project docs",
1906
+ ].join("\n");
1907
+
1908
+ updateProjectMemoryCommand("HISTORY", history);
1909
+ }
1910
+
1911
+ async function collectCreateAppDescription(args) {
1912
+ const initial = args.join(" ").trim();
1913
+ if (initial) return initial;
1914
+
1915
+ const readline = await import("readline/promises");
1916
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1917
+ try {
1918
+ const idea = await rl.question("What do you want to build? ");
1919
+ const platform = await rl.question("Platform? (full-stack/web/backend) ");
1920
+ const auth = await rl.question("Auth and roles? (yes/no) ");
1921
+ const reports = await rl.question("Reports/dashboard? (yes/no) ");
1922
+ return [idea, platform, auth.includes("y") ? "with auth and roles" : "", reports.includes("y") ? "with reports dashboard" : ""].filter(Boolean).join(" ");
1923
+ } finally {
1924
+ rl.close();
1925
+ }
1926
+ }
1927
+
1928
+ async function createAppCommand(args) {
1929
+ const description = await collectCreateAppDescription(args);
1930
+ const spec = inferAppSpec(description);
1931
+ const traceId = generateULID();
1932
+
1933
+ ensureDir(path.join(targetDir, "apps/backend"));
1934
+ ensureDir(path.join(targetDir, "apps/web"));
1935
+
1936
+ createBackendFiles(spec);
1937
+ createWebFiles(spec);
1938
+ updateProjectDocs(spec);
1939
+
1940
+ const backendTypesPath = path.join(targetDir, "apps/backend/src/types/index.ts");
1941
+ if (fs.existsSync(backendTypesPath)) {
1942
+ const existing = fs.readFileSync(backendTypesPath, "utf8");
1943
+ fs.writeFileSync(backendTypesPath, buildSharedTypesContent(existing));
1944
+ updateContractHashFile();
1945
+ }
1946
+
1947
+ const activeTraceId = traceNewCommand(`Generate ${spec.title} from natural language request`, "manager", "P1") || traceId;
1948
+ updateMemoryForGeneratedApp(spec, activeTraceId);
1949
+
1950
+ console.warn(`\n✅ Created ${spec.title}`);
1951
+ console.warn("📁 Generated apps/backend and apps/web");
1952
+ console.warn("📜 Updated project docs and app-local types contract");
1953
+ console.warn("\nNext commands:");
1954
+ console.warn(" npm install");
1955
+ console.warn(" npm run enderun:build");
1956
+ console.warn(" agent-enderun frontend:dev\n");
1957
+ }
1958
+
1959
+ // --- MAIN DISPATCHER ---
1960
+
1961
+ async function main() {
1962
+ const [command, ...args] = process.argv.slice(2);
1963
+
1964
+ // Skip header for version and mcp commands to keep output clean
1965
+ if (command !== "version" && command !== "-v" && command !== "--version" && command !== "mcp") {
1966
+ console.warn("🤖 Agent Enderun CLI — Initializing...");
1967
+ }
1968
+
1969
+ switch (command) {
1970
+ case "init":
1971
+ await initCommand(args[0]);
1972
+ break;
1973
+ case "status":
1974
+ statusCommand();
1975
+ break;
1976
+ case "trace:new":
1977
+ if (!args[0]) {
1978
+ console.error("❌ Usage: agent-enderun trace:new <description> [agent] [priority]");
1979
+ } else {
1980
+ traceNewCommand(args[0], args[1], args[2]);
1981
+ }
1982
+ break;
1983
+ case "create-app":
1984
+ case "new":
1985
+ case "start":
1986
+ case "build-app":
1987
+ await createAppCommand(args);
1988
+ break;
1989
+ case "verify-contract":
1990
+ verifyContractCommand();
1991
+ break;
1992
+ case "mcp": {
1993
+ const mcpServerPath = path.join(sourceDir, "framework-mcp/dist/index.js");
1994
+ if (fs.existsSync(mcpServerPath)) {
1995
+ const { spawn } = await import("child_process");
1996
+ // Use node to execute the built MCP server
1997
+ const child = spawn("node", [mcpServerPath], { stdio: "inherit" });
1998
+ child.on("exit", (code) => process.exit(code || 0));
1999
+ } else {
2000
+ console.error("❌ MCP Server not built. Run 'npm run enderun:build' first.");
2001
+ process.exit(1);
2002
+ }
2003
+ break;
2004
+ }
2005
+ case "log_agent_action": {
2006
+ // Handle both structured JSON and positional args
2007
+ let data = {};
2008
+ try {
2009
+ if (args[0] && args[0].startsWith("{")) {
2010
+ data = JSON.parse(args.join(" "));
2011
+ } else {
2012
+ data = {
2013
+ agent: args[0],
2014
+ action: args[1],
2015
+ requestId: args[2],
2016
+ status: args[3] || "SUCCESS",
2017
+ summary: args[4] || "",
2018
+ };
2019
+ }
2020
+ } catch {
2021
+ console.error("❌ Error parsing arguments for log_agent_action");
2022
+ process.exit(1);
2023
+ }
2024
+ logAgentActionCommand(data);
2025
+ break;
2026
+ }
2027
+ case "update_project_memory": {
2028
+ let section, content;
2029
+ try {
2030
+ if (args[0] && args[0].startsWith("{")) {
2031
+ const data = JSON.parse(args.join(" "));
2032
+ section = data.section;
2033
+ content = data.content;
2034
+ } else {
2035
+ section = args[0];
2036
+ content = args.slice(1).join(" ");
2037
+ }
2038
+ } catch {
2039
+ console.error("❌ Error parsing arguments for update_project_memory");
2040
+ process.exit(1);
2041
+ }
2042
+ updateProjectMemoryCommand(section, content);
2043
+ break;
2044
+ }
2045
+ case "update_knowledge_base": {
2046
+ const topic = args[0];
2047
+ const content = args.slice(1).join(" ");
2048
+ if (!topic || !content) {
2049
+ console.error("❌ Usage: agent-enderun update_knowledge_base <topic> <content>");
2050
+ process.exit(1);
2051
+ }
2052
+ const frameworkDir = getFrameworkDir();
2053
+ const kbDir = path.join(targetDir, frameworkDir, "knowledge");
2054
+ if (!fs.existsSync(kbDir)) fs.mkdirSync(kbDir, { recursive: true });
2055
+ const fileName = topic.replace(/[^a-z0-9]/gi, "_").toLowerCase() + ".md";
2056
+ fs.writeFileSync(path.join(kbDir, fileName), content);
2057
+ console.warn(`✅ Knowledge base updated: ${topic}`);
2058
+ break;
2059
+ }
2060
+ case "search_knowledge_base": {
2061
+ const query = args[0];
2062
+ if (!query) {
2063
+ console.error("❌ Usage: agent-enderun search_knowledge_base <query>");
2064
+ process.exit(1);
2065
+ }
2066
+ const frameworkDir = getFrameworkDir();
2067
+ const kbDir = path.join(targetDir, frameworkDir, "knowledge");
2068
+ if (!fs.existsSync(kbDir)) {
2069
+ console.warn("ℹ️ Knowledge base is empty.");
2070
+ break;
2071
+ }
2072
+ const files = fs.readdirSync(kbDir).filter(f => f.endsWith(".md"));
2073
+ let found = false;
2074
+ for (const file of files) {
2075
+ const content = fs.readFileSync(path.join(kbDir, file), "utf-8");
2076
+ if (content.toLowerCase().includes(query.toLowerCase()) || file.toLowerCase().includes(query.toLowerCase())) {
2077
+ console.warn(`\n### ${file.replace(".md", "")}\n${content.slice(0, 300)}...`);
2078
+ found = true;
2079
+ }
2080
+ }
2081
+ if (!found) console.warn("ℹ️ No matching entries found.");
2082
+ break;
2083
+ }
2084
+ case "check":
2085
+ checkCommand();
2086
+ break;
2087
+ case "check:security":
2088
+ securityAuditCommand(args[0] || ".");
2089
+ break;
2090
+ case "check:compliance":
2091
+ complianceCheckCommand(args[0] || ".");
2092
+ break;
2093
+ case "git:commit":
2094
+ gitCommitCommand(args[0] || "TRACE-ID-MISSING");
2095
+ break;
2096
+ case "explorer:graph":
2097
+ explorerGraphCommand(args[0] || ".");
2098
+ break;
2099
+ case "explorer:audit":
2100
+ explorerAuditCommand(args[0] || ".");
2101
+ break;
2102
+ case "frontend:dev":
2103
+ runScriptCommand("dev", "apps/web");
2104
+ break;
2105
+ case "frontend:build":
2106
+ runScriptCommand("build", "apps/web");
2107
+ break;
2108
+ case "mobile:dev":
2109
+ runScriptCommand("start", "apps/mobile");
2110
+ break;
2111
+ case "git:sync":
2112
+ gitSyncCommand();
2113
+ break;
2114
+ case "version":
2115
+ case "-v":
2116
+ case "--version":
2117
+ console.warn(`v${FRAMEWORK_VERSION}`);
2118
+ break;
2119
+ default:
2120
+ if (command && (command.includes(" ") || args.length > 0)) {
2121
+ await createAppCommand([command, ...args]);
2122
+ break;
2123
+ }
2124
+ console.warn(`
2125
+ 🤖 Agent Enderun CLI (v${FRAMEWORK_VERSION})
2126
+
2127
+ Available Commands:
2128
+ init [adapter] Initialize (gemini only - installs to .gemini/)
2129
+ create-app <idea> Generate a full-stack starter from natural language
2130
+ check Full health check
2131
+ check:security Run security audit scan
2132
+ check:compliance Run constitution compliance check
2133
+ status Show current phase and task status
2134
+ trace:new <desc> Generate a new Trace ID
2135
+ verify-contract Check shared types integrity
2136
+ explorer:graph Generate dependency graph
2137
+ explorer:audit Codebase intelligence scan
2138
+ git:commit <id> Suggest semantic commit message
2139
+ mcp Start the MCP server
2140
+ version Show version information
2141
+
2142
+ Supported adapter:
2143
+ - gemini → Creates .gemini/ + gemini.md for Gemini CLI users
2144
+
2145
+ Example:
2146
+ agent-enderun init
2147
+ agent-enderun init gemini
2148
+ agent-enderun trace:new "Auth module design" backend P1
2149
+ `);
2150
+ break;
2151
+ }
2152
+ }
2153
+
2154
+ main().catch(console.error);