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.
Files changed (3) hide show
  1. package/package.json +4 -1
  2. package/src/format.js +289 -29
  3. package/src/lib.js +8 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incremnt",
3
- "version": "0.1.0",
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
- function formatShortDate(dateString) {
6
+ // --- Shared helpers ---
7
+
8
+ function parseDate(dateString) {
5
9
  const date = new Date(dateString);
6
- if (Number.isNaN(date.getTime())) {
7
- return dateString;
8
- }
10
+ return Number.isNaN(date.getTime()) ? null : date;
11
+ }
9
12
 
10
- return `${date.getDate()} ${shortMonths[date.getMonth()]}`;
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 = new Date(dateString);
15
- if (Number.isNaN(date.getTime())) {
16
- return dateString;
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
- return `${shortDays[date.getDay()]} ${date.getDate()} ${shortMonths[date.getMonth()]}`;
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
- return payload.map((record) => {
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
- return `${name} ${reps} BW \u00b7 ${date}`;
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
- const weight = `${Number(record.weight).toFixed(1)} kg`.padStart(12);
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
- return payload.map((session) => {
50
- const date = formatDayAndDate(session.sessionDate);
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
- ? `${Math.round(session.durationSeconds / 60)} min`
55
- : '';
56
- const suffix = duration ? ` \u00b7 ${exercises} \u00b7 ${duration}` : ` \u00b7 ${exercises}`;
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
- return `${date} ${dayName}${suffix}`;
59
- }).join('\n');
92
+ return lines.join('\n');
60
93
  }
61
94
 
62
- export function formatPretty(command, payload) {
63
- if (command === 'records') {
64
- return formatRecords(payload);
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 (command === 'session-insights') {
68
- return formatSessionInsights(payload);
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
- return null;
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 pretty = options.pretty ? formatPretty(normalizedCommand, payload) : null;
270
- stdout.write(pretty ? `${pretty}\n` : `${JSON.stringify(payload, null, 2)}\n`);
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`);