@justmpm/ai-tool 0.1.0 → 0.3.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.
@@ -1,764 +0,0 @@
1
- // src/commands/map.ts
2
- import skott from "skott";
3
-
4
- // src/utils/detect.ts
5
- function detectCategory(filePath) {
6
- const normalized = filePath.replace(/\\/g, "/").toLowerCase();
7
- const fileName = normalized.split("/").pop() || "";
8
- const fileNameNoExt = fileName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
9
- if (fileName.includes(".test.") || fileName.includes(".spec.") || normalized.includes("/__tests__/") || normalized.includes("/test/")) {
10
- return "test";
11
- }
12
- if (isConfigFile(fileName)) {
13
- return "config";
14
- }
15
- if (fileNameNoExt === "page") return "page";
16
- if (fileNameNoExt === "layout") return "layout";
17
- if (fileNameNoExt === "route") return "route";
18
- if (["error", "global-error", "not-found", "loading", "template", "middleware", "default"].includes(
19
- fileNameNoExt
20
- )) {
21
- return "page";
22
- }
23
- if (normalized.includes("/api/")) return "route";
24
- if (normalized.includes("/pages/")) return "page";
25
- if (fileNameNoExt.startsWith("use") && fileNameNoExt.length > 3) return "hook";
26
- if (normalized.includes("/hooks/")) return "hook";
27
- if (normalized.includes("/types/") || fileName.endsWith(".d.ts") || fileNameNoExt === "types" || fileNameNoExt === "interfaces") {
28
- return "type";
29
- }
30
- if (normalized.includes("/services/") || fileNameNoExt.endsWith("service")) {
31
- return "service";
32
- }
33
- if (normalized.includes("/store/") || normalized.includes("/stores/") || normalized.includes("/context/") || normalized.includes("/contexts/") || normalized.includes("/providers/")) {
34
- return "store";
35
- }
36
- if (normalized.includes("/utils/") || normalized.includes("/lib/") || normalized.includes("/helpers/") || normalized.includes("/common/")) {
37
- return "util";
38
- }
39
- if (normalized.includes("/components/") || normalized.includes("/ui/") || normalized.includes("/features/")) {
40
- return "component";
41
- }
42
- return "other";
43
- }
44
- function isConfigFile(fileName) {
45
- const patterns = [
46
- /eslint\.config\./,
47
- /prettier\.config\./,
48
- /tailwind\.config\./,
49
- /next\.config\./,
50
- /vite\.config\./,
51
- /tsconfig/,
52
- /jest\.config/,
53
- /vitest\.config/,
54
- /postcss\.config/,
55
- /babel\.config/,
56
- /webpack\.config/,
57
- /firebase-messaging-sw/,
58
- /sw\./,
59
- /service-worker/,
60
- /knip\.config/,
61
- /\.env/
62
- ];
63
- return patterns.some((p) => p.test(fileName));
64
- }
65
- var categoryIcons = {
66
- page: "\u{1F4C4}",
67
- layout: "\u{1F532}",
68
- route: "\u{1F6E3}\uFE0F",
69
- component: "\u{1F9E9}",
70
- hook: "\u{1FA9D}",
71
- store: "\u{1F5C4}\uFE0F",
72
- service: "\u2699\uFE0F",
73
- util: "\u{1F527}",
74
- type: "\u{1F4DD}",
75
- config: "\u2699\uFE0F",
76
- test: "\u{1F9EA}",
77
- other: "\u{1F4C1}"
78
- };
79
- function isEntryPoint(filePath) {
80
- const normalized = filePath.replace(/\\/g, "/").toLowerCase();
81
- const fileName = normalized.split("/").pop() || "";
82
- const entryPoints = [
83
- "main.tsx",
84
- "main.ts",
85
- "main.jsx",
86
- "main.js",
87
- "index.tsx",
88
- "index.ts",
89
- "app.tsx",
90
- "app.ts"
91
- ];
92
- if (entryPoints.includes(fileName)) {
93
- const depth = normalized.split("/").length;
94
- if (depth <= 2) return true;
95
- }
96
- return false;
97
- }
98
- var CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
99
- function isCodeFile(filePath) {
100
- return CODE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
101
- }
102
-
103
- // src/formatters/text.ts
104
- function formatMapText(result) {
105
- let out = "";
106
- out += `
107
- `;
108
- out += `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
109
- `;
110
- out += `\u2551 \u{1F4C1} PROJECT MAP \u2551
111
- `;
112
- out += `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
113
-
114
- `;
115
- out += `\u{1F4CA} RESUMO
116
- `;
117
- out += ` Arquivos: ${result.summary.totalFiles}
118
- `;
119
- out += ` Pastas: ${result.summary.totalFolders}
120
- `;
121
- out += ` Diret\xF3rio: ${result.cwd}
122
-
123
- `;
124
- out += `\u{1F4C2} CATEGORIAS
125
- `;
126
- const catOrder = [
127
- "page",
128
- "layout",
129
- "route",
130
- "component",
131
- "hook",
132
- "service",
133
- "store",
134
- "util",
135
- "type",
136
- "config",
137
- "test",
138
- "other"
139
- ];
140
- for (const cat of catOrder) {
141
- const count = result.summary.categories[cat];
142
- if (count) {
143
- const icon = categoryIcons[cat];
144
- out += ` ${icon} ${cat.padEnd(12)} ${count}
145
- `;
146
- }
147
- }
148
- out += `
149
- \u{1F4C1} ESTRUTURA (Top 15 pastas)
150
- `;
151
- const topFolders = result.folders.sort((a, b) => b.fileCount - a.fileCount).slice(0, 15);
152
- for (const folder of topFolders) {
153
- out += ` ${folder.path}/ (${folder.fileCount} arquivos)
154
- `;
155
- }
156
- if (result.folders.length > 15) {
157
- out += ` ... e mais ${result.folders.length - 15} pastas
158
- `;
159
- }
160
- if (result.circularDependencies.length > 0) {
161
- out += `
162
- \u26A0\uFE0F DEPEND\xCANCIAS CIRCULARES (${result.circularDependencies.length})
163
- `;
164
- for (const cycle of result.circularDependencies.slice(0, 5)) {
165
- out += ` ${cycle.join(" \u2192 ")}
166
- `;
167
- }
168
- if (result.circularDependencies.length > 5) {
169
- out += ` ... e mais ${result.circularDependencies.length - 5}
170
- `;
171
- }
172
- }
173
- return out;
174
- }
175
- function formatDeadText(result) {
176
- let out = "";
177
- out += `
178
- `;
179
- out += `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
180
- `;
181
- out += `\u2551 \u{1F480} DEAD CODE \u2551
182
- `;
183
- out += `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
184
-
185
- `;
186
- if (result.summary.totalDead === 0) {
187
- out += `\u2705 Nenhum c\xF3digo morto encontrado!
188
- `;
189
- out += ` Todos os arquivos e exports est\xE3o sendo utilizados.
190
- `;
191
- return out;
192
- }
193
- out += `\u{1F4CA} RESUMO
194
- `;
195
- out += ` Total: ${result.summary.totalDead} itens n\xE3o utilizados
196
- `;
197
- out += ` Arquivos \xF3rf\xE3os: ${result.summary.byType.files}
198
- `;
199
- out += ` Exports n\xE3o usados: ${result.summary.byType.exports}
200
- `;
201
- out += ` Depend\xEAncias n\xE3o usadas: ${result.summary.byType.dependencies}
202
-
203
- `;
204
- if (result.files.length > 0) {
205
- out += `\u{1F5D1}\uFE0F ARQUIVOS \xD3RF\xC3OS (${result.files.length})
206
- `;
207
- out += ` Arquivos que ningu\xE9m importa:
208
-
209
- `;
210
- const byCategory = /* @__PURE__ */ new Map();
211
- for (const file of result.files) {
212
- if (!byCategory.has(file.category)) {
213
- byCategory.set(file.category, []);
214
- }
215
- byCategory.get(file.category).push(file);
216
- }
217
- for (const [category, files] of byCategory) {
218
- const icon = categoryIcons[category];
219
- out += ` ${icon} ${category}/ (${files.length})
220
- `;
221
- for (const file of files.slice(0, 5)) {
222
- out += ` ${file.path}
223
- `;
224
- }
225
- if (files.length > 5) {
226
- out += ` ... e mais ${files.length - 5}
227
- `;
228
- }
229
- out += `
230
- `;
231
- }
232
- }
233
- if (result.exports.length > 0) {
234
- out += `\u{1F4E4} EXPORTS N\xC3O USADOS (${result.exports.length})
235
- `;
236
- for (const exp of result.exports.slice(0, 10)) {
237
- out += ` ${exp.file}: ${exp.export}
238
- `;
239
- }
240
- if (result.exports.length > 10) {
241
- out += ` ... e mais ${result.exports.length - 10}
242
- `;
243
- }
244
- out += `
245
- `;
246
- }
247
- if (result.dependencies.length > 0) {
248
- out += `\u{1F4E6} DEPEND\xCANCIAS N\xC3O USADAS (${result.dependencies.length})
249
- `;
250
- for (const dep of result.dependencies) {
251
- out += ` ${dep}
252
- `;
253
- }
254
- out += `
255
- `;
256
- }
257
- out += `\u{1F4A1} SUGEST\xC3O
258
- `;
259
- out += ` Execute 'npx knip --fix' para remover automaticamente.
260
- `;
261
- return out;
262
- }
263
- function formatImpactText(result) {
264
- let out = "";
265
- out += `
266
- `;
267
- out += `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
268
- `;
269
- out += `\u2551 \u{1F3AF} IMPACT ANALYSIS \u2551
270
- `;
271
- out += `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
272
-
273
- `;
274
- const icon = categoryIcons[result.category];
275
- out += `\u{1F4CD} ARQUIVO: ${result.target}
276
- `;
277
- out += ` ${icon} ${result.category}
278
-
279
- `;
280
- out += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
281
-
282
- `;
283
- out += `\u2B06\uFE0F USADO POR (${result.upstream.total} arquivo${result.upstream.total !== 1 ? "s" : ""} \xFAnico${result.upstream.total !== 1 ? "s" : ""})
284
- `;
285
- if (result.upstream.direct.length > 0 || result.upstream.indirect.length > 0) {
286
- out += ` \u{1F4CD} ${result.upstream.direct.length} direto${result.upstream.direct.length !== 1 ? "s" : ""} + ${result.upstream.indirect.length} indireto${result.upstream.indirect.length !== 1 ? "s" : ""}
287
- `;
288
- }
289
- out += ` Quem importa este arquivo:
290
-
291
- `;
292
- if (result.upstream.total === 0) {
293
- out += ` Ningu\xE9m importa este arquivo diretamente.
294
- `;
295
- } else {
296
- for (const file of result.upstream.direct.slice(0, 10)) {
297
- const fileIcon = categoryIcons[file.category];
298
- out += ` ${fileIcon} ${file.path}
299
- `;
300
- }
301
- if (result.upstream.direct.length > 10) {
302
- out += ` ... e mais ${result.upstream.direct.length - 10} diretos
303
- `;
304
- }
305
- if (result.upstream.indirect.length > 0) {
306
- out += `
307
- Indiretos: ${result.upstream.indirect.slice(0, 5).map((f) => f.path.split("/").pop()).join(", ")}`;
308
- if (result.upstream.indirect.length > 5) {
309
- out += ` (+${result.upstream.indirect.length - 5})`;
310
- }
311
- out += `
312
- `;
313
- }
314
- }
315
- out += `
316
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
317
-
318
- `;
319
- out += `\u2B07\uFE0F DEPEND\xCANCIAS (${result.downstream.total} arquivo${result.downstream.total !== 1 ? "s" : ""} \xFAnico${result.downstream.total !== 1 ? "s" : ""})
320
- `;
321
- if (result.downstream.direct.length > 0 || result.downstream.indirect.length > 0) {
322
- out += ` \u{1F4CD} ${result.downstream.direct.length} direto${result.downstream.direct.length !== 1 ? "s" : ""} + ${result.downstream.indirect.length} indireto${result.downstream.indirect.length !== 1 ? "s" : ""}
323
- `;
324
- }
325
- out += ` O que este arquivo importa:
326
-
327
- `;
328
- if (result.downstream.total === 0) {
329
- out += ` Este arquivo n\xE3o importa nenhum arquivo local.
330
- `;
331
- } else {
332
- for (const file of result.downstream.direct.slice(0, 10)) {
333
- const fileIcon = categoryIcons[file.category];
334
- out += ` ${fileIcon} ${file.path}
335
- `;
336
- }
337
- if (result.downstream.direct.length > 10) {
338
- out += ` ... e mais ${result.downstream.direct.length - 10}
339
- `;
340
- }
341
- }
342
- out += `
343
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
344
-
345
- `;
346
- out += `\u{1F4CA} M\xC9TRICAS DE IMPACTO
347
-
348
- `;
349
- out += ` Arquivos que importam este (upstream): ${result.upstream.total} \xFAnico${result.upstream.total !== 1 ? "s" : ""}
350
- `;
351
- out += ` Arquivos que este importa (downstream): ${result.downstream.total} \xFAnico${result.downstream.total !== 1 ? "s" : ""}
352
- `;
353
- if (result.risks.length > 0) {
354
- out += `
355
- \u26A0\uFE0F RISCOS IDENTIFICADOS (${result.risks.length})
356
-
357
- `;
358
- for (const risk of result.risks) {
359
- const severity = risk.severity === "high" ? "\u{1F534}" : risk.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
360
- out += ` ${severity} ${risk.severity.toUpperCase()}: ${risk.message}
361
- `;
362
- }
363
- }
364
- if (result.suggestions.length > 0) {
365
- out += `
366
- \u{1F4A1} SUGEST\xD5ES
367
-
368
- `;
369
- for (const suggestion of result.suggestions) {
370
- out += ` \u2022 ${suggestion}
371
- `;
372
- }
373
- }
374
- return out;
375
- }
376
-
377
- // src/commands/map.ts
378
- async function map(options = {}) {
379
- const cwd = options.cwd || process.cwd();
380
- const format = options.format || "text";
381
- try {
382
- const { getStructure, useGraph } = await skott({
383
- cwd,
384
- includeBaseDir: false,
385
- dependencyTracking: {
386
- thirdParty: options.trackDependencies ?? false,
387
- builtin: false,
388
- typeOnly: false
389
- },
390
- fileExtensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
391
- tsConfigPath: "tsconfig.json"
392
- });
393
- const structure = getStructure();
394
- const { findCircularDependencies } = useGraph();
395
- const files = Object.entries(structure.graph).map(([path]) => ({
396
- path,
397
- category: detectCategory(path),
398
- size: 0
399
- // Skott não fornece tamanho
400
- }));
401
- const folderMap = /* @__PURE__ */ new Map();
402
- for (const file of files) {
403
- const parts = file.path.split("/");
404
- if (parts.length > 1) {
405
- const folder = parts.slice(0, -1).join("/");
406
- if (!folderMap.has(folder)) {
407
- folderMap.set(folder, {
408
- path: folder,
409
- fileCount: 0,
410
- categories: {}
411
- });
412
- }
413
- const stats = folderMap.get(folder);
414
- stats.fileCount++;
415
- stats.categories[file.category] = (stats.categories[file.category] || 0) + 1;
416
- }
417
- }
418
- const categories = {};
419
- for (const file of files) {
420
- categories[file.category] = (categories[file.category] || 0) + 1;
421
- }
422
- const circular = findCircularDependencies();
423
- const result = {
424
- version: "1.0.0",
425
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
426
- cwd,
427
- summary: {
428
- totalFiles: files.length,
429
- totalFolders: folderMap.size,
430
- categories
431
- },
432
- folders: Array.from(folderMap.values()),
433
- files,
434
- circularDependencies: circular
435
- };
436
- if (format === "json") {
437
- return JSON.stringify(result, null, 2);
438
- }
439
- return formatMapText(result);
440
- } catch (error) {
441
- const message = error instanceof Error ? error.message : String(error);
442
- throw new Error(`Erro ao executar map: ${message}`);
443
- }
444
- }
445
-
446
- // src/commands/dead.ts
447
- import { execSync } from "child_process";
448
- async function dead(options = {}) {
449
- const cwd = options.cwd || process.cwd();
450
- const format = options.format || "text";
451
- try {
452
- let knipOutput;
453
- try {
454
- const output = execSync("npx knip --reporter=json", {
455
- cwd,
456
- encoding: "utf-8",
457
- maxBuffer: 50 * 1024 * 1024,
458
- stdio: ["pipe", "pipe", "pipe"]
459
- });
460
- knipOutput = JSON.parse(output || "{}");
461
- } catch (execError) {
462
- const error = execError;
463
- if (error.stdout) {
464
- try {
465
- knipOutput = JSON.parse(error.stdout);
466
- } catch {
467
- knipOutput = {};
468
- }
469
- } else {
470
- knipOutput = {};
471
- }
472
- }
473
- const deadFiles = (knipOutput.files || []).map((file) => ({
474
- path: file,
475
- category: detectCategory(file),
476
- type: "file"
477
- }));
478
- const deadExports = [];
479
- if (knipOutput.issues) {
480
- for (const issue of knipOutput.issues) {
481
- if (issue.symbol && issue.symbolType === "export") {
482
- deadExports.push({
483
- file: issue.file,
484
- export: issue.symbol
485
- });
486
- }
487
- }
488
- }
489
- const deadDependencies = [
490
- ...knipOutput.dependencies || [],
491
- ...knipOutput.devDependencies || []
492
- ];
493
- const result = {
494
- version: "1.0.0",
495
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
496
- cwd,
497
- summary: {
498
- totalDead: deadFiles.length + deadExports.length + deadDependencies.length,
499
- byType: {
500
- files: deadFiles.length,
501
- exports: deadExports.length,
502
- dependencies: deadDependencies.length
503
- }
504
- },
505
- files: deadFiles,
506
- exports: deadExports,
507
- dependencies: deadDependencies
508
- };
509
- if (format === "json") {
510
- return JSON.stringify(result, null, 2);
511
- }
512
- return formatDeadText(result);
513
- } catch (error) {
514
- const message = error instanceof Error ? error.message : String(error);
515
- throw new Error(`Erro ao executar dead: ${message}`);
516
- }
517
- }
518
- async function deadFix(options = {}) {
519
- const cwd = options.cwd || process.cwd();
520
- try {
521
- const output = execSync("npx knip --fix", {
522
- cwd,
523
- encoding: "utf-8",
524
- maxBuffer: 50 * 1024 * 1024
525
- });
526
- return `\u2705 Fix executado com sucesso!
527
-
528
- ${output}`;
529
- } catch (error) {
530
- const message = error instanceof Error ? error.message : String(error);
531
- throw new Error(`Erro ao executar fix: ${message}`);
532
- }
533
- }
534
-
535
- // src/commands/impact.ts
536
- import skott2 from "skott";
537
- async function impact(target, options = {}) {
538
- const cwd = options.cwd || process.cwd();
539
- const format = options.format || "text";
540
- if (!target) {
541
- throw new Error("Target \xE9 obrigat\xF3rio. Exemplo: ai-tool impact src/components/Button.tsx");
542
- }
543
- try {
544
- const { getStructure, useGraph } = await skott2({
545
- cwd,
546
- includeBaseDir: false,
547
- dependencyTracking: {
548
- thirdParty: false,
549
- builtin: false,
550
- typeOnly: false
551
- },
552
- fileExtensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
553
- tsConfigPath: "tsconfig.json"
554
- });
555
- const structure = getStructure();
556
- const graphApi = useGraph();
557
- const allFiles = Object.keys(structure.graph);
558
- const targetPath = findTargetFile(target, allFiles);
559
- if (!targetPath) {
560
- return formatNotFound(target, allFiles);
561
- }
562
- const dependingOnNodes = graphApi.collectFilesDependingOn(targetPath, "deep");
563
- const dependingOnShallow = graphApi.collectFilesDependingOn(targetPath, "shallow");
564
- const dependenciesNodes = graphApi.collectFilesDependencies(targetPath, "deep");
565
- const dependenciesShallow = graphApi.collectFilesDependencies(targetPath, "shallow");
566
- const dependingOn = dependingOnNodes.map((n) => n.id);
567
- const dependencies = dependenciesNodes.map((n) => n.id);
568
- const directUpstream = dependingOnShallow.map((n) => n.id);
569
- const directDownstream = dependenciesShallow.map((n) => n.id);
570
- const indirectUpstream = dependingOn.filter((f) => !directUpstream.includes(f));
571
- const indirectDownstream = dependencies.filter((f) => !directDownstream.includes(f));
572
- const findCircular = () => graphApi.findCircularDependencies();
573
- const risks = detectRisks(targetPath, dependingOn, dependencies, findCircular);
574
- const suggestions = generateSuggestions(dependingOn, dependencies, risks);
575
- const result = {
576
- version: "1.0.0",
577
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
578
- target: targetPath,
579
- category: detectCategory(targetPath),
580
- upstream: {
581
- direct: directUpstream.map(toImpactFile(true)),
582
- indirect: indirectUpstream.map(toImpactFile(false)),
583
- total: dependingOn.length
584
- },
585
- downstream: {
586
- direct: directDownstream.map(toImpactFile(true)),
587
- indirect: indirectDownstream.map(toImpactFile(false)),
588
- total: dependencies.length
589
- },
590
- risks,
591
- suggestions
592
- };
593
- if (format === "json") {
594
- return JSON.stringify(result, null, 2);
595
- }
596
- return formatImpactText(result);
597
- } catch (error) {
598
- const message = error instanceof Error ? error.message : String(error);
599
- throw new Error(`Erro ao executar impact: ${message}`);
600
- }
601
- }
602
- function findTargetFile(target, allFiles) {
603
- const normalizedTarget = target.replace(/\\/g, "/");
604
- if (allFiles.includes(normalizedTarget)) {
605
- return normalizedTarget;
606
- }
607
- const targetName = normalizedTarget.split("/").pop()?.toLowerCase() || "";
608
- const targetNameNoExt = targetName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
609
- const matches = [];
610
- for (const file of allFiles) {
611
- const fileName = file.split("/").pop()?.toLowerCase() || "";
612
- const fileNameNoExt = fileName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
613
- if (fileNameNoExt === targetNameNoExt) {
614
- matches.unshift(file);
615
- } else if (file.toLowerCase().includes(normalizedTarget.toLowerCase())) {
616
- matches.push(file);
617
- }
618
- }
619
- if (matches.length === 1) {
620
- return matches[0];
621
- }
622
- if (matches.length > 1) {
623
- return matches[0];
624
- }
625
- return null;
626
- }
627
- function formatNotFound(target, allFiles) {
628
- const normalizedTarget = target.toLowerCase();
629
- const similar = allFiles.filter((f) => {
630
- const fileName = f.split("/").pop()?.toLowerCase() || "";
631
- const fileNameNoExt = fileName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
632
- return fileName.includes(normalizedTarget) || fileNameNoExt.includes(normalizedTarget) || levenshteinDistance(fileNameNoExt, normalizedTarget) <= 3;
633
- }).slice(0, 5);
634
- let out = `\u274C Arquivo n\xE3o encontrado no \xEDndice: "${target}"
635
-
636
- `;
637
- out += `\u{1F4CA} Total de arquivos indexados: ${allFiles.length}
638
-
639
- `;
640
- if (similar.length > 0) {
641
- out += `\u{1F4DD} Arquivos com nome similar:
642
- `;
643
- for (const s of similar) {
644
- out += ` \u2022 ${s}
645
- `;
646
- }
647
- out += `
648
- `;
649
- }
650
- out += `\u{1F4A1} Dicas:
651
- `;
652
- out += ` \u2022 Use o caminho relativo: src/components/Header.tsx
653
- `;
654
- out += ` \u2022 Ou apenas o nome do arquivo: Header
655
- `;
656
- out += ` \u2022 Verifique se o arquivo est\xE1 em uma pasta inclu\xEDda no scan
657
- `;
658
- return out;
659
- }
660
- function toImpactFile(isDirect) {
661
- return (path) => ({
662
- path,
663
- category: detectCategory(path),
664
- isDirect
665
- });
666
- }
667
- function detectRisks(targetPath, upstream, downstream, findCircular) {
668
- const risks = [];
669
- if (upstream.length >= 15) {
670
- risks.push({
671
- type: "widely-used",
672
- severity: "high",
673
- message: `Arquivo CR\xCDTICO: usado por ${upstream.length} arquivos \xFAnicos`
674
- });
675
- } else if (upstream.length >= 5) {
676
- risks.push({
677
- type: "widely-used",
678
- severity: "medium",
679
- message: `Arquivo compartilhado: usado por ${upstream.length} arquivos \xFAnicos`
680
- });
681
- }
682
- if (downstream.length >= 20) {
683
- risks.push({
684
- type: "deep-chain",
685
- severity: "medium",
686
- message: `Arquivo importa ${downstream.length} depend\xEAncias (considere dividir)`
687
- });
688
- } else if (downstream.length >= 10) {
689
- risks.push({
690
- type: "deep-chain",
691
- severity: "low",
692
- message: `Arquivo importa ${downstream.length} depend\xEAncias`
693
- });
694
- }
695
- const circular = findCircular();
696
- const targetCircular = circular.filter((cycle) => cycle.includes(targetPath));
697
- if (targetCircular.length > 0) {
698
- risks.push({
699
- type: "circular",
700
- severity: "medium",
701
- message: `Envolvido em ${targetCircular.length} depend\xEAncia${targetCircular.length > 1 ? "s" : ""} circular${targetCircular.length > 1 ? "es" : ""}`
702
- });
703
- }
704
- return risks;
705
- }
706
- function generateSuggestions(upstream, downstream, risks) {
707
- const suggestions = [];
708
- if (upstream.length > 0) {
709
- suggestions.push(
710
- `Verifique os ${upstream.length} arquivo(s) que importam este antes de modificar`
711
- );
712
- }
713
- if (upstream.length >= 10) {
714
- suggestions.push(`Considere criar testes para garantir que mudan\xE7as n\xE3o quebrem dependentes`);
715
- }
716
- if (downstream.length > 0) {
717
- suggestions.push(`Teste as ${downstream.length} depend\xEAncia(s) ap\xF3s mudan\xE7as`);
718
- }
719
- if (risks.some((r) => r.type === "circular")) {
720
- suggestions.push(`Considere resolver as depend\xEAncias circulares antes de refatorar`);
721
- }
722
- if (risks.some((r) => r.type === "widely-used" && r.severity === "high")) {
723
- suggestions.push(`Este arquivo \xE9 cr\xEDtico - planeje mudan\xE7as com cuidado`);
724
- }
725
- return suggestions;
726
- }
727
- function levenshteinDistance(a, b) {
728
- const matrix = [];
729
- for (let i = 0; i <= b.length; i++) {
730
- matrix[i] = [i];
731
- }
732
- for (let j = 0; j <= a.length; j++) {
733
- matrix[0][j] = j;
734
- }
735
- for (let i = 1; i <= b.length; i++) {
736
- for (let j = 1; j <= a.length; j++) {
737
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
738
- matrix[i][j] = matrix[i - 1][j - 1];
739
- } else {
740
- matrix[i][j] = Math.min(
741
- matrix[i - 1][j - 1] + 1,
742
- matrix[i][j - 1] + 1,
743
- matrix[i - 1][j] + 1
744
- );
745
- }
746
- }
747
- }
748
- return matrix[b.length][a.length];
749
- }
750
-
751
- // src/index.ts
752
- var VERSION = "0.1.0";
753
-
754
- export {
755
- detectCategory,
756
- categoryIcons,
757
- isEntryPoint,
758
- isCodeFile,
759
- map,
760
- dead,
761
- deadFix,
762
- impact,
763
- VERSION
764
- };