@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,264 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { ErrorContext } from "../types";
|
|
3
|
+
import { isNodeError } from "../types";
|
|
4
|
+
import type { CliConsole } from "./console.js";
|
|
5
|
+
import { detectProjectContext } from "./context-detector.js";
|
|
6
|
+
|
|
7
|
+
interface ErrorRecovery {
|
|
8
|
+
message: string;
|
|
9
|
+
suggestions: string[];
|
|
10
|
+
commands?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class EnhancedErrorHandler {
|
|
14
|
+
constructor(private console: CliConsole) {}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Common error patterns with recovery suggestions
|
|
18
|
+
*/
|
|
19
|
+
private readonly errorPatterns = new Map<string | RegExp, ErrorRecovery>([
|
|
20
|
+
[
|
|
21
|
+
"ENOENT",
|
|
22
|
+
{
|
|
23
|
+
message: "File or directory not found",
|
|
24
|
+
suggestions: [
|
|
25
|
+
"Check if the path exists",
|
|
26
|
+
"Use --path to specify the correct location",
|
|
27
|
+
],
|
|
28
|
+
commands: ["ls", "pwd"],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
"EACCES",
|
|
33
|
+
{
|
|
34
|
+
message: "Permission denied",
|
|
35
|
+
suggestions: [
|
|
36
|
+
"Check file permissions",
|
|
37
|
+
"Try running with appropriate permissions",
|
|
38
|
+
],
|
|
39
|
+
commands: ["ls -la"],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
"ECONNREFUSED",
|
|
44
|
+
{
|
|
45
|
+
message: "Connection refused",
|
|
46
|
+
suggestions: [
|
|
47
|
+
"Check your internet connection",
|
|
48
|
+
"Verify your authentication",
|
|
49
|
+
"Check if backend services are running",
|
|
50
|
+
],
|
|
51
|
+
commands: ["ollieshop login", "ollieshop status"],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
/Component .* already exists/,
|
|
56
|
+
{
|
|
57
|
+
message: "Component already exists",
|
|
58
|
+
suggestions: [
|
|
59
|
+
"Use a different name",
|
|
60
|
+
"Delete the existing component first",
|
|
61
|
+
"Update the existing component instead",
|
|
62
|
+
],
|
|
63
|
+
commands: ["ollieshop component list", "rm -rf ./components/{name}"],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
/Invalid component name/,
|
|
68
|
+
{
|
|
69
|
+
message: "Component name validation failed",
|
|
70
|
+
suggestions: [
|
|
71
|
+
"Use lowercase letters and hyphens only",
|
|
72
|
+
"Examples: header-nav, shopping-cart, product-list",
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Handle errors with enhanced recovery suggestions
|
|
80
|
+
*/
|
|
81
|
+
handle(error: Error, context?: ErrorContext): void {
|
|
82
|
+
// Display the main error
|
|
83
|
+
this.console.error(`\n❌ ${error.message}\n`);
|
|
84
|
+
|
|
85
|
+
// Get recovery suggestions
|
|
86
|
+
const recovery = this.getRecoverySuggestions(error);
|
|
87
|
+
|
|
88
|
+
if (recovery) {
|
|
89
|
+
this.displayRecoverySuggestions(recovery, context);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Show contextual information
|
|
93
|
+
this.displayContextualInfo(error, context);
|
|
94
|
+
|
|
95
|
+
// Suggest similar commands if applicable
|
|
96
|
+
if (this.isCommandError(error)) {
|
|
97
|
+
this.suggestSimilarCommands(context);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get recovery suggestions based on error
|
|
103
|
+
*/
|
|
104
|
+
private getRecoverySuggestions(error: Error): ErrorRecovery | undefined {
|
|
105
|
+
// Check error code
|
|
106
|
+
if (isNodeError(error) && this.errorPatterns.has(error.code)) {
|
|
107
|
+
return this.errorPatterns.get(error.code);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check error message patterns
|
|
111
|
+
for (const [pattern, recovery] of this.errorPatterns) {
|
|
112
|
+
if (pattern instanceof RegExp && pattern.test(error.message)) {
|
|
113
|
+
return recovery;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Display recovery suggestions
|
|
122
|
+
*/
|
|
123
|
+
private displayRecoverySuggestions(
|
|
124
|
+
recovery: ErrorRecovery,
|
|
125
|
+
context?: ErrorContext,
|
|
126
|
+
): void {
|
|
127
|
+
this.console.warn("💡 Possible solutions:\n");
|
|
128
|
+
|
|
129
|
+
for (const [index, suggestion] of recovery.suggestions.entries()) {
|
|
130
|
+
// Replace placeholders in suggestions
|
|
131
|
+
const formatted = this.formatSuggestion(suggestion, context);
|
|
132
|
+
this.console.log(chalk.yellow(` ${index + 1}. ${formatted}`));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (recovery.commands && recovery.commands.length > 0) {
|
|
136
|
+
this.console.log(chalk.dim("\n📍 Helpful commands:"));
|
|
137
|
+
for (const cmd of recovery.commands) {
|
|
138
|
+
const formatted = this.formatSuggestion(cmd, context);
|
|
139
|
+
this.console.log(chalk.dim(` $ ${formatted}`));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Display contextual information
|
|
146
|
+
*/
|
|
147
|
+
private displayContextualInfo(_error: Error, context?: ErrorContext): void {
|
|
148
|
+
const projectContext = detectProjectContext();
|
|
149
|
+
|
|
150
|
+
this.console.log(chalk.dim("\n📍 Context:"));
|
|
151
|
+
this.console.log(chalk.dim(` Current directory: ${process.cwd()}`));
|
|
152
|
+
|
|
153
|
+
if (projectContext.type !== "unknown") {
|
|
154
|
+
this.console.log(chalk.dim(` Project type: ${projectContext.type}`));
|
|
155
|
+
if (projectContext.name) {
|
|
156
|
+
this.console.log(
|
|
157
|
+
chalk.dim(` ${projectContext.type} name: ${projectContext.name}`),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (context?.path) {
|
|
163
|
+
this.console.log(chalk.dim(` Target path: ${context.path}`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Format suggestion with context values
|
|
169
|
+
*/
|
|
170
|
+
private formatSuggestion(suggestion: string, context?: ErrorContext): string {
|
|
171
|
+
if (!context) return suggestion;
|
|
172
|
+
|
|
173
|
+
return suggestion.replace(/{(\w+)}/g, (match, key) => {
|
|
174
|
+
const value = context[key as keyof ErrorContext];
|
|
175
|
+
return typeof value === "string" ? value : match;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if this is a command-related error
|
|
181
|
+
*/
|
|
182
|
+
private isCommandError(error: Error): boolean {
|
|
183
|
+
return (
|
|
184
|
+
error.message.includes("Unknown command") ||
|
|
185
|
+
error.message.includes("not found") ||
|
|
186
|
+
(isNodeError(error) && error.code === "COMMAND_NOT_FOUND")
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Suggest similar commands
|
|
192
|
+
*/
|
|
193
|
+
private suggestSimilarCommands(context?: ErrorContext): void {
|
|
194
|
+
if (!context?.attemptedCommand) return;
|
|
195
|
+
|
|
196
|
+
const suggestions = this.findSimilarCommands(
|
|
197
|
+
String(context.attemptedCommand),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (suggestions.length > 0) {
|
|
201
|
+
this.console.log(chalk.dim("\n💡 Did you mean:"));
|
|
202
|
+
for (const cmd of suggestions) {
|
|
203
|
+
this.console.log(chalk.dim(` $ ollieshop ${cmd}`));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Find similar commands using basic string matching
|
|
210
|
+
*/
|
|
211
|
+
private findSimilarCommands(attempted: string): string[] {
|
|
212
|
+
const commands = [
|
|
213
|
+
"component create",
|
|
214
|
+
"component validate",
|
|
215
|
+
"component build",
|
|
216
|
+
"component deploy",
|
|
217
|
+
"component list",
|
|
218
|
+
"function create",
|
|
219
|
+
"function validate",
|
|
220
|
+
"function build",
|
|
221
|
+
"function test",
|
|
222
|
+
"function deploy",
|
|
223
|
+
"store-version create",
|
|
224
|
+
"store-version list",
|
|
225
|
+
"template list",
|
|
226
|
+
"login",
|
|
227
|
+
"whoami",
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
return commands
|
|
231
|
+
.filter((cmd) => {
|
|
232
|
+
// Simple similarity check
|
|
233
|
+
const cmdParts = cmd.toLowerCase().split(" ");
|
|
234
|
+
const attemptedParts = attempted.toLowerCase().split(" ");
|
|
235
|
+
|
|
236
|
+
// Check if any part matches
|
|
237
|
+
return cmdParts.some((part) =>
|
|
238
|
+
attemptedParts.some(
|
|
239
|
+
(aPart) => part.startsWith(aPart) || aPart.startsWith(part),
|
|
240
|
+
),
|
|
241
|
+
);
|
|
242
|
+
})
|
|
243
|
+
.slice(0, 3); // Show top 3 suggestions
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Create an error with additional context
|
|
249
|
+
*/
|
|
250
|
+
export function createContextualError(
|
|
251
|
+
message: string,
|
|
252
|
+
code?: string,
|
|
253
|
+
context?: Record<string, unknown>,
|
|
254
|
+
): Error {
|
|
255
|
+
const error = new Error(message) as Error & {
|
|
256
|
+
code?: string;
|
|
257
|
+
context?: Record<string, unknown>;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
if (code) error.code = code;
|
|
261
|
+
if (context) error.context = context;
|
|
262
|
+
|
|
263
|
+
return error;
|
|
264
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ErrorContext } from "../types";
|
|
2
|
+
import { console as cliConsole } from "./console";
|
|
3
|
+
import { EnhancedErrorHandler } from "./enhanced-error-handler.js";
|
|
4
|
+
import { OllieShopCLIError, type RecoveryAction } from "./errors";
|
|
5
|
+
|
|
6
|
+
function handleOllieShopError(error: OllieShopCLIError): void {
|
|
7
|
+
cliConsole.error(`\n${error.message}`);
|
|
8
|
+
|
|
9
|
+
if (error.details) {
|
|
10
|
+
displayErrorDetails(error.details);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (error.suggestions && error.suggestions.length > 0) {
|
|
14
|
+
displayRecoveryActions(error.suggestions);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function displayErrorDetails(details: ErrorContext): void {
|
|
19
|
+
cliConsole.dim("\nContext:");
|
|
20
|
+
for (const [key, value] of Object.entries(details)) {
|
|
21
|
+
if (key === "fields" && Array.isArray(value)) {
|
|
22
|
+
displayFieldErrors(value);
|
|
23
|
+
} else {
|
|
24
|
+
cliConsole.dim(` ${key}: ${value}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function displayFieldErrors(
|
|
30
|
+
fields: Array<{ field: string; message: string }>,
|
|
31
|
+
): void {
|
|
32
|
+
cliConsole.dim(" fields:");
|
|
33
|
+
for (const field of fields) {
|
|
34
|
+
cliConsole.dim(` ${field.field}: ${field.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function displayRecoveryActions(actions: RecoveryAction[]): void {
|
|
39
|
+
cliConsole.info("\nSuggestions:");
|
|
40
|
+
for (const action of actions) {
|
|
41
|
+
cliConsole.dim(` • ${action.description}`);
|
|
42
|
+
if (action.command) {
|
|
43
|
+
cliConsole.dim(` $ ${action.command}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function handleError(error: unknown, context?: ErrorContext): never {
|
|
49
|
+
if (error instanceof OllieShopCLIError) {
|
|
50
|
+
handleOllieShopError(error);
|
|
51
|
+
} else if (error instanceof Error) {
|
|
52
|
+
const enhancedHandler = new EnhancedErrorHandler(cliConsole);
|
|
53
|
+
enhancedHandler.handle(error, context);
|
|
54
|
+
} else {
|
|
55
|
+
cliConsole.error("\nAn unexpected error occurred");
|
|
56
|
+
cliConsole.dim(String(error));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { ZodError } from "zod";
|
|
2
|
+
import type { ErrorContext } from "../types";
|
|
3
|
+
|
|
4
|
+
export interface RecoveryAction {
|
|
5
|
+
description: string;
|
|
6
|
+
command?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class OllieShopCLIError extends Error {
|
|
10
|
+
public readonly context: ErrorContext;
|
|
11
|
+
public readonly recoveryActions: RecoveryAction[];
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
message: string,
|
|
15
|
+
context: ErrorContext = {},
|
|
16
|
+
recoveryActions: RecoveryAction[] = [],
|
|
17
|
+
) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "OllieShopCLIError";
|
|
20
|
+
this.context = context;
|
|
21
|
+
this.recoveryActions = recoveryActions;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get details(): ErrorContext {
|
|
25
|
+
return this.context;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get suggestions(): RecoveryAction[] {
|
|
29
|
+
return this.recoveryActions;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static fromZodError(
|
|
33
|
+
error: ZodError,
|
|
34
|
+
context: ErrorContext = {},
|
|
35
|
+
): OllieShopCLIError {
|
|
36
|
+
const firstIssue = error.issues[0];
|
|
37
|
+
if (!firstIssue) {
|
|
38
|
+
return new OllieShopCLIError("Validation failed", context);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const message =
|
|
42
|
+
error.issues.length > 1
|
|
43
|
+
? `${firstIssue.message} (and ${error.issues.length - 1} more error${error.issues.length === 2 ? "" : "s"})`
|
|
44
|
+
: firstIssue.message;
|
|
45
|
+
|
|
46
|
+
return new OllieShopCLIError(message, context, [
|
|
47
|
+
{ description: "Check the command documentation for valid formats" },
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static fromUnknown(
|
|
52
|
+
error: unknown,
|
|
53
|
+
context: ErrorContext = {},
|
|
54
|
+
): OllieShopCLIError {
|
|
55
|
+
if (error instanceof OllieShopCLIError) {
|
|
56
|
+
return error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (error instanceof Error) {
|
|
60
|
+
return new OllieShopCLIError(error.message, context, [
|
|
61
|
+
{ description: "Check the command syntax and try again" },
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return new OllieShopCLIError(
|
|
66
|
+
String(error) || "Unknown error occurred",
|
|
67
|
+
context,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static fileNotFound(
|
|
72
|
+
path: string,
|
|
73
|
+
context: ErrorContext = {},
|
|
74
|
+
): OllieShopCLIError {
|
|
75
|
+
return new OllieShopCLIError(
|
|
76
|
+
`File not found: ${path}`,
|
|
77
|
+
{ ...context, file: path },
|
|
78
|
+
[
|
|
79
|
+
{ description: "Check if the file path is correct" },
|
|
80
|
+
{ description: "Ensure the file exists in the specified location" },
|
|
81
|
+
],
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const CLIError = OllieShopCLIError;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Command execution error
|
|
90
|
+
*/
|
|
91
|
+
export class CommandError extends OllieShopCLIError {
|
|
92
|
+
public readonly code: string;
|
|
93
|
+
|
|
94
|
+
constructor(message: string, code: string, context: ErrorContext = {}) {
|
|
95
|
+
super(message, context);
|
|
96
|
+
this.name = "CommandError";
|
|
97
|
+
this.code = code;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validation error with detailed field information
|
|
103
|
+
*/
|
|
104
|
+
export class ValidationError extends OllieShopCLIError {
|
|
105
|
+
public readonly fields: Array<{ field: string; message: string }>;
|
|
106
|
+
|
|
107
|
+
constructor(
|
|
108
|
+
message: string,
|
|
109
|
+
fields: Array<{ field: string; message: string }> = [],
|
|
110
|
+
context: ErrorContext = {},
|
|
111
|
+
) {
|
|
112
|
+
super(message, context);
|
|
113
|
+
this.name = "ValidationError";
|
|
114
|
+
this.fields = fields;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get details(): ErrorContext & {
|
|
118
|
+
fields: Array<{ field: string; message: string }>;
|
|
119
|
+
} {
|
|
120
|
+
return {
|
|
121
|
+
...this.context,
|
|
122
|
+
fields: this.fields,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { ComponentSlot } from "@ollie-shop/core";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import type { ComponentDeployOptions } from "../schemas/command.schema";
|
|
5
|
+
import type { InteractiveChoice, MockComponent } from "../types";
|
|
6
|
+
import type { CliConsole } from "./console.js";
|
|
7
|
+
|
|
8
|
+
export class InteractiveCommandBuilder {
|
|
9
|
+
constructor(private console: CliConsole) {}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build component deploy command interactively
|
|
13
|
+
*/
|
|
14
|
+
async buildDeployComponentCommand(): Promise<void> {
|
|
15
|
+
this.console.log(chalk.blue.bold("\n🚀 Component Deployment Wizard\n"));
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Get available components (mock data for now)
|
|
19
|
+
const components = await this.getAvailableComponents();
|
|
20
|
+
|
|
21
|
+
const answers = await inquirer.prompt([
|
|
22
|
+
{
|
|
23
|
+
type: "list",
|
|
24
|
+
name: "component",
|
|
25
|
+
message: "Select component to deploy:",
|
|
26
|
+
choices: components.map(
|
|
27
|
+
(c): InteractiveChoice => ({
|
|
28
|
+
name: `${c.name} (${c.slot}) - v${c.version || "1.0.0"} - ${chalk.dim(
|
|
29
|
+
`Created ${new Date(c.createdAt).toLocaleDateString()}`,
|
|
30
|
+
)}`,
|
|
31
|
+
value: c.id,
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: "checkbox",
|
|
37
|
+
name: "options",
|
|
38
|
+
message: "Deployment options:",
|
|
39
|
+
choices: [
|
|
40
|
+
{ name: "Wait for completion", value: "wait", checked: true },
|
|
41
|
+
{ name: "Run tests before deploy", value: "test", checked: true },
|
|
42
|
+
{
|
|
43
|
+
name: "Enable verbose logging",
|
|
44
|
+
value: "verbose",
|
|
45
|
+
checked: false,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// Build the command
|
|
52
|
+
let command = `ollieshop component deploy --id ${answers.component}`;
|
|
53
|
+
|
|
54
|
+
if (answers.options.includes("wait")) {
|
|
55
|
+
command += " --wait";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (answers.options.includes("verbose")) {
|
|
59
|
+
command += " --verbose";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Show preview
|
|
63
|
+
this.console.log(`\n${chalk.dim("[Preview] This will run:")}`);
|
|
64
|
+
this.console.log(chalk.cyan(` ${command}\n`));
|
|
65
|
+
|
|
66
|
+
// Confirm execution
|
|
67
|
+
const { proceed } = await inquirer.prompt({
|
|
68
|
+
type: "confirm",
|
|
69
|
+
name: "proceed",
|
|
70
|
+
message: "Execute this command?",
|
|
71
|
+
default: true,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (proceed) {
|
|
75
|
+
// Execute the command
|
|
76
|
+
this.console.log(chalk.dim("\nExecuting command...\n"));
|
|
77
|
+
|
|
78
|
+
// Import and execute the action directly
|
|
79
|
+
const { deployComponent } = await import(
|
|
80
|
+
"../actions/component.actions.js"
|
|
81
|
+
);
|
|
82
|
+
const deployOptions: ComponentDeployOptions = {
|
|
83
|
+
componentId: answers.component,
|
|
84
|
+
path: process.cwd(),
|
|
85
|
+
wait: answers.options.includes("wait"),
|
|
86
|
+
};
|
|
87
|
+
await deployComponent(deployOptions, this.console);
|
|
88
|
+
} else {
|
|
89
|
+
this.console.log(chalk.yellow("\n⚠️ Deployment cancelled"));
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof Error && error.name === "ExitPromptError") {
|
|
93
|
+
this.console.log(chalk.yellow("\n⚠️ Wizard cancelled"));
|
|
94
|
+
} else {
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build component create command interactively
|
|
102
|
+
*/
|
|
103
|
+
async buildCreateComponentCommand(): Promise<void> {
|
|
104
|
+
this.console.log(chalk.blue.bold("\n🎨 Component Creation Wizard\n"));
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const answers = await inquirer.prompt([
|
|
108
|
+
{
|
|
109
|
+
type: "input",
|
|
110
|
+
name: "name",
|
|
111
|
+
message: "Component name:",
|
|
112
|
+
validate: (input: string) => {
|
|
113
|
+
if (!input) return "Component name is required";
|
|
114
|
+
if (!/^[a-z0-9-]+$/.test(input)) {
|
|
115
|
+
return "Must be lowercase with hyphens only (e.g., header-nav)";
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
},
|
|
119
|
+
transformer: (input: string) => {
|
|
120
|
+
// Show live validation
|
|
121
|
+
if (!/^[a-z0-9-]+$/.test(input) && input) {
|
|
122
|
+
return chalk.red(input);
|
|
123
|
+
}
|
|
124
|
+
return input;
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
type: "list",
|
|
129
|
+
name: "slot",
|
|
130
|
+
message: "Component slot:",
|
|
131
|
+
choices: ComponentSlot.options.map((slot) => ({
|
|
132
|
+
name:
|
|
133
|
+
slot.charAt(0).toUpperCase() + slot.slice(1).replace(/-/g, " "),
|
|
134
|
+
value: slot,
|
|
135
|
+
})),
|
|
136
|
+
default: "main",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: "list",
|
|
140
|
+
name: "language",
|
|
141
|
+
message: "Language:",
|
|
142
|
+
choices: [
|
|
143
|
+
{ name: "TypeScript (recommended)", value: "typescript" },
|
|
144
|
+
{ name: "JavaScript", value: "javascript" },
|
|
145
|
+
],
|
|
146
|
+
default: "typescript",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: "confirm",
|
|
150
|
+
name: "tests",
|
|
151
|
+
message: "Include test files?",
|
|
152
|
+
default: true,
|
|
153
|
+
},
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
// Build the command
|
|
157
|
+
let command = `ollieshop component create --name ${answers.name}`;
|
|
158
|
+
|
|
159
|
+
if (answers.slot !== "main") {
|
|
160
|
+
command += ` --slot ${answers.slot}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (answers.language === "javascript") {
|
|
164
|
+
command += " --no-typescript";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!answers.tests) {
|
|
168
|
+
command += " --no-tests";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Show preview
|
|
172
|
+
this.console.log(`\n${chalk.dim("[Preview] This will run:")}`);
|
|
173
|
+
this.console.log(chalk.cyan(` ${command}\n`));
|
|
174
|
+
|
|
175
|
+
// Confirm execution
|
|
176
|
+
const { proceed } = await inquirer.prompt({
|
|
177
|
+
type: "confirm",
|
|
178
|
+
name: "proceed",
|
|
179
|
+
message: "Create this component?",
|
|
180
|
+
default: true,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (proceed) {
|
|
184
|
+
// Execute the command
|
|
185
|
+
this.console.log(chalk.dim("\nCreating component...\n"));
|
|
186
|
+
|
|
187
|
+
const { createComponent } = await import(
|
|
188
|
+
"../actions/component.actions.js"
|
|
189
|
+
);
|
|
190
|
+
await createComponent(
|
|
191
|
+
{
|
|
192
|
+
name: answers.name,
|
|
193
|
+
slot: answers.slot,
|
|
194
|
+
tests: answers.tests,
|
|
195
|
+
},
|
|
196
|
+
this.console,
|
|
197
|
+
);
|
|
198
|
+
} else {
|
|
199
|
+
this.console.log(chalk.yellow("\n⚠️ Component creation cancelled"));
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error instanceof Error && error.name === "ExitPromptError") {
|
|
203
|
+
this.console.log(chalk.yellow("\n⚠️ Wizard cancelled"));
|
|
204
|
+
} else {
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get available components (mock for now)
|
|
212
|
+
*/
|
|
213
|
+
private async getAvailableComponents(): Promise<MockComponent[]> {
|
|
214
|
+
// TODO: Replace with actual API call
|
|
215
|
+
return [
|
|
216
|
+
{
|
|
217
|
+
id: "comp_header_123",
|
|
218
|
+
name: "header-nav",
|
|
219
|
+
slot: "header",
|
|
220
|
+
version: "1.2.0",
|
|
221
|
+
enabled: true,
|
|
222
|
+
createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
|
|
223
|
+
type: "ui",
|
|
224
|
+
description: "Navigation header component",
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "comp_cart_456",
|
|
228
|
+
name: "shopping-cart",
|
|
229
|
+
slot: "sidebar",
|
|
230
|
+
version: "1.0.0",
|
|
231
|
+
enabled: true,
|
|
232
|
+
createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
|
|
233
|
+
type: "ui",
|
|
234
|
+
description: "Shopping cart widget",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "comp_product_789",
|
|
238
|
+
name: "product-list",
|
|
239
|
+
slot: "main",
|
|
240
|
+
version: "2.1.0",
|
|
241
|
+
enabled: true,
|
|
242
|
+
createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
|
|
243
|
+
type: "ui",
|
|
244
|
+
description: "Product listing component",
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Create an interactive command builder
|
|
252
|
+
*/
|
|
253
|
+
export async function runInteractiveCommand(
|
|
254
|
+
command: string,
|
|
255
|
+
console: CliConsole,
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
const builder = new InteractiveCommandBuilder(console);
|
|
258
|
+
|
|
259
|
+
switch (command) {
|
|
260
|
+
case "deploy":
|
|
261
|
+
case "component-deploy":
|
|
262
|
+
await builder.buildDeployComponentCommand();
|
|
263
|
+
break;
|
|
264
|
+
case "create":
|
|
265
|
+
case "component-create":
|
|
266
|
+
await builder.buildCreateComponentCommand();
|
|
267
|
+
break;
|
|
268
|
+
default:
|
|
269
|
+
console.warn(`Interactive mode not available for command: ${command}`);
|
|
270
|
+
}
|
|
271
|
+
}
|