@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,320 @@
1
+ import chalk from "chalk";
2
+ import cliProgress from "cli-progress";
3
+ import type { ProgressParams, ProgressPayload, Spinner } from "../types";
4
+
5
+ // Enhanced build progress type for CLI display
6
+ interface CLIBuildProgress {
7
+ phase: string;
8
+ message: string;
9
+ progress: number;
10
+ metadata?: {
11
+ bundleSize?: number;
12
+ gzippedSize?: number;
13
+ dependencies?: number;
14
+ removedExports?: number;
15
+ };
16
+ }
17
+
18
+ interface ProgressStep {
19
+ name: string;
20
+ emoji: string;
21
+ weight: number;
22
+ status?: "pending" | "active" | "completed" | "failed";
23
+ message?: string;
24
+ }
25
+
26
+ export class RichProgressReporter {
27
+ private multibar: cliProgress.MultiBar;
28
+ private bars: Map<string, cliProgress.SingleBar> = new Map();
29
+ private steps: ProgressStep[] = [
30
+ { name: "Validating", emoji: "🔍", weight: 10 },
31
+ { name: "Building", emoji: "🔨", weight: 40 },
32
+ { name: "Optimizing", emoji: "⚡", weight: 20 },
33
+ { name: "Deploying", emoji: "🚀", weight: 20 },
34
+ { name: "Verifying", emoji: "✅", weight: 10 },
35
+ ];
36
+ private startTime: number = Date.now();
37
+ private stats: {
38
+ bundleSize?: number;
39
+ gzippedSize?: number;
40
+ dependencies?: number;
41
+ removedExports?: number;
42
+ } = {};
43
+
44
+ constructor() {
45
+ // Create a formatter function that matches cli-progress expectations
46
+ const formatter = (
47
+ options: Record<string, unknown>,
48
+ params: Record<string, unknown>,
49
+ payload: Record<string, unknown>,
50
+ ): string => {
51
+ // Safe type conversion with validation
52
+ const formatOptions = {
53
+ barCompleteString:
54
+ typeof options.barCompleteString === "string"
55
+ ? options.barCompleteString
56
+ : undefined,
57
+ barIncompleteString:
58
+ typeof options.barIncompleteString === "string"
59
+ ? options.barIncompleteString
60
+ : undefined,
61
+ barsize:
62
+ typeof options.barsize === "number" ? options.barsize : undefined,
63
+ };
64
+
65
+ // Convert params to expected format
66
+ const progressParams: ProgressParams = {
67
+ progress: typeof params.progress === "number" ? params.progress : 0,
68
+ value: typeof params.value === "number" ? params.value : 0,
69
+ total: typeof params.total === "number" ? params.total : 0,
70
+ eta: typeof params.eta === "number" ? params.eta : 0,
71
+ startTime:
72
+ typeof params.startTime === "number" ? params.startTime : Date.now(),
73
+ stopTime: typeof params.stopTime === "number" ? params.stopTime : null,
74
+ duration: typeof params.duration === "number" ? params.duration : 0,
75
+ };
76
+
77
+ // Convert payload to expected format
78
+ const progressPayload: ProgressPayload = {
79
+ ...payload,
80
+ emoji: typeof payload.emoji === "string" ? payload.emoji : undefined,
81
+ step: typeof payload.step === "string" ? payload.step : undefined,
82
+ message:
83
+ typeof payload.message === "string" ? payload.message : undefined,
84
+ startTime:
85
+ typeof payload.startTime === "number" ? payload.startTime : undefined,
86
+ };
87
+
88
+ return this.formatBar(formatOptions, progressParams, progressPayload);
89
+ };
90
+
91
+ // Cast formatter to unknown first to bypass TypeScript's structural typing check
92
+ // cli-progress expects a GenericFormatter but the type definitions don't match exactly
93
+ const formatFunction = formatter as unknown;
94
+
95
+ this.multibar = new cliProgress.MultiBar(
96
+ {
97
+ format: formatFunction as cliProgress.GenericFormatter,
98
+ barCompleteChar: "\u2588",
99
+ barIncompleteChar: "\u2591",
100
+ hideCursor: true,
101
+ clearOnComplete: false,
102
+ forceRedraw: true,
103
+ },
104
+ cliProgress.Presets.shades_classic,
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Custom format for progress bars
110
+ */
111
+ private formatBar(
112
+ options: {
113
+ barCompleteString?: string;
114
+ barIncompleteString?: string;
115
+ barsize?: number;
116
+ [key: string]: unknown;
117
+ },
118
+ params: ProgressParams,
119
+ payload: ProgressPayload,
120
+ ): string {
121
+ const barCompleteString = options.barCompleteString || "\u2588";
122
+ const barIncompleteString = options.barIncompleteString || "\u2591";
123
+ const barsize = options.barsize || 40;
124
+
125
+ const bar = barCompleteString.substring(
126
+ 0,
127
+ Math.round(params.progress * barsize),
128
+ );
129
+ const empty = barIncompleteString.substring(0, barsize - bar.length);
130
+
131
+ const percent = Math.round(params.progress * 100);
132
+ const emoji = payload.emoji || "📦";
133
+ const step = payload.step || "Processing";
134
+ const message = payload.message || "";
135
+
136
+ let line = `${emoji} ${step.padEnd(12)} ${bar}${empty} ${percent.toString().padStart(3)}%`;
137
+
138
+ if (percent === 100 && payload.startTime) {
139
+ const duration = ((Date.now() - payload.startTime) / 1000).toFixed(1);
140
+ line += chalk.green(` ✓ ${duration}s`);
141
+ } else if (message) {
142
+ line += chalk.dim(` | ${message}`);
143
+ }
144
+
145
+ return line;
146
+ }
147
+
148
+ /**
149
+ * Start progress tracking
150
+ */
151
+ start(): void {
152
+ console.log(chalk.blue.bold("\n🚀 Starting deployment...\n"));
153
+
154
+ // Create bars for each step
155
+ for (const step of this.steps) {
156
+ const bar = this.multibar.create(100, 0, {
157
+ emoji: step.emoji,
158
+ step: step.name,
159
+ startTime: Date.now(),
160
+ });
161
+ this.bars.set(step.name, bar);
162
+ }
163
+ }
164
+
165
+ private readonly stepMap: Record<string, string> = {
166
+ validate: "Validating",
167
+ build: "Building",
168
+ optimize: "Optimizing",
169
+ upload: "Deploying",
170
+ verify: "Verifying",
171
+ };
172
+
173
+ /**
174
+ * Update progress based on CLIBuildProgress
175
+ */
176
+ updateProgress(progress: CLIBuildProgress): void {
177
+ const { phase, message, progress: percent } = progress;
178
+ const stepName = this.stepMap[phase] || "Processing";
179
+
180
+ this.updateBar(stepName, percent, message);
181
+ this.updatePreviousSteps(stepName);
182
+ this.updateStats(progress.metadata);
183
+ }
184
+
185
+ private updateBar(stepName: string, percent: number, message?: string): void {
186
+ const bar = this.bars.get(stepName);
187
+ if (bar) {
188
+ bar.update(percent * 100, { message });
189
+ }
190
+ }
191
+
192
+ private updatePreviousSteps(currentStepName: string): void {
193
+ let foundCurrent = false;
194
+
195
+ for (const step of this.steps) {
196
+ if (step.name === currentStepName) {
197
+ foundCurrent = true;
198
+ step.status = "active";
199
+ } else if (!foundCurrent) {
200
+ this.markStepComplete(step);
201
+ }
202
+ }
203
+ }
204
+
205
+ private markStepComplete(step: ProgressStep): void {
206
+ step.status = "completed";
207
+ const bar = this.bars.get(step.name);
208
+ if (bar) {
209
+ bar.update(100);
210
+ }
211
+ }
212
+
213
+ private updateStats(metadata?: CLIBuildProgress["metadata"]): void {
214
+ if (!metadata) return;
215
+
216
+ if (metadata.bundleSize) {
217
+ this.stats.bundleSize = metadata.bundleSize;
218
+ }
219
+ if (metadata.gzippedSize) {
220
+ this.stats.gzippedSize = metadata.gzippedSize;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Stop progress tracking
226
+ */
227
+ stop(success = true): void {
228
+ // Complete all bars if they exist
229
+ if (this.bars.size > 0) {
230
+ for (const bar of this.bars.values()) {
231
+ bar.update(100);
232
+ }
233
+ }
234
+
235
+ this.multibar.stop();
236
+
237
+ // Show summary
238
+ if (success) {
239
+ this.showSuccessSummary();
240
+ } else {
241
+ console.log(chalk.red("\n❌ Deployment failed\n"));
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Show success summary with stats
247
+ */
248
+ private showSuccessSummary(): void {
249
+ const duration = ((Date.now() - this.startTime) / 1000).toFixed(1);
250
+
251
+ console.log(chalk.green.bold("\n✅ Deployment completed successfully!\n"));
252
+
253
+ if (Object.keys(this.stats).length > 0) {
254
+ console.log(chalk.blue("📊 Build Stats:"));
255
+
256
+ if (this.stats.bundleSize) {
257
+ const sizeKB = (this.stats.bundleSize / 1024).toFixed(1);
258
+ const gzipKB = this.stats.gzippedSize
259
+ ? (this.stats.gzippedSize / 1024).toFixed(1)
260
+ : "?";
261
+ console.log(` • Bundle size: ${sizeKB}KB (gzipped: ${gzipKB}KB)`);
262
+ }
263
+
264
+ if (this.stats.removedExports) {
265
+ console.log(
266
+ ` • Tree shaking: Removed ${this.stats.removedExports} unused exports`,
267
+ );
268
+ }
269
+
270
+ if (this.stats.dependencies) {
271
+ console.log(` • Dependencies: ${this.stats.dependencies}`);
272
+ }
273
+
274
+ console.log("");
275
+ }
276
+
277
+ console.log(chalk.dim(`Total time: ${duration}s`));
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Simple progress reporter for single operations
283
+ */
284
+ export class SimpleProgressReporter {
285
+ private spinner: Spinner | null = null;
286
+ private startTime: number;
287
+
288
+ constructor(private message: string) {
289
+ this.startTime = Date.now();
290
+ }
291
+
292
+ start(): void {
293
+ const ora = require("ora");
294
+ this.spinner = ora({
295
+ text: this.message,
296
+ spinner: "dots",
297
+ }).start();
298
+ }
299
+
300
+ update(message: string): void {
301
+ if (this.spinner) {
302
+ this.spinner.text = message;
303
+ }
304
+ }
305
+
306
+ succeed(message?: string): void {
307
+ if (this.spinner) {
308
+ const duration = ((Date.now() - this.startTime) / 1000).toFixed(1);
309
+ this.spinner.succeed(
310
+ message || `${this.message} ${chalk.dim(`(${duration}s)`)}`,
311
+ );
312
+ }
313
+ }
314
+
315
+ fail(message?: string): void {
316
+ if (this.spinner) {
317
+ this.spinner.fail(message || this.message);
318
+ }
319
+ }
320
+ }
@@ -0,0 +1,337 @@
1
+ import type { ZodError, ZodIssue } from "zod";
2
+ import type { CliConsole } from "./console";
3
+
4
+ export interface ValidationError {
5
+ field: string;
6
+ message: string;
7
+ suggestedValue?: string;
8
+ examples?: ValidationExample[];
9
+ }
10
+
11
+ export interface ValidationExample {
12
+ description: string;
13
+ command: string;
14
+ }
15
+
16
+ const COMMON_EXAMPLES: Record<string, ValidationExample[]> = {
17
+ component: [
18
+ {
19
+ description: "Create a header component",
20
+ command: "ollieshop component create --name header-nav --slot header",
21
+ },
22
+ {
23
+ description: "Create a product listing component",
24
+ command: "ollieshop component create --name product-list --slot main",
25
+ },
26
+ ],
27
+ function: [
28
+ {
29
+ description: "Create a cart validation function",
30
+ command: "ollieshop function create --name validate-cart --event cart",
31
+ },
32
+ {
33
+ description: "Create an order processing function",
34
+ command:
35
+ "ollieshop function create --name process-order --event order --timing after",
36
+ },
37
+ ],
38
+ "store-version": [
39
+ {
40
+ description: "Create a new version",
41
+ command: "ollieshop store-version create --name summer-2024",
42
+ },
43
+ {
44
+ description: "Clone from existing version",
45
+ command: "ollieshop store-version clone --source v1 --name v2",
46
+ },
47
+ ],
48
+ };
49
+
50
+ function formatIssue(
51
+ issue: ZodIssue,
52
+ field: string,
53
+ ): { message: string; suggestedValue?: string } {
54
+ switch (issue.code) {
55
+ case "invalid_type": {
56
+ const invalidTypeIssue = issue as ZodIssue & { received?: string };
57
+ if (invalidTypeIssue.received === "undefined") {
58
+ return {
59
+ message: `${humanizeFieldName(field)} is required`,
60
+ };
61
+ }
62
+ return {
63
+ message: `${humanizeFieldName(field)} must be ${issue.expected}`,
64
+ };
65
+ }
66
+
67
+ case "invalid_enum_value": {
68
+ const options =
69
+ (issue as ZodIssue & { options?: string[] }).options || [];
70
+ return {
71
+ message: `Invalid ${humanizeFieldName(field)}`,
72
+ suggestedValue: `Valid options: ${options.join(", ")}`,
73
+ };
74
+ }
75
+
76
+ case "too_small": {
77
+ const minimum = (issue as ZodIssue & { minimum?: number }).minimum || 0;
78
+ return {
79
+ message: `${humanizeFieldName(field)} must be at least ${minimum} characters`,
80
+ };
81
+ }
82
+
83
+ case "invalid_string": {
84
+ const validation = (issue as ZodIssue & { validation?: string })
85
+ .validation;
86
+ if (!validation) {
87
+ return { message: humanizeMessage(issue.message, field) };
88
+ }
89
+
90
+ return formatStringValidation(validation, field);
91
+ }
92
+
93
+ default:
94
+ return { message: humanizeMessage(issue.message, field) };
95
+ }
96
+ }
97
+
98
+ function formatStringValidation(
99
+ validation: string,
100
+ field: string,
101
+ ): { message: string } {
102
+ const validationMessages: Record<string, string> = {
103
+ regex: getRegexMessage(field),
104
+ email: `${humanizeFieldName(field)} must be a valid email address`,
105
+ url: `${humanizeFieldName(field)} must be a valid URL`,
106
+ uuid: `${humanizeFieldName(field)} must be a valid UUID`,
107
+ };
108
+
109
+ return {
110
+ message:
111
+ validationMessages[validation] ||
112
+ `Invalid ${humanizeFieldName(field)} format`,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Format Zod errors into user-friendly validation errors
118
+ */
119
+ export function formatZodError(
120
+ error: ZodError,
121
+ context?: string,
122
+ ): ValidationError[] {
123
+ const errors: ValidationError[] = [];
124
+
125
+ for (const issue of error.issues) {
126
+ const field = issue.path.join(".");
127
+ const { message, suggestedValue } = formatIssue(issue, field);
128
+
129
+ const error: ValidationError = {
130
+ field,
131
+ message,
132
+ suggestedValue,
133
+ };
134
+
135
+ // Add examples based on context
136
+ if (context && COMMON_EXAMPLES[context]) {
137
+ error.examples = COMMON_EXAMPLES[context];
138
+ }
139
+
140
+ errors.push(error);
141
+ }
142
+
143
+ return errors;
144
+ }
145
+
146
+ /**
147
+ * Display validation errors in a formatted way
148
+ */
149
+ export function displayErrors(
150
+ console: CliConsole,
151
+ errors: ValidationError[],
152
+ command?: string,
153
+ ): void {
154
+ console.error("❌ Validation failed:");
155
+ console.log("");
156
+
157
+ for (const error of errors) {
158
+ console.error(` • ${error.field}: ${error.message}`);
159
+
160
+ if (error.suggestedValue) {
161
+ console.log(` 💡 ${error.suggestedValue}`);
162
+ }
163
+
164
+ if (error.examples && error.examples.length > 0) {
165
+ console.log("");
166
+ console.log(" 📝 Examples:");
167
+ for (const example of error.examples) {
168
+ console.log(` ${example.description}:`);
169
+ console.log(` $ ${example.command}`);
170
+ }
171
+ }
172
+ }
173
+
174
+ if (command) {
175
+ console.log("");
176
+ console.info(`💡 For more help, run: ollieshop ${command} --help`);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Suggest a command from available commands
182
+ */
183
+ export function suggestCommand(
184
+ input: string,
185
+ availableCommands: string[],
186
+ ): string | undefined {
187
+ const normalizedInput = input.toLowerCase();
188
+
189
+ // Exact match
190
+ if (availableCommands.includes(normalizedInput)) {
191
+ return normalizedInput;
192
+ }
193
+
194
+ // Find commands that start with the input
195
+ const startsWithMatches = availableCommands.filter((cmd) =>
196
+ cmd.toLowerCase().startsWith(normalizedInput),
197
+ );
198
+
199
+ if (startsWithMatches.length === 1) {
200
+ return startsWithMatches[0];
201
+ }
202
+
203
+ // Find commands that contain the input
204
+ const containsMatches = availableCommands.filter((cmd) =>
205
+ cmd.toLowerCase().includes(normalizedInput),
206
+ );
207
+
208
+ if (containsMatches.length === 1) {
209
+ return containsMatches[0];
210
+ }
211
+
212
+ // Simple Levenshtein distance for close matches
213
+ const closeMatches = availableCommands
214
+ .map((cmd) => ({
215
+ command: cmd,
216
+ distance: levenshteinDistance(normalizedInput, cmd.toLowerCase()),
217
+ }))
218
+ .filter((match) => match.distance <= 2)
219
+ .sort((a, b) => a.distance - b.distance);
220
+
221
+ if (closeMatches.length > 0) {
222
+ const firstMatch = closeMatches[0];
223
+ return firstMatch ? firstMatch.command : undefined;
224
+ }
225
+ return undefined;
226
+ }
227
+
228
+ /**
229
+ * Transform field names to human-readable format
230
+ */
231
+ function humanizeFieldName(field: string): string {
232
+ if (!field) return "Value";
233
+
234
+ return field
235
+ .split(/[._-]/)
236
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
237
+ .join(" ");
238
+ }
239
+
240
+ /**
241
+ * Transform error messages to be more user-friendly
242
+ */
243
+ function humanizeMessage(message: string, field: string): string {
244
+ // Handle "Required" messages
245
+ if (message === "Required" || message.toLowerCase() === "required") {
246
+ return `${humanizeFieldName(field)} is required`;
247
+ }
248
+
249
+ // Handle regex validation messages
250
+ if (message.includes("Invalid input")) {
251
+ return getRegexMessage(field);
252
+ }
253
+
254
+ return message;
255
+ }
256
+
257
+ /**
258
+ * Get user-friendly message for regex validation
259
+ */
260
+ function getRegexMessage(field: string): string {
261
+ const regexMessages: Record<string, string> = {
262
+ name: "Must be lowercase with hyphens only (e.g., 'my-component', 'header-nav')",
263
+ version: "Must follow semantic versioning (e.g., '1.0.0', '2.1.3')",
264
+ slot: "Must be a valid slot name",
265
+ };
266
+
267
+ return regexMessages[field] || "Invalid format";
268
+ }
269
+
270
+ function initializeMatrix(a: string, b: string): number[][] {
271
+ const matrix: number[][] = [];
272
+
273
+ // Initialize matrix with proper dimensions
274
+ for (let i = 0; i <= b.length; i++) {
275
+ const row: number[] = [];
276
+ matrix[i] = row;
277
+ for (let j = 0; j <= a.length; j++) {
278
+ if (i === 0) {
279
+ row[j] = j;
280
+ } else if (j === 0) {
281
+ row[j] = i;
282
+ } else {
283
+ row[j] = 0;
284
+ }
285
+ }
286
+ }
287
+
288
+ return matrix;
289
+ }
290
+
291
+ function computeDistance(
292
+ a: string,
293
+ b: string,
294
+ i: number,
295
+ j: number,
296
+ matrix: number[][],
297
+ ): number {
298
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
299
+ const prevRow = matrix[i - 1];
300
+ return prevRow ? (prevRow[j - 1] ?? 0) : 0;
301
+ }
302
+
303
+ const currentRow = matrix[i];
304
+ const prevRow = matrix[i - 1];
305
+
306
+ const substitution = prevRow
307
+ ? (prevRow[j - 1] ?? 0) + 1
308
+ : Number.MAX_SAFE_INTEGER;
309
+ const insertion = currentRow
310
+ ? (currentRow[j - 1] ?? 0) + 1
311
+ : Number.MAX_SAFE_INTEGER;
312
+ const deletion = prevRow ? (prevRow[j] ?? 0) + 1 : Number.MAX_SAFE_INTEGER;
313
+
314
+ return Math.min(substitution, insertion, deletion);
315
+ }
316
+
317
+ /**
318
+ * Calculate Levenshtein distance between two strings
319
+ */
320
+ function levenshteinDistance(a: string, b: string): number {
321
+ if (a.length === 0) return b.length;
322
+ if (b.length === 0) return a.length;
323
+
324
+ const matrix = initializeMatrix(a, b);
325
+
326
+ for (let i = 1; i <= b.length; i++) {
327
+ const row = matrix[i];
328
+ if (!row) continue;
329
+
330
+ for (let j = 1; j <= a.length; j++) {
331
+ row[j] = computeDistance(a, b, i, j, matrix);
332
+ }
333
+ }
334
+
335
+ const lastRow = matrix[b.length];
336
+ return lastRow ? (lastRow[a.length] ?? 0) : 0;
337
+ }