activo 0.3.0 → 0.3.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.md +8 -0
- package/dist/core/tools/dependencyAnalysis.d.ts +3 -0
- package/dist/core/tools/dependencyAnalysis.d.ts.map +1 -0
- package/dist/core/tools/dependencyAnalysis.js +295 -0
- package/dist/core/tools/dependencyAnalysis.js.map +1 -0
- package/dist/core/tools/index.d.ts +3 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +7 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/openapiAnalysis.d.ts +3 -0
- package/dist/core/tools/openapiAnalysis.d.ts.map +1 -0
- package/dist/core/tools/openapiAnalysis.js +356 -0
- package/dist/core/tools/openapiAnalysis.js.map +1 -0
- package/dist/core/tools/pythonAnalysis.d.ts +3 -0
- package/dist/core/tools/pythonAnalysis.d.ts.map +1 -0
- package/dist/core/tools/pythonAnalysis.js +387 -0
- package/dist/core/tools/pythonAnalysis.js.map +1 -0
- package/package.json +1 -1
- package/src/core/tools/dependencyAnalysis.ts +363 -0
- package/src/core/tools/index.ts +7 -1
- package/src/core/tools/openapiAnalysis.ts +431 -0
- package/src/core/tools/pythonAnalysis.ts +477 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { Tool, ToolResult } from "./types.js";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
interface ApiEndpoint {
|
|
6
|
+
path: string;
|
|
7
|
+
method: string;
|
|
8
|
+
operationId?: string;
|
|
9
|
+
summary?: string;
|
|
10
|
+
tags?: string[];
|
|
11
|
+
parameters: {
|
|
12
|
+
name: string;
|
|
13
|
+
in: string;
|
|
14
|
+
required: boolean;
|
|
15
|
+
type?: string;
|
|
16
|
+
}[];
|
|
17
|
+
requestBody?: {
|
|
18
|
+
contentType: string;
|
|
19
|
+
schema?: string;
|
|
20
|
+
};
|
|
21
|
+
responses: {
|
|
22
|
+
status: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
}[];
|
|
25
|
+
issues: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface OpenApiAnalysisResult {
|
|
29
|
+
file: string;
|
|
30
|
+
version: string;
|
|
31
|
+
info: {
|
|
32
|
+
title?: string;
|
|
33
|
+
version?: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
};
|
|
36
|
+
servers?: string[];
|
|
37
|
+
endpoints: ApiEndpoint[];
|
|
38
|
+
schemas: string[];
|
|
39
|
+
securitySchemes: string[];
|
|
40
|
+
issues: string[];
|
|
41
|
+
summary: {
|
|
42
|
+
totalEndpoints: number;
|
|
43
|
+
endpointsWithIssues: number;
|
|
44
|
+
totalSchemas: number;
|
|
45
|
+
coverage: {
|
|
46
|
+
hasSummary: number;
|
|
47
|
+
hasDescription: number;
|
|
48
|
+
hasOperationId: number;
|
|
49
|
+
hasResponses: number;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// YAML 간단 파서 (기본적인 구조만)
|
|
55
|
+
function parseSimpleYaml(content: string): any {
|
|
56
|
+
// JSON인 경우 바로 파싱
|
|
57
|
+
if (content.trim().startsWith("{")) {
|
|
58
|
+
return JSON.parse(content);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result: any = {};
|
|
62
|
+
const lines = content.split("\n");
|
|
63
|
+
const stack: { indent: number; obj: any; key: string }[] = [{ indent: -1, obj: result, key: "" }];
|
|
64
|
+
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
if (!line.trim() || line.trim().startsWith("#")) continue;
|
|
67
|
+
|
|
68
|
+
const indent = line.search(/\S/);
|
|
69
|
+
const trimmed = line.trim();
|
|
70
|
+
|
|
71
|
+
// 키-값 파싱
|
|
72
|
+
const colonIndex = trimmed.indexOf(":");
|
|
73
|
+
if (colonIndex === -1) continue;
|
|
74
|
+
|
|
75
|
+
const key = trimmed.substring(0, colonIndex).trim();
|
|
76
|
+
let value = trimmed.substring(colonIndex + 1).trim();
|
|
77
|
+
|
|
78
|
+
// 따옴표 제거
|
|
79
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
80
|
+
value = value.slice(1, -1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 스택 조정
|
|
84
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
85
|
+
stack.pop();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const parent = stack[stack.length - 1].obj;
|
|
89
|
+
|
|
90
|
+
if (value === "" || value === "|" || value === ">") {
|
|
91
|
+
// 중첩 객체
|
|
92
|
+
parent[key] = {};
|
|
93
|
+
stack.push({ indent, obj: parent[key], key });
|
|
94
|
+
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
95
|
+
// 인라인 배열
|
|
96
|
+
parent[key] = value
|
|
97
|
+
.slice(1, -1)
|
|
98
|
+
.split(",")
|
|
99
|
+
.map((s) => s.trim().replace(/['"]/g, ""));
|
|
100
|
+
} else if (trimmed.startsWith("- ")) {
|
|
101
|
+
// 배열 항목
|
|
102
|
+
if (!Array.isArray(parent)) {
|
|
103
|
+
const parentKey = stack[stack.length - 1].key;
|
|
104
|
+
const grandParent = stack.length > 1 ? stack[stack.length - 2].obj : result;
|
|
105
|
+
grandParent[parentKey] = [];
|
|
106
|
+
stack[stack.length - 1].obj = grandParent[parentKey];
|
|
107
|
+
}
|
|
108
|
+
stack[stack.length - 1].obj.push(trimmed.substring(2));
|
|
109
|
+
} else {
|
|
110
|
+
parent[key] = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// OpenAPI 스펙 분석
|
|
118
|
+
function analyzeOpenApiSpec(filePath: string): OpenApiAnalysisResult {
|
|
119
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
120
|
+
let spec: any;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
if (filePath.endsWith(".json")) {
|
|
124
|
+
spec = JSON.parse(content);
|
|
125
|
+
} else {
|
|
126
|
+
spec = parseSimpleYaml(content);
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return {
|
|
130
|
+
file: filePath,
|
|
131
|
+
version: "unknown",
|
|
132
|
+
info: {},
|
|
133
|
+
endpoints: [],
|
|
134
|
+
schemas: [],
|
|
135
|
+
securitySchemes: [],
|
|
136
|
+
issues: [`파싱 실패: ${e}`],
|
|
137
|
+
summary: {
|
|
138
|
+
totalEndpoints: 0,
|
|
139
|
+
endpointsWithIssues: 0,
|
|
140
|
+
totalSchemas: 0,
|
|
141
|
+
coverage: { hasSummary: 0, hasDescription: 0, hasOperationId: 0, hasResponses: 0 },
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const version = spec.openapi || spec.swagger || "unknown";
|
|
147
|
+
const info = spec.info || {};
|
|
148
|
+
const issues: string[] = [];
|
|
149
|
+
const endpoints: ApiEndpoint[] = [];
|
|
150
|
+
|
|
151
|
+
// 전역 이슈 검사
|
|
152
|
+
if (!info.title) issues.push("info.title 누락");
|
|
153
|
+
if (!info.version) issues.push("info.version 누락");
|
|
154
|
+
if (!info.description) issues.push("info.description 누락 (API 설명 권장)");
|
|
155
|
+
|
|
156
|
+
const servers = spec.servers?.map((s: any) => s.url || s) || [];
|
|
157
|
+
if (servers.length === 0 && !spec.host) {
|
|
158
|
+
issues.push("servers/host 미정의");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// paths 분석
|
|
162
|
+
const paths = spec.paths || {};
|
|
163
|
+
const httpMethods = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
164
|
+
|
|
165
|
+
for (const [pathUrl, pathItem] of Object.entries(paths)) {
|
|
166
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
167
|
+
|
|
168
|
+
for (const method of httpMethods) {
|
|
169
|
+
const operation = (pathItem as any)[method];
|
|
170
|
+
if (!operation) continue;
|
|
171
|
+
|
|
172
|
+
const endpointIssues: string[] = [];
|
|
173
|
+
|
|
174
|
+
// 엔드포인트 정보 추출
|
|
175
|
+
const parameters: ApiEndpoint["parameters"] = [];
|
|
176
|
+
if (operation.parameters) {
|
|
177
|
+
for (const param of operation.parameters) {
|
|
178
|
+
parameters.push({
|
|
179
|
+
name: param.name || "unknown",
|
|
180
|
+
in: param.in || "query",
|
|
181
|
+
required: param.required || false,
|
|
182
|
+
type: param.schema?.type || param.type,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 공통 파라미터
|
|
188
|
+
if ((pathItem as any).parameters) {
|
|
189
|
+
for (const param of (pathItem as any).parameters) {
|
|
190
|
+
parameters.push({
|
|
191
|
+
name: param.name || "unknown",
|
|
192
|
+
in: param.in || "query",
|
|
193
|
+
required: param.required || false,
|
|
194
|
+
type: param.schema?.type || param.type,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 요청 본문
|
|
200
|
+
let requestBody: ApiEndpoint["requestBody"];
|
|
201
|
+
if (operation.requestBody?.content) {
|
|
202
|
+
const contentTypes = Object.keys(operation.requestBody.content);
|
|
203
|
+
requestBody = {
|
|
204
|
+
contentType: contentTypes[0] || "unknown",
|
|
205
|
+
schema: operation.requestBody.content[contentTypes[0]]?.schema?.$ref,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 응답
|
|
210
|
+
const responses: ApiEndpoint["responses"] = [];
|
|
211
|
+
if (operation.responses) {
|
|
212
|
+
for (const [status, response] of Object.entries(operation.responses)) {
|
|
213
|
+
responses.push({
|
|
214
|
+
status,
|
|
215
|
+
description: (response as any)?.description,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 이슈 검사
|
|
221
|
+
if (!operation.summary && !operation.description) {
|
|
222
|
+
endpointIssues.push("summary/description 없음");
|
|
223
|
+
}
|
|
224
|
+
if (!operation.operationId) {
|
|
225
|
+
endpointIssues.push("operationId 없음 (코드 생성 시 필요)");
|
|
226
|
+
}
|
|
227
|
+
if (responses.length === 0) {
|
|
228
|
+
endpointIssues.push("responses 정의 없음");
|
|
229
|
+
} else {
|
|
230
|
+
if (!responses.some((r) => r.status.startsWith("2"))) {
|
|
231
|
+
endpointIssues.push("성공 응답(2xx) 정의 없음");
|
|
232
|
+
}
|
|
233
|
+
if (!responses.some((r) => r.status.startsWith("4") || r.status.startsWith("5"))) {
|
|
234
|
+
endpointIssues.push("에러 응답(4xx/5xx) 정의 없음");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// POST/PUT/PATCH에 requestBody 없음
|
|
239
|
+
if (["post", "put", "patch"].includes(method) && !requestBody && !operation.requestBody) {
|
|
240
|
+
endpointIssues.push(`${method.toUpperCase()} 요청에 requestBody 없음`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 경로 파라미터 검사
|
|
244
|
+
const pathParams = (pathUrl.match(/\{([^}]+)\}/g) || []).map((p) => p.slice(1, -1));
|
|
245
|
+
for (const pathParam of pathParams) {
|
|
246
|
+
if (!parameters.some((p) => p.name === pathParam && p.in === "path")) {
|
|
247
|
+
endpointIssues.push(`경로 파라미터 '${pathParam}' 정의 없음`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 태그 없음
|
|
252
|
+
if (!operation.tags || operation.tags.length === 0) {
|
|
253
|
+
endpointIssues.push("tags 없음 (API 그룹화 권장)");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
endpoints.push({
|
|
257
|
+
path: pathUrl,
|
|
258
|
+
method: method.toUpperCase(),
|
|
259
|
+
operationId: operation.operationId,
|
|
260
|
+
summary: operation.summary,
|
|
261
|
+
tags: operation.tags,
|
|
262
|
+
parameters,
|
|
263
|
+
requestBody,
|
|
264
|
+
responses,
|
|
265
|
+
issues: endpointIssues,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 스키마 추출
|
|
271
|
+
const schemas: string[] = [];
|
|
272
|
+
const components = spec.components || spec.definitions || {};
|
|
273
|
+
if (components.schemas) {
|
|
274
|
+
schemas.push(...Object.keys(components.schemas));
|
|
275
|
+
} else if (spec.definitions) {
|
|
276
|
+
schemas.push(...Object.keys(spec.definitions));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 보안 스키마 추출
|
|
280
|
+
const securitySchemes: string[] = [];
|
|
281
|
+
if (components.securitySchemes) {
|
|
282
|
+
securitySchemes.push(...Object.keys(components.securitySchemes));
|
|
283
|
+
} else if (spec.securityDefinitions) {
|
|
284
|
+
securitySchemes.push(...Object.keys(spec.securityDefinitions));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (securitySchemes.length === 0) {
|
|
288
|
+
issues.push("securitySchemes 미정의 (인증 방식 정의 권장)");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 커버리지 계산
|
|
292
|
+
const coverage = {
|
|
293
|
+
hasSummary: endpoints.filter((e) => e.summary).length,
|
|
294
|
+
hasDescription: endpoints.filter((e) => e.summary).length, // summary로 대체
|
|
295
|
+
hasOperationId: endpoints.filter((e) => e.operationId).length,
|
|
296
|
+
hasResponses: endpoints.filter((e) => e.responses.length > 0).length,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
file: filePath,
|
|
301
|
+
version,
|
|
302
|
+
info: {
|
|
303
|
+
title: info.title,
|
|
304
|
+
version: info.version,
|
|
305
|
+
description: info.description,
|
|
306
|
+
},
|
|
307
|
+
servers,
|
|
308
|
+
endpoints,
|
|
309
|
+
schemas,
|
|
310
|
+
securitySchemes,
|
|
311
|
+
issues,
|
|
312
|
+
summary: {
|
|
313
|
+
totalEndpoints: endpoints.length,
|
|
314
|
+
endpointsWithIssues: endpoints.filter((e) => e.issues.length > 0).length,
|
|
315
|
+
totalSchemas: schemas.length,
|
|
316
|
+
coverage,
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 도구 정의
|
|
322
|
+
export const openapiTools: Tool[] = [
|
|
323
|
+
{
|
|
324
|
+
name: "openapi_check",
|
|
325
|
+
description:
|
|
326
|
+
"OpenAPI/Swagger 스펙 파일을 분석합니다. 엔드포인트, 파라미터, 응답을 추출하고 누락된 필드, 문서화 품질, 베스트 프랙티스를 검사합니다.",
|
|
327
|
+
parameters: {
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: {
|
|
330
|
+
path: {
|
|
331
|
+
type: "string",
|
|
332
|
+
description: "분석할 OpenAPI/Swagger 파일 또는 디렉토리 경로 (.yaml, .yml, .json)",
|
|
333
|
+
},
|
|
334
|
+
recursive: {
|
|
335
|
+
type: "boolean",
|
|
336
|
+
description: "디렉토리인 경우 하위 폴더 포함 여부 (기본: true)",
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
required: ["path"],
|
|
340
|
+
},
|
|
341
|
+
handler: async (args: Record<string, unknown>): Promise<ToolResult> => {
|
|
342
|
+
const targetPath = args.path as string;
|
|
343
|
+
const recursive = args.recursive !== false;
|
|
344
|
+
|
|
345
|
+
if (!fs.existsSync(targetPath)) {
|
|
346
|
+
return {
|
|
347
|
+
success: false,
|
|
348
|
+
content: "",
|
|
349
|
+
error: `경로를 찾을 수 없습니다: ${targetPath}`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const results: OpenApiAnalysisResult[] = [];
|
|
354
|
+
const stats = fs.statSync(targetPath);
|
|
355
|
+
const apiExtensions = [".yaml", ".yml", ".json"];
|
|
356
|
+
|
|
357
|
+
const isOpenApiFile = (filePath: string): boolean => {
|
|
358
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
359
|
+
return content.includes("openapi") || content.includes("swagger") || content.includes("paths:");
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const analyzeFile = (filePath: string) => {
|
|
363
|
+
try {
|
|
364
|
+
if (isOpenApiFile(filePath)) {
|
|
365
|
+
results.push(analyzeOpenApiSpec(filePath));
|
|
366
|
+
}
|
|
367
|
+
} catch (e) {
|
|
368
|
+
// 파싱 실패 무시
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
if (stats.isFile()) {
|
|
373
|
+
analyzeFile(targetPath);
|
|
374
|
+
} else if (stats.isDirectory()) {
|
|
375
|
+
const walkDir = (dir: string) => {
|
|
376
|
+
const files = fs.readdirSync(dir);
|
|
377
|
+
for (const file of files) {
|
|
378
|
+
const filePath = path.join(dir, file);
|
|
379
|
+
const fileStat = fs.statSync(filePath);
|
|
380
|
+
if (fileStat.isDirectory() && recursive) {
|
|
381
|
+
if (!file.startsWith(".") && file !== "node_modules" && file !== "target" && file !== "build") {
|
|
382
|
+
walkDir(filePath);
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
const ext = path.extname(file).toLowerCase();
|
|
386
|
+
if (apiExtensions.includes(ext)) {
|
|
387
|
+
analyzeFile(filePath);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
walkDir(targetPath);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 전체 통계
|
|
396
|
+
const totalEndpoints = results.reduce((sum, r) => sum + r.summary.totalEndpoints, 0);
|
|
397
|
+
const totalWithIssues = results.reduce((sum, r) => sum + r.summary.endpointsWithIssues, 0);
|
|
398
|
+
|
|
399
|
+
const output = {
|
|
400
|
+
analyzedFiles: results.length,
|
|
401
|
+
totalEndpoints,
|
|
402
|
+
endpointsWithIssues: totalWithIssues,
|
|
403
|
+
specs: results.map((r) => ({
|
|
404
|
+
file: r.file,
|
|
405
|
+
version: r.version,
|
|
406
|
+
info: r.info,
|
|
407
|
+
servers: r.servers,
|
|
408
|
+
schemas: r.schemas,
|
|
409
|
+
securitySchemes: r.securitySchemes,
|
|
410
|
+
specIssues: r.issues,
|
|
411
|
+
summary: r.summary,
|
|
412
|
+
endpoints: r.endpoints.map((e) => ({
|
|
413
|
+
method: e.method,
|
|
414
|
+
path: e.path,
|
|
415
|
+
operationId: e.operationId,
|
|
416
|
+
summary: e.summary,
|
|
417
|
+
tags: e.tags,
|
|
418
|
+
parametersCount: e.parameters.length,
|
|
419
|
+
responsesCount: e.responses.length,
|
|
420
|
+
issues: e.issues,
|
|
421
|
+
})),
|
|
422
|
+
})),
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
success: true,
|
|
427
|
+
content: JSON.stringify(output, null, 2),
|
|
428
|
+
};
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
];
|