agent-enderun 0.5.0 → 0.5.2
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 +0 -0
- package/.enderun/PROJECT_MEMORY.md +40 -1
- package/.enderun/STATUS.md +2 -0
- package/.enderun/agents/analyst.md +8 -8
- package/.enderun/agents/backend.md +11 -11
- package/.enderun/agents/explorer.md +4 -4
- package/.enderun/agents/frontend.md +7 -7
- package/.enderun/agents/git.md +5 -5
- package/.enderun/agents/manager.md +12 -12
- package/.enderun/agents/mobile.md +5 -5
- package/.enderun/agents/native.md +5 -5
- package/.enderun/benchmarks/.gitkeep +0 -0
- package/.enderun/cli-commands.json +0 -0
- package/.enderun/config.json +0 -0
- package/.enderun/docs/api/README.md +0 -0
- package/.enderun/docs/api/auth.md +0 -0
- package/.enderun/docs/api/errors.md +0 -0
- package/.enderun/docs/error-handling.md +0 -0
- package/.enderun/docs/privacy.md +0 -0
- package/.enderun/docs/project-docs.md +0 -0
- package/.enderun/docs/security.md +0 -0
- package/.enderun/docs/tech-stack.md +38 -10
- package/.enderun/docs/troubleshooting.md +0 -0
- package/.enderun/knowledge/api_design_rules.md +0 -0
- package/.enderun/knowledge/async_error_handling.md +0 -0
- package/.enderun/knowledge/branded_types_pattern.md +7 -0
- package/.enderun/knowledge/code_review_checklist.md +0 -0
- package/.enderun/knowledge/contract_versioning.md +0 -0
- package/.enderun/knowledge/database_migration.md +0 -0
- package/.enderun/knowledge/deployment_checklist.md +0 -0
- package/.enderun/knowledge/git_commit_strategy.md +0 -0
- package/.enderun/knowledge/hermes_protocol.md +59 -0
- package/.enderun/knowledge/legacy_onboarding.md +0 -0
- package/.enderun/knowledge/monitoring_setup.md +0 -0
- package/.enderun/knowledge/performance_guidelines.md +0 -0
- package/.enderun/knowledge/repository_patterns.md +0 -0
- package/.enderun/knowledge/responsive_design_standards.md +0 -0
- package/.enderun/knowledge/security_scanning.md +0 -0
- package/.enderun/knowledge/testing_standards.md +1 -1
- package/.enderun/knowledge/troubleshooting_guide.md +0 -0
- package/.enderun/knowledge/zero_ui_library_policy.md +8 -4
- package/.enderun/logs/.gitkeep +0 -0
- package/.enderun/messages/.gitkeep +0 -0
- package/.enderun/monitoring/.gitkeep +0 -0
- package/.env.example +0 -0
- package/ENDERUN.md +10 -5
- package/LICENSE +0 -0
- package/README.md +93 -45
- package/bin/cli.js +633 -3
- package/bin/update-contract.js +63 -0
- package/claude.md +2 -2
- package/codex.md +2 -2
- package/cursor.md +2 -2
- package/docs/README.md +23 -0
- package/gemini-extension.json +8 -2
- package/gemini.md +2 -2
- package/mcp.json +0 -0
- package/package.json +4 -3
- package/packages/framework-mcp/dist/index.js +0 -0
- package/packages/framework-mcp/dist/schemas.js +16 -0
- package/packages/framework-mcp/dist/tools/contract.js +13 -7
- package/packages/framework-mcp/dist/tools/framework.js +25 -0
- package/packages/framework-mcp/dist/tools/knowledge.js +75 -11
- package/packages/framework-mcp/dist/tools/messages.js +73 -11
- package/packages/framework-mcp/dist/tools/repository.js +24 -3
- package/packages/framework-mcp/dist/utils.js +2 -2
- package/packages/framework-mcp/package.json +2 -2
- package/packages/framework-mcp/src/schemas.ts +20 -0
- package/packages/framework-mcp/src/tools/contract.ts +14 -7
- package/packages/framework-mcp/src/tools/framework.ts +24 -0
- package/packages/framework-mcp/src/tools/knowledge.ts +86 -12
- package/packages/framework-mcp/src/tools/messages.ts +80 -11
- package/packages/framework-mcp/src/tools/repository.ts +24 -3
- package/packages/framework-mcp/src/utils.ts +2 -2
- package/packages/shared-types/README.md +0 -0
- package/packages/shared-types/contract.version.json +6 -3
- package/packages/shared-types/package.json +4 -4
- package/packages/shared-types/src/index.ts +0 -0
- package/packages/shared-types/tsconfig.json +0 -0
- package/panda.config.ts +5 -1
- package/tsconfig.json +0 -0
- package/.enderun/ENDERUN.md +0 -205
- package/packages/framework-mcp/dist/index.d.ts +0 -1
- package/packages/shared-types/dist/index.d.ts.map +0 -1
package/bin/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ function getPackageVersion() {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function getFrameworkDir() {
|
|
25
|
-
const adapters = [".gemini", ".claude", ".cursor", ".
|
|
25
|
+
const adapters = [".gemini", ".claude", ".cursor", ".enderun", ".codex"];
|
|
26
26
|
for (const adp of adapters) {
|
|
27
27
|
const fullPath = path.join(targetDir, adp);
|
|
28
28
|
if (fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory()) {
|
|
@@ -323,7 +323,7 @@ async function initCommand(selectedAdapter) {
|
|
|
323
323
|
codex: ["codex.md"],
|
|
324
324
|
};
|
|
325
325
|
|
|
326
|
-
const targetBase = selectedAdapter ? `.${selectedAdapter}` : ".enderun";
|
|
326
|
+
const targetBase = selectedAdapter && selectedAdapter !== "codex" ? `.${selectedAdapter}` : ".enderun";
|
|
327
327
|
|
|
328
328
|
const targetFrameworkDir = path.join(targetDir, targetBase);
|
|
329
329
|
|
|
@@ -351,7 +351,7 @@ async function initCommand(selectedAdapter) {
|
|
|
351
351
|
`${targetBase}/messages`,
|
|
352
352
|
"apps/web",
|
|
353
353
|
"apps/backend",
|
|
354
|
-
|
|
354
|
+
"docs",
|
|
355
355
|
"packages/shared-types",
|
|
356
356
|
"packages/framework-mcp",
|
|
357
357
|
];
|
|
@@ -454,6 +454,8 @@ async function initCommand(selectedAdapter) {
|
|
|
454
454
|
|
|
455
455
|
content = content.replace(/\{\{FRAMEWORK_DIR\}\}/g, targetBase);
|
|
456
456
|
content = content.replace(/\{\{ADAPTER\}\}/g, currentAdapter);
|
|
457
|
+
// Fallback: replace any residual hardcoded .enderun/ paths
|
|
458
|
+
content = content.replace(/\.enderun\//g, `${targetBase}/`);
|
|
457
459
|
|
|
458
460
|
if (ext === ".json") {
|
|
459
461
|
try {
|
|
@@ -497,6 +499,21 @@ async function initCommand(selectedAdapter) {
|
|
|
497
499
|
|
|
498
500
|
|
|
499
501
|
if (selectedAdapter === "gemini") {
|
|
502
|
+
// Patch gemini-extension.json to wire up the MCP server automatically
|
|
503
|
+
const geminiExtPath = path.join(targetDir, "gemini-extension.json");
|
|
504
|
+
try {
|
|
505
|
+
const ext = JSON.parse(fs.readFileSync(geminiExtPath, "utf8"));
|
|
506
|
+
ext.mcpServers = {
|
|
507
|
+
"agent-enderun": {
|
|
508
|
+
command: "node",
|
|
509
|
+
args: ["packages/framework-mcp/dist/index.js"]
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
fs.writeFileSync(geminiExtPath, JSON.stringify(ext, null, 2) + "\n");
|
|
513
|
+
console.log("💎 Gemini: MCP server wired up in gemini-extension.json automatically.");
|
|
514
|
+
} catch (e) {
|
|
515
|
+
console.warn("⚠️ Gemini: Could not patch gemini-extension.json for MCP. Wire it up manually.");
|
|
516
|
+
}
|
|
500
517
|
console.log(`💎 Gemini: Adapter gemini.md and ${targetBase}/ folder are ready.`);
|
|
501
518
|
}
|
|
502
519
|
|
|
@@ -525,6 +542,12 @@ async function initCommand(selectedAdapter) {
|
|
|
525
542
|
const buildCmd = pkgMgr === "npm" ? "npm run enderun:build" : `${pkgMgr} run enderun:build`;
|
|
526
543
|
|
|
527
544
|
console.log(`\n✨ Framework scaffolded! (v${FRAMEWORK_VERSION})`);
|
|
545
|
+
|
|
546
|
+
// Allow skipping install in test/CI environments
|
|
547
|
+
if (process.env.ENDERUN_SKIP_INSTALL === "1") {
|
|
548
|
+
console.log("\n⏭️ Skipping install steps (ENDERUN_SKIP_INSTALL=1).");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
528
551
|
|
|
529
552
|
try {
|
|
530
553
|
const { execSync } = await import("child_process");
|
|
@@ -724,6 +747,8 @@ function copyDir(src, dest, skipSet = new Set(), nonDestructive = false, framewo
|
|
|
724
747
|
if (textExtensions.includes(ext)) {
|
|
725
748
|
let content = fs.readFileSync(srcPath, "utf8");
|
|
726
749
|
content = content.replace(/\{\{FRAMEWORK_DIR\}\}/g, frameworkDir);
|
|
750
|
+
// Also replace any residual hardcoded .enderun/ paths left in source files
|
|
751
|
+
content = content.replace(/\.enderun\//g, `${frameworkDir}/`);
|
|
727
752
|
|
|
728
753
|
// Sanitize workspace: protocol
|
|
729
754
|
if (ext === ".json") {
|
|
@@ -745,6 +770,8 @@ function copyDir(src, dest, skipSet = new Set(), nonDestructive = false, framewo
|
|
|
745
770
|
|
|
746
771
|
content = content.replace(/\{\{FRAMEWORK_DIR\}\}/g, frameworkDir);
|
|
747
772
|
content = content.replace(/\{\{ADAPTER\}\}/g, currentAdapter);
|
|
773
|
+
// Fallback: replace any residual hardcoded .enderun/ paths
|
|
774
|
+
content = content.replace(/\.enderun\//g, `${frameworkDir}/`);
|
|
748
775
|
|
|
749
776
|
fs.writeFileSync(destPath, content);
|
|
750
777
|
} else {
|
|
@@ -819,6 +846,7 @@ function traceNewCommand(description, agent = "manager", priority = "P2") {
|
|
|
819
846
|
fs.writeFileSync(memoryPath, updated);
|
|
820
847
|
console.log(`\n✅ New Trace ID created: ${traceId}`);
|
|
821
848
|
console.log(`📝 Added to task list: ${description}\n`);
|
|
849
|
+
return traceId;
|
|
822
850
|
} finally {
|
|
823
851
|
releaseMemoryLock(lockPath);
|
|
824
852
|
}
|
|
@@ -844,7 +872,10 @@ function verifyContractCommand() {
|
|
|
844
872
|
const files = walk(sharedDir).sort();
|
|
845
873
|
const h = crypto.createHash("sha256");
|
|
846
874
|
for (const f of files) {
|
|
875
|
+
h.update(path.relative(targetDir, f));
|
|
876
|
+
h.update("\0");
|
|
847
877
|
h.update(fs.readFileSync(f));
|
|
878
|
+
h.update("\0");
|
|
848
879
|
}
|
|
849
880
|
const currentHash = h.digest("hex");
|
|
850
881
|
|
|
@@ -1093,6 +1124,593 @@ function updateProjectMemoryCommand(section, content) {
|
|
|
1093
1124
|
}
|
|
1094
1125
|
}
|
|
1095
1126
|
|
|
1127
|
+
function ensureDir(dirPath) {
|
|
1128
|
+
if (!fs.existsSync(dirPath)) {
|
|
1129
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function writeTextFile(filePath, content) {
|
|
1134
|
+
ensureDir(path.dirname(filePath));
|
|
1135
|
+
fs.writeFileSync(filePath, content.endsWith("\n") ? content : `${content}\n`);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function writeJsonFile(filePath, value) {
|
|
1139
|
+
writeTextFile(filePath, JSON.stringify(value, null, 2));
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function slugifyName(value) {
|
|
1143
|
+
const slug = String(value || "enderun-app")
|
|
1144
|
+
.toLowerCase()
|
|
1145
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1146
|
+
.replace(/^-+|-+$/g, "");
|
|
1147
|
+
return slug || "enderun-app";
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function titleCase(value) {
|
|
1151
|
+
return String(value || "Enderun App")
|
|
1152
|
+
.replace(/[-_]+/g, " ")
|
|
1153
|
+
.replace(/\s+/g, " ")
|
|
1154
|
+
.trim()
|
|
1155
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function inferAppSpec(description) {
|
|
1159
|
+
const normalized = String(description || "").toLowerCase();
|
|
1160
|
+
const isCrm = /\bcrm\b|customer|musteri|müşteri/.test(normalized);
|
|
1161
|
+
const hasAuth = /auth|login|giris|giriş|signin|sign in|user|kullanici|kullanıcı|role|rol/.test(normalized);
|
|
1162
|
+
const hasRoles = /role|rol|permission|yetki|admin/.test(normalized);
|
|
1163
|
+
const hasReports = /report|rapor|analytics|dashboard|chart|metric/.test(normalized);
|
|
1164
|
+
const appName = isCrm ? "crm-dashboard" : slugifyName(description).split("-").slice(0, 4).join("-");
|
|
1165
|
+
|
|
1166
|
+
return {
|
|
1167
|
+
rawDescription: description,
|
|
1168
|
+
appName,
|
|
1169
|
+
title: isCrm ? "CRM Dashboard" : titleCase(appName),
|
|
1170
|
+
domain: isCrm ? "CRM" : "Business",
|
|
1171
|
+
modules: {
|
|
1172
|
+
auth: hasAuth || isCrm,
|
|
1173
|
+
users: hasAuth || hasRoles || isCrm,
|
|
1174
|
+
roles: hasRoles || isCrm,
|
|
1175
|
+
reports: hasReports || isCrm,
|
|
1176
|
+
},
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function buildSharedTypesContent(existingContent) {
|
|
1181
|
+
const marker = "// --- Generated Application Contract ---";
|
|
1182
|
+
const generated = [
|
|
1183
|
+
marker,
|
|
1184
|
+
'export type RoleID = Brand<string, "RoleID">;',
|
|
1185
|
+
'export type ReportID = Brand<string, "ReportID">;',
|
|
1186
|
+
'export type CustomerID = Brand<string, "CustomerID">;',
|
|
1187
|
+
"",
|
|
1188
|
+
"export interface AuthSession {",
|
|
1189
|
+
" user: User;",
|
|
1190
|
+
" token: string;",
|
|
1191
|
+
" expiresAt: string;",
|
|
1192
|
+
"}",
|
|
1193
|
+
"",
|
|
1194
|
+
"export interface Role {",
|
|
1195
|
+
" id: RoleID;",
|
|
1196
|
+
" name: string;",
|
|
1197
|
+
" permissions: string[];",
|
|
1198
|
+
"}",
|
|
1199
|
+
"",
|
|
1200
|
+
"export interface Customer {",
|
|
1201
|
+
" id: CustomerID;",
|
|
1202
|
+
" name: string;",
|
|
1203
|
+
" ownerId: UserID;",
|
|
1204
|
+
" status: \"LEAD\" | \"ACTIVE\" | \"AT_RISK\";",
|
|
1205
|
+
" annualValue: number;",
|
|
1206
|
+
" createdAt: string;",
|
|
1207
|
+
"}",
|
|
1208
|
+
"",
|
|
1209
|
+
"export interface ReportMetric {",
|
|
1210
|
+
" id: ReportID;",
|
|
1211
|
+
" label: string;",
|
|
1212
|
+
" value: number;",
|
|
1213
|
+
" trend: \"UP\" | \"DOWN\" | \"FLAT\";",
|
|
1214
|
+
"}",
|
|
1215
|
+
"",
|
|
1216
|
+
"export interface DashboardSummary {",
|
|
1217
|
+
" customers: Customer[];",
|
|
1218
|
+
" users: User[];",
|
|
1219
|
+
" roles: Role[];",
|
|
1220
|
+
" reports: ReportMetric[];",
|
|
1221
|
+
"}",
|
|
1222
|
+
].join("\n");
|
|
1223
|
+
|
|
1224
|
+
if (existingContent.includes(marker)) return existingContent;
|
|
1225
|
+
return `${existingContent.trim()}\n\n${generated}\n`;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function updateContractHashFile() {
|
|
1229
|
+
const sharedDir = path.join(targetDir, "packages/shared-types/src");
|
|
1230
|
+
const contractPath = path.join(targetDir, "packages/shared-types/contract.version.json");
|
|
1231
|
+
if (!fs.existsSync(sharedDir) || !fs.existsSync(contractPath)) return;
|
|
1232
|
+
|
|
1233
|
+
const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
1234
|
+
const fullPath = path.join(dir, entry.name);
|
|
1235
|
+
return entry.isDirectory() ? walk(fullPath) : (entry.name.endsWith(".ts") ? [fullPath] : []);
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
const hash = crypto.createHash("sha256");
|
|
1239
|
+
for (const filePath of walk(sharedDir).sort()) {
|
|
1240
|
+
hash.update(path.relative(targetDir, filePath));
|
|
1241
|
+
hash.update("\0");
|
|
1242
|
+
hash.update(fs.readFileSync(filePath));
|
|
1243
|
+
hash.update("\0");
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
|
|
1247
|
+
contract.contract_hash = hash.digest("hex");
|
|
1248
|
+
contract.last_updated = new Date().toISOString();
|
|
1249
|
+
fs.writeFileSync(contractPath, JSON.stringify(contract, null, 2));
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function createBackendFiles(spec) {
|
|
1253
|
+
writeJsonFile(path.join(targetDir, "apps/backend/package.json"), {
|
|
1254
|
+
name: "@agent-enderun/backend",
|
|
1255
|
+
version: "0.1.0",
|
|
1256
|
+
private: true,
|
|
1257
|
+
type: "module",
|
|
1258
|
+
scripts: {
|
|
1259
|
+
dev: "tsx src/server.ts",
|
|
1260
|
+
build: "tsc -p tsconfig.json",
|
|
1261
|
+
start: "node dist/server.js",
|
|
1262
|
+
test: "vitest run",
|
|
1263
|
+
},
|
|
1264
|
+
dependencies: {
|
|
1265
|
+
"@fastify/cors": "^11.0.0",
|
|
1266
|
+
fastify: "^5.0.0",
|
|
1267
|
+
zod: "^3.24.2",
|
|
1268
|
+
},
|
|
1269
|
+
devDependencies: {
|
|
1270
|
+
"@types/node": "^22.13.4",
|
|
1271
|
+
tsx: "^4.19.4",
|
|
1272
|
+
typescript: "^5.9.3",
|
|
1273
|
+
vitest: "^3.0.5",
|
|
1274
|
+
},
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
writeJsonFile(path.join(targetDir, "apps/backend/tsconfig.json"), {
|
|
1278
|
+
extends: "../../tsconfig.json",
|
|
1279
|
+
compilerOptions: {
|
|
1280
|
+
outDir: "dist",
|
|
1281
|
+
rootDir: "src",
|
|
1282
|
+
module: "NodeNext",
|
|
1283
|
+
moduleResolution: "NodeNext",
|
|
1284
|
+
target: "ES2022",
|
|
1285
|
+
strict: true,
|
|
1286
|
+
skipLibCheck: true,
|
|
1287
|
+
},
|
|
1288
|
+
include: ["src/**/*.ts"],
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
writeTextFile(path.join(targetDir, "apps/backend/src/data.ts"), [
|
|
1292
|
+
'import type { Customer, DashboardSummary, ReportMetric, Role, User } from "../../../packages/shared-types/src/index.js";',
|
|
1293
|
+
"",
|
|
1294
|
+
'const now = new Date().toISOString();',
|
|
1295
|
+
"",
|
|
1296
|
+
"export const roles: Role[] = [",
|
|
1297
|
+
' { id: "role_admin" as Role["id"], name: "Admin", permissions: ["users:manage", "reports:view", "customers:manage"] },',
|
|
1298
|
+
' { id: "role_manager" as Role["id"], name: "Manager", permissions: ["reports:view", "customers:manage"] },',
|
|
1299
|
+
' { id: "role_viewer" as Role["id"], name: "Viewer", permissions: ["reports:view"] },',
|
|
1300
|
+
"];",
|
|
1301
|
+
"",
|
|
1302
|
+
"export const users: User[] = [",
|
|
1303
|
+
' { id: "user_1" as User["id"], email: "admin@example.com", fullName: "Admin User", role: "ADMIN", createdAt: now },',
|
|
1304
|
+
' { id: "user_2" as User["id"], email: "manager@example.com", fullName: "Sales Manager", role: "DEVELOPER", createdAt: now },',
|
|
1305
|
+
"];",
|
|
1306
|
+
"",
|
|
1307
|
+
"export const customers: Customer[] = [",
|
|
1308
|
+
' { id: "customer_1" as Customer["id"], name: "Northwind", ownerId: users[1].id, status: "ACTIVE", annualValue: 125000, createdAt: now },',
|
|
1309
|
+
' { id: "customer_2" as Customer["id"], name: "Acme Corp", ownerId: users[1].id, status: "LEAD", annualValue: 82000, createdAt: now },',
|
|
1310
|
+
' { id: "customer_3" as Customer["id"], name: "Globex", ownerId: users[0].id, status: "AT_RISK", annualValue: 54000, createdAt: now },',
|
|
1311
|
+
"];",
|
|
1312
|
+
"",
|
|
1313
|
+
"export const reports: ReportMetric[] = [",
|
|
1314
|
+
' { id: "report_pipeline" as ReportMetric["id"], label: "Pipeline", value: 261000, trend: "UP" },',
|
|
1315
|
+
' { id: "report_active_customers" as ReportMetric["id"], label: "Active Customers", value: 1, trend: "FLAT" },',
|
|
1316
|
+
' { id: "report_risk" as ReportMetric["id"], label: "At Risk", value: 1, trend: "DOWN" },',
|
|
1317
|
+
"];",
|
|
1318
|
+
"",
|
|
1319
|
+
"export function getDashboardSummary(): DashboardSummary {",
|
|
1320
|
+
" return { customers, users, roles, reports };",
|
|
1321
|
+
"}",
|
|
1322
|
+
].join("\n"));
|
|
1323
|
+
|
|
1324
|
+
writeTextFile(path.join(targetDir, "apps/backend/src/server.ts"), [
|
|
1325
|
+
'import Fastify from "fastify";',
|
|
1326
|
+
'import cors from "@fastify/cors";',
|
|
1327
|
+
'import { z } from "zod";',
|
|
1328
|
+
'import { customers, getDashboardSummary, reports, roles, users } from "./data.js";',
|
|
1329
|
+
"",
|
|
1330
|
+
"const app = Fastify({ logger: true });",
|
|
1331
|
+
"await app.register(cors, { origin: true });",
|
|
1332
|
+
"",
|
|
1333
|
+
'app.get("/health", async () => ({ ok: true, service: "agent-enderun-backend" }));',
|
|
1334
|
+
'app.get("/api/v1/dashboard", async () => ({ data: getDashboardSummary() }));',
|
|
1335
|
+
'app.get("/api/v1/users", async () => ({ data: users }));',
|
|
1336
|
+
'app.get("/api/v1/roles", async () => ({ data: roles }));',
|
|
1337
|
+
'app.get("/api/v1/customers", async () => ({ data: customers }));',
|
|
1338
|
+
'app.get("/api/v1/reports", async () => ({ data: reports }));',
|
|
1339
|
+
"",
|
|
1340
|
+
'app.post("/api/v1/auth/login", async (request, reply) => {',
|
|
1341
|
+
" const body = z.object({ email: z.string().email(), password: z.string().min(1) }).safeParse(request.body);",
|
|
1342
|
+
" if (!body.success) return reply.code(400).send({ error: { code: \"VALIDATION_ERROR\", message: \"Invalid login payload\" } });",
|
|
1343
|
+
"",
|
|
1344
|
+
" const user = users.find((item) => item.email === body.data.email) || users[0];",
|
|
1345
|
+
" return { data: { user, token: \"demo-token\", expiresAt: new Date(Date.now() + 3600000).toISOString() } };",
|
|
1346
|
+
"});",
|
|
1347
|
+
"",
|
|
1348
|
+
"const port = Number(process.env.PORT || 4000);",
|
|
1349
|
+
"await app.listen({ port, host: \"0.0.0.0\" });",
|
|
1350
|
+
].join("\n"));
|
|
1351
|
+
|
|
1352
|
+
writeTextFile(path.join(targetDir, "apps/backend/README.md"), [
|
|
1353
|
+
`# ${spec.title} Backend`,
|
|
1354
|
+
"",
|
|
1355
|
+
"Fastify API generated by Agent Enderun.",
|
|
1356
|
+
"",
|
|
1357
|
+
"## Commands",
|
|
1358
|
+
"",
|
|
1359
|
+
"- `npm run dev`",
|
|
1360
|
+
"- `npm run build`",
|
|
1361
|
+
"- `npm run test`",
|
|
1362
|
+
].join("\n"));
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function createWebFiles(spec) {
|
|
1366
|
+
writeJsonFile(path.join(targetDir, "apps/web/package.json"), {
|
|
1367
|
+
name: "@agent-enderun/web",
|
|
1368
|
+
version: "0.1.0",
|
|
1369
|
+
private: true,
|
|
1370
|
+
type: "module",
|
|
1371
|
+
scripts: {
|
|
1372
|
+
dev: "vite --host 0.0.0.0",
|
|
1373
|
+
build: "tsc -p tsconfig.json && vite build",
|
|
1374
|
+
preview: "vite preview",
|
|
1375
|
+
test: "vitest run",
|
|
1376
|
+
},
|
|
1377
|
+
dependencies: {
|
|
1378
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
1379
|
+
vite: "^7.0.0",
|
|
1380
|
+
react: "^19.0.0",
|
|
1381
|
+
"react-dom": "^19.0.0",
|
|
1382
|
+
"lucide-react": "^0.468.0",
|
|
1383
|
+
},
|
|
1384
|
+
devDependencies: {
|
|
1385
|
+
"@types/react": "^19.0.0",
|
|
1386
|
+
"@types/react-dom": "^19.0.0",
|
|
1387
|
+
typescript: "^5.9.3",
|
|
1388
|
+
vitest: "^3.0.5",
|
|
1389
|
+
},
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
writeJsonFile(path.join(targetDir, "apps/web/tsconfig.json"), {
|
|
1393
|
+
extends: "../../tsconfig.json",
|
|
1394
|
+
compilerOptions: {
|
|
1395
|
+
jsx: "react-jsx",
|
|
1396
|
+
module: "NodeNext",
|
|
1397
|
+
moduleResolution: "NodeNext",
|
|
1398
|
+
target: "ES2022",
|
|
1399
|
+
strict: true,
|
|
1400
|
+
skipLibCheck: true,
|
|
1401
|
+
},
|
|
1402
|
+
include: ["src/**/*.ts", "src/**/*.tsx"],
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
writeTextFile(path.join(targetDir, "apps/web/index.html"), [
|
|
1406
|
+
'<div id="root"></div>',
|
|
1407
|
+
'<script type="module" src="/src/main.tsx"></script>',
|
|
1408
|
+
].join("\n"));
|
|
1409
|
+
|
|
1410
|
+
writeTextFile(path.join(targetDir, "apps/web/src/main.tsx"), [
|
|
1411
|
+
'import React from "react";',
|
|
1412
|
+
'import { createRoot } from "react-dom/client";',
|
|
1413
|
+
'import { App } from "./App.js";',
|
|
1414
|
+
'import "./styles.css";',
|
|
1415
|
+
"",
|
|
1416
|
+
'createRoot(document.getElementById("root") as HTMLElement).render(',
|
|
1417
|
+
" <React.StrictMode>",
|
|
1418
|
+
" <App />",
|
|
1419
|
+
" </React.StrictMode>,",
|
|
1420
|
+
");",
|
|
1421
|
+
].join("\n"));
|
|
1422
|
+
|
|
1423
|
+
writeTextFile(path.join(targetDir, "apps/web/src/App.tsx"), [
|
|
1424
|
+
'import { BarChart3, ShieldCheck, UsersRound } from "lucide-react";',
|
|
1425
|
+
"",
|
|
1426
|
+
"const metrics = [",
|
|
1427
|
+
' { label: "Pipeline", value: "$261K", tone: "green" },',
|
|
1428
|
+
' { label: "Active customers", value: "18", tone: "blue" },',
|
|
1429
|
+
' { label: "At risk", value: "3", tone: "red" },',
|
|
1430
|
+
"];",
|
|
1431
|
+
"",
|
|
1432
|
+
"const customers = [",
|
|
1433
|
+
' { name: "Northwind", status: "Active", owner: "Sales Manager", value: "$125K" },',
|
|
1434
|
+
' { name: "Acme Corp", status: "Lead", owner: "Sales Manager", value: "$82K" },',
|
|
1435
|
+
' { name: "Globex", status: "At risk", owner: "Admin User", value: "$54K" },',
|
|
1436
|
+
"];",
|
|
1437
|
+
"",
|
|
1438
|
+
"export function App() {",
|
|
1439
|
+
" return (",
|
|
1440
|
+
' <main className="shell">',
|
|
1441
|
+
' <aside className="sidebar" aria-label="Primary navigation">',
|
|
1442
|
+
' <div className="brand">AE</div>',
|
|
1443
|
+
' <nav>',
|
|
1444
|
+
' <a className="active" href="#dashboard"><BarChart3 size={18} /> Dashboard</a>',
|
|
1445
|
+
' <a href="#users"><UsersRound size={18} /> Users</a>',
|
|
1446
|
+
' <a href="#roles"><ShieldCheck size={18} /> Roles</a>',
|
|
1447
|
+
" </nav>",
|
|
1448
|
+
" </aside>",
|
|
1449
|
+
"",
|
|
1450
|
+
' <section className="workspace">',
|
|
1451
|
+
' <header className="topbar">',
|
|
1452
|
+
" <div>",
|
|
1453
|
+
` <p>${spec.domain}</p>`,
|
|
1454
|
+
` <h1>${spec.title}</h1>`,
|
|
1455
|
+
" </div>",
|
|
1456
|
+
' <button type="button">New customer</button>',
|
|
1457
|
+
" </header>",
|
|
1458
|
+
"",
|
|
1459
|
+
' <section className="metrics" aria-label="Report metrics">',
|
|
1460
|
+
" {metrics.map((metric) => (",
|
|
1461
|
+
' <article className={`metric ${metric.tone}`} key={metric.label}>',
|
|
1462
|
+
" <span>{metric.label}</span>",
|
|
1463
|
+
" <strong>{metric.value}</strong>",
|
|
1464
|
+
" </article>",
|
|
1465
|
+
" ))}",
|
|
1466
|
+
" </section>",
|
|
1467
|
+
"",
|
|
1468
|
+
' <section className="panel">',
|
|
1469
|
+
" <div>",
|
|
1470
|
+
" <h2>Customers</h2>",
|
|
1471
|
+
" <p>Ownership, value and status at a glance.</p>",
|
|
1472
|
+
" </div>",
|
|
1473
|
+
' <div className="table">',
|
|
1474
|
+
" {customers.map((customer) => (",
|
|
1475
|
+
' <div className="row" key={customer.name}>',
|
|
1476
|
+
" <strong>{customer.name}</strong>",
|
|
1477
|
+
" <span>{customer.status}</span>",
|
|
1478
|
+
" <span>{customer.owner}</span>",
|
|
1479
|
+
" <b>{customer.value}</b>",
|
|
1480
|
+
" </div>",
|
|
1481
|
+
" ))}",
|
|
1482
|
+
" </div>",
|
|
1483
|
+
" </section>",
|
|
1484
|
+
" </section>",
|
|
1485
|
+
" </main>",
|
|
1486
|
+
" );",
|
|
1487
|
+
"}",
|
|
1488
|
+
].join("\n"));
|
|
1489
|
+
|
|
1490
|
+
writeTextFile(path.join(targetDir, "apps/web/src/styles.css"), [
|
|
1491
|
+
":root {",
|
|
1492
|
+
" color: #172026;",
|
|
1493
|
+
" background: #f4f7f6;",
|
|
1494
|
+
" font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;",
|
|
1495
|
+
"}",
|
|
1496
|
+
"",
|
|
1497
|
+
"* { box-sizing: border-box; }",
|
|
1498
|
+
"body { margin: 0; }",
|
|
1499
|
+
"button { font: inherit; }",
|
|
1500
|
+
"",
|
|
1501
|
+
".shell {",
|
|
1502
|
+
" min-height: 100vh;",
|
|
1503
|
+
" display: grid;",
|
|
1504
|
+
" grid-template-columns: 240px 1fr;",
|
|
1505
|
+
"}",
|
|
1506
|
+
"",
|
|
1507
|
+
".sidebar {",
|
|
1508
|
+
" background: #102022;",
|
|
1509
|
+
" color: #eef6f2;",
|
|
1510
|
+
" padding: 24px;",
|
|
1511
|
+
"}",
|
|
1512
|
+
"",
|
|
1513
|
+
".brand {",
|
|
1514
|
+
" width: 40px;",
|
|
1515
|
+
" height: 40px;",
|
|
1516
|
+
" display: grid;",
|
|
1517
|
+
" place-items: center;",
|
|
1518
|
+
" background: #d8f36a;",
|
|
1519
|
+
" color: #102022;",
|
|
1520
|
+
" font-weight: 800;",
|
|
1521
|
+
" border-radius: 8px;",
|
|
1522
|
+
" margin-bottom: 32px;",
|
|
1523
|
+
"}",
|
|
1524
|
+
"",
|
|
1525
|
+
"nav { display: grid; gap: 8px; }",
|
|
1526
|
+
"nav a {",
|
|
1527
|
+
" color: inherit;",
|
|
1528
|
+
" text-decoration: none;",
|
|
1529
|
+
" display: flex;",
|
|
1530
|
+
" gap: 10px;",
|
|
1531
|
+
" align-items: center;",
|
|
1532
|
+
" padding: 10px 12px;",
|
|
1533
|
+
" border-radius: 8px;",
|
|
1534
|
+
"}",
|
|
1535
|
+
"nav a.active, nav a:hover { background: rgba(255,255,255,0.12); }",
|
|
1536
|
+
"",
|
|
1537
|
+
".workspace { padding: 32px; }",
|
|
1538
|
+
".topbar {",
|
|
1539
|
+
" display: flex;",
|
|
1540
|
+
" justify-content: space-between;",
|
|
1541
|
+
" align-items: center;",
|
|
1542
|
+
" gap: 24px;",
|
|
1543
|
+
" margin-bottom: 24px;",
|
|
1544
|
+
"}",
|
|
1545
|
+
".topbar p { margin: 0 0 4px; color: #58666a; font-size: 14px; }",
|
|
1546
|
+
".topbar h1 { margin: 0; font-size: 32px; letter-spacing: 0; }",
|
|
1547
|
+
".topbar button {",
|
|
1548
|
+
" border: 0;",
|
|
1549
|
+
" border-radius: 8px;",
|
|
1550
|
+
" background: #176b5d;",
|
|
1551
|
+
" color: white;",
|
|
1552
|
+
" padding: 10px 14px;",
|
|
1553
|
+
"}",
|
|
1554
|
+
"",
|
|
1555
|
+
".metrics {",
|
|
1556
|
+
" display: grid;",
|
|
1557
|
+
" grid-template-columns: repeat(3, minmax(0, 1fr));",
|
|
1558
|
+
" gap: 16px;",
|
|
1559
|
+
" margin-bottom: 24px;",
|
|
1560
|
+
"}",
|
|
1561
|
+
".metric, .panel {",
|
|
1562
|
+
" background: white;",
|
|
1563
|
+
" border: 1px solid #d9e3e0;",
|
|
1564
|
+
" border-radius: 8px;",
|
|
1565
|
+
"}",
|
|
1566
|
+
".metric { padding: 18px; }",
|
|
1567
|
+
".metric span { display: block; color: #58666a; margin-bottom: 8px; }",
|
|
1568
|
+
".metric strong { font-size: 28px; }",
|
|
1569
|
+
".metric.green { border-top: 4px solid #49a078; }",
|
|
1570
|
+
".metric.blue { border-top: 4px solid #3f7cac; }",
|
|
1571
|
+
".metric.red { border-top: 4px solid #d95d39; }",
|
|
1572
|
+
"",
|
|
1573
|
+
".panel { padding: 20px; }",
|
|
1574
|
+
".panel h2 { margin: 0 0 4px; font-size: 20px; }",
|
|
1575
|
+
".panel p { margin: 0 0 18px; color: #58666a; }",
|
|
1576
|
+
".table { display: grid; gap: 8px; }",
|
|
1577
|
+
".row {",
|
|
1578
|
+
" display: grid;",
|
|
1579
|
+
" grid-template-columns: 1.4fr 0.8fr 1fr 0.6fr;",
|
|
1580
|
+
" gap: 16px;",
|
|
1581
|
+
" align-items: center;",
|
|
1582
|
+
" padding: 12px;",
|
|
1583
|
+
" border-radius: 8px;",
|
|
1584
|
+
" background: #f7faf9;",
|
|
1585
|
+
"}",
|
|
1586
|
+
"",
|
|
1587
|
+
"@media (max-width: 760px) {",
|
|
1588
|
+
" .shell { grid-template-columns: 1fr; }",
|
|
1589
|
+
" .sidebar { position: static; }",
|
|
1590
|
+
" .metrics { grid-template-columns: 1fr; }",
|
|
1591
|
+
" .topbar { align-items: flex-start; flex-direction: column; }",
|
|
1592
|
+
" .row { grid-template-columns: 1fr; }",
|
|
1593
|
+
"}",
|
|
1594
|
+
].join("\n"));
|
|
1595
|
+
|
|
1596
|
+
writeTextFile(path.join(targetDir, "apps/web/README.md"), [
|
|
1597
|
+
`# ${spec.title} Web`,
|
|
1598
|
+
"",
|
|
1599
|
+
"React dashboard generated by Agent Enderun.",
|
|
1600
|
+
"",
|
|
1601
|
+
"## Commands",
|
|
1602
|
+
"",
|
|
1603
|
+
"- `npm run dev`",
|
|
1604
|
+
"- `npm run build`",
|
|
1605
|
+
"- `npm run test`",
|
|
1606
|
+
].join("\n"));
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function updateProjectDocs(spec) {
|
|
1610
|
+
const frameworkDir = getFrameworkDir();
|
|
1611
|
+
const docsDir = path.join(targetDir, frameworkDir, "docs");
|
|
1612
|
+
const apiDir = path.join(docsDir, "api");
|
|
1613
|
+
ensureDir(apiDir);
|
|
1614
|
+
|
|
1615
|
+
writeTextFile(path.join(docsDir, "project-docs.md"), [
|
|
1616
|
+
`# ${spec.title} Requirements`,
|
|
1617
|
+
"",
|
|
1618
|
+
"## Request",
|
|
1619
|
+
"",
|
|
1620
|
+
spec.rawDescription,
|
|
1621
|
+
"",
|
|
1622
|
+
"## Generated Scope",
|
|
1623
|
+
"",
|
|
1624
|
+
`- Domain: ${spec.domain}`,
|
|
1625
|
+
`- Auth: ${spec.modules.auth ? "yes" : "no"}`,
|
|
1626
|
+
`- Users: ${spec.modules.users ? "yes" : "no"}`,
|
|
1627
|
+
`- Roles: ${spec.modules.roles ? "yes" : "no"}`,
|
|
1628
|
+
`- Reports: ${spec.modules.reports ? "yes" : "no"}`,
|
|
1629
|
+
"",
|
|
1630
|
+
"## Architecture",
|
|
1631
|
+
"",
|
|
1632
|
+
"- `apps/backend`: Fastify API",
|
|
1633
|
+
"- `apps/web`: React dashboard",
|
|
1634
|
+
"- `packages/shared-types`: Contract-first shared TypeScript types",
|
|
1635
|
+
].join("\n"));
|
|
1636
|
+
|
|
1637
|
+
writeTextFile(path.join(apiDir, "README.md"), [
|
|
1638
|
+
"# API Registry",
|
|
1639
|
+
"",
|
|
1640
|
+
"- `POST /api/v1/auth/login`",
|
|
1641
|
+
"- `GET /api/v1/dashboard`",
|
|
1642
|
+
"- `GET /api/v1/users`",
|
|
1643
|
+
"- `GET /api/v1/roles`",
|
|
1644
|
+
"- `GET /api/v1/customers`",
|
|
1645
|
+
"- `GET /api/v1/reports`",
|
|
1646
|
+
].join("\n"));
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function updateMemoryForGeneratedApp(spec, traceId) {
|
|
1650
|
+
const memoryPath = getMemoryPath();
|
|
1651
|
+
if (!fs.existsSync(memoryPath)) return;
|
|
1652
|
+
|
|
1653
|
+
const today = new Date().toISOString().split("T")[0];
|
|
1654
|
+
const history = [
|
|
1655
|
+
`### ${today} — Generated ${spec.title}`,
|
|
1656
|
+
"",
|
|
1657
|
+
"- **Agent:** @manager",
|
|
1658
|
+
`- **Trace ID:** ${traceId}`,
|
|
1659
|
+
"- **Action:** Created full-stack starter from natural language request.",
|
|
1660
|
+
"- **Files:** apps/backend, apps/web, shared-types, project docs",
|
|
1661
|
+
].join("\n");
|
|
1662
|
+
|
|
1663
|
+
updateProjectMemoryCommand("HISTORY", history);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
async function collectCreateAppDescription(args) {
|
|
1667
|
+
const initial = args.join(" ").trim();
|
|
1668
|
+
if (initial) return initial;
|
|
1669
|
+
|
|
1670
|
+
const readline = await import("readline/promises");
|
|
1671
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1672
|
+
try {
|
|
1673
|
+
const idea = await rl.question("What do you want to build? ");
|
|
1674
|
+
const platform = await rl.question("Platform? (full-stack/web/backend) ");
|
|
1675
|
+
const auth = await rl.question("Auth and roles? (yes/no) ");
|
|
1676
|
+
const reports = await rl.question("Reports/dashboard? (yes/no) ");
|
|
1677
|
+
return [idea, platform, auth.includes("y") ? "with auth and roles" : "", reports.includes("y") ? "with reports dashboard" : ""].filter(Boolean).join(" ");
|
|
1678
|
+
} finally {
|
|
1679
|
+
rl.close();
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
async function createAppCommand(args) {
|
|
1684
|
+
const description = await collectCreateAppDescription(args);
|
|
1685
|
+
const spec = inferAppSpec(description);
|
|
1686
|
+
const traceId = generateULID();
|
|
1687
|
+
|
|
1688
|
+
ensureDir(path.join(targetDir, "apps/backend"));
|
|
1689
|
+
ensureDir(path.join(targetDir, "apps/web"));
|
|
1690
|
+
|
|
1691
|
+
createBackendFiles(spec);
|
|
1692
|
+
createWebFiles(spec);
|
|
1693
|
+
updateProjectDocs(spec);
|
|
1694
|
+
|
|
1695
|
+
const sharedTypesPath = path.join(targetDir, "packages/shared-types/src/index.ts");
|
|
1696
|
+
if (fs.existsSync(sharedTypesPath)) {
|
|
1697
|
+
const existing = fs.readFileSync(sharedTypesPath, "utf8");
|
|
1698
|
+
fs.writeFileSync(sharedTypesPath, buildSharedTypesContent(existing));
|
|
1699
|
+
updateContractHashFile();
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
const activeTraceId = traceNewCommand(`Generate ${spec.title} from natural language request`, "manager", "P1") || traceId;
|
|
1703
|
+
updateMemoryForGeneratedApp(spec, activeTraceId);
|
|
1704
|
+
|
|
1705
|
+
console.log(`\n✅ Created ${spec.title}`);
|
|
1706
|
+
console.log("📁 Generated apps/backend and apps/web");
|
|
1707
|
+
console.log("📜 Updated project docs and shared-types contract");
|
|
1708
|
+
console.log("\nNext commands:");
|
|
1709
|
+
console.log(" npm install");
|
|
1710
|
+
console.log(" npm run enderun:build");
|
|
1711
|
+
console.log(" agent-enderun frontend:dev\n");
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1096
1714
|
// --- MAIN DISPATCHER ---
|
|
1097
1715
|
|
|
1098
1716
|
async function main() {
|
|
@@ -1117,6 +1735,12 @@ async function main() {
|
|
|
1117
1735
|
traceNewCommand(args[0], args[1], args[2]);
|
|
1118
1736
|
}
|
|
1119
1737
|
break;
|
|
1738
|
+
case "create-app":
|
|
1739
|
+
case "new":
|
|
1740
|
+
case "start":
|
|
1741
|
+
case "build-app":
|
|
1742
|
+
await createAppCommand(args);
|
|
1743
|
+
break;
|
|
1120
1744
|
case "verify-contract":
|
|
1121
1745
|
verifyContractCommand();
|
|
1122
1746
|
break;
|
|
@@ -1248,11 +1872,16 @@ async function main() {
|
|
|
1248
1872
|
console.log(`v${FRAMEWORK_VERSION}`);
|
|
1249
1873
|
break;
|
|
1250
1874
|
default:
|
|
1875
|
+
if (command && (command.includes(" ") || args.length > 0)) {
|
|
1876
|
+
await createAppCommand([command, ...args]);
|
|
1877
|
+
break;
|
|
1878
|
+
}
|
|
1251
1879
|
console.log(`
|
|
1252
1880
|
🤖 Agent Enderun CLI (v${FRAMEWORK_VERSION})
|
|
1253
1881
|
|
|
1254
1882
|
Available Commands:
|
|
1255
1883
|
init [adapter] Initialize the framework (gemini, claude, cursor, codex)
|
|
1884
|
+
create-app <idea> Generate a full-stack starter from natural language
|
|
1256
1885
|
check Full health check
|
|
1257
1886
|
check:security Run security audit scan
|
|
1258
1887
|
check:compliance Run constitution compliance check
|
|
@@ -1267,6 +1896,7 @@ Available Commands:
|
|
|
1267
1896
|
|
|
1268
1897
|
Example:
|
|
1269
1898
|
agent-enderun trace:new "Auth module design" backend P1
|
|
1899
|
+
agent-enderun create-app "CRM dashboard with auth, users, roles, reports"
|
|
1270
1900
|
`);
|
|
1271
1901
|
break;
|
|
1272
1902
|
}
|