@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/index.cjs CHANGED
@@ -1,10 +1,21 @@
1
1
  "use strict";
2
+
3
+ var __esmCache = {};
4
+ var __loadESMSync = function(moduleName) {
5
+ if (!__esmCache[moduleName]) {
6
+ throw new Error(`ESM module "${moduleName}" not loaded. Please call the load() function first: const toolkit = require("@nmakarov/cli-toolkit"); await toolkit.load();`);
7
+ }
8
+ return __esmCache[moduleName];
9
+ };
2
10
  var __create = Object.create;
3
11
  var __defProp = Object.defineProperty;
4
12
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
13
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
14
  var __getProtoOf = Object.getPrototypeOf;
7
15
  var __hasOwnProp = Object.prototype.hasOwnProperty;
16
+ var __esm = (fn, res) => function __init() {
17
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
18
+ };
8
19
  var __export = (target, all) => {
9
20
  for (var name in all)
10
21
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,1788 +38,1941 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
38
  ));
28
39
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
40
 
30
- // src/index.ts
31
- var src_exports = {};
32
- __export(src_exports, {
33
- Args: () => Args,
34
- Box: () => import_ink5.Box,
35
- Db: () => Db,
36
- Divider: () => Divider,
37
- FileDatabase: () => FileDatabase,
38
- FileDatabaseError: () => FileDatabaseError,
39
- FooterPresets: () => FooterPresets,
40
- GridCell: () => GridCell,
41
- InputField: () => InputField,
42
- ListComponent: () => ListComponent,
43
- ListItem: () => ListItem,
44
- MultiColumnListComponent: () => MultiColumnListComponent,
45
- MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
46
- Params: () => Params,
47
- React: () => import_react5.default,
48
- ScreenBody: () => ScreenBody,
49
- ScreenContainer: () => ScreenContainer,
50
- ScreenDivider: () => ScreenDivider,
51
- ScreenFooter: () => ScreenFooter,
52
- ScreenRow: () => ScreenRow,
53
- ScreenTitle: () => ScreenTitle,
54
- Text: () => import_ink5.Text,
55
- TextBlock: () => TextBlock,
56
- buildBreadcrumb: () => buildBreadcrumb,
57
- buildDetailBreadcrumb: () => buildDetailBreadcrumb,
58
- buildFooter: () => buildFooter,
59
- defaultFileSynopsisFunction: () => defaultFileSynopsisFunction,
60
- defaultVersionSynopsisFunction: () => defaultVersionSynopsisFunction,
61
- getArgsInstance: () => getArgsInstance,
62
- getParamsInstance: () => getParamsInstance,
63
- h: () => import_react5.createElement,
64
- joiEdateType: () => joiEdateType,
65
- joiStringArrayType: () => joiStringArrayType,
66
- load: () => load,
67
- organizeFooterMessages: () => organizeFooterMessages,
68
- setupContext: () => setupContext,
69
- showListScreen: () => showListScreen,
70
- showMenuScreen: () => showMenuScreen,
71
- showMultiColumnListScreen: () => showMultiColumnListScreen,
72
- showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
73
- showScreen: () => showScreen,
74
- showWordGridScreen: () => showWordGridScreen,
75
- useCallback: () => import_react5.useCallback,
76
- useEffect: () => import_react5.useEffect,
77
- useInput: () => import_ink5.useInput,
78
- useMemo: () => import_react5.useMemo,
79
- useRef: () => import_react5.useRef,
80
- useState: () => import_react5.useState
81
- });
82
- module.exports = __toCommonJS(src_exports);
83
-
84
- // src/args/index.ts
85
- var import_fs = require("fs");
86
- var import_path = require("path");
87
- var import_dotenv = require("dotenv");
88
- var Args = class {
89
- args = {};
90
- flags = {};
91
- options = {};
92
- commands = [];
93
- usedKeys = /* @__PURE__ */ new Set();
94
- aliases = {};
95
- overrides = {};
96
- defaults = {};
97
- prefixes = [];
98
- nots = [];
99
- configValues = {};
100
- configsLoaded = [];
101
- env = "local";
102
- constructor(config2 = {}) {
103
- this.aliases = config2.aliases || {};
104
- this.overrides = config2.overrides || {};
105
- this.defaults = config2.defaults || {};
106
- this.prefixes = config2.prefixes || ["not", "no"];
107
- const args = config2.args || process.argv.slice(2);
108
- this.parseArgs(args);
109
- this.env = this.get("env")?.toLowerCase() || "local";
110
- this.loadDotEnv();
111
- this.loadConfigFiles();
112
- this.checkConflicts();
113
- }
114
- /**
115
- * Parse command line arguments
116
- */
117
- parseArgs(args) {
118
- let i = 0;
119
- while (i < args.length) {
120
- const arg = args[i];
121
- if (arg.startsWith("--")) {
122
- const [key, value] = this.parseLongOption(arg);
123
- this.setValue(key, value);
124
- i++;
125
- } else if (arg.startsWith("-")) {
126
- const result = this.parseShortOption(arg, args, i);
127
- if (result.consumed > 0) {
128
- i += result.consumed;
41
+ // src/screen/components.ts
42
+ function getScreenWidth(maxWidth = null) {
43
+ const terminalWidth = process.stdout.columns || 80;
44
+ const availableWidth = Math.max(20, terminalWidth - 4);
45
+ return maxWidth ? Math.min(availableWidth, maxWidth) : availableWidth;
46
+ }
47
+ function ScreenContainer({ children }) {
48
+ const width = getScreenWidth();
49
+ return (0, import_react.createElement)(import_ink.Box, {
50
+ flexDirection: "column",
51
+ marginTop: 1,
52
+ borderStyle: "single",
53
+ borderColor: "cyan",
54
+ paddingX: 1,
55
+ width
56
+ // Use the calculated width directly
57
+ }, children);
58
+ }
59
+ function ScreenRow({ children }) {
60
+ return (0, import_react.createElement)(import_ink.Box, { flexDirection: "column" }, children);
61
+ }
62
+ function ScreenTitle({ text }) {
63
+ return (0, import_react.createElement)(
64
+ ScreenRow,
65
+ {},
66
+ (0, import_react.createElement)(import_ink.Text, { bold: true, color: "cyan" }, text)
67
+ );
68
+ }
69
+ function ScreenDivider({ width }) {
70
+ const dividerWidth = width || getScreenWidth() - 4;
71
+ return (0, import_react.createElement)(import_ink.Text, { color: "cyan", dimColor: true }, "\u2500".repeat(dividerWidth));
72
+ }
73
+ function ScreenBody({ children, alignItems = "flex-start" }) {
74
+ return (0, import_react.createElement)(import_ink.Box, { flexDirection: "column", alignItems }, children);
75
+ }
76
+ function ScreenFooter({ lines, textStyle }) {
77
+ const defaultTextStyle = {
78
+ dimColor: true,
79
+ color: "white"
80
+ };
81
+ const finalTextStyle = { ...defaultTextStyle, ...textStyle };
82
+ const flattenAndWrap = (items, keyPrefix = "") => {
83
+ const result = [];
84
+ let keyIndex = 0;
85
+ items.forEach((item, index) => {
86
+ if (Array.isArray(item)) {
87
+ const nested = flattenAndWrap(item, `${keyPrefix}-${index}`);
88
+ result.push(...nested);
89
+ } else if (typeof item === "string") {
90
+ result.push(
91
+ (0, import_react.createElement)(import_ink.Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, item)
92
+ );
93
+ } else {
94
+ const element = item;
95
+ if (element.key === null || element.key === void 0) {
96
+ result.push(
97
+ (0, import_react.createElement)(import_ink.Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, element)
98
+ );
129
99
  } else {
130
- i++;
100
+ result.push(element);
131
101
  }
132
- } else {
133
- this.commands.push(arg);
134
- i++;
135
102
  }
136
- }
103
+ });
104
+ return result;
105
+ };
106
+ const wrappedItems = flattenAndWrap(lines);
107
+ return (0, import_react.createElement)(
108
+ import_ink.Box,
109
+ { flexDirection: "column" },
110
+ (0, import_react.createElement)(import_ink.Box, { flexDirection: "row" }, ...wrappedItems)
111
+ );
112
+ }
113
+ var import_react, import_ink;
114
+ var init_components = __esm({
115
+ "src/screen/components.ts"() {
116
+ "use strict";
117
+ import_react = require("react");
118
+ import_ink = require("ink");
137
119
  }
138
- /**
139
- * Parse long option (--key=value or --key)
140
- */
141
- parseLongOption(arg) {
142
- const key = arg.slice(2);
143
- const prefix = this.prefixes.find((p) => key.startsWith(p));
144
- if (prefix) {
145
- let strippedKey = key.slice(prefix.length);
146
- if (strippedKey.startsWith("-")) {
147
- strippedKey = strippedKey.slice(1);
120
+ });
121
+
122
+ // src/screen/list-components.ts
123
+ function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
124
+ const [, forceUpdate] = (0, import_react2.useState)({});
125
+ const termWidth = (process.stdout.columns || 80) - 8;
126
+ const maxItemLength = Math.max(...items.map((w) => w.length));
127
+ const columnWidth = maxItemLength + 3;
128
+ const columns = Math.max(1, Math.floor(termWidth / columnWidth));
129
+ const itemsPerColumn = Math.ceil(items.length / columns);
130
+ (0, import_react2.useEffect)(() => {
131
+ ctx.setAction("moveUp", () => {
132
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
133
+ forceUpdate({});
134
+ });
135
+ ctx.setAction("moveDown", () => {
136
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
137
+ forceUpdate({});
138
+ });
139
+ ctx.setAction("moveLeft", () => {
140
+ if (selectedIndexRef.current === 0) {
141
+ ctx.goBack();
142
+ } else {
143
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
144
+ forceUpdate({});
145
+ }
146
+ });
147
+ ctx.setAction("moveRight", () => {
148
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
149
+ forceUpdate({});
150
+ });
151
+ ctx.setKeyBinding([
152
+ { key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
153
+ { key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
154
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
155
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
156
+ ]);
157
+ ctx.addFooter(`Total: ${items.length} items`);
158
+ }, []);
159
+ const selectedIndex = selectedIndexRef.current;
160
+ const rows = [];
161
+ for (let row = 0; row < itemsPerColumn; row++) {
162
+ const cols = [];
163
+ for (let col = 0; col < columns; col++) {
164
+ const index = col * itemsPerColumn + row;
165
+ if (index < items.length) {
166
+ const isSelected = index === selectedIndex;
167
+ cols.push(
168
+ h2(
169
+ import_ink2.Box,
170
+ { key: index, width: columnWidth },
171
+ h2(import_ink2.Text, {
172
+ color: isSelected ? "black" : "white",
173
+ backgroundColor: isSelected ? "cyan" : void 0,
174
+ bold: isSelected
175
+ }, items[index].padEnd(maxItemLength))
176
+ )
177
+ );
148
178
  }
149
- this.nots.push(key);
150
- return [strippedKey, false];
151
- }
152
- if (key.includes("=")) {
153
- const eqIndex = key.indexOf("=");
154
- const optionKey = key.slice(0, eqIndex);
155
- const value = key.slice(eqIndex + 1);
156
- return [optionKey, this.parseValue(value)];
157
- } else {
158
- return [key, true];
159
179
  }
180
+ rows.push(
181
+ h2(ScreenRow, { key: row, children: h2(import_ink2.Box, { flexDirection: "row" }, ...cols) })
182
+ );
160
183
  }
161
- /**
162
- * Parse short option (-k=value, -k, or bundled -vsd)
163
- */
164
- parseShortOption(arg, args, index) {
165
- const key = arg.slice(1);
166
- if (key.length === 1 && index + 1 < args.length && !args[index + 1].startsWith("-")) {
167
- const value = args[index + 1];
168
- this.setValue(key, this.parseValue(value));
169
- return { consumed: 2 };
184
+ return h2(import_ink2.Box, { flexDirection: "column" }, ...rows);
185
+ }
186
+ function MultiColumnListWithPreviewComponent({
187
+ items,
188
+ getPreviewContent,
189
+ ctx,
190
+ selectedIndexRef
191
+ }) {
192
+ const [, forceUpdate] = (0, import_react2.useState)({});
193
+ const termWidth = (process.stdout.columns || 80) - 8;
194
+ const maxItemLength = Math.max(...items.map((w) => w.length));
195
+ const columnWidth = maxItemLength + 3;
196
+ const columns = Math.max(1, Math.floor(termWidth / columnWidth));
197
+ const itemsPerColumn = Math.ceil(items.length / columns);
198
+ (0, import_react2.useEffect)(() => {
199
+ ctx.setAction("moveUp", () => {
200
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
201
+ forceUpdate({});
202
+ });
203
+ ctx.setAction("moveDown", () => {
204
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
205
+ forceUpdate({});
206
+ });
207
+ ctx.setAction("moveLeft", () => {
208
+ if (selectedIndexRef.current === 0) {
209
+ ctx.goBack();
210
+ } else {
211
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
212
+ forceUpdate({});
213
+ }
214
+ });
215
+ ctx.setAction("moveRight", () => {
216
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
217
+ forceUpdate({});
218
+ });
219
+ ctx.setKeyBinding([
220
+ { key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
221
+ { key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
222
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
223
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
224
+ ]);
225
+ ctx.addFooter(`Total: ${items.length} items`);
226
+ }, []);
227
+ const selectedIndex = selectedIndexRef.current;
228
+ const selectedItem = items[selectedIndex];
229
+ const rows = [];
230
+ for (let row = 0; row < itemsPerColumn; row++) {
231
+ const cols = [];
232
+ for (let col = 0; col < columns; col++) {
233
+ const index = col * itemsPerColumn + row;
234
+ if (index < items.length) {
235
+ const isSelected = index === selectedIndex;
236
+ cols.push(
237
+ h2(
238
+ import_ink2.Box,
239
+ { key: index, width: columnWidth },
240
+ h2(import_ink2.Text, {
241
+ color: isSelected ? "black" : "white",
242
+ backgroundColor: isSelected ? "cyan" : void 0,
243
+ bold: isSelected
244
+ }, items[index].padEnd(maxItemLength))
245
+ )
246
+ );
247
+ }
170
248
  }
171
- if (key.length > 1 && !key.includes("=")) {
172
- for (let i = 0; i < key.length; i++) {
173
- const shortKey = key[i];
174
- if (shortKey in this.aliases) {
175
- this.setValue(shortKey, true);
249
+ rows.push(
250
+ h2(ScreenRow, { key: row, children: h2(import_ink2.Box, { flexDirection: "row" }, ...cols) })
251
+ );
252
+ }
253
+ const previewContent = getPreviewContent ? getPreviewContent(selectedItem) : selectedItem;
254
+ const previewRows = [];
255
+ if (typeof previewContent === "string") {
256
+ previewRows.push(h2(ScreenRow, { key: "preview-string", children: h2(import_ink2.Text, { bold: true }, previewContent) }));
257
+ } else if (typeof previewContent === "object" && !import_react2.default.isValidElement(previewContent) && previewContent !== null) {
258
+ Object.entries(previewContent).forEach(([key, value], idx) => {
259
+ previewRows.push(h2(ScreenRow, { key: `preview-${key}-${idx}`, children: h2(import_ink2.Text, {}, `${key}: ${value}`) }));
260
+ });
261
+ } else if (import_react2.default.isValidElement(previewContent)) {
262
+ previewRows.push(h2(ScreenRow, { key: "preview-element", children: previewContent }));
263
+ }
264
+ return h2(
265
+ import_ink2.Box,
266
+ { flexDirection: "column" },
267
+ ...rows,
268
+ h2(ScreenRow, { key: "spacer-1", children: h2(import_ink2.Text, {}, " ") }),
269
+ h2(ScreenDivider, { key: "divider" }),
270
+ h2(ScreenRow, { key: "spacer-2", children: h2(import_ink2.Text, {}, " ") }),
271
+ ...previewRows
272
+ );
273
+ }
274
+ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
275
+ const [, forceUpdate] = (0, import_react2.useState)({});
276
+ const [sortOrder, setSortOrder] = (0, import_react2.useState)("none");
277
+ const [scrollOffset, setScrollOffset] = (0, import_react2.useState)(0);
278
+ const scrollStateRef = (0, import_react2.useRef)({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
279
+ const defaultGetTitle = (item) => {
280
+ return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
281
+ };
282
+ const titleGetter = getTitle || defaultGetTitle;
283
+ const displayItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
284
+ const titleA = titleGetter(a).toLowerCase();
285
+ const titleB = titleGetter(b).toLowerCase();
286
+ if (sortOrder === "asc") {
287
+ return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
288
+ } else {
289
+ return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
290
+ }
291
+ }) : items;
292
+ const effectiveMaxHeight = maxHeight || displayItems.length;
293
+ const canScroll = displayItems.length > effectiveMaxHeight;
294
+ const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
295
+ const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
296
+ const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
297
+ const canScrollUp = clampedScrollOffset > 0;
298
+ const canScrollDown = clampedScrollOffset < maxScrollOffset;
299
+ scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
300
+ (0, import_react2.useEffect)(() => {
301
+ ctx.setAction("moveUp", () => {
302
+ const newIndex = Math.max(0, selectedIndexRef.current - 1);
303
+ selectedIndexRef.current = newIndex;
304
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
305
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
306
+ const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
307
+ if (newIndex < currentClampedScrollOffset) {
308
+ setScrollOffset(newIndex);
309
+ }
310
+ forceUpdate({});
311
+ });
312
+ ctx.setAction("moveDown", () => {
313
+ const currentItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
314
+ const titleA = titleGetter(a).toLowerCase();
315
+ const titleB = titleGetter(b).toLowerCase();
316
+ if (sortOrder === "asc") {
317
+ return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
176
318
  } else {
177
- this.args[shortKey] = true;
319
+ return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
178
320
  }
321
+ }) : items;
322
+ const maxIndex = currentItems.length - 1;
323
+ const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
324
+ selectedIndexRef.current = newIndex;
325
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
326
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
327
+ const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
328
+ if (newIndex >= currentClampedScrollOffset + currentMaxHeight) {
329
+ setScrollOffset(newIndex - currentMaxHeight + 1);
179
330
  }
180
- return { consumed: 1 };
181
- }
182
- if (key.includes("=")) {
183
- const eqIndex = key.indexOf("=");
184
- const optionKey = key.slice(0, eqIndex);
185
- const value = key.slice(eqIndex + 1);
186
- if (optionKey.length > 1) {
187
- for (let i = 0; i < optionKey.length - 1; i++) {
188
- const shortKey = optionKey[i];
189
- if (shortKey in this.aliases) {
190
- this.setValue(shortKey, true);
331
+ forceUpdate({});
332
+ });
333
+ ctx.setAction("scrollUp", () => {
334
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
335
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
336
+ const newScrollOffset = Math.max(0, currentScrollOffset - 1);
337
+ setScrollOffset(newScrollOffset);
338
+ forceUpdate({});
339
+ });
340
+ ctx.setAction("scrollDown", () => {
341
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
342
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
343
+ const newScrollOffset = Math.min(currentMaxScrollOffset, currentScrollOffset + 1);
344
+ setScrollOffset(newScrollOffset);
345
+ forceUpdate({});
346
+ });
347
+ if (sortable) {
348
+ ctx.setAction("toggleSort", () => {
349
+ const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
350
+ const currentSelectedItem = displayItems[selectedIndexRef.current];
351
+ setSortOrder(nextSort);
352
+ const newSortedItems = nextSort !== "none" ? [...items].sort((a, b) => {
353
+ const titleA = titleGetter(a).toLowerCase();
354
+ const titleB = titleGetter(b).toLowerCase();
355
+ if (nextSort === "asc") {
356
+ return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
191
357
  } else {
192
- this.args[shortKey] = true;
358
+ return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
193
359
  }
360
+ }) : items;
361
+ const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
362
+ if (newIndex !== -1) {
363
+ selectedIndexRef.current = newIndex;
364
+ setScrollOffset(newIndex);
365
+ } else {
366
+ selectedIndexRef.current = 0;
367
+ setScrollOffset(0);
194
368
  }
195
- const lastKey = optionKey[optionKey.length - 1];
196
- if (lastKey in this.aliases) {
197
- this.setValue(lastKey, this.parseValue(value));
369
+ forceUpdate({});
370
+ });
371
+ const defaultHighlightStyle = {
372
+ color: "black",
373
+ backgroundColor: "green",
374
+ bold: true
375
+ };
376
+ const highlightStyle = { ...defaultHighlightStyle, ...sortHighlightStyle };
377
+ const sortCaption = () => {
378
+ if (sortOrder === "none") {
379
+ return h2(import_ink2.Text, {}, "s to toggle sort");
198
380
  } else {
199
- this.args[lastKey] = this.parseValue(value);
381
+ const sortLabel = sortOrder === "asc" ? "ASC" : "DESC";
382
+ return h2(
383
+ import_ink2.Text,
384
+ {},
385
+ "s to toggle ",
386
+ h2(import_ink2.Text, { color: "white", bold: true }, "sort"),
387
+ " ",
388
+ h2(import_ink2.Text, highlightStyle, ` ${sortLabel} `)
389
+ );
200
390
  }
201
- } else {
202
- this.setValue(optionKey, this.parseValue(value));
203
- }
204
- return { consumed: 1 };
391
+ };
392
+ ctx.setKeyBinding([
393
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
394
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
395
+ {
396
+ key: "s",
397
+ caption: sortCaption,
398
+ action: "toggleSort",
399
+ order: 5
400
+ }
401
+ ]);
402
+ ctx.update();
205
403
  } else {
206
- this.setValue(key, true);
207
- return { consumed: 1 };
208
- }
209
- }
210
- /**
211
- * Parse value (handle quotes)
212
- */
213
- parseValue(value) {
214
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
215
- return value.slice(1, -1);
404
+ ctx.setKeyBinding([
405
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
406
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
407
+ ]);
216
408
  }
217
- return value;
218
- }
219
- /**
220
- * Set a value with proper categorization
221
- */
222
- setValue(key, value) {
223
- const resolvedKey = this.aliases[key] || key;
224
- if (typeof value === "boolean") {
225
- this.flags[resolvedKey] = value;
409
+ }, [sortOrder, sortable]);
410
+ const selectedIndex = selectedIndexRef.current;
411
+ const defaultRenderItem = (item, isSelected, displayIndex, actualIndex) => {
412
+ const isFirstVisible = displayIndex === 0;
413
+ const isLastVisible = displayIndex === visibleItems.length - 1;
414
+ let arrowPrefix = "";
415
+ let selectionPrefix = "";
416
+ if (isFirstVisible && canScrollUp) {
417
+ arrowPrefix = "\u2191 ";
418
+ } else if (isLastVisible && canScrollDown) {
419
+ arrowPrefix = "\u2193 ";
226
420
  } else {
227
- this.options[resolvedKey] = value;
228
- }
229
- this.args[resolvedKey.toLowerCase()] = value;
230
- }
231
- /**
232
- * Check for conflicts (short + long form of same option)
233
- */
234
- checkConflicts() {
235
- const conflicts = [];
236
- for (const [shortKey, longKey] of Object.entries(this.aliases)) {
237
- const hasShort = this.args[shortKey] !== void 0;
238
- const hasLong = this.args[longKey] !== void 0;
239
- if (hasShort && hasLong) {
240
- conflicts.push(`Both -${shortKey} and --${longKey} specified`);
241
- }
242
- }
243
- if (conflicts.length > 0) {
244
- throw new Error(`Argument conflicts: ${conflicts.join(", ")}`);
245
- }
246
- }
247
- /**
248
- * Get a value with precedence order
249
- */
250
- get(key) {
251
- const resolvedKey = this.aliases[key] || key;
252
- this.usedKeys.add(resolvedKey);
253
- if (this.overrides[resolvedKey] !== void 0) {
254
- return this.overrides[resolvedKey];
255
- }
256
- const lcKey = resolvedKey.toLowerCase();
257
- const lcKeyWithEnv = `${lcKey}${this.env ? `_${this.env.toLowerCase()}` : ""}`;
258
- if (this.env && this.args[lcKeyWithEnv] !== void 0) {
259
- return this.args[lcKeyWithEnv];
260
- } else if (this.args[lcKey] !== void 0) {
261
- return this.args[lcKey];
421
+ arrowPrefix = " ";
262
422
  }
263
- if (this.configValues[resolvedKey] !== void 0) {
264
- return this.configValues[resolvedKey];
423
+ if (isSelected) {
424
+ selectionPrefix = selectionMarker;
425
+ } else {
426
+ selectionPrefix = " ".repeat(selectionMarker.length);
265
427
  }
266
- const envKey = this.toEnvKey(resolvedKey);
267
- const envKeyWithEnv = `${envKey}${this.env ? `_${this.env.toUpperCase()}` : ""}`;
268
- const envSpecificKey = Object.keys(process.env).find(
269
- (k) => this.env && k.toUpperCase() === envKeyWithEnv
428
+ return h2(
429
+ import_ink2.Box,
430
+ { flexDirection: "row" },
431
+ // Arrow (clickable if functional, not highlighted)
432
+ h2(import_ink2.Text, {
433
+ key: `arrow-${actualIndex}`,
434
+ color: "white"
435
+ }, arrowPrefix),
436
+ // Selection marker space (always same width, not highlighted)
437
+ h2(import_ink2.Text, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
438
+ // Item name (highlighted if selected)
439
+ h2(import_ink2.Text, {
440
+ key: `name-${actualIndex}`,
441
+ color: isSelected ? "black" : "white",
442
+ backgroundColor: isSelected ? "cyan" : void 0,
443
+ bold: isSelected
444
+ }, item.name)
270
445
  );
271
- const envKeyFound = Object.keys(process.env).find((k) => k.toUpperCase() === envKey);
272
- if (envSpecificKey) {
273
- return process.env[envSpecificKey];
274
- } else if (envKeyFound) {
275
- return process.env[envKeyFound];
276
- }
277
- if (this.defaults[resolvedKey] !== void 0) {
278
- return this.defaults[resolvedKey];
279
- }
280
- if (resolvedKey === "env" && process.env.NODE_ENV !== void 0) {
281
- return process.env.NODE_ENV;
282
- }
283
- return void 0;
284
- }
285
- /**
286
- * Set a value (for testing/internal use)
287
- */
288
- set(key, value) {
289
- this.args[key] = value;
290
- }
291
- /**
292
- * Check if a command exists (case-insensitive)
293
- */
294
- hasCommand(cmd) {
295
- return this.commands.some((command) => command.toLowerCase() === cmd.toLowerCase());
296
- }
297
- /**
298
- * Get all commands
299
- */
300
- getCommands() {
301
- return [...this.commands];
302
- }
303
- /**
304
- * Get used keys (as array)
305
- */
306
- getUsed() {
307
- return Array.from(this.usedKeys);
308
- }
309
- /**
310
- * Get unused keys (as array)
311
- */
312
- getUnused() {
313
- const unused = [];
314
- for (const key of Object.keys(this.args)) {
315
- if (!this.usedKeys.has(key) && !this.nots.includes(key)) {
316
- unused.push(key);
446
+ };
447
+ const itemRenderer = renderItem || defaultRenderItem;
448
+ return h2(
449
+ import_ink2.Box,
450
+ { flexDirection: "column" },
451
+ ...visibleItems.map((item, displayIndex) => {
452
+ const actualIndex = clampedScrollOffset + displayIndex;
453
+ const isSelected = actualIndex === selectedIndex;
454
+ if (renderItem) {
455
+ const isFirstVisible = displayIndex === 0;
456
+ const isLastVisible = displayIndex === visibleItems.length - 1;
457
+ let arrowPrefix = "";
458
+ let selectionPrefix = "";
459
+ if (isFirstVisible && canScrollUp) {
460
+ arrowPrefix = "\u2191 ";
461
+ } else if (isLastVisible && canScrollDown) {
462
+ arrowPrefix = "\u2193 ";
463
+ } else {
464
+ arrowPrefix = " ";
465
+ }
466
+ if (isSelected) {
467
+ selectionPrefix = selectionMarker;
468
+ } else {
469
+ selectionPrefix = " ".repeat(selectionMarker.length);
470
+ }
471
+ return h2(ScreenRow, {
472
+ key: `item-${actualIndex}`,
473
+ children: h2(
474
+ import_ink2.Box,
475
+ { flexDirection: "row" },
476
+ // Arrow (clickable if functional, not highlighted)
477
+ h2(import_ink2.Text, {
478
+ key: `arrow-${actualIndex}`,
479
+ color: "white"
480
+ }, arrowPrefix),
481
+ // Selection marker space (always same width, not highlighted)
482
+ h2(import_ink2.Text, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
483
+ // Custom rendered content
484
+ renderItem(item, isSelected, displayIndex)
485
+ )
486
+ });
487
+ } else {
488
+ return h2(ScreenRow, {
489
+ key: `item-${actualIndex}`,
490
+ children: itemRenderer(item, isSelected, displayIndex, actualIndex)
491
+ });
317
492
  }
318
- }
319
- return unused;
320
- }
321
- /**
322
- * Convert key to environment variable format
323
- */
324
- toEnvKey(key) {
325
- return key.replace(
326
- /[A-Z0-9]/g,
327
- (match, offset) => offset === 0 ? match : "_" + match.toLowerCase()
328
- ).toUpperCase();
493
+ })
494
+ );
495
+ }
496
+ var import_react2, import_ink2, h2;
497
+ var init_list_components = __esm({
498
+ "src/screen/list-components.ts"() {
499
+ "use strict";
500
+ import_react2 = __toESM(require("react"), 1);
501
+ import_ink2 = require("ink");
502
+ init_components();
503
+ h2 = import_react2.createElement;
329
504
  }
330
- /**
331
- * Load .env file
332
- */
333
- loadDotEnv() {
334
- const dotEnvPath = this.get("dotEnvPath") || process.cwd();
335
- const dotEnvFile = this.get("dotEnvFile") || ".env";
336
- if (this.get("dotEnvFile")) {
337
- const customPath = (0, import_path.resolve)(dotEnvPath, dotEnvFile);
338
- if ((0, import_fs.existsSync)(customPath)) {
339
- (0, import_dotenv.config)({ path: customPath, quiet: true });
340
- }
341
- return;
505
+ });
506
+
507
+ // src/screen/screens.ts
508
+ function groupKeyBindings(bindings) {
509
+ const groups = {};
510
+ const enabledBindings = bindings.filter((b) => b.enabled !== false);
511
+ enabledBindings.forEach((binding) => {
512
+ const caption = typeof binding.caption === "string" ? binding.caption : "";
513
+ if (!groups[caption]) {
514
+ groups[caption] = {
515
+ keys: [],
516
+ caption,
517
+ order: binding.order || 999
518
+ };
342
519
  }
343
- let dotEnvPathFile = null;
344
- const envSpecificFile = `.env.${this.env}`;
345
- const envSpecificPath = (0, import_path.resolve)(dotEnvPath, envSpecificFile);
346
- if ((0, import_fs.existsSync)(envSpecificPath)) {
347
- dotEnvPathFile = envSpecificPath;
520
+ groups[caption].keys.push(binding.key);
521
+ });
522
+ return Object.values(groups);
523
+ }
524
+ function formatKeyBindings(bindings, mode = "long") {
525
+ const resolvedBindings = bindings.map((binding) => {
526
+ let resolvedCaption = binding.caption;
527
+ if (typeof binding.caption === "function") {
528
+ resolvedCaption = binding.caption();
348
529
  }
349
- if (!dotEnvPathFile && !this.get("dotEnvPath")) {
350
- const examplesPath = (0, import_path.resolve)(dotEnvPath, "examples");
351
- const examplesEnvSpecificPath = (0, import_path.resolve)(examplesPath, envSpecificFile);
352
- if ((0, import_fs.existsSync)(examplesEnvSpecificPath)) {
353
- dotEnvPathFile = examplesEnvSpecificPath;
530
+ return {
531
+ ...binding,
532
+ resolvedCaption
533
+ };
534
+ });
535
+ const groups = groupKeyBindings(resolvedBindings.map((b) => ({
536
+ ...b,
537
+ caption: typeof b.resolvedCaption === "string" ? b.resolvedCaption : ""
538
+ })));
539
+ groups.sort((a, b) => a.order - b.order);
540
+ const items = [];
541
+ groups.forEach((group) => {
542
+ const bindingWithCustom = resolvedBindings.find(
543
+ (b) => group.keys.includes(b.key) && typeof b.resolvedCaption !== "string"
544
+ );
545
+ if (bindingWithCustom && bindingWithCustom.resolvedCaption) {
546
+ items.push(bindingWithCustom.resolvedCaption);
547
+ } else {
548
+ const keyStr = formatKeys(group.keys);
549
+ if (mode === "long") {
550
+ items.push(`${keyStr} to ${group.caption}`);
551
+ } else {
552
+ items.push(keyStr);
354
553
  }
355
554
  }
356
- if (!dotEnvPathFile) {
357
- dotEnvPathFile = (0, import_path.resolve)(dotEnvPath, dotEnvFile);
358
- if (!(0, import_fs.existsSync)(dotEnvPathFile)) {
359
- if (!this.get("dotEnvPath")) {
360
- const examplesPath = (0, import_path.resolve)(dotEnvPath, "examples");
361
- const examplesEnvFile = (0, import_path.resolve)(examplesPath, dotEnvFile);
362
- if ((0, import_fs.existsSync)(examplesEnvFile)) {
363
- dotEnvPathFile = examplesEnvFile;
364
- } else {
365
- dotEnvPathFile = (0, import_path.resolve)(dotEnvPath, "..", dotEnvFile);
555
+ });
556
+ return items;
557
+ }
558
+ function formatKeys(keys) {
559
+ const keyMap = {
560
+ "escape": "esc",
561
+ "leftArrow": "\u2190",
562
+ "rightArrow": "\u2192",
563
+ "upArrow": "\u2191",
564
+ "downArrow": "\u2193",
565
+ "return": "enter"
566
+ };
567
+ return keys.map((k) => keyMap[k] || k).join("/");
568
+ }
569
+ async function showScreen(config2) {
570
+ const {
571
+ title,
572
+ onRender,
573
+ parentData = {}
574
+ } = config2;
575
+ return new Promise((resolve2) => {
576
+ let instance2;
577
+ const keyBindings = [];
578
+ const actions = {};
579
+ const customFooterItems = [];
580
+ let renderResult = null;
581
+ let initialized = false;
582
+ const Screen = () => {
583
+ const [updateCounter, setUpdateCounter] = (0, import_react3.useState)(0);
584
+ if (!initialized) {
585
+ const defaultBindings = [
586
+ { key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
587
+ { key: "leftArrow", caption: "go back", action: "back", protected: false, order: 1 }
588
+ // Note: 'select' is not a default - components add it if needed
589
+ ];
590
+ defaultBindings.forEach((binding) => {
591
+ keyBindings.push(binding);
592
+ });
593
+ actions.back = () => {
594
+ cleanup(null);
595
+ };
596
+ initialized = true;
597
+ }
598
+ const context = {
599
+ setAction: (actionName, handlerFn) => {
600
+ actions[actionName] = handlerFn;
601
+ },
602
+ setKeyBinding: (bindingOrBindings) => {
603
+ const bindingsToSet = Array.isArray(bindingOrBindings) ? bindingOrBindings : [bindingOrBindings];
604
+ bindingsToSet.forEach((binding) => {
605
+ const existingIndex = keyBindings.findIndex((b) => b.key === binding.key);
606
+ if (existingIndex >= 0) {
607
+ const existing = keyBindings[existingIndex];
608
+ if (existing.protected) {
609
+ console.warn(`Cannot override protected key: ${binding.key}`);
610
+ return;
611
+ }
612
+ keyBindings[existingIndex] = {
613
+ ...existing,
614
+ ...binding,
615
+ order: binding.order !== void 0 ? binding.order : existing.order,
616
+ enabled: binding.enabled !== void 0 ? binding.enabled : existing.enabled !== void 0 ? existing.enabled : true
617
+ };
618
+ } else {
619
+ keyBindings.push({
620
+ protected: false,
621
+ order: 999,
622
+ enabled: true,
623
+ ...binding
624
+ });
625
+ }
626
+ });
627
+ },
628
+ updateKeyBinding: (keyName, updates) => {
629
+ const index = keyBindings.findIndex((b) => b.key === keyName);
630
+ if (index >= 0) {
631
+ keyBindings[index] = {
632
+ ...keyBindings[index],
633
+ ...updates
634
+ };
366
635
  }
367
- }
636
+ },
637
+ removeKeyBinding: (keyName) => {
638
+ const index = keyBindings.findIndex((b) => b.key === keyName);
639
+ if (index >= 0) {
640
+ if (keyBindings[index].protected) {
641
+ console.warn(`Cannot remove protected key: ${keyName}`);
642
+ return;
643
+ }
644
+ keyBindings.splice(index, 1);
645
+ }
646
+ },
647
+ addFooter: (item) => {
648
+ customFooterItems.push(item);
649
+ },
650
+ clearFooter: () => {
651
+ customFooterItems.length = 0;
652
+ },
653
+ setFooter: (items) => {
654
+ customFooterItems.length = 0;
655
+ const itemsArray = Array.isArray(items) ? items : [items];
656
+ customFooterItems.push(...itemsArray);
657
+ },
658
+ update: () => {
659
+ setUpdateCounter((c) => c + 1);
660
+ },
661
+ goBack: () => {
662
+ if (actions.back) {
663
+ actions.back();
664
+ }
665
+ },
666
+ close: (result) => {
667
+ cleanup(result);
668
+ },
669
+ parentData
670
+ };
671
+ if (!renderResult) {
672
+ renderResult = onRender(context);
368
673
  }
369
- }
370
- if (dotEnvPathFile && (0, import_fs.existsSync)(dotEnvPathFile)) {
371
- (0, import_dotenv.config)({ path: dotEnvPathFile, quiet: true });
372
- }
373
- }
374
- /**
375
- * Load configuration files
376
- */
377
- loadConfigFiles() {
378
- this.configsLoaded = [];
379
- this.configValues = {};
380
- const _defaultConfigExtension = this.get("defaultConfigExtension") || "js";
381
- const optConfigFiles = this.get("config") || this.get("configs") || "";
382
- const configFiles = optConfigFiles ? optConfigFiles.split(/,\s*/) : [];
383
- const optConfigFilePath = this.get("configPath");
384
- if (configFiles.length > 0) {
385
- for (const cfgFile of configFiles) {
386
- let notLoaded = false;
387
- let notLoadedEnvSpecific = false;
388
- const cfgFileWithPath = this.resolveFileWithPath(optConfigFilePath, cfgFile);
389
- try {
390
- const cfgContents = this.requireConfigFile(cfgFileWithPath);
391
- this.configValues = { ...this.configValues, ...cfgContents };
392
- this.configsLoaded.push(cfgFileWithPath);
393
- } catch {
394
- notLoaded = true;
674
+ (0, import_ink3.useInput)((input, key) => {
675
+ if (key.ctrl && input === "c") {
676
+ cleanup(null);
677
+ process.exit(0);
678
+ return;
395
679
  }
396
- const cfgEnvFileWithPath = this.resolveFileWithPath(
397
- optConfigFilePath,
398
- cfgFile,
399
- this.env
400
- );
401
- if (cfgEnvFileWithPath !== cfgFileWithPath) {
402
- try {
403
- const cfgContents = this.requireConfigFile(cfgEnvFileWithPath);
404
- this.configValues = { ...this.configValues, ...cfgContents };
405
- this.configsLoaded.push(cfgEnvFileWithPath);
406
- } catch {
407
- notLoadedEnvSpecific = true;
680
+ let matchedBinding = null;
681
+ for (const binding of keyBindings) {
682
+ let keyMatches = false;
683
+ if (key[binding.key]) {
684
+ keyMatches = true;
685
+ } else if (input === binding.key) {
686
+ keyMatches = true;
687
+ }
688
+ if (keyMatches) {
689
+ if (binding.enabled === false) {
690
+ continue;
691
+ }
692
+ if (binding.condition && !binding.condition(context)) {
693
+ continue;
694
+ }
695
+ matchedBinding = binding;
696
+ break;
408
697
  }
409
- } else {
410
- notLoadedEnvSpecific = true;
411
698
  }
412
- if (notLoaded && notLoadedEnvSpecific) {
413
- throw new Error(`can't load config file "${cfgFileWithPath}"`);
699
+ if (matchedBinding && actions[matchedBinding.action]) {
700
+ const actionResult = actions[matchedBinding.action]({
701
+ input,
702
+ key,
703
+ binding: matchedBinding
704
+ });
705
+ }
706
+ });
707
+ const footerLines = [];
708
+ const bindingItems = formatKeyBindings(keyBindings, "long");
709
+ if (bindingItems.length > 0) {
710
+ const bindingsLine = [];
711
+ bindingItems.forEach((item, idx) => {
712
+ if (idx > 0) {
713
+ bindingsLine.push(", ");
714
+ }
715
+ bindingsLine.push(item);
716
+ });
717
+ const allStrings = bindingItems.every((item) => typeof item === "string");
718
+ if (allStrings) {
719
+ footerLines.push(bindingsLine.join(""));
720
+ } else {
721
+ const wrappedBindingsLine = bindingsLine.map(
722
+ (item) => typeof item === "string" ? (0, import_react3.createElement)(import_ink3.Text, {}, item) : item
723
+ );
724
+ footerLines.push(wrappedBindingsLine);
414
725
  }
415
726
  }
416
- }
417
- }
418
- /**
419
- * Resolve file path with environment-specific naming
420
- */
421
- resolveFileWithPath(optConfigFilePath, cfgFile, env) {
422
- let cfgFileWithPath = optConfigFilePath ? (0, import_path.isAbsolute)(optConfigFilePath) ? (0, import_path.resolve)(optConfigFilePath, cfgFile) : (0, import_path.resolve)(process.cwd(), optConfigFilePath, cfgFile) : (0, import_path.isAbsolute)(cfgFile) ? cfgFile : (0, import_path.resolve)(process.cwd(), cfgFile);
423
- const { basePathWithName, extension } = this.splitPath(cfgFileWithPath);
424
- if (env) {
425
- cfgFileWithPath = `${basePathWithName}.${env}.${extension || "js"}`;
426
- } else {
427
- cfgFileWithPath = `${basePathWithName}.${extension || "js"}`;
428
- }
429
- return cfgFileWithPath;
430
- }
431
- /**
432
- * Split file path into base path and extension
433
- */
434
- splitPath(filePath) {
435
- const basePathWithName = (0, import_path.join)((0, import_path.dirname)(filePath), (0, import_path.basename)(filePath, (0, import_path.extname)(filePath)));
436
- const extension = (0, import_path.extname)(filePath).slice(1);
437
- return { basePathWithName, extension };
438
- }
439
- /**
440
- * Require a configuration file (supports .js and .json)
441
- */
442
- requireConfigFile(filePath) {
443
- if (!(0, import_fs.existsSync)(filePath)) {
444
- throw new Error(`Config file not found: ${filePath}`);
445
- }
446
- const ext = (0, import_path.extname)(filePath).toLowerCase();
447
- if (ext === ".json") {
448
- const content = (0, import_fs.readFileSync)(filePath, "utf8");
449
- return JSON.parse(content);
450
- } else if (ext === ".js") {
451
- try {
452
- delete require.cache[require.resolve(filePath)];
453
- return require(filePath);
454
- } catch (error) {
455
- throw new Error(`Failed to load JS config file: ${error instanceof Error ? error.message : String(error)}`);
456
- }
457
- } else {
458
- throw new Error(`Unsupported file extension: ${ext}`);
459
- }
460
- }
461
- /**
462
- * Get all parsed data
463
- */
464
- getParsed() {
465
- return {
466
- command: this.commands[0] || "",
467
- flags: { ...this.flags },
468
- options: { ...this.options },
469
- usedKeys: Array.from(this.usedKeys)
727
+ customFooterItems.forEach((item) => {
728
+ if (typeof item === "string") {
729
+ footerLines.push(item);
730
+ } else {
731
+ footerLines.push(item);
732
+ }
733
+ });
734
+ return (0, import_react3.createElement)(
735
+ ScreenContainer,
736
+ {},
737
+ (0, import_react3.createElement)(ScreenTitle, { text: title }),
738
+ (0, import_react3.createElement)(ScreenDivider),
739
+ (0, import_react3.createElement)(ScreenRow, {}, (0, import_react3.createElement)(import_ink3.Text, {}, " ")),
740
+ renderResult,
741
+ (0, import_react3.createElement)(ScreenRow, {}, (0, import_react3.createElement)(import_ink3.Text, {}, " ")),
742
+ (0, import_react3.createElement)(ScreenDivider),
743
+ (0, import_react3.createElement)(ScreenFooter, { lines: footerLines })
744
+ );
470
745
  };
471
- }
472
- /**
473
- * Set prefixes dynamically and re-parse arguments (like legacy)
474
- */
475
- setPrefixes(prefixes) {
476
- const arr = Array.isArray(prefixes) ? prefixes : prefixes.split(/,\s*/);
477
- const sortedArr = arr.sort(
478
- (a, b) => a.length < b.length ? 1 : a.length > b.length ? -1 : 0
479
- );
480
- this.prefixes = sortedArr.map((el) => el.toLowerCase());
481
- const args = process.argv.slice(2);
482
- this.parseArgs(args);
483
- }
484
- };
485
- var instance = null;
486
- function getArgsInstance() {
487
- return instance;
746
+ const cleanup = (result) => {
747
+ if (instance2) instance2.unmount();
748
+ setTimeout(() => resolve2(result), 50);
749
+ };
750
+ instance2 = (0, import_ink3.render)((0, import_react3.createElement)(Screen));
751
+ });
488
752
  }
489
-
490
- // src/params/index.ts
491
- var import_joi = __toESM(require("joi"), 1);
492
-
493
- // src/errors.ts
494
- var FrameworkError = class extends Error {
495
- constructor(message) {
496
- super(message);
497
- this.name = "FrameworkError";
498
- }
499
- };
500
- var ParamError = class extends FrameworkError {
501
- constructor(message) {
502
- super(message);
503
- this.name = "ParamError";
504
- }
505
- };
506
-
507
- // src/params/custom-types.ts
508
- var joiEdateType = (value, helpers) => {
509
- if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) {
510
- const testDate = new Date(value);
511
- if (!isNaN(testDate.getTime())) {
512
- return value;
513
- }
514
- }
515
- if (value instanceof Date) {
516
- return value.toISOString();
517
- }
518
- if (typeof value !== "string") {
519
- value = String(value);
520
- }
521
- if (value.toLowerCase() === "now") {
522
- return (/* @__PURE__ */ new Date()).toISOString();
523
- }
524
- const referenceRegex = /^@(\w+)([+-]\d+[smhdwy])$/i;
525
- const referenceMatch = value.match(referenceRegex);
526
- if (referenceMatch) {
527
- const [, paramName, relativeExpr] = referenceMatch;
528
- const context = helpers.prefs?.context;
529
- if (!context || !context.params) {
530
- throw new ParamError(`Cannot resolve cross-parameter reference @${paramName}: context not available. Ensure parameters are processed with proper context.`);
531
- }
532
- const referencedValue = context.params[paramName];
533
- if (referencedValue === void 0 || referencedValue === null) {
534
- throw new ParamError(`Cannot resolve @${paramName}: parameter "${paramName}" is not defined or has no value. Parameters are evaluated left-to-right.`);
535
- }
536
- let referenceDate;
537
- if (referencedValue instanceof Date) {
538
- referenceDate = referencedValue;
539
- } else if (typeof referencedValue === "string") {
540
- referenceDate = new Date(referencedValue);
541
- if (isNaN(referenceDate.getTime())) {
542
- throw new ParamError(`Referenced parameter @${paramName} has invalid date value: ${referencedValue}`);
753
+ async function showListScreen(config2) {
754
+ const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker } = config2;
755
+ return showScreen({
756
+ title,
757
+ parentData,
758
+ onRender: (ctx) => {
759
+ const selectedIndexRef = { current: initialSelectedIndex };
760
+ ctx.setAction("select", () => {
761
+ const selected = items[selectedIndexRef.current];
762
+ if (onSelect) {
763
+ const result = onSelect(selected.value, selectedIndexRef.current);
764
+ ctx.close(result);
765
+ }
766
+ });
767
+ if (onEscape) {
768
+ ctx.setAction("back", () => {
769
+ const result = onEscape(selectedIndexRef.current);
770
+ ctx.close(result);
771
+ });
543
772
  }
544
- } else {
545
- throw new ParamError(`Referenced parameter @${paramName} is not a valid date type (found: ${typeof referencedValue})`);
546
- }
547
- const relativeMatch2 = relativeExpr.match(/^([+-])(\d+)([smhdwy])$/i);
548
- if (!relativeMatch2) {
549
- throw new ParamError(`Invalid relative time expression in @${paramName}${relativeExpr}`);
550
- }
551
- const [, sign, amount, unit] = relativeMatch2;
552
- const offset = calculateTimeOffset(parseInt(amount, 10), unit, sign);
553
- const resultDate = new Date(referenceDate.getTime() + offset);
554
- return resultDate.toISOString();
555
- }
556
- const relativeTimeRegex = /^([+-])(\d+)([smhdwy])$/i;
557
- const relativeMatch = value.match(relativeTimeRegex);
558
- if (relativeMatch) {
559
- const [, sign, amount, unit] = relativeMatch;
560
- const numAmount = parseInt(amount, 10);
561
- if (isNaN(numAmount)) {
562
- throw new ParamError(`Invalid relative time amount: ${amount}`);
773
+ ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
774
+ return (0, import_react3.createElement)(ListComponent, { items, ctx, selectedIndexRef, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker });
563
775
  }
564
- const offset = calculateTimeOffset(numAmount, unit, sign);
565
- const resultDate = new Date(Date.now() + offset);
566
- return resultDate.toISOString();
567
- }
568
- const parsedDate = new Date(value);
569
- if (isNaN(parsedDate.getTime())) {
570
- throw new ParamError(`Invalid date format: ${value}. Expected a valid date string, "now", relative time expression (e.g., "-2h", "+1d"), or cross-parameter reference (e.g., "@startTime+2h")`);
571
- }
572
- return parsedDate.toISOString();
573
- };
574
- function calculateTimeOffset(amount, unit, sign) {
575
- let multiplier = 1;
576
- switch (unit.toLowerCase()) {
577
- case "s":
578
- multiplier = 1e3;
579
- break;
580
- case "m":
581
- multiplier = 60 * 1e3;
582
- break;
583
- case "h":
584
- multiplier = 60 * 60 * 1e3;
585
- break;
586
- case "d":
587
- multiplier = 24 * 60 * 60 * 1e3;
588
- break;
589
- case "w":
590
- multiplier = 7 * 24 * 60 * 60 * 1e3;
591
- break;
592
- case "y":
593
- multiplier = 365 * 24 * 60 * 60 * 1e3;
594
- break;
595
- default:
596
- throw new ParamError(`Invalid time unit: ${unit}. Supported units: s, m, h, d, w, y`);
597
- }
598
- return sign === "+" ? amount * multiplier : -amount * multiplier;
776
+ });
599
777
  }
600
- var joiStringArrayType = (type) => (value, helpers) => {
601
- if (value === void 0 || typeof value === "function") {
602
- return [];
603
- }
604
- const arr = value.split(/,\s*/).map((el) => {
605
- if (type === "number") {
606
- const v = parseInt(el, 10);
607
- if (isNaN(v)) {
608
- throw new ParamError(`array element "${el}" should be numeric`);
778
+ async function showMultiColumnListScreen(config2) {
779
+ const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config2;
780
+ return showScreen({
781
+ title,
782
+ parentData,
783
+ onRender: (ctx) => {
784
+ const selectedIndexRef = { current: initialSelectedIndex };
785
+ ctx.setAction("select", () => {
786
+ const selected = items[selectedIndexRef.current];
787
+ if (onSelect) {
788
+ const result = onSelect(selected, selectedIndexRef.current);
789
+ ctx.close(result);
790
+ }
791
+ });
792
+ if (onEscape) {
793
+ ctx.setAction("back", () => {
794
+ const result = onEscape(selectedIndexRef.current);
795
+ ctx.close(result);
796
+ });
609
797
  }
610
- return v;
611
- } else if (type === "boolean") {
612
- const v = el.match(/true|t|yes|1/i) ? true : el.match(/false|f|no|0/i) ? false : null;
613
- if (v === null) {
614
- throw new ParamError(`array element "${el}" should be boolean`);
798
+ ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
799
+ return (0, import_react3.createElement)(MultiColumnListComponent, { items, ctx, selectedIndexRef });
800
+ }
801
+ });
802
+ }
803
+ async function showMultiColumnListWithPreviewScreen(config2) {
804
+ const { title, items, getPreviewContent, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config2;
805
+ return showScreen({
806
+ title,
807
+ parentData,
808
+ onRender: (ctx) => {
809
+ const selectedIndexRef = { current: initialSelectedIndex };
810
+ ctx.setAction("select", () => {
811
+ const selected = items[selectedIndexRef.current];
812
+ if (onSelect) {
813
+ const result = onSelect(selected, selectedIndexRef.current);
814
+ ctx.close(result);
815
+ }
816
+ });
817
+ if (onEscape) {
818
+ ctx.setAction("back", () => {
819
+ const result = onEscape(selectedIndexRef.current);
820
+ ctx.close(result);
821
+ });
615
822
  }
616
- return v;
617
- } else if (type === "string") {
618
- return el;
619
- } else {
620
- throw new ParamError(`unknown type "${type}" for array elements`);
823
+ ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
824
+ return (0, import_react3.createElement)(MultiColumnListWithPreviewComponent, { items, getPreviewContent, ctx, selectedIndexRef });
621
825
  }
622
826
  });
623
- return arr;
624
- };
827
+ }
828
+ var import_react3, import_ink3, showMenuScreen, showWordGridScreen;
829
+ var init_screens = __esm({
830
+ "src/screen/screens.ts"() {
831
+ "use strict";
832
+ import_react3 = require("react");
833
+ import_ink3 = require("ink");
834
+ init_components();
835
+ init_list_components();
836
+ showMenuScreen = showListScreen;
837
+ showWordGridScreen = showMultiColumnListScreen;
838
+ }
839
+ });
625
840
 
626
- // src/params/index.ts
627
- var Params = class {
628
- params = {};
629
- definitions = {};
630
- args;
631
- paramSetters = [];
632
- paramGetters = [];
633
- constructor({ args }, opts = {}) {
634
- this.args = args;
635
- for (const [k, v] of Object.entries(opts)) {
636
- this.params[k] = v;
637
- }
841
+ // src/screen/ui-elements.ts
842
+ function ListItem({
843
+ children,
844
+ isSelected = false,
845
+ color = "white",
846
+ backgroundColor,
847
+ bold = false,
848
+ dimColor = false
849
+ }) {
850
+ return (0, import_react4.createElement)(
851
+ import_ink4.Box,
852
+ {},
853
+ (0, import_react4.createElement)(import_ink4.Text, {
854
+ color: isSelected ? backgroundColor || "green" : color,
855
+ backgroundColor: isSelected ? color : backgroundColor,
856
+ bold: isSelected || bold,
857
+ dimColor: !isSelected && dimColor
858
+ }, children)
859
+ );
860
+ }
861
+ function TextBlock({
862
+ text,
863
+ color = "white",
864
+ dimmed = false,
865
+ bold = false,
866
+ maxWidth
867
+ }) {
868
+ return (0, import_react4.createElement)(
869
+ import_ink4.Box,
870
+ {},
871
+ (0, import_react4.createElement)(import_ink4.Text, {
872
+ color,
873
+ dimColor: dimmed,
874
+ bold
875
+ }, text)
876
+ );
877
+ }
878
+ function Divider({ character = "\u2500", width = 80 }) {
879
+ return (0, import_react4.createElement)(
880
+ import_ink4.Box,
881
+ { marginY: 1 },
882
+ (0, import_react4.createElement)(import_ink4.Text, { dimColor: true }, character.repeat(width))
883
+ );
884
+ }
885
+ function GridCell({
886
+ children,
887
+ width,
888
+ color = "white",
889
+ backgroundColor,
890
+ bold = false,
891
+ dimColor = false,
892
+ align = "left"
893
+ }) {
894
+ return (0, import_react4.createElement)(
895
+ import_ink4.Box,
896
+ { width },
897
+ (0, import_react4.createElement)(import_ink4.Text, {
898
+ color,
899
+ backgroundColor,
900
+ bold,
901
+ dimColor,
902
+ textAlign: align
903
+ }, children)
904
+ );
905
+ }
906
+ function InputField({ prompt, value, onChange, onSubmit }) {
907
+ return (0, import_react4.createElement)(
908
+ import_ink4.Box,
909
+ { flexDirection: "column" },
910
+ (0, import_react4.createElement)(import_ink4.Text, {}, prompt),
911
+ (0, import_react4.createElement)(
912
+ import_ink4.Box,
913
+ { marginTop: 1 },
914
+ (0, import_react4.createElement)(import_ink4.Text, { color: "cyan" }, " > ", value, "_")
915
+ )
916
+ );
917
+ }
918
+ var import_react4, import_ink4;
919
+ var init_ui_elements = __esm({
920
+ "src/screen/ui-elements.ts"() {
921
+ "use strict";
922
+ import_react4 = require("react");
923
+ import_ink4 = require("ink");
638
924
  }
639
- /**
640
- * Assign a parameter definition
641
- */
642
- assignDefinition(key, definition) {
643
- if (this.definitions[key] && !definition) {
644
- return this.definitions[key];
645
- }
646
- let type;
647
- if (!definition) {
648
- type = import_joi.default.string();
649
- } else if (import_joi.default.isSchema(definition)) {
650
- type = definition;
651
- } else if (import_joi.default.isSchema(definition.type)) {
652
- type = definition.type;
653
- } else if (typeof definition === "string") {
654
- type = this.toJoi(definition);
655
- } else if (typeof definition.type === "string") {
656
- type = this.toJoi(definition.type);
657
- } else if (!definition.type) {
658
- type = import_joi.default.string();
659
- } else {
660
- type = import_joi.default.string();
661
- }
662
- if (!this.definitions[key]) {
663
- this.definitions[key] = {};
664
- }
665
- this.definitions[key].type = type;
666
- if (definition && definition.values) {
667
- if (Array.isArray(definition.values)) {
668
- this.definitions[key].values = definition.values;
669
- }
670
- }
671
- return this.definitions[key];
925
+ });
926
+
927
+ // src/screen/utils.ts
928
+ function buildBreadcrumb(parts) {
929
+ if (parts.length === 0) return "";
930
+ if (parts.length === 1) return parts[0];
931
+ return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
932
+ }
933
+ function buildDetailBreadcrumb(path4, suffix = "") {
934
+ if (path4.length <= 1) {
935
+ return suffix ? `\u2190 ${suffix}` : path4[0] || "";
672
936
  }
673
- /**
674
- * Convert string definition to Joi schema
675
- */
676
- toJoi(str) {
677
- let type;
678
- if (str.match(/^string|^text/i)) {
679
- type = import_joi.default.string();
680
- } else if (str.match(/^number|^integer|^int/i)) {
681
- type = import_joi.default.number();
682
- } else if (str.match(/^boolean|^bool/i)) {
683
- type = import_joi.default.boolean();
684
- } else if (str.match(/^date/i)) {
685
- type = import_joi.default.custom(joiEdateType);
686
- } else if (str.match(/^duration/i)) {
687
- type = import_joi.default.string().isoDuration();
688
- } else if (str.match(/^array/i)) {
689
- let elementTypes = "string";
690
- const tmp = str.match(/\((.*)\)/);
691
- if (tmp && tmp[1].match(/string/i)) {
692
- elementTypes = "string";
693
- } else if (tmp && tmp[1].match(/number|integer|int/i)) {
694
- elementTypes = "number";
695
- } else if (tmp && tmp[1].match(/boolean|bool/i)) {
696
- elementTypes = "boolean";
697
- }
698
- type = import_joi.default.custom(joiStringArrayType(elementTypes));
699
- } else {
700
- type = import_joi.default.string();
701
- }
702
- const regexForDefault = /\bdefault\s+([^\s]+)/;
703
- const matchForDefault = str.match(regexForDefault);
704
- if (matchForDefault) {
705
- const defValObj = type.validate(matchForDefault[1]);
706
- if (defValObj.error) {
707
- throw new ParamError(`default value "${defValObj.value}" type mismatch`);
708
- }
709
- type = type.default(defValObj.value);
710
- } else if (str.match(/required/)) {
711
- type = type.required();
712
- }
713
- return type;
937
+ const breadcrumb = buildBreadcrumb(path4);
938
+ return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
939
+ }
940
+ var init_utils = __esm({
941
+ "src/screen/utils.ts"() {
942
+ "use strict";
714
943
  }
715
- /**
716
- * Validate a value against a definition
717
- */
718
- validate(key, val, def) {
719
- const { value, error } = def.type.validate(val, { context: { params: this.params } });
720
- if (error) {
721
- const errs = error.details.map((el) => el.message).join(", ");
722
- throw new ParamError(`"${key}" validation error: ${errs}`);
723
- }
724
- return value;
944
+ });
945
+
946
+ // src/screen/footer-builder.ts
947
+ function buildFooter(config2 = {}) {
948
+ const {
949
+ navigation = null,
950
+ actions = null,
951
+ info = null,
952
+ escape = "Esc to go back",
953
+ custom = null
954
+ } = config2;
955
+ const lines = [];
956
+ const mainParts = [];
957
+ if (navigation) {
958
+ mainParts.push(navigation);
725
959
  }
726
- /**
727
- * Get a parameter value with validation
728
- */
729
- get(key, definition) {
730
- const def = this.assignDefinition(key, definition);
731
- let valFromGetters = void 0;
732
- if (def.volatile || true) {
733
- valFromGetters = this.runAllRegisteredGetters(key);
734
- }
735
- const valFromArgs = this.args.get(key);
736
- const valFromParams = this.params[key];
737
- const res = valFromGetters ? this.validate(key, valFromGetters, def) : valFromArgs ? this.validate(key, valFromArgs, def) : this.validate(key, valFromParams, def);
738
- if (res !== void 0 && def.values && !def.values.includes(res)) {
739
- throw new ParamError(`key ${key} should be one of ${def.values}`);
740
- }
741
- return res;
960
+ if (actions) {
961
+ mainParts.push(actions);
742
962
  }
743
- /**
744
- * Set a parameter value with validation
745
- */
746
- set(key, val, definition) {
747
- if (val && val.type && val.value) {
748
- definition = val;
749
- val = val.value;
750
- }
751
- const def = this.assignDefinition(key, definition);
752
- if (!this.runAllRegisteredSetters(key, val)) {
753
- this.params[key] = val;
754
- }
963
+ if (escape) {
964
+ mainParts.push(escape);
755
965
  }
756
- /**
757
- * Get all parameters from definitions
758
- * Processes parameters left-to-right to support cross-parameter references
759
- */
760
- getAll(defs) {
761
- const res = {};
762
- for (const [k, def] of Object.entries(defs)) {
763
- const value = this.get(k, def);
764
- res[k] = value;
765
- if (value !== void 0) {
766
- this.params[k] = value;
767
- }
768
- }
769
- return res;
966
+ if (mainParts.length > 0) {
967
+ lines.push(mainParts.join(", "));
770
968
  }
771
- /**
772
- * Run all registered getters for a key
773
- */
774
- runAllRegisteredGetters(key) {
775
- let val = null;
776
- for (const getter of this.paramGetters) {
777
- val = getter(key, this.definitions[key]);
778
- if (val !== void 0) {
779
- break;
780
- }
781
- }
782
- return val;
969
+ if (info) {
970
+ const infoLines = Array.isArray(info) ? info : [info];
971
+ lines.push(...infoLines);
783
972
  }
784
- /**
785
- * Run all registered setters for a key
786
- */
787
- runAllRegisteredSetters(key, value) {
788
- let setterUsed = false;
789
- for (const setter of this.paramSetters) {
790
- setterUsed = setter(key, value);
791
- if (setterUsed) {
792
- break;
973
+ if (custom) {
974
+ const customLines = Array.isArray(custom) ? custom : [custom];
975
+ lines.push(...customLines);
976
+ }
977
+ return lines;
978
+ }
979
+ function organizeFooterMessages(messages) {
980
+ if (!messages || messages.length === 0) {
981
+ return ["Esc to go back"];
982
+ }
983
+ const navigation = messages.filter((m) => m.includes("\u2191") || m.includes("\u2193") || m.includes("\u2190") || m.includes("\u2192"));
984
+ const actions = messages.filter((m) => m.includes("Enter") || m.includes("select") || m.includes("submit"));
985
+ const escape = messages.filter((m) => m.includes("Esc"));
986
+ const others = messages.filter(
987
+ (m) => !navigation.includes(m) && !actions.includes(m) && !escape.includes(m)
988
+ );
989
+ const lines = [];
990
+ const mainLine = [...navigation, ...actions, ...escape].join(", ");
991
+ if (mainLine) lines.push(mainLine);
992
+ lines.push(...others);
993
+ return lines;
994
+ }
995
+ var FooterPresets;
996
+ var init_footer_builder = __esm({
997
+ "src/screen/footer-builder.ts"() {
998
+ "use strict";
999
+ FooterPresets = {
1000
+ /**
1001
+ * Menu screen footer
1002
+ */
1003
+ menu: (customInfo = null) => buildFooter({
1004
+ navigation: "\u2191/\u2193 to navigate",
1005
+ actions: "Enter to select",
1006
+ escape: "Esc to go back",
1007
+ info: customInfo
1008
+ }),
1009
+ /**
1010
+ * Word grid footer
1011
+ */
1012
+ wordGrid: (totalWords) => buildFooter({
1013
+ navigation: "\u2191\u2193\u2190\u2192 to navigate",
1014
+ actions: "Enter to select",
1015
+ escape: "Esc to go back",
1016
+ info: `Total: ${totalWords} words`
1017
+ }),
1018
+ /**
1019
+ * Text input footer
1020
+ */
1021
+ textInput: () => buildFooter({
1022
+ actions: "Type and press Enter to submit",
1023
+ escape: "Esc to cancel"
1024
+ }),
1025
+ /**
1026
+ * Info/static screen footer
1027
+ */
1028
+ info: () => buildFooter({
1029
+ escape: "Esc to continue"
1030
+ }),
1031
+ /**
1032
+ * Main menu footer (escape exits)
1033
+ */
1034
+ mainMenu: () => buildFooter({
1035
+ navigation: "\u2191/\u2193 to navigate",
1036
+ actions: "Enter to select",
1037
+ escape: "Esc to exit"
1038
+ }),
1039
+ /**
1040
+ * Action menu footer (for word cards, etc.)
1041
+ */
1042
+ actionMenu: (hasAudio = false) => {
1043
+ const parts = buildFooter({
1044
+ navigation: "\u2191/\u2193 to navigate",
1045
+ actions: "Enter to select",
1046
+ escape: "Esc to go back"
1047
+ });
1048
+ if (hasAudio) {
1049
+ parts.push("Audio available");
1050
+ }
1051
+ return parts;
793
1052
  }
1053
+ };
1054
+ }
1055
+ });
1056
+
1057
+ // src/screen/index.ts
1058
+ async function load() {
1059
+ if (loadPromise) return loadPromise;
1060
+
1061
+ loadPromise = Promise.resolve();
1062
+ return loadPromise;
1063
+ }
1064
+ var import_react5, import_ink5, loadPromise;
1065
+ var init_screen = __esm({
1066
+ "src/screen/index.ts"() {
1067
+ "use strict";
1068
+ import_react5 = __toESM(require("react"), 1);
1069
+ import_ink5 = require("ink");
1070
+ init_screens();
1071
+ init_list_components();
1072
+ init_components();
1073
+ init_ui_elements();
1074
+ init_utils();
1075
+ init_footer_builder();
1076
+ loadPromise = null;
1077
+ if (typeof window === "undefined") {
1078
+ load().catch(() => {
1079
+ });
794
1080
  }
795
- return setterUsed;
1081
+ }
1082
+ });
1083
+
1084
+ // src/index.ts
1085
+ var src_exports = {};
1086
+ __export(src_exports, {
1087
+ Args: () => Args,
1088
+ Box: () => import_ink5.Box,
1089
+ Db: () => Db,
1090
+ Divider: () => Divider,
1091
+ FileDatabase: () => FileDatabase,
1092
+ FileDatabaseError: () => FileDatabaseError,
1093
+ FooterPresets: () => FooterPresets,
1094
+ GridCell: () => GridCell,
1095
+ InputField: () => InputField,
1096
+ ListComponent: () => ListComponent,
1097
+ ListItem: () => ListItem,
1098
+ MultiColumnListComponent: () => MultiColumnListComponent,
1099
+ MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
1100
+ Params: () => Params,
1101
+ React: () => import_react5.default,
1102
+ ScreenBody: () => ScreenBody,
1103
+ ScreenContainer: () => ScreenContainer,
1104
+ ScreenDivider: () => ScreenDivider,
1105
+ ScreenFooter: () => ScreenFooter,
1106
+ ScreenRow: () => ScreenRow,
1107
+ ScreenTitle: () => ScreenTitle,
1108
+ Text: () => import_ink5.Text,
1109
+ TextBlock: () => TextBlock,
1110
+ buildBreadcrumb: () => buildBreadcrumb,
1111
+ buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1112
+ buildFooter: () => buildFooter,
1113
+ defaultFileSynopsisFunction: () => defaultFileSynopsisFunction,
1114
+ defaultVersionSynopsisFunction: () => defaultVersionSynopsisFunction,
1115
+ getArgsInstance: () => getArgsInstance,
1116
+ h: () => import_react5.createElement,
1117
+ joiEdateType: () => joiEdateType,
1118
+ joiStringArrayType: () => joiStringArrayType,
1119
+ load: () => load,
1120
+ organizeFooterMessages: () => organizeFooterMessages,
1121
+ setupContext: () => setupContext,
1122
+ showListScreen: () => showListScreen,
1123
+ showMenuScreen: () => showMenuScreen,
1124
+ showMultiColumnListScreen: () => showMultiColumnListScreen,
1125
+ showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
1126
+ showScreen: () => showScreen,
1127
+ showWordGridScreen: () => showWordGridScreen,
1128
+ useCallback: () => import_react5.useCallback,
1129
+ useEffect: () => import_react5.useEffect,
1130
+ useInput: () => import_ink5.useInput,
1131
+ useMemo: () => import_react5.useMemo,
1132
+ useRef: () => import_react5.useRef,
1133
+ useState: () => import_react5.useState
1134
+ });
1135
+ module.exports = __toCommonJS(src_exports);
1136
+
1137
+ // src/args/index.ts
1138
+ var import_fs = require("fs");
1139
+ var import_path = require("path");
1140
+ var import_dotenv = require("dotenv");
1141
+ var Args = class _Args {
1142
+ args = {};
1143
+ flags = {};
1144
+ options = {};
1145
+ commands = [];
1146
+ usedKeys = /* @__PURE__ */ new Set();
1147
+ aliases = {};
1148
+ overrides = {};
1149
+ defaults = {};
1150
+ prefixes = [];
1151
+ nots = [];
1152
+ configValues = {};
1153
+ configsLoaded = [];
1154
+ env = "local";
1155
+ constructor(config2 = {}) {
1156
+ this.aliases = {};
1157
+ this.overrides = {};
1158
+ this.defaults = {};
1159
+ this.prefixes = ["not", "no"];
1160
+ if (Object.keys(config2).length > 0) {
1161
+ this.configure(config2);
1162
+ }
1163
+ const args = config2.args || process.argv.slice(2);
1164
+ this.parseArgs(args);
1165
+ this.env = this.get("env")?.toLowerCase() || "local";
1166
+ this.loadDotEnv();
1167
+ this.loadConfigFiles();
1168
+ this.checkConflicts();
796
1169
  }
797
1170
  /**
798
- * Register a parameter getter
1171
+ * Configure Args options
1172
+ * Only parameters present in config are updated
1173
+ * Note: Args is special - it's initialized first, so it can't take context
799
1174
  */
800
- registerParamGetter(fn) {
801
- this.paramGetters.push(fn);
1175
+ configure(config2) {
1176
+ if (config2.aliases !== void 0) {
1177
+ this.aliases = config2.aliases;
1178
+ }
1179
+ if (config2.overrides !== void 0) {
1180
+ this.overrides = config2.overrides;
1181
+ }
1182
+ if (config2.defaults !== void 0) {
1183
+ this.defaults = config2.defaults;
1184
+ }
1185
+ if (config2.prefixes !== void 0) {
1186
+ this.prefixes = config2.prefixes;
1187
+ }
802
1188
  }
803
1189
  /**
804
- * Register a parameter setter
1190
+ * Initialize Args instance
1191
+ * Note: Args is special - it's initialized first, so it can't take context
1192
+ * This static method is for consistency with other components
805
1193
  */
806
- registerParamSetter(fn) {
807
- this.paramSetters.push(fn);
1194
+ static init(config2 = {}) {
1195
+ return new _Args(config2);
808
1196
  }
809
- };
810
- var paramsInstance = null;
811
- var getParamsInstance = () => paramsInstance;
812
-
813
- // src/screen/index.ts
814
- var import_react5 = __toESM(require("react"), 1);
815
- var import_ink5 = require("ink");
816
-
817
- // src/screen/screens.ts
818
- var import_react3 = require("react");
819
- var import_ink3 = require("ink");
820
-
821
- // src/screen/components.ts
822
- var import_react = require("react");
823
- var import_ink = require("ink");
824
- function getScreenWidth(maxWidth = null) {
825
- const terminalWidth = process.stdout.columns || 80;
826
- const availableWidth = Math.max(20, terminalWidth - 4);
827
- return maxWidth ? Math.min(availableWidth, maxWidth) : availableWidth;
828
- }
829
- function ScreenContainer({ children }) {
830
- const width = getScreenWidth();
831
- return (0, import_react.createElement)(import_ink.Box, {
832
- flexDirection: "column",
833
- marginTop: 1,
834
- borderStyle: "single",
835
- borderColor: "cyan",
836
- paddingX: 1,
837
- width
838
- // Use the calculated width directly
839
- }, children);
840
- }
841
- function ScreenRow({ children }) {
842
- return (0, import_react.createElement)(import_ink.Box, { flexDirection: "column" }, children);
843
- }
844
- function ScreenTitle({ text }) {
845
- return (0, import_react.createElement)(
846
- ScreenRow,
847
- {},
848
- (0, import_react.createElement)(import_ink.Text, { bold: true, color: "cyan" }, text)
849
- );
850
- }
851
- function ScreenDivider({ width }) {
852
- const dividerWidth = width || getScreenWidth() - 4;
853
- return (0, import_react.createElement)(import_ink.Text, { color: "cyan", dimColor: true }, "\u2500".repeat(dividerWidth));
854
- }
855
- function ScreenBody({ children, alignItems = "flex-start" }) {
856
- return (0, import_react.createElement)(import_ink.Box, { flexDirection: "column", alignItems }, children);
857
- }
858
- function ScreenFooter({ lines, textStyle }) {
859
- const defaultTextStyle = {
860
- dimColor: true,
861
- color: "white"
862
- };
863
- const finalTextStyle = { ...defaultTextStyle, ...textStyle };
864
- const flattenAndWrap = (items, keyPrefix = "") => {
865
- const result = [];
866
- let keyIndex = 0;
867
- items.forEach((item, index) => {
868
- if (Array.isArray(item)) {
869
- const nested = flattenAndWrap(item, `${keyPrefix}-${index}`);
870
- result.push(...nested);
871
- } else if (typeof item === "string") {
872
- result.push(
873
- (0, import_react.createElement)(import_ink.Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, item)
874
- );
875
- } else {
876
- const element = item;
877
- if (element.key === null || element.key === void 0) {
878
- result.push(
879
- (0, import_react.createElement)(import_ink.Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, element)
880
- );
1197
+ /**
1198
+ * Parse command line arguments
1199
+ */
1200
+ parseArgs(args) {
1201
+ let i = 0;
1202
+ while (i < args.length) {
1203
+ const arg = args[i];
1204
+ if (arg.startsWith("--")) {
1205
+ const [key, value] = this.parseLongOption(arg);
1206
+ this.setValue(key, value);
1207
+ i++;
1208
+ } else if (arg.startsWith("-")) {
1209
+ const result = this.parseShortOption(arg, args, i);
1210
+ if (result.consumed > 0) {
1211
+ i += result.consumed;
881
1212
  } else {
882
- result.push(element);
1213
+ i++;
883
1214
  }
884
- }
885
- });
886
- return result;
887
- };
888
- const wrappedItems = flattenAndWrap(lines);
889
- return (0, import_react.createElement)(
890
- import_ink.Box,
891
- { flexDirection: "column" },
892
- (0, import_react.createElement)(import_ink.Box, { flexDirection: "row" }, ...wrappedItems)
893
- );
894
- }
895
-
896
- // src/screen/list-components.ts
897
- var import_react2 = __toESM(require("react"), 1);
898
- var import_ink2 = require("ink");
899
- var h2 = import_react2.createElement;
900
- function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
901
- const [, forceUpdate] = (0, import_react2.useState)({});
902
- const termWidth = (process.stdout.columns || 80) - 8;
903
- const maxItemLength = Math.max(...items.map((w) => w.length));
904
- const columnWidth = maxItemLength + 3;
905
- const columns = Math.max(1, Math.floor(termWidth / columnWidth));
906
- const itemsPerColumn = Math.ceil(items.length / columns);
907
- (0, import_react2.useEffect)(() => {
908
- ctx.setAction("moveUp", () => {
909
- selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
910
- forceUpdate({});
911
- });
912
- ctx.setAction("moveDown", () => {
913
- selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
914
- forceUpdate({});
915
- });
916
- ctx.setAction("moveLeft", () => {
917
- if (selectedIndexRef.current === 0) {
918
- ctx.goBack();
919
1215
  } else {
920
- selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
921
- forceUpdate({});
922
- }
923
- });
924
- ctx.setAction("moveRight", () => {
925
- selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
926
- forceUpdate({});
927
- });
928
- ctx.setKeyBinding([
929
- { key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
930
- { key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
931
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
932
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
933
- ]);
934
- ctx.addFooter(`Total: ${items.length} items`);
935
- }, []);
936
- const selectedIndex = selectedIndexRef.current;
937
- const rows = [];
938
- for (let row = 0; row < itemsPerColumn; row++) {
939
- const cols = [];
940
- for (let col = 0; col < columns; col++) {
941
- const index = col * itemsPerColumn + row;
942
- if (index < items.length) {
943
- const isSelected = index === selectedIndex;
944
- cols.push(
945
- h2(
946
- import_ink2.Box,
947
- { key: index, width: columnWidth },
948
- h2(import_ink2.Text, {
949
- color: isSelected ? "black" : "white",
950
- backgroundColor: isSelected ? "cyan" : void 0,
951
- bold: isSelected
952
- }, items[index].padEnd(maxItemLength))
953
- )
954
- );
1216
+ this.commands.push(arg);
1217
+ i++;
955
1218
  }
956
1219
  }
957
- rows.push(
958
- h2(ScreenRow, { key: row, children: h2(import_ink2.Box, { flexDirection: "row" }, ...cols) })
959
- );
960
1220
  }
961
- return h2(import_ink2.Box, { flexDirection: "column" }, ...rows);
962
- }
963
- function MultiColumnListWithPreviewComponent({
964
- items,
965
- getPreviewContent,
966
- ctx,
967
- selectedIndexRef
968
- }) {
969
- const [, forceUpdate] = (0, import_react2.useState)({});
970
- const termWidth = (process.stdout.columns || 80) - 8;
971
- const maxItemLength = Math.max(...items.map((w) => w.length));
972
- const columnWidth = maxItemLength + 3;
973
- const columns = Math.max(1, Math.floor(termWidth / columnWidth));
974
- const itemsPerColumn = Math.ceil(items.length / columns);
975
- (0, import_react2.useEffect)(() => {
976
- ctx.setAction("moveUp", () => {
977
- selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
978
- forceUpdate({});
979
- });
980
- ctx.setAction("moveDown", () => {
981
- selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
982
- forceUpdate({});
983
- });
984
- ctx.setAction("moveLeft", () => {
985
- if (selectedIndexRef.current === 0) {
986
- ctx.goBack();
987
- } else {
988
- selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
989
- forceUpdate({});
990
- }
991
- });
992
- ctx.setAction("moveRight", () => {
993
- selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
994
- forceUpdate({});
995
- });
996
- ctx.setKeyBinding([
997
- { key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
998
- { key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
999
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
1000
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
1001
- ]);
1002
- ctx.addFooter(`Total: ${items.length} items`);
1003
- }, []);
1004
- const selectedIndex = selectedIndexRef.current;
1005
- const selectedItem = items[selectedIndex];
1006
- const rows = [];
1007
- for (let row = 0; row < itemsPerColumn; row++) {
1008
- const cols = [];
1009
- for (let col = 0; col < columns; col++) {
1010
- const index = col * itemsPerColumn + row;
1011
- if (index < items.length) {
1012
- const isSelected = index === selectedIndex;
1013
- cols.push(
1014
- h2(
1015
- import_ink2.Box,
1016
- { key: index, width: columnWidth },
1017
- h2(import_ink2.Text, {
1018
- color: isSelected ? "black" : "white",
1019
- backgroundColor: isSelected ? "cyan" : void 0,
1020
- bold: isSelected
1021
- }, items[index].padEnd(maxItemLength))
1022
- )
1023
- );
1221
+ /**
1222
+ * Parse long option (--key=value or --key)
1223
+ */
1224
+ parseLongOption(arg) {
1225
+ const key = arg.slice(2);
1226
+ const prefix = this.prefixes.find((p) => key.startsWith(p));
1227
+ if (prefix) {
1228
+ let strippedKey = key.slice(prefix.length);
1229
+ if (strippedKey.startsWith("-")) {
1230
+ strippedKey = strippedKey.slice(1);
1024
1231
  }
1232
+ this.nots.push(key);
1233
+ return [strippedKey, false];
1025
1234
  }
1026
- rows.push(
1027
- h2(ScreenRow, { key: row, children: h2(import_ink2.Box, { flexDirection: "row" }, ...cols) })
1028
- );
1029
- }
1030
- const previewContent = getPreviewContent ? getPreviewContent(selectedItem) : selectedItem;
1031
- const previewRows = [];
1032
- if (typeof previewContent === "string") {
1033
- previewRows.push(h2(ScreenRow, { key: "preview-string", children: h2(import_ink2.Text, { bold: true }, previewContent) }));
1034
- } else if (typeof previewContent === "object" && !import_react2.default.isValidElement(previewContent) && previewContent !== null) {
1035
- Object.entries(previewContent).forEach(([key, value], idx) => {
1036
- previewRows.push(h2(ScreenRow, { key: `preview-${key}-${idx}`, children: h2(import_ink2.Text, {}, `${key}: ${value}`) }));
1037
- });
1038
- } else if (import_react2.default.isValidElement(previewContent)) {
1039
- previewRows.push(h2(ScreenRow, { key: "preview-element", children: previewContent }));
1040
- }
1041
- return h2(
1042
- import_ink2.Box,
1043
- { flexDirection: "column" },
1044
- ...rows,
1045
- h2(ScreenRow, { key: "spacer-1", children: h2(import_ink2.Text, {}, " ") }),
1046
- h2(ScreenDivider, { key: "divider" }),
1047
- h2(ScreenRow, { key: "spacer-2", children: h2(import_ink2.Text, {}, " ") }),
1048
- ...previewRows
1049
- );
1050
- }
1051
- function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
1052
- const [, forceUpdate] = (0, import_react2.useState)({});
1053
- const [sortOrder, setSortOrder] = (0, import_react2.useState)("none");
1054
- const [scrollOffset, setScrollOffset] = (0, import_react2.useState)(0);
1055
- const scrollStateRef = (0, import_react2.useRef)({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
1056
- const defaultGetTitle = (item) => {
1057
- return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
1058
- };
1059
- const titleGetter = getTitle || defaultGetTitle;
1060
- const displayItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
1061
- const titleA = titleGetter(a).toLowerCase();
1062
- const titleB = titleGetter(b).toLowerCase();
1063
- if (sortOrder === "asc") {
1064
- return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
1235
+ if (key.includes("=")) {
1236
+ const eqIndex = key.indexOf("=");
1237
+ const optionKey = key.slice(0, eqIndex);
1238
+ const value = key.slice(eqIndex + 1);
1239
+ return [optionKey, this.parseValue(value)];
1065
1240
  } else {
1066
- return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
1241
+ return [key, true];
1067
1242
  }
1068
- }) : items;
1069
- const effectiveMaxHeight = maxHeight || displayItems.length;
1070
- const canScroll = displayItems.length > effectiveMaxHeight;
1071
- const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
1072
- const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
1073
- const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
1074
- const canScrollUp = clampedScrollOffset > 0;
1075
- const canScrollDown = clampedScrollOffset < maxScrollOffset;
1076
- scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
1077
- (0, import_react2.useEffect)(() => {
1078
- ctx.setAction("moveUp", () => {
1079
- const newIndex = Math.max(0, selectedIndexRef.current - 1);
1080
- selectedIndexRef.current = newIndex;
1081
- const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
1082
- const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
1083
- const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
1084
- if (newIndex < currentClampedScrollOffset) {
1085
- setScrollOffset(newIndex);
1086
- }
1087
- forceUpdate({});
1088
- });
1089
- ctx.setAction("moveDown", () => {
1090
- const currentItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
1091
- const titleA = titleGetter(a).toLowerCase();
1092
- const titleB = titleGetter(b).toLowerCase();
1093
- if (sortOrder === "asc") {
1094
- return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
1243
+ }
1244
+ /**
1245
+ * Parse short option (-k=value, -k, or bundled -vsd)
1246
+ */
1247
+ parseShortOption(arg, args, index) {
1248
+ const key = arg.slice(1);
1249
+ if (key.length === 1 && index + 1 < args.length && !args[index + 1].startsWith("-")) {
1250
+ const value = args[index + 1];
1251
+ this.setValue(key, this.parseValue(value));
1252
+ return { consumed: 2 };
1253
+ }
1254
+ if (key.length > 1 && !key.includes("=")) {
1255
+ for (let i = 0; i < key.length; i++) {
1256
+ const shortKey = key[i];
1257
+ if (shortKey in this.aliases) {
1258
+ this.setValue(shortKey, true);
1095
1259
  } else {
1096
- return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
1260
+ this.args[shortKey] = true;
1097
1261
  }
1098
- }) : items;
1099
- const maxIndex = currentItems.length - 1;
1100
- const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
1101
- selectedIndexRef.current = newIndex;
1102
- const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
1103
- const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
1104
- const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
1105
- if (newIndex >= currentClampedScrollOffset + currentMaxHeight) {
1106
- setScrollOffset(newIndex - currentMaxHeight + 1);
1107
1262
  }
1108
- forceUpdate({});
1109
- });
1110
- ctx.setAction("scrollUp", () => {
1111
- const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
1112
- const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
1113
- const newScrollOffset = Math.max(0, currentScrollOffset - 1);
1114
- setScrollOffset(newScrollOffset);
1115
- forceUpdate({});
1116
- });
1117
- ctx.setAction("scrollDown", () => {
1118
- const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
1119
- const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
1120
- const newScrollOffset = Math.min(currentMaxScrollOffset, currentScrollOffset + 1);
1121
- setScrollOffset(newScrollOffset);
1122
- forceUpdate({});
1123
- });
1124
- if (sortable) {
1125
- ctx.setAction("toggleSort", () => {
1126
- const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
1127
- const currentSelectedItem = displayItems[selectedIndexRef.current];
1128
- setSortOrder(nextSort);
1129
- const newSortedItems = nextSort !== "none" ? [...items].sort((a, b) => {
1130
- const titleA = titleGetter(a).toLowerCase();
1131
- const titleB = titleGetter(b).toLowerCase();
1132
- if (nextSort === "asc") {
1133
- return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
1263
+ return { consumed: 1 };
1264
+ }
1265
+ if (key.includes("=")) {
1266
+ const eqIndex = key.indexOf("=");
1267
+ const optionKey = key.slice(0, eqIndex);
1268
+ const value = key.slice(eqIndex + 1);
1269
+ if (optionKey.length > 1) {
1270
+ for (let i = 0; i < optionKey.length - 1; i++) {
1271
+ const shortKey = optionKey[i];
1272
+ if (shortKey in this.aliases) {
1273
+ this.setValue(shortKey, true);
1134
1274
  } else {
1135
- return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
1275
+ this.args[shortKey] = true;
1136
1276
  }
1137
- }) : items;
1138
- const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
1139
- if (newIndex !== -1) {
1140
- selectedIndexRef.current = newIndex;
1141
- setScrollOffset(newIndex);
1142
- } else {
1143
- selectedIndexRef.current = 0;
1144
- setScrollOffset(0);
1145
1277
  }
1146
- forceUpdate({});
1147
- });
1148
- const defaultHighlightStyle = {
1149
- color: "black",
1150
- backgroundColor: "green",
1151
- bold: true
1152
- };
1153
- const highlightStyle = { ...defaultHighlightStyle, ...sortHighlightStyle };
1154
- const sortCaption = () => {
1155
- if (sortOrder === "none") {
1156
- return h2(import_ink2.Text, {}, "s to toggle sort");
1278
+ const lastKey = optionKey[optionKey.length - 1];
1279
+ if (lastKey in this.aliases) {
1280
+ this.setValue(lastKey, this.parseValue(value));
1157
1281
  } else {
1158
- const sortLabel = sortOrder === "asc" ? "ASC" : "DESC";
1159
- return h2(
1160
- import_ink2.Text,
1161
- {},
1162
- "s to toggle ",
1163
- h2(import_ink2.Text, { color: "white", bold: true }, "sort"),
1164
- " ",
1165
- h2(import_ink2.Text, highlightStyle, ` ${sortLabel} `)
1166
- );
1167
- }
1168
- };
1169
- ctx.setKeyBinding([
1170
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
1171
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
1172
- {
1173
- key: "s",
1174
- caption: sortCaption,
1175
- action: "toggleSort",
1176
- order: 5
1282
+ this.args[lastKey] = this.parseValue(value);
1177
1283
  }
1178
- ]);
1179
- ctx.update();
1284
+ } else {
1285
+ this.setValue(optionKey, this.parseValue(value));
1286
+ }
1287
+ return { consumed: 1 };
1180
1288
  } else {
1181
- ctx.setKeyBinding([
1182
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
1183
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
1184
- ]);
1289
+ this.setValue(key, true);
1290
+ return { consumed: 1 };
1185
1291
  }
1186
- }, [sortOrder, sortable]);
1187
- const selectedIndex = selectedIndexRef.current;
1188
- const defaultRenderItem = (item, isSelected, displayIndex, actualIndex) => {
1189
- const isFirstVisible = displayIndex === 0;
1190
- const isLastVisible = displayIndex === visibleItems.length - 1;
1191
- let arrowPrefix = "";
1192
- let selectionPrefix = "";
1193
- if (isFirstVisible && canScrollUp) {
1194
- arrowPrefix = "\u2191 ";
1195
- } else if (isLastVisible && canScrollDown) {
1196
- arrowPrefix = "\u2193 ";
1197
- } else {
1198
- arrowPrefix = " ";
1292
+ }
1293
+ /**
1294
+ * Parse value (handle quotes)
1295
+ */
1296
+ parseValue(value) {
1297
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1298
+ return value.slice(1, -1);
1199
1299
  }
1200
- if (isSelected) {
1201
- selectionPrefix = selectionMarker;
1300
+ return value;
1301
+ }
1302
+ /**
1303
+ * Set a value with proper categorization
1304
+ */
1305
+ setValue(key, value) {
1306
+ const resolvedKey = this.aliases[key] || key;
1307
+ if (typeof value === "boolean") {
1308
+ this.flags[resolvedKey] = value;
1202
1309
  } else {
1203
- selectionPrefix = " ".repeat(selectionMarker.length);
1310
+ this.options[resolvedKey] = value;
1204
1311
  }
1205
- return h2(
1206
- import_ink2.Box,
1207
- { flexDirection: "row" },
1208
- // Arrow (clickable if functional, not highlighted)
1209
- h2(import_ink2.Text, {
1210
- key: `arrow-${actualIndex}`,
1211
- color: "white"
1212
- }, arrowPrefix),
1213
- // Selection marker space (always same width, not highlighted)
1214
- h2(import_ink2.Text, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
1215
- // Item name (highlighted if selected)
1216
- h2(import_ink2.Text, {
1217
- key: `name-${actualIndex}`,
1218
- color: isSelected ? "black" : "white",
1219
- backgroundColor: isSelected ? "cyan" : void 0,
1220
- bold: isSelected
1221
- }, item.name)
1222
- );
1223
- };
1224
- const itemRenderer = renderItem || defaultRenderItem;
1225
- return h2(
1226
- import_ink2.Box,
1227
- { flexDirection: "column" },
1228
- ...visibleItems.map((item, displayIndex) => {
1229
- const actualIndex = clampedScrollOffset + displayIndex;
1230
- const isSelected = actualIndex === selectedIndex;
1231
- if (renderItem) {
1232
- const isFirstVisible = displayIndex === 0;
1233
- const isLastVisible = displayIndex === visibleItems.length - 1;
1234
- let arrowPrefix = "";
1235
- let selectionPrefix = "";
1236
- if (isFirstVisible && canScrollUp) {
1237
- arrowPrefix = "\u2191 ";
1238
- } else if (isLastVisible && canScrollDown) {
1239
- arrowPrefix = "\u2193 ";
1240
- } else {
1241
- arrowPrefix = " ";
1242
- }
1243
- if (isSelected) {
1244
- selectionPrefix = selectionMarker;
1245
- } else {
1246
- selectionPrefix = " ".repeat(selectionMarker.length);
1247
- }
1248
- return h2(ScreenRow, {
1249
- key: `item-${actualIndex}`,
1250
- children: h2(
1251
- import_ink2.Box,
1252
- { flexDirection: "row" },
1253
- // Arrow (clickable if functional, not highlighted)
1254
- h2(import_ink2.Text, {
1255
- key: `arrow-${actualIndex}`,
1256
- color: "white"
1257
- }, arrowPrefix),
1258
- // Selection marker space (always same width, not highlighted)
1259
- h2(import_ink2.Text, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
1260
- // Custom rendered content
1261
- renderItem(item, isSelected, displayIndex)
1262
- )
1263
- });
1264
- } else {
1265
- return h2(ScreenRow, {
1266
- key: `item-${actualIndex}`,
1267
- children: itemRenderer(item, isSelected, displayIndex, actualIndex)
1268
- });
1312
+ this.args[resolvedKey.toLowerCase()] = value;
1313
+ }
1314
+ /**
1315
+ * Check for conflicts (short + long form of same option)
1316
+ */
1317
+ checkConflicts() {
1318
+ const conflicts = [];
1319
+ for (const [shortKey, longKey] of Object.entries(this.aliases)) {
1320
+ const hasShort = this.args[shortKey] !== void 0;
1321
+ const hasLong = this.args[longKey] !== void 0;
1322
+ if (hasShort && hasLong) {
1323
+ conflicts.push(`Both -${shortKey} and --${longKey} specified`);
1269
1324
  }
1270
- })
1271
- );
1272
- }
1273
-
1274
- // src/screen/screens.ts
1275
- function groupKeyBindings(bindings) {
1276
- const groups = {};
1277
- const enabledBindings = bindings.filter((b) => b.enabled !== false);
1278
- enabledBindings.forEach((binding) => {
1279
- const caption = typeof binding.caption === "string" ? binding.caption : "";
1280
- if (!groups[caption]) {
1281
- groups[caption] = {
1282
- keys: [],
1283
- caption,
1284
- order: binding.order || 999
1285
- };
1286
1325
  }
1287
- groups[caption].keys.push(binding.key);
1288
- });
1289
- return Object.values(groups);
1290
- }
1291
- function formatKeyBindings(bindings, mode = "long") {
1292
- const resolvedBindings = bindings.map((binding) => {
1293
- let resolvedCaption = binding.caption;
1294
- if (typeof binding.caption === "function") {
1295
- resolvedCaption = binding.caption();
1326
+ if (conflicts.length > 0) {
1327
+ throw new Error(`Argument conflicts: ${conflicts.join(", ")}`);
1296
1328
  }
1297
- return {
1298
- ...binding,
1299
- resolvedCaption
1300
- };
1301
- });
1302
- const groups = groupKeyBindings(resolvedBindings.map((b) => ({
1303
- ...b,
1304
- caption: typeof b.resolvedCaption === "string" ? b.resolvedCaption : ""
1305
- })));
1306
- groups.sort((a, b) => a.order - b.order);
1307
- const items = [];
1308
- groups.forEach((group) => {
1309
- const bindingWithCustom = resolvedBindings.find(
1310
- (b) => group.keys.includes(b.key) && typeof b.resolvedCaption !== "string"
1329
+ }
1330
+ /**
1331
+ * Get a value with precedence order
1332
+ */
1333
+ get(key) {
1334
+ const resolvedKey = this.aliases[key] || key;
1335
+ this.usedKeys.add(resolvedKey);
1336
+ if (this.overrides[resolvedKey] !== void 0) {
1337
+ return this.overrides[resolvedKey];
1338
+ }
1339
+ const lcKey = resolvedKey.toLowerCase();
1340
+ const lcKeyWithEnv = `${lcKey}${this.env ? `_${this.env.toLowerCase()}` : ""}`;
1341
+ if (this.env && this.args[lcKeyWithEnv] !== void 0) {
1342
+ return this.args[lcKeyWithEnv];
1343
+ } else if (this.args[lcKey] !== void 0) {
1344
+ return this.args[lcKey];
1345
+ }
1346
+ if (this.configValues[resolvedKey] !== void 0) {
1347
+ return this.configValues[resolvedKey];
1348
+ }
1349
+ const envKey = this.toEnvKey(resolvedKey);
1350
+ const envKeyWithEnv = `${envKey}${this.env ? `_${this.env.toUpperCase()}` : ""}`;
1351
+ const envSpecificKey = Object.keys(process.env).find(
1352
+ (k) => this.env && k.toUpperCase() === envKeyWithEnv
1311
1353
  );
1312
- if (bindingWithCustom && bindingWithCustom.resolvedCaption) {
1313
- items.push(bindingWithCustom.resolvedCaption);
1314
- } else {
1315
- const keyStr = formatKeys(group.keys);
1316
- if (mode === "long") {
1317
- items.push(`${keyStr} to ${group.caption}`);
1318
- } else {
1319
- items.push(keyStr);
1354
+ const envKeyFound = Object.keys(process.env).find((k) => k.toUpperCase() === envKey);
1355
+ if (envSpecificKey) {
1356
+ return process.env[envSpecificKey];
1357
+ } else if (envKeyFound) {
1358
+ return process.env[envKeyFound];
1359
+ }
1360
+ if (this.defaults[resolvedKey] !== void 0) {
1361
+ return this.defaults[resolvedKey];
1362
+ }
1363
+ if (resolvedKey === "env" && process.env.NODE_ENV !== void 0) {
1364
+ return process.env.NODE_ENV;
1365
+ }
1366
+ return void 0;
1367
+ }
1368
+ /**
1369
+ * Set a value (for testing/internal use)
1370
+ */
1371
+ set(key, value) {
1372
+ this.args[key] = value;
1373
+ }
1374
+ /**
1375
+ * Check if a command exists (case-insensitive)
1376
+ */
1377
+ hasCommand(cmd) {
1378
+ return this.commands.some((command) => command.toLowerCase() === cmd.toLowerCase());
1379
+ }
1380
+ /**
1381
+ * Get all commands
1382
+ */
1383
+ getCommands() {
1384
+ return [...this.commands];
1385
+ }
1386
+ /**
1387
+ * Get used keys (as array)
1388
+ */
1389
+ getUsed() {
1390
+ return Array.from(this.usedKeys);
1391
+ }
1392
+ /**
1393
+ * Get unused keys (as array)
1394
+ */
1395
+ getUnused() {
1396
+ const unused = [];
1397
+ for (const key of Object.keys(this.args)) {
1398
+ if (!this.usedKeys.has(key) && !this.nots.includes(key)) {
1399
+ unused.push(key);
1320
1400
  }
1321
1401
  }
1322
- });
1323
- return items;
1324
- }
1325
- function formatKeys(keys) {
1326
- const keyMap = {
1327
- "escape": "esc",
1328
- "leftArrow": "\u2190",
1329
- "rightArrow": "\u2192",
1330
- "upArrow": "\u2191",
1331
- "downArrow": "\u2193",
1332
- "return": "enter"
1333
- };
1334
- return keys.map((k) => keyMap[k] || k).join("/");
1335
- }
1336
- async function showScreen(config2) {
1337
- const {
1338
- title,
1339
- onRender,
1340
- parentData = {}
1341
- } = config2;
1342
- return new Promise((resolve2) => {
1343
- let instance2;
1344
- const keyBindings = [];
1345
- const actions = {};
1346
- const customFooterItems = [];
1347
- let renderResult = null;
1348
- let initialized = false;
1349
- const Screen = () => {
1350
- const [updateCounter, setUpdateCounter] = (0, import_react3.useState)(0);
1351
- if (!initialized) {
1352
- const defaultBindings = [
1353
- { key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
1354
- { key: "leftArrow", caption: "go back", action: "back", protected: false, order: 1 }
1355
- // Note: 'select' is not a default - components add it if needed
1356
- ];
1357
- defaultBindings.forEach((binding) => {
1358
- keyBindings.push(binding);
1359
- });
1360
- actions.back = () => {
1361
- cleanup(null);
1362
- };
1363
- initialized = true;
1402
+ return unused;
1403
+ }
1404
+ /**
1405
+ * Convert key to environment variable format
1406
+ */
1407
+ toEnvKey(key) {
1408
+ return key.replace(
1409
+ /[A-Z0-9]/g,
1410
+ (match, offset) => offset === 0 ? match : "_" + match.toLowerCase()
1411
+ ).toUpperCase();
1412
+ }
1413
+ /**
1414
+ * Load .env file
1415
+ */
1416
+ loadDotEnv() {
1417
+ const dotEnvPath = this.get("dotEnvPath") || process.cwd();
1418
+ const dotEnvFile = this.get("dotEnvFile") || ".env";
1419
+ if (this.get("dotEnvFile")) {
1420
+ const customPath = (0, import_path.resolve)(dotEnvPath, dotEnvFile);
1421
+ if ((0, import_fs.existsSync)(customPath)) {
1422
+ (0, import_dotenv.config)({ path: customPath, quiet: true });
1364
1423
  }
1365
- const context = {
1366
- setAction: (actionName, handlerFn) => {
1367
- actions[actionName] = handlerFn;
1368
- },
1369
- setKeyBinding: (bindingOrBindings) => {
1370
- const bindingsToSet = Array.isArray(bindingOrBindings) ? bindingOrBindings : [bindingOrBindings];
1371
- bindingsToSet.forEach((binding) => {
1372
- const existingIndex = keyBindings.findIndex((b) => b.key === binding.key);
1373
- if (existingIndex >= 0) {
1374
- const existing = keyBindings[existingIndex];
1375
- if (existing.protected) {
1376
- console.warn(`Cannot override protected key: ${binding.key}`);
1377
- return;
1378
- }
1379
- keyBindings[existingIndex] = {
1380
- ...existing,
1381
- ...binding,
1382
- order: binding.order !== void 0 ? binding.order : existing.order,
1383
- enabled: binding.enabled !== void 0 ? binding.enabled : existing.enabled !== void 0 ? existing.enabled : true
1384
- };
1385
- } else {
1386
- keyBindings.push({
1387
- protected: false,
1388
- order: 999,
1389
- enabled: true,
1390
- ...binding
1391
- });
1392
- }
1393
- });
1394
- },
1395
- updateKeyBinding: (keyName, updates) => {
1396
- const index = keyBindings.findIndex((b) => b.key === keyName);
1397
- if (index >= 0) {
1398
- keyBindings[index] = {
1399
- ...keyBindings[index],
1400
- ...updates
1401
- };
1402
- }
1403
- },
1404
- removeKeyBinding: (keyName) => {
1405
- const index = keyBindings.findIndex((b) => b.key === keyName);
1406
- if (index >= 0) {
1407
- if (keyBindings[index].protected) {
1408
- console.warn(`Cannot remove protected key: ${keyName}`);
1409
- return;
1410
- }
1411
- keyBindings.splice(index, 1);
1412
- }
1413
- },
1414
- addFooter: (item) => {
1415
- customFooterItems.push(item);
1416
- },
1417
- clearFooter: () => {
1418
- customFooterItems.length = 0;
1419
- },
1420
- setFooter: (items) => {
1421
- customFooterItems.length = 0;
1422
- const itemsArray = Array.isArray(items) ? items : [items];
1423
- customFooterItems.push(...itemsArray);
1424
- },
1425
- update: () => {
1426
- setUpdateCounter((c) => c + 1);
1427
- },
1428
- goBack: () => {
1429
- if (actions.back) {
1430
- actions.back();
1431
- }
1432
- },
1433
- close: (result) => {
1434
- cleanup(result);
1435
- },
1436
- parentData
1437
- };
1438
- if (!renderResult) {
1439
- renderResult = onRender(context);
1424
+ return;
1425
+ }
1426
+ let dotEnvPathFile = null;
1427
+ const envSpecificFile = `.env.${this.env}`;
1428
+ const envSpecificPath = (0, import_path.resolve)(dotEnvPath, envSpecificFile);
1429
+ if ((0, import_fs.existsSync)(envSpecificPath)) {
1430
+ dotEnvPathFile = envSpecificPath;
1431
+ }
1432
+ if (!dotEnvPathFile && !this.get("dotEnvPath")) {
1433
+ const examplesPath = (0, import_path.resolve)(dotEnvPath, "examples");
1434
+ const examplesEnvSpecificPath = (0, import_path.resolve)(examplesPath, envSpecificFile);
1435
+ if ((0, import_fs.existsSync)(examplesEnvSpecificPath)) {
1436
+ dotEnvPathFile = examplesEnvSpecificPath;
1440
1437
  }
1441
- (0, import_ink3.useInput)((input, key) => {
1442
- if (key.ctrl && input === "c") {
1443
- cleanup(null);
1444
- process.exit(0);
1445
- return;
1446
- }
1447
- let matchedBinding = null;
1448
- for (const binding of keyBindings) {
1449
- let keyMatches = false;
1450
- if (key[binding.key]) {
1451
- keyMatches = true;
1452
- } else if (input === binding.key) {
1453
- keyMatches = true;
1454
- }
1455
- if (keyMatches) {
1456
- if (binding.enabled === false) {
1457
- continue;
1458
- }
1459
- if (binding.condition && !binding.condition(context)) {
1460
- continue;
1461
- }
1462
- matchedBinding = binding;
1463
- break;
1438
+ }
1439
+ if (!dotEnvPathFile) {
1440
+ dotEnvPathFile = (0, import_path.resolve)(dotEnvPath, dotEnvFile);
1441
+ if (!(0, import_fs.existsSync)(dotEnvPathFile)) {
1442
+ if (!this.get("dotEnvPath")) {
1443
+ const examplesPath = (0, import_path.resolve)(dotEnvPath, "examples");
1444
+ const examplesEnvFile = (0, import_path.resolve)(examplesPath, dotEnvFile);
1445
+ if ((0, import_fs.existsSync)(examplesEnvFile)) {
1446
+ dotEnvPathFile = examplesEnvFile;
1447
+ } else {
1448
+ dotEnvPathFile = (0, import_path.resolve)(dotEnvPath, "..", dotEnvFile);
1464
1449
  }
1465
1450
  }
1466
- if (matchedBinding && actions[matchedBinding.action]) {
1467
- const actionResult = actions[matchedBinding.action]({
1468
- input,
1469
- key,
1470
- binding: matchedBinding
1471
- });
1451
+ }
1452
+ }
1453
+ if (dotEnvPathFile && (0, import_fs.existsSync)(dotEnvPathFile)) {
1454
+ (0, import_dotenv.config)({ path: dotEnvPathFile, quiet: true });
1455
+ }
1456
+ }
1457
+ /**
1458
+ * Load configuration files
1459
+ */
1460
+ loadConfigFiles() {
1461
+ this.configsLoaded = [];
1462
+ this.configValues = {};
1463
+ const _defaultConfigExtension = this.get("defaultConfigExtension") || "js";
1464
+ const optConfigFiles = this.get("config") || this.get("configs") || "";
1465
+ const configFiles = optConfigFiles ? optConfigFiles.split(/,\s*/) : [];
1466
+ const optConfigFilePath = this.get("configPath");
1467
+ if (configFiles.length > 0) {
1468
+ for (const cfgFile of configFiles) {
1469
+ let notLoaded = false;
1470
+ let notLoadedEnvSpecific = false;
1471
+ const cfgFileWithPath = this.resolveFileWithPath(optConfigFilePath, cfgFile);
1472
+ try {
1473
+ const cfgContents = this.requireConfigFile(cfgFileWithPath);
1474
+ this.configValues = { ...this.configValues, ...cfgContents };
1475
+ this.configsLoaded.push(cfgFileWithPath);
1476
+ } catch {
1477
+ notLoaded = true;
1472
1478
  }
1473
- });
1474
- const footerLines = [];
1475
- const bindingItems = formatKeyBindings(keyBindings, "long");
1476
- if (bindingItems.length > 0) {
1477
- const bindingsLine = [];
1478
- bindingItems.forEach((item, idx) => {
1479
- if (idx > 0) {
1480
- bindingsLine.push(", ");
1479
+ const cfgEnvFileWithPath = this.resolveFileWithPath(
1480
+ optConfigFilePath,
1481
+ cfgFile,
1482
+ this.env
1483
+ );
1484
+ if (cfgEnvFileWithPath !== cfgFileWithPath) {
1485
+ try {
1486
+ const cfgContents = this.requireConfigFile(cfgEnvFileWithPath);
1487
+ this.configValues = { ...this.configValues, ...cfgContents };
1488
+ this.configsLoaded.push(cfgEnvFileWithPath);
1489
+ } catch {
1490
+ notLoadedEnvSpecific = true;
1481
1491
  }
1482
- bindingsLine.push(item);
1483
- });
1484
- const allStrings = bindingItems.every((item) => typeof item === "string");
1485
- if (allStrings) {
1486
- footerLines.push(bindingsLine.join(""));
1487
1492
  } else {
1488
- const wrappedBindingsLine = bindingsLine.map(
1489
- (item) => typeof item === "string" ? (0, import_react3.createElement)(import_ink3.Text, {}, item) : item
1490
- );
1491
- footerLines.push(wrappedBindingsLine);
1493
+ notLoadedEnvSpecific = true;
1492
1494
  }
1493
- }
1494
- customFooterItems.forEach((item) => {
1495
- if (typeof item === "string") {
1496
- footerLines.push(item);
1497
- } else {
1498
- footerLines.push(item);
1495
+ if (notLoaded && notLoadedEnvSpecific) {
1496
+ throw new Error(`can't load config file "${cfgFileWithPath}"`);
1499
1497
  }
1500
- });
1501
- return (0, import_react3.createElement)(
1502
- ScreenContainer,
1503
- {},
1504
- (0, import_react3.createElement)(ScreenTitle, { text: title }),
1505
- (0, import_react3.createElement)(ScreenDivider),
1506
- (0, import_react3.createElement)(ScreenRow, {}, (0, import_react3.createElement)(import_ink3.Text, {}, " ")),
1507
- renderResult,
1508
- (0, import_react3.createElement)(ScreenRow, {}, (0, import_react3.createElement)(import_ink3.Text, {}, " ")),
1509
- (0, import_react3.createElement)(ScreenDivider),
1510
- (0, import_react3.createElement)(ScreenFooter, { lines: footerLines })
1511
- );
1512
- };
1513
- const cleanup = (result) => {
1514
- if (instance2) instance2.unmount();
1515
- setTimeout(() => resolve2(result), 50);
1498
+ }
1499
+ }
1500
+ }
1501
+ /**
1502
+ * Resolve file path with environment-specific naming
1503
+ */
1504
+ resolveFileWithPath(optConfigFilePath, cfgFile, env) {
1505
+ let cfgFileWithPath = optConfigFilePath ? (0, import_path.isAbsolute)(optConfigFilePath) ? (0, import_path.resolve)(optConfigFilePath, cfgFile) : (0, import_path.resolve)(process.cwd(), optConfigFilePath, cfgFile) : (0, import_path.isAbsolute)(cfgFile) ? cfgFile : (0, import_path.resolve)(process.cwd(), cfgFile);
1506
+ const { basePathWithName, extension } = this.splitPath(cfgFileWithPath);
1507
+ if (env) {
1508
+ cfgFileWithPath = `${basePathWithName}.${env}.${extension || "js"}`;
1509
+ } else {
1510
+ cfgFileWithPath = `${basePathWithName}.${extension || "js"}`;
1511
+ }
1512
+ return cfgFileWithPath;
1513
+ }
1514
+ /**
1515
+ * Split file path into base path and extension
1516
+ */
1517
+ splitPath(filePath) {
1518
+ const basePathWithName = (0, import_path.join)((0, import_path.dirname)(filePath), (0, import_path.basename)(filePath, (0, import_path.extname)(filePath)));
1519
+ const extension = (0, import_path.extname)(filePath).slice(1);
1520
+ return { basePathWithName, extension };
1521
+ }
1522
+ /**
1523
+ * Require a configuration file (supports .js and .json)
1524
+ */
1525
+ requireConfigFile(filePath) {
1526
+ if (!(0, import_fs.existsSync)(filePath)) {
1527
+ throw new Error(`Config file not found: ${filePath}`);
1528
+ }
1529
+ const ext = (0, import_path.extname)(filePath).toLowerCase();
1530
+ if (ext === ".json") {
1531
+ const content = (0, import_fs.readFileSync)(filePath, "utf8");
1532
+ return JSON.parse(content);
1533
+ } else if (ext === ".js") {
1534
+ try {
1535
+ delete require.cache[require.resolve(filePath)];
1536
+ return require(filePath);
1537
+ } catch (error) {
1538
+ throw new Error(`Failed to load JS config file: ${error instanceof Error ? error.message : String(error)}`);
1539
+ }
1540
+ } else {
1541
+ throw new Error(`Unsupported file extension: ${ext}`);
1542
+ }
1543
+ }
1544
+ /**
1545
+ * Get all parsed data
1546
+ */
1547
+ getParsed() {
1548
+ return {
1549
+ command: this.commands[0] || "",
1550
+ flags: { ...this.flags },
1551
+ options: { ...this.options },
1552
+ usedKeys: Array.from(this.usedKeys)
1516
1553
  };
1517
- instance2 = (0, import_ink3.render)((0, import_react3.createElement)(Screen));
1518
- });
1554
+ }
1555
+ /**
1556
+ * Set prefixes dynamically and re-parse arguments (like legacy)
1557
+ */
1558
+ setPrefixes(prefixes) {
1559
+ const arr = Array.isArray(prefixes) ? prefixes : prefixes.split(/,\s*/);
1560
+ const sortedArr = arr.sort(
1561
+ (a, b) => a.length < b.length ? 1 : a.length > b.length ? -1 : 0
1562
+ );
1563
+ this.prefixes = sortedArr.map((el) => el.toLowerCase());
1564
+ const args = process.argv.slice(2);
1565
+ this.parseArgs(args);
1566
+ }
1567
+ };
1568
+ var instance = null;
1569
+ function getArgsInstance() {
1570
+ return instance;
1519
1571
  }
1520
- async function showListScreen(config2) {
1521
- const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker } = config2;
1522
- return showScreen({
1523
- title,
1524
- parentData,
1525
- onRender: (ctx) => {
1526
- const selectedIndexRef = { current: initialSelectedIndex };
1527
- ctx.setAction("select", () => {
1528
- const selected = items[selectedIndexRef.current];
1529
- if (onSelect) {
1530
- const result = onSelect(selected.value, selectedIndexRef.current);
1531
- ctx.close(result);
1532
- }
1533
- });
1534
- if (onEscape) {
1535
- ctx.setAction("back", () => {
1536
- const result = onEscape(selectedIndexRef.current);
1537
- ctx.close(result);
1538
- });
1539
- }
1540
- ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
1541
- return (0, import_react3.createElement)(ListComponent, { items, ctx, selectedIndexRef, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker });
1572
+
1573
+ // src/params/index.ts
1574
+ var import_joi = __toESM(require("joi"), 1);
1575
+
1576
+ // src/errors.ts
1577
+ var FrameworkError = class extends Error {
1578
+ constructor(message) {
1579
+ super(message);
1580
+ this.name = "FrameworkError";
1581
+ }
1582
+ };
1583
+ var ParamError = class extends FrameworkError {
1584
+ constructor(message) {
1585
+ super(message);
1586
+ this.name = "ParamError";
1587
+ }
1588
+ };
1589
+
1590
+ // src/params/custom-types.ts
1591
+ var joiEdateType = (value, helpers) => {
1592
+ if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) {
1593
+ const testDate = new Date(value);
1594
+ if (!isNaN(testDate.getTime())) {
1595
+ return value;
1596
+ }
1597
+ }
1598
+ if (value instanceof Date) {
1599
+ return value.toISOString();
1600
+ }
1601
+ if (typeof value !== "string") {
1602
+ value = String(value);
1603
+ }
1604
+ if (value.toLowerCase() === "now") {
1605
+ return (/* @__PURE__ */ new Date()).toISOString();
1606
+ }
1607
+ const referenceRegex = /^@(\w+)([+-]\d+[smhdwy])$/i;
1608
+ const referenceMatch = value.match(referenceRegex);
1609
+ if (referenceMatch) {
1610
+ const [, paramName, relativeExpr] = referenceMatch;
1611
+ const context = helpers.prefs?.context;
1612
+ if (!context || !context.params) {
1613
+ throw new ParamError(`Cannot resolve cross-parameter reference @${paramName}: context not available. Ensure parameters are processed with proper context.`);
1542
1614
  }
1543
- });
1544
- }
1545
- async function showMultiColumnListScreen(config2) {
1546
- const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config2;
1547
- return showScreen({
1548
- title,
1549
- parentData,
1550
- onRender: (ctx) => {
1551
- const selectedIndexRef = { current: initialSelectedIndex };
1552
- ctx.setAction("select", () => {
1553
- const selected = items[selectedIndexRef.current];
1554
- if (onSelect) {
1555
- const result = onSelect(selected, selectedIndexRef.current);
1556
- ctx.close(result);
1557
- }
1558
- });
1559
- if (onEscape) {
1560
- ctx.setAction("back", () => {
1561
- const result = onEscape(selectedIndexRef.current);
1562
- ctx.close(result);
1563
- });
1615
+ const referencedValue = context.params[paramName];
1616
+ if (referencedValue === void 0 || referencedValue === null) {
1617
+ throw new ParamError(`Cannot resolve @${paramName}: parameter "${paramName}" is not defined or has no value. Parameters are evaluated left-to-right.`);
1618
+ }
1619
+ let referenceDate;
1620
+ if (referencedValue instanceof Date) {
1621
+ referenceDate = referencedValue;
1622
+ } else if (typeof referencedValue === "string") {
1623
+ referenceDate = new Date(referencedValue);
1624
+ if (isNaN(referenceDate.getTime())) {
1625
+ throw new ParamError(`Referenced parameter @${paramName} has invalid date value: ${referencedValue}`);
1564
1626
  }
1565
- ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
1566
- return (0, import_react3.createElement)(MultiColumnListComponent, { items, ctx, selectedIndexRef });
1627
+ } else {
1628
+ throw new ParamError(`Referenced parameter @${paramName} is not a valid date type (found: ${typeof referencedValue})`);
1567
1629
  }
1568
- });
1630
+ const relativeMatch2 = relativeExpr.match(/^([+-])(\d+)([smhdwy])$/i);
1631
+ if (!relativeMatch2) {
1632
+ throw new ParamError(`Invalid relative time expression in @${paramName}${relativeExpr}`);
1633
+ }
1634
+ const [, sign, amount, unit] = relativeMatch2;
1635
+ const offset = calculateTimeOffset(parseInt(amount, 10), unit, sign);
1636
+ const resultDate = new Date(referenceDate.getTime() + offset);
1637
+ return resultDate.toISOString();
1638
+ }
1639
+ const relativeTimeRegex = /^([+-])(\d+)([smhdwy])$/i;
1640
+ const relativeMatch = value.match(relativeTimeRegex);
1641
+ if (relativeMatch) {
1642
+ const [, sign, amount, unit] = relativeMatch;
1643
+ const numAmount = parseInt(amount, 10);
1644
+ if (isNaN(numAmount)) {
1645
+ throw new ParamError(`Invalid relative time amount: ${amount}`);
1646
+ }
1647
+ const offset = calculateTimeOffset(numAmount, unit, sign);
1648
+ const resultDate = new Date(Date.now() + offset);
1649
+ return resultDate.toISOString();
1650
+ }
1651
+ const parsedDate = new Date(value);
1652
+ if (isNaN(parsedDate.getTime())) {
1653
+ throw new ParamError(`Invalid date format: ${value}. Expected a valid date string, "now", relative time expression (e.g., "-2h", "+1d"), or cross-parameter reference (e.g., "@startTime+2h")`);
1654
+ }
1655
+ return parsedDate.toISOString();
1656
+ };
1657
+ function calculateTimeOffset(amount, unit, sign) {
1658
+ let multiplier = 1;
1659
+ switch (unit.toLowerCase()) {
1660
+ case "s":
1661
+ multiplier = 1e3;
1662
+ break;
1663
+ case "m":
1664
+ multiplier = 60 * 1e3;
1665
+ break;
1666
+ case "h":
1667
+ multiplier = 60 * 60 * 1e3;
1668
+ break;
1669
+ case "d":
1670
+ multiplier = 24 * 60 * 60 * 1e3;
1671
+ break;
1672
+ case "w":
1673
+ multiplier = 7 * 24 * 60 * 60 * 1e3;
1674
+ break;
1675
+ case "y":
1676
+ multiplier = 365 * 24 * 60 * 60 * 1e3;
1677
+ break;
1678
+ default:
1679
+ throw new ParamError(`Invalid time unit: ${unit}. Supported units: s, m, h, d, w, y`);
1680
+ }
1681
+ return sign === "+" ? amount * multiplier : -amount * multiplier;
1569
1682
  }
1570
- async function showMultiColumnListWithPreviewScreen(config2) {
1571
- const { title, items, getPreviewContent, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config2;
1572
- return showScreen({
1573
- title,
1574
- parentData,
1575
- onRender: (ctx) => {
1576
- const selectedIndexRef = { current: initialSelectedIndex };
1577
- ctx.setAction("select", () => {
1578
- const selected = items[selectedIndexRef.current];
1579
- if (onSelect) {
1580
- const result = onSelect(selected, selectedIndexRef.current);
1581
- ctx.close(result);
1582
- }
1583
- });
1584
- if (onEscape) {
1585
- ctx.setAction("back", () => {
1586
- const result = onEscape(selectedIndexRef.current);
1587
- ctx.close(result);
1588
- });
1683
+ var joiStringArrayType = (type) => (value, helpers) => {
1684
+ if (value === void 0 || typeof value === "function") {
1685
+ return [];
1686
+ }
1687
+ const arr = value.split(/,\s*/).map((el) => {
1688
+ if (type === "number") {
1689
+ const v = parseInt(el, 10);
1690
+ if (isNaN(v)) {
1691
+ throw new ParamError(`array element "${el}" should be numeric`);
1589
1692
  }
1590
- ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
1591
- return (0, import_react3.createElement)(MultiColumnListWithPreviewComponent, { items, getPreviewContent, ctx, selectedIndexRef });
1693
+ return v;
1694
+ } else if (type === "boolean") {
1695
+ const v = el.match(/true|t|yes|1/i) ? true : el.match(/false|f|no|0/i) ? false : null;
1696
+ if (v === null) {
1697
+ throw new ParamError(`array element "${el}" should be boolean`);
1698
+ }
1699
+ return v;
1700
+ } else if (type === "string") {
1701
+ return el;
1702
+ } else {
1703
+ throw new ParamError(`unknown type "${type}" for array elements`);
1592
1704
  }
1593
1705
  });
1594
- }
1595
- var showMenuScreen = showListScreen;
1596
- var showWordGridScreen = showMultiColumnListScreen;
1597
-
1598
- // src/screen/ui-elements.ts
1599
- var import_react4 = require("react");
1600
- var import_ink4 = require("ink");
1601
- function ListItem({
1602
- children,
1603
- isSelected = false,
1604
- color = "white",
1605
- backgroundColor,
1606
- bold = false,
1607
- dimColor = false
1608
- }) {
1609
- return (0, import_react4.createElement)(
1610
- import_ink4.Box,
1611
- {},
1612
- (0, import_react4.createElement)(import_ink4.Text, {
1613
- color: isSelected ? backgroundColor || "green" : color,
1614
- backgroundColor: isSelected ? color : backgroundColor,
1615
- bold: isSelected || bold,
1616
- dimColor: !isSelected && dimColor
1617
- }, children)
1618
- );
1619
- }
1620
- function TextBlock({
1621
- text,
1622
- color = "white",
1623
- dimmed = false,
1624
- bold = false,
1625
- maxWidth
1626
- }) {
1627
- return (0, import_react4.createElement)(
1628
- import_ink4.Box,
1629
- {},
1630
- (0, import_react4.createElement)(import_ink4.Text, {
1631
- color,
1632
- dimColor: dimmed,
1633
- bold
1634
- }, text)
1635
- );
1636
- }
1637
- function Divider({ character = "\u2500", width = 80 }) {
1638
- return (0, import_react4.createElement)(
1639
- import_ink4.Box,
1640
- { marginY: 1 },
1641
- (0, import_react4.createElement)(import_ink4.Text, { dimColor: true }, character.repeat(width))
1642
- );
1643
- }
1644
- function GridCell({
1645
- children,
1646
- width,
1647
- color = "white",
1648
- backgroundColor,
1649
- bold = false,
1650
- dimColor = false,
1651
- align = "left"
1652
- }) {
1653
- return (0, import_react4.createElement)(
1654
- import_ink4.Box,
1655
- { width },
1656
- (0, import_react4.createElement)(import_ink4.Text, {
1657
- color,
1658
- backgroundColor,
1659
- bold,
1660
- dimColor,
1661
- textAlign: align
1662
- }, children)
1663
- );
1664
- }
1665
- function InputField({ prompt, value, onChange, onSubmit }) {
1666
- return (0, import_react4.createElement)(
1667
- import_ink4.Box,
1668
- { flexDirection: "column" },
1669
- (0, import_react4.createElement)(import_ink4.Text, {}, prompt),
1670
- (0, import_react4.createElement)(
1671
- import_ink4.Box,
1672
- { marginTop: 1 },
1673
- (0, import_react4.createElement)(import_ink4.Text, { color: "cyan" }, " > ", value, "_")
1674
- )
1675
- );
1676
- }
1677
-
1678
- // src/screen/utils.ts
1679
- function buildBreadcrumb(parts) {
1680
- if (parts.length === 0) return "";
1681
- if (parts.length === 1) return parts[0];
1682
- return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
1683
- }
1684
- function buildDetailBreadcrumb(path4, suffix = "") {
1685
- if (path4.length <= 1) {
1686
- return suffix ? `\u2190 ${suffix}` : path4[0] || "";
1687
- }
1688
- const breadcrumb = buildBreadcrumb(path4);
1689
- return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
1690
- }
1706
+ return arr;
1707
+ };
1691
1708
 
1692
- // src/screen/footer-builder.ts
1693
- function buildFooter(config2 = {}) {
1694
- const {
1695
- navigation = null,
1696
- actions = null,
1697
- info = null,
1698
- escape = "Esc to go back",
1699
- custom = null
1700
- } = config2;
1701
- const lines = [];
1702
- const mainParts = [];
1703
- if (navigation) {
1704
- mainParts.push(navigation);
1709
+ // src/params/index.ts
1710
+ var Params = class _Params {
1711
+ context;
1712
+ // Partial context during initialization
1713
+ params = {};
1714
+ definitions = {};
1715
+ args;
1716
+ paramSetters = [];
1717
+ paramGetters = [];
1718
+ trackedParams = [];
1719
+ constructor(context, options = {}) {
1720
+ this.context = context;
1721
+ this.args = context.args;
1722
+ if (Object.keys(options).length > 0) {
1723
+ this.configure(options);
1724
+ }
1705
1725
  }
1706
- if (actions) {
1707
- mainParts.push(actions);
1726
+ /**
1727
+ * Configure parameters
1728
+ * Only parameters present in options are updated
1729
+ */
1730
+ configure(options) {
1731
+ for (const [k, v] of Object.entries(options)) {
1732
+ this.params[k] = v;
1733
+ }
1708
1734
  }
1709
- if (escape) {
1710
- mainParts.push(escape);
1735
+ /**
1736
+ * Initialize Params from context and CLI parameters
1737
+ * Note: Params is special - it's initialized early with partial context
1738
+ */
1739
+ static init(context, options) {
1740
+ return new _Params(context, options || {});
1711
1741
  }
1712
- if (mainParts.length > 0) {
1713
- lines.push(mainParts.join(", "));
1742
+ /**
1743
+ * Track a parameter request for --stopAfter=init feature
1744
+ */
1745
+ trackParam(key, definition, value, source) {
1746
+ this.trackedParams.push({
1747
+ key,
1748
+ definition,
1749
+ value,
1750
+ source
1751
+ });
1714
1752
  }
1715
- if (info) {
1716
- const infoLines = Array.isArray(info) ? info : [info];
1717
- lines.push(...infoLines);
1753
+ /**
1754
+ * Get all tracked parameters (for --stopAfter=init)
1755
+ */
1756
+ getTrackedParams() {
1757
+ return [...this.trackedParams];
1718
1758
  }
1719
- if (custom) {
1720
- const customLines = Array.isArray(custom) ? custom : [custom];
1721
- lines.push(...customLines);
1759
+ /**
1760
+ * Get all figured parameters as a record
1761
+ * Returns all parameters that were collected during initialization,
1762
+ * whether from CLI args, options, or defaults
1763
+ */
1764
+ getAllFigured() {
1765
+ const result = {};
1766
+ for (const param of this.trackedParams) {
1767
+ result[param.key] = {
1768
+ value: param.value,
1769
+ source: param.source
1770
+ };
1771
+ }
1772
+ return result;
1773
+ }
1774
+ /**
1775
+ * Clear tracked parameters
1776
+ */
1777
+ clearTrackedParams() {
1778
+ this.trackedParams = [];
1779
+ }
1780
+ /**
1781
+ * Assign a parameter definition
1782
+ */
1783
+ assignDefinition(key, definition) {
1784
+ if (this.definitions[key] && !definition) {
1785
+ return this.definitions[key];
1786
+ }
1787
+ let type;
1788
+ if (!definition) {
1789
+ type = import_joi.default.string();
1790
+ } else if (import_joi.default.isSchema(definition)) {
1791
+ type = definition;
1792
+ } else if (import_joi.default.isSchema(definition.type)) {
1793
+ type = definition.type;
1794
+ } else if (typeof definition === "string") {
1795
+ type = this.toJoi(definition);
1796
+ } else if (typeof definition.type === "string") {
1797
+ type = this.toJoi(definition.type);
1798
+ } else if (!definition.type) {
1799
+ type = import_joi.default.string();
1800
+ } else {
1801
+ type = import_joi.default.string();
1802
+ }
1803
+ if (!this.definitions[key]) {
1804
+ this.definitions[key] = {};
1805
+ }
1806
+ this.definitions[key].type = type;
1807
+ if (definition && definition.values) {
1808
+ if (Array.isArray(definition.values)) {
1809
+ this.definitions[key].values = definition.values;
1810
+ }
1811
+ }
1812
+ return this.definitions[key];
1813
+ }
1814
+ /**
1815
+ * Convert string definition to Joi schema
1816
+ */
1817
+ toJoi(str) {
1818
+ let type;
1819
+ if (str.match(/^string|^text/i)) {
1820
+ type = import_joi.default.string();
1821
+ } else if (str.match(/^number|^integer|^int/i)) {
1822
+ type = import_joi.default.number();
1823
+ } else if (str.match(/^boolean|^bool/i)) {
1824
+ type = import_joi.default.boolean();
1825
+ } else if (str.match(/^date/i)) {
1826
+ type = import_joi.default.custom(joiEdateType);
1827
+ } else if (str.match(/^duration/i)) {
1828
+ type = import_joi.default.string().isoDuration();
1829
+ } else if (str.match(/^array/i)) {
1830
+ let elementTypes = "string";
1831
+ const tmp = str.match(/\((.*)\)/);
1832
+ if (tmp && tmp[1].match(/string/i)) {
1833
+ elementTypes = "string";
1834
+ } else if (tmp && tmp[1].match(/number|integer|int/i)) {
1835
+ elementTypes = "number";
1836
+ } else if (tmp && tmp[1].match(/boolean|bool/i)) {
1837
+ elementTypes = "boolean";
1838
+ }
1839
+ type = import_joi.default.custom(joiStringArrayType(elementTypes));
1840
+ } else {
1841
+ type = import_joi.default.string();
1842
+ }
1843
+ const regexForDefault = /\bdefault\s+([^\s]+)/;
1844
+ const matchForDefault = str.match(regexForDefault);
1845
+ if (matchForDefault) {
1846
+ const defValObj = type.validate(matchForDefault[1]);
1847
+ if (defValObj.error) {
1848
+ throw new ParamError(`default value "${defValObj.value}" type mismatch`);
1849
+ }
1850
+ type = type.default(defValObj.value);
1851
+ } else if (str.match(/required/)) {
1852
+ type = type.required();
1853
+ } else {
1854
+ type = type.optional();
1855
+ }
1856
+ return type;
1722
1857
  }
1723
- return lines;
1724
- }
1725
- var FooterPresets = {
1726
1858
  /**
1727
- * Menu screen footer
1859
+ * Validate a value against a definition
1728
1860
  */
1729
- menu: (customInfo = null) => buildFooter({
1730
- navigation: "\u2191/\u2193 to navigate",
1731
- actions: "Enter to select",
1732
- escape: "Esc to go back",
1733
- info: customInfo
1734
- }),
1861
+ validate(key, val, def) {
1862
+ const normalizedVal = val === null ? void 0 : val;
1863
+ const { value, error } = def.type.validate(normalizedVal, {
1864
+ context: { params: this.params },
1865
+ abortEarly: false,
1866
+ allowUnknown: false
1867
+ });
1868
+ if (error) {
1869
+ const errs = error.details.map((el) => el.message).join(", ");
1870
+ throw new ParamError(`"${key}" validation error: ${errs}`);
1871
+ }
1872
+ return value;
1873
+ }
1735
1874
  /**
1736
- * Word grid footer
1875
+ * Get a parameter value with validation
1737
1876
  */
1738
- wordGrid: (totalWords) => buildFooter({
1739
- navigation: "\u2191\u2193\u2190\u2192 to navigate",
1740
- actions: "Enter to select",
1741
- escape: "Esc to go back",
1742
- info: `Total: ${totalWords} words`
1743
- }),
1877
+ get(key, definition) {
1878
+ const def = this.assignDefinition(key, definition);
1879
+ let valFromGetters = void 0;
1880
+ if (def.volatile || true) {
1881
+ valFromGetters = this.runAllRegisteredGetters(key);
1882
+ }
1883
+ const valFromArgs = this.args.get(key);
1884
+ const valFromParams = this.params[key];
1885
+ let source = "default";
1886
+ let value;
1887
+ if (valFromGetters !== void 0 && valFromGetters !== null) {
1888
+ value = this.validate(key, valFromGetters, def);
1889
+ source = "options";
1890
+ } else if (valFromArgs !== void 0 && valFromArgs !== null) {
1891
+ value = this.validate(key, valFromArgs, def);
1892
+ source = "cli";
1893
+ } else if (valFromParams !== void 0 && valFromParams !== null) {
1894
+ value = this.validate(key, valFromParams, def);
1895
+ source = "options";
1896
+ } else {
1897
+ value = this.validate(key, void 0, def);
1898
+ source = "default";
1899
+ }
1900
+ this.trackParam(key, definition || "string", value, source);
1901
+ if (value !== void 0 && def.values && !def.values.includes(value)) {
1902
+ throw new ParamError(`key ${key} should be one of ${def.values}`);
1903
+ }
1904
+ return value;
1905
+ }
1744
1906
  /**
1745
- * Text input footer
1907
+ * Set a parameter value with validation
1746
1908
  */
1747
- textInput: () => buildFooter({
1748
- actions: "Type and press Enter to submit",
1749
- escape: "Esc to cancel"
1750
- }),
1909
+ set(key, val, definition) {
1910
+ if (val && val.type && val.value) {
1911
+ definition = val;
1912
+ val = val.value;
1913
+ }
1914
+ const def = this.assignDefinition(key, definition);
1915
+ if (!this.runAllRegisteredSetters(key, val)) {
1916
+ this.params[key] = val;
1917
+ }
1918
+ }
1751
1919
  /**
1752
- * Info/static screen footer
1920
+ * Get all parameters from definitions
1921
+ * Processes parameters left-to-right to support cross-parameter references
1753
1922
  */
1754
- info: () => buildFooter({
1755
- escape: "Esc to continue"
1756
- }),
1923
+ getAll(defs) {
1924
+ const res = {};
1925
+ for (const [k, def] of Object.entries(defs)) {
1926
+ const value = this.get(k, def);
1927
+ res[k] = value;
1928
+ if (value !== void 0) {
1929
+ this.params[k] = value;
1930
+ }
1931
+ }
1932
+ return res;
1933
+ }
1757
1934
  /**
1758
- * Main menu footer (escape exits)
1935
+ * Run all registered getters for a key
1759
1936
  */
1760
- mainMenu: () => buildFooter({
1761
- navigation: "\u2191/\u2193 to navigate",
1762
- actions: "Enter to select",
1763
- escape: "Esc to exit"
1764
- }),
1937
+ runAllRegisteredGetters(key) {
1938
+ let val = void 0;
1939
+ for (const getter of this.paramGetters) {
1940
+ val = getter(key, this.definitions[key]);
1941
+ if (val !== void 0 && val !== null) {
1942
+ break;
1943
+ }
1944
+ }
1945
+ return val;
1946
+ }
1765
1947
  /**
1766
- * Action menu footer (for word cards, etc.)
1948
+ * Run all registered setters for a key
1767
1949
  */
1768
- actionMenu: (hasAudio = false) => {
1769
- const parts = buildFooter({
1770
- navigation: "\u2191/\u2193 to navigate",
1771
- actions: "Enter to select",
1772
- escape: "Esc to go back"
1773
- });
1774
- if (hasAudio) {
1775
- parts.push("Audio available");
1950
+ runAllRegisteredSetters(key, value) {
1951
+ let setterUsed = false;
1952
+ for (const setter of this.paramSetters) {
1953
+ setterUsed = setter(key, value);
1954
+ if (setterUsed) {
1955
+ break;
1956
+ }
1776
1957
  }
1777
- return parts;
1958
+ return setterUsed;
1778
1959
  }
1779
- };
1780
- function organizeFooterMessages(messages) {
1781
- if (!messages || messages.length === 0) {
1782
- return ["Esc to go back"];
1960
+ /**
1961
+ * Register a parameter getter
1962
+ */
1963
+ registerParamGetter(fn) {
1964
+ this.paramGetters.push(fn);
1783
1965
  }
1784
- const navigation = messages.filter((m) => m.includes("\u2191") || m.includes("\u2193") || m.includes("\u2190") || m.includes("\u2192"));
1785
- const actions = messages.filter((m) => m.includes("Enter") || m.includes("select") || m.includes("submit"));
1786
- const escape = messages.filter((m) => m.includes("Esc"));
1787
- const others = messages.filter(
1788
- (m) => !navigation.includes(m) && !actions.includes(m) && !escape.includes(m)
1789
- );
1790
- const lines = [];
1791
- const mainLine = [...navigation, ...actions, ...escape].join(", ");
1792
- if (mainLine) lines.push(mainLine);
1793
- lines.push(...others);
1794
- return lines;
1795
- }
1966
+ /**
1967
+ * Register a parameter setter
1968
+ */
1969
+ registerParamSetter(fn) {
1970
+ this.paramSetters.push(fn);
1971
+ }
1972
+ };
1796
1973
 
1797
- // src/screen/index.ts
1798
- var loadPromise = null;
1799
- async function load() {
1800
- if (loadPromise) return loadPromise;
1801
- loadPromise = Promise.all([
1802
- import("react"),
1803
- import("ink")
1804
- ]).then(() => {
1805
- });
1806
- return loadPromise;
1807
- }
1808
- if (typeof window === "undefined") {
1809
- load().catch(() => {
1810
- });
1811
- }
1974
+ // src/screen.ts
1975
+ init_screen();
1812
1976
 
1813
1977
  // src/filedatabase/index.ts
1814
1978
  var import_fs4 = __toESM(require("fs"), 1);
@@ -3031,6 +3195,7 @@ var ALL_LEVELS = [
3031
3195
  "response",
3032
3196
  "progress"
3033
3197
  ];
3198
+ var MAX_LEVEL_LENGTH = Math.max(...ALL_LEVELS.map((level) => level.toUpperCase().length));
3034
3199
  var LEVEL_COLORS = {
3035
3200
  error: import_chalk.default.red.bold,
3036
3201
  warn: import_chalk.default.rgb(255, 165, 0),
@@ -3044,13 +3209,104 @@ var LEVEL_COLORS = {
3044
3209
  progress: import_chalk.default.green,
3045
3210
  results: import_chalk.default.magenta
3046
3211
  };
3047
- var CliToolkitLogger = class {
3212
+ var Logger = class _Logger {
3213
+ context;
3214
+ // Partial context during initialization
3048
3215
  options;
3049
3216
  transport;
3050
3217
  startTimes = {};
3051
3218
  lastProgressTimes = {};
3052
- constructor(options = {}) {
3053
- this.options = this.normalizeOptions(options);
3219
+ constructor(context, options = {}) {
3220
+ this.context = context;
3221
+ this.options = this.getDefaultOptions();
3222
+ if (options) {
3223
+ this.configure(options);
3224
+ }
3225
+ this.updateTransport();
3226
+ }
3227
+ /**
3228
+ * Configure logger options
3229
+ * Only parameters present in options are updated
3230
+ */
3231
+ configure(options) {
3232
+ if (options.mode !== void 0) {
3233
+ this.options.mode = this.isValidMode(options.mode) ? options.mode : "text";
3234
+ }
3235
+ if (options.route !== void 0) {
3236
+ this.options.route = options.route;
3237
+ this.updateTransport();
3238
+ }
3239
+ if (options.prefix !== void 0) {
3240
+ this.options.prefix = options.prefix;
3241
+ }
3242
+ if (options.silent !== void 0) {
3243
+ this.options.silent = options.silent;
3244
+ }
3245
+ if (options.showLevel !== void 0) {
3246
+ this.options.showLevel = options.showLevel;
3247
+ }
3248
+ if (options.timestamp !== void 0) {
3249
+ this.options.timestamp = options.timestamp;
3250
+ }
3251
+ if (options.levels !== void 0) {
3252
+ this.options.levels = this.normalizeLevels(options.levels);
3253
+ }
3254
+ if (options.progress !== void 0) {
3255
+ if (options.progress.withTimes !== void 0) {
3256
+ this.options.progressTimes = options.progress.withTimes;
3257
+ }
3258
+ if (options.progress.throttleMs !== void 0) {
3259
+ this.options.progressThrottle = options.progress.throttleMs;
3260
+ }
3261
+ }
3262
+ }
3263
+ /**
3264
+ * Initialize logger from context and CLI parameters
3265
+ */
3266
+ static init(context, options) {
3267
+ const paramDefs = {
3268
+ mode: "string default text",
3269
+ route: "string default console",
3270
+ prefix: "string",
3271
+ silent: "boolean default false",
3272
+ showLevel: "boolean default true",
3273
+ timestamp: "boolean default false",
3274
+ levels: "string",
3275
+ progressWithTimes: "boolean default false",
3276
+ progressThrottleMs: "number"
3277
+ };
3278
+ const cliParams = context.params.getAll(paramDefs);
3279
+ const config2 = {
3280
+ mode: options?.mode ?? cliParams.mode,
3281
+ route: options?.route ?? cliParams.route,
3282
+ prefix: options?.prefix ?? cliParams.prefix,
3283
+ silent: options?.silent ?? cliParams.silent,
3284
+ showLevel: options?.showLevel ?? cliParams.showLevel,
3285
+ timestamp: options?.timestamp ?? cliParams.timestamp,
3286
+ levels: options?.levels ?? (cliParams.levels ? cliParams.levels.split(",") : void 0),
3287
+ progress: options?.progress ?? {
3288
+ withTimes: cliParams.progressWithTimes,
3289
+ throttleMs: cliParams.progressThrottleMs
3290
+ }
3291
+ };
3292
+ const logger = new _Logger(context, config2);
3293
+ context.logger = logger;
3294
+ return logger;
3295
+ }
3296
+ getDefaultOptions() {
3297
+ return {
3298
+ mode: "text",
3299
+ route: this.shouldUseIpcRoute() ? "ipc" : "console",
3300
+ prefix: void 0,
3301
+ silent: false,
3302
+ showLevel: true,
3303
+ timestamp: false,
3304
+ levels: ALL_LEVELS,
3305
+ progressTimes: false,
3306
+ progressThrottle: void 0
3307
+ };
3308
+ }
3309
+ updateTransport() {
3054
3310
  this.transport = this.options.route === "ipc" ? new ParentProcessTransport() : new ConsoleTransport();
3055
3311
  }
3056
3312
  setMode(mode) {
@@ -3159,7 +3415,7 @@ var CliToolkitLogger = class {
3159
3415
  parts.push(now.toISOString());
3160
3416
  }
3161
3417
  if (this.options.showLevel) {
3162
- parts.push(struct.level.toUpperCase());
3418
+ parts.push(struct.level.toUpperCase().padEnd(MAX_LEVEL_LENGTH));
3163
3419
  }
3164
3420
  if (struct.level === "progress") {
3165
3421
  if (struct.prefix) {
@@ -3193,22 +3449,6 @@ var CliToolkitLogger = class {
3193
3449
  inspectChunks(chunks) {
3194
3450
  return chunks.map((chunk) => import_util.default.inspect(chunk, { colors: true, depth: null })).join(" ");
3195
3451
  }
3196
- normalizeOptions(options) {
3197
- const { route, mode, prefix, silent, showLevel, timestamp, levels, progress } = options;
3198
- const shouldUseIpc = this.shouldUseIpcRoute();
3199
- const normalized = {
3200
- mode: this.isValidMode(mode) ? mode : "text",
3201
- route: route ?? (shouldUseIpc ? "ipc" : "console"),
3202
- prefix,
3203
- silent: silent ?? false,
3204
- showLevel: showLevel ?? true,
3205
- timestamp: timestamp ?? false,
3206
- levels: this.normalizeLevels(levels),
3207
- progressTimes: progress?.withTimes ?? false,
3208
- progressThrottle: progress?.throttleMs
3209
- };
3210
- return normalized;
3211
- }
3212
3452
  shouldUseIpcRoute() {
3213
3453
  if (process.env.VITEST || process.env.NODE_ENV === "test") {
3214
3454
  return false;
@@ -3239,35 +3479,44 @@ var CliToolkitLogger = class {
3239
3479
 
3240
3480
  // src/init/index.ts
3241
3481
  var import_events = require("events");
3482
+ function extractComponentOptions(opts, componentName) {
3483
+ const reservedKeys = ["overrides", "defaults", "modules"];
3484
+ const componentOptions = {};
3485
+ for (const [key, value] of Object.entries(opts)) {
3486
+ if (!reservedKeys.includes(key)) {
3487
+ componentOptions[key] = value;
3488
+ }
3489
+ }
3490
+ return componentOptions;
3491
+ }
3242
3492
  function setup(opts = {}) {
3243
- const args = new Args({
3493
+ const args = Args.init({
3244
3494
  overrides: opts.overrides || {},
3245
3495
  defaults: opts.defaults || {}
3246
3496
  });
3247
- const params = new Params({ args }, opts.overrides || {});
3248
- const loggerOptions = opts.logger || {};
3249
- const logger = new CliToolkitLogger({
3250
- mode: loggerOptions.mode || "text",
3251
- route: loggerOptions.route || "console",
3252
- prefix: loggerOptions.prefix,
3253
- silent: loggerOptions.silent,
3254
- showLevel: loggerOptions.showLevel,
3255
- timestamp: loggerOptions.timestamp,
3256
- levels: loggerOptions.levels
3257
- });
3258
- const cleanupFunctions = [];
3259
- const context = {
3497
+ const partialContext = {
3260
3498
  args,
3261
- params,
3262
- logger,
3263
3499
  emitter: new import_events.EventEmitter(),
3264
3500
  isStop: () => false,
3265
- // Will be set in init function
3266
- cleanupFunctions,
3501
+ cleanupFunctions: [],
3267
3502
  registerCleanup: (fn) => {
3268
- cleanupFunctions.push(fn);
3503
+ partialContext.cleanupFunctions.push(fn);
3269
3504
  }
3270
3505
  };
3506
+ const params = Params.init(partialContext, opts.overrides || {});
3507
+ partialContext.params = params;
3508
+ const loggerOptions = extractComponentOptions(opts, "logger");
3509
+ const logger = Logger.init(partialContext, loggerOptions);
3510
+ partialContext.logger = logger;
3511
+ const context = {
3512
+ args,
3513
+ params,
3514
+ logger,
3515
+ emitter: partialContext.emitter,
3516
+ isStop: partialContext.isStop,
3517
+ cleanupFunctions: partialContext.cleanupFunctions,
3518
+ registerCleanup: partialContext.registerCleanup
3519
+ };
3271
3520
  logger.debug("[setup] completed successfully");
3272
3521
  return context;
3273
3522
  }
@@ -3305,7 +3554,6 @@ function setupContext(opts = {}) {
3305
3554
  defaultFileSynopsisFunction,
3306
3555
  defaultVersionSynopsisFunction,
3307
3556
  getArgsInstance,
3308
- getParamsInstance,
3309
3557
  h,
3310
3558
  joiEdateType,
3311
3559
  joiStringArrayType,