@nickchristensen/cliftin 1.2.0 → 1.3.1

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
@@ -5,8 +5,8 @@ CLIftin: A read-only CLI for Liftin'
5
5
 
6
6
 
7
7
  [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io)
8
- [![Version](https://img.shields.io/npm/v/@nickchristensen/cliftin.svg)](https://npmjs.org/package/cliftin)
9
- [![Downloads/week](https://img.shields.io/npm/dw/@nickchristensen/cliftin.svg)](https://npmjs.org/package/cliftin)
8
+ [![Version](https://img.shields.io/npm/v/@nickchristensen/cliftin.svg)](https://npmjs.org/package/@nickchristensen/cliftin)
9
+ [![Downloads/week](https://img.shields.io/npm/dw/@nickchristensen/cliftin.svg)](https://npmjs.org/package/@nickchristensen/cliftin)
10
10
 
11
11
 
12
12
  <!-- toc -->
@@ -47,6 +47,7 @@ USAGE
47
47
  * [`cliftin programs list`](#cliftin-programs-list)
48
48
  * [`cliftin programs show [SELECTOR]`](#cliftin-programs-show-selector)
49
49
  * [`cliftin workouts list`](#cliftin-workouts-list)
50
+ * [`cliftin workouts next`](#cliftin-workouts-next)
50
51
  * [`cliftin workouts show [WORKOUTID]`](#cliftin-workouts-show-workoutid)
51
52
 
52
53
  ## `cliftin exercises list`
@@ -72,7 +73,7 @@ DESCRIPTION
72
73
  List exercises
73
74
  ```
74
75
 
75
- _See code: [src/commands/exercises/list.ts](https://github.com/nickchristensen/cliftin/blob/v1.2.0/src/commands/exercises/list.ts)_
76
+ _See code: [src/commands/exercises/list.ts](https://github.com/nickchristensen/cliftin/blob/v1.3.1/src/commands/exercises/list.ts)_
76
77
 
77
78
  ## `cliftin exercises show SELECTOR`
78
79
 
@@ -106,7 +107,7 @@ DESCRIPTION
106
107
  Show one exercise detail and history
107
108
  ```
108
109
 
109
- _See code: [src/commands/exercises/show.ts](https://github.com/nickchristensen/cliftin/blob/v1.2.0/src/commands/exercises/show.ts)_
110
+ _See code: [src/commands/exercises/show.ts](https://github.com/nickchristensen/cliftin/blob/v1.3.1/src/commands/exercises/show.ts)_
110
111
 
111
112
  ## `cliftin help [COMMAND]`
112
113
 
@@ -143,7 +144,7 @@ DESCRIPTION
143
144
  List programs
144
145
  ```
145
146
 
146
- _See code: [src/commands/programs/list.ts](https://github.com/nickchristensen/cliftin/blob/v1.2.0/src/commands/programs/list.ts)_
147
+ _See code: [src/commands/programs/list.ts](https://github.com/nickchristensen/cliftin/blob/v1.3.1/src/commands/programs/list.ts)_
147
148
 
148
149
  ## `cliftin programs show [SELECTOR]`
149
150
 
@@ -163,7 +164,7 @@ DESCRIPTION
163
164
  Show one program hierarchy
164
165
  ```
165
166
 
166
- _See code: [src/commands/programs/show.ts](https://github.com/nickchristensen/cliftin/blob/v1.2.0/src/commands/programs/show.ts)_
167
+ _See code: [src/commands/programs/show.ts](https://github.com/nickchristensen/cliftin/blob/v1.3.1/src/commands/programs/show.ts)_
167
168
 
168
169
  ## `cliftin workouts list`
169
170
 
@@ -190,7 +191,24 @@ DESCRIPTION
190
191
  List workouts
191
192
  ```
192
193
 
193
- _See code: [src/commands/workouts/list.ts](https://github.com/nickchristensen/cliftin/blob/v1.2.0/src/commands/workouts/list.ts)_
194
+ _See code: [src/commands/workouts/list.ts](https://github.com/nickchristensen/cliftin/blob/v1.3.1/src/commands/workouts/list.ts)_
195
+
196
+ ## `cliftin workouts next`
197
+
198
+ Show the up-next routine from the active program
199
+
200
+ ```
201
+ USAGE
202
+ $ cliftin workouts next [--json]
203
+
204
+ GLOBAL FLAGS
205
+ --json Format output as json.
206
+
207
+ DESCRIPTION
208
+ Show the up-next routine from the active program
209
+ ```
210
+
211
+ _See code: [src/commands/workouts/next.ts](https://github.com/nickchristensen/cliftin/blob/v1.3.1/src/commands/workouts/next.ts)_
194
212
 
195
213
  ## `cliftin workouts show [WORKOUTID]`
196
214
 
@@ -210,5 +228,5 @@ DESCRIPTION
210
228
  Show one workout with exercises and sets
211
229
  ```
212
230
 
213
- _See code: [src/commands/workouts/show.ts](https://github.com/nickchristensen/cliftin/blob/v1.2.0/src/commands/workouts/show.ts)_
231
+ _See code: [src/commands/workouts/show.ts](https://github.com/nickchristensen/cliftin/blob/v1.3.1/src/commands/workouts/show.ts)_
214
232
  <!-- commandsstop -->
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class WorkoutsNext extends Command {
3
+ static description: string;
4
+ static enableJsonFlag: boolean;
5
+ run(): Promise<unknown | void>;
6
+ }
@@ -0,0 +1,44 @@
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
+ export default class WorkoutsNext extends Command {
9
+ static description = 'Show the up-next routine from the active program';
10
+ static enableJsonFlag = true;
11
+ async run() {
12
+ 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);
42
+ }
43
+ }
44
+ }
@@ -20,7 +20,7 @@ function formatWorkoutDate(dateIso) {
20
20
  }
21
21
  export default class WorkoutsShow extends Command {
22
22
  static args = {
23
- workoutId: Args.string({ description: 'workout id (default: latest workout)', required: false }),
23
+ workoutId: Args.string({ description: 'workout id (default: latest workout)', ignoreStdin: true, required: false }),
24
24
  };
25
25
  static description = 'Show one workout with exercises and sets';
26
26
  static enableJsonFlag = true;
package/dist/lib/db.d.ts CHANGED
@@ -60,6 +60,7 @@ export interface DatabaseSchema {
60
60
  ZNAME: null | string;
61
61
  ZPERIOD: null | number;
62
62
  ZSOFTDELETED: null | number;
63
+ ZUPNEXT: null | number;
63
64
  ZWORKOUTPLAN: null | number;
64
65
  };
65
66
  ZSETCONFIGURATION: {
@@ -1,7 +1,8 @@
1
- import { ExerciseHistoryRow, ProgramDetailTree, WorkoutDetail } from './types.js';
1
+ import { ExerciseHistoryRow, NextWorkoutDetail, ProgramDetailTree, 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
8
  export declare function serializeWorkoutDetailWithWeightUnits(detail: WorkoutDetail, unitPreference: UnitPreference): unknown;
@@ -24,6 +24,22 @@ export function serializeProgramDetailWithWeightUnits(detail, unitPreference) {
24
24
  })),
25
25
  };
26
26
  }
27
+ export function serializeNextWorkoutDetailWithWeightUnits(detail, unitPreference) {
28
+ return {
29
+ ...detail,
30
+ routine: {
31
+ ...detail.routine,
32
+ exercises: detail.routine.exercises.map((exercise) => ({
33
+ ...exercise,
34
+ plannedWeight: withWeightUnit(exercise.plannedWeight, unitPreference),
35
+ sets: exercise.sets.map((set) => ({
36
+ ...set,
37
+ weight: withWeightUnit(set.weight, unitPreference),
38
+ })),
39
+ })),
40
+ },
41
+ };
42
+ }
27
43
  export function serializeWorkoutDetailWithWeightUnits(detail, unitPreference) {
28
44
  return {
29
45
  ...detail,
@@ -1,6 +1,6 @@
1
1
  import { Kysely } from 'kysely';
2
2
  import { DatabaseSchema } from '../db.js';
3
- import { WorkoutDetail, WorkoutSummary } from '../types.js';
3
+ import { NextWorkoutDetail, WorkoutDetail, WorkoutSummary } from '../types.js';
4
4
  export type WorkoutFilters = {
5
5
  from?: string;
6
6
  limit?: number;
@@ -9,5 +9,6 @@ export type WorkoutFilters = {
9
9
  routine?: string;
10
10
  to?: string;
11
11
  };
12
+ export declare function getNextWorkoutDetail(db: Kysely<DatabaseSchema>): Promise<NextWorkoutDetail>;
12
13
  export declare function listWorkouts(db: Kysely<DatabaseSchema>, filters: WorkoutFilters): Promise<WorkoutSummary[]>;
13
14
  export declare function getWorkoutDetail(db: Kysely<DatabaseSchema>, workoutId: number): Promise<WorkoutDetail>;
@@ -2,10 +2,50 @@ import { formatExerciseDisplayName } from '../names.js';
2
2
  import { normalizeRpe } from '../rpe.js';
3
3
  import { appleSecondsToIso, dateRangeToAppleSeconds } from '../time.js';
4
4
  import { convertKgToDisplayVolume, convertKgToDisplayWeight, resolveGlobalWeightUnit } from '../units.js';
5
+ import { getProgramDetail, resolveProgramSelector } from './programs.js';
5
6
  import { resolveIdOrName } from './selectors.js';
6
7
  function asBool(value) {
7
8
  return value === 1;
8
9
  }
10
+ export async function getNextWorkoutDetail(db) {
11
+ const programId = await resolveProgramSelector(db, undefined, true);
12
+ const programDetail = await getProgramDetail(db, programId);
13
+ const nextRoutines = await db
14
+ .selectFrom('ZROUTINE as r')
15
+ .leftJoin('ZPERIOD as p', 'p.Z_PK', 'r.ZPERIOD')
16
+ .select(['r.Z_PK as id', 'r.ZPERIOD as weekId'])
17
+ .where('r.ZUPNEXT', '=', 1)
18
+ .where('r.ZSOFTDELETED', 'is not', 1)
19
+ .where((eb) => eb.or([eb('p.ZWORKOUTPLAN', '=', programId), eb('r.ZWORKOUTPLAN', '=', programId)]))
20
+ .orderBy('p.Z_FOK_WORKOUTPLAN', 'asc')
21
+ .orderBy('r.Z_FOK_PERIOD', 'asc')
22
+ .orderBy('r.Z_PK', 'asc')
23
+ .execute();
24
+ if (nextRoutines.length === 0) {
25
+ throw new Error(`No up-next routine found for active program ${programDetail.program.name}.`);
26
+ }
27
+ if (nextRoutines.length > 1) {
28
+ throw new Error(`Expected exactly one up-next routine for active program ${programDetail.program.name}. Found ${nextRoutines.length}.`);
29
+ }
30
+ const nextRoutine = nextRoutines[0];
31
+ const weekIndex = programDetail.weeks.findIndex((week) => week.id === nextRoutine.weekId);
32
+ if (weekIndex === -1) {
33
+ throw new Error(`Up-next routine ${nextRoutine.id} is linked to unknown week ${nextRoutine.weekId}.`);
34
+ }
35
+ const week = programDetail.weeks[weekIndex];
36
+ const routine = week.routines.find((entry) => entry.id === nextRoutine.id);
37
+ if (!routine) {
38
+ throw new Error(`Up-next routine ${nextRoutine.id} was not found in active program detail.`);
39
+ }
40
+ return {
41
+ program: programDetail.program,
42
+ routine,
43
+ week: {
44
+ id: week.id,
45
+ number: weekIndex + 1,
46
+ },
47
+ };
48
+ }
9
49
  export async function listWorkouts(db, filters) {
10
50
  const dateRange = dateRangeToAppleSeconds({ from: filters.from, on: filters.on, to: filters.to });
11
51
  let query = db
@@ -68,6 +68,14 @@ export type WorkoutDetail = {
68
68
  program: null | string;
69
69
  routine: null | string;
70
70
  };
71
+ export type NextWorkoutDetail = {
72
+ program: ProgramSummary;
73
+ routine: ProgramRoutine;
74
+ week: {
75
+ id: number;
76
+ number: number;
77
+ };
78
+ };
71
79
  export type ExerciseSummary = {
72
80
  equipment: null | string;
73
81
  id: number;
@@ -334,6 +334,35 @@
334
334
  "list.js"
335
335
  ]
336
336
  },
337
+ "workouts:next": {
338
+ "aliases": [],
339
+ "args": {},
340
+ "description": "Show the up-next routine from the active program",
341
+ "flags": {
342
+ "json": {
343
+ "description": "Format output as json.",
344
+ "helpGroup": "GLOBAL",
345
+ "name": "json",
346
+ "allowNo": false,
347
+ "type": "boolean"
348
+ }
349
+ },
350
+ "hasDynamicHelp": false,
351
+ "hiddenAliases": [],
352
+ "id": "workouts:next",
353
+ "pluginAlias": "@nickchristensen/cliftin",
354
+ "pluginName": "@nickchristensen/cliftin",
355
+ "pluginType": "core",
356
+ "strict": true,
357
+ "enableJsonFlag": true,
358
+ "isESM": true,
359
+ "relativePath": [
360
+ "dist",
361
+ "commands",
362
+ "workouts",
363
+ "next.js"
364
+ ]
365
+ },
337
366
  "workouts:show": {
338
367
  "aliases": [],
339
368
  "args": {
@@ -370,5 +399,5 @@
370
399
  ]
371
400
  }
372
401
  },
373
- "version": "1.2.0"
402
+ "version": "1.3.1"
374
403
  }
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": "1.2.0",
4
+ "version": "1.3.1",
5
5
  "author": "Nick Christensen",
6
6
  "bin": {
7
7
  "cliftin": "./bin/run.js"
@@ -22,13 +22,13 @@
22
22
  "@oclif/test": "^4",
23
23
  "@types/better-sqlite3": "^7.6.13",
24
24
  "@types/chai": "^4",
25
- "@types/mocha": "^10",
25
+ "@types/mocha": "^10.0.10",
26
26
  "@types/node": "^24.10.13",
27
27
  "chai": "^4",
28
28
  "eslint": "^9",
29
29
  "eslint-config-oclif": "^6",
30
30
  "eslint-config-prettier": "^10",
31
- "mocha": "^10",
31
+ "mocha": "^11.7.5",
32
32
  "oclif": "^4",
33
33
  "shx": "^0.3.3",
34
34
  "simple-git-hooks": "^2.13.1",