Revert "feat(plugin-manager): refuse to switch a row alone when its bundle switches its rows as a whole"

This reverts commit 5bc789e8617bc7b7fe8b56ad3629ed385692c54d.
This commit is contained in:
ZiyaZhang committed 2026-09-25 06:57:32 -07:00
1 parent dbd98c2b7e
commit 6085a56eb6
26 files changed
+69 -152

No files matched your search

-4
View File
@@ -333,10 +333,6 @@ describe('web e2e: plugin manager', () => {
await expect.poll(running, { timeout: 20_000 }).toBe(3)
await expect.poll(() => rows.locator('[data-plugin-row]', { hasText: '运行中' }).count(), { timeout: 20_000 }).toBe(3)
expect(await rows.getByRole('switch').count()).toBe(0)
// The Host refuses to switch one of the rows alone, so the Remote and the tool agree with the page.
const plugins = await scaffold.ctx.pluginManager.listPlugins()
expect(scheduleRows().map(entry => plugins.find(plugin => plugin.entryId === entry.id)?.readOnlyReason))
.toEqual(['bundle-switch', 'bundle-switch', 'bundle-switch'])
await page.getByRole('navigation', { name: '全局面板' }).getByRole('button', { name: '自动化任务', exact: true }).waitFor()
} finally {
if (await toggle.getAttribute('aria-checked') === 'true') await toggle.click()
+3 -3
View File
@@ -56,9 +56,9 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:246`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:252`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:259`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:245`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:251`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:258`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:471`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:55`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+3 -3
View File
@@ -58,9 +58,9 @@
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:246`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:252`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:259`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:245`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:251`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:258`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:471`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:55`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
-1
View File
@@ -59,7 +59,6 @@ export {
DEFAULT_PROFILE_BUNDLES,
OPTIONAL_BUNDLES,
bundlePatchFiles,
bundleRowSwitches,
bundlePatchPaths,
initProfile,
removeLinkProjections,
-13
View File
@@ -63,19 +63,6 @@ export function bundlePatchFiles(bundle: DshBundleManifest): string[] {
return declared
}
/**
* Whether the plugin manager may switch the rows a bundle inserts one by one.
* @param bundle - the bundle's `dsh.bundle` declaration, as read from package.json.
* @returns the declared `rowSwitches`, or true when the bundle omits it.
* @throws {Error} when `rowSwitches` is present and not a boolean.
*/
export function bundleRowSwitches(bundle: DshBundleManifest): boolean {
const declared: unknown = bundle.rowSwitches
if (declared === undefined) return true
if (typeof declared !== 'boolean') throw new Error('dsh.bundle.rowSwitches must be a boolean')
return declared
}
/**
* Resolve a bundle declaration to its ordered absolute patch files.
* @param packageDir - absolute directory of the bundle package.
@@ -13,7 +13,6 @@ import { dirname, join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterAll, describe, expect, it, onTestFinished, vi } from 'vitest'
import {
bundleRowSwitches,
composeEntries,
createRuntimeResolution,
getDshRuntimeVersion,
@@ -236,16 +235,6 @@ describe('manifest round-trip', () => {
})
})
describe('bundleRowSwitches', () => {
it('defaults to row switches, reads a declared boolean, and fails loud on any other value', () => {
expect(bundleRowSwitches({ patch: './cordis.patch.yml' })).toBe(true)
expect(bundleRowSwitches({ patch: './cordis.patch.yml', rowSwitches: false })).toBe(false)
expect(bundleRowSwitches({ patch: './cordis.patch.yml', rowSwitches: true })).toBe(true)
const declared = JSON.parse('{"patch":"./cordis.patch.yml","rowSwitches":"false"}') as Parameters<typeof bundleRowSwitches>[0]
expect(() => bundleRowSwitches(declared)).toThrow('dsh.bundle.rowSwitches must be a boolean')
})
})
describe('resolveBundleDir', () => {
it('prefers the installation anchor, falls back to the profile, and fails loud', () => {
const anchor = stageInstallation({ 'in-box': { patch: '[]\n' } })
@@ -15,8 +15,8 @@
en: 87679c51aa3693f5
zh: 11684a48a5806afb
/deepseek-ai-dsh-plugin-manager/use-this-package:
en: a11b1e73a1ffa4a2
zh: c86572342a6b1749
en: 6d0181eebb089d9d
zh: 73b0c782bd017dd4
/deepseek-ai-dsh-plugin-manager/use-this-package/version-compatibility-and-exemptions:
en: 920c0acb3b9feaf3
zh: ba6e832a256880a7
+2 -2
View File
@@ -37,11 +37,11 @@ For a deployment without agent presets, enable the tool in the profile patch. Pr
disabled: false
```
A plugin toggle updates only `disabled` in the last matching override in the profile's `cordis.patch.yml`, or appends an override when none matches. Matching uses the entry id and any module-name assertion. A row a selected bundle inserts under `dsh.bundle.rowSwitches: false` carries the `bundle-switch` read-only reason in `listPlugins`, and `setPluginEnabled` refuses it; the bundle toggle switches those rows together. A bundle toggle changes `package.json`'s ordered `dsh.profile.bundles` list. Disabling retains the dependency; enabling appends the bundle at the end, which can change configuration precedence. Installation enables a new bundle by default. Home and invocation patches retain their higher priority.
A plugin toggle updates only `disabled` in the last matching override in the profile's `cordis.patch.yml`, or appends an override when none matches. Matching uses the entry id and any module-name assertion. A bundle toggle changes `package.json`'s ordered `dsh.profile.bundles` list. Disabling retains the dependency; enabling appends the bundle at the end, which can change configuration precedence. Installation enables a new bundle by default. Home and invocation patches retain their higher priority.
A selected bundle that cannot load remains in `listBundles` with an `error`; `enabled` records the saved selection, not successful loading. The plugin page shows the error and allows deselection. A broken bundle cannot be enabled. Management bundles remain protected if their files become unreadable.
`listBundles` includes optional display `meta` for each bundle and its declared plugin rows, including disabled bundles. The Client selects a language from these values. The separate `description` field is the bundle's raw `package.json.description`; a metadata diagnostic does not block management. The `plugin_manager` tool omits UI display metadata from list results.
`listBundles` includes optional display `meta` for each bundle and its declared plugin rows, including disabled bundles. The Client selects a language from these values. The separate `description` field is the bundle's raw `package.json.description`; a metadata diagnostic does not block management. The `plugin_manager` tool omits UI display metadata from list results. `rowSwitches` reports the bundle's `dsh.bundle.rowSwitches`, true unless the bundle sets it to false.
`inspect(spec, options)` reads what a spec names before anything installs: a registry name is asked of the registry through `pnpm view`, run in the profile directory so the same proxy and authentication settings apply as to the install; an absolute path has its `package.json` read; a git address or tarball answers only its form and the `host` it is fetched from. The answer carries the name, version, description, whether the package declares a bundle, and the `registry` that answered, or a `problem`: `invalid-spec`, `already-installed`, `not-found`, `not-a-package`, `not-a-bundle`, `network`, or `unknown`, with the `registries` asked. A caller's `signal` or `inspectTimeoutMs` ends the lookup.
+2 -2
View File
@@ -37,11 +37,11 @@ kind: "package-reference"
disabled: false
```
插件开关只更新 profile 的 `cordis.patch.yml` 中最后一条匹配覆盖项的 `disabled`;没有匹配项时追加。匹配依据是条目 id,以及覆盖项声明的模块名称。已选组合包在 `dsh.bundle.rowSwitches: false` 下插入的行在 `listPlugins` 中带只读原因 `bundle-switch`,`setPluginEnabled` 拒绝单独开关它们;这些行随组合包开关一起开关。组合包开关修改 `package.json` 的有序 `dsh.profile.bundles` 列表。关闭保留依赖;开启追加到列表末尾,可能改变配置优先级。安装新组合包默认启用。home 和单次启动 patch 保留更高优先级。
插件开关只更新 profile 的 `cordis.patch.yml` 中最后一条匹配覆盖项的 `disabled`;没有匹配项时追加。匹配依据是条目 id,以及覆盖项声明的模块名称。组合包开关修改 `package.json` 的有序 `dsh.profile.bundles` 列表。关闭保留依赖;开启追加到列表末尾,可能改变配置优先级。安装新组合包默认启用。home 和单次启动 patch 保留更高优先级。
已选择但无法加载的组合包仍会出现在 `listBundles` 中,并携带 `error`;`enabled` 表示保存的选择,不代表加载成功。插件页面显示错误并允许取消选择。损坏的组合包无法启用。管理组合包的文件变得不可读后仍受保护。
`listBundles` 为各组合包及其声明的插件行提供可选的展示 `meta`,包括已禁用的组合包。Client 从这些值中选择语言。单独的 `description` 字段是该组合包原始的 `package.json.description`;元信息诊断不会阻止管理操作。`plugin_manager` 工具的列表结果不包含 UI 展示元信息。
`listBundles` 为各组合包及其声明的插件行提供可选的展示 `meta`,包括已禁用的组合包。Client 从这些值中选择语言。单独的 `description` 字段是该组合包原始的 `package.json.description`;元信息诊断不会阻止管理操作。`plugin_manager` 工具的列表结果不包含 UI 展示元信息。`rowSwitches` 报告组合包的 `dsh.bundle.rowSwitches`,除非组合包将其设为 false,否则为 true。
`inspect(spec, options)` 在任何东西安装之前读出 spec 指向什么:注册表包名通过 `pnpm view` 询问注册表,在 profile 目录中运行,因而与安装使用同样的代理与认证设置;绝对路径读取其 `package.json`;git 地址或 tarball 只答复自己的形式和它被拉取的 `host`。答复携带名称、版本、描述、该包是否声明组合包,以及作答的 `registry`,否则给出 `problem`:`invalid-spec`、`already-installed`、`not-found`、`not-a-package`、`not-a-bundle`、`network` 或 `unknown`,并附上问过的 `registries`。调用方的 `signal` 或 `inspectTimeoutMs` 会结束查询。
+12 -39
View File
@@ -13,7 +13,7 @@ import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol'
import { pluginEntryId, readPluginInventory } from '@deepseek-ai/dsh-host-plugin-inventory'
import {
readPluginMeta, readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries,
reconcileProfilePatches, readProfilePatches, OPTIONAL_BUNDLES, bundlePatchPaths, bundleRowSwitches,
reconcileProfilePatches, readProfilePatches, OPTIONAL_BUNDLES, bundlePatchPaths,
evaluatePluginCompatibility, readProfileCompatibility, readProfileVersionExemptions,
setProfileVersionExemption, PROFILE_COMPATIBILITY_FILENAME,
} from '@deepseek-ai/dsh-app-boot'
@@ -123,11 +123,6 @@ interface InstallationManifest {
dependencies?: Record<string, string>
}
/** The bundle layers a profile manifest selects, in order. */
function selectedBundles(manifest: ProfileManifest): string[] {
return manifest.dsh?.profile?.bundles ?? []
}
/** What a package manifest says about the package: identity, one-liner, and whether it is a bundle. */
function inspectionOf(kind: 'registry' | 'path', manifest: object, registry: Registry): Extract<PluginSpecInspection, { status: 'accepted' }> {
const dsh = (manifest as { dsh?: unknown }).dsh
@@ -260,7 +255,6 @@ export class PluginManager extends TypertRemoteService {
@Remote
async listPlugins(): Promise<PluginInfo[]> {
const rows = flatten(composeEntries([readProfilePatches('dsh', this.profile)]))
const grouped = this.groupedRows()
const snapshot = await readPluginInventory(this.ctx)
return snapshot.entries.map((entry) => {
const actual = [...this.ctx.loader.entries()].find(row => row.id === entry.entryId)
@@ -273,7 +267,6 @@ export class PluginManager extends TypertRemoteService {
|| actual?.parent.tree.ctx.fiber.entry?.id !== 'include') {
return { ...entry, readOnlyReason: 'unaddressable' as const }
}
if (grouped.has(candidate.id)) return { ...entry, readOnlyReason: 'bundle-switch' as const }
return { ...entry, patchId: candidate.id }
})
}
@@ -287,7 +280,7 @@ export class PluginManager extends TypertRemoteService {
listBundles(): Promise<BundleInfo[]> {
const manifest = readProfileManifest('dsh', this.profile.dir)
const exemptions = readProfileVersionExemptions(this.profile.dir)
const selected = selectedBundles(manifest)
const selected = manifest.dsh?.profile?.bundles ?? []
const dependencies = Object.keys(manifest.dependencies ?? {})
const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8')) as InstallationManifest
const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {})])]
@@ -302,7 +295,8 @@ export class PluginManager extends TypertRemoteService {
const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor)
if (info === undefined) {
if (enabled) bundles.push({ name, enabled, installed, optional, removable: removable && readOnlyReason === undefined,
...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: { code: 'not-bundle' }, rows: [], overrides: [] })
...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: { code: 'not-bundle' },
rows: [], rowSwitches: true, overrides: [] })
continue
}
const compatibility = evaluatePluginCompatibility(info, exemptions)
@@ -318,7 +312,8 @@ export class PluginManager extends TypertRemoteService {
} catch (error) {
if (enabled || installed) {
bundles.push({ name, enabled, installed, optional, removable: removable && readOnlyReason === undefined,
...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: managementError(error), rows: [], overrides: [] })
...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: managementError(error),
rows: [], rowSwitches: true, overrides: [] })
}
}
}
@@ -357,7 +352,7 @@ export class PluginManager extends TypertRemoteService {
const manifest = readProfileManifest('dsh', this.profile.dir)
const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8')) as InstallationManifest
const known = new Set([
...selectedBundles(manifest), ...Object.keys(manifest.dependencies ?? {}), ...Object.keys(installation.dependencies ?? {}),
...manifest.dsh?.profile?.bundles ?? [], ...Object.keys(manifest.dependencies ?? {}), ...Object.keys(installation.dependencies ?? {}),
])
const plan = registryPlan(options?.registry, await this.registries())
const registry = plan[0] as Registry
@@ -632,13 +627,11 @@ export class PluginManager extends TypertRemoteService {
}, { stage: 'remove', target: name }, 'remove')
}
/** The rows a bundle's patch inserts and the existing rows it changes; an unreadable patch or an invalid `rowSwitches` throws. */
private declaredRows(name: string, info: ProfileManifest): Pick<BundleInfo, 'rows' | 'overrides'> {
/** The rows a bundle's patch inserts, whether each gets a switch, and the existing rows it changes; an unreadable patch throws. */
private declaredRows(name: string, info: ProfileManifest): Pick<BundleInfo, 'rows' | 'rowSwitches' | 'overrides'> {
const bundle = info.dsh?.bundle
/* v8 ignore next -- bundleManifest answers only manifests that declare a patch */
if (bundle === undefined) return { rows: [], overrides: [] }
// listBundles reports an invalid `rowSwitches` on the bundle; listPlugins then leaves its rows switchable.
bundleRowSwitches(bundle)
if (bundle === undefined) return { rows: [], rowSwitches: true, overrides: [] }
const dir = resolveBundleDir('dsh', name, this.profile.installAnchor, this.profile.dir)
const patches: PatchOptions[] = bundlePatchPaths(dir, bundle).flatMap(file => loadOverlayPatches('dsh', file))
// One entry per row id: the Loader keeps a single entry for an id, whichever layer declared it last.
@@ -663,7 +656,7 @@ export class PluginManager extends TypertRemoteService {
const declared = new Set(rows.map(row => row.rowId))
const overrides = [...new Set(patches.flatMap(item =>
item.insert === undefined && typeof item.id === 'string' && !declared.has(item.id) ? [item.id] : []))]
return { rows, overrides }
return { rows, rowSwitches: bundle.rowSwitches !== false, overrides }
}
/** Run one pnpm command in the profile, streaming its output as install-log chunks. */
@@ -723,7 +716,7 @@ export class PluginManager extends TypertRemoteService {
private async selectBundle(name: string, enabled: boolean): Promise<void> {
const manifest = readProfileManifest('dsh', this.profile.dir)
const previous = selectedBundles(manifest)
const previous = manifest.dsh?.profile?.bundles ?? []
if (enabled || !previous.includes(name)) {
const metadata = bundleManifest(name, this.profile.dir, this.profile.installAnchor)
if (metadata === undefined) throw new ManagementFailure('not-bundle')
@@ -750,26 +743,6 @@ export class PluginManager extends TypertRemoteService {
return flatten(composeEntries([bundlePatchPaths(dir, info.dsh.bundle).flatMap(file => loadOverlayPatches('dsh', file))]))
}
/** Row ids the selected bundles insert under `dsh.bundle.rowSwitches: false`; an unreadable bundle contributes none. */
private groupedRows(): Set<string> {
const grouped = new Set<string>()
for (const name of selectedBundles(readProfileManifest('dsh', this.profile.dir))) {
let rows: EntryOptions[]
try { rows = this.rowsSwitchedAsWhole(name) } catch (_error) {
// listBundles reports an unreadable bundle or an invalid `rowSwitches`; its rows stay switchable here.
continue
}
for (const row of rows) grouped.add(row.id)
}
return grouped
}
/** The rows a bundle inserts when its manifest sets `dsh.bundle.rowSwitches: false`, and none otherwise. */
private rowsSwitchedAsWhole(name: string): EntryOptions[] {
const bundle = bundleManifest(name, this.profile.dir, this.profile.installAnchor)?.dsh?.bundle
return bundle === undefined || bundleRowSwitches(bundle) ? [] : this.bundleRows(name)
}
private protectsManager(name: string): boolean {
if (this.managementBundles.has(name)) return true
let rows: EntryOptions[]
+4 -5
View File
@@ -5,11 +5,8 @@ import type { PluginInventoryEntry } from '@deepseek-ai/dsh-host-plugin-inventor
export type { PluginEntryId } from '@deepseek-ai/dsh-host-plugin-inventory/types'
import type { PluginEntryId } from '@deepseek-ai/dsh-host-plugin-inventory/types'
/**
* Reasons a profile control cannot modify its target. `bundle-switch` marks a row inserted by a bundle whose
* manifest sets `dsh.bundle.rowSwitches: false`: only that bundle's own switch turns the row on and off.
*/
export type ReadOnlyReason = 'management-required' | 'unaddressable' | 'bundle-switch'
/** Reasons a profile control cannot modify its target. */
export type ReadOnlyReason = 'management-required' | 'unaddressable'
/** A package whose declared DSH peers reject the running DSH version, without an exemption for the exact pair. */
export interface IncompatiblePlugin {
@@ -68,6 +65,8 @@ export interface BundleInfo {
error?: ManagementError
/** The rows the bundle's patch inserts, in declaration order; empty when the patch cannot be read. */
rows: BundleRowInfo[]
/** Whether the plugin manager offers a switch for each inserted row: false when the bundle's `dsh.bundle.rowSwitches` is false. */
rowSwitches: boolean
/** Ids of rows the bundle's patch changes without declaring them: the built-in rows it configures or disables. */
overrides: string[]
}
@@ -292,21 +292,21 @@ it('lists bundle versions and current-profile plugin targets', async () => {
{
name: 'core', version: '1.0.0', enabled: true, installed: false, optional: false, removable: false, readOnlyReason: 'management-required',
meta: { title: 'core' },
rows: [{ rowId: 'manager', moduleName: 'cordis:manager', entryId: 'include:manager' }], overrides: [],
rows: [{ rowId: 'manager', moduleName: 'cordis:manager', entryId: 'include:manager' }], rowSwitches: true, overrides: [],
},
{
name: 'extra', version: '1.0.0', enabled: true, installed: true, optional: false, removable: true,
meta: { title: 'extra' },
rows: [{ rowId: 'managed', moduleName: pathToFileURL(join(dir, 'node_modules', 'extra', 'plugin.mjs')).href, entryId: 'include:managed' }], overrides: [],
rows: [{ rowId: 'managed', moduleName: pathToFileURL(join(dir, 'node_modules', 'extra', 'plugin.mjs')).href, entryId: 'include:managed' }], rowSwitches: true, overrides: [],
},
])
})
it('describes a bundle by its manifest and patch: one-liner, rows without a live entry, and the built-in rows it changes', async () => {
it('describes a bundle by its manifest and patch: one-liner, rows without a live entry, row switches, and the built-in rows it changes', async () => {
const { manager, dir, bundle } = await fixture()
bundle('described', [{ id: 'described-row', name: './plugin.mjs' }])
writeFileSync(join(dir, 'node_modules', 'described', 'package.json'), JSON.stringify({
name: 'described', version: '2.0.0', description: 'Describes itself.', dsh: { bundle: { patch: './cordis.patch.yml' } },
name: 'described', version: '2.0.0', description: 'Describes itself.', dsh: { bundle: { patch: './cordis.patch.yml', rowSwitches: false } },
}))
// An anonymous row is not addressable and is left out of the rows.
writeFileSync(join(dir, 'node_modules', 'described', 'cordis.patch.yml'), JSON.stringify([
@@ -319,7 +319,7 @@ it('describes a bundle by its manifest and patch: one-liner, rows without a live
expect((await manager.listBundles()).find(row => row.name === 'described')).toEqual({
name: 'described', version: '2.0.0', description: 'Describes itself.', enabled: false, installed: true, optional: false, removable: true,
meta: { title: 'described', description: 'Describes itself.' },
rows: [{ rowId: 'described-row', moduleName }], overrides: ['managed'],
rows: [{ rowId: 'described-row', moduleName }], rowSwitches: false, overrides: ['managed'],
})
await manager.setBundleEnabled('described', true)
expect((await manager.listBundles()).find(row => row.name === 'described')?.rows).toEqual([
@@ -330,33 +330,6 @@ it('describes a bundle by its manifest and patch: one-liner, rows without a live
expect((await manager.listBundles()).find(row => row.name === 'described')?.rows).toEqual([{ rowId: 'described-row', moduleName }])
})
it('keeps the rows of a bundle that switches them only as a whole read-only and reports an invalid declaration', async () => {
const { manager, dir, bundle } = await fixture()
bundle('grouped', [{ id: 'grouped-row', name: './plugin.mjs', config: { service: 'groupedProbe' } }])
const declare = (fields: object) => {
writeFileSync(join(dir, 'node_modules', 'grouped', 'package.json'), JSON.stringify({
name: 'grouped', version: '1.0.0', dsh: { bundle: { patch: './cordis.patch.yml', ...fields } },
}))
}
declare({ rowSwitches: false })
const manifest = readProfileManifest('test', dir)
manifest.dependencies = { ...manifest.dependencies, grouped: '1.0.0' }
writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest))
expect(await manager.setBundleEnabled('grouped', true)).toMatchObject({ application: 'applied' })
const row = (await manager.listPlugins()).find(item => item.entryId === 'include:grouped-row')
expect(row).toMatchObject({ enabled: true, readOnlyReason: 'bundle-switch' })
expect(row?.patchId).toBeUndefined()
expect(await manager.setPluginEnabled(row!.entryId, false)).toMatchObject({ application: 'failed', error: { code: 'bundle-switch' } })
// Another bundle keeps a switch per row.
expect((await manager.listPlugins()).find(item => item.entryId === 'include:managed')).toMatchObject({ patchId: 'managed' })
// A declaration that is not a boolean is reported on the bundle and leaves its rows switchable.
declare({ rowSwitches: 'false' })
expect((await manager.listBundles()).find(item => item.name === 'grouped')).toMatchObject({
error: { code: 'operation-error', diagnostic: 'dsh.bundle.rowSwitches must be a boolean' },
})
expect((await manager.listPlugins()).find(item => item.entryId === 'include:grouped-row')).toMatchObject({ patchId: 'grouped-row' })
})
it.each(['native', 'runtime'] as const)('reads a disabled bundle and its independent exported plugins without running them with %s resolution', async (mode) => {
const { ctx, manager, dir, bundle, profile } = await fixture('startup')
bundle('localized', [{ id: 'first', name: 'local-child/first' }, { id: 'second', name: 'local-child/second' }])
@@ -438,7 +411,7 @@ it.each([
expect((await manager.listBundles()).find(row => row.name === 'unnamed')).toEqual({
name: 'unnamed', version: '1.0.0', enabled: false, installed: true, optional: false, removable: true,
rows: [], overrides: [], ...expected,
rows: [], rowSwitches: true, overrides: [], ...expected,
})
})
@@ -1072,7 +1045,7 @@ it('offers the launcher\'s optional bundles switched off and never removable', a
name: offered, version: '3.0.0', description: 'Package one-liner.',
meta: { title: offered, description: 'Package one-liner.' },
enabled: false, installed: false, optional: true, removable: false,
rows: [{ rowId: 'offered-row', moduleName: pathToFileURL(join(supplied, 'plugin.mjs')).href }], overrides: [],
rows: [{ rowId: 'offered-row', moduleName: pathToFileURL(join(supplied, 'plugin.mjs')).href }], rowSwitches: true, overrides: [],
})
expect(await manager.setBundleEnabled(offered, true)).toMatchObject({ application: 'applied' })
expect((await manager.listBundles()).find(row => row.name === offered)).toMatchObject({ enabled: true, optional: true, removable: false })
@@ -24,8 +24,8 @@
en: abc90531efa889d5
zh: 0562570f85e69c35
/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/switching-one-row-of-a-bundle:
en: 4cc5b672f686c191
zh: 26e57ed286d39740
en: eafc0f85bac6a1b0
zh: 6b51fc0cb83e5a4a
/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/configuration-pages:
en: c32019330fd9586d
zh: dd22110abd10159c
+1 -1
View File
@@ -47,7 +47,7 @@ A bundle's page shows its full package name under the title, the spec that insta
### Switching one row of a bundle
A row's switch on the bundle's page calls `pluginManager.setPluginEnabled`, which writes the row's `disabled` override into the profile's `cordis.patch.yml`. The tree recomposes at once on a profile with HMR, so the row's host half unmounts or mounts while the rest of the bundle keeps running, and the page follows the client module graph without reloading. Rows use the shared status marker for their Host fiber phase: pending and disabled are idle, loading and unloading are ongoing, active is done, and failed is error. The switch appears only on a bundle that is on; a row the Host marks `bundle-switch`, because its bundle's manifest sets `dsh.bundle.rowSwitches: false`, shows its state and no switch. A row without a live entry, or one the Host will not address through the profile patch, is locked with the Host's reason. A list longer than ten rows gets a filter over localized titles, descriptions, row ids, and module names.
A row's switch on the bundle's page calls `pluginManager.setPluginEnabled`, which writes the row's `disabled` override into the profile's `cordis.patch.yml`. The tree recomposes at once on a profile with HMR, so the row's host half unmounts or mounts while the rest of the bundle keeps running, and the page follows the client module graph without reloading. Rows use the shared status marker for their Host fiber phase: pending and disabled are idle, loading and unloading are ongoing, active is done, and failed is error. The switch appears only on a bundle that is on; a bundle whose manifest sets `dsh.bundle.rowSwitches: false` lists its rows with their state and no switches. A row without a live entry, or one the Host will not address through the profile patch, is locked with the Host's reason. A list longer than ten rows gets a filter over localized titles, descriptions, row ids, and module names.
### Configuration pages
@@ -47,7 +47,7 @@ Host 将网络失败或超时归因于 GitHub 地址,且提供 npmmirror 时
### 切换组合包里的一行
组合包页面上行的开关调用 `pluginManager.setPluginEnabled`,往 profile 的 `cordis.patch.yml` 写入该行的 `disabled` 覆盖。启用了 HMR 的 profile 的树随即重组,该行的宿主半区卸下或挂上,组合包其余部分照常运行,页面无需重载即跟随客户端模块图。行使用共享状态标记表示 Host fiber 阶段:pending 与 disabled 为 idle,loading 与 unloading 为 ongoing,active 为 done,failed 为 error。开关只出现在已打开的组合包上;Host 标为 `bundle-switch` 的行(其组合包清单设置了 `dsh.bundle.rowSwitches: false`)只显示状态,不带开关。没有存活条目的行,以及 Host 不通过 profile patch 寻址的行,带着 Host 的原因锁定。超过十行的列表带一个按本地化标题、描述、行 id 和模块名筛选的输入框。
组合包页面上行的开关调用 `pluginManager.setPluginEnabled`,往 profile 的 `cordis.patch.yml` 写入该行的 `disabled` 覆盖。启用了 HMR 的 profile 的树随即重组,该行的宿主半区卸下或挂上,组合包其余部分照常运行,页面无需重载即跟随客户端模块图。行使用共享状态标记表示 Host fiber 阶段:pending 与 disabled 为 idle,loading 与 unloading 为 ongoing,active 为 done,failed 为 error。开关只出现在已打开的组合包上;清单设置 `dsh.bundle.rowSwitches: false` 的组合包只列出各行及其状态,不带开关。没有存活条目的行,以及 Host 不通过 profile patch 寻址的行,带着 Host 的原因锁定。超过十行的列表带一个按本地化标题、描述、行 id 和模块名筛选的输入框。
### 配置页
@@ -188,9 +188,9 @@ function MetadataError({ error, t }: { readonly error: string | undefined; reado
/**
* A pack's rows as a list in the order the pack declares them: a state dot,
* the row id, one line saying its state, a configure control for a row that
* registered a page, and, when the pack is on, a switch; a row that switches
* only with its pack has none. A pack like base carries close to a hundred
* rows, so a long list gets a filter.
* registered a page, and, when the pack is on and offers row switches, a
* switch. A pack like base carries close to a hundred rows, so a long list
* gets a filter.
*/
function RowsSection({ rows, t, resolveText, toggle, configure }: {
readonly rows: readonly PackageRow[]
@@ -254,7 +254,7 @@ function RowsSection({ rows, t, resolveText, toggle, configure }: {
<StateDot state={rowDotState(row)} />
{rowStateText(row, t)}
</span>
{toggle === undefined || row.readOnlyReason === 'bundle-switch'
{toggle === undefined
? null
: <RowSwitch
row={row} title={title} t={t} busy={toggle.busy(row)}
@@ -585,7 +585,7 @@ function PackageDetail({
rows={pkg.rows}
t={t}
resolveText={resolveText}
toggle={pkg.enabled ? { busy: row => busy || rowBusy(row), onSetEnabled: onSetRowEnabled } : undefined}
toggle={pkg.enabled && pkg.rowSwitches ? { busy: row => busy || rowBusy(row), onSetEnabled: onSetRowEnabled } : undefined}
configure={configure}
/>
{renderSlot('plugins.detail.section', { subject })}
@@ -177,7 +177,6 @@ export const zh = {
failedRowDisable: '组件停用失败:{reason}',
reasonManagementRequired: '插件管理所需,不能停用或卸载',
reasonUnaddressable: '当前 profile 的 patch 无法唯一定位这一项',
reasonBundleSwitch: '这一项随插件包一起开关,不能单独开关',
reasonUnknownPlugin: '找不到该插件',
reasonInvalidSpec: '请输入有效的包名或地址',
reasonAmbiguousInstall: '无法从依赖变更中确定安装了哪一个包',
@@ -371,7 +370,6 @@ export const en = {
failedRowDisable: 'Could not disable the component: {reason}',
reasonManagementRequired: 'Plugin management needs it; it cannot be switched off or uninstalled.',
reasonUnaddressable: 'The profile patch cannot address this one uniquely.',
reasonBundleSwitch: 'This switches on and off with its plugin pack and cannot be switched alone.',
reasonUnknownPlugin: 'No such plugin.',
reasonInvalidSpec: 'Enter a valid package name or address.',
reasonAmbiguousInstall: 'Which package was installed cannot be told from the dependency change.',
@@ -92,6 +92,8 @@ export interface PackageView {
/** Why the Host cannot read the bundle, when it cannot. */
readonly error?: ManagementError
readonly rows: readonly PackageRow[]
/** Whether the page offers a switch for each row while the bundle is on; false when the bundle declares its rows work only together. */
readonly rowSwitches: boolean
}
/** The typed spec as the Host read it, on the installing, installed, and failed screens. */
@@ -412,6 +414,7 @@ export function packageView(bundle: BundleInfo, plugins: readonly PluginInfo[]):
optional: bundle.optional,
enabled: bundle.enabled,
rows,
rowSwitches: bundle.rowSwitches,
...bundle.version === undefined ? {} : { version: bundle.version },
...bundle.description === undefined ? {} : { description: bundle.description },
...bundle.meta === undefined ? {} : { meta: bundle.meta },
@@ -48,7 +48,6 @@ function registryHost(registry: string): string {
const CODE_KEYS = {
'management-required': 'reasonManagementRequired',
'unaddressable': 'reasonUnaddressable',
'bundle-switch': 'reasonBundleSwitch',
'unknown-plugin': 'reasonUnknownPlugin',
'invalid-spec': 'reasonInvalidSpec',
'ambiguous-install': 'reasonAmbiguousInstall',
@@ -35,6 +35,7 @@ function pkg(overrides: Partial<PackageView> = {}): PackageView {
optional: false,
enabled: true,
rows: [],
rowSwitches: true,
...overrides,
}
}
@@ -505,19 +506,17 @@ describe('PluginManagerPage', () => {
expect(screen.getByRole('form', { name: 'theme settings' })).toBeTruthy()
})
it('lists a row that switches only with its bundle with its state and no switch', () => {
const clock = row({
rowId: 'clock', entryId: 'include:clock' as PluginEntryId, moduleName: '@acme/dsh-clock', readOnlyReason: 'bundle-switch',
meta: { title: { en: 'Time awareness' } },
})
renderTab({ packages: [pkg({ rows: [row(), clock] })] })
it('lists the rows of a bundle that declares its rows work only together with their state and no switches', () => {
const clock = row({ rowId: 'clock', entryId: 'include:clock' as PluginEntryId, moduleName: '@acme/dsh-clock', meta: { title: { en: 'Time awareness' } } })
renderTab({ packages: [pkg({ rows: [row(), clock], rowSwitches: false })] })
fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') }))
const listed = document.querySelector('[data-plugin-row="include:clock"]') as HTMLElement
expect(within(listed).getByText('Time awareness')).toBeTruthy()
expect(within(listed).getByText(en.rowPhaseActive)).toBeTruthy()
expect(within(listed).queryByRole('switch')).toBeNull()
// A row the bundle lets the person switch keeps its switch.
expect(screen.getByRole('switch', { name: en.partToggle.replace('{name}', 'dsh-better-sidebar') })).toBeTruthy()
expect(document.querySelectorAll('[data-plugin-row]')).toHaveLength(2)
// The bundle's own switch stays; its rows have none.
expect(screen.getByRole('switch', { name: en.enableToggle.replace('{name}', 'dsh-better-sidebar') })).toBeTruthy()
expect(within(document.querySelector('[data-plugin-rows]') as HTMLElement).queryByRole('switch')).toBeNull()
})
it('shows a row id only once when it is the localized title in lists and configuration pages', () => {
@@ -733,7 +732,7 @@ describe('PluginManagerPage', () => {
})
it('leaves the version out of a bundle the Host reports none for', () => {
const unversioned: PackageView = { name: 'dsh-better-sidebar', installed: true, optional: false, enabled: true, rows: [] }
const unversioned: PackageView = { name: 'dsh-better-sidebar', installed: true, optional: false, enabled: true, rows: [], rowSwitches: true }
renderTab({ packages: [unversioned] }, {}, bodies)
fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') }))
expect(subjects.at(-1)).toEqual({ kind: 'bundle', pkg: { name: 'dsh-better-sidebar', installed: true, enabled: true, rows: [] } })
@@ -22,6 +22,7 @@ const BUNDLE: BundleInfo = {
optional: false,
removable: true,
rows: [{ rowId: 'sidebar', moduleName: 'dsh-better-sidebar', entryId: ROW_ENTRY }, { rowId: 'theme', moduleName: 'dsh-better-sidebar/theme' }],
rowSwitches: true,
overrides: [],
}
@@ -112,7 +113,7 @@ describe('packageView', () => {
it('joins a bundle with the entries its rows run as', () => {
expect(packageView(BUNDLE, PLUGINS)).toEqual({
name: 'dsh-better-sidebar', version: '0.16.0', description: 'A sidebar.',
installed: true, optional: false, enabled: false,
installed: true, optional: false, enabled: false, rowSwitches: true,
rows: [
{ rowId: 'sidebar', moduleName: 'dsh-better-sidebar', entryId: ROW_ENTRY, enabled: true, phase: 'active' },
{ rowId: 'theme', moduleName: 'dsh-better-sidebar/theme', enabled: false, phase: null },
@@ -123,11 +124,12 @@ describe('packageView', () => {
name: '@deepseek-ai/dsh-base', enabled: true, installed: false, optional: false, removable: false, readOnlyReason: 'management-required',
error: { code: 'operation-error', diagnostic: 'broken' },
rows: [{ rowId: 'core', moduleName: '@deepseek-ai/dsh-base', entryId: 'include:core' as PluginEntryId }, { rowId: 'gone', moduleName: 'x', entryId: 'include:gone' as PluginEntryId }],
rowSwitches: false,
overrides: [],
}
expect(packageView(protectedBundle, PLUGINS)).toEqual({
name: '@deepseek-ai/dsh-base', installed: false, optional: false, enabled: true, readOnlyReason: 'management-required',
error: { code: 'operation-error', diagnostic: 'broken' },
error: { code: 'operation-error', diagnostic: 'broken' }, rowSwitches: false,
rows: [
{ rowId: 'core', moduleName: '@deepseek-ai/dsh-base', entryId: 'include:core', enabled: true, phase: 'active', readOnlyReason: 'management-required' },
{ rowId: 'gone', moduleName: 'x', entryId: 'include:gone', enabled: false, phase: null },
@@ -138,7 +140,7 @@ describe('packageView', () => {
describe('sortPackages', () => {
it('orders packages by the short name a person reads, not by the Host order or enablement', async () => {
const plain = { enabled: true, installed: true, optional: false, removable: true, rows: [], overrides: [] }
const plain = { enabled: true, installed: true, optional: false, removable: true, rows: [], rowSwitches: true, overrides: [] }
const zeta: BundleInfo = { ...plain, name: 'dsh-zeta' }
const alpha: BundleInfo = { ...plain, name: '@acme/dsh-alpha', enabled: false }
const views = [zeta, BUNDLE, alpha].map(bundle => packageView(bundle, PLUGINS))
@@ -4631,7 +4631,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'BundleInfo',
declaration: 'export interface BundleInfo {\n name: string;\n version?: string;\n meta?: PluginLocalizedMeta;\n description?: string;\n enabled: boolean;\n installed: boolean;\n optional: boolean;\n removable: boolean;\n readOnlyReason?: ReadOnlyReason;\n error?: ManagementError;\n rows: BundleRowInfo[];\n overrides: string[];\n}',
declaration: 'export interface BundleInfo {\n name: string;\n version?: string;\n meta?: PluginLocalizedMeta;\n description?: string;\n enabled: boolean;\n installed: boolean;\n optional: boolean;\n removable: boolean;\n readOnlyReason?: ReadOnlyReason;\n error?: ManagementError;\n rows: BundleRowInfo[];\n rowSwitches: boolean;\n overrides: string[];\n}',
},
{
name: 'BundleRowInfo',
@@ -6047,7 +6047,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'ReadOnlyReason',
declaration: 'export type ReadOnlyReason = \'management-required\' | \'unaddressable\' | \'bundle-switch\';',
declaration: 'export type ReadOnlyReason = \'management-required\' | \'unaddressable\';',
},
{
name: 'ReadResultView',
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 1db86285abf6c499
/deepseek-ai-dsh-package-manifest/use-this-package:
en: de53644a1f09b490
zh: a6531649809e2d3a
en: d0ac340eb31e948b
zh: 64c740b5697de6ec
/deepseek-ai-dsh-package-manifest/understand-the-implementation:
en: 64e1e57d1c3761e0
zh: 9fc039770ce43c63
+1 -1
View File
@@ -43,7 +43,7 @@ const manifest: DshPackageManifest = {
}
```
`DshPackageManifest` describes the package.json fields used by DSH, with required `name` and `version`; it is not an exhaustive npm schema. Local profile readers use `Partial<DshPackageManifest>` because profiles need no published version. `DshManifest` describes only public author fields under `dsh`. `DshBundleManifest.patch` is one patch file path or an ordered list of them, each relative to the package root; the launcher applies a list in order as one bundle layer. `DshBundleManifest.rowSwitches: false` declares that the bundle's rows work only together, so the plugin manager refuses to switch one of them alone; absent means true, and a non-boolean value is rejected. TypeScript checks the example and erases `import type`; these interfaces do not parse JSON or write a file.
`DshPackageManifest` describes the package.json fields used by DSH, with required `name` and `version`; it is not an exhaustive npm schema. Local profile readers use `Partial<DshPackageManifest>` because profiles need no published version. `DshManifest` describes only public author fields under `dsh`. `DshBundleManifest.patch` is one patch file path or an ordered list of them, each relative to the package root; the launcher applies a list in order as one bundle layer. `DshBundleManifest.rowSwitches: false` declares that the bundle's rows work only together, so the plugin manager offers no switch per row; absent means true. TypeScript checks the example and erases `import type`; these interfaces do not parse JSON or write a file.
The following metadata fields are optional. Omitting them leaves the format version or compatible host versions undeclared; readers do not infer defaults.
+1 -1
View File
@@ -43,7 +43,7 @@ const manifest: DshPackageManifest = {
}
```
`DshPackageManifest` 描述 DSH 使用的 package.json 字段,其中 `name` 和 `version` 必填;它不是完整的 npm schema(模式)。本地 profile 读取方使用 `Partial<DshPackageManifest>`,因为 profile 无需发布版本。`DshManifest` 仅描述 `dsh` 下的公共作者字段。`DshBundleManifest.patch` 是一个 patch 文件路径,或一个有序的路径列表,均相对于包根目录;launcher 按列表顺序把它们作为同一个组合包层应用。`DshBundleManifest.rowSwitches: false` 声明该组合包的各行只能一起工作,插件管理器因此拒绝单独开关其中任何一行;缺省视为 true,非布尔值会被拒绝。TypeScript 检查示例并删除 `import type`;这些接口不解析 JSON,也不写入文件。
`DshPackageManifest` 描述 DSH 使用的 package.json 字段,其中 `name` 和 `version` 必填;它不是完整的 npm schema(模式)。本地 profile 读取方使用 `Partial<DshPackageManifest>`,因为 profile 无需发布版本。`DshManifest` 仅描述 `dsh` 下的公共作者字段。`DshBundleManifest.patch` 是一个 patch 文件路径,或一个有序的路径列表,均相对于包根目录;launcher 按列表顺序把它们作为同一个组合包层应用。`DshBundleManifest.rowSwitches: false` 声明该组合包的各行只能一起工作,插件管理器因此不为单行提供开关;缺省视为 true。TypeScript 检查示例并删除 `import type`;这些接口不解析 JSON,也不写入文件。
以下元数据字段均可选。省略时,格式版本或兼容的宿主版本保持未声明状态;读取方不推断默认值。
+2 -2
View File
@@ -70,8 +70,8 @@ export interface DshBundleManifest {
/** One patch file path, or an ordered list applied in sequence, each relative to the declaring package root. */
patch: string | string[]
/**
* Whether the plugin manager may switch each row the bundle inserts on its own; false for a bundle whose rows work
* only together, which then switch only with the bundle. Absent means true.
* Whether the plugin manager offers a switch for each row the bundle inserts; false for a bundle whose rows work
* only together, which the bundle's own switch turns on and off. Absent means true.
*/
rowSwitches?: boolean
}