Files
02965143d0 fix: fail fast when package manager is not installed (#360)
* fix: fail fast with clear error when package manager is not installed

When PMG shims intercept a package manager that isn't installed (e.g. a
clean JAMF-provisioned laptop where PMG is set up before dev tooling),
real-binary resolution failed with a generic "unknown" error and a
bug-report link, making it look like PMG itself had crashed.

Introduce a typed BinaryNotFoundError that exits with code 127 (standard
"command not found") and maps to a new PackageManagerNotFound error code
with an actionable message instead of the Unknown classification.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: improve missing package manager help message

Use a concise, dynamic install hint instead of explaining PMG's
PATH forwarding internals.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: mention PATH in missing package manager help text

Covers the common case where a package manager is installed but its bin
directory is not on PATH yet (e.g. after curl | bash install).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 18:31:18 +05:30

86 lines
2.1 KiB
Go

package ui
import (
"errors"
"fmt"
"os"
)
type exitCoder interface {
ExitCode() int
}
// transparentExit is satisfied by *runner.ChildExitError without importing it.
type transparentExit interface {
error
Transparent() bool
ExitCode() int
IsSignaled() bool
}
// scrubbedEnvReporter is optionally satisfied by a transparent exit error to
// surface how many environment variables the sandbox scrubbed from the failed
// run. Kept separate from transparentExit so older implementations still
// classify as transparent.
type scrubbedEnvReporter interface {
ScrubbedEnvCount() int
}
type exitDecision struct {
transparent bool
notice bool
code int
message string
}
// classifyExit is the pure decision behind ExitFromCommandError. The notice is
// suppressed for signal exits (the user initiated the interrupt) and in silent
// mode.
func classifyExit(err error) exitDecision {
var te transparentExit
if !errors.As(err, &te) || !te.Transparent() {
return exitDecision{}
}
d := exitDecision{transparent: true, code: te.ExitCode()}
if !te.IsSignaled() && verbosityLevel != VerbosityLevelSilent {
d.notice = true
d.message = "↳ pmg: " + te.Error()
// Env scrubbing produces no sandbox violation and the child's own
// error (e.g. a registry 401) does not point at the cause, so hint at
// it here. Names are not printed; they are at info level via --debug.
if sr, ok := te.(scrubbedEnvReporter); ok && sr.ScrubbedEnvCount() > 0 {
d.message += fmt.Sprintf(
"\n↳ pmg: sandbox scrubbed %d env var(s) (names via --debug, re-allow with --sandbox-allow env=NAME)",
sr.ScrubbedEnvCount())
}
}
return d
}
// ExitFromCommandError is the single exit point for package-manager commands. A
// child that exited on its own is mirrored transparently; everything else keeps
// the visible PMG error framing.
func ExitFromCommandError(err error) {
if err == nil {
return
}
if d := classifyExit(err); d.transparent {
ClearStatus()
if d.notice {
fmt.Fprintln(os.Stderr, Colors.Dim(d.message))
}
os.Exit(d.code)
}
var ec exitCoder
if errors.As(err, &ec) {
ErrorExitWithCode(err, ec.ExitCode())
return
}
ErrorExit(err)
}