@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/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
OpenVCS SDK for npm-based plugin development.
|
|
8
8
|
|
|
9
9
|
Install this package in plugin projects, scaffold a starter plugin, and package
|
|
10
|
-
plugins into `.ovcsp` bundles
|
|
10
|
+
plugins into `.ovcsp` bundles.
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
@@ -15,18 +15,55 @@ plugins into `.ovcsp` bundles with `npm run build`.
|
|
|
15
15
|
npm install --save-dev @openvcs/sdk
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
This package installs a local CLI command named `openvcs`.
|
|
19
|
+
|
|
20
|
+
One-off usage without adding to a project:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx --package @openvcs/sdk openvcs --help
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## SDK development
|
|
27
|
+
|
|
28
|
+
This repository is authored in TypeScript under `src/` and compiles runtime files
|
|
29
|
+
to `bin/` and `lib/`.
|
|
30
|
+
|
|
31
|
+
Build the SDK:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm run build
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Run tests (builds first):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm test
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Run the local CLI through npm scripts:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm run openvcs -- --help
|
|
47
|
+
npm run openvcs -- init --help
|
|
48
|
+
npm run openvcs -- dist --help
|
|
49
|
+
```
|
|
50
|
+
|
|
18
51
|
## Scaffold a plugin
|
|
19
52
|
|
|
20
53
|
Interactive module plugin scaffold:
|
|
21
54
|
|
|
22
55
|
```bash
|
|
23
|
-
|
|
56
|
+
openvcs init my-plugin
|
|
24
57
|
```
|
|
25
58
|
|
|
59
|
+
The generated module template includes TypeScript and Node typings (`@types/node`).
|
|
60
|
+
Plugin IDs entered during scaffold must not be `.`/`..` and must not contain path
|
|
61
|
+
separators (`/` or `\\`).
|
|
62
|
+
|
|
26
63
|
Interactive theme plugin scaffold:
|
|
27
64
|
|
|
28
65
|
```bash
|
|
29
|
-
|
|
66
|
+
openvcs init --theme my-theme
|
|
30
67
|
```
|
|
31
68
|
|
|
32
69
|
## Build a `.ovcsp` bundle
|
|
@@ -39,6 +76,9 @@ npm run build
|
|
|
39
76
|
|
|
40
77
|
This produces `dist/<plugin-id>.ovcsp`.
|
|
41
78
|
|
|
79
|
+
`.ovcsp` is a gzip-compressed tar archive (`tar.gz`) that contains a top-level
|
|
80
|
+
`<plugin-id>/` directory with `openvcs.plugin.json` and plugin runtime assets.
|
|
81
|
+
|
|
42
82
|
Dependency behavior while packaging:
|
|
43
83
|
|
|
44
84
|
- npm dependency bundling is enabled by default when `package.json` exists.
|
|
@@ -48,11 +88,28 @@ Dependency behavior while packaging:
|
|
|
48
88
|
- Disable npm dependency processing with `--no-npm-deps`.
|
|
49
89
|
- Native Node addons (`*.node`) are rejected for portable bundles.
|
|
50
90
|
|
|
51
|
-
##
|
|
91
|
+
## CLI usage
|
|
92
|
+
|
|
93
|
+
Package a plugin manually:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
openvcs dist --plugin-dir /path/to/plugin --out /path/to/dist
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Show command help:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
openvcs --help
|
|
103
|
+
openvcs dist --help
|
|
104
|
+
openvcs init --help
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Releases
|
|
52
108
|
|
|
53
|
-
|
|
109
|
+
Stable releases are published from `.github/workflows/release.yml`.
|
|
54
110
|
|
|
55
|
-
|
|
111
|
+
- npm publishes use npm Trusted Publishing (OIDC), so no `NPM_TOKEN` is required.
|
|
112
|
+
- `npm prepack` compiles TypeScript so published packages include `bin/` and `lib/` JS outputs.
|
|
56
113
|
|
|
57
114
|
## License
|
|
58
115
|
|
package/bin/openvcs.d.ts
ADDED
package/bin/openvcs.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const cli_1 = require("../lib/cli");
|
|
5
|
+
(0, cli_1.runCli)(process.argv.slice(2)).catch((error) => {
|
|
6
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
7
|
+
process.stderr.write(`${detail}\n`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
package/lib/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCli(args: string[]): Promise<void>;
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runCli = runCli;
|
|
4
|
+
const dist_1 = require("./dist");
|
|
5
|
+
const init_1 = require("./init");
|
|
6
|
+
const packageJson = require("../package.json");
|
|
7
|
+
function hasCode(error, code) {
|
|
8
|
+
return (typeof error === "object" &&
|
|
9
|
+
error !== null &&
|
|
10
|
+
"code" in error &&
|
|
11
|
+
error.code === code);
|
|
12
|
+
}
|
|
13
|
+
function usage() {
|
|
14
|
+
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";
|
|
15
|
+
}
|
|
16
|
+
async function runCli(args) {
|
|
17
|
+
if (args.length === 0) {
|
|
18
|
+
process.stderr.write(usage());
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (args.includes("-v") || args.includes("--version")) {
|
|
23
|
+
process.stdout.write(`openvcs ${packageJson.version}\n`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const [command, ...rest] = args;
|
|
27
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
28
|
+
process.stdout.write(usage());
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (command === "dist") {
|
|
32
|
+
if (rest.includes("--help")) {
|
|
33
|
+
process.stdout.write((0, dist_1.distUsage)());
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const parsed = (0, dist_1.parseDistArgs)(rest);
|
|
38
|
+
const outPath = await (0, dist_1.bundlePlugin)(parsed);
|
|
39
|
+
process.stdout.write(`${outPath}\n`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (hasCode(error, "USAGE")) {
|
|
44
|
+
throw new Error((0, dist_1.distUsage)());
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (command === "init") {
|
|
50
|
+
if (rest.includes("--help")) {
|
|
51
|
+
process.stdout.write((0, init_1.initUsage)());
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const targetDir = await (0, init_1.runInitCommand)(rest);
|
|
56
|
+
process.stdout.write(`Initialized plugin at ${targetDir}\n`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (hasCode(error, "USAGE")) {
|
|
61
|
+
throw new Error((0, init_1.initUsage)());
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`unknown command: ${command}`);
|
|
67
|
+
}
|
package/lib/dist.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
interface DistArgs {
|
|
2
|
+
pluginDir: string;
|
|
3
|
+
outDir: string;
|
|
4
|
+
verbose: boolean;
|
|
5
|
+
noNpmDeps: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface ManifestInfo {
|
|
8
|
+
pluginId: string;
|
|
9
|
+
moduleExec: string | undefined;
|
|
10
|
+
manifestPath: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function distUsage(commandName?: string): string;
|
|
13
|
+
export declare function parseDistArgs(args: string[]): DistArgs;
|
|
14
|
+
declare function readManifest(pluginDir: string): ManifestInfo;
|
|
15
|
+
declare function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined): void;
|
|
16
|
+
declare function rejectNativeAddonsRecursive(dirPath: string): void;
|
|
17
|
+
declare function copyIcon(pluginDir: string, bundleDir: string): void;
|
|
18
|
+
declare function uniqueStagingDir(outDir: string): string;
|
|
19
|
+
declare function writeTarGz(outPath: string, baseDir: string, folderName: string): Promise<void>;
|
|
20
|
+
export declare function bundlePlugin(parsedArgs: DistArgs): Promise<string>;
|
|
21
|
+
export declare const __private: {
|
|
22
|
+
ICON_EXTENSIONS: string[];
|
|
23
|
+
copyIcon: typeof copyIcon;
|
|
24
|
+
readManifest: typeof readManifest;
|
|
25
|
+
rejectNativeAddonsRecursive: typeof rejectNativeAddonsRecursive;
|
|
26
|
+
uniqueStagingDir: typeof uniqueStagingDir;
|
|
27
|
+
validateDeclaredModuleExec: typeof validateDeclaredModuleExec;
|
|
28
|
+
writeTarGz: typeof writeTarGz;
|
|
29
|
+
};
|
|
30
|
+
export {};
|
package/lib/dist.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.__private = void 0;
|
|
4
|
+
exports.distUsage = distUsage;
|
|
5
|
+
exports.parseDistArgs = parseDistArgs;
|
|
6
|
+
exports.bundlePlugin = bundlePlugin;
|
|
7
|
+
const fs = require("node:fs");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const tar = require("tar");
|
|
11
|
+
const fs_utils_1 = require("./fs-utils");
|
|
12
|
+
const ICON_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "avif", "svg"];
|
|
13
|
+
function npmExecutable() {
|
|
14
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
15
|
+
}
|
|
16
|
+
function distUsage(commandName = "openvcs") {
|
|
17
|
+
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`;
|
|
18
|
+
}
|
|
19
|
+
function parseDistArgs(args) {
|
|
20
|
+
let pluginDir = process.cwd();
|
|
21
|
+
let outDir = "dist";
|
|
22
|
+
let verbose = false;
|
|
23
|
+
let noNpmDeps = false;
|
|
24
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
25
|
+
const arg = args[index];
|
|
26
|
+
if (arg === "--plugin-dir") {
|
|
27
|
+
index += 1;
|
|
28
|
+
if (index >= args.length) {
|
|
29
|
+
throw new Error("missing value for --plugin-dir");
|
|
30
|
+
}
|
|
31
|
+
pluginDir = args[index];
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (arg === "--out") {
|
|
35
|
+
index += 1;
|
|
36
|
+
if (index >= args.length) {
|
|
37
|
+
throw new Error("missing value for --out");
|
|
38
|
+
}
|
|
39
|
+
outDir = args[index];
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (arg === "--no-npm-deps") {
|
|
43
|
+
noNpmDeps = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (arg === "-V" || arg === "--verbose") {
|
|
47
|
+
verbose = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (arg === "--help") {
|
|
51
|
+
const error = new Error(distUsage());
|
|
52
|
+
error.code = "USAGE";
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`unknown flag: ${arg}`);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
pluginDir: path.resolve(pluginDir),
|
|
59
|
+
outDir: path.resolve(outDir),
|
|
60
|
+
verbose,
|
|
61
|
+
noNpmDeps,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function readManifest(pluginDir) {
|
|
65
|
+
const manifestPath = path.join(pluginDir, "openvcs.plugin.json");
|
|
66
|
+
let manifestRaw;
|
|
67
|
+
let manifestFd;
|
|
68
|
+
let manifest;
|
|
69
|
+
try {
|
|
70
|
+
manifestFd = fs.openSync(manifestPath, "r");
|
|
71
|
+
const manifestStat = fs.fstatSync(manifestFd);
|
|
72
|
+
if (!manifestStat.isFile()) {
|
|
73
|
+
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
74
|
+
}
|
|
75
|
+
manifestRaw = fs.readFileSync(manifestFd, "utf8");
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code === "ENOENT") {
|
|
79
|
+
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
if (typeof manifestFd === "number") {
|
|
85
|
+
fs.closeSync(manifestFd);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
manifest = JSON.parse(manifestRaw);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
93
|
+
throw new Error(`parse ${manifestPath}: ${detail}`);
|
|
94
|
+
}
|
|
95
|
+
const pluginId = typeof manifest.id === "string"
|
|
96
|
+
? manifest.id.trim()
|
|
97
|
+
: "";
|
|
98
|
+
if (!pluginId) {
|
|
99
|
+
throw new Error(`manifest ${manifestPath} is missing a string 'id'`);
|
|
100
|
+
}
|
|
101
|
+
if (pluginId === "." || pluginId === ".." || pluginId.includes("/") || pluginId.includes("\\")) {
|
|
102
|
+
throw new Error(`manifest id must not contain path separators: ${pluginId}`);
|
|
103
|
+
}
|
|
104
|
+
const moduleValue = manifest.module;
|
|
105
|
+
const moduleExec = typeof moduleValue?.exec === "string" ? moduleValue.exec.trim() : undefined;
|
|
106
|
+
return {
|
|
107
|
+
pluginId,
|
|
108
|
+
moduleExec,
|
|
109
|
+
manifestPath,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function validateDeclaredModuleExec(pluginDir, moduleExec) {
|
|
113
|
+
if (!moduleExec) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const normalizedExec = moduleExec.trim();
|
|
117
|
+
const lowered = normalizedExec.toLowerCase();
|
|
118
|
+
if (!lowered.endsWith(".js") && !lowered.endsWith(".mjs") && !lowered.endsWith(".cjs")) {
|
|
119
|
+
throw new Error(`manifest exec must end with .js/.mjs/.cjs (Node runtime): ${moduleExec}`);
|
|
120
|
+
}
|
|
121
|
+
if (path.isAbsolute(normalizedExec)) {
|
|
122
|
+
throw new Error(`manifest module.exec must be a relative path under bin/: ${moduleExec}`);
|
|
123
|
+
}
|
|
124
|
+
const binDir = path.resolve(pluginDir, "bin");
|
|
125
|
+
const targetPath = path.resolve(binDir, normalizedExec);
|
|
126
|
+
if (!(0, fs_utils_1.isPathInside)(binDir, targetPath) || targetPath === binDir) {
|
|
127
|
+
throw new Error(`manifest module.exec must point to a file under bin/: ${moduleExec}`);
|
|
128
|
+
}
|
|
129
|
+
if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
|
|
130
|
+
throw new Error(`module entrypoint not found at ${targetPath}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function hasPackageJson(pluginDir) {
|
|
134
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
135
|
+
return fs.existsSync(packageJsonPath) && fs.lstatSync(packageJsonPath).isFile();
|
|
136
|
+
}
|
|
137
|
+
function runCommand(program, args, cwd, verbose) {
|
|
138
|
+
if (verbose) {
|
|
139
|
+
process.stderr.write(`Running command in ${cwd}: ${program} ${args.join(" ")}\n`);
|
|
140
|
+
}
|
|
141
|
+
const result = (0, node_child_process_1.spawnSync)(program, args, {
|
|
142
|
+
cwd,
|
|
143
|
+
stdio: ["ignore", verbose ? "inherit" : "ignore", "inherit"],
|
|
144
|
+
});
|
|
145
|
+
if (result.error) {
|
|
146
|
+
throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
|
|
147
|
+
}
|
|
148
|
+
if (result.status === 0) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`command failed (${program} ${args.join(" ")}), exit code ${result.status}`);
|
|
152
|
+
}
|
|
153
|
+
function ensurePackageLock(pluginDir, verbose) {
|
|
154
|
+
if (!hasPackageJson(pluginDir)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const lockPath = path.join(pluginDir, "package-lock.json");
|
|
158
|
+
if (fs.existsSync(lockPath) && fs.lstatSync(lockPath).isFile()) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (verbose) {
|
|
162
|
+
process.stderr.write(`Generating package-lock.json in ${pluginDir}\n`);
|
|
163
|
+
}
|
|
164
|
+
runCommand(npmExecutable(), ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"], pluginDir, verbose);
|
|
165
|
+
}
|
|
166
|
+
function copyNpmFilesToStaging(pluginDir, bundleDir) {
|
|
167
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
168
|
+
const lockPath = path.join(pluginDir, "package-lock.json");
|
|
169
|
+
if (!fs.existsSync(packageJsonPath) || !fs.lstatSync(packageJsonPath).isFile()) {
|
|
170
|
+
throw new Error(`missing package.json at ${packageJsonPath}`);
|
|
171
|
+
}
|
|
172
|
+
if (!fs.existsSync(lockPath) || !fs.lstatSync(lockPath).isFile()) {
|
|
173
|
+
throw new Error(`missing package-lock.json at ${lockPath}`);
|
|
174
|
+
}
|
|
175
|
+
(0, fs_utils_1.copyFileStrict)(packageJsonPath, path.join(bundleDir, "package.json"));
|
|
176
|
+
(0, fs_utils_1.copyFileStrict)(lockPath, path.join(bundleDir, "package-lock.json"));
|
|
177
|
+
}
|
|
178
|
+
function rejectNativeAddonsRecursive(dirPath) {
|
|
179
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
182
|
+
const stats = fs.lstatSync(entryPath);
|
|
183
|
+
if (stats.isSymbolicLink()) {
|
|
184
|
+
throw new Error(`plugin contains a symlink: ${entryPath}`);
|
|
185
|
+
}
|
|
186
|
+
if (stats.isDirectory()) {
|
|
187
|
+
rejectNativeAddonsRecursive(entryPath);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (!stats.isFile()) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (entry.name.toLowerCase().endsWith(".node")) {
|
|
194
|
+
throw new Error(`native Node addon files are not supported in portable bundles: ${entryPath}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function installNpmDependencies(pluginDir, bundleDir, verbose) {
|
|
199
|
+
copyNpmFilesToStaging(pluginDir, bundleDir);
|
|
200
|
+
runCommand(npmExecutable(), ["ci", "--omit=dev", "--ignore-scripts", "--no-bin-links", "--no-audit", "--no-fund"], bundleDir, verbose);
|
|
201
|
+
const nodeModulesPath = path.join(bundleDir, "node_modules");
|
|
202
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!fs.lstatSync(nodeModulesPath).isDirectory()) {
|
|
206
|
+
throw new Error(`npm install produced non-directory node_modules path: ${nodeModulesPath}`);
|
|
207
|
+
}
|
|
208
|
+
rejectNativeAddonsRecursive(nodeModulesPath);
|
|
209
|
+
}
|
|
210
|
+
function copyIcon(pluginDir, bundleDir) {
|
|
211
|
+
for (const extension of ICON_EXTENSIONS) {
|
|
212
|
+
const fileName = `icon.${extension}`;
|
|
213
|
+
const sourcePath = path.join(pluginDir, fileName);
|
|
214
|
+
if (!fs.existsSync(sourcePath)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
(0, fs_utils_1.copyFileStrict)(sourcePath, path.join(bundleDir, fileName));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function uniqueStagingDir(outDir) {
|
|
222
|
+
return path.join(outDir, `.openvcs-plugin-staging-${Date.now()}-${process.pid}`);
|
|
223
|
+
}
|
|
224
|
+
async function writeTarGz(outPath, baseDir, folderName) {
|
|
225
|
+
const folderPath = path.join(baseDir, folderName);
|
|
226
|
+
(0, fs_utils_1.rejectSymlinksRecursive)(folderPath);
|
|
227
|
+
await tar.create({
|
|
228
|
+
cwd: baseDir,
|
|
229
|
+
file: outPath,
|
|
230
|
+
gzip: true,
|
|
231
|
+
portable: true,
|
|
232
|
+
noMtime: true,
|
|
233
|
+
preservePaths: false,
|
|
234
|
+
strict: true,
|
|
235
|
+
}, [folderName]);
|
|
236
|
+
}
|
|
237
|
+
async function bundlePlugin(parsedArgs) {
|
|
238
|
+
const { pluginDir, outDir, verbose, noNpmDeps } = parsedArgs;
|
|
239
|
+
if (verbose) {
|
|
240
|
+
process.stderr.write(`Bundling plugin from: ${pluginDir}\n`);
|
|
241
|
+
}
|
|
242
|
+
const { pluginId, moduleExec, manifestPath } = readManifest(pluginDir);
|
|
243
|
+
const themesPath = path.join(pluginDir, "themes");
|
|
244
|
+
const hasThemes = fs.existsSync(themesPath) && fs.lstatSync(themesPath).isDirectory();
|
|
245
|
+
if (!moduleExec && !hasThemes) {
|
|
246
|
+
throw new Error("manifest has no module.exec or themes/");
|
|
247
|
+
}
|
|
248
|
+
validateDeclaredModuleExec(pluginDir, moduleExec);
|
|
249
|
+
(0, fs_utils_1.ensureDirectory)(outDir);
|
|
250
|
+
const stagingRoot = uniqueStagingDir(outDir);
|
|
251
|
+
const bundleDir = path.join(stagingRoot, pluginId);
|
|
252
|
+
(0, fs_utils_1.ensureDirectory)(bundleDir);
|
|
253
|
+
try {
|
|
254
|
+
(0, fs_utils_1.copyFileStrict)(manifestPath, path.join(bundleDir, "openvcs.plugin.json"));
|
|
255
|
+
copyIcon(pluginDir, bundleDir);
|
|
256
|
+
const sourceBinDir = path.join(pluginDir, "bin");
|
|
257
|
+
if (fs.existsSync(sourceBinDir) && fs.lstatSync(sourceBinDir).isDirectory()) {
|
|
258
|
+
(0, fs_utils_1.copyDirectoryRecursiveStrict)(sourceBinDir, path.join(bundleDir, "bin"));
|
|
259
|
+
}
|
|
260
|
+
if (hasThemes) {
|
|
261
|
+
(0, fs_utils_1.copyDirectoryRecursiveStrict)(themesPath, path.join(bundleDir, "themes"));
|
|
262
|
+
}
|
|
263
|
+
if (!noNpmDeps && hasPackageJson(pluginDir)) {
|
|
264
|
+
ensurePackageLock(pluginDir, verbose);
|
|
265
|
+
installNpmDependencies(pluginDir, bundleDir, verbose);
|
|
266
|
+
}
|
|
267
|
+
const outPath = path.join(outDir, `${pluginId}.ovcsp`);
|
|
268
|
+
if (fs.existsSync(outPath)) {
|
|
269
|
+
fs.rmSync(outPath, { force: true });
|
|
270
|
+
}
|
|
271
|
+
await writeTarGz(outPath, stagingRoot, pluginId);
|
|
272
|
+
return outPath;
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
exports.__private = {
|
|
279
|
+
ICON_EXTENSIONS,
|
|
280
|
+
copyIcon,
|
|
281
|
+
readManifest,
|
|
282
|
+
rejectNativeAddonsRecursive,
|
|
283
|
+
uniqueStagingDir,
|
|
284
|
+
validateDeclaredModuleExec,
|
|
285
|
+
writeTarGz,
|
|
286
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function isPathInside(rootPath: string, candidatePath: string): boolean;
|
|
2
|
+
export declare function rejectSymlinksRecursive(rootDir: string): void;
|
|
3
|
+
export declare function ensureDirectory(filePath: string): void;
|
|
4
|
+
export declare function copyFileStrict(sourcePath: string, destinationPath: string): void;
|
|
5
|
+
export declare function copyDirectoryRecursiveStrict(sourceDir: string, destinationDir: string): void;
|
package/lib/fs-utils.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isPathInside = isPathInside;
|
|
4
|
+
exports.rejectSymlinksRecursive = rejectSymlinksRecursive;
|
|
5
|
+
exports.ensureDirectory = ensureDirectory;
|
|
6
|
+
exports.copyFileStrict = copyFileStrict;
|
|
7
|
+
exports.copyDirectoryRecursiveStrict = copyDirectoryRecursiveStrict;
|
|
8
|
+
const fs = require("node:fs");
|
|
9
|
+
const path = require("node:path");
|
|
10
|
+
function isPathInside(rootPath, candidatePath) {
|
|
11
|
+
const relative = path.relative(rootPath, candidatePath);
|
|
12
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
13
|
+
}
|
|
14
|
+
function rejectSymlinksRecursive(rootDir) {
|
|
15
|
+
const stack = [rootDir];
|
|
16
|
+
while (stack.length > 0) {
|
|
17
|
+
const current = stack.pop();
|
|
18
|
+
if (!current) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const entryPath = path.join(current, entry.name);
|
|
24
|
+
const stats = fs.lstatSync(entryPath);
|
|
25
|
+
if (stats.isSymbolicLink()) {
|
|
26
|
+
throw new Error(`plugin contains a symlink: ${entryPath}`);
|
|
27
|
+
}
|
|
28
|
+
if (stats.isDirectory()) {
|
|
29
|
+
stack.push(entryPath);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function ensureDirectory(filePath) {
|
|
35
|
+
fs.mkdirSync(filePath, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
function copyFileStrict(sourcePath, destinationPath) {
|
|
38
|
+
const stats = fs.lstatSync(sourcePath);
|
|
39
|
+
if (stats.isSymbolicLink()) {
|
|
40
|
+
throw new Error(`plugin contains a symlink: ${sourcePath}`);
|
|
41
|
+
}
|
|
42
|
+
if (!stats.isFile()) {
|
|
43
|
+
throw new Error(`expected file: ${sourcePath}`);
|
|
44
|
+
}
|
|
45
|
+
ensureDirectory(path.dirname(destinationPath));
|
|
46
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
47
|
+
}
|
|
48
|
+
function copyDirectoryRecursiveStrict(sourceDir, destinationDir) {
|
|
49
|
+
if (!fs.existsSync(sourceDir)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const stats = fs.lstatSync(sourceDir);
|
|
53
|
+
if (stats.isSymbolicLink()) {
|
|
54
|
+
throw new Error(`plugin contains a symlink: ${sourceDir}`);
|
|
55
|
+
}
|
|
56
|
+
if (!stats.isDirectory()) {
|
|
57
|
+
throw new Error(`expected directory: ${sourceDir}`);
|
|
58
|
+
}
|
|
59
|
+
ensureDirectory(destinationDir);
|
|
60
|
+
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
63
|
+
const destinationPath = path.join(destinationDir, entry.name);
|
|
64
|
+
const entryStats = fs.lstatSync(sourcePath);
|
|
65
|
+
if (entryStats.isSymbolicLink()) {
|
|
66
|
+
throw new Error(`plugin contains a symlink: ${sourcePath}`);
|
|
67
|
+
}
|
|
68
|
+
if (entryStats.isDirectory()) {
|
|
69
|
+
copyDirectoryRecursiveStrict(sourcePath, destinationPath);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (entryStats.isFile()) {
|
|
73
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
package/lib/init.d.ts
ADDED
|
@@ -0,0 +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
|
+
}
|
|
19
|
+
interface InitCommandError {
|
|
20
|
+
code?: string;
|
|
21
|
+
}
|
|
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>;
|
|
28
|
+
export declare function runInitCommand(args: string[]): Promise<string>;
|
|
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
|
+
};
|
|
37
|
+
export {};
|