@ollie-shop/cli 0.2.0 → 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 (182) hide show
  1. package/.turbo/turbo-build.log +2 -11
  2. package/CHANGELOG.md +13 -7
  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 +88 -478
  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 +43 -11
  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 +11 -11
  140. package/src/commands/function.ts +252 -0
  141. package/src/commands/help.ts +8 -18
  142. package/src/commands/index.ts +14 -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 +45 -8
  147. package/src/commands/whoami.ts +8 -13
  148. package/src/index.ts +108 -34
  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 +0 -1
  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/validation-error-formatter.ts +337 -0
  176. package/src/utils/validation-helpers.ts +192 -0
  177. package/tsconfig.json +13 -7
  178. package/vitest.config.ts +28 -0
  179. package/vitest.setup.ts +29 -0
  180. package/src/commands/validate.ts +0 -62
  181. package/src/utils/core.ts +0 -105
  182. 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
+ }