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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variable Interpolation Engine
|
|
3
|
+
*
|
|
4
|
+
* Replaces ${variable} patterns in template strings with actual values.
|
|
5
|
+
* Supports:
|
|
6
|
+
* - Simple variables: ${paces.easy} → "5:30/km"
|
|
7
|
+
* - Math expressions: ${10 + (reps * 3)} → "22"
|
|
8
|
+
* - Nested access: ${zones.hr.lthr} → "170"
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Pattern to match ${...} interpolation markers.
|
|
12
|
+
*/
|
|
13
|
+
const INTERPOLATION_PATTERN = /\$\{([^}]+)\}/g;
|
|
14
|
+
/**
|
|
15
|
+
* Pattern to detect if a string contains a math expression.
|
|
16
|
+
*/
|
|
17
|
+
const MATH_EXPRESSION_PATTERN = /[+\-*/%()]/;
|
|
18
|
+
/**
|
|
19
|
+
* Get a nested value from an object using dot notation.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* getNestedValue({ paces: { easy: "5:30" } }, "paces.easy") // "5:30"
|
|
23
|
+
* getNestedValue({ a: { b: { c: 1 } } }, "a.b.c") // 1
|
|
24
|
+
*/
|
|
25
|
+
function getNestedValue(obj, path) {
|
|
26
|
+
const parts = path.split(".");
|
|
27
|
+
let current = obj;
|
|
28
|
+
for (const part of parts) {
|
|
29
|
+
if (current === null || current === undefined) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (typeof current !== "object") {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
current = current[part];
|
|
36
|
+
}
|
|
37
|
+
return current;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Evaluate a simple math expression with variable substitution.
|
|
41
|
+
*
|
|
42
|
+
* This uses a safe evaluation approach that only allows basic math operations.
|
|
43
|
+
* Variables in the expression are first replaced with their values from context.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* evaluateExpression("10 + (reps * 3)", { reps: 4 }) // 22
|
|
47
|
+
*/
|
|
48
|
+
export function evaluateExpression(expr, context) {
|
|
49
|
+
// First, replace any variable references in the expression
|
|
50
|
+
let substituted = expr;
|
|
51
|
+
// Find all word tokens that could be variables
|
|
52
|
+
const variablePattern = /\b([a-zA-Z_][a-zA-Z0-9_.]*)\b/g;
|
|
53
|
+
let match;
|
|
54
|
+
while ((match = variablePattern.exec(expr)) !== null) {
|
|
55
|
+
const varName = match[1];
|
|
56
|
+
// Skip JavaScript keywords and numbers
|
|
57
|
+
if (["true", "false", "null", "undefined", "NaN", "Infinity"].includes(varName)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// Try to get the value from context
|
|
61
|
+
const value = getNestedValue(context, varName);
|
|
62
|
+
if (value !== undefined) {
|
|
63
|
+
// Convert to appropriate format for the expression
|
|
64
|
+
let replacement;
|
|
65
|
+
if (typeof value === "number") {
|
|
66
|
+
replacement = String(value);
|
|
67
|
+
}
|
|
68
|
+
else if (typeof value === "string") {
|
|
69
|
+
// If it's a string that looks like a number, use it directly
|
|
70
|
+
const numValue = parseFloat(value);
|
|
71
|
+
if (!isNaN(numValue)) {
|
|
72
|
+
replacement = String(numValue);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// For non-numeric strings, quote them
|
|
76
|
+
replacement = `"${value}"`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
replacement = String(value);
|
|
81
|
+
}
|
|
82
|
+
// Replace this specific occurrence
|
|
83
|
+
substituted = substituted.replace(new RegExp(`\\b${varName}\\b`, "g"), replacement);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Check if the result looks like a math expression
|
|
87
|
+
if (MATH_EXPRESSION_PATTERN.test(substituted)) {
|
|
88
|
+
try {
|
|
89
|
+
// Safe evaluation using Function constructor
|
|
90
|
+
// Only allow numbers, operators, and parentheses
|
|
91
|
+
const sanitized = substituted.replace(/[^0-9+\-*/().%\s]/g, "");
|
|
92
|
+
if (sanitized.trim() !== substituted.trim()) {
|
|
93
|
+
// Expression contained invalid characters after substitution
|
|
94
|
+
return substituted;
|
|
95
|
+
}
|
|
96
|
+
// Evaluate the expression
|
|
97
|
+
const fn = new Function(`return (${sanitized})`);
|
|
98
|
+
const result = fn();
|
|
99
|
+
if (typeof result === "number" && !isNaN(result) && isFinite(result)) {
|
|
100
|
+
// Round to reasonable precision
|
|
101
|
+
return Math.round(result * 100) / 100;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// If evaluation fails, return the substituted string
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Return the substituted string (might be a simple value)
|
|
109
|
+
return substituted;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Interpolate all ${variable} patterns in a string.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* interpolate("Run ${duration} min @ ${paces.easy}", {
|
|
116
|
+
* duration: 30,
|
|
117
|
+
* paces: { easy: "5:30/km" }
|
|
118
|
+
* })
|
|
119
|
+
* // "Run 30 min @ 5:30/km"
|
|
120
|
+
*/
|
|
121
|
+
export function interpolate(template, context) {
|
|
122
|
+
return template.replace(INTERPOLATION_PATTERN, (_, expr) => {
|
|
123
|
+
const trimmed = expr.trim();
|
|
124
|
+
// Check if it's a simple variable reference (no math)
|
|
125
|
+
if (!MATH_EXPRESSION_PATTERN.test(trimmed)) {
|
|
126
|
+
const value = getNestedValue(context, trimmed);
|
|
127
|
+
if (value !== undefined && value !== null) {
|
|
128
|
+
return String(value);
|
|
129
|
+
}
|
|
130
|
+
// Return placeholder if variable not found
|
|
131
|
+
return `\${${trimmed}}`;
|
|
132
|
+
}
|
|
133
|
+
// It's an expression - evaluate it
|
|
134
|
+
const result = evaluateExpression(trimmed, context);
|
|
135
|
+
return String(result);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Interpolate all string values in an object recursively.
|
|
140
|
+
*/
|
|
141
|
+
export function interpolateObject(obj, context) {
|
|
142
|
+
if (typeof obj === "string") {
|
|
143
|
+
return interpolate(obj, context);
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(obj)) {
|
|
146
|
+
return obj.map((item) => interpolateObject(item, context));
|
|
147
|
+
}
|
|
148
|
+
if (obj !== null && typeof obj === "object") {
|
|
149
|
+
const result = {};
|
|
150
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
151
|
+
result[key] = interpolateObject(value, context);
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
return obj;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Create an interpolation context from a compact plan's athlete data.
|
|
159
|
+
*/
|
|
160
|
+
export function createContext(paces, zones, params) {
|
|
161
|
+
return {
|
|
162
|
+
paces,
|
|
163
|
+
zones,
|
|
164
|
+
...params,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Check if a string contains any interpolation markers.
|
|
169
|
+
*/
|
|
170
|
+
export function hasInterpolation(str) {
|
|
171
|
+
return INTERPOLATION_PATTERN.test(str);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Extract all variable names from a template string.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* extractVariables("Run ${duration} min @ ${paces.easy}")
|
|
178
|
+
* // ["duration", "paces.easy"]
|
|
179
|
+
*/
|
|
180
|
+
export function extractVariables(template) {
|
|
181
|
+
const variables = [];
|
|
182
|
+
let match;
|
|
183
|
+
// Reset the regex
|
|
184
|
+
const pattern = new RegExp(INTERPOLATION_PATTERN.source, "g");
|
|
185
|
+
while ((match = pattern.exec(template)) !== null) {
|
|
186
|
+
const expr = match[1].trim();
|
|
187
|
+
// For simple variables, add directly
|
|
188
|
+
if (!MATH_EXPRESSION_PATTERN.test(expr)) {
|
|
189
|
+
variables.push(expr);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// For expressions, extract variable names
|
|
193
|
+
const varPattern = /\b([a-zA-Z_][a-zA-Z0-9_.]*)\b/g;
|
|
194
|
+
let varMatch;
|
|
195
|
+
while ((varMatch = varPattern.exec(expr)) !== null) {
|
|
196
|
+
const varName = varMatch[1];
|
|
197
|
+
if (!["true", "false", "null", "undefined", "NaN", "Infinity"].includes(varName)) {
|
|
198
|
+
variables.push(varName);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return [...new Set(variables)];
|
|
204
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads workout templates from YAML files in the templates directory.
|
|
5
|
+
*/
|
|
6
|
+
import { type WorkoutTemplate } from "./template.schema.js";
|
|
7
|
+
import type { TemplateRegistry } from "./template.types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Load all templates from the templates directory.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadTemplates(templatesDir?: string): TemplateRegistry;
|
|
12
|
+
/**
|
|
13
|
+
* Load templates from an array of template objects (for testing or embedded templates).
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadTemplatesFromArray(templateArray: WorkoutTemplate[]): TemplateRegistry;
|
|
16
|
+
/**
|
|
17
|
+
* Get the path to the templates directory.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getTemplatesPath(): string;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads workout templates from YAML files in the templates directory.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
7
|
+
import { join, dirname, basename } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { parse as parseYaml } from "./yaml-parser.js";
|
|
10
|
+
import { validateTemplateOrThrow } from "./template.schema.js";
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
/**
|
|
14
|
+
* Default templates directory location.
|
|
15
|
+
* In development: ./templates relative to project root
|
|
16
|
+
* In production: bundled with the package
|
|
17
|
+
*/
|
|
18
|
+
function getTemplatesDir() {
|
|
19
|
+
// Try multiple locations
|
|
20
|
+
const locations = [
|
|
21
|
+
join(__dirname, "..", "..", "templates"), // From dist/templates
|
|
22
|
+
join(__dirname, "..", "templates"), // From src/templates
|
|
23
|
+
join(process.cwd(), "templates"), // From current working directory
|
|
24
|
+
];
|
|
25
|
+
for (const loc of locations) {
|
|
26
|
+
if (existsSync(loc)) {
|
|
27
|
+
return loc;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Templates directory not found. Searched: ${locations.join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Recursively find all YAML files in a directory.
|
|
34
|
+
*/
|
|
35
|
+
function findYamlFiles(dir) {
|
|
36
|
+
const files = [];
|
|
37
|
+
if (!existsSync(dir)) {
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
const entries = readdirSync(dir);
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const fullPath = join(dir, entry);
|
|
43
|
+
const stat = statSync(fullPath);
|
|
44
|
+
if (stat.isDirectory()) {
|
|
45
|
+
files.push(...findYamlFiles(fullPath));
|
|
46
|
+
}
|
|
47
|
+
else if (stat.isFile() && (entry.endsWith(".yaml") || entry.endsWith(".yml"))) {
|
|
48
|
+
files.push(fullPath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return files;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Load a single template from a YAML file.
|
|
55
|
+
*/
|
|
56
|
+
function loadTemplateFile(filePath) {
|
|
57
|
+
const content = readFileSync(filePath, "utf-8");
|
|
58
|
+
const data = parseYaml(content);
|
|
59
|
+
// Validate against schema
|
|
60
|
+
return validateTemplateOrThrow(data);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Load all templates from the templates directory.
|
|
64
|
+
*/
|
|
65
|
+
export function loadTemplates(templatesDir) {
|
|
66
|
+
const dir = templatesDir ?? getTemplatesDir();
|
|
67
|
+
const yamlFiles = findYamlFiles(dir);
|
|
68
|
+
const templates = new Map();
|
|
69
|
+
for (const file of yamlFiles) {
|
|
70
|
+
try {
|
|
71
|
+
const template = loadTemplateFile(file);
|
|
72
|
+
if (templates.has(template.id)) {
|
|
73
|
+
console.warn(`Duplicate template ID: ${template.id} in ${file}`);
|
|
74
|
+
}
|
|
75
|
+
templates.set(template.id, template);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const fileName = basename(file);
|
|
79
|
+
if (error instanceof Error) {
|
|
80
|
+
console.error(`Failed to load template ${fileName}: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.error(`Failed to load template ${fileName}: Unknown error`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return createRegistry(templates);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Load templates from an array of template objects (for testing or embedded templates).
|
|
91
|
+
*/
|
|
92
|
+
export function loadTemplatesFromArray(templateArray) {
|
|
93
|
+
const templates = new Map();
|
|
94
|
+
for (const template of templateArray) {
|
|
95
|
+
const validated = validateTemplateOrThrow(template);
|
|
96
|
+
templates.set(validated.id, validated);
|
|
97
|
+
}
|
|
98
|
+
return createRegistry(templates);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create a template registry from a Map of templates.
|
|
102
|
+
*/
|
|
103
|
+
function createRegistry(templates) {
|
|
104
|
+
return {
|
|
105
|
+
templates,
|
|
106
|
+
get(id) {
|
|
107
|
+
return templates.get(id);
|
|
108
|
+
},
|
|
109
|
+
list(sport) {
|
|
110
|
+
const all = Array.from(templates.values());
|
|
111
|
+
if (!sport) {
|
|
112
|
+
return all;
|
|
113
|
+
}
|
|
114
|
+
return all.filter((t) => t.sport === sport);
|
|
115
|
+
},
|
|
116
|
+
has(id) {
|
|
117
|
+
return templates.has(id);
|
|
118
|
+
},
|
|
119
|
+
ids() {
|
|
120
|
+
return Array.from(templates.keys());
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get the path to the templates directory.
|
|
126
|
+
*/
|
|
127
|
+
export function getTemplatesPath() {
|
|
128
|
+
return getTemplatesDir();
|
|
129
|
+
}
|