@openvcs/sdk 0.2.4 → 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.
package/README.md CHANGED
@@ -63,32 +63,34 @@ The generated module template includes TypeScript and Node typings (`@types/node
63
63
  Plugin IDs entered during scaffold must not be `.`/`..` and must not contain path
64
64
  separators (`/` or `\\`).
65
65
 
66
- Generated module plugins now start with a working SDK runtime entrypoint:
66
+ Generated module plugins now export a declarative `PluginDefinition` plus an
67
+ `OnPluginStart()` hook that the SDK-owned bootstrap calls for them:
67
68
 
68
69
  ```ts
69
- import { createPluginRuntime, startPluginRuntime } from '@openvcs/sdk/runtime';
70
+ import type { PluginModuleDefinition } from '@openvcs/sdk/runtime';
70
71
 
71
- const runtime = createPluginRuntime({
72
+ export const PluginDefinition: PluginModuleDefinition = {
72
73
  plugin: {
73
74
  async 'plugin.init'(_params, context) {
74
75
  context.host.info('OpenVCS plugin started');
75
76
  return null;
76
77
  },
77
78
  },
78
- });
79
+ };
79
80
 
80
- startPluginRuntime(runtime);
81
+ export function OnPluginStart(): void {}
81
82
  ```
82
83
 
83
84
  Runtime and protocol imports are exposed as npm subpaths:
84
85
 
85
86
  ```ts
86
- import { createPluginRuntime, pluginError } from '@openvcs/sdk/runtime';
87
+ import { pluginError } from '@openvcs/sdk/runtime';
87
88
  import type { PluginDelegates, VcsDelegates } from '@openvcs/sdk/types';
88
89
  ```
89
90
 
90
91
  The runtime handles stdio framing, JSON-RPC request dispatch, host notifications,
91
- default `plugin.*` handlers, and exact-method delegate registration for `vcs.*`.
92
+ default `plugin.*` handlers, exact-method delegate registration for `vcs.*`, and
93
+ the generated `bin/<module.exec>` bootstrap created by `openvcs build`.
92
94
 
93
95
  Interactive theme plugin scaffold:
94
96
 
@@ -104,8 +106,10 @@ In a generated code plugin folder:
104
106
  npm run build
105
107
  ```
106
108
 
107
- This runs `openvcs build`, which executes `scripts["build:plugin"]` and verifies
108
- that `bin/<module.exec>` now exists.
109
+ This runs `openvcs build`, which executes `scripts["build:plugin"]`, expects your
110
+ compiled plugin author module at `bin/plugin.js`, and then generates the SDK-owned
111
+ `bin/<module.exec>` bootstrap that imports `./plugin.js`, applies `PluginDefinition`,
112
+ invokes `OnPluginStart()`, and starts the runtime.
109
113
 
110
114
  Theme-only plugins can also run `npm run build`; the command exits successfully
111
115
  without producing `bin/` output.
@@ -135,6 +139,9 @@ Generated code plugin scripts use this split by default:
135
139
  }
136
140
  ```
137
141
 
142
+ For code plugins, reserve `bin/plugin.js` for the compiled author module and point
143
+ `module.exec` at a different bootstrap filename such as `openvcs-plugin.js`.
144
+
138
145
  `.ovcsp` is a gzip-compressed tar archive (`tar.gz`) that contains a top-level
139
146
  `<plugin-id>/` directory with `openvcs.plugin.json` and plugin runtime assets.
140
147
 
package/lib/build.d.ts CHANGED
@@ -20,6 +20,14 @@ export declare function parseBuildArgs(args: string[]): BuildArgs;
20
20
  export declare function readManifest(pluginDir: string): ManifestInfo;
21
21
  /** Verifies that a declared module entry resolves to a real file under `bin/`. */
22
22
  export declare function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined): void;
23
+ /** Returns the compiled plugin module path imported by the generated bootstrap. */
24
+ export declare function authoredPluginModulePath(pluginDir: string): string;
25
+ /** Ensures the plugin's authored module and generated bootstrap paths are compatible. */
26
+ export declare function validateGeneratedBootstrapTargets(pluginDir: string, moduleExec: string | undefined): void;
27
+ /** Renders the generated Node bootstrap that owns runtime startup. */
28
+ export declare function renderGeneratedBootstrap(pluginModuleImportPath: string, isEsm: boolean): string;
29
+ /** Writes the generated SDK-owned module entrypoint under `bin/<module.exec>`. */
30
+ export declare function generateModuleBootstrap(pluginDir: string, moduleExec: string | undefined): void;
23
31
  /** Returns whether the plugin repository has a `package.json`. */
24
32
  export declare function hasPackageJson(pluginDir: string): boolean;
25
33
  /** Runs a command in the given directory with optional verbose logging. */
package/lib/build.js CHANGED
@@ -7,6 +7,10 @@ exports.buildUsage = buildUsage;
7
7
  exports.parseBuildArgs = parseBuildArgs;
8
8
  exports.readManifest = readManifest;
9
9
  exports.validateDeclaredModuleExec = validateDeclaredModuleExec;
10
+ exports.authoredPluginModulePath = authoredPluginModulePath;
11
+ exports.validateGeneratedBootstrapTargets = validateGeneratedBootstrapTargets;
12
+ exports.renderGeneratedBootstrap = renderGeneratedBootstrap;
13
+ exports.generateModuleBootstrap = generateModuleBootstrap;
10
14
  exports.hasPackageJson = hasPackageJson;
11
15
  exports.runCommand = runCommand;
12
16
  exports.buildPluginAssets = buildPluginAssets;
@@ -14,6 +18,7 @@ const fs = require("node:fs");
14
18
  const path = require("node:path");
15
19
  const node_child_process_1 = require("node:child_process");
16
20
  const fs_utils_1 = require("./fs-utils");
21
+ const AUTHORED_PLUGIN_MODULE_BASENAME = "plugin.js";
17
22
  /** Returns the npm executable name for the current platform. */
18
23
  function npmExecutable() {
19
24
  return process.platform === "win32" ? "npm.cmd" : "npm";
@@ -109,6 +114,13 @@ function validateDeclaredModuleExec(pluginDir, moduleExec) {
109
114
  if (!moduleExec) {
110
115
  return;
111
116
  }
117
+ const targetPath = resolveDeclaredModuleExecPath(pluginDir, moduleExec);
118
+ if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
119
+ throw new Error(`module entrypoint not found at ${targetPath}`);
120
+ }
121
+ }
122
+ /** Resolves `module.exec` to an absolute path under `bin/`, rejecting invalid targets. */
123
+ function resolveDeclaredModuleExecPath(pluginDir, moduleExec) {
112
124
  const normalizedExec = moduleExec.trim();
113
125
  const lowered = normalizedExec.toLowerCase();
114
126
  if (!lowered.endsWith(".js") && !lowered.endsWith(".mjs") && !lowered.endsWith(".cjs")) {
@@ -122,9 +134,75 @@ function validateDeclaredModuleExec(pluginDir, moduleExec) {
122
134
  if (!(0, fs_utils_1.isPathInside)(binDir, targetPath) || targetPath === binDir) {
123
135
  throw new Error(`manifest module.exec must point to a file under bin/: ${moduleExec}`);
124
136
  }
125
- if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
126
- throw new Error(`module entrypoint not found at ${targetPath}`);
137
+ return targetPath;
138
+ }
139
+ /** Returns the compiled plugin module path imported by the generated bootstrap. */
140
+ function authoredPluginModulePath(pluginDir) {
141
+ return path.resolve(pluginDir, "bin", AUTHORED_PLUGIN_MODULE_BASENAME);
142
+ }
143
+ /** Ensures the plugin's authored module and generated bootstrap paths are compatible. */
144
+ function validateGeneratedBootstrapTargets(pluginDir, moduleExec) {
145
+ if (!moduleExec) {
146
+ return;
147
+ }
148
+ resolveDeclaredModuleExecPath(pluginDir, moduleExec);
149
+ const normalizedExec = moduleExec.trim().toLowerCase();
150
+ if (normalizedExec === AUTHORED_PLUGIN_MODULE_BASENAME.toLowerCase()) {
151
+ throw new Error(`manifest module.exec must not be ${AUTHORED_PLUGIN_MODULE_BASENAME}; SDK reserves bin/${AUTHORED_PLUGIN_MODULE_BASENAME} for the compiled OnPluginStart module`);
152
+ }
153
+ const authoredModulePath = authoredPluginModulePath(pluginDir);
154
+ if (!fs.existsSync(authoredModulePath) || !fs.lstatSync(authoredModulePath).isFile()) {
155
+ throw new Error(`compiled plugin module not found at ${authoredModulePath}; build:plugin must emit bin/${AUTHORED_PLUGIN_MODULE_BASENAME}`);
156
+ }
157
+ }
158
+ /** Returns a normalized import specifier from one bin file to another. */
159
+ function relativeBinImport(fromExecPath, toModulePath) {
160
+ const relativePath = path.relative(path.dirname(fromExecPath), toModulePath).replace(/\\/g, "/");
161
+ return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
162
+ }
163
+ /** Validates that a module import path contains only safe characters for code generation. */
164
+ function validateModuleImportPath(importPath) {
165
+ if (!/^[./a-zA-Z0-9_-]+$/.test(importPath)) {
166
+ throw new Error(`unsafe module import path: ${importPath}; path must contain only alphanumeric characters, dots, slashes, hyphens, and underscores`);
167
+ }
168
+ }
169
+ /** Renders the generated Node bootstrap that owns runtime startup. */
170
+ function renderGeneratedBootstrap(pluginModuleImportPath, isEsm) {
171
+ validateModuleImportPath(pluginModuleImportPath);
172
+ if (isEsm) {
173
+ return `#!/usr/bin/env node\n// Copyright © 2025-2026 OpenVCS Contributors\n// SPDX-License-Identifier: GPL-3.0-or-later\n\nimport { bootstrapPluginModule } from '@openvcs/sdk/runtime';\n\nawait bootstrapPluginModule({\n importPluginModule: async () => import('${pluginModuleImportPath}'),\n modulePath: '${pluginModuleImportPath}',\n});\n`;
174
+ }
175
+ return `#!/usr/bin/env node\n// Copyright © 2025-2026 OpenVCS Contributors\n// SPDX-License-Identifier: GPL-3.0-or-later\n\n(async () => {\n const { bootstrapPluginModule } = require('@openvcs/sdk/runtime');\n await bootstrapPluginModule({\n importPluginModule: async () => require('${pluginModuleImportPath}'),\n modulePath: '${pluginModuleImportPath}',\n });\n})();\n`;
176
+ }
177
+ /** Writes the generated SDK-owned module entrypoint under `bin/<module.exec>`. */
178
+ function generateModuleBootstrap(pluginDir, moduleExec) {
179
+ if (!moduleExec) {
180
+ console.debug(`generateModuleBootstrap: no module.exec defined, skipping bootstrap generation for ${pluginDir}`);
181
+ return;
182
+ }
183
+ validateGeneratedBootstrapTargets(pluginDir, moduleExec);
184
+ const execPath = resolveDeclaredModuleExecPath(pluginDir, moduleExec);
185
+ const pluginModulePath = authoredPluginModulePath(pluginDir);
186
+ const pluginModuleImportPath = relativeBinImport(execPath, pluginModulePath);
187
+ const isEsm = detectEsmMode(pluginDir, moduleExec);
188
+ fs.mkdirSync(path.dirname(execPath), { recursive: true });
189
+ fs.writeFileSync(execPath, renderGeneratedBootstrap(pluginModuleImportPath, isEsm), "utf8");
190
+ }
191
+ /** Detects whether the plugin runs in ESM mode based on package.json or file extension. */
192
+ function detectEsmMode(pluginDir, moduleExec) {
193
+ const packageJsonPath = path.join(pluginDir, "package.json");
194
+ if (fs.existsSync(packageJsonPath) && fs.lstatSync(packageJsonPath).isFile()) {
195
+ try {
196
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
197
+ if (packageJson.type === "module") {
198
+ return true;
199
+ }
200
+ }
201
+ catch {
202
+ // Ignore JSON parse errors
203
+ }
127
204
  }
205
+ return moduleExec.trim().endsWith(".mjs");
128
206
  }
129
207
  /** Returns whether the plugin repository has a `package.json`. */
130
208
  function hasPackageJson(pluginDir) {
@@ -183,6 +261,7 @@ function buildPluginAssets(parsedArgs) {
183
261
  throw new Error(`code plugins must define scripts[\"build:plugin\"] in ${path.join(parsedArgs.pluginDir, "package.json")}`);
184
262
  }
185
263
  runCommand(npmExecutable(), ["run", "build:plugin"], parsedArgs.pluginDir, parsedArgs.verbose);
264
+ generateModuleBootstrap(parsedArgs.pluginDir, manifest.moduleExec);
186
265
  validateDeclaredModuleExec(parsedArgs.pluginDir, manifest.moduleExec);
187
266
  return manifest;
188
267
  }
package/lib/dist.js CHANGED
@@ -195,6 +195,7 @@ async function bundlePlugin(parsedArgs) {
195
195
  if (!moduleExec && !hasThemes && !entry) {
196
196
  throw new Error("manifest has no module.exec, entry, or themes/");
197
197
  }
198
+ (0, build_1.validateGeneratedBootstrapTargets)(pluginDir, moduleExec);
198
199
  (0, build_1.validateDeclaredModuleExec)(pluginDir, moduleExec);
199
200
  (0, fs_utils_1.ensureDirectory)(outDir);
200
201
  const stagingRoot = uniqueStagingDir(outDir);
package/lib/init.js CHANGED
@@ -180,7 +180,7 @@ function writeCommonFiles(answers) {
180
180
  name: answers.pluginName,
181
181
  version: answers.pluginVersion,
182
182
  default_enabled: answers.defaultEnabled,
183
- ...(answers.kind === "module" ? { module: { exec: "plugin.js" } } : {}),
183
+ ...(answers.kind === "module" ? { module: { exec: "openvcs-plugin.js" } } : {}),
184
184
  });
185
185
  writeText(path.join(answers.targetDir, ".gitignore"), "node_modules/\ndist/\n");
186
186
  }
@@ -218,7 +218,7 @@ function writeModuleTemplate(answers) {
218
218
  },
219
219
  include: ["src/**/*.ts"],
220
220
  });
221
- writeText(path.join(answers.targetDir, "src", "plugin.ts"), "// Copyright © 2025-2026 OpenVCS Contributors\n// SPDX-License-Identifier: GPL-3.0-or-later\n\nimport { createPluginRuntime, startPluginRuntime } from '@openvcs/sdk/runtime';\n\nconst runtime = createPluginRuntime({\n plugin: {\n async 'plugin.init'(_params, context) {\n context.host.info('OpenVCS plugin started');\n return null;\n },\n },\n});\n\nstartPluginRuntime(runtime);\n");
221
+ writeText(path.join(answers.targetDir, "src", "plugin.ts"), "// 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");
222
222
  }
223
223
  function writeThemeTemplate(answers) {
224
224
  writeCommonFiles(answers);
@@ -0,0 +1,3 @@
1
+ import type { CreatePluginRuntimeOptions, PluginRuntime } from './contracts';
2
+ /** Creates a reusable stdio JSON-RPC runtime for OpenVCS Node plugins. */
3
+ export declare function createPluginRuntime(options?: CreatePluginRuntimeOptions): PluginRuntime;
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ // Copyright © 2025-2026 OpenVCS Contributors
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.createPluginRuntime = createPluginRuntime;
6
+ const dispatcher_1 = require("./dispatcher");
7
+ const host_1 = require("./host");
8
+ const transport_1 = require("./transport");
9
+ /** Creates a reusable stdio JSON-RPC runtime for OpenVCS Node plugins. */
10
+ function createPluginRuntime(options = {}) {
11
+ let buffer = Buffer.alloc(0);
12
+ let processing = Promise.resolve();
13
+ let started = false;
14
+ let currentTransport = null;
15
+ let chunkLock = Promise.resolve();
16
+ const runtime = {
17
+ start(transport = defaultTransport()) {
18
+ if (started) {
19
+ return;
20
+ }
21
+ started = true;
22
+ currentTransport = transport;
23
+ transport.stdin.on('data', (chunk) => {
24
+ runtime.consumeChunk(chunk);
25
+ });
26
+ transport.stdin.on('error', () => {
27
+ process.exit(1);
28
+ });
29
+ options.onStart?.();
30
+ },
31
+ stop() {
32
+ if (!started) {
33
+ return;
34
+ }
35
+ started = false;
36
+ processing = processing
37
+ .catch(async (error) => {
38
+ const errorMessage = error instanceof Error ? error.message : String(error);
39
+ console.error(`[runtime] shutdown error: ${errorMessage}`);
40
+ const err = error instanceof Error ? error : new Error(errorMessage);
41
+ await options.onShutdown?.(err);
42
+ })
43
+ .then(async () => {
44
+ await options.onShutdown?.();
45
+ });
46
+ currentTransport = null;
47
+ },
48
+ consumeChunk(chunk) {
49
+ if (!started) {
50
+ return;
51
+ }
52
+ const lock = chunkLock.then(() => {
53
+ buffer = Buffer.concat([buffer, normalizeChunk(chunk)]);
54
+ const parsed = (0, transport_1.parseFramedMessages)(buffer);
55
+ buffer = parsed.remainder;
56
+ for (const request of parsed.messages) {
57
+ processing = processing
58
+ .then(async () => {
59
+ await runtime.dispatchRequest(request);
60
+ })
61
+ .catch((error) => {
62
+ const message = error instanceof Error ? error.message : String(error || 'unknown plugin processing error');
63
+ const host = createRuntimeHost(currentTransport, options.logTarget);
64
+ host.error(message);
65
+ });
66
+ }
67
+ });
68
+ chunkLock = lock;
69
+ },
70
+ async dispatchRequest(request) {
71
+ const id = request.id;
72
+ const host = createRuntimeHost(currentTransport, options.logTarget);
73
+ const method = asTrimmedString(request.method);
74
+ const validationErrors = [];
75
+ if (!method)
76
+ validationErrors.push('missing method');
77
+ if (typeof id !== 'number' && typeof id !== 'string')
78
+ validationErrors.push(`invalid id type: ${typeof id}`);
79
+ if (validationErrors.length > 0) {
80
+ host.error(`invalid request: ${validationErrors.join(', ')}`);
81
+ return;
82
+ }
83
+ const dispatcher = (0, dispatcher_1.createRuntimeDispatcher)(options, host, {
84
+ async sendResult(requestId, result) {
85
+ await sendMessage(currentTransport, {
86
+ jsonrpc: '2.0',
87
+ id: requestId,
88
+ result,
89
+ });
90
+ },
91
+ async sendError(requestId, code, message, data) {
92
+ await sendMessage(currentTransport, {
93
+ jsonrpc: '2.0',
94
+ id: requestId,
95
+ error: {
96
+ code,
97
+ message,
98
+ ...(data == null ? {} : { data }),
99
+ },
100
+ });
101
+ },
102
+ });
103
+ const methodName = method;
104
+ const params = asRecord(request.params) ?? {};
105
+ const requestId = id;
106
+ await dispatcher(requestId, methodName, params);
107
+ },
108
+ };
109
+ return runtime;
110
+ }
111
+ /** Returns the default stdio transport used by the runtime. */
112
+ function defaultTransport() {
113
+ return {
114
+ stdin: process.stdin,
115
+ stdout: process.stdout,
116
+ };
117
+ }
118
+ /** Sends one JSON-RPC response or notification through the active transport. */
119
+ async function sendMessage(transport, value) {
120
+ await (0, transport_1.writeFramedMessage)((transport ?? defaultTransport()).stdout, value);
121
+ }
122
+ /** Builds the host helper for the currently active transport. */
123
+ function createRuntimeHost(transport, logTarget) {
124
+ return (0, host_1.createHost)(async (method, params) => {
125
+ await sendMessage(transport, {
126
+ jsonrpc: '2.0',
127
+ method,
128
+ params,
129
+ });
130
+ }, { logTarget });
131
+ }
132
+ /** Type guard that returns true if the value is a valid RequestParams object. */
133
+ function isRequestParams(value) {
134
+ if (value == null || typeof value !== 'object' || Array.isArray(value)) {
135
+ return false;
136
+ }
137
+ return true;
138
+ }
139
+ /** Coerces unknown request method values into trimmed strings. Returns null for non-strings. */
140
+ function asTrimmedString(value) {
141
+ return typeof value === 'string' ? value.trim() : null;
142
+ }
143
+ /** Coerces unknown params to a Record, returning null for invalid input. */
144
+ function asRecord(value) {
145
+ if (isRequestParams(value)) {
146
+ return value;
147
+ }
148
+ return null;
149
+ }
150
+ /** Normalizes incoming data chunks to UTF-8 buffers. */
151
+ function normalizeChunk(chunk) {
152
+ return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, 'utf8');
153
+ }
@@ -1,9 +1,10 @@
1
- import type { CreatePluginRuntimeOptions, PluginRuntime, PluginRuntimeTransport } from './contracts';
1
+ import type { PluginRuntime, PluginRuntimeTransport } from './contracts';
2
2
  export type { CreatePluginRuntimeOptions, PluginRuntime, PluginRuntimeContext, PluginRuntimeTransport, } from './contracts';
3
- export { createDefaultPluginDelegates } from './dispatcher';
3
+ export type { BootstrapPluginModuleOptions, OnPluginStartHandler, PluginBootstrapModule, PluginModuleDefinition, } from './registration';
4
+ export { createDefaultPluginDelegates, createRuntimeDispatcher } from './dispatcher';
4
5
  export { isPluginFailure, pluginError } from './errors';
6
+ export { createPluginRuntime } from './factory';
5
7
  export { createHost } from './host';
6
- /** Creates a reusable stdio JSON-RPC runtime for OpenVCS Node plugins. */
7
- export declare function createPluginRuntime(options?: CreatePluginRuntimeOptions): PluginRuntime;
8
+ export { bootstrapPluginModule, createRegisteredPluginRuntime, } from './registration';
8
9
  /** Starts a previously created plugin runtime on process stdio. */
9
10
  export declare function startPluginRuntime(runtime: PluginRuntime, transport?: PluginRuntimeTransport): void;
@@ -2,165 +2,22 @@
2
2
  // Copyright © 2025-2026 OpenVCS Contributors
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.createHost = exports.pluginError = exports.isPluginFailure = exports.createDefaultPluginDelegates = void 0;
6
- exports.createPluginRuntime = createPluginRuntime;
5
+ exports.createRegisteredPluginRuntime = exports.bootstrapPluginModule = exports.createHost = exports.createPluginRuntime = exports.pluginError = exports.isPluginFailure = exports.createRuntimeDispatcher = exports.createDefaultPluginDelegates = void 0;
7
6
  exports.startPluginRuntime = startPluginRuntime;
8
- const dispatcher_1 = require("./dispatcher");
9
- const host_1 = require("./host");
10
- const transport_1 = require("./transport");
11
- var dispatcher_2 = require("./dispatcher");
12
- Object.defineProperty(exports, "createDefaultPluginDelegates", { enumerable: true, get: function () { return dispatcher_2.createDefaultPluginDelegates; } });
7
+ var dispatcher_1 = require("./dispatcher");
8
+ Object.defineProperty(exports, "createDefaultPluginDelegates", { enumerable: true, get: function () { return dispatcher_1.createDefaultPluginDelegates; } });
9
+ Object.defineProperty(exports, "createRuntimeDispatcher", { enumerable: true, get: function () { return dispatcher_1.createRuntimeDispatcher; } });
13
10
  var errors_1 = require("./errors");
14
11
  Object.defineProperty(exports, "isPluginFailure", { enumerable: true, get: function () { return errors_1.isPluginFailure; } });
15
12
  Object.defineProperty(exports, "pluginError", { enumerable: true, get: function () { return errors_1.pluginError; } });
16
- var host_2 = require("./host");
17
- Object.defineProperty(exports, "createHost", { enumerable: true, get: function () { return host_2.createHost; } });
18
- /** Creates a reusable stdio JSON-RPC runtime for OpenVCS Node plugins. */
19
- function createPluginRuntime(options = {}) {
20
- let buffer = Buffer.alloc(0);
21
- let processing = Promise.resolve();
22
- let started = false;
23
- let currentTransport = null;
24
- let chunkLock = Promise.resolve();
25
- const runtime = {
26
- start(transport = defaultTransport()) {
27
- if (started) {
28
- return;
29
- }
30
- started = true;
31
- currentTransport = transport;
32
- transport.stdin.on('data', (chunk) => {
33
- runtime.consumeChunk(chunk);
34
- });
35
- transport.stdin.on('error', () => {
36
- process.exit(1);
37
- });
38
- options.onStart?.();
39
- },
40
- stop() {
41
- if (!started) {
42
- return;
43
- }
44
- started = false;
45
- processing = processing
46
- .catch(async (error) => {
47
- const errorMessage = error instanceof Error ? error.message : String(error);
48
- console.error(`[runtime] shutdown error: ${errorMessage}`);
49
- const err = error instanceof Error ? error : new Error(errorMessage);
50
- await options.onShutdown?.(err);
51
- })
52
- .then(async () => {
53
- await options.onShutdown?.();
54
- });
55
- currentTransport = null;
56
- },
57
- consumeChunk(chunk) {
58
- if (!started) {
59
- return;
60
- }
61
- const lock = chunkLock.then(() => {
62
- buffer = Buffer.concat([buffer, normalizeChunk(chunk)]);
63
- const parsed = (0, transport_1.parseFramedMessages)(buffer);
64
- buffer = parsed.remainder;
65
- for (const request of parsed.messages) {
66
- processing = processing
67
- .then(async () => {
68
- await runtime.dispatchRequest(request);
69
- })
70
- .catch((error) => {
71
- const message = error instanceof Error ? error.message : String(error || 'unknown plugin processing error');
72
- const host = createRuntimeHost(currentTransport, options.logTarget);
73
- host.error(message);
74
- });
75
- }
76
- });
77
- chunkLock = lock;
78
- },
79
- async dispatchRequest(request) {
80
- const id = request.id;
81
- const host = createRuntimeHost(currentTransport, options.logTarget);
82
- const method = asTrimmedString(request.method);
83
- const validationErrors = [];
84
- if (!method)
85
- validationErrors.push('missing method');
86
- if (typeof id !== 'number' && typeof id !== 'string')
87
- validationErrors.push(`invalid id type: ${typeof id}`);
88
- if (validationErrors.length > 0) {
89
- host.error(`invalid request: ${validationErrors.join(', ')}`);
90
- return;
91
- }
92
- const dispatcher = (0, dispatcher_1.createRuntimeDispatcher)(options, host, {
93
- async sendResult(requestId, result) {
94
- await sendMessage(currentTransport, {
95
- jsonrpc: '2.0',
96
- id: requestId,
97
- result,
98
- });
99
- },
100
- async sendError(requestId, code, message, data) {
101
- await sendMessage(currentTransport, {
102
- jsonrpc: '2.0',
103
- id: requestId,
104
- error: {
105
- code,
106
- message,
107
- ...(data == null ? {} : { data }),
108
- },
109
- });
110
- },
111
- });
112
- const methodName = method;
113
- const params = asRecord(request.params) ?? {};
114
- const requestId = id;
115
- await dispatcher(requestId, methodName, params);
116
- },
117
- };
118
- return runtime;
119
- }
13
+ var factory_1 = require("./factory");
14
+ Object.defineProperty(exports, "createPluginRuntime", { enumerable: true, get: function () { return factory_1.createPluginRuntime; } });
15
+ var host_1 = require("./host");
16
+ Object.defineProperty(exports, "createHost", { enumerable: true, get: function () { return host_1.createHost; } });
17
+ var registration_1 = require("./registration");
18
+ Object.defineProperty(exports, "bootstrapPluginModule", { enumerable: true, get: function () { return registration_1.bootstrapPluginModule; } });
19
+ Object.defineProperty(exports, "createRegisteredPluginRuntime", { enumerable: true, get: function () { return registration_1.createRegisteredPluginRuntime; } });
120
20
  /** Starts a previously created plugin runtime on process stdio. */
121
21
  function startPluginRuntime(runtime, transport) {
122
22
  runtime.start(transport);
123
23
  }
124
- /** Returns the default stdio transport used by the runtime. */
125
- function defaultTransport() {
126
- return {
127
- stdin: process.stdin,
128
- stdout: process.stdout,
129
- };
130
- }
131
- /** Sends one JSON-RPC response or notification through the active transport. */
132
- async function sendMessage(transport, value) {
133
- await (0, transport_1.writeFramedMessage)((transport ?? defaultTransport()).stdout, value);
134
- }
135
- /** Builds the host helper for the currently active transport. */
136
- function createRuntimeHost(transport, logTarget) {
137
- return (0, host_1.createHost)(async (method, params) => {
138
- await sendMessage(transport, {
139
- jsonrpc: '2.0',
140
- method,
141
- params,
142
- });
143
- }, { logTarget });
144
- }
145
- /** Type guard that returns true if the value is a valid RequestParams object. */
146
- function isRequestParams(value) {
147
- if (value == null || typeof value !== 'object' || Array.isArray(value)) {
148
- return false;
149
- }
150
- return true;
151
- }
152
- /** Coerces unknown request method values into trimmed strings. Returns null for non-strings. */
153
- function asTrimmedString(value) {
154
- return typeof value === 'string' ? value.trim() : null;
155
- }
156
- /** Coerces unknown params to a Record, returning null for invalid input. */
157
- function asRecord(value) {
158
- if (isRequestParams(value)) {
159
- return value;
160
- }
161
- return null;
162
- }
163
- /** Normalizes incoming data chunks to UTF-8 buffers. */
164
- function normalizeChunk(chunk) {
165
- return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, 'utf8');
166
- }
@@ -0,0 +1,39 @@
1
+ import type { PluginDelegates, PluginImplements, VcsDelegates } from '../types';
2
+ import type { PluginRuntime, PluginRuntimeContext, PluginRuntimeTransport } from './contracts';
3
+ /** Describes the plugin module startup hook invoked by the generated bootstrap. */
4
+ export type OnPluginStartHandler = () => void | Promise<void>;
5
+ /** Describes runtime-wide options applied by the generated bootstrap. */
6
+ export interface PluginModuleDefinition {
7
+ /** Stores optional `plugin.*` delegates contributed by the plugin module. */
8
+ plugin?: PluginDelegates<PluginRuntimeContext>;
9
+ /** Stores optional `vcs.*` delegates contributed by the plugin module. */
10
+ vcs?: VcsDelegates<PluginRuntimeContext>;
11
+ /** Stores optional capability overrides for `plugin.initialize`. */
12
+ implements?: Partial<PluginImplements>;
13
+ /** Stores the `host.log` target emitted by the runtime. */
14
+ logTarget?: string;
15
+ /** Stores the timeout in milliseconds for request handlers. */
16
+ timeout?: number;
17
+ /** Called during stop() after pending operations complete. */
18
+ onShutdown?: (error?: Error) => void | Promise<void>;
19
+ }
20
+ /** Describes one imported plugin module consumed by the generated bootstrap. */
21
+ export interface PluginBootstrapModule {
22
+ /** Stores the plugin author's declarative runtime definition. */
23
+ PluginDefinition?: PluginModuleDefinition;
24
+ /** Stores the plugin author's startup hook. */
25
+ OnPluginStart?: OnPluginStartHandler;
26
+ }
27
+ /** Describes the generated bootstrap inputs used to import and start a plugin. */
28
+ export interface BootstrapPluginModuleOptions {
29
+ /** Imports the plugin author's compiled runtime module. */
30
+ importPluginModule: () => Promise<PluginBootstrapModule>;
31
+ /** Stores the plugin module path for error messages. */
32
+ modulePath: string;
33
+ /** Overrides the transport used by the runtime loop. */
34
+ transport?: PluginRuntimeTransport;
35
+ }
36
+ /** Creates a runtime from one declarative plugin module definition. */
37
+ export declare function createRegisteredPluginRuntime(definition?: PluginModuleDefinition): PluginRuntime;
38
+ /** Imports the author module, applies its definition, runs `OnPluginStart`, and starts the runtime loop. */
39
+ export declare function bootstrapPluginModule(options: BootstrapPluginModuleOptions): Promise<PluginRuntime>;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ // Copyright © 2025-2026 OpenVCS Contributors
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.createRegisteredPluginRuntime = createRegisteredPluginRuntime;
6
+ exports.bootstrapPluginModule = bootstrapPluginModule;
7
+ const factory_1 = require("./factory");
8
+ /** Creates a runtime from one declarative plugin module definition. */
9
+ function createRegisteredPluginRuntime(definition = {}) {
10
+ const options = {
11
+ plugin: definition.plugin,
12
+ vcs: definition.vcs,
13
+ implements: definition.implements,
14
+ logTarget: definition.logTarget,
15
+ timeout: definition.timeout,
16
+ onShutdown: definition.onShutdown,
17
+ };
18
+ return (0, factory_1.createPluginRuntime)(options);
19
+ }
20
+ /** Imports the author module, applies its definition, runs `OnPluginStart`, and starts the runtime loop. */
21
+ async function bootstrapPluginModule(options) {
22
+ const pluginModule = await options.importPluginModule();
23
+ const onPluginStart = pluginModule.OnPluginStart;
24
+ if (typeof onPluginStart !== 'function') {
25
+ throw new Error(`plugin module '${options.modulePath}' must export OnPluginStart()`);
26
+ }
27
+ try {
28
+ await onPluginStart();
29
+ }
30
+ catch (error) {
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ throw new Error(`plugin startup failed: ${message}`);
33
+ }
34
+ const runtime = createRegisteredPluginRuntime(pluginModule.PluginDefinition);
35
+ runtime.start(options.transport);
36
+ return runtime;
37
+ }