trainerroad-cli 0.2.0 → 0.4.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/CHANGELOG.md +28 -0
- package/README.md +65 -1
- package/package.json +11 -2
- package/src/cli.mjs +120 -37
- package/src/commands/annotation-mutations.mjs +253 -0
- package/src/commands/auth.mjs +3 -2
- package/src/commands/discovery.mjs +9 -8
- package/src/commands/event-mutations.mjs +174 -0
- package/src/commands/plan.mjs +23 -5
- package/src/commands/power.mjs +2 -1
- package/src/commands/workout-image.mjs +99 -0
- package/src/commands/workout-library.mjs +1 -0
- package/src/commands/workout-mutations.mjs +186 -46
- package/src/commands/workout-tools.mjs +104 -41
- package/src/lib/command-manifest.mjs +458 -35
- package/src/lib/planning-normalizers.mjs +110 -4
- package/src/trainerroad-client.mjs +246 -36
|
@@ -31,6 +31,7 @@ function summarizeWorkout(workout) {
|
|
|
31
31
|
firstPublishDate: workout.firstPublishDate ?? null,
|
|
32
32
|
indoorAlternativeId: workout.indoorAlternativeId ?? null,
|
|
33
33
|
energyKj: workout.kj ?? null,
|
|
34
|
+
chartUrl: workout.picUrl ?? null,
|
|
34
35
|
goal: stripHtml(workout.goalDescription),
|
|
35
36
|
description: stripHtml(workout.workoutDescription),
|
|
36
37
|
};
|
|
@@ -100,14 +101,6 @@ function summarizeChart(chartData, pointLimit = 200) {
|
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
function requireFlag(flags, name) {
|
|
104
|
-
const value = flags[name];
|
|
105
|
-
if (value === undefined || value === null || value === "") {
|
|
106
|
-
throw new Error(`Missing required flag --${name}.`);
|
|
107
|
-
}
|
|
108
|
-
return value;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
104
|
async function requirePrivateMember(flags, deps) {
|
|
112
105
|
const { withClient } = deps;
|
|
113
106
|
const client = await withClient(flags);
|
|
@@ -125,30 +118,6 @@ async function sleep(ms) {
|
|
|
125
118
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
126
119
|
}
|
|
127
120
|
|
|
128
|
-
async function findPlannedWorkoutOnDate(client, memberInfo, dateIso, workoutId) {
|
|
129
|
-
const timeline = await client.getTimeline(memberInfo.memberId, memberInfo.username);
|
|
130
|
-
const candidateIds = (Array.isArray(timeline?.plannedActivities) ? timeline.plannedActivities : [])
|
|
131
|
-
.filter((item) => {
|
|
132
|
-
const date = item?.date;
|
|
133
|
-
const asIso = date
|
|
134
|
-
? `${String(date.year).padStart(4, "0")}-${String(date.month).padStart(2, "0")}-${String(date.day).padStart(2, "0")}`
|
|
135
|
-
: null;
|
|
136
|
-
return asIso === dateIso;
|
|
137
|
-
})
|
|
138
|
-
.map((item) => item.id)
|
|
139
|
-
.filter(Boolean);
|
|
140
|
-
|
|
141
|
-
if (candidateIds.length === 0) return null;
|
|
142
|
-
const details = await client.getPlannedActivitiesByIds(
|
|
143
|
-
memberInfo.memberId,
|
|
144
|
-
memberInfo.username,
|
|
145
|
-
candidateIds,
|
|
146
|
-
);
|
|
147
|
-
const matches = details.filter((item) => Number(item?.workout?.id) === Number(workoutId));
|
|
148
|
-
if (matches.length === 0) return null;
|
|
149
|
-
return matches.sort((a, b) => String(b.modified ?? "").localeCompare(String(a.modified ?? "")))[0];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
121
|
async function listPlannedWorkoutsOnDate(client, memberInfo, dateIso) {
|
|
153
122
|
const timeline = await client.getTimeline(memberInfo.memberId, memberInfo.username);
|
|
154
123
|
const candidateIds = (Array.isArray(timeline?.plannedActivities) ? timeline.plannedActivities : [])
|
|
@@ -167,8 +136,8 @@ async function listPlannedWorkoutsOnDate(client, memberInfo, dateIso) {
|
|
|
167
136
|
}
|
|
168
137
|
|
|
169
138
|
export async function commandWorkoutDetails(flags, deps) {
|
|
170
|
-
const { isJsonMode, requirePositiveInteger, toBoolean, writeOutput } = deps;
|
|
171
|
-
const workoutId = Number(requireFlag(flags, "id"));
|
|
139
|
+
const { isJsonMode, requireFlag, requirePositiveInteger, toBoolean, writeOutput } = deps;
|
|
140
|
+
const workoutId = Number(requireFlag("workout-details", flags, "id"));
|
|
172
141
|
if (!Number.isFinite(workoutId)) {
|
|
173
142
|
throw new Error(`Invalid --id "${flags.id}". Expected a numeric workout ID.`);
|
|
174
143
|
}
|
|
@@ -217,9 +186,10 @@ export async function commandWorkoutDetails(flags, deps) {
|
|
|
217
186
|
}
|
|
218
187
|
|
|
219
188
|
export async function commandAddWorkout(flags, deps) {
|
|
220
|
-
const { isJsonMode, toBoolean, writeOutput } = deps;
|
|
221
|
-
const
|
|
222
|
-
const
|
|
189
|
+
const { isJsonMode, requireFlag, toBoolean, writeOutput } = deps;
|
|
190
|
+
const dryRun = toBoolean(flags["dry-run"], false);
|
|
191
|
+
const workoutId = Number(requireFlag("add-workout", flags, "workout-id"));
|
|
192
|
+
const dateIso = String(requireFlag("add-workout", flags, "date"));
|
|
223
193
|
if (!Number.isFinite(workoutId)) {
|
|
224
194
|
throw new Error(`Invalid --workout-id "${flags["workout-id"]}". Expected a numeric workout ID.`);
|
|
225
195
|
}
|
|
@@ -232,6 +202,54 @@ export async function commandAddWorkout(flags, deps) {
|
|
|
232
202
|
throw new Error(`Workout ${workoutId} was not found in the library.`);
|
|
233
203
|
}
|
|
234
204
|
|
|
205
|
+
const existingOnDate = await listPlannedWorkoutsOnDate(client, memberInfo, dateIso);
|
|
206
|
+
const beforeIds = new Set(existingOnDate.map((item) => item.id));
|
|
207
|
+
const matchingExisting = existingOnDate
|
|
208
|
+
.filter((item) => {
|
|
209
|
+
const plannedWorkoutId = Number(item?.workout?.id);
|
|
210
|
+
const plannedOutside = item?.workout?.isOutside;
|
|
211
|
+
return plannedWorkoutId === workoutId && (plannedOutside == null || plannedOutside === outside);
|
|
212
|
+
})
|
|
213
|
+
.map((item) => summarizePlannedActivity(item))
|
|
214
|
+
.filter(Boolean);
|
|
215
|
+
|
|
216
|
+
if (dryRun) {
|
|
217
|
+
const payload = {
|
|
218
|
+
generatedAt: new Date().toISOString(),
|
|
219
|
+
command: "add-workout",
|
|
220
|
+
member: { memberId: memberInfo.memberId, username: memberInfo.username },
|
|
221
|
+
query: { workoutId, date: dateIso, outside },
|
|
222
|
+
dryRun,
|
|
223
|
+
noop: false,
|
|
224
|
+
workout,
|
|
225
|
+
existingMatches: matchingExisting,
|
|
226
|
+
created: null,
|
|
227
|
+
attempts: [],
|
|
228
|
+
warnings:
|
|
229
|
+
matchingExisting.length > 0
|
|
230
|
+
? [`Found ${matchingExisting.length} matching workout(s) already scheduled on ${dateIso}.`]
|
|
231
|
+
: [],
|
|
232
|
+
message: `Would add workout ${workoutId} to ${dateIso}.`,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
if (!isJsonMode(flags)) {
|
|
236
|
+
await writeOutput(payload, flags, (value) => {
|
|
237
|
+
const lines = [
|
|
238
|
+
`Would add ${value.workout?.workoutName ?? "workout"} to ${value.query.date} | workoutId=${value.query.workoutId}`,
|
|
239
|
+
];
|
|
240
|
+
if (value.warnings.length > 0) {
|
|
241
|
+
lines.push(...value.warnings.map((warning) => `Warning: ${warning}`));
|
|
242
|
+
}
|
|
243
|
+
lines.push("No changes made.");
|
|
244
|
+
return lines.join("\n");
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await writeOutput(payload, { ...flags, json: !flags.jsonl });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
235
253
|
const attempts = await client.tryAddWorkoutToCalendar(workoutId, dateIso, {
|
|
236
254
|
outside,
|
|
237
255
|
usernameForReferer: memberInfo.username,
|
|
@@ -239,7 +257,10 @@ export async function commandAddWorkout(flags, deps) {
|
|
|
239
257
|
|
|
240
258
|
let created = null;
|
|
241
259
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
242
|
-
|
|
260
|
+
const afterTarget = await listPlannedWorkoutsOnDate(client, memberInfo, dateIso);
|
|
261
|
+
created = afterTarget.find(
|
|
262
|
+
(item) => !beforeIds.has(item.id) && Number(item?.workout?.id) === Number(workoutId),
|
|
263
|
+
);
|
|
243
264
|
if (created) break;
|
|
244
265
|
await sleep(750);
|
|
245
266
|
}
|
|
@@ -277,13 +298,55 @@ export async function commandAddWorkout(flags, deps) {
|
|
|
277
298
|
}
|
|
278
299
|
|
|
279
300
|
export async function commandCopyWorkout(flags, deps) {
|
|
280
|
-
const { isJsonMode, writeOutput } = deps;
|
|
281
|
-
const
|
|
282
|
-
const
|
|
301
|
+
const { isJsonMode, requireFlag, toBoolean, writeOutput } = deps;
|
|
302
|
+
const dryRun = toBoolean(flags["dry-run"], false);
|
|
303
|
+
const sourcePlannedActivityId = String(requireFlag("copy-workout", flags, "id"));
|
|
304
|
+
const targetDate = String(requireFlag("copy-workout", flags, "date"));
|
|
283
305
|
const { client, memberInfo } = await requirePrivateMember(flags, deps);
|
|
284
306
|
const source = await client.getPlannedActivity(sourcePlannedActivityId, memberInfo.username);
|
|
285
307
|
const beforeTarget = await listPlannedWorkoutsOnDate(client, memberInfo, targetDate);
|
|
286
308
|
const beforeIds = new Set(beforeTarget.map((item) => item.id));
|
|
309
|
+
const matchingExisting = beforeTarget
|
|
310
|
+
.filter((item) => Number(item?.workout?.id) === Number(source?.workout?.id))
|
|
311
|
+
.map((item) => summarizePlannedActivity(item))
|
|
312
|
+
.filter(Boolean);
|
|
313
|
+
|
|
314
|
+
if (dryRun) {
|
|
315
|
+
const payload = {
|
|
316
|
+
generatedAt: new Date().toISOString(),
|
|
317
|
+
command: "copy-workout",
|
|
318
|
+
member: { memberId: memberInfo.memberId, username: memberInfo.username },
|
|
319
|
+
query: { sourcePlannedActivityId, date: targetDate },
|
|
320
|
+
dryRun,
|
|
321
|
+
noop: false,
|
|
322
|
+
source: summarizePlannedActivity(source),
|
|
323
|
+
existingMatches: matchingExisting,
|
|
324
|
+
created: null,
|
|
325
|
+
mutation: null,
|
|
326
|
+
warnings:
|
|
327
|
+
matchingExisting.length > 0
|
|
328
|
+
? [`Found ${matchingExisting.length} matching workout(s) already scheduled on ${targetDate}.`]
|
|
329
|
+
: [],
|
|
330
|
+
message: `Would copy workout to ${targetDate}.`,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
if (!isJsonMode(flags)) {
|
|
334
|
+
await writeOutput(payload, flags, (value) => {
|
|
335
|
+
const lines = [
|
|
336
|
+
`Would copy ${value.source?.workoutName ?? "workout"} to ${value.query.date} | sourcePlannedActivityId=${value.query.sourcePlannedActivityId}`,
|
|
337
|
+
];
|
|
338
|
+
if (value.warnings.length > 0) {
|
|
339
|
+
lines.push(...value.warnings.map((warning) => `Warning: ${warning}`));
|
|
340
|
+
}
|
|
341
|
+
lines.push("No changes made.");
|
|
342
|
+
return lines.join("\n");
|
|
343
|
+
});
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
await writeOutput(payload, { ...flags, json: !flags.jsonl });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
287
350
|
|
|
288
351
|
const mutation = await client.copyPlannedActivity(sourcePlannedActivityId, targetDate, memberInfo.username);
|
|
289
352
|
|