mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-21 13:21:17 +08:00
docs(opencode): sketch plugin redesign
This commit is contained in:
193
packages/opencode/specs/plugin-architecture.md
Normal file
193
packages/opencode/specs/plugin-architecture.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Plugin architecture
|
||||
|
||||
This is a working note for reorganizing plugin code while the codebase migrates to Effect.
|
||||
|
||||
## Current shape
|
||||
|
||||
The main problem is that one conceptual system is split across a few large modules with overlapping responsibilities.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[ConfigPlugin origins] --> B[plugin/shared.ts]
|
||||
A --> C[plugin/loader.ts]
|
||||
A --> D[plugin/install.ts]
|
||||
|
||||
B --> C
|
||||
B --> E[plugin/index.ts server runtime]
|
||||
B --> F[tui/plugin/runtime.ts TUI runtime]
|
||||
B --> D
|
||||
|
||||
C --> E
|
||||
C --> F
|
||||
|
||||
D --> G[cli/cmd/plug.ts]
|
||||
D --> F
|
||||
|
||||
H[plugin/meta.ts] --> F
|
||||
|
||||
I[npm/index.ts] --> B
|
||||
I --> C
|
||||
I --> D
|
||||
|
||||
style B fill:#3a2f1f,stroke:#c98a2b,color:#fff
|
||||
style C fill:#3a2f1f,stroke:#c98a2b,color:#fff
|
||||
style D fill:#3a2f1f,stroke:#c98a2b,color:#fff
|
||||
style E fill:#4a1f1f,stroke:#d46a6a,color:#fff
|
||||
style F fill:#4a1f1f,stroke:#d46a6a,color:#fff
|
||||
```
|
||||
|
||||
### What is mixed together today
|
||||
|
||||
- `src/plugin/shared.ts`
|
||||
spec parsing, package reading, entry resolution, compatibility checks, theme discovery, module validation, id resolution
|
||||
- `src/plugin/loader.ts`
|
||||
plan building, target resolution, import, retry rules, reporting hooks
|
||||
- `src/plugin/install.ts`
|
||||
install wrapper, manifest inspection, config patching, file locking
|
||||
- `src/plugin/index.ts`
|
||||
server plugin runtime, hook loading, config fanout, event subscription
|
||||
- `src/cli/cmd/tui/plugin/runtime.ts`
|
||||
TUI runtime state, loading, activation, API adaptation, theme sync, install flow, pending state, process singleton
|
||||
- `src/plugin/meta.ts`
|
||||
file-backed mutable plugin metadata store
|
||||
|
||||
## Target shape
|
||||
|
||||
The redesign should split stateless plugin plumbing from stateful runtimes.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Pure[Pure helpers]
|
||||
S1[plugin/spec.ts]
|
||||
S2[plugin/module.ts]
|
||||
S3[plugin/manifest.ts]
|
||||
end
|
||||
|
||||
subgraph Effects[Effect functions]
|
||||
E1[plugin/package.ts]
|
||||
E2[plugin/external.ts]
|
||||
E3[plugin/install.ts]
|
||||
T2[tui/plugin/theme.ts]
|
||||
T3[tui/plugin/api.ts]
|
||||
T4[tui/plugin/scope.ts]
|
||||
T5[tui/plugin/activation.ts]
|
||||
end
|
||||
|
||||
subgraph Services[Effect services]
|
||||
SV1[plugin/meta-store.ts PluginMetaStore.Service]
|
||||
SV2[plugin/server.ts PluginServer.Service]
|
||||
SV3[tui/plugin/manager.ts TuiPluginManager.Service]
|
||||
end
|
||||
|
||||
Cfg[ConfigPlugin Origins] --> S1
|
||||
S1 --> E1
|
||||
S1 --> E2
|
||||
S2 --> E2
|
||||
S3 --> E3
|
||||
E1 --> E2
|
||||
E1 --> E3
|
||||
E2 --> SV2
|
||||
E2 --> SV3
|
||||
T2 --> SV3
|
||||
T3 --> SV3
|
||||
T4 --> SV3
|
||||
T5 --> SV3
|
||||
SV1 --> SV3
|
||||
E3 --> CLI[cli/cmd/plug.ts]
|
||||
E3 --> SV3
|
||||
|
||||
style Pure fill:#1f3a2a,stroke:#4fa06b,color:#fff
|
||||
style Effects fill:#1f2f4a,stroke:#5c8fda,color:#fff
|
||||
style Services fill:#3b1f4a,stroke:#b070d6,color:#fff
|
||||
```
|
||||
|
||||
## Module boundaries
|
||||
|
||||
### Pure helpers
|
||||
|
||||
- `src/plugin/spec.ts`
|
||||
parse specifiers, detect npm vs file, normalize ids
|
||||
- `src/plugin/module.ts`
|
||||
validate exported module shape, extract `id`, read v1 server or TUI modules
|
||||
- `src/plugin/manifest.ts`
|
||||
derive package capabilities from package metadata
|
||||
|
||||
These should not touch the filesystem or global state.
|
||||
|
||||
### Effect functions
|
||||
|
||||
- `src/plugin/package.ts`
|
||||
read `package.json`, check compatibility, read theme files
|
||||
- `src/plugin/external.ts`
|
||||
resolve targets, resolve entrypoints, import modules, retry local file plugins after dependency prep
|
||||
- `src/plugin/install.ts`
|
||||
shared install and config-patch workflow used by CLI and TUI
|
||||
- `src/cli/cmd/tui/plugin/theme.ts`
|
||||
sync and persist themes
|
||||
- `src/cli/cmd/tui/plugin/api.ts`
|
||||
adapt host API to plugin API
|
||||
- `src/cli/cmd/tui/plugin/scope.ts`
|
||||
lifecycle resource helpers
|
||||
- `src/cli/cmd/tui/plugin/activation.ts`
|
||||
activate and deactivate one plugin entry
|
||||
|
||||
These are composable functions that return `Effect`, but do not own long-lived mutable state.
|
||||
|
||||
### Services
|
||||
|
||||
- `PluginMetaStore.Service`
|
||||
owns the metadata file and lock-backed updates
|
||||
- `PluginServer.Service`
|
||||
owns loaded server hooks and bus subscription state per project/worktree via `InstanceState`
|
||||
- `TuiPluginManager.Service`
|
||||
owns loaded TUI entries, enabled state, pending installs, and activation lifecycle
|
||||
|
||||
## Runtime split
|
||||
|
||||
### Server side
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[PluginServer.Service] --> B[build plugin input]
|
||||
A --> C[load internal plugins]
|
||||
A --> D[load external plugins]
|
||||
D --> E[plugin/external.ts]
|
||||
A --> F[notify config]
|
||||
A --> G[subscribe bus events]
|
||||
```
|
||||
|
||||
### TUI side
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[TuiPluginManager.Service] --> B[load internal entries]
|
||||
A --> C[load external entries]
|
||||
C --> D[plugin/external.ts]
|
||||
A --> E[track plugin metadata]
|
||||
E --> F[PluginMetaStore.Service]
|
||||
A --> G[activate and deactivate]
|
||||
G --> H[tui/plugin/activation.ts]
|
||||
A --> I[sync themes]
|
||||
I --> J[tui/plugin/theme.ts]
|
||||
A --> K[install and configure plugin]
|
||||
K --> L[plugin/install.ts]
|
||||
```
|
||||
|
||||
## Design rules
|
||||
|
||||
- Keep orchestration readable at the top level.
|
||||
- Put state in services, not module globals.
|
||||
- Prefer typed results over callback-driven reporting.
|
||||
- Share install/configure workflow between CLI and TUI.
|
||||
- Keep plugin discovery parallel, but keep activation and hook registration sequential.
|
||||
- Preserve the special cases that already matter:
|
||||
theme-only TUI plugins, legacy server plugins, local file-plugin retry after dependency prep.
|
||||
|
||||
## Suggested migration order
|
||||
|
||||
1. Split `shared.ts` into `spec.ts`, `module.ts`, `manifest.ts`, and `package.ts` without changing behavior.
|
||||
2. Replace `loader.ts` with flat exports in `external.ts` and return typed result values instead of report callbacks.
|
||||
3. Collapse duplicated install flow into one shared `plugin/install.ts` workflow used by CLI and TUI.
|
||||
4. Convert `meta.ts` into `PluginMetaStore.Service`.
|
||||
5. Shrink `plugin/index.ts` into a thin `PluginServer.Service` composition root.
|
||||
6. Break up `tui/plugin/runtime.ts` and move its mutable runtime state into `TuiPluginManager.Service`.
|
||||
56
packages/opencode/src/plug/README.md
Normal file
56
packages/opencode/src/plug/README.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# plug
|
||||
|
||||
Type-only sketch for a cleaner plugin architecture.
|
||||
|
||||
This folder is intentionally not wired into the application yet.
|
||||
It exists so the shape of a redesign is easy to inspect without mixing design work with runtime changes.
|
||||
|
||||
## Files
|
||||
|
||||
- `common.ts`
|
||||
shared helper types used by the rest of the sketch
|
||||
- `spec.ts`
|
||||
plugin declaration and normalization types
|
||||
- `package.ts`
|
||||
package metadata and capability inspection types
|
||||
- `module.ts`
|
||||
imported module shape and validation types
|
||||
- `external.ts`
|
||||
external plugin load pipeline types
|
||||
- `meta.ts`
|
||||
plugin metadata store types
|
||||
- `install.ts`
|
||||
install and config patch workflow types
|
||||
- `server.ts`
|
||||
server plugin service types
|
||||
- `tui.ts`
|
||||
TUI plugin manager service types
|
||||
|
||||
## Reading order
|
||||
|
||||
If you want the sketch to build up from small concepts to runtime orchestration, read the files in this order:
|
||||
|
||||
1. `spec.ts`
|
||||
Start here for the basic nouns: plugin kinds, sources, declarations, config origins, and normalized candidates.
|
||||
2. `package.ts`
|
||||
Next read how package metadata is described after a plugin target has been resolved.
|
||||
3. `module.ts`
|
||||
Then read the imported module shapes and validation results for v1 and legacy modules.
|
||||
4. `external.ts`
|
||||
This is the shared external loading pipeline that connects spec parsing, package inspection, and module import.
|
||||
5. `meta.ts`
|
||||
Read this next to see what state should be persisted across runs and why it belongs behind a service.
|
||||
6. `install.ts`
|
||||
This describes the install, manifest, and config patch workflow shared by CLI and TUI.
|
||||
7. `server.ts`
|
||||
Read the server runtime service after the lower-level pipeline files, since it mainly composes those pieces.
|
||||
8. `tui.ts`
|
||||
Read this last because it has the largest runtime surface and depends on most of the earlier concepts.
|
||||
9. `common.ts`
|
||||
This file is only shared utility typing. You can skim it first or ignore it until you see a helper type you want to expand.
|
||||
|
||||
## Intent
|
||||
|
||||
- Stateful parts are described as service interfaces.
|
||||
- Stateless parts are described as function types returning `Effect`.
|
||||
- The comments explain what each type is for and where it would sit in the architecture.
|
||||
48
packages/opencode/src/plug/common.ts
Normal file
48
packages/opencode/src/plug/common.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Small helper alias used throughout this folder so the design signatures stay readable.
|
||||
*
|
||||
* These files are only a type sketch, so we reference the Effect type without creating any runtime code.
|
||||
*/
|
||||
export type Fx<Success, Error = never, Requirements = never> = import("effect").Effect.Effect<
|
||||
Success,
|
||||
Error,
|
||||
Requirements
|
||||
>
|
||||
|
||||
/**
|
||||
* Standard success result shape used by the sketch files.
|
||||
*
|
||||
* The current plugin code already uses similar tagged unions in a few places.
|
||||
*/
|
||||
export type Ok<Value> = {
|
||||
ok: true
|
||||
value: Value
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard failure result shape used by the sketch files.
|
||||
*
|
||||
* `code` stays machine-friendly while the extra data explains the specific failure.
|
||||
*/
|
||||
export type Failure<Code extends string, Data> = {
|
||||
ok: false
|
||||
code: Code
|
||||
} & Data
|
||||
|
||||
/**
|
||||
* Generic loaded module namespace.
|
||||
*
|
||||
* Dynamic imports return a namespace object, and the next stage decides whether that namespace is a
|
||||
* v1 module, a legacy server export set, or an invalid module.
|
||||
*/
|
||||
export type ModuleNamespace = Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Shared shape for objects that carry both a configured spec and its resolved on-disk target.
|
||||
*/
|
||||
export interface SpecTarget {
|
||||
/** The original normalized plugin spec, for example `pkg@1.2.3` or `file:///.../plugin.ts`. */
|
||||
readonly spec: string
|
||||
/** The resolved install location or local file URL that later stages work against. */
|
||||
readonly target: string
|
||||
}
|
||||
123
packages/opencode/src/plug/external.ts
Normal file
123
packages/opencode/src/plug/external.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { Candidate, Kind, Options, Origin, Source } from "./spec"
|
||||
import type { Fx, ModuleNamespace, SpecTarget } from "./common"
|
||||
import type { PackageRecord } from "./package"
|
||||
|
||||
/**
|
||||
* Normalized external plugin plan before any filesystem or npm work happens.
|
||||
*/
|
||||
export interface Plan {
|
||||
/** Original normalized string spec. */
|
||||
readonly spec: string
|
||||
/** Optional inline config tuple payload. */
|
||||
readonly options: Options | undefined
|
||||
/** Whether the package is deprecated because the functionality is now built in. */
|
||||
readonly deprecated: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* External plugin that has been resolved to a concrete on-disk entrypoint.
|
||||
*/
|
||||
export interface Resolved extends SpecTarget {
|
||||
/** Options to forward when the plugin is instantiated. */
|
||||
readonly options: Options | undefined
|
||||
/** Whether the plugin came from a file path or npm install. */
|
||||
readonly source: Source
|
||||
/** JavaScript module entrypoint that can be dynamically imported. */
|
||||
readonly entry: string
|
||||
/** Loaded package metadata when a package.json exists. */
|
||||
readonly pkg: PackageRecord | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* External plugin target that was found but does not expose the requested runtime entrypoint.
|
||||
*
|
||||
* This is a first-class result because TUI still cares about theme-only packages.
|
||||
*/
|
||||
export interface MissingEntrypoint extends SpecTarget {
|
||||
/** Options to forward if some later stage still wants to keep the plugin record. */
|
||||
readonly options: Options | undefined
|
||||
/** Whether the target came from a file path or npm install. */
|
||||
readonly source: Source
|
||||
/** Loaded package metadata when a package.json exists. */
|
||||
readonly pkg: PackageRecord | undefined
|
||||
/** Human-readable explanation of what was missing. */
|
||||
readonly message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved plugin whose module has been imported successfully.
|
||||
*/
|
||||
export interface Loaded extends Resolved {
|
||||
/** Raw dynamic import namespace. */
|
||||
readonly module: ModuleNamespace
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipeline stages where a plugin load can fail.
|
||||
*/
|
||||
export type FailureStage = "install" | "entry" | "compatibility" | "load"
|
||||
|
||||
/**
|
||||
* External load failure record.
|
||||
*
|
||||
* This replaces the current callback-heavy report object with an explicit value.
|
||||
*/
|
||||
export interface Failure {
|
||||
/** Which configured plugin was being processed. */
|
||||
readonly candidate: Candidate
|
||||
/** Which pass failed. */
|
||||
readonly stage: FailureStage
|
||||
/** Whether the failure happened during the retry-after-dependencies pass. */
|
||||
readonly retry: boolean
|
||||
/** Underlying failure object. */
|
||||
readonly error: unknown
|
||||
/** Best-known resolution details when the failure happened after entry resolution. */
|
||||
readonly resolved: Resolved | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of processing one configured external plugin.
|
||||
*/
|
||||
export type AttemptResult =
|
||||
| {
|
||||
/** Successfully resolved and imported plugin module. */
|
||||
readonly type: "loaded"
|
||||
readonly origin: Origin
|
||||
readonly retry: boolean
|
||||
readonly value: Loaded
|
||||
}
|
||||
| {
|
||||
/** Target exists but does not expose the requested runtime entrypoint. */
|
||||
readonly type: "missing"
|
||||
readonly origin: Origin
|
||||
readonly retry: boolean
|
||||
readonly value: MissingEntrypoint
|
||||
}
|
||||
| {
|
||||
/** Operational failure during install, resolution, compatibility, or import. */
|
||||
readonly type: "failed"
|
||||
readonly value: Failure
|
||||
}
|
||||
|
||||
/**
|
||||
* Input to the external loading workflow.
|
||||
*/
|
||||
export interface LoadRequest {
|
||||
/** Ordered merged config origins to process. */
|
||||
readonly items: readonly Origin[]
|
||||
/** Which runtime is asking for plugins. */
|
||||
readonly kind: Kind
|
||||
/**
|
||||
* Optional dependency-prep effect.
|
||||
*
|
||||
* If provided, file plugins that failed during the first pass may be retried after this effect completes.
|
||||
*/
|
||||
readonly waitForDependencies: Fx<void> | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Intended signature for the shared external loader.
|
||||
*
|
||||
* The return value preserves order and gives callers explicit success, missing-entry, and failure results.
|
||||
*/
|
||||
export type LoadExternal = (request: LoadRequest) => Fx<readonly AttemptResult[]>
|
||||
147
packages/opencode/src/plug/install.ts
Normal file
147
packages/opencode/src/plug/install.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import type { Origin } from "./spec"
|
||||
import type { Failure, Fx, Ok } from "./common"
|
||||
|
||||
/**
|
||||
* How a config patch changed one file.
|
||||
*/
|
||||
export type PatchMode = "noop" | "add" | "replace"
|
||||
|
||||
/**
|
||||
* Why a package is considered to target one runtime.
|
||||
*/
|
||||
export type TargetReason = "server-export" | "tui-export" | "package-main" | "themes"
|
||||
|
||||
/**
|
||||
* One runtime target inferred from an installed package.
|
||||
*/
|
||||
export interface Target {
|
||||
/** Which runtime should receive a config entry. */
|
||||
readonly kind: "server" | "tui"
|
||||
/** Optional default options to write alongside the plugin spec. */
|
||||
readonly options: Record<string, unknown> | undefined
|
||||
/** Why this runtime target was inferred from package metadata. */
|
||||
readonly reason: TargetReason
|
||||
}
|
||||
|
||||
/**
|
||||
* High-level package manifest summary returned by plugin inspection.
|
||||
*/
|
||||
export interface Manifest {
|
||||
/** Installed or resolved target used to inspect the package. */
|
||||
readonly target: string
|
||||
/** Runtime targets inferred from that package. */
|
||||
readonly targets: readonly Target[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Context needed when patching config files after install.
|
||||
*/
|
||||
export interface PatchRequest {
|
||||
/** Plugin spec to add or replace. */
|
||||
readonly spec: string
|
||||
/** Runtime targets that should be written into config. */
|
||||
readonly targets: readonly Target[]
|
||||
/** Whether an existing npm package entry may be replaced by package name. */
|
||||
readonly force: boolean
|
||||
/** Whether the write should go to the global config directory. */
|
||||
readonly global: boolean
|
||||
/** VCS hint used to decide whether the worktree root should own the local config write. */
|
||||
readonly vcs: string | undefined
|
||||
/** Current worktree path. */
|
||||
readonly worktree: string
|
||||
/** Current working directory. */
|
||||
readonly directory: string
|
||||
/** Optional explicit global config directory override. */
|
||||
readonly config: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* One config file mutation performed during patching.
|
||||
*/
|
||||
export interface PatchItem {
|
||||
/** Which runtime file was touched. */
|
||||
readonly kind: "server" | "tui"
|
||||
/** Whether the plugin row was added, replaced, or already present. */
|
||||
readonly mode: PatchMode
|
||||
/** Concrete config file path that was inspected or written. */
|
||||
readonly file: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Final result of a successful config patch operation.
|
||||
*/
|
||||
export interface PatchSuccess {
|
||||
/** Directory that owned the config write. */
|
||||
readonly dir: string
|
||||
/** Per-runtime write details. */
|
||||
readonly items: readonly PatchItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared error union for install and config patch workflows.
|
||||
*/
|
||||
export type InstallFailure =
|
||||
| Failure<"install_failed", { error: unknown }>
|
||||
| Failure<"manifest_read_failed", { file: string; error: unknown }>
|
||||
| Failure<"manifest_no_targets", { file: string }>
|
||||
| Failure<"invalid_json", { kind: "server" | "tui"; file: string; line: number; col: number; parse: string }>
|
||||
| Failure<"patch_failed", { kind: "server" | "tui"; error: unknown }>
|
||||
|
||||
/**
|
||||
* Current low-level install result shape.
|
||||
*/
|
||||
export type InstallResult = Ok<{ target: string }>
|
||||
|
||||
/**
|
||||
* Current low-level manifest inspection result shape.
|
||||
*/
|
||||
export type ManifestResult = Ok<{ manifest: Manifest }> | InstallFailure
|
||||
|
||||
/**
|
||||
* Current low-level patch result shape.
|
||||
*/
|
||||
export type PatchResult = Ok<PatchSuccess> | InstallFailure
|
||||
|
||||
/**
|
||||
* High-level request for the full install-and-configure workflow.
|
||||
*/
|
||||
export interface InstallAndConfigureRequest {
|
||||
/** Raw package spec entered by the user. */
|
||||
readonly spec: string
|
||||
/** Whether the installed plugin should be written to global config. */
|
||||
readonly global: boolean
|
||||
/** Whether an already-configured npm package may be replaced by package name. */
|
||||
readonly force: boolean
|
||||
/** Current working directory. */
|
||||
readonly directory: string
|
||||
/** Current worktree root. */
|
||||
readonly worktree: string
|
||||
/** VCS hint used when picking the local config directory. */
|
||||
readonly vcs: string | undefined
|
||||
/** Optional explicit global config directory override. */
|
||||
readonly config: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of the full install-and-configure workflow.
|
||||
*
|
||||
* The optional `origin` field is useful for the TUI runtime, which wants to track newly configured
|
||||
* plugins before they are actually activated.
|
||||
*/
|
||||
export interface InstallAndConfigureSuccess {
|
||||
/** Resolved install target returned by the package install step. */
|
||||
readonly target: string
|
||||
/** Inferred package manifest. */
|
||||
readonly manifest: Manifest
|
||||
/** Config patch details. */
|
||||
readonly patch: PatchSuccess
|
||||
/** Newly configured TUI origin when the package exposed a TUI target. */
|
||||
readonly origin: Origin | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Intended shared workflow used by both `opencode plug` and the TUI runtime.
|
||||
*/
|
||||
export type InstallAndConfigure = (
|
||||
request: InstallAndConfigureRequest,
|
||||
) => Fx<InstallAndConfigureSuccess, InstallFailure>
|
||||
102
packages/opencode/src/plug/meta.ts
Normal file
102
packages/opencode/src/plug/meta.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { Fx } from "./common"
|
||||
|
||||
/**
|
||||
* External sources tracked in the metadata file.
|
||||
*
|
||||
* Internal plugins are intentionally excluded because the metadata file is for user-configurable external plugins.
|
||||
*/
|
||||
export type Source = "file" | "npm"
|
||||
|
||||
/**
|
||||
* Persisted information about one installed theme file provided by a TUI plugin.
|
||||
*/
|
||||
export interface ThemeEntry {
|
||||
/** Original theme file inside the plugin package or local plugin root. */
|
||||
readonly src: string
|
||||
/** Final persisted destination in the user's themes directory. */
|
||||
readonly dest: string
|
||||
/** Source file modification time used to detect changes. */
|
||||
readonly mtime: number | undefined
|
||||
/** Source file size used to detect changes. */
|
||||
readonly size: number | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Persisted metadata about one external plugin id.
|
||||
*/
|
||||
export interface Entry {
|
||||
/** Logical runtime id. */
|
||||
readonly id: string
|
||||
/** Whether this record describes a file plugin or npm plugin. */
|
||||
readonly source: Source
|
||||
/** Normalized config spec that produced the record. */
|
||||
readonly spec: string
|
||||
/** Resolved install directory or file target. */
|
||||
readonly target: string
|
||||
/** Requested npm version or range from the original spec, when relevant. */
|
||||
readonly requested: string | undefined
|
||||
/** Installed package version, when relevant. */
|
||||
readonly version: string | undefined
|
||||
/** Modified time for local file plugins, when relevant. */
|
||||
readonly modified: number | undefined
|
||||
/** First time this plugin id was seen. */
|
||||
readonly first_time: number
|
||||
/** Most recent time this plugin id was loaded. */
|
||||
readonly last_time: number
|
||||
/** Most recent time the record fingerprint changed. */
|
||||
readonly time_changed: number
|
||||
/** Number of times the plugin has been loaded. */
|
||||
readonly load_count: number
|
||||
/** Compact identity used to decide whether the plugin changed between runs. */
|
||||
readonly fingerprint: string
|
||||
/** Theme files installed on behalf of this plugin. */
|
||||
readonly themes: Readonly<Record<string, ThemeEntry>> | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* How the latest touch compared with the previously persisted record.
|
||||
*/
|
||||
export type TouchState = "first" | "updated" | "same"
|
||||
|
||||
/**
|
||||
* Input required to update metadata for one external plugin load.
|
||||
*/
|
||||
export interface TouchInput {
|
||||
/** Normalized config spec. */
|
||||
readonly spec: string
|
||||
/** Resolved install target or file target. */
|
||||
readonly target: string
|
||||
/** Logical runtime id. */
|
||||
readonly id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Result returned after updating metadata for one plugin.
|
||||
*/
|
||||
export interface TouchResult {
|
||||
/** Whether the plugin is new, changed, or unchanged compared with the previous record. */
|
||||
readonly state: TouchState
|
||||
/** The full persisted entry after the update. */
|
||||
readonly entry: Entry
|
||||
}
|
||||
|
||||
/**
|
||||
* Entire on-disk metadata store keyed by runtime plugin id.
|
||||
*/
|
||||
export type Store = Readonly<Record<string, Entry>>
|
||||
|
||||
/**
|
||||
* Stateful service responsible for the plugin metadata file.
|
||||
*
|
||||
* This is a strong service boundary because it owns a lock, persistence format, and mutation rules.
|
||||
*/
|
||||
export interface Interface {
|
||||
/** Update metadata for one plugin load. */
|
||||
readonly touch: (input: TouchInput) => Fx<TouchResult>
|
||||
/** Update metadata for many plugin loads in a single locked write. */
|
||||
readonly touchMany: (input: readonly TouchInput[]) => Fx<readonly TouchResult[]>
|
||||
/** Persist one installed theme record under an existing plugin id. */
|
||||
readonly setTheme: (input: { id: string; name: string; theme: ThemeEntry }) => Fx<void>
|
||||
/** Read the entire current metadata store. */
|
||||
readonly list: () => Fx<Store>
|
||||
}
|
||||
87
packages/opencode/src/plug/module.ts
Normal file
87
packages/opencode/src/plug/module.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { Plugin as ServerPluginFactory, PluginModule as ServerPluginModule } from "@opencode-ai/plugin"
|
||||
import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
|
||||
import type { ModuleNamespace } from "./common"
|
||||
|
||||
/**
|
||||
* Validation mode for imported plugin modules.
|
||||
*
|
||||
* `strict` means the module must clearly match the expected target shape.
|
||||
* `detect` means the loader is probing for a v1 module before falling back to older compatibility paths.
|
||||
*/
|
||||
export type ValidationMode = "strict" | "detect"
|
||||
|
||||
/**
|
||||
* Current v1 server module shape.
|
||||
*
|
||||
* A v1 module is target-exclusive, so it must not export both `server` and `tui` from the same default export.
|
||||
*/
|
||||
export type V1ServerModule = {
|
||||
/** Optional logical plugin id. npm plugins may omit this and fall back to package name. */
|
||||
readonly id?: string
|
||||
/** Server plugin factory used to create hook handlers. */
|
||||
readonly server: ServerPluginModule["server"]
|
||||
/** Explicitly absent to make the target-exclusive shape obvious. */
|
||||
readonly tui?: never
|
||||
}
|
||||
|
||||
/**
|
||||
* Current v1 TUI module shape.
|
||||
*/
|
||||
export type V1TuiModule = {
|
||||
/** Optional logical plugin id. */
|
||||
readonly id?: string
|
||||
/** TUI plugin factory used to register commands, routes, and UI hooks. */
|
||||
readonly tui: TuiPluginModule["tui"]
|
||||
/** Explicitly absent to make the target-exclusive shape obvious. */
|
||||
readonly server?: never
|
||||
}
|
||||
|
||||
/**
|
||||
* Union of the currently supported v1 module shapes.
|
||||
*/
|
||||
export type V1Module = V1ServerModule | V1TuiModule
|
||||
|
||||
/**
|
||||
* Older server plugin export styles still supported for backward compatibility.
|
||||
*
|
||||
* These only exist on the server side.
|
||||
*/
|
||||
export type LegacyServerExport =
|
||||
| ServerPluginFactory
|
||||
| {
|
||||
readonly server: ServerPluginFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of validating an imported module namespace.
|
||||
*/
|
||||
export type ValidationResult =
|
||||
| {
|
||||
/** Namespace matched the modern v1 shape. */
|
||||
readonly type: "v1"
|
||||
/** Parsed target-exclusive default export. */
|
||||
readonly module: V1Module
|
||||
}
|
||||
| {
|
||||
/** Namespace did not match v1 but did contain legacy server plugin exports. */
|
||||
readonly type: "legacy-server"
|
||||
/** Distinct legacy server plugin factories extracted from the namespace. */
|
||||
readonly exports: readonly LegacyServerExport[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Intended signature for the module validation function.
|
||||
*
|
||||
* This stays separate from the external loader so module-shape rules are easy to inspect on their own.
|
||||
*/
|
||||
export type ValidateModule = (
|
||||
namespace: ModuleNamespace,
|
||||
input: {
|
||||
/** Human-readable spec used in error messages. */
|
||||
readonly spec: string
|
||||
/** Which runtime is being validated. */
|
||||
readonly kind: "server" | "tui"
|
||||
/** Whether this is a hard validation pass or a soft probe. */
|
||||
readonly mode: ValidationMode
|
||||
},
|
||||
) => ValidationResult | undefined
|
||||
84
packages/opencode/src/plug/package.ts
Normal file
84
packages/opencode/src/plug/package.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { Kind, Source } from "./spec"
|
||||
|
||||
/**
|
||||
* Raw package.json object for a plugin package.
|
||||
*
|
||||
* The real implementation would read this from disk and then derive higher-level capability types
|
||||
* from it.
|
||||
*/
|
||||
export type PackageJson = Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Package metadata resolved from a plugin target on disk.
|
||||
*/
|
||||
export interface PackageRecord {
|
||||
/** Root directory that owns the package.json. */
|
||||
readonly dir: string
|
||||
/** Absolute path to the package.json file. */
|
||||
readonly pkg: string
|
||||
/** Parsed package.json content. */
|
||||
readonly json: PackageJson
|
||||
}
|
||||
|
||||
/**
|
||||
* Why a runtime target was inferred from a package.
|
||||
*
|
||||
* This makes install and diagnostics code easier to read because the source of a capability is explicit.
|
||||
*/
|
||||
export type CapabilityReason = "server-export" | "tui-export" | "package-main" | "themes"
|
||||
|
||||
/**
|
||||
* One runtime capability inferred from package metadata.
|
||||
*/
|
||||
export interface Capability {
|
||||
/** Which runtime this capability belongs to. */
|
||||
readonly kind: Kind
|
||||
/** Why this capability exists. */
|
||||
readonly reason: CapabilityReason
|
||||
/** Optional default config written during install when the package provides it. */
|
||||
readonly options: Record<string, unknown> | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary of theme assets declared by a package.
|
||||
*/
|
||||
export interface ThemeManifest {
|
||||
/** Directory that relative theme paths should be resolved from. */
|
||||
readonly root: string
|
||||
/** Package-relative theme files that survived validation. */
|
||||
readonly files: readonly string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatibility information declared by an npm package.
|
||||
*/
|
||||
export interface Compatibility {
|
||||
/** Whether the compatibility gate was actually checked. */
|
||||
readonly checked: boolean
|
||||
/** The running opencode version used during the check. */
|
||||
readonly runtimeVersion: string
|
||||
/** The declared `engines.opencode` range, if any. */
|
||||
readonly range: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Package inspection result after a target has been resolved.
|
||||
*/
|
||||
export interface PackageResolution {
|
||||
/** Normalized plugin spec. */
|
||||
readonly spec: string
|
||||
/** External source kind. */
|
||||
readonly source: Source
|
||||
/** Resolved install directory or file URL. */
|
||||
readonly target: string
|
||||
/** Loaded package metadata when a package.json exists. */
|
||||
readonly pkg: PackageRecord | undefined
|
||||
/** Entrypoint picked for the requested runtime target, if one exists. */
|
||||
readonly entry: string | undefined
|
||||
/** Runtime capabilities inferred from the package metadata. */
|
||||
readonly capabilities: readonly Capability[]
|
||||
/** Validated theme manifest when the package declares `oc-themes`. */
|
||||
readonly themes: ThemeManifest | undefined
|
||||
/** Compatibility info for npm packages. */
|
||||
readonly compatibility: Compatibility | undefined
|
||||
}
|
||||
93
packages/opencode/src/plug/server.ts
Normal file
93
packages/opencode/src/plug/server.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { Hooks, Plugin as ServerPluginFactory, PluginInput } from "@opencode-ai/plugin"
|
||||
import type { Fx } from "./common"
|
||||
import type { Loaded } from "./external"
|
||||
import type { Source } from "./spec"
|
||||
|
||||
/**
|
||||
* Built-in server plugin that ships with the app and does not go through the external loader.
|
||||
*/
|
||||
export interface InternalPlugin {
|
||||
/** Stable id used for diagnostics and duplicate detection. */
|
||||
readonly id: string
|
||||
/** Factory that creates one `Hooks` object when the plugin service starts. */
|
||||
readonly plugin: ServerPluginFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* One successfully instantiated server plugin hook set.
|
||||
*/
|
||||
export interface HookEntry {
|
||||
/** Stable runtime id. */
|
||||
readonly id: string
|
||||
/** Normalized spec that produced this hook set. */
|
||||
readonly spec: string
|
||||
/** Whether the plugin came from npm, a file path, or built-in code. */
|
||||
readonly source: Source | "internal"
|
||||
/** Hook handlers returned by the plugin factory. */
|
||||
readonly hooks: Hooks
|
||||
}
|
||||
|
||||
/**
|
||||
* Stateful data owned by the server plugin runtime per project/worktree instance.
|
||||
*/
|
||||
export interface State {
|
||||
/** Fully initialized hook sets in the order they should run. */
|
||||
readonly loaded: readonly HookEntry[]
|
||||
/** Flat hook list kept for the existing trigger API. */
|
||||
readonly hooks: readonly Hooks[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook names that follow the trigger pattern `(input, output) => Promise<void>`.
|
||||
*/
|
||||
export type TriggerName = {
|
||||
[Name in keyof Hooks]-?: NonNullable<Hooks[Name]> extends (input: infer _Input, output: infer _Output) => Promise<void>
|
||||
? Name
|
||||
: never
|
||||
}[keyof Hooks]
|
||||
|
||||
/**
|
||||
* Input type for one triggerable hook name.
|
||||
*/
|
||||
export type TriggerInput<Name extends TriggerName> = Parameters<Required<Hooks>[Name]>[0]
|
||||
|
||||
/**
|
||||
* Output accumulator type for one triggerable hook name.
|
||||
*/
|
||||
export type TriggerOutput<Name extends TriggerName> = Parameters<Required<Hooks>[Name]>[1]
|
||||
|
||||
/**
|
||||
* Context assembled before any server plugins are instantiated.
|
||||
*/
|
||||
export interface RuntimeContext {
|
||||
/** Rich plugin input passed to every server plugin factory. */
|
||||
readonly input: PluginInput
|
||||
/** Whether external plugins should be skipped because the runtime is in pure mode. */
|
||||
readonly pure: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Stateless adapter that turns a loaded external module into one or more server hook sets.
|
||||
*
|
||||
* The return type is plural because legacy server modules can still expose multiple plugin factories.
|
||||
*/
|
||||
export type ApplyLoaded = (load: Loaded, input: PluginInput) => Fx<readonly Hooks[]>
|
||||
|
||||
/**
|
||||
* Public service surface for the server plugin runtime.
|
||||
*
|
||||
* The implementation would be backed by `InstanceState` because loaded hooks and bus subscriptions are
|
||||
* scoped to one project/worktree instance.
|
||||
*/
|
||||
export interface Interface {
|
||||
/** Ensure the per-instance plugin state has been initialized. */
|
||||
readonly init: () => Fx<void>
|
||||
/** Return the currently loaded hook objects. */
|
||||
readonly list: () => Fx<readonly Hooks[]>
|
||||
/** Trigger one hook name across loaded plugins while preserving plugin order. */
|
||||
readonly trigger: <Name extends TriggerName>(
|
||||
name: Name,
|
||||
input: TriggerInput<Name>,
|
||||
output: TriggerOutput<Name>,
|
||||
) => Fx<TriggerOutput<Name>>
|
||||
}
|
||||
91
packages/opencode/src/plug/spec.ts
Normal file
91
packages/opencode/src/plug/spec.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type {
|
||||
Options as ConfigPluginOptions,
|
||||
Origin as ConfigPluginOrigin,
|
||||
Scope as ConfigPluginScope,
|
||||
Spec as ConfigPluginSpec,
|
||||
} from "@/config/plugin"
|
||||
|
||||
/**
|
||||
* The two external plugin sources supported by the current system.
|
||||
*
|
||||
* `file` means a local path or `file://` URL.
|
||||
* `npm` means an installable package spec.
|
||||
*/
|
||||
export type Source = "file" | "npm"
|
||||
|
||||
/**
|
||||
* `internal` is used by the TUI runtime for built-in plugins that are shipped with the app.
|
||||
*
|
||||
* It is kept separate from `Source` because it is not part of the external plugin loading pipeline.
|
||||
*/
|
||||
export type RuntimeSource = Source | "internal"
|
||||
|
||||
/**
|
||||
* The two runtime targets a package can expose.
|
||||
*/
|
||||
export type Kind = "server" | "tui"
|
||||
|
||||
/**
|
||||
* Inline config tuple options forwarded to the plugin factory.
|
||||
*/
|
||||
export type Options = ConfigPluginOptions
|
||||
|
||||
/**
|
||||
* Raw plugin config entry as it appears in `opencode.json` or `tui.json`.
|
||||
*/
|
||||
export type Input = ConfigPluginSpec
|
||||
|
||||
/**
|
||||
* Config provenance attached to a plugin declaration after config merging.
|
||||
*
|
||||
* This answers "which config file declared this plugin?" so follow-up writes can go back to the
|
||||
* right place.
|
||||
*/
|
||||
export type Origin = ConfigPluginOrigin
|
||||
|
||||
/**
|
||||
* Whether a config origin should behave like a global or project-local plugin declaration.
|
||||
*/
|
||||
export type Scope = ConfigPluginScope
|
||||
|
||||
/**
|
||||
* Parsed npm-style package identity.
|
||||
*
|
||||
* This is the identity used for dedupe and for better install error messages.
|
||||
*/
|
||||
export interface ParsedSpecifier {
|
||||
/** Package name portion of the spec. For file specs this will usually just be the original string. */
|
||||
readonly pkg: string
|
||||
/** Version request portion of the spec. Bare package names typically normalize to `latest`. */
|
||||
readonly version: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized external plugin declaration.
|
||||
*
|
||||
* This is the shape that downstream loading code should work with instead of raw config tuples.
|
||||
*/
|
||||
export interface Declared {
|
||||
/** Original config provenance. */
|
||||
readonly origin: Origin
|
||||
/** Normalized string spec extracted from the raw config value. */
|
||||
readonly spec: string
|
||||
/** Optional config tuple payload that should be forwarded to the plugin factory. */
|
||||
readonly options: Options | undefined
|
||||
/** Whether this should be treated as a file plugin or npm plugin. */
|
||||
readonly source: Source
|
||||
/** Whether the package name maps to a built-in plugin and should therefore be ignored. */
|
||||
readonly deprecated: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Candidate item passed into the external load pipeline.
|
||||
*
|
||||
* The name exists to make it obvious that the plugin has not been resolved or imported yet.
|
||||
*/
|
||||
export interface Candidate {
|
||||
/** Original config entry and provenance. */
|
||||
readonly origin: Origin
|
||||
/** Normalized declaration derived from that config entry. */
|
||||
readonly declared: Declared
|
||||
}
|
||||
158
packages/opencode/src/plug/tui.ts
Normal file
158
packages/opencode/src/plug/tui.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import type {
|
||||
TuiDispose,
|
||||
TuiPlugin,
|
||||
TuiPluginApi,
|
||||
TuiPluginInstallResult,
|
||||
TuiPluginMeta,
|
||||
TuiPluginModule,
|
||||
TuiPluginStatus,
|
||||
} from "@opencode-ai/plugin/tui"
|
||||
import type { Info as TuiConfigInfo } from "@/cli/cmd/tui/config/tui"
|
||||
import type { HostPluginApi, HostSlots } from "@/cli/cmd/tui/plugin/slots"
|
||||
import type { Origin, RuntimeSource } from "./spec"
|
||||
import type { Fx } from "./common"
|
||||
import type { ThemeEntry, TouchResult } from "./meta"
|
||||
|
||||
/**
|
||||
* Loaded TUI plugin before it is wrapped in runtime state.
|
||||
*/
|
||||
export interface PluginLoad {
|
||||
/** Inline config tuple payload forwarded to the plugin factory. */
|
||||
readonly options: Record<string, unknown> | undefined
|
||||
/** Normalized string spec. */
|
||||
readonly spec: string
|
||||
/** Resolved install target or file target. */
|
||||
readonly target: string
|
||||
/** Whether this load happened during the retry-after-dependencies pass. */
|
||||
readonly retry: boolean
|
||||
/** Whether this plugin is external or built in. */
|
||||
readonly source: RuntimeSource
|
||||
/** Stable runtime id. */
|
||||
readonly id: string
|
||||
/** Target-exclusive TUI module. */
|
||||
readonly module: TuiPluginModule
|
||||
/** Config provenance. Internal plugins still get a synthetic origin for consistency. */
|
||||
readonly origin: Origin
|
||||
/** Root used to resolve package-relative theme files. */
|
||||
readonly theme_root: string
|
||||
/** Valid theme files discovered from `oc-themes`. */
|
||||
readonly theme_files: readonly string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* One plugin-owned lifecycle scope.
|
||||
*
|
||||
* The current runtime models this manually with cleanup functions and an `AbortController`.
|
||||
*/
|
||||
export interface PluginScope {
|
||||
/** Lifecycle object exposed to the plugin API. */
|
||||
readonly lifecycle: TuiPluginApi["lifecycle"]
|
||||
/** Register a plain cleanup callback and return an unregister function. */
|
||||
readonly track: (fn: TuiDispose | undefined) => () => void
|
||||
/** Dispose the scope and run cleanup callbacks in reverse order. */
|
||||
readonly dispose: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* One plugin entry tracked by the TUI manager.
|
||||
*/
|
||||
export interface PluginEntry {
|
||||
/** Stable runtime id. */
|
||||
readonly id: string
|
||||
/** Raw load details used for diagnostics and reload decisions. */
|
||||
readonly load: PluginLoad
|
||||
/** Metadata exposed to plugin code and the UI. */
|
||||
readonly meta: TuiPluginMeta
|
||||
/** Persisted theme install records keyed by theme name. */
|
||||
readonly themes: Readonly<Record<string, ThemeEntry>>
|
||||
/** Concrete TUI plugin factory. */
|
||||
readonly plugin: TuiPlugin
|
||||
/** Current enabled state. */
|
||||
readonly enabled: boolean
|
||||
/** Live lifecycle scope when the plugin is active. */
|
||||
readonly scope: PluginScope | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of running one plugin cleanup callback.
|
||||
*/
|
||||
export type CleanupResult =
|
||||
| { readonly type: "ok" }
|
||||
| { readonly type: "error"; readonly error: unknown }
|
||||
| { readonly type: "timeout" }
|
||||
|
||||
/**
|
||||
* Stateful data owned by the TUI plugin manager.
|
||||
*/
|
||||
export interface State {
|
||||
/** Current directory the runtime was initialized for. */
|
||||
readonly directory: string
|
||||
/** Host-side API exposed to plugins. */
|
||||
readonly api: HostPluginApi
|
||||
/** Slot registry used by TUI plugins. */
|
||||
readonly slots: HostSlots
|
||||
/** Ordered plugin list used for activation, disposal, and status display. */
|
||||
readonly plugins: readonly PluginEntry[]
|
||||
/** Fast lookup by plugin id. */
|
||||
readonly plugins_by_id: ReadonlyMap<string, PluginEntry>
|
||||
/** Newly installed plugin origins waiting to be added to the live runtime. */
|
||||
readonly pending: ReadonlyMap<string, Origin>
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of tracking newly loaded external plugins in the metadata store.
|
||||
*/
|
||||
export interface MetadataBatch {
|
||||
/** Metadata touch results in the same order as the loaded plugins. */
|
||||
readonly results: readonly TouchResult[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Public service surface for the TUI plugin runtime.
|
||||
*
|
||||
* This would replace the current module-global singleton state with an explicit service.
|
||||
*/
|
||||
export interface Interface {
|
||||
/** Initialize the runtime for one working directory and config snapshot. */
|
||||
readonly init: (input: { api: HostPluginApi; config: TuiConfigInfo }) => Fx<void>
|
||||
/** Return current status rows for the TUI plugin UI. */
|
||||
readonly list: () => Fx<readonly TuiPluginStatus[]>
|
||||
/** Enable one plugin id. */
|
||||
readonly activate: (id: string) => Fx<boolean>
|
||||
/** Disable one plugin id. */
|
||||
readonly deactivate: (id: string) => Fx<boolean>
|
||||
/** Add one already-configured plugin spec to the live runtime. */
|
||||
readonly add: (spec: string) => Fx<boolean>
|
||||
/** Install a package and patch config, returning the current TUI-facing result type. */
|
||||
readonly install: (spec: string, options: { global?: boolean } | undefined) => Fx<TuiPluginInstallResult>
|
||||
/** Dispose all active plugins in reverse order. */
|
||||
readonly dispose: () => Fx<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Stateless helper signature for creating one active plugin scope.
|
||||
*/
|
||||
export type CreateScope = (input: { load: PluginLoad; id: string; disposeTimeoutMs: number }) => PluginScope
|
||||
|
||||
/**
|
||||
* Stateless helper signature for activating one plugin entry.
|
||||
*/
|
||||
export type ActivateEntry = (input: {
|
||||
state: State
|
||||
plugin: PluginEntry
|
||||
persist: boolean
|
||||
}) => Fx<boolean>
|
||||
|
||||
/**
|
||||
* Stateless helper signature for deactivating one plugin entry.
|
||||
*/
|
||||
export type DeactivateEntry = (input: {
|
||||
state: State
|
||||
plugin: PluginEntry
|
||||
persist: boolean
|
||||
}) => Fx<boolean>
|
||||
|
||||
/**
|
||||
* Stateless helper signature for syncing theme files for one plugin entry.
|
||||
*/
|
||||
export type SyncThemes = (plugin: PluginEntry) => Fx<void>
|
||||
Reference in New Issue
Block a user