224 lines
5.9 KiB
TypeScript
224 lines
5.9 KiB
TypeScript
/**
|
|
* 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 <id> — revoke a token
|
|
* pi-remote auth name <id> <name> — rename a token
|
|
*
|
|
* Invoked by the extension via pi's flag registration or as a standalone
|
|
* script: `node cli/index.js <args>`
|
|
*
|
|
* 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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 <name>` 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 <id>");
|
|
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 <id> <new-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 <name> Create a new named bearer token
|
|
auth revoke <id> Revoke a token by id
|
|
auth name <id> <name> Rename a token
|
|
|
|
Options:
|
|
--help, -h Show this help
|
|
`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Standalone entrypoint
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Run when invoked as: node cli/index.js <args>
|
|
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);
|
|
});
|
|
}
|