endurance-coach 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/cli.js +318 -35
- package/dist/expander/expander.d.ts +20 -0
- package/dist/expander/expander.js +339 -0
- package/dist/expander/index.d.ts +8 -0
- package/dist/expander/index.js +9 -0
- package/dist/expander/types.d.ts +169 -0
- package/dist/expander/types.js +6 -0
- package/dist/expander/zones.d.ts +50 -0
- package/dist/expander/zones.js +159 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +9 -1
- package/dist/schema/compact-plan.d.ts +175 -0
- package/dist/schema/compact-plan.js +64 -0
- package/dist/schema/compact-plan.schema.d.ts +277 -0
- package/dist/schema/compact-plan.schema.js +205 -0
- package/dist/templates/index.d.ts +10 -0
- package/dist/templates/index.js +13 -0
- package/dist/templates/interpolate.d.ts +51 -0
- package/dist/templates/interpolate.js +204 -0
- package/dist/templates/loader.d.ts +19 -0
- package/dist/templates/loader.js +129 -0
- package/dist/templates/template.schema.d.ts +401 -0
- package/dist/templates/template.schema.js +101 -0
- package/dist/templates/template.types.d.ts +155 -0
- package/dist/templates/template.types.js +7 -0
- package/dist/templates/yaml-parser.d.ts +15 -0
- package/dist/templates/yaml-parser.js +18 -0
- package/package.json +2 -1
- package/templates/bike/CLAUDE.md +7 -0
- package/templates/bike/easy.yaml +38 -0
- package/templates/bike/endurance.yaml +42 -0
- package/templates/bike/hills.yaml +80 -0
- package/templates/bike/overunders.yaml +81 -0
- package/templates/bike/rest.yaml +16 -0
- package/templates/bike/sweetspot.yaml +80 -0
- package/templates/bike/tempo.yaml +79 -0
- package/templates/bike/threshold.yaml +83 -0
- package/templates/bike/vo2max.yaml +84 -0
- package/templates/brick/CLAUDE.md +7 -0
- package/templates/brick/halfironman.yaml +72 -0
- package/templates/brick/ironman.yaml +72 -0
- package/templates/brick/olympic.yaml +70 -0
- package/templates/brick/sprint.yaml +70 -0
- package/templates/plan-viewer.html +22 -22
- package/templates/run/CLAUDE.md +7 -0
- package/templates/run/easy.yaml +36 -0
- package/templates/run/fartlek.yaml +40 -0
- package/templates/run/hills.yaml +36 -0
- package/templates/run/intervals.1k.yaml +63 -0
- package/templates/run/intervals.400.yaml +63 -0
- package/templates/run/intervals.800.yaml +63 -0
- package/templates/run/intervals.mile.yaml +64 -0
- package/templates/run/long.yaml +41 -0
- package/templates/run/progression.yaml +49 -0
- package/templates/run/race.5k.yaml +36 -0
- package/templates/run/recovery.yaml +36 -0
- package/templates/run/rest.yaml +16 -0
- package/templates/run/strides.yaml +49 -0
- package/templates/run/tempo.yaml +56 -0
- package/templates/run/threshold.yaml +56 -0
- package/templates/strength/CLAUDE.md +7 -0
- package/templates/strength/core.yaml +56 -0
- package/templates/strength/foundation.yaml +65 -0
- package/templates/strength/full.yaml +73 -0
- package/templates/strength/maintenance.yaml +62 -0
- package/templates/swim/CLAUDE.md +7 -0
- package/templates/swim/aerobic.yaml +67 -0
- package/templates/swim/easy.yaml +51 -0
- package/templates/swim/openwater.yaml +60 -0
- package/templates/swim/rest.yaml +16 -0
- package/templates/swim/technique.yaml +67 -0
- package/templates/swim/threshold.yaml +75 -0
- package/templates/swim/vo2max.yaml +88 -0
- /package/bin/{claude-coach.js → endurance-coach.js} +0 -0
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Endurance Coach
|
|
2
2
|
|
|
3
|
+
Originally forked from Claude Coach.
|
|
4
|
+
This project is now independently maintained and has evolved with a different architecture and goals, focusing on agent-first, schema-driven endurance planning.
|
|
5
|
+
|
|
3
6
|
Endurance Coach allows you to use Claude (or any AI assistant) to create custom-tailored training programs for triathlons, marathons, and other endurance activities. Using a data-driven approach and principles from top training plans, the AI will create a training plan that's uniquely fit for you, your personal fitness, and the constraints you have in the next couple of weeks. Maybe you're recovering from an injury, maybe you're traveling and don't have access to a pool or track in a certain week - tell the AI about it and it'll create a plan that works for you.
|
|
4
7
|
|
|
5
8
|
The output is a beautiful training plan app that allows you to add, edit, or move workouts, mark them as complete, and update key training data like heart rate zones, LTHR, threshold paces, FTP, and others. Your data is kept locally in your browser.
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,9 @@ import { dirname, join } from "path";
|
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
import { ProxyAgent, setGlobalDispatcher } from "undici";
|
|
11
11
|
import { validatePlan, formatValidationErrors } from "./schema/training-plan.schema.js";
|
|
12
|
+
import { validateCompactPlan, formatCompactValidationErrors, } from "./schema/compact-plan.schema.js";
|
|
13
|
+
import { loadTemplates, parseYaml, stringifyYaml } from "./templates/index.js";
|
|
14
|
+
import { expandPlan, validateWorkoutRefs } from "./expander/index.js";
|
|
12
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
16
|
// ============================================================================
|
|
14
17
|
// Proxy Configuration
|
|
@@ -62,6 +65,10 @@ function parseArgs() {
|
|
|
62
65
|
else if (args[i].startsWith("--output=")) {
|
|
63
66
|
renderArgs.outputFile = args[i].split("=")[1];
|
|
64
67
|
}
|
|
68
|
+
else if (!args[i].startsWith("-") && !renderArgs.outputFile) {
|
|
69
|
+
// Accept positional output argument (helps when npm consumes -o)
|
|
70
|
+
renderArgs.outputFile = args[i];
|
|
71
|
+
}
|
|
65
72
|
}
|
|
66
73
|
return renderArgs;
|
|
67
74
|
}
|
|
@@ -97,10 +104,61 @@ function parseArgs() {
|
|
|
97
104
|
log.error("validate command requires an input file");
|
|
98
105
|
process.exit(1);
|
|
99
106
|
}
|
|
100
|
-
|
|
107
|
+
const validateArgs = {
|
|
101
108
|
command: "validate",
|
|
102
109
|
inputFile: args[1],
|
|
110
|
+
compact: args.includes("--compact") || args[1].endsWith(".yaml") || args[1].endsWith(".yml"),
|
|
111
|
+
};
|
|
112
|
+
return validateArgs;
|
|
113
|
+
}
|
|
114
|
+
if (args[0] === "expand") {
|
|
115
|
+
if (!args[1]) {
|
|
116
|
+
log.error("expand command requires an input file");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const expandArgs = {
|
|
120
|
+
command: "expand",
|
|
121
|
+
inputFile: args[1],
|
|
122
|
+
};
|
|
123
|
+
for (let i = 2; i < args.length; i++) {
|
|
124
|
+
if (args[i] === "--output" || args[i] === "-o") {
|
|
125
|
+
expandArgs.outputFile = args[i + 1];
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
else if (args[i].startsWith("--output=")) {
|
|
129
|
+
expandArgs.outputFile = args[i].split("=")[1];
|
|
130
|
+
}
|
|
131
|
+
else if (args[i] === "--format") {
|
|
132
|
+
expandArgs.format = args[i + 1];
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
135
|
+
else if (args[i].startsWith("--format=")) {
|
|
136
|
+
expandArgs.format = args[i].split("=")[1];
|
|
137
|
+
}
|
|
138
|
+
else if (args[i] === "--verbose" || args[i] === "-v") {
|
|
139
|
+
expandArgs.verbose = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return expandArgs;
|
|
143
|
+
}
|
|
144
|
+
if (args[0] === "templates") {
|
|
145
|
+
const templatesArgs = {
|
|
146
|
+
command: "templates",
|
|
103
147
|
};
|
|
148
|
+
for (let i = 1; i < args.length; i++) {
|
|
149
|
+
if (args[i] === "--sport") {
|
|
150
|
+
templatesArgs.sport = args[i + 1];
|
|
151
|
+
i++;
|
|
152
|
+
}
|
|
153
|
+
else if (args[i].startsWith("--sport=")) {
|
|
154
|
+
templatesArgs.sport = args[i].split("=")[1];
|
|
155
|
+
}
|
|
156
|
+
else if (args[i] === "show") {
|
|
157
|
+
templatesArgs.show = args[i + 1];
|
|
158
|
+
i++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return templatesArgs;
|
|
104
162
|
}
|
|
105
163
|
if (args[0] === "schema") {
|
|
106
164
|
return { command: "schema" };
|
|
@@ -160,8 +218,10 @@ Commands:
|
|
|
160
218
|
sync Sync activities from Strava
|
|
161
219
|
auth Get Strava authorization URL or exchange code for tokens
|
|
162
220
|
schema Print the training plan JSON schema reference
|
|
163
|
-
validate <file> Validate a training plan
|
|
164
|
-
|
|
221
|
+
validate <file> Validate a training plan against the schema
|
|
222
|
+
expand <file> Expand a compact YAML plan to full JSON format
|
|
223
|
+
render <file> Render a training plan to HTML
|
|
224
|
+
templates List available workout templates
|
|
165
225
|
query <sql> Run a SQL query against the database
|
|
166
226
|
modify Apply backup changes to a training plan
|
|
167
227
|
help Show this help message
|
|
@@ -181,6 +241,18 @@ Sync Options:
|
|
|
181
241
|
--client-secret=SEC Strava API client secret (for OAuth flow)
|
|
182
242
|
--days=N Days of history to sync (default: 730)
|
|
183
243
|
|
|
244
|
+
Validate Options:
|
|
245
|
+
--compact Force compact plan validation (auto-detected for .yaml files)
|
|
246
|
+
|
|
247
|
+
Expand Options:
|
|
248
|
+
--output, -o FILE Output file (default: stdout)
|
|
249
|
+
--format json|yaml Output format (default: json)
|
|
250
|
+
--verbose, -v Show template resolution details
|
|
251
|
+
|
|
252
|
+
Templates Options:
|
|
253
|
+
--sport SPORT Filter by sport (run, bike, swim)
|
|
254
|
+
show <template-id> Show details of a specific template
|
|
255
|
+
|
|
184
256
|
Render Options:
|
|
185
257
|
--output, -o FILE Output HTML file (default: <input>.html)
|
|
186
258
|
|
|
@@ -497,32 +569,62 @@ function getTemplatePath() {
|
|
|
497
569
|
}
|
|
498
570
|
function runRender(args) {
|
|
499
571
|
log.start("Rendering training plan...");
|
|
500
|
-
|
|
501
|
-
|
|
572
|
+
const isCompact = args.inputFile.endsWith(".yaml") || args.inputFile.endsWith(".yml");
|
|
573
|
+
// Read the plan file
|
|
574
|
+
let planContent;
|
|
502
575
|
try {
|
|
503
|
-
|
|
576
|
+
planContent = readFileSync(args.inputFile, "utf-8");
|
|
504
577
|
}
|
|
505
|
-
catch
|
|
578
|
+
catch {
|
|
506
579
|
log.error(`Could not read input file: ${args.inputFile}`);
|
|
507
580
|
process.exit(1);
|
|
508
581
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
582
|
+
let planJson;
|
|
583
|
+
if (isCompact) {
|
|
584
|
+
// Handle compact YAML plan
|
|
585
|
+
log.info("Detected compact YAML plan, expanding...");
|
|
586
|
+
// Parse YAML
|
|
587
|
+
let compactData;
|
|
588
|
+
try {
|
|
589
|
+
compactData = parseYaml(planContent);
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
log.error("Input file is not valid YAML");
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
// Validate compact plan
|
|
596
|
+
const compactValidation = validateCompactPlan(compactData);
|
|
597
|
+
if (!compactValidation.success) {
|
|
598
|
+
log.error("Compact plan validation failed:");
|
|
599
|
+
console.error(formatCompactValidationErrors(compactValidation.errors));
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
// Load templates and expand
|
|
603
|
+
const templates = loadTemplates();
|
|
604
|
+
const expanded = expandPlan(compactValidation.data, templates);
|
|
605
|
+
log.success("Plan expanded successfully");
|
|
606
|
+
planJson = JSON.stringify(expanded, null, 2);
|
|
517
607
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
608
|
+
else {
|
|
609
|
+
// Handle full JSON plan
|
|
610
|
+
let planData;
|
|
611
|
+
try {
|
|
612
|
+
planData = JSON.parse(planContent);
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
log.error("Input file is not valid JSON");
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
// Validate against schema
|
|
619
|
+
const validation = validatePlan(planData);
|
|
620
|
+
if (!validation.success) {
|
|
621
|
+
log.error("Training plan validation failed:");
|
|
622
|
+
console.error(formatValidationErrors(validation.errors));
|
|
623
|
+
process.exit(1);
|
|
624
|
+
}
|
|
625
|
+
log.success("Plan schema validated successfully");
|
|
626
|
+
planJson = planContent;
|
|
524
627
|
}
|
|
525
|
-
log.success("Plan schema validated successfully");
|
|
526
628
|
// Read the template
|
|
527
629
|
const templatePath = getTemplatePath();
|
|
528
630
|
let template = readFileSync(templatePath, "utf-8");
|
|
@@ -558,33 +660,208 @@ async function runQuery(args) {
|
|
|
558
660
|
// Validate Command
|
|
559
661
|
// ============================================================================
|
|
560
662
|
function runValidate(args) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
663
|
+
const isCompact = args.compact;
|
|
664
|
+
log.start(`Validating ${isCompact ? "compact" : "full"} training plan...`);
|
|
665
|
+
// Read the plan file
|
|
666
|
+
let planContent;
|
|
564
667
|
try {
|
|
565
|
-
|
|
668
|
+
planContent = readFileSync(args.inputFile, "utf-8");
|
|
566
669
|
}
|
|
567
|
-
catch
|
|
670
|
+
catch {
|
|
568
671
|
log.error(`Could not read input file: ${args.inputFile}`);
|
|
569
672
|
process.exit(1);
|
|
570
673
|
}
|
|
571
|
-
// Parse JSON
|
|
674
|
+
// Parse content (YAML or JSON)
|
|
572
675
|
let planData;
|
|
573
676
|
try {
|
|
574
|
-
|
|
677
|
+
if (args.inputFile.endsWith(".yaml") || args.inputFile.endsWith(".yml")) {
|
|
678
|
+
planData = parseYaml(planContent);
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
planData = JSON.parse(planContent);
|
|
682
|
+
}
|
|
575
683
|
}
|
|
576
684
|
catch (err) {
|
|
577
|
-
log.error(
|
|
685
|
+
log.error(`Input file is not valid ${isCompact ? "YAML" : "JSON"}`);
|
|
578
686
|
process.exit(1);
|
|
579
687
|
}
|
|
580
|
-
|
|
581
|
-
|
|
688
|
+
if (isCompact) {
|
|
689
|
+
// Validate against compact schema
|
|
690
|
+
const validation = validateCompactPlan(planData);
|
|
691
|
+
if (!validation.success) {
|
|
692
|
+
log.error("Compact plan validation failed:");
|
|
693
|
+
console.error(formatCompactValidationErrors(validation.errors));
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
// Also validate template references
|
|
697
|
+
const templates = loadTemplates();
|
|
698
|
+
const templateErrors = validateWorkoutRefs(validation.data, templates);
|
|
699
|
+
if (templateErrors.length > 0) {
|
|
700
|
+
log.warn("Template reference warnings:");
|
|
701
|
+
templateErrors.forEach((e) => console.error(` - ${e}`));
|
|
702
|
+
}
|
|
703
|
+
log.success("Compact plan is valid!");
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
// Validate against full schema
|
|
707
|
+
const validation = validatePlan(planData);
|
|
708
|
+
if (!validation.success) {
|
|
709
|
+
log.error("Validation failed:");
|
|
710
|
+
console.error(formatValidationErrors(validation.errors));
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
log.success("Plan is valid!");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// ============================================================================
|
|
717
|
+
// Expand Command
|
|
718
|
+
// ============================================================================
|
|
719
|
+
function runExpand(args) {
|
|
720
|
+
log.start("Expanding compact plan...");
|
|
721
|
+
// Read the compact plan
|
|
722
|
+
let planContent;
|
|
723
|
+
try {
|
|
724
|
+
planContent = readFileSync(args.inputFile, "utf-8");
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
log.error(`Could not read input file: ${args.inputFile}`);
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
// Parse YAML
|
|
731
|
+
let planData;
|
|
732
|
+
try {
|
|
733
|
+
planData = parseYaml(planContent);
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
log.error("Input file is not valid YAML");
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|
|
739
|
+
// Validate compact plan
|
|
740
|
+
const validation = validateCompactPlan(planData);
|
|
582
741
|
if (!validation.success) {
|
|
583
|
-
log.error("
|
|
584
|
-
console.error(
|
|
742
|
+
log.error("Compact plan validation failed:");
|
|
743
|
+
console.error(formatCompactValidationErrors(validation.errors));
|
|
585
744
|
process.exit(1);
|
|
586
745
|
}
|
|
587
|
-
|
|
746
|
+
// Load templates
|
|
747
|
+
const templates = loadTemplates();
|
|
748
|
+
if (args.verbose) {
|
|
749
|
+
log.info(`Loaded ${templates.ids().length} templates`);
|
|
750
|
+
}
|
|
751
|
+
// Validate template references
|
|
752
|
+
const templateErrors = validateWorkoutRefs(validation.data, templates);
|
|
753
|
+
if (templateErrors.length > 0) {
|
|
754
|
+
log.warn("Template reference warnings:");
|
|
755
|
+
templateErrors.forEach((e) => console.error(` - ${e}`));
|
|
756
|
+
}
|
|
757
|
+
// Expand the plan
|
|
758
|
+
const expanded = expandPlan(validation.data, templates);
|
|
759
|
+
if (args.verbose) {
|
|
760
|
+
log.info(`Expanded ${expanded.weeks.length} weeks`);
|
|
761
|
+
}
|
|
762
|
+
// Format output
|
|
763
|
+
let output;
|
|
764
|
+
if (args.format === "yaml") {
|
|
765
|
+
output = stringifyYaml(expanded);
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
output = JSON.stringify(expanded, null, 2);
|
|
769
|
+
}
|
|
770
|
+
// Write output
|
|
771
|
+
if (args.outputFile) {
|
|
772
|
+
writeFileSync(args.outputFile, output);
|
|
773
|
+
log.success(`Expanded plan written to: ${args.outputFile}`);
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
console.log(output);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// ============================================================================
|
|
780
|
+
// Templates Command
|
|
781
|
+
// ============================================================================
|
|
782
|
+
function runTemplates(args) {
|
|
783
|
+
const templates = loadTemplates();
|
|
784
|
+
if (args.show) {
|
|
785
|
+
// Show details of a specific template
|
|
786
|
+
const template = templates.get(args.show);
|
|
787
|
+
if (!template) {
|
|
788
|
+
log.error(`Template not found: ${args.show}`);
|
|
789
|
+
console.log("\nAvailable templates:");
|
|
790
|
+
templates.ids().forEach((id) => console.log(` - ${id}`));
|
|
791
|
+
process.exit(1);
|
|
792
|
+
}
|
|
793
|
+
console.log(`\n${template.name} (${template.id})`);
|
|
794
|
+
console.log(`${"=".repeat(template.name.length + template.id.length + 3)}`);
|
|
795
|
+
console.log(`\nSport: ${template.sport}`);
|
|
796
|
+
console.log(`Category: ${template.category}`);
|
|
797
|
+
console.log(`Type: ${template.type}`);
|
|
798
|
+
if (template.targetZone) {
|
|
799
|
+
console.log(`Target Zone: ${template.targetZone}`);
|
|
800
|
+
}
|
|
801
|
+
if (template.rpe) {
|
|
802
|
+
console.log(`RPE: ${template.rpe}`);
|
|
803
|
+
}
|
|
804
|
+
if (template.estimatedDuration) {
|
|
805
|
+
console.log(`Estimated Duration: ${template.estimatedDuration} min`);
|
|
806
|
+
}
|
|
807
|
+
if (template.params && Object.keys(template.params).length > 0) {
|
|
808
|
+
console.log("\nParameters:");
|
|
809
|
+
for (const [name, param] of Object.entries(template.params)) {
|
|
810
|
+
const required = param.required ? " (required)" : "";
|
|
811
|
+
const defaultVal = param.default !== undefined ? ` [default: ${param.default}]` : "";
|
|
812
|
+
console.log(` - ${name}: ${param.type}${required}${defaultVal}`);
|
|
813
|
+
if (param.description) {
|
|
814
|
+
console.log(` ${param.description}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
console.log("\nUsage examples:");
|
|
819
|
+
const paramNames = template.params ? Object.keys(template.params) : [];
|
|
820
|
+
if (paramNames.length === 0) {
|
|
821
|
+
console.log(` ${template.id}`);
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
const defaults = paramNames
|
|
825
|
+
.filter((p) => template.params[p].default !== undefined)
|
|
826
|
+
.map((p) => template.params[p].default);
|
|
827
|
+
if (defaults.length > 0) {
|
|
828
|
+
console.log(` ${template.id}(${defaults.join(", ")})`);
|
|
829
|
+
}
|
|
830
|
+
console.log(` ${template.id}(${paramNames.join(", ")})`);
|
|
831
|
+
}
|
|
832
|
+
console.log("\nWorkout description:");
|
|
833
|
+
console.log(template.humanReadable);
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
// List all templates
|
|
837
|
+
const sport = args.sport;
|
|
838
|
+
const list = templates.list(sport);
|
|
839
|
+
if (list.length === 0) {
|
|
840
|
+
console.log("No templates found.");
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
console.log(`\nAvailable Templates${sport ? ` (${sport})` : ""}:`);
|
|
844
|
+
console.log("=".repeat(40));
|
|
845
|
+
// Group by category
|
|
846
|
+
const byCategory = new Map();
|
|
847
|
+
for (const t of list) {
|
|
848
|
+
const cat = t.category;
|
|
849
|
+
if (!byCategory.has(cat)) {
|
|
850
|
+
byCategory.set(cat, []);
|
|
851
|
+
}
|
|
852
|
+
byCategory.get(cat).push(t);
|
|
853
|
+
}
|
|
854
|
+
for (const [category, categoryTemplates] of byCategory) {
|
|
855
|
+
console.log(`\n${category.toUpperCase()}:`);
|
|
856
|
+
for (const t of categoryTemplates) {
|
|
857
|
+
const params = t.params ? Object.keys(t.params) : [];
|
|
858
|
+
const paramStr = params.length > 0 ? `(${params.join(", ")})` : "";
|
|
859
|
+
console.log(` ${t.id}${paramStr} - ${t.name}`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
console.log(`\nTotal: ${list.length} templates`);
|
|
863
|
+
console.log("\nUse 'endurance-coach templates show <id>' to see template details.");
|
|
864
|
+
}
|
|
588
865
|
}
|
|
589
866
|
// ============================================================================
|
|
590
867
|
// Schema Command
|
|
@@ -1060,6 +1337,12 @@ async function main() {
|
|
|
1060
1337
|
case "validate":
|
|
1061
1338
|
runValidate(args);
|
|
1062
1339
|
break;
|
|
1340
|
+
case "expand":
|
|
1341
|
+
runExpand(args);
|
|
1342
|
+
break;
|
|
1343
|
+
case "templates":
|
|
1344
|
+
runTemplates(args);
|
|
1345
|
+
break;
|
|
1063
1346
|
case "render":
|
|
1064
1347
|
runRender(args);
|
|
1065
1348
|
break;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Expander
|
|
3
|
+
*
|
|
4
|
+
* Converts compact training plans to expanded format for HTML rendering.
|
|
5
|
+
*/
|
|
6
|
+
import type { CompactPlan } from "../schema/compact-plan.js";
|
|
7
|
+
import type { TemplateRegistry, InterpolationContext } from "../templates/index.js";
|
|
8
|
+
import type { ExpandedPlan, ExpandedWorkout, ExpansionOptions } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Expand a single workout from its template reference.
|
|
11
|
+
*/
|
|
12
|
+
export declare function expandWorkout(ref: string, workoutId: string, context: InterpolationContext, templates: TemplateRegistry): ExpandedWorkout;
|
|
13
|
+
/**
|
|
14
|
+
* Expand a compact plan into the full format for HTML rendering.
|
|
15
|
+
*/
|
|
16
|
+
export declare function expandPlan(compact: CompactPlan, templates: TemplateRegistry, options?: ExpansionOptions): ExpandedPlan;
|
|
17
|
+
/**
|
|
18
|
+
* Validate that all workout references in a compact plan have corresponding templates.
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateWorkoutRefs(compact: CompactPlan, templates: TemplateRegistry): string[];
|