opencroc 0.1.7 → 0.3.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/dist/cli/index.js CHANGED
@@ -21,51 +21,1234 @@ var init_esm_shims = __esm({
21
21
  // src/cli/commands/init.ts
22
22
  var init_exports = {};
23
23
  __export(init_exports, {
24
+ buildConfigContent: () => buildConfigContent,
24
25
  initProject: () => initProject
25
26
  });
26
27
  import chalk from "chalk";
27
28
  import { writeFileSync, mkdirSync, existsSync } from "fs";
28
29
  import { join } from "path";
29
- async function initProject() {
30
+ import { createInterface } from "readline/promises";
31
+ import { stdin, stdout } from "process";
32
+ function buildConfigContent(answers) {
33
+ const llmBlock = answers.llmProvider === "none" ? "" : `
34
+ llm: {
35
+ provider: '${answers.llmProvider}',${answers.llmProvider === "ollama" ? "" : "\n // apiKey: process.env.OPENCROC_LLM_API_KEY,"}
36
+ model: '${answers.llmProvider === "zhipu" ? "glm-4" : answers.llmProvider === "ollama" ? "llama3" : "gpt-4o-mini"}',
37
+ },`;
38
+ return `import { defineConfig } from 'opencroc';
39
+
40
+ export default defineConfig({
41
+ backendRoot: '${answers.backendRoot}',
42
+ adapter: '${answers.adapter}',${llmBlock}
43
+ outDir: '${answers.outDir}',
44
+ selfHealing: {
45
+ enabled: true,
46
+ maxIterations: 3,
47
+ },
48
+ });
49
+ `;
50
+ }
51
+ async function prompt(rl, question, defaultValue) {
52
+ const answer = await rl.question(` ${question} ${chalk.gray(`(${defaultValue})`)}: `);
53
+ return answer.trim() || defaultValue;
54
+ }
55
+ async function promptChoice(rl, question, choices, defaultValue) {
56
+ const list = choices.map((c) => c === defaultValue ? chalk.underline(c) : c).join(" / ");
57
+ const answer = await rl.question(` ${question} [${list}]: `);
58
+ const trimmed = answer.trim().toLowerCase();
59
+ if (!trimmed) return defaultValue;
60
+ return choices.find((c) => c.toLowerCase() === trimmed) || defaultValue;
61
+ }
62
+ async function collectAnswers() {
63
+ const rl = createInterface({ input: stdin, output: stdout });
64
+ try {
65
+ const backendRoot = await prompt(rl, "Backend source root", DEFAULTS.backendRoot);
66
+ const adapter = await promptChoice(rl, "ORM adapter", ADAPTERS, DEFAULTS.adapter);
67
+ const llmProvider = await promptChoice(rl, "LLM provider", LLM_PROVIDERS, DEFAULTS.llmProvider);
68
+ const outDir = await prompt(rl, "Test output directory", DEFAULTS.outDir);
69
+ return { backendRoot, adapter, llmProvider, outDir };
70
+ } finally {
71
+ rl.close();
72
+ }
73
+ }
74
+ function writeProject(cwd, answers) {
75
+ const configPath = join(cwd, "opencroc.config.ts");
76
+ writeFileSync(configPath, buildConfigContent(answers), "utf-8");
77
+ console.log(chalk.green(" \u2713 Created opencroc.config.ts"));
78
+ const outputDir = join(cwd, answers.outDir);
79
+ if (!existsSync(outputDir)) {
80
+ mkdirSync(outputDir, { recursive: true });
81
+ console.log(chalk.green(` \u2713 Created ${answers.outDir}/`));
82
+ }
83
+ }
84
+ function printNextSteps(answers) {
85
+ const needsKey = answers.llmProvider !== "none" && answers.llmProvider !== "ollama";
86
+ console.log("");
87
+ console.log(chalk.cyan(" Next steps:"));
88
+ let step = 1;
89
+ console.log(` ${step++}. Review opencroc.config.ts`);
90
+ if (needsKey) {
91
+ console.log(` ${step++}. Set OPENCROC_LLM_API_KEY environment variable`);
92
+ }
93
+ console.log(` ${step++}. npx opencroc generate --all`);
94
+ console.log(` ${step}. npx opencroc test`);
95
+ }
96
+ async function initProject(opts) {
30
97
  const cwd = process.cwd();
31
98
  const configPath = join(cwd, "opencroc.config.ts");
32
99
  if (existsSync(configPath)) {
33
- console.log(chalk.yellow("opencroc.config.ts already exists. Skipping."));
100
+ console.log(chalk.yellow("\n \u26A0 opencroc.config.ts already exists. Skipping.\n"));
34
101
  return;
35
102
  }
36
- writeFileSync(configPath, CONFIG_TEMPLATE, "utf-8");
37
- console.log(chalk.green("\u2713 Created opencroc.config.ts"));
38
- const outDir = join(cwd, "opencroc-output");
39
- if (!existsSync(outDir)) {
40
- mkdirSync(outDir, { recursive: true });
41
- console.log(chalk.green("\u2713 Created opencroc-output/"));
42
- }
103
+ console.log(chalk.cyan.bold("\n \u{1F40A} OpenCroc \u2014 Project Setup\n"));
104
+ const answers = opts?.yes ? { ...DEFAULTS } : await collectAnswers();
105
+ console.log("");
106
+ writeProject(cwd, answers);
107
+ printNextSteps(answers);
43
108
  console.log("");
44
- console.log(chalk.cyan("Next steps:"));
45
- console.log(" 1. Edit opencroc.config.ts with your project settings");
46
- console.log(" 2. Run: npx opencroc generate --all");
47
- console.log(" 3. Run: npx opencroc test");
48
109
  }
49
- var CONFIG_TEMPLATE;
110
+ var ADAPTERS, LLM_PROVIDERS, DEFAULTS;
50
111
  var init_init = __esm({
51
112
  "src/cli/commands/init.ts"() {
52
113
  "use strict";
53
114
  init_esm_shims();
54
- CONFIG_TEMPLATE = `import { defineConfig } from 'opencroc';
115
+ ADAPTERS = ["sequelize", "typeorm", "prisma"];
116
+ LLM_PROVIDERS = ["openai", "zhipu", "ollama", "none"];
117
+ DEFAULTS = {
118
+ backendRoot: "./backend",
119
+ adapter: "sequelize",
120
+ llmProvider: "openai",
121
+ outDir: "./opencroc-output"
122
+ };
123
+ }
124
+ });
55
125
 
56
- export default defineConfig({
57
- backendRoot: './backend',
58
- adapter: 'sequelize',
59
- llm: {
60
- provider: 'openai',
61
- model: 'gpt-4o-mini',
62
- },
63
- selfHealing: {
64
- enabled: true,
65
- maxIterations: 3,
66
- },
126
+ // src/cli/load-config.ts
127
+ import { cosmiconfig } from "cosmiconfig";
128
+ async function loadConfig(cwd) {
129
+ const explorer = cosmiconfig(MODULE_NAME, {
130
+ searchPlaces: SEARCH_PLACES,
131
+ ...cwd ? { stopDir: cwd } : {}
132
+ });
133
+ const result = cwd ? await explorer.search(cwd) : await explorer.search();
134
+ if (!result || result.isEmpty) {
135
+ throw new Error(
136
+ "No opencroc config found. Run `opencroc init` to create one."
137
+ );
138
+ }
139
+ const config = result.config?.default ?? result.config;
140
+ if (!config.backendRoot) {
141
+ throw new Error(
142
+ `Invalid config in ${result.filepath}: "backendRoot" is required.`
143
+ );
144
+ }
145
+ return { config, filepath: result.filepath };
146
+ }
147
+ var MODULE_NAME, SEARCH_PLACES;
148
+ var init_load_config = __esm({
149
+ "src/cli/load-config.ts"() {
150
+ "use strict";
151
+ init_esm_shims();
152
+ MODULE_NAME = "opencroc";
153
+ SEARCH_PLACES = [
154
+ "opencroc.config.ts",
155
+ "opencroc.config.js",
156
+ "opencroc.config.json",
157
+ ".opencrocrc.json",
158
+ "package.json"
159
+ ];
160
+ }
67
161
  });
68
- `;
162
+
163
+ // src/parsers/model-parser.ts
164
+ import * as fs from "fs";
165
+ import * as path2 from "path";
166
+ import {
167
+ Project,
168
+ SyntaxKind
169
+ } from "ts-morph";
170
+ function parseModelFile(filePath) {
171
+ const absolutePath = path2.resolve(filePath);
172
+ if (!fs.existsSync(absolutePath)) return null;
173
+ const project = new Project({ compilerOptions: { strict: false } });
174
+ const sourceFile = project.addSourceFileAtPath(absolutePath);
175
+ const initCall = findInitCall(sourceFile);
176
+ if (!initCall) return null;
177
+ const args = initCall.getArguments();
178
+ if (args.length < 2) return null;
179
+ const fields = parseFieldDefinitions(args[0]);
180
+ const { tableName, indexes } = parseOptions(args[1]);
181
+ if (!tableName) return null;
182
+ return { tableName, fields, indexes };
183
+ }
184
+ function parseModuleModels(modelDir) {
185
+ const absoluteDir = path2.resolve(modelDir);
186
+ if (!fs.existsSync(absoluteDir)) return [];
187
+ const files = fs.readdirSync(absoluteDir).filter(
188
+ (f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts") && f !== "index.ts" && f !== "associations.ts"
189
+ );
190
+ const schemas = [];
191
+ for (const file of files) {
192
+ try {
193
+ const schema = parseModelFile(path2.join(absoluteDir, file));
194
+ if (schema) schemas.push(schema);
195
+ } catch {
196
+ }
197
+ }
198
+ return schemas;
199
+ }
200
+ function findInitCall(sourceFile) {
201
+ const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
202
+ for (const call of calls) {
203
+ const expr = call.getExpression();
204
+ if (expr.getKind() === SyntaxKind.PropertyAccessExpression) {
205
+ const propAccess = expr.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
206
+ if (propAccess.getName() === "init") return call;
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+ function parseFieldDefinitions(fieldsNode) {
212
+ const fields = [];
213
+ if (fieldsNode.getKind() !== SyntaxKind.ObjectLiteralExpression) return fields;
214
+ const objLiteral = fieldsNode;
215
+ for (const prop of objLiteral.getProperties()) {
216
+ if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
217
+ const propAssign = prop;
218
+ const initializer = propAssign.getInitializer();
219
+ if (!initializer || initializer.getKind() !== SyntaxKind.ObjectLiteralExpression) continue;
220
+ fields.push(parseFieldObject(propAssign.getName(), initializer));
221
+ }
222
+ return fields;
223
+ }
224
+ function parseFieldObject(fieldName, fieldObj) {
225
+ const field = { name: fieldName, type: "STRING", allowNull: true, primaryKey: false };
226
+ for (const prop of fieldObj.getProperties()) {
227
+ if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
228
+ const propAssign = prop;
229
+ const key = propAssign.getName();
230
+ const init = propAssign.getInitializer();
231
+ if (!init) continue;
232
+ switch (key) {
233
+ case "type":
234
+ field.type = extractDataType(init);
235
+ break;
236
+ case "allowNull":
237
+ field.allowNull = init.getText().trim() === "true";
238
+ break;
239
+ case "primaryKey":
240
+ field.primaryKey = init.getText().trim() === "true";
241
+ break;
242
+ case "defaultValue":
243
+ field.defaultValue = extractDefaultValue(init);
244
+ break;
245
+ }
246
+ }
247
+ return field;
248
+ }
249
+ function extractDataType(node) {
250
+ const text = node.getText().trim();
251
+ const callMatch = text.match(/^DataTypes\.(\w+)\((.+)\)$/);
252
+ if (callMatch) return `${callMatch[1]}(${callMatch[2]})`;
253
+ const propMatch = text.match(/^DataTypes\.(\w+)$/);
254
+ if (propMatch) return propMatch[1];
255
+ return text;
256
+ }
257
+ function extractDefaultValue(node) {
258
+ const text = node.getText().trim();
259
+ if (text === "DataTypes.NOW") return "DataTypes.NOW";
260
+ if (text.startsWith("'") && text.endsWith("'") || text.startsWith('"') && text.endsWith('"'))
261
+ return text.slice(1, -1);
262
+ if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
263
+ if (text === "true") return true;
264
+ if (text === "false") return false;
265
+ if (text === "null") return null;
266
+ return text;
267
+ }
268
+ function parseOptions(optionsNode) {
269
+ let tableName = null;
270
+ let indexes = [];
271
+ if (optionsNode.getKind() !== SyntaxKind.ObjectLiteralExpression) return { tableName, indexes };
272
+ const objLiteral = optionsNode;
273
+ for (const prop of objLiteral.getProperties()) {
274
+ if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
275
+ const propAssign = prop;
276
+ const key = propAssign.getName();
277
+ const init = propAssign.getInitializer();
278
+ if (!init) continue;
279
+ if (key === "tableName") tableName = extractStringValue(init);
280
+ if (key === "indexes") indexes = parseIndexes(init);
281
+ }
282
+ return { tableName, indexes };
283
+ }
284
+ function extractStringValue(node) {
285
+ const text = node.getText().trim();
286
+ if (text.startsWith("'") && text.endsWith("'") || text.startsWith('"') && text.endsWith('"'))
287
+ return text.slice(1, -1);
288
+ return null;
289
+ }
290
+ function parseIndexes(node) {
291
+ if (node.getKind() !== SyntaxKind.ArrayLiteralExpression) return [];
292
+ const arr = node.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
293
+ const indexes = [];
294
+ for (const el of arr.getElements()) {
295
+ if (el.getKind() !== SyntaxKind.ObjectLiteralExpression) continue;
296
+ const idx = parseIndexObject(el);
297
+ if (idx) indexes.push(idx);
298
+ }
299
+ return indexes;
300
+ }
301
+ function parseIndexObject(obj) {
302
+ let name = "";
303
+ let fields = [];
304
+ let unique = false;
305
+ for (const prop of obj.getProperties()) {
306
+ if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
307
+ const pa = prop;
308
+ const init = pa.getInitializer();
309
+ if (!init) continue;
310
+ switch (pa.getName()) {
311
+ case "name":
312
+ name = extractStringValue(init) || "";
313
+ break;
314
+ case "fields":
315
+ fields = extractStringArray(init);
316
+ break;
317
+ case "unique":
318
+ unique = init.getText().trim() === "true";
319
+ break;
320
+ }
321
+ }
322
+ if (!name || fields.length === 0) return null;
323
+ return { name, fields, unique };
324
+ }
325
+ function extractStringArray(node) {
326
+ if (node.getKind() !== SyntaxKind.ArrayLiteralExpression) return [];
327
+ const arr = node.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
328
+ return arr.getElements().map((el) => el.getText().trim()).filter((t) => t.startsWith("'") || t.startsWith('"')).map((t) => t.slice(1, -1));
329
+ }
330
+ var init_model_parser = __esm({
331
+ "src/parsers/model-parser.ts"() {
332
+ "use strict";
333
+ init_esm_shims();
334
+ }
335
+ });
336
+
337
+ // src/parsers/controller-parser.ts
338
+ import * as fs2 from "fs";
339
+ import * as path3 from "path";
340
+ import {
341
+ Project as Project2,
342
+ SyntaxKind as SyntaxKind2
343
+ } from "ts-morph";
344
+ function parseControllerFile(filePath) {
345
+ const absolutePath = path3.resolve(filePath);
346
+ if (!fs2.existsSync(absolutePath)) return [];
347
+ try {
348
+ const project = new Project2({ compilerOptions: { strict: false } });
349
+ const sourceFile = project.addSourceFileAtPath(absolutePath);
350
+ const endpoints = [];
351
+ endpoints.push(...extractRouterCalls(sourceFile));
352
+ endpoints.push(...extractBaseCrudRoutes(sourceFile));
353
+ return deduplicateEndpoints(endpoints);
354
+ } catch {
355
+ return [];
356
+ }
357
+ }
358
+ function parseControllerDirectory(dirPath) {
359
+ const absoluteDir = path3.resolve(dirPath);
360
+ if (!fs2.existsSync(absoluteDir)) return [];
361
+ const files = fs2.readdirSync(absoluteDir).filter(
362
+ (f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts") && f !== "index.ts"
363
+ );
364
+ const endpoints = [];
365
+ for (const file of files) {
366
+ endpoints.push(...parseControllerFile(path3.join(absoluteDir, file)));
367
+ }
368
+ return deduplicateEndpoints(endpoints);
369
+ }
370
+ function extractRouterCalls(sourceFile) {
371
+ const endpoints = [];
372
+ const calls = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
373
+ for (const call of calls) {
374
+ const expr = call.getExpression();
375
+ if (expr.getKind() !== SyntaxKind2.PropertyAccessExpression) continue;
376
+ const propAccess = expr;
377
+ const methodName = propAccess.getName().toLowerCase();
378
+ if (!HTTP_METHODS.has(methodName)) continue;
379
+ const objectText = propAccess.getExpression().getText().trim();
380
+ if (!isRouterLike(objectText)) continue;
381
+ const args = call.getArguments();
382
+ if (args.length === 0) continue;
383
+ const routePath = resolveRoutePath(args[0], sourceFile);
384
+ if (!routePath) continue;
385
+ endpoints.push({
386
+ method: METHOD_MAP[methodName],
387
+ path: routePath,
388
+ pathParams: extractPathParams(routePath),
389
+ queryParams: [],
390
+ bodyFields: [],
391
+ responseFields: [],
392
+ relatedTables: [],
393
+ description: extractDescription(call)
394
+ });
395
+ }
396
+ return endpoints;
397
+ }
398
+ function isRouterLike(text) {
399
+ return text === "router" || text === "this.router";
400
+ }
401
+ function extractBaseCrudRoutes(sourceFile) {
402
+ const endpoints = [];
403
+ let isBaseCrud = false;
404
+ for (const cls of sourceFile.getClasses()) {
405
+ const heritage = cls.getExtends();
406
+ if (heritage?.getText().includes("BaseCrudController")) {
407
+ isBaseCrud = true;
408
+ break;
409
+ }
410
+ }
411
+ if (!isBaseCrud) return endpoints;
412
+ const calls = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
413
+ let resourcePath = null;
414
+ for (const call of calls) {
415
+ const exprText = call.getExpression().getText();
416
+ if ((exprText === "super.registerRoutes" || exprText.endsWith(".registerRoutes")) && !exprText.includes("Custom")) {
417
+ const args = call.getArguments();
418
+ if (args.length >= 2) resourcePath = extractStringLiteral(args[1]);
419
+ }
420
+ }
421
+ if (!resourcePath) return endpoints;
422
+ const basePath = `/v1/:tenantId/${resourcePath}`;
423
+ const crudRoutes = [
424
+ { method: "GET", path: basePath, desc: `List ${resourcePath}` },
425
+ { method: "GET", path: `${basePath}/:id`, desc: `Get ${resourcePath} by ID` },
426
+ { method: "POST", path: basePath, desc: `Create ${resourcePath}` },
427
+ { method: "PUT", path: `${basePath}/:id`, desc: `Update ${resourcePath}` },
428
+ { method: "DELETE", path: `${basePath}/:id`, desc: `Delete ${resourcePath}` },
429
+ { method: "POST", path: `${basePath}/batch-delete`, desc: `Batch delete ${resourcePath}` }
430
+ ];
431
+ for (const route of crudRoutes) {
432
+ endpoints.push({
433
+ method: route.method,
434
+ path: route.path,
435
+ pathParams: extractPathParams(route.path),
436
+ queryParams: [],
437
+ bodyFields: [],
438
+ responseFields: [],
439
+ relatedTables: [],
440
+ description: route.desc
441
+ });
442
+ }
443
+ return endpoints;
444
+ }
445
+ function resolveRoutePath(node, sourceFile) {
446
+ const kind = node.getKind();
447
+ if (kind === SyntaxKind2.StringLiteral) return node.getText().slice(1, -1);
448
+ if (kind === SyntaxKind2.TemplateExpression || kind === SyntaxKind2.NoSubstitutionTemplateLiteral) {
449
+ return resolveTemplateLiteral(node, sourceFile);
450
+ }
451
+ if (kind === SyntaxKind2.Identifier) {
452
+ return resolveVariableValue(sourceFile, node.getText().trim());
453
+ }
454
+ return null;
455
+ }
456
+ function resolveTemplateLiteral(node, sourceFile) {
457
+ let result = node.getText().slice(1, -1);
458
+ result = result.replace(/\$\{([^}]+)\}/g, (_match, expr) => {
459
+ const resolved = resolveVariableValue(sourceFile, expr.trim());
460
+ return resolved || `{${expr.trim()}}`;
461
+ });
462
+ return result;
463
+ }
464
+ function resolveVariableValue(sourceFile, varName) {
465
+ for (const decl of sourceFile.getDescendantsOfKind(SyntaxKind2.VariableDeclaration)) {
466
+ if (decl.getName() === varName) {
467
+ const init = decl.getInitializer();
468
+ if (!init) continue;
469
+ const t = init.getText().trim();
470
+ if (t.startsWith("'") && t.endsWith("'") || t.startsWith('"') && t.endsWith('"'))
471
+ return t.slice(1, -1);
472
+ if (t.startsWith("`") && t.endsWith("`"))
473
+ return resolveTemplateLiteral(init, sourceFile);
474
+ }
475
+ }
476
+ return null;
477
+ }
478
+ function extractPathParams(routePath) {
479
+ const params = [];
480
+ const regex = /:(\w+)/g;
481
+ let match;
482
+ while ((match = regex.exec(routePath)) !== null) params.push(match[1]);
483
+ return params;
484
+ }
485
+ function extractDescription(call) {
486
+ let current = call;
487
+ while (current.getParent() && current.getParent().getKind() !== SyntaxKind2.SourceFile && current.getParent().getKind() !== SyntaxKind2.Block) {
488
+ current = current.getParent();
489
+ }
490
+ const fullText = current.getFullText();
491
+ const leadingText = fullText.substring(0, fullText.indexOf(current.getText()));
492
+ const jsdocMatch = leadingText.match(/\/\*\*[\s\S]*?\*\s+(.+?)(?:\n|\*\/)/);
493
+ if (jsdocMatch) return jsdocMatch[1].replace(/^\*\s*/, "").trim();
494
+ const lineMatch = leadingText.match(/\/\/\s*(.+)/);
495
+ if (lineMatch) return lineMatch[1].trim();
496
+ return "";
497
+ }
498
+ function extractStringLiteral(node) {
499
+ const t = node.getText().trim();
500
+ if (t.startsWith("'") && t.endsWith("'") || t.startsWith('"') && t.endsWith('"'))
501
+ return t.slice(1, -1);
502
+ return null;
503
+ }
504
+ function deduplicateEndpoints(endpoints) {
505
+ const seen = /* @__PURE__ */ new Map();
506
+ for (const ep of endpoints) {
507
+ const key = `${ep.method}:${ep.path}`;
508
+ if (!seen.has(key)) {
509
+ seen.set(key, ep);
510
+ } else {
511
+ const existing = seen.get(key);
512
+ const merged = /* @__PURE__ */ new Set([...existing.relatedTables, ...ep.relatedTables]);
513
+ existing.relatedTables = Array.from(merged);
514
+ if (!existing.description && ep.description) existing.description = ep.description;
515
+ }
516
+ }
517
+ return Array.from(seen.values());
518
+ }
519
+ var HTTP_METHODS, METHOD_MAP;
520
+ var init_controller_parser = __esm({
521
+ "src/parsers/controller-parser.ts"() {
522
+ "use strict";
523
+ init_esm_shims();
524
+ HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "delete", "patch"]);
525
+ METHOD_MAP = {
526
+ get: "GET",
527
+ post: "POST",
528
+ put: "PUT",
529
+ delete: "DELETE",
530
+ patch: "PATCH"
531
+ };
532
+ }
533
+ });
534
+
535
+ // src/parsers/association-parser.ts
536
+ import * as fs3 from "fs";
537
+ import * as path4 from "path";
538
+ import {
539
+ Project as Project3,
540
+ SyntaxKind as SyntaxKind3
541
+ } from "ts-morph";
542
+ function parseAssociationFile(filePath, classToTableMap, moduleTablePrefix) {
543
+ const absolutePath = path4.resolve(filePath);
544
+ if (!fs3.existsSync(absolutePath)) return [];
545
+ const project = new Project3({ compilerOptions: { strict: false } });
546
+ const sourceFile = project.addSourceFileAtPath(absolutePath);
547
+ const importPathMap = collectImportPaths(sourceFile);
548
+ const rawAssociations = extractAssociationCalls(sourceFile, importPathMap);
549
+ if (rawAssociations.length === 0) return [];
550
+ return deduplicateRelations(rawAssociations, classToTableMap, moduleTablePrefix);
551
+ }
552
+ function collectImportPaths(sourceFile) {
553
+ const map = /* @__PURE__ */ new Map();
554
+ for (const decl of sourceFile.getImportDeclarations()) {
555
+ const moduleSpecifier = decl.getModuleSpecifierValue();
556
+ for (const named of decl.getNamedImports()) {
557
+ map.set(named.getName(), moduleSpecifier);
558
+ }
559
+ }
560
+ return map;
561
+ }
562
+ function extractAssociationCalls(sourceFile, importPathMap) {
563
+ const associations = [];
564
+ const calls = sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression);
565
+ for (const call of calls) {
566
+ const expr = call.getExpression();
567
+ if (expr.getKind() !== SyntaxKind3.PropertyAccessExpression) continue;
568
+ const propAccess = expr.asKindOrThrow(SyntaxKind3.PropertyAccessExpression);
569
+ const methodName = propAccess.getName();
570
+ if (methodName !== "hasMany" && methodName !== "belongsTo" && methodName !== "hasOne") continue;
571
+ const sourceClass = propAccess.getExpression().getText().trim();
572
+ const args = call.getArguments();
573
+ if (args.length < 1) continue;
574
+ const targetClass = args[0].getText().trim();
575
+ let foreignKey = "";
576
+ if (args.length >= 2 && args[1].getKind() === SyntaxKind3.ObjectLiteralExpression) {
577
+ foreignKey = extractStringProperty(args[1], "foreignKey");
578
+ }
579
+ associations.push({
580
+ sourceClass,
581
+ targetClass,
582
+ foreignKey,
583
+ type: methodName,
584
+ importPath: importPathMap.get(targetClass)
585
+ });
586
+ }
587
+ return associations;
588
+ }
589
+ function extractStringProperty(obj, propertyName) {
590
+ for (const prop of obj.getProperties()) {
591
+ if (prop.getKind() !== SyntaxKind3.PropertyAssignment) continue;
592
+ const pa = prop;
593
+ if (pa.getName() !== propertyName) continue;
594
+ const init = pa.getInitializer();
595
+ if (!init) continue;
596
+ const text = init.getText().trim();
597
+ if (text.startsWith("'") && text.endsWith("'") || text.startsWith('"') && text.endsWith('"'))
598
+ return text.slice(1, -1);
599
+ return text;
600
+ }
601
+ return "";
602
+ }
603
+ function classNameToTableName(className) {
604
+ return className.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
605
+ }
606
+ function resolveTableName(className, classToTableMap) {
607
+ if (classToTableMap?.has(className)) return classToTableMap.get(className);
608
+ return classNameToTableName(className);
609
+ }
610
+ function isCrossModuleRef(targetTableName, importPath, moduleTablePrefix) {
611
+ if (moduleTablePrefix) return !targetTableName.startsWith(moduleTablePrefix);
612
+ if (importPath) {
613
+ const upLevels = (importPath.match(/\.\.\//g) || []).length;
614
+ return upLevels >= 2;
615
+ }
616
+ return false;
617
+ }
618
+ function deduplicateRelations(rawAssociations, classToTableMap, moduleTablePrefix) {
619
+ const seen = /* @__PURE__ */ new Map();
620
+ for (const raw of rawAssociations) {
621
+ const sourceTable = resolveTableName(raw.sourceClass, classToTableMap);
622
+ const targetTable = resolveTableName(raw.targetClass, classToTableMap);
623
+ const crossModule = isCrossModuleRef(targetTable, raw.importPath, moduleTablePrefix);
624
+ let parentTable;
625
+ let childTable;
626
+ let cardinality;
627
+ switch (raw.type) {
628
+ case "hasMany":
629
+ parentTable = sourceTable;
630
+ childTable = targetTable;
631
+ cardinality = "1:N";
632
+ break;
633
+ case "belongsTo":
634
+ parentTable = targetTable;
635
+ childTable = sourceTable;
636
+ cardinality = "N:1";
637
+ break;
638
+ case "hasOne":
639
+ parentTable = sourceTable;
640
+ childTable = targetTable;
641
+ cardinality = "1:1";
642
+ break;
643
+ }
644
+ const dedupeKey = `${parentTable}|${childTable}|${raw.foreignKey}`;
645
+ if (seen.has(dedupeKey)) {
646
+ const existing = seen.get(dedupeKey);
647
+ if (existing.cardinality === "N:1" && (cardinality === "1:N" || cardinality === "1:1")) {
648
+ seen.set(dedupeKey, {
649
+ sourceTable: parentTable,
650
+ sourceField: "id",
651
+ targetTable: childTable,
652
+ targetField: raw.foreignKey,
653
+ cardinality,
654
+ isCrossModule: crossModule || existing.isCrossModule
655
+ });
656
+ }
657
+ } else {
658
+ seen.set(dedupeKey, {
659
+ sourceTable: parentTable,
660
+ sourceField: "id",
661
+ targetTable: childTable,
662
+ targetField: raw.foreignKey,
663
+ cardinality,
664
+ isCrossModule: crossModule
665
+ });
666
+ }
667
+ }
668
+ return Array.from(seen.values());
669
+ }
670
+ var init_association_parser = __esm({
671
+ "src/parsers/association-parser.ts"() {
672
+ "use strict";
673
+ init_esm_shims();
674
+ init_model_parser();
675
+ }
676
+ });
677
+
678
+ // src/analyzers/api-chain-analyzer.ts
679
+ function toNodeKey(endpoint) {
680
+ return `${endpoint.method} ${endpoint.path}`;
681
+ }
682
+ function paramToResourceHint(param) {
683
+ const stripped = param.endsWith("Id") ? param.slice(0, -2) : param;
684
+ return stripped.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
685
+ }
686
+ function postProducesResource(postEndpoint, resourceHint) {
687
+ const segments = postEndpoint.path.split("/").filter((s) => s && !s.startsWith(":"));
688
+ if (segments.length === 0) return false;
689
+ const lastSegment = segments[segments.length - 1].toLowerCase();
690
+ if (lastSegment.includes(resourceHint)) return true;
691
+ const parts = lastSegment.split("-");
692
+ if (parts.some((p) => p === resourceHint || p.startsWith(resourceHint))) return true;
693
+ if (resourceHint.length <= 4) {
694
+ const abbreviation = parts.map((p) => p[0]).join("");
695
+ if (abbreviation.startsWith(resourceHint)) return true;
696
+ }
697
+ return false;
698
+ }
699
+ function inferDependencies(endpoints) {
700
+ const dependencies = [];
701
+ const postEndpoints = endpoints.filter((ep) => ep.method === "POST");
702
+ for (const consumer of endpoints) {
703
+ const consumedParams = consumer.pathParams.filter((p) => !EXCLUDED_PARAMS.has(p));
704
+ if (consumedParams.length === 0) continue;
705
+ for (const param of consumedParams) {
706
+ if (param === "id") {
707
+ const basePath = consumer.path.replace(/\/:id(\/.*)?$/, "");
708
+ const producer2 = postEndpoints.find((ep) => ep.path === basePath);
709
+ if (producer2 && toNodeKey(producer2) !== toNodeKey(consumer)) {
710
+ dependencies.push({ from: consumer, to: producer2, paramMapping: { [`:${param}`]: "response.data.id" } });
711
+ }
712
+ continue;
713
+ }
714
+ const resourceHint = paramToResourceHint(param);
715
+ if (!resourceHint) continue;
716
+ const producer = postEndpoints.find((ep) => postProducesResource(ep, resourceHint));
717
+ if (producer && toNodeKey(producer) !== toNodeKey(consumer)) {
718
+ dependencies.push({ from: consumer, to: producer, paramMapping: { [`:${param}`]: "response.data.id" } });
719
+ }
720
+ }
721
+ }
722
+ return deduplicateDependencies(dependencies);
723
+ }
724
+ function deduplicateDependencies(deps) {
725
+ const map = /* @__PURE__ */ new Map();
726
+ for (const dep of deps) {
727
+ const key = `${toNodeKey(dep.from)}\u2192${toNodeKey(dep.to)}`;
728
+ if (map.has(key)) {
729
+ Object.assign(map.get(key).paramMapping, dep.paramMapping);
730
+ } else {
731
+ map.set(key, { ...dep, paramMapping: { ...dep.paramMapping } });
732
+ }
733
+ }
734
+ return Array.from(map.values());
735
+ }
736
+ function buildGraph(endpoints, dependencies) {
737
+ const nodeSet = /* @__PURE__ */ new Set();
738
+ for (const ep of endpoints) nodeSet.add(toNodeKey(ep));
739
+ const edges = [];
740
+ for (const dep of dependencies) {
741
+ edges.push({
742
+ from: toNodeKey(dep.from),
743
+ to: toNodeKey(dep.to),
744
+ label: Object.keys(dep.paramMapping).join(", ") || void 0
745
+ });
746
+ }
747
+ return { nodes: Array.from(nodeSet), edges };
748
+ }
749
+ function detectCycles(dag) {
750
+ const adjacency = /* @__PURE__ */ new Map();
751
+ for (const node of dag.nodes) adjacency.set(node, []);
752
+ for (const edge of dag.edges) adjacency.get(edge.from)?.push(edge.to);
753
+ const color = /* @__PURE__ */ new Map();
754
+ for (const node of dag.nodes) color.set(node, 0 /* WHITE */);
755
+ const warnings = [];
756
+ const path8 = [];
757
+ function dfs(node) {
758
+ color.set(node, 1 /* GRAY */);
759
+ path8.push(node);
760
+ for (const neighbor of adjacency.get(node) || []) {
761
+ const nc = color.get(neighbor);
762
+ if (nc === 1 /* GRAY */) {
763
+ const cycleStart = path8.indexOf(neighbor);
764
+ warnings.push(`Cycle detected: ${path8.slice(cycleStart).concat(neighbor).join(" \u2192 ")}`);
765
+ } else if (nc === 0 /* WHITE */) {
766
+ dfs(neighbor);
767
+ }
768
+ }
769
+ path8.pop();
770
+ color.set(node, 2 /* BLACK */);
771
+ }
772
+ for (const node of dag.nodes) {
773
+ if (color.get(node) === 0 /* WHITE */) dfs(node);
774
+ }
775
+ return warnings;
776
+ }
777
+ function topologicalSort(dag) {
778
+ const inDegree = /* @__PURE__ */ new Map();
779
+ const adjacency = /* @__PURE__ */ new Map();
780
+ for (const node of dag.nodes) {
781
+ inDegree.set(node, 0);
782
+ adjacency.set(node, []);
783
+ }
784
+ for (const edge of dag.edges) {
785
+ adjacency.get(edge.from)?.push(edge.to);
786
+ inDegree.set(edge.to, (inDegree.get(edge.to) || 0) + 1);
787
+ }
788
+ const queue = [];
789
+ for (const [node, degree] of inDegree) {
790
+ if (degree === 0) queue.push(node);
791
+ }
792
+ const sorted = [];
793
+ while (queue.length > 0) {
794
+ const node = queue.shift();
795
+ sorted.push(node);
796
+ for (const neighbor of adjacency.get(node) || []) {
797
+ const nd = (inDegree.get(neighbor) || 1) - 1;
798
+ inDegree.set(neighbor, nd);
799
+ if (nd === 0) queue.push(neighbor);
800
+ }
801
+ }
802
+ return sorted;
803
+ }
804
+ function createApiChainAnalyzer() {
805
+ return {
806
+ analyze(endpoints) {
807
+ const dependencies = inferDependencies(endpoints);
808
+ const dag = buildGraph(endpoints, dependencies);
809
+ const cycleWarnings = detectCycles(dag);
810
+ return {
811
+ moduleName: "",
812
+ endpoints,
813
+ dependencies,
814
+ dag,
815
+ hasCycles: cycleWarnings.length > 0,
816
+ cycleWarnings
817
+ };
818
+ }
819
+ };
820
+ }
821
+ var EXCLUDED_PARAMS;
822
+ var init_api_chain_analyzer = __esm({
823
+ "src/analyzers/api-chain-analyzer.ts"() {
824
+ "use strict";
825
+ init_esm_shims();
826
+ EXCLUDED_PARAMS = /* @__PURE__ */ new Set(["tenantId"]);
827
+ }
828
+ });
829
+
830
+ // src/generators/er-diagram-generator.ts
831
+ function toMermaidType(fieldType) {
832
+ const upper = fieldType.toUpperCase();
833
+ if (upper.startsWith("STRING")) return "string";
834
+ if (upper === "BIGINT" || upper === "INTEGER") return "bigint";
835
+ if (upper === "BOOLEAN") return "boolean";
836
+ if (upper.startsWith("DATE") || upper === "NOW") return "datetime";
837
+ if (upper === "JSON" || upper === "JSONB") return "json";
838
+ if (upper === "TEXT") return "text";
839
+ if (upper === "FLOAT" || upper === "DOUBLE" || upper === "DECIMAL") return "float";
840
+ if (upper === "UUID") return "uuid";
841
+ if (upper.startsWith("ENUM")) return "enum";
842
+ return "string";
843
+ }
844
+ function sanitizeEntityName(name) {
845
+ return name.replace(/[^a-zA-Z0-9_]/g, "_");
846
+ }
847
+ function generateMermaidER(tables, relations) {
848
+ const lines = ["erDiagram"];
849
+ for (const table of tables) {
850
+ const entityName = sanitizeEntityName(table.tableName);
851
+ lines.push(` ${entityName} {`);
852
+ for (const field of table.fields) {
853
+ const mType = toMermaidType(field.type);
854
+ const pk = field.primaryKey ? "PK" : "";
855
+ const comment = field.comment ? ` "${field.comment}"` : "";
856
+ lines.push(` ${mType} ${field.name}${pk ? " " + pk : ""}${comment}`);
857
+ }
858
+ lines.push(" }");
859
+ }
860
+ const tableNames = new Set(tables.map((t) => t.tableName));
861
+ for (const rel of relations) {
862
+ if (!tableNames.has(rel.sourceTable) || !tableNames.has(rel.targetTable)) continue;
863
+ const src = sanitizeEntityName(rel.sourceTable);
864
+ const tgt = sanitizeEntityName(rel.targetTable);
865
+ const linkStyle = rel.isCrossModule ? ".." : "--";
866
+ let cardinality;
867
+ switch (rel.cardinality) {
868
+ case "1:N":
869
+ cardinality = `||${linkStyle}o{`;
870
+ break;
871
+ case "N:1":
872
+ cardinality = `}o${linkStyle}||`;
873
+ break;
874
+ case "1:1":
875
+ cardinality = `||${linkStyle}||`;
876
+ break;
877
+ default:
878
+ cardinality = `||${linkStyle}o{`;
879
+ }
880
+ lines.push(` ${src} ${cardinality} ${tgt} : "${rel.targetField}"`);
881
+ }
882
+ return lines.join("\n");
883
+ }
884
+ function createERDiagramGenerator() {
885
+ return {
886
+ generate(tables, relations) {
887
+ const mermaidText = generateMermaidER(tables, relations);
888
+ return { tables, relations, mermaidText };
889
+ }
890
+ };
891
+ }
892
+ var init_er_diagram_generator = __esm({
893
+ "src/generators/er-diagram-generator.ts"() {
894
+ "use strict";
895
+ init_esm_shims();
896
+ }
897
+ });
898
+
899
+ // src/generators/test-code-generator.ts
900
+ function resolvePathParam(param, ids) {
901
+ if (ids.includes(param)) return `createdIds['${param}']`;
902
+ const stripped = param.endsWith("Id") ? param.slice(0, -2) : param;
903
+ if (ids.includes(stripped)) return `createdIds['${stripped}']`;
904
+ if (param === "id") return `createdIds['id']`;
905
+ return `createdIds['${param}'] || '1'`;
906
+ }
907
+ function buildUrlCode(step) {
908
+ const pathParams = step.endpoint.pathParams;
909
+ if (pathParams.length === 0) return `const url = '${step.endpoint.path}';`;
910
+ let urlTemplate = step.endpoint.path;
911
+ const replacements = [];
912
+ for (const param of pathParams) {
913
+ urlTemplate = urlTemplate.replace(`:${param}`, `\${${resolvePathParam(param, pathParams)}}`);
914
+ replacements.push(param);
915
+ }
916
+ return `const url = \`${urlTemplate}\`;`;
917
+ }
918
+ function generateAssertions(step) {
919
+ const lines = [];
920
+ if (step.assertions.length > 0) {
921
+ for (const assertion of step.assertions) {
922
+ lines.push(` expect(${assertion}).toBeTruthy();`);
923
+ }
924
+ } else {
925
+ if (step.endpoint.method === "POST") {
926
+ lines.push(" expect(response.status()).toBeLessThan(400);");
927
+ lines.push(" const body = await response.json();");
928
+ lines.push(" if (body.data?.id) createdIds['id'] = body.data.id;");
929
+ } else if (step.endpoint.method === "GET") {
930
+ lines.push(" expect(response.ok()).toBeTruthy();");
931
+ } else if (step.endpoint.method === "DELETE") {
932
+ lines.push(" expect(response.status()).toBeLessThan(400);");
933
+ } else {
934
+ lines.push(" expect(response.status()).toBeLessThan(400);");
935
+ }
936
+ }
937
+ return lines;
938
+ }
939
+ function generateTestFile(chain) {
940
+ const lines = [];
941
+ lines.push(`import { test, expect } from '@playwright/test';`);
942
+ lines.push("");
943
+ lines.push(`test.describe('${chain.name}', () => {`);
944
+ lines.push(" const createdIds: Record<string, string> = {};");
945
+ lines.push("");
946
+ for (const step of chain.steps) {
947
+ lines.push(` test('Step ${step.order}: ${step.description}', async ({ request }) => {`);
948
+ lines.push(` // ${step.action}: ${step.endpoint.method} ${step.endpoint.path}`);
949
+ lines.push(` ${buildUrlCode(step)}`);
950
+ lines.push("");
951
+ if (step.endpoint.method === "GET") {
952
+ lines.push(" const response = await request.get(url);");
953
+ } else if (step.endpoint.method === "POST") {
954
+ lines.push(" const response = await request.post(url, { data: {} });");
955
+ } else if (step.endpoint.method === "PUT") {
956
+ lines.push(" const response = await request.put(url, { data: {} });");
957
+ } else if (step.endpoint.method === "DELETE") {
958
+ lines.push(" const response = await request.delete(url);");
959
+ } else if (step.endpoint.method === "PATCH") {
960
+ lines.push(" const response = await request.patch(url, { data: {} });");
961
+ }
962
+ lines.push("");
963
+ lines.push(...generateAssertions(step));
964
+ lines.push(" });");
965
+ lines.push("");
966
+ }
967
+ lines.push("});");
968
+ return lines.join("\n");
969
+ }
970
+ function createTestCodeGenerator() {
971
+ return {
972
+ generate(chains) {
973
+ return chains.map((chain) => ({
974
+ filePath: `${chain.module}/${chain.name.replace(/\s+/g, "-").toLowerCase()}.spec.ts`,
975
+ content: generateTestFile(chain),
976
+ module: chain.module,
977
+ chain: chain.name
978
+ }));
979
+ }
980
+ };
981
+ }
982
+ var init_test_code_generator = __esm({
983
+ "src/generators/test-code-generator.ts"() {
984
+ "use strict";
985
+ init_esm_shims();
986
+ }
987
+ });
988
+
989
+ // src/validators/config-validator.ts
990
+ function validateConfig(config) {
991
+ const errors = [];
992
+ for (const field of REQUIRED_FIELDS) {
993
+ if (!config[field]) {
994
+ errors.push({
995
+ module: "config",
996
+ field,
997
+ message: `Missing required field: ${field}`,
998
+ severity: "error"
999
+ });
1000
+ }
1001
+ }
1002
+ if (config.backendRoot && typeof config.backendRoot !== "string") {
1003
+ errors.push({
1004
+ module: "config",
1005
+ field: "backendRoot",
1006
+ message: "backendRoot must be a string path",
1007
+ severity: "error"
1008
+ });
1009
+ }
1010
+ if (config.adapter && typeof config.adapter === "string") {
1011
+ if (!VALID_ADAPTERS.includes(config.adapter)) {
1012
+ errors.push({
1013
+ module: "config",
1014
+ field: "adapter",
1015
+ message: `Invalid adapter: ${config.adapter}. Must be one of: ${VALID_ADAPTERS.join(", ")}`,
1016
+ severity: "error"
1017
+ });
1018
+ }
1019
+ }
1020
+ if (config.steps && Array.isArray(config.steps)) {
1021
+ for (const step of config.steps) {
1022
+ if (!VALID_STEPS.includes(step)) {
1023
+ errors.push({
1024
+ module: "config",
1025
+ field: "steps",
1026
+ message: `Invalid pipeline step: ${step}. Must be one of: ${VALID_STEPS.join(", ")}`,
1027
+ severity: "error"
1028
+ });
1029
+ }
1030
+ }
1031
+ }
1032
+ if (config.llm && typeof config.llm === "object") {
1033
+ const llm = config.llm;
1034
+ if (llm.provider && !VALID_LLM_PROVIDERS.includes(llm.provider)) {
1035
+ errors.push({
1036
+ module: "config",
1037
+ field: "llm.provider",
1038
+ message: `Invalid LLM provider: ${llm.provider}. Must be one of: ${VALID_LLM_PROVIDERS.join(", ")}`,
1039
+ severity: "error"
1040
+ });
1041
+ }
1042
+ if (llm.provider && llm.provider !== "ollama" && !llm.apiKey) {
1043
+ errors.push({
1044
+ module: "config",
1045
+ field: "llm.apiKey",
1046
+ message: "LLM apiKey is required for cloud providers",
1047
+ severity: "warning"
1048
+ });
1049
+ }
1050
+ }
1051
+ if (config.report && typeof config.report === "object") {
1052
+ const report2 = config.report;
1053
+ if (report2.format && Array.isArray(report2.format)) {
1054
+ for (const fmt of report2.format) {
1055
+ if (!VALID_REPORT_FORMATS.includes(fmt)) {
1056
+ errors.push({
1057
+ module: "config",
1058
+ field: "report.format",
1059
+ message: `Invalid report format: ${fmt}. Must be one of: ${VALID_REPORT_FORMATS.join(", ")}`,
1060
+ severity: "error"
1061
+ });
1062
+ }
1063
+ }
1064
+ }
1065
+ }
1066
+ if (config.selfHealing && typeof config.selfHealing === "object") {
1067
+ const sh = config.selfHealing;
1068
+ if (sh.mode && !VALID_HEAL_MODES.includes(sh.mode)) {
1069
+ errors.push({
1070
+ module: "config",
1071
+ field: "selfHealing.mode",
1072
+ message: `Invalid self-healing mode: ${sh.mode}. Must be one of: ${VALID_HEAL_MODES.join(", ")}`,
1073
+ severity: "error"
1074
+ });
1075
+ }
1076
+ if (sh.maxIterations && (typeof sh.maxIterations !== "number" || sh.maxIterations < 1)) {
1077
+ errors.push({
1078
+ module: "config",
1079
+ field: "selfHealing.maxIterations",
1080
+ message: "maxIterations must be a positive number",
1081
+ severity: "error"
1082
+ });
1083
+ }
1084
+ }
1085
+ return errors;
1086
+ }
1087
+ var REQUIRED_FIELDS, VALID_ADAPTERS, VALID_STEPS, VALID_LLM_PROVIDERS, VALID_REPORT_FORMATS, VALID_HEAL_MODES;
1088
+ var init_config_validator = __esm({
1089
+ "src/validators/config-validator.ts"() {
1090
+ "use strict";
1091
+ init_esm_shims();
1092
+ REQUIRED_FIELDS = ["backendRoot"];
1093
+ VALID_ADAPTERS = ["sequelize", "typeorm", "prisma"];
1094
+ VALID_STEPS = ["scan", "er-diagram", "api-chain", "plan", "codegen", "validate"];
1095
+ VALID_LLM_PROVIDERS = ["openai", "zhipu", "ollama", "custom"];
1096
+ VALID_REPORT_FORMATS = ["html", "json", "markdown"];
1097
+ VALID_HEAL_MODES = ["config-only", "config-and-source"];
1098
+ }
1099
+ });
1100
+
1101
+ // src/pipeline/index.ts
1102
+ import * as fs4 from "fs";
1103
+ import * as path5 from "path";
1104
+ function createPipeline(config) {
1105
+ return {
1106
+ async run(steps) {
1107
+ const startTime = Date.now();
1108
+ const activeSteps = steps || config.steps || ALL_STEPS;
1109
+ const result = {
1110
+ modules: [],
1111
+ erDiagrams: /* @__PURE__ */ new Map(),
1112
+ chainPlans: /* @__PURE__ */ new Map(),
1113
+ generatedFiles: [],
1114
+ validationErrors: [],
1115
+ duration: 0
1116
+ };
1117
+ if (activeSteps.includes("scan")) {
1118
+ const backendRoot = path5.resolve(config.backendRoot);
1119
+ const modelsDir = path5.join(backendRoot, "models");
1120
+ if (fs4.existsSync(modelsDir)) {
1121
+ const dirs = fs4.readdirSync(modelsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1122
+ const moduleFilter = config.modules;
1123
+ for (const dir of dirs) {
1124
+ if (moduleFilter && !moduleFilter.includes(dir)) continue;
1125
+ result.modules.push(dir);
1126
+ }
1127
+ if (result.modules.length === 0) {
1128
+ result.modules.push("default");
1129
+ } else {
1130
+ const rootFiles = fs4.readdirSync(modelsDir).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && f !== "index.ts");
1131
+ if (rootFiles.length > 0) {
1132
+ result.modules.unshift("default");
1133
+ }
1134
+ }
1135
+ }
1136
+ }
1137
+ const resolveModelDir = (backendRoot, mod) => mod === "default" ? path5.join(backendRoot, "models") : path5.join(backendRoot, "models", mod);
1138
+ const resolveControllerDir = (backendRoot, mod) => mod === "default" ? path5.join(backendRoot, "controllers") : path5.join(backendRoot, "controllers", mod);
1139
+ if (activeSteps.includes("er-diagram")) {
1140
+ const erGen = createERDiagramGenerator();
1141
+ const backendRoot = path5.resolve(config.backendRoot);
1142
+ for (const mod of result.modules) {
1143
+ const modelDir = resolveModelDir(backendRoot, mod);
1144
+ const tables = fs4.existsSync(modelDir) ? parseModuleModels(modelDir) : [];
1145
+ const relations = [];
1146
+ const assocFile = path5.join(modelDir, "associations.ts");
1147
+ if (fs4.existsSync(assocFile)) {
1148
+ relations.push(...parseAssociationFile(assocFile));
1149
+ }
1150
+ if (fs4.existsSync(modelDir)) {
1151
+ const modelFiles = fs4.readdirSync(modelDir).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && f !== "index.ts" && f !== "associations.ts");
1152
+ for (const file of modelFiles) {
1153
+ try {
1154
+ const embedded = parseAssociationFile(path5.join(modelDir, file));
1155
+ relations.push(...embedded);
1156
+ } catch {
1157
+ }
1158
+ }
1159
+ }
1160
+ const erResult = erGen.generate(tables, relations);
1161
+ result.erDiagrams.set(mod, erResult);
1162
+ }
1163
+ }
1164
+ if (activeSteps.includes("api-chain")) {
1165
+ const chainAnalyzer = createApiChainAnalyzer();
1166
+ const backendRoot = path5.resolve(config.backendRoot);
1167
+ for (const mod of result.modules) {
1168
+ const controllerDir = resolveControllerDir(backendRoot, mod);
1169
+ const endpoints = fs4.existsSync(controllerDir) ? parseControllerDirectory(controllerDir) : [];
1170
+ const analysis = chainAnalyzer.analyze(endpoints);
1171
+ analysis.moduleName = mod;
1172
+ if (analysis.hasCycles) {
1173
+ for (const warning of analysis.cycleWarnings) {
1174
+ result.validationErrors.push({
1175
+ module: mod,
1176
+ field: "api-chain",
1177
+ message: warning,
1178
+ severity: "warning"
1179
+ });
1180
+ }
1181
+ }
1182
+ }
1183
+ }
1184
+ if (activeSteps.includes("plan")) {
1185
+ const backendRoot = path5.resolve(config.backendRoot);
1186
+ const chainAnalyzer = createApiChainAnalyzer();
1187
+ for (const mod of result.modules) {
1188
+ const controllerDir = resolveControllerDir(backendRoot, mod);
1189
+ const endpoints = fs4.existsSync(controllerDir) ? parseControllerDirectory(controllerDir) : [];
1190
+ const analysis = chainAnalyzer.analyze(endpoints);
1191
+ const topoOrder = topologicalSort(analysis.dag);
1192
+ const chains = generateChainPlan(mod, endpoints, topoOrder);
1193
+ result.chainPlans.set(mod, chains);
1194
+ }
1195
+ }
1196
+ if (activeSteps.includes("codegen")) {
1197
+ const testGen = createTestCodeGenerator();
1198
+ const outDir = config.outDir || "./opencroc-output";
1199
+ for (const [_mod, plan] of result.chainPlans) {
1200
+ const files = testGen.generate(plan.chains);
1201
+ for (const file of files) {
1202
+ file.filePath = path5.join(outDir, file.filePath);
1203
+ }
1204
+ result.generatedFiles.push(...files);
1205
+ }
1206
+ }
1207
+ if (activeSteps.includes("validate")) {
1208
+ const configErrors = validateConfig(config);
1209
+ result.validationErrors.push(...configErrors);
1210
+ }
1211
+ result.duration = Date.now() - startTime;
1212
+ return result;
1213
+ }
1214
+ };
1215
+ }
1216
+ function generateChainPlan(moduleName, endpoints, _topoOrder) {
1217
+ const groups = /* @__PURE__ */ new Map();
1218
+ for (const ep of endpoints) {
1219
+ const segments = ep.path.split("/").filter((s) => s && !s.startsWith(":"));
1220
+ const resource = segments[segments.length - 1] || "default";
1221
+ if (!groups.has(resource)) groups.set(resource, []);
1222
+ groups.get(resource).push(ep);
1223
+ }
1224
+ const chains = [];
1225
+ let totalSteps = 0;
1226
+ for (const [resource, eps] of groups) {
1227
+ const steps = eps.map((ep, i) => ({
1228
+ order: i + 1,
1229
+ action: ep.method,
1230
+ endpoint: ep,
1231
+ description: ep.description || `${ep.method} ${ep.path}`,
1232
+ assertions: []
1233
+ }));
1234
+ chains.push({ name: `${resource} CRUD chain`, module: moduleName, steps });
1235
+ totalSteps += steps.length;
1236
+ }
1237
+ return { chains, totalSteps };
1238
+ }
1239
+ var ALL_STEPS;
1240
+ var init_pipeline = __esm({
1241
+ "src/pipeline/index.ts"() {
1242
+ "use strict";
1243
+ init_esm_shims();
1244
+ init_model_parser();
1245
+ init_controller_parser();
1246
+ init_association_parser();
1247
+ init_api_chain_analyzer();
1248
+ init_er_diagram_generator();
1249
+ init_test_code_generator();
1250
+ init_config_validator();
1251
+ ALL_STEPS = ["scan", "er-diagram", "api-chain", "plan", "codegen", "validate"];
69
1252
  }
70
1253
  });
71
1254
 
@@ -75,15 +1258,82 @@ __export(generate_exports, {
75
1258
  generate: () => generate
76
1259
  });
77
1260
  import chalk2 from "chalk";
1261
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync6 } from "fs";
1262
+ import { dirname } from "path";
1263
+ function parseSteps(raw) {
1264
+ if (!raw) return void 0;
1265
+ const names = raw.split(",").map((s) => s.trim());
1266
+ for (const name of names) {
1267
+ if (!VALID_STEPS2.includes(name)) {
1268
+ throw new Error(`Unknown pipeline step "${name}". Valid steps: ${VALID_STEPS2.join(", ")}`);
1269
+ }
1270
+ }
1271
+ return names;
1272
+ }
1273
+ function writeGeneratedFiles(result) {
1274
+ let written = 0;
1275
+ for (const file of result.generatedFiles) {
1276
+ const dir = dirname(file.filePath);
1277
+ if (!existsSync6(dir)) {
1278
+ mkdirSync2(dir, { recursive: true });
1279
+ }
1280
+ writeFileSync2(file.filePath, file.content, "utf-8");
1281
+ written++;
1282
+ console.log(chalk2.green(` \u2713 ${file.filePath}`));
1283
+ }
1284
+ return written;
1285
+ }
1286
+ function printSummary(result, dryRun) {
1287
+ console.log("");
1288
+ console.log(chalk2.cyan.bold(" Summary"));
1289
+ console.log(` Modules discovered : ${result.modules.length}`);
1290
+ console.log(` ER diagrams : ${result.erDiagrams.size}`);
1291
+ console.log(` Chain plans : ${result.chainPlans.size}`);
1292
+ console.log(` Generated files : ${result.generatedFiles.length}${dryRun ? " (dry-run, not written)" : ""}`);
1293
+ if (result.validationErrors.length > 0) {
1294
+ const errors = result.validationErrors.filter((e) => e.severity === "error");
1295
+ const warnings = result.validationErrors.filter((e) => e.severity === "warning");
1296
+ if (errors.length > 0) console.log(chalk2.red(` Errors : ${errors.length}`));
1297
+ if (warnings.length > 0) console.log(chalk2.yellow(` Warnings : ${warnings.length}`));
1298
+ for (const err of result.validationErrors) {
1299
+ const icon = err.severity === "error" ? chalk2.red("\u2717") : chalk2.yellow("\u26A0");
1300
+ console.log(` ${icon} [${err.module}] ${err.message}`);
1301
+ }
1302
+ }
1303
+ console.log(chalk2.gray(` Duration : ${result.duration}ms`));
1304
+ console.log("");
1305
+ }
78
1306
  async function generate(opts) {
79
- console.log(chalk2.cyan("\u{1F40A} OpenCroc \u2014 Generating E2E tests...\n"));
80
- console.log(chalk2.yellow("Generation pipeline is under development."));
81
- console.log("Options:", opts);
1307
+ console.log(chalk2.cyan.bold("\n \u{1F40A} OpenCroc \u2014 Generate E2E Tests\n"));
1308
+ const { config, filepath } = await loadConfig();
1309
+ console.log(chalk2.gray(` Config: ${filepath}`));
1310
+ if (opts.module) {
1311
+ config.modules = [opts.module];
1312
+ }
1313
+ const steps = parseSteps(opts.steps);
1314
+ const pipeline = createPipeline(config);
1315
+ const result = await pipeline.run(steps);
1316
+ if (!opts.dryRun && result.generatedFiles.length > 0) {
1317
+ console.log("");
1318
+ console.log(chalk2.cyan(" Generated files:"));
1319
+ writeGeneratedFiles(result);
1320
+ } else if (opts.dryRun && result.generatedFiles.length > 0) {
1321
+ console.log("");
1322
+ console.log(chalk2.yellow(" Dry-run \u2014 files that would be generated:"));
1323
+ for (const file of result.generatedFiles) {
1324
+ console.log(chalk2.gray(` ${file.filePath}`));
1325
+ }
1326
+ }
1327
+ printSummary(result, !!opts.dryRun);
82
1328
  }
1329
+ var VALID_STEPS2;
83
1330
  var init_generate = __esm({
84
1331
  "src/cli/commands/generate.ts"() {
85
1332
  "use strict";
86
1333
  init_esm_shims();
1334
+ init_load_config();
1335
+ init_pipeline();
1336
+ VALID_STEPS2 = ["scan", "er-diagram", "api-chain", "plan", "codegen", "validate"];
87
1337
  }
88
1338
  });
89
1339
 
@@ -93,15 +1343,62 @@ __export(test_exports, {
93
1343
  runTests: () => runTests
94
1344
  });
95
1345
  import chalk3 from "chalk";
1346
+ import { readdirSync as readdirSync5, existsSync as existsSync7 } from "fs";
1347
+ import { join as join6, resolve as resolve5 } from "path";
1348
+ import { execFileSync } from "child_process";
1349
+ function discoverTestFiles(outDir, moduleFilter) {
1350
+ const absDir = resolve5(outDir);
1351
+ if (!existsSync7(absDir)) return [];
1352
+ const files = [];
1353
+ const entries = readdirSync5(absDir, { withFileTypes: true, recursive: true });
1354
+ for (const entry of entries) {
1355
+ if (!entry.isFile()) continue;
1356
+ if (!entry.name.endsWith(".spec.ts") && !entry.name.endsWith(".test.ts")) continue;
1357
+ const fullPath = join6(entry.parentPath || entry.path || absDir, entry.name);
1358
+ if (moduleFilter && !fullPath.includes(moduleFilter)) continue;
1359
+ files.push(fullPath);
1360
+ }
1361
+ return files;
1362
+ }
96
1363
  async function runTests(opts) {
97
- console.log(chalk3.cyan("\u{1F40A} OpenCroc \u2014 Running E2E tests...\n"));
98
- console.log(chalk3.yellow("Test runner is under development."));
99
- console.log("Options:", opts);
1364
+ console.log(chalk3.cyan.bold("\n \u{1F40A} OpenCroc \u2014 Run E2E Tests\n"));
1365
+ const { config, filepath } = await loadConfig();
1366
+ console.log(chalk3.gray(` Config: ${filepath}`));
1367
+ const outDir = config.outDir || "./opencroc-output";
1368
+ const testFiles = discoverTestFiles(outDir, opts.module);
1369
+ if (testFiles.length === 0) {
1370
+ console.log(chalk3.yellow(" No test files found. Run `opencroc generate` first.\n"));
1371
+ return;
1372
+ }
1373
+ console.log(` Found ${testFiles.length} test file(s)`);
1374
+ for (const f of testFiles) {
1375
+ console.log(chalk3.gray(` ${f}`));
1376
+ }
1377
+ console.log("");
1378
+ const args = ["test", ...testFiles];
1379
+ if (!opts.headed) {
1380
+ args.push("--reporter=list");
1381
+ } else {
1382
+ args.push("--headed");
1383
+ }
1384
+ const npxCmd = process.platform === "win32" ? "npx.cmd" : "npx";
1385
+ try {
1386
+ console.log(chalk3.cyan(" Running Playwright...\n"));
1387
+ execFileSync(npxCmd, ["playwright", ...args], {
1388
+ stdio: "inherit",
1389
+ cwd: process.cwd()
1390
+ });
1391
+ console.log(chalk3.green("\n \u2713 All tests passed.\n"));
1392
+ } catch {
1393
+ console.log(chalk3.red("\n \u2717 Some tests failed.\n"));
1394
+ process.exitCode = 1;
1395
+ }
100
1396
  }
101
1397
  var init_test = __esm({
102
1398
  "src/cli/commands/test.ts"() {
103
1399
  "use strict";
104
1400
  init_esm_shims();
1401
+ init_load_config();
105
1402
  }
106
1403
  });
107
1404
 
@@ -111,15 +1408,128 @@ __export(validate_exports, {
111
1408
  validate: () => validate
112
1409
  });
113
1410
  import chalk4 from "chalk";
1411
+ function printErrors(errors) {
1412
+ for (const err of errors) {
1413
+ const icon = err.severity === "error" ? chalk4.red("\u2717") : chalk4.yellow("\u26A0");
1414
+ const scope = err.module === "config" ? "" : ` [${err.module}]`;
1415
+ console.log(` ${icon}${scope} ${err.field}: ${err.message}`);
1416
+ }
1417
+ }
114
1418
  async function validate(opts) {
115
- console.log(chalk4.cyan("\u{1F40A} OpenCroc \u2014 Validating configurations...\n"));
116
- console.log(chalk4.yellow("Validation pipeline is under development."));
117
- console.log("Options:", opts);
1419
+ console.log(chalk4.cyan.bold("\n \u{1F40A} OpenCroc \u2014 Validate\n"));
1420
+ const { config, filepath } = await loadConfig();
1421
+ console.log(chalk4.gray(` Config: ${filepath}`));
1422
+ const configErrors = validateConfig(config);
1423
+ if (opts.module) {
1424
+ config.modules = [opts.module];
1425
+ }
1426
+ const pipeline = createPipeline(config);
1427
+ const result = await pipeline.run(["scan", "validate"]);
1428
+ const allErrors = [...configErrors, ...result.validationErrors];
1429
+ const errors = allErrors.filter((e) => e.severity === "error");
1430
+ const warnings = allErrors.filter((e) => e.severity === "warning");
1431
+ if (allErrors.length === 0) {
1432
+ console.log(chalk4.green(" \u2713 Configuration is valid."));
1433
+ console.log(chalk4.gray(` Modules: ${result.modules.join(", ") || "(none)"}
1434
+ `));
1435
+ return;
1436
+ }
1437
+ if (errors.length > 0) {
1438
+ console.log(chalk4.red(` ${errors.length} error(s):`));
1439
+ printErrors(errors);
1440
+ }
1441
+ if (warnings.length > 0) {
1442
+ console.log(chalk4.yellow(` ${warnings.length} warning(s):`));
1443
+ printErrors(warnings);
1444
+ }
1445
+ console.log("");
1446
+ if (errors.length > 0) {
1447
+ process.exitCode = 1;
1448
+ }
118
1449
  }
119
1450
  var init_validate = __esm({
120
1451
  "src/cli/commands/validate.ts"() {
121
1452
  "use strict";
122
1453
  init_esm_shims();
1454
+ init_load_config();
1455
+ init_config_validator();
1456
+ init_pipeline();
1457
+ }
1458
+ });
1459
+
1460
+ // src/llm/openai.ts
1461
+ var init_openai = __esm({
1462
+ "src/llm/openai.ts"() {
1463
+ "use strict";
1464
+ init_esm_shims();
1465
+ }
1466
+ });
1467
+
1468
+ // src/llm/ollama.ts
1469
+ var init_ollama = __esm({
1470
+ "src/llm/ollama.ts"() {
1471
+ "use strict";
1472
+ init_esm_shims();
1473
+ }
1474
+ });
1475
+
1476
+ // src/llm/index.ts
1477
+ var init_llm = __esm({
1478
+ "src/llm/index.ts"() {
1479
+ "use strict";
1480
+ init_esm_shims();
1481
+ init_openai();
1482
+ init_ollama();
1483
+ init_openai();
1484
+ init_ollama();
1485
+ }
1486
+ });
1487
+
1488
+ // src/self-healing/index.ts
1489
+ async function attemptConfigFix(_testResultsDir, _mode, _llm) {
1490
+ return {
1491
+ success: false,
1492
+ scope: "config-only",
1493
+ fixedItems: [],
1494
+ rolledBack: false
1495
+ };
1496
+ }
1497
+ function createSelfHealingLoop(config, llm) {
1498
+ return {
1499
+ async run(testResultsDir) {
1500
+ const maxIterations = config.maxIterations || 3;
1501
+ const mode = config.mode || "config-only";
1502
+ const fixed = [];
1503
+ const remaining = [];
1504
+ let iterations = 0;
1505
+ let totalTokensUsed = 0;
1506
+ for (let i = 0; i < maxIterations; i++) {
1507
+ iterations = i + 1;
1508
+ const outcome = await attemptConfigFix(testResultsDir, mode, llm);
1509
+ if (outcome.success) {
1510
+ fixed.push(...outcome.fixedItems);
1511
+ } else {
1512
+ remaining.push(`iteration-${i + 1}: no fix applied`);
1513
+ }
1514
+ if (llm) {
1515
+ totalTokensUsed += llm.estimateTokens(`iteration-${i + 1}`);
1516
+ }
1517
+ if (outcome.success && outcome.fixedItems.length > 0) break;
1518
+ }
1519
+ return {
1520
+ iterations,
1521
+ fixed,
1522
+ remaining,
1523
+ totalTokensUsed
1524
+ };
1525
+ }
1526
+ };
1527
+ }
1528
+ var init_self_healing = __esm({
1529
+ "src/self-healing/index.ts"() {
1530
+ "use strict";
1531
+ init_esm_shims();
1532
+ init_llm();
123
1533
  }
124
1534
  });
125
1535
 
@@ -130,14 +1540,490 @@ __export(heal_exports, {
130
1540
  });
131
1541
  import chalk5 from "chalk";
132
1542
  async function heal(opts) {
133
- console.log(chalk5.cyan("\u{1F40A} OpenCroc \u2014 Running self-healing loop...\n"));
134
- console.log(chalk5.yellow("Self-healing loop is under development."));
135
- console.log("Options:", opts);
1543
+ console.log(chalk5.cyan.bold("\n \u{1F40A} OpenCroc \u2014 Self-Healing\n"));
1544
+ const { config, filepath } = await loadConfig();
1545
+ console.log(chalk5.gray(` Config: ${filepath}`));
1546
+ const outDir = config.outDir || "./opencroc-output";
1547
+ const maxIterations = opts.maxIterations ? parseInt(opts.maxIterations, 10) : 3;
1548
+ const healingConfig = {
1549
+ enabled: true,
1550
+ maxIterations,
1551
+ mode: config.selfHealing?.mode || "config-only"
1552
+ };
1553
+ console.log(chalk5.gray(` Mode: ${healingConfig.mode}`));
1554
+ console.log(chalk5.gray(` Max iterations: ${maxIterations}`));
1555
+ if (opts.module) {
1556
+ console.log(chalk5.gray(` Module: ${opts.module}`));
1557
+ }
1558
+ console.log("");
1559
+ const loop = createSelfHealingLoop(healingConfig);
1560
+ const result = await loop.run(outDir);
1561
+ console.log(chalk5.cyan(" Results:"));
1562
+ console.log(` Iterations : ${result.iterations}`);
1563
+ console.log(` Fixed : ${result.fixed.length > 0 ? chalk5.green(result.fixed.join(", ")) : chalk5.gray("(none)")}`);
1564
+ console.log(` Remaining : ${result.remaining.length > 0 ? chalk5.yellow(result.remaining.join(", ")) : chalk5.gray("(none)")}`);
1565
+ if (result.totalTokensUsed > 0) {
1566
+ console.log(` Tokens used : ${result.totalTokensUsed}`);
1567
+ }
1568
+ console.log("");
1569
+ if (result.remaining.length > 0) {
1570
+ console.log(chalk5.yellow(" Some issues could not be auto-fixed. Manual review needed.\n"));
1571
+ } else if (result.fixed.length > 0) {
1572
+ console.log(chalk5.green(" \u2713 All issues resolved.\n"));
1573
+ } else {
1574
+ console.log(chalk5.gray(" No issues detected.\n"));
1575
+ }
136
1576
  }
137
1577
  var init_heal = __esm({
138
1578
  "src/cli/commands/heal.ts"() {
139
1579
  "use strict";
140
1580
  init_esm_shims();
1581
+ init_load_config();
1582
+ init_self_healing();
1583
+ }
1584
+ });
1585
+
1586
+ // src/ci/index.ts
1587
+ function generateGitHubActionsTemplate(opts = {}) {
1588
+ const nodeVersions = opts.nodeVersions ?? ["20.x"];
1589
+ const install = opts.installCommand ?? "npm ci";
1590
+ const genArgs = opts.generateArgs ?? "--all";
1591
+ const testArgs = opts.testArgs ?? "";
1592
+ const healStep = opts.selfHeal ? `
1593
+ - name: Self-heal failures
1594
+ if: failure()
1595
+ run: npx opencroc heal --max-iterations 3` : "";
1596
+ const matrix = nodeVersions.length > 1 ? `
1597
+ strategy:
1598
+ matrix:
1599
+ node-version: [${nodeVersions.join(", ")}]` : "";
1600
+ const nodeSetup = nodeVersions.length > 1 ? "${{ matrix.node-version }}" : nodeVersions[0];
1601
+ return `# Generated by OpenCroc \u2014 AI-native E2E testing
1602
+ # https://github.com/opencroc/opencroc
1603
+
1604
+ name: OpenCroc E2E
1605
+
1606
+ on:
1607
+ push:
1608
+ branches: [main]
1609
+ pull_request:
1610
+ branches: [main]
1611
+
1612
+ jobs:
1613
+ e2e:
1614
+ runs-on: ubuntu-latest${matrix}
1615
+ steps:
1616
+ - uses: actions/checkout@v4
1617
+
1618
+ - uses: actions/setup-node@v4
1619
+ with:
1620
+ node-version: '${nodeSetup}'
1621
+
1622
+ - name: Install dependencies
1623
+ run: ${install}
1624
+
1625
+ - name: Install Playwright browsers
1626
+ run: npx playwright install --with-deps chromium
1627
+
1628
+ - name: Generate E2E tests
1629
+ run: npx opencroc generate ${genArgs}
1630
+
1631
+ - name: Run E2E tests
1632
+ run: npx opencroc test ${testArgs}
1633
+ ${healStep}
1634
+ - name: Upload test report
1635
+ if: always()
1636
+ uses: actions/upload-artifact@v4
1637
+ with:
1638
+ name: opencroc-report
1639
+ path: opencroc-output/
1640
+ retention-days: 14
1641
+ `;
1642
+ }
1643
+ function generateGitLabCITemplate(opts = {}) {
1644
+ const install = opts.installCommand ?? "npm ci";
1645
+ const genArgs = opts.generateArgs ?? "--all";
1646
+ const testArgs = opts.testArgs ?? "";
1647
+ const nodeVersion = opts.nodeVersions?.[0] ?? "20";
1648
+ return `# Generated by OpenCroc \u2014 AI-native E2E testing
1649
+ # https://github.com/opencroc/opencroc
1650
+
1651
+ image: node:${nodeVersion}
1652
+
1653
+ stages:
1654
+ - generate
1655
+ - test
1656
+
1657
+ variables:
1658
+ PLAYWRIGHT_BROWSERS_PATH: \${CI_PROJECT_DIR}/.cache/ms-playwright
1659
+
1660
+ cache:
1661
+ key: \${CI_COMMIT_REF_SLUG}
1662
+ paths:
1663
+ - node_modules/
1664
+ - .cache/ms-playwright/
1665
+
1666
+ generate:
1667
+ stage: generate
1668
+ script:
1669
+ - ${install}
1670
+ - npx opencroc generate ${genArgs}
1671
+ artifacts:
1672
+ paths:
1673
+ - opencroc-output/
1674
+ expire_in: 1 day
1675
+
1676
+ e2e:
1677
+ stage: test
1678
+ needs: [generate]
1679
+ before_script:
1680
+ - ${install}
1681
+ - npx playwright install --with-deps chromium
1682
+ script:
1683
+ - npx opencroc test ${testArgs}
1684
+ artifacts:
1685
+ when: always
1686
+ paths:
1687
+ - opencroc-output/
1688
+ expire_in: 14 days
1689
+ `;
1690
+ }
1691
+ function listCiPlatforms() {
1692
+ return Object.keys(TEMPLATES);
1693
+ }
1694
+ function generateCiTemplate(platform, opts = {}) {
1695
+ const generator = TEMPLATES[platform];
1696
+ if (!generator) {
1697
+ throw new Error(
1698
+ `Unknown CI platform: "${platform}". Available: ${Object.keys(TEMPLATES).join(", ")}`
1699
+ );
1700
+ }
1701
+ return generator(opts);
1702
+ }
1703
+ var TEMPLATES;
1704
+ var init_ci = __esm({
1705
+ "src/ci/index.ts"() {
1706
+ "use strict";
1707
+ init_esm_shims();
1708
+ TEMPLATES = {
1709
+ github: generateGitHubActionsTemplate,
1710
+ gitlab: generateGitLabCITemplate
1711
+ };
1712
+ }
1713
+ });
1714
+
1715
+ // src/cli/commands/ci.ts
1716
+ var ci_exports = {};
1717
+ __export(ci_exports, {
1718
+ ci: () => ci
1719
+ });
1720
+ import * as fs5 from "fs";
1721
+ import * as path6 from "path";
1722
+ import chalk6 from "chalk";
1723
+ async function ci(opts) {
1724
+ const platform = opts.platform ?? "github";
1725
+ const available = listCiPlatforms();
1726
+ if (!available.includes(platform)) {
1727
+ console.error(chalk6.red(`Unknown platform: "${platform}". Available: ${available.join(", ")}`));
1728
+ process.exitCode = 1;
1729
+ return;
1730
+ }
1731
+ const templateOpts = {
1732
+ selfHeal: opts.selfHeal ?? false
1733
+ };
1734
+ if (opts.node) {
1735
+ templateOpts.nodeVersions = opts.node.split(",").map((s) => s.trim());
1736
+ }
1737
+ const content = generateCiTemplate(platform, templateOpts);
1738
+ let outputPath;
1739
+ if (platform === "github") {
1740
+ outputPath = path6.join(".github", "workflows", "opencroc.yml");
1741
+ } else if (platform === "gitlab") {
1742
+ outputPath = ".gitlab-ci.yml";
1743
+ } else {
1744
+ outputPath = `opencroc-ci-${platform}.yml`;
1745
+ }
1746
+ const dir = path6.dirname(outputPath);
1747
+ if (dir !== "." && !fs5.existsSync(dir)) {
1748
+ fs5.mkdirSync(dir, { recursive: true });
1749
+ }
1750
+ fs5.writeFileSync(outputPath, content, "utf-8");
1751
+ console.log(chalk6.green(`\u2714 CI template written to ${outputPath}`));
1752
+ console.log(chalk6.dim(` Platform: ${platform}`));
1753
+ }
1754
+ var init_ci2 = __esm({
1755
+ "src/cli/commands/ci.ts"() {
1756
+ "use strict";
1757
+ init_esm_shims();
1758
+ init_ci();
1759
+ }
1760
+ });
1761
+
1762
+ // src/reporters/index.ts
1763
+ function generateJsonReport(result) {
1764
+ const serializable = {
1765
+ modules: result.modules,
1766
+ erDiagrams: Object.fromEntries(
1767
+ Array.from(result.erDiagrams.entries()).map(([k, v]) => [
1768
+ k,
1769
+ { tables: v.tables.length, relations: v.relations.length, mermaidText: v.mermaidText }
1770
+ ])
1771
+ ),
1772
+ chainPlans: Object.fromEntries(
1773
+ Array.from(result.chainPlans.entries()).map(([k, v]) => [
1774
+ k,
1775
+ { chains: v.chains.length, totalSteps: v.totalSteps }
1776
+ ])
1777
+ ),
1778
+ generatedFiles: result.generatedFiles.map((f) => ({
1779
+ filePath: f.filePath,
1780
+ module: f.module,
1781
+ chain: f.chain
1782
+ })),
1783
+ validationErrors: result.validationErrors,
1784
+ duration: result.duration
1785
+ };
1786
+ return {
1787
+ format: "json",
1788
+ content: JSON.stringify(serializable, null, 2),
1789
+ filename: "opencroc-report.json"
1790
+ };
1791
+ }
1792
+ function generateMarkdownReport(result) {
1793
+ const lines = [
1794
+ "# OpenCroc Report",
1795
+ "",
1796
+ `**Duration**: ${result.duration}ms`,
1797
+ `**Modules**: ${result.modules.length} (${result.modules.join(", ")})`,
1798
+ "",
1799
+ "## ER Diagrams",
1800
+ ""
1801
+ ];
1802
+ for (const [mod, er] of result.erDiagrams) {
1803
+ lines.push(`### ${mod}`);
1804
+ lines.push(`- Tables: ${er.tables.length}`);
1805
+ lines.push(`- Relations: ${er.relations.length}`);
1806
+ lines.push("");
1807
+ }
1808
+ lines.push("## Chain Plans", "");
1809
+ for (const [mod, plan] of result.chainPlans) {
1810
+ lines.push(`### ${mod}`);
1811
+ lines.push(`- Chains: ${plan.chains.length}`);
1812
+ lines.push(`- Total Steps: ${plan.totalSteps}`);
1813
+ lines.push("");
1814
+ }
1815
+ lines.push(`## Generated Files (${result.generatedFiles.length})`, "");
1816
+ for (const f of result.generatedFiles) {
1817
+ lines.push(`- \`${f.filePath}\` (${f.module} / ${f.chain})`);
1818
+ }
1819
+ if (result.validationErrors.length > 0) {
1820
+ lines.push("", "## Validation Issues", "");
1821
+ const errors = result.validationErrors.filter((e) => e.severity === "error");
1822
+ const warnings = result.validationErrors.filter((e) => e.severity === "warning");
1823
+ if (errors.length > 0) {
1824
+ lines.push(`### Errors (${errors.length})`, "");
1825
+ for (const e of errors) {
1826
+ lines.push(`- **[${e.module}]** ${e.field}: ${e.message}`);
1827
+ }
1828
+ }
1829
+ if (warnings.length > 0) {
1830
+ lines.push(`### Warnings (${warnings.length})`, "");
1831
+ for (const w of warnings) {
1832
+ lines.push(`- **[${w.module}]** ${w.field}: ${w.message}`);
1833
+ }
1834
+ }
1835
+ }
1836
+ lines.push("", "---", "*Generated by [OpenCroc](https://github.com/opencroc/opencroc)*");
1837
+ return {
1838
+ format: "markdown",
1839
+ content: lines.join("\n"),
1840
+ filename: "opencroc-report.md"
1841
+ };
1842
+ }
1843
+ function escapeHtml(s) {
1844
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1845
+ }
1846
+ function erSummaryRows(erDiagrams) {
1847
+ const rows = [];
1848
+ for (const [mod, er] of erDiagrams) {
1849
+ rows.push(`<tr><td>${escapeHtml(mod)}</td><td>${er.tables.length}</td><td>${er.relations.length}</td></tr>`);
1850
+ }
1851
+ return rows.join("\n");
1852
+ }
1853
+ function chainSummaryRows(chainPlans) {
1854
+ const rows = [];
1855
+ for (const [mod, plan] of chainPlans) {
1856
+ rows.push(`<tr><td>${escapeHtml(mod)}</td><td>${plan.chains.length}</td><td>${plan.totalSteps}</td></tr>`);
1857
+ }
1858
+ return rows.join("\n");
1859
+ }
1860
+ function fileListRows(files) {
1861
+ return files.map((f) => `<tr><td><code>${escapeHtml(f.filePath)}</code></td><td>${escapeHtml(f.module)}</td><td>${escapeHtml(f.chain)}</td></tr>`).join("\n");
1862
+ }
1863
+ function validationRows(errors) {
1864
+ return errors.map(
1865
+ (e) => `<tr class="${e.severity}"><td><span class="badge ${e.severity}">${e.severity}</span></td><td>${escapeHtml(e.module)}</td><td>${escapeHtml(e.field)}</td><td>${escapeHtml(e.message)}</td></tr>`
1866
+ ).join("\n");
1867
+ }
1868
+ function generateHtmlReport(result) {
1869
+ const totalTables = Array.from(result.erDiagrams.values()).reduce((s, e) => s + e.tables.length, 0);
1870
+ const totalRelations = Array.from(result.erDiagrams.values()).reduce((s, e) => s + e.relations.length, 0);
1871
+ const totalChains = Array.from(result.chainPlans.values()).reduce((s, p) => s + p.chains.length, 0);
1872
+ const totalSteps = Array.from(result.chainPlans.values()).reduce((s, p) => s + p.totalSteps, 0);
1873
+ const errorCount = result.validationErrors.filter((e) => e.severity === "error").length;
1874
+ const warnCount = result.validationErrors.filter((e) => e.severity === "warning").length;
1875
+ const html = `<!DOCTYPE html>
1876
+ <html lang="en">
1877
+ <head>
1878
+ <meta charset="utf-8" />
1879
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1880
+ <title>OpenCroc Report</title>
1881
+ <style>
1882
+ :root { --bg: #0d1117; --fg: #c9d1d9; --card: #161b22; --border: #30363d; --accent: #58a6ff; --green: #3fb950; --yellow: #d29922; --red: #f85149; }
1883
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1884
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--fg); padding: 2rem; }
1885
+ h1 { color: var(--accent); margin-bottom: 0.25rem; }
1886
+ .subtitle { color: #8b949e; margin-bottom: 2rem; }
1887
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
1888
+ .card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; }
1889
+ .card .label { font-size: 0.85rem; color: #8b949e; }
1890
+ .card .value { font-size: 2rem; font-weight: 700; color: var(--accent); }
1891
+ .card .value.green { color: var(--green); }
1892
+ .card .value.yellow { color: var(--yellow); }
1893
+ .card .value.red { color: var(--red); }
1894
+ section { margin-bottom: 2rem; }
1895
+ h2 { color: var(--fg); border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; margin-bottom: 1rem; }
1896
+ table { width: 100%; border-collapse: collapse; background: var(--card); border-radius: 8px; overflow: hidden; }
1897
+ th, td { text-align: left; padding: 0.6rem 1rem; border-bottom: 1px solid var(--border); }
1898
+ th { background: #21262d; color: #8b949e; font-weight: 600; font-size: 0.85rem; text-transform: uppercase; }
1899
+ tr:last-child td { border-bottom: none; }
1900
+ code { background: #21262d; padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.85rem; }
1901
+ .badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
1902
+ .badge.error { background: rgba(248,81,73,0.15); color: var(--red); }
1903
+ .badge.warning { background: rgba(210,153,34,0.15); color: var(--yellow); }
1904
+ footer { margin-top: 3rem; text-align: center; color: #484f58; font-size: 0.85rem; }
1905
+ footer a { color: var(--accent); text-decoration: none; }
1906
+ </style>
1907
+ </head>
1908
+ <body>
1909
+ <h1>OpenCroc Report</h1>
1910
+ <p class="subtitle">Generated in ${result.duration}ms &middot; ${result.modules.length} module(s)</p>
1911
+
1912
+ <div class="grid">
1913
+ <div class="card"><div class="label">Modules</div><div class="value">${result.modules.length}</div></div>
1914
+ <div class="card"><div class="label">Tables</div><div class="value">${totalTables}</div></div>
1915
+ <div class="card"><div class="label">Relations</div><div class="value">${totalRelations}</div></div>
1916
+ <div class="card"><div class="label">Chains</div><div class="value">${totalChains}</div></div>
1917
+ <div class="card"><div class="label">Steps</div><div class="value">${totalSteps}</div></div>
1918
+ <div class="card"><div class="label">Files</div><div class="value green">${result.generatedFiles.length}</div></div>
1919
+ <div class="card"><div class="label">Errors</div><div class="value${errorCount > 0 ? " red" : ""}">${errorCount}</div></div>
1920
+ <div class="card"><div class="label">Warnings</div><div class="value${warnCount > 0 ? " yellow" : ""}">${warnCount}</div></div>
1921
+ </div>
1922
+
1923
+ <section>
1924
+ <h2>ER Diagrams</h2>
1925
+ <table>
1926
+ <thead><tr><th>Module</th><th>Tables</th><th>Relations</th></tr></thead>
1927
+ <tbody>${erSummaryRows(result.erDiagrams)}</tbody>
1928
+ </table>
1929
+ </section>
1930
+
1931
+ <section>
1932
+ <h2>Chain Plans</h2>
1933
+ <table>
1934
+ <thead><tr><th>Module</th><th>Chains</th><th>Steps</th></tr></thead>
1935
+ <tbody>${chainSummaryRows(result.chainPlans)}</tbody>
1936
+ </table>
1937
+ </section>
1938
+
1939
+ <section>
1940
+ <h2>Generated Files (${result.generatedFiles.length})</h2>
1941
+ <table>
1942
+ <thead><tr><th>File</th><th>Module</th><th>Chain</th></tr></thead>
1943
+ <tbody>${fileListRows(result.generatedFiles)}</tbody>
1944
+ </table>
1945
+ </section>
1946
+
1947
+ ${result.validationErrors.length > 0 ? `<section>
1948
+ <h2>Validation Issues (${result.validationErrors.length})</h2>
1949
+ <table>
1950
+ <thead><tr><th>Severity</th><th>Module</th><th>Field</th><th>Message</th></tr></thead>
1951
+ <tbody>${validationRows(result.validationErrors)}</tbody>
1952
+ </table>
1953
+ </section>` : ""}
1954
+
1955
+ <footer>
1956
+ Generated by <a href="https://github.com/opencroc/opencroc">OpenCroc</a>
1957
+ </footer>
1958
+ </body>
1959
+ </html>`;
1960
+ return {
1961
+ format: "html",
1962
+ content: html,
1963
+ filename: "opencroc-report.html"
1964
+ };
1965
+ }
1966
+ function generateReports(result, formats = ["html"]) {
1967
+ return formats.map((fmt) => {
1968
+ const gen = REPORTERS[fmt];
1969
+ if (!gen) throw new Error(`Unknown report format: "${fmt}". Available: ${Object.keys(REPORTERS).join(", ")}`);
1970
+ return gen(result);
1971
+ });
1972
+ }
1973
+ var REPORTERS;
1974
+ var init_reporters = __esm({
1975
+ "src/reporters/index.ts"() {
1976
+ "use strict";
1977
+ init_esm_shims();
1978
+ REPORTERS = {
1979
+ html: generateHtmlReport,
1980
+ json: generateJsonReport,
1981
+ markdown: generateMarkdownReport
1982
+ };
1983
+ }
1984
+ });
1985
+
1986
+ // src/cli/commands/report.ts
1987
+ var report_exports = {};
1988
+ __export(report_exports, {
1989
+ report: () => report
1990
+ });
1991
+ import * as fs6 from "fs";
1992
+ import * as path7 from "path";
1993
+ import chalk7 from "chalk";
1994
+ async function report(opts) {
1995
+ let loaded;
1996
+ try {
1997
+ loaded = await loadConfig();
1998
+ } catch {
1999
+ console.error(chalk7.red("No opencroc config found. Run `opencroc init` first."));
2000
+ process.exitCode = 1;
2001
+ return;
2002
+ }
2003
+ const { config } = loaded;
2004
+ console.log(chalk7.cyan("Running pipeline to generate report..."));
2005
+ const pipeline = createPipeline(config);
2006
+ const result = await pipeline.run();
2007
+ const formats = (opts.format ?? "html").split(",").map((s) => s.trim());
2008
+ const reports = generateReports(result, formats);
2009
+ const outDir = opts.output ?? config.outDir ?? "./opencroc-output";
2010
+ if (!fs6.existsSync(outDir)) {
2011
+ fs6.mkdirSync(outDir, { recursive: true });
2012
+ }
2013
+ for (const r of reports) {
2014
+ const filePath = path7.join(outDir, r.filename);
2015
+ fs6.writeFileSync(filePath, r.content, "utf-8");
2016
+ console.log(chalk7.green(`\u2714 ${r.format} report \u2192 ${filePath}`));
2017
+ }
2018
+ console.log(chalk7.dim(` ${result.modules.length} modules, ${result.generatedFiles.length} files, ${result.duration}ms`));
2019
+ }
2020
+ var init_report = __esm({
2021
+ "src/cli/commands/report.ts"() {
2022
+ "use strict";
2023
+ init_esm_shims();
2024
+ init_load_config();
2025
+ init_pipeline();
2026
+ init_reporters();
141
2027
  }
142
2028
  });
143
2029
 
@@ -145,10 +2031,10 @@ var init_heal = __esm({
145
2031
  init_esm_shims();
146
2032
  import { Command } from "commander";
147
2033
  var program = new Command();
148
- program.name("opencroc").description("AI-native E2E testing framework").version("0.1.0");
149
- program.command("init").description("Initialize OpenCroc in the current project").action(async () => {
2034
+ program.name("opencroc").description("AI-native E2E testing framework").version("0.3.1");
2035
+ program.command("init").description("Initialize OpenCroc in the current project").option("-y, --yes", "Skip prompts and use defaults").action(async (opts) => {
150
2036
  const { initProject: initProject2 } = await Promise.resolve().then(() => (init_init(), init_exports));
151
- await initProject2();
2037
+ await initProject2(opts);
152
2038
  });
153
2039
  program.command("generate").description("Generate E2E test cases from source code").option("-m, --module <name>", "Generate for a specific module").option("-a, --all", "Generate for all discovered modules").option("--steps <steps>", "Run specific pipeline steps (comma-separated)").option("--dry-run", "Preview without writing files").action(async (opts) => {
154
2040
  const { generate: generate2 } = await Promise.resolve().then(() => (init_generate(), generate_exports));
@@ -166,5 +2052,13 @@ program.command("heal").description("Run self-healing loop on failed tests").opt
166
2052
  const { heal: heal2 } = await Promise.resolve().then(() => (init_heal(), heal_exports));
167
2053
  await heal2(opts);
168
2054
  });
2055
+ program.command("ci").description("Generate CI/CD pipeline template").option("-p, --platform <name>", "CI platform (github, gitlab)", "github").option("--self-heal", "Include self-healing step").option("--node <versions>", "Node.js versions (comma-separated)", "20.x").action(async (opts) => {
2056
+ const { ci: ci2 } = await Promise.resolve().then(() => (init_ci2(), ci_exports));
2057
+ await ci2(opts);
2058
+ });
2059
+ program.command("report").description("Generate pipeline report (HTML/JSON/Markdown)").option("-f, --format <formats>", "Report formats (comma-separated)", "html").option("-o, --output <dir>", "Output directory").action(async (opts) => {
2060
+ const { report: report2 } = await Promise.resolve().then(() => (init_report(), report_exports));
2061
+ await report2(opts);
2062
+ });
169
2063
  program.parse();
170
2064
  //# sourceMappingURL=index.js.map