Files
zcode-tooling/zcode-env-sync

ZCode 环境同步插件(zcode-env-sync)

团队共享场景:一键导出 / 导入本机 ZCode 环境,默认四项全量:

  1. 官方插件清单 — marketplace 版本 + 已启用列表(data/ 目录即启用态)
  2. 自研插件源码 — ~/.zcode/plugins/* 整体按字节打包:你自己写的插件全在里面 (如本插件自身、zcode-tps、zcode-usage,以后新增的也自动带上); node_modules/.git 排除,以 . 开头的备份目录不算插件, 两个插件 manifest 同名直接报错(否则快照里互相覆盖)
  3. MCP 配置 — cli/config.json 的 mcp.servers + plugins.dirs,路径占位符化 (gitea 的 -H 地址、GITEA_ACCESS_TOKEN_FILE 指针都在这,身份各用各的见下)
  4. 密钥束 — ~/.dsh/secrets/* + ~/.ssh/ssh-profiles.json,AES-256-GCM 加密后才进仓 (含 gitea-token.txt 本体)

可选第 5 项(ZCODE_SYNC_WITH_MODELS=1):模型 provider 脱敏快照,见下。

后端:Gitea 私仓 root/zcode-env-sync。

安装

cp -r <本目录> ~/.zcode/plugins/zcode-env-sync
# 把该路径追加进 ~/.zcode/cli/config.json 的 plugins.dirs,然后重启 ZCode

用法

命令 动作
/sync-export 导出本机快照 → 推 Gitea 私仓
/sync-import 从私仓拉快照 → 还原到本机(先自动备份)
/sync-status 对比本机与远端快照差异(只读)
/sync-list 列出远端全部快照,标出 latest 指向哪个(只读)
/sync-verify 校验快照完整性:hash / 密钥束 / 占位符(只读,只比哈希不落盘)
/sync-prune 清理远端旧快照(默认只预演,--apply 才真删)
/sync-doctor 自诊断:一次查完前置条件,告诉你同步为什么不能用(只读)

排查顺序:/sync-doctor → /sync-verify → /sync-status。 先说"同步不工作"就跑 doctor(缺口令、latest 解不开、追踪引用分叉都在这层); 怀疑快照坏了跑 verify(它会用当前口令试解 secrets.enc); 只想看差异跑 status。

七个命令统一用 SYNC_PLUGIN 自动定位脚本,命令文档里不写死绝对路径—— 快照要跨机器还原,写死的路径到了别的机器就是废的:

SYNC_PLUGIN="${ZCODE_SYNC_PLUGIN:-$(find ~/.zcode/plugins -maxdepth 3 -name export.mjs -path '*env-sync*' | head -1)}"
node "$SYNC_PLUGIN"

口令:环境变量 ZCODE_SYNC_PASSPHRASE(团队线下约定,不进仓)。 未设置或短于 8 位时脚本直接报错退出,不会用弱默认口令。

快照保留与清理

node prune.mjs                      # 预演:列出将删哪几个、各多大、共释放多少
node prune.mjs --keep 5             # 预演,只保留最近 5 个
node prune.mjs --keep 5 --apply     # 真删并推送
node prune.mjs --exclude <快照名>    # 钉住某个快照(可重复)

永远不动:latest.json 指向的那个、最新的那个、--exclude 钉住的。 删完若 latest 指向被删的快照会自动重指到存活的一个。ZCODE_SYNC_KEEP 设默认保留数。

快照校验

node verify.mjs              # 校验 latest 指向的那个
node verify.mjs <快照名>      # 校验指定快照
node verify.mjs --all        # 扫全部:逐个标「结构OK / 可解密 / 口令不符」,给出能用的是哪个
node verify.mjs --strict     # 有警告也算失败(退出码 1)

--all 用在团队仓里混着不同口令导出的快照时:GCM 认证失败无法区分"口令错"与"密文损坏", 它把每个都拿当前口令试一遍,直接告诉你 /sync-import <哪个> 能用。

快照结构

snapshots/<hostname>-<yyyyMMdd-HHmmss>/
  manifest.json            # 清单:官方插件/自研插件 hash/MCP 名/密钥指针/provider 摘要
  mcp.servers.json         # 占位符化后的 MCP 配置(明文)
  plugins-dirs.json        # plugins.dirs 占位符化(明文)
  v2.providers.json        # 仅 WITH_MODELS=1:provider 脱敏版(密钥位置是占位符)
  custom-plugins/          # 自研插件源码(明文,团队内私仓)
  secrets.enc              # 密钥束 AES-256-GCM 密文
  secrets.manifest.json    # 密钥清单(只有路径+sha256,无值)
latest.json                # 指向最新快照

跨机器路径还原

导出时把本机绝对路径替换为占位符,导入时按本机实际路径反向替换:

占位符 含义
${ZCODE_HOME} / ${ZCODE_HOME_WIN} ~/.zcode(正/反斜杠两种写法)
${DSH_HOME} / ${DSH_HOME_WIN} ~/.dsh
${USER_HOME} / ${USER_HOME_WIN} 用户主目录
${USERS_POSIX} / ${USERS_WIN} Users/<用户名> 片段

替换按 JSON 值逐个进行,不是对序列化文本做字符串替换—— 文本里反斜杠是转义态(\\Users\\),对文本替换会整片漏掉。

机器特有的 host/port/command 可写 ~/.zcode-env-sync/overrides.json(不进仓), 导入时自动深度合并覆盖。

模型 provider(默认关)

~/.zcode/v2/config.json 里通常是活的 apiKey,所以默认完全不碰(快照里无模型)。 需要同步时导出导入两边都显式开:

ZCODE_SYNC_WITH_MODELS=1 node export.mjs   # 导出:脱敏结构明文 + 真密钥进加密束
ZCODE_SYNC_WITH_MODELS=1 node import.mjs   # 导入:结构合并 + 密钥回填

导出:v2.providers.json 脱敏版明文(密钥位置是 ${MODEL_SECRET_REF}), 真密钥以 MODELS/<provider>/<key> 条目并入 secrets.enc; manifest 只记摘要 + 密钥哈希(modelKeys),不记值。

导入(ZCODE_SYNC_WITH_MODELS=1 时才动 v2/config.json,先备份):

  • 快照有、本机无的 provider:整体新增
  • 已有 provider:只补缺的模型定义与非密钥字段,不碰本机已有密钥
  • 密钥回填:只填空位(占位符/空/过短);本机已有密钥默认跳过保护, 确需覆盖再加 ZCODE_SYNC_OVERWRITE_MODEL_SECRETS=1
  • 选中态(当前用哪个 provider/model)默认不动, 加 ZCODE_SYNC_APPLY_MODEL_SELECTION=1 才应用(同样先备份 v2/setting.json)
变量 说明
ZCODE_SYNC_OVERWRITE_MODEL_SECRETS=1 允许用快照密钥覆盖本机已有密钥(默认跳过保护)

内置闸门

导出过程有数道自检,命中即中断且不推送:

闸门 行为
明文区出现真密钥值 硬失败(扫全部非 secrets.enc 文件)
配置区残留未占位符化的绝对路径 硬失败(按解析后的值判断,不限于本机用户名)
provider 脱敏不彻底 硬失败
自研插件 manifest 同名 硬失败(否则快照里互相覆盖)
自研插件源码里的硬编码路径 警告并列出文件(源码按字节归档,不擅自改写)

放行开关:ZCODE_SYNC_ALLOW_SECRETS(逗号分隔路径白名单)、ZCODE_SYNC_ALLOW_PATH_LEAK=1。

环境变量

变量 默认 说明
ZCODE_SYNC_PASSPHRASE — 必填,加密/解密密钥束
ZCODE_SYNC_REPO root/zcode-env-sync 私仓坐标
ZCODE_SYNC_HOST https://gitea.mymoyu.top 托管地址
ZCODE_SYNC_WORK ~/.zcode-env-sync 本地工作区(repo clone + backup)
ZCODE_SYNC_WITH_MODELS 关 1 时连带同步 provider(脱敏)
ZCODE_SYNC_APPLY_MODEL_SELECTION 关 1 时导入应用模型选中态
ZCODE_SYNC_SKIP_SECRETS 空 逗号分隔的密钥 rel,导入时跳过不落盘(只报告)。团队场景:连接方式统一、身份各用各的。例:ZCODE_SYNC_SKIP_SECRETS='DSH_SECRETS/gitea-token.txt'(每人保留自己的 gitea token,只同步 -H 地址等配置);模型密钥用 MODELS/<provider>/<key> 格式
ZCODE_SYNC_NO_PUSH 关 1 只落本地,不推远端
ZCODE_SYNC_GIT_REMOTE 自动 覆盖远端地址(测试用)
ZCODE_SYNC_APPLY_OFFICIAL 开 0 跳过官方插件差异报告
ZCODE_SYNC_KEEP 10 /sync-prune 默认保留数量
ZCODE_SYNC_VERIFY_STRICT 关 1 等价于 verify.mjs --strict(有警告即失败)
ZCODE_SYNC_ALLOW_PATH_LEAK 关 1 放行路径残留自检(确认无害时才用)

换行符与字节一致性

快照是字节级归档:仓库里锁了 .gitattributes(* -text)并把本仓 core.autocrlf 设为 false。否则 Windows 上常见的 core.autocrlf=true 会在 add/checkout 时改写 CRLF,导致 manifest 里的 hash 与克隆下来的内容对不上, /sync-status 会永久报「版本不同」。

测试

npm test            # 63 项:单元 + 沙箱 E2E + 新命令(prune/verify/doctor/list)
npm run test:core   # 纯函数单测(35 项)
npm run test:e2e    # 沙箱端到端(11 项,伪造两台机器 + 本地裸仓)
npm run test:tools  # 新命令(17 项:prune/verify/doctor/list/推送隔离)
node tests/live-roundtrip.mjs   # 真机往返(需私仓已建 + 口令)

E2E 用 USERPROFILE 把 HOME 重定向到临时目录、用本地裸仓冒充 Gitea, 全程不碰真实 HOME、不走网络。覆盖:跨机路径还原、密钥解密落地、覆盖前备份、 密钥/路径泄漏闸门、CRLF 字节完整性、provider 脱敏与密钥跨机回填; tools 那组覆盖 prune 保留策略与 latest 重指、verify 的 hash 篡改检测与口令不符分支、 doctor 的三种 fail 场景、status --list、以及推送隔离 (git add 只加本次快照,不把别的会话未推送的快照连带推上去)。

故障排查

先说一句:跑 /sync-doctor,它把这页大部分症状直接指出来并给修复命令。

现象 原因与处理
未设置 ZCODE_SYNC_PASSPHRASE 先 export ZCODE_SYNC_PASSPHRASE='...'(≥8 位)
密钥束解密失败:口令不对或文件损坏 双方口令不一致,或 secrets.enc 被改过。先 node verify.mjs --all 看当前口令能用哪版
推送失败 私仓没建 / token 无写权限;token 读 ~/.dsh/secrets/gitea-token.txt
token 权限不足建仓 Gitea 个人建仓需 write:user;管理员可用 POST /api/v1/admin/users/<user>/repos
自研插件名冲突 两个插件的 plugin.json 里 name 相同,改名或移走一个
status 一直报「版本不同」 检查仓库 .gitattributes 是否为 * -text、core.autocrlf 是否为 false
配置区残留未占位符化的绝对路径 配置里有写死的绝对路径,改成占位符或确认后设 ZCODE_SYNC_ALLOW_PATH_LEAK=1
HEAD != origin/main 本地与远端分叉,先 /sync-status 看差异再决定推还是拉
快照太多占空间 node prune.mjs --keep 10 预演,确认后加 --apply
导出报告「落下 N 项非本次内容」 正常:仓库里有别人的未推送内容,git add 只加本次快照。那些文件仍在本地,不会丢
latest 指向的快照解不开 该快照可能用了别的口令。node verify.mjs --all 找出当前口令能用的版本,再 /sync-import <快照名>

安全说明

  • 除 secrets.enc 外不含任何密钥明文;导出时有脱敏扫描,命中即中断
  • git 网络操作使用带 token 的临时 URL,origin 只存干净地址,token 不落 .git/config
  • 密钥还原时拒绝目录穿越与绝对路径,权限设为 600
  • 私仓保持 private;换口令即换 ZCODE_SYNC_PASSPHRASE 后重导
  • 导入前自动备份被覆盖文件到 ~/.zcode-env-sync/backup/<时间>/,报告里给出实际路径