openpencil/packages/cli/src/app-client.ts
Danila Poyarkov 2ec382fc9d Fix RPC bridge review issues: centralize colors, ports, remove non-null assertions
- Use colorToHex/colorDistance from color.ts instead of hand-rolled hex math
  in rpc/commands.ts (removes toHex, hexToRgb, colorDistance duplicates)
- Extract AUTOMATION_HTTP_PORT/AUTOMATION_WS_PORT to core constants, import
  in bridge.ts, server.ts, app-client.ts instead of duplicating magic numbers
- Add requireFile() helper in app-client.ts, replace all file! non-null
  assertions across 12 CLI command files
- Document same-machine trust security model in bridge.ts header comment
- Replace (globalThis as any).Bun with typed isBunRuntime() helper
- Add changelog entry for CLI-to-app RPC bridge feature
2026-03-07 15:11:31 +03:00

57 lines
1.8 KiB
TypeScript

import { AUTOMATION_HTTP_PORT } from '@open-pencil/core'
const HEALTH_URL = `http://127.0.0.1:${AUTOMATION_HTTP_PORT}/health`
const RPC_URL = `http://127.0.0.1:${AUTOMATION_HTTP_PORT}/rpc`
let cachedToken: string | null = null
export async function getAppToken(): Promise<string> {
if (cachedToken) return cachedToken
const res = await fetch(HEALTH_URL).catch(() => null)
if (!res || !res.ok) {
throw new Error(
`Could not connect to OpenPencil app on localhost:${AUTOMATION_HTTP_PORT}.\n` +
'Is the app running? Start it with: bun run tauri dev'
)
}
const data = (await res.json()) as { status: string; token?: string }
if (data.status !== 'ok' || !data.token) {
throw new Error(
'OpenPencil app is running but no document is open.\n' +
'Open a document in the app, or provide a .fig file path.'
)
}
cachedToken = data.token
return cachedToken
}
export async function rpc<T = unknown>(command: string, args: unknown = {}): Promise<T> {
const token = await getAppToken()
const res = await fetch(RPC_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({ command, args })
})
if (!res.ok) {
const body = (await res.json().catch(() => ({ error: `HTTP ${res.status}` }))) as { error?: string; ok?: boolean }
throw new Error(body.error ?? `RPC failed: HTTP ${res.status}`)
}
const body = (await res.json()) as { ok?: boolean; result?: T; error?: string }
if (body.ok === false) throw new Error(body.error ?? 'RPC failed')
return body.result as T
}
export function isAppMode(file?: string): boolean {
return !file
}
export function requireFile(file?: string): string {
if (!file) throw new Error('File path is required for headless mode')
return file
}