@kaiord/cli 0.1.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.
- package/README.md +227 -0
- package/dist/bin/kaiord.js +884 -0
- package/dist/chunk-TI3WVGXE.js +10 -0
- package/dist/pretty-logger-P2OWMOGW.js +58 -0
- package/dist/structured-logger-HMEQGEES.js +37 -0
- package/package.json +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# @kaiord/cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for Kaiord workout file conversion. Convert workout files between FIT, KRD, TCX, and ZWO formats with ease.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install globally using npm or pnpm:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @kaiord/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
or
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add -g @kaiord/cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
After installation, the `kaiord` command will be available globally.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Convert Command
|
|
24
|
+
|
|
25
|
+
Convert workout files between different formats.
|
|
26
|
+
|
|
27
|
+
#### Basic Usage
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
kaiord convert --input workout.fit --output workout.krd
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
#### FIT to KRD
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
kaiord convert --input workout.fit --output workout.krd
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### KRD to FIT
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
kaiord convert --input workout.krd --output workout.fit
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
#### Batch Conversion
|
|
46
|
+
|
|
47
|
+
Convert multiple files using glob patterns:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
kaiord convert --input "workouts/*.fit" --output-dir converted/
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### Format Override
|
|
54
|
+
|
|
55
|
+
Override automatic format detection:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
kaiord convert --input data.bin --input-format fit --output workout.krd
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Validate Command
|
|
62
|
+
|
|
63
|
+
Perform round-trip validation to verify data integrity.
|
|
64
|
+
|
|
65
|
+
#### Basic Validation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
kaiord validate --input workout.fit
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### Custom Tolerances
|
|
72
|
+
|
|
73
|
+
Use a custom tolerance configuration file:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
kaiord validate --input workout.fit --tolerance-config tolerance.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Example `tolerance.json`:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"time": { "absolute": 1, "unit": "seconds" },
|
|
84
|
+
"power": { "absolute": 1, "percentage": 1, "unit": "watts" },
|
|
85
|
+
"heartRate": { "absolute": 1, "unit": "bpm" },
|
|
86
|
+
"cadence": { "absolute": 1, "unit": "rpm" }
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Global Options
|
|
91
|
+
|
|
92
|
+
### Verbosity Control
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Verbose output (detailed logging)
|
|
96
|
+
kaiord convert --input workout.fit --output workout.krd --verbose
|
|
97
|
+
|
|
98
|
+
# Quiet mode (errors only)
|
|
99
|
+
kaiord convert --input workout.fit --output workout.krd --quiet
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Output Format
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# JSON output (machine-readable)
|
|
106
|
+
kaiord convert --input workout.fit --output workout.krd --json
|
|
107
|
+
|
|
108
|
+
# Force pretty terminal output
|
|
109
|
+
kaiord convert --input workout.fit --output workout.krd --log-format pretty
|
|
110
|
+
|
|
111
|
+
# Force structured JSON logs
|
|
112
|
+
kaiord convert --input workout.fit --output workout.krd --log-format json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Environment Variables
|
|
116
|
+
|
|
117
|
+
The CLI automatically detects the environment and adjusts its behavior:
|
|
118
|
+
|
|
119
|
+
- **CI=true**: Enables structured JSON logging
|
|
120
|
+
- **NODE_ENV=production**: Enables structured JSON logging
|
|
121
|
+
- **TTY detection**: Automatically disables colors and spinners in non-interactive environments
|
|
122
|
+
|
|
123
|
+
## Supported Formats
|
|
124
|
+
|
|
125
|
+
- **FIT** (.fit) - Garmin's binary workout file format
|
|
126
|
+
- **KRD** (.krd) - Kaiord's canonical JSON format
|
|
127
|
+
- **TCX** (.tcx) - Training Center XML format
|
|
128
|
+
- **ZWO** (.zwo) - Zwift workout XML format
|
|
129
|
+
|
|
130
|
+
## Exit Codes
|
|
131
|
+
|
|
132
|
+
- **0**: Success
|
|
133
|
+
- **1**: Error (invalid arguments, file not found, parsing error, validation error)
|
|
134
|
+
|
|
135
|
+
## Troubleshooting
|
|
136
|
+
|
|
137
|
+
### File Not Found
|
|
138
|
+
|
|
139
|
+
Ensure the input file path is correct and the file exists:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
ls -la workout.fit
|
|
143
|
+
kaiord convert --input workout.fit --output workout.krd
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Permission Errors
|
|
147
|
+
|
|
148
|
+
Check file permissions:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
chmod 644 workout.fit
|
|
152
|
+
kaiord convert --input workout.fit --output workout.krd
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Format Detection Issues
|
|
156
|
+
|
|
157
|
+
If automatic format detection fails, use explicit format flags:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
kaiord convert --input data.bin --input-format fit --output workout.krd --output-format krd
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Corrupted Files
|
|
164
|
+
|
|
165
|
+
If a file is corrupted, the CLI will display a descriptive error message:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
kaiord convert --input corrupted.fit --output workout.krd
|
|
169
|
+
# Error: Failed to parse FIT file
|
|
170
|
+
# Details: Corrupted file header
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Testing
|
|
174
|
+
|
|
175
|
+
Run the test suites:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Run all tests
|
|
179
|
+
pnpm test
|
|
180
|
+
|
|
181
|
+
# Run unit tests only
|
|
182
|
+
pnpm test:unit
|
|
183
|
+
|
|
184
|
+
# Run integration tests
|
|
185
|
+
pnpm test:integration
|
|
186
|
+
|
|
187
|
+
# Run smoke tests
|
|
188
|
+
pnpm test:smoke
|
|
189
|
+
|
|
190
|
+
# Run tests in watch mode
|
|
191
|
+
pnpm test:watch
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Development
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Install dependencies
|
|
198
|
+
pnpm install
|
|
199
|
+
|
|
200
|
+
# Build the CLI
|
|
201
|
+
pnpm build
|
|
202
|
+
|
|
203
|
+
# Run in development mode
|
|
204
|
+
pnpm dev -- convert --input workout.fit --output workout.krd
|
|
205
|
+
|
|
206
|
+
# Link for local testing
|
|
207
|
+
npm link
|
|
208
|
+
kaiord --version
|
|
209
|
+
npm unlink -g
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Documentation
|
|
213
|
+
|
|
214
|
+
For more information about the Kaiord project and the KRD format, see:
|
|
215
|
+
|
|
216
|
+
- [Kaiord Core Library](https://github.com/your-org/kaiord/tree/main/packages/core)
|
|
217
|
+
- [KRD Format Specification](https://github.com/your-org/kaiord/blob/main/docs/KRD_FORMAT.md)
|
|
218
|
+
- [Contributing Guide](https://github.com/your-org/kaiord/blob/main/CONTRIBUTING.md)
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT - See [LICENSE](../../LICENSE) file for details.
|
|
223
|
+
|
|
224
|
+
## Support
|
|
225
|
+
|
|
226
|
+
- Report issues: [GitHub Issues](https://github.com/your-org/kaiord/issues)
|
|
227
|
+
- Ask questions: [GitHub Discussions](https://github.com/your-org/kaiord/discussions)
|
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
isTTY
|
|
4
|
+
} from "../chunk-TI3WVGXE.js";
|
|
5
|
+
|
|
6
|
+
// src/bin/kaiord.ts
|
|
7
|
+
import chalk3 from "chalk";
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import yargs from "yargs";
|
|
12
|
+
import { hideBin } from "yargs/helpers";
|
|
13
|
+
|
|
14
|
+
// src/commands/convert.ts
|
|
15
|
+
import {
|
|
16
|
+
createDefaultProviders,
|
|
17
|
+
FitParsingError as FitParsingError2,
|
|
18
|
+
KrdValidationError as KrdValidationError2,
|
|
19
|
+
ToleranceExceededError as ToleranceExceededError2
|
|
20
|
+
} from "@kaiord/core";
|
|
21
|
+
import chalk2 from "chalk";
|
|
22
|
+
import ora from "ora";
|
|
23
|
+
import { basename, join } from "path";
|
|
24
|
+
import { z as z2 } from "zod";
|
|
25
|
+
|
|
26
|
+
// src/utils/error-formatter.ts
|
|
27
|
+
import {
|
|
28
|
+
FitParsingError,
|
|
29
|
+
KrdValidationError,
|
|
30
|
+
ToleranceExceededError
|
|
31
|
+
} from "@kaiord/core";
|
|
32
|
+
import chalk from "chalk";
|
|
33
|
+
var shouldUseColors = () => {
|
|
34
|
+
return isTTY() || process.env.FORCE_COLOR === "1";
|
|
35
|
+
};
|
|
36
|
+
var formatError = (error, options = {}) => {
|
|
37
|
+
if (options.json) {
|
|
38
|
+
return formatErrorAsJson(error);
|
|
39
|
+
}
|
|
40
|
+
return formatErrorAsPretty(error);
|
|
41
|
+
};
|
|
42
|
+
var formatValidationErrors = (errors) => {
|
|
43
|
+
if (errors.length === 0) {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
const useColors = shouldUseColors();
|
|
47
|
+
const lines = [
|
|
48
|
+
useColors ? chalk.red("Validation errors:") : "Validation errors:"
|
|
49
|
+
];
|
|
50
|
+
for (const error of errors) {
|
|
51
|
+
const fieldPath = useColors ? chalk.yellow(error.field) : error.field;
|
|
52
|
+
const bullet = useColors ? chalk.red("\u2022") : "\u2022";
|
|
53
|
+
lines.push(` ${bullet} ${fieldPath}: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
};
|
|
57
|
+
var formatToleranceViolations = (violations) => {
|
|
58
|
+
if (violations.length === 0) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
const useColors = shouldUseColors();
|
|
62
|
+
const lines = [
|
|
63
|
+
useColors ? chalk.red("Tolerance violations:") : "Tolerance violations:"
|
|
64
|
+
];
|
|
65
|
+
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;
|
|
70
|
+
const deviation = Math.abs(violation.deviation);
|
|
71
|
+
const tolerance = violation.tolerance;
|
|
72
|
+
lines.push(
|
|
73
|
+
` ${bullet} ${fieldPath}: expected ${expected}, got ${actual} (deviation: ${deviation}, tolerance: \xB1${tolerance})`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
};
|
|
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
|
+
}
|
|
91
|
+
lines.push(
|
|
92
|
+
"",
|
|
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."
|
|
96
|
+
);
|
|
97
|
+
return lines.join("\n");
|
|
98
|
+
}
|
|
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."
|
|
110
|
+
);
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
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."
|
|
124
|
+
);
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
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");
|
|
139
|
+
}
|
|
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");
|
|
146
|
+
};
|
|
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
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (error instanceof KrdValidationError) {
|
|
164
|
+
return JSON.stringify(
|
|
165
|
+
{
|
|
166
|
+
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
|
|
176
|
+
);
|
|
177
|
+
}
|
|
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
|
+
);
|
|
192
|
+
}
|
|
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
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return JSON.stringify(
|
|
208
|
+
{
|
|
209
|
+
success: false,
|
|
210
|
+
error: {
|
|
211
|
+
type: "UnknownError",
|
|
212
|
+
message: String(error)
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
null,
|
|
216
|
+
2
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
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) => {
|
|
229
|
+
try {
|
|
230
|
+
if (format === "fit") {
|
|
231
|
+
const buffer = await fsReadFile(path);
|
|
232
|
+
return new Uint8Array(buffer);
|
|
233
|
+
} else {
|
|
234
|
+
return await fsReadFile(path, "utf-8");
|
|
235
|
+
}
|
|
236
|
+
} 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}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
throw new Error(`Failed to read file: ${path}`);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
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
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (error instanceof Error && error.message.includes("require")) {
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
throw new Error(`Failed to write file: ${path}`);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
var findFiles = async (pattern) => {
|
|
276
|
+
const files = await glob(pattern, {
|
|
277
|
+
nodir: true,
|
|
278
|
+
absolute: false
|
|
279
|
+
});
|
|
280
|
+
return files.sort();
|
|
281
|
+
};
|
|
282
|
+
|
|
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;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// src/utils/logger-factory.ts
|
|
299
|
+
var isCI = () => {
|
|
300
|
+
return process.env.CI === "true" || process.env.NODE_ENV === "production" || !process.stdout.isTTY;
|
|
301
|
+
};
|
|
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);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
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");
|
|
339
|
+
}
|
|
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");
|
|
344
|
+
}
|
|
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");
|
|
349
|
+
}
|
|
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}`);
|
|
365
|
+
}
|
|
366
|
+
await writeFile(outputFile, outputData, outputFormat);
|
|
367
|
+
};
|
|
368
|
+
var convertCommand = async (options) => {
|
|
369
|
+
const validatedOptions = convertOptionsSchema.parse(options);
|
|
370
|
+
const logger = await createLogger({
|
|
371
|
+
type: validatedOptions.logFormat,
|
|
372
|
+
level: validatedOptions.verbose ? "debug" : validatedOptions.quiet ? "error" : "info",
|
|
373
|
+
quiet: validatedOptions.quiet
|
|
374
|
+
});
|
|
375
|
+
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({
|
|
436
|
+
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
|
+
}
|
|
491
|
+
} 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
|
+
}
|
|
560
|
+
}
|
|
561
|
+
} catch (error) {
|
|
562
|
+
logger.error("Conversion failed", { error });
|
|
563
|
+
const formattedError = formatError(error, {
|
|
564
|
+
json: validatedOptions.json
|
|
565
|
+
});
|
|
566
|
+
if (validatedOptions.json) {
|
|
567
|
+
console.log(formattedError);
|
|
568
|
+
} else {
|
|
569
|
+
console.error(formattedError);
|
|
570
|
+
}
|
|
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);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
// src/commands/validate.ts
|
|
592
|
+
import {
|
|
593
|
+
createDefaultProviders as createDefaultProviders2,
|
|
594
|
+
createToleranceChecker,
|
|
595
|
+
toleranceConfigSchema,
|
|
596
|
+
validateRoundTrip
|
|
597
|
+
} from "@kaiord/core";
|
|
598
|
+
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()
|
|
608
|
+
});
|
|
609
|
+
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
|
+
});
|
|
617
|
+
try {
|
|
618
|
+
const format = detectFormat(opts.input);
|
|
619
|
+
if (!format) {
|
|
620
|
+
throw new Error(`Unable to detect format from file: ${opts.input}`);
|
|
621
|
+
}
|
|
622
|
+
if (format !== "fit") {
|
|
623
|
+
throw new Error(
|
|
624
|
+
`Validation currently only supports FIT files. Got: ${format}`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
logger.debug("Reading input file", { path: opts.input, format });
|
|
628
|
+
const inputData = await readFile(opts.input, format);
|
|
629
|
+
if (typeof inputData === "string") {
|
|
630
|
+
throw new Error("Expected binary data for FIT file");
|
|
631
|
+
}
|
|
632
|
+
let toleranceConfig;
|
|
633
|
+
if (opts.toleranceConfig) {
|
|
634
|
+
logger.debug("Loading custom tolerance config", {
|
|
635
|
+
path: opts.toleranceConfig
|
|
636
|
+
});
|
|
637
|
+
const configContent = await fsReadFile2(opts.toleranceConfig, "utf-8");
|
|
638
|
+
const configJson = JSON.parse(configContent);
|
|
639
|
+
toleranceConfig = toleranceConfigSchema.parse(configJson);
|
|
640
|
+
logger.debug("Custom tolerance config loaded", {
|
|
641
|
+
config: toleranceConfig
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
const providers = createDefaultProviders2(logger);
|
|
645
|
+
const toleranceChecker = toleranceConfig ? createToleranceChecker(toleranceConfig) : providers.toleranceChecker;
|
|
646
|
+
const roundTripValidator = validateRoundTrip(
|
|
647
|
+
providers.fitReader,
|
|
648
|
+
providers.fitWriter,
|
|
649
|
+
toleranceChecker,
|
|
650
|
+
logger
|
|
651
|
+
);
|
|
652
|
+
const spinner = opts.quiet || opts.json ? null : ora2("Validating round-trip conversion...").start();
|
|
653
|
+
logger.info("Starting round-trip validation", { file: opts.input });
|
|
654
|
+
const violations = await roundTripValidator.validateFitToKrdToFit({
|
|
655
|
+
originalFit: inputData
|
|
656
|
+
});
|
|
657
|
+
if (spinner) {
|
|
658
|
+
if (violations.length === 0) {
|
|
659
|
+
spinner.succeed("Validation complete - no tolerance violations");
|
|
660
|
+
} else {
|
|
661
|
+
spinner.fail(
|
|
662
|
+
`Validation failed - ${violations.length} tolerance violation(s)`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (violations.length === 0) {
|
|
667
|
+
logger.info("Round-trip validation passed");
|
|
668
|
+
if (opts.json) {
|
|
669
|
+
console.log(
|
|
670
|
+
JSON.stringify(
|
|
671
|
+
{
|
|
672
|
+
success: true,
|
|
673
|
+
file: opts.input,
|
|
674
|
+
format,
|
|
675
|
+
violations: []
|
|
676
|
+
},
|
|
677
|
+
null,
|
|
678
|
+
2
|
|
679
|
+
)
|
|
680
|
+
);
|
|
681
|
+
} else if (!opts.quiet) {
|
|
682
|
+
console.log("\u2713 Round-trip validation passed");
|
|
683
|
+
}
|
|
684
|
+
process.exit(0);
|
|
685
|
+
} else {
|
|
686
|
+
logger.warn("Round-trip validation failed", {
|
|
687
|
+
violationCount: violations.length
|
|
688
|
+
});
|
|
689
|
+
if (opts.json) {
|
|
690
|
+
console.log(
|
|
691
|
+
JSON.stringify(
|
|
692
|
+
{
|
|
693
|
+
success: false,
|
|
694
|
+
file: opts.input,
|
|
695
|
+
format,
|
|
696
|
+
violations
|
|
697
|
+
},
|
|
698
|
+
null,
|
|
699
|
+
2
|
|
700
|
+
)
|
|
701
|
+
);
|
|
702
|
+
} else {
|
|
703
|
+
console.error("\u2716 Round-trip validation failed\n");
|
|
704
|
+
console.error(formatToleranceViolations(violations));
|
|
705
|
+
}
|
|
706
|
+
process.exit(1);
|
|
707
|
+
}
|
|
708
|
+
} catch (error) {
|
|
709
|
+
logger.error("Validation failed", { error });
|
|
710
|
+
if (opts.json) {
|
|
711
|
+
console.log(
|
|
712
|
+
JSON.stringify(
|
|
713
|
+
{
|
|
714
|
+
success: false,
|
|
715
|
+
error: formatError(error, { json: true })
|
|
716
|
+
},
|
|
717
|
+
null,
|
|
718
|
+
2
|
|
719
|
+
)
|
|
720
|
+
);
|
|
721
|
+
} else {
|
|
722
|
+
console.error(formatError(error, { json: false }));
|
|
723
|
+
}
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
// src/bin/kaiord.ts
|
|
729
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
730
|
+
var __dirname2 = dirname2(__filename2);
|
|
731
|
+
var packageJsonPath = __dirname2.includes("/dist") ? join2(__dirname2, "../../package.json") : join2(__dirname2, "../../package.json");
|
|
732
|
+
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
733
|
+
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
|
+
var main = async () => {
|
|
756
|
+
try {
|
|
757
|
+
const args = process.argv.slice(2);
|
|
758
|
+
if (args.includes("--kiro") || args.includes("--kiroween")) {
|
|
759
|
+
showKiroEasterEgg();
|
|
760
|
+
}
|
|
761
|
+
await yargs(hideBin(process.argv)).scriptName("kaiord").usage("$0 <command> [options]").command(
|
|
762
|
+
"convert",
|
|
763
|
+
"Convert workout files between formats",
|
|
764
|
+
(yargs2) => {
|
|
765
|
+
return yargs2.option("input", {
|
|
766
|
+
alias: "i",
|
|
767
|
+
type: "string",
|
|
768
|
+
description: "Input file path or glob pattern",
|
|
769
|
+
demandOption: true
|
|
770
|
+
}).option("output", {
|
|
771
|
+
alias: "o",
|
|
772
|
+
type: "string",
|
|
773
|
+
description: "Output file path"
|
|
774
|
+
}).option("output-dir", {
|
|
775
|
+
type: "string",
|
|
776
|
+
description: "Output directory for batch conversion"
|
|
777
|
+
}).option("input-format", {
|
|
778
|
+
type: "string",
|
|
779
|
+
choices: ["fit", "krd", "tcx", "zwo"],
|
|
780
|
+
description: "Override input format detection"
|
|
781
|
+
}).option("output-format", {
|
|
782
|
+
type: "string",
|
|
783
|
+
choices: ["fit", "krd", "tcx", "zwo"],
|
|
784
|
+
description: "Override output format detection"
|
|
785
|
+
}).example(
|
|
786
|
+
"$0 convert -i workout.fit -o workout.krd",
|
|
787
|
+
"Convert FIT to KRD"
|
|
788
|
+
).example(
|
|
789
|
+
"$0 convert -i workout.krd -o workout.fit",
|
|
790
|
+
"Convert KRD to FIT"
|
|
791
|
+
).example(
|
|
792
|
+
'$0 convert -i "workouts/*.fit" --output-dir converted/',
|
|
793
|
+
"Batch convert all FIT files"
|
|
794
|
+
);
|
|
795
|
+
},
|
|
796
|
+
async (argv) => {
|
|
797
|
+
await convertCommand({
|
|
798
|
+
input: argv.input,
|
|
799
|
+
output: argv.output,
|
|
800
|
+
outputDir: argv.outputDir,
|
|
801
|
+
inputFormat: argv.inputFormat,
|
|
802
|
+
outputFormat: argv.outputFormat,
|
|
803
|
+
verbose: argv.verbose,
|
|
804
|
+
quiet: argv.quiet,
|
|
805
|
+
json: argv.json,
|
|
806
|
+
logFormat: argv.logFormat
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
).command(
|
|
810
|
+
"validate",
|
|
811
|
+
"Validate round-trip conversion of workout files",
|
|
812
|
+
(yargs2) => {
|
|
813
|
+
return yargs2.option("input", {
|
|
814
|
+
alias: "i",
|
|
815
|
+
type: "string",
|
|
816
|
+
description: "Input file path",
|
|
817
|
+
demandOption: true
|
|
818
|
+
}).option("tolerance-config", {
|
|
819
|
+
type: "string",
|
|
820
|
+
description: "Path to custom tolerance configuration JSON"
|
|
821
|
+
}).example(
|
|
822
|
+
"$0 validate -i workout.fit",
|
|
823
|
+
"Validate round-trip conversion"
|
|
824
|
+
).example(
|
|
825
|
+
"$0 validate -i workout.fit --tolerance-config custom.json",
|
|
826
|
+
"Validate with custom tolerances"
|
|
827
|
+
);
|
|
828
|
+
},
|
|
829
|
+
async (argv) => {
|
|
830
|
+
await validateCommand({
|
|
831
|
+
input: argv.input,
|
|
832
|
+
toleranceConfig: argv.toleranceConfig,
|
|
833
|
+
verbose: argv.verbose,
|
|
834
|
+
quiet: argv.quiet,
|
|
835
|
+
json: argv.json,
|
|
836
|
+
logFormat: argv.logFormat
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
).option("verbose", {
|
|
840
|
+
type: "boolean",
|
|
841
|
+
description: "Enable verbose logging",
|
|
842
|
+
global: true
|
|
843
|
+
}).option("quiet", {
|
|
844
|
+
type: "boolean",
|
|
845
|
+
description: "Suppress all output except errors",
|
|
846
|
+
global: true
|
|
847
|
+
}).option("json", {
|
|
848
|
+
type: "boolean",
|
|
849
|
+
description: "Output results in JSON format",
|
|
850
|
+
global: true
|
|
851
|
+
}).option("log-format", {
|
|
852
|
+
type: "string",
|
|
853
|
+
choices: ["pretty", "structured"],
|
|
854
|
+
description: "Force specific log format",
|
|
855
|
+
global: true
|
|
856
|
+
}).version(version).alias("version", "v").help().alias("help", "h").demandCommand(1, "You must specify a command").strict().parse();
|
|
857
|
+
} catch (error) {
|
|
858
|
+
const formattedError = formatError(error, { json: false });
|
|
859
|
+
console.error(formattedError);
|
|
860
|
+
if (error && typeof error === "object" && "name" in error) {
|
|
861
|
+
const errorName = error.name;
|
|
862
|
+
if (errorName === "FitParsingError") {
|
|
863
|
+
process.exit(4);
|
|
864
|
+
} else if (errorName === "KrdValidationError") {
|
|
865
|
+
process.exit(5);
|
|
866
|
+
} else if (errorName === "ToleranceExceededError") {
|
|
867
|
+
process.exit(6);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
process.exit(99);
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
process.on("unhandledRejection", (reason) => {
|
|
874
|
+
console.error("Unhandled rejection:", reason);
|
|
875
|
+
process.exit(99);
|
|
876
|
+
});
|
|
877
|
+
process.on("uncaughtException", (error) => {
|
|
878
|
+
console.error("Uncaught exception:", error);
|
|
879
|
+
process.exit(99);
|
|
880
|
+
});
|
|
881
|
+
main().catch((error) => {
|
|
882
|
+
console.error("Fatal error:", error);
|
|
883
|
+
process.exit(99);
|
|
884
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
isTTY
|
|
4
|
+
} from "./chunk-TI3WVGXE.js";
|
|
5
|
+
|
|
6
|
+
// src/adapters/logger/pretty-logger.ts
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
var createPrettyLogger = (options = {}) => {
|
|
9
|
+
const level = options.level || "info";
|
|
10
|
+
const quiet = options.quiet || false;
|
|
11
|
+
const forceColor = process.env.FORCE_COLOR === "1";
|
|
12
|
+
const useColors = isTTY() || forceColor;
|
|
13
|
+
const levels = ["debug", "info", "warn", "error"];
|
|
14
|
+
const minLevelIndex = levels.indexOf(level);
|
|
15
|
+
const shouldLog = (messageLevel) => {
|
|
16
|
+
if (quiet && messageLevel !== "error") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const messageLevelIndex = levels.indexOf(messageLevel);
|
|
20
|
+
return messageLevelIndex >= minLevelIndex;
|
|
21
|
+
};
|
|
22
|
+
const formatContext = (context) => {
|
|
23
|
+
if (!context || Object.keys(context).length === 0) {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
const contextStr = JSON.stringify(context);
|
|
27
|
+
return " " + (useColors ? chalk.gray(contextStr) : contextStr);
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
debug: (message, context) => {
|
|
31
|
+
if (shouldLog("debug")) {
|
|
32
|
+
const formatted = `\u{1F41B} ${message}${formatContext(context)}`;
|
|
33
|
+
console.log(useColors ? chalk.gray(formatted) : formatted);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
info: (message, context) => {
|
|
37
|
+
if (shouldLog("info")) {
|
|
38
|
+
const formatted = `\u2139 ${message}${formatContext(context)}`;
|
|
39
|
+
console.log(useColors ? chalk.blue(formatted) : formatted);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
warn: (message, context) => {
|
|
43
|
+
if (shouldLog("warn")) {
|
|
44
|
+
const formatted = `\u26A0 ${message}${formatContext(context)}`;
|
|
45
|
+
console.warn(useColors ? chalk.yellow(formatted) : formatted);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
error: (message, context) => {
|
|
49
|
+
if (shouldLog("error")) {
|
|
50
|
+
const formatted = `\u2716 ${message}${formatContext(context)}`;
|
|
51
|
+
console.error(useColors ? chalk.red(formatted) : formatted);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
export {
|
|
57
|
+
createPrettyLogger
|
|
58
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/adapters/logger/structured-logger.ts
|
|
4
|
+
import winston from "winston";
|
|
5
|
+
var createStructuredLogger = (options = {}) => {
|
|
6
|
+
const level = options.level || "info";
|
|
7
|
+
const quiet = options.quiet || false;
|
|
8
|
+
const winstonLogger = winston.createLogger({
|
|
9
|
+
level: quiet ? "error" : level,
|
|
10
|
+
format: winston.format.combine(
|
|
11
|
+
winston.format.timestamp(),
|
|
12
|
+
winston.format.json()
|
|
13
|
+
),
|
|
14
|
+
transports: [
|
|
15
|
+
new winston.transports.Console({
|
|
16
|
+
stderrLevels: ["error", "warn", "info", "debug"]
|
|
17
|
+
})
|
|
18
|
+
]
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
debug: (message, context) => {
|
|
22
|
+
winstonLogger.debug(message, context);
|
|
23
|
+
},
|
|
24
|
+
info: (message, context) => {
|
|
25
|
+
winstonLogger.info(message, context);
|
|
26
|
+
},
|
|
27
|
+
warn: (message, context) => {
|
|
28
|
+
winstonLogger.warn(message, context);
|
|
29
|
+
},
|
|
30
|
+
error: (message, context) => {
|
|
31
|
+
winstonLogger.error(message, context);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export {
|
|
36
|
+
createStructuredLogger
|
|
37
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kaiord/cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Command-line interface for Kaiord workout file conversion",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kaiord": "./dist/bin/kaiord.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:unit": "vitest run --exclude '**/*-{integration,smoke,snapshot}.test.ts'",
|
|
18
|
+
"test:integration": "vitest run src/commands/convert-integration.test.ts src/commands/validate-integration.test.ts",
|
|
19
|
+
"test:smoke": "vitest run src/tests/cli-smoke.test.ts",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"dev": "tsx src/bin/kaiord.ts",
|
|
22
|
+
"prepublishOnly": "pnpm build && pnpm test",
|
|
23
|
+
"check-licenses": "license-checker --onlyAllow 'MIT;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause;ISC' --production"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"kaiord",
|
|
27
|
+
"fit",
|
|
28
|
+
"tcx",
|
|
29
|
+
"zwo",
|
|
30
|
+
"workout",
|
|
31
|
+
"garmin",
|
|
32
|
+
"zwift",
|
|
33
|
+
"cli",
|
|
34
|
+
"converter"
|
|
35
|
+
],
|
|
36
|
+
"author": "Kaiord Contributors",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/your-org/kaiord.git",
|
|
41
|
+
"directory": "packages/cli"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://www.npmjs.com/package/@kaiord/cli",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/your-org/kaiord/issues"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@kaiord/core": "workspace:*",
|
|
52
|
+
"chalk": "^5.3.0",
|
|
53
|
+
"glob": "^10.3.10",
|
|
54
|
+
"ora": "^8.0.1",
|
|
55
|
+
"winston": "^3.11.0",
|
|
56
|
+
"yargs": "^17.7.2",
|
|
57
|
+
"zod": "^3.22.4"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/yargs": "^17.0.32",
|
|
61
|
+
"execa": "^8.0.1",
|
|
62
|
+
"license-checker": "^25.0.1",
|
|
63
|
+
"strip-ansi": "^7.1.0",
|
|
64
|
+
"tmp-promise": "^3.0.3",
|
|
65
|
+
"tsup": "^8.0.1",
|
|
66
|
+
"tsx": "^4.7.0",
|
|
67
|
+
"typescript": "^5.3.3",
|
|
68
|
+
"vitest": "^1.2.0"
|
|
69
|
+
}
|
|
70
|
+
}
|