@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.
- package/dist/cjs/Sprinkle/__tests__/encryption.test.js +3 -1
- package/dist/cjs/Sprinkle/__tests__/encryption.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/enhancements.test.js +3 -37
- package/dist/cjs/Sprinkle/__tests__/enhancements.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/field-utils.test.js +170 -0
- package/dist/cjs/Sprinkle/__tests__/field-utils.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +377 -84
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/formatting.test.js +97 -0
- package/dist/cjs/Sprinkle/__tests__/formatting.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/show-menu.test.js +9 -5
- package/dist/cjs/Sprinkle/__tests__/show-menu.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js +9 -0
- package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
- package/dist/cjs/Sprinkle/index.js +174 -94
- package/dist/cjs/Sprinkle/index.js.map +1 -1
- package/dist/cjs/Sprinkle/menus/array-menu.js +195 -0
- package/dist/cjs/Sprinkle/menus/array-menu.js.map +1 -0
- package/dist/cjs/Sprinkle/menus/field-menu.js +161 -0
- package/dist/cjs/Sprinkle/menus/field-menu.js.map +1 -0
- package/dist/cjs/Sprinkle/menus/index.js +33 -0
- package/dist/cjs/Sprinkle/menus/index.js.map +1 -0
- package/dist/cjs/Sprinkle/menus/object-menu.js +324 -0
- package/dist/cjs/Sprinkle/menus/object-menu.js.map +1 -0
- package/dist/cjs/Sprinkle/prompts.js +68 -2
- package/dist/cjs/Sprinkle/prompts.js.map +1 -1
- package/dist/cjs/Sprinkle/type-guards.js +48 -1
- package/dist/cjs/Sprinkle/type-guards.js.map +1 -1
- package/dist/cjs/Sprinkle/types.js +24 -0
- package/dist/cjs/Sprinkle/types.js.map +1 -1
- package/dist/cjs/Sprinkle/utils/field-utils.js +154 -0
- package/dist/cjs/Sprinkle/utils/field-utils.js.map +1 -0
- package/dist/cjs/Sprinkle/utils/formatting.js +126 -0
- package/dist/cjs/Sprinkle/utils/formatting.js.map +1 -0
- package/dist/cjs/Sprinkle/utils/index.js +56 -0
- package/dist/cjs/Sprinkle/utils/index.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/encryption.test.js +3 -1
- package/dist/esm/Sprinkle/__tests__/encryption.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/enhancements.test.js +3 -37
- package/dist/esm/Sprinkle/__tests__/enhancements.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/field-utils.test.js +168 -0
- package/dist/esm/Sprinkle/__tests__/field-utils.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +378 -85
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/formatting.test.js +95 -0
- package/dist/esm/Sprinkle/__tests__/formatting.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/show-menu.test.js +9 -5
- package/dist/esm/Sprinkle/__tests__/show-menu.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js +9 -0
- package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
- package/dist/esm/Sprinkle/index.js +141 -96
- package/dist/esm/Sprinkle/index.js.map +1 -1
- package/dist/esm/Sprinkle/menus/array-menu.js +190 -0
- package/dist/esm/Sprinkle/menus/array-menu.js.map +1 -0
- package/dist/esm/Sprinkle/menus/field-menu.js +155 -0
- package/dist/esm/Sprinkle/menus/field-menu.js.map +1 -0
- package/dist/esm/Sprinkle/menus/index.js +8 -0
- package/dist/esm/Sprinkle/menus/index.js.map +1 -0
- package/dist/esm/Sprinkle/menus/object-menu.js +318 -0
- package/dist/esm/Sprinkle/menus/object-menu.js.map +1 -0
- package/dist/esm/Sprinkle/prompts.js +59 -1
- package/dist/esm/Sprinkle/prompts.js.map +1 -1
- package/dist/esm/Sprinkle/type-guards.js +42 -0
- package/dist/esm/Sprinkle/type-guards.js.map +1 -1
- package/dist/esm/Sprinkle/types.js +24 -0
- package/dist/esm/Sprinkle/types.js.map +1 -1
- package/dist/esm/Sprinkle/utils/field-utils.js +145 -0
- package/dist/esm/Sprinkle/utils/field-utils.js.map +1 -0
- package/dist/esm/Sprinkle/utils/formatting.js +118 -0
- package/dist/esm/Sprinkle/utils/formatting.js.map +1 -0
- package/dist/esm/Sprinkle/utils/index.js +7 -0
- package/dist/esm/Sprinkle/utils/index.js.map +1 -0
- package/dist/types/Sprinkle/index.d.ts +9 -3
- package/dist/types/Sprinkle/index.d.ts.map +1 -1
- package/dist/types/Sprinkle/menus/array-menu.d.ts +31 -0
- package/dist/types/Sprinkle/menus/array-menu.d.ts.map +1 -0
- package/dist/types/Sprinkle/menus/field-menu.d.ts +34 -0
- package/dist/types/Sprinkle/menus/field-menu.d.ts.map +1 -0
- package/dist/types/Sprinkle/menus/index.d.ts +10 -0
- package/dist/types/Sprinkle/menus/index.d.ts.map +1 -0
- package/dist/types/Sprinkle/menus/object-menu.d.ts +34 -0
- package/dist/types/Sprinkle/menus/object-menu.d.ts.map +1 -0
- package/dist/types/Sprinkle/prompts.d.ts +25 -0
- package/dist/types/Sprinkle/prompts.d.ts.map +1 -1
- package/dist/types/Sprinkle/type-guards.d.ts +24 -1
- package/dist/types/Sprinkle/type-guards.d.ts.map +1 -1
- package/dist/types/Sprinkle/types.d.ts +53 -0
- package/dist/types/Sprinkle/types.d.ts.map +1 -1
- package/dist/types/Sprinkle/utils/field-utils.d.ts +47 -0
- package/dist/types/Sprinkle/utils/field-utils.d.ts.map +1 -0
- package/dist/types/Sprinkle/utils/formatting.d.ts +30 -0
- package/dist/types/Sprinkle/utils/formatting.d.ts.map +1 -0
- package/dist/types/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/Sprinkle/__tests__/encryption.test.ts +2 -0
- package/src/Sprinkle/__tests__/enhancements.test.ts +3 -42
- package/src/Sprinkle/__tests__/field-utils.test.ts +191 -0
- package/src/Sprinkle/__tests__/fill-in-struct.test.ts +393 -100
- package/src/Sprinkle/__tests__/formatting.test.ts +115 -0
- package/src/Sprinkle/__tests__/show-menu.test.ts +14 -8
- package/src/Sprinkle/__tests__/tx-dialog.test.ts +9 -0
- package/src/Sprinkle/index.ts +175 -122
- package/src/Sprinkle/menus/array-menu.ts +191 -0
- package/src/Sprinkle/menus/field-menu.ts +145 -0
- package/src/Sprinkle/menus/index.ts +12 -0
- package/src/Sprinkle/menus/object-menu.ts +336 -0
- package/src/Sprinkle/prompts.ts +71 -1
- package/src/Sprinkle/type-guards.ts +42 -0
- package/src/Sprinkle/types.ts +43 -0
- package/src/Sprinkle/utils/field-utils.ts +158 -0
- package/src/Sprinkle/utils/formatting.ts +127 -0
- 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),
|
|
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)
|
|
160
|
-
.mockResolvedValueOnce(0)
|
|
161
|
-
.mockResolvedValueOnce(
|
|
162
|
-
.mockResolvedValueOnce(-1)
|
|
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
|
|
174
|
-
|
|
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
|
package/src/Sprinkle/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
543
|
-
|
|
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
|
-
|
|
551
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1103
|
+
const pathDisplay = formatPath(path) || "value";
|
|
1104
|
+
const shouldSet = await selectWithClear({
|
|
1096
1105
|
message: Sprinkle.ExtractMessage(
|
|
1097
1106
|
type,
|
|
1098
|
-
`Set value for ${
|
|
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
|
|
1137
|
+
const selectionResult = await selectWithClear({
|
|
1128
1138
|
message: Sprinkle.ExtractMessage(
|
|
1129
1139
|
resolved,
|
|
1130
|
-
`Enter a choice for ${
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ${
|
|
1217
|
+
`Enter a string for ${pathDisplay}`,
|
|
1175
1218
|
);
|
|
1176
1219
|
let answer: string | null;
|
|
1177
1220
|
if (isSensitive(type)) {
|
|
1178
|
-
answer = await
|
|
1221
|
+
answer = await passwordWithClear({ message });
|
|
1179
1222
|
} else {
|
|
1180
|
-
answer = await
|
|
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
|
|
1235
|
+
const pathDisplay = formatPath(path) || "value";
|
|
1236
|
+
const answer = await inputWithClear({
|
|
1193
1237
|
message: Sprinkle.ExtractMessage(
|
|
1194
1238
|
type,
|
|
1195
|
-
`Enter a bigint for ${
|
|
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
|
-
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
-
|
|
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 ${
|
|
1331
|
+
`Unable to fill in struct for type at path ${pathDisplay}`,
|
|
1279
1332
|
);
|
|
1280
1333
|
}
|
|
1281
1334
|
|