Update files

This commit is contained in:
Mohamed Boudra
2026-03-11 23:13:36 +07:00
parent aeefa22ddf
commit 586b48e150
11 changed files with 396 additions and 761 deletions
@@ -22,21 +22,18 @@ import { confirmDialog } from '@/utils/confirm-dialog'
import { openExternalUrl } from '@/utils/open-external-url'
import { formatVersionWithPrefix, isVersionMismatch } from '@/desktop/updates/desktop-updates'
import {
getCliShimStatus,
getCliSymlinkInstructions,
getManagedDaemonLogs,
getManagedDaemonPairing,
getManagedDaemonStatus,
installManagedCliShim,
restartManagedDaemon,
shouldUseManagedDesktopDaemon,
startManagedDaemon,
stopManagedDaemon,
uninstallManagedCliShim,
type CliSymlinkInstructions,
type ManagedDaemonLogs,
type ManagedPairingOffer,
type ManagedDaemonStatus,
type CliManualInstructions,
type CliShimStatus,
} from '@/desktop/managed-runtime/managed-runtime'
export interface LocalDaemonSectionProps {
@@ -51,29 +48,27 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
const [statusError, setStatusError] = useState<string | null>(null)
const [isRestartingDaemon, setIsRestartingDaemon] = useState(false)
const [isUpdatingDaemonManagement, setIsUpdatingDaemonManagement] = useState(false)
const [isInstallingCli, setIsInstallingCli] = useState(false)
const [isLoadingCliSymlinkInstructions, setIsLoadingCliSymlinkInstructions] = useState(false)
const [statusMessage, setStatusMessage] = useState<string | null>(null)
const [cliStatusMessage, setCliStatusMessage] = useState<string | null>(null)
const [managedLogs, setManagedLogs] = useState<ManagedDaemonLogs | null>(null)
const [cliShimStatus, setCliShimStatus] = useState<CliShimStatus | null>(null)
const [isLogsModalOpen, setIsLogsModalOpen] = useState(false)
const [isPairingModalOpen, setIsPairingModalOpen] = useState(false)
const [isCliInstallModalOpen, setIsCliInstallModalOpen] = useState(false)
const [isCliSymlinkModalOpen, setIsCliSymlinkModalOpen] = useState(false)
const [isLoadingPairing, setIsLoadingPairing] = useState(false)
const [pairingOffer, setPairingOffer] = useState<ManagedPairingOffer | null>(null)
const [cliInstallInstructions, setCliInstallInstructions] =
useState<CliManualInstructions | null>(null)
const [cliSymlinkInstructions, setCliSymlinkInstructions] =
useState<CliSymlinkInstructions | null>(null)
const [pairingStatusMessage, setPairingStatusMessage] = useState<string | null>(null)
const loadManagedStatus = useCallback(() => {
if (!showSection) {
return Promise.resolve()
}
return Promise.all([getManagedDaemonStatus(), getManagedDaemonLogs(), getCliShimStatus()])
.then(([status, logs, shimStatus]) => {
return Promise.all([getManagedDaemonStatus(), getManagedDaemonLogs()])
.then(([status, logs]) => {
setManagedStatus(status)
setManagedLogs(logs)
setCliShimStatus(shimStatus)
setStatusError(null)
})
.catch((error) => {
@@ -224,50 +219,39 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
updateSettings,
])
const handleToggleCliShim = useCallback(() => {
if (!showSection || isInstallingCli) {
const handleOpenCliSymlinkInstructions = useCallback(() => {
if (!showSection || isLoadingCliSymlinkInstructions) {
return
}
setIsInstallingCli(true)
const isInstalling = !cliShimStatus?.path
setCliStatusMessage(
isInstalling ? 'A permissions popup may appear while Paseo installs the CLI globally.' : null
)
const action = cliShimStatus?.path ? uninstallManagedCliShim : installManagedCliShim
void action()
.then((result) => {
setCliStatusMessage(result.message)
if (result.manualInstructions) {
setCliInstallInstructions(result.manualInstructions)
setIsCliInstallModalOpen(true)
} else {
setCliInstallInstructions(null)
setIsCliInstallModalOpen(false)
}
return loadManagedStatus()
setIsLoadingCliSymlinkInstructions(true)
setCliStatusMessage(null)
void getCliSymlinkInstructions()
.then((instructions) => {
setCliSymlinkInstructions(instructions)
setIsCliSymlinkModalOpen(true)
})
.catch((error) => {
const message = error instanceof Error ? error.message : String(error)
setCliStatusMessage(`CLI install failed: ${message}`)
setCliStatusMessage(`Unable to load CLI symlink instructions: ${message}`)
})
.finally(() => {
setIsInstallingCli(false)
setIsLoadingCliSymlinkInstructions(false)
})
}, [cliShimStatus?.path, isInstallingCli, loadManagedStatus, showSection])
}, [isLoadingCliSymlinkInstructions, showSection])
const handleCopyCliInstallCommands = useCallback(() => {
if (!cliInstallInstructions?.commands) {
const handleCopyCliSymlinkCommands = useCallback(() => {
if (!cliSymlinkInstructions?.commands) {
return
}
void Clipboard.setStringAsync(cliInstallInstructions.commands)
void Clipboard.setStringAsync(cliSymlinkInstructions.commands)
.then(() => {
Alert.alert('Copied', 'CLI install commands copied.')
Alert.alert('Copied', 'CLI symlink commands copied.')
})
.catch((error) => {
console.error('[Settings] Failed to copy CLI install commands', error)
Alert.alert('Error', 'Unable to copy CLI install commands.')
console.error('[Settings] Failed to copy CLI symlink commands', error)
Alert.alert('Error', 'Unable to copy CLI symlink commands.')
})
}, [cliInstallInstructions?.commands])
}, [cliSymlinkInstructions?.commands])
const handleCopyLogPath = useCallback(() => {
const logPath = managedLogs?.logPath
@@ -417,17 +401,17 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
<View style={[styles.row, styles.rowBorder]}>
<View style={styles.rowContent}>
<Text style={styles.rowTitle}>Command line (CLI)</Text>
<Text style={styles.hintText}>Adds the `paseo` command to your terminal.</Text>
<Text style={styles.hintText}>Shows the command to add `paseo` to your terminal.</Text>
{cliStatusMessage ? <Text style={styles.statusText}>{cliStatusMessage}</Text> : null}
</View>
<Button
variant="outline"
size="sm"
leftIcon={<Terminal size={theme.iconSize.sm} color={theme.colors.foreground} />}
onPress={handleToggleCliShim}
disabled={isInstallingCli}
onPress={handleOpenCliSymlinkInstructions}
disabled={isLoadingCliSymlinkInstructions}
>
{isInstallingCli ? 'Working...' : cliShimStatus?.path ? 'Uninstall CLI' : 'Install CLI'}
{isLoadingCliSymlinkInstructions ? 'Loading...' : 'Show instructions'}
</Button>
</View>
<View style={[styles.row, styles.rowBorder]}>
@@ -483,27 +467,26 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
) : null}
<AdaptiveModalSheet
visible={isCliInstallModalOpen}
onClose={() => setIsCliInstallModalOpen(false)}
title="Install CLI manually"
testID="managed-daemon-cli-install-dialog"
visible={isCliSymlinkModalOpen}
onClose={() => setIsCliSymlinkModalOpen(false)}
title="Add paseo to your shell"
testID="managed-daemon-cli-symlink-dialog"
>
<View style={styles.modalBody}>
<Text style={styles.hintText}>
A permissions popup should appear when Paseo installs the CLI globally. If it does not
complete, open a terminal and run the commands below.
Paseo does not add the command for you. Run the command below in your terminal.
</Text>
{cliInstallInstructions?.detail ? (
<Text style={styles.hintText}>{cliInstallInstructions.detail}</Text>
{cliSymlinkInstructions?.detail ? (
<Text style={styles.hintText}>{cliSymlinkInstructions.detail}</Text>
) : null}
<Text style={styles.codeBlock} selectable>
{cliInstallInstructions?.commands ?? ''}
{cliSymlinkInstructions?.commands ?? ''}
</Text>
<View style={styles.modalActions}>
<Button variant="outline" size="sm" onPress={() => setIsCliInstallModalOpen(false)}>
<Button variant="outline" size="sm" onPress={() => setIsCliSymlinkModalOpen(false)}>
Close
</Button>
<Button size="sm" onPress={handleCopyCliInstallCommands}>
<Button size="sm" onPress={handleCopyCliSymlinkCommands}>
Copy commands
</Button>
</View>
@@ -1,46 +1,24 @@
import { describe, expect, it } from "vitest";
import { parseCliShimResult } from "./managed-runtime";
import { parseCliSymlinkInstructions } from "./managed-runtime";
describe("parseCliShimResult", () => {
it("parses manual install payloads from the desktop backend", () => {
describe("parseCliSymlinkInstructions", () => {
it("parses CLI symlink instructions from the desktop backend", () => {
expect(
parseCliShimResult({
status: "manualInstallRequired",
installed: false,
path: "/usr/local/bin/paseo",
message: "Install it manually.",
manualInstructions: {
title: "Install from Terminal",
detail: "Run these commands.",
commands: "sudo tee /usr/local/bin/paseo",
},
parseCliSymlinkInstructions({
title: "Add paseo to your shell",
detail: "Create a symlink to the Paseo desktop executable.",
commands: "sudo ln -sf /Applications/Paseo.app/Contents/MacOS/Paseo /usr/local/bin/paseo",
})
).toEqual({
status: "manualInstallRequired",
installed: false,
path: "/usr/local/bin/paseo",
message: "Install it manually.",
manualInstructions: {
title: "Install from Terminal",
detail: "Run these commands.",
commands: "sudo tee /usr/local/bin/paseo",
},
title: "Add paseo to your shell",
detail: "Create a symlink to the Paseo desktop executable.",
commands: "sudo ln -sf /Applications/Paseo.app/Contents/MacOS/Paseo /usr/local/bin/paseo",
});
});
it("falls back to installed or removed when older payloads omit status", () => {
expect(
parseCliShimResult({
installed: true,
path: "/usr/local/bin/paseo",
message: "Installed.",
})
).toEqual({
status: "installed",
installed: true,
path: "/usr/local/bin/paseo",
message: "Installed.",
manualInstructions: null,
});
it("rejects non-object payloads", () => {
expect(() => parseCliSymlinkInstructions(null)).toThrow(
"Unexpected CLI symlink instructions response."
);
});
});
@@ -23,30 +23,13 @@ export type ManagedDaemonLogs = {
contents: string
}
export type CliShimStatus = {
path: string | null
}
export type ManagedPairingOffer = {
relayEnabled: boolean
url: string | null
qr: string | null
}
export type CliShimResult = {
status:
| 'installed'
| 'removed'
| 'elevationDenied'
| 'automaticInstallUnavailable'
| 'manualInstallRequired'
installed: boolean
path: string | null
message: string
manualInstructions: CliManualInstructions | null
}
export type CliManualInstructions = {
export type CliSymlinkInstructions = {
title: string
detail: string
commands: string
@@ -122,15 +105,6 @@ function parseManagedDaemonLogs(raw: unknown): ManagedDaemonLogs {
}
}
function parseCliShimStatus(raw: unknown): CliShimStatus {
if (!isRecord(raw)) {
throw new Error('Unexpected CLI shim status response.')
}
return {
path: toStringOrNull(raw.path),
}
}
function parseManagedPairingOffer(raw: unknown): ManagedPairingOffer {
if (!isRecord(raw)) {
throw new Error('Unexpected managed daemon pairing response.')
@@ -142,7 +116,7 @@ function parseManagedPairingOffer(raw: unknown): ManagedPairingOffer {
}
}
function parseCliManualInstructions(raw: unknown): CliManualInstructions | null {
function parseCliSymlinkInstructionsInternal(raw: unknown): CliSymlinkInstructions | null {
if (!isRecord(raw)) {
return null
}
@@ -153,21 +127,6 @@ function parseCliManualInstructions(raw: unknown): CliManualInstructions | null
}
}
export function parseCliShimResult(raw: unknown): CliShimResult {
if (!isRecord(raw)) {
throw new Error('Unexpected CLI shim response.')
}
return {
status:
(toStringOrNull(raw.status) as CliShimResult['status'] | null) ??
(raw.installed === true ? 'installed' : 'removed'),
installed: raw.installed === true,
path: toStringOrNull(raw.path),
message: toStringOrNull(raw.message) ?? '',
manualInstructions: parseCliManualInstructions(raw.manualInstructions),
}
}
export function shouldUseManagedDesktopDaemon(): boolean {
return isTauriEnvironment() && getTauri() !== null
}
@@ -196,20 +155,20 @@ export async function getManagedDaemonLogs(): Promise<ManagedDaemonLogs> {
return parseManagedDaemonLogs(await invokeDesktopCommand('managed_daemon_logs'))
}
export async function getCliShimStatus(): Promise<CliShimStatus> {
return parseCliShimStatus(await invokeDesktopCommand('cli_shim_status'))
}
export async function getManagedDaemonPairing(): Promise<ManagedPairingOffer> {
return parseManagedPairingOffer(await invokeDesktopCommand('managed_daemon_pairing'))
}
export async function installManagedCliShim(): Promise<CliShimResult> {
return parseCliShimResult(await invokeDesktopCommand('install_cli_shim'))
export function parseCliSymlinkInstructions(raw: unknown): CliSymlinkInstructions {
const instructions = parseCliSymlinkInstructionsInternal(raw)
if (!instructions) {
throw new Error('Unexpected CLI symlink instructions response.')
}
return instructions
}
export async function uninstallManagedCliShim(): Promise<CliShimResult> {
return parseCliShimResult(await invokeDesktopCommand('uninstall_cli_shim'))
export async function getCliSymlinkInstructions(): Promise<CliSymlinkInstructions> {
return parseCliSymlinkInstructions(await invokeDesktopCommand('cli_symlink_instructions'))
}
export async function updateManagedDaemonTcpSettings(
@@ -1,5 +1,7 @@
import { Image, Text, View } from "react-native";
import { Text, View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { PaseoLogo } from "@/components/icons/paseo-logo";
import { useTauriDragHandlers } from "@/utils/tauri-window";
const styles = StyleSheet.create((theme) => ({
container: {
@@ -8,11 +10,6 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
backgroundColor: theme.colors.surface0,
},
logo: {
width: 96,
height: 96,
marginBottom: theme.spacing[6],
},
status: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.base,
@@ -20,13 +17,11 @@ const styles = StyleSheet.create((theme) => ({
}));
export function StartupSplashScreen() {
const dragHandlers = useTauriDragHandlers();
return (
<View style={styles.container}>
<Image
source={require("../../assets/images/icon.png")}
style={styles.logo}
resizeMode="contain"
/>
<View style={styles.container} {...dragHandlers}>
<PaseoLogo size={96} />
<Text style={styles.status}>Starting up…</Text>
</View>
);