mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
Merge pull request #5428 from deepseek-harness/turtle/plugin-source-section
feat(plugins): 在组合包详情页显示来源信息
This commit is contained in:
@@ -14,6 +14,12 @@
|
||||
- code: fixture-live-client
|
||||
- text: 运行中
|
||||
- switch "启用组件 @fixture/live-client" [checked]
|
||||
- heading "来源信息" [level=4]
|
||||
- term: 代码来源
|
||||
- definition:
|
||||
- code: file:{{fixtures}}/fixture-live-client
|
||||
- term: 当前版本
|
||||
- definition: 0.0.1
|
||||
- region "夹具区块":
|
||||
- heading "夹具区块" [level=4]
|
||||
- paragraph: 来自夹具的区块内容
|
||||
|
||||
@@ -22,3 +22,9 @@
|
||||
- text: "@fixture/bundle/review"
|
||||
- code: fixture-review
|
||||
- text: "Off"
|
||||
- heading "Source" [level=4]
|
||||
- term: Code source
|
||||
- definition:
|
||||
- code: file:{{fixtures}}/fixture-bundle
|
||||
- term: Current version
|
||||
- definition: 0.0.1
|
||||
|
||||
@@ -23,3 +23,9 @@
|
||||
- code: fixture-review
|
||||
- code: "@fixture/bundle/review"
|
||||
- text: 已关闭
|
||||
- heading "来源信息" [level=4]
|
||||
- term: 代码来源
|
||||
- definition:
|
||||
- code: file:{{fixtures}}/fixture-bundle
|
||||
- term: 当前版本
|
||||
- definition: 0.0.1
|
||||
|
||||
@@ -317,7 +317,9 @@ describe('web e2e: plugin configuration pages', () => {
|
||||
expect(await bundlePage.getByRole('button', { name: '夹具操作' }).count()).toBe(1)
|
||||
expect(await bundlePage.locator('[data-live-badge="bundle"]').textContent()).toBe('夹具标签')
|
||||
expect(await bundlePage.getByRole('region', { name: '夹具区块' }).getByText('来自夹具的区块内容').count()).toBe(1)
|
||||
const bundleSnapshot = await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd)
|
||||
const bundleSnapshot = await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd, {
|
||||
replacements: [[FIXTURE_PLUGINS, '{{fixtures}}']],
|
||||
})
|
||||
await compareOrRefreshGolden(BUNDLE_EXPECTED, bundleSnapshot, MODE)
|
||||
await configure.click()
|
||||
|
||||
|
||||
@@ -547,14 +547,18 @@ describe('web e2e: plugin manager', () => {
|
||||
expect(await review.getByText('审查工作区中的改动。', { exact: true }).count()).toBe(1)
|
||||
await panel.getByText('Registry description for the fixture bundle.', { exact: true }).first().waitFor()
|
||||
expect([...scaffold.ctx.loader.entries()].some(entry => entry.options.name.startsWith('@fixture/bundle'))).toBe(false)
|
||||
await compareOrRefreshGolden(EXPORTS_EXPECTED, await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd), MODE)
|
||||
await compareOrRefreshGolden(EXPORTS_EXPECTED, await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd, {
|
||||
replacements: [[FIXTURE_PLUGINS, '{{fixtures}}']],
|
||||
}), MODE)
|
||||
try {
|
||||
await setLanguage('en')
|
||||
await search.getByText('File Search', { exact: true }).waitFor()
|
||||
expect(await review.getByText('@fixture/bundle/review', { exact: true }).count()).toBeGreaterThan(0)
|
||||
expect(await search.getByText('Search package introduction.', { exact: true }).count()).toBe(1)
|
||||
expect(await review.getByText('审查工作区中的改动。', { exact: true }).count()).toBe(0)
|
||||
await compareOrRefreshGolden(EXPORTS_EN_EXPECTED, await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd), MODE)
|
||||
await compareOrRefreshGolden(EXPORTS_EN_EXPECTED, await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd, {
|
||||
replacements: [[FIXTURE_PLUGINS, '{{fixtures}}']],
|
||||
}), MODE)
|
||||
expect(await panel.locator('[data-plugin-name]').textContent()).toBe('@fixture/bundle')
|
||||
} finally {
|
||||
await setLanguage('zh')
|
||||
|
||||
@@ -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:243`](../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:249`](../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:256`](../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:248`](../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:254`](../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:261`](../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:44`](../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) |
|
||||
|
||||
@@ -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:243`](../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:249`](../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:256`](../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:248`](../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:254`](../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:261`](../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:44`](../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) |
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
en: 962b4398c9052959
|
||||
zh: 14471c340a9f6e60
|
||||
/profile-management/management-records:
|
||||
en: c49daf51d1468b37
|
||||
zh: f4964a074ca9002f
|
||||
en: 6929d1001684c024
|
||||
zh: 06d3d3f6eea848ec
|
||||
|
||||
@@ -10,7 +10,7 @@ The [boot package group](../../packages/boot/README.md) owns launcher-provided p
|
||||
|
||||
`PluginInfo` carries module identity, effective enablement, fiber phase and optional display `meta`, plus a unique `patchId` or a `readOnlyReason`.
|
||||
|
||||
`BundleInfo` carries the package name, optional installed version, selected enablement, removal availability and optional resolution error. Its optional `meta` and each `BundleRowInfo.meta` contain display text or a metadata diagnostic; Clients select a language at render time.
|
||||
`BundleInfo` carries the package name, optional installed version, selected enablement, removal availability, optional resolution error, and, for a bundle the profile's own dependency supplies and the installation does not, `source`: that dependency as a spec `pnpm add` accepts. Its optional `meta` and each `BundleRowInfo.meta` contain display text or a metadata diagnostic; Clients select a language at render time.
|
||||
|
||||
`InstallBundleOptions.enabled` defaults to true. False installs without selecting the bundle layer. `approvedBuilds` grants persistent script permission to the supplied pending package names before installation. `registry` names the registry asked first; absent, the configured one.
|
||||
|
||||
@@ -111,8 +111,8 @@ Manage profile files and apply their declared reload lifecycle.
|
||||
|
||||
/** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
|
||||
* A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
|
||||
* @returns Package versions, manifest descriptions, rows, optional display metadata, activation selections,
|
||||
* whether the installation offers the bundle, and removal availability.
|
||||
* @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
|
||||
* display metadata, activation selections, whether the installation offers the bundle, and removal availability.
|
||||
*/
|
||||
@Remote listBundles(): Promise<BundleInfo[]>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
`PluginInfo` 包含模块标识、实际启停状态、fiber 阶段和可选的展示 `meta`,以及唯一的 `patchId` 或 `readOnlyReason`。
|
||||
|
||||
`BundleInfo` 包含包名、可选的安装版本、组合层选择状态、删除可用性及可选的解析错误。它的可选 `meta` 与各行的 `BundleRowInfo.meta` 包含展示文本或元信息诊断;Client 在渲染时选择语言。
|
||||
`BundleInfo` 包含包名、可选的安装版本、组合层选择状态、删除可用性、可选的解析错误;由 profile 自身依赖提供、且安装不提供的组合包还带有 `source`,即该依赖在 `pnpm add` 中可用的 spec。它的可选 `meta` 与各行的 `BundleRowInfo.meta` 包含展示文本或元信息诊断;Client 在渲染时选择语言。
|
||||
|
||||
`InstallBundleOptions.enabled` 默认为 true,false 表示安装但不选择组合包层。`approvedBuilds` 在安装前向指定的待审批包名授予持久脚本权限。`registry` 指定首先询问的注册表;缺省为配置的那个。
|
||||
|
||||
@@ -111,8 +111,8 @@ Manage profile files and apply their declared reload lifecycle.
|
||||
|
||||
/** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
|
||||
* A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
|
||||
* @returns Package versions, manifest descriptions, rows, optional display metadata, activation selections,
|
||||
* whether the installation offers the bundle, and removal availability.
|
||||
* @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
|
||||
* display metadata, activation selections, whether the installation offers the bundle, and removal availability.
|
||||
*/
|
||||
@Remote listBundles(): Promise<BundleInfo[]>
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 87679c51aa3693f5
|
||||
zh: 11684a48a5806afb
|
||||
/deepseek-ai-dsh-plugin-manager/use-this-package:
|
||||
en: b8d7061ee289b2a0
|
||||
zh: 49aba7df4761fb23
|
||||
en: d0f7d42dffe6d3c7
|
||||
zh: b4bf81958cda876d
|
||||
/deepseek-ai-dsh-plugin-manager/use-this-package/version-compatibility-and-exemptions:
|
||||
en: 920c0acb3b9feaf3
|
||||
zh: ba6e832a256880a7
|
||||
|
||||
@@ -41,7 +41,7 @@ A plugin toggle updates only `disabled` in the last matching override in the pro
|
||||
|
||||
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. `source` gives a profile dependency as a spec `pnpm add` accepts: a git address or URL as recorded without the user information of an http(s) URL (a query string stays as recorded), a `file:` or `link:` path made absolute against the profile or the home directory for `~`, and a registry range, tag, or alias after the package name, as is any spec of a package installed under another name; a bundle the installation supplies has none, even when the profile also depends on it, because the installation's copy is the one that loads. The `plugin_manager` tool omits UI display metadata from list results.
|
||||
|
||||
`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.
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ kind: "package-reference"
|
||||
|
||||
已选择但无法加载的组合包仍会出现在 `listBundles` 中,并携带 `error`;`enabled` 表示保存的选择,不代表加载成功。插件页面显示错误并允许取消选择。损坏的组合包无法启用。管理组合包的文件变得不可读后仍受保护。
|
||||
|
||||
`listBundles` 为各组合包及其声明的插件行提供可选的展示 `meta`,包括已禁用的组合包。Client 从这些值中选择语言。单独的 `description` 字段是该组合包原始的 `package.json.description`;元信息诊断不会阻止管理操作。`plugin_manager` 工具的列表结果不包含 UI 展示元信息。
|
||||
`listBundles` 为各组合包及其声明的插件行提供可选的展示 `meta`,包括已禁用的组合包。Client 从这些值中选择语言。单独的 `description` 字段是该组合包原始的 `package.json.description`;元信息诊断不会阻止管理操作。`source` 以 `pnpm add` 接受的 spec 给出 profile 依赖:git 地址或 URL 按记录给出,但去掉 http(s) URL 的 user information(查询字符串保持原样);`file:` 或 `link:` 路径按 profile 目录解析为绝对路径,`~` 展开为主目录;注册表版本范围、标签或别名接在包名的 `@` 之后,以别名安装的包的 spec 也是如此。安装提供的组合包没有该字段,即使 profile 也依赖它,因为实际加载的是安装中的副本。`plugin_manager` 工具的列表结果不包含 UI 展示元信息。
|
||||
|
||||
`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` 会结束查询。
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import type {} from '@deepseek-ai/dsh-hmr'
|
||||
import type { ProfileContext, ProfileManifest } from '@deepseek-ai/dsh-app-boot'
|
||||
import { bundleManifest, readProfileRegistry, registryArguments, runProfilePnpm, saveManifest, viewProfilePackage } from './operations.ts'
|
||||
import { classifyInstallFailure } from './install-failure.ts'
|
||||
import { InvalidInstallSpecError, parseInstallSpec, type ParsedInstallSpec } from './install-spec.ts'
|
||||
import { dependencySpec, InvalidInstallSpecError, parseInstallSpec, type ParsedInstallSpec } from './install-spec.ts'
|
||||
import { attributeFailure, normalizeRegistry, NPMMIRROR_REGISTRY, registryPlan } from './registry.ts'
|
||||
import { writePluginEnabled } from './patch.ts'
|
||||
import { incompatiblePlugin, ManagementFailure } from './failure.ts'
|
||||
@@ -273,15 +273,16 @@ export class PluginManager extends TypertRemoteService {
|
||||
|
||||
/** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
|
||||
* A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
|
||||
* @returns Package versions, manifest descriptions, rows, optional display metadata, activation selections,
|
||||
* whether the installation offers the bundle, and removal availability.
|
||||
* @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
|
||||
* display metadata, activation selections, whether the installation offers the bundle, and removal availability.
|
||||
*/
|
||||
@Remote
|
||||
listBundles(): Promise<BundleInfo[]> {
|
||||
const manifest = readProfileManifest('dsh', this.profile.dir)
|
||||
const exemptions = readProfileVersionExemptions(this.profile.dir)
|
||||
const selected = manifest.dsh?.profile?.bundles ?? []
|
||||
const dependencies = Object.keys(manifest.dependencies ?? {})
|
||||
const recorded = manifest.dependencies ?? {}
|
||||
const dependencies = Object.keys(recorded)
|
||||
const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8')) as InstallationManifest
|
||||
const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {})])]
|
||||
const bundles: BundleInfo[] = []
|
||||
@@ -289,12 +290,17 @@ export class PluginManager extends TypertRemoteService {
|
||||
const installed = dependencies.includes(name)
|
||||
const optional = OPTIONAL_BUNDLES.includes(name)
|
||||
const removable = installed && !Object.hasOwn(installation.dependencies ?? {}, name)
|
||||
// Bundle resolution reads the installation first, so a profile dependency the installation manifest also names,
|
||||
// like one it forbids removing, is not the loaded copy.
|
||||
const sourceOf = (packageName?: string): { source?: string } =>
|
||||
removable ? { source: dependencySpec(name, recorded[name] as string, this.profile.dir, packageName) } : {}
|
||||
const enabled = selected.includes(name)
|
||||
const readOnlyReason = this.protectsManager(name) ? 'management-required' as const : undefined
|
||||
try {
|
||||
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,
|
||||
if (enabled) bundles.push({ name, ...sourceOf(), enabled, installed, optional,
|
||||
removable: removable && readOnlyReason === undefined,
|
||||
...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: { code: 'not-bundle' }, rows: [], overrides: [] })
|
||||
continue
|
||||
}
|
||||
@@ -305,12 +311,12 @@ export class PluginManager extends TypertRemoteService {
|
||||
bundles.push({ name, ...(info.version === undefined ? {} : { version: info.version }),
|
||||
...(info.description === undefined || info.description === '' ? {} : { description: info.description }),
|
||||
...meta === undefined ? {} : { meta },
|
||||
enabled, installed, optional, removable: removable && readOnlyReason === undefined,
|
||||
...sourceOf(info.name), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
|
||||
...(readOnlyReason === undefined ? {} : { readOnlyReason }),
|
||||
...this.declaredRows(name, info) })
|
||||
} catch (error) {
|
||||
if (enabled || installed) {
|
||||
bundles.push({ name, enabled, installed, optional, removable: removable && readOnlyReason === undefined,
|
||||
bundles.push({ name, ...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
|
||||
...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: managementError(error), rows: [], overrides: [] })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* @module @deepseek-ai/dsh-plugin-manager/install-spec
|
||||
*/
|
||||
|
||||
import { isAbsolute } from 'node:path'
|
||||
import { homedir } from 'node:os'
|
||||
import { isAbsolute, resolve } from 'node:path'
|
||||
|
||||
/**
|
||||
* One spec read into its form and the parts an inspection needs. A git spec
|
||||
@@ -89,3 +90,34 @@ export function parseInstallSpec(raw: string): ParsedInstallSpec {
|
||||
if (range === '') throw invalid(spec, 'a version after @ must not be empty')
|
||||
return range === undefined ? { kind: 'registry', spec, name } : { kind: 'registry', spec, name, range }
|
||||
}
|
||||
|
||||
/** A recorded dependency value pnpm resolves outside a registry: a protocol other than a registry alias, or an scp-like git address. */
|
||||
const NON_REGISTRY_VALUE = /^(?!(?:npm|jsr|workspace|catalog):)[a-z][a-z0-9+.-]*:|^[^@/:\s]+@[^:/\s]+:/i
|
||||
/** The user information of an http(s) URL, up to the last `@` before the path, which holds a password or access token when present. */
|
||||
const HTTP_USER_INFO = /^((?:git\+)?https?:\/\/)[^/]*@/i
|
||||
|
||||
/**
|
||||
* The spec `pnpm add` accepts for a dependency the profile manifest records. A git address, URL, or other
|
||||
* non-registry protocol is the recorded value without the user information of an http(s) URL, whose query string
|
||||
* is kept; a `file:` or `link:` path, which pnpm records relative to the profile or with `~` for the home
|
||||
* directory, becomes absolute. A registry range, tag, or alias follows the dependency name after `@`, as does a
|
||||
* non-registry value installed under a name other than the package's own.
|
||||
* @param name - the dependency name.
|
||||
* @param recorded - the value the profile manifest records for it.
|
||||
* @param profileDir - the profile directory a relative local path is resolved against.
|
||||
* @param packageName - the name the installed package's manifest declares, when it could be read.
|
||||
* @returns the installable spec.
|
||||
*/
|
||||
export function dependencySpec(name: string, recorded: string, profileDir: string, packageName = name): string {
|
||||
const local = /^(file|link):(.*)$/s.exec(recorded)
|
||||
let spec: string
|
||||
if (local !== null) {
|
||||
const path = (local[2] as string).replace(/^~(?=$|[\\/])/, () => homedir())
|
||||
spec = `${local[1]}:${resolve(profileDir, path)}`
|
||||
} else if (NON_REGISTRY_VALUE.test(recorded)) {
|
||||
spec = recorded.replace(HTTP_USER_INFO, '$1')
|
||||
} else {
|
||||
return `${name}@${recorded}`
|
||||
}
|
||||
return packageName === name ? spec : `${name}@${spec}`
|
||||
}
|
||||
|
||||
@@ -55,6 +55,11 @@ export interface BundleInfo {
|
||||
enabled: boolean
|
||||
/** Whether the profile's own dependencies hold the package; false for a bundle the dsh installation supplies. */
|
||||
installed: boolean
|
||||
/**
|
||||
* Present for a profile dependency the installation does not also supply: the spec `pnpm add` accepts, with local
|
||||
* paths made absolute and the user information of an http(s) URL removed.
|
||||
*/
|
||||
source?: string
|
||||
/**
|
||||
* Whether the installation ships the bundle for the person to switch on: named by the launcher's `OPTIONAL_BUNDLES`,
|
||||
* held by the installation's dependencies, selected by no shipped template, and never removable.
|
||||
|
||||
@@ -4,8 +4,8 @@ import { realpath } from 'node:fs/promises'
|
||||
import { once } from 'node:events'
|
||||
import { createServer } from 'node:http'
|
||||
import type { Socket } from 'node:net'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { execa } from 'execa'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
@@ -241,7 +241,7 @@ it.each(['missing package', 'invalid manifest', 'not a bundle', 'missing patch',
|
||||
})
|
||||
expect([...ctx.loader.entries()].some(entry => entry.id === 'include:managed')).toBe(false)
|
||||
expect((await manager.listBundles()).find(row => row.name === 'extra')).toMatchObject({
|
||||
enabled: true, error: { code: failure === 'not a bundle' ? 'not-bundle' : 'operation-error' }, rows: [],
|
||||
enabled: true, source: 'extra@1.0.0', error: { code: failure === 'not a bundle' ? 'not-bundle' : 'operation-error' }, rows: [],
|
||||
})
|
||||
expect(await manager.setBundleEnabled('extra', false)).toMatchObject({ changed: true, application: 'applied' })
|
||||
expect(readProfileManifest('test', dir).dsh?.profile?.bundles).toEqual(['core'])
|
||||
@@ -301,7 +301,7 @@ it('lists bundle versions and current-profile plugin targets', async () => {
|
||||
rows: [{ rowId: 'manager', moduleName: 'cordis:manager', entryId: 'include:manager' }], overrides: [],
|
||||
},
|
||||
{
|
||||
name: 'extra', version: '1.0.0', enabled: true, installed: true, optional: false, removable: true,
|
||||
name: 'extra', version: '1.0.0', source: 'extra@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: [],
|
||||
},
|
||||
@@ -323,7 +323,7 @@ it('describes a bundle by its manifest and patch: one-liner, rows without a live
|
||||
writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest))
|
||||
const moduleName = pathToFileURL(join(dir, 'node_modules', 'described', 'plugin.mjs')).href
|
||||
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,
|
||||
name: 'described', version: '2.0.0', description: 'Describes itself.', source: 'described@2.0.0', enabled: false, installed: true, optional: false, removable: true,
|
||||
meta: { title: 'described', description: 'Describes itself.' },
|
||||
rows: [{ rowId: 'described-row', moduleName }], overrides: ['managed'],
|
||||
})
|
||||
@@ -336,6 +336,36 @@ 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('names where each installed bundle comes from as a spec pnpm installs', async () => {
|
||||
const { manager, dir, bundle, profile } = await fixture()
|
||||
const recorded: Record<string, string> = {
|
||||
extra: '^1.0.0', tagged: 'latest', aliased: 'npm:@acme/aliased@2', jsr: 'jsr:@acme/jsr@^1', github: 'github:someone/dsh-plugin#v1',
|
||||
ssh: 'git@github.com:someone/dsh-plugin.git', deploy: 'deploy@git.corp:team/dsh-plugin.git',
|
||||
sshUrl: 'git+ssh://git@github.com/someone/dsh-plugin.git', email: 'git+http://user@example.com:secret@git.example.com/repo.git',
|
||||
tarball: 'https://cdn.example.com/dsh-x-1.0.0.tgz', token: 'https://ghp_secret@cdn.example.com/dsh-x-1.0.0.tgz',
|
||||
password: 'git+https://someone:secret@git.example.com/someone/dsh-plugin.git#main',
|
||||
relative: 'file:../plugins/relative', linked: 'link:/plugins/linked', home: 'file:~/plugins/home',
|
||||
aliasLocal: 'file:../plugins/original', shadowed: '1.0.0',
|
||||
}
|
||||
for (const name of Object.keys(recorded)) bundle(name, [])
|
||||
// Installed under a name other than its own, the package keeps that name only when the spec carries it.
|
||||
writeFileSync(join(dir, 'node_modules', 'aliasLocal', 'package.json'), JSON.stringify({ name: 'original', version: '1.0.0', dsh: { bundle: { patch: './cordis.patch.yml' } } }))
|
||||
const manifest = readProfileManifest('test', dir)
|
||||
manifest.dependencies = recorded
|
||||
writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest))
|
||||
// The installation's own copy of a bundle is the one that loads, so the profile's dependency on it names nothing.
|
||||
writeFileSync(profile.installAnchor, JSON.stringify({ name: 'installation', dependencies: { shadowed: '1.0.0' } }))
|
||||
expect(Object.fromEntries((await manager.listBundles()).map(row => [row.name, row.source]))).toEqual({
|
||||
core: undefined, extra: 'extra@^1.0.0', tagged: 'tagged@latest', aliased: 'aliased@npm:@acme/aliased@2', jsr: 'jsr@jsr:@acme/jsr@^1',
|
||||
github: 'github:someone/dsh-plugin#v1', ssh: 'git@github.com:someone/dsh-plugin.git', deploy: 'deploy@git.corp:team/dsh-plugin.git',
|
||||
sshUrl: 'git+ssh://git@github.com/someone/dsh-plugin.git', email: 'git+http://git.example.com/repo.git',
|
||||
tarball: 'https://cdn.example.com/dsh-x-1.0.0.tgz', token: 'https://cdn.example.com/dsh-x-1.0.0.tgz',
|
||||
password: 'git+https://git.example.com/someone/dsh-plugin.git#main',
|
||||
relative: `file:${resolve(dir, '../plugins/relative')}`, linked: `link:${resolve(dir, '/plugins/linked')}`,
|
||||
home: `file:${resolve(homedir(), 'plugins/home')}`, aliasLocal: `aliasLocal@file:${resolve(dir, '../plugins/original')}`, shadowed: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
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' }])
|
||||
@@ -416,7 +446,7 @@ it.each([
|
||||
writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest))
|
||||
|
||||
expect((await manager.listBundles()).find(row => row.name === 'unnamed')).toEqual({
|
||||
name: 'unnamed', version: '1.0.0', enabled: false, installed: true, optional: false, removable: true,
|
||||
name: 'unnamed', version: '1.0.0', source: 'unnamed@1.0.0', enabled: false, installed: true, optional: false, removable: true,
|
||||
rows: [], overrides: [], ...expected,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -195,10 +195,10 @@ it('keeps UI translation metadata out of model-facing plugin and bundle lists',
|
||||
entries: [{ entryId: 'include:plugin', enabled: true }], total: 1, nextOffset: null,
|
||||
})
|
||||
manager.listBundles.mockImplementationOnce(async () => [{
|
||||
name: 'bundle', enabled: true, meta, rows: [{ rowId: 'plugin', moduleName: 'plugin', meta }],
|
||||
name: 'bundle', source: 'bundle@^1.0.0', enabled: true, meta, rows: [{ rowId: 'plugin', moduleName: 'plugin', meta }],
|
||||
}])
|
||||
expect(JSON.parse(resultText(await call({ action: 'list_bundles' })))).toEqual({
|
||||
entries: [{ name: 'bundle', enabled: true, rows: [{ rowId: 'plugin', moduleName: 'plugin' }] }], total: 1, nextOffset: null,
|
||||
entries: [{ name: 'bundle', source: 'bundle@^1.0.0', enabled: true, rows: [{ rowId: 'plugin', moduleName: 'plugin' }] }], total: 1, nextOffset: null,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
en: c3fad003bf7f8a5a
|
||||
zh: 89a22aa6e8bb2986
|
||||
/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/switching-a-bundle:
|
||||
en: abc90531efa889d5
|
||||
zh: 0562570f85e69c35
|
||||
en: 2d29970dcd0b2836
|
||||
zh: 73f6b519546d2b6b
|
||||
/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/switching-one-row-of-a-bundle:
|
||||
en: 620c6a3ee20848bd
|
||||
zh: a9429f047a12bb4c
|
||||
|
||||
@@ -58,7 +58,7 @@ When the Host attributes a network failure or timeout to a GitHub address and of
|
||||
|
||||
### Switching a bundle
|
||||
|
||||
A bundle's page shows its full package name under the title, the spec that installs it elsewhere. A bundle's switch changes its layer selection. A profile with HMR recomposes before the operation completes; one without HMR, and a bundle a higher layer overrides, say so in a toast. A bundle the Host cannot read carries a problem tag and its reason on its page and cannot be switched on; one that provides the management components stays locked. The Host answers with error codes, which the page's dictionary words; pnpm's and the Loader's own diagnostics are shown as they are. The page excludes built-in profile bundles from cards and counts even when the profile holds them as dependencies or the Host reports an error. The Host inventory remains complete; the Settings Plugins section's Plugin list tab inspects their plugins.
|
||||
A bundle's page shows its full package name under the title. Its **Source** section, after the rows, names the spec that installs it elsewhere, as `pnpm add` accepts it, or **Built in** for an official bundle the installation ships, then its current version. A bundle's switch changes its layer selection. A profile with HMR recomposes before the operation completes; one without HMR, and a bundle a higher layer overrides, say so in a toast. A bundle the Host cannot read carries a problem tag and its reason on its page and cannot be switched on; one that provides the management components stays locked. The Host answers with error codes, which the page's dictionary words; pnpm's and the Loader's own diagnostics are shown as they are. The page excludes built-in profile bundles from cards and counts even when the profile holds them as dependencies or the Host reports an error. The Host inventory remains complete; the Settings Plugins section's Plugin list tab inspects their plugins.
|
||||
|
||||
### Switching one row of a bundle
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ Host 将网络失败或超时归因于 GitHub 地址,且提供 npmmirror 时
|
||||
|
||||
### 切换一个组合包
|
||||
|
||||
组合包页面在标题下方显示完整包名,也就是在别处安装它所需的 spec。组合包开关改变其层选择。启用了 HMR 的 profile 在操作完成前重组;没有 HMR 的 profile,以及被更高层覆盖的组合包,会以 toast 说明。Host 读不了的组合包带异常标签,其页面给出原因,且不能打开;提供管理组件的组合包保持锁定。Host 以错误码作答,由页面字典措辞;pnpm 与 Loader 自己的诊断原样显示。页面从卡片与数量中排除内置 profile 组合包,即使 profile 将它们列为依赖或 Host 报告了异常。Host 清单仍保留完整数据;设置中「插件」分区的「插件列表」标签页负责查看它们的插件。
|
||||
组合包页面在标题下方显示完整包名。组件列表之后的「来源信息」给出在别处安装它所需的 spec(`pnpm add` 接受的形式),安装随附的官方组合包显示「内置」,随后是当前版本。组合包开关改变其层选择。启用了 HMR 的 profile 在操作完成前重组;没有 HMR 的 profile,以及被更高层覆盖的组合包,会以 toast 说明。Host 读不了的组合包带异常标签,其页面给出原因,且不能打开;提供管理组件的组合包保持锁定。Host 以错误码作答,由页面字典措辞;pnpm 与 Loader 自己的诊断原样显示。页面从卡片与数量中排除内置 profile 组合包,即使 profile 将它们列为依赖或 Host 报告了异常。Host 清单仍保留完整数据;设置中「插件」分区的「插件列表」标签页负责查看它们的插件。
|
||||
|
||||
### 切换组合包里的一行
|
||||
|
||||
|
||||
@@ -1246,8 +1246,8 @@ label.registryOption {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
/* The package or row identity under the title: the exact spec that installs the
|
||||
package elsewhere, where a title of its own or a shortened name hides it. */
|
||||
/* The package or row identity under the title, where a title of its own or a
|
||||
shortened name hides it. */
|
||||
.detailName {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
@@ -1297,6 +1297,46 @@ label.registryOption {
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
/* A bundle's source facts: label at the start, value at the end, one ruled line each. */
|
||||
.facts {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.facts div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 44px;
|
||||
padding: 8px 2px;
|
||||
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
||||
}
|
||||
|
||||
.facts dt {
|
||||
flex: none;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.facts dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
text-align: end;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.facts code {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--dsw-alias-bg-module-platform);
|
||||
box-decoration-break: clone;
|
||||
font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* The way into a package's page. */
|
||||
.cardArrow {
|
||||
flex: none;
|
||||
|
||||
@@ -308,6 +308,35 @@ function RowsSection({ rows, t, resolveText, toggle, configure }: {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a bundle comes from: the spec that installs it elsewhere, or built in
|
||||
* for one whose loaded copy the installation supplies, and its version. A
|
||||
* selected bundle that neither the profile nor the installation holds has no
|
||||
* section.
|
||||
*/
|
||||
function SourceSection({ pkg, t }: { readonly pkg: PackageView; readonly t: Translate }): ReactNode {
|
||||
if (pkg.source === undefined && !pkg.installed && !pkg.optional) return null
|
||||
return (
|
||||
<section className={css.detailSection} data-plugin-source>
|
||||
<h4 className={css.sectionTitle}>{t('sourceTitle')}</h4>
|
||||
<dl className={css.facts}>
|
||||
<div>
|
||||
<dt>{t('sourceSpec')}</dt>
|
||||
<dd>{pkg.source === undefined ? t('sourceBuiltIn') : <code>{pkg.source}</code>}</dd>
|
||||
</div>
|
||||
{pkg.version === undefined
|
||||
? null
|
||||
: (
|
||||
<div>
|
||||
<dt>{t('sourceVersion')}</dt>
|
||||
<dd>{pkg.version}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A bundle's enable switch on its card and its page: locked, saying why, for
|
||||
* one the Host protects; off and locked for one it cannot read.
|
||||
@@ -575,9 +604,9 @@ function RowDetail({ pkg, row, t, resolveText, onBack, renderSlot, form }: {
|
||||
* One package's page: the crumb back to the list; its icon with its switch
|
||||
* and, for a package the profile installed, uninstall; its title beside its
|
||||
* version tag, its beta tag, and its problem tag; the package name the title
|
||||
* stands for, which is what installs it elsewhere; its one-liner; the Host's
|
||||
* problem when it reports one; the configuration the bundle registered for
|
||||
* itself; and its rows with their switches and configure controls.
|
||||
* stands for; its one-liner; the Host's problem when it reports one; the
|
||||
* configuration the bundle registered for itself; its rows with their switches
|
||||
* and configure controls; and where it comes from.
|
||||
*/
|
||||
function PackageDetail({
|
||||
pkg, t, resolveText, busy, rowBusy, configured, configure, renderSlot,
|
||||
@@ -659,6 +688,7 @@ function PackageDetail({
|
||||
toggle={pkg.enabled ? { busy: row => busy || rowBusy(row), onSetEnabled: onSetRowEnabled } : undefined}
|
||||
configure={configure}
|
||||
/>
|
||||
<SourceSection pkg={pkg} t={t} />
|
||||
{renderSlot('plugins.detail.section', { subject })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,10 @@ export const zh = {
|
||||
metadataError: '包元信息错误:{error}',
|
||||
versionTag: 'v{version}',
|
||||
partsLabel: '包含的组件',
|
||||
sourceTitle: '来源信息',
|
||||
sourceSpec: '代码来源',
|
||||
sourceBuiltIn: '内置',
|
||||
sourceVersion: '当前版本',
|
||||
partsEmpty: '这个插件包不包含任何组件。',
|
||||
partsCountTotal: '共 {count} 个',
|
||||
partsCountRunning: '{count} 运行中',
|
||||
@@ -232,6 +236,10 @@ export const en = {
|
||||
metadataError: 'Package metadata error: {error}',
|
||||
versionTag: 'v{version}',
|
||||
partsLabel: 'Components',
|
||||
sourceTitle: 'Source',
|
||||
sourceSpec: 'Code source',
|
||||
sourceBuiltIn: 'Built in',
|
||||
sourceVersion: 'Current version',
|
||||
partsEmpty: 'This plugin pack contains no components.',
|
||||
partsCountTotal: '{count} total',
|
||||
partsCountRunning: '{count} running',
|
||||
|
||||
@@ -85,6 +85,8 @@ export interface PackageView {
|
||||
readonly meta?: PluginLocalizedMeta
|
||||
/** Whether the profile's own dependencies hold the package; false for a bundle the installation supplies. */
|
||||
readonly installed: boolean
|
||||
/** Present for a profile dependency the installation does not also supply: the spec `pnpm add` accepts. */
|
||||
readonly source?: string
|
||||
/** Whether the installation ships the bundle for the person to switch on: official, off until selected, never removable. */
|
||||
readonly optional: boolean
|
||||
/** Whether the bundle is in the profile's layer list. */
|
||||
@@ -425,6 +427,7 @@ export function packageView(bundle: BundleInfo, plugins: readonly PluginInfo[]):
|
||||
enabled: bundle.enabled,
|
||||
rows,
|
||||
...bundle.version === undefined ? {} : { version: bundle.version },
|
||||
...bundle.source === undefined ? {} : { source: bundle.source },
|
||||
...bundle.description === undefined ? {} : { description: bundle.description },
|
||||
...bundle.meta === undefined ? {} : { meta: bundle.meta },
|
||||
...bundle.readOnlyReason === undefined ? {} : { readOnlyReason: bundle.readOnlyReason },
|
||||
|
||||
@@ -285,6 +285,26 @@ describe('PluginManagerPage', () => {
|
||||
expect(document.querySelector('[data-plugin-package="dsh-better-sidebar"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('says where a bundle comes from: the spec that installs it, or built in, with its version', () => {
|
||||
const b = renderTab({ packages: [
|
||||
pkg({ source: 'github:someone/dsh-better-sidebar' }),
|
||||
{ name: 'dsh-official', installed: false, optional: true, enabled: false, rows: [] },
|
||||
{ name: 'dsh-shadowed', installed: true, optional: false, enabled: true, rows: [] },
|
||||
{ name: 'dsh-missing', installed: false, optional: false, enabled: true, error: { code: 'unknown-plugin' }, rows: [] },
|
||||
] })
|
||||
const facts = (): string[] => [...document.querySelectorAll('[data-plugin-source] dt, [data-plugin-source] dd')].map(node => node.textContent)
|
||||
act(() => { b.navigation.actions.setView({ kind: 'package', name: 'dsh-better-sidebar' }) })
|
||||
expect(within(document.querySelector('[data-plugin-source]') as HTMLElement).getByRole('heading').textContent).toBe(en.sourceTitle)
|
||||
expect(facts()).toEqual([en.sourceSpec, 'github:someone/dsh-better-sidebar', en.sourceVersion, '0.16.0'])
|
||||
act(() => { b.navigation.actions.setView({ kind: 'package', name: 'dsh-official' }) })
|
||||
expect(facts()).toEqual([en.sourceSpec, en.sourceBuiltIn])
|
||||
// A profile dependency the installation also supplies loads the installation's copy.
|
||||
act(() => { b.navigation.actions.setView({ kind: 'package', name: 'dsh-shadowed' }) })
|
||||
expect(facts()).toEqual([en.sourceSpec, en.sourceBuiltIn])
|
||||
act(() => { b.navigation.actions.setView({ kind: 'package', name: 'dsh-missing' }) })
|
||||
expect(document.querySelector('[data-plugin-source]')).toBeNull()
|
||||
})
|
||||
|
||||
it('preserves the requested bundle through StrictMode effect replay and page remounts', () => {
|
||||
const b = renderTab({ packages: [pkg()] })
|
||||
b.unmount()
|
||||
|
||||
@@ -19,6 +19,7 @@ const BUNDLE: BundleInfo = {
|
||||
description: 'A sidebar.',
|
||||
enabled: false,
|
||||
installed: true,
|
||||
source: 'dsh-better-sidebar@^0.16.0',
|
||||
optional: false,
|
||||
removable: true,
|
||||
rows: [{ rowId: 'sidebar', moduleName: 'dsh-better-sidebar', entryId: ROW_ENTRY }, { rowId: 'theme', moduleName: 'dsh-better-sidebar/theme' }],
|
||||
@@ -115,7 +116,7 @@ it('hands a custom page the shared configuration form of its entry', () => {
|
||||
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.',
|
||||
name: 'dsh-better-sidebar', version: '0.16.0', description: 'A sidebar.', source: 'dsh-better-sidebar@^0.16.0',
|
||||
installed: true, optional: false, enabled: false,
|
||||
rows: [
|
||||
{ rowId: 'sidebar', moduleName: 'dsh-better-sidebar', entryId: ROW_ENTRY, enabled: true, phase: 'active' },
|
||||
|
||||
@@ -1656,7 +1656,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
signature: '@Remote listBundles(): Promise<BundleInfo[]>',
|
||||
description: 'Read the profile\'s installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles. A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.',
|
||||
parameters: [],
|
||||
returns: 'Package versions, manifest descriptions, rows, optional display metadata, activation selections, whether the installation offers the bundle, and removal availability.',
|
||||
returns: 'Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional display metadata, activation selections, whether the installation offers the bundle, and removal availability.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote async registries(): Promise<PluginRegistries>',
|
||||
@@ -4707,7 +4707,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 source?: string;\n optional: boolean;\n removable: boolean;\n readOnlyReason?: ReadOnlyReason;\n error?: ManagementError;\n rows: BundleRowInfo[];\n overrides: string[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'BundleRowInfo',
|
||||
|
||||
Reference in New Issue
Block a user