alishahryar1--free-claude-code
d98a6b0ca0
## Problem Free Claude Code currently has to remain attached to a terminal, so closing that window stops the proxy and users have no native way to reopen Admin or control the background server. The contributed Windows wrapper also would have introduced a second bundled server lifecycle instead of reusing FCC's cleanup and restart ownership. Fixes #1147. ## Changes | Before | After | | --- | --- | | Users keep `fcc-server` running in a terminal. | Windows and macOS users can launch a console-free FCC Desktop host from a desktop/application shortcut and control it from the tray or menu bar. | | A wrapper would need to spawn and terminate a child server. | The terminal and desktop paths share one in-process supervisor, one graceful runtime shutdown path, and an OS-held singleton lock. | | Installers manage only command entry points. | Windows installs desktop and Start-menu shortcuts; macOS installs a per-user app bundle and owned desktop link; uninstallers remove only those FCC artifacts. | | Desktop behavior had no contract coverage. | Lifecycle, duplicate launch, restart/quit, GUI packaging, Windows shortcuts, macOS bundle creation, quoting, and ownership boundaries are covered alongside the full CI suite. | <!-- greptile_comment --> <details open><summary><h3>Greptile Summary</h3></summary> This PR adds a native FCC desktop launcher for Windows and macOS. The main changes are: - A shared server supervisor for terminal and desktop launches. - A singleton desktop host with tray or menu-bar controls. - Windows shortcuts and a per-user macOS app bundle. - Ownership checks for launcher installation and removal. - Tests for lifecycle, packaging, shortcuts, and uninstall behavior. </details> <h3>Confidence Score: 5/5</h3> This looks safe to merge. Startup restart requests are reserved before the worker starts. macOS bundle operations verify ownership before modifying or deleting files. Windows shortcut operations verify their targets before replacement or removal. No blocking issues were found in the updated code. <details><summary><h3><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="20" align="absmiddle"></a> T-Rex Logs</h3></summary> **What T-Rex did** - Compared the pre-change contract test results against the parent commit f81af55630aa1adb518b748b9e6985c73c4c4775 and observed 3 failures and 4 passes, indicating the missing scheduled-startup lifecycle contract. - Executed the after-state contract validation with uv run pytest -n 0 tests/cli/test\_desktop.py -q and confirmed the run finished with 7 passes and an exit code of 0. - Verified that no real proxy or native GUI dependency was started during the after-state run. - Inspected the two log artifacts that accompany the proof to corroborate the test outcomes. <a href="https://app.greptile.com/trex/runs/15235588/artifacts"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=4"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"><img alt="View all artifacts" src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"></picture></a> <sub><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="14" align="absmiddle"></a> Ran code and verified through T-Rex</sub> </details> <details open><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | src/free_claude_code/cli/commands.py | Adds the shared server supervisor and scheduled-run state for startup restart requests. | | src/free_claude_code/cli/desktop.py | Adds singleton locking and coordinates the tray loop with the server worker. | | scripts/install.sh | Creates the macOS app bundle only when an existing bundle is FCC-owned. | | scripts/uninstall.sh | Removes the macOS launcher only on macOS and only with the expected ownership marker. | | scripts/install.ps1 | Creates Windows shortcuts while preserving shortcuts with unrelated targets. | | scripts/uninstall.ps1 | Removes Windows shortcuts only when their targets match an FCC desktop entry point. | </details> <sub>Reviews (3): Last reviewed commit: ["fix: coalesce desktop startup restarts"](https://github.com/alishahryar1/free-claude-code/commit/b9554729770e08a58818d677de39757c751e760e) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=45925660)</sub> <!-- /greptile_comment -->
338 行
9.4 KiB
PowerShell
338 行
9.4 KiB
PowerShell
param(
|
|
[switch] $DryRun,
|
|
[switch] $Help,
|
|
[Parameter(ValueFromRemainingArguments = $true)]
|
|
[object[]] $RemainingArgs = @()
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$PackageName = "free-claude-code"
|
|
$FccHomeDirname = ".fcc"
|
|
$FccCommands = @(
|
|
# Include retired entry points so older installations are fully stopped and removed.
|
|
"fcc-desktop",
|
|
"fcc-server",
|
|
"fcc-claude",
|
|
"fcc-codex",
|
|
"fcc-pi",
|
|
"fcc-init",
|
|
"free-claude-code"
|
|
)
|
|
$script:UvPath = ""
|
|
$script:UvToolBin = ""
|
|
|
|
function Show-Usage {
|
|
@"
|
|
Usage: uninstall.ps1 [options]
|
|
|
|
Removes the Free Claude Code uv tool and deletes ~/.fcc/ after removal is verified.
|
|
Does not remove uv, Claude Code, Codex, Pi, the uv-managed Python runtime, or shared PATH entries.
|
|
|
|
Options:
|
|
-DryRun Print commands without running them.
|
|
-Help Show this help text.
|
|
"@
|
|
}
|
|
|
|
function Write-Step {
|
|
param([string] $Message)
|
|
|
|
Write-Host ""
|
|
Write-Host "==> $Message"
|
|
}
|
|
|
|
function Format-Argument {
|
|
param([string] $Value)
|
|
|
|
if ($Value -match '^[A-Za-z0-9_./:@%+=,\[\]\\-]+$') {
|
|
return $Value
|
|
}
|
|
return "'" + ($Value -replace "'", "''") + "'"
|
|
}
|
|
|
|
function Format-Command {
|
|
param(
|
|
[string] $FilePath,
|
|
[string[]] $Arguments = @()
|
|
)
|
|
|
|
$parts = @($FilePath) + $Arguments
|
|
return ($parts | ForEach-Object { Format-Argument ([string] $_) }) -join " "
|
|
}
|
|
|
|
function Get-ApplicationCommand {
|
|
param([string] $Name)
|
|
|
|
$commands = @(Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue)
|
|
if ($commands.Count -eq 0) {
|
|
return $null
|
|
}
|
|
return $commands[0]
|
|
}
|
|
|
|
function Invoke-NativeResult {
|
|
param(
|
|
[string] $FilePath,
|
|
[string[]] $Arguments
|
|
)
|
|
|
|
$previousErrorActionPreference = $ErrorActionPreference
|
|
$ErrorActionPreference = "Continue"
|
|
try {
|
|
$global:LASTEXITCODE = 0
|
|
$output = (& $FilePath @Arguments 2>&1 | Out-String).Trim()
|
|
return [pscustomobject] @{
|
|
ExitCode = $LASTEXITCODE
|
|
Output = $output
|
|
}
|
|
}
|
|
finally {
|
|
$ErrorActionPreference = $previousErrorActionPreference
|
|
}
|
|
}
|
|
|
|
function Test-MissingUvToolError {
|
|
param([string] $Output)
|
|
|
|
$normalized = $Output.ToLowerInvariant()
|
|
return $normalized.Contains($PackageName) -and $normalized.Contains("is not installed")
|
|
}
|
|
|
|
function Add-PathEntry {
|
|
param([string] $PathEntry)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($PathEntry)) {
|
|
return
|
|
}
|
|
$separator = [IO.Path]::PathSeparator
|
|
$entries = @()
|
|
if (-not [string]::IsNullOrEmpty($env:Path)) {
|
|
$entries = $env:Path -split [regex]::Escape([string] $separator)
|
|
}
|
|
if ($entries -notcontains $PathEntry) {
|
|
$env:Path = "$PathEntry$separator$env:Path"
|
|
}
|
|
}
|
|
|
|
function Add-KnownUvPaths {
|
|
Add-PathEntry (Join-Path $env:USERPROFILE ".local\bin")
|
|
Add-PathEntry (Join-Path $env:USERPROFILE ".cargo\bin")
|
|
}
|
|
|
|
function Assert-NoFccProcessesRunning {
|
|
$running = @()
|
|
foreach ($commandName in $FccCommands) {
|
|
$processes = @(Get-Process -Name $commandName -ErrorAction SilentlyContinue)
|
|
if ($processes.Count -gt 0) {
|
|
$running += $commandName
|
|
}
|
|
}
|
|
if ($running.Count -gt 0) {
|
|
throw "Free Claude Code is still running ($($running -join ', ')). Stop those processes, then rerun uninstall."
|
|
}
|
|
}
|
|
|
|
function Initialize-UvContext {
|
|
Add-KnownUvPaths
|
|
|
|
if ($DryRun) {
|
|
Write-Host "+ uv tool dir --bin"
|
|
return
|
|
}
|
|
|
|
$uvCommand = Get-ApplicationCommand "uv"
|
|
if (-not $uvCommand) {
|
|
throw "uv is required to remove the Free Claude Code tool. Install uv, then rerun this uninstaller; ~/.fcc was not deleted."
|
|
}
|
|
$script:UvPath = $uvCommand.Source
|
|
|
|
$commandText = Format-Command -FilePath $script:UvPath -Arguments @("tool", "dir", "--bin")
|
|
Write-Host "+ $commandText"
|
|
$result = Invoke-NativeResult -FilePath $script:UvPath -Arguments @("tool", "dir", "--bin")
|
|
if ($result.ExitCode -ne 0) {
|
|
if (-not [string]::IsNullOrWhiteSpace($result.Output)) {
|
|
[Console]::Error.WriteLine($result.Output)
|
|
}
|
|
throw "Could not determine the uv tool bin directory (exit code $($result.ExitCode)); ~/.fcc was not deleted."
|
|
}
|
|
$script:UvToolBin = $result.Output.Trim()
|
|
if ([string]::IsNullOrWhiteSpace($script:UvToolBin)) {
|
|
throw "uv returned an empty tool bin directory; ~/.fcc was not deleted."
|
|
}
|
|
}
|
|
|
|
function Uninstall-FreeClaudeCode {
|
|
Write-Host "+ uv tool uninstall $PackageName"
|
|
if ($DryRun) {
|
|
return
|
|
}
|
|
|
|
$result = Invoke-NativeResult -FilePath $script:UvPath -Arguments @(
|
|
"tool",
|
|
"uninstall",
|
|
$PackageName
|
|
)
|
|
if ($result.ExitCode -eq 0) {
|
|
if (-not [string]::IsNullOrWhiteSpace($result.Output)) {
|
|
Write-Host $result.Output
|
|
}
|
|
return
|
|
}
|
|
if (Test-MissingUvToolError -Output $result.Output) {
|
|
Write-Host "Free Claude Code uv tool is already absent; verifying its entry points."
|
|
return
|
|
}
|
|
if (-not [string]::IsNullOrWhiteSpace($result.Output)) {
|
|
[Console]::Error.WriteLine($result.Output)
|
|
}
|
|
throw "uv tool uninstall $PackageName failed with exit code $($result.ExitCode); ~/.fcc was not deleted."
|
|
}
|
|
|
|
function Confirm-FccCommandsRemoved {
|
|
if ($DryRun) {
|
|
Write-Host "+ verify all Free Claude Code entry points are absent from the uv tool bin directory"
|
|
return
|
|
}
|
|
|
|
$remaining = @()
|
|
$extensions = @("", ".exe", ".cmd", ".bat", ".ps1")
|
|
foreach ($commandName in $FccCommands) {
|
|
foreach ($extension in $extensions) {
|
|
$commandPath = Join-Path $script:UvToolBin "$commandName$extension"
|
|
if (Test-Path -LiteralPath $commandPath) {
|
|
$remaining += $commandPath
|
|
}
|
|
}
|
|
}
|
|
if ($remaining.Count -gt 0) {
|
|
throw "Free Claude Code entry points remain after uv uninstall: $($remaining -join ', '); ~/.fcc was not deleted."
|
|
}
|
|
}
|
|
|
|
function Test-EquivalentPath {
|
|
param(
|
|
[string] $Left,
|
|
[string] $Right
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Left) -or [string]::IsNullOrWhiteSpace($Right)) {
|
|
return $false
|
|
}
|
|
try {
|
|
return [string]::Equals(
|
|
[IO.Path]::GetFullPath($Left),
|
|
[IO.Path]::GetFullPath($Right),
|
|
[StringComparison]::OrdinalIgnoreCase
|
|
)
|
|
}
|
|
catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Test-FccDesktopShortcutTarget {
|
|
param([string] $TargetPath)
|
|
|
|
foreach ($extension in @("", ".exe", ".cmd", ".bat", ".ps1")) {
|
|
$expectedTarget = Join-Path $script:UvToolBin "fcc-desktop$extension"
|
|
if (Test-EquivalentPath -Left $TargetPath -Right $expectedTarget) {
|
|
return $true
|
|
}
|
|
}
|
|
return $false
|
|
}
|
|
|
|
function Remove-FccDesktopShortcuts {
|
|
$shortcutPaths = @(
|
|
(Join-Path $env:USERPROFILE "Desktop\Free Claude Code.lnk"),
|
|
(Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Free Claude Code.lnk")
|
|
)
|
|
$shell = New-Object -ComObject WScript.Shell
|
|
foreach ($shortcutPath in $shortcutPaths) {
|
|
if (-not (Test-Path -LiteralPath $shortcutPath)) {
|
|
continue
|
|
}
|
|
try {
|
|
$shortcut = $shell.CreateShortcut($shortcutPath)
|
|
$isFccShortcut = Test-FccDesktopShortcutTarget -TargetPath $shortcut.TargetPath
|
|
}
|
|
catch {
|
|
$isFccShortcut = $false
|
|
}
|
|
if (-not $isFccShortcut) {
|
|
Write-Host "A shortcut not managed by Free Claude Code exists at $shortcutPath; leaving it unchanged."
|
|
continue
|
|
}
|
|
Write-Host "+ Remove-Item -LiteralPath $(Format-Argument $shortcutPath) -Force"
|
|
if (-not $DryRun) {
|
|
Remove-Item -LiteralPath $shortcutPath -Force
|
|
}
|
|
}
|
|
}
|
|
|
|
function Purge-FccHome {
|
|
$fccHome = Join-Path $env:USERPROFILE $FccHomeDirname
|
|
if (-not (Test-Path -LiteralPath $fccHome)) {
|
|
Write-Host "No FCC config directory at $fccHome; skipping purge."
|
|
return
|
|
}
|
|
|
|
$commandText = @(
|
|
"Remove-Item",
|
|
"-LiteralPath",
|
|
(Format-Argument $fccHome),
|
|
"-Recurse",
|
|
"-Force"
|
|
) -join " "
|
|
Write-Host "+ $commandText"
|
|
if ($DryRun) {
|
|
return
|
|
}
|
|
|
|
Remove-Item -LiteralPath $fccHome -Recurse -Force
|
|
if (Test-Path -LiteralPath $fccHome) {
|
|
throw "FCC config directory still exists after deletion: $fccHome"
|
|
}
|
|
}
|
|
|
|
if ($Help) {
|
|
Show-Usage
|
|
return
|
|
}
|
|
if ($RemainingArgs.Count -gt 0) {
|
|
Show-Usage
|
|
throw "Unknown option: $($RemainingArgs -join ' ')"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($env:USERPROFILE)) {
|
|
throw "USERPROFILE is not set; cannot locate Free Claude Code data."
|
|
}
|
|
|
|
Write-Step "Checking for running Free Claude Code processes"
|
|
Assert-NoFccProcessesRunning
|
|
|
|
Write-Step "Locating the uv-managed Free Claude Code installation"
|
|
Initialize-UvContext
|
|
|
|
Write-Step "Removing the Free Claude Code uv tool"
|
|
Uninstall-FreeClaudeCode
|
|
|
|
Write-Step "Verifying Free Claude Code entry points were removed"
|
|
Confirm-FccCommandsRemoved
|
|
|
|
Write-Step "Removing Free Claude Code desktop shortcuts"
|
|
Remove-FccDesktopShortcuts
|
|
|
|
Write-Step "Purging FCC config and data from ~/.fcc"
|
|
Purge-FccHome
|
|
|
|
Write-Host ""
|
|
if ($DryRun) {
|
|
Write-Host "Dry run complete. No changes were made."
|
|
}
|
|
else {
|
|
Write-Host "Free Claude Code has been removed and verified."
|
|
Write-Host "uv, Claude Code, Codex, Pi, the uv-managed Python runtime, and shared PATH entries were left installed."
|
|
}
|