feat: routines export with folder sections + fitdown polish#50
Merged
Conversation
Adds `liftoff-export routines list` and `routines show <name-or-id>` for exporting saved workout templates. Liftoff calls them "presets" internally under fitnessService.* — the JSON output preserves that naming so jq recipes match the upstream API, while the CLI surface uses "routines" since that's what lifters call them. Read path: fitnessService.fetchUserPresetsWithFolders (single GET, no input). Folder-organized routines emit a stderr warning and are skipped; support is a follow-up once a non-empty folder example is captured. The fitdown set-line renderer is duplicated rather than extracted from workouts.go so that file stays untouched; the WR/BR/AB/WD/DD/ND switch is small enough that one shared helper would not yet pay for itself. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
Adds a small parallel job that cross-compiles darwin-arm64, darwin-amd64, linux-amd64, linux-arm64 and uploads them as a build artifact attached to the workflow run. Reviewers (and the author on a machine without a Go toolchain) can now download the binary right off the PR's checks page rather than rebuilding locally. Goreleaser still owns tagged releases; this is for short-lived review binaries only — retention is 14 days to keep storage tidy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
Plain "Routine NAME" headers blended into the exercise list — hard to spot where one routine ended and the next began. Switching to "# Routine: NAME" (markdown H1) plus a "---" horizontal rule between routines gives both terminal readers and markdown renderers a clear break. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
…own # "# Left Only" under an exercise name renders as a markdown H1 in any viewer, which is wrong — the note is an annotation, not a heading. Switching to "Kettlebell Swing (Left Only)" keeps the note attached to the exercise without colliding with markdown semantics. (workouts.go has the same issue on SessionNotes — left as a separate follow-up rather than expanding this PR's scope.) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
Workouts had "# %s" for SessionNotes — a markdown H1 collision identical
to the one just fixed on the routines side. Switching SessionNotes to
parens on the "Workout DATE" header, and adding the missing per-exercise
note rendering ("Exercise Name (Seat 3)") so workout output matches
routine output for the same data shape.
Per-exercise notes like "Seat 3" / "Pos 5" / "Left Only" were silently
dropped on the workouts side; they now surface in both list and show.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
Refactors printFitdown / printRoutinesFitdown to take an io.Writer (thin os.Stdout wrappers stay) so the renderers are testable without stdout capture. Adds 12 cases across two new test files: - Routine H1 header + --- separator between routines, no trailing rule. - Single routine omits the separator. - Favorite star renders. - Routine ExerciseNotes render in parens, NOT as a markdown H1. - Consecutive identical sets compress to Nx notation. - pickPreset name/id/case-insensitive/collision/miss behaviors. - Workout SessionNotes render in parens on header, NOT as a markdown H1. - Workout ExerciseNotes render in parens, NOT as a markdown H1. - Fitdown set notation for WR / BR / AB / ND types. The "NOT a markdown H1" guards explicitly catch the old "# %s" pattern so a future regression would surface as a named test failure rather than as a visual quirk in someone's terminal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
Liftoff lets users group routines into folders ("Valley Creek") and
shows the ungrouped section as "My Routines" in-app. The CLI now
mirrors that layout: folder names are H1 section headings, routines
become H2 under them, with the existing "---" rule separating siblings
and section boundaries.
- Decode the previously-skipped folders array into typed []Folder, drop
the stderr "folders not rendered" warning.
- routines list --format json now emits the upstream-faithful nested
shape ({folders, presetsWithoutFolder}) so foldered presets are
surfaced instead of silently dropped.
- pickPreset searches across both unfiled and foldered presets so
`routines show "Valley Creek 1"` works regardless of where the
routine lives.
- Empty unfiled section is suppressed so an account with only foldered
routines doesn't emit a stray "# My Routines" heading.
- Tests cover folder rendering, H2-inside-folder, cross-folder
pickPreset, and the empty-section guard.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
routinessubcommand:listandshow <name-or-id>. Exports the saved workout templates Liftoff calls "presets" internally underfitnessService.*— discovered via mitmproxy capture of the iOS app's Routines screen.# Valley Creek), with their routines as H2 (## Routine: NAME) underneath. Ungrouped routines appear under# My Routines— the same label the Liftoff app uses for the implicit section.---boundaries between routines, and a parens convention for notes that replaces the old# note(which rendered as a markdown H1 in any viewer). Applied to bothroutinesandworkoutsso the family is consistent.cmdpackage — 18 cases covering renderer output, folder layout, note rendering,pickPresetlookup paths, and fitdown set notation.liftoff-exportartifact (darwin arm64/amd64, linux amd64/arm64) attached to each run so a reviewer can download and try a PR without a local Go toolchain.Behavior
routines list(markdown, default) renders the account as:Favorite routines are starred (
## Routine: Push ★). Per-exercise notes append in parens (Machine Seated Crunch (Seat 3)) — never as a markdown#heading. Consecutive identical set lines compress toNx…notation, sharing the WR/BR/AB/WD/DD/ND mapping withworkouts list.routines list --format jsonemits the upstream-faithful nested shape{folders: [...], presetsWithoutFolder: [...]}— preserves Liftoff's field names so jq recipes match the API. Folders contain full nestedPresetobjects.routines show NAME-OR-IDsearches both unfiled and foldered presets. Case-insensitive name match falls back to id match; multi-match returns a disambiguation error listing colliding ids. The single returned routine prints as an H1 (no enclosing section).Hermeticity: bad
--formatrejected before any network call (verified by pointingLIFTOFF_API_BASEat a black hole — no connection attempted).Workouts side also picks up the parens convention (
SessionNotesandExerciseNoteswere either rendered as#H1 or silently dropped before).Out of scope / follow-ups
v2-13-15while the compiled default isv2-13-10. The daily api-host-probe workflow should catch this on its next run.routines list --format jsonshifted from a flat[Preset]array (which silently dropped foldered routines) to the nested upstream shape. Pre-merge change since the flat output was incomplete by design; the only way to surface folder presets cleanly was to match upstream. Any jq recipe.[]becomes.presetsWithoutFolder[], .folders[].presets[].Test plan
go vet ./...clean;go buildclean;go test ./cmd/...18/18 passing.liftoff-export routines list— renders# My Routinessection followed by# Valley Creekfolder section against live account.liftoff-export routines list --format json | jq 'keys'— top-level{folders, presetsWithoutFolder}shape preserved.liftoff-export routines show "Valley Creek 1"— finds a routine inside a folder.liftoff-export routines show Push/routines show push/routines show <cuid>— name+id+case-insensitive paths.liftoff-export routines show "does not exist"— exits 1, stderr-only error.liftoff-export routines list --format=boguswith bogusLIFTOFF_API_BASE— rejected before network call.liftoff-export workouts list --since 1d— no regression; SessionNotes/ExerciseNotes (when present) render in parens.liftoff-export primeshows the routines block under SUBCOMMANDS with a jq recipe.liftoff-export-darwin-arm64,chmod +x, run — works without a local Go toolchain.🤖 Generated with Claude Code