Skip to content

Commit 43c45c0

Browse files
authored
feat: add multi-user profile support (#3)
* feat: add multi-user profile support Add profile-based multi-user support so multiple people can track their workouts independently on the same machine. - Add profile management module (src/data/profiles.ts) with CRUD operations - Update Storage class to be profile-aware with per-user data isolation - Add global --profile flag for explicit profile selection - Add profile commands: list, create, delete - Exercises are shared across profiles, templates/workouts/config are per-user - Auto-migrate existing data to 'default' profile on first use - Single profile works automatically (backwards compatible) - Multiple profiles require explicit --profile flag * refactor: simplify profile implementation - Replace imperative loop with declarative some() in hasLegacyData() - Remove redundant fs.existsSync checks in ensureDir() (mkdirSync recursive is idempotent) - Extract requireProfileDir() helper to consolidate duplicate checks - Remove redundant null check in updateExercise() - Remove redundant profileExists check in delete command
1 parent 70ae50f commit 43c45c0

File tree

14 files changed

+649
-182
lines changed

14 files changed

+649
-182
lines changed

README.md

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,52 @@ workout log overhead-press 95 10,9,8
3434
workout done
3535
```
3636

37+
## Multi-User Profiles
38+
39+
Multiple people can track workouts independently on the same machine using profiles.
40+
41+
### How it Works
42+
43+
- **Single profile**: Commands work automatically (backwards compatible)
44+
- **Multiple profiles**: Use `--profile <name>` to specify which profile
45+
- **Shared exercises**: The exercise library is shared across all profiles
46+
- **Per-user data**: Templates, workouts, config, and current session are per-profile
47+
48+
### Profile Commands
49+
50+
```bash
51+
# List all profiles
52+
workout profile list
53+
54+
# Create a new profile
55+
workout profile create sarah
56+
57+
# Delete a profile (cannot delete the last one)
58+
workout profile delete old-profile
59+
```
60+
61+
### Using Profiles
62+
63+
```bash
64+
# When multiple profiles exist, specify which one to use
65+
workout --profile mike start push-day
66+
workout --profile mike log bench-press 185 8,8,7,6
67+
workout --profile mike done
68+
69+
# Sarah can workout simultaneously
70+
workout --profile sarah start leg-day
71+
workout --profile sarah log squat 135 8,8,8
72+
workout --profile sarah done
73+
74+
# Check each person's status
75+
workout --profile mike status
76+
workout --profile sarah status
77+
```
78+
79+
### Migration
80+
81+
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.
82+
3783
## Commands
3884

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

378424
```
379425
~/.workout/
380-
config.json # User preferences
381-
exercises.json # Custom exercises
382-
templates.json # Workout templates
383-
history/ # Completed workouts
384-
current.json # Active workout (if any)
426+
├── exercises.json # Shared exercise library
427+
├── profiles/
428+
│ ├── default/ # Default profile (or your profile name)
429+
│ │ ├── config.json # User preferences (units, etc.)
430+
│ │ ├── templates.json
431+
│ │ ├── current.json # Active workout (if any)
432+
│ │ └── workouts/ # Completed workouts
433+
│ └── sarah/ # Another profile
434+
│ ├── config.json
435+
│ ├── templates.json
436+
│ ├── current.json
437+
│ └── workouts/
385438
```
386439

387440
## JSON Output

src/commands/analytics.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ function findPRs(storage: ReturnType<typeof getStorage>): Map<string, PR> {
4747
return prs;
4848
}
4949

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

126-
export function createVolumeCommand(): Command {
126+
export function createVolumeCommand(getProfile: () => string | undefined): Command {
127127
return new Command('volume')
128128
.description('Analyze training volume')
129129
.option('-w, --week', 'Show current week')
@@ -139,7 +139,7 @@ export function createVolumeCommand(): Command {
139139
by?: string;
140140
json?: boolean;
141141
}) => {
142-
const storage = getStorage();
142+
const storage = getStorage(getProfile());
143143
const config = storage.getConfig();
144144
const unit = config.units;
145145
const workouts = storage.getAllWorkouts();
@@ -268,14 +268,14 @@ export function createVolumeCommand(): Command {
268268
);
269269
}
270270

271-
export function createProgressionCommand(): Command {
271+
export function createProgressionCommand(getProfile: () => string | undefined): Command {
272272
return new Command('progression')
273273
.description('Show progression over time for an exercise')
274274
.argument('<exercise>', 'Exercise ID')
275275
.option('-n, --last <count>', 'Show last N sessions', '10')
276276
.option('--json', 'Output as JSON')
277277
.action((exerciseId: string, options: { last: string; json?: boolean }) => {
278-
const storage = getStorage();
278+
const storage = getStorage(getProfile());
279279
const config = storage.getConfig();
280280
const unit = config.units;
281281
const exercise = storage.getExercise(exerciseId);

src/commands/exercises.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Command } from 'commander';
2-
import { getStorage } from '../data/storage.js';
2+
import { getSharedStorage } from '../data/storage.js';
33
import {
44
Exercise,
55
slugify,
@@ -8,7 +8,7 @@ import {
88
type Equipment,
99
} from '../types.js';
1010

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

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

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

5757
if (!exercise) {
@@ -98,7 +98,7 @@ export function createExercisesCommand(): Command {
9898
notes?: string;
9999
}
100100
) => {
101-
const storage = getStorage();
101+
const storage = getSharedStorage();
102102
const id = options.id ?? slugify(name);
103103
const muscles = options.muscles.split(',').map((m) => m.trim()) as MuscleGroup[];
104104
const aliases = options.aliases ? options.aliases.split(',').map((a) => a.trim()) : [];
@@ -146,7 +146,7 @@ export function createExercisesCommand(): Command {
146146
notes?: string;
147147
}
148148
) => {
149-
const storage = getStorage();
149+
const storage = getSharedStorage();
150150
const exercise = storage.getExercise(id);
151151

152152
if (!exercise) {
@@ -192,7 +192,7 @@ export function createExercisesCommand(): Command {
192192
.command('delete <id>')
193193
.description('Delete an exercise')
194194
.action((id: string) => {
195-
const storage = getStorage();
195+
const storage = getSharedStorage();
196196

197197
try {
198198
storage.deleteExercise(id);

src/commands/history.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Command } from 'commander';
22
import { getStorage } from '../data/storage.js';
33

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

1313
if (!workout) {
@@ -82,14 +82,14 @@ export function createLastCommand(): Command {
8282
});
8383
}
8484

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

9595
if (!exercise) {

src/commands/profile.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Command } from 'commander';
2+
import { getProfiles, createProfile, deleteProfile } from '../data/profiles.js';
3+
4+
export function createProfileCommand(): Command {
5+
const profile = new Command('profile').description('Manage user profiles');
6+
7+
profile
8+
.command('list')
9+
.description('List all profiles')
10+
.action(() => {
11+
const profiles = getProfiles();
12+
13+
if (profiles.length === 0) {
14+
console.log('No profiles found. A default profile will be created on first use.');
15+
return;
16+
}
17+
18+
console.log('Profiles:');
19+
for (const p of profiles) {
20+
console.log(` ${p}`);
21+
}
22+
});
23+
24+
profile
25+
.command('create <name>')
26+
.description('Create a new profile')
27+
.action((name: string) => {
28+
try {
29+
createProfile(name);
30+
console.log(`Created profile: ${name}`);
31+
} catch (err) {
32+
console.error((err as Error).message);
33+
process.exit(1);
34+
}
35+
});
36+
37+
profile
38+
.command('delete <name>')
39+
.description('Delete a profile')
40+
.action((name: string) => {
41+
try {
42+
deleteProfile(name);
43+
console.log(`Deleted profile: ${name}`);
44+
} catch (err) {
45+
console.error((err as Error).message);
46+
process.exit(1);
47+
}
48+
});
49+
50+
return profile;
51+
}

src/commands/session.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ function calculateStats(workout: Workout, storage: ReturnType<typeof getStorage>
3636
};
3737
}
3838

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

4848
if (options.continue) {
4949
const current = storage.getCurrentWorkout();
@@ -101,15 +101,15 @@ export function createStartCommand(): Command {
101101
});
102102
}
103103

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

115115
if (!workout) {
@@ -168,9 +168,9 @@ export function createLogCommand(): Command {
168168
});
169169
}
170170

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

176176
if (!workout) {
@@ -218,9 +218,9 @@ export function createStatusCommand(): Command {
218218
});
219219
}
220220

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

226226
if (!workout) {
@@ -247,9 +247,9 @@ export function createDoneCommand(): Command {
247247
});
248248
}
249249

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

255255
if (!workout) {
@@ -262,12 +262,12 @@ export function createCancelCommand(): Command {
262262
});
263263
}
264264

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

273273
if (!workout) {
@@ -297,13 +297,13 @@ export function createNoteCommand(): Command {
297297
});
298298
}
299299

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

309309
if (!workout) {
@@ -347,12 +347,12 @@ export function createSwapCommand(): Command {
347347
});
348348
}
349349

350-
export function createAddCommand(): Command {
350+
export function createAddCommand(getProfile: () => string | undefined): Command {
351351
return new Command('add')
352352
.description('Add an exercise to the current workout')
353353
.argument('<exercise>', 'Exercise ID to add')
354354
.action((exerciseId: string) => {
355-
const storage = getStorage();
355+
const storage = getStorage(getProfile());
356356
const workout = storage.getCurrentWorkout();
357357

358358
if (!workout) {

0 commit comments

Comments
 (0)