UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

295 lines (294 loc) 16 kB
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { n as isErrno } from "./errno-CkbDOfLk.js"; import { l as pathExists } from "./utils-P__uGsPB.js"; import { w as resolveStateDir } from "./paths-D2sRr1a_.js"; import "./errors-Db3Ymjlb.js"; import { r as decodeWindowsTextFileBuffer } from "./windows-encoding-Cl7_e1FK.js"; import { n as quotePowerShellArg } from "./quote-cli-arg-BEt71TUh.js"; import { t as publishOutputFileAtomically } from "./output-file.runtime.js"; import { existsSync } from "node:fs"; import path from "node:path"; import fs$1 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"; async function readCompletionProfile(profilePath, shell) { const buffer = await fs$1.readFile(profilePath); let encoding = "utf8"; if (shell === "powershell" && process.platform === "win32") { const [first, second, third] = buffer; if (first === 255 && second === 254 || first === 254 && second === 255) encoding = first === 255 ? "utf16le" : "utf16be"; else if (first === 239 && second === 187 && third === 191) encoding = "utf8bom"; } return { content: encoding === "utf8" ? buffer.toString("utf8") : decodeWindowsTextFileBuffer({ buffer }), encoding }; } function encodeCompletionProfile(content, encoding) { if (encoding === "utf8") return Buffer.from(content, "utf8"); const buffer = Buffer.from(`\uFEFF${content}`, encoding === "utf8bom" ? "utf8" : "utf16le"); return encoding === "utf16be" ? buffer.swap16() : buffer; } /** 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); const basename = winBasename.length < platformBasename.length ? winBasename : platformBasename; return normalizeLowercaseStringOrEmpty(basename.replace(/\.(?:exe|cmd|bat)$/i, "")); } /** Resolves the active shell from environment paths, using the platform's native default. */ function resolveShellFromEnv(env = process.env, platform = process.platform) { const shellPath = normalizeOptionalString(env.SHELL) ?? ""; const shellName = shellPath ? resolveShellBasename(shellPath, platform) : ""; if (isCompletionShell(shellName)) return shellName; if (shellName === "pwsh") return "powershell"; return platform === "win32" ? "powershell" : "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); return path.join(resolveCompletionCacheDir(), `${basename}.${shell === "powershell" ? "ps1" : shell}`); } /** Check if the completion cache file exists for the given shell. */ async function completionCacheExists(shell, binName = "openclaw") { const cachePath = resolveCompletionCachePath(shell, binName); return pathExists(cachePath); } function quoteCompletionPath(shell, value) { if (shell === "powershell") return quotePowerShellArg(value); return `'${shell === "fish" ? value.replace(/[\\']/gu, "\\$&") : value.replaceAll("'", "'\\''")}'`; } function formatCompletionSourceLine(shell, cachePath) { const quotedPath = quoteCompletionPath(shell, cachePath); if (shell === "powershell") return `. ${quotedPath}`; if (shell === "fish") return `test -f ${quotedPath}; and source ${quotedPath}`; return `[ -f ${quotedPath} ] && source ${quotedPath}`; } function appendCompletionProfilePath(directory, pathApi, ...segments) { const nativeDirectory = pathApi.sep === "\\" ? directory.replaceAll("/", pathApi.sep) : directory; return `${nativeDirectory}${nativeDirectory.endsWith(pathApi.sep) ? "" : pathApi.sep}${segments.join(pathApi.sep)}`; } /** Formats a current-shell command to load a profile or cached completion script. */ function formatCompletionReloadCommand(shell, scriptPath) { if (shell === "powershell") return `. ${quoteCompletionPath(shell, scriptPath)}`; if (/^[a-zA-Z0-9_./~+-]+$/u.test(scriptPath)) return `source ${scriptPath}`; const homePrefix = scriptPath.startsWith("~/") ? "~/" : ""; return `source ${homePrefix}${quoteCompletionPath(shell, scriptPath.slice(homePrefix.length))}`; } function isCompletionProfileHeader(line) { return line.trim() === "# OpenClaw Completion"; } function isCompletionProfileLine(line, binName, cachePath) { if (isSlowDynamicCompletionLine(line, binName)) return true; const trimmed = line.trim(); return trimmed === `source "${cachePath}"` || trimmed === `[ -f "${cachePath}" ] && source "${cachePath}"` || trimmed === `test -f "${cachePath}"; and source "${cachePath}"` || COMPLETION_SHELLS.some((shell) => trimmed === formatCompletionSourceLine(shell, cachePath)); } function isPreviousCompletionSourceLine(line, currentCachePath, shell) { const trimmed = line.trim(); const guarded = /^(?:\[\s+-f|test\s+-f)\s+(.+?)\s*(?:\]\s*&&|;\s*and)\s+source\s+\1$/u.exec(trimmed); const direct = /^(source|\.)\s+(.+)$/u.exec(trimmed); const quotedPath = guarded?.[1] ?? direct?.[2]; if (!quotedPath) return false; const quoteShell = direct?.[1] === "." ? "powershell" : shell; const legacyPath = guarded ? /^"(.+)"$/u.exec(quotedPath)?.[1] : /^source\s+"([^"]+)"$/u.exec(trimmed)?.[1]; const escapedPath = quotedPath.slice(1, -1); const sourcePath = legacyPath ?? (quoteShell === "powershell" ? escapedPath.replace(/(['‘-‛])\1/gu, "$1") : quoteShell === "fish" ? escapedPath.replace(/\\([\\'])/gu, "$1") : escapedPath.replaceAll("'\\''", "'")); const matchesLegacyPowerShellLiteral = quoteShell === "powershell" && `'${sourcePath.replaceAll("'", "''")}'` === quotedPath; if (!legacyPath && !matchesLegacyPowerShellLiteral && quoteCompletionPath(quoteShell, sourcePath) !== quotedPath) return false; const sourcePaths = sourcePath.includes("\\") ? path.win32 : path; return sourcePaths.basename(sourcePaths.dirname(sourcePath)) === "completions" && sourcePaths.basename(sourcePath) === path.basename(currentCachePath); } function isOwnedCompletionInvocation(invocation, binName) { const [command, action, ...args] = invocation.trim().split(/\s+/u); if (command !== binName || action !== "completion") return false; if (args.length === 2 && (args[0] === "--shell" || args[0] === "-s")) return isCompletionShell(args[1] ?? ""); return args.length === 0 || args.length === 1 && isCompletionShell((args[0] ?? "").replace(/^(?:--shell=|-s=?)/u, "")); } /** Check if a line uses an owned slow dynamic completion pattern (source <(...)). */ function isSlowDynamicCompletionLine(line, binName) { const trimmed = line.trim(); const dynamicMarker = `<(${binName} completion`; const markerIndex = trimmed.indexOf(dynamicMarker); if (markerIndex >= 0) { const expression = trimmed.slice(markerIndex); return /^(?:(?:\[\s+-f\s+[^\]]+\]\s*&&\s*)?(?:source|\.))\s*$/u.test(trimmed.slice(0, markerIndex).trimEnd()) && expression.endsWith(")") && isOwnedCompletionInvocation(expression.slice(2, -1), binName); } const invocationIndex = trimmed.indexOf(`${binName} completion`); if (invocationIndex < 0) return false; const invocationPrefix = trimmed.slice(0, invocationIndex).trimEnd(); const evalPrefix = /^eval\s+(["']?)\$\($/u.exec(invocationPrefix); if (evalPrefix) { const invocation = trimmed.slice(invocationIndex); const closing = `)${evalPrefix[1] ?? ""}`; return invocation.endsWith(closing) && isOwnedCompletionInvocation(invocation.slice(0, -closing.length), binName); } if (invocationIndex !== 0 || /[;&]/u.test(trimmed)) return false; const pipeline = trimmed.split("|").map((stage) => stage.trim()); const terminal = pipeline.at(-1) ?? ""; return isOwnedCompletionInvocation(pipeline[0] ?? "", binName) && /^(?:source|Invoke-Expression|iex)$/iu.test(terminal) && (pipeline.length === 2 || pipeline.length === 3 && /^Out-String$/iu.test(pipeline[1] ?? "")); } function updateCompletionProfile(content, binName, cachePath, shell) { 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; const following = lines[i + 1] ?? ""; if (isCompletionProfileLine(following, binName, cachePath) || isPreviousCompletionSourceLine(following, cachePath, shell)) 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${formatCompletionSourceLine(shell, cachePath)}`; const next = trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`; return { next, changed: next !== content, hadExisting }; } async function resolveCompletionProfileWritePath(profilePath) { const profileDir = path.dirname(profilePath); await fs$1.mkdir(profileDir, { recursive: true }); const canonicalDir = await fs$1.realpath(profileDir); try { return await fs$1.realpath(profilePath); } catch (error) { if (error.code !== "ENOENT") throw error; } const linkTarget = await fs$1.readlink(profilePath).catch((error) => { const code = error.code; if (code === "ENOENT" || code === "EINVAL") return; throw error; }); if (linkTarget === void 0) return path.join(canonicalDir, path.basename(profilePath)); const targetPath = path.isAbsolute(linkTarget) ? linkTarget : `${canonicalDir}${path.sep}${linkTarget}`; const targetDir = path.dirname(targetPath); await fs$1.mkdir(targetDir, { recursive: true }); return path.join(await fs$1.realpath(targetDir), path.basename(targetPath)); } /** 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 pathApi = platform === "win32" ? path.win32 : path.posix; const home = env.HOME || homeDir(); if (shell === "zsh") return appendCompletionProfilePath(env.ZDOTDIR === void 0 ? home : env.ZDOTDIR || pathApi.sep, pathApi, ".zshrc"); if (shell === "bash") { const bashrc = appendCompletionProfilePath(home, pathApi, ".bashrc"); return existsSync(bashrc) ? bashrc : appendCompletionProfilePath(home, pathApi, ".bash_profile"); } if (shell === "fish") { const configuredHome = env.XDG_CONFIG_HOME; return appendCompletionProfilePath(configuredHome && pathApi.isAbsolute(configuredHome) ? configuredHome : appendCompletionProfilePath(home, pathApi, ".config"), pathApi, "fish", "config.fish"); } if (platform === "win32") { const shellPath = normalizeOptionalString(env.SHELL) ?? ""; const profileDirectory = (shellPath ? resolveShellBasename(shellPath, platform) : "") === "powershell" ? "WindowsPowerShell" : "PowerShell"; return appendCompletionProfilePath(env.USERPROFILE || home, pathApi, "Documents", profileDirectory, "Microsoft.PowerShell_profile.ps1"); } return appendCompletionProfilePath(home, pathApi, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"); } /** Formats the resolved startup profile relative to HOME when that preserves its actual location. */ function resolveCompletionProfileHint(shell) { const profilePath = resolveCompletionProfilePath(shell); if (shell === "powershell") return profilePath; if (!path.isAbsolute(profilePath)) return profilePath.startsWith(`.${path.sep}`) || profilePath.startsWith(`..${path.sep}`) ? profilePath : `.${path.sep}${profilePath}`; const home = process.env.HOME; return home && profilePath.startsWith(`${home}${path.sep}`) ? `~/${profilePath.slice(home.length + 1)}` : profilePath; } /** 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 cachePath = resolveCompletionCachePath(shell, binName); const { content } = await readCompletionProfile(profilePath, shell); return content.split("\n").some((line) => isCompletionProfileLine(line, binName, cachePath)); } /** * 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 { content } = await readCompletionProfile(profilePath, shell); return content.split("\n").some((line) => isSlowDynamicCompletionLine(line, binName) && !line.includes(cachePath)); } const PROFILE_WRITE_ERROR_CODES = /* @__PURE__ */ new Set([ "EACCES", "EPERM", "EROFS" ]); function findCompletionProfileWriteError(err) { if (isErrno(err) && PROFILE_WRITE_ERROR_CODES.has(err.code ?? "")) return err; return err instanceof Error ? findCompletionProfileWriteError(err.cause) : void 0; } 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.`); const profilePath = resolveCompletionProfilePath(shell); try { let content; let encoding = "utf8"; try { ({content, encoding} = await readCompletionProfile(profilePath, shell)); } catch (error) { if (error.code !== "ENOENT") throw error; if (!yes) console.warn(`Profile not found at ${profilePath}. Creating a new one.`); content = ""; } const update = updateCompletionProfile(content, binName, cachePath, shell); 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 publishOutputFileAtomically({ filePath: await resolveCompletionProfileWritePath(profilePath), tempPrefix: ".openclaw-completion-profile", durable: true, writeTemp: async (tempPath) => { await fs$1.writeFile(tempPath, encodeCompletionProfile(update.next, encoding), { flag: "wx" }); } }); if (!yes) console.log(`Completion installed. Restart your shell or run: ${formatCompletionReloadCommand(shell, resolveCompletionProfileHint(shell))}`); } catch (err) { const message = err instanceof Error ? err.message : String(err); throw new Error(`Failed to install completion: ${message}`, { cause: err }); } } //#endregion export { formatCompletionReloadCommand as a, isCompletionShell as c, resolveCompletionProfilePath as d, resolveShellFromEnv as f, findCompletionProfileWriteError as i, resolveCompletionCachePath as l, COMPLETION_SKIP_PLUGIN_COMMANDS_ENV as n, installCompletion as o, usesSlowDynamicCompletion as p, completionCacheExists as r, isCompletionInstalled as s, COMPLETION_SHELLS as t, resolveCompletionProfileHint as u };