Files
deepseek-harness/packages/boot/plugin-manager/src/view.ts
T

205 lines
8.4 KiB
TypeScript

/**
* The view fold: one package's manifest facts, static declarations, and live-tree
* rows folded into the `PluginPackageView` the manager lists, plus the row
* ownership walk the dependents query shares.
* @module @deepseek-ai/dsh-plugin-manager/view
*/
import type { Context, FiberState } from '@deepseek-ai/cordis'
import type { Entry } from '@deepseek-ai/cordis-plugin-loader'
import { rootIncludeEntry, layerTrust, type EntryIssue, type PackageMetadata, type ProfileManifest, type ProfileRuntime } from '@deepseek-ai/dsh-app-boot'
import type { BundleStage } from '@deepseek-ai/dsh-package-manifest'
import { bundlesOf, dependenciesOf, messageOf, optional } from './helpers.ts'
import type { PluginInstaller } from './installer.ts'
import { addableView } from './modules.ts'
import type { PluginPackageRowView, PluginPackageStatus, PluginPackageView, PluginRowPhase } from './types.ts'
/** 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 projection of Cordis Fiber states onto the row phase vocabulary. */
const ROW_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, PluginRowPhase>
/** Fields of one row's provenance a package view needs. */
export interface RowFacts {
readonly entry: Entry
readonly rowId: string
}
/**
* Fold one package's manifest, declarations, and tree facts into its view.
* @param ctx - the context whose Loader tree and current entry diagnostics the rows come from.
* @param runtime - the booted profile.
* @param installer - the installer over the same profile, for static declarations and the installed manifest.
* @param manifest - the profile manifest, read once per list.
* @param name - the package.
* @param allIssues - issues observed once for this list request.
* @returns the view.
*/
export function packageView(
ctx: Context,
runtime: ProfileRuntime,
installer: PluginInstaller,
manifest: ProfileManifest,
name: string,
allIssues: readonly EntryIssue[],
): PluginPackageView {
const installed = name in dependenciesOf(manifest)
const enabled = bundlesOf(manifest).includes(name)
const layer = runtime.layers.find(candidate => candidate.packageName === name)
const trust = layer?.trust ?? layerTrust(manifest, name)
const liveReload = runtime.patchReload === 'live'
let metadata: PackageMetadata | undefined
let metadataFailure: string | undefined
if (installed) {
try {
metadata = installer.metadata(name)
} catch (error) {
metadataFailure = messageOf(error)
}
}
let packageManifest: ReturnType<PluginInstaller['readInstalledManifest']>
try {
packageManifest = installer.readInstalledManifest(name)
} catch (error) {
metadataFailure = messageOf(error)
}
const stage: BundleStage = layer?.stage
?? (manifest.dsh?.profile?.stages?.[name] ?? packageManifest?.dsh?.bundle?.stage ?? 'runtime')
const kind = metadata?.kind ?? (layer !== undefined || packageManifest?.dsh?.bundle !== undefined ? 'bundle' : 'unknown')
const composed = layer !== undefined
const rootTree = rootIncludeEntry(ctx)?.subtree
const affected = allIssues.filter(issue => runtime.originOfEntry(issue.entry)?.packageName === name
|| (issue.entry.parent.tree === rootTree && metadata?.overrides.includes(issue.entry.options.id)))
const rows = composed ? composedRows(ctx, runtime, name, allIssues) : declaredRows(metadata)
const status = packageStatus({ kind, installed, enabled, composed, liveReload, metadataFailure, rows, affected })
const reason = metadataFailure ?? (status === 'restart-required'
? 'the profile applies layer changes at its next start'
: status === 'failed' || status === 'partial'
? affected[0]?.message ?? rows.find(row => row.failure !== undefined)?.failure?.message
: undefined)
return {
name,
...optional('version', packageManifest?.version),
...optional('title', packageManifest?.dsh?.title),
...optional('description', packageManifest?.description),
kind,
trust,
stage,
installed,
enabled,
status,
...optional('reason', reason),
...optional('enginesDsh', metadata?.enginesDsh),
cordisSameCopy: metadata?.cordisSameCopy ?? null,
rows,
overrides: metadata?.overrides ?? [],
addable: (metadata?.addable ?? []).map(entry => addableView(name, entry)),
...affected.length === 0 ? {} : { issues: affected.map(issue => ({
entryId: issue.entry.id, moduleName: issue.entry.options.name, stage: issue.stage, message: issue.message,
})) },
liveReload,
}
}
/** The package's status, folded from what the view already knows. */
function packageStatus(facts: {
kind: PluginPackageView['kind']
installed: boolean
enabled: boolean
composed: boolean
liveReload: boolean
metadataFailure: string | undefined
affected: readonly EntryIssue[]
rows: readonly PluginPackageRowView[]
}): PluginPackageStatus {
if (facts.kind !== 'bundle') return 'plain'
if (facts.installed && facts.metadataFailure !== undefined) return 'not-enableable'
if (facts.enabled !== facts.composed) return 'restart-required'
if (!facts.enabled) return 'disabled'
const live = facts.rows.filter(row => row.enabled)
if (live.length === 0) return facts.affected.length === 0 ? 'running' : 'failed'
const active = live.filter(row => row.phase === 'active').length
if (active === live.length && facts.affected.length === 0) return 'running'
return active === 0 ? 'failed' : 'partial'
}
/** Current native entries and failed groups belonging to the bundle. */
function composedRows(ctx: Context, runtime: ProfileRuntime, name: string, issues: readonly EntryIssue[]): PluginPackageRowView[] {
const failures = new Map(issues.map(issue => [issue.entry, issue]))
const rows: PluginPackageRowView[] = []
for (const entry of ctx.loader.entries()) {
if (runtime.originOfEntry(entry)?.packageName !== name) continue
const failure = failures.get(entry)
if (entry.options.group && failure === undefined) continue
const enabled = failure?.stage === 'disabled-expression' || !entry.disabled
rows.push({
entryId: entry.id,
rowId: entry.options.id,
moduleName: entry.options.name,
enabled,
...enabled ? {} : { disabledBy: runtime.userDisables(entry) ? 'user' as const : 'composition' as const },
phase: entry.fiber === undefined ? (failure === undefined ? null : 'failed') : ROW_PHASE[entry.fiber.state],
...failure === undefined ? {} : { failure: { stage: failure.stage, message: failure.message } },
})
}
// A row the composition left out never reached the tree; the runtime's
// conflicts name the bundle that lost, so nothing is looked up by id.
for (const conflict of runtime.conflicts) {
if (conflict.packageName !== name) continue
rows.push({
entryId: `conflict:${conflict.layer}:${conflict.rowId}`,
rowId: conflict.rowId,
moduleName: conflict.moduleName,
enabled: true,
phase: 'failed',
failure: { stage: 'conflict', message: conflict.message },
})
}
return rows
}
/**
* The non-group tree entries a package's layer inserted.
* @param ctx - the context whose Loader tree to walk.
* @param runtime - the booted profile, which says which layer owns each row id.
* @param name - the package.
* @returns the entries with the row ids their patches declare.
*/
export function ownedEntries(ctx: Context, runtime: ProfileRuntime, name: string): RowFacts[] {
const found: RowFacts[] = []
for (const entry of ctx.loader.entries()) {
if (entry.options.group) continue
const rowId = entry.options.id
if (runtime.originOfEntry(entry)?.packageName !== name) continue
found.push({ entry, rowId })
}
return found
}
/** The rows a bundle would contribute, from its patch declarations, when it is not composed. */
function declaredRows(metadata: PackageMetadata | undefined): PluginPackageRowView[] {
return (metadata?.rows ?? []).map(row => ({
entryId: row.id ?? row.name,
rowId: row.id ?? row.name,
moduleName: row.name,
enabled: !row.gated,
...row.gated ? { disabledBy: 'composition' as const } : {},
phase: null,
}))
}