@openvcs/sdk 0.2.2 → 0.2.4
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 +76 -7
- package/lib/build.d.ts +28 -0
- package/lib/build.js +188 -0
- package/lib/cli.js +21 -2
- package/lib/dist.d.ts +4 -7
- package/lib/dist.js +67 -113
- package/lib/init.d.ts +2 -0
- package/lib/init.js +13 -8
- package/lib/runtime/contracts.d.ts +45 -0
- package/lib/runtime/contracts.js +4 -0
- package/lib/runtime/dispatcher.d.ts +16 -0
- package/lib/runtime/dispatcher.js +133 -0
- package/lib/runtime/errors.d.ts +5 -0
- package/lib/runtime/errors.js +26 -0
- package/lib/runtime/host.d.ts +10 -0
- package/lib/runtime/host.js +48 -0
- package/lib/runtime/index.d.ts +9 -0
- package/lib/runtime/index.js +166 -0
- package/lib/runtime/transport.d.ts +14 -0
- package/lib/runtime/transport.js +72 -0
- package/lib/types/host.d.ts +57 -0
- package/lib/types/host.js +4 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.js +22 -0
- package/lib/types/plugin.d.ts +56 -0
- package/lib/types/plugin.js +4 -0
- package/lib/types/protocol.d.ts +77 -0
- package/lib/types/protocol.js +13 -0
- package/lib/types/vcs.d.ts +459 -0
- package/lib/types/vcs.js +4 -0
- package/package.json +16 -3
- package/src/lib/build.ts +229 -0
- package/src/lib/cli.ts +21 -2
- package/src/lib/dist.ts +76 -128
- package/src/lib/init.ts +13 -8
- package/src/lib/runtime/contracts.ts +52 -0
- package/src/lib/runtime/dispatcher.ts +185 -0
- package/src/lib/runtime/errors.ts +27 -0
- package/src/lib/runtime/host.ts +72 -0
- package/src/lib/runtime/index.ts +201 -0
- package/src/lib/runtime/transport.ts +93 -0
- package/src/lib/types/host.ts +71 -0
- package/src/lib/types/index.ts +7 -0
- package/src/lib/types/plugin.ts +110 -0
- package/src/lib/types/protocol.ts +97 -0
- package/src/lib/types/vcs.ts +579 -0
- package/test/build.test.js +95 -0
- package/test/cli.test.js +37 -0
- package/test/dist.test.js +239 -15
- package/test/init.test.js +25 -0
- package/test/runtime.test.js +118 -0
package/lib/dist.js
CHANGED
|
@@ -6,21 +6,19 @@ exports.parseDistArgs = parseDistArgs;
|
|
|
6
6
|
exports.bundlePlugin = bundlePlugin;
|
|
7
7
|
const fs = require("node:fs");
|
|
8
8
|
const path = require("node:path");
|
|
9
|
-
const node_child_process_1 = require("node:child_process");
|
|
10
9
|
const tar = require("tar");
|
|
10
|
+
const build_1 = require("./build");
|
|
11
11
|
const fs_utils_1 = require("./fs-utils");
|
|
12
12
|
const ICON_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "avif", "svg"];
|
|
13
|
-
function npmExecutable() {
|
|
14
|
-
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
15
|
-
}
|
|
16
13
|
function distUsage(commandName = "openvcs") {
|
|
17
|
-
return `${commandName} dist [args]\n\n --plugin-dir <path> Plugin repository root (contains openvcs.plugin.json)\n --out <path> Output directory (default: ./dist)\n --no-npm-deps Disable npm dependency bundling (enabled by default)\n -V, --verbose Enable verbose output\n`;
|
|
14
|
+
return `${commandName} dist [args]\n\n --plugin-dir <path> Plugin repository root (contains openvcs.plugin.json)\n --out <path> Output directory (default: ./dist)\n --no-build Skip the plugin build step before packaging\n --no-npm-deps Disable npm dependency bundling (enabled by default)\n -V, --verbose Enable verbose output\n`;
|
|
18
15
|
}
|
|
19
16
|
function parseDistArgs(args) {
|
|
20
17
|
let pluginDir = process.cwd();
|
|
21
18
|
let outDir = "dist";
|
|
22
19
|
let verbose = false;
|
|
23
20
|
let noNpmDeps = false;
|
|
21
|
+
let noBuild = false;
|
|
24
22
|
for (let index = 0; index < args.length; index += 1) {
|
|
25
23
|
const arg = args[index];
|
|
26
24
|
if (arg === "--plugin-dir") {
|
|
@@ -43,6 +41,10 @@ function parseDistArgs(args) {
|
|
|
43
41
|
noNpmDeps = true;
|
|
44
42
|
continue;
|
|
45
43
|
}
|
|
44
|
+
if (arg === "--no-build") {
|
|
45
|
+
noBuild = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
46
48
|
if (arg === "-V" || arg === "--verbose") {
|
|
47
49
|
verbose = true;
|
|
48
50
|
continue;
|
|
@@ -59,109 +61,44 @@ function parseDistArgs(args) {
|
|
|
59
61
|
outDir: path.resolve(outDir),
|
|
60
62
|
verbose,
|
|
61
63
|
noNpmDeps,
|
|
64
|
+
noBuild,
|
|
62
65
|
};
|
|
63
66
|
}
|
|
64
|
-
function
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
let manifest;
|
|
69
|
-
try {
|
|
70
|
-
manifestFd = fs.openSync(manifestPath, "r");
|
|
71
|
-
const manifestStat = fs.fstatSync(manifestFd);
|
|
72
|
-
if (!manifestStat.isFile()) {
|
|
73
|
-
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
74
|
-
}
|
|
75
|
-
manifestRaw = fs.readFileSync(manifestFd, "utf8");
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
if (error.code === "ENOENT") {
|
|
79
|
-
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
80
|
-
}
|
|
81
|
-
throw error;
|
|
82
|
-
}
|
|
83
|
-
finally {
|
|
84
|
-
if (typeof manifestFd === "number") {
|
|
85
|
-
fs.closeSync(manifestFd);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
try {
|
|
89
|
-
manifest = JSON.parse(manifestRaw);
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
93
|
-
throw new Error(`parse ${manifestPath}: ${detail}`);
|
|
94
|
-
}
|
|
95
|
-
const pluginId = typeof manifest.id === "string"
|
|
96
|
-
? manifest.id.trim()
|
|
97
|
-
: "";
|
|
98
|
-
if (!pluginId) {
|
|
99
|
-
throw new Error(`manifest ${manifestPath} is missing a string 'id'`);
|
|
100
|
-
}
|
|
101
|
-
if (pluginId === "." || pluginId === ".." || pluginId.includes("/") || pluginId.includes("\\")) {
|
|
102
|
-
throw new Error(`manifest id must not contain path separators: ${pluginId}`);
|
|
103
|
-
}
|
|
104
|
-
const moduleValue = manifest.module;
|
|
105
|
-
const moduleExec = typeof moduleValue?.exec === "string" ? moduleValue.exec.trim() : undefined;
|
|
106
|
-
return {
|
|
107
|
-
pluginId,
|
|
108
|
-
moduleExec,
|
|
109
|
-
manifestPath,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
function validateDeclaredModuleExec(pluginDir, moduleExec) {
|
|
113
|
-
if (!moduleExec) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const normalizedExec = moduleExec.trim();
|
|
117
|
-
const lowered = normalizedExec.toLowerCase();
|
|
118
|
-
if (!lowered.endsWith(".js") && !lowered.endsWith(".mjs") && !lowered.endsWith(".cjs")) {
|
|
119
|
-
throw new Error(`manifest exec must end with .js/.mjs/.cjs (Node runtime): ${moduleExec}`);
|
|
67
|
+
function validateManifestEntry(pluginDir, entry) {
|
|
68
|
+
const normalized = entry.trim();
|
|
69
|
+
if (path.isAbsolute(normalized)) {
|
|
70
|
+
throw new Error(`manifest entry must be a relative path: ${entry}`);
|
|
120
71
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const targetPath = path.resolve(binDir, normalizedExec);
|
|
126
|
-
if (!(0, fs_utils_1.isPathInside)(binDir, targetPath) || targetPath === binDir) {
|
|
127
|
-
throw new Error(`manifest module.exec must point to a file under bin/: ${moduleExec}`);
|
|
72
|
+
const targetPath = path.resolve(pluginDir, normalized);
|
|
73
|
+
const pluginDirResolved = path.resolve(pluginDir);
|
|
74
|
+
if (!(0, fs_utils_1.isPathInside)(pluginDirResolved, targetPath) || targetPath === pluginDirResolved) {
|
|
75
|
+
throw new Error(`manifest entry must point to a file under the plugin directory: ${entry}`);
|
|
128
76
|
}
|
|
129
77
|
if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
|
|
130
|
-
throw new Error(`
|
|
78
|
+
throw new Error(`manifest entry file not found: ${entry}`);
|
|
131
79
|
}
|
|
132
80
|
}
|
|
133
|
-
function
|
|
134
|
-
const
|
|
135
|
-
|
|
81
|
+
function copyEntryDirectory(pluginDir, bundleDir, entry) {
|
|
82
|
+
const normalized = entry.trim();
|
|
83
|
+
const entryDir = path.dirname(normalized);
|
|
84
|
+
const sourceDir = path.join(pluginDir, entryDir);
|
|
85
|
+
const destDir = path.join(bundleDir, entryDir);
|
|
86
|
+
(0, fs_utils_1.copyDirectoryRecursiveStrict)(sourceDir, destDir);
|
|
136
87
|
}
|
|
137
|
-
function
|
|
138
|
-
if (
|
|
139
|
-
process.stderr.write(`Running command in ${cwd}: ${program} ${args.join(" ")}\n`);
|
|
140
|
-
}
|
|
141
|
-
const result = (0, node_child_process_1.spawnSync)(program, args, {
|
|
142
|
-
cwd,
|
|
143
|
-
stdio: ["ignore", verbose ? "inherit" : "ignore", "inherit"],
|
|
144
|
-
});
|
|
145
|
-
if (result.error) {
|
|
146
|
-
throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
|
|
147
|
-
}
|
|
148
|
-
if (result.status === 0) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
throw new Error(`command failed (${program} ${args.join(" ")}), exit code ${result.status}`);
|
|
152
|
-
}
|
|
153
|
-
function ensurePackageLock(pluginDir, verbose) {
|
|
154
|
-
if (!hasPackageJson(pluginDir)) {
|
|
88
|
+
function ensurePackageLock(pluginDir, bundleDir, verbose) {
|
|
89
|
+
if (!(0, build_1.hasPackageJson)(pluginDir)) {
|
|
155
90
|
return;
|
|
156
91
|
}
|
|
157
92
|
const lockPath = path.join(pluginDir, "package-lock.json");
|
|
158
93
|
if (fs.existsSync(lockPath) && fs.lstatSync(lockPath).isFile()) {
|
|
159
94
|
return;
|
|
160
95
|
}
|
|
96
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
97
|
+
(0, fs_utils_1.copyFileStrict)(packageJsonPath, path.join(bundleDir, "package.json"));
|
|
161
98
|
if (verbose) {
|
|
162
|
-
process.stderr.write(`Generating package-lock.json in
|
|
99
|
+
process.stderr.write(`Generating package-lock.json in staging\n`);
|
|
163
100
|
}
|
|
164
|
-
runCommand(npmExecutable(), ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"],
|
|
101
|
+
(0, build_1.runCommand)((0, build_1.npmExecutable)(), ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"], bundleDir, verbose);
|
|
165
102
|
}
|
|
166
103
|
function copyNpmFilesToStaging(pluginDir, bundleDir) {
|
|
167
104
|
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
@@ -169,11 +106,14 @@ function copyNpmFilesToStaging(pluginDir, bundleDir) {
|
|
|
169
106
|
if (!fs.existsSync(packageJsonPath) || !fs.lstatSync(packageJsonPath).isFile()) {
|
|
170
107
|
throw new Error(`missing package.json at ${packageJsonPath}`);
|
|
171
108
|
}
|
|
172
|
-
if (!fs.existsSync(lockPath) || !fs.lstatSync(lockPath).isFile()) {
|
|
173
|
-
throw new Error(`missing package-lock.json at ${lockPath}`);
|
|
174
|
-
}
|
|
175
109
|
(0, fs_utils_1.copyFileStrict)(packageJsonPath, path.join(bundleDir, "package.json"));
|
|
176
|
-
|
|
110
|
+
const stagedLockPath = path.join(bundleDir, "package-lock.json");
|
|
111
|
+
if (fs.existsSync(stagedLockPath) && fs.lstatSync(stagedLockPath).isFile()) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (fs.existsSync(lockPath) && fs.lstatSync(lockPath).isFile()) {
|
|
115
|
+
(0, fs_utils_1.copyFileStrict)(lockPath, path.join(bundleDir, "package-lock.json"));
|
|
116
|
+
}
|
|
177
117
|
}
|
|
178
118
|
function rejectNativeAddonsRecursive(dirPath) {
|
|
179
119
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
@@ -197,7 +137,7 @@ function rejectNativeAddonsRecursive(dirPath) {
|
|
|
197
137
|
}
|
|
198
138
|
function installNpmDependencies(pluginDir, bundleDir, verbose) {
|
|
199
139
|
copyNpmFilesToStaging(pluginDir, bundleDir);
|
|
200
|
-
runCommand(npmExecutable(), ["ci", "--omit=dev", "--ignore-scripts", "--no-bin-links", "--no-audit", "--no-fund"], bundleDir, verbose);
|
|
140
|
+
(0, build_1.runCommand)((0, build_1.npmExecutable)(), ["ci", "--omit=dev", "--ignore-scripts", "--no-bin-links", "--no-audit", "--no-fund"], bundleDir, verbose);
|
|
201
141
|
const nodeModulesPath = path.join(bundleDir, "node_modules");
|
|
202
142
|
if (!fs.existsSync(nodeModulesPath)) {
|
|
203
143
|
return;
|
|
@@ -208,14 +148,19 @@ function installNpmDependencies(pluginDir, bundleDir, verbose) {
|
|
|
208
148
|
rejectNativeAddonsRecursive(nodeModulesPath);
|
|
209
149
|
}
|
|
210
150
|
function copyIcon(pluginDir, bundleDir) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
151
|
+
const entries = fs.readdirSync(pluginDir, { withFileTypes: true });
|
|
152
|
+
const iconEntries = entries.filter((e) => {
|
|
153
|
+
if (!e.isFile())
|
|
154
|
+
return false;
|
|
155
|
+
const name = e.name.toLowerCase();
|
|
156
|
+
return name.startsWith("icon.") && ICON_EXTENSIONS.includes(name.slice(5));
|
|
157
|
+
});
|
|
158
|
+
for (const ext of ICON_EXTENSIONS) {
|
|
159
|
+
const found = iconEntries.find((e) => e.name.toLowerCase() === `icon.${ext}`);
|
|
160
|
+
if (found) {
|
|
161
|
+
(0, fs_utils_1.copyFileStrict)(path.join(pluginDir, found.name), path.join(bundleDir, found.name));
|
|
162
|
+
return;
|
|
216
163
|
}
|
|
217
|
-
(0, fs_utils_1.copyFileStrict)(sourcePath, path.join(bundleDir, fileName));
|
|
218
|
-
return;
|
|
219
164
|
}
|
|
220
165
|
}
|
|
221
166
|
function uniqueStagingDir(outDir) {
|
|
@@ -235,17 +180,22 @@ async function writeTarGz(outPath, baseDir, folderName) {
|
|
|
235
180
|
}, [folderName]);
|
|
236
181
|
}
|
|
237
182
|
async function bundlePlugin(parsedArgs) {
|
|
238
|
-
const { pluginDir, outDir, verbose, noNpmDeps } = parsedArgs;
|
|
183
|
+
const { pluginDir, outDir, verbose, noNpmDeps, noBuild } = parsedArgs;
|
|
239
184
|
if (verbose) {
|
|
240
185
|
process.stderr.write(`Bundling plugin from: ${pluginDir}\n`);
|
|
241
186
|
}
|
|
242
|
-
const { pluginId, moduleExec, manifestPath } =
|
|
187
|
+
const { pluginId, moduleExec, entry, manifestPath } = noBuild
|
|
188
|
+
? (0, build_1.readManifest)(pluginDir)
|
|
189
|
+
: (0, build_1.buildPluginAssets)({ pluginDir, verbose });
|
|
243
190
|
const themesPath = path.join(pluginDir, "themes");
|
|
244
191
|
const hasThemes = fs.existsSync(themesPath) && fs.lstatSync(themesPath).isDirectory();
|
|
245
|
-
if (
|
|
246
|
-
|
|
192
|
+
if (entry) {
|
|
193
|
+
validateManifestEntry(pluginDir, entry);
|
|
247
194
|
}
|
|
248
|
-
|
|
195
|
+
if (!moduleExec && !hasThemes && !entry) {
|
|
196
|
+
throw new Error("manifest has no module.exec, entry, or themes/");
|
|
197
|
+
}
|
|
198
|
+
(0, build_1.validateDeclaredModuleExec)(pluginDir, moduleExec);
|
|
249
199
|
(0, fs_utils_1.ensureDirectory)(outDir);
|
|
250
200
|
const stagingRoot = uniqueStagingDir(outDir);
|
|
251
201
|
const bundleDir = path.join(stagingRoot, pluginId);
|
|
@@ -253,6 +203,9 @@ async function bundlePlugin(parsedArgs) {
|
|
|
253
203
|
try {
|
|
254
204
|
(0, fs_utils_1.copyFileStrict)(manifestPath, path.join(bundleDir, "openvcs.plugin.json"));
|
|
255
205
|
copyIcon(pluginDir, bundleDir);
|
|
206
|
+
if (entry) {
|
|
207
|
+
copyEntryDirectory(pluginDir, bundleDir, entry);
|
|
208
|
+
}
|
|
256
209
|
const sourceBinDir = path.join(pluginDir, "bin");
|
|
257
210
|
if (fs.existsSync(sourceBinDir) && fs.lstatSync(sourceBinDir).isDirectory()) {
|
|
258
211
|
(0, fs_utils_1.copyDirectoryRecursiveStrict)(sourceBinDir, path.join(bundleDir, "bin"));
|
|
@@ -260,8 +213,8 @@ async function bundlePlugin(parsedArgs) {
|
|
|
260
213
|
if (hasThemes) {
|
|
261
214
|
(0, fs_utils_1.copyDirectoryRecursiveStrict)(themesPath, path.join(bundleDir, "themes"));
|
|
262
215
|
}
|
|
263
|
-
if (!noNpmDeps && hasPackageJson(pluginDir)) {
|
|
264
|
-
ensurePackageLock(pluginDir, verbose);
|
|
216
|
+
if (!noNpmDeps && (0, build_1.hasPackageJson)(pluginDir)) {
|
|
217
|
+
ensurePackageLock(pluginDir, bundleDir, verbose);
|
|
265
218
|
installNpmDependencies(pluginDir, bundleDir, verbose);
|
|
266
219
|
}
|
|
267
220
|
const outPath = path.join(outDir, `${pluginId}.ovcsp`);
|
|
@@ -278,9 +231,10 @@ async function bundlePlugin(parsedArgs) {
|
|
|
278
231
|
exports.__private = {
|
|
279
232
|
ICON_EXTENSIONS,
|
|
280
233
|
copyIcon,
|
|
281
|
-
readManifest,
|
|
234
|
+
readManifest: build_1.readManifest,
|
|
282
235
|
rejectNativeAddonsRecursive,
|
|
283
236
|
uniqueStagingDir,
|
|
284
|
-
validateDeclaredModuleExec,
|
|
237
|
+
validateDeclaredModuleExec: build_1.validateDeclaredModuleExec,
|
|
238
|
+
validateManifestEntry,
|
|
285
239
|
writeTarGz,
|
|
286
240
|
};
|
package/lib/init.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ declare function defaultPluginIdFromDir(targetDir: string): string;
|
|
|
25
25
|
declare function validatePluginId(pluginId: string): string | undefined;
|
|
26
26
|
declare function createReadlinePromptDriver(output?: NodeJS.WritableStream): PromptDriver;
|
|
27
27
|
declare function collectAnswers({ forceTheme, targetHint }: CollectAnswersOptions, promptDriver?: PromptDriver, output?: NodeJS.WritableStream): Promise<InitAnswers>;
|
|
28
|
+
declare function writeModuleTemplate(answers: InitAnswers): void;
|
|
28
29
|
export declare function runInitCommand(args: string[]): Promise<string>;
|
|
29
30
|
export declare function isUsageError(error: unknown): error is InitCommandError;
|
|
30
31
|
export declare const __private: {
|
|
@@ -33,5 +34,6 @@ export declare const __private: {
|
|
|
33
34
|
defaultPluginIdFromDir: typeof defaultPluginIdFromDir;
|
|
34
35
|
sanitizeIdToken: typeof sanitizeIdToken;
|
|
35
36
|
validatePluginId: typeof validatePluginId;
|
|
37
|
+
writeModuleTemplate: typeof writeModuleTemplate;
|
|
36
38
|
};
|
|
37
39
|
export {};
|
package/lib/init.js
CHANGED
|
@@ -192,12 +192,15 @@ function writeModuleTemplate(answers) {
|
|
|
192
192
|
private: true,
|
|
193
193
|
type: "module",
|
|
194
194
|
scripts: {
|
|
195
|
-
"build:
|
|
196
|
-
build: "
|
|
197
|
-
|
|
195
|
+
"build:plugin": "tsc -p tsconfig.json",
|
|
196
|
+
build: "openvcs build",
|
|
197
|
+
dist: "openvcs dist --plugin-dir . --out dist",
|
|
198
|
+
test: "openvcs dist --plugin-dir . --out dist --no-build --no-npm-deps",
|
|
198
199
|
},
|
|
199
|
-
|
|
200
|
+
dependencies: {
|
|
200
201
|
"@openvcs/sdk": `^${packageJson.version}`,
|
|
202
|
+
},
|
|
203
|
+
devDependencies: {
|
|
201
204
|
"@types/node": "^22.0.0",
|
|
202
205
|
typescript: "^5.8.2",
|
|
203
206
|
},
|
|
@@ -215,7 +218,7 @@ function writeModuleTemplate(answers) {
|
|
|
215
218
|
},
|
|
216
219
|
include: ["src/**/*.ts"],
|
|
217
220
|
});
|
|
218
|
-
writeText(path.join(answers.targetDir, "src", "plugin.ts"), "
|
|
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");
|
|
219
222
|
}
|
|
220
223
|
function writeThemeTemplate(answers) {
|
|
221
224
|
writeCommonFiles(answers);
|
|
@@ -224,10 +227,11 @@ function writeThemeTemplate(answers) {
|
|
|
224
227
|
version: answers.pluginVersion,
|
|
225
228
|
private: true,
|
|
226
229
|
scripts: {
|
|
227
|
-
build: "openvcs
|
|
228
|
-
|
|
230
|
+
build: "openvcs build",
|
|
231
|
+
dist: "openvcs dist --plugin-dir . --out dist",
|
|
232
|
+
test: "openvcs dist --plugin-dir . --out dist --no-build --no-npm-deps",
|
|
229
233
|
},
|
|
230
|
-
|
|
234
|
+
dependencies: {
|
|
231
235
|
"@openvcs/sdk": `^${packageJson.version}`,
|
|
232
236
|
},
|
|
233
237
|
});
|
|
@@ -304,4 +308,5 @@ exports.__private = {
|
|
|
304
308
|
defaultPluginIdFromDir,
|
|
305
309
|
sanitizeIdToken,
|
|
306
310
|
validatePluginId,
|
|
311
|
+
writeModuleTemplate,
|
|
307
312
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { PluginHost, PluginImplements, PluginDelegates, JsonRpcId, JsonRpcRequest, VcsDelegates } from '../types';
|
|
2
|
+
/** Describes the transport endpoints used by the plugin runtime loop. */
|
|
3
|
+
export interface PluginRuntimeTransport {
|
|
4
|
+
/** Stores the readable stdin-like stream receiving framed messages. */
|
|
5
|
+
stdin: NodeJS.ReadStream;
|
|
6
|
+
/** Stores the writable stdout-like stream sending framed messages. */
|
|
7
|
+
stdout: NodeJS.WritableStream;
|
|
8
|
+
}
|
|
9
|
+
/** Describes the context object passed to every SDK delegate handler. */
|
|
10
|
+
export interface PluginRuntimeContext {
|
|
11
|
+
/** Stores the active host notification helper. */
|
|
12
|
+
host: PluginHost;
|
|
13
|
+
/** Stores the request id currently being processed. */
|
|
14
|
+
requestId: JsonRpcId;
|
|
15
|
+
/** Stores the current host method name. */
|
|
16
|
+
method: string;
|
|
17
|
+
}
|
|
18
|
+
/** Describes the options accepted by `createPluginRuntime`. */
|
|
19
|
+
export interface CreatePluginRuntimeOptions {
|
|
20
|
+
/** Stores optional plugin lifecycle and settings delegates. */
|
|
21
|
+
plugin?: PluginDelegates<PluginRuntimeContext>;
|
|
22
|
+
/** Stores optional VCS method delegates. */
|
|
23
|
+
vcs?: VcsDelegates<PluginRuntimeContext>;
|
|
24
|
+
/** Stores optional capability overrides for `plugin.initialize`. */
|
|
25
|
+
implements?: Partial<PluginImplements>;
|
|
26
|
+
/** Stores the `host.log` target emitted by the runtime. */
|
|
27
|
+
logTarget?: string;
|
|
28
|
+
/** Stores the timeout in milliseconds for request handlers. */
|
|
29
|
+
timeout?: number;
|
|
30
|
+
/** Called when runtime starts and begins processing requests. */
|
|
31
|
+
onStart?: () => void | Promise<void>;
|
|
32
|
+
/** Called during stop() after pending operations complete. Called with error if shutdown due to processing error. */
|
|
33
|
+
onShutdown?: (error?: Error) => void | Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/** Describes one created SDK plugin runtime instance. */
|
|
36
|
+
export interface PluginRuntime {
|
|
37
|
+
/** Starts listening on stdio for framed JSON-RPC requests. */
|
|
38
|
+
start(transport?: PluginRuntimeTransport): void;
|
|
39
|
+
/** Stops the runtime and cleans up pending operations. */
|
|
40
|
+
stop(): void;
|
|
41
|
+
/** Consumes one raw stdio chunk and dispatches complete requests. */
|
|
42
|
+
consumeChunk(chunk: Buffer | string): void;
|
|
43
|
+
/** Dispatches one already-decoded JSON-RPC request. */
|
|
44
|
+
dispatchRequest(request: JsonRpcRequest): Promise<void>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { JsonRpcId, PluginDelegates, RequestParams } from '../types';
|
|
2
|
+
import type { PluginHost } from '../types';
|
|
3
|
+
import type { CreatePluginRuntimeOptions } from './contracts';
|
|
4
|
+
/** Describes the response emitter used by the dispatcher. */
|
|
5
|
+
export interface DispatcherResponseWriter {
|
|
6
|
+
/** Emits a JSON-RPC success response. */
|
|
7
|
+
sendResult<TResult>(id: JsonRpcId, result: TResult): void;
|
|
8
|
+
/** Emits a JSON-RPC error response. */
|
|
9
|
+
sendError(id: JsonRpcId, code: number, message: string, data?: unknown): void;
|
|
10
|
+
}
|
|
11
|
+
/** Describes the request handler built by `createRuntimeDispatcher`. */
|
|
12
|
+
export type RuntimeRequestDispatcher = (id: JsonRpcId, method: string, params: RequestParams) => Promise<void>;
|
|
13
|
+
/** Creates the plugin default handlers used when a delegate is omitted. */
|
|
14
|
+
export declare function createDefaultPluginDelegates<TContext>(): Required<Omit<PluginDelegates<TContext>, 'plugin.initialize'>>;
|
|
15
|
+
/** Creates the JSON-RPC dispatcher used by the SDK plugin runtime. */
|
|
16
|
+
export declare function createRuntimeDispatcher(options: CreatePluginRuntimeOptions, host: PluginHost, writer: DispatcherResponseWriter): RuntimeRequestDispatcher;
|
|
@@ -0,0 +1,133 @@
|
|
|
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.createDefaultPluginDelegates = createDefaultPluginDelegates;
|
|
6
|
+
exports.createRuntimeDispatcher = createRuntimeDispatcher;
|
|
7
|
+
const types_1 = require("../types");
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
/** Creates the plugin default handlers used when a delegate is omitted. */
|
|
10
|
+
function createDefaultPluginDelegates() {
|
|
11
|
+
return {
|
|
12
|
+
async 'plugin.init'() {
|
|
13
|
+
return null;
|
|
14
|
+
},
|
|
15
|
+
async 'plugin.deinit'() {
|
|
16
|
+
return null;
|
|
17
|
+
},
|
|
18
|
+
async 'plugin.get_menus'() {
|
|
19
|
+
return [];
|
|
20
|
+
},
|
|
21
|
+
async 'plugin.handle_action'() {
|
|
22
|
+
return null;
|
|
23
|
+
},
|
|
24
|
+
async 'plugin.settings.defaults'() {
|
|
25
|
+
return [];
|
|
26
|
+
},
|
|
27
|
+
async 'plugin.settings.on_load'(params) {
|
|
28
|
+
return Array.isArray(params.values) ? params.values : [];
|
|
29
|
+
},
|
|
30
|
+
async 'plugin.settings.on_apply'() {
|
|
31
|
+
return null;
|
|
32
|
+
},
|
|
33
|
+
async 'plugin.settings.on_save'(params) {
|
|
34
|
+
return Array.isArray(params.values) ? params.values : [];
|
|
35
|
+
},
|
|
36
|
+
async 'plugin.settings.on_reset'() {
|
|
37
|
+
return null;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Creates the JSON-RPC dispatcher used by the SDK plugin runtime. */
|
|
42
|
+
function createRuntimeDispatcher(options, host, writer) {
|
|
43
|
+
const defaultPluginDelegates = createDefaultPluginDelegates();
|
|
44
|
+
const pluginDelegates = options.plugin ?? {};
|
|
45
|
+
const vcsDelegates = options.vcs ?? {};
|
|
46
|
+
const runtimeImplements = buildRuntimeImplements(options.implements, options.vcs);
|
|
47
|
+
const timeout = options.timeout;
|
|
48
|
+
const typedPluginDelegates = pluginDelegates;
|
|
49
|
+
const typedDefaultPluginDelegates = defaultPluginDelegates;
|
|
50
|
+
const typedVcsDelegates = vcsDelegates;
|
|
51
|
+
return async (id, method, params) => {
|
|
52
|
+
try {
|
|
53
|
+
if (method === 'plugin.initialize') {
|
|
54
|
+
const expectedVersion = params.expected_protocol_version;
|
|
55
|
+
if (typeof expectedVersion === 'number' && expectedVersion !== types_1.PROTOCOL_VERSION) {
|
|
56
|
+
writer.sendError(id, types_1.PROTOCOL_VERSION_MISMATCH_CODE, 'protocol version mismatch', {
|
|
57
|
+
code: 'protocol-version-mismatch',
|
|
58
|
+
message: `host expects protocol ${expectedVersion}, plugin supports ${types_1.PROTOCOL_VERSION}`,
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const override = pluginDelegates['plugin.initialize']
|
|
63
|
+
? await pluginDelegates['plugin.initialize'](params, {
|
|
64
|
+
host,
|
|
65
|
+
requestId: id,
|
|
66
|
+
method,
|
|
67
|
+
})
|
|
68
|
+
: {};
|
|
69
|
+
writer.sendResult(id, {
|
|
70
|
+
protocol_version: override.protocol_version ?? types_1.PROTOCOL_VERSION,
|
|
71
|
+
implements: {
|
|
72
|
+
...runtimeImplements,
|
|
73
|
+
...override.implements,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const handler = typedPluginDelegates[method] ??
|
|
79
|
+
typedDefaultPluginDelegates[method] ??
|
|
80
|
+
typedVcsDelegates[method];
|
|
81
|
+
if (!handler) {
|
|
82
|
+
throw (0, errors_1.pluginError)('rpc-method-not-found', `method '${method}' is not implemented`);
|
|
83
|
+
}
|
|
84
|
+
let result;
|
|
85
|
+
if (timeout && timeout > 0) {
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
88
|
+
try {
|
|
89
|
+
result = await handler(params, {
|
|
90
|
+
host,
|
|
91
|
+
requestId: id,
|
|
92
|
+
method,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
clearTimeout(timeoutId);
|
|
97
|
+
if (error.name === 'AbortError') {
|
|
98
|
+
throw (0, errors_1.pluginError)('request-timeout', `method '${method}' timed out after ${timeout}ms`);
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
clearTimeout(timeoutId);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
result = await handler(params, {
|
|
106
|
+
host,
|
|
107
|
+
requestId: id,
|
|
108
|
+
method,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
writer.sendResult(id, result == null ? null : result);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if ((0, errors_1.isPluginFailure)(error)) {
|
|
115
|
+
writer.sendError(id, error.code, error.message, error.data);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const messageText = error instanceof Error ? error.message : String(error || 'unknown error');
|
|
119
|
+
host.error(messageText);
|
|
120
|
+
writer.sendError(id, types_1.PLUGIN_INTERNAL_ERROR_CODE, messageText, {
|
|
121
|
+
code: 'plugin-internal-error',
|
|
122
|
+
message: messageText,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Builds the handshake capability flags returned from `plugin.initialize`. */
|
|
128
|
+
function buildRuntimeImplements(overrides, vcsDelegates) {
|
|
129
|
+
return {
|
|
130
|
+
plugin: overrides?.plugin ?? true,
|
|
131
|
+
vcs: overrides?.vcs ?? Boolean(vcsDelegates && Object.keys(vcsDelegates).length > 0),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PluginFailure } from '../types';
|
|
2
|
+
/** Builds the host-facing plugin failure payload used for operational errors. */
|
|
3
|
+
export declare function pluginError(code: string, message: string): PluginFailure;
|
|
4
|
+
/** Returns whether the supplied value is a structured plugin failure. */
|
|
5
|
+
export declare function isPluginFailure(value: unknown): value is PluginFailure;
|
|
@@ -0,0 +1,26 @@
|
|
|
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.pluginError = pluginError;
|
|
6
|
+
exports.isPluginFailure = isPluginFailure;
|
|
7
|
+
const types_1 = require("../types");
|
|
8
|
+
/** Builds the host-facing plugin failure payload used for operational errors. */
|
|
9
|
+
function pluginError(code, message) {
|
|
10
|
+
return {
|
|
11
|
+
code: types_1.PLUGIN_FAILURE_CODE,
|
|
12
|
+
message,
|
|
13
|
+
data: {
|
|
14
|
+
code,
|
|
15
|
+
message,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Returns whether the supplied value is a structured plugin failure. */
|
|
20
|
+
function isPluginFailure(value) {
|
|
21
|
+
if (value == null || typeof value !== 'object') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const candidate = value;
|
|
25
|
+
return candidate.code === types_1.PLUGIN_FAILURE_CODE && typeof candidate.message === 'string';
|
|
26
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PluginHost } from '../types';
|
|
2
|
+
/** Describes the low-level notification sender used by `createHost`. */
|
|
3
|
+
export type HostNotificationSender = (method: string, params: unknown) => void;
|
|
4
|
+
/** Describes the options accepted by `createHost`. */
|
|
5
|
+
export interface CreateHostOptions {
|
|
6
|
+
/** Stores the `host.log` target name to emit for diagnostic messages. */
|
|
7
|
+
logTarget?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Creates the host notification helper used inside runtime delegates. */
|
|
10
|
+
export declare function createHost(sendNotification: HostNotificationSender, options?: CreateHostOptions): PluginHost;
|
|
@@ -0,0 +1,48 @@
|
|
|
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.createHost = createHost;
|
|
6
|
+
/** Creates the host notification helper used inside runtime delegates. */
|
|
7
|
+
function createHost(sendNotification, options = {}) {
|
|
8
|
+
const logTarget = options.logTarget ?? 'openvcs.plugin';
|
|
9
|
+
return {
|
|
10
|
+
log(level, message) {
|
|
11
|
+
sendNotification('host.log', {
|
|
12
|
+
level,
|
|
13
|
+
target: logTarget,
|
|
14
|
+
message,
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
info(message) {
|
|
18
|
+
sendNotification('host.log', {
|
|
19
|
+
level: 'info',
|
|
20
|
+
target: logTarget,
|
|
21
|
+
message,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
error(message) {
|
|
25
|
+
sendNotification('host.log', {
|
|
26
|
+
level: 'error',
|
|
27
|
+
target: logTarget,
|
|
28
|
+
message,
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
uiNotify(params) {
|
|
32
|
+
sendNotification('host.ui_notify', params);
|
|
33
|
+
},
|
|
34
|
+
statusSet(params) {
|
|
35
|
+
sendNotification('host.status_set', params);
|
|
36
|
+
},
|
|
37
|
+
emitEvent(params) {
|
|
38
|
+
sendNotification('host.event_emit', params);
|
|
39
|
+
},
|
|
40
|
+
emitVcsEvent(sessionId, requestId, event) {
|
|
41
|
+
sendNotification('vcs.event', {
|
|
42
|
+
session_id: sessionId,
|
|
43
|
+
request_id: requestId,
|
|
44
|
+
event,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CreatePluginRuntimeOptions, PluginRuntime, PluginRuntimeTransport } from './contracts';
|
|
2
|
+
export type { CreatePluginRuntimeOptions, PluginRuntime, PluginRuntimeContext, PluginRuntimeTransport, } from './contracts';
|
|
3
|
+
export { createDefaultPluginDelegates } from './dispatcher';
|
|
4
|
+
export { isPluginFailure, pluginError } from './errors';
|
|
5
|
+
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
|
+
/** Starts a previously created plugin runtime on process stdio. */
|
|
9
|
+
export declare function startPluginRuntime(runtime: PluginRuntime, transport?: PluginRuntimeTransport): void;
|