one
Version:
One is a new React Framework that makes Vite serve both native and web.
253 lines (210 loc) • 7.98 kB
text/typescript
// one daemon CLI command
import colors from 'picocolors'
import { labelProcess } from './label-process'
export async function daemon(args: {
subcommand?: string
port?: string
host?: string
app?: string
slot?: string
project?: string
tui?: boolean
}) {
const subcommand = args.subcommand || 'run'
switch (subcommand) {
case 'run':
case 'start':
return daemonStart(args)
case 'stop':
return daemonStop()
case 'status':
return daemonStatus()
case 'route':
return daemonRoute(args)
default:
console.log(`Unknown daemon subcommand: ${subcommand}`)
console.log('Available: start, stop, status, route')
process.exit(1)
}
}
async function daemonStart(args: { port?: string; host?: string; tui?: boolean }) {
labelProcess('daemon')
const { isDaemonRunning } = await import('../daemon/ipc')
if (await isDaemonRunning()) {
console.log(colors.yellow('Daemon is already running'))
console.log("Use 'one daemon status' to see registered servers")
process.exit(1)
}
// suggest tray app if available
await suggestTrayApp()
const { startDaemon } = await import('../daemon/server')
// default to TUI if running in interactive terminal
const useTUI = args.tui ?? process.stdin.isTTY
const { state } = await startDaemon({
port: args.port ? parseInt(args.port, 10) : undefined,
host: args.host,
quiet: useTUI, // suppress normal logs when TUI is active
})
if (useTUI) {
const { startTUI } = await import('../daemon/tui')
startTUI(state)
}
}
async function daemonStop() {
const { isDaemonRunning, getSocketPath, cleanupSocket } = await import('../daemon/ipc')
if (!(await isDaemonRunning())) {
console.log(colors.yellow('Daemon is not running'))
process.exit(1)
}
// send shutdown signal via IPC
// for now, just cleanup socket and let user stop the process manually
console.log(
colors.yellow(
'Note: daemon runs in foreground. Press Ctrl+C in the daemon terminal to stop.'
)
)
console.log(colors.dim(`Socket path: ${getSocketPath()}`))
}
async function daemonStatus() {
const { isDaemonRunning, getDaemonStatus, getLastActiveDaemonServer } =
await import('../daemon/ipc')
if (!(await isDaemonRunning())) {
console.log(colors.yellow('Daemon is not running'))
console.log(colors.dim("Start with 'one daemon'"))
process.exit(1)
}
try {
const status = await getDaemonStatus()
const lastActive = await getLastActiveDaemonServer()
console.log(colors.cyan('\n═══════════════════════════════════════════════════'))
console.log(colors.cyan(' one daemon status'))
console.log(colors.cyan('═══════════════════════════════════════════════════\n'))
if (status.servers.length === 0) {
console.log(colors.dim(' No servers registered'))
} else {
console.log(' Registered servers:')
for (const server of status.servers) {
const shortRoot = server.root.replace(process.env.HOME || '', '~')
const isActive = lastActive?.id === server.id
const activeMarker = isActive ? colors.yellow(' ★') : ''
console.log(
` ${colors.green(server.id)} ${server.bundleId} → :${server.port} (${shortRoot})${activeMarker}`
)
}
if (lastActive) {
console.log(colors.dim('\n ★ = last active (used by oi/oa)'))
}
}
if (status.routes.length > 0) {
console.log('\n Active routes:')
for (const route of status.routes) {
console.log(` ${route.key} → ${route.serverId}`)
}
}
console.log('')
} catch (err) {
console.log(colors.red('Failed to get daemon status'))
console.error(err)
process.exit(1)
}
}
export async function openPlatform(platform: 'ios' | 'android') {
const { isDaemonRunning, getDaemonStatus, setDaemonRoute, touchDaemonServer } =
await import('../daemon/ipc')
const { getBundleIdFromConfig } = await import('../daemon/utils')
const cwd = process.cwd()
const bundleId = getBundleIdFromConfig(cwd)
if (!bundleId) {
console.log(colors.yellow('No app.json found in current directory'))
console.log(colors.dim('Run this command from a One project directory'))
process.exit(1)
}
// if daemon is running, pre-set the route so simulator connects to THIS project
if (await isDaemonRunning()) {
try {
const status = await getDaemonStatus()
// find server for this project root
const server = status.servers.find((s) => s.root === cwd)
if (server) {
// set route so next connection for this bundleId goes to this server
await setDaemonRoute(bundleId, server.id)
await touchDaemonServer(server.id)
console.log(colors.cyan(`[daemon] Route set: ${bundleId} → this project`))
} else {
console.log(colors.yellow(`[daemon] No server registered for this project`))
console.log(
colors.dim(`Run 'one dev' first, or the simulator will connect directly`)
)
}
} catch (err) {
console.log(colors.dim(`[daemon] Could not set route: ${err}`))
}
}
// run from current directory
if (platform === 'ios') {
const { run } = await import('./runIos')
await run({})
} else {
const { run } = await import('./runAndroid')
await run({})
}
}
async function daemonRoute(args: { app?: string; slot?: string; project?: string }) {
const { isDaemonRunning, getDaemonStatus, setDaemonRoute, clearDaemonRoute } =
await import('../daemon/ipc')
if (!(await isDaemonRunning())) {
console.log(colors.yellow('Daemon is not running'))
process.exit(1)
}
if (!args.app) {
console.log(colors.red('Missing --app parameter'))
console.log('Usage: one daemon route --app=com.example.app --slot=0')
console.log(' or: one daemon route --app=com.example.app --project=~/myapp')
process.exit(1)
}
const status = await getDaemonStatus()
// find the server to route to
let targetServer: (typeof status.servers)[0] | undefined
if (args.slot !== undefined) {
// route by slot (index in server list)
const slotIndex = parseInt(args.slot, 10)
const matchingServers = status.servers.filter((s) => s.bundleId === args.app)
if (slotIndex < 0 || slotIndex >= matchingServers.length) {
console.log(colors.red(`Invalid slot: ${args.slot}`))
console.log(`Available slots for ${args.app}: 0-${matchingServers.length - 1}`)
process.exit(1)
}
targetServer = matchingServers[slotIndex]
} else if (args.project) {
// route by project path
const normalizedProject = args.project.replace(/^~/, process.env.HOME || '')
targetServer = status.servers.find(
(s) => s.bundleId === args.app && s.root === normalizedProject
)
if (!targetServer) {
console.log(colors.red(`No server found for ${args.app} at ${args.project}`))
process.exit(1)
}
} else {
console.log(colors.red('Missing --slot or --project parameter'))
process.exit(1)
}
await setDaemonRoute(args.app, targetServer.id)
const shortRoot = targetServer.root.replace(process.env.HOME || '', '~')
console.log(colors.green(`Route set: ${args.app} → ${targetServer.id} (${shortRoot})`))
}
async function suggestTrayApp() {
const { existsSync } = await import('node:fs')
const trayPaths = [
'/Applications/OneTray.app',
`${process.env.HOME}/Applications/OneTray.app`,
]
const installed = trayPaths.some((p) => existsSync(p))
if (!installed) {
console.log(
colors.dim(' Tip: install OneTray.app for a native macOS cable interface')
)
console.log(colors.dim(' https://github.com/onejs/one/releases?q=one-tray'))
console.log('')
}
}