Files
Neo-ZQYY/tools/claude-code/migrate_ai_environment.py
Neo f2e0de8fab chore(migration): Cursor → Claude Code 反向迁移 + 单轨化(v2)
- 删除 5 个 AGENTS.md(根 + 4 子模块)与 .cursor/、.cursorignore,全部已备份
- 在 CLAUDE.md 末尾追加 5 节迁移必需内容(CLI/Shell 中文与编码、Claude Code 资产入口、Hook 与权限、不破坏原则、历史追溯),保留用户选定的 226 行项目规则全集
- 用户级 12 个 skills 从 ~/.cursor/skills/ 剥包装回迁到 ~/.claude/skills/(neozqyy-cursor-migration → neozqyy-claude-code-migration)
- docs/ai-env-history/ 顶层 10 文件入仓(含 conversation_index.csv、file_impact_index.csv,已脱敏);sessions/ 原文继续本地保留
- 新增 tools/claude-code/migrate_ai_environment.py(--check 14/14 通过)
- 新增 docs/claude_code_migration.md 与 docs/audit/changes/2026-05-02__claude_code_migration.md
- .gitignore 调整:开放 2 个 CSV 索引入仓,保留 sessions/ 与 claude-history/ 排除
- 不混入 124 个业务变更(AI 模块重构、runtime_context、sandbox 等保持 unstaged)
- 备份位置:~/.claude/backups/pre-claude-code-migration-2026-05-02/

第二轮迁移(第一轮 commit 6facb2d 已被 git reset 回滚;本轮策略为追加而非重写 CLAUDE.md)
2026-05-03 21:08:13 +08:00

400 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Cursor → Claude Code 反向迁移决策固化脚本。
用途:
1. `--check` 模式:扫描当前仓库与用户目录,校验本次反向迁移的所有产物
是否就位CLAUDE.md 单轨化追加章节、AGENTS.md 已归档、.cursor/ 已删除、
用户级 skills 已剥包装、敏感串扫描通过)。
2. 作为决策记录与回滚参考——脚本注释里明确写了每条决策的原因和源/目标。
3. 不再实现 `--apply` 模式:本次迁移已手动执行完毕,重跑没有价值;
如需复用决策,请参考 `tools/cursor/migrate_ai_environment.py` 的结构
与本脚本里的常量定义。
使用:
python tools/claude-code/migrate_ai_environment.py --check
python tools/claude-code/migrate_ai_environment.py --check --strict # 失败立即退出
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable
REPO_ROOT = Path(os.environ.get("NEOZQYY_ROOT", Path(__file__).resolve().parents[2]))
USER_HOME = Path(os.environ.get("USERPROFILE") or os.path.expanduser("~"))
BACKUP_ROOT = USER_HOME / ".claude" / "backups" / "pre-claude-code-migration-2026-05-02"
# ---------- 决策常量(迁移决策一处定义) ----------
# 单轨化:以下 5 个 AGENTS.md 应被删除(备份在 BACKUP_ROOT/project_root/
AGENTS_MD_TO_REMOVE = [
"AGENTS.md",
"apps/backend/AGENTS.md",
"apps/etl/connectors/feiqiu/AGENTS.md",
"apps/demo-miniprogram/AGENTS.md",
"db/AGENTS.md",
]
# 单轨化以下文件应保留CLAUDE.md 是 Claude Code 原生加载入口)
CLAUDE_MD_REQUIRED = [
"CLAUDE.md",
"apps/backend/CLAUDE.md",
"apps/etl/connectors/feiqiu/CLAUDE.md",
"apps/demo-miniprogram/CLAUDE.md",
"db/CLAUDE.md",
]
# 已归档删除的 Cursor 资产
CURSOR_PATHS_TO_REMOVE = [
".cursor",
".cursorignore",
]
# 项目级 Claude Code 资产(必须存在)
PROJECT_CLAUDE_REQUIRED = [
".claude/settings.json",
".claude/commands/audit.md",
".claude/commands/db-docs.md",
".claude/commands/doc-sync.md",
".claude/commands/pre-change.md",
".claude/commands/spec-close.md",
".claude/hooks/session_start_context.py",
".claude/hooks/pre_read_archived_block.py",
".claude/hooks/pre_demo_protect.py",
".claude/hooks/post_edit_audit_reminder.py",
".claude/hooks/post_edit_db_doc_sync.py",
".claude/hooks/post_edit_rls_dual_schema.py",
".claude/hooks/stop_audit_check.py",
".claude/hooks/stop_verify_check.py",
]
# 用户级 8 个 subagent
USER_AGENTS_REQUIRED = [
"architect.md",
"code-reviewer.md",
"database-reviewer.md",
"planner.md",
"python-reviewer.md",
"refactor-cleaner.md",
"security-reviewer.md",
"tdd-guide.md",
]
# 用户级 skills12 个,回迁自 ~/.cursor/skills/,剥包装)
USER_SKILLS_REQUIRED = [
"agent-introspection-debugging",
"claude-agent-roles",
"claude-rules-reference",
"code-tour",
"codebase-onboarding",
"neozqyy-claude-code-migration", # 重命名自 neozqyy-cursor-migration
"repo-scan",
"rules-distill",
"search-first",
"security-review",
"strategic-compact",
"tdd-workflow",
]
# 第 10.1 节:自动触发(保留 description 关键词激活)
USER_SKILLS_AUTO_TRIGGER = {"strategic-compact", "neozqyy-claude-code-migration"}
# Cursor 时代留下的兼容前缀,迁回时应被剥掉
CURSOR_RESIDUAL_PATTERNS = [
re.compile(r"disable-model-invocation:\s*true", re.IGNORECASE),
re.compile(r"^>\s*迁移说明:本 skill 从 [^\n]+ 转换而来", re.MULTILINE),
]
# 根 CLAUDE.md 必须包含的迁移章节(与原有项目章节并存)
CLAUDE_MD_MIGRATION_SECTIONS = [
"## CLI / Shell 中文与编码(强制)",
"## Claude Code 资产入口",
"## Hook 与权限",
"## 不破坏原则",
"## 历史追溯",
]
# 历史索引敏感扫描
SENSITIVE_PATTERNS = {
"DSN_with_password": re.compile(r"://[^:/\s]+:[^@/\s'\"]+@[^/\s'\"]+/[A-Za-z]"),
"Anthropic_key": re.compile(r"\bsk-ant-[A-Za-z0-9_\-]{30,}\b"),
"OpenAI_key": re.compile(r"\bsk-proj-[A-Za-z0-9_\-]{30,}\b"),
"JWT_long": re.compile(r"\beyJ[A-Za-z0-9_\-]{30,}\.[A-Za-z0-9_\-]{30,}\.[A-Za-z0-9_\-]{30,}\b"),
"AWS_AKIA": re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
}
# ---------- 检查框架 ----------
@dataclass
class CheckResult:
name: str
passed: bool
detail: str = ""
@dataclass
class CheckReport:
results: list[CheckResult] = field(default_factory=list)
def add(self, result: CheckResult) -> None:
self.results.append(result)
def passed_count(self) -> int:
return sum(1 for r in self.results if r.passed)
def failed(self) -> list[CheckResult]:
return [r for r in self.results if not r.passed]
def _check_files_exist(paths: list[str], base: Path, label: str) -> CheckResult:
missing = [p for p in paths if not (base / p).exists()]
if missing:
return CheckResult(label, False, f"缺失 {len(missing)} 个:{missing[:5]}")
return CheckResult(label, True, f"全部 {len(paths)} 个就位")
def _check_files_absent(paths: list[str], base: Path, label: str) -> CheckResult:
present = [p for p in paths if (base / p).exists()]
if present:
return CheckResult(label, False, f"应已删除但仍存在 {len(present)} 个:{present}")
return CheckResult(label, True, f"全部 {len(paths)} 个已归档删除")
def check_root_claude_md(report: CheckReport) -> None:
"""根 CLAUDE.md 是否包含迁移所需章节(与原有项目章节并存)。"""
p = REPO_ROOT / "CLAUDE.md"
if not p.exists():
report.add(CheckResult("根 CLAUDE.md 存在", False, "文件不存在"))
return
text = p.read_text(encoding="utf-8")
missing = [s for s in CLAUDE_MD_MIGRATION_SECTIONS if s not in text]
if missing:
report.add(CheckResult("根 CLAUDE.md 迁移章节", False, f"缺章节:{missing}"))
else:
report.add(CheckResult("根 CLAUDE.md 迁移章节", True, f"全部 {len(CLAUDE_MD_MIGRATION_SECTIONS)} 节存在"))
def check_agents_md_removed(report: CheckReport) -> None:
report.add(_check_files_absent(AGENTS_MD_TO_REMOVE, REPO_ROOT, "AGENTS.md 已归档5 个)"))
def check_cursor_removed(report: CheckReport) -> None:
report.add(_check_files_absent(CURSOR_PATHS_TO_REMOVE, REPO_ROOT, ".cursor/ 与 .cursorignore 已归档"))
def check_claude_md_present(report: CheckReport) -> None:
report.add(_check_files_exist(CLAUDE_MD_REQUIRED, REPO_ROOT, "CLAUDE.md 单轨保留5 个)"))
def check_project_claude_assets(report: CheckReport) -> None:
report.add(_check_files_exist(PROJECT_CLAUDE_REQUIRED, REPO_ROOT, ".claude/ 项目级资产"))
def check_user_agents(report: CheckReport) -> None:
base = USER_HOME / ".claude" / "agents"
report.add(_check_files_exist(USER_AGENTS_REQUIRED, base, "用户级 8 个 subagent"))
def check_user_skills(report: CheckReport) -> None:
base = USER_HOME / ".claude" / "skills"
if not base.exists():
report.add(CheckResult("用户级 12 个 skills 目录", False, f"目录不存在:{base}"))
return
missing = [name for name in USER_SKILLS_REQUIRED if not (base / name / "SKILL.md").exists()]
if missing:
report.add(CheckResult("用户级 12 个 skills 目录", False, f"缺失 {len(missing)}{missing}"))
else:
report.add(CheckResult("用户级 12 个 skills 目录", True, "全部 12 个 SKILL.md 就位"))
def check_user_skills_stripped(report: CheckReport) -> None:
"""检查用户级 skills 已剥掉 Cursor 包装。"""
base = USER_HOME / ".claude" / "skills"
if not base.exists():
report.add(CheckResult("Skills 已剥 Cursor 包装", False, "目录不存在"))
return
residual = []
for name in USER_SKILLS_REQUIRED:
skill_md = base / name / "SKILL.md"
if not skill_md.exists():
continue
text = skill_md.read_text(encoding="utf-8")
for pat in CURSOR_RESIDUAL_PATTERNS:
if pat.search(text):
residual.append((name, pat.pattern[:50]))
break
if residual:
report.add(CheckResult("Skills 已剥 Cursor 包装", False, f"残留 {len(residual)}{residual[:5]}"))
else:
report.add(CheckResult("Skills 已剥 Cursor 包装", True, "12 个 skill 均已清理"))
def check_mcp_single_source(report: CheckReport) -> None:
""".mcp.json 是单一源,不应有 .cursor/mcp.json 副本。"""
primary = REPO_ROOT / ".mcp.json"
duplicate = REPO_ROOT / ".cursor" / "mcp.json"
if not primary.exists():
report.add(CheckResult("MCP 单一源", False, ".mcp.json 不存在"))
return
if duplicate.exists():
report.add(CheckResult("MCP 单一源", False, ".cursor/mcp.json 仍存在(应已随 .cursor/ 归档)"))
return
try:
cfg = json.loads(primary.read_text(encoding="utf-8"))
except Exception as e:
report.add(CheckResult("MCP 单一源", False, f".mcp.json 解析失败:{e}"))
return
servers = cfg.get("mcpServers", {})
issues = []
for name in ("pg-etl", "pg-app"):
if name in servers and not servers[name].get("disabled", False):
issues.append(f"{name} 应 disabled=true")
for name in ("pg-etl-test", "pg-app-test"):
if name in servers and servers[name].get("disabled", False):
issues.append(f"{name} 应 disabled=false")
if issues:
report.add(CheckResult("MCP 单一源", False, "; ".join(issues)))
else:
report.add(CheckResult("MCP 单一源", True, ".mcp.json 是唯一源prod/test 状态正确"))
def check_history_index_present(report: CheckReport) -> None:
p = REPO_ROOT / "docs" / "ai-env-history"
if not p.exists():
report.add(CheckResult("docs/ai-env-history/ 入仓", False, "目录不存在"))
return
file_count = sum(1 for f in p.rglob("*") if f.is_file())
if file_count < 100:
report.add(CheckResult("docs/ai-env-history/ 入仓", False, f"文件偏少:{file_count}"))
else:
report.add(CheckResult("docs/ai-env-history/ 入仓", True, f"{file_count} 个文件"))
def check_history_no_secrets(report: CheckReport) -> None:
p = REPO_ROOT / "docs" / "ai-env-history"
if not p.exists():
report.add(CheckResult("历史索引敏感扫描", False, "目录不存在"))
return
hits = []
for f in p.rglob("*"):
if not f.is_file():
continue
try:
text = f.read_text(encoding="utf-8", errors="replace")
except Exception:
continue
for label, pat in SENSITIVE_PATTERNS.items():
for m in pat.finditer(text):
snippet = m.group(0)
# 白名单:文档中描述敏感扫描正则模式自身
if "postgresql://|postgres://" in text[max(0, m.start()-30):m.end()+30]:
continue
hits.append((f.relative_to(REPO_ROOT).as_posix(), label, snippet[:80]))
if hits:
report.add(CheckResult("历史索引敏感扫描", False, f"{len(hits)} hits{hits[:3]}"))
else:
report.add(CheckResult("历史索引敏感扫描", True, "无密钥/DSN 泄露"))
def check_cold_storage_intact(report: CheckReport) -> None:
"""冷备路径仍在tools/cursor、tools/codex 不应被删除,因为 .mcp.json 引用 mcp-postgres.ps1"""
required = [
"tools/cursor/migrate_ai_environment.py",
"tools/codex/mcp-postgres.ps1",
]
report.add(_check_files_exist(required, REPO_ROOT, "冷备资产保留"))
def check_backup_present(report: CheckReport) -> None:
if not BACKUP_ROOT.exists():
report.add(CheckResult("迁移备份目录", False, f"不存在:{BACKUP_ROOT}"))
return
manifest = BACKUP_ROOT / "BACKUP_MANIFEST.md"
if not manifest.exists():
report.add(CheckResult("迁移备份目录", False, "BACKUP_MANIFEST.md 缺失"))
return
report.add(CheckResult("迁移备份目录", True, str(BACKUP_ROOT)))
def check_memory_untouched(report: CheckReport) -> None:
"""用户记忆目录必须存在且未被迁移触碰。"""
mem = USER_HOME / ".claude" / "projects" / "c--Project-NeoZQYY" / "memory"
if not mem.exists():
report.add(CheckResult("用户记忆目录未受影响", False, f"不存在:{mem}"))
return
files = list(mem.glob("*.md"))
if len(files) < 5:
report.add(CheckResult("用户记忆目录未受影响", False, f"文件数过少:{len(files)}"))
else:
report.add(CheckResult("用户记忆目录未受影响", True, f"{len(files)} 个记忆文件"))
CHECKS: list[Callable[[CheckReport], None]] = [
check_backup_present,
check_root_claude_md,
check_claude_md_present,
check_agents_md_removed,
check_cursor_removed,
check_project_claude_assets,
check_user_agents,
check_user_skills,
check_user_skills_stripped,
check_mcp_single_source,
check_history_index_present,
check_history_no_secrets,
check_cold_storage_intact,
check_memory_untouched,
]
def main() -> int:
parser = argparse.ArgumentParser(description="Cursor → Claude Code 反向迁移决策固化与校验")
parser.add_argument("--check", action="store_true", help="校验当前状态(默认行为)")
parser.add_argument("--strict", action="store_true", help="任一检查失败即退出非零")
parser.add_argument("--json", action="store_true", help="JSON 输出(供脚本消费)")
args = parser.parse_args()
report = CheckReport()
for fn in CHECKS:
try:
fn(report)
except Exception as e:
report.add(CheckResult(fn.__name__, False, f"异常:{e}"))
if args.json:
out = {
"passed": report.passed_count(),
"total": len(report.results),
"results": [
{"name": r.name, "passed": r.passed, "detail": r.detail}
for r in report.results
],
}
print(json.dumps(out, ensure_ascii=False, indent=2))
else:
for r in report.results:
mark = "[OK]" if r.passed else "[X] "
print(f"{mark} {r.name}: {r.detail}")
print()
print(f"通过 {report.passed_count()} / {len(report.results)}")
if report.failed():
print("\n失败项:")
for r in report.failed():
print(f" - {r.name}: {r.detail}")
if args.strict and report.failed():
return 1
return 0
if __name__ == "__main__":
sys.exit(main())