Module Structure
该组件提供各运行表面共享的进程级基础状态与观测入口;它区分启动目录、当前工作区和稳定项目根,并把 UI 状态、日志、环境探测及项目发现收敛到可被 Agent、CLI、Server、工具和扩展复用的 API。
Directory Layout
packages/cli/src/bootstrap/state.ts— 惰性初始化的 cwd、originalCwd 与 projectRoot 三态packages/cli/src/store/— React 与非 React 共用的 Zustand Store、切片、类型和细粒度选择器packages/cli/src/logging/— 分类 JSONL 日志与独立流式调试日志packages/cli/src/utils/cwd.ts— 基于 AsyncLocalStorage 的并发 cwd 覆写packages/cli/src/utils/environment.ts— 项目根发现、环境提示和目录概览packages/cli/src/utils/packageInfo.ts— 源码与打包布局兼容的运行时包信息packages/cli/src/utils/sessionId.ts— 可安全用于存储路径的 Session IDpackages/cli/src/services/DirectoryPicker.ts— macOS、Windows 与 Linux 原生目录选择packages/cli/src/services/ProjectRegistry.ts— 规范化项目路径的持久绑定注册表
Key Entry Points
getCwd()/runWithCwdOverride()— 获取当前执行上下文的 workspace,或为异步调用链临时覆写vanillaStore/useBladeStore()/ensureStoreInitialized()— 共享状态实例、React 订阅与幂等配置初始化createLogger()/setLoggerSessionId()/streamDebug()— 分类日志、会话日志路由与流式专项诊断getEnvironmentContext()/getVersion()/createSessionId()— 构造环境上下文、读取运行版本和生成存储安全标识NativeDirectoryPicker.pick()/ProjectRegistry.list()— 选择本机目录并读取规范化项目集合
API Surface
Workspace Location
getCwd()— 优先返回当前 AsyncLocalStorage 覆写,否则返回进程级 cwdrunWithCwdOverride(cwd, fn)— 在同步及后续异步调用链内隔离 workspacegetOriginalCwd()— 返回 CLI 被调用时的原始目录,用于解析用户传入的相对路径getProjectRoot()— 返回启动后保持稳定的项目标识根findProjectRoot(startDir)/setCwd(newPath, relativeTo?)— 发现工作区根并更新规范化的全局 cwd
Shared Store
useBladeStore(selector)— React 对共享 Vanilla Store 的细粒度订阅入口getState()— Agent、服务与命令读取当前 Store 快照sessionActions()/appActions()/configActions()— 面向不同状态域的动作入口ensureStoreInitialized()— 以共享 Promise 合并并发配置初始化useCurrentStreamingBuffer()— 以浅比较订阅流式行缓冲、尾部、计数和版本
Observability
createLogger(category, options?)— 创建分类日志器,文件输出与终端调试过滤相互独立Logger.setGlobalDebug(config)— 动态设置全局终端日志开关或分类过滤setLoggerSessionId(sessionId)— 切换后续 JSONL 日志的文件名streamDebug(source, message, data?)— 向独立流式诊断文件追加记录且不向调用方抛错getEnvironmentContext(options?)— 组装系统、Git、脚本和目录信息供系统提示使用
Runtime Identity And Project Discovery
getPackageInfo()/getVersion()— 从运行时邻近 package.json 读取并缓存实际发布信息createSessionId(prefix?, size?)— 校验前缀后生成可用于文件名的 IDNativeDirectoryPicker.pick()— 跨平台打开至多一个并发原生目录选择器ProjectRegistry.bind()/list()/unbind()— 规范化、持久化并查询项目绑定
Usage Examples
CLI 启动时固定项目根
const invocationCwd = process.cwd();
const detectedRoot = findProjectRoot(invocationCwd);
setCwd(detectedRoot);
setProjectRoot(getCwd());
启动 Server 前确保共享 Store 可用
await ensureStoreInitialized();
const server = await BladeServer.listenAsync(opts);
TUI 初始化完成后绑定会话日志
const state = getState();
const sessionId = state.session.sessionId;
setLoggerSessionId(sessionId);
Gotchas
originalCwd、cwd与projectRoot不能互换:CLI 相对参数以调用目录解析,运行工具使用可变 cwd,历史、Skill 和 Session 等项目身份使用启动后固定的 projectRoot (packages/cli/src/bootstrap/state.ts,packages/cli/src/blade.tsx,packages/cli/src/cli/settings.ts,packages/cli/src/mcp/loadMcpConfig.ts)- CLI 必须先执行
findProjectRoot()、setCwd()和setProjectRoot()再启动依赖 workspace 的流程,否则配置、信任与资源发现会以调用子目录为根 (packages/cli/src/blade.tsx,packages/cli/src/utils/environment.ts) - 并发 Session 或子代理不能用全局
setCwd()做临时切换;应使用runWithCwdOverride(),其 AsyncLocalStorage 覆写会沿异步调用链传播且不污染其他执行上下文 (packages/cli/src/utils/cwd.ts,git:4543542b) setCwd()解析相对路径时默认基于真实process.cwd()而不是当前 AsyncLocalStorage 覆写;在隔离上下文内传相对路径必须显式提供relativeTo(packages/cli/src/utils/environment.ts,packages/cli/src/utils/cwd.ts)- Store 是否初始化仅以
config.config !== null判断;CLI、Server、ACP 和 Hook 等非 React 入口在读取配置前都必须等待ensureStoreInitialized(),并发调用会共享同一个初始化 Promise (packages/cli/src/store/vanilla.ts,packages/cli/src/commands/serve.ts,packages/cli/src/acp/BladeAgent.ts) resetSession()只重置 Zustand 内的 Session 字段并保留 sessionId,不会隐式清空模块级conversationProjection;切换事件流时还要显式调用resetConversationProjection()(packages/cli/src/store/slices/sessionSlice.ts)Logger的 debug 开关只控制终端输出,所有级别仍同步追加到 JSONL;不要通过关闭 debug 假设磁盘日志已停用 (packages/cli/src/logging/Logger.ts)- 日志文件路由使用进程级
currentSessionId而非异步上下文;TUI 绑定 Session 前的记录进入blade-default.jsonl,该机制也不能区分并发 Server Session (packages/cli/src/logging/Logger.ts,packages/cli/src/ui/App.tsx) - 日志目录初始化只尝试一次;权限或创建失败后会缓存不可用状态,当前进程内即使外部修复目录也不会自动重试 (
packages/cli/src/logging/Logger.ts) streamDebug()不受Logger.setGlobalDebug()控制,并会在每个 storage root 的首次调用时重写stream-debug.log;它是临时专项诊断通道,不是普通分类日志 (packages/cli/src/logging/StreamDebugLogger.ts,packages/cli/src/ui/utils/loopEventHandler.ts)ProjectRegistry遇到缺失、损坏或版本不匹配的注册表会按空集合继续,并始终临时注入当前项目;因此列表只剩当前项目不代表绑定从未存在 (packages/cli/src/services/ProjectRegistry.ts,packages/cli/tests/unit/services/project-registry.test.ts)- 原生目录选择服务本身不校验请求来源,只有 Server 路由拒绝非本机 Origin;新增调用表面时必须复用同等访问边界 (
packages/cli/src/services/DirectoryPicker.ts,packages/cli/src/server/routes/projects.ts) - Linux 目录选择仅在命令不存在时从
zenity回退到kdialog;已安装选择器返回真实错误时会立即失败,而空输出的退出码 1 被视为用户取消 (packages/cli/src/services/DirectoryPicker.ts,packages/cli/tests/unit/platform/services/directory-picker.test.ts)
Architecture
bootstrap/state.ts保存进程级三态,cwd.ts只在其上增加异步上下文覆盖;绝大多数业务代码应依赖getCwd(),只有启动和测试夹具直接操作底层状态 (packages/cli/src/bootstrap/state.ts,packages/cli/src/utils/cwd.ts)- 单一
vanillaStore同时服务 Ink React 订阅和 Agent、服务、命令的命令式访问;Store 不使用 persist 中间件,配置和会话持久化分别交给 ConfigService 与 Context/JSONL (packages/cli/src/store/index.ts,packages/cli/src/store/vanilla.ts) - Session Store 同时投影持久消息、临时流式缓冲和 Provider/停滞/重试等观测事件;命令结束或取消会集中清除易过期的运行态诊断,避免下一轮展示旧状态 (
packages/cli/src/store/slices/sessionSlice.ts,packages/cli/src/store/slices/commandSlice.ts,git:2aa2b22b,git:e6bd4e15) getEnvironmentContext()默认采集 Git 分支、工作树、最近提交、根目录清单和常用脚本;单项命令失败只省略对应片段,不会阻断提示构建 (packages/cli/src/utils/environment.ts,packages/cli/tests/unit/platform/utils/environment.test.ts,git:7680e3eb)- 项目根发现会继续向上寻找
.git、.blade或.claude,仅在没有这些标记时回退到最近的 package.json,因此 monorepo 子包默认归属仓库根 (packages/cli/src/utils/environment.ts,packages/cli/tests/unit/platform/utils/environment.test.ts) - 项目注册表以 realpath 作为去重身份,保留已失联项目供用户修复或解绑,并把当前项目固定排在名称排序之前 (
packages/cli/src/services/ProjectRegistry.ts,packages/cli/tests/unit/services/project-registry.test.ts)
Decisions
- cwd 隔离采用 AsyncLocalStorage 而不是
process.chdir(),使并行子代理、ACP 和真实 API 驱动可在同一进程内保持各自 workspace (packages/cli/src/utils/cwd.ts,packages/cli/tests/support/browserToolAcpDriver.ts) - Zustand 只承担当前进程读模型,不承担磁盘权威状态;这一边界允许 Session 事件日志和配置服务独立处理恢复、原子写与版本迁移 (
packages/cli/src/store/vanilla.ts,packages/cli/src/store/types.ts) - 包版本改为从源码或 bundle 邻近目录向上搜索名称匹配的 package.json,避免发布包仍报告构建时导入的旧版本;找不到时才使用导入值或
BLADE_VERSION(packages/cli/src/utils/packageInfo.ts,git:092d356b) - Session ID 统一由带受限前缀的 nanoid 生成,替代可能包含路径非法字符的临时标识,使 TUI、Web、ACP、任务和子代理共享存储安全格式 (
packages/cli/src/utils/sessionId.ts,packages/cli/src/store/slices/sessionSlice.ts,git:d148a8fa)
Patterns
- React 消费者通过专用 selector 订阅最小状态片段,对对象组合使用
useShallow,空模型列表复用常量引用;非 React 消费者通过getState()和动作访问器共享同一实例 (packages/cli/src/store/index.ts,packages/cli/src/store/selectors/index.ts,packages/cli/src/store/vanilla.ts) - Store action 在并发清理时使用身份或请求 ID 防止迟到回调覆盖新状态,例如
clearAbortController(expectedController)和旁路对话完成动作都先核对当前所有者 (packages/cli/src/store/slices/commandSlice.ts,packages/cli/src/store/slices/appSlice.ts) - 项目绑定写入遵循 mutex 内 read-modify-write、原子替换和
0600文件权限;目录本身同步收紧到0700(packages/cli/src/services/ProjectRegistry.ts) NativeDirectoryPicker.pick()缓存进行中的 Promise,并在 settle 后清空,多个并发 Web 请求只会打开一个系统对话框 (packages/cli/src/services/DirectoryPicker.ts,packages/cli/tests/unit/platform/services/directory-picker.test.ts)
Dependencies
- 原生目录选择分别依赖 macOS
osascript、Windows PowerShell Forms、Linuxzenity或kdialog,统一受 15 分钟超时和 64 KiB 输出上限约束 (packages/cli/src/services/DirectoryPicker.ts) - 日志和项目注册都从 Blade storage root 派生文件位置;前者容忍写入失败,后者使用
write-file-atomic并向调用方传播非 ENOENT/语法错误 (packages/cli/src/logging/Logger.ts,packages/cli/src/services/ProjectRegistry.ts)
Consumer Analysis
- TUI 是共享 Store 的最高频消费者,通过 selector 渲染消息、焦点和运行态,并通过动作协调初始化、模型、权限和日志 Session 绑定 (
packages/cli/src/ui/App.tsx,packages/cli/src/ui/hooks/useAgent.ts,packages/cli/src/store/selectors/index.ts) - Server 与 CLI 命令在无 React 环境调用
ensureStoreInitialized()、配置动作、环境版本和项目注册服务,启动顺序错误会直接影响 Web/Headless 可用性 (packages/cli/src/commands/serve.ts,packages/cli/src/server/server.ts,packages/cli/src/server/routes/projects.ts) - Agent、SessionRuntime 与子代理使用共享 Store 投影运行状态,并以
getCwd()或 AsyncLocalStorage 覆写确定每次执行的 workspace (packages/cli/src/agent/Agent.ts,packages/cli/src/agent/runtime/SessionRuntime.ts,packages/cli/src/agent/subagents/BackgroundAgentManager.ts) - 工具、Hooks、MCP、Plugins 和 LSP 广泛依赖
getCwd()与分类 Logger;这些扩展必须优先使用显式context.workspaceRoot,缺失时才回退进程上下文 (packages/cli/src/tools/execution/ToolExecutor.ts,packages/cli/src/hooks/HookManager.ts,packages/cli/src/plugins/PluginRegistry.ts,packages/cli/src/lsp/LspSessionManager.ts) - SessionService、PersistentStore 和协议适配层消费运行版本与存储安全 ID,把同一身份格式写入会话元数据、JSONL 路径、ACP 握手和 MCP 客户端信息 (
packages/cli/src/services/SessionService.ts,packages/cli/src/context/storage/PersistentStore.ts,packages/cli/src/acp/BladeAgent.ts,packages/cli/src/mcp/McpClient.ts)