Update files
This commit is contained in:
@@ -22,21 +22,18 @@ import { confirmDialog } from '@/utils/confirm-dialog'
|
|||||||
import { openExternalUrl } from '@/utils/open-external-url'
|
import { openExternalUrl } from '@/utils/open-external-url'
|
||||||
import { formatVersionWithPrefix, isVersionMismatch } from '@/desktop/updates/desktop-updates'
|
import { formatVersionWithPrefix, isVersionMismatch } from '@/desktop/updates/desktop-updates'
|
||||||
import {
|
import {
|
||||||
getCliShimStatus,
|
getCliSymlinkInstructions,
|
||||||
getManagedDaemonLogs,
|
getManagedDaemonLogs,
|
||||||
getManagedDaemonPairing,
|
getManagedDaemonPairing,
|
||||||
getManagedDaemonStatus,
|
getManagedDaemonStatus,
|
||||||
installManagedCliShim,
|
|
||||||
restartManagedDaemon,
|
restartManagedDaemon,
|
||||||
shouldUseManagedDesktopDaemon,
|
shouldUseManagedDesktopDaemon,
|
||||||
startManagedDaemon,
|
startManagedDaemon,
|
||||||
stopManagedDaemon,
|
stopManagedDaemon,
|
||||||
uninstallManagedCliShim,
|
type CliSymlinkInstructions,
|
||||||
type ManagedDaemonLogs,
|
type ManagedDaemonLogs,
|
||||||
type ManagedPairingOffer,
|
type ManagedPairingOffer,
|
||||||
type ManagedDaemonStatus,
|
type ManagedDaemonStatus,
|
||||||
type CliManualInstructions,
|
|
||||||
type CliShimStatus,
|
|
||||||
} from '@/desktop/managed-runtime/managed-runtime'
|
} from '@/desktop/managed-runtime/managed-runtime'
|
||||||
|
|
||||||
export interface LocalDaemonSectionProps {
|
export interface LocalDaemonSectionProps {
|
||||||
@@ -51,29 +48,27 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
|
|||||||
const [statusError, setStatusError] = useState<string | null>(null)
|
const [statusError, setStatusError] = useState<string | null>(null)
|
||||||
const [isRestartingDaemon, setIsRestartingDaemon] = useState(false)
|
const [isRestartingDaemon, setIsRestartingDaemon] = useState(false)
|
||||||
const [isUpdatingDaemonManagement, setIsUpdatingDaemonManagement] = 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 [statusMessage, setStatusMessage] = useState<string | null>(null)
|
||||||
const [cliStatusMessage, setCliStatusMessage] = useState<string | null>(null)
|
const [cliStatusMessage, setCliStatusMessage] = useState<string | null>(null)
|
||||||
const [managedLogs, setManagedLogs] = useState<ManagedDaemonLogs | null>(null)
|
const [managedLogs, setManagedLogs] = useState<ManagedDaemonLogs | null>(null)
|
||||||
const [cliShimStatus, setCliShimStatus] = useState<CliShimStatus | null>(null)
|
|
||||||
const [isLogsModalOpen, setIsLogsModalOpen] = useState(false)
|
const [isLogsModalOpen, setIsLogsModalOpen] = useState(false)
|
||||||
const [isPairingModalOpen, setIsPairingModalOpen] = useState(false)
|
const [isPairingModalOpen, setIsPairingModalOpen] = useState(false)
|
||||||
const [isCliInstallModalOpen, setIsCliInstallModalOpen] = useState(false)
|
const [isCliSymlinkModalOpen, setIsCliSymlinkModalOpen] = useState(false)
|
||||||
const [isLoadingPairing, setIsLoadingPairing] = useState(false)
|
const [isLoadingPairing, setIsLoadingPairing] = useState(false)
|
||||||
const [pairingOffer, setPairingOffer] = useState<ManagedPairingOffer | null>(null)
|
const [pairingOffer, setPairingOffer] = useState<ManagedPairingOffer | null>(null)
|
||||||
const [cliInstallInstructions, setCliInstallInstructions] =
|
const [cliSymlinkInstructions, setCliSymlinkInstructions] =
|
||||||
useState<CliManualInstructions | null>(null)
|
useState<CliSymlinkInstructions | null>(null)
|
||||||
const [pairingStatusMessage, setPairingStatusMessage] = useState<string | null>(null)
|
const [pairingStatusMessage, setPairingStatusMessage] = useState<string | null>(null)
|
||||||
|
|
||||||
const loadManagedStatus = useCallback(() => {
|
const loadManagedStatus = useCallback(() => {
|
||||||
if (!showSection) {
|
if (!showSection) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
return Promise.all([getManagedDaemonStatus(), getManagedDaemonLogs(), getCliShimStatus()])
|
return Promise.all([getManagedDaemonStatus(), getManagedDaemonLogs()])
|
||||||
.then(([status, logs, shimStatus]) => {
|
.then(([status, logs]) => {
|
||||||
setManagedStatus(status)
|
setManagedStatus(status)
|
||||||
setManagedLogs(logs)
|
setManagedLogs(logs)
|
||||||
setCliShimStatus(shimStatus)
|
|
||||||
setStatusError(null)
|
setStatusError(null)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -224,50 +219,39 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
|
|||||||
updateSettings,
|
updateSettings,
|
||||||
])
|
])
|
||||||
|
|
||||||
const handleToggleCliShim = useCallback(() => {
|
const handleOpenCliSymlinkInstructions = useCallback(() => {
|
||||||
if (!showSection || isInstallingCli) {
|
if (!showSection || isLoadingCliSymlinkInstructions) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setIsInstallingCli(true)
|
setIsLoadingCliSymlinkInstructions(true)
|
||||||
const isInstalling = !cliShimStatus?.path
|
setCliStatusMessage(null)
|
||||||
setCliStatusMessage(
|
void getCliSymlinkInstructions()
|
||||||
isInstalling ? 'A permissions popup may appear while Paseo installs the CLI globally.' : null
|
.then((instructions) => {
|
||||||
)
|
setCliSymlinkInstructions(instructions)
|
||||||
const action = cliShimStatus?.path ? uninstallManagedCliShim : installManagedCliShim
|
setIsCliSymlinkModalOpen(true)
|
||||||
void action()
|
|
||||||
.then((result) => {
|
|
||||||
setCliStatusMessage(result.message)
|
|
||||||
if (result.manualInstructions) {
|
|
||||||
setCliInstallInstructions(result.manualInstructions)
|
|
||||||
setIsCliInstallModalOpen(true)
|
|
||||||
} else {
|
|
||||||
setCliInstallInstructions(null)
|
|
||||||
setIsCliInstallModalOpen(false)
|
|
||||||
}
|
|
||||||
return loadManagedStatus()
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
setCliStatusMessage(`CLI install failed: ${message}`)
|
setCliStatusMessage(`Unable to load CLI symlink instructions: ${message}`)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsInstallingCli(false)
|
setIsLoadingCliSymlinkInstructions(false)
|
||||||
})
|
})
|
||||||
}, [cliShimStatus?.path, isInstallingCli, loadManagedStatus, showSection])
|
}, [isLoadingCliSymlinkInstructions, showSection])
|
||||||
|
|
||||||
const handleCopyCliInstallCommands = useCallback(() => {
|
const handleCopyCliSymlinkCommands = useCallback(() => {
|
||||||
if (!cliInstallInstructions?.commands) {
|
if (!cliSymlinkInstructions?.commands) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
void Clipboard.setStringAsync(cliInstallInstructions.commands)
|
void Clipboard.setStringAsync(cliSymlinkInstructions.commands)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
Alert.alert('Copied', 'CLI install commands copied.')
|
Alert.alert('Copied', 'CLI symlink commands copied.')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('[Settings] Failed to copy CLI install commands', error)
|
console.error('[Settings] Failed to copy CLI symlink commands', error)
|
||||||
Alert.alert('Error', 'Unable to copy CLI install commands.')
|
Alert.alert('Error', 'Unable to copy CLI symlink commands.')
|
||||||
})
|
})
|
||||||
}, [cliInstallInstructions?.commands])
|
}, [cliSymlinkInstructions?.commands])
|
||||||
|
|
||||||
const handleCopyLogPath = useCallback(() => {
|
const handleCopyLogPath = useCallback(() => {
|
||||||
const logPath = managedLogs?.logPath
|
const logPath = managedLogs?.logPath
|
||||||
@@ -417,17 +401,17 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
|
|||||||
<View style={[styles.row, styles.rowBorder]}>
|
<View style={[styles.row, styles.rowBorder]}>
|
||||||
<View style={styles.rowContent}>
|
<View style={styles.rowContent}>
|
||||||
<Text style={styles.rowTitle}>Command line (CLI)</Text>
|
<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}
|
{cliStatusMessage ? <Text style={styles.statusText}>{cliStatusMessage}</Text> : null}
|
||||||
</View>
|
</View>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
leftIcon={<Terminal size={theme.iconSize.sm} color={theme.colors.foreground} />}
|
leftIcon={<Terminal size={theme.iconSize.sm} color={theme.colors.foreground} />}
|
||||||
onPress={handleToggleCliShim}
|
onPress={handleOpenCliSymlinkInstructions}
|
||||||
disabled={isInstallingCli}
|
disabled={isLoadingCliSymlinkInstructions}
|
||||||
>
|
>
|
||||||
{isInstallingCli ? 'Working...' : cliShimStatus?.path ? 'Uninstall CLI' : 'Install CLI'}
|
{isLoadingCliSymlinkInstructions ? 'Loading...' : 'Show instructions'}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.row, styles.rowBorder]}>
|
<View style={[styles.row, styles.rowBorder]}>
|
||||||
@@ -483,27 +467,26 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<AdaptiveModalSheet
|
<AdaptiveModalSheet
|
||||||
visible={isCliInstallModalOpen}
|
visible={isCliSymlinkModalOpen}
|
||||||
onClose={() => setIsCliInstallModalOpen(false)}
|
onClose={() => setIsCliSymlinkModalOpen(false)}
|
||||||
title="Install CLI manually"
|
title="Add paseo to your shell"
|
||||||
testID="managed-daemon-cli-install-dialog"
|
testID="managed-daemon-cli-symlink-dialog"
|
||||||
>
|
>
|
||||||
<View style={styles.modalBody}>
|
<View style={styles.modalBody}>
|
||||||
<Text style={styles.hintText}>
|
<Text style={styles.hintText}>
|
||||||
A permissions popup should appear when Paseo installs the CLI globally. If it does not
|
Paseo does not add the command for you. Run the command below in your terminal.
|
||||||
complete, open a terminal and run the commands below.
|
|
||||||
</Text>
|
</Text>
|
||||||
{cliInstallInstructions?.detail ? (
|
{cliSymlinkInstructions?.detail ? (
|
||||||
<Text style={styles.hintText}>{cliInstallInstructions.detail}</Text>
|
<Text style={styles.hintText}>{cliSymlinkInstructions.detail}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
<Text style={styles.codeBlock} selectable>
|
<Text style={styles.codeBlock} selectable>
|
||||||
{cliInstallInstructions?.commands ?? ''}
|
{cliSymlinkInstructions?.commands ?? ''}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.modalActions}>
|
<View style={styles.modalActions}>
|
||||||
<Button variant="outline" size="sm" onPress={() => setIsCliInstallModalOpen(false)}>
|
<Button variant="outline" size="sm" onPress={() => setIsCliSymlinkModalOpen(false)}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" onPress={handleCopyCliInstallCommands}>
|
<Button size="sm" onPress={handleCopyCliSymlinkCommands}>
|
||||||
Copy commands
|
Copy commands
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,46 +1,24 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { parseCliShimResult } from "./managed-runtime";
|
import { parseCliSymlinkInstructions } from "./managed-runtime";
|
||||||
|
|
||||||
describe("parseCliShimResult", () => {
|
describe("parseCliSymlinkInstructions", () => {
|
||||||
it("parses manual install payloads from the desktop backend", () => {
|
it("parses CLI symlink instructions from the desktop backend", () => {
|
||||||
expect(
|
expect(
|
||||||
parseCliShimResult({
|
parseCliSymlinkInstructions({
|
||||||
status: "manualInstallRequired",
|
title: "Add paseo to your shell",
|
||||||
installed: false,
|
detail: "Create a symlink to the Paseo desktop executable.",
|
||||||
path: "/usr/local/bin/paseo",
|
commands: "sudo ln -sf /Applications/Paseo.app/Contents/MacOS/Paseo /usr/local/bin/paseo",
|
||||||
message: "Install it manually.",
|
|
||||||
manualInstructions: {
|
|
||||||
title: "Install from Terminal",
|
|
||||||
detail: "Run these commands.",
|
|
||||||
commands: "sudo tee /usr/local/bin/paseo",
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
).toEqual({
|
).toEqual({
|
||||||
status: "manualInstallRequired",
|
title: "Add paseo to your shell",
|
||||||
installed: false,
|
detail: "Create a symlink to the Paseo desktop executable.",
|
||||||
path: "/usr/local/bin/paseo",
|
commands: "sudo ln -sf /Applications/Paseo.app/Contents/MacOS/Paseo /usr/local/bin/paseo",
|
||||||
message: "Install it manually.",
|
|
||||||
manualInstructions: {
|
|
||||||
title: "Install from Terminal",
|
|
||||||
detail: "Run these commands.",
|
|
||||||
commands: "sudo tee /usr/local/bin/paseo",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to installed or removed when older payloads omit status", () => {
|
it("rejects non-object payloads", () => {
|
||||||
expect(
|
expect(() => parseCliSymlinkInstructions(null)).toThrow(
|
||||||
parseCliShimResult({
|
"Unexpected CLI symlink instructions response."
|
||||||
installed: true,
|
);
|
||||||
path: "/usr/local/bin/paseo",
|
|
||||||
message: "Installed.",
|
|
||||||
})
|
|
||||||
).toEqual({
|
|
||||||
status: "installed",
|
|
||||||
installed: true,
|
|
||||||
path: "/usr/local/bin/paseo",
|
|
||||||
message: "Installed.",
|
|
||||||
manualInstructions: null,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,30 +23,13 @@ export type ManagedDaemonLogs = {
|
|||||||
contents: string
|
contents: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CliShimStatus = {
|
|
||||||
path: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ManagedPairingOffer = {
|
export type ManagedPairingOffer = {
|
||||||
relayEnabled: boolean
|
relayEnabled: boolean
|
||||||
url: string | null
|
url: string | null
|
||||||
qr: string | null
|
qr: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CliShimResult = {
|
export type CliSymlinkInstructions = {
|
||||||
status:
|
|
||||||
| 'installed'
|
|
||||||
| 'removed'
|
|
||||||
| 'elevationDenied'
|
|
||||||
| 'automaticInstallUnavailable'
|
|
||||||
| 'manualInstallRequired'
|
|
||||||
installed: boolean
|
|
||||||
path: string | null
|
|
||||||
message: string
|
|
||||||
manualInstructions: CliManualInstructions | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CliManualInstructions = {
|
|
||||||
title: string
|
title: string
|
||||||
detail: string
|
detail: string
|
||||||
commands: 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 {
|
function parseManagedPairingOffer(raw: unknown): ManagedPairingOffer {
|
||||||
if (!isRecord(raw)) {
|
if (!isRecord(raw)) {
|
||||||
throw new Error('Unexpected managed daemon pairing response.')
|
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)) {
|
if (!isRecord(raw)) {
|
||||||
return null
|
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 {
|
export function shouldUseManagedDesktopDaemon(): boolean {
|
||||||
return isTauriEnvironment() && getTauri() !== null
|
return isTauriEnvironment() && getTauri() !== null
|
||||||
}
|
}
|
||||||
@@ -196,20 +155,20 @@ export async function getManagedDaemonLogs(): Promise<ManagedDaemonLogs> {
|
|||||||
return parseManagedDaemonLogs(await invokeDesktopCommand('managed_daemon_logs'))
|
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> {
|
export async function getManagedDaemonPairing(): Promise<ManagedPairingOffer> {
|
||||||
return parseManagedPairingOffer(await invokeDesktopCommand('managed_daemon_pairing'))
|
return parseManagedPairingOffer(await invokeDesktopCommand('managed_daemon_pairing'))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installManagedCliShim(): Promise<CliShimResult> {
|
export function parseCliSymlinkInstructions(raw: unknown): CliSymlinkInstructions {
|
||||||
return parseCliShimResult(await invokeDesktopCommand('install_cli_shim'))
|
const instructions = parseCliSymlinkInstructionsInternal(raw)
|
||||||
|
if (!instructions) {
|
||||||
|
throw new Error('Unexpected CLI symlink instructions response.')
|
||||||
|
}
|
||||||
|
return instructions
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uninstallManagedCliShim(): Promise<CliShimResult> {
|
export async function getCliSymlinkInstructions(): Promise<CliSymlinkInstructions> {
|
||||||
return parseCliShimResult(await invokeDesktopCommand('uninstall_cli_shim'))
|
return parseCliSymlinkInstructions(await invokeDesktopCommand('cli_symlink_instructions'))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateManagedDaemonTcpSettings(
|
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 { StyleSheet } from "react-native-unistyles";
|
||||||
|
import { PaseoLogo } from "@/components/icons/paseo-logo";
|
||||||
|
import { useTauriDragHandlers } from "@/utils/tauri-window";
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
container: {
|
container: {
|
||||||
@@ -8,11 +10,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
backgroundColor: theme.colors.surface0,
|
backgroundColor: theme.colors.surface0,
|
||||||
},
|
},
|
||||||
logo: {
|
|
||||||
width: 96,
|
|
||||||
height: 96,
|
|
||||||
marginBottom: theme.spacing[6],
|
|
||||||
},
|
|
||||||
status: {
|
status: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
@@ -20,13 +17,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export function StartupSplashScreen() {
|
export function StartupSplashScreen() {
|
||||||
|
const dragHandlers = useTauriDragHandlers();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container} {...dragHandlers}>
|
||||||
<Image
|
<PaseoLogo size={96} />
|
||||||
source={require("../../assets/images/icon.png")}
|
|
||||||
style={styles.logo}
|
|
||||||
resizeMode="contain"
|
|
||||||
/>
|
|
||||||
<Text style={styles.status}>Starting up…</Text>
|
<Text style={styles.status}>Starting up…</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -228,6 +228,50 @@ async function runBundledRuntimeCli(runtimeRoot, managedHome, args, env) {
|
|||||||
return { stdout, stderr };
|
return { stdout, stderr };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolvePackagedRuntimeStatus(binaryPath) {
|
||||||
|
const binaryDir = path.dirname(binaryPath);
|
||||||
|
const resourceDirs = [
|
||||||
|
path.join(binaryDir, "resources"),
|
||||||
|
binaryDir,
|
||||||
|
path.join(binaryDir, "..", "Resources"),
|
||||||
|
path.join(binaryDir, "..", "resources"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let bundledRoot = null;
|
||||||
|
for (const resourceDir of resourceDirs) {
|
||||||
|
for (const candidate of [
|
||||||
|
path.join(resourceDir, "resources", "managed-runtime"),
|
||||||
|
path.join(resourceDir, "managed-runtime"),
|
||||||
|
]) {
|
||||||
|
if (await pathExists(candidate)) {
|
||||||
|
bundledRoot = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bundledRoot) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bundledRoot) {
|
||||||
|
throw new Error("Managed runtime resources are not bundled with this desktop build.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const pointer = JSON.parse(
|
||||||
|
await fs.readFile(path.join(bundledRoot, "current-runtime.json"), "utf8")
|
||||||
|
);
|
||||||
|
const runtimeRoot = path.join(bundledRoot, pointer.relativeRoot);
|
||||||
|
const manifest = JSON.parse(
|
||||||
|
await fs.readFile(path.join(runtimeRoot, "runtime-manifest.json"), "utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
runtimeId: manifest.runtimeId,
|
||||||
|
runtimeVersion: manifest.runtimeVersion,
|
||||||
|
runtimeRoot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function readDaemonStatus(home, env) {
|
async function readDaemonStatus(home, env) {
|
||||||
const result = await runWorkspaceCli(["daemon", "status", "--home", home, "--json"], env);
|
const result = await runWorkspaceCli(["daemon", "status", "--home", home, "--json"], env);
|
||||||
return result.json ?? {};
|
return result.json ?? {};
|
||||||
@@ -464,10 +508,6 @@ function logStep(label) {
|
|||||||
console.log(`\n[managed-smoke] ${label}`);
|
console.log(`\n[managed-smoke] ${label}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldAttemptCliShimInstall(env) {
|
|
||||||
return !(process.platform === "darwin" && env.CI === "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
const packagedBinary = resolvePackagedBinary();
|
const packagedBinary = resolvePackagedBinary();
|
||||||
await ensurePackagedArtifact(packagedBinary);
|
await ensurePackagedArtifact(packagedBinary);
|
||||||
if (!(await pathExists(packagedBinary))) {
|
if (!(await pathExists(packagedBinary))) {
|
||||||
@@ -527,6 +567,8 @@ const managedEnv = {
|
|||||||
let externalPid = null;
|
let externalPid = null;
|
||||||
let startedTemporaryExternalDaemon = false;
|
let startedTemporaryExternalDaemon = false;
|
||||||
let relayProcess = null;
|
let relayProcess = null;
|
||||||
|
let runtimeStatus = null;
|
||||||
|
let managedStart = null;
|
||||||
const forbiddenManagedReferences = ["127.0.0.1:6767", fakePaseoHome, managedRuntimeDir];
|
const forbiddenManagedReferences = ["127.0.0.1:6767", fakePaseoHome, managedRuntimeDir];
|
||||||
const npmExecPath = process.env.npm_execpath;
|
const npmExecPath = process.env.npm_execpath;
|
||||||
if (!npmExecPath) {
|
if (!npmExecPath) {
|
||||||
@@ -584,35 +626,42 @@ try {
|
|||||||
externalPid = externalStatus.pid;
|
externalPid = externalStatus.pid;
|
||||||
assert.ok(externalPid, "external daemon pid should be present");
|
assert.ok(externalPid, "external daemon pid should be present");
|
||||||
|
|
||||||
logStep("Bootstrapping managed runtime from packaged desktop binary");
|
logStep("Resolving bundled runtime from packaged desktop artifact");
|
||||||
const runtimeStatus = await runBinary(packagedBinary, ["--managed-headless", "runtime-status"], managedEnv);
|
runtimeStatus = await resolvePackagedRuntimeStatus(packagedBinary);
|
||||||
assert.equal(runtimeStatus.json.runtimeId, currentRuntimeId);
|
assert.equal(runtimeStatus.runtimeId, currentRuntimeId);
|
||||||
assert.equal(runtimeStatus.json.runtimeVersion, currentRuntimeVersion);
|
assert.equal(runtimeStatus.runtimeVersion, currentRuntimeVersion);
|
||||||
assert.match(
|
assert.match(
|
||||||
runtimeStatus.json.runtimeRoot,
|
runtimeStatus.runtimeRoot,
|
||||||
new RegExp(escapeForRegExp(currentRuntimeId)),
|
new RegExp(escapeForRegExp(currentRuntimeId)),
|
||||||
"runtime status should point into the bundled runtime selected by current-runtime.json"
|
"runtime status should point into the bundled runtime selected by current-runtime.json"
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
runtimeStatus.json.runtimeRoot.startsWith(testRoot),
|
runtimeStatus.runtimeRoot.startsWith(testRoot),
|
||||||
false,
|
false,
|
||||||
"runtime status should not resolve into managed app data"
|
"runtime status should not resolve into managed app data"
|
||||||
);
|
);
|
||||||
assertNoForbiddenPathsOrPorts(runtimeStatus.json, forbiddenManagedReferences);
|
assertNoForbiddenPathsOrPorts(runtimeStatus, forbiddenManagedReferences);
|
||||||
assert.equal(await pathExists(managedRuntimeDir), false, "managed app data should not gain a runtime tree");
|
assert.equal(await pathExists(managedRuntimeDir), false, "managed app data should not gain a runtime tree");
|
||||||
|
|
||||||
const managedBootstrap = await runBinary(packagedBinary, ["--managed-headless", "bootstrap"], managedEnv);
|
const managedBootstrap = await runBundledRuntimeCli(
|
||||||
const managedStart = managedBootstrap.json ?? await waitFor(
|
runtimeStatus.runtimeRoot,
|
||||||
|
cliScratchHome,
|
||||||
|
["start", "--json"],
|
||||||
|
managedEnv
|
||||||
|
);
|
||||||
|
managedStart = (managedBootstrap.stdout.trim() ? JSON.parse(managedBootstrap.stdout.trim()) : null) ?? await waitFor(
|
||||||
async () => {
|
async () => {
|
||||||
const status = await runBinary(
|
const status = await runBundledRuntimeCli(
|
||||||
packagedBinary,
|
runtimeStatus.runtimeRoot,
|
||||||
["--managed-headless", "daemon-status"],
|
cliScratchHome,
|
||||||
|
["daemon", "status", "--json"],
|
||||||
managedEnv
|
managedEnv
|
||||||
);
|
);
|
||||||
assert.equal(status.json?.status, "running");
|
const json = JSON.parse(status.stdout.trim());
|
||||||
assert.ok(status.json?.pid, "managed daemon pid should exist");
|
assert.equal(json.status, "running");
|
||||||
assert.ok(status.json?.serverId, "managed daemon should expose a server id");
|
assert.ok(json.pid, "managed daemon pid should exist");
|
||||||
return status.json;
|
assert.ok(json.serverId, "managed daemon should expose a server id");
|
||||||
|
return json;
|
||||||
},
|
},
|
||||||
10_000,
|
10_000,
|
||||||
"managed daemon bootstrap status"
|
"managed daemon bootstrap status"
|
||||||
@@ -629,111 +678,64 @@ try {
|
|||||||
|
|
||||||
logStep("Verifying the managed daemon stays alive after the packaged command exits");
|
logStep("Verifying the managed daemon stays alive after the packaged command exits");
|
||||||
await sleep(1_500);
|
await sleep(1_500);
|
||||||
const persistedManagedStatus = await runBinary(
|
const persistedManagedStatus = JSON.parse(
|
||||||
packagedBinary,
|
(
|
||||||
["--managed-headless", "daemon-status"],
|
await runBundledRuntimeCli(
|
||||||
managedEnv
|
runtimeStatus.runtimeRoot,
|
||||||
);
|
|
||||||
assert.equal(persistedManagedStatus.json.pid, managedPid);
|
|
||||||
assert.equal(persistedManagedStatus.json.status, "running");
|
|
||||||
assert.equal(persistedManagedStatus.json.serverId, managedStart.serverId);
|
|
||||||
assertNoForbiddenPathsOrPorts(persistedManagedStatus.json, forbiddenManagedReferences);
|
|
||||||
|
|
||||||
const attemptCliShimInstall = shouldAttemptCliShimInstall(managedEnv);
|
|
||||||
const cliInstall = attemptCliShimInstall
|
|
||||||
? await (async () => {
|
|
||||||
logStep("Installing CLI shim and verifying the bundled CLI target");
|
|
||||||
return await runBinary(
|
|
||||||
packagedBinary,
|
|
||||||
["--managed-headless", "install-cli-shim"],
|
|
||||||
managedEnv
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
: {
|
|
||||||
json: {
|
|
||||||
status: "skippedInCi",
|
|
||||||
installed: false,
|
|
||||||
path: null,
|
|
||||||
message: "Skipping privileged macOS CLI shim install in CI; verifying bundled CLI directly.",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const cliShimPath = cliInstall.json.path;
|
|
||||||
const cliShimInstalled =
|
|
||||||
Boolean(attemptCliShimInstall && cliShimPath) && cliInstall.json.installed === true && (await pathExists(cliShimPath));
|
|
||||||
if (attemptCliShimInstall) {
|
|
||||||
assert.ok(cliShimPath, "CLI shim path should be returned");
|
|
||||||
if (!cliShimInstalled) {
|
|
||||||
assert.ok(cliInstall.json.manualInstructions, "manual CLI install instructions should be returned");
|
|
||||||
assert.match(
|
|
||||||
cliInstall.json.manualInstructions.commands,
|
|
||||||
new RegExp(escapeForRegExp(runtimeStatus.json.runtimeRoot)),
|
|
||||||
"manual CLI install instructions should point at the bundled runtime"
|
|
||||||
);
|
|
||||||
assertNoForbiddenPathsOrPorts(cliInstall.json.manualInstructions, forbiddenManagedReferences);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logStep("Skipping privileged CLI shim install in CI and verifying the bundled CLI target directly");
|
|
||||||
}
|
|
||||||
const cliVersion = cliShimInstalled
|
|
||||||
? await execFileWithTimeout(cliShimPath, ["--version"], {
|
|
||||||
env: managedEnv,
|
|
||||||
cwd: repoRoot,
|
|
||||||
maxBuffer: 1024 * 1024,
|
|
||||||
}, "installed CLI shim version check")
|
|
||||||
: await runBundledRuntimeCli(
|
|
||||||
runtimeStatus.json.runtimeRoot,
|
|
||||||
managedStart.home,
|
|
||||||
["--version"],
|
|
||||||
managedEnv
|
|
||||||
);
|
|
||||||
assert.match(cliVersion.stdout.trim(), /^0\./);
|
|
||||||
const shimStatus = cliShimInstalled
|
|
||||||
? await execFileWithTimeout(cliShimPath, ["daemon", "status", "--json"], {
|
|
||||||
env: managedEnv,
|
|
||||||
cwd: repoRoot,
|
|
||||||
maxBuffer: 1024 * 1024,
|
|
||||||
}, "installed CLI shim daemon status")
|
|
||||||
: await runBundledRuntimeCli(
|
|
||||||
runtimeStatus.json.runtimeRoot,
|
|
||||||
managedStart.home,
|
managedStart.home,
|
||||||
["daemon", "status", "--json"],
|
["daemon", "status", "--json"],
|
||||||
managedEnv
|
managedEnv
|
||||||
);
|
)
|
||||||
const shimDaemonStatus = JSON.parse(shimStatus.stdout.trim());
|
).stdout.trim()
|
||||||
assert.equal(shimDaemonStatus.pid, managedPid);
|
);
|
||||||
assertNoForbiddenPathsOrPorts(shimDaemonStatus, forbiddenManagedReferences);
|
assert.equal(persistedManagedStatus.pid, managedPid);
|
||||||
|
assert.equal(persistedManagedStatus.status, "running");
|
||||||
|
assert.equal(persistedManagedStatus.serverId, managedStart.serverId);
|
||||||
|
assertNoForbiddenPathsOrPorts(persistedManagedStatus, forbiddenManagedReferences);
|
||||||
|
|
||||||
|
logStep("Verifying bundled CLI access without installing a shim");
|
||||||
|
const cliVersion = await runBundledRuntimeCli(
|
||||||
|
runtimeStatus.runtimeRoot,
|
||||||
|
managedStart.home,
|
||||||
|
["--version"],
|
||||||
|
managedEnv
|
||||||
|
);
|
||||||
|
assert.match(cliVersion.stdout.trim(), /^0\./);
|
||||||
|
const bundledCliStatus = await runBundledRuntimeCli(
|
||||||
|
runtimeStatus.runtimeRoot,
|
||||||
|
managedStart.home,
|
||||||
|
["daemon", "status", "--json"],
|
||||||
|
managedEnv
|
||||||
|
);
|
||||||
|
const bundledCliDaemonStatus = JSON.parse(bundledCliStatus.stdout.trim());
|
||||||
|
assert.equal(bundledCliDaemonStatus.pid, managedPid);
|
||||||
|
assertNoForbiddenPathsOrPorts(bundledCliDaemonStatus, forbiddenManagedReferences);
|
||||||
|
|
||||||
logStep("Verifying relay connectivity still works after the desktop command has exited");
|
logStep("Verifying relay connectivity still works after the desktop command has exited");
|
||||||
const relayPairing = cliShimInstalled
|
const relayPairing = await runBundledRuntimeCli(
|
||||||
? await execFileWithTimeout(
|
runtimeStatus.runtimeRoot,
|
||||||
cliShimPath,
|
managedStart.home,
|
||||||
["daemon", "pair"],
|
["daemon", "pair"],
|
||||||
{
|
managedEnv
|
||||||
env: managedEnv,
|
);
|
||||||
cwd: repoRoot,
|
|
||||||
maxBuffer: 4 * 1024 * 1024,
|
|
||||||
},
|
|
||||||
"installed CLI shim relay pairing"
|
|
||||||
)
|
|
||||||
: await runBundledRuntimeCli(
|
|
||||||
runtimeStatus.json.runtimeRoot,
|
|
||||||
managedStart.home,
|
|
||||||
["daemon", "pair"],
|
|
||||||
managedEnv
|
|
||||||
);
|
|
||||||
const relayOfferUrl = parseOfferUrlFromCommandOutput(relayPairing.stdout);
|
const relayOfferUrl = parseOfferUrlFromCommandOutput(relayPairing.stdout);
|
||||||
const relayOffer = decodeOfferFromFragmentUrl(relayOfferUrl);
|
const relayOffer = decodeOfferFromFragmentUrl(relayOfferUrl);
|
||||||
assert.equal(relayOffer.relay?.endpoint, relayEndpoint);
|
assert.equal(relayOffer.relay?.endpoint, relayEndpoint);
|
||||||
const relayPong = await connectViaRelay(relayEndpoint, relayOffer);
|
const relayPong = await connectViaRelay(relayEndpoint, relayOffer);
|
||||||
assert.deepEqual(relayPong, { type: "pong" });
|
assert.deepEqual(relayPong, { type: "pong" });
|
||||||
|
|
||||||
logStep("Reopening packaged desktop command path without spawning duplicate daemons");
|
logStep("Re-running managed start without spawning duplicate daemons");
|
||||||
const managedRestartless = await runBinary(
|
const managedRestartless = JSON.parse(
|
||||||
packagedBinary,
|
(
|
||||||
["--managed-headless", "bootstrap"],
|
await runBundledRuntimeCli(
|
||||||
managedEnv
|
runtimeStatus.runtimeRoot,
|
||||||
|
managedStart.home,
|
||||||
|
["start", "--json"],
|
||||||
|
managedEnv
|
||||||
|
)
|
||||||
|
).stdout.trim()
|
||||||
);
|
);
|
||||||
assert.equal(managedRestartless.json.pid, managedPid);
|
assert.equal(managedRestartless.pid, managedPid);
|
||||||
|
|
||||||
logStep("Verifying managed and external daemons coexist");
|
logStep("Verifying managed and external daemons coexist");
|
||||||
const externalStatusAfter = await readDaemonStatus(externalHome, managedEnv);
|
const externalStatusAfter = await readDaemonStatus(externalHome, managedEnv);
|
||||||
@@ -741,13 +743,18 @@ try {
|
|||||||
assert.equal(externalStatusAfter.status, "running");
|
assert.equal(externalStatusAfter.status, "running");
|
||||||
assert.equal(externalPidAfter, externalPid);
|
assert.equal(externalPidAfter, externalPid);
|
||||||
await runWorkspaceCli(["ls", "--host", externalEndpoint, "--json"], managedEnv);
|
await runWorkspaceCli(["ls", "--host", externalEndpoint, "--json"], managedEnv);
|
||||||
const managedStatus = await runBinary(
|
const managedStatus = JSON.parse(
|
||||||
packagedBinary,
|
(
|
||||||
["--managed-headless", "daemon-status"],
|
await runBundledRuntimeCli(
|
||||||
managedEnv
|
runtimeStatus.runtimeRoot,
|
||||||
|
managedStart.home,
|
||||||
|
["daemon", "status", "--json"],
|
||||||
|
managedEnv
|
||||||
|
)
|
||||||
|
).stdout.trim()
|
||||||
);
|
);
|
||||||
assert.ok(managedStatus.json.serverId, "managed daemon should expose a server id");
|
assert.ok(managedStatus.serverId, "managed daemon should expose a server id");
|
||||||
assertNoForbiddenPathsOrPorts(managedStatus.json, forbiddenManagedReferences);
|
assertNoForbiddenPathsOrPorts(managedStatus, forbiddenManagedReferences);
|
||||||
|
|
||||||
logStep("Capturing diagnostics and verifying the fake ~/.paseo stayed untouched");
|
logStep("Capturing diagnostics and verifying the fake ~/.paseo stayed untouched");
|
||||||
const fakePaseoSnapshotAfter = await snapshotTree(fakePaseoHome);
|
const fakePaseoSnapshotAfter = await snapshotTree(fakePaseoHome);
|
||||||
@@ -756,20 +763,19 @@ try {
|
|||||||
path.join(testRoot, "managed-daemon-smoke-diagnostics.json"),
|
path.join(testRoot, "managed-daemon-smoke-diagnostics.json"),
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
runtimeStatus: runtimeStatus.json,
|
runtimeStatus,
|
||||||
managedBootstrap: managedBootstrap.json,
|
managedBootstrap: managedBootstrap.stdout.trim() ? JSON.parse(managedBootstrap.stdout.trim()) : null,
|
||||||
managedStart,
|
managedStart,
|
||||||
persistedManagedStatus: persistedManagedStatus.json,
|
persistedManagedStatus,
|
||||||
managedRestartless: managedRestartless.json,
|
managedRestartless,
|
||||||
cliInstall: cliInstall.json,
|
bundledCliDaemonStatus,
|
||||||
shimDaemonStatus,
|
|
||||||
relayEndpoint,
|
relayEndpoint,
|
||||||
relayOfferUrl,
|
relayOfferUrl,
|
||||||
relayPong,
|
relayPong,
|
||||||
externalEndpoint,
|
externalEndpoint,
|
||||||
externalPid,
|
externalPid,
|
||||||
externalPidAfter,
|
externalPidAfter,
|
||||||
managedStatus: managedStatus.json,
|
managedStatus,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
@@ -781,11 +787,13 @@ try {
|
|||||||
} finally {
|
} finally {
|
||||||
clearInterval(heartbeat);
|
clearInterval(heartbeat);
|
||||||
try {
|
try {
|
||||||
await runBinary(packagedBinary, ["--managed-headless", "stop-daemon"], managedEnv);
|
if (runtimeStatus?.runtimeRoot) {
|
||||||
} catch {}
|
await runBundledRuntimeCli(
|
||||||
try {
|
runtimeStatus.runtimeRoot,
|
||||||
if (shouldAttemptCliShimInstall(managedEnv)) {
|
managedStart?.home ?? cliScratchHome,
|
||||||
await runBinary(packagedBinary, ["--managed-headless", "uninstall-cli-shim"], managedEnv);
|
["daemon", "stop", "--json"],
|
||||||
|
managedEnv
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
!macro NSIS_HOOK_POSTINSTALL
|
||||||
|
CreateDirectory "$LOCALAPPDATA\Microsoft\WinGet\Links"
|
||||||
|
FileOpen $0 "$LOCALAPPDATA\Microsoft\WinGet\Links\paseo.cmd" w
|
||||||
|
FileWrite $0 "@echo off$\r$\n"
|
||||||
|
FileWrite $0 '"$INSTDIR\Paseo.exe" %*$\r$\n'
|
||||||
|
FileClose $0
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro NSIS_HOOK_POSTUNINSTALL
|
||||||
|
Delete "$LOCALAPPDATA\Microsoft\WinGet\Links\paseo.cmd"
|
||||||
|
!macroend
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::json;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -14,11 +13,11 @@ use tauri_plugin_updater::UpdaterExt;
|
|||||||
|
|
||||||
mod runtime_manager;
|
mod runtime_manager;
|
||||||
use runtime_manager::{
|
use runtime_manager::{
|
||||||
cli_shim_status, close_local_daemon_transport, install_cli_shim, managed_daemon_logs,
|
cli_symlink_instructions, close_local_daemon_transport, managed_daemon_logs,
|
||||||
managed_daemon_pairing, managed_daemon_status, managed_runtime_status,
|
managed_daemon_pairing, managed_daemon_status, managed_runtime_status,
|
||||||
open_local_daemon_transport, restart_managed_daemon, send_local_daemon_transport_message,
|
open_local_daemon_transport, restart_managed_daemon, run_managed_cli_from_current_process,
|
||||||
start_managed_daemon, stop_managed_daemon, uninstall_cli_shim,
|
send_local_daemon_transport_message, start_managed_daemon, stop_managed_daemon,
|
||||||
update_managed_daemon_tcp_settings, LocalTransportState, ManagedTcpSettings,
|
update_managed_daemon_tcp_settings, LocalTransportState,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store zoom as u64 bits (f64 * 100 as integer for atomic ops)
|
// Store zoom as u64 bits (f64 * 100 as integer for atomic ops)
|
||||||
@@ -74,145 +73,32 @@ struct AttachmentFileResult {
|
|||||||
byte_size: u64,
|
byte_size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
fn is_ignored_gui_launch_arg(arg: &str) -> bool {
|
||||||
enum ManagedHeadlessCommand {
|
arg.starts_with("-psn_")
|
||||||
RuntimeStatus,
|
|
||||||
Bootstrap,
|
|
||||||
DaemonStatus,
|
|
||||||
StopDaemon,
|
|
||||||
InstallCliShim,
|
|
||||||
UninstallCliShim,
|
|
||||||
UpdateTcp(ManagedTcpSettings),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_managed_headless_command() -> Result<Option<ManagedHeadlessCommand>, String> {
|
fn parse_cli_passthrough_args_from_argv(args: &[String]) -> Option<Vec<String>> {
|
||||||
let args = std::env::args().collect::<Vec<_>>();
|
let effective = args
|
||||||
let Some(flag_index) = args.iter().position(|value| value == "--managed-headless") else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let command = args
|
|
||||||
.get(flag_index + 1)
|
|
||||||
.ok_or_else(|| "Missing command after --managed-headless".to_string())?;
|
|
||||||
let tail = args
|
|
||||||
.iter()
|
.iter()
|
||||||
.skip(flag_index + 2)
|
.skip(1)
|
||||||
|
.filter(|arg| !is_ignored_gui_launch_arg(arg))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
match command.as_str() {
|
(!effective.is_empty()).then_some(effective)
|
||||||
"runtime-status" => Ok(Some(ManagedHeadlessCommand::RuntimeStatus)),
|
|
||||||
"bootstrap" => Ok(Some(ManagedHeadlessCommand::Bootstrap)),
|
|
||||||
"daemon-status" => Ok(Some(ManagedHeadlessCommand::DaemonStatus)),
|
|
||||||
"stop-daemon" => Ok(Some(ManagedHeadlessCommand::StopDaemon)),
|
|
||||||
"install-cli-shim" => Ok(Some(ManagedHeadlessCommand::InstallCliShim)),
|
|
||||||
"uninstall-cli-shim" => Ok(Some(ManagedHeadlessCommand::UninstallCliShim)),
|
|
||||||
"update-tcp" => {
|
|
||||||
let mut enabled = false;
|
|
||||||
let mut host = "127.0.0.1".to_string();
|
|
||||||
let mut port = 7771_u16;
|
|
||||||
let mut index = 0_usize;
|
|
||||||
while index < tail.len() {
|
|
||||||
match tail[index].as_str() {
|
|
||||||
"--enabled" => {
|
|
||||||
enabled = tail
|
|
||||||
.get(index + 1)
|
|
||||||
.ok_or_else(|| "Missing value after --enabled".to_string())?
|
|
||||||
.parse::<bool>()
|
|
||||||
.map_err(|error| format!("Invalid --enabled value: {error}"))?;
|
|
||||||
index += 2;
|
|
||||||
}
|
|
||||||
"--host" => {
|
|
||||||
host = tail
|
|
||||||
.get(index + 1)
|
|
||||||
.ok_or_else(|| "Missing value after --host".to_string())?
|
|
||||||
.to_string();
|
|
||||||
index += 2;
|
|
||||||
}
|
|
||||||
"--port" => {
|
|
||||||
port = tail
|
|
||||||
.get(index + 1)
|
|
||||||
.ok_or_else(|| "Missing value after --port".to_string())?
|
|
||||||
.parse::<u16>()
|
|
||||||
.map_err(|error| format!("Invalid --port value: {error}"))?;
|
|
||||||
index += 2;
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
return Err(format!(
|
|
||||||
"Unknown --managed-headless update-tcp option: {other}"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Some(ManagedHeadlessCommand::UpdateTcp(
|
|
||||||
ManagedTcpSettings {
|
|
||||||
enabled,
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
},
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
other => Err(format!("Unknown --managed-headless command: {other}")),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maybe_run_managed_headless_command(app: &AppHandle) -> Result<bool, String> {
|
pub fn try_run_pre_tauri_mode() -> Option<i32> {
|
||||||
let Some(command) = parse_managed_headless_command()? else {
|
let args = std::env::args().collect::<Vec<_>>();
|
||||||
return Ok(false);
|
let Some(cli_args) = parse_cli_passthrough_args_from_argv(&args) else {
|
||||||
|
return None;
|
||||||
};
|
};
|
||||||
log::info!("[headless] running managed headless command: {:?}", command);
|
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
|
||||||
let _ = window.hide();
|
|
||||||
}
|
|
||||||
let app_handle = app.clone();
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
let result = async {
|
|
||||||
let payload = match command {
|
|
||||||
ManagedHeadlessCommand::RuntimeStatus => {
|
|
||||||
json!(managed_runtime_status(app_handle.clone()).await?)
|
|
||||||
}
|
|
||||||
ManagedHeadlessCommand::Bootstrap => {
|
|
||||||
json!(start_managed_daemon(app_handle.clone()).await?)
|
|
||||||
}
|
|
||||||
ManagedHeadlessCommand::DaemonStatus => {
|
|
||||||
json!(managed_daemon_status(app_handle.clone()).await?)
|
|
||||||
}
|
|
||||||
ManagedHeadlessCommand::StopDaemon => {
|
|
||||||
json!(stop_managed_daemon(app_handle.clone()).await?)
|
|
||||||
}
|
|
||||||
ManagedHeadlessCommand::InstallCliShim => {
|
|
||||||
json!(install_cli_shim(app_handle.clone()).await?)
|
|
||||||
}
|
|
||||||
ManagedHeadlessCommand::UninstallCliShim => {
|
|
||||||
json!(uninstall_cli_shim(app_handle.clone()).await?)
|
|
||||||
}
|
|
||||||
ManagedHeadlessCommand::UpdateTcp(settings) => {
|
|
||||||
json!(update_managed_daemon_tcp_settings(app_handle.clone(), settings).await?)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok::<serde_json::Value, String>(payload)
|
|
||||||
}
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match result {
|
Some(
|
||||||
Ok(payload) => {
|
run_managed_cli_from_current_process(cli_args).unwrap_or_else(|error| {
|
||||||
println!(
|
eprintln!("{error}");
|
||||||
"{}",
|
1
|
||||||
serde_json::to_string_pretty(&payload)
|
}),
|
||||||
.unwrap_or_else(|error| format!(r#"{{"error":"{error}"}}"#))
|
)
|
||||||
);
|
|
||||||
app_handle.exit(0);
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!(
|
|
||||||
"{}",
|
|
||||||
serde_json::to_string_pretty(&json!({ "error": error })).unwrap_or_else(|_| {
|
|
||||||
r#"{"error":"managed headless command failed"}"#.to_string()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app_handle.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shell_command(script: &str) -> Command {
|
fn shell_command(script: &str) -> Command {
|
||||||
@@ -622,14 +508,12 @@ pub fn run() {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
managed_runtime_status,
|
managed_runtime_status,
|
||||||
managed_daemon_status,
|
managed_daemon_status,
|
||||||
cli_shim_status,
|
cli_symlink_instructions,
|
||||||
start_managed_daemon,
|
start_managed_daemon,
|
||||||
stop_managed_daemon,
|
stop_managed_daemon,
|
||||||
restart_managed_daemon,
|
restart_managed_daemon,
|
||||||
managed_daemon_logs,
|
managed_daemon_logs,
|
||||||
managed_daemon_pairing,
|
managed_daemon_pairing,
|
||||||
install_cli_shim,
|
|
||||||
uninstall_cli_shim,
|
|
||||||
update_managed_daemon_tcp_settings,
|
update_managed_daemon_tcp_settings,
|
||||||
open_local_daemon_transport,
|
open_local_daemon_transport,
|
||||||
send_local_daemon_transport_message,
|
send_local_daemon_transport_message,
|
||||||
@@ -650,10 +534,6 @@ pub fn run() {
|
|||||||
app.package_info().version
|
app.package_info().version
|
||||||
);
|
);
|
||||||
|
|
||||||
if maybe_run_managed_headless_command(app.handle())? {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
app.handle().plugin(
|
app.handle().plugin(
|
||||||
tauri_plugin_log::Builder::default()
|
tauri_plugin_log::Builder::default()
|
||||||
.level(log::LevelFilter::Info)
|
.level(log::LevelFilter::Info)
|
||||||
@@ -780,3 +660,38 @@ pub fn run() {
|
|||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::parse_cli_passthrough_args_from_argv;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn routes_meaningful_args_to_cli_mode() {
|
||||||
|
let args = vec![
|
||||||
|
"/Applications/Paseo.app/Contents/MacOS/Paseo".to_string(),
|
||||||
|
"--version".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_cli_passthrough_args_from_argv(&args),
|
||||||
|
Some(vec!["--version".to_string()])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_plain_gui_launch() {
|
||||||
|
let args = vec!["/Applications/Paseo.app/Contents/MacOS/Paseo".to_string()];
|
||||||
|
|
||||||
|
assert_eq!(parse_cli_passthrough_args_from_argv(&args), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_macos_process_serial_number_argument() {
|
||||||
|
let args = vec![
|
||||||
|
"/Applications/Paseo.app/Contents/MacOS/Paseo".to_string(),
|
||||||
|
"-psn_0_12345".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(parse_cli_passthrough_args_from_argv(&args), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,5 +8,8 @@
|
|||||||
tauri::embed_plist::embed_info_plist!("../Info.plist");
|
tauri::embed_plist::embed_info_plist!("../Info.plist");
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
if let Some(exit_code) = paseo_lib::try_run_pre_tauri_mode() {
|
||||||
|
std::process::exit(exit_code);
|
||||||
|
}
|
||||||
paseo_lib::run();
|
paseo_lib::run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ const LOCAL_TRANSPORT_EVENT_NAME: &str = "local-daemon-transport-event";
|
|||||||
const UNIX_CLIENT_URL: &str = "ws://localhost/ws";
|
const UNIX_CLIENT_URL: &str = "ws://localhost/ws";
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
const PIPE_CLIENT_URL: &str = "ws://localhost/ws";
|
const PIPE_CLIENT_URL: &str = "ws://localhost/ws";
|
||||||
const CLI_SHIM_NAME: &str = "paseo";
|
const CLI_LINK_NAME: &str = "paseo";
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
const CLI_SHIM_WINDOWS_NAME: &str = "paseo.cmd";
|
const CLI_LINK_WINDOWS_NAME: &str = "paseo.exe";
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ManagedRuntimeManifest {
|
pub struct ManagedRuntimeManifest {
|
||||||
@@ -84,12 +84,6 @@ pub struct ManagedDaemonStatus {
|
|||||||
pub home: String,
|
pub home: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct CliShimStatus {
|
|
||||||
pub path: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ManagedDaemonLogs {
|
pub struct ManagedDaemonLogs {
|
||||||
@@ -107,17 +101,7 @@ pub struct ManagedPairingOffer {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CliShimResult {
|
pub struct CliSymlinkInstructions {
|
||||||
pub status: String,
|
|
||||||
pub installed: bool,
|
|
||||||
pub path: Option<String>,
|
|
||||||
pub message: String,
|
|
||||||
pub manual_instructions: Option<CliManualInstructions>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct CliManualInstructions {
|
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub detail: String,
|
pub detail: String,
|
||||||
pub commands: String,
|
pub commands: String,
|
||||||
@@ -272,58 +256,24 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_shim_result_serializes_manual_instructions_with_camel_case_keys() {
|
fn cli_symlink_instructions_serialize_with_camel_case_keys() {
|
||||||
let value = serde_json::to_value(CliShimResult {
|
let value = serde_json::to_value(CliSymlinkInstructions {
|
||||||
status: "manualInstallRequired".to_string(),
|
title: "Add paseo to your shell".to_string(),
|
||||||
installed: false,
|
detail: "Create a symlink to the Paseo desktop executable.".to_string(),
|
||||||
path: Some("/usr/local/bin/paseo".to_string()),
|
commands: "sudo ...".to_string(),
|
||||||
message: "Manual install required.".to_string(),
|
|
||||||
manual_instructions: Some(CliManualInstructions {
|
|
||||||
title: "Install from Terminal".to_string(),
|
|
||||||
detail: "Run the commands below.".to_string(),
|
|
||||||
commands: "sudo ...".to_string(),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.expect("serializes");
|
.expect("serializes");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value.get("status").and_then(|entry| entry.as_str()),
|
value.get("title").and_then(|entry| entry.as_str()),
|
||||||
Some("manualInstallRequired")
|
Some("Add paseo to your shell")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value
|
value.get("commands").and_then(|entry| entry.as_str()),
|
||||||
.get("manualInstructions")
|
|
||||||
.and_then(|entry| entry.get("commands"))
|
|
||||||
.and_then(|entry| entry.as_str()),
|
|
||||||
Some("sudo ...")
|
Some("sudo ...")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cli_shim_contents_runs_bundled_runtime_in_place() {
|
|
||||||
let runtime_root =
|
|
||||||
PathBuf::from("/Applications/Paseo.app/Contents/Resources/managed-runtime/runtime-1");
|
|
||||||
let manifest = ManagedRuntimeManifest {
|
|
||||||
runtime_id: "runtime-1".to_string(),
|
|
||||||
runtime_version: "0.1.0".to_string(),
|
|
||||||
platform: "darwin".to_string(),
|
|
||||||
arch: "arm64".to_string(),
|
|
||||||
created_at: "2025-01-01T00:00:00.000Z".to_string(),
|
|
||||||
node_relative_path: "node/node".to_string(),
|
|
||||||
cli_entrypoint_relative_path: "node_modules/@getpaseo/cli/dist/index.js".to_string(),
|
|
||||||
cli_shim_relative_path: "node_modules/@getpaseo/cli/bin/paseo".to_string(),
|
|
||||||
server_runner_relative_path:
|
|
||||||
"node_modules/@getpaseo/server/dist/scripts/daemon-runner.js".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let contents = cli_shim_contents(&runtime_root, &manifest);
|
|
||||||
|
|
||||||
assert!(contents.contains("Contents/Resources/managed-runtime/runtime-1/node/node"));
|
|
||||||
assert!(contents.contains("node_modules/@getpaseo/cli/dist/index.js"));
|
|
||||||
assert!(!contents.contains("PASEO_HOME"));
|
|
||||||
assert!(!contents.contains("--paseo-cli-shim"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires a running local daemon socket"]
|
#[ignore = "requires a running local daemon socket"]
|
||||||
@@ -366,22 +316,25 @@ fn dev_resource_root() -> PathBuf {
|
|||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources")
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bundled_runtime_root_from_resource_dir(resource_dir: &Path) -> Option<PathBuf> {
|
||||||
|
for candidate in [
|
||||||
|
resource_dir.join("resources").join("managed-runtime"),
|
||||||
|
resource_dir.join("managed-runtime"),
|
||||||
|
] {
|
||||||
|
if candidate.exists() {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn bundled_runtime_root(app: &AppHandle) -> Result<PathBuf, String> {
|
fn bundled_runtime_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||||
if let Ok(raw_resource_dir) = app.path().resource_dir() {
|
if let Ok(raw_resource_dir) = app.path().resource_dir() {
|
||||||
let resource_dir = dunce::simplified(&raw_resource_dir).to_path_buf();
|
let resource_dir = dunce::simplified(&raw_resource_dir).to_path_buf();
|
||||||
// Tauri's bundle.resources preserves the source directory structure, so
|
if let Some(candidate) = bundled_runtime_root_from_resource_dir(&resource_dir) {
|
||||||
// "resources/**/*" in tauri.conf.json places files at
|
log::info!("[runtime] found bundled runtime at {}", candidate.display());
|
||||||
// $RESOURCE/resources/managed-runtime/. Try the nested path first (installed
|
return Ok(candidate);
|
||||||
// builds), then the flat path for any future config changes.
|
|
||||||
for candidate in [
|
|
||||||
resource_dir.join("resources").join("managed-runtime"),
|
|
||||||
resource_dir.join("managed-runtime"),
|
|
||||||
] {
|
|
||||||
if candidate.exists() {
|
|
||||||
log::info!("[runtime] found bundled runtime at {}", candidate.display());
|
|
||||||
return Ok(candidate);
|
|
||||||
}
|
|
||||||
log::info!("[runtime] no bundled runtime at {}", candidate.display());
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::info!("[runtime] resource_dir() unavailable, checking dev path");
|
log::info!("[runtime] resource_dir() unavailable, checking dev path");
|
||||||
@@ -464,131 +417,82 @@ fn shell_single_quote(value: &str) -> String {
|
|||||||
format!("'{}'", value.replace('\'', r"'\''"))
|
format!("'{}'", value.replace('\'', r"'\''"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape_applescript_string(value: &str) -> String {
|
|
||||||
value.replace('\\', r"\\").replace('"', "\\\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn powershell_double_quote(value: &str) -> String {
|
fn powershell_double_quote(value: &str) -> String {
|
||||||
value.replace('`', "``").replace('"', "`\"")
|
value.replace('`', "``").replace('"', "`\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn outer_cli_shim_path() -> Result<PathBuf, String> {
|
fn outer_cli_link_path() -> Result<PathBuf, String> {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
return Ok(PathBuf::from("/usr/local/bin").join(CLI_SHIM_NAME));
|
return Ok(PathBuf::from("/usr/local/bin").join(CLI_LINK_NAME));
|
||||||
}
|
}
|
||||||
#[cfg(all(not(target_os = "macos"), not(windows)))]
|
#[cfg(all(not(target_os = "macos"), not(windows)))]
|
||||||
{
|
{
|
||||||
return Ok(PathBuf::from("/usr/local/bin").join(CLI_SHIM_NAME));
|
return Ok(PathBuf::from("/usr/local/bin").join(CLI_LINK_NAME));
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let local_app_data = dirs::data_local_dir().ok_or_else(|| {
|
let local_app_data = dirs::data_local_dir().ok_or_else(|| {
|
||||||
"Failed to resolve LocalAppData for CLI shim instructions.".to_string()
|
"Failed to resolve LocalAppData for CLI symlink instructions.".to_string()
|
||||||
})?;
|
})?;
|
||||||
Ok(local_app_data
|
Ok(local_app_data
|
||||||
.join("Microsoft")
|
.join("Microsoft")
|
||||||
.join("WinGet")
|
.join("WinGet")
|
||||||
.join("Links")
|
.join("Links")
|
||||||
.join(CLI_SHIM_WINDOWS_NAME))
|
.join(CLI_LINK_WINDOWS_NAME))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cli_shim_contents(runtime_root: &Path, manifest: &ManagedRuntimeManifest) -> String {
|
fn desktop_cli_source_path() -> Result<PathBuf, String> {
|
||||||
let node = runtime_root.join(&manifest.node_relative_path);
|
let current_exe = std::env::current_exe()
|
||||||
let cli = runtime_root.join(&manifest.cli_entrypoint_relative_path);
|
.map_err(|error| format!("Failed to resolve desktop executable path: {error}"))?;
|
||||||
|
Ok(dunce::simplified(¤t_exe).to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cli_symlink_instructions_internal() -> Result<CliSymlinkInstructions, String> {
|
||||||
|
let outer_link = outer_cli_link_path()?;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
return format!(
|
let desktop_executable = desktop_cli_source_path()?;
|
||||||
"@echo off\r\n\"{}\" \"{}\" %*\r\n",
|
let target_dir = outer_link
|
||||||
node.display(),
|
|
||||||
cli.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
{
|
|
||||||
format!(
|
|
||||||
"#!/bin/sh\nexec \"{}\" \"{}\" \"$@\"\n",
|
|
||||||
node.display(),
|
|
||||||
cli.display()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_cli_launcher(path: &Path, contents: &str) -> Result<(), String> {
|
|
||||||
if let Some(parent) = path.parent() {
|
|
||||||
fs::create_dir_all(parent)
|
|
||||||
.map_err(|error| format!("Failed to create {}: {error}", parent.display()))?;
|
|
||||||
}
|
|
||||||
fs::write(path, contents)
|
|
||||||
.map_err(|error| format!("Failed to write {}: {error}", path.display()))?;
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
fs::set_permissions(path, fs::Permissions::from_mode(0o755))
|
|
||||||
.map_err(|error| format!("Failed to chmod {}: {error}", path.display()))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cli_manual_instructions(
|
|
||||||
runtime_root: &Path,
|
|
||||||
manifest: &ManagedRuntimeManifest,
|
|
||||||
) -> Result<CliManualInstructions, String> {
|
|
||||||
let outer_shim = outer_cli_shim_path()?;
|
|
||||||
let contents = cli_shim_contents(runtime_root, manifest);
|
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
let target_dir = outer_shim
|
|
||||||
.parent()
|
.parent()
|
||||||
.ok_or_else(|| "CLI shim target is missing a parent directory.".to_string())?;
|
.ok_or_else(|| "CLI symlink target is missing a parent directory.".to_string())?;
|
||||||
return Ok(CliManualInstructions {
|
return Ok(CliSymlinkInstructions {
|
||||||
title: "Install the Paseo CLI from PowerShell".to_string(),
|
title: "Add paseo to your shell".to_string(),
|
||||||
detail: "If the automatic install does not complete, run these commands in PowerShell to write the PATH shim manually.".to_string(),
|
detail: "Create a symlink to the Paseo desktop executable.".to_string(),
|
||||||
commands: format!(
|
commands: format!(
|
||||||
"$target = \"{}\"\nNew-Item -ItemType Directory -Force -Path \"{}\" | Out-Null\n$contents = @\"\n{}\"@\nSet-Content -Path $target -Value $contents -Encoding ASCII\n",
|
"$target = \"{}\"\nNew-Item -ItemType Directory -Force -Path \"{}\" | Out-Null\nif (Test-Path $target) {{ Remove-Item -Path $target -Force }}\nNew-Item -ItemType SymbolicLink -Path $target -Target \"{}\" | Out-Null\n",
|
||||||
powershell_double_quote(outer_shim.to_string_lossy().as_ref()),
|
powershell_double_quote(outer_link.to_string_lossy().as_ref()),
|
||||||
powershell_double_quote(target_dir.to_string_lossy().as_ref()),
|
powershell_double_quote(target_dir.to_string_lossy().as_ref()),
|
||||||
contents
|
powershell_double_quote(desktop_executable.to_string_lossy().as_ref())
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
Ok(CliManualInstructions {
|
let desktop_executable = desktop_cli_source_path()?;
|
||||||
title: "Install the Paseo CLI from Terminal".to_string(),
|
Ok(CliSymlinkInstructions {
|
||||||
detail: "If the automatic install does not complete, run these commands in Terminal to write the global PATH shim manually.".to_string(),
|
title: "Add paseo to your shell".to_string(),
|
||||||
|
detail: "Create a symlink to the Paseo desktop executable.".to_string(),
|
||||||
commands: format!(
|
commands: format!(
|
||||||
"sudo mkdir -p {target_dir}\nprintf '%s\\n' {lines} | sudo tee {target_path} >/dev/null\nsudo chmod 755 {target_path}\n",
|
"sudo mkdir -p {target_dir}\nsudo ln -sf {source_path} {target_path}\n",
|
||||||
target_dir = shell_single_quote(
|
target_dir = shell_single_quote(
|
||||||
outer_shim
|
outer_link
|
||||||
.parent()
|
.parent()
|
||||||
.ok_or_else(|| "CLI shim target is missing a parent directory.".to_string())?
|
.ok_or_else(
|
||||||
|
|| "CLI symlink target is missing a parent directory.".to_string()
|
||||||
|
)?
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
),
|
),
|
||||||
target_path = shell_single_quote(outer_shim.to_string_lossy().as_ref()),
|
target_path = shell_single_quote(outer_link.to_string_lossy().as_ref()),
|
||||||
lines = contents
|
source_path = shell_single_quote(desktop_executable.to_string_lossy().as_ref())
|
||||||
.lines()
|
|
||||||
.map(|line| shell_single_quote(line))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ")
|
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_installed_cli_shim_path(
|
|
||||||
runtime_root: &Path,
|
|
||||||
manifest: &ManagedRuntimeManifest,
|
|
||||||
) -> Option<String> {
|
|
||||||
let outer_shim = outer_cli_shim_path().ok()?;
|
|
||||||
let expected = cli_shim_contents(runtime_root, manifest);
|
|
||||||
let actual = fs::read_to_string(&outer_shim).ok()?;
|
|
||||||
(actual == expected).then(|| outer_shim.to_string_lossy().into_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_runtime_ready_internal(app: &AppHandle) -> Result<ManagedRuntimeStatus, String> {
|
fn ensure_runtime_ready_internal(app: &AppHandle) -> Result<ManagedRuntimeStatus, String> {
|
||||||
log::info!("[runtime] ensuring runtime is ready");
|
log::info!("[runtime] ensuring runtime is ready");
|
||||||
let (bundled_root, pointer) = load_bundled_runtime_pointer(app)?;
|
let (bundled_root, pointer) = load_bundled_runtime_pointer(app)?;
|
||||||
@@ -645,6 +549,19 @@ fn run_cli_json_command(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_cli_passthrough_command(
|
||||||
|
runtime_root: &Path,
|
||||||
|
manifest: &ManagedRuntimeManifest,
|
||||||
|
args: &[String],
|
||||||
|
) -> Result<i32, String> {
|
||||||
|
log::info!("[cli] passthrough: {:?}", args);
|
||||||
|
let status = cli_command(runtime_root, manifest, &[])?
|
||||||
|
.args(args)
|
||||||
|
.status()
|
||||||
|
.map_err(|error| format!("Failed to run bundled CLI: {error}"))?;
|
||||||
|
Ok(status.code().unwrap_or(1))
|
||||||
|
}
|
||||||
|
|
||||||
fn managed_daemon_status_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
|
fn managed_daemon_status_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
|
||||||
let status = ensure_runtime_ready_internal(app)?;
|
let status = ensure_runtime_ready_internal(app)?;
|
||||||
let runtime_root = PathBuf::from(&status.runtime_root);
|
let runtime_root = PathBuf::from(&status.runtime_root);
|
||||||
@@ -665,163 +582,6 @@ fn managed_daemon_status_internal(app: &AppHandle) -> Result<ManagedDaemonStatus
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cli_shim_status_internal(app: &AppHandle) -> Result<CliShimStatus, String> {
|
|
||||||
let status = ensure_runtime_ready_internal(app)?;
|
|
||||||
let runtime_root = PathBuf::from(&status.runtime_root);
|
|
||||||
let manifest = load_runtime_manifest(&runtime_root)?;
|
|
||||||
Ok(CliShimStatus {
|
|
||||||
path: detect_installed_cli_shim_path(&runtime_root, &manifest),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
fn install_cli_shim_via_macos_prompt(shim_path: &Path, contents: &str) -> Result<(), String> {
|
|
||||||
// Manual smoke path: click Install CLI in the desktop app on macOS and confirm the
|
|
||||||
// /usr/local/bin/paseo prompt writes the trivial outer shim shown in cli_manual_instructions().
|
|
||||||
let temp_path = std::env::temp_dir().join(format!("paseo-cli-shim-{}", std::process::id()));
|
|
||||||
write_cli_launcher(&temp_path, contents)?;
|
|
||||||
let command = format!(
|
|
||||||
"mkdir -p {target_dir} && install -m 755 {temp_path} {target_path}",
|
|
||||||
target_dir = shell_single_quote(
|
|
||||||
shim_path
|
|
||||||
.parent()
|
|
||||||
.ok_or_else(|| "CLI shim target is missing a parent directory.".to_string())?
|
|
||||||
.to_string_lossy()
|
|
||||||
.as_ref()
|
|
||||||
),
|
|
||||||
temp_path = shell_single_quote(temp_path.to_string_lossy().as_ref()),
|
|
||||||
target_path = shell_single_quote(shim_path.to_string_lossy().as_ref())
|
|
||||||
);
|
|
||||||
let output = Command::new("osascript")
|
|
||||||
.arg("-e")
|
|
||||||
.arg(format!(
|
|
||||||
"do shell script \"{}\" with administrator privileges",
|
|
||||||
escape_applescript_string(&command)
|
|
||||||
))
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::piped())
|
|
||||||
.output()
|
|
||||||
.map_err(|error| format!("Failed to request macOS CLI install privileges: {error}"))?;
|
|
||||||
let _ = fs::remove_file(&temp_path);
|
|
||||||
if output.status.success() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
|
||||||
if stderr.contains("User canceled") || stderr.contains("(-128)") {
|
|
||||||
return Err("ELEVATION_DENIED".to_string());
|
|
||||||
}
|
|
||||||
Err(if stderr.is_empty() {
|
|
||||||
"Failed to install the global CLI shim.".to_string()
|
|
||||||
} else {
|
|
||||||
stderr
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
fn remove_cli_shim_via_macos_prompt(shim_path: &Path) -> Result<(), String> {
|
|
||||||
let command = format!(
|
|
||||||
"rm -f {}",
|
|
||||||
shell_single_quote(shim_path.to_string_lossy().as_ref())
|
|
||||||
);
|
|
||||||
let output = Command::new("osascript")
|
|
||||||
.arg("-e")
|
|
||||||
.arg(format!(
|
|
||||||
"do shell script \"{}\" with administrator privileges",
|
|
||||||
escape_applescript_string(&command)
|
|
||||||
))
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::piped())
|
|
||||||
.output()
|
|
||||||
.map_err(|error| format!("Failed to request macOS CLI uninstall privileges: {error}"))?;
|
|
||||||
if output.status.success() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
|
||||||
Err(if stderr.is_empty() {
|
|
||||||
"Failed to remove the global CLI shim.".to_string()
|
|
||||||
} else {
|
|
||||||
stderr
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn install_cli_shim_internal(app: &AppHandle) -> Result<CliShimResult, String> {
|
|
||||||
log::info!("[cli-shim] installing CLI shim");
|
|
||||||
let status = ensure_runtime_ready_internal(app)?;
|
|
||||||
let runtime_root = PathBuf::from(&status.runtime_root);
|
|
||||||
let manifest = load_runtime_manifest(&runtime_root)?;
|
|
||||||
let shim_path = outer_cli_shim_path()?;
|
|
||||||
let shim_contents = cli_shim_contents(&runtime_root, &manifest);
|
|
||||||
let manual_instructions = cli_manual_instructions(&runtime_root, &manifest)?;
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let install_result = install_cli_shim_via_macos_prompt(&shim_path, &shim_contents);
|
|
||||||
#[cfg(all(not(target_os = "macos"), not(windows)))]
|
|
||||||
let install_result = write_cli_launcher(&shim_path, &shim_contents);
|
|
||||||
#[cfg(windows)]
|
|
||||||
let install_result: Result<(), String> = Err("AUTOMATIC_INSTALL_UNAVAILABLE".to_string());
|
|
||||||
|
|
||||||
log::info!("[cli-shim] target path: {}", shim_path.display());
|
|
||||||
match install_result {
|
|
||||||
Ok(()) => Ok(CliShimResult {
|
|
||||||
status: "installed".to_string(),
|
|
||||||
installed: true,
|
|
||||||
path: Some(shim_path.to_string_lossy().into_owned()),
|
|
||||||
message: format!("Paseo CLI installed at {}.", shim_path.display()),
|
|
||||||
manual_instructions: None,
|
|
||||||
}),
|
|
||||||
Err(error) if error == "ELEVATION_DENIED" => Ok(CliShimResult {
|
|
||||||
status: "elevationDenied".to_string(),
|
|
||||||
installed: false,
|
|
||||||
path: Some(shim_path.to_string_lossy().into_owned()),
|
|
||||||
message: "CLI install needs administrator approval. If you dismissed the prompt, install it from Terminal with the commands below.".to_string(),
|
|
||||||
manual_instructions: Some(manual_instructions),
|
|
||||||
}),
|
|
||||||
Err(error) if error == "AUTOMATIC_INSTALL_UNAVAILABLE" => Ok(CliShimResult {
|
|
||||||
status: "automaticInstallUnavailable".to_string(),
|
|
||||||
installed: false,
|
|
||||||
path: Some(shim_path.to_string_lossy().into_owned()),
|
|
||||||
message: "Automatic CLI install is not available on this platform. Finish the install manually with the commands below.".to_string(),
|
|
||||||
manual_instructions: Some(manual_instructions),
|
|
||||||
}),
|
|
||||||
Err(error) => Ok(CliShimResult {
|
|
||||||
status: "manualInstallRequired".to_string(),
|
|
||||||
installed: false,
|
|
||||||
path: Some(shim_path.to_string_lossy().into_owned()),
|
|
||||||
message: format!(
|
|
||||||
"Automatic CLI install did not complete: {error}. Finish the install manually with the commands below."
|
|
||||||
),
|
|
||||||
manual_instructions: Some(manual_instructions),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn uninstall_cli_shim_internal(app: &AppHandle) -> Result<CliShimResult, String> {
|
|
||||||
let status = ensure_runtime_ready_internal(app)?;
|
|
||||||
let runtime_root = PathBuf::from(&status.runtime_root);
|
|
||||||
let manifest = load_runtime_manifest(&runtime_root)?;
|
|
||||||
let shim_path = detect_installed_cli_shim_path(&runtime_root, &manifest)
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.unwrap_or(outer_cli_shim_path()?);
|
|
||||||
if shim_path.exists() {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
remove_cli_shim_via_macos_prompt(&shim_path)?;
|
|
||||||
#[cfg(all(not(target_os = "macos"), not(windows)))]
|
|
||||||
fs::remove_file(&shim_path)
|
|
||||||
.map_err(|error| format!("Failed to remove {}: {error}", shim_path.display()))?;
|
|
||||||
#[cfg(windows)]
|
|
||||||
fs::remove_file(&shim_path)
|
|
||||||
.map_err(|error| format!("Failed to remove {}: {error}", shim_path.display()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(CliShimResult {
|
|
||||||
status: "removed".to_string(),
|
|
||||||
installed: false,
|
|
||||||
path: Some(shim_path.to_string_lossy().into_owned()),
|
|
||||||
message: "Paseo CLI shim removed.".to_string(),
|
|
||||||
manual_instructions: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
|
fn start_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
|
||||||
log::info!("[daemon] starting managed daemon");
|
log::info!("[daemon] starting managed daemon");
|
||||||
let status = ensure_runtime_ready_internal(app)?;
|
let status = ensure_runtime_ready_internal(app)?;
|
||||||
@@ -969,10 +729,13 @@ pub async fn restart_managed_daemon(app: AppHandle) -> Result<ManagedDaemonStatu
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn cli_shim_status(app: AppHandle) -> Result<CliShimStatus, String> {
|
pub async fn cli_symlink_instructions(app: AppHandle) -> Result<CliSymlinkInstructions, String> {
|
||||||
tauri::async_runtime::spawn_blocking(move || cli_shim_status_internal(&app))
|
tauri::async_runtime::spawn_blocking(move || {
|
||||||
.await
|
let _ = app;
|
||||||
.map_err(|error| format!("CLI shim status task failed: {error}"))?
|
cli_symlink_instructions_internal()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|error| format!("CLI symlink instructions task failed: {error}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -1009,20 +772,6 @@ pub async fn managed_daemon_pairing(app: AppHandle) -> Result<ManagedPairingOffe
|
|||||||
.map_err(|error| format!("Managed daemon pairing task failed: {error}"))?
|
.map_err(|error| format!("Managed daemon pairing task failed: {error}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn install_cli_shim(app: AppHandle) -> Result<CliShimResult, String> {
|
|
||||||
tauri::async_runtime::spawn_blocking(move || install_cli_shim_internal(&app))
|
|
||||||
.await
|
|
||||||
.map_err(|error| format!("CLI shim install task failed: {error}"))?
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn uninstall_cli_shim(app: AppHandle) -> Result<CliShimResult, String> {
|
|
||||||
tauri::async_runtime::spawn_blocking(move || uninstall_cli_shim_internal(&app))
|
|
||||||
.await
|
|
||||||
.map_err(|error| format!("CLI shim uninstall task failed: {error}"))?
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn update_managed_daemon_tcp_settings(
|
pub async fn update_managed_daemon_tcp_settings(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -1035,6 +784,35 @@ pub async fn update_managed_daemon_tcp_settings(
|
|||||||
.map_err(|error| format!("Managed daemon TCP settings task failed: {error}"))?
|
.map_err(|error| format!("Managed daemon TCP settings task failed: {error}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn run_managed_cli_from_current_process(args: Vec<String>) -> Result<i32, String> {
|
||||||
|
let current_exe = std::env::current_exe()
|
||||||
|
.map_err(|error| format!("Failed to resolve desktop executable path: {error}"))?;
|
||||||
|
let current_exe = dunce::simplified(¤t_exe).to_path_buf();
|
||||||
|
let exe_dir = current_exe
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| "Desktop executable path is missing a parent directory.".to_string())?;
|
||||||
|
let mut resource_dirs = vec![exe_dir.join("resources"), exe_dir.to_path_buf()];
|
||||||
|
if let Some(contents_dir) = exe_dir.parent() {
|
||||||
|
resource_dirs.push(contents_dir.join("Resources"));
|
||||||
|
resource_dirs.push(contents_dir.join("resources"));
|
||||||
|
}
|
||||||
|
let bundled_root = resource_dirs
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|resource_dir| bundled_runtime_root_from_resource_dir(&resource_dir))
|
||||||
|
.or_else(|| {
|
||||||
|
let dev = dev_resource_root().join("managed-runtime");
|
||||||
|
dev.exists().then_some(dev)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
"Managed runtime resources are not bundled with this desktop build.".to_string()
|
||||||
|
})?;
|
||||||
|
let pointer =
|
||||||
|
read_json_file::<BundledRuntimePointer>(&bundled_root.join("current-runtime.json"))?;
|
||||||
|
let runtime_root = bundled_root.join(pointer.relative_root);
|
||||||
|
let manifest = load_runtime_manifest(&runtime_root)?;
|
||||||
|
run_cli_passthrough_command(&runtime_root, &manifest, &args)
|
||||||
|
}
|
||||||
|
|
||||||
async fn spawn_local_transport_session<S>(
|
async fn spawn_local_transport_session<S>(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
transport_state: State<'_, LocalTransportState>,
|
transport_state: State<'_, LocalTransportState>,
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
"resources/**/*"
|
"resources/**/*"
|
||||||
],
|
],
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
|
"windows": {
|
||||||
|
"nsis": {
|
||||||
|
"installerHooks": "./installer-hooks.nsh"
|
||||||
|
}
|
||||||
|
},
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ function Home() {
|
|||||||
transition={{ duration: 0.8, delay: 0.5, ease: 'easeOut' }}
|
transition={{ duration: 0.8, delay: 0.5, ease: 'easeOut' }}
|
||||||
className="relative px-6 md:px-8 pb-8 md:pb-16"
|
className="relative px-6 md:px-8 pb-8 md:pb-16"
|
||||||
>
|
>
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl lg:max-w-7xl xl:max-w-[90rem] mx-auto">
|
||||||
<img
|
<img
|
||||||
src="/paseo-mockup.png"
|
src="/paseo-mockup.png"
|
||||||
alt="Paseo app showing agent management interface"
|
alt="Paseo app showing agent management interface"
|
||||||
|
|||||||
Reference in New Issue
Block a user