Merge branch 'feat/plugin-mgmt-3-settings' into feat/plugin-mgmt-4-web
This commit is contained in:
commit
8866b37604
6 files changed
+24
-10
No files matched your search
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
|
||||
README.md: d8251091c67d562698e69459bda44ea805914833
|
||||
README.zh.md: c2ecfa9b5717529b041a45c3773ab624719e5a1b
|
||||
README.md: 51ccb305c5afce576093f6b432dbb6211d800b75
|
||||
README.zh.md: 16dbc5f3caac0de66c9e6bf8d73b43274f71d598
|
||||
@@ -89,7 +89,7 @@ This section explains how the outcomes above are realized and points at the code
|
||||
- **External bundles are groups.** The vendored `EntryGroup.update` is all-or-nothing, so `composeExternalLayer` wraps each `runtime`-stage external layer's inserts in one `cordis:contained-group` under the ids the bundle declares; the group's `create()` records a failed row on the root's `pluginFailures` registry instead of rejecting, and `assertEntriesActivated` exempts recorded rows while still failing a built-in row left pending.
|
||||
- **Row ids are owned, not rewritten.** Entry ids are unique per tree and a `create()` that finds an existing id re-parents that entry instead of rejecting, so `composeProfileStack` decides ownership before anything mounts: built-in and boot-staged layers claim first and a duplicate among them fails the boot, a contained bundle that collides is left out whole, a user insert of a taken id is dropped, and every such row is a `conflict` record in `pluginFailures`. Boot, live recomposition, and `--dump-config` compose through the same function.
|
||||
- **Fail-loud is boot-scoped.** `installFailLoud` exits on any unhandled rejection because during startup one is a load failure; the launcher uninstalls it once the tree is up and installs `installRuntimeGuards`, which reports a rejection and keeps running and exits on an uncaught exception. Nested fibers (a `ctx.inject()` continuation) that fail under a built-in entry are reported by `warnNestedFiberFailures` as advisory lines.
|
||||
- **The probe never runs a package in the host.** `probePackage` reads an installed package's manifest here and imports it in a child process, so a package that throws, exits, hangs, or brings its own copy of cordis costs one child and yields a record with the reason. It calls a package a `plugin` only when the package declares itself to dsh — a `dsh` section or a dependency on `@deepseek-ai/cordis` — and its main export is plugin-shaped; a bare function export (`lodash`) is a `library`.
|
||||
- **The probe never runs a package in the host.** `probePackage` reads an installed package's manifest here and imports it in a child process, so a package that throws, exits, hangs, or brings its own copy of cordis costs one child and yields a record with the reason. It calls a package a `plugin` only when the package declares itself to dsh — a `dsh` section or a dependency on `@deepseek-ai/cordis` — and its main export is plugin-shaped; a bare function export (`lodash`) is a `library`. Records are cached under the profile's `.dsh-plugins/` with a format number, so a record an older probe wrote is probed again rather than trusted.
|
||||
- **Profile module fallback.** Bare plugin specifiers resolve through the Loader from the config directory. Plain Node maintains one symlink per package in the installation dependency closure. A packaged executable instead reads each installed export map with Node ESM conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Missing exports stay unavailable, malformed maps fail startup, and a cross-process writer lock replaces stale entries without exposing partial proxies. A selected external bundle absent from the installation closure receives a profile-local `.dsh-module-fallback` link; existing pnpm entries win, projected links are excluded from later closure discovery, and cleanup removes only dsh-owned links.
|
||||
- **One rejection checkpoint.** `assertEntriesActivated` keeps the exact reasons it folds into the boot diagnostic visible through the next process rejection checkpoint, so `installFailLoud` coalesces Loader's duplicate notification while unrelated unhandled rejections remain fatal.
|
||||
- **Two-stage failure labels.** `boot()` distinguishes `host preparation failed` — `prepare` threw before any config-tree entry mounted — from `plugin tree failed to load`, and appends the deepest plugin error's stack so the startup diagnostic preserves the original activation error instead of only the wrap chain.
|
||||
|
||||
@@ -89,7 +89,7 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
|
||||
- **外部组合包即组。** vendored 的 `EntryGroup.update` 是整组事务,因此 `composeExternalLayer` 把每个 `runtime` 阶段外部层的插入行按组合包声明的 id 包进一个 `cordis:contained-group`;该组的 `create()` 把失败的行记录到根上的 `pluginFailures` 注册表而不是 reject,`assertEntriesActivated` 豁免已记录的行,但内置行停在 pending 时仍然失败。
|
||||
- **行 id 归属而非改写。** entry id 在整棵树内唯一,而 `create()` 遇到已有 id 时会把那个 entry 挪到自己名下而不是 reject,所以 `composeProfileStack` 在任何行挂载之前先判定归属:内置层与 boot 阶段的层先占有 id,它们之间重复即启动失败;撞名的受控组合包整层排除;用户层插入已被占用的 id 时该行丢弃;每一条被排除的行都是 `pluginFailures` 里的一条 `conflict` 记录。启动、运行时重组与 `--dump-config` 走同一个函数。
|
||||
- **fail-loud 只在启动期。** `installFailLoud` 对任何未处理 rejection 退出,因为启动期间它就是加载失败;树起来后 launcher 卸载它并安装 `installRuntimeGuards`:rejection 被报告并继续运行,未捕获异常被报告并退出。内置条目下失败的嵌套 fiber(`ctx.inject()` 的延续)由 `warnNestedFiberFailures` 以提示行报告。
|
||||
- **探针从不在宿主内运行包。** `probePackage` 在本进程读取已安装包的 manifest,在子进程里 import 它,因此抛错、退出、挂起或自带 cordis 副本的包只消耗一个子进程,得到一条带原因的记录。只有包向 dsh 声明了自己——有 `dsh` 段或依赖 `@deepseek-ai/cordis`——且主导出是插件形状时才判为 `plugin`;光是导出一个函数(`lodash`)的包是 `library`。
|
||||
- **探针从不在宿主内运行包。** `probePackage` 在本进程读取已安装包的 manifest,在子进程里 import 它,因此抛错、退出、挂起或自带 cordis 副本的包只消耗一个子进程,得到一条带原因的记录。只有包向 dsh 声明了自己——有 `dsh` 段或依赖 `@deepseek-ai/cordis`——且主导出是插件形状时才判为 `plugin`;光是导出一个函数(`lodash`)的包是 `library`。记录缓存在 profile 的 `.dsh-plugins/` 下并带格式号,旧版探针写的记录会重新探测而不是被信任。
|
||||
- **Profile 模块后备机制。** 裸插件 specifier 由 Loader 从配置目录解析。普通 Node 会为安装依赖闭包中的每个包维护一个符号链接。打包可执行文件无法让操作系统符号链接进入 pkg 的 `/snapshot` 树,因此会按 Node ESM 条件读取已安装包的 export map,并写入重新导出虚拟模块 URL 的真实代理包。缺失 export 保持不可用,错误 export map 会让启动失败,跨进程 writer lock 则会在不暴露部分代理的情况下替换陈旧条目。所选外部 bundle 若不在安装闭包中,则会获得 profile 本地的 `.dsh-module-fallback` 链接;已有 pnpm 条目优先,后续闭包发现会排除投影链接,清理也只删除 dsh 自有链接。
|
||||
- **单一 rejection 检查点。** `assertEntriesActivated` 把折入启动诊断的确切原因保持到下一个进程级 rejection 检查点可见,使 `installFailLoud` 能合并 Loader 的重复通知,而所有无关的未处理 rejection 仍然致命。
|
||||
- **两阶段失败标签。** `boot()` 区分 `host preparation failed`(`prepare` 在任何配置树条目挂载前抛出)与 `plugin tree failed to load`(此后的一切失败),并追加最深层插件错误的堆栈,使启动诊断保留原始激活错误,而不只是包装链。
|
||||
|
||||
@@ -75,7 +75,7 @@ export {
|
||||
ProfileRuntime, type ProfileRuntimeOptions, type RowOrigin,
|
||||
} from './profile-runtime.ts'
|
||||
export {
|
||||
PLUGIN_PROBE_DIR, probePackage, readProbeCache, writeProbeCache,
|
||||
PLUGIN_PROBE_DIR, PLUGIN_PROBE_FORMAT, probePackage, readProbeCache, writeProbeCache,
|
||||
type PluginProbe, type PluginProbeRow, type ProbeOptions,
|
||||
} from './probe.ts'
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ import { readProfileManifest, resolveBundleDir, type ProfileManifest } from './p
|
||||
/** Directory under a profile holding one probe record per package. */
|
||||
export const PLUGIN_PROBE_DIR = '.dsh-plugins'
|
||||
|
||||
/**
|
||||
* The probe record format. Bumped when the probe's verdicts change meaning,
|
||||
* so a record an older probe wrote is re-probed instead of trusted: 2 made a
|
||||
* bare function export a `library` rather than a `plugin`.
|
||||
*/
|
||||
export const PLUGIN_PROBE_FORMAT = 2
|
||||
|
||||
/** One row a bundle's patch inserts, as its own patch declares it. */
|
||||
export interface PluginProbeRow {
|
||||
/** The row id the bundle declares, or undefined when the row leaves it to the Loader. */
|
||||
@@ -341,17 +348,19 @@ function probeCachePath(profileDir: string, packageName: string): string {
|
||||
* @param profileDir - the profile directory.
|
||||
* @param packageName - the package.
|
||||
* @param version - when given, a record for a different version is treated as absent.
|
||||
* @returns the record, or undefined when none is cached.
|
||||
* @returns the record, or undefined when none is cached or the cached one was written by another probe format.
|
||||
*/
|
||||
export function readProbeCache(profileDir: string, packageName: string, version?: string): PluginProbe | undefined {
|
||||
const path = probeCachePath(profileDir, packageName)
|
||||
if (!existsSync(path)) return undefined
|
||||
let record: PluginProbe
|
||||
let stored: PluginProbe & { format?: number }
|
||||
try {
|
||||
record = JSON.parse(readFileSync(path, 'utf8')) as PluginProbe
|
||||
stored = JSON.parse(readFileSync(path, 'utf8')) as PluginProbe & { format?: number }
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
const { format, ...record } = stored
|
||||
if (format !== PLUGIN_PROBE_FORMAT) return undefined
|
||||
if (version !== undefined && record.version !== version) return undefined
|
||||
return record
|
||||
}
|
||||
@@ -364,5 +373,5 @@ export function readProbeCache(profileDir: string, packageName: string, version?
|
||||
export function writeProbeCache(profileDir: string, probe: PluginProbe): void {
|
||||
const path = probeCachePath(profileDir, probe.packageName)
|
||||
mkdirSync(join(profileDir, PLUGIN_PROBE_DIR), { recursive: true })
|
||||
writeFileSync(path, JSON.stringify(probe, undefined, 2) + '\n')
|
||||
writeFileSync(path, JSON.stringify({ format: PLUGIN_PROBE_FORMAT, ...probe }, undefined, 2) + '\n')
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { PLUGIN_PROBE_DIR, probePackage, readProbeCache, writeProbeCache, type PluginProbe } from '../src/index.ts'
|
||||
import { PLUGIN_PROBE_DIR, PLUGIN_PROBE_FORMAT, probePackage, readProbeCache, writeProbeCache, type PluginProbe } from '../src/index.ts'
|
||||
|
||||
const NAME = 'dsh-test-bin'
|
||||
|
||||
@@ -220,5 +220,10 @@ describe('probe cache', () => {
|
||||
expect(readProbeCache(profileDir, '@scope/pkg', '2.0.0')).toBeUndefined()
|
||||
writeFileSync(join(profileDir, PLUGIN_PROBE_DIR, '@scope__pkg.json'), '{ not json')
|
||||
expect(readProbeCache(profileDir, '@scope/pkg')).toBeUndefined()
|
||||
// A record an older probe wrote — no format, or another one — is probed again, not trusted.
|
||||
writeFileSync(join(profileDir, PLUGIN_PROBE_DIR, '@scope__pkg.json'), JSON.stringify(record))
|
||||
expect(readProbeCache(profileDir, '@scope/pkg')).toBeUndefined()
|
||||
writeFileSync(join(profileDir, PLUGIN_PROBE_DIR, '@scope__pkg.json'), JSON.stringify({ format: PLUGIN_PROBE_FORMAT - 1, ...record }))
|
||||
expect(readProbeCache(profileDir, '@scope/pkg')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user