@ollie-shop/cli 0.1.3 → 0.3.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 (181) hide show
  1. package/.turbo/turbo-build.log +2 -11
  2. package/CHANGELOG.md +17 -5
  3. package/CLAUDE_CLI.md +265 -0
  4. package/README.md +704 -8
  5. package/__tests__/mocks/console.ts +22 -0
  6. package/__tests__/mocks/core.ts +137 -0
  7. package/__tests__/mocks/index.ts +4 -0
  8. package/__tests__/mocks/inquirer.ts +16 -0
  9. package/__tests__/mocks/progress.ts +19 -0
  10. package/dist/__tests__/helpers/cli-test-helper.d.ts +89 -0
  11. package/dist/__tests__/helpers/cli-test-helper.d.ts.map +1 -0
  12. package/dist/__tests__/helpers/cli-test-helper.js +220 -0
  13. package/dist/__tests__/mocks/index.d.ts +69 -0
  14. package/dist/__tests__/mocks/index.d.ts.map +1 -0
  15. package/dist/__tests__/mocks/index.js +77 -0
  16. package/dist/actions/component.actions.d.ts +14 -0
  17. package/dist/actions/component.actions.d.ts.map +1 -0
  18. package/dist/actions/component.actions.js +273 -0
  19. package/dist/actions/function.actions.d.ts +15 -0
  20. package/dist/actions/function.actions.d.ts.map +1 -0
  21. package/dist/actions/function.actions.js +254 -0
  22. package/dist/actions/project.actions.d.ts +17 -0
  23. package/dist/actions/project.actions.d.ts.map +1 -0
  24. package/dist/actions/project.actions.js +97 -0
  25. package/dist/actions/version.actions.d.ts +19 -0
  26. package/dist/actions/version.actions.d.ts.map +1 -0
  27. package/dist/actions/version.actions.js +216 -0
  28. package/dist/commands/component.d.ts +3 -0
  29. package/dist/commands/component.d.ts.map +1 -0
  30. package/dist/commands/component.js +192 -0
  31. package/dist/commands/docs.d.ts +3 -0
  32. package/dist/commands/docs.d.ts.map +1 -0
  33. package/dist/commands/docs.js +16 -0
  34. package/dist/commands/function.d.ts +3 -0
  35. package/dist/commands/function.d.ts.map +1 -0
  36. package/dist/commands/function.js +243 -0
  37. package/dist/commands/help.d.ts +3 -0
  38. package/dist/commands/help.d.ts.map +1 -0
  39. package/dist/commands/help.js +20 -0
  40. package/dist/commands/index.d.ts +3 -0
  41. package/dist/commands/index.d.ts.map +1 -0
  42. package/dist/commands/index.js +26 -0
  43. package/dist/commands/login.d.ts +3 -0
  44. package/dist/commands/login.d.ts.map +1 -0
  45. package/dist/commands/login.js +175 -0
  46. package/dist/commands/project.d.ts +3 -0
  47. package/dist/commands/project.d.ts.map +1 -0
  48. package/dist/commands/project.js +78 -0
  49. package/dist/commands/store-version.d.ts +3 -0
  50. package/dist/commands/store-version.d.ts.map +1 -0
  51. package/dist/commands/store-version.js +241 -0
  52. package/dist/commands/version.d.ts +3 -0
  53. package/dist/commands/version.d.ts.map +1 -0
  54. package/dist/commands/version.js +46 -0
  55. package/dist/commands/whoami.d.ts +3 -0
  56. package/dist/commands/whoami.d.ts.map +1 -0
  57. package/dist/commands/whoami.js +41 -0
  58. package/dist/index.d.ts +3 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +93 -226
  61. package/dist/prompts/component.prompts.d.ts +14 -0
  62. package/dist/prompts/component.prompts.d.ts.map +1 -0
  63. package/dist/prompts/component.prompts.js +75 -0
  64. package/dist/prompts/function.prompts.d.ts +21 -0
  65. package/dist/prompts/function.prompts.d.ts.map +1 -0
  66. package/dist/prompts/function.prompts.js +127 -0
  67. package/dist/schemas/command.schema.d.ts +516 -0
  68. package/dist/schemas/command.schema.d.ts.map +1 -0
  69. package/dist/schemas/command.schema.js +267 -0
  70. package/dist/types/index.d.ts +147 -0
  71. package/dist/types/index.d.ts.map +1 -0
  72. package/dist/types/index.js +18 -0
  73. package/dist/utils/auth.d.ts +4 -0
  74. package/dist/utils/auth.d.ts.map +1 -0
  75. package/dist/utils/auth.js +26 -0
  76. package/dist/utils/cli-progress-reporter.d.ts +12 -0
  77. package/dist/utils/cli-progress-reporter.d.ts.map +1 -0
  78. package/dist/utils/cli-progress-reporter.js +77 -0
  79. package/dist/utils/command-builder.d.ts +22 -0
  80. package/dist/utils/command-builder.d.ts.map +1 -0
  81. package/dist/utils/command-builder.js +268 -0
  82. package/dist/utils/command-helpers.d.ts +19 -0
  83. package/dist/utils/command-helpers.d.ts.map +1 -0
  84. package/dist/utils/command-helpers.js +79 -0
  85. package/dist/utils/command-parser.d.ts +146 -0
  86. package/dist/utils/command-parser.d.ts.map +1 -0
  87. package/dist/utils/command-parser.js +179 -0
  88. package/dist/utils/command-suggestions.d.ts +35 -0
  89. package/dist/utils/command-suggestions.d.ts.map +1 -0
  90. package/dist/utils/command-suggestions.js +152 -0
  91. package/dist/utils/console.d.ts +44 -0
  92. package/dist/utils/console.d.ts.map +1 -0
  93. package/dist/utils/console.js +233 -0
  94. package/dist/utils/constants.d.ts +8 -0
  95. package/dist/utils/constants.d.ts.map +1 -0
  96. package/dist/utils/constants.js +10 -0
  97. package/dist/utils/context-detector.d.ts +12 -0
  98. package/dist/utils/context-detector.d.ts.map +1 -0
  99. package/dist/utils/context-detector.js +155 -0
  100. package/dist/utils/enhanced-error-handler.d.ts +47 -0
  101. package/dist/utils/enhanced-error-handler.d.ts.map +1 -0
  102. package/dist/utils/enhanced-error-handler.js +221 -0
  103. package/dist/utils/error-handler.d.ts +3 -0
  104. package/dist/utils/error-handler.d.ts.map +1 -0
  105. package/dist/utils/error-handler.js +55 -0
  106. package/dist/utils/errors.d.ts +44 -0
  107. package/dist/utils/errors.d.ts.map +1 -0
  108. package/dist/utils/errors.js +76 -0
  109. package/dist/utils/interactive-builder.d.ts +22 -0
  110. package/dist/utils/interactive-builder.d.ts.map +1 -0
  111. package/dist/utils/interactive-builder.js +246 -0
  112. package/dist/utils/rich-progress.d.ts +59 -0
  113. package/dist/utils/rich-progress.d.ts.map +1 -0
  114. package/dist/utils/rich-progress.js +234 -0
  115. package/dist/utils/store.d.ts +11 -0
  116. package/dist/utils/store.d.ts.map +1 -0
  117. package/dist/utils/store.js +19 -0
  118. package/dist/utils/validation-error-formatter.d.ts +25 -0
  119. package/dist/utils/validation-error-formatter.d.ts.map +1 -0
  120. package/dist/utils/validation-error-formatter.js +258 -0
  121. package/dist/utils/validation-helpers.d.ts +60 -0
  122. package/dist/utils/validation-helpers.d.ts.map +1 -0
  123. package/dist/utils/validation-helpers.js +152 -0
  124. package/package.json +44 -9
  125. package/src/__tests__/helpers/cli-test-helper.ts +281 -0
  126. package/src/__tests__/mocks/index.ts +142 -0
  127. package/src/actions/component.actions.ts +334 -0
  128. package/src/actions/function.actions.ts +313 -0
  129. package/src/actions/project.actions.ts +126 -0
  130. package/src/actions/version.actions.ts +233 -0
  131. package/src/commands/__tests__/component-validation.test.ts +250 -0
  132. package/src/commands/__tests__/component.test.ts +321 -0
  133. package/src/commands/__tests__/function-validation.test.ts +220 -0
  134. package/src/commands/__tests__/function.test.ts +286 -0
  135. package/src/commands/__tests__/store-version-validation.test.ts +414 -0
  136. package/src/commands/__tests__/store-version.test.ts +405 -0
  137. package/src/commands/__tests__/version.test.ts +71 -0
  138. package/src/commands/component.ts +188 -0
  139. package/src/commands/docs.ts +24 -0
  140. package/src/commands/function.ts +252 -0
  141. package/src/commands/help.ts +18 -0
  142. package/src/commands/index.ts +21 -7
  143. package/src/commands/login.ts +19 -79
  144. package/src/commands/project.ts +107 -0
  145. package/src/commands/store-version.ts +242 -0
  146. package/src/commands/version.ts +51 -0
  147. package/src/commands/whoami.ts +46 -0
  148. package/src/index.ts +110 -15
  149. package/src/prompts/component.prompts.ts +94 -0
  150. package/src/prompts/function.prompts.ts +168 -0
  151. package/src/schemas/command.schema.ts +354 -0
  152. package/src/types/index.ts +183 -0
  153. package/src/utils/__tests__/command-parser.test.ts +159 -0
  154. package/src/utils/__tests__/command-suggestions.test.ts +185 -0
  155. package/src/utils/__tests__/console.test.ts +192 -0
  156. package/src/utils/__tests__/context-detector.test.ts +258 -0
  157. package/src/utils/__tests__/enhanced-error-handler.test.ts +137 -0
  158. package/src/utils/__tests__/error-handler.test.ts +107 -0
  159. package/src/utils/__tests__/rich-progress.test.ts +170 -0
  160. package/src/utils/__tests__/validation-error-formatter.test.ts +175 -0
  161. package/src/utils/__tests__/validation-helpers.test.ts +125 -0
  162. package/src/utils/auth.ts +41 -0
  163. package/src/utils/cli-progress-reporter.ts +84 -0
  164. package/src/utils/command-builder.ts +390 -0
  165. package/src/utils/command-helpers.ts +83 -0
  166. package/src/utils/command-parser.ts +250 -0
  167. package/src/utils/command-suggestions.ts +176 -0
  168. package/src/utils/console.ts +291 -0
  169. package/src/utils/context-detector.ts +177 -0
  170. package/src/utils/enhanced-error-handler.ts +264 -0
  171. package/src/utils/error-handler.ts +60 -0
  172. package/src/utils/errors.ts +125 -0
  173. package/src/utils/interactive-builder.ts +271 -0
  174. package/src/utils/rich-progress.ts +320 -0
  175. package/src/utils/store.ts +23 -0
  176. package/src/utils/validation-error-formatter.ts +337 -0
  177. package/src/utils/validation-helpers.ts +192 -0
  178. package/tsconfig.json +13 -7
  179. package/vitest.config.ts +28 -0
  180. package/vitest.setup.ts +29 -0
  181. package/tsup.config.ts +0 -15
@@ -0,0 +1,258 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { detectProjectContext, getContextualPath } from "../context-detector";
5
+
6
+ // Mock fs module
7
+ vi.mock("fs", () => ({
8
+ existsSync: vi.fn(),
9
+ readFileSync: vi.fn(),
10
+ statSync: vi.fn(),
11
+ }));
12
+
13
+ describe("Context Detector", () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ describe("detectProjectContext", () => {
19
+ it("should detect component directory by meta.json", () => {
20
+ const mockPath = "/project/components/header";
21
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
22
+ if (p === path.join(mockPath, "package.json")) return true;
23
+ if (p === path.join(mockPath, "meta.json")) return true;
24
+ return false;
25
+ });
26
+
27
+ vi.mocked(fs.readFileSync).mockReturnValue(
28
+ JSON.stringify({
29
+ type: "component",
30
+ name: "header",
31
+ }),
32
+ );
33
+
34
+ const result = detectProjectContext(mockPath);
35
+
36
+ expect(result.type).toBe("component");
37
+ expect(result.path).toBe(mockPath);
38
+ expect(result.name).toBe("header");
39
+ });
40
+
41
+ it("should detect component directory by index.tsx", () => {
42
+ const mockPath = "/project/components/footer";
43
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
44
+ if (p === path.join(mockPath, "package.json")) return true;
45
+ if (p === path.join(mockPath, "meta.json")) return true;
46
+ if (p === path.join(mockPath, "index.tsx")) return true;
47
+ return false;
48
+ });
49
+
50
+ vi.mocked(fs.readFileSync).mockReturnValue("{}");
51
+
52
+ const result = detectProjectContext(mockPath);
53
+
54
+ expect(result.type).toBe("component");
55
+ expect(result.path).toBe(mockPath);
56
+ expect(result.name).toBe("footer");
57
+ });
58
+
59
+ it("should detect function directory by meta.json", () => {
60
+ const mockPath = "/project/functions/validate-order";
61
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
62
+ if (p === path.join(mockPath, "package.json")) return true;
63
+ if (p === path.join(mockPath, "meta.json")) return true;
64
+ return false;
65
+ });
66
+
67
+ vi.mocked(fs.readFileSync).mockReturnValue(
68
+ JSON.stringify({
69
+ type: "function",
70
+ name: "validate-order",
71
+ }),
72
+ );
73
+
74
+ const result = detectProjectContext(mockPath);
75
+
76
+ expect(result.type).toBe("function");
77
+ expect(result.path).toBe(mockPath);
78
+ expect(result.name).toBe("validate-order");
79
+ });
80
+
81
+ it("should detect function directory by index.ts", () => {
82
+ const mockPath = "/project/functions/process-payment";
83
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
84
+ if (p === path.join(mockPath, "package.json")) return true;
85
+ if (p === path.join(mockPath, "meta.json")) return true;
86
+ if (p === path.join(mockPath, "index.ts")) return true;
87
+ return false;
88
+ });
89
+
90
+ vi.mocked(fs.readFileSync).mockReturnValue("{}");
91
+
92
+ const result = detectProjectContext(mockPath);
93
+
94
+ expect(result.type).toBe("function");
95
+ expect(result.path).toBe(mockPath);
96
+ expect(result.name).toBe("process-payment");
97
+ });
98
+
99
+ it("should detect project root with components directory", () => {
100
+ const mockPath = "/project";
101
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
102
+ if (p === path.join(mockPath, "components")) return true;
103
+ return false;
104
+ });
105
+
106
+ vi.mocked(fs.statSync).mockReturnValue({
107
+ isDirectory: () => true,
108
+ } as fs.Stats);
109
+
110
+ const result = detectProjectContext(mockPath);
111
+
112
+ expect(result.type).toBe("unknown");
113
+ expect(result.path).toBe(mockPath);
114
+ expect(result.parentPath).toBe(mockPath);
115
+ });
116
+
117
+ it("should detect when inside components/name directory structure", () => {
118
+ const mockPath = "/project/components/my-component";
119
+ vi.mocked(fs.existsSync).mockReturnValue(false);
120
+
121
+ const result = detectProjectContext(mockPath);
122
+
123
+ expect(result.type).toBe("component");
124
+ expect(result.path).toBe(mockPath);
125
+ expect(result.name).toBe("my-component");
126
+ });
127
+
128
+ it("should detect when inside functions/name directory structure", () => {
129
+ const mockPath = "/project/functions/my-function";
130
+ vi.mocked(fs.existsSync).mockReturnValue(false);
131
+
132
+ const result = detectProjectContext(mockPath);
133
+
134
+ expect(result.type).toBe("function");
135
+ expect(result.path).toBe(mockPath);
136
+ expect(result.name).toBe("my-function");
137
+ });
138
+
139
+ it("should return unknown for unrecognized directory", () => {
140
+ const mockPath = "/some/random/path";
141
+ vi.mocked(fs.existsSync).mockReturnValue(false);
142
+
143
+ const result = detectProjectContext(mockPath);
144
+
145
+ expect(result.type).toBe("unknown");
146
+ expect(result.path).toBe(mockPath);
147
+ expect(result.name).toBeUndefined();
148
+ });
149
+
150
+ it("should handle invalid meta.json gracefully", () => {
151
+ const mockPath = "/project/components/broken";
152
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
153
+ if (p === path.join(mockPath, "package.json")) return true;
154
+ if (p === path.join(mockPath, "meta.json")) return true;
155
+ if (p === path.join(mockPath, "index.tsx")) return true;
156
+ return false;
157
+ });
158
+
159
+ vi.mocked(fs.readFileSync).mockReturnValue("invalid json");
160
+
161
+ const result = detectProjectContext(mockPath);
162
+
163
+ // Should fall back to file detection
164
+ expect(result.type).toBe("component");
165
+ expect(result.path).toBe(mockPath);
166
+ });
167
+ });
168
+
169
+ describe("getContextualPath", () => {
170
+ it("should return explicit path when provided", () => {
171
+ const explicitPath = "/explicit/path";
172
+ const result = getContextualPath(explicitPath, "component");
173
+
174
+ expect(result).toBe(explicitPath);
175
+ });
176
+
177
+ it("should return current directory when in matching context", () => {
178
+ const mockPath = "/project/components/header";
179
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
180
+ if (p === path.join(mockPath, "package.json")) return true;
181
+ if (p === path.join(mockPath, "meta.json")) return true;
182
+ if (p === path.join(mockPath, "index.tsx")) return true;
183
+ return false;
184
+ });
185
+
186
+ vi.mocked(fs.readFileSync).mockReturnValue("{}");
187
+
188
+ // Mock process.cwd to return our test path
189
+ const originalCwd = process.cwd;
190
+ process.cwd = vi.fn().mockReturnValue(mockPath);
191
+
192
+ const result = getContextualPath(undefined, "component");
193
+
194
+ expect(result).toBe(mockPath);
195
+
196
+ process.cwd = originalCwd;
197
+ });
198
+
199
+ it("should throw error when in parent directory", () => {
200
+ const mockPath = "/project";
201
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
202
+ if (p === path.join(mockPath, "components")) return true;
203
+ return false;
204
+ });
205
+
206
+ vi.mocked(fs.statSync).mockReturnValue({
207
+ isDirectory: () => true,
208
+ } as fs.Stats);
209
+
210
+ // Mock process.cwd to return our test path
211
+ const originalCwd = process.cwd;
212
+ process.cwd = vi.fn().mockReturnValue(mockPath);
213
+
214
+ expect(() => getContextualPath(undefined, "component")).toThrowError(
215
+ /No component found in current directory/,
216
+ );
217
+
218
+ process.cwd = originalCwd;
219
+ });
220
+
221
+ it("should throw error when in wrong context type", () => {
222
+ const mockPath = "/project/functions/my-function";
223
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
224
+ if (p === path.join(mockPath, "package.json")) return true;
225
+ if (p === path.join(mockPath, "meta.json")) return true;
226
+ if (p === path.join(mockPath, "index.ts")) return true;
227
+ return false;
228
+ });
229
+
230
+ vi.mocked(fs.readFileSync).mockReturnValue("{}");
231
+
232
+ // Mock process.cwd to return our test path
233
+ const originalCwd = process.cwd;
234
+ process.cwd = vi.fn().mockReturnValue(mockPath);
235
+
236
+ expect(() => getContextualPath(undefined, "component")).toThrowError(
237
+ /Current directory appears to be a function, not a component/,
238
+ );
239
+
240
+ process.cwd = originalCwd;
241
+ });
242
+
243
+ it("should return current directory for unknown context", () => {
244
+ const mockPath = "/some/random/path";
245
+ vi.mocked(fs.existsSync).mockReturnValue(false);
246
+
247
+ // Mock process.cwd to return our test path
248
+ const originalCwd = process.cwd;
249
+ process.cwd = vi.fn().mockReturnValue(mockPath);
250
+
251
+ const result = getContextualPath(undefined, "component");
252
+
253
+ expect(result).toBe(mockPath);
254
+
255
+ process.cwd = originalCwd;
256
+ });
257
+ });
258
+ });
@@ -0,0 +1,137 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { CliConsole } from "../console";
3
+ import {
4
+ EnhancedErrorHandler,
5
+ createContextualError,
6
+ } from "../enhanced-error-handler";
7
+
8
+ describe("EnhancedErrorHandler", () => {
9
+ let mockConsole: CliConsole;
10
+ let handler: EnhancedErrorHandler;
11
+
12
+ beforeEach(() => {
13
+ // Create a mock console
14
+ mockConsole = {
15
+ log: vi.fn(),
16
+ error: vi.fn(),
17
+ warn: vi.fn(),
18
+ info: vi.fn(),
19
+ success: vi.fn(),
20
+ dim: vi.fn(),
21
+ bold: vi.fn(),
22
+ list: vi.fn(),
23
+ table: vi.fn(),
24
+ json: vi.fn(),
25
+ spinner: vi.fn(),
26
+ confirm: vi.fn(),
27
+ prompt: vi.fn(),
28
+ setOptions: vi.fn(),
29
+ group: vi.fn(),
30
+ groupEnd: vi.fn(),
31
+ separator: vi.fn(),
32
+ suggestions: vi.fn(),
33
+ debug: vi.fn(),
34
+ nextSteps: vi.fn(),
35
+ };
36
+ handler = new EnhancedErrorHandler(mockConsole);
37
+ vi.clearAllMocks();
38
+ });
39
+
40
+ describe("handle", () => {
41
+ it("should display basic error message", () => {
42
+ const error = new Error("Something went wrong");
43
+ handler.handle(error);
44
+
45
+ expect(mockConsole.error).toHaveBeenCalledWith(
46
+ "\n❌ Something went wrong\n",
47
+ );
48
+ });
49
+
50
+ it("should show recovery suggestions for ENOENT errors", () => {
51
+ const error = createContextualError("File not found", "ENOENT");
52
+ handler.handle(error);
53
+
54
+ expect(mockConsole.warn).toHaveBeenCalledWith("💡 Possible solutions:\n");
55
+ expect(mockConsole.log).toHaveBeenCalledWith(
56
+ expect.stringContaining("Check if the path exists"),
57
+ );
58
+ expect(mockConsole.log).toHaveBeenCalledWith(
59
+ expect.stringContaining("--path"),
60
+ );
61
+ });
62
+
63
+ it("should show recovery suggestions for ECONNREFUSED errors", () => {
64
+ const error = createContextualError("Connection failed", "ECONNREFUSED");
65
+ handler.handle(error);
66
+
67
+ expect(mockConsole.warn).toHaveBeenCalledWith("💡 Possible solutions:\n");
68
+ expect(mockConsole.log).toHaveBeenCalledWith(
69
+ expect.stringContaining("Check your internet connection"),
70
+ );
71
+ expect(mockConsole.log).toHaveBeenCalledWith(
72
+ expect.stringContaining("ollieshop login"),
73
+ );
74
+ });
75
+
76
+ it("should show recovery suggestions for component exists errors", () => {
77
+ const error = new Error("Component header-nav already exists");
78
+ handler.handle(error, { name: "header-nav" });
79
+
80
+ expect(mockConsole.warn).toHaveBeenCalledWith("💡 Possible solutions:\n");
81
+ expect(mockConsole.log).toHaveBeenCalledWith(
82
+ expect.stringContaining("Use a different name"),
83
+ );
84
+ });
85
+
86
+ it("should display contextual information", () => {
87
+ const error = new Error("Test error");
88
+ handler.handle(error, { path: "/test/path" });
89
+
90
+ expect(mockConsole.log).toHaveBeenCalledWith(
91
+ expect.stringContaining("📍 Context:"),
92
+ );
93
+ expect(mockConsole.log).toHaveBeenCalledWith(
94
+ expect.stringContaining("Current directory:"),
95
+ );
96
+ expect(mockConsole.log).toHaveBeenCalledWith(
97
+ expect.stringContaining("Target path: /test/path"),
98
+ );
99
+ });
100
+
101
+ it("should suggest similar commands for command errors", () => {
102
+ const error = createContextualError(
103
+ "Unknown command",
104
+ "COMMAND_NOT_FOUND",
105
+ {
106
+ attemptedCommand: "component crate",
107
+ },
108
+ );
109
+ handler.handle(error, { attemptedCommand: "component crate" });
110
+
111
+ expect(mockConsole.log).toHaveBeenCalledWith(
112
+ expect.stringContaining("💡 Did you mean:"),
113
+ );
114
+ expect(mockConsole.log).toHaveBeenCalledWith(
115
+ expect.stringContaining("component create"),
116
+ );
117
+ });
118
+ });
119
+
120
+ describe("createContextualError", () => {
121
+ it("should create error with code", () => {
122
+ const error = createContextualError("Test error", "TEST_CODE");
123
+
124
+ expect(error.message).toBe("Test error");
125
+ expect((error as Error & { code?: string }).code).toBe("TEST_CODE");
126
+ });
127
+
128
+ it("should create error with context", () => {
129
+ const context = { foo: "bar" };
130
+ const error = createContextualError("Test error", undefined, context);
131
+
132
+ expect(
133
+ (error as Error & { context?: Record<string, unknown> }).context,
134
+ ).toEqual(context);
135
+ });
136
+ });
137
+ });
@@ -0,0 +1,107 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { console as cliConsole } from "../console";
3
+ import { handleError } from "../error-handler";
4
+ import { CommandError, ValidationError } from "../errors";
5
+
6
+ // Mock console with all required methods
7
+ vi.mock("../console", () => ({
8
+ console: {
9
+ log: vi.fn(),
10
+ error: vi.fn(),
11
+ warn: vi.fn(),
12
+ info: vi.fn(),
13
+ success: vi.fn(),
14
+ dim: vi.fn(),
15
+ bold: vi.fn(),
16
+ list: vi.fn(),
17
+ table: vi.fn(),
18
+ json: vi.fn(),
19
+ spinner: vi.fn(),
20
+ confirm: vi.fn(),
21
+ prompt: vi.fn(),
22
+ setOptions: vi.fn(),
23
+ group: vi.fn(),
24
+ groupEnd: vi.fn(),
25
+ separator: vi.fn(),
26
+ suggestions: vi.fn(),
27
+ debug: vi.fn(),
28
+ nextSteps: vi.fn(),
29
+ },
30
+ }));
31
+
32
+ // Mock process.exit to follow CLI testing best practices
33
+ const _mockExit = vi.spyOn(process, "exit").mockImplementation((code) => {
34
+ throw new Error(`process.exit(${code})`);
35
+ });
36
+
37
+ describe("Error Handler", () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks();
40
+ process.env.DEBUG = undefined;
41
+ });
42
+
43
+ it("should handle CommandError with specific message", () => {
44
+ // Arrange
45
+ const error = new CommandError("Command failed", "COMMAND_FAILED");
46
+
47
+ // Act & Assert
48
+ expect(() => handleError(error)).toThrow(/process\.exit.*1/);
49
+ expect(cliConsole.error).toHaveBeenCalledWith("\nCommand failed");
50
+ });
51
+
52
+ it("should handle ValidationError with details", () => {
53
+ // Arrange
54
+ const error = new ValidationError("Validation failed", [
55
+ { field: "name", message: "Name is required" },
56
+ { field: "version", message: "Invalid version format" },
57
+ ]);
58
+
59
+ // Act & Assert
60
+ expect(() => handleError(error)).toThrow(/process\.exit.*1/);
61
+ expect(cliConsole.error).toHaveBeenCalledWith("\nValidation failed");
62
+ expect(cliConsole.dim).toHaveBeenCalledWith("\nContext:");
63
+ expect(cliConsole.dim).toHaveBeenCalledWith(" fields:");
64
+ expect(cliConsole.dim).toHaveBeenCalledWith(" name: Name is required");
65
+ expect(cliConsole.dim).toHaveBeenCalledWith(
66
+ " version: Invalid version format",
67
+ );
68
+ });
69
+
70
+ it("should handle generic Error", () => {
71
+ // Arrange
72
+ const error = new Error("Something went wrong");
73
+
74
+ // Act & Assert
75
+ expect(() => handleError(error)).toThrow(/process\.exit.*1/);
76
+ // Enhanced error handler formats with emoji
77
+ expect(cliConsole.error).toHaveBeenCalledWith(
78
+ "\n❌ Something went wrong\n",
79
+ );
80
+ });
81
+
82
+ it("should handle non-Error objects", () => {
83
+ // Arrange
84
+ const error = "String error";
85
+
86
+ // Act & Assert
87
+ expect(() => handleError(error)).toThrow(/process\.exit.*1/);
88
+ expect(cliConsole.error).toHaveBeenCalledWith(
89
+ "\nAn unexpected error occurred",
90
+ );
91
+ expect(cliConsole.dim).toHaveBeenCalledWith("String error");
92
+ });
93
+
94
+ it("should show debug information when NODE_ENV is development", () => {
95
+ // Arrange
96
+ process.env.DEBUG = "1";
97
+ const error = new Error("Debug error");
98
+ error.stack = "Error: Debug error\n at test.js:1:1";
99
+
100
+ // Act & Assert
101
+ expect(() => handleError(error)).toThrow(/process\.exit.*1/);
102
+ // Enhanced error handler shows context instead of stack trace in dim
103
+ expect(cliConsole.log).toHaveBeenCalledWith(
104
+ expect.stringContaining("📍 Context:"),
105
+ );
106
+ });
107
+ });
@@ -0,0 +1,170 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { RichProgressReporter, SimpleProgressReporter } from "../rich-progress";
3
+
4
+ // Mock cli-progress
5
+ vi.mock("cli-progress", () => {
6
+ const mockBar = {
7
+ update: vi.fn(),
8
+ };
9
+
10
+ const mockMultiBar = {
11
+ create: vi.fn().mockReturnValue(mockBar),
12
+ stop: vi.fn(),
13
+ };
14
+
15
+ return {
16
+ default: {
17
+ MultiBar: vi.fn(() => mockMultiBar),
18
+ Presets: {
19
+ shades_classic: {},
20
+ },
21
+ },
22
+ };
23
+ });
24
+
25
+ // Mock ora
26
+ vi.mock("ora", () => ({
27
+ default: vi.fn(() => ({
28
+ start: vi.fn().mockReturnThis(),
29
+ succeed: vi.fn(),
30
+ fail: vi.fn(),
31
+ text: "",
32
+ })),
33
+ }));
34
+
35
+ describe("RichProgressReporter", () => {
36
+ let reporter: RichProgressReporter;
37
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>;
38
+
39
+ beforeEach(() => {
40
+ // Skip creating the actual reporter to avoid mocking issues
41
+ consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {
42
+ // Intentionally empty for test isolation
43
+ });
44
+ vi.clearAllMocks();
45
+ });
46
+
47
+ afterEach(() => {
48
+ consoleLogSpy.mockRestore();
49
+ });
50
+
51
+ it("should start progress tracking", () => {
52
+ reporter = new RichProgressReporter();
53
+ reporter.start();
54
+
55
+ expect(consoleLogSpy).toHaveBeenCalledWith(
56
+ expect.stringContaining("🚀 Starting deployment..."),
57
+ );
58
+ });
59
+
60
+ it("should update progress with build status", () => {
61
+ reporter = new RichProgressReporter();
62
+
63
+ const progress = {
64
+ phase: "build",
65
+ message: "Bundling files...",
66
+ progress: 0.5,
67
+ };
68
+
69
+ // Just verify no errors
70
+ expect(() => reporter.updateProgress(progress)).not.toThrow();
71
+ });
72
+
73
+ it("should show success summary", () => {
74
+ // Test just the summary method
75
+ const reporter = new RichProgressReporter();
76
+ // Access private method for testing
77
+ const reporterWithPrivate = reporter as unknown as {
78
+ showSuccessSummary(): void;
79
+ };
80
+ reporterWithPrivate.showSuccessSummary();
81
+
82
+ expect(consoleLogSpy).toHaveBeenCalledWith(
83
+ expect.stringContaining("✅ Deployment completed successfully!"),
84
+ );
85
+ });
86
+
87
+ it("should show failure message", () => {
88
+ // Create a minimal test that doesn't rely on bars
89
+ const reporter = new RichProgressReporter();
90
+ // Access private properties for testing
91
+ const reporterWithPrivate = reporter as unknown as {
92
+ multibar: { stop: () => void };
93
+ bars: Map<string, unknown>;
94
+ };
95
+ reporterWithPrivate.multibar = { stop: vi.fn() };
96
+ reporterWithPrivate.bars = new Map(); // Empty map
97
+
98
+ reporter.stop(false);
99
+
100
+ expect(consoleLogSpy).toHaveBeenCalledWith(
101
+ expect.stringContaining("❌ Deployment failed"),
102
+ );
103
+ });
104
+
105
+ it("should display build stats when available", () => {
106
+ const reporter = new RichProgressReporter();
107
+
108
+ // Set stats directly for testing
109
+ const reporterWithPrivate = reporter as unknown as {
110
+ stats: { bundleSize?: number; gzippedSize?: number };
111
+ };
112
+ reporterWithPrivate.stats = {
113
+ bundleSize: 150000,
114
+ gzippedSize: 50000,
115
+ };
116
+
117
+ // Call the summary method
118
+ const reporterWithSummary = reporter as unknown as {
119
+ showSuccessSummary(): void;
120
+ };
121
+ reporterWithSummary.showSuccessSummary();
122
+
123
+ expect(consoleLogSpy).toHaveBeenCalledWith(
124
+ expect.stringContaining("📊 Build Stats:"),
125
+ );
126
+ expect(consoleLogSpy).toHaveBeenCalledWith(
127
+ expect.stringContaining("Bundle size: 146.5KB"),
128
+ );
129
+ });
130
+ });
131
+
132
+ describe("SimpleProgressReporter", () => {
133
+ let reporter: SimpleProgressReporter;
134
+
135
+ beforeEach(() => {
136
+ reporter = new SimpleProgressReporter("Testing...");
137
+ vi.clearAllMocks();
138
+ });
139
+
140
+ it("should start spinner", () => {
141
+ reporter.start();
142
+
143
+ // Spinner is mocked, verify no errors
144
+ expect(() => reporter.start()).not.toThrow();
145
+ });
146
+
147
+ it("should update message", () => {
148
+ reporter.start();
149
+ reporter.update("New message");
150
+
151
+ // Update is mocked, verify no errors
152
+ expect(() => reporter.update("test")).not.toThrow();
153
+ });
154
+
155
+ it("should succeed with message", () => {
156
+ reporter.start();
157
+ reporter.succeed("Success!");
158
+
159
+ // Success is mocked, verify no errors
160
+ expect(() => reporter.succeed()).not.toThrow();
161
+ });
162
+
163
+ it("should fail with message", () => {
164
+ reporter.start();
165
+ reporter.fail("Failed!");
166
+
167
+ // Fail is mocked, verify no errors
168
+ expect(() => reporter.fail()).not.toThrow();
169
+ });
170
+ });