@brimveyn/aimux-plugin 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/package.json +42 -0
- package/src/daemon-api.ts +156 -0
- package/src/define-plugin.ts +21 -0
- package/src/effects.ts +67 -0
- package/src/event-bus.ts +137 -0
- package/src/index.ts +82 -0
- package/src/manifest.ts +109 -0
- package/src/test-context.ts +215 -0
- package/src/types.ts +151 -0
- package/src/ui.ts +326 -0
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The manifest — `aimux-plugin.json` — is everything the host must know
|
|
2
|
+
// *before* it runs a line of plugin code: which halves exist (so it knows
|
|
3
|
+
// which process to reload), which API version the code was written against,
|
|
4
|
+
// what configuration it takes (so the settings screen can be generated), and
|
|
5
|
+
// which subprocess commands it contributes.
|
|
6
|
+
//
|
|
7
|
+
// It is therefore parsed and validated on its own, never by importing the
|
|
8
|
+
// plugin. `validateManifest` in the host reports the offending field by name.
|
|
9
|
+
|
|
10
|
+
/** API generation the plugin was written against. Bumped only on a break. */
|
|
11
|
+
export const PLUGIN_API_VERSION = 1
|
|
12
|
+
|
|
13
|
+
/** Which host process a plugin half runs in. The terminal manager loads none. */
|
|
14
|
+
export type PluginHost = 'ui' | 'daemon'
|
|
15
|
+
|
|
16
|
+
export type PluginConfigFieldType = 'string' | 'number' | 'boolean'
|
|
17
|
+
|
|
18
|
+
export interface PluginConfigField {
|
|
19
|
+
type: PluginConfigFieldType
|
|
20
|
+
/** Shown as the settings-row label; defaults to the field key. */
|
|
21
|
+
label?: string
|
|
22
|
+
description?: string
|
|
23
|
+
/** Applied when neither the registry nor `aimux.config.ts` provides a value. */
|
|
24
|
+
default?: string | number | boolean
|
|
25
|
+
/** Required fields make the plugin fail to load rather than misbehave. */
|
|
26
|
+
required?: boolean
|
|
27
|
+
/**
|
|
28
|
+
* Secrets are never echoed: not in `aimux plugin list`, not in the settings
|
|
29
|
+
* screen, not in the plugin log. Storage is still plaintext JSON — this is
|
|
30
|
+
* shoulder-surfing hygiene, not encryption.
|
|
31
|
+
*/
|
|
32
|
+
secret?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A subprocess contribution, herdr-style. Interpreted by the built-in
|
|
37
|
+
* `aimux.exec` plugin (phase 3); parsed and carried here from day one so a
|
|
38
|
+
* manifest written against the documented schema does not have to change.
|
|
39
|
+
*/
|
|
40
|
+
export interface PluginCommandSpec {
|
|
41
|
+
id: string
|
|
42
|
+
title?: string
|
|
43
|
+
/** argv, not a shell string — no quoting rules to get wrong. */
|
|
44
|
+
command: string[]
|
|
45
|
+
/** Where the command may be invoked from. */
|
|
46
|
+
contexts?: string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Where a plugin's bar widget goes when nothing has placed it yet.
|
|
51
|
+
*
|
|
52
|
+
* A proposal, not a claim: the host places it once, marks the placement as the
|
|
53
|
+
* plugin's, and never re-places it — so a user who moves it, hides it, or
|
|
54
|
+
* throws it out has the last word, and an unload withdraws only what it put
|
|
55
|
+
* there and the user left alone.
|
|
56
|
+
*/
|
|
57
|
+
export interface PluginBarContribution {
|
|
58
|
+
/** Unqualified widget id — the same one `ctx.ui.widgets.register` takes. */
|
|
59
|
+
widget: string
|
|
60
|
+
/** Default `left`. */
|
|
61
|
+
side?: 'left' | 'right'
|
|
62
|
+
/** Default `end`. */
|
|
63
|
+
position?: 'start' | 'end'
|
|
64
|
+
/** Share of the bar, relative to its neighbours. Default 50. */
|
|
65
|
+
grow?: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A keybinding a plugin asks for. Refused rather than applied when the key is
|
|
70
|
+
* already bound in `aimux.config.ts`: the file the user writes by hand outranks
|
|
71
|
+
* every plugin, here as everywhere else.
|
|
72
|
+
*/
|
|
73
|
+
export interface PluginKeymapContribution {
|
|
74
|
+
/** Mode id, e.g. `navigation`, or the plugin's own pane mode. */
|
|
75
|
+
mode: string
|
|
76
|
+
/** Key notation, `<leader>` included. */
|
|
77
|
+
key: string
|
|
78
|
+
/** Unqualified action verb — the host prefixes it with the plugin id. */
|
|
79
|
+
action: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface PluginContributions {
|
|
83
|
+
bars?: PluginBarContribution[]
|
|
84
|
+
keymaps?: PluginKeymapContribution[]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface PluginManifest {
|
|
88
|
+
/** Reverse-DNS-ish, `<vendor>.<name>`; the namespace for every registration. */
|
|
89
|
+
id: string
|
|
90
|
+
/** Human-facing name. Defaults to `id` when absent. */
|
|
91
|
+
name?: string
|
|
92
|
+
version: string
|
|
93
|
+
description?: string
|
|
94
|
+
/** Refuses to load on an older aimux. Semver, compared numerically. */
|
|
95
|
+
minAimuxVersion?: string
|
|
96
|
+
apiVersion: number
|
|
97
|
+
/** Entry files per host, relative to the plugin root. Both are optional. */
|
|
98
|
+
entries?: Partial<Record<PluginHost, string>>
|
|
99
|
+
/** argv lists run once at install/link time (typically `bun install`). */
|
|
100
|
+
build?: string[][]
|
|
101
|
+
config?: Record<string, PluginConfigField>
|
|
102
|
+
commands?: PluginCommandSpec[]
|
|
103
|
+
/**
|
|
104
|
+
* What the plugin asks the interface for: a place for its widget, a key for
|
|
105
|
+
* its action. Applied by the host when the UI half loads, withdrawn when it
|
|
106
|
+
* unloads, and outranked by anything the user has decided.
|
|
107
|
+
*/
|
|
108
|
+
contributes?: PluginContributions
|
|
109
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Disposer,
|
|
3
|
+
PluginContext,
|
|
4
|
+
PluginDefinition,
|
|
5
|
+
PluginEventListener,
|
|
6
|
+
PluginPaths,
|
|
7
|
+
} from './types'
|
|
8
|
+
|
|
9
|
+
import { EffectStack } from './effects'
|
|
10
|
+
import { PluginEventBus } from './event-bus'
|
|
11
|
+
import { PLUGIN_API_VERSION, type PluginHost, type PluginManifest } from './manifest'
|
|
12
|
+
|
|
13
|
+
export interface TestLogEntry {
|
|
14
|
+
level: 'debug' | 'info' | 'warn' | 'error'
|
|
15
|
+
message: string
|
|
16
|
+
data?: Record<string, unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TestRpcCall {
|
|
20
|
+
verb: string
|
|
21
|
+
payload: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TestContextOptions {
|
|
25
|
+
id?: string
|
|
26
|
+
host?: PluginHost
|
|
27
|
+
config?: Record<string, unknown>
|
|
28
|
+
manifest?: Partial<PluginManifest>
|
|
29
|
+
paths?: Partial<PluginPaths>
|
|
30
|
+
/** Services the plugin can read through `ctx.service(...)`. */
|
|
31
|
+
services?: Record<string, unknown>
|
|
32
|
+
/**
|
|
33
|
+
* Stands in for the other half. Return a value (or a promise) to resolve
|
|
34
|
+
* `ctx.rpc.call`; leave it out and every call rejects the way an unhandled
|
|
35
|
+
* verb does in production.
|
|
36
|
+
*/
|
|
37
|
+
onCall?: (verb: string, payload: unknown) => unknown
|
|
38
|
+
/**
|
|
39
|
+
* Attaches host services to the context, the way the real hosts do through
|
|
40
|
+
* the kernel's `extendContext`. Without it a plugin that touches `ctx.ui` or
|
|
41
|
+
* `ctx.tabs` cannot be applied here at all — which is what made
|
|
42
|
+
* `aimux plugin doctor` unable to check the plugins that need checking most.
|
|
43
|
+
*
|
|
44
|
+
* A test usually wants recording stubs; `doctor` supplies exactly that.
|
|
45
|
+
*/
|
|
46
|
+
extend?: (ctx: PluginContext) => void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TestContextHandle {
|
|
50
|
+
ctx: PluginContext
|
|
51
|
+
bus: PluginEventBus
|
|
52
|
+
/** Every `ctx.log.*` line, in order. */
|
|
53
|
+
logs: TestLogEntry[]
|
|
54
|
+
/** Every `ctx.rpc.call` the plugin made. */
|
|
55
|
+
calls: TestRpcCall[]
|
|
56
|
+
/** Every `ctx.rpc.broadcast` the plugin sent. */
|
|
57
|
+
broadcasts: TestRpcCall[]
|
|
58
|
+
/** Services the plugin published via `ctx.provide`. */
|
|
59
|
+
provided: Map<string, unknown>
|
|
60
|
+
/**
|
|
61
|
+
* Run a definition's `apply` against this context. Generic so an inline
|
|
62
|
+
* `{ apply(ctx) { … } }` infers the base context and a typed
|
|
63
|
+
* `PluginDefinition<UiPluginContext>` is accepted as written.
|
|
64
|
+
*/
|
|
65
|
+
apply: <Ctx extends PluginContext = PluginContext>(
|
|
66
|
+
definition: PluginDefinition<Ctx>
|
|
67
|
+
) => Promise<void>
|
|
68
|
+
/** Call a verb the plugin registered with `ctx.rpc.handle`. */
|
|
69
|
+
invoke: <T = unknown>(verb: string, payload?: unknown) => Promise<T>
|
|
70
|
+
/** Verbs the plugin currently handles. */
|
|
71
|
+
handledVerbs: () => string[]
|
|
72
|
+
/** How many disposers the plugin has registered and not yet released. */
|
|
73
|
+
effectCount: () => number
|
|
74
|
+
/** Unwind, exactly as an unload would. Resolves with any disposer errors. */
|
|
75
|
+
dispose: () => Promise<unknown[]>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A standalone plugin context for tests, with no aimux process behind it.
|
|
80
|
+
* The event bus and effect stack are the real implementations, so `bail`,
|
|
81
|
+
* `waterfall` and disposal ordering behave as they do at runtime; only the
|
|
82
|
+
* process-crossing parts (RPC, filesystem paths) are stubs the test drives.
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* const t = createTestContext({ config: { botToken: 'x' } })
|
|
86
|
+
* await t.apply(plugin)
|
|
87
|
+
* t.bus.emit('tab:turnComplete', { tabId: 't1' })
|
|
88
|
+
* expect(t.calls).toHaveLength(1)
|
|
89
|
+
* await t.dispose()
|
|
90
|
+
* expect(t.effectCount()).toBe(0)
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function createTestContext(options: TestContextOptions = {}): TestContextHandle {
|
|
94
|
+
const id = options.id ?? 'test.plugin'
|
|
95
|
+
const host: PluginHost = options.host ?? 'daemon'
|
|
96
|
+
const logs: TestLogEntry[] = []
|
|
97
|
+
const calls: TestRpcCall[] = []
|
|
98
|
+
const broadcasts: TestRpcCall[] = []
|
|
99
|
+
const provided = new Map<string, unknown>()
|
|
100
|
+
const handlers = new Map<string, (payload: unknown) => unknown>()
|
|
101
|
+
const services = new Map<string, unknown>(Object.entries(options.services ?? {}))
|
|
102
|
+
|
|
103
|
+
const bus = new PluginEventBus({
|
|
104
|
+
onError: (error, context) => {
|
|
105
|
+
logs.push({
|
|
106
|
+
data: { error: String(error), event: context.event },
|
|
107
|
+
level: 'error',
|
|
108
|
+
message: 'listener failed',
|
|
109
|
+
})
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
const effects = new EffectStack()
|
|
113
|
+
|
|
114
|
+
const record =
|
|
115
|
+
(level: TestLogEntry['level']) =>
|
|
116
|
+
(message: string, data?: Record<string, unknown>): void => {
|
|
117
|
+
logs.push(data === undefined ? { level, message } : { data, level, message })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const manifest: PluginManifest = {
|
|
121
|
+
apiVersion: PLUGIN_API_VERSION,
|
|
122
|
+
id,
|
|
123
|
+
name: id,
|
|
124
|
+
version: '0.0.0',
|
|
125
|
+
...options.manifest,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const paths: PluginPaths = {
|
|
129
|
+
config: `/tmp/aimux-test/${id}/config`,
|
|
130
|
+
log: `/tmp/aimux-test/${id}/state/plugin.log`,
|
|
131
|
+
root: `/tmp/aimux-test/${id}`,
|
|
132
|
+
state: `/tmp/aimux-test/${id}/state`,
|
|
133
|
+
...options.paths,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ctx: PluginContext = {
|
|
137
|
+
bail: async (event, payload) => bus.bail(event, payload),
|
|
138
|
+
config: options.config ?? {},
|
|
139
|
+
effect: (setup) => {
|
|
140
|
+
void effects.run(setup)
|
|
141
|
+
},
|
|
142
|
+
emit: (event, payload) => {
|
|
143
|
+
bus.emit(event, payload)
|
|
144
|
+
},
|
|
145
|
+
host,
|
|
146
|
+
id,
|
|
147
|
+
log: {
|
|
148
|
+
debug: record('debug'),
|
|
149
|
+
error: record('error'),
|
|
150
|
+
info: record('info'),
|
|
151
|
+
warn: record('warn'),
|
|
152
|
+
},
|
|
153
|
+
manifest,
|
|
154
|
+
on: <T = unknown>(event: string, listener: PluginEventListener<T>): Disposer => {
|
|
155
|
+
const off = bus.on(event, listener, id)
|
|
156
|
+
effects.add(off)
|
|
157
|
+
return off
|
|
158
|
+
},
|
|
159
|
+
parallel: async (event, payload) => bus.parallel(event, payload),
|
|
160
|
+
paths,
|
|
161
|
+
provide: (name, value) => {
|
|
162
|
+
provided.set(name, value)
|
|
163
|
+
services.set(name, value)
|
|
164
|
+
effects.add(() => {
|
|
165
|
+
provided.delete(name)
|
|
166
|
+
services.delete(name)
|
|
167
|
+
})
|
|
168
|
+
},
|
|
169
|
+
rpc: {
|
|
170
|
+
broadcast: (verb, payload) => {
|
|
171
|
+
broadcasts.push({ payload, verb })
|
|
172
|
+
},
|
|
173
|
+
call: async <T = unknown>(verb: string, payload?: unknown): Promise<T> => {
|
|
174
|
+
calls.push({ payload, verb })
|
|
175
|
+
if (!options.onCall) {
|
|
176
|
+
throw new Error(`no handler for plugin rpc verb: ${id}.${verb}`)
|
|
177
|
+
}
|
|
178
|
+
return (await options.onCall(verb, payload)) as T
|
|
179
|
+
},
|
|
180
|
+
handle: (verb, handler) => {
|
|
181
|
+
handlers.set(verb, handler)
|
|
182
|
+
const off = (): void => {
|
|
183
|
+
if (handlers.get(verb) === handler) handlers.delete(verb)
|
|
184
|
+
}
|
|
185
|
+
effects.add(off)
|
|
186
|
+
return off
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
serial: async (event, payload) => bus.serial(event, payload),
|
|
190
|
+
service: <T = unknown>(name: string): T | undefined => services.get(name) as T | undefined,
|
|
191
|
+
waterfall: async <T>(event: string, value: T) => bus.waterfall(event, value),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
options.extend?.(ctx)
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
apply: async (definition) => {
|
|
198
|
+
await (definition as PluginDefinition).apply(ctx)
|
|
199
|
+
},
|
|
200
|
+
broadcasts,
|
|
201
|
+
bus,
|
|
202
|
+
calls,
|
|
203
|
+
ctx,
|
|
204
|
+
dispose: async () => effects.dispose(),
|
|
205
|
+
effectCount: () => effects.size,
|
|
206
|
+
handledVerbs: () => [...handlers.keys()],
|
|
207
|
+
invoke: async <T = unknown>(verb: string, payload?: unknown): Promise<T> => {
|
|
208
|
+
const handler = handlers.get(verb)
|
|
209
|
+
if (!handler) throw new Error(`no handler for plugin rpc verb: ${id}.${verb}`)
|
|
210
|
+
return (await handler(payload)) as T
|
|
211
|
+
},
|
|
212
|
+
logs,
|
|
213
|
+
provided,
|
|
214
|
+
}
|
|
215
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PluginAssistantsApi,
|
|
3
|
+
PluginCliApi,
|
|
4
|
+
PluginHooksApi,
|
|
5
|
+
PluginMetricsApi,
|
|
6
|
+
PluginProjectsApi,
|
|
7
|
+
PluginTabsApi,
|
|
8
|
+
PluginWorkspacesApi,
|
|
9
|
+
} from './daemon-api'
|
|
10
|
+
import type { PluginHost, PluginManifest } from './manifest'
|
|
11
|
+
import type { PluginActionsApi, PluginStoreApi, PluginUiApi } from './ui'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Everything a plugin registers hands back one of these. The kernel calls
|
|
15
|
+
* every disposer a fiber collected before it re-imports the module, which is
|
|
16
|
+
* what makes hot reload safe by construction rather than by discipline.
|
|
17
|
+
*/
|
|
18
|
+
export type Disposer = () => void | Promise<void>
|
|
19
|
+
|
|
20
|
+
export interface PluginLogger {
|
|
21
|
+
debug: (message: string, data?: Record<string, unknown>) => void
|
|
22
|
+
info: (message: string, data?: Record<string, unknown>) => void
|
|
23
|
+
warn: (message: string, data?: Record<string, unknown>) => void
|
|
24
|
+
error: (message: string, data?: Record<string, unknown>) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PluginPaths {
|
|
28
|
+
/** The plugin's own directory — read-only as far as the plugin is concerned. */
|
|
29
|
+
root: string
|
|
30
|
+
/** `<profile>/plugins-config/<id>`: files a human is expected to edit. */
|
|
31
|
+
config: string
|
|
32
|
+
/** `<profile>/plugins-state/<id>`: caches, databases, anything disposable. */
|
|
33
|
+
state: string
|
|
34
|
+
/** `<state>/plugin.log`, where `ctx.log` and load failures are written. */
|
|
35
|
+
log: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type PluginEventListener<T = never> = (payload: T) => unknown
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Five dispatch modes, after Cordis. `emit` is the one you want unless you
|
|
42
|
+
* need a result: it never awaits and never lets one listener's rejection
|
|
43
|
+
* reach another.
|
|
44
|
+
*/
|
|
45
|
+
export interface PluginEventDispatch {
|
|
46
|
+
/** Fire and forget. Rejections are logged against the emitting plugin. */
|
|
47
|
+
emit: (event: string, payload?: unknown) => void
|
|
48
|
+
/** All listeners at once; resolves with every result. */
|
|
49
|
+
parallel: (event: string, payload?: unknown) => Promise<unknown[]>
|
|
50
|
+
/** One listener at a time, in registration order. */
|
|
51
|
+
serial: (event: string, payload?: unknown) => Promise<unknown[]>
|
|
52
|
+
/** Stops at the first listener returning something other than `undefined`. */
|
|
53
|
+
bail: <T>(event: string, payload?: unknown) => Promise<T | undefined>
|
|
54
|
+
/** Threads a value through every listener; each returns the next input. */
|
|
55
|
+
waterfall: <T>(event: string, value: T) => Promise<T>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Talk to this plugin's other half. `call` crosses the process boundary
|
|
60
|
+
* (UI ⇄ daemon) over aimux's existing IPC socket; the payload is opaque to
|
|
61
|
+
* the protocol, which validates the envelope once and never the contents.
|
|
62
|
+
*
|
|
63
|
+
* A plugin with only one half still gets `rpc` — `call` simply rejects with
|
|
64
|
+
* "no handler", which is also what an unhandled verb does.
|
|
65
|
+
*/
|
|
66
|
+
export interface PluginRpc {
|
|
67
|
+
call: <T = unknown>(verb: string, payload?: unknown) => Promise<T>
|
|
68
|
+
handle: (verb: string, handler: (payload: unknown) => unknown) => Disposer
|
|
69
|
+
/** One-way fanout to every live instance of the other half. */
|
|
70
|
+
broadcast: (verb: string, payload?: unknown) => void
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The object a plugin's `apply` receives. Services beyond this base
|
|
75
|
+
* (`ctx.ui.*`, `ctx.assistants`, `ctx.tabs`…) arrive in later API phases and
|
|
76
|
+
* are always reached through `inject`, so a plugin that asks for one it does
|
|
77
|
+
* not get stays `pending` instead of crashing.
|
|
78
|
+
*/
|
|
79
|
+
export interface PluginContext {
|
|
80
|
+
readonly id: string
|
|
81
|
+
readonly manifest: PluginManifest
|
|
82
|
+
/** Which process this half is running in. */
|
|
83
|
+
readonly host: PluginHost
|
|
84
|
+
readonly log: PluginLogger
|
|
85
|
+
/** Merged and defaulted from the manifest schema, the registry and user config. */
|
|
86
|
+
readonly config: Record<string, unknown>
|
|
87
|
+
readonly paths: PluginPaths
|
|
88
|
+
readonly rpc: PluginRpc
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Register teardown. The setup function runs immediately (awaited when it
|
|
92
|
+
* returns a promise); the disposer it returns runs on unload, in reverse
|
|
93
|
+
* registration order. Anything a plugin allocates — a timer, a watcher, a
|
|
94
|
+
* socket — belongs in here.
|
|
95
|
+
*/
|
|
96
|
+
effect: (setup: () => Disposer | void | Promise<Disposer | void>) => void
|
|
97
|
+
|
|
98
|
+
/** Subscribe to a host or plugin event. Auto-disposed on unload. */
|
|
99
|
+
on: <T = unknown>(event: string, listener: PluginEventListener<T>) => Disposer
|
|
100
|
+
|
|
101
|
+
emit: PluginEventDispatch['emit']
|
|
102
|
+
parallel: PluginEventDispatch['parallel']
|
|
103
|
+
serial: PluginEventDispatch['serial']
|
|
104
|
+
bail: PluginEventDispatch['bail']
|
|
105
|
+
waterfall: PluginEventDispatch['waterfall']
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Publish a service other plugins may `inject`. Withdrawn automatically on
|
|
109
|
+
* unload, which unloads whoever injected it.
|
|
110
|
+
*/
|
|
111
|
+
provide: (name: string, value: unknown) => void
|
|
112
|
+
/** Read a service. Prefer `inject` — it makes the dependency load-ordered. */
|
|
113
|
+
service: <T = unknown>(name: string) => T | undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The UI half's context. `ui`, `actions` and `store` are attached by the UI
|
|
118
|
+
* host; a plugin declaring `entries.ui` always receives them.
|
|
119
|
+
*/
|
|
120
|
+
export interface UiPluginContext<Slice = unknown> extends PluginContext {
|
|
121
|
+
readonly host: 'ui'
|
|
122
|
+
readonly ui: PluginUiApi
|
|
123
|
+
readonly actions: PluginActionsApi
|
|
124
|
+
readonly store: PluginStoreApi<Slice>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The daemon half's context. Attached by the daemon host; a plugin declaring
|
|
129
|
+
* `entries.daemon` always receives them.
|
|
130
|
+
*/
|
|
131
|
+
export interface DaemonPluginContext extends PluginContext {
|
|
132
|
+
readonly host: 'daemon'
|
|
133
|
+
readonly tabs: PluginTabsApi
|
|
134
|
+
readonly projects: PluginProjectsApi
|
|
135
|
+
readonly workspaces: PluginWorkspacesApi
|
|
136
|
+
readonly assistants: PluginAssistantsApi
|
|
137
|
+
readonly hooks: PluginHooksApi
|
|
138
|
+
readonly cli: PluginCliApi
|
|
139
|
+
readonly metrics: PluginMetricsApi
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface PluginDefinition<Ctx extends PluginContext = PluginContext> {
|
|
143
|
+
/** Diagnostic label. Defaults to the manifest id plus the half. */
|
|
144
|
+
name?: string
|
|
145
|
+
/**
|
|
146
|
+
* Services this half needs. The fiber stays `pending` until every one is
|
|
147
|
+
* provided, then applies; if one is later withdrawn the fiber unloads.
|
|
148
|
+
*/
|
|
149
|
+
inject?: readonly string[]
|
|
150
|
+
apply: (ctx: Ctx) => void | Promise<void>
|
|
151
|
+
}
|