@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.
- package/README.md +206 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/exercises/list.d.ts +13 -0
- package/dist/commands/exercises/list.js +54 -0
- package/dist/commands/exercises/show.d.ts +21 -0
- package/dist/commands/exercises/show.js +140 -0
- package/dist/commands/programs/list.d.ts +6 -0
- package/dist/commands/programs/list.js +27 -0
- package/dist/commands/programs/show.d.ts +13 -0
- package/dist/commands/programs/show.js +71 -0
- package/dist/commands/workouts/list.d.ts +15 -0
- package/dist/commands/workouts/list.js +72 -0
- package/dist/commands/workouts/show.d.ts +9 -0
- package/dist/commands/workouts/show.js +72 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.js +24 -0
- package/dist/lib/db.d.ts +104 -0
- package/dist/lib/db.js +15 -0
- package/dist/lib/json-error.d.ts +5 -0
- package/dist/lib/json-error.js +6 -0
- package/dist/lib/json-weight.d.ts +7 -0
- package/dist/lib/json-weight.js +42 -0
- package/dist/lib/names.d.ts +5 -0
- package/dist/lib/names.js +70 -0
- package/dist/lib/output.d.ts +7 -0
- package/dist/lib/output.js +73 -0
- package/dist/lib/repositories/exercises.d.ts +30 -0
- package/dist/lib/repositories/exercises.js +280 -0
- package/dist/lib/repositories/programs.d.ts +6 -0
- package/dist/lib/repositories/programs.js +217 -0
- package/dist/lib/repositories/selectors.d.ts +3 -0
- package/dist/lib/repositories/selectors.js +35 -0
- package/dist/lib/repositories/workouts.d.ts +13 -0
- package/dist/lib/repositories/workouts.js +128 -0
- package/dist/lib/rpe.d.ts +2 -0
- package/dist/lib/rpe.js +4 -0
- package/dist/lib/time.d.ts +10 -0
- package/dist/lib/time.js +49 -0
- package/dist/lib/types.d.ts +123 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/units.d.ts +16 -0
- package/dist/lib/units.js +81 -0
- package/oclif.manifest.json +386 -0
- 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
|
+
[](https://oclif.io)
|
|
8
|
+
[](https://npmjs.org/package/cliftin)
|
|
9
|
+
[](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
package/bin/dev.js
ADDED
package/bin/run.cmd
ADDED
package/bin/run.js
ADDED
|
@@ -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,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
|
+
}
|