@bpmnkit/cli 0.0.16 → 0.0.18
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 +219 -0
- package/dist/commands/plugin.js +1 -1
- package/dist/commands/profile.js +62 -3
- package/dist/commands/story.js +42 -0
- package/dist/commands/test.js +77 -0
- package/dist/commands/worker.js +16 -12
- package/dist/generated/commands.js +4 -0
- package/dist/output.js +25 -0
- package/dist/run.js +28 -4
- package/dist/tui.js +798 -44
- package/package.json +5 -3
package/dist/tui.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { renderBpmnAscii } from "@bpmnkit/ascii";
|
|
3
|
-
import { appendAuditEntry, getAuditLog, getSettings, saveSettings } from "@bpmnkit/profiles";
|
|
3
|
+
import { appendAuditEntry, deleteProfile, getActiveName, getAuditLog, getProfile, getSettings, listProfiles, saveSettings, useProfile, } 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
|
});
|
|
@@ -189,8 +193,9 @@ function entriesToJson(entries) {
|
|
|
189
193
|
}
|
|
190
194
|
/**
|
|
191
195
|
* Build initial entries for the JSON editor.
|
|
192
|
-
* If fieldSpecs are provided, pre-populate
|
|
193
|
-
* merging in any existing values.
|
|
196
|
+
* If fieldSpecs are provided, pre-populate only required fields (in spec order),
|
|
197
|
+
* merging in any existing values. Optional fields are available via the add-row picker.
|
|
198
|
+
* Extra keys from existing JSON that are not in the spec are appended at the end.
|
|
194
199
|
*/
|
|
195
200
|
function buildInitialEntries(existingJson, fieldSpecs) {
|
|
196
201
|
const existing = {};
|
|
@@ -209,7 +214,10 @@ function buildInitialEntries(existingJson, fieldSpecs) {
|
|
|
209
214
|
return parseJsonToEntries(existingJson);
|
|
210
215
|
const seenKeys = new Set();
|
|
211
216
|
const entries = [];
|
|
217
|
+
// Only pre-populate required fields; optional ones are added on demand
|
|
212
218
|
for (const spec of fieldSpecs) {
|
|
219
|
+
if (!spec.required && !(spec.name in existing))
|
|
220
|
+
continue;
|
|
213
221
|
seenKeys.add(spec.name);
|
|
214
222
|
const val = existing[spec.name] ?? "";
|
|
215
223
|
entries.push({ key: spec.name, keyCursor: spec.name.length, val, valCursor: val.length });
|
|
@@ -226,6 +234,57 @@ function buildInitialEntries(existingJson, fieldSpecs) {
|
|
|
226
234
|
function getFieldSpec(key, fieldSpecs) {
|
|
227
235
|
return fieldSpecs?.find((s) => s.name === key);
|
|
228
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Return specs not already used as keys by OTHER entries (not the one at currentIndex).
|
|
239
|
+
* Pass null for currentIndex when adding a brand-new entry.
|
|
240
|
+
*/
|
|
241
|
+
function getAvailableFieldSpecs(currentIndex, entries, fieldSpecs) {
|
|
242
|
+
const usedKeys = new Set(entries
|
|
243
|
+
.filter((_, i) => i !== currentIndex)
|
|
244
|
+
.map((e) => e.key)
|
|
245
|
+
.filter(Boolean));
|
|
246
|
+
return fieldSpecs.filter((s) => !usedKeys.has(s.name));
|
|
247
|
+
}
|
|
248
|
+
/** Build profile info entries (key/value pairs with secrets redacted). */
|
|
249
|
+
function buildProfileInfoEntries(profileName) {
|
|
250
|
+
const p = getProfile(profileName);
|
|
251
|
+
if (!p)
|
|
252
|
+
return [{ key: "status", value: "profile not found" }];
|
|
253
|
+
const info = [
|
|
254
|
+
{ key: "name", value: p.name },
|
|
255
|
+
{ key: "apiType", value: p.apiType },
|
|
256
|
+
{ key: "baseUrl", value: p.config.baseUrl ?? "(default)" },
|
|
257
|
+
];
|
|
258
|
+
const auth = p.config.auth;
|
|
259
|
+
if (auth) {
|
|
260
|
+
info.push({ key: "auth.type", value: auth.type });
|
|
261
|
+
if (auth.type === "bearer") {
|
|
262
|
+
info.push({ key: "auth.token", value: "***" });
|
|
263
|
+
}
|
|
264
|
+
else if (auth.type === "oauth2") {
|
|
265
|
+
info.push({ key: "auth.clientId", value: auth.clientId });
|
|
266
|
+
info.push({ key: "auth.clientSecret", value: "***" });
|
|
267
|
+
info.push({ key: "auth.tokenUrl", value: auth.tokenUrl });
|
|
268
|
+
}
|
|
269
|
+
else if (auth.type === "basic") {
|
|
270
|
+
info.push({ key: "auth.username", value: auth.username });
|
|
271
|
+
info.push({ key: "auth.password", value: "***" });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return info;
|
|
275
|
+
}
|
|
276
|
+
/** Rebuild profile list items after use/delete. */
|
|
277
|
+
function rebuildProfileListItems() {
|
|
278
|
+
const profiles = listProfiles();
|
|
279
|
+
const active = getActiveName();
|
|
280
|
+
return profiles.map((p) => ({
|
|
281
|
+
active: p.name === active ? "●" : " ",
|
|
282
|
+
name: p.name,
|
|
283
|
+
apiType: p.apiType,
|
|
284
|
+
baseUrl: p.config.baseUrl ?? "(from env/file)",
|
|
285
|
+
authType: p.config.auth?.type ?? "—",
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
229
288
|
// ─── Table helpers ────────────────────────────────────────────────────────────
|
|
230
289
|
function getCellStr(item, col) {
|
|
231
290
|
let val = item;
|
|
@@ -376,8 +435,12 @@ function renderMain(state, screen) {
|
|
|
376
435
|
const desc = dim(fit(g.description, cols - nameW - 8));
|
|
377
436
|
const line = ` ${name} ${desc}`;
|
|
378
437
|
lines.push(isCursor ? inv(line.padEnd(cols - 1)) : line);
|
|
379
|
-
//
|
|
380
|
-
if (!searching && g.name === "
|
|
438
|
+
// Separator after pinned section (after "worker", before plugins or api groups)
|
|
439
|
+
if (!searching && g.name === "worker" && i < groups.length - 1) {
|
|
440
|
+
lines.push(` ${dim("─".repeat(cols - 4))}`);
|
|
441
|
+
}
|
|
442
|
+
// Separator after the last plugin group (before api groups), only when plugins exist
|
|
443
|
+
if (!searching && g._plugin && !groups[i + 1]?._plugin && i < groups.length - 1) {
|
|
381
444
|
lines.push(` ${dim("─".repeat(cols - 4))}`);
|
|
382
445
|
}
|
|
383
446
|
}
|
|
@@ -507,6 +570,22 @@ function renderInput(state, screen) {
|
|
|
507
570
|
lines.push("");
|
|
508
571
|
if (screen.error) {
|
|
509
572
|
lines.push(` ${red("error:")} ${screen.error}`);
|
|
573
|
+
if (screen.errorRaw) {
|
|
574
|
+
const raw = screen.errorRaw;
|
|
575
|
+
lines.push(` ${dim(`${raw.method} ${raw.url}`)}`);
|
|
576
|
+
lines.push(` ${red(`HTTP ${raw.status}`)}`);
|
|
577
|
+
if (raw.body) {
|
|
578
|
+
try {
|
|
579
|
+
const parsed = JSON.parse(raw.body);
|
|
580
|
+
for (const l of JSON.stringify(parsed, null, 2).split("\n")) {
|
|
581
|
+
lines.push(` ${dim(l)}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
lines.push(` ${dim(raw.body)}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
510
589
|
}
|
|
511
590
|
else if (screen.running) {
|
|
512
591
|
lines.push(` ${dim("running…")}`);
|
|
@@ -699,7 +778,12 @@ function renderResults(state, screen) {
|
|
|
699
778
|
lines.push(` ${dim(`${screen.scroll + 1}–${hi} of ${out.total}`)}`);
|
|
700
779
|
}
|
|
701
780
|
const followupHint = screen.cmd.relations ? ` ${cyan("f")} follow-up` : "";
|
|
702
|
-
|
|
781
|
+
const profileListHints = screen.group.name === "profile" && screen.cmd.name === "list"
|
|
782
|
+
? ` ${cyan("u")} use ${cyan("s")} show ${cyan("d")} delete`
|
|
783
|
+
: "";
|
|
784
|
+
if (screen.message)
|
|
785
|
+
lines.push(` ${green(screen.message)}`);
|
|
786
|
+
lines.push(` ${dim("↑↓")} navigate ${cyan("enter")} detail${followupHint}${profileListHints} ${dim("pgup/pgdn")} page${rawToggle}${curlToggle} ${cyan("m")} main menu ${cyan("esc")} back ${cyan("q")} quit`);
|
|
703
787
|
}
|
|
704
788
|
else if (out.type === "item") {
|
|
705
789
|
lines.push(renderHeader([screen.group.name, screen.cmd.name], cols, state.profile));
|
|
@@ -836,7 +920,7 @@ function renderProfile(state, screen) {
|
|
|
836
920
|
const hi = Math.min(screen.scroll + viewH, state.profileInfo.length);
|
|
837
921
|
lines.push(` ${dim(`${screen.scroll + 1}–${hi} of ${state.profileInfo.length}`)}`);
|
|
838
922
|
}
|
|
839
|
-
lines.push(` ${dim("↑↓")} scroll ${cyan("esc
|
|
923
|
+
lines.push(` ${dim("↑↓")} scroll ${cyan("esc")} close ${cyan("q")} quit`);
|
|
840
924
|
return lines;
|
|
841
925
|
}
|
|
842
926
|
const SETTINGS_ROWS = [
|
|
@@ -854,6 +938,24 @@ const SETTINGS_ROWS = [
|
|
|
854
938
|
label: "Show Audit Log",
|
|
855
939
|
description: "View recent commands executed for the active profile",
|
|
856
940
|
},
|
|
941
|
+
{
|
|
942
|
+
kind: "nav",
|
|
943
|
+
id: "active-profile",
|
|
944
|
+
label: "Active Profile",
|
|
945
|
+
description: "View details for the current connection profile",
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
kind: "nav",
|
|
949
|
+
id: "profiles",
|
|
950
|
+
label: "Profiles",
|
|
951
|
+
description: "Manage and switch connection profiles",
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
kind: "nav",
|
|
955
|
+
id: "plugins",
|
|
956
|
+
label: "Plugins",
|
|
957
|
+
description: "Browse and manage CLI plugins",
|
|
958
|
+
},
|
|
857
959
|
];
|
|
858
960
|
function renderSettings(state, screen) {
|
|
859
961
|
const { cols } = termSize();
|
|
@@ -936,7 +1038,7 @@ function renderWorker(state, screen) {
|
|
|
936
1038
|
: screen.status === "stopping"
|
|
937
1039
|
? cyan("◌ STOPPING")
|
|
938
1040
|
: dim("■ STOPPED");
|
|
939
|
-
const lines = [renderHeader(["
|
|
1041
|
+
const lines = [renderHeader(["worker"], cols, state.profile), ""];
|
|
940
1042
|
lines.push(` ${padEnd(dim("type"), 8)} ${cyan(screen.jobType)} ${dim("status")} ${statusStr}`);
|
|
941
1043
|
lines.push(` ${dim("─".repeat(cols - 4))}`);
|
|
942
1044
|
const { activated, completed, failed } = screen.stats;
|
|
@@ -1006,12 +1108,15 @@ function renderJsonEditor(state, screen) {
|
|
|
1006
1108
|
const editKey = isCursor && screen.col === "key" && screen.editing;
|
|
1007
1109
|
const editVal = isCursor && screen.col === "val" && screen.editing;
|
|
1008
1110
|
const keyStr = renderText(entry.key, entry.keyCursor, keyW - (isKnown ? 0 : 0), editKey);
|
|
1009
|
-
// For known enum fields in nav mode, show cycling hint instead of raw value
|
|
1111
|
+
// For known enum fields in nav mode, show cycling hint instead of raw value.
|
|
1112
|
+
// For non-enum known fields, show a type placeholder when value is empty.
|
|
1010
1113
|
const valDisplay = !editVal && hasEnum && entry.val
|
|
1011
1114
|
? `${entry.val} ${dim("↑↓")}`
|
|
1012
1115
|
: !editVal && hasEnum && !entry.val
|
|
1013
1116
|
? dim("<pick ↑↓>")
|
|
1014
|
-
:
|
|
1117
|
+
: !editVal && !hasEnum && spec && !entry.val
|
|
1118
|
+
? dim(`<${spec.type}>`)
|
|
1119
|
+
: renderText(entry.val, entry.valCursor, valW - 4, editVal);
|
|
1015
1120
|
const valStr = valDisplay;
|
|
1016
1121
|
const keyPart = isCursor && screen.col === "key" && !editKey
|
|
1017
1122
|
? cyan(padEnd(keyStr, keyW))
|
|
@@ -1053,7 +1158,9 @@ function renderJsonEditor(state, screen) {
|
|
|
1053
1158
|
: undefined;
|
|
1054
1159
|
const editHint = editSpec?.enum
|
|
1055
1160
|
? `${dim("↑↓")} pick value ${cyan("enter")} confirm ${cyan("esc")} cancel`
|
|
1056
|
-
:
|
|
1161
|
+
: screen.col === "key" && screen.fieldSpecs && screen.fieldSpecs.length > 0
|
|
1162
|
+
? `${dim("↑↓")} pick field ${dim("←→")} cursor ${cyan("tab")} switch col ${cyan("enter")} confirm ${cyan("esc")} cancel`
|
|
1163
|
+
: `${dim("←→")} cursor ${cyan("tab")} switch col ${cyan("enter")} confirm ${cyan("esc")} cancel`;
|
|
1057
1164
|
lines.push(` ${editHint}`);
|
|
1058
1165
|
}
|
|
1059
1166
|
else {
|
|
@@ -1114,7 +1221,6 @@ async function runWorkerLoop(ws, state, variables, jobTimeout, maxJobs) {
|
|
|
1114
1221
|
worker: "casen-worker",
|
|
1115
1222
|
timeout: jobTimeout,
|
|
1116
1223
|
maxJobsToActivate: maxJobs,
|
|
1117
|
-
requestTimeout: 20000,
|
|
1118
1224
|
}));
|
|
1119
1225
|
}
|
|
1120
1226
|
catch (err) {
|
|
@@ -1128,20 +1234,56 @@ async function runWorkerLoop(ws, state, variables, jobTimeout, maxJobs) {
|
|
|
1128
1234
|
for (const job of jobs) {
|
|
1129
1235
|
if (!running)
|
|
1130
1236
|
break;
|
|
1237
|
+
// Some Zeebe-compatible engines return `key` instead of `jobKey`
|
|
1238
|
+
const jobKey = job.jobKey ?? job.key;
|
|
1239
|
+
if (!jobKey) {
|
|
1240
|
+
addWorkerLog(ws, "err", "Activated job has no key — skipping");
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1131
1243
|
ws.stats.activated++;
|
|
1132
|
-
addWorkerLog(ws, "info", `Activated ${
|
|
1244
|
+
addWorkerLog(ws, "info", `Activated ${jobKey} process=${job.processDefinitionId} element=${job.elementId}`);
|
|
1245
|
+
let jobResult = { outcome: "complete", variables };
|
|
1246
|
+
const processJob = ws.cmd._worker?.processJob;
|
|
1247
|
+
if (processJob) {
|
|
1248
|
+
try {
|
|
1249
|
+
jobResult = await processJob({ ...job, jobKey });
|
|
1250
|
+
}
|
|
1251
|
+
catch (err) {
|
|
1252
|
+
addWorkerLog(ws, "err", `Handler error: ${err instanceof Error ? err.message : String(err)} — using defaults`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1133
1255
|
try {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1256
|
+
if (jobResult.outcome === "complete") {
|
|
1257
|
+
await client.job.completeJob(jobKey, { variables: jobResult.variables });
|
|
1258
|
+
ws.stats.completed++;
|
|
1259
|
+
addWorkerLog(ws, "ok", `Completed ${jobKey}`);
|
|
1260
|
+
}
|
|
1261
|
+
else if (jobResult.outcome === "fail") {
|
|
1262
|
+
await client.job.failJob(jobKey, {
|
|
1263
|
+
errorMessage: jobResult.errorMessage,
|
|
1264
|
+
retries: jobResult.retries,
|
|
1265
|
+
retryBackOff: jobResult.retryBackOff,
|
|
1266
|
+
});
|
|
1267
|
+
ws.stats.failed++;
|
|
1268
|
+
addWorkerLog(ws, "err", `Failed ${jobKey}: ${jobResult.errorMessage}`);
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
await client.job.throwJobError(jobKey, {
|
|
1272
|
+
errorCode: jobResult.errorCode,
|
|
1273
|
+
errorMessage: jobResult.errorMessage,
|
|
1274
|
+
variables: jobResult.variables,
|
|
1275
|
+
});
|
|
1276
|
+
ws.stats.failed++;
|
|
1277
|
+
addWorkerLog(ws, "err", `Error ${jobKey} [${jobResult.errorCode}]: ${jobResult.errorMessage ?? ""}`);
|
|
1278
|
+
}
|
|
1137
1279
|
}
|
|
1138
1280
|
catch (err) {
|
|
1139
1281
|
ws.stats.failed++;
|
|
1140
|
-
addWorkerLog(ws, "err", `Failed to
|
|
1282
|
+
addWorkerLog(ws, "err", `Failed to settle ${jobKey}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1141
1283
|
}
|
|
1142
1284
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1285
|
+
// Sleep between polls: brief yield after processing jobs, longer pause when idle
|
|
1286
|
+
await new Promise((r) => setTimeout(r, jobs.length > 0 ? 100 : 2000));
|
|
1145
1287
|
}
|
|
1146
1288
|
ws.status = "stopped";
|
|
1147
1289
|
addWorkerLog(ws, "info", `Stopped. Activated: ${ws.stats.activated} Completed: ${ws.stats.completed} Failed: ${ws.stats.failed}`);
|
|
@@ -1265,6 +1407,7 @@ async function runAskInTui(screen, state) {
|
|
|
1265
1407
|
},
|
|
1266
1408
|
raw: null,
|
|
1267
1409
|
rawView: false,
|
|
1410
|
+
message: "",
|
|
1268
1411
|
curlView: false,
|
|
1269
1412
|
altView: false,
|
|
1270
1413
|
cursor: 0,
|
|
@@ -1403,6 +1546,9 @@ function render(state) {
|
|
|
1403
1546
|
case "json-editor":
|
|
1404
1547
|
lines = renderJsonEditor(state, screen);
|
|
1405
1548
|
break;
|
|
1549
|
+
case "plugins":
|
|
1550
|
+
lines = renderPlugins(state, screen);
|
|
1551
|
+
break;
|
|
1406
1552
|
}
|
|
1407
1553
|
process.stdout.write(`${CLEAR}${lines.join("\n")}\n`);
|
|
1408
1554
|
}
|
|
@@ -1485,6 +1631,25 @@ function handleMainKey(key, screen, state, done) {
|
|
|
1485
1631
|
_timer: null,
|
|
1486
1632
|
});
|
|
1487
1633
|
}
|
|
1634
|
+
else if (group &&
|
|
1635
|
+
group.commands.length === 1 &&
|
|
1636
|
+
group.commands[0] &&
|
|
1637
|
+
"_worker" in group.commands[0]) {
|
|
1638
|
+
// Single-command worker group: skip commands list, go straight to input
|
|
1639
|
+
const cmd = group.commands[0];
|
|
1640
|
+
state.stack.push({
|
|
1641
|
+
kind: "input",
|
|
1642
|
+
group,
|
|
1643
|
+
cmd,
|
|
1644
|
+
fields: buildFields(cmd),
|
|
1645
|
+
cursor: 0,
|
|
1646
|
+
scroll: 0,
|
|
1647
|
+
editing: false,
|
|
1648
|
+
error: "",
|
|
1649
|
+
errorRaw: null,
|
|
1650
|
+
running: false,
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1488
1653
|
else if (group) {
|
|
1489
1654
|
state.stack.push({ kind: "commands", group, cursor: 0, search: "" });
|
|
1490
1655
|
}
|
|
@@ -1549,6 +1714,7 @@ function handleCommandsKey(key, screen, state, done) {
|
|
|
1549
1714
|
scroll: 0,
|
|
1550
1715
|
editing: false,
|
|
1551
1716
|
error: "",
|
|
1717
|
+
errorRaw: null,
|
|
1552
1718
|
running: false,
|
|
1553
1719
|
});
|
|
1554
1720
|
}
|
|
@@ -1578,6 +1744,7 @@ async function executeCommand(screen, state) {
|
|
|
1578
1744
|
}
|
|
1579
1745
|
screen.running = true;
|
|
1580
1746
|
screen.error = "";
|
|
1747
|
+
screen.errorRaw = null;
|
|
1581
1748
|
render(state);
|
|
1582
1749
|
const { writer, get } = makeCapturingWriter();
|
|
1583
1750
|
// Wrap client factories to capture the last raw HTTP response
|
|
@@ -1636,6 +1803,7 @@ async function executeCommand(screen, state) {
|
|
|
1636
1803
|
altView: false,
|
|
1637
1804
|
cursor: 0,
|
|
1638
1805
|
scroll: 0,
|
|
1806
|
+
message: "",
|
|
1639
1807
|
});
|
|
1640
1808
|
}
|
|
1641
1809
|
catch (err) {
|
|
@@ -1649,6 +1817,7 @@ async function executeCommand(screen, state) {
|
|
|
1649
1817
|
error: msg,
|
|
1650
1818
|
});
|
|
1651
1819
|
screen.error = msg;
|
|
1820
|
+
screen.errorRaw = rawCapture;
|
|
1652
1821
|
}
|
|
1653
1822
|
finally {
|
|
1654
1823
|
screen.running = false;
|
|
@@ -1794,7 +1963,7 @@ async function handleInputKey(key, screen, state, done) {
|
|
|
1794
1963
|
if (!field)
|
|
1795
1964
|
break;
|
|
1796
1965
|
if (field.kind === "run") {
|
|
1797
|
-
if (screen.cmd
|
|
1966
|
+
if ("_worker" in screen.cmd) {
|
|
1798
1967
|
launchWorkerView(screen, state);
|
|
1799
1968
|
}
|
|
1800
1969
|
else {
|
|
@@ -1864,6 +2033,60 @@ async function handleInputKey(key, screen, state, done) {
|
|
|
1864
2033
|
function handleResultsKey(key, screen, state, done) {
|
|
1865
2034
|
const { rows } = termSize();
|
|
1866
2035
|
const viewH = Math.max(3, rows - 10);
|
|
2036
|
+
// Profile list inline shortcuts: u=use, s=show, d=delete
|
|
2037
|
+
if (screen.group.name === "profile" &&
|
|
2038
|
+
screen.cmd.name === "list" &&
|
|
2039
|
+
screen.output.type === "list") {
|
|
2040
|
+
// Clear stale message on any key in this context
|
|
2041
|
+
screen.message = "";
|
|
2042
|
+
if (key === "u" || key === "U") {
|
|
2043
|
+
const item = screen.output.items[screen.cursor];
|
|
2044
|
+
const name = item?.name;
|
|
2045
|
+
if (name && useProfile(name)) {
|
|
2046
|
+
state.profile = name;
|
|
2047
|
+
state.profileInfo = buildProfileInfoEntries(name);
|
|
2048
|
+
const items = rebuildProfileListItems();
|
|
2049
|
+
screen.output = { type: "list", items, columns: screen.output.columns, total: items.length };
|
|
2050
|
+
screen.message = `✓ Active profile: ${name}`;
|
|
2051
|
+
}
|
|
2052
|
+
else if (name) {
|
|
2053
|
+
screen.message = `Profile "${name}" not found`;
|
|
2054
|
+
}
|
|
2055
|
+
render(state);
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (key === "s" || key === "S") {
|
|
2059
|
+
const item = screen.output.items[screen.cursor];
|
|
2060
|
+
if (item) {
|
|
2061
|
+
state.stack.push({
|
|
2062
|
+
kind: "detail",
|
|
2063
|
+
group: screen.group,
|
|
2064
|
+
cmd: screen.cmd,
|
|
2065
|
+
item,
|
|
2066
|
+
label: String(item.name ?? "profile"),
|
|
2067
|
+
cursor: 0,
|
|
2068
|
+
scroll: 0,
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
render(state);
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
if (key === "d" || key === "D") {
|
|
2075
|
+
const item = screen.output.items[screen.cursor];
|
|
2076
|
+
const name = item?.name;
|
|
2077
|
+
if (name && deleteProfile(name)) {
|
|
2078
|
+
const items = rebuildProfileListItems();
|
|
2079
|
+
screen.output = { type: "list", items, columns: screen.output.columns, total: items.length };
|
|
2080
|
+
screen.cursor = Math.min(screen.cursor, Math.max(0, items.length - 1));
|
|
2081
|
+
screen.message = `✓ Deleted profile "${name}"`;
|
|
2082
|
+
}
|
|
2083
|
+
else if (name) {
|
|
2084
|
+
screen.message = `Cannot delete "${name}" (modeler profiles are read-only)`;
|
|
2085
|
+
}
|
|
2086
|
+
render(state);
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
1867
2090
|
// r/R toggles raw view; u/U toggles curl view — mutually exclusive
|
|
1868
2091
|
if (key === "r" || key === "R") {
|
|
1869
2092
|
screen.rawView = !screen.rawView;
|
|
@@ -2304,6 +2527,7 @@ function handleFollowupKey(key, screen, state, done) {
|
|
|
2304
2527
|
scroll: 0,
|
|
2305
2528
|
editing: false,
|
|
2306
2529
|
error: "",
|
|
2530
|
+
errorRaw: null,
|
|
2307
2531
|
running: false,
|
|
2308
2532
|
});
|
|
2309
2533
|
}
|
|
@@ -2354,6 +2578,29 @@ function handleJsonEditorKey(key, screen, state, done) {
|
|
|
2354
2578
|
entry.valCursor = cur;
|
|
2355
2579
|
}
|
|
2356
2580
|
};
|
|
2581
|
+
// Key cycling for key column when fieldSpecs are available
|
|
2582
|
+
if (activeIsKey && screen.fieldSpecs && screen.fieldSpecs.length > 0) {
|
|
2583
|
+
const available = getAvailableFieldSpecs(screen.cursor, screen.entries, screen.fieldSpecs);
|
|
2584
|
+
if (available.length > 0) {
|
|
2585
|
+
if (key === "\x1b[A" || key === "\x1b[B") {
|
|
2586
|
+
const curIdx = available.findIndex((s) => s.name === entry.key);
|
|
2587
|
+
let nextIdx;
|
|
2588
|
+
if (key === "\x1b[A") {
|
|
2589
|
+
nextIdx = curIdx <= 0 ? available.length - 1 : curIdx - 1;
|
|
2590
|
+
}
|
|
2591
|
+
else {
|
|
2592
|
+
nextIdx = curIdx < 0 || curIdx >= available.length - 1 ? 0 : curIdx + 1;
|
|
2593
|
+
}
|
|
2594
|
+
const newSpec = available[nextIdx];
|
|
2595
|
+
if (newSpec) {
|
|
2596
|
+
entry.key = newSpec.name;
|
|
2597
|
+
entry.keyCursor = entry.key.length;
|
|
2598
|
+
}
|
|
2599
|
+
render(state);
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2357
2604
|
// Enum cycling for value column of a known enum field
|
|
2358
2605
|
const valSpec = !activeIsKey ? getFieldSpec(entry.key, screen.fieldSpecs) : undefined;
|
|
2359
2606
|
const enumVals = valSpec?.enum;
|
|
@@ -2476,8 +2723,17 @@ function handleJsonEditorKey(key, screen, state, done) {
|
|
|
2476
2723
|
case "\r":
|
|
2477
2724
|
case "\n":
|
|
2478
2725
|
if (isAddRow) {
|
|
2479
|
-
// Add a new entry
|
|
2480
|
-
|
|
2726
|
+
// Add a new entry; pre-seed key from first available field spec if available
|
|
2727
|
+
const newEntry = { key: "", keyCursor: 0, val: "", valCursor: 0 };
|
|
2728
|
+
if (screen.fieldSpecs && screen.fieldSpecs.length > 0) {
|
|
2729
|
+
const available = getAvailableFieldSpecs(null, screen.entries, screen.fieldSpecs);
|
|
2730
|
+
const firstSpec = available[0];
|
|
2731
|
+
if (firstSpec) {
|
|
2732
|
+
newEntry.key = firstSpec.name;
|
|
2733
|
+
newEntry.keyCursor = firstSpec.name.length;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
screen.entries.push(newEntry);
|
|
2481
2737
|
screen.cursor = screen.entries.length - 1;
|
|
2482
2738
|
screen.col = "key";
|
|
2483
2739
|
screen.editing = true;
|
|
@@ -2495,18 +2751,23 @@ function handleJsonEditorKey(key, screen, state, done) {
|
|
|
2495
2751
|
}
|
|
2496
2752
|
break;
|
|
2497
2753
|
case "a":
|
|
2498
|
-
case "A":
|
|
2499
|
-
// Insert new entry after cursor
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2754
|
+
case "A": {
|
|
2755
|
+
// Insert new entry after cursor; pre-seed key from first available field spec
|
|
2756
|
+
const addEntry = { key: "", keyCursor: 0, val: "", valCursor: 0 };
|
|
2757
|
+
if (screen.fieldSpecs && screen.fieldSpecs.length > 0) {
|
|
2758
|
+
const available = getAvailableFieldSpecs(null, screen.entries, screen.fieldSpecs);
|
|
2759
|
+
const firstSpec = available[0];
|
|
2760
|
+
if (firstSpec) {
|
|
2761
|
+
addEntry.key = firstSpec.name;
|
|
2762
|
+
addEntry.keyCursor = firstSpec.name.length;
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
screen.entries.splice(screen.cursor + 1, 0, addEntry);
|
|
2506
2766
|
screen.cursor = Math.min(screen.cursor + 1, screen.entries.length - 1);
|
|
2507
2767
|
screen.col = "key";
|
|
2508
2768
|
screen.editing = true;
|
|
2509
2769
|
break;
|
|
2770
|
+
}
|
|
2510
2771
|
case "d":
|
|
2511
2772
|
case "D":
|
|
2512
2773
|
if (!isAddRow && screen.entries.length > 0) {
|
|
@@ -2600,6 +2861,15 @@ function handleSettingsKey(key, screen, state, done) {
|
|
|
2600
2861
|
else if (row?.kind === "action" && row.id === "show-audit-log") {
|
|
2601
2862
|
state.stack.push({ kind: "audit-log", scroll: 0 });
|
|
2602
2863
|
}
|
|
2864
|
+
else if (row?.kind === "nav" && row.id === "active-profile") {
|
|
2865
|
+
state.stack.push({ kind: "profile", scroll: 0 });
|
|
2866
|
+
}
|
|
2867
|
+
else if (row?.kind === "nav" && row.id === "profiles") {
|
|
2868
|
+
state.stack.push({ kind: "commands", group: profileGroup, cursor: 0, search: "" });
|
|
2869
|
+
}
|
|
2870
|
+
else if (row?.kind === "nav" && row.id === "plugins") {
|
|
2871
|
+
state.stack.push(newPluginsScreen());
|
|
2872
|
+
}
|
|
2603
2873
|
break;
|
|
2604
2874
|
}
|
|
2605
2875
|
case "\x1b":
|
|
@@ -2693,6 +2963,499 @@ function handleWorkerKey(key, screen, state, done) {
|
|
|
2693
2963
|
}
|
|
2694
2964
|
render(state);
|
|
2695
2965
|
}
|
|
2966
|
+
// ─── Plugins screen ───────────────────────────────────────────────────────────
|
|
2967
|
+
const PLUGIN_MENU = [
|
|
2968
|
+
{ id: "search", label: "search", description: "Search the npm registry for plugins" },
|
|
2969
|
+
{ id: "list", label: "list", description: "Show installed plugins" },
|
|
2970
|
+
{ id: "install", label: "install", description: "Install a plugin from npm or local path" },
|
|
2971
|
+
{
|
|
2972
|
+
id: "update",
|
|
2973
|
+
label: "update",
|
|
2974
|
+
description: "Update installed plugins to their latest versions",
|
|
2975
|
+
},
|
|
2976
|
+
{ id: "remove", label: "remove", description: "Remove a plugin" },
|
|
2977
|
+
];
|
|
2978
|
+
function newPluginsScreen() {
|
|
2979
|
+
return {
|
|
2980
|
+
kind: "plugins",
|
|
2981
|
+
subview: "menu",
|
|
2982
|
+
menuCursor: 0,
|
|
2983
|
+
query: "",
|
|
2984
|
+
queryCursor: 0,
|
|
2985
|
+
resultKind: "search",
|
|
2986
|
+
results: [],
|
|
2987
|
+
resultCursor: 0,
|
|
2988
|
+
resultScroll: 0,
|
|
2989
|
+
promptLabel: "",
|
|
2990
|
+
promptValue: "",
|
|
2991
|
+
promptCursor: 0,
|
|
2992
|
+
promptAction: "install",
|
|
2993
|
+
message: "",
|
|
2994
|
+
error: "",
|
|
2995
|
+
running: false,
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
function renderPlugins(state, screen) {
|
|
2999
|
+
const { cols, rows } = termSize();
|
|
3000
|
+
if (screen.subview === "menu") {
|
|
3001
|
+
const nameW = PLUGIN_MENU.reduce((m, item) => Math.max(m, item.label.length), 0) + 2;
|
|
3002
|
+
const lines = [renderHeader(["settings", "plugins"], cols, state.profile), ""];
|
|
3003
|
+
for (let i = 0; i < PLUGIN_MENU.length; i++) {
|
|
3004
|
+
const item = PLUGIN_MENU[i];
|
|
3005
|
+
if (!item)
|
|
3006
|
+
continue;
|
|
3007
|
+
const isCursor = i === screen.menuCursor;
|
|
3008
|
+
const name = padEnd(isCursor ? cyan(item.label) : item.label, nameW);
|
|
3009
|
+
const desc = dim(fit(item.description, cols - nameW - 6));
|
|
3010
|
+
const line = ` ${name} ${desc}`;
|
|
3011
|
+
lines.push(isCursor ? inv(line.padEnd(cols - 1)) : line);
|
|
3012
|
+
}
|
|
3013
|
+
lines.push("");
|
|
3014
|
+
lines.push(` ${dim("↑↓")} navigate ${cyan("enter")} open ${cyan("esc")} back ${cyan("q")} quit`);
|
|
3015
|
+
return lines;
|
|
3016
|
+
}
|
|
3017
|
+
if (screen.subview === "search") {
|
|
3018
|
+
const lines = [renderHeader(["settings", "plugins", "search"], cols, state.profile), ""];
|
|
3019
|
+
lines.push(` ${dim("Search the npm registry — leave empty to browse all plugins")}`);
|
|
3020
|
+
lines.push("");
|
|
3021
|
+
lines.push(` > ${renderText(screen.query, screen.queryCursor, cols - 6, true)}`);
|
|
3022
|
+
if (screen.running) {
|
|
3023
|
+
lines.push("");
|
|
3024
|
+
lines.push(` ${dim("Searching…")}`);
|
|
3025
|
+
}
|
|
3026
|
+
else if (screen.error) {
|
|
3027
|
+
lines.push("");
|
|
3028
|
+
lines.push(` ${red("error:")} ${screen.error}`);
|
|
3029
|
+
}
|
|
3030
|
+
lines.push("");
|
|
3031
|
+
lines.push(` ${dim("type query")} ${cyan("enter")} search ${cyan("esc")} back ${cyan("^C")} quit`);
|
|
3032
|
+
return lines;
|
|
3033
|
+
}
|
|
3034
|
+
if (screen.subview === "results") {
|
|
3035
|
+
const viewH = Math.max(3, rows - 10);
|
|
3036
|
+
const isSearch = screen.resultKind === "search";
|
|
3037
|
+
const title = isSearch ? "search results" : "installed";
|
|
3038
|
+
const lines = [renderHeader(["settings", "plugins", title], cols, state.profile), ""];
|
|
3039
|
+
if (screen.results.length === 0) {
|
|
3040
|
+
lines.push(` ${dim(isSearch ? "No plugins found." : "No plugins installed.")}`);
|
|
3041
|
+
}
|
|
3042
|
+
else {
|
|
3043
|
+
const nameW = Math.min(32, screen.results.reduce((m, r) => Math.max(m, r.name.length), 0)) + 2;
|
|
3044
|
+
// Official badge shown for @bpmnkit/* packages (2 visible chars: "◆ ")
|
|
3045
|
+
const BADGE = `${cyan("◆")} `;
|
|
3046
|
+
const BADGE_W = 2;
|
|
3047
|
+
const visible = screen.results.slice(screen.resultScroll, screen.resultScroll + viewH);
|
|
3048
|
+
for (let vi = 0; vi < visible.length; vi++) {
|
|
3049
|
+
const r = visible[vi];
|
|
3050
|
+
if (!r)
|
|
3051
|
+
continue;
|
|
3052
|
+
const ri = screen.resultScroll + vi;
|
|
3053
|
+
const isCursor = ri === screen.resultCursor;
|
|
3054
|
+
const isOfficial = r.name.startsWith("@bpmnkit/");
|
|
3055
|
+
const badge = isOfficial ? BADGE : " ".repeat(BADGE_W);
|
|
3056
|
+
const name = padEnd(isCursor ? cyan(r.name) : r.name, nameW);
|
|
3057
|
+
const verStr = r.version.padEnd(10);
|
|
3058
|
+
const extra = isSearch
|
|
3059
|
+
? dim(fit(r.description, cols - nameW - BADGE_W - 14))
|
|
3060
|
+
: dim(r.installedAt.slice(0, 10));
|
|
3061
|
+
const line = ` ${badge}${name} ${verStr} ${extra}`;
|
|
3062
|
+
lines.push(isCursor ? inv(line.padEnd(cols - 1)) : line);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
if (screen.message) {
|
|
3066
|
+
lines.push("");
|
|
3067
|
+
lines.push(` ${screen.message}`);
|
|
3068
|
+
}
|
|
3069
|
+
lines.push("");
|
|
3070
|
+
const actions = isSearch
|
|
3071
|
+
? `${cyan("enter/i")} install ${cyan("esc")} back ${cyan("q")} quit`
|
|
3072
|
+
: `${cyan("enter/r")} remove ${cyan("esc")} back ${cyan("q")} quit`;
|
|
3073
|
+
lines.push(` ${dim("↑↓")} navigate ${actions}`);
|
|
3074
|
+
lines.push(` ${cyan("◆")} ${dim("= official @bpmnkit plugin")}`);
|
|
3075
|
+
return lines;
|
|
3076
|
+
}
|
|
3077
|
+
if (screen.subview === "prompt") {
|
|
3078
|
+
const lines = [
|
|
3079
|
+
renderHeader(["settings", "plugins", screen.promptAction], cols, state.profile),
|
|
3080
|
+
"",
|
|
3081
|
+
];
|
|
3082
|
+
lines.push(` ${screen.promptLabel}`);
|
|
3083
|
+
lines.push("");
|
|
3084
|
+
lines.push(` > ${renderText(screen.promptValue, screen.promptCursor, cols - 6, true)}`);
|
|
3085
|
+
if (screen.running) {
|
|
3086
|
+
lines.push("");
|
|
3087
|
+
lines.push(` ${dim("Running…")}`);
|
|
3088
|
+
}
|
|
3089
|
+
else if (screen.error) {
|
|
3090
|
+
lines.push("");
|
|
3091
|
+
lines.push(` ${red("error:")} ${screen.error}`);
|
|
3092
|
+
}
|
|
3093
|
+
lines.push("");
|
|
3094
|
+
lines.push(` ${dim("type name")} ${cyan("enter")} confirm ${cyan("esc")} back ${cyan("^C")} quit`);
|
|
3095
|
+
return lines;
|
|
3096
|
+
}
|
|
3097
|
+
// "done"
|
|
3098
|
+
const lines = [renderHeader(["settings", "plugins"], cols, state.profile), "", ""];
|
|
3099
|
+
if (screen.error) {
|
|
3100
|
+
lines.push(` ${red("error:")} ${screen.error}`);
|
|
3101
|
+
}
|
|
3102
|
+
else {
|
|
3103
|
+
for (const line of screen.message.split("\n")) {
|
|
3104
|
+
lines.push(` ${line}`);
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
lines.push("");
|
|
3108
|
+
lines.push(` ${cyan("esc")} back ${cyan("q")} quit`);
|
|
3109
|
+
return lines;
|
|
3110
|
+
}
|
|
3111
|
+
/** Temporarily restore the normal terminal, run fn (which may spawn processes), then re-enter. */
|
|
3112
|
+
async function runWithTerminal(fn) {
|
|
3113
|
+
process.stdout.write(`${ALT_OFF}${SHOW}`);
|
|
3114
|
+
if (process.stdin.isTTY)
|
|
3115
|
+
process.stdin.setRawMode(false);
|
|
3116
|
+
try {
|
|
3117
|
+
await fn();
|
|
3118
|
+
}
|
|
3119
|
+
finally {
|
|
3120
|
+
if (process.stdin.isTTY)
|
|
3121
|
+
process.stdin.setRawMode(true);
|
|
3122
|
+
process.stdout.write(`${ALT_ON}${HIDE}${CLEAR}`);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
async function handlePluginsKey(key, screen, state, done) {
|
|
3126
|
+
if (key === "\x03") {
|
|
3127
|
+
done();
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
// ── Menu subview ──────────────────────────────────────────────────────────
|
|
3131
|
+
if (screen.subview === "menu") {
|
|
3132
|
+
switch (key) {
|
|
3133
|
+
case "\x1b[A":
|
|
3134
|
+
if (screen.menuCursor > 0)
|
|
3135
|
+
screen.menuCursor--;
|
|
3136
|
+
break;
|
|
3137
|
+
case "\x1b[B":
|
|
3138
|
+
if (screen.menuCursor < PLUGIN_MENU.length - 1)
|
|
3139
|
+
screen.menuCursor++;
|
|
3140
|
+
break;
|
|
3141
|
+
case "\r":
|
|
3142
|
+
case "\n": {
|
|
3143
|
+
const item = PLUGIN_MENU[screen.menuCursor];
|
|
3144
|
+
if (item?.id === "search") {
|
|
3145
|
+
screen.subview = "search";
|
|
3146
|
+
screen.query = "";
|
|
3147
|
+
screen.queryCursor = 0;
|
|
3148
|
+
screen.error = "";
|
|
3149
|
+
}
|
|
3150
|
+
else if (item?.id === "list") {
|
|
3151
|
+
screen.running = true;
|
|
3152
|
+
screen.error = "";
|
|
3153
|
+
render(state);
|
|
3154
|
+
try {
|
|
3155
|
+
const plugins = await readInstalledPlugins();
|
|
3156
|
+
screen.resultKind = "installed";
|
|
3157
|
+
screen.results = plugins.map((p) => ({
|
|
3158
|
+
name: p.package,
|
|
3159
|
+
version: p.version,
|
|
3160
|
+
description: "",
|
|
3161
|
+
publisher: "",
|
|
3162
|
+
score: "",
|
|
3163
|
+
installedAt: p.installedAt,
|
|
3164
|
+
}));
|
|
3165
|
+
screen.resultCursor = 0;
|
|
3166
|
+
screen.resultScroll = 0;
|
|
3167
|
+
screen.message = "";
|
|
3168
|
+
screen.subview = "results";
|
|
3169
|
+
}
|
|
3170
|
+
catch (err) {
|
|
3171
|
+
screen.error = err instanceof Error ? err.message : String(err);
|
|
3172
|
+
}
|
|
3173
|
+
finally {
|
|
3174
|
+
screen.running = false;
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
else if (item?.id === "install") {
|
|
3178
|
+
screen.subview = "prompt";
|
|
3179
|
+
screen.promptLabel = "Package name (e.g. casen-deploy or ./local-path):";
|
|
3180
|
+
screen.promptValue = "";
|
|
3181
|
+
screen.promptCursor = 0;
|
|
3182
|
+
screen.promptAction = "install";
|
|
3183
|
+
screen.error = "";
|
|
3184
|
+
}
|
|
3185
|
+
else if (item?.id === "update") {
|
|
3186
|
+
await runWithTerminal(async () => {
|
|
3187
|
+
const cmd = pluginGroup.commands.find((c) => c.name === "update");
|
|
3188
|
+
if (!cmd)
|
|
3189
|
+
return;
|
|
3190
|
+
const { writer } = makeCapturingWriter();
|
|
3191
|
+
await cmd.run({
|
|
3192
|
+
positional: [],
|
|
3193
|
+
flags: {},
|
|
3194
|
+
output: writer,
|
|
3195
|
+
getClient: state.getClient,
|
|
3196
|
+
getAdminClient: state.getAdminClient,
|
|
3197
|
+
});
|
|
3198
|
+
});
|
|
3199
|
+
screen.subview = "done";
|
|
3200
|
+
screen.message = green("✓ Update complete. Restart casen to activate changes.");
|
|
3201
|
+
screen.error = "";
|
|
3202
|
+
}
|
|
3203
|
+
else if (item?.id === "remove") {
|
|
3204
|
+
screen.subview = "prompt";
|
|
3205
|
+
screen.promptLabel = "Plugin package name to remove:";
|
|
3206
|
+
screen.promptValue = "";
|
|
3207
|
+
screen.promptCursor = 0;
|
|
3208
|
+
screen.promptAction = "remove";
|
|
3209
|
+
screen.error = "";
|
|
3210
|
+
}
|
|
3211
|
+
break;
|
|
3212
|
+
}
|
|
3213
|
+
case "\x1b":
|
|
3214
|
+
state.stack.pop();
|
|
3215
|
+
break;
|
|
3216
|
+
case "q":
|
|
3217
|
+
case "Q":
|
|
3218
|
+
done();
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
render(state);
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3224
|
+
// ── Search subview ────────────────────────────────────────────────────────
|
|
3225
|
+
if (screen.subview === "search") {
|
|
3226
|
+
if (key === "\x1b") {
|
|
3227
|
+
screen.subview = "menu";
|
|
3228
|
+
render(state);
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
if (key === "\r" || key === "\n") {
|
|
3232
|
+
screen.running = true;
|
|
3233
|
+
screen.error = "";
|
|
3234
|
+
render(state);
|
|
3235
|
+
try {
|
|
3236
|
+
const raw = await searchNpmRegistry(screen.query);
|
|
3237
|
+
screen.results = raw.map((r) => ({
|
|
3238
|
+
name: r.package.name,
|
|
3239
|
+
version: r.package.version,
|
|
3240
|
+
description: r.package.description ?? "",
|
|
3241
|
+
publisher: r.package.publisher?.username ?? "",
|
|
3242
|
+
score: r.score.final.toFixed(2),
|
|
3243
|
+
installedAt: "",
|
|
3244
|
+
}));
|
|
3245
|
+
screen.resultKind = "search";
|
|
3246
|
+
screen.resultCursor = 0;
|
|
3247
|
+
screen.resultScroll = 0;
|
|
3248
|
+
screen.message = "";
|
|
3249
|
+
screen.subview = "results";
|
|
3250
|
+
}
|
|
3251
|
+
catch (err) {
|
|
3252
|
+
screen.error = err instanceof Error ? err.message : String(err);
|
|
3253
|
+
}
|
|
3254
|
+
finally {
|
|
3255
|
+
screen.running = false;
|
|
3256
|
+
}
|
|
3257
|
+
render(state);
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
if (key === "\x7f" || key === "\x08") {
|
|
3261
|
+
if (screen.queryCursor > 0) {
|
|
3262
|
+
screen.query =
|
|
3263
|
+
screen.query.slice(0, screen.queryCursor - 1) + screen.query.slice(screen.queryCursor);
|
|
3264
|
+
screen.queryCursor--;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
else if (key === "\x1b[D") {
|
|
3268
|
+
if (screen.queryCursor > 0)
|
|
3269
|
+
screen.queryCursor--;
|
|
3270
|
+
}
|
|
3271
|
+
else if (key === "\x1b[C") {
|
|
3272
|
+
if (screen.queryCursor < screen.query.length)
|
|
3273
|
+
screen.queryCursor++;
|
|
3274
|
+
}
|
|
3275
|
+
else if (key === "\x01" || key === "\x1b[H") {
|
|
3276
|
+
screen.queryCursor = 0;
|
|
3277
|
+
}
|
|
3278
|
+
else if (key === "\x05" || key === "\x1b[F") {
|
|
3279
|
+
screen.queryCursor = screen.query.length;
|
|
3280
|
+
}
|
|
3281
|
+
else if (key.length === 1 && key >= " ") {
|
|
3282
|
+
screen.query =
|
|
3283
|
+
screen.query.slice(0, screen.queryCursor) + key + screen.query.slice(screen.queryCursor);
|
|
3284
|
+
screen.queryCursor++;
|
|
3285
|
+
}
|
|
3286
|
+
render(state);
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
// ── Results subview ───────────────────────────────────────────────────────
|
|
3290
|
+
if (screen.subview === "results") {
|
|
3291
|
+
const { rows } = termSize();
|
|
3292
|
+
const viewH = Math.max(3, rows - 10);
|
|
3293
|
+
switch (key) {
|
|
3294
|
+
case "\x1b[A":
|
|
3295
|
+
if (screen.resultCursor > 0) {
|
|
3296
|
+
screen.resultCursor--;
|
|
3297
|
+
if (screen.resultCursor < screen.resultScroll)
|
|
3298
|
+
screen.resultScroll--;
|
|
3299
|
+
}
|
|
3300
|
+
break;
|
|
3301
|
+
case "\x1b[B":
|
|
3302
|
+
if (screen.resultCursor < screen.results.length - 1) {
|
|
3303
|
+
screen.resultCursor++;
|
|
3304
|
+
if (screen.resultCursor >= screen.resultScroll + viewH)
|
|
3305
|
+
screen.resultScroll++;
|
|
3306
|
+
}
|
|
3307
|
+
break;
|
|
3308
|
+
case "\r":
|
|
3309
|
+
case "\n":
|
|
3310
|
+
case "i":
|
|
3311
|
+
case "I":
|
|
3312
|
+
case "r":
|
|
3313
|
+
case "R": {
|
|
3314
|
+
const result = screen.results[screen.resultCursor];
|
|
3315
|
+
if (!result)
|
|
3316
|
+
break;
|
|
3317
|
+
if (screen.resultKind === "installed" && (key === "i" || key === "I"))
|
|
3318
|
+
break;
|
|
3319
|
+
const action = screen.resultKind === "installed" ? "remove" : "install";
|
|
3320
|
+
const pkgName = result.name;
|
|
3321
|
+
screen.message = dim(`${action === "install" ? "Installing" : "Removing"} ${pkgName}…`);
|
|
3322
|
+
render(state);
|
|
3323
|
+
const cmd = pluginGroup.commands.find((c) => c.name === action);
|
|
3324
|
+
if (!cmd)
|
|
3325
|
+
break;
|
|
3326
|
+
let opError = "";
|
|
3327
|
+
let opMessage = "";
|
|
3328
|
+
await runWithTerminal(async () => {
|
|
3329
|
+
const { writer, get } = makeCapturingWriter();
|
|
3330
|
+
try {
|
|
3331
|
+
await cmd.run({
|
|
3332
|
+
positional: [pkgName],
|
|
3333
|
+
flags: {},
|
|
3334
|
+
output: writer,
|
|
3335
|
+
getClient: state.getClient,
|
|
3336
|
+
getAdminClient: state.getAdminClient,
|
|
3337
|
+
});
|
|
3338
|
+
const out = get();
|
|
3339
|
+
if (out.type === "messages")
|
|
3340
|
+
opMessage = out.lines.join("\n");
|
|
3341
|
+
else
|
|
3342
|
+
opMessage = green(`✓ ${action === "install" ? "Installed" : "Removed"} ${pkgName}`);
|
|
3343
|
+
}
|
|
3344
|
+
catch (err) {
|
|
3345
|
+
opError = err instanceof Error ? err.message : String(err);
|
|
3346
|
+
}
|
|
3347
|
+
});
|
|
3348
|
+
screen.subview = "done";
|
|
3349
|
+
screen.message = opMessage;
|
|
3350
|
+
screen.error = opError;
|
|
3351
|
+
break;
|
|
3352
|
+
}
|
|
3353
|
+
case "\x1b":
|
|
3354
|
+
// Go back to search input if came from search, else back to menu
|
|
3355
|
+
if (screen.resultKind === "search") {
|
|
3356
|
+
screen.subview = "search";
|
|
3357
|
+
}
|
|
3358
|
+
else {
|
|
3359
|
+
screen.subview = "menu";
|
|
3360
|
+
}
|
|
3361
|
+
break;
|
|
3362
|
+
case "q":
|
|
3363
|
+
case "Q":
|
|
3364
|
+
done();
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
render(state);
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
// ── Prompt subview ────────────────────────────────────────────────────────
|
|
3371
|
+
if (screen.subview === "prompt") {
|
|
3372
|
+
if (key === "\x1b") {
|
|
3373
|
+
screen.subview = "menu";
|
|
3374
|
+
render(state);
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
if (key === "\r" || key === "\n") {
|
|
3378
|
+
const pkgName = screen.promptValue.trim();
|
|
3379
|
+
if (!pkgName) {
|
|
3380
|
+
screen.error = "Package name is required";
|
|
3381
|
+
render(state);
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
const cmd = pluginGroup.commands.find((c) => c.name === screen.promptAction);
|
|
3385
|
+
if (!cmd)
|
|
3386
|
+
return;
|
|
3387
|
+
let opError = "";
|
|
3388
|
+
let opMessage = "";
|
|
3389
|
+
await runWithTerminal(async () => {
|
|
3390
|
+
const { writer, get } = makeCapturingWriter();
|
|
3391
|
+
try {
|
|
3392
|
+
await cmd.run({
|
|
3393
|
+
positional: [pkgName],
|
|
3394
|
+
flags: {},
|
|
3395
|
+
output: writer,
|
|
3396
|
+
getClient: state.getClient,
|
|
3397
|
+
getAdminClient: state.getAdminClient,
|
|
3398
|
+
});
|
|
3399
|
+
const out = get();
|
|
3400
|
+
if (out.type === "messages")
|
|
3401
|
+
opMessage = out.lines.join("\n");
|
|
3402
|
+
else
|
|
3403
|
+
opMessage = green("✓ Done");
|
|
3404
|
+
}
|
|
3405
|
+
catch (err) {
|
|
3406
|
+
opError = err instanceof Error ? err.message : String(err);
|
|
3407
|
+
}
|
|
3408
|
+
});
|
|
3409
|
+
screen.subview = "done";
|
|
3410
|
+
screen.message = opMessage;
|
|
3411
|
+
screen.error = opError;
|
|
3412
|
+
render(state);
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
if (key === "\x7f" || key === "\x08") {
|
|
3416
|
+
if (screen.promptCursor > 0) {
|
|
3417
|
+
screen.promptValue =
|
|
3418
|
+
screen.promptValue.slice(0, screen.promptCursor - 1) +
|
|
3419
|
+
screen.promptValue.slice(screen.promptCursor);
|
|
3420
|
+
screen.promptCursor--;
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
else if (key === "\x1b[D") {
|
|
3424
|
+
if (screen.promptCursor > 0)
|
|
3425
|
+
screen.promptCursor--;
|
|
3426
|
+
}
|
|
3427
|
+
else if (key === "\x1b[C") {
|
|
3428
|
+
if (screen.promptCursor < screen.promptValue.length)
|
|
3429
|
+
screen.promptCursor++;
|
|
3430
|
+
}
|
|
3431
|
+
else if (key === "\x01" || key === "\x1b[H") {
|
|
3432
|
+
screen.promptCursor = 0;
|
|
3433
|
+
}
|
|
3434
|
+
else if (key === "\x05" || key === "\x1b[F") {
|
|
3435
|
+
screen.promptCursor = screen.promptValue.length;
|
|
3436
|
+
}
|
|
3437
|
+
else if (key.length === 1 && key >= " ") {
|
|
3438
|
+
screen.promptValue =
|
|
3439
|
+
screen.promptValue.slice(0, screen.promptCursor) +
|
|
3440
|
+
key +
|
|
3441
|
+
screen.promptValue.slice(screen.promptCursor);
|
|
3442
|
+
screen.promptCursor++;
|
|
3443
|
+
}
|
|
3444
|
+
render(state);
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
// ── Done subview ──────────────────────────────────────────────────────────
|
|
3448
|
+
switch (key) {
|
|
3449
|
+
case "\x1b":
|
|
3450
|
+
screen.subview = "menu";
|
|
3451
|
+
break;
|
|
3452
|
+
case "q":
|
|
3453
|
+
case "Q":
|
|
3454
|
+
done();
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
render(state);
|
|
3458
|
+
}
|
|
2696
3459
|
async function handleKey(key, state, done) {
|
|
2697
3460
|
if (state.quitting)
|
|
2698
3461
|
return;
|
|
@@ -2703,18 +3466,6 @@ async function handleKey(key, state, done) {
|
|
|
2703
3466
|
const screen = state.stack[state.stack.length - 1];
|
|
2704
3467
|
if (!screen)
|
|
2705
3468
|
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
3469
|
switch (screen.kind) {
|
|
2719
3470
|
case "main":
|
|
2720
3471
|
handleMainKey(key, screen, state, done);
|
|
@@ -2752,6 +3503,9 @@ async function handleKey(key, state, done) {
|
|
|
2752
3503
|
case "json-editor":
|
|
2753
3504
|
handleJsonEditorKey(key, screen, state, done);
|
|
2754
3505
|
break;
|
|
3506
|
+
case "plugins":
|
|
3507
|
+
await handlePluginsKey(key, screen, state, done);
|
|
3508
|
+
break;
|
|
2755
3509
|
}
|
|
2756
3510
|
}
|
|
2757
3511
|
// ─── Shared TUI runner ────────────────────────────────────────────────────────
|