@nickchristensen/cliftin 3.0.0 → 4.0.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 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/v3.0.0/src/commands/exercises/list.ts)_
81
+ _See code: [src/commands/exercises/list.ts](https://github.com/nickchristensen/cliftin/blob/v4.0.0/src/commands/exercises/list.ts)_
77
82
 
78
83
  ## `cliftin exercises show SELECTOR`
79
84
 
@@ -107,7 +112,7 @@ DESCRIPTION
107
112
  Show one exercise detail and history
108
113
  ```
109
114
 
110
- _See code: [src/commands/exercises/show.ts](https://github.com/nickchristensen/cliftin/blob/v3.0.0/src/commands/exercises/show.ts)_
115
+ _See code: [src/commands/exercises/show.ts](https://github.com/nickchristensen/cliftin/blob/v4.0.0/src/commands/exercises/show.ts)_
111
116
 
112
117
  ## `cliftin help [COMMAND]`
113
118
 
@@ -144,7 +149,7 @@ DESCRIPTION
144
149
  List programs
145
150
  ```
146
151
 
147
- _See code: [src/commands/programs/list.ts](https://github.com/nickchristensen/cliftin/blob/v3.0.0/src/commands/programs/list.ts)_
152
+ _See code: [src/commands/programs/list.ts](https://github.com/nickchristensen/cliftin/blob/v4.0.0/src/commands/programs/list.ts)_
148
153
 
149
154
  ## `cliftin programs show [SELECTOR]`
150
155
 
@@ -164,7 +169,103 @@ DESCRIPTION
164
169
  Show one program hierarchy
165
170
  ```
166
171
 
167
- _See code: [src/commands/programs/show.ts](https://github.com/nickchristensen/cliftin/blob/v3.0.0/src/commands/programs/show.ts)_
172
+ _See code: [src/commands/programs/show.ts](https://github.com/nickchristensen/cliftin/blob/v4.0.0/src/commands/programs/show.ts)_
173
+
174
+ ## `cliftin routines from-workout [WORKOUTID]`
175
+
176
+ Show the planned routine for a completed workout
177
+
178
+ ```
179
+ USAGE
180
+ $ cliftin routines from-workout [WORKOUTID] [--json]
181
+
182
+ ARGUMENTS
183
+ [WORKOUTID] workout id (default: latest workout)
184
+
185
+ GLOBAL FLAGS
186
+ --json Format output as json.
187
+
188
+ DESCRIPTION
189
+ Show the planned routine for a completed workout
190
+ ```
191
+
192
+ _See code: [src/commands/routines/from-workout.ts](https://github.com/nickchristensen/cliftin/blob/v4.0.0/src/commands/routines/from-workout.ts)_
193
+
194
+ ## `cliftin routines latest`
195
+
196
+ Show the planned routine for the latest workout
197
+
198
+ ```
199
+ USAGE
200
+ $ cliftin routines latest [--json]
201
+
202
+ GLOBAL FLAGS
203
+ --json Format output as json.
204
+
205
+ DESCRIPTION
206
+ Show the planned routine for the latest workout
207
+ ```
208
+
209
+ _See code: [src/commands/routines/latest.ts](https://github.com/nickchristensen/cliftin/blob/v4.0.0/src/commands/routines/latest.ts)_
210
+
211
+ ## `cliftin routines list`
212
+
213
+ List planned routines
214
+
215
+ ```
216
+ USAGE
217
+ $ cliftin routines list [--json] [--name <value>] [--program <value>] [--week <value>]
218
+
219
+ FLAGS
220
+ --name=<value> Filter by routine name contains
221
+ --program=<value> Filter by program id or name
222
+ --week=<value> Filter by week number
223
+
224
+ GLOBAL FLAGS
225
+ --json Format output as json.
226
+
227
+ DESCRIPTION
228
+ List planned routines
229
+ ```
230
+
231
+ _See code: [src/commands/routines/list.ts](https://github.com/nickchristensen/cliftin/blob/v4.0.0/src/commands/routines/list.ts)_
232
+
233
+ ## `cliftin routines next`
234
+
235
+ Show the up-next routine from the active program
236
+
237
+ ```
238
+ USAGE
239
+ $ cliftin routines next [--json]
240
+
241
+ GLOBAL FLAGS
242
+ --json Format output as json.
243
+
244
+ DESCRIPTION
245
+ Show the up-next routine from the active program
246
+ ```
247
+
248
+ _See code: [src/commands/routines/next.ts](https://github.com/nickchristensen/cliftin/blob/v4.0.0/src/commands/routines/next.ts)_
249
+
250
+ ## `cliftin routines show SELECTOR`
251
+
252
+ Show one planned routine
253
+
254
+ ```
255
+ USAGE
256
+ $ cliftin routines show SELECTOR [--json]
257
+
258
+ ARGUMENTS
259
+ SELECTOR routine id or name
260
+
261
+ GLOBAL FLAGS
262
+ --json Format output as json.
263
+
264
+ DESCRIPTION
265
+ Show one planned routine
266
+ ```
267
+
268
+ _See code: [src/commands/routines/show.ts](https://github.com/nickchristensen/cliftin/blob/v4.0.0/src/commands/routines/show.ts)_
168
269
 
169
270
  ## `cliftin workouts list`
170
271
 
@@ -191,11 +292,11 @@ DESCRIPTION
191
292
  List workouts
192
293
  ```
193
294
 
194
- _See code: [src/commands/workouts/list.ts](https://github.com/nickchristensen/cliftin/blob/v3.0.0/src/commands/workouts/list.ts)_
295
+ _See code: [src/commands/workouts/list.ts](https://github.com/nickchristensen/cliftin/blob/v4.0.0/src/commands/workouts/list.ts)_
195
296
 
196
297
  ## `cliftin workouts next`
197
298
 
198
- Show the up-next routine from the active program
299
+ Redirect to routines next
199
300
 
200
301
  ```
201
302
  USAGE
@@ -205,10 +306,10 @@ GLOBAL FLAGS
205
306
  --json Format output as json.
206
307
 
207
308
  DESCRIPTION
208
- Show the up-next routine from the active program
309
+ Redirect to routines next
209
310
  ```
210
311
 
211
- _See code: [src/commands/workouts/next.ts](https://github.com/nickchristensen/cliftin/blob/v3.0.0/src/commands/workouts/next.ts)_
312
+ _See code: [src/commands/workouts/next.ts](https://github.com/nickchristensen/cliftin/blob/v4.0.0/src/commands/workouts/next.ts)_
212
313
 
213
314
  ## `cliftin workouts show [WORKOUTID]`
214
315
 
@@ -228,5 +329,5 @@ DESCRIPTION
228
329
  Show one workout with exercises and sets
229
330
  ```
230
331
 
231
- _See code: [src/commands/workouts/show.ts](https://github.com/nickchristensen/cliftin/blob/v3.0.0/src/commands/workouts/show.ts)_
332
+ _See code: [src/commands/workouts/show.ts](https://github.com/nickchristensen/cliftin/blob/v4.0.0/src/commands/workouts/show.ts)_
232
333
  <!-- commandsstop -->
@@ -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,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class RoutinesLatest extends Command {
3
+ static description: string;
4
+ static enableJsonFlag: boolean;
5
+ run(): Promise<unknown | void>;
6
+ }
@@ -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,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class RoutinesNext extends Command {
3
+ static description: string;
4
+ static enableJsonFlag: boolean;
5
+ run(): Promise<unknown | void>;
6
+ }
@@ -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 = 'Show the up-next routine from the active program';
3
+ static description = 'Redirect to routines next';
10
4
  static enableJsonFlag = true;
11
5
  async run() {
12
6
  await this.parse(WorkoutsNext);
13
- const context = openDb();
14
- try {
15
- const detail = await getNextWorkoutDetail(context.db);
16
- const unitPreference = await resolveProgramWeightUnit(context.db, detail.program.id);
17
- const unitLabel = weightUnitLabel(unitPreference);
18
- if (this.jsonEnabled()) {
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,8 +1,8 @@
1
- import { ExerciseHistoryRow, NextWorkoutDetail, ProgramDetailTree, WorkoutDetail } from './types.js';
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 serializeNextWorkoutDetailWithWeightUnits(detail: NextWorkoutDetail, unitPreference: UnitPreference): unknown;
7
+ export declare function serializeRoutineDetailWithWeightUnits(detail: RoutineDetail, unitPreference: UnitPreference): unknown;
8
8
  export declare function serializeWorkoutDetailWithWeightUnits(detail: WorkoutDetail, unitPreference: UnitPreference): unknown;
@@ -31,9 +31,9 @@ export function serializeProgramDetailWithWeightUnits(detail, unitPreference) {
31
31
  })),
32
32
  };
33
33
  }
34
- export function serializeNextWorkoutDetailWithWeightUnits(detail, unitPreference) {
34
+ export function serializeRoutineDetailWithWeightUnits(detail, unitPreference) {
35
35
  const program = omitId(detail.program);
36
- return {
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
+ }
@@ -44,6 +44,7 @@ export async function getNextWorkoutDetail(db) {
44
44
  id: week.id,
45
45
  number: weekIndex + 1,
46
46
  },
47
+ workout: null,
47
48
  };
48
49
  }
49
50
  export async function listWorkouts(db, filters) {
@@ -0,0 +1,2 @@
1
+ import { ProgramRoutine } from './types.js';
2
+ export declare function buildRoutineRows(routine: ProgramRoutine, unitLabel: string): Array<Record<string, unknown>>;
@@ -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
+ }
@@ -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;
@@ -241,6 +241,184 @@
241
241
  "show.js"
242
242
  ]
243
243
  },
244
+ "routines:from-workout": {
245
+ "aliases": [],
246
+ "args": {
247
+ "workoutId": {
248
+ "description": "workout id (default: latest workout)",
249
+ "name": "workoutId",
250
+ "required": false
251
+ }
252
+ },
253
+ "description": "Show the planned routine for a completed workout",
254
+ "flags": {
255
+ "json": {
256
+ "description": "Format output as json.",
257
+ "helpGroup": "GLOBAL",
258
+ "name": "json",
259
+ "allowNo": false,
260
+ "type": "boolean"
261
+ }
262
+ },
263
+ "hasDynamicHelp": false,
264
+ "hiddenAliases": [],
265
+ "id": "routines:from-workout",
266
+ "pluginAlias": "@nickchristensen/cliftin",
267
+ "pluginName": "@nickchristensen/cliftin",
268
+ "pluginType": "core",
269
+ "strict": true,
270
+ "enableJsonFlag": true,
271
+ "isESM": true,
272
+ "relativePath": [
273
+ "dist",
274
+ "commands",
275
+ "routines",
276
+ "from-workout.js"
277
+ ]
278
+ },
279
+ "routines:latest": {
280
+ "aliases": [],
281
+ "args": {},
282
+ "description": "Show the planned routine for the latest workout",
283
+ "flags": {
284
+ "json": {
285
+ "description": "Format output as json.",
286
+ "helpGroup": "GLOBAL",
287
+ "name": "json",
288
+ "allowNo": false,
289
+ "type": "boolean"
290
+ }
291
+ },
292
+ "hasDynamicHelp": false,
293
+ "hiddenAliases": [],
294
+ "id": "routines:latest",
295
+ "pluginAlias": "@nickchristensen/cliftin",
296
+ "pluginName": "@nickchristensen/cliftin",
297
+ "pluginType": "core",
298
+ "strict": true,
299
+ "enableJsonFlag": true,
300
+ "isESM": true,
301
+ "relativePath": [
302
+ "dist",
303
+ "commands",
304
+ "routines",
305
+ "latest.js"
306
+ ]
307
+ },
308
+ "routines:list": {
309
+ "aliases": [],
310
+ "args": {},
311
+ "description": "List planned routines",
312
+ "flags": {
313
+ "json": {
314
+ "description": "Format output as json.",
315
+ "helpGroup": "GLOBAL",
316
+ "name": "json",
317
+ "allowNo": false,
318
+ "type": "boolean"
319
+ },
320
+ "name": {
321
+ "description": "Filter by routine name contains",
322
+ "name": "name",
323
+ "hasDynamicHelp": false,
324
+ "multiple": false,
325
+ "type": "option"
326
+ },
327
+ "program": {
328
+ "description": "Filter by program id or name",
329
+ "name": "program",
330
+ "hasDynamicHelp": false,
331
+ "multiple": false,
332
+ "type": "option"
333
+ },
334
+ "week": {
335
+ "description": "Filter by week number",
336
+ "name": "week",
337
+ "hasDynamicHelp": false,
338
+ "multiple": false,
339
+ "type": "option"
340
+ }
341
+ },
342
+ "hasDynamicHelp": false,
343
+ "hiddenAliases": [],
344
+ "id": "routines:list",
345
+ "pluginAlias": "@nickchristensen/cliftin",
346
+ "pluginName": "@nickchristensen/cliftin",
347
+ "pluginType": "core",
348
+ "strict": true,
349
+ "enableJsonFlag": true,
350
+ "isESM": true,
351
+ "relativePath": [
352
+ "dist",
353
+ "commands",
354
+ "routines",
355
+ "list.js"
356
+ ]
357
+ },
358
+ "routines:next": {
359
+ "aliases": [],
360
+ "args": {},
361
+ "description": "Show the up-next routine from the active program",
362
+ "flags": {
363
+ "json": {
364
+ "description": "Format output as json.",
365
+ "helpGroup": "GLOBAL",
366
+ "name": "json",
367
+ "allowNo": false,
368
+ "type": "boolean"
369
+ }
370
+ },
371
+ "hasDynamicHelp": false,
372
+ "hiddenAliases": [],
373
+ "id": "routines:next",
374
+ "pluginAlias": "@nickchristensen/cliftin",
375
+ "pluginName": "@nickchristensen/cliftin",
376
+ "pluginType": "core",
377
+ "strict": true,
378
+ "enableJsonFlag": true,
379
+ "isESM": true,
380
+ "relativePath": [
381
+ "dist",
382
+ "commands",
383
+ "routines",
384
+ "next.js"
385
+ ]
386
+ },
387
+ "routines:show": {
388
+ "aliases": [],
389
+ "args": {
390
+ "selector": {
391
+ "description": "routine id or name",
392
+ "name": "selector",
393
+ "required": true
394
+ }
395
+ },
396
+ "description": "Show one planned routine",
397
+ "flags": {
398
+ "json": {
399
+ "description": "Format output as json.",
400
+ "helpGroup": "GLOBAL",
401
+ "name": "json",
402
+ "allowNo": false,
403
+ "type": "boolean"
404
+ }
405
+ },
406
+ "hasDynamicHelp": false,
407
+ "hiddenAliases": [],
408
+ "id": "routines:show",
409
+ "pluginAlias": "@nickchristensen/cliftin",
410
+ "pluginName": "@nickchristensen/cliftin",
411
+ "pluginType": "core",
412
+ "strict": true,
413
+ "enableJsonFlag": true,
414
+ "isESM": true,
415
+ "relativePath": [
416
+ "dist",
417
+ "commands",
418
+ "routines",
419
+ "show.js"
420
+ ]
421
+ },
244
422
  "workouts:list": {
245
423
  "aliases": [],
246
424
  "args": {},
@@ -337,7 +515,7 @@
337
515
  "workouts:next": {
338
516
  "aliases": [],
339
517
  "args": {},
340
- "description": "Show the up-next routine from the active program",
518
+ "description": "Redirect to routines next",
341
519
  "flags": {
342
520
  "json": {
343
521
  "description": "Format output as json.",
@@ -399,5 +577,5 @@
399
577
  ]
400
578
  }
401
579
  },
402
- "version": "3.0.0"
580
+ "version": "4.0.0"
403
581
  }
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": "3.0.0",
4
+ "version": "4.0.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
  }