Skip to content

feat: routines export with folder sections + fitdown polish#50

Merged
DTTerastar merged 8 commits into
mainfrom
feat/routines
Jun 15, 2026
Merged

feat: routines export with folder sections + fitdown polish#50
DTTerastar merged 8 commits into
mainfrom
feat/routines

Conversation

@DTTerastar

@DTTerastar DTTerastar commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • New top-level routines subcommand: list and show <name-or-id>. Exports the saved workout templates Liftoff calls "presets" internally under fitnessService.* — discovered via mitmproxy capture of the iOS app's Routines screen.
  • Folder rendering: folders surface as markdown H1 section headings (# 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.
  • Fitdown polish that grew out of review: scannable H1 + --- 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 both routines and workouts so the family is consistent.
  • First unit tests for the cmd package — 18 cases covering renderer output, folder layout, note rendering, pickPreset lookup paths, and fitdown set notation.
  • CI cross-compiles a liftoff-export artifact (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:

# My Routines

## Routine: Free Weights
...

---

## Routine: Legs
...

---

# Valley Creek

## Routine: Valley Creek 1
...

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 to Nx… notation, sharing the WR/BR/AB/WD/DD/ND mapping with workouts list.

routines list --format json emits the upstream-faithful nested shape {folders: [...], presetsWithoutFolder: [...]} — preserves Liftoff's field names so jq recipes match the API. Folders contain full nested Preset objects.

routines show NAME-OR-ID searches 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 --format rejected before any network call (verified by pointing LIFTOFF_API_BASE at a black hole — no connection attempted).

Workouts side also picks up the parens convention (SessionNotes and ExerciseNotes were either rendered as # H1 or silently dropped before).

Out of scope / follow-ups

  • API host rotation. mitmproxy capture surfaced that the live host is now v2-13-15 while the compiled default is v2-13-10. The daily api-host-probe workflow should catch this on its next run.
  • JSON shape change. routines list --format json shifted 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 build clean; go test ./cmd/... 18/18 passing.
  • liftoff-export routines list — renders # My Routines section followed by # Valley Creek folder 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=bogus with bogus LIFTOFF_API_BASE — rejected before network call.
  • liftoff-export workouts list --since 1d — no regression; SessionNotes/ExerciseNotes (when present) render in parens.
  • liftoff-export prime shows the routines block under SUBCOMMANDS with a jq recipe.
  • CI artifact: download liftoff-export-darwin-arm64, chmod +x, run — works without a local Go toolchain.

🤖 Generated with Claude Code

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>
DTTerastar and others added 7 commits June 8, 2026 18:55
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>
@DTTerastar DTTerastar changed the title feat(routines): add list and show subcommands feat: routines export with folder sections + fitdown polish Jun 15, 2026
@DTTerastar DTTerastar merged commit 45ff795 into main Jun 15, 2026
3 checks passed
@DTTerastar DTTerastar deleted the feat/routines branch June 15, 2026 04:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant