diff --git a/scripts/test-shell-completion.ts b/scripts/test-shell-completion.ts new file mode 100644 index 000000000..688f27886 --- /dev/null +++ b/scripts/test-shell-completion.ts @@ -0,0 +1,284 @@ +/** + * Test script for shell completion installation feature. + * + * This script simulates the shell completion prompt that appears during + * `openclaw update`. Use it to verify the completion installation flow + * without running a full update. + * + * Run from repo root: + * node --import tsx scripts/test-shell-completion.ts [options] + * npx tsx scripts/test-shell-completion.ts [options] + * bun scripts/test-shell-completion.ts [options] + * + * Options: + * --shell Override shell detection (zsh, bash, fish, powershell) + * --check-only Only check status, don't prompt to install + * --force Skip the "already installed" check and prompt anyway + * --help Show this help message + * + * Examples: + * node --import tsx scripts/test-shell-completion.ts + * node --import tsx scripts/test-shell-completion.ts --check-only + * node --import tsx scripts/test-shell-completion.ts --shell bash + * node --import tsx scripts/test-shell-completion.ts --force + */ + +import { confirm, isCancel } from "@clack/prompts"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { + installCompletion, + isCompletionInstalled, + resolveShellFromEnv, +} from "../src/cli/completion-cli.js"; +import { resolveStateDir } from "../src/config/paths.js"; +import { stylePromptMessage } from "../src/terminal/prompt-style.js"; +import { theme } from "../src/terminal/theme.js"; + +const CLI_NAME = "openclaw"; +const SUPPORTED_SHELLS = ["zsh", "bash", "fish", "powershell"] as const; +type SupportedShell = (typeof SUPPORTED_SHELLS)[number]; + +interface Options { + shell?: string; + checkOnly: boolean; + force: boolean; + help: boolean; +} + +function parseArgs(args: string[]): Options { + const options: Options = { + checkOnly: false, + force: false, + help: false, + }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--shell" && args[i + 1]) { + options.shell = args[i + 1]; + i++; + } else if (arg === "--check-only") { + options.checkOnly = true; + } else if (arg === "--force") { + options.force = true; + } else if (arg === "--help" || arg === "-h") { + options.help = true; + } + } + + return options; +} + +function printHelp(): void { + console.log(` +${theme.heading("Shell Completion Test Script")} + +This script simulates the shell completion prompt that appears during +\`openclaw update\`. Use it to verify the completion installation flow +without running a full update. + +${theme.heading("Usage (run from repo root):")} + node --import tsx scripts/test-shell-completion.ts [options] + npx tsx scripts/test-shell-completion.ts [options] + bun scripts/test-shell-completion.ts [options] + +${theme.heading("Options:")} + --shell Override shell detection (zsh, bash, fish, powershell) + --check-only Only check status, don't prompt to install + --force Skip the "already installed" check and prompt anyway + --help, -h Show this help message + +${theme.heading("Examples:")} + node --import tsx scripts/test-shell-completion.ts + node --import tsx scripts/test-shell-completion.ts --check-only + node --import tsx scripts/test-shell-completion.ts --shell bash + node --import tsx scripts/test-shell-completion.ts --force +`); +} + +function isSupportedShell(shell: string): shell is SupportedShell { + return SUPPORTED_SHELLS.includes(shell as SupportedShell); +} + +async function pathExists(targetPath: string): Promise { + try { + await fs.access(targetPath); + return true; + } catch { + return false; + } +} + +function resolveCompletionCacheExtension(shell: SupportedShell): string { + switch (shell) { + case "powershell": + return "ps1"; + case "fish": + return "fish"; + case "bash": + return "bash"; + default: + return "zsh"; + } +} + +async function ensureCompletionCache(shell: SupportedShell): Promise { + const stateDir = resolveStateDir(process.env, os.homedir); + const cacheDir = path.join(stateDir, "completions"); + const ext = resolveCompletionCacheExtension(shell); + const cachePath = path.join(cacheDir, `${CLI_NAME}.${ext}`); + + if (await pathExists(cachePath)) { + console.log(` Completion cache: ${theme.success("exists")} ${theme.muted(`(${cachePath})`)}`); + return true; + } + + console.log(` Completion cache: ${theme.warn("missing")}`); + console.log(theme.muted(" Generating completion cache...")); + + // Use the CLI to generate the cache (same approach as tryWriteCompletionCache in update-cli.ts) + const binPath = path.join(process.cwd(), "openclaw.mjs"); + if (!(await pathExists(binPath))) { + console.log(theme.error(` Cannot find ${binPath}. Run from the repo root.`)); + return false; + } + + // Use the same runtime as the CLI + const runtime = process.execPath; + const result = spawnSync(runtime, [binPath, "completion", "--write-state"], { + cwd: process.cwd(), + env: process.env, + encoding: "utf-8", + // Windows needs shell: true for proper execution in some cases + shell: process.platform === "win32", + }); + + if (result.error) { + console.log(theme.error(` Failed to generate cache: ${String(result.error)}`)); + return false; + } + + if (result.status !== 0) { + const stderr = result.stderr?.trim(); + console.log( + theme.error( + ` Failed to generate cache (exit ${result.status})${stderr ? `: ${stderr}` : ""}`, + ), + ); + return false; + } + + console.log(theme.success(" Completion cache generated.")); + return true; +} + +function getShellProfilePath(shell: SupportedShell): string { + const home = process.env.HOME || os.homedir(); + + switch (shell) { + case "zsh": + return path.join(home, ".zshrc"); + case "bash": + // Linux typically uses .bashrc, macOS uses .bash_profile + return process.platform === "darwin" + ? path.join(home, ".bash_profile") + : path.join(home, ".bashrc"); + case "fish": + return path.join(home, ".config", "fish", "config.fish"); + case "powershell": + // PowerShell profile location varies by platform + if (process.platform === "win32") { + return path.join( + process.env.USERPROFILE || home, + "Documents", + "PowerShell", + "Microsoft.PowerShell_profile.ps1", + ); + } + return path.join(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"); + } +} + +async function main() { + const args = process.argv.slice(2); + const options = parseArgs(args); + + if (options.help) { + printHelp(); + return; + } + + console.log(theme.heading("Shell Completion Test")); + console.log(""); + + // Determine shell + let shell: SupportedShell; + if (options.shell) { + if (!isSupportedShell(options.shell)) { + console.log(theme.error(`Unsupported shell: ${options.shell}`)); + console.log(theme.muted(`Supported shells: ${SUPPORTED_SHELLS.join(", ")}`)); + process.exit(1); + } + shell = options.shell; + console.log(` Shell: ${theme.accent(shell)} ${theme.muted("(override)")}`); + } else { + shell = resolveShellFromEnv() as SupportedShell; + console.log(` Shell: ${theme.accent(shell)} ${theme.muted("(detected from $SHELL)")}`); + } + + // Show platform info + console.log(` Platform: ${theme.muted(process.platform)} ${theme.muted(`(${os.release()})`)}`); + console.log(` Profile: ${theme.muted(getShellProfilePath(shell))}`); + console.log(""); + + // Ensure completion cache exists + const cacheOk = await ensureCompletionCache(shell); + if (!cacheOk) { + console.log(theme.warn(" Continuing without cache (will use dynamic completion)...")); + } + + // Check if completion is installed in shell profile + const installed = await isCompletionInstalled(shell, CLI_NAME); + console.log(` Profile configured: ${installed ? theme.success("yes") : theme.warn("no")}`); + console.log(""); + + if (options.checkOnly) { + console.log(theme.muted("Check-only mode, exiting.")); + return; + } + + if (installed && !options.force) { + console.log(theme.muted("Shell completion is already installed. To test the prompt:")); + console.log( + theme.muted(" 1. Remove the '# OpenClaw Completion' block from your shell profile"), + ); + console.log(theme.muted(" 2. Re-run this script")); + console.log(theme.muted(" Or use --force to prompt anyway")); + console.log(""); + return; + } + + // Simulate the prompt from update-cli.ts + console.log(theme.heading("Shell completion")); + + const shouldInstall = await confirm({ + message: stylePromptMessage(`Enable ${shell} shell completion for ${CLI_NAME}?`), + initialValue: true, + }); + + if (isCancel(shouldInstall) || !shouldInstall) { + console.log(theme.muted(`Skipped. Run \`openclaw completion --install\` later to enable.`)); + return; + } + + // Install completion + await installCompletion(shell, false, CLI_NAME); +} + +main().catch((err) => { + console.error(theme.error(`Error: ${String(err)}`)); + process.exit(1); +});