
我手里有三台工作机器、一台家用机器还有团队里另外四位同事的开发环境需要统一。每次换电脑或者有新人入职最烦的就是重新装终端工具、配 shell 别名、设置编辑器快捷键——这事儿说大不大但真做起来零零碎碎能折腾掉半天。后来我把这套流程全塞进了一个叫 t3code 的命令行工具里一条命令跑完整个开发环境从终端配置到常用代码模板全部就位。这篇文章就是把我从零开始搭建、反复踩坑、最后稳定用了大半年的经验完整写下来包括配置文件的每个字段含义、模板设计思路、跨平台同步的坑以及团队协作时的版本管理方式希望对正在折腾开发环境自动化的朋友有点帮助。1. 为什么要用 t3code 管理开发环境我的真实痛点先聊一个反直觉的结论开发环境配置这件事恰恰是越熟练的工程师越容易忽略的地方。因为熟练了每台机器上装什么工具、改什么配置都成了肌肉记忆反而不会系统性地记录和沉淀。直到某天自己的主力笔记本硬盘报废、或者新入职同事在你旁边干坐了四个小时等着环境装完、或者你发现自己在两台机器上维护的.zshrc已经分叉到完全对不上时才会意识到这本质上是个工程问题。1.1 团队协作场景下的隐性成本我算过一笔账。团队里日常开发涉及的工具包括但不限于Git 全局配置、SSH key 生成与注册、Node/Rust/Python 三个语言环境、Docker 与 docker-compose、十个左右的失效别名、neovim 的插件与键位、VS Code 的 settings.json 和快捷键、终端外观主题、以及四个不同语言项目的脚手架模板。这些东西手动装一遍的平均耗时在三个小时以上而且每个人装出来的结果不可能一样——有人装的是 Docker Desktop 稳定版有人是 edge 版有人 npm 全局包用 yarn 装的有人用 pnpm。差异意味着问题排查时要多考虑一层环境不一致的因素这成本很难量化但真实存在。1.2 为什么不能直接复制配置文件可能有人会说把.zshrc、settings.json这些文件直接同步一份不就完了我在早期确实是这么干的但很快发现几个硬伤不同机器的操作系统不完全一样macOS 上的brew和 Linux 上的apt安装路径、软件包名全不一样直接同步必然会挂。配置文件里有大量机器相关的绝对路径比如某个 Python 虚拟环境的路径、某些本地工具的安装目录直接复制会指向一个不存在的位置。项目级别的模板和全局配置文件是两码事。全局配置你想随时修改、提交、审阅而项目模板通常只有脚手架相关的那几个文件它们的生命周期和更新策略完全不同。所以我真正需要的是一个声明式描述开发环境的工具我在一个统一的描述文件里写清楚我需要装什么、配置什么、生成什么工具负责解析并执行。t3code 就是围绕这个思路设计的。它的核心价值不在于取代手动操作本身而在于把手动操作变成了可版本化、可审查、可复现的流程。2. 安装与基础概念先弄清楚 t3code 是怎么工作的t3code 的安装方式并不特殊它本质上是一个 Node.js 编写的命令行程序通过 npm 全局安装即可。但只停留在装得上层面是没有意义的重点在于理解它的运行模型。npm install -g t3code t3code --version安装完之后先跑一下t3code --help你会发现它的核心命令只有几个t3code init用于生成初始的配置文件t3code apply用于执行配置t3code plan用于预览将要执行的操作t3code sync用于从远程模板仓库拉取更新。命令不多但设计上有讲究。2.1 声明式配置与命令式操作的区别理解 t3code 的核心要抓住声明式这三个字。传统的做法比如你写一个setup.sh里面是一串apt install、npm i -g、echo alias xy ~/.zshrc这叫命令式——它描述的是怎么做。t3code 走的是另一条路它让你描述最终要变成什么样具体怎么做由工具自己决定。举个例子在t3code.yaml配置文件里我想让机器上有一个名为freeman的 Git 提交身份并且配置了rebase策略我不会写git config --global user.name freeman而是写git: config: user.name: freeman pull.rebase: truet3code 会自己判断如果这个配置已经生效就直接跳过如果没有则执行写入。这套逻辑的好处非常明显——操作是幂等的。你可以在同一台机器上反复执行t3code apply一百次结果都是一样的不会像 shell 脚本那样多次追加同一行配置导致冲突。再比如我想确保 Docker 已安装我要做的也不是写sudo apt install docker而是声明一个需要存在的包packages: - name: dockert3code 会检查当前系统类型macOS 用 brewDebian/Ubuntu 用 aptFedora 用 dnf然后调用对应的包管理器去安装。你不需要在配置里写任何分支逻辑工具自己处理跨平台的差异。这一点是我最后决定长期用它而不是继续维护 shell 脚本的最大原因。2.2 初始化配置文件的完整过程执行t3code init时它会交互式问你几个问题当前机器的操作系统、常用的包管理器、是否初始化 Git 全局配置、是否需要 Docker 环境、是否生成 SSH key、使用哪种 shellzsh/bash/fish。回答完之后生成一个初始的t3code.yaml。初始化生成的配置长这样version: 1.0 system: os: auto shell: zsh packages: common: - curl - git - jq macos: - wget - ripgrep shell: fallback: false hooks: [] git: config: user.name: freeman user.email: devexample.com注意system.os写成auto表示允许 t3code 每次运行时自动检测操作系统这样同一份配置就能在多台机器上复用。packages下面分了common和macos两个组common在所有操作系统上都会安装macos只在检测到 macOS 时执行。如果是 Linux 机器不想装 macOS 相关的包额外加一个linux组就行。2.3 找到对应配置文件保留的位置t3code 的默认配置路径是~/.config/t3code/t3code.yaml。如果你有多套配置场景比如个人开发一套、公司开发一套可以通过环境变量T3CODE_CONFIG_FILE指定加载不同的配置文件例如T3CODE_CONFIG_FILE~/work/t3code/t3code.yaml t3code apply这个能力在团队场景很重要我在后面讲多人复用时会专门展开。3. 深度拆解 t3code.yaml每个字段到底在控制什么这是 t3code 的核心知识点也是大家最容易懵的地方。我把实际使用中最高频的字段逐个讲清楚包括它们存在的原因、含义、以及我在实践中的推荐写法。3.1 packages 字段跨平台依赖管理的关键packages字段支持三个内置标签common、macos、linux。t3code 在运行时先检测系统类型再合并common和对应系统的包列表最后调用当前系统默认的包管理器完成安装。packages: common: - git - curl - htop linux: - build-essential - libssl-dev macos: - watchman这里有一个实际经验不要试图在common里塞太多东西。很多包在不同系统上的名字不一样比如build-essential是 apt 里的名称macOS 对应的其实是 Xcode Command Line Tools包名完全不同。如果你把它们都写进common在 macOS 上执行时 t3code 会去找一个不存在的build-essential最终报错。正确的做法是只有真正在所有平台下名称一致的包才放common其余一律根据平台分组。t3code 对已安装的包有校验逻辑。所谓安装成功不是指apt或brew返回了退出码 0而是指 t3code 会在安装后执行一次which 包名或command -v 包名来确认它确实出现在 PATH 里。这一点我觉得设计得很细心因为某些安装器即使失败也会返回一个 0 退出码。3.2 git 字段全局 Git 配置的声明式管理git字段是我用得最多的。它的语法直接对应用 Git 的--global配置项git: config: user.name: freeman user.email: devexample.com init.defaultBranch: main pull.rebase: true core.editor: nvimt3code 在执行时会把key: value逐行写入~/.gitconfig。和手动执行的差别在于如果配置文件里删除了某一个 keyt3code 也会主动从~/.gitconfig里移除它。这一点是纯粹的 shell 脚本很难做到的——shell 脚本一般只会git config --global设置值但不会记得清理不存在的旧配置。同时git.aliases子字段定义别名git: aliases: st: status co: checkout br: branch lg: log --oneline --graph --all --decorate这些别名实际上还是会落到~/.gitconfig的[alias]段格式和手动配置完全一致。唯一好处还是那句——集中管理多台机器保持一致。3.3 ssh 字段密钥生成与自动注册开发环境里一个新人和一个新电脑最容易卡住的就是 SSH key。t3code 对这个场景的支持是把整个链路都收了ssh: ensure: true type: ed25519 comment: work-laptop-2024 github: true执行t3code apply时如果~/.ssh/id_ed25519不存在t3code 会生成一对新的 ed25519 密钥。github: true意味着生成的公钥会被尝试注册到 GitHub 账户——当然这一步需要你提前在系统里配置过gh或其他认证工具否则它会停下提示你手工授权。我的建议是把type固定为ed25519不要用rsa。目前绝大多数代码托管平台都支持 ed25519它的密钥更短、生成速度更快、安全性也在线没必要再用老旧的 RSA 2048。3.4 shell 字段alias、环境变量与 PATH 的增量管理shell字段控制所有与 shell 相关的配置写入。默认情况下t3code 使用 fallback 策略如果检测到当前用户使用的是 zsh就写~/.zshrc如果是 bash就写~/.bashrc。这样可以保证你不需要把所有机器都切换到同一个 shell。shell: env: EDITOR: nvim LANG: en_US.UTF-8 aliases: ll: ls -alF la: ls -A tf: tail -f gs: git status path: - ~/bin - $HOME/.local/binshell.env里定义的变量会被写入到 shell 启动脚本里shell.path里的目录会被追加到PATH环境变量。需要注意这几块都是增量追加式——t3code 不会把原有的alias或env清掉它只负责确保这几项存在。如果某个 alias 已经存在且值和配置里不一样它会警告你不会强行改掉。不过我建议在shell字段里不要写太多 Python 相关的东西。我遇到过一个问题配置里写了export PYTHONPATH/some/path后来发现和其他工具自动追加的路径互相干扰排查了很久。现在我的原则是——只在 t3code 里声明机器级别的稳定配置项目级别的环境变量一律交给 direnv 之类的工具去处理。3.5 files 字段把模板文件变成真实文件files字段是 t3code 最有价值、也最容易被低估的能力。它允许你定义一组文件和对应的内容t3code apply时会把这些内容写入到目标路径files: - path: ~/.config/neovim/init.vim content: | set number set relativenumber set tabstop2 set shiftwidth2 set expandtab这本质上是一个单机文件管理。如果只有配置文件的原样拷贝需求用它是够的。但如果你想通过模板变量来生成文件就需要 t3code 的模板目录功能这个我会在第 4 章专门讲。3.6 hooks 字段处理特殊步骤的逃生舱口t3code 再怎么说也没办法覆盖所有人的定制需求所以它留了hooks字段——允许你在 apply 流程的不同阶段执行自定义脚本hooks: after: - name: install-vim-plug command: sh -c \curl -fLo $HOME/.local/share/nvim/site/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim\after表示在配置应用完成之后执行。还有before和fail两个阶段。before在配置应用前执行fail在流程出错时执行。我通常在after阶段放两类东西一是无法用包管理器安装的工具如 vim-plug、oh-my-zsh二是需要等待文件生成完才能执行的操作如编译某个插件。4. 从一份空配置到一台能用的开发机完整实操记录这一章我写一个从零开始的完整流程用一台全新的 macOS 机器演示然后补充说明在 Linux 机器上会有什么不同。这里的每一步都是我自己实际操作过的你可以照着做。4.1 安装 Node.js 与 t3code 本体macOS 上装 Node.js 最省事的方式是 brew/bin/bash -c $(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh) brew install node npm install -g t3codeLinux 上用 apt 或 dnf 直接装 Node 也行但注意 20.04 之类的老版本 Ubuntu 自带的 node 可能版本太低建议装 NodeSource 的源之后apt install nodejs。安装完成后跑一下t3code init按提示初始化。注意即使你打算后面用现成的配置模板也建议先跑一次 init因为 t3code 会生成必要的基础目录结构和默认模板。4.2 设计自己的 t3code.yaml 配置文件以下是我的 m1 笔记本上实际使用的配置已做部分脱敏可以作为一个参考骨架version: 1.0 system: os: auto shell: zsh packages: common: - git - curl - jq - htop - ripgrep - tmux macos: - wget - watchman linux: - build-essential git: config: user.name: freeman user.email: devexample.com init.defaultBranch: main pull.rebase: true aliases: st: status co: checkout ci: commit br: branch ssh: ensure: true type: ed25519 github: true shell: env: EDITOR: nvim aliases: ll: ls -alF gs: git status gd: git diff files: - path: ~/.config/git/ignore content: | .DS_Store *.class node_modules/ dist/ coverage/ hooks: after: - name: install-oh-my-zsh command: sh -c \$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\ --unattended这个配置没有覆盖所有可能性但它体现了我认为最重要的设计原则配置只描述稳定的、长期有效的环境状态不描述一次性的临时操作。4.3 应用配置与验证结果执行t3code apply第一次执行时t3code 会依次完成这些事情安装包、写 git 配置、生成 ssh key、追加 shell 配置、写入文件、执行 hook。整个过程大概耗时五分钟取决于你的网络速度。执行完毕建议自己做一轮验证不要迷信工具的输出which git jq tmux git config --global user.name ls -la ~/.ssh/ nvim ~/.zshrc我专门把验证这一步写出来是因为在实际使用中遇到过 t3code 输出写着成功但某项配置并没有真正生效的情况。最典型的是 shell 配置t3code 往~/.zshrc追加了内容但你当前会话的PATH还是旧的必须新开一个终端窗口才能生效。这不是 bug而是 shell 环境的固有特性——启动时读取一次配置文件之后不会自动重载。4.4 在 Linux 机器上的差异说明换到一台 Ubuntu 22.04 上同一个配置文件不是直接就能跑的需要先改两处第一把packages.macos里的watchman删掉或挪走因为apt里没有watchman虽然有源码可以编译但那不符合 t3code 的自动化场景。第二确认brew不在路径里t3code 会自动用apt。因为 t3code 的包管理器选择逻辑是macOS 默认走brewLinux 走/etc/os-release里的 ID 来选apt还是dnf。所以如果想做到真正的一套配置走天下需要在处理包时更小心。我的做法是维护两套分支macOS 的放macosLinux 的放linux绝不把它们混进common。这也是配置文件一开始就要考虑好的结构。5. 多机器与团队场景模板目录和版本化工作流和团队合作时t3code 的最大价值不是你个人省了配置时间而是全团队的环境可以复用同一套配置模板。这里就需要引入 t3code 的模板目录概念。5.1 模板目录的组成结构在配置文件的同级目录下可以放一个_templates/文件夹。里面按照树形结构组织文件文件名支持{{.变量名}}的模板语法~/.config/t3code/ ├── t3code.yaml └── _templates/ ├── gitignore.go.txt ├── config.ts.txt └── env.zsh.txt这些.txt后缀是 t3code 用来识别这是一个模板不要原样复制的约定。模板内部可以通过{{.ProjectName}}、{{.ProjectDomain}}这样的占位符定义变量。5.2 执行模板生成一条命令出新项目用 t3code 的生成命令可以快速创建新项目t3code new my-project --template gitignore.got3code 会把_templates/gitignore.go.txt渲染成my-project/.gitignore同时根据命令行参数自动填充模板变量。如果模板里定义了变量而命令行没有提供t3code 会在生成前交互式询问。我在团队里维护了四个常用模板模板文件生成的目标文件用途gitignore.node.txt.gitignoreNode.js 项目的忽略规则gitignore.go.txt.gitignoreGo 项目的忽略规则env.ts.txt.env.tsTypeScript 项目的环境变量示例文件main.rs.txtsrc/main.rsRust 项目的入口文件骨架5.3 用 Git 仓库同步配置而不是复制文件团队场景下正确的工作流是把整个 t3code 配置目录放进一个 Git 仓库。每个成员把仓库克隆下来然后通过T3CODE_CONFIG_FILE指向仓库里的配置文件即可。projects/ └── team-env/ ├── t3code.yaml └── _templates/ ├── gitignore.node.txt └── gitignore.go.txt然后每个人在本地执行git clone gitgithub.com:your-org/team-env.git T3CODE_CONFIG_FILE$(pwd)/team-env/t3code.yaml t3code apply这套模式有一个非常大的优势变更可以走代码审查。比如团队决定默认分支从master改成main或者新增一个统一的 git alias只需要在仓库里改配置并提交 PR大家 review 通过后合并其他成员执行t3code apply就自动同步了。比起群聊里发一句大家记得执行一下这个命令这种方式要可靠得多而且每一条变更都留痕。5.4 自动更新时的执行差异t3code apply在执行时会自动比较远程仓库和本地仓库的差异只对发生变化的部分做增量更新——这一点在多次执行中很重要。比如你新增了一个别名其他成员已有的配置里没有t3code 就会只追加这一条不会重新走一遍全量安装流程。6. 我踩过的几个坑给正在用或准备用的人提个醒工具用久了总会积累一些文档里不会写的实战经验。这里集中写几条我觉得最容易让新人栽跟头的。6.1 macOS 上 CLI 工具签名问题的处理macOS 对从网络下载的 CLI 工具有签名检查。t3code 在配置里要求安装某些工具时包管理器能装、但执行which校验那一步可能因为 Gatekeeper 拦截而失败。解决办法是在被拦截的执行文件上手动允许一次sudo xattr -d com.apple.quarantine /path/to/binary但注意这是个不推荐的命令级操作正常情况下 brew 或 npm 安装的工具不会触发。如果真遇到说明你的包下载途径被 macOS 标记了谨慎处理不要盲目对所有文件执行xattr清除。6.2 shell 别名覆盖系统命令的冲突我在shell.aliases里写过pip: pip3目的是为了在默认 Python 是 2.x 时代的朋友机器上统一版本。结果有一台机器上已经存在pip这个命令t3code 没有清理它两个定义冲突导致每次在 shell 里输入pip都提示command not found。后来我把所有覆盖默认值的 alias 全部从 t3code 配置里移除。我的建议是alias 只加不减只写新增、常用、无歧义的别名比如ll、gs、gd这种不去尝试篡改系统已有的命令。否则维护成本会迅速上升。6.3 hook 脚本执行路径的不确定性t3code 的 hook 执行时的当前工作目录是调用 apply 时所在的目录不保证是配置文件目录。所以 hook 里的相对路径很容易出问题。我踩过的一次在 hook 里写了cp ./settings.json ~/something/settings.json看似没问题但换了个目录执行 apply就变成了从另一个地方复制。解决方案非常简单在 hook 命令里显式使用绝对路径或者先cd到明确目录。我的一个通用写法hooks: after: - name: sync-vscode-config command: bash -c cd $HOME cp ./.config/t3code/vscode-settings.json ~/Library/Application\\ Support/Code/User/settings.json6.4 换行符与跨平台文件的问题files字段里的content如果是 Windows 风格CRLF 换行写进去的在 macOS/Linux 上会导致一些对格式敏感的工具异常。我遇到最典型案例是 git 的.gitignore规则没有生效排查半天发现是文件里有\r符号。所以写模板时建议统一用 LF 换行。如果从 Windows 上拷贝过配置文件用file命令检查一下格式必要时预处理一下。这些细节在自动化配置工具里虽然不是大问题但一旦出现很浪费时间。7. 结合 CI/CD 的思路让 t3code 发挥更大的作用最后聊一点进阶用法。t3code 不只是本地装环境的工具它的plan模式加上无互动执行方式很适合放进自动化流水线里。7.1 用于容器镜像的预构建在 CI 里构建一个开发容器镜像时与其在 Dockerfile 里写一长串RUN apt-get install和RUN git config --global不如这样写FROM node:20 COPY team-env/ /opt/team-env/ ENV T3CODE_CONFIG_FILE/opt/team-env/t3code.yaml RUN t3code apply这样团队的环境变更只需要升级模板仓库版本重新构建镜像即可不需要动 Dockerfile 本身。我实际就是这么做的——每次构建镜像时t3code 会把 git 配置、ssh key、shell 别名、常用工具全部装好镜像一启动就是一个可开发的容器环境。7.2 用 plan 模式做配置审计t3code plan会列出所有将要执行的变更不直接执行。配合--json输出可以在 CI 里做人眼可读的审计报告。比如团队配置了某个安全相关的 git 设置就可以在 CI 里检查已有机器是否符合t3code plan --json --check如果 check 返回非零退出码说明有配置漂移CI 直接标红。这样团队机器的环境一致性就变成了一条自动化的规则而不是靠每个成员自觉。它把我最初折腾这个工具的初衷真正落地了——让环境管理和写代码一样有版本、有审查、有自动化。如果你也在为多台机器、多个项目的重复配置感到心累不妨从 init 开始跑一遍试试。