@ollie-shop/cli 0.3.4 → 1.0.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 (79) hide show
  1. package/.turbo/turbo-build.log +6 -9
  2. package/CHANGELOG.md +21 -0
  3. package/dist/index.js +986 -3956
  4. package/package.json +15 -37
  5. package/src/README.md +126 -0
  6. package/src/cli.tsx +45 -0
  7. package/src/commands/help.tsx +79 -0
  8. package/src/commands/login.tsx +92 -0
  9. package/src/commands/start.tsx +411 -0
  10. package/src/index.tsx +8 -0
  11. package/src/utils/auth.ts +218 -21
  12. package/src/utils/bundle.ts +177 -0
  13. package/src/utils/config.ts +123 -0
  14. package/src/utils/esbuild.ts +533 -0
  15. package/tsconfig.json +10 -15
  16. package/tsup.config.ts +7 -7
  17. package/CLAUDE_CLI.md +0 -265
  18. package/README.md +0 -711
  19. package/__tests__/mocks/console.ts +0 -22
  20. package/__tests__/mocks/core.ts +0 -137
  21. package/__tests__/mocks/index.ts +0 -4
  22. package/__tests__/mocks/inquirer.ts +0 -16
  23. package/__tests__/mocks/progress.ts +0 -19
  24. package/dist/index.d.ts +0 -1
  25. package/src/__tests__/helpers/cli-test-helper.ts +0 -281
  26. package/src/__tests__/mocks/index.ts +0 -142
  27. package/src/actions/component.actions.ts +0 -278
  28. package/src/actions/function.actions.ts +0 -220
  29. package/src/actions/project.actions.ts +0 -131
  30. package/src/actions/version.actions.ts +0 -233
  31. package/src/commands/__tests__/component-validation.test.ts +0 -250
  32. package/src/commands/__tests__/component.test.ts +0 -318
  33. package/src/commands/__tests__/function-validation.test.ts +0 -220
  34. package/src/commands/__tests__/function.test.ts +0 -286
  35. package/src/commands/__tests__/store-version-validation.test.ts +0 -414
  36. package/src/commands/__tests__/store-version.test.ts +0 -402
  37. package/src/commands/component.ts +0 -178
  38. package/src/commands/docs.ts +0 -24
  39. package/src/commands/function.ts +0 -201
  40. package/src/commands/help.ts +0 -18
  41. package/src/commands/index.ts +0 -27
  42. package/src/commands/login.ts +0 -267
  43. package/src/commands/project.ts +0 -107
  44. package/src/commands/store-version.ts +0 -242
  45. package/src/commands/version.ts +0 -51
  46. package/src/commands/whoami.ts +0 -46
  47. package/src/index.ts +0 -116
  48. package/src/prompts/component.prompts.ts +0 -94
  49. package/src/prompts/function.prompts.ts +0 -168
  50. package/src/schemas/command.schema.ts +0 -644
  51. package/src/types/index.ts +0 -183
  52. package/src/utils/__tests__/command-parser.test.ts +0 -159
  53. package/src/utils/__tests__/command-suggestions.test.ts +0 -185
  54. package/src/utils/__tests__/console.test.ts +0 -192
  55. package/src/utils/__tests__/context-detector.test.ts +0 -258
  56. package/src/utils/__tests__/enhanced-error-handler.test.ts +0 -137
  57. package/src/utils/__tests__/error-handler.test.ts +0 -107
  58. package/src/utils/__tests__/rich-progress.test.ts +0 -181
  59. package/src/utils/__tests__/validation-error-formatter.test.ts +0 -175
  60. package/src/utils/__tests__/validation-helpers.test.ts +0 -125
  61. package/src/utils/cli-progress-reporter.ts +0 -84
  62. package/src/utils/command-builder.ts +0 -390
  63. package/src/utils/command-helpers.ts +0 -83
  64. package/src/utils/command-parser.ts +0 -245
  65. package/src/utils/command-suggestions.ts +0 -176
  66. package/src/utils/console.ts +0 -320
  67. package/src/utils/constants.ts +0 -39
  68. package/src/utils/context-detector.ts +0 -177
  69. package/src/utils/deploy-helpers.ts +0 -357
  70. package/src/utils/enhanced-error-handler.ts +0 -264
  71. package/src/utils/error-handler.ts +0 -60
  72. package/src/utils/errors.ts +0 -256
  73. package/src/utils/interactive-builder.ts +0 -325
  74. package/src/utils/rich-progress.ts +0 -331
  75. package/src/utils/store.ts +0 -23
  76. package/src/utils/validation-error-formatter.ts +0 -337
  77. package/src/utils/validation-helpers.ts +0 -325
  78. package/vitest.config.ts +0 -35
  79. package/vitest.setup.ts +0 -29
@@ -1,331 +0,0 @@
1
- import chalk from "chalk";
2
- import cliProgress from "cli-progress";
3
- import type { ProgressParams, ProgressPayload, Spinner } from "../types";
4
- import type { CliConsole } from "./console";
5
-
6
- // Enhanced build progress type for CLI display
7
- interface CLIBuildProgress {
8
- phase: string;
9
- message: string;
10
- progress: number;
11
- metadata?: {
12
- bundleSize?: number;
13
- gzippedSize?: number;
14
- dependencies?: number;
15
- removedExports?: number;
16
- };
17
- }
18
-
19
- interface ProgressStep {
20
- name: string;
21
- emoji: string;
22
- weight: number;
23
- status?: "pending" | "active" | "completed" | "failed";
24
- message?: string;
25
- }
26
-
27
- export class RichProgressReporter {
28
- private multibar: cliProgress.MultiBar;
29
- private bars: Map<string, cliProgress.SingleBar> = new Map();
30
- private steps: ProgressStep[] = [
31
- { name: "Validating", emoji: "🔍", weight: 10 },
32
- { name: "Building", emoji: "🔨", weight: 40 },
33
- { name: "Optimizing", emoji: "⚡", weight: 20 },
34
- { name: "Deploying", emoji: "🚀", weight: 20 },
35
- { name: "Verifying", emoji: "✅", weight: 10 },
36
- ];
37
- private startTime: number = Date.now();
38
- private stats: {
39
- bundleSize?: number;
40
- gzippedSize?: number;
41
- dependencies?: number;
42
- removedExports?: number;
43
- } = {};
44
- private cliConsole: CliConsole;
45
-
46
- constructor(cliConsole: CliConsole) {
47
- this.cliConsole = cliConsole;
48
- // Create a formatter function that matches cli-progress expectations
49
- const formatter = (
50
- options: Record<string, unknown>,
51
- params: Record<string, unknown>,
52
- payload: Record<string, unknown>,
53
- ): string => {
54
- // Safe type conversion with validation
55
- const formatOptions = {
56
- barCompleteString:
57
- typeof options.barCompleteString === "string"
58
- ? options.barCompleteString
59
- : undefined,
60
- barIncompleteString:
61
- typeof options.barIncompleteString === "string"
62
- ? options.barIncompleteString
63
- : undefined,
64
- barsize:
65
- typeof options.barsize === "number" ? options.barsize : undefined,
66
- };
67
-
68
- // Convert params to expected format
69
- const progressParams: ProgressParams = {
70
- progress: typeof params.progress === "number" ? params.progress : 0,
71
- value: typeof params.value === "number" ? params.value : 0,
72
- total: typeof params.total === "number" ? params.total : 0,
73
- eta: typeof params.eta === "number" ? params.eta : 0,
74
- startTime:
75
- typeof params.startTime === "number" ? params.startTime : Date.now(),
76
- stopTime: typeof params.stopTime === "number" ? params.stopTime : null,
77
- duration: typeof params.duration === "number" ? params.duration : 0,
78
- };
79
-
80
- // Convert payload to expected format
81
- const progressPayload: ProgressPayload = {
82
- ...payload,
83
- emoji: typeof payload.emoji === "string" ? payload.emoji : undefined,
84
- step: typeof payload.step === "string" ? payload.step : undefined,
85
- message:
86
- typeof payload.message === "string" ? payload.message : undefined,
87
- startTime:
88
- typeof payload.startTime === "number" ? payload.startTime : undefined,
89
- };
90
-
91
- return this.formatBar(formatOptions, progressParams, progressPayload);
92
- };
93
-
94
- // Create a type-safe wrapper for the cli-progress formatter
95
- // This ensures our formatter function matches the expected signature
96
- const formatFunction: cliProgress.GenericFormatter = (
97
- options: Record<string, unknown>,
98
- params: Record<string, unknown>,
99
- payload: Record<string, unknown>,
100
- ) => formatter(options, params, payload);
101
-
102
- this.multibar = new cliProgress.MultiBar(
103
- {
104
- format: formatFunction,
105
- barCompleteChar: "\u2588",
106
- barIncompleteChar: "\u2591",
107
- hideCursor: true,
108
- clearOnComplete: false,
109
- forceRedraw: true,
110
- },
111
- cliProgress.Presets.shades_classic,
112
- );
113
- }
114
-
115
- /**
116
- * Custom format for progress bars
117
- */
118
- private formatBar(
119
- options: {
120
- barCompleteString?: string;
121
- barIncompleteString?: string;
122
- barsize?: number;
123
- [key: string]: unknown;
124
- },
125
- params: ProgressParams,
126
- payload: ProgressPayload,
127
- ): string {
128
- const barCompleteString = options.barCompleteString || "\u2588";
129
- const barIncompleteString = options.barIncompleteString || "\u2591";
130
- const barsize = options.barsize || 40;
131
-
132
- const bar = barCompleteString.substring(
133
- 0,
134
- Math.round(params.progress * barsize),
135
- );
136
- const empty = barIncompleteString.substring(0, barsize - bar.length);
137
-
138
- const percent = Math.round(params.progress * 100);
139
- const emoji = payload.emoji || "📦";
140
- const step = payload.step || "Processing";
141
- const message = payload.message || "";
142
-
143
- let line = `${emoji} ${step.padEnd(12)} ${bar}${empty} ${percent.toString().padStart(3)}%`;
144
-
145
- if (percent === 100 && payload.startTime) {
146
- const duration = ((Date.now() - payload.startTime) / 1000).toFixed(1);
147
- line += chalk.green(` ✓ ${duration}s`);
148
- } else if (message) {
149
- line += chalk.dim(` | ${message}`);
150
- }
151
-
152
- return line;
153
- }
154
-
155
- /**
156
- * Start progress tracking
157
- */
158
- start(): void {
159
- this.cliConsole.info(chalk.blue.bold("\n🚀 Starting deployment...\n"));
160
-
161
- // Create bars for each step
162
- for (const step of this.steps) {
163
- const bar = this.multibar.create(100, 0, {
164
- emoji: step.emoji,
165
- step: step.name,
166
- startTime: Date.now(),
167
- });
168
- this.bars.set(step.name, bar);
169
- }
170
- }
171
-
172
- private readonly stepMap: Record<string, string> = {
173
- validate: "Validating",
174
- build: "Building",
175
- optimize: "Optimizing",
176
- upload: "Deploying",
177
- verify: "Verifying",
178
- };
179
-
180
- /**
181
- * Update progress based on CLIBuildProgress
182
- */
183
- updateProgress(progress: CLIBuildProgress): void {
184
- const { phase, message, progress: percent } = progress;
185
- const stepName = this.stepMap[phase] || "Processing";
186
-
187
- this.updateBar(stepName, percent, message);
188
- this.updatePreviousSteps(stepName);
189
- this.updateStats(progress.metadata);
190
- }
191
-
192
- private updateBar(stepName: string, percent: number, message?: string): void {
193
- const bar = this.bars.get(stepName);
194
- if (bar) {
195
- bar.update(percent * 100, { message });
196
- }
197
- }
198
-
199
- private updatePreviousSteps(currentStepName: string): void {
200
- let foundCurrent = false;
201
-
202
- for (const step of this.steps) {
203
- if (step.name === currentStepName) {
204
- foundCurrent = true;
205
- step.status = "active";
206
- } else if (!foundCurrent) {
207
- this.markStepComplete(step);
208
- }
209
- }
210
- }
211
-
212
- private markStepComplete(step: ProgressStep): void {
213
- step.status = "completed";
214
- const bar = this.bars.get(step.name);
215
- if (bar) {
216
- bar.update(100);
217
- }
218
- }
219
-
220
- private updateStats(metadata?: CLIBuildProgress["metadata"]): void {
221
- if (!metadata) return;
222
-
223
- if (metadata.bundleSize) {
224
- this.stats.bundleSize = metadata.bundleSize;
225
- }
226
- if (metadata.gzippedSize) {
227
- this.stats.gzippedSize = metadata.gzippedSize;
228
- }
229
- }
230
-
231
- /**
232
- * Stop progress tracking
233
- */
234
- stop(success = true): void {
235
- // Complete all bars if they exist
236
- if (this.bars.size > 0) {
237
- for (const bar of this.bars.values()) {
238
- bar.update(100);
239
- }
240
- }
241
-
242
- this.multibar.stop();
243
-
244
- // Show summary
245
- if (success) {
246
- this.showSuccessSummary();
247
- } else {
248
- this.cliConsole.error(chalk.red("\n❌ Deployment failed\n"));
249
- }
250
- }
251
-
252
- /**
253
- * Show success summary with stats
254
- */
255
- private showSuccessSummary(): void {
256
- const duration = ((Date.now() - this.startTime) / 1000).toFixed(1);
257
-
258
- this.cliConsole.success(
259
- chalk.green.bold("\n✅ Deployment completed successfully!\n"),
260
- );
261
-
262
- if (Object.keys(this.stats).length > 0) {
263
- this.cliConsole.info(chalk.blue("📊 Build Stats:"));
264
-
265
- if (this.stats.bundleSize) {
266
- const sizeKB = (this.stats.bundleSize / 1024).toFixed(1);
267
- const gzipKB = this.stats.gzippedSize
268
- ? (this.stats.gzippedSize / 1024).toFixed(1)
269
- : "?";
270
- this.cliConsole.info(
271
- ` • Bundle size: ${sizeKB}KB (gzipped: ${gzipKB}KB)`,
272
- );
273
- }
274
-
275
- if (this.stats.removedExports) {
276
- this.cliConsole.info(
277
- ` • Tree shaking: Removed ${this.stats.removedExports} unused exports`,
278
- );
279
- }
280
-
281
- if (this.stats.dependencies) {
282
- this.cliConsole.info(` • Dependencies: ${this.stats.dependencies}`);
283
- }
284
-
285
- this.cliConsole.info("");
286
- }
287
-
288
- this.cliConsole.info(chalk.dim(`Total time: ${duration}s`));
289
- }
290
- }
291
-
292
- /**
293
- * Simple progress reporter for single operations
294
- */
295
- export class SimpleProgressReporter {
296
- private spinner: Spinner | null = null;
297
- private startTime: number;
298
-
299
- constructor(private message: string) {
300
- this.startTime = Date.now();
301
- }
302
-
303
- start(): void {
304
- const ora = require("ora");
305
- this.spinner = ora({
306
- text: this.message,
307
- spinner: "dots",
308
- }).start();
309
- }
310
-
311
- update(message: string): void {
312
- if (this.spinner) {
313
- this.spinner.text = message;
314
- }
315
- }
316
-
317
- succeed(message?: string): void {
318
- if (this.spinner) {
319
- const duration = ((Date.now() - this.startTime) / 1000).toFixed(1);
320
- this.spinner.succeed(
321
- message || `${this.message} ${chalk.dim(`(${duration}s)`)}`,
322
- );
323
- }
324
- }
325
-
326
- fail(message?: string): void {
327
- if (this.spinner) {
328
- this.spinner.fail(message || this.message);
329
- }
330
- }
331
- }
@@ -1,23 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- export type OllieConfig = {
5
- storeId: string;
6
- versionId: string;
7
- platform: string;
8
- platformStoreId: string;
9
- sessionId: string;
10
- props: unknown;
11
- theme: Record<string, unknown>;
12
- };
13
-
14
- export async function getOllieConfig(): Promise<OllieConfig | null> {
15
- try {
16
- const configPath = path.join(process.cwd(), "ollie.json");
17
- const raw = await fs.readFile(configPath, "utf-8");
18
- const data: OllieConfig = JSON.parse(raw);
19
- return data;
20
- } catch {
21
- return null;
22
- }
23
- }
@@ -1,337 +0,0 @@
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
- }