命令行与 Headless 运行
dsh 是按 profile 启动应用的命令行入口;headless 是其帮助文本展示的单任务调用形式。本页重点说明命令行如何解析参数、分发运行模式及准备 profile,不将尚未核实的 headless 插件内部执行机制当作已知事实。
Purpose and Scope
本文覆盖 dsh 启动器拥有的参数、调用模式、profile 启动前的配置准备,以及 dsh headless "run the tests" 的已验证入口语义。profile 插件本身如何解释任务、生成答案、输出结果和退出,不能从已读取的启动器实现中确认;插件配置组合的全部规则、Desktop 启动及其他应用界面也不在本页展开。有关插件依赖管理和配置导出,请参阅各自的专题;此处只交代其在命令行分发中的位置。
Overview
启动器只解析自己负责的 profile 名称、额外 patch 和配置导出开关;第一个不属于启动器的参数以及后续参数都交给启动后的应用插件。这让 dsh tui --resume <session> 中的 --resume 属于应用,而不是全局 CLI。dsh <name> 是 dsh --profile <name> 的简写;帮助文本将 dsh headless "run the tests" 描述为执行一个任务、打印结果然后退出,但已读取的代码没有展示执行任务的插件实现。参数解析说明、帮助文本。
Architecture
Sources: bin.ts、profile-boot.ts。
图中 runCli 按解析出的 mode 延迟导入对应模块。普通 profile 模式加载分层环境并交给 runProfile;composeProfile 可见的部分负责准备 profile、创建运行时解析上下文和加载命令行 overlay。runProfile 函数体未在本次取证范围内,因此图中的后续运行阶段不展开。入口分发、组合逻辑。
命令行解析与模式分发
参数归属与 profile 选择
parseDshArgs(argv: readonly string[], version: string, manageDesktopProfile = false): DshInvocation 使用 Commander:启动器选项必须在应用参数之前;解析器允许未知选项并启用透传。首参数不是选项且不是 plugin 时,先在内部插入 --profile,使 dsh headless ... 与显式 profile 形式一致。无 profile 时,-h 或 --help 打印启动器帮助;有 profile 时,帮助参数可交给应用。实现。
1 .allowUnknownOption()
2 .passThroughOptions()
3 .enablePositionalOptions()
4 .argument('[args...]', 'arguments for the booted profile\'s app (see: dsh --profile <name> --help)')
5 .option('--profile <name>', 'the profile under $DSH_HOME/profiles to boot', selectProfile)
6 .option('--from-default-profile <name>', 'initialize a new custom profile from a shipped profile template')
7 .option('--patch <path>', 'extra patch-list overlay applied after the profile layer (repeatable)', collect)Source: args.ts
这里的 --patch 是单值可重复选项,不是可变长参数,否则可能吞掉应用参数;selectProfile 拒绝重复选择 profile。解析结果是 profile、plugin、dump-config、dump-config-schema 四类判别联合,而不是同一条启动路径。类型定义、检查逻辑。
启动与旁路操作
runCli(options: RunCliOptions = {}): Promise<void> 从进程参数解析 invocation,然后按 mode 分发;普通启动加载 loadLayeredEnv('dsh'),将 profile、模板、patch 文件和原样的应用参数传入 runProfile。plugin 转给 runPlugin 并以返回码退出;两种 dump 分别转给配置树和 schema 输出实现,不进入普通启动分支。入口实现。
1 try {
2 await runProfile({
3 environment: loadLayeredEnv('dsh'),
4 profile: invocation.profile,
5 fromDefaultProfile: invocation.fromDefaultProfile,
6 patchFiles: invocation.patches,
7 args: invocation.args,
8 ...profileOptions,
9 })
10 } catch (error) {
11 if (!(error instanceof StartupError)) throw error
12 await reportStartupFailure(error, { home: resolveDshHome(), version, profile: invocation.profile })
13 process.exit(1)
14 }Source: bin.ts
Core Flow
Source: args.ts
此图刻画 resolveBoot 的实际判定:配置导出要求没有应用参数,三种导出开关互斥,--dump-default-config 还拒绝附加 --patch。这样避免展示一个与应用参数参与后的真实启动树不同的静态配置树。校验及注释。
Profile 准备与运行边界
目录初始化与空根配置
prepareProfile(name, userLayer = true, fromDefaultProfile?) 在指定模板时先调用 initializeProfileFromDefault,再加载 profile、报告跳过的 bundle,并将 profile 目录内的 cordis.yml 重写成空条目列表。注释说明:加载器写回当前组合树可能把已应用的条目固化到根文件;下次启动再次叠加 bundle 会重复插入。保留真实的空根文件是为了给 Loader 的 include 路径提供 profile 目录的 baseUrl,配置导出使用同一根文件。实现和设计注释、准备流程。
1export function prepareProfile(name: string, userLayer = true, fromDefaultProfile?: string): Profile {
2 if (fromDefaultProfile !== undefined) initializeProfileFromDefault(name, fromDefaultProfile)
3 const profile = loadProfile(NAME, name, INSTALL_ANCHOR, undefined, { userLayer })
4 reportSkippedBundles(NAME, profile)
5 writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG)
6 return profile
7}Source: profile-boot.ts
模板初始化只复制已发布模板的 bundle 列表;它拒绝未知模板、已发布 profile 名和已经存在的目标目录。mkdirSync(dir) 用于独占声明目标;若初始化失败,尝试移除新目录,清理也失败时抛出 AggregateError。这避免把已有的用户状态误当成全新 profile。实现。
Patch 顺序与参数快照
composeProfile 的可见代码先取得已解析 profile 或调用 prepareProfile,随后 createRuntimeResolution,最后对每个命令行 patchFiles 按 argv 顺序执行 loadOverlayPatches(NAME, resolve(file))。文件头和函数注释进一步说明完整预期栈为 bundle 层、profile 用户层、home 用户层、命令行 overlay、遥测开关;但各层最终挂载的函数体不在本次读取的节选里,因此此处只将其作为源码注释给出的顺序,不声称已观察到挂载实现。组合代码、模块说明。
1 const profile = resolvedProfile?.profile ?? prepareProfile(name, true, fromDefaultProfile)
2 if (resolvedProfile !== undefined) writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG)
3 const resolutionOptions = { installAnchor: resolvedProfile?.installAnchor ?? INSTALL_ANCHOR, profile }
4 const resolution = await createRuntimeResolution(resolutionOptions)
5 const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
6 return { profile, resolution, overlays }Source: profile-boot.ts
RunProfileOptions 要求环境快照、profile、patchFiles、args;可选已解析的应用自有 profile 与 packageManager。文件注释称内部应用参数经 ctx.cmdlineArgs 传到插件树,以供注入的应用插件读取同一不可变快照;具体注入代码未包含在读取节选中。接口、说明。
Usage Examples
下列是仓库已有的 CLI 帮助示例,不是另行设计的运行命令;headless 的实际任务语法应由启动后的应用提供者决定。
1 dsh headless "run the tests" answer one task, print the result, and exit
2 dsh tui --patch ./extra.yml boot a custom profile with one extra overlay
3 dsh tui --resume <session> arguments after the launcher flags reach the app
4 dsh web --help the web app's own flags and helpSource: args.ts
命令行对同一 profile 可以通过 --profile 明确选择;下面也是仓库帮助文本中的示例,用于区分 profile 选择和模板初始化。
dsh web boot the web profile (same as: dsh --profile web)
dsh rescue --from-default-profile web
create rescue from the shipped web template, then boot itSource: args.ts
Configuration Options
| 选项 | 类型/默认值 | 作用与约束 |
|---|---|---|
<name> 或 --profile <name> | string/未提供 | 选择 $DSH_HOME/profiles 下的 profile;dsh <name> 为简写;普通调用缺少名称时出错。 |
--from-default-profile <name> | string/未提供 | 仅用于把已发布模板初始化为新的自定义 profile;不能覆盖已存在目录。 |
--patch <path> | 可重复的 string/[] | 按 argv 顺序加入 overlay;默认配置导出不接受此项。 |
--dump-config | boolean/false | 输出组合配置树而不进入普通 profile 启动分支。 |
--dump-default-config | boolean/false | 只输出 bundle 层,不包含用户层及额外 patch。 |
--dump-config-schema | boolean/false | 请求配置 schema 输出;与其他导出开关互斥。 |
应用参数 [app-args...] | string[]/[] | 透传给启动后的应用;配置导出模式不允许提供。 |
选项及默认值由解析器的收集和 resolveBoot 分支确定;--dump-default-config 的作用来自 DumpConfigInvocation 注释,导出实现不在本次阅读范围内。参数类型与默认处理、约束、注册。
API Reference
| API | 参数 | 返回/异常行为 |
|---|---|---|
parseDshArgs(argv: readonly string[], version: string, manageDesktopProfile = false): DshInvocation | Node 脚本后的 argv、版本字符串、是否允许管理 Desktop profile | 返回四种 mode 之一;帮助、版本和解析错误在函数内退出进程。见定义。 |
runCli(options: RunCliOptions = {}): Promise<void> | 可选 packageManager、manageDesktopProfile | 选定分支结束时 resolve;普通启动的 StartupError 报告后以状态 1 退出,其余异常重新抛出。见定义。 |
initializeProfileFromDefault(name: string, fromDefaultProfile: string, home: string = resolveDshHome()): void | 新 profile 名、模板名、可选 home | 初始化模板 bundle 列表;无效模板、保留名称或目标已存在时抛错。见定义。 |
prepareProfile(name: string, userLayer = true, fromDefaultProfile?: string): Profile | profile 名、是否加载用户层、可选初始化模板 | 返回加载后的 profile,写回空根配置;模板初始化异常向上传播。见定义。 |
homePatchPath(): string | 无 | 按调用时的 home 路径返回 home 层 patch 的绝对路径,而不是模块载入时固定路径。见定义。 |
Failure Modes, Edge Cases & Concurrency
- 参数校验:空
--patch路径、空模板名、重复 profile、多个导出选项同时使用、导出时传入应用参数,均有显式错误路径;desktopprofile 对常规 CLI 保留给 Electron。插件管理分支只有传入manageDesktopProfile才允许管理 Desktop profile,并要求至少一个 pnpm 参数。解析、导出约束、插件分支。 - 启动失败:
runCli只特殊处理StartupError,通过reportStartupFailure报告版本、home、profile 上下文并退出码 1;非该类型的异常重新抛出。参数解析器对 Commander 错误使用其退出码,其他解析异常退出码 1。启动错误、解析异常。 - 并发初始化:目标 profile 目录用非递归
mkdirSync(dir)申请;EEXIST会被明确拒绝,不复用潜在的另一个进程创建的状态。初始化失败会清理该目录;若清理失败则同时保留原错和清理错。实现。
Performance, Operations & Extension Points
runCli 根据 mode 动态导入分支实现,且 config dump 不运行应用参数提供者;不能把 dump 结果解释为应用参数影响后的启动状态。overlay 路径在组合时解析为绝对路径,随后依命令行顺序加载。启动器将剩余参数透传给应用插件:新增应用级参数应由相应插件解释,而不是在此处添加全局解析规则。动态导入、导出限制说明、overlay 加载。未读取到 headless 的具体插件实现或测试,因此无法确认其退出码约定、输出格式、超时/重试策略、会话持久化及并发任务行为。