@openvcs/sdk 0.2.3 → 0.2.5

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.
Files changed (54) hide show
  1. package/README.md +39 -3
  2. package/lib/build.d.ts +8 -0
  3. package/lib/build.js +81 -2
  4. package/lib/dist.js +1 -0
  5. package/lib/init.d.ts +2 -0
  6. package/lib/init.js +7 -4
  7. package/lib/runtime/contracts.d.ts +45 -0
  8. package/lib/runtime/contracts.js +4 -0
  9. package/lib/runtime/dispatcher.d.ts +16 -0
  10. package/lib/runtime/dispatcher.js +133 -0
  11. package/lib/runtime/errors.d.ts +5 -0
  12. package/lib/runtime/errors.js +26 -0
  13. package/lib/runtime/factory.d.ts +3 -0
  14. package/lib/runtime/factory.js +153 -0
  15. package/lib/runtime/host.d.ts +10 -0
  16. package/lib/runtime/host.js +48 -0
  17. package/lib/runtime/index.d.ts +10 -0
  18. package/lib/runtime/index.js +23 -0
  19. package/lib/runtime/registration.d.ts +39 -0
  20. package/lib/runtime/registration.js +37 -0
  21. package/lib/runtime/transport.d.ts +14 -0
  22. package/lib/runtime/transport.js +72 -0
  23. package/lib/types/host.d.ts +57 -0
  24. package/lib/types/host.js +4 -0
  25. package/lib/types/index.d.ts +4 -0
  26. package/lib/types/index.js +22 -0
  27. package/lib/types/plugin.d.ts +56 -0
  28. package/lib/types/plugin.js +4 -0
  29. package/lib/types/protocol.d.ts +77 -0
  30. package/lib/types/protocol.js +13 -0
  31. package/lib/types/vcs.d.ts +459 -0
  32. package/lib/types/vcs.js +4 -0
  33. package/package.json +14 -1
  34. package/src/lib/build.ts +104 -2
  35. package/src/lib/dist.ts +2 -0
  36. package/src/lib/init.ts +7 -4
  37. package/src/lib/runtime/contracts.ts +52 -0
  38. package/src/lib/runtime/dispatcher.ts +185 -0
  39. package/src/lib/runtime/errors.ts +27 -0
  40. package/src/lib/runtime/factory.ts +182 -0
  41. package/src/lib/runtime/host.ts +72 -0
  42. package/src/lib/runtime/index.ts +36 -0
  43. package/src/lib/runtime/registration.ts +93 -0
  44. package/src/lib/runtime/transport.ts +93 -0
  45. package/src/lib/types/host.ts +71 -0
  46. package/src/lib/types/index.ts +7 -0
  47. package/src/lib/types/plugin.ts +110 -0
  48. package/src/lib/types/protocol.ts +97 -0
  49. package/src/lib/types/vcs.ts +579 -0
  50. package/test/build.test.js +147 -6
  51. package/test/cli.test.js +5 -3
  52. package/test/dist.test.js +27 -18
  53. package/test/init.test.js +29 -0
  54. package/test/runtime.test.js +235 -0
package/src/lib/init.ts CHANGED
@@ -223,7 +223,7 @@ function writeCommonFiles(answers: InitAnswers): void {
223
223
  name: answers.pluginName,
224
224
  version: answers.pluginVersion,
225
225
  default_enabled: answers.defaultEnabled,
226
- ...(answers.kind === "module" ? { module: { exec: "plugin.js" } } : {}),
226
+ ...(answers.kind === "module" ? { module: { exec: "openvcs-plugin.js" } } : {}),
227
227
  });
228
228
  writeText(path.join(answers.targetDir, ".gitignore"), "node_modules/\ndist/\n");
229
229
  }
@@ -241,8 +241,10 @@ function writeModuleTemplate(answers: InitAnswers): void {
241
241
  dist: "openvcs dist --plugin-dir . --out dist",
242
242
  test: "openvcs dist --plugin-dir . --out dist --no-build --no-npm-deps",
243
243
  },
244
- devDependencies: {
244
+ dependencies: {
245
245
  "@openvcs/sdk": `^${packageJson.version}`,
246
+ },
247
+ devDependencies: {
246
248
  "@types/node": "^22.0.0",
247
249
  typescript: "^5.8.2",
248
250
  },
@@ -262,7 +264,7 @@ function writeModuleTemplate(answers: InitAnswers): void {
262
264
  });
263
265
  writeText(
264
266
  path.join(answers.targetDir, "src", "plugin.ts"),
265
- "const message = \"OpenVCS plugin started\";\nprocess.stderr.write(`${message}\\n`);\n"
267
+ "// Copyright © 2025-2026 OpenVCS Contributors\n// SPDX-License-Identifier: GPL-3.0-or-later\n\nimport type { PluginModuleDefinition } from '@openvcs/sdk/runtime';\n\nexport const PluginDefinition: PluginModuleDefinition = {\n plugin: {\n async 'plugin.init'(_params, context) {\n context.host.info('OpenVCS plugin started');\n return null;\n },\n },\n};\n\n/** Runs plugin startup work before the generated runtime begins processing requests. */\nexport function OnPluginStart(): void {}\n"
266
268
  );
267
269
  }
268
270
 
@@ -277,7 +279,7 @@ function writeThemeTemplate(answers: InitAnswers): void {
277
279
  dist: "openvcs dist --plugin-dir . --out dist",
278
280
  test: "openvcs dist --plugin-dir . --out dist --no-build --no-npm-deps",
279
281
  },
280
- devDependencies: {
282
+ dependencies: {
281
283
  "@openvcs/sdk": `^${packageJson.version}`,
282
284
  },
283
285
  });
@@ -363,4 +365,5 @@ export const __private = {
363
365
  defaultPluginIdFromDir,
364
366
  sanitizeIdToken,
365
367
  validatePluginId,
368
+ writeModuleTemplate,
366
369
  };
@@ -0,0 +1,52 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import type { PluginHost, PluginImplements, PluginDelegates, JsonRpcId, JsonRpcRequest, VcsDelegates } from '../types';
5
+
6
+ /** Describes the transport endpoints used by the plugin runtime loop. */
7
+ export interface PluginRuntimeTransport {
8
+ /** Stores the readable stdin-like stream receiving framed messages. */
9
+ stdin: NodeJS.ReadStream;
10
+ /** Stores the writable stdout-like stream sending framed messages. */
11
+ stdout: NodeJS.WritableStream;
12
+ }
13
+
14
+ /** Describes the context object passed to every SDK delegate handler. */
15
+ export interface PluginRuntimeContext {
16
+ /** Stores the active host notification helper. */
17
+ host: PluginHost;
18
+ /** Stores the request id currently being processed. */
19
+ requestId: JsonRpcId;
20
+ /** Stores the current host method name. */
21
+ method: string;
22
+ }
23
+
24
+ /** Describes the options accepted by `createPluginRuntime`. */
25
+ export interface CreatePluginRuntimeOptions {
26
+ /** Stores optional plugin lifecycle and settings delegates. */
27
+ plugin?: PluginDelegates<PluginRuntimeContext>;
28
+ /** Stores optional VCS method delegates. */
29
+ vcs?: VcsDelegates<PluginRuntimeContext>;
30
+ /** Stores optional capability overrides for `plugin.initialize`. */
31
+ implements?: Partial<PluginImplements>;
32
+ /** Stores the `host.log` target emitted by the runtime. */
33
+ logTarget?: string;
34
+ /** Stores the timeout in milliseconds for request handlers. */
35
+ timeout?: number;
36
+ /** Called when runtime starts and begins processing requests. */
37
+ onStart?: () => void | Promise<void>;
38
+ /** Called during stop() after pending operations complete. Called with error if shutdown due to processing error. */
39
+ onShutdown?: (error?: Error) => void | Promise<void>;
40
+ }
41
+
42
+ /** Describes one created SDK plugin runtime instance. */
43
+ export interface PluginRuntime {
44
+ /** Starts listening on stdio for framed JSON-RPC requests. */
45
+ start(transport?: PluginRuntimeTransport): void;
46
+ /** Stops the runtime and cleans up pending operations. */
47
+ stop(): void;
48
+ /** Consumes one raw stdio chunk and dispatches complete requests. */
49
+ consumeChunk(chunk: Buffer | string): void;
50
+ /** Dispatches one already-decoded JSON-RPC request. */
51
+ dispatchRequest(request: JsonRpcRequest): Promise<void>;
52
+ }
@@ -0,0 +1,185 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import {
5
+ PLUGIN_INTERNAL_ERROR_CODE,
6
+ PROTOCOL_VERSION,
7
+ PROTOCOL_VERSION_MISMATCH_CODE,
8
+ } from '../types';
9
+ import type {
10
+ JsonRpcId,
11
+ PluginDelegates,
12
+ PluginImplements,
13
+ RequestParams,
14
+ RpcMethodHandler,
15
+ VcsDelegates,
16
+ } from '../types';
17
+ import type { PluginHost } from '../types';
18
+
19
+ import type { CreatePluginRuntimeOptions, PluginRuntimeContext } from './contracts';
20
+ import { isPluginFailure, pluginError } from './errors';
21
+
22
+ /** Describes the response emitter used by the dispatcher. */
23
+ export interface DispatcherResponseWriter {
24
+ /** Emits a JSON-RPC success response. */
25
+ sendResult<TResult>(id: JsonRpcId, result: TResult): void;
26
+ /** Emits a JSON-RPC error response. */
27
+ sendError(id: JsonRpcId, code: number, message: string, data?: unknown): void;
28
+ }
29
+
30
+ /** Describes the request handler built by `createRuntimeDispatcher`. */
31
+ export type RuntimeRequestDispatcher = (
32
+ id: JsonRpcId,
33
+ method: string,
34
+ params: RequestParams,
35
+ ) => Promise<void>;
36
+
37
+ /** Creates the plugin default handlers used when a delegate is omitted. */
38
+ export function createDefaultPluginDelegates<
39
+ TContext,
40
+ >(): Required<Omit<PluginDelegates<TContext>, 'plugin.initialize'>> {
41
+ return {
42
+ async 'plugin.init'(): Promise<null> {
43
+ return null;
44
+ },
45
+ async 'plugin.deinit'(): Promise<null> {
46
+ return null;
47
+ },
48
+ async 'plugin.get_menus'(): Promise<[]> {
49
+ return [];
50
+ },
51
+ async 'plugin.handle_action'(): Promise<null> {
52
+ return null;
53
+ },
54
+ async 'plugin.settings.defaults'(): Promise<[]> {
55
+ return [];
56
+ },
57
+ async 'plugin.settings.on_load'(params: { values?: unknown[] }): Promise<unknown[]> {
58
+ return Array.isArray(params.values) ? params.values : [];
59
+ },
60
+ async 'plugin.settings.on_apply'(): Promise<null> {
61
+ return null;
62
+ },
63
+ async 'plugin.settings.on_save'(params: { values?: unknown[] }): Promise<unknown[]> {
64
+ return Array.isArray(params.values) ? params.values : [];
65
+ },
66
+ async 'plugin.settings.on_reset'(): Promise<null> {
67
+ return null;
68
+ },
69
+ };
70
+ }
71
+
72
+ /** Creates the JSON-RPC dispatcher used by the SDK plugin runtime. */
73
+ export function createRuntimeDispatcher(
74
+ options: CreatePluginRuntimeOptions,
75
+ host: PluginHost,
76
+ writer: DispatcherResponseWriter,
77
+ ): RuntimeRequestDispatcher {
78
+ const defaultPluginDelegates = createDefaultPluginDelegates<PluginRuntimeContext>();
79
+ const pluginDelegates = options.plugin ?? {};
80
+ const vcsDelegates = options.vcs ?? {};
81
+ const runtimeImplements = buildRuntimeImplements(options.implements, options.vcs);
82
+ const timeout = options.timeout;
83
+ const typedPluginDelegates = pluginDelegates as Record<
84
+ string,
85
+ RpcMethodHandler<RequestParams, unknown, PluginRuntimeContext> | undefined
86
+ >;
87
+ const typedDefaultPluginDelegates = defaultPluginDelegates as Record<
88
+ string,
89
+ RpcMethodHandler<RequestParams, unknown, PluginRuntimeContext> | undefined
90
+ >;
91
+ const typedVcsDelegates = vcsDelegates as Record<
92
+ string,
93
+ RpcMethodHandler<RequestParams, unknown, PluginRuntimeContext> | undefined
94
+ >;
95
+
96
+ return async (id: JsonRpcId, method: string, params: RequestParams): Promise<void> => {
97
+ try {
98
+ if (method === 'plugin.initialize') {
99
+ const expectedVersion = params.expected_protocol_version;
100
+ if (typeof expectedVersion === 'number' && expectedVersion !== PROTOCOL_VERSION) {
101
+ writer.sendError(id, PROTOCOL_VERSION_MISMATCH_CODE, 'protocol version mismatch', {
102
+ code: 'protocol-version-mismatch',
103
+ message: `host expects protocol ${expectedVersion}, plugin supports ${PROTOCOL_VERSION}`,
104
+ });
105
+ return;
106
+ }
107
+
108
+ const override = pluginDelegates['plugin.initialize']
109
+ ? await pluginDelegates['plugin.initialize'](params, {
110
+ host,
111
+ requestId: id,
112
+ method,
113
+ })
114
+ : {};
115
+ writer.sendResult(id, {
116
+ protocol_version: override.protocol_version ?? PROTOCOL_VERSION,
117
+ implements: {
118
+ ...runtimeImplements,
119
+ ...override.implements,
120
+ },
121
+ });
122
+ return;
123
+ }
124
+
125
+ const handler =
126
+ typedPluginDelegates[method] ??
127
+ typedDefaultPluginDelegates[method] ??
128
+ typedVcsDelegates[method];
129
+
130
+ if (!handler) {
131
+ throw pluginError('rpc-method-not-found', `method '${method}' is not implemented`);
132
+ }
133
+
134
+ let result: unknown;
135
+ if (timeout && timeout > 0) {
136
+ const controller = new AbortController();
137
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
138
+ try {
139
+ result = await handler(params, {
140
+ host,
141
+ requestId: id,
142
+ method,
143
+ });
144
+ } catch (error) {
145
+ clearTimeout(timeoutId);
146
+ if ((error as Error).name === 'AbortError') {
147
+ throw pluginError('request-timeout', `method '${method}' timed out after ${timeout}ms`);
148
+ }
149
+ throw error;
150
+ }
151
+ clearTimeout(timeoutId);
152
+ } else {
153
+ result = await handler(params, {
154
+ host,
155
+ requestId: id,
156
+ method,
157
+ });
158
+ }
159
+ writer.sendResult(id, result == null ? null : result);
160
+ } catch (error) {
161
+ if (isPluginFailure(error)) {
162
+ writer.sendError(id, error.code, error.message, error.data);
163
+ return;
164
+ }
165
+
166
+ const messageText = error instanceof Error ? error.message : String(error || 'unknown error');
167
+ host.error(messageText);
168
+ writer.sendError(id, PLUGIN_INTERNAL_ERROR_CODE, messageText, {
169
+ code: 'plugin-internal-error',
170
+ message: messageText,
171
+ });
172
+ }
173
+ };
174
+ }
175
+
176
+ /** Builds the handshake capability flags returned from `plugin.initialize`. */
177
+ function buildRuntimeImplements(
178
+ overrides: Partial<PluginImplements> | undefined,
179
+ vcsDelegates: VcsDelegates<PluginRuntimeContext> | undefined,
180
+ ): PluginImplements {
181
+ return {
182
+ plugin: overrides?.plugin ?? true,
183
+ vcs: overrides?.vcs ?? Boolean(vcsDelegates && Object.keys(vcsDelegates).length > 0),
184
+ };
185
+ }
@@ -0,0 +1,27 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import { PLUGIN_FAILURE_CODE } from '../types';
5
+ import type { PluginFailure } from '../types';
6
+
7
+ /** Builds the host-facing plugin failure payload used for operational errors. */
8
+ export function pluginError(code: string, message: string): PluginFailure {
9
+ return {
10
+ code: PLUGIN_FAILURE_CODE,
11
+ message,
12
+ data: {
13
+ code,
14
+ message,
15
+ },
16
+ };
17
+ }
18
+
19
+ /** Returns whether the supplied value is a structured plugin failure. */
20
+ export function isPluginFailure(value: unknown): value is PluginFailure {
21
+ if (value == null || typeof value !== 'object') {
22
+ return false;
23
+ }
24
+
25
+ const candidate = value as Partial<PluginFailure>;
26
+ return candidate.code === PLUGIN_FAILURE_CODE && typeof candidate.message === 'string';
27
+ }
@@ -0,0 +1,182 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import type { JsonRpcId, JsonRpcRequest, RequestParams } from '../types';
5
+
6
+ import type {
7
+ CreatePluginRuntimeOptions,
8
+ PluginRuntime,
9
+ PluginRuntimeTransport,
10
+ } from './contracts';
11
+ import { createRuntimeDispatcher } from './dispatcher';
12
+ import { createHost } from './host';
13
+ import { parseFramedMessages, writeFramedMessage } from './transport';
14
+
15
+ /** Creates a reusable stdio JSON-RPC runtime for OpenVCS Node plugins. */
16
+ export function createPluginRuntime(
17
+ options: CreatePluginRuntimeOptions = {},
18
+ ): PluginRuntime {
19
+ let buffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
20
+ let processing: Promise<void> = Promise.resolve();
21
+ let started = false;
22
+ let currentTransport: PluginRuntimeTransport | null = null;
23
+ let chunkLock: Promise<void> = Promise.resolve();
24
+
25
+ const runtime: PluginRuntime = {
26
+ start(transport: PluginRuntimeTransport = defaultTransport()): void {
27
+ if (started) {
28
+ return;
29
+ }
30
+
31
+ started = true;
32
+ currentTransport = transport;
33
+ transport.stdin.on('data', (chunk: Buffer | string) => {
34
+ runtime.consumeChunk(chunk);
35
+ });
36
+ transport.stdin.on('error', () => {
37
+ process.exit(1);
38
+ });
39
+ options.onStart?.();
40
+ },
41
+ stop(): void {
42
+ if (!started) {
43
+ return;
44
+ }
45
+ started = false;
46
+ processing = processing
47
+ .catch(async (error: unknown) => {
48
+ const errorMessage = error instanceof Error ? error.message : String(error);
49
+ console.error(`[runtime] shutdown error: ${errorMessage}`);
50
+ const err = error instanceof Error ? error : new Error(errorMessage);
51
+ await options.onShutdown?.(err);
52
+ })
53
+ .then(async () => {
54
+ await options.onShutdown?.();
55
+ });
56
+ currentTransport = null;
57
+ },
58
+ consumeChunk(chunk: Buffer | string): void {
59
+ if (!started) {
60
+ return;
61
+ }
62
+
63
+ const lock = chunkLock.then(() => {
64
+ buffer = Buffer.concat([buffer, normalizeChunk(chunk)]);
65
+ const parsed = parseFramedMessages(buffer);
66
+ buffer = parsed.remainder;
67
+
68
+ for (const request of parsed.messages) {
69
+ processing = processing
70
+ .then(async () => {
71
+ await runtime.dispatchRequest(request);
72
+ })
73
+ .catch((error: unknown) => {
74
+ const message = error instanceof Error ? error.message : String(error || 'unknown plugin processing error');
75
+ const host = createRuntimeHost(currentTransport, options.logTarget);
76
+ host.error(message);
77
+ });
78
+ }
79
+ });
80
+
81
+ chunkLock = lock;
82
+ },
83
+ async dispatchRequest(request: JsonRpcRequest): Promise<void> {
84
+ const id = request.id;
85
+ const host = createRuntimeHost(currentTransport, options.logTarget);
86
+ const method = asTrimmedString(request.method);
87
+ const validationErrors: string[] = [];
88
+ if (!method) validationErrors.push('missing method');
89
+ if (typeof id !== 'number' && typeof id !== 'string') validationErrors.push(`invalid id type: ${typeof id}`);
90
+ if (validationErrors.length > 0) {
91
+ host.error(`invalid request: ${validationErrors.join(', ')}`);
92
+ return;
93
+ }
94
+ const dispatcher = createRuntimeDispatcher(options, host, {
95
+ async sendResult<TResult>(requestId: JsonRpcId, result: TResult): Promise<void> {
96
+ await sendMessage(currentTransport, {
97
+ jsonrpc: '2.0',
98
+ id: requestId,
99
+ result,
100
+ });
101
+ },
102
+ async sendError(requestId: JsonRpcId, code: number, message: string, data?: unknown): Promise<void> {
103
+ await sendMessage(currentTransport, {
104
+ jsonrpc: '2.0',
105
+ id: requestId,
106
+ error: {
107
+ code,
108
+ message,
109
+ ...(data == null ? {} : { data }),
110
+ },
111
+ });
112
+ },
113
+ });
114
+
115
+ const methodName = method as string;
116
+ const params = asRecord(request.params) ?? {};
117
+ const requestId = id as JsonRpcId;
118
+ await dispatcher(requestId, methodName, params);
119
+ },
120
+ };
121
+
122
+ return runtime;
123
+ }
124
+
125
+ /** Returns the default stdio transport used by the runtime. */
126
+ function defaultTransport(): PluginRuntimeTransport {
127
+ return {
128
+ stdin: process.stdin,
129
+ stdout: process.stdout,
130
+ };
131
+ }
132
+
133
+ /** Sends one JSON-RPC response or notification through the active transport. */
134
+ async function sendMessage(
135
+ transport: PluginRuntimeTransport | null,
136
+ value: unknown,
137
+ ): Promise<void> {
138
+ await writeFramedMessage((transport ?? defaultTransport()).stdout, value);
139
+ }
140
+
141
+ /** Builds the host helper for the currently active transport. */
142
+ function createRuntimeHost(
143
+ transport: PluginRuntimeTransport | null,
144
+ logTarget: string | undefined,
145
+ ) {
146
+ return createHost(
147
+ async (method: string, params: unknown) => {
148
+ await sendMessage(transport, {
149
+ jsonrpc: '2.0',
150
+ method,
151
+ params,
152
+ });
153
+ },
154
+ { logTarget },
155
+ );
156
+ }
157
+
158
+ /** Type guard that returns true if the value is a valid RequestParams object. */
159
+ function isRequestParams(value: unknown): value is RequestParams {
160
+ if (value == null || typeof value !== 'object' || Array.isArray(value)) {
161
+ return false;
162
+ }
163
+ return true;
164
+ }
165
+
166
+ /** Coerces unknown request method values into trimmed strings. Returns null for non-strings. */
167
+ function asTrimmedString(value: unknown): string | null {
168
+ return typeof value === 'string' ? value.trim() : null;
169
+ }
170
+
171
+ /** Coerces unknown params to a Record, returning null for invalid input. */
172
+ function asRecord(value: unknown): Record<string, unknown> | null {
173
+ if (isRequestParams(value)) {
174
+ return value as Record<string, unknown>;
175
+ }
176
+ return null;
177
+ }
178
+
179
+ /** Normalizes incoming data chunks to UTF-8 buffers. */
180
+ function normalizeChunk(chunk: Buffer | string): Buffer {
181
+ return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, 'utf8');
182
+ }
@@ -0,0 +1,72 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import type {
5
+ HostEventEmitParams,
6
+ HostLogLevel,
7
+ HostStatusSetParams,
8
+ HostUiNotifyParams,
9
+ JsonRpcId,
10
+ PluginHost,
11
+ } from '../types';
12
+
13
+ /** Describes the low-level notification sender used by `createHost`. */
14
+ export type HostNotificationSender = (method: string, params: unknown) => void;
15
+
16
+ /** Describes the options accepted by `createHost`. */
17
+ export interface CreateHostOptions {
18
+ /** Stores the `host.log` target name to emit for diagnostic messages. */
19
+ logTarget?: string;
20
+ }
21
+
22
+ /** Creates the host notification helper used inside runtime delegates. */
23
+ export function createHost(
24
+ sendNotification: HostNotificationSender,
25
+ options: CreateHostOptions = {},
26
+ ): PluginHost {
27
+ const logTarget = options.logTarget ?? 'openvcs.plugin';
28
+
29
+ return {
30
+ log(level: HostLogLevel, message: string): void {
31
+ sendNotification('host.log', {
32
+ level,
33
+ target: logTarget,
34
+ message,
35
+ });
36
+ },
37
+ info(message: string): void {
38
+ sendNotification('host.log', {
39
+ level: 'info',
40
+ target: logTarget,
41
+ message,
42
+ });
43
+ },
44
+ error(message: string): void {
45
+ sendNotification('host.log', {
46
+ level: 'error',
47
+ target: logTarget,
48
+ message,
49
+ });
50
+ },
51
+ uiNotify(params: HostUiNotifyParams): void {
52
+ sendNotification('host.ui_notify', params);
53
+ },
54
+ statusSet(params: HostStatusSetParams): void {
55
+ sendNotification('host.status_set', params);
56
+ },
57
+ emitEvent(params: HostEventEmitParams): void {
58
+ sendNotification('host.event_emit', params);
59
+ },
60
+ emitVcsEvent(
61
+ sessionId: string,
62
+ requestId: JsonRpcId | null,
63
+ event: Record<string, unknown>,
64
+ ): void {
65
+ sendNotification('vcs.event', {
66
+ session_id: sessionId,
67
+ request_id: requestId,
68
+ event,
69
+ });
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,36 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import type {
5
+ PluginRuntime,
6
+ PluginRuntimeTransport,
7
+ } from './contracts';
8
+
9
+ export type {
10
+ CreatePluginRuntimeOptions,
11
+ PluginRuntime,
12
+ PluginRuntimeContext,
13
+ PluginRuntimeTransport,
14
+ } from './contracts';
15
+ export type {
16
+ BootstrapPluginModuleOptions,
17
+ OnPluginStartHandler,
18
+ PluginBootstrapModule,
19
+ PluginModuleDefinition,
20
+ } from './registration';
21
+ export { createDefaultPluginDelegates, createRuntimeDispatcher } from './dispatcher';
22
+ export { isPluginFailure, pluginError } from './errors';
23
+ export { createPluginRuntime } from './factory';
24
+ export { createHost } from './host';
25
+ export {
26
+ bootstrapPluginModule,
27
+ createRegisteredPluginRuntime,
28
+ } from './registration';
29
+
30
+ /** Starts a previously created plugin runtime on process stdio. */
31
+ export function startPluginRuntime(
32
+ runtime: PluginRuntime,
33
+ transport?: PluginRuntimeTransport,
34
+ ): void {
35
+ runtime.start(transport);
36
+ }
@@ -0,0 +1,93 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import type {
5
+ PluginDelegates,
6
+ PluginImplements,
7
+ VcsDelegates,
8
+ } from '../types';
9
+
10
+ import type {
11
+ CreatePluginRuntimeOptions,
12
+ PluginRuntime,
13
+ PluginRuntimeContext,
14
+ PluginRuntimeTransport,
15
+ } from './contracts';
16
+ import { createPluginRuntime } from './factory';
17
+
18
+ /** Describes the plugin module startup hook invoked by the generated bootstrap. */
19
+ export type OnPluginStartHandler = () => void | Promise<void>;
20
+
21
+ /** Describes runtime-wide options applied by the generated bootstrap. */
22
+ export interface PluginModuleDefinition {
23
+ /** Stores optional `plugin.*` delegates contributed by the plugin module. */
24
+ plugin?: PluginDelegates<PluginRuntimeContext>;
25
+ /** Stores optional `vcs.*` delegates contributed by the plugin module. */
26
+ vcs?: VcsDelegates<PluginRuntimeContext>;
27
+ /** Stores optional capability overrides for `plugin.initialize`. */
28
+ implements?: Partial<PluginImplements>;
29
+ /** Stores the `host.log` target emitted by the runtime. */
30
+ logTarget?: string;
31
+ /** Stores the timeout in milliseconds for request handlers. */
32
+ timeout?: number;
33
+ /** Called during stop() after pending operations complete. */
34
+ onShutdown?: (error?: Error) => void | Promise<void>;
35
+ }
36
+
37
+ /** Describes one imported plugin module consumed by the generated bootstrap. */
38
+ export interface PluginBootstrapModule {
39
+ /** Stores the plugin author's declarative runtime definition. */
40
+ PluginDefinition?: PluginModuleDefinition;
41
+ /** Stores the plugin author's startup hook. */
42
+ OnPluginStart?: OnPluginStartHandler;
43
+ }
44
+
45
+ /** Describes the generated bootstrap inputs used to import and start a plugin. */
46
+ export interface BootstrapPluginModuleOptions {
47
+ /** Imports the plugin author's compiled runtime module. */
48
+ importPluginModule: () => Promise<PluginBootstrapModule>;
49
+ /** Stores the plugin module path for error messages. */
50
+ modulePath: string;
51
+ /** Overrides the transport used by the runtime loop. */
52
+ transport?: PluginRuntimeTransport;
53
+ }
54
+
55
+ /** Creates a runtime from one declarative plugin module definition. */
56
+ export function createRegisteredPluginRuntime(
57
+ definition: PluginModuleDefinition = {},
58
+ ): PluginRuntime {
59
+ const options: CreatePluginRuntimeOptions = {
60
+ plugin: definition.plugin,
61
+ vcs: definition.vcs,
62
+ implements: definition.implements,
63
+ logTarget: definition.logTarget,
64
+ timeout: definition.timeout,
65
+ onShutdown: definition.onShutdown,
66
+ };
67
+ return createPluginRuntime(options);
68
+ }
69
+
70
+ /** Imports the author module, applies its definition, runs `OnPluginStart`, and starts the runtime loop. */
71
+ export async function bootstrapPluginModule(
72
+ options: BootstrapPluginModuleOptions,
73
+ ): Promise<PluginRuntime> {
74
+ const pluginModule = await options.importPluginModule();
75
+ const onPluginStart = pluginModule.OnPluginStart;
76
+
77
+ if (typeof onPluginStart !== 'function') {
78
+ throw new Error(
79
+ `plugin module '${options.modulePath}' must export OnPluginStart()`,
80
+ );
81
+ }
82
+
83
+ try {
84
+ await onPluginStart();
85
+ } catch (error) {
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ throw new Error(`plugin startup failed: ${message}`);
88
+ }
89
+
90
+ const runtime = createRegisteredPluginRuntime(pluginModule.PluginDefinition);
91
+ runtime.start(options.transport);
92
+ return runtime;
93
+ }