Create docs/plugin-ecosystem(.en).md as the ecosystem manifesto: an open, composable, sustainable vision with composition-first, declare-clearly, and compatibility-first principles, the desktop shell as the first example, living-document community governance, and the marketplace as the trust signal. The root README intro now ends with the co-progress vision and links the manifesto; the docs index and root README tables list it; the plugin-development ecosystem-vision section points to it as the full statement.
6.6 KiB
DSH Desktop Plugin Development
Understand the two plugin layers
A normal DSH plugin can provide Host services, commands, routes, bundles, or a Web Client. It should depend on upstream DSH contracts whenever possible so the same package can work in the CLI, an ordinary Web profile, and DSH Desktop.
Desktop adds two public Host services:
desktopProfiles: reads the active profile, discovers selectable profiles, and requests a safe profile switch.desktopPnpm: runs pnpm in the active profile, or manages plugins through the officialdsh pluginsemantics.
These services live in the Host Cordis generation in Electron's main process. The renderer cannot read them directly; a plugin with browser UI should continue to use ordinary DSH Web routes, RPC, client metadata, services, and slots.
The complete types, lifecycle, and failure semantics are in dsh-plugin-desktop/docs/plugin-services.md. This page focuses on selection and the minimum safe patterns.
Desktop-only plugins
If a plugin only makes sense in Desktop, declare the services as required injections:
import type { Context } from '@deepseek-ai/cordis'
import type {} from 'dsh-plugin-desktop/profile-service'
import type { DesktopPnpmHandle } from 'dsh-plugin-desktop/pnpm'
export const name = 'example-desktop-plugin'
export const inject = ['desktopProfiles', 'desktopPnpm']
export function apply(ctx: Context): void {
ctx.logger.info(`profile: ${ctx.desktopProfiles.current.name}`)
let active: DesktopPnpmHandle | undefined
// Connect this function to an explicit user action in the plugin UI.
async function installExample(): Promise<void> {
active = ctx.desktopPnpm.runPlugin(
['add', 'example-plugin'],
process.cwd(),
)
await active.done
}
ctx.effect(() => {
return async () => {
active?.cancel()
await active?.done.catch(() => {})
}
}, 'example plugin operation')
}
Production code should invoke package operations from an explicit user action, validate the target, read stdout/stderr, set its own timeout, and check both exitCode and signal. A generation allows only one desktopPnpm package operation at a time; dispose must cancel and await it.
Plugins that work in Desktop and ordinary DSH
When the same package must also run under ordinary dsh web, do not put Desktop services in the top-level required inject list. Inject ordinary dependencies first and dynamically detect Desktop:
export const inject = ['webServer', 'loader']
export function apply(ctx: Context, config: { profile?: string }): void {
const profiles = ctx.get('desktopProfiles')
if (profiles === undefined) {
mountOrdinaryDshManager(ctx, config.profile ?? 'web')
return
}
ctx.inject(['desktopPnpm'], (desktopPnpm) => {
mountManager(ctx, {
profile: profiles.current.name,
profileDir: profiles.current.dir,
runPlugin: (args, cwd, signal) => desktopPnpm.runPlugin(args, cwd, signal),
})
})
}
The ordinary DSH fallback remains the plugin's authoritative implementation. Do not infer the Desktop profile from process.argv, ctx.baseUrl, settings, or $DSH_HOME; in Desktop, use desktopProfiles.current.
run() versus runPlugin()
desktopPnpm.run(args) is a low-level pnpm operation with the active profile as its cwd. It does not promise DSH profile initialization, caller-relative file:/link: anchoring, or dsh.profile.bundles reconciliation.
desktopPnpm.runPlugin(args, invokingDir) runs packaged dsh plugin --profile <active> and preserves upstream plugin-management semantics. Use it for install, remove, update, and dependency repair:
desktopPnpm.runPlugin(['add', target], invokingDir, signal)
desktopPnpm.runPlugin(['remove', packageName], invokingDir, signal)
desktopPnpm.runPlugin(['update'], invokingDir, signal)
desktopPnpm.runPlugin(['install', '--no-frozen-lockfile'], invokingDir, signal)
Arguments are passed as argv. Do not concatenate shell strings or depend on Windows .cmd shims. The service settles only after the whole subprocess tree exits and terminates active operations during generation disposal.
APIs not to depend on
desktopRuntime, desktopPnpmBootstrap, Electron BrowserWindow, the tray registry, private Node helpers, ELECTRON_RUN_AS_NODE, and generated shims are Desktop internals. Their presence in declarations or runtime context does not make them third-party compatibility contracts.
Testing and release checks
At minimum, a plugin should cover:
- Loading under ordinary DSH without Desktop services, or staying pending by design.
- Matching the profile name and directory reported by Desktop to the user's actual selection.
- Cancellation, non-zero exit, spawn failure, and generation teardown for package operations.
- Restarting after a plugin change and seeing the bundle in the next Loader composition.
Read the architecture next, then use the package-level service contract as the API reference.
Ecosystem vision: keep the plugin ecosystem composable
The DSH plugin ecosystem is growing quickly. The more plugins there are, the more their ability to work together matters — if every plugin assumes or overrides another plugin's internals, installing a few plugins starts to conflict and the ecosystem fragments.
We advocate a browser-plugin style of development: everyone extends the same platform against the same conventions, instead of each maintaining a modified runtime of their own. DSH Desktop is the first practitioner of this approach — the desktop shell itself is an ordinary plugin on the same composition path as official and third-party plugins, with no special privileges.
To that end we are starting a development-conventions initiative and hope it becomes a de facto standard through community adoption:
- Composition first: compose capabilities through official slots, services, and patches; do not assume or override other plugins' internals.
- Declare clearly: state the services and slots you depend on; do not rely on runtime coincidences.
- Compatibility first: keep upgrades backward compatible and never break existing compositions.
The conventions are a living document that follows ecosystem practice and accepts community discussion and revisions. Once the plugin marketplace ships, compliant plugins will be easier to discover, install, and trust, making convention-driven development the beneficial choice for every author. See the DSH plugin ecosystem manifesto for the full vision, principles, and how to participate.