incremnt 0.1.0 → 0.1.1
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/package.json +4 -1
- package/src/format.js +289 -29
- package/src/lib.js +8 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "incremnt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Command-line tool for querying your incremnt strength training data",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,5 +13,8 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"test": "node --test",
|
|
15
15
|
"dev:sync-fixture": "node ./scripts/dev-sync-fixture-server.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"chalk": "^5.6.2"
|
|
16
19
|
}
|
|
17
20
|
}
|
package/src/format.js
CHANGED
|
@@ -1,44 +1,72 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
1
3
|
const shortMonths = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
2
4
|
const shortDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
// --- Shared helpers ---
|
|
7
|
+
|
|
8
|
+
function parseDate(dateString) {
|
|
5
9
|
const date = new Date(dateString);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
10
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
11
|
+
}
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
function formatShortDate(dateString) {
|
|
14
|
+
const date = parseDate(dateString);
|
|
15
|
+
return date ? `${date.getDate()} ${shortMonths[date.getMonth()]}` : dateString;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
function formatDayAndDate(dateString) {
|
|
14
|
-
const date =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
const date = parseDate(dateString);
|
|
20
|
+
return date
|
|
21
|
+
? `${shortDays[date.getDay()]} ${date.getDate()} ${shortMonths[date.getMonth()]}`
|
|
22
|
+
: dateString;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function header(text) {
|
|
26
|
+
return chalk.bold(` ${text}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function dimDot() {
|
|
30
|
+
return chalk.dim(' \u00b7 ');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatWeight(weight) {
|
|
34
|
+
return chalk.bold.cyan(`${Number(weight).toFixed(1)} kg`);
|
|
35
|
+
}
|
|
18
36
|
|
|
19
|
-
|
|
37
|
+
function formatDuration(seconds) {
|
|
38
|
+
return `${Math.round(seconds / 60)} min`;
|
|
20
39
|
}
|
|
21
40
|
|
|
41
|
+
function keyValue(key, value, keyWidth = 12) {
|
|
42
|
+
return ` ${chalk.dim(key.padEnd(keyWidth))} ${chalk.bold(value)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Formatters ---
|
|
46
|
+
|
|
22
47
|
function formatRecords(payload) {
|
|
23
48
|
if (!Array.isArray(payload) || payload.length === 0) {
|
|
24
49
|
return 'No records found.';
|
|
25
50
|
}
|
|
26
51
|
|
|
27
52
|
const maxNameLength = Math.max(...payload.map((record) => record.exerciseName.length));
|
|
53
|
+
const lines = [header('PERSONAL RECORDS'), ''];
|
|
28
54
|
|
|
29
|
-
|
|
55
|
+
for (const record of payload) {
|
|
30
56
|
const name = record.exerciseName.padEnd(maxNameLength);
|
|
31
57
|
const date = formatShortDate(record.sessionDate);
|
|
32
58
|
const isBodyweight = Number(record.weight) === 0;
|
|
33
59
|
|
|
34
60
|
if (isBodyweight) {
|
|
35
61
|
const reps = `${record.reps} reps`.padStart(12);
|
|
36
|
-
|
|
62
|
+
lines.push(` ${name} ${chalk.bold.cyan(reps)} ${chalk.dim('BW')}${dimDot()}${chalk.dim(date)}`);
|
|
63
|
+
} else {
|
|
64
|
+
const weight = `${Number(record.weight).toFixed(1)} kg`.padStart(12);
|
|
65
|
+
lines.push(` ${name} ${chalk.bold.cyan(weight)} ${chalk.dim('e1RM')}${dimDot()}${chalk.dim(date)}`);
|
|
37
66
|
}
|
|
67
|
+
}
|
|
38
68
|
|
|
39
|
-
|
|
40
|
-
return `${name} ${weight} e1RM \u00b7 ${date}`;
|
|
41
|
-
}).join('\n');
|
|
69
|
+
return lines.join('\n');
|
|
42
70
|
}
|
|
43
71
|
|
|
44
72
|
function formatSessionInsights(payload) {
|
|
@@ -46,27 +74,259 @@ function formatSessionInsights(payload) {
|
|
|
46
74
|
return 'No sessions found.';
|
|
47
75
|
}
|
|
48
76
|
|
|
49
|
-
|
|
50
|
-
|
|
77
|
+
const lines = [header('RECENT SESSIONS'), ''];
|
|
78
|
+
const maxDateLength = Math.max(...payload.map((session) => formatDayAndDate(session.sessionDate).length));
|
|
79
|
+
|
|
80
|
+
for (const session of payload) {
|
|
81
|
+
const date = formatDayAndDate(session.sessionDate).padEnd(maxDateLength);
|
|
51
82
|
const dayName = session.dayName ?? 'Workout';
|
|
52
83
|
const exercises = `${session.exerciseCount ?? '?'} exercises`;
|
|
53
|
-
const duration = session.durationSeconds
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
84
|
+
const duration = session.durationSeconds ? formatDuration(session.durationSeconds) : '';
|
|
85
|
+
const suffix = duration
|
|
86
|
+
? `${dimDot()}${chalk.dim(exercises)}${dimDot()}${chalk.dim(duration)}`
|
|
87
|
+
: `${dimDot()}${chalk.dim(exercises)}`;
|
|
88
|
+
|
|
89
|
+
lines.push(` ${chalk.bold(date)} ${dayName}${suffix}`);
|
|
90
|
+
}
|
|
57
91
|
|
|
58
|
-
|
|
59
|
-
}).join('\n');
|
|
92
|
+
return lines.join('\n');
|
|
60
93
|
}
|
|
61
94
|
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
return
|
|
95
|
+
function formatSessionShow(payload) {
|
|
96
|
+
if (!payload) {
|
|
97
|
+
return 'Session not found.';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const date = formatDayAndDate(payload.sessionDate);
|
|
101
|
+
const lines = [` ${chalk.bold('SESSION')}${dimDot()}${date}`, ''];
|
|
102
|
+
|
|
103
|
+
if (payload.programName) {
|
|
104
|
+
lines.push(keyValue('Program', payload.programName));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (payload.dayName) {
|
|
108
|
+
const dayDetail = payload.programDayIndex != null
|
|
109
|
+
? `${payload.dayName} (Day ${payload.programDayIndex + 1})`
|
|
110
|
+
: payload.dayName;
|
|
111
|
+
lines.push(keyValue('Day', dayDetail));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (payload.historicalContext?.programWeekNumber) {
|
|
115
|
+
lines.push(keyValue('Week', String(payload.historicalContext.programWeekNumber)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (payload.totalVolume) {
|
|
119
|
+
lines.push(keyValue('Volume', `${Number(payload.totalVolume).toLocaleString()} kg`));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (payload.durationSeconds) {
|
|
123
|
+
lines.push(keyValue('Duration', formatDuration(payload.durationSeconds)));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (payload.effortScore != null) {
|
|
127
|
+
lines.push(keyValue('Effort', String(payload.effortScore)));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (payload.averageHeartRate != null || payload.maxHeartRate != null) {
|
|
131
|
+
const parts = [];
|
|
132
|
+
if (payload.averageHeartRate != null) {
|
|
133
|
+
parts.push(`${payload.averageHeartRate} avg`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (payload.maxHeartRate != null) {
|
|
137
|
+
parts.push(`${payload.maxHeartRate} max`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
lines.push(keyValue('HR', parts.join(chalk.dim(' \u00b7 '))));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (payload.activeCalories != null) {
|
|
144
|
+
lines.push(keyValue('Calories', `${payload.activeCalories} kcal`));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatExerciseHistory(payload) {
|
|
151
|
+
if (!Array.isArray(payload) || payload.length === 0) {
|
|
152
|
+
return 'No history found.';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const exerciseName = payload[0]?.exerciseName ?? 'Exercise';
|
|
156
|
+
const lines = [` ${chalk.bold(exerciseName.toUpperCase())}${dimDot()}History`, ''];
|
|
157
|
+
const maxDateLength = Math.max(...payload.map((entry) => formatShortDate(entry.sessionDate).length));
|
|
158
|
+
|
|
159
|
+
for (const entry of payload) {
|
|
160
|
+
const date = formatShortDate(entry.sessionDate).padEnd(maxDateLength);
|
|
161
|
+
const weight = entry.weight > 0 ? `${formatWeight(entry.weight)} \u00d7 ${entry.reps}` : `${chalk.bold.cyan(`${entry.reps} reps`)}`;
|
|
162
|
+
const rir = entry.rir != null ? ` ${chalk.dim(`RIR ${entry.rir}`)}` : '';
|
|
163
|
+
lines.push(` ${chalk.dim(date)} ${weight}${rir}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatProgramSummary(payload) {
|
|
170
|
+
if (!payload) {
|
|
171
|
+
return 'No active program.';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const lines = [header('CURRENT PROGRAM'), ''];
|
|
175
|
+
|
|
176
|
+
lines.push(keyValue('Name', payload.programName));
|
|
177
|
+
lines.push(keyValue('Week', String(payload.currentWeek)));
|
|
178
|
+
|
|
179
|
+
if (payload.currentDayTitle) {
|
|
180
|
+
const nextDay = payload.currentDayIndex != null
|
|
181
|
+
? `${payload.currentDayTitle} (Day ${payload.currentDayIndex + 1})`
|
|
182
|
+
: payload.currentDayTitle;
|
|
183
|
+
lines.push(keyValue('Next', nextDay));
|
|
65
184
|
}
|
|
66
185
|
|
|
67
|
-
if (
|
|
68
|
-
|
|
186
|
+
if (payload.trainingWeekdays?.length > 0) {
|
|
187
|
+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
188
|
+
const schedule = payload.trainingWeekdays.map((d) => dayNames[d] ?? d).join(', ');
|
|
189
|
+
lines.push(keyValue('Schedule', schedule));
|
|
69
190
|
}
|
|
70
191
|
|
|
71
|
-
|
|
192
|
+
lines.push(keyValue('Cycles', `${payload.completedCyclesCount} completed`));
|
|
193
|
+
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatProgramList(payload) {
|
|
198
|
+
if (!Array.isArray(payload) || payload.length === 0) {
|
|
199
|
+
return 'No programs found.';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const lines = [header('PROGRAMS'), ''];
|
|
203
|
+
|
|
204
|
+
for (const program of payload) {
|
|
205
|
+
const dot = program.isActive ? chalk.green('\u25cf') : chalk.dim('\u25cb');
|
|
206
|
+
const name = program.isActive ? chalk.bold(program.programName) : program.programName;
|
|
207
|
+
const week = `Week ${program.currentWeek}`;
|
|
208
|
+
const day = program.currentDayTitle ? `Day ${program.currentDayIndex + 1} (${program.currentDayTitle})` : `Day ${program.currentDayIndex + 1}`;
|
|
209
|
+
lines.push(` ${dot} ${name} ${chalk.dim(`${week}${' \u00b7 '}${day}`)}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return lines.join('\n');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function formatPlannedVsActual(payload) {
|
|
216
|
+
if (!payload) {
|
|
217
|
+
return 'No comparison data found.';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const date = formatShortDate(payload.sessionDate);
|
|
221
|
+
const dayTitle = payload.dayTitle ?? 'Workout';
|
|
222
|
+
const lines = [` ${chalk.bold('PLANNED vs ACTUAL')}${dimDot()}${dayTitle} (${date})`, ''];
|
|
223
|
+
|
|
224
|
+
for (const exercise of payload.exercises ?? []) {
|
|
225
|
+
lines.push(` ${chalk.bold(exercise.exerciseName)}`);
|
|
226
|
+
|
|
227
|
+
if (exercise.plannedSets.length > 0) {
|
|
228
|
+
const weights = [...new Set(exercise.plannedSets.map((s) => s.weight))];
|
|
229
|
+
const reps = [...new Set(exercise.plannedSets.map((s) => s.reps))];
|
|
230
|
+
const setCount = exercise.plannedSets.length;
|
|
231
|
+
const weightStr = weights.length === 1 ? `${weights[0]} kg` : `${Math.min(...weights)}-${Math.max(...weights)} kg`;
|
|
232
|
+
const repStr = reps.length === 1 ? `${setCount}\u00d7${reps[0]}` : `${setCount}\u00d7${reps.join('/')}`;
|
|
233
|
+
const rirStr = exercise.plannedRir != null ? ` ${chalk.dim(`RIR ${exercise.plannedRir}`)}` : '';
|
|
234
|
+
lines.push(` ${chalk.dim('Planned')} ${repStr} @ ${weightStr}${rirStr}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (exercise.actualSets.length > 0) {
|
|
238
|
+
const repsList = exercise.actualSets.map((s) => s.reps).join(', ');
|
|
239
|
+
const weights = [...new Set(exercise.actualSets.map((s) => s.weight))];
|
|
240
|
+
const weightStr = weights.length === 1 ? `${weights[0]} kg` : `${Math.min(...weights)}-${Math.max(...weights)} kg`;
|
|
241
|
+
const rirStr = exercise.actualRir != null ? ` ${chalk.dim(`RIR ${exercise.actualRir}`)}` : '';
|
|
242
|
+
lines.push(` ${chalk.dim('Actual')} ${repsList} @ ${weightStr}${rirStr}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
lines.push('');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Remove trailing blank line
|
|
249
|
+
if (lines.at(-1) === '') {
|
|
250
|
+
lines.pop();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return lines.join('\n');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function formatWhyDidThisChange(payload) {
|
|
257
|
+
if (!payload) {
|
|
258
|
+
return 'No session context found.';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const date = formatShortDate(payload.sessionDate);
|
|
262
|
+
const lines = [` ${chalk.bold('SESSION CONTEXT')}${dimDot()}${date}`, ''];
|
|
263
|
+
|
|
264
|
+
if (payload.programWeekNumber != null) {
|
|
265
|
+
lines.push(keyValue('Week', String(payload.programWeekNumber)));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (payload.sessionIntent) {
|
|
269
|
+
lines.push(keyValue('Intent', payload.sessionIntent));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (payload.programProgressionType) {
|
|
273
|
+
lines.push(keyValue('Progression', payload.programProgressionType));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (payload.effortScore != null) {
|
|
277
|
+
lines.push(keyValue('Effort', String(payload.effortScore)));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (payload.averageHeartRate != null || payload.maxHeartRate != null) {
|
|
281
|
+
const parts = [];
|
|
282
|
+
if (payload.averageHeartRate != null) {
|
|
283
|
+
parts.push(`${payload.averageHeartRate} avg`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (payload.maxHeartRate != null) {
|
|
287
|
+
parts.push(`${payload.maxHeartRate} max`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
lines.push(keyValue('HR', parts.join(chalk.dim(' \u00b7 '))));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (payload.latestAdaptationSummary) {
|
|
294
|
+
lines.push(keyValue('Adaptation', payload.latestAdaptationSummary));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const recommendations = payload.recommendations ?? {};
|
|
298
|
+
const recEntries = Object.entries(recommendations);
|
|
299
|
+
|
|
300
|
+
if (recEntries.length > 0) {
|
|
301
|
+
lines.push('');
|
|
302
|
+
lines.push(` ${chalk.bold('Recommendations')}`);
|
|
303
|
+
|
|
304
|
+
for (const [exerciseName, rec] of recEntries) {
|
|
305
|
+
const description = rec.kind === 'increaseWeight'
|
|
306
|
+
? `Increase weight by ${rec.amount} ${rec.unit ?? 'kg'}`
|
|
307
|
+
: rec.kind === 'maintainWeight'
|
|
308
|
+
? 'Maintain current weight'
|
|
309
|
+
: rec.kind ?? 'Adjust';
|
|
310
|
+
lines.push(` ${exerciseName.padEnd(20)} ${chalk.dim(description)}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return lines.join('\n');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --- Main export ---
|
|
318
|
+
|
|
319
|
+
export function formatPretty(command, payload) {
|
|
320
|
+
const formatter = {
|
|
321
|
+
records: formatRecords,
|
|
322
|
+
'session-insights': formatSessionInsights,
|
|
323
|
+
'session-show': formatSessionShow,
|
|
324
|
+
'exercise-history': formatExerciseHistory,
|
|
325
|
+
'program-summary': formatProgramSummary,
|
|
326
|
+
'program-list': formatProgramList,
|
|
327
|
+
'planned-vs-actual': formatPlannedVsActual,
|
|
328
|
+
'why-did-this-change': formatWhyDidThisChange
|
|
329
|
+
}[command];
|
|
330
|
+
|
|
331
|
+
return formatter ? formatter(payload) : null;
|
|
72
332
|
}
|
package/src/lib.js
CHANGED
|
@@ -266,8 +266,14 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
266
266
|
|
|
267
267
|
try {
|
|
268
268
|
const payload = await transport.executeReadCommand(normalizedCommand, options);
|
|
269
|
-
const
|
|
270
|
-
|
|
269
|
+
const wantJson = options.json || !(stdout.isTTY ?? false);
|
|
270
|
+
if (wantJson) {
|
|
271
|
+
stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
272
|
+
} else {
|
|
273
|
+
const pretty = formatPretty(normalizedCommand, payload);
|
|
274
|
+
stdout.write(`${pretty ?? JSON.stringify(payload, null, 2)}\n`);
|
|
275
|
+
}
|
|
276
|
+
|
|
271
277
|
return 0;
|
|
272
278
|
} catch (error) {
|
|
273
279
|
stderr.write(`${error.message}\n`);
|