trainerroad-cli 0.1.1 → 0.3.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 +10 -0
- package/README.md +60 -1
- package/package.json +13 -1
- package/src/cli.mjs +121 -21
- package/src/commands/auth.mjs +3 -2
- package/src/commands/discovery.mjs +9 -8
- package/src/commands/train-now.mjs +142 -0
- package/src/commands/workout-library.mjs +430 -0
- package/src/commands/workout-mutations.mjs +310 -0
- package/src/commands/workout-recommend.mjs +175 -0
- package/src/commands/workout-tools.mjs +388 -0
- package/src/lib/command-manifest.mjs +465 -25
- package/src/trainerroad-client.mjs +295 -1
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
function stripHtml(value) {
|
|
2
|
+
return String(value ?? "")
|
|
3
|
+
.replace(/<[^>]+>/g, " ")
|
|
4
|
+
.replace(/ /gi, " ")
|
|
5
|
+
.replace(/&/gi, "&")
|
|
6
|
+
.replace(/"/gi, '"')
|
|
7
|
+
.replace(/'/gi, "'")
|
|
8
|
+
.replace(/\s+/g, " ")
|
|
9
|
+
.trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function summarizeWorkout(workout) {
|
|
13
|
+
if (!workout || typeof workout !== "object") return null;
|
|
14
|
+
return {
|
|
15
|
+
workoutId: workout.id ?? null,
|
|
16
|
+
workoutName: workout.workoutName ?? workout.name ?? null,
|
|
17
|
+
zoneId: workout.progressionId ?? workout.progression?.id ?? null,
|
|
18
|
+
zoneName: workout.progression?.text ?? null,
|
|
19
|
+
profileId: workout.profileId ?? null,
|
|
20
|
+
profileName: workout.profileName ?? null,
|
|
21
|
+
durationMinutes: workout.duration ?? null,
|
|
22
|
+
tss: workout.tss ?? null,
|
|
23
|
+
intensityFactor: workout.intensityFactor ?? null,
|
|
24
|
+
averageFtpPercent: workout.averageFtpPercent ?? null,
|
|
25
|
+
progressionLevel: workout.progressionLevel ?? null,
|
|
26
|
+
workoutDifficultyRating: workout.workoutDifficultyRating ?? null,
|
|
27
|
+
workoutTypeId: workout.workoutTypeId ?? null,
|
|
28
|
+
workoutLabelId: workout.workoutLabelId ?? null,
|
|
29
|
+
isOutside: workout.isOutside ?? null,
|
|
30
|
+
hasInstructions: workout.hasInstructions ?? null,
|
|
31
|
+
firstPublishDate: workout.firstPublishDate ?? null,
|
|
32
|
+
indoorAlternativeId: workout.indoorAlternativeId ?? null,
|
|
33
|
+
energyKj: workout.kj ?? null,
|
|
34
|
+
goal: stripHtml(workout.goalDescription),
|
|
35
|
+
description: stripHtml(workout.workoutDescription),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function summarizePlannedActivity(activity) {
|
|
40
|
+
if (!activity || typeof activity !== "object") return null;
|
|
41
|
+
const workout = activity.workout ?? null;
|
|
42
|
+
const date = activity.date
|
|
43
|
+
? `${String(activity.date.year).padStart(4, "0")}-${String(activity.date.month).padStart(2, "0")}-${String(activity.date.day).padStart(2, "0")}`
|
|
44
|
+
: null;
|
|
45
|
+
return {
|
|
46
|
+
plannedActivityId: activity.id ?? null,
|
|
47
|
+
date,
|
|
48
|
+
timeOfDay: activity.timeOfDay ?? null,
|
|
49
|
+
workoutId: workout?.id ?? null,
|
|
50
|
+
workoutName: workout?.name ?? activity.name ?? null,
|
|
51
|
+
isOutside: workout?.isOutside ?? null,
|
|
52
|
+
durationMinutes:
|
|
53
|
+
workout?.duration ??
|
|
54
|
+
(Number.isFinite(Number(activity.durationInSeconds))
|
|
55
|
+
? Math.round(Number(activity.durationInSeconds) / 60)
|
|
56
|
+
: null),
|
|
57
|
+
tss: activity.tss ?? workout?.tss ?? null,
|
|
58
|
+
recommendationReason: activity.recommendationReason ?? null,
|
|
59
|
+
modified: activity.modified ?? null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function summarizeLevels(levelsPayload) {
|
|
64
|
+
return {
|
|
65
|
+
workoutLevels: Array.isArray(levelsPayload?.workoutLevels) ? levelsPayload.workoutLevels : [],
|
|
66
|
+
athleteLevels:
|
|
67
|
+
levelsPayload && typeof levelsPayload.athleteLevels === "object" ? levelsPayload.athleteLevels : {},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function summarizeChart(chartData, pointLimit = 200) {
|
|
72
|
+
const rawPoints = Array.isArray(chartData?.courseData) ? chartData.courseData : [];
|
|
73
|
+
const pointsUseMilliseconds =
|
|
74
|
+
rawPoints.length > 1 && Number(rawPoints[1]?.seconds) >= 1000 && Number(rawPoints[1]?.seconds) % 1000 === 0;
|
|
75
|
+
const points = rawPoints.map((point) => ({
|
|
76
|
+
...point,
|
|
77
|
+
seconds:
|
|
78
|
+
pointsUseMilliseconds && Number.isFinite(Number(point?.seconds))
|
|
79
|
+
? Number(point.seconds) / 1000
|
|
80
|
+
: point?.seconds ?? null,
|
|
81
|
+
}));
|
|
82
|
+
let minPercent = null;
|
|
83
|
+
let maxPercent = null;
|
|
84
|
+
let durationSeconds = 0;
|
|
85
|
+
for (const point of points) {
|
|
86
|
+
const ftpPercent = Number(point?.ftpPercent);
|
|
87
|
+
if (Number.isFinite(ftpPercent)) {
|
|
88
|
+
minPercent = minPercent == null ? ftpPercent : Math.min(minPercent, ftpPercent);
|
|
89
|
+
maxPercent = maxPercent == null ? ftpPercent : Math.max(maxPercent, ftpPercent);
|
|
90
|
+
}
|
|
91
|
+
const seconds = Number(point?.seconds);
|
|
92
|
+
if (Number.isFinite(seconds)) durationSeconds = Math.max(durationSeconds, seconds);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
pointCount: points.length,
|
|
96
|
+
durationSeconds,
|
|
97
|
+
minFtpPercent: minPercent,
|
|
98
|
+
maxFtpPercent: maxPercent,
|
|
99
|
+
points: points.slice(0, pointLimit),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function requirePrivateMember(flags, deps) {
|
|
104
|
+
const { withClient } = deps;
|
|
105
|
+
const client = await withClient(flags);
|
|
106
|
+
try {
|
|
107
|
+
const memberInfo = await client.getMemberInfo();
|
|
108
|
+
return { client, memberInfo };
|
|
109
|
+
} catch {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"This command requires private authenticated mode. Login first with trainerroad-cli login.",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function sleep(ms) {
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function listPlannedWorkoutsOnDate(client, memberInfo, dateIso) {
|
|
121
|
+
const timeline = await client.getTimeline(memberInfo.memberId, memberInfo.username);
|
|
122
|
+
const candidateIds = (Array.isArray(timeline?.plannedActivities) ? timeline.plannedActivities : [])
|
|
123
|
+
.filter((item) => {
|
|
124
|
+
const date = item?.date;
|
|
125
|
+
const asIso = date
|
|
126
|
+
? `${String(date.year).padStart(4, "0")}-${String(date.month).padStart(2, "0")}-${String(date.day).padStart(2, "0")}`
|
|
127
|
+
: null;
|
|
128
|
+
return asIso === dateIso;
|
|
129
|
+
})
|
|
130
|
+
.map((item) => item.id)
|
|
131
|
+
.filter(Boolean);
|
|
132
|
+
|
|
133
|
+
if (candidateIds.length === 0) return [];
|
|
134
|
+
return client.getPlannedActivitiesByIds(memberInfo.memberId, memberInfo.username, candidateIds);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function commandWorkoutDetails(flags, deps) {
|
|
138
|
+
const { isJsonMode, requireFlag, requirePositiveInteger, toBoolean, writeOutput } = deps;
|
|
139
|
+
const workoutId = Number(requireFlag("workout-details", flags, "id"));
|
|
140
|
+
if (!Number.isFinite(workoutId)) {
|
|
141
|
+
throw new Error(`Invalid --id "${flags.id}". Expected a numeric workout ID.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const includeChart = toBoolean(flags["include-chart"], false);
|
|
145
|
+
const chartPointLimit = requirePositiveInteger(flags["chart-point-limit"], 200);
|
|
146
|
+
const { client, memberInfo } = await requirePrivateMember(flags, deps);
|
|
147
|
+
|
|
148
|
+
const [byIdRows, summaryPayload, levelsPayload, chartData] = await Promise.all([
|
|
149
|
+
client.getWorkoutsByIds([workoutId], memberInfo.username),
|
|
150
|
+
client.getWorkoutSummary(workoutId, memberInfo.username),
|
|
151
|
+
client.getWorkoutLevels(workoutId, memberInfo.username),
|
|
152
|
+
includeChart ? client.getWorkoutChartData(workoutId, memberInfo.username) : Promise.resolve(null),
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
const byIdWorkout = Array.isArray(byIdRows) && byIdRows.length > 0 ? byIdRows[0] : null;
|
|
156
|
+
const summaryWorkout = summaryPayload?.summary ?? null;
|
|
157
|
+
const workout = summarizeWorkout(summaryWorkout ?? byIdWorkout);
|
|
158
|
+
const payload = {
|
|
159
|
+
generatedAt: new Date().toISOString(),
|
|
160
|
+
command: "workout-details",
|
|
161
|
+
member: { memberId: memberInfo.memberId, username: memberInfo.username },
|
|
162
|
+
query: { workoutId, includeChart, chartPointLimit },
|
|
163
|
+
workout,
|
|
164
|
+
levels: summarizeLevels(levelsPayload),
|
|
165
|
+
chart: includeChart ? summarizeChart(chartData, chartPointLimit) : undefined,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (!isJsonMode(flags)) {
|
|
169
|
+
await writeOutput(payload, flags, (value) => {
|
|
170
|
+
const lines = [
|
|
171
|
+
`${value.workout?.workoutName ?? "Workout"} | workoutId=${value.query.workoutId} | zone=${value.workout?.zoneName ?? value.workout?.zoneId ?? "?"} | profile=${value.workout?.profileName ?? value.workout?.profileId ?? "?"}`,
|
|
172
|
+
`duration=${value.workout?.durationMinutes ?? "?"}m | tss=${value.workout?.tss ?? "?"} | if=${value.workout?.intensityFactor ?? "?"} | level=${value.workout?.progressionLevel ?? "?"} | outside=${value.workout?.isOutside}`,
|
|
173
|
+
];
|
|
174
|
+
if (value.chart) {
|
|
175
|
+
lines.push(
|
|
176
|
+
`chart: points=${value.chart.pointCount} duration=${value.chart.durationSeconds}s ftp%=${value.chart.minFtpPercent ?? "?"}-${value.chart.maxFtpPercent ?? "?"}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return lines.join("\n");
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await writeOutput(payload, { ...flags, json: !flags.jsonl });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function commandAddWorkout(flags, deps) {
|
|
188
|
+
const { isJsonMode, requireFlag, toBoolean, writeOutput } = deps;
|
|
189
|
+
const dryRun = toBoolean(flags["dry-run"], false);
|
|
190
|
+
const workoutId = Number(requireFlag("add-workout", flags, "workout-id"));
|
|
191
|
+
const dateIso = String(requireFlag("add-workout", flags, "date"));
|
|
192
|
+
if (!Number.isFinite(workoutId)) {
|
|
193
|
+
throw new Error(`Invalid --workout-id "${flags["workout-id"]}". Expected a numeric workout ID.`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const outside = toBoolean(flags.outside, false);
|
|
197
|
+
const { client, memberInfo } = await requirePrivateMember(flags, deps);
|
|
198
|
+
const workoutRows = await client.getWorkoutsByIds([workoutId], memberInfo.username);
|
|
199
|
+
const workout = summarizeWorkout(Array.isArray(workoutRows) ? workoutRows[0] : null);
|
|
200
|
+
if (!workout) {
|
|
201
|
+
throw new Error(`Workout ${workoutId} was not found in the library.`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const existingOnDate = await listPlannedWorkoutsOnDate(client, memberInfo, dateIso);
|
|
205
|
+
const beforeIds = new Set(existingOnDate.map((item) => item.id));
|
|
206
|
+
const matchingExisting = existingOnDate
|
|
207
|
+
.filter((item) => {
|
|
208
|
+
const plannedWorkoutId = Number(item?.workout?.id);
|
|
209
|
+
const plannedOutside = item?.workout?.isOutside;
|
|
210
|
+
return plannedWorkoutId === workoutId && (plannedOutside == null || plannedOutside === outside);
|
|
211
|
+
})
|
|
212
|
+
.map((item) => summarizePlannedActivity(item))
|
|
213
|
+
.filter(Boolean);
|
|
214
|
+
|
|
215
|
+
if (dryRun) {
|
|
216
|
+
const payload = {
|
|
217
|
+
generatedAt: new Date().toISOString(),
|
|
218
|
+
command: "add-workout",
|
|
219
|
+
member: { memberId: memberInfo.memberId, username: memberInfo.username },
|
|
220
|
+
query: { workoutId, date: dateIso, outside },
|
|
221
|
+
dryRun,
|
|
222
|
+
noop: false,
|
|
223
|
+
workout,
|
|
224
|
+
existingMatches: matchingExisting,
|
|
225
|
+
created: null,
|
|
226
|
+
attempts: [],
|
|
227
|
+
warnings:
|
|
228
|
+
matchingExisting.length > 0
|
|
229
|
+
? [`Found ${matchingExisting.length} matching workout(s) already scheduled on ${dateIso}.`]
|
|
230
|
+
: [],
|
|
231
|
+
message: `Would add workout ${workoutId} to ${dateIso}.`,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (!isJsonMode(flags)) {
|
|
235
|
+
await writeOutput(payload, flags, (value) => {
|
|
236
|
+
const lines = [
|
|
237
|
+
`Would add ${value.workout?.workoutName ?? "workout"} to ${value.query.date} | workoutId=${value.query.workoutId}`,
|
|
238
|
+
];
|
|
239
|
+
if (value.warnings.length > 0) {
|
|
240
|
+
lines.push(...value.warnings.map((warning) => `Warning: ${warning}`));
|
|
241
|
+
}
|
|
242
|
+
lines.push("No changes made.");
|
|
243
|
+
return lines.join("\n");
|
|
244
|
+
});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await writeOutput(payload, { ...flags, json: !flags.jsonl });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const attempts = await client.tryAddWorkoutToCalendar(workoutId, dateIso, {
|
|
253
|
+
outside,
|
|
254
|
+
usernameForReferer: memberInfo.username,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
let created = null;
|
|
258
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
259
|
+
const afterTarget = await listPlannedWorkoutsOnDate(client, memberInfo, dateIso);
|
|
260
|
+
created = afterTarget.find(
|
|
261
|
+
(item) => !beforeIds.has(item.id) && Number(item?.workout?.id) === Number(workoutId),
|
|
262
|
+
);
|
|
263
|
+
if (created) break;
|
|
264
|
+
await sleep(750);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!created) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`TrainerRoad did not expose a confirmed added workout for ${dateIso}. Attempts: ${JSON.stringify(attempts)}`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const payload = {
|
|
274
|
+
generatedAt: new Date().toISOString(),
|
|
275
|
+
command: "add-workout",
|
|
276
|
+
member: { memberId: memberInfo.memberId, username: memberInfo.username },
|
|
277
|
+
query: { workoutId, date: dateIso, outside },
|
|
278
|
+
workout,
|
|
279
|
+
created: summarizePlannedActivity(created),
|
|
280
|
+
attempts,
|
|
281
|
+
warnings: attempts.some((item) => !item.ok)
|
|
282
|
+
? [
|
|
283
|
+
"TrainerRoad returned one or more non-2xx responses during add-workout, but the workout was observed on the calendar after reconciliation.",
|
|
284
|
+
]
|
|
285
|
+
: [],
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
if (!isJsonMode(flags)) {
|
|
289
|
+
await writeOutput(payload, flags, (value) => {
|
|
290
|
+
const warning = value.warnings.length > 0 ? " | warning=server-status-mismatch" : "";
|
|
291
|
+
return `Added ${value.workout?.workoutName ?? "workout"} to ${value.query.date} | plannedActivityId=${value.created?.plannedActivityId}${warning}`;
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await writeOutput(payload, { ...flags, json: !flags.jsonl });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function commandCopyWorkout(flags, deps) {
|
|
300
|
+
const { isJsonMode, requireFlag, toBoolean, writeOutput } = deps;
|
|
301
|
+
const dryRun = toBoolean(flags["dry-run"], false);
|
|
302
|
+
const sourcePlannedActivityId = String(requireFlag("copy-workout", flags, "id"));
|
|
303
|
+
const targetDate = String(requireFlag("copy-workout", flags, "date"));
|
|
304
|
+
const { client, memberInfo } = await requirePrivateMember(flags, deps);
|
|
305
|
+
const source = await client.getPlannedActivity(sourcePlannedActivityId, memberInfo.username);
|
|
306
|
+
const beforeTarget = await listPlannedWorkoutsOnDate(client, memberInfo, targetDate);
|
|
307
|
+
const beforeIds = new Set(beforeTarget.map((item) => item.id));
|
|
308
|
+
const matchingExisting = beforeTarget
|
|
309
|
+
.filter((item) => Number(item?.workout?.id) === Number(source?.workout?.id))
|
|
310
|
+
.map((item) => summarizePlannedActivity(item))
|
|
311
|
+
.filter(Boolean);
|
|
312
|
+
|
|
313
|
+
if (dryRun) {
|
|
314
|
+
const payload = {
|
|
315
|
+
generatedAt: new Date().toISOString(),
|
|
316
|
+
command: "copy-workout",
|
|
317
|
+
member: { memberId: memberInfo.memberId, username: memberInfo.username },
|
|
318
|
+
query: { sourcePlannedActivityId, date: targetDate },
|
|
319
|
+
dryRun,
|
|
320
|
+
noop: false,
|
|
321
|
+
source: summarizePlannedActivity(source),
|
|
322
|
+
existingMatches: matchingExisting,
|
|
323
|
+
created: null,
|
|
324
|
+
mutation: null,
|
|
325
|
+
warnings:
|
|
326
|
+
matchingExisting.length > 0
|
|
327
|
+
? [`Found ${matchingExisting.length} matching workout(s) already scheduled on ${targetDate}.`]
|
|
328
|
+
: [],
|
|
329
|
+
message: `Would copy workout to ${targetDate}.`,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
if (!isJsonMode(flags)) {
|
|
333
|
+
await writeOutput(payload, flags, (value) => {
|
|
334
|
+
const lines = [
|
|
335
|
+
`Would copy ${value.source?.workoutName ?? "workout"} to ${value.query.date} | sourcePlannedActivityId=${value.query.sourcePlannedActivityId}`,
|
|
336
|
+
];
|
|
337
|
+
if (value.warnings.length > 0) {
|
|
338
|
+
lines.push(...value.warnings.map((warning) => `Warning: ${warning}`));
|
|
339
|
+
}
|
|
340
|
+
lines.push("No changes made.");
|
|
341
|
+
return lines.join("\n");
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
await writeOutput(payload, { ...flags, json: !flags.jsonl });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const mutation = await client.copyPlannedActivity(sourcePlannedActivityId, targetDate, memberInfo.username);
|
|
351
|
+
|
|
352
|
+
let created = null;
|
|
353
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
354
|
+
const afterTarget = await listPlannedWorkoutsOnDate(client, memberInfo, targetDate);
|
|
355
|
+
created = afterTarget.find(
|
|
356
|
+
(item) =>
|
|
357
|
+
!beforeIds.has(item.id) &&
|
|
358
|
+
Number(item?.workout?.id) === Number(source?.workout?.id),
|
|
359
|
+
);
|
|
360
|
+
if (created) break;
|
|
361
|
+
await sleep(500);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!created) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Copy completed but no new planned workout was found on ${targetDate} for source ${sourcePlannedActivityId}.`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const payload = {
|
|
371
|
+
generatedAt: new Date().toISOString(),
|
|
372
|
+
command: "copy-workout",
|
|
373
|
+
member: { memberId: memberInfo.memberId, username: memberInfo.username },
|
|
374
|
+
query: { sourcePlannedActivityId, date: targetDate },
|
|
375
|
+
source: summarizePlannedActivity(source),
|
|
376
|
+
created: summarizePlannedActivity(created),
|
|
377
|
+
mutation,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
if (!isJsonMode(flags)) {
|
|
381
|
+
await writeOutput(payload, flags, (value) => {
|
|
382
|
+
return `Copied ${value.source?.workoutName ?? "workout"} to ${value.query.date} | plannedActivityId=${value.created?.plannedActivityId}`;
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
await writeOutput(payload, { ...flags, json: !flags.jsonl });
|
|
388
|
+
}
|