@justmpm/ai-tool 0.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 +161 -0
- package/dist/chunk-EGBEXF4G.js +764 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +100 -0
- package/dist/index.d.ts +172 -0
- package/dist/index.js +22 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# ai-tool
|
|
2
|
+
|
|
3
|
+
Ferramenta de análise de dependências e impacto para projetos TypeScript/JavaScript.
|
|
4
|
+
|
|
5
|
+
Usa [Skott](https://github.com/antoine-coulon/skott) + [Knip](https://knip.dev) internamente para análise precisa.
|
|
6
|
+
|
|
7
|
+
## Instalação
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Via npx (sem instalar)
|
|
11
|
+
npx ai-tool map
|
|
12
|
+
npx ai-tool dead
|
|
13
|
+
npx ai-tool impact Button
|
|
14
|
+
|
|
15
|
+
# Ou instalar globalmente
|
|
16
|
+
npm install -g ai-tool
|
|
17
|
+
|
|
18
|
+
# Ou como devDependency
|
|
19
|
+
npm install -D ai-tool
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Comandos
|
|
23
|
+
|
|
24
|
+
### `map` - Mapa do Projeto
|
|
25
|
+
|
|
26
|
+
Gera um mapa completo do projeto com categorização de arquivos.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
ai-tool map
|
|
30
|
+
ai-tool map --format=json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Output:**
|
|
34
|
+
- Total de arquivos e pastas
|
|
35
|
+
- Categorização automática (component, hook, service, util, etc.)
|
|
36
|
+
- Estrutura de pastas
|
|
37
|
+
- Dependências circulares detectadas
|
|
38
|
+
|
|
39
|
+
### `dead` - Código Morto
|
|
40
|
+
|
|
41
|
+
Detecta arquivos, exports e dependências não utilizados.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
ai-tool dead
|
|
45
|
+
ai-tool dead --format=json
|
|
46
|
+
ai-tool dead --fix # Remove automaticamente
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Detecta:**
|
|
50
|
+
- Arquivos órfãos (ninguém importa)
|
|
51
|
+
- Exports não utilizados
|
|
52
|
+
- Dependências npm não usadas
|
|
53
|
+
|
|
54
|
+
### `impact` - Análise de Impacto
|
|
55
|
+
|
|
56
|
+
Analisa o impacto de modificar um arquivo específico.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ai-tool impact Button
|
|
60
|
+
ai-tool impact src/components/Button.tsx
|
|
61
|
+
ai-tool impact useAuth --format=json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Output:**
|
|
65
|
+
- **Upstream**: Quem importa este arquivo (afetados por mudanças)
|
|
66
|
+
- **Downstream**: O que este arquivo importa (dependências)
|
|
67
|
+
- **Riscos**: Arquivo crítico, dependências circulares, etc.
|
|
68
|
+
- **Sugestões**: Recomendações para modificação segura
|
|
69
|
+
|
|
70
|
+
## Uso Programático
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { map, dead, impact } from "ai-tool";
|
|
74
|
+
|
|
75
|
+
// Mapa do projeto
|
|
76
|
+
const projectMap = await map({ format: "json" });
|
|
77
|
+
|
|
78
|
+
// Código morto
|
|
79
|
+
const deadCode = await dead({ format: "json" });
|
|
80
|
+
|
|
81
|
+
// Análise de impacto
|
|
82
|
+
const analysis = await impact("src/components/Button.tsx", {
|
|
83
|
+
format: "json"
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Opções
|
|
88
|
+
|
|
89
|
+
| Opção | Descrição | Default |
|
|
90
|
+
|-------|-----------|---------|
|
|
91
|
+
| `--format=text\|json` | Formato de saída | `text` |
|
|
92
|
+
| `--cwd=<path>` | Diretório do projeto | `process.cwd()` |
|
|
93
|
+
| `--fix` | Remove código morto (só para `dead`) | `false` |
|
|
94
|
+
|
|
95
|
+
## Categorias de Arquivos
|
|
96
|
+
|
|
97
|
+
O ai-tool categoriza automaticamente os arquivos:
|
|
98
|
+
|
|
99
|
+
| Categoria | Descrição |
|
|
100
|
+
|-----------|-----------|
|
|
101
|
+
| `page` | Páginas (Next.js, etc.) |
|
|
102
|
+
| `layout` | Layouts |
|
|
103
|
+
| `route` | Rotas de API |
|
|
104
|
+
| `component` | Componentes React/Vue |
|
|
105
|
+
| `hook` | React Hooks |
|
|
106
|
+
| `service` | Serviços/API |
|
|
107
|
+
| `store` | Estado global (Redux, Zustand, Context) |
|
|
108
|
+
| `util` | Utilitários |
|
|
109
|
+
| `type` | Tipos TypeScript |
|
|
110
|
+
| `config` | Configurações |
|
|
111
|
+
| `test` | Testes |
|
|
112
|
+
| `other` | Outros |
|
|
113
|
+
|
|
114
|
+
## Integração com IA
|
|
115
|
+
|
|
116
|
+
Este pacote foi criado para ser usado com ferramentas de IA como Claude Code, OpenCode, etc.
|
|
117
|
+
|
|
118
|
+
Exemplo de tool para OpenCode:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { tool } from "@opencode-ai/plugin";
|
|
122
|
+
import { execSync } from "child_process";
|
|
123
|
+
|
|
124
|
+
export default tool({
|
|
125
|
+
description: `Analisa dependências e impacto do projeto.
|
|
126
|
+
|
|
127
|
+
COMANDOS:
|
|
128
|
+
- map: Mapa do projeto
|
|
129
|
+
- dead: Código morto
|
|
130
|
+
- impact <arquivo>: Análise de impacto`,
|
|
131
|
+
|
|
132
|
+
args: {
|
|
133
|
+
command: tool.schema.enum(["map", "dead", "impact"]),
|
|
134
|
+
target: tool.schema.string().optional(),
|
|
135
|
+
format: tool.schema.enum(["text", "json"]).optional()
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
async execute({ command, target, format }) {
|
|
139
|
+
const fmt = format || "text";
|
|
140
|
+
const cmd = target
|
|
141
|
+
? `npx ai-tool ${command} "${target}" --format=${fmt}`
|
|
142
|
+
: `npx ai-tool ${command} --format=${fmt}`;
|
|
143
|
+
|
|
144
|
+
return execSync(cmd, { encoding: "utf-8" });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Requisitos
|
|
150
|
+
|
|
151
|
+
- Node.js >= 18.0.0
|
|
152
|
+
- TypeScript/JavaScript project
|
|
153
|
+
|
|
154
|
+
## Créditos
|
|
155
|
+
|
|
156
|
+
- [Skott](https://github.com/antoine-coulon/skott) - Análise de dependências
|
|
157
|
+
- [Knip](https://knip.dev) - Detecção de código morto
|
|
158
|
+
|
|
159
|
+
## Licença
|
|
160
|
+
|
|
161
|
+
MIT - [Koda AI Studio](https://kodaai.app)
|
|
@@ -0,0 +1,764 @@
|
|
|
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
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
VERSION,
|
|
4
|
+
dead,
|
|
5
|
+
deadFix,
|
|
6
|
+
impact,
|
|
7
|
+
map
|
|
8
|
+
} from "./chunk-EGBEXF4G.js";
|
|
9
|
+
|
|
10
|
+
// src/cli.ts
|
|
11
|
+
var HELP = `
|
|
12
|
+
ai-tool v${VERSION} - An\xE1lise de depend\xEAncias e impacto
|
|
13
|
+
|
|
14
|
+
COMANDOS:
|
|
15
|
+
map Mapa completo do projeto (usa Skott)
|
|
16
|
+
dead Detecta c\xF3digo morto (usa Knip)
|
|
17
|
+
dead --fix Remove c\xF3digo morto automaticamente
|
|
18
|
+
impact <arquivo> An\xE1lise de impacto antes de modificar
|
|
19
|
+
|
|
20
|
+
OP\xC7\xD5ES:
|
|
21
|
+
--format=text|json Formato de sa\xEDda (default: text)
|
|
22
|
+
--cwd=<path> Diret\xF3rio do projeto (default: cwd)
|
|
23
|
+
--help, -h Mostra esta ajuda
|
|
24
|
+
--version, -v Mostra vers\xE3o
|
|
25
|
+
|
|
26
|
+
EXEMPLOS:
|
|
27
|
+
ai-tool map
|
|
28
|
+
ai-tool map --format=json
|
|
29
|
+
ai-tool dead
|
|
30
|
+
ai-tool dead --fix
|
|
31
|
+
ai-tool impact Button
|
|
32
|
+
ai-tool impact src/hooks/useAuth.ts
|
|
33
|
+
ai-tool impact src/components/Header.tsx --format=json
|
|
34
|
+
|
|
35
|
+
SOBRE:
|
|
36
|
+
Criado por Koda AI Studio (kodaai.app)
|
|
37
|
+
Usa Skott para an\xE1lise de depend\xEAncias e Knip para dead code detection.
|
|
38
|
+
`;
|
|
39
|
+
async function main() {
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const flags = {};
|
|
42
|
+
const positional = [];
|
|
43
|
+
for (const arg of args) {
|
|
44
|
+
if (arg.startsWith("--")) {
|
|
45
|
+
const [key, value] = arg.slice(2).split("=");
|
|
46
|
+
flags[key] = value ?? true;
|
|
47
|
+
} else if (arg.startsWith("-")) {
|
|
48
|
+
const key = arg.slice(1);
|
|
49
|
+
flags[key] = true;
|
|
50
|
+
} else {
|
|
51
|
+
positional.push(arg);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (flags.help || flags.h || positional.length === 0) {
|
|
55
|
+
console.log(HELP);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
if (flags.version || flags.v) {
|
|
59
|
+
console.log(`ai-tool v${VERSION}`);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
const command = positional[0];
|
|
63
|
+
const target = positional[1];
|
|
64
|
+
const format = flags.format || "text";
|
|
65
|
+
const cwd = flags.cwd || process.cwd();
|
|
66
|
+
try {
|
|
67
|
+
let result;
|
|
68
|
+
switch (command) {
|
|
69
|
+
case "map":
|
|
70
|
+
result = await map({ format, cwd });
|
|
71
|
+
break;
|
|
72
|
+
case "dead":
|
|
73
|
+
if (flags.fix) {
|
|
74
|
+
result = await deadFix({ cwd });
|
|
75
|
+
} else {
|
|
76
|
+
result = await dead({ format, cwd });
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case "impact":
|
|
80
|
+
if (!target) {
|
|
81
|
+
console.error("\u274C Erro: arquivo alvo \xE9 obrigat\xF3rio para o comando impact");
|
|
82
|
+
console.error(" Exemplo: ai-tool impact src/components/Button.tsx");
|
|
83
|
+
console.error(" Exemplo: ai-tool impact Button");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
result = await impact(target, { format, cwd });
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
console.error(`\u274C Comando desconhecido: ${command}`);
|
|
90
|
+
console.error(" Use 'ai-tool --help' para ver comandos dispon\xEDveis.");
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
console.log(result);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
96
|
+
console.error(`\u274C Erro: ${message}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
main();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tipos para o ai-tool
|
|
3
|
+
*/
|
|
4
|
+
type OutputFormat = "text" | "json";
|
|
5
|
+
type FileCategory = "page" | "layout" | "route" | "component" | "hook" | "service" | "store" | "util" | "type" | "config" | "test" | "other";
|
|
6
|
+
interface CommandOptions {
|
|
7
|
+
format?: OutputFormat;
|
|
8
|
+
cwd?: string;
|
|
9
|
+
save?: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface MapOptions extends CommandOptions {
|
|
12
|
+
trackDependencies?: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface DeadOptions extends CommandOptions {
|
|
15
|
+
include?: string[];
|
|
16
|
+
exclude?: string[];
|
|
17
|
+
fix?: boolean;
|
|
18
|
+
}
|
|
19
|
+
interface ImpactOptions extends CommandOptions {
|
|
20
|
+
depth?: number;
|
|
21
|
+
}
|
|
22
|
+
interface FileInfo {
|
|
23
|
+
path: string;
|
|
24
|
+
category: FileCategory;
|
|
25
|
+
size: number;
|
|
26
|
+
}
|
|
27
|
+
interface FolderStats {
|
|
28
|
+
path: string;
|
|
29
|
+
fileCount: number;
|
|
30
|
+
categories: Partial<Record<FileCategory, number>>;
|
|
31
|
+
}
|
|
32
|
+
interface MapResult {
|
|
33
|
+
version: string;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
cwd: string;
|
|
36
|
+
summary: {
|
|
37
|
+
totalFiles: number;
|
|
38
|
+
totalFolders: number;
|
|
39
|
+
categories: Partial<Record<FileCategory, number>>;
|
|
40
|
+
};
|
|
41
|
+
folders: FolderStats[];
|
|
42
|
+
files: FileInfo[];
|
|
43
|
+
circularDependencies: string[][];
|
|
44
|
+
}
|
|
45
|
+
interface DeadFile {
|
|
46
|
+
path: string;
|
|
47
|
+
category: FileCategory;
|
|
48
|
+
type: "file" | "export" | "dependency";
|
|
49
|
+
}
|
|
50
|
+
interface DeadResult {
|
|
51
|
+
version: string;
|
|
52
|
+
timestamp: string;
|
|
53
|
+
cwd: string;
|
|
54
|
+
summary: {
|
|
55
|
+
totalDead: number;
|
|
56
|
+
byType: {
|
|
57
|
+
files: number;
|
|
58
|
+
exports: number;
|
|
59
|
+
dependencies: number;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
files: DeadFile[];
|
|
63
|
+
exports: Array<{
|
|
64
|
+
file: string;
|
|
65
|
+
export: string;
|
|
66
|
+
}>;
|
|
67
|
+
dependencies: string[];
|
|
68
|
+
}
|
|
69
|
+
interface ImpactFile {
|
|
70
|
+
path: string;
|
|
71
|
+
category: FileCategory;
|
|
72
|
+
isDirect: boolean;
|
|
73
|
+
}
|
|
74
|
+
interface RiskInfo {
|
|
75
|
+
type: "widely-used" | "circular" | "deep-chain";
|
|
76
|
+
severity: "low" | "medium" | "high";
|
|
77
|
+
message: string;
|
|
78
|
+
}
|
|
79
|
+
interface ImpactResult {
|
|
80
|
+
version: string;
|
|
81
|
+
timestamp: string;
|
|
82
|
+
target: string;
|
|
83
|
+
category: FileCategory;
|
|
84
|
+
upstream: {
|
|
85
|
+
direct: ImpactFile[];
|
|
86
|
+
indirect: ImpactFile[];
|
|
87
|
+
total: number;
|
|
88
|
+
};
|
|
89
|
+
downstream: {
|
|
90
|
+
direct: ImpactFile[];
|
|
91
|
+
indirect: ImpactFile[];
|
|
92
|
+
total: number;
|
|
93
|
+
};
|
|
94
|
+
risks: RiskInfo[];
|
|
95
|
+
suggestions: string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Comando MAP - Mapa do projeto usando Skott
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Executa o comando MAP
|
|
104
|
+
*/
|
|
105
|
+
declare function map(options?: MapOptions): Promise<string>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Comando DEAD - Detecção de código morto usando Knip
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Executa o comando DEAD
|
|
113
|
+
*/
|
|
114
|
+
declare function dead(options?: DeadOptions): Promise<string>;
|
|
115
|
+
/**
|
|
116
|
+
* Executa fix automático do Knip
|
|
117
|
+
*/
|
|
118
|
+
declare function deadFix(options?: DeadOptions): Promise<string>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Comando IMPACT - Análise de impacto usando Skott API
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Executa o comando IMPACT
|
|
126
|
+
*/
|
|
127
|
+
declare function impact(target: string, options?: ImpactOptions): Promise<string>;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Utilitários para detecção e classificação de arquivos
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Detecta a categoria de um arquivo baseado no path
|
|
135
|
+
*/
|
|
136
|
+
declare function detectCategory(filePath: string): FileCategory;
|
|
137
|
+
/**
|
|
138
|
+
* Ícones para cada categoria (para output text)
|
|
139
|
+
*/
|
|
140
|
+
declare const categoryIcons: Record<FileCategory, string>;
|
|
141
|
+
/**
|
|
142
|
+
* Verifica se um arquivo é entry point
|
|
143
|
+
*/
|
|
144
|
+
declare function isEntryPoint(filePath: string): boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Verifica se é um arquivo de código
|
|
147
|
+
*/
|
|
148
|
+
declare function isCodeFile(filePath: string): boolean;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* ai-tool - Ferramenta de análise de dependências e impacto
|
|
152
|
+
*
|
|
153
|
+
* Usa Skott + Knip internamente para análise precisa.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```typescript
|
|
157
|
+
* import { map, dead, impact } from "ai-tool";
|
|
158
|
+
*
|
|
159
|
+
* // Mapa do projeto
|
|
160
|
+
* const projectMap = await map({ format: "json" });
|
|
161
|
+
*
|
|
162
|
+
* // Código morto
|
|
163
|
+
* const deadCode = await dead({ format: "json" });
|
|
164
|
+
*
|
|
165
|
+
* // Análise de impacto
|
|
166
|
+
* const analysis = await impact("src/components/Button.tsx", { format: "json" });
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
declare const VERSION = "0.1.0";
|
|
171
|
+
|
|
172
|
+
export { type CommandOptions, type DeadFile, type DeadOptions, type DeadResult, type FileCategory, type FileInfo, type FolderStats, type ImpactFile, type ImpactOptions, type ImpactResult, type MapOptions, type MapResult, type OutputFormat, type RiskInfo, VERSION, categoryIcons, dead, deadFix, detectCategory, impact, isCodeFile, isEntryPoint, map };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VERSION,
|
|
3
|
+
categoryIcons,
|
|
4
|
+
dead,
|
|
5
|
+
deadFix,
|
|
6
|
+
detectCategory,
|
|
7
|
+
impact,
|
|
8
|
+
isCodeFile,
|
|
9
|
+
isEntryPoint,
|
|
10
|
+
map
|
|
11
|
+
} from "./chunk-EGBEXF4G.js";
|
|
12
|
+
export {
|
|
13
|
+
VERSION,
|
|
14
|
+
categoryIcons,
|
|
15
|
+
dead,
|
|
16
|
+
deadFix,
|
|
17
|
+
detectCategory,
|
|
18
|
+
impact,
|
|
19
|
+
isCodeFile,
|
|
20
|
+
isEntryPoint,
|
|
21
|
+
map
|
|
22
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@justmpm/ai-tool",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Ferramenta de análise de dependências e impacto para projetos TypeScript/JavaScript. Usa Skott + Knip internamente.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dependency-analysis",
|
|
7
|
+
"impact-analysis",
|
|
8
|
+
"dead-code",
|
|
9
|
+
"typescript",
|
|
10
|
+
"javascript",
|
|
11
|
+
"skott",
|
|
12
|
+
"knip",
|
|
13
|
+
"ai",
|
|
14
|
+
"claude",
|
|
15
|
+
"opencode"
|
|
16
|
+
],
|
|
17
|
+
"author": "Koda AI Studio <studio.kodaai@gmail.com>",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/anthropic-studio/ai-tool"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"bin": {
|
|
27
|
+
"ai-tool": "./dist/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
|
|
40
|
+
"dev": "tsup src/index.ts src/cli.ts --format esm --dts --watch",
|
|
41
|
+
"prepublishOnly": "npm run build",
|
|
42
|
+
"test": "node --test",
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"knip": "^5.44.0",
|
|
47
|
+
"skott": "^0.35.2"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.15.21",
|
|
51
|
+
"tsup": "^8.5.0",
|
|
52
|
+
"typescript": "^5.8.3"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|