mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
feat: implement completion command and shell generators
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import { Command, Option } from "commander";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { getSubCliEntries, registerSubCliByName } from "./program/register.subclis.js";
|
||||
|
||||
export function registerCompletionCli(program: Command) {
|
||||
@@ -10,6 +13,8 @@ export function registerCompletionCli(program: Command) {
|
||||
.choices(["zsh", "bash", "powershell", "fish"])
|
||||
.default("zsh"),
|
||||
)
|
||||
.option("-i, --install", "Install completion script to shell profile")
|
||||
.option("-y, --yes", "Skip confirmation (non-interactive)", false)
|
||||
.action(async (options) => {
|
||||
const shell = options.shell;
|
||||
// Eagerly register all subcommands to build the full tree
|
||||
@@ -20,6 +25,11 @@ export function registerCompletionCli(program: Command) {
|
||||
await registerSubCliByName(program, entry.name);
|
||||
}
|
||||
|
||||
if (options.install) {
|
||||
await installCompletion(shell, Boolean(options.yes), program.name());
|
||||
return;
|
||||
}
|
||||
|
||||
let script = "";
|
||||
if (shell === "zsh") {
|
||||
script = generateZshCompletion(program);
|
||||
@@ -35,18 +45,75 @@ export function registerCompletionCli(program: Command) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function installCompletion(shell: string, yes: boolean, binName = "openclaw") {
|
||||
const home = process.env.HOME || os.homedir();
|
||||
let profilePath = "";
|
||||
let sourceLine = "";
|
||||
|
||||
if (shell === "zsh") {
|
||||
profilePath = path.join(home, ".zshrc");
|
||||
sourceLine = `source <(${binName} completion --shell zsh)`;
|
||||
} else if (shell === "bash") {
|
||||
// Try .bashrc first, then .bash_profile
|
||||
profilePath = path.join(home, ".bashrc");
|
||||
try {
|
||||
await fs.access(profilePath);
|
||||
} catch {
|
||||
profilePath = path.join(home, ".bash_profile");
|
||||
}
|
||||
sourceLine = `source <(${binName} completion --shell bash)`;
|
||||
} else if (shell === "fish") {
|
||||
profilePath = path.join(home, ".config", "fish", "config.fish");
|
||||
sourceLine = `${binName} completion --shell fish | source`;
|
||||
} else {
|
||||
console.error(`Automated installation not supported for ${shell} yet.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if profile exists
|
||||
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 content = await fs.readFile(profilePath, "utf-8");
|
||||
if (content.includes(`${binName} completion`)) {
|
||||
if (!yes) console.log(`Completion already installed in ${profilePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!yes) {
|
||||
// Simple confirmation could go here if we had a prompter,
|
||||
// but for now we assume --yes or manual invocation implies consent or we print info.
|
||||
// Since we don't have a prompter passed in here easily without adding deps, we'll log.
|
||||
console.log(`Installing completion to ${profilePath}...`);
|
||||
}
|
||||
|
||||
await fs.appendFile(profilePath, `\n# OpenClaw Completion\n${sourceLine}\n`);
|
||||
console.log(`Completion installed. Restart your shell or run: source ${profilePath}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to install completion: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
function generateZshCompletion(program: Command): string {
|
||||
const rootCmd = program.name();
|
||||
const script = `
|
||||
#compdef ${rootCmd}
|
||||
|
||||
_${rootCmd}_completion() {
|
||||
_${rootCmd}_root_completion() {
|
||||
local -a commands
|
||||
local -a options
|
||||
|
||||
_arguments -C \\
|
||||
${generateZshArgs(program)} \\
|
||||
"1: :_commands" \\
|
||||
${generateZshSubcmdList(program)} \\
|
||||
"*::arg:->args"
|
||||
|
||||
case $state in
|
||||
@@ -60,7 +127,7 @@ _${rootCmd}_completion() {
|
||||
|
||||
${generateZshSubcommands(program, rootCmd)}
|
||||
|
||||
compdef _${rootCmd}_completion ${rootCmd}
|
||||
compdef _${rootCmd}_root_completion ${rootCmd}
|
||||
`;
|
||||
return script;
|
||||
}
|
||||
@@ -73,13 +140,27 @@ function generateZshArgs(cmd: Command): string {
|
||||
const short = flags.find((f) => f.startsWith("-") && !f.startsWith("--"));
|
||||
const desc = opt.description.replace(/'/g, "'\\''");
|
||||
if (short) {
|
||||
return `"(${name} ${short})"'{${name},${short}}'[${desc}]"`;
|
||||
return `"(${name} ${short})"{${name},${short}}"[${desc}]"`;
|
||||
}
|
||||
return `"${name}[${desc}]"`;
|
||||
})
|
||||
.join(" \\\n ");
|
||||
}
|
||||
|
||||
function generateZshSubcmdList(cmd: Command): string {
|
||||
const list = cmd.commands
|
||||
.map((c) => {
|
||||
const desc = c
|
||||
.description()
|
||||
.replace(/'/g, "'\\''")
|
||||
.replace(/\[/g, "\\[")
|
||||
.replace(/\]/g, "\\]");
|
||||
return `'${c.name()}[${desc}]'`;
|
||||
})
|
||||
.join(" ");
|
||||
return `"1: :_values 'command' ${list}"`;
|
||||
}
|
||||
|
||||
function generateZshSubcommands(program: Command, prefix: string): string {
|
||||
let script = "";
|
||||
for (const cmd of program.commands) {
|
||||
@@ -98,7 +179,7 @@ ${funcName}() {
|
||||
|
||||
_arguments -C \\
|
||||
${generateZshArgs(cmd)} \\
|
||||
"1: :_commands" \\
|
||||
${generateZshSubcmdList(cmd)} \\
|
||||
"*::arg:->args"
|
||||
|
||||
case $state in
|
||||
|
||||
Reference in New Issue
Block a user