The sidebar Plugins page now drives `ctx.pluginManager` from #4182 and the settings Plugin list tab goes back to read-only: its bundle form and row checkboxes are removed, and the page joins `listBundles` with `listPlugins` into one view per bundle, decides availability from the inventory's `managementAvailable`, switches bundles and rows through `setBundleEnabled` and `setPluginEnabled`, and confirms uninstalls. The manager gains what the guided install dialog needs: `inspect(spec)` reads a spec through the registry or a directory before anything installs and refuses specs without a bundle; `installBundle` takes a request id, streams each pnpm run as `plugin-manager/install-log`, reports its phases as `plugin-manager/install-state`, classifies a failed run into `packageResult.kind`, names the bundle it added, and restores `package.json` and `pnpm-lock.yaml` when the run fails, is cancelled, or adds a package without a bundle patch; `cancelInstall(requestId)` answers only after pnpm exited and the files are back; `listBundles` carries titles, one-liners, declared rows with their live entries, and the built-in rows a bundle overrides; every completed operation and every patch generation applied outside the manager emits `plugin-manager/changed`. The config gains `pnpmCommand` and `inspectTimeoutMs`. App boot emits `profile/reconciled` after each applied patch generation and finds a root Include mounted under its pinned id by another copy of the module. Row diagnostics, dependency checks, and the non-bundle dependency group are not carried over; the web e2e scaffold mounts a launcher-style managed profile instead of the removed profile runtime.
106 lines
3.8 KiB
TypeScript
106 lines
3.8 KiB
TypeScript
/** Read-only projection of the current Cordis Loader plugin entries. */
|
|
|
|
import type { Context, FiberState } from '@deepseek-ai/cordis'
|
|
import type {} from '@deepseek-ai/cordis-plugin-loader'
|
|
// Type-only: the optional agent-preset roster resolved through `ctx.get`.
|
|
import type {} from '@deepseek-ai/dsh-agent-presets'
|
|
import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol'
|
|
// Typert-generated ./typert and ./remote artifacts import Zod at runtime.
|
|
import type {} from 'zod'
|
|
import type {
|
|
AgentPresetPluginGroup,
|
|
PluginEntryId,
|
|
PluginFiberPhase,
|
|
PluginInventoryEntry,
|
|
PluginInventorySnapshot,
|
|
} from './types.ts'
|
|
|
|
export type * from './types.ts'
|
|
|
|
/**
|
|
* Brand an existing Loader-tree entry id at the owning boundary.
|
|
* @param value - the entry id as the Loader tree spells it.
|
|
* @returns the same id as the inventory's branded entry id.
|
|
*/
|
|
export function pluginEntryId(value: string): PluginEntryId {
|
|
return value as PluginEntryId
|
|
}
|
|
|
|
/** Runtime mirror: FiberState is a cross-package const enum. */
|
|
const FIBER_STATE = {
|
|
PENDING: 0 as FiberState.PENDING,
|
|
LOADING: 1 as FiberState.LOADING,
|
|
ACTIVE: 2 as FiberState.ACTIVE,
|
|
FAILED: 3 as FiberState.FAILED,
|
|
DISPOSED: 4 as FiberState.DISPOSED,
|
|
UNLOADING: 5 as FiberState.UNLOADING,
|
|
} as const
|
|
|
|
/** Complete public projection of Cordis Fiber states. */
|
|
const FIBER_PHASE = {
|
|
[FIBER_STATE.PENDING]: 'pending',
|
|
[FIBER_STATE.LOADING]: 'loading',
|
|
[FIBER_STATE.ACTIVE]: 'active',
|
|
[FIBER_STATE.FAILED]: 'failed',
|
|
[FIBER_STATE.DISPOSED]: null,
|
|
[FIBER_STATE.UNLOADING]: 'unloading',
|
|
} as const satisfies Record<FiberState, PluginFiberPhase>
|
|
|
|
/** Remote-only service exposing the Loader's current non-group entry state. */
|
|
export class PluginInventoryGateway extends TypertRemoteService {
|
|
static inject = ['loader']
|
|
|
|
constructor(ctx: Context) {
|
|
super(ctx, 'pluginInventory')
|
|
}
|
|
|
|
/**
|
|
* Read the Loader directly on every call. Cordis's internal plugin/status
|
|
* events already maintain Entry.fiber and Fiber.state, so a second cache
|
|
* would only add another lifecycle truth to keep synchronized.
|
|
*
|
|
* When an agent-preset roster is composed, the snapshot also carries each
|
|
* preset's composition rows, because those rows — not the Loader's own
|
|
* entries — are where a deployment that mounts the roster runs its
|
|
* model-facing plugins.
|
|
* @returns Current non-group Loader entries in Loader order, with per-preset
|
|
* compositions when a roster is composed.
|
|
*/
|
|
@Remote('list')
|
|
async list(): Promise<PluginInventorySnapshot> {
|
|
return readPluginInventory(this.ctx)
|
|
}
|
|
}
|
|
|
|
export default PluginInventoryGateway
|
|
|
|
/** Read current Loader entries and optional preset compositions.
|
|
* @param ctx Context with the Loader service.
|
|
* @returns Current inventory without a separate runtime cache.
|
|
*/
|
|
export async function readPluginInventory(ctx: Context): Promise<PluginInventorySnapshot> {
|
|
const entries: PluginInventoryEntry[] = []
|
|
for (const entry of ctx.loader.entries()) {
|
|
if (entry.options.group) continue
|
|
entries.push({
|
|
entryId: pluginEntryId(entry.id),
|
|
moduleName: entry.options.name,
|
|
enabled: !entry.disabled,
|
|
fiberPhase: entry.fiber === undefined ? null : FIBER_PHASE[entry.fiber.state],
|
|
})
|
|
}
|
|
const presets = ctx.get('agentPresets')
|
|
const management = ctx.get('pluginManager') === undefined ? {} : { managementAvailable: true }
|
|
if (presets === undefined) return { entries, ...management }
|
|
const agentPresets: AgentPresetPluginGroup[] = (await presets.compositionInventory()).map(
|
|
composition => ({
|
|
...composition,
|
|
rows: composition.rows.map(({ fiberState, ...row }) => ({
|
|
...row,
|
|
fiberPhase: fiberState === undefined ? null : FIBER_PHASE[fiberState],
|
|
})),
|
|
}),
|
|
)
|
|
return { entries, agentPresets, ...management }
|
|
}
|