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