From 5e13098134de6341707c13f2bb2f6ae9352d7ccb Mon Sep 17 00:00:00 2001 From: Teethat Kamsai <79137211+teethatkamsai@users.noreply.github.com> Date: Sun, 5 Jul 2026 17:01:54 +0700 Subject: [PATCH 1/3] Make uninstall remove all managed data --- cli/main.go | 63 +++++++++++++++++++++++++++---------------- cli/uninstall_test.go | 14 +++------- install.ps1 | 30 ++++++++++++++------- install.sh | 2 +- 4 files changed, 65 insertions(+), 44 deletions(-) diff --git a/cli/main.go b/cli/main.go index 7b7140a..0f1f180 100644 --- a/cli/main.go +++ b/cli/main.go @@ -316,7 +316,7 @@ func mcpInstall() int { } func uninstall(args []string) int { - yes, dryRun, purge := false, false, false + yes, dryRun := false, false for _, a := range args { switch a { case "--yes", "-y": @@ -324,7 +324,7 @@ func uninstall(args []string) int { case "--dry-run": dryRun = true case "--purge": - purge = true + // Kept for compatibility; uninstall already removes everything. case "--help", "-h": printUninstallHelp() return 0 @@ -341,25 +341,22 @@ func uninstall(args []string) int { if !pathContains(paths.BinDir(), self) { self = "" } - targets := uninstallTargets(home, purge) + targets := uninstallTargets(home) links := podcliLinks(managed, self) fmt.Println("podcli uninstall") - if purge { - fmt.Printf(" This will remove podcli and all managed data under: %s\n", home) - } else { - fmt.Printf(" This will remove podcli's app files under: %s\n", home) - fmt.Println(" Your config, knowledge, presets, assets, history, and cache are kept.") - fmt.Println(" Use `podcli uninstall --purge` to remove everything in that folder.") - } + fmt.Printf(" This will remove podcli and all managed data under: %s\n", home) for _, p := range targets { fmt.Printf(" remove: %s\n", p) } for _, p := range links { fmt.Printf(" unlink: %s\n", p) } + if runtime.GOOS == "windows" { + fmt.Printf(" remove from user PATH: %s\n", paths.BinDir()) + } if dryRun { - fmt.Println("Dry run only — nothing removed.") + fmt.Println("Dry run only - nothing removed.") return 0 } if !yes && !confirm("Continue? [y/N] ") { @@ -372,6 +369,13 @@ func uninstall(args []string) int { fmt.Fprintf(os.Stderr, " warning: could not remove %s: %v\n", p, err) } } + if runtime.GOOS == "windows" { + if removed, err := removeFromWindowsUserPath(paths.BinDir()); err != nil { + fmt.Fprintf(os.Stderr, " warning: could not remove %s from user PATH: %v\n", paths.BinDir(), err) + } else if removed { + fmt.Println(" removed from user PATH (restart your terminal)") + } + } runningInUse := false for _, p := range targets { if runtime.GOOS == "windows" && pathContains(p, self) { @@ -389,18 +393,12 @@ func uninstall(args []string) int { fmt.Fprintf(os.Stderr, " note: the running binary is still in use and was left in place: %s\n", self) fmt.Fprintln(os.Stderr, " Delete it after this command exits, or run the installer script with --uninstall.") } - fmt.Println("Done — podcli app files were removed.") - if !purge { - fmt.Printf("Kept user data in: %s\n", home) - } + fmt.Println("Done - podcli app files were removed.") return 0 } -func uninstallTargets(home string, purge bool) []string { - if purge { - return []string{home} - } - return []string{paths.BinDir(), paths.RuntimeDir(), paths.ModelsDir(), filepath.Join(home, "tools")} +func uninstallTargets(home string) []string { + return []string{home} } func podcliLinks(managed, self string) []string { @@ -422,6 +420,26 @@ func podcliLinks(managed, self string) []string { return out } +func removeFromWindowsUserPath(remove string) (bool, error) { + ps := `$remove = [IO.Path]::GetFullPath($env:PODCLI_REMOVE_PATH).TrimEnd('\') +$path = [Environment]::GetEnvironmentVariable('Path', 'User') +$parts = @($path -split ';' | Where-Object { $_ }) +$kept = @($parts | Where-Object { + try { [IO.Path]::GetFullPath($_).TrimEnd('\') -ine $remove } catch { $_.TrimEnd('\') -ine $env:PODCLI_REMOVE_PATH.TrimEnd('\') } +}) +if ($kept.Count -eq $parts.Count) { exit 2 } +[Environment]::SetEnvironmentVariable('Path', ($kept -join ';'), 'User')` + cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps) + cmd.Env = append(os.Environ(), "PODCLI_REMOVE_PATH="+remove) + if err := cmd.Run(); err != nil { + if exit, ok := err.(*exec.ExitError); ok && exit.ExitCode() == 2 { + return false, nil + } + return false, err + } + return true, nil +} + func pathContains(dir, file string) bool { if file == "" { return false @@ -477,13 +495,12 @@ func confirm(prompt string) bool { func printUninstallHelp() { fmt.Println(`Usage: podcli uninstall [--yes] [--dry-run] [--purge] -Removes podcli's managed binary, runtimes, models, and installer-created links. -By default, user data (config, knowledge, presets, assets, history, cache) is kept. +Removes podcli's managed folder, user data, and installer-created links. Options: -y, --yes Do not prompt for confirmation --dry-run Show what would be removed without deleting anything - --purge Remove the entire podcli managed folder, including user data`) + --purge Kept for compatibility; uninstall already removes everything`) } func doctor() { diff --git a/cli/uninstall_test.go b/cli/uninstall_test.go index b989a21..1f6c6ad 100644 --- a/cli/uninstall_test.go +++ b/cli/uninstall_test.go @@ -30,17 +30,11 @@ func TestLinkPointsToAbsoluteAndRelativeSymlinks(t *testing.T) { } } -func TestUninstallTargetsPreserveUserDataUnlessPurged(t *testing.T) { +func TestUninstallTargetsRemoveAllByDefault(t *testing.T) { home := filepath.Join(t.TempDir(), "podcli") - got := uninstallTargets(home, false) - for _, p := range got { - if p == home { - t.Fatalf("non-purge uninstall should not remove the whole home: %v", got) - } - } - purged := uninstallTargets(home, true) - if len(purged) != 1 || purged[0] != home { - t.Fatalf("purge targets = %v, want only %s", purged, home) + got := uninstallTargets(home) + if len(got) != 1 || got[0] != home { + t.Fatalf("uninstall targets = %v, want only %s", got, home) } } diff --git a/install.ps1 b/install.ps1 index a9453ff..29fcdfb 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,8 +1,9 @@ -# podcli installer for Windows — downloads the prebuilt native binary (no Go, +# podcli installer for Windows - downloads the prebuilt native binary (no Go, # Node, Python, or ffmpeg needed; the binary provisions those on first run). # Usage: irm https://raw.githubusercontent.com/nmbrthirteen/podcli/main/install.ps1 | iex # Uninstall: & ([scriptblock]::Create((irm https://raw.githubusercontent.com/nmbrthirteen/podcli/main/install.ps1))) -Uninstall -param([switch]$Uninstall) +# Purge: & ([scriptblock]::Create((irm https://raw.githubusercontent.com/nmbrthirteen/podcli/main/install.ps1))) -Uninstall -Purge +param([switch]$Uninstall, [switch]$Purge) $ErrorActionPreference = 'Stop' $repo = 'nmbrthirteen/podcli' $target = 'windows-amd64' @@ -12,13 +13,18 @@ $binDir = Join-Path $homeDir 'bin' if ($Uninstall) { Write-Host "Uninstalling podcli..." - foreach ($p in @($binDir, (Join-Path $homeDir 'runtime'), (Join-Path $homeDir 'models'), (Join-Path $homeDir 'tools'))) { + if ($Purge) { + $targets = @($homeDir) + } else { + $targets = @($binDir, (Join-Path $homeDir 'runtime'), (Join-Path $homeDir 'models'), (Join-Path $homeDir 'tools')) + } + foreach ($p in $targets) { if (Test-Path $p) { try { Remove-Item $p -Recurse -Force -ErrorAction Stop Write-Host " removed: $p" } catch { - Write-Warning "could not remove $p`: $($_.Exception.Message)" + Write-Warning ("could not remove {0}: {1}" -f $p, $_.Exception.Message) } } } @@ -28,8 +34,12 @@ if ($Uninstall) { [Environment]::SetEnvironmentVariable('Path', ($parts -join ';'), 'User') Write-Host " removed from user PATH (restart your terminal)" } - Write-Host " kept user data (config, knowledge, presets, assets, history, cache)." - Write-Host " To remove everything: Remove-Item '$homeDir' -Recurse -Force" + if ($Purge) { + Write-Host " removed managed data." + } else { + Write-Host " kept user data (config, knowledge, presets, assets, history, cache)." + Write-Host " To remove everything: rerun with -Uninstall -Purge" + } exit 0 } @@ -43,7 +53,7 @@ if (-not $version) { $asset = "podcli-$target.exe" $base = "https://github.com/$repo/releases/download/v$version" -Write-Host "Installing podcli v$version ($target)…" +Write-Host "Installing podcli v$version ($target)..." $dest = Join-Path $binDir 'podcli.exe' Invoke-WebRequest "$base/$asset" -OutFile $dest -UseBasicParsing @@ -58,7 +68,7 @@ try { if ($got -ne $want.ToLower()) { Remove-Item $dest -Force; throw "checksum mismatch (got $got want $want)" } Write-Host " checksum verified" } else { - Write-Host " no checksum entry for $asset — skipped verification" + Write-Host " no checksum entry for $asset - skipped verification" } } catch { Write-Host " checksum verification skipped: $($_.Exception.Message)" @@ -66,8 +76,8 @@ try { $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if ($userPath -notlike "*$binDir*") { - [Environment]::SetEnvironmentVariable('Path', "$binDir;$userPath", 'User') + [Environment]::SetEnvironmentVariable('Path', ($binDir + ';' + $userPath), 'User') Write-Host " added to PATH (restart your terminal)" } Write-Host "" -Write-Host "Done — run: podcli" +Write-Host "Done - run: podcli" diff --git a/install.sh b/install.sh index 81c78ff..04a6676 100755 --- a/install.sh +++ b/install.sh @@ -108,7 +108,7 @@ done echo if [ -n "$linked" ]; then - echo "Done — run: podcli" +echo "Done - run: podcli" else echo "Done. Add podcli to your PATH:" echo " export PATH=\"$bin_dir:\$PATH\"" From 72e2f7e14af868b01d11a3454fd47d3b3fb9a541 Mon Sep 17 00:00:00 2001 From: Teethat Kamsai <79137211+teethatkamsai@users.noreply.github.com> Date: Sun, 5 Jul 2026 17:25:04 +0700 Subject: [PATCH 2/3] Quiet setup noise and fix installer output --- backend/cli.py | 4 ++-- backend/utils/proc.py | 15 ++++++++------- cli/internal/provision/provision.go | 22 ++++++++++----------- cli/internal/update/update.go | 8 ++++---- cli/main.go | 30 +++++++++++++++-------------- install.ps1 | 3 +++ install.sh | 10 +++++----- 7 files changed, 49 insertions(+), 43 deletions(-) diff --git a/backend/cli.py b/backend/cli.py index 8101da5..5c78d09 100644 --- a/backend/cli.py +++ b/backend/cli.py @@ -3094,8 +3094,8 @@ def cmd_info(args): │ ██╔══██╗██╔═══██╗██╔══██╗ │ │ ██████╔╝██║ ██║██║ ██║ │ │ ██╔═══╝ ██║ ██║██║ ██║ │ - │ ██║ ╚██████╔╝██████╔╝\033[0m\033[1m CLI\033[0m\033[38;2;212;135;74m │ - │ ╚═╝ ╚═════╝ ╚═════╝ │ + │ ██║ ╚██████╔╝██████╔╝\033[0m\033[1m CLI\033[0m\033[38;2;212;135;74m │ + │ ╚═╝ ╚═════╝ ╚═════╝ │ │ │ └─────────────────────────────────────┘\033[0m""" diff --git a/backend/utils/proc.py b/backend/utils/proc.py index e48b530..b09f300 100644 --- a/backend/utils/proc.py +++ b/backend/utils/proc.py @@ -78,15 +78,16 @@ def run( duration = time.monotonic() - t0 if result.returncode != 0: - log.warning( - "proc.fail tool=%s rc=%d duration=%.2fs stderr=%s", - tool, - result.returncode, - duration, - (result.stderr or "")[-400:].strip(), - ) if check: + log.warning( + "proc.fail tool=%s rc=%d duration=%.2fs stderr=%s", + tool, + result.returncode, + duration, + (result.stderr or "")[-400:].strip(), + ) raise ProcError(cmd, result.returncode, result.stderr or "", duration) + log.debug("proc.nonzero tool=%s rc=%d duration=%.2fs", tool, result.returncode, duration) else: log.debug("proc.ok tool=%s duration=%.2fs", tool, duration) return result diff --git a/cli/internal/provision/provision.go b/cli/internal/provision/provision.go index 999b2af..ec33d11 100644 --- a/cli/internal/provision/provision.go +++ b/cli/internal/provision/provision.go @@ -100,7 +100,7 @@ func fetch(url, dest, label string) error { break } lastErr = err - fmt.Fprintf(os.Stderr, "\n %s interrupted (attempt %d/%d): %v — resuming\n", label, attempt, maxAttempts, err) + fmt.Fprintf(os.Stderr, "\n %s interrupted (attempt %d/%d): %v - resuming\n", label, attempt, maxAttempts, err) time.Sleep(time.Duration(attempt) * time.Second) } if lastErr != nil { @@ -115,7 +115,7 @@ func download(url, dest, wantSHA, label string) error { return err } if wantSHA == "" { - fmt.Fprintf(os.Stderr, " (no pinned checksum for %s — skipped verification)\n", label) + fmt.Fprintf(os.Stderr, " (no pinned checksum for %s - skipped verification)\n", label) return nil } got, err := sha256file(dest) @@ -136,12 +136,12 @@ func download(url, dest, wantSHA, label string) error { func verifyDownload(archive, sumsURL, name string) error { resp, err := downloadHTTPClient().Get(sumsURL) if err != nil { - fmt.Fprintf(os.Stderr, " (could not fetch checksums for %s — skipped verification)\n", name) + fmt.Fprintf(os.Stderr, " (could not fetch checksums for %s - skipped verification)\n", name) return nil } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - fmt.Fprintf(os.Stderr, " (no checksums for %s — skipped verification)\n", name) + fmt.Fprintf(os.Stderr, " (no checksums for %s - skipped verification)\n", name) return nil } data, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) @@ -150,7 +150,7 @@ func verifyDownload(archive, sumsURL, name string) error { } want := ParseChecksums(data)[name] if want == "" { - fmt.Fprintf(os.Stderr, " (no checksum entry for %s — skipped verification)\n", name) + fmt.Fprintf(os.Stderr, " (no checksum entry for %s - skipped verification)\n", name) return nil } got, err := sha256file(archive) @@ -408,17 +408,17 @@ func httpGetBytes(url string) ([]byte, error) { func verifyReleaseAsset(assets map[string]string, assetName, path string) error { sumsURL, ok := assets["checksums.txt"] if !ok { - fmt.Fprintf(os.Stderr, " (no checksums.txt in release — skipped verification of %s)\n", assetName) + fmt.Fprintf(os.Stderr, " (no checksums.txt in release - skipped verification of %s)\n", assetName) return nil } data, err := httpGetBytes(sumsURL) if err != nil { - fmt.Fprintf(os.Stderr, " (could not fetch checksums.txt: %v — skipped verification of %s)\n", err, assetName) + fmt.Fprintf(os.Stderr, " (could not fetch checksums.txt: %v - skipped verification of %s)\n", err, assetName) return nil } want, ok := ParseChecksums(data)[assetName] if !ok { - fmt.Fprintf(os.Stderr, " (no checksum entry for %s — skipped verification)\n", assetName) + fmt.Fprintf(os.Stderr, " (no checksum entry for %s - skipped verification)\n", assetName) return nil } got, err := sha256file(path) @@ -719,7 +719,7 @@ func EnsurePython(requirements string) (string, error) { } func pipInstall(pybin, requirements string) error { - fmt.Fprintf(os.Stderr, " installing python deps (%s) — pulls ~80MB, first run takes a minute\n", filepath.Base(requirements)) + fmt.Fprintf(os.Stderr, " installing python deps (%s) - pulls ~80MB, first run takes a minute\n", filepath.Base(requirements)) cmd := exec.Command(pybin, "-m", "pip", "install", "--disable-pip-version-check", "--progress-bar=on", "-r", requirements) cmd.Stdout, cmd.Stderr = os.Stderr, os.Stderr cmd.Env = append(os.Environ(), "PYTHONUNBUFFERED=1") @@ -732,9 +732,9 @@ func pipInstall(pybin, requirements string) error { func EnsureSpeakerDeps() error { bin := PythonBin() if !have(bin) { - return fmt.Errorf("python not provisioned — run `podcli setup` first") + return fmt.Errorf("python not provisioned - run `podcli setup` first") } - fmt.Fprintln(os.Stderr, " installing speaker deps (pyannote.audio + torch) — large download (~2GB), several minutes") + fmt.Fprintln(os.Stderr, " installing speaker deps (pyannote.audio + torch) - large download (~2GB), several minutes") cmd := exec.Command(bin, "-m", "pip", "install", "--disable-pip-version-check", "--progress-bar=on", "pyannote.audio>=3.1.0", "speechbrain") cmd.Stdout, cmd.Stderr = os.Stderr, os.Stderr cmd.Env = append(os.Environ(), "PYTHONUNBUFFERED=1") diff --git a/cli/internal/update/update.go b/cli/internal/update/update.go index b190c81..07d0ec8 100644 --- a/cli/internal/update/update.go +++ b/cli/internal/update/update.go @@ -143,7 +143,7 @@ func NotifyIfOutdated(current string) { return } if newer(tag, current) { - fmt.Fprintf(os.Stderr, " ↑ podcli %s available (you have %s) — run `podcli update`\n", tag, current) + fmt.Fprintf(os.Stderr, " podcli %s available (you have %s) - run `podcli update`\n", tag, current) } } @@ -157,7 +157,7 @@ func Run(current string) int { fmt.Printf("podcli %s is up to date.\n", current) return 0 } - fmt.Printf("Updating podcli %s → %s ...\n", current, tag) + fmt.Printf("Updating podcli %s -> %s ...\n", current, tag) if err := apply(tag); err != nil { printSelfUpdateFailure(os.Stderr, err) return 1 @@ -222,7 +222,7 @@ func verifyStaged(tag, staged string) error { } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { - fmt.Fprintln(os.Stderr, " (no checksums.txt in release — skipped verification)") + fmt.Fprintln(os.Stderr, " (no checksums.txt in release - skipped verification)") return nil } if resp.StatusCode != http.StatusOK { @@ -234,7 +234,7 @@ func verifyStaged(tag, staged string) error { } want, ok := provision.ParseChecksums(data)[assetName()] if !ok { - fmt.Fprintf(os.Stderr, " (no checksum entry for %s — skipped verification)\n", assetName()) + fmt.Fprintf(os.Stderr, " (no checksum entry for %s - skipped verification)\n", assetName()) return nil } got, err := provision.Sha256File(staged) diff --git a/cli/main.go b/cli/main.go index 0f1f180..c72fdb2 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,4 +1,4 @@ -// podcli — native launcher. Reserved verbs are handled here; everything else +// podcli - native launcher. Reserved verbs are handled here; everything else // routes to the Python engine. package main @@ -52,7 +52,7 @@ func main() { if len(args) >= 2 && (args[1] == "get" || args[1] == "set") { os.Exit(configCmd(args[1:])) } - os.Exit(runEngine(args)) // status/export/import/use → Python + os.Exit(runEngine(args)) // status/export/import/use to Python case "help", "--help", "-h": printHelp() default: @@ -82,7 +82,7 @@ func ensureRuntime() { if _, ok := engine.BackendRoot(); ok { return } - fmt.Fprintln(os.Stderr, "First run — setting up podcli (one-time download)…") + fmt.Fprintln(os.Stderr, "First run - setting up podcli (one-time download)...") setup(nil) } @@ -204,13 +204,13 @@ func setup(args []string) int { fmt.Printf(" vad: %s\n", vp) } if fp, err := provision.EnsureFFmpeg(); err != nil { - fmt.Fprintf(os.Stderr, " ffmpeg: skipped (%v) — backend will use PATH ffmpeg\n", err) + fmt.Fprintf(os.Stderr, " ffmpeg: skipped (%v) - backend will use PATH ffmpeg\n", err) } else { fmt.Printf(" ffmpeg: %s\n", fp) } backendDir := filepath.Join(paths.RuntimeDir(), "backend") if err := backend.Extract(backendDir); err != nil { - fmt.Fprintf(os.Stderr, " backend: skipped (%v) — falling back to repo/PODCLI_BACKEND\n", err) + fmt.Fprintf(os.Stderr, " backend: skipped (%v) - falling back to repo/PODCLI_BACKEND\n", err) backendDir, _ = engine.BackendRoot() } else { fmt.Printf(" backend: %s\n", backendDir) @@ -218,7 +218,7 @@ func setup(args []string) int { if backendDir != "" { reqs := filepath.Join(backendDir, "requirements-runtime.txt") if pb, err := provision.EnsurePython(reqs); err != nil { - fmt.Fprintf(os.Stderr, " python: skipped (%v) — using dev venv / system python\n", err) + fmt.Fprintf(os.Stderr, " python: skipped (%v) - using dev venv / system python\n", err) } else { fmt.Printf(" python: %s\n", pb) } @@ -231,22 +231,22 @@ func setup(args []string) int { fmt.Printf(" speakers: pyannote.audio installed (set HF_TOKEN to use)\n") } if wc, err := provision.EnsureWhisperCpp(); err != nil { - fmt.Fprintf(os.Stderr, " whisper: skipped (%v) — backend will use PATH whisper-cli\n", err) + fmt.Fprintf(os.Stderr, " whisper: skipped (%v) - backend will use PATH whisper-cli\n", err) } else { fmt.Printf(" whisper: %s\n", wc) } if nb, err := provision.EnsureNode(); err != nil { - fmt.Fprintf(os.Stderr, " node: skipped (%v) — Web UI will use system Node if present\n", err) + fmt.Fprintf(os.Stderr, " node: skipped (%v) - Web UI will use system Node if present\n", err) } else { fmt.Printf(" node: %s\n", nb) } if sd, err := provision.EnsureStudio(); err != nil { - fmt.Fprintf(os.Stderr, " studio: skipped (%v) — Web UI needs a published release\n", err) + fmt.Fprintf(os.Stderr, " studio: skipped (%v) - Web UI needs a published release\n", err) } else { fmt.Printf(" studio: %s\n", sd) } if rd, err := provision.EnsureRemotion(); err != nil { - fmt.Fprintf(os.Stderr, " remotion: skipped (%v) — captions/thumbnails need a published release\n", err) + fmt.Fprintf(os.Stderr, " remotion: skipped (%v) - captions/thumbnails need a published release\n", err) } else { fmt.Printf(" remotion: %s\n", rd) if err := provision.PrewarmRemotion(); err != nil { @@ -263,8 +263,10 @@ func setup(args []string) int { if engine.MCPServer() != "" { if mcpRegisteredToSelf() { fmt.Printf(" mcp: already registered\n") + } else if _, err := exec.LookPath("claude"); err != nil { + // Claude MCP registration is optional; Codex users do not need this. } else if err := registerMCPServer(); err != nil { - fmt.Fprintf(os.Stderr, " mcp: not registered (%v) — run `podcli mcp install`\n", err) + fmt.Fprintf(os.Stderr, " mcp: not registered (%v) - run `podcli mcp install`\n", err) } else { fmt.Printf(" mcp: registered with Claude Code\n") } @@ -509,7 +511,7 @@ func doctor() { fmt.Printf(" home: %s\n", paths.Home()) fmt.Printf(" runtime: %s\n", paths.RuntimeDir()) fmt.Printf(" models: %s\n", paths.ModelsDir()) - fmt.Printf(" presets/knowledge/assets/history/cache: %s (global — follow you everywhere)\n", paths.Home()) + fmt.Printf(" presets/knowledge/assets/history/cache: %s (global - follow you everywhere)\n", paths.Home()) if cwd, err := os.Getwd(); err == nil { fmt.Printf(" clips: %s (rendered into your working directory)\n", filepath.Join(cwd, "podcli-clips")) } @@ -562,7 +564,7 @@ func presence(p string) string { if fi, err := os.Stat(p); err == nil && fi.Size() > 0 { return fmt.Sprintf("%s (%s)", p, humanBytes(fi.Size())) } - return "not provisioned — run `podcli setup`" + return "not provisioned - run `podcli setup`" } func fileExists(p string) bool { @@ -582,7 +584,7 @@ func humanBytes(n int64) string { } func printHelp() { - fmt.Printf(`podcli %s — AI podcast clip generator + fmt.Printf(`podcli %s - AI podcast clip generator Usage: podcli [args] diff --git a/install.ps1 b/install.ps1 index 29fcdfb..16e3b5f 100644 --- a/install.ps1 +++ b/install.ps1 @@ -60,6 +60,9 @@ Invoke-WebRequest "$base/$asset" -OutFile $dest -UseBasicParsing try { $sums = (Invoke-WebRequest "$base/checksums.txt" -UseBasicParsing).Content + if ($sums -is [byte[]]) { + $sums = [System.Text.Encoding]::UTF8.GetString($sums) + } $want = $sums -split "`n" | Where-Object { $_ -match ([regex]::Escape($asset) + '\s*$') } | ForEach-Object { ($_ -split '\s+')[0] } | Select-Object -First 1 diff --git a/install.sh b/install.sh index 04a6676..e0e0009 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/bin/sh -# podcli installer — downloads the prebuilt native binary (no Go, Node, Python, +# podcli installer - downloads the prebuilt native binary (no Go, Node, Python, # or ffmpeg needed; the binary provisions those itself on first run). # Usage: curl -fsSL https://raw.githubusercontent.com/nmbrthirteen/podcli/main/install.sh | sh # Uninstall: curl -fsSL https://raw.githubusercontent.com/nmbrthirteen/podcli/main/install.sh | sh -s -- --uninstall @@ -19,7 +19,7 @@ esac bin_dir="$home_dir/bin" if [ "${1:-}" = "--uninstall" ]; then - echo "Uninstalling podcli…" + echo "Uninstalling podcli..." for d in /usr/local/bin "$HOME/.local/bin"; do link="$d/podcli" if [ -L "$link" ] && [ "$(readlink "$link")" = "$bin_dir/podcli" ]; then @@ -65,7 +65,7 @@ fi asset="podcli-${target}" base="https://github.com/$REPO/releases/download/v${version}" -echo "Installing podcli v${version} (${target})…" +echo "Installing podcli v${version} (${target})..." tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT @@ -82,10 +82,10 @@ if curl -fsSL "$base/checksums.txt" -o "$tmp/sums" 2>/dev/null; then [ "$got" = "$want" ] || err "checksum mismatch (got $got want $want)" echo " checksum verified" else - echo " no checksum entry for $asset — skipped verification" >&2 + echo " no checksum entry for $asset - skipped verification" >&2 fi else - echo " no checksums.txt in release — skipped verification" >&2 + echo " no checksums.txt in release - skipped verification" >&2 fi cp "$tmp/$asset" "$bin_dir/podcli" From 995942545b5de2b431e7f0999c798a678a515426 Mon Sep 17 00:00:00 2001 From: Teethat Kamsai <79137211+teethatkamsai@users.noreply.github.com> Date: Sun, 5 Jul 2026 18:36:43 +0700 Subject: [PATCH 3/3] Preserve expandable user PATH entries --- cli/main.go | 9 ++++++--- install.ps1 | 45 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/cli/main.go b/cli/main.go index c72fdb2..50b6d7f 100644 --- a/cli/main.go +++ b/cli/main.go @@ -424,13 +424,16 @@ func podcliLinks(managed, self string) []string { func removeFromWindowsUserPath(remove string) (bool, error) { ps := `$remove = [IO.Path]::GetFullPath($env:PODCLI_REMOVE_PATH).TrimEnd('\') -$path = [Environment]::GetEnvironmentVariable('Path', 'User') +$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true) +if (-not $key) { exit 2 } +try { $kind = $key.GetValueKind('Path') } catch { $kind = [Microsoft.Win32.RegistryValueKind]::ExpandString } +$path = [string]$key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) $parts = @($path -split ';' | Where-Object { $_ }) $kept = @($parts | Where-Object { - try { [IO.Path]::GetFullPath($_).TrimEnd('\') -ine $remove } catch { $_.TrimEnd('\') -ine $env:PODCLI_REMOVE_PATH.TrimEnd('\') } + try { [IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($_)).TrimEnd('\') -ine $remove } catch { $_.TrimEnd('\') -ine $env:PODCLI_REMOVE_PATH.TrimEnd('\') } }) if ($kept.Count -eq $parts.Count) { exit 2 } -[Environment]::SetEnvironmentVariable('Path', ($kept -join ';'), 'User')` +$key.SetValue('Path', ($kept -join ';'), $kind)` cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps) cmd.Env = append(os.Environ(), "PODCLI_REMOVE_PATH="+remove) if err := cmd.Run(); err != nil { diff --git a/install.ps1 b/install.ps1 index 16e3b5f..a3a042e 100644 --- a/install.ps1 +++ b/install.ps1 @@ -11,6 +11,27 @@ $target = 'windows-amd64' $homeDir = Join-Path $env:LOCALAPPDATA 'podcli' $binDir = Join-Path $homeDir 'bin' +function Get-UserPathEntry { + $key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment') + if (-not $key) { return $null } + $kind = [Microsoft.Win32.RegistryValueKind]::ExpandString + try { $kind = $key.GetValueKind('Path') } catch {} + [pscustomobject]@{ + Key = $key + Kind = $kind + Value = [string]$key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) + } +} + +function Test-PathEntryEquals { + param([string]$Entry, [string]$Target) + try { + return [IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($Entry)).TrimEnd('\') -ieq [IO.Path]::GetFullPath($Target).TrimEnd('\') + } catch { + return $Entry.TrimEnd('\') -ieq $Target.TrimEnd('\') + } +} + if ($Uninstall) { Write-Host "Uninstalling podcli..." if ($Purge) { @@ -28,11 +49,14 @@ if ($Uninstall) { } } } - $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') - if ($userPath -like "*$binDir*") { - $parts = $userPath -split ';' | Where-Object { $_ -and ($_ -ne $binDir) } - [Environment]::SetEnvironmentVariable('Path', ($parts -join ';'), 'User') - Write-Host " removed from user PATH (restart your terminal)" + $pathEntry = Get-UserPathEntry + if ($pathEntry) { + $parts = @($pathEntry.Value -split ';' | Where-Object { $_ }) + $kept = @($parts | Where-Object { -not (Test-PathEntryEquals $_ $binDir) }) + if ($kept.Count -ne $parts.Count) { + $pathEntry.Key.SetValue('Path', ($kept -join ';'), $pathEntry.Kind) + Write-Host " removed from user PATH (restart your terminal)" + } } if ($Purge) { Write-Host " removed managed data." @@ -77,10 +101,13 @@ try { Write-Host " checksum verification skipped: $($_.Exception.Message)" } -$userPath = [Environment]::GetEnvironmentVariable('Path', 'User') -if ($userPath -notlike "*$binDir*") { - [Environment]::SetEnvironmentVariable('Path', ($binDir + ';' + $userPath), 'User') - Write-Host " added to PATH (restart your terminal)" +$pathEntry = Get-UserPathEntry +if ($pathEntry) { + $parts = @($pathEntry.Value -split ';' | Where-Object { $_ }) + if (-not ($parts | Where-Object { Test-PathEntryEquals $_ $binDir })) { + $pathEntry.Key.SetValue('Path', ($binDir + ';' + $pathEntry.Value), $pathEntry.Kind) + Write-Host " added to PATH (restart your terminal)" + } } Write-Host "" Write-Host "Done - run: podcli"