@cwe-platform/plugin-cli 0.1.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 +28 -0
- package/bin/cwe-plugin.mjs +124 -0
- package/package.json +25 -0
- package/src/lib.mjs +84 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @cwe-platform/plugin-cli
|
|
2
|
+
|
|
3
|
+
The CWE plugin developer loop. Thin by design: `build` runs your plugin's own tsup; `dev` and
|
|
4
|
+
`validate` talk to a dev runtime's harness endpoints.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npx @cwe-platform/plugin-cli build # tsup + local validateManifest (doctor-lite)
|
|
8
|
+
npx @cwe-platform/plugin-cli push # build once + sideload to the dev runtime
|
|
9
|
+
npx @cwe-platform/plugin-cli dev # watch src/ → rebuild → hot-reload sideload
|
|
10
|
+
npx @cwe-platform/plugin-cli validate # full plugin doctor on the runtime
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Config — `cwe-plugin.json` in your plugin repo:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"runtimeUrl": "http://localhost:3000",
|
|
18
|
+
"pluginKey": "player-favorites",
|
|
19
|
+
"devToken": "<staff bearer token>",
|
|
20
|
+
"bundle": "dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The sideload endpoint (`PUT /dev/plugins/:key/bundle`) exists only on runtimes with the dev
|
|
25
|
+
harness enabled (`docker compose -f docker-compose.plugindev.yml up` in the platform repo);
|
|
26
|
+
production runtimes reject the harness at config time, so the endpoint 404s there.
|
|
27
|
+
|
|
28
|
+
MIT © CasinoWebEngine
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cwe-plugin — the CWE plugin developer loop:
|
|
4
|
+
*
|
|
5
|
+
* cwe-plugin build tsup build + local manifest validation
|
|
6
|
+
* cwe-plugin dev watch → rebuild → sideload → report (sub-5s reload)
|
|
7
|
+
* cwe-plugin validate run the runtime's plugin doctor
|
|
8
|
+
*
|
|
9
|
+
* Config: cwe-plugin.json (runtimeUrl, pluginKey, devToken, bundle?).
|
|
10
|
+
*/
|
|
11
|
+
import { watch } from "node:fs";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
import { Command } from "commander";
|
|
15
|
+
import { fetchTrace, loadConfig, runBuild, sideload, validateOnRuntime } from "../src/lib.mjs";
|
|
16
|
+
|
|
17
|
+
const program = new Command();
|
|
18
|
+
program.name("cwe-plugin").description("CasinoWebEngine plugin developer CLI").version("0.1.0");
|
|
19
|
+
|
|
20
|
+
async function validateLocal() {
|
|
21
|
+
// Import the built bundle + the SDK's doctor-lite from the PLUGIN's own
|
|
22
|
+
// node_modules (the CLI has no SDK dependency of its own — no dual copies).
|
|
23
|
+
const config = await loadConfig();
|
|
24
|
+
const bundleUrl = pathToFileURL(resolve(process.cwd(), config.bundle)).href;
|
|
25
|
+
const testingUrl = pathToFileURL(
|
|
26
|
+
resolve(process.cwd(), "node_modules/@cwe-platform/plugin-sdk/dist/testing.js"),
|
|
27
|
+
).href;
|
|
28
|
+
const [{ default: plugin }, { validateManifest }] = await Promise.all([
|
|
29
|
+
import(`${bundleUrl}?t=${Date.now()}`),
|
|
30
|
+
import(testingUrl),
|
|
31
|
+
]);
|
|
32
|
+
const result = validateManifest(plugin);
|
|
33
|
+
for (const warning of result.warnings) console.warn(` ⚠ ${warning}`);
|
|
34
|
+
for (const error of result.errors) console.error(` ✗ ${error}`);
|
|
35
|
+
if (!result.ok) throw new Error("local manifest validation failed");
|
|
36
|
+
console.log(" ✓ manifest valid (doctor-lite)");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.command("build")
|
|
41
|
+
.description("tsup build + local validateManifest")
|
|
42
|
+
.action(async () => {
|
|
43
|
+
await runBuild();
|
|
44
|
+
await validateLocal();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
program
|
|
48
|
+
.command("validate")
|
|
49
|
+
.description("run the plugin doctor on the dev runtime")
|
|
50
|
+
.action(async () => {
|
|
51
|
+
const config = await loadConfig();
|
|
52
|
+
const report = await validateOnRuntime(config);
|
|
53
|
+
for (const check of report.checks ?? []) {
|
|
54
|
+
console.log(` ${check.ok ? "✓" : "✗"} ${check.check}${check.detail ? ` (${check.detail})` : ""}`);
|
|
55
|
+
}
|
|
56
|
+
for (const warning of report.warnings ?? []) console.warn(` ⚠ ${warning}`);
|
|
57
|
+
if (!report.ok) process.exit(1);
|
|
58
|
+
console.log(`\n${report.plugin}@${report.version}: doctor OK`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
program
|
|
62
|
+
.command("push")
|
|
63
|
+
.description("build once and sideload to the dev runtime")
|
|
64
|
+
.action(async () => {
|
|
65
|
+
const config = await loadConfig();
|
|
66
|
+
await runBuild();
|
|
67
|
+
const result = await sideload(config);
|
|
68
|
+
console.log(` ✓ sideloaded ${result.plugin}@${result.version} (dev channel, hot-reloaded)`);
|
|
69
|
+
for (const warning of result.warnings ?? []) console.warn(` ⚠ ${warning}`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
program
|
|
73
|
+
.command("dev")
|
|
74
|
+
.description("watch src/ → rebuild → sideload → tail the harness trace")
|
|
75
|
+
.action(async () => {
|
|
76
|
+
const config = await loadConfig();
|
|
77
|
+
let building = false;
|
|
78
|
+
let dirty = false;
|
|
79
|
+
|
|
80
|
+
const cycle = async () => {
|
|
81
|
+
if (building) {
|
|
82
|
+
dirty = true;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
building = true;
|
|
86
|
+
const startedAt = Date.now();
|
|
87
|
+
try {
|
|
88
|
+
await runBuild();
|
|
89
|
+
const result = await sideload(config);
|
|
90
|
+
console.log(
|
|
91
|
+
` ✓ ${result.plugin}@${result.version} reloaded in ${Date.now() - startedAt}ms`,
|
|
92
|
+
);
|
|
93
|
+
for (const warning of result.warnings ?? []) console.warn(` ⚠ ${warning}`);
|
|
94
|
+
try {
|
|
95
|
+
const trace = await fetchTrace(config);
|
|
96
|
+
const last = trace.steps?.[trace.steps.length - 1];
|
|
97
|
+
if (last) console.log(` ↳ last trace step: ${last.step} (${last.ok ? "ok" : "FAILED"})`);
|
|
98
|
+
} catch {
|
|
99
|
+
// No trace yet — fine.
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(` ✗ ${error instanceof Error ? error.message : error}`);
|
|
103
|
+
} finally {
|
|
104
|
+
building = false;
|
|
105
|
+
if (dirty) {
|
|
106
|
+
dirty = false;
|
|
107
|
+
void cycle();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
console.log(`watching src/ — sideloading '${config.pluginKey}' to ${config.runtimeUrl}`);
|
|
113
|
+
await cycle();
|
|
114
|
+
let timer;
|
|
115
|
+
watch(resolve(process.cwd(), "src"), { recursive: true }, () => {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
timer = setTimeout(() => void cycle(), 150); // debounce editor bursts
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
program.parseAsync().catch((error) => {
|
|
122
|
+
console.error(error instanceof Error ? error.message : error);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cwe-platform/plugin-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CasinoWebEngine plugin developer CLI — build, validate and hot-reload plugins against a dev runtime.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"private": false,
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"cwe-plugin": "./bin/cwe-plugin.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"commander": "^12.1.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node --test test/"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/lib.mjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cwe-platform/plugin-cli internals. Deliberately thin: build = the plugin's
|
|
3
|
+
* own tsup, validate/dev = HTTP against the dev runtime's harness endpoints.
|
|
4
|
+
* Plain ESM JS — the CLI ships source, no build step of its own.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
/** cwe-plugin.json: { runtimeUrl, devToken, pluginKey, bundle? } */
|
|
12
|
+
export async function loadConfig(cwd = process.cwd()) {
|
|
13
|
+
const path = join(cwd, "cwe-plugin.json");
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = await readFile(path, "utf8");
|
|
17
|
+
} catch {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Missing cwe-plugin.json in ${cwd} — create it with { "runtimeUrl": "http://localhost:3000", "pluginKey": "<key>", "devToken": "<staff token>" }`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const config = JSON.parse(raw);
|
|
23
|
+
for (const field of ["runtimeUrl", "pluginKey"]) {
|
|
24
|
+
if (!config[field]) throw new Error(`cwe-plugin.json is missing '${field}'`);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
runtimeUrl: String(config.runtimeUrl).replace(/\/$/, ""),
|
|
28
|
+
pluginKey: config.pluginKey,
|
|
29
|
+
devToken: config.devToken ?? "",
|
|
30
|
+
bundle: config.bundle ?? "dist/index.js",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function runBuild(cwd = process.cwd()) {
|
|
35
|
+
return new Promise((resolvePromise, reject) => {
|
|
36
|
+
const child = spawn("npx", ["tsup"], { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
37
|
+
child.on("exit", (code) => {
|
|
38
|
+
if (code === 0) resolvePromise(undefined);
|
|
39
|
+
else reject(new Error(`tsup exited with code ${code}`));
|
|
40
|
+
});
|
|
41
|
+
child.on("error", reject);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function api(config, method, path, body) {
|
|
46
|
+
const response = await fetch(`${config.runtimeUrl}${path}`, {
|
|
47
|
+
method,
|
|
48
|
+
headers: {
|
|
49
|
+
"content-type": "application/json",
|
|
50
|
+
...(config.devToken ? { authorization: `Bearer ${config.devToken}` } : {}),
|
|
51
|
+
},
|
|
52
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
53
|
+
});
|
|
54
|
+
const text = await response.text();
|
|
55
|
+
let json;
|
|
56
|
+
try {
|
|
57
|
+
json = text ? JSON.parse(text) : {};
|
|
58
|
+
} catch {
|
|
59
|
+
json = { raw: text };
|
|
60
|
+
}
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const message = json?.error?.message ?? json?.message ?? text.slice(0, 300);
|
|
63
|
+
throw new Error(`${method} ${path} → HTTP ${response.status}: ${message}`);
|
|
64
|
+
}
|
|
65
|
+
return json;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Upload the built bundle to the dev runtime (checksum-verified sideload). */
|
|
69
|
+
export async function sideload(config, cwd = process.cwd()) {
|
|
70
|
+
const bundlePath = resolve(cwd, config.bundle);
|
|
71
|
+
const bundle = await readFile(bundlePath, "utf8");
|
|
72
|
+
const checksum = createHash("sha256").update(bundle, "utf8").digest("hex");
|
|
73
|
+
return api(config, "PUT", `/dev/plugins/${config.pluginKey}/bundle`, { bundle, checksum });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Run the full plugin doctor on the runtime. */
|
|
77
|
+
export function validateOnRuntime(config) {
|
|
78
|
+
return api(config, "POST", `/dev/plugins/${config.pluginKey}/validate`, {});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Fetch the last stored harness trace (plugin logs/steps). */
|
|
82
|
+
export function fetchTrace(config) {
|
|
83
|
+
return api(config, "GET", `/dev/plugins/${config.pluginKey}/trace`);
|
|
84
|
+
}
|