larats 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,7 +2,8 @@
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 { parseArgs } from "util";
6
7
  import * as p from "@clack/prompts";
7
8
  import pc from "picocolors";
8
9
 
@@ -12,7 +13,7 @@ import path from "path";
12
13
  function readJsonSafe(filePath) {
13
14
  try {
14
15
  if (!fs.existsSync(filePath)) return null;
15
- const content = fs.readFileSync(filePath, "utf-8");
16
+ const content = fs.readFileSync(filePath, "utf-8").replace(/^\uFEFF/, "");
16
17
  return JSON.parse(content);
17
18
  } catch {
18
19
  return null;
@@ -39,11 +40,13 @@ async function detectProjectType(targetDir = process.cwd()) {
39
40
  const hasInertia = "inertiajs/inertia-laravel" in allDeps;
40
41
  const hasSanctum = "laravel/sanctum" in allDeps;
41
42
  const rawVersion = allDeps["laravel/framework"] || "";
42
- const cleanVersion = rawVersion.replace(/[\^~><=]/g, "").trim();
43
+ const versionMatch = rawVersion.match(/\d+(?:\.\d+)*/);
44
+ const cleanVersion = versionMatch ? versionMatch[0] : "";
43
45
  const appModulesDir = path.join(resolvedDir, "app", "Modules");
44
46
  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;
47
+ const rootModulesCapDir = path.join(resolvedDir, "Modules");
48
+ const isModular = fs.existsSync(appModulesDir) || fs.existsSync(rootModulesDir) || fs.existsSync(rootModulesCapDir);
49
+ const modulesDir = fs.existsSync(appModulesDir) ? "app/Modules" : fs.existsSync(rootModulesDir) ? "modules" : fs.existsSync(rootModulesCapDir) ? "Modules" : void 0;
47
50
  const extras = [];
48
51
  if (hasFilament) extras.push("Filament");
49
52
  if (hasLivewire) extras.push("Livewire");
@@ -125,6 +128,8 @@ async function detectProjectType(targetDir = process.cwd()) {
125
128
  }
126
129
 
127
130
  // src/scanner.ts
131
+ import path2 from "path";
132
+ import os from "os";
128
133
  import fg from "fast-glob";
129
134
  var SAFE_IGNORE_LIST = [
130
135
  // Dependencies & package management
@@ -136,32 +141,42 @@ var SAFE_IGNORE_LIST = [
136
141
  "**/.git/**",
137
142
  "**/.svn/**",
138
143
  "**/.hg/**",
139
- // Storage, cache & build outputs
144
+ // Storage, cache & build outputs (scoped to root to preserve src/build/)
140
145
  "**/storage/**",
141
146
  "**/dist/**",
142
- "**/build/**",
143
- "**/out/**",
147
+ "build/**",
148
+ "out/**",
144
149
  "**/.next/**",
145
150
  "**/.nuxt/**",
146
151
  "**/.turbo/**",
147
152
  "**/.cache/**",
148
153
  "**/coverage/**",
149
154
  "**/.temp/**",
150
- "**/tmp/**",
155
+ "tmp/**",
151
156
  // IDEs & OS files
152
157
  "**/.idea/**",
153
158
  "**/.vscode/**",
154
159
  "**/.DS_Store",
155
160
  "**/Thumbs.db",
156
- // Secrets & environment configurations
161
+ // Secrets, private keys & cloud credentials
157
162
  "**/.env*",
158
163
  "**/*.pem",
159
164
  "**/*.key",
160
165
  "**/*.pfx",
166
+ "**/*.p12",
167
+ "**/*.jks",
168
+ "**/*.keystore",
161
169
  "**/*.cert",
162
170
  "**/*.crt",
163
171
  "**/*.secret",
164
- "**/*.id_rsa",
172
+ "**/secrets/**",
173
+ "**/.aws/**",
174
+ "**/.ssh/**",
175
+ "**/credentials*.json",
176
+ "**/*service-account*.json",
177
+ "**/id_rsa*",
178
+ "**/id_ed25519*",
179
+ "**/id_ecdsa*",
165
180
  "**/*.pub",
166
181
  // Logs & Lock files
167
182
  "**/*.log",
@@ -171,6 +186,9 @@ var SAFE_IGNORE_LIST = [
171
186
  "**/pnpm-lock.yaml",
172
187
  "**/composer.lock"
173
188
  ];
189
+ function localeIndependentCompare(a, b) {
190
+ return a < b ? -1 : a > b ? 1 : 0;
191
+ }
174
192
  function buildCleanDirectoryTree(files, maxDepth = 3) {
175
193
  const root = { name: "", isDir: true, children: /* @__PURE__ */ new Map() };
176
194
  for (const file of files) {
@@ -194,7 +212,7 @@ function buildCleanDirectoryTree(files, maxDepth = 3) {
194
212
  if (currentDepth > maxDepth) return;
195
213
  const entries = Array.from(node.children.values()).sort((a, b) => {
196
214
  if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
197
- return a.name.localeCompare(b.name);
215
+ return localeIndependentCompare(a.name, b.name);
198
216
  });
199
217
  if (currentDepth === maxDepth && entries.length > 0) {
200
218
  lines.push(`${prefix}\u2514\u2500\u2500 ... (${entries.length} items hidden)`);
@@ -219,7 +237,7 @@ function buildCleanDirectoryTree(files, maxDepth = 3) {
219
237
  }
220
238
  const topLevel = Array.from(root.children.values()).sort((a, b) => {
221
239
  if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
222
- return a.name.localeCompare(b.name);
240
+ return localeIndependentCompare(a.name, b.name);
223
241
  });
224
242
  topLevel.forEach((child, index) => {
225
243
  const isLast = index === topLevel.length - 1;
@@ -236,52 +254,76 @@ function buildCleanDirectoryTree(files, maxDepth = 3) {
236
254
  async function scanProject(projectInfo) {
237
255
  const startTime = Date.now();
238
256
  const root = projectInfo.rootPath;
257
+ const resolvedRoot = path2.resolve(root);
258
+ const parsedRoot = path2.parse(resolvedRoot);
259
+ if (parsedRoot.root === resolvedRoot || typeof os.homedir === "function" && resolvedRoot === os.homedir()) {
260
+ throw new Error(`Refusing to scan system drive or home directory root directly: "${resolvedRoot}".`);
261
+ }
239
262
  const allFiles = await fg("**/*", {
240
263
  cwd: root,
241
264
  ignore: SAFE_IGNORE_LIST,
242
265
  dot: false,
243
- onlyFiles: true
266
+ onlyFiles: true,
267
+ followSymbolicLinks: false,
268
+ suppressErrors: true
244
269
  });
245
- const normalizedFiles = allFiles.map((f) => f.replace(/\\/g, "/"));
270
+ const MAX_FILES_CAP = 1e4;
271
+ let normalizedFiles = allFiles.map((f) => f.replace(/\\/g, "/"));
272
+ if (normalizedFiles.length > MAX_FILES_CAP) {
273
+ console.warn(`[WARN] Large codebase detected (${normalizedFiles.length} files). Capping scan analysis to first ${MAX_FILES_CAP} files.`);
274
+ normalizedFiles = normalizedFiles.slice(0, MAX_FILES_CAP);
275
+ }
246
276
  const components = {};
247
277
  const modulesSet = /* @__PURE__ */ new Set();
248
278
  if (projectInfo.type === "laravel") {
249
- const models = normalizedFiles.filter((f) => /^app\/(Models\/.*|.*Model)\.php$/i.test(f));
279
+ const models = normalizedFiles.filter(
280
+ (f) => /^(?:app|modules|Modules)\/(?:.*\/)?Models\/.*\.php$/i.test(f)
281
+ );
250
282
  components["Models"] = {
251
283
  name: "Eloquent Models",
252
284
  count: models.length,
253
285
  files: models,
254
286
  description: "Database entity mappings and business logic models"
255
287
  };
256
- const controllers = normalizedFiles.filter((f) => /^app\/Http\/Controllers\/.*\.php$/i.test(f));
288
+ const controllers = normalizedFiles.filter(
289
+ (f) => /^(?:app|modules|Modules)\/(?:.*\/)?Controllers\/.*\.php$/i.test(f)
290
+ );
257
291
  components["Controllers"] = {
258
292
  name: "HTTP Controllers",
259
293
  count: controllers.length,
260
294
  files: controllers,
261
295
  description: "Request handling and response orchestration"
262
296
  };
263
- const services = normalizedFiles.filter((f) => /^app\/(Services|Domain\/.*\/Services)\/.*\.php$/i.test(f));
297
+ const services = normalizedFiles.filter(
298
+ (f) => /^(?:app|modules|Modules)\/(?:.*\/)?Services\/.*\.php$/i.test(f)
299
+ );
264
300
  components["Services"] = {
265
301
  name: "Service Classes",
266
302
  count: services.length,
267
303
  files: services,
268
304
  description: "Encapsulated business logic and external integrations"
269
305
  };
270
- const actions = normalizedFiles.filter((f) => /^app\/(Actions|Domain\/.*\/Actions)\/.*\.php$/i.test(f));
306
+ const actions = normalizedFiles.filter(
307
+ (f) => /^(?:app|modules|Modules)\/(?:.*\/)?Actions\/.*\.php$/i.test(f)
308
+ );
271
309
  components["Actions"] = {
272
310
  name: "Action Classes",
273
311
  count: actions.length,
274
312
  files: actions,
275
313
  description: "Single-responsibility invokable operations"
276
314
  };
277
- const requests = normalizedFiles.filter((f) => /^app\/Http\/Requests\/.*\.php$/i.test(f));
315
+ const requests = normalizedFiles.filter(
316
+ (f) => /^(?:app|modules|Modules)\/(?:.*\/)?(?:Http\/)?Requests\/.*\.php$/i.test(f)
317
+ );
278
318
  components["Form Requests"] = {
279
319
  name: "Form Requests",
280
320
  count: requests.length,
281
321
  files: requests,
282
322
  description: "Input validation and authorization rules"
283
323
  };
284
- const filament = normalizedFiles.filter((f) => /^app\/Filament\/.*\.php$/i.test(f));
324
+ const filament = normalizedFiles.filter(
325
+ (f) => /^(?:app|modules|Modules)\/(?:.*\/)?Filament\/.*\.php$/i.test(f)
326
+ );
285
327
  if (filament.length > 0 || projectInfo.laravel?.hasFilament) {
286
328
  components["Filament Resources"] = {
287
329
  name: "Filament Admin Resources",
@@ -290,55 +332,74 @@ async function scanProject(projectInfo) {
290
332
  description: "Admin panel CRUD pages, widgets, and schemas"
291
333
  };
292
334
  }
293
- const routes = normalizedFiles.filter((f) => /^routes\/.*\.php$/i.test(f));
335
+ const routes = normalizedFiles.filter(
336
+ (f) => /^(?:routes\/.*\.php|(?:app\/Modules|modules|Modules)\/.*\/routes?.*\.php)$/i.test(f)
337
+ );
294
338
  components["Routes"] = {
295
339
  name: "Routing Definitions",
296
340
  count: routes.length,
297
341
  files: routes,
298
342
  description: "Web, API, and console endpoint routes"
299
343
  };
300
- const migrations = normalizedFiles.filter((f) => /^database\/migrations\/.*\.php$/i.test(f));
344
+ const migrations = normalizedFiles.filter(
345
+ (f) => /(?:^database\/migrations\/.*\.php$|\/migrations\/.*\.php$)/i.test(f)
346
+ );
301
347
  components["Migrations"] = {
302
348
  name: "Database Migrations",
303
349
  count: migrations.length,
304
350
  files: migrations,
305
351
  description: "Schema version control and table definitions"
306
352
  };
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]);
353
+ const modDirPattern = projectInfo.laravel?.modulesDir ? `(?:${projectInfo.laravel.modulesDir}|app/Modules|modules|Modules)` : "(?:app/Modules|modules|Modules)";
354
+ const modRegex = new RegExp(`^${modDirPattern}/([^/]+)/`, "i");
355
+ for (const f of normalizedFiles) {
356
+ const match = f.match(modRegex);
357
+ if (match && match[1]) {
358
+ modulesSet.add(match[1]);
359
+ }
311
360
  }
312
361
  } else if (projectInfo.type === "typescript") {
313
- const controllers = normalizedFiles.filter((f) => /(?:^src\/.*(?:\.|\/)controller\.(?:ts|js)$)/i.test(f));
362
+ const srcDir = projectInfo.typescript?.srcDir ?? "src";
363
+ const tsPrefix = `(?:${srcDir}|(?:apps|packages)/[^/]+/(?:${srcDir}|src))`;
364
+ const controllers = normalizedFiles.filter(
365
+ (f) => !f.endsWith(".d.ts") && new RegExp(`^${tsPrefix}/(?:.*/)?(?:controllers/.*\\.(?:ts|js|mts|cts)|.*[./]controller\\.(?:ts|js|mts|cts))$`, "i").test(f)
366
+ );
314
367
  components["Controllers"] = {
315
368
  name: "Controllers",
316
369
  count: controllers.length,
317
370
  files: controllers,
318
371
  description: "HTTP request handlers and response dispatchers"
319
372
  };
320
- const services = normalizedFiles.filter((f) => /(?:^src\/.*(?:\.|\/)service\.(?:ts|js)$)/i.test(f));
373
+ const services = normalizedFiles.filter(
374
+ (f) => !f.endsWith(".d.ts") && new RegExp(`^${tsPrefix}/(?:.*/)?(?:services/.*\\.(?:ts|js|mts|cts)|.*[./]service\\.(?:ts|js|mts|cts))$`, "i").test(f)
375
+ );
321
376
  components["Services"] = {
322
377
  name: "Services",
323
378
  count: services.length,
324
379
  files: services,
325
380
  description: "Core domain logic and application workflows"
326
381
  };
327
- const repositories = normalizedFiles.filter((f) => /(?:^src\/.*(?:\.|\/)repository\.(?:ts|js)$)/i.test(f));
382
+ const repositories = normalizedFiles.filter(
383
+ (f) => !f.endsWith(".d.ts") && new RegExp(`^${tsPrefix}/(?:.*/)?(?:repositories/.*\\.(?:ts|js|mts|cts)|.*[./]repository\\.(?:ts|js|mts|cts))$`, "i").test(f)
384
+ );
328
385
  components["Repositories"] = {
329
386
  name: "Repositories",
330
387
  count: repositories.length,
331
388
  files: repositories,
332
389
  description: "Data access abstraction and database queries"
333
390
  };
334
- const routes = normalizedFiles.filter((f) => /(?:^src\/.*(?:\.|\/)routes?\.(?:ts|js)$)/i.test(f));
391
+ const routes = normalizedFiles.filter(
392
+ (f) => !f.endsWith(".d.ts") && new RegExp(`^${tsPrefix}/(?:.*/)?(?:routes/.*\\.(?:ts|js|mts|cts)|.*[./]routes?\\.(?:ts|js|mts|cts))$`, "i").test(f)
393
+ );
335
394
  components["Routes"] = {
336
395
  name: "Routes",
337
396
  count: routes.length,
338
397
  files: routes,
339
398
  description: "API endpoint mappings and router handlers"
340
399
  };
341
- const dtos = normalizedFiles.filter((f) => /(?:^src\/.*(?:\.|\/)(?:dto|schema)\.(?:ts|js)$)/i.test(f));
400
+ const dtos = normalizedFiles.filter(
401
+ (f) => !f.endsWith(".d.ts") && new RegExp(`^${tsPrefix}/(?:.*/)?(?:(?:dtos|schemas)/.*\\.(?:ts|js|mts|cts)|.*[./](?:dto|schema)\\.(?:ts|js|mts|cts))$`, "i").test(f)
402
+ );
342
403
  components["DTOs & Schemas"] = {
343
404
  name: "DTOs & Schemas",
344
405
  count: dtos.length,
@@ -346,7 +407,7 @@ async function scanProject(projectInfo) {
346
407
  description: "Data transfer objects and request validation schemas"
347
408
  };
348
409
  const models = normalizedFiles.filter(
349
- (f) => /(?:^src\/.*(?:\.|\/)(?:model|entity)\.(?:ts|js)$|^prisma\/schema\.prisma$)/i.test(f)
410
+ (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
411
  );
351
412
  components["Models & Entities"] = {
352
413
  name: "Models & Entities",
@@ -354,7 +415,9 @@ async function scanProject(projectInfo) {
354
415
  files: models,
355
416
  description: "Database schema definitions and entity declarations"
356
417
  };
357
- const componentsFiles = normalizedFiles.filter((f) => /^src\/(?:components|ui)\/.*\.(?:tsx|jsx|vue|ts|js)$/i.test(f));
418
+ const componentsFiles = normalizedFiles.filter(
419
+ (f) => new RegExp(`^${tsPrefix}/(?:components|ui)/.*\\.(?:tsx|jsx|vue|svelte|ts|js)$`, "i").test(f)
420
+ );
358
421
  if (componentsFiles.length > 0) {
359
422
  components["UI Components"] = {
360
423
  name: "UI Components",
@@ -363,14 +426,39 @@ async function scanProject(projectInfo) {
363
426
  description: "Reusable user interface views and components"
364
427
  };
365
428
  }
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]);
429
+ const moduleRegex = new RegExp(`^(?:${tsPrefix}/modules|modules)/([^/]+)/`, "i");
430
+ for (const mf of normalizedFiles) {
431
+ const match = mf.match(moduleRegex);
432
+ if (match && match[1]) {
433
+ modulesSet.add(match[1]);
434
+ }
435
+ }
436
+ const totalWebLayers = Object.values(components).reduce((acc, c) => acc + c.count, 0);
437
+ if (totalWebLayers === 0) {
438
+ const srcFiles = normalizedFiles.filter(
439
+ (f) => new RegExp(`^${tsPrefix}/`, "i").test(f)
440
+ );
441
+ const testFiles = normalizedFiles.filter(
442
+ (f) => /(?:^|[._-])(?:test|spec)\.[a-z]+$|(?:^|\/)__tests__\//i.test(f)
443
+ );
444
+ components["Source Files"] = {
445
+ name: "Source Files",
446
+ count: srcFiles.length,
447
+ files: srcFiles,
448
+ description: "Main application source code"
449
+ };
450
+ components["Test Files"] = {
451
+ name: "Test Files",
452
+ count: testFiles.length,
453
+ files: testFiles,
454
+ description: "Automated test suites and specifications"
455
+ };
370
456
  }
371
457
  } else {
372
458
  const srcFiles = normalizedFiles.filter((f) => /^src\//i.test(f));
373
- const testFiles = normalizedFiles.filter((f) => /(?:test|spec)\.[a-z]+$/i.test(f));
459
+ const testFiles = normalizedFiles.filter(
460
+ (f) => /(?:^|[._-])(?:test|spec)\.[a-z]+$|(?:^|\/)__tests__\//i.test(f)
461
+ );
374
462
  components["Source Files"] = {
375
463
  name: "Source Files",
376
464
  count: srcFiles.length,
@@ -390,20 +478,66 @@ async function scanProject(projectInfo) {
390
478
  projectInfo,
391
479
  totalFilesScanned: normalizedFiles.length,
392
480
  components,
393
- modules: Array.from(modulesSet).sort(),
481
+ modules: Array.from(modulesSet).sort(localeIndependentCompare),
394
482
  directoryTree,
395
483
  scanDurationMs
396
484
  };
397
485
  }
398
486
 
399
487
  // src/generator/markdown.ts
488
+ function mdCell(str) {
489
+ if (!str) return "-";
490
+ const cleaned = str.replace(/\|/g, "\\|").replace(/[\r\n]+/g, " ").trim();
491
+ return cleaned || "-";
492
+ }
493
+ function fence(body, lang = "") {
494
+ const matches = body.match(/`+/g);
495
+ let maxBackticks = 2;
496
+ if (matches) {
497
+ for (const match of matches) {
498
+ if (match.length > maxBackticks) maxBackticks = match.length;
499
+ }
500
+ }
501
+ const f = "`".repeat(maxBackticks + 1);
502
+ return `${f}${lang ? lang : ""}
503
+ ${body}
504
+ ${f}`;
505
+ }
506
+ function mermaidLabel(str, maxLen = 40) {
507
+ if (!str) return "";
508
+ let cleaned = str.replace(/"/g, "'").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/`/g, "'").replace(/[\r\n]+/g, " ").trim();
509
+ if (cleaned.length > maxLen) {
510
+ cleaned = cleaned.slice(0, maxLen - 3) + "...";
511
+ }
512
+ return cleaned;
513
+ }
514
+ function renderModulesSubgraph(modules) {
515
+ if (modules.length === 0) return "";
516
+ let output = `
517
+ subgraph ModulesLayer ["\u{1F4E6} Detected Modules (${modules.length})"]
518
+ `;
519
+ modules.slice(0, 6).forEach((mod, idx) => {
520
+ output += ` Mod_${idx}["${mermaidLabel(mod)} Module"]:::service
521
+ `;
522
+ });
523
+ if (modules.length > 6) {
524
+ output += ` Mod_More["...and ${modules.length - 6} more modules"]:::service
525
+ `;
526
+ }
527
+ output += ` end
528
+ `;
529
+ return output;
530
+ }
400
531
  function generateMermaidDiagram(scanResult) {
401
532
  const { projectInfo, components, modules } = scanResult;
402
533
  if (projectInfo.type === "laravel") {
403
534
  const hasFilament = projectInfo.laravel?.hasFilament || (components["Filament Resources"]?.count ?? 0) > 0;
404
535
  const hasActions = (components["Actions"]?.count ?? 0) > 0;
405
536
  const hasServices = (components["Services"]?.count ?? 0) > 0;
406
- let diagram = `flowchart TD
537
+ const hasRequests = (components["Form Requests"]?.count ?? 0) > 0;
538
+ const hasModels = (components["Models"]?.count ?? 0) > 0;
539
+ const hasMigrations = (components["Migrations"]?.count ?? 0) > 0;
540
+ let diagram2 = `flowchart TD
407
541
  %% Styling Definitions
408
542
  classDef client fill:#E0F2FE,stroke:#0284C7,stroke-width:2px,color:#0369A1;
409
543
  classDef routing fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#B45309;
@@ -412,92 +546,155 @@ function generateMermaidDiagram(scanResult) {
412
546
  classDef data fill:#FEE2E2,stroke:#DC2626,stroke-width:2px,color:#B91C1C;
413
547
  classDef db fill:#F1F5F9,stroke:#475569,stroke-width:2px,color:#334155;
414
548
 
415
- subgraph Layer1 ["1. Client & Entrypoints"]
549
+ subgraph EntryLayer ["1. Client & Entrypoints"]
416
550
  Client["Client / Browser / API Consumer"]:::client
417
- Routes["HTTP Routes<br/>(routes/web.php, routes/api.php)"]:::routing
551
+ Routes["HTTP Routes"]:::routing
418
552
  end
419
-
420
- subgraph Layer2 ["2. Request Pipeline & Security"]
553
+ `;
554
+ if (hasRequests) {
555
+ diagram2 += `
556
+ subgraph RequestLayer ["2. Request Pipeline & Validation"]
421
557
  Middleware["Middleware Pipeline<br/>(Auth, CSRF, Rate Limiting)"]:::routing
422
558
  FormRequests["Form Requests<br/>(Validation & Authorization)"]:::routing
423
559
  end
424
-
425
- subgraph Layer3 ["3. Controllers & Presentation"]
426
- Controllers["HTTP Controllers<br/>(app/Http/Controllers)"]:::controller
560
+ `;
561
+ }
562
+ diagram2 += `
563
+ subgraph HttpLayer ["3. Controllers & Presentation"]
564
+ Controllers["HTTP Controllers"]:::controller
427
565
  `;
428
566
  if (hasFilament) {
429
- diagram += ` Filament["Filament Admin Panel<br/>(app/Filament/Resources)"]:::controller
567
+ diagram2 += ` Filament["Filament Admin Resources"]:::controller
430
568
  `;
431
569
  }
432
- diagram += ` end
433
-
434
- subgraph Layer4 ["4. Domain & Business Logic"]
570
+ diagram2 += ` end
435
571
  `;
436
- if (hasServices || !hasActions) {
437
- diagram += ` Services["Service Layer<br/>(app/Services)"]:::service
572
+ if (hasServices || hasActions) {
573
+ diagram2 += `
574
+ subgraph DomainLayer ["4. Domain & Business Logic"]
575
+ `;
576
+ if (hasServices) {
577
+ diagram2 += ` Services["Service Classes"]:::service
578
+ `;
579
+ }
580
+ if (hasActions) {
581
+ diagram2 += ` Actions["Action Classes"]:::service
582
+ `;
583
+ }
584
+ diagram2 += ` end
438
585
  `;
439
586
  }
440
- if (hasActions) {
441
- diagram += ` Actions["Action Classes<br/>(app/Actions)"]:::service
587
+ diagram2 += `
588
+ subgraph DataLayer ["5. Data Persistence & Storage"]
589
+ `;
590
+ if (hasModels) {
591
+ diagram2 += ` Models["Eloquent Models"]:::data
442
592
  `;
443
593
  }
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
594
+ diagram2 += ` Database[("Database<br/>(MySQL / Postgres / SQLite)")]:::db
595
+ `;
596
+ if (hasMigrations) {
597
+ diagram2 += ` Migrations["Database Migrations"]:::data
598
+ `;
599
+ }
600
+ diagram2 += ` end
451
601
 
452
- Client --> Routes
453
- Routes --> Middleware
454
- Middleware --> FormRequests
455
- FormRequests --> Controllers
456
602
  `;
603
+ diagram2 += ` Client --> Routes
604
+ `;
605
+ if (hasRequests) {
606
+ diagram2 += ` Routes --> Middleware
607
+ `;
608
+ diagram2 += ` Middleware --> FormRequests
609
+ `;
610
+ diagram2 += ` FormRequests --> Controllers
611
+ `;
612
+ } else {
613
+ diagram2 += ` Routes --> Controllers
614
+ `;
615
+ }
457
616
  if (hasServices && hasActions) {
458
- diagram += ` Controllers --> Services
459
- Controllers --> Actions
460
- Services --> Models
461
- Actions --> Models
617
+ diagram2 += ` Controllers --> Services
618
+ `;
619
+ diagram2 += ` Controllers --> Actions
620
+ `;
621
+ if (hasModels) {
622
+ diagram2 += ` Services --> Models
462
623
  `;
624
+ diagram2 += ` Actions --> Models
625
+ `;
626
+ } else {
627
+ diagram2 += ` Services --> Database
628
+ `;
629
+ diagram2 += ` Actions --> Database
630
+ `;
631
+ }
632
+ } else if (hasServices) {
633
+ diagram2 += ` Controllers --> Services
634
+ `;
635
+ if (hasModels) {
636
+ diagram2 += ` Services --> Models
637
+ `;
638
+ } else {
639
+ diagram2 += ` Services --> Database
640
+ `;
641
+ }
463
642
  } else if (hasActions) {
464
- diagram += ` Controllers --> Actions
465
- Actions --> Models
643
+ diagram2 += ` Controllers --> Actions
644
+ `;
645
+ if (hasModels) {
646
+ diagram2 += ` Actions --> Models
466
647
  `;
648
+ } else {
649
+ diagram2 += ` Actions --> Database
650
+ `;
651
+ }
467
652
  } else {
468
- diagram += ` Controllers --> Services
469
- Services --> Models
653
+ if (hasModels) {
654
+ diagram2 += ` Controllers --> Models
655
+ `;
656
+ } else {
657
+ diagram2 += ` Controllers --> Database
470
658
  `;
659
+ }
471
660
  }
472
661
  if (hasFilament) {
473
- diagram += ` Client --> Filament
662
+ if (hasModels) {
663
+ diagram2 += ` Client --> Filament
474
664
  Filament --> Models
475
665
  `;
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
666
+ } else {
667
+ diagram2 += ` Client --> Filament
668
+ Filament --> Database
490
669
  `;
491
670
  }
492
- diagram += ` end
671
+ }
672
+ if (hasModels) {
673
+ diagram2 += ` Models --> Database
493
674
  `;
494
675
  }
495
- return diagram;
676
+ if (hasMigrations) {
677
+ diagram2 += ` Migrations -.-> Database
678
+ `;
679
+ }
680
+ if (modules.length > 0) {
681
+ diagram2 += renderModulesSubgraph(modules);
682
+ }
683
+ return diagram2;
496
684
  }
497
685
  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
686
+ const rawFw = projectInfo.typescript?.framework || "express";
687
+ const fwDisplay = rawFw !== "unknown" && rawFw !== "node-vanilla" ? `${rawFw.charAt(0).toUpperCase() + rawFw.slice(1)} Router` : "API Endpoints & Handlers";
688
+ const orm = projectInfo.typescript?.hasPrisma ? "Prisma" : projectInfo.typescript?.hasTypeORM ? "TypeORM" : projectInfo.typescript?.hasDrizzle ? "Drizzle" : null;
689
+ const hasControllers = (components["Controllers"]?.count ?? 0) > 0;
690
+ const hasServices = (components["Services"]?.count ?? 0) > 0;
691
+ const hasRepositories = (components["Repositories"]?.count ?? 0) > 0;
692
+ const hasDtos = (components["DTOs & Schemas"]?.count ?? 0) > 0;
693
+ const hasUi = (components["UI Components"]?.count ?? 0) > 0;
694
+ const hasModels = (components["Models & Entities"]?.count ?? 0) > 0;
695
+ const hasAnyLayer = hasControllers || hasServices || hasRepositories || hasDtos || hasUi || hasModels;
696
+ if (hasAnyLayer) {
697
+ let diagram2 = `flowchart TD
501
698
  %% Styling Definitions
502
699
  classDef client fill:#E0F2FE,stroke:#0284C7,stroke-width:2px,color:#0369A1;
503
700
  classDef routing fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#B45309;
@@ -506,77 +703,146 @@ function generateMermaidDiagram(scanResult) {
506
703
  classDef data fill:#FEE2E2,stroke:#DC2626,stroke-width:2px,color:#B91C1C;
507
704
  classDef db fill:#F1F5F9,stroke:#475569,stroke-width:2px,color:#334155;
508
705
 
509
- subgraph Layer1 ["1. Client & API Gateway"]
706
+ subgraph EntryLayer ["1. Client & API Gateway"]
510
707
  Client["Frontend / API Client"]:::client
511
- Router["Router & Endpoints<br/>(${fw} Routes)"]:::routing
708
+ Router["Router & Endpoints<br/>(${fwDisplay})"]:::routing
512
709
  end
513
-
514
- subgraph Layer2 ["2. Validation & Guards"]
515
- Middleware["Auth & Error Middleware"]:::routing
710
+ `;
711
+ if (hasUi) {
712
+ diagram2 += `
713
+ subgraph UiLayer ["Frontend Presentation"]
714
+ UiComponents["UI Components<br/>(src/components)"]:::controller
715
+ end
716
+ `;
717
+ }
718
+ if (hasDtos) {
719
+ diagram2 += `
720
+ subgraph ValidationLayer ["2. Validation & Schemas"]
516
721
  DTOs["DTOs & Schemas<br/>(Input Validation)"]:::routing
517
722
  end
518
-
519
- subgraph Layer3 ["3. Controllers"]
723
+ `;
724
+ }
725
+ if (hasControllers) {
726
+ diagram2 += `
727
+ subgraph ControllerLayer ["3. Controllers"]
520
728
  Controllers["Controllers<br/>(Request Handling)"]:::controller
521
729
  end
522
-
523
- subgraph Layer4 ["4. Application Services"]
730
+ `;
731
+ }
732
+ if (hasServices) {
733
+ diagram2 += `
734
+ subgraph ServiceLayer ["4. Application Services"]
524
735
  Services["Domain Services<br/>(Business Logic)"]:::service
525
736
  end
737
+ `;
738
+ }
739
+ diagram2 += `
740
+ subgraph PersistenceLayer ["5. Data Layer & Storage"]
741
+ `;
742
+ if (hasRepositories) {
743
+ diagram2 += ` Repositories["Repositories / Data Access<br/>(${orm || "Data Layer"})"]:::data
744
+ `;
745
+ } else if (hasModels) {
746
+ diagram2 += ` Entities["Models & Entities<br/>(${orm || "ORM"})"]:::data
747
+ `;
748
+ }
749
+ diagram2 += ` Database[("Database / Storage")]:::db
750
+ `;
751
+ diagram2 += ` end
526
752
 
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
753
  `;
540
- if (modules.length > 0) {
541
- diagram += `
542
- subgraph ModulesLayer ["\u{1F4E6} Detected Feature Modules (${modules.length})"]
754
+ diagram2 += ` Client --> Router
543
755
  `;
544
- modules.slice(0, 6).forEach((mod, idx) => {
545
- diagram += ` Mod_${idx}["${mod} Module"]:::service
756
+ if (hasUi) {
757
+ diagram2 += ` Client --> UiComponents
546
758
  `;
547
- });
548
- if (modules.length > 6) {
549
- diagram += ` Mod_More["...and ${modules.length - 6} more modules"]:::service
759
+ diagram2 += ` UiComponents --> Router
760
+ `;
761
+ }
762
+ let previousNode = "Router";
763
+ if (hasDtos) {
764
+ diagram2 += ` ${previousNode} --> DTOs
765
+ `;
766
+ previousNode = "DTOs";
767
+ }
768
+ if (hasControllers) {
769
+ diagram2 += ` ${previousNode} --> Controllers
550
770
  `;
771
+ previousNode = "Controllers";
551
772
  }
552
- diagram += ` end
773
+ if (hasServices) {
774
+ diagram2 += ` ${previousNode} --> Services
775
+ `;
776
+ previousNode = "Services";
777
+ }
778
+ if (hasRepositories) {
779
+ diagram2 += ` ${previousNode} --> Repositories
780
+ `;
781
+ diagram2 += ` Repositories --> Database
782
+ `;
783
+ } else if (hasModels) {
784
+ diagram2 += ` ${previousNode} --> Entities
785
+ `;
786
+ diagram2 += ` Entities --> Database
553
787
  `;
788
+ } else {
789
+ diagram2 += ` ${previousNode} --> Database
790
+ `;
791
+ }
792
+ if (modules.length > 0) {
793
+ diagram2 += renderModulesSubgraph(modules);
794
+ }
795
+ return diagram2;
554
796
  }
555
- return diagram;
556
797
  }
557
- return `flowchart TD
798
+ const hasEntry = (components["Source Files"]?.files || []).some(
799
+ (f) => /(?:^|\/)(?:index|cli|main|bin)\.[a-z]+$/i.test(f)
800
+ );
801
+ const hasSource = (components["Source Files"]?.count ?? 0) > 0;
802
+ const hasTests = (components["Test Files"]?.count ?? 0) > 0;
803
+ let diagram = `flowchart TD
558
804
  classDef client fill:#E0F2FE,stroke:#0284C7,stroke-width:2px,color:#0369A1;
559
805
  classDef core fill:#DCFCE7,stroke:#16A34A,stroke-width:2px,color:#15803D;
560
806
  classDef test fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#B45309;
561
807
 
562
- subgraph App ["Application Architecture"]
563
- Entry["Entrypoint / CLI"]:::client
564
- Source["Source Core Logic (src/)"]:::core
565
- Tests["Test Suites"]:::test
566
- end
808
+ subgraph App ["CLI & Application Architecture"]
809
+ `;
810
+ if (hasEntry) {
811
+ diagram += ` Entry["Entrypoint CLI (bin/)"]:::client
812
+ `;
813
+ }
814
+ if (hasSource) {
815
+ diagram += ` Source["Core Modules & Generators (src/)"]:::core
816
+ `;
817
+ }
818
+ if (hasTests) {
819
+ diagram += ` Tests["Automated Test Suites (tests/)"]:::test
820
+ `;
821
+ }
822
+ if (!hasEntry && !hasSource && !hasTests) {
823
+ diagram += ` Source["Source Files"]:::core
824
+ `;
825
+ }
826
+ diagram += ` end
567
827
 
568
- Entry --> Source
569
- Tests -.-> Source
570
828
  `;
829
+ if (hasEntry && hasSource) {
830
+ diagram += ` Entry --> Source
831
+ `;
832
+ }
833
+ if (hasTests && hasSource) {
834
+ diagram += ` Tests -.-> Source
835
+ `;
836
+ }
837
+ return diagram;
571
838
  }
572
839
  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";
840
+ const { projectInfo, totalFilesScanned, components, modules, directoryTree } = scanResult;
575
841
  const mermaid = generateMermaidDiagram(scanResult);
576
- const componentRows = Object.entries(components).map(([key, comp]) => {
842
+ const componentRows = Object.entries(components).map(([, comp]) => {
577
843
  const sampleFiles = comp.files.slice(0, 3).map((f) => `\`${f}\``).join(", ");
578
844
  const moreSuffix = comp.files.length > 3 ? ` *(+${comp.files.length - 3} more)*` : "";
579
- return `| **${comp.name}** | \`${comp.count}\` | ${comp.description} | ${sampleFiles || "-"} ${moreSuffix} |`;
845
+ return `| **${mdCell(comp.name)}** | \`${comp.count}\` | ${mdCell(comp.description)} | ${sampleFiles ? mdCell(sampleFiles + moreSuffix) : "-"} |`;
580
846
  }).join("\n");
581
847
  let modulesSection = "";
582
848
  if (modules.length > 0) {
@@ -588,10 +854,19 @@ This project organizes features into modular domains:
588
854
  ${modules.map((m) => `- **\`${m}\`**`).join("\n")}
589
855
  `;
590
856
  }
857
+ const sampleIgnores = SAFE_IGNORE_LIST.slice(0, 8).map((p2) => `\`${p2}\``).join(", ");
858
+ const securityNotice = `## \u{1F512} Security & Privacy Notice
859
+
860
+ This architectural map was produced with strict privacy guarantees:
861
+ - **Zero Network Transmission:** Executed locally without telemetry, tracking, or outbound communication.
862
+ - **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.
863
+ - **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.
864
+
865
+ *Generated with \u2764\uFE0F by LaraTS CLI.*`;
591
866
  return `# \u{1F3DB}\uFE0F Architecture Overview: ${projectInfo.name}
592
867
 
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.
868
+ > Auto-generated by **[LaraTS CLI](https://github.com/SidiqFatuh187/Larats)**.
869
+ > **100% Local & Safe:** Generated entirely in offline mode via local filesystem manifest & structure analysis. Zero data transmitted.
595
870
 
596
871
  ---
597
872
 
@@ -603,7 +878,6 @@ ${modules.map((m) => `- **\`${m}\`**`).join("\n")}
603
878
  | **Project Type** | **${projectInfo.type.toUpperCase()}** |
604
879
  | **Detected Stack** | \`${projectInfo.summary}\` |
605
880
  | **Scanned Files** | \`${totalFilesScanned}\` files indexed (safe ignore filter applied) |
606
- | **Scan Duration** | \`${scanDurationMs} ms\` |
607
881
 
608
882
  ---
609
883
 
@@ -619,9 +893,7 @@ ${modulesSection}
619
893
 
620
894
  Visual representation of the project layers and request lifecycle rendered with [Mermaid.js](https://mermaid.js.org/):
621
895
 
622
- \`\`\`mermaid
623
- ${mermaid}
624
- \`\`\`
896
+ ${fence(mermaid, "mermaid")}
625
897
 
626
898
  ---
627
899
 
@@ -629,49 +901,79 @@ ${mermaid}
629
901
 
630
902
  Clean, high-level directory tree showing key folders (ignoring build artifacts, vendor files, and sensitive keys):
631
903
 
632
- \`\`\`text
633
- ${directoryTree || "(Empty directory structure)"}
634
- \`\`\`
904
+ ${fence(directoryTree || "(Empty directory structure)", "text")}
635
905
 
636
906
  ---
637
907
 
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.*
908
+ ${securityNotice}
646
909
  `;
647
910
  }
648
911
 
649
912
  // src/generator/scaffolder.ts
650
913
  import fs2 from "fs";
651
- import path2 from "path";
914
+ import path3 from "path";
652
915
 
653
916
  // src/templates/laravel.ts
917
+ function pluralize(word) {
918
+ const lower = word.toLowerCase();
919
+ if (lower.endsWith("s") && !lower.endsWith("ss")) {
920
+ return word;
921
+ }
922
+ if (/[bcdfghjklmnpqrstvwxyz]y$/i.test(word)) {
923
+ return word.slice(0, -1) + (word.endsWith("Y") ? "IES" : "ies");
924
+ }
925
+ if (/(?:s|x|z|ch|sh)$/i.test(word)) {
926
+ return word + (word === word.toUpperCase() ? "ES" : "es");
927
+ }
928
+ return word + (word === word.toUpperCase() ? "S" : "s");
929
+ }
654
930
  function getNaming(input) {
655
931
  const words = input.trim().replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^a-zA-Z0-9]/g, " ").split(/\s+/).filter(Boolean);
656
932
  if (words.length === 0) {
657
- words.push("SampleModule");
933
+ words.push("sample", "module");
658
934
  }
659
935
  const pascal = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
660
936
  const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
661
937
  const snake = words.map((w) => w.toLowerCase()).join("_");
662
938
  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;
939
+ const pluralWords = [...words];
940
+ const lastIdx = pluralWords.length - 1;
941
+ pluralWords[lastIdx] = pluralize(pluralWords[lastIdx]);
942
+ const pluralSnake = pluralWords.map((w) => w.toLowerCase()).join("_");
943
+ const pluralPascal = pluralWords.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
666
944
  return { pascal, camel, kebab, snake, pluralSnake, pluralPascal };
667
945
  }
668
- function laravelModelTemplate(names, namespace = "App\\Models") {
946
+ function laravelModelTemplate(names, namespace = "App\\Models", laravelVersion) {
947
+ const major = laravelVersion ? parseInt(laravelVersion.split(".")[0], 10) : 11;
948
+ const isLaravel10OrOlder = major > 0 && major < 11;
949
+ const castsBlock = isLaravel10OrOlder ? ` /**
950
+ * The attributes that should be cast.
951
+ *
952
+ * @var array<string, string>
953
+ */
954
+ protected $casts = [
955
+ 'metadata' => 'array',
956
+ 'created_at' => 'datetime',
957
+ 'updated_at' => 'datetime',
958
+ ];` : ` /**
959
+ * Get the attributes that should be cast.
960
+ *
961
+ * @return array<string, string>
962
+ */
963
+ protected function casts(): array
964
+ {
965
+ return [
966
+ 'metadata' => 'array',
967
+ 'created_at' => 'datetime',
968
+ 'updated_at' => 'datetime',
969
+ ];
970
+ }`;
669
971
  return `<?php
670
972
 
671
973
  namespace ${namespace};
672
974
 
673
- use IlluminateDatabaseEloquentFactoriesHasFactory;
674
- use IlluminateDatabaseEloquentModel;
975
+ use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
976
+ use Illuminate\\Database\\Eloquent\\Model;
675
977
 
676
978
  class ${names.pascal} extends Model
677
979
  {
@@ -695,19 +997,7 @@ class ${names.pascal} extends Model
695
997
  'metadata',
696
998
  ];
697
999
 
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
- }
1000
+ ${castsBlock}
711
1001
  }
712
1002
  `;
713
1003
  }
@@ -716,15 +1006,16 @@ function laravelControllerTemplate(names, options = {}) {
716
1006
  const modelNamespace = options.modelNamespace || "App\\Models";
717
1007
  const serviceNamespace = options.serviceNamespace || "App\\Services";
718
1008
  const requestNamespace = options.requestNamespace || "App\\Http\\Requests";
1009
+ const baseControllerImport = namespace !== "App\\Http\\Controllers" ? "use App\\Http\\Controllers\\Controller;\n" : "";
719
1010
  return `<?php
720
1011
 
721
1012
  namespace ${namespace};
722
1013
 
723
- use ${modelNamespace}\\${names.pascal};
1014
+ ${baseControllerImport}use ${modelNamespace}\\${names.pascal};
724
1015
  use ${serviceNamespace}\\${names.pascal}Service;
725
1016
  use ${requestNamespace}\\${names.pascal}Request;
726
- use IlluminateHttpJsonResponse;
727
- use IlluminateHttpRequest;
1017
+ use Illuminate\\Http\\JsonResponse;
1018
+ use Illuminate\\Http\\Request;
728
1019
 
729
1020
  class ${names.pascal}Controller extends Controller
730
1021
  {
@@ -809,7 +1100,7 @@ function laravelServiceTemplate(names, options = {}) {
809
1100
  namespace ${namespace};
810
1101
 
811
1102
  use ${modelNamespace}\\${names.pascal};
812
- use IlluminateDatabaseEloquentCollection;
1103
+ use Illuminate\\Database\\Eloquent\\Collection;
813
1104
 
814
1105
  class ${names.pascal}Service
815
1106
  {
@@ -888,7 +1179,7 @@ function laravelRequestTemplate(names, namespace = "App\\Http\\Requests") {
888
1179
 
889
1180
  namespace ${namespace};
890
1181
 
891
- use IlluminateFoundationHttpFormRequest;
1182
+ use Illuminate\\Foundation\\Http\\FormRequest;
892
1183
 
893
1184
  class ${names.pascal}Request extends FormRequest
894
1185
  {
@@ -931,9 +1222,9 @@ class ${names.pascal}Request extends FormRequest
931
1222
  function laravelMigrationTemplate(names) {
932
1223
  return `<?php
933
1224
 
934
- use IlluminateDatabaseMigrationsMigration;
935
- use IlluminateDatabaseSchemaBlueprint;
936
- use IlluminateSupportFacadesSchema;
1225
+ use Illuminate\\Database\\Migrations\\Migration;
1226
+ use Illuminate\\Database\\Schema\\Blueprint;
1227
+ use Illuminate\\Support\\Facades\\Schema;
937
1228
 
938
1229
  return new class extends Migration
939
1230
  {
@@ -997,7 +1288,11 @@ export class ${names.pascal}Controller {
997
1288
  return;
998
1289
  }
999
1290
  res.status(200).json({ status: 'success', data: item });
1000
- } catch (error) {
1291
+ } catch (error: any) {
1292
+ if (error?.name === 'NotFoundError') {
1293
+ res.status(404).json({ status: 'fail', message: error.message });
1294
+ return;
1295
+ }
1001
1296
  next(error);
1002
1297
  }
1003
1298
  };
@@ -1024,7 +1319,11 @@ export class ${names.pascal}Controller {
1024
1319
  const payload: Update${names.pascal}Dto = req.body;
1025
1320
  const updated = await this.${names.camel}Service.update(id, payload);
1026
1321
  res.status(200).json({ status: 'success', data: updated });
1027
- } catch (error) {
1322
+ } catch (error: any) {
1323
+ if (error?.name === 'NotFoundError') {
1324
+ res.status(404).json({ status: 'fail', message: error.message });
1325
+ return;
1326
+ }
1028
1327
  next(error);
1029
1328
  }
1030
1329
  };
@@ -1035,9 +1334,17 @@ export class ${names.pascal}Controller {
1035
1334
  delete = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
1036
1335
  try {
1037
1336
  const { id } = req.params;
1038
- await this.${names.camel}Service.delete(id);
1337
+ const deleted = await this.${names.camel}Service.delete(id);
1338
+ if (!deleted) {
1339
+ res.status(404).json({ status: 'fail', message: '${names.pascal} not found' });
1340
+ return;
1341
+ }
1039
1342
  res.status(200).json({ status: 'success', message: '${names.pascal} deleted successfully' });
1040
- } catch (error) {
1343
+ } catch (error: any) {
1344
+ if (error?.name === 'NotFoundError') {
1345
+ res.status(404).json({ status: 'fail', message: error.message });
1346
+ return;
1347
+ }
1041
1348
  next(error);
1042
1349
  }
1043
1350
  };
@@ -1077,7 +1384,8 @@ export class ${names.pascal}Service {
1077
1384
  }
1078
1385
  function tsRepositoryTemplate(names, isModular = true) {
1079
1386
  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}';
1387
+ return `import { randomUUID } from 'node:crypto';
1388
+ import { NotFoundError, type ${names.pascal}Entity, type Create${names.pascal}Dto, type Update${names.pascal}Dto } from '${dtoImport}';
1081
1389
 
1082
1390
  export interface I${names.pascal}Repository {
1083
1391
  findAll(query?: Record<string, any>): Promise<${names.pascal}Entity[]>;
@@ -1100,7 +1408,7 @@ export class ${names.pascal}Repository implements I${names.pascal}Repository {
1100
1408
  }
1101
1409
 
1102
1410
  async create(data: Create${names.pascal}Dto): Promise<${names.pascal}Entity> {
1103
- const id = (Math.random() * 100000).toFixed(0);
1411
+ const id = randomUUID();
1104
1412
  const newRecord: ${names.pascal}Entity = {
1105
1413
  id,
1106
1414
  ...data,
@@ -1115,7 +1423,7 @@ export class ${names.pascal}Repository implements I${names.pascal}Repository {
1115
1423
  async update(id: string, data: Update${names.pascal}Dto): Promise<${names.pascal}Entity> {
1116
1424
  const existing = await this.findById(id);
1117
1425
  if (!existing) {
1118
- throw new Error(\`${names.pascal} with id \${id} not found\`);
1426
+ throw new NotFoundError(\`${names.pascal} with id \${id} not found\`);
1119
1427
  }
1120
1428
  const updated: ${names.pascal}Entity = {
1121
1429
  ...existing,
@@ -1154,7 +1462,14 @@ export default router;
1154
1462
  `;
1155
1463
  }
1156
1464
  function tsDtoTemplate(names) {
1157
- return `export interface ${names.pascal}Entity {
1465
+ return `export class NotFoundError extends Error {
1466
+ constructor(message: string) {
1467
+ super(message);
1468
+ this.name = 'NotFoundError';
1469
+ }
1470
+ }
1471
+
1472
+ export interface ${names.pascal}Entity {
1158
1473
  id: string;
1159
1474
  name: string;
1160
1475
  status: 'draft' | 'active' | 'archived';
@@ -1181,6 +1496,39 @@ export interface Update${names.pascal}Dto {
1181
1496
  }
1182
1497
 
1183
1498
  // src/generator/scaffolder.ts
1499
+ var RESERVED_NAMES = /* @__PURE__ */ new Set([
1500
+ "model",
1501
+ "controller",
1502
+ "service",
1503
+ "action",
1504
+ "request",
1505
+ "migration",
1506
+ "repository",
1507
+ "route",
1508
+ "dto",
1509
+ "object",
1510
+ "class",
1511
+ "interface",
1512
+ "module",
1513
+ "app",
1514
+ "base"
1515
+ ]);
1516
+ function validateModuleName(name) {
1517
+ if (!name || !name.trim()) {
1518
+ return "Module name cannot be empty.";
1519
+ }
1520
+ const trimmed = name.trim();
1521
+ if (!/^[a-zA-Z]/.test(trimmed)) {
1522
+ return "Module name must start with an alphabetical letter.";
1523
+ }
1524
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(trimmed)) {
1525
+ return "Module name may only contain alphanumeric characters, hyphens, and underscores.";
1526
+ }
1527
+ if (RESERVED_NAMES.has(trimmed.toLowerCase())) {
1528
+ return `"${trimmed}" is a reserved framework name and cannot be used as a module name.`;
1529
+ }
1530
+ return null;
1531
+ }
1184
1532
  function getMigrationTimestamp() {
1185
1533
  const now = /* @__PURE__ */ new Date();
1186
1534
  const YYYY = now.getFullYear();
@@ -1195,16 +1543,46 @@ async function scaffoldModule(projectInfo, options) {
1195
1543
  const root = projectInfo.rootPath;
1196
1544
  const names = getNaming(options.moduleName);
1197
1545
  const filesToGenerate = [];
1546
+ const results = [];
1547
+ let createdCount = 0;
1548
+ let skippedCount = 0;
1549
+ let errorCount = 0;
1198
1550
  const isModular = options.isModular ?? (projectInfo.type === "laravel" ? Boolean(projectInfo.laravel?.isModular) : projectInfo.type === "typescript" ? Boolean(projectInfo.typescript?.isModular) : true);
1199
1551
  if (projectInfo.type === "laravel") {
1200
1552
  const selected = new Set(options.components);
1553
+ const addMigrationIfRequested = () => {
1554
+ const migrationsDir = path3.join(root, "database", "migrations");
1555
+ let existingMigration = null;
1556
+ if (fs2.existsSync(migrationsDir)) {
1557
+ const files = fs2.readdirSync(migrationsDir);
1558
+ const match = files.find((f) => f.endsWith(`_create_${names.pluralSnake}_table.php`));
1559
+ if (match) {
1560
+ existingMigration = match;
1561
+ }
1562
+ }
1563
+ if (existingMigration && !options.forceOverwrite) {
1564
+ results.push({
1565
+ filePath: path3.join(migrationsDir, existingMigration),
1566
+ relativeFilePath: `database/migrations/${existingMigration}`,
1567
+ status: "skipped",
1568
+ reason: "Migration for table already exists (overwrite protected)"
1569
+ });
1570
+ skippedCount++;
1571
+ } else {
1572
+ const timestamp = existingMigration ? existingMigration.split("_create_")[0] : getMigrationTimestamp();
1573
+ filesToGenerate.push({
1574
+ relativePath: `database/migrations/${timestamp}_create_${names.pluralSnake}_table.php`,
1575
+ content: laravelMigrationTemplate(names)
1576
+ });
1577
+ }
1578
+ };
1201
1579
  if (isModular) {
1202
1580
  const baseDir = `app/Modules/${names.pascal}`;
1203
1581
  const baseNamespace = `App\\Modules\\${names.pascal}`;
1204
1582
  if (selected.has("model")) {
1205
1583
  filesToGenerate.push({
1206
1584
  relativePath: `${baseDir}/Models/${names.pascal}.php`,
1207
- content: laravelModelTemplate(names, `${baseNamespace}\\Models`)
1585
+ content: laravelModelTemplate(names, `${baseNamespace}\\Models`, projectInfo.laravel?.version)
1208
1586
  });
1209
1587
  }
1210
1588
  if (selected.has("controller")) {
@@ -1243,17 +1621,13 @@ async function scaffoldModule(projectInfo, options) {
1243
1621
  });
1244
1622
  }
1245
1623
  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
- });
1624
+ addMigrationIfRequested();
1251
1625
  }
1252
1626
  } else {
1253
1627
  if (selected.has("model")) {
1254
1628
  filesToGenerate.push({
1255
1629
  relativePath: `app/Models/${names.pascal}.php`,
1256
- content: laravelModelTemplate(names, "App\\Models")
1630
+ content: laravelModelTemplate(names, "App\\Models", projectInfo.laravel?.version)
1257
1631
  });
1258
1632
  }
1259
1633
  if (selected.has("controller")) {
@@ -1281,11 +1655,7 @@ async function scaffoldModule(projectInfo, options) {
1281
1655
  });
1282
1656
  }
1283
1657
  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
- });
1658
+ addMigrationIfRequested();
1289
1659
  }
1290
1660
  }
1291
1661
  } else {
@@ -1330,11 +1700,15 @@ async function scaffoldModule(projectInfo, options) {
1330
1700
  if (selected.has("repository")) exports.push(`export * from './${names.kebab}.repository.js';`);
1331
1701
  if (selected.has("route")) exports.push(`export * from './${names.kebab}.routes.js';`);
1332
1702
  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
- });
1703
+ if (exports.length > 0) {
1704
+ filesToGenerate.push({
1705
+ relativePath: `${moduleFolder}/index.ts`,
1706
+ content: `${exports.join("\n")}
1707
+ `,
1708
+ isBarrel: true,
1709
+ barrelExports: exports
1710
+ });
1711
+ }
1338
1712
  }
1339
1713
  } else {
1340
1714
  if (selected.has("controller")) {
@@ -1369,20 +1743,17 @@ async function scaffoldModule(projectInfo, options) {
1369
1743
  }
1370
1744
  }
1371
1745
  }
1372
- const results = [];
1373
- let createdCount = 0;
1374
- let skippedCount = 0;
1375
- let errorCount = 0;
1376
1746
  for (const item of filesToGenerate) {
1377
- const fullPath = path2.resolve(root, item.relativePath);
1378
- if (fs2.existsSync(fullPath) && !options.forceOverwrite) {
1747
+ const fullPath = path3.resolve(root, item.relativePath);
1748
+ const rel = path3.relative(root, fullPath);
1749
+ if (rel.startsWith("..") || path3.isAbsolute(rel)) {
1379
1750
  results.push({
1380
1751
  filePath: fullPath,
1381
1752
  relativeFilePath: item.relativePath,
1382
- status: "skipped",
1383
- reason: "File already exists (overwrite protected)"
1753
+ status: "error",
1754
+ reason: "Path traversal attempt detected outside project root"
1384
1755
  });
1385
- skippedCount++;
1756
+ errorCount++;
1386
1757
  continue;
1387
1758
  }
1388
1759
  if (options.dryRun) {
@@ -1395,12 +1766,52 @@ async function scaffoldModule(projectInfo, options) {
1395
1766
  createdCount++;
1396
1767
  continue;
1397
1768
  }
1769
+ if (item.isBarrel && item.barrelExports && fs2.existsSync(fullPath)) {
1770
+ try {
1771
+ const existingContent = fs2.readFileSync(fullPath, "utf-8");
1772
+ const missingExports = item.barrelExports.filter(
1773
+ (line) => !existingContent.includes(line)
1774
+ );
1775
+ if (missingExports.length > 0) {
1776
+ const newContent = existingContent.endsWith("\n") ? existingContent + missingExports.join("\n") + "\n" : existingContent + "\n" + missingExports.join("\n") + "\n";
1777
+ fs2.writeFileSync(fullPath, newContent, "utf-8");
1778
+ results.push({
1779
+ filePath: fullPath,
1780
+ relativeFilePath: item.relativePath,
1781
+ status: "created",
1782
+ reason: "Appended exports to existing barrel index"
1783
+ });
1784
+ createdCount++;
1785
+ } else {
1786
+ results.push({
1787
+ filePath: fullPath,
1788
+ relativeFilePath: item.relativePath,
1789
+ status: "skipped",
1790
+ reason: "File already exists and contains all module exports"
1791
+ });
1792
+ skippedCount++;
1793
+ }
1794
+ continue;
1795
+ } catch (err) {
1796
+ results.push({
1797
+ filePath: fullPath,
1798
+ relativeFilePath: item.relativePath,
1799
+ status: "error",
1800
+ reason: err.message || String(err)
1801
+ });
1802
+ errorCount++;
1803
+ continue;
1804
+ }
1805
+ }
1398
1806
  try {
1399
- const parentDir = path2.dirname(fullPath);
1807
+ const parentDir = path3.dirname(fullPath);
1400
1808
  if (!fs2.existsSync(parentDir)) {
1401
1809
  fs2.mkdirSync(parentDir, { recursive: true });
1402
1810
  }
1403
- fs2.writeFileSync(fullPath, item.content, "utf-8");
1811
+ fs2.writeFileSync(fullPath, item.content, {
1812
+ flag: options.forceOverwrite ? "w" : "wx",
1813
+ encoding: "utf-8"
1814
+ });
1404
1815
  results.push({
1405
1816
  filePath: fullPath,
1406
1817
  relativeFilePath: item.relativePath,
@@ -1408,13 +1819,23 @@ async function scaffoldModule(projectInfo, options) {
1408
1819
  });
1409
1820
  createdCount++;
1410
1821
  } catch (err) {
1411
- results.push({
1412
- filePath: fullPath,
1413
- relativeFilePath: item.relativePath,
1414
- status: "error",
1415
- reason: err.message || String(err)
1416
- });
1417
- errorCount++;
1822
+ if (err.code === "EEXIST") {
1823
+ results.push({
1824
+ filePath: fullPath,
1825
+ relativeFilePath: item.relativePath,
1826
+ status: "skipped",
1827
+ reason: "File already exists (overwrite protected)"
1828
+ });
1829
+ skippedCount++;
1830
+ } else {
1831
+ results.push({
1832
+ filePath: fullPath,
1833
+ relativeFilePath: item.relativePath,
1834
+ status: "error",
1835
+ reason: err.message || String(err)
1836
+ });
1837
+ errorCount++;
1838
+ }
1418
1839
  }
1419
1840
  }
1420
1841
  return {
@@ -1423,12 +1844,13 @@ async function scaffoldModule(projectInfo, options) {
1423
1844
  files: results,
1424
1845
  createdCount,
1425
1846
  skippedCount,
1426
- errorCount
1847
+ errorCount,
1848
+ dryRun: Boolean(options.dryRun)
1427
1849
  };
1428
1850
  }
1429
1851
 
1430
1852
  // src/index.ts
1431
- var VERSION = "1.0.0";
1853
+ var VERSION = true ? "1.0.2" : "1.0.1";
1432
1854
  function showHelp() {
1433
1855
  console.log(`
1434
1856
  ${pc.bold(pc.cyan("LaraTS CLI"))} ${pc.gray(`v${VERSION}`)}
@@ -1465,41 +1887,94 @@ function handleCancel() {
1465
1887
  p.cancel(pc.yellow("Operation cancelled. See you next time! \u{1F44B}"));
1466
1888
  process.exit(0);
1467
1889
  }
1468
- async function runScan(projectInfo, outputPath = "ARCHITECTURE.md") {
1890
+ async function runScan(projectInfo, outputPath = "ARCHITECTURE.md", options = {}) {
1469
1891
  const spinner2 = p.spinner();
1470
1892
  spinner2.start("Scanning codebase structure & components (strict ignore list active)...");
1471
1893
  try {
1894
+ const destination = path4.resolve(projectInfo.rootPath, outputPath);
1895
+ const rel = path4.relative(projectInfo.rootPath, destination);
1896
+ if (rel.startsWith("..") || path4.isAbsolute(rel)) {
1897
+ spinner2.stop(pc.red("Scan failed"));
1898
+ p.log.error(`Refusing to write output: "${outputPath}" resolves outside project root directory.`);
1899
+ process.exitCode = 1;
1900
+ return;
1901
+ }
1902
+ if (fs3.existsSync(destination) && !options.force) {
1903
+ const existingContent = fs3.readFileSync(destination, "utf-8");
1904
+ const isLaraTsFile = existingContent.includes("Auto-generated by **[LaraTS CLI]") || existingContent.includes("Auto-generated by **[ArchMind CLI]") || existingContent.includes("LaraTS CLI") || existingContent.includes("<!-- larats-generated -->");
1905
+ if (!isLaraTsFile) {
1906
+ spinner2.stop(pc.red("Scan refused"));
1907
+ p.log.error(
1908
+ `Destination file "${outputPath}" already exists and does not appear to be generated by LaraTS.
1909
+ To protect user files from accidental overwrites, use --force to overwrite.`
1910
+ );
1911
+ process.exitCode = 1;
1912
+ return;
1913
+ }
1914
+ }
1472
1915
  const scanResult = await scanProject(projectInfo);
1916
+ const nonZeroLayers = Object.values(scanResult.components).filter((c) => c.count > 0).length;
1917
+ const totalComponents = Object.values(scanResult.components).reduce((sum, c) => sum + c.count, 0);
1918
+ if (options.dryRun) {
1919
+ spinner2.stop(pc.green(`Scan completed in ${scanResult.scanDurationMs}ms (dry run)`));
1920
+ p.note(
1921
+ `${pc.bold("Scanned Files:")} ${scanResult.totalFilesScanned}
1922
+ ${pc.bold("Total Components:")} ${totalComponents} across ${nonZeroLayers} active architectural layers
1923
+ ${pc.bold("Modules Detected:")} ${scanResult.modules.length > 0 ? scanResult.modules.join(", ") : "None (monolithic structure)"}
1924
+ ${pc.bold("Simulated Output:")} Would write ARCHITECTURE.md to ${pc.cyan(outputPath)} (dry run: no files written)`,
1925
+ pc.green("Architecture Map (Dry Run) \u{1F5FA}\uFE0F")
1926
+ );
1927
+ p.outro(pc.cyan("Dry run completed: no files written."));
1928
+ return;
1929
+ }
1473
1930
  spinner2.stop(pc.green(`Scan completed in ${scanResult.scanDurationMs}ms`));
1474
1931
  const markdown = generateArchitectureMarkdown(scanResult);
1475
- const destination = path3.resolve(projectInfo.rootPath, outputPath);
1476
- const parentDir = path3.dirname(destination);
1932
+ const parentDir = path4.dirname(destination);
1477
1933
  if (!fs3.existsSync(parentDir)) {
1478
1934
  fs3.mkdirSync(parentDir, { recursive: true });
1479
1935
  }
1480
1936
  fs3.writeFileSync(destination, markdown, "utf-8");
1481
1937
  p.note(
1482
1938
  `${pc.bold("Scanned Files:")} ${scanResult.totalFilesScanned}
1483
- ${pc.bold("Components Identified:")} ${Object.keys(scanResult.components).length}
1939
+ ${pc.bold("Total Components:")} ${totalComponents} across ${nonZeroLayers} active architectural layers
1484
1940
  ${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)}`,
1941
+ ${pc.bold("Documentation:")} Saved to ${pc.cyan(path4.relative(projectInfo.rootPath, destination) || outputPath)}`,
1486
1942
  pc.green("Architecture Map Generated! \u{1F5FA}\uFE0F")
1487
1943
  );
1488
1944
  p.outro(pc.cyan(`Open ${outputPath} to view your project structure & Mermaid diagram!`));
1489
1945
  } catch (err) {
1490
1946
  spinner2.stop(pc.red("Scan failed"));
1491
1947
  p.log.error(err.message || String(err));
1948
+ process.exitCode = 1;
1492
1949
  }
1493
1950
  }
1494
1951
  async function runScaffold(projectInfo, moduleNameArg, cliOptions = {}) {
1952
+ if (projectInfo.type === "unknown") {
1953
+ p.log.warn(pc.yellow("Warning: Project type is unknown (neither Laravel nor TypeScript detected). Scaffolding standard TypeScript/Express components."));
1954
+ } else if (projectInfo.type === "typescript" && projectInfo.typescript?.framework && !["express", "node-vanilla", "unknown"].includes(projectInfo.typescript.framework)) {
1955
+ p.log.warn(
1956
+ pc.yellow(`Warning: Project detected as "${projectInfo.typescript.framework}". LaraTS generates standard Express-compatible TypeScript boilerplate.`)
1957
+ );
1958
+ }
1495
1959
  let moduleName = moduleNameArg;
1496
- if (!moduleName) {
1960
+ if (moduleName) {
1961
+ const errorMsg = validateModuleName(moduleName);
1962
+ if (errorMsg) {
1963
+ p.log.error(pc.red(`Invalid module name: ${errorMsg}`));
1964
+ process.exitCode = 1;
1965
+ return;
1966
+ }
1967
+ } else {
1968
+ if (!process.stdin.isTTY) {
1969
+ p.log.error(pc.red("Error: Module name is required in non-interactive (non-TTY) mode. Example: `larats scaffold Payment`"));
1970
+ process.exitCode = 1;
1971
+ return;
1972
+ }
1497
1973
  const input = await p.text({
1498
1974
  message: "What is the name of the new module or feature?",
1499
1975
  placeholder: "e.g. Payment, Order, UserProfile",
1500
1976
  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.";
1977
+ return validateModuleName(val) || void 0;
1503
1978
  }
1504
1979
  });
1505
1980
  if (p.isCancel(input)) handleCancel();
@@ -1507,42 +1982,68 @@ async function runScaffold(projectInfo, moduleNameArg, cliOptions = {}) {
1507
1982
  }
1508
1983
  let selectedComponents = [];
1509
1984
  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;
1985
+ if (!process.stdin.isTTY) {
1986
+ selectedComponents = ["model", "controller", "service", "request", "migration"];
1987
+ } else {
1988
+ const componentsPrompt = await p.multiselect({
1989
+ message: `Select components to scaffold for ${pc.cyan(moduleName)}:`,
1990
+ options: [
1991
+ { value: "model", label: "Eloquent Model", hint: "Includes table name, fillables, and casts" },
1992
+ { value: "controller", label: "Resourceful Controller", hint: "Includes standard CRUD methods & DI service" },
1993
+ { value: "service", label: "Service Class", hint: "Encapsulated business logic operations" },
1994
+ { value: "action", label: "Single Action Class", hint: "Invokable single-responsibility class" },
1995
+ { value: "request", label: "Form Request", hint: "Request validation rules & authorization" },
1996
+ { value: "migration", label: "Database Migration", hint: "Schema definition with timestamps" }
1997
+ ],
1998
+ initialValues: ["model", "controller", "service", "request", "migration"],
1999
+ required: true
2000
+ });
2001
+ if (p.isCancel(componentsPrompt)) handleCancel();
2002
+ selectedComponents = componentsPrompt;
2003
+ }
2004
+ if (selectedComponents.includes("controller") && !selectedComponents.includes("service")) {
2005
+ p.log.warn(pc.yellow('Notice: Controller requires Service class. Auto-adding "service" to scaffolded components.'));
2006
+ selectedComponents.push("service");
2007
+ }
1525
2008
  } 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;
2009
+ if (!process.stdin.isTTY) {
2010
+ selectedComponents = ["controller", "service", "repository", "route", "dto"];
2011
+ } else {
2012
+ const componentsPrompt = await p.multiselect({
2013
+ message: `Select components to scaffold for ${pc.cyan(moduleName)}:`,
2014
+ options: [
2015
+ { value: "controller", label: "Controller", hint: "Express request handlers & response orchestration" },
2016
+ { value: "service", label: "Service", hint: "Application logic & repository coordination" },
2017
+ { value: "repository", label: "Repository", hint: "Data persistence abstraction & queries" },
2018
+ { value: "route", label: "Router & Routes", hint: "Express router mapping endpoints to controller" },
2019
+ { value: "dto", label: "DTOs & Interfaces", hint: "Typed interfaces for entities and payloads" }
2020
+ ],
2021
+ initialValues: ["controller", "service", "repository", "route", "dto"],
2022
+ required: true
2023
+ });
2024
+ if (p.isCancel(componentsPrompt)) handleCancel();
2025
+ selectedComponents = componentsPrompt;
2026
+ }
2027
+ if (selectedComponents.includes("route") && !selectedComponents.includes("controller")) {
2028
+ p.log.warn(pc.yellow('Notice: Route requires Controller. Auto-adding "controller" to scaffolded components.'));
2029
+ selectedComponents.push("controller");
2030
+ }
2031
+ if (selectedComponents.includes("controller") && !selectedComponents.includes("service")) {
2032
+ p.log.warn(pc.yellow('Notice: Controller requires Service. Auto-adding "service" to scaffolded components.'));
2033
+ selectedComponents.push("service");
2034
+ }
2035
+ if (selectedComponents.includes("service") && !selectedComponents.includes("repository")) {
2036
+ p.log.warn(pc.yellow('Notice: Service requires Repository. Auto-adding "repository" to scaffolded components.'));
2037
+ selectedComponents.push("repository");
2038
+ }
1540
2039
  }
1541
2040
  let isModular = true;
1542
2041
  if (cliOptions.modular) {
1543
2042
  isModular = true;
1544
2043
  } else if (cliOptions.standard) {
1545
2044
  isModular = false;
2045
+ } else if (!process.stdin.isTTY) {
2046
+ isModular = projectInfo.type === "laravel" ? Boolean(projectInfo.laravel?.isModular) : projectInfo.type === "typescript" ? Boolean(projectInfo.typescript?.isModular) : true;
1546
2047
  } else {
1547
2048
  const defaultModular = projectInfo.type === "laravel" ? Boolean(projectInfo.laravel?.isModular) : projectInfo.type === "typescript" ? Boolean(projectInfo.typescript?.isModular) : true;
1548
2049
  const structurePrompt = await p.select({
@@ -1585,8 +2086,14 @@ async function runScaffold(projectInfo, moduleNameArg, cliOptions = {}) {
1585
2086
  forceOverwrite: cliOptions.force
1586
2087
  });
1587
2088
  spinner2.stop("Generation finished!");
2089
+ if (result.errorCount > 0) {
2090
+ process.exitCode = 1;
2091
+ }
1588
2092
  const fileLines = result.files.map((f) => {
1589
2093
  if (f.status === "created") {
2094
+ if (cliOptions.dryRun) {
2095
+ return ` ${pc.blue("\u2139")} ${f.relativeFilePath} ${pc.gray("(would create)")}`;
2096
+ }
1590
2097
  return ` ${pc.green("\u2714")} ${f.relativeFilePath} ${pc.gray("(created)")}`;
1591
2098
  } else if (f.status === "skipped") {
1592
2099
  return ` ${pc.yellow("\u26A0")} ${f.relativeFilePath} ${pc.yellow(`(skipped - ${f.reason})`)}`;
@@ -1594,6 +2101,16 @@ async function runScaffold(projectInfo, moduleNameArg, cliOptions = {}) {
1594
2101
  return ` ${pc.red("\u2716")} ${f.relativeFilePath} ${pc.red(`(error - ${f.reason})`)}`;
1595
2102
  }
1596
2103
  });
2104
+ if (cliOptions.dryRun) {
2105
+ p.note(
2106
+ fileLines.join("\n") + `
2107
+
2108
+ ${pc.bold("Dry run summary:")} ${pc.cyan(`${result.createdCount} would be created`)}, ${pc.yellow(`${result.skippedCount} skipped`)}, ${pc.red(`${result.errorCount} errors`)}`,
2109
+ pc.cyan(`Dry run: Scaffolding ${moduleName}`)
2110
+ );
2111
+ p.outro(pc.cyan("Dry run: no files written."));
2112
+ return;
2113
+ }
1597
2114
  p.note(
1598
2115
  fileLines.join("\n") + `
1599
2116
 
@@ -1603,6 +2120,25 @@ ${pc.bold("Summary:")} ${pc.green(`${result.createdCount} created`)}, ${pc.yello
1603
2120
  if (result.skippedCount > 0 && !cliOptions.force) {
1604
2121
  p.log.warn(pc.yellow("Anti-Overwrite Protection prevented modifying existing files. Use --force to overwrite if intended."));
1605
2122
  }
2123
+ const names = getNaming(moduleName);
2124
+ if (result.createdCount > 0) {
2125
+ if (projectInfo.type === "laravel") {
2126
+ p.note(
2127
+ `1. Run database migration:
2128
+ ${pc.cyan("php artisan migrate")}
2129
+ 2. Register routes in routes/api.php or routes/web.php:
2130
+ ${pc.cyan(`Route::apiResource('${names.kebab}', \\${isModular ? `App\\Modules\\${names.pascal}\\Controllers\\` : "App\\Http\\Controllers\\"}${names.pascal}Controller::class);`)}`,
2131
+ "\u{1F680} Next Steps"
2132
+ );
2133
+ } else {
2134
+ p.note(
2135
+ `1. Mount routes into Express app:
2136
+ ${pc.cyan(`import { ${names.camel}Routes } from './${isModular ? `modules/${names.kebab}` : `routes/${names.kebab}.routes.js`}';
2137
+ app.use('/api/${names.kebab}', ${names.camel}Routes);`)}`,
2138
+ "\u{1F680} Next Steps"
2139
+ );
2140
+ }
2141
+ }
1606
2142
  p.outro(pc.green(`\u2728 Successfully scaffolded ${moduleName} module!`));
1607
2143
  }
1608
2144
  function showProjectInfo(projectInfo) {
@@ -1626,34 +2162,61 @@ ${pc.bold("Modular Src:")} ${projectInfo.typescript.isModular ? pc.green("Yes")
1626
2162
  p.note(details, pc.cyan("Detected Project Architecture"));
1627
2163
  }
1628
2164
  async function main() {
1629
- const args = process.argv.slice(2);
1630
- if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
2165
+ let parsed;
2166
+ try {
2167
+ parsed = parseArgs({
2168
+ args: process.argv.slice(2),
2169
+ options: {
2170
+ output: { type: "string", short: "o" },
2171
+ modular: { type: "boolean", short: "m" },
2172
+ standard: { type: "boolean", short: "s" },
2173
+ force: { type: "boolean", short: "f" },
2174
+ "dry-run": { type: "boolean" },
2175
+ version: { type: "boolean", short: "v" },
2176
+ help: { type: "boolean", short: "h" }
2177
+ },
2178
+ allowPositionals: true,
2179
+ strict: true
2180
+ });
2181
+ } catch (err) {
2182
+ console.error(pc.red(`Error: ${err.message}`));
1631
2183
  showHelp();
2184
+ process.exitCode = 1;
1632
2185
  return;
1633
2186
  }
1634
- if (args.includes("--version") || args.includes("-v")) {
2187
+ if (parsed.values.help || parsed.positionals[0] === "help") {
2188
+ showHelp();
2189
+ return;
2190
+ }
2191
+ if (parsed.values.version || parsed.positionals[0] === "version") {
1635
2192
  console.log(`larats v${VERSION}`);
1636
2193
  return;
1637
2194
  }
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];
2195
+ const command = parsed.positionals[0];
2196
+ const VALID_COMMANDS = /* @__PURE__ */ new Set(["scan", "scaffold", "make", "info", "help"]);
2197
+ if (command && !VALID_COMMANDS.has(command)) {
2198
+ console.error(pc.red(`Error: Unknown command "${command}".`));
2199
+ showHelp();
2200
+ process.exitCode = 1;
2201
+ return;
1642
2202
  }
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;
2203
+ const outputPath = parsed.values.output || "ARCHITECTURE.md";
2204
+ const isModularFlag = parsed.values.modular;
2205
+ const isStandardFlag = parsed.values.standard;
2206
+ const isForceFlag = parsed.values.force;
2207
+ const isDryRunFlag = parsed.values["dry-run"];
1648
2208
  const projectInfo = await detectProjectType(process.cwd());
1649
2209
  if (command === "scan") {
1650
2210
  p.intro(`${pc.bgCyan(pc.black(" LaraTS "))} ${pc.gray(`v${VERSION}`)} - Scanning Architecture`);
1651
2211
  p.log.info(`Detected: ${pc.green(projectInfo.summary)}`);
1652
- await runScan(projectInfo, outputPath);
2212
+ await runScan(projectInfo, outputPath, {
2213
+ force: isForceFlag,
2214
+ dryRun: isDryRunFlag
2215
+ });
1653
2216
  return;
1654
2217
  }
1655
2218
  if (command === "scaffold" || command === "make") {
1656
- const moduleName = args[1] && !args[1].startsWith("-") ? args[1] : void 0;
2219
+ const moduleName = parsed.positionals[1];
1657
2220
  p.intro(`${pc.bgCyan(pc.black(" LaraTS "))} ${pc.gray(`v${VERSION}`)} - Module Scaffolder`);
1658
2221
  p.log.info(`Target Project: ${pc.green(projectInfo.summary)}`);
1659
2222
  await runScaffold(projectInfo, moduleName, {
@@ -1670,6 +2233,10 @@ async function main() {
1670
2233
  p.outro(pc.cyan("Done."));
1671
2234
  return;
1672
2235
  }
2236
+ if (!process.stdin.isTTY) {
2237
+ showHelp();
2238
+ return;
2239
+ }
1673
2240
  p.intro(`${pc.bgCyan(pc.black(" LaraTS "))} ${pc.bold("Smart Architecture Visualizer & Scaffolder")} ${pc.gray(`v${VERSION}`)}`);
1674
2241
  p.log.info(`Working Directory: ${pc.gray(process.cwd())}`);
1675
2242
  p.log.info(`Detected Architecture: ${pc.bold(pc.green(projectInfo.summary))}`);
@@ -1701,7 +2268,10 @@ async function main() {
1701
2268
  handleCancel();
1702
2269
  }
1703
2270
  if (action === "scan") {
1704
- await runScan(projectInfo, outputPath);
2271
+ await runScan(projectInfo, outputPath, {
2272
+ force: isForceFlag,
2273
+ dryRun: isDryRunFlag
2274
+ });
1705
2275
  } else if (action === "scaffold") {
1706
2276
  await runScaffold(projectInfo, void 0, {
1707
2277
  modular: isModularFlag,