@nickchristensen/cliftin 3.0.0 → 4.1.0
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 +117 -12
- package/dist/commands/exercises/show.d.ts +1 -0
- package/dist/commands/exercises/show.js +5 -2
- package/dist/commands/routines/from-workout.d.ts +9 -0
- package/dist/commands/routines/from-workout.js +51 -0
- package/dist/commands/routines/latest.d.ts +6 -0
- package/dist/commands/routines/latest.js +39 -0
- package/dist/commands/routines/list.d.ts +11 -0
- package/dist/commands/routines/list.js +40 -0
- package/dist/commands/routines/next.d.ts +6 -0
- package/dist/commands/routines/next.js +37 -0
- package/dist/commands/routines/show.d.ts +9 -0
- package/dist/commands/routines/show.js +34 -0
- package/dist/commands/workouts/next.js +8 -36
- package/dist/commands/workouts/show.d.ts +3 -0
- package/dist/commands/workouts/show.js +17 -3
- package/dist/lib/json-weight.d.ts +2 -2
- package/dist/lib/json-weight.js +15 -2
- package/dist/lib/repositories/routines.d.ts +13 -0
- package/dist/lib/repositories/routines.js +171 -0
- package/dist/lib/repositories/workouts.js +1 -0
- package/dist/lib/routine-output.d.ts +2 -0
- package/dist/lib/routine-output.js +19 -0
- package/dist/lib/types.d.ts +18 -8
- package/oclif.manifest.json +192 -2
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -46,6 +46,11 @@ USAGE
|
|
|
46
46
|
* [`cliftin help [COMMAND]`](#cliftin-help-command)
|
|
47
47
|
* [`cliftin programs list`](#cliftin-programs-list)
|
|
48
48
|
* [`cliftin programs show [SELECTOR]`](#cliftin-programs-show-selector)
|
|
49
|
+
* [`cliftin routines from-workout [WORKOUTID]`](#cliftin-routines-from-workout-workoutid)
|
|
50
|
+
* [`cliftin routines latest`](#cliftin-routines-latest)
|
|
51
|
+
* [`cliftin routines list`](#cliftin-routines-list)
|
|
52
|
+
* [`cliftin routines next`](#cliftin-routines-next)
|
|
53
|
+
* [`cliftin routines show SELECTOR`](#cliftin-routines-show-selector)
|
|
49
54
|
* [`cliftin workouts list`](#cliftin-workouts-list)
|
|
50
55
|
* [`cliftin workouts next`](#cliftin-workouts-next)
|
|
51
56
|
* [`cliftin workouts show [WORKOUTID]`](#cliftin-workouts-show-workoutid)
|
|
@@ -73,7 +78,7 @@ DESCRIPTION
|
|
|
73
78
|
List exercises
|
|
74
79
|
```
|
|
75
80
|
|
|
76
|
-
_See code: [src/commands/exercises/list.ts](https://github.com/nickchristensen/cliftin/blob/
|
|
81
|
+
_See code: [src/commands/exercises/list.ts](https://github.com/nickchristensen/cliftin/blob/v4.1.0/src/commands/exercises/list.ts)_
|
|
77
82
|
|
|
78
83
|
## `cliftin exercises show SELECTOR`
|
|
79
84
|
|
|
@@ -82,8 +87,8 @@ Show one exercise detail and history
|
|
|
82
87
|
```
|
|
83
88
|
USAGE
|
|
84
89
|
$ cliftin exercises show SELECTOR [--json] [--all | --limit <value>] [--from <value>] [--max-reps <value>]
|
|
85
|
-
[--max-weight <value>] [--min-reps <value>] [--min-weight <value>] [--
|
|
86
|
-
<value>]
|
|
90
|
+
[--max-weight <value>] [--min-reps <value>] [--min-weight <value>] [--no-warmup] [--program <value>] [--routine
|
|
91
|
+
<value>] [--to <value>]
|
|
87
92
|
|
|
88
93
|
ARGUMENTS
|
|
89
94
|
SELECTOR exercise id or name
|
|
@@ -96,6 +101,7 @@ FLAGS
|
|
|
96
101
|
--max-weight=<value> History max top weight
|
|
97
102
|
--min-reps=<value> History min top reps
|
|
98
103
|
--min-weight=<value> History min top weight
|
|
104
|
+
--no-warmup Hide warmup sets from output
|
|
99
105
|
--program=<value> History filter by program id or name
|
|
100
106
|
--routine=<value> History filter by routine id or name
|
|
101
107
|
--to=<value> History end date YYYY-MM-DD
|
|
@@ -107,7 +113,7 @@ DESCRIPTION
|
|
|
107
113
|
Show one exercise detail and history
|
|
108
114
|
```
|
|
109
115
|
|
|
110
|
-
_See code: [src/commands/exercises/show.ts](https://github.com/nickchristensen/cliftin/blob/
|
|
116
|
+
_See code: [src/commands/exercises/show.ts](https://github.com/nickchristensen/cliftin/blob/v4.1.0/src/commands/exercises/show.ts)_
|
|
111
117
|
|
|
112
118
|
## `cliftin help [COMMAND]`
|
|
113
119
|
|
|
@@ -144,7 +150,7 @@ DESCRIPTION
|
|
|
144
150
|
List programs
|
|
145
151
|
```
|
|
146
152
|
|
|
147
|
-
_See code: [src/commands/programs/list.ts](https://github.com/nickchristensen/cliftin/blob/
|
|
153
|
+
_See code: [src/commands/programs/list.ts](https://github.com/nickchristensen/cliftin/blob/v4.1.0/src/commands/programs/list.ts)_
|
|
148
154
|
|
|
149
155
|
## `cliftin programs show [SELECTOR]`
|
|
150
156
|
|
|
@@ -164,7 +170,103 @@ DESCRIPTION
|
|
|
164
170
|
Show one program hierarchy
|
|
165
171
|
```
|
|
166
172
|
|
|
167
|
-
_See code: [src/commands/programs/show.ts](https://github.com/nickchristensen/cliftin/blob/
|
|
173
|
+
_See code: [src/commands/programs/show.ts](https://github.com/nickchristensen/cliftin/blob/v4.1.0/src/commands/programs/show.ts)_
|
|
174
|
+
|
|
175
|
+
## `cliftin routines from-workout [WORKOUTID]`
|
|
176
|
+
|
|
177
|
+
Show the planned routine for a completed workout
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
USAGE
|
|
181
|
+
$ cliftin routines from-workout [WORKOUTID] [--json]
|
|
182
|
+
|
|
183
|
+
ARGUMENTS
|
|
184
|
+
[WORKOUTID] workout id (default: latest workout)
|
|
185
|
+
|
|
186
|
+
GLOBAL FLAGS
|
|
187
|
+
--json Format output as json.
|
|
188
|
+
|
|
189
|
+
DESCRIPTION
|
|
190
|
+
Show the planned routine for a completed workout
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
_See code: [src/commands/routines/from-workout.ts](https://github.com/nickchristensen/cliftin/blob/v4.1.0/src/commands/routines/from-workout.ts)_
|
|
194
|
+
|
|
195
|
+
## `cliftin routines latest`
|
|
196
|
+
|
|
197
|
+
Show the planned routine for the latest workout
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
USAGE
|
|
201
|
+
$ cliftin routines latest [--json]
|
|
202
|
+
|
|
203
|
+
GLOBAL FLAGS
|
|
204
|
+
--json Format output as json.
|
|
205
|
+
|
|
206
|
+
DESCRIPTION
|
|
207
|
+
Show the planned routine for the latest workout
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
_See code: [src/commands/routines/latest.ts](https://github.com/nickchristensen/cliftin/blob/v4.1.0/src/commands/routines/latest.ts)_
|
|
211
|
+
|
|
212
|
+
## `cliftin routines list`
|
|
213
|
+
|
|
214
|
+
List planned routines
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
USAGE
|
|
218
|
+
$ cliftin routines list [--json] [--name <value>] [--program <value>] [--week <value>]
|
|
219
|
+
|
|
220
|
+
FLAGS
|
|
221
|
+
--name=<value> Filter by routine name contains
|
|
222
|
+
--program=<value> Filter by program id or name
|
|
223
|
+
--week=<value> Filter by week number
|
|
224
|
+
|
|
225
|
+
GLOBAL FLAGS
|
|
226
|
+
--json Format output as json.
|
|
227
|
+
|
|
228
|
+
DESCRIPTION
|
|
229
|
+
List planned routines
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
_See code: [src/commands/routines/list.ts](https://github.com/nickchristensen/cliftin/blob/v4.1.0/src/commands/routines/list.ts)_
|
|
233
|
+
|
|
234
|
+
## `cliftin routines next`
|
|
235
|
+
|
|
236
|
+
Show the up-next routine from the active program
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
USAGE
|
|
240
|
+
$ cliftin routines next [--json]
|
|
241
|
+
|
|
242
|
+
GLOBAL FLAGS
|
|
243
|
+
--json Format output as json.
|
|
244
|
+
|
|
245
|
+
DESCRIPTION
|
|
246
|
+
Show the up-next routine from the active program
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
_See code: [src/commands/routines/next.ts](https://github.com/nickchristensen/cliftin/blob/v4.1.0/src/commands/routines/next.ts)_
|
|
250
|
+
|
|
251
|
+
## `cliftin routines show SELECTOR`
|
|
252
|
+
|
|
253
|
+
Show one planned routine
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
USAGE
|
|
257
|
+
$ cliftin routines show SELECTOR [--json]
|
|
258
|
+
|
|
259
|
+
ARGUMENTS
|
|
260
|
+
SELECTOR routine id or name
|
|
261
|
+
|
|
262
|
+
GLOBAL FLAGS
|
|
263
|
+
--json Format output as json.
|
|
264
|
+
|
|
265
|
+
DESCRIPTION
|
|
266
|
+
Show one planned routine
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
_See code: [src/commands/routines/show.ts](https://github.com/nickchristensen/cliftin/blob/v4.1.0/src/commands/routines/show.ts)_
|
|
168
270
|
|
|
169
271
|
## `cliftin workouts list`
|
|
170
272
|
|
|
@@ -191,11 +293,11 @@ DESCRIPTION
|
|
|
191
293
|
List workouts
|
|
192
294
|
```
|
|
193
295
|
|
|
194
|
-
_See code: [src/commands/workouts/list.ts](https://github.com/nickchristensen/cliftin/blob/
|
|
296
|
+
_See code: [src/commands/workouts/list.ts](https://github.com/nickchristensen/cliftin/blob/v4.1.0/src/commands/workouts/list.ts)_
|
|
195
297
|
|
|
196
298
|
## `cliftin workouts next`
|
|
197
299
|
|
|
198
|
-
|
|
300
|
+
Redirect to routines next
|
|
199
301
|
|
|
200
302
|
```
|
|
201
303
|
USAGE
|
|
@@ -205,10 +307,10 @@ GLOBAL FLAGS
|
|
|
205
307
|
--json Format output as json.
|
|
206
308
|
|
|
207
309
|
DESCRIPTION
|
|
208
|
-
|
|
310
|
+
Redirect to routines next
|
|
209
311
|
```
|
|
210
312
|
|
|
211
|
-
_See code: [src/commands/workouts/next.ts](https://github.com/nickchristensen/cliftin/blob/
|
|
313
|
+
_See code: [src/commands/workouts/next.ts](https://github.com/nickchristensen/cliftin/blob/v4.1.0/src/commands/workouts/next.ts)_
|
|
212
314
|
|
|
213
315
|
## `cliftin workouts show [WORKOUTID]`
|
|
214
316
|
|
|
@@ -216,11 +318,14 @@ Show one workout with exercises and sets
|
|
|
216
318
|
|
|
217
319
|
```
|
|
218
320
|
USAGE
|
|
219
|
-
$ cliftin workouts show [WORKOUTID] [--json]
|
|
321
|
+
$ cliftin workouts show [WORKOUTID] [--json] [--no-warmup]
|
|
220
322
|
|
|
221
323
|
ARGUMENTS
|
|
222
324
|
[WORKOUTID] workout id (default: latest workout)
|
|
223
325
|
|
|
326
|
+
FLAGS
|
|
327
|
+
--no-warmup Hide warmup sets from output
|
|
328
|
+
|
|
224
329
|
GLOBAL FLAGS
|
|
225
330
|
--json Format output as json.
|
|
226
331
|
|
|
@@ -228,5 +333,5 @@ DESCRIPTION
|
|
|
228
333
|
Show one workout with exercises and sets
|
|
229
334
|
```
|
|
230
335
|
|
|
231
|
-
_See code: [src/commands/workouts/show.ts](https://github.com/nickchristensen/cliftin/blob/
|
|
336
|
+
_See code: [src/commands/workouts/show.ts](https://github.com/nickchristensen/cliftin/blob/v4.1.0/src/commands/workouts/show.ts)_
|
|
232
337
|
<!-- commandsstop -->
|
|
@@ -13,6 +13,7 @@ export default class ExercisesShow extends Command {
|
|
|
13
13
|
'max-weight': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
14
|
'min-reps': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
15
|
'min-weight': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
'no-warmup': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
17
|
program: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
18
|
routine: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
19
|
to: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -40,6 +40,7 @@ export default class ExercisesShow extends Command {
|
|
|
40
40
|
'max-weight': Flags.integer({ description: 'History max top weight' }),
|
|
41
41
|
'min-reps': Flags.integer({ description: 'History min top reps' }),
|
|
42
42
|
'min-weight': Flags.integer({ description: 'History min top weight' }),
|
|
43
|
+
'no-warmup': Flags.boolean({ description: 'Hide warmup sets from output' }),
|
|
43
44
|
program: Flags.string({ description: 'History filter by program id or name' }),
|
|
44
45
|
routine: Flags.string({ description: 'History filter by routine id or name' }),
|
|
45
46
|
to: Flags.string({ description: 'History end date YYYY-MM-DD' }),
|
|
@@ -58,7 +59,9 @@ export default class ExercisesShow extends Command {
|
|
|
58
59
|
const historyUnitPreference = await resolveExerciseWeightUnit(context.db, exerciseId);
|
|
59
60
|
const history = (await getExerciseHistoryWithSetsRows(context.db, exerciseId, historyFilters)).map((row) => ({
|
|
60
61
|
...row,
|
|
61
|
-
sets: row.sets
|
|
62
|
+
sets: row.sets
|
|
63
|
+
.filter((set) => (parsedFlags['no-warmup'] ? !set.isWarmup : true))
|
|
64
|
+
.map((set) => ({
|
|
62
65
|
...set,
|
|
63
66
|
weight: withWeightUnit(set.weight, historyUnitPreference),
|
|
64
67
|
})),
|
|
@@ -92,7 +95,7 @@ export default class ExercisesShow extends Command {
|
|
|
92
95
|
this.log(`Program: ${lastPerformedSnapshot.workout.program ?? 'n/a'}`);
|
|
93
96
|
this.log(`Date: ${formatWorkoutDate(lastPerformedSnapshot.workout.date)}`);
|
|
94
97
|
this.log('');
|
|
95
|
-
this.log(renderTable(lastPerformedSnapshot.exercise.sets.map((set) => ({
|
|
98
|
+
this.log(renderTable(lastPerformedSnapshot.exercise.sets.filter((set) => (parsedFlags['no-warmup'] ? !set.isWarmup : true)).map((set) => ({
|
|
96
99
|
id: set.id,
|
|
97
100
|
isWarmup: set.isWarmup,
|
|
98
101
|
reps: set.reps,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class RoutinesFromWorkout extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
workoutId: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static enableJsonFlag: boolean;
|
|
8
|
+
run(): Promise<unknown | void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { closeDb, openDb } from '../../lib/db.js';
|
|
3
|
+
import { toJsonErrorPayload } from '../../lib/json-error.js';
|
|
4
|
+
import { serializeRoutineDetailWithWeightUnits } from '../../lib/json-weight.js';
|
|
5
|
+
import { renderTable } from '../../lib/output.js';
|
|
6
|
+
import { getLatestRoutineDetail, getRoutineDetailFromWorkout } from '../../lib/repositories/routines.js';
|
|
7
|
+
import { buildRoutineRows } from '../../lib/routine-output.js';
|
|
8
|
+
import { resolveProgramWeightUnit, weightUnitLabel } from '../../lib/units.js';
|
|
9
|
+
export default class RoutinesFromWorkout extends Command {
|
|
10
|
+
static args = {
|
|
11
|
+
workoutId: Args.string({
|
|
12
|
+
description: 'workout id (default: latest workout)',
|
|
13
|
+
ignoreStdin: true,
|
|
14
|
+
required: false,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
static description = 'Show the planned routine for a completed workout';
|
|
18
|
+
static enableJsonFlag = true;
|
|
19
|
+
async run() {
|
|
20
|
+
const { args } = await this.parse(RoutinesFromWorkout);
|
|
21
|
+
const context = openDb();
|
|
22
|
+
try {
|
|
23
|
+
if (args.workoutId !== undefined && !/^\d+$/.test(args.workoutId)) {
|
|
24
|
+
throw new Error('Workout id must be numeric.');
|
|
25
|
+
}
|
|
26
|
+
const detail = args.workoutId
|
|
27
|
+
? await getRoutineDetailFromWorkout(context.db, Number(args.workoutId))
|
|
28
|
+
: await getLatestRoutineDetail(context.db);
|
|
29
|
+
const unitPreference = await resolveProgramWeightUnit(context.db, detail.program.id);
|
|
30
|
+
const unitLabel = weightUnitLabel(unitPreference);
|
|
31
|
+
if (this.jsonEnabled()) {
|
|
32
|
+
return serializeRoutineDetailWithWeightUnits(detail, unitPreference);
|
|
33
|
+
}
|
|
34
|
+
this.log(`[${detail.routine.id}] ${detail.routine.name ?? 'Routine'}`);
|
|
35
|
+
this.log(`Program: ${detail.program.name}`);
|
|
36
|
+
this.log(`Week: ${detail.week.number}`);
|
|
37
|
+
if (detail.workout)
|
|
38
|
+
this.log(`Workout: [${detail.workout.id}] ${detail.workout.routine ?? 'Workout'}`);
|
|
39
|
+
this.log('');
|
|
40
|
+
this.log(renderTable(buildRoutineRows(detail.routine, unitLabel)).replace(/^\n+/, ''));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (this.jsonEnabled())
|
|
44
|
+
return toJsonErrorPayload(error);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await closeDb(context);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { closeDb, openDb } from '../../lib/db.js';
|
|
3
|
+
import { toJsonErrorPayload } from '../../lib/json-error.js';
|
|
4
|
+
import { serializeRoutineDetailWithWeightUnits } from '../../lib/json-weight.js';
|
|
5
|
+
import { renderTable } from '../../lib/output.js';
|
|
6
|
+
import { getLatestRoutineDetail } from '../../lib/repositories/routines.js';
|
|
7
|
+
import { buildRoutineRows } from '../../lib/routine-output.js';
|
|
8
|
+
import { resolveProgramWeightUnit, weightUnitLabel } from '../../lib/units.js';
|
|
9
|
+
export default class RoutinesLatest extends Command {
|
|
10
|
+
static description = 'Show the planned routine for the latest workout';
|
|
11
|
+
static enableJsonFlag = true;
|
|
12
|
+
async run() {
|
|
13
|
+
await this.parse(RoutinesLatest);
|
|
14
|
+
const context = openDb();
|
|
15
|
+
try {
|
|
16
|
+
const detail = await getLatestRoutineDetail(context.db);
|
|
17
|
+
const unitPreference = await resolveProgramWeightUnit(context.db, detail.program.id);
|
|
18
|
+
const unitLabel = weightUnitLabel(unitPreference);
|
|
19
|
+
if (this.jsonEnabled()) {
|
|
20
|
+
return serializeRoutineDetailWithWeightUnits(detail, unitPreference);
|
|
21
|
+
}
|
|
22
|
+
this.log(`[${detail.routine.id}] ${detail.routine.name ?? 'Routine'}`);
|
|
23
|
+
this.log(`Program: ${detail.program.name}`);
|
|
24
|
+
this.log(`Week: ${detail.week.number}`);
|
|
25
|
+
if (detail.workout)
|
|
26
|
+
this.log(`Workout: [${detail.workout.id}] ${detail.workout.routine ?? 'Workout'}`);
|
|
27
|
+
this.log('');
|
|
28
|
+
this.log(renderTable(buildRoutineRows(detail.routine, unitLabel)).replace(/^\n+/, ''));
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (this.jsonEnabled())
|
|
32
|
+
return toJsonErrorPayload(error);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await closeDb(context);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class RoutinesList extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static enableJsonFlag: boolean;
|
|
5
|
+
static flags: {
|
|
6
|
+
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
program: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
week: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<unknown | void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { closeDb, openDb } from '../../lib/db.js';
|
|
3
|
+
import { renderTable } from '../../lib/output.js';
|
|
4
|
+
import { listRoutines } from '../../lib/repositories/routines.js';
|
|
5
|
+
export default class RoutinesList extends Command {
|
|
6
|
+
static description = 'List planned routines';
|
|
7
|
+
static enableJsonFlag = true;
|
|
8
|
+
static flags = {
|
|
9
|
+
name: Flags.string({ description: 'Filter by routine name contains' }),
|
|
10
|
+
program: Flags.string({ description: 'Filter by program id or name' }),
|
|
11
|
+
week: Flags.integer({ description: 'Filter by week number' }),
|
|
12
|
+
};
|
|
13
|
+
async run() {
|
|
14
|
+
const { flags } = await this.parse(RoutinesList);
|
|
15
|
+
const context = openDb();
|
|
16
|
+
try {
|
|
17
|
+
const routines = await listRoutines(context.db, {
|
|
18
|
+
name: flags.name,
|
|
19
|
+
program: flags.program,
|
|
20
|
+
week: flags.week,
|
|
21
|
+
});
|
|
22
|
+
if (this.jsonEnabled()) {
|
|
23
|
+
return routines.map(({ id, ...routine }) => ({
|
|
24
|
+
...routine,
|
|
25
|
+
routineId: id,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
this.log(renderTable(routines.map((routine) => ({
|
|
29
|
+
id: routine.id,
|
|
30
|
+
isNext: routine.isNext,
|
|
31
|
+
name: routine.name,
|
|
32
|
+
program: routine.program,
|
|
33
|
+
week: routine.week,
|
|
34
|
+
}))));
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await closeDb(context);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { closeDb, openDb } from '../../lib/db.js';
|
|
3
|
+
import { toJsonErrorPayload } from '../../lib/json-error.js';
|
|
4
|
+
import { serializeRoutineDetailWithWeightUnits } from '../../lib/json-weight.js';
|
|
5
|
+
import { renderTable } from '../../lib/output.js';
|
|
6
|
+
import { getNextRoutineDetail } from '../../lib/repositories/routines.js';
|
|
7
|
+
import { buildRoutineRows } from '../../lib/routine-output.js';
|
|
8
|
+
import { resolveProgramWeightUnit, weightUnitLabel } from '../../lib/units.js';
|
|
9
|
+
export default class RoutinesNext extends Command {
|
|
10
|
+
static description = 'Show the up-next routine from the active program';
|
|
11
|
+
static enableJsonFlag = true;
|
|
12
|
+
async run() {
|
|
13
|
+
await this.parse(RoutinesNext);
|
|
14
|
+
const context = openDb();
|
|
15
|
+
try {
|
|
16
|
+
const detail = await getNextRoutineDetail(context.db);
|
|
17
|
+
const unitPreference = await resolveProgramWeightUnit(context.db, detail.program.id);
|
|
18
|
+
const unitLabel = weightUnitLabel(unitPreference);
|
|
19
|
+
if (this.jsonEnabled()) {
|
|
20
|
+
return serializeRoutineDetailWithWeightUnits(detail, unitPreference);
|
|
21
|
+
}
|
|
22
|
+
this.log(`[${detail.routine.id}] ${detail.routine.name ?? 'Routine'}`);
|
|
23
|
+
this.log(`Program: ${detail.program.name}`);
|
|
24
|
+
this.log(`Week: ${detail.week.number}`);
|
|
25
|
+
this.log('');
|
|
26
|
+
this.log(renderTable(buildRoutineRows(detail.routine, unitLabel)).replace(/^\n+/, ''));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (this.jsonEnabled())
|
|
30
|
+
return toJsonErrorPayload(error);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await closeDb(context);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class RoutinesShow 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
|
+
run(): Promise<unknown | void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { closeDb, openDb } from '../../lib/db.js';
|
|
3
|
+
import { serializeRoutineDetailWithWeightUnits } from '../../lib/json-weight.js';
|
|
4
|
+
import { renderTable } from '../../lib/output.js';
|
|
5
|
+
import { getRoutineDetail } from '../../lib/repositories/routines.js';
|
|
6
|
+
import { buildRoutineRows } from '../../lib/routine-output.js';
|
|
7
|
+
import { resolveProgramWeightUnit, weightUnitLabel } from '../../lib/units.js';
|
|
8
|
+
export default class RoutinesShow extends Command {
|
|
9
|
+
static args = {
|
|
10
|
+
selector: Args.string({ description: 'routine id or name', ignoreStdin: true, required: true }),
|
|
11
|
+
};
|
|
12
|
+
static description = 'Show one planned routine';
|
|
13
|
+
static enableJsonFlag = true;
|
|
14
|
+
async run() {
|
|
15
|
+
const { args } = await this.parse(RoutinesShow);
|
|
16
|
+
const context = openDb();
|
|
17
|
+
try {
|
|
18
|
+
const detail = await getRoutineDetail(context.db, args.selector);
|
|
19
|
+
const unitPreference = await resolveProgramWeightUnit(context.db, detail.program.id);
|
|
20
|
+
const unitLabel = weightUnitLabel(unitPreference);
|
|
21
|
+
if (this.jsonEnabled()) {
|
|
22
|
+
return serializeRoutineDetailWithWeightUnits(detail, unitPreference);
|
|
23
|
+
}
|
|
24
|
+
this.log(`[${detail.routine.id}] ${detail.routine.name ?? 'Routine'}`);
|
|
25
|
+
this.log(`Program: ${detail.program.name}`);
|
|
26
|
+
this.log(`Week: ${detail.week.number}`);
|
|
27
|
+
this.log('');
|
|
28
|
+
this.log(renderTable(buildRoutineRows(detail.routine, unitLabel)).replace(/^\n+/, ''));
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
await closeDb(context);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,44 +1,16 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import { closeDb, openDb } from '../../lib/db.js';
|
|
3
|
-
import { toJsonErrorPayload } from '../../lib/json-error.js';
|
|
4
|
-
import { serializeNextWorkoutDetailWithWeightUnits } from '../../lib/json-weight.js';
|
|
5
|
-
import { renderTable } from '../../lib/output.js';
|
|
6
|
-
import { getNextWorkoutDetail } from '../../lib/repositories/workouts.js';
|
|
7
|
-
import { resolveProgramWeightUnit, weightUnitLabel } from '../../lib/units.js';
|
|
8
2
|
export default class WorkoutsNext extends Command {
|
|
9
|
-
static description = '
|
|
3
|
+
static description = 'Redirect to routines next';
|
|
10
4
|
static enableJsonFlag = true;
|
|
11
5
|
async run() {
|
|
12
6
|
await this.parse(WorkoutsNext);
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return serializeNextWorkoutDetailWithWeightUnits(detail, unitPreference);
|
|
20
|
-
}
|
|
21
|
-
this.log(`[${detail.routine.id}] ${detail.routine.name ?? 'Workout'}`);
|
|
22
|
-
this.log(`Program: ${detail.program.name}`);
|
|
23
|
-
this.log(`Week: ${detail.week.number}`);
|
|
24
|
-
for (const exercise of detail.routine.exercises) {
|
|
25
|
-
this.log('');
|
|
26
|
-
this.log(`[${exercise.id ?? exercise.exerciseConfigId}] ${exercise.name ?? '(unnamed)'}`);
|
|
27
|
-
this.log(renderTable(exercise.sets.map((set) => ({
|
|
28
|
-
reps: set.reps,
|
|
29
|
-
rpe: set.rpe,
|
|
30
|
-
timeSeconds: set.timeSeconds,
|
|
31
|
-
weight: set.weight === null ? null : `${set.weight} ${unitLabel}`,
|
|
32
|
-
}))));
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
if (this.jsonEnabled())
|
|
37
|
-
return toJsonErrorPayload(error);
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
finally {
|
|
41
|
-
await closeDb(context);
|
|
7
|
+
const message = 'workouts next has moved. Use "cliftin routines next" instead.';
|
|
8
|
+
if (this.jsonEnabled()) {
|
|
9
|
+
return {
|
|
10
|
+
command: 'routines next',
|
|
11
|
+
message,
|
|
12
|
+
};
|
|
42
13
|
}
|
|
14
|
+
this.log(message);
|
|
43
15
|
}
|
|
44
16
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Args, Command } from '@oclif/core';
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import { format, isValid, parseISO } from 'date-fns';
|
|
3
3
|
import { closeDb, openDb } from '../../lib/db.js';
|
|
4
4
|
import { serializeWorkoutDetailWithWeightUnits } from '../../lib/json-weight.js';
|
|
@@ -18,14 +18,28 @@ function formatWorkoutDate(dateIso) {
|
|
|
18
18
|
return dateIso;
|
|
19
19
|
return format(parsed, 'yyyy-MM-dd HH:mm');
|
|
20
20
|
}
|
|
21
|
+
function maybeExcludeWarmupSets(detail, omitWarmups) {
|
|
22
|
+
if (!omitWarmups)
|
|
23
|
+
return detail;
|
|
24
|
+
return {
|
|
25
|
+
...detail,
|
|
26
|
+
exercises: detail.exercises.map((exercise) => ({
|
|
27
|
+
...exercise,
|
|
28
|
+
sets: exercise.sets.filter((set) => !set.isWarmup),
|
|
29
|
+
})),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
21
32
|
export default class WorkoutsShow extends Command {
|
|
22
33
|
static args = {
|
|
23
34
|
workoutId: Args.string({ description: 'workout id (default: latest workout)', ignoreStdin: true, required: false }),
|
|
24
35
|
};
|
|
25
36
|
static description = 'Show one workout with exercises and sets';
|
|
26
37
|
static enableJsonFlag = true;
|
|
38
|
+
static flags = {
|
|
39
|
+
'no-warmup': Flags.boolean({ description: 'Hide warmup sets from output' }),
|
|
40
|
+
};
|
|
27
41
|
async run() {
|
|
28
|
-
const { args } = await this.parse(WorkoutsShow);
|
|
42
|
+
const { args, flags } = await this.parse(WorkoutsShow);
|
|
29
43
|
const context = openDb();
|
|
30
44
|
try {
|
|
31
45
|
if (args.workoutId !== undefined && !/^\d+$/.test(args.workoutId)) {
|
|
@@ -38,7 +52,7 @@ export default class WorkoutsShow extends Command {
|
|
|
38
52
|
throw new Error('No workouts found.');
|
|
39
53
|
return rows[0].id;
|
|
40
54
|
});
|
|
41
|
-
const detail = await getWorkoutDetail(context.db, workoutId);
|
|
55
|
+
const detail = maybeExcludeWarmupSets(await getWorkoutDetail(context.db, workoutId), flags['no-warmup']);
|
|
42
56
|
const unitPreference = await resolveGlobalWeightUnit(context.db);
|
|
43
57
|
const unitLabel = weightUnitLabel(unitPreference);
|
|
44
58
|
if (this.jsonEnabled()) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ExerciseHistoryRow,
|
|
1
|
+
import { ExerciseHistoryRow, ProgramDetailTree, RoutineDetail, WorkoutDetail } from './types.js';
|
|
2
2
|
import { UnitPreference, withWeightUnit } from './units.js';
|
|
3
3
|
export declare function serializeExerciseHistoryRowsWithWeightUnits(rows: ExerciseHistoryRow[], unitPreference: UnitPreference): Array<Omit<ExerciseHistoryRow, 'topWeight'> & {
|
|
4
4
|
topWeight: ReturnType<typeof withWeightUnit>;
|
|
5
5
|
}>;
|
|
6
6
|
export declare function serializeProgramDetailWithWeightUnits(detail: ProgramDetailTree, unitPreference: UnitPreference): unknown;
|
|
7
|
-
export declare function
|
|
7
|
+
export declare function serializeRoutineDetailWithWeightUnits(detail: RoutineDetail, unitPreference: UnitPreference): unknown;
|
|
8
8
|
export declare function serializeWorkoutDetailWithWeightUnits(detail: WorkoutDetail, unitPreference: UnitPreference): unknown;
|
package/dist/lib/json-weight.js
CHANGED
|
@@ -31,9 +31,9 @@ export function serializeProgramDetailWithWeightUnits(detail, unitPreference) {
|
|
|
31
31
|
})),
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
export function
|
|
34
|
+
export function serializeRoutineDetailWithWeightUnits(detail, unitPreference) {
|
|
35
35
|
const program = omitId(detail.program);
|
|
36
|
-
|
|
36
|
+
const payload = {
|
|
37
37
|
program: { ...program, programId: detail.program.id },
|
|
38
38
|
routine: {
|
|
39
39
|
...omitId(detail.routine),
|
|
@@ -54,6 +54,19 @@ export function serializeNextWorkoutDetailWithWeightUnits(detail, unitPreference
|
|
|
54
54
|
weekId: detail.week.id,
|
|
55
55
|
},
|
|
56
56
|
};
|
|
57
|
+
if (detail.workout) {
|
|
58
|
+
payload.workout = {
|
|
59
|
+
date: detail.workout.date,
|
|
60
|
+
duration: {
|
|
61
|
+
unit: 'seconds',
|
|
62
|
+
value: detail.workout.duration,
|
|
63
|
+
},
|
|
64
|
+
program: detail.workout.program,
|
|
65
|
+
routine: detail.workout.routine,
|
|
66
|
+
workoutId: detail.workout.id,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return payload;
|
|
57
70
|
}
|
|
58
71
|
export function serializeWorkoutDetailWithWeightUnits(detail, unitPreference) {
|
|
59
72
|
return {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Kysely } from 'kysely';
|
|
2
|
+
import { DatabaseSchema } from '../db.js';
|
|
3
|
+
import { RoutineDetail, RoutineSummary } from '../types.js';
|
|
4
|
+
export type RoutineFilters = {
|
|
5
|
+
name?: string;
|
|
6
|
+
program?: string;
|
|
7
|
+
week?: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function listRoutines(db: Kysely<DatabaseSchema>, filters: RoutineFilters): Promise<RoutineSummary[]>;
|
|
10
|
+
export declare function getRoutineDetail(db: Kysely<DatabaseSchema>, selector: string): Promise<RoutineDetail>;
|
|
11
|
+
export declare function getNextRoutineDetail(db: Kysely<DatabaseSchema>): Promise<RoutineDetail>;
|
|
12
|
+
export declare function getRoutineDetailFromWorkout(db: Kysely<DatabaseSchema>, workoutId: number): Promise<RoutineDetail>;
|
|
13
|
+
export declare function getLatestRoutineDetail(db: Kysely<DatabaseSchema>): Promise<RoutineDetail>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { sql } from 'kysely';
|
|
2
|
+
import { appleSecondsToIso } from '../time.js';
|
|
3
|
+
import { getProgramDetail, resolveProgramSelector } from './programs.js';
|
|
4
|
+
import { resolveIdOrName } from './selectors.js';
|
|
5
|
+
async function getRoutinePlanContext(db, routineId) {
|
|
6
|
+
const row = await db
|
|
7
|
+
.selectFrom('ZROUTINE as r')
|
|
8
|
+
.leftJoin('ZPERIOD as p', 'p.Z_PK', 'r.ZPERIOD')
|
|
9
|
+
.leftJoin('ZWORKOUTPLAN as planDirect', 'planDirect.Z_PK', 'r.ZWORKOUTPLAN')
|
|
10
|
+
.leftJoin('ZWORKOUTPLAN as planFromPeriod', 'planFromPeriod.Z_PK', 'p.ZWORKOUTPLAN')
|
|
11
|
+
.select([
|
|
12
|
+
'r.Z_PK as id',
|
|
13
|
+
'p.Z_PK as weekId',
|
|
14
|
+
'planDirect.Z_PK as programIdDirect',
|
|
15
|
+
'planFromPeriod.Z_PK as programIdFromPeriod',
|
|
16
|
+
])
|
|
17
|
+
.where('r.Z_PK', '=', routineId)
|
|
18
|
+
.where('r.ZSOFTDELETED', 'is not', 1)
|
|
19
|
+
.where((eb) => eb.or([eb('planDirect.ZSOFTDELETED', 'is not', 1), eb('planFromPeriod.ZSOFTDELETED', 'is not', 1)]))
|
|
20
|
+
.executeTakeFirst();
|
|
21
|
+
if (!row)
|
|
22
|
+
throw new Error(`Routine not found: ${routineId}`);
|
|
23
|
+
const programId = row.programIdDirect ?? row.programIdFromPeriod;
|
|
24
|
+
if (programId === null)
|
|
25
|
+
throw new Error(`Routine ${routineId} is not linked to a program.`);
|
|
26
|
+
if (row.weekId === null)
|
|
27
|
+
throw new Error(`Routine ${routineId} is not linked to a week.`);
|
|
28
|
+
return { programId, weekId: row.weekId };
|
|
29
|
+
}
|
|
30
|
+
async function getRoutineDetailById(db, routineId, workout) {
|
|
31
|
+
const context = await getRoutinePlanContext(db, routineId);
|
|
32
|
+
const programDetail = await getProgramDetail(db, context.programId);
|
|
33
|
+
const weekIndex = programDetail.weeks.findIndex((week) => week.id === context.weekId);
|
|
34
|
+
if (weekIndex === -1) {
|
|
35
|
+
throw new Error(`Routine ${routineId} is linked to unknown week ${context.weekId}.`);
|
|
36
|
+
}
|
|
37
|
+
const week = programDetail.weeks[weekIndex];
|
|
38
|
+
const routine = week.routines.find((entry) => entry.id === routineId);
|
|
39
|
+
if (!routine) {
|
|
40
|
+
throw new Error(`Routine ${routineId} was not found in program ${programDetail.program.name}.`);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
program: programDetail.program,
|
|
44
|
+
routine,
|
|
45
|
+
week: {
|
|
46
|
+
id: week.id,
|
|
47
|
+
number: weekIndex + 1,
|
|
48
|
+
},
|
|
49
|
+
workout,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export async function listRoutines(db, filters) {
|
|
53
|
+
let query = db
|
|
54
|
+
.selectFrom('ZROUTINE as r')
|
|
55
|
+
.leftJoin('ZPERIOD as p', 'p.Z_PK', 'r.ZPERIOD')
|
|
56
|
+
.leftJoin('ZWORKOUTPLAN as planDirect', 'planDirect.Z_PK', 'r.ZWORKOUTPLAN')
|
|
57
|
+
.leftJoin('ZWORKOUTPLAN as planFromPeriod', 'planFromPeriod.Z_PK', 'p.ZWORKOUTPLAN')
|
|
58
|
+
.select([
|
|
59
|
+
'r.Z_PK as id',
|
|
60
|
+
'r.ZNAME as name',
|
|
61
|
+
'r.ZUPNEXT as upNext',
|
|
62
|
+
'p.Z_PK as weekId',
|
|
63
|
+
'planDirect.Z_PK as programIdDirect',
|
|
64
|
+
'planFromPeriod.Z_PK as programIdFromPeriod',
|
|
65
|
+
'planDirect.ZNAME as programNameDirect',
|
|
66
|
+
'planFromPeriod.ZNAME as programNameFromPeriod',
|
|
67
|
+
])
|
|
68
|
+
.where('r.ZSOFTDELETED', 'is not', 1)
|
|
69
|
+
.where((eb) => eb.or([eb('planDirect.ZSOFTDELETED', 'is not', 1), eb('planFromPeriod.ZSOFTDELETED', 'is not', 1)]));
|
|
70
|
+
if (filters.program) {
|
|
71
|
+
const programId = await resolveProgramSelector(db, filters.program, false);
|
|
72
|
+
query = query.where((eb) => eb.or([eb('r.ZWORKOUTPLAN', '=', programId), eb('p.ZWORKOUTPLAN', '=', programId)]));
|
|
73
|
+
}
|
|
74
|
+
if (filters.name) {
|
|
75
|
+
const lowered = `%${filters.name.toLowerCase()}%`;
|
|
76
|
+
query = query.where(sql `lower(r.ZNAME) like ${lowered}`);
|
|
77
|
+
}
|
|
78
|
+
query = query.orderBy('planFromPeriod.ZDATEADDED', 'desc').orderBy('planDirect.ZDATEADDED', 'desc').orderBy('p.Z_FOK_WORKOUTPLAN', 'asc').orderBy('r.Z_FOK_PERIOD', 'asc').orderBy('r.Z_PK', 'asc');
|
|
79
|
+
const rows = await query.execute();
|
|
80
|
+
const weekNumbers = new Map();
|
|
81
|
+
const countsByProgram = new Map();
|
|
82
|
+
for (const row of rows) {
|
|
83
|
+
const programId = row.programIdDirect ?? row.programIdFromPeriod;
|
|
84
|
+
if (programId === null || row.weekId === null)
|
|
85
|
+
continue;
|
|
86
|
+
const key = row.weekId;
|
|
87
|
+
if (!weekNumbers.has(key)) {
|
|
88
|
+
const count = (countsByProgram.get(programId) ?? 0) + 1;
|
|
89
|
+
countsByProgram.set(programId, count);
|
|
90
|
+
weekNumbers.set(key, count);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return rows
|
|
94
|
+
.map((row) => ({
|
|
95
|
+
id: row.id,
|
|
96
|
+
isNext: row.upNext === 1,
|
|
97
|
+
name: row.name,
|
|
98
|
+
program: row.programNameDirect ?? row.programNameFromPeriod,
|
|
99
|
+
week: row.weekId === null ? null : weekNumbers.get(row.weekId) ?? null,
|
|
100
|
+
}))
|
|
101
|
+
.filter((row) => (filters.week === undefined ? true : row.week === filters.week));
|
|
102
|
+
}
|
|
103
|
+
export async function getRoutineDetail(db, selector) {
|
|
104
|
+
const routineId = await resolveIdOrName(db, 'ZROUTINE', selector);
|
|
105
|
+
return getRoutineDetailById(db, routineId, null);
|
|
106
|
+
}
|
|
107
|
+
export async function getNextRoutineDetail(db) {
|
|
108
|
+
const programId = await resolveProgramSelector(db, undefined, true);
|
|
109
|
+
const nextRoutines = await db
|
|
110
|
+
.selectFrom('ZROUTINE as r')
|
|
111
|
+
.leftJoin('ZPERIOD as p', 'p.Z_PK', 'r.ZPERIOD')
|
|
112
|
+
.select(['r.Z_PK as id'])
|
|
113
|
+
.where('r.ZUPNEXT', '=', 1)
|
|
114
|
+
.where('r.ZSOFTDELETED', 'is not', 1)
|
|
115
|
+
.where((eb) => eb.or([eb('p.ZWORKOUTPLAN', '=', programId), eb('r.ZWORKOUTPLAN', '=', programId)]))
|
|
116
|
+
.orderBy('p.Z_FOK_WORKOUTPLAN', 'asc')
|
|
117
|
+
.orderBy('r.Z_FOK_PERIOD', 'asc')
|
|
118
|
+
.orderBy('r.Z_PK', 'asc')
|
|
119
|
+
.execute();
|
|
120
|
+
if (nextRoutines.length === 0) {
|
|
121
|
+
const programDetail = await getProgramDetail(db, programId);
|
|
122
|
+
throw new Error(`No up-next routine found for active program ${programDetail.program.name}.`);
|
|
123
|
+
}
|
|
124
|
+
if (nextRoutines.length > 1) {
|
|
125
|
+
const programDetail = await getProgramDetail(db, programId);
|
|
126
|
+
throw new Error(`Expected exactly one up-next routine for active program ${programDetail.program.name}. Found ${nextRoutines.length}.`);
|
|
127
|
+
}
|
|
128
|
+
return getRoutineDetailById(db, nextRoutines[0].id, null);
|
|
129
|
+
}
|
|
130
|
+
export async function getRoutineDetailFromWorkout(db, workoutId) {
|
|
131
|
+
const workout = await db
|
|
132
|
+
.selectFrom('ZWORKOUTRESULT as wr')
|
|
133
|
+
.leftJoin('ZROUTINE as r', 'r.Z_PK', 'wr.ZROUTINE')
|
|
134
|
+
.leftJoin('ZPERIOD as p', 'p.Z_PK', 'r.ZPERIOD')
|
|
135
|
+
.leftJoin('ZWORKOUTPLAN as planDirect', 'planDirect.Z_PK', 'r.ZWORKOUTPLAN')
|
|
136
|
+
.leftJoin('ZWORKOUTPLAN as planFromPeriod', 'planFromPeriod.Z_PK', 'p.ZWORKOUTPLAN')
|
|
137
|
+
.select([
|
|
138
|
+
'wr.Z_PK as id',
|
|
139
|
+
'wr.ZDURATION as duration',
|
|
140
|
+
'wr.ZSTARTDATE as startDate',
|
|
141
|
+
'wr.ZROUTINE as routineId',
|
|
142
|
+
'wr.ZROUTINENAME as routineNameFromResult',
|
|
143
|
+
'r.ZNAME as routineNameFromPlan',
|
|
144
|
+
'planDirect.ZNAME as programNameDirect',
|
|
145
|
+
'planFromPeriod.ZNAME as programNameFromPeriod',
|
|
146
|
+
])
|
|
147
|
+
.where('wr.Z_PK', '=', workoutId)
|
|
148
|
+
.executeTakeFirst();
|
|
149
|
+
if (!workout)
|
|
150
|
+
throw new Error(`Workout not found: ${workoutId}`);
|
|
151
|
+
if (workout.routineId === null)
|
|
152
|
+
throw new Error(`Workout ${workoutId} is not linked to a planned routine.`);
|
|
153
|
+
return getRoutineDetailById(db, workout.routineId, {
|
|
154
|
+
date: appleSecondsToIso(workout.startDate),
|
|
155
|
+
duration: workout.duration,
|
|
156
|
+
id: workout.id,
|
|
157
|
+
program: workout.programNameDirect ?? workout.programNameFromPeriod,
|
|
158
|
+
routine: workout.routineNameFromResult ?? workout.routineNameFromPlan,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
export async function getLatestRoutineDetail(db) {
|
|
162
|
+
const latestWorkout = await db
|
|
163
|
+
.selectFrom('ZWORKOUTRESULT')
|
|
164
|
+
.select('Z_PK as id')
|
|
165
|
+
.orderBy('ZSTARTDATE', 'desc')
|
|
166
|
+
.limit(1)
|
|
167
|
+
.executeTakeFirst();
|
|
168
|
+
if (!latestWorkout)
|
|
169
|
+
throw new Error('No workouts found.');
|
|
170
|
+
return getRoutineDetailFromWorkout(db, latestWorkout.id);
|
|
171
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function buildRoutineRows(routine, unitLabel) {
|
|
2
|
+
return routine.exercises.flatMap((exercise) => {
|
|
3
|
+
const headingRow = {
|
|
4
|
+
exercise: `[${exercise.id ?? exercise.exerciseConfigId}] ${exercise.name ?? '(unnamed)'}`,
|
|
5
|
+
reps: null,
|
|
6
|
+
rpe: null,
|
|
7
|
+
timeSeconds: null,
|
|
8
|
+
weight: null,
|
|
9
|
+
};
|
|
10
|
+
const setRows = exercise.sets.map((set) => ({
|
|
11
|
+
exercise: '',
|
|
12
|
+
reps: set.reps,
|
|
13
|
+
rpe: set.rpe,
|
|
14
|
+
timeSeconds: set.timeSeconds,
|
|
15
|
+
weight: set.weight === null ? null : `${set.weight} ${unitLabel}`,
|
|
16
|
+
}));
|
|
17
|
+
return [headingRow, ...setRows];
|
|
18
|
+
});
|
|
19
|
+
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -39,6 +39,23 @@ export type ProgramDetailTree = {
|
|
|
39
39
|
program: ProgramSummary;
|
|
40
40
|
weeks: ProgramWeek[];
|
|
41
41
|
};
|
|
42
|
+
export type RoutineWeekRef = {
|
|
43
|
+
id: number;
|
|
44
|
+
number: number;
|
|
45
|
+
};
|
|
46
|
+
export type RoutineSummary = {
|
|
47
|
+
id: number;
|
|
48
|
+
isNext: boolean;
|
|
49
|
+
name: null | string;
|
|
50
|
+
program: null | string;
|
|
51
|
+
week: null | number;
|
|
52
|
+
};
|
|
53
|
+
export type RoutineDetail = {
|
|
54
|
+
program: ProgramSummary;
|
|
55
|
+
routine: ProgramRoutine;
|
|
56
|
+
week: RoutineWeekRef;
|
|
57
|
+
workout: null | WorkoutSummary;
|
|
58
|
+
};
|
|
42
59
|
export type WorkoutSummary = {
|
|
43
60
|
date: null | string;
|
|
44
61
|
duration: null | number;
|
|
@@ -69,14 +86,7 @@ export type WorkoutDetail = {
|
|
|
69
86
|
program: null | string;
|
|
70
87
|
routine: null | string;
|
|
71
88
|
};
|
|
72
|
-
export type NextWorkoutDetail =
|
|
73
|
-
program: ProgramSummary;
|
|
74
|
-
routine: ProgramRoutine;
|
|
75
|
-
week: {
|
|
76
|
-
id: number;
|
|
77
|
-
number: number;
|
|
78
|
-
};
|
|
79
|
-
};
|
|
89
|
+
export type NextWorkoutDetail = RoutineDetail;
|
|
80
90
|
export type ExerciseSummary = {
|
|
81
91
|
equipment: null | string;
|
|
82
92
|
id: number;
|
package/oclif.manifest.json
CHANGED
|
@@ -139,6 +139,12 @@
|
|
|
139
139
|
"multiple": false,
|
|
140
140
|
"type": "option"
|
|
141
141
|
},
|
|
142
|
+
"no-warmup": {
|
|
143
|
+
"description": "Hide warmup sets from output",
|
|
144
|
+
"name": "no-warmup",
|
|
145
|
+
"allowNo": false,
|
|
146
|
+
"type": "boolean"
|
|
147
|
+
},
|
|
142
148
|
"program": {
|
|
143
149
|
"description": "History filter by program id or name",
|
|
144
150
|
"name": "program",
|
|
@@ -241,6 +247,184 @@
|
|
|
241
247
|
"show.js"
|
|
242
248
|
]
|
|
243
249
|
},
|
|
250
|
+
"routines:from-workout": {
|
|
251
|
+
"aliases": [],
|
|
252
|
+
"args": {
|
|
253
|
+
"workoutId": {
|
|
254
|
+
"description": "workout id (default: latest workout)",
|
|
255
|
+
"name": "workoutId",
|
|
256
|
+
"required": false
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
"description": "Show the planned routine for a completed workout",
|
|
260
|
+
"flags": {
|
|
261
|
+
"json": {
|
|
262
|
+
"description": "Format output as json.",
|
|
263
|
+
"helpGroup": "GLOBAL",
|
|
264
|
+
"name": "json",
|
|
265
|
+
"allowNo": false,
|
|
266
|
+
"type": "boolean"
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
"hasDynamicHelp": false,
|
|
270
|
+
"hiddenAliases": [],
|
|
271
|
+
"id": "routines:from-workout",
|
|
272
|
+
"pluginAlias": "@nickchristensen/cliftin",
|
|
273
|
+
"pluginName": "@nickchristensen/cliftin",
|
|
274
|
+
"pluginType": "core",
|
|
275
|
+
"strict": true,
|
|
276
|
+
"enableJsonFlag": true,
|
|
277
|
+
"isESM": true,
|
|
278
|
+
"relativePath": [
|
|
279
|
+
"dist",
|
|
280
|
+
"commands",
|
|
281
|
+
"routines",
|
|
282
|
+
"from-workout.js"
|
|
283
|
+
]
|
|
284
|
+
},
|
|
285
|
+
"routines:latest": {
|
|
286
|
+
"aliases": [],
|
|
287
|
+
"args": {},
|
|
288
|
+
"description": "Show the planned routine for the latest workout",
|
|
289
|
+
"flags": {
|
|
290
|
+
"json": {
|
|
291
|
+
"description": "Format output as json.",
|
|
292
|
+
"helpGroup": "GLOBAL",
|
|
293
|
+
"name": "json",
|
|
294
|
+
"allowNo": false,
|
|
295
|
+
"type": "boolean"
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
"hasDynamicHelp": false,
|
|
299
|
+
"hiddenAliases": [],
|
|
300
|
+
"id": "routines:latest",
|
|
301
|
+
"pluginAlias": "@nickchristensen/cliftin",
|
|
302
|
+
"pluginName": "@nickchristensen/cliftin",
|
|
303
|
+
"pluginType": "core",
|
|
304
|
+
"strict": true,
|
|
305
|
+
"enableJsonFlag": true,
|
|
306
|
+
"isESM": true,
|
|
307
|
+
"relativePath": [
|
|
308
|
+
"dist",
|
|
309
|
+
"commands",
|
|
310
|
+
"routines",
|
|
311
|
+
"latest.js"
|
|
312
|
+
]
|
|
313
|
+
},
|
|
314
|
+
"routines:list": {
|
|
315
|
+
"aliases": [],
|
|
316
|
+
"args": {},
|
|
317
|
+
"description": "List planned routines",
|
|
318
|
+
"flags": {
|
|
319
|
+
"json": {
|
|
320
|
+
"description": "Format output as json.",
|
|
321
|
+
"helpGroup": "GLOBAL",
|
|
322
|
+
"name": "json",
|
|
323
|
+
"allowNo": false,
|
|
324
|
+
"type": "boolean"
|
|
325
|
+
},
|
|
326
|
+
"name": {
|
|
327
|
+
"description": "Filter by routine name contains",
|
|
328
|
+
"name": "name",
|
|
329
|
+
"hasDynamicHelp": false,
|
|
330
|
+
"multiple": false,
|
|
331
|
+
"type": "option"
|
|
332
|
+
},
|
|
333
|
+
"program": {
|
|
334
|
+
"description": "Filter by program id or name",
|
|
335
|
+
"name": "program",
|
|
336
|
+
"hasDynamicHelp": false,
|
|
337
|
+
"multiple": false,
|
|
338
|
+
"type": "option"
|
|
339
|
+
},
|
|
340
|
+
"week": {
|
|
341
|
+
"description": "Filter by week number",
|
|
342
|
+
"name": "week",
|
|
343
|
+
"hasDynamicHelp": false,
|
|
344
|
+
"multiple": false,
|
|
345
|
+
"type": "option"
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
"hasDynamicHelp": false,
|
|
349
|
+
"hiddenAliases": [],
|
|
350
|
+
"id": "routines:list",
|
|
351
|
+
"pluginAlias": "@nickchristensen/cliftin",
|
|
352
|
+
"pluginName": "@nickchristensen/cliftin",
|
|
353
|
+
"pluginType": "core",
|
|
354
|
+
"strict": true,
|
|
355
|
+
"enableJsonFlag": true,
|
|
356
|
+
"isESM": true,
|
|
357
|
+
"relativePath": [
|
|
358
|
+
"dist",
|
|
359
|
+
"commands",
|
|
360
|
+
"routines",
|
|
361
|
+
"list.js"
|
|
362
|
+
]
|
|
363
|
+
},
|
|
364
|
+
"routines:next": {
|
|
365
|
+
"aliases": [],
|
|
366
|
+
"args": {},
|
|
367
|
+
"description": "Show the up-next routine from the active program",
|
|
368
|
+
"flags": {
|
|
369
|
+
"json": {
|
|
370
|
+
"description": "Format output as json.",
|
|
371
|
+
"helpGroup": "GLOBAL",
|
|
372
|
+
"name": "json",
|
|
373
|
+
"allowNo": false,
|
|
374
|
+
"type": "boolean"
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
"hasDynamicHelp": false,
|
|
378
|
+
"hiddenAliases": [],
|
|
379
|
+
"id": "routines:next",
|
|
380
|
+
"pluginAlias": "@nickchristensen/cliftin",
|
|
381
|
+
"pluginName": "@nickchristensen/cliftin",
|
|
382
|
+
"pluginType": "core",
|
|
383
|
+
"strict": true,
|
|
384
|
+
"enableJsonFlag": true,
|
|
385
|
+
"isESM": true,
|
|
386
|
+
"relativePath": [
|
|
387
|
+
"dist",
|
|
388
|
+
"commands",
|
|
389
|
+
"routines",
|
|
390
|
+
"next.js"
|
|
391
|
+
]
|
|
392
|
+
},
|
|
393
|
+
"routines:show": {
|
|
394
|
+
"aliases": [],
|
|
395
|
+
"args": {
|
|
396
|
+
"selector": {
|
|
397
|
+
"description": "routine id or name",
|
|
398
|
+
"name": "selector",
|
|
399
|
+
"required": true
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
"description": "Show one planned routine",
|
|
403
|
+
"flags": {
|
|
404
|
+
"json": {
|
|
405
|
+
"description": "Format output as json.",
|
|
406
|
+
"helpGroup": "GLOBAL",
|
|
407
|
+
"name": "json",
|
|
408
|
+
"allowNo": false,
|
|
409
|
+
"type": "boolean"
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
"hasDynamicHelp": false,
|
|
413
|
+
"hiddenAliases": [],
|
|
414
|
+
"id": "routines:show",
|
|
415
|
+
"pluginAlias": "@nickchristensen/cliftin",
|
|
416
|
+
"pluginName": "@nickchristensen/cliftin",
|
|
417
|
+
"pluginType": "core",
|
|
418
|
+
"strict": true,
|
|
419
|
+
"enableJsonFlag": true,
|
|
420
|
+
"isESM": true,
|
|
421
|
+
"relativePath": [
|
|
422
|
+
"dist",
|
|
423
|
+
"commands",
|
|
424
|
+
"routines",
|
|
425
|
+
"show.js"
|
|
426
|
+
]
|
|
427
|
+
},
|
|
244
428
|
"workouts:list": {
|
|
245
429
|
"aliases": [],
|
|
246
430
|
"args": {},
|
|
@@ -337,7 +521,7 @@
|
|
|
337
521
|
"workouts:next": {
|
|
338
522
|
"aliases": [],
|
|
339
523
|
"args": {},
|
|
340
|
-
"description": "
|
|
524
|
+
"description": "Redirect to routines next",
|
|
341
525
|
"flags": {
|
|
342
526
|
"json": {
|
|
343
527
|
"description": "Format output as json.",
|
|
@@ -380,6 +564,12 @@
|
|
|
380
564
|
"name": "json",
|
|
381
565
|
"allowNo": false,
|
|
382
566
|
"type": "boolean"
|
|
567
|
+
},
|
|
568
|
+
"no-warmup": {
|
|
569
|
+
"description": "Hide warmup sets from output",
|
|
570
|
+
"name": "no-warmup",
|
|
571
|
+
"allowNo": false,
|
|
572
|
+
"type": "boolean"
|
|
383
573
|
}
|
|
384
574
|
},
|
|
385
575
|
"hasDynamicHelp": false,
|
|
@@ -399,5 +589,5 @@
|
|
|
399
589
|
]
|
|
400
590
|
}
|
|
401
591
|
},
|
|
402
|
-
"version": "
|
|
592
|
+
"version": "4.1.0"
|
|
403
593
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nickchristensen/cliftin",
|
|
3
3
|
"description": "CLIftin: A read-only CLI for Liftin'",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.1.0",
|
|
5
5
|
"author": "Nick Christensen",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cliftin": "./bin/run.js"
|
|
@@ -71,6 +71,9 @@
|
|
|
71
71
|
"programs": {
|
|
72
72
|
"description": "Program hierarchy and planning"
|
|
73
73
|
},
|
|
74
|
+
"routines": {
|
|
75
|
+
"description": "Planned routine detail and navigation"
|
|
76
|
+
},
|
|
74
77
|
"workouts": {
|
|
75
78
|
"description": "Workout history and detail"
|
|
76
79
|
}
|