kahunas-cli 1.0.6 → 1.3.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 +15 -167
- package/dist/cli.js +470 -202
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,188 +1,36 @@
|
|
|
1
1
|
# Kahunas CLI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-

|
|
3
|
+
Fetch workouts from Kahunas and preview them locally.
|
|
6
4
|
|
|
7
5
|
## Quick start
|
|
8
6
|
|
|
9
|
-
1) Install
|
|
7
|
+
1) Install and build:
|
|
10
8
|
|
|
11
9
|
```bash
|
|
12
10
|
pnpm install
|
|
13
11
|
pnpm build
|
|
14
12
|
```
|
|
15
13
|
|
|
16
|
-
2)
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
pnpm kahunas -- auth login
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
3) Fetch data:
|
|
14
|
+
2) Optional: add auto-login credentials at `~/.config/kahunas/auth.json`:
|
|
23
15
|
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
You can also run without installing globally:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
npx kahunas-cli checkins list
|
|
34
|
-
npx kahunas-cli workout events
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"email": "you@example.com",
|
|
19
|
+
"password": "your-password"
|
|
20
|
+
}
|
|
35
21
|
```
|
|
36
22
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
### Auth
|
|
40
|
-
|
|
41
|
-
- `kahunas auth login`
|
|
42
|
-
- Opens a browser, lets you log in, and saves the `auth-user-token`.
|
|
43
|
-
- `kahunas auth status`
|
|
44
|
-
- Checks whether the stored token is valid.
|
|
45
|
-
- `kahunas auth show`
|
|
46
|
-
- Prints the stored token.
|
|
47
|
-
|
|
48
|
-
Tokens are saved to:
|
|
49
|
-
|
|
50
|
-
- `~/.config/kahunas/config.json`
|
|
51
|
-
|
|
52
|
-
### Check-ins
|
|
53
|
-
|
|
54
|
-
- `kahunas checkins list`
|
|
55
|
-
- Lists recent check-ins.
|
|
56
|
-
|
|
57
|
-
### Workouts
|
|
58
|
-
|
|
59
|
-
- `kahunas workout list`
|
|
60
|
-
- Lists workout programs.
|
|
61
|
-
- `kahunas workout pick`
|
|
62
|
-
- Shows a numbered list and lets you choose a program.
|
|
63
|
-
- `kahunas workout latest`
|
|
64
|
-
- Loads the most recently updated program.
|
|
65
|
-
- `kahunas workout events`
|
|
66
|
-
- Lists workout log events with dates and a human-friendly workout summary (from the calendar endpoint).
|
|
67
|
-
- `kahunas workout serve`
|
|
68
|
-
- Starts a local dev server with a workout preview page and a JSON endpoint that matches the CLI output.
|
|
69
|
-
- `kahunas workout program <id>`
|
|
70
|
-
- Fetches a program by UUID.
|
|
71
|
-
|
|
72
|
-
### Workout sync (browser capture)
|
|
73
|
-
|
|
74
|
-
If the API list is missing a program you see in the web UI, run:
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
pnpm kahunas -- workout sync
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
This opens a browser, you log in, then navigate to your workouts page. After you press Enter, the CLI captures the workout list from network responses and writes a cache:
|
|
81
|
-
|
|
82
|
-
- `~/.config/kahunas/workouts.json`
|
|
83
|
-
|
|
84
|
-
`workout list`, `workout pick`, and `workout latest` automatically merge the API list with this cache.
|
|
85
|
-
Raw output (`--raw`) prints the API response only.
|
|
86
|
-
|
|
87
|
-
### Workout events (dates)
|
|
88
|
-
|
|
89
|
-
To see when workouts happened, the calendar endpoint returns log events with timestamps. By default each event is summarized into a human-friendly structure (total volume sets, exercises, supersets). Use `--full` to return the full program payload (best effort; falls back to cached summary if needed).
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
pnpm kahunas -- workout events --user <user-uuid>
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Or via pnpm:
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
pnpm kahunas -- workout events
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
Default timezone is `Europe/London`. Override with `--timezone`.
|
|
102
|
-
|
|
103
|
-
You can filter by program or workout UUID:
|
|
23
|
+
3) Sync workouts, then run the preview server:
|
|
104
24
|
|
|
105
25
|
```bash
|
|
106
|
-
pnpm kahunas
|
|
107
|
-
pnpm kahunas
|
|
26
|
+
pnpm kahunas sync
|
|
27
|
+
pnpm kahunas serve
|
|
108
28
|
```
|
|
109
29
|
|
|
110
|
-
|
|
30
|
+
Open `http://127.0.0.1:3000`.
|
|
111
31
|
|
|
112
|
-
If
|
|
113
|
-
|
|
114
|
-
- `KAHUNAS_USER_UUID=...`
|
|
115
|
-
- `--user <uuid>`
|
|
116
|
-
|
|
117
|
-
### Workout preview server
|
|
118
|
-
|
|
119
|
-
Run a local dev server to preview workouts in a browser:
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
pnpm kahunas -- workout serve
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
The HTML page is available at `http://127.0.0.1:3000` and the JSON endpoint is at `http://127.0.0.1:3000/api/workout`.
|
|
126
|
-
The JSON response matches the CLI output for `workout events --latest`, so there is only one data shape to maintain.
|
|
127
|
-
|
|
128
|
-
Options:
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
pnpm kahunas -- workout serve --program <program-uuid>
|
|
132
|
-
pnpm kahunas -- workout serve --workout <workout-uuid>
|
|
133
|
-
pnpm kahunas -- workout serve --limit 3
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Use `?day=<index>` to switch the selected workout day tab in the browser.
|
|
137
|
-
|
|
138
|
-
## Auto-login
|
|
139
|
-
|
|
140
|
-
Most commands auto-login by default if a token is missing or expired. To disable:
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
pnpm kahunas -- checkins list --no-auto-login
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
## Flags
|
|
147
|
-
|
|
148
|
-
- `--raw` prints raw API responses (no formatting).
|
|
149
|
-
- `--headless` runs Playwright without a visible browser window.
|
|
150
|
-
|
|
151
|
-
## Environment variables
|
|
152
|
-
|
|
153
|
-
- `KAHUNAS_TOKEN`
|
|
154
|
-
- `KAHUNAS_CSRF`
|
|
155
|
-
- `KAHUNAS_CSRF_COOKIE`
|
|
156
|
-
- `KAHUNAS_COOKIE`
|
|
157
|
-
- `KAHUNAS_WEB_BASE_URL`
|
|
158
|
-
- `KAHUNAS_USER_UUID`
|
|
159
|
-
|
|
160
|
-
## Playwright
|
|
161
|
-
|
|
162
|
-
If Playwright did not download a browser during install:
|
|
163
|
-
|
|
164
|
-
```bash
|
|
165
|
-
pnpm exec playwright install chromium
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
## Testing
|
|
169
|
-
|
|
170
|
-
Run the unit tests with:
|
|
171
|
-
|
|
172
|
-
```bash
|
|
173
|
-
pnpm test
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
## Publishing
|
|
177
|
-
|
|
178
|
-
The npm package is built from `dist/cli.js`. Publishing runs the build automatically:
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
pnpm publish
|
|
182
|
-
```
|
|
32
|
+
If `auth.json` is missing, `sync` will prompt for credentials and save them.
|
|
183
33
|
|
|
184
|
-
##
|
|
34
|
+
## Advanced usage
|
|
185
35
|
|
|
186
|
-
|
|
187
|
-
- `auth login` is the most reliable way to refresh the token.
|
|
188
|
-
- `workout events` relies on session cookies captured during `auth login`.
|
|
36
|
+
See `docs/advanced.md` for all commands, flags, and configuration options.
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
9
10
|
var __copyProps = (to, from, except, desc) => {
|
|
10
11
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
12
|
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
@@ -68,17 +69,13 @@ function isFlagEnabled(options, name) {
|
|
|
68
69
|
const value = options[name];
|
|
69
70
|
return value === "true" || value === "1" || value === "yes";
|
|
70
71
|
}
|
|
71
|
-
function shouldAutoLogin(options, defaultValue) {
|
|
72
|
-
if (isFlagEnabled(options, "auto-login")) return true;
|
|
73
|
-
if (isFlagEnabled(options, "no-auto-login")) return false;
|
|
74
|
-
return defaultValue;
|
|
75
|
-
}
|
|
76
72
|
|
|
77
73
|
//#endregion
|
|
78
74
|
//#region src/config.ts
|
|
79
75
|
const DEFAULT_BASE_URL = "https://api.kahunas.io";
|
|
80
76
|
const DEFAULT_WEB_BASE_URL = "https://kahunas.io";
|
|
81
77
|
const CONFIG_PATH = node_path.join(node_os.homedir(), ".config", "kahunas", "config.json");
|
|
78
|
+
const AUTH_PATH = node_path.join(node_os.homedir(), ".config", "kahunas", "auth.json");
|
|
82
79
|
const WORKOUT_CACHE_PATH = node_path.join(node_os.homedir(), ".config", "kahunas", "workouts.json");
|
|
83
80
|
function readConfig() {
|
|
84
81
|
if (!node_fs.existsSync(CONFIG_PATH)) return {};
|
|
@@ -89,6 +86,20 @@ function readConfig() {
|
|
|
89
86
|
throw new Error(`Invalid JSON in ${CONFIG_PATH}.`);
|
|
90
87
|
}
|
|
91
88
|
}
|
|
89
|
+
function readAuthConfig() {
|
|
90
|
+
if (!node_fs.existsSync(AUTH_PATH)) return;
|
|
91
|
+
const raw = node_fs.readFileSync(AUTH_PATH, "utf-8");
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(raw);
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error(`Invalid JSON in ${AUTH_PATH}.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function writeAuthConfig(auth) {
|
|
99
|
+
const dir = node_path.dirname(AUTH_PATH);
|
|
100
|
+
node_fs.mkdirSync(dir, { recursive: true });
|
|
101
|
+
node_fs.writeFileSync(AUTH_PATH, `${JSON.stringify(auth, null, 2)}\n`, "utf-8");
|
|
102
|
+
}
|
|
92
103
|
function writeConfig(config) {
|
|
93
104
|
const dir = node_path.dirname(CONFIG_PATH);
|
|
94
105
|
node_fs.mkdirSync(dir, { recursive: true });
|
|
@@ -113,26 +124,26 @@ function writeWorkoutCache(plans) {
|
|
|
113
124
|
node_fs.writeFileSync(WORKOUT_CACHE_PATH, `${JSON.stringify(cache, null, 2)}\n`, "utf-8");
|
|
114
125
|
return cache;
|
|
115
126
|
}
|
|
116
|
-
function resolveToken(
|
|
117
|
-
return
|
|
127
|
+
function resolveToken(_, config) {
|
|
128
|
+
return config.token;
|
|
118
129
|
}
|
|
119
|
-
function resolveCsrfToken(
|
|
120
|
-
return
|
|
130
|
+
function resolveCsrfToken(_, config) {
|
|
131
|
+
return config.csrfToken;
|
|
121
132
|
}
|
|
122
|
-
function resolveCsrfCookie(
|
|
123
|
-
return
|
|
133
|
+
function resolveCsrfCookie(_, config) {
|
|
134
|
+
return config.csrfCookie;
|
|
124
135
|
}
|
|
125
|
-
function resolveAuthCookie(
|
|
126
|
-
return
|
|
136
|
+
function resolveAuthCookie(_, config) {
|
|
137
|
+
return config.authCookie;
|
|
127
138
|
}
|
|
128
|
-
function resolveUserUuid(
|
|
129
|
-
return
|
|
139
|
+
function resolveUserUuid(_, config) {
|
|
140
|
+
return config.userUuid;
|
|
130
141
|
}
|
|
131
142
|
function resolveBaseUrl(options, config) {
|
|
132
|
-
return
|
|
143
|
+
return config.baseUrl ?? DEFAULT_BASE_URL;
|
|
133
144
|
}
|
|
134
145
|
function resolveWebBaseUrl(options, config) {
|
|
135
|
-
return
|
|
146
|
+
return config.webBaseUrl ?? DEFAULT_WEB_BASE_URL;
|
|
136
147
|
}
|
|
137
148
|
|
|
138
149
|
//#endregion
|
|
@@ -172,6 +183,37 @@ function isLikelyLoginHtml(text) {
|
|
|
172
183
|
if (!trimmed.startsWith("<")) return false;
|
|
173
184
|
return trimmed.includes("login to your account") || trimmed.includes("welcome back") || trimmed.includes("<title>kahunas");
|
|
174
185
|
}
|
|
186
|
+
function extractJwtExpiry(token) {
|
|
187
|
+
const parts = token.split(".");
|
|
188
|
+
if (parts.length < 2) return;
|
|
189
|
+
const payload = decodeBase64Url(parts[1]);
|
|
190
|
+
if (!payload) return;
|
|
191
|
+
try {
|
|
192
|
+
const data = JSON.parse(payload);
|
|
193
|
+
if (typeof data.exp !== "number" || !Number.isFinite(data.exp)) return;
|
|
194
|
+
return (/* @__PURE__ */ new Date(data.exp * 1e3)).toISOString();
|
|
195
|
+
} catch {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const DEFAULT_TOKEN_TTL_MS = 7200 * 1e3;
|
|
200
|
+
function resolveTokenExpiry(token, tokenUpdatedAt) {
|
|
201
|
+
const jwtExpiry = extractJwtExpiry(token);
|
|
202
|
+
if (jwtExpiry) return jwtExpiry;
|
|
203
|
+
const updatedAt = Date.parse(tokenUpdatedAt);
|
|
204
|
+
if (!Number.isFinite(updatedAt)) return;
|
|
205
|
+
return new Date(updatedAt + DEFAULT_TOKEN_TTL_MS).toISOString();
|
|
206
|
+
}
|
|
207
|
+
function decodeBase64Url(value) {
|
|
208
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
209
|
+
const padding = normalized.length % 4;
|
|
210
|
+
const padded = padding === 0 ? normalized : normalized + "=".repeat(4 - padding);
|
|
211
|
+
try {
|
|
212
|
+
return Buffer.from(padded, "base64").toString("utf-8");
|
|
213
|
+
} catch {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
175
217
|
|
|
176
218
|
//#endregion
|
|
177
219
|
//#region src/http.ts
|
|
@@ -249,6 +291,20 @@ async function fetchAuthToken(csrfToken, cookieHeader, webBaseUrl) {
|
|
|
249
291
|
};
|
|
250
292
|
}
|
|
251
293
|
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/output.ts
|
|
296
|
+
function printResponse(response, rawOutput) {
|
|
297
|
+
if (rawOutput) {
|
|
298
|
+
console.log(response.text);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (response.json !== void 0) {
|
|
302
|
+
console.log(JSON.stringify(response.json, null, 2));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
console.log(response.text);
|
|
306
|
+
}
|
|
307
|
+
|
|
252
308
|
//#endregion
|
|
253
309
|
//#region src/responses.ts
|
|
254
310
|
function isTokenExpiredResponse(payload) {
|
|
@@ -272,18 +328,107 @@ function extractUserUuidFromCheckins(payload) {
|
|
|
272
328
|
}
|
|
273
329
|
|
|
274
330
|
//#endregion
|
|
275
|
-
//#region
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
331
|
+
//#region node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js
|
|
332
|
+
var require_picocolors = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
333
|
+
let p = process || {}, argv = p.argv || [], env = p.env || {};
|
|
334
|
+
let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
335
|
+
let formatter = (open, close, replace = open) => (input) => {
|
|
336
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
337
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
338
|
+
};
|
|
339
|
+
let replaceClose = (string, close, replace, index) => {
|
|
340
|
+
let result = "", cursor = 0;
|
|
341
|
+
do {
|
|
342
|
+
result += string.substring(cursor, index) + replace;
|
|
343
|
+
cursor = index + close.length;
|
|
344
|
+
index = string.indexOf(close, cursor);
|
|
345
|
+
} while (~index);
|
|
346
|
+
return result + string.substring(cursor);
|
|
347
|
+
};
|
|
348
|
+
let createColors = (enabled = isColorSupported) => {
|
|
349
|
+
let f = enabled ? formatter : () => String;
|
|
350
|
+
return {
|
|
351
|
+
isColorSupported: enabled,
|
|
352
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
353
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
354
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
355
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
356
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
357
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
358
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
359
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
360
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
361
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
362
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
363
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
364
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
365
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
366
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
367
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
368
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
369
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
370
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
371
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
372
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
373
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
374
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
375
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
376
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
377
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
378
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
379
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
380
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
381
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
382
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
383
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
384
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
385
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
386
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
387
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
388
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
389
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
390
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
391
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
392
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
module.exports = createColors();
|
|
396
|
+
module.exports.createColors = createColors;
|
|
397
|
+
}));
|
|
398
|
+
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/logger.ts
|
|
401
|
+
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors());
|
|
402
|
+
function formatLabel(label, color) {
|
|
403
|
+
return import_picocolors.default.bold(color(label));
|
|
404
|
+
}
|
|
405
|
+
function logInfo(message) {
|
|
406
|
+
console.log(`${formatLabel("info", import_picocolors.default.cyan)} ${message}`);
|
|
407
|
+
}
|
|
408
|
+
function logError(message) {
|
|
409
|
+
console.error(`${formatLabel("error", import_picocolors.default.red)} ${message}`);
|
|
410
|
+
}
|
|
411
|
+
function logDebug(enabled, message) {
|
|
412
|
+
if (!enabled) return;
|
|
413
|
+
console.error(`${formatLabel("debug", import_picocolors.default.gray)} ${message}`);
|
|
281
414
|
}
|
|
282
|
-
function
|
|
415
|
+
function logPlain(message) {
|
|
416
|
+
console.log(message);
|
|
417
|
+
}
|
|
418
|
+
function formatHeading(message) {
|
|
419
|
+
return import_picocolors.default.bold(message);
|
|
420
|
+
}
|
|
421
|
+
function formatDim(message) {
|
|
422
|
+
return import_picocolors.default.dim(message);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region src/utils.ts
|
|
427
|
+
function askQuestion(prompt, output = process.stdout) {
|
|
283
428
|
return new Promise((resolve) => {
|
|
284
429
|
const rl = node_readline.createInterface({
|
|
285
430
|
input: process.stdin,
|
|
286
|
-
output
|
|
431
|
+
output
|
|
287
432
|
});
|
|
288
433
|
rl.question(prompt, (answer) => {
|
|
289
434
|
rl.close();
|
|
@@ -291,8 +436,11 @@ function askQuestion(prompt) {
|
|
|
291
436
|
});
|
|
292
437
|
});
|
|
293
438
|
}
|
|
294
|
-
function waitForEnter(prompt) {
|
|
295
|
-
return askQuestion(prompt).then(() => void 0);
|
|
439
|
+
function waitForEnter(prompt, output = process.stdout) {
|
|
440
|
+
return askQuestion(prompt, output).then(() => void 0);
|
|
441
|
+
}
|
|
442
|
+
function debugLog(enabled, message) {
|
|
443
|
+
logDebug(enabled, message);
|
|
296
444
|
}
|
|
297
445
|
|
|
298
446
|
//#endregion
|
|
@@ -401,9 +549,181 @@ function buildWorkoutPlanIndex(plans) {
|
|
|
401
549
|
|
|
402
550
|
//#endregion
|
|
403
551
|
//#region src/auth.ts
|
|
552
|
+
const LOGIN_SELECTORS = {
|
|
553
|
+
username: [
|
|
554
|
+
"input[name=\"email\"]",
|
|
555
|
+
"input[id=\"email\"]",
|
|
556
|
+
"input[type=\"email\"]",
|
|
557
|
+
"input[autocomplete=\"email\"]",
|
|
558
|
+
"input[autocomplete=\"username\"]",
|
|
559
|
+
"input[name=\"username\"]",
|
|
560
|
+
"input[id=\"username\"]",
|
|
561
|
+
"input[placeholder*=\"email\" i]",
|
|
562
|
+
"input[placeholder*=\"username\" i]",
|
|
563
|
+
"input[type=\"text\"]"
|
|
564
|
+
],
|
|
565
|
+
password: [
|
|
566
|
+
"input[name=\"password\"]",
|
|
567
|
+
"input[id=\"password\"]",
|
|
568
|
+
"input[type=\"password\"]",
|
|
569
|
+
"input[autocomplete=\"current-password\"]"
|
|
570
|
+
],
|
|
571
|
+
submit: [
|
|
572
|
+
"button[type=\"submit\"]",
|
|
573
|
+
"input[type=\"submit\"]",
|
|
574
|
+
"button:has-text(\"Log in\")",
|
|
575
|
+
"button:has-text(\"Login\")",
|
|
576
|
+
"button:has-text(\"Sign in\")",
|
|
577
|
+
"button:has-text(\"Continue\")"
|
|
578
|
+
]
|
|
579
|
+
};
|
|
580
|
+
const PASSWORD_SELECTOR = LOGIN_SELECTORS.password.join(", ");
|
|
581
|
+
const WORKOUT_NAV_SELECTORS = [
|
|
582
|
+
"#client-workout_plan-view-button",
|
|
583
|
+
".select-client-action[data-action=\"workout_program\"]",
|
|
584
|
+
"[data-action=\"workout_program\"]"
|
|
585
|
+
];
|
|
586
|
+
const WORKOUT_NAV_QUERY_SELECTORS = [
|
|
587
|
+
"#client-workout_plan-view-button",
|
|
588
|
+
".select-client-action[data-action=\"workout_program\"]",
|
|
589
|
+
"[data-action=\"workout_program\"]",
|
|
590
|
+
"a.nav-link",
|
|
591
|
+
"button"
|
|
592
|
+
];
|
|
593
|
+
function normalizePath(pathname) {
|
|
594
|
+
if (pathname.startsWith("/")) return pathname;
|
|
595
|
+
return `/${pathname}`;
|
|
596
|
+
}
|
|
597
|
+
function normalizeToken(token) {
|
|
598
|
+
return token.trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1").replace(/^bearer\s+/i, "");
|
|
599
|
+
}
|
|
600
|
+
function resolveStoredAuth() {
|
|
601
|
+
const auth = readAuthConfig();
|
|
602
|
+
if (!auth) return;
|
|
603
|
+
const login = auth.username ?? auth.email;
|
|
604
|
+
if (!login || !auth.password) throw new Error(`Invalid auth.json at ${AUTH_PATH}. Expected \"username\" or \"email\" and \"password\".`);
|
|
605
|
+
return {
|
|
606
|
+
login,
|
|
607
|
+
password: auth.password,
|
|
608
|
+
loginPath: auth.loginPath
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
async function isSelectorVisible(page, selector) {
|
|
612
|
+
try {
|
|
613
|
+
return await page.isVisible(selector);
|
|
614
|
+
} catch {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async function findVisibleSelector(page, selectors) {
|
|
619
|
+
for (const selector of selectors) if (await isSelectorVisible(page, selector)) return selector;
|
|
620
|
+
}
|
|
621
|
+
async function waitForAnyVisibleSelector(page, selectors, timeoutMs) {
|
|
622
|
+
const combined = selectors.join(", ");
|
|
623
|
+
try {
|
|
624
|
+
await page.waitForSelector(combined, {
|
|
625
|
+
state: "visible",
|
|
626
|
+
timeout: timeoutMs
|
|
627
|
+
});
|
|
628
|
+
return true;
|
|
629
|
+
} catch {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function waitForAnySelectorMatch(page, selectors, timeoutMs) {
|
|
634
|
+
const started = Date.now();
|
|
635
|
+
while (Date.now() - started < timeoutMs) {
|
|
636
|
+
for (const selector of selectors) if (await page.$(selector)) return true;
|
|
637
|
+
await page.waitForTimeout(300);
|
|
638
|
+
}
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
async function waitForPasswordFieldGone(page, timeoutMs) {
|
|
642
|
+
const started = Date.now();
|
|
643
|
+
while (Date.now() - started < timeoutMs) {
|
|
644
|
+
if (!await isSelectorVisible(page, PASSWORD_SELECTOR)) return true;
|
|
645
|
+
await page.waitForTimeout(500);
|
|
646
|
+
}
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
async function attemptAutoLogin(page, auth, debug) {
|
|
650
|
+
if (!await waitForAnyVisibleSelector(page, LOGIN_SELECTORS.password, 5e3)) {
|
|
651
|
+
debugLog(debug, "Login form not detected; skipping auto-login.");
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
const usernameSelector = await findVisibleSelector(page, LOGIN_SELECTORS.username);
|
|
655
|
+
const passwordSelector = await findVisibleSelector(page, LOGIN_SELECTORS.password);
|
|
656
|
+
if (!passwordSelector) return false;
|
|
657
|
+
if (usernameSelector) await page.fill(usernameSelector, auth.login);
|
|
658
|
+
await page.fill(passwordSelector, auth.password);
|
|
659
|
+
const submitSelector = await findVisibleSelector(page, LOGIN_SELECTORS.submit);
|
|
660
|
+
if (submitSelector) await page.click(submitSelector);
|
|
661
|
+
else await page.press(passwordSelector, "Enter");
|
|
662
|
+
await page.waitForTimeout(1500);
|
|
663
|
+
debugLog(debug, "Submitted login form.");
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
async function waitForPlans(plans, timeoutMs) {
|
|
667
|
+
const started = Date.now();
|
|
668
|
+
while (Date.now() - started < timeoutMs) {
|
|
669
|
+
if (plans.length > 0) return true;
|
|
670
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
671
|
+
}
|
|
672
|
+
return plans.length > 0;
|
|
673
|
+
}
|
|
674
|
+
async function clickWorkoutNav(page, debug) {
|
|
675
|
+
for (const selector of WORKOUT_NAV_SELECTORS) {
|
|
676
|
+
const locator = page.locator(selector).first();
|
|
677
|
+
if (await locator.count() === 0) continue;
|
|
678
|
+
try {
|
|
679
|
+
await locator.scrollIntoViewIfNeeded();
|
|
680
|
+
} catch {}
|
|
681
|
+
try {
|
|
682
|
+
await locator.click({
|
|
683
|
+
timeout: 2e3,
|
|
684
|
+
force: true
|
|
685
|
+
});
|
|
686
|
+
debugLog(debug, `Clicked workout nav selector: ${selector}`);
|
|
687
|
+
return true;
|
|
688
|
+
} catch {}
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
return await page.evaluate((selectors) => {
|
|
692
|
+
const resolveCandidates = (sel) => Array.from(document.querySelectorAll(sel)).filter((node) => node instanceof HTMLElement);
|
|
693
|
+
const candidates = selectors.flatMap(resolveCandidates);
|
|
694
|
+
const byAction = candidates.find((node) => node.dataset.action === "workout_program");
|
|
695
|
+
const byText = candidates.find((node) => /workout/i.test(node.textContent ?? ""));
|
|
696
|
+
const target = byAction ?? byText;
|
|
697
|
+
if (!target) return false;
|
|
698
|
+
target.dispatchEvent(new MouseEvent("click", {
|
|
699
|
+
bubbles: true,
|
|
700
|
+
cancelable: true,
|
|
701
|
+
view: window
|
|
702
|
+
}));
|
|
703
|
+
target.click();
|
|
704
|
+
return true;
|
|
705
|
+
}, WORKOUT_NAV_QUERY_SELECTORS);
|
|
706
|
+
} catch {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async function triggerWorkoutCapture(page, webOrigin, plans, debug) {
|
|
711
|
+
if (plans.length > 0) return true;
|
|
712
|
+
if (!await waitForAnySelectorMatch(page, WORKOUT_NAV_SELECTORS, 12e3)) {
|
|
713
|
+
debugLog(debug, "Workout nav not detected.");
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
if (!await clickWorkoutNav(page, debug)) debugLog(debug, "Workout nav click failed.");
|
|
717
|
+
await page.waitForTimeout(1e3);
|
|
718
|
+
await waitForPlans(plans, 8e3);
|
|
719
|
+
debugLog(debug, `Workout capture plans=${plans.length}`);
|
|
720
|
+
return plans.length > 0;
|
|
721
|
+
}
|
|
404
722
|
async function captureWorkoutsFromBrowser(options, config) {
|
|
405
723
|
const webBaseUrl = resolveWebBaseUrl(options, config);
|
|
406
|
-
const headless =
|
|
724
|
+
const headless = config.headless ?? true;
|
|
725
|
+
const debug = config.debug === true;
|
|
726
|
+
const storedAuth = resolveStoredAuth();
|
|
407
727
|
const browser = await (await import("playwright")).chromium.launch({ headless });
|
|
408
728
|
const context = await browser.newContext();
|
|
409
729
|
const plans = [];
|
|
@@ -411,7 +731,8 @@ async function captureWorkoutsFromBrowser(options, config) {
|
|
|
411
731
|
let observedToken;
|
|
412
732
|
const recordToken = (candidate) => {
|
|
413
733
|
if (!candidate || observedToken) return;
|
|
414
|
-
|
|
734
|
+
const normalized = normalizeToken(candidate);
|
|
735
|
+
if (isLikelyAuthToken(normalized)) observedToken = normalized;
|
|
415
736
|
};
|
|
416
737
|
const recordPlans = (incoming) => {
|
|
417
738
|
for (const plan of incoming) {
|
|
@@ -438,12 +759,25 @@ async function captureWorkoutsFromBrowser(options, config) {
|
|
|
438
759
|
try {
|
|
439
760
|
const page = await context.newPage();
|
|
440
761
|
const webOrigin = new URL(webBaseUrl).origin;
|
|
441
|
-
|
|
442
|
-
await
|
|
762
|
+
const startPath = storedAuth?.loginPath ? normalizePath(storedAuth.loginPath) : "/dashboard";
|
|
763
|
+
await page.goto(new URL(startPath, webOrigin).toString(), { waitUntil: "domcontentloaded" });
|
|
764
|
+
debugLog(debug, `Opened ${startPath}`);
|
|
765
|
+
if (storedAuth) {
|
|
766
|
+
debugLog(debug, "auth.json detected; attempting auto-login.");
|
|
767
|
+
if (await attemptAutoLogin(page, storedAuth, debug)) await waitForPasswordFieldGone(page, 15e3);
|
|
768
|
+
if (!await triggerWorkoutCapture(page, webOrigin, plans, debug)) await waitForEnter("Log in, open your workouts page, then press Enter to capture...");
|
|
769
|
+
} else await waitForEnter("Log in, open your workouts page, then press Enter to capture...");
|
|
443
770
|
const cookies = await context.cookies(webOrigin);
|
|
444
771
|
cookieHeader = cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
|
|
445
772
|
csrfCookie = cookies.find((cookie) => cookie.name === "csrf_kahunas_cookie_token")?.value;
|
|
446
773
|
csrfToken = csrfCookie ?? resolveCsrfToken(options, config);
|
|
774
|
+
if (!observedToken && csrfToken && cookieHeader) try {
|
|
775
|
+
const { token: fetchedToken } = await fetchAuthToken(csrfToken, cookieHeader, webBaseUrl);
|
|
776
|
+
recordToken(fetchedToken);
|
|
777
|
+
debugLog(debug, "Fetched auth token via /get-token.");
|
|
778
|
+
} catch (error) {
|
|
779
|
+
debugLog(debug, `Failed to fetch auth token via /get-token: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
780
|
+
}
|
|
447
781
|
if (plans.length === 0) await page.waitForTimeout(1500);
|
|
448
782
|
} finally {
|
|
449
783
|
await browser.close();
|
|
@@ -459,13 +793,16 @@ async function captureWorkoutsFromBrowser(options, config) {
|
|
|
459
793
|
}
|
|
460
794
|
async function loginWithBrowser(options, config) {
|
|
461
795
|
const webBaseUrl = resolveWebBaseUrl(options, config);
|
|
462
|
-
const headless =
|
|
796
|
+
const headless = config.headless ?? true;
|
|
797
|
+
const debug = config.debug === true;
|
|
798
|
+
const storedAuth = resolveStoredAuth();
|
|
463
799
|
const browser = await (await import("playwright")).chromium.launch({ headless });
|
|
464
800
|
const context = await browser.newContext();
|
|
465
801
|
let observedToken;
|
|
466
802
|
const recordToken = (candidate) => {
|
|
467
803
|
if (!candidate || observedToken) return;
|
|
468
|
-
|
|
804
|
+
const normalized = normalizeToken(candidate);
|
|
805
|
+
if (isLikelyAuthToken(normalized)) observedToken = normalized;
|
|
469
806
|
};
|
|
470
807
|
context.on("request", (request) => {
|
|
471
808
|
recordToken(request.headers()["auth-user-token"]);
|
|
@@ -473,8 +810,15 @@ async function loginWithBrowser(options, config) {
|
|
|
473
810
|
try {
|
|
474
811
|
const page = await context.newPage();
|
|
475
812
|
const webOrigin = new URL(webBaseUrl).origin;
|
|
476
|
-
|
|
477
|
-
await
|
|
813
|
+
const startPath = storedAuth?.loginPath ? normalizePath(storedAuth.loginPath) : "/dashboard";
|
|
814
|
+
await page.goto(new URL(startPath, webOrigin).toString(), { waitUntil: "domcontentloaded" });
|
|
815
|
+
debugLog(debug, `Opened ${startPath}`);
|
|
816
|
+
if (storedAuth) {
|
|
817
|
+
debugLog(debug, "auth.json detected; attempting auto-login.");
|
|
818
|
+
if (await attemptAutoLogin(page, storedAuth, debug)) {
|
|
819
|
+
if (!await waitForPasswordFieldGone(page, 15e3)) await waitForEnter("Finish logging in, then press Enter to continue...");
|
|
820
|
+
}
|
|
821
|
+
} else await waitForEnter("Finish logging in, then press Enter to continue...");
|
|
478
822
|
if (!observedToken) {
|
|
479
823
|
await page.reload({ waitUntil: "domcontentloaded" });
|
|
480
824
|
await page.waitForTimeout(1500);
|
|
@@ -495,7 +839,7 @@ async function loginWithBrowser(options, config) {
|
|
|
495
839
|
const csrfToken = csrfCookie ?? resolveCsrfToken(options, config);
|
|
496
840
|
let raw;
|
|
497
841
|
if (!observedToken) {
|
|
498
|
-
if (!csrfToken) throw new Error("Missing CSRF token after login. Try again or
|
|
842
|
+
if (!csrfToken) throw new Error("Missing CSRF token after login. Try again or run 'kahunas workout sync'.");
|
|
499
843
|
if (!cookieHeader) throw new Error("Missing cookies after login. Try again.");
|
|
500
844
|
const { token: extractedToken, raw: fetchedRaw } = await fetchAuthToken(csrfToken, cookieHeader, webBaseUrl);
|
|
501
845
|
recordToken(extractedToken);
|
|
@@ -516,10 +860,14 @@ async function loginWithBrowser(options, config) {
|
|
|
516
860
|
}
|
|
517
861
|
async function loginAndPersist(options, config, outputMode) {
|
|
518
862
|
const result = await loginWithBrowser(options, config);
|
|
863
|
+
const tokenUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
864
|
+
const tokenExpiresAt = resolveTokenExpiry(result.token, tokenUpdatedAt) ?? null;
|
|
519
865
|
const nextConfig = {
|
|
520
866
|
...config,
|
|
521
867
|
token: result.token,
|
|
522
|
-
webBaseUrl: result.webBaseUrl
|
|
868
|
+
webBaseUrl: result.webBaseUrl,
|
|
869
|
+
tokenUpdatedAt,
|
|
870
|
+
tokenExpiresAt
|
|
523
871
|
};
|
|
524
872
|
if (result.csrfToken) nextConfig.csrfToken = result.csrfToken;
|
|
525
873
|
if (result.cookieHeader) nextConfig.authCookie = result.cookieHeader;
|
|
@@ -533,120 +881,7 @@ async function loginAndPersist(options, config, outputMode) {
|
|
|
533
881
|
//#endregion
|
|
534
882
|
//#region src/usage.ts
|
|
535
883
|
function printUsage() {
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
//#endregion
|
|
540
|
-
//#region src/commands/auth.ts
|
|
541
|
-
async function handleAuth(positionals, options) {
|
|
542
|
-
const action = positionals[0];
|
|
543
|
-
if (!action || action === "help") {
|
|
544
|
-
printUsage();
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
if (action === "set") {
|
|
548
|
-
const token = positionals[1] ?? options.token;
|
|
549
|
-
if (!token) throw new Error("Missing token for auth set.");
|
|
550
|
-
const config = readConfig();
|
|
551
|
-
const baseUrl = resolveBaseUrl(options, config);
|
|
552
|
-
const csrfToken = resolveCsrfToken(options, config);
|
|
553
|
-
const webBaseUrl = resolveWebBaseUrl(options, config);
|
|
554
|
-
const authCookie = resolveAuthCookie(options, config);
|
|
555
|
-
const csrfCookie = resolveCsrfCookie(options, config);
|
|
556
|
-
writeConfig({
|
|
557
|
-
...config,
|
|
558
|
-
token,
|
|
559
|
-
baseUrl,
|
|
560
|
-
csrfToken,
|
|
561
|
-
webBaseUrl,
|
|
562
|
-
authCookie,
|
|
563
|
-
csrfCookie
|
|
564
|
-
});
|
|
565
|
-
console.log(`Saved token to ${CONFIG_PATH}`);
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
if (action === "token") {
|
|
569
|
-
const config = readConfig();
|
|
570
|
-
const csrfToken = resolveCsrfToken(options, config);
|
|
571
|
-
if (!csrfToken) throw new Error("Missing CSRF token. Provide --csrf or set KAHUNAS_CSRF.");
|
|
572
|
-
const webBaseUrl = resolveWebBaseUrl(options, config);
|
|
573
|
-
const authCookie = resolveAuthCookie(options, config);
|
|
574
|
-
const csrfCookie = resolveCsrfCookie(options, config);
|
|
575
|
-
const cookieHeader = authCookie ?? `csrf_kahunas_cookie_token=${csrfCookie ?? csrfToken}`;
|
|
576
|
-
const rawOutput = isFlagEnabled(options, "raw");
|
|
577
|
-
const { token: extractedToken, raw } = await fetchAuthToken(csrfToken, cookieHeader, webBaseUrl);
|
|
578
|
-
const token = extractedToken && isLikelyAuthToken(extractedToken) ? extractedToken : void 0;
|
|
579
|
-
if (rawOutput) {
|
|
580
|
-
console.log(raw);
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
if (!token) {
|
|
584
|
-
console.log(raw);
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
const nextConfig = {
|
|
588
|
-
...config,
|
|
589
|
-
token,
|
|
590
|
-
csrfToken,
|
|
591
|
-
webBaseUrl
|
|
592
|
-
};
|
|
593
|
-
if (authCookie) nextConfig.authCookie = authCookie;
|
|
594
|
-
if (csrfCookie) nextConfig.csrfCookie = csrfCookie;
|
|
595
|
-
writeConfig(nextConfig);
|
|
596
|
-
console.log(token);
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
if (action === "login") {
|
|
600
|
-
await loginAndPersist(options, readConfig(), isFlagEnabled(options, "raw") ? "raw" : "token");
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
if (action === "status") {
|
|
604
|
-
const config = readConfig();
|
|
605
|
-
const autoLogin = shouldAutoLogin(options, false);
|
|
606
|
-
let token = resolveToken(options, config);
|
|
607
|
-
if (!token) if (autoLogin) token = await loginAndPersist(options, config, "silent");
|
|
608
|
-
else throw new Error("Missing auth token. Set KAHUNAS_TOKEN or run 'kahunas auth login'.");
|
|
609
|
-
const baseUrl = resolveBaseUrl(options, config);
|
|
610
|
-
let response = await postJson("/api/v2/checkin/list", token, baseUrl, {
|
|
611
|
-
page: 1,
|
|
612
|
-
rpp: 1
|
|
613
|
-
});
|
|
614
|
-
if (autoLogin && isTokenExpiredResponse(response.json)) {
|
|
615
|
-
token = await loginAndPersist(options, config, "silent");
|
|
616
|
-
response = await postJson("/api/v2/checkin/list", token, baseUrl, {
|
|
617
|
-
page: 1,
|
|
618
|
-
rpp: 1
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.text}`);
|
|
622
|
-
if (response.json === void 0) {
|
|
623
|
-
console.log("unknown");
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
console.log(isTokenExpiredResponse(response.json) ? "expired" : "valid");
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
if (action === "show") {
|
|
630
|
-
const config = readConfig();
|
|
631
|
-
if (!config.token) throw new Error("No token saved. Use 'kahunas auth set <token>' or set KAHUNAS_TOKEN.");
|
|
632
|
-
console.log(config.token);
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
throw new Error(`Unknown auth action: ${action}`);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
//#endregion
|
|
639
|
-
//#region src/output.ts
|
|
640
|
-
function printResponse(response, rawOutput) {
|
|
641
|
-
if (rawOutput) {
|
|
642
|
-
console.log(response.text);
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
if (response.json !== void 0) {
|
|
646
|
-
console.log(JSON.stringify(response.json, null, 2));
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
console.log(response.text);
|
|
884
|
+
logPlain(`${formatHeading("kahunas - CLI for Kahunas API")}\n\nUsage:\n kahunas checkins list [--raw]\n kahunas workout list [--raw]\n kahunas workout pick [--raw]\n kahunas workout latest [--raw]\n kahunas workout events [--minimal] [--full] [--debug-preview] [--raw]\n kahunas workout serve\n kahunas serve\n kahunas workout sync\n kahunas sync\n kahunas workout program <id> [--raw]\n\n${formatDim("Config:")}\n ${CONFIG_PATH}`);
|
|
650
885
|
}
|
|
651
886
|
|
|
652
887
|
//#endregion
|
|
@@ -659,19 +894,17 @@ async function handleCheckins(positionals, options) {
|
|
|
659
894
|
}
|
|
660
895
|
if (action !== "list") throw new Error(`Unknown checkins action: ${action}`);
|
|
661
896
|
const config = readConfig();
|
|
662
|
-
const autoLogin = shouldAutoLogin(options, true);
|
|
663
897
|
let token = resolveToken(options, config);
|
|
664
|
-
if (!token)
|
|
665
|
-
else throw new Error("Missing auth token. Set KAHUNAS_TOKEN or run 'kahunas auth login'.");
|
|
898
|
+
if (!token) token = await loginAndPersist(options, config, "silent");
|
|
666
899
|
const baseUrl = resolveBaseUrl(options, config);
|
|
667
|
-
const page =
|
|
668
|
-
const rpp =
|
|
900
|
+
const page = 1;
|
|
901
|
+
const rpp = 12;
|
|
669
902
|
const rawOutput = isFlagEnabled(options, "raw");
|
|
670
903
|
let response = await postJson("/api/v2/checkin/list", token, baseUrl, {
|
|
671
904
|
page,
|
|
672
905
|
rpp
|
|
673
906
|
});
|
|
674
|
-
if (
|
|
907
|
+
if (isTokenExpiredResponse(response.json)) {
|
|
675
908
|
token = await loginAndPersist(options, config, "silent");
|
|
676
909
|
response = await postJson("/api/v2/checkin/list", token, baseUrl, {
|
|
677
910
|
page,
|
|
@@ -1822,16 +2055,15 @@ async function handleWorkout(positionals, options) {
|
|
|
1822
2055
|
return;
|
|
1823
2056
|
}
|
|
1824
2057
|
const config = readConfig();
|
|
1825
|
-
const
|
|
2058
|
+
const debug = config.debug === true;
|
|
2059
|
+
const autoLogin = true;
|
|
1826
2060
|
let token = resolveToken(options, config);
|
|
1827
2061
|
const ensureToken = async () => {
|
|
1828
|
-
if (!token)
|
|
1829
|
-
else throw new Error("Missing auth token. Set KAHUNAS_TOKEN or run 'kahunas auth login'.");
|
|
2062
|
+
if (!token) token = await loginAndPersist(options, config, "silent");
|
|
1830
2063
|
return token;
|
|
1831
2064
|
};
|
|
1832
2065
|
let webLoginInFlight = null;
|
|
1833
2066
|
const ensureWebLogin = async () => {
|
|
1834
|
-
if (!autoLogin) return;
|
|
1835
2067
|
if (!webLoginInFlight) webLoginInFlight = loginAndPersist(options, config, "silent").then(() => void 0).finally(() => {
|
|
1836
2068
|
webLoginInFlight = null;
|
|
1837
2069
|
});
|
|
@@ -1839,16 +2071,15 @@ async function handleWorkout(positionals, options) {
|
|
|
1839
2071
|
};
|
|
1840
2072
|
const baseUrl = resolveBaseUrl(options, config);
|
|
1841
2073
|
const rawOutput = isFlagEnabled(options, "raw");
|
|
1842
|
-
const page =
|
|
1843
|
-
const
|
|
1844
|
-
const listRpp = action === "latest" && options.rpp === void 0 ? 100 : rpp;
|
|
2074
|
+
const page = 1;
|
|
2075
|
+
const listRpp = action === "latest" ? 100 : 12;
|
|
1845
2076
|
const fetchList = async () => {
|
|
1846
2077
|
await ensureToken();
|
|
1847
2078
|
const url = new URL("/api/v1/workoutprogram", baseUrl);
|
|
1848
|
-
|
|
2079
|
+
url.searchParams.set("page", String(page));
|
|
1849
2080
|
if (listRpp) url.searchParams.set("rpp", String(listRpp));
|
|
1850
2081
|
let response = await getWithAuth(url.pathname + url.search, token, baseUrl);
|
|
1851
|
-
if (
|
|
2082
|
+
if (isTokenExpiredResponse(response.json)) {
|
|
1852
2083
|
token = await loginAndPersist(options, config, "silent");
|
|
1853
2084
|
response = await getWithAuth(url.pathname + url.search, token, baseUrl);
|
|
1854
2085
|
}
|
|
@@ -1864,7 +2095,7 @@ async function handleWorkout(positionals, options) {
|
|
|
1864
2095
|
const fetchWorkoutEventsPayload = async () => {
|
|
1865
2096
|
const baseWebUrl = resolveWebBaseUrl(options, config);
|
|
1866
2097
|
const webOrigin = new URL(baseWebUrl).origin;
|
|
1867
|
-
const timezone =
|
|
2098
|
+
const timezone = process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "Europe/London";
|
|
1868
2099
|
let userUuid = resolveUserUuid(options, config);
|
|
1869
2100
|
if (!userUuid) try {
|
|
1870
2101
|
await ensureToken();
|
|
@@ -1872,7 +2103,7 @@ async function handleWorkout(positionals, options) {
|
|
|
1872
2103
|
page: 1,
|
|
1873
2104
|
rpp: 1
|
|
1874
2105
|
});
|
|
1875
|
-
if (
|
|
2106
|
+
if (isTokenExpiredResponse(checkinsResponse.json)) {
|
|
1876
2107
|
token = await loginAndPersist(options, config, "silent");
|
|
1877
2108
|
checkinsResponse = await postJson("/api/v2/checkin/list", token, baseUrl, {
|
|
1878
2109
|
page: 1,
|
|
@@ -1890,7 +2121,7 @@ async function handleWorkout(positionals, options) {
|
|
|
1890
2121
|
}
|
|
1891
2122
|
}
|
|
1892
2123
|
} catch {}
|
|
1893
|
-
if (!userUuid) throw new Error("Missing user uuid.
|
|
2124
|
+
if (!userUuid) throw new Error("Missing user uuid. Run 'kahunas checkins list' or 'kahunas workout sync' once.");
|
|
1894
2125
|
if (userUuid !== config.userUuid) writeConfig({
|
|
1895
2126
|
...config,
|
|
1896
2127
|
userUuid
|
|
@@ -1909,13 +2140,13 @@ async function handleWorkout(positionals, options) {
|
|
|
1909
2140
|
effectiveCsrfToken = csrfCookie ?? csrfToken$1;
|
|
1910
2141
|
cookieHeader = authCookie ?? (effectiveCsrfToken ? `csrf_kahunas_cookie_token=${effectiveCsrfToken}` : void 0);
|
|
1911
2142
|
}
|
|
1912
|
-
if (!effectiveCsrfToken) throw new Error("Missing CSRF token. Run 'kahunas
|
|
1913
|
-
if (!cookieHeader) throw new Error("Missing cookies. Run 'kahunas
|
|
2143
|
+
if (!effectiveCsrfToken) throw new Error("Missing CSRF token. Run 'kahunas workout sync' and try again.");
|
|
2144
|
+
if (!cookieHeader) throw new Error("Missing cookies. Run 'kahunas workout sync' and try again.");
|
|
1914
2145
|
const url = new URL(`/coach/clients/calendar/getEvent/${userUuid}`, webOrigin);
|
|
1915
2146
|
url.searchParams.set("timezone", timezone);
|
|
1916
2147
|
const body = new URLSearchParams();
|
|
1917
2148
|
body.set("csrf_kahunas_token", effectiveCsrfToken);
|
|
1918
|
-
body.set("filter",
|
|
2149
|
+
body.set("filter", "");
|
|
1919
2150
|
let response = await fetch(url.toString(), {
|
|
1920
2151
|
method: "POST",
|
|
1921
2152
|
headers: {
|
|
@@ -1930,7 +2161,7 @@ async function handleWorkout(positionals, options) {
|
|
|
1930
2161
|
});
|
|
1931
2162
|
let text = await response.text();
|
|
1932
2163
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
|
|
1933
|
-
if (
|
|
2164
|
+
if (isLikelyLoginHtml(text)) {
|
|
1934
2165
|
await ensureWebLogin();
|
|
1935
2166
|
const refreshed = readConfig();
|
|
1936
2167
|
csrfToken$1 = resolveCsrfToken(options, refreshed);
|
|
@@ -1938,7 +2169,7 @@ async function handleWorkout(positionals, options) {
|
|
|
1938
2169
|
authCookie = resolveAuthCookie(options, refreshed);
|
|
1939
2170
|
effectiveCsrfToken = csrfCookie ?? csrfToken$1;
|
|
1940
2171
|
cookieHeader = authCookie ?? (effectiveCsrfToken ? `csrf_kahunas_cookie_token=${effectiveCsrfToken}` : void 0);
|
|
1941
|
-
if (!effectiveCsrfToken || !cookieHeader) throw new Error("Login required. Run 'kahunas
|
|
2172
|
+
if (!effectiveCsrfToken || !cookieHeader) throw new Error("Login required. Run 'kahunas workout sync' and try again.");
|
|
1942
2173
|
const retry = await fetch(url.toString(), {
|
|
1943
2174
|
method: "POST",
|
|
1944
2175
|
headers: {
|
|
@@ -1970,7 +2201,7 @@ async function handleWorkout(positionals, options) {
|
|
|
1970
2201
|
listUrl.searchParams.set("page", "1");
|
|
1971
2202
|
listUrl.searchParams.set("rpp", "100");
|
|
1972
2203
|
let listResponse = await getWithAuth(listUrl.pathname + listUrl.search, token, baseUrl);
|
|
1973
|
-
if (
|
|
2204
|
+
if (isTokenExpiredResponse(listResponse.json)) {
|
|
1974
2205
|
token = await loginAndPersist(options, config, "silent");
|
|
1975
2206
|
listResponse = await getWithAuth(listUrl.pathname + listUrl.search, token, baseUrl);
|
|
1976
2207
|
}
|
|
@@ -1990,7 +2221,7 @@ async function handleWorkout(positionals, options) {
|
|
|
1990
2221
|
try {
|
|
1991
2222
|
await ensureToken();
|
|
1992
2223
|
let responseProgram$1 = await fetchWorkoutProgram(token, baseUrl, programId$1, effectiveCsrfToken);
|
|
1993
|
-
if (
|
|
2224
|
+
if (isTokenExpiredResponse(responseProgram$1.json)) {
|
|
1994
2225
|
token = await loginAndPersist(options, config, "silent");
|
|
1995
2226
|
responseProgram$1 = await fetchWorkoutProgram(token, baseUrl, programId$1, effectiveCsrfToken);
|
|
1996
2227
|
}
|
|
@@ -2056,9 +2287,9 @@ async function handleWorkout(positionals, options) {
|
|
|
2056
2287
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.text}`);
|
|
2057
2288
|
if (plans.length === 0) throw new Error("No workout programs found.");
|
|
2058
2289
|
if (!rawOutput) {
|
|
2059
|
-
|
|
2290
|
+
logPlain(formatHeading("Pick a workout program:"));
|
|
2060
2291
|
plans.forEach((plan, index) => {
|
|
2061
|
-
|
|
2292
|
+
logPlain(`${index + 1}) ${formatWorkoutSummary(plan)}`);
|
|
2062
2293
|
});
|
|
2063
2294
|
}
|
|
2064
2295
|
const answer = await askQuestion(`Enter number (1-${plans.length}): `);
|
|
@@ -2068,7 +2299,7 @@ async function handleWorkout(positionals, options) {
|
|
|
2068
2299
|
if (!chosen.uuid) throw new Error("Selected workout is missing a uuid.");
|
|
2069
2300
|
const csrfToken$1 = resolveCsrfToken(options, config);
|
|
2070
2301
|
let responseProgram$1 = await fetchWorkoutProgram(await ensureToken(), baseUrl, chosen.uuid, csrfToken$1);
|
|
2071
|
-
if (
|
|
2302
|
+
if (isTokenExpiredResponse(responseProgram$1.json)) {
|
|
2072
2303
|
token = await loginAndPersist(options, config, "silent");
|
|
2073
2304
|
responseProgram$1 = await fetchWorkoutProgram(token, baseUrl, chosen.uuid, csrfToken$1);
|
|
2074
2305
|
}
|
|
@@ -2084,7 +2315,7 @@ async function handleWorkout(positionals, options) {
|
|
|
2084
2315
|
if (!chosen || !chosen.uuid) throw new Error("Latest workout is missing a uuid.");
|
|
2085
2316
|
const csrfToken$1 = resolveCsrfToken(options, config);
|
|
2086
2317
|
let responseProgram$1 = await fetchWorkoutProgram(await ensureToken(), baseUrl, chosen.uuid, csrfToken$1);
|
|
2087
|
-
if (
|
|
2318
|
+
if (isTokenExpiredResponse(responseProgram$1.json)) {
|
|
2088
2319
|
token = await loginAndPersist(options, config, "silent");
|
|
2089
2320
|
responseProgram$1 = await fetchWorkoutProgram(token, baseUrl, chosen.uuid, csrfToken$1);
|
|
2090
2321
|
}
|
|
@@ -2095,8 +2326,8 @@ async function handleWorkout(positionals, options) {
|
|
|
2095
2326
|
if (action === "events") {
|
|
2096
2327
|
const minimal = isFlagEnabled(options, "minimal");
|
|
2097
2328
|
const full = isFlagEnabled(options, "full");
|
|
2098
|
-
const debugPreview = isFlagEnabled(options, "debug-preview");
|
|
2099
|
-
const limit =
|
|
2329
|
+
const debugPreview = isFlagEnabled(options, "debug-preview") || debug;
|
|
2330
|
+
const limit = 1;
|
|
2100
2331
|
const { text, payload, timezone } = await fetchWorkoutEventsPayload();
|
|
2101
2332
|
if (rawOutput) {
|
|
2102
2333
|
console.log(text);
|
|
@@ -2106,7 +2337,7 @@ async function handleWorkout(positionals, options) {
|
|
|
2106
2337
|
console.log(text);
|
|
2107
2338
|
return;
|
|
2108
2339
|
}
|
|
2109
|
-
const sorted = sortWorkoutEvents(filterWorkoutEvents(payload
|
|
2340
|
+
const sorted = sortWorkoutEvents(filterWorkoutEvents(payload));
|
|
2110
2341
|
const limited = limit > 0 ? sorted.slice(-limit) : sorted;
|
|
2111
2342
|
if (minimal) {
|
|
2112
2343
|
console.log(JSON.stringify(limited, null, 2));
|
|
@@ -2121,7 +2352,7 @@ async function handleWorkout(positionals, options) {
|
|
|
2121
2352
|
const match = findWorkoutPreviewHtmlMatch(record) ?? (program ? findWorkoutPreviewHtmlMatch(program) : void 0);
|
|
2122
2353
|
const dayIndex = resolveWorkoutEventDayIndex(entry, program);
|
|
2123
2354
|
const source = match ? match.source : "not_found";
|
|
2124
|
-
|
|
2355
|
+
debugLog(true, `preview event=${eventId} program=${programUuid ?? "unknown"} day_index=${dayIndex ?? "none"} source=${source}`);
|
|
2125
2356
|
}
|
|
2126
2357
|
if (full) {
|
|
2127
2358
|
const enriched = enrichWorkoutEvents(limited, programDetails);
|
|
@@ -2130,27 +2361,27 @@ async function handleWorkout(positionals, options) {
|
|
|
2130
2361
|
}
|
|
2131
2362
|
const formatted = formatWorkoutEventsOutput(limited, programDetails, {
|
|
2132
2363
|
timezone,
|
|
2133
|
-
program:
|
|
2134
|
-
workout:
|
|
2364
|
+
program: void 0,
|
|
2365
|
+
workout: void 0
|
|
2135
2366
|
});
|
|
2136
2367
|
console.log(JSON.stringify(formatted, null, 2));
|
|
2137
2368
|
return;
|
|
2138
2369
|
}
|
|
2139
2370
|
if (action === "serve") {
|
|
2140
|
-
const host =
|
|
2141
|
-
const port =
|
|
2142
|
-
const limit =
|
|
2143
|
-
const cacheTtlMs =
|
|
2371
|
+
const host = "127.0.0.1";
|
|
2372
|
+
const port = 3e3;
|
|
2373
|
+
const limit = 1;
|
|
2374
|
+
const cacheTtlMs = 3e4;
|
|
2144
2375
|
const loadSummary = async () => {
|
|
2145
2376
|
const { text, payload, timezone } = await fetchWorkoutEventsPayload();
|
|
2146
2377
|
if (!Array.isArray(payload)) throw new Error(`Unexpected calendar response: ${text.slice(0, 200)}`);
|
|
2147
|
-
const sorted = sortWorkoutEvents(filterWorkoutEvents(payload
|
|
2378
|
+
const sorted = sortWorkoutEvents(filterWorkoutEvents(payload));
|
|
2148
2379
|
const bounded = limit > 0 ? sorted.slice(-limit) : sorted;
|
|
2149
2380
|
const programDetails = await buildProgramDetails(sorted);
|
|
2150
2381
|
const formatted = formatWorkoutEventsOutput(bounded, programDetails, {
|
|
2151
2382
|
timezone,
|
|
2152
|
-
program:
|
|
2153
|
-
workout:
|
|
2383
|
+
program: void 0,
|
|
2384
|
+
workout: void 0
|
|
2154
2385
|
});
|
|
2155
2386
|
const summary = formatted.events[0];
|
|
2156
2387
|
const programUuid = summary?.program?.uuid ?? (bounded[0] && typeof bounded[0] === "object" ? bounded[0].program : void 0);
|
|
@@ -2221,15 +2452,47 @@ async function handleWorkout(positionals, options) {
|
|
|
2221
2452
|
res.end(error instanceof Error ? error.message : "Server error");
|
|
2222
2453
|
}
|
|
2223
2454
|
}).listen(port, host, () => {
|
|
2224
|
-
|
|
2225
|
-
|
|
2455
|
+
const cache = readWorkoutCache();
|
|
2456
|
+
const freshConfig = readConfig();
|
|
2457
|
+
const lastSync = cache?.updatedAt ?? "none";
|
|
2458
|
+
const tokenExpiry = freshConfig.tokenExpiresAt ?? "unknown";
|
|
2459
|
+
const tokenUpdatedAt = freshConfig.tokenUpdatedAt ?? "unknown";
|
|
2460
|
+
logInfo(`Local workout server running at http://${host}:${port}`);
|
|
2461
|
+
logInfo(`JSON endpoint at http://${host}:${port}/api/workout`);
|
|
2462
|
+
logInfo(`Config: ${CONFIG_PATH}`);
|
|
2463
|
+
logInfo(`Last workout sync: ${lastSync}`);
|
|
2464
|
+
logInfo(`Token expiry: ${tokenExpiry}`);
|
|
2465
|
+
if (tokenExpiry === "unknown" && tokenUpdatedAt !== "unknown") logInfo(`Token updated at: ${tokenUpdatedAt}`);
|
|
2226
2466
|
});
|
|
2227
2467
|
return;
|
|
2228
2468
|
}
|
|
2229
2469
|
if (action === "sync") {
|
|
2470
|
+
const authConfig = readAuthConfig();
|
|
2471
|
+
const hasAuthConfig = !!authConfig && !!authConfig.password && (!!authConfig.email || !!authConfig.username);
|
|
2472
|
+
const tokenUpdatedAt = config.tokenUpdatedAt ?? void 0;
|
|
2473
|
+
const tokenExpiry = token && tokenUpdatedAt ? resolveTokenExpiry(token, tokenUpdatedAt) : null;
|
|
2474
|
+
if (!(!!tokenExpiry && Date.now() < Date.parse(tokenExpiry)) && !hasAuthConfig) {
|
|
2475
|
+
if (!process.stdin.isTTY) throw new Error("Missing auth credentials. Create ~/.config/kahunas/auth.json or run 'kahunas sync' in a terminal.");
|
|
2476
|
+
const login = await askQuestion("Email or username: ", process.stderr);
|
|
2477
|
+
if (!login) throw new Error("Missing email/username for login.");
|
|
2478
|
+
const password = await askQuestion("Password: ", process.stderr);
|
|
2479
|
+
if (!password) throw new Error("Missing password for login.");
|
|
2480
|
+
const isEmail = login.includes("@");
|
|
2481
|
+
writeAuthConfig({
|
|
2482
|
+
email: isEmail ? login : void 0,
|
|
2483
|
+
username: isEmail ? void 0 : login,
|
|
2484
|
+
password
|
|
2485
|
+
});
|
|
2486
|
+
console.error(`Saved credentials to ${AUTH_PATH}`);
|
|
2487
|
+
}
|
|
2230
2488
|
const captured = await captureWorkoutsFromBrowser(options, config);
|
|
2231
2489
|
const nextConfig = { ...config };
|
|
2232
|
-
if (captured.token)
|
|
2490
|
+
if (captured.token) {
|
|
2491
|
+
nextConfig.token = captured.token;
|
|
2492
|
+
const tokenUpdatedAt$1 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2493
|
+
nextConfig.tokenUpdatedAt = tokenUpdatedAt$1;
|
|
2494
|
+
nextConfig.tokenExpiresAt = resolveTokenExpiry(captured.token, tokenUpdatedAt$1) ?? null;
|
|
2495
|
+
}
|
|
2233
2496
|
if (captured.csrfToken) nextConfig.csrfToken = captured.csrfToken;
|
|
2234
2497
|
if (captured.webBaseUrl) nextConfig.webBaseUrl = captured.webBaseUrl;
|
|
2235
2498
|
if (captured.cookieHeader) nextConfig.authCookie = captured.cookieHeader;
|
|
@@ -2244,6 +2507,9 @@ async function handleWorkout(positionals, options) {
|
|
|
2244
2507
|
path: WORKOUT_CACHE_PATH
|
|
2245
2508
|
}
|
|
2246
2509
|
}, null, 2));
|
|
2510
|
+
if (process.stdin.isTTY) {
|
|
2511
|
+
if ((await askQuestion("Start the preview server now? (y/N): ", process.stderr)).toLowerCase().startsWith("y")) await handleWorkout(["serve"], options);
|
|
2512
|
+
}
|
|
2247
2513
|
return;
|
|
2248
2514
|
}
|
|
2249
2515
|
if (action !== "program") throw new Error(`Unknown workout action: ${action}`);
|
|
@@ -2252,7 +2518,7 @@ async function handleWorkout(positionals, options) {
|
|
|
2252
2518
|
const ensuredToken = await ensureToken();
|
|
2253
2519
|
const csrfToken = resolveCsrfToken(options, config);
|
|
2254
2520
|
let responseProgram = await fetchWorkoutProgram(ensuredToken, baseUrl, programId, csrfToken);
|
|
2255
|
-
if (
|
|
2521
|
+
if (isTokenExpiredResponse(responseProgram.json)) {
|
|
2256
2522
|
token = await loginAndPersist(options, config, "silent");
|
|
2257
2523
|
responseProgram = await fetchWorkoutProgram(token, baseUrl, programId, csrfToken);
|
|
2258
2524
|
}
|
|
@@ -2271,12 +2537,15 @@ async function main() {
|
|
|
2271
2537
|
const command = positionals[0];
|
|
2272
2538
|
const rest = positionals.slice(1);
|
|
2273
2539
|
switch (command) {
|
|
2274
|
-
case "auth":
|
|
2275
|
-
await handleAuth(rest, options);
|
|
2276
|
-
return;
|
|
2277
2540
|
case "checkins":
|
|
2278
2541
|
await handleCheckins(rest, options);
|
|
2279
2542
|
return;
|
|
2543
|
+
case "sync":
|
|
2544
|
+
await handleWorkout(["sync", ...rest], options);
|
|
2545
|
+
return;
|
|
2546
|
+
case "serve":
|
|
2547
|
+
await handleWorkout(["serve", ...rest], options);
|
|
2548
|
+
return;
|
|
2280
2549
|
case "workout":
|
|
2281
2550
|
await handleWorkout(rest, options);
|
|
2282
2551
|
return;
|
|
@@ -2287,8 +2556,7 @@ async function main() {
|
|
|
2287
2556
|
}
|
|
2288
2557
|
}
|
|
2289
2558
|
main().catch((error) => {
|
|
2290
|
-
|
|
2291
|
-
console.error(message);
|
|
2559
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
2292
2560
|
process.exit(1);
|
|
2293
2561
|
});
|
|
2294
2562
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kahunas-cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"ai-screenshot.png"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
+
"picocolors": "^1.1.1",
|
|
22
23
|
"playwright": "^1.49.1"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|