Follow-up to #41. That issue raised two things; the remote half was a real bug and is
fixed in #44. This is the other half, which needs a design decision rather than a
message tweak.
The problem
Reported in #41: when a command fails on a permission error, the raw Go error leads and
the actionable hint trails behind it.
upgrading local... ✗ cannot backup current binary: rename /usr/local/bin/homebutler /usr/local/bin/homebutler.bak: permission denied
⚠️ Try: sudo homebutler upgrade
The hint is correct. The complaint is about ordering and noise — the only thing the user
needed was "rerun with sudo", and rename /usr/local/bin/homebutler /usr/local/bin/homebutler.bak: permission denied tells them nothing they can act on.
The suggestion in #41 was to show just the fix and keep the raw error for verbose mode.
The second half is the blocker: there is no --verbose/--debug flag today, so dropping
the raw error would make it unrecoverable. I'd rather add the flag than lose the detail.
Scope
This is not upgrade-specific. The same raw error + hint shape is used in nine places:
internal/remote/upgrade.go:107,119 — backup/write binary
internal/backup/backup.go:79,85 — create backup/compose dir
internal/backup/restore.go:127 — create bind mount dir
internal/install/install.go:39,45,63,683,692 — registry and app dirs
cmd/ports.go:31 — missing process names
Whatever we decide should apply to all of them, otherwise the output gets inconsistent.
Why it isn't a one-line change
The messages are built inside internal/*, but a flag lives in cmd/. So this needs a
way for the internal packages to know the verbosity level. Options:
- A package-level setting in
internal/util that cmd/root.go sets before dispatch.
Simplest, but it's global mutable state.
- Thread a verbosity value through the call sites. Cleanest, but touches a lot of
signatures for a presentation concern.
- Keep returning the full error and strip the raw part at the presentation layer. Needs
the two parts to be separable — a typed error carrying {cause, hint} rather than a
pre-formatted string.
I lean toward (3): a small error type with a hint field, formatted at the point of
display. It fixes the ordering problem without any global state, and --verbose just
selects a different formatter. util.IsPermissionError already exists and is what gates
the hint today, so the detection side is done.
Notes
- The flag goes in
cmd/root.go:30-34 alongside --json, --server, --all, --config.
maybeRouteRemote strips --server, --all, --config before forwarding argv to a
remote host (cmd/root.go:79,88). --verbose should not be stripped — if you ask
for detail you want it from the remote too.
--json output should be unaffected; it isn't the human-facing path.
- Naming:
--verbose/-v collides with the usual short flag for version. homebutler version is a subcommand here, not a flag, so -v looks free — worth double-checking
before claiming it.
Proposed default output
upgrading local... ✗ permission denied — rerun with: sudo homebutler upgrade
with the current full text available under --verbose.
Follow-up to #41. That issue raised two things; the remote half was a real bug and is
fixed in #44. This is the other half, which needs a design decision rather than a
message tweak.
The problem
Reported in #41: when a command fails on a permission error, the raw Go error leads and
the actionable hint trails behind it.
The hint is correct. The complaint is about ordering and noise — the only thing the user
needed was "rerun with sudo", and
rename /usr/local/bin/homebutler /usr/local/bin/homebutler.bak: permission deniedtells them nothing they can act on.The suggestion in #41 was to show just the fix and keep the raw error for verbose mode.
The second half is the blocker: there is no
--verbose/--debugflag today, so droppingthe raw error would make it unrecoverable. I'd rather add the flag than lose the detail.
Scope
This is not upgrade-specific. The same
raw error + hintshape is used in nine places:internal/remote/upgrade.go:107,119— backup/write binaryinternal/backup/backup.go:79,85— create backup/compose dirinternal/backup/restore.go:127— create bind mount dirinternal/install/install.go:39,45,63,683,692— registry and app dirscmd/ports.go:31— missing process namesWhatever we decide should apply to all of them, otherwise the output gets inconsistent.
Why it isn't a one-line change
The messages are built inside
internal/*, but a flag lives incmd/. So this needs away for the internal packages to know the verbosity level. Options:
internal/utilthatcmd/root.gosets before dispatch.Simplest, but it's global mutable state.
signatures for a presentation concern.
the two parts to be separable — a typed error carrying
{cause, hint}rather than apre-formatted string.
I lean toward (3): a small error type with a hint field, formatted at the point of
display. It fixes the ordering problem without any global state, and
--verbosejustselects a different formatter.
util.IsPermissionErroralready exists and is what gatesthe hint today, so the detection side is done.
Notes
cmd/root.go:30-34alongside--json,--server,--all,--config.maybeRouteRemotestrips--server,--all,--configbefore forwarding argv to aremote host (
cmd/root.go:79,88).--verboseshould not be stripped — if you askfor detail you want it from the remote too.
--jsonoutput should be unaffected; it isn't the human-facing path.--verbose/-vcollides with the usual short flag for version.homebutler versionis a subcommand here, not a flag, so-vlooks free — worth double-checkingbefore claiming it.
Proposed default output
with the current full text available under
--verbose.