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
63 changes: 58 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,52 @@ workout log overhead-press 95 10,9,8
workout done
```

## Multi-User Profiles

Multiple people can track workouts independently on the same machine using profiles.

### How it Works

- **Single profile**: Commands work automatically (backwards compatible)
- **Multiple profiles**: Use `--profile <name>` to specify which profile
- **Shared exercises**: The exercise library is shared across all profiles
- **Per-user data**: Templates, workouts, config, and current session are per-profile

### Profile Commands

```bash
# List all profiles
workout profile list

# Create a new profile
workout profile create sarah

# Delete a profile (cannot delete the last one)
workout profile delete old-profile
```

### Using Profiles

```bash
# When multiple profiles exist, specify which one to use
workout --profile mike start push-day
workout --profile mike log bench-press 185 8,8,7,6
workout --profile mike done

# Sarah can workout simultaneously
workout --profile sarah start leg-day
workout --profile sarah log squat 135 8,8,8
workout --profile sarah done

# Check each person's status
workout --profile mike status
workout --profile sarah status
```

### Migration

If you have existing data, it will automatically migrate to a `default` profile on first use. The exercise library stays shared at the root level.

## Commands

### Workout Sessions
Expand Down Expand Up @@ -377,11 +423,18 @@ All data is stored locally in `~/.workout/`:

```
~/.workout/
config.json # User preferences
exercises.json # Custom exercises
templates.json # Workout templates
history/ # Completed workouts
current.json # Active workout (if any)
├── exercises.json # Shared exercise library
├── profiles/
│ ├── default/ # Default profile (or your profile name)
│ │ ├── config.json # User preferences (units, etc.)
│ │ ├── templates.json
│ │ ├── current.json # Active workout (if any)
│ │ └── workouts/ # Completed workouts
│ └── sarah/ # Another profile
│ ├── config.json
│ ├── templates.json
│ ├── current.json
│ └── workouts/
```

## JSON Output
Expand Down
12 changes: 6 additions & 6 deletions src/commands/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ function findPRs(storage: ReturnType<typeof getStorage>): Map<string, PR> {
return prs;
}

export function createPRCommand(): Command {
export function createPRCommand(getProfile: () => string | undefined): Command {
return new Command('pr')
.description('Show personal records')
.argument('[exercise]', 'Exercise ID (optional, shows all if omitted)')
.option('-m, --muscle <muscle>', 'Filter by muscle group')
.option('--json', 'Output as JSON')
.action((exerciseId: string | undefined, options: { muscle?: string; json?: boolean }) => {
const storage = getStorage();
const storage = getStorage(getProfile());
const config = storage.getConfig();
const unit = config.units;
const prs = findPRs(storage);
Expand Down Expand Up @@ -123,7 +123,7 @@ function formatDateRange(start: Date, end: Date): string {
return `${startStr} to ${endStr}`;
}

export function createVolumeCommand(): Command {
export function createVolumeCommand(getProfile: () => string | undefined): Command {
return new Command('volume')
.description('Analyze training volume')
.option('-w, --week', 'Show current week')
Expand All @@ -139,7 +139,7 @@ export function createVolumeCommand(): Command {
by?: string;
json?: boolean;
}) => {
const storage = getStorage();
const storage = getStorage(getProfile());
const config = storage.getConfig();
const unit = config.units;
const workouts = storage.getAllWorkouts();
Expand Down Expand Up @@ -268,14 +268,14 @@ export function createVolumeCommand(): Command {
);
}

export function createProgressionCommand(): Command {
export function createProgressionCommand(getProfile: () => string | undefined): Command {
return new Command('progression')
.description('Show progression over time for an exercise')
.argument('<exercise>', 'Exercise ID')
.option('-n, --last <count>', 'Show last N sessions', '10')
.option('--json', 'Output as JSON')
.action((exerciseId: string, options: { last: string; json?: boolean }) => {
const storage = getStorage();
const storage = getStorage(getProfile());
const config = storage.getConfig();
const unit = config.units;
const exercise = storage.getExercise(exerciseId);
Expand Down
14 changes: 7 additions & 7 deletions src/commands/exercises.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Command } from 'commander';
import { getStorage } from '../data/storage.js';
import { getSharedStorage } from '../data/storage.js';
import {
Exercise,
slugify,
Expand All @@ -8,7 +8,7 @@ import {
type Equipment,
} from '../types.js';

export function createExercisesCommand(): Command {
export function createExercisesCommand(_getProfile: () => string | undefined): Command {
const exercises = new Command('exercises').description('Manage exercise library');

exercises
Expand All @@ -18,7 +18,7 @@ export function createExercisesCommand(): Command {
.option('-t, --type <type>', 'Filter by exercise type (compound/isolation)')
.option('--json', 'Output as JSON')
.action((options: { muscle?: string; type?: string; json?: boolean }) => {
const storage = getStorage();
const storage = getSharedStorage();
let exerciseList = storage.getExercises();

if (options.muscle) {
Expand Down Expand Up @@ -51,7 +51,7 @@ export function createExercisesCommand(): Command {
.description('Show exercise details')
.option('--json', 'Output as JSON')
.action((id: string, options: { json?: boolean }) => {
const storage = getStorage();
const storage = getSharedStorage();
const exercise = storage.getExercise(id);

if (!exercise) {
Expand Down Expand Up @@ -98,7 +98,7 @@ export function createExercisesCommand(): Command {
notes?: string;
}
) => {
const storage = getStorage();
const storage = getSharedStorage();
const id = options.id ?? slugify(name);
const muscles = options.muscles.split(',').map((m) => m.trim()) as MuscleGroup[];
const aliases = options.aliases ? options.aliases.split(',').map((a) => a.trim()) : [];
Expand Down Expand Up @@ -146,7 +146,7 @@ export function createExercisesCommand(): Command {
notes?: string;
}
) => {
const storage = getStorage();
const storage = getSharedStorage();
const exercise = storage.getExercise(id);

if (!exercise) {
Expand Down Expand Up @@ -192,7 +192,7 @@ export function createExercisesCommand(): Command {
.command('delete <id>')
.description('Delete an exercise')
.action((id: string) => {
const storage = getStorage();
const storage = getSharedStorage();

try {
storage.deleteExercise(id);
Expand Down
8 changes: 4 additions & 4 deletions src/commands/history.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Command } from 'commander';
import { getStorage } from '../data/storage.js';

export function createLastCommand(): Command {
export function createLastCommand(getProfile: () => string | undefined): Command {
return new Command('last')
.description('Show last workout')
.option('--full', 'Show full details')
.option('--json', 'Output as JSON')
.action((options: { full?: boolean; json?: boolean }) => {
const storage = getStorage();
const storage = getStorage(getProfile());
const workout = storage.getLastWorkout();

if (!workout) {
Expand Down Expand Up @@ -82,14 +82,14 @@ export function createLastCommand(): Command {
});
}

export function createHistoryCommand(): Command {
export function createHistoryCommand(getProfile: () => string | undefined): Command {
return new Command('history')
.description('Show exercise history')
.argument('<exercise>', 'Exercise ID')
.option('-n, --last <count>', 'Show last N sessions', '10')
.option('--json', 'Output as JSON')
.action((exerciseId: string, options: { last: string; json?: boolean }) => {
const storage = getStorage();
const storage = getStorage(getProfile());
const exercise = storage.getExercise(exerciseId);

if (!exercise) {
Expand Down
51 changes: 51 additions & 0 deletions src/commands/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Command } from 'commander';
import { getProfiles, createProfile, deleteProfile } from '../data/profiles.js';

export function createProfileCommand(): Command {
const profile = new Command('profile').description('Manage user profiles');

profile
.command('list')
.description('List all profiles')
.action(() => {
const profiles = getProfiles();

if (profiles.length === 0) {
console.log('No profiles found. A default profile will be created on first use.');
return;
}

console.log('Profiles:');
for (const p of profiles) {
console.log(` ${p}`);
}
});

profile
.command('create <name>')
.description('Create a new profile')
.action((name: string) => {
try {
createProfile(name);
console.log(`Created profile: ${name}`);
} catch (err) {
console.error((err as Error).message);
process.exit(1);
}
});

profile
.command('delete <name>')
.description('Delete a profile')
.action((name: string) => {
try {
deleteProfile(name);
console.log(`Deleted profile: ${name}`);
} catch (err) {
console.error((err as Error).message);
process.exit(1);
}
});

return profile;
}
32 changes: 16 additions & 16 deletions src/commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ function calculateStats(workout: Workout, storage: ReturnType<typeof getStorage>
};
}

export function createStartCommand(): Command {
export function createStartCommand(getProfile: () => string | undefined): Command {
return new Command('start')
.description('Start a new workout session')
.argument('[template]', 'Template ID to use')
.option('--empty', 'Start an empty freestyle session')
.option('--continue', 'Resume an interrupted session')
.action((templateId: string | undefined, options: { empty?: boolean; continue?: boolean }) => {
const storage = getStorage();
const storage = getStorage(getProfile());

if (options.continue) {
const current = storage.getCurrentWorkout();
Expand Down Expand Up @@ -101,15 +101,15 @@ export function createStartCommand(): Command {
});
}

export function createLogCommand(): Command {
export function createLogCommand(getProfile: () => string | undefined): Command {
return new Command('log')
.description('Log a set')
.argument('<exercise>', 'Exercise ID')
.argument('<weight>', 'Weight (number or +/- for relative)')
.argument('<reps>', 'Reps (single number or comma-separated for multiple sets)')
.option('--rir <rir>', 'Reps in reserve (0-10)')
.action((exerciseId: string, weightStr: string, repsStr: string, options: { rir?: string }) => {
const storage = getStorage();
const storage = getStorage(getProfile());
const workout = storage.getCurrentWorkout();

if (!workout) {
Expand Down Expand Up @@ -168,9 +168,9 @@ export function createLogCommand(): Command {
});
}

export function createStatusCommand(): Command {
export function createStatusCommand(getProfile: () => string | undefined): Command {
return new Command('status').description('Show current workout status').action(() => {
const storage = getStorage();
const storage = getStorage(getProfile());
const workout = storage.getCurrentWorkout();

if (!workout) {
Expand Down Expand Up @@ -218,9 +218,9 @@ export function createStatusCommand(): Command {
});
}

export function createDoneCommand(): Command {
export function createDoneCommand(getProfile: () => string | undefined): Command {
return new Command('done').description('Finish current workout').action(() => {
const storage = getStorage();
const storage = getStorage(getProfile());
const workout = storage.getCurrentWorkout();

if (!workout) {
Expand All @@ -247,9 +247,9 @@ export function createDoneCommand(): Command {
});
}

export function createCancelCommand(): Command {
export function createCancelCommand(getProfile: () => string | undefined): Command {
return new Command('cancel').description('Cancel current workout without saving').action(() => {
const storage = getStorage();
const storage = getStorage(getProfile());
const workout = storage.getCurrentWorkout();

if (!workout) {
Expand All @@ -262,12 +262,12 @@ export function createCancelCommand(): Command {
});
}

export function createNoteCommand(): Command {
export function createNoteCommand(getProfile: () => string | undefined): Command {
return new Command('note')
.description('Add a note to the current workout')
.argument('<text...>', 'Note text (or exercise ID followed by note text)')
.action((textParts: string[]) => {
const storage = getStorage();
const storage = getStorage(getProfile());
const workout = storage.getCurrentWorkout();

if (!workout) {
Expand Down Expand Up @@ -297,13 +297,13 @@ export function createNoteCommand(): Command {
});
}

export function createSwapCommand(): Command {
export function createSwapCommand(getProfile: () => string | undefined): Command {
return new Command('swap')
.description('Swap an exercise in the current workout with another')
.argument('<old-exercise>', 'Exercise ID to replace')
.argument('<new-exercise>', 'Exercise ID to swap in')
.action((oldExerciseId: string, newExerciseId: string) => {
const storage = getStorage();
const storage = getStorage(getProfile());
const workout = storage.getCurrentWorkout();

if (!workout) {
Expand Down Expand Up @@ -347,12 +347,12 @@ export function createSwapCommand(): Command {
});
}

export function createAddCommand(): Command {
export function createAddCommand(getProfile: () => string | undefined): Command {
return new Command('add')
.description('Add an exercise to the current workout')
.argument('<exercise>', 'Exercise ID to add')
.action((exerciseId: string) => {
const storage = getStorage();
const storage = getStorage(getProfile());
const workout = storage.getCurrentWorkout();

if (!workout) {
Expand Down
Loading