@openvcs/sdk 0.4.0 → 0.4.1-edge.20260530.101
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 +12 -0
- package/lib/build.d.ts +1 -4
- package/lib/build.js +6 -16
- package/lib/init.d.ts +4 -0
- package/lib/init.js +5 -14
- package/lib/npm-runner.d.ts +11 -0
- package/lib/npm-runner.js +62 -0
- package/lib/runtime/dispatcher.js +19 -11
- package/package.json +6 -3
- package/src/lib/build.ts +5 -17
- package/src/lib/init.ts +6 -17
- package/src/lib/npm-runner.ts +47 -0
- package/src/lib/runtime/dispatcher.ts +19 -11
- package/test/build.test.js +99 -0
- package/test/cli-direct.test.js +57 -0
- package/test/dispatcher.test.js +104 -0
- package/test/fs-utils.test.js +57 -0
- package/test/host.test.js +39 -0
- package/test/init.test.js +209 -1
- package/test/menu.test.js +147 -0
- package/test/modal.test.js +74 -0
- package/test/npm-runner.test.js +89 -0
- package/test/runtime.test.js +143 -0
- package/test/transport.test.js +71 -0
- package/test/vcs-delegate-base.test.js +21 -0
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
OpenVCS SDK for npm-based plugin development.
|
|
7
7
|
|
|
8
|
+
Requires Node.js 20 or newer.
|
|
9
|
+
|
|
8
10
|
Install this package in plugin projects, scaffold a starter plugin, and build
|
|
9
11
|
plugin runtime assets. The SDK also exports a Node-only JSON-RPC runtime
|
|
10
12
|
layer and shared protocol/types so plugins do not have to hand-roll stdio
|
|
@@ -41,6 +43,16 @@ Run tests (builds first):
|
|
|
41
43
|
npm test
|
|
42
44
|
```
|
|
43
45
|
|
|
46
|
+
Generate a coverage report:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm run coverage
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This runs the full test flow, writes c8 output to `coverage/`, and fails if
|
|
53
|
+
coverage drops below 95% lines, 95% functions, 85% branches, or 95%
|
|
54
|
+
statements.
|
|
55
|
+
|
|
44
56
|
Run the local CLI through npm scripts:
|
|
45
57
|
|
|
46
58
|
```bash
|
package/lib/build.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { npmExecutable } from "./npm-runner";
|
|
1
2
|
/** CLI arguments for `openvcs build`. */
|
|
2
3
|
export interface BuildArgs {
|
|
3
4
|
pluginDir: string;
|
|
@@ -10,10 +11,6 @@ export interface ManifestInfo {
|
|
|
10
11
|
entry: string | undefined;
|
|
11
12
|
manifestPath: string;
|
|
12
13
|
}
|
|
13
|
-
/** Returns the npm executable name for the current platform. */
|
|
14
|
-
export declare function npmExecutable(): string;
|
|
15
|
-
/** Returns whether a command must be launched via the Windows shell. */
|
|
16
|
-
export declare function shouldUseWindowsShell(program: string): boolean;
|
|
17
14
|
/** Formats help text for the build command. */
|
|
18
15
|
export declare function buildUsage(commandName?: string): string;
|
|
19
16
|
/** Parses `openvcs build` arguments. */
|
package/lib/build.js
CHANGED
|
@@ -35,8 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
};
|
|
36
36
|
})();
|
|
37
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
-
exports.npmExecutable =
|
|
39
|
-
exports.shouldUseWindowsShell = shouldUseWindowsShell;
|
|
38
|
+
exports.npmExecutable = void 0;
|
|
40
39
|
exports.buildUsage = buildUsage;
|
|
41
40
|
exports.parseBuildArgs = parseBuildArgs;
|
|
42
41
|
exports.readManifest = readManifest;
|
|
@@ -52,19 +51,10 @@ const fs = __importStar(require("node:fs"));
|
|
|
52
51
|
const path = __importStar(require("node:path"));
|
|
53
52
|
const node_child_process_1 = require("node:child_process");
|
|
54
53
|
const fs_utils_1 = require("./fs-utils");
|
|
54
|
+
const npm_runner_1 = require("./npm-runner");
|
|
55
|
+
var npm_runner_2 = require("./npm-runner");
|
|
56
|
+
Object.defineProperty(exports, "npmExecutable", { enumerable: true, get: function () { return npm_runner_2.npmExecutable; } });
|
|
55
57
|
const AUTHORED_PLUGIN_MODULE_BASENAME = "plugin.js";
|
|
56
|
-
/** Returns the npm executable name for the current platform. */
|
|
57
|
-
function npmExecutable() {
|
|
58
|
-
return "npm";
|
|
59
|
-
}
|
|
60
|
-
/** Returns whether a command must be launched via the Windows shell. */
|
|
61
|
-
function shouldUseWindowsShell(program) {
|
|
62
|
-
if (process.platform !== "win32") {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
const normalized = program.toLowerCase();
|
|
66
|
-
return normalized === "npm" || normalized.endsWith(".cmd") || normalized.endsWith(".bat");
|
|
67
|
-
}
|
|
68
58
|
/** Formats help text for the build command. */
|
|
69
59
|
function buildUsage(commandName = "openvcs") {
|
|
70
60
|
return `${commandName} build [args]\n\n --plugin-dir <path> Plugin repository root (contains package.json with openvcs metadata)\n -V, --verbose Enable verbose output\n`;
|
|
@@ -262,8 +252,8 @@ function runCommand(program, args, cwd, verbose) {
|
|
|
262
252
|
}
|
|
263
253
|
const result = (0, node_child_process_1.spawnSync)(program, args, {
|
|
264
254
|
cwd,
|
|
265
|
-
shell: shouldUseWindowsShell(program),
|
|
266
255
|
stdio: ["ignore", verbose ? "inherit" : "ignore", "inherit"],
|
|
256
|
+
windowsHide: true,
|
|
267
257
|
});
|
|
268
258
|
if (result.error) {
|
|
269
259
|
throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
|
|
@@ -307,7 +297,7 @@ function buildPluginAssets(parsedArgs) {
|
|
|
307
297
|
if (typeof buildScript !== "string" || buildScript.trim() === "") {
|
|
308
298
|
throw new Error(`code plugins must define scripts[\"build:plugin\"] in ${path.join(parsedArgs.pluginDir, "package.json")}`);
|
|
309
299
|
}
|
|
310
|
-
runCommand(npmExecutable(), ["run", "build:plugin"], parsedArgs.pluginDir, parsedArgs.verbose);
|
|
300
|
+
runCommand((0, npm_runner_1.npmExecutable)(), [...(0, npm_runner_1.npmArgsPrefix)(), "run", "build:plugin"], parsedArgs.pluginDir, parsedArgs.verbose);
|
|
311
301
|
generateModuleBootstrap(parsedArgs.pluginDir, manifest.moduleExec);
|
|
312
302
|
validateDeclaredModuleExec(parsedArgs.pluginDir, manifest.moduleExec);
|
|
313
303
|
return manifest;
|
package/lib/init.d.ts
CHANGED
|
@@ -22,18 +22,22 @@ interface InitCommandError {
|
|
|
22
22
|
export declare function initUsage(commandName?: string): string;
|
|
23
23
|
declare function sanitizeIdToken(raw: string): string;
|
|
24
24
|
declare function defaultPluginIdFromDir(targetDir: string): string;
|
|
25
|
+
declare function defaultPluginNameFromId(pluginId: string): string;
|
|
25
26
|
declare function validatePluginId(pluginId: string): string | undefined;
|
|
26
27
|
declare function createReadlinePromptDriver(output?: NodeJS.WritableStream): PromptDriver;
|
|
27
28
|
declare function collectAnswers({ forceTheme, targetHint }: CollectAnswersOptions, promptDriver?: PromptDriver, output?: NodeJS.WritableStream): Promise<InitAnswers>;
|
|
28
29
|
declare function writeModuleTemplate(answers: InitAnswers): void;
|
|
30
|
+
declare function writeThemeTemplate(answers: InitAnswers): void;
|
|
29
31
|
export declare function runInitCommand(args: string[]): Promise<string>;
|
|
30
32
|
export declare function isUsageError(error: unknown): error is InitCommandError;
|
|
31
33
|
export declare const __private: {
|
|
32
34
|
collectAnswers: typeof collectAnswers;
|
|
33
35
|
createReadlinePromptDriver: typeof createReadlinePromptDriver;
|
|
36
|
+
defaultPluginNameFromId: typeof defaultPluginNameFromId;
|
|
34
37
|
defaultPluginIdFromDir: typeof defaultPluginIdFromDir;
|
|
35
38
|
sanitizeIdToken: typeof sanitizeIdToken;
|
|
36
39
|
validatePluginId: typeof validatePluginId;
|
|
37
40
|
writeModuleTemplate: typeof writeModuleTemplate;
|
|
41
|
+
writeThemeTemplate: typeof writeThemeTemplate;
|
|
38
42
|
};
|
|
39
43
|
export {};
|
package/lib/init.js
CHANGED
|
@@ -42,17 +42,8 @@ const path = __importStar(require("node:path"));
|
|
|
42
42
|
const readline = __importStar(require("node:readline/promises"));
|
|
43
43
|
const node_process_1 = require("node:process");
|
|
44
44
|
const node_child_process_1 = require("node:child_process");
|
|
45
|
+
const npm_runner_1 = require("./npm-runner");
|
|
45
46
|
const packageJson = require("../package.json");
|
|
46
|
-
function npmExecutable() {
|
|
47
|
-
return "npm";
|
|
48
|
-
}
|
|
49
|
-
function shouldUseWindowsShell(program) {
|
|
50
|
-
if (process.platform !== "win32") {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
const normalized = program.toLowerCase();
|
|
54
|
-
return normalized === "npm" || normalized.endsWith(".cmd") || normalized.endsWith(".bat");
|
|
55
|
-
}
|
|
56
47
|
function initUsage(commandName = "openvcs") {
|
|
57
48
|
return `Usage: ${commandName} init [--theme] [target-dir]\n\nOptions:\n --theme Start with a theme-only plugin template\n`;
|
|
58
49
|
}
|
|
@@ -203,10 +194,10 @@ async function collectAnswers({ forceTheme, targetHint }, promptDriver = createR
|
|
|
203
194
|
}
|
|
204
195
|
}
|
|
205
196
|
function runNpmInstall(targetDir) {
|
|
206
|
-
const result = (0, node_child_process_1.spawnSync)(npmExecutable(), ["install"], {
|
|
197
|
+
const result = (0, node_child_process_1.spawnSync)((0, npm_runner_1.npmExecutable)(), [...(0, npm_runner_1.npmArgsPrefix)(), "install"], {
|
|
207
198
|
cwd: targetDir,
|
|
208
|
-
shell: shouldUseWindowsShell(npmExecutable()),
|
|
209
199
|
stdio: "inherit",
|
|
200
|
+
windowsHide: true,
|
|
210
201
|
});
|
|
211
202
|
if (result.error) {
|
|
212
203
|
throw new Error(`failed to spawn npm install in ${targetDir}: ${result.error.message}`);
|
|
@@ -228,7 +219,6 @@ function writeModuleTemplate(answers) {
|
|
|
228
219
|
openvcs: {
|
|
229
220
|
id: answers.pluginId,
|
|
230
221
|
name: answers.pluginName,
|
|
231
|
-
version: answers.pluginVersion,
|
|
232
222
|
default_enabled: answers.defaultEnabled,
|
|
233
223
|
module: { exec: "openvcs-plugin.js" },
|
|
234
224
|
},
|
|
@@ -269,7 +259,6 @@ function writeThemeTemplate(answers) {
|
|
|
269
259
|
openvcs: {
|
|
270
260
|
id: answers.pluginId,
|
|
271
261
|
name: answers.pluginName,
|
|
272
|
-
version: answers.pluginVersion,
|
|
273
262
|
default_enabled: answers.defaultEnabled,
|
|
274
263
|
},
|
|
275
264
|
scripts: {
|
|
@@ -350,8 +339,10 @@ function isUsageError(error) {
|
|
|
350
339
|
exports.__private = {
|
|
351
340
|
collectAnswers,
|
|
352
341
|
createReadlinePromptDriver,
|
|
342
|
+
defaultPluginNameFromId,
|
|
353
343
|
defaultPluginIdFromDir,
|
|
354
344
|
sanitizeIdToken,
|
|
355
345
|
validatePluginId,
|
|
356
346
|
writeModuleTemplate,
|
|
347
|
+
writeThemeTemplate,
|
|
357
348
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type ExistsFn = (path: string) => boolean;
|
|
2
|
+
type ResolveFn = (specifier: string) => string;
|
|
3
|
+
export interface NpmCommand {
|
|
4
|
+
program: string;
|
|
5
|
+
argsPrefix: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveNpmCli(execPath?: string, exists?: ExistsFn, resolve?: ResolveFn): string;
|
|
8
|
+
export declare function npmCommand(platform?: NodeJS.Platform, execPath?: string, exists?: ExistsFn, resolve?: ResolveFn): NpmCommand;
|
|
9
|
+
export declare function npmExecutable(): string;
|
|
10
|
+
export declare function npmArgsPrefix(): string[];
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright © 2025-2026 OpenVCS Contributors
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
+
var ownKeys = function(o) {
|
|
22
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
+
var ar = [];
|
|
24
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
+
return ar;
|
|
26
|
+
};
|
|
27
|
+
return ownKeys(o);
|
|
28
|
+
};
|
|
29
|
+
return function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.resolveNpmCli = resolveNpmCli;
|
|
39
|
+
exports.npmCommand = npmCommand;
|
|
40
|
+
exports.npmExecutable = npmExecutable;
|
|
41
|
+
exports.npmArgsPrefix = npmArgsPrefix;
|
|
42
|
+
const fs = __importStar(require("node:fs"));
|
|
43
|
+
const path = __importStar(require("node:path"));
|
|
44
|
+
function resolveNpmCli(execPath = process.execPath, exists = fs.existsSync, resolve = require.resolve) {
|
|
45
|
+
const localNodeModules = path.join(path.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
46
|
+
if (exists(localNodeModules)) {
|
|
47
|
+
return localNodeModules;
|
|
48
|
+
}
|
|
49
|
+
return resolve("npm/bin/npm-cli.js");
|
|
50
|
+
}
|
|
51
|
+
function npmCommand(platform = process.platform, execPath = process.execPath, exists = fs.existsSync, resolve = require.resolve) {
|
|
52
|
+
if (platform !== "win32") {
|
|
53
|
+
return { program: "npm", argsPrefix: [] };
|
|
54
|
+
}
|
|
55
|
+
return { program: execPath, argsPrefix: [resolveNpmCli(execPath, exists, resolve)] };
|
|
56
|
+
}
|
|
57
|
+
function npmExecutable() {
|
|
58
|
+
return npmCommand().program;
|
|
59
|
+
}
|
|
60
|
+
function npmArgsPrefix() {
|
|
61
|
+
return npmCommand().argsPrefix;
|
|
62
|
+
}
|
|
@@ -83,23 +83,31 @@ function createRuntimeDispatcher(options, host, writer) {
|
|
|
83
83
|
}
|
|
84
84
|
let result;
|
|
85
85
|
if (timeout && timeout > 0) {
|
|
86
|
-
|
|
87
|
-
const
|
|
86
|
+
let timeoutId;
|
|
87
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
88
|
+
timeoutId = setTimeout(() => {
|
|
89
|
+
reject((0, errors_1.pluginError)('request-timeout', `method '${method}' timed out after ${timeout}ms`));
|
|
90
|
+
}, timeout);
|
|
91
|
+
});
|
|
88
92
|
try {
|
|
89
|
-
result = await
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
result = await Promise.race([
|
|
94
|
+
handler(params, {
|
|
95
|
+
host,
|
|
96
|
+
requestId: id,
|
|
97
|
+
method,
|
|
98
|
+
}),
|
|
99
|
+
timeoutPromise,
|
|
100
|
+
]);
|
|
94
101
|
}
|
|
95
102
|
catch (error) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
throw (0, errors_1.pluginError)('request-timeout', `method '${method}' timed out after ${timeout}ms`);
|
|
103
|
+
if (timeoutId !== undefined) {
|
|
104
|
+
clearTimeout(timeoutId);
|
|
99
105
|
}
|
|
100
106
|
throw error;
|
|
101
107
|
}
|
|
102
|
-
|
|
108
|
+
if (timeoutId !== undefined) {
|
|
109
|
+
clearTimeout(timeoutId);
|
|
110
|
+
}
|
|
103
111
|
}
|
|
104
112
|
else {
|
|
105
113
|
result = await handler(params, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openvcs/sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1-edge.20260530.101",
|
|
4
4
|
"description": "OpenVCS SDK CLI for plugin scaffolding and runtime asset builds",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"homepage": "https://openvcs.app/",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"url": "https://github.com/Open-VCS/OpenVCS-SDK/issues"
|
|
13
13
|
},
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
15
|
+
"node": ">=20"
|
|
16
16
|
},
|
|
17
17
|
"bin": {
|
|
18
18
|
"openvcs": "bin/openvcs.js"
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "node scripts/build-sdk.js",
|
|
35
|
-
"test": "npm run build && npm run test:types && node
|
|
35
|
+
"test": "npm run build && npm run test:types && node scripts/run-tests.js",
|
|
36
|
+
"coverage": "npm run build && npm run test:types && c8 --check-coverage --lines 95 --functions 95 --branches 85 --statements 95 --reporter=text --reporter=lcov node scripts/run-tests.js",
|
|
37
|
+
"test:coverage": "npm run coverage",
|
|
36
38
|
"test:types": "node ./node_modules/typescript/bin/tsc -p test/tsconfig.json",
|
|
37
39
|
"prepack": "npm run build",
|
|
38
40
|
"preopenvcs": "npm run build",
|
|
@@ -40,6 +42,7 @@
|
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
44
|
"@types/node": "^25.3.3",
|
|
45
|
+
"c8": "^11.0.0",
|
|
43
46
|
"typescript": "^6.0.3"
|
|
44
47
|
},
|
|
45
48
|
"files": [
|
package/src/lib/build.ts
CHANGED
|
@@ -6,6 +6,9 @@ import * as path from "node:path";
|
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
|
|
8
8
|
import { isPathInside } from "./fs-utils";
|
|
9
|
+
import { npmArgsPrefix, npmExecutable } from "./npm-runner";
|
|
10
|
+
|
|
11
|
+
export { npmExecutable } from "./npm-runner";
|
|
9
12
|
|
|
10
13
|
type UsageError = Error & { code?: string };
|
|
11
14
|
|
|
@@ -34,21 +37,6 @@ interface PackageScripts {
|
|
|
34
37
|
|
|
35
38
|
const AUTHORED_PLUGIN_MODULE_BASENAME = "plugin.js";
|
|
36
39
|
|
|
37
|
-
/** Returns the npm executable name for the current platform. */
|
|
38
|
-
export function npmExecutable(): string {
|
|
39
|
-
return "npm";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Returns whether a command must be launched via the Windows shell. */
|
|
43
|
-
export function shouldUseWindowsShell(program: string): boolean {
|
|
44
|
-
if (process.platform !== "win32") {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const normalized = program.toLowerCase();
|
|
49
|
-
return normalized === "npm" || normalized.endsWith(".cmd") || normalized.endsWith(".bat");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
40
|
/** Formats help text for the build command. */
|
|
53
41
|
export function buildUsage(commandName = "openvcs"): string {
|
|
54
42
|
return `${commandName} build [args]\n\n --plugin-dir <path> Plugin repository root (contains package.json with openvcs metadata)\n -V, --verbose Enable verbose output\n`;
|
|
@@ -288,8 +276,8 @@ export function runCommand(program: string, args: string[], cwd: string, verbose
|
|
|
288
276
|
|
|
289
277
|
const result = spawnSync(program, args, {
|
|
290
278
|
cwd,
|
|
291
|
-
shell: shouldUseWindowsShell(program),
|
|
292
279
|
stdio: ["ignore", verbose ? "inherit" : "ignore", "inherit"],
|
|
280
|
+
windowsHide: true,
|
|
293
281
|
}) as CommandResult;
|
|
294
282
|
|
|
295
283
|
if (result.error) {
|
|
@@ -342,7 +330,7 @@ export function buildPluginAssets(parsedArgs: BuildArgs): ManifestInfo {
|
|
|
342
330
|
);
|
|
343
331
|
}
|
|
344
332
|
|
|
345
|
-
runCommand(npmExecutable(), ["run", "build:plugin"], parsedArgs.pluginDir, parsedArgs.verbose);
|
|
333
|
+
runCommand(npmExecutable(), [...npmArgsPrefix(), "run", "build:plugin"], parsedArgs.pluginDir, parsedArgs.verbose);
|
|
346
334
|
generateModuleBootstrap(parsedArgs.pluginDir, manifest.moduleExec);
|
|
347
335
|
validateDeclaredModuleExec(parsedArgs.pluginDir, manifest.moduleExec);
|
|
348
336
|
return manifest;
|
package/src/lib/init.ts
CHANGED
|
@@ -4,6 +4,8 @@ import * as readline from "node:readline/promises";
|
|
|
4
4
|
import { stdin, stdout } from "node:process";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
|
|
7
|
+
import { npmArgsPrefix, npmExecutable } from "./npm-runner";
|
|
8
|
+
|
|
7
9
|
const packageJson: { version: string } = require("../package.json");
|
|
8
10
|
|
|
9
11
|
type UsageError = Error & { code?: string };
|
|
@@ -33,19 +35,6 @@ interface InitCommandError {
|
|
|
33
35
|
code?: string;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
function npmExecutable(): string {
|
|
37
|
-
return "npm";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function shouldUseWindowsShell(program: string): boolean {
|
|
41
|
-
if (process.platform !== "win32") {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const normalized = program.toLowerCase();
|
|
46
|
-
return normalized === "npm" || normalized.endsWith(".cmd") || normalized.endsWith(".bat");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
38
|
export function initUsage(commandName = "openvcs"): string {
|
|
50
39
|
return `Usage: ${commandName} init [--theme] [target-dir]\n\nOptions:\n --theme Start with a theme-only plugin template\n`;
|
|
51
40
|
}
|
|
@@ -214,10 +203,10 @@ async function collectAnswers(
|
|
|
214
203
|
}
|
|
215
204
|
|
|
216
205
|
function runNpmInstall(targetDir: string): void {
|
|
217
|
-
const result = spawnSync(npmExecutable(), ["install"], {
|
|
206
|
+
const result = spawnSync(npmExecutable(), [...npmArgsPrefix(), "install"], {
|
|
218
207
|
cwd: targetDir,
|
|
219
|
-
shell: shouldUseWindowsShell(npmExecutable()),
|
|
220
208
|
stdio: "inherit",
|
|
209
|
+
windowsHide: true,
|
|
221
210
|
});
|
|
222
211
|
if (result.error) {
|
|
223
212
|
throw new Error(`failed to spawn npm install in ${targetDir}: ${result.error.message}`);
|
|
@@ -241,7 +230,6 @@ function writeModuleTemplate(answers: InitAnswers): void {
|
|
|
241
230
|
openvcs: {
|
|
242
231
|
id: answers.pluginId,
|
|
243
232
|
name: answers.pluginName,
|
|
244
|
-
version: answers.pluginVersion,
|
|
245
233
|
default_enabled: answers.defaultEnabled,
|
|
246
234
|
module: { exec: "openvcs-plugin.js" },
|
|
247
235
|
},
|
|
@@ -286,7 +274,6 @@ function writeThemeTemplate(answers: InitAnswers): void {
|
|
|
286
274
|
openvcs: {
|
|
287
275
|
id: answers.pluginId,
|
|
288
276
|
name: answers.pluginName,
|
|
289
|
-
version: answers.pluginVersion,
|
|
290
277
|
default_enabled: answers.defaultEnabled,
|
|
291
278
|
},
|
|
292
279
|
scripts: {
|
|
@@ -376,8 +363,10 @@ export function isUsageError(error: unknown): error is InitCommandError {
|
|
|
376
363
|
export const __private = {
|
|
377
364
|
collectAnswers,
|
|
378
365
|
createReadlinePromptDriver,
|
|
366
|
+
defaultPluginNameFromId,
|
|
379
367
|
defaultPluginIdFromDir,
|
|
380
368
|
sanitizeIdToken,
|
|
381
369
|
validatePluginId,
|
|
382
370
|
writeModuleTemplate,
|
|
371
|
+
writeThemeTemplate,
|
|
383
372
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Copyright © 2025-2026 OpenVCS Contributors
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
|
|
7
|
+
type ExistsFn = (path: string) => boolean;
|
|
8
|
+
type ResolveFn = (specifier: string) => string;
|
|
9
|
+
|
|
10
|
+
export interface NpmCommand {
|
|
11
|
+
program: string;
|
|
12
|
+
argsPrefix: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveNpmCli(
|
|
16
|
+
execPath = process.execPath,
|
|
17
|
+
exists: ExistsFn = fs.existsSync,
|
|
18
|
+
resolve: ResolveFn = require.resolve,
|
|
19
|
+
): string {
|
|
20
|
+
const localNodeModules = path.join(path.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
21
|
+
if (exists(localNodeModules)) {
|
|
22
|
+
return localNodeModules;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return resolve("npm/bin/npm-cli.js");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function npmCommand(
|
|
29
|
+
platform = process.platform,
|
|
30
|
+
execPath = process.execPath,
|
|
31
|
+
exists: ExistsFn = fs.existsSync,
|
|
32
|
+
resolve: ResolveFn = require.resolve,
|
|
33
|
+
): NpmCommand {
|
|
34
|
+
if (platform !== "win32") {
|
|
35
|
+
return { program: "npm", argsPrefix: [] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { program: execPath, argsPrefix: [resolveNpmCli(execPath, exists, resolve)] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function npmExecutable(): string {
|
|
42
|
+
return npmCommand().program;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function npmArgsPrefix(): string[] {
|
|
46
|
+
return npmCommand().argsPrefix;
|
|
47
|
+
}
|
|
@@ -133,22 +133,30 @@ export function createRuntimeDispatcher(
|
|
|
133
133
|
|
|
134
134
|
let result: unknown;
|
|
135
135
|
if (timeout && timeout > 0) {
|
|
136
|
-
|
|
137
|
-
const
|
|
136
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
137
|
+
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
138
|
+
timeoutId = setTimeout(() => {
|
|
139
|
+
reject(pluginError('request-timeout', `method '${method}' timed out after ${timeout}ms`));
|
|
140
|
+
}, timeout);
|
|
141
|
+
});
|
|
138
142
|
try {
|
|
139
|
-
result = await
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
result = await Promise.race([
|
|
144
|
+
handler(params, {
|
|
145
|
+
host,
|
|
146
|
+
requestId: id,
|
|
147
|
+
method,
|
|
148
|
+
}),
|
|
149
|
+
timeoutPromise,
|
|
150
|
+
]);
|
|
144
151
|
} catch (error) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
throw pluginError('request-timeout', `method '${method}' timed out after ${timeout}ms`);
|
|
152
|
+
if (timeoutId !== undefined) {
|
|
153
|
+
clearTimeout(timeoutId);
|
|
148
154
|
}
|
|
149
155
|
throw error;
|
|
150
156
|
}
|
|
151
|
-
|
|
157
|
+
if (timeoutId !== undefined) {
|
|
158
|
+
clearTimeout(timeoutId);
|
|
159
|
+
}
|
|
152
160
|
} else {
|
|
153
161
|
result = await handler(params, {
|
|
154
162
|
host,
|
package/test/build.test.js
CHANGED
|
@@ -4,9 +4,13 @@ const path = require("node:path");
|
|
|
4
4
|
const test = require("node:test");
|
|
5
5
|
|
|
6
6
|
const {
|
|
7
|
+
authoredPluginModulePath,
|
|
7
8
|
buildPluginAssets,
|
|
9
|
+
generateModuleBootstrap,
|
|
10
|
+
hasPackageJson,
|
|
8
11
|
parseBuildArgs,
|
|
9
12
|
readManifest,
|
|
13
|
+
runCommand,
|
|
10
14
|
validateDeclaredModuleExec,
|
|
11
15
|
validateGeneratedBootstrapTargets,
|
|
12
16
|
} = require("../lib/build");
|
|
@@ -48,6 +52,10 @@ test("parseBuildArgs help returns usage error", () => {
|
|
|
48
52
|
assert.throws(() => parseBuildArgs(["--help"]), /openvcs build \[args\]/);
|
|
49
53
|
});
|
|
50
54
|
|
|
55
|
+
test("parseBuildArgs rejects unknown flags", () => {
|
|
56
|
+
assert.throws(() => parseBuildArgs(["--wat"]), /unknown flag: --wat/);
|
|
57
|
+
});
|
|
58
|
+
|
|
51
59
|
test("buildPluginAssets no-ops for theme-only plugins", () => {
|
|
52
60
|
const root = makeTempDir("openvcs-sdk-test");
|
|
53
61
|
const pluginDir = path.join(root, "plugin");
|
|
@@ -136,6 +144,73 @@ test("readManifest and validateDeclaredModuleExec stay reusable", () => {
|
|
|
136
144
|
cleanupTempDir(root);
|
|
137
145
|
});
|
|
138
146
|
|
|
147
|
+
test("readManifest reports missing and invalid package manifests", () => {
|
|
148
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
149
|
+
const missingDir = path.join(root, "missing");
|
|
150
|
+
fs.mkdirSync(missingDir, { recursive: true });
|
|
151
|
+
assert.throws(() => readManifest(missingDir), /missing package\.json/);
|
|
152
|
+
|
|
153
|
+
const invalidDir = path.join(root, "invalid");
|
|
154
|
+
writeText(path.join(invalidDir, "package.json"), "{");
|
|
155
|
+
assert.throws(() => readManifest(invalidDir), /parse .*package\.json/);
|
|
156
|
+
|
|
157
|
+
const noOpenVcs = path.join(root, "no-openvcs");
|
|
158
|
+
writeJson(path.join(noOpenVcs, "package.json"), { name: "x" });
|
|
159
|
+
assert.throws(() => readManifest(noOpenVcs), /missing an 'openvcs' object/);
|
|
160
|
+
|
|
161
|
+
const badId = path.join(root, "bad-id");
|
|
162
|
+
writeJson(path.join(badId, "package.json"), { openvcs: { id: "bad/id" } });
|
|
163
|
+
assert.throws(() => readManifest(badId), /must not contain path separators/);
|
|
164
|
+
|
|
165
|
+
const missingId = path.join(root, "missing-id");
|
|
166
|
+
writeJson(path.join(missingId, "package.json"), { openvcs: {} });
|
|
167
|
+
assert.throws(() => readManifest(missingId), /missing openvcs\.id/);
|
|
168
|
+
|
|
169
|
+
cleanupTempDir(root);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("validateDeclaredModuleExec rejects invalid module paths", () => {
|
|
173
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
174
|
+
const pluginDir = path.join(root, "plugin");
|
|
175
|
+
fs.mkdirSync(path.join(pluginDir, "bin"), { recursive: true });
|
|
176
|
+
|
|
177
|
+
assert.doesNotThrow(() => validateDeclaredModuleExec(pluginDir, undefined));
|
|
178
|
+
assert.throws(() => validateDeclaredModuleExec(pluginDir, "native.node"), /must end with/);
|
|
179
|
+
assert.throws(() => validateDeclaredModuleExec(pluginDir, path.join(pluginDir, "bin", "x.js")), /must be a relative path/);
|
|
180
|
+
assert.throws(() => validateDeclaredModuleExec(pluginDir, "../escape.js"), /must point to a file under bin/);
|
|
181
|
+
assert.throws(() => validateDeclaredModuleExec(pluginDir, "missing.js"), /module entrypoint not found/);
|
|
182
|
+
|
|
183
|
+
cleanupTempDir(root);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("renderGeneratedBootstrap rejects unsafe import paths", () => {
|
|
187
|
+
assert.throws(() => require("../lib/build").renderGeneratedBootstrap("./bad path.js", true), /unsafe module import path/);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("build helpers handle no-op and package existence paths", () => {
|
|
191
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
192
|
+
const pluginDir = path.join(root, "plugin");
|
|
193
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
194
|
+
|
|
195
|
+
assert.equal(hasPackageJson(pluginDir), false);
|
|
196
|
+
writeJson(path.join(pluginDir, "package.json"), { openvcs: { id: "x" } });
|
|
197
|
+
assert.equal(hasPackageJson(pluginDir), true);
|
|
198
|
+
assert.equal(authoredPluginModulePath(pluginDir), path.join(pluginDir, "bin", "plugin.js"));
|
|
199
|
+
assert.doesNotThrow(() => generateModuleBootstrap(pluginDir, undefined));
|
|
200
|
+
|
|
201
|
+
cleanupTempDir(root);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("runCommand reports spawn failures and non-zero exits", () => {
|
|
205
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
206
|
+
|
|
207
|
+
assert.doesNotThrow(() => runCommand(process.execPath, ["-e", "process.exit(0)"], root, true));
|
|
208
|
+
assert.throws(() => runCommand(process.execPath, ["-e", "process.exit(7)"], root, false), /exit code 7/);
|
|
209
|
+
assert.throws(() => runCommand(path.join(root, "missing-binary"), [], root, false), /failed to spawn/);
|
|
210
|
+
|
|
211
|
+
cleanupTempDir(root);
|
|
212
|
+
});
|
|
213
|
+
|
|
139
214
|
test("validateGeneratedBootstrapTargets rejects module.exec collisions", () => {
|
|
140
215
|
const root = makeTempDir("openvcs-sdk-test");
|
|
141
216
|
const pluginDir = path.join(root, "plugin");
|
|
@@ -168,6 +243,30 @@ test("validateGeneratedBootstrapTargets rejects case-insensitive collisions", ()
|
|
|
168
243
|
cleanupTempDir(root);
|
|
169
244
|
});
|
|
170
245
|
|
|
246
|
+
test("validateGeneratedBootstrapTargets no-ops without module exec and rejects missing compiled module", () => {
|
|
247
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
248
|
+
const pluginDir = path.join(root, "plugin");
|
|
249
|
+
fs.mkdirSync(path.join(pluginDir, "bin"), { recursive: true });
|
|
250
|
+
|
|
251
|
+
assert.doesNotThrow(() => validateGeneratedBootstrapTargets(pluginDir, undefined));
|
|
252
|
+
assert.throws(() => validateGeneratedBootstrapTargets(pluginDir, "openvcs-plugin.js"), /compiled plugin module not found/);
|
|
253
|
+
|
|
254
|
+
cleanupTempDir(root);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("generateModuleBootstrap tolerates invalid package json and uses extension for ESM", () => {
|
|
258
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
259
|
+
const pluginDir = path.join(root, "plugin");
|
|
260
|
+
writeText(path.join(pluginDir, "package.json"), "{");
|
|
261
|
+
writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
|
|
262
|
+
writeText(path.join(pluginDir, "bin", "bootstrap.mjs"), "");
|
|
263
|
+
|
|
264
|
+
generateModuleBootstrap(pluginDir, "bootstrap.mjs");
|
|
265
|
+
|
|
266
|
+
assert.match(fs.readFileSync(path.join(pluginDir, "bin", "bootstrap.mjs"), "utf8"), /^import/m);
|
|
267
|
+
cleanupTempDir(root);
|
|
268
|
+
});
|
|
269
|
+
|
|
171
270
|
test("generateModuleBootstrap handles subdirectory module.exec paths", () => {
|
|
172
271
|
const root = makeTempDir("openvcs-sdk-test");
|
|
173
272
|
const pluginDir = path.join(root, "plugin");
|