larats 1.0.1 → 1.1.0

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/index.js CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  // src/index.ts
4
4
  import fs3 from "fs";
5
- import path3 from "path";
5
+ import path4 from "path";
6
+ import os2 from "os";
7
+ import { exec } from "child_process";
8
+ import { parseArgs } from "util";
6
9
  import * as p from "@clack/prompts";
7
10
  import pc from "picocolors";
8
11
 
@@ -12,7 +15,7 @@ import path from "path";
12
15
  function readJsonSafe(filePath) {
13
16
  try {
14
17
  if (!fs.existsSync(filePath)) return null;
15
- const content = fs.readFileSync(filePath, "utf-8");
18
+ const content = fs.readFileSync(filePath, "utf-8").replace(/^\uFEFF/, "");
16
19
  return JSON.parse(content);
17
20
  } catch {
18
21
  return null;
@@ -39,11 +42,13 @@ async function detectProjectType(targetDir = process.cwd()) {
39
42
  const hasInertia = "inertiajs/inertia-laravel" in allDeps;
40
43
  const hasSanctum = "laravel/sanctum" in allDeps;
41
44
  const rawVersion = allDeps["laravel/framework"] || "";
42
- const cleanVersion = rawVersion.replace(/[\^~><=]/g, "").trim();
45
+ const versionMatch = rawVersion.match(/\d+(?:\.\d+)*/);
46
+ const cleanVersion = versionMatch ? versionMatch[0] : "";
43
47
  const appModulesDir = path.join(resolvedDir, "app", "Modules");
44
48
  const rootModulesDir = path.join(resolvedDir, "modules");
45
- const isModular = fs.existsSync(appModulesDir) || fs.existsSync(rootModulesDir);
46
- const modulesDir = fs.existsSync(appModulesDir) ? "app/Modules" : fs.existsSync(rootModulesDir) ? "modules" : void 0;
49
+ const rootModulesCapDir = path.join(resolvedDir, "Modules");
50
+ const isModular = fs.existsSync(appModulesDir) || fs.existsSync(rootModulesDir) || fs.existsSync(rootModulesCapDir);
51
+ const modulesDir = fs.existsSync(appModulesDir) ? "app/Modules" : fs.existsSync(rootModulesDir) ? "modules" : fs.existsSync(rootModulesCapDir) ? "Modules" : void 0;
47
52
  const extras = [];
48
53
  if (hasFilament) extras.push("Filament");
49
54
  if (hasLivewire) extras.push("Livewire");
@@ -66,7 +71,7 @@ async function detectProjectType(targetDir = process.cwd()) {
66
71
  summary
67
72
  };
68
73
  }
69
- if (hasTsConfig && hasPackageJson) {
74
+ if (hasPackageJson) {
70
75
  const pkg = readJsonSafe(packageJsonPath);
71
76
  const allDeps = {
72
77
  ...pkg?.dependencies || {},
@@ -88,13 +93,15 @@ async function detectProjectType(targetDir = process.cwd()) {
88
93
  const hasSrc = fs.existsSync(path.join(resolvedDir, "src"));
89
94
  const srcDir = hasSrc ? "src" : ".";
90
95
  const isModular = fs.existsSync(path.join(resolvedDir, srcDir, "modules")) || fs.existsSync(path.join(resolvedDir, "modules"));
91
- const extras = [framework !== "node-vanilla" ? framework : "TypeScript"];
96
+ const isTypeScript = hasTsConfig || "typescript" in allDeps;
97
+ const stackLabel = isTypeScript ? "TypeScript" : "JavaScript / Node.js";
98
+ const extras = [framework !== "node-vanilla" ? framework : stackLabel];
92
99
  if (hasPrisma) extras.push("Prisma");
93
100
  if (hasTypeORM) extras.push("TypeORM");
94
101
  if (hasDrizzle) extras.push("Drizzle");
95
102
  if (isMonorepo) extras.push("Monorepo");
96
103
  if (isModular) extras.push("Modular");
97
- const summary = `TypeScript (${extras.join(", ")})`;
104
+ const summary = isTypeScript ? `TypeScript (${extras.join(", ")})` : `JavaScript / Node.js (${extras.join(", ")})`;
98
105
  return {
99
106
  type: "typescript",
100
107
  name: pkg?.name || dirName,
@@ -106,7 +113,8 @@ async function detectProjectType(targetDir = process.cwd()) {
106
113
  hasDrizzle,
107
114
  isMonorepo,
108
115
  isModular,
109
- srcDir
116
+ srcDir,
117
+ isTypeScript
110
118
  },
111
119
  summary
112
120
  };
@@ -125,6 +133,8 @@ async function detectProjectType(targetDir = process.cwd()) {
125
133
  }
126
134
 
127
135
  // src/scanner.ts
136
+ import path2 from "path";
137
+ import os from "os";
128
138
  import fg from "fast-glob";
129
139
  var SAFE_IGNORE_LIST = [
130
140
  // Dependencies & package management
@@ -136,32 +146,42 @@ var SAFE_IGNORE_LIST = [
136
146
  "**/.git/**",
137
147
  "**/.svn/**",
138
148
  "**/.hg/**",
139
- // Storage, cache & build outputs
149
+ // Storage, cache & build outputs (scoped to root to preserve src/build/)
140
150
  "**/storage/**",
141
151
  "**/dist/**",
142
- "**/build/**",
143
- "**/out/**",
152
+ "build/**",
153
+ "out/**",
144
154
  "**/.next/**",
145
155
  "**/.nuxt/**",
146
156
  "**/.turbo/**",
147
157
  "**/.cache/**",
148
158
  "**/coverage/**",
149
159
  "**/.temp/**",
150
- "**/tmp/**",
160
+ "tmp/**",
151
161
  // IDEs & OS files
152
162
  "**/.idea/**",
153
163
  "**/.vscode/**",
154
164
  "**/.DS_Store",
155
165
  "**/Thumbs.db",
156
- // Secrets & environment configurations
166
+ // Secrets, private keys & cloud credentials
157
167
  "**/.env*",
158
168
  "**/*.pem",
159
169
  "**/*.key",
160
170
  "**/*.pfx",
171
+ "**/*.p12",
172
+ "**/*.jks",
173
+ "**/*.keystore",
161
174
  "**/*.cert",
162
175
  "**/*.crt",
163
176
  "**/*.secret",
164
- "**/*.id_rsa",
177
+ "**/secrets/**",
178
+ "**/.aws/**",
179
+ "**/.ssh/**",
180
+ "**/credentials*.json",
181
+ "**/*service-account*.json",
182
+ "**/id_rsa*",
183
+ "**/id_ed25519*",
184
+ "**/id_ecdsa*",
165
185
  "**/*.pub",
166
186
  // Logs & Lock files
167
187
  "**/*.log",
@@ -171,6 +191,9 @@ var SAFE_IGNORE_LIST = [
171
191
  "**/pnpm-lock.yaml",
172
192
  "**/composer.lock"
173
193
  ];
194
+ function localeIndependentCompare(a, b) {
195
+ return a < b ? -1 : a > b ? 1 : 0;
196
+ }
174
197
  function buildCleanDirectoryTree(files, maxDepth = 3) {
175
198
  const root = { name: "", isDir: true, children: /* @__PURE__ */ new Map() };
176
199
  for (const file of files) {
@@ -194,7 +217,7 @@ function buildCleanDirectoryTree(files, maxDepth = 3) {
194
217
  if (currentDepth > maxDepth) return;
195
218
  const entries = Array.from(node.children.values()).sort((a, b) => {
196
219
  if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
197
- return a.name.localeCompare(b.name);
220
+ return localeIndependentCompare(a.name, b.name);
198
221
  });
199
222
  if (currentDepth === maxDepth && entries.length > 0) {
200
223
  lines.push(`${prefix}\u2514\u2500\u2500 ... (${entries.length} items hidden)`);
@@ -219,7 +242,7 @@ function buildCleanDirectoryTree(files, maxDepth = 3) {
219
242
  }
220
243
  const topLevel = Array.from(root.children.values()).sort((a, b) => {
221
244
  if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
222
- return a.name.localeCompare(b.name);
245
+ return localeIndependentCompare(a.name, b.name);
223
246
  });
224
247
  topLevel.forEach((child, index) => {
225
248
  const isLast = index === topLevel.length - 1;
@@ -236,52 +259,76 @@ function buildCleanDirectoryTree(files, maxDepth = 3) {
236
259
  async function scanProject(projectInfo) {
237
260
  const startTime = Date.now();
238
261
  const root = projectInfo.rootPath;
262
+ const resolvedRoot = path2.resolve(root);
263
+ const parsedRoot = path2.parse(resolvedRoot);
264
+ if (parsedRoot.root === resolvedRoot || typeof os.homedir === "function" && resolvedRoot === os.homedir()) {
265
+ throw new Error(`Refusing to scan system drive or home directory root directly: "${resolvedRoot}".`);
266
+ }
239
267
  const allFiles = await fg("**/*", {
240
268
  cwd: root,
241
269
  ignore: SAFE_IGNORE_LIST,
242
270
  dot: false,
243
- onlyFiles: true
271
+ onlyFiles: true,
272
+ followSymbolicLinks: false,
273
+ suppressErrors: true
244
274
  });
245
- const normalizedFiles = allFiles.map((f) => f.replace(/\\/g, "/"));
275
+ const MAX_FILES_CAP = 1e4;
276
+ let normalizedFiles = allFiles.map((f) => f.replace(/\\/g, "/"));
277
+ if (normalizedFiles.length > MAX_FILES_CAP) {
278
+ console.warn(`[WARN] Large codebase detected (${normalizedFiles.length} files). Capping scan analysis to first ${MAX_FILES_CAP} files.`);
279
+ normalizedFiles = normalizedFiles.slice(0, MAX_FILES_CAP);
280
+ }
246
281
  const components = {};
247
282
  const modulesSet = /* @__PURE__ */ new Set();
248
283
  if (projectInfo.type === "laravel") {
249
- const models = normalizedFiles.filter((f) => /^app\/(Models\/.*|.*Model)\.php$/i.test(f));
284
+ const models = normalizedFiles.filter(
285
+ (f) => /^(?:app|modules|Modules)\/(?:.*\/)?Models\/.*\.php$/i.test(f)
286
+ );
250
287
  components["Models"] = {
251
288
  name: "Eloquent Models",
252
289
  count: models.length,
253
290
  files: models,
254
291
  description: "Database entity mappings and business logic models"
255
292
  };
256
- const controllers = normalizedFiles.filter((f) => /^app\/Http\/Controllers\/.*\.php$/i.test(f));
293
+ const controllers = normalizedFiles.filter(
294
+ (f) => /^(?:app|modules|Modules)\/(?:.*\/)?Controllers\/.*\.php$/i.test(f)
295
+ );
257
296
  components["Controllers"] = {
258
297
  name: "HTTP Controllers",
259
298
  count: controllers.length,
260
299
  files: controllers,
261
300
  description: "Request handling and response orchestration"
262
301
  };
263
- const services = normalizedFiles.filter((f) => /^app\/(Services|Domain\/.*\/Services)\/.*\.php$/i.test(f));
302
+ const services = normalizedFiles.filter(
303
+ (f) => /^(?:app|modules|Modules)\/(?:.*\/)?Services\/.*\.php$/i.test(f)
304
+ );
264
305
  components["Services"] = {
265
306
  name: "Service Classes",
266
307
  count: services.length,
267
308
  files: services,
268
309
  description: "Encapsulated business logic and external integrations"
269
310
  };
270
- const actions = normalizedFiles.filter((f) => /^app\/(Actions|Domain\/.*\/Actions)\/.*\.php$/i.test(f));
311
+ const actions = normalizedFiles.filter(
312
+ (f) => /^(?:app|modules|Modules)\/(?:.*\/)?Actions\/.*\.php$/i.test(f)
313
+ );
271
314
  components["Actions"] = {
272
315
  name: "Action Classes",
273
316
  count: actions.length,
274
317
  files: actions,
275
318
  description: "Single-responsibility invokable operations"
276
319
  };
277
- const requests = normalizedFiles.filter((f) => /^app\/Http\/Requests\/.*\.php$/i.test(f));
320
+ const requests = normalizedFiles.filter(
321
+ (f) => /^(?:app|modules|Modules)\/(?:.*\/)?(?:Http\/)?Requests\/.*\.php$/i.test(f)
322
+ );
278
323
  components["Form Requests"] = {
279
324
  name: "Form Requests",
280
325
  count: requests.length,
281
326
  files: requests,
282
327
  description: "Input validation and authorization rules"
283
328
  };
284
- const filament = normalizedFiles.filter((f) => /^app\/Filament\/.*\.php$/i.test(f));
329
+ const filament = normalizedFiles.filter(
330
+ (f) => /^(?:app|modules|Modules)\/(?:.*\/)?Filament\/.*\.php$/i.test(f)
331
+ );
285
332
  if (filament.length > 0 || projectInfo.laravel?.hasFilament) {
286
333
  components["Filament Resources"] = {
287
334
  name: "Filament Admin Resources",
@@ -290,55 +337,74 @@ async function scanProject(projectInfo) {
290
337
  description: "Admin panel CRUD pages, widgets, and schemas"
291
338
  };
292
339
  }
293
- const routes = normalizedFiles.filter((f) => /^routes\/.*\.php$/i.test(f));
340
+ const routes = normalizedFiles.filter(
341
+ (f) => /^(?:routes\/.*\.php|(?:app\/Modules|modules|Modules)\/.*\/routes?.*\.php)$/i.test(f)
342
+ );
294
343
  components["Routes"] = {
295
344
  name: "Routing Definitions",
296
345
  count: routes.length,
297
346
  files: routes,
298
347
  description: "Web, API, and console endpoint routes"
299
348
  };
300
- const migrations = normalizedFiles.filter((f) => /^database\/migrations\/.*\.php$/i.test(f));
349
+ const migrations = normalizedFiles.filter(
350
+ (f) => /(?:^database\/migrations\/.*\.php$|\/migrations\/.*\.php$)/i.test(f)
351
+ );
301
352
  components["Migrations"] = {
302
353
  name: "Database Migrations",
303
354
  count: migrations.length,
304
355
  files: migrations,
305
356
  description: "Schema version control and table definitions"
306
357
  };
307
- const moduleFiles = normalizedFiles.filter((f) => /^app\/Modules\/([^/]+)/i.test(f) || /^modules\/([^/]+)/i.test(f));
308
- for (const mf of moduleFiles) {
309
- const match = mf.match(/^(?:app\/Modules|modules)\/([^/]+)/i);
310
- if (match && match[1]) modulesSet.add(match[1]);
358
+ const modDirPattern = projectInfo.laravel?.modulesDir ? `(?:${projectInfo.laravel.modulesDir}|app/Modules|modules|Modules)` : "(?:app/Modules|modules|Modules)";
359
+ const modRegex = new RegExp(`^${modDirPattern}/([^/]+)/`, "i");
360
+ for (const f of normalizedFiles) {
361
+ const match = f.match(modRegex);
362
+ if (match && match[1]) {
363
+ modulesSet.add(match[1]);
364
+ }
311
365
  }
312
366
  } else if (projectInfo.type === "typescript") {
313
- const controllers = normalizedFiles.filter((f) => /(?:^src\/.*(?:\.|\/)controller\.(?:ts|js)$)/i.test(f));
367
+ const srcDir = projectInfo.typescript?.srcDir ?? "src";
368
+ const tsPrefix = `(?:${srcDir}|(?:apps|packages)/[^/]+/(?:${srcDir}|src))`;
369
+ const controllers = normalizedFiles.filter(
370
+ (f) => !f.endsWith(".d.ts") && new RegExp(`^${tsPrefix}/(?:.*/)?(?:controllers/.*\\.(?:ts|js|mts|cts)|.*[./]controller\\.(?:ts|js|mts|cts))$`, "i").test(f)
371
+ );
314
372
  components["Controllers"] = {
315
373
  name: "Controllers",
316
374
  count: controllers.length,
317
375
  files: controllers,
318
376
  description: "HTTP request handlers and response dispatchers"
319
377
  };
320
- const services = normalizedFiles.filter((f) => /(?:^src\/.*(?:\.|\/)service\.(?:ts|js)$)/i.test(f));
378
+ const services = normalizedFiles.filter(
379
+ (f) => !f.endsWith(".d.ts") && new RegExp(`^${tsPrefix}/(?:.*/)?(?:services/.*\\.(?:ts|js|mts|cts)|.*[./]service\\.(?:ts|js|mts|cts))$`, "i").test(f)
380
+ );
321
381
  components["Services"] = {
322
382
  name: "Services",
323
383
  count: services.length,
324
384
  files: services,
325
385
  description: "Core domain logic and application workflows"
326
386
  };
327
- const repositories = normalizedFiles.filter((f) => /(?:^src\/.*(?:\.|\/)repository\.(?:ts|js)$)/i.test(f));
387
+ const repositories = normalizedFiles.filter(
388
+ (f) => !f.endsWith(".d.ts") && new RegExp(`^${tsPrefix}/(?:.*/)?(?:repositories/.*\\.(?:ts|js|mts|cts)|.*[./]repository\\.(?:ts|js|mts|cts))$`, "i").test(f)
389
+ );
328
390
  components["Repositories"] = {
329
391
  name: "Repositories",
330
392
  count: repositories.length,
331
393
  files: repositories,
332
394
  description: "Data access abstraction and database queries"
333
395
  };
334
- const routes = normalizedFiles.filter((f) => /(?:^src\/.*(?:\.|\/)routes?\.(?:ts|js)$)/i.test(f));
396
+ const routes = normalizedFiles.filter(
397
+ (f) => !f.endsWith(".d.ts") && new RegExp(`^${tsPrefix}/(?:.*/)?(?:routes/.*\\.(?:ts|js|mts|cts)|.*[./]routes?\\.(?:ts|js|mts|cts))$`, "i").test(f)
398
+ );
335
399
  components["Routes"] = {
336
400
  name: "Routes",
337
401
  count: routes.length,
338
402
  files: routes,
339
403
  description: "API endpoint mappings and router handlers"
340
404
  };
341
- const dtos = normalizedFiles.filter((f) => /(?:^src\/.*(?:\.|\/)(?:dto|schema)\.(?:ts|js)$)/i.test(f));
405
+ const dtos = normalizedFiles.filter(
406
+ (f) => !f.endsWith(".d.ts") && new RegExp(`^${tsPrefix}/(?:.*/)?(?:(?:dtos|schemas)/.*\\.(?:ts|js|mts|cts)|.*[./](?:dto|schema)\\.(?:ts|js|mts|cts))$`, "i").test(f)
407
+ );
342
408
  components["DTOs & Schemas"] = {
343
409
  name: "DTOs & Schemas",
344
410
  count: dtos.length,
@@ -346,7 +412,7 @@ async function scanProject(projectInfo) {
346
412
  description: "Data transfer objects and request validation schemas"
347
413
  };
348
414
  const models = normalizedFiles.filter(
349
- (f) => /(?:^src\/.*(?:\.|\/)(?:model|entity)\.(?:ts|js)$|^prisma\/schema\.prisma$)/i.test(f)
415
+ (f) => !f.endsWith(".d.ts") && (new RegExp(`^${tsPrefix}/(?:.*/)?(?:(?:models|entities)/.*\\.(?:ts|js|mts|cts)|.*[./](?:model|entity)\\.(?:ts|js|mts|cts))$`, "i").test(f) || /(?:^|\/)prisma\/schema\.prisma$/i.test(f))
350
416
  );
351
417
  components["Models & Entities"] = {
352
418
  name: "Models & Entities",
@@ -354,7 +420,9 @@ async function scanProject(projectInfo) {
354
420
  files: models,
355
421
  description: "Database schema definitions and entity declarations"
356
422
  };
357
- const componentsFiles = normalizedFiles.filter((f) => /^src\/(?:components|ui)\/.*\.(?:tsx|jsx|vue|ts|js)$/i.test(f));
423
+ const componentsFiles = normalizedFiles.filter(
424
+ (f) => new RegExp(`^${tsPrefix}/(?:components|ui)/.*\\.(?:tsx|jsx|vue|svelte|ts|js)$`, "i").test(f)
425
+ );
358
426
  if (componentsFiles.length > 0) {
359
427
  components["UI Components"] = {
360
428
  name: "UI Components",
@@ -363,14 +431,39 @@ async function scanProject(projectInfo) {
363
431
  description: "Reusable user interface views and components"
364
432
  };
365
433
  }
366
- const moduleFiles = normalizedFiles.filter((f) => /^src\/modules\/([^/]+)/i.test(f));
367
- for (const mf of moduleFiles) {
368
- const match = mf.match(/^src\/modules\/([^/]+)/i);
369
- if (match && match[1]) modulesSet.add(match[1]);
434
+ const moduleRegex = new RegExp(`^(?:${tsPrefix}/modules|modules)/([^/]+)/`, "i");
435
+ for (const mf of normalizedFiles) {
436
+ const match = mf.match(moduleRegex);
437
+ if (match && match[1]) {
438
+ modulesSet.add(match[1]);
439
+ }
440
+ }
441
+ const totalWebLayers = Object.values(components).reduce((acc, c) => acc + c.count, 0);
442
+ if (totalWebLayers === 0) {
443
+ const srcFiles = normalizedFiles.filter(
444
+ (f) => new RegExp(`^${tsPrefix}/`, "i").test(f)
445
+ );
446
+ const testFiles = normalizedFiles.filter(
447
+ (f) => /(?:^|[._-])(?:test|spec)\.[a-z]+$|(?:^|\/)__tests__\//i.test(f)
448
+ );
449
+ components["Source Files"] = {
450
+ name: "Source Files",
451
+ count: srcFiles.length,
452
+ files: srcFiles,
453
+ description: "Main application source code"
454
+ };
455
+ components["Test Files"] = {
456
+ name: "Test Files",
457
+ count: testFiles.length,
458
+ files: testFiles,
459
+ description: "Automated test suites and specifications"
460
+ };
370
461
  }
371
462
  } else {
372
463
  const srcFiles = normalizedFiles.filter((f) => /^src\//i.test(f));
373
- const testFiles = normalizedFiles.filter((f) => /(?:test|spec)\.[a-z]+$/i.test(f));
464
+ const testFiles = normalizedFiles.filter(
465
+ (f) => /(?:^|[._-])(?:test|spec)\.[a-z]+$|(?:^|\/)__tests__\//i.test(f)
466
+ );
374
467
  components["Source Files"] = {
375
468
  name: "Source Files",
376
469
  count: srcFiles.length,
@@ -390,20 +483,66 @@ async function scanProject(projectInfo) {
390
483
  projectInfo,
391
484
  totalFilesScanned: normalizedFiles.length,
392
485
  components,
393
- modules: Array.from(modulesSet).sort(),
486
+ modules: Array.from(modulesSet).sort(localeIndependentCompare),
394
487
  directoryTree,
395
488
  scanDurationMs
396
489
  };
397
490
  }
398
491
 
399
492
  // src/generator/markdown.ts
493
+ function mdCell(str) {
494
+ if (!str) return "-";
495
+ const cleaned = str.replace(/\|/g, "\\|").replace(/[\r\n]+/g, " ").trim();
496
+ return cleaned || "-";
497
+ }
498
+ function fence(body, lang = "") {
499
+ const matches = body.match(/`+/g);
500
+ let maxBackticks = 2;
501
+ if (matches) {
502
+ for (const match of matches) {
503
+ if (match.length > maxBackticks) maxBackticks = match.length;
504
+ }
505
+ }
506
+ const f = "`".repeat(maxBackticks + 1);
507
+ return `${f}${lang ? lang : ""}
508
+ ${body}
509
+ ${f}`;
510
+ }
511
+ function mermaidLabel(str, maxLen = 40) {
512
+ if (!str) return "";
513
+ let cleaned = str.replace(/"/g, "'").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/`/g, "'").replace(/[\[\]\(\)\{\}]/g, "").replace(/[\r\n]+/g, " ").trim();
514
+ if (cleaned.length > maxLen) {
515
+ cleaned = cleaned.slice(0, maxLen - 3) + "...";
516
+ }
517
+ return cleaned;
518
+ }
519
+ function renderModulesSubgraph(modules) {
520
+ if (modules.length === 0) return "";
521
+ let output = `
522
+ subgraph ModulesLayer ["\u{1F4E6} Detected Modules (${modules.length})"]
523
+ `;
524
+ modules.slice(0, 6).forEach((mod, idx) => {
525
+ output += ` Mod_${idx}["${mermaidLabel(mod)} Module"]:::service
526
+ `;
527
+ });
528
+ if (modules.length > 6) {
529
+ output += ` Mod_More["...and ${modules.length - 6} more modules"]:::service
530
+ `;
531
+ }
532
+ output += ` end
533
+ `;
534
+ return output;
535
+ }
400
536
  function generateMermaidDiagram(scanResult) {
401
537
  const { projectInfo, components, modules } = scanResult;
402
538
  if (projectInfo.type === "laravel") {
403
539
  const hasFilament = projectInfo.laravel?.hasFilament || (components["Filament Resources"]?.count ?? 0) > 0;
404
540
  const hasActions = (components["Actions"]?.count ?? 0) > 0;
405
541
  const hasServices = (components["Services"]?.count ?? 0) > 0;
406
- let diagram = `flowchart TD
542
+ const hasRequests = (components["Form Requests"]?.count ?? 0) > 0;
543
+ const hasModels = (components["Models"]?.count ?? 0) > 0;
544
+ const hasMigrations = (components["Migrations"]?.count ?? 0) > 0;
545
+ let diagram2 = `flowchart TD
407
546
  %% Styling Definitions
408
547
  classDef client fill:#E0F2FE,stroke:#0284C7,stroke-width:2px,color:#0369A1;
409
548
  classDef routing fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#B45309;
@@ -412,92 +551,155 @@ function generateMermaidDiagram(scanResult) {
412
551
  classDef data fill:#FEE2E2,stroke:#DC2626,stroke-width:2px,color:#B91C1C;
413
552
  classDef db fill:#F1F5F9,stroke:#475569,stroke-width:2px,color:#334155;
414
553
 
415
- subgraph Layer1 ["1. Client & Entrypoints"]
554
+ subgraph EntryLayer ["1. Client & Entrypoints"]
416
555
  Client["Client / Browser / API Consumer"]:::client
417
- Routes["HTTP Routes<br/>(routes/web.php, routes/api.php)"]:::routing
556
+ Routes["HTTP Routes"]:::routing
418
557
  end
419
-
420
- subgraph Layer2 ["2. Request Pipeline & Security"]
558
+ `;
559
+ if (hasRequests) {
560
+ diagram2 += `
561
+ subgraph RequestLayer ["2. Request Pipeline & Validation"]
421
562
  Middleware["Middleware Pipeline<br/>(Auth, CSRF, Rate Limiting)"]:::routing
422
563
  FormRequests["Form Requests<br/>(Validation & Authorization)"]:::routing
423
564
  end
424
-
425
- subgraph Layer3 ["3. Controllers & Presentation"]
426
- Controllers["HTTP Controllers<br/>(app/Http/Controllers)"]:::controller
565
+ `;
566
+ }
567
+ diagram2 += `
568
+ subgraph HttpLayer ["3. Controllers & Presentation"]
569
+ Controllers["HTTP Controllers"]:::controller
427
570
  `;
428
571
  if (hasFilament) {
429
- diagram += ` Filament["Filament Admin Panel<br/>(app/Filament/Resources)"]:::controller
572
+ diagram2 += ` Filament["Filament Admin Resources"]:::controller
430
573
  `;
431
574
  }
432
- diagram += ` end
433
-
434
- subgraph Layer4 ["4. Domain & Business Logic"]
575
+ diagram2 += ` end
435
576
  `;
436
- if (hasServices || !hasActions) {
437
- diagram += ` Services["Service Layer<br/>(app/Services)"]:::service
577
+ if (hasServices || hasActions) {
578
+ diagram2 += `
579
+ subgraph DomainLayer ["4. Domain & Business Logic"]
580
+ `;
581
+ if (hasServices) {
582
+ diagram2 += ` Services["Service Classes"]:::service
583
+ `;
584
+ }
585
+ if (hasActions) {
586
+ diagram2 += ` Actions["Action Classes"]:::service
587
+ `;
588
+ }
589
+ diagram2 += ` end
438
590
  `;
439
591
  }
440
- if (hasActions) {
441
- diagram += ` Actions["Action Classes<br/>(app/Actions)"]:::service
592
+ diagram2 += `
593
+ subgraph DataLayer ["5. Data Persistence & Storage"]
594
+ `;
595
+ if (hasModels) {
596
+ diagram2 += ` Models["Eloquent Models"]:::data
442
597
  `;
443
598
  }
444
- diagram += ` end
445
-
446
- subgraph Layer5 ["5. Data Persistence & Storage"]
447
- Models["Eloquent Models<br/>(app/Models)"]:::data
448
- Database[("Database<br/>(MySQL / Postgres / SQLite)")]:::db
449
- Migrations["Database Migrations<br/>(database/migrations)"]:::data
450
- end
599
+ diagram2 += ` Database[("Database<br/>(MySQL / Postgres / SQLite)")]:::db
600
+ `;
601
+ if (hasMigrations) {
602
+ diagram2 += ` Migrations["Database Migrations"]:::data
603
+ `;
604
+ }
605
+ diagram2 += ` end
451
606
 
452
- Client --> Routes
453
- Routes --> Middleware
454
- Middleware --> FormRequests
455
- FormRequests --> Controllers
456
607
  `;
608
+ diagram2 += ` Client --> Routes
609
+ `;
610
+ if (hasRequests) {
611
+ diagram2 += ` Routes --> Middleware
612
+ `;
613
+ diagram2 += ` Middleware --> FormRequests
614
+ `;
615
+ diagram2 += ` FormRequests --> Controllers
616
+ `;
617
+ } else {
618
+ diagram2 += ` Routes --> Controllers
619
+ `;
620
+ }
457
621
  if (hasServices && hasActions) {
458
- diagram += ` Controllers --> Services
459
- Controllers --> Actions
460
- Services --> Models
461
- Actions --> Models
622
+ diagram2 += ` Controllers --> Services
623
+ `;
624
+ diagram2 += ` Controllers --> Actions
462
625
  `;
626
+ if (hasModels) {
627
+ diagram2 += ` Services --> Models
628
+ `;
629
+ diagram2 += ` Actions --> Models
630
+ `;
631
+ } else {
632
+ diagram2 += ` Services --> Database
633
+ `;
634
+ diagram2 += ` Actions --> Database
635
+ `;
636
+ }
637
+ } else if (hasServices) {
638
+ diagram2 += ` Controllers --> Services
639
+ `;
640
+ if (hasModels) {
641
+ diagram2 += ` Services --> Models
642
+ `;
643
+ } else {
644
+ diagram2 += ` Services --> Database
645
+ `;
646
+ }
463
647
  } else if (hasActions) {
464
- diagram += ` Controllers --> Actions
465
- Actions --> Models
648
+ diagram2 += ` Controllers --> Actions
649
+ `;
650
+ if (hasModels) {
651
+ diagram2 += ` Actions --> Models
652
+ `;
653
+ } else {
654
+ diagram2 += ` Actions --> Database
466
655
  `;
656
+ }
467
657
  } else {
468
- diagram += ` Controllers --> Services
469
- Services --> Models
658
+ if (hasModels) {
659
+ diagram2 += ` Controllers --> Models
660
+ `;
661
+ } else {
662
+ diagram2 += ` Controllers --> Database
470
663
  `;
664
+ }
471
665
  }
472
666
  if (hasFilament) {
473
- diagram += ` Client --> Filament
667
+ if (hasModels) {
668
+ diagram2 += ` Client --> Filament
474
669
  Filament --> Models
475
670
  `;
476
- }
477
- diagram += ` Models --> Database
478
- Migrations -.-> Database
479
- `;
480
- if (modules.length > 0) {
481
- diagram += `
482
- subgraph ModulesLayer ["\u{1F4E6} Detected Modules (${modules.length})"]
483
- `;
484
- modules.slice(0, 6).forEach((mod, idx) => {
485
- diagram += ` Mod_${idx}["${mod} Module"]:::service
486
- `;
487
- });
488
- if (modules.length > 6) {
489
- diagram += ` Mod_More["...and ${modules.length - 6} more modules"]:::service
671
+ } else {
672
+ diagram2 += ` Client --> Filament
673
+ Filament --> Database
490
674
  `;
491
675
  }
492
- diagram += ` end
676
+ }
677
+ if (hasModels) {
678
+ diagram2 += ` Models --> Database
493
679
  `;
494
680
  }
495
- return diagram;
681
+ if (hasMigrations) {
682
+ diagram2 += ` Migrations -.-> Database
683
+ `;
684
+ }
685
+ if (modules.length > 0) {
686
+ diagram2 += renderModulesSubgraph(modules);
687
+ }
688
+ return diagram2;
496
689
  }
497
690
  if (projectInfo.type === "typescript") {
498
- const fw = projectInfo.typescript?.framework || "Express";
499
- const orm = projectInfo.typescript?.hasPrisma ? "Prisma" : projectInfo.typescript?.hasTypeORM ? "TypeORM" : "ORM";
500
- let diagram = `flowchart TD
691
+ const rawFw = projectInfo.typescript?.framework || "express";
692
+ const fwDisplay = rawFw !== "unknown" && rawFw !== "node-vanilla" ? `${rawFw.charAt(0).toUpperCase() + rawFw.slice(1)} Router` : "API Endpoints & Handlers";
693
+ const orm = projectInfo.typescript?.hasPrisma ? "Prisma" : projectInfo.typescript?.hasTypeORM ? "TypeORM" : projectInfo.typescript?.hasDrizzle ? "Drizzle" : null;
694
+ const hasControllers = (components["Controllers"]?.count ?? 0) > 0;
695
+ const hasServices = (components["Services"]?.count ?? 0) > 0;
696
+ const hasRepositories = (components["Repositories"]?.count ?? 0) > 0;
697
+ const hasDtos = (components["DTOs & Schemas"]?.count ?? 0) > 0;
698
+ const hasUi = (components["UI Components"]?.count ?? 0) > 0;
699
+ const hasModels = (components["Models & Entities"]?.count ?? 0) > 0;
700
+ const hasAnyLayer = hasControllers || hasServices || hasRepositories || hasDtos || hasUi || hasModels;
701
+ if (hasAnyLayer) {
702
+ let diagram2 = `flowchart TD
501
703
  %% Styling Definitions
502
704
  classDef client fill:#E0F2FE,stroke:#0284C7,stroke-width:2px,color:#0369A1;
503
705
  classDef routing fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#B45309;
@@ -506,77 +708,146 @@ function generateMermaidDiagram(scanResult) {
506
708
  classDef data fill:#FEE2E2,stroke:#DC2626,stroke-width:2px,color:#B91C1C;
507
709
  classDef db fill:#F1F5F9,stroke:#475569,stroke-width:2px,color:#334155;
508
710
 
509
- subgraph Layer1 ["1. Client & API Gateway"]
711
+ subgraph EntryLayer ["1. Client & API Gateway"]
510
712
  Client["Frontend / API Client"]:::client
511
- Router["Router & Endpoints<br/>(${fw} Routes)"]:::routing
713
+ Router["Router & Endpoints<br/>(${fwDisplay})"]:::routing
512
714
  end
513
-
514
- subgraph Layer2 ["2. Validation & Guards"]
515
- Middleware["Auth & Error Middleware"]:::routing
715
+ `;
716
+ if (hasUi) {
717
+ diagram2 += `
718
+ subgraph UiLayer ["Frontend Presentation"]
719
+ UiComponents["UI Components<br/>(src/components)"]:::controller
720
+ end
721
+ `;
722
+ }
723
+ if (hasDtos) {
724
+ diagram2 += `
725
+ subgraph ValidationLayer ["2. Validation & Schemas"]
516
726
  DTOs["DTOs & Schemas<br/>(Input Validation)"]:::routing
517
727
  end
518
-
519
- subgraph Layer3 ["3. Controllers"]
728
+ `;
729
+ }
730
+ if (hasControllers) {
731
+ diagram2 += `
732
+ subgraph ControllerLayer ["3. Controllers"]
520
733
  Controllers["Controllers<br/>(Request Handling)"]:::controller
521
734
  end
522
-
523
- subgraph Layer4 ["4. Application Services"]
735
+ `;
736
+ }
737
+ if (hasServices) {
738
+ diagram2 += `
739
+ subgraph ServiceLayer ["4. Application Services"]
524
740
  Services["Domain Services<br/>(Business Logic)"]:::service
525
741
  end
742
+ `;
743
+ }
744
+ diagram2 += `
745
+ subgraph PersistenceLayer ["5. Data Layer & Storage"]
746
+ `;
747
+ if (hasRepositories) {
748
+ diagram2 += ` Repositories["Repositories / Data Access<br/>(${orm || "Data Layer"})"]:::data
749
+ `;
750
+ } else if (hasModels) {
751
+ diagram2 += ` Entities["Models & Entities<br/>(${orm || "ORM"})"]:::data
752
+ `;
753
+ }
754
+ diagram2 += ` Database[("Database / Storage")]:::db
755
+ `;
756
+ diagram2 += ` end
526
757
 
527
- subgraph Layer5 ["5. Data Layer & Storage"]
528
- Repositories["Repositories / ${orm}<br/>(Data Access Layer)"]:::data
529
- Database[("Database<br/>(SQL / NoSQL)")]:::db
530
- end
531
-
532
- Client --> Router
533
- Router --> Middleware
534
- Middleware --> DTOs
535
- DTOs --> Controllers
536
- Controllers --> Services
537
- Services --> Repositories
538
- Repositories --> Database
539
758
  `;
540
- if (modules.length > 0) {
541
- diagram += `
542
- subgraph ModulesLayer ["\u{1F4E6} Detected Feature Modules (${modules.length})"]
759
+ diagram2 += ` Client --> Router
543
760
  `;
544
- modules.slice(0, 6).forEach((mod, idx) => {
545
- diagram += ` Mod_${idx}["${mod} Module"]:::service
761
+ if (hasUi) {
762
+ diagram2 += ` Client --> UiComponents
546
763
  `;
547
- });
548
- if (modules.length > 6) {
549
- diagram += ` Mod_More["...and ${modules.length - 6} more modules"]:::service
764
+ diagram2 += ` UiComponents --> Router
765
+ `;
766
+ }
767
+ let previousNode = "Router";
768
+ if (hasDtos) {
769
+ diagram2 += ` ${previousNode} --> DTOs
770
+ `;
771
+ previousNode = "DTOs";
772
+ }
773
+ if (hasControllers) {
774
+ diagram2 += ` ${previousNode} --> Controllers
775
+ `;
776
+ previousNode = "Controllers";
777
+ }
778
+ if (hasServices) {
779
+ diagram2 += ` ${previousNode} --> Services
550
780
  `;
781
+ previousNode = "Services";
551
782
  }
552
- diagram += ` end
783
+ if (hasRepositories) {
784
+ diagram2 += ` ${previousNode} --> Repositories
553
785
  `;
786
+ diagram2 += ` Repositories --> Database
787
+ `;
788
+ } else if (hasModels) {
789
+ diagram2 += ` ${previousNode} --> Entities
790
+ `;
791
+ diagram2 += ` Entities --> Database
792
+ `;
793
+ } else {
794
+ diagram2 += ` ${previousNode} --> Database
795
+ `;
796
+ }
797
+ if (modules.length > 0) {
798
+ diagram2 += renderModulesSubgraph(modules);
799
+ }
800
+ return diagram2;
554
801
  }
555
- return diagram;
556
802
  }
557
- return `flowchart TD
803
+ const hasEntry = (components["Source Files"]?.files || []).some(
804
+ (f) => /(?:^|\/)(?:index|cli|main|bin)\.[a-z]+$/i.test(f)
805
+ );
806
+ const hasSource = (components["Source Files"]?.count ?? 0) > 0;
807
+ const hasTests = (components["Test Files"]?.count ?? 0) > 0;
808
+ let diagram = `flowchart TD
558
809
  classDef client fill:#E0F2FE,stroke:#0284C7,stroke-width:2px,color:#0369A1;
559
810
  classDef core fill:#DCFCE7,stroke:#16A34A,stroke-width:2px,color:#15803D;
560
811
  classDef test fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#B45309;
561
812
 
562
- subgraph App ["Application Architecture"]
563
- Entry["Entrypoint / CLI"]:::client
564
- Source["Source Core Logic (src/)"]:::core
565
- Tests["Test Suites"]:::test
566
- end
813
+ subgraph App ["CLI & Application Architecture"]
814
+ `;
815
+ if (hasEntry) {
816
+ diagram += ` Entry["Entrypoint CLI (bin/)"]:::client
817
+ `;
818
+ }
819
+ if (hasSource) {
820
+ diagram += ` Source["Core Modules & Generators (src/)"]:::core
821
+ `;
822
+ }
823
+ if (hasTests) {
824
+ diagram += ` Tests["Automated Test Suites (tests/)"]:::test
825
+ `;
826
+ }
827
+ if (!hasEntry && !hasSource && !hasTests) {
828
+ diagram += ` Source["Source Files"]:::core
829
+ `;
830
+ }
831
+ diagram += ` end
567
832
 
568
- Entry --> Source
569
- Tests -.-> Source
570
833
  `;
834
+ if (hasEntry && hasSource) {
835
+ diagram += ` Entry --> Source
836
+ `;
837
+ }
838
+ if (hasTests && hasSource) {
839
+ diagram += ` Tests -.-> Source
840
+ `;
841
+ }
842
+ return diagram;
571
843
  }
572
844
  function generateArchitectureMarkdown(scanResult) {
573
- const { projectInfo, totalFilesScanned, components, modules, directoryTree, scanDurationMs } = scanResult;
574
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").substring(0, 19) + " UTC";
845
+ const { projectInfo, totalFilesScanned, components, modules, directoryTree } = scanResult;
575
846
  const mermaid = generateMermaidDiagram(scanResult);
576
- const componentRows = Object.entries(components).map(([key, comp]) => {
847
+ const componentRows = Object.entries(components).map(([, comp]) => {
577
848
  const sampleFiles = comp.files.slice(0, 3).map((f) => `\`${f}\``).join(", ");
578
849
  const moreSuffix = comp.files.length > 3 ? ` *(+${comp.files.length - 3} more)*` : "";
579
- return `| **${comp.name}** | \`${comp.count}\` | ${comp.description} | ${sampleFiles || "-"} ${moreSuffix} |`;
850
+ return `| **${mdCell(comp.name)}** | \`${comp.count}\` | ${mdCell(comp.description)} | ${sampleFiles ? mdCell(sampleFiles + moreSuffix) : "-"} |`;
580
851
  }).join("\n");
581
852
  let modulesSection = "";
582
853
  if (modules.length > 0) {
@@ -588,10 +859,19 @@ This project organizes features into modular domains:
588
859
  ${modules.map((m) => `- **\`${m}\`**`).join("\n")}
589
860
  `;
590
861
  }
862
+ const sampleIgnores = SAFE_IGNORE_LIST.slice(0, 8).map((p2) => `\`${p2}\``).join(", ");
863
+ const securityNotice = `## \u{1F512} Security & Privacy Notice
864
+
865
+ This architectural map was produced with strict privacy guarantees:
866
+ - **Zero Network Transmission:** Executed locally without telemetry, tracking, or outbound communication.
867
+ - **Safe Exclusion Filter:** Automatically ignores sensitive files and secret patterns (${sampleIgnores}, etc.) including \`node_modules\`, \`vendor\`, \`.env*\`, private keys, secrets, credentials, cloud configs, and cache folders.
868
+ - **Manifest & Structure Only:** Safely reads standard manifest dependencies (\`package.json\` / \`composer.json\`) to determine framework types, and relative file paths to categorize components. Never reads private configurations or file contents.
869
+
870
+ *Generated with \u2764\uFE0F by LaraTS CLI.*`;
591
871
  return `# \u{1F3DB}\uFE0F Architecture Overview: ${projectInfo.name}
592
872
 
593
- > Auto-generated by **[LaraTS CLI](https://github.com/SidiqFatuh187/Larats)** on \`${timestamp}\`.
594
- > **100% Local & Safe:** Generated entirely in offline mode via local filesystem metadata analysis. Zero data transmitted.
873
+ > Auto-generated by **[LaraTS CLI](https://github.com/SidiqFatuh187/Larats)**.
874
+ > **100% Local & Safe:** Generated entirely in offline mode via local filesystem manifest & structure analysis. Zero data transmitted.
595
875
 
596
876
  ---
597
877
 
@@ -603,7 +883,6 @@ ${modules.map((m) => `- **\`${m}\`**`).join("\n")}
603
883
  | **Project Type** | **${projectInfo.type.toUpperCase()}** |
604
884
  | **Detected Stack** | \`${projectInfo.summary}\` |
605
885
  | **Scanned Files** | \`${totalFilesScanned}\` files indexed (safe ignore filter applied) |
606
- | **Scan Duration** | \`${scanDurationMs} ms\` |
607
886
 
608
887
  ---
609
888
 
@@ -619,9 +898,7 @@ ${modulesSection}
619
898
 
620
899
  Visual representation of the project layers and request lifecycle rendered with [Mermaid.js](https://mermaid.js.org/):
621
900
 
622
- \`\`\`mermaid
623
- ${mermaid}
624
- \`\`\`
901
+ ${fence(mermaid, "mermaid")}
625
902
 
626
903
  ---
627
904
 
@@ -629,49 +906,256 @@ ${mermaid}
629
906
 
630
907
  Clean, high-level directory tree showing key folders (ignoring build artifacts, vendor files, and sensitive keys):
631
908
 
632
- \`\`\`text
633
- ${directoryTree || "(Empty directory structure)"}
634
- \`\`\`
909
+ ${fence(directoryTree || "(Empty directory structure)", "text")}
635
910
 
636
911
  ---
637
912
 
638
- ## \u{1F512} Security & Privacy Notice
639
-
640
- This architectural map was produced with strict privacy rules:
641
- - **Zero Network Transmission:** Executed locally without telemetry or outbound communication.
642
- - **Safe Exclusion Filter:** Ignored \`node_modules\`, \`vendor\`, \`.env*\`, key files, credentials, and cache folders.
643
- - **Metadata Only:** Scanned file names and relative paths only; no secrets or private business contents were extracted.
644
-
645
- *Generated with \u2764\uFE0F by LaraTS CLI.*
913
+ ${securityNotice}
646
914
  `;
647
915
  }
916
+ function renderTerminalFlow(scanResult) {
917
+ const { projectInfo, components } = scanResult;
918
+ const steps = [];
919
+ if (projectInfo.type === "laravel") {
920
+ steps.push("Client");
921
+ const routes = components["Routes"]?.count ?? 0;
922
+ if (routes > 0) steps.push(`Routes [${routes}]`);
923
+ const requests = components["Form Requests"]?.count ?? 0;
924
+ if (requests > 0) steps.push(`Requests [${requests}]`);
925
+ const controllers = components["Controllers"]?.count ?? 0;
926
+ if (controllers > 0) steps.push(`Controllers [${controllers}]`);
927
+ const services = (components["Services"]?.count ?? 0) + (components["Actions"]?.count ?? 0);
928
+ if (services > 0) steps.push(`Services [${services}]`);
929
+ const models = components["Models"]?.count ?? 0;
930
+ if (models > 0) steps.push(`Models [${models}]`);
931
+ steps.push("Database");
932
+ } else if (projectInfo.type === "typescript") {
933
+ const hasAnyLayer = ["Controllers", "Services", "Repositories", "Routes", "DTOs & Schemas", "Models & Entities", "UI Components"].some((k) => (components[k]?.count ?? 0) > 0);
934
+ if (hasAnyLayer) {
935
+ steps.push("Client");
936
+ const ui = components["UI Components"]?.count ?? 0;
937
+ if (ui > 0) steps.push(`UI [${ui}]`);
938
+ const routes = components["Routes"]?.count ?? 0;
939
+ if (routes > 0) steps.push(`Router [${routes}]`);
940
+ const dtos = components["DTOs & Schemas"]?.count ?? 0;
941
+ if (dtos > 0) steps.push(`DTOs [${dtos}]`);
942
+ const controllers = components["Controllers"]?.count ?? 0;
943
+ if (controllers > 0) steps.push(`Controllers [${controllers}]`);
944
+ const services = components["Services"]?.count ?? 0;
945
+ if (services > 0) steps.push(`Services [${services}]`);
946
+ const data = (components["Repositories"]?.count ?? 0) + (components["Models & Entities"]?.count ?? 0);
947
+ if (data > 0) steps.push(`Data Layer [${data}]`);
948
+ steps.push("Database");
949
+ } else {
950
+ const src = components["Source Files"]?.count ?? 0;
951
+ const tests = components["Test Files"]?.count ?? 0;
952
+ steps.push("CLI / Entrypoint");
953
+ if (src > 0) steps.push(`Source Modules [${src}]`);
954
+ if (tests > 0) steps.push(`Test Suites [${tests}]`);
955
+ }
956
+ } else {
957
+ const src = components["Source Files"]?.count ?? 0;
958
+ const tests = components["Test Files"]?.count ?? 0;
959
+ steps.push("Project Root");
960
+ if (src > 0) steps.push(`Source [${src}]`);
961
+ if (tests > 0) steps.push(`Tests [${tests}]`);
962
+ }
963
+ return steps.join(" \u2500\u2500> ");
964
+ }
965
+ function escapeHtml(str) {
966
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
967
+ }
968
+ function generateArchitectureHtml(scanResult) {
969
+ const { projectInfo, totalFilesScanned, components, modules, directoryTree } = scanResult;
970
+ const mermaid = generateMermaidDiagram(scanResult);
971
+ const componentRowsHtml = Object.entries(components).map(([, comp]) => {
972
+ const samples = comp.files.slice(0, 3).map((f) => `<code>${escapeHtml(f)}</code>`).join(", ");
973
+ const more = comp.files.length > 3 ? ` <span class="muted">(+${comp.files.length - 3} more)</span>` : "";
974
+ return `<tr>
975
+ <td><strong>${escapeHtml(comp.name)}</strong></td>
976
+ <td><span class="badge">${comp.count}</span></td>
977
+ <td>${escapeHtml(comp.description)}</td>
978
+ <td>${samples ? samples + more : "-"}</td>
979
+ </tr>`;
980
+ }).join("\n");
981
+ const modulesHtml = modules.length > 0 ? `<div class="card">
982
+ <h2>\u{1F4E6} Detected Modules & Domains (${modules.length})</h2>
983
+ <div class="modules-grid">
984
+ ${modules.map((m) => `<div class="module-item"><code>${escapeHtml(m)}</code></div>`).join("\n")}
985
+ </div>
986
+ </div>` : "";
987
+ return `<!DOCTYPE html>
988
+ <html lang="en">
989
+ <head>
990
+ <meta charset="UTF-8">
991
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
992
+ <title>LaraTS Architecture: ${escapeHtml(projectInfo.name)}</title>
993
+ <style>
994
+ :root {
995
+ --bg: #0f172a;
996
+ --card-bg: #1e293b;
997
+ --text: #f8fafc;
998
+ --muted: #94a3b8;
999
+ --border: #334155;
1000
+ --primary: #38bdf8;
1001
+ --accent: #818cf8;
1002
+ }
1003
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1004
+ body {
1005
+ background: var(--bg);
1006
+ color: var(--text);
1007
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
1008
+ line-height: 1.5;
1009
+ padding: 2rem 1rem;
1010
+ }
1011
+ .container { max-width: 1080px; margin: 0 auto; }
1012
+ header { margin-bottom: 2rem; border-bottom: 1px solid var(--border); padding-bottom: 1.5rem; }
1013
+ .header-top { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1rem; }
1014
+ h1 { font-size: 1.8rem; font-weight: 700; color: #fff; }
1015
+ .pill { background: #0284c7; color: #fff; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.85rem; font-weight: 600; }
1016
+ .subtitle { color: var(--muted); margin-top: 0.5rem; font-size: 0.95rem; }
1017
+ .card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 1.5rem; margin-bottom: 2rem; }
1018
+ h2 { font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem; color: #e2e8f0; }
1019
+ table { width: 100%; border-collapse: collapse; margin-top: 0.5rem; font-size: 0.9rem; }
1020
+ th, td { text-align: left; padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); }
1021
+ th { color: var(--muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
1022
+ .badge { background: #334155; padding: 0.2rem 0.6rem; border-radius: 4px; font-weight: 600; font-size: 0.85rem; }
1023
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; background: #090d16; padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.85rem; color: #38bdf8; }
1024
+ pre { background: #090d16; padding: 1rem; border-radius: 6px; overflow-x: auto; font-size: 0.85rem; border: 1px solid var(--border); line-height: 1.4; }
1025
+ .mermaid-box { display: flex; justify-content: center; background: #090d16; border-radius: 8px; padding: 2rem 1rem; overflow-x: auto; border: 1px solid var(--border); }
1026
+ .modules-grid { display: flex; flex-wrap: wrap; gap: 0.5rem; }
1027
+ .module-item { background: #090d16; border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 1rem; }
1028
+ .muted { color: var(--muted); font-size: 0.85rem; }
1029
+ footer { text-align: center; color: var(--muted); font-size: 0.85rem; margin-top: 3rem; }
1030
+ </style>
1031
+ </head>
1032
+ <body>
1033
+ <div class="container">
1034
+ <header>
1035
+ <div class="header-top">
1036
+ <div>
1037
+ <h1>\u{1F3DB}\uFE0F Architecture Overview: ${escapeHtml(projectInfo.name)}</h1>
1038
+ <p class="subtitle">Stack: <strong>${escapeHtml(projectInfo.summary)}</strong> &bull; Total Files: <strong>${totalFilesScanned}</strong></p>
1039
+ </div>
1040
+ <span class="pill">LaraTS Architecture Map</span>
1041
+ </div>
1042
+ </header>
1043
+
1044
+ <div class="card">
1045
+ <h2>\u{1F5FA}\uFE0F Interactive Architecture Flowchart</h2>
1046
+ <div class="mermaid-box">
1047
+ <pre class="mermaid">${mermaid}</pre>
1048
+ </div>
1049
+ </div>
1050
+
1051
+ <div class="card">
1052
+ <h2>\u{1F4CA} Architectural Component Statistics</h2>
1053
+ <table>
1054
+ <thead>
1055
+ <tr>
1056
+ <th>Component Layer</th>
1057
+ <th>Files</th>
1058
+ <th>Description</th>
1059
+ <th>Sample Files</th>
1060
+ </tr>
1061
+ </thead>
1062
+ <tbody>
1063
+ ${componentRowsHtml || '<tr><td colspan="4">No architectural components detected</td></tr>'}
1064
+ </tbody>
1065
+ </table>
1066
+ </div>
1067
+
1068
+ ${modulesHtml}
1069
+
1070
+ <div class="card">
1071
+ <h2>\u{1F332} Directory Structure Tree</h2>
1072
+ <pre><code>${escapeHtml(directoryTree || "(Empty directory)")}</code></pre>
1073
+ </div>
1074
+
1075
+ <footer>
1076
+ Generated locally &amp; offline with \u2764\uFE0F by <strong>LaraTS CLI</strong>
1077
+ </footer>
1078
+ </div>
1079
+
1080
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
1081
+ <script>
1082
+ if (typeof mermaid !== 'undefined') {
1083
+ mermaid.initialize({
1084
+ startOnLoad: true,
1085
+ theme: 'dark',
1086
+ flowchart: { curve: 'basis' }
1087
+ });
1088
+ }
1089
+ </script>
1090
+ </body>
1091
+ </html>`;
1092
+ }
648
1093
 
649
1094
  // src/generator/scaffolder.ts
650
1095
  import fs2 from "fs";
651
- import path2 from "path";
1096
+ import path3 from "path";
652
1097
 
653
1098
  // src/templates/laravel.ts
1099
+ function pluralize(word) {
1100
+ const lower = word.toLowerCase();
1101
+ if (lower.endsWith("s") && !lower.endsWith("ss")) {
1102
+ return word;
1103
+ }
1104
+ if (/[bcdfghjklmnpqrstvwxyz]y$/i.test(word)) {
1105
+ return word.slice(0, -1) + (word.endsWith("Y") ? "IES" : "ies");
1106
+ }
1107
+ if (/(?:s|x|z|ch|sh)$/i.test(word)) {
1108
+ return word + (word === word.toUpperCase() ? "ES" : "es");
1109
+ }
1110
+ return word + (word === word.toUpperCase() ? "S" : "s");
1111
+ }
654
1112
  function getNaming(input) {
655
1113
  const words = input.trim().replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^a-zA-Z0-9]/g, " ").split(/\s+/).filter(Boolean);
656
1114
  if (words.length === 0) {
657
- words.push("SampleModule");
1115
+ words.push("sample", "module");
658
1116
  }
659
1117
  const pascal = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
660
1118
  const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
661
1119
  const snake = words.map((w) => w.toLowerCase()).join("_");
662
1120
  const kebab = words.map((w) => w.toLowerCase()).join("-");
663
- const pluralSuffix = snake.endsWith("s") || snake.endsWith("ch") || snake.endsWith("sh") ? "es" : "s";
664
- const pluralSnake = snake + pluralSuffix;
665
- const pluralPascal = pascal + pluralSuffix;
1121
+ const pluralWords = [...words];
1122
+ const lastIdx = pluralWords.length - 1;
1123
+ pluralWords[lastIdx] = pluralize(pluralWords[lastIdx]);
1124
+ const pluralSnake = pluralWords.map((w) => w.toLowerCase()).join("_");
1125
+ const pluralPascal = pluralWords.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
666
1126
  return { pascal, camel, kebab, snake, pluralSnake, pluralPascal };
667
1127
  }
668
- function laravelModelTemplate(names, namespace = "App\\Models") {
1128
+ function laravelModelTemplate(names, namespace = "App\\Models", laravelVersion) {
1129
+ const major = laravelVersion ? parseInt(laravelVersion.split(".")[0], 10) : 11;
1130
+ const isLaravel10OrOlder = major > 0 && major < 11;
1131
+ const castsBlock = isLaravel10OrOlder ? ` /**
1132
+ * The attributes that should be cast.
1133
+ *
1134
+ * @var array<string, string>
1135
+ */
1136
+ protected $casts = [
1137
+ 'metadata' => 'array',
1138
+ 'created_at' => 'datetime',
1139
+ 'updated_at' => 'datetime',
1140
+ ];` : ` /**
1141
+ * Get the attributes that should be cast.
1142
+ *
1143
+ * @return array<string, string>
1144
+ */
1145
+ protected function casts(): array
1146
+ {
1147
+ return [
1148
+ 'metadata' => 'array',
1149
+ 'created_at' => 'datetime',
1150
+ 'updated_at' => 'datetime',
1151
+ ];
1152
+ }`;
669
1153
  return `<?php
670
1154
 
671
1155
  namespace ${namespace};
672
1156
 
673
- use IlluminateDatabaseEloquentFactoriesHasFactory;
674
- use IlluminateDatabaseEloquentModel;
1157
+ use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
1158
+ use Illuminate\\Database\\Eloquent\\Model;
675
1159
 
676
1160
  class ${names.pascal} extends Model
677
1161
  {
@@ -695,19 +1179,7 @@ class ${names.pascal} extends Model
695
1179
  'metadata',
696
1180
  ];
697
1181
 
698
- /**
699
- * Get the attributes that should be cast.
700
- *
701
- * @return array<string, string>
702
- */
703
- protected function casts(): array
704
- {
705
- return [
706
- 'metadata' => 'array',
707
- 'created_at' => 'datetime',
708
- 'updated_at' => 'datetime',
709
- ];
710
- }
1182
+ ${castsBlock}
711
1183
  }
712
1184
  `;
713
1185
  }
@@ -716,15 +1188,16 @@ function laravelControllerTemplate(names, options = {}) {
716
1188
  const modelNamespace = options.modelNamespace || "App\\Models";
717
1189
  const serviceNamespace = options.serviceNamespace || "App\\Services";
718
1190
  const requestNamespace = options.requestNamespace || "App\\Http\\Requests";
1191
+ const baseControllerImport = namespace !== "App\\Http\\Controllers" ? "use App\\Http\\Controllers\\Controller;\n" : "";
719
1192
  return `<?php
720
1193
 
721
1194
  namespace ${namespace};
722
1195
 
723
- use ${modelNamespace}\\${names.pascal};
1196
+ ${baseControllerImport}use ${modelNamespace}\\${names.pascal};
724
1197
  use ${serviceNamespace}\\${names.pascal}Service;
725
1198
  use ${requestNamespace}\\${names.pascal}Request;
726
- use IlluminateHttpJsonResponse;
727
- use IlluminateHttpRequest;
1199
+ use Illuminate\\Http\\JsonResponse;
1200
+ use Illuminate\\Http\\Request;
728
1201
 
729
1202
  class ${names.pascal}Controller extends Controller
730
1203
  {
@@ -809,7 +1282,7 @@ function laravelServiceTemplate(names, options = {}) {
809
1282
  namespace ${namespace};
810
1283
 
811
1284
  use ${modelNamespace}\\${names.pascal};
812
- use IlluminateDatabaseEloquentCollection;
1285
+ use Illuminate\\Database\\Eloquent\\Collection;
813
1286
 
814
1287
  class ${names.pascal}Service
815
1288
  {
@@ -888,7 +1361,7 @@ function laravelRequestTemplate(names, namespace = "App\\Http\\Requests") {
888
1361
 
889
1362
  namespace ${namespace};
890
1363
 
891
- use IlluminateFoundationHttpFormRequest;
1364
+ use Illuminate\\Foundation\\Http\\FormRequest;
892
1365
 
893
1366
  class ${names.pascal}Request extends FormRequest
894
1367
  {
@@ -931,9 +1404,9 @@ class ${names.pascal}Request extends FormRequest
931
1404
  function laravelMigrationTemplate(names) {
932
1405
  return `<?php
933
1406
 
934
- use IlluminateDatabaseMigrationsMigration;
935
- use IlluminateDatabaseSchemaBlueprint;
936
- use IlluminateSupportFacadesSchema;
1407
+ use Illuminate\\Database\\Migrations\\Migration;
1408
+ use Illuminate\\Database\\Schema\\Blueprint;
1409
+ use Illuminate\\Support\\Facades\\Schema;
937
1410
 
938
1411
  return new class extends Migration
939
1412
  {
@@ -997,7 +1470,11 @@ export class ${names.pascal}Controller {
997
1470
  return;
998
1471
  }
999
1472
  res.status(200).json({ status: 'success', data: item });
1000
- } catch (error) {
1473
+ } catch (error: any) {
1474
+ if (error?.name === 'NotFoundError') {
1475
+ res.status(404).json({ status: 'fail', message: error.message });
1476
+ return;
1477
+ }
1001
1478
  next(error);
1002
1479
  }
1003
1480
  };
@@ -1024,7 +1501,11 @@ export class ${names.pascal}Controller {
1024
1501
  const payload: Update${names.pascal}Dto = req.body;
1025
1502
  const updated = await this.${names.camel}Service.update(id, payload);
1026
1503
  res.status(200).json({ status: 'success', data: updated });
1027
- } catch (error) {
1504
+ } catch (error: any) {
1505
+ if (error?.name === 'NotFoundError') {
1506
+ res.status(404).json({ status: 'fail', message: error.message });
1507
+ return;
1508
+ }
1028
1509
  next(error);
1029
1510
  }
1030
1511
  };
@@ -1035,9 +1516,17 @@ export class ${names.pascal}Controller {
1035
1516
  delete = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
1036
1517
  try {
1037
1518
  const { id } = req.params;
1038
- await this.${names.camel}Service.delete(id);
1519
+ const deleted = await this.${names.camel}Service.delete(id);
1520
+ if (!deleted) {
1521
+ res.status(404).json({ status: 'fail', message: '${names.pascal} not found' });
1522
+ return;
1523
+ }
1039
1524
  res.status(200).json({ status: 'success', message: '${names.pascal} deleted successfully' });
1040
- } catch (error) {
1525
+ } catch (error: any) {
1526
+ if (error?.name === 'NotFoundError') {
1527
+ res.status(404).json({ status: 'fail', message: error.message });
1528
+ return;
1529
+ }
1041
1530
  next(error);
1042
1531
  }
1043
1532
  };
@@ -1077,7 +1566,8 @@ export class ${names.pascal}Service {
1077
1566
  }
1078
1567
  function tsRepositoryTemplate(names, isModular = true) {
1079
1568
  const dtoImport = isModular ? `./${names.kebab}.dto.js` : `../dtos/${names.kebab}.dto.js`;
1080
- return `import type { ${names.pascal}Entity, Create${names.pascal}Dto, Update${names.pascal}Dto } from '${dtoImport}';
1569
+ return `import { randomUUID } from 'node:crypto';
1570
+ import { NotFoundError, type ${names.pascal}Entity, type Create${names.pascal}Dto, type Update${names.pascal}Dto } from '${dtoImport}';
1081
1571
 
1082
1572
  export interface I${names.pascal}Repository {
1083
1573
  findAll(query?: Record<string, any>): Promise<${names.pascal}Entity[]>;
@@ -1100,7 +1590,7 @@ export class ${names.pascal}Repository implements I${names.pascal}Repository {
1100
1590
  }
1101
1591
 
1102
1592
  async create(data: Create${names.pascal}Dto): Promise<${names.pascal}Entity> {
1103
- const id = (Math.random() * 100000).toFixed(0);
1593
+ const id = randomUUID();
1104
1594
  const newRecord: ${names.pascal}Entity = {
1105
1595
  id,
1106
1596
  ...data,
@@ -1115,7 +1605,7 @@ export class ${names.pascal}Repository implements I${names.pascal}Repository {
1115
1605
  async update(id: string, data: Update${names.pascal}Dto): Promise<${names.pascal}Entity> {
1116
1606
  const existing = await this.findById(id);
1117
1607
  if (!existing) {
1118
- throw new Error(\`${names.pascal} with id \${id} not found\`);
1608
+ throw new NotFoundError(\`${names.pascal} with id \${id} not found\`);
1119
1609
  }
1120
1610
  const updated: ${names.pascal}Entity = {
1121
1611
  ...existing,
@@ -1154,7 +1644,14 @@ export default router;
1154
1644
  `;
1155
1645
  }
1156
1646
  function tsDtoTemplate(names) {
1157
- return `export interface ${names.pascal}Entity {
1647
+ return `export class NotFoundError extends Error {
1648
+ constructor(message: string) {
1649
+ super(message);
1650
+ this.name = 'NotFoundError';
1651
+ }
1652
+ }
1653
+
1654
+ export interface ${names.pascal}Entity {
1158
1655
  id: string;
1159
1656
  name: string;
1160
1657
  status: 'draft' | 'active' | 'archived';
@@ -1181,6 +1678,39 @@ export interface Update${names.pascal}Dto {
1181
1678
  }
1182
1679
 
1183
1680
  // src/generator/scaffolder.ts
1681
+ var RESERVED_NAMES = /* @__PURE__ */ new Set([
1682
+ "model",
1683
+ "controller",
1684
+ "service",
1685
+ "action",
1686
+ "request",
1687
+ "migration",
1688
+ "repository",
1689
+ "route",
1690
+ "dto",
1691
+ "object",
1692
+ "class",
1693
+ "interface",
1694
+ "module",
1695
+ "app",
1696
+ "base"
1697
+ ]);
1698
+ function validateModuleName(name) {
1699
+ if (!name || !name.trim()) {
1700
+ return "Module name cannot be empty.";
1701
+ }
1702
+ const trimmed = name.trim();
1703
+ if (!/^[a-zA-Z]/.test(trimmed)) {
1704
+ return "Module name must start with an alphabetical letter.";
1705
+ }
1706
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(trimmed)) {
1707
+ return "Module name may only contain alphanumeric characters, hyphens, and underscores.";
1708
+ }
1709
+ if (RESERVED_NAMES.has(trimmed.toLowerCase())) {
1710
+ return `"${trimmed}" is a reserved framework name and cannot be used as a module name.`;
1711
+ }
1712
+ return null;
1713
+ }
1184
1714
  function getMigrationTimestamp() {
1185
1715
  const now = /* @__PURE__ */ new Date();
1186
1716
  const YYYY = now.getFullYear();
@@ -1195,16 +1725,46 @@ async function scaffoldModule(projectInfo, options) {
1195
1725
  const root = projectInfo.rootPath;
1196
1726
  const names = getNaming(options.moduleName);
1197
1727
  const filesToGenerate = [];
1728
+ const results = [];
1729
+ let createdCount = 0;
1730
+ let skippedCount = 0;
1731
+ let errorCount = 0;
1198
1732
  const isModular = options.isModular ?? (projectInfo.type === "laravel" ? Boolean(projectInfo.laravel?.isModular) : projectInfo.type === "typescript" ? Boolean(projectInfo.typescript?.isModular) : true);
1199
1733
  if (projectInfo.type === "laravel") {
1200
1734
  const selected = new Set(options.components);
1735
+ const addMigrationIfRequested = () => {
1736
+ const migrationsDir = path3.join(root, "database", "migrations");
1737
+ let existingMigration = null;
1738
+ if (fs2.existsSync(migrationsDir)) {
1739
+ const files = fs2.readdirSync(migrationsDir);
1740
+ const match = files.find((f) => f.endsWith(`_create_${names.pluralSnake}_table.php`));
1741
+ if (match) {
1742
+ existingMigration = match;
1743
+ }
1744
+ }
1745
+ if (existingMigration && !options.forceOverwrite) {
1746
+ results.push({
1747
+ filePath: path3.join(migrationsDir, existingMigration),
1748
+ relativeFilePath: `database/migrations/${existingMigration}`,
1749
+ status: "skipped",
1750
+ reason: "Migration for table already exists (overwrite protected)"
1751
+ });
1752
+ skippedCount++;
1753
+ } else {
1754
+ const timestamp = existingMigration ? existingMigration.split("_create_")[0] : getMigrationTimestamp();
1755
+ filesToGenerate.push({
1756
+ relativePath: `database/migrations/${timestamp}_create_${names.pluralSnake}_table.php`,
1757
+ content: laravelMigrationTemplate(names)
1758
+ });
1759
+ }
1760
+ };
1201
1761
  if (isModular) {
1202
1762
  const baseDir = `app/Modules/${names.pascal}`;
1203
1763
  const baseNamespace = `App\\Modules\\${names.pascal}`;
1204
1764
  if (selected.has("model")) {
1205
1765
  filesToGenerate.push({
1206
1766
  relativePath: `${baseDir}/Models/${names.pascal}.php`,
1207
- content: laravelModelTemplate(names, `${baseNamespace}\\Models`)
1767
+ content: laravelModelTemplate(names, `${baseNamespace}\\Models`, projectInfo.laravel?.version)
1208
1768
  });
1209
1769
  }
1210
1770
  if (selected.has("controller")) {
@@ -1243,17 +1803,13 @@ async function scaffoldModule(projectInfo, options) {
1243
1803
  });
1244
1804
  }
1245
1805
  if (selected.has("migration")) {
1246
- const timestamp = getMigrationTimestamp();
1247
- filesToGenerate.push({
1248
- relativePath: `database/migrations/${timestamp}_create_${names.pluralSnake}_table.php`,
1249
- content: laravelMigrationTemplate(names)
1250
- });
1806
+ addMigrationIfRequested();
1251
1807
  }
1252
1808
  } else {
1253
1809
  if (selected.has("model")) {
1254
1810
  filesToGenerate.push({
1255
1811
  relativePath: `app/Models/${names.pascal}.php`,
1256
- content: laravelModelTemplate(names, "App\\Models")
1812
+ content: laravelModelTemplate(names, "App\\Models", projectInfo.laravel?.version)
1257
1813
  });
1258
1814
  }
1259
1815
  if (selected.has("controller")) {
@@ -1281,11 +1837,7 @@ async function scaffoldModule(projectInfo, options) {
1281
1837
  });
1282
1838
  }
1283
1839
  if (selected.has("migration")) {
1284
- const timestamp = getMigrationTimestamp();
1285
- filesToGenerate.push({
1286
- relativePath: `database/migrations/${timestamp}_create_${names.pluralSnake}_table.php`,
1287
- content: laravelMigrationTemplate(names)
1288
- });
1840
+ addMigrationIfRequested();
1289
1841
  }
1290
1842
  }
1291
1843
  } else {
@@ -1330,11 +1882,15 @@ async function scaffoldModule(projectInfo, options) {
1330
1882
  if (selected.has("repository")) exports.push(`export * from './${names.kebab}.repository.js';`);
1331
1883
  if (selected.has("route")) exports.push(`export * from './${names.kebab}.routes.js';`);
1332
1884
  if (selected.has("dto")) exports.push(`export * from './${names.kebab}.dto.js';`);
1333
- filesToGenerate.push({
1334
- relativePath: `${moduleFolder}/index.ts`,
1335
- content: `${exports.join("\n")}
1336
- `
1337
- });
1885
+ if (exports.length > 0) {
1886
+ filesToGenerate.push({
1887
+ relativePath: `${moduleFolder}/index.ts`,
1888
+ content: `${exports.join("\n")}
1889
+ `,
1890
+ isBarrel: true,
1891
+ barrelExports: exports
1892
+ });
1893
+ }
1338
1894
  }
1339
1895
  } else {
1340
1896
  if (selected.has("controller")) {
@@ -1369,20 +1925,17 @@ async function scaffoldModule(projectInfo, options) {
1369
1925
  }
1370
1926
  }
1371
1927
  }
1372
- const results = [];
1373
- let createdCount = 0;
1374
- let skippedCount = 0;
1375
- let errorCount = 0;
1376
1928
  for (const item of filesToGenerate) {
1377
- const fullPath = path2.resolve(root, item.relativePath);
1378
- if (fs2.existsSync(fullPath) && !options.forceOverwrite) {
1929
+ const fullPath = path3.resolve(root, item.relativePath);
1930
+ const rel = path3.relative(root, fullPath);
1931
+ if (rel.startsWith("..") || path3.isAbsolute(rel)) {
1379
1932
  results.push({
1380
1933
  filePath: fullPath,
1381
1934
  relativeFilePath: item.relativePath,
1382
- status: "skipped",
1383
- reason: "File already exists (overwrite protected)"
1935
+ status: "error",
1936
+ reason: "Path traversal attempt detected outside project root"
1384
1937
  });
1385
- skippedCount++;
1938
+ errorCount++;
1386
1939
  continue;
1387
1940
  }
1388
1941
  if (options.dryRun) {
@@ -1395,12 +1948,52 @@ async function scaffoldModule(projectInfo, options) {
1395
1948
  createdCount++;
1396
1949
  continue;
1397
1950
  }
1951
+ if (item.isBarrel && item.barrelExports && fs2.existsSync(fullPath)) {
1952
+ try {
1953
+ const existingContent = fs2.readFileSync(fullPath, "utf-8");
1954
+ const missingExports = item.barrelExports.filter(
1955
+ (line) => !existingContent.includes(line)
1956
+ );
1957
+ if (missingExports.length > 0) {
1958
+ const newContent = existingContent.endsWith("\n") ? existingContent + missingExports.join("\n") + "\n" : existingContent + "\n" + missingExports.join("\n") + "\n";
1959
+ fs2.writeFileSync(fullPath, newContent, "utf-8");
1960
+ results.push({
1961
+ filePath: fullPath,
1962
+ relativeFilePath: item.relativePath,
1963
+ status: "created",
1964
+ reason: "Appended exports to existing barrel index"
1965
+ });
1966
+ createdCount++;
1967
+ } else {
1968
+ results.push({
1969
+ filePath: fullPath,
1970
+ relativeFilePath: item.relativePath,
1971
+ status: "skipped",
1972
+ reason: "File already exists and contains all module exports"
1973
+ });
1974
+ skippedCount++;
1975
+ }
1976
+ continue;
1977
+ } catch (err) {
1978
+ results.push({
1979
+ filePath: fullPath,
1980
+ relativeFilePath: item.relativePath,
1981
+ status: "error",
1982
+ reason: err.message || String(err)
1983
+ });
1984
+ errorCount++;
1985
+ continue;
1986
+ }
1987
+ }
1398
1988
  try {
1399
- const parentDir = path2.dirname(fullPath);
1989
+ const parentDir = path3.dirname(fullPath);
1400
1990
  if (!fs2.existsSync(parentDir)) {
1401
1991
  fs2.mkdirSync(parentDir, { recursive: true });
1402
1992
  }
1403
- fs2.writeFileSync(fullPath, item.content, "utf-8");
1993
+ fs2.writeFileSync(fullPath, item.content, {
1994
+ flag: options.forceOverwrite ? "w" : "wx",
1995
+ encoding: "utf-8"
1996
+ });
1404
1997
  results.push({
1405
1998
  filePath: fullPath,
1406
1999
  relativeFilePath: item.relativePath,
@@ -1408,13 +2001,23 @@ async function scaffoldModule(projectInfo, options) {
1408
2001
  });
1409
2002
  createdCount++;
1410
2003
  } catch (err) {
1411
- results.push({
1412
- filePath: fullPath,
1413
- relativeFilePath: item.relativePath,
1414
- status: "error",
1415
- reason: err.message || String(err)
1416
- });
1417
- errorCount++;
2004
+ if (err.code === "EEXIST") {
2005
+ results.push({
2006
+ filePath: fullPath,
2007
+ relativeFilePath: item.relativePath,
2008
+ status: "skipped",
2009
+ reason: "File already exists (overwrite protected)"
2010
+ });
2011
+ skippedCount++;
2012
+ } else {
2013
+ results.push({
2014
+ filePath: fullPath,
2015
+ relativeFilePath: item.relativePath,
2016
+ status: "error",
2017
+ reason: err.message || String(err)
2018
+ });
2019
+ errorCount++;
2020
+ }
1418
2021
  }
1419
2022
  }
1420
2023
  return {
@@ -1423,12 +2026,13 @@ async function scaffoldModule(projectInfo, options) {
1423
2026
  files: results,
1424
2027
  createdCount,
1425
2028
  skippedCount,
1426
- errorCount
2029
+ errorCount,
2030
+ dryRun: Boolean(options.dryRun)
1427
2031
  };
1428
2032
  }
1429
2033
 
1430
2034
  // src/index.ts
1431
- var VERSION = "1.0.1";
2035
+ var VERSION = true ? "1.1.0" : "1.1.0";
1432
2036
  function showHelp() {
1433
2037
  console.log(`
1434
2038
  ${pc.bold(pc.cyan("LaraTS CLI"))} ${pc.gray(`v${VERSION}`)}
@@ -1440,19 +2044,22 @@ ${pc.bold("USAGE:")}
1440
2044
 
1441
2045
  ${pc.bold("COMMANDS:")}
1442
2046
  ${pc.green("scan")} Scan repository & generate ARCHITECTURE.md
2047
+ ${pc.green("view")}, ${pc.green("preview")} Open interactive architecture diagram in local browser
1443
2048
  ${pc.green("scaffold")}, ${pc.green("make")} [name] Scaffold a new module or feature with clean boilerplate
1444
2049
  ${pc.green("info")} Show detected project type & architectural flavor
1445
2050
  ${pc.green("help")} Display this help message
1446
2051
 
1447
2052
  ${pc.bold("OPTIONS:")}
1448
- ${pc.yellow("-o, --output")} <file> Output markdown path for scan (default: ARCHITECTURE.md)
2053
+ ${pc.yellow("-o, --output")} <file> Output path for scan (default: ARCHITECTURE.md)
2054
+ ${pc.yellow("--view")} Open local browser viewer immediately after scanning
2055
+ ${pc.yellow("-t, --tree")} Print directory tree in terminal
2056
+ ${pc.yellow("--html")} Also export standalone ARCHITECTURE.html
1449
2057
  ${pc.yellow("-m, --modular")} Enforce modular directory structure
1450
2058
  ${pc.yellow("-s, --standard")} Enforce standard/layered directory structure
1451
2059
  ${pc.yellow("--dry-run")} Simulate generation without writing files
1452
2060
  ${pc.yellow("-f, --force")} Overwrite files if they already exist
1453
2061
  ${pc.yellow("-v, --version")} Display CLI version
1454
2062
  ${pc.yellow("-h, --help")} Display this help message
1455
-
1456
2063
  ${pc.bold("EXAMPLES:")}
1457
2064
  ${pc.cyan("$")} larats
1458
2065
  ${pc.cyan("$")} larats scan
@@ -1465,41 +2072,165 @@ function handleCancel() {
1465
2072
  p.cancel(pc.yellow("Operation cancelled. See you next time! \u{1F44B}"));
1466
2073
  process.exit(0);
1467
2074
  }
1468
- async function runScan(projectInfo, outputPath = "ARCHITECTURE.md") {
2075
+ function checkAndPromptStar() {
2076
+ try {
2077
+ const configDir = path4.join(os2.homedir(), ".larats");
2078
+ const statsFile = path4.join(configDir, "stats.json");
2079
+ if (!fs3.existsSync(configDir)) {
2080
+ fs3.mkdirSync(configDir, { recursive: true });
2081
+ }
2082
+ let stats = {};
2083
+ if (fs3.existsSync(statsFile)) {
2084
+ stats = JSON.parse(fs3.readFileSync(statsFile, "utf-8"));
2085
+ }
2086
+ const runs = (stats.runs || 0) + 1;
2087
+ stats.runs = runs;
2088
+ fs3.writeFileSync(statsFile, JSON.stringify(stats, null, 2), "utf-8");
2089
+ if (runs === 2 || runs === 3) {
2090
+ console.log(
2091
+ `
2092
+ ${pc.yellow("\u2B50 Enjoying LaraTS?")} If you find this CLI useful, please consider giving it a star on GitHub:
2093
+ ${pc.bold(pc.cyan("\u{1F449} https://github.com/SidiqFatuh187/larats-cli"))}
2094
+ `
2095
+ );
2096
+ }
2097
+ } catch {
2098
+ }
2099
+ }
2100
+ function openViewer(destination) {
2101
+ const startCmd = process.platform === "win32" ? `start "" "${destination}"` : process.platform === "darwin" ? `open "${destination}"` : `xdg-open "${destination}"`;
2102
+ try {
2103
+ exec(startCmd, (err) => {
2104
+ if (err) {
2105
+ p.log.warn(`Could not open browser automatically. Please open ${pc.cyan(destination)} manually.`);
2106
+ }
2107
+ });
2108
+ } catch {
2109
+ p.log.warn(`Could not open browser automatically. Please open ${pc.cyan(destination)} manually.`);
2110
+ }
2111
+ }
2112
+ async function runScan(projectInfo, outputPath = "ARCHITECTURE.md", options = {}) {
1469
2113
  const spinner2 = p.spinner();
1470
2114
  spinner2.start("Scanning codebase structure & components (strict ignore list active)...");
1471
2115
  try {
2116
+ const destination = path4.resolve(projectInfo.rootPath, outputPath);
2117
+ const rel = path4.relative(projectInfo.rootPath, destination);
2118
+ if (rel.startsWith("..") || path4.isAbsolute(rel)) {
2119
+ spinner2.stop(pc.red("Scan failed"));
2120
+ p.log.error(`Refusing to write output: "${outputPath}" resolves outside project root directory.`);
2121
+ process.exitCode = 1;
2122
+ return;
2123
+ }
2124
+ if (fs3.existsSync(destination) && !options.force) {
2125
+ const existingContent = fs3.readFileSync(destination, "utf-8");
2126
+ const isLaraTsFile = existingContent.includes("Auto-generated by **[LaraTS CLI]") || existingContent.includes("Auto-generated by **[ArchMind CLI]") || existingContent.includes("LaraTS CLI") || existingContent.includes("<!-- larats-generated -->");
2127
+ if (!isLaraTsFile) {
2128
+ spinner2.stop(pc.red("Scan refused"));
2129
+ p.log.error(
2130
+ `Destination file "${outputPath}" already exists and does not appear to be generated by LaraTS.
2131
+ To protect user files from accidental overwrites, use --force to overwrite.`
2132
+ );
2133
+ process.exitCode = 1;
2134
+ return;
2135
+ }
2136
+ }
1472
2137
  const scanResult = await scanProject(projectInfo);
2138
+ const nonZeroLayers = Object.values(scanResult.components).filter((c) => c.count > 0).length;
2139
+ const totalComponents = Object.values(scanResult.components).reduce((sum, c) => sum + c.count, 0);
2140
+ const terminalFlow = renderTerminalFlow(scanResult);
2141
+ if (options.dryRun) {
2142
+ spinner2.stop(pc.green(`Scan completed in ${scanResult.scanDurationMs}ms (dry run)`));
2143
+ p.note(
2144
+ `${pc.bold("Architecture Flow:")}
2145
+ ${pc.cyan(terminalFlow)}
2146
+
2147
+ ${pc.bold("Scanned Files:")} ${scanResult.totalFilesScanned}
2148
+ ${pc.bold("Total Components:")} ${totalComponents} across ${nonZeroLayers} active architectural layers
2149
+ ${pc.bold("Modules Detected:")} ${scanResult.modules.length > 0 ? scanResult.modules.join(", ") : "None (monolithic structure)"}
2150
+ ${pc.bold("Simulated Output:")} Would write ARCHITECTURE.md to ${pc.cyan(outputPath)} (dry run: no files written)`,
2151
+ pc.green("Architecture Map (Dry Run) \u{1F5FA}\uFE0F")
2152
+ );
2153
+ if (options.tree) {
2154
+ console.log(`
2155
+ ${pc.bold("Directory Tree:")}
2156
+ ${scanResult.directoryTree}
2157
+ `);
2158
+ }
2159
+ p.outro(pc.cyan("Dry run completed: no files written."));
2160
+ return;
2161
+ }
1473
2162
  spinner2.stop(pc.green(`Scan completed in ${scanResult.scanDurationMs}ms`));
1474
2163
  const markdown = generateArchitectureMarkdown(scanResult);
1475
- const destination = path3.resolve(projectInfo.rootPath, outputPath);
1476
- const parentDir = path3.dirname(destination);
2164
+ const parentDir = path4.dirname(destination);
1477
2165
  if (!fs3.existsSync(parentDir)) {
1478
2166
  fs3.mkdirSync(parentDir, { recursive: true });
1479
2167
  }
1480
2168
  fs3.writeFileSync(destination, markdown, "utf-8");
2169
+ const shouldWriteHtml = options.html || options.view;
2170
+ if (shouldWriteHtml) {
2171
+ const htmlDestination = destination.replace(/\.md$/i, "") + ".html";
2172
+ fs3.writeFileSync(htmlDestination, generateArchitectureHtml(scanResult), "utf-8");
2173
+ p.log.success(`HTML architecture saved to ${pc.cyan(path4.relative(projectInfo.rootPath, htmlDestination) || htmlDestination)}`);
2174
+ if (options.view) {
2175
+ openViewer(htmlDestination);
2176
+ p.log.info(pc.green("\u{1F310} Opened interactive architecture diagram in your browser!"));
2177
+ }
2178
+ }
2179
+ if (options.tree) {
2180
+ console.log(`
2181
+ ${pc.bold("Directory Tree:")}
2182
+ ${scanResult.directoryTree}
2183
+ `);
2184
+ }
1481
2185
  p.note(
1482
- `${pc.bold("Scanned Files:")} ${scanResult.totalFilesScanned}
1483
- ${pc.bold("Components Identified:")} ${Object.keys(scanResult.components).length}
2186
+ `${pc.bold("Architecture Flow:")}
2187
+ ${pc.cyan(terminalFlow)}
2188
+
2189
+ ${pc.bold("Scanned Files:")} ${scanResult.totalFilesScanned}
2190
+ ${pc.bold("Total Components:")} ${totalComponents} across ${nonZeroLayers} active architectural layers
1484
2191
  ${pc.bold("Modules Detected:")} ${scanResult.modules.length > 0 ? scanResult.modules.join(", ") : "None (monolithic structure)"}
1485
- ${pc.bold("Documentation:")} Saved to ${pc.cyan(path3.relative(projectInfo.rootPath, destination) || outputPath)}`,
2192
+ ${pc.bold("Documentation:")} Saved to ${pc.cyan(path4.relative(projectInfo.rootPath, destination) || outputPath)}
2193
+
2194
+ ${pc.bold("\u{1F4A1} How to view diagram:")}
2195
+ \u2022 On GitHub: Push ${outputPath} (Mermaid renders automatically)
2196
+ \u2022 In VS Code: Press ${pc.yellow("Ctrl+Shift+V")} for Markdown Preview
2197
+ \u2022 In Browser: Run ${pc.cyan("npx larats view")} for instant local web preview`,
1486
2198
  pc.green("Architecture Map Generated! \u{1F5FA}\uFE0F")
1487
2199
  );
1488
- p.outro(pc.cyan(`Open ${outputPath} to view your project structure & Mermaid diagram!`));
2200
+ p.outro(pc.cyan(`Done! Open ${outputPath} or run "npx larats view" to inspect architecture!`));
1489
2201
  } catch (err) {
1490
2202
  spinner2.stop(pc.red("Scan failed"));
1491
2203
  p.log.error(err.message || String(err));
2204
+ process.exitCode = 1;
1492
2205
  }
1493
2206
  }
1494
2207
  async function runScaffold(projectInfo, moduleNameArg, cliOptions = {}) {
2208
+ if (projectInfo.type === "unknown") {
2209
+ p.log.warn(pc.yellow("Warning: Project type is unknown (neither Laravel nor TypeScript detected). Scaffolding standard TypeScript/Express components."));
2210
+ } else if (projectInfo.type === "typescript" && projectInfo.typescript?.framework && !["express", "node-vanilla", "unknown"].includes(projectInfo.typescript.framework)) {
2211
+ p.log.warn(
2212
+ pc.yellow(`Warning: Project detected as "${projectInfo.typescript.framework}". LaraTS generates standard Express-compatible TypeScript boilerplate.`)
2213
+ );
2214
+ }
1495
2215
  let moduleName = moduleNameArg;
1496
- if (!moduleName) {
2216
+ if (moduleName) {
2217
+ const errorMsg = validateModuleName(moduleName);
2218
+ if (errorMsg) {
2219
+ p.log.error(pc.red(`Invalid module name: ${errorMsg}`));
2220
+ process.exitCode = 1;
2221
+ return;
2222
+ }
2223
+ } else {
2224
+ if (!process.stdin.isTTY) {
2225
+ p.log.error(pc.red("Error: Module name is required in non-interactive (non-TTY) mode. Example: `larats scaffold Payment`"));
2226
+ process.exitCode = 1;
2227
+ return;
2228
+ }
1497
2229
  const input = await p.text({
1498
2230
  message: "What is the name of the new module or feature?",
1499
2231
  placeholder: "e.g. Payment, Order, UserProfile",
1500
2232
  validate(val) {
1501
- if (!val || !val.trim()) return "Module name cannot be empty.";
1502
- if (!/^[a-zA-Z0-9_-]+$/.test(val.trim())) return "Use only alphanumeric characters, dashes, or underscores.";
2233
+ return validateModuleName(val) || void 0;
1503
2234
  }
1504
2235
  });
1505
2236
  if (p.isCancel(input)) handleCancel();
@@ -1507,42 +2238,68 @@ async function runScaffold(projectInfo, moduleNameArg, cliOptions = {}) {
1507
2238
  }
1508
2239
  let selectedComponents = [];
1509
2240
  if (projectInfo.type === "laravel") {
1510
- const componentsPrompt = await p.multiselect({
1511
- message: `Select components to scaffold for ${pc.cyan(moduleName)}:`,
1512
- options: [
1513
- { value: "model", label: "Eloquent Model", hint: "Includes table name, fillables, and casts" },
1514
- { value: "controller", label: "Resourceful Controller", hint: "Includes standard CRUD methods & DI service" },
1515
- { value: "service", label: "Service Class", hint: "Encapsulated business logic operations" },
1516
- { value: "action", label: "Single Action Class", hint: "Invokable single-responsibility class" },
1517
- { value: "request", label: "Form Request", hint: "Request validation rules & authorization" },
1518
- { value: "migration", label: "Database Migration", hint: "Schema definition with timestamps" }
1519
- ],
1520
- initialValues: ["model", "controller", "service", "request", "migration"],
1521
- required: true
1522
- });
1523
- if (p.isCancel(componentsPrompt)) handleCancel();
1524
- selectedComponents = componentsPrompt;
2241
+ if (!process.stdin.isTTY) {
2242
+ selectedComponents = ["model", "controller", "service", "request", "migration"];
2243
+ } else {
2244
+ const componentsPrompt = await p.multiselect({
2245
+ message: `Select components to scaffold for ${pc.cyan(moduleName)}:`,
2246
+ options: [
2247
+ { value: "model", label: "Eloquent Model", hint: "Includes table name, fillables, and casts" },
2248
+ { value: "controller", label: "Resourceful Controller", hint: "Includes standard CRUD methods & DI service" },
2249
+ { value: "service", label: "Service Class", hint: "Encapsulated business logic operations" },
2250
+ { value: "action", label: "Single Action Class", hint: "Invokable single-responsibility class" },
2251
+ { value: "request", label: "Form Request", hint: "Request validation rules & authorization" },
2252
+ { value: "migration", label: "Database Migration", hint: "Schema definition with timestamps" }
2253
+ ],
2254
+ initialValues: ["model", "controller", "service", "request", "migration"],
2255
+ required: true
2256
+ });
2257
+ if (p.isCancel(componentsPrompt)) handleCancel();
2258
+ selectedComponents = componentsPrompt;
2259
+ }
2260
+ if (selectedComponents.includes("controller") && !selectedComponents.includes("service")) {
2261
+ p.log.warn(pc.yellow('Notice: Controller requires Service class. Auto-adding "service" to scaffolded components.'));
2262
+ selectedComponents.push("service");
2263
+ }
1525
2264
  } else {
1526
- const componentsPrompt = await p.multiselect({
1527
- message: `Select components to scaffold for ${pc.cyan(moduleName)}:`,
1528
- options: [
1529
- { value: "controller", label: "Controller", hint: "Express request handlers & response orchestration" },
1530
- { value: "service", label: "Service", hint: "Application logic & repository coordination" },
1531
- { value: "repository", label: "Repository", hint: "Data persistence abstraction & queries" },
1532
- { value: "route", label: "Router & Routes", hint: "Express router mapping endpoints to controller" },
1533
- { value: "dto", label: "DTOs & Interfaces", hint: "Typed interfaces for entities and payloads" }
1534
- ],
1535
- initialValues: ["controller", "service", "repository", "route", "dto"],
1536
- required: true
1537
- });
1538
- if (p.isCancel(componentsPrompt)) handleCancel();
1539
- selectedComponents = componentsPrompt;
2265
+ if (!process.stdin.isTTY) {
2266
+ selectedComponents = ["controller", "service", "repository", "route", "dto"];
2267
+ } else {
2268
+ const componentsPrompt = await p.multiselect({
2269
+ message: `Select components to scaffold for ${pc.cyan(moduleName)}:`,
2270
+ options: [
2271
+ { value: "controller", label: "Controller", hint: "Express request handlers & response orchestration" },
2272
+ { value: "service", label: "Service", hint: "Application logic & repository coordination" },
2273
+ { value: "repository", label: "Repository", hint: "Data persistence abstraction & queries" },
2274
+ { value: "route", label: "Router & Routes", hint: "Express router mapping endpoints to controller" },
2275
+ { value: "dto", label: "DTOs & Interfaces", hint: "Typed interfaces for entities and payloads" }
2276
+ ],
2277
+ initialValues: ["controller", "service", "repository", "route", "dto"],
2278
+ required: true
2279
+ });
2280
+ if (p.isCancel(componentsPrompt)) handleCancel();
2281
+ selectedComponents = componentsPrompt;
2282
+ }
2283
+ if (selectedComponents.includes("route") && !selectedComponents.includes("controller")) {
2284
+ p.log.warn(pc.yellow('Notice: Route requires Controller. Auto-adding "controller" to scaffolded components.'));
2285
+ selectedComponents.push("controller");
2286
+ }
2287
+ if (selectedComponents.includes("controller") && !selectedComponents.includes("service")) {
2288
+ p.log.warn(pc.yellow('Notice: Controller requires Service. Auto-adding "service" to scaffolded components.'));
2289
+ selectedComponents.push("service");
2290
+ }
2291
+ if (selectedComponents.includes("service") && !selectedComponents.includes("repository")) {
2292
+ p.log.warn(pc.yellow('Notice: Service requires Repository. Auto-adding "repository" to scaffolded components.'));
2293
+ selectedComponents.push("repository");
2294
+ }
1540
2295
  }
1541
2296
  let isModular = true;
1542
2297
  if (cliOptions.modular) {
1543
2298
  isModular = true;
1544
2299
  } else if (cliOptions.standard) {
1545
2300
  isModular = false;
2301
+ } else if (!process.stdin.isTTY) {
2302
+ isModular = projectInfo.type === "laravel" ? Boolean(projectInfo.laravel?.isModular) : projectInfo.type === "typescript" ? Boolean(projectInfo.typescript?.isModular) : true;
1546
2303
  } else {
1547
2304
  const defaultModular = projectInfo.type === "laravel" ? Boolean(projectInfo.laravel?.isModular) : projectInfo.type === "typescript" ? Boolean(projectInfo.typescript?.isModular) : true;
1548
2305
  const structurePrompt = await p.select({
@@ -1585,8 +2342,14 @@ async function runScaffold(projectInfo, moduleNameArg, cliOptions = {}) {
1585
2342
  forceOverwrite: cliOptions.force
1586
2343
  });
1587
2344
  spinner2.stop("Generation finished!");
2345
+ if (result.errorCount > 0) {
2346
+ process.exitCode = 1;
2347
+ }
1588
2348
  const fileLines = result.files.map((f) => {
1589
2349
  if (f.status === "created") {
2350
+ if (cliOptions.dryRun) {
2351
+ return ` ${pc.blue("\u2139")} ${f.relativeFilePath} ${pc.gray("(would create)")}`;
2352
+ }
1590
2353
  return ` ${pc.green("\u2714")} ${f.relativeFilePath} ${pc.gray("(created)")}`;
1591
2354
  } else if (f.status === "skipped") {
1592
2355
  return ` ${pc.yellow("\u26A0")} ${f.relativeFilePath} ${pc.yellow(`(skipped - ${f.reason})`)}`;
@@ -1594,6 +2357,16 @@ async function runScaffold(projectInfo, moduleNameArg, cliOptions = {}) {
1594
2357
  return ` ${pc.red("\u2716")} ${f.relativeFilePath} ${pc.red(`(error - ${f.reason})`)}`;
1595
2358
  }
1596
2359
  });
2360
+ if (cliOptions.dryRun) {
2361
+ p.note(
2362
+ fileLines.join("\n") + `
2363
+
2364
+ ${pc.bold("Dry run summary:")} ${pc.cyan(`${result.createdCount} would be created`)}, ${pc.yellow(`${result.skippedCount} skipped`)}, ${pc.red(`${result.errorCount} errors`)}`,
2365
+ pc.cyan(`Dry run: Scaffolding ${moduleName}`)
2366
+ );
2367
+ p.outro(pc.cyan("Dry run: no files written."));
2368
+ return;
2369
+ }
1597
2370
  p.note(
1598
2371
  fileLines.join("\n") + `
1599
2372
 
@@ -1603,6 +2376,25 @@ ${pc.bold("Summary:")} ${pc.green(`${result.createdCount} created`)}, ${pc.yello
1603
2376
  if (result.skippedCount > 0 && !cliOptions.force) {
1604
2377
  p.log.warn(pc.yellow("Anti-Overwrite Protection prevented modifying existing files. Use --force to overwrite if intended."));
1605
2378
  }
2379
+ const names = getNaming(moduleName);
2380
+ if (result.createdCount > 0) {
2381
+ if (projectInfo.type === "laravel") {
2382
+ p.note(
2383
+ `1. Run database migration:
2384
+ ${pc.cyan("php artisan migrate")}
2385
+ 2. Register routes in routes/api.php or routes/web.php:
2386
+ ${pc.cyan(`Route::apiResource('${names.kebab}', \\${isModular ? `App\\Modules\\${names.pascal}\\Controllers\\` : "App\\Http\\Controllers\\"}${names.pascal}Controller::class);`)}`,
2387
+ "\u{1F680} Next Steps"
2388
+ );
2389
+ } else {
2390
+ p.note(
2391
+ `1. Mount routes into Express app:
2392
+ ${pc.cyan(`import { ${names.camel}Routes } from './${isModular ? `modules/${names.kebab}` : `routes/${names.kebab}.routes.js`}';
2393
+ app.use('/api/${names.kebab}', ${names.camel}Routes);`)}`,
2394
+ "\u{1F680} Next Steps"
2395
+ );
2396
+ }
2397
+ }
1606
2398
  p.outro(pc.green(`\u2728 Successfully scaffolded ${moduleName} module!`));
1607
2399
  }
1608
2400
  function showProjectInfo(projectInfo) {
@@ -1626,34 +2418,99 @@ ${pc.bold("Modular Src:")} ${projectInfo.typescript.isModular ? pc.green("Yes")
1626
2418
  p.note(details, pc.cyan("Detected Project Architecture"));
1627
2419
  }
1628
2420
  async function main() {
1629
- const args = process.argv.slice(2);
1630
- if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
2421
+ let parsed;
2422
+ try {
2423
+ parsed = parseArgs({
2424
+ args: process.argv.slice(2),
2425
+ options: {
2426
+ output: { type: "string", short: "o" },
2427
+ modular: { type: "boolean", short: "m" },
2428
+ standard: { type: "boolean", short: "s" },
2429
+ force: { type: "boolean", short: "f" },
2430
+ "dry-run": { type: "boolean" },
2431
+ view: { type: "boolean" },
2432
+ tree: { type: "boolean", short: "t" },
2433
+ html: { type: "boolean" },
2434
+ version: { type: "boolean", short: "v" },
2435
+ help: { type: "boolean", short: "h" }
2436
+ },
2437
+ allowPositionals: true,
2438
+ strict: true
2439
+ });
2440
+ } catch (err) {
2441
+ console.error(pc.red(`Error: ${err.message}`));
1631
2442
  showHelp();
2443
+ process.exitCode = 1;
1632
2444
  return;
1633
2445
  }
1634
- if (args.includes("--version") || args.includes("-v")) {
2446
+ if (parsed.values.help || parsed.positionals[0] === "help") {
2447
+ showHelp();
2448
+ return;
2449
+ }
2450
+ if (parsed.values.version || parsed.positionals[0] === "version") {
1635
2451
  console.log(`larats v${VERSION}`);
1636
2452
  return;
1637
2453
  }
1638
- let outputPath = "ARCHITECTURE.md";
1639
- const outputIdx = args.findIndex((a) => a === "--output" || a === "-o");
1640
- if (outputIdx !== -1 && args[outputIdx + 1]) {
1641
- outputPath = args[outputIdx + 1];
2454
+ const command = parsed.positionals[0];
2455
+ const VALID_COMMANDS = {
2456
+ scan: true,
2457
+ view: true,
2458
+ preview: true,
2459
+ scaffold: true,
2460
+ make: true,
2461
+ info: true,
2462
+ help: true
2463
+ };
2464
+ if (command && !VALID_COMMANDS[command]) {
2465
+ console.error(pc.red(`Error: Unknown command "${command}".`));
2466
+ showHelp();
2467
+ process.exitCode = 1;
2468
+ return;
1642
2469
  }
1643
- const isModularFlag = args.includes("--modular") || args.includes("-m");
1644
- const isStandardFlag = args.includes("--standard") || args.includes("-s");
1645
- const isForceFlag = args.includes("--force") || args.includes("-f");
1646
- const isDryRunFlag = args.includes("--dry-run");
1647
- const command = args[0] && !args[0].startsWith("-") ? args[0] : null;
2470
+ const outputPath = parsed.values.output || "ARCHITECTURE.md";
2471
+ const isModularFlag = parsed.values.modular;
2472
+ const isStandardFlag = parsed.values.standard;
2473
+ const isForceFlag = parsed.values.force;
2474
+ const isDryRunFlag = parsed.values["dry-run"];
2475
+ const isViewFlag = parsed.values.view;
2476
+ const isTreeFlag = parsed.values.tree;
2477
+ const isHtmlFlag = parsed.values.html;
1648
2478
  const projectInfo = await detectProjectType(process.cwd());
1649
2479
  if (command === "scan") {
1650
2480
  p.intro(`${pc.bgCyan(pc.black(" LaraTS "))} ${pc.gray(`v${VERSION}`)} - Scanning Architecture`);
1651
2481
  p.log.info(`Detected: ${pc.green(projectInfo.summary)}`);
1652
- await runScan(projectInfo, outputPath);
2482
+ await runScan(projectInfo, outputPath, {
2483
+ force: isForceFlag,
2484
+ dryRun: isDryRunFlag,
2485
+ view: isViewFlag,
2486
+ tree: isTreeFlag,
2487
+ html: isHtmlFlag
2488
+ });
2489
+ checkAndPromptStar();
2490
+ return;
2491
+ }
2492
+ if (command === "view" || command === "preview") {
2493
+ p.intro(`${pc.bgCyan(pc.black(" LaraTS "))} ${pc.gray(`v${VERSION}`)} - Architecture Viewer`);
2494
+ p.log.info(`Target: ${pc.green(projectInfo.summary)}`);
2495
+ const spinner2 = p.spinner();
2496
+ spinner2.start("Scanning repository & generating architecture map...");
2497
+ const scanResult = await scanProject(projectInfo);
2498
+ spinner2.stop(pc.green(`Scanned ${scanResult.totalFilesScanned} files in ${scanResult.scanDurationMs}ms`));
2499
+ const htmlDestination = path4.resolve(projectInfo.rootPath, "ARCHITECTURE.html");
2500
+ fs3.writeFileSync(htmlDestination, generateArchitectureHtml(scanResult), "utf-8");
2501
+ openViewer(htmlDestination);
2502
+ p.note(
2503
+ `${pc.bold("File:")} ${pc.cyan("ARCHITECTURE.html")}
2504
+ Interactive Mermaid.js flowchart & component breakdown loaded in your browser.
2505
+ Double-click ${pc.cyan("ARCHITECTURE.html")} anytime to view offline!`,
2506
+ pc.green("Opened in Browser! \u{1F310}")
2507
+ );
2508
+ p.outro(pc.cyan("Done!"));
2509
+ checkAndPromptStar();
1653
2510
  return;
1654
2511
  }
1655
2512
  if (command === "scaffold" || command === "make") {
1656
- const moduleName = args[1] && !args[1].startsWith("-") ? args[1] : void 0;
2513
+ const moduleName = parsed.positionals[1];
1657
2514
  p.intro(`${pc.bgCyan(pc.black(" LaraTS "))} ${pc.gray(`v${VERSION}`)} - Module Scaffolder`);
1658
2515
  p.log.info(`Target Project: ${pc.green(projectInfo.summary)}`);
1659
2516
  await runScaffold(projectInfo, moduleName, {
@@ -1662,12 +2519,18 @@ async function main() {
1662
2519
  force: isForceFlag,
1663
2520
  dryRun: isDryRunFlag
1664
2521
  });
2522
+ checkAndPromptStar();
1665
2523
  return;
1666
2524
  }
1667
2525
  if (command === "info") {
1668
2526
  p.intro(`${pc.bgCyan(pc.black(" LaraTS "))} ${pc.gray(`v${VERSION}`)}`);
1669
2527
  showProjectInfo(projectInfo);
1670
2528
  p.outro(pc.cyan("Done."));
2529
+ checkAndPromptStar();
2530
+ return;
2531
+ }
2532
+ if (!process.stdin.isTTY) {
2533
+ showHelp();
1671
2534
  return;
1672
2535
  }
1673
2536
  p.intro(`${pc.bgCyan(pc.black(" LaraTS "))} ${pc.bold("Smart Architecture Visualizer & Scaffolder")} ${pc.gray(`v${VERSION}`)}`);
@@ -1681,6 +2544,11 @@ async function main() {
1681
2544
  label: "\u{1F50D} Scan Architecture & Generate ARCHITECTURE.md",
1682
2545
  hint: "Creates component stats, directory tree & Mermaid flowchart"
1683
2546
  },
2547
+ {
2548
+ value: "view",
2549
+ label: "\u{1F310} Open Interactive Architecture Viewer in Browser",
2550
+ hint: "Spawns local browser tab with Mermaid flowchart & stats"
2551
+ },
1684
2552
  {
1685
2553
  value: "scaffold",
1686
2554
  label: "\u{1F3D7}\uFE0F Scaffold New Feature / Module",
@@ -1701,7 +2569,22 @@ async function main() {
1701
2569
  handleCancel();
1702
2570
  }
1703
2571
  if (action === "scan") {
1704
- await runScan(projectInfo, outputPath);
2572
+ await runScan(projectInfo, outputPath, {
2573
+ force: isForceFlag,
2574
+ dryRun: isDryRunFlag,
2575
+ view: isViewFlag,
2576
+ tree: isTreeFlag,
2577
+ html: isHtmlFlag
2578
+ });
2579
+ } else if (action === "view") {
2580
+ const spinner2 = p.spinner();
2581
+ spinner2.start("Scanning repository & generating architecture map...");
2582
+ const scanResult = await scanProject(projectInfo);
2583
+ spinner2.stop(pc.green(`Scanned in ${scanResult.scanDurationMs}ms`));
2584
+ const htmlDestination = path4.resolve(projectInfo.rootPath, "ARCHITECTURE.html");
2585
+ fs3.writeFileSync(htmlDestination, generateArchitectureHtml(scanResult), "utf-8");
2586
+ openViewer(htmlDestination);
2587
+ p.outro(pc.green("\u{1F310} Opened interactive architecture diagram in your browser!"));
1705
2588
  } else if (action === "scaffold") {
1706
2589
  await runScaffold(projectInfo, void 0, {
1707
2590
  modular: isModularFlag,
@@ -1713,9 +2596,13 @@ async function main() {
1713
2596
  showProjectInfo(projectInfo);
1714
2597
  p.outro(pc.cyan("Done!"));
1715
2598
  }
2599
+ checkAndPromptStar();
1716
2600
  }
1717
2601
  main().catch((err) => {
1718
2602
  p.log.error(pc.red(`Fatal error: ${err.message || String(err)}`));
1719
2603
  process.exit(1);
1720
2604
  });
2605
+ export {
2606
+ checkAndPromptStar
2607
+ };
1721
2608
  //# sourceMappingURL=index.js.map