UNPKG

codn_ts

Version:

智能代码分析工具 - 支持语义搜索、调用链分析和代码结构可视化,对大模型/AI agent 友好

704 lines 28.2 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const commander_1 = require("commander"); const index_1 = require("./index"); const search_1 = require("./search"); const ls_1 = require("./ls"); const cat_1 = require("./cat"); const process = __importStar(require("process")); const callChain_1 = require("./callChain"); const index_2 = require("./index"); const mcp_1 = require("./mcp"); const index_3 = require("./index"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const program = new commander_1.Command(); // 辅助函数:生成 Mermaid 图表 function generateMermaidDiagram(result, opts) { let mermaid = "graph TD\n"; for (const symbolRef of result) { const nodeId = symbolRef.symbol.replace(/[^a-zA-Z0-9]/g, "_"); // 添加主节点 mermaid += ` ${nodeId}["${symbolRef.symbol}<br/>${symbolRef.file}:${symbolRef.line}"]\n`; // 添加调用方 for (const caller of symbolRef.callers) { const callerId = caller.caller.replace(/[^a-zA-Z0-9]/g, "_"); mermaid += ` ${callerId}["${caller.caller}"]\n`; mermaid += ` ${callerId} --> ${nodeId}\n`; } // 添加被调用方 for (const callee of symbolRef.callees) { const calleeId = callee.callee.replace(/[^a-zA-Z0-9]/g, "_"); mermaid += ` ${calleeId}["${callee.callee}"]\n`; mermaid += ` ${nodeId} --> ${calleeId}\n`; } } return mermaid; } // 辅助函数:格式化文本输出 function formatTextOutput(result, opts) { let output = ""; for (const symbolRef of result) { output += `符号: ${symbolRef.symbol}\n`; output += `类型: ${symbolRef.kind}\n`; output += `位置: ${symbolRef.file}:${symbolRef.line}:${symbolRef.column}\n`; if (opts.showDefinition && symbolRef.definition) { output += `定义: ${symbolRef.definition.content}\n`; } if (opts.callers || !opts.callees) { output += `\n调用方 (${symbolRef.callers.length} 个):\n`; if (symbolRef.callers.length === 0) { output += " 无\n"; } else { for (const caller of symbolRef.callers) { output += ` ${caller.caller} @ ${caller.location}\n`; } } } if (opts.callees || !opts.callers) { output += `\n被调用方 (${symbolRef.callees.length} 个):\n`; if (symbolRef.callees.length === 0) { output += " 无\n"; } else { for (const callee of symbolRef.callees) { output += ` ${callee.callee} @ ${callee.location}\n`; } } } output += "\n" + "=".repeat(50) + "\n\n"; } return output; } function summarize(references) { const files = new Set(); const callers = new Set(); const callees = new Set(); for (const ref of references) { files.add(ref.caller.split(":")[0]); files.add(ref.callee.split(":")[0]); callers.add(ref.caller); callees.add(ref.callee); } return { totalReferences: references.length, totalFiles: files.size, totalCallers: callers.size, totalCallees: callees.size, }; } // 读取 package.json 中的版本号 const packageJsonPath = path.join(__dirname, "..", "package.json"); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); const version = packageJson.version; program.name("cod").description("代码分析工具").version(version); // analyze 命令 program .command("analyze") .description("分析项目中的函数/方法引用关系") .option("-p, --project <path>", "项目根目录,默认为当前目录", process.cwd()) .option("--json", "以 JSON 格式输出", false) .option("--summary", "只输出统计信息", false) .option("--call-chain", "生成调用链可视化", false) .option("--from <functions>", "起始函数列表,用逗号分隔", "") .option("--to <functions>", "目标函数列表,用逗号分隔", "") .option("--depth <number>", "调用链最大深度", "10") .option("--format <format>", "可视化格式: text|dot|mermaid|json-graph", "text") .option("--output <file>", "输出文件路径", "") .option("--filter <pattern>", "文件过滤模式", "") .option("--exclude <pattern>", "排除文件模式", "") .option("--include-external", "包含外部依赖调用", false) .option("--show-cycles", "显示循环调用", false) .option("--min-calls <number>", "最小调用次数阈值", "1") .action(async (opts) => { try { const result = await (0, index_1.analyzeProjectReferences)(opts.project); const summary = summarize(result); // 调用链可视化处理 if (opts.callChain || opts["call-chain"]) { const callChainResult = await (0, callChain_1.generateCallChain)(result, { from: opts.from ? opts.from.split(",").map((f) => f.trim()) : [], to: opts.to ? opts.to.split(",").map((f) => f.trim()) : [], maxDepth: parseInt(opts.depth), format: opts.format, includeExternal: opts.includeExternal, showCycles: opts.showCycles, minCalls: parseInt(opts.minCalls), filter: opts.filter, exclude: opts.exclude, }); if (opts.output) { // 输出到文件 await (0, callChain_1.writeCallChainToFile)(callChainResult, opts.output, opts.format); console.log(`调用链已保存到: ${opts.output}`); } else if (opts.json) { console.log(JSON.stringify(callChainResult, null, 2)); } else { // 控制台输出 (0, callChain_1.displayCallChain)(callChainResult, opts.format); } return; } // 原有的分析逻辑 if (opts.json) { if (opts.summary) { console.log(JSON.stringify({ summary }, null, 2)); } else { console.log(JSON.stringify({ summary, references: result }, null, 2)); } return; } if (opts.summary) { console.log(`引用总数: ${summary.totalReferences}`); console.log(`涉及文件数: ${summary.totalFiles}`); console.log(`调用方数: ${summary.totalCallers}`); console.log(`被调方数: ${summary.totalCallees}`); } else { for (const ref of result) { console.log(`${ref.caller} -> ${ref.callee} @ ${ref.location}`); } } } catch (err) { if (opts.json) { console.log(JSON.stringify({ error: { message: err.message, stack: err.stack } }, null, 2)); } else { console.error("分析失败:", err); } process.exit(1); } }); // search 命令 program .command("search") .description("搜索项目中的文件、函数、类、文件内容等") .argument("[query]", "搜索查询(可选,默认为空字符串)") .option("-p, --project <path>", "项目根目录,默认为当前目录", process.cwd()) .option("-f, --file <path>", "限制搜索到指定文件") .option("--json", "以 JSON 格式输出", false) .option("-t, --type <type>", "搜索类型: file|function|class|interface|content|semantic|all", "all") .option("-l, --limit <number>", "最大结果数量", "20") .option("-i, --case-insensitive", "忽略大小写", false) .option("-r, --regex", "使用正则表达式", false) .option("-C, --context <lines>", "显示匹配行前后的行数", "2") .option("-s, --semantic", "启用语义搜索", false) .option("--min-relevance <score>", "语义搜索最小相关性分数 (0-1)", "0.1") .action(async (query = "", opts) => { try { // If query is undefined (not provided), set it to empty string const searchQuery = query || ""; // Handle multiple types (comma-separated) const types = opts.type.split(",").map((t) => t.trim()); let allResults = []; if (types.length === 1) { // Single type - use original logic const result = await (0, search_1.searchProject)(searchQuery, opts.project, { type: opts.type, limit: parseInt(opts.limit), caseSensitive: !opts.caseInsensitive, regex: opts.regex, contextLines: parseInt(opts.context), semantic: opts.semantic, minRelevance: parseFloat(opts.minRelevance), file: opts.file, }); allResults = result; } else { // Multiple types - search each type separately const limit = Math.ceil(parseInt(opts.limit) / types.length); for (const singleType of types) { const result = await (0, search_1.searchProject)(searchQuery, opts.project, { type: singleType, limit: limit, caseSensitive: !opts.caseInsensitive, regex: opts.regex, contextLines: parseInt(opts.context), semantic: opts.semantic, minRelevance: parseFloat(opts.minRelevance), file: opts.file, }); allResults.push(...result); } // Remove duplicates and apply overall limit const seen = new Set(); allResults = allResults .filter((item) => { const key = `${item.file}:${item.line}:${item.column}:${item.name}`; if (seen.has(key)) return false; seen.add(key); return true; }) .slice(0, parseInt(opts.limit)); } const result = allResults; if (opts.json) { console.log(JSON.stringify({ query: searchQuery, results: result }, null, 2)); } else { if (result.length === 0) { console.log(`未找到匹配 "${searchQuery}" 的结果`); return; } console.log(`找到 ${result.length} 个匹配 "${searchQuery}" 的结果:`); console.log(); for (const item of result) { console.log(`${item.type.toUpperCase()}: ${item.name}`); console.log(` 文件: ${item.file}`); console.log(` 位置: ${item.line}:${item.column}`); if (item.description) { console.log(` 描述: ${item.description}`); } if (item.relevance !== undefined) { console.log(` 相关性: ${(item.relevance * 100).toFixed(1)}%`); } if (item.keywords && item.keywords.length > 0) { console.log(` 关键词: ${item.keywords.join(", ")}`); } if (item.content) { console.log(` 内容: ${item.content}`); } if (item.context) { console.log(` 上下文:`); console.log(` ${item.context.split("\n").join("\n ")}`); } console.log(); } } } catch (err) { if (opts.json) { console.log(JSON.stringify({ error: { message: err.message, stack: err.stack } }, null, 2)); } else { console.error("搜索失败:", err); } process.exit(1); } }); // ls 命令 program .command("ls") .description("列出目录内容") .argument("[path]", "目录路径,默认为当前目录", ".") .option("--json", "以 JSON 格式输出", false) .option("-a, --all", "显示隐藏文件", false) .option("-l, --long", "显示详细信息", false) .option("-d, --depth <number>", "递归深度", "1") .option("--ignore <patterns>", "忽略的文件模式,用逗号分隔", "node_modules,dist,.git") .action(async (dirPath, opts) => { try { const ignorePatterns = opts.ignore.split(",").map((p) => p.trim()); const result = await (0, ls_1.listDirectory)(dirPath, { showHidden: opts.all, showDetails: opts.long, maxDepth: parseInt(opts.depth), ignorePatterns, }); if (opts.json) { console.log(JSON.stringify({ path: dirPath, items: result }, null, 2)); } else { if (result.length === 0) { console.log(`目录 "${dirPath}" 为空或不存在`); return; } console.log(`目录: ${dirPath}`); console.log(); // 按类型分组显示 const files = result.filter((item) => item.type === "file"); const dirs = result.filter((item) => item.type === "directory"); if (dirs.length > 0) { console.log("📁 目录:"); for (const dir of dirs) { if (opts.long) { console.log(` ${dir.name.padEnd(30)} ${dir.size?.toString().padStart(10)} ${dir.modified}`); } else { console.log(` ${dir.name}/`); } } console.log(); } if (files.length > 0) { console.log("📄 文件:"); for (const file of files) { if (opts.long) { console.log(` ${file.name.padEnd(30)} ${file.size?.toString().padStart(10)} ${file.modified}`); } else { console.log(` ${file.name}`); } } } console.log(); console.log(`总计: ${dirs.length} 个目录, ${files.length} 个文件`); } } catch (err) { if (opts.json) { console.log(JSON.stringify({ error: { message: err.message, stack: err.stack } }, null, 2)); } else { console.error("列出目录失败:", err); } process.exit(1); } }); // cat 命令 program .command("cat") .description("读取文件内容") .argument("<file>", "文件路径") .option("--json", "以 JSON 格式输出", false) .option("-n, --line-numbers", "显示行号", false) .option("-s, --start <line>", "起始行号", "1") .option("-e, --end <line>", "结束行号", "-1") .option("--encoding <encoding>", "文件编码", "utf-8") .option("--info", "只显示文件信息,不读取内容", false) .option("-p, --file <file>", "指定文件或目录", ".") .action(async (filePath, opts) => { try { if (opts.info) { // 只显示文件信息 const info = (0, cat_1.getFileInfo)(filePath); const fileType = (0, cat_1.detectFileType)(filePath); if (opts.json) { console.log(JSON.stringify({ ...info, type: fileType }, null, 2)); } else { if (!info.exists) { console.log(`文件不存在: ${filePath}`); return; } if (!info.isFile) { console.log(`路径不是文件: ${filePath}`); return; } console.log(`文件: ${info.file}`); console.log(`类型: ${fileType}`); console.log(`大小: ${info.size} 字节`); console.log(`修改时间: ${info.modified}`); console.log(`编码: ${info.encoding}`); } return; } // 读取文件内容 const result = await (0, cat_1.catFile)(filePath, { showLineNumbers: opts.lineNumbers, startLine: parseInt(opts.start), endLine: parseInt(opts.end), encoding: opts.encoding, }); if (opts.json) { console.log(JSON.stringify(result, null, 2)); } else { console.log(`文件: ${result.file} (${result.totalLines} 行, ${result.size} 字节)`); console.log(`范围: ${result.startLine}-${result.endLine}`); console.log(`编码: ${result.encoding}, 修改时间: ${result.modified}`); console.log(); console.log(result.content); } } catch (err) { if (opts.json) { console.log(JSON.stringify({ error: { message: err.message, stack: err.stack } }, null, 2)); } else { console.error("读取文件失败:", err); } process.exit(1); } }); // ref 命令 program .command("ref") .description("分析特定函数/符号的引用关系") .option("-s, --symbol <name>", "要分析的符号名称", "") .option("-p, --project <path>", "项目根目录,默认为当前目录", process.cwd()) .option("--json", "以 JSON 格式输出", false) .option("--callers", "显示调用方", false) .option("--callees", "显示被调用方", false) .option("--format <format>", "输出格式: text|json|mermaid", "text") .option("--depth <number>", "分析深度", "3") .option("--include-external", "包含外部依赖调用", false) .option("--case-sensitive", "区分大小写", false) .option("--show-definition", "显示符号定义", false) .option("--output <file>", "输出到文件", "") .action(async (opts) => { try { if (!opts.symbol) { const errorMsg = "错误: 必须指定符号名称 (-s, --symbol)"; if (opts.json) { console.log(JSON.stringify({ error: { message: errorMsg } }, null, 2)); } else { console.error(errorMsg); console.log("用法: cod ref -s <symbol_name>"); } process.exit(1); } const { analyzeSymbolReferences } = await Promise.resolve().then(() => __importStar(require("./utils/reference_analyzer"))); const result = await analyzeSymbolReferences(opts.project, opts.symbol, { includeExternal: opts.includeExternal, maxDepth: parseInt(opts.depth), caseSensitive: opts.caseSensitive, }); if (result.length === 0) { const notFoundMsg = `未找到符号 "${opts.symbol}" 的引用关系`; if (opts.json) { console.log(JSON.stringify({ symbol: opts.symbol, message: notFoundMsg, results: [], }, null, 2)); } else { console.log(notFoundMsg); } return; } // 处理输出格式 if (opts.output) { const fs = await Promise.resolve().then(() => __importStar(require("fs/promises"))); let outputContent = ""; if (opts.format === "mermaid") { outputContent = generateMermaidDiagram(result, opts); } else if (opts.json) { outputContent = JSON.stringify({ symbol: opts.symbol, project: opts.project, results: result, }, null, 2); } else { outputContent = formatTextOutput(result, opts); } await fs.writeFile(opts.output, outputContent, "utf-8"); console.log(`引用分析结果已保存到: ${opts.output}`); return; } if (opts.json) { console.log(JSON.stringify({ symbol: opts.symbol, project: opts.project, results: result, }, null, 2)); } else if (opts.format === "mermaid") { console.log(generateMermaidDiagram(result, opts)); } else { console.log(formatTextOutput(result, opts)); } } catch (err) { if (opts.json) { console.log(JSON.stringify({ error: { message: err.message, stack: err.stack }, }, null, 2)); } else { console.error("引用分析失败:", err); } process.exit(1); } }); // show 命令 program .command("show") .description("展示文件区间或类定义(完整/方法列表/初始化)") .option("-f, --file <path>", "文件路径") .option("-p, --project <dir>", "项目根目录,自动查找符号文件") .option("-s, --symbol <name>", "符号名称(如类名)") .option("--start <line>", "起始行号", "1") .option("--end <line>", "结束行号", "-1") .option("-m, --mode <mode>", "展示模式: full|methods|init", "full") .option("-c, --context <lines>", "上下文行数", "0") .option("--json", "JSON 格式输出", false) .action(async (opts) => { try { // 参数校验:mode 必须是 full/methods/init 之一 const validModes = ["full", "methods", "init"]; if (opts.mode && !validModes.includes(opts.mode)) { const msg = `参数错误: --mode/-m 仅支持 full|methods|init,当前为 '${opts.mode}'`; if (opts.json) { console.log(JSON.stringify({ error: { message: msg } }, null, 2)); } else { console.error(msg); } process.exit(1); } let file = opts.file; if (!file && opts.project && opts.symbol) { // 自动查找 symbol 所在文件 const { searchProject } = await Promise.resolve().then(() => __importStar(require("./search"))); const results = await searchProject(opts.symbol, opts.project, { type: "class", limit: 1, }); if (results && results.length > 0) { file = results[0].file; console.log(`[自动定位] 符号 ${opts.symbol} 位于文件: ${file}`); } else { console.error(`未找到符号 ${opts.symbol} 对应的文件`); return; } } if (!file) file = "."; if (opts.symbol) { // 展示类定义 const result = await (0, index_2.showClassDefinition)({ file, symbol: opts.symbol, mode: opts.mode, }); if (opts.json) { console.log(JSON.stringify(result, null, 2)); } else { if (opts.mode === "methods") { // methods 模式直接输出 result.content,保留分文件分组格式 console.log(`类: ${result.symbol}`); console.log(result.content); } else { // 其他模式原样输出 console.log(`文件: ${result.file}`); console.log(`符号: ${result.symbol}`); console.log(`模式: ${result.mode}`); console.log(`范围: ${result.startLine}-${result.endLine}`); console.log("\n" + result.content); } } } else { // 展示文件区间 const result = (0, index_2.showFileRange)({ file, startLine: parseInt(opts.start), endLine: parseInt(opts.end), contextLines: parseInt(opts.context), }); if (opts.json) { console.log(JSON.stringify(result, null, 2)); } else { console.log(`文件: ${result.file}`); console.log(`范围: ${result.startLine}-${result.endLine}`); console.log("\n" + result.content); } } } catch (err) { if (opts.json) { console.log(JSON.stringify({ error: { message: err.message, stack: err.stack } }, null, 2)); } else { console.error("展示失败:", err); } process.exit(1); } }); // mcp 命令 program .command("mcp") .description("以 MCP 协议模式启动服务") .option("-p, --port <port>", "监听端口", "7788") .action((opts) => { (0, mcp_1.startMcpServer)(Number(opts.port)); }); // check 命令 program .command("check") .description("LSP 诊断检查,输出语法/类型错误和警告") .option("-f, --file <file>", "指定单个文件进行检查") .option("-p, --project <dir>", "指定项目根目录,默认为当前目录", process.cwd()) .option("--json", "以 JSON 格式输出", false) .action(async (opts) => { try { const diagnostics = await (0, index_3.collectDiagnostics)({ projectRoot: opts.project, file: opts.file || "", }); if (opts.json) { console.log(JSON.stringify(diagnostics, null, 2)); } else { if (diagnostics.length === 0) { console.log("\x1b[32m✔ 没有发现错误或警告\x1b[0m"); } else { for (const d of diagnostics) { let color = "\x1b[37m"; if (d.severity === "error") color = "\x1b[31m"; else if (d.severity === "warning") color = "\x1b[33m"; else if (d.severity === "info") color = "\x1b[36m"; else if (d.severity === "hint") color = "\x1b[34m"; console.log(`${color}${d.file}:${d.line}:${d.column} ${d.severity.padEnd(7)} ${d.message}\x1b[0m`); } } } } catch (err) { console.error("诊断失败:", err); process.exit(1); } }); // 默认命令(向后兼容) program.action(async () => { console.log("使用 'cod analyze' 分析引用关系,或使用 'cod search <query>' 搜索代码"); console.log("运行 'cod --help' 查看所有命令"); }); program.parse(process.argv); //# sourceMappingURL=cli.js.map