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/README.md +21 -11
- package/dist/index.js +851 -281
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/dist/index.cjs +0 -1744
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -2
- package/dist/index.d.ts +0 -2
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import fs3 from "fs";
|
|
5
|
-
import
|
|
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
|
|
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
|
|
46
|
-
const
|
|
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
|
-
"
|
|
143
|
-
"
|
|
147
|
+
"build/**",
|
|
148
|
+
"out/**",
|
|
144
149
|
"**/.next/**",
|
|
145
150
|
"**/.nuxt/**",
|
|
146
151
|
"**/.turbo/**",
|
|
147
152
|
"**/.cache/**",
|
|
148
153
|
"**/coverage/**",
|
|
149
154
|
"**/.temp/**",
|
|
150
|
-
"
|
|
155
|
+
"tmp/**",
|
|
151
156
|
// IDEs & OS files
|
|
152
157
|
"**/.idea/**",
|
|
153
158
|
"**/.vscode/**",
|
|
154
159
|
"**/.DS_Store",
|
|
155
160
|
"**/Thumbs.db",
|
|
156
|
-
// Secrets &
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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) => /(
|
|
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(
|
|
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
|
|
367
|
-
for (const mf of
|
|
368
|
-
const match = mf.match(
|
|
369
|
-
if (match && 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(
|
|
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, "<").replace(/>/g, ">").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
|
-
|
|
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
|
|
549
|
+
subgraph EntryLayer ["1. Client & Entrypoints"]
|
|
416
550
|
Client["Client / Browser / API Consumer"]:::client
|
|
417
|
-
Routes["HTTP Routes
|
|
551
|
+
Routes["HTTP Routes"]:::routing
|
|
418
552
|
end
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
426
|
-
|
|
560
|
+
`;
|
|
561
|
+
}
|
|
562
|
+
diagram2 += `
|
|
563
|
+
subgraph HttpLayer ["3. Controllers & Presentation"]
|
|
564
|
+
Controllers["HTTP Controllers"]:::controller
|
|
427
565
|
`;
|
|
428
566
|
if (hasFilament) {
|
|
429
|
-
|
|
567
|
+
diagram2 += ` Filament["Filament Admin Resources"]:::controller
|
|
430
568
|
`;
|
|
431
569
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
subgraph Layer4 ["4. Domain & Business Logic"]
|
|
570
|
+
diagram2 += ` end
|
|
435
571
|
`;
|
|
436
|
-
if (hasServices ||
|
|
437
|
-
|
|
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
|
-
|
|
441
|
-
|
|
587
|
+
diagram2 += `
|
|
588
|
+
subgraph DataLayer ["5. Data Persistence & Storage"]
|
|
589
|
+
`;
|
|
590
|
+
if (hasModels) {
|
|
591
|
+
diagram2 += ` Models["Eloquent Models"]:::data
|
|
442
592
|
`;
|
|
443
593
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
465
|
-
|
|
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
|
-
|
|
469
|
-
|
|
653
|
+
if (hasModels) {
|
|
654
|
+
diagram2 += ` Controllers --> Models
|
|
655
|
+
`;
|
|
656
|
+
} else {
|
|
657
|
+
diagram2 += ` Controllers --> Database
|
|
470
658
|
`;
|
|
659
|
+
}
|
|
471
660
|
}
|
|
472
661
|
if (hasFilament) {
|
|
473
|
-
|
|
662
|
+
if (hasModels) {
|
|
663
|
+
diagram2 += ` Client --> Filament
|
|
474
664
|
Filament --> Models
|
|
475
665
|
`;
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
671
|
+
}
|
|
672
|
+
if (hasModels) {
|
|
673
|
+
diagram2 += ` Models --> Database
|
|
493
674
|
`;
|
|
494
675
|
}
|
|
495
|
-
|
|
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
|
|
499
|
-
const
|
|
500
|
-
|
|
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
|
|
706
|
+
subgraph EntryLayer ["1. Client & API Gateway"]
|
|
510
707
|
Client["Frontend / API Client"]:::client
|
|
511
|
-
Router["Router & Endpoints<br/>(${
|
|
708
|
+
Router["Router & Endpoints<br/>(${fwDisplay})"]:::routing
|
|
512
709
|
end
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
541
|
-
diagram += `
|
|
542
|
-
subgraph ModulesLayer ["\u{1F4E6} Detected Feature Modules (${modules.length})"]
|
|
754
|
+
diagram2 += ` Client --> Router
|
|
543
755
|
`;
|
|
544
|
-
|
|
545
|
-
|
|
756
|
+
if (hasUi) {
|
|
757
|
+
diagram2 += ` Client --> UiComponents
|
|
546
758
|
`;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
|
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(([
|
|
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
|
|
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)
|
|
594
|
-
> **100% Local & Safe:** Generated entirely in offline mode via local filesystem
|
|
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
|
-
|
|
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
|
-
|
|
633
|
-
${directoryTree || "(Empty directory structure)"}
|
|
634
|
-
\`\`\`
|
|
904
|
+
${fence(directoryTree || "(Empty directory structure)", "text")}
|
|
635
905
|
|
|
636
906
|
---
|
|
637
907
|
|
|
638
|
-
|
|
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
|
|
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("
|
|
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
|
|
664
|
-
const
|
|
665
|
-
|
|
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
|
|
674
|
-
use
|
|
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
|
|
727
|
-
use
|
|
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
|
|
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
|
|
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
|
|
935
|
-
use
|
|
936
|
-
use
|
|
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
|
|
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 = (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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 =
|
|
1378
|
-
|
|
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: "
|
|
1383
|
-
reason: "
|
|
1753
|
+
status: "error",
|
|
1754
|
+
reason: "Path traversal attempt detected outside project root"
|
|
1384
1755
|
});
|
|
1385
|
-
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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
|
|
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
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
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
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
-
|
|
1630
|
-
|
|
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 (
|
|
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
|
-
|
|
1639
|
-
const
|
|
1640
|
-
if (
|
|
1641
|
-
|
|
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
|
|
1644
|
-
const
|
|
1645
|
-
const
|
|
1646
|
-
const
|
|
1647
|
-
const
|
|
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 =
|
|
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,
|