kahunas-cli 1.0.6 → 1.2.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.
Files changed (3) hide show
  1. package/README.md +45 -67
  2. package/dist/cli.js +310 -189
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,18 +13,12 @@ pnpm install
13
13
  pnpm build
14
14
  ```
15
15
 
16
- 2) Log in once (opens a browser):
16
+ 2) Fetch data (browser login runs automatically on first run, headless by default):
17
17
 
18
18
  ```bash
19
- pnpm kahunas -- auth login
20
- ```
21
-
22
- 3) Fetch data:
23
-
24
- ```bash
25
- pnpm kahunas -- checkins list
26
- pnpm kahunas -- workout list
27
- pnpm kahunas -- workout pick
19
+ pnpm kahunas checkins list
20
+ pnpm kahunas workout list
21
+ pnpm kahunas workout pick
28
22
  ```
29
23
 
30
24
  You can also run without installing globally:
@@ -36,19 +30,6 @@ npx kahunas-cli workout events
36
30
 
37
31
  ## Commands
38
32
 
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
33
  ### Check-ins
53
34
 
54
35
  - `kahunas checkins list`
@@ -74,88 +55,86 @@ Tokens are saved to:
74
55
  If the API list is missing a program you see in the web UI, run:
75
56
 
76
57
  ```bash
77
- pnpm kahunas -- workout sync
58
+ pnpm kahunas sync
78
59
  ```
79
60
 
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:
61
+ Or:
62
+
63
+ ```bash
64
+ pnpm kahunas workout sync
65
+ ```
66
+
67
+ This runs a browser session (headless by default). 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
68
 
82
69
  - `~/.config/kahunas/workouts.json`
83
70
 
84
71
  `workout list`, `workout pick`, and `workout latest` automatically merge the API list with this cache.
85
72
  Raw output (`--raw`) prints the API response only.
86
73
 
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).
74
+ If you add `~/.config/kahunas/auth.json`, the browser flow will attempt an automatic login and open your workouts page before capturing. Example:
90
75
 
91
- ```bash
92
- pnpm kahunas -- workout events --user <user-uuid>
76
+ ```json
77
+ {
78
+ "email": "you@example.com",
79
+ "password": "your-password"
80
+ }
93
81
  ```
94
82
 
95
- Or via pnpm:
83
+ Keep this file private; it contains credentials.
96
84
 
97
- ```bash
98
- pnpm kahunas -- workout events
99
- ```
85
+ Optional fields:
86
+
87
+ - `username` (use instead of `email`)
88
+ - `loginPath` (default: `/dashboard`)
100
89
 
101
- Default timezone is `Europe/London`. Override with `--timezone`.
90
+ If auto-capture does not find workouts, the CLI falls back to the manual prompt.
91
+
92
+ ### Workout events (dates)
102
93
 
103
- You can filter by program or workout UUID:
94
+ To see when workouts happened, the calendar endpoint returns log events with timestamps. The CLI returns the latest event 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).
104
95
 
105
96
  ```bash
106
- pnpm kahunas -- workout events --program <program-uuid>
107
- pnpm kahunas -- workout events --workout <workout-uuid>
97
+ pnpm kahunas workout events
108
98
  ```
109
99
 
110
- Use `--minimal` to return the raw event objects without program enrichment. Use `--full` to return the full enriched output. Use `--latest` for only the most recent event, or `--limit N` for the most recent N events. Use `--debug-preview` to log where preview HTML was discovered (stderr only).
100
+ Use `--minimal` to return the raw event objects without program enrichment. Use `--full` for full enriched output. Use `--debug-preview` to log where preview HTML was discovered (stderr only).
111
101
 
112
- If the user UUID is missing, `workout events` will attempt to discover it from check-ins and save it. You can also set it directly:
113
-
114
- - `KAHUNAS_USER_UUID=...`
115
- - `--user <uuid>`
102
+ If the user UUID is missing, `workout events` will attempt to discover it from check-ins and save it.
116
103
 
117
104
  ### Workout preview server
118
105
 
119
106
  Run a local dev server to preview workouts in a browser:
120
107
 
121
108
  ```bash
122
- pnpm kahunas -- workout serve
109
+ pnpm kahunas serve
123
110
  ```
124
111
 
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:
112
+ Or:
129
113
 
130
114
  ```bash
131
- pnpm kahunas -- workout serve --program <program-uuid>
132
- pnpm kahunas -- workout serve --workout <workout-uuid>
133
- pnpm kahunas -- workout serve --limit 3
115
+ pnpm kahunas workout serve
134
116
  ```
135
117
 
118
+ 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`.
119
+ The JSON response matches the CLI output for `workout events`, so there is only one data shape to maintain.
120
+
136
121
  Use `?day=<index>` to switch the selected workout day tab in the browser.
137
122
 
138
123
  ## Auto-login
139
124
 
140
- Most commands auto-login by default if a token is missing or expired. To disable:
125
+ Most commands auto-login if a token is missing or expired. This runs a browser session and saves session details in `~/.config/kahunas/config.json` (headless by default). If `~/.config/kahunas/auth.json` is present, the login step is automated.
141
126
 
142
- ```bash
143
- pnpm kahunas -- checkins list --no-auto-login
144
- ```
127
+ ## Debug logging
145
128
 
146
- ## Flags
129
+ Set `debug` to `true` in `~/.config/kahunas/config.json` to enable extra logs on stderr (includes workout preview debug output).
147
130
 
148
- - `--raw` prints raw API responses (no formatting).
149
- - `--headless` runs Playwright without a visible browser window.
131
+ ## Headless mode
132
+
133
+ Set `headless` to `false` in `~/.config/kahunas/config.json` to show the Playwright browser. Defaults to `true`.
150
134
 
151
- ## Environment variables
135
+ ## Flags
152
136
 
153
- - `KAHUNAS_TOKEN`
154
- - `KAHUNAS_CSRF`
155
- - `KAHUNAS_CSRF_COOKIE`
156
- - `KAHUNAS_COOKIE`
157
- - `KAHUNAS_WEB_BASE_URL`
158
- - `KAHUNAS_USER_UUID`
137
+ - `--raw` prints raw API responses (no formatting).
159
138
 
160
139
  ## Playwright
161
140
 
@@ -184,5 +163,4 @@ pnpm publish
184
163
  ## Notes
185
164
 
186
165
  - This CLI uses the same APIs the web app uses; tokens can expire quickly.
187
- - `auth login` is the most reliable way to refresh the token.
188
- - `workout events` relies on session cookies captured during `auth login`.
166
+ - Re-run any command (or `workout sync`) to refresh login when needed.
package/dist/cli.js CHANGED
@@ -68,17 +68,13 @@ function isFlagEnabled(options, name) {
68
68
  const value = options[name];
69
69
  return value === "true" || value === "1" || value === "yes";
70
70
  }
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
71
 
77
72
  //#endregion
78
73
  //#region src/config.ts
79
74
  const DEFAULT_BASE_URL = "https://api.kahunas.io";
80
75
  const DEFAULT_WEB_BASE_URL = "https://kahunas.io";
81
76
  const CONFIG_PATH = node_path.join(node_os.homedir(), ".config", "kahunas", "config.json");
77
+ const AUTH_PATH = node_path.join(node_os.homedir(), ".config", "kahunas", "auth.json");
82
78
  const WORKOUT_CACHE_PATH = node_path.join(node_os.homedir(), ".config", "kahunas", "workouts.json");
83
79
  function readConfig() {
84
80
  if (!node_fs.existsSync(CONFIG_PATH)) return {};
@@ -89,6 +85,15 @@ function readConfig() {
89
85
  throw new Error(`Invalid JSON in ${CONFIG_PATH}.`);
90
86
  }
91
87
  }
88
+ function readAuthConfig() {
89
+ if (!node_fs.existsSync(AUTH_PATH)) return;
90
+ const raw = node_fs.readFileSync(AUTH_PATH, "utf-8");
91
+ try {
92
+ return JSON.parse(raw);
93
+ } catch {
94
+ throw new Error(`Invalid JSON in ${AUTH_PATH}.`);
95
+ }
96
+ }
92
97
  function writeConfig(config) {
93
98
  const dir = node_path.dirname(CONFIG_PATH);
94
99
  node_fs.mkdirSync(dir, { recursive: true });
@@ -113,26 +118,26 @@ function writeWorkoutCache(plans) {
113
118
  node_fs.writeFileSync(WORKOUT_CACHE_PATH, `${JSON.stringify(cache, null, 2)}\n`, "utf-8");
114
119
  return cache;
115
120
  }
116
- function resolveToken(options, config) {
117
- return options.token ?? process.env.KAHUNAS_TOKEN ?? config.token;
121
+ function resolveToken(_, config) {
122
+ return config.token;
118
123
  }
119
- function resolveCsrfToken(options, config) {
120
- return options.csrf ?? process.env.KAHUNAS_CSRF ?? config.csrfToken;
124
+ function resolveCsrfToken(_, config) {
125
+ return config.csrfToken;
121
126
  }
122
- function resolveCsrfCookie(options, config) {
123
- return options["csrf-cookie"] ?? process.env.KAHUNAS_CSRF_COOKIE ?? config.csrfCookie;
127
+ function resolveCsrfCookie(_, config) {
128
+ return config.csrfCookie;
124
129
  }
125
- function resolveAuthCookie(options, config) {
126
- return options.cookie ?? process.env.KAHUNAS_COOKIE ?? config.authCookie;
130
+ function resolveAuthCookie(_, config) {
131
+ return config.authCookie;
127
132
  }
128
- function resolveUserUuid(options, config) {
129
- return options.user ?? process.env.KAHUNAS_USER_UUID ?? config.userUuid;
133
+ function resolveUserUuid(_, config) {
134
+ return config.userUuid;
130
135
  }
131
136
  function resolveBaseUrl(options, config) {
132
- return options["base-url"] ?? config.baseUrl ?? DEFAULT_BASE_URL;
137
+ return config.baseUrl ?? DEFAULT_BASE_URL;
133
138
  }
134
139
  function resolveWebBaseUrl(options, config) {
135
- return options["web-base-url"] ?? process.env.KAHUNAS_WEB_BASE_URL ?? config.webBaseUrl ?? DEFAULT_WEB_BASE_URL;
140
+ return config.webBaseUrl ?? DEFAULT_WEB_BASE_URL;
136
141
  }
137
142
 
138
143
  //#endregion
@@ -172,6 +177,29 @@ function isLikelyLoginHtml(text) {
172
177
  if (!trimmed.startsWith("<")) return false;
173
178
  return trimmed.includes("login to your account") || trimmed.includes("welcome back") || trimmed.includes("<title>kahunas");
174
179
  }
180
+ function extractJwtExpiry(token) {
181
+ const parts = token.split(".");
182
+ if (parts.length < 2) return;
183
+ const payload = decodeBase64Url(parts[1]);
184
+ if (!payload) return;
185
+ try {
186
+ const data = JSON.parse(payload);
187
+ if (typeof data.exp !== "number" || !Number.isFinite(data.exp)) return;
188
+ return (/* @__PURE__ */ new Date(data.exp * 1e3)).toISOString();
189
+ } catch {
190
+ return;
191
+ }
192
+ }
193
+ function decodeBase64Url(value) {
194
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
195
+ const padding = normalized.length % 4;
196
+ const padded = padding === 0 ? normalized : normalized + "=".repeat(4 - padding);
197
+ try {
198
+ return Buffer.from(padded, "base64").toString("utf-8");
199
+ } catch {
200
+ return;
201
+ }
202
+ }
175
203
 
176
204
  //#endregion
177
205
  //#region src/http.ts
@@ -249,6 +277,20 @@ async function fetchAuthToken(csrfToken, cookieHeader, webBaseUrl) {
249
277
  };
250
278
  }
251
279
 
280
+ //#endregion
281
+ //#region src/output.ts
282
+ function printResponse(response, rawOutput) {
283
+ if (rawOutput) {
284
+ console.log(response.text);
285
+ return;
286
+ }
287
+ if (response.json !== void 0) {
288
+ console.log(JSON.stringify(response.json, null, 2));
289
+ return;
290
+ }
291
+ console.log(response.text);
292
+ }
293
+
252
294
  //#endregion
253
295
  //#region src/responses.ts
254
296
  function isTokenExpiredResponse(payload) {
@@ -273,12 +315,6 @@ function extractUserUuidFromCheckins(payload) {
273
315
 
274
316
  //#endregion
275
317
  //#region src/utils.ts
276
- function parseNumber$1(value, fallback) {
277
- if (!value) return fallback;
278
- const parsed = Number.parseInt(value, 10);
279
- if (Number.isNaN(parsed)) return fallback;
280
- return parsed;
281
- }
282
318
  function askQuestion(prompt) {
283
319
  return new Promise((resolve) => {
284
320
  const rl = node_readline.createInterface({
@@ -294,6 +330,10 @@ function askQuestion(prompt) {
294
330
  function waitForEnter(prompt) {
295
331
  return askQuestion(prompt).then(() => void 0);
296
332
  }
333
+ function debugLog(enabled, message) {
334
+ if (!enabled) return;
335
+ console.error(`[debug] ${message}`);
336
+ }
297
337
 
298
338
  //#endregion
299
339
  //#region src/workouts.ts
@@ -401,9 +441,178 @@ function buildWorkoutPlanIndex(plans) {
401
441
 
402
442
  //#endregion
403
443
  //#region src/auth.ts
444
+ const LOGIN_SELECTORS = {
445
+ username: [
446
+ "input[name=\"email\"]",
447
+ "input[id=\"email\"]",
448
+ "input[type=\"email\"]",
449
+ "input[autocomplete=\"email\"]",
450
+ "input[autocomplete=\"username\"]",
451
+ "input[name=\"username\"]",
452
+ "input[id=\"username\"]",
453
+ "input[placeholder*=\"email\" i]",
454
+ "input[placeholder*=\"username\" i]",
455
+ "input[type=\"text\"]"
456
+ ],
457
+ password: [
458
+ "input[name=\"password\"]",
459
+ "input[id=\"password\"]",
460
+ "input[type=\"password\"]",
461
+ "input[autocomplete=\"current-password\"]"
462
+ ],
463
+ submit: [
464
+ "button[type=\"submit\"]",
465
+ "input[type=\"submit\"]",
466
+ "button:has-text(\"Log in\")",
467
+ "button:has-text(\"Login\")",
468
+ "button:has-text(\"Sign in\")",
469
+ "button:has-text(\"Continue\")"
470
+ ]
471
+ };
472
+ const PASSWORD_SELECTOR = LOGIN_SELECTORS.password.join(", ");
473
+ const WORKOUT_NAV_SELECTORS = [
474
+ "#client-workout_plan-view-button",
475
+ ".select-client-action[data-action=\"workout_program\"]",
476
+ "[data-action=\"workout_program\"]"
477
+ ];
478
+ const WORKOUT_NAV_QUERY_SELECTORS = [
479
+ "#client-workout_plan-view-button",
480
+ ".select-client-action[data-action=\"workout_program\"]",
481
+ "[data-action=\"workout_program\"]",
482
+ "a.nav-link",
483
+ "button"
484
+ ];
485
+ function normalizePath(pathname) {
486
+ if (pathname.startsWith("/")) return pathname;
487
+ return `/${pathname}`;
488
+ }
489
+ function resolveStoredAuth() {
490
+ const auth = readAuthConfig();
491
+ if (!auth) return;
492
+ const login = auth.username ?? auth.email;
493
+ if (!login || !auth.password) throw new Error(`Invalid auth.json at ${AUTH_PATH}. Expected \"username\" or \"email\" and \"password\".`);
494
+ return {
495
+ login,
496
+ password: auth.password,
497
+ loginPath: auth.loginPath
498
+ };
499
+ }
500
+ async function isSelectorVisible(page, selector) {
501
+ try {
502
+ return await page.isVisible(selector);
503
+ } catch {
504
+ return false;
505
+ }
506
+ }
507
+ async function findVisibleSelector(page, selectors) {
508
+ for (const selector of selectors) if (await isSelectorVisible(page, selector)) return selector;
509
+ }
510
+ async function waitForAnyVisibleSelector(page, selectors, timeoutMs) {
511
+ const combined = selectors.join(", ");
512
+ try {
513
+ await page.waitForSelector(combined, {
514
+ state: "visible",
515
+ timeout: timeoutMs
516
+ });
517
+ return true;
518
+ } catch {
519
+ return false;
520
+ }
521
+ }
522
+ async function waitForAnySelectorMatch(page, selectors, timeoutMs) {
523
+ const started = Date.now();
524
+ while (Date.now() - started < timeoutMs) {
525
+ for (const selector of selectors) if (await page.$(selector)) return true;
526
+ await page.waitForTimeout(300);
527
+ }
528
+ return false;
529
+ }
530
+ async function waitForPasswordFieldGone(page, timeoutMs) {
531
+ const started = Date.now();
532
+ while (Date.now() - started < timeoutMs) {
533
+ if (!await isSelectorVisible(page, PASSWORD_SELECTOR)) return true;
534
+ await page.waitForTimeout(500);
535
+ }
536
+ return false;
537
+ }
538
+ async function attemptAutoLogin(page, auth, debug) {
539
+ if (!await waitForAnyVisibleSelector(page, LOGIN_SELECTORS.password, 5e3)) {
540
+ debugLog(debug, "Login form not detected; skipping auto-login.");
541
+ return false;
542
+ }
543
+ const usernameSelector = await findVisibleSelector(page, LOGIN_SELECTORS.username);
544
+ const passwordSelector = await findVisibleSelector(page, LOGIN_SELECTORS.password);
545
+ if (!passwordSelector) return false;
546
+ if (usernameSelector) await page.fill(usernameSelector, auth.login);
547
+ await page.fill(passwordSelector, auth.password);
548
+ const submitSelector = await findVisibleSelector(page, LOGIN_SELECTORS.submit);
549
+ if (submitSelector) await page.click(submitSelector);
550
+ else await page.press(passwordSelector, "Enter");
551
+ await page.waitForTimeout(1500);
552
+ debugLog(debug, "Submitted login form.");
553
+ return true;
554
+ }
555
+ async function waitForPlans(plans, timeoutMs) {
556
+ const started = Date.now();
557
+ while (Date.now() - started < timeoutMs) {
558
+ if (plans.length > 0) return true;
559
+ await new Promise((resolve) => setTimeout(resolve, 250));
560
+ }
561
+ return plans.length > 0;
562
+ }
563
+ async function clickWorkoutNav(page, debug) {
564
+ for (const selector of WORKOUT_NAV_SELECTORS) {
565
+ const locator = page.locator(selector).first();
566
+ if (await locator.count() === 0) continue;
567
+ try {
568
+ await locator.scrollIntoViewIfNeeded();
569
+ } catch {}
570
+ try {
571
+ await locator.click({
572
+ timeout: 2e3,
573
+ force: true
574
+ });
575
+ debugLog(debug, `Clicked workout nav selector: ${selector}`);
576
+ return true;
577
+ } catch {}
578
+ }
579
+ try {
580
+ return await page.evaluate((selectors) => {
581
+ const resolveCandidates = (sel) => Array.from(document.querySelectorAll(sel)).filter((node) => node instanceof HTMLElement);
582
+ const candidates = selectors.flatMap(resolveCandidates);
583
+ const byAction = candidates.find((node) => node.dataset.action === "workout_program");
584
+ const byText = candidates.find((node) => /workout/i.test(node.textContent ?? ""));
585
+ const target = byAction ?? byText;
586
+ if (!target) return false;
587
+ target.dispatchEvent(new MouseEvent("click", {
588
+ bubbles: true,
589
+ cancelable: true,
590
+ view: window
591
+ }));
592
+ target.click();
593
+ return true;
594
+ }, WORKOUT_NAV_QUERY_SELECTORS);
595
+ } catch {
596
+ return false;
597
+ }
598
+ }
599
+ async function triggerWorkoutCapture(page, webOrigin, plans, debug) {
600
+ if (plans.length > 0) return true;
601
+ if (!await waitForAnySelectorMatch(page, WORKOUT_NAV_SELECTORS, 12e3)) {
602
+ debugLog(debug, "Workout nav not detected.");
603
+ return false;
604
+ }
605
+ if (!await clickWorkoutNav(page, debug)) debugLog(debug, "Workout nav click failed.");
606
+ await page.waitForTimeout(1e3);
607
+ await waitForPlans(plans, 8e3);
608
+ debugLog(debug, `Workout capture plans=${plans.length}`);
609
+ return plans.length > 0;
610
+ }
404
611
  async function captureWorkoutsFromBrowser(options, config) {
405
612
  const webBaseUrl = resolveWebBaseUrl(options, config);
406
- const headless = isFlagEnabled(options, "headless");
613
+ const headless = config.headless ?? true;
614
+ const debug = config.debug === true;
615
+ const storedAuth = resolveStoredAuth();
407
616
  const browser = await (await import("playwright")).chromium.launch({ headless });
408
617
  const context = await browser.newContext();
409
618
  const plans = [];
@@ -438,8 +647,14 @@ async function captureWorkoutsFromBrowser(options, config) {
438
647
  try {
439
648
  const page = await context.newPage();
440
649
  const webOrigin = new URL(webBaseUrl).origin;
441
- await page.goto(`${webOrigin}/dashboard`, { waitUntil: "domcontentloaded" });
442
- await waitForEnter("Log in, open your workouts page, then press Enter to capture...");
650
+ const startPath = storedAuth?.loginPath ? normalizePath(storedAuth.loginPath) : "/dashboard";
651
+ await page.goto(new URL(startPath, webOrigin).toString(), { waitUntil: "domcontentloaded" });
652
+ debugLog(debug, `Opened ${startPath}`);
653
+ if (storedAuth) {
654
+ debugLog(debug, "auth.json detected; attempting auto-login.");
655
+ if (await attemptAutoLogin(page, storedAuth, debug)) await waitForPasswordFieldGone(page, 15e3);
656
+ if (!await triggerWorkoutCapture(page, webOrigin, plans, debug)) await waitForEnter("Log in, open your workouts page, then press Enter to capture...");
657
+ } else await waitForEnter("Log in, open your workouts page, then press Enter to capture...");
443
658
  const cookies = await context.cookies(webOrigin);
444
659
  cookieHeader = cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
445
660
  csrfCookie = cookies.find((cookie) => cookie.name === "csrf_kahunas_cookie_token")?.value;
@@ -459,7 +674,9 @@ async function captureWorkoutsFromBrowser(options, config) {
459
674
  }
460
675
  async function loginWithBrowser(options, config) {
461
676
  const webBaseUrl = resolveWebBaseUrl(options, config);
462
- const headless = isFlagEnabled(options, "headless");
677
+ const headless = config.headless ?? true;
678
+ const debug = config.debug === true;
679
+ const storedAuth = resolveStoredAuth();
463
680
  const browser = await (await import("playwright")).chromium.launch({ headless });
464
681
  const context = await browser.newContext();
465
682
  let observedToken;
@@ -473,8 +690,15 @@ async function loginWithBrowser(options, config) {
473
690
  try {
474
691
  const page = await context.newPage();
475
692
  const webOrigin = new URL(webBaseUrl).origin;
476
- await page.goto(`${webOrigin}/dashboard`, { waitUntil: "domcontentloaded" });
477
- await waitForEnter("Finish logging in, then press Enter to continue...");
693
+ const startPath = storedAuth?.loginPath ? normalizePath(storedAuth.loginPath) : "/dashboard";
694
+ await page.goto(new URL(startPath, webOrigin).toString(), { waitUntil: "domcontentloaded" });
695
+ debugLog(debug, `Opened ${startPath}`);
696
+ if (storedAuth) {
697
+ debugLog(debug, "auth.json detected; attempting auto-login.");
698
+ if (await attemptAutoLogin(page, storedAuth, debug)) {
699
+ if (!await waitForPasswordFieldGone(page, 15e3)) await waitForEnter("Finish logging in, then press Enter to continue...");
700
+ }
701
+ } else await waitForEnter("Finish logging in, then press Enter to continue...");
478
702
  if (!observedToken) {
479
703
  await page.reload({ waitUntil: "domcontentloaded" });
480
704
  await page.waitForTimeout(1500);
@@ -495,7 +719,7 @@ async function loginWithBrowser(options, config) {
495
719
  const csrfToken = csrfCookie ?? resolveCsrfToken(options, config);
496
720
  let raw;
497
721
  if (!observedToken) {
498
- if (!csrfToken) throw new Error("Missing CSRF token after login. Try again or provide --csrf.");
722
+ if (!csrfToken) throw new Error("Missing CSRF token after login. Try again or run 'kahunas workout sync'.");
499
723
  if (!cookieHeader) throw new Error("Missing cookies after login. Try again.");
500
724
  const { token: extractedToken, raw: fetchedRaw } = await fetchAuthToken(csrfToken, cookieHeader, webBaseUrl);
501
725
  recordToken(extractedToken);
@@ -516,10 +740,14 @@ async function loginWithBrowser(options, config) {
516
740
  }
517
741
  async function loginAndPersist(options, config, outputMode) {
518
742
  const result = await loginWithBrowser(options, config);
743
+ const tokenUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
744
+ const tokenExpiresAt = extractJwtExpiry(result.token) ?? null;
519
745
  const nextConfig = {
520
746
  ...config,
521
747
  token: result.token,
522
- webBaseUrl: result.webBaseUrl
748
+ webBaseUrl: result.webBaseUrl,
749
+ tokenUpdatedAt,
750
+ tokenExpiresAt
523
751
  };
524
752
  if (result.csrfToken) nextConfig.csrfToken = result.csrfToken;
525
753
  if (result.cookieHeader) nextConfig.authCookie = result.cookieHeader;
@@ -533,120 +761,7 @@ async function loginAndPersist(options, config, outputMode) {
533
761
  //#endregion
534
762
  //#region src/usage.ts
535
763
  function printUsage() {
536
- console.log(`kahunas - CLI for Kahunas API\n\nUsage:\n kahunas auth set <token> [--base-url URL] [--csrf CSRF] [--web-base-url URL] [--cookie COOKIE] [--csrf-cookie VALUE]\n kahunas auth token [--csrf CSRF] [--cookie COOKIE] [--csrf-cookie VALUE] [--web-base-url URL] [--raw]\n kahunas auth login [--web-base-url URL] [--headless] [--raw]\n kahunas auth status [--token TOKEN] [--base-url URL] [--auto-login] [--headless]\n kahunas auth show\n kahunas checkins list [--page N] [--rpp N] [--token TOKEN] [--base-url URL] [--raw] [--no-auto-login] [--headless]\n kahunas workout list [--page N] [--rpp N] [--token TOKEN] [--base-url URL] [--raw] [--no-auto-login] [--headless]\n kahunas workout pick [--page N] [--rpp N] [--token TOKEN] [--base-url URL] [--raw] [--no-auto-login] [--headless]\n kahunas workout latest [--token TOKEN] [--base-url URL] [--raw] [--no-auto-login] [--headless]\n kahunas workout events [--user UUID] [--timezone TZ] [--program UUID] [--workout UUID] [--minimal] [--full] [--latest] [--limit N] [--debug-preview] [--raw] [--no-auto-login] [--headless]\n kahunas workout serve [--host HOST] [--port N] [--timezone TZ] [--program UUID] [--workout UUID] [--limit N] [--cache-ttl MS] [--no-auto-login] [--headless]\n kahunas workout sync [--headless]\n kahunas workout program <id> [--csrf CSRF] [--token TOKEN] [--base-url URL] [--raw] [--no-auto-login] [--headless]\n\nEnv:\n KAHUNAS_TOKEN=...\n KAHUNAS_CSRF=...\n KAHUNAS_CSRF_COOKIE=...\n KAHUNAS_COOKIE=...\n KAHUNAS_WEB_BASE_URL=...\n KAHUNAS_USER_UUID=...\n\nConfig:\n ${CONFIG_PATH}`);
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);
764
+ console.log(`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\nConfig:\n ${CONFIG_PATH}`);
650
765
  }
651
766
 
652
767
  //#endregion
@@ -659,19 +774,17 @@ async function handleCheckins(positionals, options) {
659
774
  }
660
775
  if (action !== "list") throw new Error(`Unknown checkins action: ${action}`);
661
776
  const config = readConfig();
662
- const autoLogin = shouldAutoLogin(options, true);
663
777
  let token = resolveToken(options, config);
664
- if (!token) if (autoLogin) token = await loginAndPersist(options, config, "silent");
665
- else throw new Error("Missing auth token. Set KAHUNAS_TOKEN or run 'kahunas auth login'.");
778
+ if (!token) token = await loginAndPersist(options, config, "silent");
666
779
  const baseUrl = resolveBaseUrl(options, config);
667
- const page = parseNumber$1(options.page, 1);
668
- const rpp = parseNumber$1(options.rpp, 12);
780
+ const page = 1;
781
+ const rpp = 12;
669
782
  const rawOutput = isFlagEnabled(options, "raw");
670
783
  let response = await postJson("/api/v2/checkin/list", token, baseUrl, {
671
784
  page,
672
785
  rpp
673
786
  });
674
- if (autoLogin && isTokenExpiredResponse(response.json)) {
787
+ if (isTokenExpiredResponse(response.json)) {
675
788
  token = await loginAndPersist(options, config, "silent");
676
789
  response = await postJson("/api/v2/checkin/list", token, baseUrl, {
677
790
  page,
@@ -1822,16 +1935,15 @@ async function handleWorkout(positionals, options) {
1822
1935
  return;
1823
1936
  }
1824
1937
  const config = readConfig();
1825
- const autoLogin = shouldAutoLogin(options, true);
1938
+ const debug = config.debug === true;
1939
+ const autoLogin = true;
1826
1940
  let token = resolveToken(options, config);
1827
1941
  const ensureToken = async () => {
1828
- if (!token) if (autoLogin) token = await loginAndPersist(options, config, "silent");
1829
- else throw new Error("Missing auth token. Set KAHUNAS_TOKEN or run 'kahunas auth login'.");
1942
+ if (!token) token = await loginAndPersist(options, config, "silent");
1830
1943
  return token;
1831
1944
  };
1832
1945
  let webLoginInFlight = null;
1833
1946
  const ensureWebLogin = async () => {
1834
- if (!autoLogin) return;
1835
1947
  if (!webLoginInFlight) webLoginInFlight = loginAndPersist(options, config, "silent").then(() => void 0).finally(() => {
1836
1948
  webLoginInFlight = null;
1837
1949
  });
@@ -1839,16 +1951,15 @@ async function handleWorkout(positionals, options) {
1839
1951
  };
1840
1952
  const baseUrl = resolveBaseUrl(options, config);
1841
1953
  const rawOutput = isFlagEnabled(options, "raw");
1842
- const page = parseNumber$1(options.page, 1);
1843
- const rpp = parseNumber$1(options.rpp, 12);
1844
- const listRpp = action === "latest" && options.rpp === void 0 ? 100 : rpp;
1954
+ const page = 1;
1955
+ const listRpp = action === "latest" ? 100 : 12;
1845
1956
  const fetchList = async () => {
1846
1957
  await ensureToken();
1847
1958
  const url = new URL("/api/v1/workoutprogram", baseUrl);
1848
- if (page) url.searchParams.set("page", String(page));
1959
+ url.searchParams.set("page", String(page));
1849
1960
  if (listRpp) url.searchParams.set("rpp", String(listRpp));
1850
1961
  let response = await getWithAuth(url.pathname + url.search, token, baseUrl);
1851
- if (autoLogin && isTokenExpiredResponse(response.json)) {
1962
+ if (isTokenExpiredResponse(response.json)) {
1852
1963
  token = await loginAndPersist(options, config, "silent");
1853
1964
  response = await getWithAuth(url.pathname + url.search, token, baseUrl);
1854
1965
  }
@@ -1864,7 +1975,7 @@ async function handleWorkout(positionals, options) {
1864
1975
  const fetchWorkoutEventsPayload = async () => {
1865
1976
  const baseWebUrl = resolveWebBaseUrl(options, config);
1866
1977
  const webOrigin = new URL(baseWebUrl).origin;
1867
- const timezone = options.timezone ?? process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "Europe/London";
1978
+ const timezone = process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "Europe/London";
1868
1979
  let userUuid = resolveUserUuid(options, config);
1869
1980
  if (!userUuid) try {
1870
1981
  await ensureToken();
@@ -1872,7 +1983,7 @@ async function handleWorkout(positionals, options) {
1872
1983
  page: 1,
1873
1984
  rpp: 1
1874
1985
  });
1875
- if (autoLogin && isTokenExpiredResponse(checkinsResponse.json)) {
1986
+ if (isTokenExpiredResponse(checkinsResponse.json)) {
1876
1987
  token = await loginAndPersist(options, config, "silent");
1877
1988
  checkinsResponse = await postJson("/api/v2/checkin/list", token, baseUrl, {
1878
1989
  page: 1,
@@ -1890,7 +2001,7 @@ async function handleWorkout(positionals, options) {
1890
2001
  }
1891
2002
  }
1892
2003
  } catch {}
1893
- if (!userUuid) throw new Error("Missing user uuid. Use --user or run 'kahunas checkins list' once.");
2004
+ if (!userUuid) throw new Error("Missing user uuid. Run 'kahunas checkins list' or 'kahunas workout sync' once.");
1894
2005
  if (userUuid !== config.userUuid) writeConfig({
1895
2006
  ...config,
1896
2007
  userUuid
@@ -1909,13 +2020,13 @@ async function handleWorkout(positionals, options) {
1909
2020
  effectiveCsrfToken = csrfCookie ?? csrfToken$1;
1910
2021
  cookieHeader = authCookie ?? (effectiveCsrfToken ? `csrf_kahunas_cookie_token=${effectiveCsrfToken}` : void 0);
1911
2022
  }
1912
- if (!effectiveCsrfToken) throw new Error("Missing CSRF token. Run 'kahunas auth login' and try again.");
1913
- if (!cookieHeader) throw new Error("Missing cookies. Run 'kahunas auth login' and try again.");
2023
+ if (!effectiveCsrfToken) throw new Error("Missing CSRF token. Run 'kahunas workout sync' and try again.");
2024
+ if (!cookieHeader) throw new Error("Missing cookies. Run 'kahunas workout sync' and try again.");
1914
2025
  const url = new URL(`/coach/clients/calendar/getEvent/${userUuid}`, webOrigin);
1915
2026
  url.searchParams.set("timezone", timezone);
1916
2027
  const body = new URLSearchParams();
1917
2028
  body.set("csrf_kahunas_token", effectiveCsrfToken);
1918
- body.set("filter", options.filter ?? "");
2029
+ body.set("filter", "");
1919
2030
  let response = await fetch(url.toString(), {
1920
2031
  method: "POST",
1921
2032
  headers: {
@@ -1930,7 +2041,7 @@ async function handleWorkout(positionals, options) {
1930
2041
  });
1931
2042
  let text = await response.text();
1932
2043
  if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
1933
- if (autoLogin && isLikelyLoginHtml(text)) {
2044
+ if (isLikelyLoginHtml(text)) {
1934
2045
  await ensureWebLogin();
1935
2046
  const refreshed = readConfig();
1936
2047
  csrfToken$1 = resolveCsrfToken(options, refreshed);
@@ -1938,7 +2049,7 @@ async function handleWorkout(positionals, options) {
1938
2049
  authCookie = resolveAuthCookie(options, refreshed);
1939
2050
  effectiveCsrfToken = csrfCookie ?? csrfToken$1;
1940
2051
  cookieHeader = authCookie ?? (effectiveCsrfToken ? `csrf_kahunas_cookie_token=${effectiveCsrfToken}` : void 0);
1941
- if (!effectiveCsrfToken || !cookieHeader) throw new Error("Login required. Run 'kahunas auth login' and try again.");
2052
+ if (!effectiveCsrfToken || !cookieHeader) throw new Error("Login required. Run 'kahunas workout sync' and try again.");
1942
2053
  const retry = await fetch(url.toString(), {
1943
2054
  method: "POST",
1944
2055
  headers: {
@@ -1970,7 +2081,7 @@ async function handleWorkout(positionals, options) {
1970
2081
  listUrl.searchParams.set("page", "1");
1971
2082
  listUrl.searchParams.set("rpp", "100");
1972
2083
  let listResponse = await getWithAuth(listUrl.pathname + listUrl.search, token, baseUrl);
1973
- if (autoLogin && isTokenExpiredResponse(listResponse.json)) {
2084
+ if (isTokenExpiredResponse(listResponse.json)) {
1974
2085
  token = await loginAndPersist(options, config, "silent");
1975
2086
  listResponse = await getWithAuth(listUrl.pathname + listUrl.search, token, baseUrl);
1976
2087
  }
@@ -1990,7 +2101,7 @@ async function handleWorkout(positionals, options) {
1990
2101
  try {
1991
2102
  await ensureToken();
1992
2103
  let responseProgram$1 = await fetchWorkoutProgram(token, baseUrl, programId$1, effectiveCsrfToken);
1993
- if (autoLogin && isTokenExpiredResponse(responseProgram$1.json)) {
2104
+ if (isTokenExpiredResponse(responseProgram$1.json)) {
1994
2105
  token = await loginAndPersist(options, config, "silent");
1995
2106
  responseProgram$1 = await fetchWorkoutProgram(token, baseUrl, programId$1, effectiveCsrfToken);
1996
2107
  }
@@ -2068,7 +2179,7 @@ async function handleWorkout(positionals, options) {
2068
2179
  if (!chosen.uuid) throw new Error("Selected workout is missing a uuid.");
2069
2180
  const csrfToken$1 = resolveCsrfToken(options, config);
2070
2181
  let responseProgram$1 = await fetchWorkoutProgram(await ensureToken(), baseUrl, chosen.uuid, csrfToken$1);
2071
- if (autoLogin && isTokenExpiredResponse(responseProgram$1.json)) {
2182
+ if (isTokenExpiredResponse(responseProgram$1.json)) {
2072
2183
  token = await loginAndPersist(options, config, "silent");
2073
2184
  responseProgram$1 = await fetchWorkoutProgram(token, baseUrl, chosen.uuid, csrfToken$1);
2074
2185
  }
@@ -2084,7 +2195,7 @@ async function handleWorkout(positionals, options) {
2084
2195
  if (!chosen || !chosen.uuid) throw new Error("Latest workout is missing a uuid.");
2085
2196
  const csrfToken$1 = resolveCsrfToken(options, config);
2086
2197
  let responseProgram$1 = await fetchWorkoutProgram(await ensureToken(), baseUrl, chosen.uuid, csrfToken$1);
2087
- if (autoLogin && isTokenExpiredResponse(responseProgram$1.json)) {
2198
+ if (isTokenExpiredResponse(responseProgram$1.json)) {
2088
2199
  token = await loginAndPersist(options, config, "silent");
2089
2200
  responseProgram$1 = await fetchWorkoutProgram(token, baseUrl, chosen.uuid, csrfToken$1);
2090
2201
  }
@@ -2095,8 +2206,8 @@ async function handleWorkout(positionals, options) {
2095
2206
  if (action === "events") {
2096
2207
  const minimal = isFlagEnabled(options, "minimal");
2097
2208
  const full = isFlagEnabled(options, "full");
2098
- const debugPreview = isFlagEnabled(options, "debug-preview");
2099
- const limit = isFlagEnabled(options, "latest") || isFlagEnabled(options, "last") ? 1 : parseNumber$1(options.limit, 0);
2209
+ const debugPreview = isFlagEnabled(options, "debug-preview") || debug;
2210
+ const limit = 1;
2100
2211
  const { text, payload, timezone } = await fetchWorkoutEventsPayload();
2101
2212
  if (rawOutput) {
2102
2213
  console.log(text);
@@ -2106,7 +2217,7 @@ async function handleWorkout(positionals, options) {
2106
2217
  console.log(text);
2107
2218
  return;
2108
2219
  }
2109
- const sorted = sortWorkoutEvents(filterWorkoutEvents(payload, options.program, options.workout));
2220
+ const sorted = sortWorkoutEvents(filterWorkoutEvents(payload));
2110
2221
  const limited = limit > 0 ? sorted.slice(-limit) : sorted;
2111
2222
  if (minimal) {
2112
2223
  console.log(JSON.stringify(limited, null, 2));
@@ -2121,7 +2232,7 @@ async function handleWorkout(positionals, options) {
2121
2232
  const match = findWorkoutPreviewHtmlMatch(record) ?? (program ? findWorkoutPreviewHtmlMatch(program) : void 0);
2122
2233
  const dayIndex = resolveWorkoutEventDayIndex(entry, program);
2123
2234
  const source = match ? match.source : "not_found";
2124
- console.error(`debug-preview event=${eventId} program=${programUuid ?? "unknown"} day_index=${dayIndex ?? "none"} source=${source}`);
2235
+ debugLog(true, `preview event=${eventId} program=${programUuid ?? "unknown"} day_index=${dayIndex ?? "none"} source=${source}`);
2125
2236
  }
2126
2237
  if (full) {
2127
2238
  const enriched = enrichWorkoutEvents(limited, programDetails);
@@ -2130,27 +2241,27 @@ async function handleWorkout(positionals, options) {
2130
2241
  }
2131
2242
  const formatted = formatWorkoutEventsOutput(limited, programDetails, {
2132
2243
  timezone,
2133
- program: options.program,
2134
- workout: options.workout
2244
+ program: void 0,
2245
+ workout: void 0
2135
2246
  });
2136
2247
  console.log(JSON.stringify(formatted, null, 2));
2137
2248
  return;
2138
2249
  }
2139
2250
  if (action === "serve") {
2140
- const host = options.host ?? "127.0.0.1";
2141
- const port = parseNumber$1(options.port, 3e3);
2142
- const limit = parseNumber$1(options.limit, 1);
2143
- const cacheTtlMs = parseNumber$1(options["cache-ttl"], 3e4);
2251
+ const host = "127.0.0.1";
2252
+ const port = 3e3;
2253
+ const limit = 1;
2254
+ const cacheTtlMs = 3e4;
2144
2255
  const loadSummary = async () => {
2145
2256
  const { text, payload, timezone } = await fetchWorkoutEventsPayload();
2146
2257
  if (!Array.isArray(payload)) throw new Error(`Unexpected calendar response: ${text.slice(0, 200)}`);
2147
- const sorted = sortWorkoutEvents(filterWorkoutEvents(payload, options.program, options.workout));
2258
+ const sorted = sortWorkoutEvents(filterWorkoutEvents(payload));
2148
2259
  const bounded = limit > 0 ? sorted.slice(-limit) : sorted;
2149
2260
  const programDetails = await buildProgramDetails(sorted);
2150
2261
  const formatted = formatWorkoutEventsOutput(bounded, programDetails, {
2151
2262
  timezone,
2152
- program: options.program,
2153
- workout: options.workout
2263
+ program: void 0,
2264
+ workout: void 0
2154
2265
  });
2155
2266
  const summary = formatted.events[0];
2156
2267
  const programUuid = summary?.program?.uuid ?? (bounded[0] && typeof bounded[0] === "object" ? bounded[0].program : void 0);
@@ -2221,8 +2332,15 @@ async function handleWorkout(positionals, options) {
2221
2332
  res.end(error instanceof Error ? error.message : "Server error");
2222
2333
  }
2223
2334
  }).listen(port, host, () => {
2335
+ const cache = readWorkoutCache();
2336
+ const freshConfig = readConfig();
2337
+ const lastSync = cache?.updatedAt ?? "none";
2338
+ const tokenExpiry = freshConfig.tokenExpiresAt ?? "unknown";
2224
2339
  console.log(`Local workout server running at http://${host}:${port}`);
2225
2340
  console.log(`JSON endpoint at http://${host}:${port}/api/workout`);
2341
+ console.log(`Config: ${CONFIG_PATH}`);
2342
+ console.log(`Last workout sync: ${lastSync}`);
2343
+ console.log(`Token expiry: ${tokenExpiry}`);
2226
2344
  });
2227
2345
  return;
2228
2346
  }
@@ -2252,7 +2370,7 @@ async function handleWorkout(positionals, options) {
2252
2370
  const ensuredToken = await ensureToken();
2253
2371
  const csrfToken = resolveCsrfToken(options, config);
2254
2372
  let responseProgram = await fetchWorkoutProgram(ensuredToken, baseUrl, programId, csrfToken);
2255
- if (autoLogin && isTokenExpiredResponse(responseProgram.json)) {
2373
+ if (isTokenExpiredResponse(responseProgram.json)) {
2256
2374
  token = await loginAndPersist(options, config, "silent");
2257
2375
  responseProgram = await fetchWorkoutProgram(token, baseUrl, programId, csrfToken);
2258
2376
  }
@@ -2271,12 +2389,15 @@ async function main() {
2271
2389
  const command = positionals[0];
2272
2390
  const rest = positionals.slice(1);
2273
2391
  switch (command) {
2274
- case "auth":
2275
- await handleAuth(rest, options);
2276
- return;
2277
2392
  case "checkins":
2278
2393
  await handleCheckins(rest, options);
2279
2394
  return;
2395
+ case "sync":
2396
+ await handleWorkout(["sync", ...rest], options);
2397
+ return;
2398
+ case "serve":
2399
+ await handleWorkout(["serve", ...rest], options);
2400
+ return;
2280
2401
  case "workout":
2281
2402
  await handleWorkout(rest, options);
2282
2403
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kahunas-cli",
3
- "version": "1.0.6",
3
+ "version": "1.2.0",
4
4
  "description": "",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {