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