refactor(desktop): simplify managed runtime by removing state file and delegating to CLI daemon status

This commit is contained in:
Mohamed Boudra
2026-03-11 19:01:49 +07:00
parent 327b315610
commit 4c6d21af4a
16 changed files with 1010 additions and 1785 deletions
+8
View File
@@ -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 (
+41
View File
@@ -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
}
+109 -54
View File
@@ -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 <StartupSplashScreen />;
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 <StartupSplashScreen />
}
if (!targetServerId) {
if (isReconciling) {
return <StartupSplashScreen />;
}
return <WelcomeScreen />;
if (!onlineServerId) {
return <WelcomeScreen />
}
return null;
return null
}
+5
View File
@@ -0,0 +1,5 @@
import { WelcomeScreen } from '@/components/welcome-screen'
export default function WelcomeRoute() {
return <WelcomeScreen />
}
@@ -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<HostProfile> & Pick<HostProfile, 'serverId'>):
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<HostProfile> & Pick<HostProfile, 'serverId'>):
}
}
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)
})
})
@@ -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<string, never>
export type HostProfile = {
serverId: string
@@ -73,33 +62,9 @@ export type HostProfile = {
export type UpdateHostInput = Partial<Omit<HostProfile, 'serverId' | 'createdAt'>>
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<DaemonRegistryContextValue | null>(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<void> {
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<string, unknown>) : 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<HostLifecycle>
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<AppSettings, 'manageBuiltInDaemon'>
startManagedDaemonFn?: () => Promise<ManagedDaemonStatus>
getManagedDaemonStatusFn?: () => Promise<ManagedDaemonStatus>
}): Promise<ManagedDaemonStatus> {
if (input.settings.manageBuiltInDaemon) {
return await (input.startManagedDaemonFn ?? startManagedDaemon)()
}
return await (input.getManagedDaemonStatusFn ?? getManagedDaemonStatus)()
}
async function probeManagedStartupTarget(input: {
managedDaemon: ManagedDaemonStatus
cancelled?: () => boolean
}): Promise<ManagedHostReconciliationInput | null> {
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<ManagedHostReconciliationInput | null> {
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<HostLifecycle>
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,
@@ -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<ManagedDaemonStatus | null>(null);
const [statusError, setStatusError] = useState<string | null>(null);
const [isRestartingDaemon, setIsRestartingDaemon] = useState(false);
const [isUpdatingDaemonManagement, setIsUpdatingDaemonManagement] = useState(false);
const [isInstallingCli, setIsInstallingCli] = useState(false);
const [statusMessage, setStatusMessage] = useState<string | null>(null);
const [cliStatusMessage, setCliStatusMessage] = useState<string | null>(null);
const [managedLogs, setManagedLogs] = useState<ManagedDaemonLogs | null>(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<ManagedPairingOffer | null>(null);
const [cliInstallInstructions, setCliInstallInstructions] = useState<CliManualInstructions | null>(
null
);
const [pairingStatusMessage, setPairingStatusMessage] = useState<string | null>(null);
const { theme } = useUnistyles()
const showSection = shouldUseManagedDesktopDaemon()
const { settings, updateSettings } = useAppSettings()
const [managedStatus, setManagedStatus] = useState<ManagedDaemonStatus | null>(null)
const [statusError, setStatusError] = useState<string | null>(null)
const [isRestartingDaemon, setIsRestartingDaemon] = useState(false)
const [isUpdatingDaemonManagement, setIsUpdatingDaemonManagement] = useState(false)
const [isInstallingCli, setIsInstallingCli] = useState(false)
const [statusMessage, setStatusMessage] = useState<string | null>(null)
const [cliStatusMessage, setCliStatusMessage] = useState<string | null>(null)
const [managedLogs, setManagedLogs] = useState<ManagedDaemonLogs | null>(null)
const [cliShimStatus, setCliShimStatus] = useState<CliShimStatus | null>(null)
const [isLogsModalOpen, setIsLogsModalOpen] = useState(false)
const [isPairingModalOpen, setIsPairingModalOpen] = useState(false)
const [isCliInstallModalOpen, setIsCliInstallModalOpen] = useState(false)
const [isLoadingPairing, setIsLoadingPairing] = useState(false)
const [pairingOffer, setPairingOffer] = useState<ManagedPairingOffer | null>(null)
const [cliInstallInstructions, setCliInstallInstructions] =
useState<CliManualInstructions | null>(null)
const [pairingStatusMessage, setPairingStatusMessage] = useState<string | null>(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) {
<Text style={styles.rowTitle}>Daemon management</Text>
<Text style={styles.hintText}>
{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.'}
</Text>
</View>
<Button
variant="outline"
size="sm"
leftIcon={isDaemonManagementPaused
? <Play size={theme.iconSize.sm} color={theme.colors.foreground} />
: <Pause size={theme.iconSize.sm} color={theme.colors.foreground} />}
leftIcon={
isDaemonManagementPaused ? (
<Play size={theme.iconSize.sm} color={theme.colors.foreground} />
) : (
<Pause size={theme.iconSize.sm} color={theme.colors.foreground} />
)
}
onPress={handleToggleDaemonManagement}
disabled={isUpdatingDaemonManagement}
>
{isUpdatingDaemonManagement
? isDaemonManagementPaused
? "Resuming..."
: "Pausing..."
? 'Resuming...'
: 'Pausing...'
: isDaemonManagementPaused
? "Resume"
: "Pause"}
? 'Resume'
: 'Pause'}
</Button>
</View>
<View style={[styles.row, styles.rowBorder]}>
<View style={styles.rowContent}>
<Text style={styles.rowTitle}>{daemonActionLabel}</Text>
<Text style={styles.hintText}>{daemonActionMessage}</Text>
{statusMessage ? (
<Text style={styles.statusText}>{statusMessage}</Text>
) : null}
{statusMessage ? <Text style={styles.statusText}>{statusMessage}</Text> : null}
</View>
<Button
variant="outline"
@@ -402,18 +408,16 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
disabled={isRestartingDaemon}
>
{isRestartingDaemon
? managedStatus?.daemonRunning
? "Restarting..."
: "Starting..."
? managedStatus?.status === 'running'
? 'Restarting...'
: 'Starting...'
: daemonActionLabel}
</Button>
</View>
<View style={[styles.row, styles.rowBorder]}>
<View style={styles.rowContent}>
<Text style={styles.rowTitle}>Command line (CLI)</Text>
<Text style={styles.hintText}>
Adds the `paseo` command to your terminal.
</Text>
<Text style={styles.hintText}>Adds the `paseo` command to your terminal.</Text>
{cliStatusMessage ? <Text style={styles.statusText}>{cliStatusMessage}</Text> : null}
</View>
<Button
@@ -423,25 +427,22 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
onPress={handleToggleCliShim}
disabled={isInstallingCli}
>
{isInstallingCli
? "Working..."
: managedStatus?.cliShimPath
? "Uninstall CLI"
: "Install CLI"}
{isInstallingCli ? 'Working...' : cliShimStatus?.path ? 'Uninstall CLI' : 'Install CLI'}
</Button>
</View>
<View style={[styles.row, styles.rowBorder]}>
<View style={styles.rowContent}>
<Text style={styles.rowTitle}>Log file</Text>
<Text style={styles.hintText}>
{managedLogs?.logPath ??
managedStatus?.logPath ??
"Log path unavailable."}
</Text>
<Text style={styles.hintText}>{managedLogs?.logPath ?? 'Log path unavailable.'}</Text>
</View>
<View style={styles.actionGroup}>
{(managedLogs?.logPath ?? managedStatus?.logPath) ? (
<Button variant="outline" size="sm" leftIcon={<Copy size={theme.iconSize.sm} color={theme.colors.foreground} />} onPress={handleCopyLogPath}>
{managedLogs?.logPath ? (
<Button
variant="outline"
size="sm"
leftIcon={<Copy size={theme.iconSize.sm} color={theme.colors.foreground} />}
onPress={handleCopyLogPath}
>
Copy path
</Button>
) : null}
@@ -459,11 +460,14 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
<View style={[styles.row, styles.rowBorder]}>
<View style={styles.rowContent}>
<Text style={styles.rowTitle}>Pair device</Text>
<Text style={styles.hintText}>
Connect your phone to this computer.
</Text>
<Text style={styles.hintText}>Connect your phone to this computer.</Text>
</View>
<Button variant="outline" size="sm" leftIcon={<Smartphone size={theme.iconSize.sm} color={theme.colors.foreground} />} onPress={handleOpenPairingModal}>
<Button
variant="outline"
size="sm"
leftIcon={<Smartphone size={theme.iconSize.sm} color={theme.colors.foreground} />}
onPress={handleOpenPairingModal}
>
Pair device
</Button>
</View>
@@ -493,7 +497,7 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
<Text style={styles.hintText}>{cliInstallInstructions.detail}</Text>
) : null}
<Text style={styles.codeBlock} selectable>
{cliInstallInstructions?.commands ?? ""}
{cliInstallInstructions?.commands ?? ''}
</Text>
<View style={styles.modalActions}>
<Button variant="outline" size="sm" onPress={() => setIsCliInstallModalOpen(false)}>
@@ -525,71 +529,67 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
onClose={() => setIsLogsModalOpen(false)}
title="Daemon logs"
testID="managed-daemon-logs-dialog"
snapPoints={["70%", "92%"]}
snapPoints={['70%', '92%']}
>
<View style={styles.modalBody}>
<Text style={styles.hintText}>
{managedLogs?.logPath ??
managedStatus?.logPath ??
"Log path unavailable."}
</Text>
<Text style={styles.hintText}>{managedLogs?.logPath ?? 'Log path unavailable.'}</Text>
<Text style={styles.logOutput} selectable>
{managedLogs?.contents.length ? managedLogs.contents : "(log file is empty)"}
{managedLogs?.contents.length ? managedLogs.contents : '(log file is empty)'}
</Text>
</View>
</AdaptiveModalSheet>
</View>
);
)
}
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<string | null>(null);
const [qrError, setQrError] = useState<string | null>(null);
const { isLoading, pairingOffer, statusMessage, onCopyLink } = input
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null)
const [qrError, setQrError] = useState<string | null>(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: {
<ActivityIndicator size="small" />
<Text style={styles.hintText}>Loading pairing offer…</Text>
</View>
);
)
}
if (statusMessage) {
@@ -605,7 +605,7 @@ function PairingOfferDialogContent(input: {
<View style={styles.modalBody}>
<Text style={styles.hintText}>{statusMessage}</Text>
</View>
);
)
}
if (!pairingOffer?.url) {
@@ -613,7 +613,7 @@ function PairingOfferDialogContent(input: {
<View style={styles.modalBody}>
<Text style={styles.hintText}>Pairing offer unavailable.</Text>
</View>
);
)
}
return (
@@ -640,20 +640,20 @@ function PairingOfferDialogContent(input: {
</Button>
</View>
</View>
);
)
}
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],
},
}));
}))
@@ -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<string, unknown> {
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<ManagedRuntimeStatus> {
return parseManagedRuntimeStatus(await invokeDesktopCommand("managed_runtime_status"));
return parseManagedRuntimeStatus(await invokeDesktopCommand('managed_runtime_status'))
}
export async function getManagedDaemonStatus(): Promise<ManagedDaemonStatus> {
return parseManagedDaemonStatus(await invokeDesktopCommand("managed_daemon_status"));
return parseManagedDaemonStatus(await invokeDesktopCommand('managed_daemon_status'))
}
export async function startManagedDaemon(): Promise<ManagedDaemonStatus> {
return parseManagedDaemonStatus(await invokeDesktopCommand("start_managed_daemon"));
return parseManagedDaemonStatus(await invokeDesktopCommand('start_managed_daemon'))
}
export async function stopManagedDaemon(): Promise<ManagedDaemonStatus> {
return parseManagedDaemonStatus(await invokeDesktopCommand("stop_managed_daemon"));
return parseManagedDaemonStatus(await invokeDesktopCommand('stop_managed_daemon'))
}
export async function restartManagedDaemon(): Promise<ManagedDaemonStatus> {
return parseManagedDaemonStatus(await invokeDesktopCommand("restart_managed_daemon"));
return parseManagedDaemonStatus(await invokeDesktopCommand('restart_managed_daemon'))
}
export async function getManagedDaemonLogs(): Promise<ManagedDaemonLogs> {
return parseManagedDaemonLogs(await invokeDesktopCommand("managed_daemon_logs"));
return parseManagedDaemonLogs(await invokeDesktopCommand('managed_daemon_logs'))
}
export async function getCliShimStatus(): Promise<CliShimStatus> {
return parseCliShimStatus(await invokeDesktopCommand('cli_shim_status'))
}
export async function getManagedDaemonPairing(): Promise<ManagedPairingOffer> {
return parseManagedPairingOffer(await invokeDesktopCommand("managed_daemon_pairing"));
return parseManagedPairingOffer(await invokeDesktopCommand('managed_daemon_pairing'))
}
export async function installManagedCliShim(): Promise<CliShimResult> {
return parseCliShimResult(await invokeDesktopCommand("install_cli_shim"));
return parseCliShimResult(await invokeDesktopCommand('install_cli_shim'))
}
export async function uninstallManagedCliShim(): Promise<CliShimResult> {
return parseCliShimResult(await invokeDesktopCommand("uninstall_cli_shim"));
return parseCliShimResult(await invokeDesktopCommand('uninstall_cli_shim'))
}
export async function updateManagedDaemonTcpSettings(
settings: ManagedTcpSettings
): Promise<ManagedDaemonStatus> {
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<LocalTransportEventUnlisten> {
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<string> {
const raw = await invokeDesktopCommand<unknown>("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<unknown>('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<void> {
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<void> {
await invokeDesktopCommand("close_local_daemon_transport", { sessionId });
await invokeDesktopCommand('close_local_daemon_transport', { sessionId })
}
@@ -176,12 +176,7 @@ function makeHost(input?: Partial<HostProfile>): 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(),
@@ -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<StatusRow> {
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,
+2
View File
@@ -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')
}
@@ -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
+6 -5
View File
@@ -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,
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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";
+161
View File
@@ -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