@nmakarov/cli-toolkit 0.66.0 → 0.68.0

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.
@@ -493,6 +493,59 @@ var init_list_components = __esm({
493
493
  }
494
494
  });
495
495
 
496
+ // src/screen/key-bindings.js
497
+ function bindingIdentity(b) {
498
+ return [
499
+ String(b?.key ?? ""),
500
+ b?.meta ? "m" : "",
501
+ b?.ctrl ? "c" : "",
502
+ b?.shift ? "s" : ""
503
+ ].join("|");
504
+ }
505
+ function bindingMatchesInput(binding, input, key) {
506
+ if (!binding?.key) return false;
507
+ let keyMatches = false;
508
+ if (key?.[binding.key]) {
509
+ keyMatches = true;
510
+ } else if (input === binding.key) {
511
+ keyMatches = true;
512
+ } else if (key?.name === binding.key) {
513
+ keyMatches = true;
514
+ }
515
+ if (!keyMatches) return false;
516
+ const modOk = (flag, pressed) => {
517
+ if (flag === true) return !!pressed;
518
+ if (flag === false) return !pressed;
519
+ return !pressed;
520
+ };
521
+ if (!modOk(binding.meta, key?.meta)) return false;
522
+ if (!modOk(binding.ctrl, key?.ctrl)) return false;
523
+ if (binding.shift !== void 0 && !!key?.shift !== !!binding.shift) return false;
524
+ return true;
525
+ }
526
+ function formatBindingKey(binding) {
527
+ let label = KEY_LABELS[binding.key] || binding.key;
528
+ if (binding.shift) label = `\u21E7${label}`;
529
+ if (binding.ctrl) label = `^${label}`;
530
+ if (binding.meta) label = `\u2325${label}`;
531
+ return label;
532
+ }
533
+ var KEY_LABELS;
534
+ var init_key_bindings = __esm({
535
+ "src/screen/key-bindings.js"() {
536
+ KEY_LABELS = {
537
+ escape: "esc",
538
+ leftArrow: "\u2190",
539
+ rightArrow: "\u2192",
540
+ upArrow: "\u2191",
541
+ downArrow: "\u2193",
542
+ return: "enter",
543
+ pageUp: "PgUp",
544
+ pageDown: "PgDn"
545
+ };
546
+ }
547
+ });
548
+
496
549
  // src/screen/screens.js
497
550
  function groupKeyBindings(bindings) {
498
551
  const groups = {};
@@ -506,7 +559,7 @@ function groupKeyBindings(bindings) {
506
559
  order: binding.order || 999
507
560
  };
508
561
  }
509
- groups[caption].keys.push(binding.key);
562
+ groups[caption].keys.push(formatBindingKey(binding));
510
563
  });
511
564
  return Object.values(groups);
512
565
  }
@@ -546,12 +599,14 @@ function formatKeyBindings(bindings, mode = "long") {
546
599
  }
547
600
  function formatKeys(keys) {
548
601
  const keyMap = {
549
- "escape": "esc",
550
- "leftArrow": "\u2190",
551
- "rightArrow": "\u2192",
552
- "upArrow": "\u2191",
553
- "downArrow": "\u2193",
554
- "return": "enter"
602
+ escape: "esc",
603
+ leftArrow: "\u2190",
604
+ rightArrow: "\u2192",
605
+ upArrow: "\u2191",
606
+ downArrow: "\u2193",
607
+ return: "enter",
608
+ pageUp: "PgUp",
609
+ pageDown: "PgDn"
555
610
  };
556
611
  return keys.map((k) => keyMap[k] || k).join("/");
557
612
  }
@@ -591,7 +646,10 @@ async function showScreen(config2) {
591
646
  setKeyBinding: (bindingOrBindings) => {
592
647
  const bindingsToSet = Array.isArray(bindingOrBindings) ? bindingOrBindings : [bindingOrBindings];
593
648
  bindingsToSet.forEach((binding) => {
594
- const existingIndex = keyBindings.findIndex((b) => b.key === binding.key);
649
+ const id = bindingIdentity(binding);
650
+ const existingIndex = keyBindings.findIndex(
651
+ (b) => bindingIdentity(b) === id
652
+ );
595
653
  if (existingIndex >= 0) {
596
654
  const existing = keyBindings[existingIndex];
597
655
  if (existing.protected) {
@@ -615,7 +673,10 @@ async function showScreen(config2) {
615
673
  });
616
674
  },
617
675
  updateKeyBinding: (keyName, updates) => {
618
- const index = keyBindings.findIndex((b) => b.key === keyName);
676
+ const id = updates && (updates.meta != null || updates.ctrl != null || updates.shift != null) ? bindingIdentity({ key: keyName, ...updates }) : null;
677
+ const index = keyBindings.findIndex(
678
+ (b) => id ? bindingIdentity(b) === id : b.key === keyName
679
+ );
619
680
  if (index >= 0) {
620
681
  keyBindings[index] = {
621
682
  ...keyBindings[index],
@@ -623,11 +684,13 @@ async function showScreen(config2) {
623
684
  };
624
685
  }
625
686
  },
626
- removeKeyBinding: (keyName) => {
627
- const index = keyBindings.findIndex((b) => b.key === keyName);
687
+ removeKeyBinding: (keyNameOrBinding) => {
688
+ const index = typeof keyNameOrBinding === "object" && keyNameOrBinding ? keyBindings.findIndex(
689
+ (b) => bindingIdentity(b) === bindingIdentity(keyNameOrBinding)
690
+ ) : keyBindings.findIndex((b) => b.key === keyNameOrBinding);
628
691
  if (index >= 0) {
629
692
  if (keyBindings[index].protected) {
630
- console.warn(`Cannot remove protected key: ${keyName}`);
693
+ console.warn(`Cannot remove protected key: ${keyNameOrBinding}`);
631
694
  return;
632
695
  }
633
696
  keyBindings.splice(index, 1);
@@ -668,24 +731,11 @@ async function showScreen(config2) {
668
731
  }
669
732
  let matchedBinding = null;
670
733
  for (const binding of keyBindings) {
671
- let keyMatches = false;
672
- if (key[binding.key]) {
673
- keyMatches = true;
674
- } else if (input === binding.key) {
675
- keyMatches = true;
676
- } else if (key?.name === binding.key) {
677
- keyMatches = true;
678
- }
679
- if (keyMatches) {
680
- if (binding.enabled === false) {
681
- continue;
682
- }
683
- if (binding.condition && !binding.condition(context)) {
684
- continue;
685
- }
686
- matchedBinding = binding;
687
- break;
688
- }
734
+ if (binding.enabled === false) continue;
735
+ if (!bindingMatchesInput(binding, input, key)) continue;
736
+ if (binding.condition && !binding.condition(context)) continue;
737
+ matchedBinding = binding;
738
+ break;
689
739
  }
690
740
  if (matchedBinding && actions[matchedBinding.action]) {
691
741
  actions[matchedBinding.action]({
@@ -823,6 +873,7 @@ var init_screens = __esm({
823
873
  import_ink3 = require("ink");
824
874
  init_components();
825
875
  init_list_components();
876
+ init_key_bindings();
826
877
  showMenuScreen = showListScreen;
827
878
  showWordGridScreen = showMultiColumnListScreen;
828
879
  }
@@ -913,6 +964,157 @@ var init_ui_elements = __esm({
913
964
  }
914
965
  });
915
966
 
967
+ // src/screen/scrollable-text.js
968
+ function wrapTextLines(text, cols) {
969
+ const w = Math.max(1, Math.floor(Number(cols) || 1));
970
+ const out = [];
971
+ for (const raw of String(text ?? "").split("\n")) {
972
+ if (raw.length === 0) {
973
+ out.push("");
974
+ continue;
975
+ }
976
+ let rest = raw;
977
+ while (rest.length > w) {
978
+ out.push(rest.slice(0, w));
979
+ rest = rest.slice(w);
980
+ }
981
+ if (rest.length > 0) out.push(rest);
982
+ }
983
+ return out;
984
+ }
985
+ function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
986
+ const view = Math.max(1, Math.floor(viewportRows));
987
+ const total = Math.max(0, Math.floor(totalLines));
988
+ if (total <= view) return null;
989
+ const maxScroll = total - view;
990
+ const thumbSize = Math.max(1, Math.round(view / total * view));
991
+ const travel = Math.max(0, view - thumbSize);
992
+ const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
993
+ const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
994
+ const glyphs = [];
995
+ for (let i = 0; i < view; i++) {
996
+ glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? "\u2588" : "\u2502");
997
+ }
998
+ return glyphs;
999
+ }
1000
+ function padEndVisible(s, w) {
1001
+ const t = String(s ?? "");
1002
+ if (t.length >= w) return t.slice(0, w);
1003
+ return t + " ".repeat(w - t.length);
1004
+ }
1005
+ function ScrollableText({
1006
+ ctx,
1007
+ text = "",
1008
+ lines: linesProp,
1009
+ maxHeight,
1010
+ wrap = true,
1011
+ showScrollbar = true,
1012
+ showStatus = true,
1013
+ bindKeys = true,
1014
+ header = null
1015
+ }) {
1016
+ const [scrollTop, setScrollTop] = (0, import_react5.useState)(0);
1017
+ const [, bump] = (0, import_react5.useState)(0);
1018
+ const termCols = process.stdout.columns || 80;
1019
+ const termRows = process.stdout.rows || 24;
1020
+ const viewportRows = Math.max(
1021
+ 4,
1022
+ maxHeight != null ? Math.floor(maxHeight) : Math.max(8, termRows - 8)
1023
+ );
1024
+ const provisionalBar = showScrollbar ? 1 : 0;
1025
+ const wrapCols = Math.max(8, termCols - provisionalBar);
1026
+ const allLines = (0, import_react5.useMemo)(() => {
1027
+ if (Array.isArray(linesProp)) return linesProp.map((l) => String(l ?? ""));
1028
+ return wrap ? wrapTextLines(text, wrapCols) : String(text ?? "").split("\n");
1029
+ }, [linesProp, text, wrap, wrapCols]);
1030
+ const needsBar = showScrollbar && allLines.length > viewportRows;
1031
+ const textWidth = Math.max(8, termCols - (needsBar ? 1 : 0));
1032
+ const maxScroll = Math.max(0, allLines.length - viewportRows);
1033
+ const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
1034
+ const visible = allLines.slice(clamped, clamped + viewportRows);
1035
+ const bar = needsBar ? scrollbarGlyphs(viewportRows, allLines.length, clamped) : null;
1036
+ (0, import_react5.useEffect)(() => {
1037
+ setScrollTop((s) => Math.min(s, maxScroll));
1038
+ }, [maxScroll]);
1039
+ const maxScrollRef = (0, import_react5.useRef)(maxScroll);
1040
+ const pageSizeRef = (0, import_react5.useRef)(viewportRows);
1041
+ maxScrollRef.current = maxScroll;
1042
+ pageSizeRef.current = viewportRows;
1043
+ (0, import_react5.useEffect)(() => {
1044
+ if (!ctx || !bindKeys) return void 0;
1045
+ ctx.setKeyBinding(SCROLL_KEYS);
1046
+ ctx.setAction("scrollUp", () => {
1047
+ setScrollTop((s) => Math.max(0, s - 1));
1048
+ bump((n) => n + 1);
1049
+ ctx.update?.();
1050
+ });
1051
+ ctx.setAction("scrollDown", () => {
1052
+ setScrollTop((s) => Math.min(maxScrollRef.current, s + 1));
1053
+ bump((n) => n + 1);
1054
+ ctx.update?.();
1055
+ });
1056
+ ctx.setAction("pageUp", () => {
1057
+ setScrollTop((s) => Math.max(0, s - pageSizeRef.current));
1058
+ bump((n) => n + 1);
1059
+ ctx.update?.();
1060
+ });
1061
+ ctx.setAction("pageDown", () => {
1062
+ setScrollTop((s) => Math.min(maxScrollRef.current, s + pageSizeRef.current));
1063
+ bump((n) => n + 1);
1064
+ ctx.update?.();
1065
+ });
1066
+ return void 0;
1067
+ }, [ctx, bindKeys]);
1068
+ const status = allLines.length === 0 ? "empty" : `lines ${clamped + 1}-${Math.min(clamped + visible.length, allLines.length)} of ${allLines.length}` + (needsBar ? " \xB7 \u2325\u2191/\u2193 or PgUp/Dn page" : "");
1069
+ const rowNodes = visible.map((line, i) => {
1070
+ const body = padEndVisible(line, textWidth);
1071
+ const glyph = bar ? bar[i] ?? "\u2502" : "";
1072
+ return h5(
1073
+ import_ink5.Box,
1074
+ { key: `L${clamped + i}`, flexDirection: "row" },
1075
+ h5(import_ink5.Text, {}, body),
1076
+ glyph ? h5(import_ink5.Text, { color: bar[i] === "\u2588" ? "cyan" : "gray" }, glyph) : null
1077
+ );
1078
+ });
1079
+ if (bar && visible.length < viewportRows) {
1080
+ for (let i = visible.length; i < viewportRows; i++) {
1081
+ rowNodes.push(
1082
+ h5(
1083
+ import_ink5.Box,
1084
+ { key: `pad${i}`, flexDirection: "row" },
1085
+ h5(import_ink5.Text, {}, padEndVisible("", textWidth)),
1086
+ h5(import_ink5.Text, { color: bar[i] === "\u2588" ? "cyan" : "gray" }, bar[i] ?? "\u2502")
1087
+ )
1088
+ );
1089
+ }
1090
+ }
1091
+ return h5(
1092
+ import_ink5.Box,
1093
+ { flexDirection: "column" },
1094
+ header == null ? null : typeof header === "string" ? h5(import_ink5.Text, { color: "gray" }, header) : header,
1095
+ showStatus ? h5(import_ink5.Text, { color: "gray" }, status) : null,
1096
+ ...rowNodes
1097
+ );
1098
+ }
1099
+ var import_react5, import_ink5, h5, SCROLL_KEYS;
1100
+ var init_scrollable_text = __esm({
1101
+ "src/screen/scrollable-text.js"() {
1102
+ import_react5 = require("react");
1103
+ import_ink5 = require("ink");
1104
+ h5 = import_react5.createElement;
1105
+ SCROLL_KEYS = [
1106
+ { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
1107
+ { key: "downArrow", caption: "scroll", action: "scrollDown", order: 0 },
1108
+ { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
1109
+ { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
1110
+ { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
1111
+ { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
1112
+ { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
1113
+ { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
1114
+ ];
1115
+ }
1116
+ });
1117
+
916
1118
  // src/screen/utils.js
917
1119
  function buildBreadcrumb(parts) {
918
1120
  if (parts.length === 0) return "";
@@ -1044,7 +1246,7 @@ var init_footer_builder = __esm({
1044
1246
  // src/screen/index.js
1045
1247
  var screen_exports = {};
1046
1248
  __export(screen_exports, {
1047
- Box: () => import_ink5.Box,
1249
+ Box: () => import_ink6.Box,
1048
1250
  Divider: () => Divider,
1049
1251
  FooterPresets: () => FooterPresets,
1050
1252
  GridCell: () => GridCell,
@@ -1053,35 +1255,41 @@ __export(screen_exports, {
1053
1255
  ListItem: () => ListItem,
1054
1256
  MultiColumnListComponent: () => MultiColumnListComponent,
1055
1257
  MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
1056
- React: () => import_react5.default,
1258
+ React: () => import_react6.default,
1057
1259
  ScreenBody: () => ScreenBody,
1058
1260
  ScreenContainer: () => ScreenContainer,
1059
1261
  ScreenDivider: () => ScreenDivider,
1060
1262
  ScreenFooter: () => ScreenFooter,
1061
1263
  ScreenRow: () => ScreenRow,
1062
1264
  ScreenTitle: () => ScreenTitle,
1063
- Text: () => import_ink5.Text,
1265
+ ScrollableText: () => ScrollableText,
1266
+ Text: () => import_ink6.Text,
1064
1267
  TextBlock: () => TextBlock,
1268
+ bindingIdentity: () => bindingIdentity,
1269
+ bindingMatchesInput: () => bindingMatchesInput,
1065
1270
  buildBreadcrumb: () => buildBreadcrumb,
1066
1271
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1067
1272
  buildFooter: () => buildFooter,
1068
- h: () => import_react5.createElement,
1273
+ formatBindingKey: () => formatBindingKey,
1274
+ h: () => import_react6.createElement,
1069
1275
  load: () => load,
1070
- memo: () => import_react5.memo,
1276
+ memo: () => import_react6.memo,
1071
1277
  organizeFooterMessages: () => organizeFooterMessages,
1278
+ scrollbarGlyphs: () => scrollbarGlyphs,
1072
1279
  showListScreen: () => showListScreen,
1073
1280
  showMenuScreen: () => showMenuScreen,
1074
1281
  showMultiColumnListScreen: () => showMultiColumnListScreen,
1075
1282
  showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
1076
1283
  showScreen: () => showScreen,
1077
1284
  showWordGridScreen: () => showWordGridScreen,
1078
- useCallback: () => import_react5.useCallback,
1079
- useEffect: () => import_react5.useEffect,
1080
- useInput: () => import_ink5.useInput,
1081
- useLayoutEffect: () => import_react5.useLayoutEffect,
1082
- useMemo: () => import_react5.useMemo,
1083
- useRef: () => import_react5.useRef,
1084
- useState: () => import_react5.useState
1285
+ useCallback: () => import_react6.useCallback,
1286
+ useEffect: () => import_react6.useEffect,
1287
+ useInput: () => import_ink6.useInput,
1288
+ useLayoutEffect: () => import_react6.useLayoutEffect,
1289
+ useMemo: () => import_react6.useMemo,
1290
+ useRef: () => import_react6.useRef,
1291
+ useState: () => import_react6.useState,
1292
+ wrapTextLines: () => wrapTextLines
1085
1293
  });
1086
1294
  async function load() {
1087
1295
  if (loadPromise) return loadPromise;
@@ -1092,15 +1300,17 @@ async function load() {
1092
1300
  });
1093
1301
  return loadPromise;
1094
1302
  }
1095
- var import_react5, import_ink5, loadPromise;
1303
+ var import_react6, import_ink6, loadPromise;
1096
1304
  var init_screen = __esm({
1097
1305
  "src/screen/index.js"() {
1098
- import_react5 = __toESM(require("react"), 1);
1099
- import_ink5 = require("ink");
1306
+ import_react6 = __toESM(require("react"), 1);
1307
+ import_ink6 = require("ink");
1100
1308
  init_screens();
1101
1309
  init_list_components();
1102
1310
  init_components();
1103
1311
  init_ui_elements();
1312
+ init_scrollable_text();
1313
+ init_key_bindings();
1104
1314
  init_utils();
1105
1315
  init_footer_builder();
1106
1316
  loadPromise = null;
@@ -6334,8 +6544,18 @@ var LOGGER_RUNTIME_KEYS = [
6334
6544
  "progressThrottleMs"
6335
6545
  ];
6336
6546
  var CONTROL_LANE_TASK_NAMES = ["stopRunner", "stop", "setRuntimeParam", "setRunnerParam"];
6337
- function controlLaneTaskNames() {
6338
- return [...CONTROL_LANE_TASK_NAMES];
6547
+ function controlLaneTaskNames(extra) {
6548
+ const names = [...CONTROL_LANE_TASK_NAMES];
6549
+ if (extra == null || extra === "") return names;
6550
+ const more = Array.isArray(extra) ? extra : String(extra).split(",");
6551
+ const seen = new Set(names);
6552
+ for (const raw of more) {
6553
+ const n = String(raw ?? "").trim();
6554
+ if (!n || seen.has(n)) continue;
6555
+ seen.add(n);
6556
+ names.push(n);
6557
+ }
6558
+ return names;
6339
6559
  }
6340
6560
  function asPositiveInt(value, key, { min = 1 } = {}) {
6341
6561
  const n = Number(value);
@@ -6936,6 +7156,7 @@ async function runTasksLoop(context, options) {
6936
7156
  const queueName = options.queueName ?? "tasks";
6937
7157
  const target = options.target;
6938
7158
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
7159
+ const controlLaneNames = controlLaneTaskNames(options.controlLaneTasks);
6939
7160
  const registry = normalizeRegistry(options.registry);
6940
7161
  const { tasksTable, historyTable } = queueToTableNames(queueName);
6941
7162
  if (!target) throw new Error("runTasksLoop: target is required");
@@ -7006,7 +7227,7 @@ async function runTasksLoop(context, options) {
7006
7227
  target,
7007
7228
  registry,
7008
7229
  10,
7009
- controlLaneTaskNames(),
7230
+ controlLaneNames,
7010
7231
  runnerIdentity
7011
7232
  );
7012
7233
  if (claimedControlTask) {