@cwe-platform/plugin-cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/bin/cwe-plugin.mjs +74 -4
- package/package.json +1 -1
- package/src/lib.mjs +69 -2
package/README.md
CHANGED
|
@@ -4,25 +4,32 @@ The CWE plugin developer loop. Thin by design: `build` runs your plugin's own ts
|
|
|
4
4
|
`validate` talk to a dev runtime's harness endpoints.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
|
+
npx @cwe-platform/plugin-cli login # staff login → saves devToken into cwe-plugin.json
|
|
7
8
|
npx @cwe-platform/plugin-cli build # tsup + local validateManifest (doctor-lite)
|
|
8
9
|
npx @cwe-platform/plugin-cli push # build once + sideload to the dev runtime
|
|
9
10
|
npx @cwe-platform/plugin-cli dev # watch src/ → rebuild → hot-reload sideload
|
|
10
11
|
npx @cwe-platform/plugin-cli validate # full plugin doctor on the runtime
|
|
11
12
|
```
|
|
12
13
|
|
|
13
|
-
Config — `cwe-plugin.json` in your plugin repo:
|
|
14
|
+
Config — `cwe-plugin.json` in your plugin repo (`login` fills in `devToken` for you):
|
|
14
15
|
|
|
15
16
|
```json
|
|
16
17
|
{
|
|
17
18
|
"runtimeUrl": "http://localhost:3000",
|
|
18
19
|
"pluginKey": "player-favorites",
|
|
19
|
-
"devToken": "<staff bearer token>",
|
|
20
20
|
"bundle": "dist/index.js"
|
|
21
21
|
}
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
## Auth
|
|
25
|
+
|
|
26
|
+
`devToken` is a staff access token holding the `plugin:manage` permission. `cwe-plugin login`
|
|
27
|
+
prompts for staff credentials, calls `POST /staff/auth/login` on the runtime and stores the
|
|
28
|
+
token. Tokens are short-lived (default 15 min); for long `dev` sessions export
|
|
29
|
+
`CWE_STAFF_EMAIL` / `CWE_STAFF_PASSWORD` and the watcher re-logs-in automatically on 401.
|
|
30
|
+
|
|
24
31
|
The sideload endpoint (`PUT /dev/plugins/:key/bundle`) exists only on runtimes with the dev
|
|
25
|
-
harness enabled
|
|
26
|
-
|
|
32
|
+
harness enabled; production runtimes reject the harness at config time, so the endpoint 404s
|
|
33
|
+
there. Ask your CWE platform contact for a dev runtime.
|
|
27
34
|
|
|
28
35
|
MIT © CasinoWebEngine
|
package/bin/cwe-plugin.mjs
CHANGED
|
@@ -9,13 +9,63 @@
|
|
|
9
9
|
* Config: cwe-plugin.json (runtimeUrl, pluginKey, devToken, bundle?).
|
|
10
10
|
*/
|
|
11
11
|
import { watch } from "node:fs";
|
|
12
|
+
import { createInterface } from "node:readline";
|
|
12
13
|
import { pathToFileURL } from "node:url";
|
|
13
14
|
import { resolve } from "node:path";
|
|
14
15
|
import { Command } from "commander";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
fetchTrace,
|
|
18
|
+
loadConfig,
|
|
19
|
+
reloginFromEnv,
|
|
20
|
+
runBuild,
|
|
21
|
+
saveDevToken,
|
|
22
|
+
sideload,
|
|
23
|
+
staffLogin,
|
|
24
|
+
validateOnRuntime,
|
|
25
|
+
} from "../src/lib.mjs";
|
|
16
26
|
|
|
17
27
|
const program = new Command();
|
|
18
|
-
program.name("cwe-plugin").description("CasinoWebEngine plugin developer CLI").version("0.1.
|
|
28
|
+
program.name("cwe-plugin").description("CasinoWebEngine plugin developer CLI").version("0.1.1");
|
|
29
|
+
|
|
30
|
+
/** Interactive prompt; `hidden` masks the input (password entry). */
|
|
31
|
+
function prompt(question, { hidden = false } = {}) {
|
|
32
|
+
return new Promise((resolvePrompt) => {
|
|
33
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
34
|
+
if (hidden) {
|
|
35
|
+
// Mask by rewriting the line with asterisks on every keypress echo.
|
|
36
|
+
const write = rl._writeToOutput.bind(rl);
|
|
37
|
+
rl._writeToOutput = (str) => {
|
|
38
|
+
if (str.includes(question)) write(str);
|
|
39
|
+
else write("*");
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
rl.question(question, (answer) => {
|
|
43
|
+
rl.close();
|
|
44
|
+
if (hidden) process.stdout.write("\n");
|
|
45
|
+
resolvePrompt(answer.trim());
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
program
|
|
51
|
+
.command("login")
|
|
52
|
+
.description("staff login against the dev runtime; saves devToken into cwe-plugin.json")
|
|
53
|
+
.option("--email <email>", "staff email (prompted if omitted)")
|
|
54
|
+
.option("--password <password>", "staff password (prompted if omitted — prefer the prompt: flags land in shell history)")
|
|
55
|
+
.action(async (opts) => {
|
|
56
|
+
const config = await loadConfig();
|
|
57
|
+
const email = opts.email ?? (await prompt("staff email: "));
|
|
58
|
+
const password = opts.password ?? (await prompt("staff password: ", { hidden: true }));
|
|
59
|
+
const token = await staffLogin(config.runtimeUrl, email, password);
|
|
60
|
+
const path = await saveDevToken(token);
|
|
61
|
+
console.log(` ✓ logged in — devToken saved to ${path}`);
|
|
62
|
+
console.log(
|
|
63
|
+
" ℹ staff tokens are short-lived (default 15 min). For long `dev` sessions, export",
|
|
64
|
+
);
|
|
65
|
+
console.log(
|
|
66
|
+
" CWE_STAFF_EMAIL / CWE_STAFF_PASSWORD and the watcher re-logs-in on 401 automatically.",
|
|
67
|
+
);
|
|
68
|
+
});
|
|
19
69
|
|
|
20
70
|
async function validateLocal() {
|
|
21
71
|
// Import the built bundle + the SDK's doctor-lite from the PLUGIN's own
|
|
@@ -58,13 +108,31 @@ program
|
|
|
58
108
|
console.log(`\n${report.plugin}@${report.version}: doctor OK`);
|
|
59
109
|
});
|
|
60
110
|
|
|
111
|
+
/** Sideload with one 401 → env-credential re-login retry. */
|
|
112
|
+
async function sideloadWithRelogin(config) {
|
|
113
|
+
try {
|
|
114
|
+
return await sideload(config);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error?.unauthorized && (await reloginFromEnv(config))) {
|
|
117
|
+
console.log(" ↻ devToken expired — re-logged-in from CWE_STAFF_EMAIL/PASSWORD");
|
|
118
|
+
return sideload(config);
|
|
119
|
+
}
|
|
120
|
+
if (error?.unauthorized) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
"devToken missing/expired — run `cwe-plugin login` (or export CWE_STAFF_EMAIL/CWE_STAFF_PASSWORD for auto re-login)",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
61
129
|
program
|
|
62
130
|
.command("push")
|
|
63
131
|
.description("build once and sideload to the dev runtime")
|
|
64
132
|
.action(async () => {
|
|
65
133
|
const config = await loadConfig();
|
|
66
134
|
await runBuild();
|
|
67
|
-
const result = await
|
|
135
|
+
const result = await sideloadWithRelogin(config);
|
|
68
136
|
console.log(` ✓ sideloaded ${result.plugin}@${result.version} (dev channel, hot-reloaded)`);
|
|
69
137
|
for (const warning of result.warnings ?? []) console.warn(` ⚠ ${warning}`);
|
|
70
138
|
});
|
|
@@ -86,7 +154,9 @@ program
|
|
|
86
154
|
const startedAt = Date.now();
|
|
87
155
|
try {
|
|
88
156
|
await runBuild();
|
|
89
|
-
|
|
157
|
+
// 401s re-login from env credentials and retry once, so the 15-min
|
|
158
|
+
// token TTL never interrupts a watch session.
|
|
159
|
+
const result = await sideloadWithRelogin(config);
|
|
90
160
|
console.log(
|
|
91
161
|
` ✓ ${result.plugin}@${result.version} reloaded in ${Date.now() - startedAt}ms`,
|
|
92
162
|
);
|
package/package.json
CHANGED
package/src/lib.mjs
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* Plain ESM JS — the CLI ships source, no build step of its own.
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
|
-
import { readFile } from "node:fs/promises";
|
|
7
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { join, resolve } from "node:path";
|
|
10
10
|
|
|
11
|
+
export const STAFF_ACCESS_COOKIE = "cwe_staff_access_token";
|
|
12
|
+
|
|
11
13
|
/** cwe-plugin.json: { runtimeUrl, devToken, pluginKey, bundle? } */
|
|
12
14
|
export async function loadConfig(cwd = process.cwd()) {
|
|
13
15
|
const path = join(cwd, "cwe-plugin.json");
|
|
@@ -60,11 +62,76 @@ async function api(config, method, path, body) {
|
|
|
60
62
|
}
|
|
61
63
|
if (!response.ok) {
|
|
62
64
|
const message = json?.error?.message ?? json?.message ?? text.slice(0, 300);
|
|
63
|
-
|
|
65
|
+
const error = new Error(`${method} ${path} → HTTP ${response.status}: ${message}`);
|
|
66
|
+
// Marker for callers: 401 = missing/expired devToken → try a re-login.
|
|
67
|
+
error.unauthorized = response.status === 401;
|
|
68
|
+
throw error;
|
|
64
69
|
}
|
|
65
70
|
return json;
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Staff login → staff access token. The API deliberately never returns the
|
|
75
|
+
* JWT in the response body — it arrives as the HttpOnly staff cookie — so
|
|
76
|
+
* this parses it out of Set-Cookie. Tokens are tenant-bound and short-lived
|
|
77
|
+
* (AUTH_ACCESS_TOKEN_TTL_SECONDS, default 15 min).
|
|
78
|
+
*/
|
|
79
|
+
export async function staffLogin(runtimeUrl, email, password) {
|
|
80
|
+
const response = await fetch(`${runtimeUrl.replace(/\/$/, "")}/staff/auth/login`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "content-type": "application/json" },
|
|
83
|
+
body: JSON.stringify({ email, password }),
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
let message = `HTTP ${response.status}`;
|
|
87
|
+
try {
|
|
88
|
+
const json = await response.json();
|
|
89
|
+
message = json?.error?.message ?? json?.message ?? message;
|
|
90
|
+
} catch {
|
|
91
|
+
// keep the status-only message
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`staff login failed: ${message}`);
|
|
94
|
+
}
|
|
95
|
+
const cookies = response.headers.getSetCookie?.() ?? [];
|
|
96
|
+
const tokenCookie = cookies.find((c) => c.startsWith(`${STAFF_ACCESS_COOKIE}=`));
|
|
97
|
+
if (!tokenCookie) {
|
|
98
|
+
throw new Error("login succeeded but the staff access token cookie was not returned");
|
|
99
|
+
}
|
|
100
|
+
const token = tokenCookie.split(";")[0].slice(STAFF_ACCESS_COOKIE.length + 1);
|
|
101
|
+
if (!token) throw new Error("staff access token cookie was empty");
|
|
102
|
+
return token;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Persist the devToken into cwe-plugin.json (preserving other fields). */
|
|
106
|
+
export async function saveDevToken(token, cwd = process.cwd()) {
|
|
107
|
+
const path = join(cwd, "cwe-plugin.json");
|
|
108
|
+
let config = {};
|
|
109
|
+
try {
|
|
110
|
+
config = JSON.parse(await readFile(path, "utf8"));
|
|
111
|
+
} catch {
|
|
112
|
+
throw new Error(`Missing cwe-plugin.json in ${cwd} — create it first (see README)`);
|
|
113
|
+
}
|
|
114
|
+
config.devToken = token;
|
|
115
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
116
|
+
return path;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Re-login with credentials from CWE_STAFF_EMAIL / CWE_STAFF_PASSWORD env
|
|
121
|
+
* vars (for unattended `dev` sessions — the 15-min token TTL otherwise
|
|
122
|
+
* interrupts the loop). Returns the fresh token, or null when the env vars
|
|
123
|
+
* are not both set.
|
|
124
|
+
*/
|
|
125
|
+
export async function reloginFromEnv(config, env = process.env) {
|
|
126
|
+
const email = env.CWE_STAFF_EMAIL;
|
|
127
|
+
const password = env.CWE_STAFF_PASSWORD;
|
|
128
|
+
if (!email || !password) return null;
|
|
129
|
+
const token = await staffLogin(config.runtimeUrl, email, password);
|
|
130
|
+
await saveDevToken(token);
|
|
131
|
+
config.devToken = token;
|
|
132
|
+
return token;
|
|
133
|
+
}
|
|
134
|
+
|
|
68
135
|
/** Upload the built bundle to the dev runtime (checksum-verified sideload). */
|
|
69
136
|
export async function sideload(config, cwd = process.cwd()) {
|
|
70
137
|
const bundlePath = resolve(cwd, config.bundle);
|