@clovnet/plugin-cli 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/CHANGELOG.md +75 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/bin/cwe-plugin.mjs +14 -0
- package/dist/cli.js +10400 -0
- package/package.json +62 -0
- package/templates/blank/README.md +60 -0
- package/templates/blank/_gitignore +3 -0
- package/templates/blank/cwe-plugin.json +9 -0
- package/templates/blank/package.json +31 -0
- package/templates/blank/settings.dev.json +8 -0
- package/templates/blank/src/handlers/hooks.ts +29 -0
- package/templates/blank/src/handlers/jobs.ts +9 -0
- package/templates/blank/src/handlers/routes.ts +44 -0
- package/templates/blank/src/index.ts +68 -0
- package/templates/blank/src/settings.ts +23 -0
- package/templates/blank/test/hooks.test.ts +43 -0
- package/templates/blank/tsconfig.json +14 -0
- package/templates/cashback/README.md +61 -0
- package/templates/cashback/_gitignore +3 -0
- package/templates/cashback/cwe-plugin.json +9 -0
- package/templates/cashback/package.json +31 -0
- package/templates/cashback/settings.dev.json +9 -0
- package/templates/cashback/src/handlers/hooks.ts +19 -0
- package/templates/cashback/src/handlers/jobs.ts +23 -0
- package/templates/cashback/src/handlers/routes.ts +188 -0
- package/templates/cashback/src/index.ts +128 -0
- package/templates/cashback/src/settings.ts +29 -0
- package/templates/cashback/test/hooks.test.ts +83 -0
- package/templates/cashback/tsconfig.json +14 -0
- package/templates/provider-skeleton/README.md +63 -0
- package/templates/provider-skeleton/_gitignore +3 -0
- package/templates/provider-skeleton/cwe-plugin.json +9 -0
- package/templates/provider-skeleton/package.json +31 -0
- package/templates/provider-skeleton/settings.dev.json +10 -0
- package/templates/provider-skeleton/src/adapter.ts +164 -0
- package/templates/provider-skeleton/src/fake-backend.ts +56 -0
- package/templates/provider-skeleton/src/handlers/hooks.ts +30 -0
- package/templates/provider-skeleton/src/handlers/jobs.ts +16 -0
- package/templates/provider-skeleton/src/handlers/routes.ts +128 -0
- package/templates/provider-skeleton/src/index.ts +83 -0
- package/templates/provider-skeleton/src/settings.ts +37 -0
- package/templates/provider-skeleton/test/hooks.test.ts +83 -0
- package/templates/provider-skeleton/tsconfig.json +14 -0
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clovnet/plugin-cli",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Clovnet plugin developer CLI \u2014 create, dev-loop (hot reload + live logs), lifecycle-test and publish plugins against a Clovnet runtime.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"private": false,
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"clovnet-plugin": "bin/cwe-plugin.mjs",
|
|
13
|
+
"cwe-plugin": "bin/cwe-plugin.mjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"dist",
|
|
18
|
+
"templates",
|
|
19
|
+
"README.md",
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"test": "pnpm build && tsx --test test/*.test.ts",
|
|
26
|
+
"verify": "pnpm build && tsx --test test/*.test.ts",
|
|
27
|
+
"prepack": "pnpm build"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"esbuild": "^0.24.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@clovnet/plugin-sdk": "workspace:*",
|
|
34
|
+
"@cwe/shared": "workspace:*",
|
|
35
|
+
"@types/node": "^20.16.5",
|
|
36
|
+
"commander": "^12.1.0",
|
|
37
|
+
"tsup": "^8.3.0",
|
|
38
|
+
"tsx": "^4.19.1",
|
|
39
|
+
"typescript": "^5.6.2",
|
|
40
|
+
"zod": "^3.23.8"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"casinowebengine",
|
|
44
|
+
"cwe",
|
|
45
|
+
"plugin",
|
|
46
|
+
"cli",
|
|
47
|
+
"igaming"
|
|
48
|
+
],
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/Clovnet/casino-runtime-core.git",
|
|
55
|
+
"directory": "packages/plugin-cli"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://clovnet.com",
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/Clovnet/casino-runtime-core/issues"
|
|
60
|
+
},
|
|
61
|
+
"author": "Clovnet"
|
|
62
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# __PLUGIN_NAME__
|
|
2
|
+
|
|
3
|
+
A CasinoWebEngine plugin, scaffolded with `cwe-plugin create`.
|
|
4
|
+
|
|
5
|
+
## The golden path
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm create cwe-plugin my-cashback # scaffold: manifest, handlers, tests, config
|
|
9
|
+
cd my-cashback && pnpm install
|
|
10
|
+
|
|
11
|
+
pnpm dev # = cwe-plugin dev
|
|
12
|
+
# → first contact with the scaffold's preset `local` environment: the handshake verifies
|
|
13
|
+
# class=local + devHarness=true and pins its fingerprint → green banner
|
|
14
|
+
# → doctor passes → sandbox tenant resolved → plugin installed+enabled (prompted)
|
|
15
|
+
# → esbuild watch starts, bundle hot-loaded, log stream attached
|
|
16
|
+
# → edit src/handlers/routes.ts, save → rebuilt + hot-reloaded in <2s, logs streaming live
|
|
17
|
+
|
|
18
|
+
# in the dev session (keyboard): i install · e enable · c configure · x disable · u uninstall
|
|
19
|
+
# r reload · d doctor · l clear · q quit
|
|
20
|
+
# each lifecycle key prints a full hook trace: capability calls, events, audit refs, state
|
|
21
|
+
|
|
22
|
+
pnpm test # unit tests on hooks/handlers via the SDK test kit
|
|
23
|
+
|
|
24
|
+
cwe-plugin lifecycle cycle # conformance sweep: install→enable→configure→disable→
|
|
25
|
+
# uninstall→reinstall, asserting idempotency + clean teardown
|
|
26
|
+
|
|
27
|
+
cwe-plugin login # only needed for shared dev clusters / publishing
|
|
28
|
+
cwe-plugin env add dev-eu https://dev-eu.cwe.dev && cwe-plugin env use dev-eu
|
|
29
|
+
pnpm dev # same loop against the shared cluster, per-dev sandbox
|
|
30
|
+
|
|
31
|
+
pnpm publish:dev # = cwe-plugin publish — always the dev channel:
|
|
32
|
+
# doctor + codegen-diff gate + publish
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## What's in this template
|
|
36
|
+
|
|
37
|
+
| File | Purpose |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `cwe-plugin.json` | project config: plugin key, entry, environments (committed — no secrets, ever) |
|
|
40
|
+
| `src/index.ts` | `definePlugin({ manifest, hooks, handlers })` — the whole platform contract |
|
|
41
|
+
| `src/settings.ts` | Zod-typed tenant settings (one secret field included) |
|
|
42
|
+
| `src/handlers/hooks.ts` | `onInstall` / `onConfigure` / `onUninstall` with commented examples |
|
|
43
|
+
| `src/handlers/routes.ts` | one public route + one player action |
|
|
44
|
+
| `src/handlers/jobs.ts` | one cron job (dry-run with `cwe-plugin jobs run heartbeat`) |
|
|
45
|
+
| `settings.dev.json` | sandbox settings applied by `cwe-plugin lifecycle configure` / the `c` key |
|
|
46
|
+
| `test/hooks.test.ts` | in-memory test-context examples — breakpoints work under `vitest --inspect` |
|
|
47
|
+
|
|
48
|
+
## Useful commands
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cwe-plugin env status # am I pointed at a dev environment? auth? sandbox state?
|
|
52
|
+
cwe-plugin invoke GET /info # execute a route without curl (add --surface player --player p1)
|
|
53
|
+
cwe-plugin settings set greeting=hi # validated locally against the schema before it is sent
|
|
54
|
+
cwe-plugin logs --level warn # standalone log tail for a second terminal
|
|
55
|
+
cwe-plugin doctor --remote # full server-side validation
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Machine-local state (pinned environment fingerprints, log cursors) lives in `.cwe/` — gitignored
|
|
59
|
+
and always safe to delete. Credentials never touch the repo: `cwe-plugin login` stores them in the
|
|
60
|
+
OS keychain.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cwe-plugin-__PLUGIN_KEY__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "__PLUGIN_NAME__ \u2014 a CasinoWebEngine plugin",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "cwe-plugin dev",
|
|
12
|
+
"doctor": "cwe-plugin doctor",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"build": "cwe-plugin build",
|
|
15
|
+
"codegen": "cwe-plugin codegen client",
|
|
16
|
+
"publish:dev": "cwe-plugin publish"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@clovnet/plugin-sdk": "^0.1.4"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"zod": "^3.23.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@clovnet/plugin-cli": "^0.2.0",
|
|
26
|
+
"@types/node": "^20.16.5",
|
|
27
|
+
"typescript": "^5.6.2",
|
|
28
|
+
"vitest": "^2.1.1",
|
|
29
|
+
"zod": "^3.23.8"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PluginContext } from "@clovnet/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lifecycle hooks. The platform runs each with a 5s watchdog (lifted to 10
|
|
5
|
+
* minutes under `cwe-plugin lifecycle <action> --debug` so breakpoints work);
|
|
6
|
+
* a throwing hook marks the plugin `errored` — it never corrupts core data.
|
|
7
|
+
* Hooks must be idempotent: `cwe-plugin lifecycle cycle` asserts a re-run of
|
|
8
|
+
* every action is a no-op.
|
|
9
|
+
*/
|
|
10
|
+
export async function onInstall(ctx: PluginContext): Promise<void> {
|
|
11
|
+
ctx.logger.info("__PLUGIN_KEY__.installed");
|
|
12
|
+
// Seed data here (idempotently), e.g.:
|
|
13
|
+
// await ctx.datasets.collection("records").put("welcome", { id: "welcome" });
|
|
14
|
+
// For long-running seeds declare `install: { seedTask: "seed" }` in the
|
|
15
|
+
// manifest and implement it under handlers.tasks — it runs async with
|
|
16
|
+
// checkpoints instead of blocking the install.
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function onConfigure(ctx: PluginContext): Promise<void> {
|
|
20
|
+
// Runs after settings were validated and saved for the tenant.
|
|
21
|
+
const { greeting } = ctx.settings.get<{ greeting?: string }>();
|
|
22
|
+
ctx.logger.info("__PLUGIN_KEY__.configured", { greetingLength: greeting?.length ?? 0 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function onUninstall(ctx: PluginContext): Promise<void> {
|
|
26
|
+
// Datasets are archived automatically on uninstall (and restored if the
|
|
27
|
+
// plugin is reinstalled) — only clean up EXTERNAL state here.
|
|
28
|
+
ctx.logger.info("__PLUGIN_KEY__.uninstalled");
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PluginContext } from "@clovnet/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cron jobs run in the worker process on the declared schedule. Dry-run any
|
|
5
|
+
* time with `cwe-plugin jobs run heartbeat` — output streams into `pnpm dev`.
|
|
6
|
+
*/
|
|
7
|
+
export async function heartbeat(ctx: PluginContext): Promise<void> {
|
|
8
|
+
ctx.logger.info("__PLUGIN_KEY__.heartbeat", { at: new Date().toISOString() });
|
|
9
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { PluginContext, PluginRequest } from "@clovnet/plugin-sdk";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Route handlers. Surfaces:
|
|
6
|
+
* public → /api/ext/__PLUGIN_KEY__/… (no auth)
|
|
7
|
+
* player → /api/ext/__PLUGIN_KEY__/… (player session required)
|
|
8
|
+
* admin → backoffice-only (staff RBAC)
|
|
9
|
+
* callback → server-to-server webhooks (verifySignature)
|
|
10
|
+
* Try them instantly: `cwe-plugin invoke GET /info` ·
|
|
11
|
+
* `cwe-plugin invoke GET /me --surface player --player p1`.
|
|
12
|
+
*/
|
|
13
|
+
export const infoOutputSchema = z.object({
|
|
14
|
+
plugin: z.string(),
|
|
15
|
+
greeting: z.string(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export async function info(_req: PluginRequest, ctx: PluginContext) {
|
|
19
|
+
const settings = ctx.settings.get<{ greeting?: string }>();
|
|
20
|
+
return {
|
|
21
|
+
body: {
|
|
22
|
+
plugin: "__PLUGIN_KEY__",
|
|
23
|
+
greeting: settings.greeting ?? "Hello from __PLUGIN_NAME__",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const meOutputSchema = z.object({
|
|
29
|
+
playerId: z.string(),
|
|
30
|
+
pinged: z.literal(true),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** A player action (declared in manifest.actions → shows up in the SDK
|
|
34
|
+
* extension catalog and the generated typed client). */
|
|
35
|
+
export async function getMe(req: PluginRequest, ctx: PluginContext) {
|
|
36
|
+
const playerId = req.player?.id;
|
|
37
|
+
if (!playerId) return { status: 401, body: { error: { code: "UNAUTHORIZED" } } };
|
|
38
|
+
await ctx.events.emit({
|
|
39
|
+
name: "plugin.__PLUGIN_KEY__.pinged",
|
|
40
|
+
playerId,
|
|
41
|
+
payload: { at: new Date().toISOString() },
|
|
42
|
+
});
|
|
43
|
+
return { body: { playerId, pinged: true as const } };
|
|
44
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { definePlugin } from "@clovnet/plugin-sdk";
|
|
2
|
+
import { settings } from "./settings.js";
|
|
3
|
+
import { onConfigure, onInstall, onUninstall } from "./handlers/hooks.js";
|
|
4
|
+
import { getMe, info, infoOutputSchema, meOutputSchema } from "./handlers/routes.js";
|
|
5
|
+
import { heartbeat } from "./handlers/jobs.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* __PLUGIN_NAME__ — scaffolded from the `blank` template.
|
|
9
|
+
*
|
|
10
|
+
* The manifest is the plugin's entire contract with the platform: routes,
|
|
11
|
+
* jobs, datasets, permissions, settings. Everything NOT declared here is
|
|
12
|
+
* denied at runtime (and `cwe-plugin doctor` catches drift before the host
|
|
13
|
+
* does). Changing the manifest in `pnpm dev` triggers a reinstall prompt —
|
|
14
|
+
* handler-only edits hot-reload in under 2s.
|
|
15
|
+
*/
|
|
16
|
+
export default definePlugin({
|
|
17
|
+
manifest: {
|
|
18
|
+
key: "__PLUGIN_KEY__",
|
|
19
|
+
name: "__PLUGIN_NAME__",
|
|
20
|
+
author: "you",
|
|
21
|
+
version: "0.1.0",
|
|
22
|
+
kind: "__PLUGIN_KIND__",
|
|
23
|
+
runtimeCompat: ">=0.1.0 <0.2.0",
|
|
24
|
+
description: "__PLUGIN_NAME__ — scaffolded with cwe-plugin create.",
|
|
25
|
+
permissions: {
|
|
26
|
+
// Explicit allowlists — commands/events not listed here are denied.
|
|
27
|
+
commands: [],
|
|
28
|
+
events: {
|
|
29
|
+
subscribe: [],
|
|
30
|
+
emit: ["plugin.__PLUGIN_KEY__.pinged"],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
settings,
|
|
34
|
+
routes: [
|
|
35
|
+
{
|
|
36
|
+
method: "GET",
|
|
37
|
+
path: "/info",
|
|
38
|
+
surface: "public",
|
|
39
|
+
handler: "info",
|
|
40
|
+
output: infoOutputSchema,
|
|
41
|
+
rateLimit: { windowSec: 60, max: 120 },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
method: "GET",
|
|
45
|
+
path: "/me",
|
|
46
|
+
surface: "player",
|
|
47
|
+
handler: "getMe",
|
|
48
|
+
output: meOutputSchema,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
actions: [
|
|
52
|
+
{
|
|
53
|
+
key: "getMe",
|
|
54
|
+
route: "GET /me",
|
|
55
|
+
title: "Ping as the current player",
|
|
56
|
+
kind: "query",
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
jobs: {
|
|
60
|
+
heartbeat: { schedule: "0 6 * * *", handler: "heartbeat", timeoutSec: 60 },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
hooks: { onInstall, onConfigure, onUninstall },
|
|
64
|
+
handlers: {
|
|
65
|
+
routes: { info, getMe },
|
|
66
|
+
jobs: { heartbeat },
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { settingsField, type PluginSettingsSchema } from "@clovnet/plugin-sdk";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Typed per-tenant settings. Each field pairs backoffice UI metadata with the
|
|
6
|
+
* Zod validator the platform enforces at the boundary. Secret fields are
|
|
7
|
+
* stored encrypted, are write-only over the API and never appear in logs.
|
|
8
|
+
*/
|
|
9
|
+
export const settings: PluginSettingsSchema = {
|
|
10
|
+
fields: {
|
|
11
|
+
greeting: settingsField.string({
|
|
12
|
+
label: "Greeting",
|
|
13
|
+
description: "Returned by the public /info route.",
|
|
14
|
+
default: "Hello from __PLUGIN_NAME__",
|
|
15
|
+
zod: z.string().min(1).max(120),
|
|
16
|
+
}),
|
|
17
|
+
apiKey: settingsField.string({
|
|
18
|
+
label: "API key",
|
|
19
|
+
description: "Example write-only secret — read it with ctx.secrets.get('apiKey').",
|
|
20
|
+
secret: true,
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createTestContext } from "@clovnet/plugin-sdk/testing";
|
|
3
|
+
import type { PluginRequest } from "@clovnet/plugin-sdk";
|
|
4
|
+
import plugin from "../src/index.js";
|
|
5
|
+
import { getMe } from "../src/handlers/routes.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Unit tests on the in-memory test context — no runtime, no docker, and
|
|
9
|
+
* `vitest --inspect` breakpoints inside hooks/handlers just work. The fake
|
|
10
|
+
* enforces the SAME permission rules as production: an undeclared command,
|
|
11
|
+
* a foreign dataset or a non-allowlisted host throws here too.
|
|
12
|
+
*/
|
|
13
|
+
const asRequest = (partial: Partial<PluginRequest>): PluginRequest =>
|
|
14
|
+
({ params: {}, query: {}, headers: {}, ...partial }) as PluginRequest;
|
|
15
|
+
|
|
16
|
+
describe("__PLUGIN_KEY__", () => {
|
|
17
|
+
it("installs and configures cleanly (idempotent)", async () => {
|
|
18
|
+
const t = createTestContext(plugin, {
|
|
19
|
+
tenantId: "t-test",
|
|
20
|
+
settings: { greeting: "Hi!" },
|
|
21
|
+
secrets: { apiKey: "test-key" },
|
|
22
|
+
});
|
|
23
|
+
await t.runHook("onInstall");
|
|
24
|
+
await t.runHook("onInstall"); // hooks must tolerate re-runs
|
|
25
|
+
await t.runHook("onConfigure");
|
|
26
|
+
expect(t.recorder.commands).toHaveLength(0); // this template moves no money
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("emits the ping event for the current player", async () => {
|
|
30
|
+
const t = createTestContext(plugin, { settings: { greeting: "Hi!" } });
|
|
31
|
+
const response = await getMe(asRequest({ player: { id: "p1" } }), t.ctx);
|
|
32
|
+
expect(response.body).toMatchObject({ playerId: "p1", pinged: true });
|
|
33
|
+
expect(t.recorder.events).toHaveLength(1);
|
|
34
|
+
expect(t.recorder.events[0]?.name).toBe("plugin.__PLUGIN_KEY__.pinged");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("rejects anonymous callers on the player surface", async () => {
|
|
38
|
+
const t = createTestContext(plugin, { settings: { greeting: "Hi!" } });
|
|
39
|
+
const response = await getMe(asRequest({}), t.ctx);
|
|
40
|
+
expect(response.status).toBe(401);
|
|
41
|
+
expect(t.recorder.events).toHaveLength(0);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUncheckedIndexedAccess": true,
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"types": ["node"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src", "test"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# __PLUGIN_NAME__
|
|
2
|
+
|
|
3
|
+
A CasinoWebEngine cashback plugin (the reference-plugin shape), scaffolded with `cwe-plugin create`.
|
|
4
|
+
|
|
5
|
+
## The golden path
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm create cwe-plugin my-cashback # scaffold: manifest, handlers, tests, config
|
|
9
|
+
cd my-cashback && pnpm install
|
|
10
|
+
|
|
11
|
+
pnpm dev # = cwe-plugin dev
|
|
12
|
+
# → first contact with the scaffold's preset `local` environment: the handshake verifies
|
|
13
|
+
# class=local + devHarness=true and pins its fingerprint → green banner
|
|
14
|
+
# → doctor passes → sandbox tenant resolved → plugin installed+enabled (prompted)
|
|
15
|
+
# → esbuild watch starts, bundle hot-loaded, log stream attached
|
|
16
|
+
# → edit src/handlers/routes.ts, save → rebuilt + hot-reloaded in <2s, logs streaming live
|
|
17
|
+
|
|
18
|
+
# in the dev session (keyboard): i install · e enable · c configure · x disable · u uninstall
|
|
19
|
+
# r reload · d doctor · l clear · q quit
|
|
20
|
+
# each lifecycle key prints a full hook trace: capability calls, events, audit refs, state
|
|
21
|
+
|
|
22
|
+
pnpm test # unit tests on hooks/handlers via the SDK test kit
|
|
23
|
+
|
|
24
|
+
cwe-plugin lifecycle cycle # conformance sweep: install→enable→configure→disable→
|
|
25
|
+
# uninstall→reinstall, asserting idempotency + clean teardown
|
|
26
|
+
|
|
27
|
+
cwe-plugin login # only needed for shared dev clusters / publishing
|
|
28
|
+
cwe-plugin env add dev-eu https://dev-eu.cwe.dev && cwe-plugin env use dev-eu
|
|
29
|
+
pnpm dev # same loop against the shared cluster, per-dev sandbox
|
|
30
|
+
|
|
31
|
+
pnpm publish:dev # = cwe-plugin publish — always the dev channel:
|
|
32
|
+
# doctor + codegen-diff gate + publish
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## What's in this template
|
|
36
|
+
|
|
37
|
+
| File | Purpose |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `cwe-plugin.json` | project config: plugin key, entry, environments (committed — no secrets, ever) |
|
|
40
|
+
| `src/index.ts` | `definePlugin({ manifest, hooks, handlers })` — the whole platform contract |
|
|
41
|
+
| `src/settings.ts` | Zod-typed tenant settings (one secret field included) |
|
|
42
|
+
| `src/handlers/hooks.ts` | `onInstall` / `onConfigure` / `onUninstall` with commented examples |
|
|
43
|
+
| `src/handlers/routes.ts` | player actions `getSummary`/`listPeriods`/`claim` + admin `accrue` |
|
|
44
|
+
| `src/handlers/jobs.ts` | nightly accrual sweep (dry-run with `cwe-plugin jobs run nightly-accrual`) |
|
|
45
|
+
| `settings.dev.json` | sandbox settings applied by `cwe-plugin lifecycle configure` / the `c` key |
|
|
46
|
+
| `test/hooks.test.ts` | in-memory test-context examples — breakpoints work under `vitest --inspect` |
|
|
47
|
+
|
|
48
|
+
## Useful commands
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cwe-plugin env status # am I pointed at a dev environment? auth? sandbox state?
|
|
52
|
+
cwe-plugin invoke POST /accrue --surface admin --body '{"playerId":"p1","amount":5}'
|
|
53
|
+
cwe-plugin invoke GET /summary --surface player --player p1
|
|
54
|
+
cwe-plugin settings set greeting=hi # validated locally against the schema before it is sent
|
|
55
|
+
cwe-plugin logs --level warn # standalone log tail for a second terminal
|
|
56
|
+
cwe-plugin doctor --remote # full server-side validation
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Machine-local state (pinned environment fingerprints, log cursors) lives in `.cwe/` — gitignored
|
|
60
|
+
and always safe to delete. Credentials never touch the repo: `cwe-plugin login` stores them in the
|
|
61
|
+
OS keychain.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cwe-plugin-__PLUGIN_KEY__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "__PLUGIN_NAME__ \u2014 a CasinoWebEngine plugin",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "cwe-plugin dev",
|
|
12
|
+
"doctor": "cwe-plugin doctor",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"build": "cwe-plugin build",
|
|
15
|
+
"codegen": "cwe-plugin codegen client",
|
|
16
|
+
"publish:dev": "cwe-plugin publish"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@clovnet/plugin-sdk": "^0.1.4"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"zod": "^3.23.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@clovnet/plugin-cli": "^0.2.0",
|
|
26
|
+
"@types/node": "^20.16.5",
|
|
27
|
+
"typescript": "^5.6.2",
|
|
28
|
+
"vitest": "^2.1.1",
|
|
29
|
+
"zod": "^3.23.8"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PluginContext } from "@clovnet/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lifecycle hooks — all idempotent (`cwe-plugin lifecycle cycle` asserts a
|
|
5
|
+
* re-run of every action is a no-op). Datasets archive automatically on
|
|
6
|
+
* uninstall, so there is nothing to clean up here.
|
|
7
|
+
*/
|
|
8
|
+
export async function onInstall(ctx: PluginContext): Promise<void> {
|
|
9
|
+
ctx.logger.info("__PLUGIN_KEY__.installed");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function onConfigure(ctx: PluginContext): Promise<void> {
|
|
13
|
+
const { cashbackPercent } = ctx.settings.get<{ cashbackPercent?: number }>();
|
|
14
|
+
ctx.logger.info("__PLUGIN_KEY__.configured", { cashbackPercent: cashbackPercent ?? 5 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function onUninstall(ctx: PluginContext): Promise<void> {
|
|
18
|
+
ctx.logger.info("__PLUGIN_KEY__.uninstalled");
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PluginContext } from "@clovnet/plugin-sdk";
|
|
2
|
+
import { type AccrualPeriod } from "./routes.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Nightly sweep: periods still `accruing` at the cycle boundary become
|
|
6
|
+
* `claimable`. Idempotent by construction (flipping twice is a no-op) —
|
|
7
|
+
* dry-run any time with `cwe-plugin jobs run nightly-accrual`.
|
|
8
|
+
*/
|
|
9
|
+
export async function nightlyAccrual(ctx: PluginContext): Promise<void> {
|
|
10
|
+
const periods = ctx.datasets.collection<AccrualPeriod>("accrual_periods");
|
|
11
|
+
let cursor: string | undefined;
|
|
12
|
+
let flipped = 0;
|
|
13
|
+
for (;;) {
|
|
14
|
+
const page = await periods.query({ where: { status: "accruing" }, cursor, limit: 200 });
|
|
15
|
+
for (const record of page.records) {
|
|
16
|
+
await periods.put(record.key, { ...record.value, status: "claimable" });
|
|
17
|
+
flipped += 1;
|
|
18
|
+
}
|
|
19
|
+
if (!page.nextCursor) break;
|
|
20
|
+
cursor = page.nextCursor;
|
|
21
|
+
}
|
|
22
|
+
ctx.logger.info("__PLUGIN_KEY__.nightly_accrual", { flipped });
|
|
23
|
+
}
|