kahunas-cli 1.3.0 → 1.4.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 +1 -1
  2. package/dist/cli.js +133 -107
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -29,7 +29,7 @@ pnpm kahunas serve
29
29
 
30
30
  Open `http://127.0.0.1:3000`.
31
31
 
32
- If `auth.json` is missing, `sync` will prompt for credentials and save them.
32
+ If `auth.json` is missing, `sync` will prompt for credentials and save them after a successful login.
33
33
 
34
34
  ## Advanced usage
35
35
 
package/dist/cli.js CHANGED
@@ -439,6 +439,24 @@ function askQuestion(prompt, output = process.stdout) {
439
439
  function waitForEnter(prompt, output = process.stdout) {
440
440
  return askQuestion(prompt, output).then(() => void 0);
441
441
  }
442
+ function askHiddenQuestion(prompt, output = process.stderr) {
443
+ return new Promise((resolve) => {
444
+ const rl = node_readline.createInterface({
445
+ input: process.stdin,
446
+ output,
447
+ terminal: true
448
+ });
449
+ const writable = rl;
450
+ const originalWrite = writable._writeToOutput.bind(rl);
451
+ writable._writeToOutput = (value) => {
452
+ if (value.includes("\n") || value.includes("\r")) originalWrite(value);
453
+ };
454
+ rl.question(prompt, (answer) => {
455
+ rl.close();
456
+ resolve(answer.trim());
457
+ });
458
+ });
459
+ }
442
460
  function debugLog(enabled, message) {
443
461
  logDebug(enabled, message);
444
462
  }
@@ -597,8 +615,8 @@ function normalizePath(pathname) {
597
615
  function normalizeToken(token) {
598
616
  return token.trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1").replace(/^bearer\s+/i, "");
599
617
  }
600
- function resolveStoredAuth() {
601
- const auth = readAuthConfig();
618
+ function resolveStoredAuth(override) {
619
+ const auth = override ?? readAuthConfig();
602
620
  if (!auth) return;
603
621
  const login = auth.username ?? auth.email;
604
622
  if (!login || !auth.password) throw new Error(`Invalid auth.json at ${AUTH_PATH}. Expected \"username\" or \"email\" and \"password\".`);
@@ -719,11 +737,11 @@ async function triggerWorkoutCapture(page, webOrigin, plans, debug) {
719
737
  debugLog(debug, `Workout capture plans=${plans.length}`);
720
738
  return plans.length > 0;
721
739
  }
722
- async function captureWorkoutsFromBrowser(options, config) {
740
+ async function captureWorkoutsFromBrowser(options, config, authOverride) {
723
741
  const webBaseUrl = resolveWebBaseUrl(options, config);
724
742
  const headless = config.headless ?? true;
725
743
  const debug = config.debug === true;
726
- const storedAuth = resolveStoredAuth();
744
+ const storedAuth = resolveStoredAuth(authOverride);
727
745
  const browser = await (await import("playwright")).chromium.launch({ headless });
728
746
  const context = await browser.newContext();
729
747
  const plans = [];
@@ -2243,6 +2261,104 @@ async function handleWorkout(positionals, options) {
2243
2261
  }
2244
2262
  return programDetails;
2245
2263
  };
2264
+ const startWorkoutServer = async () => {
2265
+ const host = "127.0.0.1";
2266
+ const port = 3e3;
2267
+ const limit = 1;
2268
+ const cacheTtlMs = 3e4;
2269
+ const loadSummary = async () => {
2270
+ const { text, payload, timezone } = await fetchWorkoutEventsPayload();
2271
+ if (!Array.isArray(payload)) throw new Error(`Unexpected calendar response: ${text.slice(0, 200)}`);
2272
+ const sorted = sortWorkoutEvents(filterWorkoutEvents(payload));
2273
+ const bounded = limit > 0 ? sorted.slice(-limit) : sorted;
2274
+ const programDetails = await buildProgramDetails(sorted);
2275
+ const formatted = formatWorkoutEventsOutput(bounded, programDetails, {
2276
+ timezone,
2277
+ program: void 0,
2278
+ workout: void 0
2279
+ });
2280
+ const summary = formatted.events[0];
2281
+ const programUuid = summary?.program?.uuid ?? (bounded[0] && typeof bounded[0] === "object" ? bounded[0].program : void 0);
2282
+ return {
2283
+ formatted,
2284
+ days: summarizeWorkoutProgramDays(programUuid ? programDetails[programUuid] : void 0),
2285
+ summary,
2286
+ timezone
2287
+ };
2288
+ };
2289
+ let cached;
2290
+ let summaryInFlight = null;
2291
+ const getSummary = async (forceRefresh) => {
2292
+ if (!forceRefresh && cached && Date.now() - cached.fetchedAt < cacheTtlMs) return cached.data;
2293
+ if (!forceRefresh && summaryInFlight) return summaryInFlight;
2294
+ const pending = loadSummary().finally(() => {
2295
+ summaryInFlight = null;
2296
+ });
2297
+ summaryInFlight = pending;
2298
+ const data = await pending;
2299
+ cached = {
2300
+ data,
2301
+ fetchedAt: Date.now()
2302
+ };
2303
+ return data;
2304
+ };
2305
+ (0, node_http.createServer)(async (req, res) => {
2306
+ try {
2307
+ const url = new URL(req.url ?? "/", `http://${host}:${port}`);
2308
+ const wantsRefresh = url.searchParams.get("refresh") === "1";
2309
+ if (url.pathname === "/api/workout") {
2310
+ const data$1 = await getSummary(wantsRefresh);
2311
+ res.statusCode = 200;
2312
+ res.setHeader("content-type", "application/json; charset=utf-8");
2313
+ res.setHeader("cache-control", "no-store");
2314
+ res.end(JSON.stringify(data$1.formatted, null, 2));
2315
+ return;
2316
+ }
2317
+ if (url.pathname === "/favicon.ico") {
2318
+ res.statusCode = 204;
2319
+ res.end();
2320
+ return;
2321
+ }
2322
+ if (url.pathname !== "/") {
2323
+ res.statusCode = 404;
2324
+ res.end("Not found");
2325
+ return;
2326
+ }
2327
+ const data = await getSummary(wantsRefresh);
2328
+ const dayParam = url.searchParams.get("day");
2329
+ const selectedDayIndex = resolveSelectedDayIndex(data.days, data.summary?.workout_day?.day_index, data.summary?.workout_day?.day_label, dayParam);
2330
+ const html = renderWorkoutPage({
2331
+ summary: data.summary,
2332
+ days: data.days,
2333
+ selectedDayIndex,
2334
+ timezone: data.timezone,
2335
+ apiPath: "/api/workout",
2336
+ refreshPath: "/?refresh=1",
2337
+ isLatest: limit === 1
2338
+ });
2339
+ res.statusCode = 200;
2340
+ res.setHeader("content-type", "text/html; charset=utf-8");
2341
+ res.setHeader("cache-control", "no-store");
2342
+ res.end(html);
2343
+ } catch (error) {
2344
+ res.statusCode = 500;
2345
+ res.setHeader("content-type", "text/plain; charset=utf-8");
2346
+ res.end(error instanceof Error ? error.message : "Server error");
2347
+ }
2348
+ }).listen(port, host, () => {
2349
+ const cache = readWorkoutCache();
2350
+ const freshConfig = readConfig();
2351
+ const lastSync = cache?.updatedAt ?? "none";
2352
+ const tokenExpiry = freshConfig.tokenExpiresAt ?? "unknown";
2353
+ const tokenUpdatedAt = freshConfig.tokenUpdatedAt ?? "unknown";
2354
+ logInfo(`Local workout server running at http://${host}:${port}`);
2355
+ logInfo(`JSON endpoint at http://${host}:${port}/api/workout`);
2356
+ logInfo(`Config: ${CONFIG_PATH}`);
2357
+ logInfo(`Last workout sync: ${lastSync}`);
2358
+ logInfo(`Token expiry: ${tokenExpiry}`);
2359
+ if (tokenExpiry === "unknown" && tokenUpdatedAt !== "unknown") logInfo(`Token updated at: ${tokenUpdatedAt}`);
2360
+ });
2361
+ };
2246
2362
  const resolveSelectedDayIndex = (days, eventDayIndex, eventDayLabel, dayParam) => {
2247
2363
  const parseOptionalInt = (value) => {
2248
2364
  if (!value) return;
@@ -2368,102 +2484,7 @@ async function handleWorkout(positionals, options) {
2368
2484
  return;
2369
2485
  }
2370
2486
  if (action === "serve") {
2371
- const host = "127.0.0.1";
2372
- const port = 3e3;
2373
- const limit = 1;
2374
- const cacheTtlMs = 3e4;
2375
- const loadSummary = async () => {
2376
- const { text, payload, timezone } = await fetchWorkoutEventsPayload();
2377
- if (!Array.isArray(payload)) throw new Error(`Unexpected calendar response: ${text.slice(0, 200)}`);
2378
- const sorted = sortWorkoutEvents(filterWorkoutEvents(payload));
2379
- const bounded = limit > 0 ? sorted.slice(-limit) : sorted;
2380
- const programDetails = await buildProgramDetails(sorted);
2381
- const formatted = formatWorkoutEventsOutput(bounded, programDetails, {
2382
- timezone,
2383
- program: void 0,
2384
- workout: void 0
2385
- });
2386
- const summary = formatted.events[0];
2387
- const programUuid = summary?.program?.uuid ?? (bounded[0] && typeof bounded[0] === "object" ? bounded[0].program : void 0);
2388
- return {
2389
- formatted,
2390
- days: summarizeWorkoutProgramDays(programUuid ? programDetails[programUuid] : void 0),
2391
- summary,
2392
- timezone
2393
- };
2394
- };
2395
- let cached;
2396
- let summaryInFlight = null;
2397
- const getSummary = async (forceRefresh) => {
2398
- if (!forceRefresh && cached && Date.now() - cached.fetchedAt < cacheTtlMs) return cached.data;
2399
- if (!forceRefresh && summaryInFlight) return summaryInFlight;
2400
- const pending = loadSummary().finally(() => {
2401
- summaryInFlight = null;
2402
- });
2403
- summaryInFlight = pending;
2404
- const data = await pending;
2405
- cached = {
2406
- data,
2407
- fetchedAt: Date.now()
2408
- };
2409
- return data;
2410
- };
2411
- (0, node_http.createServer)(async (req, res) => {
2412
- try {
2413
- const url = new URL(req.url ?? "/", `http://${host}:${port}`);
2414
- const wantsRefresh = url.searchParams.get("refresh") === "1";
2415
- if (url.pathname === "/api/workout") {
2416
- const data$1 = await getSummary(wantsRefresh);
2417
- res.statusCode = 200;
2418
- res.setHeader("content-type", "application/json; charset=utf-8");
2419
- res.setHeader("cache-control", "no-store");
2420
- res.end(JSON.stringify(data$1.formatted, null, 2));
2421
- return;
2422
- }
2423
- if (url.pathname === "/favicon.ico") {
2424
- res.statusCode = 204;
2425
- res.end();
2426
- return;
2427
- }
2428
- if (url.pathname !== "/") {
2429
- res.statusCode = 404;
2430
- res.end("Not found");
2431
- return;
2432
- }
2433
- const data = await getSummary(wantsRefresh);
2434
- const dayParam = url.searchParams.get("day");
2435
- const selectedDayIndex = resolveSelectedDayIndex(data.days, data.summary?.workout_day?.day_index, data.summary?.workout_day?.day_label, dayParam);
2436
- const html = renderWorkoutPage({
2437
- summary: data.summary,
2438
- days: data.days,
2439
- selectedDayIndex,
2440
- timezone: data.timezone,
2441
- apiPath: "/api/workout",
2442
- refreshPath: "/?refresh=1",
2443
- isLatest: limit === 1
2444
- });
2445
- res.statusCode = 200;
2446
- res.setHeader("content-type", "text/html; charset=utf-8");
2447
- res.setHeader("cache-control", "no-store");
2448
- res.end(html);
2449
- } catch (error) {
2450
- res.statusCode = 500;
2451
- res.setHeader("content-type", "text/plain; charset=utf-8");
2452
- res.end(error instanceof Error ? error.message : "Server error");
2453
- }
2454
- }).listen(port, host, () => {
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}`);
2466
- });
2487
+ await startWorkoutServer();
2467
2488
  return;
2468
2489
  }
2469
2490
  if (action === "sync") {
@@ -2471,21 +2492,22 @@ async function handleWorkout(positionals, options) {
2471
2492
  const hasAuthConfig = !!authConfig && !!authConfig.password && (!!authConfig.email || !!authConfig.username);
2472
2493
  const tokenUpdatedAt = config.tokenUpdatedAt ?? void 0;
2473
2494
  const tokenExpiry = token && tokenUpdatedAt ? resolveTokenExpiry(token, tokenUpdatedAt) : null;
2474
- if (!(!!tokenExpiry && Date.now() < Date.parse(tokenExpiry)) && !hasAuthConfig) {
2495
+ const hasValidToken = !!tokenExpiry && Date.now() < Date.parse(tokenExpiry);
2496
+ let pendingAuth;
2497
+ if (!hasValidToken && !hasAuthConfig) {
2475
2498
  if (!process.stdin.isTTY) throw new Error("Missing auth credentials. Create ~/.config/kahunas/auth.json or run 'kahunas sync' in a terminal.");
2476
2499
  const login = await askQuestion("Email or username: ", process.stderr);
2477
2500
  if (!login) throw new Error("Missing email/username for login.");
2478
- const password = await askQuestion("Password: ", process.stderr);
2501
+ const password = await askHiddenQuestion("Password: ", process.stderr);
2479
2502
  if (!password) throw new Error("Missing password for login.");
2480
2503
  const isEmail = login.includes("@");
2481
- writeAuthConfig({
2504
+ pendingAuth = {
2482
2505
  email: isEmail ? login : void 0,
2483
2506
  username: isEmail ? void 0 : login,
2484
2507
  password
2485
- });
2486
- console.error(`Saved credentials to ${AUTH_PATH}`);
2508
+ };
2487
2509
  }
2488
- const captured = await captureWorkoutsFromBrowser(options, config);
2510
+ const captured = await captureWorkoutsFromBrowser(options, config, pendingAuth);
2489
2511
  const nextConfig = { ...config };
2490
2512
  if (captured.token) {
2491
2513
  nextConfig.token = captured.token;
@@ -2507,8 +2529,12 @@ async function handleWorkout(positionals, options) {
2507
2529
  path: WORKOUT_CACHE_PATH
2508
2530
  }
2509
2531
  }, null, 2));
2532
+ if (pendingAuth && captured.token) {
2533
+ writeAuthConfig(pendingAuth);
2534
+ console.error(`Saved credentials to ${AUTH_PATH}`);
2535
+ } else if (pendingAuth) console.error("Login was not detected; credentials were not saved.");
2510
2536
  if (process.stdin.isTTY) {
2511
- if ((await askQuestion("Start the preview server now? (y/N): ", process.stderr)).toLowerCase().startsWith("y")) await handleWorkout(["serve"], options);
2537
+ if ((await askQuestion("Start the preview server now? (y/N): ", process.stderr)).toLowerCase().startsWith("y")) await startWorkoutServer();
2512
2538
  }
2513
2539
  return;
2514
2540
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kahunas-cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {