Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/ca.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package cmd

import "github.com/prvious/pv/internal/commands/ca"

func init() {
ca.Register(rootCmd)
}
12 changes: 3 additions & 9 deletions cmd/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"strings"

Expand Down Expand Up @@ -664,14 +663,9 @@ func checkPortListening(port int) bool {
}

func checkCATrusted() bool {
out, err := exec.Command("security", "find-certificate", "-c", "Caddy Local Authority", "/Library/Keychains/System.keychain").CombinedOutput()
trusted, err := setup.IsCATrusted()
if err != nil {
// Also check login keychain.
out2, err2 := exec.Command("security", "find-certificate", "-c", "Caddy Local Authority").CombinedOutput()
if err2 != nil {
return false
}
return strings.Contains(string(out2), "Caddy Local Authority")
return false
}
return strings.Contains(string(out), "Caddy Local Authority")
return trusted
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func init() {
&cobra.Group{ID: "mago", Title: "Mago"},
&cobra.Group{ID: "colima", Title: "Colima"},
&cobra.Group{ID: "service", Title: "Services"},
&cobra.Group{ID: "ca", Title: "CA"},
&cobra.Group{ID: "daemon", Title: "Daemon"},
)
}
Expand Down
94 changes: 63 additions & 31 deletions cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/prvious/pv/internal/config"
"github.com/prvious/pv/internal/phpenv"
"github.com/prvious/pv/internal/services"
setupinternal "github.com/prvious/pv/internal/setup"
"github.com/prvious/pv/internal/tools"
"github.com/prvious/pv/internal/ui"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -117,28 +118,42 @@ var setupCmd = &cobra.Command{
return err
}

if err := ui.Step("Checking prerequisites...", func() (string, error) {
if err := setupinternal.CheckOS(); err != nil {
return "", err
}
return fmt.Sprintf("macOS %s", setupinternal.PlatformLabel()), nil
}); err != nil {
return err
}

// Acquire sudo upfront.
if err := acquireSudo(); err != nil {
return err
}

// Ensure directories exist.
if err := config.EnsureDirs(); err != nil {
return fmt.Errorf("cannot create directories: %w", err)
}
if err := ui.Step("Preparing environment...", func() (string, error) {
if err := config.EnsureDirs(); err != nil {
return "", fmt.Errorf("cannot create directories: %w", err)
}

// Build settings from wizard output, preserving existing PHP default.
s := &config.Settings{
Defaults: config.Defaults{TLD: tld, PHP: settings.Defaults.PHP, Daemon: config.BoolPtr(daemon)},
Automation: automation,
}
if err := s.Save(); err != nil {
return fmt.Errorf("cannot save settings: %w", err)
}
// Build settings from wizard output, preserving existing PHP default.
s := &config.Settings{
Defaults: config.Defaults{TLD: tld, PHP: settings.Defaults.PHP, Daemon: config.BoolPtr(daemon)},
Automation: automation,
}
if err := s.Save(); err != nil {
return "", fmt.Errorf("cannot save settings: %w", err)
}

// Write Valet-compatible config for Vite TLS auto-detection.
if err := certs.EnsureValetConfig(tld); err != nil {
ui.Subtle(fmt.Sprintf("Vite TLS config: %v", err))
// Write Valet-compatible config for Vite TLS auto-detection.
if err := certs.EnsureValetConfig(tld); err != nil {
ui.Subtle(fmt.Sprintf("Vite TLS config: %v", err))
}

return "Settings saved", nil
}); err != nil {
return err
}

// Install PHP versions.
Expand All @@ -159,11 +174,24 @@ var setupCmd = &cobra.Command{
}
}

// Set global PHP if not set.
if _, err := phpenv.GlobalVersion(); err != nil && len(selectedPHP) > 0 {
if err := ui.Step("Configuring global PHP...", func() (string, error) {
if len(selectedPHP) == 0 {
return "No PHP versions selected", nil
}

if _, err := phpenv.GlobalVersion(); err == nil {
return "Global PHP already configured", nil
}

latest := selectedPHP[len(selectedPHP)-1]
if err := phpenv.SetGlobal(latest); err == nil {
ui.Success(fmt.Sprintf("PHP %s set as global default", latest))
if err := phpenv.SetGlobal(latest); err != nil {
return "", err
}

return fmt.Sprintf("PHP %s set as global default", latest), nil
}); err != nil {
if !errors.Is(err, ui.ErrAlreadyPrinted) {
ui.Fail(fmt.Sprintf("Global PHP setup failed: %v", err))
}
}

Expand All @@ -188,20 +216,24 @@ var setupCmd = &cobra.Command{
}
}

// Expose all installed tools (shims + symlinks).
if err := tools.ExposeAll(); err != nil {
ui.Fail(fmt.Sprintf("Tool exposure failed: %v", err))
}

// Save version manifest.
vs, err := binaries.LoadVersions()
if err == nil {
if len(selectedPHP) > 0 {
vs.Set("php", selectedPHP[len(selectedPHP)-1])
if err := ui.Step("Updating tool shims...", func() (string, error) {
if err := tools.ExposeAll(); err != nil {
return "", err
}
if saveErr := vs.Save(); saveErr != nil {
ui.Fail(fmt.Sprintf("Cannot save version manifest: %v", saveErr))

vs, err := binaries.LoadVersions()
if err == nil {
if len(selectedPHP) > 0 {
vs.Set("php", selectedPHP[len(selectedPHP)-1])
}
if saveErr := vs.Save(); saveErr != nil {
ui.Fail(fmt.Sprintf("Cannot save version manifest: %v", saveErr))
}
}

return "Tool shims updated", nil
}); err != nil {
ui.Fail(fmt.Sprintf("Tool exposure failed: %v", err))
}

// Finalize: Caddyfile, DNS, CA trust, shell PATH.
Expand Down
80 changes: 49 additions & 31 deletions cmd/uninstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ var uninstallCmd = &cobra.Command{
GroupID: "core",
Short: "Completely remove pv and all its data",
RunE: func(cmd *cobra.Command, args []string) error {
start := time.Now()

// Confirmation prompt.
ui.Subtle("This will remove:")
ui.Subtle("- The pv binary")
Expand Down Expand Up @@ -81,19 +83,30 @@ var uninstallCmd = &cobra.Command{
}
}

// Read registry before deletion (for pv.yml file scan later).
fmt.Fprintln(os.Stderr)
ui.Header(version)

var projectPaths []string
reg, err := registry.Load()
if err == nil {
for _, p := range reg.List() {
projectPaths = append(projectPaths, p.Path)
tld := "test"
var reg *registry.Registry

if err := ui.Step("Preparing uninstall...", func() (string, error) {
loadedReg, err := registry.Load()
if err == nil {
reg = loadedReg
for _, p := range reg.List() {
projectPaths = append(projectPaths, p.Path)
}
}
}

settings, _ := config.LoadSettings()
tld := "test"
if settings != nil {
tld = settings.Defaults.TLD
settings, _ := config.LoadSettings()
if settings != nil {
tld = settings.Defaults.TLD
}

return fmt.Sprintf("Using .%s domain", tld), nil
}); err != nil {
return err
}

// Uninstall tools (each cleans up its own binary + PATH entry).
Expand Down Expand Up @@ -162,9 +175,31 @@ var uninstallCmd = &cobra.Command{
// Error already displayed by ui.Step
}

resolverFile := filepath.Join("/etc/resolver", tld)
caCertPath := config.CACertPath()
if err := ui.Step("Checking system cleanup requirements...", func() (string, error) {
needSudo := false
if _, err := os.Stat(resolverFile); err == nil {
needSudo = true
}
if _, err := os.Stat(caCertPath); err == nil {
needSudo = true
}

if needSudo {
if err := acquireSudo(); err != nil {
return "", err
}
return "Sudo ready for system cleanup", nil
}

return "No sudo cleanup needed", nil
}); err != nil {
return err
}

// Remove system configuration (sudo).
if err := ui.Step("Removing DNS resolver...", func() (string, error) {
resolverFile := filepath.Join("/etc/resolver", tld)
if runSudo("rm", "-f", resolverFile) {
return "DNS resolver removed", nil
}
Expand All @@ -174,30 +209,12 @@ var uninstallCmd = &cobra.Command{
}

// Untrust CA certificate.
caCertPath := config.CACertPath()
if _, err := os.Stat(caCertPath); err == nil {
if err := ui.Step("Removing CA certificate...", func() (string, error) {
certCmd := exec.Command("sudo", "-n", "security", "remove-trusted-cert", "-d", caCertPath)
certCmd.Stdout = os.Stdout
certCmd.Stderr = os.Stderr

if err := certCmd.Start(); err != nil {
if err := setup.RunSudoUntrustCACert(); err != nil {
return "", fmt.Errorf("could not untrust CA — run: sudo security remove-trusted-cert -d %s", caCertPath)
}

done := make(chan error, 1)
go func() { done <- certCmd.Wait() }()
select {
case err := <-done:
if err != nil {
return "", fmt.Errorf("could not untrust CA — run: sudo security remove-trusted-cert -d %s", caCertPath)
}
return "CA certificate removed", nil
case <-time.After(10 * time.Second):
certCmd.Process.Kill()
<-done
return "", fmt.Errorf("CA removal timed out — run: sudo security remove-trusted-cert -d %s", caCertPath)
}
return "CA certificate removed", nil
}); err != nil {
// Error already displayed by ui.Step
}
Expand Down Expand Up @@ -278,6 +295,7 @@ var uninstallCmd = &cobra.Command{
ui.Subtle(" eval \"$(pv env)\" # if present")

ui.Success("pv has been completely uninstalled. Your projects were not modified.")
ui.Footer(start, "https://pv.prvious.dev/docs")

return nil
},
Expand Down
45 changes: 30 additions & 15 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Examples:
curl -fsSL https://pv.prvious.dev/install | bash
curl -fsSL https://pv.prvious.dev/install | bash -s -- --version 0.2.0
curl -fsSL https://pv.prvious.dev/install | bash -s -- --install-dir /usr/local/bin
PV_INSTALL_LOCAL_BUILD=./pv bash install.sh
EOF
}

Expand All @@ -36,6 +37,7 @@ EOF
requested_version=""
no_modify_path=false
install_dir=""
local_build="${PV_INSTALL_LOCAL_BUILD:-}"

while [[ $# -gt 0 ]]; do
case "$1" in
Expand Down Expand Up @@ -352,9 +354,11 @@ main() {
local platform
platform=$(detect_platform)

# Resolve version
local version
version=$(resolve_version)
# Resolve version unless we are testing with a local build.
local version="local"
if [[ -z "$local_build" ]]; then
version=$(resolve_version)
fi

# Print header
print_header "$version"
Expand All @@ -378,26 +382,37 @@ main() {
# Check for existing installation
check_existing

# Download pv binary
local url="https://github.com/prvious/pv/releases/download/v${version}/pv-${platform}"
local tmp_dir="${TMPDIR:-/tmp}/pv_install_$$"
mkdir -p "$tmp_dir"
trap "rm -rf '$tmp_dir'" EXIT

echo -e " ${MUTED}Downloading pv...${NC}"

if [[ -t 2 ]] && download_with_progress "$url" "$tmp_dir/pv" 2>&1; then
: # progress bar showed
else
# Fallback: no TTY or progress bar failed
if ! curl -fsSL -o "$tmp_dir/pv" "$url"; then
if [[ -n "$local_build" ]]; then
if [[ ! -f "$local_build" ]]; then
echo ""
echo -e " ${RED}Failed to download pv${NC}"
echo -e " ${MUTED}Check your internet connection and try again.${NC}"
echo -e " ${MUTED}Manual download: https://github.com/prvious/pv/releases${NC}"
echo -e " ${RED}Local build not found: $local_build${NC}"
echo ""
exit 1
fi
echo -e " ${MUTED}Using local pv build from ${local_build}...${NC}"
cp "$local_build" "$tmp_dir/pv"
else
local url="https://github.com/prvious/pv/releases/download/v${version}/pv-${platform}"

echo -e " ${MUTED}Downloading pv...${NC}"

if [[ -t 2 ]] && download_with_progress "$url" "$tmp_dir/pv" 2>&1; then
: # progress bar showed
else
# Fallback: no TTY or progress bar failed
if ! curl -fsSL -o "$tmp_dir/pv" "$url"; then
echo ""
echo -e " ${RED}Failed to download pv${NC}"
echo -e " ${MUTED}Check your internet connection and try again.${NC}"
echo -e " ${MUTED}Manual download: https://github.com/prvious/pv/releases${NC}"
echo ""
exit 1
fi
fi
fi

chmod 755 "$tmp_dir/pv"
Expand Down
Loading
Loading