The marathon CLI is the primary entry point for every dev workflow —
build, deploy, snapshot, unlock, launch, tail logs, run doctor, probe
device, benchmark, save/replay test scenarios, multi-device orchestration.
Every ad-hoc SSH command you would have run has a corresponding verb.
Rule of thumb: if you find yourself typing
sshpass -p marathon ssh root@marathon.local …, there's already a verb for that. If there isn't, add one — the CLI is meant to grow with the project.
The CLI lives at scripts/marathon in this repo. Symlink into your
PATH so marathon works from anywhere:
mkdir -p ~/.local/bin ~/.local/share/bash-completion/completions
ln -sf "$(pwd)/scripts/marathon" ~/.local/bin/marathon
marathon completions bash > ~/.local/share/bash-completion/completions/marathon
Then set up config (one-time):
mkdir -p ~/.config/marathon-dev
cat > ~/.config/marathon-dev/config <<'EOF'
MARATHON_PIN=027602
# MARATHON_PASSWORD=marathon # default; override if you changed it
# MARATHON_IMAGE=$HOME/Developer/Marathon-Shell/packaging
EOF
Register your devices:
cat > ~/.config/marathon-dev/devices.conf <<'EOF'
# name kind ssh-host notes
l5 librem5 root@marathon.local USB-net 172.16.42.1 fallback
cm5 hackberry root@marathon.local HackberryPi CM5
EOF
Verify the environment:
marathon doctor
22 checks; anything red is auto-fixable with marathon doctor --fix.
Run marathon --help for the full menu. Grouped:
| verb | what |
|---|---|
marathon status |
shell pid + hash + backlight + governor + freq + battery in one command |
marathon deploy [--hot|--full] |
build shell (if src changed) → push binary → restart greetd → verify hash |
marathon reset |
systemctl restart greetd + wait for shell up |
marathon wake |
force bl_power=0 + brightness=200 |
marathon unlock [PIN] |
swipe up + PIN entry (batched via touchctl stdin) |
marathon snap [LABEL] |
screenshot device to $MARATHON_SCRATCH/<LABEL>.png |
marathon logs [-f] [GREP] |
tail shell log; --warnings filter |
marathon apps |
every marathon-app cgroup: uclamp / freeze / procs / state |
marathon launch APPID |
wake + unlock + tap the app icon |
| verb | what |
|---|---|
marathon tap X Y |
synthetic touch tap |
marathon swipe X1 Y1 X2 Y2 [STEPS] |
synthetic swipe |
marathon power |
synthetic KEY_POWER (test wake path) |
marathon doze enter|exit|status |
Doze cycle |
marathon freq [--sample N] |
cpufreq time-in-state histogram |
marathon irqs [SECS] |
top interrupts/second |
marathon wakeups |
wakeup sources by event count |
marathon cgroup APPID |
detailed cgroup state for one app |
marathon monitor cgroup |
live report of every uclamp/freeze transition |
marathon monitor logs [PATTERN] |
tail log with regex filter |
marathon monitor wifi|modem|freq|bl|wakeups |
poll changes |
| verb | what |
|---|---|
marathon modem info |
mmcli summary |
marathon modem at 'AT+CFUN?' |
raw AT (stops MM, restores after) |
marathon modem psm on|off|status |
Power-Saving Mode |
marathon modem edrx N|off|status |
eDRX config |
marathon modem reset |
mmcli soft reset |
marathon wifi psm on|off|status |
wifi power-save |
marathon wifi scan |
SSID + signal |
marathon gov [NAME] |
show / set cpufreq governor |
marathon scheduler [DISK [SCHED]] |
show / set block I/O scheduler |
marathon fuel-gauge |
full battery + thermal report |
marathon backlight [VAL] |
show state or set brightness |
| verb | what |
|---|---|
marathon app new NAME |
scaffold app from template |
marathon app run [--hot] DIR |
package + install + launch |
marathon app package [--sign] DIR |
build .marathon package |
marathon app validate DIR |
manifest + structure check |
marathon app install FILE |
scp + on-device install |
marathon app watch DIR |
auto-repackage + install on file change (inotify) |
marathon app registry |
list installed marathon apps |
marathon app permissions APPID |
show manifest permissions |
| verb | what |
|---|---|
marathon doctor |
22 checks across host env + device + services + hardware |
marathon doctor --fix |
attempt auto-repair on known failures |
| verb | what |
|---|---|
marathon device list |
registered devices |
marathon device current |
active target |
marathon device use NAME |
persist default |
marathon device add NAME KIND HOST [NOTES] |
append to devices.conf |
marathon device probe [NAME|all] |
reachable? arch? uptime? |
marathon --device NAME <verb> |
one-shot override |
marathon all <verb> |
run on every reachable device in parallel |
marathon compare <verb> |
run on all → paste side-by-side |
| verb | what |
|---|---|
marathon build shell |
wrap pmbootstrap (mirrors --src) |
marathon build image DEVICE |
duranium build-image.py |
marathon build apps |
package every ./apps/*/ |
marathon flash DEVICE IMAGE |
L5 uuu / CM5 dd |
marathon fmt [--fix] |
clang-format + qmllint sweep |
marathon lint [FILES] |
clang-tidy on staged .cpp |
marathon audit |
fmt + lint + qmllint end-to-end |
marathon changelog [SINCE] |
commits since tag / last 20 |
| verb | what |
|---|---|
marathon session new NAME |
create empty session |
marathon session record NAME <verb> |
append a step |
marathon session run NAME |
replay every step |
marathon session list|rm|diff |
manage |
marathon bench doze|wake|foreground-boost|battery-idle |
canned bench |
| verb | what |
|---|---|
marathon quickstart |
9-step guided walkthrough |
marathon docs [TOPIC] |
open docs/TOPIC.md in $PAGER |
marathon completions bash|zsh|fish |
shell completion |
marathon deploy
Auto-detects the current pkgrel from MARATHON_IMAGE/packages/marathon-shell/APKBUILD, builds via pmbootstrap if the APK for that rev is missing, extracts the binary, pushes to /tmp on device, atomic-replaces /usr/bin/marathon-shell-bin, restarts greetd, verifies the hash on both sides matches. One command, ~60 seconds cold, ~15 seconds if the APK is already cached.
The shell's QML is qrc-embedded (compiled into the binary), so yes:
marathon deploy. marathon-ui's QML is disk-imported and can be
hot-deployed via scripts/hot-deploy.sh, but the CLI hasn't wrapped
that yet — TODO for Phase 2.5.
marathon doctor
If anything's red:
marathon doctor --fix
marathon status # snapshot: shell + display + cpu + battery
marathon apps # every app's cgroup state
marathon logs # last 30 lines of shell log
For a live view:
marathon monitor cgroup # every uclamp/freeze transition
marathon monitor logs # tail crash.log
marathon logs -f WARNING # tail, filtered to warnings
marathon session new doze-regression
marathon session record doze-regression wake
marathon session record doze-regression unlock
marathon session record doze-regression "snap doze-active"
marathon session record doze-regression "bench doze"
marathon session record doze-regression "snap doze-in-doze"
marathon session record doze-regression "bench wake"
marathon session record doze-regression "snap doze-back"
marathon session record doze-regression apps
marathon session run doze-regression
Re-run whenever you want to check for regressions:
marathon session run doze-regression
Requires an app to be foreground first:
marathon launch settings
marathon bench foreground-boost
Two-stage check:
- Stage A — the current foreground app has
cpu.uclamp.min=30. - Stage B — launching a different running app swaps: old→0, new→30.
Register both in devices.conf, then:
marathon all status
marathon all doctor
marathon --device cm5 deploy
For a side-by-side comparison:
marathon compare freq
marathon compare fuel-gauge
marathon modem at 'AT+CFUN?'
The CLI stops ModemManager briefly (so we can grab /dev/ttyUSB2), runs the AT, then restarts MM. Call/data connection is unaffected beyond a ~5-second pause.
marathon app new my-app
cd my-app
# edit main.qml, manifest.json
marathon app validate .
marathon app watch . # auto-package + install on save
# in another terminal:
marathon monitor cgroup # see the app appear
marathon launch my-app
Try USB-net fallback (L5 only):
marathon --host root@172.16.42.1 status
Or probe both:
marathon device probe all
marathon audit
Runs clang-format --dry-run --Werror, qmllint, and clang-tidy
on staged files. Same checks the pre-commit hook runs.
marathon freq --sample 30
Or continuously:
marathon monitor freq
| file | purpose |
|---|---|
~/.config/marathon-dev/config |
env-style KEY=VAL overrides for defaults |
~/.config/marathon-dev/devices.conf |
one line per device: name kind host [notes] |
~/.config/marathon-dev/sessions/*.session |
saved test scenarios |
~/.marathon-secrets/askpass.sh |
sudo askpass — copied to /tmp/askpass.sh on demand |
| var | default | notes |
|---|---|---|
MARATHON_HOST |
first device in devices.conf | full user@host |
MARATHON_DEVICE |
first in devices.conf | picks a device by name |
MARATHON_PASSWORD |
marathon |
SSH password |
MARATHON_PIN |
(unset — must be configured) | 6-digit unlock PIN |
MARATHON_SRC |
resolved from CLI location | Marathon-Shell repo root |
MARATHON_IMAGE |
in-tree packaging/ |
packaging tree (aports + pipeline patches) |
MARATHON_SCRATCH |
session-specific tmp dir | scratchpad for snaps/APKs |
MARATHON_VERBOSE |
0 | 1 = show debug traces |
MARATHON_QUIET |
0 | 1 = suppress info/step lines |
| flag | purpose |
|---|---|
--device, -d NAME |
one-shot device override |
--host, -H HOST |
one-shot SSH host override |
--verbose, -v |
debug output |
--quiet, -q |
suppress info/step |
--help, -h |
help |
scripts/
marathon # dispatcher: parse flags, load modules, dispatch
marathon.d/
common.sh # SSH/config/emit helpers + device resolution
phase1.sh # status, deploy, reset, wake, unlock, snap, logs, apps, launch
phase2.sh # tap, swipe, power, doze, freq, irqs, wakeups, cgroup, monitor
phase3.sh # modem, wifi, gov, scheduler, fuel-gauge, backlight
phase4.sh # app new/run/package/validate/install/watch/registry/permissions
phase5.sh # doctor + --fix
phase6.sh # device use/add/probe, all, compare
phase7.sh # build, flash, fmt, lint, audit, changelog
phase8.sh # session, bench
phase9.sh # quickstart, docs, completions
Every verb is a shell function named cmd_<verb> (or
cmd_<verb>_<subverb> for grouped commands). The dispatcher looks
them up by name. Example:
# in scripts/marathon.d/phase3.sh
cmd_thermal() {
marathon::ssh 'for zone in /sys/class/thermal/thermal_zone*/; do
name=$(cat "$zone/type" 2>/dev/null)
temp=$(cat "$zone/temp" 2>/dev/null)
awk -v n="$name" -v t="$temp" "BEGIN{printf \" %-20s %.1f°C\n\", n, t/1000}"
done'
}Then marathon thermal works immediately. No dispatcher change.
For a grouped verb (marathon modem info → cmd_modem_info), define
one function per subverb. The dispatcher finds the longest matching
prefix and hands the rest as args.
Add it to marathon::usage in common.sh and update this file.
| symptom | fix |
|---|---|
marathon: command not found |
Symlink not in PATH — see Install |
askpass: not found |
cp ~/.marathon-secrets/askpass.sh /tmp/askpass.sh && chmod 700 /tmp/askpass.sh (or marathon doctor --fix) |
unreachable |
Try marathon --host root@172.16.42.1 status (USB-net); confirm device is on the same LAN |
unlock types dots but device stays at PIN screen |
Some digit dropped. marathon snap to see how many dots landed, then either re-run marathon unlock (it's idempotent) or tap the missing digit(s) via marathon tap |
launch APPID reports "already running — tapping to bring foreground" but the app stays frozen |
Icon-tap missed. Use marathon snap to find the actual icon location and marathon tap X Y directly |
bench doze reports "timed out waiting for bl_power=4" |
Shell may be in an odd state; try marathon reset then re-bench |
CLI shows "0/22 pass" from marathon doctor |
Almost certainly askpass — see above |
docs/DEVELOPMENT_WORKFLOW.md— source vs installed paths, edit rulesdocs/APP_DEVELOPMENT.md— writing a new Marathon appdocs/MAPP_GUIDE.md— MApp lifecycledocs/ARCHITECTURE.md— the shell's process modeldocs/UI_DESIGN_SYSTEM.md— design tokens + Marathon-Doze power model