kahunas-cli 1.0.5 → 1.0.6
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 +23 -0
- package/dist/cli.js +592 -83
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -64,6 +64,8 @@ Tokens are saved to:
|
|
|
64
64
|
- Loads the most recently updated program.
|
|
65
65
|
- `kahunas workout events`
|
|
66
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.
|
|
67
69
|
- `kahunas workout program <id>`
|
|
68
70
|
- Fetches a program by UUID.
|
|
69
71
|
|
|
@@ -112,6 +114,27 @@ If the user UUID is missing, `workout events` will attempt to discover it from c
|
|
|
112
114
|
- `KAHUNAS_USER_UUID=...`
|
|
113
115
|
- `--user <uuid>`
|
|
114
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
|
+
|
|
115
138
|
## Auto-login
|
|
116
139
|
|
|
117
140
|
Most commands auto-login by default if a token is missing or expired. To disable:
|
package/dist/cli.js
CHANGED
|
@@ -34,6 +34,7 @@ let node_path = require("node:path");
|
|
|
34
34
|
node_path = __toESM(node_path);
|
|
35
35
|
let node_readline = require("node:readline");
|
|
36
36
|
node_readline = __toESM(node_readline);
|
|
37
|
+
let node_http = require("node:http");
|
|
37
38
|
|
|
38
39
|
//#region src/args.ts
|
|
39
40
|
function parseArgs(argv) {
|
|
@@ -532,7 +533,7 @@ async function loginAndPersist(options, config, outputMode) {
|
|
|
532
533
|
//#endregion
|
|
533
534
|
//#region src/usage.ts
|
|
534
535
|
function printUsage() {
|
|
535
|
-
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 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}`);
|
|
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}`);
|
|
536
537
|
}
|
|
537
538
|
|
|
538
539
|
//#endregion
|
|
@@ -688,6 +689,32 @@ async function handleCheckins(positionals, options) {
|
|
|
688
689
|
|
|
689
690
|
//#endregion
|
|
690
691
|
//#region src/events.ts
|
|
692
|
+
function summarizeWorkoutProgramDays(program) {
|
|
693
|
+
const candidates = extractProgramDayCandidates(program);
|
|
694
|
+
if (candidates.length === 0) return [];
|
|
695
|
+
const seen = /* @__PURE__ */ new Set();
|
|
696
|
+
const unique = [];
|
|
697
|
+
for (const candidate of candidates) {
|
|
698
|
+
const key = [
|
|
699
|
+
candidate.day_index ?? "none",
|
|
700
|
+
candidate.day_label ?? "unknown",
|
|
701
|
+
candidate.sections.length
|
|
702
|
+
].join("|");
|
|
703
|
+
if (seen.has(key)) continue;
|
|
704
|
+
seen.add(key);
|
|
705
|
+
unique.push(candidate);
|
|
706
|
+
}
|
|
707
|
+
return unique.sort((a, b) => {
|
|
708
|
+
const aIndex = a.day_index;
|
|
709
|
+
const bIndex = b.day_index;
|
|
710
|
+
if (aIndex !== void 0 && bIndex !== void 0) return aIndex - bIndex;
|
|
711
|
+
if (aIndex !== void 0) return -1;
|
|
712
|
+
if (bIndex !== void 0) return 1;
|
|
713
|
+
const aLabel = a.day_label ?? "";
|
|
714
|
+
const bLabel = b.day_label ?? "";
|
|
715
|
+
return aLabel.localeCompare(bLabel);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
691
718
|
function filterWorkoutEvents(payload, programFilter, workoutFilter) {
|
|
692
719
|
if (!Array.isArray(payload)) return [];
|
|
693
720
|
return payload.filter((entry) => {
|
|
@@ -1437,6 +1464,355 @@ function formatPathKey(key) {
|
|
|
1437
1464
|
return `\"${key.replace(/\"/g, "\\\"")}\"`;
|
|
1438
1465
|
}
|
|
1439
1466
|
|
|
1467
|
+
//#endregion
|
|
1468
|
+
//#region src/server/workout-view.ts
|
|
1469
|
+
function renderWorkoutPage(options) {
|
|
1470
|
+
const { summary, days, selectedDayIndex, timezone, apiPath, refreshPath, isLatest } = options;
|
|
1471
|
+
const eventTitle = summary?.event.title ?? "Workout";
|
|
1472
|
+
const eventStart = summary?.event.start ?? "";
|
|
1473
|
+
const programTitle = summary?.program?.title ?? "Program";
|
|
1474
|
+
const selected = selectedDayIndex !== void 0 && days[selectedDayIndex] ? days[selectedDayIndex] : summary?.workout_day ?? null;
|
|
1475
|
+
const tabs = days.length > 0 ? `<nav class="tabs">${days.map((day, index) => {
|
|
1476
|
+
const label = escapeHtml(day.day_label ?? `Day ${index + 1}`);
|
|
1477
|
+
return `<a class="tab ${index === selectedDayIndex ? "active" : ""}" href="/?day=${index}">${label}</a>`;
|
|
1478
|
+
}).join("")}</nav>` : "";
|
|
1479
|
+
const totalVolumeSets = selected?.total_volume_sets ?? [];
|
|
1480
|
+
const totalVolumeSection = totalVolumeSets.length > 0 ? `
|
|
1481
|
+
<section class="section">
|
|
1482
|
+
<h2>Total Volume Sets</h2>
|
|
1483
|
+
<div class="chips">
|
|
1484
|
+
${totalVolumeSets.map((entry) => `<span class="chip">${escapeHtml(entry.body_part)} ${formatNumber(entry.sets)}</span>`).join("")}
|
|
1485
|
+
</div>
|
|
1486
|
+
</section>
|
|
1487
|
+
` : "";
|
|
1488
|
+
const sections = selected?.sections?.length ? selected.sections.map((section) => {
|
|
1489
|
+
const groups = section.groups.map((group, groupIndex) => {
|
|
1490
|
+
return `<div class="group">${group.type === "superset" ? `<div class="group-label">Superset</div>` : ""}${group.exercises.map((exercise, rowIndex) => renderExerciseRow(exercise, groupIndex + rowIndex)).join("")}</div>`;
|
|
1491
|
+
}).join("");
|
|
1492
|
+
return `
|
|
1493
|
+
<section class="section">
|
|
1494
|
+
<h2>${escapeHtml(section.label)}</h2>
|
|
1495
|
+
<div class="section-body">${groups}</div>
|
|
1496
|
+
</section>
|
|
1497
|
+
`;
|
|
1498
|
+
}).join("") : `<div class="empty">No workout data found for this day.</div>`;
|
|
1499
|
+
return `<!doctype html>
|
|
1500
|
+
<html lang="en">
|
|
1501
|
+
<head>
|
|
1502
|
+
<meta charset="utf-8" />
|
|
1503
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1504
|
+
<title>${escapeHtml(programTitle)} | Workout</title>
|
|
1505
|
+
<style>
|
|
1506
|
+
@import url("https://fonts.googleapis.com/css2?family=Fraunces:wght@600;700&family=Spline+Sans:wght@400;500;600&display=swap");
|
|
1507
|
+
|
|
1508
|
+
:root {
|
|
1509
|
+
--bg-1: #f6f7fb;
|
|
1510
|
+
--bg-2: #eef3f9;
|
|
1511
|
+
--card: #ffffff;
|
|
1512
|
+
--ink: #1f2430;
|
|
1513
|
+
--muted: #6b7280;
|
|
1514
|
+
--line: #e3e8f2;
|
|
1515
|
+
--accent: #2f7f6f;
|
|
1516
|
+
--accent-soft: #e7f5f1;
|
|
1517
|
+
--chip: #eaf5ff;
|
|
1518
|
+
--shadow: 0 12px 30px rgba(31, 36, 48, 0.08);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
* {
|
|
1522
|
+
box-sizing: border-box;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
body {
|
|
1526
|
+
margin: 0;
|
|
1527
|
+
font-family: "Spline Sans", sans-serif;
|
|
1528
|
+
color: var(--ink);
|
|
1529
|
+
background: linear-gradient(160deg, var(--bg-1), var(--bg-2));
|
|
1530
|
+
min-height: 100vh;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
body::before {
|
|
1534
|
+
content: "";
|
|
1535
|
+
position: fixed;
|
|
1536
|
+
inset: 0;
|
|
1537
|
+
background: radial-gradient(circle at top right, rgba(47, 127, 111, 0.12), transparent 45%);
|
|
1538
|
+
pointer-events: none;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
.page {
|
|
1542
|
+
max-width: 1120px;
|
|
1543
|
+
margin: 0 auto;
|
|
1544
|
+
padding: 40px 24px 72px;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
.hero {
|
|
1548
|
+
background: var(--card);
|
|
1549
|
+
border-radius: 28px;
|
|
1550
|
+
padding: 28px 32px;
|
|
1551
|
+
box-shadow: var(--shadow);
|
|
1552
|
+
display: grid;
|
|
1553
|
+
gap: 20px;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
.title {
|
|
1557
|
+
font-family: "Fraunces", serif;
|
|
1558
|
+
font-size: 32px;
|
|
1559
|
+
margin: 0;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
.subtitle {
|
|
1563
|
+
color: var(--muted);
|
|
1564
|
+
font-size: 14px;
|
|
1565
|
+
letter-spacing: 0.02em;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
.meta {
|
|
1569
|
+
display: flex;
|
|
1570
|
+
flex-wrap: wrap;
|
|
1571
|
+
gap: 12px;
|
|
1572
|
+
align-items: center;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
.meta-pill {
|
|
1576
|
+
background: var(--accent-soft);
|
|
1577
|
+
color: var(--accent);
|
|
1578
|
+
padding: 6px 12px;
|
|
1579
|
+
border-radius: 999px;
|
|
1580
|
+
font-weight: 600;
|
|
1581
|
+
font-size: 12px;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
.meta-pill.latest {
|
|
1585
|
+
background: #1f2a3d;
|
|
1586
|
+
color: #f5f7ff;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
.actions {
|
|
1590
|
+
display: flex;
|
|
1591
|
+
gap: 12px;
|
|
1592
|
+
flex-wrap: wrap;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
.button {
|
|
1596
|
+
display: inline-flex;
|
|
1597
|
+
align-items: center;
|
|
1598
|
+
gap: 8px;
|
|
1599
|
+
background: var(--card);
|
|
1600
|
+
border: 1px solid var(--line);
|
|
1601
|
+
border-radius: 999px;
|
|
1602
|
+
padding: 8px 16px;
|
|
1603
|
+
text-decoration: none;
|
|
1604
|
+
color: var(--ink);
|
|
1605
|
+
font-weight: 600;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
.tabs {
|
|
1609
|
+
display: flex;
|
|
1610
|
+
gap: 10px;
|
|
1611
|
+
flex-wrap: wrap;
|
|
1612
|
+
padding: 18px 0 4px;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
.tab {
|
|
1616
|
+
padding: 8px 14px;
|
|
1617
|
+
border-radius: 999px;
|
|
1618
|
+
border: 1px solid var(--line);
|
|
1619
|
+
text-decoration: none;
|
|
1620
|
+
color: var(--muted);
|
|
1621
|
+
font-weight: 600;
|
|
1622
|
+
background: #f9fbff;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
.tab.active {
|
|
1626
|
+
color: var(--accent);
|
|
1627
|
+
border-color: var(--accent);
|
|
1628
|
+
background: #f2fbf8;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
.section {
|
|
1632
|
+
margin-top: 28px;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
.section h2 {
|
|
1636
|
+
margin: 0 0 14px;
|
|
1637
|
+
font-size: 18px;
|
|
1638
|
+
font-weight: 700;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
.section-body {
|
|
1642
|
+
display: grid;
|
|
1643
|
+
gap: 14px;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
.group {
|
|
1647
|
+
display: grid;
|
|
1648
|
+
gap: 10px;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
.group-label {
|
|
1652
|
+
font-size: 12px;
|
|
1653
|
+
text-transform: uppercase;
|
|
1654
|
+
letter-spacing: 0.1em;
|
|
1655
|
+
color: var(--muted);
|
|
1656
|
+
padding-left: 6px;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
.exercise {
|
|
1660
|
+
background: var(--card);
|
|
1661
|
+
border-radius: 16px;
|
|
1662
|
+
border: 1px solid var(--line);
|
|
1663
|
+
padding: 16px 18px;
|
|
1664
|
+
display: grid;
|
|
1665
|
+
grid-template-columns: 56px 1fr auto;
|
|
1666
|
+
gap: 12px;
|
|
1667
|
+
align-items: center;
|
|
1668
|
+
box-shadow: 0 4px 16px rgba(31, 36, 48, 0.04);
|
|
1669
|
+
animation: fadeSlide 0.5s ease both;
|
|
1670
|
+
animation-delay: var(--delay, 0s);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
.badge {
|
|
1674
|
+
width: 46px;
|
|
1675
|
+
height: 46px;
|
|
1676
|
+
border-radius: 14px;
|
|
1677
|
+
background: var(--chip);
|
|
1678
|
+
display: flex;
|
|
1679
|
+
align-items: center;
|
|
1680
|
+
justify-content: center;
|
|
1681
|
+
font-weight: 700;
|
|
1682
|
+
color: #275b8b;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
.exercise h3 {
|
|
1686
|
+
margin: 0;
|
|
1687
|
+
font-size: 16px;
|
|
1688
|
+
font-weight: 700;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
.exercise p {
|
|
1692
|
+
margin: 4px 0 0;
|
|
1693
|
+
color: var(--muted);
|
|
1694
|
+
font-size: 13px;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
.metrics {
|
|
1698
|
+
display: flex;
|
|
1699
|
+
flex-direction: column;
|
|
1700
|
+
gap: 6px;
|
|
1701
|
+
font-size: 13px;
|
|
1702
|
+
color: var(--muted);
|
|
1703
|
+
min-width: 140px;
|
|
1704
|
+
text-align: right;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
.chips {
|
|
1708
|
+
display: flex;
|
|
1709
|
+
flex-wrap: wrap;
|
|
1710
|
+
gap: 8px;
|
|
1711
|
+
margin-top: 12px;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
.chip {
|
|
1715
|
+
padding: 6px 12px;
|
|
1716
|
+
border-radius: 999px;
|
|
1717
|
+
background: #e9f7f2;
|
|
1718
|
+
color: #2f7f6f;
|
|
1719
|
+
font-size: 12px;
|
|
1720
|
+
font-weight: 600;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
.chip.muted {
|
|
1724
|
+
background: #f1f3f7;
|
|
1725
|
+
color: var(--muted);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
.empty {
|
|
1729
|
+
margin-top: 20px;
|
|
1730
|
+
padding: 24px;
|
|
1731
|
+
border-radius: 16px;
|
|
1732
|
+
border: 1px dashed var(--line);
|
|
1733
|
+
color: var(--muted);
|
|
1734
|
+
background: rgba(255, 255, 255, 0.6);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
@keyframes fadeSlide {
|
|
1738
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
1739
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
@media (max-width: 720px) {
|
|
1743
|
+
.exercise {
|
|
1744
|
+
grid-template-columns: 48px 1fr;
|
|
1745
|
+
}
|
|
1746
|
+
.metrics {
|
|
1747
|
+
text-align: left;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
</style>
|
|
1751
|
+
</head>
|
|
1752
|
+
<body>
|
|
1753
|
+
<div class="page">
|
|
1754
|
+
<header class="hero">
|
|
1755
|
+
<div>
|
|
1756
|
+
<div class="subtitle">${escapeHtml(programTitle)}</div>
|
|
1757
|
+
<h1 class="title">${escapeHtml(eventTitle)}</h1>
|
|
1758
|
+
<div class="meta">
|
|
1759
|
+
${eventStart ? `<span class="meta-pill">${escapeHtml(eventStart)}</span>` : ""}
|
|
1760
|
+
<span class="meta-pill">${escapeHtml(timezone)}</span>
|
|
1761
|
+
${isLatest ? `<span class="meta-pill latest">Latest event</span>` : ""}
|
|
1762
|
+
</div>
|
|
1763
|
+
</div>
|
|
1764
|
+
<div class="actions">
|
|
1765
|
+
<a class="button" href="${escapeHtml(refreshPath)}">Refresh</a>
|
|
1766
|
+
<a class="button" href="${escapeHtml(apiPath)}" target="_blank">JSON</a>
|
|
1767
|
+
</div>
|
|
1768
|
+
${tabs}
|
|
1769
|
+
</header>
|
|
1770
|
+
|
|
1771
|
+
${totalVolumeSection}
|
|
1772
|
+
${sections}
|
|
1773
|
+
</div>
|
|
1774
|
+
</body>
|
|
1775
|
+
</html>`;
|
|
1776
|
+
}
|
|
1777
|
+
function renderExerciseRow(exercise, index) {
|
|
1778
|
+
const badge = escapeHtml(exercise.sequence ?? String(index + 1));
|
|
1779
|
+
const metrics = [
|
|
1780
|
+
exercise.sets !== void 0 ? `Sets ${exercise.sets}` : null,
|
|
1781
|
+
exercise.reps ? `Reps ${escapeHtml(exercise.reps)}` : null,
|
|
1782
|
+
exercise.rest_seconds !== void 0 ? `Rest ${formatDuration(exercise.rest_seconds)}` : null,
|
|
1783
|
+
exercise.time_seconds !== void 0 ? `Time ${formatDuration(exercise.time_seconds)}` : null
|
|
1784
|
+
].filter(Boolean).join(" | ");
|
|
1785
|
+
return `
|
|
1786
|
+
<div class="exercise" style="--delay: ${Math.min(index * .04, .4)}s">
|
|
1787
|
+
<div class="badge">${badge}</div>
|
|
1788
|
+
<div>
|
|
1789
|
+
<h3>${escapeHtml(exercise.name)}</h3>
|
|
1790
|
+
<p>${metrics || "No prescription"}</p>
|
|
1791
|
+
</div>
|
|
1792
|
+
<div class="metrics">
|
|
1793
|
+
${exercise.body_parts?.length ? formatBodyParts(exercise.body_parts) : ""}
|
|
1794
|
+
</div>
|
|
1795
|
+
</div>
|
|
1796
|
+
`;
|
|
1797
|
+
}
|
|
1798
|
+
function formatBodyParts(parts) {
|
|
1799
|
+
return parts.slice(0, 3).map((part) => `${escapeHtml(part.name)}${part.volume !== void 0 ? ` ${formatNumber(part.volume)}` : ""}`).join("<br />");
|
|
1800
|
+
}
|
|
1801
|
+
function formatDuration(value) {
|
|
1802
|
+
const total = Math.max(0, Math.round(value));
|
|
1803
|
+
const minutes = Math.floor(total / 60);
|
|
1804
|
+
const seconds = total % 60;
|
|
1805
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
1806
|
+
return `${seconds}s`;
|
|
1807
|
+
}
|
|
1808
|
+
function formatNumber(value) {
|
|
1809
|
+
if (Number.isInteger(value)) return String(value);
|
|
1810
|
+
return value.toFixed(1).replace(/\.0$/, "");
|
|
1811
|
+
}
|
|
1812
|
+
function escapeHtml(value) {
|
|
1813
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """).replace(/'/g, "'");
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1440
1816
|
//#endregion
|
|
1441
1817
|
//#region src/commands/workout.ts
|
|
1442
1818
|
async function handleWorkout(positionals, options) {
|
|
@@ -1453,6 +1829,14 @@ async function handleWorkout(positionals, options) {
|
|
|
1453
1829
|
else throw new Error("Missing auth token. Set KAHUNAS_TOKEN or run 'kahunas auth login'.");
|
|
1454
1830
|
return token;
|
|
1455
1831
|
};
|
|
1832
|
+
let webLoginInFlight = null;
|
|
1833
|
+
const ensureWebLogin = async () => {
|
|
1834
|
+
if (!autoLogin) return;
|
|
1835
|
+
if (!webLoginInFlight) webLoginInFlight = loginAndPersist(options, config, "silent").then(() => void 0).finally(() => {
|
|
1836
|
+
webLoginInFlight = null;
|
|
1837
|
+
});
|
|
1838
|
+
await webLoginInFlight;
|
|
1839
|
+
};
|
|
1456
1840
|
const baseUrl = resolveBaseUrl(options, config);
|
|
1457
1841
|
const rawOutput = isFlagEnabled(options, "raw");
|
|
1458
1842
|
const page = parseNumber$1(options.page, 1);
|
|
@@ -1477,67 +1861,7 @@ async function handleWorkout(positionals, options) {
|
|
|
1477
1861
|
cache
|
|
1478
1862
|
};
|
|
1479
1863
|
};
|
|
1480
|
-
|
|
1481
|
-
const { response, plans, cache } = await fetchList();
|
|
1482
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.text}`);
|
|
1483
|
-
if (rawOutput) {
|
|
1484
|
-
printResponse(response, rawOutput);
|
|
1485
|
-
return;
|
|
1486
|
-
}
|
|
1487
|
-
const output = {
|
|
1488
|
-
source: cache ? "api+cache" : "api",
|
|
1489
|
-
cache: cache ? {
|
|
1490
|
-
updated_at: cache.updatedAt,
|
|
1491
|
-
count: cache.plans.length,
|
|
1492
|
-
path: WORKOUT_CACHE_PATH
|
|
1493
|
-
} : void 0,
|
|
1494
|
-
data: { workout_plan: plans }
|
|
1495
|
-
};
|
|
1496
|
-
console.log(JSON.stringify(output, null, 2));
|
|
1497
|
-
return;
|
|
1498
|
-
}
|
|
1499
|
-
if (action === "pick") {
|
|
1500
|
-
const { response, plans } = await fetchList();
|
|
1501
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.text}`);
|
|
1502
|
-
if (plans.length === 0) throw new Error("No workout programs found.");
|
|
1503
|
-
if (!rawOutput) {
|
|
1504
|
-
console.log("Pick a workout program:");
|
|
1505
|
-
plans.forEach((plan, index) => {
|
|
1506
|
-
console.log(`${index + 1}) ${formatWorkoutSummary(plan)}`);
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
const answer = await askQuestion(`Enter number (1-${plans.length}): `);
|
|
1510
|
-
const selection = Number.parseInt(answer, 10);
|
|
1511
|
-
if (Number.isNaN(selection) || selection < 1 || selection > plans.length) throw new Error("Invalid selection.");
|
|
1512
|
-
const chosen = plans[selection - 1];
|
|
1513
|
-
if (!chosen.uuid) throw new Error("Selected workout is missing a uuid.");
|
|
1514
|
-
const csrfToken$1 = resolveCsrfToken(options, config);
|
|
1515
|
-
let responseProgram$1 = await fetchWorkoutProgram(await ensureToken(), baseUrl, chosen.uuid, csrfToken$1);
|
|
1516
|
-
if (autoLogin && isTokenExpiredResponse(responseProgram$1.json)) {
|
|
1517
|
-
token = await loginAndPersist(options, config, "silent");
|
|
1518
|
-
responseProgram$1 = await fetchWorkoutProgram(token, baseUrl, chosen.uuid, csrfToken$1);
|
|
1519
|
-
}
|
|
1520
|
-
if (!responseProgram$1.ok) throw new Error(`HTTP ${responseProgram$1.status}: ${responseProgram$1.text}`);
|
|
1521
|
-
printResponse(responseProgram$1, rawOutput);
|
|
1522
|
-
return;
|
|
1523
|
-
}
|
|
1524
|
-
if (action === "latest") {
|
|
1525
|
-
const { response, plans } = await fetchList();
|
|
1526
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.text}`);
|
|
1527
|
-
if (plans.length === 0) throw new Error("No workout programs found.");
|
|
1528
|
-
const chosen = pickLatestWorkout(plans);
|
|
1529
|
-
if (!chosen || !chosen.uuid) throw new Error("Latest workout is missing a uuid.");
|
|
1530
|
-
const csrfToken$1 = resolveCsrfToken(options, config);
|
|
1531
|
-
let responseProgram$1 = await fetchWorkoutProgram(await ensureToken(), baseUrl, chosen.uuid, csrfToken$1);
|
|
1532
|
-
if (autoLogin && isTokenExpiredResponse(responseProgram$1.json)) {
|
|
1533
|
-
token = await loginAndPersist(options, config, "silent");
|
|
1534
|
-
responseProgram$1 = await fetchWorkoutProgram(token, baseUrl, chosen.uuid, csrfToken$1);
|
|
1535
|
-
}
|
|
1536
|
-
if (!responseProgram$1.ok) throw new Error(`HTTP ${responseProgram$1.status}: ${responseProgram$1.text}`);
|
|
1537
|
-
printResponse(responseProgram$1, rawOutput);
|
|
1538
|
-
return;
|
|
1539
|
-
}
|
|
1540
|
-
if (action === "events") {
|
|
1864
|
+
const fetchWorkoutEventsPayload = async () => {
|
|
1541
1865
|
const baseWebUrl = resolveWebBaseUrl(options, config);
|
|
1542
1866
|
const webOrigin = new URL(baseWebUrl).origin;
|
|
1543
1867
|
const timezone = options.timezone ?? process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "Europe/London";
|
|
@@ -1571,17 +1895,13 @@ async function handleWorkout(positionals, options) {
|
|
|
1571
1895
|
...config,
|
|
1572
1896
|
userUuid
|
|
1573
1897
|
});
|
|
1574
|
-
const minimal = isFlagEnabled(options, "minimal");
|
|
1575
|
-
const full = isFlagEnabled(options, "full");
|
|
1576
|
-
const debugPreview = isFlagEnabled(options, "debug-preview");
|
|
1577
|
-
const limit = isFlagEnabled(options, "latest") || isFlagEnabled(options, "last") ? 1 : parseNumber$1(options.limit, 0);
|
|
1578
1898
|
let csrfToken$1 = resolveCsrfToken(options, config);
|
|
1579
1899
|
let csrfCookie = resolveCsrfCookie(options, config);
|
|
1580
1900
|
let authCookie = resolveAuthCookie(options, config);
|
|
1581
1901
|
let effectiveCsrfToken = csrfCookie ?? csrfToken$1;
|
|
1582
1902
|
let cookieHeader = authCookie ?? (effectiveCsrfToken ? `csrf_kahunas_cookie_token=${effectiveCsrfToken}` : void 0);
|
|
1583
1903
|
if ((!csrfToken$1 || !cookieHeader || !authCookie) && autoLogin) {
|
|
1584
|
-
await
|
|
1904
|
+
await ensureWebLogin();
|
|
1585
1905
|
const refreshed = readConfig();
|
|
1586
1906
|
csrfToken$1 = resolveCsrfToken(options, refreshed);
|
|
1587
1907
|
csrfCookie = resolveCsrfCookie(options, refreshed);
|
|
@@ -1611,7 +1931,7 @@ async function handleWorkout(positionals, options) {
|
|
|
1611
1931
|
let text = await response.text();
|
|
1612
1932
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
|
|
1613
1933
|
if (autoLogin && isLikelyLoginHtml(text)) {
|
|
1614
|
-
await
|
|
1934
|
+
await ensureWebLogin();
|
|
1615
1935
|
const refreshed = readConfig();
|
|
1616
1936
|
csrfToken$1 = resolveCsrfToken(options, refreshed);
|
|
1617
1937
|
csrfCookie = resolveCsrfCookie(options, refreshed);
|
|
@@ -1634,21 +1954,14 @@ async function handleWorkout(positionals, options) {
|
|
|
1634
1954
|
text = await retry.text();
|
|
1635
1955
|
if (!retry.ok) throw new Error(`HTTP ${retry.status}: ${text}`);
|
|
1636
1956
|
}
|
|
1637
|
-
if (rawOutput) {
|
|
1638
|
-
console.log(text);
|
|
1639
|
-
return;
|
|
1640
|
-
}
|
|
1641
1957
|
const payload = parseJsonText(text);
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
console.log(JSON.stringify(limited, null, 2));
|
|
1650
|
-
return;
|
|
1651
|
-
}
|
|
1958
|
+
return {
|
|
1959
|
+
text,
|
|
1960
|
+
payload,
|
|
1961
|
+
timezone
|
|
1962
|
+
};
|
|
1963
|
+
};
|
|
1964
|
+
const buildProgramDetails = async (events) => {
|
|
1652
1965
|
let programIndex;
|
|
1653
1966
|
let plans = readWorkoutCache()?.plans ?? [];
|
|
1654
1967
|
try {
|
|
@@ -1664,8 +1977,11 @@ async function handleWorkout(positionals, options) {
|
|
|
1664
1977
|
if (listResponse.ok) plans = mergeWorkoutPlans(extractWorkoutPlans(listResponse.json), plans);
|
|
1665
1978
|
} catch {}
|
|
1666
1979
|
if (plans.length > 0) programIndex = buildWorkoutPlanIndex(plans);
|
|
1980
|
+
const refreshed = readConfig();
|
|
1981
|
+
const csrfToken$1 = resolveCsrfToken(options, refreshed);
|
|
1982
|
+
const effectiveCsrfToken = resolveCsrfCookie(options, refreshed) ?? csrfToken$1;
|
|
1667
1983
|
const programDetails = {};
|
|
1668
|
-
const programIds = Array.from(new Set(
|
|
1984
|
+
const programIds = Array.from(new Set(events.map((entry) => {
|
|
1669
1985
|
if (!entry || typeof entry !== "object") return;
|
|
1670
1986
|
const record = entry;
|
|
1671
1987
|
return typeof record.program === "string" ? record.program : void 0;
|
|
@@ -1694,6 +2010,109 @@ async function handleWorkout(positionals, options) {
|
|
|
1694
2010
|
} catch {}
|
|
1695
2011
|
programDetails[programId$1] = programIndex?.[programId$1] ?? null;
|
|
1696
2012
|
}
|
|
2013
|
+
return programDetails;
|
|
2014
|
+
};
|
|
2015
|
+
const resolveSelectedDayIndex = (days, eventDayIndex, eventDayLabel, dayParam) => {
|
|
2016
|
+
const parseOptionalInt = (value) => {
|
|
2017
|
+
if (!value) return;
|
|
2018
|
+
const parsed = Number.parseInt(value, 10);
|
|
2019
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2020
|
+
};
|
|
2021
|
+
const normalize = (value) => value.trim().toLowerCase();
|
|
2022
|
+
const paramIndex = parseOptionalInt(dayParam);
|
|
2023
|
+
if (paramIndex !== void 0 && days[paramIndex]) return paramIndex;
|
|
2024
|
+
if (eventDayIndex !== void 0) {
|
|
2025
|
+
const matchIndex = days.findIndex((day) => day.day_index === eventDayIndex);
|
|
2026
|
+
if (matchIndex >= 0) return matchIndex;
|
|
2027
|
+
}
|
|
2028
|
+
if (eventDayLabel) {
|
|
2029
|
+
const normalized = normalize(eventDayLabel);
|
|
2030
|
+
const matchIndex = days.findIndex((day) => day.day_label ? normalize(day.day_label).includes(normalized) : false);
|
|
2031
|
+
if (matchIndex >= 0) return matchIndex;
|
|
2032
|
+
}
|
|
2033
|
+
if (days.length > 0) return 0;
|
|
2034
|
+
};
|
|
2035
|
+
if (action === "list") {
|
|
2036
|
+
const { response, plans, cache } = await fetchList();
|
|
2037
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.text}`);
|
|
2038
|
+
if (rawOutput) {
|
|
2039
|
+
printResponse(response, rawOutput);
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
const output = {
|
|
2043
|
+
source: cache ? "api+cache" : "api",
|
|
2044
|
+
cache: cache ? {
|
|
2045
|
+
updated_at: cache.updatedAt,
|
|
2046
|
+
count: cache.plans.length,
|
|
2047
|
+
path: WORKOUT_CACHE_PATH
|
|
2048
|
+
} : void 0,
|
|
2049
|
+
data: { workout_plan: plans }
|
|
2050
|
+
};
|
|
2051
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
if (action === "pick") {
|
|
2055
|
+
const { response, plans } = await fetchList();
|
|
2056
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.text}`);
|
|
2057
|
+
if (plans.length === 0) throw new Error("No workout programs found.");
|
|
2058
|
+
if (!rawOutput) {
|
|
2059
|
+
console.log("Pick a workout program:");
|
|
2060
|
+
plans.forEach((plan, index) => {
|
|
2061
|
+
console.log(`${index + 1}) ${formatWorkoutSummary(plan)}`);
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
const answer = await askQuestion(`Enter number (1-${plans.length}): `);
|
|
2065
|
+
const selection = Number.parseInt(answer, 10);
|
|
2066
|
+
if (Number.isNaN(selection) || selection < 1 || selection > plans.length) throw new Error("Invalid selection.");
|
|
2067
|
+
const chosen = plans[selection - 1];
|
|
2068
|
+
if (!chosen.uuid) throw new Error("Selected workout is missing a uuid.");
|
|
2069
|
+
const csrfToken$1 = resolveCsrfToken(options, config);
|
|
2070
|
+
let responseProgram$1 = await fetchWorkoutProgram(await ensureToken(), baseUrl, chosen.uuid, csrfToken$1);
|
|
2071
|
+
if (autoLogin && isTokenExpiredResponse(responseProgram$1.json)) {
|
|
2072
|
+
token = await loginAndPersist(options, config, "silent");
|
|
2073
|
+
responseProgram$1 = await fetchWorkoutProgram(token, baseUrl, chosen.uuid, csrfToken$1);
|
|
2074
|
+
}
|
|
2075
|
+
if (!responseProgram$1.ok) throw new Error(`HTTP ${responseProgram$1.status}: ${responseProgram$1.text}`);
|
|
2076
|
+
printResponse(responseProgram$1, rawOutput);
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
if (action === "latest") {
|
|
2080
|
+
const { response, plans } = await fetchList();
|
|
2081
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.text}`);
|
|
2082
|
+
if (plans.length === 0) throw new Error("No workout programs found.");
|
|
2083
|
+
const chosen = pickLatestWorkout(plans);
|
|
2084
|
+
if (!chosen || !chosen.uuid) throw new Error("Latest workout is missing a uuid.");
|
|
2085
|
+
const csrfToken$1 = resolveCsrfToken(options, config);
|
|
2086
|
+
let responseProgram$1 = await fetchWorkoutProgram(await ensureToken(), baseUrl, chosen.uuid, csrfToken$1);
|
|
2087
|
+
if (autoLogin && isTokenExpiredResponse(responseProgram$1.json)) {
|
|
2088
|
+
token = await loginAndPersist(options, config, "silent");
|
|
2089
|
+
responseProgram$1 = await fetchWorkoutProgram(token, baseUrl, chosen.uuid, csrfToken$1);
|
|
2090
|
+
}
|
|
2091
|
+
if (!responseProgram$1.ok) throw new Error(`HTTP ${responseProgram$1.status}: ${responseProgram$1.text}`);
|
|
2092
|
+
printResponse(responseProgram$1, rawOutput);
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
if (action === "events") {
|
|
2096
|
+
const minimal = isFlagEnabled(options, "minimal");
|
|
2097
|
+
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);
|
|
2100
|
+
const { text, payload, timezone } = await fetchWorkoutEventsPayload();
|
|
2101
|
+
if (rawOutput) {
|
|
2102
|
+
console.log(text);
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
if (!Array.isArray(payload)) {
|
|
2106
|
+
console.log(text);
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
const sorted = sortWorkoutEvents(filterWorkoutEvents(payload, options.program, options.workout));
|
|
2110
|
+
const limited = limit > 0 ? sorted.slice(-limit) : sorted;
|
|
2111
|
+
if (minimal) {
|
|
2112
|
+
console.log(JSON.stringify(limited, null, 2));
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
const programDetails = await buildProgramDetails(sorted);
|
|
1697
2116
|
if (debugPreview) for (const entry of limited) {
|
|
1698
2117
|
const record = entry;
|
|
1699
2118
|
const eventId = typeof record.id === "string" || typeof record.id === "number" ? record.id : "unknown";
|
|
@@ -1717,6 +2136,96 @@ async function handleWorkout(positionals, options) {
|
|
|
1717
2136
|
console.log(JSON.stringify(formatted, null, 2));
|
|
1718
2137
|
return;
|
|
1719
2138
|
}
|
|
2139
|
+
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);
|
|
2144
|
+
const loadSummary = async () => {
|
|
2145
|
+
const { text, payload, timezone } = await fetchWorkoutEventsPayload();
|
|
2146
|
+
if (!Array.isArray(payload)) throw new Error(`Unexpected calendar response: ${text.slice(0, 200)}`);
|
|
2147
|
+
const sorted = sortWorkoutEvents(filterWorkoutEvents(payload, options.program, options.workout));
|
|
2148
|
+
const bounded = limit > 0 ? sorted.slice(-limit) : sorted;
|
|
2149
|
+
const programDetails = await buildProgramDetails(sorted);
|
|
2150
|
+
const formatted = formatWorkoutEventsOutput(bounded, programDetails, {
|
|
2151
|
+
timezone,
|
|
2152
|
+
program: options.program,
|
|
2153
|
+
workout: options.workout
|
|
2154
|
+
});
|
|
2155
|
+
const summary = formatted.events[0];
|
|
2156
|
+
const programUuid = summary?.program?.uuid ?? (bounded[0] && typeof bounded[0] === "object" ? bounded[0].program : void 0);
|
|
2157
|
+
return {
|
|
2158
|
+
formatted,
|
|
2159
|
+
days: summarizeWorkoutProgramDays(programUuid ? programDetails[programUuid] : void 0),
|
|
2160
|
+
summary,
|
|
2161
|
+
timezone
|
|
2162
|
+
};
|
|
2163
|
+
};
|
|
2164
|
+
let cached;
|
|
2165
|
+
let summaryInFlight = null;
|
|
2166
|
+
const getSummary = async (forceRefresh) => {
|
|
2167
|
+
if (!forceRefresh && cached && Date.now() - cached.fetchedAt < cacheTtlMs) return cached.data;
|
|
2168
|
+
if (!forceRefresh && summaryInFlight) return summaryInFlight;
|
|
2169
|
+
const pending = loadSummary().finally(() => {
|
|
2170
|
+
summaryInFlight = null;
|
|
2171
|
+
});
|
|
2172
|
+
summaryInFlight = pending;
|
|
2173
|
+
const data = await pending;
|
|
2174
|
+
cached = {
|
|
2175
|
+
data,
|
|
2176
|
+
fetchedAt: Date.now()
|
|
2177
|
+
};
|
|
2178
|
+
return data;
|
|
2179
|
+
};
|
|
2180
|
+
(0, node_http.createServer)(async (req, res) => {
|
|
2181
|
+
try {
|
|
2182
|
+
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
2183
|
+
const wantsRefresh = url.searchParams.get("refresh") === "1";
|
|
2184
|
+
if (url.pathname === "/api/workout") {
|
|
2185
|
+
const data$1 = await getSummary(wantsRefresh);
|
|
2186
|
+
res.statusCode = 200;
|
|
2187
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
2188
|
+
res.setHeader("cache-control", "no-store");
|
|
2189
|
+
res.end(JSON.stringify(data$1.formatted, null, 2));
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
if (url.pathname === "/favicon.ico") {
|
|
2193
|
+
res.statusCode = 204;
|
|
2194
|
+
res.end();
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
if (url.pathname !== "/") {
|
|
2198
|
+
res.statusCode = 404;
|
|
2199
|
+
res.end("Not found");
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
const data = await getSummary(wantsRefresh);
|
|
2203
|
+
const dayParam = url.searchParams.get("day");
|
|
2204
|
+
const selectedDayIndex = resolveSelectedDayIndex(data.days, data.summary?.workout_day?.day_index, data.summary?.workout_day?.day_label, dayParam);
|
|
2205
|
+
const html = renderWorkoutPage({
|
|
2206
|
+
summary: data.summary,
|
|
2207
|
+
days: data.days,
|
|
2208
|
+
selectedDayIndex,
|
|
2209
|
+
timezone: data.timezone,
|
|
2210
|
+
apiPath: "/api/workout",
|
|
2211
|
+
refreshPath: "/?refresh=1",
|
|
2212
|
+
isLatest: limit === 1
|
|
2213
|
+
});
|
|
2214
|
+
res.statusCode = 200;
|
|
2215
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
2216
|
+
res.setHeader("cache-control", "no-store");
|
|
2217
|
+
res.end(html);
|
|
2218
|
+
} catch (error) {
|
|
2219
|
+
res.statusCode = 500;
|
|
2220
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
2221
|
+
res.end(error instanceof Error ? error.message : "Server error");
|
|
2222
|
+
}
|
|
2223
|
+
}).listen(port, host, () => {
|
|
2224
|
+
console.log(`Local workout server running at http://${host}:${port}`);
|
|
2225
|
+
console.log(`JSON endpoint at http://${host}:${port}/api/workout`);
|
|
2226
|
+
});
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
1720
2229
|
if (action === "sync") {
|
|
1721
2230
|
const captured = await captureWorkoutsFromBrowser(options, config);
|
|
1722
2231
|
const nextConfig = { ...config };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kahunas-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^22.10.5",
|
|
26
|
+
"lefthook": "^2.0.15",
|
|
26
27
|
"rolldown": "1.0.0-beta.59",
|
|
27
28
|
"typescript": "^5.7.3",
|
|
28
29
|
"vitest": "^1.6.0"
|