/** * cli.ts – Command-line interface for piclaw sub-commands. * * Handles --help, --version, and the `keychain` sub-command family * (set, get, list, delete). If a CLI sub-command is detected, * handleCliOptions() processes it and returns true so index.ts can * exit without starting the runtime. * * Consumers: * - index.ts calls handleCliOptions() before main(). */ import { readFileSync } from "fs"; import { join } from "path"; import { initDatabase } from "./db.js"; import { deleteKeychainEntry, getKeychainEntry, listKeychainEntries, setKeychainEntry, } from "./secure/keychain.js"; const HELP_TEXT = `piclaw - Pi Coding Agent Assistant Usage: piclaw [options] piclaw keychain [args] Options: -h, --help Show this help -v, --version Show version -w, --workspace Workspace root (also relocates .piclaw state under it) -p, --port Web UI port (default: 8080) --host Web UI host (default: 0.0.0.0) --idle-timeout Web idle timeout in seconds (default: 0 = disabled) Keychain commands: piclaw keychain set --secret [--type token|password|basic|secret] [--username ] piclaw keychain set --secret-file [--type token|password|basic|secret] [--username ] piclaw keychain get piclaw keychain list piclaw keychain delete `; /** Read the version string from package.json. */ export function getVersion(): string { try { const packagePath = join(import.meta.dir, "..", "..", "package.json"); const data = JSON.parse(readFileSync(packagePath, "utf-8")); return data.version || "unknown"; } catch { return "unknown"; } } function getFlagValue(args: string[], flag: string): string | undefined { const index = args.indexOf(flag); if (index < 0) return undefined; const value = args[index + 1]; if (!value || value.startsWith("-")) return undefined; return value; } function consumeLeadingGlobalOptions(args: string[]): string[] { const remaining = [...args]; while (remaining.length > 0) { const [current] = remaining; if (!current) break; if (current === "--") { remaining.shift(); break; } if (current === "-w" || current === "--workspace" || current === "-p" || current === "--port" || current === "--host" || current === "--idle-timeout" || current === "--tls-cert" || current === "--tls-key") { remaining.shift(); if (remaining.length > 0 && !remaining[0]?.startsWith("-")) { remaining.shift(); } continue; } if (current.startsWith("--workspace=") || current.startsWith("--port=") || current.startsWith("--host=") || current.startsWith("--idle-timeout=") || current.startsWith("--tls-cert=") || current.startsWith("--tls-key=")) { remaining.shift(); continue; } break; } return remaining; } async function handleKeychainCommand(args: string[]): Promise { const [subcommand, name] = args; initDatabase(); switch (subcommand) { case "set": { if (!name) throw new Error("Keychain name is required."); const type = (getFlagValue(args, "--type") || "secret") as | "token" | "password" | "basic" | "secret"; const username = getFlagValue(args, "--username") || null; const secret = getFlagValue(args, "--secret"); const secretFile = getFlagValue(args, "--secret-file"); let resolvedSecret = secret || ""; if (!resolvedSecret && secretFile) { resolvedSecret = readFileSync(secretFile, "utf8").trim(); } if (!resolvedSecret) throw new Error("Keychain secret is required."); await setKeychainEntry({ name, type, secret: resolvedSecret, username }); console.log(`Stored keychain entry ${name}.`); return; } case "get": { if (!name) throw new Error("Keychain name is required."); const entry = await getKeychainEntry(name); console.log(JSON.stringify(entry, null, 2)); return; } case "list": { const entries = listKeychainEntries(); console.log(JSON.stringify(entries, null, 2)); return; } case "delete": { if (!name) throw new Error("Keychain name is required."); const removed = deleteKeychainEntry(name); if (!removed) { console.log(`Keychain entry not found: ${name}`); } else { console.log(`Deleted keychain entry ${name}.`); } return; } default: { throw new Error("Unknown keychain command."); } } } /** Parse CLI args for sub-commands. Returns true if a sub-command was handled. */ export async function handleCliOptions(args = process.argv.slice(2)): Promise { if (args.includes("-h") || args.includes("--help")) { console.log(HELP_TEXT.trim()); return true; } if (args.includes("-v") || args.includes("--version")) { console.log(getVersion()); return true; } const commandArgs = consumeLeadingGlobalOptions(args); if (commandArgs[0] === "keychain") { try { await handleKeychainCommand(commandArgs.slice(1)); } catch (error) { console.error((error as Error).message); return true; } return true; } return false; }