sync: WIN10-I9-64G 2026-07-20

This commit is contained in:
Myfro9
2026-07-20 18:14:49 +08:00
parent bceafcd58c
commit 2ef32a9dcc
7 changed files with 502 additions and 22 deletions
+3
View File
@@ -348,3 +348,6 @@ web-design-guidelines/SKILL.md
*.mjs
*.pckl
*.pyc
*.sqlite
**/.langgraph_api/
+36 -22
View File
@@ -7,31 +7,38 @@ description: Use the LangGraph plan-review workflow from the current Claude Code
Use this skill to run the LangGraph plan-review workflow. All scripts are bundled under the skill's own `scripts\` directory (`C:\Users\bowei\.claude\skills\langgraph-plan-review\scripts\`) — no dependency on `E:\LangGraph_Design\lab00`.
## ⚠️ 强制执行规则(MANDATORY — read first)
## 运行方式:进程内驱动,无 dev server(2026-07 起)
**plan-review 工作流必须、且只能通过 `langgraph dev` 服务器来驱动。**
**本 skill 已去掉 `langgraph dev` 服务器**。工作流由进程内 runner **`scripts\clean_run.py`** 直接驱动:
它 `build_workflow()` 编译图、用 **SqliteSaver**(`scripts\.plan_review_state.sqlite`)做跨调用持久化、
自己处理 `interrupt`/`Command(resume=...)`。不再需要 `manage_dev_server.ps1` / `run_langgraph.ps1` /
`run_workflow.py`(这些是旧的 dev-server 路径,保留仅作参考,勿再使用)。
- ✅ **唯一允许的入口**:`run_langgraph.ps1` → `run_workflow.py`。后者是一个**纯 REST/SDK 客户端**
(`langgraph_sdk.get_sync_client(url=http://127.0.0.1:2024)`),每次 start/resume 都把 run
投递到 `langgraph dev` 服务器执行。先用 `manage_dev_server.ps1 start` 把服务器拉起。
这样 LangGraph Studio 才能实时追踪线程、节点、中断与状态。
- **入口**:用 `langgraph` conda 环境的 python 跑 `clean_run.py`:
```powershell
$PY = "C:\Users\bowei\.conda\envs\langgraph\python.exe"
$RUN = "C:\Users\bowei\.claude\skills\langgraph-plan-review\scripts\clean_run.py"
# 首次(到 write_plan 中断):
& $PY $RUN --task-id 10 --work-dir "<work_dir>" --notes-dir Notes --prompt "<原始请求>"
# resume(写完/改完 Task10.md 后;resume 值 base64(JSON)):
$resume = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('true'))
& $PY $RUN --task-id 10 --work-dir "<work_dir>" --notes-dir Notes --resume-b64 $resume --codex-review-command-b64 $CMD --timeout 900
```
每次调用推进到下一个 `interrupt` 或结束,打印一行 JSON:`{"status":"waiting","action_required":{...}}`
或 `{"status":"done","has_serious_issue":...}`。`action_required` 的处理同下(write_plan / decide /
revise / write_rejection),resume 值:`write_plan`/`revise`/`write_rejection` 用 `true`;`decide` 用
`{"accepts":true}` 或 `{"accepts":false,"reason":"..."}`。
- 🚫 **绝对禁止**:直接 `import` / 在进程内运行本 skill 的图定义模块
(`scripts\graph.py`、`scripts\workflow.py`、`scripts\agents.py`)来跑工作流。包括但不限于:
- `python -c "from graph import ...; graph.invoke(...)"` 或 `.stream(...)`
- 写临时脚本 build 编译图后在进程内调用
- 任何绕过 `http://127.0.0.1:2024` 的「就地执行」方式
- **codex 评审的两个必备条件**(本次踩坑记录):
1. **codex CLI 要足够新**。config.toml 若配了较新模型(如 `gpt-5.6-sol`),旧 codex 会返回 400
`requires a newer version of Codex`。升级:`npm i -g @openai/codex@latest`(≥0.144.5 实测可用)。
2. **codex 子进程要能到 chatgpt.com**。`clean_run.py` 会自动读 Windows 系统代理(HKCU Internet
Settings 的 `ProxyServer`)设进 `HTTP(S)_PROXY`,让 codex 走代理;也可用 `PLAN_REVIEW_PROXY` 覆盖。
3. **codex 超时**:`review_task.py` 的内层 codex 超时取 argv[5](默认 300s,评审常不够)。用
`--codex-review-command-b64` 传一条**末尾带超时秒数**的命令(见 Workflow),并把 `--timeout` 设一致。
这类调用在 dev server 进程**之外**运行,Studio 追踪不到状态,**一律不允许**——
即使只是想「快速验证一个节点」也不行。
- ℹ️ **例外说明**:`review_task.py`(Codex 评审 shim)会直接调 `codex`,这是**合规的**——
它是由运行在 dev server 内的图通过 `codex_review_command` 调起的评审步骤,不是绕过
dev server 去驱动工作流本身。区别在于:驱动图 = 必须走 dev server;图内部调用的工具 = 正常。
- 🛠 **改了 `graph.py`/`workflow.py`/`agents.py` 后**:dev server 内存里仍是旧代码。先
`manage_dev_server.ps1 stop` 再 `start`(或让 `--reload` 生效),再经 `run_langgraph.ps1`
重跑,使改动在被追踪的运行中生效。
- **改了 `workflow.py`/`agents.py`/`clean_run.py` 后**:无 dev server 缓存,下次 `clean_run.py` 直接生效。
若图结构改到与旧 checkpoint 不兼容,删 `scripts\.plan_review_state.sqlite` 重新开始该 thread。
## Pre-flight: Environment Setup
@@ -75,7 +82,10 @@ conda run -n langgraph langgraph --version
- The workflow uses LangGraph `interrupt()` and persists state via the langgraph dev server.
- Each `run_langgraph.ps1` invocation advances the graph to the next interrupt or completion. Use `-Resume` to continue the same thread after completing the requested Claude Code action.
## Dev Server Lifecycle
## Dev Server Lifecycle ⛔ DEPRECATED
> **已弃用(2026-07 起)。** 本 skill 现用 `clean_run.py` 进程内驱动,**不再需要 dev server**。
> 下面这套 `manage_dev_server.ps1` / `langgraph dev` / Studio 仅作历史参考,正常流程不要用。
The `langgraph dev` server must be running for the entire duration of a workflow session.
Use `manage_dev_server.ps1` to start it before the first `run_langgraph.ps1` call and stop it after the workflow completes.
@@ -99,6 +109,10 @@ C:\Users\bowei\.claude\skills\langgraph-plan-review\scripts\manage_dev_server.ps
## Workflow
> **注意**:下列步骤是**旧的 dev-server 流程**(`manage_dev_server.ps1` + `run_langgraph.ps1`)。
> 正常请用上方"**运行方式:进程内驱动**"节的 `clean_run.py`。以下 action 语义(write_plan / decide /
> revise / write_rejection 及其 resume 值)两种流程通用,可参考。
1. Determine `TaskId` from the user. If missing, ask for it.
2. Start the dev server (idempotent):
```powershell
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""In-process driver for the plan-review graph — NO langgraph dev server.
Builds the compiled graph in-process (workflow.build_workflow) with a SqliteSaver so
state survives between invocations, and drives interrupts directly. Sets HTTP(S)_PROXY
from the Windows system proxy (or PLAN_REVIEW_PROXY) so the codex subprocess spawned by
the review node can reach its backend through the proxy.
CLI (mirrors the old run_workflow.py):
--task-id 10 [--work-dir DIR] [--notes-dir Notes] [--prompt "..."]
[--resume-b64 <b64 json>] [--codex-review-command-b64 <b64 json>] [--timeout 600]
Prints one JSON line: {"status":"waiting","action_required":{...}} or {"status":"done",...}.
"""
from __future__ import annotations
import os, sys, json, base64, argparse, sqlite3
from pathlib import Path
SCRIPTS = Path(__file__).resolve().parent
sys.path.insert(0, str(SCRIPTS))
def resolve_proxy() -> str:
px = os.environ.get("PLAN_REVIEW_PROXY", "").strip()
if not px:
try:
import winreg
k = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Internet Settings",
)
enable, _ = winreg.QueryValueEx(k, "ProxyEnable")
server, _ = winreg.QueryValueEx(k, "ProxyServer")
if enable and server and "=" not in str(server): # simple host:port, not per-scheme
px = "http://" + str(server)
except Exception:
px = ""
if px:
for v in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY",
"http_proxy", "https_proxy", "all_proxy"):
os.environ[v] = px
return px
def open_saver(db_path: str):
from langgraph.checkpoint.sqlite import SqliteSaver
conn = sqlite3.connect(db_path, check_same_thread=False)
saver = SqliteSaver(conn)
try:
saver.setup()
except Exception:
pass
return saver
def extract_interrupt(result):
if not isinstance(result, dict):
return None
ints = result.get("__interrupt__")
if not ints:
return None
first = ints[0] if isinstance(ints, (list, tuple)) else ints
return getattr(first, "value", first)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--task-id", required=True)
ap.add_argument("--work-dir", default=".")
ap.add_argument("--notes-dir", default="Notes")
ap.add_argument("--prompt", default="")
ap.add_argument("--resume-b64", default="")
ap.add_argument("--codex-review-command-b64", default="")
ap.add_argument("--timeout", type=int, default=600)
ap.add_argument("--db", default=str(SCRIPTS / ".plan_review_state.sqlite"))
args = ap.parse_args()
proxy = resolve_proxy()
from workflow import build_workflow
from langgraph.types import Command
codex_cmd = None
if args.codex_review_command_b64:
codex_cmd = json.loads(base64.b64decode(args.codex_review_command_b64).decode("utf-8"))
saver = open_saver(args.db)
graph = build_workflow(checkpointer=saver)
config = {"configurable": {"thread_id": str(args.task_id), "codex_timeout": args.timeout}}
if codex_cmd:
config["configurable"]["codex_review_command"] = codex_cmd
if args.resume_b64:
resume_val = json.loads(base64.b64decode(args.resume_b64).decode("utf-8"))
result = graph.invoke(Command(resume=resume_val), config)
else:
init = {
"task_id": str(args.task_id),
"user_prompt": args.prompt,
"work_dir": args.work_dir,
"notes_dir": args.notes_dir,
"codex_session_id": "",
}
result = graph.invoke(init, config)
payload = extract_interrupt(result)
if payload is not None:
out = {"status": "waiting", "thread_id": str(args.task_id),
"action_required": payload, "proxy": proxy}
else:
out = {"status": "done", "thread_id": str(args.task_id),
"has_serious_issue": bool(result.get("has_serious_issue")),
"review_comment": result.get("review_comment", ""),
"proxy": proxy}
print(json.dumps(out, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+162
View File
@@ -0,0 +1,162 @@
---
name: tools-sync
description: 在本机统一同步并配置 Claude Code 与 Codex 的 skills、MCP server 和开发环境:将 ~\.claude\skills 和 ~\.codex\skills 与 git.rpchannel.com 上的 bowei/skills-win 仓库保持同步,审计 skills 中的硬编码参数并迁移到本机 ~\skills_env.md,校验 langgraph conda 环境,并确保 E:\MCP_design 下所有 MCP 都已在 Claude Code 和 Codex 中注册。当用户提到"同步skills"、"skills同步"、"tools sync"、"tools-sync"、"同步codex"、"同步MCP"、"环境同步"、"skills-win"、"把skills推上去/拉下来"、"新电脑配置claude/codex环境"、"检查MCP注册"、"skills_env"时,必须使用此技能,即使用户没有明确说"skill"二字。
---
# tools-sync:Claude Code / Codex 工具链同步
## 目标与总体思路
一台开发机上有两个 AI 编码工具(Claude Code 和 Codex),它们各自有 skills 目录和 MCP
注册表。这个 skill 的职责是让它们保持一致,并且可以在一台新电脑上从零复原整套配置:
- **skills 的唯一真源**是 `http://git.rpchannel.com/bowei/skills-win.git`(remote 名 `origin`)。
`~\.claude\skills` 是主工作副本,`~\.codex\skills` 是同一仓库的第二个检出(镜像)。
Codex 没有注册命令——它在会话启动时自动扫描 `~\.codex\skills` 下所有含 `SKILL.md`
的目录,所以"注册到 Codex" = "目录出现在那里且含 SKILL.md"。
- **机器相关的参数不进 git**。每台电脑的软件安装路径、IP、账号等都记录在
`~\skills_env.md`(用户主目录下,天然在两个仓库检出之外,不会被 git 同步)。
skills 里引用参数时写"见 ~\skills_env.md 的 XXX 条目",不写死路径。
- **凭据不写进任何会被同步的文件**。git.rpchannel.com 的凭据优先用 Windows
凭据管理器(`cmdkey /list` 里应有 `git:http://git.rpchannel.com` 条目);
只有在新机器首次配置时,才从 `~\skills_env.md` 读取用户名/密码完成首次
clone(之后 credential helper 会缓存)。
完整跑一遍下面 6 个阶段就是一次"同步"。用户只要求其中一部分(比如"把 skills 推上去")
时,跑对应阶段即可,但阶段 0 永远先做。
## 阶段 0 — 读取本机环境档案 ~\skills_env.md
先读 `~\skills_env.md`。后续所有阶段的路径、URL、账号都以它为准。
- 如果文件不存在(通常是新电脑):从本 skill 的
`references/skills_env_template.md` 复制一份到 `~\skills_env.md`,
然后逐项核对模板里的路径在本机是否真实存在(`Test-Path`),把不符的改成实际值,
缺失的软件记为"未安装"。凭据字段让用户填写或从用户处确认,不要猜。
- 如果存在:快速通读,注意 `status` 类标记和上次同步遗留的 TODO。
## 阶段 1 — 同步 ~\.claude\skills(主副本)
用 `scripts/sync_repos.ps1` 做机械部分;它失败或报冲突时再人工介入。
```powershell
# 从 skill 目录运行;RepoUrl 取自 skills_env.md
powershell -File <skill目录>\scripts\sync_repos.ps1 -RepoUrl <origin_url>
```
脚本对主副本做的事:`git add -A` → 有变更则 commit(消息格式
`sync: <机器名> <日期> <一句话摘要>`)→ `git pull --rebase` → `git push`。
人工介入的情形:
- **rebase 冲突**:逐个文件看冲突双方,skills 文本冲突通常保留两边语义合并;
解不了就 `git rebase --abort` 并向用户报告冲突文件清单,让用户裁决。
- **push 被拒(认证)**:检查凭据管理器条目;新机器上用
`git config --global credential.helper manager` 后重试一次,让 credential
helper 弹窗或从 skills_env.md 读凭据拼一次性 URL 完成首次认证。
认证成功后确认凭据已被缓存(再 fetch 一次不应再要密码)。
- **不该入库的文件**:commit 前扫一眼 `git status`,运行时产物(.sqlite、.pckl、
日志、缓存目录)应加进 `.gitignore` 而不是提交。看到就顺手补 `.gitignore`。
## 阶段 2 — 同步 ~\.codex\skills(镜像检出)
同一个脚本会接着处理 codex 侧。逻辑和原因:
- 已是 git 仓库 → `git pull --rebase` 即可。
- 还不是 git 仓库(首次转换)→ 脚本执行 `git init` + `remote add origin` +
`fetch` + `git reset origin/main`,然后**停下来**列出"本地与仓库不一致"的文件。
这一步停是有原因的:codex 目录里可能有比仓库新的本地修改或 codex 独有的
skill。处理原则——codex 独有且有价值的 skill,先复制到 `~\.claude\skills`
走阶段 1 入库,再回来;确认没有需要保留的差异后,用 `-Force` 重跑让脚本
`git checkout -f` 对齐到仓库版本。codex 自带的系统 skills(`.system/`、
vercel 系列等)已在 `.gitignore` 里,不会被动。
- 同步完成后校验注册:仓库里每个顶层 skill 目录都应在 `~\.codex\skills`
出现且含 `SKILL.md`。列出两侧目录做差集,差集应为空(忽略 .gitignore
中的 codex 专属条目)。提醒用户:Codex 需要开新会话才会扫到新 skills。
## 阶段 3 — 硬编码审计与 skills_env.md 回填
目的:让仓库里的 skills 在任何一台按 `~\skills_env.md` 配置好的电脑上都能用。
在 `~\.claude\skills`(排除 `.system`)里搜这几类硬编码:
- 盘符绝对路径:正则 `[A-Za-z]:[\\/]`(如 `D:\Xilinx\Vivado`、`E:\MCP_design`)
- IP 地址:`\d+\.\d+\.\d+\.\d+`
- 明文凭据:`password`、`密码`、常见口令字样
对每个命中做判断,不要机械替换:
- **机器相关的值**(安装路径、板卡 IP、conda 路径)→ 确认已记录在
`~\skills_env.md` 对应条目(没有就补上),然后把 skill 原文改成引用式写法,
例如"Vivado 路径见 `~\skills_env.md` 的 `VIVADO_PATH`"。skill 的执行逻辑
改为:运行时先读 skills_env.md 取值。
- **明文密码出现在会被 git 同步的文件里** → 这是必须当场修复的问题:
从文件里删掉,移入 `~\skills_env.md`,并向用户报告(密码已进过 git
历史的话,建议用户改密码)。
- **协议性/普适的值**(如标准端口号、公开 URL)→ 保留,不算硬编码。
一次审计不必把所有历史 skills 全部重构完;至少保证:新增/本次改动的 skill 零硬编码,
存量 skill 的命中清单写进最终报告,让用户决定分批改。改动之后回到阶段 1 再
commit + push 一次,并在 codex 侧 pull,保证两边一致。
## 阶段 4 — langgraph 开发环境校验
很多 skills(vivado-mcp、fpga 系列、langgraph 系列)依赖 conda 的 `langgraph` 环境。
校验方式(路径以 skills_env.md 的 `CONDA_LANGGRAPH_PYTHON` 为准):
```powershell
& <CONDA_LANGGRAPH_PYTHON> -c "import langgraph, langchain; print('langgraph env OK')"
```
失败(环境不存在或包缺失)→ 调用 `langgraph-vivado-install` skill 完成安装,
装好后把实际的 python 路径回填到 `~\skills_env.md`。
## 阶段 5 — MCP server 注册校验(Claude Code + Codex 两边)
MCP 的清单真源是 `E:\MCP_design\mcp_list.md`(路径见 skills_env.md 的
`MCP_DESIGN_ROOT`),每行一个 MCP 工程目录。对清单里的每一项:
1. **Claude Code 侧**:`claude mcp list` 或读 `~\.claude.json` 的 `mcpServers`,
确认存在对应条目。缺失的用 `claude mcp add <name> ...` 注册,命令、解释器
路径和 env 变量都取自 `~\skills_env.md` 的 MCP 配置节。
2. **Codex 侧**:读 `~\.codex\config.toml` 的 `[mcp_servers.<name>]` 节。
缺失的按 skills_env.md 里的同一份配置补写 TOML 节(stdio 类型,
记得带 `PYTHONIOENCODING=utf-8`、`PYTHONUTF8=1`,Codex 在 Windows 上
没有这两个变量时中文输出会乱码)。
3. **可启动性抽查**:对新注册的 MCP,直接运行其 command + args 数秒,
确认进程能起来、没有立刻报 import/路径错误(stdio MCP 起来后会静默等
输入,这就算通过,Ctrl-C 掉即可)。
注意两侧配置语义相同但格式不同(JSON vs TOML),同一个 MCP 在两边的
command/args/env 应当一致;发现两边漂移(比如只有一边改了 VIVADO_PATH)时,
以 skills_env.md 记录的值为准改齐两边。
## 阶段 6 — 输出同步报告
最后用这个结构向用户汇报(有问题的条目放前面):
```
# tools-sync 同步报告 <日期>
## skills 同步
- ~\.claude\skills: <pull/push 结果,commit 摘要>
- ~\.codex\skills: <pull 结果,注册差集>
## 硬编码审计
- 本次修复: <清单>
- 遗留待改: <清单>
## 环境
- langgraph env: OK / 已重装 / 失败原因
- MCP 注册: <两侧各缺了什么、补了什么、抽查结果>
## 需要用户决定的事项
- <冲突、密码泄露、codex 独有 skill 去留……没有则写"无">
```
## 边界与安全
- 本 skill 会 push 到远端仓库——commit 前把将要提交的文件列表给用户过目
(自动化场景下至少写进报告)。绝不 `push --force`。
- `~\skills_env.md` 含凭据,任何情况下不将其内容或凭据字段写入仓库内文件、
commit message 或对外输出。
- 不动 `~\.codex\skills\.system\`(Codex 自带系统 skills)和
`~\.claude\skills\.system\`。
- 修改 `~\.codex\config.toml` 前先备份为 `config.toml.bak-<日期>`,TOML 写坏
会导致 Codex 起不来。
@@ -0,0 +1,68 @@
# skills_env — 本机环境档案(模板)
> 复制本文件到 `~\skills_env.md` 后逐项核对。此文件**只留在本机,不进 git**:
> 每台电脑的路径/IP/凭据不同。skills 中凡是机器相关的参数都从这里取值。
> 值为 `KEY = value` 形式,方便脚本和模型直接检索。
## 同步仓库
```
SKILLS_REPO_URL = http://git.rpchannel.com/bowei/skills-win.git
SKILLS_REPO_USER = bowei
SKILLS_REPO_PASS = <填写,或留空表示已存入 Windows 凭据管理器>
CLAUDE_SKILLS_DIR = C:\Users\<user>\.claude\skills
CODEX_SKILLS_DIR = C:\Users\<user>\.codex\skills
```
凭据说明:首次 clone 成功后运行 `git config --global credential.helper manager`,
让 Windows 凭据管理器接管,此后 `SKILLS_REPO_PASS` 可以清空。
## Python / Conda
```
CONDA_ROOT = C:\ProgramData\miniconda3
CONDA_LANGGRAPH_PYTHON = C:\Users\<user>\.conda\envs\langgraph\python.exe
```
## EDA / 硬件工具
```
VIVADO_PATH = D:\Xilinx\Vivado\2018.1\bin\vivado.bat
KICAD_PYTHONPATH = C:\Program Files\KiCad\10.0\bin\Lib\site-packages
MATLAB_MCP_EXE = C:\Users\<user>\.matlab\agentic-toolkits\bin\matlab-mcp-server.exe
```
## 板卡 / 仪器 IP
```
N9320B_HOST = 192.168.1.147 # Keysight N9320B 频谱仪
SMOD_BOARD_IP = 192.168.1.143 # SMOD 板(ATSC 激励器远程升级)
```
## MCP 注册配置(真源:MCP_DESIGN_ROOT\mcp_list.md)
```
MCP_DESIGN_ROOT = E:\MCP_design
```
每个 MCP 在 Claude Code(`~\.claude.json` 的 `mcpServers`)和 Codex
(`~\.codex\config.toml` 的 `[mcp_servers.*]`)中应使用**同一份** command/args/env。
标准配置如下(路径变量引用上文条目):
| name | command | args | env |
|------------|----------------------------------|---------------------------------------------------|-----|
| vivado-mcp | {CONDA_LANGGRAPH_PYTHON} | E:\MCP_design\vivado-mcp-main\run_server.py(Codex)/ -m vivado_mcp(Claude) | VIVADO_PATH={VIVADO_PATH} |
| mcp_n9320b | {CONDA_LANGGRAPH_PYTHON} | E:\MCP_design\mcp_n9320b\n9320b_mcp.py | N9320B_HOST={N9320B_HOST} |
| cubemx | E:\MCP_design\MCP_cubemx\cubemx-mcp\.venv\Scripts\python.exe | E:\MCP_design\MCP_cubemx\cubemx-mcp\server\main.py | PYTHONPATH=E:\MCP_design\MCP_cubemx\cubemx-mcp\server |
| kicad | node | E:\MCP_design\KiCAD-MCP-Server\dist\index.js | NODE_ENV=production; PYTHONPATH={KICAD_PYTHONPATH}; KICAD_AUTO_LAUNCH=false |
| simetrix | <conda env>\Scripts\mcp-simetrix.exe | | SIMETRIX_PROJECT_ROOT=E:\ |
| matlab | {MATLAB_MCP_EXE} | --matlab-session-mode=existing --extension-file=...\simulink\tools\tools.json | |
Codex 侧所有 stdio MCP 额外加 `PYTHONIOENCODING=utf-8`、`PYTHONUTF8=1`。
## 本机状态备注(同步时更新)
```
LAST_SYNC = <日期>
TODO = <遗留事项>
```
+113
View File
@@ -0,0 +1,113 @@
# tools-sync: mechanical git sync of the two skills checkouts.
# Claude side (~\.claude\skills) is the primary working copy: commit -> pull --rebase -> push.
# Codex side (~\.codex\skills) is a mirror checkout of the same repo: bootstrap if needed, then pull.
# Judgment calls (conflicts, codex-only skills worth keeping) are left to the caller.
param(
[Parameter(Mandatory = $true)][string]$RepoUrl,
[string]$ClaudeSkills = "$env:USERPROFILE\.claude\skills",
[string]$CodexSkills = "$env:USERPROFILE\.codex\skills",
[string]$Branch = "main",
# Bootstrap of the codex checkout stops when local files differ from the repo;
# -Force lets it overwrite them (caller must have rescued codex-only edits first).
[switch]$Force,
# Report what would happen without committing/pushing/checking out.
[switch]$DryRun
)
$ErrorActionPreference = "Stop"
function Invoke-Git {
param([string]$Dir, [string[]]$GitArgs)
# PS 5.1: git writes progress/warnings to stderr; 2>&1 wraps them in ErrorRecords
# which terminate under EAP=Stop. Relax EAP locally and stringify every line.
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
$out = & git -C $Dir @GitArgs 2>&1 | ForEach-Object { "$_" }
$code = $LASTEXITCODE
$ErrorActionPreference = $prev
return @{ Code = $code; Out = (($out -join "`n")).Trim() }
}
function Fail([string]$Msg) { Write-Host "FAIL: $Msg"; exit 1 }
# ---------- Phase 1: primary checkout (~\.claude\skills) ----------
Write-Host "=== [1/2] primary: $ClaudeSkills ==="
if (-not (Test-Path (Join-Path $ClaudeSkills ".git"))) {
Fail "$ClaudeSkills is not a git repo. Clone $RepoUrl there first (new machine: git clone $RepoUrl `"$ClaudeSkills`")."
}
$st = Invoke-Git $ClaudeSkills @("status", "--porcelain")
if ($st.Out) {
Write-Host "local changes:"; Write-Host $st.Out
if ($DryRun) {
Write-Host "(dry-run) would commit these changes"
} else {
Invoke-Git $ClaudeSkills @("add", "-A") | Out-Null
$msg = "sync: $env:COMPUTERNAME $(Get-Date -Format yyyy-MM-dd)"
$r = Invoke-Git $ClaudeSkills @("commit", "-m", $msg)
if ($r.Code -ne 0) { Fail "commit failed:`n$($r.Out)" }
Write-Host "committed: $msg"
}
} else {
Write-Host "working tree clean"
}
if (-not $DryRun) {
$r = Invoke-Git $ClaudeSkills @("pull", "--rebase", "origin", $Branch)
if ($r.Code -ne 0) {
Write-Host $r.Out
Fail "pull --rebase failed on primary. Resolve conflicts (or 'git rebase --abort') then re-run."
}
Write-Host $r.Out
$r = Invoke-Git $ClaudeSkills @("push", "origin", $Branch)
if ($r.Code -ne 0) { Write-Host $r.Out; Fail "push failed (auth? see skill Phase 1)." }
Write-Host "primary in sync with origin/$Branch"
}
# ---------- Phase 2: mirror checkout (~\.codex\skills) ----------
Write-Host "=== [2/2] mirror: $CodexSkills ==="
if (-not (Test-Path $CodexSkills)) { New-Item -ItemType Directory -Force $CodexSkills | Out-Null }
if (-not (Test-Path (Join-Path $CodexSkills ".git"))) {
Write-Host "bootstrapping mirror as a checkout of $RepoUrl"
if ($DryRun) { Write-Host "(dry-run) would git init + fetch + align to origin/$Branch"; exit 0 }
Invoke-Git $CodexSkills @("init") | Out-Null
Invoke-Git $CodexSkills @("remote", "add", "origin", $RepoUrl) | Out-Null
$r = Invoke-Git $CodexSkills @("fetch", "origin", $Branch)
if ($r.Code -ne 0) { Write-Host $r.Out; Fail "fetch failed during bootstrap." }
# Point index at origin/main without touching files, so status shows local-vs-repo drift.
Invoke-Git $CodexSkills @("reset", "origin/$Branch") | Out-Null
Invoke-Git $CodexSkills @("branch", "-f", $Branch, "origin/$Branch") | Out-Null
Invoke-Git $CodexSkills @("checkout", $Branch) 2>$null | Out-Null
$drift = Invoke-Git $CodexSkills @("status", "--porcelain")
$modified = ($drift.Out -split "`n" | Where-Object { $_ -match '^\s*(M|D)' })
if ($modified -and -not $Force) {
Write-Host "these tracked files differ locally from the repo:"
$modified | ForEach-Object { Write-Host " $_" }
Fail "codex dir has local drift. Rescue anything worth keeping into the primary checkout, then re-run with -Force."
}
$r = Invoke-Git $CodexSkills @("checkout", "-f", $Branch)
if ($r.Code -ne 0) { Write-Host $r.Out; Fail "checkout failed during bootstrap." }
Invoke-Git $CodexSkills @("branch", "--set-upstream-to=origin/$Branch", $Branch) | Out-Null
Write-Host "mirror bootstrapped at origin/$Branch (untracked codex-local files left untouched)"
} else {
if ($DryRun) {
Invoke-Git $CodexSkills @("fetch", "origin", $Branch) | Out-Null
$r = Invoke-Git $CodexSkills @("log", "--oneline", "HEAD..origin/$Branch")
Write-Host "(dry-run) mirror is behind by:"
if ($r.Out) { Write-Host $r.Out } else { Write-Host " (nothing, or not yet a checkout)" }
} else {
$st2 = Invoke-Git $CodexSkills @("status", "--porcelain")
if ($st2.Out) {
Write-Host "WARNING: mirror has local changes (should be edited on the primary side instead):"
Write-Host $st2.Out
}
$r = Invoke-Git $CodexSkills @("pull", "--rebase", "origin", $Branch)
if ($r.Code -ne 0) { Write-Host $r.Out; Fail "pull failed on mirror." }
Write-Host $r.Out
Write-Host "mirror in sync with origin/$Branch"
}
}
Write-Host "=== sync_repos done ==="