@tgrv/void-cli 1.0.0
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 +59 -0
- package/bin/void.js +567 -0
- package/package.json +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Void CLI (`@tgrv/void-cli`)
|
|
2
|
+
|
|
3
|
+
The official developer command-line toolkit for initializing, building, and publishing WebAssembly plugins for the **Void** framework.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install globally or use on-demand via `npx`:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @tgrv/void-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## CLI Usage
|
|
14
|
+
|
|
15
|
+
### 1. Initialize a new application project
|
|
16
|
+
```bash
|
|
17
|
+
void init [path]
|
|
18
|
+
```
|
|
19
|
+
Creates standard configuration templates and installs the `@tgrv/void-runtime` engine automatically.
|
|
20
|
+
|
|
21
|
+
### 2. Create a new Rust or Go plugin project
|
|
22
|
+
```bash
|
|
23
|
+
void create [plugin-path]
|
|
24
|
+
```
|
|
25
|
+
Prompts for language configuration and instantiates starting templates with pre-configured SDK targets.
|
|
26
|
+
|
|
27
|
+
### 3. Build a plugin locally
|
|
28
|
+
```bash
|
|
29
|
+
void build [plugin-path]
|
|
30
|
+
```
|
|
31
|
+
Compiles source files (Cargo/Go) and wraps the WASM binary under a local scoped output folder configured in `void.json`. Defaults to current folder (`.`).
|
|
32
|
+
|
|
33
|
+
### 4. Publish a plugin
|
|
34
|
+
```bash
|
|
35
|
+
void publish [plugin-path]
|
|
36
|
+
```
|
|
37
|
+
Compiles and triggers `npm publish` natively from inside the scoped build directory. Defaults to current folder (`.`).
|
|
38
|
+
|
|
39
|
+
### 5. Install / Add a plugin
|
|
40
|
+
```bash
|
|
41
|
+
void add <plugin-name>
|
|
42
|
+
```
|
|
43
|
+
Downloads the plugin from the NPM registry, or links local build outputs during development, adding dependencies to `void.config.json` and `package.json`.
|
|
44
|
+
|
|
45
|
+
### 6. Remove a plugin
|
|
46
|
+
```bash
|
|
47
|
+
void remove <plugin-name>
|
|
48
|
+
```
|
|
49
|
+
Uninstalls the package, deletes the local `node_modules` subfolder, and cleans configurations.
|
|
50
|
+
|
|
51
|
+
### 7. Inspect a plugin
|
|
52
|
+
```bash
|
|
53
|
+
void view <plugin-name>
|
|
54
|
+
```
|
|
55
|
+
Queries function metadata directly from the compiled WASM binary.
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
ISC License. See `LICENSE` for details.
|
package/bin/void.js
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
7
|
+
import readline from "readline";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const workspaceDir = path.resolve(__dirname, "../../..");
|
|
12
|
+
|
|
13
|
+
// ANSI Color formatting codes
|
|
14
|
+
const colors = {
|
|
15
|
+
reset: "\x1b[0m",
|
|
16
|
+
bold: "\x1b[1m",
|
|
17
|
+
green: "\x1b[32m",
|
|
18
|
+
red: "\x1b[31m",
|
|
19
|
+
cyan: "\x1b[36m",
|
|
20
|
+
yellow: "\x1b[33m",
|
|
21
|
+
blue: "\x1b[34m",
|
|
22
|
+
magenta: "\x1b[35m"
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const tick = `\x1b[32m✔\x1b[0m`;
|
|
26
|
+
const cross = `\x1b[31m✘\x1b[0m`;
|
|
27
|
+
const info = `\x1b[36mℹ\x1b[0m`;
|
|
28
|
+
const warning = `\x1b[33m⚠\x1b[0m`;
|
|
29
|
+
|
|
30
|
+
// Helper to find the root void.config.json by scanning upward
|
|
31
|
+
function findConfig(dir) {
|
|
32
|
+
const configPath = path.join(dir, "void.config.json");
|
|
33
|
+
if (fs.existsSync(configPath)) {
|
|
34
|
+
return { configPath, configDir: dir };
|
|
35
|
+
}
|
|
36
|
+
const parent = path.dirname(dir);
|
|
37
|
+
if (parent === dir) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return findConfig(parent);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runCommand(cmd, options = {}) {
|
|
44
|
+
try {
|
|
45
|
+
execSync(cmd, { stdio: "inherit", ...options });
|
|
46
|
+
return true;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ensureDir(dir) {
|
|
53
|
+
if (!fs.existsSync(dir)) {
|
|
54
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function copyFolderRecursive(src, dest, replacements = {}) {
|
|
59
|
+
ensureDir(dest);
|
|
60
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const srcPath = path.join(src, entry.name);
|
|
63
|
+
const destPath = path.join(dest, entry.name);
|
|
64
|
+
if (entry.isDirectory()) {
|
|
65
|
+
// Avoid copying build output folder back into itself
|
|
66
|
+
if (entry.name === "@void" || entry.name === "@tgrv") {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
copyFolderRecursive(srcPath, destPath, replacements);
|
|
70
|
+
} else {
|
|
71
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
72
|
+
const isBinary = [".wasm", ".png", ".jpg", ".jpeg", ".gif", ".ico"].includes(ext);
|
|
73
|
+
if (isBinary) {
|
|
74
|
+
fs.copyFileSync(srcPath, destPath);
|
|
75
|
+
} else {
|
|
76
|
+
let content = fs.readFileSync(srcPath, "utf8");
|
|
77
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
78
|
+
content = content.replace(new RegExp(key, "g"), value);
|
|
79
|
+
}
|
|
80
|
+
fs.writeFileSync(destPath, content);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const args = process.argv.slice(2);
|
|
87
|
+
const command = args[0];
|
|
88
|
+
|
|
89
|
+
if (!command) {
|
|
90
|
+
console.log(`
|
|
91
|
+
${colors.bold}${colors.cyan}Void CLI V2 - WASM Plugin Manager${colors.reset}
|
|
92
|
+
|
|
93
|
+
${colors.bold}Usage:${colors.reset}
|
|
94
|
+
${colors.green}void init [path]${colors.reset} - Initialize a new application project at path (default: .)
|
|
95
|
+
${colors.green}void create [plugin-path]${colors.reset} - Create a new Rust or Go plugin project template (default: .)
|
|
96
|
+
${colors.green}void build [plugin-path]${colors.reset} - Compile and place builds inside the plugin folder (default: .)
|
|
97
|
+
${colors.green}void publish [plugin-path]${colors.reset} - Build and publish the plugin to npm registry (default: .)
|
|
98
|
+
${colors.green}void add <plugin-name>${colors.reset} - Add a plugin from registry/local build into application
|
|
99
|
+
${colors.green}void remove <plugin-name>${colors.reset} - Remove a plugin from application and configuration
|
|
100
|
+
${colors.green}void view <plugin-name>${colors.reset} - Inspect an installed plugin and list all its exposed functions
|
|
101
|
+
`);
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Build core compilation wrapper function to resolve output directory using void.json manifest
|
|
106
|
+
function runBuild(pluginPathArg) {
|
|
107
|
+
const absolutePluginDir = path.resolve(process.cwd(), pluginPathArg);
|
|
108
|
+
if (!fs.existsSync(absolutePluginDir)) {
|
|
109
|
+
console.error(`${cross} ${colors.red}Error: Directory does not exist: ${absolutePluginDir}${colors.reset}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Load and parse void.json manifest file
|
|
114
|
+
const manifestPath = path.join(absolutePluginDir, "void.json");
|
|
115
|
+
if (!fs.existsSync(manifestPath)) {
|
|
116
|
+
console.error(`${cross} ${colors.red}Error: void.json manifest not found at: ${manifestPath}${colors.reset}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let manifest;
|
|
121
|
+
try {
|
|
122
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.error(`${cross} ${colors.red}Error: Failed to parse void.json manifest: ${err.message}${colors.reset}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!manifest.name || !manifest.type || !manifest.buildDir) {
|
|
129
|
+
console.error(`${cross} ${colors.red}Error: void.json must specify 'name', 'type', and 'buildDir'${colors.reset}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pluginName = manifest.name;
|
|
134
|
+
const pluginType = manifest.type;
|
|
135
|
+
const buildDir = manifest.buildDir;
|
|
136
|
+
const buildOutputDir = path.join(absolutePluginDir, buildDir);
|
|
137
|
+
|
|
138
|
+
if (pluginType === "sdk") {
|
|
139
|
+
console.log(`\n${info} Packaging SDK '${colors.bold}${pluginName}${colors.reset}' to local build at: ${colors.bold}${buildOutputDir}${colors.reset}`);
|
|
140
|
+
ensureDir(buildOutputDir);
|
|
141
|
+
copyFolderRecursive(absolutePluginDir, buildOutputDir);
|
|
142
|
+
console.log(`${tick} ${colors.green}Successfully completed SDK packaging!${colors.reset}`);
|
|
143
|
+
return buildOutputDir;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(`\n${info} Compiling plugin '${colors.bold}${pluginName}${colors.reset}'...`);
|
|
147
|
+
let builtWasmPath = null;
|
|
148
|
+
|
|
149
|
+
// Rust plugin compilation
|
|
150
|
+
if (pluginType === "rust") {
|
|
151
|
+
console.log(`${info} Running: ${colors.blue}cargo build --target wasm32-unknown-unknown --release${colors.reset}`);
|
|
152
|
+
const compileSuccess = runCommand("cargo build --target wasm32-unknown-unknown --release", { cwd: absolutePluginDir });
|
|
153
|
+
if (!compileSuccess) {
|
|
154
|
+
console.error(`${cross} ${colors.red}Rust compilation failed.${colors.reset}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const targetDir = path.join(absolutePluginDir, "target", "wasm32-unknown-unknown", "release");
|
|
159
|
+
const wasmFiles = fs.readdirSync(targetDir).filter((file) => file.endsWith(".wasm"));
|
|
160
|
+
if (wasmFiles.length === 0) {
|
|
161
|
+
console.error(`${cross} ${colors.red}Could not find compiled .wasm file in Rust target directory${colors.reset}`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
builtWasmPath = path.join(targetDir, wasmFiles[0]);
|
|
165
|
+
}
|
|
166
|
+
// Go plugin compilation
|
|
167
|
+
else if (pluginType === "go") {
|
|
168
|
+
const outputWasm = path.join(absolutePluginDir, "plugin.wasm");
|
|
169
|
+
console.log(`${info} Running: ${colors.blue}go build -o plugin.wasm${colors.reset}`);
|
|
170
|
+
const compileSuccess = runCommand("go build -o plugin.wasm", {
|
|
171
|
+
cwd: absolutePluginDir,
|
|
172
|
+
env: { ...process.env, GOOS: "wasip1", GOARCH: "wasm" },
|
|
173
|
+
});
|
|
174
|
+
if (!compileSuccess) {
|
|
175
|
+
console.error(`${cross} ${colors.red}Go compilation failed.${colors.reset}`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
builtWasmPath = outputWasm;
|
|
179
|
+
} else {
|
|
180
|
+
console.error(`${cross} ${colors.red}Unsupported plugin type: '${pluginType}' in void.json${colors.reset}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(`${info} Placing build output to local folder at ${colors.bold}${buildOutputDir}${colors.reset}...`);
|
|
185
|
+
ensureDir(buildOutputDir);
|
|
186
|
+
|
|
187
|
+
// Copy WASM
|
|
188
|
+
fs.copyFileSync(builtWasmPath, path.join(buildOutputDir, "plugin.wasm"));
|
|
189
|
+
|
|
190
|
+
// Write package.json
|
|
191
|
+
const pkgJson = {
|
|
192
|
+
name: pluginName,
|
|
193
|
+
version: "1.0.0",
|
|
194
|
+
type: "module",
|
|
195
|
+
main: "index.js",
|
|
196
|
+
dependencies: {
|
|
197
|
+
"@tgrv/void-runtime": "^1.0.0",
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
fs.writeFileSync(path.join(buildOutputDir, "package.json"), JSON.stringify(pkgJson, null, 2));
|
|
201
|
+
|
|
202
|
+
// Generate standard ESM index.js loader
|
|
203
|
+
const indexJsContent = `import { runtime } from "@tgrv/void-runtime";
|
|
204
|
+
import { fileURLToPath } from "url";
|
|
205
|
+
import { dirname, join } from "path";
|
|
206
|
+
|
|
207
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
208
|
+
const __dirname = dirname(__filename);
|
|
209
|
+
|
|
210
|
+
const plugin = await runtime.load(
|
|
211
|
+
join(__dirname, "plugin.wasm")
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
export default plugin;
|
|
215
|
+
`;
|
|
216
|
+
fs.writeFileSync(path.join(buildOutputDir, "index.js"), indexJsContent);
|
|
217
|
+
console.log(`${tick} ${colors.green}Successfully completed compilation & packaging!${colors.reset}`);
|
|
218
|
+
return buildOutputDir;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function main() {
|
|
222
|
+
switch (command) {
|
|
223
|
+
case "init": {
|
|
224
|
+
const targetPathArg = args[1] || ".";
|
|
225
|
+
const targetDir = path.resolve(process.cwd(), targetPathArg);
|
|
226
|
+
ensureDir(targetDir);
|
|
227
|
+
|
|
228
|
+
console.log(`\n${info} Initializing Void project at: ${colors.bold}${targetDir}${colors.reset}`);
|
|
229
|
+
|
|
230
|
+
// Initialize package.json if not exists
|
|
231
|
+
const pkgPath = path.join(targetDir, "package.json");
|
|
232
|
+
if (!fs.existsSync(pkgPath)) {
|
|
233
|
+
const defaultPkg = {
|
|
234
|
+
name: path.basename(targetDir),
|
|
235
|
+
version: "1.0.0",
|
|
236
|
+
type: "module",
|
|
237
|
+
dependencies: {},
|
|
238
|
+
};
|
|
239
|
+
fs.writeFileSync(pkgPath, JSON.stringify(defaultPkg, null, 2));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Initialize void.config.json
|
|
243
|
+
const configPath = path.join(targetDir, "void.config.json");
|
|
244
|
+
if (!fs.existsSync(configPath)) {
|
|
245
|
+
const config = {
|
|
246
|
+
registry: "./registry",
|
|
247
|
+
plugins: {},
|
|
248
|
+
};
|
|
249
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Write default application app.js
|
|
253
|
+
const appPath = path.join(targetDir, "app.js");
|
|
254
|
+
if (!fs.existsSync(appPath)) {
|
|
255
|
+
const appContent = `import math from "@tgrv/void-math";
|
|
256
|
+
|
|
257
|
+
console.log("=== Void Application ===");
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
console.log("Math sum (40 + 2):", math.add({ a: 40, b: 2 }));
|
|
261
|
+
} catch (e) {
|
|
262
|
+
console.error("Error running application:", e.message);
|
|
263
|
+
}
|
|
264
|
+
`;
|
|
265
|
+
fs.writeFileSync(appPath, appContent);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Install void-runtime via NPM
|
|
269
|
+
console.log(`${info} Running: ${colors.blue}npm install @tgrv/void-runtime${colors.reset}`);
|
|
270
|
+
const npmSuccess = runCommand("npm install @tgrv/void-runtime", { cwd: targetDir });
|
|
271
|
+
if (!npmSuccess) {
|
|
272
|
+
console.error(`${cross} ${colors.red}Failed to install @tgrv/void-runtime via npm.${colors.reset}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(`${tick} ${colors.green}Successfully initialized Void project!${colors.reset}`);
|
|
276
|
+
console.log(`\nTo add plugins, you can run:`);
|
|
277
|
+
console.log(` ${colors.cyan}npx void add <plugin-name>${colors.reset}`);
|
|
278
|
+
console.log(`For example:`);
|
|
279
|
+
console.log(` ${colors.cyan}npx void add @tgrv/void-math${colors.reset}\n`);
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
case "create": {
|
|
284
|
+
const pluginPathArg = args[1] || ".";
|
|
285
|
+
const targetDir = path.resolve(process.cwd(), pluginPathArg);
|
|
286
|
+
const folderName = path.basename(targetDir);
|
|
287
|
+
|
|
288
|
+
// Prompt for language
|
|
289
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
290
|
+
const rawLanguage = await new Promise((resolve) => {
|
|
291
|
+
rl.question(`${info} Select plugin language (go/rust) [default: rust]: `, (ans) => {
|
|
292
|
+
resolve(ans.trim().toLowerCase() || "rust");
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
rl.close();
|
|
296
|
+
|
|
297
|
+
if (rawLanguage !== "rust" && rawLanguage !== "go") {
|
|
298
|
+
console.error(`${cross} ${colors.red}Error: Unsupported language: ${rawLanguage}${colors.reset}`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (rawLanguage === "rust") {
|
|
303
|
+
// Verify Cargo
|
|
304
|
+
const cargoCheck = runCommand("cargo --version");
|
|
305
|
+
if (!cargoCheck) {
|
|
306
|
+
console.error(`\n${cross} ${colors.red}Error: 'rustc' or 'cargo' is not installed on your machine.${colors.reset}`);
|
|
307
|
+
console.error(`${colors.yellow}Please install Rust from https://rustup.rs/ first.${colors.reset}\n`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log(`${info} Creating Rust plugin template at ${colors.bold}${targetDir}${colors.reset}...`);
|
|
312
|
+
ensureDir(targetDir);
|
|
313
|
+
const templateSrc = path.join(workspaceDir, "templates", "rust");
|
|
314
|
+
copyFolderRecursive(templateSrc, targetDir, {
|
|
315
|
+
"\\{\\{name\\}\\}": folderName,
|
|
316
|
+
"\\{\\{type\\}\\}": "rust",
|
|
317
|
+
"\\{\\{workspace_dir\\}\\}": workspaceDir.replace(/\\/g, "/"),
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
// Verify Go
|
|
321
|
+
const goCheck = runCommand("go version");
|
|
322
|
+
if (!goCheck) {
|
|
323
|
+
console.error(`\n${cross} ${colors.red}Error: 'go' is not installed on your machine.${colors.reset}`);
|
|
324
|
+
console.error(`${colors.yellow}Please install Go from https://go.dev/doc/install first.${colors.reset}\n`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log(`${info} Creating Go plugin template at ${colors.bold}${targetDir}${colors.reset}...`);
|
|
329
|
+
ensureDir(targetDir);
|
|
330
|
+
const templateSrc = path.join(workspaceDir, "templates", "go");
|
|
331
|
+
copyFolderRecursive(templateSrc, targetDir, {
|
|
332
|
+
"\\{\\{name\\}\\}": folderName,
|
|
333
|
+
"\\{\\{type\\}\\}": "go",
|
|
334
|
+
"\\{\\{workspace_dir\\}\\}": workspaceDir.replace(/\\/g, "/"),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log(`${tick} ${colors.green}Plugin created successfully under '${pluginPathArg}'!${colors.reset}\n`);
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
case "build": {
|
|
343
|
+
const pluginPathArg = args[1] || ".";
|
|
344
|
+
runBuild(pluginPathArg);
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
case "publish": {
|
|
349
|
+
const pluginPathArg = args[1] || ".";
|
|
350
|
+
// 1. Run build
|
|
351
|
+
const buildOutputDir = runBuild(pluginPathArg);
|
|
352
|
+
|
|
353
|
+
// 2. Call npm publish inside the output build folder
|
|
354
|
+
console.log(`\n${info} Running 'npm publish' inside: ${colors.bold}${buildOutputDir}${colors.reset}`);
|
|
355
|
+
const publishSuccess = runCommand("npm publish", { cwd: buildOutputDir });
|
|
356
|
+
if (!publishSuccess) {
|
|
357
|
+
console.log(`\n${warning} ${colors.yellow}NPM publish failed or completed as dry run (check NPM login credentials).${colors.reset}`);
|
|
358
|
+
} else {
|
|
359
|
+
console.log(`${tick} ${colors.green}Successfully published package to NPM!${colors.reset}\n`);
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
case "add": {
|
|
365
|
+
const pluginName = args[1];
|
|
366
|
+
if (!pluginName) {
|
|
367
|
+
console.error(`${cross} ${colors.red}Error: Please specify the plugin to add. e.g. void add @tgrv/void-math${colors.reset}`);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const configInfo = findConfig(process.cwd());
|
|
372
|
+
if (!configInfo) {
|
|
373
|
+
console.error(`${cross} ${colors.red}Error: void.config.json not found. Run 'void init' first.${colors.reset}`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const config = JSON.parse(fs.readFileSync(configInfo.configPath, "utf8"));
|
|
378
|
+
|
|
379
|
+
// Look up local build directory inside the plugins/ or packages/ directory tree
|
|
380
|
+
let localBuildDir = null;
|
|
381
|
+
|
|
382
|
+
const scanLocations = [
|
|
383
|
+
path.join(workspaceDir, "plugins"),
|
|
384
|
+
path.join(workspaceDir, "packages")
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
for (const root of scanLocations) {
|
|
388
|
+
if (fs.existsSync(root)) {
|
|
389
|
+
const subdirs = fs.readdirSync(root);
|
|
390
|
+
for (const subdir of subdirs) {
|
|
391
|
+
const pPath = path.join(root, subdir);
|
|
392
|
+
if (fs.statSync(pPath).isDirectory()) {
|
|
393
|
+
// Scans nested scopes (e.g. plugins/math/@tgrv/void-math)
|
|
394
|
+
const entries = fs.readdirSync(pPath);
|
|
395
|
+
for (const entry of entries) {
|
|
396
|
+
if (entry.startsWith("@")) {
|
|
397
|
+
const scopePath = path.join(pPath, entry);
|
|
398
|
+
const subfolders = fs.readdirSync(scopePath);
|
|
399
|
+
for (const name of subfolders) {
|
|
400
|
+
const pkgPath = path.join(scopePath, name, "package.json");
|
|
401
|
+
if (fs.existsSync(pkgPath)) {
|
|
402
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
403
|
+
if (pkg.name === pluginName) {
|
|
404
|
+
localBuildDir = path.join(scopePath, name);
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (localBuildDir) break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (localBuildDir) break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (localBuildDir) break;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const targetPluginDir = path.join(process.cwd(), "node_modules", pluginName);
|
|
420
|
+
ensureDir(path.dirname(targetPluginDir));
|
|
421
|
+
|
|
422
|
+
if (localBuildDir) {
|
|
423
|
+
console.log(`${info} Installing local build of '${colors.bold}${pluginName}${colors.reset}' from ${localBuildDir} to ${targetPluginDir}...`);
|
|
424
|
+
if (fs.existsSync(targetPluginDir)) {
|
|
425
|
+
fs.rmSync(targetPluginDir, { recursive: true, force: true });
|
|
426
|
+
}
|
|
427
|
+
copyFolderRecursive(localBuildDir, targetPluginDir);
|
|
428
|
+
} else {
|
|
429
|
+
console.log(`${info} Installing '${colors.bold}${pluginName}${colors.reset}' from NPM registry...`);
|
|
430
|
+
const npmSuccess = runCommand(`npm install ${pluginName}`, { cwd: process.cwd() });
|
|
431
|
+
if (!npmSuccess) {
|
|
432
|
+
console.error(`${cross} ${colors.red}Error: Failed to install package '${pluginName}' via npm.${colors.reset}`);
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Update package.json of the application
|
|
438
|
+
const pkgPath = path.join(process.cwd(), "package.json");
|
|
439
|
+
if (fs.existsSync(pkgPath)) {
|
|
440
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
441
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
442
|
+
pkg.dependencies[pluginName] = "1.0.0";
|
|
443
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Update void.config.json
|
|
447
|
+
config.plugins = config.plugins || {};
|
|
448
|
+
config.plugins[pluginName] = "^1.0.0";
|
|
449
|
+
fs.writeFileSync(configInfo.configPath, JSON.stringify(config, null, 2));
|
|
450
|
+
|
|
451
|
+
console.log(`${tick} ${colors.green}Successfully added '${pluginName}'!${colors.reset}\n`);
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
case "remove": {
|
|
456
|
+
const pluginName = args[1];
|
|
457
|
+
if (!pluginName) {
|
|
458
|
+
console.error(`${cross} ${colors.red}Error: Please specify the plugin to remove. e.g. void remove @tgrv/void-math${colors.reset}`);
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const configInfo = findConfig(process.cwd());
|
|
463
|
+
if (!configInfo) {
|
|
464
|
+
console.error(`${cross} ${colors.red}Error: void.config.json not found.${colors.reset}`);
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const config = JSON.parse(fs.readFileSync(configInfo.configPath, "utf8"));
|
|
469
|
+
|
|
470
|
+
console.log(`${info} Removing '${colors.bold}${pluginName}${colors.reset}'...`);
|
|
471
|
+
|
|
472
|
+
// Run npm uninstall
|
|
473
|
+
const npmSuccess = runCommand(`npm uninstall ${pluginName}`, { cwd: process.cwd() });
|
|
474
|
+
if (!npmSuccess) {
|
|
475
|
+
console.error(`${cross} ${colors.red}Error: Failed to uninstall package '${pluginName}' via npm.${colors.reset}`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Explicitly delete folder from node_modules if it exists
|
|
479
|
+
const targetPluginDir = path.join(process.cwd(), "node_modules", pluginName);
|
|
480
|
+
if (fs.existsSync(targetPluginDir)) {
|
|
481
|
+
fs.rmSync(targetPluginDir, { recursive: true, force: true });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Update package.json of the application
|
|
485
|
+
const pkgPath = path.join(process.cwd(), "package.json");
|
|
486
|
+
if (fs.existsSync(pkgPath)) {
|
|
487
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
488
|
+
if (pkg.dependencies && pkg.dependencies[pluginName]) {
|
|
489
|
+
delete pkg.dependencies[pluginName];
|
|
490
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Update void.config.json
|
|
495
|
+
if (config.plugins && config.plugins[pluginName]) {
|
|
496
|
+
delete config.plugins[pluginName];
|
|
497
|
+
fs.writeFileSync(configInfo.configPath, JSON.stringify(config, null, 2));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
console.log(`${tick} ${colors.green}Successfully removed '${pluginName}'!${colors.reset}\n`);
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
case "view": {
|
|
505
|
+
const pluginName = args[1];
|
|
506
|
+
if (!pluginName) {
|
|
507
|
+
console.error(`${cross} ${colors.red}Error: Please specify the plugin to view, e.g. void view @tgrv/void-math${colors.reset}`);
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const configInfo = findConfig(process.cwd());
|
|
512
|
+
if (!configInfo) {
|
|
513
|
+
console.error(`${cross} ${colors.red}Error: void.config.json not found.${colors.reset}`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const pluginDir = path.join(process.cwd(), "node_modules", pluginName);
|
|
518
|
+
if (!fs.existsSync(pluginDir)) {
|
|
519
|
+
console.error(`${cross} ${colors.red}Error: Plugin '${pluginName}' is not installed in this project.${colors.reset}`);
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const wasmPath = path.join(pluginDir, "plugin.wasm");
|
|
524
|
+
if (!fs.existsSync(wasmPath)) {
|
|
525
|
+
console.error(`${cross} ${colors.red}Error: WASM binary not found at: ${wasmPath}${colors.reset}`);
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Dynamically load the plugin and query its functions
|
|
530
|
+
try {
|
|
531
|
+
const runtimePath = path.join(process.cwd(), "node_modules", "@tgrv", "void-runtime", "index.js");
|
|
532
|
+
const { runtime } = await import(pathToFileURL(path.resolve(runtimePath)).toString());
|
|
533
|
+
const plugin = await runtime.load(wasmPath);
|
|
534
|
+
|
|
535
|
+
// Query reflected functions list
|
|
536
|
+
const functions = plugin.callInternal("__list_functions__", {});
|
|
537
|
+
|
|
538
|
+
console.log(`\n${colors.bold}${colors.magenta}=========================================${colors.reset}`);
|
|
539
|
+
console.log(`${colors.bold}${colors.cyan}Plugin '${pluginName}' Details:${colors.reset}`);
|
|
540
|
+
console.log(`${colors.bold}${colors.magenta}=========================================${colors.reset}`);
|
|
541
|
+
console.log(`${colors.bold}Location:${colors.reset} ${pluginDir}`);
|
|
542
|
+
console.log(`${colors.bold}Functions:${colors.reset}`);
|
|
543
|
+
if (Array.isArray(functions) && functions.length > 0) {
|
|
544
|
+
for (const f of functions) {
|
|
545
|
+
console.log(" " + tick + " " + f);
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
console.log(` (no functions registered)`);
|
|
549
|
+
}
|
|
550
|
+
console.log(`${colors.bold}${colors.magenta}=========================================${colors.reset}\n`);
|
|
551
|
+
} catch (err) {
|
|
552
|
+
console.error(`${cross} ${colors.red}Error loading plugin details: ${err.message}${colors.reset}`);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
default:
|
|
559
|
+
console.error(`${cross} ${colors.red}Unknown command: ${command}${colors.reset}`);
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
main().catch((err) => {
|
|
565
|
+
console.error("CLI error:", err);
|
|
566
|
+
process.exit(1);
|
|
567
|
+
});
|