From a14cec9a766c60f516beb0dc1af71bfa21fd0664 Mon Sep 17 00:00:00 2001 From: Greg Pstrucha <875316+Gricha@users.noreply.github.com> Date: Sun, 25 Jan 2026 06:29:35 +0000 Subject: [PATCH] feat: add undo, edit, and delete commands for logged sets Add ability to fix mistakes when logging sets: - `workout undo [exercise]` - remove last set - `workout edit [weight] [reps]` - modify a set - `workout delete ` - remove specific set --- src/commands/session.ts | 174 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 6 ++ test/commands.test.ts | 178 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 358 insertions(+) diff --git a/src/commands/session.ts b/src/commands/session.ts index d4d3d6f..0ee0498 100644 --- a/src/commands/session.ts +++ b/src/commands/session.ts @@ -377,3 +377,177 @@ export function createAddCommand(getProfile: () => string | undefined): Command console.log(`Added ${exercise.name} to workout`); }); } + +export function createUndoCommand(getProfile: () => string | undefined): Command { + return new Command('undo') + .description('Remove the last logged set') + .argument('[exercise]', 'Exercise ID (defaults to last exercise with sets)') + .action((exerciseId: string | undefined) => { + const storage = getStorage(getProfile()); + const workout = storage.getCurrentWorkout(); + + if (!workout) { + console.error('No active workout. Start one with "workout start".'); + process.exit(1); + } + + const config = storage.getConfig(); + const unit = config.units; + + let exerciseLog: ExerciseLog | undefined; + let exerciseName: string; + + if (exerciseId) { + const exercise = storage.getExercise(exerciseId); + if (!exercise) { + console.error(`Exercise "${exerciseId}" not found.`); + process.exit(1); + } + exerciseLog = workout.exercises.find((e) => e.exercise === exercise.id); + exerciseName = exercise.name; + if (!exerciseLog) { + console.error(`Exercise "${exercise.name}" is not in the current workout.`); + process.exit(1); + } + } else { + for (let i = workout.exercises.length - 1; i >= 0; i--) { + const log = workout.exercises[i]!; + if (log.sets.length > 0) { + exerciseLog = log; + break; + } + } + if (!exerciseLog) { + console.error('No sets to undo.'); + process.exit(1); + } + const exercise = storage.getExercise(exerciseLog.exercise); + exerciseName = exercise?.name ?? exerciseLog.exercise; + } + + if (exerciseLog.sets.length === 0) { + console.error(`No sets to undo for ${exerciseName}.`); + process.exit(1); + } + + const removedSet = exerciseLog.sets.pop()!; + storage.saveCurrentWorkout(workout); + console.log( + `Removed set: ${removedSet.weight}${unit} x ${removedSet.reps} from ${exerciseName}` + ); + }); +} + +export function createEditCommand(getProfile: () => string | undefined): Command { + return new Command('edit') + .description('Edit a specific set') + .argument('', 'Exercise ID') + .argument('', 'Set number (1-indexed)') + .argument('[weight]', 'New weight') + .argument('[reps]', 'New reps') + .option('--reps ', 'New reps (alternative to positional)') + .option('--rir ', 'New RIR value') + .action( + ( + exerciseId: string, + setNum: string, + weightStr: string | undefined, + repsStr: string | undefined, + options: { reps?: string; rir?: string } + ) => { + const storage = getStorage(getProfile()); + const workout = storage.getCurrentWorkout(); + + if (!workout) { + console.error('No active workout. Start one with "workout start".'); + process.exit(1); + } + + const exercise = storage.getExercise(exerciseId); + if (!exercise) { + console.error(`Exercise "${exerciseId}" not found.`); + process.exit(1); + } + + const exerciseLog = workout.exercises.find((e) => e.exercise === exercise.id); + if (!exerciseLog) { + console.error(`Exercise "${exercise.name}" is not in the current workout.`); + process.exit(1); + } + + const setIndex = parseInt(setNum, 10) - 1; + if (isNaN(setIndex) || setIndex < 0 || setIndex >= exerciseLog.sets.length) { + console.error( + `Invalid set number. ${exercise.name} has ${exerciseLog.sets.length} set(s).` + ); + process.exit(1); + } + + const set = exerciseLog.sets[setIndex]!; + const config = storage.getConfig(); + const unit = config.units; + const before = `${set.weight}${unit}x${set.reps}`; + + if (weightStr !== undefined) { + set.weight = parseFloat(weightStr); + } + + const repsValue = repsStr ?? options.reps; + if (repsValue !== undefined) { + set.reps = parseInt(repsValue, 10); + } + + if (options.rir !== undefined) { + set.rir = parseInt(options.rir, 10); + } + + storage.saveCurrentWorkout(workout); + const after = `${set.weight}${unit}x${set.reps}`; + console.log(`Updated set ${setNum}: ${before} → ${after}`); + } + ); +} + +export function createDeleteCommand(getProfile: () => string | undefined): Command { + return new Command('delete') + .description('Delete a specific set') + .argument('', 'Exercise ID') + .argument('', 'Set number (1-indexed)') + .action((exerciseId: string, setNum: string) => { + const storage = getStorage(getProfile()); + const workout = storage.getCurrentWorkout(); + + if (!workout) { + console.error('No active workout. Start one with "workout start".'); + process.exit(1); + } + + const exercise = storage.getExercise(exerciseId); + if (!exercise) { + console.error(`Exercise "${exerciseId}" not found.`); + process.exit(1); + } + + const exerciseLog = workout.exercises.find((e) => e.exercise === exercise.id); + if (!exerciseLog) { + console.error(`Exercise "${exercise.name}" is not in the current workout.`); + process.exit(1); + } + + const setIndex = parseInt(setNum, 10) - 1; + if (isNaN(setIndex) || setIndex < 0 || setIndex >= exerciseLog.sets.length) { + console.error( + `Invalid set number. ${exercise.name} has ${exerciseLog.sets.length} set(s).` + ); + process.exit(1); + } + + const config = storage.getConfig(); + const unit = config.units; + const [removedSet] = exerciseLog.sets.splice(setIndex, 1); + storage.saveCurrentWorkout(workout); + console.log( + `Deleted set ${setNum}: ${removedSet!.weight}${unit} x ${removedSet!.reps} from ${exercise.name}` + ); + }); +} diff --git a/src/index.ts b/src/index.ts index d3e0a10..8912770 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,9 @@ import { createNoteCommand, createSwapCommand, createAddCommand, + createUndoCommand, + createEditCommand, + createDeleteCommand, } from './commands/session.js'; import { createLastCommand, createHistoryCommand } from './commands/history.js'; import { @@ -43,6 +46,9 @@ program.addCommand(createCancelCommand(getProfile)); program.addCommand(createNoteCommand(getProfile)); program.addCommand(createSwapCommand(getProfile)); program.addCommand(createAddCommand(getProfile)); +program.addCommand(createUndoCommand(getProfile)); +program.addCommand(createEditCommand(getProfile)); +program.addCommand(createDeleteCommand(getProfile)); program.addCommand(createLastCommand(getProfile)); program.addCommand(createHistoryCommand(getProfile)); program.addCommand(createPRCommand(getProfile)); diff --git a/test/commands.test.ts b/test/commands.test.ts index 16b8c16..6ae25f8 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -190,6 +190,184 @@ describe('workout session flow', () => { expect(sets).toHaveLength(3); expect(sets.map((s) => s.reps)).toEqual([12, 10, 8]); }); + + it('undo removes last set from specified exercise', () => { + const now = new Date(); + const date = now.toISOString().split('T')[0]!; + + storage.saveCurrentWorkout({ + id: `${date}-test`, + date, + template: null, + startTime: now.toISOString(), + endTime: null, + exercises: [ + { + exercise: 'bench-press', + sets: [ + { weight: 135, reps: 10, rir: null }, + { weight: 135, reps: 9, rir: null }, + { weight: 135, reps: 8, rir: null }, + ], + }, + ], + notes: [], + }); + + const current = storage.getCurrentWorkout()!; + const benchLog = current.exercises.find((e) => e.exercise === 'bench-press')!; + benchLog.sets.pop(); + storage.saveCurrentWorkout(current); + + const updated = storage.getCurrentWorkout()!; + const sets = updated.exercises.find((e) => e.exercise === 'bench-press')!.sets; + expect(sets).toHaveLength(2); + expect(sets.map((s) => s.reps)).toEqual([10, 9]); + }); + + it('undo finds last exercise with sets when no exercise specified', () => { + const now = new Date(); + const date = now.toISOString().split('T')[0]!; + + storage.saveCurrentWorkout({ + id: `${date}-test`, + date, + template: null, + startTime: now.toISOString(), + endTime: null, + exercises: [ + { + exercise: 'bench-press', + sets: [{ weight: 135, reps: 10, rir: null }], + }, + { + exercise: 'overhead-press', + sets: [], + }, + { + exercise: 'squat', + sets: [ + { weight: 185, reps: 5, rir: null }, + { weight: 185, reps: 5, rir: null }, + ], + }, + ], + notes: [], + }); + + const current = storage.getCurrentWorkout()!; + for (let i = current.exercises.length - 1; i >= 0; i--) { + const log = current.exercises[i]!; + if (log.sets.length > 0) { + log.sets.pop(); + break; + } + } + storage.saveCurrentWorkout(current); + + const updated = storage.getCurrentWorkout()!; + const squatSets = updated.exercises.find((e) => e.exercise === 'squat')!.sets; + expect(squatSets).toHaveLength(1); + }); + + it('edit updates weight and reps for a specific set', () => { + const now = new Date(); + const date = now.toISOString().split('T')[0]!; + + storage.saveCurrentWorkout({ + id: `${date}-test`, + date, + template: null, + startTime: now.toISOString(), + endTime: null, + exercises: [ + { + exercise: 'bench-press', + sets: [ + { weight: 135, reps: 10, rir: null }, + { weight: 135, reps: 9, rir: null }, + ], + }, + ], + notes: [], + }); + + const current = storage.getCurrentWorkout()!; + const benchLog = current.exercises.find((e) => e.exercise === 'bench-press')!; + benchLog.sets[0]!.weight = 185; + benchLog.sets[0]!.reps = 12; + storage.saveCurrentWorkout(current); + + const updated = storage.getCurrentWorkout()!; + const set1 = updated.exercises.find((e) => e.exercise === 'bench-press')!.sets[0]!; + expect(set1.weight).toBe(185); + expect(set1.reps).toBe(12); + }); + + it('edit can update only weight or only reps', () => { + const now = new Date(); + const date = now.toISOString().split('T')[0]!; + + storage.saveCurrentWorkout({ + id: `${date}-test`, + date, + template: null, + startTime: now.toISOString(), + endTime: null, + exercises: [ + { + exercise: 'squat', + sets: [{ weight: 185, reps: 5, rir: null }], + }, + ], + notes: [], + }); + + const current = storage.getCurrentWorkout()!; + const squatLog = current.exercises.find((e) => e.exercise === 'squat')!; + squatLog.sets[0]!.weight = 225; + storage.saveCurrentWorkout(current); + + const updated = storage.getCurrentWorkout()!; + const set = updated.exercises.find((e) => e.exercise === 'squat')!.sets[0]!; + expect(set.weight).toBe(225); + expect(set.reps).toBe(5); + }); + + it('delete removes a specific set by index', () => { + const now = new Date(); + const date = now.toISOString().split('T')[0]!; + + storage.saveCurrentWorkout({ + id: `${date}-test`, + date, + template: null, + startTime: now.toISOString(), + endTime: null, + exercises: [ + { + exercise: 'bench-press', + sets: [ + { weight: 135, reps: 10, rir: null }, + { weight: 145, reps: 8, rir: null }, + { weight: 155, reps: 6, rir: null }, + ], + }, + ], + notes: [], + }); + + const current = storage.getCurrentWorkout()!; + const benchLog = current.exercises.find((e) => e.exercise === 'bench-press')!; + benchLog.sets.splice(1, 1); + storage.saveCurrentWorkout(current); + + const updated = storage.getCurrentWorkout()!; + const sets = updated.exercises.find((e) => e.exercise === 'bench-press')!.sets; + expect(sets).toHaveLength(2); + expect(sets[0]!.weight).toBe(135); + expect(sets[1]!.weight).toBe(155); + }); }); describe('exercise management', () => {