mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* fix(sandbox): classify helper-tool errors with usefulerror Sandbox helper commands (profile lint/diff/show/init/list) used to bubble up plain fmt.Errorf chains from the registry layer, which the TUI then classified as Unknown and decorated with a bug-report link. Wrap each error path at the cmd/sandbox boundary so the TUI prints NotFound, InvalidArgument, or PermissionDenied with actionable hints instead. Closes #269 * refactor(sandbox): classify registry errors via sentinel wrapping Replace the fragile substring match in profileLoadError with errors.Is against new sandbox.ErrProfileNotFound / sandbox.ErrProfileInvalid sentinels. Every fmt.Errorf in registry.go that previously communicated "missing" or "malformed" by message text now wraps the corresponding sentinel, so the cmd layer can classify without inspecting strings. * fix(sandbox): detect IO error class when wrapping helper errors Replace static ErrCodeUnknown / ErrCodePermissionDenied wrappings with ioErrorCode, which inspects the error chain for fs.ErrPermission and fs.ErrNotExist before falling back. Applied to runProfileList (where an unreadable user profile directory now classifies as PermissionDenied), registryInitError, and the stat/MkdirAll/WriteFile paths in profile init. Also drop redundant doc comments on helpers whose names are self-evident. --------- Co-authored-by: Claude <noreply@anthropic.com>
149 lines
3.7 KiB
Go
149 lines
3.7 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/safedep/pmg/internal/ui"
|
|
pmgsandbox "github.com/safedep/pmg/sandbox"
|
|
"github.com/safedep/pmg/usefulerror"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type profileListOptions struct {
|
|
jsonOut bool
|
|
}
|
|
|
|
func newProfileListCommand(factory registryFactory) *cobra.Command {
|
|
opts := &profileListOptions{}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "list",
|
|
Short: "List available sandbox profiles (built-in and user)",
|
|
Example: " pmg sandbox profile list",
|
|
SilenceErrors: false,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
err := runProfileList(cmd.OutOrStdout(), opts, factory)
|
|
if err != nil {
|
|
return sandboxErrorExit(cmd, err)
|
|
}
|
|
return err
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVar(&opts.jsonOut, "json", false, "Emit profiles as JSON")
|
|
return cmd
|
|
}
|
|
|
|
func runProfileList(out io.Writer, opts *profileListOptions, factory registryFactory) error {
|
|
registry, err := factory()
|
|
if err != nil {
|
|
return registryInitError(err)
|
|
}
|
|
|
|
summaries, err := registry.ListProfiles()
|
|
if err != nil {
|
|
return wrapUseful(err, ioErrorCode(err, usefulerror.ErrCodeUnknown),
|
|
"Failed to enumerate sandbox profiles. Check the user profile directory permissions.")
|
|
}
|
|
|
|
if opts.jsonOut {
|
|
return writeProfileListJSON(out, summaries)
|
|
}
|
|
|
|
return renderProfileListHuman(out, summaries)
|
|
}
|
|
|
|
type jsonProfileSummary struct {
|
|
Name string `json:"name"`
|
|
Source string `json:"source"`
|
|
Path string `json:"path,omitempty"`
|
|
Inherits string `json:"inherits,omitempty"`
|
|
PackageManagers []string `json:"package_managers,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Shadowed bool `json:"shadowed,omitempty"`
|
|
}
|
|
|
|
type jsonProfileListReport struct {
|
|
Profiles []jsonProfileSummary `json:"profiles"`
|
|
}
|
|
|
|
func writeProfileListJSON(out io.Writer, summaries []pmgsandbox.ProfileSummary) error {
|
|
report := jsonProfileListReport{Profiles: make([]jsonProfileSummary, 0, len(summaries))}
|
|
for _, s := range summaries {
|
|
report.Profiles = append(report.Profiles, jsonProfileSummary{
|
|
Name: s.Name,
|
|
Source: string(s.Source),
|
|
Path: s.Path,
|
|
Inherits: s.Inherits,
|
|
PackageManagers: s.PackageManagers,
|
|
Description: s.Description,
|
|
Shadowed: s.Shadowed,
|
|
})
|
|
}
|
|
|
|
return writeJSONIndent(out, report)
|
|
}
|
|
|
|
func renderProfileListHuman(out io.Writer, summaries []pmgsandbox.ProfileSummary) error {
|
|
if len(summaries) == 0 {
|
|
_, err := fmt.Fprintln(out, ui.Colors.Dim("No sandbox profiles available."))
|
|
return err
|
|
}
|
|
|
|
if _, err := fmt.Fprintln(out); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(out, ui.Colors.Cyan("Sandbox Profiles")); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(out, ui.Colors.Normal("-----------------")); err != nil {
|
|
return err
|
|
}
|
|
|
|
rows := make([][]string, 0, len(summaries)+1)
|
|
rows = append(rows, []string{
|
|
ui.Colors.Bold("STATUS"),
|
|
ui.Colors.Bold("NAME"),
|
|
ui.Colors.Bold("SOURCE"),
|
|
ui.Colors.Bold("INHERITS"),
|
|
ui.Colors.Bold("PMS"),
|
|
ui.Colors.Bold("DESCRIPTION"),
|
|
})
|
|
for _, s := range summaries {
|
|
rows = append(rows, []string{
|
|
statusCell(s),
|
|
s.Name,
|
|
sourceCell(s),
|
|
emptyDash(s.Inherits),
|
|
truncate(strings.Join(s.PackageManagers, ","), 30),
|
|
truncate(s.Description, 60),
|
|
})
|
|
}
|
|
|
|
return renderTable(out, rows, nil)
|
|
}
|
|
|
|
func statusCell(s pmgsandbox.ProfileSummary) string {
|
|
if s.Shadowed {
|
|
return ui.Colors.Dim("SHADOWED")
|
|
}
|
|
return " "
|
|
}
|
|
|
|
func sourceCell(s pmgsandbox.ProfileSummary) string {
|
|
if s.Source == pmgsandbox.ProfileSourceBuiltin {
|
|
return ui.Colors.Dim("builtin")
|
|
}
|
|
return truncateLeft(s.Path, 50)
|
|
}
|
|
|
|
func emptyDash(s string) string {
|
|
if s == "" {
|
|
return ui.Colors.Dim("—")
|
|
}
|
|
return s
|
|
}
|