@sundaeswap/sprinkles 0.3.0 → 0.5.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 (88) hide show
  1. package/dist/cjs/Sprinkle/__tests__/encryption.test.js +20 -8
  2. package/dist/cjs/Sprinkle/__tests__/encryption.test.js.map +1 -1
  3. package/dist/cjs/Sprinkle/__tests__/enhancements.test.js +41 -16
  4. package/dist/cjs/Sprinkle/__tests__/enhancements.test.js.map +1 -1
  5. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +85 -38
  6. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  7. package/dist/cjs/Sprinkle/__tests__/settings-persistence.test.js +120 -0
  8. package/dist/cjs/Sprinkle/__tests__/settings-persistence.test.js.map +1 -1
  9. package/dist/cjs/Sprinkle/__tests__/show-menu.test.js +93 -7
  10. package/dist/cjs/Sprinkle/__tests__/show-menu.test.js.map +1 -1
  11. package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js +21 -0
  12. package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
  13. package/dist/cjs/Sprinkle/encryption.js +131 -0
  14. package/dist/cjs/Sprinkle/encryption.js.map +1 -0
  15. package/dist/cjs/Sprinkle/index.js +318 -352
  16. package/dist/cjs/Sprinkle/index.js.map +1 -1
  17. package/dist/cjs/Sprinkle/prompts.js +393 -0
  18. package/dist/cjs/Sprinkle/prompts.js.map +1 -0
  19. package/dist/cjs/Sprinkle/schemas.js +97 -0
  20. package/dist/cjs/Sprinkle/schemas.js.map +1 -0
  21. package/dist/cjs/Sprinkle/tx-dialog.js +101 -0
  22. package/dist/cjs/Sprinkle/tx-dialog.js.map +1 -0
  23. package/dist/cjs/Sprinkle/type-guards.js +42 -0
  24. package/dist/cjs/Sprinkle/type-guards.js.map +1 -0
  25. package/dist/cjs/Sprinkle/types.js +49 -0
  26. package/dist/cjs/Sprinkle/types.js.map +1 -0
  27. package/dist/cjs/Sprinkle/wallet.js +98 -0
  28. package/dist/cjs/Sprinkle/wallet.js.map +1 -0
  29. package/dist/esm/Sprinkle/__tests__/encryption.test.js +20 -8
  30. package/dist/esm/Sprinkle/__tests__/encryption.test.js.map +1 -1
  31. package/dist/esm/Sprinkle/__tests__/enhancements.test.js +41 -16
  32. package/dist/esm/Sprinkle/__tests__/enhancements.test.js.map +1 -1
  33. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +85 -38
  34. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  35. package/dist/esm/Sprinkle/__tests__/settings-persistence.test.js +120 -0
  36. package/dist/esm/Sprinkle/__tests__/settings-persistence.test.js.map +1 -1
  37. package/dist/esm/Sprinkle/__tests__/show-menu.test.js +94 -8
  38. package/dist/esm/Sprinkle/__tests__/show-menu.test.js.map +1 -1
  39. package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js +21 -0
  40. package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
  41. package/dist/esm/Sprinkle/encryption.js +117 -0
  42. package/dist/esm/Sprinkle/encryption.js.map +1 -0
  43. package/dist/esm/Sprinkle/index.js +172 -337
  44. package/dist/esm/Sprinkle/index.js.map +1 -1
  45. package/dist/esm/Sprinkle/prompts.js +385 -0
  46. package/dist/esm/Sprinkle/prompts.js.map +1 -0
  47. package/dist/esm/Sprinkle/schemas.js +91 -0
  48. package/dist/esm/Sprinkle/schemas.js.map +1 -0
  49. package/dist/esm/Sprinkle/tx-dialog.js +90 -0
  50. package/dist/esm/Sprinkle/tx-dialog.js.map +1 -0
  51. package/dist/esm/Sprinkle/type-guards.js +24 -0
  52. package/dist/esm/Sprinkle/type-guards.js.map +1 -0
  53. package/dist/esm/Sprinkle/types.js +42 -0
  54. package/dist/esm/Sprinkle/types.js.map +1 -0
  55. package/dist/esm/Sprinkle/wallet.js +90 -0
  56. package/dist/esm/Sprinkle/wallet.js.map +1 -0
  57. package/dist/types/Sprinkle/encryption.d.ts +43 -0
  58. package/dist/types/Sprinkle/encryption.d.ts.map +1 -0
  59. package/dist/types/Sprinkle/index.d.ts +13 -174
  60. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  61. package/dist/types/Sprinkle/prompts.d.ts +94 -0
  62. package/dist/types/Sprinkle/prompts.d.ts.map +1 -0
  63. package/dist/types/Sprinkle/schemas.d.ts +125 -0
  64. package/dist/types/Sprinkle/schemas.d.ts.map +1 -0
  65. package/dist/types/Sprinkle/tx-dialog.d.ts +37 -0
  66. package/dist/types/Sprinkle/tx-dialog.d.ts.map +1 -0
  67. package/dist/types/Sprinkle/type-guards.d.ts +22 -0
  68. package/dist/types/Sprinkle/type-guards.d.ts.map +1 -0
  69. package/dist/types/Sprinkle/types.d.ts +62 -0
  70. package/dist/types/Sprinkle/types.d.ts.map +1 -0
  71. package/dist/types/Sprinkle/wallet.d.ts +27 -0
  72. package/dist/types/Sprinkle/wallet.d.ts.map +1 -0
  73. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  74. package/package.json +1 -1
  75. package/src/Sprinkle/__tests__/encryption.test.ts +21 -8
  76. package/src/Sprinkle/__tests__/enhancements.test.ts +41 -15
  77. package/src/Sprinkle/__tests__/fill-in-struct.test.ts +104 -38
  78. package/src/Sprinkle/__tests__/settings-persistence.test.ts +108 -0
  79. package/src/Sprinkle/__tests__/show-menu.test.ts +96 -8
  80. package/src/Sprinkle/__tests__/tx-dialog.test.ts +21 -0
  81. package/src/Sprinkle/encryption.ts +130 -0
  82. package/src/Sprinkle/index.ts +265 -478
  83. package/src/Sprinkle/prompts.ts +481 -0
  84. package/src/Sprinkle/schemas.ts +111 -0
  85. package/src/Sprinkle/tx-dialog.ts +100 -0
  86. package/src/Sprinkle/type-guards.ts +51 -0
  87. package/src/Sprinkle/types.ts +73 -0
  88. package/src/Sprinkle/wallet.ts +133 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sundaeswap/sprinkles",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "A TypeScript library for building interactive CLI menus and TUI applications with TypeBox schema validation",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -8,6 +8,9 @@ import { withProfile } from "./test-helpers.js";
8
8
  const mockSelect = mock();
9
9
  const mockInput = mock();
10
10
  const mockPassword = mock();
11
+ const mockSelectCancellable = mock();
12
+ const mockInputCancellable = mock();
13
+ const mockPasswordCancellable = mock();
11
14
 
12
15
  mock.module("@inquirer/prompts", () => ({
13
16
  select: mockSelect,
@@ -15,6 +18,13 @@ mock.module("@inquirer/prompts", () => ({
15
18
  password: mockPassword,
16
19
  }));
17
20
 
21
+ mock.module("../prompts.js", () => ({
22
+ selectCancellable: mockSelectCancellable,
23
+ inputCancellable: mockInputCancellable,
24
+ passwordCancellable: mockPasswordCancellable,
25
+ confirmCancellable: mock(),
26
+ }));
27
+
18
28
  describe("Encryption & Sensitive Fields", () => {
19
29
  let tmpDir: string;
20
30
 
@@ -23,6 +33,9 @@ describe("Encryption & Sensitive Fields", () => {
23
33
  mockSelect.mockClear();
24
34
  mockInput.mockClear();
25
35
  mockPassword.mockClear();
36
+ mockSelectCancellable.mockClear();
37
+ mockInputCancellable.mockClear();
38
+ mockPasswordCancellable.mockClear();
26
39
  });
27
40
 
28
41
  afterEach(() => {
@@ -36,13 +49,13 @@ describe("Encryption & Sensitive Fields", () => {
36
49
  });
37
50
  const sprinkle = new Sprinkle(schema, tmpDir);
38
51
 
39
- mockPassword.mockResolvedValueOnce("my-secret");
52
+ mockPasswordCancellable.mockResolvedValueOnce("my-secret");
40
53
 
41
54
  const result = await sprinkle.FillInStruct(schema);
42
55
  expect(result).toEqual({ secret: "my-secret" });
43
- expect(mockPassword).toHaveBeenCalledTimes(1);
44
- expect(mockPassword.mock.calls[0][0].message).toBe("Enter secret");
45
- expect(mockInput).not.toHaveBeenCalled();
56
+ expect(mockPasswordCancellable).toHaveBeenCalledTimes(1);
57
+ expect(mockPasswordCancellable.mock.calls[0][0].message).toBe("Enter secret");
58
+ expect(mockInputCancellable).not.toHaveBeenCalled();
46
59
  });
47
60
 
48
61
  test("uses input() for non-sensitive string fields", async () => {
@@ -51,19 +64,19 @@ describe("Encryption & Sensitive Fields", () => {
51
64
  });
52
65
  const sprinkle = new Sprinkle(schema, tmpDir);
53
66
 
54
- mockInput.mockResolvedValueOnce("visible");
67
+ mockInputCancellable.mockResolvedValueOnce("visible");
55
68
 
56
69
  const result = await sprinkle.FillInStruct(schema);
57
70
  expect(result).toEqual({ name: "visible" });
58
- expect(mockInput).toHaveBeenCalledTimes(1);
59
- expect(mockPassword).not.toHaveBeenCalled();
71
+ expect(mockInputCancellable).toHaveBeenCalledTimes(1);
72
+ expect(mockPasswordCancellable).not.toHaveBeenCalled();
60
73
  });
61
74
 
62
75
  test("does not remember sensitive field as default", async () => {
63
76
  const schema = Type.String({ sensitive: true });
64
77
  const sprinkle = new Sprinkle(Type.Object({ p: Type.String() }), tmpDir);
65
78
 
66
- mockPassword.mockResolvedValueOnce("secret-val");
79
+ mockPasswordCancellable.mockResolvedValueOnce("secret-val");
67
80
 
68
81
  await sprinkle.FillInStruct(schema);
69
82
  expect(sprinkle.defaults["string"]).toBeUndefined();
@@ -4,13 +4,25 @@ import { Sprinkle, Type, type IMenu } from "../index.js";
4
4
  const mockSelect = mock();
5
5
  const mockInput = mock();
6
6
  const mockPassword = mock();
7
- const mockSearch = mock();
7
+ const mockSelectCancellable = mock();
8
+ const mockInputCancellable = mock();
9
+ const mockPasswordCancellable = mock();
10
+ const mockConfirmCancellable = mock();
11
+ const mockSearchCancellable = mock();
8
12
 
9
13
  mock.module("@inquirer/prompts", () => ({
10
14
  select: mockSelect,
11
15
  input: mockInput,
12
16
  password: mockPassword,
13
- search: mockSearch,
17
+ }));
18
+
19
+ mock.module("../prompts.js", () => ({
20
+ selectCancellable: mockSelectCancellable,
21
+ inputCancellable: mockInputCancellable,
22
+ passwordCancellable: mockPasswordCancellable,
23
+ confirmCancellable: mockConfirmCancellable,
24
+ searchCancellable: mockSearchCancellable,
25
+ select: mockSelectCancellable,
14
26
  }));
15
27
 
16
28
  describe("beforeShow hook (2.2)", () => {
@@ -21,6 +33,7 @@ describe("beforeShow hook (2.2)", () => {
21
33
  sprinkle = new Sprinkle(schema, "/tmp/test");
22
34
  sprinkle.settings = { name: "test" } as any;
23
35
  mockSelect.mockClear();
36
+ mockSelectCancellable.mockClear();
24
37
  });
25
38
 
26
39
  test("calls beforeShow before rendering menu", async () => {
@@ -30,7 +43,7 @@ describe("beforeShow hook (2.2)", () => {
30
43
  callOrder.push("beforeShow");
31
44
  });
32
45
 
33
- mockSelect.mockImplementation(async () => {
46
+ mockSelectCancellable.mockImplementation(async () => {
34
47
  callOrder.push("select");
35
48
  return -1; // exit
36
49
  });
@@ -51,7 +64,7 @@ describe("beforeShow hook (2.2)", () => {
51
64
  test("passes sprinkle instance to beforeShow", async () => {
52
65
  let receivedSprinkle: any;
53
66
 
54
- mockSelect.mockResolvedValueOnce(-1);
67
+ mockSelectCancellable.mockResolvedValueOnce(-1);
55
68
 
56
69
  const menu: IMenu<any> = {
57
70
  title: "Test",
@@ -66,7 +79,7 @@ describe("beforeShow hook (2.2)", () => {
66
79
  });
67
80
 
68
81
  test("menu works without beforeShow", async () => {
69
- mockSelect.mockResolvedValueOnce(-1);
82
+ mockSelectCancellable.mockResolvedValueOnce(-1);
70
83
 
71
84
  const menu: IMenu<any> = {
72
85
  title: "Test",
@@ -80,11 +93,11 @@ describe("beforeShow hook (2.2)", () => {
80
93
 
81
94
  describe("SearchSelect (2.3)", () => {
82
95
  beforeEach(() => {
83
- mockSearch.mockClear();
96
+ mockSearchCancellable.mockClear();
84
97
  });
85
98
 
86
99
  test("delegates to search prompt", async () => {
87
- mockSearch.mockResolvedValueOnce("selected-value");
100
+ mockSearchCancellable.mockResolvedValueOnce("selected-value");
88
101
 
89
102
  const result = await Sprinkle.SearchSelect({
90
103
  message: "Pick one",
@@ -95,20 +108,31 @@ describe("SearchSelect (2.3)", () => {
95
108
  });
96
109
 
97
110
  expect(result).toBe("selected-value");
98
- expect(mockSearch).toHaveBeenCalledTimes(1);
99
- expect(mockSearch.mock.calls[0][0].message).toBe("Pick one");
111
+ expect(mockSearchCancellable).toHaveBeenCalledTimes(1);
112
+ expect(mockSearchCancellable.mock.calls[0][0].message).toBe("Pick one");
100
113
  });
101
114
 
102
115
  test("passes source function through", async () => {
103
116
  const sourceFn = mock(() => [{ name: "X", value: "x" }]);
104
- mockSearch.mockResolvedValueOnce("x");
117
+ mockSearchCancellable.mockResolvedValueOnce("x");
105
118
 
106
119
  await Sprinkle.SearchSelect({
107
120
  message: "Search",
108
121
  source: sourceFn,
109
122
  });
110
123
 
111
- expect(mockSearch.mock.calls[0][0].source).toBe(sourceFn);
124
+ expect(mockSearchCancellable.mock.calls[0][0].source).toBe(sourceFn);
125
+ });
126
+
127
+ test("returns null when cancelled", async () => {
128
+ mockSearchCancellable.mockResolvedValueOnce(null);
129
+
130
+ const result = await Sprinkle.SearchSelect({
131
+ message: "Pick one",
132
+ source: () => [{ name: "A", value: "a" }],
133
+ });
134
+
135
+ expect(result).toBeNull();
112
136
  });
113
137
  });
114
138
 
@@ -120,6 +144,8 @@ describe("Optional type support (2.4)", () => {
120
144
  sprinkle = new Sprinkle(schema, "/tmp/test");
121
145
  mockSelect.mockClear();
122
146
  mockInput.mockClear();
147
+ mockSelectCancellable.mockClear();
148
+ mockInputCancellable.mockClear();
123
149
  });
124
150
 
125
151
  test("skips optional field when user selects Skip", async () => {
@@ -128,8 +154,8 @@ describe("Optional type support (2.4)", () => {
128
154
  nickname: Type.Optional(Type.String()),
129
155
  });
130
156
 
131
- mockInput.mockResolvedValueOnce("Alice"); // name
132
- mockSelect.mockResolvedValueOnce(false); // skip nickname
157
+ mockInputCancellable.mockResolvedValueOnce("Alice"); // name
158
+ mockSelectCancellable.mockResolvedValueOnce(false); // skip nickname
133
159
 
134
160
  const result = await sprinkle.FillInStruct(schema);
135
161
  expect(result.name).toBe("Alice");
@@ -142,10 +168,10 @@ describe("Optional type support (2.4)", () => {
142
168
  nickname: Type.Optional(Type.String()),
143
169
  });
144
170
 
145
- mockInput
171
+ mockInputCancellable
146
172
  .mockResolvedValueOnce("Alice") // name
147
173
  .mockResolvedValueOnce("Ali"); // nickname
148
- mockSelect.mockResolvedValueOnce(true); // fill nickname
174
+ mockSelectCancellable.mockResolvedValueOnce(true); // fill nickname
149
175
 
150
176
  const result = await sprinkle.FillInStruct(schema);
151
177
  expect(result.name).toBe("Alice");
@@ -1,11 +1,16 @@
1
1
  import { describe, expect, test, mock, beforeEach, spyOn } from "bun:test";
2
2
  import { Sprinkle, Type, WalletSettingsSchema } from "../index.js";
3
+ import { UserCancelledError } from "../types.js";
3
4
 
4
5
  // Mock @inquirer/prompts
5
6
  const mockSelect = mock();
6
7
  const mockInput = mock();
7
8
  const mockPassword = mock();
8
9
  const mockConfirm = mock();
10
+ const mockSelectCancellable = mock();
11
+ const mockInputCancellable = mock();
12
+ const mockPasswordCancellable = mock();
13
+ const mockConfirmCancellable = mock();
9
14
 
10
15
  mock.module("@inquirer/prompts", () => ({
11
16
  select: mockSelect,
@@ -14,6 +19,13 @@ mock.module("@inquirer/prompts", () => ({
14
19
  confirm: mockConfirm,
15
20
  }));
16
21
 
22
+ mock.module("../prompts.js", () => ({
23
+ selectCancellable: mockSelectCancellable,
24
+ inputCancellable: mockInputCancellable,
25
+ passwordCancellable: mockPasswordCancellable,
26
+ confirmCancellable: mockConfirmCancellable,
27
+ }));
28
+
17
29
  describe("FillInStruct", () => {
18
30
  let sprinkle: Sprinkle<any>;
19
31
 
@@ -22,24 +34,30 @@ describe("FillInStruct", () => {
22
34
  sprinkle = new Sprinkle(schema, "/tmp/test");
23
35
  mockSelect.mockClear();
24
36
  mockInput.mockClear();
37
+ mockPassword.mockClear();
38
+ mockConfirm.mockClear();
39
+ mockSelectCancellable.mockClear();
40
+ mockInputCancellable.mockClear();
41
+ mockPasswordCancellable.mockClear();
42
+ mockConfirmCancellable.mockClear();
25
43
  });
26
44
 
27
45
  test("fills a simple string field", async () => {
28
- mockInput.mockResolvedValueOnce("hello");
46
+ mockInputCancellable.mockResolvedValueOnce("hello");
29
47
 
30
48
  const result = await sprinkle.FillInStruct(Type.String());
31
49
  expect(result).toBe("hello");
32
50
  });
33
51
 
34
52
  test("fills a string with title as prompt", async () => {
35
- mockInput.mockResolvedValueOnce("world");
53
+ mockInputCancellable.mockResolvedValueOnce("world");
36
54
 
37
55
  await sprinkle.FillInStruct(Type.String({ title: "Enter name" }));
38
- expect(mockInput.mock.calls[0][0].message).toBe("Enter name");
56
+ expect(mockInputCancellable.mock.calls[0][0].message).toBe("Enter name");
39
57
  });
40
58
 
41
59
  test("fills a bigint field", async () => {
42
- mockInput.mockResolvedValueOnce("42");
60
+ mockInputCancellable.mockResolvedValueOnce("42");
43
61
 
44
62
  const result = await sprinkle.FillInStruct(Type.BigInt());
45
63
  expect(result).toBe(42n);
@@ -48,8 +66,8 @@ describe("FillInStruct", () => {
48
66
  test("fills a literal field without prompting", async () => {
49
67
  const result = await sprinkle.FillInStruct(Type.Literal("fixed"));
50
68
  expect(result).toBe("fixed");
51
- expect(mockInput).not.toHaveBeenCalled();
52
- expect(mockSelect).not.toHaveBeenCalled();
69
+ expect(mockInputCancellable).not.toHaveBeenCalled();
70
+ expect(mockSelectCancellable).not.toHaveBeenCalled();
53
71
  });
54
72
 
55
73
  test("fills an object with multiple fields", async () => {
@@ -58,7 +76,7 @@ describe("FillInStruct", () => {
58
76
  age: Type.BigInt(),
59
77
  });
60
78
 
61
- mockInput
79
+ mockInputCancellable
62
80
  .mockResolvedValueOnce("Alice")
63
81
  .mockResolvedValueOnce("30");
64
82
 
@@ -79,10 +97,10 @@ describe("FillInStruct", () => {
79
97
  ]);
80
98
 
81
99
  // Select first variant (the Object with type "a")
82
- mockSelect.mockImplementationOnce(async (opts: any) => {
100
+ mockSelectCancellable.mockImplementationOnce(async (opts: any) => {
83
101
  return opts.choices[0].value;
84
102
  });
85
- mockInput.mockResolvedValueOnce("test-value");
103
+ mockInputCancellable.mockResolvedValueOnce("test-value");
86
104
 
87
105
  const result = await sprinkle.FillInStruct(schema);
88
106
  expect(result).toEqual({ type: "a", value: "test-value" });
@@ -91,10 +109,10 @@ describe("FillInStruct", () => {
91
109
  test("fills an array with items", async () => {
92
110
  const schema = Type.Array(Type.String());
93
111
 
94
- mockInput.mockResolvedValueOnce("first");
95
- mockSelect.mockResolvedValueOnce(true); // add another
96
- mockInput.mockResolvedValueOnce("second");
97
- mockSelect.mockResolvedValueOnce(false); // stop
112
+ mockInputCancellable.mockResolvedValueOnce("first");
113
+ mockSelectCancellable.mockResolvedValueOnce(true); // add another
114
+ mockInputCancellable.mockResolvedValueOnce("second");
115
+ mockSelectCancellable.mockResolvedValueOnce(false); // stop
98
116
 
99
117
  const result = await sprinkle.FillInStruct(schema);
100
118
  expect(result).toEqual(["first", "second"]);
@@ -103,30 +121,30 @@ describe("FillInStruct", () => {
103
121
  test("fills an array with single item", async () => {
104
122
  const schema = Type.Array(Type.String());
105
123
 
106
- mockInput.mockResolvedValueOnce("only");
107
- mockSelect.mockResolvedValueOnce(false); // stop
124
+ mockInputCancellable.mockResolvedValueOnce("only");
125
+ mockSelectCancellable.mockResolvedValueOnce(false); // stop
108
126
 
109
127
  const result = await sprinkle.FillInStruct(schema);
110
128
  expect(result).toEqual(["only"]);
111
129
  });
112
130
 
113
131
  test("uses default value for string", async () => {
114
- mockInput.mockResolvedValueOnce("used-default");
132
+ mockInputCancellable.mockResolvedValueOnce("used-default");
115
133
 
116
134
  await sprinkle.FillInStruct(
117
135
  Type.String(),
118
136
  "my-default" as any,
119
137
  );
120
138
 
121
- expect(mockInput.mock.calls[0][0].default).toBe("my-default");
139
+ expect(mockInputCancellable.mock.calls[0][0].default).toBe("my-default");
122
140
  });
123
141
 
124
142
  test("uses default value for bigint", async () => {
125
- mockInput.mockResolvedValueOnce("99");
143
+ mockInputCancellable.mockResolvedValueOnce("99");
126
144
 
127
145
  await sprinkle.FillInStruct(Type.BigInt(), 99n as any);
128
146
 
129
- expect(mockInput.mock.calls[0][0].default).toBe("99");
147
+ expect(mockInputCancellable.mock.calls[0][0].default).toBe("99");
130
148
  });
131
149
 
132
150
  test("throws for unsupported types", async () => {
@@ -142,14 +160,14 @@ describe("FillInStruct", () => {
142
160
  }),
143
161
  });
144
162
 
145
- mockInput.mockResolvedValueOnce("deep-value");
163
+ mockInputCancellable.mockResolvedValueOnce("deep-value");
146
164
 
147
165
  const result = await sprinkle.FillInStruct(schema);
148
166
  expect(result).toEqual({ outer: { inner: "deep-value" } });
149
167
  });
150
168
 
151
169
  test("remembers last string input as default", async () => {
152
- mockInput
170
+ mockInputCancellable
153
171
  .mockResolvedValueOnce("first-input")
154
172
  .mockResolvedValueOnce("second-input");
155
173
 
@@ -158,13 +176,13 @@ describe("FillInStruct", () => {
158
176
 
159
177
  await sprinkle.FillInStruct(Type.String());
160
178
  // Second call should have the first input as default
161
- expect(mockInput.mock.calls[1][0].default).toBe("first-input");
179
+ expect(mockInputCancellable.mock.calls[1][0].default).toBe("first-input");
162
180
  });
163
181
 
164
182
  test("fills a tuple with same-type elements", async () => {
165
183
  const schema = Type.Tuple([Type.String(), Type.String()]);
166
184
 
167
- mockInput
185
+ mockInputCancellable
168
186
  .mockResolvedValueOnce("policyId")
169
187
  .mockResolvedValueOnce("assetName");
170
188
 
@@ -175,7 +193,7 @@ describe("FillInStruct", () => {
175
193
  test("fills a tuple with mixed types", async () => {
176
194
  const schema = Type.Tuple([Type.String(), Type.BigInt()]);
177
195
 
178
- mockInput
196
+ mockInputCancellable
179
197
  .mockResolvedValueOnce("label")
180
198
  .mockResolvedValueOnce("42");
181
199
 
@@ -186,14 +204,14 @@ describe("FillInStruct", () => {
186
204
  test("fills a tuple with default values", async () => {
187
205
  const schema = Type.Tuple([Type.String(), Type.BigInt()]);
188
206
 
189
- mockInput
207
+ mockInputCancellable
190
208
  .mockResolvedValueOnce("new-label")
191
209
  .mockResolvedValueOnce("99");
192
210
 
193
211
  await sprinkle.FillInStruct(schema, ["default-label", 50n] as any);
194
212
 
195
- expect(mockInput.mock.calls[0][0].default).toBe("default-label");
196
- expect(mockInput.mock.calls[1][0].default).toBe("50");
213
+ expect(mockInputCancellable.mock.calls[0][0].default).toBe("default-label");
214
+ expect(mockInputCancellable.mock.calls[1][0].default).toBe("50");
197
215
  });
198
216
 
199
217
  test("fills a tuple nested in an object", async () => {
@@ -201,7 +219,7 @@ describe("FillInStruct", () => {
201
219
  asset: Type.Tuple([Type.String(), Type.String()]),
202
220
  });
203
221
 
204
- mockInput
222
+ mockInputCancellable
205
223
  .mockResolvedValueOnce("policy123")
206
224
  .mockResolvedValueOnce("token456");
207
225
 
@@ -213,14 +231,14 @@ describe("FillInStruct", () => {
213
231
  const schema = Type.String({ title: "Hot Wallet Private Key" });
214
232
 
215
233
  // Select "existing" option
216
- mockSelect.mockResolvedValueOnce("existing");
217
- mockPassword.mockResolvedValueOnce("my-private-key");
234
+ mockSelectCancellable.mockResolvedValueOnce("existing");
235
+ mockPasswordCancellable.mockResolvedValueOnce("my-private-key");
218
236
 
219
237
  const result = await sprinkle.FillInStruct(schema);
220
238
 
221
239
  // Verify select was called with correct options
222
- expect(mockSelect.mock.calls[0][0].message).toBe("Hot wallet setup:");
223
- expect(mockSelect.mock.calls[0][0].choices).toEqual([
240
+ expect(mockSelectCancellable.mock.calls[0][0].message).toBe("Hot wallet setup:");
241
+ expect(mockSelectCancellable.mock.calls[0][0].choices).toEqual([
224
242
  { name: "Enter existing private key", value: "existing" },
225
243
  { name: "Generate new wallet", value: "generate" },
226
244
  ]);
@@ -230,12 +248,12 @@ describe("FillInStruct", () => {
230
248
  test("hot wallet existing key prompts for password", async () => {
231
249
  const schema = Type.String({ title: "Hot Wallet Private Key" });
232
250
 
233
- mockSelect.mockResolvedValueOnce("existing");
234
- mockPassword.mockResolvedValueOnce("deadbeef1234");
251
+ mockSelectCancellable.mockResolvedValueOnce("existing");
252
+ mockPasswordCancellable.mockResolvedValueOnce("deadbeef1234");
235
253
 
236
254
  const result = await sprinkle.FillInStruct(schema);
237
255
 
238
- expect(mockPassword).toHaveBeenCalledWith({
256
+ expect(mockPasswordCancellable).toHaveBeenCalledWith({
239
257
  message: "Enter your private key:",
240
258
  });
241
259
  expect(result).toBe("deadbeef1234");
@@ -243,12 +261,12 @@ describe("FillInStruct", () => {
243
261
 
244
262
  test("full wallet settings schema with existing key", async () => {
245
263
  // Select "hot" variant
246
- mockSelect.mockImplementationOnce(async (opts: any) => {
264
+ mockSelectCancellable.mockImplementationOnce(async (opts: any) => {
247
265
  return opts.choices[0].value; // hot wallet object
248
266
  });
249
267
  // Select "existing" key option
250
- mockSelect.mockResolvedValueOnce("existing");
251
- mockPassword.mockResolvedValueOnce("abc123privatekey");
268
+ mockSelectCancellable.mockResolvedValueOnce("existing");
269
+ mockPasswordCancellable.mockResolvedValueOnce("abc123privatekey");
252
270
 
253
271
  const result = await sprinkle.FillInStruct(WalletSettingsSchema);
254
272
 
@@ -257,4 +275,52 @@ describe("FillInStruct", () => {
257
275
  privateKey: "abc123privatekey",
258
276
  });
259
277
  });
278
+
279
+ test("throws UserCancelledError when input prompt is cancelled", async () => {
280
+ mockInputCancellable.mockResolvedValueOnce(null);
281
+
282
+ await expect(sprinkle.FillInStruct(Type.String())).rejects.toThrow(
283
+ UserCancelledError,
284
+ );
285
+ });
286
+
287
+ test("throws UserCancelledError when select prompt is cancelled", async () => {
288
+ const schema = Type.Union([
289
+ Type.Object({ type: Type.Literal("a") }),
290
+ Type.Object({ type: Type.Literal("b") }),
291
+ ]);
292
+
293
+ mockSelectCancellable.mockResolvedValueOnce(null);
294
+
295
+ await expect(sprinkle.FillInStruct(schema)).rejects.toThrow(
296
+ UserCancelledError,
297
+ );
298
+ });
299
+
300
+ test("throws UserCancelledError when nested prompt is cancelled", async () => {
301
+ const schema = Type.Object({
302
+ name: Type.String(),
303
+ age: Type.BigInt(),
304
+ });
305
+
306
+ // First field succeeds, second is cancelled
307
+ mockInputCancellable
308
+ .mockResolvedValueOnce("Alice")
309
+ .mockResolvedValueOnce(null);
310
+
311
+ await expect(sprinkle.FillInStruct(schema)).rejects.toThrow(
312
+ UserCancelledError,
313
+ );
314
+ });
315
+
316
+ test("throws UserCancelledError when array add-another prompt is cancelled", async () => {
317
+ const schema = Type.Array(Type.String());
318
+
319
+ mockInputCancellable.mockResolvedValueOnce("first");
320
+ mockSelectCancellable.mockResolvedValueOnce(null); // cancel on "add another?"
321
+
322
+ await expect(sprinkle.FillInStruct(schema)).rejects.toThrow(
323
+ UserCancelledError,
324
+ );
325
+ });
260
326
  });
@@ -178,4 +178,112 @@ describe("Settings Persistence", () => {
178
178
  expect(sprinkle.defaults).toEqual({ string: "old" });
179
179
  expect(sprinkle.profileMeta.name).toBe("Default");
180
180
  });
181
+
182
+ describe("currentProfile", () => {
183
+ test("returns null when no profile loaded", () => {
184
+ const schema = Type.Object({ name: Type.String() });
185
+ const sprinkle = new Sprinkle(schema, tmpDir);
186
+
187
+ expect(sprinkle.currentProfile).toBeNull();
188
+ });
189
+
190
+ test("returns profile info when loaded", () => {
191
+ const schema = Type.Object({ name: Type.String() });
192
+ const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
193
+
194
+ const profile = sprinkle.currentProfile;
195
+ expect(profile).not.toBeNull();
196
+ expect(profile!.id).toBe("test");
197
+ expect(profile!.name).toBe("Test");
198
+ expect(profile!.createdAt).toBeDefined();
199
+ expect(profile!.updatedAt).toBeDefined();
200
+ });
201
+
202
+ test("includes description when present", () => {
203
+ const schema = Type.Object({ name: Type.String() });
204
+ const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
205
+ sprinkle.profileMeta.description = "Test profile description";
206
+
207
+ const profile = sprinkle.currentProfile;
208
+ expect(profile!.description).toBe("Test profile description");
209
+ });
210
+
211
+ test("has undefined description when not set", () => {
212
+ const schema = Type.Object({ name: Type.String() });
213
+ const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
214
+ delete sprinkle.profileMeta.description;
215
+
216
+ const profile = sprinkle.currentProfile;
217
+ expect(profile!.description).toBeUndefined();
218
+ });
219
+ });
220
+
221
+ describe("getDisplaySettings", () => {
222
+ test("masks sensitive fields", () => {
223
+ const schema = Type.Object({
224
+ name: Type.String(),
225
+ secret: Type.String({ sensitive: true }),
226
+ });
227
+ const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
228
+ sprinkle.settings = { name: "visible", secret: "hidden" } as any;
229
+
230
+ const display = sprinkle.getDisplaySettings();
231
+ expect(display.name).toBe("visible");
232
+ expect(display.secret).toBe("********");
233
+ });
234
+
235
+ test("preserves non-sensitive fields", () => {
236
+ const schema = Type.Object({
237
+ name: Type.String(),
238
+ count: Type.Number(),
239
+ });
240
+ const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
241
+ sprinkle.settings = { name: "test", count: 42 } as any;
242
+
243
+ const display = sprinkle.getDisplaySettings();
244
+ expect(display.name).toBe("test");
245
+ expect(display.count).toBe(42);
246
+ });
247
+
248
+ test("preserves BigInt values", () => {
249
+ const schema = Type.Object({
250
+ amount: Type.BigInt(),
251
+ secret: Type.String({ sensitive: true }),
252
+ });
253
+ const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
254
+ sprinkle.settings = { amount: 42n, secret: "hidden" } as any;
255
+
256
+ const display = sprinkle.getDisplaySettings();
257
+ expect(display.amount).toBe(42n);
258
+ expect(display.secret).toBe("********");
259
+ });
260
+
261
+ test("masks nested sensitive fields", () => {
262
+ const schema = Type.Object({
263
+ wallet: Type.Object({
264
+ key: Type.String({ sensitive: true }),
265
+ address: Type.String(),
266
+ }),
267
+ });
268
+ const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
269
+ sprinkle.settings = {
270
+ wallet: { key: "secret", address: "addr1..." },
271
+ } as any;
272
+
273
+ const display = sprinkle.getDisplaySettings();
274
+ expect(display.wallet.key).toBe("********");
275
+ expect(display.wallet.address).toBe("addr1...");
276
+ });
277
+
278
+ test("does not mask empty sensitive fields", () => {
279
+ const schema = Type.Object({
280
+ secret: Type.String({ sensitive: true }),
281
+ });
282
+ const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
283
+ sprinkle.settings = { secret: "" } as any;
284
+
285
+ const display = sprinkle.getDisplaySettings();
286
+ expect(display.secret).toBe("");
287
+ });
288
+ });
181
289
  });