@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/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 ────────────────────────────────────────────────────────
|