@openvcs/sdk 0.2.0 → 0.2.1

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