devops-analyzer 0.1.0__cp312-cp312-win_amd64.whl

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.
devops/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Devops:扫描源码、提取代码块、大模型分析并生成 README。"""
2
+
3
+ __version__ = "0.1.0"
devops/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from devops.cli import main
2
+
3
+ raise SystemExit(main())
devops/analysis.py ADDED
@@ -0,0 +1,234 @@
1
+ """调用大模型 API 分析 extract 提取的代码块。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import defaultdict
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from devops.extract import CodeBlock, extract_blocks_from_paths
11
+ from devops.models import BaseChatModel, get_chat_model
12
+
13
+ MAX_CODE_LINES = 80
14
+ MAX_CHARS_PER_REQUEST = 24_000
15
+
16
+ SYSTEM_PROMPT = """你是一名资深代码审查专家。
17
+ 请根据给出的代码块,输出简洁、结构化的中文分析,包含:
18
+ 1. 文件/模块职责概述
19
+ 2. 主要函数/类的作用
20
+ 3. 潜在问题或改进建议(如有)
21
+ 4. 关键依赖或调用关系(如能看出)
22
+
23
+ 直接输出分析正文,不要重复粘贴完整源码。"""
24
+
25
+
26
+ def _truncate_code(code: str, max_lines: int = MAX_CODE_LINES) -> str:
27
+ lines = code.splitlines()
28
+ if len(lines) <= max_lines:
29
+ return code
30
+ head = "\n".join(lines[:max_lines])
31
+ return f"{head}\n# ... (省略 {len(lines) - max_lines} 行)"
32
+
33
+
34
+ def _format_blocks_for_prompt(blocks: list[CodeBlock]) -> str:
35
+ parts: list[str] = []
36
+ for i, block in enumerate(blocks, 1):
37
+ title = block.name or "(anonymous)"
38
+ parts.append(
39
+ f"### 块 {i}: {block.block_type} `{title}` "
40
+ f"(L{block.start_line}-L{block.end_line})\n"
41
+ f"```{block.language}\n{_truncate_code(block.code)}\n```"
42
+ )
43
+ return "\n\n".join(parts)
44
+
45
+
46
+ def _chat(model: BaseChatModel, prompt: str) -> str:
47
+ return model.chat(SYSTEM_PROMPT, prompt, temperature=0.2)
48
+
49
+
50
+ def analyze_blocks(
51
+ blocks: list[CodeBlock],
52
+ *,
53
+ provider: str | None = None,
54
+ api_key: str | None = None,
55
+ model_name: str | None = None,
56
+ base_url: str | None = None,
57
+ ) -> dict[str, Any]:
58
+ """按文件分批调用大模型,并生成总览。"""
59
+ chat_model = get_chat_model(
60
+ provider=provider,
61
+ api_key=api_key,
62
+ model=model_name,
63
+ base_url=base_url,
64
+ )
65
+
66
+ if not blocks:
67
+ return {
68
+ "summary": "未提取到任何代码块,请确认目录内有支持的源码文件。",
69
+ "files": [],
70
+ "blocks_count": 0,
71
+ }
72
+
73
+ by_file: dict[str, list[CodeBlock]] = defaultdict(list)
74
+ for block in blocks:
75
+ by_file[block.file_path].append(block)
76
+
77
+ per_file: list[dict[str, Any]] = []
78
+ file_summaries: list[str] = []
79
+
80
+ for file_path, file_blocks in sorted(by_file.items()):
81
+ body = _format_blocks_for_prompt(file_blocks)
82
+ if len(body) > MAX_CHARS_PER_REQUEST:
83
+ body = body[:MAX_CHARS_PER_REQUEST] + "\n\n...(内容过长已截断)"
84
+
85
+ prompt = (
86
+ f"文件路径: {file_path}\n"
87
+ f"语言: {file_blocks[0].language}\n"
88
+ f"代码块数量: {len(file_blocks)}\n\n"
89
+ f"{body}"
90
+ )
91
+ analysis = _chat(chat_model, prompt)
92
+ per_file.append(
93
+ {
94
+ "file_path": file_path,
95
+ "blocks_count": len(file_blocks),
96
+ "analysis": analysis,
97
+ }
98
+ )
99
+ file_summaries.append(f"【{file_path}】\n{analysis}")
100
+
101
+ overview_prompt = (
102
+ "以下是多个源文件的逐项分析,请生成一份项目级总览(中文):\n"
103
+ "1. 项目整体结构\n"
104
+ "2. 各文件之间的关系\n"
105
+ "3. 共性问题或架构建议\n\n"
106
+ + "\n\n---\n\n".join(file_summaries)
107
+ )
108
+ if len(overview_prompt) > MAX_CHARS_PER_REQUEST:
109
+ overview_prompt = overview_prompt[:MAX_CHARS_PER_REQUEST] + "\n...(已截断)"
110
+
111
+ summary = (
112
+ _chat(chat_model, overview_prompt)
113
+ if len(per_file) > 1
114
+ else per_file[0]["analysis"]
115
+ )
116
+
117
+ return {
118
+ "summary": summary,
119
+ "files": per_file,
120
+ "blocks_count": len(blocks),
121
+ "files_count": len(per_file),
122
+ "model_provider": chat_model.config.provider,
123
+ "model_name": chat_model.config.model,
124
+ }
125
+
126
+
127
+ def analyze_files(
128
+ paths: list[str],
129
+ *,
130
+ provider: str | None = None,
131
+ api_key: str | None = None,
132
+ model_name: str | None = None,
133
+ base_url: str | None = None,
134
+ ) -> dict[str, Any]:
135
+ """从文件路径列表提取代码块并分析。"""
136
+ blocks = extract_blocks_from_paths(paths)
137
+ return analyze_blocks(
138
+ blocks,
139
+ provider=provider,
140
+ api_key=api_key,
141
+ model_name=model_name,
142
+ base_url=base_url,
143
+ )
144
+
145
+
146
+ def analyze_folder(
147
+ folder: str,
148
+ *,
149
+ provider: str | None = None,
150
+ api_key: str | None = None,
151
+ model_name: str | None = None,
152
+ base_url: str | None = None,
153
+ ) -> dict[str, Any]:
154
+ """扫描目录、提取代码块并分析。"""
155
+ from devops.scan import scan_code_files
156
+
157
+ folder_path = Path(folder).resolve()
158
+ paths = scan_code_files(str(folder_path))
159
+ result = analyze_files(
160
+ paths,
161
+ provider=provider,
162
+ api_key=api_key,
163
+ model_name=model_name,
164
+ base_url=base_url,
165
+ )
166
+ result["folder"] = str(folder_path)
167
+ result["project_name"] = folder_path.name
168
+ result["scanned_files_count"] = len(paths)
169
+ result["generated_at"] = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
170
+ return result
171
+
172
+
173
+ def render_readme(result: dict[str, Any]) -> str:
174
+ """将分析结果渲染为 README Markdown 正文。"""
175
+ project = result.get("project_name") or Path(result.get("folder", ".")).name
176
+ folder = result.get("folder", "")
177
+ generated_at = result.get("generated_at", "")
178
+ scanned = result.get("scanned_files_count", 0)
179
+ blocks = result.get("blocks_count", 0)
180
+ files_count = result.get("files_count", 0)
181
+ summary = result.get("summary", "").strip()
182
+ per_file: list[dict[str, Any]] = result.get("files", [])
183
+
184
+ lines: list[str] = [
185
+ f"# {project}",
186
+ "",
187
+ "> 本文档由代码分析工具自动生成。",
188
+ "",
189
+ "## 概览",
190
+ "",
191
+ summary or "_暂无分析内容。_",
192
+ "",
193
+ "## 统计",
194
+ "",
195
+ "| 指标 | 数值 |",
196
+ "| --- | --- |",
197
+ f"| 项目路径 | `{folder}` |",
198
+ f"| 扫描文件数 | {scanned} |",
199
+ f"| 提取代码块数 | {blocks} |",
200
+ f"| 已分析文件数 | {files_count} |",
201
+ f"| 生成时间 | {generated_at} |",
202
+ f"| 分析模型 | {result.get('model_provider', '')} / {result.get('model_name', '')} |",
203
+ "",
204
+ ]
205
+
206
+ if per_file:
207
+ lines.extend(["## 文件分析", ""])
208
+ for item in per_file:
209
+ rel = item.get("file_path", "")
210
+ if folder and rel.startswith(folder):
211
+ rel = rel[len(folder) :].lstrip("/\\")
212
+ count = item.get("blocks_count", 0)
213
+ analysis = (item.get("analysis") or "").strip()
214
+ lines.append(f"### `{rel}`")
215
+ lines.append("")
216
+ lines.append(f"- **代码块数量**: {count}")
217
+ lines.append("")
218
+ lines.append(analysis or "_无分析结果。_")
219
+ lines.append("")
220
+
221
+ lines.append("---")
222
+ lines.append("")
223
+ lines.append("*Generated by Devops code analyzer*")
224
+ lines.append("")
225
+ return "\n".join(lines)
226
+
227
+
228
+ def write_project_readme(folder: str, result: dict[str, Any], filename: str = "README.md") -> Path:
229
+ """将 Markdown 写入目标项目目录下的 README.md。"""
230
+ folder_path = Path(folder).resolve()
231
+ output_path = folder_path / filename
232
+ markdown = render_readme(result)
233
+ output_path.write_text(markdown, encoding="utf-8")
234
+ return output_path
devops/cli.py ADDED
@@ -0,0 +1,190 @@
1
+ """终端入口:devops <项目路径> | devops config"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from devops import __version__
10
+ from devops.analysis import analyze_folder, write_project_readme
11
+ from devops.config import config_exists
12
+ from devops.config_cmd import run_config
13
+
14
+
15
+ def build_parser() -> argparse.ArgumentParser:
16
+ parser = argparse.ArgumentParser(
17
+ prog="devops",
18
+ description="扫描项目源码,提取代码块,调用大模型分析并生成 README.md",
19
+ )
20
+ parser.add_argument(
21
+ "-V",
22
+ "--version",
23
+ action="version",
24
+ version=f"%(prog)s {__version__}",
25
+ )
26
+
27
+ subparsers = parser.add_subparsers(dest="command")
28
+
29
+ config_parser = subparsers.add_parser(
30
+ "config",
31
+ help="配置模型 API(首次使用前必须执行)",
32
+ )
33
+ config_parser.add_argument(
34
+ "-s",
35
+ "--show",
36
+ action="store_true",
37
+ help="查看当前配置(API Key 脱敏)",
38
+ )
39
+
40
+ analyze_parser = subparsers.add_parser(
41
+ "analyze",
42
+ help="分析项目并生成 README",
43
+ )
44
+ analyze_parser.add_argument(
45
+ "project",
46
+ nargs="?",
47
+ help="要分析的项目目录路径",
48
+ )
49
+ analyze_parser.add_argument(
50
+ "-o",
51
+ "--output",
52
+ default="README.md",
53
+ help="输出 Markdown 文件名(默认: README.md)",
54
+ )
55
+ analyze_parser.add_argument(
56
+ "--provider",
57
+ choices=["openai", "zhipuai", "deepseek", "moonshot"],
58
+ help="覆盖配置文件中的模型提供商",
59
+ )
60
+ analyze_parser.add_argument(
61
+ "--model",
62
+ dest="model_name",
63
+ help="覆盖配置文件中的模型名称",
64
+ )
65
+ analyze_parser.add_argument(
66
+ "--api-key",
67
+ help="临时指定 API Key(不推荐,会留在 shell 历史)",
68
+ )
69
+ analyze_parser.add_argument(
70
+ "--base-url",
71
+ help="临时指定 API Base URL(OpenAI 兼容接口)",
72
+ )
73
+
74
+ # 兼容: devops /path/to/project(无子命令)
75
+ parser.add_argument(
76
+ "project_legacy",
77
+ nargs="?",
78
+ help=argparse.SUPPRESS,
79
+ )
80
+ parser.add_argument(
81
+ "-o",
82
+ "--output",
83
+ default="README.md",
84
+ help=argparse.SUPPRESS,
85
+ )
86
+ parser.add_argument(
87
+ "--provider",
88
+ choices=["openai", "zhipuai", "deepseek", "moonshot"],
89
+ help=argparse.SUPPRESS,
90
+ )
91
+ parser.add_argument(
92
+ "--model",
93
+ dest="model_name",
94
+ help=argparse.SUPPRESS,
95
+ )
96
+ parser.add_argument(
97
+ "--api-key",
98
+ help=argparse.SUPPRESS,
99
+ )
100
+ parser.add_argument(
101
+ "--base-url",
102
+ help=argparse.SUPPRESS,
103
+ )
104
+
105
+ return parser
106
+
107
+
108
+ def _run_analyze(args: argparse.Namespace) -> int:
109
+ folder = getattr(args, "project", None) or getattr(args, "project_legacy", None)
110
+ if not folder:
111
+ folder = input("请输入项目文件夹路径: ").strip()
112
+ if not folder:
113
+ print("错误: 未提供项目路径", file=sys.stderr)
114
+ return 1
115
+
116
+ if not config_exists() and not args.api_key:
117
+ print(
118
+ "错误: 尚未配置模型 API。请先运行:\n\n"
119
+ " devops config\n",
120
+ file=sys.stderr,
121
+ )
122
+ return 1
123
+
124
+ folder_path = Path(folder).expanduser().resolve()
125
+ if not folder_path.is_dir():
126
+ print(f"错误: 不是有效目录: {folder_path}", file=sys.stderr)
127
+ return 1
128
+
129
+ try:
130
+ from devops.config import load_config
131
+
132
+ cfg = load_config(
133
+ provider=args.provider,
134
+ api_key=args.api_key,
135
+ model=args.model_name,
136
+ base_url=args.base_url,
137
+ )
138
+ print(
139
+ f"正在分析: {folder_path} "
140
+ f"[{cfg.provider} / {cfg.model}]"
141
+ )
142
+ result = analyze_folder(
143
+ str(folder_path),
144
+ provider=args.provider,
145
+ api_key=args.api_key,
146
+ model_name=args.model_name,
147
+ base_url=args.base_url,
148
+ )
149
+ output = write_project_readme(
150
+ str(folder_path), result, filename=args.output
151
+ )
152
+ print(f"已生成: {output}")
153
+ return 0
154
+ except KeyboardInterrupt:
155
+ print("\n已取消", file=sys.stderr)
156
+ return 130
157
+ except Exception as exc:
158
+ print(f"错误: {exc}", file=sys.stderr)
159
+ return 1
160
+
161
+
162
+ def main(argv: list[str] | None = None) -> int:
163
+ argv = list(argv) if argv is not None else sys.argv[1:]
164
+
165
+ # devops /path/to/project -> 自动当作 analyze
166
+ if argv and not argv[0].startswith("-") and argv[0] not in ("config", "analyze"):
167
+ argv = ["analyze", *argv]
168
+
169
+ parser = build_parser()
170
+ args = parser.parse_args(argv)
171
+
172
+ if args.command == "config":
173
+ extra = ["--show"] if args.show else []
174
+ return run_config(extra)
175
+
176
+ if args.command == "analyze" or args.project_legacy is not None:
177
+ return _run_analyze(args)
178
+
179
+ parser.print_help()
180
+ print(
181
+ "\n快速开始:\n"
182
+ " 1. devops config # 首次配置 API\n"
183
+ " 2. devops /path/to/project\n",
184
+ file=sys.stderr,
185
+ )
186
+ return 0
187
+
188
+
189
+ if __name__ == "__main__":
190
+ raise SystemExit(main())
devops/config.py ADDED
@@ -0,0 +1,159 @@
1
+ """用户模型 API 配置(~/.devops/config.json)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from dataclasses import asdict, dataclass
8
+ from pathlib import Path
9
+ from typing import Any, Optional
10
+
11
+ CONFIG_DIR = Path.home() / ".devops"
12
+ CONFIG_FILE = CONFIG_DIR / "config.json"
13
+
14
+ # provider -> (默认模型, 环境变量 API Key 备选名, 环境变量 Base URL 备选名)
15
+ PROVIDER_DEFAULTS: dict[str, dict[str, Any]] = {
16
+ "openai": {
17
+ "model": "gpt-4o-mini",
18
+ "base_url": None,
19
+ "api_key_env": ["DEVOPS_API_KEY", "OPENAI_API_KEY", "LLM_API_KEY"],
20
+ "base_url_env": ["DEVOPS_BASE_URL", "OPENAI_BASE_URL", "LLM_BASE_URL"],
21
+ "model_env": ["DEVOPS_MODEL", "OPENAI_MODEL", "LLM_MODEL"],
22
+ },
23
+ "zhipuai": {
24
+ "model": "glm-4-flash",
25
+ "base_url": None,
26
+ "api_key_env": ["DEVOPS_API_KEY", "ZHIPUAI_API_KEY"],
27
+ "base_url_env": ["DEVOPS_BASE_URL"],
28
+ "model_env": ["DEVOPS_MODEL", "ZHIPUAI_MODEL"],
29
+ },
30
+ "deepseek": {
31
+ "model": "deepseek-chat",
32
+ "base_url": "https://api.deepseek.com",
33
+ "api_key_env": ["DEVOPS_API_KEY", "DEEPSEEK_API_KEY"],
34
+ "base_url_env": ["DEVOPS_BASE_URL", "DEEPSEEK_BASE_URL"],
35
+ "model_env": ["DEVOPS_MODEL", "DEEPSEEK_MODEL"],
36
+ },
37
+ "moonshot": {
38
+ "model": "moonshot-v1-8k",
39
+ "base_url": "https://api.moonshot.cn/v1",
40
+ "api_key_env": ["DEVOPS_API_KEY", "MOONSHOT_API_KEY"],
41
+ "base_url_env": ["DEVOPS_BASE_URL", "MOONSHOT_BASE_URL"],
42
+ "model_env": ["DEVOPS_MODEL", "MOONSHOT_MODEL"],
43
+ },
44
+ }
45
+
46
+
47
+ @dataclass
48
+ class ModelConfig:
49
+ provider: str
50
+ api_key: str
51
+ model: str
52
+ base_url: Optional[str] = None
53
+
54
+ def masked(self) -> dict[str, Any]:
55
+ key = self.api_key
56
+ if len(key) > 8:
57
+ masked_key = f"{key[:4]}...{key[-4:]}"
58
+ else:
59
+ masked_key = "****"
60
+ return {
61
+ "provider": self.provider,
62
+ "api_key": masked_key,
63
+ "model": self.model,
64
+ "base_url": self.base_url or "(默认)",
65
+ }
66
+
67
+
68
+ def _first_env(names: list[str]) -> Optional[str]:
69
+ for name in names:
70
+ value = os.environ.get(name, "").strip()
71
+ if value:
72
+ return value
73
+ return None
74
+
75
+
76
+ def list_providers() -> list[str]:
77
+ return list(PROVIDER_DEFAULTS.keys())
78
+
79
+
80
+ def config_exists() -> bool:
81
+ return CONFIG_FILE.is_file()
82
+
83
+
84
+ def load_config_file() -> Optional[dict[str, Any]]:
85
+ if not CONFIG_FILE.is_file():
86
+ return None
87
+ try:
88
+ return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
89
+ except (json.JSONDecodeError, OSError):
90
+ return None
91
+
92
+
93
+ def save_config(config: ModelConfig) -> Path:
94
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
95
+ data = asdict(config)
96
+ CONFIG_FILE.write_text(
97
+ json.dumps(data, ensure_ascii=False, indent=2),
98
+ encoding="utf-8",
99
+ )
100
+ CONFIG_FILE.chmod(0o600)
101
+ return CONFIG_FILE
102
+
103
+
104
+ def load_config(
105
+ *,
106
+ provider: Optional[str] = None,
107
+ api_key: Optional[str] = None,
108
+ model: Optional[str] = None,
109
+ base_url: Optional[str] = None,
110
+ ) -> ModelConfig:
111
+ """加载配置:CLI 参数 > 配置文件 > 环境变量。"""
112
+ file_data = load_config_file() or {}
113
+
114
+ resolved_provider = (
115
+ provider
116
+ or os.environ.get("DEVOPS_PROVIDER", "").strip()
117
+ or file_data.get("provider", "").strip()
118
+ or "openai"
119
+ ).lower()
120
+
121
+ if resolved_provider not in PROVIDER_DEFAULTS:
122
+ supported = ", ".join(list_providers())
123
+ raise ValueError(f"不支持的模型提供商: {resolved_provider},可选: {supported}")
124
+
125
+ defaults = PROVIDER_DEFAULTS[resolved_provider]
126
+
127
+ resolved_api_key = api_key or file_data.get("api_key", "").strip()
128
+ if not resolved_api_key:
129
+ resolved_api_key = _first_env(defaults["api_key_env"]) or ""
130
+ if not resolved_api_key:
131
+ raise RuntimeError(
132
+ "未配置 API Key。请先运行: devops config\n"
133
+ f"或设置环境变量: {defaults['api_key_env'][0]}"
134
+ )
135
+
136
+ resolved_model = (
137
+ model
138
+ or file_data.get("model", "").strip()
139
+ or _first_env(defaults["model_env"])
140
+ or defaults["model"]
141
+ )
142
+
143
+ resolved_base_url = base_url
144
+ if resolved_base_url is None:
145
+ file_base = file_data.get("base_url")
146
+ resolved_base_url = (
147
+ file_base.strip() if isinstance(file_base, str) and file_base.strip() else None
148
+ )
149
+ if resolved_base_url is None:
150
+ resolved_base_url = _first_env(defaults["base_url_env"])
151
+ if resolved_base_url is None:
152
+ resolved_base_url = defaults["base_url"]
153
+
154
+ return ModelConfig(
155
+ provider=resolved_provider,
156
+ api_key=resolved_api_key,
157
+ model=resolved_model,
158
+ base_url=resolved_base_url,
159
+ )
devops/config_cmd.py ADDED
@@ -0,0 +1,88 @@
1
+ """devops config 子命令:交互式配置模型 API。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import getpass
6
+ import sys
7
+
8
+ from devops.config import (
9
+ CONFIG_FILE,
10
+ ModelConfig,
11
+ list_providers,
12
+ load_config,
13
+ save_config,
14
+ )
15
+
16
+
17
+ def _prompt(label: str, default: str = "") -> str:
18
+ if default:
19
+ value = input(f"{label} [{default}]: ").strip()
20
+ return value or default
21
+ return input(f"{label}: ").strip()
22
+
23
+
24
+ def run_config_show() -> int:
25
+ try:
26
+ config = load_config()
27
+ except RuntimeError as exc:
28
+ print(str(exc), file=sys.stderr)
29
+ print(f"\n配置文件路径: {CONFIG_FILE}", file=sys.stderr)
30
+ return 1
31
+
32
+ print("当前模型配置:")
33
+ for key, value in config.masked().items():
34
+ print(f" {key}: {value}")
35
+ print(f"\n配置文件: {CONFIG_FILE}")
36
+ return 0
37
+
38
+
39
+ def run_config_interactive() -> int:
40
+ providers = list_providers()
41
+ print("支持的模型提供商:")
42
+ for i, name in enumerate(providers, 1):
43
+ print(f" {i}. {name}")
44
+ print()
45
+
46
+ provider = _prompt("提供商 (openai/zhipuai/deepseek/moonshot)", "openai").lower()
47
+ if provider not in providers:
48
+ print(f"错误: 未知提供商 {provider}", file=sys.stderr)
49
+ return 1
50
+
51
+ api_key = getpass.getpass("API Key (输入不可见): ").strip()
52
+ if not api_key:
53
+ print("错误: API Key 不能为空", file=sys.stderr)
54
+ return 1
55
+
56
+ default_models = {
57
+ "openai": "gpt-4o-mini",
58
+ "zhipuai": "glm-4-flash",
59
+ "deepseek": "deepseek-chat",
60
+ "moonshot": "moonshot-v1-8k",
61
+ }
62
+ model = _prompt("模型名称", default_models.get(provider, "gpt-4o-mini"))
63
+
64
+ base_url = ""
65
+ if provider in ("openai",):
66
+ base_url = _prompt("Base URL (留空=官方 OpenAI)", "")
67
+ elif provider in ("deepseek", "moonshot"):
68
+ from devops.config import PROVIDER_DEFAULTS
69
+ default_url = PROVIDER_DEFAULTS[provider]["base_url"] or ""
70
+ base_url = _prompt("Base URL", default_url)
71
+
72
+ config = ModelConfig(
73
+ provider=provider,
74
+ api_key=api_key,
75
+ model=model,
76
+ base_url=base_url or None,
77
+ )
78
+ path = save_config(config)
79
+ print(f"\n配置已保存: {path}")
80
+ print("现在可以运行: devops /path/to/your/project")
81
+ return 0
82
+
83
+
84
+ def run_config(argv: list[str] | None = None) -> int:
85
+ args = argv or []
86
+ if "--show" in args or "-s" in args:
87
+ return run_config_show()
88
+ return run_config_interactive()
devops/extract.py ADDED
@@ -0,0 +1,215 @@
1
+ """用 Tree-sitter 从源码文件中提取代码块。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass
6
+ from pathlib import Path
7
+ from typing import Iterator, Optional
8
+
9
+ from tree_sitter import Node, Tree
10
+ from tree_sitter_languages import get_parser
11
+
12
+ from devops.scan import scan_code_files
13
+
14
+ EXT_TO_LANGUAGE: dict[str, str] = {
15
+ ".py": "python",
16
+ ".pyw": "python",
17
+ ".js": "javascript",
18
+ ".mjs": "javascript",
19
+ ".cjs": "javascript",
20
+ ".jsx": "javascript",
21
+ ".ts": "typescript",
22
+ ".tsx": "typescript",
23
+ ".cpp": "cpp",
24
+ ".cc": "cpp",
25
+ ".cxx": "cpp",
26
+ ".h": "cpp",
27
+ ".hh": "cpp",
28
+ ".hpp": "cpp",
29
+ ".hxx": "cpp",
30
+ ".c": "c",
31
+ ".java": "java",
32
+ ".go": "go",
33
+ ".rs": "rust",
34
+ ".rb": "ruby",
35
+ ".php": "php",
36
+ ".cs": "c_sharp",
37
+ ".swift": "swift",
38
+ ".kt": "kotlin",
39
+ ".scala": "scala",
40
+ ".sh": "bash",
41
+ }
42
+
43
+ LANGUAGE_NODE_TYPES: dict[str, tuple[str, ...]] = {
44
+ "python": ("function_definition", "async_function_definition", "class_definition"),
45
+ "javascript": (
46
+ "function_declaration",
47
+ "class_declaration",
48
+ "method_definition",
49
+ ),
50
+ "typescript": (
51
+ "function_declaration",
52
+ "class_declaration",
53
+ "method_definition",
54
+ "interface_declaration",
55
+ ),
56
+ "cpp": ("function_definition", "class_specifier", "struct_specifier", "namespace_definition"),
57
+ "c": ("function_definition", "struct_specifier"),
58
+ "java": ("method_declaration", "class_declaration", "interface_declaration"),
59
+ "go": ("function_declaration", "method_declaration", "type_declaration"),
60
+ "rust": ("function_item", "impl_item", "struct_item", "enum_item", "trait_item"),
61
+ "ruby": ("method", "class", "module"),
62
+ "php": ("function_definition", "method_declaration", "class_declaration"),
63
+ "c_sharp": ("method_declaration", "class_declaration", "interface_declaration"),
64
+ "kotlin": ("function_declaration", "class_declaration"),
65
+ "scala": ("function_definition", "class_definition", "object_definition"),
66
+ "bash": ("function_definition",),
67
+ "swift": ("function_declaration", "class_declaration", "struct_declaration"),
68
+ }
69
+
70
+ _parser_cache: dict[str, object] = {}
71
+
72
+
73
+ @dataclass
74
+ class CodeBlock:
75
+ file_path: str
76
+ language: str
77
+ block_type: str
78
+ name: str
79
+ start_line: int
80
+ end_line: int
81
+ code: str
82
+
83
+ def to_dict(self) -> dict:
84
+ return asdict(self)
85
+
86
+
87
+ def language_for_path(path: str | Path) -> Optional[str]:
88
+ return EXT_TO_LANGUAGE.get(Path(path).suffix.lower())
89
+
90
+
91
+ def _get_parser(language: str):
92
+ if language not in _parser_cache:
93
+ _parser_cache[language] = get_parser(language)
94
+ return _parser_cache[language]
95
+
96
+
97
+ def _read_source(path: Path) -> Optional[bytes]:
98
+ try:
99
+ return path.read_bytes()
100
+ except OSError:
101
+ return None
102
+
103
+
104
+ def _line_number(source: bytes, byte_offset: int) -> int:
105
+ return source[:byte_offset].count(b"\n") + 1
106
+
107
+
108
+ def _declarator_name(node: Node) -> str:
109
+ inner = node.child_by_field_name("declarator")
110
+ if inner is not None:
111
+ return _declarator_name(inner)
112
+ if node.type in ("identifier", "field_identifier", "operator_name"):
113
+ return node.text.decode("utf-8", errors="replace")
114
+ for child in node.children:
115
+ name = _declarator_name(child)
116
+ if name:
117
+ return name
118
+ return ""
119
+
120
+
121
+ def _node_name(node: Node, language: str) -> str:
122
+ name_node = node.child_by_field_name("name")
123
+ if name_node is not None:
124
+ return name_node.text.decode("utf-8", errors="replace")
125
+
126
+ if language == "python" and node.type == "decorated_definition":
127
+ for child in node.children:
128
+ if child.type in ("function_definition", "class_definition"):
129
+ return _node_name(child, language)
130
+
131
+ if language == "cpp" and node.type == "function_definition":
132
+ decl = node.child_by_field_name("declarator")
133
+ if decl is not None:
134
+ return _declarator_name(decl)
135
+
136
+ for child in node.children:
137
+ if child.type in ("identifier", "type_identifier", "property_identifier"):
138
+ text = child.text.decode("utf-8", errors="replace")
139
+ if text not in ("def", "class", "fn", "function"):
140
+ return text
141
+ return ""
142
+
143
+
144
+ def _walk_nodes(node: Node) -> Iterator[Node]:
145
+ yield node
146
+ for child in node.children:
147
+ yield from _walk_nodes(child)
148
+
149
+
150
+ def _extract_from_tree(
151
+ tree: Tree,
152
+ source: bytes,
153
+ file_path: str,
154
+ language: str,
155
+ ) -> list[CodeBlock]:
156
+ target_types = set(LANGUAGE_NODE_TYPES.get(language, ()))
157
+ if not target_types:
158
+ return []
159
+
160
+ blocks: list[CodeBlock] = []
161
+ seen: set[tuple[int, int]] = set()
162
+
163
+ for node in _walk_nodes(tree.root_node):
164
+ if node.type not in target_types:
165
+ continue
166
+ key = (node.start_byte, node.end_byte)
167
+ if key in seen:
168
+ continue
169
+ seen.add(key)
170
+
171
+ blocks.append(
172
+ CodeBlock(
173
+ file_path=file_path,
174
+ language=language,
175
+ block_type=node.type,
176
+ name=_node_name(node, language),
177
+ start_line=_line_number(source, node.start_byte),
178
+ end_line=_line_number(source, node.end_byte),
179
+ code=source[node.start_byte : node.end_byte].decode(
180
+ "utf-8", errors="replace"
181
+ ),
182
+ )
183
+ )
184
+
185
+ blocks.sort(key=lambda b: (b.start_line, b.name))
186
+ return blocks
187
+
188
+
189
+ def extract_blocks_from_file(path: str | Path) -> list[CodeBlock]:
190
+ p = Path(path)
191
+ language = language_for_path(p)
192
+ if language is None:
193
+ return []
194
+
195
+ source = _read_source(p)
196
+ if source is None:
197
+ return []
198
+
199
+ try:
200
+ tree = _get_parser(language).parse(source)
201
+ except Exception:
202
+ return []
203
+
204
+ return _extract_from_tree(tree, source, str(p.resolve()), language)
205
+
206
+
207
+ def extract_blocks_from_paths(paths: list[str]) -> list[CodeBlock]:
208
+ blocks: list[CodeBlock] = []
209
+ for path in paths:
210
+ blocks.extend(extract_blocks_from_file(path))
211
+ return blocks
212
+
213
+
214
+ def extract_blocks_from_folder(folder: str) -> list[CodeBlock]:
215
+ return extract_blocks_from_paths(scan_code_files(folder))
@@ -0,0 +1,50 @@
1
+ """多模型提供商统一入口。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from devops.config import ModelConfig, list_providers
6
+ from devops.models.base import BaseChatModel
7
+ from devops.models.openai import OpenAIChatModel
8
+ from devops.models.zhipuai import ZhipuAIChatModel
9
+
10
+ # deepseek / moonshot 使用 OpenAI 兼容协议
11
+ _OPENAI_COMPAT = {"openai", "deepseek", "moonshot"}
12
+
13
+
14
+ def create_chat_model(config: ModelConfig) -> BaseChatModel:
15
+ provider = config.provider.lower()
16
+ if provider in _OPENAI_COMPAT:
17
+ return OpenAIChatModel(config)
18
+ if provider == "zhipuai":
19
+ return ZhipuAIChatModel(config)
20
+ raise ValueError(
21
+ f"不支持的提供商: {provider},可选: {', '.join(list_providers())}"
22
+ )
23
+
24
+
25
+ def get_chat_model(
26
+ *,
27
+ provider: str | None = None,
28
+ api_key: str | None = None,
29
+ model: str | None = None,
30
+ base_url: str | None = None,
31
+ ) -> BaseChatModel:
32
+ from devops.config import load_config
33
+
34
+ config = load_config(
35
+ provider=provider,
36
+ api_key=api_key,
37
+ model=model,
38
+ base_url=base_url,
39
+ )
40
+ return create_chat_model(config)
41
+
42
+
43
+ __all__ = [
44
+ "BaseChatModel",
45
+ "OpenAIChatModel",
46
+ "ZhipuAIChatModel",
47
+ "create_chat_model",
48
+ "get_chat_model",
49
+ "list_providers",
50
+ ]
devops/models/base.py ADDED
@@ -0,0 +1,16 @@
1
+ """大模型调用抽象基类。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+ from devops.config import ModelConfig
8
+
9
+
10
+ class BaseChatModel(ABC):
11
+ def __init__(self, config: ModelConfig) -> None:
12
+ self.config = config
13
+
14
+ @abstractmethod
15
+ def chat(self, system: str, user: str, *, temperature: float = 0.2) -> str:
16
+ raise NotImplementedError
@@ -0,0 +1,30 @@
1
+ """OpenAI 及兼容 OpenAI 协议的服务(DeepSeek、Moonshot、本地 Ollama 等)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from devops.config import ModelConfig
8
+ from devops.models.base import BaseChatModel
9
+
10
+
11
+ class OpenAIChatModel(BaseChatModel):
12
+ def __init__(self, config: ModelConfig) -> None:
13
+ super().__init__(config)
14
+ from openai import OpenAI
15
+
16
+ kwargs: dict[str, Any] = {"api_key": config.api_key}
17
+ if config.base_url:
18
+ kwargs["base_url"] = config.base_url.rstrip("/")
19
+ self._client = OpenAI(**kwargs)
20
+
21
+ def chat(self, system: str, user: str, *, temperature: float = 0.2) -> str:
22
+ response = self._client.chat.completions.create(
23
+ model=self.config.model,
24
+ messages=[
25
+ {"role": "system", "content": system},
26
+ {"role": "user", "content": user},
27
+ ],
28
+ temperature=temperature,
29
+ )
30
+ return (response.choices[0].message.content or "").strip()
@@ -0,0 +1,28 @@
1
+ """智谱 AI (GLM) 模型。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from devops.config import ModelConfig
6
+ from devops.models.base import BaseChatModel
7
+
8
+
9
+ class ZhipuAIChatModel(BaseChatModel):
10
+ def __init__(self, config: ModelConfig) -> None:
11
+ super().__init__(config)
12
+ try:
13
+ from zhipuai import ZhipuAI
14
+ except ImportError as exc:
15
+ raise ImportError("使用智谱 AI 需要安装: pip install zhipuai") from exc
16
+
17
+ self._client = ZhipuAI(api_key=config.api_key)
18
+
19
+ def chat(self, system: str, user: str, *, temperature: float = 0.2) -> str:
20
+ response = self._client.chat.completions.create(
21
+ model=self.config.model,
22
+ messages=[
23
+ {"role": "system", "content": system},
24
+ {"role": "user", "content": user},
25
+ ],
26
+ temperature=temperature,
27
+ )
28
+ return (response.choices[0].message.content or "").strip()
@@ -0,0 +1,3 @@
1
+ from devops.scan.scan_native import scan_code_files
2
+
3
+ __all__ = ["scan_code_files"]
devops/scan/scan.cpp ADDED
@@ -0,0 +1,121 @@
1
+ #include "scan.h"
2
+
3
+ #include <algorithm>
4
+ #include <cctype>
5
+ #include <filesystem>
6
+ #include <string>
7
+ #include <unordered_set>
8
+ #include <vector>
9
+
10
+ namespace fs = std::filesystem;
11
+
12
+ namespace {
13
+
14
+ // Python 传入 UTF-8 路径,各平台统一用 u8path 解析
15
+ fs::path toPath(const std::string& utf8Path) {
16
+ return fs::u8path(utf8Path);
17
+ }
18
+
19
+ // 返回统一使用正斜杠的路径,便于跨平台在 Python 中使用
20
+ std::string toGenericPath(const fs::path& path) {
21
+ return path.generic_string();
22
+ }
23
+
24
+ bool nameEquals(const std::string& a, const std::string& b) {
25
+ #if defined(_WIN32)
26
+ if (a.size() != b.size()) {
27
+ return false;
28
+ }
29
+ for (size_t i = 0; i < a.size(); ++i) {
30
+ if (std::tolower(static_cast<unsigned char>(a[i])) !=
31
+ std::tolower(static_cast<unsigned char>(b[i]))) {
32
+ return false;
33
+ }
34
+ }
35
+ return true;
36
+ #else
37
+ return a == b;
38
+ #endif
39
+ }
40
+
41
+ bool shouldSkipDir(const fs::path& dir) {
42
+ static const char* skip[] = {
43
+ ".git", ".svn", ".hg",
44
+ "node_modules", "vendor",
45
+ "build", "dist", "out", "target", "bin", "obj",
46
+ ".idea", ".vscode", ".vs",
47
+ "__pycache__", ".next", ".venv", "venv", "env",
48
+ // Windows / MSVC 常见输出目录
49
+ "Debug", "Release", "x64", "Win32", "CMakeFiles",
50
+ };
51
+
52
+ const std::string name = dir.filename().string();
53
+ for (const char* entry : skip) {
54
+ if (nameEquals(name, entry)) {
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+
61
+ bool isCodeFile(const fs::path& path) {
62
+ static const std::unordered_set<std::string> exts = {
63
+ ".c", ".cc", ".cpp", ".cxx", ".h", ".hpp",
64
+ ".py", ".js", ".ts", ".tsx", ".jsx",
65
+ ".java", ".go", ".rs", ".rb", ".php",
66
+ ".cs", ".swift", ".m", ".mm",
67
+ ".sh", ".sql", ".html", ".css",
68
+ ".vue", ".kt", ".scala",
69
+ };
70
+
71
+ std::string ext = path.extension().string();
72
+ for (char& c : ext) {
73
+ c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
74
+ }
75
+ return !ext.empty() && exts.count(ext) > 0;
76
+ }
77
+
78
+ } // namespace
79
+
80
+ std::vector<std::string> scanCodeFiles(const std::string& folderPath) {
81
+ std::vector<std::string> result;
82
+ std::error_code ec;
83
+
84
+ const fs::path root = toPath(folderPath);
85
+ if (!fs::exists(root, ec) || !fs::is_directory(root, ec)) {
86
+ return result;
87
+ }
88
+
89
+ fs::recursive_directory_iterator it(
90
+ root,
91
+ fs::directory_options::skip_permission_denied,
92
+ ec);
93
+ const fs::recursive_directory_iterator end;
94
+
95
+ for (; it != end; it.increment(ec)) {
96
+ if (ec) {
97
+ ec.clear();
98
+ continue;
99
+ }
100
+
101
+ const fs::directory_entry& entry = *it;
102
+ if (entry.is_directory(ec)) {
103
+ if (shouldSkipDir(entry.path())) {
104
+ it.disable_recursion_pending();
105
+ }
106
+ continue;
107
+ }
108
+
109
+ if (entry.is_regular_file(ec) && isCodeFile(entry.path())) {
110
+ fs::path resolved = fs::weakly_canonical(entry.path(), ec);
111
+ if (ec) {
112
+ resolved = entry.path();
113
+ ec.clear();
114
+ }
115
+ result.push_back(toGenericPath(resolved));
116
+ }
117
+ }
118
+
119
+ std::sort(result.begin(), result.end());
120
+ return result;
121
+ }
devops/scan/scan.h ADDED
@@ -0,0 +1,7 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+ #include <vector>
5
+
6
+ // 递归扫描文件夹,返回其中所有代码文件的路径(已排序)
7
+ std::vector<std::string> scanCodeFiles(const std::string& folderPath);
@@ -0,0 +1,15 @@
1
+ #include <pybind11/pybind11.h>
2
+ #include <pybind11/stl.h>
3
+
4
+ #include "scan.h"
5
+
6
+ namespace py = pybind11;
7
+
8
+ PYBIND11_MODULE(scan_native, m) {
9
+ m.doc() = "Scan a project folder and return code file paths";
10
+ m.def(
11
+ "scan_code_files",
12
+ &scanCodeFiles,
13
+ py::arg("folder_path"),
14
+ "Recursively list all code file paths under folder_path.");
15
+ }
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: devops-analyzer
3
+ Version: 0.1.0
4
+ Summary: 扫描源码、提取代码块、大模型分析并生成 README.md
5
+ Author: ZHUHK
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/example/devops-analyzer
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: tree-sitter<0.22,>=0.21.0
12
+ Requires-Dist: tree-sitter-languages>=1.10.0
13
+ Requires-Dist: openai>=1.40.0
14
+ Provides-Extra: zhipuai
15
+ Requires-Dist: zhipuai>=2.0.0; extra == "zhipuai"
16
+ Provides-Extra: all
17
+ Requires-Dist: zhipuai>=2.0.0; extra == "all"
18
+ Provides-Extra: build
19
+ Requires-Dist: build>=1.2.0; extra == "build"
20
+ Dynamic: license-file
21
+
22
+ # Devops Analyzer
23
+
24
+ 扫描项目源码 → Tree-sitter 提取代码块 → 大模型分析 → 生成 `README.md`。
25
+
26
+ ## 安装
27
+
28
+ ```bash
29
+ cd /path/to/Devops
30
+ conda activate devops
31
+ pip install -e .
32
+
33
+ # 使用智谱 AI 时额外安装
34
+ pip install zhipuai
35
+ ```
36
+
37
+ ## 首次使用:配置模型 API
38
+
39
+ **必须先配置你自己的 API Key**,再分析项目:
40
+
41
+ ```bash
42
+ devops config
43
+ ```
44
+
45
+ 按提示选择提供商、输入 API Key 和模型名称。配置保存在 `~/.devops/config.json`(仅当前用户可读)。
46
+
47
+ 查看当前配置(Key 脱敏显示):
48
+
49
+ ```bash
50
+ devops config --show
51
+ ```
52
+
53
+ ## 支持的模型提供商
54
+
55
+ | 提供商 | `provider` 值 | 默认模型 | 说明 |
56
+ |--------|---------------|----------|------|
57
+ | OpenAI | `openai` | `gpt-4o-mini` | 官方或自定义 Base URL |
58
+ | 智谱 AI | `zhipuai` | `glm-4-flash` | 需 `pip install zhipuai` |
59
+ | DeepSeek | `deepseek` | `deepseek-chat` | OpenAI 兼容协议 |
60
+ | Moonshot | `moonshot` | `moonshot-v1-8k` | OpenAI 兼容协议 |
61
+
62
+ 也可用环境变量覆盖(优先级低于配置文件):
63
+
64
+ ```bash
65
+ export DEVOPS_PROVIDER=openai
66
+ export DEVOPS_API_KEY=sk-...
67
+ export DEVOPS_MODEL=gpt-4o-mini
68
+ export DEVOPS_BASE_URL=https://api.openai.com/v1 # 可选
69
+ ```
70
+
71
+ ## 分析项目
72
+
73
+ ```bash
74
+ devops /path/to/your/project
75
+
76
+ # 或显式子命令
77
+ devops analyze /path/to/your/project
78
+
79
+ # 指定输出文件
80
+ devops /path/to/project -o README.md
81
+
82
+ # 临时覆盖模型(不改配置文件)
83
+ devops /path/to/project --provider deepseek --model deepseek-chat
84
+ ```
85
+
86
+ ## 命令一览
87
+
88
+ ```bash
89
+ devops config # 交互式配置 API
90
+ devops config --show # 查看配置
91
+ devops <项目路径> # 分析并生成 README.md
92
+ devops analyze <项目路径>
93
+ devops --help
94
+ devops --version
95
+ ```
@@ -0,0 +1,22 @@
1
+ devops/__init__.py,sha256=efMmj0qlEd8t459QNAdKbZFaWNurNByvxNdZew1I2sI,109
2
+ devops/__main__.py,sha256=CqvNIJzp4XxyAaK_QKk5XJzwkYGN6hhlbEm76hbMetE,57
3
+ devops/analysis.py,sha256=yVHV5HRbs6w-cKEhbyFSlUbXv7I-uqmzRxzDGTjOqJg,7745
4
+ devops/cli.py,sha256=uVYJVFGn4QnZSf24i92c3zDiZwG9MKGsk0xevxOvAuY,5422
5
+ devops/config.py,sha256=hOhQrkQHGFxfroik9fomkCpWgDosmBtWBOZEJNSd8XI,4961
6
+ devops/config_cmd.py,sha256=tebWZmX9qq9lz2tuoqgiwHqAPW8BrVHaVDfju80kgtU,2577
7
+ devops/extract.py,sha256=JufuM_3OjQFM4POkQ2nPSiMUBcyJLVirGej_zdZ9Ddo,6450
8
+ devops/models/__init__.py,sha256=pAaRPi5Uj1qrX6KpaJSpgpzckeoYZFhsOIbsepoxngY,1261
9
+ devops/models/base.py,sha256=3vhle8dDC0_8LujFuPOxNQiN0MVy7V9q1I7XKQC5IYM,410
10
+ devops/models/openai.py,sha256=gkrNq-7zRcD3cced0ZiW_dsxirt6lREDQMLZeNSIEAw,1052
11
+ devops/models/zhipuai.py,sha256=4vAyS9AMl2RXczvt8dytHm1cmS1ZACF4J25qWmAyzYo,976
12
+ devops/scan/__init__.py,sha256=dwVWZaan-saFaPY6KIre1-bUrmTvmSfq4z5bzCjr8VY,86
13
+ devops/scan/scan.cpp,sha256=aMzImUcvqFldWYRHa7H-GH5h3BZtZAfGaVlOG4u6HTw,3376
14
+ devops/scan/scan.h,sha256=eCrTNjqxu6jlySNnpC7eyMIknqLkUTHFuc234_HXzoY,211
15
+ devops/scan/scan_bindings.cpp,sha256=q139NtDIy07v78Pq4MMt7BX_-QT4_ob8wn37bw0tNYk,382
16
+ devops/scan/scan_native.cp312-win_amd64.pyd,sha256=kXolVgXTnD0l4QG_uUuaW-otpNZtXzPv4_YzPLUgfNI,181760
17
+ devops_analyzer-0.1.0.dist-info/licenses/LICENSE,sha256=MBvA7WLDGW82KmxFZLLfA7h0VS_jK5cRBq8crggZlDk,1083
18
+ devops_analyzer-0.1.0.dist-info/METADATA,sha256=aYhrwciUIvkOZLSW17CxhKB7h1UuAedeCdZnFTvmFHA,2515
19
+ devops_analyzer-0.1.0.dist-info/WHEEL,sha256=rR5QfsWcZl3mra5AmSD7Fd0dzQxZ3lHCpDo70IkfDK4,101
20
+ devops_analyzer-0.1.0.dist-info/entry_points.txt,sha256=6kfJgW01wJybJetKc0qeIpHlldoiZn4g52Sd8B48p_U,43
21
+ devops_analyzer-0.1.0.dist-info/top_level.txt,sha256=qcdbAA3CortK6Ce66IZXkIMR65YMclmS95JKCGGgTv0,7
22
+ devops_analyzer-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: false
4
+ Tag: cp312-cp312-win_amd64
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ devops = devops.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ZHUHK
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ devops