endurance-coach 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +11 -0
  2. package/dist/cli/args.d.ts +117 -0
  3. package/dist/cli/args.js +452 -0
  4. package/dist/cli/commands/expand.d.ts +16 -0
  5. package/dist/cli/commands/expand.js +89 -0
  6. package/dist/cli/commands/foundation.d.ts +11 -0
  7. package/dist/cli/commands/foundation.js +82 -0
  8. package/dist/cli/commands/hr-zones.d.ts +14 -0
  9. package/dist/cli/commands/hr-zones.js +62 -0
  10. package/dist/cli/commands/modify.d.ts +23 -0
  11. package/dist/cli/commands/modify.js +251 -0
  12. package/dist/cli/commands/query.d.ts +7 -0
  13. package/dist/cli/commands/query.js +20 -0
  14. package/dist/cli/commands/render.d.ts +18 -0
  15. package/dist/cli/commands/render.js +132 -0
  16. package/dist/cli/commands/schedule-preferences.d.ts +13 -0
  17. package/dist/cli/commands/schedule-preferences.js +78 -0
  18. package/dist/cli/commands/schema.d.ts +6 -0
  19. package/dist/cli/commands/schema.js +94 -0
  20. package/dist/cli/commands/stats.d.ts +13 -0
  21. package/dist/cli/commands/stats.js +76 -0
  22. package/dist/cli/commands/strava.d.ts +27 -0
  23. package/dist/cli/commands/strava.js +342 -0
  24. package/dist/cli/commands/strength.d.ts +15 -0
  25. package/dist/cli/commands/strength.js +114 -0
  26. package/dist/cli/commands/templates.d.ts +13 -0
  27. package/dist/cli/commands/templates.js +792 -0
  28. package/dist/cli/commands/training-load.d.ts +9 -0
  29. package/dist/cli/commands/training-load.js +39 -0
  30. package/dist/cli/commands/validate.d.ts +13 -0
  31. package/dist/cli/commands/validate.js +77 -0
  32. package/dist/cli/help.d.ts +8 -0
  33. package/dist/cli/help.js +161 -0
  34. package/dist/cli/index.d.ts +1 -0
  35. package/dist/cli/index.js +111 -0
  36. package/dist/cli/utils/colors.d.ts +8 -0
  37. package/dist/cli/utils/colors.js +8 -0
  38. package/dist/cli/utils/format-table.d.ts +9 -0
  39. package/dist/cli/utils/format-table.js +24 -0
  40. package/dist/cli/utils/number-utils.d.ts +8 -0
  41. package/dist/cli/utils/number-utils.js +13 -0
  42. package/dist/cli/utils/printSection.d.ts +10 -0
  43. package/dist/cli/utils/printSection.js +14 -0
  44. package/dist/cli.d.ts +9 -6
  45. package/dist/cli.js +7 -1220
  46. package/dist/db/client.js +1 -1
  47. package/dist/expander/expander.d.ts +23 -3
  48. package/dist/expander/expander.js +67 -26
  49. package/dist/expander/index.d.ts +1 -0
  50. package/dist/expander/index.js +2 -0
  51. package/dist/expander/types.d.ts +4 -0
  52. package/dist/expander/validation.d.ts +84 -0
  53. package/dist/expander/validation.js +193 -0
  54. package/dist/expander/zones.js +4 -4
  55. package/dist/schema/compact-plan.d.ts +16 -9
  56. package/dist/schema/compact-plan.js +11 -5
  57. package/dist/schema/compact-plan.schema.d.ts +8 -18
  58. package/dist/schema/compact-plan.schema.js +12 -8
  59. package/dist/schema/training-plan.d.ts +7 -0
  60. package/dist/schema/training-plan.js +1 -1
  61. package/dist/schema/training-plan.schema.d.ts +33 -53
  62. package/dist/schema/training-plan.schema.js +11 -11
  63. package/dist/strava/api.d.ts +28 -1
  64. package/dist/strava/api.js +50 -10
  65. package/dist/strava/types.d.ts +30 -0
  66. package/dist/templates/converter.d.ts +62 -0
  67. package/dist/templates/converter.js +293 -0
  68. package/dist/templates/index.d.ts +2 -1
  69. package/dist/templates/index.js +3 -1
  70. package/dist/templates/loader.d.ts +51 -1
  71. package/dist/templates/loader.js +149 -20
  72. package/dist/templates/template.schema.d.ts +29 -33
  73. package/dist/templates/template.schema.js +5 -5
  74. package/dist/templates/template.types.d.ts +4 -0
  75. package/dist/viewer/lib/export/fit.d.ts +11 -2
  76. package/dist/viewer/lib/export/fit.js +99 -38
  77. package/dist/viewer/lib/export/index.d.ts +8 -2
  78. package/dist/viewer/lib/export/index.js +9 -3
  79. package/package.json +21 -13
  80. package/templates/bike/hills.yaml +1 -1
  81. package/templates/plan-viewer.html +24 -24
  82. package/templates/run/easy.yaml +1 -1
  83. package/templates/run/fartlek.yaml +29 -2
  84. package/templates/run/hills.yaml +26 -1
  85. package/templates/run/intervals.1k.yaml +1 -1
  86. package/templates/run/intervals.400.yaml +1 -1
  87. package/templates/run/intervals.800.yaml +1 -1
  88. package/templates/run/intervals.mile.yaml +1 -1
  89. package/templates/run/long.yaml +1 -1
  90. package/templates/run/progression.yaml +1 -1
  91. package/templates/run/race.5k.yaml +2 -2
  92. package/templates/run/recovery.yaml +1 -1
  93. package/templates/run/rest.yaml +2 -2
  94. package/templates/run/strides.yaml +14 -1
  95. package/templates/run/tempo.yaml +1 -1
  96. package/templates/run/threshold.yaml +1 -1
  97. package/templates/strength/rest.yaml +16 -0
  98. package/templates/swim/endurance.yaml +2 -2
  99. package/templates/swim/technique.yaml +2 -2
package/README.md CHANGED
@@ -89,6 +89,17 @@ In the next step, the AI will ask you about yourself, the event you're training
89
89
 
90
90
  The AI will use this information to create a plan tailored to your current fitness level. The more detail you provide, the better your plan will be.
91
91
 
92
+ ## Contributing
93
+
94
+ We welcome contributions from the community! Whether you want to add workout templates, improve the UI, fix bugs, or enhance documentation, your help is appreciated.
95
+
96
+ Please see our [CONTRIBUTING.md](CONTRIBUTING.md) for:
97
+
98
+ - Development setup instructions
99
+ - Coding standards and guidelines
100
+ - How to submit pull requests
101
+ - Areas where we need help
102
+
92
103
  # About
93
104
 
94
105
  ## Lineage & Architectural Evolution
@@ -0,0 +1,117 @@
1
+ export interface SyncArgs {
2
+ command: "sync";
3
+ clientId?: string;
4
+ clientSecret?: string;
5
+ accessToken?: string;
6
+ refreshToken?: string;
7
+ days?: number;
8
+ }
9
+ export interface RenderArgs {
10
+ command: "render";
11
+ inputFile: string;
12
+ outputFile?: string;
13
+ }
14
+ export interface StatsArgs {
15
+ command: "stats";
16
+ weeks?: number;
17
+ longestWeeks?: number;
18
+ json: boolean;
19
+ }
20
+ export interface TrainingLoadArgs {
21
+ command: "training-load";
22
+ weeks?: number;
23
+ json: boolean;
24
+ }
25
+ export interface FoundationArgs {
26
+ command: "foundation";
27
+ topWeeks?: number;
28
+ json: boolean;
29
+ }
30
+ export interface StrengthArgs {
31
+ command: "strength";
32
+ months?: number;
33
+ longMonths?: number;
34
+ easyHrMax?: number;
35
+ longMinutes?: number;
36
+ years?: number;
37
+ json: boolean;
38
+ }
39
+ export interface SchedulePreferencesArgs {
40
+ command: "schedule-preferences";
41
+ rideMinutes?: number;
42
+ runMinutes?: number;
43
+ json: boolean;
44
+ }
45
+ export interface HrZonesArgs {
46
+ command: "hr-zones";
47
+ weeks?: number;
48
+ distributionWeeks?: number;
49
+ json: boolean;
50
+ }
51
+ export interface QueryArgs {
52
+ command: "query";
53
+ sql: string;
54
+ json: boolean;
55
+ }
56
+ export interface AuthArgs {
57
+ command: "auth";
58
+ clientId?: string;
59
+ clientSecret?: string;
60
+ code?: string;
61
+ }
62
+ export interface ActivityLapsArgs {
63
+ command: "activity";
64
+ id: number;
65
+ laps: true;
66
+ }
67
+ export interface HelpArgs {
68
+ command: "help";
69
+ }
70
+ export interface ValidateArgs {
71
+ command: "validate";
72
+ inputFile: string;
73
+ compact?: boolean;
74
+ }
75
+ export interface ExpandArgs {
76
+ command: "expand";
77
+ inputFile: string;
78
+ outputFile?: string;
79
+ format?: "json" | "yaml";
80
+ verbose?: boolean;
81
+ }
82
+ export interface TemplatesArgs {
83
+ command: "templates";
84
+ sport?: string;
85
+ show?: string;
86
+ type?: string;
87
+ source?: "user" | "builtin" | "all";
88
+ verbose?: boolean;
89
+ create?: string;
90
+ category?: string;
91
+ templateFile?: string;
92
+ overwrite?: boolean;
93
+ dryRun?: boolean;
94
+ example?: boolean;
95
+ userTemplatesDir?: string;
96
+ validate?: string;
97
+ }
98
+ export interface SchemaArgs {
99
+ command: "schema";
100
+ }
101
+ export interface ModifyArgs {
102
+ command: "modify";
103
+ backup: string;
104
+ plan: string;
105
+ output?: string;
106
+ }
107
+ export type CliArgs = SyncArgs | RenderArgs | StatsArgs | TrainingLoadArgs | FoundationArgs | StrengthArgs | SchedulePreferencesArgs | HrZonesArgs | QueryArgs | AuthArgs | ActivityLapsArgs | HelpArgs | ModifyArgs | ValidateArgs | SchemaArgs | ExpandArgs | TemplatesArgs;
108
+ /**
109
+ * Parse command-line arguments from process.argv and produce the corresponding CLI command object.
110
+ *
111
+ * The returned object identifies the selected command and any parsed options or flags.
112
+ *
113
+ * Note: for missing required inputs or invalid option values this function logs an error and exits the process with status 1.
114
+ *
115
+ * @returns A `CliArgs` object describing the requested command and its parsed options
116
+ */
117
+ export declare function parseArgs(): CliArgs;
@@ -0,0 +1,452 @@
1
+ import { log } from "../lib/logging.js";
2
+ // ============================================================================
3
+ // MARK: Argument Parsing
4
+ // ============================================================================
5
+ /**
6
+ * Parse command-line arguments from process.argv and produce the corresponding CLI command object.
7
+ *
8
+ * The returned object identifies the selected command and any parsed options or flags.
9
+ *
10
+ * Note: for missing required inputs or invalid option values this function logs an error and exits the process with status 1.
11
+ *
12
+ * @returns A `CliArgs` object describing the requested command and its parsed options
13
+ */
14
+ export function parseArgs() {
15
+ const args = process.argv.slice(2);
16
+ if (args.length === 0 || args[0] === "sync") {
17
+ // Sync command (default)
18
+ const syncArgs = { command: "sync" };
19
+ for (const arg of args) {
20
+ if (arg.startsWith("--client-id=")) {
21
+ syncArgs.clientId = arg.split("=")[1];
22
+ }
23
+ else if (arg.startsWith("--client-secret=")) {
24
+ syncArgs.clientSecret = arg.split("=")[1];
25
+ }
26
+ else if (arg.startsWith("--access-token=")) {
27
+ syncArgs.accessToken = arg.split("=")[1];
28
+ }
29
+ else if (arg.startsWith("--refresh-token=")) {
30
+ syncArgs.refreshToken = arg.split("=")[1];
31
+ }
32
+ else if (arg.startsWith("--days=")) {
33
+ const parsed = parseInt(arg.split("=")[1], 10);
34
+ if (Number.isNaN(parsed)) {
35
+ log.error("Invalid --days value: must be a number");
36
+ process.exit(1);
37
+ }
38
+ syncArgs.days = parsed;
39
+ }
40
+ }
41
+ return syncArgs;
42
+ }
43
+ if (args[0] === "render") {
44
+ if (!args[1]) {
45
+ log.error("render command requires an input file");
46
+ process.exit(1);
47
+ }
48
+ const renderArgs = {
49
+ command: "render",
50
+ inputFile: args[1],
51
+ };
52
+ for (let i = 2; i < args.length; i++) {
53
+ if (args[i] === "--output" || args[i] === "-o") {
54
+ renderArgs.outputFile = args[i + 1];
55
+ i++;
56
+ }
57
+ else if (args[i].startsWith("--output=")) {
58
+ renderArgs.outputFile = args[i].split("=")[1];
59
+ }
60
+ else if (!args[i].startsWith("-") && !renderArgs.outputFile) {
61
+ // Accept positional output argument (helps when npm consumes -o)
62
+ renderArgs.outputFile = args[i];
63
+ }
64
+ }
65
+ return renderArgs;
66
+ }
67
+ if (args[0] === "stats") {
68
+ const statsArgs = {
69
+ command: "stats",
70
+ json: args.includes("--json"),
71
+ };
72
+ for (let i = 1; i < args.length; i++) {
73
+ if (args[i] === "--weeks") {
74
+ statsArgs.weeks = parseInt(args[i + 1], 10);
75
+ i++;
76
+ }
77
+ else if (args[i].startsWith("--weeks=")) {
78
+ statsArgs.weeks = parseInt(args[i].split("=")[1], 10);
79
+ }
80
+ else if (args[i] === "--longest-weeks") {
81
+ statsArgs.longestWeeks = parseInt(args[i + 1], 10);
82
+ i++;
83
+ }
84
+ else if (args[i].startsWith("--longest-weeks=")) {
85
+ statsArgs.longestWeeks = parseInt(args[i].split("=")[1], 10);
86
+ }
87
+ }
88
+ return statsArgs;
89
+ }
90
+ if (args[0] === "training-load") {
91
+ const trainingLoadArgs = {
92
+ command: "training-load",
93
+ json: args.includes("--json"),
94
+ };
95
+ for (let i = 1; i < args.length; i++) {
96
+ if (args[i] === "--weeks") {
97
+ trainingLoadArgs.weeks = parseInt(args[i + 1]);
98
+ i++;
99
+ }
100
+ else if (args[i].startsWith("--weeks=")) {
101
+ trainingLoadArgs.weeks = parseInt(args[i].split("=")[1]);
102
+ }
103
+ }
104
+ return trainingLoadArgs;
105
+ }
106
+ if (args[0] === "foundation") {
107
+ const foundationArgs = {
108
+ command: "foundation",
109
+ json: args.includes("--json"),
110
+ };
111
+ for (let i = 1; i < args.length; i++) {
112
+ if (args[i] === "--top-weeks") {
113
+ foundationArgs.topWeeks = parseInt(args[i + 1]);
114
+ i++;
115
+ }
116
+ else if (args[i].startsWith("--top-weeks=")) {
117
+ foundationArgs.topWeeks = parseInt(args[i].split("=")[1]);
118
+ }
119
+ }
120
+ return foundationArgs;
121
+ }
122
+ if (args[0] === "strength") {
123
+ const strengthArgs = {
124
+ command: "strength",
125
+ json: args.includes("--json"),
126
+ };
127
+ for (let i = 1; i < args.length; i++) {
128
+ if (args[i] === "--months") {
129
+ strengthArgs.months = parseInt(args[i + 1]);
130
+ i++;
131
+ }
132
+ else if (args[i].startsWith("--months=")) {
133
+ strengthArgs.months = parseInt(args[i].split("=")[1]);
134
+ }
135
+ else if (args[i] === "--long-months") {
136
+ strengthArgs.longMonths = parseInt(args[i + 1]);
137
+ i++;
138
+ }
139
+ else if (args[i].startsWith("--long-months=")) {
140
+ strengthArgs.longMonths = parseInt(args[i].split("=")[1]);
141
+ }
142
+ else if (args[i] === "--easy-hr-max") {
143
+ strengthArgs.easyHrMax = parseInt(args[i + 1]);
144
+ i++;
145
+ }
146
+ else if (args[i].startsWith("--easy-hr-max=")) {
147
+ strengthArgs.easyHrMax = parseInt(args[i].split("=")[1]);
148
+ }
149
+ else if (args[i] === "--long-minutes") {
150
+ strengthArgs.longMinutes = parseInt(args[i + 1]);
151
+ i++;
152
+ }
153
+ else if (args[i].startsWith("--long-minutes=")) {
154
+ strengthArgs.longMinutes = parseInt(args[i].split("=")[1]);
155
+ }
156
+ else if (args[i] === "--years") {
157
+ strengthArgs.years = parseInt(args[i + 1]);
158
+ i++;
159
+ }
160
+ else if (args[i].startsWith("--years=")) {
161
+ strengthArgs.years = parseInt(args[i].split("=")[1]);
162
+ }
163
+ }
164
+ return strengthArgs;
165
+ }
166
+ if (args[0] === "schedule-preferences") {
167
+ const scheduleArgs = {
168
+ command: "schedule-preferences",
169
+ json: args.includes("--json"),
170
+ };
171
+ for (let i = 1; i < args.length; i++) {
172
+ if (args[i] === "--ride-minutes") {
173
+ scheduleArgs.rideMinutes = parseInt(args[i + 1]);
174
+ i++;
175
+ }
176
+ else if (args[i].startsWith("--ride-minutes=")) {
177
+ scheduleArgs.rideMinutes = parseInt(args[i].split("=")[1]);
178
+ }
179
+ else if (args[i] === "--run-minutes") {
180
+ scheduleArgs.runMinutes = parseInt(args[i + 1]);
181
+ i++;
182
+ }
183
+ else if (args[i].startsWith("--run-minutes=")) {
184
+ scheduleArgs.runMinutes = parseInt(args[i].split("=")[1]);
185
+ }
186
+ }
187
+ return scheduleArgs;
188
+ }
189
+ if (args[0] === "hr-zones") {
190
+ const hrArgs = {
191
+ command: "hr-zones",
192
+ json: args.includes("--json"),
193
+ };
194
+ for (let i = 1; i < args.length; i++) {
195
+ if (args[i] === "--weeks") {
196
+ hrArgs.weeks = parseInt(args[i + 1]);
197
+ i++;
198
+ }
199
+ else if (args[i].startsWith("--weeks=")) {
200
+ hrArgs.weeks = parseInt(args[i].split("=")[1]);
201
+ }
202
+ else if (args[i] === "--distribution-weeks") {
203
+ hrArgs.distributionWeeks = parseInt(args[i + 1]);
204
+ i++;
205
+ }
206
+ else if (args[i].startsWith("--distribution-weeks=")) {
207
+ hrArgs.distributionWeeks = parseInt(args[i].split("=")[1]);
208
+ }
209
+ }
210
+ return hrArgs;
211
+ }
212
+ if (args[0] === "query") {
213
+ if (!args[1]) {
214
+ log.error("query command requires a SQL statement");
215
+ process.exit(1);
216
+ }
217
+ const queryArgs = {
218
+ command: "query",
219
+ sql: args[1],
220
+ json: args.includes("--json"),
221
+ };
222
+ return queryArgs;
223
+ }
224
+ if (args[0] === "auth") {
225
+ const authArgs = { command: "auth" };
226
+ for (const arg of args) {
227
+ if (arg.startsWith("--client-id=")) {
228
+ authArgs.clientId = arg.slice("--client-id=".length);
229
+ }
230
+ else if (arg.startsWith("--client-secret=")) {
231
+ authArgs.clientSecret = arg.slice("--client-secret=".length);
232
+ }
233
+ else if (arg.startsWith("--code=")) {
234
+ authArgs.code = arg.slice("--code=".length);
235
+ }
236
+ }
237
+ return authArgs;
238
+ }
239
+ if (args[0] === "activity") {
240
+ if (!args[1]) {
241
+ log.error("activity command requires an activity ID");
242
+ process.exit(1);
243
+ }
244
+ const id = parseInt(args[1], 10);
245
+ if (Number.isNaN(id)) {
246
+ log.error(`Invalid activity ID: ${args[1]}`);
247
+ process.exit(1);
248
+ }
249
+ let laps = false;
250
+ for (let i = 2; i < args.length; i++) {
251
+ if (args[i] === "--laps") {
252
+ laps = true;
253
+ }
254
+ }
255
+ if (!laps) {
256
+ log.error("activity command requires a subcommand flag like --laps");
257
+ process.exit(1);
258
+ }
259
+ const activityArgs = {
260
+ command: "activity",
261
+ id,
262
+ laps: true,
263
+ };
264
+ return activityArgs;
265
+ }
266
+ if (args[0] === "validate") {
267
+ if (!args[1]) {
268
+ log.error("validate command requires an input file");
269
+ process.exit(1);
270
+ }
271
+ const validateArgs = {
272
+ command: "validate",
273
+ inputFile: args[1],
274
+ compact: args.includes("--compact") || args[1].endsWith(".yaml") || args[1].endsWith(".yml"),
275
+ };
276
+ return validateArgs;
277
+ }
278
+ if (args[0] === "expand") {
279
+ if (!args[1]) {
280
+ log.error("expand command requires an input file");
281
+ process.exit(1);
282
+ }
283
+ const expandArgs = {
284
+ command: "expand",
285
+ inputFile: args[1],
286
+ };
287
+ for (let i = 2; i < args.length; i++) {
288
+ if (args[i] === "--output" || args[i] === "-o") {
289
+ expandArgs.outputFile = args[i + 1];
290
+ i++;
291
+ }
292
+ else if (args[i].startsWith("--output=")) {
293
+ expandArgs.outputFile = args[i].split("=")[1];
294
+ }
295
+ else if (args[i] === "--format") {
296
+ expandArgs.format = args[i + 1];
297
+ i++;
298
+ }
299
+ else if (args[i].startsWith("--format=")) {
300
+ expandArgs.format = args[i].split("=")[1];
301
+ }
302
+ else if (args[i] === "--verbose" || args[i] === "-v") {
303
+ expandArgs.verbose = true;
304
+ }
305
+ }
306
+ return expandArgs;
307
+ }
308
+ if (args[0] === "templates") {
309
+ const templatesArgs = {
310
+ command: "templates",
311
+ };
312
+ for (let i = 1; i < args.length; i++) {
313
+ if (args[i] === "list") {
314
+ // Default subcommand, no action needed
315
+ }
316
+ else if (args[i] === "show") {
317
+ if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
318
+ templatesArgs.show = args[i + 1];
319
+ i++;
320
+ }
321
+ }
322
+ else if (args[i] === "create") {
323
+ if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
324
+ templatesArgs.create = args[i + 1];
325
+ i++;
326
+ }
327
+ }
328
+ else if (args[i] === "validate") {
329
+ if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
330
+ templatesArgs.validate = args[i + 1];
331
+ i++;
332
+ }
333
+ }
334
+ else if (args[i] === "--sport") {
335
+ templatesArgs.sport = args[i + 1];
336
+ i++;
337
+ }
338
+ else if (args[i] === "--type") {
339
+ templatesArgs.type = args[i + 1];
340
+ i++;
341
+ }
342
+ else if (args[i] === "--source") {
343
+ const sourceVal = args[i + 1];
344
+ if (sourceVal === "user" || sourceVal === "builtin" || sourceVal === "all") {
345
+ templatesArgs.source = sourceVal;
346
+ }
347
+ else {
348
+ log.error(`Invalid source value: ${sourceVal}. Must be 'user', 'builtin', or 'all'`);
349
+ process.exit(1);
350
+ }
351
+ i++;
352
+ }
353
+ else if (args[i] === "--verbose" || args[i] === "-v") {
354
+ templatesArgs.verbose = true;
355
+ }
356
+ else if (args[i] === "--category") {
357
+ templatesArgs.category = args[i + 1];
358
+ i++;
359
+ }
360
+ else if (args[i] === "--template-file") {
361
+ templatesArgs.templateFile = args[i + 1];
362
+ i++;
363
+ }
364
+ else if (args[i] === "--overwrite") {
365
+ templatesArgs.overwrite = true;
366
+ }
367
+ else if (args[i] === "--dry-run") {
368
+ templatesArgs.dryRun = true;
369
+ }
370
+ else if (args[i] === "--example") {
371
+ templatesArgs.example = true;
372
+ }
373
+ else if (args[i].startsWith("--sport=")) {
374
+ templatesArgs.sport = args[i].split("=")[1];
375
+ }
376
+ else if (args[i].startsWith("--type=")) {
377
+ templatesArgs.type = args[i].split("=")[1];
378
+ }
379
+ else if (args[i].startsWith("--source=")) {
380
+ const sourceVal = args[i].split("=")[1];
381
+ if (sourceVal === "user" || sourceVal === "builtin" || sourceVal === "all") {
382
+ templatesArgs.source = sourceVal;
383
+ }
384
+ else {
385
+ log.error(`Invalid source value: ${sourceVal}. Must be 'user', 'builtin', or 'all'`);
386
+ process.exit(1);
387
+ }
388
+ }
389
+ else if (args[i].startsWith("--category=")) {
390
+ templatesArgs.category = args[i].split("=")[1];
391
+ }
392
+ else if (args[i].startsWith("--template-file=")) {
393
+ templatesArgs.templateFile = args[i].split("=")[1];
394
+ }
395
+ else if (!args[i].startsWith("-") &&
396
+ !templatesArgs.show &&
397
+ !templatesArgs.create &&
398
+ !templatesArgs.validate) {
399
+ // Treat as template ID for 'show' subcommand
400
+ templatesArgs.show = args[i];
401
+ }
402
+ }
403
+ return templatesArgs;
404
+ }
405
+ if (args[0] === "schema") {
406
+ return { command: "schema" };
407
+ }
408
+ if (args[0] === "modify") {
409
+ if (!args[1] || !args[2]) {
410
+ log.error("modify command requires --backup and --plan arguments");
411
+ process.exit(1);
412
+ }
413
+ const modifyArgs = {
414
+ command: "modify",
415
+ backup: "",
416
+ plan: "",
417
+ };
418
+ for (let i = 1; i < args.length; i++) {
419
+ if (args[i] === "--backup" || args[i] === "-b") {
420
+ modifyArgs.backup = args[i + 1];
421
+ i++;
422
+ }
423
+ else if (args[i].startsWith("--backup=")) {
424
+ modifyArgs.backup = args[i].split("=")[1];
425
+ }
426
+ else if (args[i] === "--plan" || args[i] === "-p") {
427
+ modifyArgs.plan = args[i + 1];
428
+ i++;
429
+ }
430
+ else if (args[i].startsWith("--plan=")) {
431
+ modifyArgs.plan = args[i].split("=")[1];
432
+ }
433
+ else if (args[i] === "--output" || args[i] === "-o") {
434
+ modifyArgs.output = args[i + 1];
435
+ i++;
436
+ }
437
+ else if (args[i].startsWith("--output=")) {
438
+ modifyArgs.output = args[i].split("=")[1];
439
+ }
440
+ }
441
+ if (!modifyArgs.backup || !modifyArgs.plan) {
442
+ log.error("Both --backup and --plan are required");
443
+ process.exit(1);
444
+ }
445
+ return modifyArgs;
446
+ }
447
+ if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
448
+ return { command: "help" };
449
+ }
450
+ log.error(`Unknown command: ${args[0]}`);
451
+ process.exit(1);
452
+ }
@@ -0,0 +1,16 @@
1
+ import type { ExpandArgs } from "../args.js";
2
+ /**
3
+ * Executes the Expand command to transform a compact plan into an expanded plan.
4
+ *
5
+ * Reads the compact plan from disk, parses and validates it, loads templates (including user templates),
6
+ * validates template references, expands the plan, and emits the expanded plan as YAML or JSON.
7
+ *
8
+ * @param args - Command arguments:
9
+ * - inputFile: Path to the compact plan file (YAML).
10
+ * - outputFile: Optional path to write the expanded plan; if omitted the result is printed to stdout.
11
+ * - format: Output format, either `"yaml"` or `"json"`.
12
+ * - verbose: When true, emits additional progress information.
13
+ *
14
+ * Exits the process with code 1 on file read, YAML parse, or validation failures.
15
+ */
16
+ export declare function runExpand(args: ExpandArgs): void;
@@ -0,0 +1,89 @@
1
+ import { readFileSync, writeFileSync } from "fs";
2
+ import { log } from "../../lib/logging.js";
3
+ import { validateCompactPlan, formatCompactValidationErrors, } from "../../schema/compact-plan.schema.js";
4
+ import { loadTemplates, parseYaml, stringifyYaml } from "../../templates/index.js";
5
+ import { expandPlan, validateWorkoutRefs } from "../../expander/index.js";
6
+ // ============================================================================
7
+ // MARK: Expand Command
8
+ // ============================================================================
9
+ /**
10
+ * Executes the Expand command to transform a compact plan into an expanded plan.
11
+ *
12
+ * Reads the compact plan from disk, parses and validates it, loads templates (including user templates),
13
+ * validates template references, expands the plan, and emits the expanded plan as YAML or JSON.
14
+ *
15
+ * @param args - Command arguments:
16
+ * - inputFile: Path to the compact plan file (YAML).
17
+ * - outputFile: Optional path to write the expanded plan; if omitted the result is printed to stdout.
18
+ * - format: Output format, either `"yaml"` or `"json"`.
19
+ * - verbose: When true, emits additional progress information.
20
+ *
21
+ * Exits the process with code 1 on file read, YAML parse, or validation failures.
22
+ */
23
+ export function runExpand(args) {
24
+ log.start("Expanding compact plan...");
25
+ // Read the compact plan
26
+ let planContent;
27
+ try {
28
+ planContent = readFileSync(args.inputFile, "utf-8");
29
+ }
30
+ catch {
31
+ log.error(`Could not read input file: ${args.inputFile}`);
32
+ process.exit(1);
33
+ }
34
+ // Parse YAML
35
+ let planData;
36
+ try {
37
+ planData = parseYaml(planContent);
38
+ }
39
+ catch {
40
+ log.error("Input file is not valid YAML");
41
+ process.exit(1);
42
+ }
43
+ // Validate compact plan
44
+ const validation = validateCompactPlan(planData);
45
+ if (!validation.success) {
46
+ log.error("Compact plan validation failed:");
47
+ console.error(formatCompactValidationErrors(validation.errors));
48
+ process.exit(1);
49
+ }
50
+ // Load templates (include user templates)
51
+ const templates = loadTemplates({ includeUserTemplates: true });
52
+ if (args.verbose) {
53
+ log.info(`Loaded ${templates.ids().length} templates`);
54
+ }
55
+ // Validate template references
56
+ const templateErrors = validateWorkoutRefs(validation.data, templates);
57
+ if (templateErrors.length > 0) {
58
+ log.warn("Compact plan validation failed with template reference warnings:");
59
+ templateErrors.forEach((e) => log.error(` - ${e}`));
60
+ process.exit(1);
61
+ }
62
+ // Expand the plan
63
+ let expanded;
64
+ try {
65
+ expanded = expandPlan(validation.data, templates);
66
+ }
67
+ catch (error) {
68
+ log.error(`Failed to expand plan: ${error instanceof Error ? error.message : "Unknown error"}`);
69
+ process.exit(1);
70
+ }
71
+ if (args.verbose) {
72
+ log.info(`Expanded ${expanded.weeks.length} weeks`);
73
+ }
74
+ // Format output + write
75
+ try {
76
+ const output = args.format === "yaml" ? stringifyYaml(expanded) : JSON.stringify(expanded, null, 2);
77
+ if (args.outputFile) {
78
+ writeFileSync(args.outputFile, output);
79
+ log.success(`Expanded plan written to: ${args.outputFile}`);
80
+ }
81
+ else {
82
+ console.log(output);
83
+ }
84
+ }
85
+ catch (error) {
86
+ log.error(`Failed to write expanded plan: ${error instanceof Error ? error.message : "Unknown error"}`);
87
+ process.exit(1);
88
+ }
89
+ }