把 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.json、sessions/、浏览器 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:path 和 node:os 负责路径和 home 目录,node:util.parseArgs 负责参数,node:fs/promises 负责文件系统动作。核心能力是 fsPromises.symlink(target, path, type):它在 Windows 上接受 file、dir、junction,在其他平台上忽略这个类型参数。
目录链接的关键分支可以短到这样:
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 也能做很多跨平台路径处理,pathlib、os.symlink、os.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' },
];有了这张表,主流程就能保持稳定:
- 目标文件不存在时,必需项直接报错,可选目录跳过。
- 目标链接已经正确时,打印
exists并跳过。 - 目标位置已有普通文件、普通目录或错误链接时,先改名成
.local-backup.<timestamp>。 - 根据平台和对象类型创建 symlink、junction 或 hard link。
- 最后再跑一次 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共享运行目录。
跨平台脚本把差异固定在少数函数里。读者面对的是一个稳定入口,脚本内部负责尊重每个系统真实的文件系统规则。