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/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()