@nmakarov/cli-toolkit 0.7.0 → 0.7.2

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