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
devmate/app/cli.py
ADDED
|
@@ -0,0 +1,1663 @@
|
|
|
1
|
+
"""CLI 主入口——click Group + rich 渲染"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
from devmate import __app_name__, __description__, __version__
|
|
12
|
+
from devmate.core.logging import logger
|
|
13
|
+
from devmate.core.security import sanitize
|
|
14
|
+
from devmate.module.registry import registry
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ═══════════════════════════════════════════════════
|
|
20
|
+
# Welcome 横幅
|
|
21
|
+
# ═══════════════════════════════════════════════════
|
|
22
|
+
|
|
23
|
+
def print_welcome():
|
|
24
|
+
"""显示欢迎信息"""
|
|
25
|
+
text = Text()
|
|
26
|
+
text.append(f"\n {__app_name__} ", style="bold cyan")
|
|
27
|
+
text.append(f"v{__version__}", style="yellow")
|
|
28
|
+
text.append(f"\n {__description__}", style="dim")
|
|
29
|
+
console.print(Panel(text, border_style="cyan", padding=(1, 4)))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_INIT_FLAG = Path.home() / ".devmate" / ".initialized"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def first_run_wizard():
|
|
36
|
+
"""首次运行引导——4 步交互式向导"""
|
|
37
|
+
if _INIT_FLAG.exists():
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
_INIT_FLAG.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
welcome = Text()
|
|
43
|
+
welcome.append("\n ⚡ 欢迎使用 DevMate!", style="bold green")
|
|
44
|
+
welcome.append("\n\n 来看看你能用它做什么:", style="white")
|
|
45
|
+
console.print(Panel(welcome, border_style="green", padding=(1, 4)))
|
|
46
|
+
|
|
47
|
+
steps = [
|
|
48
|
+
("📁 整理文件", "devmate tk organize ~/Downloads --dry-run",
|
|
49
|
+
"整理 Downloads 目录,按类型分类"),
|
|
50
|
+
("🗄 查数据库", "devmate db connect sqlite:///test.db",
|
|
51
|
+
"连接 SQLite 数据库并执行查询"),
|
|
52
|
+
("🌐 调 API", 'devmate api get https://httpbin.org/get',
|
|
53
|
+
"发送 HTTP 请求查看响应"),
|
|
54
|
+
("🤖 AI 对话", "devmate ai add default --provider openai --key <你的 Key>",
|
|
55
|
+
"配置 AI 模型开始对话"),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
for i, (title, cmd, desc) in enumerate(steps, 1):
|
|
59
|
+
console.print(f"\n[bold cyan]步骤 {i}:{title}[/bold cyan]")
|
|
60
|
+
console.print(f" [dim]{desc}[/dim]")
|
|
61
|
+
console.print(f" [yellow]$ {cmd}[/yellow]")
|
|
62
|
+
|
|
63
|
+
console.print("\n[green]试试上面的命令吧![/green]")
|
|
64
|
+
console.print("[dim]提示:运行 devmate --help 查看全部命令 | Ctrl+C 跳过引导[/dim]\n")
|
|
65
|
+
|
|
66
|
+
# 标记首次运行完成
|
|
67
|
+
_INIT_FLAG.write_text("initialized")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_help():
|
|
71
|
+
"""打印主帮助"""
|
|
72
|
+
print_welcome()
|
|
73
|
+
console.print("\n [bold]用法:[/bold] devmate [模块] [命令] [参数]")
|
|
74
|
+
console.print(" [bold] [/bold] devmate --help")
|
|
75
|
+
|
|
76
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
77
|
+
table.add_column("模块", style="cyan", width=14)
|
|
78
|
+
table.add_column("入口", style="yellow", width=18)
|
|
79
|
+
table.add_column("说明", style="white")
|
|
80
|
+
|
|
81
|
+
table.add_row("toolkit", "devmate tk", "文件整理、压缩、查重、批量重命名")
|
|
82
|
+
table.add_row("apidev", "devmate api", "HTTP 客户端、Mock Server")
|
|
83
|
+
table.add_row("dbadmin", "devmate db", "SQL 编辑器、表浏览器")
|
|
84
|
+
table.add_row("apihub", "devmate ai", "AI 模型统一接入")
|
|
85
|
+
table.add_row("agent", "devmate agent", "Agent 执行引擎")
|
|
86
|
+
table.add_row("sshman", "devmate ssh", "SSH 连接管理器")
|
|
87
|
+
table.add_row("gitflow", "devmate git", "Git 工作流增强")
|
|
88
|
+
table.add_row("regexlab", "devmate re", "正则测试器")
|
|
89
|
+
table.add_row("notekeeper", "devmate note", "笔记知识库")
|
|
90
|
+
table.add_row("monitor", "devmate top", "系统监控")
|
|
91
|
+
table.add_row("reporter", "devmate report", "日报/周报生成")
|
|
92
|
+
table.add_row("scaffold", "devmate scaf", "项目脚手架")
|
|
93
|
+
table.add_row("publisher", "devmate pub", "文章发布")
|
|
94
|
+
table.add_row("vault", "devmate vault", "密钥管理器")
|
|
95
|
+
table.add_row("plugin", "devmate plugin", "插件管理")
|
|
96
|
+
table.add_row("audit", "devmate audit", "审计日志")
|
|
97
|
+
|
|
98
|
+
console.print(table)
|
|
99
|
+
console.print("\n [dim]运行 devmate [模块] --help 查看模块详情[/dim]")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ═══════════════════════════════════════════════════
|
|
103
|
+
# 顶层 click Group
|
|
104
|
+
# ═══════════════════════════════════════════════════
|
|
105
|
+
|
|
106
|
+
@click.group(invoke_without_command=True)
|
|
107
|
+
@click.option("--version", "-V", is_flag=True, help="显示版本号")
|
|
108
|
+
@click.option("--debug", is_flag=True, help="调试模式")
|
|
109
|
+
@click.option("--json", "json_output", is_flag=True, help="JSON 格式输出")
|
|
110
|
+
@click.option("--quiet", "-q", is_flag=True, help="安静模式,最小输出")
|
|
111
|
+
@click.pass_context
|
|
112
|
+
def cli(ctx, version, debug, json_output, quiet):
|
|
113
|
+
"""DevMate — 程序员终端瑞士军刀"""
|
|
114
|
+
ctx.ensure_object(dict)
|
|
115
|
+
|
|
116
|
+
if version:
|
|
117
|
+
console.print(f"{__app_name__} v{__version__}")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if debug:
|
|
121
|
+
logger.setLevel("DEBUG")
|
|
122
|
+
logger.debug("调试模式已启用")
|
|
123
|
+
|
|
124
|
+
if ctx.invoked_subcommand is None:
|
|
125
|
+
if not _INIT_FLAG.exists():
|
|
126
|
+
first_run_wizard()
|
|
127
|
+
else:
|
|
128
|
+
print_help()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ═══════════════════════════════════════════════════
|
|
132
|
+
# 模块子命令占位
|
|
133
|
+
# ═══════════════════════════════════════════════════
|
|
134
|
+
|
|
135
|
+
@cli.group("tk", help="文件整理、压缩、查重、批量重命名")
|
|
136
|
+
def tk_group():
|
|
137
|
+
"""toolkit — 文件瑞士军刀"""
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@tk_group.command("organize", help="按类型/日期整理文件")
|
|
142
|
+
@click.argument("source", default=".")
|
|
143
|
+
@click.option("--dry-run", is_flag=True, help="预览模式,不实际移动文件")
|
|
144
|
+
@click.option("--by", type=click.Choice(["type", "date"]), default="type", help="整理方式")
|
|
145
|
+
def tk_organize(source, dry_run, by):
|
|
146
|
+
"""按文件类型或修改日期将目录中的文件分类到子目录"""
|
|
147
|
+
from rich.table import Table
|
|
148
|
+
|
|
149
|
+
from devmate_toolkit import organize as _organize
|
|
150
|
+
|
|
151
|
+
result = _organize(source, dry_run=dry_run, by=by)
|
|
152
|
+
if not result.success:
|
|
153
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
data = result.data
|
|
157
|
+
console.print(f"\n[bold cyan]{data['message']}[/bold cyan]\n")
|
|
158
|
+
|
|
159
|
+
if result.data["details"]:
|
|
160
|
+
table = Table(show_header=True)
|
|
161
|
+
table.add_column("文件", style="cyan")
|
|
162
|
+
table.add_column("操作", style="yellow")
|
|
163
|
+
for d in data["details"][:20]: # 最多显示 20 条
|
|
164
|
+
table.add_row(d["file"], d["action"])
|
|
165
|
+
console.print(table)
|
|
166
|
+
if len(data["details"]) > 20:
|
|
167
|
+
console.print(f"[dim]... 还有 {len(data['details']) - 20} 个文件[/dim]")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@tk_group.command("size", help="目录大小排行")
|
|
171
|
+
@click.argument("path", default=".")
|
|
172
|
+
@click.option("--top", default=10, help="显示前 N 项")
|
|
173
|
+
def tk_size(path, top):
|
|
174
|
+
"""显示指定目录下的文件和子目录大小排行"""
|
|
175
|
+
from rich.table import Table
|
|
176
|
+
|
|
177
|
+
from devmate_toolkit import size as _size
|
|
178
|
+
|
|
179
|
+
result = _size(path, top=top)
|
|
180
|
+
if not result.success:
|
|
181
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
data = result.data
|
|
185
|
+
console.print(f"\n[bold cyan]📊 {data['path']} 的大小排行[/bold cyan]")
|
|
186
|
+
console.print(f" 总计:{data['total_size']} | 文件数:{data['file_count']}\n")
|
|
187
|
+
|
|
188
|
+
table = Table(show_header=True)
|
|
189
|
+
table.add_column("#", style="dim")
|
|
190
|
+
table.add_column("名称", style="cyan")
|
|
191
|
+
table.add_column("大小", style="yellow", justify="right")
|
|
192
|
+
|
|
193
|
+
for i, entry in enumerate(data["entries"], 1):
|
|
194
|
+
table.add_row(str(i), entry["name"], entry["size_str"])
|
|
195
|
+
console.print(table)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@tk_group.command("cleanup", help="清理缓存文件")
|
|
199
|
+
@click.argument("path", default=".")
|
|
200
|
+
@click.option("--dry-run", is_flag=True, help="预览模式,不实际删除")
|
|
201
|
+
def tk_cleanup(path, dry_run):
|
|
202
|
+
"""清理 __pycache__、.pyc、.DS_Store 等缓存文件"""
|
|
203
|
+
from rich.table import Table
|
|
204
|
+
|
|
205
|
+
from devmate_toolkit import cleanup as _cleanup
|
|
206
|
+
|
|
207
|
+
result = _cleanup(path, dry_run=dry_run)
|
|
208
|
+
if not result.success:
|
|
209
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
data = result.data
|
|
213
|
+
console.print(f"\n[bold cyan]{data['message']}[/bold cyan]\n")
|
|
214
|
+
|
|
215
|
+
if data["details"]:
|
|
216
|
+
table = Table(show_header=True)
|
|
217
|
+
table.add_column("路径", style="cyan")
|
|
218
|
+
table.add_column("大小", style="yellow", justify="right")
|
|
219
|
+
for d in data["details"][:20]:
|
|
220
|
+
table.add_row(d["name"], d["size"])
|
|
221
|
+
console.print(table)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@cli.group("api", help="HTTP 客户端、Mock Server")
|
|
225
|
+
def api_group():
|
|
226
|
+
"""apidev — API 开发工具"""
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
_HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
|
|
231
|
+
|
|
232
|
+
@api_group.command("request", help="发送 HTTP 请求")
|
|
233
|
+
@click.argument("method", type=click.Choice(_HTTP_METHODS))
|
|
234
|
+
@click.argument("url")
|
|
235
|
+
@click.option("--header", "-H", multiple=True, help="请求头")
|
|
236
|
+
@click.option("--param", "-P", multiple=True, help="URL 查询参数")
|
|
237
|
+
@click.option("--body", "-b", help="请求体(JSON)")
|
|
238
|
+
@click.option("--timeout", "-t", default=30, help="超时秒数")
|
|
239
|
+
@click.option("--save", is_flag=True, help="保存到历史记录")
|
|
240
|
+
def api_request(method, url, header, param, body, timeout, save):
|
|
241
|
+
from rich.syntax import Syntax
|
|
242
|
+
|
|
243
|
+
from devmate_apidev import request as _request
|
|
244
|
+
from devmate_apidev import save_history
|
|
245
|
+
|
|
246
|
+
headers = {}
|
|
247
|
+
for h in header:
|
|
248
|
+
if ":" in h:
|
|
249
|
+
k, v = h.split(":", 1)
|
|
250
|
+
headers[k.strip()] = v.strip()
|
|
251
|
+
|
|
252
|
+
params = {}
|
|
253
|
+
for p in param:
|
|
254
|
+
if "=" in p:
|
|
255
|
+
k, v = p.split("=", 1)
|
|
256
|
+
params[k.strip()] = v.strip()
|
|
257
|
+
|
|
258
|
+
result = _request(method, url, headers=headers, params=params, body=body, timeout=timeout)
|
|
259
|
+
if not result.success:
|
|
260
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
data = result.data
|
|
264
|
+
sc = data["status_code"]
|
|
265
|
+
color = "green" if sc < 300 else "yellow" if sc < 400 else "red"
|
|
266
|
+
console.print(f"\n[bold]{method.upper()} {data['url']}[/bold]")
|
|
267
|
+
console.print(f"[{color}]{sc} {data['status_text']}[/{color}] "
|
|
268
|
+
f"[dim]{data['elapsed_ms']}ms[/dim]\n")
|
|
269
|
+
|
|
270
|
+
console.print("[bold underline]响应头:[/bold underline]")
|
|
271
|
+
from rich.table import Table
|
|
272
|
+
ht = Table(show_header=False, box=None)
|
|
273
|
+
for k, v in list(data["headers"].items())[:15]:
|
|
274
|
+
ht.add_row(f" [cyan]{k}[/cyan]", str(v))
|
|
275
|
+
console.print(ht)
|
|
276
|
+
|
|
277
|
+
if data.get("body_json"):
|
|
278
|
+
import json as _json
|
|
279
|
+
console.print("\n[bold underline]响应体 (JSON):[/bold underline]")
|
|
280
|
+
syntax = Syntax(_json.dumps(data["body_json"], indent=2, ensure_ascii=False), "json")
|
|
281
|
+
console.print(syntax)
|
|
282
|
+
elif data["body"]:
|
|
283
|
+
console.print(f"\n[bold underline]响应体:[/bold underline]\n{data['body'][:2000]}")
|
|
284
|
+
|
|
285
|
+
if save:
|
|
286
|
+
save_history({"method": method.upper(), "url": url, "status": sc})
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@api_group.command("mock", help="启动/停止 Mock Server")
|
|
290
|
+
@click.argument("action", type=click.Choice(["start", "stop"]))
|
|
291
|
+
@click.option("--spec", "-s", default="openapi.json", help="OpenAPI spec")
|
|
292
|
+
@click.option("--port", "-p", default=8765, help="端口")
|
|
293
|
+
def api_mock(action, spec, port):
|
|
294
|
+
from devmate_apidev import mock_start, mock_stop
|
|
295
|
+
|
|
296
|
+
if action == "start":
|
|
297
|
+
result = mock_start(spec, port=port)
|
|
298
|
+
if result.success:
|
|
299
|
+
d = result.data
|
|
300
|
+
console.print(f"[green]✅ {d['message']}[/green]")
|
|
301
|
+
console.print(f" 端口: {d['port']} PID: {d['pid']}")
|
|
302
|
+
if d.get("endpoints"):
|
|
303
|
+
console.print("[bold underline]可用端点:[/bold underline]")
|
|
304
|
+
for ep in d["endpoints"]:
|
|
305
|
+
console.print(f" [cyan]{ep}[/cyan]")
|
|
306
|
+
else:
|
|
307
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
308
|
+
else:
|
|
309
|
+
result = mock_stop()
|
|
310
|
+
console.print(f"[green]✅ {result.data['message']}[/green]" if result.success
|
|
311
|
+
else f"[red]❌ {result.error}[/red]")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@api_group.command("history", help="查看请求历史")
|
|
315
|
+
@click.option("--limit", default=20, help="显示条数")
|
|
316
|
+
def api_history(limit):
|
|
317
|
+
from rich.table import Table
|
|
318
|
+
|
|
319
|
+
from devmate_apidev import load_history
|
|
320
|
+
|
|
321
|
+
entries = load_history(limit=limit)
|
|
322
|
+
if not entries:
|
|
323
|
+
console.print("[yellow]暂无请求历史[/yellow]")
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
table = Table(title=f"请求历史(最近 {len(entries)} 条)")
|
|
327
|
+
table.add_column("方法", style="bold")
|
|
328
|
+
table.add_column("URL", style="cyan")
|
|
329
|
+
table.add_column("状态码", style="yellow")
|
|
330
|
+
for e in entries:
|
|
331
|
+
table.add_row(e.get("method", ""), e.get("url", ""), str(e.get("status", "")))
|
|
332
|
+
console.print(table)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@cli.group("db", help="SQL 编辑器、表浏览器")
|
|
336
|
+
def db_group():
|
|
337
|
+
"""dbadmin — 数据库管理"""
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@db_group.command("connect", help="连接数据库")
|
|
342
|
+
@click.argument("dsn")
|
|
343
|
+
@click.option("--name", default="default", help="连接名称")
|
|
344
|
+
def db_connect(dsn, name):
|
|
345
|
+
"""连接数据库,如:devmate db connect sqlite:///test.db"""
|
|
346
|
+
from devmate_dbadmin import connect as _connect
|
|
347
|
+
result = _connect(dsn, name=name)
|
|
348
|
+
if result.success:
|
|
349
|
+
console.print(f"[green]{result.data['message']}[/green]")
|
|
350
|
+
else:
|
|
351
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@db_group.command("list-tables", help="列出所有表")
|
|
355
|
+
@click.option("--name", default="default", help="连接名称")
|
|
356
|
+
def db_list_tables(name):
|
|
357
|
+
"""列出数据库中所有表及其行数"""
|
|
358
|
+
from rich.table import Table
|
|
359
|
+
|
|
360
|
+
from devmate_dbadmin import list_tables
|
|
361
|
+
|
|
362
|
+
result = list_tables(name=name)
|
|
363
|
+
if not result.success:
|
|
364
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
data = result.data
|
|
368
|
+
table = Table(title=f"数据库表列表(共 {data['total']} 张)")
|
|
369
|
+
table.add_column("表名", style="cyan")
|
|
370
|
+
table.add_column("行数", style="yellow", justify="right")
|
|
371
|
+
|
|
372
|
+
for t in data["tables"]:
|
|
373
|
+
table.add_row(t["name"], str(t["row_count"]))
|
|
374
|
+
console.print(table)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@db_group.command("show", help="查看表结构")
|
|
378
|
+
@click.argument("table_name")
|
|
379
|
+
@click.option("--name", default="default", help="连接名称")
|
|
380
|
+
def db_show(table_name, name):
|
|
381
|
+
"""查看表的列信息、类型、约束,及前 10 行数据预览"""
|
|
382
|
+
from rich.table import Table
|
|
383
|
+
|
|
384
|
+
from devmate_dbadmin import show_table
|
|
385
|
+
|
|
386
|
+
result = show_table(table_name, name=name)
|
|
387
|
+
if not result.success:
|
|
388
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
data = result.data
|
|
392
|
+
# 列信息
|
|
393
|
+
col_table = Table(title=f"表结构: {data['table']}")
|
|
394
|
+
col_table.add_column("列名", style="cyan")
|
|
395
|
+
col_table.add_column("类型", style="yellow")
|
|
396
|
+
col_table.add_column("可空", style="green")
|
|
397
|
+
col_table.add_column("主键", style="bold")
|
|
398
|
+
col_table.add_column("默认值", style="dim")
|
|
399
|
+
|
|
400
|
+
for col in data["columns"]:
|
|
401
|
+
col_table.add_row(
|
|
402
|
+
col["name"],
|
|
403
|
+
col["type"],
|
|
404
|
+
"✅" if col["nullable"] else "❌",
|
|
405
|
+
"🔑" if col["primary_key"] else "",
|
|
406
|
+
col["default"],
|
|
407
|
+
)
|
|
408
|
+
console.print(col_table)
|
|
409
|
+
|
|
410
|
+
# 数据预览
|
|
411
|
+
preview = data.get("preview")
|
|
412
|
+
if preview and preview["rows"]:
|
|
413
|
+
pt = Table(title="数据预览(前 10 行)")
|
|
414
|
+
for c in preview["columns"]:
|
|
415
|
+
pt.add_column(c)
|
|
416
|
+
for row in preview["rows"]:
|
|
417
|
+
pt.add_row(*[str(v) if v is not None else "[dim]NULL[/dim]" for v in row])
|
|
418
|
+
console.print(pt)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@db_group.command("query", help="执行 SQL 查询")
|
|
422
|
+
@click.argument("sql")
|
|
423
|
+
@click.option("--name", default="default", help="连接名称")
|
|
424
|
+
@click.option("--timeout", default=5, help="超时秒数")
|
|
425
|
+
def db_query(sql, name, timeout):
|
|
426
|
+
"""执行 SQL SELECT 查询并显示结果表格"""
|
|
427
|
+
from rich.table import Table
|
|
428
|
+
|
|
429
|
+
from devmate_dbadmin import query as _query
|
|
430
|
+
|
|
431
|
+
result = _query(sql, name=name, timeout=timeout)
|
|
432
|
+
if not result.success:
|
|
433
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
434
|
+
if result.suggestion:
|
|
435
|
+
console.print(f"[yellow]💡 {result.suggestion}[/yellow]")
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
data = result.data
|
|
439
|
+
if data["type"] == "query":
|
|
440
|
+
dt = Table(title=f"查询结果({data['row_count']} 行, {data['elapsed_ms']}ms)")
|
|
441
|
+
for col in data["columns"]:
|
|
442
|
+
dt.add_column(str(col))
|
|
443
|
+
for row in data["rows"]:
|
|
444
|
+
dt.add_row(*[str(v) if v is not None else "[dim]NULL[/dim]" for v in row])
|
|
445
|
+
console.print(dt)
|
|
446
|
+
else:
|
|
447
|
+
console.print(f"[green]{data.get('message', '执行成功')}[/green]"
|
|
448
|
+
f" [dim]{data['elapsed_ms']}ms[/dim]")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@db_group.command("saved", help="管理保存的查询")
|
|
452
|
+
@click.argument("action", type=click.Choice(["list", "save", "run"]))
|
|
453
|
+
@click.argument("name", required=False)
|
|
454
|
+
@click.option("--sql", help="SQL 语句(save 时使用)")
|
|
455
|
+
@click.option("--conn", default="default", help="连接名称(run 时使用)")
|
|
456
|
+
def db_saved(action, name, sql, conn):
|
|
457
|
+
"""保存、列出、运行常用查询"""
|
|
458
|
+
from rich.table import Table
|
|
459
|
+
|
|
460
|
+
from devmate_dbadmin import list_saved_queries, run_saved_query, save_query
|
|
461
|
+
|
|
462
|
+
if action == "list":
|
|
463
|
+
result = list_saved_queries()
|
|
464
|
+
if not result.success:
|
|
465
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
466
|
+
return
|
|
467
|
+
queries = result.data["queries"]
|
|
468
|
+
if not queries:
|
|
469
|
+
hint = "暂无保存的查询。用 devmate db saved save <名字> --sql \"...\" 保存"
|
|
470
|
+
console.print(f"[yellow]{hint}[/yellow]")
|
|
471
|
+
return
|
|
472
|
+
qt = Table(title=f"保存的查询(共 {len(queries)} 个)")
|
|
473
|
+
qt.add_column("名称", style="cyan")
|
|
474
|
+
qt.add_column("SQL", style="yellow")
|
|
475
|
+
qt.add_column("创建时间", style="dim")
|
|
476
|
+
for q in queries:
|
|
477
|
+
sql_display = q["sql"][:60]
|
|
478
|
+
if len(q["sql"]) > 60:
|
|
479
|
+
sql_display += "..."
|
|
480
|
+
qt.add_row(q["name"], sql_display, q.get("created", ""))
|
|
481
|
+
console.print(qt)
|
|
482
|
+
elif action == "save":
|
|
483
|
+
if not name or not sql:
|
|
484
|
+
err = "请指定查询名称和 SQL。用法: devmate db saved save <名字> --sql \"SELECT ...\""
|
|
485
|
+
console.print(f"[red]{err}[/red]")
|
|
486
|
+
return
|
|
487
|
+
result = save_query(name, sql)
|
|
488
|
+
console.print(f"[green]✅ {result.data['message']}[/green]")
|
|
489
|
+
elif action == "run":
|
|
490
|
+
if not name:
|
|
491
|
+
console.print("[red]请指定查询名称。用法: devmate db saved run <名字>[/red]")
|
|
492
|
+
return
|
|
493
|
+
result = run_saved_query(name, conn_name=conn)
|
|
494
|
+
if not result.success:
|
|
495
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
496
|
+
return
|
|
497
|
+
data = result.data
|
|
498
|
+
if data["type"] == "query":
|
|
499
|
+
dt = Table(title=f"查询结果({data['row_count']} 行)")
|
|
500
|
+
for col in data["columns"]:
|
|
501
|
+
dt.add_column(str(col))
|
|
502
|
+
for row in data["rows"]:
|
|
503
|
+
dt.add_row(*[str(v) if v is not None else "[dim]NULL[/dim]" for v in row])
|
|
504
|
+
console.print(dt)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@cli.group("ai", help="AI 模型统一接入、成本追踪")
|
|
508
|
+
def ai_group():
|
|
509
|
+
"""apihub — AI 模型中心"""
|
|
510
|
+
pass
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@ai_group.command("add", help="添加 AI 模型配置")
|
|
514
|
+
@click.argument("name")
|
|
515
|
+
@click.option("--provider", "-p", default="openai", help="提供商: openai, deepseek")
|
|
516
|
+
@click.option("--key", "-k", help="API Key(或交互式输入)")
|
|
517
|
+
@click.option("--model", "-m", help="模型名")
|
|
518
|
+
@click.option("--api-base", help="API 地址")
|
|
519
|
+
def ai_add(name, provider, key, model, api_base):
|
|
520
|
+
"""添加一个 AI 模型提供商配置"""
|
|
521
|
+
from devmate_apihub import add_provider
|
|
522
|
+
|
|
523
|
+
if not key:
|
|
524
|
+
key = click.prompt("请输入 API Key", hide_input=True)
|
|
525
|
+
|
|
526
|
+
result = add_provider(name, provider, key, model=model or "", api_base=api_base or "")
|
|
527
|
+
if result.success:
|
|
528
|
+
console.print(f"[green]{result.data['message']}[/green]")
|
|
529
|
+
else:
|
|
530
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@ai_group.command("list", help="列出已配置的模型")
|
|
534
|
+
def ai_list():
|
|
535
|
+
"""列出所有已配置的 AI 模型"""
|
|
536
|
+
from rich.table import Table
|
|
537
|
+
|
|
538
|
+
from devmate_apihub import list_providers
|
|
539
|
+
|
|
540
|
+
result = list_providers()
|
|
541
|
+
if not result.success:
|
|
542
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
providers = result.data["providers"]
|
|
546
|
+
if not providers:
|
|
547
|
+
console.print("[yellow]暂无配置。用 devmate ai add <名字> --provider openai 添加[/yellow]")
|
|
548
|
+
return
|
|
549
|
+
|
|
550
|
+
table = Table(title="已配置的 AI 模型")
|
|
551
|
+
table.add_column("名称", style="cyan")
|
|
552
|
+
table.add_column("提供商", style="yellow")
|
|
553
|
+
table.add_column("模型", style="green")
|
|
554
|
+
|
|
555
|
+
for p in providers:
|
|
556
|
+
table.add_row(p["name"], p["provider"], p["model"])
|
|
557
|
+
console.print(table)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
@ai_group.command("remove", help="删除模型配置")
|
|
561
|
+
@click.argument("name")
|
|
562
|
+
@click.confirmation_option(prompt="确认删除此配置?")
|
|
563
|
+
def ai_remove(name):
|
|
564
|
+
"""删除指定的 AI 模型配置"""
|
|
565
|
+
from devmate_apihub import remove_provider
|
|
566
|
+
|
|
567
|
+
result = remove_provider(name)
|
|
568
|
+
if result.success:
|
|
569
|
+
console.print(f"[green]✅ {result.data['message']}[/green]")
|
|
570
|
+
else:
|
|
571
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
@ai_group.command("chat", help="AI 对话")
|
|
575
|
+
@click.argument("message")
|
|
576
|
+
@click.option("--provider", "-p", default="default", help="模型配置名")
|
|
577
|
+
@click.option("--system", "-s", help="系统提示词")
|
|
578
|
+
def ai_chat(message, provider, system):
|
|
579
|
+
"""与 AI 模型对话(流式输出)"""
|
|
580
|
+
from devmate_apihub import chat as _chat
|
|
581
|
+
|
|
582
|
+
console.print("[dim]🤖 正在思考...[/dim]")
|
|
583
|
+
result = _chat(message, provider_name=provider, system_prompt=system or "")
|
|
584
|
+
if result.success:
|
|
585
|
+
console.print(f"\n[cyan]🤖 {result.data['reply']}[/cyan]\n")
|
|
586
|
+
else:
|
|
587
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@ai_group.command("history", help="查看对话历史")
|
|
591
|
+
@click.option("--provider", "-p", help="按模型名过滤")
|
|
592
|
+
@click.option("--load", help="加载指定对话")
|
|
593
|
+
def ai_history(provider, load):
|
|
594
|
+
"""查看或加载 AI 对话历史"""
|
|
595
|
+
from rich.table import Table
|
|
596
|
+
|
|
597
|
+
from devmate_apihub import list_chats, load_chat
|
|
598
|
+
|
|
599
|
+
if load:
|
|
600
|
+
result = load_chat(load)
|
|
601
|
+
if not result.success:
|
|
602
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
603
|
+
return
|
|
604
|
+
console.print(f"\n[bold underline]对话: {load}[/bold underline]\n")
|
|
605
|
+
for msg in result.data["messages"]:
|
|
606
|
+
prefix = "[cyan]🤖[/cyan]" if msg["role"] == "assistant" else "[green]🧑[/green]"
|
|
607
|
+
console.print(f"{prefix} {msg['content'][:500]}\n")
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
result = list_chats(provider_name=provider or "")
|
|
611
|
+
if not result.success:
|
|
612
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
chats = result.data["chats"]
|
|
616
|
+
if not chats:
|
|
617
|
+
console.print("[yellow]暂无对话历史[/yellow]")
|
|
618
|
+
return
|
|
619
|
+
|
|
620
|
+
table = Table(title="对话历史")
|
|
621
|
+
table.add_column("文件名", style="cyan")
|
|
622
|
+
table.add_column("大小", style="yellow")
|
|
623
|
+
table.add_column("时间", style="dim")
|
|
624
|
+
for c in chats:
|
|
625
|
+
table.add_row(c["name"], str(c["size"]), c["modified"])
|
|
626
|
+
console.print(table)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
@cli.group("agent", help="Agent 执行引擎(调用工具完成任务)")
|
|
630
|
+
def agent_group():
|
|
631
|
+
"""agent-harness — Agent 引擎"""
|
|
632
|
+
pass
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@agent_group.command("chat", help="与 Agent 对话,完成任务")
|
|
636
|
+
@click.argument("message")
|
|
637
|
+
@click.option("--system", "-s", help="自定义系统提示词")
|
|
638
|
+
def agent_chat(message, system):
|
|
639
|
+
"""向 Agent 发送任务,Agent 会自动调用工具完成"""
|
|
640
|
+
from rich.table import Table
|
|
641
|
+
|
|
642
|
+
from devmate_agent import AgentRuntime
|
|
643
|
+
|
|
644
|
+
console.print("[dim]🤖 Agent 思考中...[/dim]\n")
|
|
645
|
+
runtime = AgentRuntime(system_prompt=system or "")
|
|
646
|
+
result = runtime.run(message)
|
|
647
|
+
|
|
648
|
+
data = result.data
|
|
649
|
+
|
|
650
|
+
if data["output"]:
|
|
651
|
+
console.print(f"[cyan]{data['output']}[/cyan]\n")
|
|
652
|
+
|
|
653
|
+
if data.get("steps"):
|
|
654
|
+
table = Table(title=f"执行步骤(共 {data['step_count']} 步, {data['duration_ms']}ms)")
|
|
655
|
+
table.add_column("#", style="dim")
|
|
656
|
+
table.add_column("工具", style="cyan")
|
|
657
|
+
table.add_column("状态", style="bold")
|
|
658
|
+
table.add_column("结果", style="white")
|
|
659
|
+
|
|
660
|
+
for s in data["steps"]:
|
|
661
|
+
status = "[green]✅[/green]" if s["status"] == "success" else "[red]❌[/red]"
|
|
662
|
+
result_text = s.get("result", "")[:40]
|
|
663
|
+
table.add_row(str(s["step"]), s["tool"], status, result_text)
|
|
664
|
+
console.print(table)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
@agent_group.command("tools", help="列出 Agent 可用工具")
|
|
668
|
+
def agent_tools():
|
|
669
|
+
"""列出 Agent 可调用的所有工具"""
|
|
670
|
+
from rich.table import Table
|
|
671
|
+
|
|
672
|
+
from devmate_agent import AgentRuntime
|
|
673
|
+
|
|
674
|
+
runtime = AgentRuntime()
|
|
675
|
+
tools = runtime.list_available_tools()
|
|
676
|
+
|
|
677
|
+
table = Table(title=f"可用工具(共 {len(tools)} 个)")
|
|
678
|
+
table.add_column("工具名", style="cyan")
|
|
679
|
+
table.add_column("说明", style="white")
|
|
680
|
+
table.add_column("参数", style="yellow", width=40)
|
|
681
|
+
|
|
682
|
+
for t in tools:
|
|
683
|
+
params = ", ".join(f"{p['name']}({p['type']})" for p in t["parameters"])
|
|
684
|
+
table.add_row(f"!{t['name']}", t["description"], params)
|
|
685
|
+
console.print(table)
|
|
686
|
+
console.print("[dim]在对话中使用 !工具名(参数) 来调用工具[/dim]")
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
@agent_group.command("review", help="代码审查 Agent")
|
|
690
|
+
@click.option("--path", default=".", help="Git 仓库路径")
|
|
691
|
+
def agent_review(path):
|
|
692
|
+
"""审查当前 Git 仓库的代码变更"""
|
|
693
|
+
from devmate_agent import code_review as _review
|
|
694
|
+
|
|
695
|
+
console.print("[dim]🔍 分析代码变更...[/dim]")
|
|
696
|
+
result = _review(diff_path=path)
|
|
697
|
+
|
|
698
|
+
if result.success:
|
|
699
|
+
console.print(f"\n[cyan]{result.data['output']}[/cyan]")
|
|
700
|
+
else:
|
|
701
|
+
console.print(f"[yellow]输出: {result.data.get('output', '')}[/yellow]")
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
@agent_group.command("files", help="文件管家 Agent")
|
|
705
|
+
@click.argument("task")
|
|
706
|
+
@click.option("--path", default=".", help="工作目录")
|
|
707
|
+
def agent_files(task, path):
|
|
708
|
+
"""用自然语言管理文件"""
|
|
709
|
+
from devmate_agent import file_butler
|
|
710
|
+
|
|
711
|
+
console.print("[dim]📁 文件管家工作中...[/dim]")
|
|
712
|
+
result = file_butler(task, path=path)
|
|
713
|
+
|
|
714
|
+
if result.success:
|
|
715
|
+
console.print(f"\n[cyan]{result.data['output']}[/cyan]")
|
|
716
|
+
else:
|
|
717
|
+
console.print(f"[yellow]{result.data.get('output', '')}[/yellow]")
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@cli.group("ssh", help="SSH 连接管理器、命令执行、端口转发")
|
|
721
|
+
def ssh_group():
|
|
722
|
+
"""sshman — SSH 管理器"""
|
|
723
|
+
pass
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
@ssh_group.command("add", help="添加 SSH 连接配置")
|
|
727
|
+
@click.argument("name")
|
|
728
|
+
@click.argument("hostname")
|
|
729
|
+
@click.argument("username")
|
|
730
|
+
@click.option("--port", "-p", default=22, help="SSH 端口")
|
|
731
|
+
@click.option("--key", "key_file", help="私钥文件路径")
|
|
732
|
+
@click.option("--password", help="密码(加密存储在 vault)")
|
|
733
|
+
def ssh_add(name, hostname, username, port, key_file, password):
|
|
734
|
+
"""添加 SSH 服务器连接配置"""
|
|
735
|
+
from devmate_sshman import add as _add
|
|
736
|
+
|
|
737
|
+
result = _add(name, hostname, username, port=port,
|
|
738
|
+
key_file=key_file or "", password=password or "")
|
|
739
|
+
if result.success:
|
|
740
|
+
console.print(f"[green]{result.data['message']}[/green]")
|
|
741
|
+
else:
|
|
742
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
@ssh_group.command("list", help="列出 SSH 连接")
|
|
746
|
+
def ssh_list():
|
|
747
|
+
"""列出所有已配置的 SSH 连接"""
|
|
748
|
+
from rich.table import Table
|
|
749
|
+
|
|
750
|
+
from devmate_sshman import list_connections
|
|
751
|
+
|
|
752
|
+
result = list_connections()
|
|
753
|
+
if not result.success:
|
|
754
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
755
|
+
return
|
|
756
|
+
|
|
757
|
+
connections = result.data["connections"]
|
|
758
|
+
if not connections:
|
|
759
|
+
hint = "暂无配置。用 devmate ssh add <名字> <主机> <用户> 添加"
|
|
760
|
+
console.print(f"[yellow]{hint}[/yellow]")
|
|
761
|
+
return
|
|
762
|
+
|
|
763
|
+
table = Table(title="SSH 连接")
|
|
764
|
+
table.add_column("名称", style="cyan")
|
|
765
|
+
table.add_column("主机", style="yellow")
|
|
766
|
+
table.add_column("用户", style="green")
|
|
767
|
+
table.add_column("端口", style="dim")
|
|
768
|
+
table.add_column("密钥", style="dim")
|
|
769
|
+
|
|
770
|
+
for c in connections:
|
|
771
|
+
table.add_row(c["name"], c["hostname"], c["username"],
|
|
772
|
+
str(c["port"]), c.get("key_file", "") or "-")
|
|
773
|
+
console.print(table)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
@ssh_group.command("remove", help="删除 SSH 连接")
|
|
777
|
+
@click.argument("name")
|
|
778
|
+
@click.confirmation_option(prompt="确认删除此连接?")
|
|
779
|
+
def ssh_remove(name):
|
|
780
|
+
"""删除指定的 SSH 连接配置"""
|
|
781
|
+
from devmate_sshman import remove as _remove
|
|
782
|
+
|
|
783
|
+
result = _remove(name)
|
|
784
|
+
if result.success:
|
|
785
|
+
console.print(f"[green]✅ {result.data['message']}[/green]")
|
|
786
|
+
else:
|
|
787
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
@ssh_group.command("connect", help="测试 SSH 连接")
|
|
791
|
+
@click.argument("name")
|
|
792
|
+
def ssh_connect(name):
|
|
793
|
+
"""测试能否连接到 SSH 服务器"""
|
|
794
|
+
from devmate_sshman import connect as _connect
|
|
795
|
+
|
|
796
|
+
result = _connect(name)
|
|
797
|
+
if result.success:
|
|
798
|
+
console.print(f"[green]{result.data['message']}[/green]")
|
|
799
|
+
else:
|
|
800
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
@ssh_group.command("run", help="在远程服务器执行命令")
|
|
804
|
+
@click.argument("name")
|
|
805
|
+
@click.argument("command")
|
|
806
|
+
def ssh_run(name, command):
|
|
807
|
+
"""在远程服务器上执行一条命令"""
|
|
808
|
+
from devmate_sshman import run_command as _run
|
|
809
|
+
|
|
810
|
+
result = _run(name, command)
|
|
811
|
+
if not result.success:
|
|
812
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
813
|
+
return
|
|
814
|
+
|
|
815
|
+
data = result.data
|
|
816
|
+
console.print(f"[dim]📡 {data['hostname']}$ {data['command']}[/dim]\n")
|
|
817
|
+
if data["stdout"]:
|
|
818
|
+
console.print(data["stdout"])
|
|
819
|
+
if data["stderr"]:
|
|
820
|
+
console.print(f"[red]{data['stderr']}[/red]")
|
|
821
|
+
console.print(f"\n[dim]exit code: {data['exit_code']}[/dim]")
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
@ssh_group.command("batch", help="批量执行命令")
|
|
825
|
+
@click.argument("names", nargs=-1, required=True)
|
|
826
|
+
@click.argument("command")
|
|
827
|
+
def ssh_batch(names, command):
|
|
828
|
+
"""在多个服务器上批量执行同一命令"""
|
|
829
|
+
from rich.table import Table
|
|
830
|
+
|
|
831
|
+
from devmate_sshman import batch as _batch
|
|
832
|
+
|
|
833
|
+
console.print(f"[bold]在 {len(names)} 台服务器上执行:[/bold] [yellow]{command}[/yellow]\n")
|
|
834
|
+
|
|
835
|
+
result = _batch(list(names), command)
|
|
836
|
+
if not result.success:
|
|
837
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
838
|
+
return
|
|
839
|
+
|
|
840
|
+
data = result.data
|
|
841
|
+
table = Table(title=f"批量执行结果({data['success_count']} 成功, {data['fail_count']} 失败)")
|
|
842
|
+
table.add_column("服务器", style="cyan")
|
|
843
|
+
table.add_column("结果", style="bold")
|
|
844
|
+
table.add_column("输出", style="white")
|
|
845
|
+
|
|
846
|
+
for r in data["results"]:
|
|
847
|
+
if r["success"]:
|
|
848
|
+
out = r.get("stdout", "").strip()
|
|
849
|
+
table.add_row(r["name"], "✅", out[:60] if out else "(空)")
|
|
850
|
+
else:
|
|
851
|
+
table.add_row(r["name"], "❌", r.get("error", "")[:60])
|
|
852
|
+
console.print(table)
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
@ssh_group.command("tunnel", help="启动 SSH 端口转发")
|
|
856
|
+
@click.argument("name")
|
|
857
|
+
@click.option("--local-port", "-L", default=8080, help="本地端口")
|
|
858
|
+
@click.option("--remote-host", "-R", default="127.0.0.1", help="远程主机")
|
|
859
|
+
@click.option("--remote-port", "-P", default=80, help="远程端口")
|
|
860
|
+
def ssh_tunnel(name, local_port, remote_host, remote_port):
|
|
861
|
+
"""启动 SSH 端口转发到远程服务器"""
|
|
862
|
+
from devmate_sshman import tunnel as _tunnel
|
|
863
|
+
|
|
864
|
+
result = _tunnel(name, local_port, remote_host=remote_host, remote_port=remote_port)
|
|
865
|
+
if result.success:
|
|
866
|
+
console.print(f"[green]✅ {result.data['message']}[/green]")
|
|
867
|
+
else:
|
|
868
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
@cli.group("git", help="Git 工作流增强、Commit 规范、统计")
|
|
872
|
+
def git_group():
|
|
873
|
+
"""gitflow — Git 增强"""
|
|
874
|
+
pass
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
@git_group.command("check", help="校验 Conventional Commit 格式")
|
|
878
|
+
@click.argument("message")
|
|
879
|
+
def git_check(message):
|
|
880
|
+
"""校验 Git commit message 是否符合 Conventional Commit 规范"""
|
|
881
|
+
from rich.table import Table
|
|
882
|
+
|
|
883
|
+
from devmate_gitflow import commit_check
|
|
884
|
+
|
|
885
|
+
result = commit_check(message)
|
|
886
|
+
if not result.success:
|
|
887
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
888
|
+
return
|
|
889
|
+
|
|
890
|
+
data = result.data
|
|
891
|
+
style = "[green]" if data["valid"] else "[red]"
|
|
892
|
+
console.print(f"\n{style}提交信息结构分析[/]\n")
|
|
893
|
+
table = Table(show_header=False, box=None)
|
|
894
|
+
table.add_column("字段", style="cyan", width=12)
|
|
895
|
+
table.add_column("值", style="yellow")
|
|
896
|
+
table.add_row("类型", data["type"])
|
|
897
|
+
table.add_row("范围", data["scope"] or "(无)")
|
|
898
|
+
table.add_row("描述", data["description"])
|
|
899
|
+
table.add_row("Breaking", "⚠️ 是" if data["breaking"] else "否")
|
|
900
|
+
console.print(table)
|
|
901
|
+
|
|
902
|
+
if data.get("warnings"):
|
|
903
|
+
console.print("\n[bold yellow]⚠️ 警告:[/bold yellow]")
|
|
904
|
+
for w in data["warnings"]:
|
|
905
|
+
console.print(f" [yellow]• {w}[/yellow]")
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
@git_group.command("changelog", help="自动生成 CHANGELOG")
|
|
909
|
+
@click.option("--since", "-s", help="起始日期,如 '2026-01-01'")
|
|
910
|
+
@click.option("--path", default=".", help="Git 仓库路径")
|
|
911
|
+
def git_changelog(since, path):
|
|
912
|
+
"""从 git log 自动生成 CHANGELOG"""
|
|
913
|
+
from devmate_gitflow import changelog
|
|
914
|
+
|
|
915
|
+
result = changelog(path=path, since=since or "")
|
|
916
|
+
if not result.success:
|
|
917
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
918
|
+
return
|
|
919
|
+
|
|
920
|
+
console.print(result.data["markdown"])
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
@git_group.command("ignore", help="生成 .gitignore")
|
|
924
|
+
@click.argument("types", nargs=-1, required=True)
|
|
925
|
+
def git_ignore(types):
|
|
926
|
+
"""生成 .gitignore 内容"""
|
|
927
|
+
from devmate_gitflow import gitignore
|
|
928
|
+
|
|
929
|
+
result = gitignore(list(types))
|
|
930
|
+
if not result.success:
|
|
931
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
932
|
+
return
|
|
933
|
+
|
|
934
|
+
console.print(f"[dim]# .gitignore 生成: {', '.join(result.data['types'])}[/dim]")
|
|
935
|
+
console.print(result.data["content"])
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
@git_group.command("stats", help="Git 仓库统计")
|
|
939
|
+
@click.option("--path", default=".", help="Git 仓库路径")
|
|
940
|
+
def git_stats(path):
|
|
941
|
+
"""统计仓库信息"""
|
|
942
|
+
from rich.table import Table
|
|
943
|
+
|
|
944
|
+
from devmate_gitflow import stats
|
|
945
|
+
|
|
946
|
+
result = stats(path=path)
|
|
947
|
+
if not result.success:
|
|
948
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
949
|
+
return
|
|
950
|
+
|
|
951
|
+
data = result.data
|
|
952
|
+
console.print(f"\n[bold]📊 Git 仓库统计: {data['path']}[/bold]\n")
|
|
953
|
+
|
|
954
|
+
table = Table(show_header=False, box=None)
|
|
955
|
+
table.add_column("指标", style="cyan", width=18)
|
|
956
|
+
table.add_column("值", style="yellow")
|
|
957
|
+
table.add_row("总提交数", str(data["total_commits"]))
|
|
958
|
+
table.add_row("贡献者数", str(data["contributor_count"]))
|
|
959
|
+
table.add_row("分支数", str(data["branch_count"]))
|
|
960
|
+
console.print(table)
|
|
961
|
+
|
|
962
|
+
if data.get("contributors"):
|
|
963
|
+
ct = Table(title="贡献者排名")
|
|
964
|
+
ct.add_column("#", style="dim", width=4)
|
|
965
|
+
ct.add_column("名字", style="cyan")
|
|
966
|
+
ct.add_column("提交数", style="yellow", justify="right")
|
|
967
|
+
for i, c in enumerate(data["contributors"], 1):
|
|
968
|
+
ct.add_row(str(i), c["name"], str(c["commits"]))
|
|
969
|
+
console.print(ct)
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
@cli.group("re", help="正则测试器、常用正则库")
|
|
973
|
+
def re_group():
|
|
974
|
+
"""regexlab — 正则实验室"""
|
|
975
|
+
pass
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
@re_group.command("test", help="测试正则表达式")
|
|
979
|
+
@click.argument("pattern")
|
|
980
|
+
@click.argument("text")
|
|
981
|
+
@click.option("--flags", "-f", help="正则标志: i(忽略大小写) m(多行) s(DOTALL)")
|
|
982
|
+
def re_test(pattern, text, flags):
|
|
983
|
+
"""测试正则表达式匹配结果"""
|
|
984
|
+
from rich.table import Table
|
|
985
|
+
|
|
986
|
+
from devmate_regexlab import match
|
|
987
|
+
|
|
988
|
+
result = match(pattern, text, flags=flags or "")
|
|
989
|
+
if not result.success:
|
|
990
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
991
|
+
if result.suggestion:
|
|
992
|
+
console.print(f"[yellow]💡 {result.suggestion}[/yellow]")
|
|
993
|
+
return
|
|
994
|
+
|
|
995
|
+
data = result.data
|
|
996
|
+
console.print(f"\n[bold]正则:[/bold] [yellow]{data['pattern']}[/yellow]")
|
|
997
|
+
console.print(f"[bold]目标文本:[/bold] [dim]{text[:200]}[/dim]")
|
|
998
|
+
console.print(f"[bold]匹配结果:[/bold] [cyan]{data['total']}[/cyan] 个匹配\n")
|
|
999
|
+
|
|
1000
|
+
if data["matches"]:
|
|
1001
|
+
table = Table(show_header=True)
|
|
1002
|
+
table.add_column("#", style="dim")
|
|
1003
|
+
table.add_column("位置", style="cyan")
|
|
1004
|
+
table.add_column("匹配内容", style="yellow")
|
|
1005
|
+
for i, m in enumerate(data["matches"], 1):
|
|
1006
|
+
table.add_row(str(i), f"{m['start']}-{m['end']}", m['matched'][:60])
|
|
1007
|
+
console.print(table)
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
@re_group.command("search", help="在文件中搜索正则")
|
|
1011
|
+
@click.argument("pattern")
|
|
1012
|
+
@click.argument("path", default=".")
|
|
1013
|
+
@click.option("--flags", "-f", help="正则标志")
|
|
1014
|
+
@click.option("--max", default=50, help="最大匹配数")
|
|
1015
|
+
def re_search(pattern, path, flags, max):
|
|
1016
|
+
"""在指定路径的文件中搜索正则模式"""
|
|
1017
|
+
from rich.table import Table
|
|
1018
|
+
|
|
1019
|
+
from devmate_regexlab import search
|
|
1020
|
+
|
|
1021
|
+
result = search(pattern, path, flags=flags or "", max_results=max)
|
|
1022
|
+
if not result.success:
|
|
1023
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1024
|
+
return
|
|
1025
|
+
|
|
1026
|
+
data = result.data
|
|
1027
|
+
console.print(f"\n[bold]在 {data['target']} 中搜索[/bold] [yellow]{data['pattern']}[/yellow]")
|
|
1028
|
+
msg = f"检查了 {data['files_checked']} 个文件,找到 [cyan]{data['total']}[/cyan] 处匹配"
|
|
1029
|
+
console.print(f"{msg}\n")
|
|
1030
|
+
|
|
1031
|
+
if data["matches"]:
|
|
1032
|
+
table = Table(show_header=True)
|
|
1033
|
+
table.add_column("文件", style="cyan")
|
|
1034
|
+
table.add_column("行号", style="yellow")
|
|
1035
|
+
table.add_column("内容", style="white")
|
|
1036
|
+
for m in data["matches"]:
|
|
1037
|
+
table.add_row(m["file"], str(m["line"]), m["content"])
|
|
1038
|
+
console.print(table)
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
@re_group.command("lib", help="管理正则库")
|
|
1042
|
+
@click.argument("action", type=click.Choice(["list", "add", "remove"]))
|
|
1043
|
+
@click.argument("name", required=False)
|
|
1044
|
+
@click.option("--pattern", "-p", help="正则表达式(add 时使用)")
|
|
1045
|
+
@click.option("--desc", "-d", help="描述")
|
|
1046
|
+
@click.option("--example", "-e", help="示例文本")
|
|
1047
|
+
def re_lib(action, name, pattern, desc, example):
|
|
1048
|
+
"""查看、添加、删除常用正则表达式"""
|
|
1049
|
+
from rich.table import Table
|
|
1050
|
+
|
|
1051
|
+
from devmate_regexlab import lib_add, lib_list, lib_remove
|
|
1052
|
+
|
|
1053
|
+
if action == "list":
|
|
1054
|
+
result = lib_list()
|
|
1055
|
+
if not result.success:
|
|
1056
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1057
|
+
return
|
|
1058
|
+
|
|
1059
|
+
builtin = result.data["builtin"]
|
|
1060
|
+
table = Table(title=f"内置正则(共 {len(builtin)} 个)")
|
|
1061
|
+
table.add_column("名称", style="cyan")
|
|
1062
|
+
table.add_column("正则", style="yellow")
|
|
1063
|
+
table.add_column("示例", style="dim")
|
|
1064
|
+
for item in builtin:
|
|
1065
|
+
table.add_row(item["name"], item["pattern"][:50], item.get("example", ""))
|
|
1066
|
+
console.print(table)
|
|
1067
|
+
|
|
1068
|
+
custom = result.data["custom"]
|
|
1069
|
+
if custom:
|
|
1070
|
+
ct = Table(title=f"自定义正则(共 {len(custom)} 个)")
|
|
1071
|
+
ct.add_column("名称", style="cyan")
|
|
1072
|
+
ct.add_column("正则", style="yellow")
|
|
1073
|
+
for item in custom:
|
|
1074
|
+
ct.add_row(item["name"], item["pattern"][:50])
|
|
1075
|
+
console.print(ct)
|
|
1076
|
+
|
|
1077
|
+
elif action == "add":
|
|
1078
|
+
if not name or not pattern:
|
|
1079
|
+
err = "请指定名称和正则。用法: devmate re lib add <名字> --pattern <正则>"
|
|
1080
|
+
console.print(f"[red]{err}[/red]")
|
|
1081
|
+
return
|
|
1082
|
+
result = lib_add(name, pattern, desc=desc or "", example=example or "")
|
|
1083
|
+
console.print(f"[green]✅ {result.data['message']}[/green]" if result.success
|
|
1084
|
+
else f"[red]❌ {result.error}[/red]")
|
|
1085
|
+
|
|
1086
|
+
elif action == "remove":
|
|
1087
|
+
if not name:
|
|
1088
|
+
console.print("[red]请指定名称。用法: devmate re lib remove <名字>[/red]")
|
|
1089
|
+
return
|
|
1090
|
+
result = lib_remove(name)
|
|
1091
|
+
console.print(f"[green]✅ {result.data['message']}[/green]" if result.success
|
|
1092
|
+
else f"[red]❌ {result.error}[/red]")
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
@cli.group("note", help="笔记知识库、全文搜索")
|
|
1096
|
+
def note_group():
|
|
1097
|
+
"""notekeeper — 笔记知识库"""
|
|
1098
|
+
pass
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
@note_group.command("new", help="创建笔记")
|
|
1102
|
+
@click.argument("title")
|
|
1103
|
+
@click.option("--content", "-c", help="笔记内容")
|
|
1104
|
+
@click.option("--tags", "-t", help="标签(逗号分隔)")
|
|
1105
|
+
def note_new(title, content, tags):
|
|
1106
|
+
"""创建一条新笔记"""
|
|
1107
|
+
from devmate_notekeeper import new as _new
|
|
1108
|
+
result = _new(title, content=content or "", tags=tags or "")
|
|
1109
|
+
if result.success:
|
|
1110
|
+
console.print(f"[green]{result.data['message']}[/green]")
|
|
1111
|
+
else:
|
|
1112
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
@note_group.command("list", help="列出笔记")
|
|
1116
|
+
@click.option("--limit", default=20, help="显示条数")
|
|
1117
|
+
def note_list(limit):
|
|
1118
|
+
"""列出最近笔记"""
|
|
1119
|
+
from rich.table import Table
|
|
1120
|
+
|
|
1121
|
+
from devmate_notekeeper import list_notes
|
|
1122
|
+
|
|
1123
|
+
result = list_notes(limit=limit)
|
|
1124
|
+
if not result.success:
|
|
1125
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1126
|
+
return
|
|
1127
|
+
|
|
1128
|
+
notes = result.data["notes"]
|
|
1129
|
+
if not notes:
|
|
1130
|
+
console.print("[yellow]暂无笔记[/yellow]")
|
|
1131
|
+
return
|
|
1132
|
+
|
|
1133
|
+
table = Table(title=f"笔记列表(共 {result.data['total']} 条)")
|
|
1134
|
+
table.add_column("ID", style="dim", width=4)
|
|
1135
|
+
table.add_column("标题", style="cyan")
|
|
1136
|
+
table.add_column("标签", style="yellow")
|
|
1137
|
+
table.add_column("更新时间", style="dim", width=16)
|
|
1138
|
+
for n in notes:
|
|
1139
|
+
tags_str = ", ".join(n.get("tags", []))
|
|
1140
|
+
table.add_row(str(n["id"]), n["title"][:40], tags_str, n.get("updated_at", ""))
|
|
1141
|
+
console.print(table)
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
@note_group.command("show", help="查看笔记")
|
|
1145
|
+
@click.argument("note_id", type=int)
|
|
1146
|
+
def note_show(note_id):
|
|
1147
|
+
"""查看单条笔记的完整内容"""
|
|
1148
|
+
|
|
1149
|
+
from devmate_notekeeper import show as _show
|
|
1150
|
+
|
|
1151
|
+
result = _show(note_id)
|
|
1152
|
+
if not result.success:
|
|
1153
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1154
|
+
return
|
|
1155
|
+
|
|
1156
|
+
n = result.data
|
|
1157
|
+
console.print(f"\n[bold cyan]# {n['title']}[/bold cyan]")
|
|
1158
|
+
if n.get("tags"):
|
|
1159
|
+
console.print(f"[dim]标签: {', '.join(n['tags'])}[/dim]")
|
|
1160
|
+
console.print(f"[dim]创建: {n.get('created_at', '')} | 更新: {n.get('updated_at', '')}[/dim]\n")
|
|
1161
|
+
console.print(n.get("content", "(无内容)"))
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
@note_group.command("search", help="全文搜索笔记")
|
|
1165
|
+
@click.argument("query")
|
|
1166
|
+
@click.option("--limit", default=20, help="显示条数")
|
|
1167
|
+
def note_search(query, limit):
|
|
1168
|
+
"""全文搜索笔记内容"""
|
|
1169
|
+
from rich.table import Table
|
|
1170
|
+
|
|
1171
|
+
from devmate_notekeeper import search as _search
|
|
1172
|
+
|
|
1173
|
+
result = _search(query, limit=limit)
|
|
1174
|
+
if not result.success:
|
|
1175
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1176
|
+
return
|
|
1177
|
+
|
|
1178
|
+
notes = result.data["notes"]
|
|
1179
|
+
if not notes:
|
|
1180
|
+
console.print(f"[yellow]未找到匹配 \"{query}\" 的笔记[/yellow]")
|
|
1181
|
+
return
|
|
1182
|
+
|
|
1183
|
+
table = Table(title=f"搜索结果: \"{query}\"({result.data['total']} 条)")
|
|
1184
|
+
table.add_column("ID", style="dim", width=4)
|
|
1185
|
+
table.add_column("标题", style="cyan")
|
|
1186
|
+
table.add_column("内容预览", style="white", width=60)
|
|
1187
|
+
for n in notes:
|
|
1188
|
+
table.add_row(str(n["id"]), n["title"], n.get("content", ""))
|
|
1189
|
+
console.print(table)
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
@note_group.command("edit", help="编辑笔记")
|
|
1193
|
+
@click.argument("note_id", type=int)
|
|
1194
|
+
@click.option("--title", help="新标题")
|
|
1195
|
+
@click.option("--content", "-c", help="新内容")
|
|
1196
|
+
@click.option("--tags", "-t", help="新标签(逗号分隔)")
|
|
1197
|
+
def note_edit(note_id, title, content, tags):
|
|
1198
|
+
"""编辑笔记内容"""
|
|
1199
|
+
from devmate_notekeeper import edit as _edit
|
|
1200
|
+
result = _edit(note_id, title=title or "", content=content or "", tags=tags or "")
|
|
1201
|
+
console.print(f"[green]✅ {result.data['message']}[/green]" if result.success
|
|
1202
|
+
else f"[red]❌ {result.error}[/red]")
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
@note_group.command("delete", help="删除笔记")
|
|
1206
|
+
@click.argument("note_id", type=int)
|
|
1207
|
+
@click.confirmation_option(prompt="确认删除此笔记?")
|
|
1208
|
+
def note_delete(note_id):
|
|
1209
|
+
"""删除笔记"""
|
|
1210
|
+
from devmate_notekeeper import remove as _remove
|
|
1211
|
+
result = _remove(note_id)
|
|
1212
|
+
console.print(f"[green]{result.data['message']}[/green]" if result.success
|
|
1213
|
+
else f"[red]❌ {result.error}[/red]")
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
@note_group.command("tag", help="管理标签")
|
|
1217
|
+
@click.argument("action", type=click.Choice(["list"]))
|
|
1218
|
+
def note_tag(action):
|
|
1219
|
+
"""查看所有标签"""
|
|
1220
|
+
from rich.table import Table
|
|
1221
|
+
|
|
1222
|
+
from devmate_notekeeper import tag_list
|
|
1223
|
+
|
|
1224
|
+
result = tag_list()
|
|
1225
|
+
if not result.success:
|
|
1226
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1227
|
+
return
|
|
1228
|
+
|
|
1229
|
+
tags = result.data["tags"]
|
|
1230
|
+
if not tags:
|
|
1231
|
+
console.print("[yellow]暂无标签[/yellow]")
|
|
1232
|
+
return
|
|
1233
|
+
|
|
1234
|
+
table = Table(title="标签列表")
|
|
1235
|
+
table.add_column("标签", style="cyan")
|
|
1236
|
+
table.add_column("笔记数", style="yellow", justify="right")
|
|
1237
|
+
for t in tags:
|
|
1238
|
+
table.add_row(t["name"], str(t["count"]))
|
|
1239
|
+
console.print(table)
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
@note_group.command("export", help="导出笔记为 Markdown")
|
|
1243
|
+
@click.argument("note_id", type=int)
|
|
1244
|
+
@click.option("--output", "-o", help="输出文件路径")
|
|
1245
|
+
def note_export(note_id, output):
|
|
1246
|
+
"""导出笔记为 Markdown 文件"""
|
|
1247
|
+
from devmate_notekeeper import export_note
|
|
1248
|
+
result = export_note(note_id, output=output or "")
|
|
1249
|
+
if result.success:
|
|
1250
|
+
if "markdown" in result.data:
|
|
1251
|
+
console.print(result.data["markdown"])
|
|
1252
|
+
else:
|
|
1253
|
+
console.print(f"[green]{result.data['message']}[/green]")
|
|
1254
|
+
else:
|
|
1255
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
@note_group.command("import", help="从 Markdown 导入笔记")
|
|
1259
|
+
@click.argument("path")
|
|
1260
|
+
def note_import(path):
|
|
1261
|
+
"""从 Markdown 文件导入笔记"""
|
|
1262
|
+
from devmate_notekeeper import import_markdown
|
|
1263
|
+
result = import_markdown(path)
|
|
1264
|
+
if result.success:
|
|
1265
|
+
console.print(f"[green]{result.data['message']}[/green]")
|
|
1266
|
+
else:
|
|
1267
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
@cli.group("top", help="系统资源监控")
|
|
1271
|
+
def top_group():
|
|
1272
|
+
"""monitor — 系统监控"""
|
|
1273
|
+
pass
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
@top_group.command("dashboard", help="系统资源概览")
|
|
1277
|
+
def top_dashboard():
|
|
1278
|
+
"""查看 CPU/内存/磁盘/进程概览"""
|
|
1279
|
+
from rich.table import Table
|
|
1280
|
+
|
|
1281
|
+
from devmate_monitor import dashboard
|
|
1282
|
+
|
|
1283
|
+
result = dashboard()
|
|
1284
|
+
if not result.success:
|
|
1285
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1286
|
+
return
|
|
1287
|
+
|
|
1288
|
+
data = result.data
|
|
1289
|
+
sys_info = data.get("system", {})
|
|
1290
|
+
mem = data.get("memory", {})
|
|
1291
|
+
|
|
1292
|
+
console.print(f"\n[bold cyan]📊 系统状态: {sys_info.get('hostname', '')}[/bold cyan]\n")
|
|
1293
|
+
|
|
1294
|
+
table = Table(show_header=False, box=None)
|
|
1295
|
+
table.add_column("指标", style="cyan", width=16)
|
|
1296
|
+
table.add_column("值", style="white")
|
|
1297
|
+
table.add_row("CPU 核数", str(sys_info.get("cpu_count", "?")))
|
|
1298
|
+
mem_str = f"{mem.get('used', '?')} / {mem.get('total', '?')} ({mem.get('percent', 0)}%)"
|
|
1299
|
+
table.add_row("内存", mem_str)
|
|
1300
|
+
table.add_row("进程数", str(data.get("processes", "?")))
|
|
1301
|
+
console.print(table)
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
@top_group.command("process", help="进程列表")
|
|
1305
|
+
@click.option("--sort", type=click.Choice(["cpu", "memory"]), default="cpu")
|
|
1306
|
+
@click.option("--limit", default=20)
|
|
1307
|
+
def top_process(sort, limit):
|
|
1308
|
+
"""列出按 CPU/内存排序的进程"""
|
|
1309
|
+
from rich.table import Table
|
|
1310
|
+
|
|
1311
|
+
from devmate_monitor import process_list
|
|
1312
|
+
|
|
1313
|
+
result = process_list(sort_by=sort, limit=limit)
|
|
1314
|
+
if not result.success:
|
|
1315
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1316
|
+
return
|
|
1317
|
+
|
|
1318
|
+
processes = result.data["processes"]
|
|
1319
|
+
table = Table(title=f"进程列表(按 {sort} 排序)")
|
|
1320
|
+
table.add_column("PID", style="dim", width=6)
|
|
1321
|
+
table.add_column("名称", style="cyan")
|
|
1322
|
+
table.add_column("CPU%", style="yellow", justify="right" if sort == "cpu" else "right")
|
|
1323
|
+
table.add_column("内存%", style="green", justify="right")
|
|
1324
|
+
table.add_column("状态", style="dim")
|
|
1325
|
+
|
|
1326
|
+
for p in processes:
|
|
1327
|
+
table.add_row(str(p["pid"]), p["name"][:30],
|
|
1328
|
+
f"{p['cpu']:.1f}", f"{p['memory']:.1f}", p["status"])
|
|
1329
|
+
console.print(table)
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
@top_group.command("log", help="查看日志")
|
|
1333
|
+
@click.argument("path")
|
|
1334
|
+
@click.option("--lines", default=50)
|
|
1335
|
+
def top_log(path, lines):
|
|
1336
|
+
"""查看文件末尾 N 行日志"""
|
|
1337
|
+
from devmate_monitor import log_tail
|
|
1338
|
+
|
|
1339
|
+
result = log_tail(path, lines=lines)
|
|
1340
|
+
if not result.success:
|
|
1341
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1342
|
+
return
|
|
1343
|
+
|
|
1344
|
+
data = result.data
|
|
1345
|
+
console.print(f"[dim]📄 {data['path']} ({data['size']/1024:.1f}KB)[/dim]\n")
|
|
1346
|
+
console.print(data["content"])
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
@cli.group("report", help="日报/周报自动生成")
|
|
1350
|
+
def report_group():
|
|
1351
|
+
"""reporter — 报告生成"""
|
|
1352
|
+
pass
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
@report_group.command("generate", help="生成报告")
|
|
1356
|
+
@click.option("--period", type=click.Choice(["daily", "weekly"]), default="daily")
|
|
1357
|
+
@click.option("--template", type=click.Choice(["minimal", "standup", "detailed"]), default="standup") # noqa: E501
|
|
1358
|
+
@click.option("--title", help="报告标题")
|
|
1359
|
+
@click.option("--path", default=".", help="Git 仓库路径")
|
|
1360
|
+
def report_generate(period, template, title, path):
|
|
1361
|
+
"""自动生成日报或周报"""
|
|
1362
|
+
from devmate_reporter import generate
|
|
1363
|
+
|
|
1364
|
+
result = generate(period=period, template=template, path=path, title=title or "")
|
|
1365
|
+
if result.success:
|
|
1366
|
+
console.print(f"[green]✅ 报告已生成: {result.data['path']}[/green]")
|
|
1367
|
+
console.print(f"\n{result.data['markdown']}")
|
|
1368
|
+
else:
|
|
1369
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
@report_group.command("export", help="导出报告为 HTML")
|
|
1373
|
+
@click.argument("path")
|
|
1374
|
+
def report_export(path):
|
|
1375
|
+
"""将 Markdown 报告导出为 HTML"""
|
|
1376
|
+
from devmate_reporter import export_html
|
|
1377
|
+
|
|
1378
|
+
result = export_html(path)
|
|
1379
|
+
if result.success:
|
|
1380
|
+
console.print(f"[green]✅ HTML 已导出: {result.data['path']}[/green]")
|
|
1381
|
+
else:
|
|
1382
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
@cli.group("scaf", help="项目脚手架、代码片段")
|
|
1386
|
+
def scaf_group():
|
|
1387
|
+
"""scaffold — 项目脚手架"""
|
|
1388
|
+
pass
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
@scaf_group.command("new", help="从模板创建新项目")
|
|
1392
|
+
@click.argument("template")
|
|
1393
|
+
@click.argument("name")
|
|
1394
|
+
@click.option("--output", "-o", help="输出目录")
|
|
1395
|
+
def scaf_new(template, name, output):
|
|
1396
|
+
"""基于内置模板创建新项目"""
|
|
1397
|
+
from devmate_scaffold import new_project
|
|
1398
|
+
|
|
1399
|
+
result = new_project(template, name, output=output or "")
|
|
1400
|
+
if result.success:
|
|
1401
|
+
data = result.data
|
|
1402
|
+
console.print(f"[green]{data['message']}[/green]")
|
|
1403
|
+
console.print(f"\n[dim]创建了 {len(data['files'])} 个文件:[/dim]")
|
|
1404
|
+
for f in data["files"]:
|
|
1405
|
+
console.print(f" [cyan]{f}[/cyan]")
|
|
1406
|
+
else:
|
|
1407
|
+
console.print(f"[red]❌ {result.error}[/red]")
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
@scaf_group.command("templates", help="列出可用模板")
|
|
1411
|
+
def scaf_templates():
|
|
1412
|
+
"""列出所有可用的项目模板"""
|
|
1413
|
+
from rich.table import Table
|
|
1414
|
+
|
|
1415
|
+
from devmate_scaffold import _list_templates
|
|
1416
|
+
|
|
1417
|
+
templates = _list_templates()
|
|
1418
|
+
if not templates:
|
|
1419
|
+
console.print("[yellow]暂无模板[/yellow]")
|
|
1420
|
+
return
|
|
1421
|
+
|
|
1422
|
+
table = Table(title="可用模板")
|
|
1423
|
+
table.add_column("模板名", style="cyan")
|
|
1424
|
+
table.add_column("说明", style="white")
|
|
1425
|
+
for t in templates:
|
|
1426
|
+
table.add_row(t["name"], t.get("description", ""))
|
|
1427
|
+
console.print(table)
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
@scaf_group.command("snippet", help="管理代码片段")
|
|
1431
|
+
@click.argument("action", type=click.Choice(["list", "add", "remove"]))
|
|
1432
|
+
@click.argument("name", required=False)
|
|
1433
|
+
@click.option("--code", help="代码内容(add 时)")
|
|
1434
|
+
@click.option("--lang", help="编程语言")
|
|
1435
|
+
def scaf_snippet(action, name, code, lang):
|
|
1436
|
+
"""管理常用代码片段"""
|
|
1437
|
+
from rich.table import Table
|
|
1438
|
+
|
|
1439
|
+
from devmate_scaffold import snippet_add, snippet_list, snippet_remove
|
|
1440
|
+
|
|
1441
|
+
if action == "list":
|
|
1442
|
+
result = snippet_list()
|
|
1443
|
+
snippets = result.data["snippets"]
|
|
1444
|
+
if not snippets:
|
|
1445
|
+
console.print("[yellow]暂无代码片段[/yellow]")
|
|
1446
|
+
return
|
|
1447
|
+
table = Table(title="代码片段")
|
|
1448
|
+
table.add_column("名称", style="cyan")
|
|
1449
|
+
table.add_column("语言", style="yellow")
|
|
1450
|
+
table.add_column("预览", style="white")
|
|
1451
|
+
for s in snippets:
|
|
1452
|
+
code_preview = s.get("code", "")[:60]
|
|
1453
|
+
table.add_row(s["name"], s.get("lang", ""), code_preview)
|
|
1454
|
+
console.print(table)
|
|
1455
|
+
|
|
1456
|
+
elif action == "add":
|
|
1457
|
+
if not name or not code:
|
|
1458
|
+
console.print("[red]请指定名称和代码内容[/red]")
|
|
1459
|
+
return
|
|
1460
|
+
result = snippet_add(name, code, lang=lang or "")
|
|
1461
|
+
console.print(f"[green]✅ {result.data['message']}[/green]")
|
|
1462
|
+
|
|
1463
|
+
elif action == "remove":
|
|
1464
|
+
if not name:
|
|
1465
|
+
console.print("[red]请指定要删除的片段名称[/red]")
|
|
1466
|
+
return
|
|
1467
|
+
result = snippet_remove(name)
|
|
1468
|
+
console.print(f"[green]✅ {result.data['message']}[/green]" if result.success
|
|
1469
|
+
else f"[red]❌ {result.error}[/red]")
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
@cli.group("pub", help="文章多平台发布")
|
|
1473
|
+
def pub_group():
|
|
1474
|
+
"""publisher — 文章发布"""
|
|
1475
|
+
pass
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
@cli.group("vault", help="密钥加密存储")
|
|
1479
|
+
def vault_group():
|
|
1480
|
+
"""vault — 密钥管理器"""
|
|
1481
|
+
pass
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
@vault_group.command("set", help="存储密钥(交互式输入密码和值)")
|
|
1485
|
+
@click.argument("name")
|
|
1486
|
+
def vault_set(name):
|
|
1487
|
+
"""存储一个加密密钥(交互式输入主密码和密钥值)"""
|
|
1488
|
+
from devmate.security.vault import vault as _vault
|
|
1489
|
+
password = click.prompt("请输入主密码", hide_input=True)
|
|
1490
|
+
value = click.prompt("请输入密钥值", hide_input=True)
|
|
1491
|
+
try:
|
|
1492
|
+
_vault.set(name, value, password)
|
|
1493
|
+
console.print(f"[green]✅ 密钥已存储: {name}[/green]")
|
|
1494
|
+
except Exception as e:
|
|
1495
|
+
console.print(f"[red]❌ 存储失败: {e}[/red]")
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
@vault_group.command("get", help="读取密钥")
|
|
1499
|
+
@click.argument("name")
|
|
1500
|
+
def vault_get(name):
|
|
1501
|
+
"""读取并显示解密后的密钥"""
|
|
1502
|
+
from devmate.security.vault import KeyNotFoundError
|
|
1503
|
+
from devmate.security.vault import vault as _vault
|
|
1504
|
+
password = click.prompt("请输入主密码", hide_input=True)
|
|
1505
|
+
try:
|
|
1506
|
+
value = _vault.get(name, password)
|
|
1507
|
+
console.print(f"[green]✅ {name}: {sanitize(value, name)}[/green]")
|
|
1508
|
+
console.print(" 原始值已复制到剪贴板(仅显示脱敏值)")
|
|
1509
|
+
except KeyNotFoundError:
|
|
1510
|
+
console.print(f"[red]❌ 密钥不存在: {name}[/red]")
|
|
1511
|
+
except Exception as e:
|
|
1512
|
+
console.print(f"[red]❌ 读取失败: {e}[/red]")
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
@vault_group.command("list", help="列出所有密钥")
|
|
1516
|
+
def vault_list():
|
|
1517
|
+
"""列出所有已存储的密钥名称"""
|
|
1518
|
+
from devmate.security.vault import vault as _vault
|
|
1519
|
+
keys = _vault.list()
|
|
1520
|
+
if not keys:
|
|
1521
|
+
console.print("[yellow]vault 中没有密钥[/yellow]")
|
|
1522
|
+
return
|
|
1523
|
+
table = Table(title=f"vault 密钥列表(共 {len(keys)} 个)")
|
|
1524
|
+
table.add_column("密钥名称", style="cyan")
|
|
1525
|
+
for k in keys:
|
|
1526
|
+
table.add_row(k)
|
|
1527
|
+
console.print(table)
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
@vault_group.command("delete", help="删除密钥")
|
|
1531
|
+
@click.argument("name")
|
|
1532
|
+
@click.confirmation_option(prompt="⚠️ 确认删除此密钥?")
|
|
1533
|
+
def vault_delete(name):
|
|
1534
|
+
"""删除指定密钥"""
|
|
1535
|
+
from devmate.security.vault import KeyNotFoundError
|
|
1536
|
+
from devmate.security.vault import vault as _vault
|
|
1537
|
+
password = click.prompt("请输入主密码", hide_input=True)
|
|
1538
|
+
try:
|
|
1539
|
+
_vault.delete(name, password)
|
|
1540
|
+
console.print(f"[green]✅ 密钥已删除: {name}[/green]")
|
|
1541
|
+
except KeyNotFoundError:
|
|
1542
|
+
console.print(f"[red]❌ 密钥不存在: {name}[/red]")
|
|
1543
|
+
except Exception as e:
|
|
1544
|
+
console.print(f"[red]❌ 删除失败: {e}[/red]")
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
@vault_group.command("lock", help="锁定 vault(清除内存中的密钥)")
|
|
1548
|
+
def vault_lock():
|
|
1549
|
+
"""锁定 vault"""
|
|
1550
|
+
from devmate.security.vault import vault as _vault
|
|
1551
|
+
_vault.lock()
|
|
1552
|
+
console.print("[green]✅ vault 已锁定[/green]")
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
@vault_group.command("unlock", help="解锁 vault")
|
|
1556
|
+
def vault_unlock():
|
|
1557
|
+
"""解锁 vault"""
|
|
1558
|
+
from devmate.security.vault import VaultUnlockedError
|
|
1559
|
+
from devmate.security.vault import vault as _vault
|
|
1560
|
+
try:
|
|
1561
|
+
password = click.prompt("请输入主密码", hide_input=True)
|
|
1562
|
+
_vault.unlock(password)
|
|
1563
|
+
console.print("[green]✅ vault 已解锁[/green]")
|
|
1564
|
+
except VaultUnlockedError:
|
|
1565
|
+
console.print("[yellow]vault 已经解锁[/yellow]")
|
|
1566
|
+
except Exception as e:
|
|
1567
|
+
console.print(f"[red]❌ 解锁失败: {e}[/red]")
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
@cli.group("plugin", help="插件管理")
|
|
1571
|
+
def plugin_group():
|
|
1572
|
+
"""插件管理"""
|
|
1573
|
+
pass
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
@cli.group("audit", help="审计日志")
|
|
1577
|
+
def audit_group():
|
|
1578
|
+
"""审计日志"""
|
|
1579
|
+
pass
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
@audit_group.command("log", help="查看最近审计记录")
|
|
1583
|
+
@click.option("--level", type=click.Choice(["SAFE", "SENSITIVE", "HIGH"]), help="按级别过滤")
|
|
1584
|
+
@click.option("--module", "module_filter", help="按模块过滤")
|
|
1585
|
+
@click.option("--limit", default=20, help="显示条数")
|
|
1586
|
+
def audit_log(level, module_filter, limit):
|
|
1587
|
+
"""查看最近审计记录"""
|
|
1588
|
+
from rich.table import Table
|
|
1589
|
+
|
|
1590
|
+
from devmate.security.audit import audit as _audit
|
|
1591
|
+
|
|
1592
|
+
entries = _audit.query(level=level, module=module_filter, limit=limit)
|
|
1593
|
+
if not entries:
|
|
1594
|
+
console.print("[yellow]暂无审计记录[/yellow]")
|
|
1595
|
+
return
|
|
1596
|
+
|
|
1597
|
+
table = Table(title=f"审计日志(最近 {len(entries)} 条)")
|
|
1598
|
+
table.add_column("时间", style="dim")
|
|
1599
|
+
table.add_column("级别", style="bold")
|
|
1600
|
+
table.add_column("模块", style="cyan")
|
|
1601
|
+
table.add_column("操作", style="yellow")
|
|
1602
|
+
table.add_column("结果")
|
|
1603
|
+
|
|
1604
|
+
for e in entries:
|
|
1605
|
+
level_style = {"SAFE": "green", "SENSITIVE": "yellow", "HIGH": "red"}
|
|
1606
|
+
table.add_row(
|
|
1607
|
+
e.timestamp.split(".")[0] if "T" in e.timestamp else e.timestamp[:19],
|
|
1608
|
+
f"[{level_style.get(e.level, 'white')}]{e.level}[/]",
|
|
1609
|
+
e.module,
|
|
1610
|
+
e.action,
|
|
1611
|
+
"✅" if e.result == "success" else "❌",
|
|
1612
|
+
)
|
|
1613
|
+
console.print(table)
|
|
1614
|
+
|
|
1615
|
+
|
|
1616
|
+
@audit_group.command("count", help="审计记录总数")
|
|
1617
|
+
def audit_count():
|
|
1618
|
+
"""审计记录总数"""
|
|
1619
|
+
from devmate.security.audit import audit as _audit
|
|
1620
|
+
console.print(f"审计记录总数:[cyan]{_audit.count}[/cyan] 条")
|
|
1621
|
+
|
|
1622
|
+
|
|
1623
|
+
# ═══════════════════════════════════════════════════
|
|
1624
|
+
# 顶层命令:web / upgrade
|
|
1625
|
+
# ═══════════════════════════════════════════════════
|
|
1626
|
+
|
|
1627
|
+
@cli.command("web", help="启动 Web 面板")
|
|
1628
|
+
@click.option("--host", default="127.0.0.1", help="监听地址")
|
|
1629
|
+
@click.option("--port", default=8600, help="监听端口")
|
|
1630
|
+
@click.option("--auth", type=click.Choice(["local", "oauth"]), default="local", help="认证方式")
|
|
1631
|
+
def web_cmd(host, port, auth):
|
|
1632
|
+
"""启动 Web 面板——FastAPI + Jinja2 + HTMX"""
|
|
1633
|
+
try:
|
|
1634
|
+
from devmate.app.web import run_web
|
|
1635
|
+
console.print(f"[green]🌐 DevMate Web 面板: http://{host}:{port}[/green]")
|
|
1636
|
+
console.print("[dim]按 Ctrl+C 停止[/dim]")
|
|
1637
|
+
run_web(host=host, port=port)
|
|
1638
|
+
except ImportError:
|
|
1639
|
+
console.print("[red]❌ 请安装 Web 依赖: pip install 'devmate[web]'[/red]")
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
@cli.command("upgrade", help="检查并升级 DevMate")
|
|
1643
|
+
def upgrade_cmd():
|
|
1644
|
+
"""检查更新"""
|
|
1645
|
+
console.print("[yellow]自动升级尚未实现[/yellow]")
|
|
1646
|
+
|
|
1647
|
+
|
|
1648
|
+
# ═══════════════════════════════════════════════════
|
|
1649
|
+
# 入口函数
|
|
1650
|
+
# ═══════════════════════════════════════════════════
|
|
1651
|
+
|
|
1652
|
+
def main():
|
|
1653
|
+
"""CLI 主入口"""
|
|
1654
|
+
# 发现已安装模块
|
|
1655
|
+
registry.discover()
|
|
1656
|
+
logger.debug(f"发现 {len(registry.module_names)} 个模块")
|
|
1657
|
+
|
|
1658
|
+
# 启动 CLI
|
|
1659
|
+
cli()
|
|
1660
|
+
|
|
1661
|
+
|
|
1662
|
+
if __name__ == "__main__":
|
|
1663
|
+
main()
|