diff --git a/packages/app/metro.config.js b/packages/app/metro.config.js
index a4ac59f3c..027ece81b 100644
--- a/packages/app/metro.config.js
+++ b/packages/app/metro.config.js
@@ -1,10 +1,12 @@
const { getDefaultConfig } = require("expo/metro-config");
+const exclusionList = require("metro-config/src/defaults/exclusionList");
const { resolve } = require("metro-resolver");
const fs = require("fs");
const path = require("path");
const projectRoot = __dirname;
const appNodeModulesRoot = path.resolve(projectRoot, "node_modules");
+const appSrcRoot = path.resolve(projectRoot, "src");
const serverSrcRoot = path.resolve(projectRoot, "../server/src");
const relaySrcRoot = path.resolve(projectRoot, "../relay/src");
const customWebPlatform = (process.env.PASEO_WEB_PLATFORM ?? "")
@@ -14,6 +16,9 @@ const customWebPlatform = (process.env.PASEO_WEB_PLATFORM ?? "")
const config = getDefaultConfig(projectRoot);
const defaultResolveRequest = config.resolver.resolveRequest ?? resolve;
+const escapedAppSrcRoot = appSrcRoot
+ .replace(/[|\\{}()[\]^$+*?.]/g, "\\$&")
+ .replace(/\//g, "[\\\\/]");
config.resolver.extraNodeModules = {
...(config.resolver.extraNodeModules ?? {}),
@@ -22,6 +27,9 @@ config.resolver.extraNodeModules = {
"react/jsx-runtime": path.join(appNodeModulesRoot, "react/jsx-runtime"),
"react/jsx-dev-runtime": path.join(appNodeModulesRoot, "react/jsx-dev-runtime"),
};
+config.resolver.blockList = exclusionList([
+ new RegExp(`^${escapedAppSrcRoot}[\\\\/].*\\.(test|spec)\\.(ts|tsx)$`),
+]);
function isLocalModuleImport(moduleName) {
return (
diff --git a/packages/app/src/app/index-startup.ts b/packages/app/src/app/index-startup.ts
new file mode 100644
index 000000000..337f4304c
--- /dev/null
+++ b/packages/app/src/app/index-startup.ts
@@ -0,0 +1,41 @@
+export const WELCOME_ROUTE = '/welcome'
+
+export function shouldWaitOnStartupRace(input: {
+ registryLoading: boolean
+ onlineServerId: string | null
+ hasTimedOut: boolean
+ isDesktopStartupRace: boolean
+ daemonCount: number
+ pathname: string
+}): boolean {
+ if (input.registryLoading) {
+ return true
+ }
+ if (input.onlineServerId) {
+ return false
+ }
+ if (input.pathname === WELCOME_ROUTE) {
+ return false
+ }
+ if (input.hasTimedOut) {
+ return false
+ }
+ return input.isDesktopStartupRace || input.daemonCount > 0
+}
+
+export function shouldRedirectToWelcome(input: {
+ registryLoading: boolean
+ onlineServerId: string | null
+ hasTimedOut: boolean
+ pathname: string
+ isDesktopStartupRace: boolean
+ daemonCount: number
+}): boolean {
+ if (input.registryLoading || input.onlineServerId || !input.hasTimedOut) {
+ return false
+ }
+ if (input.pathname !== '/' && input.pathname !== '') {
+ return false
+ }
+ return input.isDesktopStartupRace || input.daemonCount > 0
+}
diff --git a/packages/app/src/app/index.tsx b/packages/app/src/app/index.tsx
index 45fe75571..3502d86bf 100644
--- a/packages/app/src/app/index.tsx
+++ b/packages/app/src/app/index.tsx
@@ -1,65 +1,120 @@
-import { useEffect, useMemo } from "react";
-import { useLocalSearchParams, usePathname, useRouter } from "expo-router";
-import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
-import { useFormPreferences } from "@/hooks/use-form-preferences";
-import { buildHostRootRoute } from "@/utils/host-routes";
-import { StartupSplashScreen } from "@/screens/startup-splash-screen";
-import { WelcomeScreen } from "@/components/welcome-screen";
+import { useEffect, useSyncExternalStore, useState } from 'react'
+import { usePathname, useRouter } from 'expo-router'
+import { useDaemonRegistry } from '@/contexts/daemon-registry-context'
+import { shouldUseManagedDesktopDaemon } from '@/desktop/managed-runtime/managed-runtime'
+import { buildHostRootRoute } from '@/utils/host-routes'
+import { StartupSplashScreen } from '@/screens/startup-splash-screen'
+import { WelcomeScreen } from '@/components/welcome-screen'
+import { getHostRuntimeStore, isHostRuntimeConnected } from '@/runtime/host-runtime'
+import {
+ shouldRedirectToWelcome,
+ shouldWaitOnStartupRace,
+ WELCOME_ROUTE,
+} from './index-startup'
+
+const STARTUP_TIMEOUT_MS = 30_000
+
+function useAnyHostOnline(serverIds: string[]): string | null {
+ const runtime = getHostRuntimeStore()
+ return useSyncExternalStore(
+ (onStoreChange) => runtime.subscribeAll(onStoreChange),
+ () => {
+ let firstOnlineServerId: string | null = null
+ let firstOnlineAt: string | null = null
+ for (const serverId of serverIds) {
+ const snapshot = runtime.getSnapshot(serverId)
+ const lastOnlineAt = snapshot?.lastOnlineAt ?? null
+ if (!isHostRuntimeConnected(snapshot) || !lastOnlineAt) {
+ continue
+ }
+ if (!firstOnlineAt || lastOnlineAt < firstOnlineAt) {
+ firstOnlineAt = lastOnlineAt
+ firstOnlineServerId = serverId
+ }
+ }
+ return firstOnlineServerId
+ },
+ () => {
+ let firstOnlineServerId: string | null = null
+ let firstOnlineAt: string | null = null
+ for (const serverId of serverIds) {
+ const snapshot = runtime.getSnapshot(serverId)
+ const lastOnlineAt = snapshot?.lastOnlineAt ?? null
+ if (!isHostRuntimeConnected(snapshot) || !lastOnlineAt) {
+ continue
+ }
+ if (!firstOnlineAt || lastOnlineAt < firstOnlineAt) {
+ firstOnlineAt = lastOnlineAt
+ firstOnlineServerId = serverId
+ }
+ }
+ return firstOnlineServerId
+ }
+ )
+}
export default function Index() {
- const router = useRouter();
- const pathname = usePathname();
- const params = useLocalSearchParams<{ serverId?: string }>();
- const { daemons, isLoading: registryLoading, isReconciling } = useDaemonRegistry();
- const { preferences, isLoading: preferencesLoading } = useFormPreferences();
- const requestedServerId = useMemo(() => {
- return typeof params.serverId === "string" ? params.serverId.trim() : "";
- }, [params.serverId]);
-
- const targetServerId = useMemo(() => {
- if (daemons.length === 0) {
- return null;
- }
- if (requestedServerId) {
- const requested = daemons.find(
- (daemon) => daemon.serverId === requestedServerId
- );
- if (requested) {
- return requested.serverId;
- }
- }
- if (preferences.serverId) {
- const match = daemons.find((daemon) => daemon.serverId === preferences.serverId);
- if (match) {
- return match.serverId;
- }
- }
- return daemons[0]?.serverId ?? null;
- }, [daemons, preferences.serverId, requestedServerId]);
+ const router = useRouter()
+ const pathname = usePathname()
+ const { daemons, isLoading: registryLoading } = useDaemonRegistry()
+ const [hasTimedOut, setHasTimedOut] = useState(false)
+ const isDesktopStartupRace = shouldUseManagedDesktopDaemon()
+ const onlineServerId = useAnyHostOnline(daemons.map((daemon) => daemon.serverId))
useEffect(() => {
- if (registryLoading || preferencesLoading) {
- return;
+ const timer = setTimeout(() => {
+ setHasTimedOut(true)
+ }, STARTUP_TIMEOUT_MS)
+ return () => {
+ clearTimeout(timer)
}
- if (!targetServerId) {
- return;
- }
- if (pathname !== "/" && pathname !== "") {
- return;
- }
- router.replace(buildHostRootRoute(targetServerId) as any);
- }, [pathname, preferencesLoading, registryLoading, router, targetServerId]);
+ }, [])
- if (registryLoading || preferencesLoading) {
- return ;
+ useEffect(() => {
+ if (registryLoading) {
+ return
+ }
+ if (!onlineServerId) {
+ return
+ }
+ if (pathname !== '/' && pathname !== '') {
+ return
+ }
+ router.replace(buildHostRootRoute(onlineServerId) as any)
+ }, [onlineServerId, pathname, registryLoading, router])
+
+ useEffect(() => {
+ if (
+ !shouldRedirectToWelcome({
+ registryLoading,
+ onlineServerId,
+ hasTimedOut,
+ pathname,
+ isDesktopStartupRace,
+ daemonCount: daemons.length,
+ })
+ ) {
+ return
+ }
+ router.replace(WELCOME_ROUTE as any)
+ }, [daemons.length, hasTimedOut, isDesktopStartupRace, onlineServerId, pathname, registryLoading, router])
+
+ if (
+ shouldWaitOnStartupRace({
+ registryLoading,
+ onlineServerId,
+ hasTimedOut,
+ isDesktopStartupRace,
+ daemonCount: daemons.length,
+ pathname,
+ })
+ ) {
+ return
}
- if (!targetServerId) {
- if (isReconciling) {
- return ;
- }
- return ;
+ if (!onlineServerId) {
+ return
}
- return null;
+ return null
}
diff --git a/packages/app/src/app/welcome.tsx b/packages/app/src/app/welcome.tsx
new file mode 100644
index 000000000..ead2f1c1e
--- /dev/null
+++ b/packages/app/src/app/welcome.tsx
@@ -0,0 +1,5 @@
+import { WelcomeScreen } from '@/components/welcome-screen'
+
+export default function WelcomeRoute() {
+ return
+}
diff --git a/packages/app/src/contexts/daemon-registry-context.test.ts b/packages/app/src/contexts/daemon-registry-context.test.ts
index 13e2ca891..407361220 100644
--- a/packages/app/src/contexts/daemon-registry-context.test.ts
+++ b/packages/app/src/contexts/daemon-registry-context.test.ts
@@ -1,9 +1,8 @@
import { describe, expect, it } from 'vitest'
import {
+ connectionFromListen,
hostHasDirectEndpoint,
registryHasDirectEndpoint,
- reconcileDesktopStartupRegistry,
- resolveManagedDesktopStartupStatus,
type HostProfile,
} from './daemon-registry-context'
@@ -12,12 +11,7 @@ function makeHost(input: Partial & Pick):
return {
serverId: input.serverId,
label: input.label ?? input.serverId,
- lifecycle: input.lifecycle ?? {
- managed: false,
- managedRuntimeId: null,
- managedRuntimeVersion: null,
- associatedServerId: null,
- },
+ lifecycle: input.lifecycle ?? {},
connections: input.connections ?? [],
preferredConnectionId: input.preferredConnectionId ?? null,
createdAt: input.createdAt ?? now,
@@ -25,6 +19,47 @@ function makeHost(input: Partial & Pick):
}
}
+describe('connectionFromListen', () => {
+ it('parses tcp listen targets', () => {
+ expect(connectionFromListen('127.0.0.1:6767')).toEqual({
+ id: 'direct:127.0.0.1:6767',
+ type: 'directTcp',
+ endpoint: '127.0.0.1:6767',
+ })
+ })
+
+ it('parses unix socket listen targets', () => {
+ expect(connectionFromListen('/tmp/paseo.sock')).toEqual({
+ id: 'socket:/tmp/paseo.sock',
+ type: 'directSocket',
+ path: '/tmp/paseo.sock',
+ })
+ expect(connectionFromListen('unix:///tmp/paseo.sock')).toEqual({
+ id: 'socket:/tmp/paseo.sock',
+ type: 'directSocket',
+ path: '/tmp/paseo.sock',
+ })
+ })
+
+ it('parses named pipe listen targets', () => {
+ expect(connectionFromListen(String.raw`\\.\pipe\paseo-test`)).toEqual({
+ id: String.raw`pipe:\\.\pipe\paseo-test`,
+ type: 'directPipe',
+ path: String.raw`\\.\pipe\paseo-test`,
+ })
+ expect(connectionFromListen('pipe://paseo-test')).toEqual({
+ id: 'pipe:paseo-test',
+ type: 'directPipe',
+ path: 'paseo-test',
+ })
+ })
+
+ it('returns null for unsupported listen targets', () => {
+ expect(connectionFromListen('')).toBeNull()
+ expect(connectionFromListen('not-a-listen-target')).toBeNull()
+ })
+})
+
describe('hostHasDirectEndpoint', () => {
it('returns true when host has matching direct endpoint', () => {
const host = makeHost({
@@ -84,240 +119,3 @@ describe('registryHasDirectEndpoint', () => {
expect(registryHasDirectEndpoint(hosts, 'localhost:6767')).toBe(false)
})
})
-
-describe('reconcileDesktopStartupRegistry', () => {
- it('seeds managed and localhost connections as normal host entries', () => {
- const now = '2026-03-08T00:00:00.000Z'
-
- const result = reconcileDesktopStartupRegistry({
- existing: [],
- managed: {
- serverId: 'srv_managed',
- hostname: 'managed-host',
- runtimeId: 'runtime_1',
- runtimeVersion: '1.2.3',
- transportType: 'socket',
- transportPath: '/Users/test/.paseo-test/paseo.sock',
- associatedServerId: 'srv_managed',
- },
- localhost: {
- serverId: 'srv_localhost',
- hostname: 'local-dev',
- endpoint: 'localhost:6767',
- },
- now,
- })
-
- expect(result).toEqual([
- makeHost({
- serverId: 'srv_managed',
- label: 'managed-host',
- lifecycle: {
- managed: true,
- managedRuntimeId: 'runtime_1',
- managedRuntimeVersion: '1.2.3',
- associatedServerId: 'srv_managed',
- },
- connections: [
- {
- id: 'socket:/Users/test/.paseo-test/paseo.sock',
- type: 'directSocket',
- path: '/Users/test/.paseo-test/paseo.sock',
- },
- ],
- preferredConnectionId: 'socket:/Users/test/.paseo-test/paseo.sock',
- createdAt: now,
- updatedAt: now,
- }),
- makeHost({
- serverId: 'srv_localhost',
- label: 'local-dev',
- connections: [
- {
- id: 'direct:localhost:6767',
- type: 'directTcp',
- endpoint: 'localhost:6767',
- },
- ],
- preferredConnectionId: 'direct:localhost:6767',
- createdAt: now,
- updatedAt: now,
- }),
- ])
- })
-
- it('keeps managed and localhost connections together when they resolve to the same server', () => {
- const now = '2026-03-08T00:00:00.000Z'
-
- const result = reconcileDesktopStartupRegistry({
- existing: [],
- managed: {
- serverId: 'srv_shared',
- hostname: 'devbox',
- runtimeId: 'runtime_1',
- runtimeVersion: '1.2.3',
- transportType: 'socket',
- transportPath: '/Users/test/.paseo-test/paseo.sock',
- associatedServerId: 'srv_shared',
- },
- localhost: {
- serverId: 'srv_shared',
- hostname: 'devbox',
- endpoint: 'localhost:6767',
- },
- now,
- })
-
- expect(result).toEqual([
- makeHost({
- serverId: 'srv_shared',
- label: 'devbox',
- lifecycle: {
- managed: true,
- managedRuntimeId: 'runtime_1',
- managedRuntimeVersion: '1.2.3',
- associatedServerId: 'srv_shared',
- },
- connections: [
- {
- id: 'socket:/Users/test/.paseo-test/paseo.sock',
- type: 'directSocket',
- path: '/Users/test/.paseo-test/paseo.sock',
- },
- {
- id: 'direct:localhost:6767',
- type: 'directTcp',
- endpoint: 'localhost:6767',
- },
- ],
- preferredConnectionId: 'socket:/Users/test/.paseo-test/paseo.sock',
- createdAt: now,
- updatedAt: now,
- }),
- ])
- })
-
- it('is idempotent for repeated desktop startup reconciliation', () => {
- const now = '2026-03-08T00:00:00.000Z'
-
- const first = reconcileDesktopStartupRegistry({
- existing: [],
- managed: {
- serverId: 'srv_shared',
- hostname: 'devbox',
- runtimeId: 'runtime_1',
- runtimeVersion: '1.2.3',
- transportType: 'socket',
- transportPath: '/Users/test/.paseo-test/paseo.sock',
- associatedServerId: 'srv_shared',
- },
- localhost: {
- serverId: 'srv_shared',
- hostname: 'devbox',
- endpoint: 'localhost:6767',
- },
- now,
- })
-
- const second = reconcileDesktopStartupRegistry({
- existing: first,
- managed: {
- serverId: 'srv_shared',
- hostname: 'devbox',
- runtimeId: 'runtime_1',
- runtimeVersion: '1.2.3',
- transportType: 'socket',
- transportPath: '/Users/test/.paseo-test/paseo.sock',
- associatedServerId: 'srv_shared',
- },
- localhost: {
- serverId: 'srv_shared',
- hostname: 'devbox',
- endpoint: 'localhost:6767',
- },
- now: '2026-03-09T00:00:00.000Z',
- })
-
- expect(second).toEqual(first)
- })
-})
-
-describe('resolveManagedDesktopStartupStatus', () => {
- it('starts the managed daemon when management is enabled', async () => {
- const managedStatus = {
- runtimeId: 'runtime_1',
- runtimeVersion: '1.2.3',
- runtimeRoot: '/runtime',
- managedHome: '/home',
- transportType: 'socket',
- transportPath: '/tmp/paseo.sock',
- daemonPid: 123,
- daemonRunning: true,
- daemonStatus: 'running',
- logPath: '/tmp/daemon.log',
- serverId: 'srv_managed',
- hostname: 'managed-host',
- relayEnabled: true,
- tcpEnabled: false,
- tcpListen: null,
- cliShimPath: null,
- }
- let startCalls = 0
- let statusCalls = 0
-
- const result = await resolveManagedDesktopStartupStatus({
- settings: { manageBuiltInDaemon: true },
- startManagedDaemonFn: async () => {
- startCalls += 1
- return managedStatus
- },
- getManagedDaemonStatusFn: async () => {
- statusCalls += 1
- return managedStatus
- },
- })
-
- expect(result).toEqual(managedStatus)
- expect(startCalls).toBe(1)
- expect(statusCalls).toBe(0)
- })
-
- it('only reads managed daemon status when management is paused', async () => {
- const managedStatus = {
- runtimeId: 'runtime_1',
- runtimeVersion: '1.2.3',
- runtimeRoot: '/runtime',
- managedHome: '/home',
- transportType: 'socket',
- transportPath: '/tmp/paseo.sock',
- daemonPid: null,
- daemonRunning: false,
- daemonStatus: 'stopped',
- logPath: '/tmp/daemon.log',
- serverId: null,
- hostname: null,
- relayEnabled: true,
- tcpEnabled: false,
- tcpListen: null,
- cliShimPath: null,
- }
- let startCalls = 0
- let statusCalls = 0
-
- const result = await resolveManagedDesktopStartupStatus({
- settings: { manageBuiltInDaemon: false },
- startManagedDaemonFn: async () => {
- startCalls += 1
- return managedStatus
- },
- getManagedDaemonStatusFn: async () => {
- statusCalls += 1
- return managedStatus
- },
- })
-
- expect(result).toEqual(managedStatus)
- expect(startCalls).toBe(0)
- expect(statusCalls).toBe(1)
- })
-})
diff --git a/packages/app/src/contexts/daemon-registry-context.tsx b/packages/app/src/contexts/daemon-registry-context.tsx
index 13e7eb5b4..36d04e22a 100644
--- a/packages/app/src/contexts/daemon-registry-context.tsx
+++ b/packages/app/src/contexts/daemon-registry-context.tsx
@@ -1,14 +1,11 @@
-import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
+import { createContext, useCallback, useContext, useEffect, useRef } from 'react'
import type { ReactNode } from 'react'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { decodeOfferFragmentPayload, normalizeHostPort } from '@/utils/daemon-endpoints'
import { probeConnection } from '@/utils/test-daemon-connection'
-import { useAppSettings, type AppSettings } from '@/hooks/use-settings'
import { ConnectionOfferSchema, type ConnectionOffer } from '@server/shared/connection-offer'
import {
- getManagedDaemonStatus,
- type ManagedDaemonStatus,
shouldUseManagedDesktopDaemon,
startManagedDaemon,
} from '@/desktop/managed-runtime/managed-runtime'
@@ -18,9 +15,6 @@ const DAEMON_REGISTRY_QUERY_KEY = ['daemon-registry']
const DEFAULT_LOCALHOST_ENDPOINT = 'localhost:6767'
const DEFAULT_LOCALHOST_BOOTSTRAP_KEY = '@paseo:default-localhost-bootstrap-v1'
const DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS = 2500
-const DEFAULT_LOCAL_TRANSPORT_BOOTSTRAP_TIMEOUT_MS = 6000
-const DEFAULT_LOCAL_TRANSPORT_BOOTSTRAP_RETRY_MS = 2000
-const DEFAULT_LOCAL_TRANSPORT_BOOTSTRAP_DEADLINE_MS = 120000
const E2E_STORAGE_KEY = '@paseo:e2e'
export type DirectTcpHostConnection = {
@@ -54,12 +48,7 @@ export type HostConnection =
| DirectPipeHostConnection
| RelayHostConnection
-export type HostLifecycle = {
- managed: boolean
- managedRuntimeId: string | null
- managedRuntimeVersion: string | null
- associatedServerId: string | null
-}
+export type HostLifecycle = Record
export type HostProfile = {
serverId: string
@@ -73,33 +62,9 @@ export type HostProfile = {
export type UpdateHostInput = Partial>
-export type ManagedHostReconciliationInput = {
- serverId: string
- hostname?: string | null
- runtimeId: string
- runtimeVersion: string
- transportType: string
- transportPath: string
- associatedServerId?: string | null
-}
-
-export type LocalhostHostReconciliationInput = {
- serverId: string
- hostname: string | null
- endpoint: string
-}
-
-export type DesktopStartupReconciliationInput = {
- existing: HostProfile[]
- managed: ManagedHostReconciliationInput | null
- localhost: LocalhostHostReconciliationInput | null
- now?: string
-}
-
interface DaemonRegistryContextValue {
daemons: HostProfile[]
isLoading: boolean
- isReconciling: boolean
error: unknown | null
upsertDirectConnection: (input: {
serverId: string
@@ -122,12 +87,7 @@ interface DaemonRegistryContextValue {
const DaemonRegistryContext = createContext(null)
function defaultLifecycle(): HostLifecycle {
- return {
- managed: false,
- managedRuntimeId: null,
- managedRuntimeVersion: null,
- associatedServerId: null,
- }
+ return {}
}
function normalizeHostLabel(value: string | null | undefined, serverId: string): string {
@@ -143,51 +103,48 @@ function normalizeEndpointOrNull(endpoint: string): string | null {
}
}
-function sleep(ms: number): Promise {
- return new Promise((resolve) => {
- setTimeout(resolve, ms)
- })
-}
-
-function normalizeManagedTransportConnection(input: {
- transportType: string
- transportPath: string
-}): HostConnection | null {
- const transportPath = input.transportPath.trim()
- if (!transportPath) {
+export function connectionFromListen(listen: string): HostConnection | null {
+ const normalizedListen = listen.trim()
+ if (!normalizedListen) {
return null
}
- if (input.transportType === 'tcp') {
- try {
- const endpoint = normalizeHostPort(transportPath)
- return {
- id: `direct:${endpoint}`,
- type: 'directTcp',
- endpoint,
- }
- } catch {
- return null
- }
+ if (normalizedListen.startsWith('pipe://')) {
+ const path = normalizedListen.slice('pipe://'.length).trim()
+ return path ? { id: `pipe:${path}`, type: 'directPipe', path } : null
}
- if (input.transportType === 'pipe') {
+ if (normalizedListen.startsWith('unix://')) {
+ const path = normalizedListen.slice('unix://'.length).trim()
+ return path ? { id: `socket:${path}`, type: 'directSocket', path } : null
+ }
+
+ if (normalizedListen.startsWith('\\\\.\\pipe\\')) {
return {
- id: `pipe:${transportPath}`,
+ id: `pipe:${normalizedListen}`,
type: 'directPipe',
- path: transportPath,
+ path: normalizedListen,
}
}
- if (input.transportType === 'socket') {
+ if (normalizedListen.startsWith('/')) {
return {
- id: `socket:${transportPath}`,
+ id: `socket:${normalizedListen}`,
type: 'directSocket',
- path: transportPath,
+ path: normalizedListen,
}
}
- return null
+ try {
+ const endpoint = normalizeHostPort(normalizedListen)
+ return {
+ id: `direct:${endpoint}`,
+ type: 'directTcp',
+ endpoint,
+ }
+ } catch {
+ return null
+ }
}
function normalizeStoredConnection(connection: unknown): HostConnection | null {
@@ -231,19 +188,8 @@ function normalizeStoredConnection(connection: unknown): HostConnection | null {
return null
}
-function normalizeStoredLifecycle(lifecycle: unknown): HostLifecycle {
- const record =
- lifecycle && typeof lifecycle === 'object' ? (lifecycle as Record) : null
-
- return {
- managed: record?.managed === true,
- managedRuntimeId:
- typeof record?.managedRuntimeId === 'string' ? record.managedRuntimeId : null,
- managedRuntimeVersion:
- typeof record?.managedRuntimeVersion === 'string' ? record.managedRuntimeVersion : null,
- associatedServerId:
- typeof record?.associatedServerId === 'string' ? record.associatedServerId : null,
- }
+function normalizeStoredLifecycle(_lifecycle: unknown): HostLifecycle {
+ return defaultLifecycle()
}
function normalizeStoredHostProfile(entry: unknown): HostProfile | null {
@@ -312,19 +258,13 @@ function hostConnectionEquals(left: HostConnection, right: HostConnection): bool
}
function hostLifecycleEquals(left: HostLifecycle, right: HostLifecycle): boolean {
- return (
- left.managed === right.managed &&
- left.managedRuntimeId === right.managedRuntimeId &&
- left.managedRuntimeVersion === right.managedRuntimeVersion &&
- left.associatedServerId === right.associatedServerId
- )
+ return JSON.stringify(left) === JSON.stringify(right)
}
function upsertHostConnectionInProfiles(input: {
profiles: HostProfile[]
serverId: string
label?: string
- lifecycle?: Partial
connection: HostConnection
now?: string
}): HostProfile[] {
@@ -343,10 +283,7 @@ function upsertHostConnectionInProfiles(input: {
const profile: HostProfile = {
serverId,
label: derivedLabel,
- lifecycle: {
- ...defaultLifecycle(),
- ...(input.lifecycle ?? {}),
- },
+ lifecycle: defaultLifecycle(),
connections: [input.connection],
preferredConnectionId: input.connection.id,
createdAt: now,
@@ -371,10 +308,7 @@ function upsertHostConnectionInProfiles(input: {
)
: prev.connections
- const nextLifecycle = {
- ...prev.lifecycle,
- ...(input.lifecycle ?? {}),
- }
+ const nextLifecycle = prev.lifecycle
const nextLabel = labelTrimmed ? labelTrimmed : prev.label
const nextPreferredConnectionId = prev.preferredConnectionId ?? input.connection.id
const changed =
@@ -402,144 +336,6 @@ function upsertHostConnectionInProfiles(input: {
return next
}
-function reconcileManagedHostInProfiles(input: {
- profiles: HostProfile[]
- managed: ManagedHostReconciliationInput
- now?: string
-}): HostProfile[] {
- const connection = normalizeManagedTransportConnection(input.managed)
- if (!connection) {
- throw new Error(`Unsupported managed daemon transport: ${input.managed.transportType}`)
- }
-
- const nextBase = input.profiles.filter((daemon) => {
- return !daemon.lifecycle.managed || daemon.serverId === input.managed.serverId
- })
- const profiles = nextBase.length === input.profiles.length ? input.profiles : nextBase
-
- return upsertHostConnectionInProfiles({
- profiles,
- serverId: input.managed.serverId,
- label: input.managed.hostname ?? undefined,
- lifecycle: {
- managed: true,
- managedRuntimeId: input.managed.runtimeId,
- managedRuntimeVersion: input.managed.runtimeVersion,
- associatedServerId:
- input.managed.associatedServerId?.trim() || input.managed.serverId,
- },
- connection,
- now: input.now,
- })
-}
-
-export function reconcileDesktopStartupRegistry(
- input: DesktopStartupReconciliationInput
-): HostProfile[] {
- let next = input.existing
-
- if (input.managed) {
- next = reconcileManagedHostInProfiles({
- profiles: next,
- managed: input.managed,
- now: input.now,
- })
- }
-
- if (input.localhost) {
- next = upsertHostConnectionInProfiles({
- profiles: next,
- serverId: input.localhost.serverId,
- label: input.localhost.hostname ?? undefined,
- connection: {
- id: `direct:${input.localhost.endpoint}`,
- type: 'directTcp',
- endpoint: input.localhost.endpoint,
- },
- now: input.now,
- })
- }
-
- return next
-}
-
-export async function resolveManagedDesktopStartupStatus(input: {
- settings: Pick
- startManagedDaemonFn?: () => Promise
- getManagedDaemonStatusFn?: () => Promise
-}): Promise {
- if (input.settings.manageBuiltInDaemon) {
- return await (input.startManagedDaemonFn ?? startManagedDaemon)()
- }
-
- return await (input.getManagedDaemonStatusFn ?? getManagedDaemonStatus)()
-}
-
-async function probeManagedStartupTarget(input: {
- managedDaemon: ManagedDaemonStatus
- cancelled?: () => boolean
-}): Promise {
- const connection = normalizeManagedTransportConnection({
- transportType: input.managedDaemon.transportType,
- transportPath: input.managedDaemon.transportPath,
- })
- if (!connection) {
- return null
- }
-
- let serverId = input.managedDaemon.serverId
- let hostname = input.managedDaemon.hostname
-
- if (!serverId) {
- const probed = await probeConnection(connection, {
- timeoutMs: DEFAULT_LOCAL_TRANSPORT_BOOTSTRAP_TIMEOUT_MS,
- })
- if (input.cancelled?.()) {
- throw new Error('Managed daemon bootstrap cancelled')
- }
- serverId = probed.serverId
- hostname = hostname ?? probed.hostname
- }
-
- return {
- serverId,
- hostname,
- runtimeId: input.managedDaemon.runtimeId,
- runtimeVersion: input.managedDaemon.runtimeVersion,
- transportType: input.managedDaemon.transportType,
- transportPath: input.managedDaemon.transportPath,
- associatedServerId: input.managedDaemon.serverId,
- }
-}
-
-async function probeManagedConnectionUntilReady(
- input: {
- managedDaemon: ManagedDaemonStatus
- cancelled?: () => boolean
- }
-): Promise {
- const startedAt = Date.now()
- let lastError: unknown = null
-
- while (Date.now() - startedAt < DEFAULT_LOCAL_TRANSPORT_BOOTSTRAP_DEADLINE_MS) {
- if (input.cancelled?.()) {
- throw new Error('Managed daemon bootstrap cancelled')
- }
-
- try {
- return await probeManagedStartupTarget(input)
- } catch (error) {
- lastError = error
- if (input.cancelled?.()) {
- throw error
- }
- await sleep(DEFAULT_LOCAL_TRANSPORT_BOOTSTRAP_RETRY_MS)
- }
- }
-
- throw lastError ?? new Error('Managed daemon bootstrap timed out')
-}
-
export function hostHasDirectEndpoint(host: HostProfile, endpoint: string): boolean {
const normalized = normalizeEndpointOrNull(endpoint)
if (!normalized) {
@@ -564,10 +360,8 @@ export function useDaemonRegistry(): DaemonRegistryContextValue {
export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
const queryClient = useQueryClient()
- const desktopStartupReconciledRef = useRef(false)
+ const desktopStartupAttemptedRef = useRef(false)
const localhostBootstrapAttemptedRef = useRef(false)
- const [isReconciling, setIsReconciling] = useState(true)
- const { settings, isLoading: settingsLoading } = useAppSettings()
const {
data: daemons = [],
isPending,
@@ -648,7 +442,6 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
async (input: {
serverId: string
label?: string
- lifecycle?: Partial
connection: HostConnection
}) => {
const now = new Date().toISOString()
@@ -656,7 +449,6 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
profiles: readDaemons(),
serverId: input.serverId,
label: input.label,
- lifecycle: input.lifecycle,
connection: input.connection,
now,
})
@@ -684,101 +476,81 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
useEffect(() => {
if (isPending) return
- if (settingsLoading) return
if (!shouldUseManagedDesktopDaemon()) return
- if (desktopStartupReconciledRef.current) return
- desktopStartupReconciledRef.current = true
+ if (desktopStartupAttemptedRef.current) return
+ desktopStartupAttemptedRef.current = true
let cancelled = false
- const reconcileDesktopStartup = async () => {
+ const bootstrapDesktopStartup = async () => {
try {
const isE2E = await AsyncStorage.getItem(E2E_STORAGE_KEY)
if (cancelled || isE2E) {
return
}
- let managed: ManagedHostReconciliationInput | null = null
- try {
- const managedDaemon = await resolveManagedDesktopStartupStatus({
- settings,
- })
- if (managedDaemon.daemonRunning) {
- managed = await probeManagedConnectionUntilReady({
- managedDaemon,
- cancelled: () => cancelled,
- })
- }
- } catch (managedBootstrapError) {
- if (!cancelled) {
- console.warn(
- '[DaemonRegistry] Failed to reconcile managed daemon transport',
- managedBootstrapError
- )
- }
- }
-
- let localhost: LocalhostHostReconciliationInput | null = null
-
- try {
- const { serverId, hostname } = await probeConnection(
- {
- id: `bootstrap:${DEFAULT_LOCALHOST_ENDPOINT}`,
- type: 'directTcp',
- endpoint: DEFAULT_LOCALHOST_ENDPOINT,
- },
- { timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS }
- )
- if (!cancelled) {
- localhost = {
- serverId,
- hostname,
- endpoint: DEFAULT_LOCALHOST_ENDPOINT,
+ await Promise.allSettled([
+ (async () => {
+ const daemon = await startManagedDaemon()
+ if (cancelled || !daemon.serverId) {
+ return
}
- }
- } catch {
- // Best-effort reconciliation only; keep startup resilient if localhost isn't reachable.
- }
- if (cancelled) {
- return
- }
+ const connection = connectionFromListen(daemon.listen)
+ if (!connection) {
+ return
+ }
- const existing = readDaemons()
- const next = reconcileDesktopStartupRegistry({
- existing,
- managed,
- localhost,
- })
+ await upsertHostConnection({
+ serverId: daemon.serverId,
+ label: daemon.hostname ?? undefined,
+ connection,
+ })
+ })().catch((managedBootstrapError) => {
+ if (!cancelled) {
+ console.warn(
+ '[DaemonRegistry] Failed to bootstrap desktop daemon connection',
+ managedBootstrapError
+ )
+ }
+ }),
+ (async () => {
+ const { serverId, hostname } = await probeConnection(
+ {
+ id: `bootstrap:${DEFAULT_LOCALHOST_ENDPOINT}`,
+ type: 'directTcp',
+ endpoint: DEFAULT_LOCALHOST_ENDPOINT,
+ },
+ { timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS }
+ )
+ if (cancelled) {
+ return
+ }
- if (next !== existing) {
- await persist(next)
- }
- } catch (reconciliationError) {
+ await upsertDirectConnection({
+ serverId,
+ endpoint: DEFAULT_LOCALHOST_ENDPOINT,
+ label: hostname ?? undefined,
+ })
+ })().catch(() => {
+ // Best-effort startup race only; ignore if localhost isn't reachable.
+ }),
+ ])
+ } catch (bootstrapError) {
if (cancelled) return
console.warn(
- '[DaemonRegistry] Failed to reconcile desktop startup host connections',
- reconciliationError
+ '[DaemonRegistry] Failed to bootstrap desktop startup host connections',
+ bootstrapError
)
}
}
- void reconcileDesktopStartup().finally(() => {
- if (!cancelled) {
- setIsReconciling(false)
- }
- })
+ void bootstrapDesktopStartup()
return () => {
cancelled = true
}
- }, [
- isPending,
- persist,
- readDaemons,
- settings.manageBuiltInDaemon,
- settingsLoading,
- ])
+ }, [isPending, upsertDirectConnection, upsertHostConnection])
useEffect(() => {
if (isPending) return
@@ -832,20 +604,12 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
}
}
- void bootstrapLocalhost().finally(() => {
- if (!cancelled) {
- setIsReconciling(false)
- }
- })
+ void bootstrapLocalhost()
return () => {
cancelled = true
}
- }, [
- isPending,
- readDaemons,
- upsertDirectConnection,
- ])
+ }, [isPending, readDaemons, upsertDirectConnection])
const upsertRelayConnection = useCallback(
async (input: {
@@ -905,7 +669,6 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
const value: DaemonRegistryContextValue = {
daemons,
isLoading: isPending,
- isReconciling,
error: error ?? null,
upsertDirectConnection,
upsertRelayConnection,
diff --git a/packages/app/src/desktop/components/desktop-updates-section.tsx b/packages/app/src/desktop/components/desktop-updates-section.tsx
index 301d9e010..5e7a85e29 100644
--- a/packages/app/src/desktop/components/desktop-updates-section.tsx
+++ b/packages/app/src/desktop/components/desktop-updates-section.tsx
@@ -1,21 +1,28 @@
-import { useCallback, useEffect, useState } from "react";
-import { ActivityIndicator, Alert, Image, Text, View } from "react-native";
-import * as Clipboard from "expo-clipboard";
-import * as QRCode from "qrcode";
-import { useFocusEffect } from "@react-navigation/native";
-import { StyleSheet, useUnistyles } from "react-native-unistyles";
-import { settingsStyles } from "@/styles/settings";
-import { ArrowUpRight, Play, Pause, RotateCw, Terminal, Copy, FileText, Smartphone } from "lucide-react-native";
-import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
-import { Button } from "@/components/ui/button";
-import { useAppSettings } from "@/hooks/use-settings";
-import { confirmDialog } from "@/utils/confirm-dialog";
-import { openExternalUrl } from "@/utils/open-external-url";
+import { useCallback, useEffect, useState } from 'react'
+import { ActivityIndicator, Alert, Image, Text, View } from 'react-native'
+import * as Clipboard from 'expo-clipboard'
+import * as QRCode from 'qrcode'
+import { useFocusEffect } from '@react-navigation/native'
+import { StyleSheet, useUnistyles } from 'react-native-unistyles'
+import { settingsStyles } from '@/styles/settings'
import {
- formatVersionWithPrefix,
- isVersionMismatch,
-} from "@/desktop/updates/desktop-updates";
+ ArrowUpRight,
+ Play,
+ Pause,
+ RotateCw,
+ Terminal,
+ Copy,
+ FileText,
+ Smartphone,
+} from 'lucide-react-native'
+import { AdaptiveModalSheet } from '@/components/adaptive-modal-sheet'
+import { Button } from '@/components/ui/button'
+import { useAppSettings } from '@/hooks/use-settings'
+import { confirmDialog } from '@/utils/confirm-dialog'
+import { openExternalUrl } from '@/utils/open-external-url'
+import { formatVersionWithPrefix, isVersionMismatch } from '@/desktop/updates/desktop-updates'
import {
+ getCliShimStatus,
getManagedDaemonLogs,
getManagedDaemonPairing,
getManagedDaemonStatus,
@@ -29,307 +36,304 @@ import {
type ManagedPairingOffer,
type ManagedDaemonStatus,
type CliManualInstructions,
-} from "@/desktop/managed-runtime/managed-runtime";
+ type CliShimStatus,
+} from '@/desktop/managed-runtime/managed-runtime'
export interface LocalDaemonSectionProps {
- appVersion: string | null;
+ appVersion: string | null
}
export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
- const { theme } = useUnistyles();
- const showSection = shouldUseManagedDesktopDaemon();
- const { settings, updateSettings } = useAppSettings();
- const [managedStatus, setManagedStatus] = useState(null);
- const [statusError, setStatusError] = useState(null);
- const [isRestartingDaemon, setIsRestartingDaemon] = useState(false);
- const [isUpdatingDaemonManagement, setIsUpdatingDaemonManagement] = useState(false);
- const [isInstallingCli, setIsInstallingCli] = useState(false);
- const [statusMessage, setStatusMessage] = useState(null);
- const [cliStatusMessage, setCliStatusMessage] = useState(null);
- const [managedLogs, setManagedLogs] = useState(null);
- const [isLogsModalOpen, setIsLogsModalOpen] = useState(false);
- const [isPairingModalOpen, setIsPairingModalOpen] = useState(false);
- const [isCliInstallModalOpen, setIsCliInstallModalOpen] = useState(false);
- const [isLoadingPairing, setIsLoadingPairing] = useState(false);
- const [pairingOffer, setPairingOffer] = useState(null);
- const [cliInstallInstructions, setCliInstallInstructions] = useState(
- null
- );
- const [pairingStatusMessage, setPairingStatusMessage] = useState(null);
+ const { theme } = useUnistyles()
+ const showSection = shouldUseManagedDesktopDaemon()
+ const { settings, updateSettings } = useAppSettings()
+ const [managedStatus, setManagedStatus] = useState(null)
+ const [statusError, setStatusError] = useState(null)
+ const [isRestartingDaemon, setIsRestartingDaemon] = useState(false)
+ const [isUpdatingDaemonManagement, setIsUpdatingDaemonManagement] = useState(false)
+ const [isInstallingCli, setIsInstallingCli] = useState(false)
+ const [statusMessage, setStatusMessage] = useState(null)
+ const [cliStatusMessage, setCliStatusMessage] = useState(null)
+ const [managedLogs, setManagedLogs] = useState(null)
+ const [cliShimStatus, setCliShimStatus] = useState(null)
+ const [isLogsModalOpen, setIsLogsModalOpen] = useState(false)
+ const [isPairingModalOpen, setIsPairingModalOpen] = useState(false)
+ const [isCliInstallModalOpen, setIsCliInstallModalOpen] = useState(false)
+ const [isLoadingPairing, setIsLoadingPairing] = useState(false)
+ const [pairingOffer, setPairingOffer] = useState(null)
+ const [cliInstallInstructions, setCliInstallInstructions] =
+ useState(null)
+ const [pairingStatusMessage, setPairingStatusMessage] = useState(null)
const loadManagedStatus = useCallback(() => {
if (!showSection) {
- return Promise.resolve();
+ return Promise.resolve()
}
- return Promise.all([getManagedDaemonStatus(), getManagedDaemonLogs()])
- .then(([status, logs]) => {
- setManagedStatus(status);
- setManagedLogs(logs);
- setStatusError(null);
+ return Promise.all([getManagedDaemonStatus(), getManagedDaemonLogs(), getCliShimStatus()])
+ .then(([status, logs, shimStatus]) => {
+ setManagedStatus(status)
+ setManagedLogs(logs)
+ setCliShimStatus(shimStatus)
+ setStatusError(null)
})
.catch((error) => {
- const message = error instanceof Error ? error.message : String(error);
- setStatusError(message);
- });
- }, [showSection]);
+ const message = error instanceof Error ? error.message : String(error)
+ setStatusError(message)
+ })
+ }, [showSection])
useFocusEffect(
useCallback(() => {
if (!showSection) {
- return undefined;
+ return undefined
}
- void loadManagedStatus();
- return undefined;
+ void loadManagedStatus()
+ return undefined
}, [loadManagedStatus, showSection])
- );
+ )
- const localDaemonVersionText = formatVersionWithPrefix(managedStatus?.runtimeVersion ?? null);
- const daemonVersionMismatch = isVersionMismatch(appVersion, managedStatus?.runtimeVersion ?? null);
+ const localDaemonVersionText = formatVersionWithPrefix(managedStatus?.runtimeVersion ?? null)
+ const daemonVersionMismatch = isVersionMismatch(appVersion, managedStatus?.runtimeVersion ?? null)
const daemonStatusStateText =
- statusError ??
- (managedStatus?.daemonRunning
- ? managedStatus?.daemonStatus ?? "running"
- : "not running");
- const daemonStatusDetailText = `PID ${managedStatus?.daemonPid ? managedStatus.daemonPid : "—"}`;
- const isDaemonManagementPaused = !settings.manageBuiltInDaemon;
- const daemonActionLabel = managedStatus?.daemonRunning ? "Restart daemon" : "Start daemon";
- const daemonActionMessage = managedStatus?.daemonRunning
- ? "Restarts the built-in daemon."
- : isDaemonManagementPaused
- ? "Starts the built-in daemon manually. Paseo will not auto-start it while paused."
- : "Starts the built-in daemon.";
+ statusError ?? (managedStatus?.status === 'running' ? managedStatus.status : 'not running')
+ const daemonStatusDetailText = `PID ${managedStatus?.pid ? managedStatus.pid : '—'}`
+ const isDaemonManagementPaused = !settings.manageBuiltInDaemon
+ const daemonActionLabel = managedStatus?.status === 'running' ? 'Restart daemon' : 'Start daemon'
+ const daemonActionMessage =
+ managedStatus?.status === 'running'
+ ? 'Restarts the built-in daemon.'
+ : 'Starts the built-in daemon.'
const handleUpdateLocalDaemon = useCallback(() => {
if (!showSection) {
- return;
+ return
}
if (isRestartingDaemon) {
- return;
+ return
}
void confirmDialog({
title: daemonActionLabel,
- message: managedStatus?.daemonRunning
- ? "This will restart the built-in daemon. The app will reconnect automatically."
- : "This will start the built-in daemon.",
+ message:
+ managedStatus?.status === 'running'
+ ? 'This will restart the built-in daemon. The app will reconnect automatically.'
+ : 'This will start the built-in daemon.',
confirmLabel: daemonActionLabel,
- cancelLabel: "Cancel",
+ cancelLabel: 'Cancel',
})
.then((confirmed) => {
if (!confirmed) {
- return;
+ return
}
- setIsRestartingDaemon(true);
- setStatusMessage(null);
+ setIsRestartingDaemon(true)
+ setStatusMessage(null)
- const action = managedStatus?.daemonRunning ? restartManagedDaemon : startManagedDaemon;
+ const action =
+ managedStatus?.status === 'running' ? restartManagedDaemon : startManagedDaemon
void action()
.then((status) => {
- setManagedStatus(status);
+ setManagedStatus(status)
setStatusMessage(
- managedStatus?.daemonRunning ? "Daemon restarted." : "Daemon started."
- );
- return loadManagedStatus();
+ managedStatus?.status === 'running' ? 'Daemon restarted.' : 'Daemon started.'
+ )
+ return loadManagedStatus()
})
.catch((error) => {
- console.error("[Settings] Failed to change managed daemon state", error);
- const message = error instanceof Error ? error.message : String(error);
- setStatusMessage(`${daemonActionLabel} failed: ${message}`);
+ console.error('[Settings] Failed to change managed daemon state', error)
+ const message = error instanceof Error ? error.message : String(error)
+ setStatusMessage(`${daemonActionLabel} failed: ${message}`)
})
.finally(() => {
- setIsRestartingDaemon(false);
- });
+ setIsRestartingDaemon(false)
+ })
})
.catch((error) => {
- console.error("[Settings] Failed to open managed daemon action confirmation", error);
- Alert.alert("Error", "Unable to open the daemon confirmation dialog.");
- });
- }, [daemonActionLabel, isRestartingDaemon, loadManagedStatus, managedStatus?.daemonRunning, showSection]);
+ console.error('[Settings] Failed to open managed daemon action confirmation', error)
+ Alert.alert('Error', 'Unable to open the daemon confirmation dialog.')
+ })
+ }, [daemonActionLabel, isRestartingDaemon, loadManagedStatus, managedStatus?.status, showSection])
const handleToggleDaemonManagement = useCallback(() => {
if (isUpdatingDaemonManagement) {
- return;
+ return
}
if (!settings.manageBuiltInDaemon) {
- setIsUpdatingDaemonManagement(true);
- setStatusMessage(null);
+ setIsUpdatingDaemonManagement(true)
+ setStatusMessage(null)
void updateSettings({ manageBuiltInDaemon: true })
.then(() => {
- setStatusMessage("Paseo will resume managing the built-in daemon on startup.");
+ setStatusMessage('Built-in daemon management resumed.')
})
.catch((error) => {
- console.error("[Settings] Failed to update built-in daemon management", error);
- Alert.alert("Error", "Unable to update built-in daemon management.");
+ console.error('[Settings] Failed to update built-in daemon management', error)
+ Alert.alert('Error', 'Unable to update built-in daemon management.')
})
.finally(() => {
- setIsUpdatingDaemonManagement(false);
- });
- return;
+ setIsUpdatingDaemonManagement(false)
+ })
+ return
}
void confirmDialog({
- title: "Pause built-in daemon",
+ title: 'Pause built-in daemon',
message:
- "This will stop the built-in daemon immediately and prevent Paseo from auto-starting it on launch. Running agents and terminals connected to the built-in daemon will be stopped.",
- confirmLabel: "Pause and stop",
- cancelLabel: "Cancel",
+ 'This will stop the built-in daemon immediately. Running agents and terminals connected to the built-in daemon will be stopped.',
+ confirmLabel: 'Pause and stop',
+ cancelLabel: 'Cancel',
destructive: true,
})
.then((confirmed) => {
if (!confirmed) {
- return;
+ return
}
- setIsUpdatingDaemonManagement(true);
- setStatusMessage(null);
+ setIsUpdatingDaemonManagement(true)
+ setStatusMessage(null)
- const stopPromise = managedStatus?.daemonRunning
- ? stopManagedDaemon()
- : Promise.resolve(managedStatus ?? null);
+ const stopPromise =
+ managedStatus?.status === 'running'
+ ? stopManagedDaemon()
+ : Promise.resolve(managedStatus ?? null)
void stopPromise
.then(() => updateSettings({ manageBuiltInDaemon: false }))
.then(() => loadManagedStatus())
.then(() => {
- setStatusMessage(
- "Paseo paused the built-in daemon and will no longer auto-start it on launch."
- );
+ setStatusMessage('Built-in daemon paused and stopped.')
})
.catch((error) => {
- console.error("[Settings] Failed to pause built-in daemon management", error);
- Alert.alert("Error", "Unable to pause built-in daemon management.");
+ console.error('[Settings] Failed to pause built-in daemon management', error)
+ Alert.alert('Error', 'Unable to pause built-in daemon management.')
})
.finally(() => {
- setIsUpdatingDaemonManagement(false);
- });
+ setIsUpdatingDaemonManagement(false)
+ })
})
.catch((error) => {
- console.error("[Settings] Failed to open built-in daemon pause confirmation", error);
- Alert.alert("Error", "Unable to open the daemon confirmation dialog.");
- });
+ console.error('[Settings] Failed to open built-in daemon pause confirmation', error)
+ Alert.alert('Error', 'Unable to open the daemon confirmation dialog.')
+ })
}, [
isUpdatingDaemonManagement,
loadManagedStatus,
managedStatus,
settings.manageBuiltInDaemon,
updateSettings,
- ]);
+ ])
const handleToggleCliShim = useCallback(() => {
if (!showSection || isInstallingCli) {
- return;
+ return
}
- setIsInstallingCli(true);
- const isInstalling = !managedStatus?.cliShimPath;
+ setIsInstallingCli(true)
+ const isInstalling = !cliShimStatus?.path
setCliStatusMessage(
- isInstalling
- ? "A permissions popup may appear while Paseo installs the CLI globally."
- : null
- );
- const action = managedStatus?.cliShimPath ? uninstallManagedCliShim : installManagedCliShim;
+ 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);
+ setCliStatusMessage(result.message)
if (result.manualInstructions) {
- setCliInstallInstructions(result.manualInstructions);
- setIsCliInstallModalOpen(true);
+ setCliInstallInstructions(result.manualInstructions)
+ setIsCliInstallModalOpen(true)
} else {
- setCliInstallInstructions(null);
- setIsCliInstallModalOpen(false);
+ setCliInstallInstructions(null)
+ setIsCliInstallModalOpen(false)
}
- return loadManagedStatus();
+ return loadManagedStatus()
})
.catch((error) => {
- const message = error instanceof Error ? error.message : String(error);
- setCliStatusMessage(`CLI install failed: ${message}`);
+ const message = error instanceof Error ? error.message : String(error)
+ setCliStatusMessage(`CLI install failed: ${message}`)
})
.finally(() => {
- setIsInstallingCli(false);
- });
- }, [isInstallingCli, loadManagedStatus, managedStatus?.cliShimPath, showSection]);
+ setIsInstallingCli(false)
+ })
+ }, [cliShimStatus?.path, isInstallingCli, loadManagedStatus, showSection])
const handleCopyCliInstallCommands = useCallback(() => {
if (!cliInstallInstructions?.commands) {
- return;
+ return
}
void Clipboard.setStringAsync(cliInstallInstructions.commands)
.then(() => {
- Alert.alert("Copied", "CLI install commands copied.");
+ Alert.alert('Copied', 'CLI install commands copied.')
})
.catch((error) => {
- console.error("[Settings] Failed to copy CLI install commands", error);
- Alert.alert("Error", "Unable to copy CLI install commands.");
- });
- }, [cliInstallInstructions?.commands]);
+ console.error('[Settings] Failed to copy CLI install commands', error)
+ Alert.alert('Error', 'Unable to copy CLI install commands.')
+ })
+ }, [cliInstallInstructions?.commands])
const handleCopyLogPath = useCallback(() => {
- const logPath = managedLogs?.logPath ?? managedStatus?.logPath;
+ const logPath = managedLogs?.logPath
if (!logPath) {
- return;
+ return
}
void Clipboard.setStringAsync(logPath)
.then(() => {
- Alert.alert("Copied", "Log path copied.");
+ Alert.alert('Copied', 'Log path copied.')
})
.catch((error) => {
- console.error("[Settings] Failed to copy log path", error);
- Alert.alert("Error", "Unable to copy log path.");
- });
- }, [managedLogs?.logPath, managedStatus?.logPath]);
+ console.error('[Settings] Failed to copy log path', error)
+ Alert.alert('Error', 'Unable to copy log path.')
+ })
+ }, [managedLogs?.logPath])
const handleOpenLogs = useCallback(() => {
if (!managedLogs) {
- return;
+ return
}
- setIsLogsModalOpen(true);
- }, [managedLogs]);
+ setIsLogsModalOpen(true)
+ }, [managedLogs])
const handleOpenPairingModal = useCallback(() => {
if (isLoadingPairing) {
- return;
+ return
}
- setIsPairingModalOpen(true);
- setIsLoadingPairing(true);
- setPairingStatusMessage(null);
+ setIsPairingModalOpen(true)
+ setIsLoadingPairing(true)
+ setPairingStatusMessage(null)
void getManagedDaemonPairing()
.then((pairing) => {
- setPairingOffer(pairing);
+ setPairingOffer(pairing)
if (!pairing.relayEnabled || !pairing.url) {
- setPairingStatusMessage("Relay pairing is not available.");
+ setPairingStatusMessage('Relay pairing is not available.')
}
})
.catch((error) => {
- const message = error instanceof Error ? error.message : String(error);
- setPairingOffer(null);
- setPairingStatusMessage(`Unable to load pairing offer: ${message}`);
+ const message = error instanceof Error ? error.message : String(error)
+ setPairingOffer(null)
+ setPairingStatusMessage(`Unable to load pairing offer: ${message}`)
})
.finally(() => {
- setIsLoadingPairing(false);
- });
- }, [isLoadingPairing]);
+ setIsLoadingPairing(false)
+ })
+ }, [isLoadingPairing])
const handleCopyPairingLink = useCallback(() => {
if (!pairingOffer?.url) {
- return;
+ return
}
void Clipboard.setStringAsync(pairingOffer.url)
.then(() => {
- Alert.alert("Copied", "Pairing link copied.");
+ Alert.alert('Copied', 'Pairing link copied.')
})
.catch((error) => {
- console.error("[Settings] Failed to copy pairing link", error);
- Alert.alert("Error", "Unable to copy pairing link.");
- });
- }, [pairingOffer?.url]);
+ console.error('[Settings] Failed to copy pairing link', error)
+ Alert.alert('Error', 'Unable to copy pairing link.')
+ })
+ }, [pairingOffer?.url])
if (!showSection) {
- return null;
+ return null
}
return (
@@ -364,35 +368,37 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
Daemon management
{isDaemonManagementPaused
- ? "Paused. Paseo will not auto-start the built-in daemon on app launch."
- : "Enabled. Paseo will start the built-in daemon automatically when needed."}
+ ? 'Paused. The built-in daemon stays stopped until you start it again.'
+ : 'Enabled. Paseo can manage the built-in daemon from the desktop app.'}
- : }
+ leftIcon={
+ isDaemonManagementPaused ? (
+
+ ) : (
+
+ )
+ }
onPress={handleToggleDaemonManagement}
disabled={isUpdatingDaemonManagement}
>
{isUpdatingDaemonManagement
? isDaemonManagementPaused
- ? "Resuming..."
- : "Pausing..."
+ ? 'Resuming...'
+ : 'Pausing...'
: isDaemonManagementPaused
- ? "Resume"
- : "Pause"}
+ ? 'Resume'
+ : 'Pause'}
{daemonActionLabel}
{daemonActionMessage}
- {statusMessage ? (
- {statusMessage}
- ) : null}
+ {statusMessage ? {statusMessage} : null}
Command line (CLI)
-
- Adds the `paseo` command to your terminal.
-
+ Adds the `paseo` command to your terminal.
{cliStatusMessage ? {cliStatusMessage} : null}
Log file
-
- {managedLogs?.logPath ??
- managedStatus?.logPath ??
- "Log path unavailable."}
-
+ {managedLogs?.logPath ?? 'Log path unavailable.'}
- {(managedLogs?.logPath ?? managedStatus?.logPath) ? (
- } onPress={handleCopyLogPath}>
+ {managedLogs?.logPath ? (
+ }
+ onPress={handleCopyLogPath}
+ >
Copy path
) : null}
@@ -459,11 +460,14 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
Pair device
-
- Connect your phone to this computer.
-
+ Connect your phone to this computer.
- } onPress={handleOpenPairingModal}>
+ }
+ onPress={handleOpenPairingModal}
+ >
Pair device
@@ -493,7 +497,7 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
{cliInstallInstructions.detail}
) : null}
- {cliInstallInstructions?.commands ?? ""}
+ {cliInstallInstructions?.commands ?? ''}
- );
+ )
}
-const ADVANCED_DAEMON_SETTINGS_URL = "https://paseo.sh/docs/configuration";
+const ADVANCED_DAEMON_SETTINGS_URL = 'https://paseo.sh/docs/configuration'
function PairingOfferDialogContent(input: {
- isLoading: boolean;
- pairingOffer: ManagedPairingOffer | null;
- statusMessage: string | null;
- onCopyLink: () => void;
+ isLoading: boolean
+ pairingOffer: ManagedPairingOffer | null
+ statusMessage: string | null
+ onCopyLink: () => void
}) {
- const { isLoading, pairingOffer, statusMessage, onCopyLink } = input;
- const [qrDataUrl, setQrDataUrl] = useState(null);
- const [qrError, setQrError] = useState(null);
+ const { isLoading, pairingOffer, statusMessage, onCopyLink } = input
+ const [qrDataUrl, setQrDataUrl] = useState(null)
+ const [qrError, setQrError] = useState(null)
useEffect(() => {
- let cancelled = false;
+ let cancelled = false
if (!pairingOffer?.url) {
- setQrDataUrl(null);
- setQrError(null);
+ setQrDataUrl(null)
+ setQrError(null)
return () => {
- cancelled = true;
- };
+ cancelled = true
+ }
}
- setQrError(null);
- setQrDataUrl(null);
+ setQrError(null)
+ setQrDataUrl(null)
void QRCode.toDataURL(pairingOffer.url, {
- errorCorrectionLevel: "M",
+ errorCorrectionLevel: 'M',
margin: 1,
width: 320,
})
.then((dataUrl) => {
if (cancelled) {
- return;
+ return
}
- setQrDataUrl(dataUrl);
+ setQrDataUrl(dataUrl)
})
.catch((error) => {
if (cancelled) {
- return;
+ return
}
- setQrError(error instanceof Error ? error.message : String(error));
- });
+ setQrError(error instanceof Error ? error.message : String(error))
+ })
return () => {
- cancelled = true;
- };
- }, [pairingOffer?.url]);
+ cancelled = true
+ }
+ }, [pairingOffer?.url])
if (isLoading) {
return (
@@ -597,7 +597,7 @@ function PairingOfferDialogContent(input: {
Loading pairing offer…
- );
+ )
}
if (statusMessage) {
@@ -605,7 +605,7 @@ function PairingOfferDialogContent(input: {
{statusMessage}
- );
+ )
}
if (!pairingOffer?.url) {
@@ -613,7 +613,7 @@ function PairingOfferDialogContent(input: {
Pairing offer unavailable.
- );
+ )
}
return (
@@ -640,20 +640,20 @@ function PairingOfferDialogContent(input: {
- );
+ )
}
const styles = StyleSheet.create((theme) => ({
sectionHeader: {
- alignItems: "center",
- flexDirection: "row",
- justifyContent: "space-between",
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
marginBottom: theme.spacing[3],
marginLeft: theme.spacing[1],
},
sectionLink: {
- alignItems: "center",
- flexDirection: "row",
+ alignItems: 'center',
+ flexDirection: 'row',
gap: theme.spacing[1],
},
sectionLinkText: {
@@ -661,9 +661,9 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs,
},
row: {
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "space-between",
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
paddingVertical: theme.spacing[4],
paddingHorizontal: theme.spacing[4],
},
@@ -676,13 +676,13 @@ const styles = StyleSheet.create((theme) => ({
marginRight: theme.spacing[3],
},
actionGroup: {
- flexDirection: "row",
+ flexDirection: 'row',
gap: theme.spacing[2],
- flexWrap: "wrap",
- justifyContent: "flex-end",
+ flexWrap: 'wrap',
+ justifyContent: 'flex-end',
},
statusValueGroup: {
- alignItems: "flex-end",
+ alignItems: 'flex-end',
gap: 2,
},
rowTitle: {
@@ -713,7 +713,7 @@ const styles = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.palette.amber[500],
- backgroundColor: "rgba(245, 158, 11, 0.12)",
+ backgroundColor: 'rgba(245, 158, 11, 0.12)',
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
},
@@ -726,15 +726,15 @@ const styles = StyleSheet.create((theme) => ({
paddingBottom: theme.spacing[2],
},
pairingState: {
- alignItems: "center",
- justifyContent: "center",
+ alignItems: 'center',
+ justifyContent: 'center',
gap: theme.spacing[3],
paddingVertical: theme.spacing[6],
},
qrCard: {
- alignItems: "center",
- justifyContent: "center",
- alignSelf: "center",
+ alignItems: 'center',
+ justifyContent: 'center',
+ alignSelf: 'center',
minHeight: 220,
minWidth: 220,
padding: theme.spacing[4],
@@ -759,13 +759,13 @@ const styles = StyleSheet.create((theme) => ({
logOutput: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
lineHeight: 18,
},
codeBlock: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
lineHeight: 18,
borderWidth: 1,
borderColor: theme.colors.border,
@@ -774,8 +774,8 @@ const styles = StyleSheet.create((theme) => ({
padding: theme.spacing[3],
},
modalActions: {
- flexDirection: "row",
- justifyContent: "flex-end",
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
gap: theme.spacing[2],
},
-}));
+}))
diff --git a/packages/app/src/desktop/managed-runtime/managed-runtime.ts b/packages/app/src/desktop/managed-runtime/managed-runtime.ts
index ef402d2c0..de0aeeb22 100644
--- a/packages/app/src/desktop/managed-runtime/managed-runtime.ts
+++ b/packages/app/src/desktop/managed-runtime/managed-runtime.ts
@@ -1,282 +1,273 @@
-import { invokeDesktopCommand } from "@/desktop/tauri/invoke-desktop-command";
-import { getTauri, isTauriEnvironment } from "@/utils/tauri";
+import { invokeDesktopCommand } from '@/desktop/tauri/invoke-desktop-command'
+import { getTauri, isTauriEnvironment } from '@/utils/tauri'
export type ManagedRuntimeStatus = {
- runtimeId: string;
- runtimeVersion: string;
- runtimeRoot: string;
- managedHome: string;
- transportType: string;
- transportPath: string;
- diagnosticsRoot: string;
- stateFilePath: string;
-};
+ runtimeId: string
+ runtimeVersion: string
+ runtimeRoot: string
+}
export type ManagedDaemonStatus = {
- runtimeId: string;
- runtimeVersion: string;
- runtimeRoot: string;
- managedHome: string;
- transportType: string;
- transportPath: string;
- daemonPid: number | null;
- daemonRunning: boolean;
- daemonStatus: string;
- logPath: string;
- serverId: string | null;
- hostname: string | null;
- relayEnabled: boolean;
- tcpEnabled: boolean;
- tcpListen: string | null;
- cliShimPath: string | null;
-};
+ runtimeId: string
+ runtimeVersion: string
+ serverId: string
+ status: string
+ listen: string
+ hostname: string | null
+ pid: number | null
+ home: string
+}
export type ManagedDaemonLogs = {
- logPath: string;
- contents: string;
-};
+ logPath: string
+ contents: string
+}
+
+export type CliShimStatus = {
+ path: string | null
+}
export type ManagedPairingOffer = {
- relayEnabled: boolean;
- url: string | null;
- qr: string | null;
-};
+ 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;
-};
+ | 'installed'
+ | 'removed'
+ | 'elevationDenied'
+ | 'automaticInstallUnavailable'
+ | 'manualInstallRequired'
+ installed: boolean
+ path: string | null
+ message: string
+ manualInstructions: CliManualInstructions | null
+}
export type CliManualInstructions = {
- title: string;
- detail: string;
- commands: string;
-};
+ title: string
+ detail: string
+ commands: string
+}
export type ManagedTcpSettings = {
- enabled: boolean;
- host: string;
- port: number;
-};
+ enabled: boolean
+ host: string
+ port: number
+}
export type LocalTransportTarget = {
- transportType: "socket" | "pipe";
- transportPath: string;
-};
+ transportType: 'socket' | 'pipe'
+ transportPath: string
+}
type LocalTransportEventPayload = {
- sessionId: string;
- kind: "open" | "message" | "close" | "error";
- text?: string | null;
- binaryBase64?: string | null;
- code?: number | null;
- reason?: string | null;
- error?: string | null;
-};
+ sessionId: string
+ kind: 'open' | 'message' | 'close' | 'error'
+ text?: string | null
+ binaryBase64?: string | null
+ code?: number | null
+ reason?: string | null
+ error?: string | null
+}
function isRecord(value: unknown): value is Record {
- return typeof value === "object" && value !== null;
+ return typeof value === 'object' && value !== null
}
function toStringOrNull(value: unknown): string | null {
- return typeof value === "string" && value.trim().length > 0 ? value : null;
+ return typeof value === 'string' && value.trim().length > 0 ? value : null
}
function toNumberOrNull(value: unknown): number | null {
- return typeof value === "number" && Number.isFinite(value) ? value : null;
+ return typeof value === 'number' && Number.isFinite(value) ? value : null
}
function parseManagedRuntimeStatus(raw: unknown): ManagedRuntimeStatus {
if (!isRecord(raw)) {
- throw new Error("Unexpected managed runtime status response.");
+ throw new Error('Unexpected managed runtime status response.')
}
return {
- runtimeId: toStringOrNull(raw.runtimeId) ?? "",
- runtimeVersion: toStringOrNull(raw.runtimeVersion) ?? "",
- runtimeRoot: toStringOrNull(raw.runtimeRoot) ?? "",
- managedHome: toStringOrNull(raw.managedHome) ?? "",
- transportType: toStringOrNull(raw.transportType) ?? "socket",
- transportPath: toStringOrNull(raw.transportPath) ?? "",
- diagnosticsRoot: toStringOrNull(raw.diagnosticsRoot) ?? "",
- stateFilePath: toStringOrNull(raw.stateFilePath) ?? "",
- };
+ runtimeId: toStringOrNull(raw.runtimeId) ?? '',
+ runtimeVersion: toStringOrNull(raw.runtimeVersion) ?? '',
+ runtimeRoot: toStringOrNull(raw.runtimeRoot) ?? '',
+ }
}
function parseManagedDaemonStatus(raw: unknown): ManagedDaemonStatus {
if (!isRecord(raw)) {
- throw new Error("Unexpected managed daemon status response.");
+ throw new Error('Unexpected managed daemon status response.')
}
return {
- runtimeId: toStringOrNull(raw.runtimeId) ?? "",
- runtimeVersion: toStringOrNull(raw.runtimeVersion) ?? "",
- runtimeRoot: toStringOrNull(raw.runtimeRoot) ?? "",
- managedHome: toStringOrNull(raw.managedHome) ?? "",
- transportType: toStringOrNull(raw.transportType) ?? "socket",
- transportPath: toStringOrNull(raw.transportPath) ?? "",
- daemonPid: toNumberOrNull(raw.daemonPid),
- daemonRunning: raw.daemonRunning === true,
- daemonStatus: toStringOrNull(raw.daemonStatus) ?? "unknown",
- logPath: toStringOrNull(raw.logPath) ?? "",
- serverId: toStringOrNull(raw.serverId),
+ runtimeId: toStringOrNull(raw.runtimeId) ?? '',
+ runtimeVersion: toStringOrNull(raw.runtimeVersion) ?? '',
+ serverId: toStringOrNull(raw.serverId) ?? '',
+ status: toStringOrNull(raw.status) ?? 'unknown',
+ listen: toStringOrNull(raw.listen) ?? '',
hostname: toStringOrNull(raw.hostname),
- relayEnabled: raw.relayEnabled === true,
- tcpEnabled: raw.tcpEnabled === true,
- tcpListen: toStringOrNull(raw.tcpListen),
- cliShimPath: toStringOrNull(raw.cliShimPath),
- };
+ pid: toNumberOrNull(raw.pid),
+ home: toStringOrNull(raw.home) ?? '',
+ }
}
function parseManagedDaemonLogs(raw: unknown): ManagedDaemonLogs {
if (!isRecord(raw)) {
- throw new Error("Unexpected managed daemon logs response.");
+ throw new Error('Unexpected managed daemon logs response.')
}
return {
- logPath: toStringOrNull(raw.logPath) ?? "",
- contents: typeof raw.contents === "string" ? raw.contents : "",
- };
+ logPath: toStringOrNull(raw.logPath) ?? '',
+ contents: typeof raw.contents === 'string' ? raw.contents : '',
+ }
+}
+
+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.");
+ throw new Error('Unexpected managed daemon pairing response.')
}
return {
relayEnabled: raw.relayEnabled === true,
url: toStringOrNull(raw.url),
qr: toStringOrNull(raw.qr),
- };
+ }
}
function parseCliManualInstructions(raw: unknown): CliManualInstructions | null {
if (!isRecord(raw)) {
- return null;
+ return null
}
return {
- title: toStringOrNull(raw.title) ?? "",
- detail: toStringOrNull(raw.detail) ?? "",
- commands: toStringOrNull(raw.commands) ?? "",
- };
+ title: toStringOrNull(raw.title) ?? '',
+ detail: toStringOrNull(raw.detail) ?? '',
+ commands: toStringOrNull(raw.commands) ?? '',
+ }
}
export function parseCliShimResult(raw: unknown): CliShimResult {
if (!isRecord(raw)) {
- throw new Error("Unexpected CLI shim response.");
+ throw new Error('Unexpected CLI shim response.')
}
return {
status:
- (toStringOrNull(raw.status) as CliShimResult["status"] | null) ??
- (raw.installed === true ? "installed" : "removed"),
+ (toStringOrNull(raw.status) as CliShimResult['status'] | null) ??
+ (raw.installed === true ? 'installed' : 'removed'),
installed: raw.installed === true,
path: toStringOrNull(raw.path),
- message: toStringOrNull(raw.message) ?? "",
+ message: toStringOrNull(raw.message) ?? '',
manualInstructions: parseCliManualInstructions(raw.manualInstructions),
- };
+ }
}
export function shouldUseManagedDesktopDaemon(): boolean {
- return isTauriEnvironment() && getTauri() !== null;
+ return isTauriEnvironment() && getTauri() !== null
}
export async function getManagedRuntimeStatus(): Promise {
- return parseManagedRuntimeStatus(await invokeDesktopCommand("managed_runtime_status"));
+ return parseManagedRuntimeStatus(await invokeDesktopCommand('managed_runtime_status'))
}
export async function getManagedDaemonStatus(): Promise {
- return parseManagedDaemonStatus(await invokeDesktopCommand("managed_daemon_status"));
+ return parseManagedDaemonStatus(await invokeDesktopCommand('managed_daemon_status'))
}
export async function startManagedDaemon(): Promise {
- return parseManagedDaemonStatus(await invokeDesktopCommand("start_managed_daemon"));
+ return parseManagedDaemonStatus(await invokeDesktopCommand('start_managed_daemon'))
}
export async function stopManagedDaemon(): Promise {
- return parseManagedDaemonStatus(await invokeDesktopCommand("stop_managed_daemon"));
+ return parseManagedDaemonStatus(await invokeDesktopCommand('stop_managed_daemon'))
}
export async function restartManagedDaemon(): Promise {
- return parseManagedDaemonStatus(await invokeDesktopCommand("restart_managed_daemon"));
+ return parseManagedDaemonStatus(await invokeDesktopCommand('restart_managed_daemon'))
}
export async function getManagedDaemonLogs(): Promise {
- return parseManagedDaemonLogs(await invokeDesktopCommand("managed_daemon_logs"));
+ return parseManagedDaemonLogs(await invokeDesktopCommand('managed_daemon_logs'))
+}
+
+export async function getCliShimStatus(): Promise {
+ return parseCliShimStatus(await invokeDesktopCommand('cli_shim_status'))
}
export async function getManagedDaemonPairing(): Promise {
- return parseManagedPairingOffer(await invokeDesktopCommand("managed_daemon_pairing"));
+ return parseManagedPairingOffer(await invokeDesktopCommand('managed_daemon_pairing'))
}
export async function installManagedCliShim(): Promise {
- return parseCliShimResult(await invokeDesktopCommand("install_cli_shim"));
+ return parseCliShimResult(await invokeDesktopCommand('install_cli_shim'))
}
export async function uninstallManagedCliShim(): Promise {
- return parseCliShimResult(await invokeDesktopCommand("uninstall_cli_shim"));
+ return parseCliShimResult(await invokeDesktopCommand('uninstall_cli_shim'))
}
export async function updateManagedDaemonTcpSettings(
settings: ManagedTcpSettings
): Promise {
return parseManagedDaemonStatus(
- await invokeDesktopCommand("update_managed_daemon_tcp_settings", { settings })
- );
+ await invokeDesktopCommand('update_managed_daemon_tcp_settings', { settings })
+ )
}
-export type LocalTransportEventUnlisten = () => void;
-export type LocalTransportEventHandler = (payload: LocalTransportEventPayload) => void;
+export type LocalTransportEventUnlisten = () => void
+export type LocalTransportEventHandler = (payload: LocalTransportEventPayload) => void
export async function listenToLocalTransportEvents(
handler: LocalTransportEventHandler
): Promise {
- const listen = getTauri()?.event?.listen;
- if (typeof listen !== "function") {
- throw new Error("Tauri event API is unavailable.");
+ const listen = getTauri()?.event?.listen
+ if (typeof listen !== 'function') {
+ throw new Error('Tauri event API is unavailable.')
}
- const unlisten = await listen("local-daemon-transport-event", (event: unknown) => {
- const payload = isRecord(event) && isRecord(event.payload) ? event.payload : null;
+ const unlisten = await listen('local-daemon-transport-event', (event: unknown) => {
+ const payload = isRecord(event) && isRecord(event.payload) ? event.payload : null
if (!payload) {
- return;
+ return
}
handler({
- sessionId: toStringOrNull(payload.sessionId) ?? "",
- kind: (toStringOrNull(payload.kind) ?? "error") as LocalTransportEventPayload["kind"],
+ sessionId: toStringOrNull(payload.sessionId) ?? '',
+ kind: (toStringOrNull(payload.kind) ?? 'error') as LocalTransportEventPayload['kind'],
text: toStringOrNull(payload.text),
binaryBase64: toStringOrNull(payload.binaryBase64),
code: toNumberOrNull(payload.code),
reason: toStringOrNull(payload.reason),
error: toStringOrNull(payload.error),
- });
- });
- return typeof unlisten === "function" ? unlisten : () => {};
+ })
+ })
+ return typeof unlisten === 'function' ? unlisten : () => {}
}
export async function openLocalTransportSession(target: LocalTransportTarget): Promise {
- const raw = await invokeDesktopCommand("open_local_daemon_transport", target);
- if (typeof raw !== "string" || raw.trim().length === 0) {
- throw new Error("Unexpected local transport session response.");
+ const raw = await invokeDesktopCommand('open_local_daemon_transport', target)
+ if (typeof raw !== 'string' || raw.trim().length === 0) {
+ throw new Error('Unexpected local transport session response.')
}
- return raw;
+ return raw
}
export async function sendLocalTransportMessage(input: {
- sessionId: string;
- text?: string;
- binaryBase64?: string;
+ sessionId: string
+ text?: string
+ binaryBase64?: string
}): Promise {
- await invokeDesktopCommand("send_local_daemon_transport_message", {
+ await invokeDesktopCommand('send_local_daemon_transport_message', {
sessionId: input.sessionId,
...(input.text ? { text: input.text } : {}),
...(input.binaryBase64 ? { binaryBase64: input.binaryBase64 } : {}),
- });
+ })
}
export async function closeLocalTransportSession(sessionId: string): Promise {
- await invokeDesktopCommand("close_local_daemon_transport", { sessionId });
+ await invokeDesktopCommand('close_local_daemon_transport', { sessionId })
}
diff --git a/packages/app/src/runtime/host-runtime.test.ts b/packages/app/src/runtime/host-runtime.test.ts
index c4915d5b5..2b983f76e 100644
--- a/packages/app/src/runtime/host-runtime.test.ts
+++ b/packages/app/src/runtime/host-runtime.test.ts
@@ -176,12 +176,7 @@ function makeHost(input?: Partial): HostProfile {
return {
serverId: input?.serverId ?? "srv_test",
label: input?.label ?? "test host",
- lifecycle: input?.lifecycle ?? {
- managed: false,
- managedRuntimeId: null,
- managedRuntimeVersion: null,
- associatedServerId: null,
- },
+ lifecycle: input?.lifecycle ?? {},
connections: input?.connections ?? [direct, relay],
preferredConnectionId: input?.preferredConnectionId ?? direct.id,
createdAt: input?.createdAt ?? new Date(0).toISOString(),
diff --git a/packages/cli/src/commands/daemon/status.ts b/packages/cli/src/commands/daemon/status.ts
index 7f051fc1e..1b27cf85f 100644
--- a/packages/cli/src/commands/daemon/status.ts
+++ b/packages/cli/src/commands/daemon/status.ts
@@ -1,6 +1,7 @@
import type { Command } from 'commander'
import { spawnSync } from 'node:child_process'
import { createRequire } from 'node:module'
+import { getOrCreateServerId } from '@getpaseo/server'
import { tryConnectToDaemon } from '../../utils/client.js'
import type { CommandOptions, ListResult, OutputSchema } from '../../output/index.js'
import { resolveLocalDaemonState, resolveTcpHostFromListen } from './local-daemon.js'
@@ -12,9 +13,11 @@ import {
} from './runtime-toolchain.js'
interface DaemonStatus {
+ serverId: string | null
status: 'running' | 'stopped' | 'unresponsive'
home: string
listen: string
+ hostname: string | null
pid: number | null
startedAt: string | null
owner: string | null
@@ -196,9 +199,11 @@ function createStatusSchema(status: DaemonStatus): OutputSchema {
function toStatusRows(status: DaemonStatus): StatusRow[] {
const rows: StatusRow[] = [
+ { key: 'Server ID', value: status.serverId ?? '-' },
{ key: 'Status', value: status.status },
{ key: 'Home', value: status.home },
{ key: 'Listen', value: status.listen },
+ { key: 'Hostname', value: status.hostname ?? '-' },
{ key: 'PID', value: status.pid === null ? '-' : String(status.pid) },
{ key: 'Started', value: status.startedAt ?? '-' },
{ key: 'Owner', value: status.owner ?? '-' },
@@ -323,10 +328,19 @@ export async function runStatusCommand(
updateStatus = 'unknown (npm unresolved)'
}
+ let serverId: string | null = null
+ try {
+ serverId = getOrCreateServerId(state.home)
+ } catch (error) {
+ note = appendNote(note, `serverId unavailable: ${shortenMessage(normalizeError(error))}`)
+ }
+
const daemonStatus: DaemonStatus = {
+ serverId,
status,
home: state.home,
listen: state.listen,
+ hostname: state.pidInfo?.hostname ?? null,
pid: state.pidInfo?.pid ?? null,
startedAt: state.pidInfo?.startedAt ?? null,
owner,
diff --git a/packages/cli/tests/03-daemon.test.ts b/packages/cli/tests/03-daemon.test.ts
index 502c7223a..1f3a5218e 100644
--- a/packages/cli/tests/03-daemon.test.ts
+++ b/packages/cli/tests/03-daemon.test.ts
@@ -85,8 +85,10 @@ try {
await $`PASEO_HOME=${paseoHome} npx paseo daemon status --json`.nothrow()
assert.strictEqual(result.exitCode, 0, '--json status should succeed')
const status = JSON.parse(result.stdout)
+ assert.strictEqual(typeof status.serverId, 'string', 'json status should include serverId')
assert.strictEqual(status.status, 'stopped', 'json status should report stopped')
assert.strictEqual(status.home, paseoHome, 'json status should reflect the isolated home')
+ assert.strictEqual(status.hostname, null, 'json status should include hostname when unavailable')
console.log('✓ daemon status --json outputs valid JSON\n')
}
diff --git a/packages/desktop/scripts/managed-daemon-smoke.mjs b/packages/desktop/scripts/managed-daemon-smoke.mjs
index c7619bcee..917110ba9 100644
--- a/packages/desktop/scripts/managed-daemon-smoke.mjs
+++ b/packages/desktop/scripts/managed-daemon-smoke.mjs
@@ -588,7 +588,6 @@ try {
const runtimeStatus = await runBinary(packagedBinary, ["--managed-headless", "runtime-status"], managedEnv);
assert.equal(runtimeStatus.json.runtimeId, currentRuntimeId);
assert.equal(runtimeStatus.json.runtimeVersion, currentRuntimeVersion);
- assert.match(runtimeStatus.json.managedHome, new RegExp(`^${escapeForRegExp(testRoot)}`));
assert.match(
runtimeStatus.json.runtimeRoot,
new RegExp(escapeForRegExp(currentRuntimeId)),
@@ -599,15 +598,8 @@ try {
false,
"runtime status should not resolve into managed app data"
);
- assert.ok(runtimeStatus.json.transportType === "socket" || runtimeStatus.json.transportType === "pipe");
- assert.notEqual(runtimeStatus.json.transportPath, "127.0.0.1:6767");
assertNoForbiddenPathsOrPorts(runtimeStatus.json, forbiddenManagedReferences);
assert.equal(await pathExists(managedRuntimeDir), false, "managed app data should not gain a runtime tree");
- const stateAfterRuntimeStatus = JSON.parse(
- await fs.readFile(path.join(testRoot, "managed-state.json"), "utf8")
- );
- assert.equal(stateAfterRuntimeStatus.runtimeId, currentRuntimeId);
- assert.equal(stateAfterRuntimeStatus.runtimeRoot, runtimeStatus.json.runtimeRoot);
const managedBootstrap = await runBinary(packagedBinary, ["--managed-headless", "bootstrap"], managedEnv);
const managedStart = managedBootstrap.json ?? await waitFor(
@@ -617,27 +609,23 @@ try {
["--managed-headless", "daemon-status"],
managedEnv
);
- assert.equal(status.json?.daemonRunning, true);
- assert.ok(status.json?.daemonPid, "managed daemon pid should exist");
+ assert.equal(status.json?.status, "running");
+ assert.ok(status.json?.pid, "managed daemon pid should exist");
+ assert.ok(status.json?.serverId, "managed daemon should expose a server id");
return status.json;
},
10_000,
"managed daemon bootstrap status"
);
- assert.equal(managedStart.daemonRunning, true);
- assert.ok(managedStart.daemonPid, "managed daemon pid should exist");
- assert.equal(managedStart.runtimeRoot, runtimeStatus.json.runtimeRoot);
- assert.ok(
- managedStart.transportType === "socket" || managedStart.transportType === "pipe",
- "managed daemon should default to private IPC transport"
- );
- assert.notEqual(managedStart.transportPath, "127.0.0.1:6767");
+ assert.equal(managedStart.status, "running");
+ assert.ok(managedStart.pid, "managed daemon pid should exist");
+ assert.ok(managedStart.serverId, "managed daemon should expose a server id");
+ assert.ok(managedStart.home, "managed daemon should expose its home directory");
+ assert.ok(managedStart.listen, "managed daemon should expose its listen target");
assertNoForbiddenPathsOrPorts(managedStart, forbiddenManagedReferences);
assert.equal(await pathExists(managedRuntimeDir), false, "starting the daemon should not install a runtime copy");
- const managedPid = managedStart.daemonPid;
- const stateFile = path.join(testRoot, "managed-state.json");
- assert.equal(await pathExists(stateFile), true, "managed state file should be written");
+ const managedPid = managedStart.pid;
logStep("Verifying the managed daemon stays alive after the packaged command exits");
await sleep(1_500);
@@ -646,9 +634,9 @@ try {
["--managed-headless", "daemon-status"],
managedEnv
);
- assert.equal(persistedManagedStatus.json.daemonPid, managedPid);
- assert.equal(persistedManagedStatus.json.daemonRunning, true);
- assert.equal(persistedManagedStatus.json.relayEnabled, true);
+ 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);
@@ -694,7 +682,7 @@ try {
}, "installed CLI shim version check")
: await runBundledRuntimeCli(
runtimeStatus.json.runtimeRoot,
- managedStart.managedHome,
+ managedStart.home,
["--version"],
managedEnv
);
@@ -707,7 +695,7 @@ try {
}, "installed CLI shim daemon status")
: await runBundledRuntimeCli(
runtimeStatus.json.runtimeRoot,
- managedStart.managedHome,
+ managedStart.home,
["daemon", "status", "--json"],
managedEnv
);
@@ -719,7 +707,7 @@ try {
const relayPairing = cliShimInstalled
? await execFileWithTimeout(
cliShimPath,
- ["daemon", "pair", "--home", managedStart.managedHome],
+ ["daemon", "pair"],
{
env: managedEnv,
cwd: repoRoot,
@@ -729,8 +717,8 @@ try {
)
: await runBundledRuntimeCli(
runtimeStatus.json.runtimeRoot,
- managedStart.managedHome,
- ["daemon", "pair", "--home", managedStart.managedHome],
+ managedStart.home,
+ ["daemon", "pair"],
managedEnv
);
const relayOfferUrl = parseOfferUrlFromCommandOutput(relayPairing.stdout);
@@ -745,7 +733,7 @@ try {
["--managed-headless", "bootstrap"],
managedEnv
);
- assert.equal(managedRestartless.json.daemonPid, managedPid);
+ assert.equal(managedRestartless.json.pid, managedPid);
logStep("Verifying managed and external daemons coexist");
const externalStatusAfter = await readDaemonStatus(externalHome, managedEnv);
@@ -761,46 +749,6 @@ try {
assert.ok(managedStatus.json.serverId, "managed daemon should expose a server id");
assertNoForbiddenPathsOrPorts(managedStatus.json, forbiddenManagedReferences);
- logStep("Enabling managed TCP exposure on an explicit non-default port");
- const tcpEnabled = await runBinary(
- packagedBinary,
- [
- "--managed-headless",
- "update-tcp",
- "--enabled",
- "true",
- "--host",
- "127.0.0.1",
- "--port",
- "7771",
- ],
- managedEnv
- );
- assert.equal(tcpEnabled.json.tcpEnabled, true);
- assert.equal(tcpEnabled.json.transportType, "tcp");
- assert.equal(tcpEnabled.json.transportPath, "127.0.0.1:7771");
- assert.notEqual(tcpEnabled.json.transportPath, "127.0.0.1:6767");
- assertNoForbiddenPathsOrPorts(tcpEnabled.json, [fakePaseoHome]);
-
- logStep("Disabling managed TCP exposure and returning to private transport");
- const tcpDisabled = await runBinary(
- packagedBinary,
- [
- "--managed-headless",
- "update-tcp",
- "--enabled",
- "false",
- "--host",
- "127.0.0.1",
- "--port",
- "7771",
- ],
- managedEnv
- );
- assert.equal(tcpDisabled.json.tcpEnabled, false);
- assert.notEqual(tcpDisabled.json.transportType, "tcp");
- assertNoForbiddenPathsOrPorts(tcpDisabled.json, forbiddenManagedReferences);
-
logStep("Capturing diagnostics and verifying the fake ~/.paseo stayed untouched");
const fakePaseoSnapshotAfter = await snapshotTree(fakePaseoHome);
assert.deepEqual(fakePaseoSnapshotAfter, fakePaseoSnapshotBefore);
@@ -811,7 +759,6 @@ try {
runtimeStatus: runtimeStatus.json,
managedBootstrap: managedBootstrap.json,
managedStart,
- stateAfterRuntimeStatus,
persistedManagedStatus: persistedManagedStatus.json,
managedRestartless: managedRestartless.json,
cliInstall: cliInstall.json,
@@ -823,8 +770,6 @@ try {
externalPid,
externalPidAfter,
managedStatus: managedStatus.json,
- tcpEnabled: tcpEnabled.json,
- tcpDisabled: tcpDisabled.json,
},
null,
2
diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs
index 008b397f1..90279009d 100644
--- a/packages/desktop/src-tauri/src/lib.rs
+++ b/packages/desktop/src-tauri/src/lib.rs
@@ -14,11 +14,11 @@ use tauri_plugin_updater::UpdaterExt;
mod runtime_manager;
use runtime_manager::{
- close_local_daemon_transport, install_cli_shim, managed_daemon_logs, managed_daemon_pairing,
- managed_daemon_status, managed_runtime_status, open_local_daemon_transport,
- restart_managed_daemon, send_local_daemon_transport_message, start_managed_daemon,
- stop_managed_daemon, uninstall_cli_shim, update_managed_daemon_tcp_settings,
- LocalTransportState, ManagedTcpSettings,
+ cli_shim_status, close_local_daemon_transport, install_cli_shim, managed_daemon_logs,
+ managed_daemon_pairing, managed_daemon_status, managed_runtime_status,
+ open_local_daemon_transport, restart_managed_daemon, send_local_daemon_transport_message,
+ start_managed_daemon, stop_managed_daemon, uninstall_cli_shim,
+ update_managed_daemon_tcp_settings, LocalTransportState, ManagedTcpSettings,
};
// Store zoom as u64 bits (f64 * 100 as integer for atomic ops)
@@ -622,6 +622,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
managed_runtime_status,
managed_daemon_status,
+ cli_shim_status,
start_managed_daemon,
stop_managed_daemon,
restart_managed_daemon,
diff --git a/packages/desktop/src-tauri/src/runtime_manager.rs b/packages/desktop/src-tauri/src/runtime_manager.rs
index da85c354a..423345a6f 100644
--- a/packages/desktop/src-tauri/src/runtime_manager.rs
+++ b/packages/desktop/src-tauri/src/runtime_manager.rs
@@ -3,9 +3,6 @@ use futures_util::{SinkExt, StreamExt};
use http::Request;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
-use std::env;
-#[cfg(windows)]
-use std::hash::{DefaultHasher, Hash, Hasher};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
@@ -18,18 +15,9 @@ use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use tokio_tungstenite::tungstenite::Message;
const LOCAL_TRANSPORT_EVENT_NAME: &str = "local-daemon-transport-event";
-const MANAGED_STATE_FILE: &str = "managed-state.json";
-const DEFAULT_MANAGED_HOME_BASENAME: &str = ".paseo";
-const DEFAULT_MANAGED_HOME_DIRNAME: &str = "managed-home";
-#[cfg(not(windows))]
-const SHORT_SOCKET_FILENAME: &str = "paseo.sock";
const UNIX_CLIENT_URL: &str = "ws://localhost/ws";
#[cfg(windows)]
const PIPE_CLIENT_URL: &str = "ws://localhost/ws";
-#[cfg(windows)]
-const PIPE_PREFIX: &str = r"\\.\pipe\";
-const DEFAULT_MANAGED_TCP_HOST: &str = "127.0.0.1";
-const DEFAULT_MANAGED_TCP_PORT: u16 = 7771;
const CLI_SHIM_NAME: &str = "paseo";
#[cfg(windows)]
const CLI_SHIM_WINDOWS_NAME: &str = "paseo.cmd";
@@ -55,41 +43,6 @@ struct BundledRuntimePointer {
relative_root: String,
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct ManagedStateFile {
- runtime_id: String,
- runtime_root: String,
- managed_home: String,
- transport_type: String,
- transport_path: String,
- tcp_enabled: bool,
- tcp_listen: Option,
- cli_shim_path: Option,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct PidFile {
- pid: Option,
- hostname: Option,
-}
-
-#[derive(Debug, Clone)]
-struct ManagedPaths {
- managed_home: PathBuf,
- transport_path: PathBuf,
- logs_path: PathBuf,
- state_file_path: PathBuf,
- diagnostics_root: PathBuf,
-}
-
-#[derive(Debug, Clone)]
-struct ManagedTransportTarget {
- transport_type: String,
- transport_path: String,
-}
-
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ManagedTcpSettings {
@@ -104,11 +57,18 @@ pub struct ManagedRuntimeStatus {
pub runtime_id: String,
pub runtime_version: String,
pub runtime_root: String,
- pub managed_home: String,
- pub transport_type: String,
- pub transport_path: String,
- pub diagnostics_root: String,
- pub state_file_path: String,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct CliDaemonStatus {
+ server_id: Option,
+ status: String,
+ listen: String,
+ hostname: Option,
+ pid: Option,
+ home: String,
+ log_path: String,
}
#[derive(Debug, Clone, Serialize)]
@@ -116,20 +76,18 @@ pub struct ManagedRuntimeStatus {
pub struct ManagedDaemonStatus {
pub runtime_id: String,
pub runtime_version: String,
- pub runtime_root: String,
- pub managed_home: String,
- pub transport_type: String,
- pub transport_path: String,
- pub daemon_pid: Option,
- pub daemon_running: bool,
- pub daemon_status: String,
- pub log_path: String,
- pub server_id: Option,
+ pub server_id: String,
+ pub status: String,
+ pub listen: String,
pub hostname: Option,
- pub relay_enabled: bool,
- pub tcp_enabled: bool,
- pub tcp_listen: Option,
- pub cli_shim_path: Option,
+ pub pid: Option,
+ pub home: String,
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CliShimStatus {
+ pub path: Option,
}
#[derive(Debug, Clone, Serialize)]
@@ -345,15 +303,6 @@ mod tests {
fn cli_shim_contents_runs_bundled_runtime_in_place() {
let runtime_root =
PathBuf::from("/Applications/Paseo.app/Contents/Resources/managed-runtime/runtime-1");
- let paths = ManagedPaths {
- managed_home: PathBuf::from("/Users/me/.paseo"),
- transport_path: PathBuf::from("/Users/me/.paseo/paseo.sock"),
- logs_path: PathBuf::from("/Users/me/.paseo/daemon.log"),
- state_file_path: PathBuf::from(
- "/Users/me/Library/Application Support/Paseo/managed-state.json",
- ),
- diagnostics_root: PathBuf::from("/Users/me/Library/Application Support/Paseo"),
- };
let manifest = ManagedRuntimeManifest {
runtime_id: "runtime-1".to_string(),
runtime_version: "0.1.0".to_string(),
@@ -367,11 +316,11 @@ mod tests {
"node_modules/@getpaseo/server/dist/scripts/daemon-runner.js".to_string(),
};
- let contents = cli_shim_contents(&runtime_root, &manifest, &paths);
+ let contents = cli_shim_contents(&runtime_root, &manifest);
- assert!(contents.contains("export PASEO_HOME='/Users/me/.paseo'"));
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"));
}
@@ -383,12 +332,7 @@ mod tests {
.ok()
.filter(|value| !value.trim().is_empty())
.map(PathBuf::from)
- .or_else(|| {
- dirs::home_dir().map(|home| {
- home.join(DEFAULT_MANAGED_HOME_BASENAME)
- .join(SHORT_SOCKET_FILENAME)
- })
- })
+ .or_else(|| dirs::home_dir().map(|home| home.join(".paseo").join("paseo.sock")))
.expect("socket path should resolve");
assert!(
@@ -418,145 +362,6 @@ fn read_json_file Deserialize<'de>>(path: &Path) -> Result(path: &Path, value: &T) -> Result<(), String> {
- let raw = serde_json::to_string_pretty(value)
- .map_err(|error| format!("Failed to serialize {}: {error}", path.display()))?;
- if let Some(parent) = path.parent() {
- fs::create_dir_all(parent)
- .map_err(|error| format!("Failed to create {}: {error}", parent.display()))?;
- }
- fs::write(path, format!("{raw}\n"))
- .map_err(|error| format!("Failed to write {}: {error}", path.display()))
-}
-
-fn app_data_root(app: &AppHandle) -> Result {
- app.path()
- .app_data_dir()
- .map_err(|error| format!("Failed to resolve app data directory: {error}"))
-}
-
-fn resolve_test_root() -> Option {
- std::env::var("PASEO_DESKTOP_TEST_ROOT")
- .ok()
- .map(PathBuf::from)
-}
-
-fn resolve_override_path(name: &str) -> Option {
- std::env::var(name).ok().map(PathBuf::from)
-}
-
-#[cfg(windows)]
-fn current_username() -> String {
- std::env::var("USER")
- .ok()
- .or_else(|| std::env::var("USERNAME").ok())
- .filter(|value| !value.trim().is_empty())
- .unwrap_or_else(|| "user".to_string())
-}
-
-#[cfg(windows)]
-fn hash_seed(value: &str) -> String {
- let mut hasher = DefaultHasher::new();
- value.hash(&mut hasher);
- format!("{:016x}", hasher.finish())
-}
-
-#[cfg(windows)]
-fn build_windows_pipe_path(seed: &str) -> String {
- let user = current_username()
- .chars()
- .map(|character| {
- if character.is_ascii_alphanumeric() {
- character.to_ascii_lowercase()
- } else {
- '-'
- }
- })
- .collect::();
- format!("{PIPE_PREFIX}paseo-managed-{user}-{}", hash_seed(seed))
-}
-
-fn default_transport_type() -> &'static str {
- #[cfg(windows)]
- {
- "pipe"
- }
- #[cfg(not(windows))]
- {
- "socket"
- }
-}
-
-#[cfg(target_os = "macos")]
-fn default_managed_home() -> PathBuf {
- dirs::home_dir()
- .unwrap_or_else(|| PathBuf::from("/tmp"))
- .join(DEFAULT_MANAGED_HOME_BASENAME)
-}
-
-#[cfg(not(target_os = "macos"))]
-fn default_managed_home() -> PathBuf {
- dirs::home_dir()
- .unwrap_or_else(|| PathBuf::from("/tmp"))
- .join(DEFAULT_MANAGED_HOME_BASENAME)
-}
-
-fn default_transport_path(managed_home: &Path, diagnostics_root: &Path) -> PathBuf {
- #[cfg(windows)]
- {
- PathBuf::from(build_windows_pipe_path(
- diagnostics_root.to_string_lossy().as_ref(),
- ))
- }
- #[cfg(target_os = "macos")]
- {
- let _ = diagnostics_root;
- managed_home.join(SHORT_SOCKET_FILENAME)
- }
- #[cfg(all(not(windows), not(target_os = "macos")))]
- {
- let _ = diagnostics_root;
- managed_home.join(SHORT_SOCKET_FILENAME)
- }
-}
-
-fn resolve_paths(app: &AppHandle) -> Result {
- if let Some(test_root) = resolve_test_root() {
- let managed_home = resolve_override_path("PASEO_DESKTOP_MANAGED_HOME")
- .unwrap_or_else(|| test_root.join(DEFAULT_MANAGED_HOME_DIRNAME));
- let transport_path = resolve_override_path("PASEO_DESKTOP_MANAGED_SOCKET_PATH")
- .unwrap_or_else(|| default_transport_path(&managed_home, &test_root));
- return Ok(ManagedPaths {
- managed_home: managed_home.clone(),
- transport_path,
- logs_path: managed_home.join("daemon.log"),
- state_file_path: test_root.join(MANAGED_STATE_FILE),
- diagnostics_root: test_root,
- });
- }
-
- let root = app_data_root(app)?;
- let managed_home = resolve_override_path("PASEO_DESKTOP_MANAGED_HOME").unwrap_or_else(|| {
- #[cfg(target_os = "macos")]
- {
- default_managed_home()
- }
- #[cfg(not(target_os = "macos"))]
- {
- default_managed_home()
- }
- });
- let transport_path = resolve_override_path("PASEO_DESKTOP_MANAGED_SOCKET_PATH")
- .unwrap_or_else(|| default_transport_path(&managed_home, &root));
- Ok(ManagedPaths {
- managed_home: managed_home.clone(),
- transport_path,
- logs_path: managed_home.join("daemon.log"),
- state_file_path: root.join(MANAGED_STATE_FILE),
- diagnostics_root: root,
- })
-}
-
fn dev_resource_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources")
}
@@ -576,10 +381,7 @@ fn bundled_runtime_root(app: &AppHandle) -> Result {
log::info!("[runtime] found bundled runtime at {}", candidate.display());
return Ok(candidate);
}
- log::info!(
- "[runtime] no bundled runtime at {}",
- candidate.display()
- );
+ log::info!("[runtime] no bundled runtime at {}", candidate.display());
}
} else {
log::info!("[runtime] resource_dir() unavailable, checking dev path");
@@ -606,16 +408,6 @@ fn load_runtime_manifest(runtime_root: &Path) -> Result(&runtime_root.join("runtime-manifest.json"))
}
-fn read_server_id(managed_home: &Path) -> Option {
- let raw = fs::read_to_string(managed_home.join("server-id")).ok()?;
- let trimmed = raw.trim();
- if trimmed.is_empty() {
- None
- } else {
- Some(trimmed.to_string())
- }
-}
-
fn tail_log(path: &Path, max_lines: usize) -> String {
let raw = match fs::read_to_string(path) {
Ok(value) => value,
@@ -626,122 +418,14 @@ fn tail_log(path: &Path, max_lines: usize) -> String {
lines.join("\n")
}
-fn is_pid_running(pid: i32) -> bool {
- #[cfg(unix)]
- {
- let output = Command::new("kill")
- .arg("-0")
- .arg(pid.to_string())
- .stdout(Stdio::null())
- .stderr(Stdio::null())
- .status();
- return output.map(|status| status.success()).unwrap_or(false);
- }
- #[cfg(windows)]
- {
- use std::os::windows::process::CommandExt;
- let output = Command::new("tasklist")
- .args(["/FI", &format!("PID eq {pid}"), "/NH", "/FO", "CSV"])
- .stdout(Stdio::piped())
- .stderr(Stdio::null())
- .creation_flags(0x08000000)
- .output();
- return output
- .map(|out| {
- let stdout = String::from_utf8_lossy(&out.stdout);
- stdout.contains(&pid.to_string())
- })
- .unwrap_or(false);
- }
-}
-
fn to_stdio_message(input: Option<&str>) -> String {
input.unwrap_or_default().trim().to_string()
}
-fn parse_tcp_listen(listen: &str) -> Result<(String, u16), String> {
- let trimmed = listen.trim();
- let (host, port_raw) = trimmed
- .rsplit_once(':')
- .ok_or_else(|| "Managed TCP listen target must be host:port.".to_string())?;
- let host = host.trim();
- if host.is_empty() {
- return Err("Managed TCP host cannot be empty.".to_string());
- }
- let port = port_raw
- .trim()
- .parse::()
- .map_err(|_| "Managed TCP port must be a valid integer.".to_string())?;
- if port == 0 {
- return Err("Managed TCP port must be greater than 0.".to_string());
- }
- if port == 6767 {
- return Err("Managed TCP mode cannot use port 6767.".to_string());
- }
- Ok((host.to_string(), port))
-}
-
-fn resolve_tcp_settings_from_state(state: Option<&ManagedStateFile>) -> ManagedTcpSettings {
- let listen = state
- .and_then(|value| value.tcp_listen.clone())
- .unwrap_or_else(|| format!("{DEFAULT_MANAGED_TCP_HOST}:{DEFAULT_MANAGED_TCP_PORT}"));
- let (host, port) = parse_tcp_listen(&listen).unwrap_or((
- DEFAULT_MANAGED_TCP_HOST.to_string(),
- DEFAULT_MANAGED_TCP_PORT,
- ));
- ManagedTcpSettings {
- enabled: state.map(|value| value.tcp_enabled).unwrap_or(false),
- host,
- port,
- }
-}
-
-fn managed_transport_target(
- paths: &ManagedPaths,
- state: Option<&ManagedStateFile>,
-) -> Result {
- let tcp_settings = resolve_tcp_settings_from_state(state);
- if tcp_settings.enabled {
- return Ok(ManagedTransportTarget {
- transport_type: "tcp".to_string(),
- transport_path: format!("{}:{}", tcp_settings.host, tcp_settings.port),
- });
- }
- Ok(ManagedTransportTarget {
- transport_type: default_transport_type().to_string(),
- transport_path: paths.transport_path.to_string_lossy().into_owned(),
- })
-}
-
-fn cli_host_for_target(target: &ManagedTransportTarget) -> String {
- match target.transport_type.as_str() {
- "tcp" => target.transport_path.clone(),
- "pipe" => format!("pipe://{}", target.transport_path),
- _ => format!("unix://{}", target.transport_path),
- }
-}
-
-fn cli_env(paths: &ManagedPaths) -> Vec<(String, String)> {
- let state = read_state_file(&paths.state_file_path);
- let target =
- managed_transport_target(paths, state.as_ref()).unwrap_or(ManagedTransportTarget {
- transport_type: default_transport_type().to_string(),
- transport_path: paths.transport_path.to_string_lossy().into_owned(),
- });
- vec![
- (
- "PASEO_HOME".to_string(),
- paths.managed_home.to_string_lossy().into_owned(),
- ),
- ("PASEO_HOST".to_string(), cli_host_for_target(&target)),
- ]
-}
-
fn cli_command(
runtime_root: &Path,
manifest: &ManagedRuntimeManifest,
args: &[&str],
- paths: &ManagedPaths,
) -> Result {
let node = runtime_root.join(&manifest.node_relative_path);
let cli = runtime_root.join(&manifest.cli_entrypoint_relative_path);
@@ -773,9 +457,6 @@ fn cli_command(
use std::os::windows::process::CommandExt;
command.creation_flags(0x08000000); // CREATE_NO_WINDOW
}
- for (key, value) in cli_env(paths) {
- command.env(key, value);
- }
Ok(command)
}
@@ -814,18 +495,13 @@ fn outer_cli_shim_path() -> Result {
}
}
-fn cli_shim_contents(
- runtime_root: &Path,
- manifest: &ManagedRuntimeManifest,
- paths: &ManagedPaths,
-) -> String {
+fn cli_shim_contents(runtime_root: &Path, manifest: &ManagedRuntimeManifest) -> String {
let node = runtime_root.join(&manifest.node_relative_path);
let cli = runtime_root.join(&manifest.cli_entrypoint_relative_path);
#[cfg(windows)]
{
return format!(
- "@echo off\r\nset \"PASEO_HOME={}\"\r\n\"{}\" \"{}\" %*\r\n",
- paths.managed_home.display(),
+ "@echo off\r\n\"{}\" \"{}\" %*\r\n",
node.display(),
cli.display()
);
@@ -833,8 +509,7 @@ fn cli_shim_contents(
#[cfg(not(windows))]
{
format!(
- "#!/bin/sh\nexport PASEO_HOME={}\nexec \"{}\" \"{}\" \"$@\"\n",
- shell_single_quote(paths.managed_home.to_string_lossy().as_ref()),
+ "#!/bin/sh\nexec \"{}\" \"{}\" \"$@\"\n",
node.display(),
cli.display()
)
@@ -859,11 +534,10 @@ fn write_cli_launcher(path: &Path, contents: &str) -> Result<(), String> {
fn cli_manual_instructions(
runtime_root: &Path,
- paths: &ManagedPaths,
manifest: &ManagedRuntimeManifest,
) -> Result {
let outer_shim = outer_cli_shim_path()?;
- let contents = cli_shim_contents(runtime_root, manifest, paths);
+ let contents = cli_shim_contents(runtime_root, manifest);
#[cfg(windows)]
{
let target_dir = outer_shim
@@ -907,91 +581,29 @@ fn cli_manual_instructions(
fn detect_installed_cli_shim_path(
runtime_root: &Path,
- paths: &ManagedPaths,
manifest: &ManagedRuntimeManifest,
) -> Option {
let outer_shim = outer_cli_shim_path().ok()?;
- let expected = cli_shim_contents(runtime_root, manifest, paths);
+ 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 read_pid_from_home(managed_home: &Path) -> Option {
- read_json_file::(&managed_home.join("paseo.pid"))
- .ok()
- .and_then(|parsed| parsed.pid)
-}
-
-fn read_hostname_from_home(managed_home: &Path) -> Option {
- read_json_file::(&managed_home.join("paseo.pid"))
- .ok()
- .and_then(|parsed| parsed.hostname)
-}
-
-fn read_state_file(path: &Path) -> Option {
- read_json_file::(path).ok()
-}
-
-fn write_state_file(path: &Path, value: &ManagedStateFile) -> Result<(), String> {
- write_json_file(path, value)
-}
-
fn ensure_runtime_ready_internal(app: &AppHandle) -> Result {
log::info!("[runtime] ensuring runtime is ready");
let (bundled_root, pointer) = load_bundled_runtime_pointer(app)?;
let runtime_root = bundled_root.join(&pointer.relative_root);
- let paths = resolve_paths(app)?;
- log::info!(
- "[runtime] runtime_root={} managed_home={} transport={}",
- runtime_root.display(),
- paths.managed_home.display(),
- paths.transport_path.display()
- );
let manifest = load_runtime_manifest(&runtime_root)?;
log::info!(
"[runtime] manifest: id={} version={}",
manifest.runtime_id,
manifest.runtime_version
);
- if let Some(parent) = paths.transport_path.parent() {
- fs::create_dir_all(parent)
- .map_err(|error| format!("Failed to create {}: {error}", parent.display()))?;
- }
- fs::create_dir_all(&paths.managed_home)
- .map_err(|error| format!("Failed to create {}: {error}", paths.managed_home.display()))?;
- let existing_state = read_state_file(&paths.state_file_path);
- let target = managed_transport_target(&paths, existing_state.as_ref())?;
- log::info!(
- "[runtime] transport target: type={} path={}",
- target.transport_type,
- target.transport_path
- );
- let state = ManagedStateFile {
- runtime_id: manifest.runtime_id.clone(),
- runtime_root: runtime_root.to_string_lossy().into_owned(),
- managed_home: paths.managed_home.to_string_lossy().into_owned(),
- transport_type: target.transport_type.clone(),
- transport_path: target.transport_path.clone(),
- tcp_enabled: existing_state
- .as_ref()
- .map(|entry| entry.tcp_enabled)
- .unwrap_or(false),
- tcp_listen: existing_state
- .as_ref()
- .and_then(|entry| entry.tcp_listen.clone()),
- cli_shim_path: existing_state.and_then(|entry| entry.cli_shim_path),
- };
- write_state_file(&paths.state_file_path, &state)?;
Ok(ManagedRuntimeStatus {
runtime_id: manifest.runtime_id,
runtime_version: manifest.runtime_version,
runtime_root: runtime_root.to_string_lossy().into_owned(),
- managed_home: paths.managed_home.to_string_lossy().into_owned(),
- transport_type: state.transport_type,
- transport_path: state.transport_path,
- diagnostics_root: paths.diagnostics_root.to_string_lossy().into_owned(),
- state_file_path: paths.state_file_path.to_string_lossy().into_owned(),
})
}
@@ -999,10 +611,9 @@ fn run_cli_json_command(
runtime_root: &Path,
manifest: &ManagedRuntimeManifest,
args: &[&str],
- paths: &ManagedPaths,
) -> Result {
log::info!("[cli] running: {:?}", args);
- let output = cli_command(runtime_root, manifest, args, paths)?
+ let output = cli_command(runtime_root, manifest, args)?
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
@@ -1036,70 +647,30 @@ fn run_cli_json_command(
fn managed_daemon_status_internal(app: &AppHandle) -> Result {
let status = ensure_runtime_ready_internal(app)?;
- let paths = resolve_paths(app)?;
let runtime_root = PathBuf::from(&status.runtime_root);
let manifest = load_runtime_manifest(&runtime_root)?;
- let cli_shim_path = detect_installed_cli_shim_path(&runtime_root, &paths, &manifest);
- let state = read_state_file(&paths.state_file_path);
- let target = managed_transport_target(&paths, state.as_ref())?;
- let tcp_settings = resolve_tcp_settings_from_state(state.as_ref());
- let cli_status = run_cli_json_command(
- &runtime_root,
- &manifest,
- &[
- "daemon",
- "status",
- "--home",
- &paths.managed_home.to_string_lossy(),
- "--json",
- ],
- &paths,
- )
- .ok();
-
- let daemon_status = cli_status
- .as_ref()
- .and_then(|value| value.get("status"))
- .and_then(|value| value.as_str())
- .unwrap_or(if read_pid_from_home(&paths.managed_home).is_some() {
- "running"
- } else {
- "stopped"
- })
- .to_string();
- let daemon_pid = cli_status
- .as_ref()
- .and_then(|value| value.get("pid"))
- .and_then(|value| {
- value
- .as_i64()
- .or_else(|| value.as_str().and_then(|raw| raw.parse::().ok()))
- })
- .or_else(|| read_pid_from_home(&paths.managed_home));
- let daemon_running = daemon_pid
- .map(|pid| is_pid_running(pid as i32))
- .unwrap_or(false)
- || daemon_status == "running";
+ let value = run_cli_json_command(&runtime_root, &manifest, &["daemon", "status", "--json"])?;
+ let daemon_status: CliDaemonStatus = serde_json::from_value(value)
+ .map_err(|error| format!("Failed to parse managed daemon status: {error}"))?;
Ok(ManagedDaemonStatus {
runtime_id: manifest.runtime_id,
runtime_version: manifest.runtime_version,
- runtime_root: runtime_root.to_string_lossy().into_owned(),
- managed_home: paths.managed_home.to_string_lossy().into_owned(),
- transport_type: target.transport_type,
- transport_path: target.transport_path,
- daemon_pid,
- daemon_running,
- daemon_status,
- log_path: paths.logs_path.to_string_lossy().into_owned(),
- server_id: read_server_id(&paths.managed_home),
- hostname: read_hostname_from_home(&paths.managed_home),
- relay_enabled: true,
- tcp_enabled: tcp_settings.enabled,
- tcp_listen: tcp_settings
- .enabled
- .then(|| format!("{}:{}", tcp_settings.host, tcp_settings.port)),
- cli_shim_path,
+ server_id: daemon_status.server_id.unwrap_or_default(),
+ status: daemon_status.status,
+ listen: daemon_status.listen,
+ hostname: daemon_status.hostname,
+ pid: daemon_status.pid,
+ home: daemon_status.home,
+ })
+}
+
+fn cli_shim_status_internal(app: &AppHandle) -> Result {
+ 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),
})
}
@@ -1176,12 +747,11 @@ fn remove_cli_shim_via_macos_prompt(shim_path: &Path) -> Result<(), String> {
fn install_cli_shim_internal(app: &AppHandle) -> Result {
log::info!("[cli-shim] installing CLI shim");
let status = ensure_runtime_ready_internal(app)?;
- let paths = resolve_paths(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, &paths);
- let manual_instructions = cli_manual_instructions(&runtime_root, &paths, &manifest)?;
+ 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);
@@ -1192,27 +762,13 @@ fn install_cli_shim_internal(app: &AppHandle) -> Result {
log::info!("[cli-shim] target path: {}", shim_path.display());
match install_result {
- Ok(()) => {
- let mut state = read_state_file(&paths.state_file_path).unwrap_or(ManagedStateFile {
- runtime_id: status.runtime_id.clone(),
- runtime_root: runtime_root.to_string_lossy().into_owned(),
- managed_home: paths.managed_home.to_string_lossy().into_owned(),
- transport_type: status.transport_type.clone(),
- transport_path: status.transport_path.clone(),
- tcp_enabled: false,
- tcp_listen: None,
- cli_shim_path: None,
- });
- state.cli_shim_path = Some(shim_path.to_string_lossy().into_owned());
- write_state_file(&paths.state_file_path, &state)?;
- 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,
- })
- }
+ 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,
@@ -1241,15 +797,10 @@ fn install_cli_shim_internal(app: &AppHandle) -> Result {
fn uninstall_cli_shim_internal(app: &AppHandle) -> Result {
let status = ensure_runtime_ready_internal(app)?;
- let paths = resolve_paths(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, &paths, &manifest)
+ let shim_path = detect_installed_cli_shim_path(&runtime_root, &manifest)
.map(PathBuf::from)
- .or_else(|| {
- read_state_file(&paths.state_file_path)
- .and_then(|entry| entry.cli_shim_path.map(PathBuf::from))
- })
.unwrap_or(outer_cli_shim_path()?);
if shim_path.exists() {
#[cfg(target_os = "macos")]
@@ -1261,18 +812,6 @@ fn uninstall_cli_shim_internal(app: &AppHandle) -> Result
fs::remove_file(&shim_path)
.map_err(|error| format!("Failed to remove {}: {error}", shim_path.display()))?;
}
- let mut state = read_state_file(&paths.state_file_path).unwrap_or(ManagedStateFile {
- runtime_id: status.runtime_id.clone(),
- runtime_root: runtime_root.to_string_lossy().into_owned(),
- managed_home: paths.managed_home.to_string_lossy().into_owned(),
- transport_type: status.transport_type.clone(),
- transport_path: status.transport_path.clone(),
- tcp_enabled: false,
- tcp_listen: None,
- cli_shim_path: None,
- });
- state.cli_shim_path = None;
- write_state_file(&paths.state_file_path, &state)?;
Ok(CliShimResult {
status: "removed".to_string(),
@@ -1286,48 +825,16 @@ fn uninstall_cli_shim_internal(app: &AppHandle) -> Result
fn start_managed_daemon_internal(app: &AppHandle) -> Result {
log::info!("[daemon] starting managed daemon");
let status = ensure_runtime_ready_internal(app)?;
- let paths = resolve_paths(app)?;
- let existing_status = managed_daemon_status_internal(app)?;
- if existing_status.daemon_running {
- log::info!(
- "[daemon] already running (pid={:?})",
- existing_status.daemon_pid
- );
- return Ok(existing_status);
- }
- if let Some(parent) = paths.transport_path.parent() {
- fs::create_dir_all(parent)
- .map_err(|error| format!("Failed to create {}: {error}", parent.display()))?;
- }
let runtime_root = PathBuf::from(&status.runtime_root);
let manifest = load_runtime_manifest(&runtime_root)?;
- let state = read_state_file(&paths.state_file_path);
- let target = managed_transport_target(&paths, state.as_ref())?;
- log::info!(
- "[daemon] spawning: home={} listen={} (type={})",
- paths.managed_home.display(),
- target.transport_path,
- target.transport_type
- );
- let output = cli_command(
- &runtime_root,
- &manifest,
- &[
- "start",
- "--home",
- &paths.managed_home.to_string_lossy(),
- "--listen",
- &target.transport_path,
- ],
- &paths,
- )?
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .output()
- .map_err(|error| {
- log::error!("[daemon] failed to spawn: {error}");
- format!("Failed to launch managed daemon: {error}")
- })?;
+ let output = cli_command(&runtime_root, &manifest, &["start"])?
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .map_err(|error| {
+ log::error!("[daemon] failed to spawn: {error}");
+ format!("Failed to launch managed daemon: {error}")
+ })?;
if !output.status.success() {
let stderr = to_stdio_message(Some(&String::from_utf8_lossy(&output.stderr)));
log::error!(
@@ -1342,13 +849,13 @@ fn start_managed_daemon_internal(app: &AppHandle) -> Result Result Result {
log::info!("[daemon] stopping managed daemon");
let status = ensure_runtime_ready_internal(app)?;
- let paths = resolve_paths(app)?;
let runtime_root = PathBuf::from(&status.runtime_root);
let manifest = load_runtime_manifest(&runtime_root)?;
- let output = cli_command(
- &runtime_root,
- &manifest,
- &[
- "daemon",
- "stop",
- "--home",
- &paths.managed_home.to_string_lossy(),
- "--json",
- ],
- &paths,
- )?
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .output()
- .map_err(|error| {
- log::error!("[daemon] failed to spawn stop command: {error}");
- format!("Failed to stop managed daemon: {error}")
- })?;
+ let output = cli_command(&runtime_root, &manifest, &["daemon", "stop", "--json"])?
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .map_err(|error| {
+ log::error!("[daemon] failed to spawn stop command: {error}");
+ format!("Failed to stop managed daemon: {error}")
+ })?;
if !output.status.success() {
let stderr = to_stdio_message(Some(&String::from_utf8_lossy(&output.stderr)));
log::error!(
@@ -1403,37 +898,16 @@ fn stop_managed_daemon_internal(app: &AppHandle) -> Result Result {
log::info!("[daemon] restarting managed daemon");
let status = ensure_runtime_ready_internal(app)?;
- let paths = resolve_paths(app)?;
let runtime_root = PathBuf::from(&status.runtime_root);
let manifest = load_runtime_manifest(&runtime_root)?;
- let state = read_state_file(&paths.state_file_path);
- let target = managed_transport_target(&paths, state.as_ref())?;
- log::info!(
- "[daemon] restart: listen={} (type={})",
- target.transport_path,
- target.transport_type
- );
- let output = cli_command(
- &runtime_root,
- &manifest,
- &[
- "daemon",
- "restart",
- "--home",
- &paths.managed_home.to_string_lossy(),
- "--listen",
- &target.transport_path,
- "--json",
- ],
- &paths,
- )?
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .output()
- .map_err(|error| {
- log::error!("[daemon] failed to spawn restart command: {error}");
- format!("Failed to restart managed daemon: {error}")
- })?;
+ let output = cli_command(&runtime_root, &manifest, &["daemon", "restart", "--json"])?
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .map_err(|error| {
+ log::error!("[daemon] failed to spawn restart command: {error}");
+ format!("Failed to restart managed daemon: {error}")
+ })?;
if !output.status.success() {
let stderr = to_stdio_message(Some(&String::from_utf8_lossy(&output.stderr)));
log::error!(
@@ -1453,40 +927,10 @@ fn restart_managed_daemon_internal(app: &AppHandle) -> Result Result {
- log::info!(
- "[tcp] updating settings: enabled={} host={} port={}",
- settings.enabled,
- settings.host,
- settings.port
- );
- if settings.enabled {
- parse_tcp_listen(&format!("{}:{}", settings.host.trim(), settings.port))?;
- }
- let status = ensure_runtime_ready_internal(app)?;
- let paths = resolve_paths(app)?;
- let mut state = read_state_file(&paths.state_file_path).unwrap_or(ManagedStateFile {
- runtime_id: status.runtime_id.clone(),
- runtime_root: status.runtime_root.clone(),
- managed_home: paths.managed_home.to_string_lossy().into_owned(),
- transport_type: status.transport_type.clone(),
- transport_path: status.transport_path.clone(),
- tcp_enabled: false,
- tcp_listen: None,
- cli_shim_path: None,
- });
- state.tcp_enabled = settings.enabled;
- state.tcp_listen = Some(format!("{}:{}", settings.host.trim(), settings.port));
- let target = managed_transport_target(&paths, Some(&state))?;
- state.transport_type = target.transport_type;
- state.transport_path = target.transport_path;
- write_state_file(&paths.state_file_path, &state)?;
-
- if managed_daemon_status_internal(app)?.daemon_running {
- return restart_managed_daemon_internal(app);
- }
- managed_daemon_status_internal(app)
+ let _ = app;
+ Err("Managed daemon TCP settings are no longer configurable from desktop.".to_string())
}
#[tauri::command]
@@ -1524,14 +968,27 @@ pub async fn restart_managed_daemon(app: AppHandle) -> Result Result {
+ tauri::async_runtime::spawn_blocking(move || cli_shim_status_internal(&app))
+ .await
+ .map_err(|error| format!("CLI shim status task failed: {error}"))?
+}
+
#[tauri::command]
pub async fn managed_daemon_logs(app: AppHandle) -> Result {
tauri::async_runtime::spawn_blocking(move || {
- ensure_runtime_ready_internal(&app)?;
- let paths = resolve_paths(&app)?;
+ let runtime_status = ensure_runtime_ready_internal(&app)?;
+ let runtime_root = PathBuf::from(&runtime_status.runtime_root);
+ let manifest = load_runtime_manifest(&runtime_root)?;
+ let value =
+ run_cli_json_command(&runtime_root, &manifest, &["daemon", "status", "--json"])?;
+ let daemon_status: CliDaemonStatus = serde_json::from_value(value)
+ .map_err(|error| format!("Failed to parse managed daemon status for logs: {error}"))?;
+ let log_path = PathBuf::from(&daemon_status.log_path);
Ok(ManagedDaemonLogs {
- log_path: paths.logs_path.to_string_lossy().into_owned(),
- contents: tail_log(&paths.logs_path, 400),
+ log_path: log_path.to_string_lossy().into_owned(),
+ contents: tail_log(&log_path, 400),
})
})
.await
@@ -1542,21 +999,9 @@ pub async fn managed_daemon_logs(app: AppHandle) -> Result Result {
tauri::async_runtime::spawn_blocking(move || {
let status = ensure_runtime_ready_internal(&app)?;
- let paths = resolve_paths(&app)?;
let runtime_root = PathBuf::from(&status.runtime_root);
let manifest = load_runtime_manifest(&runtime_root)?;
- let value = run_cli_json_command(
- &runtime_root,
- &manifest,
- &[
- "daemon",
- "pair",
- "--home",
- &paths.managed_home.to_string_lossy(),
- "--json",
- ],
- &paths,
- )?;
+ let value = run_cli_json_command(&runtime_root, &manifest, &["daemon", "pair", "--json"])?;
serde_json::from_value::(value)
.map_err(|error| format!("Failed to parse managed pairing offer: {error}"))
})
diff --git a/packages/server/src/server/exports.ts b/packages/server/src/server/exports.ts
index dee16d888..df7ea3411 100644
--- a/packages/server/src/server/exports.ts
+++ b/packages/server/src/server/exports.ts
@@ -2,6 +2,7 @@
export { createPaseoDaemon, type PaseoDaemon, type PaseoDaemonConfig } from "./bootstrap.js";
export { loadConfig, type CliConfigOverrides } from "./config.js";
export { resolvePaseoHome } from "./paseo-home.js";
+export { getOrCreateServerId } from "./server-id.js";
export { createRootLogger, type LogLevel, type LogFormat } from "./logger.js";
export { loadPersistedConfig, type PersistedConfig } from "./persisted-config.js";
export { generateLocalPairingOffer, type LocalPairingOffer } from "./pairing-offer.js";
diff --git a/windows-startup-refactor-plan.md b/windows-startup-refactor-plan.md
new file mode 100644
index 000000000..663b9b151
--- /dev/null
+++ b/windows-startup-refactor-plan.md
@@ -0,0 +1,161 @@
+# Desktop Startup Refactor Plan
+
+## Goal
+
+1. Tauri runs `paseo start`, then polls `paseo daemon status --json` until ready
+2. `daemon status --json` returns ALL needed data: serverId, listen address, hostname, status, pid
+3. App takes that data and adds it to the registry like any other host
+4. App blocks on startup splash until any host runtime goes online (race)
+5. 30s timeout → redirect to welcome/add-connection screen
+
+## Core Principles
+
+- The daemon is autonomous. Tauri does not pass `--home`, `--listen`, or read any files.
+- `daemon status --json` is the single source of truth. All data comes from it.
+- The host registry has no concept of "managed" daemons. A host is just a serverId + connections.
+
+---
+
+## Implementation Plan
+
+### Phase 1: Add serverId to `daemon status --json`
+
+**File: `packages/cli/src/commands/daemon/status.ts`**
+
+`DaemonStatus` currently has: `status`, `home`, `listen`, `pid`, `startedAt`, `owner`, `logPath`, etc.
+
+Add:
+- `serverId: string | null`
+- `hostname: string | null` (already available from pidInfo, just not in the output)
+
+The status command already calls `resolveLocalDaemonState()` which reads the pid file (has hostname) and resolves home. From home it can call `getOrCreateServerId(home)` to get the serverId. All internal to the status command — Tauri never touches these files.
+
+### Phase 2: Tauri — just start + poll status
+
+**File: `packages/desktop/src-tauri/src/runtime_manager.rs`**
+
+#### 2a. `start_managed_daemon_internal` becomes:
+
+1. Run `paseo start` (no `--home`, no `--listen`)
+2. Poll `paseo daemon status --json` in a loop until `status == "running"` and `serverId` is present
+3. Return the parsed JSON to the app
+
+#### 2b. Remove all daemon plumbing from Rust
+
+Delete:
+- `managed_transport_target()`, `ManagedTransportTarget`
+- `default_transport_path()`, `default_transport_type()`
+- `default_managed_home()`
+- `resolve_paths()` / `ManagedPaths`
+- `read_server_id()`, `read_hostname_from_home()`, `read_pid_from_home()`
+- `cli_env()` — no `PASEO_HOME` or `PASEO_HOST` injection
+- State file reading/writing for transport config
+- All direct file I/O for daemon state
+
+Keep:
+- `bundled_runtime_root()` — to find bundled Node.js + CLI
+- `cli_command()` — to invoke the bundled CLI
+- `ensure_runtime_ready_internal()` — to verify runtime extraction
+
+#### 2c. `ManagedDaemonStatus` mirrors `daemon status --json`
+
+- `serverId: string`
+- `status: string`
+- `listen: string`
+- `hostname: string | null`
+- `pid: number | null`
+- `home: string`
+- Plus: `runtimeId`, `runtimeVersion` (from bundled runtime manifest)
+
+App parses `listen` to determine transport type:
+- `\\.\pipe\*` or `pipe://` → pipe
+- `/` or `unix://` → socket
+- `host:port` → tcp
+
+### Phase 3: App — remove "managed" concept from registry
+
+**File: `packages/app/src/contexts/daemon-registry-context.tsx`**
+
+#### 3a. Delete managed-specific code
+
+- `ManagedHostReconciliationInput`
+- `DesktopStartupReconciliationInput`
+- `reconcileManagedHostInProfiles()`
+- `reconcileDesktopStartupRegistry()`
+- `probeManagedConnectionUntilReady()`
+- `probeManagedStartupTarget()`
+- `resolveManagedDesktopStartupStatus()`
+- `DEFAULT_LOCAL_TRANSPORT_BOOTSTRAP_*` constants
+
+#### 3b. Remove `managed` from `HostLifecycle`
+
+Remove `managed`, `managedRuntimeId`, `managedRuntimeVersion`, `associatedServerId`. Remove all code that checks `lifecycle.managed`.
+
+#### 3c. Desktop startup effect becomes:
+
+```typescript
+const bootstrap = async () => {
+ const daemon = await startManagedDaemon()
+ if (cancelled || !daemon.serverId) return
+
+ const connection = connectionFromListen(daemon.listen)
+ if (!connection) return
+
+ await upsertHostConnection({
+ serverId: daemon.serverId,
+ label: daemon.hostname ?? undefined,
+ connection,
+ })
+}
+```
+
+#### 3d. Remove `isReconciling`
+
+No longer needed. Registry update triggers `MultiDaemonSessionHost` → `syncHosts()` → `HostRuntimeController` connects.
+
+### Phase 4: Index screen — race until online
+
+**File: `packages/app/src/app/index.tsx`**
+
+- New hook: `useAnyHostOnline(serverIds)` — subscribes to `HostRuntimeStore`
+- Show splash while: registry loading OR (hosts exist, none online, within 30s)
+- Any host goes online → navigate to it
+- 30s timeout → WelcomeScreen
+
+Both managed daemon and localhost:6767 race. First to connect wins.
+
+### Phase 5: Localhost bootstrap
+
+Non-desktop: keep the lightweight probe (2.5s, needs serverId from handshake).
+Desktop: localhost:6767 added as a race participant alongside the daemon connection.
+
+### Phase 6: Metro must ignore app test files
+
+**Files: `packages/app/metro.config.js` and any directly related Metro resolver helpers/tests**
+
+The app already contains many `*.test.ts` and `*.test.tsx` files under `packages/app/src`. Those files must remain in the repo and must NOT be deleted to make Metro happy.
+
+Implement Metro-side protection so Expo/Metro does not try to bundle app test files from `packages/app/src` during normal app/web startup:
+
+- Treat `*.test.ts`, `*.test.tsx`, `*.spec.ts`, and `*.spec.tsx` under `packages/app/src` as ignored/non-bundle inputs for Metro app resolution
+- If the current Metro resolver hook needs to special-case local imports or extensions, update it so test files are skipped cleanly instead of causing bundle resolution crashes
+- Preserve Vitest usage for those files; the change is only about Metro bundling behavior, not removing or renaming tests
+- If a test index/helper file is needed for app tests, keep it and make Metro ignore it rather than deleting it
+
+---
+
+## What stays the same
+
+- `HostRuntimeController`, `HostRuntimeStore`, `MultiDaemonSessionHost` — no changes
+- `DaemonClient` transports — no changes
+- The daemon itself — no changes (already autonomous)
+
+## Acceptance Criteria
+
+1. `paseo daemon status --json` returns `serverId`, `listen`, `hostname`, `status`, and `pid`, and desktop startup consumes that shape as the source of truth
+2. The app registry no longer contains managed-daemon lifecycle concepts; desktop startup upserts the managed daemon like any other host
+3. Desktop startup blocks on the startup splash during the connection race and only leaves it when a host goes online or the 30s timeout expires
+4. Desktop still races the managed daemon and `localhost:6767`; non-desktop keeps the lightweight localhost probe
+5. Metro does not try to bundle `packages/app/src/**/*.test.*` or `packages/app/src/**/*.spec.*` during normal app startup
+6. Existing app test files, including any `index.test.ts` helper needed by tests, remain in the repo and are not deleted as a workaround
+7. `npm run typecheck` passes