配置目录、环境变量与运行时覆盖
启动配置不仅取决于 cordis.yml:引导层还解析配置路径、读取调用目录与 Harness home 的 .env,并为 Loader 准备用户补丁层。本页聚焦这些入口和环境变量的安全边界。
Purpose and Scope
本文覆盖 @deepseek-ai/dsh-app-boot 中已核实的路径解析、环境变量分层读取与启动时覆盖的边界。配置 schema 的生成、profile 包安装、HMR 的文件监视及 Loader 内部的条目合并应分别参见相应专题;这里不推断它们的未读取实现。引导模块的说明明确提及从 ~/.dsh 取得可选用户补丁、向配置表达式暴露 home 路径解析器,并驱动 Cordis Loader 加载叶级 cordis.yml;下面只对本页读到的实现给出具体调用顺序。启动模块说明。
Overview
这里有两种不同的“覆盖”:一是配置文件名选择,resolveConfigPath 在 snapshot replay 模式下将匹配的 cordis.yml 或 cordis.yaml basename 换成 cordis.snapshot.yml;二是环境值来源,loadLayeredEnv 的接口注释规定继承环境优先于调用目录 .env,后者优先于 Harness-home .env,接受的文件值不得替换继承值。环境文件还受启动专用变量规则约束,不能把改变进程启动、模块加载、网络或信任范围的变量随项目文件带入。参见 路径解析 与 分层环境接口。
Architecture
Source: index.ts
图中将旧的单目录 loadEnv 与面向产品 CLI 的 loadLayeredEnv 分开:前者直接调用 Node 的 process.loadEnvFile;后者的已读实现先解析 home 并复制继承环境。readEnvLayer 是文件解析与拒绝危险名称的内部辅助函数;图不暗示未读取的后续调用或合并细节。两个入口、分层入口。
配置路径与文件名
resolveConfigPath(configPath: string, snapshotMode: string | undefined, cwd: string = process.cwd()): string 始终先用 resolve(cwd, configPath) 得到绝对路径。只有 snapshotMode === 'replay' 才对绝对路径的 basename 执行 /cordis\.ya?ml$/ 替换并在原目录重新解析;其他模式原样返回绝对路径。由于匹配的是尾缀而不是对整个 basename 做相等判断,调用者不应把它理解为“仅匹配文件名恰好等于 cordis.yml”;不匹配时替换不会改变文件名。实现。
1export function resolveConfigPath(
2 configPath: string, snapshotMode: string | undefined, cwd: string = process.cwd(),
3): string {
4 const absolute = resolve(cwd, configPath)
5 if (snapshotMode !== 'replay') return absolute
6 const dir = dirname(absolute)
7 const replayName = basename(absolute).replace(/cordis\.ya?ml$/, 'cordis.snapshot.yml')
8 return resolve(dir, replayName)
9}Source: index.ts
环境文件的两个入口
loadEnv(binName: string, dir: string = process.cwd(), warn: (line: string) => void = ...): void 是直接加载指定目录 .env 的入口。缺文件 ENOENT 不报警,保留进程环境;其他读取异常交给 warn,默认写标准错误。它没有在此函数内执行 readEnvLayer 的启动变量限制;不要把它与受校验的分层入口混为一谈。实现。
1export function loadEnv(
2 binName: string, dir: string = process.cwd(),
3 warn: (line: string) => void = line => void process.stderr.write(line),
4): void {
5 try {
6 process.loadEnvFile(resolve(dir, '.env'))
7 } catch (error) {
8 if ((error as NodeJS.ErrnoException | null)?.code !== 'ENOENT') {
9 warn(`${binName}: failed to load .env: ${String(error)}\n`)
10 }
11 // ENOENT (no .env) is fine — rely on the ambient environment.
12 }
13}Source: index.ts
loadLayeredEnv(binName: string, cwd: string = process.cwd(), warn: (line: string) => void = ...): LaunchEnvironmentSnapshot 的文档规定在应用任何文件值之前检查两个文件,并保留每个值来自哪个层。已读取的函数开头先 resolveDshHome(),随后浅复制 process.env 为 inherited;本次阅读未覆盖函数剩余部分,因此不在这里指定内部具体的写回顺序或 snapshot 字段结构。接口及开头。
1export function loadLayeredEnv(
2 binName: string, cwd: string = process.cwd(),
3 warn: (line: string) => void = line => void process.stderr.write(line),
4): LaunchEnvironmentSnapshot {
5 const home = resolveDshHome()
6 const inherited = { ...process.env } as Record<string, string>Source: index.ts
校验先于物化
内部 readEnvLayer 从目录读取 .env,ENOENT 或其他读取失败均返回 undefined;仅后者调用 warn。成功后使用 parseEnv(content) 解析一次,再遍历实际解析出的名称:这一顺序让校验面对的键与随后返回的值相同。若名称属于引导专用集合,抛错而非静默忽略;唯一例外是 home 目录文件中的代理变量。错误文本包含文件路径、变量名与修正建议。解析和校验。
1 // Parse once so validation and materialization use exactly the same entries.
2 const values = parseEnv(content) as Record<string, string>
3 for (const name of Object.keys(values)) {
4 if (!isBootstrapOnly(name)) continue
5 const proxyName = HOME_LAYER_PROXY_NAMES.has(name.toUpperCase())
6 if (isHome && proxyName) continueSource: index.ts
实际顺序与异常分支
Source: index.ts
图展示两个已读局部片段:loadLayeredEnv 的入口和 readEnvLayer 的独立分支,并非宣称已核实这两个函数之间的完整调用链。
环境变量策略与配置选项
这里的“选项”是环境名称及函数参数,不是某个独立配置文件的 schema。isBootstrapOnly 将名称先转换为大写,再比较精确名称或前缀;因此大小写不同的拼写也会命中限制。被拒绝的类别包含启动/加载路径(如 PATH、NODE_OPTIONS)、脚本启动钩子、Git 与编辑器选择、网络目标、代理和 TLS/CA 信任设置;完整常量在实现中。精确名称及前缀。
| 选项 / 名称 | 类型 | 默认或优先级 | 作用与边界 |
|---|---|---|---|
configPath | string | 无函数默认值 | 相对路径以 cwd 为基准;解析为绝对路径。实现 |
snapshotMode | string | undefined | 无函数默认值 | 仅精确值 replay 触发配置 basename 替换。实现 |
cwd / dir | string | process.cwd() | 分别作为配置路径解析基准、项目 .env 或单目录 .env 所在目录。路径、环境入口 |
warn | (line: string) => void | 写入 process.stderr | 文件不可读且不是 ENOENT 时输出诊断。实现 |
DSH_SNAPSHOT | 环境字符串 | 未在已读代码中规定默认值 | 通过调用方传给 resolveConfigPath 的 snapshotMode 影响 replay 解析;不能从 .env 设置 DSH_ 前缀变量。注释及规则、前缀 |
DSH_、XDG_、DYLD_、BASH_FUNC_ | 名称前缀 | 无 | 分层文件不得设置,必须使用继承环境。规则 |
HTTP_PROXY、HTTPS_PROXY、ALL_PROXY、NO_PROXY | 精确变量名 | 仅 home .env 可例外 | 项目 .env 不得设置;继承环境可设置。TLS/CA 名称没有此例外。策略 |
为何给 home 目录的代理变量开口?代码注释指出项目文件可能跟随 clone,而 home 文件属于用户;代理决定请求路由,不能由携带项目的文件改变。相反,CA 与 TLS 配置决定信任范围,所以即使 home 文件也不豁免。设计说明。
API Reference
| API | 参数 | 返回 | 已核实的异常或诊断 |
|---|---|---|---|
resolveConfigPath(configPath: string, snapshotMode: string | undefined, cwd: string = process.cwd()): string | 要解析的路径、模式、基准目录 | 配置文件绝对路径 | 已读函数内未定义显式异常处理。实现 |
loadEnv(binName: string, dir: string = process.cwd(), warn: (line: string) => void = ...): void | 诊断前缀、.env 目录、警告接收器 | 无 | ENOENT 静默;其他加载失败经 warn 报告。实现 |
loadLayeredEnv(binName: string, cwd: string = process.cwd(), warn: (line: string) => void = ...): LaunchEnvironmentSnapshot | 诊断前缀、调用目录、警告接收器 | 记录来源层的环境快照(接口注释) | 接口说明:两文件任一声明禁止的 bootstrap 名称会抛错;home 代理例外。声明 |
readEnvLayer 和 isBootstrapOnly 没有 export 声明,是该模块的内部辅助函数,不属于上述导出 API。声明。
Failure Modes, Edge Cases & Operations
- 缺文件不等于错误:
loadEnv和内部readEnvLayer都将ENOENT视为可缺省;后者在非ENOENT读取错误时也返回undefined,但先发出警告。直接加载、解析入口。 - 非法名称导致快速失败:
readEnvLayer抛出的消息说明将名称改为 shellexport;代理变量还可迁入 home.env。两层文件在应用前接受检查的语义由loadLayeredEnv接口注释给出。错误分支。 - 目录判别采用解析后路径:home 豁免条件是
resolve(dir) === home;此处没有显示额外的符号链接规范化。比较。 - 运行中一致性:入口复制当时的
process.env,接口称返回本次运行的快照;所读片段未说明并发修改全局环境时的同步机制,不能据此声称线程安全或实时刷新。接口与复制。 - 扩展时的边界:若要改变项目可加载的环境名称,需要同时审视
BOOTSTRAP_NAMES、BOOTSTRAP_PREFIXES和 home 代理例外;只改loadEnv不会改变readEnvLayer的校验策略。规则与校验、解析。
Related Links
- 配置 schema 的生成入口见 config-schema 导出,应由配置 schema 专题介绍。
- profile 与补丁相关导出见 profile 导出;用户补丁的合并及运行时 Loader 更新属于 profile/配置重载专题。
- 本页所述启动时环境和路径入口见 引导模块。