UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

197 lines (196 loc) 9.26 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { y as resolveStateDir } from "./paths-mvMm5bYV.js"; import { u as pathExists } from "./utils-CCC-BEJH.js"; import path from "node:path"; import fs from "node:fs/promises"; import os from "node:os"; //#region src/cli/completion-runtime.ts const COMPLETION_SHELLS = [ "zsh", "bash", "powershell", "fish" ]; const COMPLETION_SKIP_PLUGIN_COMMANDS_ENV = "OPENCLAW_COMPLETION_SKIP_PLUGIN_COMMANDS"; /** Narrows an arbitrary shell label to a completion shell supported by installer logic. */ function isCompletionShell(value) { return COMPLETION_SHELLS.includes(value); } function resolveShellBasename(shellPath, platform = process.platform) { const platformBasename = platform === "win32" ? path.win32.basename(shellPath) : path.basename(shellPath); const winBasename = path.win32.basename(shellPath); return normalizeLowercaseStringOrEmpty((winBasename.length < platformBasename.length ? winBasename : platformBasename).replace(/\.(?:exe|cmd|bat)$/i, "")); } /** Resolves the active shell from environment paths, defaulting to zsh for unknown shells. */ function resolveShellFromEnv(env = process.env) { const shellPath = normalizeOptionalString(env.SHELL) ?? ""; const shellName = shellPath ? resolveShellBasename(shellPath) : ""; if (shellName === "zsh") return "zsh"; if (shellName === "bash") return "bash"; if (shellName === "fish") return "fish"; if (shellName === "pwsh" || shellName === "powershell") return "powershell"; return "zsh"; } function sanitizeCompletionBasename(value) { const trimmed = value.trim(); if (!trimmed) return "openclaw"; return trimmed.replace(/[^a-zA-Z0-9._-]/g, "-"); } function resolveCompletionCacheDir(env = process.env) { const stateDir = resolveStateDir(env, os.homedir); return path.join(stateDir, "completions"); } /** Returns the per-shell cached completion script path for a sanitized CLI binary name. */ function resolveCompletionCachePath(shell, binName) { const basename = sanitizeCompletionBasename(binName); const extension = shell === "powershell" ? "ps1" : shell === "fish" ? "fish" : shell === "bash" ? "bash" : "zsh"; return path.join(resolveCompletionCacheDir(), `${basename}.${extension}`); } /** Check if the completion cache file exists for the given shell. */ async function completionCacheExists(shell, binName = "openclaw") { return pathExists(resolveCompletionCachePath(shell, binName)); } function escapePowerShellSingleQuotedString(value) { return value.replace(/'/g, "''"); } /** Formats the profile line that sources the cached completion script for a shell. */ function formatCompletionSourceLine(shell, _binName, cachePath) { if (shell === "powershell") return `. '${escapePowerShellSingleQuotedString(cachePath)}'`; if (shell === "fish") return `test -f "${cachePath}"; and source "${cachePath}"`; return `[ -f "${cachePath}" ] && source "${cachePath}"`; } /** Formats the command users can run to reload the shell profile after installation. */ function formatCompletionReloadCommand(shell, profilePath) { if (shell === "powershell") return `. '${escapePowerShellSingleQuotedString(profilePath)}'`; return `source ${profilePath}`; } function isCompletionProfileHeader(line) { return line.trim() === "# OpenClaw Completion"; } function isCompletionProfileLine(line, binName, cachePath) { if (line.includes(`${binName} completion`)) return true; if (cachePath && line.includes(cachePath)) return true; return false; } /** Check if a line uses the slow dynamic completion pattern (source <(...)) */ function isSlowDynamicCompletionLine(line, binName) { return line.includes(`<(${binName} completion`) || line.includes(`${binName} completion`) && line.includes("| source"); } function updateCompletionProfile(content, binName, cachePath, sourceLine) { const lines = content.split("\n"); const filtered = []; let hadExisting = false; for (let i = 0; i < lines.length; i += 1) { const line = lines[i] ?? ""; if (isCompletionProfileHeader(line)) { hadExisting = true; i += 1; continue; } if (isCompletionProfileLine(line, binName, cachePath)) { hadExisting = true; continue; } filtered.push(line); } const trimmed = filtered.join("\n").trimEnd(); const block = `# OpenClaw Completion\n${sourceLine}`; const next = trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`; return { next, changed: next !== content, hadExisting }; } /** Resolves the shell startup profile path that should contain the OpenClaw completion block. */ function resolveCompletionProfilePath(shell, options = {}) { const env = options.env ?? process.env; const homeDir = options.homeDir ?? os.homedir; const platform = options.platform ?? process.platform; const home = env.HOME || homeDir(); if (shell === "zsh") return path.join(home, ".zshrc"); if (shell === "bash") return path.join(home, ".bashrc"); if (shell === "fish") return path.join(home, ".config", "fish", "config.fish"); if (platform === "win32") { const shellPath = normalizeOptionalString(env.SHELL) ?? ""; const profileDirectory = (shellPath ? resolveShellBasename(shellPath, platform) : "") === "powershell" ? "WindowsPowerShell" : "PowerShell"; return path.win32.join(env.USERPROFILE || home, "Documents", profileDirectory, "Microsoft.PowerShell_profile.ps1"); } return path.join(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"); } /** Returns whether a shell profile already contains an OpenClaw completion block or source line. */ async function isCompletionInstalled(shell, binName = "openclaw") { const profilePath = resolveCompletionProfilePath(shell); if (!await pathExists(profilePath)) return false; const cachePathCandidate = resolveCompletionCachePath(shell, binName); const cachedPath = await pathExists(cachePathCandidate) ? cachePathCandidate : null; return (await fs.readFile(profilePath, "utf-8")).split("\n").some((line) => isCompletionProfileHeader(line) || isCompletionProfileLine(line, binName, cachedPath)); } /** * Check if the profile uses the slow dynamic completion pattern. * Returns true if profile has `source <(openclaw completion ...)` instead of cached file. */ async function usesSlowDynamicCompletion(shell, binName = "openclaw") { const profilePath = resolveCompletionProfilePath(shell); if (!await pathExists(profilePath)) return false; const cachePath = resolveCompletionCachePath(shell, binName); const lines = (await fs.readFile(profilePath, "utf-8")).split("\n"); for (const line of lines) if (isSlowDynamicCompletionLine(line, binName) && !line.includes(cachePath)) return true; return false; } async function installCompletion(shell, yes, binName = "openclaw") { if (!isCompletionShell(shell)) throw new Error(`Automated installation not supported for ${shell} yet.`); const cachePath = resolveCompletionCachePath(shell, binName); if (!await pathExists(cachePath)) throw new Error(`Completion cache not found at ${cachePath}. Run \`${binName} completion --write-state\` first.`); let profilePath; let sourceLine; switch (shell) { case "zsh": profilePath = resolveCompletionProfilePath("zsh"); sourceLine = formatCompletionSourceLine("zsh", binName, cachePath); break; case "bash": profilePath = resolveCompletionProfilePath("bash"); try { await fs.access(profilePath); } catch { const home = process.env.HOME || os.homedir(); profilePath = path.join(home, ".bash_profile"); } sourceLine = formatCompletionSourceLine("bash", binName, cachePath); break; case "fish": profilePath = resolveCompletionProfilePath("fish"); sourceLine = formatCompletionSourceLine("fish", binName, cachePath); break; case "powershell": profilePath = resolveCompletionProfilePath("powershell"); sourceLine = formatCompletionSourceLine("powershell", binName, cachePath); break; } try { try { await fs.access(profilePath); } catch { if (!yes) console.warn(`Profile not found at ${profilePath}. Created a new one.`); await fs.mkdir(path.dirname(profilePath), { recursive: true }); await fs.writeFile(profilePath, "", "utf-8"); } const update = updateCompletionProfile(await fs.readFile(profilePath, "utf-8"), binName, cachePath, sourceLine); if (!update.changed) { if (!yes) console.log(`Completion already installed in ${profilePath}`); return; } if (!yes) { const action = update.hadExisting ? "Updating" : "Installing"; console.log(`${action} completion in ${profilePath}...`); } await fs.writeFile(profilePath, update.next, "utf-8"); if (!yes) console.log(`Completion installed. Restart your shell or run: ${formatCompletionReloadCommand(shell, profilePath)}`); } catch (err) { const message = err instanceof Error ? err.message : String(err); throw new Error(`Failed to install completion: ${message}`, { cause: err }); } } //#endregion export { installCompletion as a, resolveCompletionCachePath as c, usesSlowDynamicCompletion as d, formatCompletionReloadCommand as i, resolveCompletionProfilePath as l, COMPLETION_SKIP_PLUGIN_COMMANDS_ENV as n, isCompletionInstalled as o, completionCacheExists as r, isCompletionShell as s, COMPLETION_SHELLS as t, resolveShellFromEnv as u };