@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.
Files changed (51) hide show
  1. package/README.md +76 -7
  2. package/lib/build.d.ts +28 -0
  3. package/lib/build.js +188 -0
  4. package/lib/cli.js +21 -2
  5. package/lib/dist.d.ts +4 -7
  6. package/lib/dist.js +67 -113
  7. package/lib/init.d.ts +2 -0
  8. package/lib/init.js +13 -8
  9. package/lib/runtime/contracts.d.ts +45 -0
  10. package/lib/runtime/contracts.js +4 -0
  11. package/lib/runtime/dispatcher.d.ts +16 -0
  12. package/lib/runtime/dispatcher.js +133 -0
  13. package/lib/runtime/errors.d.ts +5 -0
  14. package/lib/runtime/errors.js +26 -0
  15. package/lib/runtime/host.d.ts +10 -0
  16. package/lib/runtime/host.js +48 -0
  17. package/lib/runtime/index.d.ts +9 -0
  18. package/lib/runtime/index.js +166 -0
  19. package/lib/runtime/transport.d.ts +14 -0
  20. package/lib/runtime/transport.js +72 -0
  21. package/lib/types/host.d.ts +57 -0
  22. package/lib/types/host.js +4 -0
  23. package/lib/types/index.d.ts +4 -0
  24. package/lib/types/index.js +22 -0
  25. package/lib/types/plugin.d.ts +56 -0
  26. package/lib/types/plugin.js +4 -0
  27. package/lib/types/protocol.d.ts +77 -0
  28. package/lib/types/protocol.js +13 -0
  29. package/lib/types/vcs.d.ts +459 -0
  30. package/lib/types/vcs.js +4 -0
  31. package/package.json +16 -3
  32. package/src/lib/build.ts +229 -0
  33. package/src/lib/cli.ts +21 -2
  34. package/src/lib/dist.ts +76 -128
  35. package/src/lib/init.ts +13 -8
  36. package/src/lib/runtime/contracts.ts +52 -0
  37. package/src/lib/runtime/dispatcher.ts +185 -0
  38. package/src/lib/runtime/errors.ts +27 -0
  39. package/src/lib/runtime/host.ts +72 -0
  40. package/src/lib/runtime/index.ts +201 -0
  41. package/src/lib/runtime/transport.ts +93 -0
  42. package/src/lib/types/host.ts +71 -0
  43. package/src/lib/types/index.ts +7 -0
  44. package/src/lib/types/plugin.ts +110 -0
  45. package/src/lib/types/protocol.ts +97 -0
  46. package/src/lib/types/vcs.ts +579 -0
  47. package/test/build.test.js +95 -0
  48. package/test/cli.test.js +37 -0
  49. package/test/dist.test.js +239 -15
  50. package/test/init.test.js +25 -0
  51. package/test/runtime.test.js +118 -0
package/README.md CHANGED
@@ -7,7 +7,9 @@
7
7
  OpenVCS SDK for npm-based plugin development.
8
8
 
9
9
  Install this package in plugin projects, scaffold a starter plugin, and package
10
- plugins into `.ovcsp` bundles.
10
+ plugins into `.ovcsp` bundles. The SDK also exports a Node-only JSON-RPC runtime
11
+ layer and shared protocol/types so plugins do not have to hand-roll stdio
12
+ framing or method dispatch.
11
13
 
12
14
  ## Install
13
15
 
@@ -44,6 +46,7 @@ Run the local CLI through npm scripts:
44
46
 
45
47
  ```bash
46
48
  npm run openvcs -- --help
49
+ npm run openvcs -- build --help
47
50
  npm run openvcs -- init --help
48
51
  npm run openvcs -- dist --help
49
52
  ```
@@ -60,29 +63,93 @@ The generated module template includes TypeScript and Node typings (`@types/node
60
63
  Plugin IDs entered during scaffold must not be `.`/`..` and must not contain path
61
64
  separators (`/` or `\\`).
62
65
 
66
+ Generated module plugins now start with a working SDK runtime entrypoint:
67
+
68
+ ```ts
69
+ import { createPluginRuntime, startPluginRuntime } from '@openvcs/sdk/runtime';
70
+
71
+ const runtime = createPluginRuntime({
72
+ plugin: {
73
+ async 'plugin.init'(_params, context) {
74
+ context.host.info('OpenVCS plugin started');
75
+ return null;
76
+ },
77
+ },
78
+ });
79
+
80
+ startPluginRuntime(runtime);
81
+ ```
82
+
83
+ Runtime and protocol imports are exposed as npm subpaths:
84
+
85
+ ```ts
86
+ import { createPluginRuntime, pluginError } from '@openvcs/sdk/runtime';
87
+ import type { PluginDelegates, VcsDelegates } from '@openvcs/sdk/types';
88
+ ```
89
+
90
+ The runtime handles stdio framing, JSON-RPC request dispatch, host notifications,
91
+ default `plugin.*` handlers, and exact-method delegate registration for `vcs.*`.
92
+
63
93
  Interactive theme plugin scaffold:
64
94
 
65
95
  ```bash
66
96
  openvcs init --theme my-theme
67
97
  ```
68
98
 
99
+ ## Build plugin assets
100
+
101
+ In a generated code plugin folder:
102
+
103
+ ```bash
104
+ npm run build
105
+ ```
106
+
107
+ This runs `openvcs build`, which executes `scripts["build:plugin"]` and verifies
108
+ that `bin/<module.exec>` now exists.
109
+
110
+ Theme-only plugins can also run `npm run build`; the command exits successfully
111
+ without producing `bin/` output.
112
+
69
113
  ## Build a `.ovcsp` bundle
70
114
 
71
115
  In a generated plugin folder:
72
116
 
73
117
  ```bash
74
- npm run build
118
+ npm run dist
75
119
  ```
76
120
 
77
121
  This produces `dist/<plugin-id>.ovcsp`.
78
122
 
123
+ `openvcs dist` runs `openvcs build` first unless `--no-build` is provided.
124
+ Use `--no-build` when packaging prebuilt plugin assets.
125
+
126
+ Generated code plugin scripts use this split by default:
127
+
128
+ ```json
129
+ {
130
+ "scripts": {
131
+ "build:plugin": "tsc -p tsconfig.json",
132
+ "build": "openvcs build",
133
+ "dist": "openvcs dist --plugin-dir . --out dist"
134
+ }
135
+ }
136
+ ```
137
+
79
138
  `.ovcsp` is a gzip-compressed tar archive (`tar.gz`) that contains a top-level
80
139
  `<plugin-id>/` directory with `openvcs.plugin.json` and plugin runtime assets.
81
140
 
141
+ Bundle contents:
142
+ - `openvcs.plugin.json` (required)
143
+ - `icon.*` (optional, first found by extension priority)
144
+ - `bin/` (required for code plugins with `module.exec`)
145
+ - `entry` directory (required for UI plugins with top-level `entry` field; the entire directory containing the entry file is bundled)
146
+ - `themes/` (required for theme plugins)
147
+ - `node_modules/` (if npm dependencies are bundled)
148
+
82
149
  Dependency behavior while packaging:
83
150
 
84
151
  - npm dependency bundling is enabled by default when `package.json` exists.
85
- - If `package-lock.json` is missing, SDK generates it in the plugin worktree.
152
+ - If `package-lock.json` is missing, SDK generates it in the staging area (not the plugin worktree).
86
153
  - Dependencies are installed into the bundle staging dir with:
87
154
  - `npm ci --omit=dev --ignore-scripts --no-bin-links --no-audit --no-fund`
88
155
  - Disable npm dependency processing with `--no-npm-deps`.
@@ -93,15 +160,17 @@ Dependency behavior while packaging:
93
160
  Package a plugin manually:
94
161
 
95
162
  ```bash
96
- openvcs dist --plugin-dir /path/to/plugin --out /path/to/dist
163
+ npx openvcs build --plugin-dir /path/to/plugin
164
+ npx openvcs dist --plugin-dir /path/to/plugin --out /path/to/dist
97
165
  ```
98
166
 
99
167
  Show command help:
100
168
 
101
169
  ```bash
102
- openvcs --help
103
- openvcs dist --help
104
- openvcs init --help
170
+ npx openvcs --help
171
+ npx openvcs build --help
172
+ npx openvcs dist --help
173
+ npx openvcs init --help
105
174
  ```
106
175
 
107
176
  ## Releases
package/lib/build.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /** CLI arguments for `openvcs build`. */
2
+ export interface BuildArgs {
3
+ pluginDir: string;
4
+ verbose: boolean;
5
+ }
6
+ /** Trimmed manifest details used by build and dist flows. */
7
+ export interface ManifestInfo {
8
+ pluginId: string;
9
+ moduleExec: string | undefined;
10
+ entry: string | undefined;
11
+ manifestPath: string;
12
+ }
13
+ /** Returns the npm executable name for the current platform. */
14
+ export declare function npmExecutable(): string;
15
+ /** Formats help text for the build command. */
16
+ export declare function buildUsage(commandName?: string): string;
17
+ /** Parses `openvcs build` arguments. */
18
+ export declare function parseBuildArgs(args: string[]): BuildArgs;
19
+ /** Reads and validates the plugin manifest. */
20
+ export declare function readManifest(pluginDir: string): ManifestInfo;
21
+ /** Verifies that a declared module entry resolves to a real file under `bin/`. */
22
+ export declare function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined): void;
23
+ /** Returns whether the plugin repository has a `package.json`. */
24
+ export declare function hasPackageJson(pluginDir: string): boolean;
25
+ /** Runs a command in the given directory with optional verbose logging. */
26
+ export declare function runCommand(program: string, args: string[], cwd: string, verbose: boolean): void;
27
+ /** Builds a plugin's runtime assets when it declares a code module. */
28
+ export declare function buildPluginAssets(parsedArgs: BuildArgs): ManifestInfo;
package/lib/build.js ADDED
@@ -0,0 +1,188 @@
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.npmExecutable = npmExecutable;
6
+ exports.buildUsage = buildUsage;
7
+ exports.parseBuildArgs = parseBuildArgs;
8
+ exports.readManifest = readManifest;
9
+ exports.validateDeclaredModuleExec = validateDeclaredModuleExec;
10
+ exports.hasPackageJson = hasPackageJson;
11
+ exports.runCommand = runCommand;
12
+ exports.buildPluginAssets = buildPluginAssets;
13
+ const fs = require("node:fs");
14
+ const path = require("node:path");
15
+ const node_child_process_1 = require("node:child_process");
16
+ const fs_utils_1 = require("./fs-utils");
17
+ /** Returns the npm executable name for the current platform. */
18
+ function npmExecutable() {
19
+ return process.platform === "win32" ? "npm.cmd" : "npm";
20
+ }
21
+ /** Formats help text for the build command. */
22
+ function buildUsage(commandName = "openvcs") {
23
+ return `${commandName} build [args]\n\n --plugin-dir <path> Plugin repository root (contains openvcs.plugin.json)\n -V, --verbose Enable verbose output\n`;
24
+ }
25
+ /** Parses `openvcs build` arguments. */
26
+ function parseBuildArgs(args) {
27
+ let pluginDir = process.cwd();
28
+ let verbose = false;
29
+ for (let index = 0; index < args.length; index += 1) {
30
+ const arg = args[index];
31
+ if (arg === "--plugin-dir") {
32
+ index += 1;
33
+ if (index >= args.length) {
34
+ throw new Error("missing value for --plugin-dir");
35
+ }
36
+ pluginDir = args[index];
37
+ continue;
38
+ }
39
+ if (arg === "-V" || arg === "--verbose") {
40
+ verbose = true;
41
+ continue;
42
+ }
43
+ if (arg === "--help") {
44
+ const error = new Error(buildUsage());
45
+ error.code = "USAGE";
46
+ throw error;
47
+ }
48
+ throw new Error(`unknown flag: ${arg}`);
49
+ }
50
+ return {
51
+ pluginDir: path.resolve(pluginDir),
52
+ verbose,
53
+ };
54
+ }
55
+ /** Reads and validates the plugin manifest. */
56
+ function readManifest(pluginDir) {
57
+ const manifestPath = path.join(pluginDir, "openvcs.plugin.json");
58
+ let manifestRaw;
59
+ let manifestFd;
60
+ let manifest;
61
+ try {
62
+ manifestFd = fs.openSync(manifestPath, "r");
63
+ const manifestStat = fs.fstatSync(manifestFd);
64
+ if (!manifestStat.isFile()) {
65
+ throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
66
+ }
67
+ manifestRaw = fs.readFileSync(manifestFd, "utf8");
68
+ }
69
+ catch (error) {
70
+ if (error.code === "ENOENT") {
71
+ throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
72
+ }
73
+ throw error;
74
+ }
75
+ finally {
76
+ if (typeof manifestFd === "number") {
77
+ fs.closeSync(manifestFd);
78
+ }
79
+ }
80
+ try {
81
+ manifest = JSON.parse(manifestRaw);
82
+ }
83
+ catch (error) {
84
+ const detail = error instanceof Error ? error.message : String(error);
85
+ throw new Error(`parse ${manifestPath}: ${detail}`);
86
+ }
87
+ const pluginId = typeof manifest.id === "string"
88
+ ? manifest.id.trim()
89
+ : "";
90
+ if (!pluginId) {
91
+ throw new Error(`manifest ${manifestPath} is missing a string 'id'`);
92
+ }
93
+ if (pluginId === "." || pluginId === ".." || pluginId.includes("/") || pluginId.includes("\\")) {
94
+ throw new Error(`manifest id must not contain path separators: ${pluginId}`);
95
+ }
96
+ const moduleValue = manifest.module;
97
+ const moduleExec = typeof moduleValue?.exec === "string" ? moduleValue.exec.trim() : undefined;
98
+ const entryValue = manifest.entry;
99
+ const entry = typeof entryValue === "string" ? entryValue.trim() : undefined;
100
+ return {
101
+ pluginId,
102
+ moduleExec,
103
+ entry,
104
+ manifestPath,
105
+ };
106
+ }
107
+ /** Verifies that a declared module entry resolves to a real file under `bin/`. */
108
+ function validateDeclaredModuleExec(pluginDir, moduleExec) {
109
+ if (!moduleExec) {
110
+ return;
111
+ }
112
+ const normalizedExec = moduleExec.trim();
113
+ const lowered = normalizedExec.toLowerCase();
114
+ if (!lowered.endsWith(".js") && !lowered.endsWith(".mjs") && !lowered.endsWith(".cjs")) {
115
+ throw new Error(`manifest exec must end with .js/.mjs/.cjs (Node runtime): ${moduleExec}`);
116
+ }
117
+ if (path.isAbsolute(normalizedExec)) {
118
+ throw new Error(`manifest module.exec must be a relative path under bin/: ${moduleExec}`);
119
+ }
120
+ const binDir = path.resolve(pluginDir, "bin");
121
+ const targetPath = path.resolve(binDir, normalizedExec);
122
+ if (!(0, fs_utils_1.isPathInside)(binDir, targetPath) || targetPath === binDir) {
123
+ throw new Error(`manifest module.exec must point to a file under bin/: ${moduleExec}`);
124
+ }
125
+ if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
126
+ throw new Error(`module entrypoint not found at ${targetPath}`);
127
+ }
128
+ }
129
+ /** Returns whether the plugin repository has a `package.json`. */
130
+ function hasPackageJson(pluginDir) {
131
+ const packageJsonPath = path.join(pluginDir, "package.json");
132
+ return fs.existsSync(packageJsonPath) && fs.lstatSync(packageJsonPath).isFile();
133
+ }
134
+ /** Runs a command in the given directory with optional verbose logging. */
135
+ function runCommand(program, args, cwd, verbose) {
136
+ if (verbose) {
137
+ process.stderr.write(`Running command in ${cwd}: ${program} ${args.join(" ")}\n`);
138
+ }
139
+ const result = (0, node_child_process_1.spawnSync)(program, args, {
140
+ cwd,
141
+ stdio: ["ignore", verbose ? "inherit" : "ignore", "inherit"],
142
+ });
143
+ if (result.error) {
144
+ throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
145
+ }
146
+ if (result.status === 0) {
147
+ return;
148
+ }
149
+ throw new Error(`command failed (${program} ${args.join(" ")}), exit code ${result.status}`);
150
+ }
151
+ /** Reads `package.json` scripts for the plugin, if present. */
152
+ function readPackageScripts(pluginDir) {
153
+ const packageJsonPath = path.join(pluginDir, "package.json");
154
+ let packageData;
155
+ try {
156
+ packageData = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
157
+ }
158
+ catch (error) {
159
+ if (error.code === "ENOENT") {
160
+ throw new Error(`code plugins must include package.json: ${packageJsonPath}`);
161
+ }
162
+ const detail = error instanceof Error ? error.message : String(error);
163
+ throw new Error(`parse ${packageJsonPath}: ${detail}`);
164
+ }
165
+ const scripts = packageData.scripts;
166
+ if (typeof scripts !== "object" || scripts === null) {
167
+ return {};
168
+ }
169
+ return scripts;
170
+ }
171
+ /** Builds a plugin's runtime assets when it declares a code module. */
172
+ function buildPluginAssets(parsedArgs) {
173
+ const manifest = readManifest(parsedArgs.pluginDir);
174
+ if (!manifest.moduleExec) {
175
+ return manifest;
176
+ }
177
+ if (!hasPackageJson(parsedArgs.pluginDir)) {
178
+ throw new Error(`code plugins must include package.json: ${path.join(parsedArgs.pluginDir, "package.json")}`);
179
+ }
180
+ const scripts = readPackageScripts(parsedArgs.pluginDir);
181
+ const buildScript = scripts["build:plugin"];
182
+ if (typeof buildScript !== "string" || buildScript.trim() === "") {
183
+ throw new Error(`code plugins must define scripts[\"build:plugin\"] in ${path.join(parsedArgs.pluginDir, "package.json")}`);
184
+ }
185
+ runCommand(npmExecutable(), ["run", "build:plugin"], parsedArgs.pluginDir, parsedArgs.verbose);
186
+ validateDeclaredModuleExec(parsedArgs.pluginDir, manifest.moduleExec);
187
+ return manifest;
188
+ }
package/lib/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runCli = runCli;
4
+ const build_1 = require("./build");
4
5
  const dist_1 = require("./dist");
5
6
  const init_1 = require("./init");
6
7
  const packageJson = require("../package.json");
@@ -11,7 +12,7 @@ function hasCode(error, code) {
11
12
  error.code === code);
12
13
  }
13
14
  function usage() {
14
- return "Usage: openvcs <command> [options]\n\nCommands:\n dist [args] Package plugin into .ovcsp\n init [--theme] [dir] Interactively scaffold a plugin project\n -v, --version Show version information\n\nDist args:\n --plugin-dir <path> Plugin root containing openvcs.plugin.json\n --out <path> Output directory (default: ./dist)\n --no-npm-deps Skip npm dependency bundling\n -V, --verbose Verbose output\n";
15
+ return "Usage: openvcs <command> [options]\n\nCommands:\n build [args] Build plugin runtime assets\n dist [args] Package plugin into .ovcsp\n init [--theme] [dir] Interactively scaffold a plugin project\n -v, --version Show version information\n\nBuild args:\n --plugin-dir <path> Plugin root containing openvcs.plugin.json\n -V, --verbose Verbose output\n\nDist args:\n --plugin-dir <path> Plugin root containing openvcs.plugin.json\n --out <path> Output directory (default: ./dist)\n --no-build Skip plugin build before packaging\n --no-npm-deps Skip npm dependency bundling\n -V, --verbose Verbose output\n";
15
16
  }
16
17
  async function runCli(args) {
17
18
  if (args.length === 0) {
@@ -19,7 +20,7 @@ async function runCli(args) {
19
20
  process.exitCode = 1;
20
21
  return;
21
22
  }
22
- if (args.includes("-v") || args.includes("--version")) {
23
+ if (args[0] === "-v" || args[0] === "--version") {
23
24
  process.stdout.write(`openvcs ${packageJson.version}\n`);
24
25
  return;
25
26
  }
@@ -46,6 +47,24 @@ async function runCli(args) {
46
47
  throw error;
47
48
  }
48
49
  }
50
+ if (command === "build") {
51
+ if (rest.includes("--help")) {
52
+ process.stdout.write((0, build_1.buildUsage)());
53
+ return;
54
+ }
55
+ try {
56
+ const parsed = (0, build_1.parseBuildArgs)(rest);
57
+ const manifest = (0, build_1.buildPluginAssets)(parsed);
58
+ process.stdout.write(`${manifest.pluginId}\n`);
59
+ return;
60
+ }
61
+ catch (error) {
62
+ if (hasCode(error, "USAGE")) {
63
+ throw new Error((0, build_1.buildUsage)());
64
+ }
65
+ throw error;
66
+ }
67
+ }
49
68
  if (command === "init") {
50
69
  if (rest.includes("--help")) {
51
70
  process.stdout.write((0, init_1.initUsage)());
package/lib/dist.d.ts CHANGED
@@ -1,18 +1,14 @@
1
+ import { readManifest, validateDeclaredModuleExec } from "./build";
1
2
  interface DistArgs {
2
3
  pluginDir: string;
3
4
  outDir: string;
4
5
  verbose: boolean;
5
6
  noNpmDeps: boolean;
6
- }
7
- interface ManifestInfo {
8
- pluginId: string;
9
- moduleExec: string | undefined;
10
- manifestPath: string;
7
+ noBuild: boolean;
11
8
  }
12
9
  export declare function distUsage(commandName?: string): string;
13
10
  export declare function parseDistArgs(args: string[]): DistArgs;
14
- declare function readManifest(pluginDir: string): ManifestInfo;
15
- declare function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined): void;
11
+ declare function validateManifestEntry(pluginDir: string, entry: string): void;
16
12
  declare function rejectNativeAddonsRecursive(dirPath: string): void;
17
13
  declare function copyIcon(pluginDir: string, bundleDir: string): void;
18
14
  declare function uniqueStagingDir(outDir: string): string;
@@ -25,6 +21,7 @@ export declare const __private: {
25
21
  rejectNativeAddonsRecursive: typeof rejectNativeAddonsRecursive;
26
22
  uniqueStagingDir: typeof uniqueStagingDir;
27
23
  validateDeclaredModuleExec: typeof validateDeclaredModuleExec;
24
+ validateManifestEntry: typeof validateManifestEntry;
28
25
  writeTarGz: typeof writeTarGz;
29
26
  };
30
27
  export {};