@bpmnkit/cli 0.0.16 → 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 +2 -0
- package/dist/commands/index.js +21 -5
- package/dist/commands/lint.js +65 -0
- package/dist/commands/plugin.js +1 -1
- package/dist/commands/story.js +42 -0
- package/dist/commands/test.js +77 -0
- package/dist/commands/worker.js +4 -2
- package/dist/run.js +24 -3
- package/dist/tui.js +593 -23
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -109,6 +109,8 @@ casen instances list --state active
|
|
|
109
109
|
| [`@bpmnkit/cli-sdk`](https://www.npmjs.com/package/@bpmnkit/cli-sdk) | Plugin authoring SDK for the casen CLI |
|
|
110
110
|
| [`@bpmnkit/create-casen-plugin`](https://www.npmjs.com/package/@bpmnkit/create-casen-plugin) | Scaffold a new casen CLI plugin in seconds |
|
|
111
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 |
|
|
112
114
|
|
|
113
115
|
## License
|
|
114
116
|
|
package/dist/commands/index.js
CHANGED
|
@@ -4,10 +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";
|
|
7
8
|
import { pluginGroup } from "./plugin.js";
|
|
8
9
|
import { profileGroup } from "./profile.js";
|
|
9
10
|
import { computeRelations } from "./relations.js";
|
|
10
11
|
import { settingsGroup } from "./settings.js";
|
|
12
|
+
import { storyGroup } from "./story.js";
|
|
13
|
+
import { testGroup } from "./test.js";
|
|
11
14
|
import { workerCmd } from "./worker.js";
|
|
12
15
|
// Inject custom commands into generated groups without modifying generated files.
|
|
13
16
|
// Also remove the broken generated get-x-m-l commands (return text/xml, not JSON)
|
|
@@ -30,23 +33,36 @@ const customisedGroups = generatedCommandGroups.map((g) => {
|
|
|
30
33
|
return { ...g, commands: [...commands, getUserTaskFormCmd] };
|
|
31
34
|
}
|
|
32
35
|
if (g === jobGroup) {
|
|
33
|
-
return
|
|
36
|
+
return g;
|
|
34
37
|
}
|
|
35
38
|
return g;
|
|
36
39
|
});
|
|
37
40
|
const sortedOtherGroups = [
|
|
38
41
|
connectorGroup,
|
|
39
|
-
pluginGroup,
|
|
40
42
|
...customisedGroups,
|
|
41
43
|
...adminCommandGroups,
|
|
42
44
|
completionGroup,
|
|
43
45
|
].sort((a, b) => a.name.localeCompare(b.name));
|
|
44
|
-
|
|
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 = [
|
|
45
53
|
askGroup,
|
|
54
|
+
lintGroup,
|
|
55
|
+
storyGroup,
|
|
46
56
|
settingsGroup,
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
testGroup,
|
|
58
|
+
workerGroup,
|
|
49
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 };
|
|
50
66
|
// Compute follow-up relations between commands based on shared field/arg names
|
|
51
67
|
computeRelations(commandGroups);
|
|
52
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
|
package/dist/commands/plugin.js
CHANGED
|
@@ -17,7 +17,7 @@ function runNpm(args, cwd) {
|
|
|
17
17
|
child.on("error", reject);
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
|
-
async function searchNpmRegistry(query) {
|
|
20
|
+
export async function searchNpmRegistry(query) {
|
|
21
21
|
const text = `keywords:casen-plugin${query ? ` ${query}` : ""}`;
|
|
22
22
|
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(text)}&size=50`;
|
|
23
23
|
const res = await fetch(url);
|
|
@@ -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) {
|
package/dist/run.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendAuditEntry, createAdminClientFromProfile, createClientFromProfile, getActiveName, getProfile, } from "@bpmnkit/profiles";
|
|
2
2
|
import { parseArgs } from "./args.js";
|
|
3
|
-
import { commandGroups } from "./commands/index.js";
|
|
3
|
+
import { apiGroups, commandGroups, pinnedGroups, pluginGroup, profileGroup, } 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";
|
|
@@ -51,7 +51,7 @@ export async function run(argv) {
|
|
|
51
51
|
// Load plugin-contributed groups and merge with built-in groups.
|
|
52
52
|
// Failures are isolated inside loadPlugins — a broken plugin cannot crash the CLI.
|
|
53
53
|
const pluginGroups = await loadPlugins();
|
|
54
|
-
const allGroups = [...commandGroups, ...pluginGroups];
|
|
54
|
+
const allGroups = [...commandGroups, profileGroup, pluginGroup, ...pluginGroups];
|
|
55
55
|
// ── Completion protocol ───────────────────────────────────────────────────
|
|
56
56
|
// casen --complete <cursorWordIndex> -- <words...>
|
|
57
57
|
const completeIdx = argv.indexOf("--complete");
|
|
@@ -82,7 +82,13 @@ export async function run(argv) {
|
|
|
82
82
|
}
|
|
83
83
|
else {
|
|
84
84
|
const { name: pName, info: pInfo } = buildProfileInfo(profileName);
|
|
85
|
-
await runMainTui(
|
|
85
|
+
await runMainTui(
|
|
86
|
+
// Order: pinned → plugins (tagged) → api groups
|
|
87
|
+
[
|
|
88
|
+
...pinnedGroups,
|
|
89
|
+
...pluginGroups.map((g) => ({ ...g, _plugin: true })),
|
|
90
|
+
...apiGroups,
|
|
91
|
+
], () => Promise.resolve(createClientFromProfile(profileName)), () => Promise.resolve(createAdminClientFromProfile(profileName)), { profile: pName, profileInfo: pInfo });
|
|
86
92
|
}
|
|
87
93
|
return;
|
|
88
94
|
}
|
|
@@ -96,6 +102,21 @@ export async function run(argv) {
|
|
|
96
102
|
process.exitCode = 1;
|
|
97
103
|
return;
|
|
98
104
|
}
|
|
105
|
+
// ── worker: treat positional[1] as the job type argument ─────────────────
|
|
106
|
+
if (group.name === "worker" && positional.length >= 2 && !wantHelp) {
|
|
107
|
+
const output = createOutputWriter(outputFormat, noColor);
|
|
108
|
+
const ctx = {
|
|
109
|
+
positional: positional.slice(1),
|
|
110
|
+
flags,
|
|
111
|
+
output,
|
|
112
|
+
getClient,
|
|
113
|
+
getAdminClient,
|
|
114
|
+
};
|
|
115
|
+
const cmd = group.commands[0];
|
|
116
|
+
if (cmd)
|
|
117
|
+
await cmd.run(ctx);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
99
120
|
// ── ask: join remaining positionals as the query ─────────────────────────
|
|
100
121
|
if (group.name === "ask" && positional.length >= 2 && !wantHelp) {
|
|
101
122
|
const queryWords = positional.slice(1);
|
package/dist/tui.js
CHANGED
|
@@ -2,6 +2,9 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { renderBpmnAscii } from "@bpmnkit/ascii";
|
|
3
3
|
import { appendAuditEntry, getAuditLog, getSettings, saveSettings } from "@bpmnkit/profiles";
|
|
4
4
|
import { runAskQuery } from "./commands/ask.js";
|
|
5
|
+
import { pluginGroup, searchNpmRegistry } from "./commands/plugin.js";
|
|
6
|
+
import { profileGroup } from "./commands/profile.js";
|
|
7
|
+
import { readInstalledPlugins } from "./plugin-loader.js";
|
|
5
8
|
// ─── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
6
9
|
const CSI = "\x1b[";
|
|
7
10
|
const HIDE = `${CSI}?25l`;
|
|
@@ -83,12 +86,13 @@ const SKIP_FLAGS = new Set(["output", "profile", "help", "no-color", "debug"]);
|
|
|
83
86
|
function buildFields(cmd) {
|
|
84
87
|
const fields = [];
|
|
85
88
|
for (const arg of cmd.args ?? []) {
|
|
89
|
+
const defaultVal = arg.default ?? "";
|
|
86
90
|
fields.push({
|
|
87
91
|
kind: "arg",
|
|
88
92
|
label: arg.name,
|
|
89
93
|
hint: arg.required ? "required" : "optional",
|
|
90
|
-
value:
|
|
91
|
-
cursor:
|
|
94
|
+
value: defaultVal,
|
|
95
|
+
cursor: defaultVal.length,
|
|
92
96
|
required: arg.required ?? false,
|
|
93
97
|
argSpec: arg,
|
|
94
98
|
});
|
|
@@ -376,8 +380,12 @@ function renderMain(state, screen) {
|
|
|
376
380
|
const desc = dim(fit(g.description, cols - nameW - 8));
|
|
377
381
|
const line = ` ${name} ${desc}`;
|
|
378
382
|
lines.push(isCursor ? inv(line.padEnd(cols - 1)) : line);
|
|
379
|
-
//
|
|
380
|
-
if (!searching && g.name === "
|
|
383
|
+
// Separator after pinned section (after "worker", before plugins or api groups)
|
|
384
|
+
if (!searching && g.name === "worker" && i < groups.length - 1) {
|
|
385
|
+
lines.push(` ${dim("─".repeat(cols - 4))}`);
|
|
386
|
+
}
|
|
387
|
+
// Separator after the last plugin group (before api groups), only when plugins exist
|
|
388
|
+
if (!searching && g._plugin && !groups[i + 1]?._plugin && i < groups.length - 1) {
|
|
381
389
|
lines.push(` ${dim("─".repeat(cols - 4))}`);
|
|
382
390
|
}
|
|
383
391
|
}
|
|
@@ -836,7 +844,7 @@ function renderProfile(state, screen) {
|
|
|
836
844
|
const hi = Math.min(screen.scroll + viewH, state.profileInfo.length);
|
|
837
845
|
lines.push(` ${dim(`${screen.scroll + 1}–${hi} of ${state.profileInfo.length}`)}`);
|
|
838
846
|
}
|
|
839
|
-
lines.push(` ${dim("↑↓")} scroll ${cyan("esc
|
|
847
|
+
lines.push(` ${dim("↑↓")} scroll ${cyan("esc")} close ${cyan("q")} quit`);
|
|
840
848
|
return lines;
|
|
841
849
|
}
|
|
842
850
|
const SETTINGS_ROWS = [
|
|
@@ -854,6 +862,24 @@ const SETTINGS_ROWS = [
|
|
|
854
862
|
label: "Show Audit Log",
|
|
855
863
|
description: "View recent commands executed for the active profile",
|
|
856
864
|
},
|
|
865
|
+
{
|
|
866
|
+
kind: "nav",
|
|
867
|
+
id: "active-profile",
|
|
868
|
+
label: "Active Profile",
|
|
869
|
+
description: "View details for the current connection profile",
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
kind: "nav",
|
|
873
|
+
id: "profiles",
|
|
874
|
+
label: "Profiles",
|
|
875
|
+
description: "Manage and switch connection profiles",
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
kind: "nav",
|
|
879
|
+
id: "plugins",
|
|
880
|
+
label: "Plugins",
|
|
881
|
+
description: "Browse and manage CLI plugins",
|
|
882
|
+
},
|
|
857
883
|
];
|
|
858
884
|
function renderSettings(state, screen) {
|
|
859
885
|
const { cols } = termSize();
|
|
@@ -936,7 +962,7 @@ function renderWorker(state, screen) {
|
|
|
936
962
|
: screen.status === "stopping"
|
|
937
963
|
? cyan("◌ STOPPING")
|
|
938
964
|
: dim("■ STOPPED");
|
|
939
|
-
const lines = [renderHeader(["
|
|
965
|
+
const lines = [renderHeader(["worker"], cols, state.profile), ""];
|
|
940
966
|
lines.push(` ${padEnd(dim("type"), 8)} ${cyan(screen.jobType)} ${dim("status")} ${statusStr}`);
|
|
941
967
|
lines.push(` ${dim("─".repeat(cols - 4))}`);
|
|
942
968
|
const { activated, completed, failed } = screen.stats;
|
|
@@ -1130,14 +1156,44 @@ async function runWorkerLoop(ws, state, variables, jobTimeout, maxJobs) {
|
|
|
1130
1156
|
break;
|
|
1131
1157
|
ws.stats.activated++;
|
|
1132
1158
|
addWorkerLog(ws, "info", `Activated ${job.jobKey} process=${job.processDefinitionId} element=${job.elementId}`);
|
|
1159
|
+
let jobResult = { outcome: "complete", variables };
|
|
1160
|
+
const processJob = ws.cmd._worker?.processJob;
|
|
1161
|
+
if (processJob) {
|
|
1162
|
+
try {
|
|
1163
|
+
jobResult = await processJob(job);
|
|
1164
|
+
}
|
|
1165
|
+
catch (err) {
|
|
1166
|
+
addWorkerLog(ws, "err", `Handler error: ${err instanceof Error ? err.message : String(err)} — using defaults`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1133
1169
|
try {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1170
|
+
if (jobResult.outcome === "complete") {
|
|
1171
|
+
await client.job.completeJob(job.jobKey, { variables: jobResult.variables });
|
|
1172
|
+
ws.stats.completed++;
|
|
1173
|
+
addWorkerLog(ws, "ok", `Completed ${job.jobKey}`);
|
|
1174
|
+
}
|
|
1175
|
+
else if (jobResult.outcome === "fail") {
|
|
1176
|
+
await client.job.failJob(job.jobKey, {
|
|
1177
|
+
errorMessage: jobResult.errorMessage,
|
|
1178
|
+
retries: jobResult.retries,
|
|
1179
|
+
retryBackOff: jobResult.retryBackOff,
|
|
1180
|
+
});
|
|
1181
|
+
ws.stats.failed++;
|
|
1182
|
+
addWorkerLog(ws, "err", `Failed ${job.jobKey}: ${jobResult.errorMessage}`);
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
await client.job.throwJobError(job.jobKey, {
|
|
1186
|
+
errorCode: jobResult.errorCode,
|
|
1187
|
+
errorMessage: jobResult.errorMessage,
|
|
1188
|
+
variables: jobResult.variables,
|
|
1189
|
+
});
|
|
1190
|
+
ws.stats.failed++;
|
|
1191
|
+
addWorkerLog(ws, "err", `Error ${job.jobKey} [${jobResult.errorCode}]: ${jobResult.errorMessage ?? ""}`);
|
|
1192
|
+
}
|
|
1137
1193
|
}
|
|
1138
1194
|
catch (err) {
|
|
1139
1195
|
ws.stats.failed++;
|
|
1140
|
-
addWorkerLog(ws, "err", `Failed to
|
|
1196
|
+
addWorkerLog(ws, "err", `Failed to settle ${job.jobKey}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1141
1197
|
}
|
|
1142
1198
|
}
|
|
1143
1199
|
if (jobs.length > 0)
|
|
@@ -1403,6 +1459,9 @@ function render(state) {
|
|
|
1403
1459
|
case "json-editor":
|
|
1404
1460
|
lines = renderJsonEditor(state, screen);
|
|
1405
1461
|
break;
|
|
1462
|
+
case "plugins":
|
|
1463
|
+
lines = renderPlugins(state, screen);
|
|
1464
|
+
break;
|
|
1406
1465
|
}
|
|
1407
1466
|
process.stdout.write(`${CLEAR}${lines.join("\n")}\n`);
|
|
1408
1467
|
}
|
|
@@ -1485,6 +1544,24 @@ function handleMainKey(key, screen, state, done) {
|
|
|
1485
1544
|
_timer: null,
|
|
1486
1545
|
});
|
|
1487
1546
|
}
|
|
1547
|
+
else if (group &&
|
|
1548
|
+
group.commands.length === 1 &&
|
|
1549
|
+
group.commands[0] &&
|
|
1550
|
+
"_worker" in group.commands[0]) {
|
|
1551
|
+
// Single-command worker group: skip commands list, go straight to input
|
|
1552
|
+
const cmd = group.commands[0];
|
|
1553
|
+
state.stack.push({
|
|
1554
|
+
kind: "input",
|
|
1555
|
+
group,
|
|
1556
|
+
cmd,
|
|
1557
|
+
fields: buildFields(cmd),
|
|
1558
|
+
cursor: 0,
|
|
1559
|
+
scroll: 0,
|
|
1560
|
+
editing: false,
|
|
1561
|
+
error: "",
|
|
1562
|
+
running: false,
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1488
1565
|
else if (group) {
|
|
1489
1566
|
state.stack.push({ kind: "commands", group, cursor: 0, search: "" });
|
|
1490
1567
|
}
|
|
@@ -1794,7 +1871,7 @@ async function handleInputKey(key, screen, state, done) {
|
|
|
1794
1871
|
if (!field)
|
|
1795
1872
|
break;
|
|
1796
1873
|
if (field.kind === "run") {
|
|
1797
|
-
if (screen.cmd
|
|
1874
|
+
if ("_worker" in screen.cmd) {
|
|
1798
1875
|
launchWorkerView(screen, state);
|
|
1799
1876
|
}
|
|
1800
1877
|
else {
|
|
@@ -2600,6 +2677,15 @@ function handleSettingsKey(key, screen, state, done) {
|
|
|
2600
2677
|
else if (row?.kind === "action" && row.id === "show-audit-log") {
|
|
2601
2678
|
state.stack.push({ kind: "audit-log", scroll: 0 });
|
|
2602
2679
|
}
|
|
2680
|
+
else if (row?.kind === "nav" && row.id === "active-profile") {
|
|
2681
|
+
state.stack.push({ kind: "profile", scroll: 0 });
|
|
2682
|
+
}
|
|
2683
|
+
else if (row?.kind === "nav" && row.id === "profiles") {
|
|
2684
|
+
state.stack.push({ kind: "commands", group: profileGroup, cursor: 0, search: "" });
|
|
2685
|
+
}
|
|
2686
|
+
else if (row?.kind === "nav" && row.id === "plugins") {
|
|
2687
|
+
state.stack.push(newPluginsScreen());
|
|
2688
|
+
}
|
|
2603
2689
|
break;
|
|
2604
2690
|
}
|
|
2605
2691
|
case "\x1b":
|
|
@@ -2693,6 +2779,499 @@ function handleWorkerKey(key, screen, state, done) {
|
|
|
2693
2779
|
}
|
|
2694
2780
|
render(state);
|
|
2695
2781
|
}
|
|
2782
|
+
// ─── Plugins screen ───────────────────────────────────────────────────────────
|
|
2783
|
+
const PLUGIN_MENU = [
|
|
2784
|
+
{ id: "search", label: "search", description: "Search the npm registry for plugins" },
|
|
2785
|
+
{ id: "list", label: "list", description: "Show installed plugins" },
|
|
2786
|
+
{ id: "install", label: "install", description: "Install a plugin from npm or local path" },
|
|
2787
|
+
{
|
|
2788
|
+
id: "update",
|
|
2789
|
+
label: "update",
|
|
2790
|
+
description: "Update installed plugins to their latest versions",
|
|
2791
|
+
},
|
|
2792
|
+
{ id: "remove", label: "remove", description: "Remove a plugin" },
|
|
2793
|
+
];
|
|
2794
|
+
function newPluginsScreen() {
|
|
2795
|
+
return {
|
|
2796
|
+
kind: "plugins",
|
|
2797
|
+
subview: "menu",
|
|
2798
|
+
menuCursor: 0,
|
|
2799
|
+
query: "",
|
|
2800
|
+
queryCursor: 0,
|
|
2801
|
+
resultKind: "search",
|
|
2802
|
+
results: [],
|
|
2803
|
+
resultCursor: 0,
|
|
2804
|
+
resultScroll: 0,
|
|
2805
|
+
promptLabel: "",
|
|
2806
|
+
promptValue: "",
|
|
2807
|
+
promptCursor: 0,
|
|
2808
|
+
promptAction: "install",
|
|
2809
|
+
message: "",
|
|
2810
|
+
error: "",
|
|
2811
|
+
running: false,
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
function renderPlugins(state, screen) {
|
|
2815
|
+
const { cols, rows } = termSize();
|
|
2816
|
+
if (screen.subview === "menu") {
|
|
2817
|
+
const nameW = PLUGIN_MENU.reduce((m, item) => Math.max(m, item.label.length), 0) + 2;
|
|
2818
|
+
const lines = [renderHeader(["settings", "plugins"], cols, state.profile), ""];
|
|
2819
|
+
for (let i = 0; i < PLUGIN_MENU.length; i++) {
|
|
2820
|
+
const item = PLUGIN_MENU[i];
|
|
2821
|
+
if (!item)
|
|
2822
|
+
continue;
|
|
2823
|
+
const isCursor = i === screen.menuCursor;
|
|
2824
|
+
const name = padEnd(isCursor ? cyan(item.label) : item.label, nameW);
|
|
2825
|
+
const desc = dim(fit(item.description, cols - nameW - 6));
|
|
2826
|
+
const line = ` ${name} ${desc}`;
|
|
2827
|
+
lines.push(isCursor ? inv(line.padEnd(cols - 1)) : line);
|
|
2828
|
+
}
|
|
2829
|
+
lines.push("");
|
|
2830
|
+
lines.push(` ${dim("↑↓")} navigate ${cyan("enter")} open ${cyan("esc")} back ${cyan("q")} quit`);
|
|
2831
|
+
return lines;
|
|
2832
|
+
}
|
|
2833
|
+
if (screen.subview === "search") {
|
|
2834
|
+
const lines = [renderHeader(["settings", "plugins", "search"], cols, state.profile), ""];
|
|
2835
|
+
lines.push(` ${dim("Search the npm registry — leave empty to browse all plugins")}`);
|
|
2836
|
+
lines.push("");
|
|
2837
|
+
lines.push(` > ${renderText(screen.query, screen.queryCursor, cols - 6, true)}`);
|
|
2838
|
+
if (screen.running) {
|
|
2839
|
+
lines.push("");
|
|
2840
|
+
lines.push(` ${dim("Searching…")}`);
|
|
2841
|
+
}
|
|
2842
|
+
else if (screen.error) {
|
|
2843
|
+
lines.push("");
|
|
2844
|
+
lines.push(` ${red("error:")} ${screen.error}`);
|
|
2845
|
+
}
|
|
2846
|
+
lines.push("");
|
|
2847
|
+
lines.push(` ${dim("type query")} ${cyan("enter")} search ${cyan("esc")} back ${cyan("^C")} quit`);
|
|
2848
|
+
return lines;
|
|
2849
|
+
}
|
|
2850
|
+
if (screen.subview === "results") {
|
|
2851
|
+
const viewH = Math.max(3, rows - 10);
|
|
2852
|
+
const isSearch = screen.resultKind === "search";
|
|
2853
|
+
const title = isSearch ? "search results" : "installed";
|
|
2854
|
+
const lines = [renderHeader(["settings", "plugins", title], cols, state.profile), ""];
|
|
2855
|
+
if (screen.results.length === 0) {
|
|
2856
|
+
lines.push(` ${dim(isSearch ? "No plugins found." : "No plugins installed.")}`);
|
|
2857
|
+
}
|
|
2858
|
+
else {
|
|
2859
|
+
const nameW = Math.min(32, screen.results.reduce((m, r) => Math.max(m, r.name.length), 0)) + 2;
|
|
2860
|
+
// Official badge shown for @bpmnkit/* packages (2 visible chars: "◆ ")
|
|
2861
|
+
const BADGE = `${cyan("◆")} `;
|
|
2862
|
+
const BADGE_W = 2;
|
|
2863
|
+
const visible = screen.results.slice(screen.resultScroll, screen.resultScroll + viewH);
|
|
2864
|
+
for (let vi = 0; vi < visible.length; vi++) {
|
|
2865
|
+
const r = visible[vi];
|
|
2866
|
+
if (!r)
|
|
2867
|
+
continue;
|
|
2868
|
+
const ri = screen.resultScroll + vi;
|
|
2869
|
+
const isCursor = ri === screen.resultCursor;
|
|
2870
|
+
const isOfficial = r.name.startsWith("@bpmnkit/");
|
|
2871
|
+
const badge = isOfficial ? BADGE : " ".repeat(BADGE_W);
|
|
2872
|
+
const name = padEnd(isCursor ? cyan(r.name) : r.name, nameW);
|
|
2873
|
+
const verStr = r.version.padEnd(10);
|
|
2874
|
+
const extra = isSearch
|
|
2875
|
+
? dim(fit(r.description, cols - nameW - BADGE_W - 14))
|
|
2876
|
+
: dim(r.installedAt.slice(0, 10));
|
|
2877
|
+
const line = ` ${badge}${name} ${verStr} ${extra}`;
|
|
2878
|
+
lines.push(isCursor ? inv(line.padEnd(cols - 1)) : line);
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
if (screen.message) {
|
|
2882
|
+
lines.push("");
|
|
2883
|
+
lines.push(` ${screen.message}`);
|
|
2884
|
+
}
|
|
2885
|
+
lines.push("");
|
|
2886
|
+
const actions = isSearch
|
|
2887
|
+
? `${cyan("enter/i")} install ${cyan("esc")} back ${cyan("q")} quit`
|
|
2888
|
+
: `${cyan("enter/r")} remove ${cyan("esc")} back ${cyan("q")} quit`;
|
|
2889
|
+
lines.push(` ${dim("↑↓")} navigate ${actions}`);
|
|
2890
|
+
lines.push(` ${cyan("◆")} ${dim("= official @bpmnkit plugin")}`);
|
|
2891
|
+
return lines;
|
|
2892
|
+
}
|
|
2893
|
+
if (screen.subview === "prompt") {
|
|
2894
|
+
const lines = [
|
|
2895
|
+
renderHeader(["settings", "plugins", screen.promptAction], cols, state.profile),
|
|
2896
|
+
"",
|
|
2897
|
+
];
|
|
2898
|
+
lines.push(` ${screen.promptLabel}`);
|
|
2899
|
+
lines.push("");
|
|
2900
|
+
lines.push(` > ${renderText(screen.promptValue, screen.promptCursor, cols - 6, true)}`);
|
|
2901
|
+
if (screen.running) {
|
|
2902
|
+
lines.push("");
|
|
2903
|
+
lines.push(` ${dim("Running…")}`);
|
|
2904
|
+
}
|
|
2905
|
+
else if (screen.error) {
|
|
2906
|
+
lines.push("");
|
|
2907
|
+
lines.push(` ${red("error:")} ${screen.error}`);
|
|
2908
|
+
}
|
|
2909
|
+
lines.push("");
|
|
2910
|
+
lines.push(` ${dim("type name")} ${cyan("enter")} confirm ${cyan("esc")} back ${cyan("^C")} quit`);
|
|
2911
|
+
return lines;
|
|
2912
|
+
}
|
|
2913
|
+
// "done"
|
|
2914
|
+
const lines = [renderHeader(["settings", "plugins"], cols, state.profile), "", ""];
|
|
2915
|
+
if (screen.error) {
|
|
2916
|
+
lines.push(` ${red("error:")} ${screen.error}`);
|
|
2917
|
+
}
|
|
2918
|
+
else {
|
|
2919
|
+
for (const line of screen.message.split("\n")) {
|
|
2920
|
+
lines.push(` ${line}`);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
lines.push("");
|
|
2924
|
+
lines.push(` ${cyan("esc")} back ${cyan("q")} quit`);
|
|
2925
|
+
return lines;
|
|
2926
|
+
}
|
|
2927
|
+
/** Temporarily restore the normal terminal, run fn (which may spawn processes), then re-enter. */
|
|
2928
|
+
async function runWithTerminal(fn) {
|
|
2929
|
+
process.stdout.write(`${ALT_OFF}${SHOW}`);
|
|
2930
|
+
if (process.stdin.isTTY)
|
|
2931
|
+
process.stdin.setRawMode(false);
|
|
2932
|
+
try {
|
|
2933
|
+
await fn();
|
|
2934
|
+
}
|
|
2935
|
+
finally {
|
|
2936
|
+
if (process.stdin.isTTY)
|
|
2937
|
+
process.stdin.setRawMode(true);
|
|
2938
|
+
process.stdout.write(`${ALT_ON}${HIDE}${CLEAR}`);
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
async function handlePluginsKey(key, screen, state, done) {
|
|
2942
|
+
if (key === "\x03") {
|
|
2943
|
+
done();
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
// ── Menu subview ──────────────────────────────────────────────────────────
|
|
2947
|
+
if (screen.subview === "menu") {
|
|
2948
|
+
switch (key) {
|
|
2949
|
+
case "\x1b[A":
|
|
2950
|
+
if (screen.menuCursor > 0)
|
|
2951
|
+
screen.menuCursor--;
|
|
2952
|
+
break;
|
|
2953
|
+
case "\x1b[B":
|
|
2954
|
+
if (screen.menuCursor < PLUGIN_MENU.length - 1)
|
|
2955
|
+
screen.menuCursor++;
|
|
2956
|
+
break;
|
|
2957
|
+
case "\r":
|
|
2958
|
+
case "\n": {
|
|
2959
|
+
const item = PLUGIN_MENU[screen.menuCursor];
|
|
2960
|
+
if (item?.id === "search") {
|
|
2961
|
+
screen.subview = "search";
|
|
2962
|
+
screen.query = "";
|
|
2963
|
+
screen.queryCursor = 0;
|
|
2964
|
+
screen.error = "";
|
|
2965
|
+
}
|
|
2966
|
+
else if (item?.id === "list") {
|
|
2967
|
+
screen.running = true;
|
|
2968
|
+
screen.error = "";
|
|
2969
|
+
render(state);
|
|
2970
|
+
try {
|
|
2971
|
+
const plugins = await readInstalledPlugins();
|
|
2972
|
+
screen.resultKind = "installed";
|
|
2973
|
+
screen.results = plugins.map((p) => ({
|
|
2974
|
+
name: p.package,
|
|
2975
|
+
version: p.version,
|
|
2976
|
+
description: "",
|
|
2977
|
+
publisher: "",
|
|
2978
|
+
score: "",
|
|
2979
|
+
installedAt: p.installedAt,
|
|
2980
|
+
}));
|
|
2981
|
+
screen.resultCursor = 0;
|
|
2982
|
+
screen.resultScroll = 0;
|
|
2983
|
+
screen.message = "";
|
|
2984
|
+
screen.subview = "results";
|
|
2985
|
+
}
|
|
2986
|
+
catch (err) {
|
|
2987
|
+
screen.error = err instanceof Error ? err.message : String(err);
|
|
2988
|
+
}
|
|
2989
|
+
finally {
|
|
2990
|
+
screen.running = false;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
else if (item?.id === "install") {
|
|
2994
|
+
screen.subview = "prompt";
|
|
2995
|
+
screen.promptLabel = "Package name (e.g. casen-deploy or ./local-path):";
|
|
2996
|
+
screen.promptValue = "";
|
|
2997
|
+
screen.promptCursor = 0;
|
|
2998
|
+
screen.promptAction = "install";
|
|
2999
|
+
screen.error = "";
|
|
3000
|
+
}
|
|
3001
|
+
else if (item?.id === "update") {
|
|
3002
|
+
await runWithTerminal(async () => {
|
|
3003
|
+
const cmd = pluginGroup.commands.find((c) => c.name === "update");
|
|
3004
|
+
if (!cmd)
|
|
3005
|
+
return;
|
|
3006
|
+
const { writer } = makeCapturingWriter();
|
|
3007
|
+
await cmd.run({
|
|
3008
|
+
positional: [],
|
|
3009
|
+
flags: {},
|
|
3010
|
+
output: writer,
|
|
3011
|
+
getClient: state.getClient,
|
|
3012
|
+
getAdminClient: state.getAdminClient,
|
|
3013
|
+
});
|
|
3014
|
+
});
|
|
3015
|
+
screen.subview = "done";
|
|
3016
|
+
screen.message = green("✓ Update complete. Restart casen to activate changes.");
|
|
3017
|
+
screen.error = "";
|
|
3018
|
+
}
|
|
3019
|
+
else if (item?.id === "remove") {
|
|
3020
|
+
screen.subview = "prompt";
|
|
3021
|
+
screen.promptLabel = "Plugin package name to remove:";
|
|
3022
|
+
screen.promptValue = "";
|
|
3023
|
+
screen.promptCursor = 0;
|
|
3024
|
+
screen.promptAction = "remove";
|
|
3025
|
+
screen.error = "";
|
|
3026
|
+
}
|
|
3027
|
+
break;
|
|
3028
|
+
}
|
|
3029
|
+
case "\x1b":
|
|
3030
|
+
state.stack.pop();
|
|
3031
|
+
break;
|
|
3032
|
+
case "q":
|
|
3033
|
+
case "Q":
|
|
3034
|
+
done();
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
render(state);
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
// ── Search subview ────────────────────────────────────────────────────────
|
|
3041
|
+
if (screen.subview === "search") {
|
|
3042
|
+
if (key === "\x1b") {
|
|
3043
|
+
screen.subview = "menu";
|
|
3044
|
+
render(state);
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
if (key === "\r" || key === "\n") {
|
|
3048
|
+
screen.running = true;
|
|
3049
|
+
screen.error = "";
|
|
3050
|
+
render(state);
|
|
3051
|
+
try {
|
|
3052
|
+
const raw = await searchNpmRegistry(screen.query);
|
|
3053
|
+
screen.results = raw.map((r) => ({
|
|
3054
|
+
name: r.package.name,
|
|
3055
|
+
version: r.package.version,
|
|
3056
|
+
description: r.package.description ?? "",
|
|
3057
|
+
publisher: r.package.publisher?.username ?? "",
|
|
3058
|
+
score: r.score.final.toFixed(2),
|
|
3059
|
+
installedAt: "",
|
|
3060
|
+
}));
|
|
3061
|
+
screen.resultKind = "search";
|
|
3062
|
+
screen.resultCursor = 0;
|
|
3063
|
+
screen.resultScroll = 0;
|
|
3064
|
+
screen.message = "";
|
|
3065
|
+
screen.subview = "results";
|
|
3066
|
+
}
|
|
3067
|
+
catch (err) {
|
|
3068
|
+
screen.error = err instanceof Error ? err.message : String(err);
|
|
3069
|
+
}
|
|
3070
|
+
finally {
|
|
3071
|
+
screen.running = false;
|
|
3072
|
+
}
|
|
3073
|
+
render(state);
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
if (key === "\x7f" || key === "\x08") {
|
|
3077
|
+
if (screen.queryCursor > 0) {
|
|
3078
|
+
screen.query =
|
|
3079
|
+
screen.query.slice(0, screen.queryCursor - 1) + screen.query.slice(screen.queryCursor);
|
|
3080
|
+
screen.queryCursor--;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
else if (key === "\x1b[D") {
|
|
3084
|
+
if (screen.queryCursor > 0)
|
|
3085
|
+
screen.queryCursor--;
|
|
3086
|
+
}
|
|
3087
|
+
else if (key === "\x1b[C") {
|
|
3088
|
+
if (screen.queryCursor < screen.query.length)
|
|
3089
|
+
screen.queryCursor++;
|
|
3090
|
+
}
|
|
3091
|
+
else if (key === "\x01" || key === "\x1b[H") {
|
|
3092
|
+
screen.queryCursor = 0;
|
|
3093
|
+
}
|
|
3094
|
+
else if (key === "\x05" || key === "\x1b[F") {
|
|
3095
|
+
screen.queryCursor = screen.query.length;
|
|
3096
|
+
}
|
|
3097
|
+
else if (key.length === 1 && key >= " ") {
|
|
3098
|
+
screen.query =
|
|
3099
|
+
screen.query.slice(0, screen.queryCursor) + key + screen.query.slice(screen.queryCursor);
|
|
3100
|
+
screen.queryCursor++;
|
|
3101
|
+
}
|
|
3102
|
+
render(state);
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
// ── Results subview ───────────────────────────────────────────────────────
|
|
3106
|
+
if (screen.subview === "results") {
|
|
3107
|
+
const { rows } = termSize();
|
|
3108
|
+
const viewH = Math.max(3, rows - 10);
|
|
3109
|
+
switch (key) {
|
|
3110
|
+
case "\x1b[A":
|
|
3111
|
+
if (screen.resultCursor > 0) {
|
|
3112
|
+
screen.resultCursor--;
|
|
3113
|
+
if (screen.resultCursor < screen.resultScroll)
|
|
3114
|
+
screen.resultScroll--;
|
|
3115
|
+
}
|
|
3116
|
+
break;
|
|
3117
|
+
case "\x1b[B":
|
|
3118
|
+
if (screen.resultCursor < screen.results.length - 1) {
|
|
3119
|
+
screen.resultCursor++;
|
|
3120
|
+
if (screen.resultCursor >= screen.resultScroll + viewH)
|
|
3121
|
+
screen.resultScroll++;
|
|
3122
|
+
}
|
|
3123
|
+
break;
|
|
3124
|
+
case "\r":
|
|
3125
|
+
case "\n":
|
|
3126
|
+
case "i":
|
|
3127
|
+
case "I":
|
|
3128
|
+
case "r":
|
|
3129
|
+
case "R": {
|
|
3130
|
+
const result = screen.results[screen.resultCursor];
|
|
3131
|
+
if (!result)
|
|
3132
|
+
break;
|
|
3133
|
+
if (screen.resultKind === "installed" && (key === "i" || key === "I"))
|
|
3134
|
+
break;
|
|
3135
|
+
const action = screen.resultKind === "installed" ? "remove" : "install";
|
|
3136
|
+
const pkgName = result.name;
|
|
3137
|
+
screen.message = dim(`${action === "install" ? "Installing" : "Removing"} ${pkgName}…`);
|
|
3138
|
+
render(state);
|
|
3139
|
+
const cmd = pluginGroup.commands.find((c) => c.name === action);
|
|
3140
|
+
if (!cmd)
|
|
3141
|
+
break;
|
|
3142
|
+
let opError = "";
|
|
3143
|
+
let opMessage = "";
|
|
3144
|
+
await runWithTerminal(async () => {
|
|
3145
|
+
const { writer, get } = makeCapturingWriter();
|
|
3146
|
+
try {
|
|
3147
|
+
await cmd.run({
|
|
3148
|
+
positional: [pkgName],
|
|
3149
|
+
flags: {},
|
|
3150
|
+
output: writer,
|
|
3151
|
+
getClient: state.getClient,
|
|
3152
|
+
getAdminClient: state.getAdminClient,
|
|
3153
|
+
});
|
|
3154
|
+
const out = get();
|
|
3155
|
+
if (out.type === "messages")
|
|
3156
|
+
opMessage = out.lines.join("\n");
|
|
3157
|
+
else
|
|
3158
|
+
opMessage = green(`✓ ${action === "install" ? "Installed" : "Removed"} ${pkgName}`);
|
|
3159
|
+
}
|
|
3160
|
+
catch (err) {
|
|
3161
|
+
opError = err instanceof Error ? err.message : String(err);
|
|
3162
|
+
}
|
|
3163
|
+
});
|
|
3164
|
+
screen.subview = "done";
|
|
3165
|
+
screen.message = opMessage;
|
|
3166
|
+
screen.error = opError;
|
|
3167
|
+
break;
|
|
3168
|
+
}
|
|
3169
|
+
case "\x1b":
|
|
3170
|
+
// Go back to search input if came from search, else back to menu
|
|
3171
|
+
if (screen.resultKind === "search") {
|
|
3172
|
+
screen.subview = "search";
|
|
3173
|
+
}
|
|
3174
|
+
else {
|
|
3175
|
+
screen.subview = "menu";
|
|
3176
|
+
}
|
|
3177
|
+
break;
|
|
3178
|
+
case "q":
|
|
3179
|
+
case "Q":
|
|
3180
|
+
done();
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
3183
|
+
render(state);
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
// ── Prompt subview ────────────────────────────────────────────────────────
|
|
3187
|
+
if (screen.subview === "prompt") {
|
|
3188
|
+
if (key === "\x1b") {
|
|
3189
|
+
screen.subview = "menu";
|
|
3190
|
+
render(state);
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
if (key === "\r" || key === "\n") {
|
|
3194
|
+
const pkgName = screen.promptValue.trim();
|
|
3195
|
+
if (!pkgName) {
|
|
3196
|
+
screen.error = "Package name is required";
|
|
3197
|
+
render(state);
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
const cmd = pluginGroup.commands.find((c) => c.name === screen.promptAction);
|
|
3201
|
+
if (!cmd)
|
|
3202
|
+
return;
|
|
3203
|
+
let opError = "";
|
|
3204
|
+
let opMessage = "";
|
|
3205
|
+
await runWithTerminal(async () => {
|
|
3206
|
+
const { writer, get } = makeCapturingWriter();
|
|
3207
|
+
try {
|
|
3208
|
+
await cmd.run({
|
|
3209
|
+
positional: [pkgName],
|
|
3210
|
+
flags: {},
|
|
3211
|
+
output: writer,
|
|
3212
|
+
getClient: state.getClient,
|
|
3213
|
+
getAdminClient: state.getAdminClient,
|
|
3214
|
+
});
|
|
3215
|
+
const out = get();
|
|
3216
|
+
if (out.type === "messages")
|
|
3217
|
+
opMessage = out.lines.join("\n");
|
|
3218
|
+
else
|
|
3219
|
+
opMessage = green("✓ Done");
|
|
3220
|
+
}
|
|
3221
|
+
catch (err) {
|
|
3222
|
+
opError = err instanceof Error ? err.message : String(err);
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3225
|
+
screen.subview = "done";
|
|
3226
|
+
screen.message = opMessage;
|
|
3227
|
+
screen.error = opError;
|
|
3228
|
+
render(state);
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
if (key === "\x7f" || key === "\x08") {
|
|
3232
|
+
if (screen.promptCursor > 0) {
|
|
3233
|
+
screen.promptValue =
|
|
3234
|
+
screen.promptValue.slice(0, screen.promptCursor - 1) +
|
|
3235
|
+
screen.promptValue.slice(screen.promptCursor);
|
|
3236
|
+
screen.promptCursor--;
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
else if (key === "\x1b[D") {
|
|
3240
|
+
if (screen.promptCursor > 0)
|
|
3241
|
+
screen.promptCursor--;
|
|
3242
|
+
}
|
|
3243
|
+
else if (key === "\x1b[C") {
|
|
3244
|
+
if (screen.promptCursor < screen.promptValue.length)
|
|
3245
|
+
screen.promptCursor++;
|
|
3246
|
+
}
|
|
3247
|
+
else if (key === "\x01" || key === "\x1b[H") {
|
|
3248
|
+
screen.promptCursor = 0;
|
|
3249
|
+
}
|
|
3250
|
+
else if (key === "\x05" || key === "\x1b[F") {
|
|
3251
|
+
screen.promptCursor = screen.promptValue.length;
|
|
3252
|
+
}
|
|
3253
|
+
else if (key.length === 1 && key >= " ") {
|
|
3254
|
+
screen.promptValue =
|
|
3255
|
+
screen.promptValue.slice(0, screen.promptCursor) +
|
|
3256
|
+
key +
|
|
3257
|
+
screen.promptValue.slice(screen.promptCursor);
|
|
3258
|
+
screen.promptCursor++;
|
|
3259
|
+
}
|
|
3260
|
+
render(state);
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
// ── Done subview ──────────────────────────────────────────────────────────
|
|
3264
|
+
switch (key) {
|
|
3265
|
+
case "\x1b":
|
|
3266
|
+
screen.subview = "menu";
|
|
3267
|
+
break;
|
|
3268
|
+
case "q":
|
|
3269
|
+
case "Q":
|
|
3270
|
+
done();
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
render(state);
|
|
3274
|
+
}
|
|
2696
3275
|
async function handleKey(key, state, done) {
|
|
2697
3276
|
if (state.quitting)
|
|
2698
3277
|
return;
|
|
@@ -2703,18 +3282,6 @@ async function handleKey(key, state, done) {
|
|
|
2703
3282
|
const screen = state.stack[state.stack.length - 1];
|
|
2704
3283
|
if (!screen)
|
|
2705
3284
|
return;
|
|
2706
|
-
// Global p/P: open profile view (skip when actively editing text)
|
|
2707
|
-
if (key === "p" || key === "P") {
|
|
2708
|
-
const isEditing = (screen.kind === "input" && screen.editing) ||
|
|
2709
|
-
(screen.kind === "json-editor" && screen.editing) ||
|
|
2710
|
-
(screen.kind === "settings" && screen.editing);
|
|
2711
|
-
const isProfile = screen.kind === "profile";
|
|
2712
|
-
if (!isEditing && !isProfile) {
|
|
2713
|
-
state.stack.push({ kind: "profile", scroll: 0 });
|
|
2714
|
-
render(state);
|
|
2715
|
-
return;
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
3285
|
switch (screen.kind) {
|
|
2719
3286
|
case "main":
|
|
2720
3287
|
handleMainKey(key, screen, state, done);
|
|
@@ -2752,6 +3319,9 @@ async function handleKey(key, state, done) {
|
|
|
2752
3319
|
case "json-editor":
|
|
2753
3320
|
handleJsonEditorKey(key, screen, state, done);
|
|
2754
3321
|
break;
|
|
3322
|
+
case "plugins":
|
|
3323
|
+
await handlePluginsKey(key, screen, state, done);
|
|
3324
|
+
break;
|
|
2755
3325
|
}
|
|
2756
3326
|
}
|
|
2757
3327
|
// ─── Shared TUI runner ────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bpmnkit/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "Command-line interface for Camunda 8 — deploy, manage, and monitor processes from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@bpmnkit/api": "0.0.13",
|
|
19
|
-
"@bpmnkit/ascii": "0.0.
|
|
19
|
+
"@bpmnkit/ascii": "0.0.15",
|
|
20
20
|
"@bpmnkit/connector-gen": "0.0.8",
|
|
21
|
+
"@bpmnkit/core": "0.0.15",
|
|
22
|
+
"@bpmnkit/engine": "0.1.14",
|
|
21
23
|
"@bpmnkit/profiles": "0.0.10"
|
|
22
24
|
},
|
|
23
25
|
"publishConfig": {
|