trainerroad-cli 0.1.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/LICENSE +15 -0
- package/README.md +87 -0
- package/package.json +52 -0
- package/src/cli.mjs +677 -0
- package/src/commands/annotations.mjs +53 -0
- package/src/commands/auth.mjs +28 -0
- package/src/commands/discovery.mjs +165 -0
- package/src/commands/events.mjs +52 -0
- package/src/commands/ftp.mjs +210 -0
- package/src/commands/levels.mjs +58 -0
- package/src/commands/plan.mjs +81 -0
- package/src/commands/power.mjs +111 -0
- package/src/commands/timeline.mjs +74 -0
- package/src/commands/weight-history.mjs +51 -0
- package/src/commands/workouts.mjs +436 -0
- package/src/lib/agent-filters.mjs +287 -0
- package/src/lib/command-manifest.mjs +328 -0
- package/src/lib/planning-normalizers.mjs +193 -0
- package/src/trainerroad-client.mjs +465 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
function toIsoDate(value) {
|
|
2
|
+
if (typeof value === "string" && value.length >= 10) return value.slice(0, 10);
|
|
3
|
+
return new Date(value).toISOString().slice(0, 10);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function toIsoDateFromPlannedRecord(record) {
|
|
7
|
+
return `${String(record.date.year).padStart(4, "0")}-${String(record.date.month).padStart(2, "0")}-${String(record.date.day).padStart(2, "0")}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeDateOnlyInput(value, fallback) {
|
|
11
|
+
if (value == null || value === "") return fallback;
|
|
12
|
+
const normalized = String(value).trim();
|
|
13
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
|
|
14
|
+
throw new Error(`Invalid date "${value}". Expected YYYY-MM-DD.`);
|
|
15
|
+
}
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function requireNumber(value, fallback) {
|
|
20
|
+
if (value == null) return fallback;
|
|
21
|
+
const parsed = Number(value);
|
|
22
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requirePositiveInteger(value, fallback) {
|
|
27
|
+
const parsed = requireNumber(value, fallback);
|
|
28
|
+
if (!Number.isInteger(parsed) || parsed < 1) return fallback;
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function splitCsv(value) {
|
|
33
|
+
if (value == null || value === true) return [];
|
|
34
|
+
return String(value)
|
|
35
|
+
.split(",")
|
|
36
|
+
.map((part) => part.trim())
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toLowerOrNull(value) {
|
|
41
|
+
if (value == null) return null;
|
|
42
|
+
return String(value).toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getByPath(object, pathValue) {
|
|
46
|
+
if (!pathValue) return undefined;
|
|
47
|
+
const segments = String(pathValue)
|
|
48
|
+
.split(".")
|
|
49
|
+
.map((part) => part.trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
let cursor = object;
|
|
52
|
+
for (const segment of segments) {
|
|
53
|
+
if (cursor == null || typeof cursor !== "object") return undefined;
|
|
54
|
+
cursor = cursor[segment];
|
|
55
|
+
}
|
|
56
|
+
return cursor;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function projectRecordFields(record, fields) {
|
|
60
|
+
const output = {};
|
|
61
|
+
for (const field of fields) {
|
|
62
|
+
const value = getByPath(record, field);
|
|
63
|
+
output[field] = value === undefined ? null : value;
|
|
64
|
+
}
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveRecordDateOnly(record) {
|
|
69
|
+
if (!record || typeof record !== "object") return null;
|
|
70
|
+
if (typeof record.dateOnly === "string") return record.dateOnly;
|
|
71
|
+
if (record.date && typeof record.date === "object" && Number.isFinite(record.date.year)) {
|
|
72
|
+
return toIsoDateFromPlannedRecord(record);
|
|
73
|
+
}
|
|
74
|
+
if (typeof record.date === "string") return toIsoDate(record.date);
|
|
75
|
+
if (typeof record.started === "string") return toIsoDate(record.started);
|
|
76
|
+
if (typeof record.workoutDate === "string") return toIsoDate(record.workoutDate);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveRecordType(record) {
|
|
81
|
+
if (!record || typeof record !== "object") return null;
|
|
82
|
+
if (record.recordType != null) return record.recordType;
|
|
83
|
+
if (record.type != null) return record.type;
|
|
84
|
+
if (record.activityType != null) return record.activityType;
|
|
85
|
+
if (record.typeId != null) return record.typeId;
|
|
86
|
+
if (record.progressionId != null) return record.progressionId;
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveRecordTss(record) {
|
|
91
|
+
if (!record || typeof record !== "object") return null;
|
|
92
|
+
const candidates = [
|
|
93
|
+
record.tss,
|
|
94
|
+
record.actualTss,
|
|
95
|
+
record.plannedTssTotal,
|
|
96
|
+
record.estimatedTss,
|
|
97
|
+
record.Tss,
|
|
98
|
+
];
|
|
99
|
+
for (const candidate of candidates) {
|
|
100
|
+
const numeric = Number(candidate);
|
|
101
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveRecordText(record) {
|
|
107
|
+
if (!record || typeof record !== "object") return "";
|
|
108
|
+
const textParts = [
|
|
109
|
+
record.name,
|
|
110
|
+
record.title,
|
|
111
|
+
record.planName,
|
|
112
|
+
record.zoneLabel,
|
|
113
|
+
record.zoneKey,
|
|
114
|
+
record.typeLabel,
|
|
115
|
+
record.workoutRecordName,
|
|
116
|
+
record.recordType,
|
|
117
|
+
record.type,
|
|
118
|
+
]
|
|
119
|
+
.filter((value) => value != null)
|
|
120
|
+
.map((value) => String(value));
|
|
121
|
+
return textParts.join(" ").toLowerCase();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function compareNullableNumbers(a, b) {
|
|
125
|
+
const left = Number.isFinite(a) ? a : null;
|
|
126
|
+
const right = Number.isFinite(b) ? b : null;
|
|
127
|
+
if (left == null && right == null) return 0;
|
|
128
|
+
if (left == null) return 1;
|
|
129
|
+
if (right == null) return -1;
|
|
130
|
+
return left - right;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseAgentFilterConfig(flags) {
|
|
134
|
+
const fromDate = flags.from ? normalizeDateOnlyInput(flags.from, null) : null;
|
|
135
|
+
const toDate = flags.to ? normalizeDateOnlyInput(flags.to, null) : null;
|
|
136
|
+
const minTss =
|
|
137
|
+
flags["min-tss"] != null && flags["min-tss"] !== true ? Number(flags["min-tss"]) : null;
|
|
138
|
+
const maxTss =
|
|
139
|
+
flags["max-tss"] != null && flags["max-tss"] !== true ? Number(flags["max-tss"]) : null;
|
|
140
|
+
const sort = toLowerOrNull(flags.sort);
|
|
141
|
+
const contains = toLowerOrNull(flags.contains);
|
|
142
|
+
const typeFilters = splitCsv(flags.type).map((value) => value.toLowerCase());
|
|
143
|
+
const fields = splitCsv(flags.fields);
|
|
144
|
+
const resultLimit = requirePositiveInteger(flags["result-limit"], null);
|
|
145
|
+
return {
|
|
146
|
+
fromDate,
|
|
147
|
+
toDate,
|
|
148
|
+
minTss: Number.isFinite(minTss) ? minTss : null,
|
|
149
|
+
maxTss: Number.isFinite(maxTss) ? maxTss : null,
|
|
150
|
+
sort,
|
|
151
|
+
contains,
|
|
152
|
+
typeFilters,
|
|
153
|
+
fields,
|
|
154
|
+
resultLimit,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function applyAgentRecordFilters(records, flags) {
|
|
159
|
+
const config = parseAgentFilterConfig(flags);
|
|
160
|
+
const input = Array.isArray(records) ? records : [];
|
|
161
|
+
let output = [...input];
|
|
162
|
+
|
|
163
|
+
if (config.fromDate || config.toDate) {
|
|
164
|
+
output = output.filter((record) => {
|
|
165
|
+
const dateOnly = resolveRecordDateOnly(record);
|
|
166
|
+
if (!dateOnly) return false;
|
|
167
|
+
if (config.fromDate && dateOnly < config.fromDate) return false;
|
|
168
|
+
if (config.toDate && dateOnly > config.toDate) return false;
|
|
169
|
+
return true;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (config.typeFilters.length > 0) {
|
|
174
|
+
output = output.filter((record) => {
|
|
175
|
+
const recordTypeRaw = resolveRecordType(record);
|
|
176
|
+
if (recordTypeRaw == null) return false;
|
|
177
|
+
const recordType = String(recordTypeRaw).toLowerCase();
|
|
178
|
+
const numericType = Number(recordTypeRaw);
|
|
179
|
+
return config.typeFilters.some((candidate) => {
|
|
180
|
+
if (candidate === recordType) return true;
|
|
181
|
+
const numericCandidate = Number(candidate);
|
|
182
|
+
return (
|
|
183
|
+
Number.isFinite(numericCandidate) &&
|
|
184
|
+
Number.isFinite(numericType) &&
|
|
185
|
+
numericCandidate === numericType
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (config.contains) {
|
|
192
|
+
output = output.filter((record) => resolveRecordText(record).includes(config.contains));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (config.minTss != null || config.maxTss != null) {
|
|
196
|
+
output = output.filter((record) => {
|
|
197
|
+
const tss = resolveRecordTss(record);
|
|
198
|
+
if (!Number.isFinite(tss)) return false;
|
|
199
|
+
if (config.minTss != null && tss < config.minTss) return false;
|
|
200
|
+
if (config.maxTss != null && tss > config.maxTss) return false;
|
|
201
|
+
return true;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
switch (config.sort) {
|
|
206
|
+
case "date":
|
|
207
|
+
output.sort((a, b) => {
|
|
208
|
+
const left = resolveRecordDateOnly(a) ?? "";
|
|
209
|
+
const right = resolveRecordDateOnly(b) ?? "";
|
|
210
|
+
return left.localeCompare(right);
|
|
211
|
+
});
|
|
212
|
+
break;
|
|
213
|
+
case "date-desc":
|
|
214
|
+
output.sort((a, b) => {
|
|
215
|
+
const left = resolveRecordDateOnly(a) ?? "";
|
|
216
|
+
const right = resolveRecordDateOnly(b) ?? "";
|
|
217
|
+
return right.localeCompare(left);
|
|
218
|
+
});
|
|
219
|
+
break;
|
|
220
|
+
case "tss":
|
|
221
|
+
output.sort((a, b) => compareNullableNumbers(resolveRecordTss(a), resolveRecordTss(b)));
|
|
222
|
+
break;
|
|
223
|
+
case "tss-desc":
|
|
224
|
+
output.sort((a, b) => compareNullableNumbers(resolveRecordTss(b), resolveRecordTss(a)));
|
|
225
|
+
break;
|
|
226
|
+
case "name":
|
|
227
|
+
output.sort((a, b) => resolveRecordText(a).localeCompare(resolveRecordText(b)));
|
|
228
|
+
break;
|
|
229
|
+
case "name-desc":
|
|
230
|
+
output.sort((a, b) => resolveRecordText(b).localeCompare(resolveRecordText(a)));
|
|
231
|
+
break;
|
|
232
|
+
default:
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (config.resultLimit != null) {
|
|
237
|
+
output = output.slice(0, config.resultLimit);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (config.fields.length > 0) {
|
|
241
|
+
output = output.map((record) => projectRecordFields(record, config.fields));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const filterSummary = {
|
|
245
|
+
from: config.fromDate,
|
|
246
|
+
to: config.toDate,
|
|
247
|
+
type: config.typeFilters,
|
|
248
|
+
contains: config.contains,
|
|
249
|
+
minTss: config.minTss,
|
|
250
|
+
maxTss: config.maxTss,
|
|
251
|
+
sort: config.sort,
|
|
252
|
+
resultLimit: config.resultLimit,
|
|
253
|
+
fields: config.fields,
|
|
254
|
+
inputCount: input.length,
|
|
255
|
+
outputCount: output.length,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return { records: output, filterSummary };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function hasAgentRecordTransforms(flags) {
|
|
262
|
+
return Boolean(
|
|
263
|
+
flags.from ||
|
|
264
|
+
flags.to ||
|
|
265
|
+
flags.type ||
|
|
266
|
+
flags.contains ||
|
|
267
|
+
flags["min-tss"] ||
|
|
268
|
+
flags["max-tss"] ||
|
|
269
|
+
flags.sort ||
|
|
270
|
+
flags["result-limit"] ||
|
|
271
|
+
flags.fields,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function toRecordsOnlyPayload(payload) {
|
|
276
|
+
return {
|
|
277
|
+
mode: payload?.mode ?? null,
|
|
278
|
+
generatedAt: payload?.generatedAt ?? new Date().toISOString(),
|
|
279
|
+
command: payload?.command ?? null,
|
|
280
|
+
query: payload?.query ?? null,
|
|
281
|
+
filters: payload?.filters ?? null,
|
|
282
|
+
member: payload?.member ?? null,
|
|
283
|
+
count: Array.isArray(payload?.records) ? payload.records.length : payload?.count ?? 0,
|
|
284
|
+
records: Array.isArray(payload?.records) ? payload.records : [],
|
|
285
|
+
limitations: payload?.limitations ?? undefined,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
export const COMMANDS = {
|
|
2
|
+
help: {
|
|
3
|
+
summary: "Show global help or command help.",
|
|
4
|
+
usage: [
|
|
5
|
+
"node src/cli.mjs help",
|
|
6
|
+
"node src/cli.mjs help <command>",
|
|
7
|
+
"node src/cli.mjs <command> --help",
|
|
8
|
+
],
|
|
9
|
+
},
|
|
10
|
+
discover: {
|
|
11
|
+
summary: "Agent-oriented command discovery with progressive disclosure levels.",
|
|
12
|
+
usage: [
|
|
13
|
+
"node src/cli.mjs discover",
|
|
14
|
+
"node src/cli.mjs discover --level 1|2|3 [--json]",
|
|
15
|
+
"node src/cli.mjs discover --command future --level 3 --json",
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
capabilities: {
|
|
19
|
+
summary: "Show supported auth/data capabilities (private + public modes).",
|
|
20
|
+
usage: ["node src/cli.mjs capabilities [--json]"],
|
|
21
|
+
},
|
|
22
|
+
login: {
|
|
23
|
+
summary: "Authenticate and persist cookie session for private data access.",
|
|
24
|
+
usage: [
|
|
25
|
+
"node src/cli.mjs login --username <u> --password <p> [--return-path /app/career/<username>]",
|
|
26
|
+
"node src/cli.mjs login --username <u> --password-stdin",
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
whoami: {
|
|
30
|
+
summary: "Fetch authenticated member profile info (`/app/api/member-info`).",
|
|
31
|
+
usage: ["node src/cli.mjs whoami [--json]"],
|
|
32
|
+
},
|
|
33
|
+
timeline: {
|
|
34
|
+
summary: "Get profile summary (private full timeline or public TSS-derived summary).",
|
|
35
|
+
usage: [
|
|
36
|
+
"node src/cli.mjs timeline [--target <username>] [--public] [--full] [--json]",
|
|
37
|
+
"node src/cli.mjs timeline --target quinnsprouse --public --json",
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
events: {
|
|
41
|
+
summary: "Show calendar events/races from timeline (private mode).",
|
|
42
|
+
usage: [
|
|
43
|
+
"node src/cli.mjs events [--full] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--type <value>] [--contains <text>] [--min-tss <n>] [--max-tss <n>] [--sort date|date-desc|name|name-desc|tss|tss-desc] [--result-limit <n>] [--fields a,b] [--records-only] [--json|--jsonl]",
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
annotations: {
|
|
47
|
+
summary: "Show timeline annotations (time off, notes, illness/injury markers) (private mode).",
|
|
48
|
+
usage: [
|
|
49
|
+
"node src/cli.mjs annotations [--full] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--type <value>] [--contains <text>] [--sort date|date-desc|name|name-desc] [--result-limit <n>] [--fields a,b] [--records-only] [--json|--jsonl]",
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
levels: {
|
|
53
|
+
summary: "Show progression levels by zone (private mode).",
|
|
54
|
+
usage: [
|
|
55
|
+
"node src/cli.mjs levels [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--type <zone|progressionId>] [--contains <text>] [--sort name|name-desc|date|date-desc] [--result-limit <n>] [--fields a,b] [--records-only] [--json|--jsonl]",
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
plan: {
|
|
59
|
+
summary: "Show training plan data (current plan, phases, or all plans) (private mode).",
|
|
60
|
+
usage: [
|
|
61
|
+
"node src/cli.mjs plan [--view current|phases|plans] [--full] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--type <value>] [--contains <text>] [--sort date|date-desc|name|name-desc] [--result-limit <n>] [--fields a,b] [--records-only] [--json|--jsonl]",
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
"weight-history": {
|
|
65
|
+
summary: "Show historical body-weight entries (private mode).",
|
|
66
|
+
usage: [
|
|
67
|
+
"node src/cli.mjs weight-history [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--contains <text>] [--sort date|date-desc] [--result-limit <n>] [--fields a,b] [--records-only] [--json|--jsonl]",
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
today: {
|
|
71
|
+
summary: "Show today's planned/completed activity for private or public profile mode.",
|
|
72
|
+
usage: [
|
|
73
|
+
"node src/cli.mjs today [--date YYYY-MM-DD] [--target <username>] [--public] [--details] [--type <value>] [--contains <text>] [--min-tss <n>] [--max-tss <n>] [--sort date|date-desc|tss|tss-desc] [--result-limit <n>] [--fields a,b] [--records-only] [--json|--jsonl]",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
future: {
|
|
77
|
+
summary: "Show future plan data for private or public profile mode.",
|
|
78
|
+
usage: [
|
|
79
|
+
"node src/cli.mjs future [--days <n>] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--target <username>] [--public] [--details] [--type <value>] [--contains <text>] [--min-tss <n>] [--max-tss <n>] [--sort date|date-desc|tss|tss-desc] [--result-limit <n>] [--fields a,b] [--records-only] [--json|--jsonl]",
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
past: {
|
|
83
|
+
summary: "Show past activity data for private or public profile mode.",
|
|
84
|
+
usage: [
|
|
85
|
+
"node src/cli.mjs past [--days <n>] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--limit <n>] [--target <username>] [--public] [--details] [--type <value>] [--contains <text>] [--min-tss <n>] [--max-tss <n>] [--sort date|date-desc|tss|tss-desc] [--result-limit <n>] [--fields a,b] [--records-only] [--json|--jsonl]",
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
ftp: {
|
|
89
|
+
summary: "Show FTP snapshot and FTP history (private or public mode).",
|
|
90
|
+
usage: [
|
|
91
|
+
"node src/cli.mjs ftp [--target <username>] [--public] [--history-limit <n>] [--json|--jsonl]",
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
"ftp-prediction": {
|
|
95
|
+
summary: "Show AI FTP detection eligibility/status and progression impact (private mode).",
|
|
96
|
+
usage: ["node src/cli.mjs ftp-prediction [--json]"],
|
|
97
|
+
},
|
|
98
|
+
"power-ranking": {
|
|
99
|
+
summary: "Show best-power percentile ranking by duration (private mode).",
|
|
100
|
+
usage: ["node src/cli.mjs power-ranking [--json|--jsonl]"],
|
|
101
|
+
},
|
|
102
|
+
"power-records": {
|
|
103
|
+
summary: "Show date-range personal power records from TrainerRoad PR endpoint (private mode).",
|
|
104
|
+
usage: [
|
|
105
|
+
"node src/cli.mjs power-records [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD] [--row-type 100|101] [--indoor-only true|false] [--limit <n>] [--full] [--json|--jsonl]",
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
logout: {
|
|
109
|
+
summary: "Clear local persisted session.",
|
|
110
|
+
usage: ["node src/cli.mjs logout"],
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const PROJECT_NOTICE = "Unofficial tool. Not affiliated with or endorsed by TrainerRoad.";
|
|
115
|
+
|
|
116
|
+
export const GLOBAL_NOTES = [
|
|
117
|
+
PROJECT_NOTICE,
|
|
118
|
+
"Environment: TR_USERNAME, TR_PASSWORD, TR_SESSION_FILE",
|
|
119
|
+
"Output modes: default JSON, --json, --jsonl, --output <path>",
|
|
120
|
+
"Session file default: .trainerroad/session.json",
|
|
121
|
+
"Private mode: authenticated cookie session + full workout endpoints.",
|
|
122
|
+
"Public mode: username-based endpoint (`/app/api/tss/{username}`) with limited detail.",
|
|
123
|
+
"Agent filters: --from --to --type --contains --min-tss --max-tss --sort --result-limit --fields",
|
|
124
|
+
"Agent output: --records-only",
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
export const AGENT_FILTER_OPTIONS = [
|
|
128
|
+
{ flag: "--from", type: "YYYY-MM-DD", description: "Inclusive lower date bound." },
|
|
129
|
+
{ flag: "--to", type: "YYYY-MM-DD", description: "Inclusive upper date bound." },
|
|
130
|
+
{ flag: "--type", type: "csv", description: "Record type filter (e.g. 1,planned,completed)." },
|
|
131
|
+
{ flag: "--contains", type: "string", description: "Case-insensitive text match against title/name." },
|
|
132
|
+
{ flag: "--min-tss", type: "number", description: "Minimum TSS threshold." },
|
|
133
|
+
{ flag: "--max-tss", type: "number", description: "Maximum TSS threshold." },
|
|
134
|
+
{ flag: "--sort", type: "enum", description: "date|date-desc|tss|tss-desc|name|name-desc" },
|
|
135
|
+
{ flag: "--result-limit", type: "number", description: "Post-filter record cap." },
|
|
136
|
+
{ flag: "--fields", type: "csv", description: "Project records to selected field paths." },
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
export const AGENT_OUTPUT_OPTIONS = [
|
|
140
|
+
{
|
|
141
|
+
flag: "--records-only",
|
|
142
|
+
type: "boolean",
|
|
143
|
+
description: "Return only envelope + records (drops heavy side payload fields).",
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
export const FILTERABLE_COMMANDS = new Set([
|
|
148
|
+
"today",
|
|
149
|
+
"future",
|
|
150
|
+
"past",
|
|
151
|
+
"events",
|
|
152
|
+
"annotations",
|
|
153
|
+
"levels",
|
|
154
|
+
"plan",
|
|
155
|
+
"weight-history",
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
function trimFlagPrefix(flag) {
|
|
159
|
+
return String(flag ?? "").replace(/^--/, "").trim();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function mergeFlagGroups(...groups) {
|
|
163
|
+
return new Set(
|
|
164
|
+
groups
|
|
165
|
+
.flat()
|
|
166
|
+
.map((flag) => trimFlagPrefix(flag))
|
|
167
|
+
.filter(Boolean),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const SHARED_FLAGS = {
|
|
172
|
+
help: ["help"],
|
|
173
|
+
output: ["output"],
|
|
174
|
+
session: ["session-file"],
|
|
175
|
+
credentials: ["username", "password"],
|
|
176
|
+
publicProfile: ["target", "public"],
|
|
177
|
+
json: ["json"],
|
|
178
|
+
jsonAndJsonl: ["json", "jsonl"],
|
|
179
|
+
agentFilters: AGENT_FILTER_OPTIONS.map((option) => trimFlagPrefix(option.flag)),
|
|
180
|
+
agentOutput: AGENT_OUTPUT_OPTIONS.map((option) => trimFlagPrefix(option.flag)),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const COMMAND_FLAG_ALLOWLIST = {
|
|
184
|
+
help: mergeFlagGroups(SHARED_FLAGS.help, SHARED_FLAGS.json),
|
|
185
|
+
discover: mergeFlagGroups(
|
|
186
|
+
SHARED_FLAGS.help,
|
|
187
|
+
SHARED_FLAGS.output,
|
|
188
|
+
SHARED_FLAGS.json,
|
|
189
|
+
["level", "command"],
|
|
190
|
+
),
|
|
191
|
+
capabilities: mergeFlagGroups(SHARED_FLAGS.help, SHARED_FLAGS.output, SHARED_FLAGS.json),
|
|
192
|
+
login: mergeFlagGroups(
|
|
193
|
+
SHARED_FLAGS.help,
|
|
194
|
+
SHARED_FLAGS.output,
|
|
195
|
+
SHARED_FLAGS.session,
|
|
196
|
+
SHARED_FLAGS.credentials,
|
|
197
|
+
["password-stdin", "return-path"],
|
|
198
|
+
),
|
|
199
|
+
whoami: mergeFlagGroups(
|
|
200
|
+
SHARED_FLAGS.help,
|
|
201
|
+
SHARED_FLAGS.output,
|
|
202
|
+
SHARED_FLAGS.json,
|
|
203
|
+
SHARED_FLAGS.session,
|
|
204
|
+
SHARED_FLAGS.credentials,
|
|
205
|
+
),
|
|
206
|
+
timeline: mergeFlagGroups(
|
|
207
|
+
SHARED_FLAGS.help,
|
|
208
|
+
SHARED_FLAGS.output,
|
|
209
|
+
SHARED_FLAGS.json,
|
|
210
|
+
SHARED_FLAGS.session,
|
|
211
|
+
SHARED_FLAGS.credentials,
|
|
212
|
+
SHARED_FLAGS.publicProfile,
|
|
213
|
+
["full"],
|
|
214
|
+
),
|
|
215
|
+
events: mergeFlagGroups(
|
|
216
|
+
SHARED_FLAGS.help,
|
|
217
|
+
SHARED_FLAGS.output,
|
|
218
|
+
SHARED_FLAGS.jsonAndJsonl,
|
|
219
|
+
SHARED_FLAGS.session,
|
|
220
|
+
SHARED_FLAGS.credentials,
|
|
221
|
+
SHARED_FLAGS.agentFilters,
|
|
222
|
+
SHARED_FLAGS.agentOutput,
|
|
223
|
+
["full"],
|
|
224
|
+
),
|
|
225
|
+
annotations: mergeFlagGroups(
|
|
226
|
+
SHARED_FLAGS.help,
|
|
227
|
+
SHARED_FLAGS.output,
|
|
228
|
+
SHARED_FLAGS.jsonAndJsonl,
|
|
229
|
+
SHARED_FLAGS.session,
|
|
230
|
+
SHARED_FLAGS.credentials,
|
|
231
|
+
SHARED_FLAGS.agentFilters,
|
|
232
|
+
SHARED_FLAGS.agentOutput,
|
|
233
|
+
["full"],
|
|
234
|
+
),
|
|
235
|
+
levels: mergeFlagGroups(
|
|
236
|
+
SHARED_FLAGS.help,
|
|
237
|
+
SHARED_FLAGS.output,
|
|
238
|
+
SHARED_FLAGS.jsonAndJsonl,
|
|
239
|
+
SHARED_FLAGS.session,
|
|
240
|
+
SHARED_FLAGS.credentials,
|
|
241
|
+
SHARED_FLAGS.agentFilters,
|
|
242
|
+
SHARED_FLAGS.agentOutput,
|
|
243
|
+
),
|
|
244
|
+
plan: mergeFlagGroups(
|
|
245
|
+
SHARED_FLAGS.help,
|
|
246
|
+
SHARED_FLAGS.output,
|
|
247
|
+
SHARED_FLAGS.jsonAndJsonl,
|
|
248
|
+
SHARED_FLAGS.session,
|
|
249
|
+
SHARED_FLAGS.credentials,
|
|
250
|
+
SHARED_FLAGS.agentFilters,
|
|
251
|
+
SHARED_FLAGS.agentOutput,
|
|
252
|
+
["view", "full"],
|
|
253
|
+
),
|
|
254
|
+
"weight-history": mergeFlagGroups(
|
|
255
|
+
SHARED_FLAGS.help,
|
|
256
|
+
SHARED_FLAGS.output,
|
|
257
|
+
SHARED_FLAGS.jsonAndJsonl,
|
|
258
|
+
SHARED_FLAGS.session,
|
|
259
|
+
SHARED_FLAGS.credentials,
|
|
260
|
+
SHARED_FLAGS.agentFilters,
|
|
261
|
+
SHARED_FLAGS.agentOutput,
|
|
262
|
+
),
|
|
263
|
+
today: mergeFlagGroups(
|
|
264
|
+
SHARED_FLAGS.help,
|
|
265
|
+
SHARED_FLAGS.output,
|
|
266
|
+
SHARED_FLAGS.jsonAndJsonl,
|
|
267
|
+
SHARED_FLAGS.session,
|
|
268
|
+
SHARED_FLAGS.credentials,
|
|
269
|
+
SHARED_FLAGS.publicProfile,
|
|
270
|
+
SHARED_FLAGS.agentFilters,
|
|
271
|
+
SHARED_FLAGS.agentOutput,
|
|
272
|
+
["date", "details"],
|
|
273
|
+
),
|
|
274
|
+
future: mergeFlagGroups(
|
|
275
|
+
SHARED_FLAGS.help,
|
|
276
|
+
SHARED_FLAGS.output,
|
|
277
|
+
SHARED_FLAGS.jsonAndJsonl,
|
|
278
|
+
SHARED_FLAGS.session,
|
|
279
|
+
SHARED_FLAGS.credentials,
|
|
280
|
+
SHARED_FLAGS.publicProfile,
|
|
281
|
+
SHARED_FLAGS.agentFilters,
|
|
282
|
+
SHARED_FLAGS.agentOutput,
|
|
283
|
+
["days", "from", "to", "details"],
|
|
284
|
+
),
|
|
285
|
+
past: mergeFlagGroups(
|
|
286
|
+
SHARED_FLAGS.help,
|
|
287
|
+
SHARED_FLAGS.output,
|
|
288
|
+
SHARED_FLAGS.jsonAndJsonl,
|
|
289
|
+
SHARED_FLAGS.session,
|
|
290
|
+
SHARED_FLAGS.credentials,
|
|
291
|
+
SHARED_FLAGS.publicProfile,
|
|
292
|
+
SHARED_FLAGS.agentFilters,
|
|
293
|
+
SHARED_FLAGS.agentOutput,
|
|
294
|
+
["days", "limit", "from", "to", "details"],
|
|
295
|
+
),
|
|
296
|
+
ftp: mergeFlagGroups(
|
|
297
|
+
SHARED_FLAGS.help,
|
|
298
|
+
SHARED_FLAGS.output,
|
|
299
|
+
SHARED_FLAGS.jsonAndJsonl,
|
|
300
|
+
SHARED_FLAGS.session,
|
|
301
|
+
SHARED_FLAGS.credentials,
|
|
302
|
+
SHARED_FLAGS.publicProfile,
|
|
303
|
+
["history-limit"],
|
|
304
|
+
),
|
|
305
|
+
"ftp-prediction": mergeFlagGroups(
|
|
306
|
+
SHARED_FLAGS.help,
|
|
307
|
+
SHARED_FLAGS.output,
|
|
308
|
+
SHARED_FLAGS.json,
|
|
309
|
+
SHARED_FLAGS.session,
|
|
310
|
+
SHARED_FLAGS.credentials,
|
|
311
|
+
),
|
|
312
|
+
"power-ranking": mergeFlagGroups(
|
|
313
|
+
SHARED_FLAGS.help,
|
|
314
|
+
SHARED_FLAGS.output,
|
|
315
|
+
SHARED_FLAGS.jsonAndJsonl,
|
|
316
|
+
SHARED_FLAGS.session,
|
|
317
|
+
SHARED_FLAGS.credentials,
|
|
318
|
+
),
|
|
319
|
+
"power-records": mergeFlagGroups(
|
|
320
|
+
SHARED_FLAGS.help,
|
|
321
|
+
SHARED_FLAGS.output,
|
|
322
|
+
SHARED_FLAGS.jsonAndJsonl,
|
|
323
|
+
SHARED_FLAGS.session,
|
|
324
|
+
SHARED_FLAGS.credentials,
|
|
325
|
+
["start-date", "end-date", "row-type", "indoor-only", "slot", "limit", "full"],
|
|
326
|
+
),
|
|
327
|
+
logout: mergeFlagGroups(SHARED_FLAGS.help, SHARED_FLAGS.output, SHARED_FLAGS.session),
|
|
328
|
+
};
|