@nmakarov/cli-toolkit 0.7.1 → 0.8.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.
package/dist/init.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,1091 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/screen/components.ts
34
+ function getScreenWidth(maxWidth = null) {
35
+ const terminalWidth = process.stdout.columns || 80;
36
+ const availableWidth = Math.max(20, terminalWidth - 4);
37
+ return maxWidth ? Math.min(availableWidth, maxWidth) : availableWidth;
38
+ }
39
+ function ScreenContainer({ children }) {
40
+ const width = getScreenWidth();
41
+ return (0, import_react.createElement)(import_ink.Box, {
42
+ flexDirection: "column",
43
+ marginTop: 1,
44
+ borderStyle: "single",
45
+ borderColor: "cyan",
46
+ paddingX: 1,
47
+ width
48
+ // Use the calculated width directly
49
+ }, children);
50
+ }
51
+ function ScreenRow({ children }) {
52
+ return (0, import_react.createElement)(import_ink.Box, { flexDirection: "column" }, children);
53
+ }
54
+ function ScreenTitle({ text }) {
55
+ return (0, import_react.createElement)(
56
+ ScreenRow,
57
+ {},
58
+ (0, import_react.createElement)(import_ink.Text, { bold: true, color: "cyan" }, text)
59
+ );
60
+ }
61
+ function ScreenDivider({ width }) {
62
+ const dividerWidth = width || getScreenWidth() - 4;
63
+ return (0, import_react.createElement)(import_ink.Text, { color: "cyan", dimColor: true }, "\u2500".repeat(dividerWidth));
64
+ }
65
+ function ScreenBody({ children, alignItems = "flex-start" }) {
66
+ return (0, import_react.createElement)(import_ink.Box, { flexDirection: "column", alignItems }, children);
67
+ }
68
+ function ScreenFooter({ lines, textStyle }) {
69
+ const defaultTextStyle = {
70
+ dimColor: true,
71
+ color: "white"
72
+ };
73
+ const finalTextStyle = { ...defaultTextStyle, ...textStyle };
74
+ const flattenAndWrap = (items, keyPrefix = "") => {
75
+ const result = [];
76
+ let keyIndex = 0;
77
+ items.forEach((item, index) => {
78
+ if (Array.isArray(item)) {
79
+ const nested = flattenAndWrap(item, `${keyPrefix}-${index}`);
80
+ result.push(...nested);
81
+ } else if (typeof item === "string") {
82
+ result.push(
83
+ (0, import_react.createElement)(import_ink.Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, item)
84
+ );
85
+ } else {
86
+ const element = item;
87
+ if (element.key === null || element.key === void 0) {
88
+ result.push(
89
+ (0, import_react.createElement)(import_ink.Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, element)
90
+ );
91
+ } else {
92
+ result.push(element);
93
+ }
94
+ }
95
+ });
96
+ return result;
97
+ };
98
+ const wrappedItems = flattenAndWrap(lines);
99
+ return (0, import_react.createElement)(
100
+ import_ink.Box,
101
+ { flexDirection: "column" },
102
+ (0, import_react.createElement)(import_ink.Box, { flexDirection: "row" }, ...wrappedItems)
103
+ );
104
+ }
105
+ var import_react, import_ink;
106
+ var init_components = __esm({
107
+ "src/screen/components.ts"() {
108
+ "use strict";
109
+ import_react = require("react");
110
+ import_ink = require("ink");
111
+ }
112
+ });
113
+
114
+ // src/screen/list-components.ts
115
+ function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
116
+ const [, forceUpdate] = (0, import_react2.useState)({});
117
+ const termWidth = (process.stdout.columns || 80) - 8;
118
+ const maxItemLength = Math.max(...items.map((w) => w.length));
119
+ const columnWidth = maxItemLength + 3;
120
+ const columns = Math.max(1, Math.floor(termWidth / columnWidth));
121
+ const itemsPerColumn = Math.ceil(items.length / columns);
122
+ (0, import_react2.useEffect)(() => {
123
+ ctx.setAction("moveUp", () => {
124
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
125
+ forceUpdate({});
126
+ });
127
+ ctx.setAction("moveDown", () => {
128
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
129
+ forceUpdate({});
130
+ });
131
+ ctx.setAction("moveLeft", () => {
132
+ if (selectedIndexRef.current === 0) {
133
+ ctx.goBack();
134
+ } else {
135
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
136
+ forceUpdate({});
137
+ }
138
+ });
139
+ ctx.setAction("moveRight", () => {
140
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
141
+ forceUpdate({});
142
+ });
143
+ ctx.setKeyBinding([
144
+ { key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
145
+ { key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
146
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
147
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
148
+ ]);
149
+ ctx.addFooter(`Total: ${items.length} items`);
150
+ }, []);
151
+ const selectedIndex = selectedIndexRef.current;
152
+ const rows = [];
153
+ for (let row = 0; row < itemsPerColumn; row++) {
154
+ const cols = [];
155
+ for (let col = 0; col < columns; col++) {
156
+ const index = col * itemsPerColumn + row;
157
+ if (index < items.length) {
158
+ const isSelected = index === selectedIndex;
159
+ cols.push(
160
+ h2(
161
+ import_ink2.Box,
162
+ { key: index, width: columnWidth },
163
+ h2(import_ink2.Text, {
164
+ color: isSelected ? "black" : "white",
165
+ backgroundColor: isSelected ? "cyan" : void 0,
166
+ bold: isSelected
167
+ }, items[index].padEnd(maxItemLength))
168
+ )
169
+ );
170
+ }
171
+ }
172
+ rows.push(
173
+ h2(ScreenRow, { key: row, children: h2(import_ink2.Box, { flexDirection: "row" }, ...cols) })
174
+ );
175
+ }
176
+ return h2(import_ink2.Box, { flexDirection: "column" }, ...rows);
177
+ }
178
+ function MultiColumnListWithPreviewComponent({
179
+ items,
180
+ getPreviewContent,
181
+ ctx,
182
+ selectedIndexRef
183
+ }) {
184
+ const [, forceUpdate] = (0, import_react2.useState)({});
185
+ const termWidth = (process.stdout.columns || 80) - 8;
186
+ const maxItemLength = Math.max(...items.map((w) => w.length));
187
+ const columnWidth = maxItemLength + 3;
188
+ const columns = Math.max(1, Math.floor(termWidth / columnWidth));
189
+ const itemsPerColumn = Math.ceil(items.length / columns);
190
+ (0, import_react2.useEffect)(() => {
191
+ ctx.setAction("moveUp", () => {
192
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
193
+ forceUpdate({});
194
+ });
195
+ ctx.setAction("moveDown", () => {
196
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
197
+ forceUpdate({});
198
+ });
199
+ ctx.setAction("moveLeft", () => {
200
+ if (selectedIndexRef.current === 0) {
201
+ ctx.goBack();
202
+ } else {
203
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
204
+ forceUpdate({});
205
+ }
206
+ });
207
+ ctx.setAction("moveRight", () => {
208
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
209
+ forceUpdate({});
210
+ });
211
+ ctx.setKeyBinding([
212
+ { key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
213
+ { key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
214
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
215
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
216
+ ]);
217
+ ctx.addFooter(`Total: ${items.length} items`);
218
+ }, []);
219
+ const selectedIndex = selectedIndexRef.current;
220
+ const selectedItem = items[selectedIndex];
221
+ const rows = [];
222
+ for (let row = 0; row < itemsPerColumn; row++) {
223
+ const cols = [];
224
+ for (let col = 0; col < columns; col++) {
225
+ const index = col * itemsPerColumn + row;
226
+ if (index < items.length) {
227
+ const isSelected = index === selectedIndex;
228
+ cols.push(
229
+ h2(
230
+ import_ink2.Box,
231
+ { key: index, width: columnWidth },
232
+ h2(import_ink2.Text, {
233
+ color: isSelected ? "black" : "white",
234
+ backgroundColor: isSelected ? "cyan" : void 0,
235
+ bold: isSelected
236
+ }, items[index].padEnd(maxItemLength))
237
+ )
238
+ );
239
+ }
240
+ }
241
+ rows.push(
242
+ h2(ScreenRow, { key: row, children: h2(import_ink2.Box, { flexDirection: "row" }, ...cols) })
243
+ );
244
+ }
245
+ const previewContent = getPreviewContent ? getPreviewContent(selectedItem) : selectedItem;
246
+ const previewRows = [];
247
+ if (typeof previewContent === "string") {
248
+ previewRows.push(h2(ScreenRow, { key: "preview-string", children: h2(import_ink2.Text, { bold: true }, previewContent) }));
249
+ } else if (typeof previewContent === "object" && !import_react2.default.isValidElement(previewContent) && previewContent !== null) {
250
+ Object.entries(previewContent).forEach(([key, value], idx) => {
251
+ previewRows.push(h2(ScreenRow, { key: `preview-${key}-${idx}`, children: h2(import_ink2.Text, {}, `${key}: ${value}`) }));
252
+ });
253
+ } else if (import_react2.default.isValidElement(previewContent)) {
254
+ previewRows.push(h2(ScreenRow, { key: "preview-element", children: previewContent }));
255
+ }
256
+ return h2(
257
+ import_ink2.Box,
258
+ { flexDirection: "column" },
259
+ ...rows,
260
+ h2(ScreenRow, { key: "spacer-1", children: h2(import_ink2.Text, {}, " ") }),
261
+ h2(ScreenDivider, { key: "divider" }),
262
+ h2(ScreenRow, { key: "spacer-2", children: h2(import_ink2.Text, {}, " ") }),
263
+ ...previewRows
264
+ );
265
+ }
266
+ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
267
+ const [, forceUpdate] = (0, import_react2.useState)({});
268
+ const [sortOrder, setSortOrder] = (0, import_react2.useState)("none");
269
+ const [scrollOffset, setScrollOffset] = (0, import_react2.useState)(0);
270
+ const scrollStateRef = (0, import_react2.useRef)({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
271
+ const defaultGetTitle = (item) => {
272
+ return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
273
+ };
274
+ const titleGetter = getTitle || defaultGetTitle;
275
+ const displayItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
276
+ const titleA = titleGetter(a).toLowerCase();
277
+ const titleB = titleGetter(b).toLowerCase();
278
+ if (sortOrder === "asc") {
279
+ return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
280
+ } else {
281
+ return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
282
+ }
283
+ }) : items;
284
+ const effectiveMaxHeight = maxHeight || displayItems.length;
285
+ const canScroll = displayItems.length > effectiveMaxHeight;
286
+ const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
287
+ const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
288
+ const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
289
+ const canScrollUp = clampedScrollOffset > 0;
290
+ const canScrollDown = clampedScrollOffset < maxScrollOffset;
291
+ scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
292
+ (0, import_react2.useEffect)(() => {
293
+ ctx.setAction("moveUp", () => {
294
+ const newIndex = Math.max(0, selectedIndexRef.current - 1);
295
+ selectedIndexRef.current = newIndex;
296
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
297
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
298
+ const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
299
+ if (newIndex < currentClampedScrollOffset) {
300
+ setScrollOffset(newIndex);
301
+ }
302
+ forceUpdate({});
303
+ });
304
+ ctx.setAction("moveDown", () => {
305
+ const currentItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
306
+ const titleA = titleGetter(a).toLowerCase();
307
+ const titleB = titleGetter(b).toLowerCase();
308
+ if (sortOrder === "asc") {
309
+ return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
310
+ } else {
311
+ return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
312
+ }
313
+ }) : items;
314
+ const maxIndex = currentItems.length - 1;
315
+ const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
316
+ selectedIndexRef.current = newIndex;
317
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
318
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
319
+ const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
320
+ if (newIndex >= currentClampedScrollOffset + currentMaxHeight) {
321
+ setScrollOffset(newIndex - currentMaxHeight + 1);
322
+ }
323
+ forceUpdate({});
324
+ });
325
+ ctx.setAction("scrollUp", () => {
326
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
327
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
328
+ const newScrollOffset = Math.max(0, currentScrollOffset - 1);
329
+ setScrollOffset(newScrollOffset);
330
+ forceUpdate({});
331
+ });
332
+ ctx.setAction("scrollDown", () => {
333
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
334
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
335
+ const newScrollOffset = Math.min(currentMaxScrollOffset, currentScrollOffset + 1);
336
+ setScrollOffset(newScrollOffset);
337
+ forceUpdate({});
338
+ });
339
+ if (sortable) {
340
+ ctx.setAction("toggleSort", () => {
341
+ const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
342
+ const currentSelectedItem = displayItems[selectedIndexRef.current];
343
+ setSortOrder(nextSort);
344
+ const newSortedItems = nextSort !== "none" ? [...items].sort((a, b) => {
345
+ const titleA = titleGetter(a).toLowerCase();
346
+ const titleB = titleGetter(b).toLowerCase();
347
+ if (nextSort === "asc") {
348
+ return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
349
+ } else {
350
+ return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
351
+ }
352
+ }) : items;
353
+ const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
354
+ if (newIndex !== -1) {
355
+ selectedIndexRef.current = newIndex;
356
+ setScrollOffset(newIndex);
357
+ } else {
358
+ selectedIndexRef.current = 0;
359
+ setScrollOffset(0);
360
+ }
361
+ forceUpdate({});
362
+ });
363
+ const defaultHighlightStyle = {
364
+ color: "black",
365
+ backgroundColor: "green",
366
+ bold: true
367
+ };
368
+ const highlightStyle = { ...defaultHighlightStyle, ...sortHighlightStyle };
369
+ const sortCaption = () => {
370
+ if (sortOrder === "none") {
371
+ return h2(import_ink2.Text, {}, "s to toggle sort");
372
+ } else {
373
+ const sortLabel = sortOrder === "asc" ? "ASC" : "DESC";
374
+ return h2(
375
+ import_ink2.Text,
376
+ {},
377
+ "s to toggle ",
378
+ h2(import_ink2.Text, { color: "white", bold: true }, "sort"),
379
+ " ",
380
+ h2(import_ink2.Text, highlightStyle, ` ${sortLabel} `)
381
+ );
382
+ }
383
+ };
384
+ ctx.setKeyBinding([
385
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
386
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
387
+ {
388
+ key: "s",
389
+ caption: sortCaption,
390
+ action: "toggleSort",
391
+ order: 5
392
+ }
393
+ ]);
394
+ ctx.update();
395
+ } else {
396
+ ctx.setKeyBinding([
397
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
398
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
399
+ ]);
400
+ }
401
+ }, [sortOrder, sortable]);
402
+ const selectedIndex = selectedIndexRef.current;
403
+ const defaultRenderItem = (item, isSelected, displayIndex, actualIndex) => {
404
+ const isFirstVisible = displayIndex === 0;
405
+ const isLastVisible = displayIndex === visibleItems.length - 1;
406
+ let arrowPrefix = "";
407
+ let selectionPrefix = "";
408
+ if (isFirstVisible && canScrollUp) {
409
+ arrowPrefix = "\u2191 ";
410
+ } else if (isLastVisible && canScrollDown) {
411
+ arrowPrefix = "\u2193 ";
412
+ } else {
413
+ arrowPrefix = " ";
414
+ }
415
+ if (isSelected) {
416
+ selectionPrefix = selectionMarker;
417
+ } else {
418
+ selectionPrefix = " ".repeat(selectionMarker.length);
419
+ }
420
+ return h2(
421
+ import_ink2.Box,
422
+ { flexDirection: "row" },
423
+ // Arrow (clickable if functional, not highlighted)
424
+ h2(import_ink2.Text, {
425
+ key: `arrow-${actualIndex}`,
426
+ color: "white"
427
+ }, arrowPrefix),
428
+ // Selection marker space (always same width, not highlighted)
429
+ h2(import_ink2.Text, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
430
+ // Item name (highlighted if selected)
431
+ h2(import_ink2.Text, {
432
+ key: `name-${actualIndex}`,
433
+ color: isSelected ? "black" : "white",
434
+ backgroundColor: isSelected ? "cyan" : void 0,
435
+ bold: isSelected
436
+ }, item.name)
437
+ );
438
+ };
439
+ const itemRenderer = renderItem || defaultRenderItem;
440
+ return h2(
441
+ import_ink2.Box,
442
+ { flexDirection: "column" },
443
+ ...visibleItems.map((item, displayIndex) => {
444
+ const actualIndex = clampedScrollOffset + displayIndex;
445
+ const isSelected = actualIndex === selectedIndex;
446
+ if (renderItem) {
447
+ const isFirstVisible = displayIndex === 0;
448
+ const isLastVisible = displayIndex === visibleItems.length - 1;
449
+ let arrowPrefix = "";
450
+ let selectionPrefix = "";
451
+ if (isFirstVisible && canScrollUp) {
452
+ arrowPrefix = "\u2191 ";
453
+ } else if (isLastVisible && canScrollDown) {
454
+ arrowPrefix = "\u2193 ";
455
+ } else {
456
+ arrowPrefix = " ";
457
+ }
458
+ if (isSelected) {
459
+ selectionPrefix = selectionMarker;
460
+ } else {
461
+ selectionPrefix = " ".repeat(selectionMarker.length);
462
+ }
463
+ return h2(ScreenRow, {
464
+ key: `item-${actualIndex}`,
465
+ children: h2(
466
+ import_ink2.Box,
467
+ { flexDirection: "row" },
468
+ // Arrow (clickable if functional, not highlighted)
469
+ h2(import_ink2.Text, {
470
+ key: `arrow-${actualIndex}`,
471
+ color: "white"
472
+ }, arrowPrefix),
473
+ // Selection marker space (always same width, not highlighted)
474
+ h2(import_ink2.Text, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
475
+ // Custom rendered content
476
+ renderItem(item, isSelected, displayIndex)
477
+ )
478
+ });
479
+ } else {
480
+ return h2(ScreenRow, {
481
+ key: `item-${actualIndex}`,
482
+ children: itemRenderer(item, isSelected, displayIndex, actualIndex)
483
+ });
484
+ }
485
+ })
486
+ );
487
+ }
488
+ var import_react2, import_ink2, h2;
489
+ var init_list_components = __esm({
490
+ "src/screen/list-components.ts"() {
491
+ "use strict";
492
+ import_react2 = __toESM(require("react"), 1);
493
+ import_ink2 = require("ink");
494
+ init_components();
495
+ h2 = import_react2.createElement;
496
+ }
497
+ });
498
+
499
+ // src/screen/screens.ts
500
+ function groupKeyBindings(bindings) {
501
+ const groups = {};
502
+ const enabledBindings = bindings.filter((b) => b.enabled !== false);
503
+ enabledBindings.forEach((binding) => {
504
+ const caption = typeof binding.caption === "string" ? binding.caption : "";
505
+ if (!groups[caption]) {
506
+ groups[caption] = {
507
+ keys: [],
508
+ caption,
509
+ order: binding.order || 999
510
+ };
511
+ }
512
+ groups[caption].keys.push(binding.key);
513
+ });
514
+ return Object.values(groups);
515
+ }
516
+ function formatKeyBindings(bindings, mode = "long") {
517
+ const resolvedBindings = bindings.map((binding) => {
518
+ let resolvedCaption = binding.caption;
519
+ if (typeof binding.caption === "function") {
520
+ resolvedCaption = binding.caption();
521
+ }
522
+ return {
523
+ ...binding,
524
+ resolvedCaption
525
+ };
526
+ });
527
+ const groups = groupKeyBindings(resolvedBindings.map((b) => ({
528
+ ...b,
529
+ caption: typeof b.resolvedCaption === "string" ? b.resolvedCaption : ""
530
+ })));
531
+ groups.sort((a, b) => a.order - b.order);
532
+ const items = [];
533
+ groups.forEach((group) => {
534
+ const bindingWithCustom = resolvedBindings.find(
535
+ (b) => group.keys.includes(b.key) && typeof b.resolvedCaption !== "string"
536
+ );
537
+ if (bindingWithCustom && bindingWithCustom.resolvedCaption) {
538
+ items.push(bindingWithCustom.resolvedCaption);
539
+ } else {
540
+ const keyStr = formatKeys(group.keys);
541
+ if (mode === "long") {
542
+ items.push(`${keyStr} to ${group.caption}`);
543
+ } else {
544
+ items.push(keyStr);
545
+ }
546
+ }
547
+ });
548
+ return items;
549
+ }
550
+ function formatKeys(keys) {
551
+ const keyMap = {
552
+ "escape": "esc",
553
+ "leftArrow": "\u2190",
554
+ "rightArrow": "\u2192",
555
+ "upArrow": "\u2191",
556
+ "downArrow": "\u2193",
557
+ "return": "enter"
558
+ };
559
+ return keys.map((k) => keyMap[k] || k).join("/");
560
+ }
561
+ async function showScreen(config2) {
562
+ const {
563
+ title,
564
+ onRender,
565
+ parentData = {}
566
+ } = config2;
567
+ return new Promise((resolve2) => {
568
+ let instance;
569
+ const keyBindings = [];
570
+ const actions = {};
571
+ const customFooterItems = [];
572
+ let renderResult = null;
573
+ let initialized = false;
574
+ const Screen = () => {
575
+ const [updateCounter, setUpdateCounter] = (0, import_react3.useState)(0);
576
+ if (!initialized) {
577
+ const defaultBindings = [
578
+ { key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
579
+ { key: "leftArrow", caption: "go back", action: "back", protected: false, order: 1 }
580
+ // Note: 'select' is not a default - components add it if needed
581
+ ];
582
+ defaultBindings.forEach((binding) => {
583
+ keyBindings.push(binding);
584
+ });
585
+ actions.back = () => {
586
+ cleanup(null);
587
+ };
588
+ initialized = true;
589
+ }
590
+ const context = {
591
+ setAction: (actionName, handlerFn) => {
592
+ actions[actionName] = handlerFn;
593
+ },
594
+ setKeyBinding: (bindingOrBindings) => {
595
+ const bindingsToSet = Array.isArray(bindingOrBindings) ? bindingOrBindings : [bindingOrBindings];
596
+ bindingsToSet.forEach((binding) => {
597
+ const existingIndex = keyBindings.findIndex((b) => b.key === binding.key);
598
+ if (existingIndex >= 0) {
599
+ const existing = keyBindings[existingIndex];
600
+ if (existing.protected) {
601
+ console.warn(`Cannot override protected key: ${binding.key}`);
602
+ return;
603
+ }
604
+ keyBindings[existingIndex] = {
605
+ ...existing,
606
+ ...binding,
607
+ order: binding.order !== void 0 ? binding.order : existing.order,
608
+ enabled: binding.enabled !== void 0 ? binding.enabled : existing.enabled !== void 0 ? existing.enabled : true
609
+ };
610
+ } else {
611
+ keyBindings.push({
612
+ protected: false,
613
+ order: 999,
614
+ enabled: true,
615
+ ...binding
616
+ });
617
+ }
618
+ });
619
+ },
620
+ updateKeyBinding: (keyName, updates) => {
621
+ const index = keyBindings.findIndex((b) => b.key === keyName);
622
+ if (index >= 0) {
623
+ keyBindings[index] = {
624
+ ...keyBindings[index],
625
+ ...updates
626
+ };
627
+ }
628
+ },
629
+ removeKeyBinding: (keyName) => {
630
+ const index = keyBindings.findIndex((b) => b.key === keyName);
631
+ if (index >= 0) {
632
+ if (keyBindings[index].protected) {
633
+ console.warn(`Cannot remove protected key: ${keyName}`);
634
+ return;
635
+ }
636
+ keyBindings.splice(index, 1);
637
+ }
638
+ },
639
+ addFooter: (item) => {
640
+ customFooterItems.push(item);
641
+ },
642
+ clearFooter: () => {
643
+ customFooterItems.length = 0;
644
+ },
645
+ setFooter: (items) => {
646
+ customFooterItems.length = 0;
647
+ const itemsArray = Array.isArray(items) ? items : [items];
648
+ customFooterItems.push(...itemsArray);
649
+ },
650
+ update: () => {
651
+ setUpdateCounter((c) => c + 1);
652
+ },
653
+ goBack: () => {
654
+ if (actions.back) {
655
+ actions.back();
656
+ }
657
+ },
658
+ close: (result) => {
659
+ cleanup(result);
660
+ },
661
+ parentData
662
+ };
663
+ if (!renderResult) {
664
+ renderResult = onRender(context);
665
+ }
666
+ (0, import_ink3.useInput)((input, key) => {
667
+ if (key.ctrl && input === "c") {
668
+ cleanup(null);
669
+ process.exit(0);
670
+ return;
671
+ }
672
+ let matchedBinding = null;
673
+ for (const binding of keyBindings) {
674
+ let keyMatches = false;
675
+ if (key[binding.key]) {
676
+ keyMatches = true;
677
+ } else if (input === binding.key) {
678
+ keyMatches = true;
679
+ }
680
+ if (keyMatches) {
681
+ if (binding.enabled === false) {
682
+ continue;
683
+ }
684
+ if (binding.condition && !binding.condition(context)) {
685
+ continue;
686
+ }
687
+ matchedBinding = binding;
688
+ break;
689
+ }
690
+ }
691
+ if (matchedBinding && actions[matchedBinding.action]) {
692
+ const actionResult = actions[matchedBinding.action]({
693
+ input,
694
+ key,
695
+ binding: matchedBinding
696
+ });
697
+ }
698
+ });
699
+ const footerLines = [];
700
+ const bindingItems = formatKeyBindings(keyBindings, "long");
701
+ if (bindingItems.length > 0) {
702
+ const bindingsLine = [];
703
+ bindingItems.forEach((item, idx) => {
704
+ if (idx > 0) {
705
+ bindingsLine.push(", ");
706
+ }
707
+ bindingsLine.push(item);
708
+ });
709
+ const allStrings = bindingItems.every((item) => typeof item === "string");
710
+ if (allStrings) {
711
+ footerLines.push(bindingsLine.join(""));
712
+ } else {
713
+ const wrappedBindingsLine = bindingsLine.map(
714
+ (item) => typeof item === "string" ? (0, import_react3.createElement)(import_ink3.Text, {}, item) : item
715
+ );
716
+ footerLines.push(wrappedBindingsLine);
717
+ }
718
+ }
719
+ customFooterItems.forEach((item) => {
720
+ if (typeof item === "string") {
721
+ footerLines.push(item);
722
+ } else {
723
+ footerLines.push(item);
724
+ }
725
+ });
726
+ return (0, import_react3.createElement)(
727
+ ScreenContainer,
728
+ {},
729
+ (0, import_react3.createElement)(ScreenTitle, { text: title }),
730
+ (0, import_react3.createElement)(ScreenDivider),
731
+ (0, import_react3.createElement)(ScreenRow, {}, (0, import_react3.createElement)(import_ink3.Text, {}, " ")),
732
+ renderResult,
733
+ (0, import_react3.createElement)(ScreenRow, {}, (0, import_react3.createElement)(import_ink3.Text, {}, " ")),
734
+ (0, import_react3.createElement)(ScreenDivider),
735
+ (0, import_react3.createElement)(ScreenFooter, { lines: footerLines })
736
+ );
737
+ };
738
+ const cleanup = (result) => {
739
+ if (instance) instance.unmount();
740
+ setTimeout(() => resolve2(result), 50);
741
+ };
742
+ instance = (0, import_ink3.render)((0, import_react3.createElement)(Screen));
743
+ });
744
+ }
745
+ async function showListScreen(config2) {
746
+ const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker } = config2;
747
+ return showScreen({
748
+ title,
749
+ parentData,
750
+ onRender: (ctx) => {
751
+ const selectedIndexRef = { current: initialSelectedIndex };
752
+ ctx.setAction("select", () => {
753
+ const selected = items[selectedIndexRef.current];
754
+ if (onSelect) {
755
+ const result = onSelect(selected.value, selectedIndexRef.current);
756
+ ctx.close(result);
757
+ }
758
+ });
759
+ if (onEscape) {
760
+ ctx.setAction("back", () => {
761
+ const result = onEscape(selectedIndexRef.current);
762
+ ctx.close(result);
763
+ });
764
+ }
765
+ ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
766
+ return (0, import_react3.createElement)(ListComponent, { items, ctx, selectedIndexRef, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker });
767
+ }
768
+ });
769
+ }
770
+ async function showMultiColumnListScreen(config2) {
771
+ const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config2;
772
+ return showScreen({
773
+ title,
774
+ parentData,
775
+ onRender: (ctx) => {
776
+ const selectedIndexRef = { current: initialSelectedIndex };
777
+ ctx.setAction("select", () => {
778
+ const selected = items[selectedIndexRef.current];
779
+ if (onSelect) {
780
+ const result = onSelect(selected, selectedIndexRef.current);
781
+ ctx.close(result);
782
+ }
783
+ });
784
+ if (onEscape) {
785
+ ctx.setAction("back", () => {
786
+ const result = onEscape(selectedIndexRef.current);
787
+ ctx.close(result);
788
+ });
789
+ }
790
+ ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
791
+ return (0, import_react3.createElement)(MultiColumnListComponent, { items, ctx, selectedIndexRef });
792
+ }
793
+ });
794
+ }
795
+ async function showMultiColumnListWithPreviewScreen(config2) {
796
+ const { title, items, getPreviewContent, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config2;
797
+ return showScreen({
798
+ title,
799
+ parentData,
800
+ onRender: (ctx) => {
801
+ const selectedIndexRef = { current: initialSelectedIndex };
802
+ ctx.setAction("select", () => {
803
+ const selected = items[selectedIndexRef.current];
804
+ if (onSelect) {
805
+ const result = onSelect(selected, selectedIndexRef.current);
806
+ ctx.close(result);
807
+ }
808
+ });
809
+ if (onEscape) {
810
+ ctx.setAction("back", () => {
811
+ const result = onEscape(selectedIndexRef.current);
812
+ ctx.close(result);
813
+ });
814
+ }
815
+ ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
816
+ return (0, import_react3.createElement)(MultiColumnListWithPreviewComponent, { items, getPreviewContent, ctx, selectedIndexRef });
817
+ }
818
+ });
819
+ }
820
+ var import_react3, import_ink3, showMenuScreen, showWordGridScreen;
821
+ var init_screens = __esm({
822
+ "src/screen/screens.ts"() {
823
+ "use strict";
824
+ import_react3 = require("react");
825
+ import_ink3 = require("ink");
826
+ init_components();
827
+ init_list_components();
828
+ showMenuScreen = showListScreen;
829
+ showWordGridScreen = showMultiColumnListScreen;
830
+ }
831
+ });
832
+
833
+ // src/screen/ui-elements.ts
834
+ function ListItem({
835
+ children,
836
+ isSelected = false,
837
+ color = "white",
838
+ backgroundColor,
839
+ bold = false,
840
+ dimColor = false
841
+ }) {
842
+ return (0, import_react4.createElement)(
843
+ import_ink4.Box,
844
+ {},
845
+ (0, import_react4.createElement)(import_ink4.Text, {
846
+ color: isSelected ? backgroundColor || "green" : color,
847
+ backgroundColor: isSelected ? color : backgroundColor,
848
+ bold: isSelected || bold,
849
+ dimColor: !isSelected && dimColor
850
+ }, children)
851
+ );
852
+ }
853
+ function TextBlock({
854
+ text,
855
+ color = "white",
856
+ dimmed = false,
857
+ bold = false,
858
+ maxWidth
859
+ }) {
860
+ return (0, import_react4.createElement)(
861
+ import_ink4.Box,
862
+ {},
863
+ (0, import_react4.createElement)(import_ink4.Text, {
864
+ color,
865
+ dimColor: dimmed,
866
+ bold
867
+ }, text)
868
+ );
869
+ }
870
+ function Divider({ character = "\u2500", width = 80 }) {
871
+ return (0, import_react4.createElement)(
872
+ import_ink4.Box,
873
+ { marginY: 1 },
874
+ (0, import_react4.createElement)(import_ink4.Text, { dimColor: true }, character.repeat(width))
875
+ );
876
+ }
877
+ function GridCell({
878
+ children,
879
+ width,
880
+ color = "white",
881
+ backgroundColor,
882
+ bold = false,
883
+ dimColor = false,
884
+ align = "left"
885
+ }) {
886
+ return (0, import_react4.createElement)(
887
+ import_ink4.Box,
888
+ { width },
889
+ (0, import_react4.createElement)(import_ink4.Text, {
890
+ color,
891
+ backgroundColor,
892
+ bold,
893
+ dimColor,
894
+ textAlign: align
895
+ }, children)
896
+ );
897
+ }
898
+ function InputField({ prompt, value, onChange, onSubmit }) {
899
+ return (0, import_react4.createElement)(
900
+ import_ink4.Box,
901
+ { flexDirection: "column" },
902
+ (0, import_react4.createElement)(import_ink4.Text, {}, prompt),
903
+ (0, import_react4.createElement)(
904
+ import_ink4.Box,
905
+ { marginTop: 1 },
906
+ (0, import_react4.createElement)(import_ink4.Text, { color: "cyan" }, " > ", value, "_")
907
+ )
908
+ );
909
+ }
910
+ var import_react4, import_ink4;
911
+ var init_ui_elements = __esm({
912
+ "src/screen/ui-elements.ts"() {
913
+ "use strict";
914
+ import_react4 = require("react");
915
+ import_ink4 = require("ink");
916
+ }
917
+ });
918
+
919
+ // src/screen/utils.ts
920
+ function buildBreadcrumb(parts) {
921
+ if (parts.length === 0) return "";
922
+ if (parts.length === 1) return parts[0];
923
+ return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
924
+ }
925
+ function buildDetailBreadcrumb(path, suffix = "") {
926
+ if (path.length <= 1) {
927
+ return suffix ? `\u2190 ${suffix}` : path[0] || "";
928
+ }
929
+ const breadcrumb = buildBreadcrumb(path);
930
+ return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
931
+ }
932
+ var init_utils = __esm({
933
+ "src/screen/utils.ts"() {
934
+ "use strict";
935
+ }
936
+ });
937
+
938
+ // src/screen/footer-builder.ts
939
+ function buildFooter(config2 = {}) {
940
+ const {
941
+ navigation = null,
942
+ actions = null,
943
+ info = null,
944
+ escape = "Esc to go back",
945
+ custom = null
946
+ } = config2;
947
+ const lines = [];
948
+ const mainParts = [];
949
+ if (navigation) {
950
+ mainParts.push(navigation);
951
+ }
952
+ if (actions) {
953
+ mainParts.push(actions);
954
+ }
955
+ if (escape) {
956
+ mainParts.push(escape);
957
+ }
958
+ if (mainParts.length > 0) {
959
+ lines.push(mainParts.join(", "));
960
+ }
961
+ if (info) {
962
+ const infoLines = Array.isArray(info) ? info : [info];
963
+ lines.push(...infoLines);
964
+ }
965
+ if (custom) {
966
+ const customLines = Array.isArray(custom) ? custom : [custom];
967
+ lines.push(...customLines);
968
+ }
969
+ return lines;
970
+ }
971
+ function organizeFooterMessages(messages) {
972
+ if (!messages || messages.length === 0) {
973
+ return ["Esc to go back"];
974
+ }
975
+ const navigation = messages.filter((m) => m.includes("\u2191") || m.includes("\u2193") || m.includes("\u2190") || m.includes("\u2192"));
976
+ const actions = messages.filter((m) => m.includes("Enter") || m.includes("select") || m.includes("submit"));
977
+ const escape = messages.filter((m) => m.includes("Esc"));
978
+ const others = messages.filter(
979
+ (m) => !navigation.includes(m) && !actions.includes(m) && !escape.includes(m)
980
+ );
981
+ const lines = [];
982
+ const mainLine = [...navigation, ...actions, ...escape].join(", ");
983
+ if (mainLine) lines.push(mainLine);
984
+ lines.push(...others);
985
+ return lines;
986
+ }
987
+ var FooterPresets;
988
+ var init_footer_builder = __esm({
989
+ "src/screen/footer-builder.ts"() {
990
+ "use strict";
991
+ FooterPresets = {
992
+ /**
993
+ * Menu screen footer
994
+ */
995
+ menu: (customInfo = null) => buildFooter({
996
+ navigation: "\u2191/\u2193 to navigate",
997
+ actions: "Enter to select",
998
+ escape: "Esc to go back",
999
+ info: customInfo
1000
+ }),
1001
+ /**
1002
+ * Word grid footer
1003
+ */
1004
+ wordGrid: (totalWords) => buildFooter({
1005
+ navigation: "\u2191\u2193\u2190\u2192 to navigate",
1006
+ actions: "Enter to select",
1007
+ escape: "Esc to go back",
1008
+ info: `Total: ${totalWords} words`
1009
+ }),
1010
+ /**
1011
+ * Text input footer
1012
+ */
1013
+ textInput: () => buildFooter({
1014
+ actions: "Type and press Enter to submit",
1015
+ escape: "Esc to cancel"
1016
+ }),
1017
+ /**
1018
+ * Info/static screen footer
1019
+ */
1020
+ info: () => buildFooter({
1021
+ escape: "Esc to continue"
1022
+ }),
1023
+ /**
1024
+ * Main menu footer (escape exits)
1025
+ */
1026
+ mainMenu: () => buildFooter({
1027
+ navigation: "\u2191/\u2193 to navigate",
1028
+ actions: "Enter to select",
1029
+ escape: "Esc to exit"
1030
+ }),
1031
+ /**
1032
+ * Action menu footer (for word cards, etc.)
1033
+ */
1034
+ actionMenu: (hasAudio = false) => {
1035
+ const parts = buildFooter({
1036
+ navigation: "\u2191/\u2193 to navigate",
1037
+ actions: "Enter to select",
1038
+ escape: "Esc to go back"
1039
+ });
1040
+ if (hasAudio) {
1041
+ parts.push("Audio available");
1042
+ }
1043
+ return parts;
1044
+ }
1045
+ };
1046
+ }
1047
+ });
1048
+
1049
+ // src/screen/index.ts
1050
+ var screen_exports = {};
1051
+ __export(screen_exports, {
1052
+ Box: () => import_ink5.Box,
1053
+ Divider: () => Divider,
1054
+ FooterPresets: () => FooterPresets,
1055
+ GridCell: () => GridCell,
1056
+ InputField: () => InputField,
1057
+ ListComponent: () => ListComponent,
1058
+ ListItem: () => ListItem,
1059
+ MultiColumnListComponent: () => MultiColumnListComponent,
1060
+ MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
1061
+ React: () => import_react5.default,
1062
+ ScreenBody: () => ScreenBody,
1063
+ ScreenContainer: () => ScreenContainer,
1064
+ ScreenDivider: () => ScreenDivider,
1065
+ ScreenFooter: () => ScreenFooter,
1066
+ ScreenRow: () => ScreenRow,
1067
+ ScreenTitle: () => ScreenTitle,
1068
+ Text: () => import_ink5.Text,
1069
+ TextBlock: () => TextBlock,
1070
+ buildBreadcrumb: () => buildBreadcrumb,
1071
+ buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1072
+ buildFooter: () => buildFooter,
1073
+ h: () => import_react5.createElement,
1074
+ load: () => load,
1075
+ organizeFooterMessages: () => organizeFooterMessages,
1076
+ showListScreen: () => showListScreen,
1077
+ showMenuScreen: () => showMenuScreen,
1078
+ showMultiColumnListScreen: () => showMultiColumnListScreen,
1079
+ showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
1080
+ showScreen: () => showScreen,
1081
+ showWordGridScreen: () => showWordGridScreen,
1082
+ useCallback: () => import_react5.useCallback,
1083
+ useEffect: () => import_react5.useEffect,
1084
+ useInput: () => import_ink5.useInput,
1085
+ useMemo: () => import_react5.useMemo,
1086
+ useRef: () => import_react5.useRef,
1087
+ useState: () => import_react5.useState
1088
+ });
1089
+ async function load() {
1090
+ if (loadPromise) return loadPromise;
1091
+ loadPromise = Promise.all([
1092
+ import("react"),
1093
+ import("ink")
1094
+ ]).then(() => {
1095
+ });
1096
+ return loadPromise;
1097
+ }
1098
+ var import_react5, import_ink5, loadPromise;
1099
+ var init_screen = __esm({
1100
+ "src/screen/index.ts"() {
1101
+ "use strict";
1102
+ import_react5 = __toESM(require("react"), 1);
1103
+ import_ink5 = require("ink");
1104
+ init_screens();
1105
+ init_list_components();
1106
+ init_components();
1107
+ init_ui_elements();
1108
+ init_utils();
1109
+ init_footer_builder();
1110
+ loadPromise = null;
1111
+ if (typeof window === "undefined") {
1112
+ load().catch(() => {
1113
+ });
1114
+ }
1115
+ }
1116
+ });
1117
+
30
1118
  // src/init.ts
31
1119
  var init_exports = {};
32
1120
  __export(init_exports, {
@@ -39,7 +1127,7 @@ module.exports = __toCommonJS(init_exports);
39
1127
  var import_fs = require("fs");
40
1128
  var import_path = require("path");
41
1129
  var import_dotenv = require("dotenv");
42
- var Args = class {
1130
+ var Args = class _Args {
43
1131
  args = {};
44
1132
  flags = {};
45
1133
  options = {};
@@ -54,10 +1142,13 @@ var Args = class {
54
1142
  configsLoaded = [];
55
1143
  env = "local";
56
1144
  constructor(config2 = {}) {
57
- this.aliases = config2.aliases || {};
58
- this.overrides = config2.overrides || {};
59
- this.defaults = config2.defaults || {};
60
- this.prefixes = config2.prefixes || ["not", "no"];
1145
+ this.aliases = {};
1146
+ this.overrides = {};
1147
+ this.defaults = {};
1148
+ this.prefixes = ["not", "no"];
1149
+ if (Object.keys(config2).length > 0) {
1150
+ this.configure(config2);
1151
+ }
61
1152
  const args = config2.args || process.argv.slice(2);
62
1153
  this.parseArgs(args);
63
1154
  this.env = this.get("env")?.toLowerCase() || "local";
@@ -65,6 +1156,33 @@ var Args = class {
65
1156
  this.loadConfigFiles();
66
1157
  this.checkConflicts();
67
1158
  }
1159
+ /**
1160
+ * Configure Args options
1161
+ * Only parameters present in config are updated
1162
+ * Note: Args is special - it's initialized first, so it can't take context
1163
+ */
1164
+ configure(config2) {
1165
+ if (config2.aliases !== void 0) {
1166
+ this.aliases = config2.aliases;
1167
+ }
1168
+ if (config2.overrides !== void 0) {
1169
+ this.overrides = config2.overrides;
1170
+ }
1171
+ if (config2.defaults !== void 0) {
1172
+ this.defaults = config2.defaults;
1173
+ }
1174
+ if (config2.prefixes !== void 0) {
1175
+ this.prefixes = config2.prefixes;
1176
+ }
1177
+ }
1178
+ /**
1179
+ * Initialize Args instance
1180
+ * Note: Args is special - it's initialized first, so it can't take context
1181
+ * This static method is for consistency with other components
1182
+ */
1183
+ static init(config2 = {}) {
1184
+ return new _Args(config2);
1185
+ }
68
1186
  /**
69
1187
  * Parse command line arguments
70
1188
  */
@@ -580,18 +1698,76 @@ var joiStringArrayType = (type) => (value, helpers) => {
580
1698
  };
581
1699
 
582
1700
  // src/params/index.ts
583
- var Params = class {
1701
+ var Params = class _Params {
1702
+ context;
1703
+ // Partial context during initialization
584
1704
  params = {};
585
1705
  definitions = {};
586
1706
  args;
587
1707
  paramSetters = [];
588
1708
  paramGetters = [];
589
- constructor({ args }, opts = {}) {
590
- this.args = args;
591
- for (const [k, v] of Object.entries(opts)) {
1709
+ trackedParams = [];
1710
+ constructor(context, options = {}) {
1711
+ this.context = context;
1712
+ this.args = context.args;
1713
+ if (Object.keys(options).length > 0) {
1714
+ this.configure(options);
1715
+ }
1716
+ }
1717
+ /**
1718
+ * Configure parameters
1719
+ * Only parameters present in options are updated
1720
+ */
1721
+ configure(options) {
1722
+ for (const [k, v] of Object.entries(options)) {
592
1723
  this.params[k] = v;
593
1724
  }
594
1725
  }
1726
+ /**
1727
+ * Initialize Params from context and CLI parameters
1728
+ * Note: Params is special - it's initialized early with partial context
1729
+ */
1730
+ static init(context, options) {
1731
+ return new _Params(context, options || {});
1732
+ }
1733
+ /**
1734
+ * Track a parameter request for --stopAfter=init feature
1735
+ */
1736
+ trackParam(key, definition, value, source) {
1737
+ this.trackedParams.push({
1738
+ key,
1739
+ definition,
1740
+ value,
1741
+ source
1742
+ });
1743
+ }
1744
+ /**
1745
+ * Get all tracked parameters (for --stopAfter=init)
1746
+ */
1747
+ getTrackedParams() {
1748
+ return [...this.trackedParams];
1749
+ }
1750
+ /**
1751
+ * Get all figured parameters as a record
1752
+ * Returns all parameters that were collected during initialization,
1753
+ * whether from CLI args, options, or defaults
1754
+ */
1755
+ getAllFigured() {
1756
+ const result = {};
1757
+ for (const param of this.trackedParams) {
1758
+ result[param.key] = {
1759
+ value: param.value,
1760
+ source: param.source
1761
+ };
1762
+ }
1763
+ return result;
1764
+ }
1765
+ /**
1766
+ * Clear tracked parameters
1767
+ */
1768
+ clearTrackedParams() {
1769
+ this.trackedParams = [];
1770
+ }
595
1771
  /**
596
1772
  * Assign a parameter definition
597
1773
  */
@@ -665,6 +1841,8 @@ var Params = class {
665
1841
  type = type.default(defValObj.value);
666
1842
  } else if (str.match(/required/)) {
667
1843
  type = type.required();
1844
+ } else {
1845
+ type = type.optional();
668
1846
  }
669
1847
  return type;
670
1848
  }
@@ -672,7 +1850,12 @@ var Params = class {
672
1850
  * Validate a value against a definition
673
1851
  */
674
1852
  validate(key, val, def) {
675
- const { value, error } = def.type.validate(val, { context: { params: this.params } });
1853
+ const normalizedVal = val === null ? void 0 : val;
1854
+ const { value, error } = def.type.validate(normalizedVal, {
1855
+ context: { params: this.params },
1856
+ abortEarly: false,
1857
+ allowUnknown: false
1858
+ });
676
1859
  if (error) {
677
1860
  const errs = error.details.map((el) => el.message).join(", ");
678
1861
  throw new ParamError(`"${key}" validation error: ${errs}`);
@@ -690,11 +1873,26 @@ var Params = class {
690
1873
  }
691
1874
  const valFromArgs = this.args.get(key);
692
1875
  const valFromParams = this.params[key];
693
- const res = valFromGetters ? this.validate(key, valFromGetters, def) : valFromArgs ? this.validate(key, valFromArgs, def) : this.validate(key, valFromParams, def);
694
- if (res !== void 0 && def.values && !def.values.includes(res)) {
1876
+ let source = "default";
1877
+ let value;
1878
+ if (valFromGetters !== void 0 && valFromGetters !== null) {
1879
+ value = this.validate(key, valFromGetters, def);
1880
+ source = "options";
1881
+ } else if (valFromArgs !== void 0 && valFromArgs !== null) {
1882
+ value = this.validate(key, valFromArgs, def);
1883
+ source = "cli";
1884
+ } else if (valFromParams !== void 0 && valFromParams !== null) {
1885
+ value = this.validate(key, valFromParams, def);
1886
+ source = "options";
1887
+ } else {
1888
+ value = this.validate(key, void 0, def);
1889
+ source = "default";
1890
+ }
1891
+ this.trackParam(key, definition || "string", value, source);
1892
+ if (value !== void 0 && def.values && !def.values.includes(value)) {
695
1893
  throw new ParamError(`key ${key} should be one of ${def.values}`);
696
1894
  }
697
- return res;
1895
+ return value;
698
1896
  }
699
1897
  /**
700
1898
  * Set a parameter value with validation
@@ -728,10 +1926,10 @@ var Params = class {
728
1926
  * Run all registered getters for a key
729
1927
  */
730
1928
  runAllRegisteredGetters(key) {
731
- let val = null;
1929
+ let val = void 0;
732
1930
  for (const getter of this.paramGetters) {
733
1931
  val = getter(key, this.definitions[key]);
734
- if (val !== void 0) {
1932
+ if (val !== void 0 && val !== null) {
735
1933
  break;
736
1934
  }
737
1935
  }
@@ -802,6 +2000,7 @@ var ALL_LEVELS = [
802
2000
  "response",
803
2001
  "progress"
804
2002
  ];
2003
+ var MAX_LEVEL_LENGTH = Math.max(...ALL_LEVELS.map((level) => level.toUpperCase().length));
805
2004
  var LEVEL_COLORS = {
806
2005
  error: import_chalk.default.red.bold,
807
2006
  warn: import_chalk.default.rgb(255, 165, 0),
@@ -815,13 +2014,104 @@ var LEVEL_COLORS = {
815
2014
  progress: import_chalk.default.green,
816
2015
  results: import_chalk.default.magenta
817
2016
  };
818
- var CliToolkitLogger = class {
2017
+ var Logger = class _Logger {
2018
+ context;
2019
+ // Partial context during initialization
819
2020
  options;
820
2021
  transport;
821
2022
  startTimes = {};
822
2023
  lastProgressTimes = {};
823
- constructor(options = {}) {
824
- this.options = this.normalizeOptions(options);
2024
+ constructor(context, options = {}) {
2025
+ this.context = context;
2026
+ this.options = this.getDefaultOptions();
2027
+ if (options) {
2028
+ this.configure(options);
2029
+ }
2030
+ this.updateTransport();
2031
+ }
2032
+ /**
2033
+ * Configure logger options
2034
+ * Only parameters present in options are updated
2035
+ */
2036
+ configure(options) {
2037
+ if (options.mode !== void 0) {
2038
+ this.options.mode = this.isValidMode(options.mode) ? options.mode : "text";
2039
+ }
2040
+ if (options.route !== void 0) {
2041
+ this.options.route = options.route;
2042
+ this.updateTransport();
2043
+ }
2044
+ if (options.prefix !== void 0) {
2045
+ this.options.prefix = options.prefix;
2046
+ }
2047
+ if (options.silent !== void 0) {
2048
+ this.options.silent = options.silent;
2049
+ }
2050
+ if (options.showLevel !== void 0) {
2051
+ this.options.showLevel = options.showLevel;
2052
+ }
2053
+ if (options.timestamp !== void 0) {
2054
+ this.options.timestamp = options.timestamp;
2055
+ }
2056
+ if (options.levels !== void 0) {
2057
+ this.options.levels = this.normalizeLevels(options.levels);
2058
+ }
2059
+ if (options.progress !== void 0) {
2060
+ if (options.progress.withTimes !== void 0) {
2061
+ this.options.progressTimes = options.progress.withTimes;
2062
+ }
2063
+ if (options.progress.throttleMs !== void 0) {
2064
+ this.options.progressThrottle = options.progress.throttleMs;
2065
+ }
2066
+ }
2067
+ }
2068
+ /**
2069
+ * Initialize logger from context and CLI parameters
2070
+ */
2071
+ static init(context, options) {
2072
+ const paramDefs = {
2073
+ mode: "string default text",
2074
+ route: "string default console",
2075
+ prefix: "string",
2076
+ silent: "boolean default false",
2077
+ showLevel: "boolean default true",
2078
+ timestamp: "boolean default false",
2079
+ levels: "string",
2080
+ progressWithTimes: "boolean default false",
2081
+ progressThrottleMs: "number"
2082
+ };
2083
+ const cliParams = context.params.getAll(paramDefs);
2084
+ const config2 = {
2085
+ mode: options?.mode ?? cliParams.mode,
2086
+ route: options?.route ?? cliParams.route,
2087
+ prefix: options?.prefix ?? cliParams.prefix,
2088
+ silent: options?.silent ?? cliParams.silent,
2089
+ showLevel: options?.showLevel ?? cliParams.showLevel,
2090
+ timestamp: options?.timestamp ?? cliParams.timestamp,
2091
+ levels: options?.levels ?? (cliParams.levels ? cliParams.levels.split(",") : void 0),
2092
+ progress: options?.progress ?? {
2093
+ withTimes: cliParams.progressWithTimes,
2094
+ throttleMs: cliParams.progressThrottleMs
2095
+ }
2096
+ };
2097
+ const logger = new _Logger(context, config2);
2098
+ context.logger = logger;
2099
+ return logger;
2100
+ }
2101
+ getDefaultOptions() {
2102
+ return {
2103
+ mode: "text",
2104
+ route: this.shouldUseIpcRoute() ? "ipc" : "console",
2105
+ prefix: void 0,
2106
+ silent: false,
2107
+ showLevel: true,
2108
+ timestamp: false,
2109
+ levels: ALL_LEVELS,
2110
+ progressTimes: false,
2111
+ progressThrottle: void 0
2112
+ };
2113
+ }
2114
+ updateTransport() {
825
2115
  this.transport = this.options.route === "ipc" ? new ParentProcessTransport() : new ConsoleTransport();
826
2116
  }
827
2117
  setMode(mode) {
@@ -930,7 +2220,7 @@ var CliToolkitLogger = class {
930
2220
  parts.push(now.toISOString());
931
2221
  }
932
2222
  if (this.options.showLevel) {
933
- parts.push(struct.level.toUpperCase());
2223
+ parts.push(struct.level.toUpperCase().padEnd(MAX_LEVEL_LENGTH));
934
2224
  }
935
2225
  if (struct.level === "progress") {
936
2226
  if (struct.prefix) {
@@ -964,22 +2254,6 @@ var CliToolkitLogger = class {
964
2254
  inspectChunks(chunks) {
965
2255
  return chunks.map((chunk) => import_util.default.inspect(chunk, { colors: true, depth: null })).join(" ");
966
2256
  }
967
- normalizeOptions(options) {
968
- const { route, mode, prefix, silent, showLevel, timestamp, levels, progress } = options;
969
- const shouldUseIpc = this.shouldUseIpcRoute();
970
- const normalized = {
971
- mode: this.isValidMode(mode) ? mode : "text",
972
- route: route ?? (shouldUseIpc ? "ipc" : "console"),
973
- prefix,
974
- silent: silent ?? false,
975
- showLevel: showLevel ?? true,
976
- timestamp: timestamp ?? false,
977
- levels: this.normalizeLevels(levels),
978
- progressTimes: progress?.withTimes ?? false,
979
- progressThrottle: progress?.throttleMs
980
- };
981
- return normalized;
982
- }
983
2257
  shouldUseIpcRoute() {
984
2258
  if (process.env.VITEST || process.env.NODE_ENV === "test") {
985
2259
  return false;
@@ -1010,35 +2284,44 @@ var CliToolkitLogger = class {
1010
2284
 
1011
2285
  // src/init/index.ts
1012
2286
  var import_events = require("events");
2287
+ function extractComponentOptions(opts, componentName) {
2288
+ const reservedKeys = ["overrides", "defaults", "modules"];
2289
+ const componentOptions = {};
2290
+ for (const [key, value] of Object.entries(opts)) {
2291
+ if (!reservedKeys.includes(key)) {
2292
+ componentOptions[key] = value;
2293
+ }
2294
+ }
2295
+ return componentOptions;
2296
+ }
1013
2297
  function setup(opts = {}) {
1014
- const args = new Args({
2298
+ const args = Args.init({
1015
2299
  overrides: opts.overrides || {},
1016
2300
  defaults: opts.defaults || {}
1017
2301
  });
1018
- const params = new Params({ args }, opts.overrides || {});
1019
- const loggerOptions = opts.logger || {};
1020
- const logger = new CliToolkitLogger({
1021
- mode: loggerOptions.mode || "text",
1022
- route: loggerOptions.route || "console",
1023
- prefix: loggerOptions.prefix,
1024
- silent: loggerOptions.silent,
1025
- showLevel: loggerOptions.showLevel,
1026
- timestamp: loggerOptions.timestamp,
1027
- levels: loggerOptions.levels
1028
- });
1029
- const cleanupFunctions = [];
1030
- const context = {
2302
+ const partialContext = {
1031
2303
  args,
1032
- params,
1033
- logger,
1034
2304
  emitter: new import_events.EventEmitter(),
1035
2305
  isStop: () => false,
1036
- // Will be set in init function
1037
- cleanupFunctions,
2306
+ cleanupFunctions: [],
1038
2307
  registerCleanup: (fn) => {
1039
- cleanupFunctions.push(fn);
2308
+ partialContext.cleanupFunctions.push(fn);
1040
2309
  }
1041
2310
  };
2311
+ const params = Params.init(partialContext, opts.overrides || {});
2312
+ partialContext.params = params;
2313
+ const loggerOptions = extractComponentOptions(opts, "logger");
2314
+ const logger = Logger.init(partialContext, loggerOptions);
2315
+ partialContext.logger = logger;
2316
+ const context = {
2317
+ args,
2318
+ params,
2319
+ logger,
2320
+ emitter: partialContext.emitter,
2321
+ isStop: partialContext.isStop,
2322
+ cleanupFunctions: partialContext.cleanupFunctions,
2323
+ registerCleanup: partialContext.registerCleanup
2324
+ };
1042
2325
  logger.debug("[setup] completed successfully");
1043
2326
  return context;
1044
2327
  }
@@ -1049,13 +2332,46 @@ async function setupModules(context, opts = {}) {
1049
2332
  context.logger.debug("[setupModules] completed successfully");
1050
2333
  return context;
1051
2334
  }
2335
+ function printAllParameters(context) {
2336
+ const trackedParams = context.params.getTrackedParams();
2337
+ console.log("\n=== All Figured Parameters ===");
2338
+ console.log("\nComponent: Logger");
2339
+ const loggerParams = trackedParams.filter(
2340
+ (p) => ["mode", "route", "prefix", "silent", "showLevel", "timestamp", "levels"].includes(p.key)
2341
+ );
2342
+ if (loggerParams.length > 0) {
2343
+ loggerParams.forEach((p) => {
2344
+ console.log(` ${p.key}: ${JSON.stringify(p.value)} (from ${p.source})`);
2345
+ });
2346
+ } else {
2347
+ console.log(" (no parameters requested)");
2348
+ }
2349
+ console.log("\n=== End Parameters ===\n");
2350
+ }
1052
2351
  async function init(flow, opts = {}) {
1053
2352
  let stop = false;
1054
2353
  let context = null;
1055
2354
  try {
2355
+ try {
2356
+ const screenModule = await Promise.resolve().then(() => (init_screen(), screen_exports));
2357
+ if (screenModule && typeof screenModule.load === "function") {
2358
+ await screenModule.load();
2359
+ }
2360
+ } catch {
2361
+ if (typeof require !== "undefined") {
2362
+ try {
2363
+ } catch {
2364
+ }
2365
+ }
2366
+ }
1056
2367
  context = setup(opts);
1057
2368
  context.isStop = () => stop;
1058
2369
  context = await setupModules(context, opts);
2370
+ const stopAfter = context.args.get("stopAfter");
2371
+ if (stopAfter === "init") {
2372
+ printAllParameters(context);
2373
+ process.exit(0);
2374
+ }
1059
2375
  process.on("SIGINT", async () => {
1060
2376
  if (stop) {
1061
2377
  context.logger.warn("[process] killed");
@@ -1073,14 +2389,21 @@ async function init(flow, opts = {}) {
1073
2389
  await flow(context);
1074
2390
  } catch (error) {
1075
2391
  const errorLocation = error instanceof Error && error.stack ? error.stack.split("\n")[1]?.trim() || "Unknown location" : "Unknown location";
2392
+ const logError = (msg, ...args) => {
2393
+ if (context?.logger) {
2394
+ context.logger.error(msg, ...args);
2395
+ } else {
2396
+ console.error(msg, ...args);
2397
+ }
2398
+ };
1076
2399
  if (error instanceof ParamError) {
1077
- context?.logger.error(`[params]: ${error.message} (${errorLocation})`);
2400
+ logError(`[params]: ${error.message} (${errorLocation})`);
1078
2401
  process.exitCode = 3;
1079
2402
  } else if (error instanceof InitError) {
1080
- context?.logger.error(`[init]: ${error.message} (${errorLocation})`);
2403
+ logError(`[init]: ${error.message} (${errorLocation})`);
1081
2404
  process.exitCode = 4;
1082
2405
  } else {
1083
- context?.logger.error(`[other] error:`, error, errorLocation);
2406
+ logError(`[other] error:`, error, errorLocation);
1084
2407
  process.exitCode = 5;
1085
2408
  }
1086
2409
  } finally {