把 Codex Home 迁移做成跨平台 Node 脚本

整理 Codex Home Git 同步时,第一版迁移命令很自然地写成了两套:macOS / Linux 用 ln -s,Windows 用 PowerShell 处理 symbolic link、junction 和 hard link。

这能把事情做成,但文章读起来会让人先选平台,再选命令。对一次迁移来说还好;对一个要长期在多台设备之间复用的工作流来说,平台判断最好进入脚本内部。读者真正需要记住的是同一个入口:

node tools/setup-codex-home.mjs --repo "$HOME/code/codex-home" --dry-run
node tools/setup-codex-home.mjs --repo "$HOME/code/codex-home"

Windows PowerShell 里仍然是这支脚本:

node .\tools\setup-codex-home.mjs --repo "$HOME\Desktop\code\codex-home" --dry-run
node .\tools\setup-codex-home.mjs --repo "$HOME\Desktop\code\codex-home"

跨平台的意思是读者面对同一个脚本入口,脚本在不同系统上选择合适的文件系统能力。

先定迁移边界

这个脚本解决的是一个很窄的问题:把私有仓库里的可迁移资产挂回 Codex 的运行目录。

~/.codex/AGENTS.md          -> codex-home/global/AGENTS.md
~/.codex/agents_references  -> codex-home/agents_references
~/.codex/rules              -> codex-home/rules
~/.codex/skills             -> codex-home/skills
~/.codex/env                -> codex-home/env
~/.codex/tools              -> codex-home/tools

它不搬 auth.jsonsessions/、浏览器 profile、sqlite、插件缓存和日志。这些内容属于当前机器的运行现场,新机器上应该由 Codex 自己重新生成。

边界定窄以后,脚本就不需要理解 Codex 的全部状态。它只需要做路径检查、备份旧入口、创建链接和打印验证结果。

链接原语不能混成一个概念

迁移脚本看起来只是在做「链接」,不同系统里的链接却不是同一种东西。

平台 / 对象 推荐动作 原因
macOS / Linux 文件 symbolic link POSIX 语义直接,权限和工具链都熟悉。
macOS / Linux 目录 symbolic link 目录 symlink 是常规能力,ln -s 就能表达。
Windows 目录 junction 目录 junction 更贴近 Windows 常见无管理员迁移场景。
Windows 文件 symbolic link,失败后同盘 hard link 文件 symlink 可能受权限或开发者模式影响;hard link 可以作为同一 NTFS 卷内的兜底。
WSL 按 Linux 处理 WSL2 是独立 Linux 环境,有自己的 ~/.codex 和文件系统语义。

这里最容易误判的是 Git Bash。Git Bash 能让很多类 Unix 命令在 Windows 里跑起来,但它没有把 NTFS 变成 POSIX 文件系统。ln -s 最后创建什么,仍然取决于 Windows 权限、MSYS 配置和目标类型。迁移脚本要长期复用时,把这层判断写进代码,比把一段 shell 交给兼容层更清楚。

为什么选 Node

这个场景里,Node 的优势落在标准库上:它刚好能直接表达问题。

node:pathnode:os 负责路径和 home 目录,node:util.parseArgs 负责参数,node:fs/promises 负责文件系统动作。核心能力是 fsPromises.symlink(target, path, type):它在 Windows 上接受 filedirjunction,在其他平台上忽略这个类型参数。

目录链接的关键分支可以短到这样:

const type = process.platform === 'win32' ? 'junction' : 'dir';
await symlink(targetPath, linkPath, type);

文件链接则多一层 fallback:

try {
  const type = process.platform === 'win32' ? 'file' : undefined;
  await symlink(targetPath, linkPath, type);
} catch (error) {
  if (process.platform !== 'win32') {
    throw error;
  }
  await link(targetPath, linkPath);
}

Python 也能做很多跨平台路径处理,pathlibos.symlinkos.link 都可用。问题在于 Windows junction 不是 Python 标准库里一个同等直接的参数;要完整覆盖,通常要调用 Windows 命令、PowerShell 或 Win32 API。PowerShell 处理 Windows 链接很强,但到了 macOS / Linux 又会变成另一套入口。

这个脚本只是仓库里的本地迁移工具,不值得引入完整 CLI 框架,也不需要 TypeScript 构建链路。零依赖 .mjs 文件已经够用:读者 clone 仓库后,只要本机有 Node,就能先 dry-run,再真实执行。

用声明式映射驱动流程

脚本的核心先是一张映射表。要挂载的资产、目标路径和对象类型都收在这里:

const linkSpecs = [
  { kind: 'file', name: 'AGENTS.md', target: path.join('global', 'AGENTS.md'), required: true },
  { kind: 'dir', name: 'agents_references', target: 'agents_references' },
  { kind: 'dir', name: 'rules', target: 'rules' },
  { kind: 'dir', name: 'skills', target: 'skills', preserveSystemSkills: true },
  { kind: 'dir', name: 'env', target: 'env' },
  { kind: 'dir', name: 'tools', target: 'tools' },
];

有了这张表,主流程就能保持稳定:

  1. 目标文件不存在时,必需项直接报错,可选目录跳过。
  2. 目标链接已经正确时,打印 exists 并跳过。
  3. 目标位置已有普通文件、普通目录或错误链接时,先改名成 .local-backup.<timestamp>
  4. 根据平台和对象类型创建 symlink、junction 或 hard link。
  5. 最后再跑一次 verify,确认链接能解析到仓库里的目标。

这个结构比把六条链接命令展开在三个平台小节里更容易维护。以后新增 templates/prompts/,只需要加一条映射;平台分支仍然留在统一的创建函数里。

幂等比命令短更重要

迁移脚本会改用户 home 目录,短命令不是第一目标,能重复执行才是第一目标。

--dry-run 是第一层保护。新机器上先看脚本准备做什么,再决定要不要真实执行。它应该明确打印即将创建的链接、即将备份的旧路径和会跳过的缺失目录。

备份是第二层保护。已有 ~/.codex/AGENTS.md~/.codex/skills~/.codex/rules 时,脚本不直接覆盖,而是改名成 .local-backup.<timestamp>。新机器如果已经写过自己的 Skill 或规则,后面还能从备份目录里合并。

.system/ 是第三个边界。Codex 可能会在 ~/.codex/skills/.system/ 下生成系统 Skill;整个 skills/ 链到仓库后,.system/ 会出现在仓库工作区里。脚本可以在链接前保留这个目录,仓库再用 .gitignore 忽略它。这样 Codex 能继续读系统 Skill,Git 也不会把运行态收进去。

还有一条更朴素的规则:脚本只移动自己负责的几个入口,不扫描整个 ~/.codex,也不删除 auth.json、session、sqlite、cache 或浏览器 profile。迁移资产和清理运行态是两件事,放在同一个脚本里会扩大风险面。

WSL 当成另一台 Linux 机器

WSL2 适合 Linux-native 工具链,尤其是项目、包管理器、Docker/Linux 命令和线上环境都贴近 Linux 的场景。Codex 的 WSL 文档也建议把仓库放在 WSL 的 home 目录下,避免 /mnt/c/... 带来的性能和权限问题。

这意味着 WSL 里的迁移方式按 Linux 处理:

mkdir -p ~/code
git clone <private-repo-url> ~/code/codex-home
cd ~/code/codex-home
node tools/setup-codex-home.mjs --repo "$PWD" --dry-run
node tools/setup-codex-home.mjs --repo "$PWD"

Windows 原生 Codex 和 WSL Codex 各自有自己的 ~/.codex。如果两边都要使用同一套配置,我更愿意让它们各自 clone 同一个私有仓库,再通过 Git 同步,而不是让两个运行目录互相指。这样 Windows 的登录态、浏览器 profile 和插件缓存留在 Windows;WSL 的运行态留在 Linux;共享的只有仓库里的可迁移资产。

总结

这次选型留下的规则很简单:

  • 跨平台迁移优先做成一个脚本入口,平台差异收进实现。
  • 文件系统动作优先用标准库;标准库能表达 junction 这种平台差异时,不要额外引入依赖。
  • 会改 home 目录的脚本必须支持 dry-run、备份、跳过正确链接和最终验证。
  • Windows、macOS、Linux 可以共享资产仓库,但运行态留在各自环境。
  • WSL 按 Linux 机器看待,不和 Windows 的 ~/.codex 共享运行目录。

跨平台脚本把差异固定在少数函数里。读者面对的是一个稳定入口,脚本内部负责尊重每个系统真实的文件系统规则。