@openvcs/sdk 0.2.4 → 0.2.6

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,86 @@ 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
 
84
+ VCS backends can also derive from `VcsDelegateBase` and attach exact `vcs.*`
85
+ delegates during startup:
86
+
87
+ ```ts
88
+ import {
89
+ VcsDelegateBase,
90
+ type PluginRuntimeContext,
91
+ type PluginModuleDefinition,
92
+ } from '@openvcs/sdk/runtime';
93
+ import type { RequestParams, VcsCapabilities } from '@openvcs/sdk/types';
94
+
95
+ class ExampleVcsDelegates extends VcsDelegateBase<{ cwd: string }> {
96
+ override getCaps(
97
+ _params: RequestParams,
98
+ _context: PluginRuntimeContext,
99
+ ): VcsCapabilities {
100
+ return {
101
+ commits: true,
102
+ branches: true,
103
+ tags: false,
104
+ staging: true,
105
+ push_pull: true,
106
+ fast_forward: true,
107
+ };
108
+ }
109
+ }
110
+
111
+ export const PluginDefinition: PluginModuleDefinition = {};
112
+
113
+ export function OnPluginStart(): void {
114
+ const vcs = new ExampleVcsDelegates({ cwd: process.cwd() });
115
+ PluginDefinition.vcs = vcs.toDelegates();
116
+ }
117
+ ```
118
+
119
+ Define ordinary prototype methods such as `getCaps()` and `commitIndex()` on the
120
+ subclass. `toDelegates()` maps those camelCase methods to the exact host method
121
+ names like `vcs.get_caps` and `vcs.commit_index`, and only registers methods
122
+ that differ from the SDK base class. Use `override` with the full params/context
123
+ signature so TypeScript checks your subclass against the SDK contract. Base
124
+ stubs throw through an internal `never`-returning helper, which is why concrete
125
+ plugins should always implement the methods they intend to expose.
126
+
127
+ ### Compile-time override safety
128
+
129
+ The SDK enforces correct method signatures at compile time. Subclasses that
130
+ override with the wrong return type or parameter shape produce a TypeScript error.
131
+ The SDK test suite includes `test/vcs-delegate-base.types.ts` which explicitly
132
+ verifies that incompatible overrides (e.g. returning `string` instead of
133
+ `VcsCapabilities`) fail the compiler under `@ts-expect-error`. Runtime tests
134
+ in `test/vcs-delegate-base.test.js` cover behavior — not signatures.
135
+
83
136
  Runtime and protocol imports are exposed as npm subpaths:
84
137
 
85
138
  ```ts
86
- import { createPluginRuntime, pluginError } from '@openvcs/sdk/runtime';
139
+ import { VcsDelegateBase, pluginError } from '@openvcs/sdk/runtime';
87
140
  import type { PluginDelegates, VcsDelegates } from '@openvcs/sdk/types';
88
141
  ```
89
142
 
90
143
  The runtime handles stdio framing, JSON-RPC request dispatch, host notifications,
91
- default `plugin.*` handlers, and exact-method delegate registration for `vcs.*`.
144
+ default `plugin.*` handlers, exact-method delegate registration for `vcs.*`, and
145
+ the generated `bin/<module.exec>` bootstrap created by `openvcs build`.
92
146
 
93
147
  Interactive theme plugin scaffold:
94
148
 
@@ -104,8 +158,10 @@ In a generated code plugin folder:
104
158
  npm run build
105
159
  ```
106
160
 
107
- This runs `openvcs build`, which executes `scripts["build:plugin"]` and verifies
108
- that `bin/<module.exec>` now exists.
161
+ This runs `openvcs build`, which executes `scripts["build:plugin"]`, expects your
162
+ compiled plugin author module at `bin/plugin.js`, and then generates the SDK-owned
163
+ `bin/<module.exec>` bootstrap that imports `./plugin.js`, applies `PluginDefinition`,
164
+ invokes `OnPluginStart()`, and starts the runtime.
109
165
 
110
166
  Theme-only plugins can also run `npm run build`; the command exits successfully
111
167
  without producing `bin/` output.
@@ -135,6 +191,9 @@ Generated code plugin scripts use this split by default:
135
191
  }
136
192
  ```
137
193
 
194
+ For code plugins, reserve `bin/plugin.js` for the compiled author module and point
195
+ `module.exec` at a different bootstrap filename such as `openvcs-plugin.js`.
196
+
138
197
  `.ovcsp` is a gzip-compressed tar archive (`tar.gz`) that contains a top-level
139
198
  `<plugin-id>/` directory with `openvcs.plugin.json` and plugin runtime assets.
140
199
 
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,12 @@
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';
9
+ export { VcsDelegateBase } from './vcs-delegate-base';
10
+ export type { VcsDelegateAssignments } from './vcs-delegate-metadata';
8
11
  /** Starts a previously created plugin runtime on process stdio. */
9
12
  export declare function startPluginRuntime(runtime: PluginRuntime, transport?: PluginRuntimeTransport): void;
@@ -2,165 +2,24 @@
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.VcsDelegateBase = 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; } });
20
+ var vcs_delegate_base_1 = require("./vcs-delegate-base");
21
+ Object.defineProperty(exports, "VcsDelegateBase", { enumerable: true, get: function () { return vcs_delegate_base_1.VcsDelegateBase; } });
120
22
  /** Starts a previously created plugin runtime on process stdio. */
121
23
  function startPluginRuntime(runtime, transport) {
122
24
  runtime.start(transport);
123
25
  }
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
- }