@sundaeswap/sprinkles 0.5.0 → 0.6.1

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 (112) hide show
  1. package/dist/cjs/Sprinkle/__tests__/encryption.test.js +3 -1
  2. package/dist/cjs/Sprinkle/__tests__/encryption.test.js.map +1 -1
  3. package/dist/cjs/Sprinkle/__tests__/enhancements.test.js +3 -37
  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 +377 -84
  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 +9 -5
  12. package/dist/cjs/Sprinkle/__tests__/show-menu.test.js.map +1 -1
  13. package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js +9 -0
  14. package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
  15. package/dist/cjs/Sprinkle/index.js +174 -94
  16. package/dist/cjs/Sprinkle/index.js.map +1 -1
  17. package/dist/cjs/Sprinkle/menus/array-menu.js +195 -0
  18. package/dist/cjs/Sprinkle/menus/array-menu.js.map +1 -0
  19. package/dist/cjs/Sprinkle/menus/field-menu.js +161 -0
  20. package/dist/cjs/Sprinkle/menus/field-menu.js.map +1 -0
  21. package/dist/cjs/Sprinkle/menus/index.js +33 -0
  22. package/dist/cjs/Sprinkle/menus/index.js.map +1 -0
  23. package/dist/cjs/Sprinkle/menus/object-menu.js +324 -0
  24. package/dist/cjs/Sprinkle/menus/object-menu.js.map +1 -0
  25. package/dist/cjs/Sprinkle/prompts.js +68 -2
  26. package/dist/cjs/Sprinkle/prompts.js.map +1 -1
  27. package/dist/cjs/Sprinkle/type-guards.js +48 -1
  28. package/dist/cjs/Sprinkle/type-guards.js.map +1 -1
  29. package/dist/cjs/Sprinkle/types.js +24 -0
  30. package/dist/cjs/Sprinkle/types.js.map +1 -1
  31. package/dist/cjs/Sprinkle/utils/field-utils.js +154 -0
  32. package/dist/cjs/Sprinkle/utils/field-utils.js.map +1 -0
  33. package/dist/cjs/Sprinkle/utils/formatting.js +126 -0
  34. package/dist/cjs/Sprinkle/utils/formatting.js.map +1 -0
  35. package/dist/cjs/Sprinkle/utils/index.js +56 -0
  36. package/dist/cjs/Sprinkle/utils/index.js.map +1 -0
  37. package/dist/esm/Sprinkle/__tests__/encryption.test.js +3 -1
  38. package/dist/esm/Sprinkle/__tests__/encryption.test.js.map +1 -1
  39. package/dist/esm/Sprinkle/__tests__/enhancements.test.js +3 -37
  40. package/dist/esm/Sprinkle/__tests__/enhancements.test.js.map +1 -1
  41. package/dist/esm/Sprinkle/__tests__/field-utils.test.js +168 -0
  42. package/dist/esm/Sprinkle/__tests__/field-utils.test.js.map +1 -0
  43. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +378 -85
  44. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  45. package/dist/esm/Sprinkle/__tests__/formatting.test.js +95 -0
  46. package/dist/esm/Sprinkle/__tests__/formatting.test.js.map +1 -0
  47. package/dist/esm/Sprinkle/__tests__/show-menu.test.js +9 -5
  48. package/dist/esm/Sprinkle/__tests__/show-menu.test.js.map +1 -1
  49. package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js +9 -0
  50. package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
  51. package/dist/esm/Sprinkle/index.js +141 -96
  52. package/dist/esm/Sprinkle/index.js.map +1 -1
  53. package/dist/esm/Sprinkle/menus/array-menu.js +190 -0
  54. package/dist/esm/Sprinkle/menus/array-menu.js.map +1 -0
  55. package/dist/esm/Sprinkle/menus/field-menu.js +155 -0
  56. package/dist/esm/Sprinkle/menus/field-menu.js.map +1 -0
  57. package/dist/esm/Sprinkle/menus/index.js +8 -0
  58. package/dist/esm/Sprinkle/menus/index.js.map +1 -0
  59. package/dist/esm/Sprinkle/menus/object-menu.js +318 -0
  60. package/dist/esm/Sprinkle/menus/object-menu.js.map +1 -0
  61. package/dist/esm/Sprinkle/prompts.js +59 -1
  62. package/dist/esm/Sprinkle/prompts.js.map +1 -1
  63. package/dist/esm/Sprinkle/type-guards.js +42 -0
  64. package/dist/esm/Sprinkle/type-guards.js.map +1 -1
  65. package/dist/esm/Sprinkle/types.js +24 -0
  66. package/dist/esm/Sprinkle/types.js.map +1 -1
  67. package/dist/esm/Sprinkle/utils/field-utils.js +145 -0
  68. package/dist/esm/Sprinkle/utils/field-utils.js.map +1 -0
  69. package/dist/esm/Sprinkle/utils/formatting.js +118 -0
  70. package/dist/esm/Sprinkle/utils/formatting.js.map +1 -0
  71. package/dist/esm/Sprinkle/utils/index.js +7 -0
  72. package/dist/esm/Sprinkle/utils/index.js.map +1 -0
  73. package/dist/types/Sprinkle/index.d.ts +9 -3
  74. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  75. package/dist/types/Sprinkle/menus/array-menu.d.ts +31 -0
  76. package/dist/types/Sprinkle/menus/array-menu.d.ts.map +1 -0
  77. package/dist/types/Sprinkle/menus/field-menu.d.ts +34 -0
  78. package/dist/types/Sprinkle/menus/field-menu.d.ts.map +1 -0
  79. package/dist/types/Sprinkle/menus/index.d.ts +10 -0
  80. package/dist/types/Sprinkle/menus/index.d.ts.map +1 -0
  81. package/dist/types/Sprinkle/menus/object-menu.d.ts +34 -0
  82. package/dist/types/Sprinkle/menus/object-menu.d.ts.map +1 -0
  83. package/dist/types/Sprinkle/prompts.d.ts +25 -0
  84. package/dist/types/Sprinkle/prompts.d.ts.map +1 -1
  85. package/dist/types/Sprinkle/type-guards.d.ts +24 -1
  86. package/dist/types/Sprinkle/type-guards.d.ts.map +1 -1
  87. package/dist/types/Sprinkle/types.d.ts +53 -0
  88. package/dist/types/Sprinkle/types.d.ts.map +1 -1
  89. package/dist/types/Sprinkle/utils/field-utils.d.ts +47 -0
  90. package/dist/types/Sprinkle/utils/field-utils.d.ts.map +1 -0
  91. package/dist/types/Sprinkle/utils/formatting.d.ts +30 -0
  92. package/dist/types/Sprinkle/utils/formatting.d.ts.map +1 -0
  93. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  94. package/package.json +1 -1
  95. package/src/Sprinkle/__tests__/encryption.test.ts +2 -0
  96. package/src/Sprinkle/__tests__/enhancements.test.ts +3 -42
  97. package/src/Sprinkle/__tests__/field-utils.test.ts +191 -0
  98. package/src/Sprinkle/__tests__/fill-in-struct.test.ts +393 -100
  99. package/src/Sprinkle/__tests__/formatting.test.ts +115 -0
  100. package/src/Sprinkle/__tests__/show-menu.test.ts +14 -8
  101. package/src/Sprinkle/__tests__/tx-dialog.test.ts +9 -0
  102. package/src/Sprinkle/index.ts +175 -122
  103. package/src/Sprinkle/menus/array-menu.ts +191 -0
  104. package/src/Sprinkle/menus/field-menu.ts +145 -0
  105. package/src/Sprinkle/menus/index.ts +12 -0
  106. package/src/Sprinkle/menus/object-menu.ts +336 -0
  107. package/src/Sprinkle/prompts.ts +71 -1
  108. package/src/Sprinkle/type-guards.ts +42 -0
  109. package/src/Sprinkle/types.ts +43 -0
  110. package/src/Sprinkle/utils/field-utils.ts +158 -0
  111. package/src/Sprinkle/utils/formatting.ts +127 -0
  112. package/src/Sprinkle/utils/index.ts +17 -0
@@ -0,0 +1,115 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ formatValuePreview,
4
+ formatPath,
5
+ formatBreadcrumb,
6
+ } from "../utils/formatting.js";
7
+
8
+ describe("formatValuePreview", () => {
9
+ test("returns [not set] for undefined", () => {
10
+ expect(formatValuePreview(undefined)).toBe("[not set]");
11
+ });
12
+
13
+ test("returns null for null", () => {
14
+ expect(formatValuePreview(null)).toBe("null");
15
+ });
16
+
17
+ test("formats short strings with quotes", () => {
18
+ expect(formatValuePreview("hello")).toBe('"hello"');
19
+ });
20
+
21
+ test("truncates long strings", () => {
22
+ const longString = "a".repeat(50);
23
+ const result = formatValuePreview(longString, 40);
24
+ expect(result.length).toBeLessThanOrEqual(40);
25
+ expect(result).toContain("...");
26
+ });
27
+
28
+ test("formats numbers", () => {
29
+ expect(formatValuePreview(42)).toBe("42");
30
+ expect(formatValuePreview(3.14)).toBe("3.14");
31
+ });
32
+
33
+ test("formats bigints", () => {
34
+ expect(formatValuePreview(42n)).toBe("42");
35
+ });
36
+
37
+ test("formats booleans", () => {
38
+ expect(formatValuePreview(true)).toBe("true");
39
+ expect(formatValuePreview(false)).toBe("false");
40
+ });
41
+
42
+ test("formats arrays with item count", () => {
43
+ expect(formatValuePreview([])).toBe("[0 items]");
44
+ expect(formatValuePreview([1])).toBe("[1 item]");
45
+ expect(formatValuePreview([1, 2, 3])).toBe("[3 items]");
46
+ });
47
+
48
+ test("formats empty objects", () => {
49
+ expect(formatValuePreview({})).toBe("{}");
50
+ });
51
+
52
+ test("formats single-key objects", () => {
53
+ const result = formatValuePreview({ Signature: "abc123" });
54
+ expect(result).toContain("Signature");
55
+ expect(result).toContain("abc123");
56
+ });
57
+
58
+ test("formats multi-key objects with abbreviation", () => {
59
+ const result = formatValuePreview({ a: 1, b: 2, c: 3 });
60
+ expect(result).toContain("a");
61
+ expect(result).toContain("+2");
62
+ });
63
+ });
64
+
65
+ describe("formatPath", () => {
66
+ test("returns empty string for root-only path", () => {
67
+ expect(formatPath(["root"])).toBe("");
68
+ });
69
+
70
+ test("strips root prefix", () => {
71
+ expect(formatPath(["root", "name"])).toBe("name");
72
+ });
73
+
74
+ test("joins multiple segments with dots", () => {
75
+ expect(formatPath(["root", "settings", "permissions"])).toBe(
76
+ "settings.permissions",
77
+ );
78
+ });
79
+
80
+ test("handles paths without root", () => {
81
+ expect(formatPath(["settings", "name"])).toBe("settings.name");
82
+ });
83
+
84
+ test("handles empty path", () => {
85
+ expect(formatPath([])).toBe("");
86
+ });
87
+ });
88
+
89
+ describe("formatBreadcrumb", () => {
90
+ test("returns empty string for root-only path", () => {
91
+ expect(formatBreadcrumb(["root"])).toBe("");
92
+ });
93
+
94
+ test("strips root prefix", () => {
95
+ expect(formatBreadcrumb(["root", "name"])).toBe("name");
96
+ });
97
+
98
+ test("joins segments with arrow", () => {
99
+ const result = formatBreadcrumb(["root", "settings", "permissions"]);
100
+ expect(result).toContain("settings");
101
+ expect(result).toContain("\u2192"); // Unicode right arrow
102
+ expect(result).toContain("permissions");
103
+ });
104
+
105
+ test("truncates long paths from left", () => {
106
+ const path = ["root", "a", "b", "c", "d", "e", "final"];
107
+ const result = formatBreadcrumb(path, 20);
108
+ expect(result).toContain("...");
109
+ expect(result).toContain("final");
110
+ });
111
+
112
+ test("handles empty path", () => {
113
+ expect(formatBreadcrumb([])).toBe("");
114
+ });
115
+ });
@@ -17,6 +17,8 @@ mock.module("../prompts.js", () => ({
17
17
  inputCancellable: mockInputCancellable,
18
18
  passwordCancellable: mock(),
19
19
  confirmCancellable: mock(),
20
+ searchCancellable: mock(),
21
+ select: mockSelectCancellable,
20
22
  }));
21
23
 
22
24
  describe("showMenu", () => {
@@ -154,12 +156,14 @@ describe("showMenu", () => {
154
156
  test("View settings displays masked settings", async () => {
155
157
  const consoleSpy = spyOn(console, "log").mockImplementation(() => {});
156
158
 
157
- // Select "Settings & Profiles" (-5), then "View settings" (0), then "Back" (-1), then "Exit" (-1)
159
+ // Select "Settings & Profiles" (-5), then "View settings" (0),
160
+ // then "Continue" for the press Enter prompt, then "Back" (-1), then "Exit" (-1)
158
161
  mockSelectCancellable
159
- .mockResolvedValueOnce(-5) // Settings & Profiles
160
- .mockResolvedValueOnce(0) // View settings (first item)
161
- .mockResolvedValueOnce(-1) // Back
162
- .mockResolvedValueOnce(-1); // Exit
162
+ .mockResolvedValueOnce(-5) // Settings & Profiles
163
+ .mockResolvedValueOnce(0) // View settings (first item)
164
+ .mockResolvedValueOnce("continue") // Press Enter to continue
165
+ .mockResolvedValueOnce(-1) // Back
166
+ .mockResolvedValueOnce(-1); // Exit
163
167
 
164
168
  const menu: IMenu<any> = {
165
169
  title: "Test",
@@ -168,10 +172,12 @@ describe("showMenu", () => {
168
172
 
169
173
  await sprinkle.showMenu(menu);
170
174
 
171
- // Should have called console.log with the settings
175
+ // Should have called console.log with the settings (find the JSON output among breadcrumbs)
172
176
  expect(consoleSpy).toHaveBeenCalled();
173
- const output = consoleSpy.mock.calls[0][0];
174
- expect(output).toContain("name");
177
+ const jsonOutput = consoleSpy.mock.calls.find((call: any[]) =>
178
+ call[0] && typeof call[0] === "string" && call[0].includes('"name"')
179
+ );
180
+ expect(jsonOutput).toBeDefined();
175
181
 
176
182
  consoleSpy.mockRestore();
177
183
  });
@@ -48,6 +48,15 @@ mock.module("../prompts.js", () => ({
48
48
  const response = confirmResponses.shift();
49
49
  return response ?? false;
50
50
  }),
51
+ searchCancellable: mock(async () => "result"),
52
+ select: mock(async (opts: any) => {
53
+ selectCalls.push(opts);
54
+ const response = selectResponses.shift();
55
+ if (response === undefined) {
56
+ return "cancel";
57
+ }
58
+ return response;
59
+ }),
51
60
  }));
52
61
 
53
62
  // Mock clipboardy
@@ -8,13 +8,18 @@ import {
8
8
  import { CborSet, VkeyWitness, TxCBOR } from "@blaze-cardano/core";
9
9
  import {
10
10
  selectCancellable,
11
+ selectWithClear,
11
12
  inputCancellable,
13
+ inputWithClear,
12
14
  passwordCancellable,
15
+ passwordWithClear,
13
16
  confirmCancellable,
14
17
  searchCancellable,
15
18
  select,
16
19
  } from "./prompts.js";
20
+ import colors from "yoctocolors-cjs";
17
21
  import { type TSchema, Type, OptionalKind } from "@sinclair/typebox";
22
+ import { Value } from "@sinclair/typebox/value";
18
23
  import * as fs from "fs";
19
24
  import * as path from "path";
20
25
  export * from "@sinclair/typebox";
@@ -29,6 +34,11 @@ export type {
29
34
  IProfileEntry,
30
35
  TxDialogResult,
31
36
  TxDialogOptions,
37
+ FieldState,
38
+ RequiredFieldCount,
39
+ FieldMenuResult,
40
+ ObjectMenuResult,
41
+ ArrayMenuResult,
32
42
  } from "./types.js";
33
43
  export { UserCancelledError } from "./types.js";
34
44
  import type {
@@ -67,6 +77,11 @@ import {
67
77
  isTuple,
68
78
  isUnion,
69
79
  isSensitive,
80
+ isNull,
81
+ isNullable,
82
+ unwrapNullable,
83
+ hasDefault,
84
+ getDefault,
70
85
  } from "./type-guards.js";
71
86
  export {
72
87
  isOptional,
@@ -81,6 +96,11 @@ export {
81
96
  isTuple,
82
97
  isUnion,
83
98
  isSensitive,
99
+ isNull,
100
+ isNullable,
101
+ unwrapNullable,
102
+ hasDefault,
103
+ getDefault,
84
104
  } from "./type-guards.js";
85
105
 
86
106
  // Import schemas for use in this file
@@ -122,6 +142,10 @@ import {
122
142
  mergeSignatures,
123
143
  } from "./tx-dialog.js";
124
144
 
145
+ // Import menu modules
146
+ import { promptObject, promptArray } from "./menus/index.js";
147
+ import { formatPath } from "./utils/formatting.js";
148
+
125
149
  export interface IMenuAction<S extends TSchema> {
126
150
  title: string;
127
151
  action: (sprinkle: Sprinkle<S>) => Promise<Sprinkle<S> | void>;
@@ -503,10 +527,15 @@ export class Sprinkle<S extends TSchema> {
503
527
  // --- Menu ---
504
528
 
505
529
  async showMenu(menu: IMenu<S>): Promise<void> {
506
- return this._showMenu(menu, true);
530
+ return this._showMenu(menu, true, [menu.title]);
507
531
  }
508
532
 
509
- private async _showMenu(menu: IMenu<S>, main: boolean): Promise<void> {
533
+ private async _showMenu(menu: IMenu<S>, main: boolean, path: string[], clearPrevious = false): Promise<void> {
534
+ // Clear previous breadcrumb if coming back from action/submenu
535
+ if (clearPrevious) {
536
+ process.stdout.write("\x1b[1A\x1b[2K\x1b[G");
537
+ }
538
+
510
539
  if (menu.beforeShow) {
511
540
  await menu.beforeShow(this);
512
541
  }
@@ -523,11 +552,17 @@ export class Sprinkle<S extends TSchema> {
523
552
  choices.push({ name: "Settings & Profiles", value: -5 });
524
553
  choices.push({ name: "Exit", value: -1 });
525
554
  }
526
- const selectionResult = await selectCancellable({
555
+
556
+ // Show breadcrumb
557
+ const breadcrumb = path.join(" > ");
558
+ console.log(colors.dim("🍞 " + breadcrumb));
559
+
560
+ const selectionResult = await selectWithClear({
527
561
  message: "Select an option:",
528
562
  choices: choices,
529
563
  });
530
564
  // Handle escape (null) as Back
565
+ // Don't clear here - let the caller's clearPrevious handle it
531
566
  if (selectionResult === null) {
532
567
  return;
533
568
  }
@@ -539,16 +574,32 @@ export class Sprinkle<S extends TSchema> {
539
574
  {
540
575
  title: "View settings",
541
576
  action: async () => {
542
- console.log(
543
- JSON.stringify(this.getDisplaySettings(), bigIntReplacer, 2),
544
- );
577
+ const jsonStr = JSON.stringify(this.getDisplaySettings(), bigIntReplacer, 2);
578
+ const jsonLines = jsonStr.split("\n").length;
579
+ console.log(jsonStr);
580
+
581
+ // Wait for user to press Enter
582
+ await selectWithClear({
583
+ message: "Press Enter to continue...",
584
+ choices: [{ name: "Continue", value: "continue" }],
585
+ });
586
+
587
+ // Clear the JSON output
588
+ process.stdout.write("\x1b[1A\x1b[2K".repeat(jsonLines) + "\x1b[G");
545
589
  },
546
590
  },
547
591
  {
548
592
  title: "Edit settings",
549
593
  action: async () => {
550
- this.settings = await this.EditStruct(this.type, this.settings);
551
- this.saveSettings();
594
+ try {
595
+ this.settings = await this.EditStruct(this.type, this.settings);
596
+ this.saveSettings();
597
+ } catch (e) {
598
+ if (e instanceof UserCancelledError) {
599
+ return; // User cancelled, return to menu
600
+ }
601
+ throw e;
602
+ }
552
603
  },
553
604
  },
554
605
  {
@@ -591,24 +642,29 @@ export class Sprinkle<S extends TSchema> {
591
642
  },
592
643
  ],
593
644
  };
594
- await this._showMenu(settingsMenu, false);
595
- await this._showMenu(menu, main);
645
+ await this._showMenu(settingsMenu, false, [...path, "Settings & Profiles"], true);
646
+ await this._showMenu(menu, main, path, true);
596
647
  return;
597
648
  }
598
649
  if (selection === -1) {
650
+ // Don't clear here - let the caller's clearPrevious handle it
599
651
  return;
600
652
  }
601
653
  const selectedItem = menu.items[selection]!;
602
654
  if ("action" in selectedItem) {
655
+ // Update breadcrumb to show current action
656
+ process.stdout.write("\x1b[1A\x1b[2K\x1b[G");
657
+ console.log(colors.dim("🍞 " + [...path, selectedItem.title].join(" > ")));
658
+
603
659
  const result = await selectedItem.action(this);
604
660
  if (result instanceof Sprinkle) {
605
661
  this.settings = result.settings;
606
662
  this.saveSettings();
607
663
  }
608
- await this._showMenu(menu, main);
664
+ await this._showMenu(menu, main, path, true);
609
665
  } else {
610
- await this._showMenu(selectedItem, false);
611
- await this._showMenu(menu, main);
666
+ await this._showMenu(selectedItem, false, [...path, selectedItem.title], true);
667
+ await this._showMenu(menu, main, path, true);
612
668
  }
613
669
  return;
614
670
  }
@@ -789,7 +845,7 @@ export class Sprinkle<S extends TSchema> {
789
845
  choices.push({ name: "Submit transaction", value: "submit" });
790
846
  choices.push({ name: "Cancel", value: "cancel" });
791
847
 
792
- const selection = await selectCancellable({
848
+ const selection = await selectWithClear({
793
849
  message: "Select an option:",
794
850
  choices,
795
851
  });
@@ -1002,68 +1058,20 @@ export class Sprinkle<S extends TSchema> {
1002
1058
  }
1003
1059
  }
1004
1060
 
1061
+ /**
1062
+ * Edit an existing struct value using a menu-based interface.
1063
+ * This is now unified with FillInStruct - both use the same menu system.
1064
+ * @param type - The TypeBox schema
1065
+ * @param current - The current value to edit
1066
+ * @returns The edited value
1067
+ */
1005
1068
  async EditStruct<U extends TSchema>(
1006
1069
  type: U,
1007
1070
  current: TExact<U>,
1008
1071
  ): Promise<TExact<U>> {
1009
- return this._editStruct<U>(type, ["root"], current);
1010
- }
1011
-
1012
- async _editStruct<U extends TSchema>(
1013
- type: U,
1014
- path: string[],
1015
- current?: TExact<U>,
1016
- ): Promise<TExact<U>> {
1017
- if (isObject(type)) {
1018
- const obj = {} as Record<string, unknown>;
1019
- const fields = type["properties"] as Record<string, U>;
1020
- const menuItems: TMenuItem<S>[] = [];
1021
- const currentRecord = current as Record<string, unknown>;
1022
- for (const [field, fieldType] of Object.entries(fields)) {
1023
- if (current && field in currentRecord) {
1024
- obj[field] = currentRecord[field] as TExact<U>;
1025
- }
1026
- const menuTitle = Sprinkle.ExtractMessage(
1027
- fieldType,
1028
- `Edit ${field} at ${path.join(".")}`,
1029
- );
1030
- if (
1031
- isOptional(fieldType) &&
1032
- current &&
1033
- currentRecord[field] !== undefined
1034
- ) {
1035
- menuItems.push({
1036
- title: `Clear ${field}`,
1037
- action: async (sprinkle: Sprinkle<S>) => {
1038
- obj[field] = undefined;
1039
- return sprinkle;
1040
- },
1041
- });
1042
- }
1043
- menuItems.push({
1044
- title: menuTitle,
1045
- action: async (sprinkle: Sprinkle<S>) => {
1046
- const fieldValue = await sprinkle._editStruct(
1047
- fieldType,
1048
- path.concat([field]),
1049
- current && field in currentRecord
1050
- ? (currentRecord[field] as TExact<U>)
1051
- : undefined,
1052
- );
1053
- obj[field] = fieldValue;
1054
- return sprinkle;
1055
- },
1056
- });
1057
- }
1058
- const editMenu: IMenu<S> = {
1059
- title: "Test",
1060
- items: menuItems,
1061
- };
1062
- await this._showMenu(editMenu, false);
1063
- return obj as TExact<U>;
1064
- }
1065
-
1066
- return this._fillInStruct<U>(type, path, {}, current);
1072
+ // Use FillInStruct with current values as defaults
1073
+ // The menu system will show existing values and allow editing
1074
+ return this.FillInStruct(type, current);
1067
1075
  }
1068
1076
 
1069
1077
  async FillInStruct<U extends TSchema>(
@@ -1092,10 +1100,11 @@ export class Sprinkle<S extends TSchema> {
1092
1100
  >;
1093
1101
  }
1094
1102
  if (isOptional(type)) {
1095
- const shouldSet = await selectCancellable({
1103
+ const pathDisplay = formatPath(path) || "value";
1104
+ const shouldSet = await selectWithClear({
1096
1105
  message: Sprinkle.ExtractMessage(
1097
1106
  type,
1098
- `Set value for ${path.join(".")}?`,
1107
+ `Set value for ${pathDisplay}?`,
1099
1108
  ),
1100
1109
  choices: [
1101
1110
  { name: "Yes", value: true },
@@ -1116,6 +1125,7 @@ export class Sprinkle<S extends TSchema> {
1116
1125
  }
1117
1126
 
1118
1127
  if (isUnion(type)) {
1128
+ const pathDisplay = formatPath(path) || "value";
1119
1129
  const choices = [];
1120
1130
  const resolved = this.resolveType(type, path, defs);
1121
1131
  for (const variant of resolved.anyOf) {
@@ -1124,25 +1134,57 @@ export class Sprinkle<S extends TSchema> {
1124
1134
  value: variant,
1125
1135
  });
1126
1136
  }
1127
- const selectionResult = await selectCancellable({
1137
+ const selectionResult = await selectWithClear({
1128
1138
  message: Sprinkle.ExtractMessage(
1129
1139
  resolved,
1130
- `Enter a choice for ${path.join(".")}`,
1140
+ `Enter a choice for ${pathDisplay}`,
1131
1141
  ),
1132
1142
  choices: choices,
1133
- default: def ? `${def}` : undefined,
1134
1143
  });
1135
1144
  if (selectionResult === null) {
1136
1145
  throw new UserCancelledError();
1137
1146
  }
1138
1147
  const selection = selectionResult as TSchema;
1139
- return this._fillInStruct(selection, path, defs) as Promise<TExact<U>>;
1148
+ // Determine if the provided default value matches the selected variant.
1149
+ // For discriminated unions (objects with literal fields like `type`), check
1150
+ // if the literal field values in the selected variant match those in `def`.
1151
+ // For non-discriminated unions, fall back to structural matching with Value.Check.
1152
+ let matchedDef: unknown = undefined;
1153
+ if (def !== undefined) {
1154
+ if (isObject(selection)) {
1155
+ // Check if all literal fields in the selected variant match def
1156
+ const literalFields = Object.entries(selection.properties ?? {}).filter(
1157
+ ([, fieldSchema]) => isLiteral(fieldSchema as TSchema),
1158
+ );
1159
+ if (literalFields.length > 0) {
1160
+ const defRecord = def as Record<string, unknown>;
1161
+ const allLiteralsMatch = literalFields.every(
1162
+ ([fieldName, fieldSchema]) =>
1163
+ defRecord[fieldName] === (fieldSchema as unknown as { const: unknown }).const,
1164
+ );
1165
+ if (allLiteralsMatch) {
1166
+ matchedDef = def;
1167
+ }
1168
+ } else {
1169
+ // No literal discriminators - use structural check
1170
+ if (Value.Check(selection, def)) {
1171
+ matchedDef = def;
1172
+ }
1173
+ }
1174
+ } else {
1175
+ // Non-object variant - use structural check
1176
+ if (Value.Check(selection, def)) {
1177
+ matchedDef = def;
1178
+ }
1179
+ }
1180
+ }
1181
+ return this._fillInStruct(selection, path, defs, matchedDef as TExact<typeof selection>) as Promise<TExact<U>>;
1140
1182
  }
1141
1183
 
1142
1184
  if (isString(type)) {
1143
1185
  // Special handling for hot wallet private key - offer generation option
1144
1186
  if (type.title === "Hot Wallet Private Key") {
1145
- const choice = await selectCancellable({
1187
+ const choice = await selectWithClear({
1146
1188
  message: "Hot wallet setup:",
1147
1189
  choices: [
1148
1190
  { name: "Enter existing private key", value: "existing" },
@@ -1157,7 +1199,7 @@ export class Sprinkle<S extends TSchema> {
1157
1199
  return Sprinkle.generateWalletFromMnemonic() as Promise<TExact<U>>;
1158
1200
  }
1159
1201
  // Fall through to password prompt for "existing" choice
1160
- const answer = await passwordCancellable({
1202
+ const answer = await passwordWithClear({
1161
1203
  message: "Enter your private key:",
1162
1204
  });
1163
1205
  if (answer === null) {
@@ -1166,18 +1208,19 @@ export class Sprinkle<S extends TSchema> {
1166
1208
  return answer as TExact<U>;
1167
1209
  }
1168
1210
 
1211
+ const pathDisplay = formatPath(path) || "value";
1169
1212
  const defaultString = (def ? def : this.defaults["string"]) as
1170
1213
  | string
1171
1214
  | undefined;
1172
1215
  const message = Sprinkle.ExtractMessage(
1173
1216
  type,
1174
- `Enter a string for ${path.join(".")}`,
1217
+ `Enter a string for ${pathDisplay}`,
1175
1218
  );
1176
1219
  let answer: string | null;
1177
1220
  if (isSensitive(type)) {
1178
- answer = await passwordCancellable({ message });
1221
+ answer = await passwordWithClear({ message });
1179
1222
  } else {
1180
- answer = await inputCancellable({ message, default: defaultString });
1223
+ answer = await inputWithClear({ message, default: defaultString });
1181
1224
  if (answer !== null) {
1182
1225
  this.defaults["string"] = answer;
1183
1226
  }
@@ -1189,10 +1232,11 @@ export class Sprinkle<S extends TSchema> {
1189
1232
  }
1190
1233
 
1191
1234
  if (isBigInt(type)) {
1192
- const answer = await inputCancellable({
1235
+ const pathDisplay = formatPath(path) || "value";
1236
+ const answer = await inputWithClear({
1193
1237
  message: Sprinkle.ExtractMessage(
1194
1238
  type,
1195
- `Enter a bigint for ${path.join(".")}`,
1239
+ `Enter a bigint for ${pathDisplay}`,
1196
1240
  ),
1197
1241
  default: def ? (def as bigint).toString() : undefined,
1198
1242
  validate: (s) => {
@@ -1215,47 +1259,55 @@ export class Sprinkle<S extends TSchema> {
1215
1259
  }
1216
1260
 
1217
1261
  if (isObject(type)) {
1218
- const obj = {} as Record<string, unknown>;
1219
- const fields = type["properties"] as Record<string, U>;
1220
- for (const [field, fieldType] of Object.entries(fields)) {
1221
- const fieldValue = await this._fillInStruct(
1222
- fieldType,
1223
- path.concat([field]),
1224
- defs,
1225
- def
1226
- ? ((def as Record<string, unknown>)[field] as TExact<U>)
1227
- : undefined,
1228
- );
1229
- obj[field] = fieldValue;
1262
+ // Use menu-based editing for objects
1263
+ const defaults = def as Record<string, unknown> | undefined;
1264
+ const sprinkle = this;
1265
+ const result = await promptObject({
1266
+ type,
1267
+ path,
1268
+ defs,
1269
+ defaults,
1270
+ fillField: async <T extends TSchema>(
1271
+ fieldType: T,
1272
+ fieldPath: string[],
1273
+ fieldDefs: Record<string, TSchema>,
1274
+ fieldDef?: unknown,
1275
+ ) => {
1276
+ return sprinkle._fillInStruct(fieldType, fieldPath, fieldDefs, fieldDef as TExact<T>);
1277
+ },
1278
+ });
1279
+
1280
+ if (result.action === "cancel") {
1281
+ throw new UserCancelledError();
1230
1282
  }
1231
- return obj as TExact<U>;
1283
+
1284
+ return result.value as TExact<U>;
1232
1285
  }
1233
1286
 
1234
- //TODO: support starting with default values for arrays and allow removal of items
1235
1287
  if (isArray(type)) {
1236
- const arr: unknown[] = [];
1237
- const itemType = type.items as U;
1238
- let addMore = true;
1239
- while (addMore) {
1240
- const itemValue = await this._fillInStruct(
1241
- itemType,
1242
- path.concat([`[${arr.length}]`]),
1243
- defs,
1244
- );
1245
- arr.push(itemValue);
1246
- const continueAnswer = await selectCancellable({
1247
- message: `Add another item to ${path.join(".")}?`,
1248
- choices: [
1249
- { name: "Yes", value: true },
1250
- { name: "No", value: false },
1251
- ],
1252
- });
1253
- if (continueAnswer === null) {
1254
- throw new UserCancelledError();
1255
- }
1256
- addMore = continueAnswer as boolean;
1288
+ // Use menu-based editing for arrays
1289
+ const defaults = def as unknown[] | undefined;
1290
+ const sprinkle = this;
1291
+ const result = await promptArray({
1292
+ type,
1293
+ path,
1294
+ defs,
1295
+ defaults,
1296
+ fillField: async <T extends TSchema>(
1297
+ itemType: T,
1298
+ itemPath: string[],
1299
+ itemDefs: Record<string, TSchema>,
1300
+ itemDef?: unknown,
1301
+ ) => {
1302
+ return sprinkle._fillInStruct(itemType, itemPath, itemDefs, itemDef as TExact<T>);
1303
+ },
1304
+ });
1305
+
1306
+ if (result.action === "back") {
1307
+ throw new UserCancelledError();
1257
1308
  }
1258
- return arr as TExact<U>;
1309
+
1310
+ return result.value as TExact<U>;
1259
1311
  }
1260
1312
 
1261
1313
  if (isTuple(type)) {
@@ -1274,8 +1326,9 @@ export class Sprinkle<S extends TSchema> {
1274
1326
  return result as TExact<U>;
1275
1327
  }
1276
1328
 
1329
+ const pathDisplay = formatPath(path) || "root";
1277
1330
  throw new Error(
1278
- `Unable to fill in struct for type at path ${path.join(".")}`,
1331
+ `Unable to fill in struct for type at path ${pathDisplay}`,
1279
1332
  );
1280
1333
  }
1281
1334