@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.
- package/.turbo/turbo-build.log +2 -11
- package/CHANGELOG.md +17 -5
- package/CLAUDE_CLI.md +265 -0
- package/README.md +704 -8
- package/__tests__/mocks/console.ts +22 -0
- package/__tests__/mocks/core.ts +137 -0
- package/__tests__/mocks/index.ts +4 -0
- package/__tests__/mocks/inquirer.ts +16 -0
- package/__tests__/mocks/progress.ts +19 -0
- package/dist/__tests__/helpers/cli-test-helper.d.ts +89 -0
- package/dist/__tests__/helpers/cli-test-helper.d.ts.map +1 -0
- package/dist/__tests__/helpers/cli-test-helper.js +220 -0
- package/dist/__tests__/mocks/index.d.ts +69 -0
- package/dist/__tests__/mocks/index.d.ts.map +1 -0
- package/dist/__tests__/mocks/index.js +77 -0
- package/dist/actions/component.actions.d.ts +14 -0
- package/dist/actions/component.actions.d.ts.map +1 -0
- package/dist/actions/component.actions.js +273 -0
- package/dist/actions/function.actions.d.ts +15 -0
- package/dist/actions/function.actions.d.ts.map +1 -0
- package/dist/actions/function.actions.js +254 -0
- package/dist/actions/project.actions.d.ts +17 -0
- package/dist/actions/project.actions.d.ts.map +1 -0
- package/dist/actions/project.actions.js +97 -0
- package/dist/actions/version.actions.d.ts +19 -0
- package/dist/actions/version.actions.d.ts.map +1 -0
- package/dist/actions/version.actions.js +216 -0
- package/dist/commands/component.d.ts +3 -0
- package/dist/commands/component.d.ts.map +1 -0
- package/dist/commands/component.js +192 -0
- package/dist/commands/docs.d.ts +3 -0
- package/dist/commands/docs.d.ts.map +1 -0
- package/dist/commands/docs.js +16 -0
- package/dist/commands/function.d.ts +3 -0
- package/dist/commands/function.d.ts.map +1 -0
- package/dist/commands/function.js +243 -0
- package/dist/commands/help.d.ts +3 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +20 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +26 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +175 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +78 -0
- package/dist/commands/store-version.d.ts +3 -0
- package/dist/commands/store-version.d.ts.map +1 -0
- package/dist/commands/store-version.js +241 -0
- package/dist/commands/version.d.ts +3 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +46 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +41 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +93 -226
- package/dist/prompts/component.prompts.d.ts +14 -0
- package/dist/prompts/component.prompts.d.ts.map +1 -0
- package/dist/prompts/component.prompts.js +75 -0
- package/dist/prompts/function.prompts.d.ts +21 -0
- package/dist/prompts/function.prompts.d.ts.map +1 -0
- package/dist/prompts/function.prompts.js +127 -0
- package/dist/schemas/command.schema.d.ts +516 -0
- package/dist/schemas/command.schema.d.ts.map +1 -0
- package/dist/schemas/command.schema.js +267 -0
- package/dist/types/index.d.ts +147 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +18 -0
- package/dist/utils/auth.d.ts +4 -0
- package/dist/utils/auth.d.ts.map +1 -0
- package/dist/utils/auth.js +26 -0
- package/dist/utils/cli-progress-reporter.d.ts +12 -0
- package/dist/utils/cli-progress-reporter.d.ts.map +1 -0
- package/dist/utils/cli-progress-reporter.js +77 -0
- package/dist/utils/command-builder.d.ts +22 -0
- package/dist/utils/command-builder.d.ts.map +1 -0
- package/dist/utils/command-builder.js +268 -0
- package/dist/utils/command-helpers.d.ts +19 -0
- package/dist/utils/command-helpers.d.ts.map +1 -0
- package/dist/utils/command-helpers.js +79 -0
- package/dist/utils/command-parser.d.ts +146 -0
- package/dist/utils/command-parser.d.ts.map +1 -0
- package/dist/utils/command-parser.js +179 -0
- package/dist/utils/command-suggestions.d.ts +35 -0
- package/dist/utils/command-suggestions.d.ts.map +1 -0
- package/dist/utils/command-suggestions.js +152 -0
- package/dist/utils/console.d.ts +44 -0
- package/dist/utils/console.d.ts.map +1 -0
- package/dist/utils/console.js +233 -0
- package/dist/utils/constants.d.ts +8 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/context-detector.d.ts +12 -0
- package/dist/utils/context-detector.d.ts.map +1 -0
- package/dist/utils/context-detector.js +155 -0
- package/dist/utils/enhanced-error-handler.d.ts +47 -0
- package/dist/utils/enhanced-error-handler.d.ts.map +1 -0
- package/dist/utils/enhanced-error-handler.js +221 -0
- package/dist/utils/error-handler.d.ts +3 -0
- package/dist/utils/error-handler.d.ts.map +1 -0
- package/dist/utils/error-handler.js +55 -0
- package/dist/utils/errors.d.ts +44 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +76 -0
- package/dist/utils/interactive-builder.d.ts +22 -0
- package/dist/utils/interactive-builder.d.ts.map +1 -0
- package/dist/utils/interactive-builder.js +246 -0
- package/dist/utils/rich-progress.d.ts +59 -0
- package/dist/utils/rich-progress.d.ts.map +1 -0
- package/dist/utils/rich-progress.js +234 -0
- package/dist/utils/store.d.ts +11 -0
- package/dist/utils/store.d.ts.map +1 -0
- package/dist/utils/store.js +19 -0
- package/dist/utils/validation-error-formatter.d.ts +25 -0
- package/dist/utils/validation-error-formatter.d.ts.map +1 -0
- package/dist/utils/validation-error-formatter.js +258 -0
- package/dist/utils/validation-helpers.d.ts +60 -0
- package/dist/utils/validation-helpers.d.ts.map +1 -0
- package/dist/utils/validation-helpers.js +152 -0
- package/package.json +44 -9
- package/src/__tests__/helpers/cli-test-helper.ts +281 -0
- package/src/__tests__/mocks/index.ts +142 -0
- package/src/actions/component.actions.ts +334 -0
- package/src/actions/function.actions.ts +313 -0
- package/src/actions/project.actions.ts +126 -0
- package/src/actions/version.actions.ts +233 -0
- package/src/commands/__tests__/component-validation.test.ts +250 -0
- package/src/commands/__tests__/component.test.ts +321 -0
- package/src/commands/__tests__/function-validation.test.ts +220 -0
- package/src/commands/__tests__/function.test.ts +286 -0
- package/src/commands/__tests__/store-version-validation.test.ts +414 -0
- package/src/commands/__tests__/store-version.test.ts +405 -0
- package/src/commands/__tests__/version.test.ts +71 -0
- package/src/commands/component.ts +188 -0
- package/src/commands/docs.ts +24 -0
- package/src/commands/function.ts +252 -0
- package/src/commands/help.ts +18 -0
- package/src/commands/index.ts +21 -7
- package/src/commands/login.ts +19 -79
- package/src/commands/project.ts +107 -0
- package/src/commands/store-version.ts +242 -0
- package/src/commands/version.ts +51 -0
- package/src/commands/whoami.ts +46 -0
- package/src/index.ts +110 -15
- package/src/prompts/component.prompts.ts +94 -0
- package/src/prompts/function.prompts.ts +168 -0
- package/src/schemas/command.schema.ts +354 -0
- package/src/types/index.ts +183 -0
- package/src/utils/__tests__/command-parser.test.ts +159 -0
- package/src/utils/__tests__/command-suggestions.test.ts +185 -0
- package/src/utils/__tests__/console.test.ts +192 -0
- package/src/utils/__tests__/context-detector.test.ts +258 -0
- package/src/utils/__tests__/enhanced-error-handler.test.ts +137 -0
- package/src/utils/__tests__/error-handler.test.ts +107 -0
- package/src/utils/__tests__/rich-progress.test.ts +170 -0
- package/src/utils/__tests__/validation-error-formatter.test.ts +175 -0
- package/src/utils/__tests__/validation-helpers.test.ts +125 -0
- package/src/utils/auth.ts +41 -0
- package/src/utils/cli-progress-reporter.ts +84 -0
- package/src/utils/command-builder.ts +390 -0
- package/src/utils/command-helpers.ts +83 -0
- package/src/utils/command-parser.ts +250 -0
- package/src/utils/command-suggestions.ts +176 -0
- package/src/utils/console.ts +291 -0
- package/src/utils/context-detector.ts +177 -0
- package/src/utils/enhanced-error-handler.ts +264 -0
- package/src/utils/error-handler.ts +60 -0
- package/src/utils/errors.ts +125 -0
- package/src/utils/interactive-builder.ts +271 -0
- package/src/utils/rich-progress.ts +320 -0
- package/src/utils/store.ts +23 -0
- package/src/utils/validation-error-formatter.ts +337 -0
- package/src/utils/validation-helpers.ts +192 -0
- package/tsconfig.json +13 -7
- package/vitest.config.ts +28 -0
- package/vitest.setup.ts +29 -0
- 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
|
+
});
|