☰
context-mode:让终端按场景自动切换上下文环境的开发实践
2026/10/6 5:38:22 网站建设 项目流程

上个月我有个终端翻车现场:三个标签页,一个在写 Vue 前端,一个在调 Spring 后端的接口,第三个是从跳板机登进生产环境查日志。前两个标签页长得一模一样,黑底白字毫无区分,我一走神把生产环境那套 alias 带进了前端项目,结果 npm run dev 直接报错,node 版本不对,PATH 里全是运维目录的东西。就在那一刻我决定把 context-mode 这个东西认真做出来——它本质上是一套让 shell 根据当前所在场景自动切换“上下文”的机制,翻译成人话就是:你在哪个项目、哪台机器、哪种任务状态下,你的终端就自动变成适合那个场景的样子,该有的工具链、别名、环境变量、提示符颜色全部自动到位。

这篇文章我会把整个设计思路、核心机制、配置样例和排错过程完整写出来。如果你也经常在多个项目、多台服务器、多种职责之间来回切换,或者你团队里有新人总在错误环境里敲命令,那 context-mode 这套思路应该能帮到你。

1. 没有上下文感知的终端,一天要浪费多少操作

1.1 我踩过的实际场景

先说我自己的问题。我日常大概是三个身份在切换:前端开发、后端开发、临时运维。三个身份对应的操作完全不同:

  • 前端开发时,要保证node是项目要求的版本,yarn或者pnpm可用,node_modules/.bin要能直接访问,经常要执行npm run dev、npx eslint --fix这类命令。
  • 后端开发时,要激活 Python 虚拟环境或者设置正确的JAVA_HOME/MAVEN_HOME,mvn、java、mysql客户端路径要对,数据库连接凭据要注入。
  • 运维排查时,要临时加载一批只读 alias,比如log直接查看最近一小时的应用日志,port列出端口占用,而且一定要避免rm -rf这类危险操作被误触发。

在传统 shell 里,这些环境切换基本靠人肉:手动source ./env.sh、手动改PATH、手动export一个变量。麻烦点在于,你是靠记忆力在维护一套看不见的状态。只要你切错顺序,比如先进了前端目录又想起来要跑后端测试,忘了切回,命令就可能执行在错误的环境里。

最典型的一次事故是:我为了快速处理生产环境告警,在本地终端里先 source 了一份运维环境的配置,之后忘了退出,转手去改本地服务代码。结果那条restartalias 在生产配置里被定义成直接连生产主机执行命令,我一回车就把本地环境的生产连接串了。虽然没有造成大事故,但那个被惊出一身冷汗的瞬间,足够让我下决心做 context-mode。

1.2 对比 direnv 等既有方案:为什么还值得自己做一个

聊到按目录加载环境,很多人第一反应是direnv。我承认 direnv 是个好工具,它在进入一个目录时读取.envrc并加载环境变量,退出时卸载,几年下来已经很成熟。但如果把它当成 context-mode 的替代品,会发现在真实复杂场景里有几个明显的边界:

第一,direnv 的模型是“目录绑定环境”,一个目录通常对应一套环境。可现实里同一个目录里会同时存在多种“上下文”:你可能在写代码,也可能在部署,还可能只是路过看看。direnv 很难表达“我现在是以运维身份在这个目录里操作”。

第二,direnv 的加载是线性的,不支持模式嵌套。比如我进入了一个项目目录自动进入backend模式,但此刻我还需要临时叠加一个release模式来做发布检查。direnv 做不到这种临时叠加再撤销的操作。

第三,direnv 解决了环境变量加载,但没解决“状态可视化”。我进入目录后,不敲命令的话,根本不知道自己当前背了哪些环境。你问自己“我现在在哪个模式”,回答不出来,这正是大多数人出错的原因。

context-mode 的思路不太一样。它把“上下文”抽象成一种可命名、可嵌套、可观察的模式,然后围绕模式做四件事:检测、切换、加载、展示。底层依然会用到 shell 钩子和环境变量,但它提供的是一整套工程化封装,而不是单点脚本。

1.3 context-mode 想解决的问题清单

所以我在设计这个项目时,给自己列了一个需求清单:

  1. 自动识别当前所在的项目类型或任务类型,进入对应模式。
  2. 支持手动强制进入某个模式,也支持在模式里再叠加临时模式。
  3. 进入和退出模式时,环境变量、alias、函数、提示符风格都要跟着切换,并且退出时能干净还原。
  4. 让当前生效的模式在提示符里直接可见,颜色区分,避免“我在哪”的疑问。
  5. 配置文件可以放进项目仓库,让整个团队共用同一套上下文。
  6. 不引入重量级依赖,bash、zsh、python3 就能跑,适配 mac 和 linux。

从 1.3 的目标可以看到,context-mode 不是要替代 direnv,而是想解决“目录环境”之上的“操作者身份与任务状态”的问题。

2. context-mode 的核心机制:检测、模式栈与状态还原

2.1 什么叫“模式”:模式不是一个目录,而是一份状态

很多人容易把“模式”理解成目录。其实不对。目录只是触发条件之一,模式本身是一份可命名的状态集合。

我用一个配置文件定义一种模式。比如backend.conf描述了 backend 模式长什么样:

# ~/.contextmode/modes/backend.conf [MODE] name = backend prompt_color = blue prompt_label = [BE] [ENV] JAVA_HOME=/usr/local/jdk-17 MAVEN_HOME=/usr/local/maven-3.9 PATH_ADD=.venv/bin:node_modules/.bin [ALIAS] mvn-test = mvn test -q run = ./mvnw spring-boot:run

这个模式下,我会声明要追加哪些 PATH 路径、要导出哪些环境变量、要建哪些 alias、提示符用什么颜色、什么标签。目录只是“检测器”的输入,真正起作用的是这份模式定义。

设计上我刻意区分了“检测”和“加载”两个阶段。检测阶段负责回答“我现在处于哪个上下文”,加载阶段负责“把该上下文需要的状态应用进来”。这两个职责一旦混在一起,后面做嵌套、做优先级、做 debug 都会非常痛苦。

2.2 模式检测规则:怎么知道该进哪个模式

context-mode 的检测规则放在rules.d目录下,每个规则文件描述一组触发条件。我推荐用 INI 风格写规则,理由很简单:大家都能改,少一层格式学习成本。

# ~/.contextmode/rules.d/backend.rule [RULE] mode = backend priority = 100 [COND] dir_prefix = /workspace/server, /workspace/api file_exists = pom.xml, build.gradle, go.mod git_remote_contains = backend

检测时,context-mode 会按 priority 从高到低跑规则。满足任一条件组就会激活对应模式。规则之间不互斥,所以有可能一次进入多个模式,比如某个目录既有pom.xml又放在/workspace/docs下,那 backend 和 docs 两个模式可能同时激活。这是有意的设计,后面讲模式栈你会明白为什么这样做。

文件特征检测是里面最容易写错的地方。很多人想匹配“目录下有没有 package.json”,实现时写了个find递归查询,导致每次 cd 都要遍历一遍目录树,大项目直接卡顿。我的做法是只查当前目录和最近一级父目录的文件,不做无限递归。因为进入一个目录大概率是顶层特征文件,比如package.json、pom.xml、Cargo.toml都在项目根目录。真要识别深层目录,优先用dir_prefix表达式。

2.3 模式栈:嵌套场景是必须支持的

context-mode 的核心数据结构是“模式栈”,而不是“当前模式”这么简单一个状态。

我举个实际例子。你正趴在/workspace/server里写后端代码,自动进了backend模式。中途同事让你帮忙看一下线上配置,你不想退出当前目录和环境,只需要临时叠加一个ops模式。这时候如果在“当前模式”模型里,你要么退出 backend 再进 ops,要么两个模式各自独立互不相认。但在模式栈里,你可以cm enter ops --push,把 ops 压到栈顶,此时两个模式同时生效。处理完线上问题,一个cm pop弹出 ops,backend 模式依然留在原地。

栈操作我封装成了这几个命令:

cm list # 查看当前模式栈 cm enter <mode> # 进入一个模式 cm leave # 退出最近进入的模式 cm push <mode> # 压入一个临时模式 cm pop # 弹出最近压入的临时模式 cm status # 查看当前模式详情和环境摘要

栈的好处是支持局部覆盖。比如ops模式里我定义了port这个 alias,压入后它会覆盖 backend 模式下同名的 alias;一旦 pop,backend 里的同名 alias 自动恢复。

2.4 状态还原:为什么退出比进入难

模式加载本身不难,难的是退出时怎么把环境还原。大多数人写环境脚本只记得export,不记得退出时要清理。context-mode 里我专门写了一个还原模块,核心逻辑是:进入模式之前,先记录一份“环境快照”,退出时按快照回放。

以PATH为例。假设初始PATH是:

/usr/local/bin:/usr/bin:/bin

进入 backend 模式时追加了.venv/bin和node_modules/.bin,退出时如果你只是再写一遍PATH=$OLD_PATH,在简单场景没问题,但一旦有嵌套模式,比如 backend 还没退又叠加了 ops,退出顺序就必须严格按后进先出反过来还原。我在还原模块里没有用字符串拼接,而是把 PATH 存成数组,记录每个模式加了哪些前缀、哪些后缀,退出时精确移除,不是整体覆盖。这样能避免一个经典问题:同一个目录反复进出几十次后,PATH 里堆了几十层重复路径。

环境变量也是一样。每个模式声明了EXPORT变量,进入时导出,退出时如果发现该变量在进入前不存在,就删除;如果进入前有旧值,就恢复旧值。这个过程我写在 Python 里,由 shell 调用,避免在 bash 里维护复杂数据结构。

3. 从零到一:一份可直接照抄的 context-mode 配置

3.1 安装与 shell 集成

我尽量让安装保持轻量。本地只有一个目录~/.contextmode,里面分四块:bin(程序入口)、modes(模式定义)、rules.d(检测规则)、state(运行时状态文件)。

安装脚本核心做的事情只有两步:下载代码到~/.contextmode,然后在 shell 配置文件里加两行钩子。

bash 用户,在.bashrc里加:

export CONTEXTMODE_HOME="$HOME/.contextmode" [ -f "$CONTEXTMODE_HOME/bin/cm.sh" ] && . "$CONTEXTMODE_HOME/bin/cm.sh"

zsh 用户,在.zshrc里加:

export CONTEXTMODE_HOME="$HOME/.contextmode" [ -f "$CONTEXTMODE_HOME/bin/cm.zsh" ] && . "$CONTEXTMODE_HOME/bin/cm.zsh"

为什么强调只在交互式 shell 里加载?因为非交互 shell 比如 cron 脚本、CI 脚本,不应该被这些模式污染。钩子里面的判断就是[[ $- == *i* ]],非交互直接 return。我见过有人把环境工具无条件塞进.bashrc,结果所有脚本执行环境都变了,出问题还特别难查。

zsh 集成比 bash 简单,因为它有chpwd钩子,目录变化时一定会触发。bash 没有对应的原生钩子,我用的是PROMPT_COMMAND,每次提示符出现前检查一下当前目录跟上一次是否一样,变了就触发检测。只做目录变化检测,不做全量重算。

3.2 modes 目录里的配置怎么写

下面是一个我实际在用的后端模式定义,直接贴在~/.contextmode/modes/backend.conf:

[MODE] name = backend prompt_color = blue prompt_label = [BE] [ENV] JAVA_HOME=/usr/local/jdk-17 MAVEN_HOME=/usr/local/maven-3.9 PATH_ADD=.venv/bin:node_modules/.bin [ALIAS] be-run = ./mvnw spring-boot:run be-test = ./mvnw test -q be-clean = ./mvnw clean package -DskipTests [FUNC] be_log = tail -f logs/app.log

这里PATH_ADD支持相对路径。相对路径会被解析成“当前项目目录的相对路径”,这样同一个 backend 模式放到不同项目里也能用。这个设计很关键,否则你每个项目都要写一份绝对路径的配置。

前端模式是另一个样子:

[MODE] name = frontend prompt_color = green prompt_label = [FE] [ENV] PATH_ADD=node_modules/.bin USE_NODE_VERSION=18 [ALIAS] dev = npm run dev build = npm run build lint = npx eslint --fix

如果同时还结合nvm或者fnm,我会在模式的ON_ENTER钩子里写版本切换逻辑,比如:

[HOOK] ON_ENTER = fnm use 18

ON_ENTER是在模式激活后执行的 shell 代码。我不让它做成通用脚本语言,只接受简单的单行命令,主要是为了防止有人把整个初始化脚本塞进去,导致模式切换很重。

3.3 项目内的共享配置:让队友自动进入模式

context-mode 一个很实用的场景是团队共享。我会在项目仓库根目录放一个.contextmode/文件夹,里面是检测规则和模式约定。新同事 clone 完项目后,只需要做一次本地初始化,之后进入项目目录,shell 自动进入团队约定的模式,alias 和工具链全都一致。

团队共享时要特别注意:模式配置里的绝对路径不能提交到仓库。每个人的 JDK 路径可能不一样,所以公共配置里只写相对路径和逻辑名,个人本机的绝对路径覆盖放在~/.contextmode/local-modes/里。local 目录优先级比项目目录里的配置高,这样既保证基本一致性,又不剥夺个人定制能力。

3.4 从自动切换到手动控制:避免误判的兜底手段

自动检测省事,但它不是银弹。rules 匹配必然存在误判,比如一个目录同时有后端代码和文档站点,自动检测可能激活了不需要的 docs 模式。因此我设计了手动控制优先级:

cm enter frontend --force # 强制覆盖自动检测结果 cm ignore backend # 临时忽略某个自动模式 cm auto on # 恢复自动检测 cm auto off # 关闭自动检测,完全手动

我的经验是:让自动检测解决 80% 的常规场景,剩下的 20% 保留手动开关。真正顺手的环境工具,不能剥夺用户最终控制权。

4. 提示符里的模式状态:解决“我到底在哪个模式”这个终极问题

4.1 为什么提示符比 pwd 更值得关注

很多工具做环境加载,但不改提示符。结果就是你进入一个目录后,只有执行cm status才知道自己激活了哪些模式。这等于把一个需要持续关注的信息藏进了需要主动查询的地方,出错概率自然低不了。

对人来说,眼睛扫到终端的左下角只需要零点几秒。如果那里直接显示[BE]和对应颜色,你根本不需要回忆。提示符不是装饰,它是上下文信息最重要的出口。

4.2 context-mode 如何把状态交给 PS1

我的做法是,在每次PROMPT_COMMAND执行时,让 cm.sh 生成一段环境变量CM_PROMPT_SEGMENT,然后在 PS1 里引用它。

bash 的 PS1 设置看起来像这样:

export PS1="${CM_PROMPT_SEGMENT}\u@\h:\w\$ "

CM_PROMPT_SEGMENT由 context-mode 动态生成,生成规则也很简单:遍历当前模式栈,按模式定义里的prompt_color和prompt_label拼出一段带颜色转义的内容。

比如 backend 模式是蓝色[BE],ops 模式是红色[OPS],栈里两个都存在时,显示:

[BE][OPS] user@host:/workspace/server$

颜色的实现用 ANSI 转义,24 位色还是 256 色我做了简化处理,只取常见颜色名到编码的映射。写死一套映射表,至少保证 mac 的 Terminal.app 和 linux 的 GNOME Terminal 渲染一致。

4.3 性能控制:提示符刷新不能拖慢 shell

这是所有做提示符增强的人都会踩的坑:提示符每次出现都触发一次脚本,脚本里如果再跑git status、find、pwd之类的命令,整个终端就会变得又慢又卡。

context-mode 的策略是“结果缓存 + 触发时才重算”。CM_PROMPT_SEGMENT只会在三种情况下重新计算:

  1. 当前目录发生变化;
  2. 模式栈发生变化,比如cm enter或cm pop;
  3. 手动执行了cm refresh。

目录没变的时候,PROMPT_COMMAND里只是拿一个缓存变量拼进 PS1,不做任何检测和文件操作。这个优化做下来,敲回车时的响应时间基本可以忽略。

git 分支信息我同样做了缓存,只在目录变化时才重新获取。把 git 分支检测放在每次提示符里是很奢侈的,仓库稍大一点,一次git status可能要几十毫秒到上百毫秒。

5. 实战中踩过的坑:变量污染、钩子时机与误判

5.1 PATH 重复叠加和顺序错乱

最早版本里我用的是最粗暴的追加方式:进入模式就往 PATH 尾部追加一串,退出模式时再执行一次export PATH=<旧值>。听起来没什么问题,但一旦模式嵌套,事情就开始失控。

比如 backend 模式进入时 PATH 是 A,ops 模式压入时 PATH 是 A+B,pop ops 后我把 PATH 恢复成 A。听起来没问题,可如果 backend 模式的 ON_ENTER 钩子里又跑了别的脚本,悄悄改了 PATH,那退出 ops 时的“旧值”可能已经不是真正的初始值了。调试这种问题非常痛苦,PATH 打印出来又臭又长,人眼很难看出哪一段是工具加的,哪一段是业务脚本加的。

后来我改成全量快照机制:模式栈每次变化时,把当刻的 PATH 数组快照记录在一个状态文件里,退出时直接恢复到上一次栈状态对应的快照,而不是只恢复某一个模式的旧值。这本质上是一个事务回滚,比“记录单变量旧值”靠谱得多。

5.2 chpwd 与 PROMPT_COMMAND 的触发时机差异

zsh 的chpwd钩子只在目录确实变化后触发,这是最理想的时机。但 bash 没有这个钩子,我只能在PROMPT_COMMAND里比较当前PWD和记录的PREV_PWD,不一致才触发检测。这个方案能跑,但有一个隐藏问题:在同一个目录内,如果你用cd -来回跳,bash 的 PWD 其实没变,不会触发重新检测,导致某些依赖环境的动态 alias 没有更新。

解决办法是在cm enter、cm push、cm pop里都强制清一次缓存,并且在 PS1 里加一个不常变的计数器。另外在安全的__cm_force_refresh函数里,用户可以手动触发一次完整检测。

5.3 规则误判的典型场景和解决

误判是自动检测绕不开的问题。最常见的是这样:一个 git 仓库里同时有多个独立项目,比如一个 monorepo,根目录有package.json,但子目录deploy/里是部署脚本。我站在根目录时触发了 frontend 模式,进入deploy/时又触发了 ops 模式。两个模式都有读取package.json的 alias,结果 alias 冲突,生效的是后进栈的 ops,但 frontend 的一些环境变量还在,导致行为不可预期。

我加了一个调试命令cm doctor,它会列出当前目录命中了哪些规则、为什么命中、每个规则的优先级是多少、模式栈里现在有什么。诊断信息直接打印,排查起来方便很多。还支持规则里的exclude字段:

[COND] dir_prefix = /workspace/server exclude = /workspace/server/docs

exclude 优先级高于其他条件,命中即不激活模式。这个字段看着小,实际能挡掉好多误判。

5.4 跨平台兼容的细节

作为一个要在 mac 和 linux 上跑的工具,坑主要在底层命令差异。比如 mac 原生sed -i一定要带后缀参数,linux GNU sed 不需要,写脚本时不能混用;grep -P在 mac 上默认不可用,改成正则表达式写法;mktemp的用法两边也有细微差别,mac 上-t参数行为和 linux 不同。

如果不想在这些细节上反复踩坑,写 shell 或 Python 脚本时建议一开始就做最小化兼容测试。我给 CI 加了一个简单的冒烟测试矩阵,在 mac 和 ubuntu 的容器里跑一遍基础功能,避免每次发布都要手动验证两边环境。

6. 从终端上下文到更广的上下文驱动开发

6.1 为什么 AI 编程工具也在谈 context-mode

写 context-mode 的过程中,我发现“上下文”这个词在最近的 AI 编程工具里也很热。Cursor 也好,Claude Code 也好,都在强调你给工具喂什么样的上下文,它就能给你什么样的输出。你要让它改前端,它就得知道当前项目的技术栈;你要让它做运维分析,它就得知道你的日志路径和系统状态。

终端里的上下文也是同一个道理。shell 的 alias、环境变量、工具链就是命令层面的“记忆”。你切进了对的模式,等于给所有命令提供了一个正确的“工作记忆”;切错了模式,等于让每个命令都在失忆状态下乱猜。这个类比后来成了我给团队讲 context-mode 时的万能话术。

6.2 与 tmux 会话、窗口标题、历史记录联动

做到这里之后,我发现扩展空间还很大。比如和 tmux 会话联动,让每个会话绑一个主要模式,会话取名自动带上模式标签;窗口标题栏也可以显示当前模式,抬头扫一眼就知道这个终端在干什么;shell 历史可以做分模式存储,查看历史时按模式过滤,找命令的效率高很多。

我目前把窗口标题联动做进去了:进入 backend 模式时,终端窗口标题自动变成[BE] workspace/server。窗口标题在很多场景下比提示符更显眼,尤其是你开了多个全屏终端时。实现也很简单,预设一个转义序列,在ON_ENTER钩子里写进去就行。

6.3 把 context-mode 接入自动化脚本和 agent

更进一步,我还在实验让 context-mode 为自动化脚本提供上下文信息。脚本可以在执行前调用cm status --json,拿到当前模式列表,然后决定自己的行为。比如一个发布脚本,检测到当前不是release模式时,直接拒绝执行并要求先进入 release 模式。这比在脚本里写一堆 if 判断谁在哪个目录要干净得多。

给 agent 用也是一个方向。现在很多自动化智能体在执行命令前,往往需要自己猜当前环境。如果 agent 先读一下cm status --json,它就知道自己该用什么风格执行命令了,相当于给 AI 提供了一个可编程的“环境感知层”。

6.4 做这个工具后,我最大的三点体会

一点是,上下文切换成本不全在环境,其实更多在“重新建立心智模型”。环境工具能降低切换摩擦,但治不了“脑子还在上一个场景里”的问题。所以 context-mode 特别强调可视化,提示符、窗口标题、状态命令,都是在帮人快速重建心智模型。

二点是,自动化程度要克制。自动检测虽然便利,但误触发一次带来的信任损失,比手动执行一次带来的麻烦更大。留好手动开关和 override 通道,不是退步,是对使用者负责。

三点是,这种工具最好保持低依赖、可调试。不要为了炫技引入复杂框架和重型语言。一个能用 python3 和 shell 讲清楚的工具,部署和排障成本都低得多,也更容易被团队接受。

如果你也想在终端里解决“我在哪、我在干什么、该用什么工具”这三个问题,我建议不用急着复刻 context-mode 的全部功能,先把模式栈和提示符可视化这两点做出来,你会发现日常操作的手感和安全感已经完全不一样了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询