Skip to content

Commit da74420

Browse files
authored
fix(update): give script-installed users a copy-chip update command (#672)
1 parent 17b4b04 commit da74420

3 files changed

Lines changed: 69 additions & 28 deletions

File tree

internal/version/version.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,13 +278,31 @@ func detectInstallMethodFromPath(exe string) InstallMethod {
278278
// getUpdateCommand returns the command to update based on install method.
279279
// Desktop returns empty since updates are handled in-app.
280280
func getUpdateCommand(method InstallMethod) string {
281+
return getUpdateCommandForOS(method, runtime.GOOS)
282+
}
283+
284+
// getUpdateCommandForOS returns the update command for a given install method
285+
// and OS. For InstallDirect, the install one-liner is idempotent — re-running
286+
// it upgrades an existing binary in place. Returns "" for any GOOS that the
287+
// public install script doesn't support, so the frontend falls through to the
288+
// GitHub release-download link.
289+
func getUpdateCommandForOS(method InstallMethod, goos string) string {
281290
switch method {
282291
case InstallHomebrew:
283292
return "brew upgrade skyhook-io/tap/radar"
284293
case InstallKrew:
285294
return "kubectl krew upgrade radar"
286295
case InstallScoop:
287296
return "scoop update radar"
297+
case InstallDirect:
298+
switch goos {
299+
case "darwin", "linux":
300+
return "curl -fsSL https://get.radarhq.io | sh"
301+
case "windows":
302+
return "irm https://get.radarhq.io/install.ps1 | iex"
303+
default:
304+
return ""
305+
}
288306
case InstallDesktop:
289307
return "" // in-app update, no CLI command
290308
default:

internal/version/version_test.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,28 @@ func TestIsNewerVersion(t *testing.T) {
4343

4444
func TestGetUpdateCommand(t *testing.T) {
4545
tests := []struct {
46+
name string
4647
method InstallMethod
48+
goos string
4749
want string
4850
}{
49-
{InstallHomebrew, "brew upgrade skyhook-io/tap/radar"},
50-
{InstallKrew, "kubectl krew upgrade radar"},
51-
{InstallScoop, "scoop update radar"},
52-
{InstallDirect, ""},
53-
{InstallDesktop, ""},
54-
{InstallMethod("unknown"), ""},
51+
{"homebrew", InstallHomebrew, "darwin", "brew upgrade skyhook-io/tap/radar"},
52+
{"krew", InstallKrew, "linux", "kubectl krew upgrade radar"},
53+
{"scoop", InstallScoop, "windows", "scoop update radar"},
54+
{"direct linux", InstallDirect, "linux", "curl -fsSL https://get.radarhq.io | sh"},
55+
{"direct darwin", InstallDirect, "darwin", "curl -fsSL https://get.radarhq.io | sh"},
56+
{"direct windows", InstallDirect, "windows", "irm https://get.radarhq.io/install.ps1 | iex"},
57+
{"direct freebsd falls through", InstallDirect, "freebsd", ""},
58+
{"direct empty goos falls through", InstallDirect, "", ""},
59+
{"desktop", InstallDesktop, "darwin", ""},
60+
{"unknown", InstallMethod("unknown"), "linux", ""},
5561
}
5662

5763
for _, tt := range tests {
58-
t.Run(string(tt.method), func(t *testing.T) {
59-
got := getUpdateCommand(tt.method)
64+
t.Run(tt.name, func(t *testing.T) {
65+
got := getUpdateCommandForOS(tt.method, tt.goos)
6066
if got != tt.want {
61-
t.Errorf("getUpdateCommand(%q) = %q, want %q", tt.method, got, tt.want)
67+
t.Errorf("getUpdateCommandForOS(%q, %q) = %q, want %q", tt.method, tt.goos, got, tt.want)
6268
}
6369
})
6470
}

web/src/components/ui/UpdateNotification.tsx

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
useApplyDesktopUpdate,
99
} from '../../api/client'
1010
import type { DesktopUpdateState } from '../../api/client'
11+
import { WithTooltip } from './Tooltip'
1112

1213
const DISMISSED_KEY = 'radar-update-dismissed'
1314

@@ -117,7 +118,7 @@ export function UpdateNotification() {
117118
<UpdateIcon state={effectiveState} />
118119
</div>
119120
<div className="flex-1 min-w-0">
120-
<h4 className="text-sm font-medium text-theme-text-primary">
121+
<h4 className="text-sm font-medium text-theme-text-primary pr-6">
121122
<UpdateTitle state={effectiveState} />
122123
</h4>
123124
<p className="text-xs text-theme-text-secondary mt-1">
@@ -140,15 +141,30 @@ export function UpdateNotification() {
140141

141142
{/* CLI: show update command with copy button for package managers */}
142143
{!isDesktop && versionInfo.updateCommand ? (
143-
<button
144-
onClick={handleCopyCommand}
145-
className="flex items-center gap-2 mt-2 px-2 py-1.5 bg-theme-elevated rounded text-xs font-mono text-theme-text-primary hover:bg-theme-surface-hover transition-colors w-full"
146-
>
147-
<code className="flex-1 text-left truncate">{versionInfo.updateCommand}</code>
148-
<CopyIcon copied={copied} failed={copyFailed} />
149-
</button>
144+
<>
145+
<WithTooltip tip={versionInfo.updateCommand} delay={100}>
146+
<button
147+
onClick={handleCopyCommand}
148+
className="flex items-center gap-2 mt-2 px-2 py-1.5 bg-theme-elevated rounded font-mono text-theme-text-primary hover:bg-theme-surface-hover transition-colors w-full"
149+
>
150+
<code className="flex-1 text-left truncate text-[11px]">{versionInfo.updateCommand}</code>
151+
<CopyIcon copied={copied} failed={copyFailed} />
152+
</button>
153+
</WithTooltip>
154+
{/* Direct installs may have placed the binary somewhere the install
155+
script won't touch — surface a download link as a fallback. */}
156+
{versionInfo.installMethod === 'direct' && versionInfo.releaseUrl && (
157+
<a
158+
href={versionInfo.releaseUrl}
159+
target="_blank"
160+
rel="noopener noreferrer"
161+
className="inline-flex items-center gap-1 mt-1.5 text-xs text-theme-text-tertiary hover:text-theme-text-secondary hover:underline"
162+
>
163+
or download from GitHub →
164+
</a>
165+
)}
166+
</>
150167
) : (
151-
/* Direct download - show release link */
152168
!isDesktop && versionInfo.releaseUrl && (
153169
<a
154170
href={versionInfo.releaseUrl}
@@ -161,17 +177,18 @@ export function UpdateNotification() {
161177
)
162178
)}
163179
</div>
164-
{/* Don't show dismiss during active update */}
165-
{effectiveState !== 'downloading' && effectiveState !== 'applying' && (
166-
<button
167-
onClick={handleDismiss}
168-
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded shrink-0"
169-
aria-label="Dismiss"
170-
>
171-
<X className="w-4 h-4" />
172-
</button>
173-
)}
174180
</div>
181+
{/* Dismiss is absolute so it doesn't compress the chip's width.
182+
fixed on the parent already establishes the positioning context. */}
183+
{effectiveState !== 'downloading' && effectiveState !== 'applying' && (
184+
<button
185+
onClick={handleDismiss}
186+
className="absolute top-2 right-2 p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
187+
aria-label="Dismiss"
188+
>
189+
<X className="w-4 h-4" />
190+
</button>
191+
)}
175192
</div>
176193
)
177194
}

0 commit comments

Comments
 (0)