@kaiord/cli 0.1.1 → 2.0.1

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.
@@ -1,55 +1,442 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- isTTY
4
- } from "../chunk-TI3WVGXE.js";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_tsx@4.20.6_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js
13
+ import path from "path";
14
+ import { fileURLToPath } from "url";
15
+ var init_esm_shims = __esm({
16
+ "../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_tsx@4.20.6_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js"() {
17
+ "use strict";
18
+ }
19
+ });
20
+
21
+ // src/utils/is-tty.ts
22
+ var isTTY;
23
+ var init_is_tty = __esm({
24
+ "src/utils/is-tty.ts"() {
25
+ "use strict";
26
+ init_esm_shims();
27
+ isTTY = () => {
28
+ return process.stdout.isTTY === true;
29
+ };
30
+ }
31
+ });
32
+
33
+ // src/adapters/logger/structured-logger.ts
34
+ var structured_logger_exports = {};
35
+ __export(structured_logger_exports, {
36
+ createStructuredLogger: () => createStructuredLogger
37
+ });
38
+ import winston from "winston";
39
+ var createStructuredLogger;
40
+ var init_structured_logger = __esm({
41
+ "src/adapters/logger/structured-logger.ts"() {
42
+ "use strict";
43
+ init_esm_shims();
44
+ createStructuredLogger = (options = {}) => {
45
+ const level = options.level || "info";
46
+ const quiet = options.quiet || false;
47
+ const winstonLogger = winston.createLogger({
48
+ level: quiet ? "error" : level,
49
+ format: winston.format.combine(
50
+ winston.format.timestamp(),
51
+ winston.format.json()
52
+ ),
53
+ transports: [
54
+ new winston.transports.Console({
55
+ stderrLevels: ["error", "warn", "info", "debug"]
56
+ })
57
+ ]
58
+ });
59
+ return {
60
+ debug: (message, context) => {
61
+ winstonLogger.debug(message, context);
62
+ },
63
+ info: (message, context) => {
64
+ winstonLogger.info(message, context);
65
+ },
66
+ warn: (message, context) => {
67
+ winstonLogger.warn(message, context);
68
+ },
69
+ error: (message, context) => {
70
+ winstonLogger.error(message, context);
71
+ }
72
+ };
73
+ };
74
+ }
75
+ });
76
+
77
+ // src/adapters/logger/pretty-logger.ts
78
+ var pretty_logger_exports = {};
79
+ __export(pretty_logger_exports, {
80
+ createPrettyLogger: () => createPrettyLogger
81
+ });
82
+ import chalk6 from "chalk";
83
+ var createPrettyLogger;
84
+ var init_pretty_logger = __esm({
85
+ "src/adapters/logger/pretty-logger.ts"() {
86
+ "use strict";
87
+ init_esm_shims();
88
+ init_is_tty();
89
+ createPrettyLogger = (options = {}) => {
90
+ const level = options.level || "info";
91
+ const quiet = options.quiet || false;
92
+ const forceColor = process.env.FORCE_COLOR === "1";
93
+ const useColors = isTTY() || forceColor;
94
+ const levels = ["debug", "info", "warn", "error"];
95
+ const minLevelIndex = levels.indexOf(level);
96
+ const shouldLog = (messageLevel) => {
97
+ if (quiet && messageLevel !== "error") {
98
+ return false;
99
+ }
100
+ const messageLevelIndex = levels.indexOf(messageLevel);
101
+ return messageLevelIndex >= minLevelIndex;
102
+ };
103
+ const formatContext = (context) => {
104
+ if (!context || Object.keys(context).length === 0) {
105
+ return "";
106
+ }
107
+ const contextStr = JSON.stringify(context);
108
+ return " " + (useColors ? chalk6.gray(contextStr) : contextStr);
109
+ };
110
+ return {
111
+ debug: (message, context) => {
112
+ if (shouldLog("debug")) {
113
+ const formatted = `\u{1F41B} ${message}${formatContext(context)}`;
114
+ console.log(useColors ? chalk6.gray(formatted) : formatted);
115
+ }
116
+ },
117
+ info: (message, context) => {
118
+ if (shouldLog("info")) {
119
+ const formatted = `\u2139 ${message}${formatContext(context)}`;
120
+ console.log(useColors ? chalk6.blue(formatted) : formatted);
121
+ }
122
+ },
123
+ warn: (message, context) => {
124
+ if (shouldLog("warn")) {
125
+ const formatted = `\u26A0 ${message}${formatContext(context)}`;
126
+ console.warn(useColors ? chalk6.yellow(formatted) : formatted);
127
+ }
128
+ },
129
+ error: (message, context) => {
130
+ if (shouldLog("error")) {
131
+ const formatted = `\u2716 ${message}${formatContext(context)}`;
132
+ console.error(useColors ? chalk6.red(formatted) : formatted);
133
+ }
134
+ }
135
+ };
136
+ };
137
+ }
138
+ });
5
139
 
6
140
  // src/bin/kaiord.ts
7
- import chalk3 from "chalk";
141
+ init_esm_shims();
8
142
  import { readFileSync } from "fs";
9
- import { dirname as dirname2, join as join2 } from "path";
10
- import { fileURLToPath } from "url";
143
+ import { dirname as dirname2, join as join3 } from "path";
144
+ import { fileURLToPath as fileURLToPath2 } from "url";
11
145
  import yargs from "yargs";
12
146
  import { hideBin } from "yargs/helpers";
13
147
 
14
148
  // src/commands/convert.ts
149
+ init_esm_shims();
150
+
151
+ // src/commands/convert/index.ts
152
+ init_esm_shims();
15
153
  import {
16
- createDefaultProviders,
17
- FitParsingError as FitParsingError2,
18
- KrdValidationError as KrdValidationError2,
19
- ToleranceExceededError as ToleranceExceededError2
154
+ FitParsingError as FitParsingError3,
155
+ KrdValidationError as KrdValidationError3,
156
+ ToleranceExceededError as ToleranceExceededError3
20
157
  } from "@kaiord/core";
21
- import chalk2 from "chalk";
22
- import ora from "ora";
23
- import { basename, join } from "path";
158
+ import { createAllProviders } from "@kaiord/all";
159
+
160
+ // src/utils/config-loader.ts
161
+ init_esm_shims();
162
+ import { readFile } from "fs/promises";
163
+ import { homedir } from "os";
164
+ import { join } from "path";
24
165
  import { z as z2 } from "zod";
25
166
 
167
+ // src/utils/format-detector.ts
168
+ init_esm_shims();
169
+ import { extname } from "path";
170
+ import { z } from "zod";
171
+ var fileFormatSchema = z.enum(["fit", "krd", "tcx", "zwo"]);
172
+ var EXTENSION_TO_FORMAT = {
173
+ ".fit": "fit",
174
+ ".krd": "krd",
175
+ ".tcx": "tcx",
176
+ ".zwo": "zwo"
177
+ };
178
+ var detectFormat = (filePath) => {
179
+ const ext = extname(filePath).toLowerCase();
180
+ return EXTENSION_TO_FORMAT[ext] || null;
181
+ };
182
+
183
+ // src/utils/config-loader.ts
184
+ var configSchema = z2.object({
185
+ // Default formats
186
+ defaultInputFormat: fileFormatSchema.optional(),
187
+ defaultOutputFormat: fileFormatSchema.optional(),
188
+ // Default directories
189
+ defaultOutputDir: z2.string().optional(),
190
+ // Default tolerance config path
191
+ defaultToleranceConfig: z2.string().optional(),
192
+ // Default logging options
193
+ verbose: z2.boolean().optional(),
194
+ quiet: z2.boolean().optional(),
195
+ json: z2.boolean().optional(),
196
+ logFormat: z2.enum(["pretty", "structured"]).optional()
197
+ });
198
+ var loadConfigWithMetadata = async () => {
199
+ const configPaths = [
200
+ join(process.cwd(), ".kaiordrc.json"),
201
+ join(homedir(), ".kaiordrc.json")
202
+ ];
203
+ for (const configPath of configPaths) {
204
+ try {
205
+ const configContent = await readFile(configPath, "utf-8");
206
+ const configJson = JSON.parse(configContent);
207
+ const config = configSchema.parse(configJson);
208
+ return { config, loadedFrom: configPath, searchedPaths: configPaths };
209
+ } catch {
210
+ continue;
211
+ }
212
+ }
213
+ return { config: {}, loadedFrom: null, searchedPaths: configPaths };
214
+ };
215
+ var mergeWithConfig = (cliOptions, config) => {
216
+ const merged = { ...config };
217
+ for (const key in cliOptions) {
218
+ if (cliOptions[key] !== void 0) {
219
+ merged[key] = cliOptions[key];
220
+ }
221
+ }
222
+ return merged;
223
+ };
224
+
26
225
  // src/utils/error-formatter.ts
226
+ init_esm_shims();
227
+
228
+ // src/utils/error-formatter-json.ts
229
+ init_esm_shims();
27
230
  import {
28
231
  FitParsingError,
29
232
  KrdValidationError,
30
233
  ToleranceExceededError
31
234
  } from "@kaiord/core";
235
+
236
+ // src/utils/error-suggestions.ts
237
+ init_esm_shims();
238
+ var ERROR_PATTERNS = [
239
+ {
240
+ pattern: "file not found",
241
+ title: "File not found",
242
+ suggestions: [
243
+ "Check that the file path is correct.",
244
+ "Use 'ls' or 'dir' to verify the file exists.",
245
+ "Ensure the path is relative to current directory or absolute."
246
+ ]
247
+ },
248
+ {
249
+ pattern: "permission denied",
250
+ title: "Permission denied",
251
+ suggestions: [
252
+ "Check file/directory permissions with 'ls -la'.",
253
+ "Ensure you have read/write access to the path.",
254
+ "Try running with appropriate permissions."
255
+ ]
256
+ },
257
+ {
258
+ pattern: "failed to create directory",
259
+ title: "Directory creation failed",
260
+ suggestions: [
261
+ "Check that the parent directory exists.",
262
+ "Verify you have write permissions to the parent directory.",
263
+ "Ensure the path does not conflict with an existing file."
264
+ ]
265
+ },
266
+ {
267
+ pattern: "cannot create directory",
268
+ title: "Directory creation failed",
269
+ suggestions: [
270
+ "Check that the parent directory exists.",
271
+ "Verify you have write permissions to the parent directory.",
272
+ "Ensure the path does not conflict with an existing file."
273
+ ]
274
+ },
275
+ {
276
+ pattern: "cannot use both",
277
+ title: "Invalid argument combination",
278
+ suggestions: [
279
+ "Use --output (-o) for single file conversion.",
280
+ "Use --output-dir for batch conversion with glob patterns."
281
+ ]
282
+ },
283
+ {
284
+ pattern: "no files found matching",
285
+ title: "No files matched",
286
+ suggestions: [
287
+ "Verify the glob pattern matches your files.",
288
+ 'Use quotes around patterns with wildcards: "*.fit"',
289
+ "Check that files exist in the specified directory."
290
+ ]
291
+ },
292
+ {
293
+ pattern: "batch mode requires",
294
+ title: "Invalid argument",
295
+ suggestions: [
296
+ "Add --output-dir to specify where converted files should go.",
297
+ "Add --output-format to specify the target format."
298
+ ]
299
+ },
300
+ {
301
+ pattern: "unable to detect format",
302
+ title: "Invalid argument",
303
+ suggestions: [
304
+ "Use --input-format to explicitly specify the format.",
305
+ "Supported formats: fit, krd, tcx, zwo"
306
+ ]
307
+ }
308
+ ];
309
+ var findMatchingPattern = (msg) => {
310
+ return ERROR_PATTERNS.find((p) => msg.includes(p.pattern));
311
+ };
312
+ var getErrorTitle = (error) => {
313
+ const msg = error.message.toLowerCase();
314
+ const match = findMatchingPattern(msg);
315
+ if (match) return match.title;
316
+ if (error.name === "InvalidArgumentError") return "Invalid argument";
317
+ return "An unexpected error occurred";
318
+ };
319
+ var getSuggestionForError = (error) => {
320
+ const msg = error.message.toLowerCase();
321
+ const match = findMatchingPattern(msg);
322
+ return match?.suggestions ?? null;
323
+ };
324
+
325
+ // src/utils/error-formatter-json.ts
326
+ var toJson = (result) => {
327
+ return JSON.stringify(result, null, 2);
328
+ };
329
+ var formatFitError = (error) => toJson({
330
+ success: false,
331
+ error: {
332
+ type: "FitParsingError",
333
+ message: error.message,
334
+ cause: error.cause ? String(error.cause) : void 0,
335
+ suggestion: "Verify the file is a valid FIT workout file."
336
+ }
337
+ });
338
+ var formatKrdError = (error) => toJson({
339
+ success: false,
340
+ error: {
341
+ type: "KrdValidationError",
342
+ message: error.message,
343
+ errors: error.errors,
344
+ suggestion: "Check the KRD file against the schema."
345
+ }
346
+ });
347
+ var formatToleranceError = (error) => toJson({
348
+ success: false,
349
+ error: {
350
+ type: "ToleranceExceededError",
351
+ message: error.message,
352
+ violations: error.violations,
353
+ suggestion: "Consider adjusting tolerance values if acceptable."
354
+ }
355
+ });
356
+ var formatGenericError = (error) => {
357
+ const suggestion = getSuggestionForError(error);
358
+ return toJson({
359
+ success: false,
360
+ error: {
361
+ type: error.name || "Error",
362
+ message: error.message,
363
+ suggestion: suggestion ? suggestion.join(" ") : void 0
364
+ }
365
+ });
366
+ };
367
+ var formatUnknownError = (error) => toJson({
368
+ success: false,
369
+ error: {
370
+ type: "UnknownError",
371
+ message: String(error)
372
+ }
373
+ });
374
+ var formatErrorAsJson = (error) => {
375
+ if (error instanceof FitParsingError) return formatFitError(error);
376
+ if (error instanceof KrdValidationError) return formatKrdError(error);
377
+ if (error instanceof ToleranceExceededError)
378
+ return formatToleranceError(error);
379
+ if (error instanceof Error) return formatGenericError(error);
380
+ return formatUnknownError(error);
381
+ };
382
+
383
+ // src/utils/error-formatter-pretty.ts
384
+ init_esm_shims();
385
+ init_is_tty();
386
+ import {
387
+ FitParsingError as FitParsingError2,
388
+ KrdValidationError as KrdValidationError2,
389
+ ToleranceExceededError as ToleranceExceededError2
390
+ } from "@kaiord/core";
391
+
392
+ // src/utils/pretty-formatters/index.ts
393
+ init_esm_shims();
394
+
395
+ // src/utils/pretty-formatters/fit-error.ts
396
+ init_esm_shims();
32
397
  import chalk from "chalk";
398
+ var formatFitParsingError = (error, useColors) => {
399
+ const lines = [
400
+ useColors ? chalk.red("\u2716 Error: Failed to parse FIT file") : "\u2716 Error: Failed to parse FIT file",
401
+ "",
402
+ useColors ? chalk.gray("Details:") : "Details:",
403
+ ` ${error.message}`
404
+ ];
405
+ if (error.cause) {
406
+ lines.push("", useColors ? chalk.gray("Cause:") : "Cause:");
407
+ lines.push(` ${String(error.cause)}`);
408
+ }
409
+ lines.push(
410
+ "",
411
+ useColors ? chalk.cyan("Suggestion:") : "Suggestion:",
412
+ " Verify the file is a valid FIT workout file.",
413
+ " Try opening it in Garmin Connect to confirm."
414
+ );
415
+ return lines.join("\n");
416
+ };
417
+
418
+ // src/utils/pretty-formatters/krd-error.ts
419
+ init_esm_shims();
420
+ import chalk3 from "chalk";
421
+
422
+ // src/utils/format-violations.ts
423
+ init_esm_shims();
424
+ init_is_tty();
425
+ import chalk2 from "chalk";
33
426
  var shouldUseColors = () => {
34
427
  return isTTY() || process.env.FORCE_COLOR === "1";
35
428
  };
36
- var formatError = (error, options = {}) => {
37
- if (options.json) {
38
- return formatErrorAsJson(error);
39
- }
40
- return formatErrorAsPretty(error);
41
- };
42
429
  var formatValidationErrors = (errors) => {
43
430
  if (errors.length === 0) {
44
431
  return "";
45
432
  }
46
433
  const useColors = shouldUseColors();
47
434
  const lines = [
48
- useColors ? chalk.red("Validation errors:") : "Validation errors:"
435
+ useColors ? chalk2.red("Validation errors:") : "Validation errors:"
49
436
  ];
50
437
  for (const error of errors) {
51
- const fieldPath = useColors ? chalk.yellow(error.field) : error.field;
52
- const bullet = useColors ? chalk.red("\u2022") : "\u2022";
438
+ const fieldPath = useColors ? chalk2.yellow(error.field) : error.field;
439
+ const bullet = useColors ? chalk2.red("\u2022") : "\u2022";
53
440
  lines.push(` ${bullet} ${fieldPath}: ${error.message}`);
54
441
  }
55
442
  return lines.join("\n");
@@ -60,506 +447,848 @@ var formatToleranceViolations = (violations) => {
60
447
  }
61
448
  const useColors = shouldUseColors();
62
449
  const lines = [
63
- useColors ? chalk.red("Tolerance violations:") : "Tolerance violations:"
450
+ useColors ? chalk2.red("Tolerance violations:") : "Tolerance violations:"
64
451
  ];
65
452
  for (const violation of violations) {
66
- const fieldPath = useColors ? chalk.yellow(violation.field) : violation.field;
67
- const bullet = useColors ? chalk.red("\u2022") : "\u2022";
68
- const expected = violation.expected;
69
- const actual = violation.actual;
453
+ const fieldPath = useColors ? chalk2.yellow(violation.field) : violation.field;
454
+ const bullet = useColors ? chalk2.red("\u2022") : "\u2022";
455
+ const { expected, actual, tolerance } = violation;
70
456
  const deviation = Math.abs(violation.deviation);
71
- const tolerance = violation.tolerance;
72
457
  lines.push(
73
458
  ` ${bullet} ${fieldPath}: expected ${expected}, got ${actual} (deviation: ${deviation}, tolerance: \xB1${tolerance})`
74
459
  );
75
460
  }
76
461
  return lines.join("\n");
77
462
  };
78
- var formatErrorAsPretty = (error) => {
79
- const useColors = shouldUseColors();
80
- if (error instanceof FitParsingError) {
81
- const lines = [
82
- useColors ? chalk.red("\u2716 Error: Failed to parse FIT file") : "\u2716 Error: Failed to parse FIT file",
83
- "",
84
- useColors ? chalk.gray("Details:") : "Details:",
85
- ` ${error.message}`
86
- ];
87
- if (error.cause) {
88
- lines.push("", useColors ? chalk.gray("Cause:") : "Cause:");
89
- lines.push(` ${String(error.cause)}`);
90
- }
463
+
464
+ // src/utils/pretty-formatters/krd-error.ts
465
+ var formatKrdValidationError = (error, useColors) => {
466
+ const lines = [
467
+ useColors ? chalk3.red("\u2716 Error: Invalid KRD format") : "\u2716 Error: Invalid KRD format",
468
+ "",
469
+ formatValidationErrors(error.errors),
470
+ "",
471
+ useColors ? chalk3.cyan("Suggestion:") : "Suggestion:",
472
+ " Check the KRD file against the schema.",
473
+ " Ensure all required fields are present and have valid values."
474
+ ];
475
+ return lines.join("\n");
476
+ };
477
+
478
+ // src/utils/pretty-formatters/tolerance-error.ts
479
+ init_esm_shims();
480
+ import chalk4 from "chalk";
481
+ var formatToleranceError2 = (error, useColors) => {
482
+ const lines = [
483
+ useColors ? chalk4.red("\u2716 Error: Round-trip conversion failed") : "\u2716 Error: Round-trip conversion failed",
484
+ "",
485
+ formatToleranceViolations(error.violations),
486
+ "",
487
+ useColors ? chalk4.cyan("Suggestion:") : "Suggestion:",
488
+ " The conversion may have lost precision.",
489
+ " Consider adjusting tolerance values if the deviations are acceptable."
490
+ ];
491
+ return lines.join("\n");
492
+ };
493
+
494
+ // src/utils/pretty-formatters/generic-error.ts
495
+ init_esm_shims();
496
+ import chalk5 from "chalk";
497
+ var formatGenericError2 = (error, useColors) => {
498
+ const suggestion = getSuggestionForError(error);
499
+ const lines = [
500
+ useColors ? chalk5.red(`\u2716 Error: ${getErrorTitle(error)}`) : `\u2716 Error: ${getErrorTitle(error)}`,
501
+ "",
502
+ useColors ? chalk5.gray("Details:") : "Details:",
503
+ ` ${error.message}`
504
+ ];
505
+ if (suggestion) {
91
506
  lines.push(
92
507
  "",
93
- useColors ? chalk.cyan("Suggestion:") : "Suggestion:",
94
- " Verify the file is a valid FIT workout file.",
95
- " Try opening it in Garmin Connect to confirm."
508
+ useColors ? chalk5.cyan("Suggestion:") : "Suggestion:",
509
+ ...suggestion.map((s) => ` ${s}`)
96
510
  );
97
- return lines.join("\n");
98
511
  }
99
- if (error instanceof KrdValidationError) {
100
- const lines = [
101
- useColors ? chalk.red("\u2716 Error: Invalid KRD format") : "\u2716 Error: Invalid KRD format",
102
- "",
103
- formatValidationErrors(error.errors)
104
- ];
105
- lines.push(
106
- "",
107
- useColors ? chalk.cyan("Suggestion:") : "Suggestion:",
108
- " Check the KRD file against the schema.",
109
- " Ensure all required fields are present and have valid values."
512
+ return lines.join("\n");
513
+ };
514
+ var formatUnknownError2 = (error, useColors) => {
515
+ return [
516
+ useColors ? chalk5.red("\u2716 Error: An unexpected error occurred") : "\u2716 Error: An unexpected error occurred",
517
+ "",
518
+ useColors ? chalk5.gray("Details:") : "Details:",
519
+ ` ${String(error)}`
520
+ ].join("\n");
521
+ };
522
+
523
+ // src/utils/error-formatter-pretty.ts
524
+ var shouldUseColors2 = () => {
525
+ return isTTY() || process.env.FORCE_COLOR === "1";
526
+ };
527
+ var formatErrorAsPretty = (error) => {
528
+ const useColors = shouldUseColors2();
529
+ if (error instanceof FitParsingError2) {
530
+ return formatFitParsingError(error, useColors);
531
+ }
532
+ if (error instanceof KrdValidationError2) {
533
+ return formatKrdValidationError(error, useColors);
534
+ }
535
+ if (error instanceof ToleranceExceededError2) {
536
+ return formatToleranceError2(error, useColors);
537
+ }
538
+ if (error instanceof Error) {
539
+ return formatGenericError2(error, useColors);
540
+ }
541
+ return formatUnknownError2(error, useColors);
542
+ };
543
+
544
+ // src/utils/error-formatter.ts
545
+ var formatError = (error, options = {}) => {
546
+ if (options.json) {
547
+ return formatErrorAsJson(error);
548
+ }
549
+ return formatErrorAsPretty(error);
550
+ };
551
+
552
+ // src/utils/exit-codes.ts
553
+ init_esm_shims();
554
+ var ExitCode = {
555
+ /** Successful execution */
556
+ SUCCESS: 0,
557
+ /** Invalid arguments or usage error */
558
+ INVALID_ARGUMENT: 1,
559
+ /** File not found */
560
+ FILE_NOT_FOUND: 2,
561
+ /** Permission denied */
562
+ PERMISSION_DENIED: 3,
563
+ /** Parsing error (corrupted or invalid file format) */
564
+ PARSING_ERROR: 4,
565
+ /** Validation error (schema validation failed) */
566
+ VALIDATION_ERROR: 5,
567
+ /** Tolerance exceeded (round-trip validation failed) */
568
+ TOLERANCE_EXCEEDED: 6,
569
+ /** Differences found (diff command - files are different, not an error) */
570
+ DIFFERENCES_FOUND: 10,
571
+ /** Partial success (batch operations with some failures) */
572
+ PARTIAL_SUCCESS: 11,
573
+ /** Directory creation failed */
574
+ DIRECTORY_CREATE_ERROR: 12,
575
+ /** Unknown or unhandled error */
576
+ UNKNOWN_ERROR: 99
577
+ };
578
+
579
+ // src/utils/logger-factory.ts
580
+ init_esm_shims();
581
+ var isCI = () => {
582
+ return process.env.CI === "true" || process.env.NODE_ENV === "production" || !process.stdout.isTTY;
583
+ };
584
+ var createLogger = async (options = {}) => {
585
+ const loggerType = options.type || (isCI() ? "structured" : "pretty");
586
+ if (loggerType === "structured") {
587
+ const { createStructuredLogger: createStructuredLogger2 } = await Promise.resolve().then(() => (init_structured_logger(), structured_logger_exports));
588
+ return createStructuredLogger2(options);
589
+ } else {
590
+ const { createPrettyLogger: createPrettyLogger2 } = await Promise.resolve().then(() => (init_pretty_logger(), pretty_logger_exports));
591
+ return createPrettyLogger2(options);
592
+ }
593
+ };
594
+
595
+ // src/commands/convert/batch.ts
596
+ init_esm_shims();
597
+ import chalk7 from "chalk";
598
+ import ora2 from "ora";
599
+ import { basename, join as join2 } from "path";
600
+
601
+ // src/utils/file-handler.ts
602
+ init_esm_shims();
603
+ import { readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
604
+ import { glob } from "glob";
605
+
606
+ // src/utils/directory-handler.ts
607
+ init_esm_shims();
608
+ import { mkdir } from "fs/promises";
609
+ import { dirname } from "path";
610
+
611
+ // src/utils/fs-errors.ts
612
+ init_esm_shims();
613
+ var isNodeSystemError = (error) => {
614
+ return error instanceof Error && "code" in error;
615
+ };
616
+
617
+ // src/utils/directory-handler.ts
618
+ var createOutputDirectory = async (path2) => {
619
+ const dir = dirname(path2);
620
+ try {
621
+ await mkdir(dir, { recursive: true });
622
+ } catch (error) {
623
+ if (isNodeSystemError(error)) {
624
+ if (error.code === "EACCES") {
625
+ throw new Error(`Permission denied creating directory: ${dir}`);
626
+ }
627
+ if (error.code === "ENOTDIR" || error.code === "EEXIST") {
628
+ throw new Error(
629
+ `Cannot create directory (path exists as file): ${dir}`
630
+ );
631
+ }
632
+ }
633
+ throw new Error(`Failed to create directory: ${dir}`);
634
+ }
635
+ };
636
+
637
+ // src/utils/path-security.ts
638
+ init_esm_shims();
639
+ var validatePathSecurity = (inputPath) => {
640
+ if (inputPath.includes("\0")) {
641
+ throw new Error(`Invalid path: null byte detected in ${inputPath}`);
642
+ }
643
+ if (inputPath.includes("|") || inputPath.includes(";")) {
644
+ throw new Error(
645
+ `Invalid path: shell metacharacters detected in ${inputPath}`
110
646
  );
111
- return lines.join("\n");
112
647
  }
113
- if (error instanceof ToleranceExceededError) {
114
- const lines = [
115
- useColors ? chalk.red("\u2716 Error: Round-trip conversion failed") : "\u2716 Error: Round-trip conversion failed",
116
- "",
117
- formatToleranceViolations(error.violations)
118
- ];
119
- lines.push(
120
- "",
121
- useColors ? chalk.cyan("Suggestion:") : "Suggestion:",
122
- " The conversion may have lost precision.",
123
- " Consider adjusting tolerance values if the deviations are acceptable."
648
+ };
649
+
650
+ // src/utils/file-handler.ts
651
+ var readFile2 = async (path2, format) => {
652
+ validatePathSecurity(path2);
653
+ try {
654
+ if (format === "fit") {
655
+ const buffer = await fsReadFile(path2);
656
+ return new Uint8Array(buffer);
657
+ } else {
658
+ return await fsReadFile(path2, "utf-8");
659
+ }
660
+ } catch (error) {
661
+ if (isNodeSystemError(error)) {
662
+ if (error.code === "ENOENT") {
663
+ throw new Error(`File not found: ${path2}`);
664
+ }
665
+ if (error.code === "EACCES") {
666
+ throw new Error(`Permission denied: ${path2}`);
667
+ }
668
+ }
669
+ throw new Error(`Failed to read file: ${path2}`);
670
+ }
671
+ };
672
+ var writeFile = async (path2, data, format) => {
673
+ validatePathSecurity(path2);
674
+ if (format === "fit" && !(data instanceof Uint8Array)) {
675
+ throw new Error("FIT files require Uint8Array data");
676
+ }
677
+ if (format !== "fit" && typeof data !== "string") {
678
+ throw new Error("Text files require string data");
679
+ }
680
+ await createOutputDirectory(path2);
681
+ try {
682
+ if (format === "fit") {
683
+ await fsWriteFile(path2, data);
684
+ } else {
685
+ await fsWriteFile(path2, data, "utf-8");
686
+ }
687
+ } catch (error) {
688
+ if (isNodeSystemError(error)) {
689
+ if (error.code === "EACCES") {
690
+ throw new Error(`Permission denied writing file: ${path2}`);
691
+ }
692
+ if (error.code === "EISDIR") {
693
+ throw new Error(`Cannot write file (path is a directory): ${path2}`);
694
+ }
695
+ }
696
+ throw new Error(`Failed to write file: ${path2}`);
697
+ }
698
+ };
699
+ var findFiles = async (pattern) => {
700
+ const files = await glob(pattern, {
701
+ nodir: true,
702
+ absolute: false
703
+ });
704
+ return files.sort();
705
+ };
706
+
707
+ // src/commands/convert/single-file.ts
708
+ init_esm_shims();
709
+ import ora from "ora";
710
+
711
+ // src/utils/krd-file-loader.ts
712
+ init_esm_shims();
713
+ var loadFileAsKrd = async (filePath, format, providers) => {
714
+ const detectedFormat = format || detectFormat(filePath);
715
+ if (!detectedFormat) {
716
+ throw new Error(
717
+ `Unable to detect format for file: ${filePath}. Supported formats: .fit, .krd, .tcx, .zwo`
124
718
  );
125
- return lines.join("\n");
126
719
  }
127
- if (error instanceof Error) {
128
- return [
129
- useColors ? chalk.red("\u2716 Error: An unexpected error occurred") : "\u2716 Error: An unexpected error occurred",
130
- "",
131
- useColors ? chalk.gray("Details:") : "Details:",
132
- ` ${error.message}`,
133
- "",
134
- ...error.stack ? [
135
- useColors ? chalk.gray("Stack trace:") : "Stack trace:",
136
- ` ${error.stack}`
137
- ] : []
138
- ].join("\n");
720
+ const fileData = await readFile2(filePath, detectedFormat);
721
+ return convertToKrd(fileData, detectedFormat, providers);
722
+ };
723
+ var convertToKrd = async (data, format, providers) => {
724
+ if (format === "fit") {
725
+ if (!(data instanceof Uint8Array)) {
726
+ throw new Error("FIT input must be Uint8Array");
727
+ }
728
+ return providers.convertFitToKrd({ fitBuffer: data });
139
729
  }
140
- return [
141
- useColors ? chalk.red("\u2716 Error: An unexpected error occurred") : "\u2716 Error: An unexpected error occurred",
142
- "",
143
- useColors ? chalk.gray("Details:") : "Details:",
144
- ` ${String(error)}`
145
- ].join("\n");
730
+ if (format === "tcx") {
731
+ if (typeof data !== "string") {
732
+ throw new Error("TCX input must be string");
733
+ }
734
+ return providers.convertTcxToKrd({ tcxString: data });
735
+ }
736
+ if (format === "zwo") {
737
+ if (typeof data !== "string") {
738
+ throw new Error("ZWO input must be string");
739
+ }
740
+ return providers.convertZwiftToKrd({ zwiftString: data });
741
+ }
742
+ if (format === "krd") {
743
+ if (typeof data !== "string") {
744
+ throw new Error("KRD input must be string");
745
+ }
746
+ return JSON.parse(data);
747
+ }
748
+ throw new Error(`Unsupported format: ${format}`);
146
749
  };
147
- var formatErrorAsJson = (error) => {
148
- if (error instanceof FitParsingError) {
149
- return JSON.stringify(
150
- {
151
- success: false,
152
- error: {
153
- type: "FitParsingError",
154
- message: error.message,
155
- cause: error.cause ? String(error.cause) : void 0,
156
- suggestion: "Verify the file is a valid FIT workout file."
157
- }
158
- },
159
- null,
160
- 2
750
+ var convertFromKrd = async (krd, format, providers) => {
751
+ if (format === "fit") {
752
+ return providers.convertKrdToFit({ krd });
753
+ }
754
+ if (format === "tcx") {
755
+ return providers.convertKrdToTcx({ krd });
756
+ }
757
+ if (format === "zwo") {
758
+ return providers.convertKrdToZwift({ krd });
759
+ }
760
+ if (format === "krd") {
761
+ return JSON.stringify(krd, null, 2);
762
+ }
763
+ throw new Error(`Unsupported output format: ${format}`);
764
+ };
765
+
766
+ // src/commands/convert/single-file.ts
767
+ var convertSingleFile = async (inputFile, outputFile, inputFormat, outputFormat, providers) => {
768
+ const krd = await loadFileAsKrd(inputFile, inputFormat, providers);
769
+ const outputData = await convertFromKrd(krd, outputFormat, providers);
770
+ await writeFile(outputFile, outputData, outputFormat);
771
+ };
772
+ var executeSingleFileConversion = async (options, providers, logger) => {
773
+ const inputFormat = options.inputFormat || detectFormat(options.input);
774
+ if (!inputFormat) {
775
+ const error = new Error(
776
+ `Unable to detect input format from file: ${options.input}. Supported formats: .fit, .krd, .tcx, .zwo`
777
+ );
778
+ error.name = "InvalidArgumentError";
779
+ throw error;
780
+ }
781
+ if (!options.output) {
782
+ const error = new Error("Output file is required");
783
+ error.name = "InvalidArgumentError";
784
+ throw error;
785
+ }
786
+ const outputFormat = options.outputFormat || detectFormat(options.output);
787
+ if (!outputFormat) {
788
+ const error = new Error(
789
+ `Unable to detect output format from file: ${options.output}. Supported formats: .fit, .krd, .tcx, .zwo`
161
790
  );
791
+ error.name = "InvalidArgumentError";
792
+ throw error;
162
793
  }
163
- if (error instanceof KrdValidationError) {
164
- return JSON.stringify(
165
- {
794
+ logger.debug("Convert command initialized", {
795
+ input: options.input,
796
+ output: options.output,
797
+ inputFormat,
798
+ outputFormat
799
+ });
800
+ const isTTY2 = process.stdout.isTTY && !options.quiet && !options.json;
801
+ const spinner = isTTY2 ? ora("Converting...").start() : null;
802
+ try {
803
+ await convertSingleFile(
804
+ options.input,
805
+ options.output,
806
+ inputFormat,
807
+ outputFormat,
808
+ providers
809
+ );
810
+ if (options.json) {
811
+ console.log(
812
+ JSON.stringify(
813
+ {
814
+ success: true,
815
+ inputFile: options.input,
816
+ outputFile: options.output,
817
+ inputFormat,
818
+ outputFormat
819
+ },
820
+ null,
821
+ 2
822
+ )
823
+ );
824
+ } else if (spinner) {
825
+ spinner.succeed(
826
+ `Conversion complete: ${options.input} -> ${options.output}`
827
+ );
828
+ } else {
829
+ logger.info("Conversion complete", {
830
+ input: options.input,
831
+ output: options.output
832
+ });
833
+ }
834
+ } catch (error) {
835
+ if (spinner) {
836
+ spinner.fail("Conversion failed");
837
+ }
838
+ throw error;
839
+ }
840
+ };
841
+
842
+ // src/commands/convert/batch.ts
843
+ var executeBatchConversion = async (options, providers, logger) => {
844
+ if (!options.outputDir) {
845
+ const error = new Error("Batch mode requires --output-dir flag");
846
+ error.name = "InvalidArgumentError";
847
+ throw error;
848
+ }
849
+ const outputFormat = options.outputFormat;
850
+ if (!outputFormat) {
851
+ const error = new Error(
852
+ "Batch mode requires --output-format flag to specify target format"
853
+ );
854
+ error.name = "InvalidArgumentError";
855
+ throw error;
856
+ }
857
+ const startTime = Date.now();
858
+ const files = await findFiles(options.input);
859
+ if (files.length === 0) {
860
+ const error = new Error(
861
+ `No files found matching pattern: ${options.input}`
862
+ );
863
+ error.name = "InvalidArgumentError";
864
+ throw error;
865
+ }
866
+ logger.debug("Batch conversion started", {
867
+ pattern: options.input,
868
+ fileCount: files.length,
869
+ outputDir: options.outputDir,
870
+ outputFormat
871
+ });
872
+ const isTTY2 = process.stdout.isTTY && !options.quiet && !options.json;
873
+ const spinner = isTTY2 ? ora2("Processing batch conversion...").start() : null;
874
+ const results = [];
875
+ for (const [index, file] of files.entries()) {
876
+ const fileNum = index + 1;
877
+ const fileName = basename(file);
878
+ if (spinner) {
879
+ spinner.text = `Converting ${fileNum}/${files.length}: ${fileName}`;
880
+ } else {
881
+ logger.info(`Converting ${fileNum}/${files.length}: ${fileName}`);
882
+ }
883
+ try {
884
+ const inputFormat = options.inputFormat || detectFormat(file);
885
+ if (!inputFormat) {
886
+ throw new Error(`Unable to detect format for file: ${file}`);
887
+ }
888
+ const outputFileName = fileName.replace(
889
+ /\.(fit|krd|tcx|zwo)$/i,
890
+ `.${outputFormat}`
891
+ );
892
+ const outputFile = join2(options.outputDir, outputFileName);
893
+ await convertSingleFile(
894
+ file,
895
+ outputFile,
896
+ inputFormat,
897
+ outputFormat,
898
+ providers
899
+ );
900
+ results.push({ success: true, inputFile: file, outputFile });
901
+ } catch (error) {
902
+ results.push({
166
903
  success: false,
167
- error: {
168
- type: "KrdValidationError",
169
- message: error.message,
170
- errors: error.errors,
171
- suggestion: "Check the KRD file against the schema."
172
- }
173
- },
174
- null,
175
- 2
904
+ inputFile: file,
905
+ error: error instanceof Error ? error.message : String(error)
906
+ });
907
+ logger.error(`Failed to convert ${file}`, { error });
908
+ }
909
+ }
910
+ const totalTime = Date.now() - startTime;
911
+ const successful = results.filter((r) => r.success);
912
+ const failed = results.filter((r) => !r.success);
913
+ if (spinner) {
914
+ spinner.stop();
915
+ }
916
+ if (!options.json) {
917
+ console.log("\nBatch conversion complete:");
918
+ console.log(
919
+ chalk7.green(` Successful: ${successful.length}/${files.length}`)
920
+ );
921
+ if (failed.length > 0) {
922
+ console.log(chalk7.red(` Failed: ${failed.length}/${files.length}`));
923
+ }
924
+ console.log(` Total time: ${(totalTime / 1e3).toFixed(2)}s`);
925
+ if (failed.length > 0) {
926
+ console.log(chalk7.red("\nFailed conversions:"));
927
+ for (const result of failed) {
928
+ console.log(chalk7.red(` ${result.inputFile}: ${result.error}`));
929
+ }
930
+ }
931
+ } else {
932
+ console.log(
933
+ JSON.stringify(
934
+ {
935
+ success: failed.length === 0,
936
+ total: files.length,
937
+ successful: successful.length,
938
+ failed: failed.length,
939
+ totalTime,
940
+ results
941
+ },
942
+ null,
943
+ 2
944
+ )
176
945
  );
177
946
  }
178
- if (error instanceof ToleranceExceededError) {
179
- return JSON.stringify(
180
- {
181
- success: false,
182
- error: {
183
- type: "ToleranceExceededError",
184
- message: error.message,
185
- violations: error.violations,
186
- suggestion: "Consider adjusting tolerance values if acceptable."
187
- }
188
- },
189
- null,
190
- 2
191
- );
947
+ if (failed.length === 0) {
948
+ return ExitCode.SUCCESS;
192
949
  }
193
- if (error instanceof Error) {
194
- return JSON.stringify(
195
- {
196
- success: false,
197
- error: {
198
- type: error.name || "Error",
199
- message: error.message,
200
- stack: error.stack
201
- }
202
- },
203
- null,
204
- 2
205
- );
950
+ if (successful.length === 0) {
951
+ return ExitCode.INVALID_ARGUMENT;
206
952
  }
207
- return JSON.stringify(
208
- {
209
- success: false,
210
- error: {
211
- type: "UnknownError",
212
- message: String(error)
213
- }
214
- },
215
- null,
216
- 2
217
- );
953
+ return ExitCode.PARTIAL_SUCCESS;
218
954
  };
219
955
 
220
- // src/utils/file-handler.ts
221
- import {
222
- readFile as fsReadFile,
223
- writeFile as fsWriteFile,
224
- mkdir
225
- } from "fs/promises";
226
- import { glob } from "glob";
227
- import { dirname } from "path";
228
- var readFile = async (path, format) => {
956
+ // src/commands/convert/types.ts
957
+ init_esm_shims();
958
+ import { z as z3 } from "zod";
959
+ var convertOptionsSchema = z3.object({
960
+ input: z3.string(),
961
+ output: z3.string().optional(),
962
+ outputDir: z3.string().optional(),
963
+ inputFormat: fileFormatSchema.optional(),
964
+ outputFormat: fileFormatSchema.optional(),
965
+ verbose: z3.boolean().optional(),
966
+ quiet: z3.boolean().optional(),
967
+ json: z3.boolean().optional(),
968
+ logFormat: z3.enum(["pretty", "structured"]).optional()
969
+ });
970
+
971
+ // src/commands/convert/index.ts
972
+ var isBatchMode = (input) => {
973
+ return input.includes("*") || input.includes("?");
974
+ };
975
+ var convertCommand = async (options) => {
976
+ const configResult = await loadConfigWithMetadata();
977
+ const { config } = configResult;
978
+ const mergedOptions = mergeWithConfig(options, config);
979
+ const optionsWithDefaults = {
980
+ ...mergedOptions,
981
+ inputFormat: mergedOptions.inputFormat || config.defaultInputFormat,
982
+ outputFormat: mergedOptions.outputFormat || config.defaultOutputFormat,
983
+ outputDir: mergedOptions.outputDir || config.defaultOutputDir,
984
+ verbose: mergedOptions.verbose ?? config.verbose,
985
+ quiet: mergedOptions.quiet ?? config.quiet,
986
+ json: mergedOptions.json ?? config.json,
987
+ logFormat: mergedOptions.logFormat || config.logFormat
988
+ };
989
+ const validatedOptions = convertOptionsSchema.parse(optionsWithDefaults);
990
+ if (validatedOptions.output && validatedOptions.outputDir) {
991
+ const error = new Error(
992
+ "Cannot use both --output and --output-dir. Use --output for single file, --output-dir for batch conversion."
993
+ );
994
+ error.name = "InvalidArgumentError";
995
+ throw error;
996
+ }
997
+ const logger = await createLogger({
998
+ type: validatedOptions.logFormat,
999
+ level: validatedOptions.verbose ? "debug" : validatedOptions.quiet ? "error" : "info",
1000
+ quiet: validatedOptions.quiet
1001
+ });
1002
+ if (configResult.loadedFrom) {
1003
+ logger.debug("Configuration loaded", { path: configResult.loadedFrom });
1004
+ } else {
1005
+ logger.debug("No configuration file found", {
1006
+ searchedPaths: configResult.searchedPaths
1007
+ });
1008
+ }
229
1009
  try {
230
- if (format === "fit") {
231
- const buffer = await fsReadFile(path);
232
- return new Uint8Array(buffer);
1010
+ const providers = createAllProviders(logger);
1011
+ if (isBatchMode(validatedOptions.input)) {
1012
+ return await executeBatchConversion(validatedOptions, providers, logger);
233
1013
  } else {
234
- return await fsReadFile(path, "utf-8");
1014
+ await executeSingleFileConversion(validatedOptions, providers, logger);
1015
+ return ExitCode.SUCCESS;
235
1016
  }
236
1017
  } catch (error) {
237
- if (error instanceof Error && "code" in error) {
238
- if (error.code === "ENOENT") {
239
- throw new Error(`File not found: ${path}`);
240
- }
241
- if (error.code === "EACCES") {
242
- throw new Error(`Permission denied: ${path}`);
1018
+ logger.error("Conversion failed", { error });
1019
+ const formattedError = formatError(error, {
1020
+ json: validatedOptions.json
1021
+ });
1022
+ if (validatedOptions.json) {
1023
+ console.log(formattedError);
1024
+ } else {
1025
+ console.error(formattedError);
1026
+ }
1027
+ let exitCode = ExitCode.UNKNOWN_ERROR;
1028
+ if (error instanceof Error) {
1029
+ if (error.message.includes("File not found")) {
1030
+ exitCode = ExitCode.FILE_NOT_FOUND;
1031
+ } else if (error.message.includes("Permission denied")) {
1032
+ exitCode = ExitCode.PERMISSION_DENIED;
1033
+ } else if (error instanceof FitParsingError3) {
1034
+ exitCode = ExitCode.PARSING_ERROR;
1035
+ } else if (error instanceof KrdValidationError3) {
1036
+ exitCode = ExitCode.VALIDATION_ERROR;
1037
+ } else if (error instanceof ToleranceExceededError3) {
1038
+ exitCode = ExitCode.TOLERANCE_EXCEEDED;
1039
+ } else if (error.name === "InvalidArgumentError") {
1040
+ exitCode = ExitCode.INVALID_ARGUMENT;
243
1041
  }
244
1042
  }
245
- throw new Error(`Failed to read file: ${path}`);
1043
+ return exitCode;
246
1044
  }
247
1045
  };
248
- var writeFile = async (path, data, format) => {
249
- try {
250
- const dir = dirname(path);
251
- await mkdir(dir, { recursive: true });
252
- if (format === "fit") {
253
- if (!(data instanceof Uint8Array)) {
254
- throw new Error("FIT files require Uint8Array data");
255
- }
256
- await fsWriteFile(path, data);
257
- } else {
258
- if (typeof data !== "string") {
259
- throw new Error("Text files require string data");
260
- }
261
- await fsWriteFile(path, data, "utf-8");
262
- }
263
- } catch (error) {
264
- if (error instanceof Error && "code" in error) {
265
- if (error.code === "EACCES") {
266
- throw new Error(`Permission denied: ${path}`);
267
- }
1046
+
1047
+ // src/commands/diff.ts
1048
+ init_esm_shims();
1049
+
1050
+ // src/commands/diff/index.ts
1051
+ init_esm_shims();
1052
+ import { createAllProviders as createAllProviders2 } from "@kaiord/all";
1053
+
1054
+ // src/commands/diff/comparators.ts
1055
+ init_esm_shims();
1056
+
1057
+ // src/commands/diff/compare-steps.ts
1058
+ init_esm_shims();
1059
+ var compareSteps = (krd1, krd2) => {
1060
+ const workout1 = krd1.extensions?.workout;
1061
+ const workout2 = krd2.extensions?.workout;
1062
+ const steps1 = workout1?.steps || [];
1063
+ const steps2 = workout2?.steps || [];
1064
+ const differences = [];
1065
+ const maxSteps = Math.max(steps1.length, steps2.length);
1066
+ for (let i = 0; i < maxSteps; i++) {
1067
+ const step1 = steps1[i];
1068
+ const step2 = steps2[i];
1069
+ if (!step1 && step2) {
1070
+ differences.push({
1071
+ stepIndex: i,
1072
+ field: "step",
1073
+ file1Value: void 0,
1074
+ file2Value: step2
1075
+ });
1076
+ continue;
268
1077
  }
269
- if (error instanceof Error && error.message.includes("require")) {
270
- throw error;
1078
+ if (step1 && !step2) {
1079
+ differences.push({
1080
+ stepIndex: i,
1081
+ field: "step",
1082
+ file1Value: step1,
1083
+ file2Value: void 0
1084
+ });
1085
+ continue;
271
1086
  }
272
- throw new Error(`Failed to write file: ${path}`);
1087
+ if (!step1 || !step2) continue;
1088
+ compareStepFields(i, step1, step2, differences);
273
1089
  }
1090
+ return { file1Count: steps1.length, file2Count: steps2.length, differences };
274
1091
  };
275
- var findFiles = async (pattern) => {
276
- const files = await glob(pattern, {
277
- nodir: true,
278
- absolute: false
279
- });
280
- return files.sort();
281
- };
1092
+ var STEP_FIELDS = [
1093
+ "stepIndex",
1094
+ "name",
1095
+ "durationType",
1096
+ "duration",
1097
+ "targetType",
1098
+ "target",
1099
+ "intensity",
1100
+ "notes",
1101
+ "equipment"
1102
+ ];
1103
+ function compareStepFields(stepIndex, step1, step2, differences) {
1104
+ for (const field of STEP_FIELDS) {
1105
+ if (isDifferent(step1[field], step2[field])) {
1106
+ differences.push({
1107
+ stepIndex,
1108
+ field,
1109
+ file1Value: step1[field],
1110
+ file2Value: step2[field]
1111
+ });
1112
+ }
1113
+ }
1114
+ }
282
1115
 
283
- // src/utils/format-detector.ts
284
- import { extname } from "path";
285
- import { z } from "zod";
286
- var fileFormatSchema = z.enum(["fit", "krd", "tcx", "zwo"]);
287
- var EXTENSION_TO_FORMAT = {
288
- ".fit": "fit",
289
- ".krd": "krd",
290
- ".tcx": "tcx",
291
- ".zwo": "zwo"
292
- };
293
- var detectFormat = (filePath) => {
294
- const ext = extname(filePath).toLowerCase();
295
- return EXTENSION_TO_FORMAT[ext] || null;
1116
+ // src/commands/diff/comparators.ts
1117
+ var isDifferent = (value1, value2) => {
1118
+ if (value1 === value2) return false;
1119
+ if (value1 === null || value1 === void 0) return true;
1120
+ if (value2 === null || value2 === void 0) return true;
1121
+ if (typeof value1 === "object" && typeof value2 === "object") {
1122
+ return JSON.stringify(value1) !== JSON.stringify(value2);
1123
+ }
1124
+ return value1 !== value2;
296
1125
  };
297
-
298
- // src/utils/logger-factory.ts
299
- var isCI = () => {
300
- return process.env.CI === "true" || process.env.NODE_ENV === "production" || !process.stdout.isTTY;
1126
+ var compareMetadata = (krd1, krd2) => {
1127
+ const differences = [];
1128
+ const metadataFields = [
1129
+ "created",
1130
+ "manufacturer",
1131
+ "product",
1132
+ "serialNumber",
1133
+ "sport",
1134
+ "subSport"
1135
+ ];
1136
+ for (const field of metadataFields) {
1137
+ const value1 = krd1.metadata[field];
1138
+ const value2 = krd2.metadata[field];
1139
+ if (isDifferent(value1, value2)) {
1140
+ differences.push({ field, file1Value: value1, file2Value: value2 });
1141
+ }
1142
+ }
1143
+ return differences;
301
1144
  };
302
- var createLogger = async (options = {}) => {
303
- const loggerType = options.type || (isCI() ? "structured" : "pretty");
304
- if (loggerType === "structured") {
305
- const { createStructuredLogger } = await import("../structured-logger-HMEQGEES.js");
306
- return createStructuredLogger(options);
307
- } else {
308
- const { createPrettyLogger } = await import("../pretty-logger-P2OWMOGW.js");
309
- return createPrettyLogger(options);
1145
+ var compareExtensions = (krd1, krd2) => {
1146
+ const ext1 = krd1.extensions || {};
1147
+ const ext2 = krd2.extensions || {};
1148
+ const keys1 = Object.keys(ext1);
1149
+ const keys2 = Object.keys(ext2);
1150
+ const allKeys = /* @__PURE__ */ new Set([...keys1, ...keys2]);
1151
+ const differences = [];
1152
+ for (const key of allKeys) {
1153
+ if (isDifferent(ext1[key], ext2[key])) {
1154
+ differences.push({ key, file1Value: ext1[key], file2Value: ext2[key] });
1155
+ }
310
1156
  }
1157
+ return { file1Keys: keys1, file2Keys: keys2, differences };
311
1158
  };
312
1159
 
313
- // src/commands/convert.ts
314
- var convertOptionsSchema = z2.object({
315
- input: z2.string(),
316
- output: z2.string().optional(),
317
- outputDir: z2.string().optional(),
318
- inputFormat: fileFormatSchema.optional(),
319
- outputFormat: fileFormatSchema.optional(),
320
- verbose: z2.boolean().optional(),
321
- quiet: z2.boolean().optional(),
322
- json: z2.boolean().optional(),
323
- logFormat: z2.enum(["pretty", "structured"]).optional()
324
- });
325
- var isBatchMode = (input) => {
326
- return input.includes("*") || input.includes("?");
327
- };
328
- var convertSingleFile = async (inputFile, outputFile, inputFormat, outputFormat, providers) => {
329
- const inputData = await readFile(inputFile, inputFormat);
330
- let krd;
331
- if (inputFormat === "fit") {
332
- if (!(inputData instanceof Uint8Array)) {
333
- throw new Error("FIT input must be Uint8Array");
334
- }
335
- krd = await providers.convertFitToKrd({ fitBuffer: inputData });
336
- } else if (inputFormat === "tcx") {
337
- if (typeof inputData !== "string") {
338
- throw new Error("TCX input must be string");
1160
+ // src/commands/diff/formatter.ts
1161
+ init_esm_shims();
1162
+ import chalk8 from "chalk";
1163
+ var formatDiffPretty = (result, file1, file2) => {
1164
+ if (result.identical) {
1165
+ return chalk8.green("Files are identical");
1166
+ }
1167
+ const lines = [];
1168
+ lines.push(chalk8.yellow(`
1169
+ Comparing: ${file1} vs ${file2}
1170
+ `));
1171
+ if (result.metadataDiff && result.metadataDiff.length > 0) {
1172
+ lines.push(chalk8.bold("Metadata Differences:"));
1173
+ for (const diff of result.metadataDiff) {
1174
+ lines.push(
1175
+ ` ${chalk8.cyan(diff.field)}:`,
1176
+ ` ${chalk8.red("-")} ${JSON.stringify(diff.file1Value)}`,
1177
+ ` ${chalk8.green("+")} ${JSON.stringify(diff.file2Value)}`
1178
+ );
339
1179
  }
340
- krd = await providers.convertTcxToKrd({ tcxString: inputData });
341
- } else if (inputFormat === "zwo") {
342
- if (typeof inputData !== "string") {
343
- throw new Error("ZWO input must be string");
1180
+ lines.push("");
1181
+ }
1182
+ if (result.stepsDiff && result.stepsDiff.differences.length > 0) {
1183
+ lines.push(chalk8.bold("Workout Steps Differences:"));
1184
+ lines.push(
1185
+ ` Step count: ${result.stepsDiff.file1Count} vs ${result.stepsDiff.file2Count}`
1186
+ );
1187
+ for (const diff of result.stepsDiff.differences) {
1188
+ lines.push(
1189
+ ` Step ${diff.stepIndex} - ${chalk8.cyan(diff.field)}:`,
1190
+ ` ${chalk8.red("-")} ${JSON.stringify(diff.file1Value)}`,
1191
+ ` ${chalk8.green("+")} ${JSON.stringify(diff.file2Value)}`
1192
+ );
344
1193
  }
345
- krd = await providers.convertZwiftToKrd({ zwiftString: inputData });
346
- } else if (inputFormat === "krd") {
347
- if (typeof inputData !== "string") {
348
- throw new Error("KRD input must be string");
1194
+ lines.push("");
1195
+ }
1196
+ if (result.extensionsDiff && result.extensionsDiff.differences.length > 0) {
1197
+ lines.push(chalk8.bold("Extensions Differences:"));
1198
+ lines.push(
1199
+ ` Keys in file1: ${result.extensionsDiff.file1Keys.join(", ")}`,
1200
+ ` Keys in file2: ${result.extensionsDiff.file2Keys.join(", ")}`
1201
+ );
1202
+ for (const diff of result.extensionsDiff.differences) {
1203
+ lines.push(
1204
+ ` ${chalk8.cyan(diff.key)}:`,
1205
+ ` ${chalk8.red("-")} ${JSON.stringify(diff.file1Value)}`,
1206
+ ` ${chalk8.green("+")} ${JSON.stringify(diff.file2Value)}`
1207
+ );
349
1208
  }
350
- krd = JSON.parse(inputData);
351
- } else {
352
- throw new Error(`Unsupported input format: ${inputFormat}`);
353
- }
354
- let outputData;
355
- if (outputFormat === "fit") {
356
- outputData = await providers.convertKrdToFit({ krd });
357
- } else if (outputFormat === "tcx") {
358
- outputData = await providers.convertKrdToTcx({ krd });
359
- } else if (outputFormat === "zwo") {
360
- outputData = await providers.convertKrdToZwift({ krd });
361
- } else if (outputFormat === "krd") {
362
- outputData = JSON.stringify(krd, null, 2);
363
- } else {
364
- throw new Error(`Unsupported output format: ${outputFormat}`);
1209
+ lines.push("");
365
1210
  }
366
- await writeFile(outputFile, outputData, outputFormat);
1211
+ return lines.join("\n");
367
1212
  };
368
- var convertCommand = async (options) => {
369
- const validatedOptions = convertOptionsSchema.parse(options);
1213
+
1214
+ // src/commands/diff/types.ts
1215
+ init_esm_shims();
1216
+ import { z as z4 } from "zod";
1217
+ var diffOptionsSchema = z4.object({
1218
+ file1: z4.string(),
1219
+ file2: z4.string(),
1220
+ format1: fileFormatSchema.optional(),
1221
+ format2: fileFormatSchema.optional(),
1222
+ verbose: z4.boolean().optional(),
1223
+ quiet: z4.boolean().optional(),
1224
+ json: z4.boolean().optional(),
1225
+ logFormat: z4.enum(["pretty", "structured"]).optional()
1226
+ });
1227
+
1228
+ // src/commands/diff/index.ts
1229
+ var diffCommand = async (options) => {
1230
+ const validatedOptions = diffOptionsSchema.parse(options);
370
1231
  const logger = await createLogger({
371
1232
  type: validatedOptions.logFormat,
372
1233
  level: validatedOptions.verbose ? "debug" : validatedOptions.quiet ? "error" : "info",
373
1234
  quiet: validatedOptions.quiet
374
1235
  });
1236
+ const configResult = await loadConfigWithMetadata();
1237
+ if (configResult.loadedFrom) {
1238
+ logger.debug("Configuration loaded", { path: configResult.loadedFrom });
1239
+ } else {
1240
+ logger.debug("No configuration file found", {
1241
+ searchedPaths: configResult.searchedPaths
1242
+ });
1243
+ }
375
1244
  try {
376
- const batchMode = isBatchMode(validatedOptions.input);
377
- if (batchMode) {
378
- if (!validatedOptions.outputDir) {
379
- const error = new Error("Batch mode requires --output-dir flag");
380
- error.name = "InvalidArgumentError";
381
- throw error;
382
- }
383
- const outputFormat = validatedOptions.outputFormat;
384
- if (!outputFormat) {
385
- const error = new Error(
386
- "Batch mode requires --output-format flag to specify target format"
387
- );
388
- error.name = "InvalidArgumentError";
389
- throw error;
390
- }
391
- const providers = createDefaultProviders(logger);
392
- const startTime = Date.now();
393
- const files = await findFiles(validatedOptions.input);
394
- if (files.length === 0) {
395
- const error = new Error(
396
- `No files found matching pattern: ${validatedOptions.input}`
397
- );
398
- error.name = "InvalidArgumentError";
399
- throw error;
400
- }
401
- logger.debug("Batch conversion started", {
402
- pattern: validatedOptions.input,
403
- fileCount: files.length,
404
- outputDir: validatedOptions.outputDir,
405
- outputFormat
406
- });
407
- const isTTY2 = process.stdout.isTTY && !validatedOptions.quiet && !validatedOptions.json;
408
- const spinner = isTTY2 ? ora("Processing batch conversion...").start() : null;
409
- const results = [];
410
- for (const [index, file] of files.entries()) {
411
- const fileNum = index + 1;
412
- const fileName = basename(file);
413
- if (spinner) {
414
- spinner.text = `Converting ${fileNum}/${files.length}: ${fileName}`;
415
- } else {
416
- logger.info(`Converting ${fileNum}/${files.length}: ${fileName}`);
417
- }
418
- try {
419
- const inputFormat = validatedOptions.inputFormat || detectFormat(file);
420
- if (!inputFormat) {
421
- throw new Error(`Unable to detect format for file: ${file}`);
422
- }
423
- const outputFileName = fileName.replace(
424
- /\.(fit|krd|tcx|zwo)$/i,
425
- `.${outputFormat}`
426
- );
427
- const outputFile = join(validatedOptions.outputDir, outputFileName);
428
- await convertSingleFile(
429
- file,
430
- outputFile,
431
- inputFormat,
432
- outputFormat,
433
- providers
434
- );
435
- results.push({
1245
+ const providers = createAllProviders2(logger);
1246
+ logger.debug("Loading files for comparison", {
1247
+ file1: validatedOptions.file1,
1248
+ file2: validatedOptions.file2
1249
+ });
1250
+ const krd1 = await loadFileAsKrd(
1251
+ validatedOptions.file1,
1252
+ validatedOptions.format1,
1253
+ providers
1254
+ );
1255
+ const krd2 = await loadFileAsKrd(
1256
+ validatedOptions.file2,
1257
+ validatedOptions.format2,
1258
+ providers
1259
+ );
1260
+ logger.debug("Files loaded successfully");
1261
+ const metadataDiff = compareMetadata(krd1, krd2);
1262
+ const stepsDiff = compareSteps(krd1, krd2);
1263
+ const extensionsDiff = compareExtensions(krd1, krd2);
1264
+ const identical = metadataDiff.length === 0 && stepsDiff.differences.length === 0 && extensionsDiff.differences.length === 0;
1265
+ const result = {
1266
+ identical,
1267
+ metadataDiff: metadataDiff.length > 0 ? metadataDiff : void 0,
1268
+ stepsDiff: stepsDiff.differences.length > 0 ? stepsDiff : void 0,
1269
+ extensionsDiff: extensionsDiff.differences.length > 0 ? extensionsDiff : void 0
1270
+ };
1271
+ if (validatedOptions.json) {
1272
+ console.log(
1273
+ JSON.stringify(
1274
+ {
436
1275
  success: true,
437
- inputFile: file,
438
- outputFile
439
- });
440
- } catch (error) {
441
- results.push({
442
- success: false,
443
- inputFile: file,
444
- error: error instanceof Error ? error.message : String(error)
445
- });
446
- logger.error(`Failed to convert ${file}`, { error });
447
- }
448
- }
449
- const totalTime = Date.now() - startTime;
450
- const successful = results.filter((r) => r.success);
451
- const failed = results.filter((r) => !r.success);
452
- if (spinner) {
453
- spinner.stop();
454
- }
455
- if (!validatedOptions.json) {
456
- console.log("\nBatch conversion complete:");
457
- console.log(
458
- chalk2.green(` \u2713 Successful: ${successful.length}/${files.length}`)
459
- );
460
- if (failed.length > 0) {
461
- console.log(
462
- chalk2.red(` \u2717 Failed: ${failed.length}/${files.length}`)
463
- );
464
- }
465
- console.log(` Total time: ${(totalTime / 1e3).toFixed(2)}s`);
466
- if (failed.length > 0) {
467
- console.log(chalk2.red("\nFailed conversions:"));
468
- for (const result of failed) {
469
- console.log(chalk2.red(` ${result.inputFile}: ${result.error}`));
470
- }
471
- }
472
- } else {
473
- console.log(
474
- JSON.stringify(
475
- {
476
- success: failed.length === 0,
477
- total: files.length,
478
- successful: successful.length,
479
- failed: failed.length,
480
- totalTime,
481
- results
482
- },
483
- null,
484
- 2
485
- )
486
- );
487
- }
488
- if (failed.length > 0) {
489
- process.exit(1);
490
- }
1276
+ file1: validatedOptions.file1,
1277
+ file2: validatedOptions.file2,
1278
+ ...result
1279
+ },
1280
+ null,
1281
+ 2
1282
+ )
1283
+ );
491
1284
  } else {
492
- const inputFormat = validatedOptions.inputFormat || detectFormat(validatedOptions.input);
493
- if (!inputFormat) {
494
- const error = new Error(
495
- `Unable to detect input format from file: ${validatedOptions.input}. Supported formats: .fit, .krd, .tcx, .zwo`
496
- );
497
- error.name = "InvalidArgumentError";
498
- throw error;
499
- }
500
- if (!validatedOptions.output) {
501
- const error = new Error("Output file is required");
502
- error.name = "InvalidArgumentError";
503
- throw error;
504
- }
505
- const outputFormat = validatedOptions.outputFormat || detectFormat(validatedOptions.output);
506
- if (!outputFormat) {
507
- const error = new Error(
508
- `Unable to detect output format from file: ${validatedOptions.output}. Supported formats: .fit, .krd, .tcx, .zwo`
509
- );
510
- error.name = "InvalidArgumentError";
511
- throw error;
512
- }
513
- const providers = createDefaultProviders(logger);
514
- logger.debug("Convert command initialized", {
515
- input: validatedOptions.input,
516
- output: validatedOptions.output,
517
- inputFormat,
518
- outputFormat
519
- });
520
- const isTTY2 = process.stdout.isTTY && !validatedOptions.quiet && !validatedOptions.json;
521
- const spinner = isTTY2 ? ora("Converting...").start() : null;
522
- try {
523
- await convertSingleFile(
524
- validatedOptions.input,
525
- validatedOptions.output,
526
- inputFormat,
527
- outputFormat,
528
- providers
529
- );
530
- if (validatedOptions.json) {
531
- console.log(
532
- JSON.stringify(
533
- {
534
- success: true,
535
- inputFile: validatedOptions.input,
536
- outputFile: validatedOptions.output,
537
- inputFormat,
538
- outputFormat
539
- },
540
- null,
541
- 2
542
- )
543
- );
544
- } else if (spinner) {
545
- spinner.succeed(
546
- `Conversion complete: ${validatedOptions.input} \u2192 ${validatedOptions.output}`
547
- );
548
- } else {
549
- logger.info("Conversion complete", {
550
- input: validatedOptions.input,
551
- output: validatedOptions.output
552
- });
553
- }
554
- } catch (error) {
555
- if (spinner) {
556
- spinner.fail("Conversion failed");
557
- }
558
- throw error;
559
- }
1285
+ console.log(
1286
+ formatDiffPretty(result, validatedOptions.file1, validatedOptions.file2)
1287
+ );
560
1288
  }
1289
+ return identical ? ExitCode.SUCCESS : ExitCode.DIFFERENCES_FOUND;
561
1290
  } catch (error) {
562
- logger.error("Conversion failed", { error });
1291
+ logger.error("Diff command failed", { error });
563
1292
  const formattedError = formatError(error, {
564
1293
  json: validatedOptions.json
565
1294
  });
@@ -568,53 +1297,61 @@ var convertCommand = async (options) => {
568
1297
  } else {
569
1298
  console.error(formattedError);
570
1299
  }
571
- let exitCode = 1;
572
- if (error instanceof Error) {
573
- if (error.message.includes("File not found")) {
574
- exitCode = 1;
575
- } else if (error.message.includes("Permission denied")) {
576
- exitCode = 1;
577
- } else if (error instanceof FitParsingError2) {
578
- exitCode = 1;
579
- } else if (error instanceof KrdValidationError2) {
580
- exitCode = 1;
581
- } else if (error instanceof ToleranceExceededError2) {
582
- exitCode = 1;
583
- } else if (error.name === "InvalidArgumentError") {
584
- exitCode = 1;
585
- }
586
- }
587
- process.exit(exitCode);
1300
+ return ExitCode.UNKNOWN_ERROR;
588
1301
  }
589
1302
  };
590
1303
 
591
1304
  // src/commands/validate.ts
1305
+ init_esm_shims();
592
1306
  import {
593
- createDefaultProviders as createDefaultProviders2,
594
1307
  createToleranceChecker,
595
1308
  toleranceConfigSchema,
596
1309
  validateRoundTrip
597
1310
  } from "@kaiord/core";
1311
+ import { createAllProviders as createAllProviders3 } from "@kaiord/all";
598
1312
  import { readFile as fsReadFile2 } from "fs/promises";
599
- import ora2 from "ora";
600
- import { z as z3 } from "zod";
601
- var validateOptionsSchema = z3.object({
602
- input: z3.string(),
603
- toleranceConfig: z3.string().optional(),
604
- verbose: z3.boolean().optional(),
605
- quiet: z3.boolean().optional(),
606
- json: z3.boolean().optional(),
607
- logFormat: z3.enum(["pretty", "json"]).optional()
1313
+ import ora3 from "ora";
1314
+ import { z as z5 } from "zod";
1315
+ var validateOptionsSchema = z5.object({
1316
+ input: z5.string(),
1317
+ toleranceConfig: z5.string().optional(),
1318
+ verbose: z5.boolean().optional(),
1319
+ quiet: z5.boolean().optional(),
1320
+ json: z5.boolean().optional(),
1321
+ logFormat: z5.enum(["pretty", "json"]).optional()
608
1322
  });
609
1323
  var validateCommand = async (options) => {
610
- const opts = validateOptionsSchema.parse(options);
611
- const loggerType = opts.logFormat === "json" ? "structured" : opts.logFormat;
612
- const logger = await createLogger({
613
- type: loggerType,
614
- level: opts.verbose ? "debug" : opts.quiet ? "error" : "info",
615
- quiet: opts.quiet
616
- });
1324
+ let logger;
1325
+ let spinner = null;
617
1326
  try {
1327
+ const configResult = await loadConfigWithMetadata();
1328
+ const { config } = configResult;
1329
+ const mergedOptions = mergeWithConfig(
1330
+ options,
1331
+ config
1332
+ );
1333
+ const optionsWithDefaults = {
1334
+ ...mergedOptions,
1335
+ toleranceConfig: mergedOptions.toleranceConfig || config.defaultToleranceConfig,
1336
+ verbose: mergedOptions.verbose ?? config.verbose,
1337
+ quiet: mergedOptions.quiet ?? config.quiet,
1338
+ json: mergedOptions.json ?? config.json,
1339
+ logFormat: mergedOptions.logFormat || config.logFormat
1340
+ };
1341
+ const opts = validateOptionsSchema.parse(optionsWithDefaults);
1342
+ const loggerType = opts.logFormat === "json" ? "structured" : opts.logFormat;
1343
+ logger = await createLogger({
1344
+ type: loggerType,
1345
+ level: opts.verbose ? "debug" : opts.quiet ? "error" : "info",
1346
+ quiet: opts.quiet
1347
+ });
1348
+ if (configResult.loadedFrom) {
1349
+ logger.debug("Configuration loaded", { path: configResult.loadedFrom });
1350
+ } else {
1351
+ logger.debug("No configuration file found", {
1352
+ searchedPaths: configResult.searchedPaths
1353
+ });
1354
+ }
618
1355
  const format = detectFormat(opts.input);
619
1356
  if (!format) {
620
1357
  throw new Error(`Unable to detect format from file: ${opts.input}`);
@@ -624,24 +1361,24 @@ var validateCommand = async (options) => {
624
1361
  `Validation currently only supports FIT files. Got: ${format}`
625
1362
  );
626
1363
  }
627
- logger.debug("Reading input file", { path: opts.input, format });
628
- const inputData = await readFile(opts.input, format);
1364
+ logger?.debug("Reading input file", { path: opts.input, format });
1365
+ const inputData = await readFile2(opts.input, format);
629
1366
  if (typeof inputData === "string") {
630
1367
  throw new Error("Expected binary data for FIT file");
631
1368
  }
632
1369
  let toleranceConfig;
633
1370
  if (opts.toleranceConfig) {
634
- logger.debug("Loading custom tolerance config", {
1371
+ logger?.debug("Loading custom tolerance config", {
635
1372
  path: opts.toleranceConfig
636
1373
  });
637
1374
  const configContent = await fsReadFile2(opts.toleranceConfig, "utf-8");
638
1375
  const configJson = JSON.parse(configContent);
639
1376
  toleranceConfig = toleranceConfigSchema.parse(configJson);
640
- logger.debug("Custom tolerance config loaded", {
1377
+ logger?.debug("Custom tolerance config loaded", {
641
1378
  config: toleranceConfig
642
1379
  });
643
1380
  }
644
- const providers = createDefaultProviders2(logger);
1381
+ const providers = createAllProviders3(logger);
645
1382
  const toleranceChecker = toleranceConfig ? createToleranceChecker(toleranceConfig) : providers.toleranceChecker;
646
1383
  const roundTripValidator = validateRoundTrip(
647
1384
  providers.fitReader,
@@ -649,8 +1386,8 @@ var validateCommand = async (options) => {
649
1386
  toleranceChecker,
650
1387
  logger
651
1388
  );
652
- const spinner = opts.quiet || opts.json ? null : ora2("Validating round-trip conversion...").start();
653
- logger.info("Starting round-trip validation", { file: opts.input });
1389
+ spinner = opts.quiet || opts.json ? null : ora3("Validating round-trip conversion...").start();
1390
+ logger?.info("Starting round-trip validation", { file: opts.input });
654
1391
  const violations = await roundTripValidator.validateFitToKrdToFit({
655
1392
  originalFit: inputData
656
1393
  });
@@ -664,7 +1401,7 @@ var validateCommand = async (options) => {
664
1401
  }
665
1402
  }
666
1403
  if (violations.length === 0) {
667
- logger.info("Round-trip validation passed");
1404
+ logger?.info("Round-trip validation passed");
668
1405
  if (opts.json) {
669
1406
  console.log(
670
1407
  JSON.stringify(
@@ -679,11 +1416,11 @@ var validateCommand = async (options) => {
679
1416
  )
680
1417
  );
681
1418
  } else if (!opts.quiet) {
682
- console.log("\u2713 Round-trip validation passed");
1419
+ console.log("Round-trip validation passed");
683
1420
  }
684
- process.exit(0);
1421
+ return ExitCode.SUCCESS;
685
1422
  } else {
686
- logger.warn("Round-trip validation failed", {
1423
+ logger?.warn("Round-trip validation failed", {
687
1424
  violationCount: violations.length
688
1425
  });
689
1426
  if (opts.json) {
@@ -700,19 +1437,27 @@ var validateCommand = async (options) => {
700
1437
  )
701
1438
  );
702
1439
  } else {
703
- console.error("\u2716 Round-trip validation failed\n");
1440
+ console.error("Round-trip validation failed\n");
704
1441
  console.error(formatToleranceViolations(violations));
705
1442
  }
706
- process.exit(1);
1443
+ return ExitCode.TOLERANCE_EXCEEDED;
707
1444
  }
708
1445
  } catch (error) {
709
- logger.error("Validation failed", { error });
710
- if (opts.json) {
1446
+ logger?.error("Validation failed", { error });
1447
+ let jsonOutput = false;
1448
+ try {
1449
+ const opts = validateOptionsSchema.parse(options);
1450
+ jsonOutput = opts.json || false;
1451
+ } catch {
1452
+ }
1453
+ if (jsonOutput) {
1454
+ const errorObj = formatError(error, { json: true });
1455
+ const errorData = typeof errorObj === "string" ? JSON.parse(errorObj) : errorObj;
711
1456
  console.log(
712
1457
  JSON.stringify(
713
1458
  {
714
1459
  success: false,
715
- error: formatError(error, { json: true })
1460
+ error: errorData
716
1461
  },
717
1462
  null,
718
1463
  2
@@ -721,43 +1466,28 @@ var validateCommand = async (options) => {
721
1466
  } else {
722
1467
  console.error(formatError(error, { json: false }));
723
1468
  }
724
- process.exit(1);
1469
+ if (error instanceof Error) {
1470
+ if (error.message.includes("File not found")) {
1471
+ return ExitCode.FILE_NOT_FOUND;
1472
+ }
1473
+ if (error.message.includes("only supports") || error.message.includes("Unable to detect")) {
1474
+ return ExitCode.INVALID_ARGUMENT;
1475
+ }
1476
+ }
1477
+ return ExitCode.UNKNOWN_ERROR;
1478
+ } finally {
1479
+ spinner?.stop();
725
1480
  }
726
1481
  };
727
1482
 
728
1483
  // src/bin/kaiord.ts
729
- var __filename2 = fileURLToPath(import.meta.url);
1484
+ var __filename2 = fileURLToPath2(import.meta.url);
730
1485
  var __dirname2 = dirname2(__filename2);
731
- var packageJsonPath = __dirname2.includes("/dist") ? join2(__dirname2, "../../package.json") : join2(__dirname2, "../../package.json");
1486
+ var packageJsonPath = __dirname2.includes("/dist") ? join3(__dirname2, "../../package.json") : join3(__dirname2, "../../package.json");
732
1487
  var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
733
1488
  var version = packageJson.version;
734
- var showKiroEasterEgg = () => {
735
- console.log(
736
- chalk3.cyan(`
737
- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
738
- \u2551 \u2551
739
- \u2551 \u{1F47B} Built with Kiro AI during Kiroween Hackathon \u{1F47B} \u2551
740
- \u2551 \u2551
741
- \u2551 Kiro helped design, architect, and implement this \u2551
742
- \u2551 entire CLI tool through spec-driven development. \u2551
743
- \u2551 \u2551
744
- \u2551 Learn more about Kiroween: \u2551
745
- \u2551 \u{1F449} http://kiroween.devpost.com/ \u2551
746
- \u2551 \u2551
747
- \u2551 Kiro: Your AI pair programmer for building better \u2551
748
- \u2551 software, faster. \u{1F680} \u2551
749
- \u2551 \u2551
750
- \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
751
- `)
752
- );
753
- process.exit(0);
754
- };
755
1489
  var main = async () => {
756
1490
  try {
757
- const args = process.argv.slice(2);
758
- if (args.includes("--kiro") || args.includes("--kiroween")) {
759
- showKiroEasterEgg();
760
- }
761
1491
  await yargs(hideBin(process.argv)).scriptName("kaiord").usage("$0 <command> [options]").command(
762
1492
  "convert",
763
1493
  "Convert workout files between formats",
@@ -794,7 +1524,7 @@ var main = async () => {
794
1524
  );
795
1525
  },
796
1526
  async (argv) => {
797
- await convertCommand({
1527
+ const exitCode = await convertCommand({
798
1528
  input: argv.input,
799
1529
  output: argv.output,
800
1530
  outputDir: argv.outputDir,
@@ -805,10 +1535,13 @@ var main = async () => {
805
1535
  json: argv.json,
806
1536
  logFormat: argv.logFormat
807
1537
  });
1538
+ if (exitCode !== 0) {
1539
+ process.exit(exitCode);
1540
+ }
808
1541
  }
809
1542
  ).command(
810
1543
  "validate",
811
- "Validate round-trip conversion of workout files",
1544
+ "Validate round-trip conversion of FIT files (FIT only)",
812
1545
  (yargs2) => {
813
1546
  return yargs2.option("input", {
814
1547
  alias: "i",
@@ -827,7 +1560,7 @@ var main = async () => {
827
1560
  );
828
1561
  },
829
1562
  async (argv) => {
830
- await validateCommand({
1563
+ const exitCode = await validateCommand({
831
1564
  input: argv.input,
832
1565
  toleranceConfig: argv.toleranceConfig,
833
1566
  verbose: argv.verbose,
@@ -835,6 +1568,60 @@ var main = async () => {
835
1568
  json: argv.json,
836
1569
  logFormat: argv.logFormat
837
1570
  });
1571
+ if (exitCode !== ExitCode.SUCCESS) {
1572
+ process.exit(exitCode);
1573
+ }
1574
+ }
1575
+ ).command(
1576
+ "diff",
1577
+ "Compare two workout files and show differences",
1578
+ (yargs2) => {
1579
+ return yargs2.option("file1", {
1580
+ alias: "1",
1581
+ type: "string",
1582
+ description: "First file to compare",
1583
+ demandOption: true
1584
+ }).option("file2", {
1585
+ alias: "2",
1586
+ type: "string",
1587
+ description: "Second file to compare",
1588
+ demandOption: true
1589
+ }).option("format1", {
1590
+ type: "string",
1591
+ choices: ["fit", "krd", "tcx", "zwo"],
1592
+ description: "Override format detection for first file"
1593
+ }).option("format2", {
1594
+ type: "string",
1595
+ choices: ["fit", "krd", "tcx", "zwo"],
1596
+ description: "Override format detection for second file"
1597
+ }).example(
1598
+ "$0 diff --file1 workout1.fit --file2 workout2.fit",
1599
+ "Compare two FIT files"
1600
+ ).example(
1601
+ "$0 diff --file1 workout.fit --file2 workout.krd",
1602
+ "Compare FIT and KRD files"
1603
+ ).example(
1604
+ "$0 diff --file1 workout1.krd --file2 workout2.krd --json",
1605
+ "Compare with JSON output"
1606
+ );
1607
+ },
1608
+ async (argv) => {
1609
+ const exitCode = await diffCommand({
1610
+ file1: argv.file1,
1611
+ file2: argv.file2,
1612
+ format1: argv.format1,
1613
+ format2: argv.format2,
1614
+ verbose: argv.verbose,
1615
+ quiet: argv.quiet,
1616
+ json: argv.json,
1617
+ logFormat: argv.logFormat
1618
+ });
1619
+ if (exitCode !== ExitCode.SUCCESS && exitCode !== ExitCode.DIFFERENCES_FOUND) {
1620
+ process.exit(exitCode);
1621
+ }
1622
+ if (exitCode === ExitCode.DIFFERENCES_FOUND) {
1623
+ process.exit(exitCode);
1624
+ }
838
1625
  }
839
1626
  ).option("verbose", {
840
1627
  type: "boolean",
@@ -860,25 +1647,27 @@ var main = async () => {
860
1647
  if (error && typeof error === "object" && "name" in error) {
861
1648
  const errorName = error.name;
862
1649
  if (errorName === "FitParsingError") {
863
- process.exit(4);
1650
+ process.exit(ExitCode.PARSING_ERROR);
864
1651
  } else if (errorName === "KrdValidationError") {
865
- process.exit(5);
1652
+ process.exit(ExitCode.VALIDATION_ERROR);
866
1653
  } else if (errorName === "ToleranceExceededError") {
867
- process.exit(6);
1654
+ process.exit(ExitCode.TOLERANCE_EXCEEDED);
1655
+ } else if (errorName === "InvalidArgumentError") {
1656
+ process.exit(ExitCode.INVALID_ARGUMENT);
868
1657
  }
869
1658
  }
870
- process.exit(99);
1659
+ process.exit(ExitCode.UNKNOWN_ERROR);
871
1660
  }
872
1661
  };
873
1662
  process.on("unhandledRejection", (reason) => {
874
1663
  console.error("Unhandled rejection:", reason);
875
- process.exit(99);
1664
+ process.exit(ExitCode.UNKNOWN_ERROR);
876
1665
  });
877
1666
  process.on("uncaughtException", (error) => {
878
1667
  console.error("Uncaught exception:", error);
879
- process.exit(99);
1668
+ process.exit(ExitCode.UNKNOWN_ERROR);
880
1669
  });
881
1670
  main().catch((error) => {
882
1671
  console.error("Fatal error:", error);
883
- process.exit(99);
1672
+ process.exit(ExitCode.UNKNOWN_ERROR);
884
1673
  });