Skip to content

Commit 402fa7f

Browse files
authored
feat: add undo, edit, and delete commands for logged sets (#4)
Add ability to fix mistakes when logging sets: - `workout undo [exercise]` - remove last set - `workout edit <exercise> <set#> [weight] [reps]` - modify a set - `workout delete <exercise> <set#>` - remove specific set
1 parent da51970 commit 402fa7f

File tree

3 files changed

+358
-0
lines changed

3 files changed

+358
-0
lines changed

src/commands/session.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,3 +377,177 @@ export function createAddCommand(getProfile: () => string | undefined): Command
377377
console.log(`Added ${exercise.name} to workout`);
378378
});
379379
}
380+
381+
export function createUndoCommand(getProfile: () => string | undefined): Command {
382+
return new Command('undo')
383+
.description('Remove the last logged set')
384+
.argument('[exercise]', 'Exercise ID (defaults to last exercise with sets)')
385+
.action((exerciseId: string | undefined) => {
386+
const storage = getStorage(getProfile());
387+
const workout = storage.getCurrentWorkout();
388+
389+
if (!workout) {
390+
console.error('No active workout. Start one with "workout start".');
391+
process.exit(1);
392+
}
393+
394+
const config = storage.getConfig();
395+
const unit = config.units;
396+
397+
let exerciseLog: ExerciseLog | undefined;
398+
let exerciseName: string;
399+
400+
if (exerciseId) {
401+
const exercise = storage.getExercise(exerciseId);
402+
if (!exercise) {
403+
console.error(`Exercise "${exerciseId}" not found.`);
404+
process.exit(1);
405+
}
406+
exerciseLog = workout.exercises.find((e) => e.exercise === exercise.id);
407+
exerciseName = exercise.name;
408+
if (!exerciseLog) {
409+
console.error(`Exercise "${exercise.name}" is not in the current workout.`);
410+
process.exit(1);
411+
}
412+
} else {
413+
for (let i = workout.exercises.length - 1; i >= 0; i--) {
414+
const log = workout.exercises[i]!;
415+
if (log.sets.length > 0) {
416+
exerciseLog = log;
417+
break;
418+
}
419+
}
420+
if (!exerciseLog) {
421+
console.error('No sets to undo.');
422+
process.exit(1);
423+
}
424+
const exercise = storage.getExercise(exerciseLog.exercise);
425+
exerciseName = exercise?.name ?? exerciseLog.exercise;
426+
}
427+
428+
if (exerciseLog.sets.length === 0) {
429+
console.error(`No sets to undo for ${exerciseName}.`);
430+
process.exit(1);
431+
}
432+
433+
const removedSet = exerciseLog.sets.pop()!;
434+
storage.saveCurrentWorkout(workout);
435+
console.log(
436+
`Removed set: ${removedSet.weight}${unit} x ${removedSet.reps} from ${exerciseName}`
437+
);
438+
});
439+
}
440+
441+
export function createEditCommand(getProfile: () => string | undefined): Command {
442+
return new Command('edit')
443+
.description('Edit a specific set')
444+
.argument('<exercise>', 'Exercise ID')
445+
.argument('<set>', 'Set number (1-indexed)')
446+
.argument('[weight]', 'New weight')
447+
.argument('[reps]', 'New reps')
448+
.option('--reps <reps>', 'New reps (alternative to positional)')
449+
.option('--rir <rir>', 'New RIR value')
450+
.action(
451+
(
452+
exerciseId: string,
453+
setNum: string,
454+
weightStr: string | undefined,
455+
repsStr: string | undefined,
456+
options: { reps?: string; rir?: string }
457+
) => {
458+
const storage = getStorage(getProfile());
459+
const workout = storage.getCurrentWorkout();
460+
461+
if (!workout) {
462+
console.error('No active workout. Start one with "workout start".');
463+
process.exit(1);
464+
}
465+
466+
const exercise = storage.getExercise(exerciseId);
467+
if (!exercise) {
468+
console.error(`Exercise "${exerciseId}" not found.`);
469+
process.exit(1);
470+
}
471+
472+
const exerciseLog = workout.exercises.find((e) => e.exercise === exercise.id);
473+
if (!exerciseLog) {
474+
console.error(`Exercise "${exercise.name}" is not in the current workout.`);
475+
process.exit(1);
476+
}
477+
478+
const setIndex = parseInt(setNum, 10) - 1;
479+
if (isNaN(setIndex) || setIndex < 0 || setIndex >= exerciseLog.sets.length) {
480+
console.error(
481+
`Invalid set number. ${exercise.name} has ${exerciseLog.sets.length} set(s).`
482+
);
483+
process.exit(1);
484+
}
485+
486+
const set = exerciseLog.sets[setIndex]!;
487+
const config = storage.getConfig();
488+
const unit = config.units;
489+
const before = `${set.weight}${unit}x${set.reps}`;
490+
491+
if (weightStr !== undefined) {
492+
set.weight = parseFloat(weightStr);
493+
}
494+
495+
const repsValue = repsStr ?? options.reps;
496+
if (repsValue !== undefined) {
497+
set.reps = parseInt(repsValue, 10);
498+
}
499+
500+
if (options.rir !== undefined) {
501+
set.rir = parseInt(options.rir, 10);
502+
}
503+
504+
storage.saveCurrentWorkout(workout);
505+
const after = `${set.weight}${unit}x${set.reps}`;
506+
console.log(`Updated set ${setNum}: ${before}${after}`);
507+
}
508+
);
509+
}
510+
511+
export function createDeleteCommand(getProfile: () => string | undefined): Command {
512+
return new Command('delete')
513+
.description('Delete a specific set')
514+
.argument('<exercise>', 'Exercise ID')
515+
.argument('<set>', 'Set number (1-indexed)')
516+
.action((exerciseId: string, setNum: string) => {
517+
const storage = getStorage(getProfile());
518+
const workout = storage.getCurrentWorkout();
519+
520+
if (!workout) {
521+
console.error('No active workout. Start one with "workout start".');
522+
process.exit(1);
523+
}
524+
525+
const exercise = storage.getExercise(exerciseId);
526+
if (!exercise) {
527+
console.error(`Exercise "${exerciseId}" not found.`);
528+
process.exit(1);
529+
}
530+
531+
const exerciseLog = workout.exercises.find((e) => e.exercise === exercise.id);
532+
if (!exerciseLog) {
533+
console.error(`Exercise "${exercise.name}" is not in the current workout.`);
534+
process.exit(1);
535+
}
536+
537+
const setIndex = parseInt(setNum, 10) - 1;
538+
if (isNaN(setIndex) || setIndex < 0 || setIndex >= exerciseLog.sets.length) {
539+
console.error(
540+
`Invalid set number. ${exercise.name} has ${exerciseLog.sets.length} set(s).`
541+
);
542+
process.exit(1);
543+
}
544+
545+
const config = storage.getConfig();
546+
const unit = config.units;
547+
const [removedSet] = exerciseLog.sets.splice(setIndex, 1);
548+
storage.saveCurrentWorkout(workout);
549+
console.log(
550+
`Deleted set ${setNum}: ${removedSet!.weight}${unit} x ${removedSet!.reps} from ${exercise.name}`
551+
);
552+
});
553+
}

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import {
1111
createNoteCommand,
1212
createSwapCommand,
1313
createAddCommand,
14+
createUndoCommand,
15+
createEditCommand,
16+
createDeleteCommand,
1417
} from './commands/session.js';
1518
import { createLastCommand, createHistoryCommand } from './commands/history.js';
1619
import {
@@ -43,6 +46,9 @@ program.addCommand(createCancelCommand(getProfile));
4346
program.addCommand(createNoteCommand(getProfile));
4447
program.addCommand(createSwapCommand(getProfile));
4548
program.addCommand(createAddCommand(getProfile));
49+
program.addCommand(createUndoCommand(getProfile));
50+
program.addCommand(createEditCommand(getProfile));
51+
program.addCommand(createDeleteCommand(getProfile));
4652
program.addCommand(createLastCommand(getProfile));
4753
program.addCommand(createHistoryCommand(getProfile));
4854
program.addCommand(createPRCommand(getProfile));

test/commands.test.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,184 @@ describe('workout session flow', () => {
190190
expect(sets).toHaveLength(3);
191191
expect(sets.map((s) => s.reps)).toEqual([12, 10, 8]);
192192
});
193+
194+
it('undo removes last set from specified exercise', () => {
195+
const now = new Date();
196+
const date = now.toISOString().split('T')[0]!;
197+
198+
storage.saveCurrentWorkout({
199+
id: `${date}-test`,
200+
date,
201+
template: null,
202+
startTime: now.toISOString(),
203+
endTime: null,
204+
exercises: [
205+
{
206+
exercise: 'bench-press',
207+
sets: [
208+
{ weight: 135, reps: 10, rir: null },
209+
{ weight: 135, reps: 9, rir: null },
210+
{ weight: 135, reps: 8, rir: null },
211+
],
212+
},
213+
],
214+
notes: [],
215+
});
216+
217+
const current = storage.getCurrentWorkout()!;
218+
const benchLog = current.exercises.find((e) => e.exercise === 'bench-press')!;
219+
benchLog.sets.pop();
220+
storage.saveCurrentWorkout(current);
221+
222+
const updated = storage.getCurrentWorkout()!;
223+
const sets = updated.exercises.find((e) => e.exercise === 'bench-press')!.sets;
224+
expect(sets).toHaveLength(2);
225+
expect(sets.map((s) => s.reps)).toEqual([10, 9]);
226+
});
227+
228+
it('undo finds last exercise with sets when no exercise specified', () => {
229+
const now = new Date();
230+
const date = now.toISOString().split('T')[0]!;
231+
232+
storage.saveCurrentWorkout({
233+
id: `${date}-test`,
234+
date,
235+
template: null,
236+
startTime: now.toISOString(),
237+
endTime: null,
238+
exercises: [
239+
{
240+
exercise: 'bench-press',
241+
sets: [{ weight: 135, reps: 10, rir: null }],
242+
},
243+
{
244+
exercise: 'overhead-press',
245+
sets: [],
246+
},
247+
{
248+
exercise: 'squat',
249+
sets: [
250+
{ weight: 185, reps: 5, rir: null },
251+
{ weight: 185, reps: 5, rir: null },
252+
],
253+
},
254+
],
255+
notes: [],
256+
});
257+
258+
const current = storage.getCurrentWorkout()!;
259+
for (let i = current.exercises.length - 1; i >= 0; i--) {
260+
const log = current.exercises[i]!;
261+
if (log.sets.length > 0) {
262+
log.sets.pop();
263+
break;
264+
}
265+
}
266+
storage.saveCurrentWorkout(current);
267+
268+
const updated = storage.getCurrentWorkout()!;
269+
const squatSets = updated.exercises.find((e) => e.exercise === 'squat')!.sets;
270+
expect(squatSets).toHaveLength(1);
271+
});
272+
273+
it('edit updates weight and reps for a specific set', () => {
274+
const now = new Date();
275+
const date = now.toISOString().split('T')[0]!;
276+
277+
storage.saveCurrentWorkout({
278+
id: `${date}-test`,
279+
date,
280+
template: null,
281+
startTime: now.toISOString(),
282+
endTime: null,
283+
exercises: [
284+
{
285+
exercise: 'bench-press',
286+
sets: [
287+
{ weight: 135, reps: 10, rir: null },
288+
{ weight: 135, reps: 9, rir: null },
289+
],
290+
},
291+
],
292+
notes: [],
293+
});
294+
295+
const current = storage.getCurrentWorkout()!;
296+
const benchLog = current.exercises.find((e) => e.exercise === 'bench-press')!;
297+
benchLog.sets[0]!.weight = 185;
298+
benchLog.sets[0]!.reps = 12;
299+
storage.saveCurrentWorkout(current);
300+
301+
const updated = storage.getCurrentWorkout()!;
302+
const set1 = updated.exercises.find((e) => e.exercise === 'bench-press')!.sets[0]!;
303+
expect(set1.weight).toBe(185);
304+
expect(set1.reps).toBe(12);
305+
});
306+
307+
it('edit can update only weight or only reps', () => {
308+
const now = new Date();
309+
const date = now.toISOString().split('T')[0]!;
310+
311+
storage.saveCurrentWorkout({
312+
id: `${date}-test`,
313+
date,
314+
template: null,
315+
startTime: now.toISOString(),
316+
endTime: null,
317+
exercises: [
318+
{
319+
exercise: 'squat',
320+
sets: [{ weight: 185, reps: 5, rir: null }],
321+
},
322+
],
323+
notes: [],
324+
});
325+
326+
const current = storage.getCurrentWorkout()!;
327+
const squatLog = current.exercises.find((e) => e.exercise === 'squat')!;
328+
squatLog.sets[0]!.weight = 225;
329+
storage.saveCurrentWorkout(current);
330+
331+
const updated = storage.getCurrentWorkout()!;
332+
const set = updated.exercises.find((e) => e.exercise === 'squat')!.sets[0]!;
333+
expect(set.weight).toBe(225);
334+
expect(set.reps).toBe(5);
335+
});
336+
337+
it('delete removes a specific set by index', () => {
338+
const now = new Date();
339+
const date = now.toISOString().split('T')[0]!;
340+
341+
storage.saveCurrentWorkout({
342+
id: `${date}-test`,
343+
date,
344+
template: null,
345+
startTime: now.toISOString(),
346+
endTime: null,
347+
exercises: [
348+
{
349+
exercise: 'bench-press',
350+
sets: [
351+
{ weight: 135, reps: 10, rir: null },
352+
{ weight: 145, reps: 8, rir: null },
353+
{ weight: 155, reps: 6, rir: null },
354+
],
355+
},
356+
],
357+
notes: [],
358+
});
359+
360+
const current = storage.getCurrentWorkout()!;
361+
const benchLog = current.exercises.find((e) => e.exercise === 'bench-press')!;
362+
benchLog.sets.splice(1, 1);
363+
storage.saveCurrentWorkout(current);
364+
365+
const updated = storage.getCurrentWorkout()!;
366+
const sets = updated.exercises.find((e) => e.exercise === 'bench-press')!.sets;
367+
expect(sets).toHaveLength(2);
368+
expect(sets[0]!.weight).toBe(135);
369+
expect(sets[1]!.weight).toBe(155);
370+
});
193371
});
194372

195373
describe('exercise management', () => {

0 commit comments

Comments
 (0)