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.
Files changed (75) hide show
  1. package/README.md +3 -0
  2. package/dist/cli.js +318 -35
  3. package/dist/expander/expander.d.ts +20 -0
  4. package/dist/expander/expander.js +339 -0
  5. package/dist/expander/index.d.ts +8 -0
  6. package/dist/expander/index.js +9 -0
  7. package/dist/expander/types.d.ts +169 -0
  8. package/dist/expander/types.js +6 -0
  9. package/dist/expander/zones.d.ts +50 -0
  10. package/dist/expander/zones.js +159 -0
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.js +9 -1
  13. package/dist/schema/compact-plan.d.ts +175 -0
  14. package/dist/schema/compact-plan.js +64 -0
  15. package/dist/schema/compact-plan.schema.d.ts +277 -0
  16. package/dist/schema/compact-plan.schema.js +205 -0
  17. package/dist/templates/index.d.ts +10 -0
  18. package/dist/templates/index.js +13 -0
  19. package/dist/templates/interpolate.d.ts +51 -0
  20. package/dist/templates/interpolate.js +204 -0
  21. package/dist/templates/loader.d.ts +19 -0
  22. package/dist/templates/loader.js +129 -0
  23. package/dist/templates/template.schema.d.ts +401 -0
  24. package/dist/templates/template.schema.js +101 -0
  25. package/dist/templates/template.types.d.ts +155 -0
  26. package/dist/templates/template.types.js +7 -0
  27. package/dist/templates/yaml-parser.d.ts +15 -0
  28. package/dist/templates/yaml-parser.js +18 -0
  29. package/package.json +2 -1
  30. package/templates/bike/CLAUDE.md +7 -0
  31. package/templates/bike/easy.yaml +38 -0
  32. package/templates/bike/endurance.yaml +42 -0
  33. package/templates/bike/hills.yaml +80 -0
  34. package/templates/bike/overunders.yaml +81 -0
  35. package/templates/bike/rest.yaml +16 -0
  36. package/templates/bike/sweetspot.yaml +80 -0
  37. package/templates/bike/tempo.yaml +79 -0
  38. package/templates/bike/threshold.yaml +83 -0
  39. package/templates/bike/vo2max.yaml +84 -0
  40. package/templates/brick/CLAUDE.md +7 -0
  41. package/templates/brick/halfironman.yaml +72 -0
  42. package/templates/brick/ironman.yaml +72 -0
  43. package/templates/brick/olympic.yaml +70 -0
  44. package/templates/brick/sprint.yaml +70 -0
  45. package/templates/plan-viewer.html +22 -22
  46. package/templates/run/CLAUDE.md +7 -0
  47. package/templates/run/easy.yaml +36 -0
  48. package/templates/run/fartlek.yaml +40 -0
  49. package/templates/run/hills.yaml +36 -0
  50. package/templates/run/intervals.1k.yaml +63 -0
  51. package/templates/run/intervals.400.yaml +63 -0
  52. package/templates/run/intervals.800.yaml +63 -0
  53. package/templates/run/intervals.mile.yaml +64 -0
  54. package/templates/run/long.yaml +41 -0
  55. package/templates/run/progression.yaml +49 -0
  56. package/templates/run/race.5k.yaml +36 -0
  57. package/templates/run/recovery.yaml +36 -0
  58. package/templates/run/rest.yaml +16 -0
  59. package/templates/run/strides.yaml +49 -0
  60. package/templates/run/tempo.yaml +56 -0
  61. package/templates/run/threshold.yaml +56 -0
  62. package/templates/strength/CLAUDE.md +7 -0
  63. package/templates/strength/core.yaml +56 -0
  64. package/templates/strength/foundation.yaml +65 -0
  65. package/templates/strength/full.yaml +73 -0
  66. package/templates/strength/maintenance.yaml +62 -0
  67. package/templates/swim/CLAUDE.md +7 -0
  68. package/templates/swim/aerobic.yaml +67 -0
  69. package/templates/swim/easy.yaml +51 -0
  70. package/templates/swim/openwater.yaml +60 -0
  71. package/templates/swim/rest.yaml +16 -0
  72. package/templates/swim/technique.yaml +67 -0
  73. package/templates/swim/threshold.yaml +75 -0
  74. package/templates/swim/vo2max.yaml +88 -0
  75. /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
+ }