devmate 1.0.0__py3-none-any.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.
- devmate/__init__.py +6 -0
- devmate/__main__.py +5 -0
- devmate/app/__init__.py +1 -0
- devmate/app/cli.py +1663 -0
- devmate/app/tui.py +291 -0
- devmate/app/web/__init__.py +95 -0
- devmate/bus/__init__.py +1 -0
- devmate/bus/bus.py +151 -0
- devmate/bus/command.py +84 -0
- devmate/core/__init__.py +1 -0
- devmate/core/config.py +115 -0
- devmate/core/logging.py +54 -0
- devmate/core/security.py +61 -0
- devmate/events/__init__.py +1 -0
- devmate/events/bus.py +97 -0
- devmate/events/domain_event.py +37 -0
- devmate/module/__init__.py +1 -0
- devmate/module/registry.py +74 -0
- devmate/security/audit.py +96 -0
- devmate/security/vault.py +221 -0
- devmate-1.0.0.dist-info/METADATA +116 -0
- devmate-1.0.0.dist-info/RECORD +37 -0
- devmate-1.0.0.dist-info/WHEEL +5 -0
- devmate-1.0.0.dist-info/entry_points.txt +18 -0
- devmate-1.0.0.dist-info/top_level.txt +13 -0
- devmate_agent/__init__.py +440 -0
- devmate_apidev/__init__.py +271 -0
- devmate_apihub/__init__.py +313 -0
- devmate_dbadmin/__init__.py +263 -0
- devmate_gitflow/__init__.py +247 -0
- devmate_monitor/__init__.py +112 -0
- devmate_notekeeper/__init__.py +280 -0
- devmate_regexlab/__init__.py +212 -0
- devmate_reporter/__init__.py +170 -0
- devmate_scaffold/__init__.py +181 -0
- devmate_sshman/__init__.py +330 -0
- devmate_toolkit/__init__.py +223 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dbadmin — 数据库管理
|
|
3
|
+
|
|
4
|
+
支持 SQLite 和 PostgreSQL 的:
|
|
5
|
+
- 连接管理
|
|
6
|
+
- 表列表/表结构查看
|
|
7
|
+
- SQL 查询执行
|
|
8
|
+
- 保存的查询
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from sqlalchemy import create_engine, inspect, text
|
|
18
|
+
from sqlalchemy.engine import Engine
|
|
19
|
+
|
|
20
|
+
from devmate.bus.command import CommandResult
|
|
21
|
+
|
|
22
|
+
# ── 数据库连接管理 ──────────────────────────────────
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ConnectionInfo:
|
|
26
|
+
"""已保存的数据库连接"""
|
|
27
|
+
name: str
|
|
28
|
+
dsn: str
|
|
29
|
+
engine: str = "sqlite" # sqlite / postgresql
|
|
30
|
+
created_at: str = ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_connections: dict[str, Engine] = {}
|
|
34
|
+
_CONNECTIONS_FILE = Path.home() / ".devmate" / "db_connections.json"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def connect(dsn: str, name: str = "default") -> CommandResult:
|
|
38
|
+
"""连接数据库"""
|
|
39
|
+
try:
|
|
40
|
+
engine = create_engine(dsn, pool_pre_ping=True)
|
|
41
|
+
# 测试连接
|
|
42
|
+
with engine.connect() as conn:
|
|
43
|
+
conn.execute(text("SELECT 1"))
|
|
44
|
+
_connections[name] = engine
|
|
45
|
+
|
|
46
|
+
# 保存连接信息
|
|
47
|
+
_save_connection(name, dsn)
|
|
48
|
+
|
|
49
|
+
return CommandResult(success=True, data={
|
|
50
|
+
"message": f"✅ 已连接到 {dsn}",
|
|
51
|
+
"engine": engine.name,
|
|
52
|
+
"dsn": dsn,
|
|
53
|
+
})
|
|
54
|
+
except Exception as e:
|
|
55
|
+
return CommandResult(success=False, error=f"连接失败: {e}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _save_connection(name: str, dsn: str):
|
|
59
|
+
"""持久化连接信息"""
|
|
60
|
+
_CONNECTIONS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
connections = {}
|
|
62
|
+
if _CONNECTIONS_FILE.exists():
|
|
63
|
+
connections = json.loads(_CONNECTIONS_FILE.read_text())
|
|
64
|
+
connections[name] = {"dsn": dsn, "engine": _detect_engine(dsn)}
|
|
65
|
+
_CONNECTIONS_FILE.write_text(json.dumps(connections, indent=2))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _detect_engine(dsn: str) -> str:
|
|
69
|
+
if dsn.startswith("postgresql"):
|
|
70
|
+
return "postgresql"
|
|
71
|
+
return "sqlite"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_engine(name: str = "default") -> Engine:
|
|
75
|
+
"""获取已连接的 engine"""
|
|
76
|
+
engine = _connections.get(name)
|
|
77
|
+
if engine is None:
|
|
78
|
+
# 尝试从文件恢复
|
|
79
|
+
if _CONNECTIONS_FILE.exists():
|
|
80
|
+
cons = json.loads(_CONNECTIONS_FILE.read_text())
|
|
81
|
+
if name in cons:
|
|
82
|
+
engine = create_engine(cons[name]["dsn"], pool_pre_ping=True)
|
|
83
|
+
_connections[name] = engine
|
|
84
|
+
return engine
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── 表操作 ───────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
def list_tables(name: str = "default") -> CommandResult:
|
|
90
|
+
"""列出数据库中所有表"""
|
|
91
|
+
engine = get_engine(name)
|
|
92
|
+
if engine is None:
|
|
93
|
+
return CommandResult(success=False, error="未连接数据库。先用 devmate db connect <dsn>")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
inspector = inspect(engine)
|
|
97
|
+
tables = inspector.get_table_names()
|
|
98
|
+
|
|
99
|
+
result_data = []
|
|
100
|
+
for table_name in tables:
|
|
101
|
+
with engine.connect() as conn:
|
|
102
|
+
count = conn.execute(text(f"SELECT COUNT(*) FROM \"{table_name}\"")).scalar()
|
|
103
|
+
result_data.append({"name": table_name, "row_count": count})
|
|
104
|
+
|
|
105
|
+
return CommandResult(success=True, data={
|
|
106
|
+
"tables": result_data,
|
|
107
|
+
"total": len(result_data),
|
|
108
|
+
"engine": engine.name,
|
|
109
|
+
})
|
|
110
|
+
except Exception as e:
|
|
111
|
+
return CommandResult(success=False, error=str(e))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def show_table(table_name: str, name: str = "default") -> CommandResult:
|
|
115
|
+
"""查看表结构"""
|
|
116
|
+
engine = get_engine(name)
|
|
117
|
+
if engine is None:
|
|
118
|
+
return CommandResult(success=False, error="未连接数据库")
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
inspector = inspect(engine)
|
|
122
|
+
columns = inspector.get_columns(table_name)
|
|
123
|
+
|
|
124
|
+
col_info = []
|
|
125
|
+
for col in columns:
|
|
126
|
+
col_info.append({
|
|
127
|
+
"name": col["name"],
|
|
128
|
+
"type": str(col["type"]),
|
|
129
|
+
"nullable": col.get("nullable", True),
|
|
130
|
+
"primary_key": col.get("primary_key", False),
|
|
131
|
+
"default": str(col.get("default", "")) if col.get("default") else "",
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
# 前 10 行数据预览
|
|
135
|
+
preview_rows: list[list[Any]] = []
|
|
136
|
+
preview_columns: list[str] = [c["name"] for c in columns]
|
|
137
|
+
with engine.connect() as conn:
|
|
138
|
+
quoted = _quote_table(table_name, engine)
|
|
139
|
+
result = conn.execute(text(f"SELECT * FROM {quoted} LIMIT 10"))
|
|
140
|
+
preview_rows = [list(row) for row in result]
|
|
141
|
+
|
|
142
|
+
return CommandResult(success=True, data={
|
|
143
|
+
"table": table_name,
|
|
144
|
+
"columns": col_info,
|
|
145
|
+
"preview": {
|
|
146
|
+
"columns": preview_columns,
|
|
147
|
+
"rows": preview_rows,
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
except Exception as e:
|
|
151
|
+
return CommandResult(success=False, error=str(e))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ── SQL 查询执行 ─────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
def query(sql: str, name: str = "default", timeout: int = 5) -> CommandResult:
|
|
157
|
+
"""执行 SQL 查询"""
|
|
158
|
+
engine = get_engine(name)
|
|
159
|
+
if engine is None:
|
|
160
|
+
return CommandResult(success=False, error="未连接数据库。先用 devmate db connect <dsn>")
|
|
161
|
+
|
|
162
|
+
start = time.time()
|
|
163
|
+
try:
|
|
164
|
+
with engine.connect() as conn:
|
|
165
|
+
conn.execute(text("SELECT 1")) # ping
|
|
166
|
+
|
|
167
|
+
_query_keywords = ("SELECT", "WITH", "EXPLAIN", "SHOW", "DESCRIBE", "PRAGMA")
|
|
168
|
+
if sql.strip().upper().startswith(_query_keywords):
|
|
169
|
+
result = conn.execute(text(sql))
|
|
170
|
+
columns = list(result.keys())
|
|
171
|
+
rows = [list(row) for row in result.fetchmany(100)]
|
|
172
|
+
elapsed = round((time.time() - start) * 1000, 1)
|
|
173
|
+
|
|
174
|
+
return CommandResult(success=True, data={
|
|
175
|
+
"type": "query",
|
|
176
|
+
"columns": columns,
|
|
177
|
+
"rows": rows,
|
|
178
|
+
"row_count": len(rows),
|
|
179
|
+
"elapsed_ms": elapsed,
|
|
180
|
+
})
|
|
181
|
+
else:
|
|
182
|
+
conn.execute(text(sql))
|
|
183
|
+
conn.commit()
|
|
184
|
+
elapsed = round((time.time() - start) * 1000, 1)
|
|
185
|
+
|
|
186
|
+
# 获取影响行数
|
|
187
|
+
return CommandResult(success=True, data={
|
|
188
|
+
"type": "execute",
|
|
189
|
+
"message": "查询执行成功",
|
|
190
|
+
"elapsed_ms": elapsed,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
elapsed = round((time.time() - start) * 1000, 1)
|
|
195
|
+
hint = _generate_error_hint(str(e))
|
|
196
|
+
return CommandResult(success=False, error=str(e), suggestion=hint)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _generate_error_hint(error_msg: str) -> str:
|
|
200
|
+
"""根据错误信息生成中文提示"""
|
|
201
|
+
error_lower = error_msg.lower()
|
|
202
|
+
|
|
203
|
+
if "no such table" in error_lower:
|
|
204
|
+
return "表不存在。运行 devmate db list-tables 查看所有表"
|
|
205
|
+
if "no such column" in error_lower or "unknown column" in error_lower:
|
|
206
|
+
return "列不存在。运行 devmate db show <表名> 查看表结构"
|
|
207
|
+
if "syntax error" in error_lower:
|
|
208
|
+
return "SQL 语法错误。检查关键字拼写和标点符号"
|
|
209
|
+
if "permission denied" in error_lower:
|
|
210
|
+
return "权限不足。检查数据库用户权限"
|
|
211
|
+
if "timeout" in error_lower:
|
|
212
|
+
return "查询超时。尝试添加 LIMIT 限制或优化查询"
|
|
213
|
+
if "duplicate column" in error_lower:
|
|
214
|
+
return "列名重复。检查 SELECT 中是否选择了多次同名列"
|
|
215
|
+
|
|
216
|
+
return "检查 SQL 语句是否正确,或运行 devmate db show <表名> 查看表结构"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _quote_table(table_name: str, engine: Engine) -> str:
|
|
220
|
+
"""安全地引用表名"""
|
|
221
|
+
if engine.name == "postgresql":
|
|
222
|
+
return f'"{table_name}"'
|
|
223
|
+
return f'"{table_name}"'
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ── 保存的查询 ──────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
_SAVED_FILE = Path.home() / ".devmate" / "saved_queries.json"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def save_query(name: str, sql: str) -> CommandResult:
|
|
232
|
+
"""保存查询"""
|
|
233
|
+
_SAVED_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
queries = {}
|
|
235
|
+
if _SAVED_FILE.exists():
|
|
236
|
+
queries = json.loads(_SAVED_FILE.read_text())
|
|
237
|
+
|
|
238
|
+
queries[name] = {"sql": sql, "created": time.strftime("%Y-%m-%d %H:%M")}
|
|
239
|
+
_SAVED_FILE.write_text(json.dumps(queries, indent=2, ensure_ascii=False))
|
|
240
|
+
return CommandResult(success=True, data={"message": f"查询已保存: {name}"})
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def list_saved_queries() -> CommandResult:
|
|
244
|
+
"""列出保存的查询"""
|
|
245
|
+
if not _SAVED_FILE.exists():
|
|
246
|
+
return CommandResult(success=True, data={"queries": []})
|
|
247
|
+
|
|
248
|
+
queries = json.loads(_SAVED_FILE.read_text())
|
|
249
|
+
data = [{"name": k, "sql": v["sql"], "created": v.get("created", "")}
|
|
250
|
+
for k, v in queries.items()]
|
|
251
|
+
return CommandResult(success=True, data={"queries": data})
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def run_saved_query(name: str, conn_name: str = "default") -> CommandResult:
|
|
255
|
+
"""运行保存的查询"""
|
|
256
|
+
if not _SAVED_FILE.exists():
|
|
257
|
+
return CommandResult(success=False, error="没有保存的查询")
|
|
258
|
+
|
|
259
|
+
queries = json.loads(_SAVED_FILE.read_text())
|
|
260
|
+
if name not in queries:
|
|
261
|
+
return CommandResult(success=False, error=f"查询不存在: {name}")
|
|
262
|
+
|
|
263
|
+
return query(queries[name]["sql"], name=conn_name)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gitflow — Git 工作流增强
|
|
3
|
+
|
|
4
|
+
包含 Conventional Commit 校验、CHANGELOG 自动生成、.gitignore 生成器、仓库统计。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from devmate.bus.command import CommandResult
|
|
12
|
+
|
|
13
|
+
# ── Conventional Commit 规范 ─────────────────────────
|
|
14
|
+
|
|
15
|
+
CONVENTIONAL_TYPES = {
|
|
16
|
+
"feat": "新功能",
|
|
17
|
+
"fix": "Bug 修复",
|
|
18
|
+
"docs": "文档更新",
|
|
19
|
+
"style": "代码格式(不影响功能)",
|
|
20
|
+
"refactor": "重构",
|
|
21
|
+
"perf": "性能优化",
|
|
22
|
+
"test": "测试相关",
|
|
23
|
+
"build": "构建系统",
|
|
24
|
+
"ci": "CI 配置",
|
|
25
|
+
"chore": "杂项",
|
|
26
|
+
"revert": "回退",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
COMMIT_PATTERN = re.compile(
|
|
30
|
+
r"^(?P<type>\w+)(?:\((?P<scope>[\w.-]+)\))?"
|
|
31
|
+
r"(?P<breaking>!)?:\s*(?P<description>.+)"
|
|
32
|
+
r"(?:\n\n(?P<body>.+))?"
|
|
33
|
+
r"(?:\n\n(?P<footer>.+))?$",
|
|
34
|
+
re.DOTALL,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def commit_check(message: str) -> CommandResult:
|
|
39
|
+
"""校验 Git commit message 是否符合 Conventional Commit 规范"""
|
|
40
|
+
result = {
|
|
41
|
+
"valid": False,
|
|
42
|
+
"type": "",
|
|
43
|
+
"scope": "",
|
|
44
|
+
"description": "",
|
|
45
|
+
"breaking": False,
|
|
46
|
+
"errors": [],
|
|
47
|
+
"warnings": [],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if not message or not message.strip():
|
|
51
|
+
result["errors"].append("Commit message 不能为空")
|
|
52
|
+
return CommandResult(success=False, data=result, error="Commit message 为空")
|
|
53
|
+
|
|
54
|
+
m = COMMIT_PATTERN.match(message.strip())
|
|
55
|
+
if not m:
|
|
56
|
+
result["errors"].append(
|
|
57
|
+
"格式错误。正确格式: <type>(<scope>): <description>\n"
|
|
58
|
+
"示例: feat(cli): 添加 --json 选项"
|
|
59
|
+
)
|
|
60
|
+
suggestion = result["errors"][0]
|
|
61
|
+
return CommandResult(success=False, data=result, error="格式错误", suggestion=suggestion)
|
|
62
|
+
|
|
63
|
+
result["valid"] = True
|
|
64
|
+
result["type"] = m.group("type")
|
|
65
|
+
result["scope"] = m.group("scope") or ""
|
|
66
|
+
result["description"] = m.group("description")
|
|
67
|
+
result["breaking"] = bool(m.group("breaking"))
|
|
68
|
+
|
|
69
|
+
# 校验 type
|
|
70
|
+
if result["type"] not in CONVENTIONAL_TYPES:
|
|
71
|
+
types_str = ", ".join(CONVENTIONAL_TYPES.keys())
|
|
72
|
+
result["warnings"].append(
|
|
73
|
+
f"未知类型 '{result['type']}'。标准类型: {types_str}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# 校验 description 长度
|
|
77
|
+
if len(result["description"]) > 100:
|
|
78
|
+
result["warnings"].append(f"描述过长 ({len(result['description'])} 字符),建议 ≤ 72 字符")
|
|
79
|
+
elif len(result["description"]) < 5:
|
|
80
|
+
result["warnings"].append(f"描述过短 ({len(result['description'])} 字符),建议 5-72 字符")
|
|
81
|
+
|
|
82
|
+
return CommandResult(success=True, data=result)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── CHANGELOG 生成 ──────────────────────────────────
|
|
86
|
+
|
|
87
|
+
CHANGELOG_HEADER = """# CHANGELOG
|
|
88
|
+
|
|
89
|
+
> 自动生成,基于 Conventional Commit
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def changelog(path: str = ".", since: str = "") -> CommandResult:
|
|
95
|
+
"""从 git log 自动生成 CHANGELOG"""
|
|
96
|
+
repo = Path(path).expanduser().resolve()
|
|
97
|
+
git_dir = repo / ".git"
|
|
98
|
+
if not git_dir.exists():
|
|
99
|
+
return CommandResult(success=False, error=f"不是 Git 仓库: {path}")
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
since_arg = f"--since={since}" if since else "--max-count=100"
|
|
103
|
+
result = subprocess.run(
|
|
104
|
+
["git", "log", "--format=%s|||%b|||%H|||%an|||%ai", since_arg],
|
|
105
|
+
capture_output=True, text=True, cwd=str(repo),
|
|
106
|
+
)
|
|
107
|
+
if result.returncode != 0:
|
|
108
|
+
return CommandResult(success=False, error=f"git log 失败: {result.stderr.strip()}")
|
|
109
|
+
except FileNotFoundError:
|
|
110
|
+
return CommandResult(success=False, error="git 未安装")
|
|
111
|
+
|
|
112
|
+
lines = [ln for ln in result.stdout.strip().split("\n") if ln.strip()]
|
|
113
|
+
if not lines:
|
|
114
|
+
return CommandResult(success=True, data={"message": "没有提交记录"})
|
|
115
|
+
|
|
116
|
+
grouped: dict[str, list[dict]] = {}
|
|
117
|
+
for line in lines:
|
|
118
|
+
parts = line.split("|||")
|
|
119
|
+
if len(parts) < 5:
|
|
120
|
+
continue
|
|
121
|
+
subject, body, commit_hash, author, date = parts[:5]
|
|
122
|
+
|
|
123
|
+
m = COMMIT_PATTERN.match(subject)
|
|
124
|
+
if not m:
|
|
125
|
+
commit_type = "other"
|
|
126
|
+
else:
|
|
127
|
+
commit_type = m.group("type") or "other"
|
|
128
|
+
|
|
129
|
+
grouped.setdefault(commit_type, []).append({
|
|
130
|
+
"subject": subject,
|
|
131
|
+
"hash": commit_hash[:8],
|
|
132
|
+
"author": author,
|
|
133
|
+
"date": date[:10],
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
# 渲染 Markdown
|
|
137
|
+
md = CHANGELOG_HEADER
|
|
138
|
+
md += f"## {since or '最新提交'}\n\n"
|
|
139
|
+
|
|
140
|
+
type_order = ["feat", "fix", "refactor", "perf", "docs", "style", "test", "chore", "other"]
|
|
141
|
+
for t in type_order:
|
|
142
|
+
if t not in grouped:
|
|
143
|
+
continue
|
|
144
|
+
label = CONVENTIONAL_TYPES.get(t, t.capitalize())
|
|
145
|
+
md += f"### {label}\n\n"
|
|
146
|
+
for commit in grouped[t]:
|
|
147
|
+
md += f"- {commit['subject']} ({commit['hash']})\n"
|
|
148
|
+
md += "\n"
|
|
149
|
+
|
|
150
|
+
return CommandResult(success=True, data={
|
|
151
|
+
"markdown": md,
|
|
152
|
+
"commit_count": len(lines),
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ── .gitignore 生成 ─────────────────────────────────
|
|
157
|
+
|
|
158
|
+
GITIGNORE_TEMPLATES: dict[str, list[str]] = {
|
|
159
|
+
"python": [
|
|
160
|
+
"# Python", "__pycache__/", "*.py[cod]", "*$py.class", "*.so",
|
|
161
|
+
".Python", "build/", "develop-eggs/", "dist/", "downloads/",
|
|
162
|
+
"*.egg-info/", ".eggs/", "*.egg", ".venv/", "venv/", "env/",
|
|
163
|
+
".pytest_cache/", ".ruff_cache/", ".mypy_cache/", "*.cover",
|
|
164
|
+
],
|
|
165
|
+
"node": [
|
|
166
|
+
"# Node", "node_modules/", "npm-debug.log*", "yarn-debug.log*",
|
|
167
|
+
"yarn-error.log*", ".pnp.*", ".yarn/", "dist/", "build/",
|
|
168
|
+
".next/", "coverage/", ".env.local", ".env.production",
|
|
169
|
+
],
|
|
170
|
+
"macos": [
|
|
171
|
+
"# macOS", ".DS_Store", ".AppleDouble", ".LSOverride",
|
|
172
|
+
"Icon\\x0d", "._*", ".Spotlight-V100", ".Trashes",
|
|
173
|
+
],
|
|
174
|
+
"ide": [
|
|
175
|
+
"# IDE", ".vscode/", ".idea/", "*.swp", "*.swo", "*~",
|
|
176
|
+
".project", ".classpath", ".settings/",
|
|
177
|
+
],
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def gitignore(types: list[str]) -> CommandResult:
|
|
182
|
+
"""生成 .gitignore 内容"""
|
|
183
|
+
lines = []
|
|
184
|
+
for t in types:
|
|
185
|
+
if t in GITIGNORE_TEMPLATES:
|
|
186
|
+
lines.extend(GITIGNORE_TEMPLATES[t])
|
|
187
|
+
|
|
188
|
+
if not lines:
|
|
189
|
+
types_list = ", ".join(GITIGNORE_TEMPLATES.keys())
|
|
190
|
+
return CommandResult(success=False, error=f"不支持的类型。支持: {types_list}")
|
|
191
|
+
|
|
192
|
+
content = "\n".join(lines) + "\n"
|
|
193
|
+
return CommandResult(success=True, data={"content": content, "types": types})
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ── 仓库统计 ────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
def stats(path: str = ".") -> CommandResult:
|
|
199
|
+
"""统计 Git 仓库信息"""
|
|
200
|
+
repo = Path(path).expanduser().resolve()
|
|
201
|
+
git_dir = repo / ".git"
|
|
202
|
+
if not git_dir.exists():
|
|
203
|
+
return CommandResult(success=False, error=f"不是 Git 仓库: {path}")
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
# 总提交数
|
|
207
|
+
r1 = subprocess.run(["git", "rev-list", "--count", "HEAD"],
|
|
208
|
+
capture_output=True, text=True, cwd=str(repo))
|
|
209
|
+
total_commits = r1.stdout.strip()
|
|
210
|
+
|
|
211
|
+
# 贡献者列表
|
|
212
|
+
r2 = subprocess.run(
|
|
213
|
+
["git", "shortlog", "-sn", "HEAD"],
|
|
214
|
+
capture_output=True, text=True, cwd=str(repo),
|
|
215
|
+
)
|
|
216
|
+
contributors = []
|
|
217
|
+
for line in r2.stdout.strip().split("\n"):
|
|
218
|
+
if line.strip():
|
|
219
|
+
parts = line.strip().split("\t", 1)
|
|
220
|
+
if len(parts) == 2:
|
|
221
|
+
commits = int(parts[0].strip())
|
|
222
|
+
name = parts[1].strip()
|
|
223
|
+
contributors.append({"commits": commits, "name": name})
|
|
224
|
+
|
|
225
|
+
# 分支数
|
|
226
|
+
r3 = subprocess.run(["git", "branch", "--list"],
|
|
227
|
+
capture_output=True, text=True, cwd=str(repo))
|
|
228
|
+
branch_count = len([b for b in r3.stdout.split("\n") if b.strip()])
|
|
229
|
+
|
|
230
|
+
# 代码行数
|
|
231
|
+
subprocess.run(
|
|
232
|
+
["git", "diff", "--shortstat", "HEAD~1..HEAD", "--"],
|
|
233
|
+
capture_output=True, text=True, cwd=str(repo),
|
|
234
|
+
# 如果 HEAD~1 不存在(第一次提交),忽略
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return CommandResult(success=True, data={
|
|
238
|
+
"total_commits": total_commits,
|
|
239
|
+
"contributors": contributors,
|
|
240
|
+
"contributor_count": len(contributors),
|
|
241
|
+
"branch_count": branch_count,
|
|
242
|
+
"path": str(repo),
|
|
243
|
+
})
|
|
244
|
+
except FileNotFoundError:
|
|
245
|
+
return CommandResult(success=False, error="git 未安装")
|
|
246
|
+
except Exception as e:
|
|
247
|
+
return CommandResult(success=False, error=str(e))
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
monitor — 系统资源监控
|
|
3
|
+
|
|
4
|
+
提供 CPU/内存/磁盘监控、进程列表、实时日志追踪。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from devmate.bus.command import CommandResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _run(cmd: list[str], timeout: int = 5) -> str:
|
|
15
|
+
"""运行 shell 命令并返回输出"""
|
|
16
|
+
try:
|
|
17
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
18
|
+
return result.stdout.strip()
|
|
19
|
+
except Exception:
|
|
20
|
+
return ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def dashboard() -> CommandResult:
|
|
24
|
+
"""系统资源概览"""
|
|
25
|
+
data: dict[str, Any] = {}
|
|
26
|
+
|
|
27
|
+
# CPU
|
|
28
|
+
cpu_info = _run(["python3", "-c", "import os; print(os.cpu_count() or 'N/A')"])
|
|
29
|
+
load = _run(["sysctl", "-n", "vm.loadavg"])[:50] if Path("/usr/bin/sysctl").exists() else ""
|
|
30
|
+
|
|
31
|
+
# 内存
|
|
32
|
+
try:
|
|
33
|
+
import psutil
|
|
34
|
+
mem = psutil.virtual_memory()
|
|
35
|
+
data["memory"] = {
|
|
36
|
+
"total": f"{mem.total / 1024**3:.1f}GB",
|
|
37
|
+
"used": f"{mem.used / 1024**3:.1f}GB",
|
|
38
|
+
"percent": mem.percent,
|
|
39
|
+
"available": f"{mem.available / 1024**3:.1f}GB",
|
|
40
|
+
}
|
|
41
|
+
except ImportError:
|
|
42
|
+
data["memory"] = {"total": "?", "used": "?", "percent": 0}
|
|
43
|
+
|
|
44
|
+
# 磁盘
|
|
45
|
+
disk = _run(["df", "-h", "/"])
|
|
46
|
+
data["disk"] = {"info": disk[:200]}
|
|
47
|
+
|
|
48
|
+
# 进程数
|
|
49
|
+
proc_count = _run(["ps", "-e", "--no-headers", "|", "wc", "-l"])
|
|
50
|
+
data["processes"] = proc_count
|
|
51
|
+
|
|
52
|
+
# 系统信息
|
|
53
|
+
hostname = _run(["hostname"])
|
|
54
|
+
uptime = _run(["uptime"])[:100]
|
|
55
|
+
data["system"] = {
|
|
56
|
+
"hostname": hostname,
|
|
57
|
+
"cpu_count": cpu_info,
|
|
58
|
+
"load": load,
|
|
59
|
+
"uptime": uptime,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return CommandResult(success=True, data=data)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def process_list(sort_by: str = "cpu", limit: int = 20) -> CommandResult:
|
|
66
|
+
"""进程列表"""
|
|
67
|
+
try:
|
|
68
|
+
import psutil
|
|
69
|
+
processes = []
|
|
70
|
+
for proc in psutil.process_iter(["pid", "name", "cpu_percent", "memory_percent", "status"]):
|
|
71
|
+
try:
|
|
72
|
+
info = proc.info
|
|
73
|
+
processes.append({
|
|
74
|
+
"pid": info["pid"],
|
|
75
|
+
"name": info["name"] or "",
|
|
76
|
+
"cpu": info["cpu_percent"] or 0.0,
|
|
77
|
+
"memory": info["memory_percent"] or 0.0,
|
|
78
|
+
"status": info["status"] or "",
|
|
79
|
+
})
|
|
80
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
if sort_by == "cpu":
|
|
84
|
+
processes.sort(key=lambda p: p["cpu"], reverse=True)
|
|
85
|
+
elif sort_by == "memory":
|
|
86
|
+
processes.sort(key=lambda p: p["memory"], reverse=True)
|
|
87
|
+
|
|
88
|
+
return CommandResult(success=True, data={
|
|
89
|
+
"processes": processes[:limit],
|
|
90
|
+
"total": len(processes),
|
|
91
|
+
})
|
|
92
|
+
except ImportError:
|
|
93
|
+
return CommandResult(success=False, error="需要安装 psutil: pip install psutil")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def log_tail(path: str, lines: int = 50) -> CommandResult:
|
|
97
|
+
"""实时日志追踪"""
|
|
98
|
+
log_file = Path(path).expanduser()
|
|
99
|
+
if not log_file.exists():
|
|
100
|
+
return CommandResult(success=False, error=f"日志文件不存在: {path}")
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
content = log_file.read_text(encoding="utf-8", errors="replace")
|
|
104
|
+
last_lines = "\n".join(content.split("\n")[-lines:])
|
|
105
|
+
return CommandResult(success=True, data={
|
|
106
|
+
"path": str(log_file),
|
|
107
|
+
"lines": lines,
|
|
108
|
+
"content": last_lines,
|
|
109
|
+
"size": log_file.stat().st_size,
|
|
110
|
+
})
|
|
111
|
+
except Exception as e:
|
|
112
|
+
return CommandResult(success=False, error=str(e))
|