Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions src/commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>', 'Exercise ID')
.argument('<set>', 'Set number (1-indexed)')
.argument('[weight]', 'New weight')
.argument('[reps]', 'New reps')
.option('--reps <reps>', 'New reps (alternative to positional)')
.option('--rir <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>', 'Exercise ID')
.argument('<set>', '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}`
);
});
}
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
createNoteCommand,
createSwapCommand,
createAddCommand,
createUndoCommand,
createEditCommand,
createDeleteCommand,
} from './commands/session.js';
import { createLastCommand, createHistoryCommand } from './commands/history.js';
import {
Expand Down Expand Up @@ -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));
Expand Down
178 changes: 178 additions & 0 deletions test/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down