跳转到内容

插件管理器与安装器内部机制

本文档描述了 xcsh plugin 操作如何修改磁盘上的插件状态,以及已安装的插件如何成为运行时能力(目前支持工具,钩子/命令路径解析可用)。

代码库中有两个插件管理实现:

  1. CLI 命令使用的活跃路径PluginManagersrc/extensibility/plugins/manager.ts
  2. 旧版辅助模块:安装器函数(src/extensibility/plugins/installer.ts

xcsh plugin ... 命令执行通过 PluginManager 进行。

installer.ts 仍然记录了重要的安全检查和文件系统行为,但它不是 src/commands/plugin.ts + src/cli/plugin-cli.ts 所使用的路径。

生命周期:从 CLI 调用到运行时可用

Section titled “生命周期:从 CLI 调用到运行时可用”
xcsh plugin <action> ...
-> src/commands/plugin.ts
-> runPluginCommand(...) in src/cli/plugin-cli.ts
-> PluginManager method (install/list/uninstall/link/...)
-> mutate ~/.xcsh/plugins/{package.json,node_modules,xcsh-plugins.lock.json}
-> runtime discovery: discoverAndLoadCustomTools(...)
-> getAllPluginToolPaths(cwd)
-> custom tool loader imports tool modules
  • src/commands/plugin.ts 定义命令/标志并转发到 runPluginCommand
  • src/cli/plugin-cli.ts 将子命令映射到 PluginManager 方法:
    • installuninstalllistlinkdoctorfeaturesconfigenabledisable
  • 不存在显式的 update 操作;更新通过使用新的包/版本规范重新运行 install 来完成。

全局插件状态位于 ~/.xcsh/plugins 下:

  • package.jsonbun install/bun uninstall 使用的依赖清单
  • node_modules/ — 已安装的插件包或符号链接
  • xcsh-plugins.lock.json — 运行时状态:
    • 每个插件的启用/禁用状态
    • 每个插件选定的功能集
    • 持久化的插件设置

项目本地覆盖位于:

  • <cwd>/.xcsh/plugin-overrides.json

从管理器/加载器的角度来看,覆盖是只读的(此处没有写入路径),可以为当前项目禁用插件或覆盖功能/设置。

parsePluginSpecparser.ts)支持:

  • pkg -> features: null(默认行为)
  • pkg[*] -> 启用所有清单功能
  • pkg[] -> 不启用可选功能
  • pkg[a,b] -> 启用指定功能
  • @scope/pkg@1.2.3[feat] -> 带作用域 + 版本号的包,显式选择功能

extractPackageName 去除版本后缀,用于安装后的磁盘路径查找。

清单按以下顺序解析:

  1. package.json.xcsh
  2. 回退到 package.json.pi
  3. 回退到 { version: package.version }

影响:

  • 管理器/加载器中没有严格的模式验证。
  • 缺少 xcsh/pi 的包仍然可以安装和列出。
  • 运行时插件加载(getEnabledPlugins)会跳过没有 xcsh/pi 清单的包。
  • manifest.version 始终从包的 version 字段覆盖。

格式错误的 package.json JSON 在读取时会导致硬失败;格式错误的清单结构可能仅在使用特定字段时才会失败。

安装/更新流程(PluginManager.install

Section titled “安装/更新流程(PluginManager.install)”
  1. 从安装规范中解析功能括号语法。
  2. 通过正则表达式 + shell 元字符拒绝列表验证包名。
  3. 确保插件 package.json 存在(xcsh-plugins,私有依赖映射)。
  4. ~/.xcsh/plugins 中运行 bun install <packageSpec>
  5. 读取已安装包的 node_modules/<name>/package.json
  6. 解析清单并计算 enabledFeatures
    • [*]:所有声明的功能(如果没有功能映射则为 null
    • [a,b]:验证每个功能是否存在于清单功能映射中
    • []:空功能列表
    • 裸规范:null(稍后在加载器中使用默认策略)
  7. 更新或插入锁文件运行时状态:{ version, enabledFeatures, enabled: true }

由于更新是由安装驱动的:

  • xcsh plugin install pkg@newVersion 会更新依赖和锁文件版本。
  • 现有设置被保留;状态条目中的版本/功能/启用状态会被覆盖。
  • 不存在单独的”检查更新”或事务性迁移逻辑。
  1. 验证包名。
  2. 在插件目录中运行 bun uninstall <name>
  3. 从锁文件中移除插件运行时状态:
    • config.plugins[name]
    • config.settings[name]

如果卸载命令失败,运行时状态不会更改。

  1. ~/.xcsh/plugins/package.json 读取插件依赖映射。
  2. 加载锁文件运行时配置(文件缺失 -> 空默认值)。
  3. 加载项目覆盖(<cwd>/.xcsh/plugin-overrides.json,解析/读取错误 -> 空对象并发出警告)。
  4. 对于每个具有可解析 package.json 的依赖:
    • 构建 InstalledPlugin 记录
    • 合并功能/启用状态:
      • 基础状态来自锁文件(或默认值)
      • 项目覆盖可以替换功能选择
      • 项目的 disabled 列表将插件标记为禁用

这是 CLI 状态输出和设置/功能操作使用的有效状态。

link 通过将本地包符号链接到 ~/.xcsh/plugins/node_modules/<pkg.name> 来支持本地插件开发。

行为:

  1. 根据管理器 cwd 解析 localPath
  2. 要求本地 package.jsonname 字段。
  3. 确保插件目录存在。
  4. 对于带作用域的名称,创建作用域目录。
  5. 移除目标链接位置的现有路径。
  6. 创建符号链接。
  7. 添加运行时锁文件条目,启用并使用默认功能(null)。

注意:当前的 PluginManager.link 没有强制执行旧版 installer.ts 中存在的 cwd 路径边界检查(normalizedPath.startsWith(normalizedCwd)),因此信任由调用者负责。

运行时加载:从已安装插件到可调用能力

Section titled “运行时加载:从已安装插件到可调用能力”

getEnabledPlugins(cwd)plugins/loader.ts)读取:

  • 插件依赖清单(package.json
  • 锁文件运行时状态
  • 通过 getConfigDirPaths("plugin-overrides.json", { user: false, cwd }) 获取的项目覆盖

过滤条件:

  • 如果没有插件 package.json 则跳过
  • 如果清单(xcsh/pi)不存在则跳过
  • 如果在锁文件中全局禁用则跳过
  • 如果项目禁用则跳过

对于每个已启用的插件:

  • resolvePluginToolPaths(plugin)
  • resolvePluginHookPaths(plugin)
  • resolvePluginCommandPaths(plugin)

每个解析器包含基础条目和功能条目:

  • 显式功能列表 -> 仅选定的功能
  • enabledFeatures === null -> 启用标记为 default: true 的功能

缺失的文件会被静默跳过(existsSync 守卫)。

  • 工具目前已连接到运行时,通过 discoverAndLoadCustomToolscustom-tools/loader.ts)调用 getAllPluginToolPaths(cwd)
  • 路径在自定义工具发现中通过解析后的绝对路径进行去重(seen 集合,先到先得)。
  • 钩子/命令解析器已存在并已导出,但此代码路径目前没有像工具那样将它们连接到运行时注册表中。

PluginManager 在每个实例的内存中缓存运行时配置(#runtimeConfig),并延迟加载一次。

加载行为:

  • 锁文件缺失 -> { plugins: {}, settings: {} }
  • 锁文件读取/解析失败 -> 警告 + 相同的空默认值

保存行为:

  • 每次变更时写入完整的格式化锁文件 JSON

不存在跨进程锁定或合并策略;并发写入者可能会相互覆盖。

活跃管理器路径强制执行包名验证:

  • 适用于带作用域/不带作用域包规范的正则表达式(可选带版本)
  • 显式 shell 元字符拒绝列表([;&|$(){}[]<>\]`)

这限制了调用 bun install/uninstall 时的命令注入风险。

  • 插件代码在导入自定义工具模块时在进程内执行;没有沙箱隔离。
  • 清单相对路径与插件包目录拼接,仅进行存在性检查。
  • 插件包一旦安装即被视为受信任代码。

installer.ts 包含未在 PluginManager.link 中镜像的额外链接时检查:

  • 本地路径必须解析到项目 cwd 内部
  • 针对符号链接目标命名的额外包名/路径穿越守卫

由于 CLI 使用 PluginManager,这些更严格的链接守卫目前不在主路径上。

插件管理器不是事务性的。

操作阶段失败行为回滚
bun install 失败安装中止并输出 stderr不适用(尚未写入状态)
安装成功,但清单/功能验证失败命令失败不进行卸载回滚;依赖可能保留在 node_modules/package.json
安装成功,但锁文件写入失败命令失败不回滚已安装的包
bun uninstall 成功,锁文件写入失败命令失败包已移除,过时的运行时状态可能保留
link 移除旧目标后符号链接创建失败命令失败不恢复之前的链接/目录

在运维层面,doctor --fix 可以修复部分漂移(bun install、孤立配置清理、无效功能清理),但属于尽力而为。

  • 缺失 xcsh/pi 字段:
    • 安装/列表:可容忍(最小清单)
    • 运行时已启用插件发现:作为非插件跳过
  • 安装规范或 features --set/--enable 引用了缺失的功能:硬错误并显示可用功能列表
  • 无效的 plugin-overrides.json:在管理器和加载器路径中均被忽略,回退到 {}
  • 清单引用的工具/钩子/命令文件路径缺失:在解析器展开期间静默忽略;仅由 doctor 标记为错误
  • --dry-run(安装):返回合成的安装结果,不进行文件系统/网络/状态写入。
  • --json:仅影响输出格式,不改变行为。
  • 项目覆盖在功能/设置视图中始终优先于全局锁文件。
  • 有效启用状态为 runtimeEnabled && !projectDisabled