devmate 2.1.0__tar.gz → 2.3.0__tar.gz

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.
Files changed (53) hide show
  1. {devmate-2.1.0 → devmate-2.3.0}/PKG-INFO +1 -1
  2. {devmate-2.1.0 → devmate-2.3.0}/pyproject.toml +1 -1
  3. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/__init__.py +1 -1
  4. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/app/cli.py +458 -0
  5. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/app/web/__init__.py +8 -2
  6. devmate-2.3.0/src/devmate/app/web/cli_docs.py +217 -0
  7. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/security/audit.py +29 -0
  8. {devmate-2.1.0 → devmate-2.3.0}/src/devmate.egg-info/PKG-INFO +1 -1
  9. {devmate-2.1.0 → devmate-2.3.0}/src/devmate.egg-info/SOURCES.txt +1 -0
  10. {devmate-2.1.0 → devmate-2.3.0}/src/devmate_apidev/__init__.py +151 -0
  11. {devmate-2.1.0 → devmate-2.3.0}/src/devmate_dbadmin/__init__.py +133 -0
  12. {devmate-2.1.0 → devmate-2.3.0}/src/devmate_gitflow/__init__.py +165 -0
  13. {devmate-2.1.0 → devmate-2.3.0}/src/devmate_monitor/__init__.py +87 -3
  14. {devmate-2.1.0 → devmate-2.3.0}/src/devmate_regexlab/__init__.py +105 -0
  15. {devmate-2.1.0 → devmate-2.3.0}/src/devmate_toolkit/__init__.py +191 -0
  16. {devmate-2.1.0 → devmate-2.3.0}/README.md +0 -0
  17. {devmate-2.1.0 → devmate-2.3.0}/setup.cfg +0 -0
  18. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/__main__.py +0 -0
  19. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/ai_config/__init__.py +0 -0
  20. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/ai_scenarios/__init__.py +0 -0
  21. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/app/__init__.py +0 -0
  22. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/app/tui.py +0 -0
  23. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/bus/__init__.py +0 -0
  24. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/bus/bus.py +0 -0
  25. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/bus/command.py +0 -0
  26. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/core/__init__.py +0 -0
  27. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/core/config.py +0 -0
  28. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/core/logging.py +0 -0
  29. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/core/security.py +0 -0
  30. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/events/__init__.py +0 -0
  31. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/events/bus.py +0 -0
  32. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/events/domain_event.py +0 -0
  33. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/mcp/__init__.py +0 -0
  34. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/module/__init__.py +0 -0
  35. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/module/registry.py +0 -0
  36. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/plugin/__init__.py +0 -0
  37. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/plugin_market/__init__.py +0 -0
  38. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/plugin_market/ratings.py +0 -0
  39. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/plugin_market/remote.py +0 -0
  40. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/rag/__init__.py +0 -0
  41. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/security/vault.py +0 -0
  42. {devmate-2.1.0 → devmate-2.3.0}/src/devmate/workflow/__init__.py +0 -0
  43. {devmate-2.1.0 → devmate-2.3.0}/src/devmate.egg-info/dependency_links.txt +0 -0
  44. {devmate-2.1.0 → devmate-2.3.0}/src/devmate.egg-info/entry_points.txt +0 -0
  45. {devmate-2.1.0 → devmate-2.3.0}/src/devmate.egg-info/requires.txt +0 -0
  46. {devmate-2.1.0 → devmate-2.3.0}/src/devmate.egg-info/top_level.txt +0 -0
  47. {devmate-2.1.0 → devmate-2.3.0}/src/devmate_agent/__init__.py +0 -0
  48. {devmate-2.1.0 → devmate-2.3.0}/src/devmate_apihub/__init__.py +0 -0
  49. {devmate-2.1.0 → devmate-2.3.0}/src/devmate_notekeeper/__init__.py +0 -0
  50. {devmate-2.1.0 → devmate-2.3.0}/src/devmate_publisher/__init__.py +0 -0
  51. {devmate-2.1.0 → devmate-2.3.0}/src/devmate_reporter/__init__.py +0 -0
  52. {devmate-2.1.0 → devmate-2.3.0}/src/devmate_scaffold/__init__.py +0 -0
  53. {devmate-2.1.0 → devmate-2.3.0}/src/devmate_sshman/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devmate
3
- Version: 2.1.0
3
+ Version: 2.3.0
4
4
  Summary: 程序员终端瑞士军刀 + AI 原生平台——替代 5 个独立工具,一个 CLI 搞定
5
5
  Author-email: DevMate Team <devmate@example.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devmate"
7
- version = "2.1.0"
7
+ version = "2.3.0"
8
8
  description = "程序员终端瑞士军刀 + AI 原生平台——替代 5 个独立工具,一个 CLI 搞定"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -1,6 +1,6 @@
1
1
  """
2
2
  DevMate — 程序员终端瑞士军刀
3
3
  """
4
- __version__ = "2.1.0"
4
+ __version__ = "2.3.0"
5
5
  __app_name__ = "DevMate"
6
6
  __description__ = "程序员终端瑞士军刀——替代 5 个独立工具,一个 CLI 搞定"
@@ -221,6 +221,102 @@ def tk_cleanup(path, dry_run):
221
221
  console.print(table)
222
222
 
223
223
 
224
+ @tk_group.command("rename", help="批量重命名文件")
225
+ @click.argument("path", default=".")
226
+ @click.option("--pattern", "-p", help="正则匹配模式")
227
+ @click.option("--replacement", "-r", default="", help="替换为")
228
+ @click.option("--prefix", help="统一前缀")
229
+ @click.option("--suffix", help="统一后缀(插入扩展名前)")
230
+ @click.option("--apply", is_flag=True, help="实际执行(默认预览)")
231
+ def tk_rename(path, pattern, replacement, prefix, suffix, apply):
232
+ """批量重命名文件(支持正则替换/前缀/后缀),默认预览"""
233
+ from rich.table import Table
234
+
235
+ from devmate_toolkit import rename as _rename
236
+
237
+ result = _rename(path=path, pattern=pattern or "", replacement=replacement,
238
+ prefix=prefix or "", suffix=suffix or "", dry_run=not apply)
239
+ if not result.success:
240
+ console.print(f"[red]❌ {result.error}[/red]")
241
+ return
242
+
243
+ data = result.data
244
+ console.print(f"\n[bold cyan]{data['message']}[/bold cyan]\n")
245
+
246
+ if data["results"]:
247
+ table = Table(show_header=True)
248
+ table.add_column("原文件名", style="dim")
249
+ table.add_column("新文件名", style="cyan")
250
+ table.add_column("状态", style="yellow")
251
+ for r in data["results"]:
252
+ table.add_row(r["old"], r["new"], r["status"])
253
+ console.print(table)
254
+ console.print("[dim]提示: 预览确认后加 --apply 实际执行[/dim]")
255
+
256
+
257
+ @tk_group.command("dup", help="查找重复文件")
258
+ @click.argument("path", default=".")
259
+ @click.option("--min-size", default=1, type=int, help="最小文件大小(字节)")
260
+ def tk_dup(path, min_size):
261
+ """扫描目录下重复文件(按大小分组后 MD5 校验)"""
262
+
263
+ from devmate_toolkit import find_duplicates
264
+
265
+ result = find_duplicates(path, min_size=min_size)
266
+ if not result.success:
267
+ console.print(f"[red]❌ {result.error}[/red]")
268
+ return
269
+
270
+ data = result.data
271
+ console.print(f"\n[bold cyan]{data['message']}[/bold cyan]\n")
272
+
273
+ for i, dup in enumerate(data["duplicates"], 1):
274
+ console.print(f"[yellow]组 {i}[/yellow]({dup['size']},hash {dup['hash']})")
275
+ for f in dup["files"]:
276
+ console.print(f" [dim]{f}[/dim]")
277
+
278
+
279
+ @tk_group.command("json", help="JSON 格式化/校验/压缩")
280
+ @click.option("--path", type=click.Path(exists=False), help="JSON 文件路径")
281
+ @click.option("--text", "-t", help="JSON 字符串")
282
+ @click.option("--indent", default=2, type=int, help="缩进空格数")
283
+ @click.option("--compress", is_flag=True, help="压缩模式(去除空白)")
284
+ @click.option("--check", is_flag=True, help="仅校验,不输出格式化结果")
285
+ def tk_json(path, text, indent, compress, check):
286
+ """格式化/校验/压缩 JSON"""
287
+ from devmate_toolkit import json_format
288
+
289
+ result = json_format(path=path or "", text=text or "", indent=indent,
290
+ compress=compress)
291
+ if not result.success:
292
+ console.print(f"[red]❌ {result.error}[/red]")
293
+ return
294
+
295
+ data = result.data
296
+ if check:
297
+ console.print(f"[green]✅ {data['message']}[/green]")
298
+ return
299
+ console.print(data["output"])
300
+
301
+
302
+ @tk_group.command("hash", help="计算文件哈希")
303
+ @click.argument("path")
304
+ @click.option("--algorithm", "-a", default="md5",
305
+ type=click.Choice(["md5", "sha1", "sha256", "sha512"]))
306
+ def tk_hash(path, algorithm):
307
+ """计算文件哈希(md5/sha1/sha256/sha512)"""
308
+ from devmate_toolkit import hash_file
309
+
310
+ result = hash_file(path, algorithm=algorithm)
311
+ if not result.success:
312
+ console.print(f"[red]❌ {result.error}[/red]")
313
+ return
314
+ data = result.data
315
+ console.print(f"[bold]文件:[/bold] [cyan]{data['file']}[/cyan]")
316
+ console.print(f"[bold]{algorithm}:[/bold] [yellow]{data['hash']}[/yellow]")
317
+ console.print(f"[dim]大小: {data['size']}[/dim]")
318
+
319
+
224
320
  @cli.group("api", help="HTTP 客户端、Mock Server")
225
321
  def api_group():
226
322
  """apidev — API 开发工具"""
@@ -332,6 +428,112 @@ def api_history(limit):
332
428
  console.print(table)
333
429
 
334
430
 
431
+ @api_group.command("collection", help="管理请求集合")
432
+ @click.argument("action", type=click.Choice(["add", "list", "run", "remove"]))
433
+ @click.argument("name", required=False)
434
+ @click.option("--method", "-m", default="GET", type=click.Choice(_HTTP_METHODS))
435
+ @click.option("--url", "-u", help="请求 URL")
436
+ @click.option("--body", "-b", help="请求体")
437
+ @click.option("--desc", help="描述")
438
+ def api_collection(action, name, method, url, body, desc):
439
+ """保存/查看/执行/删除请求集合(支持 {{var}} 环境变量)"""
440
+ from rich.table import Table
441
+
442
+ from devmate_apidev import (
443
+ collection_add,
444
+ collection_list,
445
+ collection_remove,
446
+ collection_run,
447
+ )
448
+
449
+ if action == "add":
450
+ if not name or not url:
451
+ console.print("[red]❌ add 需要 --name 和 --url[/red]")
452
+ return
453
+ result = collection_add(name, method, url, body=body or "",
454
+ description=desc or "")
455
+ elif action == "list":
456
+ result = collection_list()
457
+ if result.success:
458
+ items = result.data["collections"]
459
+ if not items:
460
+ console.print("[yellow]集合为空[/yellow]")
461
+ return
462
+ table = Table(title=f"请求集合({len(items)} 条)")
463
+ table.add_column("名称", style="cyan")
464
+ table.add_column("方法", style="bold")
465
+ table.add_column("URL", style="green")
466
+ table.add_column("描述", style="dim")
467
+ for c in items:
468
+ table.add_row(c["name"], c["method"], c["url"], c.get("description", ""))
469
+ console.print(table)
470
+ return
471
+ elif action == "run":
472
+ if not name:
473
+ console.print("[red]❌ run 需要集合名[/red]")
474
+ return
475
+ result = collection_run(name)
476
+ elif action == "remove":
477
+ if not name:
478
+ console.print("[red]❌ remove 需要集合名[/red]")
479
+ return
480
+ result = collection_remove(name)
481
+ else:
482
+ console.print(f"[red]未知动作: {action}[/red]")
483
+ return
484
+
485
+ if result.success:
486
+ console.print(f"[green]{result.data.get('message', '✅ 完成')}[/green]")
487
+ else:
488
+ console.print(f"[red]❌ {result.error}[/red]")
489
+
490
+
491
+ @api_group.command("env", help="管理请求环境变量")
492
+ @click.argument("action", type=click.Choice(["set", "list", "remove"]))
493
+ @click.argument("key", required=False)
494
+ @click.option("--value", "-v", help="变量值")
495
+ def api_env(action, key, value):
496
+ """设置/查看/删除环境变量(在请求中通过 {{key}} 引用)"""
497
+ from rich.table import Table
498
+
499
+ from devmate_apidev import env_list, env_remove, env_set
500
+
501
+ if action == "set":
502
+ if not key or value is None:
503
+ console.print("[red]❌ set 需要 KEY 和 --value[/red]")
504
+ return
505
+ result = env_set(key, value)
506
+ elif action == "list":
507
+ result = env_list()
508
+ if result.success:
509
+ envs = result.data["envs"]
510
+ masked = result.data["masked"]
511
+ if not envs:
512
+ console.print("[yellow]暂无环境变量[/yellow]")
513
+ return
514
+ table = Table(title=f"环境变量({len(envs)} 个)")
515
+ table.add_column("键", style="cyan")
516
+ table.add_column("值", style="dim")
517
+ table.add_column("使用方式", style="green")
518
+ for k in envs:
519
+ table.add_row(k, masked.get(k, ""), f"{{{{{k}}}}}")
520
+ console.print(table)
521
+ return
522
+ elif action == "remove":
523
+ if not key:
524
+ console.print("[red]❌ remove 需要 KEY[/red]")
525
+ return
526
+ result = env_remove(key)
527
+ else:
528
+ console.print(f"[red]未知动作: {action}[/red]")
529
+ return
530
+
531
+ if result.success:
532
+ console.print(f"[green]{result.data.get('message', '✅ 完成')}[/green]")
533
+ else:
534
+ console.print(f"[red]❌ {result.error}[/red]")
535
+
536
+
335
537
  @cli.group("db", help="SQL 编辑器、表浏览器")
336
538
  def db_group():
337
539
  """dbadmin — 数据库管理"""
@@ -418,6 +620,82 @@ def db_show(table_name, name):
418
620
  console.print(pt)
419
621
 
420
622
 
623
+ @db_group.command("describe", help="查看表结构详情")
624
+ @click.argument("table_name")
625
+ @click.option("--name", default="default", help="连接名称")
626
+ def db_describe(table_name, name):
627
+ """查看表结构详情:列/索引/外键/行数"""
628
+ from rich.table import Table
629
+
630
+ from devmate_dbadmin import describe
631
+
632
+ result = describe(table_name, name=name)
633
+ if not result.success:
634
+ console.print(f"[red]❌ {result.error}[/red]")
635
+ return
636
+
637
+ data = result.data
638
+ console.print(f"\n[bold cyan]表: {data['table']}[/bold cyan] "
639
+ f"[dim]({data['row_count']} 行)[/dim]\n")
640
+
641
+ # 列
642
+ col_table = Table(title="列")
643
+ col_table.add_column("列名", style="cyan")
644
+ col_table.add_column("类型", style="yellow")
645
+ col_table.add_column("可空", style="green")
646
+ col_table.add_column("主键", style="bold")
647
+ col_table.add_column("默认值", style="dim")
648
+ for col in data["columns"]:
649
+ col_table.add_row(col["name"], col["type"],
650
+ "✅" if col["nullable"] else "❌",
651
+ "🔑" if col["primary_key"] else "",
652
+ col["default"])
653
+ console.print(col_table)
654
+
655
+ # 索引
656
+ if data["indexes"]:
657
+ idx_table = Table(title="索引")
658
+ idx_table.add_column("名称", style="cyan")
659
+ idx_table.add_column("列", style="yellow")
660
+ idx_table.add_column("唯一", style="green")
661
+ for idx in data["indexes"]:
662
+ idx_table.add_row(idx["name"], ", ".join(idx["columns"]),
663
+ "✅" if idx["unique"] else "❌")
664
+ console.print(idx_table)
665
+
666
+ # 外键
667
+ if data["foreign_keys"]:
668
+ fk_table = Table(title="外键")
669
+ fk_table.add_column("名称", style="cyan")
670
+ fk_table.add_column("列", style="yellow")
671
+ fk_table.add_column("引用表", style="green")
672
+ fk_table.add_column("引用列", style="dim")
673
+ for fk in data["foreign_keys"]:
674
+ fk_table.add_row(fk["name"], ", ".join(fk["columns"]),
675
+ fk["ref_table"], ", ".join(fk["ref_columns"]))
676
+ console.print(fk_table)
677
+
678
+
679
+ @db_group.command("export", help="导出表数据")
680
+ @click.argument("table_name")
681
+ @click.argument("format", type=click.Choice(["csv", "json", "sql"]))
682
+ @click.option("--output", "-o", help="输出文件路径(默认当前目录 表名.格式)")
683
+ @click.option("--limit", default=1000, type=int, help="最大行数")
684
+ @click.option("--name", default="default", help="连接名称")
685
+ def db_export(table_name, format, output, limit, name):
686
+ """导出表数据为 CSV/JSON/SQL"""
687
+ from devmate_dbadmin import export_table
688
+
689
+ result = export_table(table_name, format=format, output=output or "",
690
+ limit=limit, name=name)
691
+ if not result.success:
692
+ console.print(f"[red]❌ {result.error}[/red]")
693
+ return
694
+ data = result.data
695
+ console.print(f"[green]{data['message']}[/green]")
696
+ console.print(f"[dim]列: {', '.join(data['columns'])}[/dim]")
697
+
698
+
421
699
  @db_group.command("query", help="执行 SQL 查询")
422
700
  @click.argument("sql")
423
701
  @click.option("--name", default="default", help="连接名称")
@@ -1243,6 +1521,89 @@ def git_stats(path):
1243
1521
  console.print(ct)
1244
1522
 
1245
1523
 
1524
+ @git_group.command("status", help="仓库概览")
1525
+ @click.option("--path", default=".", help="Git 仓库路径")
1526
+ def git_status(path):
1527
+ """仓库概览:分支/未提交/领先落后"""
1528
+ from rich.table import Table
1529
+
1530
+ from devmate_gitflow import overview
1531
+
1532
+ result = overview(path=path)
1533
+ if not result.success:
1534
+ console.print(f"[red]❌ {result.error}[/red]")
1535
+ return
1536
+
1537
+ data = result.data
1538
+ console.print(f"\n[bold]📂 仓库: {data['path']}[/bold]\n")
1539
+
1540
+ table = Table(show_header=False, box=None)
1541
+ table.add_column("指标", style="cyan", width=18)
1542
+ table.add_column("值", style="yellow")
1543
+ table.add_row("当前分支", f"[bold]{data['branch']}[/bold]")
1544
+ table.add_row("未提交变更", str(data["uncommitted"]))
1545
+ table.add_row("领先远程", data["ahead"] or "-")
1546
+ table.add_row("落后远程", data["behind"] or "-")
1547
+ table.add_row("总提交数", str(data["commits"]))
1548
+ console.print(table)
1549
+
1550
+ if data["status_lines"]:
1551
+ console.print("\n[bold]未提交变更:[/bold]")
1552
+ for line in data["status_lines"][:15]:
1553
+ console.print(f" [dim]{line}[/dim]")
1554
+
1555
+
1556
+ @git_group.command("br", help="分支列表")
1557
+ @click.option("--path", default=".", help="Git 仓库路径")
1558
+ def git_br(path):
1559
+ """列出本地分支 + 识别已合并分支"""
1560
+ from rich.table import Table
1561
+
1562
+ from devmate_gitflow import branches
1563
+
1564
+ result = branches(path=path)
1565
+ if not result.success:
1566
+ console.print(f"[red]❌ {result.error}[/red]")
1567
+ return
1568
+
1569
+ data = result.data
1570
+ console.print(f"\n[bold]🌿 分支列表({data['total']} 个)[/bold]\n")
1571
+
1572
+ table = Table()
1573
+ table.add_column("分支", style="cyan")
1574
+ table.add_column("状态", style="yellow")
1575
+ for b in data["branches"]:
1576
+ if b["current"]:
1577
+ table.add_row(b["name"], "✅ 当前")
1578
+ elif b["name"] in data["merged"]:
1579
+ table.add_row(b["name"], "[dim]已合并[/dim]")
1580
+ else:
1581
+ table.add_row(b["name"], "[green]未合并[/green]")
1582
+ console.print(table)
1583
+
1584
+ if data["merged"]:
1585
+ console.print(f"\n[dim]提示: 已合并分支可清理 → git branch -d {' '.join(data['merged'][:5])}[/dim]") # noqa: E501
1586
+
1587
+
1588
+ @git_group.command("pr", help="生成 PR 描述")
1589
+ @click.option("--base", default="main", help="基础分支")
1590
+ @click.option("--head", default="", help="当前分支(默认自动检测)")
1591
+ @click.option("--path", default=".", help="Git 仓库路径")
1592
+ def git_pr(base, head, path):
1593
+ """基于分支提交自动生成 PR 描述(按类型聚合)"""
1594
+ from devmate_gitflow import pr_description
1595
+
1596
+ result = pr_description(path=path, base=base, head=head)
1597
+ if not result.success:
1598
+ console.print(f"[red]❌ {result.error}[/red]")
1599
+ return
1600
+
1601
+ data = result.data
1602
+ console.print(f"\n[bold cyan]{data['description']}[/bold cyan]")
1603
+ console.print(f"\n[dim]提交数: {data['commit_count']},"
1604
+ f"类型分布: {', '.join(f'{k}:{v}' for k, v in data['types'].items())}[/dim]")
1605
+
1606
+
1246
1607
  @cli.group("re", help="正则测试器、常用正则库")
1247
1608
  def re_group():
1248
1609
  """regexlab — 正则实验室"""
@@ -1312,6 +1673,63 @@ def re_search(pattern, path, flags, max):
1312
1673
  console.print(table)
1313
1674
 
1314
1675
 
1676
+ @re_group.command("replace", help="正则替换")
1677
+ @click.argument("pattern")
1678
+ @click.argument("replacement")
1679
+ @click.option("--text", "-t", help="输入文本")
1680
+ @click.option("--path", help="输入文件路径(就地替换)")
1681
+ @click.option("--flags", "-f", help="正则标志(i=忽略大小写, m=多行)")
1682
+ @click.option("--count", "-c", default=0, type=int, help="替换次数(0=全部)")
1683
+ def re_replace(pattern, replacement, text, path, flags, count):
1684
+ """正则替换(支持字符串或文件就地替换)"""
1685
+ from devmate_regexlab import replace
1686
+
1687
+ result = replace(pattern, replacement, text=text or "", path=path or "",
1688
+ flags=flags or "", count=count)
1689
+ if not result.success:
1690
+ console.print(f"[red]❌ {result.error}[/red]")
1691
+ return
1692
+ data = result.data
1693
+ if path:
1694
+ console.print(f"[green]{data['message']}[/green]")
1695
+ else:
1696
+ console.print(f"[bold]替换结果({data['matches']} 处):[/bold]")
1697
+ console.print(data["result"])
1698
+
1699
+
1700
+ @re_group.command("extract", help="提取所有匹配")
1701
+ @click.argument("pattern")
1702
+ @click.option("--text", "-t", help="输入文本")
1703
+ @click.option("--path", help="输入文件路径")
1704
+ @click.option("--flags", "-f", help="正则标志")
1705
+ @click.option("--max", default=100, type=int, help="最大提取数")
1706
+ def re_extract(pattern, text, path, flags, max):
1707
+ """提取所有匹配结果(支持分组提取)"""
1708
+ from rich.table import Table
1709
+
1710
+ from devmate_regexlab import extract
1711
+
1712
+ result = extract(pattern, text=text or "", path=path or "",
1713
+ flags=flags or "", max_matches=max)
1714
+ if not result.success:
1715
+ console.print(f"[red]❌ {result.error}[/red]")
1716
+ return
1717
+ data = result.data
1718
+ console.print(f"\n[bold]提取 {data['pattern']}[/bold] → "
1719
+ f"[yellow]{data['total']}[/yellow] 个匹配\n")
1720
+
1721
+ if data["matches"]:
1722
+ table = Table(show_header=True)
1723
+ table.add_column("#", style="dim", width=4)
1724
+ table.add_column("匹配", style="cyan")
1725
+ table.add_column("分组", style="yellow")
1726
+ table.add_column("位置", style="dim")
1727
+ for i, m in enumerate(data["matches"], 1):
1728
+ groups = ", ".join(str(g) for g in m["groups"]) if m["groups"] else "-"
1729
+ table.add_row(str(i), m["full"], groups, str(m["position"]))
1730
+ console.print(table)
1731
+
1732
+
1315
1733
  @re_group.command("lib", help="管理正则库")
1316
1734
  @click.argument("action", type=click.Choice(["list", "add", "remove"]))
1317
1735
  @click.argument("name", required=False)
@@ -1620,6 +2038,33 @@ def top_log(path, lines):
1620
2038
  console.print(data["content"])
1621
2039
 
1622
2040
 
2041
+ @top_group.command("port", help="查询端口占用")
2042
+ @click.argument("port", required=False, type=int)
2043
+ def top_port(port):
2044
+ """查询端口占用情况(跨平台)"""
2045
+ from rich.table import Table
2046
+
2047
+ from devmate_monitor import port_usage
2048
+
2049
+ result = port_usage(port)
2050
+ if not result.success:
2051
+ console.print(f"[red]❌ {result.error}[/red]")
2052
+ return
2053
+
2054
+ data = result.data
2055
+ console.print(f"\n[bold cyan]{data['message']}[/bold cyan]\n")
2056
+
2057
+ if data["listeners"]:
2058
+ table = Table(show_header=True)
2059
+ table.add_column("端口", style="yellow")
2060
+ table.add_column("PID", style="cyan")
2061
+ table.add_column("进程", style="green")
2062
+ table.add_column("地址", style="dim")
2063
+ for l in data["listeners"][:30]: # noqa: E741
2064
+ table.add_row(str(l["port"]), str(l["pid"]), l["process"], l["host"])
2065
+ console.print(table)
2066
+
2067
+
1623
2068
  @cli.group("report", help="日报/周报自动生成")
1624
2069
  def report_group():
1625
2070
  """reporter — 报告生成"""
@@ -2321,6 +2766,19 @@ def audit_count():
2321
2766
  console.print(f"审计记录总数:[cyan]{_audit.count}[/cyan] 条")
2322
2767
 
2323
2768
 
2769
+ @audit_group.command("export", help="导出审计记录 CSV")
2770
+ @click.option("--output", "-o", help="输出文件路径(默认 ~/.devmate/audit_export.csv)")
2771
+ @click.option("--level", help="过滤级别(SAFE/SENSITIVE/HIGH)")
2772
+ @click.option("--module", "-m", help="过滤模块")
2773
+ def audit_export(output, level, module):
2774
+ """导出审计记录为 CSV(支持级别/模块过滤)"""
2775
+ from devmate.security.audit import audit as _audit
2776
+
2777
+ count = _audit.export(output=output or "", level=level, module=module)
2778
+ out = output or str(Path.home() / ".devmate" / "audit_export.csv")
2779
+ console.print(f"[green]✅ 已导出 {count} 条审计记录到 {out}[/green]")
2780
+
2781
+
2324
2782
  # ═══════════════════════════════════════════════════
2325
2783
  # 顶层命令:mcp / web / upgrade
2326
2784
  # ═══════════════════════════════════════════════════
@@ -78,15 +78,21 @@ async def dashboard(request: Request, _: str = Depends(require_auth)):
78
78
 
79
79
  @app.get("/module/{name}", response_class=HTMLResponse)
80
80
  async def module_detail(request: Request, name: str, _: str = Depends(require_auth)):
81
- """模块详情页"""
81
+ """模块详情页——含完整命令文档"""
82
+ from devmate.app.web.cli_docs import CLI_ALIASES, collect_cli_docs
83
+
82
84
  mod = next((m for m in MODULES if m["name"] == name), None)
83
85
  if mod is None:
84
86
  raise HTTPException(status_code=404, detail=f"模块不存在: {name}")
85
87
 
88
+ # 自动收集该模块的 click 命令文档
89
+ cli_name = next((k for k, v in CLI_ALIASES.items() if v == name), name)
90
+ commands = collect_cli_docs(cli_name)
91
+
86
92
  return templates.TemplateResponse(
87
93
  "module.html",
88
94
  {"request": request, "mod": mod, "modules": MODULES,
89
- "active_module": name, "version": __version__},
95
+ "active_module": name, "commands": commands, "version": __version__},
90
96
  )
91
97
 
92
98