@sundaeswap/sprinkles 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/dist/cjs/Sprinkle/__tests__/encryption.test.js +22 -8
  2. package/dist/cjs/Sprinkle/__tests__/encryption.test.js.map +1 -1
  3. package/dist/cjs/Sprinkle/__tests__/enhancements.test.js +37 -46
  4. package/dist/cjs/Sprinkle/__tests__/enhancements.test.js.map +1 -1
  5. package/dist/cjs/Sprinkle/__tests__/field-utils.test.js +170 -0
  6. package/dist/cjs/Sprinkle/__tests__/field-utils.test.js.map +1 -0
  7. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +283 -81
  8. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  9. package/dist/cjs/Sprinkle/__tests__/formatting.test.js +97 -0
  10. package/dist/cjs/Sprinkle/__tests__/formatting.test.js.map +1 -0
  11. package/dist/cjs/Sprinkle/__tests__/show-menu.test.js +97 -7
  12. package/dist/cjs/Sprinkle/__tests__/show-menu.test.js.map +1 -1
  13. package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js +30 -0
  14. package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
  15. package/dist/cjs/Sprinkle/encryption.js +131 -0
  16. package/dist/cjs/Sprinkle/encryption.js.map +1 -0
  17. package/dist/cjs/Sprinkle/index.js +427 -438
  18. package/dist/cjs/Sprinkle/index.js.map +1 -1
  19. package/dist/cjs/Sprinkle/menus/array-menu.js +195 -0
  20. package/dist/cjs/Sprinkle/menus/array-menu.js.map +1 -0
  21. package/dist/cjs/Sprinkle/menus/field-menu.js +161 -0
  22. package/dist/cjs/Sprinkle/menus/field-menu.js.map +1 -0
  23. package/dist/cjs/Sprinkle/menus/index.js +33 -0
  24. package/dist/cjs/Sprinkle/menus/index.js.map +1 -0
  25. package/dist/cjs/Sprinkle/menus/object-menu.js +324 -0
  26. package/dist/cjs/Sprinkle/menus/object-menu.js.map +1 -0
  27. package/dist/cjs/Sprinkle/prompts.js +459 -0
  28. package/dist/cjs/Sprinkle/prompts.js.map +1 -0
  29. package/dist/cjs/Sprinkle/schemas.js +97 -0
  30. package/dist/cjs/Sprinkle/schemas.js.map +1 -0
  31. package/dist/cjs/Sprinkle/tx-dialog.js +101 -0
  32. package/dist/cjs/Sprinkle/tx-dialog.js.map +1 -0
  33. package/dist/cjs/Sprinkle/type-guards.js +89 -0
  34. package/dist/cjs/Sprinkle/type-guards.js.map +1 -0
  35. package/dist/cjs/Sprinkle/types.js +73 -0
  36. package/dist/cjs/Sprinkle/types.js.map +1 -0
  37. package/dist/cjs/Sprinkle/utils/field-utils.js +154 -0
  38. package/dist/cjs/Sprinkle/utils/field-utils.js.map +1 -0
  39. package/dist/cjs/Sprinkle/utils/formatting.js +126 -0
  40. package/dist/cjs/Sprinkle/utils/formatting.js.map +1 -0
  41. package/dist/cjs/Sprinkle/utils/index.js +56 -0
  42. package/dist/cjs/Sprinkle/utils/index.js.map +1 -0
  43. package/dist/cjs/Sprinkle/wallet.js +98 -0
  44. package/dist/cjs/Sprinkle/wallet.js.map +1 -0
  45. package/dist/esm/Sprinkle/__tests__/encryption.test.js +22 -8
  46. package/dist/esm/Sprinkle/__tests__/encryption.test.js.map +1 -1
  47. package/dist/esm/Sprinkle/__tests__/enhancements.test.js +37 -46
  48. package/dist/esm/Sprinkle/__tests__/enhancements.test.js.map +1 -1
  49. package/dist/esm/Sprinkle/__tests__/field-utils.test.js +168 -0
  50. package/dist/esm/Sprinkle/__tests__/field-utils.test.js.map +1 -0
  51. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +284 -82
  52. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  53. package/dist/esm/Sprinkle/__tests__/formatting.test.js +95 -0
  54. package/dist/esm/Sprinkle/__tests__/formatting.test.js.map +1 -0
  55. package/dist/esm/Sprinkle/__tests__/show-menu.test.js +98 -8
  56. package/dist/esm/Sprinkle/__tests__/show-menu.test.js.map +1 -1
  57. package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js +30 -0
  58. package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
  59. package/dist/esm/Sprinkle/encryption.js +117 -0
  60. package/dist/esm/Sprinkle/encryption.js.map +1 -0
  61. package/dist/esm/Sprinkle/index.js +248 -425
  62. package/dist/esm/Sprinkle/index.js.map +1 -1
  63. package/dist/esm/Sprinkle/menus/array-menu.js +190 -0
  64. package/dist/esm/Sprinkle/menus/array-menu.js.map +1 -0
  65. package/dist/esm/Sprinkle/menus/field-menu.js +155 -0
  66. package/dist/esm/Sprinkle/menus/field-menu.js.map +1 -0
  67. package/dist/esm/Sprinkle/menus/index.js +8 -0
  68. package/dist/esm/Sprinkle/menus/index.js.map +1 -0
  69. package/dist/esm/Sprinkle/menus/object-menu.js +318 -0
  70. package/dist/esm/Sprinkle/menus/object-menu.js.map +1 -0
  71. package/dist/esm/Sprinkle/prompts.js +443 -0
  72. package/dist/esm/Sprinkle/prompts.js.map +1 -0
  73. package/dist/esm/Sprinkle/schemas.js +91 -0
  74. package/dist/esm/Sprinkle/schemas.js.map +1 -0
  75. package/dist/esm/Sprinkle/tx-dialog.js +90 -0
  76. package/dist/esm/Sprinkle/tx-dialog.js.map +1 -0
  77. package/dist/esm/Sprinkle/type-guards.js +66 -0
  78. package/dist/esm/Sprinkle/type-guards.js.map +1 -0
  79. package/dist/esm/Sprinkle/types.js +66 -0
  80. package/dist/esm/Sprinkle/types.js.map +1 -0
  81. package/dist/esm/Sprinkle/utils/field-utils.js +145 -0
  82. package/dist/esm/Sprinkle/utils/field-utils.js.map +1 -0
  83. package/dist/esm/Sprinkle/utils/formatting.js +118 -0
  84. package/dist/esm/Sprinkle/utils/formatting.js.map +1 -0
  85. package/dist/esm/Sprinkle/utils/index.js +7 -0
  86. package/dist/esm/Sprinkle/utils/index.js.map +1 -0
  87. package/dist/esm/Sprinkle/wallet.js +90 -0
  88. package/dist/esm/Sprinkle/wallet.js.map +1 -0
  89. package/dist/types/Sprinkle/encryption.d.ts +43 -0
  90. package/dist/types/Sprinkle/encryption.d.ts.map +1 -0
  91. package/dist/types/Sprinkle/index.d.ts +17 -177
  92. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  93. package/dist/types/Sprinkle/menus/array-menu.d.ts +31 -0
  94. package/dist/types/Sprinkle/menus/array-menu.d.ts.map +1 -0
  95. package/dist/types/Sprinkle/menus/field-menu.d.ts +34 -0
  96. package/dist/types/Sprinkle/menus/field-menu.d.ts.map +1 -0
  97. package/dist/types/Sprinkle/menus/index.d.ts +10 -0
  98. package/dist/types/Sprinkle/menus/index.d.ts.map +1 -0
  99. package/dist/types/Sprinkle/menus/object-menu.d.ts +34 -0
  100. package/dist/types/Sprinkle/menus/object-menu.d.ts.map +1 -0
  101. package/dist/types/Sprinkle/prompts.d.ts +119 -0
  102. package/dist/types/Sprinkle/prompts.d.ts.map +1 -0
  103. package/dist/types/Sprinkle/schemas.d.ts +125 -0
  104. package/dist/types/Sprinkle/schemas.d.ts.map +1 -0
  105. package/dist/types/Sprinkle/tx-dialog.d.ts +37 -0
  106. package/dist/types/Sprinkle/tx-dialog.d.ts.map +1 -0
  107. package/dist/types/Sprinkle/type-guards.d.ts +45 -0
  108. package/dist/types/Sprinkle/type-guards.d.ts.map +1 -0
  109. package/dist/types/Sprinkle/types.d.ts +115 -0
  110. package/dist/types/Sprinkle/types.d.ts.map +1 -0
  111. package/dist/types/Sprinkle/utils/field-utils.d.ts +47 -0
  112. package/dist/types/Sprinkle/utils/field-utils.d.ts.map +1 -0
  113. package/dist/types/Sprinkle/utils/formatting.d.ts +30 -0
  114. package/dist/types/Sprinkle/utils/formatting.d.ts.map +1 -0
  115. package/dist/types/Sprinkle/wallet.d.ts +27 -0
  116. package/dist/types/Sprinkle/wallet.d.ts.map +1 -0
  117. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  118. package/package.json +1 -1
  119. package/src/Sprinkle/__tests__/encryption.test.ts +23 -8
  120. package/src/Sprinkle/__tests__/enhancements.test.ts +34 -47
  121. package/src/Sprinkle/__tests__/field-utils.test.ts +191 -0
  122. package/src/Sprinkle/__tests__/fill-in-struct.test.ts +301 -86
  123. package/src/Sprinkle/__tests__/formatting.test.ts +115 -0
  124. package/src/Sprinkle/__tests__/show-menu.test.ts +102 -8
  125. package/src/Sprinkle/__tests__/tx-dialog.test.ts +30 -0
  126. package/src/Sprinkle/encryption.ts +130 -0
  127. package/src/Sprinkle/index.ts +368 -598
  128. package/src/Sprinkle/menus/array-menu.ts +191 -0
  129. package/src/Sprinkle/menus/field-menu.ts +145 -0
  130. package/src/Sprinkle/menus/index.ts +12 -0
  131. package/src/Sprinkle/menus/object-menu.ts +336 -0
  132. package/src/Sprinkle/prompts.ts +551 -0
  133. package/src/Sprinkle/schemas.ts +111 -0
  134. package/src/Sprinkle/tx-dialog.ts +100 -0
  135. package/src/Sprinkle/type-guards.ts +93 -0
  136. package/src/Sprinkle/types.ts +116 -0
  137. package/src/Sprinkle/utils/field-utils.ts +158 -0
  138. package/src/Sprinkle/utils/formatting.ts +127 -0
  139. package/src/Sprinkle/utils/index.ts +17 -0
  140. package/src/Sprinkle/wallet.ts +133 -0
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Cancellable prompt wrappers with escape key support.
3
+ * Uses @inquirer/core's createPrompt and useKeypress for proper escape handling.
4
+ */
5
+
6
+ import {
7
+ createPrompt,
8
+ useState,
9
+ useKeypress,
10
+ usePrefix,
11
+ usePagination,
12
+ useMemo,
13
+ makeTheme,
14
+ isEnterKey,
15
+ isUpKey,
16
+ isDownKey,
17
+ isBackspaceKey,
18
+ Separator,
19
+ ValidationError,
20
+ type Theme,
21
+ } from "@inquirer/core";
22
+ import type { PartialDeep } from "@inquirer/type";
23
+ import colors from "yoctocolors-cjs";
24
+ import figures from "@inquirer/figures";
25
+
26
+ // Check if key is escape
27
+ const isEscapeKey = (key: { name: string }) => key.name === "escape";
28
+
29
+ // Theme for select prompt
30
+ const selectTheme = {
31
+ icon: { cursor: figures.pointer },
32
+ style: {
33
+ disabled: (text: string) => colors.dim(`- ${text}`),
34
+ description: (text: string) => colors.cyan(text),
35
+ helpTip: colors.dim("(Use arrow keys, Enter to select, Esc to cancel)"),
36
+ },
37
+ helpMode: "always" as const,
38
+ };
39
+
40
+ // Theme for input prompt
41
+ const inputTheme = {
42
+ style: {
43
+ helpTip: colors.dim("(Esc to cancel)"),
44
+ },
45
+ };
46
+
47
+ interface SelectChoice<T> {
48
+ name?: string;
49
+ value: T;
50
+ description?: string;
51
+ short?: string;
52
+ disabled?: boolean | string;
53
+ }
54
+
55
+ interface SelectConfig<T> {
56
+ message: string;
57
+ choices: ReadonlyArray<SelectChoice<T> | Separator | string>;
58
+ default?: T;
59
+ pageSize?: number;
60
+ loop?: boolean;
61
+ theme?: PartialDeep<Theme<typeof selectTheme>>;
62
+ }
63
+
64
+ function isSelectable<T>(item: SelectChoice<T> | Separator): item is SelectChoice<T> {
65
+ return !Separator.isSeparator(item) && !item.disabled;
66
+ }
67
+
68
+ function normalizeChoices<T>(
69
+ choices: ReadonlyArray<SelectChoice<T> | Separator | string>,
70
+ ): Array<SelectChoice<T> | Separator> {
71
+ return choices.map((choice) => {
72
+ if (Separator.isSeparator(choice)) return choice;
73
+ if (typeof choice === "string") {
74
+ return {
75
+ value: choice as unknown as T,
76
+ name: choice,
77
+ short: choice,
78
+ disabled: false,
79
+ };
80
+ }
81
+ const name = choice.name ?? String(choice.value);
82
+ return {
83
+ value: choice.value,
84
+ name,
85
+ short: choice.short ?? name,
86
+ disabled: choice.disabled ?? false,
87
+ description: choice.description,
88
+ };
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Select prompt with escape key support.
94
+ * Returns null if user presses Escape.
95
+ */
96
+ export const selectCancellable = createPrompt<
97
+ unknown | null,
98
+ SelectConfig<unknown>
99
+ >((config, done) => {
100
+ const { loop = true, pageSize = 15 } = config;
101
+ const theme = makeTheme(selectTheme, config.theme);
102
+ const [status, setStatus] = useState<"idle" | "done" | "cancelled">("idle");
103
+ const prefix = usePrefix({ status: status === "cancelled" ? "done" : status, theme });
104
+
105
+ const items = useMemo(
106
+ () => normalizeChoices(config.choices),
107
+ [config.choices],
108
+ );
109
+
110
+ const bounds = useMemo(() => {
111
+ const first = items.findIndex(isSelectable);
112
+ // Find last selectable item (compatible with es2022)
113
+ let last = -1;
114
+ for (let i = items.length - 1; i >= 0; i--) {
115
+ if (isSelectable(items[i]!)) {
116
+ last = i;
117
+ break;
118
+ }
119
+ }
120
+ if (first === -1) {
121
+ throw new ValidationError(
122
+ "[select prompt] No selectable choices. All choices are disabled.",
123
+ );
124
+ }
125
+ return { first, last };
126
+ }, [items]);
127
+
128
+ const defaultItemIndex = useMemo(() => {
129
+ if (!("default" in config)) return -1;
130
+ return items.findIndex(
131
+ (item) => isSelectable(item) && item.value === config.default,
132
+ );
133
+ }, [config.default, items]);
134
+
135
+ const [active, setActive] = useState(
136
+ defaultItemIndex === -1 ? bounds.first : defaultItemIndex,
137
+ );
138
+
139
+ const selectedChoice = items[active] as SelectChoice<unknown>;
140
+
141
+ useKeypress((key, rl) => {
142
+ if (isEscapeKey(key)) {
143
+ setStatus("cancelled");
144
+ done(null);
145
+ } else if (isEnterKey(key)) {
146
+ setStatus("done");
147
+ done(selectedChoice.value);
148
+ } else if (isUpKey(key)) {
149
+ rl.clearLine(0);
150
+ if (active === bounds.first && loop) {
151
+ setActive(bounds.last);
152
+ } else {
153
+ let newActive = active - 1;
154
+ while (newActive >= bounds.first && !isSelectable(items[newActive]!)) {
155
+ newActive--;
156
+ }
157
+ if (newActive >= bounds.first) setActive(newActive);
158
+ }
159
+ } else if (isDownKey(key)) {
160
+ rl.clearLine(0);
161
+ if (active === bounds.last && loop) {
162
+ setActive(bounds.first);
163
+ } else {
164
+ let newActive = active + 1;
165
+ while (newActive <= bounds.last && !isSelectable(items[newActive]!)) {
166
+ newActive++;
167
+ }
168
+ if (newActive <= bounds.last) setActive(newActive);
169
+ }
170
+ }
171
+ });
172
+
173
+ const page = usePagination({
174
+ items,
175
+ active,
176
+ renderItem: ({ item, isActive }) => {
177
+ if (Separator.isSeparator(item)) {
178
+ return ` ${item.separator}`;
179
+ }
180
+ const line = item.name;
181
+ if (item.disabled) {
182
+ const disabledLabel =
183
+ typeof item.disabled === "string" ? item.disabled : "(disabled)";
184
+ return colors.dim(`- ${line} ${disabledLabel}`);
185
+ }
186
+ const cursor = isActive ? figures.pointer : " ";
187
+ const color = isActive ? colors.cyan : (x: string) => x;
188
+ return color(`${cursor} ${line}`);
189
+ },
190
+ pageSize,
191
+ loop,
192
+ });
193
+
194
+ if (status === "cancelled") {
195
+ return `${prefix} ${config.message} ${colors.dim("(cancelled)")}`;
196
+ }
197
+
198
+ if (status === "done") {
199
+ return `${prefix} ${config.message} ${colors.cyan(selectedChoice?.short ?? selectedChoice?.name ?? "")}`;
200
+ }
201
+
202
+ const helpTip = colors.dim("(Use arrow keys, Enter to select, Esc to cancel)");
203
+ return `${prefix} ${config.message} ${helpTip}\n${page}`;
204
+ });
205
+
206
+ /**
207
+ * Clears N lines above the cursor.
208
+ * Used after prompts to clean up menu output.
209
+ */
210
+ export function clearLines(count: number): void {
211
+ // Move up and clear each line
212
+ process.stdout.write("\x1b[1A\x1b[2K".repeat(count));
213
+ }
214
+
215
+ /**
216
+ * Select prompt that clears its output after completion.
217
+ * Returns the selected value or null if cancelled.
218
+ */
219
+ export async function selectWithClear<T>(
220
+ config: SelectConfig<T>,
221
+ ): Promise<T | null> {
222
+ const result = await selectCancellable(config as SelectConfig<unknown>);
223
+
224
+ // Clear the "done" line that inquirer left behind
225
+ // Move up one line, clear it, move cursor to start
226
+ process.stdout.write("\x1b[1A\x1b[2K\x1b[G");
227
+
228
+ return result as T | null;
229
+ }
230
+
231
+ /**
232
+ * Input prompt that clears its output after completion.
233
+ * Returns the input value or null if cancelled.
234
+ */
235
+ export async function inputWithClear(
236
+ config: InputConfig,
237
+ ): Promise<string | null> {
238
+ const result = await inputCancellable(config);
239
+
240
+ // Clear the "done" line
241
+ process.stdout.write("\x1b[1A\x1b[2K\x1b[G");
242
+
243
+ return result;
244
+ }
245
+
246
+ /**
247
+ * Password prompt that clears its output after completion.
248
+ * Returns the password or null if cancelled.
249
+ */
250
+ export async function passwordWithClear(
251
+ config: PasswordConfig,
252
+ ): Promise<string | null> {
253
+ const result = await passwordCancellable(config);
254
+
255
+ // Clear the "done" line
256
+ process.stdout.write("\x1b[1A\x1b[2K\x1b[G");
257
+
258
+ return result;
259
+ }
260
+
261
+ /**
262
+ * Confirm prompt that clears its output after completion.
263
+ * Returns true/false or null if cancelled.
264
+ */
265
+ export async function confirmWithClear(
266
+ config: ConfirmConfig,
267
+ ): Promise<boolean | null> {
268
+ const result = await confirmCancellable(config);
269
+
270
+ // Clear the "done" line
271
+ process.stdout.write("\x1b[1A\x1b[2K\x1b[G");
272
+
273
+ return result;
274
+ }
275
+
276
+ interface InputConfig {
277
+ message: string;
278
+ default?: string;
279
+ transformer?: (value: string, options: { isFinal: boolean }) => string;
280
+ validate?: (value: string) => boolean | string | Promise<boolean | string>;
281
+ }
282
+
283
+ /**
284
+ * Input prompt with escape key support.
285
+ * Returns null if user presses Escape.
286
+ */
287
+ export const inputCancellable = createPrompt<string | null, InputConfig>(
288
+ (config, done) => {
289
+ const [status, setStatus] = useState<"idle" | "done" | "cancelled">("idle");
290
+ const [value, setValue] = useState(config.default ?? "");
291
+ const [touched, setTouched] = useState(false);
292
+ const [error, setError] = useState<string | null>(null);
293
+ const theme = makeTheme(inputTheme);
294
+ const prefix = usePrefix({ status: status === "cancelled" ? "done" : status, theme });
295
+
296
+ useKeypress(async (key, rl) => {
297
+ if (isEscapeKey(key)) {
298
+ setStatus("cancelled");
299
+ done(null);
300
+ } else if (isEnterKey(key)) {
301
+ // If user has edited, use their value (even if empty); otherwise use default
302
+ const answer = touched ? value : (config.default ?? "");
303
+ if (config.validate) {
304
+ const result = await config.validate(answer);
305
+ if (result !== true) {
306
+ setError(typeof result === "string" ? result : "Invalid input");
307
+ return;
308
+ }
309
+ }
310
+ setStatus("done");
311
+ done(answer);
312
+ } else if (isBackspaceKey(key)) {
313
+ // Sync with readline's buffer after backspace
314
+ setValue(rl.line);
315
+ setTouched(true);
316
+ setError(null);
317
+ } else if (key.name !== "tab" && !key.ctrl && !(key as any).meta) {
318
+ // Sync state with readline's current line buffer
319
+ setValue(rl.line);
320
+ setTouched(true);
321
+ setError(null);
322
+ }
323
+ });
324
+
325
+ const displayValue = config.transformer
326
+ ? config.transformer(value, { isFinal: status === "done" })
327
+ : value;
328
+
329
+ if (status === "cancelled") {
330
+ return `${prefix} ${config.message} ${colors.dim("(cancelled)")}`;
331
+ }
332
+
333
+ if (status === "done") {
334
+ return `${prefix} ${config.message} ${colors.cyan(displayValue)}`;
335
+ }
336
+
337
+ const helpTip = colors.dim("(Esc to cancel)");
338
+ const errorMsg = error ? colors.red(`\n> ${error}`) : "";
339
+ return `${prefix} ${config.message} ${helpTip} ${displayValue}${errorMsg}`;
340
+ },
341
+ );
342
+
343
+ interface PasswordConfig {
344
+ message: string;
345
+ mask?: string;
346
+ validate?: (value: string) => boolean | string | Promise<boolean | string>;
347
+ }
348
+
349
+ /**
350
+ * Password prompt with escape key support.
351
+ * Returns null if user presses Escape.
352
+ */
353
+ export const passwordCancellable = createPrompt<string | null, PasswordConfig>(
354
+ (config, done) => {
355
+ const [status, setStatus] = useState<"idle" | "done" | "cancelled">("idle");
356
+ const [value, setValue] = useState("");
357
+ const [error, setError] = useState<string | null>(null);
358
+ const theme = makeTheme(inputTheme);
359
+ const prefix = usePrefix({ status: status === "cancelled" ? "done" : status, theme });
360
+ const mask = config.mask ?? "*";
361
+
362
+ useKeypress(async (key, rl) => {
363
+ if (isEscapeKey(key)) {
364
+ setStatus("cancelled");
365
+ done(null);
366
+ } else if (isEnterKey(key)) {
367
+ if (config.validate) {
368
+ const result = await config.validate(value);
369
+ if (result !== true) {
370
+ setError(typeof result === "string" ? result : "Invalid input");
371
+ return;
372
+ }
373
+ }
374
+ setStatus("done");
375
+ done(value);
376
+ } else if (isBackspaceKey(key)) {
377
+ setValue(value.slice(0, -1));
378
+ setError(null);
379
+ } else if (key.name !== "tab" && !key.ctrl && !(key as any).meta) {
380
+ // Use key.sequence for single character to avoid rl.line buffer issues
381
+ const char = (key as any).sequence ?? "";
382
+ if (char) {
383
+ rl.clearLine(0);
384
+ setValue(value + char);
385
+ setError(null);
386
+ }
387
+ }
388
+ });
389
+
390
+ const masked = mask.repeat(value.length);
391
+
392
+ if (status === "cancelled") {
393
+ return `${prefix} ${config.message} ${colors.dim("(cancelled)")}`;
394
+ }
395
+
396
+ if (status === "done") {
397
+ return `${prefix} ${config.message} ${colors.dim(masked)}`;
398
+ }
399
+
400
+ const helpTip = colors.dim("(Esc to cancel)");
401
+ const errorMsg = error ? colors.red(`\n> ${error}`) : "";
402
+ return `${prefix} ${config.message} ${helpTip} ${masked}${errorMsg}`;
403
+ },
404
+ );
405
+
406
+ interface ConfirmConfig {
407
+ message: string;
408
+ default?: boolean;
409
+ }
410
+
411
+ /**
412
+ * Confirm prompt with escape key support.
413
+ * Returns null if user presses Escape.
414
+ */
415
+ export const confirmCancellable = createPrompt<boolean | null, ConfirmConfig>(
416
+ (config, done) => {
417
+ const [status, setStatus] = useState<"idle" | "done" | "cancelled">("idle");
418
+ // Track value as undefined when no default, requiring explicit y/n
419
+ const [value, setValue] = useState<boolean | undefined>(config.default);
420
+ const theme = makeTheme(inputTheme);
421
+ const prefix = usePrefix({ status: status === "cancelled" ? "done" : status, theme });
422
+
423
+ useKeypress((key) => {
424
+ if (isEscapeKey(key)) {
425
+ setStatus("cancelled");
426
+ done(null);
427
+ } else if (isEnterKey(key)) {
428
+ // Only accept Enter if a value has been chosen (explicit or default)
429
+ if (value !== undefined) {
430
+ setStatus("done");
431
+ done(value);
432
+ }
433
+ } else if (key.name === "y" || key.name === "Y") {
434
+ setValue(true);
435
+ } else if (key.name === "n" || key.name === "N") {
436
+ setValue(false);
437
+ }
438
+ });
439
+
440
+ const hint = config.default === true ? "(Y/n)" : config.default === false ? "(y/N)" : "(y/n)";
441
+ const displayValue = value === true ? "Yes" : value === false ? "No" : "";
442
+
443
+ if (status === "cancelled") {
444
+ return `${prefix} ${config.message} ${colors.dim("(cancelled)")}`;
445
+ }
446
+
447
+ if (status === "done") {
448
+ return `${prefix} ${config.message} ${colors.cyan(displayValue)}`;
449
+ }
450
+
451
+ const helpTip = colors.dim("(Esc to cancel)");
452
+ return `${prefix} ${config.message} ${helpTip} ${hint} ${displayValue}`;
453
+ },
454
+ );
455
+
456
+ /**
457
+ * Type-safe wrapper for selectCancellable.
458
+ * Returns the selected value with proper typing, or null if cancelled.
459
+ */
460
+ export async function select<T>(config: SelectConfig<T>): Promise<T | null> {
461
+ return selectCancellable(config as SelectConfig<unknown>) as Promise<T | null>;
462
+ }
463
+
464
+ interface SearchConfig<T> {
465
+ message: string;
466
+ source: (
467
+ term: string | undefined,
468
+ ) => Promise<{ name: string; value: T }[]> | { name: string; value: T }[];
469
+ }
470
+
471
+ /**
472
+ * Search prompt with escape key support.
473
+ * Returns null if user presses Escape.
474
+ *
475
+ * Note: This wraps @inquirer/search which has built-in escape handling,
476
+ * but we provide a consistent API with other cancellable prompts.
477
+ */
478
+ export const searchCancellable = createPrompt<unknown | null, SearchConfig<unknown>>(
479
+ (config, done) => {
480
+ const [status, setStatus] = useState<"idle" | "done" | "cancelled">("idle");
481
+ const [searchTerm, setSearchTerm] = useState("");
482
+ const [results, setResults] = useState<{ name: string; value: unknown }[]>([]);
483
+ const [active, setActive] = useState(0);
484
+ const [loading, setLoading] = useState(false);
485
+ const theme = makeTheme(selectTheme);
486
+ const prefix = usePrefix({ status: status === "cancelled" ? "done" : status, theme });
487
+
488
+ // Fetch results when search term changes
489
+ useMemo(async () => {
490
+ setLoading(true);
491
+ try {
492
+ const items = await config.source(searchTerm || undefined);
493
+ setResults(items);
494
+ setActive(0);
495
+ } finally {
496
+ setLoading(false);
497
+ }
498
+ }, [searchTerm]);
499
+
500
+ useKeypress((key, rl) => {
501
+ if (isEscapeKey(key)) {
502
+ setStatus("cancelled");
503
+ done(null);
504
+ } else if (isEnterKey(key) && results.length > 0) {
505
+ setStatus("done");
506
+ done(results[active]?.value ?? null);
507
+ } else if (isUpKey(key) && results.length > 0) {
508
+ rl.clearLine(0);
509
+ setActive(active > 0 ? active - 1 : results.length - 1);
510
+ } else if (isDownKey(key) && results.length > 0) {
511
+ rl.clearLine(0);
512
+ setActive(active < results.length - 1 ? active + 1 : 0);
513
+ } else if (isBackspaceKey(key)) {
514
+ setSearchTerm(rl.line);
515
+ } else if (key.name !== "tab" && !key.ctrl && !(key as any).meta) {
516
+ setSearchTerm(rl.line);
517
+ }
518
+ });
519
+
520
+ if (status === "cancelled") {
521
+ return `${prefix} ${config.message} ${colors.dim("(cancelled)")}`;
522
+ }
523
+
524
+ if (status === "done") {
525
+ const selected = results[active];
526
+ return `${prefix} ${config.message} ${colors.cyan(selected?.name ?? "")}`;
527
+ }
528
+
529
+ const helpTip = colors.dim("(Type to search, Esc to cancel)");
530
+ const searchLine = `${prefix} ${config.message} ${helpTip} ${searchTerm}`;
531
+
532
+ if (loading) {
533
+ return `${searchLine}\n${colors.dim(" Searching...")}`;
534
+ }
535
+
536
+ if (results.length === 0) {
537
+ return `${searchLine}\n${colors.dim(" No results")}`;
538
+ }
539
+
540
+ const resultLines = results
541
+ .slice(0, 7)
542
+ .map((item, i) => {
543
+ const cursor = i === active ? figures.pointer : " ";
544
+ const color = i === active ? colors.cyan : (x: string) => x;
545
+ return color(`${cursor} ${item.name}`);
546
+ })
547
+ .join("\n");
548
+
549
+ return `${searchLine}\n${resultLines}`;
550
+ },
551
+ );
@@ -0,0 +1,111 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { TExact } from "./types.js";
3
+
4
+ /**
5
+ * Network selection schema (mainnet, preview, preprod).
6
+ */
7
+ export const NetworkSchema = Type.Union([
8
+ Type.Literal("mainnet"),
9
+ Type.Literal("preview"),
10
+ Type.Literal("preprod"),
11
+ ]);
12
+
13
+ /**
14
+ * Recursive multisig script module for Cardano native scripts.
15
+ */
16
+ export const MultisigScriptModule = Type.Module({
17
+ MultisigScript: Type.Union([
18
+ Type.Object({
19
+ Signature: Type.Object(
20
+ {
21
+ key_hash: Type.String(),
22
+ },
23
+ { ctor: 0n },
24
+ ),
25
+ }),
26
+ Type.Object({
27
+ AllOf: Type.Object(
28
+ {
29
+ scripts: Type.Array(Type.Ref("MultisigScript")),
30
+ },
31
+ { ctor: 1n },
32
+ ),
33
+ }),
34
+ Type.Object({
35
+ AnyOf: Type.Object(
36
+ {
37
+ scripts: Type.Array(Type.Ref("MultisigScript")),
38
+ },
39
+ { ctor: 2n },
40
+ ),
41
+ }),
42
+ Type.Object({
43
+ AtLeast: Type.Object(
44
+ {
45
+ required: Type.BigInt(),
46
+ scripts: Type.Array(Type.Ref("MultisigScript")),
47
+ },
48
+ { ctor: 3n },
49
+ ),
50
+ }),
51
+ Type.Object({
52
+ Before: Type.Object(
53
+ {
54
+ time: Type.BigInt(),
55
+ },
56
+ { ctor: 4n },
57
+ ),
58
+ }),
59
+ Type.Object({
60
+ After: Type.Object(
61
+ {
62
+ time: Type.BigInt(),
63
+ },
64
+ { ctor: 5n },
65
+ ),
66
+ }),
67
+ Type.Object({
68
+ Script: Type.Object(
69
+ {
70
+ script_hash: Type.String(),
71
+ },
72
+ { ctor: 6n },
73
+ ),
74
+ }),
75
+ ]),
76
+ });
77
+
78
+ export const MultisigScript = MultisigScriptModule.Import("MultisigScript");
79
+ export type TMultisigScript = TExact<typeof MultisigScript>;
80
+
81
+ /**
82
+ * Provider settings schema for blockchain data providers.
83
+ */
84
+ export const ProviderSettingsSchema = Type.Union([
85
+ Type.Object({
86
+ type: Type.Literal("blockfrost"),
87
+ projectId: Type.String({ minLength: 1, title: "Blockfrost Project ID" }),
88
+ }),
89
+ Type.Object({
90
+ type: Type.Literal("maestro"),
91
+ apiKey: Type.String({ minLength: 1, title: "Maestro API Key" }),
92
+ }),
93
+ ]);
94
+
95
+ /**
96
+ * Wallet settings schema for hot and cold wallets.
97
+ */
98
+ export const WalletSettingsSchema = Type.Union([
99
+ Type.Object({
100
+ type: Type.Literal("hot"),
101
+ privateKey: Type.String({
102
+ minLength: 1,
103
+ title: "Hot Wallet Private Key",
104
+ sensitive: true,
105
+ }),
106
+ }),
107
+ Type.Object({
108
+ type: Type.Literal("cold"),
109
+ address: Type.String({ minLength: 1, title: "Cold Wallet Address" }),
110
+ }),
111
+ ]);