mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* feat: add doctor check runner core types and logic * feat: add doctor checks for config, binary, directory, and aliases * feat: add doctor checks for sandbox and security features * feat: add protection verification check using test malicious packages * feat: add summarized package manager availability check * feat: add pmg setup doctor command with compact output * feat: add PATH shim verification to doctor command * fix: improve doctor command UX and alias detection - Capitalize all check messages for consistent output - Dim passing checks, color warn/fail for visual clarity - Silence empty Cobra error output on doctor failure - Remove redundant pmg binary check (self-evident) - Fix alias IsInstalled to skip commented-out source lines - Improve protection failure message * refactor: remove package manager availability check from doctor * fix: handle os.RemoveAll error in doctor protection check * refactor: use table layout for setup doctor, extract shared table renderer Move renderTable, truncate, and visibleWidth helpers from cmd/sandbox to internal/ui so both sandbox and setup doctor share them. Rewrite setup doctor output to use the same table structure as sandbox doctor. Fix VisibleWidth to count runes instead of bytes for correct alignment with multi-byte UTF-8 characters. * docs: add pmg setup doctor to README, remove manual verification step * refactor: use constants for check names, rename and inline doctor helpers Address PR review comments: extract check name constants, rename CheckConfigFile to CheckFileExists and CheckDirectoryWritable to CheckDirectoryExists for reusability, inline trivial wrappers (CheckSandbox, CheckSecurityFeature, CheckProxyMode), and add fix hints for all checks with correct config keys. * refactor: inline simple doctor checks into command layer * fix: skip protection check when aliases and shims are inactive Protection checks now fail immediately when shell aliases and shims are both inactive, instead of falsely passing by running through the pmg binary directly. Also clean up summary messages to remove redundant fix hints and truncated paths.
264 lines
6.6 KiB
Go
264 lines
6.6 KiB
Go
package alias
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/safedep/dry/log"
|
|
)
|
|
|
|
// AliasManager manages shell aliases for package managers.
|
|
type AliasManager struct {
|
|
config AliasConfig
|
|
rcFileManager RcFileManager
|
|
}
|
|
|
|
// AliasConfig holds configuration for alias management.
|
|
type AliasConfig struct {
|
|
RcFileName string
|
|
PackageManagers []string
|
|
Shells []Shell
|
|
}
|
|
|
|
// RcFileManager handles creation and removal of RC files.
|
|
type RcFileManager interface {
|
|
Create(aliases []string) (string, error)
|
|
Remove() error
|
|
GetRcPath() string
|
|
}
|
|
|
|
// DefaultRcFileManager implements RcFileManager for managing the RC file.
|
|
type defaultRcFileManager struct {
|
|
HomeDir string
|
|
RcFileName string
|
|
}
|
|
|
|
var _ RcFileManager = &defaultRcFileManager{}
|
|
|
|
// NewDefaultRcFileManager creates a new DefaultRcFileManager.
|
|
func NewDefaultRcFileManager(rcFileName string) (*defaultRcFileManager, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &defaultRcFileManager{
|
|
HomeDir: homeDir,
|
|
RcFileName: rcFileName,
|
|
}, nil
|
|
}
|
|
|
|
// Create creates the RC file with the given aliases.
|
|
func (m *defaultRcFileManager) Create(aliases []string) (string, error) {
|
|
rcPath := m.GetRcPath()
|
|
f, err := os.Create(rcPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer func() {
|
|
if err := f.Close(); err != nil {
|
|
log.Warnf("failed to close rc file %s: %v", rcPath, err)
|
|
}
|
|
}()
|
|
|
|
for _, alias := range aliases {
|
|
if _, err := f.WriteString(alias); err != nil {
|
|
return "", fmt.Errorf("failed to write alias: %w", err)
|
|
}
|
|
}
|
|
return rcPath, nil
|
|
}
|
|
|
|
// Remove deletes the RC file.
|
|
func (m *defaultRcFileManager) Remove() error {
|
|
rcPath := m.GetRcPath()
|
|
if err := os.Remove(rcPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("could not delete %s: %w", rcPath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetRcPath returns the full path to the RC file.
|
|
func (m *defaultRcFileManager) GetRcPath() string {
|
|
return filepath.Join(m.HomeDir, m.RcFileName)
|
|
}
|
|
|
|
// DefaultConfig returns the default configuration for alias management.
|
|
func DefaultConfig() AliasConfig {
|
|
var shells []Shell
|
|
|
|
fishShell, _ := NewFishShell()
|
|
zshShell, _ := NewZshShell()
|
|
bashShell, _ := NewBashShell()
|
|
|
|
shells = append(shells, fishShell, zshShell, bashShell)
|
|
|
|
return AliasConfig{
|
|
RcFileName: ".pmg.rc",
|
|
PackageManagers: []string{"npm", "pip", "pip3", "pnpm", "bun", "uv", "yarn", "poetry", "npx", "pnpx"},
|
|
Shells: shells,
|
|
}
|
|
}
|
|
|
|
// New creates a new AliasManager with the given configuration and RC file manager.
|
|
func New(config AliasConfig, rcFileManager RcFileManager) *AliasManager {
|
|
return &AliasManager{
|
|
config: config,
|
|
rcFileManager: rcFileManager,
|
|
}
|
|
}
|
|
|
|
// Install creates the RC file with aliases and sources it in shell configurations.
|
|
func (a *AliasManager) Install() error {
|
|
aliases := a.buildAliases()
|
|
_, err := a.rcFileManager.Create(aliases)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create alias file: %w", err)
|
|
}
|
|
|
|
err = a.sourceRcFile()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update shell configs: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Remove deletes the RC file and removes source lines from shell configurations.
|
|
func (a *AliasManager) Remove() error {
|
|
if err := a.rcFileManager.Remove(); err != nil {
|
|
log.Warnf("Warning: %v", err)
|
|
}
|
|
|
|
if err := a.removeSourceLinesFromShells(); err != nil {
|
|
return fmt.Errorf("failed to clean shell configs: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetRcPath returns the path to the alias RC file managed by AliasManager.
|
|
func (a *AliasManager) GetRcPath() string {
|
|
return a.rcFileManager.GetRcPath()
|
|
}
|
|
|
|
// IsInstalled checks if the PMG aliases are sourced in any of the shell config files.
|
|
func (a *AliasManager) IsInstalled() (bool, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
for _, shell := range a.config.Shells {
|
|
for _, configPath := range shell.CandidateRcFiles(homeDir) {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
|
|
log.Warnf("Warning: could not read %s (%s)", configPath, err)
|
|
continue
|
|
}
|
|
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trimmed, "#") {
|
|
continue
|
|
}
|
|
if strings.Contains(trimmed, a.config.RcFileName) {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// buildAliases creates the alias strings for all configured package managers.
|
|
func (a *AliasManager) buildAliases() []string {
|
|
aliases := make([]string, 0, len(a.config.PackageManagers))
|
|
for _, pm := range a.config.PackageManagers {
|
|
aliases = append(aliases, fmt.Sprintf("alias %s='pmg %s'\n", pm, pm))
|
|
}
|
|
return aliases
|
|
}
|
|
|
|
// sourceRcFile adds source lines to all shell configuration files.
|
|
func (a *AliasManager) sourceRcFile() error {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
primary := PrimaryShellName()
|
|
for _, shell := range a.config.Shells {
|
|
files, err := shell.InstallRcFiles(homeDir, shell.Name() == primary)
|
|
if err != nil {
|
|
log.Warnf("Warning: skipping %s (%s)", shell.Name(), err)
|
|
continue
|
|
}
|
|
|
|
for _, configPath := range files {
|
|
if err := a.addSourceLine(configPath, shell.Source(a.rcFileManager.GetRcPath())); err != nil {
|
|
log.Warnf("Warning: skipping %s (%s)", configPath, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// removeSourceLinesFromShells removes source lines from all shell configuration files.
|
|
func (a *AliasManager) removeSourceLinesFromShells() error {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
drop := func(line string) bool {
|
|
return strings.Contains(line, a.config.RcFileName) ||
|
|
strings.TrimSpace(line) == strings.TrimSpace(commentForRemovingShellSource)
|
|
}
|
|
|
|
for _, shell := range a.config.Shells {
|
|
for _, configPath := range shell.CandidateRcFiles(homeDir) {
|
|
if err := RewriteFileDroppingLines(configPath, drop); err != nil {
|
|
log.Warnf("Warning: failed to update %s: %s", configPath, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// addSourceLine adds a source line to the specified shell configuration file.
|
|
func (a *AliasManager) addSourceLine(configPath, sourceLine string) error {
|
|
// Read existing content - only proceed if file exists
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return err // file doesn't exist or can't read, skip
|
|
}
|
|
|
|
if strings.Contains(string(data), a.config.RcFileName) {
|
|
return nil // already sourced, skip
|
|
}
|
|
|
|
f, err := os.OpenFile(configPath, os.O_APPEND|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if err := f.Close(); err != nil {
|
|
log.Warnf("failed to close config file %s: %v", configPath, err)
|
|
}
|
|
}()
|
|
|
|
_, err = fmt.Fprintf(f, "\n%s", sourceLine)
|
|
return err
|
|
}
|