@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.
@@ -478,6 +478,59 @@ var init_list_components = __esm({
478
478
  }
479
479
  });
480
480
 
481
+ // src/screen/key-bindings.js
482
+ function bindingIdentity(b) {
483
+ return [
484
+ String(b?.key ?? ""),
485
+ b?.meta ? "m" : "",
486
+ b?.ctrl ? "c" : "",
487
+ b?.shift ? "s" : ""
488
+ ].join("|");
489
+ }
490
+ function bindingMatchesInput(binding, input, key) {
491
+ if (!binding?.key) return false;
492
+ let keyMatches = false;
493
+ if (key?.[binding.key]) {
494
+ keyMatches = true;
495
+ } else if (input === binding.key) {
496
+ keyMatches = true;
497
+ } else if (key?.name === binding.key) {
498
+ keyMatches = true;
499
+ }
500
+ if (!keyMatches) return false;
501
+ const modOk = (flag, pressed) => {
502
+ if (flag === true) return !!pressed;
503
+ if (flag === false) return !pressed;
504
+ return !pressed;
505
+ };
506
+ if (!modOk(binding.meta, key?.meta)) return false;
507
+ if (!modOk(binding.ctrl, key?.ctrl)) return false;
508
+ if (binding.shift !== void 0 && !!key?.shift !== !!binding.shift) return false;
509
+ return true;
510
+ }
511
+ function formatBindingKey(binding) {
512
+ let label = KEY_LABELS[binding.key] || binding.key;
513
+ if (binding.shift) label = `\u21E7${label}`;
514
+ if (binding.ctrl) label = `^${label}`;
515
+ if (binding.meta) label = `\u2325${label}`;
516
+ return label;
517
+ }
518
+ var KEY_LABELS;
519
+ var init_key_bindings = __esm({
520
+ "src/screen/key-bindings.js"() {
521
+ KEY_LABELS = {
522
+ escape: "esc",
523
+ leftArrow: "\u2190",
524
+ rightArrow: "\u2192",
525
+ upArrow: "\u2191",
526
+ downArrow: "\u2193",
527
+ return: "enter",
528
+ pageUp: "PgUp",
529
+ pageDown: "PgDn"
530
+ };
531
+ }
532
+ });
533
+
481
534
  // src/screen/screens.js
482
535
  import { useState as useState2, createElement as h3 } from "react";
483
536
  import { render, useInput, Text as Text3 } from "ink";
@@ -493,7 +546,7 @@ function groupKeyBindings(bindings) {
493
546
  order: binding.order || 999
494
547
  };
495
548
  }
496
- groups[caption].keys.push(binding.key);
549
+ groups[caption].keys.push(formatBindingKey(binding));
497
550
  });
498
551
  return Object.values(groups);
499
552
  }
@@ -533,12 +586,14 @@ function formatKeyBindings(bindings, mode = "long") {
533
586
  }
534
587
  function formatKeys(keys) {
535
588
  const keyMap = {
536
- "escape": "esc",
537
- "leftArrow": "\u2190",
538
- "rightArrow": "\u2192",
539
- "upArrow": "\u2191",
540
- "downArrow": "\u2193",
541
- "return": "enter"
589
+ escape: "esc",
590
+ leftArrow: "\u2190",
591
+ rightArrow: "\u2192",
592
+ upArrow: "\u2191",
593
+ downArrow: "\u2193",
594
+ return: "enter",
595
+ pageUp: "PgUp",
596
+ pageDown: "PgDn"
542
597
  };
543
598
  return keys.map((k) => keyMap[k] || k).join("/");
544
599
  }
@@ -578,7 +633,10 @@ async function showScreen(config2) {
578
633
  setKeyBinding: (bindingOrBindings) => {
579
634
  const bindingsToSet = Array.isArray(bindingOrBindings) ? bindingOrBindings : [bindingOrBindings];
580
635
  bindingsToSet.forEach((binding) => {
581
- const existingIndex = keyBindings.findIndex((b) => b.key === binding.key);
636
+ const id = bindingIdentity(binding);
637
+ const existingIndex = keyBindings.findIndex(
638
+ (b) => bindingIdentity(b) === id
639
+ );
582
640
  if (existingIndex >= 0) {
583
641
  const existing = keyBindings[existingIndex];
584
642
  if (existing.protected) {
@@ -602,7 +660,10 @@ async function showScreen(config2) {
602
660
  });
603
661
  },
604
662
  updateKeyBinding: (keyName, updates) => {
605
- const index = keyBindings.findIndex((b) => b.key === keyName);
663
+ const id = updates && (updates.meta != null || updates.ctrl != null || updates.shift != null) ? bindingIdentity({ key: keyName, ...updates }) : null;
664
+ const index = keyBindings.findIndex(
665
+ (b) => id ? bindingIdentity(b) === id : b.key === keyName
666
+ );
606
667
  if (index >= 0) {
607
668
  keyBindings[index] = {
608
669
  ...keyBindings[index],
@@ -610,11 +671,13 @@ async function showScreen(config2) {
610
671
  };
611
672
  }
612
673
  },
613
- removeKeyBinding: (keyName) => {
614
- const index = keyBindings.findIndex((b) => b.key === keyName);
674
+ removeKeyBinding: (keyNameOrBinding) => {
675
+ const index = typeof keyNameOrBinding === "object" && keyNameOrBinding ? keyBindings.findIndex(
676
+ (b) => bindingIdentity(b) === bindingIdentity(keyNameOrBinding)
677
+ ) : keyBindings.findIndex((b) => b.key === keyNameOrBinding);
615
678
  if (index >= 0) {
616
679
  if (keyBindings[index].protected) {
617
- console.warn(`Cannot remove protected key: ${keyName}`);
680
+ console.warn(`Cannot remove protected key: ${keyNameOrBinding}`);
618
681
  return;
619
682
  }
620
683
  keyBindings.splice(index, 1);
@@ -655,24 +718,11 @@ async function showScreen(config2) {
655
718
  }
656
719
  let matchedBinding = null;
657
720
  for (const binding of keyBindings) {
658
- let keyMatches = false;
659
- if (key[binding.key]) {
660
- keyMatches = true;
661
- } else if (input === binding.key) {
662
- keyMatches = true;
663
- } else if (key?.name === binding.key) {
664
- keyMatches = true;
665
- }
666
- if (keyMatches) {
667
- if (binding.enabled === false) {
668
- continue;
669
- }
670
- if (binding.condition && !binding.condition(context)) {
671
- continue;
672
- }
673
- matchedBinding = binding;
674
- break;
675
- }
721
+ if (binding.enabled === false) continue;
722
+ if (!bindingMatchesInput(binding, input, key)) continue;
723
+ if (binding.condition && !binding.condition(context)) continue;
724
+ matchedBinding = binding;
725
+ break;
676
726
  }
677
727
  if (matchedBinding && actions[matchedBinding.action]) {
678
728
  actions[matchedBinding.action]({
@@ -808,6 +858,7 @@ var init_screens = __esm({
808
858
  "src/screen/screens.js"() {
809
859
  init_components();
810
860
  init_list_components();
861
+ init_key_bindings();
811
862
  showMenuScreen = showListScreen;
812
863
  showWordGridScreen = showMultiColumnListScreen;
813
864
  }
@@ -897,6 +948,157 @@ var init_ui_elements = __esm({
897
948
  }
898
949
  });
899
950
 
951
+ // src/screen/scrollable-text.js
952
+ import { useState as useState3, useEffect as useEffect2, useMemo, useRef as useRef2, createElement as createElement2 } from "react";
953
+ import { Box as Box4, Text as Text5 } from "ink";
954
+ function wrapTextLines(text, cols) {
955
+ const w = Math.max(1, Math.floor(Number(cols) || 1));
956
+ const out = [];
957
+ for (const raw of String(text ?? "").split("\n")) {
958
+ if (raw.length === 0) {
959
+ out.push("");
960
+ continue;
961
+ }
962
+ let rest = raw;
963
+ while (rest.length > w) {
964
+ out.push(rest.slice(0, w));
965
+ rest = rest.slice(w);
966
+ }
967
+ if (rest.length > 0) out.push(rest);
968
+ }
969
+ return out;
970
+ }
971
+ function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
972
+ const view = Math.max(1, Math.floor(viewportRows));
973
+ const total = Math.max(0, Math.floor(totalLines));
974
+ if (total <= view) return null;
975
+ const maxScroll = total - view;
976
+ const thumbSize = Math.max(1, Math.round(view / total * view));
977
+ const travel = Math.max(0, view - thumbSize);
978
+ const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
979
+ const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
980
+ const glyphs = [];
981
+ for (let i = 0; i < view; i++) {
982
+ glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? "\u2588" : "\u2502");
983
+ }
984
+ return glyphs;
985
+ }
986
+ function padEndVisible(s, w) {
987
+ const t = String(s ?? "");
988
+ if (t.length >= w) return t.slice(0, w);
989
+ return t + " ".repeat(w - t.length);
990
+ }
991
+ function ScrollableText({
992
+ ctx,
993
+ text = "",
994
+ lines: linesProp,
995
+ maxHeight,
996
+ wrap = true,
997
+ showScrollbar = true,
998
+ showStatus = true,
999
+ bindKeys = true,
1000
+ header = null
1001
+ }) {
1002
+ const [scrollTop, setScrollTop] = useState3(0);
1003
+ const [, bump] = useState3(0);
1004
+ const termCols = process.stdout.columns || 80;
1005
+ const termRows = process.stdout.rows || 24;
1006
+ const viewportRows = Math.max(
1007
+ 4,
1008
+ maxHeight != null ? Math.floor(maxHeight) : Math.max(8, termRows - 8)
1009
+ );
1010
+ const provisionalBar = showScrollbar ? 1 : 0;
1011
+ const wrapCols = Math.max(8, termCols - provisionalBar);
1012
+ const allLines = useMemo(() => {
1013
+ if (Array.isArray(linesProp)) return linesProp.map((l) => String(l ?? ""));
1014
+ return wrap ? wrapTextLines(text, wrapCols) : String(text ?? "").split("\n");
1015
+ }, [linesProp, text, wrap, wrapCols]);
1016
+ const needsBar = showScrollbar && allLines.length > viewportRows;
1017
+ const textWidth = Math.max(8, termCols - (needsBar ? 1 : 0));
1018
+ const maxScroll = Math.max(0, allLines.length - viewportRows);
1019
+ const clamped = Math.min(Math.max(0, scrollTop), maxScroll);
1020
+ const visible = allLines.slice(clamped, clamped + viewportRows);
1021
+ const bar = needsBar ? scrollbarGlyphs(viewportRows, allLines.length, clamped) : null;
1022
+ useEffect2(() => {
1023
+ setScrollTop((s) => Math.min(s, maxScroll));
1024
+ }, [maxScroll]);
1025
+ const maxScrollRef = useRef2(maxScroll);
1026
+ const pageSizeRef = useRef2(viewportRows);
1027
+ maxScrollRef.current = maxScroll;
1028
+ pageSizeRef.current = viewportRows;
1029
+ useEffect2(() => {
1030
+ if (!ctx || !bindKeys) return void 0;
1031
+ ctx.setKeyBinding(SCROLL_KEYS);
1032
+ ctx.setAction("scrollUp", () => {
1033
+ setScrollTop((s) => Math.max(0, s - 1));
1034
+ bump((n) => n + 1);
1035
+ ctx.update?.();
1036
+ });
1037
+ ctx.setAction("scrollDown", () => {
1038
+ setScrollTop((s) => Math.min(maxScrollRef.current, s + 1));
1039
+ bump((n) => n + 1);
1040
+ ctx.update?.();
1041
+ });
1042
+ ctx.setAction("pageUp", () => {
1043
+ setScrollTop((s) => Math.max(0, s - pageSizeRef.current));
1044
+ bump((n) => n + 1);
1045
+ ctx.update?.();
1046
+ });
1047
+ ctx.setAction("pageDown", () => {
1048
+ setScrollTop((s) => Math.min(maxScrollRef.current, s + pageSizeRef.current));
1049
+ bump((n) => n + 1);
1050
+ ctx.update?.();
1051
+ });
1052
+ return void 0;
1053
+ }, [ctx, bindKeys]);
1054
+ 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" : "");
1055
+ const rowNodes = visible.map((line, i) => {
1056
+ const body = padEndVisible(line, textWidth);
1057
+ const glyph = bar ? bar[i] ?? "\u2502" : "";
1058
+ return h5(
1059
+ Box4,
1060
+ { key: `L${clamped + i}`, flexDirection: "row" },
1061
+ h5(Text5, {}, body),
1062
+ glyph ? h5(Text5, { color: bar[i] === "\u2588" ? "cyan" : "gray" }, glyph) : null
1063
+ );
1064
+ });
1065
+ if (bar && visible.length < viewportRows) {
1066
+ for (let i = visible.length; i < viewportRows; i++) {
1067
+ rowNodes.push(
1068
+ h5(
1069
+ Box4,
1070
+ { key: `pad${i}`, flexDirection: "row" },
1071
+ h5(Text5, {}, padEndVisible("", textWidth)),
1072
+ h5(Text5, { color: bar[i] === "\u2588" ? "cyan" : "gray" }, bar[i] ?? "\u2502")
1073
+ )
1074
+ );
1075
+ }
1076
+ }
1077
+ return h5(
1078
+ Box4,
1079
+ { flexDirection: "column" },
1080
+ header == null ? null : typeof header === "string" ? h5(Text5, { color: "gray" }, header) : header,
1081
+ showStatus ? h5(Text5, { color: "gray" }, status) : null,
1082
+ ...rowNodes
1083
+ );
1084
+ }
1085
+ var h5, SCROLL_KEYS;
1086
+ var init_scrollable_text = __esm({
1087
+ "src/screen/scrollable-text.js"() {
1088
+ h5 = createElement2;
1089
+ SCROLL_KEYS = [
1090
+ { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
1091
+ { key: "downArrow", caption: "scroll", action: "scrollDown", order: 0 },
1092
+ { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
1093
+ { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
1094
+ { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
1095
+ { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
1096
+ { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
1097
+ { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
1098
+ ];
1099
+ }
1100
+ });
1101
+
900
1102
  // src/screen/utils.js
901
1103
  function buildBreadcrumb(parts) {
902
1104
  if (parts.length === 0) return "";
@@ -1028,7 +1230,7 @@ var init_footer_builder = __esm({
1028
1230
  // src/screen/index.js
1029
1231
  var screen_exports = {};
1030
1232
  __export(screen_exports, {
1031
- Box: () => Box4,
1233
+ Box: () => Box5,
1032
1234
  Divider: () => Divider,
1033
1235
  FooterPresets: () => FooterPresets,
1034
1236
  GridCell: () => GridCell,
@@ -1044,15 +1246,20 @@ __export(screen_exports, {
1044
1246
  ScreenFooter: () => ScreenFooter,
1045
1247
  ScreenRow: () => ScreenRow,
1046
1248
  ScreenTitle: () => ScreenTitle,
1047
- Text: () => Text5,
1249
+ ScrollableText: () => ScrollableText,
1250
+ Text: () => Text6,
1048
1251
  TextBlock: () => TextBlock,
1252
+ bindingIdentity: () => bindingIdentity,
1253
+ bindingMatchesInput: () => bindingMatchesInput,
1049
1254
  buildBreadcrumb: () => buildBreadcrumb,
1050
1255
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1051
1256
  buildFooter: () => buildFooter,
1052
- h: () => createElement2,
1257
+ formatBindingKey: () => formatBindingKey,
1258
+ h: () => createElement3,
1053
1259
  load: () => load,
1054
1260
  memo: () => memo,
1055
1261
  organizeFooterMessages: () => organizeFooterMessages,
1262
+ scrollbarGlyphs: () => scrollbarGlyphs,
1056
1263
  showListScreen: () => showListScreen,
1057
1264
  showMenuScreen: () => showMenuScreen,
1058
1265
  showMultiColumnListScreen: () => showMultiColumnListScreen,
@@ -1060,15 +1267,16 @@ __export(screen_exports, {
1060
1267
  showScreen: () => showScreen,
1061
1268
  showWordGridScreen: () => showWordGridScreen,
1062
1269
  useCallback: () => useCallback,
1063
- useEffect: () => useEffect2,
1270
+ useEffect: () => useEffect3,
1064
1271
  useInput: () => useInput2,
1065
1272
  useLayoutEffect: () => useLayoutEffect,
1066
- useMemo: () => useMemo,
1067
- useRef: () => useRef2,
1068
- useState: () => useState3
1273
+ useMemo: () => useMemo2,
1274
+ useRef: () => useRef3,
1275
+ useState: () => useState4,
1276
+ wrapTextLines: () => wrapTextLines
1069
1277
  });
1070
- import React2, { useState as useState3, useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useMemo, useCallback, memo, createElement as createElement2 } from "react";
1071
- import { Box as Box4, Text as Text5, useInput as useInput2 } from "ink";
1278
+ import React2, { useState as useState4, useEffect as useEffect3, useLayoutEffect, useRef as useRef3, useMemo as useMemo2, useCallback, memo, createElement as createElement3 } from "react";
1279
+ import { Box as Box5, Text as Text6, useInput as useInput2 } from "ink";
1072
1280
  async function load() {
1073
1281
  if (loadPromise) return loadPromise;
1074
1282
  loadPromise = Promise.all([
@@ -1085,6 +1293,8 @@ var init_screen = __esm({
1085
1293
  init_list_components();
1086
1294
  init_components();
1087
1295
  init_ui_elements();
1296
+ init_scrollable_text();
1297
+ init_key_bindings();
1088
1298
  init_utils();
1089
1299
  init_footer_builder();
1090
1300
  loadPromise = null;
@@ -6318,8 +6528,18 @@ var LOGGER_RUNTIME_KEYS = [
6318
6528
  "progressThrottleMs"
6319
6529
  ];
6320
6530
  var CONTROL_LANE_TASK_NAMES = ["stopRunner", "stop", "setRuntimeParam", "setRunnerParam"];
6321
- function controlLaneTaskNames() {
6322
- return [...CONTROL_LANE_TASK_NAMES];
6531
+ function controlLaneTaskNames(extra) {
6532
+ const names = [...CONTROL_LANE_TASK_NAMES];
6533
+ if (extra == null || extra === "") return names;
6534
+ const more = Array.isArray(extra) ? extra : String(extra).split(",");
6535
+ const seen = new Set(names);
6536
+ for (const raw of more) {
6537
+ const n = String(raw ?? "").trim();
6538
+ if (!n || seen.has(n)) continue;
6539
+ seen.add(n);
6540
+ names.push(n);
6541
+ }
6542
+ return names;
6323
6543
  }
6324
6544
  function asPositiveInt(value, key, { min = 1 } = {}) {
6325
6545
  const n = Number(value);
@@ -6920,6 +7140,7 @@ async function runTasksLoop(context, options) {
6920
7140
  const queueName = options.queueName ?? "tasks";
6921
7141
  const target = options.target;
6922
7142
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
7143
+ const controlLaneNames = controlLaneTaskNames(options.controlLaneTasks);
6923
7144
  const registry = normalizeRegistry(options.registry);
6924
7145
  const { tasksTable, historyTable } = queueToTableNames(queueName);
6925
7146
  if (!target) throw new Error("runTasksLoop: target is required");
@@ -6990,7 +7211,7 @@ async function runTasksLoop(context, options) {
6990
7211
  target,
6991
7212
  registry,
6992
7213
  10,
6993
- controlLaneTaskNames(),
7214
+ controlLaneNames,
6994
7215
  runnerIdentity
6995
7216
  );
6996
7217
  if (claimedControlTask) {