/** * pi-remote CLI entrypoints. * * Subcommands: * pi-remote pair — generate QR code for device pairing * pi-remote auth list — list bearer tokens * pi-remote auth revoke — revoke a token * pi-remote auth name — rename a token * * Invoked by the extension via pi's flag registration or as a standalone * script: `node cli/index.js ` * * Owner: T-1.3 */ import os from "node:os"; import path from "node:path"; import { buildPairingUrl, generatePairingToken, printPairingQr, } from "../auth/pairing.js"; import { loadOrCreateCert } from "../auth/tls.js"; import { createToken, listTokens, renameToken, revokeToken, } from "../auth/tokens.js"; import { readRemoteControlConfig } from "../config.js"; const DEFAULT_STATE_DIR = path.join( os.homedir(), ".local", "share", "pi-remote", ); export async function runCli( argv: string[] = process.argv.slice(2), ): Promise { const [cmd, sub, ...rest] = argv; switch (cmd) { case "pair": await cmdPair(); break; case "auth": await cmdAuth(sub, rest); break; case "help": case "--help": case "-h": printHelp(); break; default: console.error(`Unknown command: ${cmd ?? "(none)"}`); printHelp(); process.exitCode = 1; } } // --------------------------------------------------------------------------- // pair // --------------------------------------------------------------------------- async function cmdPair(): Promise { const config = await readRemoteControlConfig(); const stateDir = DEFAULT_STATE_DIR; // Try to get TLS fingerprint; fall back to empty string if openssl unavailable let fingerprint = ""; try { const cert = await loadOrCreateCert({ stateDir }); fingerprint = cert.fingerprint; } catch { console.warn( "[pi-remote] Warning: openssl not available; fingerprint will be empty.", ); } const pairingTokenEntry = generatePairingToken(); // Determine host for QR — use advertised URL or fall back to hostname const bindAddress = config.bindAddress ?? "0.0.0.0:7777"; const portMatch = bindAddress.match(/:(\d+)$/); const port = portMatch ? parseInt(portMatch[1], 10) : 7777; const host = (config.publicBaseUrl ?? config.advertisedBaseUrl)?.replace( /^https?:\/\//, "", ) ?? os.hostname(); const url = buildPairingUrl({ host, port, pairingToken: pairingTokenEntry.token, fingerprint, sidecarName: "pi-remote", }); console.log("\nScan this QR code with the pi-remote iOS app:\n"); await printPairingQr(url); console.log( `\nPairing token expires in 10 minutes. Run "pi-remote pair" again to refresh.`, ); } // --------------------------------------------------------------------------- // auth // --------------------------------------------------------------------------- async function cmdAuth(sub: string | undefined, args: string[]): Promise { const stateDir = DEFAULT_STATE_DIR; switch (sub) { case "list": { const tokens = await listTokens(stateDir); if (tokens.length === 0) { console.log( "No tokens. Use `pi-remote auth create ` to create one.", ); return; } console.log("ID NAME CREATED"); for (const t of tokens) { const created = new Date(t.createdAt).toLocaleDateString(); console.log( `${t.id.padEnd(12)}${t.name.padEnd(22)}${created}${t.deviceToken ? " [device paired]" : ""}`, ); } break; } case "create": { const name = args[0] ?? "unnamed"; const entry = await createToken(name, stateDir); console.log(`Created token "${entry.name}" (id: ${entry.id})`); console.log(`Bearer token: ${entry.token}`); console.log("Save this token — it won't be shown again."); break; } case "revoke": { const id = args[0]; if (!id) { console.error("Usage: pi-remote auth revoke "); process.exitCode = 1; return; } const ok = await revokeToken(id, stateDir); if (ok) { console.log(`Revoked token ${id}.`); } else { console.error(`Token ${id} not found.`); process.exitCode = 1; } break; } case "name": { const [id, ...nameParts] = args; const newName = nameParts.join(" "); if (!id || !newName) { console.error("Usage: pi-remote auth name "); process.exitCode = 1; return; } const ok = await renameToken(id, newName, stateDir); if (ok) { console.log(`Renamed token ${id} to "${newName}".`); } else { console.error(`Token ${id} not found.`); process.exitCode = 1; } break; } default: console.error(`Unknown auth subcommand: ${sub ?? "(none)"}`); console.error("Available: list, create, revoke, name"); process.exitCode = 1; } } // --------------------------------------------------------------------------- // help // --------------------------------------------------------------------------- function printHelp(): void { console.log(` pi-remote — CLI for the pi-remote-control sidecar Commands: pair Generate a QR code to pair the iOS app auth list List all bearer tokens auth create Create a new named bearer token auth revoke Revoke a token by id auth name Rename a token Options: --help, -h Show this help `); } // --------------------------------------------------------------------------- // Standalone entrypoint // --------------------------------------------------------------------------- // Run when invoked as: node cli/index.js if ( process.argv[1] && (process.argv[1].endsWith("cli/index.js") || process.argv[1].endsWith("cli/index.ts")) ) { runCli().catch((err) => { console.error(err); process.exit(1); }); }