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.
@@ -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 workoutId = Number(requireFlag(flags, "workout-id"));
222
- const dateIso = String(requireFlag(flags, "date"));
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
- created = await findPlannedWorkoutOnDate(client, memberInfo, dateIso, workoutId);
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 sourcePlannedActivityId = String(requireFlag(flags, "id"));
282
- const targetDate = String(requireFlag(flags, "date"));
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