@openvcs/sdk 0.2.2 → 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/lib/init.js CHANGED
@@ -192,9 +192,10 @@ function writeModuleTemplate(answers) {
192
192
  private: true,
193
193
  type: "module",
194
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",
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",
198
199
  },
199
200
  devDependencies: {
200
201
  "@openvcs/sdk": `^${packageJson.version}`,
@@ -224,8 +225,9 @@ function writeThemeTemplate(answers) {
224
225
  version: answers.pluginVersion,
225
226
  private: true,
226
227
  scripts: {
227
- build: "openvcs dist --plugin-dir . --out dist",
228
- test: "openvcs dist --plugin-dir . --out dist --no-npm-deps",
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",
229
231
  },
230
232
  devDependencies: {
231
233
  "@openvcs/sdk": `^${packageJson.version}`,
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@openvcs/sdk",
3
- "version": "0.2.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://bbgames.dev/",
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": "^22.15.21",
28
+ "@types/node": "^25.3.3",
29
29
  "typescript": "^5.8.2"
30
30
  },
31
31
  "dependencies": {
@@ -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.includes("-v") || args.includes("--version")) {
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());
package/src/lib/dist.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { spawnSync } from "node:child_process";
4
3
  import tar = require("tar");
4
+ import {
5
+ buildPluginAssets,
6
+ hasPackageJson,
7
+ ManifestInfo,
8
+ npmExecutable,
9
+ readManifest,
10
+ runCommand,
11
+ validateDeclaredModuleExec,
12
+ } from "./build";
5
13
  import {
6
14
  copyDirectoryRecursiveStrict,
7
15
  copyFileStrict,
@@ -19,25 +27,11 @@ interface DistArgs {
19
27
  outDir: string;
20
28
  verbose: boolean;
21
29
  noNpmDeps: boolean;
22
- }
23
-
24
- interface ManifestInfo {
25
- pluginId: string;
26
- moduleExec: string | undefined;
27
- manifestPath: string;
28
- }
29
-
30
- interface CommandResult {
31
- status: number | null;
32
- error?: Error;
33
- }
34
-
35
- function npmExecutable(): string {
36
- return process.platform === "win32" ? "npm.cmd" : "npm";
30
+ noBuild: boolean;
37
31
  }
38
32
 
39
33
  export function distUsage(commandName = "openvcs"): string {
40
- return `${commandName} dist [args]\n\n --plugin-dir <path> Plugin repository root (contains openvcs.plugin.json)\n --out <path> Output directory (default: ./dist)\n --no-npm-deps Disable npm dependency bundling (enabled by default)\n -V, --verbose Enable verbose output\n`;
34
+ return `${commandName} dist [args]\n\n --plugin-dir <path> Plugin repository root (contains openvcs.plugin.json)\n --out <path> Output directory (default: ./dist)\n --no-build Skip the plugin build step before packaging\n --no-npm-deps Disable npm dependency bundling (enabled by default)\n -V, --verbose Enable verbose output\n`;
41
35
  }
42
36
 
43
37
  export function parseDistArgs(args: string[]): DistArgs {
@@ -45,6 +39,7 @@ export function parseDistArgs(args: string[]): DistArgs {
45
39
  let outDir = "dist";
46
40
  let verbose = false;
47
41
  let noNpmDeps = false;
42
+ let noBuild = false;
48
43
 
49
44
  for (let index = 0; index < args.length; index += 1) {
50
45
  const arg = args[index];
@@ -68,6 +63,10 @@ export function parseDistArgs(args: string[]): DistArgs {
68
63
  noNpmDeps = true;
69
64
  continue;
70
65
  }
66
+ if (arg === "--no-build") {
67
+ noBuild = true;
68
+ continue;
69
+ }
71
70
  if (arg === "-V" || arg === "--verbose") {
72
71
  verbose = true;
73
72
  continue;
@@ -85,110 +84,34 @@ export function parseDistArgs(args: string[]): DistArgs {
85
84
  outDir: path.resolve(outDir),
86
85
  verbose,
87
86
  noNpmDeps,
87
+ noBuild,
88
88
  };
89
89
  }
90
90
 
91
- function readManifest(pluginDir: string): ManifestInfo {
92
- const manifestPath = path.join(pluginDir, "openvcs.plugin.json");
93
- let manifestRaw: string;
94
- let manifestFd: number | undefined;
95
- let manifest: unknown;
96
- try {
97
- manifestFd = fs.openSync(manifestPath, "r");
98
- const manifestStat = fs.fstatSync(manifestFd);
99
- if (!manifestStat.isFile()) {
100
- throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
101
- }
102
- manifestRaw = fs.readFileSync(manifestFd, "utf8");
103
- } catch (error: unknown) {
104
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
105
- throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
106
- }
107
- throw error;
108
- } finally {
109
- if (typeof manifestFd === "number") {
110
- fs.closeSync(manifestFd);
111
- }
91
+ function validateManifestEntry(pluginDir: string, entry: string): void {
92
+ const normalized = entry.trim();
93
+ if (path.isAbsolute(normalized)) {
94
+ throw new Error(`manifest entry must be a relative path: ${entry}`);
112
95
  }
113
-
114
- try {
115
- manifest = JSON.parse(manifestRaw);
116
- } catch (error: unknown) {
117
- const detail = error instanceof Error ? error.message : String(error);
118
- throw new Error(`parse ${manifestPath}: ${detail}`);
119
- }
120
-
121
- const pluginId =
122
- typeof (manifest as { id?: unknown }).id === "string"
123
- ? ((manifest as { id: string }).id.trim() as string)
124
- : "";
125
- if (!pluginId) {
126
- throw new Error(`manifest ${manifestPath} is missing a string 'id'`);
127
- }
128
- if (pluginId === "." || pluginId === ".." || pluginId.includes("/") || pluginId.includes("\\")) {
129
- throw new Error(`manifest id must not contain path separators: ${pluginId}`);
130
- }
131
-
132
- const moduleValue = (manifest as { module?: { exec?: unknown } }).module;
133
- const moduleExec = typeof moduleValue?.exec === "string" ? moduleValue.exec.trim() : undefined;
134
-
135
- return {
136
- pluginId,
137
- moduleExec,
138
- manifestPath,
139
- };
140
- }
141
-
142
- function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined): void {
143
- if (!moduleExec) {
144
- return;
145
- }
146
-
147
- const normalizedExec = moduleExec.trim();
148
- const lowered = normalizedExec.toLowerCase();
149
- if (!lowered.endsWith(".js") && !lowered.endsWith(".mjs") && !lowered.endsWith(".cjs")) {
150
- throw new Error(`manifest exec must end with .js/.mjs/.cjs (Node runtime): ${moduleExec}`);
151
- }
152
- if (path.isAbsolute(normalizedExec)) {
153
- throw new Error(`manifest module.exec must be a relative path under bin/: ${moduleExec}`);
154
- }
155
-
156
- const binDir = path.resolve(pluginDir, "bin");
157
- const targetPath = path.resolve(binDir, normalizedExec);
158
- if (!isPathInside(binDir, targetPath) || targetPath === binDir) {
159
- throw new Error(`manifest module.exec must point to a file under bin/: ${moduleExec}`);
96
+ const targetPath = path.resolve(pluginDir, normalized);
97
+ const pluginDirResolved = path.resolve(pluginDir);
98
+ if (!isPathInside(pluginDirResolved, targetPath) || targetPath === pluginDirResolved) {
99
+ throw new Error(`manifest entry must point to a file under the plugin directory: ${entry}`);
160
100
  }
161
101
  if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
162
- throw new Error(`module entrypoint not found at ${targetPath}`);
102
+ throw new Error(`manifest entry file not found: ${entry}`);
163
103
  }
164
104
  }
165
105
 
166
- function hasPackageJson(pluginDir: string): boolean {
167
- const packageJsonPath = path.join(pluginDir, "package.json");
168
- return fs.existsSync(packageJsonPath) && fs.lstatSync(packageJsonPath).isFile();
106
+ function copyEntryDirectory(pluginDir: string, bundleDir: string, entry: string): void {
107
+ const normalized = entry.trim();
108
+ const entryDir = path.dirname(normalized);
109
+ const sourceDir = path.join(pluginDir, entryDir);
110
+ const destDir = path.join(bundleDir, entryDir);
111
+ copyDirectoryRecursiveStrict(sourceDir, destDir);
169
112
  }
170
113
 
171
- function runCommand(program: string, args: string[], cwd: string, verbose: boolean): void {
172
- if (verbose) {
173
- process.stderr.write(`Running command in ${cwd}: ${program} ${args.join(" ")}\n`);
174
- }
175
-
176
- const result = spawnSync(program, args, {
177
- cwd,
178
- stdio: ["ignore", verbose ? "inherit" : "ignore", "inherit"],
179
- }) as CommandResult;
180
-
181
- if (result.error) {
182
- throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
183
- }
184
- if (result.status === 0) {
185
- return;
186
- }
187
-
188
- throw new Error(`command failed (${program} ${args.join(" ")}), exit code ${result.status}`);
189
- }
190
-
191
- function ensurePackageLock(pluginDir: string, verbose: boolean): void {
114
+ function ensurePackageLock(pluginDir: string, bundleDir: string, verbose: boolean): void {
192
115
  if (!hasPackageJson(pluginDir)) {
193
116
  return;
194
117
  }
@@ -197,13 +120,16 @@ function ensurePackageLock(pluginDir: string, verbose: boolean): void {
197
120
  return;
198
121
  }
199
122
 
123
+ const packageJsonPath = path.join(pluginDir, "package.json");
124
+ copyFileStrict(packageJsonPath, path.join(bundleDir, "package.json"));
125
+
200
126
  if (verbose) {
201
- process.stderr.write(`Generating package-lock.json in ${pluginDir}\n`);
127
+ process.stderr.write(`Generating package-lock.json in staging\n`);
202
128
  }
203
129
  runCommand(
204
130
  npmExecutable(),
205
131
  ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"],
206
- pluginDir,
132
+ bundleDir,
207
133
  verbose
208
134
  );
209
135
  }
@@ -215,12 +141,17 @@ function copyNpmFilesToStaging(pluginDir: string, bundleDir: string): void {
215
141
  if (!fs.existsSync(packageJsonPath) || !fs.lstatSync(packageJsonPath).isFile()) {
216
142
  throw new Error(`missing package.json at ${packageJsonPath}`);
217
143
  }
218
- if (!fs.existsSync(lockPath) || !fs.lstatSync(lockPath).isFile()) {
219
- throw new Error(`missing package-lock.json at ${lockPath}`);
220
- }
221
144
 
222
145
  copyFileStrict(packageJsonPath, path.join(bundleDir, "package.json"));
223
- copyFileStrict(lockPath, path.join(bundleDir, "package-lock.json"));
146
+
147
+ const stagedLockPath = path.join(bundleDir, "package-lock.json");
148
+ if (fs.existsSync(stagedLockPath) && fs.lstatSync(stagedLockPath).isFile()) {
149
+ return;
150
+ }
151
+
152
+ if (fs.existsSync(lockPath) && fs.lstatSync(lockPath).isFile()) {
153
+ copyFileStrict(lockPath, path.join(bundleDir, "package-lock.json"));
154
+ }
224
155
  }
225
156
 
226
157
  function rejectNativeAddonsRecursive(dirPath: string): void {
@@ -264,14 +195,21 @@ function installNpmDependencies(pluginDir: string, bundleDir: string, verbose: b
264
195
  }
265
196
 
266
197
  function copyIcon(pluginDir: string, bundleDir: string): void {
267
- for (const extension of ICON_EXTENSIONS) {
268
- const fileName = `icon.${extension}`;
269
- const sourcePath = path.join(pluginDir, fileName);
270
- if (!fs.existsSync(sourcePath)) {
271
- continue;
198
+ const entries = fs.readdirSync(pluginDir, { withFileTypes: true });
199
+ const iconEntries = entries.filter((e) => {
200
+ if (!e.isFile()) return false;
201
+ const name = e.name.toLowerCase();
202
+ return name.startsWith("icon.") && ICON_EXTENSIONS.includes(name.slice(5));
203
+ });
204
+ for (const ext of ICON_EXTENSIONS) {
205
+ const found = iconEntries.find((e) => e.name.toLowerCase() === `icon.${ext}`);
206
+ if (found) {
207
+ copyFileStrict(
208
+ path.join(pluginDir, found.name),
209
+ path.join(bundleDir, found.name)
210
+ );
211
+ return;
272
212
  }
273
- copyFileStrict(sourcePath, path.join(bundleDir, fileName));
274
- return;
275
213
  }
276
214
  }
277
215
 
@@ -297,16 +235,21 @@ async function writeTarGz(outPath: string, baseDir: string, folderName: string):
297
235
  }
298
236
 
299
237
  export async function bundlePlugin(parsedArgs: DistArgs): Promise<string> {
300
- const { pluginDir, outDir, verbose, noNpmDeps } = parsedArgs;
238
+ const { pluginDir, outDir, verbose, noNpmDeps, noBuild } = parsedArgs;
301
239
  if (verbose) {
302
240
  process.stderr.write(`Bundling plugin from: ${pluginDir}\n`);
303
241
  }
304
242
 
305
- const { pluginId, moduleExec, manifestPath } = readManifest(pluginDir);
243
+ const { pluginId, moduleExec, entry, manifestPath } = noBuild
244
+ ? readManifest(pluginDir)
245
+ : buildPluginAssets({ pluginDir, verbose });
306
246
  const themesPath = path.join(pluginDir, "themes");
307
247
  const hasThemes = fs.existsSync(themesPath) && fs.lstatSync(themesPath).isDirectory();
308
- if (!moduleExec && !hasThemes) {
309
- throw new Error("manifest has no module.exec or themes/");
248
+ if (entry) {
249
+ validateManifestEntry(pluginDir, entry);
250
+ }
251
+ if (!moduleExec && !hasThemes && !entry) {
252
+ throw new Error("manifest has no module.exec, entry, or themes/");
310
253
  }
311
254
  validateDeclaredModuleExec(pluginDir, moduleExec);
312
255
 
@@ -320,6 +263,10 @@ export async function bundlePlugin(parsedArgs: DistArgs): Promise<string> {
320
263
  copyFileStrict(manifestPath, path.join(bundleDir, "openvcs.plugin.json"));
321
264
  copyIcon(pluginDir, bundleDir);
322
265
 
266
+ if (entry) {
267
+ copyEntryDirectory(pluginDir, bundleDir, entry);
268
+ }
269
+
323
270
  const sourceBinDir = path.join(pluginDir, "bin");
324
271
  if (fs.existsSync(sourceBinDir) && fs.lstatSync(sourceBinDir).isDirectory()) {
325
272
  copyDirectoryRecursiveStrict(sourceBinDir, path.join(bundleDir, "bin"));
@@ -329,7 +276,7 @@ export async function bundlePlugin(parsedArgs: DistArgs): Promise<string> {
329
276
  }
330
277
 
331
278
  if (!noNpmDeps && hasPackageJson(pluginDir)) {
332
- ensurePackageLock(pluginDir, verbose);
279
+ ensurePackageLock(pluginDir, bundleDir, verbose);
333
280
  installNpmDependencies(pluginDir, bundleDir, verbose);
334
281
  }
335
282
 
@@ -352,5 +299,6 @@ export const __private = {
352
299
  rejectNativeAddonsRecursive,
353
300
  uniqueStagingDir,
354
301
  validateDeclaredModuleExec,
302
+ validateManifestEntry,
355
303
  writeTarGz,
356
304
  };
package/src/lib/init.ts CHANGED
@@ -236,9 +236,10 @@ function writeModuleTemplate(answers: InitAnswers): void {
236
236
  private: true,
237
237
  type: "module",
238
238
  scripts: {
239
- "build:ts": "tsc -p tsconfig.json",
240
- build: "npm run build:ts && openvcs dist --plugin-dir . --out dist",
241
- test: "openvcs dist --plugin-dir . --out dist --no-npm-deps",
239
+ "build:plugin": "tsc -p tsconfig.json",
240
+ build: "openvcs build",
241
+ dist: "openvcs dist --plugin-dir . --out dist",
242
+ test: "openvcs dist --plugin-dir . --out dist --no-build --no-npm-deps",
242
243
  },
243
244
  devDependencies: {
244
245
  "@openvcs/sdk": `^${packageJson.version}`,
@@ -272,8 +273,9 @@ function writeThemeTemplate(answers: InitAnswers): void {
272
273
  version: answers.pluginVersion,
273
274
  private: true,
274
275
  scripts: {
275
- build: "openvcs dist --plugin-dir . --out dist",
276
- test: "openvcs dist --plugin-dir . --out dist --no-npm-deps",
276
+ build: "openvcs build",
277
+ dist: "openvcs dist --plugin-dir . --out dist",
278
+ test: "openvcs dist --plugin-dir . --out dist --no-build --no-npm-deps",
277
279
  },
278
280
  devDependencies: {
279
281
  "@openvcs/sdk": `^${packageJson.version}`,