@bpmnkit/cli 0.0.14 → 0.0.16

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 CHANGED
@@ -106,6 +106,9 @@ 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 |
109
112
 
110
113
  ## License
111
114
 
@@ -4,6 +4,7 @@ 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 { pluginGroup } from "./plugin.js";
7
8
  import { profileGroup } from "./profile.js";
8
9
  import { computeRelations } from "./relations.js";
9
10
  import { settingsGroup } from "./settings.js";
@@ -35,6 +36,7 @@ const customisedGroups = generatedCommandGroups.map((g) => {
35
36
  });
36
37
  const sortedOtherGroups = [
37
38
  connectorGroup,
39
+ pluginGroup,
38
40
  ...customisedGroups,
39
41
  ...adminCommandGroups,
40
42
  completionGroup,
@@ -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
+ 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
@@ -406,21 +406,6 @@ export const decisionInstanceGroup = {
406
406
  argName: "decisionEvaluationInstanceKey",
407
407
  get: (client, key) => client.decisionInstance.getDecisionInstance(key),
408
408
  }),
409
- {
410
- name: "delete",
411
- description: "Delete decision instance",
412
- args: [{ name: "decisionInstanceKey", description: "decisionInstanceKey", required: true }],
413
- flags: [DATA_OPT_FLAG],
414
- async run(ctx) {
415
- const decisionInstanceKey = ctx.positional[0];
416
- if (!decisionInstanceKey)
417
- throw new Error("Missing required argument: <decisionInstanceKey>");
418
- const body = parseJson(ctx.flags.data, "data");
419
- const client = await ctx.getClient();
420
- await client.decisionInstance.deleteDecisionInstance(decisionInstanceKey, body);
421
- ctx.output.ok("delete completed.");
422
- },
423
- },
424
409
  makeCreateCmd({
425
410
  name: "delete-batch-operation",
426
411
  description: "Delete decision instances (batch)",
@@ -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
package/dist/run.js CHANGED
@@ -4,6 +4,7 @@ import { commandGroups } from "./commands/index.js";
4
4
  import { getRuntimeCompletions } from "./completion.js";
5
5
  import { printCommandHelp, printGlobalHelp, printGroupHelp, printVersion } from "./help.js";
6
6
  import { createNullWriter, createOutputWriter, printRawResponse } from "./output.js";
7
+ import { loadPlugins } from "./plugin-loader.js";
7
8
  import { runProfileManager } from "./profile-tui.js";
8
9
  import { runSettingsManager } from "./settings-tui.js";
9
10
  import { runAskTui, runGroupTui, runMainTui } from "./tui.js";
@@ -47,6 +48,10 @@ function printError(msg, colors) {
47
48
  }
48
49
  // ─── Main ─────────────────────────────────────────────────────────────────────
49
50
  export async function run(argv) {
51
+ // Load plugin-contributed groups and merge with built-in groups.
52
+ // Failures are isolated inside loadPlugins — a broken plugin cannot crash the CLI.
53
+ const pluginGroups = await loadPlugins();
54
+ const allGroups = [...commandGroups, ...pluginGroups];
50
55
  // ── Completion protocol ───────────────────────────────────────────────────
51
56
  // casen --complete <cursorWordIndex> -- <words...>
52
57
  const completeIdx = argv.indexOf("--complete");
@@ -54,7 +59,7 @@ export async function run(argv) {
54
59
  const cursorIdx = Number(argv[completeIdx + 1] ?? "0");
55
60
  const dashDash = argv.indexOf("--", completeIdx + 2);
56
61
  const words = dashDash >= 0 ? argv.slice(dashDash + 1) : [];
57
- const suggestions = getRuntimeCompletions(commandGroups, cursorIdx, words);
62
+ const suggestions = getRuntimeCompletions(allGroups, cursorIdx, words);
58
63
  process.stdout.write(`${suggestions.join("\n")}\n`);
59
64
  return;
60
65
  }
@@ -73,11 +78,11 @@ export async function run(argv) {
73
78
  // ── Top-level: main menu TUI or help ─────────────────────────────────────
74
79
  if (positional.length === 0) {
75
80
  if (wantHelp) {
76
- printGlobalHelp(commandGroups, colors);
81
+ printGlobalHelp(allGroups, colors);
77
82
  }
78
83
  else {
79
84
  const { name: pName, info: pInfo } = buildProfileInfo(profileName);
80
- await runMainTui(commandGroups, () => Promise.resolve(createClientFromProfile(profileName)), () => Promise.resolve(createAdminClientFromProfile(profileName)), { profile: pName, profileInfo: pInfo });
85
+ await runMainTui(allGroups, () => Promise.resolve(createClientFromProfile(profileName)), () => Promise.resolve(createAdminClientFromProfile(profileName)), { profile: pName, profileInfo: pInfo });
81
86
  }
82
87
  return;
83
88
  }
@@ -85,7 +90,7 @@ export async function run(argv) {
85
90
  const getAdminClient = () => Promise.resolve(createAdminClientFromProfile(profileName));
86
91
  // ── Find group ────────────────────────────────────────────────────────────
87
92
  const groupToken = positional[0] ?? "";
88
- const group = commandGroups.find((g) => g.name === groupToken || g.aliases?.includes(groupToken));
93
+ const group = allGroups.find((g) => g.name === groupToken || g.aliases?.includes(groupToken));
89
94
  if (!group) {
90
95
  printError(`Unknown resource: "${groupToken}". Run \`casen --help\` to see all resources.`, colors);
91
96
  process.exitCode = 1;
@@ -111,7 +116,7 @@ export async function run(argv) {
111
116
  if (positional.length === 1 && !wantHelp) {
112
117
  if (group.name === "ask") {
113
118
  const { name: pName, info: pInfo } = buildProfileInfo(profileName);
114
- await runAskTui(commandGroups, getClient, getAdminClient, {
119
+ await runAskTui(allGroups, getClient, getAdminClient, {
115
120
  profile: pName,
116
121
  profileInfo: pInfo,
117
122
  });
@@ -124,7 +129,7 @@ export async function run(argv) {
124
129
  }
125
130
  else if (group.name !== "completion") {
126
131
  const { name: pName, info: pInfo } = buildProfileInfo(profileName);
127
- await runGroupTui(group, commandGroups, getClient, getAdminClient, {
132
+ await runGroupTui(group, allGroups, getClient, getAdminClient, {
128
133
  profile: pName,
129
134
  profileInfo: pInfo,
130
135
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/cli",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "Command-line interface for Camunda 8 — deploy, manage, and monitor processes from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,10 +15,10 @@
15
15
  "node": ">=20"
16
16
  },
17
17
  "dependencies": {
18
- "@bpmnkit/api": "0.0.12",
19
- "@bpmnkit/ascii": "0.0.13",
20
- "@bpmnkit/connector-gen": "0.0.6",
21
- "@bpmnkit/profiles": "0.0.9"
18
+ "@bpmnkit/api": "0.0.13",
19
+ "@bpmnkit/ascii": "0.0.14",
20
+ "@bpmnkit/connector-gen": "0.0.8",
21
+ "@bpmnkit/profiles": "0.0.10"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"