@bpmnkit/cli 0.0.15 → 0.0.17
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 +5 -0
- package/dist/commands/index.js +22 -4
- package/dist/commands/lint.js +65 -0
- package/dist/commands/plugin.js +244 -0
- package/dist/commands/story.js +42 -0
- package/dist/commands/test.js +77 -0
- package/dist/commands/worker.js +4 -2
- package/dist/plugin-loader.js +89 -0
- package/dist/run.js +33 -7
- package/dist/tui.js +593 -23
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -106,6 +106,11 @@ casen instances list --state active
|
|
|
106
106
|
| [`@bpmnkit/operate`](https://www.npmjs.com/package/@bpmnkit/operate) | Monitoring & operations frontend for Camunda clusters |
|
|
107
107
|
| [`@bpmnkit/connector-gen`](https://www.npmjs.com/package/@bpmnkit/connector-gen) | Generate connector templates from OpenAPI specs |
|
|
108
108
|
| [`@bpmnkit/proxy`](https://www.npmjs.com/package/@bpmnkit/proxy) | Local AI bridge and Camunda API proxy server |
|
|
109
|
+
| [`@bpmnkit/cli-sdk`](https://www.npmjs.com/package/@bpmnkit/cli-sdk) | Plugin authoring SDK for the casen CLI |
|
|
110
|
+
| [`@bpmnkit/create-casen-plugin`](https://www.npmjs.com/package/@bpmnkit/create-casen-plugin) | Scaffold a new casen CLI plugin in seconds |
|
|
111
|
+
| [`@bpmnkit/casen-report`](https://www.npmjs.com/package/@bpmnkit/casen-report) | HTML reports from Camunda 8 incident and SLA data |
|
|
112
|
+
| [`@bpmnkit/casen-worker-http`](https://www.npmjs.com/package/@bpmnkit/casen-worker-http) | Example HTTP worker plugin — completes jobs with live JSONPlaceholder API data |
|
|
113
|
+
| [`@bpmnkit/casen-worker-ai`](https://www.npmjs.com/package/@bpmnkit/casen-worker-ai) | AI task worker — classify, summarize, extract, and decide using Claude |
|
|
109
114
|
|
|
110
115
|
## License
|
|
111
116
|
|
package/dist/commands/index.js
CHANGED
|
@@ -4,9 +4,13 @@ import { askGroup } from "./ask.js";
|
|
|
4
4
|
import { getDmnReqsXmlCmd, getDmnXmlCmd, getStartFormCmd, getUserTaskFormCmd, getXmlCmd, renderBpmnCmd, } from "./bpmn.js";
|
|
5
5
|
import { completionGroup } from "./completion.js";
|
|
6
6
|
import { connectorGroup } from "./connector.js";
|
|
7
|
+
import { lintGroup } from "./lint.js";
|
|
8
|
+
import { pluginGroup } from "./plugin.js";
|
|
7
9
|
import { profileGroup } from "./profile.js";
|
|
8
10
|
import { computeRelations } from "./relations.js";
|
|
9
11
|
import { settingsGroup } from "./settings.js";
|
|
12
|
+
import { storyGroup } from "./story.js";
|
|
13
|
+
import { testGroup } from "./test.js";
|
|
10
14
|
import { workerCmd } from "./worker.js";
|
|
11
15
|
// Inject custom commands into generated groups without modifying generated files.
|
|
12
16
|
// Also remove the broken generated get-x-m-l commands (return text/xml, not JSON)
|
|
@@ -29,7 +33,7 @@ const customisedGroups = generatedCommandGroups.map((g) => {
|
|
|
29
33
|
return { ...g, commands: [...commands, getUserTaskFormCmd] };
|
|
30
34
|
}
|
|
31
35
|
if (g === jobGroup) {
|
|
32
|
-
return
|
|
36
|
+
return g;
|
|
33
37
|
}
|
|
34
38
|
return g;
|
|
35
39
|
});
|
|
@@ -39,12 +43,26 @@ const sortedOtherGroups = [
|
|
|
39
43
|
...adminCommandGroups,
|
|
40
44
|
completionGroup,
|
|
41
45
|
].sort((a, b) => a.name.localeCompare(b.name));
|
|
42
|
-
|
|
46
|
+
const workerGroup = {
|
|
47
|
+
name: "worker",
|
|
48
|
+
description: workerCmd.description,
|
|
49
|
+
commands: [workerCmd],
|
|
50
|
+
};
|
|
51
|
+
/** Pinned groups shown above the separator in the main TUI menu. */
|
|
52
|
+
export const pinnedGroups = [
|
|
43
53
|
askGroup,
|
|
54
|
+
lintGroup,
|
|
55
|
+
storyGroup,
|
|
44
56
|
settingsGroup,
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
testGroup,
|
|
58
|
+
workerGroup,
|
|
47
59
|
];
|
|
60
|
+
/** API command groups — shown below the plugin section in the main TUI menu. */
|
|
61
|
+
export const apiGroups = sortedOtherGroups;
|
|
62
|
+
/** All built-in groups — used for CLI routing. */
|
|
63
|
+
export const commandGroups = [...pinnedGroups, ...apiGroups];
|
|
64
|
+
// Exported for CLI routing in run.ts (not shown in main TUI menu)
|
|
65
|
+
export { pluginGroup, profileGroup };
|
|
48
66
|
// Compute follow-up relations between commands based on shared field/arg names
|
|
49
67
|
computeRelations(commandGroups);
|
|
50
68
|
// Manually inject relations on GET commands (they return a single object, not a
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { Bpmn, optimize } from "@bpmnkit/core";
|
|
3
|
+
const SEVERITY_SYMBOL = {
|
|
4
|
+
error: "✖",
|
|
5
|
+
warning: "⚠",
|
|
6
|
+
info: "ℹ",
|
|
7
|
+
};
|
|
8
|
+
const lintCmd = {
|
|
9
|
+
name: "lint",
|
|
10
|
+
description: "Lint a BPMN file — run all static analysis and pattern checks",
|
|
11
|
+
args: [{ name: "file", description: "Path to the .bpmn file", required: true }],
|
|
12
|
+
flags: [
|
|
13
|
+
{
|
|
14
|
+
name: "categories",
|
|
15
|
+
description: "Comma-separated categories to run (default: all)",
|
|
16
|
+
type: "string",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "format",
|
|
20
|
+
description: "Output format: text (default) or json",
|
|
21
|
+
type: "string",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
async run(ctx) {
|
|
25
|
+
const filePath = ctx.positional[0];
|
|
26
|
+
if (!filePath)
|
|
27
|
+
throw new Error("Missing required argument: <file>");
|
|
28
|
+
const xml = await readFile(filePath, "utf-8");
|
|
29
|
+
const defs = Bpmn.parse(xml);
|
|
30
|
+
const categoriesFlag = ctx.flags.categories;
|
|
31
|
+
const categories = typeof categoriesFlag === "string" && categoriesFlag.length > 0
|
|
32
|
+
? categoriesFlag.split(",").map((s) => s.trim())
|
|
33
|
+
: undefined;
|
|
34
|
+
const report = optimize(defs, categories !== undefined ? { categories } : undefined);
|
|
35
|
+
const { findings } = report;
|
|
36
|
+
const formatFlag = ctx.flags.format;
|
|
37
|
+
if (formatFlag === "json") {
|
|
38
|
+
ctx.output.print(findings);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (findings.length === 0) {
|
|
42
|
+
ctx.output.ok("No issues found.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for (const f of findings) {
|
|
46
|
+
const symbol = SEVERITY_SYMBOL[f.severity] ?? "·";
|
|
47
|
+
const elIds = f.elementIds.length > 0 ? ` [${f.elementIds.join(", ")}]` : "";
|
|
48
|
+
ctx.output.info(`${symbol} [${f.category}]${elIds} ${f.message}`);
|
|
49
|
+
}
|
|
50
|
+
const { total, bySeverity } = report.summary;
|
|
51
|
+
const errorCount = bySeverity.error ?? 0;
|
|
52
|
+
const warnCount = bySeverity.warning ?? 0;
|
|
53
|
+
const infoCount = bySeverity.info ?? 0;
|
|
54
|
+
ctx.output.info(`\n${total} finding${total !== 1 ? "s" : ""}: ${errorCount} error${errorCount !== 1 ? "s" : ""}, ${warnCount} warning${warnCount !== 1 ? "s" : ""}, ${infoCount} info`);
|
|
55
|
+
if (errorCount > 0) {
|
|
56
|
+
throw new Error(`Lint failed with ${errorCount} error${errorCount !== 1 ? "s" : ""}`);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
export const lintGroup = {
|
|
61
|
+
name: "lint",
|
|
62
|
+
description: "Lint BPMN files using the static analyzer",
|
|
63
|
+
commands: [lintCmd],
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=lint.js.map
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { PLUGINS_DIR, readInstalledPlugins, sanitiseName, } from "../plugin-loader.js";
|
|
6
|
+
// ── npm helper ────────────────────────────────────────────────────────────────
|
|
7
|
+
function runNpm(args, cwd) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
10
|
+
const child = spawn(npmCmd, ["install", ...args], { cwd, stdio: "inherit" });
|
|
11
|
+
child.on("close", (code) => {
|
|
12
|
+
if (code === 0)
|
|
13
|
+
resolve();
|
|
14
|
+
else
|
|
15
|
+
reject(new Error(`npm install exited with code ${String(code)}`));
|
|
16
|
+
});
|
|
17
|
+
child.on("error", reject);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export async function searchNpmRegistry(query) {
|
|
21
|
+
const text = `keywords:casen-plugin${query ? ` ${query}` : ""}`;
|
|
22
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(text)}&size=50`;
|
|
23
|
+
const res = await fetch(url);
|
|
24
|
+
if (!res.ok)
|
|
25
|
+
throw new Error(`npm registry returned HTTP ${res.status}`);
|
|
26
|
+
const data = (await res.json());
|
|
27
|
+
return data.objects;
|
|
28
|
+
}
|
|
29
|
+
// ── plugin group ──────────────────────────────────────────────────────────────
|
|
30
|
+
export const pluginGroup = {
|
|
31
|
+
name: "plugin",
|
|
32
|
+
description: "Manage casen CLI plugins",
|
|
33
|
+
commands: [
|
|
34
|
+
// ── list ──────────────────────────────────────────────────────────────
|
|
35
|
+
{
|
|
36
|
+
name: "list",
|
|
37
|
+
aliases: ["ls"],
|
|
38
|
+
description: "List installed plugins",
|
|
39
|
+
async run(ctx) {
|
|
40
|
+
const plugins = await readInstalledPlugins();
|
|
41
|
+
if (plugins.length === 0) {
|
|
42
|
+
ctx.output.info('No plugins installed. Run "casen plugin search" to discover plugins.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
ctx.output.printList({
|
|
46
|
+
items: plugins.map((p) => ({
|
|
47
|
+
name: p.package,
|
|
48
|
+
version: p.version,
|
|
49
|
+
installed: p.installedAt.slice(0, 10),
|
|
50
|
+
})),
|
|
51
|
+
}, [
|
|
52
|
+
{ key: "name", header: "NAME" },
|
|
53
|
+
{ key: "version", header: "VERSION" },
|
|
54
|
+
{ key: "installed", header: "INSTALLED" },
|
|
55
|
+
]);
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
// ── install ───────────────────────────────────────────────────────────
|
|
59
|
+
{
|
|
60
|
+
name: "install",
|
|
61
|
+
aliases: ["add"],
|
|
62
|
+
description: "Install a plugin from the npm registry or a local path",
|
|
63
|
+
args: [{ name: "name", description: "npm package name or ./local-path", required: true }],
|
|
64
|
+
examples: [
|
|
65
|
+
{ description: "Install from npm", command: "casen plugin install casen-deploy" },
|
|
66
|
+
{
|
|
67
|
+
description: "Install from local path (dev mode)",
|
|
68
|
+
command: "casen plugin install ./my-plugin",
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
async run(ctx) {
|
|
72
|
+
const nameArg = ctx.positional[0];
|
|
73
|
+
if (!nameArg)
|
|
74
|
+
throw new Error("Missing required argument: <name>");
|
|
75
|
+
// Detect local path vs npm package name
|
|
76
|
+
const isLocalPath = nameArg.startsWith(".") || nameArg.startsWith("/");
|
|
77
|
+
let pkgName;
|
|
78
|
+
if (isLocalPath) {
|
|
79
|
+
const localPath = join(homedir(), nameArg.startsWith("/") ? "" : ".", nameArg);
|
|
80
|
+
const localPkgText = await readFile(join(isLocalPath ? nameArg : localPath, "package.json"), "utf8");
|
|
81
|
+
const localPkg = JSON.parse(localPkgText);
|
|
82
|
+
if (!localPkg.name)
|
|
83
|
+
throw new Error(`No "name" field found in ${nameArg}/package.json`);
|
|
84
|
+
pkgName = localPkg.name;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
pkgName = nameArg;
|
|
88
|
+
}
|
|
89
|
+
const dirName = sanitiseName(pkgName);
|
|
90
|
+
const pluginDir = join(PLUGINS_DIR, dirName);
|
|
91
|
+
ctx.output.info(`Installing ${pkgName}…`);
|
|
92
|
+
await mkdir(pluginDir, { recursive: true });
|
|
93
|
+
// Initialise a minimal host package.json so npm has a valid workspace
|
|
94
|
+
const hostPkg = { name: "_casen-plugin-host", version: "0.0.0", private: true };
|
|
95
|
+
const hostPkgPath = join(pluginDir, "package.json");
|
|
96
|
+
try {
|
|
97
|
+
await readFile(hostPkgPath, "utf8");
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Only write if it doesn't already exist
|
|
101
|
+
await writeFile(hostPkgPath, `${JSON.stringify(hostPkg, null, 2)}\n`, "utf8");
|
|
102
|
+
}
|
|
103
|
+
await runNpm([nameArg], pluginDir);
|
|
104
|
+
// Read the installed version from the plugin's own package.json
|
|
105
|
+
const installedPkgText = await readFile(join(pluginDir, "node_modules", pkgName, "package.json"), "utf8");
|
|
106
|
+
const installedPkg = JSON.parse(installedPkgText);
|
|
107
|
+
const version = installedPkg.version ?? "unknown";
|
|
108
|
+
const meta = {
|
|
109
|
+
package: pkgName,
|
|
110
|
+
version,
|
|
111
|
+
installedAt: new Date().toISOString(),
|
|
112
|
+
};
|
|
113
|
+
await writeFile(join(pluginDir, ".meta.json"), `${JSON.stringify(meta, null, 2)}\n`, "utf8");
|
|
114
|
+
ctx.output.ok(`Installed ${pkgName}@${version}`);
|
|
115
|
+
ctx.output.info("Restart casen to activate the plugin.");
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
// ── remove ────────────────────────────────────────────────────────────
|
|
119
|
+
{
|
|
120
|
+
name: "remove",
|
|
121
|
+
aliases: ["uninstall", "rm"],
|
|
122
|
+
description: "Uninstall a plugin",
|
|
123
|
+
args: [{ name: "name", description: "Plugin package name", required: true }],
|
|
124
|
+
async run(ctx) {
|
|
125
|
+
const pkgName = ctx.positional[0];
|
|
126
|
+
if (!pkgName)
|
|
127
|
+
throw new Error("Missing required argument: <name>");
|
|
128
|
+
const pluginDir = join(PLUGINS_DIR, sanitiseName(pkgName));
|
|
129
|
+
try {
|
|
130
|
+
await readFile(join(pluginDir, ".meta.json"), "utf8");
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
throw new Error(`Plugin "${pkgName}" is not installed.`);
|
|
134
|
+
}
|
|
135
|
+
await rm(pluginDir, { recursive: true, force: true });
|
|
136
|
+
ctx.output.ok(`Removed ${pkgName}`);
|
|
137
|
+
ctx.output.info("Restart casen for the change to take effect.");
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
// ── update ────────────────────────────────────────────────────────────
|
|
141
|
+
{
|
|
142
|
+
name: "update",
|
|
143
|
+
aliases: ["upgrade"],
|
|
144
|
+
description: "Update one or all plugins to their latest versions",
|
|
145
|
+
args: [
|
|
146
|
+
{
|
|
147
|
+
name: "name",
|
|
148
|
+
description: "Plugin to update (omit to update all installed plugins)",
|
|
149
|
+
required: false,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
async run(ctx) {
|
|
153
|
+
const target = ctx.positional[0];
|
|
154
|
+
const plugins = await readInstalledPlugins();
|
|
155
|
+
if (plugins.length === 0) {
|
|
156
|
+
ctx.output.info("No plugins installed.");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const toUpdate = target ? plugins.filter((p) => p.package === target) : plugins;
|
|
160
|
+
if (toUpdate.length === 0) {
|
|
161
|
+
throw new Error(`Plugin "${String(target)}" is not installed.`);
|
|
162
|
+
}
|
|
163
|
+
for (const plugin of toUpdate) {
|
|
164
|
+
ctx.output.info(`Updating ${plugin.package}…`);
|
|
165
|
+
await runNpm([`${plugin.package}@latest`], plugin.dir);
|
|
166
|
+
const pkgText = await readFile(join(plugin.dir, "node_modules", plugin.package, "package.json"), "utf8");
|
|
167
|
+
const pkg = JSON.parse(pkgText);
|
|
168
|
+
const newVersion = pkg.version ?? "unknown";
|
|
169
|
+
const meta = {
|
|
170
|
+
package: plugin.package,
|
|
171
|
+
version: newVersion,
|
|
172
|
+
installedAt: plugin.installedAt,
|
|
173
|
+
};
|
|
174
|
+
await writeFile(join(plugin.dir, ".meta.json"), `${JSON.stringify(meta, null, 2)}\n`, "utf8");
|
|
175
|
+
ctx.output.ok(`Updated ${plugin.package} → ${newVersion}`);
|
|
176
|
+
}
|
|
177
|
+
ctx.output.info("Restart casen for the changes to take effect.");
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
// ── info ──────────────────────────────────────────────────────────────
|
|
181
|
+
{
|
|
182
|
+
name: "info",
|
|
183
|
+
description: "Show details for an installed plugin",
|
|
184
|
+
args: [{ name: "name", description: "Plugin package name", required: true }],
|
|
185
|
+
async run(ctx) {
|
|
186
|
+
const pkgName = ctx.positional[0];
|
|
187
|
+
if (!pkgName)
|
|
188
|
+
throw new Error("Missing required argument: <name>");
|
|
189
|
+
const pluginDir = join(PLUGINS_DIR, sanitiseName(pkgName));
|
|
190
|
+
const metaText = await readFile(join(pluginDir, ".meta.json"), "utf8");
|
|
191
|
+
const meta = JSON.parse(metaText);
|
|
192
|
+
const pkgText = await readFile(join(pluginDir, "node_modules", pkgName, "package.json"), "utf8");
|
|
193
|
+
const pkg = JSON.parse(pkgText);
|
|
194
|
+
const repoUrl = typeof pkg.repository === "string" ? pkg.repository : (pkg.repository?.url ?? "");
|
|
195
|
+
const author = typeof pkg.author === "string" ? pkg.author : (pkg.author?.name ?? "");
|
|
196
|
+
ctx.output.printItem({
|
|
197
|
+
name: meta.package,
|
|
198
|
+
version: meta.version,
|
|
199
|
+
installedAt: meta.installedAt,
|
|
200
|
+
description: pkg.description ?? "",
|
|
201
|
+
homepage: pkg.homepage ?? "",
|
|
202
|
+
repository: repoUrl,
|
|
203
|
+
author,
|
|
204
|
+
directory: pluginDir,
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
// ── search ────────────────────────────────────────────────────────────
|
|
209
|
+
{
|
|
210
|
+
name: "search",
|
|
211
|
+
description: "Search the npm registry for casen plugins",
|
|
212
|
+
args: [{ name: "query", description: "Search terms (optional)", required: false }],
|
|
213
|
+
examples: [
|
|
214
|
+
{ description: "Browse all plugins", command: "casen plugin search" },
|
|
215
|
+
{ description: "Search for deploy-related plugins", command: "casen plugin search deploy" },
|
|
216
|
+
],
|
|
217
|
+
async run(ctx) {
|
|
218
|
+
const query = ctx.positional[0] ?? "";
|
|
219
|
+
ctx.output.info(`Searching npm for casen plugins${query ? ` matching "${query}"` : ""}…`);
|
|
220
|
+
const results = await searchNpmRegistry(query);
|
|
221
|
+
if (results.length === 0) {
|
|
222
|
+
ctx.output.info("No plugins found.");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
ctx.output.printList({
|
|
226
|
+
items: results.map((r) => ({
|
|
227
|
+
name: r.package.name,
|
|
228
|
+
version: r.package.version,
|
|
229
|
+
description: r.package.description ?? "",
|
|
230
|
+
publisher: r.package.publisher?.username ?? "",
|
|
231
|
+
score: r.score.final.toFixed(2),
|
|
232
|
+
})),
|
|
233
|
+
}, [
|
|
234
|
+
{ key: "name", header: "NAME" },
|
|
235
|
+
{ key: "version", header: "VERSION" },
|
|
236
|
+
{ key: "description", header: "DESCRIPTION", maxWidth: 52 },
|
|
237
|
+
{ key: "publisher", header: "PUBLISHER" },
|
|
238
|
+
{ key: "score", header: "SCORE" },
|
|
239
|
+
]);
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { Bpmn, renderStoryHtml } from "@bpmnkit/core";
|
|
3
|
+
const storyCmd = {
|
|
4
|
+
name: "story",
|
|
5
|
+
description: "Render a BPMN process as a standalone story-mode HTML file",
|
|
6
|
+
args: [{ name: "file", description: "Path to the .bpmn file", required: true }],
|
|
7
|
+
flags: [
|
|
8
|
+
{
|
|
9
|
+
name: "output",
|
|
10
|
+
short: "o",
|
|
11
|
+
description: "Output path (default: <file>.story.html)",
|
|
12
|
+
type: "string",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "theme",
|
|
16
|
+
description: "Color theme: light (default) or dark",
|
|
17
|
+
type: "string",
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
async run(ctx) {
|
|
21
|
+
const filePath = ctx.positional[0];
|
|
22
|
+
if (!filePath)
|
|
23
|
+
throw new Error("Missing required argument: <file>");
|
|
24
|
+
const xml = await readFile(filePath, "utf-8");
|
|
25
|
+
const defs = Bpmn.parse(xml);
|
|
26
|
+
const themeFlag = ctx.flags.theme;
|
|
27
|
+
const theme = themeFlag === "dark" ? "dark" : "light";
|
|
28
|
+
const outputFlag = ctx.flags.output;
|
|
29
|
+
const outputPath = typeof outputFlag === "string" && outputFlag.length > 0
|
|
30
|
+
? outputFlag
|
|
31
|
+
: `${filePath}.story.html`;
|
|
32
|
+
const html = renderStoryHtml(defs, { standalone: true, theme });
|
|
33
|
+
await writeFile(outputPath, html, "utf-8");
|
|
34
|
+
ctx.output.ok(`Story HTML written to ${outputPath}`);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
export const storyGroup = {
|
|
38
|
+
name: "story",
|
|
39
|
+
description: "Render BPMN processes as narrative HTML",
|
|
40
|
+
commands: [storyCmd],
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=story.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { Bpmn } from "@bpmnkit/core";
|
|
3
|
+
import { Engine, runScenario } from "@bpmnkit/engine";
|
|
4
|
+
const testCmd = {
|
|
5
|
+
name: "test",
|
|
6
|
+
description: "Run scenario tests for a BPMN process file",
|
|
7
|
+
args: [
|
|
8
|
+
{
|
|
9
|
+
name: "file",
|
|
10
|
+
description: "Path to the .bpmn file",
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
flags: [
|
|
15
|
+
{
|
|
16
|
+
name: "scenarios",
|
|
17
|
+
short: "s",
|
|
18
|
+
description: "Path to the .bpmn.tests.json scenarios file (default: <file>.tests.json)",
|
|
19
|
+
type: "string",
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
async run(ctx) {
|
|
23
|
+
const bpmnPath = ctx.positional[0];
|
|
24
|
+
if (bpmnPath === undefined)
|
|
25
|
+
throw new Error("Missing required argument: <file>");
|
|
26
|
+
const scenariosPath = typeof ctx.flags.scenarios === "string" ? ctx.flags.scenarios : `${bpmnPath}.tests.json`;
|
|
27
|
+
const bpmnXml = await readFile(bpmnPath, "utf8").catch(() => {
|
|
28
|
+
throw new Error(`Cannot read BPMN file: ${bpmnPath}`);
|
|
29
|
+
});
|
|
30
|
+
const scenariosRaw = await readFile(scenariosPath, "utf8").catch(() => {
|
|
31
|
+
throw new Error(`Cannot read scenarios file: ${scenariosPath}`);
|
|
32
|
+
});
|
|
33
|
+
let scenarios;
|
|
34
|
+
try {
|
|
35
|
+
scenarios = JSON.parse(scenariosRaw);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`Invalid JSON in scenarios file: ${scenariosPath}`);
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(scenarios) || scenarios.length === 0) {
|
|
41
|
+
ctx.output.info("No scenarios found.");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const defs = Bpmn.parse(bpmnXml);
|
|
45
|
+
const engine = new Engine();
|
|
46
|
+
let passed = 0;
|
|
47
|
+
let failed = 0;
|
|
48
|
+
for (const scenario of scenarios) {
|
|
49
|
+
const result = await runScenario(engine, defs, scenario);
|
|
50
|
+
if (result.passed) {
|
|
51
|
+
passed++;
|
|
52
|
+
ctx.output.ok(`PASS ${scenario.name} (${result.durationMs}ms)`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
failed++;
|
|
56
|
+
ctx.output.info(`FAIL ${scenario.name} (${result.durationMs}ms)`);
|
|
57
|
+
for (const f of result.failures) {
|
|
58
|
+
ctx.output.info(` ${f.field}: expected ${JSON.stringify(f.expected)}, got ${JSON.stringify(f.actual)}`);
|
|
59
|
+
}
|
|
60
|
+
for (const e of result.errors) {
|
|
61
|
+
ctx.output.info(` error${e.elementId !== undefined ? ` (${e.elementId})` : ""}: ${e.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const total = passed + failed;
|
|
66
|
+
ctx.output.info(`\n${passed}/${total} passed`);
|
|
67
|
+
if (failed > 0) {
|
|
68
|
+
throw new Error(`${failed} scenario(s) failed`);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
export const testGroup = {
|
|
73
|
+
name: "test",
|
|
74
|
+
description: "Run scenario-based tests for BPMN processes",
|
|
75
|
+
commands: [testCmd],
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=test.js.map
|
package/dist/commands/worker.js
CHANGED
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
export const workerCmd = {
|
|
12
12
|
name: "worker",
|
|
13
13
|
description: "Run a simple job worker that auto-completes jobs of a given type",
|
|
14
|
+
_worker: { jobType: "io.camunda.connector.HttpJson:1" },
|
|
14
15
|
args: [
|
|
15
16
|
{
|
|
16
17
|
name: "type",
|
|
17
18
|
description: "Job type to subscribe to (matches the task definition type in BPMN)",
|
|
18
19
|
required: true,
|
|
20
|
+
default: "io.camunda.connector.HttpJson:1",
|
|
19
21
|
},
|
|
20
22
|
],
|
|
21
23
|
flags: [
|
|
@@ -46,11 +48,11 @@ export const workerCmd = {
|
|
|
46
48
|
examples: [
|
|
47
49
|
{
|
|
48
50
|
description: "Subscribe to jobs of type 'payment-service'",
|
|
49
|
-
command: "casen
|
|
51
|
+
command: "casen worker payment-service",
|
|
50
52
|
},
|
|
51
53
|
{
|
|
52
54
|
description: "Return custom variables on completion",
|
|
53
|
-
command: 'casen
|
|
55
|
+
command: 'casen worker payment-service --variables \'{"status":"ok","amount":100}\'',
|
|
54
56
|
},
|
|
55
57
|
],
|
|
56
58
|
async run(ctx) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
// ── Paths ─────────────────────────────────────────────────────────────────────
|
|
6
|
+
export const PLUGINS_DIR = join(homedir(), ".casen", "plugins");
|
|
7
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
8
|
+
/**
|
|
9
|
+
* Converts an npm package name to a safe directory name.
|
|
10
|
+
* "@acme/casen-deploy" → "acme__casen-deploy"
|
|
11
|
+
*/
|
|
12
|
+
export function sanitiseName(pkg) {
|
|
13
|
+
return pkg.replace(/^@/, "").replace(/\//g, "__");
|
|
14
|
+
}
|
|
15
|
+
// ── Plugin loader ─────────────────────────────────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* Loads all installed plugins from `~/.casen/plugins/` and returns their
|
|
18
|
+
* combined command groups. Failures are isolated — a broken plugin logs a
|
|
19
|
+
* warning to stderr and is skipped; it cannot crash the CLI.
|
|
20
|
+
*/
|
|
21
|
+
export async function loadPlugins() {
|
|
22
|
+
let entries;
|
|
23
|
+
try {
|
|
24
|
+
entries = await readdir(PLUGINS_DIR);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const groups = [];
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (entry.startsWith("."))
|
|
32
|
+
continue;
|
|
33
|
+
const pluginDir = join(PLUGINS_DIR, entry);
|
|
34
|
+
try {
|
|
35
|
+
// Read metadata to get the actual npm package name
|
|
36
|
+
const metaText = await readFile(join(pluginDir, ".meta.json"), "utf8");
|
|
37
|
+
const meta = JSON.parse(metaText);
|
|
38
|
+
const pkgName = meta.package;
|
|
39
|
+
// Resolve entry point via the plugin's own package.json
|
|
40
|
+
const pkgText = await readFile(join(pluginDir, "node_modules", pkgName, "package.json"), "utf8");
|
|
41
|
+
const pkg = JSON.parse(pkgText);
|
|
42
|
+
const main = pkg.main ?? "dist/index.js";
|
|
43
|
+
const entryPath = join(pluginDir, "node_modules", pkgName, main);
|
|
44
|
+
// Dynamic import — use file URL for cross-platform compatibility
|
|
45
|
+
const mod = (await import(pathToFileURL(entryPath).href));
|
|
46
|
+
const plugin = (mod.default ?? mod);
|
|
47
|
+
if (!Array.isArray(plugin.groups)) {
|
|
48
|
+
process.stderr.write(`[plugin] ${pkgName}: no groups exported, skipping\n`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
groups.push(...plugin.groups);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55
|
+
process.stderr.write(`[plugin] ${entry}: failed to load — ${msg}\n`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return groups;
|
|
59
|
+
}
|
|
60
|
+
// ── Management helpers ────────────────────────────────────────────────────────
|
|
61
|
+
/**
|
|
62
|
+
* Returns metadata for all installed plugins.
|
|
63
|
+
* Directories without a `.meta.json` are silently ignored.
|
|
64
|
+
*/
|
|
65
|
+
export async function readInstalledPlugins() {
|
|
66
|
+
let entries;
|
|
67
|
+
try {
|
|
68
|
+
entries = await readdir(PLUGINS_DIR);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const result = [];
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (entry.startsWith("."))
|
|
76
|
+
continue;
|
|
77
|
+
const dir = join(PLUGINS_DIR, entry);
|
|
78
|
+
try {
|
|
79
|
+
const metaText = await readFile(join(dir, ".meta.json"), "utf8");
|
|
80
|
+
const meta = JSON.parse(metaText);
|
|
81
|
+
result.push({ ...meta, dir });
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Directory without .meta.json — not a managed plugin, skip
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=plugin-loader.js.map
|