You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Build: clear error when devpod binary is missing #57
Build: fail with a clear message when the devpod binary is missing.
run_devpod (dl.py:695-708) catches nothing — on a box without devpod, dl --ls prints a raw FileNotFoundError traceback. Catch the missing-binary case at the shell-out seam and emit a one-line actionable error (install hint), exit non-zero. Runtime deps devpod/git/gh are all assumed on PATH and only git-lfs is probed (workspace_clone.py:111) — cover git at the same seam if it falls out naturally, but devpod is the ticket.
Landed in #71. run_devpod is the only place the package spawns devpod, so the missing binary is translated there, caught once in main(), and reported as one stderr line with exit 127 — the shell's own "command not found" code, and dl otherwise propagates devpod's returncodes verbatim.
Two modelling choices carried the work:
It is not an OSError.FileNotFoundError is one, and dl.py has ~12 broad except OSError / except (RuntimeError, OSError) handlers. Letting it travel as an OSError meant the attach path reported Failed to create workspace: [Errno 2]... — a misleading message, which is barely better than the traceback. Verified by experiment: rebasing the exception on OSError makes the survival test fail with assert 1 == 127.
It is not a returncode. A synthetic 127 in the CompletedProcess would send callers — all of which branch on returncode — down paths that assume devpod ran. An absent binary is a different kind of outcome from a command that ran and failed, so it stays out of that value domain.
Verified against the real CLI with PATH=/nonexistent, capturing the three streams separately: --help and --version are rc 0 with empty stderr and provably never call subprocess.run; --ls, --repos, --completion-data, --update-cache are rc 127 with empty stdout, one stderr line, no traceback. Empty stdout matters because the completion machinery parses it. --update-cache now exits rather than overwriting a good completion cache with an empty one.
Also checked by hand: none of the five except Exception sites in the package can swallow it (dl.py:920 sits after its run_devpod call, wrapping only clone cleanup; the rest are git/clone work in worktree/), and run_devpod passes no cwd, so FileNotFoundError there can only mean the executable — the message cannot lie about the cause.
git — assessed, deliberately not done
The ticket allowed covering git at the same seam "if it falls out naturally". It does not. git is spawned from ~20 sites across dl.py, repo_manager.py, branch_manager.py and workspace_clone.py, and — measured, not assumed — there is no traceback to fix: with git absent, dl owner/repo@main already prints one line and exits 1.
Repository 'owner/repo': [Errno 2] No such file or directory: 'git'
Cryptic, but a single line. Improving it means either a new exception threaded through three modules or a shutil.which("git") probe that covers only the clone path (dl <existing-ws> and dl ./path never touch git). That is its own slice. The completion paths catch OSError and degrade to empty lists, which is correct there.
Incidental
The metadata-hardening merge (#56) had landed with codespell red on main, which blocks every branch cut from it regardless of content. Fixed in the same PR.
Activity
Give a clear error when devpod is missing
blooop commentedon Aug 7, 2026
Answer
Landed in #71.
run_devpodis the only place the package spawns devpod, so the missing binary is translated there, caught once inmain(), and reported as one stderr line with exit 127 — the shell's own "command not found" code, anddlotherwise propagates devpod's returncodes verbatim.Two modelling choices carried the work:
It is not an
OSError.FileNotFoundErroris one, anddl.pyhas ~12 broadexcept OSError/except (RuntimeError, OSError)handlers. Letting it travel as anOSErrormeant the attach path reportedFailed to create workspace: [Errno 2]...— a misleading message, which is barely better than the traceback. Verified by experiment: rebasing the exception onOSErrormakes the survival test fail withassert 1 == 127.It is not a returncode. A synthetic
127in theCompletedProcesswould send callers — all of which branch onreturncode— down paths that assume devpod ran. An absent binary is a different kind of outcome from a command that ran and failed, so it stays out of that value domain.Verified against the real CLI with
PATH=/nonexistent, capturing the three streams separately:--helpand--versionare rc 0 with empty stderr and provably never callsubprocess.run;--ls,--repos,--completion-data,--update-cacheare rc 127 with empty stdout, one stderr line, no traceback. Empty stdout matters because the completion machinery parses it.--update-cachenow exits rather than overwriting a good completion cache with an empty one.Also checked by hand: none of the five
except Exceptionsites in the package can swallow it (dl.py:920sits after itsrun_devpodcall, wrapping only clone cleanup; the rest are git/clone work inworktree/), andrun_devpodpasses nocwd, soFileNotFoundErrorthere can only mean the executable — the message cannot lie about the cause.git— assessed, deliberately not doneThe ticket allowed covering
gitat the same seam "if it falls out naturally". It does not. git is spawned from ~20 sites acrossdl.py,repo_manager.py,branch_manager.pyandworkspace_clone.py, and — measured, not assumed — there is no traceback to fix: with git absent,dl owner/repo@mainalready prints one line and exits 1.Cryptic, but a single line. Improving it means either a new exception threaded through three modules or a
shutil.which("git")probe that covers only the clone path (dl <existing-ws>anddl ./pathnever touch git). That is its own slice. The completion paths catchOSErrorand degrade to empty lists, which is correct there.Incidental
The metadata-hardening merge (#56) had landed with codespell red on
main, which blocks every branch cut from it regardless of content. Fixed in the same PR.