@nickchristensen/cliftin 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +206 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +5 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/commands/exercises/list.d.ts +13 -0
  7. package/dist/commands/exercises/list.js +54 -0
  8. package/dist/commands/exercises/show.d.ts +21 -0
  9. package/dist/commands/exercises/show.js +140 -0
  10. package/dist/commands/programs/list.d.ts +6 -0
  11. package/dist/commands/programs/list.js +27 -0
  12. package/dist/commands/programs/show.d.ts +13 -0
  13. package/dist/commands/programs/show.js +71 -0
  14. package/dist/commands/workouts/list.d.ts +15 -0
  15. package/dist/commands/workouts/list.js +72 -0
  16. package/dist/commands/workouts/show.d.ts +9 -0
  17. package/dist/commands/workouts/show.js +72 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1 -0
  20. package/dist/lib/config.d.ts +1 -0
  21. package/dist/lib/config.js +24 -0
  22. package/dist/lib/db.d.ts +104 -0
  23. package/dist/lib/db.js +15 -0
  24. package/dist/lib/json-error.d.ts +5 -0
  25. package/dist/lib/json-error.js +6 -0
  26. package/dist/lib/json-weight.d.ts +7 -0
  27. package/dist/lib/json-weight.js +42 -0
  28. package/dist/lib/names.d.ts +5 -0
  29. package/dist/lib/names.js +70 -0
  30. package/dist/lib/output.d.ts +7 -0
  31. package/dist/lib/output.js +73 -0
  32. package/dist/lib/repositories/exercises.d.ts +30 -0
  33. package/dist/lib/repositories/exercises.js +280 -0
  34. package/dist/lib/repositories/programs.d.ts +6 -0
  35. package/dist/lib/repositories/programs.js +217 -0
  36. package/dist/lib/repositories/selectors.d.ts +3 -0
  37. package/dist/lib/repositories/selectors.js +35 -0
  38. package/dist/lib/repositories/workouts.d.ts +13 -0
  39. package/dist/lib/repositories/workouts.js +128 -0
  40. package/dist/lib/rpe.d.ts +2 -0
  41. package/dist/lib/rpe.js +4 -0
  42. package/dist/lib/time.d.ts +10 -0
  43. package/dist/lib/time.js +49 -0
  44. package/dist/lib/types.d.ts +123 -0
  45. package/dist/lib/types.js +1 -0
  46. package/dist/lib/units.d.ts +16 -0
  47. package/dist/lib/units.js +81 -0
  48. package/oclif.manifest.json +386 -0
  49. package/package.json +93 -0
package/README.md ADDED
@@ -0,0 +1,206 @@
1
+ cliftin
2
+ =================
3
+
4
+ CLIftin: A read-only CLI for Liftin'
5
+
6
+
7
+ [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io)
8
+ [![Version](https://img.shields.io/npm/v/cliftin.svg)](https://npmjs.org/package/cliftin)
9
+ [![Downloads/week](https://img.shields.io/npm/dw/cliftin.svg)](https://npmjs.org/package/cliftin)
10
+
11
+
12
+ <!-- toc -->
13
+ * [Usage](#usage)
14
+ * [Commands](#commands)
15
+ <!-- tocstop -->
16
+ # Usage
17
+ <!-- usage -->
18
+ ```sh-session
19
+ $ npm install -g @nickchristensen/cliftin
20
+ $ cliftin COMMAND
21
+ running command...
22
+ $ cliftin (--version)
23
+ @nickchristensen/cliftin/1.0.2 darwin-arm64 node-v25.6.0
24
+ $ cliftin --help [COMMAND]
25
+ USAGE
26
+ $ cliftin COMMAND
27
+ ...
28
+ ```
29
+ <!-- usagestop -->
30
+ # Commands
31
+ <!-- commands -->
32
+ * [`cliftin exercises list`](#cliftin-exercises-list)
33
+ * [`cliftin exercises show SELECTOR`](#cliftin-exercises-show-selector)
34
+ * [`cliftin help [COMMAND]`](#cliftin-help-command)
35
+ * [`cliftin programs list`](#cliftin-programs-list)
36
+ * [`cliftin programs show [SELECTOR]`](#cliftin-programs-show-selector)
37
+ * [`cliftin workouts list`](#cliftin-workouts-list)
38
+ * [`cliftin workouts show WORKOUTID`](#cliftin-workouts-show-workoutid)
39
+
40
+ ## `cliftin exercises list`
41
+
42
+ List exercises
43
+
44
+ ```
45
+ USAGE
46
+ $ cliftin exercises list [--json] [--equipment <value>] [--muscle <value>] [--name <value>] [--sort
47
+ name|lastPerformed|timesPerformed]
48
+
49
+ FLAGS
50
+ --equipment=<value> Filter by equipment name
51
+ --muscle=<value> Filter by muscle group
52
+ --name=<value> Filter by name contains
53
+ --sort=<option> [default: name]
54
+ <options: name|lastPerformed|timesPerformed>
55
+
56
+ GLOBAL FLAGS
57
+ --json Format output as json.
58
+
59
+ DESCRIPTION
60
+ List exercises
61
+ ```
62
+
63
+ _See code: [src/commands/exercises/list.ts](https://github.com/nickchristensen/cliftin/blob/v1.0.2/src/commands/exercises/list.ts)_
64
+
65
+ ## `cliftin exercises show SELECTOR`
66
+
67
+ Show one exercise detail and history
68
+
69
+ ```
70
+ USAGE
71
+ $ cliftin exercises show SELECTOR [--json] [--all | --limit <value>] [--from <value>] [--max-reps <value>]
72
+ [--max-weight <value>] [--min-reps <value>] [--min-weight <value>] [--program <value>] [--routine <value>] [--to
73
+ <value>]
74
+
75
+ ARGUMENTS
76
+ SELECTOR exercise id or name
77
+
78
+ FLAGS
79
+ --all Return all matching history rows (no limit)
80
+ --from=<value> History start date YYYY-MM-DD
81
+ --limit=<value> History row limit (default: 100)
82
+ --max-reps=<value> History max top reps
83
+ --max-weight=<value> History max top weight
84
+ --min-reps=<value> History min top reps
85
+ --min-weight=<value> History min top weight
86
+ --program=<value> History filter by program id or name
87
+ --routine=<value> History filter by routine id or name
88
+ --to=<value> History end date YYYY-MM-DD
89
+
90
+ GLOBAL FLAGS
91
+ --json Format output as json.
92
+
93
+ DESCRIPTION
94
+ Show one exercise detail and history
95
+ ```
96
+
97
+ _See code: [src/commands/exercises/show.ts](https://github.com/nickchristensen/cliftin/blob/v1.0.2/src/commands/exercises/show.ts)_
98
+
99
+ ## `cliftin help [COMMAND]`
100
+
101
+ Display help for cliftin.
102
+
103
+ ```
104
+ USAGE
105
+ $ cliftin help [COMMAND...] [-n]
106
+
107
+ ARGUMENTS
108
+ [COMMAND...] Command to show help for.
109
+
110
+ FLAGS
111
+ -n, --nested-commands Include all nested commands in the output.
112
+
113
+ DESCRIPTION
114
+ Display help for cliftin.
115
+ ```
116
+
117
+ _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.37/src/commands/help.ts)_
118
+
119
+ ## `cliftin programs list`
120
+
121
+ List programs
122
+
123
+ ```
124
+ USAGE
125
+ $ cliftin programs list [--json]
126
+
127
+ GLOBAL FLAGS
128
+ --json Format output as json.
129
+
130
+ DESCRIPTION
131
+ List programs
132
+ ```
133
+
134
+ _See code: [src/commands/programs/list.ts](https://github.com/nickchristensen/cliftin/blob/v1.0.2/src/commands/programs/list.ts)_
135
+
136
+ ## `cliftin programs show [SELECTOR]`
137
+
138
+ Show one program hierarchy
139
+
140
+ ```
141
+ USAGE
142
+ $ cliftin programs show [SELECTOR] [--json] [--active] [--current]
143
+
144
+ ARGUMENTS
145
+ [SELECTOR] program id or name
146
+
147
+ FLAGS
148
+ --active Show active program detail
149
+ --current Alias for --active
150
+
151
+ GLOBAL FLAGS
152
+ --json Format output as json.
153
+
154
+ DESCRIPTION
155
+ Show one program hierarchy
156
+ ```
157
+
158
+ _See code: [src/commands/programs/show.ts](https://github.com/nickchristensen/cliftin/blob/v1.0.2/src/commands/programs/show.ts)_
159
+
160
+ ## `cliftin workouts list`
161
+
162
+ List workouts
163
+
164
+ ```
165
+ USAGE
166
+ $ cliftin workouts list [--json] [--limit <value> | --all] [--on <value> | --from <value> | --to <value>]
167
+ [--program <value>] [--routine <value>]
168
+
169
+ FLAGS
170
+ --all Return all matching workouts (no limit)
171
+ --from=<value> Start date YYYY-MM-DD
172
+ --limit=<value> Limit workouts (default: 25)
173
+ --on=<value> Single date YYYY-MM-DD
174
+ --program=<value> Filter by program id or name
175
+ --routine=<value> Filter by routine id or name
176
+ --to=<value> End date YYYY-MM-DD
177
+
178
+ GLOBAL FLAGS
179
+ --json Format output as json.
180
+
181
+ DESCRIPTION
182
+ List workouts
183
+ ```
184
+
185
+ _See code: [src/commands/workouts/list.ts](https://github.com/nickchristensen/cliftin/blob/v1.0.2/src/commands/workouts/list.ts)_
186
+
187
+ ## `cliftin workouts show WORKOUTID`
188
+
189
+ Show one workout with exercises and sets
190
+
191
+ ```
192
+ USAGE
193
+ $ cliftin workouts show WORKOUTID [--json]
194
+
195
+ ARGUMENTS
196
+ WORKOUTID workout id
197
+
198
+ GLOBAL FLAGS
199
+ --json Format output as json.
200
+
201
+ DESCRIPTION
202
+ Show one workout with exercises and sets
203
+ ```
204
+
205
+ _See code: [src/commands/workouts/show.ts](https://github.com/nickchristensen/cliftin/blob/v1.0.2/src/commands/workouts/show.ts)_
206
+ <!-- commandsstop -->
package/bin/dev.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %*
package/bin/dev.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning
2
+
3
+ import {execute} from '@oclif/core'
4
+
5
+ await execute({development: true, dir: import.meta.url})
package/bin/run.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node "%~dp0\run" %*
package/bin/run.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {execute} from '@oclif/core'
4
+
5
+ await execute({dir: import.meta.url})
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Exercises extends Command {
3
+ static description: string;
4
+ static enableJsonFlag: boolean;
5
+ static sortOptions: readonly ["name", "lastPerformed", "timesPerformed"];
6
+ static flags: {
7
+ equipment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ muscle: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ sort: import("@oclif/core/interfaces").OptionFlag<"name" | "lastPerformed" | "timesPerformed", import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<unknown | void>;
13
+ }
@@ -0,0 +1,54 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { closeDb, openDb } from '../../lib/db.js';
3
+ import { renderTable } from '../../lib/output.js';
4
+ import { listExercises } from '../../lib/repositories/exercises.js';
5
+ function formatMusclesCell(primaryMuscles, secondaryMuscles) {
6
+ const primary = primaryMuscles ?? 'n/a';
7
+ if (!secondaryMuscles)
8
+ return primary;
9
+ const secondaryLines = secondaryMuscles
10
+ .split(',')
11
+ .map((part) => part.trim())
12
+ .filter((part) => part.length > 0)
13
+ .join('\n');
14
+ return `${primary}\n${secondaryLines}`;
15
+ }
16
+ export default class Exercises extends Command {
17
+ static description = 'List exercises';
18
+ static enableJsonFlag = true;
19
+ static sortOptions = ['name', 'lastPerformed', 'timesPerformed'];
20
+ static flags = {
21
+ equipment: Flags.string({ description: 'Filter by equipment name' }),
22
+ muscle: Flags.string({ description: 'Filter by muscle group' }),
23
+ name: Flags.string({ description: 'Filter by name contains' }),
24
+ sort: Flags.option({
25
+ default: 'name',
26
+ options: Exercises.sortOptions,
27
+ })(),
28
+ };
29
+ async run() {
30
+ const { flags } = await this.parse(Exercises);
31
+ const context = openDb();
32
+ try {
33
+ const exercises = await listExercises(context.db, {
34
+ equipment: flags.equipment,
35
+ muscle: flags.muscle,
36
+ name: flags.name,
37
+ sort: flags.sort,
38
+ });
39
+ if (this.jsonEnabled())
40
+ return exercises;
41
+ this.log(renderTable(exercises.map((exercise) => ({
42
+ equipment: exercise.equipment,
43
+ id: exercise.id,
44
+ lastPerformed: exercise.lastPerformed,
45
+ muscles: formatMusclesCell(exercise.primaryMuscles, exercise.secondaryMuscles),
46
+ name: exercise.name,
47
+ timesPerformed: exercise.timesPerformed,
48
+ }))));
49
+ }
50
+ finally {
51
+ await closeDb(context);
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,21 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class ExercisesShow extends Command {
3
+ static args: {
4
+ selector: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static enableJsonFlag: boolean;
8
+ static flags: {
9
+ all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ from: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ limit: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'max-reps': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'max-weight': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'min-reps': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ 'min-weight': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ program: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ routine: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ to: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ };
20
+ run(): Promise<unknown | void>;
21
+ }
@@ -0,0 +1,140 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { format, isValid, parseISO } from 'date-fns';
3
+ import { closeDb, openDb } from '../../lib/db.js';
4
+ import { toJsonErrorPayload } from '../../lib/json-error.js';
5
+ import { renderTable } from '../../lib/output.js';
6
+ import { getExerciseDetail, getExerciseHistoryRows, getExerciseHistoryWithSetsRows, getLastPerformedExerciseSnapshot, resolveExerciseSelector, } from '../../lib/repositories/exercises.js';
7
+ import { resolveExerciseWeightUnit, resolveGlobalWeightUnit, weightUnitLabel, withWeightUnit } from '../../lib/units.js';
8
+ function formatWorkoutDate(dateIso) {
9
+ if (!dateIso)
10
+ return 'n/a';
11
+ const parsed = parseISO(dateIso);
12
+ if (!isValid(parsed))
13
+ return dateIso;
14
+ return format(parsed, 'yyyy-MM-dd HH:mm');
15
+ }
16
+ function toHistoryFilters(flags) {
17
+ return {
18
+ from: flags.from,
19
+ limit: flags.all ? undefined : (flags.limit ?? 100),
20
+ maxReps: flags['max-reps'],
21
+ maxWeight: flags['max-weight'],
22
+ minReps: flags['min-reps'],
23
+ minWeight: flags['min-weight'],
24
+ program: flags.program,
25
+ routine: flags.routine,
26
+ to: flags.to,
27
+ };
28
+ }
29
+ export default class ExercisesShow extends Command {
30
+ static args = {
31
+ selector: Args.string({ description: 'exercise id or name', required: true }),
32
+ };
33
+ static description = 'Show one exercise detail and history';
34
+ static enableJsonFlag = true;
35
+ static flags = {
36
+ all: Flags.boolean({ description: 'Return all matching history rows (no limit)', exclusive: ['limit'] }),
37
+ from: Flags.string({ description: 'History start date YYYY-MM-DD' }),
38
+ limit: Flags.integer({ description: 'History row limit (default: 100)', exclusive: ['all'] }),
39
+ 'max-reps': Flags.integer({ description: 'History max top reps' }),
40
+ 'max-weight': Flags.integer({ description: 'History max top weight' }),
41
+ 'min-reps': Flags.integer({ description: 'History min top reps' }),
42
+ 'min-weight': Flags.integer({ description: 'History min top weight' }),
43
+ program: Flags.string({ description: 'History filter by program id or name' }),
44
+ routine: Flags.string({ description: 'History filter by routine id or name' }),
45
+ to: Flags.string({ description: 'History end date YYYY-MM-DD' }),
46
+ };
47
+ async run() {
48
+ const { args, flags } = await this.parse(ExercisesShow);
49
+ const parsedFlags = flags;
50
+ const context = openDb();
51
+ try {
52
+ const exerciseId = await resolveExerciseSelector(context.db, args.selector);
53
+ const detail = await getExerciseDetail(context.db, exerciseId);
54
+ const historyFilters = toHistoryFilters(parsedFlags);
55
+ const historyRows = await getExerciseHistoryRows(context.db, exerciseId, historyFilters);
56
+ const lastPerformedSnapshot = await getLastPerformedExerciseSnapshot(context.db, detail.id);
57
+ if (this.jsonEnabled()) {
58
+ const historyUnitPreference = await resolveExerciseWeightUnit(context.db, exerciseId);
59
+ const history = (await getExerciseHistoryWithSetsRows(context.db, exerciseId, historyFilters)).map((row) => ({
60
+ ...row,
61
+ sets: row.sets.map((set) => ({
62
+ ...set,
63
+ weight: withWeightUnit(set.weight, historyUnitPreference),
64
+ })),
65
+ topWeight: withWeightUnit(row.topWeight, historyUnitPreference),
66
+ }));
67
+ return {
68
+ defaultProgressMetric: detail.defaultProgressMetric,
69
+ equipment: detail.equipment,
70
+ history,
71
+ id: detail.id,
72
+ name: detail.name,
73
+ perceptionScale: detail.perceptionScale,
74
+ primaryMuscles: detail.primaryMuscles,
75
+ recentRoutines: detail.recentRoutines,
76
+ secondaryMuscles: detail.secondaryMuscles,
77
+ supports1RM: detail.supports1RM,
78
+ timerBased: detail.timerBased,
79
+ totalRoutines: detail.totalRoutines,
80
+ totalWorkouts: detail.totalWorkouts,
81
+ };
82
+ }
83
+ this.log(`[${detail.id}] ${detail.name ?? '(unnamed)'}`);
84
+ this.log(`Primary muscles: ${detail.primaryMuscles ?? 'n/a'}`);
85
+ this.log(`Secondary muscles: ${detail.secondaryMuscles ?? 'n/a'}`);
86
+ this.log(`Equipment: ${detail.equipment ?? 'n/a'}`);
87
+ this.log(`Timer based: ${detail.timerBased}`);
88
+ this.log(`Supports 1RM: ${detail.supports1RM}`);
89
+ this.log(`Workouts tracked: ${detail.totalWorkouts}`);
90
+ this.log(`Routines present in: ${detail.totalRoutines}`);
91
+ this.log(`Recent routines: ${detail.recentRoutines.join(', ') || 'n/a'}`);
92
+ this.log('');
93
+ this.log('Last performed');
94
+ if (!lastPerformedSnapshot) {
95
+ this.log('(no rows)');
96
+ return;
97
+ }
98
+ const unitPreference = await resolveGlobalWeightUnit(context.db);
99
+ const unitLabel = weightUnitLabel(unitPreference);
100
+ this.log(`[${lastPerformedSnapshot.workout.id}] ${lastPerformedSnapshot.workout.routine ?? 'Workout'}`);
101
+ this.log(`Program: ${lastPerformedSnapshot.workout.program ?? 'n/a'}`);
102
+ this.log(`Date: ${formatWorkoutDate(lastPerformedSnapshot.workout.date)}`);
103
+ this.log('');
104
+ this.log(renderTable(lastPerformedSnapshot.exercise.sets.map((set) => ({
105
+ id: set.id,
106
+ reps: set.reps,
107
+ rpe: set.rpe,
108
+ timeSeconds: set.timeSeconds,
109
+ volume: set.volume,
110
+ weight: set.weight === null ? null : `${set.weight} ${unitLabel}`,
111
+ }))));
112
+ this.log('');
113
+ this.log('History');
114
+ const historyUnitPreference = await resolveExerciseWeightUnit(context.db, exerciseId);
115
+ const historyUnitLabel = weightUnitLabel(historyUnitPreference);
116
+ if (historyRows.length === 0) {
117
+ this.log('(no rows)');
118
+ return;
119
+ }
120
+ this.log(renderTable(historyRows.map((row) => ({
121
+ date: row.date,
122
+ id: row.workoutId,
123
+ routine: row.routine,
124
+ sets: row.sets,
125
+ topReps: row.topReps,
126
+ topWeight: row.topWeight === null ? null : `${row.topWeight} ${historyUnitLabel}`,
127
+ totalReps: row.totalReps,
128
+ volume: row.volume,
129
+ }))));
130
+ }
131
+ catch (error) {
132
+ if (this.jsonEnabled())
133
+ return toJsonErrorPayload(error);
134
+ throw error;
135
+ }
136
+ finally {
137
+ await closeDb(context);
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Programs extends Command {
3
+ static description: string;
4
+ static enableJsonFlag: boolean;
5
+ run(): Promise<unknown | void>;
6
+ }
@@ -0,0 +1,27 @@
1
+ import { Command } from '@oclif/core';
2
+ import { closeDb, openDb } from '../../lib/db.js';
3
+ import { renderTable } from '../../lib/output.js';
4
+ import { listPrograms } from '../../lib/repositories/programs.js';
5
+ export default class Programs extends Command {
6
+ static description = 'List programs';
7
+ static enableJsonFlag = true;
8
+ async run() {
9
+ await this.parse(Programs);
10
+ const context = openDb();
11
+ try {
12
+ const programs = await listPrograms(context.db);
13
+ if (this.jsonEnabled())
14
+ return programs;
15
+ this.log(renderTable(programs.map((program) => ({
16
+ dateAdded: program.dateAdded,
17
+ id: program.id,
18
+ isActive: program.isActive,
19
+ isTemplate: program.isTemplate,
20
+ name: program.name,
21
+ }))));
22
+ }
23
+ finally {
24
+ await closeDb(context);
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class ProgramsShow extends Command {
3
+ static args: {
4
+ selector: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static enableJsonFlag: boolean;
8
+ static flags: {
9
+ active: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ current: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<unknown | void>;
13
+ }
@@ -0,0 +1,71 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { closeDb, openDb } from '../../lib/db.js';
3
+ import { serializeProgramDetailWithWeightUnits } from '../../lib/json-weight.js';
4
+ import { renderTable } from '../../lib/output.js';
5
+ import { getProgramDetail, resolveProgramSelector } from '../../lib/repositories/programs.js';
6
+ import { resolveProgramWeightUnit, weightUnitLabel } from '../../lib/units.js';
7
+ function buildProgramRows(weeks, unitLabel) {
8
+ return weeks.flatMap((week, weekIndex) => week.routines.flatMap((routine, routineIndex) => routine.exercises.flatMap((exercise, exerciseIndex) => {
9
+ const headingRow = {};
10
+ headingRow.week = routineIndex === 0 && exerciseIndex === 0 ? `Week ${weekIndex + 1}` : '';
11
+ headingRow.routine = exerciseIndex === 0 ? `[${routine.id}] ${routine.name ?? '(unnamed)'}` : '';
12
+ headingRow.exercise = `[${exercise.id ?? exercise.exerciseConfigId}] ${exercise.name ?? '(unnamed)'}`;
13
+ headingRow.reps = null;
14
+ headingRow.rpe = null;
15
+ headingRow.timeSeconds = null;
16
+ headingRow.weight = null;
17
+ const setRows = exercise.sets.map((set) => {
18
+ const row = {};
19
+ row.week = '';
20
+ row.routine = '';
21
+ row.exercise = '';
22
+ row.reps = set.reps;
23
+ row.rpe = set.rpe;
24
+ row.timeSeconds = set.timeSeconds;
25
+ row.weight = set.weight === null ? null : `${set.weight} ${unitLabel}`;
26
+ return row;
27
+ });
28
+ return [headingRow, ...setRows];
29
+ })));
30
+ }
31
+ export default class ProgramsShow extends Command {
32
+ static args = {
33
+ selector: Args.string({ description: 'program id or name', ignoreStdin: true, required: false }),
34
+ };
35
+ static description = 'Show one program hierarchy';
36
+ static enableJsonFlag = true;
37
+ static flags = {
38
+ active: Flags.boolean({ description: 'Show active program detail' }),
39
+ current: Flags.boolean({ description: 'Alias for --active' }),
40
+ };
41
+ async run() {
42
+ const { args, flags } = await this.parse(ProgramsShow);
43
+ const context = openDb();
44
+ try {
45
+ const useActive = flags.active || flags.current;
46
+ if (args.selector && useActive) {
47
+ throw new Error('Use either a selector or --active/--current, not both.');
48
+ }
49
+ if (!args.selector && !useActive) {
50
+ throw new Error('Provide a selector or use --active/--current.');
51
+ }
52
+ const programId = await resolveProgramSelector(context.db, args.selector, Boolean(useActive));
53
+ const detail = await getProgramDetail(context.db, programId);
54
+ const unitPreference = await resolveProgramWeightUnit(context.db, detail.program.id);
55
+ const unitLabel = weightUnitLabel(unitPreference);
56
+ if (this.jsonEnabled()) {
57
+ return serializeProgramDetailWithWeightUnits(detail, unitPreference);
58
+ }
59
+ this.log(`[${detail.program.id}] ${detail.program.name}`);
60
+ this.log(`Active: ${detail.program.isActive} Template: ${detail.program.isTemplate}`);
61
+ this.log('');
62
+ const programRows = buildProgramRows(detail.weeks, unitLabel);
63
+ const renderedTable = renderTable(programRows).replace(/^\n+/, '');
64
+ this.log(renderedTable);
65
+ this.log('');
66
+ }
67
+ finally {
68
+ await closeDb(context);
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,15 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Workouts extends Command {
3
+ static description: string;
4
+ static enableJsonFlag: boolean;
5
+ static flags: {
6
+ limit: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ on: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ from: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ to: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ program: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ routine: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ run(): Promise<unknown | void>;
15
+ }
@@ -0,0 +1,72 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { closeDb, openDb } from '../../lib/db.js';
3
+ import { toJsonErrorPayload } from '../../lib/json-error.js';
4
+ import { renderTable } from '../../lib/output.js';
5
+ import { listWorkouts } from '../../lib/repositories/workouts.js';
6
+ function formatDurationMinutes(durationSeconds) {
7
+ if (durationSeconds === null)
8
+ return 'n/a';
9
+ return `${Math.round(durationSeconds / 60)} minutes`;
10
+ }
11
+ export default class Workouts extends Command {
12
+ static description = 'List workouts';
13
+ static enableJsonFlag = true;
14
+ /* eslint-disable perfectionist/sort-objects */
15
+ static flags = {
16
+ limit: Flags.integer({ description: 'Limit workouts (default: 25)', exclusive: ['all'] }),
17
+ all: Flags.boolean({ description: 'Return all matching workouts (no limit)', exclusive: ['limit'] }),
18
+ on: Flags.string({ description: 'Single date YYYY-MM-DD', exclusive: ['from', 'to'] }),
19
+ from: Flags.string({ description: 'Start date YYYY-MM-DD', exclusive: ['on'] }),
20
+ to: Flags.string({ description: 'End date YYYY-MM-DD', exclusive: ['on'] }),
21
+ program: Flags.string({ description: 'Filter by program id or name' }),
22
+ routine: Flags.string({ description: 'Filter by routine id or name' }),
23
+ };
24
+ /* eslint-enable perfectionist/sort-objects */
25
+ async run() {
26
+ const { flags } = await this.parse(Workouts);
27
+ const context = openDb();
28
+ try {
29
+ const workouts = await listWorkouts(context.db, {
30
+ from: flags.from,
31
+ limit: flags.all ? undefined : (flags.limit ?? 25),
32
+ on: flags.on,
33
+ program: flags.program,
34
+ routine: flags.routine,
35
+ to: flags.to,
36
+ });
37
+ if (this.jsonEnabled()) {
38
+ return workouts.map((workout) => ({
39
+ ...workout,
40
+ duration: {
41
+ unit: 'seconds',
42
+ value: workout.duration,
43
+ },
44
+ }));
45
+ }
46
+ this.log(renderTable(workouts.map((workout) => {
47
+ const row = {
48
+ date: workout.date,
49
+ duration: formatDurationMinutes(workout.duration),
50
+ id: workout.id,
51
+ program: workout.program,
52
+ routine: workout.routine,
53
+ };
54
+ return Object.fromEntries([
55
+ ['id', row.id],
56
+ ['program', row.program],
57
+ ['routine', row.routine],
58
+ ['date', row.date],
59
+ ['duration', row.duration],
60
+ ]);
61
+ })));
62
+ }
63
+ catch (error) {
64
+ if (this.jsonEnabled())
65
+ return toJsonErrorPayload(error);
66
+ throw error;
67
+ }
68
+ finally {
69
+ await closeDb(context);
70
+ }
71
+ }
72
+ }