mdsmith 1.1.2 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-MDSMITH.md +47 -11
- package/bin/index.js +69 -247
- package/bin/vers/303/243o2.js +997 -0
- package/mdsmith.config.json +2 -1
- package/package.json +4 -1
- package/src/analysis/index.js +149 -0
- package/src/config/index.js +43 -0
- package/src/detectors/index.js +203 -0
- package/src/formatters/index.js +53 -0
- package/src/generators/index.js +27 -0
- package/src/interactive/index.js +95 -0
- package/src/readme/index.js +286 -0
- package/src/scanner/index.js +113 -0
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// npx mdsmith --help # show help
|
|
4
|
+
// npx mdsmith --init # create config file
|
|
5
|
+
// npx mdsmith --tree # show project structure
|
|
6
|
+
// npx mdsmith --deps # list dependencies
|
|
7
|
+
// npx mdsmith --readme # generate README
|
|
8
|
+
// npx mdsmith --analyze # project analyze
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const TECH_MAP = {
|
|
12
|
+
// Frameworks
|
|
13
|
+
express: "Express",
|
|
14
|
+
fastify: "Fastify",
|
|
15
|
+
koa: "Koa",
|
|
16
|
+
hapi: "Hapi",
|
|
17
|
+
"@nestjs/core": "NestJS",
|
|
18
|
+
|
|
19
|
+
// ORMs
|
|
20
|
+
prisma: "Prisma",
|
|
21
|
+
"@prisma/client": "Prisma",
|
|
22
|
+
sequelize: "Sequelize",
|
|
23
|
+
mongoose: "Mongoose",
|
|
24
|
+
typeorm: "TypeORM",
|
|
25
|
+
drizzle: "Drizzle ORM",
|
|
26
|
+
|
|
27
|
+
// Databases
|
|
28
|
+
pg: "PostgreSQL",
|
|
29
|
+
mysql: "MySQL",
|
|
30
|
+
mysql2: "MySQL",
|
|
31
|
+
sqlite3: "SQLite",
|
|
32
|
+
mongodb: "MongoDB",
|
|
33
|
+
|
|
34
|
+
// Auth
|
|
35
|
+
jsonwebtoken: "JWT",
|
|
36
|
+
passport: "Passport",
|
|
37
|
+
|
|
38
|
+
// AI
|
|
39
|
+
openai: "OpenAI",
|
|
40
|
+
langchain: "LangChain",
|
|
41
|
+
|
|
42
|
+
// Infra / Tools
|
|
43
|
+
docker: "Docker",
|
|
44
|
+
"swagger-ui-express": "Swagger",
|
|
45
|
+
puppeteer: "Puppeteer",
|
|
46
|
+
nodemailer: "Nodemailer"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
const i18n = {
|
|
51
|
+
en: {
|
|
52
|
+
projectInfo: "Project Info",
|
|
53
|
+
dependencies: "Dependencies",
|
|
54
|
+
projectStructure: "Project Structure",
|
|
55
|
+
availableScripts: "Available Scripts",
|
|
56
|
+
version: "Version",
|
|
57
|
+
packageManager: "Package Manager",
|
|
58
|
+
type: "Type",
|
|
59
|
+
generatedBy: "Generated by"
|
|
60
|
+
},
|
|
61
|
+
pt: {
|
|
62
|
+
projectInfo: "Informações do Projeto",
|
|
63
|
+
dependencies: "Dependências",
|
|
64
|
+
projectStructure: "Estrutura do Projeto",
|
|
65
|
+
availableScripts: "Scripts Disponíveis",
|
|
66
|
+
version: "Versão",
|
|
67
|
+
packageManager: "Gerenciador de Pacotes",
|
|
68
|
+
type: "Tipo",
|
|
69
|
+
generatedBy: "Gerado por"
|
|
70
|
+
},
|
|
71
|
+
es: {
|
|
72
|
+
projectInfo: "Información del Proyecto",
|
|
73
|
+
dependencies: "Dependencias",
|
|
74
|
+
projectStructure: "Estructura del Proyecto",
|
|
75
|
+
availableScripts: "Scripts Disponibles",
|
|
76
|
+
version: "Versión",
|
|
77
|
+
packageManager: "Administrador de Paquetes",
|
|
78
|
+
type: "Tipo",
|
|
79
|
+
generatedBy: "Generado por"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const fs = require("fs");
|
|
84
|
+
|
|
85
|
+
const path = require("path");
|
|
86
|
+
|
|
87
|
+
const prompts = require('prompts');
|
|
88
|
+
|
|
89
|
+
const projectRoot = process.cwd();
|
|
90
|
+
|
|
91
|
+
if(!fs.existsSync(path.join(process.cwd(), "package.json"))){
|
|
92
|
+
console.log(`
|
|
93
|
+
Error: package.json not found.
|
|
94
|
+
Please run mdsmith from the project root.
|
|
95
|
+
`)
|
|
96
|
+
process.exit(1)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let packageJson
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
packageJson = JSON.parse(
|
|
103
|
+
fs.readFileSync(path.join(projectRoot, "package.json"), "utf-8")
|
|
104
|
+
)
|
|
105
|
+
} catch {
|
|
106
|
+
console.log("Invalid package.json file.")
|
|
107
|
+
process.exit(1)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const args = process.argv.slice(2)
|
|
111
|
+
|
|
112
|
+
const defaultConfig = {
|
|
113
|
+
ignore: ["node_modules", ".git", ".vscode"],
|
|
114
|
+
depth: Infinity,
|
|
115
|
+
emojis: true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function healthChecks(packageJson, projectRoot, analysis) {
|
|
119
|
+
const checks = []
|
|
120
|
+
|
|
121
|
+
if (!analysis.testing) {
|
|
122
|
+
checks.push({
|
|
123
|
+
type: "warning",
|
|
124
|
+
message: "No test runner configured."
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!analysis.linting) {
|
|
129
|
+
checks.push({
|
|
130
|
+
type: "info",
|
|
131
|
+
message: "No ESLint detected."
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!analysis.formatting) {
|
|
136
|
+
checks.push({
|
|
137
|
+
type: "info",
|
|
138
|
+
message: "No Prettier detected."
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!packageJson.engines?.node) {
|
|
143
|
+
checks.push({
|
|
144
|
+
type: "critical",
|
|
145
|
+
message: "Node version not defined in package.json engines."
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!fs.existsSync(path.join(projectRoot, "Dockerfile"))) {
|
|
150
|
+
checks.push({
|
|
151
|
+
type: "info",
|
|
152
|
+
message: "No Dockerfile found."
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return checks
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
function formatDependencies(deps) {
|
|
161
|
+
if (!deps || Object.keys(deps).length === 0) {
|
|
162
|
+
return "No dependencies found.\n";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return Object.entries(deps)
|
|
166
|
+
.map(([name, version]) => `- ${name} ${version}`)
|
|
167
|
+
.join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function scanDir(dirPath, padding, config) {
|
|
171
|
+
let entries
|
|
172
|
+
try {
|
|
173
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
174
|
+
.sort((a, b) => {
|
|
175
|
+
if (a.isDirectory() && !b.isDirectory()) return -1
|
|
176
|
+
if (!a.isDirectory() && b.isDirectory()) return 1
|
|
177
|
+
return a.name.localeCompare(b.name)
|
|
178
|
+
})
|
|
179
|
+
} catch {
|
|
180
|
+
return ""
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let treeContent = ""
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
|
|
188
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
189
|
+
|
|
190
|
+
if (entry.isDirectory()) {
|
|
191
|
+
if (config.ignore.includes(entry.name)){
|
|
192
|
+
continue
|
|
193
|
+
} else {
|
|
194
|
+
treeContent += `${" ".repeat(padding*2)} 📂 ${entry.name}\n`
|
|
195
|
+
if (config.depth === null || padding < config.depth){
|
|
196
|
+
padding ++
|
|
197
|
+
treeContent += scanDir(fullPath, padding, config)
|
|
198
|
+
padding --
|
|
199
|
+
} else {
|
|
200
|
+
treeContent += `${" ".repeat((padding + 1) * 2)} ··· \n`
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
if (config.ignore.includes(entry.name)){
|
|
205
|
+
continue
|
|
206
|
+
} else{
|
|
207
|
+
treeContent += `${" ".repeat(padding*2)} 📄 ${entry.name}\n`
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return treeContent
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function buscarNome(nome) {
|
|
216
|
+
if (!nome) {
|
|
217
|
+
const confirm = await prompts({
|
|
218
|
+
type: 'confirm',
|
|
219
|
+
name: 'value',
|
|
220
|
+
message: 'Project name not found in package.json. Provide one?',
|
|
221
|
+
initial: true
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (!confirm.value) {
|
|
225
|
+
console.log("\nProject name will be set as Unnamed Project.\n");
|
|
226
|
+
return "Unnamed Project";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const response = await prompts({
|
|
230
|
+
type: 'text',
|
|
231
|
+
name: 'value',
|
|
232
|
+
message: 'Enter the project name:'
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
console.log(`\nProject name set to "${response.value}".\n`);
|
|
236
|
+
return response.value || "Unnamed Project";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return nome;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function buscarDescricao(descricao) {
|
|
243
|
+
if (!descricao) {
|
|
244
|
+
const confirm = await prompts({
|
|
245
|
+
type: 'confirm',
|
|
246
|
+
name: 'value',
|
|
247
|
+
message: 'Project description not found. Provide one?',
|
|
248
|
+
initial: true
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (!confirm.value) {
|
|
252
|
+
console.log("\nDescription placeholder added.\n");
|
|
253
|
+
return "*Project description goes here*";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const response = await prompts({
|
|
257
|
+
type: 'text',
|
|
258
|
+
name: 'value',
|
|
259
|
+
message: 'Enter the project description:'
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
console.log("\nDescription saved.\n");
|
|
263
|
+
return response.value || "*Project description goes here*";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return descricao;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function detectarTipoProjeto(packageJson) {
|
|
270
|
+
const deps = {
|
|
271
|
+
...packageJson.dependencies,
|
|
272
|
+
...packageJson.devDependencies
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (deps.react) return "Frontend React"
|
|
276
|
+
if (deps.next) return "Next.js Application"
|
|
277
|
+
if (deps.express) return "API REST (Express)"
|
|
278
|
+
if (deps.typescript) return "Projeto TypeScript"
|
|
279
|
+
|
|
280
|
+
return "Node.js Project"
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function formatScripts(scripts) {
|
|
284
|
+
if (!scripts || Object.keys(scripts).length === 0) {
|
|
285
|
+
return "No scripts available.\n";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return Object.entries(scripts)
|
|
289
|
+
.map(([name, script]) => `- ${name} -> ${script}`)
|
|
290
|
+
.join("\n");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function detectarEnvVars(projectRoot) {
|
|
294
|
+
const envPath = path.join(projectRoot, ".env")
|
|
295
|
+
|
|
296
|
+
if (!fs.existsSync(envPath)) return []
|
|
297
|
+
|
|
298
|
+
const content = fs.readFileSync(envPath, "utf-8")
|
|
299
|
+
|
|
300
|
+
const vars = content
|
|
301
|
+
.split("\n")
|
|
302
|
+
.map(line => line.trim())
|
|
303
|
+
.filter(line =>
|
|
304
|
+
line &&
|
|
305
|
+
!line.startsWith("#") &&
|
|
306
|
+
line.includes("=")
|
|
307
|
+
)
|
|
308
|
+
.map(line => line.replace(/^export\s+/, ""))
|
|
309
|
+
.map(line => line.split("=")[0].trim())
|
|
310
|
+
|
|
311
|
+
return vars
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function gerarEnvSection(vars) {
|
|
315
|
+
if (!vars.length) return ""
|
|
316
|
+
|
|
317
|
+
let section = "## 🔑 Environment Variables\n\n"
|
|
318
|
+
section += "Create a `.env` file in the project root with the following variables:\n\n"
|
|
319
|
+
|
|
320
|
+
vars.forEach(v => {
|
|
321
|
+
section += `- ${v}\n`
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
return section + "\n"
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function detectarPorta(scanData) {
|
|
328
|
+
const filesToScan = [...scanData.jsFiles, ...scanData.tsFiles]
|
|
329
|
+
|
|
330
|
+
for (const filePath of filesToScan) {
|
|
331
|
+
try {
|
|
332
|
+
const content = fs.readFileSync(filePath, "utf-8")
|
|
333
|
+
|
|
334
|
+
// 🔎 Regex robusta: pega .listen(...) mesmo com quebra de linha
|
|
335
|
+
const listenRegex = /\.listen\s*\(\s*([\s\S]*?)\)/g
|
|
336
|
+
|
|
337
|
+
let match
|
|
338
|
+
|
|
339
|
+
while ((match = listenRegex.exec(content)) !== null) {
|
|
340
|
+
const rawArgument = match[1].split(",")[0].trim()
|
|
341
|
+
|
|
342
|
+
// =============================
|
|
343
|
+
// ✅ 1️⃣ Número direto
|
|
344
|
+
// =============================
|
|
345
|
+
const directNumber = rawArgument.match(/^\d+$/)
|
|
346
|
+
if (directNumber) {
|
|
347
|
+
return directNumber[0]
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// =============================
|
|
351
|
+
// ✅ 2️⃣ Fallback inline
|
|
352
|
+
// =============================
|
|
353
|
+
const inlineFallback = rawArgument.match(/\|\|\s*(\d+)/)
|
|
354
|
+
if (inlineFallback) {
|
|
355
|
+
return inlineFallback[1]
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// =============================
|
|
359
|
+
// ✅ 3️⃣ process.env.PORT
|
|
360
|
+
// =============================
|
|
361
|
+
if (rawArgument.includes("process.env.PORT")) {
|
|
362
|
+
return "process.env.PORT"
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// =============================
|
|
366
|
+
// ✅ 4️⃣ Resolver variável
|
|
367
|
+
// =============================
|
|
368
|
+
const variableName = rawArgument.replace(/[^a-zA-Z0-9_]/g, "")
|
|
369
|
+
|
|
370
|
+
if (!variableName) continue
|
|
371
|
+
|
|
372
|
+
const variableRegex = new RegExp(
|
|
373
|
+
`(const|let|var)\\s+${variableName}\\s*=\\s*([\\s\\S]*?);`
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
const variableMatch = content.match(variableRegex)
|
|
377
|
+
|
|
378
|
+
if (variableMatch) {
|
|
379
|
+
const variableValue = variableMatch[2]
|
|
380
|
+
|
|
381
|
+
const fallbackMatch = variableValue.match(/\|\|\s*(\d+)/)
|
|
382
|
+
if (fallbackMatch) {
|
|
383
|
+
return fallbackMatch[1]
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const numberMatch = variableValue.match(/\d+/)
|
|
387
|
+
if (numberMatch) {
|
|
388
|
+
return numberMatch[0]
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
} catch (err) {
|
|
394
|
+
continue
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return null
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function perguntarPortaManual() {
|
|
402
|
+
const response = await prompts({
|
|
403
|
+
type: "text",
|
|
404
|
+
name: "port",
|
|
405
|
+
message: "⚠️ Não consegui detectar a porta automaticamente.\nDigite a porta da API (ou deixe vazio para pular):",
|
|
406
|
+
validate: value => {
|
|
407
|
+
if (!value) return true
|
|
408
|
+
return /^\d+$/.test(value) || "Digite apenas números"
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
return response.port || null
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function scanProjectFiles(projectRoot) {
|
|
416
|
+
const result = {
|
|
417
|
+
allFiles: [],
|
|
418
|
+
jsFiles: [],
|
|
419
|
+
tsFiles: [],
|
|
420
|
+
envFiles: [],
|
|
421
|
+
hasDockerfile: false,
|
|
422
|
+
hasPrisma: false,
|
|
423
|
+
hasSrcFolder: false,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function walk(dir) {
|
|
427
|
+
let files
|
|
428
|
+
try {
|
|
429
|
+
files = fs.readdirSync(dir)
|
|
430
|
+
} catch {
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
for (const file of files) {
|
|
435
|
+
const fullPath = path.join(dir, file)
|
|
436
|
+
|
|
437
|
+
// ignorar node_modules e .git
|
|
438
|
+
if (
|
|
439
|
+
fullPath.includes("node_modules") ||
|
|
440
|
+
fullPath.includes(".git")
|
|
441
|
+
) continue
|
|
442
|
+
|
|
443
|
+
let stat
|
|
444
|
+
try {
|
|
445
|
+
const stat = fs.lstatSync(fullPath)
|
|
446
|
+
|
|
447
|
+
if (stat.isSymbolicLink()) continue
|
|
448
|
+
} catch {
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (stat.isDirectory()) {
|
|
453
|
+
if (file === "src") {
|
|
454
|
+
result.hasSrcFolder = true
|
|
455
|
+
}
|
|
456
|
+
walk(fullPath)
|
|
457
|
+
} else {
|
|
458
|
+
result.allFiles.push(fullPath)
|
|
459
|
+
|
|
460
|
+
if (file.endsWith(".js")) result.jsFiles.push(fullPath)
|
|
461
|
+
if (file.endsWith(".ts")) result.tsFiles.push(fullPath)
|
|
462
|
+
if (file.startsWith(".env")) result.envFiles.push(fullPath)
|
|
463
|
+
|
|
464
|
+
if (file === "Dockerfile") result.hasDockerfile = true
|
|
465
|
+
if (fullPath.includes("prisma")) result.hasPrisma = true
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
walk(projectRoot)
|
|
471
|
+
|
|
472
|
+
return result
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function gerarGettingStarted(packageManager = "npm", scripts = {}, hasPrisma) {
|
|
476
|
+
const lines = []
|
|
477
|
+
|
|
478
|
+
lines.push(
|
|
479
|
+
packageManager === "npm"
|
|
480
|
+
? "npm install"
|
|
481
|
+
: `${packageManager} install`
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
if (hasPrisma) {
|
|
485
|
+
lines.push("npx prisma migrate dev")
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (scripts.dev) {
|
|
489
|
+
lines.push(getRunCommand("dev", packageManager))
|
|
490
|
+
} else if (scripts.start) {
|
|
491
|
+
lines.push(getRunCommand("start", packageManager))
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return `
|
|
495
|
+
## 🚀 Getting Started
|
|
496
|
+
|
|
497
|
+
\`\`\`bash
|
|
498
|
+
${lines.join("\n")}
|
|
499
|
+
\`\`\`
|
|
500
|
+
|
|
501
|
+
`
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
function getRunCommand(script, packageManager) {
|
|
506
|
+
if (packageManager === "yarn") return `yarn ${script}`
|
|
507
|
+
if (packageManager === "pnpm") return `pnpm ${script}`
|
|
508
|
+
return `npm run ${script}`
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
async function generateReadme(packageJson, tree, config){
|
|
514
|
+
|
|
515
|
+
const deps = {
|
|
516
|
+
...packageJson.dependencies,
|
|
517
|
+
...packageJson.devDependencies
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const techStack = detectarTechStack(deps)
|
|
521
|
+
const techSection = gerarTechStackSection(techStack)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
const envVars = detectarEnvVars(projectRoot)
|
|
527
|
+
const envSection = gerarEnvSection(envVars)
|
|
528
|
+
|
|
529
|
+
const nomeProjeto = await buscarNome(packageJson.name);
|
|
530
|
+
const descricaoProjeto = await buscarDescricao(packageJson.description);
|
|
531
|
+
const idioma = await escolherLingua()
|
|
532
|
+
const t = i18n[idioma] || i18n.en
|
|
533
|
+
|
|
534
|
+
const packageManager = detectarPackageManager(projectRoot)
|
|
535
|
+
|
|
536
|
+
const scanData = scanProjectFiles(projectRoot)
|
|
537
|
+
let port = detectarPorta(scanData)
|
|
538
|
+
|
|
539
|
+
if (!port) {
|
|
540
|
+
port = await perguntarPortaManual()
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const commands = analisarScripts(packageJson.scripts, packageManager)
|
|
544
|
+
|
|
545
|
+
const hasPrisma = detectarPrisma(projectRoot, packageJson)
|
|
546
|
+
const dbSection = hasPrisma ? gerarDatabaseSection(packageManager) : ""
|
|
547
|
+
|
|
548
|
+
const gettingStartedCommands = gerarGettingStarted(
|
|
549
|
+
packageManager,
|
|
550
|
+
packageJson.scripts,
|
|
551
|
+
hasPrisma
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
const content = `
|
|
557
|
+
# ${nomeProjeto}
|
|
558
|
+
|
|
559
|
+
${descricaoProjeto}
|
|
560
|
+
|
|
561
|
+
## ${config.emojis ? "🔖" : ""} ${t.projectInfo}
|
|
562
|
+
- **${t.version}:** ${packageJson.version || "Not specified"}
|
|
563
|
+
- **${t.packageManager}:** ${packageManager}
|
|
564
|
+
- **${t.type}:** ${detectarTipoProjeto(packageJson)}
|
|
565
|
+
|
|
566
|
+
${techSection}
|
|
567
|
+
|
|
568
|
+
## ${config.emojis ? "📁" : ""} ${t.projectStructure}
|
|
569
|
+
\`\`\`
|
|
570
|
+
${tree}
|
|
571
|
+
\`\`\`
|
|
572
|
+
|
|
573
|
+
${gettingStartedCommands}
|
|
574
|
+
|
|
575
|
+
${envSection}
|
|
576
|
+
${dbSection}
|
|
577
|
+
|
|
578
|
+
${port ? `
|
|
579
|
+
## 🌐 Server
|
|
580
|
+
|
|
581
|
+
After starting, the server will run at:
|
|
582
|
+
|
|
583
|
+
http://localhost:${port}
|
|
584
|
+
|
|
585
|
+
` : ""}
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
${t.generatedBy} **mdSmith**
|
|
591
|
+
`
|
|
592
|
+
|
|
593
|
+
return content
|
|
594
|
+
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function detectarPrisma(projectRoot, packageJson) {
|
|
598
|
+
const deps = {
|
|
599
|
+
...packageJson.dependencies,
|
|
600
|
+
...packageJson.devDependencies
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const hasPrismaDep = deps.prisma || deps["@prisma/client"]
|
|
604
|
+
const hasPrismaFolder = fs.existsSync(path.join(projectRoot, "prisma"))
|
|
605
|
+
|
|
606
|
+
return hasPrismaDep && hasPrismaFolder
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function gerarDatabaseSection(packageManager) {
|
|
610
|
+
let section = "## 🗄 Database Setup\n\n"
|
|
611
|
+
section += "Run Prisma migrations:\n\n"
|
|
612
|
+
section += "```bash\n"
|
|
613
|
+
|
|
614
|
+
if (packageManager === "npm") {
|
|
615
|
+
section += "npx prisma migrate dev\n"
|
|
616
|
+
section += "npx prisma generate\n"
|
|
617
|
+
} else {
|
|
618
|
+
section += "npx prisma migrate dev\n"
|
|
619
|
+
section += "npx prisma generate\n"
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
section += "```\n\n"
|
|
623
|
+
|
|
624
|
+
return section
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
async function escolherLingua() {
|
|
630
|
+
const response = await prompts({
|
|
631
|
+
type: 'select',
|
|
632
|
+
name: 'lang',
|
|
633
|
+
message: 'Choose README language',
|
|
634
|
+
choices: [
|
|
635
|
+
{ title: 'English', value: 'en' },
|
|
636
|
+
{ title: 'Português', value: 'pt' },
|
|
637
|
+
{ title: 'Español', value: 'es' }
|
|
638
|
+
],
|
|
639
|
+
initial: 0
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
return response.lang || 'en';
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function loadConfig(projectRoot) {
|
|
646
|
+
const configPath = path.join(projectRoot, "mdsmith.config.json")
|
|
647
|
+
|
|
648
|
+
if (!fs.existsSync(configPath)) {
|
|
649
|
+
return defaultConfig
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
try {
|
|
653
|
+
const fileContent = fs.readFileSync(configPath, "utf-8")
|
|
654
|
+
const userConfig = JSON.parse(fileContent)
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
...defaultConfig,
|
|
658
|
+
...userConfig,
|
|
659
|
+
ignore: [
|
|
660
|
+
...defaultConfig.ignore,
|
|
661
|
+
...(userConfig.ignore || [])
|
|
662
|
+
]
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
} catch (error) {
|
|
667
|
+
console.log("Failed to read mdsmith.config.json. Using default configuration.")
|
|
668
|
+
return defaultConfig
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function analisarProjeto(packageJson, projectRoot) {
|
|
673
|
+
const deps = {
|
|
674
|
+
...packageJson.dependencies,
|
|
675
|
+
...packageJson.devDependencies
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const analysis = {
|
|
679
|
+
frontend: deps.react ? "React" :
|
|
680
|
+
deps.next ? "Next.js" :
|
|
681
|
+
null,
|
|
682
|
+
|
|
683
|
+
backend: deps.express ? "Express" :
|
|
684
|
+
deps.fastify ? "Fastify" :
|
|
685
|
+
null,
|
|
686
|
+
|
|
687
|
+
language: deps.typescript ? "TypeScript" : "JavaScript",
|
|
688
|
+
|
|
689
|
+
bundler: deps.vite ? "Vite" :
|
|
690
|
+
deps.webpack ? "Webpack" :
|
|
691
|
+
null,
|
|
692
|
+
|
|
693
|
+
styling: deps.tailwindcss ? "TailwindCSS" :
|
|
694
|
+
deps.sass ? "Sass" :
|
|
695
|
+
null,
|
|
696
|
+
|
|
697
|
+
database: deps.mongoose ? "MongoDB" :
|
|
698
|
+
deps.prisma ? "Prisma" :
|
|
699
|
+
deps.pg ? "PostgreSQL" :
|
|
700
|
+
null,
|
|
701
|
+
|
|
702
|
+
testing: deps.jest ? "Jest" :
|
|
703
|
+
deps.vitest ? "Vitest" :
|
|
704
|
+
null,
|
|
705
|
+
|
|
706
|
+
linting: deps.eslint ? "ESLint" : null,
|
|
707
|
+
|
|
708
|
+
formatting: deps.prettier ? "Prettier" : null,
|
|
709
|
+
|
|
710
|
+
node: detectarNodeVersion(projectRoot, packageJson)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (analysis.frontend && analysis.backend) {
|
|
714
|
+
analysis.architecture = "Fullstack"
|
|
715
|
+
} else if (analysis.frontend) {
|
|
716
|
+
analysis.architecture = "Frontend"
|
|
717
|
+
} else if (analysis.backend) {
|
|
718
|
+
analysis.architecture = "Backend"
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return analysis
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function detectarPackageManager(projectRoot) {
|
|
725
|
+
if (fs.existsSync(path.join(projectRoot, "pnpm-lock.yaml"))) {
|
|
726
|
+
return "pnpm"
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (fs.existsSync(path.join(projectRoot, "yarn.lock"))) {
|
|
730
|
+
return "yarn"
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (fs.existsSync(path.join(projectRoot, "package-lock.json"))) {
|
|
734
|
+
return "npm"
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return "npm"
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function analisarScripts(scripts = {}, packageManager) {
|
|
741
|
+
const commands = {
|
|
742
|
+
install: packageManager === "npm"
|
|
743
|
+
? "npm install"
|
|
744
|
+
: `${packageManager} install`
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (scripts.dev) {
|
|
748
|
+
commands.dev = `${packageManager} run dev`
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (scripts.build) {
|
|
752
|
+
commands.build = `${packageManager} run build`
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (scripts.start) {
|
|
756
|
+
commands.start = packageManager === "npm"
|
|
757
|
+
? "npm start"
|
|
758
|
+
: `${packageManager} start`
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (scripts.preview) {
|
|
762
|
+
commands.preview = `${packageManager} run preview`
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return commands
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function diagnosticarExecucao(commands) {
|
|
769
|
+
const warnings = []
|
|
770
|
+
|
|
771
|
+
if (!commands.dev && !commands.start) {
|
|
772
|
+
warnings.push("No development or start script found.")
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (commands.build && !commands.start) {
|
|
776
|
+
warnings.push("Build script found but no start script for production.")
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return warnings
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function detectarNodeVersion(projectRoot, packageJson) {
|
|
783
|
+
if (packageJson.engines && packageJson.engines.node) {
|
|
784
|
+
return packageJson.engines.node
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const nvmrcPath = path.join(projectRoot, ".nvmrc")
|
|
788
|
+
if (fs.existsSync(nvmrcPath)) {
|
|
789
|
+
return fs.readFileSync(nvmrcPath, "utf-8").trim()
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const nodeVersionPath = path.join(projectRoot, ".node-version")
|
|
793
|
+
if (fs.existsSync(nodeVersionPath)) {
|
|
794
|
+
return fs.readFileSync(nodeVersionPath, "utf-8").trim()
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return process.version
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function detectarTechStack(deps = {}) {
|
|
801
|
+
const stack = new Set()
|
|
802
|
+
|
|
803
|
+
for (const dep in deps) {
|
|
804
|
+
if (TECH_MAP[dep]) {
|
|
805
|
+
stack.add(TECH_MAP[dep])
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return Array.from(stack).sort()
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function gerarTechStackSection(stack) {
|
|
813
|
+
if (!stack.length) return ""
|
|
814
|
+
|
|
815
|
+
let section = "## 🛠 Tech Stack\n\n"
|
|
816
|
+
|
|
817
|
+
stack.forEach(tech => {
|
|
818
|
+
section += `- ${tech}\n`
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
return section + "\n"
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
function formatarAnalise(analysis) {
|
|
826
|
+
console.log(`
|
|
827
|
+
Project Analysis
|
|
828
|
+
────────────────────────
|
|
829
|
+
`)
|
|
830
|
+
|
|
831
|
+
const entries = Object.entries(analysis)
|
|
832
|
+
|
|
833
|
+
for (const [key, value] of entries) {
|
|
834
|
+
if (value) {
|
|
835
|
+
const label = key.charAt(0).toUpperCase() + key.slice(1)
|
|
836
|
+
console.log(`✓ ${label}: ${value}`)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
console.log("")
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function formatarExecucao(commands, warnings) {
|
|
844
|
+
console.log(`Project Execution Strategy
|
|
845
|
+
────────────────────────`)
|
|
846
|
+
|
|
847
|
+
console.log(`Install:`)
|
|
848
|
+
console.log(` ${commands.install}\n`)
|
|
849
|
+
|
|
850
|
+
if (commands.dev) {
|
|
851
|
+
console.log(`Development:`)
|
|
852
|
+
console.log(` ${commands.dev}\n`)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (commands.build && commands.start) {
|
|
856
|
+
console.log(`Production:`)
|
|
857
|
+
console.log(` ${commands.build}`)
|
|
858
|
+
console.log(` ${commands.start}\n`)
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (commands.preview) {
|
|
862
|
+
console.log(`Preview:`)
|
|
863
|
+
console.log(` ${commands.preview}\n`)
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (warnings.length > 0) {
|
|
867
|
+
console.log(`Warnings:`)
|
|
868
|
+
warnings.forEach(w => console.log(` ⚠ ${w}`))
|
|
869
|
+
console.log()
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
async function main() {
|
|
876
|
+
const config = loadConfig(projectRoot)
|
|
877
|
+
|
|
878
|
+
if(args.length == 0){
|
|
879
|
+
console.log(`
|
|
880
|
+
mdSmith
|
|
881
|
+
Node.js project analyzer
|
|
882
|
+
────────────────────────
|
|
883
|
+
|
|
884
|
+
Run "mdsmith --help" to view available commands.
|
|
885
|
+
`);
|
|
886
|
+
process.exit(0);
|
|
887
|
+
}else if (args.includes("--init")) {
|
|
888
|
+
|
|
889
|
+
const configPath = path.join(projectRoot, "mdsmith.config.json")
|
|
890
|
+
|
|
891
|
+
if (fs.existsSync(configPath)) {
|
|
892
|
+
console.log("mdsmith.config.json already exists.")
|
|
893
|
+
process.exit(0)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
fs.writeFileSync(
|
|
897
|
+
configPath,
|
|
898
|
+
JSON.stringify(defaultConfig, null, 2)
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
console.log("Configuration file created.")
|
|
902
|
+
process.exit(0)
|
|
903
|
+
|
|
904
|
+
} else if(args.includes("--help")){
|
|
905
|
+
console.log(`
|
|
906
|
+
mdSmith — Available Commands
|
|
907
|
+
────────────────────────
|
|
908
|
+
|
|
909
|
+
--help Show help
|
|
910
|
+
--init Create config file
|
|
911
|
+
--tree Show project structure
|
|
912
|
+
--deps List dependencies
|
|
913
|
+
--readme Generate README
|
|
914
|
+
--analyze Project analysis
|
|
915
|
+
`)
|
|
916
|
+
process.exit(0)
|
|
917
|
+
} else if (args.includes("--tree")){
|
|
918
|
+
console.log(`
|
|
919
|
+
Project Structure
|
|
920
|
+
────────────────────────
|
|
921
|
+
`);
|
|
922
|
+
console.log(scanDir(projectRoot, 0, config))
|
|
923
|
+
process.exit(0)
|
|
924
|
+
} else if (args.includes("--analyze")){
|
|
925
|
+
const analysis = analisarProjeto(packageJson, projectRoot)
|
|
926
|
+
const packageManager = detectarPackageManager(projectRoot)
|
|
927
|
+
|
|
928
|
+
formatarAnalise(analysis)
|
|
929
|
+
|
|
930
|
+
const commands = analisarScripts(packageJson.scripts, packageManager)
|
|
931
|
+
const warnings = diagnosticarExecucao(commands)
|
|
932
|
+
|
|
933
|
+
formatarExecucao(commands, warnings)
|
|
934
|
+
|
|
935
|
+
const issues = healthChecks(packageJson, projectRoot, analysis)
|
|
936
|
+
|
|
937
|
+
if (issues.length > 0) {
|
|
938
|
+
console.log("Health Checks")
|
|
939
|
+
console.log("────────────────────────")
|
|
940
|
+
issues.forEach(issue => {
|
|
941
|
+
let icon = "⚠"
|
|
942
|
+
|
|
943
|
+
if (issue.type === "critical") icon = "❌"
|
|
944
|
+
if (issue.type === "info") icon = "ℹ"
|
|
945
|
+
|
|
946
|
+
console.log(`${icon} ${issue.message}`)
|
|
947
|
+
})
|
|
948
|
+
console.log()
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
process.exit(0)
|
|
952
|
+
} else if (args.includes("--deps")){
|
|
953
|
+
console.log(`
|
|
954
|
+
Dependencies
|
|
955
|
+
────────────────────────
|
|
956
|
+
`)
|
|
957
|
+
const deps = formatDependencies(packageJson.dependencies)
|
|
958
|
+
console.log(`${deps}\n`)
|
|
959
|
+
console.log()
|
|
960
|
+
process.exit(0)
|
|
961
|
+
} else if (args.includes("--readme")){
|
|
962
|
+
const content = await generateReadme(packageJson, scanDir(projectRoot, 0, config), config)
|
|
963
|
+
|
|
964
|
+
console.log(`
|
|
965
|
+
README Preview
|
|
966
|
+
────────────────────────
|
|
967
|
+
`)
|
|
968
|
+
|
|
969
|
+
console.log(content)
|
|
970
|
+
|
|
971
|
+
const confirmResponse = await prompts({
|
|
972
|
+
type: 'confirm',
|
|
973
|
+
name: 'generate',
|
|
974
|
+
message: 'Generate README-MDSMITH.md?',
|
|
975
|
+
initial: true
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
if (confirmResponse.generate) {
|
|
979
|
+
fs.writeFileSync("README-MDSMITH.md", content);
|
|
980
|
+
console.log("\nREADME-MDSMITH.md created.\n");
|
|
981
|
+
} else {
|
|
982
|
+
console.log("\nOperation cancelled.\n");
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
process.exit(0);
|
|
986
|
+
|
|
987
|
+
} else {
|
|
988
|
+
console.log(`
|
|
989
|
+
Unknown command.
|
|
990
|
+
Run "mdsmith --help" for usage.
|
|
991
|
+
`)
|
|
992
|
+
process.exit(0)
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
main()
|
|
997
|
+
|