@openvcs/sdk 0.2.0 → 0.2.2
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 +63 -6
- package/bin/openvcs.d.ts +2 -0
- package/bin/openvcs.js +9 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +67 -0
- package/lib/dist.d.ts +30 -0
- package/lib/dist.js +286 -0
- package/lib/fs-utils.d.ts +5 -0
- package/lib/fs-utils.js +76 -0
- package/lib/init.d.ts +37 -0
- package/lib/init.js +307 -0
- package/package.json +17 -6
- package/src/bin/openvcs.ts +9 -0
- package/src/lib/cli.ts +77 -0
- package/src/lib/dist.ts +356 -0
- package/src/lib/fs-utils.ts +78 -0
- package/src/lib/init.ts +364 -0
- package/test/cli.test.js +87 -0
- package/test/dist.test.js +456 -0
- package/test/fs-utils.test.js +124 -0
- package/test/helpers.js +49 -0
- package/test/init.test.js +65 -0
- package/bin/openvcs-sdk.js +0 -26
- package/lib/resolve-binary.js +0 -29
- package/scripts/postinstall.js +0 -115
package/lib/init.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.__private = void 0;
|
|
4
|
+
exports.initUsage = initUsage;
|
|
5
|
+
exports.runInitCommand = runInitCommand;
|
|
6
|
+
exports.isUsageError = isUsageError;
|
|
7
|
+
const fs = require("node:fs");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
const readline = require("node:readline/promises");
|
|
10
|
+
const node_process_1 = require("node:process");
|
|
11
|
+
const node_child_process_1 = require("node:child_process");
|
|
12
|
+
const packageJson = require("../package.json");
|
|
13
|
+
function npmExecutable() {
|
|
14
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
15
|
+
}
|
|
16
|
+
function initUsage(commandName = "openvcs") {
|
|
17
|
+
return `Usage: ${commandName} init [--theme] [target-dir]\n\nOptions:\n --theme Start with a theme-only plugin template\n`;
|
|
18
|
+
}
|
|
19
|
+
function sanitizeIdToken(raw) {
|
|
20
|
+
let output = "";
|
|
21
|
+
let lastWasSeparator = false;
|
|
22
|
+
for (const char of raw) {
|
|
23
|
+
const isValid = /[a-zA-Z0-9._-]/.test(char);
|
|
24
|
+
if (isValid) {
|
|
25
|
+
output += char.toLowerCase();
|
|
26
|
+
lastWasSeparator = false;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!lastWasSeparator) {
|
|
30
|
+
output += "-";
|
|
31
|
+
lastWasSeparator = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return output.replace(/^-+|-+$/g, "");
|
|
35
|
+
}
|
|
36
|
+
function defaultPluginIdFromDir(targetDir) {
|
|
37
|
+
const name = path.basename(targetDir) || "openvcs-plugin";
|
|
38
|
+
const token = sanitizeIdToken(name);
|
|
39
|
+
return token || "openvcs.plugin";
|
|
40
|
+
}
|
|
41
|
+
function defaultPluginNameFromId(pluginId) {
|
|
42
|
+
const words = pluginId
|
|
43
|
+
.split(/[._-]+/)
|
|
44
|
+
.map((word) => word.trim())
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.map((word) => word[0].toUpperCase() + word.slice(1));
|
|
47
|
+
return words.length > 0 ? words.join(" ") : "OpenVCS Plugin";
|
|
48
|
+
}
|
|
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 '..'.";
|
|
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
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function writeJson(filePath, value) {
|
|
93
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
94
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
95
|
+
}
|
|
96
|
+
function writeText(filePath, content) {
|
|
97
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
98
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
99
|
+
}
|
|
100
|
+
function directoryHasEntries(targetDir) {
|
|
101
|
+
const entries = fs.readdirSync(targetDir);
|
|
102
|
+
return entries.length > 0;
|
|
103
|
+
}
|
|
104
|
+
async function collectAnswers({ forceTheme, targetHint }, promptDriver = createReadlinePromptDriver(), output = process.stderr) {
|
|
105
|
+
const defaultTarget = targetHint || path.join(process.cwd(), "openvcs-plugin");
|
|
106
|
+
try {
|
|
107
|
+
const targetText = await promptDriver.promptText("Target directory", defaultTarget);
|
|
108
|
+
const targetDir = path.resolve(targetText);
|
|
109
|
+
let kind = "module";
|
|
110
|
+
if (forceTheme) {
|
|
111
|
+
kind = "theme";
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
while (true) {
|
|
115
|
+
const value = (await promptDriver.promptText("Template type (module/theme)", "module"))
|
|
116
|
+
.trim()
|
|
117
|
+
.toLowerCase();
|
|
118
|
+
if (value === "module" || value === "m") {
|
|
119
|
+
kind = "module";
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
if (value === "theme" || value === "t") {
|
|
123
|
+
kind = "theme";
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
output.write("Please choose 'module' or 'theme'.\n");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const defaultId = defaultPluginIdFromDir(targetDir);
|
|
130
|
+
let pluginId;
|
|
131
|
+
while (!pluginId) {
|
|
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`);
|
|
139
|
+
}
|
|
140
|
+
const defaultName = defaultPluginNameFromId(pluginId);
|
|
141
|
+
let pluginName = "";
|
|
142
|
+
while (!pluginName) {
|
|
143
|
+
pluginName = (await promptDriver.promptText("Plugin name", defaultName)).trim();
|
|
144
|
+
}
|
|
145
|
+
let pluginVersion = "";
|
|
146
|
+
while (!pluginVersion) {
|
|
147
|
+
pluginVersion = (await promptDriver.promptText("Version", "0.1.0")).trim();
|
|
148
|
+
}
|
|
149
|
+
const defaultEnabled = await promptDriver.promptBoolean("Default enabled", true);
|
|
150
|
+
const runNpmInstall = await promptDriver.promptBoolean("Run npm install now", true);
|
|
151
|
+
return {
|
|
152
|
+
targetDir,
|
|
153
|
+
kind,
|
|
154
|
+
pluginId,
|
|
155
|
+
pluginName,
|
|
156
|
+
pluginVersion,
|
|
157
|
+
defaultEnabled,
|
|
158
|
+
runNpmInstall,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
promptDriver.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function runNpmInstall(targetDir) {
|
|
166
|
+
const result = (0, node_child_process_1.spawnSync)(npmExecutable(), ["install"], {
|
|
167
|
+
cwd: targetDir,
|
|
168
|
+
stdio: "inherit",
|
|
169
|
+
});
|
|
170
|
+
if (result.error) {
|
|
171
|
+
throw new Error(`failed to spawn npm install in ${targetDir}: ${result.error.message}`);
|
|
172
|
+
}
|
|
173
|
+
if (result.status !== 0) {
|
|
174
|
+
throw new Error(`npm install failed in ${targetDir} (code ${result.status})`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function writeCommonFiles(answers) {
|
|
178
|
+
writeJson(path.join(answers.targetDir, "openvcs.plugin.json"), {
|
|
179
|
+
id: answers.pluginId,
|
|
180
|
+
name: answers.pluginName,
|
|
181
|
+
version: answers.pluginVersion,
|
|
182
|
+
default_enabled: answers.defaultEnabled,
|
|
183
|
+
...(answers.kind === "module" ? { module: { exec: "plugin.js" } } : {}),
|
|
184
|
+
});
|
|
185
|
+
writeText(path.join(answers.targetDir, ".gitignore"), "node_modules/\ndist/\n");
|
|
186
|
+
}
|
|
187
|
+
function writeModuleTemplate(answers) {
|
|
188
|
+
writeCommonFiles(answers);
|
|
189
|
+
writeJson(path.join(answers.targetDir, "package.json"), {
|
|
190
|
+
name: answers.pluginId,
|
|
191
|
+
version: answers.pluginVersion,
|
|
192
|
+
private: true,
|
|
193
|
+
type: "module",
|
|
194
|
+
scripts: {
|
|
195
|
+
"build:ts": "tsc -p tsconfig.json",
|
|
196
|
+
build: "npm run build:ts && openvcs dist --plugin-dir . --out dist",
|
|
197
|
+
test: "openvcs dist --plugin-dir . --out dist --no-npm-deps",
|
|
198
|
+
},
|
|
199
|
+
devDependencies: {
|
|
200
|
+
"@openvcs/sdk": `^${packageJson.version}`,
|
|
201
|
+
"@types/node": "^22.0.0",
|
|
202
|
+
typescript: "^5.8.2",
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
writeJson(path.join(answers.targetDir, "tsconfig.json"), {
|
|
206
|
+
compilerOptions: {
|
|
207
|
+
target: "ES2022",
|
|
208
|
+
module: "NodeNext",
|
|
209
|
+
moduleResolution: "NodeNext",
|
|
210
|
+
types: ["node"],
|
|
211
|
+
strict: true,
|
|
212
|
+
skipLibCheck: true,
|
|
213
|
+
outDir: "bin",
|
|
214
|
+
rootDir: "src",
|
|
215
|
+
},
|
|
216
|
+
include: ["src/**/*.ts"],
|
|
217
|
+
});
|
|
218
|
+
writeText(path.join(answers.targetDir, "src", "plugin.ts"), "const message = \"OpenVCS plugin started\";\nprocess.stderr.write(`${message}\\n`);\n");
|
|
219
|
+
}
|
|
220
|
+
function writeThemeTemplate(answers) {
|
|
221
|
+
writeCommonFiles(answers);
|
|
222
|
+
writeJson(path.join(answers.targetDir, "package.json"), {
|
|
223
|
+
name: answers.pluginId,
|
|
224
|
+
version: answers.pluginVersion,
|
|
225
|
+
private: true,
|
|
226
|
+
scripts: {
|
|
227
|
+
build: "openvcs dist --plugin-dir . --out dist",
|
|
228
|
+
test: "openvcs dist --plugin-dir . --out dist --no-npm-deps",
|
|
229
|
+
},
|
|
230
|
+
devDependencies: {
|
|
231
|
+
"@openvcs/sdk": `^${packageJson.version}`,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
writeJson(path.join(answers.targetDir, "themes", "default", "theme.json"), {
|
|
235
|
+
name: answers.pluginName || "Default Theme",
|
|
236
|
+
description: "Starter OpenVCS theme generated by @openvcs/sdk",
|
|
237
|
+
tokens: {
|
|
238
|
+
accent: "#2a7fff",
|
|
239
|
+
background: "#0f172a",
|
|
240
|
+
foreground: "#e2e8f0",
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async function runInitCommand(args) {
|
|
245
|
+
let forceTheme = false;
|
|
246
|
+
let targetHint;
|
|
247
|
+
for (const arg of args) {
|
|
248
|
+
if (arg === "--theme") {
|
|
249
|
+
forceTheme = true;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (arg === "--help") {
|
|
253
|
+
const error = new Error(initUsage());
|
|
254
|
+
error.code = "USAGE";
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
if (arg.startsWith("-")) {
|
|
258
|
+
throw new Error(`unknown argument for init: ${arg}`);
|
|
259
|
+
}
|
|
260
|
+
if (targetHint) {
|
|
261
|
+
throw new Error("init accepts at most one target directory");
|
|
262
|
+
}
|
|
263
|
+
targetHint = arg;
|
|
264
|
+
}
|
|
265
|
+
const answers = await collectAnswers({ forceTheme, targetHint });
|
|
266
|
+
if (!fs.existsSync(answers.targetDir)) {
|
|
267
|
+
fs.mkdirSync(answers.targetDir, { recursive: true });
|
|
268
|
+
}
|
|
269
|
+
else if (!fs.lstatSync(answers.targetDir).isDirectory()) {
|
|
270
|
+
throw new Error(`target path exists but is not a directory: ${answers.targetDir}`);
|
|
271
|
+
}
|
|
272
|
+
else if (directoryHasEntries(answers.targetDir)) {
|
|
273
|
+
const promptDriver = createReadlinePromptDriver();
|
|
274
|
+
try {
|
|
275
|
+
const proceed = await promptDriver.promptBoolean(`Directory ${answers.targetDir} is not empty. Continue and overwrite known files`, false);
|
|
276
|
+
if (!proceed) {
|
|
277
|
+
throw new Error("aborted by user");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
promptDriver.close();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (answers.kind === "module") {
|
|
285
|
+
writeModuleTemplate(answers);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
writeThemeTemplate(answers);
|
|
289
|
+
}
|
|
290
|
+
if (answers.runNpmInstall) {
|
|
291
|
+
runNpmInstall(answers.targetDir);
|
|
292
|
+
}
|
|
293
|
+
return answers.targetDir;
|
|
294
|
+
}
|
|
295
|
+
function isUsageError(error) {
|
|
296
|
+
return (typeof error === "object" &&
|
|
297
|
+
error !== null &&
|
|
298
|
+
"code" in error &&
|
|
299
|
+
typeof error.code === "string");
|
|
300
|
+
}
|
|
301
|
+
exports.__private = {
|
|
302
|
+
collectAnswers,
|
|
303
|
+
createReadlinePromptDriver,
|
|
304
|
+
defaultPluginIdFromDir,
|
|
305
|
+
sanitizeIdToken,
|
|
306
|
+
validatePluginId,
|
|
307
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openvcs/sdk",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "OpenVCS SDK CLI for plugin scaffolding and .ovcsp packaging",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "OpenVCS SDK CLI for plugin scaffolding and .ovcsp tar.gz packaging",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"homepage": "https://bbgames.dev/",
|
|
7
7
|
"repository": {
|
|
@@ -15,16 +15,27 @@
|
|
|
15
15
|
"node": ">=18"
|
|
16
16
|
},
|
|
17
17
|
"bin": {
|
|
18
|
-
"openvcs
|
|
18
|
+
"openvcs": "bin/openvcs.js"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"
|
|
21
|
+
"build": "node scripts/build-sdk.js",
|
|
22
|
+
"test": "npm run build && node --test test/*.test.js",
|
|
23
|
+
"prepack": "npm run build",
|
|
24
|
+
"preopenvcs": "npm run build",
|
|
25
|
+
"openvcs": "node bin/openvcs.js"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.15.21",
|
|
29
|
+
"typescript": "^5.8.2"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"tar": "^7.4.3"
|
|
22
33
|
},
|
|
23
34
|
"files": [
|
|
35
|
+
"src/",
|
|
24
36
|
"bin/",
|
|
25
37
|
"lib/",
|
|
26
|
-
"
|
|
27
|
-
"vendor/",
|
|
38
|
+
"test/",
|
|
28
39
|
"README.md",
|
|
29
40
|
"LICENSE"
|
|
30
41
|
]
|
package/src/lib/cli.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { bundlePlugin, distUsage, parseDistArgs } from "./dist";
|
|
2
|
+
import { initUsage, runInitCommand } from "./init";
|
|
3
|
+
|
|
4
|
+
const packageJson: { version: string } = require("../package.json");
|
|
5
|
+
|
|
6
|
+
interface CodedError {
|
|
7
|
+
code?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function hasCode(error: unknown, code: string): boolean {
|
|
11
|
+
return (
|
|
12
|
+
typeof error === "object" &&
|
|
13
|
+
error !== null &&
|
|
14
|
+
"code" in error &&
|
|
15
|
+
(error as CodedError).code === code
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
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
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runCli(args: string[]): Promise<void> {
|
|
24
|
+
if (args.length === 0) {
|
|
25
|
+
process.stderr.write(usage());
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (args.includes("-v") || args.includes("--version")) {
|
|
31
|
+
process.stdout.write(`openvcs ${packageJson.version}\n`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const [command, ...rest] = args;
|
|
36
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
37
|
+
process.stdout.write(usage());
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (command === "dist") {
|
|
42
|
+
if (rest.includes("--help")) {
|
|
43
|
+
process.stdout.write(distUsage());
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const parsed = parseDistArgs(rest);
|
|
48
|
+
const outPath = await bundlePlugin(parsed);
|
|
49
|
+
process.stdout.write(`${outPath}\n`);
|
|
50
|
+
return;
|
|
51
|
+
} catch (error: unknown) {
|
|
52
|
+
if (hasCode(error, "USAGE")) {
|
|
53
|
+
throw new Error(distUsage());
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (command === "init") {
|
|
60
|
+
if (rest.includes("--help")) {
|
|
61
|
+
process.stdout.write(initUsage());
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const targetDir = await runInitCommand(rest);
|
|
66
|
+
process.stdout.write(`Initialized plugin at ${targetDir}\n`);
|
|
67
|
+
return;
|
|
68
|
+
} catch (error: unknown) {
|
|
69
|
+
if (hasCode(error, "USAGE")) {
|
|
70
|
+
throw new Error(initUsage());
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new Error(`unknown command: ${command}`);
|
|
77
|
+
}
|