Files
orca/src/preload/api/plugin-host-api.ts
T
Neil 4221f8d429 refactor(preload): split the preload contract into per-domain api modules (#14403)
`src/preload/api-types.ts` was 3,752 raw lines (3,533 counted, 11.8x the
300-line budget) behind an `eslint-disable max-lines`. Almost all of it was a
single `PreloadApi` object type whose ~83 namespace properties were declared
inline, so any IPC surface change meant editing one 2,600-line type.

Give each namespace a named type in its own module under `src/preload/api/`
(`pty-api.ts`, `filesystem-api.ts`, `github-pull-request-api.ts`, ...) and
recompose `PreloadApi` from those names. `api-types.ts` keeps the `declare
global` Window augmentation and re-exports every moved name, so all 52 import
sites are untouched.

Two shapes needed care to stay type-identical rather than merely compatible:

- Three keys (`gh`, `git`, `ui`) are composed from two modules each. A plain
  intersection is NOT identical to the original flat object literal, so those
  use a `Merged<T>` mapped type; a negative control confirmed that dropping it
  fails the parity assertion.
- Keys whose module groups several namespaces use indexed access
  (`fs: FilesystemApi['fs']`) to preserve exact identity and source order.

`config/tsconfig.web.json` and `tsconfig.tc.web.json` enumerate files by path,
so they need `src/preload/api/**/*` alongside the existing `api-types.ts` seed
or the web projects fail TS6307.

Verified by exact type identity, not assignability: 41 assertions of the form
`Equals<Now.X, Before.X>` against a frozen pre-split snapshot, covering every
exported name, plus a per-key pass over all 83 `PreloadApi` keys. All three
projects typecheck clean with those assertions active.

Verification note: these tsconfigs are `composite: true`, and `tsc --noEmit`
will reuse a stale `.tsbuildinfo` and report clean for a state that genuinely
fails. Every result above was produced after deleting the buildinfo, including
a negative control confirming the gate still fails on deliberate drift.

Drops the `max-lines` bypass and its baseline entry (ratchet 346 -> 345).
2026-08-13 20:52:04 -07:00

192 lines
6.4 KiB
TypeScript

import type {
PluginPanelActionOutcome,
PluginPanelEntry
} from '../../shared/plugins/plugin-panel-bridge'
import type { PluginConsentRequest } from '../../shared/plugins/plugin-consent-request'
import type { PluginLanguagePackRegistration } from '../../shared/plugins/plugin-language-pack-artifact'
import type { PluginChangeEvent } from '../../shared/plugins/plugin-change-event'
import type { PluginManifest } from '../../shared/plugins/plugin-manifest'
import type { PluginMarketplaceGitSource } from '../../shared/plugins/plugin-marketplace'
/** Panel contribution as surfaced by the main-process plugin service. */
export type PluginHostPanel = {
id: string
title: string
/** Lucide icon name declared in the plugin manifest. */
icon?: string
tabKey: `plugin:${string}`
}
/** `pending` = awaiting (re-)consent; `idle` = enabled, worker not running
* (lazy); `restarting` = waiting for supervised backoff; `errored` = crashed past the restart budget or failed to start;
* `invalid` = unreadable manifest. */
export type PluginHostStatus =
| 'running'
| 'restarting'
| 'idle'
| 'pending'
| 'disabled'
| 'errored'
| 'invalid'
/** Wire shape of plugins:list — must stay assignable from the main-process
* projection in src/main/plugins/plugin-list-projection.ts. */
export type PluginHostListEntry = {
pluginKey: string
consentFingerprint: string | null
name: string
version: string
publisher: string
description?: string
status: PluginHostStatus
needsReconsent: boolean
error?: string
isDev: boolean
official: boolean
bundled: boolean
capabilities: { kind: string; description: string }[]
panels: PluginHostPanel[]
commands: {
id: string
title: string
context: 'global' | 'worktree'
handler: { type: 'built-in'; action: string } | { type: 'worker' }
keybindings: { key: string; when: 'global' | 'worktree' }[]
}[]
hasWorker: boolean
vmRecipes?: {
id: string
name: string
description?: string
commands: {
phase: 'create' | 'suspend' | 'resume' | 'destroy'
command: string
}[]
}[]
restarts: number
blockedByKillList?: { reason: string; advisoryUrl?: string }
source?: {
kind: 'local-path' | 'git' | 'marketplace' | 'bundled'
reference: string
resolvedCommit: string | null
contentHash: string
marketplace?: { reference: string; resolvedCommit: string }
}
}
export type PluginHostLogLine = {
ts: number
level: 'info' | 'warn' | 'error'
line: string
}
export type PluginHostInstallSource =
| { kind: 'local-path'; path: string }
| { kind: 'git'; url: string; ref: string }
export type PluginHostInstallResult =
| {
ok: true
pluginKey: string
version: string
contentHash: string
consentFingerprint: string
resolvedCommit: string | null
}
| { ok: false; error: string }
export type PluginMarketplaceHostSourceState = {
id: string
source: PluginMarketplaceGitSource
addedAt: number
marketplace: {
name: string
owner: string
resolvedCommit: string
fetchedAt: number
} | null
stale: boolean
official: boolean
error?: string
}
export type PluginMarketplaceHostListing = {
marketplaceSourceId: string
marketplaceName: string
marketplaceOwner: string
marketplaceCommit: string
pluginKey: string
source: PluginMarketplaceGitSource
description?: string
categories: string[]
official: boolean
bundled: boolean
blockedByKillList?: { reason: string; advisoryUrl?: string }
}
export type PluginMarketplaceHostInstallPreview = {
marketplaceSourceId: string
marketplaceName: string
marketplaceOwner: string
marketplaceCommit: string
pluginKey: string
source: PluginMarketplaceGitSource
resolvedCommit: string
contentHash: string
consentFingerprint: string
manifest: PluginManifest
official: boolean
bundled: boolean
blockedByKillList?: { reason: string; advisoryUrl?: string }
}
export type PluginsApi = {
list: () => Promise<PluginHostListEntry[]>
listLanguagePacks: () => Promise<PluginLanguagePackRegistration[]>
/** Records the consent-dialog answer; approval is keyed to the plugin's
* current capability and trusted-worker fingerprint. */
consent: (args: PluginConsentRequest) => Promise<PluginHostListEntry[]>
setEnabled: (args: { pluginKey: string; enabled: boolean }) => Promise<PluginHostListEntry[]>
/** Returns the panel's CSP-wrapped HTML, or null when the plugin or
* panel is missing/disabled. Rendered only inside a sandboxed iframe. */
readPanelEntry: (args: { pluginKey: string; panelId: string }) => Promise<PluginPanelEntry | null>
invokeCommand: (args: {
pluginKey: string
commandId: string
args?: unknown
}) => Promise<unknown>
/** Relays a sandboxed panel's bridge request to main, which enforces the
* plugin's consented capabilities before executing. */
panelAction: (args: {
sessionToken: string
action: string
params?: unknown
}) => Promise<PluginPanelActionOutcome>
install: (source: PluginHostInstallSource) => Promise<PluginHostInstallResult>
listMarketplaces: () => Promise<PluginMarketplaceHostSourceState[]>
addMarketplace: (source: PluginMarketplaceGitSource) => Promise<PluginMarketplaceHostSourceState>
removeMarketplace: (args: { sourceId: string }) => Promise<PluginMarketplaceHostSourceState[]>
refreshMarketplaces: (args?: { sourceId?: string }) => Promise<PluginMarketplaceHostSourceState[]>
listMarketplacePlugins: () => Promise<PluginMarketplaceHostListing[]>
previewMarketplacePlugin: (args: {
marketplaceSourceId: string
pluginKey: string
}) => Promise<PluginMarketplaceHostInstallPreview>
installMarketplacePlugin: (
preview: Pick<
PluginMarketplaceHostInstallPreview,
'marketplaceSourceId' | 'marketplaceCommit' | 'pluginKey' | 'resolvedCommit'
>
) => Promise<PluginHostInstallResult>
previewMarketplaceUpdate: (args: {
pluginKey: string
}) => Promise<PluginMarketplaceHostInstallPreview>
rollbackMarketplacePlugin: (args: { pluginKey: string }) => Promise<PluginHostInstallResult>
remove: (args: { pluginKey: string }) => Promise<PluginHostListEntry[]>
getLogs: (args: { pluginKey: string }) => Promise<PluginHostLogLine[]>
/** Re-discovers after settings edits (feature flag, dev paths). */
refresh: () => Promise<PluginHostListEntry[]>
/** Fires whenever installed plugins, worker states, panels, or content packs change. */
onChanged: (callback: (event: PluginChangeEvent) => void) => () => void
}