@openvcs/sdk 0.2.1 → 0.2.3
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 +48 -6
- 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 -104
- package/lib/init.d.ts +30 -0
- package/lib/init.js +76 -41
- package/package.json +3 -3
- package/src/lib/build.ts +229 -0
- package/src/lib/cli.ts +21 -2
- package/src/lib/dist.ts +76 -125
- package/src/lib/init.ts +86 -50
- 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 +65 -0
package/lib/init.d.ts
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
|
+
interface InitAnswers {
|
|
2
|
+
targetDir: string;
|
|
3
|
+
kind: "module" | "theme";
|
|
4
|
+
pluginId: string;
|
|
5
|
+
pluginName: string;
|
|
6
|
+
pluginVersion: string;
|
|
7
|
+
defaultEnabled: boolean;
|
|
8
|
+
runNpmInstall: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface CollectAnswersOptions {
|
|
11
|
+
forceTheme: boolean;
|
|
12
|
+
targetHint?: string;
|
|
13
|
+
}
|
|
14
|
+
interface PromptDriver {
|
|
15
|
+
promptText(label: string, defaultValue?: string): Promise<string>;
|
|
16
|
+
promptBoolean(label: string, defaultValue: boolean): Promise<boolean>;
|
|
17
|
+
close(): void;
|
|
18
|
+
}
|
|
1
19
|
interface InitCommandError {
|
|
2
20
|
code?: string;
|
|
3
21
|
}
|
|
4
22
|
export declare function initUsage(commandName?: string): string;
|
|
23
|
+
declare function sanitizeIdToken(raw: string): string;
|
|
24
|
+
declare function defaultPluginIdFromDir(targetDir: string): string;
|
|
25
|
+
declare function validatePluginId(pluginId: string): string | undefined;
|
|
26
|
+
declare function createReadlinePromptDriver(output?: NodeJS.WritableStream): PromptDriver;
|
|
27
|
+
declare function collectAnswers({ forceTheme, targetHint }: CollectAnswersOptions, promptDriver?: PromptDriver, output?: NodeJS.WritableStream): Promise<InitAnswers>;
|
|
5
28
|
export declare function runInitCommand(args: string[]): Promise<string>;
|
|
6
29
|
export declare function isUsageError(error: unknown): error is InitCommandError;
|
|
30
|
+
export declare const __private: {
|
|
31
|
+
collectAnswers: typeof collectAnswers;
|
|
32
|
+
createReadlinePromptDriver: typeof createReadlinePromptDriver;
|
|
33
|
+
defaultPluginIdFromDir: typeof defaultPluginIdFromDir;
|
|
34
|
+
sanitizeIdToken: typeof sanitizeIdToken;
|
|
35
|
+
validatePluginId: typeof validatePluginId;
|
|
36
|
+
};
|
|
7
37
|
export {};
|
package/lib/init.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.__private = void 0;
|
|
3
4
|
exports.initUsage = initUsage;
|
|
4
5
|
exports.runInitCommand = runInitCommand;
|
|
5
6
|
exports.isUsageError = isUsageError;
|
|
@@ -45,28 +46,48 @@ function defaultPluginNameFromId(pluginId) {
|
|
|
45
46
|
.map((word) => word[0].toUpperCase() + word.slice(1));
|
|
46
47
|
return words.length > 0 ? words.join(" ") : "OpenVCS Plugin";
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
async function promptBoolean(rl, label, defaultValue) {
|
|
55
|
-
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
56
|
-
while (true) {
|
|
57
|
-
const answer = await rl.question(`${label} (${suffix}): `);
|
|
58
|
-
const normalized = answer.trim().toLowerCase();
|
|
59
|
-
if (!normalized) {
|
|
60
|
-
return defaultValue;
|
|
61
|
-
}
|
|
62
|
-
if (normalized === "y" || normalized === "yes") {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
if (normalized === "n" || normalized === "no") {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
process.stderr.write("Please answer yes or no.\n");
|
|
49
|
+
function validatePluginId(pluginId) {
|
|
50
|
+
if (!pluginId) {
|
|
51
|
+
return "Plugin id is required.";
|
|
52
|
+
}
|
|
53
|
+
if (pluginId === "." || pluginId === "..") {
|
|
54
|
+
return "Plugin id must not be '.' or '..'.";
|
|
69
55
|
}
|
|
56
|
+
if (pluginId.includes("/") || pluginId.includes("\\")) {
|
|
57
|
+
return "Plugin id must not contain path separators (/ or \\).";
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
function createReadlinePromptDriver(output = process.stderr) {
|
|
62
|
+
const rl = readline.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
|
|
63
|
+
return {
|
|
64
|
+
async promptText(label, defaultValue = "") {
|
|
65
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
66
|
+
const answer = await rl.question(`${label}${suffix}: `);
|
|
67
|
+
const trimmed = answer.trim();
|
|
68
|
+
return trimmed || defaultValue;
|
|
69
|
+
},
|
|
70
|
+
async promptBoolean(label, defaultValue) {
|
|
71
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
72
|
+
while (true) {
|
|
73
|
+
const answer = await rl.question(`${label} (${suffix}): `);
|
|
74
|
+
const normalized = answer.trim().toLowerCase();
|
|
75
|
+
if (!normalized) {
|
|
76
|
+
return defaultValue;
|
|
77
|
+
}
|
|
78
|
+
if (normalized === "y" || normalized === "yes") {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (normalized === "n" || normalized === "no") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
output.write("Please answer yes or no.\n");
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
close() {
|
|
88
|
+
rl.close();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
70
91
|
}
|
|
71
92
|
function writeJson(filePath, value) {
|
|
72
93
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -80,11 +101,10 @@ function directoryHasEntries(targetDir) {
|
|
|
80
101
|
const entries = fs.readdirSync(targetDir);
|
|
81
102
|
return entries.length > 0;
|
|
82
103
|
}
|
|
83
|
-
async function collectAnswers({ forceTheme, targetHint }) {
|
|
104
|
+
async function collectAnswers({ forceTheme, targetHint }, promptDriver = createReadlinePromptDriver(), output = process.stderr) {
|
|
84
105
|
const defaultTarget = targetHint || path.join(process.cwd(), "openvcs-plugin");
|
|
85
|
-
const rl = readline.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
|
|
86
106
|
try {
|
|
87
|
-
const targetText = await promptText(
|
|
107
|
+
const targetText = await promptDriver.promptText("Target directory", defaultTarget);
|
|
88
108
|
const targetDir = path.resolve(targetText);
|
|
89
109
|
let kind = "module";
|
|
90
110
|
if (forceTheme) {
|
|
@@ -92,7 +112,7 @@ async function collectAnswers({ forceTheme, targetHint }) {
|
|
|
92
112
|
}
|
|
93
113
|
else {
|
|
94
114
|
while (true) {
|
|
95
|
-
const value = (await promptText(
|
|
115
|
+
const value = (await promptDriver.promptText("Template type (module/theme)", "module"))
|
|
96
116
|
.trim()
|
|
97
117
|
.toLowerCase();
|
|
98
118
|
if (value === "module" || value === "m") {
|
|
@@ -103,25 +123,31 @@ async function collectAnswers({ forceTheme, targetHint }) {
|
|
|
103
123
|
kind = "theme";
|
|
104
124
|
break;
|
|
105
125
|
}
|
|
106
|
-
|
|
126
|
+
output.write("Please choose 'module' or 'theme'.\n");
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
const defaultId = defaultPluginIdFromDir(targetDir);
|
|
110
|
-
let pluginId
|
|
130
|
+
let pluginId;
|
|
111
131
|
while (!pluginId) {
|
|
112
|
-
|
|
132
|
+
const candidateId = (await promptDriver.promptText("Plugin id", defaultId)).trim();
|
|
133
|
+
const validationError = validatePluginId(candidateId);
|
|
134
|
+
if (!validationError) {
|
|
135
|
+
pluginId = candidateId;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
output.write(`${validationError}\n`);
|
|
113
139
|
}
|
|
114
140
|
const defaultName = defaultPluginNameFromId(pluginId);
|
|
115
141
|
let pluginName = "";
|
|
116
142
|
while (!pluginName) {
|
|
117
|
-
pluginName = (await promptText(
|
|
143
|
+
pluginName = (await promptDriver.promptText("Plugin name", defaultName)).trim();
|
|
118
144
|
}
|
|
119
145
|
let pluginVersion = "";
|
|
120
146
|
while (!pluginVersion) {
|
|
121
|
-
pluginVersion = (await promptText(
|
|
147
|
+
pluginVersion = (await promptDriver.promptText("Version", "0.1.0")).trim();
|
|
122
148
|
}
|
|
123
|
-
const defaultEnabled = await promptBoolean(
|
|
124
|
-
const runNpmInstall = await promptBoolean(
|
|
149
|
+
const defaultEnabled = await promptDriver.promptBoolean("Default enabled", true);
|
|
150
|
+
const runNpmInstall = await promptDriver.promptBoolean("Run npm install now", true);
|
|
125
151
|
return {
|
|
126
152
|
targetDir,
|
|
127
153
|
kind,
|
|
@@ -133,7 +159,7 @@ async function collectAnswers({ forceTheme, targetHint }) {
|
|
|
133
159
|
};
|
|
134
160
|
}
|
|
135
161
|
finally {
|
|
136
|
-
|
|
162
|
+
promptDriver.close();
|
|
137
163
|
}
|
|
138
164
|
}
|
|
139
165
|
function runNpmInstall(targetDir) {
|
|
@@ -166,9 +192,10 @@ function writeModuleTemplate(answers) {
|
|
|
166
192
|
private: true,
|
|
167
193
|
type: "module",
|
|
168
194
|
scripts: {
|
|
169
|
-
"build:
|
|
170
|
-
build: "
|
|
171
|
-
|
|
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",
|
|
172
199
|
},
|
|
173
200
|
devDependencies: {
|
|
174
201
|
"@openvcs/sdk": `^${packageJson.version}`,
|
|
@@ -198,8 +225,9 @@ function writeThemeTemplate(answers) {
|
|
|
198
225
|
version: answers.pluginVersion,
|
|
199
226
|
private: true,
|
|
200
227
|
scripts: {
|
|
201
|
-
build: "openvcs
|
|
202
|
-
|
|
228
|
+
build: "openvcs build",
|
|
229
|
+
dist: "openvcs dist --plugin-dir . --out dist",
|
|
230
|
+
test: "openvcs dist --plugin-dir . --out dist --no-build --no-npm-deps",
|
|
203
231
|
},
|
|
204
232
|
devDependencies: {
|
|
205
233
|
"@openvcs/sdk": `^${packageJson.version}`,
|
|
@@ -244,15 +272,15 @@ async function runInitCommand(args) {
|
|
|
244
272
|
throw new Error(`target path exists but is not a directory: ${answers.targetDir}`);
|
|
245
273
|
}
|
|
246
274
|
else if (directoryHasEntries(answers.targetDir)) {
|
|
247
|
-
const
|
|
275
|
+
const promptDriver = createReadlinePromptDriver();
|
|
248
276
|
try {
|
|
249
|
-
const proceed = await promptBoolean(
|
|
277
|
+
const proceed = await promptDriver.promptBoolean(`Directory ${answers.targetDir} is not empty. Continue and overwrite known files`, false);
|
|
250
278
|
if (!proceed) {
|
|
251
279
|
throw new Error("aborted by user");
|
|
252
280
|
}
|
|
253
281
|
}
|
|
254
282
|
finally {
|
|
255
|
-
|
|
283
|
+
promptDriver.close();
|
|
256
284
|
}
|
|
257
285
|
}
|
|
258
286
|
if (answers.kind === "module") {
|
|
@@ -272,3 +300,10 @@ function isUsageError(error) {
|
|
|
272
300
|
"code" in error &&
|
|
273
301
|
typeof error.code === "string");
|
|
274
302
|
}
|
|
303
|
+
exports.__private = {
|
|
304
|
+
collectAnswers,
|
|
305
|
+
createReadlinePromptDriver,
|
|
306
|
+
defaultPluginIdFromDir,
|
|
307
|
+
sanitizeIdToken,
|
|
308
|
+
validatePluginId,
|
|
309
|
+
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openvcs/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "OpenVCS SDK CLI for plugin scaffolding and .ovcsp tar.gz packaging",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
|
-
"homepage": "https://
|
|
6
|
+
"homepage": "https://openvcs.app/",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/Open-VCS/OpenVCS-SDK"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"openvcs": "node bin/openvcs.js"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^
|
|
28
|
+
"@types/node": "^25.3.3",
|
|
29
29
|
"typescript": "^5.8.2"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
package/src/lib/build.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
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
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
import { isPathInside } from "./fs-utils";
|
|
9
|
+
|
|
10
|
+
type UsageError = Error & { code?: string };
|
|
11
|
+
|
|
12
|
+
/** CLI arguments for `openvcs build`. */
|
|
13
|
+
export interface BuildArgs {
|
|
14
|
+
pluginDir: string;
|
|
15
|
+
verbose: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Trimmed manifest details used by build and dist flows. */
|
|
19
|
+
export interface ManifestInfo {
|
|
20
|
+
pluginId: string;
|
|
21
|
+
moduleExec: string | undefined;
|
|
22
|
+
entry: string | undefined;
|
|
23
|
+
manifestPath: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CommandResult {
|
|
27
|
+
status: number | null;
|
|
28
|
+
error?: Error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface PackageScripts {
|
|
32
|
+
[scriptName: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Returns the npm executable name for the current platform. */
|
|
36
|
+
export function npmExecutable(): string {
|
|
37
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Formats help text for the build command. */
|
|
41
|
+
export function buildUsage(commandName = "openvcs"): string {
|
|
42
|
+
return `${commandName} build [args]\n\n --plugin-dir <path> Plugin repository root (contains openvcs.plugin.json)\n -V, --verbose Enable verbose output\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Parses `openvcs build` arguments. */
|
|
46
|
+
export function parseBuildArgs(args: string[]): BuildArgs {
|
|
47
|
+
let pluginDir = process.cwd();
|
|
48
|
+
let verbose = false;
|
|
49
|
+
|
|
50
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
51
|
+
const arg = args[index];
|
|
52
|
+
if (arg === "--plugin-dir") {
|
|
53
|
+
index += 1;
|
|
54
|
+
if (index >= args.length) {
|
|
55
|
+
throw new Error("missing value for --plugin-dir");
|
|
56
|
+
}
|
|
57
|
+
pluginDir = args[index] as string;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (arg === "-V" || arg === "--verbose") {
|
|
61
|
+
verbose = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (arg === "--help") {
|
|
65
|
+
const error = new Error(buildUsage()) as UsageError;
|
|
66
|
+
error.code = "USAGE";
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`unknown flag: ${arg}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
pluginDir: path.resolve(pluginDir),
|
|
74
|
+
verbose,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Reads and validates the plugin manifest. */
|
|
79
|
+
export function readManifest(pluginDir: string): ManifestInfo {
|
|
80
|
+
const manifestPath = path.join(pluginDir, "openvcs.plugin.json");
|
|
81
|
+
let manifestRaw: string;
|
|
82
|
+
let manifestFd: number | undefined;
|
|
83
|
+
let manifest: unknown;
|
|
84
|
+
try {
|
|
85
|
+
manifestFd = fs.openSync(manifestPath, "r");
|
|
86
|
+
const manifestStat = fs.fstatSync(manifestFd);
|
|
87
|
+
if (!manifestStat.isFile()) {
|
|
88
|
+
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
89
|
+
}
|
|
90
|
+
manifestRaw = fs.readFileSync(manifestFd, "utf8");
|
|
91
|
+
} catch (error: unknown) {
|
|
92
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
93
|
+
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
} finally {
|
|
97
|
+
if (typeof manifestFd === "number") {
|
|
98
|
+
fs.closeSync(manifestFd);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
manifest = JSON.parse(manifestRaw);
|
|
104
|
+
} catch (error: unknown) {
|
|
105
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
106
|
+
throw new Error(`parse ${manifestPath}: ${detail}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const pluginId =
|
|
110
|
+
typeof (manifest as { id?: unknown }).id === "string"
|
|
111
|
+
? ((manifest as { id: string }).id.trim() as string)
|
|
112
|
+
: "";
|
|
113
|
+
if (!pluginId) {
|
|
114
|
+
throw new Error(`manifest ${manifestPath} is missing a string 'id'`);
|
|
115
|
+
}
|
|
116
|
+
if (pluginId === "." || pluginId === ".." || pluginId.includes("/") || pluginId.includes("\\")) {
|
|
117
|
+
throw new Error(`manifest id must not contain path separators: ${pluginId}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const moduleValue = (manifest as { module?: { exec?: unknown } }).module;
|
|
121
|
+
const moduleExec = typeof moduleValue?.exec === "string" ? moduleValue.exec.trim() : undefined;
|
|
122
|
+
|
|
123
|
+
const entryValue = (manifest as { entry?: unknown }).entry;
|
|
124
|
+
const entry = typeof entryValue === "string" ? entryValue.trim() : undefined;
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
pluginId,
|
|
128
|
+
moduleExec,
|
|
129
|
+
entry,
|
|
130
|
+
manifestPath,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Verifies that a declared module entry resolves to a real file under `bin/`. */
|
|
135
|
+
export function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined): void {
|
|
136
|
+
if (!moduleExec) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const normalizedExec = moduleExec.trim();
|
|
141
|
+
const lowered = normalizedExec.toLowerCase();
|
|
142
|
+
if (!lowered.endsWith(".js") && !lowered.endsWith(".mjs") && !lowered.endsWith(".cjs")) {
|
|
143
|
+
throw new Error(`manifest exec must end with .js/.mjs/.cjs (Node runtime): ${moduleExec}`);
|
|
144
|
+
}
|
|
145
|
+
if (path.isAbsolute(normalizedExec)) {
|
|
146
|
+
throw new Error(`manifest module.exec must be a relative path under bin/: ${moduleExec}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const binDir = path.resolve(pluginDir, "bin");
|
|
150
|
+
const targetPath = path.resolve(binDir, normalizedExec);
|
|
151
|
+
if (!isPathInside(binDir, targetPath) || targetPath === binDir) {
|
|
152
|
+
throw new Error(`manifest module.exec must point to a file under bin/: ${moduleExec}`);
|
|
153
|
+
}
|
|
154
|
+
if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
|
|
155
|
+
throw new Error(`module entrypoint not found at ${targetPath}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Returns whether the plugin repository has a `package.json`. */
|
|
160
|
+
export function hasPackageJson(pluginDir: string): boolean {
|
|
161
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
162
|
+
return fs.existsSync(packageJsonPath) && fs.lstatSync(packageJsonPath).isFile();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Runs a command in the given directory with optional verbose logging. */
|
|
166
|
+
export function runCommand(program: string, args: string[], cwd: string, verbose: boolean): void {
|
|
167
|
+
if (verbose) {
|
|
168
|
+
process.stderr.write(`Running command in ${cwd}: ${program} ${args.join(" ")}\n`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result = spawnSync(program, args, {
|
|
172
|
+
cwd,
|
|
173
|
+
stdio: ["ignore", verbose ? "inherit" : "ignore", "inherit"],
|
|
174
|
+
}) as CommandResult;
|
|
175
|
+
|
|
176
|
+
if (result.error) {
|
|
177
|
+
throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
|
|
178
|
+
}
|
|
179
|
+
if (result.status === 0) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
throw new Error(`command failed (${program} ${args.join(" ")}), exit code ${result.status}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Reads `package.json` scripts for the plugin, if present. */
|
|
187
|
+
function readPackageScripts(pluginDir: string): PackageScripts {
|
|
188
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
189
|
+
let packageData: unknown;
|
|
190
|
+
try {
|
|
191
|
+
packageData = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
192
|
+
} catch (error: unknown) {
|
|
193
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
194
|
+
throw new Error(`code plugins must include package.json: ${packageJsonPath}`);
|
|
195
|
+
}
|
|
196
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
197
|
+
throw new Error(`parse ${packageJsonPath}: ${detail}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const scripts = (packageData as { scripts?: unknown }).scripts;
|
|
201
|
+
if (typeof scripts !== "object" || scripts === null) {
|
|
202
|
+
return {};
|
|
203
|
+
}
|
|
204
|
+
return scripts as PackageScripts;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Builds a plugin's runtime assets when it declares a code module. */
|
|
208
|
+
export function buildPluginAssets(parsedArgs: BuildArgs): ManifestInfo {
|
|
209
|
+
const manifest = readManifest(parsedArgs.pluginDir);
|
|
210
|
+
if (!manifest.moduleExec) {
|
|
211
|
+
return manifest;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!hasPackageJson(parsedArgs.pluginDir)) {
|
|
215
|
+
throw new Error(`code plugins must include package.json: ${path.join(parsedArgs.pluginDir, "package.json")}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const scripts = readPackageScripts(parsedArgs.pluginDir);
|
|
219
|
+
const buildScript = scripts["build:plugin"];
|
|
220
|
+
if (typeof buildScript !== "string" || buildScript.trim() === "") {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`code plugins must define scripts[\"build:plugin\"] in ${path.join(parsedArgs.pluginDir, "package.json")}`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
runCommand(npmExecutable(), ["run", "build:plugin"], parsedArgs.pluginDir, parsedArgs.verbose);
|
|
227
|
+
validateDeclaredModuleExec(parsedArgs.pluginDir, manifest.moduleExec);
|
|
228
|
+
return manifest;
|
|
229
|
+
}
|
package/src/lib/cli.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildPluginAssets, buildUsage, parseBuildArgs } from "./build";
|
|
1
2
|
import { bundlePlugin, distUsage, parseDistArgs } from "./dist";
|
|
2
3
|
import { initUsage, runInitCommand } from "./init";
|
|
3
4
|
|
|
@@ -17,7 +18,7 @@ function hasCode(error: unknown, code: string): boolean {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function usage(): string {
|
|
20
|
-
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";
|
|
21
|
+
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";
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export async function runCli(args: string[]): Promise<void> {
|
|
@@ -27,7 +28,7 @@ export async function runCli(args: string[]): Promise<void> {
|
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
if (args
|
|
31
|
+
if (args[0] === "-v" || args[0] === "--version") {
|
|
31
32
|
process.stdout.write(`openvcs ${packageJson.version}\n`);
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
@@ -56,6 +57,24 @@ export async function runCli(args: string[]): Promise<void> {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
if (command === "build") {
|
|
61
|
+
if (rest.includes("--help")) {
|
|
62
|
+
process.stdout.write(buildUsage());
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const parsed = parseBuildArgs(rest);
|
|
67
|
+
const manifest = buildPluginAssets(parsed);
|
|
68
|
+
process.stdout.write(`${manifest.pluginId}\n`);
|
|
69
|
+
return;
|
|
70
|
+
} catch (error: unknown) {
|
|
71
|
+
if (hasCode(error, "USAGE")) {
|
|
72
|
+
throw new Error(buildUsage());
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
if (command === "init") {
|
|
60
79
|
if (rest.includes("--help")) {
|
|
61
80
|
process.stdout.write(initUsage());
|