agent-enderun 0.9.0 → 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 +2 -2
- package/.enderun/PROJECT_MEMORY.md +26 -4
- package/.enderun/STATUS.md +24 -28
- package/.enderun/agents/backend.md +37 -8
- package/.enderun/agents/database.md +55 -0
- package/.enderun/agents/devops.md +28 -14
- package/.enderun/agents/explorer.md +1 -1
- package/.enderun/agents/frontend.md +21 -7
- package/.enderun/agents/git.md +1 -1
- package/.enderun/agents/manager.md +41 -20
- package/.enderun/agents/mobile.md +1 -1
- package/.enderun/agents/native.md +1 -1
- package/.enderun/agents/quality.md +122 -0
- package/.enderun/cli-commands.json +3 -3
- package/.enderun/config.json +7 -1
- 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/frontend_professionalization_guidelines.md +7 -7
- package/.enderun/knowledge/hermes_live_test_guidelines.md +5 -5
- package/.enderun/knowledge/lessons-learned.md +14 -0
- package/.enderun/knowledge/manager_authority_audit_enforcement.md +8 -8
- package/.enderun/knowledge/project_scaffold_guidelines.md +4 -4
- package/.enderun/knowledge/reference_application_guidelines.md +6 -6
- package/.enderun/memory-graph/agent-contexts/quality.json +1 -0
- package/.enderun/queue/README.md +13 -13
- package/README.md +204 -102
- package/bin/cli.js +1 -1
- package/bin/init-check.js +69 -0
- package/bin/update-contract.js +24 -2
- package/docs/README.md +21 -32
- 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 +1 -1
- package/framework-mcp/dist/index.js +7 -13
- package/package.json +3 -1
- 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 +107 -0
- 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/.enderun/agents/analyst.md +0 -236
- package/.enderun/agents/orchestrator.md +0 -172
- package/.enderun/agents/qa.md +0 -121
- 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/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/docs/action-plan-2026.md +0 -119
- package/docs/roadmap.md +0 -142
- package/docs/tech-stack.md +0 -30
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import { ensureDir, writeTextFile, writeJsonFile } from "../utils/fs.js";
|
|
5
|
+
import { updateProjectMemoryCommand } from "../commands/memory.js";
|
|
6
|
+
import { slugifyName, titleCase } from "../utils/string.js";
|
|
7
|
+
import { getConfiguredPaths } from "./memory.js";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const targetDir = process.cwd(); // Assuming targetDir is process.cwd() in the CLI context
|
|
11
|
+
|
|
12
|
+
export function inferAppSpec(description: string) {
|
|
13
|
+
const normalized = String(description || "").toLowerCase();
|
|
14
|
+
const isCrm = /\bcrm\b|customer|musteri|müşteri/.test(normalized);
|
|
15
|
+
const hasAuth = /auth|login|giris|giriş|signin|sign in|user|kullanici|kullanıcı|role|rol/.test(normalized);
|
|
16
|
+
const hasRoles = /role|rol|permission|yetki|admin/.test(normalized);
|
|
17
|
+
const hasReports = /report|rapor|analytics|dashboard|chart|metric/.test(normalized);
|
|
18
|
+
const appName = isCrm ? "crm-dashboard" : slugifyName(description).split("-").slice(0, 4).join("-");
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
rawDescription: description,
|
|
22
|
+
appName,
|
|
23
|
+
title: isCrm ? "CRM Dashboard" : titleCase(appName),
|
|
24
|
+
domain: isCrm ? "CRM" : "Business",
|
|
25
|
+
modules: {
|
|
26
|
+
auth: hasAuth || isCrm,
|
|
27
|
+
users: hasAuth || hasRoles || isCrm,
|
|
28
|
+
roles: hasRoles || isCrm,
|
|
29
|
+
reports: hasReports || isCrm,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildSharedTypesContent(existingContent: string) {
|
|
35
|
+
const marker = "// --- Generated Application Contract ---";
|
|
36
|
+
const generated = [
|
|
37
|
+
marker,
|
|
38
|
+
"export type RoleID = Brand<string, \"RoleID\">;",
|
|
39
|
+
"export type ReportID = Brand<string, \"ReportID\">;",
|
|
40
|
+
"export type CustomerID = Brand<string, \"CustomerID\">;",
|
|
41
|
+
"",
|
|
42
|
+
"export interface AuthSession {",
|
|
43
|
+
" user: User;",
|
|
44
|
+
" token: string;",
|
|
45
|
+
" expiresAt: string;",
|
|
46
|
+
"}",
|
|
47
|
+
"",
|
|
48
|
+
"export interface Role {",
|
|
49
|
+
" id: RoleID;",
|
|
50
|
+
" name: string;",
|
|
51
|
+
" permissions: string[];",
|
|
52
|
+
"}",
|
|
53
|
+
"",
|
|
54
|
+
"export interface Customer {",
|
|
55
|
+
" id: CustomerID;",
|
|
56
|
+
" name: string;",
|
|
57
|
+
" ownerId: UserID;",
|
|
58
|
+
" status: \"LEAD\" | \"ACTIVE\" | \"AT_RISK\";", // Using backticks for inner double quotes
|
|
59
|
+
" annualValue: number;",
|
|
60
|
+
" createdAt: string;",
|
|
61
|
+
"}",
|
|
62
|
+
"",
|
|
63
|
+
"export interface ReportMetric {",
|
|
64
|
+
" id: ReportID;",
|
|
65
|
+
" label: string;",
|
|
66
|
+
" value: number;",
|
|
67
|
+
" trend: \"UP\" | \"DOWN\" | \"FLAT\";", // Using backticks for inner double quotes
|
|
68
|
+
"}",
|
|
69
|
+
"",
|
|
70
|
+
"export interface DashboardSummary {",
|
|
71
|
+
" customers: Customer[];",
|
|
72
|
+
" users: User[];",
|
|
73
|
+
" roles: Role[];",
|
|
74
|
+
" reports: ReportMetric[];",
|
|
75
|
+
"}",
|
|
76
|
+
].join("\n");
|
|
77
|
+
|
|
78
|
+
if (existingContent.includes(marker)) return existingContent;
|
|
79
|
+
return `${existingContent.trim()}\n\n${generated}\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function updateContractHashFile() {
|
|
83
|
+
const pathsMap = getConfiguredPaths();
|
|
84
|
+
const sharedDir = path.join(targetDir, pathsMap.backend, "src/types");
|
|
85
|
+
const contractPath = path.join(targetDir, pathsMap.backend, "contract.version.json");
|
|
86
|
+
if (!fs.existsSync(sharedDir) || !fs.existsSync(contractPath)) return;
|
|
87
|
+
|
|
88
|
+
const walk = (dir: string): string[] => fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
89
|
+
const fullPath = path.join(dir, entry.name);
|
|
90
|
+
return entry.isDirectory() ? walk(fullPath) : (entry.name.endsWith(".ts") ? [fullPath] : []);
|
|
91
|
+
});
|
|
92
|
+
const hash = crypto.createHash("sha256");
|
|
93
|
+
for (const filePath of walk(sharedDir).sort()) {
|
|
94
|
+
hash.update(path.relative(targetDir, filePath));
|
|
95
|
+
hash.update("\0");
|
|
96
|
+
hash.update(fs.readFileSync(filePath));
|
|
97
|
+
hash.update("\0");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
|
|
101
|
+
contract.contract_hash = hash.digest("hex");
|
|
102
|
+
contract.last_updated = new Date().toISOString();
|
|
103
|
+
fs.writeFileSync(contractPath, JSON.stringify(contract, null, 2));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createBaseTypeFiles(baseDir: string) {
|
|
107
|
+
const typesDir = path.join(baseDir, "types");
|
|
108
|
+
ensureDir(typesDir);
|
|
109
|
+
|
|
110
|
+
writeTextFile(path.join(typesDir, "api.ts"), `import { TraceID } from "./brands.js";
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* API Response Wrappers
|
|
114
|
+
*/
|
|
115
|
+
export interface ApiResponse<T> {
|
|
116
|
+
data: T;
|
|
117
|
+
meta?: {
|
|
118
|
+
requestId: TraceID;
|
|
119
|
+
timestamp: string;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ApiError {
|
|
124
|
+
error: {
|
|
125
|
+
code: string;
|
|
126
|
+
message: string;
|
|
127
|
+
details?: unknown;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
`);
|
|
131
|
+
|
|
132
|
+
writeTextFile(path.join(typesDir, "brands.ts"), `/**
|
|
133
|
+
* Branded Type Utility
|
|
134
|
+
*/
|
|
135
|
+
export type Brand<K, T> = K & { __brand: T };
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Entity IDs (Branded)
|
|
139
|
+
*/
|
|
140
|
+
export type TraceID = Brand<string, "TraceID">;
|
|
141
|
+
export type AgentID = Brand<string, "AgentID">;
|
|
142
|
+
export type ProjectID = Brand<string, "ProjectID">;
|
|
143
|
+
export type UserID = Brand<string, "UserID">;
|
|
144
|
+
`);
|
|
145
|
+
|
|
146
|
+
writeTextFile(path.join(typesDir, "constants.ts"), `import { TraceID } from "./brands.js";
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Project Phases
|
|
150
|
+
*/
|
|
151
|
+
export const PROJECT_PHASES = ["PHASE_0", "PHASE_1", "PHASE_2", "PHASE_3", "PHASE_4"] as const;
|
|
152
|
+
export type ProjectPhase = (typeof PROJECT_PHASES)[number];
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Execution Profiles
|
|
156
|
+
*/
|
|
157
|
+
export const EXECUTION_PROFILES = ["Lightweight", "Full"] as const;
|
|
158
|
+
export type ExecutionProfile = (typeof EXECUTION_PROFILES)[number];
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Task Priorities
|
|
162
|
+
*/
|
|
163
|
+
export const TASK_PRIORITIES = ["P0", "P1", "P2", "P3"] as const;
|
|
164
|
+
export type TaskPriority = (typeof TASK_PRIORITIES)[number];
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Task Statuses
|
|
168
|
+
*/
|
|
169
|
+
export const TASK_STATUSES = ["PENDING", "IN_PROGRESS", "BLOCKED", "COMPLETED", "FAILED"] as const;
|
|
170
|
+
export type TaskStatus = (typeof TASK_STATUSES)[number];
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Action Types & Status
|
|
174
|
+
*/
|
|
175
|
+
export const ACTION_TYPES = ["CREATE", "MODIFY", "DELETE", "RESEARCH", "ORCHESTRATE"] as const;
|
|
176
|
+
export type ActionType = (typeof ACTION_TYPES)[number];
|
|
177
|
+
|
|
178
|
+
export const ACTION_STATUSES = ["SUCCESS", "FAILURE", "WAITING"] as const;
|
|
179
|
+
export type ActionStatus = (typeof ACTION_STATUSES)[number];
|
|
180
|
+
`);
|
|
181
|
+
|
|
182
|
+
writeTextFile(path.join(typesDir, "index.ts"), `/**
|
|
183
|
+
* Agent Enderun — App-Local Types (Modular)
|
|
184
|
+
*/
|
|
185
|
+
|
|
186
|
+
export * from "./brands.js";
|
|
187
|
+
export * from "./constants.js";
|
|
188
|
+
export * from "./models.js";
|
|
189
|
+
export * from "./api.js";
|
|
190
|
+
export * from "./logs.js";
|
|
191
|
+
`);
|
|
192
|
+
|
|
193
|
+
writeTextFile(path.join(typesDir, "logs.ts"), `import { TraceID, AgentID } from "./brands.js";
|
|
194
|
+
import { ActionType, ActionStatus } from "./constants.js";
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Audit & Agent Logging Types
|
|
198
|
+
*/
|
|
199
|
+
export interface AgentActionLog {
|
|
200
|
+
timestamp: string;
|
|
201
|
+
agent: AgentID;
|
|
202
|
+
action: ActionType;
|
|
203
|
+
requestId: TraceID | "—";
|
|
204
|
+
status: ActionStatus;
|
|
205
|
+
summary: string;
|
|
206
|
+
files?: string[];
|
|
207
|
+
details?: Record<string, unknown>;
|
|
208
|
+
}
|
|
209
|
+
`);
|
|
210
|
+
|
|
211
|
+
writeTextFile(path.join(typesDir, "models.ts"), `import { UserID, TraceID, AgentID } from "./brands.js";
|
|
212
|
+
import { ProjectPhase, ExecutionProfile, TaskPriority, TaskStatus } from "./constants.js";
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Base Entity Fields
|
|
216
|
+
*/
|
|
217
|
+
export interface BaseEntity {
|
|
218
|
+
id: string; // Usually ULID
|
|
219
|
+
createdAt: string;
|
|
220
|
+
updatedAt: string;
|
|
221
|
+
deletedAt?: string | null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Audit Log Model
|
|
226
|
+
*/
|
|
227
|
+
export interface AuditLog extends BaseEntity {
|
|
228
|
+
entityName: string;
|
|
229
|
+
entityId: string;
|
|
230
|
+
action: "CREATE" | "UPDATE" | "DELETE" | "RESTORE";
|
|
231
|
+
userId: UserID;
|
|
232
|
+
previousState?: Record<string, unknown> | null;
|
|
233
|
+
newState?: Record<string, unknown> | null;
|
|
234
|
+
traceId: TraceID;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* User Model
|
|
239
|
+
*/
|
|
240
|
+
export interface User extends BaseEntity {
|
|
241
|
+
id: UserID;
|
|
242
|
+
email: string;
|
|
243
|
+
fullName: string;
|
|
244
|
+
role: "ADMIN" | "DEVELOPER" | "VIEWER";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface UserProfile extends User {
|
|
248
|
+
avatarUrl?: string;
|
|
249
|
+
bio?: string;
|
|
250
|
+
preferences: Record<string, unknown>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Project Status
|
|
255
|
+
*/
|
|
256
|
+
export interface ProjectStatus {
|
|
257
|
+
phase: ProjectPhase;
|
|
258
|
+
profile: ExecutionProfile;
|
|
259
|
+
lastUpdate: string;
|
|
260
|
+
activeTraceId: TraceID | null;
|
|
261
|
+
blockers: string[];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Task Model
|
|
266
|
+
*/
|
|
267
|
+
export interface Task {
|
|
268
|
+
id: TraceID;
|
|
269
|
+
description: string;
|
|
270
|
+
agent: AgentID;
|
|
271
|
+
priority: TaskPriority;
|
|
272
|
+
status: TaskStatus;
|
|
273
|
+
createdAt: string;
|
|
274
|
+
updatedAt: string;
|
|
275
|
+
}
|
|
276
|
+
`);
|
|
277
|
+
console.warn(`✅ Base types created in ${path.relative(targetDir, typesDir)}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
interface AppSpec {
|
|
281
|
+
rawDescription: string;
|
|
282
|
+
appName: string;
|
|
283
|
+
title: string;
|
|
284
|
+
domain: string;
|
|
285
|
+
modules: {
|
|
286
|
+
auth: boolean;
|
|
287
|
+
users: boolean;
|
|
288
|
+
roles: boolean;
|
|
289
|
+
reports: boolean;
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function createBackendFiles(spec: AppSpec) {
|
|
294
|
+
const pathsMap = getConfiguredPaths();
|
|
295
|
+
const backendDir = pathsMap.backend;
|
|
296
|
+
|
|
297
|
+
createBaseTypeFiles(path.join(targetDir, backendDir, "src"));
|
|
298
|
+
writeJsonFile(path.join(targetDir, backendDir, "contract.version.json"), {
|
|
299
|
+
"contract_hash": "initial_hash_placeholder",
|
|
300
|
+
"last_updated": new Date().toISOString()
|
|
301
|
+
});
|
|
302
|
+
writeJsonFile(path.join(targetDir, backendDir, "package.json"), {
|
|
303
|
+
name: "@agent-enderun/backend",
|
|
304
|
+
version: "0.1.0",
|
|
305
|
+
private: true,
|
|
306
|
+
type: "module",
|
|
307
|
+
scripts: {
|
|
308
|
+
dev: "tsx src/server.ts",
|
|
309
|
+
build: "tsc -p tsconfig.json",
|
|
310
|
+
start: "node dist/server.js",
|
|
311
|
+
test: "vitest run",
|
|
312
|
+
},
|
|
313
|
+
dependencies: {
|
|
314
|
+
"@fastify/cors": "^11.0.0",
|
|
315
|
+
fastify: "^5.0.0",
|
|
316
|
+
zod: "^3.24.2",
|
|
317
|
+
},
|
|
318
|
+
devDependencies: {
|
|
319
|
+
"@types/node": "^22.13.4",
|
|
320
|
+
tsx: "^4.19.4",
|
|
321
|
+
typescript: "^5.9.3",
|
|
322
|
+
vitest: "^3.0.5",
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
writeJsonFile(path.join(targetDir, backendDir, "tsconfig.json"), {
|
|
327
|
+
extends: "../../tsconfig.json",
|
|
328
|
+
compilerOptions: {
|
|
329
|
+
outDir: "dist",
|
|
330
|
+
rootDir: "src",
|
|
331
|
+
module: "NodeNext",
|
|
332
|
+
moduleResolution: "NodeNext",
|
|
333
|
+
target: "ES2022",
|
|
334
|
+
strict: true,
|
|
335
|
+
skipLibCheck: true,
|
|
336
|
+
},
|
|
337
|
+
include: ["src/**/*.ts"],
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
writeTextFile(path.join(targetDir, backendDir, "src/data.ts"), [
|
|
341
|
+
"import type { Customer, DashboardSummary, ReportMetric, Role, User } from \"./types/index.js\";",
|
|
342
|
+
"",
|
|
343
|
+
"const now = new Date().toISOString();",
|
|
344
|
+
"",
|
|
345
|
+
"export const roles: Role[] = [",
|
|
346
|
+
" { id: \"role_admin\" as Role[\"id\"], name: \"Admin\", permissions: [\"users:manage\", \"reports:view\", \"customers:manage\"] },",
|
|
347
|
+
" { id: \"role_manager\" as Role[\"id\"], name: \"Manager\", permissions: [\"reports:view\", \"customers:manage\"] },",
|
|
348
|
+
" { id: \"role_viewer\" as Role[\"id\"], name: \"Viewer\", permissions: [\"reports:view\"] },",
|
|
349
|
+
"];",
|
|
350
|
+
"",
|
|
351
|
+
"export const users: User[] = [",
|
|
352
|
+
" { id: \"user_1\" as User[\"id\"], email: \"admin@example.com\", fullName: \"Admin User\", role: \"ADMIN\", createdAt: now },",
|
|
353
|
+
" { id: \"user_2\" as User[\"id\"], email: \"manager@example.com\", fullName: \"Sales Manager\", role: \"DEVELOPER\", createdAt: now },",
|
|
354
|
+
"];",
|
|
355
|
+
"",
|
|
356
|
+
"export const customers: Customer[] = [",
|
|
357
|
+
" { id: \"customer_1\" as Customer[\"id\"], name: \"Northwind\", ownerId: users[1].id, status: \"ACTIVE\", annualValue: 125000, createdAt: now },",
|
|
358
|
+
" { id: \"customer_2\" as Customer[\"id\"], name: \"Acme Corp\", ownerId: users[1].id, status: \"LEAD\", annualValue: 82000, createdAt: now },",
|
|
359
|
+
" { id: \"customer_3\" as Customer[\"id\"], name: \"Globex\", ownerId: users[0].id, status: \"AT_RISK\", annualValue: 54000, createdAt: now },",
|
|
360
|
+
"];",
|
|
361
|
+
"",
|
|
362
|
+
"export const reports: ReportMetric[] = [",
|
|
363
|
+
" { id: \"report_pipeline\" as ReportMetric[\"id\"], label: \"Pipeline\", value: 261000, trend: \"UP\" },",
|
|
364
|
+
" { id: \"report_active_customers\" as ReportMetric[\"id\"], label: \"Active Customers\", value: 1, trend: \"FLAT\" },",
|
|
365
|
+
" { id: \"report_risk\" as ReportMetric[\"id\"], label: \"At Risk\", value: 1, trend: \"DOWN\" },",
|
|
366
|
+
"];",
|
|
367
|
+
"",
|
|
368
|
+
"export function getDashboardSummary(): DashboardSummary {",
|
|
369
|
+
" return { customers, users, roles, reports };",
|
|
370
|
+
"}",
|
|
371
|
+
].join("\n"));
|
|
372
|
+
|
|
373
|
+
writeTextFile(path.join(targetDir, backendDir, "src/server.ts"), [
|
|
374
|
+
"import Fastify from \"fastify\";",
|
|
375
|
+
"import cors from \"@fastify/cors\";",
|
|
376
|
+
"import { z } from \"zod\";",
|
|
377
|
+
"import { customers, getDashboardSummary, reports, roles, users } from \"./data.js\";",
|
|
378
|
+
"",
|
|
379
|
+
"const app = Fastify({ logger: true });",
|
|
380
|
+
"await app.register(cors, { origin: true });",
|
|
381
|
+
"",
|
|
382
|
+
"app.get(\"/health\", async () => ({ ok: true, service: \"agent-enderun-backend\" }));",
|
|
383
|
+
"app.get(\"/api/v1/dashboard\", async () => ({ data: getDashboardSummary() }));",
|
|
384
|
+
"app.get(\"/api/v1/users\", async () => ({ data: users }));",
|
|
385
|
+
"app.get(\"/api/v1/roles\", async () => ({ data: roles }));",
|
|
386
|
+
"app.get(\"/api/v1/customers\", async () => ({ data: customers }));",
|
|
387
|
+
"app.get(\"/api/v1/reports\", async (request, reply) => {",
|
|
388
|
+
" interface QueryParams {",
|
|
389
|
+
" page?: number;",
|
|
390
|
+
" limit?: number;",
|
|
391
|
+
" }",
|
|
392
|
+
" const { page = 1, limit = 10 } = request.query as QueryParams;",
|
|
393
|
+
" const startIndex = (Number(page) - 1) * Number(limit);",
|
|
394
|
+
" const endIndex = startIndex + Number(limit);",
|
|
395
|
+
" const paginatedReports = reports.slice(startIndex, endIndex);",
|
|
396
|
+
" reply.send({",
|
|
397
|
+
" data: paginatedReports,",
|
|
398
|
+
" meta: {",
|
|
399
|
+
" total: reports.length,",
|
|
400
|
+
" page: Number(page),",
|
|
401
|
+
" limit: Number(limit),",
|
|
402
|
+
" },",
|
|
403
|
+
" });",
|
|
404
|
+
"});",
|
|
405
|
+
"",
|
|
406
|
+
"app.post(\"/api/v1/auth/login\", async (request, reply) => {",
|
|
407
|
+
" const body = z.object({ email: z.string().email(), password: z.string().min(1) }).safeParse(request.body);",
|
|
408
|
+
" if (!body.success) return reply.code(400).send({ error: { code: \"VALIDATION_ERROR\", message: \"Invalid login payload\" } });",
|
|
409
|
+
"",
|
|
410
|
+
" const user = users.find((item) => item.email === body.data.email) || users[0];",
|
|
411
|
+
" return { data: { user, token: \"demo-token\", expiresAt: new Date(Date.now() + 3600000).toISOString() } };",
|
|
412
|
+
"});",
|
|
413
|
+
"",
|
|
414
|
+
"const port = Number(process.env.PORT || 4000);",
|
|
415
|
+
"await app.listen({ port, host: \"0.0.0.0\" });",
|
|
416
|
+
].join("\n"));
|
|
417
|
+
|
|
418
|
+
writeTextFile(path.join(targetDir, backendDir, "README.md"), [
|
|
419
|
+
`# ${spec.title} Backend`,
|
|
420
|
+
"",
|
|
421
|
+
"Fastify API generated by Agent Enderun.",
|
|
422
|
+
"",
|
|
423
|
+
"## Commands",
|
|
424
|
+
"",
|
|
425
|
+
"- `npm run dev` strings",
|
|
426
|
+
"- `npm run build` strings",
|
|
427
|
+
"- `npm run test` strings",
|
|
428
|
+
].join("\n"));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function createWebFiles(spec: AppSpec) {
|
|
432
|
+
const pathsMap = getConfiguredPaths();
|
|
433
|
+
const frontendDir = pathsMap.frontend;
|
|
434
|
+
|
|
435
|
+
createBaseTypeFiles(path.join(targetDir, frontendDir, "src"));
|
|
436
|
+
writeJsonFile(path.join(targetDir, frontendDir, "package.json"), {
|
|
437
|
+
name: "@agent-enderun/web",
|
|
438
|
+
version: "0.1.0",
|
|
439
|
+
private: true,
|
|
440
|
+
type: "module",
|
|
441
|
+
scripts: {
|
|
442
|
+
dev: "vite --host 0.0.0.0",
|
|
443
|
+
build: "tsc -p tsconfig.json && vite build",
|
|
444
|
+
preview: "vite preview",
|
|
445
|
+
test: "vitest run",
|
|
446
|
+
},
|
|
447
|
+
dependencies: {
|
|
448
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
449
|
+
vite: "^7.0.0",
|
|
450
|
+
react: "^19.0.0",
|
|
451
|
+
"react-dom": "^19.0.0",
|
|
452
|
+
"lucide-react": "^0.468.0",
|
|
453
|
+
},
|
|
454
|
+
devDependencies: {
|
|
455
|
+
"@types/react": "^19.0.0",
|
|
456
|
+
"@types/react-dom": "^19.0.0",
|
|
457
|
+
typescript: "^5.9.3",
|
|
458
|
+
vitest: "^3.0.5",
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
writeJsonFile(path.join(targetDir, frontendDir, "tsconfig.json"), {
|
|
463
|
+
extends: "../../tsconfig.json",
|
|
464
|
+
compilerOptions: {
|
|
465
|
+
jsx: "react-jsx",
|
|
466
|
+
module: "NodeNext",
|
|
467
|
+
moduleResolution: "NodeNext",
|
|
468
|
+
target: "ES2022",
|
|
469
|
+
strict: true,
|
|
470
|
+
skipLibCheck: true,
|
|
471
|
+
},
|
|
472
|
+
include: ["src/**/*.ts", "src/**/*.tsx"],
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
writeTextFile(path.join(targetDir, frontendDir, "index.html"), [
|
|
476
|
+
"<div id=\"root\"></div>",
|
|
477
|
+
"<script type=\"module\" src=\"/src/main.tsx\"></script>",
|
|
478
|
+
].join("\n"));
|
|
479
|
+
|
|
480
|
+
writeTextFile(path.join(targetDir, frontendDir, "src/main.tsx"), [
|
|
481
|
+
"import React from \"react\";",
|
|
482
|
+
"import { createRoot } from \"react-dom/client\";",
|
|
483
|
+
"import { App } from \"./App.js\";",
|
|
484
|
+
"import \"./styles.css\";",
|
|
485
|
+
"",
|
|
486
|
+
"createRoot(document.getElementById(\"root\") as HTMLElement).render(",
|
|
487
|
+
" <React.StrictMode>",
|
|
488
|
+
" <App />",
|
|
489
|
+
" </React.StrictMode>,",
|
|
490
|
+
");",
|
|
491
|
+
].join("\n"));
|
|
492
|
+
|
|
493
|
+
writeTextFile(path.join(targetDir, frontendDir, "src/App.tsx"), [
|
|
494
|
+
"import { BarChart3, ShieldCheck, UsersRound } from \"lucide-react\";",
|
|
495
|
+
"",
|
|
496
|
+
"const metrics = [",
|
|
497
|
+
" { label: \"Pipeline\", value: \"$261K\", tone: \"green\" },",
|
|
498
|
+
" { label: \"Active customers\", value: \"18\", tone: \"blue\" },",
|
|
499
|
+
" { label: \"At risk\", value: \"3\", tone: \"red\" },",
|
|
500
|
+
"];",
|
|
501
|
+
"",
|
|
502
|
+
"const customers = [",
|
|
503
|
+
" { name: \"Northwind\", status: \"Active\", owner: \"Sales Manager\", value: \"$125K\" },",
|
|
504
|
+
" { name: \"Acme Corp\", status: \"Lead\", owner: \"Sales Manager\", value: \"$82K\" },",
|
|
505
|
+
" { name: \"Globex\", status: \"At risk\", owner: \"Admin User\", value: \"$54K\" },",
|
|
506
|
+
"];",
|
|
507
|
+
"",
|
|
508
|
+
"export function App() {",
|
|
509
|
+
" return (",
|
|
510
|
+
" <main className=\"shell\">",
|
|
511
|
+
" <aside className=\"sidebar\" aria-label=\"Primary navigation\">",
|
|
512
|
+
" <div className=\"brand\">AE</div>",
|
|
513
|
+
" <nav>",
|
|
514
|
+
" <a className=\"active\" href=\"#dashboard\"><BarChart3 size={18} /> Dashboard</a>",
|
|
515
|
+
" <a href=\"#users\"><UsersRound size={18} /> Users</a>",
|
|
516
|
+
" <a href=\"#roles\"><ShieldCheck size={18} /> Roles</a>",
|
|
517
|
+
" </nav>",
|
|
518
|
+
" </aside>",
|
|
519
|
+
"",
|
|
520
|
+
" <section className=\"workspace\">",
|
|
521
|
+
" <header className=\"topbar\">",
|
|
522
|
+
" <div>",
|
|
523
|
+
` <p>${spec.domain}</p>`,
|
|
524
|
+
` <h1>${spec.title}</h1>`,
|
|
525
|
+
" </div>",
|
|
526
|
+
" <button type=\"button\">New customer</button>",
|
|
527
|
+
" </header>",
|
|
528
|
+
"",
|
|
529
|
+
" <section className=\"metrics\" aria-label=\"Report metrics\">",
|
|
530
|
+
" {metrics.map((metric) => (",
|
|
531
|
+
" <article className={`metric ${metric.tone}`} key={metric.label}>",
|
|
532
|
+
" <span>{metric.label}</span>",
|
|
533
|
+
" <strong>{metric.value}</strong>",
|
|
534
|
+
" </article>",
|
|
535
|
+
" ))}",
|
|
536
|
+
" </section>",
|
|
537
|
+
"",
|
|
538
|
+
" <section className=\"panel\">",
|
|
539
|
+
" <div>",
|
|
540
|
+
" <h2>Customers</h2>",
|
|
541
|
+
" <p>Ownership, value and status at a glance.</p>",
|
|
542
|
+
" </div>",
|
|
543
|
+
" <div className=\"table\">",
|
|
544
|
+
" {customers.map((customer) => (",
|
|
545
|
+
" <div className=\"row\" key={customer.name}>",
|
|
546
|
+
" <strong>{customer.name}</strong>",
|
|
547
|
+
" <span>{customer.status}</span>",
|
|
548
|
+
" <span>{customer.owner}</span>",
|
|
549
|
+
" <b>{customer.value}</b>",
|
|
550
|
+
" </div>",
|
|
551
|
+
" ))}",
|
|
552
|
+
" </div>",
|
|
553
|
+
" </section>",
|
|
554
|
+
" </section>",
|
|
555
|
+
" </main>",
|
|
556
|
+
" );",
|
|
557
|
+
"}",
|
|
558
|
+
].join("\n"));
|
|
559
|
+
|
|
560
|
+
writeTextFile(path.join(targetDir, frontendDir, "src/styles.css"), [
|
|
561
|
+
":root {",
|
|
562
|
+
" color: #172026;",
|
|
563
|
+
" background: #f4f7f6;",
|
|
564
|
+
" font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;",
|
|
565
|
+
"}",
|
|
566
|
+
"",
|
|
567
|
+
"* { box-sizing: border-box; }",
|
|
568
|
+
"body { margin: 0; }",
|
|
569
|
+
"button { font: inherit; }",
|
|
570
|
+
"",
|
|
571
|
+
".shell {",
|
|
572
|
+
" min-height: 100vh;",
|
|
573
|
+
" display: grid;",
|
|
574
|
+
" grid-template-columns: 240px 1fr;",
|
|
575
|
+
"}",
|
|
576
|
+
"",
|
|
577
|
+
".sidebar {",
|
|
578
|
+
" background: #102022;",
|
|
579
|
+
" color: #eef6f2;",
|
|
580
|
+
" padding: 24px;",
|
|
581
|
+
"}",
|
|
582
|
+
"",
|
|
583
|
+
".brand {",
|
|
584
|
+
" width: 40px;",
|
|
585
|
+
" height: 40px;",
|
|
586
|
+
" display: grid;",
|
|
587
|
+
" place-items: center;",
|
|
588
|
+
" background: #d8f36a;",
|
|
589
|
+
" color: #102022;",
|
|
590
|
+
" font-weight: 800;",
|
|
591
|
+
" border-radius: 8px;",
|
|
592
|
+
" margin-bottom: 32px;",
|
|
593
|
+
"}",
|
|
594
|
+
"",
|
|
595
|
+
"nav { display: grid; gap: 8px; }",
|
|
596
|
+
"nav a {",
|
|
597
|
+
" color: inherit;",
|
|
598
|
+
" text-decoration: none;",
|
|
599
|
+
" display: flex;",
|
|
600
|
+
" gap: 10px;",
|
|
601
|
+
" align-items: center;",
|
|
602
|
+
" padding: 10px 12px;",
|
|
603
|
+
" border-radius: 8px;",
|
|
604
|
+
"}",
|
|
605
|
+
"nav a.active, nav a:hover { background: rgba(255,255,255,0.12); }",
|
|
606
|
+
"",
|
|
607
|
+
".workspace { padding: 32px; }",
|
|
608
|
+
".topbar {",
|
|
609
|
+
" display: flex;",
|
|
610
|
+
" justify-content: space-between;",
|
|
611
|
+
" align-items: center;",
|
|
612
|
+
" gap: 24px;",
|
|
613
|
+
" margin-bottom: 24px;",
|
|
614
|
+
"}",
|
|
615
|
+
".topbar p { margin: 0 0 4px; color: #58666a; font-size: 14px; }",
|
|
616
|
+
".topbar h1 { margin: 0; font-size: 32px; letter-spacing: 0; }",
|
|
617
|
+
".topbar button {",
|
|
618
|
+
" border: 0;",
|
|
619
|
+
" border-radius: 8px;",
|
|
620
|
+
" background: #176b5d;",
|
|
621
|
+
" color: white;",
|
|
622
|
+
" padding: 10px 14px;",
|
|
623
|
+
"}",
|
|
624
|
+
"",
|
|
625
|
+
".metrics {",
|
|
626
|
+
" display: grid;",
|
|
627
|
+
" grid-template-columns: repeat(3, minmax(0, 1fr));",
|
|
628
|
+
" gap: 16px;",
|
|
629
|
+
" margin-bottom: 24px;",
|
|
630
|
+
"}",
|
|
631
|
+
".metric, .panel {",
|
|
632
|
+
" background: white;",
|
|
633
|
+
" border: 1px solid #d9e3e0;",
|
|
634
|
+
" border-radius: 8px;",
|
|
635
|
+
"}",
|
|
636
|
+
".metric { padding: 18px; }",
|
|
637
|
+
".metric span { display: block; color: #58666a; margin-bottom: 8px; }",
|
|
638
|
+
".metric strong { font-size: 28px; }",
|
|
639
|
+
".metric.green { border-top: 4px solid #49a078; }",
|
|
640
|
+
".metric.blue { border-top: 4px solid #3f7cac; }",
|
|
641
|
+
".metric.red { border-top: 4px solid #d95d39; }",
|
|
642
|
+
"",
|
|
643
|
+
".panel { padding: 20px; }",
|
|
644
|
+
".panel h2 { margin: 0 0 4px; font-size: 20px; }",
|
|
645
|
+
".panel p { margin: 0 0 18px; color: #58666a; }",
|
|
646
|
+
".table { display: grid; gap: 8px; }",
|
|
647
|
+
".row {",
|
|
648
|
+
" display: grid;",
|
|
649
|
+
" grid-template-columns: 1.4fr 0.8fr 1fr 0.6fr;",
|
|
650
|
+
" gap: 16px;",
|
|
651
|
+
" align-items: center;",
|
|
652
|
+
" padding: 12px;",
|
|
653
|
+
" border-radius: 8px;",
|
|
654
|
+
" background: #f7faf9;",
|
|
655
|
+
"}",
|
|
656
|
+
"",
|
|
657
|
+
"@media (max-width: 760px) {",
|
|
658
|
+
" .shell { grid-template-columns: 1fr; }",
|
|
659
|
+
" .sidebar { position: static; }",
|
|
660
|
+
" .metrics { grid-template-columns: 1fr; }",
|
|
661
|
+
" .topbar { align-items: flex-start; flex-direction: column; }",
|
|
662
|
+
" .row { grid-template-columns: 1fr; }",
|
|
663
|
+
"}",
|
|
664
|
+
].join("\n"));
|
|
665
|
+
|
|
666
|
+
writeTextFile(path.join(targetDir, frontendDir, "README.md"), [
|
|
667
|
+
`# ${spec.title} Web`,
|
|
668
|
+
"",
|
|
669
|
+
"React dashboard generated by Agent Enderun.",
|
|
670
|
+
"",
|
|
671
|
+
"## Commands",
|
|
672
|
+
"",
|
|
673
|
+
"- `npm run dev` strings",
|
|
674
|
+
"- `npm run build` strings",
|
|
675
|
+
"- `npm run test` strings",
|
|
676
|
+
|
|
677
|
+
].join("\n"));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export function updateProjectDocs(spec: AppSpec) {
|
|
681
|
+
const pathsMap = getConfiguredPaths();
|
|
682
|
+
const docsDir = path.join(targetDir, pathsMap.docs);
|
|
683
|
+
const apiDir = path.join(docsDir, "api");
|
|
684
|
+
ensureDir(apiDir);
|
|
685
|
+
|
|
686
|
+
writeTextFile(path.join(docsDir, "project-docs.md"), [
|
|
687
|
+
`# ${spec.title} Requirements`,
|
|
688
|
+
"",
|
|
689
|
+
"## Request",
|
|
690
|
+
"",
|
|
691
|
+
spec.rawDescription,
|
|
692
|
+
"",
|
|
693
|
+
"## Generated Scope",
|
|
694
|
+
"",
|
|
695
|
+
`- Domain: ${spec.domain}`,
|
|
696
|
+
`- Auth: ${spec.modules.auth ? "yes" : "no"}`,
|
|
697
|
+
`- Users: ${spec.modules.users ? "yes" : "no"}`,
|
|
698
|
+
`- Roles: ${spec.modules.roles ? "yes" : "no"}`,
|
|
699
|
+
`- Reports: ${spec.modules.reports ? "yes" : "no"}`,
|
|
700
|
+
"",
|
|
701
|
+
"## Architecture",
|
|
702
|
+
"",
|
|
703
|
+
`- \`${pathsMap.backend}\`: Fastify API`,
|
|
704
|
+
`- \`${pathsMap.frontend}\`: React dashboard`,
|
|
705
|
+
`- \`${pathsMap.backend}/src/types\`: Contract-first backend TypeScript types`,
|
|
706
|
+
].join("\n"));
|
|
707
|
+
|
|
708
|
+
writeTextFile(path.join(apiDir, "README.md"), [
|
|
709
|
+
"# API Registry",
|
|
710
|
+
"",
|
|
711
|
+
"- `POST /api/v1/auth/login`",
|
|
712
|
+
"- `GET /api/v1/dashboard`",
|
|
713
|
+
"- `GET /api/v1/users`",
|
|
714
|
+
"- `GET /api/v1/roles`",
|
|
715
|
+
"- `GET /api/v1/customers`",
|
|
716
|
+
"- `GET /api/v1/reports`",
|
|
717
|
+
].join("\n"));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function updateMemoryForGeneratedApp(spec: AppSpec, traceId: string) {
|
|
721
|
+
const memoryPath = getMemoryPath();
|
|
722
|
+
if (!fs.existsSync(memoryPath)) return;
|
|
723
|
+
|
|
724
|
+
const pathsMap = getConfiguredPaths();
|
|
725
|
+
const today = new Date().toISOString().split("T")[0];
|
|
726
|
+
const history = [
|
|
727
|
+
`### ${today} — Generated ${spec.title}`,
|
|
728
|
+
"",
|
|
729
|
+
"- **Agent:** @manager",
|
|
730
|
+
`- **Trace ID:** ${traceId}`,
|
|
731
|
+
"- **Action:** Created full-stack starter from natural language request.",
|
|
732
|
+
`- **Files:** ${pathsMap.backend}, ${pathsMap.frontend}, project docs`,
|
|
733
|
+
].join("\n");
|
|
734
|
+
|
|
735
|
+
updateProjectMemoryCommand("HISTORY", history);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
export async function collectCreateAppDescription(args: string[]) {
|
|
739
|
+
const initial = args.join(" ").trim();
|
|
740
|
+
if (initial) return initial;
|
|
741
|
+
|
|
742
|
+
const readline = await import("readline/promises");
|
|
743
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
744
|
+
try {
|
|
745
|
+
const idea = await rl.question("What do you want to build? ");
|
|
746
|
+
const platform = await rl.question("Platform? (full-stack/web/backend) ");
|
|
747
|
+
const auth = await rl.question("Auth and roles? (yes/no) ");
|
|
748
|
+
const reports = await rl.question("Reports/dashboard? (yes/no) ");
|
|
749
|
+
return [idea, platform, auth.includes("y") ? "with auth and roles" : "", reports.includes("y") ? "with reports dashboard" : ""].filter(Boolean).join(" ");
|
|
750
|
+
} finally {
|
|
751
|
+
rl.close();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function getFrameworkDir(): string {
|
|
756
|
+
for (const dir of [".gemini", ".claude", ".agent", ".enderun"]) {
|
|
757
|
+
if (fs.existsSync(path.join(process.cwd(), dir))) return dir;
|
|
758
|
+
}
|
|
759
|
+
return ".gemini";
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function getMemoryPath(): string {
|
|
763
|
+
return path.join(process.cwd(), getFrameworkDir(), "PROJECT_MEMORY.md");
|
|
764
|
+
}
|