Add managed desktop daemon runtime support

This commit is contained in:
Mohamed Boudra
2026-03-08 15:40:02 +07:00
parent faa5aaab6f
commit 2d5d0dcacd
35 changed files with 5377 additions and 421 deletions
@@ -289,7 +289,12 @@ export function resolveTcpHostFromListen(listen: string): string | null {
return null
}
if (normalized.startsWith('/') || normalized.startsWith('unix://')) {
if (
normalized.startsWith('/') ||
normalized.startsWith('unix://') ||
normalized.startsWith('pipe://') ||
normalized.startsWith('\\\\.\\pipe\\')
) {
return null
}
+19 -2
View File
@@ -4,14 +4,16 @@ import { generateLocalPairingOffer, loadConfig, resolvePaseoHome } from '@getpas
interface PairOptions {
home?: string
json?: boolean
}
export function pairCommand(): Command {
return new Command('pair')
.description('Print the daemon pairing QR code and link')
.option('--json', 'Output in JSON format')
.option('--home <path>', 'Paseo home directory (default: ~/.paseo)')
.action(async (options: PairOptions) => {
await runPairCommand(options)
.action(async (_options: PairOptions, command: Command) => {
await runPairCommand(command.optsWithGlobals() as PairOptions)
})
}
@@ -37,6 +39,21 @@ export async function runPairCommand(options: PairOptions): Promise<void> {
process.exit(1)
}
if (options.json) {
process.stdout.write(
`${JSON.stringify(
{
relayEnabled: pairing.relayEnabled,
url: pairing.url,
qr: pairing.qr,
},
null,
2
)}\n`
)
return
}
const qrBlock = pairing.qr ? `${pairing.qr}\n` : ''
process.stdout.write(`\nScan to pair:\n${qrBlock}${pairing.url}\n`)
}
+58 -5
View File
@@ -10,6 +10,17 @@ export interface ConnectOptions {
const DEFAULT_HOST = 'localhost:6767'
const DEFAULT_TIMEOUT = 5000
type DaemonTarget =
| {
type: 'tcp'
url: string
}
| {
type: 'ipc'
url: string
socketPath: string
}
/**
* Get the daemon host from environment or options
*/
@@ -17,12 +28,49 @@ export function getDaemonHost(options?: ConnectOptions): string {
return options?.host ?? process.env.PASEO_HOST ?? DEFAULT_HOST
}
export function resolveDaemonTarget(host: string): DaemonTarget {
const trimmed = host.trim()
if (
trimmed.startsWith('unix://') ||
trimmed.startsWith('pipe://') ||
trimmed.startsWith('\\\\.\\pipe\\')
) {
const socketPath = trimmed.startsWith('unix://')
? trimmed.slice('unix://'.length).trim()
: trimmed.startsWith('pipe://')
? trimmed.slice('pipe://'.length).trim()
: trimmed
if (!socketPath) {
throw new Error('Invalid IPC daemon target: missing socket path')
}
const isUnixSocket = trimmed.startsWith('unix://')
return {
type: 'ipc',
url: isUnixSocket
? `ws+unix://${socketPath}:/ws`
: 'ws://localhost/ws',
socketPath,
}
}
return {
type: 'tcp',
url: `ws://${trimmed}/ws`,
}
}
/**
* Create a WebSocket factory that works in Node.js
*/
function createNodeWebSocketFactory() {
return (url: string, options?: { headers?: Record<string, string> }) => {
return new WebSocket(url, { headers: options?.headers }) as unknown as {
return (
url: string,
options?: { headers?: Record<string, string>; socketPath?: string }
) => {
return new WebSocket(url, {
headers: options?.headers,
...(options?.socketPath ? { socketPath: options.socketPath } : {}),
}) as unknown as {
readyState: number
send: (data: string | Uint8Array | ArrayBuffer) => void
close: (code?: number, reason?: string) => void
@@ -41,14 +89,19 @@ export async function connectToDaemon(options?: ConnectOptions): Promise<DaemonC
const host = getDaemonHost(options)
const timeout = options?.timeout ?? DEFAULT_TIMEOUT
const clientId = await getOrCreateCliClientId()
const url = `ws://${host}/ws`
const target = resolveDaemonTarget(host)
const nodeWebSocketFactory = createNodeWebSocketFactory()
const client = new DaemonClient(
{
url,
url: target.url,
clientId,
clientType: 'cli',
webSocketFactory: createNodeWebSocketFactory(),
webSocketFactory: (url: string, config?: { headers?: Record<string, string> }) =>
nodeWebSocketFactory(url, {
headers: config?.headers,
...(target.type === 'ipc' ? { socketPath: target.socketPath } : {}),
}),
reconnect: { enabled: false },
} as unknown as ConstructorParameters<typeof DaemonClient>[0]
)