kahunas-cli 1.0.4 → 1.0.5
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 +4 -4
- package/dist/cli.js +778 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Tokens are saved to:
|
|
|
63
63
|
- `kahunas workout latest`
|
|
64
64
|
- Loads the most recently updated program.
|
|
65
65
|
- `kahunas workout events`
|
|
66
|
-
- Lists workout log events with dates (from the calendar endpoint).
|
|
66
|
+
- Lists workout log events with dates and a human-friendly workout summary (from the calendar endpoint).
|
|
67
67
|
- `kahunas workout program <id>`
|
|
68
68
|
- Fetches a program by UUID.
|
|
69
69
|
|
|
@@ -84,7 +84,7 @@ Raw output (`--raw`) prints the API response only.
|
|
|
84
84
|
|
|
85
85
|
### Workout events (dates)
|
|
86
86
|
|
|
87
|
-
To see when workouts happened, the calendar endpoint returns log events with timestamps. By default each event is
|
|
87
|
+
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).
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
90
|
pnpm kahunas -- workout events --user <user-uuid>
|
|
@@ -105,9 +105,9 @@ pnpm kahunas -- workout events --program <program-uuid>
|
|
|
105
105
|
pnpm kahunas -- workout events --workout <workout-uuid>
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
Use `--minimal` to return the raw event objects without program enrichment.
|
|
108
|
+
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).
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
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:
|
|
111
111
|
|
|
112
112
|
- `KAHUNAS_USER_UUID=...`
|
|
113
113
|
- `--user <uuid>`
|
package/dist/cli.js
CHANGED
|
@@ -272,7 +272,7 @@ function extractUserUuidFromCheckins(payload) {
|
|
|
272
272
|
|
|
273
273
|
//#endregion
|
|
274
274
|
//#region src/utils.ts
|
|
275
|
-
function parseNumber(value, fallback) {
|
|
275
|
+
function parseNumber$1(value, fallback) {
|
|
276
276
|
if (!value) return fallback;
|
|
277
277
|
const parsed = Number.parseInt(value, 10);
|
|
278
278
|
if (Number.isNaN(parsed)) return fallback;
|
|
@@ -532,7 +532,7 @@ async function loginAndPersist(options, config, outputMode) {
|
|
|
532
532
|
//#endregion
|
|
533
533
|
//#region src/usage.ts
|
|
534
534
|
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] [--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}`);
|
|
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
536
|
}
|
|
537
537
|
|
|
538
538
|
//#endregion
|
|
@@ -663,8 +663,8 @@ async function handleCheckins(positionals, options) {
|
|
|
663
663
|
if (!token) if (autoLogin) token = await loginAndPersist(options, config, "silent");
|
|
664
664
|
else throw new Error("Missing auth token. Set KAHUNAS_TOKEN or run 'kahunas auth login'.");
|
|
665
665
|
const baseUrl = resolveBaseUrl(options, config);
|
|
666
|
-
const page = parseNumber(options.page, 1);
|
|
667
|
-
const rpp = parseNumber(options.rpp, 12);
|
|
666
|
+
const page = parseNumber$1(options.page, 1);
|
|
667
|
+
const rpp = parseNumber$1(options.rpp, 12);
|
|
668
668
|
const rawOutput = isFlagEnabled(options, "raw");
|
|
669
669
|
let response = await postJson("/api/v2/checkin/list", token, baseUrl, {
|
|
670
670
|
page,
|
|
@@ -715,6 +715,727 @@ function enrichWorkoutEvents(events, programDetails) {
|
|
|
715
715
|
};
|
|
716
716
|
});
|
|
717
717
|
}
|
|
718
|
+
const HTML_ENTITY_MAP = {
|
|
719
|
+
""": "\"",
|
|
720
|
+
""": "\"",
|
|
721
|
+
"'": "'",
|
|
722
|
+
"'": "'",
|
|
723
|
+
"&": "&",
|
|
724
|
+
"<": "<",
|
|
725
|
+
">": ">"
|
|
726
|
+
};
|
|
727
|
+
const DAY_INDEX_KEYS = [
|
|
728
|
+
"day_index",
|
|
729
|
+
"workout_day_index",
|
|
730
|
+
"day",
|
|
731
|
+
"workout_day",
|
|
732
|
+
"day_number",
|
|
733
|
+
"workout_day_number"
|
|
734
|
+
];
|
|
735
|
+
function formatWorkoutEventsOutput(events, programDetails, options) {
|
|
736
|
+
return {
|
|
737
|
+
source: "calendar",
|
|
738
|
+
timezone: options.timezone,
|
|
739
|
+
filters: {
|
|
740
|
+
program: options.program ?? null,
|
|
741
|
+
workout: options.workout ?? null
|
|
742
|
+
},
|
|
743
|
+
events: events.map((event) => formatWorkoutEvent(event, programDetails))
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function parseWorkoutDayPreview(html, dayIndex) {
|
|
747
|
+
const dayBlocks = extractWorkoutDayBlocks(html);
|
|
748
|
+
const selected = selectWorkoutDayBlock(dayBlocks, dayIndex);
|
|
749
|
+
if (dayBlocks.size > 1 && !selected) return null;
|
|
750
|
+
const dayHtml = selected?.html ?? html;
|
|
751
|
+
if (!dayHtml) return null;
|
|
752
|
+
const totalVolumeSets = parseTotalVolumeSets(dayHtml);
|
|
753
|
+
const sections = [];
|
|
754
|
+
const warmupHtml = extractSectionHtml(dayHtml, "table_warmup");
|
|
755
|
+
if (warmupHtml) {
|
|
756
|
+
const groups = buildExerciseGroups(warmupHtml);
|
|
757
|
+
if (groups.length > 0) sections.push({
|
|
758
|
+
type: "warm_up",
|
|
759
|
+
label: "Warm Up",
|
|
760
|
+
groups
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
const workoutHtml = extractSectionHtml(dayHtml, "table_workout");
|
|
764
|
+
if (workoutHtml) {
|
|
765
|
+
const groups = buildExerciseGroups(workoutHtml);
|
|
766
|
+
if (groups.length > 0) sections.push({
|
|
767
|
+
type: "workout",
|
|
768
|
+
label: "Workout",
|
|
769
|
+
groups
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
if (totalVolumeSets.length === 0 && sections.length === 0) return null;
|
|
773
|
+
const resolvedIndex = selected?.index ?? dayIndex;
|
|
774
|
+
return {
|
|
775
|
+
day_index: resolvedIndex,
|
|
776
|
+
day_label: resolvedIndex === void 0 ? void 0 : `Day ${resolvedIndex + 1}`,
|
|
777
|
+
total_volume_sets: totalVolumeSets,
|
|
778
|
+
sections
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function formatWorkoutEvent(event, programDetails) {
|
|
782
|
+
const record = event;
|
|
783
|
+
const programUuid = typeof record.program === "string" ? record.program : void 0;
|
|
784
|
+
const program = programUuid ? programDetails[programUuid] : void 0;
|
|
785
|
+
const programSummary = programUuid ? extractProgramSummary(programUuid, program) : null;
|
|
786
|
+
const dayIndex = resolveEventDayIndex(record, program);
|
|
787
|
+
const previewHtml = (findWorkoutPreviewHtmlMatch(record) ?? (program ? findWorkoutPreviewHtmlMatch(program) : void 0))?.html;
|
|
788
|
+
let workoutDay = previewHtml ? parseWorkoutDayPreview(previewHtml, dayIndex) : null;
|
|
789
|
+
if (!workoutDay) workoutDay = deriveWorkoutDayFromProgram(program ?? record, record);
|
|
790
|
+
return {
|
|
791
|
+
event: {
|
|
792
|
+
id: typeof record.id === "number" || typeof record.id === "string" ? record.id : void 0,
|
|
793
|
+
start: typeof record.start === "string" ? record.start : void 0,
|
|
794
|
+
end: typeof record.end === "string" ? record.end : void 0,
|
|
795
|
+
title: typeof record.title === "string" ? record.title : void 0
|
|
796
|
+
},
|
|
797
|
+
program: programSummary,
|
|
798
|
+
workout_day: workoutDay
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function deriveWorkoutDayFromProgram(program, event) {
|
|
802
|
+
if (!program || typeof program !== "object") return null;
|
|
803
|
+
const candidates = extractProgramDayCandidates(program);
|
|
804
|
+
if (candidates.length === 0) return null;
|
|
805
|
+
const dayIndex = resolveEventDayIndex(event, program) ?? parseDayIndexFromTitle(event.title);
|
|
806
|
+
if (dayIndex !== void 0) {
|
|
807
|
+
const match = candidates.find((candidate) => candidate.day_index === dayIndex) ?? candidates.find((candidate) => candidate.day_index === dayIndex - 1);
|
|
808
|
+
if (match) return match;
|
|
809
|
+
}
|
|
810
|
+
const title = typeof event.title === "string" ? event.title.trim() : "";
|
|
811
|
+
if (title) {
|
|
812
|
+
const normalizedTitle = normalizeLabel(title);
|
|
813
|
+
const match = candidates.find((candidate) => {
|
|
814
|
+
if (!candidate.day_label) return false;
|
|
815
|
+
const normalizedLabel = normalizeLabel(candidate.day_label);
|
|
816
|
+
return normalizedLabel.includes(normalizedTitle) || normalizedTitle.includes(normalizedLabel);
|
|
817
|
+
});
|
|
818
|
+
if (match) return match;
|
|
819
|
+
}
|
|
820
|
+
if (candidates.length === 1) return candidates[0];
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
function extractProgramDayCandidates(program) {
|
|
824
|
+
const candidates = [];
|
|
825
|
+
const queue = [program];
|
|
826
|
+
while (queue.length > 0) {
|
|
827
|
+
const value = queue.shift();
|
|
828
|
+
if (!value) continue;
|
|
829
|
+
if (Array.isArray(value)) {
|
|
830
|
+
queue.push(...value);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (typeof value !== "object") continue;
|
|
834
|
+
const record = value;
|
|
835
|
+
const sections = extractSectionsFromRecord(record);
|
|
836
|
+
if (sections.length > 0) {
|
|
837
|
+
const dayIndex = resolveDayIndexFromRecord(record);
|
|
838
|
+
const dayLabel = resolveDayLabelFromRecord(record, dayIndex);
|
|
839
|
+
const totalVolume = extractTotalVolumeFromRecord(record);
|
|
840
|
+
candidates.push({
|
|
841
|
+
day_index: dayIndex,
|
|
842
|
+
day_label: dayLabel,
|
|
843
|
+
total_volume_sets: totalVolume,
|
|
844
|
+
sections
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
queue.push(...Object.values(record));
|
|
848
|
+
}
|
|
849
|
+
return candidates;
|
|
850
|
+
}
|
|
851
|
+
function extractSectionsFromRecord(record) {
|
|
852
|
+
const sections = [];
|
|
853
|
+
const exerciseListSections = extractSectionsFromExerciseList(record.exercise_list);
|
|
854
|
+
if (exerciseListSections.length > 0) return exerciseListSections;
|
|
855
|
+
const warmup = extractExercisesFromValue(record.warmup ?? record.warm_up ?? record.warm_up_exercises ?? record.warmups);
|
|
856
|
+
if (warmup.length > 0) sections.push({
|
|
857
|
+
type: "warm_up",
|
|
858
|
+
label: "Warm Up",
|
|
859
|
+
groups: wrapExercises(warmup)
|
|
860
|
+
});
|
|
861
|
+
const workout = extractExercisesFromValue(record.workout ?? record.workout_exercises ?? record.exercises ?? record.exercise);
|
|
862
|
+
if (workout.length > 0) sections.push({
|
|
863
|
+
type: "workout",
|
|
864
|
+
label: "Workout",
|
|
865
|
+
groups: wrapExercises(workout)
|
|
866
|
+
});
|
|
867
|
+
return sections;
|
|
868
|
+
}
|
|
869
|
+
function extractSectionsFromExerciseList(value) {
|
|
870
|
+
if (!value || typeof value !== "object") return [];
|
|
871
|
+
const record = value;
|
|
872
|
+
const sections = [];
|
|
873
|
+
const warmupGroups = extractExerciseGroupsFromList(record.warmup ?? record.warm_up);
|
|
874
|
+
if (warmupGroups.length > 0) sections.push({
|
|
875
|
+
type: "warm_up",
|
|
876
|
+
label: "Warm Up",
|
|
877
|
+
groups: warmupGroups
|
|
878
|
+
});
|
|
879
|
+
const workoutGroups = extractExerciseGroupsFromList(record.workout ?? record.main);
|
|
880
|
+
if (workoutGroups.length > 0) sections.push({
|
|
881
|
+
type: "workout",
|
|
882
|
+
label: "Workout",
|
|
883
|
+
groups: workoutGroups
|
|
884
|
+
});
|
|
885
|
+
const cooldownGroups = extractExerciseGroupsFromList(record.cooldown ?? record.cool_down);
|
|
886
|
+
if (cooldownGroups.length > 0) sections.push({
|
|
887
|
+
type: "workout",
|
|
888
|
+
label: "Cooldown",
|
|
889
|
+
groups: cooldownGroups
|
|
890
|
+
});
|
|
891
|
+
return sections;
|
|
892
|
+
}
|
|
893
|
+
function extractExerciseGroupsFromList(value) {
|
|
894
|
+
if (!Array.isArray(value)) return [];
|
|
895
|
+
const groups = [];
|
|
896
|
+
for (const entry of value) {
|
|
897
|
+
if (!entry || typeof entry !== "object") continue;
|
|
898
|
+
const record = entry;
|
|
899
|
+
const exercises = extractExercisesFromValue(record.list ?? record.exercises ?? record.exercise ?? record.items);
|
|
900
|
+
if (exercises.length === 0) continue;
|
|
901
|
+
const groupType = (typeof record.type === "string" ? record.type.toLowerCase() : "").includes("superset") ? "superset" : "straight";
|
|
902
|
+
groups.push({
|
|
903
|
+
type: groupType,
|
|
904
|
+
exercises
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
return groups;
|
|
908
|
+
}
|
|
909
|
+
function extractExercisesFromValue(value) {
|
|
910
|
+
if (!value) return [];
|
|
911
|
+
if (Array.isArray(value)) return extractExercisesFromArray(value);
|
|
912
|
+
if (typeof value === "object") {
|
|
913
|
+
const record = value;
|
|
914
|
+
if (looksLikeExerciseRecord(record)) {
|
|
915
|
+
const parsed = parseExerciseFromRecord(record);
|
|
916
|
+
return parsed ? [parsed] : [];
|
|
917
|
+
}
|
|
918
|
+
const exercises = [];
|
|
919
|
+
for (const entry of Object.values(record)) exercises.push(...extractExercisesFromValue(entry));
|
|
920
|
+
return exercises;
|
|
921
|
+
}
|
|
922
|
+
return [];
|
|
923
|
+
}
|
|
924
|
+
function extractExercisesFromArray(entries) {
|
|
925
|
+
const exerciseEntries = entries.filter((entry) => entry && typeof entry === "object").map((entry) => entry);
|
|
926
|
+
if (!exerciseEntries.some((entry) => looksLikeExerciseRecord(entry))) {
|
|
927
|
+
const nested = [];
|
|
928
|
+
for (const entry of exerciseEntries) nested.push(...extractExercisesFromValue(entry));
|
|
929
|
+
return nested;
|
|
930
|
+
}
|
|
931
|
+
return exerciseEntries.map((entry) => parseExerciseFromRecord(entry)).filter((entry) => Boolean(entry));
|
|
932
|
+
}
|
|
933
|
+
function looksLikeExerciseRecord(record) {
|
|
934
|
+
if (typeof record.exercise_name === "string") return true;
|
|
935
|
+
if (typeof record.exercise_uuid === "string") return true;
|
|
936
|
+
if (typeof record.name === "string" && (record.sets !== void 0 || record.reps !== void 0)) return true;
|
|
937
|
+
const nested = record.exercise;
|
|
938
|
+
if (nested && typeof nested === "object") {
|
|
939
|
+
const nestedRecord = nested;
|
|
940
|
+
if (typeof nestedRecord.name === "string" || typeof nestedRecord.exercise_name === "string") return true;
|
|
941
|
+
if (typeof nestedRecord.uuid === "string" || typeof nestedRecord.exercise_uuid === "string") return true;
|
|
942
|
+
}
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
function parseExerciseFromRecord(record) {
|
|
946
|
+
const nested = record.exercise && typeof record.exercise === "object" ? record.exercise : void 0;
|
|
947
|
+
const name = typeof record.exercise_name === "string" ? record.exercise_name : typeof record.name === "string" ? record.name : typeof nested?.name === "string" ? nested.name : typeof nested?.exercise_name === "string" ? nested.exercise_name : typeof nested?.title === "string" ? nested.title : void 0;
|
|
948
|
+
if (!name) return null;
|
|
949
|
+
const sets = parseNumber(record.sets);
|
|
950
|
+
const reps = typeof record.reps === "string" ? record.reps : typeof record.reps === "number" ? String(record.reps) : typeof record.rep === "string" ? record.rep : typeof record.repetition === "string" ? record.repetition : void 0;
|
|
951
|
+
const rest = parseNumber(record.rest_period ?? record.rest ?? record.rest_seconds);
|
|
952
|
+
const time = parseNumber(record.time_period ?? record.time ?? record.duration);
|
|
953
|
+
const bodyParts = extractBodyPartsFromValue(record.bodypart ?? record.body_parts ?? nested?.bodypart ?? nested?.body_parts);
|
|
954
|
+
const sequence = typeof record.number === "string" || typeof record.number === "number" ? String(record.number) : typeof record.sequence === "string" || typeof record.sequence === "number" ? String(record.sequence) : typeof record.order === "string" || typeof record.order === "number" ? String(record.order) : typeof record.exercise_order === "string" || typeof record.exercise_order === "number" ? String(record.exercise_order) : typeof record.group_order === "string" || typeof record.group_order === "number" ? String(record.group_order) : void 0;
|
|
955
|
+
return {
|
|
956
|
+
name,
|
|
957
|
+
uuid: typeof record.exercise_uuid === "string" ? record.exercise_uuid : typeof nested?.uuid === "string" ? nested.uuid : typeof nested?.exercise_uuid === "string" ? nested.exercise_uuid : void 0,
|
|
958
|
+
sets: sets ?? parseNumber(nested?.sets),
|
|
959
|
+
reps,
|
|
960
|
+
rest_seconds: rest ?? parseNumber(nested?.rest_period),
|
|
961
|
+
time_seconds: time ?? parseNumber(nested?.time_period),
|
|
962
|
+
notes: typeof record.notes === "string" ? record.notes : void 0,
|
|
963
|
+
sequence,
|
|
964
|
+
body_parts: bodyParts.length > 0 ? bodyParts : void 0
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function extractBodyPartsFromValue(value) {
|
|
968
|
+
if (!value) return [];
|
|
969
|
+
if (!Array.isArray(value)) return [];
|
|
970
|
+
return value.filter((entry) => entry && typeof entry === "object").map((entry) => entry).map((entry) => {
|
|
971
|
+
const name = typeof entry.body_part_name === "string" ? entry.body_part_name : void 0;
|
|
972
|
+
if (!name) return;
|
|
973
|
+
const rawVolume = typeof entry.body_volume === "number" ? entry.body_volume : typeof entry.body_volume === "string" ? Number.parseFloat(entry.body_volume) : void 0;
|
|
974
|
+
const volume = Number.isFinite(rawVolume ?? NaN) ? rawVolume : void 0;
|
|
975
|
+
const result = { name };
|
|
976
|
+
if (volume !== void 0) result.volume = volume;
|
|
977
|
+
return result;
|
|
978
|
+
}).filter((entry) => Boolean(entry && entry.name));
|
|
979
|
+
}
|
|
980
|
+
function extractTotalVolumeFromRecord(record) {
|
|
981
|
+
const bodypart = record.bodypart ?? record.body_parts;
|
|
982
|
+
if (!Array.isArray(bodypart)) return [];
|
|
983
|
+
const totals = [];
|
|
984
|
+
for (const entry of bodypart) {
|
|
985
|
+
if (!entry || typeof entry !== "object") continue;
|
|
986
|
+
const data = entry;
|
|
987
|
+
if (typeof data.body_part_name !== "string") continue;
|
|
988
|
+
const setsValue = typeof data.body_volume === "number" ? data.body_volume : typeof data.body_volume === "string" ? Number.parseFloat(data.body_volume) : void 0;
|
|
989
|
+
if (setsValue === void 0 || !Number.isFinite(setsValue)) continue;
|
|
990
|
+
totals.push({
|
|
991
|
+
body_part: data.body_part_name,
|
|
992
|
+
sets: setsValue
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
return totals;
|
|
996
|
+
}
|
|
997
|
+
function wrapExercises(exercises) {
|
|
998
|
+
return [...exercises].sort((a, b) => {
|
|
999
|
+
const aSeq = parseSequence(a.sequence);
|
|
1000
|
+
const bSeq = parseSequence(b.sequence);
|
|
1001
|
+
if (aSeq !== void 0 && bSeq !== void 0) return aSeq - bSeq;
|
|
1002
|
+
if (aSeq !== void 0) return -1;
|
|
1003
|
+
if (bSeq !== void 0) return 1;
|
|
1004
|
+
return 0;
|
|
1005
|
+
}).map((exercise) => ({
|
|
1006
|
+
type: "straight",
|
|
1007
|
+
exercises: [exercise]
|
|
1008
|
+
}));
|
|
1009
|
+
}
|
|
1010
|
+
function parseSequence(value) {
|
|
1011
|
+
if (!value) return;
|
|
1012
|
+
const parsed = Number.parseFloat(value);
|
|
1013
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1014
|
+
}
|
|
1015
|
+
function resolveDayIndexFromRecord(record) {
|
|
1016
|
+
for (const key of DAY_INDEX_KEYS) {
|
|
1017
|
+
const parsed = parseIndex(record[key]);
|
|
1018
|
+
if (parsed !== void 0) return parsed;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
function resolveDayLabelFromRecord(record, dayIndex) {
|
|
1022
|
+
const label = typeof record.day_title === "string" ? record.day_title : typeof record.day_name === "string" ? record.day_name : typeof record.title === "string" ? record.title : typeof record.name === "string" ? record.name : void 0;
|
|
1023
|
+
if (label) return label.trim();
|
|
1024
|
+
if (dayIndex !== void 0) return `Day ${dayIndex + 1}`;
|
|
1025
|
+
}
|
|
1026
|
+
function parseDayIndexFromTitle(value) {
|
|
1027
|
+
if (typeof value !== "string") return;
|
|
1028
|
+
const match = value.match(/\\bday\\s*(\\d+)/i);
|
|
1029
|
+
if (!match) return;
|
|
1030
|
+
const parsed = Number.parseInt(match[1], 10);
|
|
1031
|
+
if (!Number.isFinite(parsed)) return;
|
|
1032
|
+
return parsed - 1;
|
|
1033
|
+
}
|
|
1034
|
+
function normalizeLabel(value) {
|
|
1035
|
+
return value.trim().toLowerCase();
|
|
1036
|
+
}
|
|
1037
|
+
function resolveWorkoutEventDayIndex(event, program) {
|
|
1038
|
+
return resolveEventDayIndex(event, program);
|
|
1039
|
+
}
|
|
1040
|
+
function findWorkoutPreviewHtmlMatch(value) {
|
|
1041
|
+
const queue = [{
|
|
1042
|
+
value,
|
|
1043
|
+
path: "$"
|
|
1044
|
+
}];
|
|
1045
|
+
while (queue.length > 0) {
|
|
1046
|
+
const current = queue.shift();
|
|
1047
|
+
if (!current) continue;
|
|
1048
|
+
const { value: entry, path } = current;
|
|
1049
|
+
if (typeof entry === "string") {
|
|
1050
|
+
if (entry.includes("workoutdays_data") || entry.includes("preview_day_content") || entry.includes("table_workout")) return {
|
|
1051
|
+
html: entry,
|
|
1052
|
+
source: path
|
|
1053
|
+
};
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
if (Array.isArray(entry)) {
|
|
1057
|
+
entry.forEach((item, index) => {
|
|
1058
|
+
queue.push({
|
|
1059
|
+
value: item,
|
|
1060
|
+
path: `${path}[${index}]`
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
if (entry && typeof entry === "object") for (const [key, child] of Object.entries(entry)) queue.push({
|
|
1066
|
+
value: child,
|
|
1067
|
+
path: `${path}.${formatPathKey(key)}`
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function resolveEventDayIndex(event, program) {
|
|
1072
|
+
for (const key of DAY_INDEX_KEYS) {
|
|
1073
|
+
const value = event[key];
|
|
1074
|
+
const parsed = parseIndex(value);
|
|
1075
|
+
if (parsed !== void 0) return parsed;
|
|
1076
|
+
}
|
|
1077
|
+
const workoutUuid = typeof event.workout === "string" ? event.workout : typeof event.workout_uuid === "string" ? event.workout_uuid : void 0;
|
|
1078
|
+
if (workoutUuid) {
|
|
1079
|
+
const found = findDayIndexByWorkoutUuid(program, workoutUuid);
|
|
1080
|
+
if (found !== void 0) return found;
|
|
1081
|
+
}
|
|
1082
|
+
const workoutDayUuid = typeof event.workout_day_uuid === "string" ? event.workout_day_uuid : void 0;
|
|
1083
|
+
if (workoutDayUuid) {
|
|
1084
|
+
const found = findDayIndexByUuid(program, workoutDayUuid);
|
|
1085
|
+
if (found !== void 0) return found;
|
|
1086
|
+
}
|
|
1087
|
+
const workoutDayId = parseIndex(event.workout_day_id ?? event.day_id);
|
|
1088
|
+
if (workoutDayId !== void 0) {
|
|
1089
|
+
const found = findDayIndexById(program, workoutDayId);
|
|
1090
|
+
if (found !== void 0) return found;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function extractProgramSummary(uuid, program) {
|
|
1094
|
+
if (!program || typeof program !== "object") return { uuid };
|
|
1095
|
+
const record = program;
|
|
1096
|
+
return {
|
|
1097
|
+
uuid,
|
|
1098
|
+
title: typeof record.title === "string" ? record.title : typeof record.name === "string" ? record.name : void 0
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
function extractWorkoutDayBlocks(html) {
|
|
1102
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
1103
|
+
const regex = /<div[^>]*\bid=(["'])day_content_(\d+)\1[^>]*>/gi;
|
|
1104
|
+
let match;
|
|
1105
|
+
while ((match = regex.exec(html)) !== null) {
|
|
1106
|
+
const index = Number.parseInt(match[2], 10);
|
|
1107
|
+
if (Number.isNaN(index)) continue;
|
|
1108
|
+
const start = match.index;
|
|
1109
|
+
const block = extractDivBlock(html, start);
|
|
1110
|
+
if (block) blocks.set(index, block);
|
|
1111
|
+
}
|
|
1112
|
+
return blocks;
|
|
1113
|
+
}
|
|
1114
|
+
function selectWorkoutDayBlock(blocks, dayIndex) {
|
|
1115
|
+
if (dayIndex !== void 0) {
|
|
1116
|
+
const direct = blocks.get(dayIndex);
|
|
1117
|
+
if (direct) return {
|
|
1118
|
+
index: dayIndex,
|
|
1119
|
+
html: direct
|
|
1120
|
+
};
|
|
1121
|
+
const adjusted = blocks.get(dayIndex - 1);
|
|
1122
|
+
if (adjusted && dayIndex > 0) return {
|
|
1123
|
+
index: dayIndex - 1,
|
|
1124
|
+
html: adjusted
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
if (blocks.size === 1) {
|
|
1128
|
+
const [index, html] = Array.from(blocks.entries())[0];
|
|
1129
|
+
return {
|
|
1130
|
+
index,
|
|
1131
|
+
html
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
for (const [index, html] of blocks.entries()) if (/display\s*:\s*block/i.test(html)) return {
|
|
1135
|
+
index,
|
|
1136
|
+
html
|
|
1137
|
+
};
|
|
1138
|
+
if (blocks.size > 0) {
|
|
1139
|
+
const [index, html] = Array.from(blocks.entries())[0];
|
|
1140
|
+
return {
|
|
1141
|
+
index,
|
|
1142
|
+
html
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
function extractDivBlock(html, startIndex) {
|
|
1147
|
+
const tagRegex = /<div\b[^>]*>|<\/div\s*>/gi;
|
|
1148
|
+
tagRegex.lastIndex = startIndex;
|
|
1149
|
+
let depth = 0;
|
|
1150
|
+
let match;
|
|
1151
|
+
while ((match = tagRegex.exec(html)) !== null) if (match[0].startsWith("</")) {
|
|
1152
|
+
depth -= 1;
|
|
1153
|
+
if (depth === 0) return html.slice(startIndex, tagRegex.lastIndex);
|
|
1154
|
+
} else depth += 1;
|
|
1155
|
+
return html.slice(startIndex);
|
|
1156
|
+
}
|
|
1157
|
+
function extractSectionHtml(html, className) {
|
|
1158
|
+
const marker = html.indexOf(className);
|
|
1159
|
+
if (marker === -1) return;
|
|
1160
|
+
const divStart = html.lastIndexOf("<div", marker);
|
|
1161
|
+
if (divStart === -1) return;
|
|
1162
|
+
return extractDivBlock(html, divStart);
|
|
1163
|
+
}
|
|
1164
|
+
function parseTotalVolumeSets(html) {
|
|
1165
|
+
const results = [];
|
|
1166
|
+
const regex = /<span[^>]*total-volume-span[^>]*>(.*?)<\/span>/gi;
|
|
1167
|
+
let match;
|
|
1168
|
+
while ((match = regex.exec(html)) !== null) {
|
|
1169
|
+
const parsed = parseBodyPartVolume(decodeHtmlEntities(match[1].trim()));
|
|
1170
|
+
if (parsed) results.push(parsed);
|
|
1171
|
+
}
|
|
1172
|
+
return results;
|
|
1173
|
+
}
|
|
1174
|
+
function parseBodyPartVolume(value) {
|
|
1175
|
+
const match = value.match(/^(.*?)(?:\s+)(-?\d+(?:\.\d+)?)$/);
|
|
1176
|
+
if (!match) return;
|
|
1177
|
+
const sets = Number.parseFloat(match[2]);
|
|
1178
|
+
if (!Number.isFinite(sets)) return;
|
|
1179
|
+
return {
|
|
1180
|
+
body_part: match[1].trim(),
|
|
1181
|
+
sets
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
function buildExerciseGroups(sectionHtml) {
|
|
1185
|
+
const supersetTables = extractSupersetTables(sectionHtml);
|
|
1186
|
+
const supersetRanges = supersetTables.map((table) => ({
|
|
1187
|
+
start: table.start,
|
|
1188
|
+
end: table.end
|
|
1189
|
+
}));
|
|
1190
|
+
const supersetGroups = [];
|
|
1191
|
+
for (const table of supersetTables) {
|
|
1192
|
+
const exercises = parseExercisesWithIndex(table.html).map((row) => row.exercise);
|
|
1193
|
+
if (exercises.length === 0) continue;
|
|
1194
|
+
supersetGroups.push({
|
|
1195
|
+
index: table.start,
|
|
1196
|
+
group: {
|
|
1197
|
+
type: "superset",
|
|
1198
|
+
label: "Superset",
|
|
1199
|
+
exercises
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
const straightGroups = [];
|
|
1204
|
+
for (const row of parseExercisesWithIndex(sectionHtml)) {
|
|
1205
|
+
if (isIndexInRanges(row.index, supersetRanges)) continue;
|
|
1206
|
+
if (row.classValue.includes("subrow")) continue;
|
|
1207
|
+
straightGroups.push({
|
|
1208
|
+
index: row.index,
|
|
1209
|
+
group: {
|
|
1210
|
+
type: "straight",
|
|
1211
|
+
exercises: [row.exercise]
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
return [...supersetGroups, ...straightGroups].sort((a, b) => a.index - b.index).map((entry) => entry.group);
|
|
1216
|
+
}
|
|
1217
|
+
function parseExercisesWithIndex(html) {
|
|
1218
|
+
const rows = [];
|
|
1219
|
+
const regex = /<tr\b[^>]*\bdata-exercise_name=(["']).*?\1[^>]*>/gi;
|
|
1220
|
+
let match;
|
|
1221
|
+
while ((match = regex.exec(html)) !== null) {
|
|
1222
|
+
const tag = match[0];
|
|
1223
|
+
const exercise = parseExerciseFromTag(tag);
|
|
1224
|
+
if (!exercise) continue;
|
|
1225
|
+
rows.push({
|
|
1226
|
+
index: match.index,
|
|
1227
|
+
exercise,
|
|
1228
|
+
classValue: extractAttribute(tag, "class") ?? ""
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
return rows;
|
|
1232
|
+
}
|
|
1233
|
+
function parseExerciseFromTag(tag) {
|
|
1234
|
+
const attrs = extractDataAttributes(tag);
|
|
1235
|
+
const name = attrs["exercise_name"];
|
|
1236
|
+
if (!name) return null;
|
|
1237
|
+
const sets = parseNumber(attrs["sets"]);
|
|
1238
|
+
const restSeconds = parseNumber(attrs["rest_period"]);
|
|
1239
|
+
const timeSeconds = parseNumber(attrs["time_period"]);
|
|
1240
|
+
const bodyParts = parseBodyParts(attrs["bodypart"]);
|
|
1241
|
+
const media = parseMedia(attrs["media"]);
|
|
1242
|
+
return {
|
|
1243
|
+
name,
|
|
1244
|
+
uuid: attrs["exercise_uuid"],
|
|
1245
|
+
sets,
|
|
1246
|
+
reps: attrs["reps"] || void 0,
|
|
1247
|
+
rest_seconds: restSeconds,
|
|
1248
|
+
time_seconds: timeSeconds,
|
|
1249
|
+
notes: attrs["notes"] || void 0,
|
|
1250
|
+
sequence: attrs["number"] || void 0,
|
|
1251
|
+
body_parts: bodyParts.length > 0 ? bodyParts : void 0,
|
|
1252
|
+
media: media.length > 0 ? media : void 0
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function extractSupersetTables(html) {
|
|
1256
|
+
const candidates = extractTableBlocks(html).filter((table) => /Superset/i.test(table.html));
|
|
1257
|
+
return candidates.filter((candidate) => !containsSmallerSuperset(candidate, candidates));
|
|
1258
|
+
}
|
|
1259
|
+
function containsSmallerSuperset(candidate, candidates) {
|
|
1260
|
+
const candidateSize = candidate.end - candidate.start;
|
|
1261
|
+
return candidates.some((other) => {
|
|
1262
|
+
if (other === candidate) return false;
|
|
1263
|
+
const otherSize = other.end - other.start;
|
|
1264
|
+
return other.start >= candidate.start && other.end <= candidate.end && otherSize < candidateSize;
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
function extractTableBlocks(html) {
|
|
1268
|
+
const blocks = [];
|
|
1269
|
+
const regex = /<table\b[^>]*>/gi;
|
|
1270
|
+
let match;
|
|
1271
|
+
while ((match = regex.exec(html)) !== null) {
|
|
1272
|
+
const start = match.index;
|
|
1273
|
+
const block = extractTableBlock(html, start);
|
|
1274
|
+
if (!block) continue;
|
|
1275
|
+
blocks.push({
|
|
1276
|
+
start,
|
|
1277
|
+
end: block.end,
|
|
1278
|
+
html: block.html
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
return blocks;
|
|
1282
|
+
}
|
|
1283
|
+
function extractTableBlock(html, startIndex) {
|
|
1284
|
+
const tagRegex = /<table\b[^>]*>|<\/table\s*>/gi;
|
|
1285
|
+
tagRegex.lastIndex = startIndex;
|
|
1286
|
+
let depth = 0;
|
|
1287
|
+
let match;
|
|
1288
|
+
while ((match = tagRegex.exec(html)) !== null) if (match[0].startsWith("</")) {
|
|
1289
|
+
depth -= 1;
|
|
1290
|
+
if (depth === 0) return {
|
|
1291
|
+
html: html.slice(startIndex, tagRegex.lastIndex),
|
|
1292
|
+
end: tagRegex.lastIndex
|
|
1293
|
+
};
|
|
1294
|
+
} else depth += 1;
|
|
1295
|
+
return {
|
|
1296
|
+
html: html.slice(startIndex),
|
|
1297
|
+
end: html.length
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
function parseBodyParts(value) {
|
|
1301
|
+
if (!value) return [];
|
|
1302
|
+
const parsed = parseJson(decodeHtmlEntities(value));
|
|
1303
|
+
if (!Array.isArray(parsed)) return [];
|
|
1304
|
+
const results = [];
|
|
1305
|
+
for (const entry of parsed) {
|
|
1306
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1307
|
+
const record = entry;
|
|
1308
|
+
const name = typeof record.body_part_name === "string" ? record.body_part_name : void 0;
|
|
1309
|
+
if (!name) continue;
|
|
1310
|
+
const volumeValue = typeof record.body_volume === "number" ? record.body_volume : typeof record.body_volume === "string" ? Number.parseFloat(record.body_volume) : void 0;
|
|
1311
|
+
results.push({
|
|
1312
|
+
name,
|
|
1313
|
+
volume: Number.isFinite(volumeValue) ? volumeValue : void 0
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
return results;
|
|
1317
|
+
}
|
|
1318
|
+
function parseMedia(value) {
|
|
1319
|
+
if (!value) return [];
|
|
1320
|
+
const parsed = parseJson(decodeHtmlEntities(value));
|
|
1321
|
+
if (!Array.isArray(parsed)) return [];
|
|
1322
|
+
const results = [];
|
|
1323
|
+
for (const entry of parsed) {
|
|
1324
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1325
|
+
const record = entry;
|
|
1326
|
+
const fileUrl = typeof record.file_url === "string" ? record.file_url : void 0;
|
|
1327
|
+
const fileType = typeof record.file_type === "number" ? record.file_type : void 0;
|
|
1328
|
+
const mediaEntry = { file_type: fileType };
|
|
1329
|
+
if (fileType === 2) mediaEntry.thumbnail_url = fileUrl;
|
|
1330
|
+
else mediaEntry.file_url = fileUrl;
|
|
1331
|
+
if (mediaEntry.file_url || mediaEntry.thumbnail_url) results.push(mediaEntry);
|
|
1332
|
+
}
|
|
1333
|
+
return results;
|
|
1334
|
+
}
|
|
1335
|
+
function extractDataAttributes(tag) {
|
|
1336
|
+
const attrs = {};
|
|
1337
|
+
const regex = /\sdata-([a-z0-9_-]+)=(["'])(.*?)\2/gi;
|
|
1338
|
+
let match;
|
|
1339
|
+
while ((match = regex.exec(tag)) !== null) attrs[match[1]] = decodeHtmlEntities(match[3]);
|
|
1340
|
+
return attrs;
|
|
1341
|
+
}
|
|
1342
|
+
function extractAttribute(tag, name) {
|
|
1343
|
+
const regex = new RegExp(`\\s${name}=(["'])(.*?)\\1`, "i");
|
|
1344
|
+
const match = tag.match(regex);
|
|
1345
|
+
return match ? match[2] : void 0;
|
|
1346
|
+
}
|
|
1347
|
+
function decodeHtmlEntities(value) {
|
|
1348
|
+
let result = value;
|
|
1349
|
+
for (const [entity, replacement] of Object.entries(HTML_ENTITY_MAP)) result = result.split(entity).join(replacement);
|
|
1350
|
+
return result;
|
|
1351
|
+
}
|
|
1352
|
+
function isIndexInRanges(index, ranges) {
|
|
1353
|
+
return ranges.some((range) => index >= range.start && index < range.end);
|
|
1354
|
+
}
|
|
1355
|
+
function parseNumber(value) {
|
|
1356
|
+
if (!value) return;
|
|
1357
|
+
const parsed = Number.parseFloat(value);
|
|
1358
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1359
|
+
}
|
|
1360
|
+
function parseIndex(value) {
|
|
1361
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1362
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
1363
|
+
const parsed = Number.parseInt(value, 10);
|
|
1364
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function parseJson(value) {
|
|
1368
|
+
try {
|
|
1369
|
+
return JSON.parse(value);
|
|
1370
|
+
} catch {
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
function findDayIndexByWorkoutUuid(program, workoutUuid) {
|
|
1375
|
+
const queue = [{ value: program }];
|
|
1376
|
+
while (queue.length > 0) {
|
|
1377
|
+
const { value, indexInArray } = queue.shift() ?? {};
|
|
1378
|
+
if (value === void 0 || value === null) continue;
|
|
1379
|
+
if (Array.isArray(value)) {
|
|
1380
|
+
value.forEach((entry, index) => {
|
|
1381
|
+
queue.push({
|
|
1382
|
+
value: entry,
|
|
1383
|
+
indexInArray: index
|
|
1384
|
+
});
|
|
1385
|
+
});
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
if (typeof value === "object") {
|
|
1389
|
+
const record = value;
|
|
1390
|
+
const candidateUuid = typeof record.workout_uuid === "string" ? record.workout_uuid : typeof record.workout === "string" ? record.workout : typeof record.uuid === "string" ? record.uuid : void 0;
|
|
1391
|
+
if (candidateUuid && candidateUuid === workoutUuid) {
|
|
1392
|
+
for (const key of DAY_INDEX_KEYS) {
|
|
1393
|
+
const parsed = parseIndex(record[key]);
|
|
1394
|
+
if (parsed !== void 0) return parsed;
|
|
1395
|
+
}
|
|
1396
|
+
if (indexInArray !== void 0) return indexInArray;
|
|
1397
|
+
}
|
|
1398
|
+
for (const entry of Object.values(record)) queue.push({ value: entry });
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
function findDayIndexByUuid(program, uuid) {
|
|
1403
|
+
return findDayIndexByMatcher(program, (record) => record.uuid === uuid);
|
|
1404
|
+
}
|
|
1405
|
+
function findDayIndexById(program, id) {
|
|
1406
|
+
return findDayIndexByMatcher(program, (record) => record.id === id);
|
|
1407
|
+
}
|
|
1408
|
+
function findDayIndexByMatcher(program, matcher) {
|
|
1409
|
+
const queue = [{ value: program }];
|
|
1410
|
+
while (queue.length > 0) {
|
|
1411
|
+
const { value, indexInArray } = queue.shift() ?? {};
|
|
1412
|
+
if (value === void 0 || value === null) continue;
|
|
1413
|
+
if (Array.isArray(value)) {
|
|
1414
|
+
value.forEach((entry, index) => {
|
|
1415
|
+
queue.push({
|
|
1416
|
+
value: entry,
|
|
1417
|
+
indexInArray: index
|
|
1418
|
+
});
|
|
1419
|
+
});
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
if (typeof value === "object") {
|
|
1423
|
+
const record = value;
|
|
1424
|
+
if (matcher(record)) {
|
|
1425
|
+
for (const key of DAY_INDEX_KEYS) {
|
|
1426
|
+
const parsed = parseIndex(record[key]);
|
|
1427
|
+
if (parsed !== void 0) return parsed;
|
|
1428
|
+
}
|
|
1429
|
+
if (indexInArray !== void 0) return indexInArray;
|
|
1430
|
+
}
|
|
1431
|
+
for (const entry of Object.values(record)) queue.push({ value: entry });
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
function formatPathKey(key) {
|
|
1436
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) return key;
|
|
1437
|
+
return `\"${key.replace(/\"/g, "\\\"")}\"`;
|
|
1438
|
+
}
|
|
718
1439
|
|
|
719
1440
|
//#endregion
|
|
720
1441
|
//#region src/commands/workout.ts
|
|
@@ -734,8 +1455,8 @@ async function handleWorkout(positionals, options) {
|
|
|
734
1455
|
};
|
|
735
1456
|
const baseUrl = resolveBaseUrl(options, config);
|
|
736
1457
|
const rawOutput = isFlagEnabled(options, "raw");
|
|
737
|
-
const page = parseNumber(options.page, 1);
|
|
738
|
-
const rpp = parseNumber(options.rpp, 12);
|
|
1458
|
+
const page = parseNumber$1(options.page, 1);
|
|
1459
|
+
const rpp = parseNumber$1(options.rpp, 12);
|
|
739
1460
|
const listRpp = action === "latest" && options.rpp === void 0 ? 100 : rpp;
|
|
740
1461
|
const fetchList = async () => {
|
|
741
1462
|
await ensureToken();
|
|
@@ -821,12 +1542,39 @@ async function handleWorkout(positionals, options) {
|
|
|
821
1542
|
const webOrigin = new URL(baseWebUrl).origin;
|
|
822
1543
|
const timezone = options.timezone ?? process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "Europe/London";
|
|
823
1544
|
let userUuid = resolveUserUuid(options, config);
|
|
824
|
-
if (!userUuid)
|
|
1545
|
+
if (!userUuid) try {
|
|
1546
|
+
await ensureToken();
|
|
1547
|
+
let checkinsResponse = await postJson("/api/v2/checkin/list", token, baseUrl, {
|
|
1548
|
+
page: 1,
|
|
1549
|
+
rpp: 1
|
|
1550
|
+
});
|
|
1551
|
+
if (autoLogin && isTokenExpiredResponse(checkinsResponse.json)) {
|
|
1552
|
+
token = await loginAndPersist(options, config, "silent");
|
|
1553
|
+
checkinsResponse = await postJson("/api/v2/checkin/list", token, baseUrl, {
|
|
1554
|
+
page: 1,
|
|
1555
|
+
rpp: 1
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
if (checkinsResponse.ok) {
|
|
1559
|
+
const extracted = extractUserUuidFromCheckins(checkinsResponse.json);
|
|
1560
|
+
if (extracted) {
|
|
1561
|
+
userUuid = extracted;
|
|
1562
|
+
if (userUuid !== config.userUuid) writeConfig({
|
|
1563
|
+
...config,
|
|
1564
|
+
userUuid
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
} catch {}
|
|
1569
|
+
if (!userUuid) throw new Error("Missing user uuid. Use --user or run 'kahunas checkins list' once.");
|
|
825
1570
|
if (userUuid !== config.userUuid) writeConfig({
|
|
826
1571
|
...config,
|
|
827
1572
|
userUuid
|
|
828
1573
|
});
|
|
829
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);
|
|
830
1578
|
let csrfToken$1 = resolveCsrfToken(options, config);
|
|
831
1579
|
let csrfCookie = resolveCsrfCookie(options, config);
|
|
832
1580
|
let authCookie = resolveAuthCookie(options, config);
|
|
@@ -896,8 +1644,9 @@ async function handleWorkout(positionals, options) {
|
|
|
896
1644
|
return;
|
|
897
1645
|
}
|
|
898
1646
|
const sorted = sortWorkoutEvents(filterWorkoutEvents(payload, options.program, options.workout));
|
|
1647
|
+
const limited = limit > 0 ? sorted.slice(-limit) : sorted;
|
|
899
1648
|
if (minimal) {
|
|
900
|
-
console.log(JSON.stringify(
|
|
1649
|
+
console.log(JSON.stringify(limited, null, 2));
|
|
901
1650
|
return;
|
|
902
1651
|
}
|
|
903
1652
|
let programIndex;
|
|
@@ -945,8 +1694,27 @@ async function handleWorkout(positionals, options) {
|
|
|
945
1694
|
} catch {}
|
|
946
1695
|
programDetails[programId$1] = programIndex?.[programId$1] ?? null;
|
|
947
1696
|
}
|
|
948
|
-
|
|
949
|
-
|
|
1697
|
+
if (debugPreview) for (const entry of limited) {
|
|
1698
|
+
const record = entry;
|
|
1699
|
+
const eventId = typeof record.id === "string" || typeof record.id === "number" ? record.id : "unknown";
|
|
1700
|
+
const programUuid = typeof record.program === "string" ? record.program : void 0;
|
|
1701
|
+
const program = programUuid ? programDetails[programUuid] : void 0;
|
|
1702
|
+
const match = findWorkoutPreviewHtmlMatch(record) ?? (program ? findWorkoutPreviewHtmlMatch(program) : void 0);
|
|
1703
|
+
const dayIndex = resolveWorkoutEventDayIndex(entry, program);
|
|
1704
|
+
const source = match ? match.source : "not_found";
|
|
1705
|
+
console.error(`debug-preview event=${eventId} program=${programUuid ?? "unknown"} day_index=${dayIndex ?? "none"} source=${source}`);
|
|
1706
|
+
}
|
|
1707
|
+
if (full) {
|
|
1708
|
+
const enriched = enrichWorkoutEvents(limited, programDetails);
|
|
1709
|
+
console.log(JSON.stringify(enriched, null, 2));
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
const formatted = formatWorkoutEventsOutput(limited, programDetails, {
|
|
1713
|
+
timezone,
|
|
1714
|
+
program: options.program,
|
|
1715
|
+
workout: options.workout
|
|
1716
|
+
});
|
|
1717
|
+
console.log(JSON.stringify(formatted, null, 2));
|
|
950
1718
|
return;
|
|
951
1719
|
}
|
|
952
1720
|
if (action === "sync") {
|