refactor(cli): extract fish completion line builders
This commit is contained in:
@@ -5,6 +5,10 @@ import { Command, Option } from "commander";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { routeLogsToStderr } from "../logging/console.js";
|
||||
import { pathExists } from "../utils.js";
|
||||
import {
|
||||
buildFishOptionCompletionLine,
|
||||
buildFishSubcommandCompletionLine,
|
||||
} from "./completion-fish.js";
|
||||
import { getCoreCliCommandNames, registerCoreCliByName } from "./program/command-registry.js";
|
||||
import { getProgramContext } from "./program/program-context.js";
|
||||
import { getSubCliEntries, registerSubCliByName } from "./program/register.subclis.js";
|
||||
@@ -598,26 +602,21 @@ function generateFishCompletion(program: Command): string {
|
||||
if (parents.length === 0) {
|
||||
// Subcommands of root
|
||||
for (const sub of cmd.commands) {
|
||||
const desc = sub.description().replace(/'/g, "'\\''");
|
||||
script += `complete -c ${rootCmd} -n "__fish_use_subcommand" -a "${sub.name()}" -d '${desc}'\n`;
|
||||
script += buildFishSubcommandCompletionLine({
|
||||
rootCmd,
|
||||
condition: "__fish_use_subcommand",
|
||||
name: sub.name(),
|
||||
description: sub.description(),
|
||||
});
|
||||
}
|
||||
// Options of root
|
||||
for (const opt of cmd.options) {
|
||||
const flags = opt.flags.split(/[ ,|]+/);
|
||||
const long = flags.find((f) => f.startsWith("--"))?.replace(/^--/, "");
|
||||
const short = flags
|
||||
.find((f) => f.startsWith("-") && !f.startsWith("--"))
|
||||
?.replace(/^-/, "");
|
||||
const desc = opt.description.replace(/'/g, "'\\''");
|
||||
let line = `complete -c ${rootCmd} -n "__fish_use_subcommand"`;
|
||||
if (short) {
|
||||
line += ` -s ${short}`;
|
||||
}
|
||||
if (long) {
|
||||
line += ` -l ${long}`;
|
||||
}
|
||||
line += ` -d '${desc}'\n`;
|
||||
script += line;
|
||||
script += buildFishOptionCompletionLine({
|
||||
rootCmd,
|
||||
condition: "__fish_use_subcommand",
|
||||
flags: opt.flags,
|
||||
description: opt.description,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Nested commands
|
||||
@@ -631,26 +630,21 @@ function generateFishCompletion(program: Command): string {
|
||||
|
||||
// Subcommands
|
||||
for (const sub of cmd.commands) {
|
||||
const desc = sub.description().replace(/'/g, "'\\''");
|
||||
script += `complete -c ${rootCmd} -n "__fish_seen_subcommand_from ${cmdName}" -a "${sub.name()}" -d '${desc}'\n`;
|
||||
script += buildFishSubcommandCompletionLine({
|
||||
rootCmd,
|
||||
condition: `__fish_seen_subcommand_from ${cmdName}`,
|
||||
name: sub.name(),
|
||||
description: sub.description(),
|
||||
});
|
||||
}
|
||||
// Options
|
||||
for (const opt of cmd.options) {
|
||||
const flags = opt.flags.split(/[ ,|]+/);
|
||||
const long = flags.find((f) => f.startsWith("--"))?.replace(/^--/, "");
|
||||
const short = flags
|
||||
.find((f) => f.startsWith("-") && !f.startsWith("--"))
|
||||
?.replace(/^-/, "");
|
||||
const desc = opt.description.replace(/'/g, "'\\''");
|
||||
let line = `complete -c ${rootCmd} -n "__fish_seen_subcommand_from ${cmdName}"`;
|
||||
if (short) {
|
||||
line += ` -s ${short}`;
|
||||
}
|
||||
if (long) {
|
||||
line += ` -l ${long}`;
|
||||
}
|
||||
line += ` -d '${desc}'\n`;
|
||||
script += line;
|
||||
script += buildFishOptionCompletionLine({
|
||||
rootCmd,
|
||||
condition: `__fish_seen_subcommand_from ${cmdName}`,
|
||||
flags: opt.flags,
|
||||
description: opt.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
48
src/cli/completion-fish.test.ts
Normal file
48
src/cli/completion-fish.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildFishOptionCompletionLine,
|
||||
buildFishSubcommandCompletionLine,
|
||||
escapeFishDescription,
|
||||
} from "./completion-fish.js";
|
||||
|
||||
describe("completion-fish helpers", () => {
|
||||
it("escapes single quotes in descriptions", () => {
|
||||
expect(escapeFishDescription("Bob's plugin")).toBe("Bob'\\''s plugin");
|
||||
});
|
||||
|
||||
it("builds a subcommand completion line", () => {
|
||||
const line = buildFishSubcommandCompletionLine({
|
||||
rootCmd: "openclaw",
|
||||
condition: "__fish_use_subcommand",
|
||||
name: "plugins",
|
||||
description: "Manage Bob's plugins",
|
||||
});
|
||||
expect(line).toBe(
|
||||
`complete -c openclaw -n "__fish_use_subcommand" -a "plugins" -d 'Manage Bob'\\''s plugins'\n`,
|
||||
);
|
||||
});
|
||||
|
||||
it("builds option line with short and long flags", () => {
|
||||
const line = buildFishOptionCompletionLine({
|
||||
rootCmd: "openclaw",
|
||||
condition: "__fish_use_subcommand",
|
||||
flags: "-s, --shell <shell>",
|
||||
description: "Shell target",
|
||||
});
|
||||
expect(line).toBe(
|
||||
`complete -c openclaw -n "__fish_use_subcommand" -s s -l shell -d 'Shell target'\n`,
|
||||
);
|
||||
});
|
||||
|
||||
it("builds option line with long-only flags", () => {
|
||||
const line = buildFishOptionCompletionLine({
|
||||
rootCmd: "openclaw",
|
||||
condition: "__fish_seen_subcommand_from completion",
|
||||
flags: "--write-state",
|
||||
description: "Write cache",
|
||||
});
|
||||
expect(line).toBe(
|
||||
`complete -c openclaw -n "__fish_seen_subcommand_from completion" -l write-state -d 'Write cache'\n`,
|
||||
);
|
||||
});
|
||||
});
|
||||
41
src/cli/completion-fish.ts
Normal file
41
src/cli/completion-fish.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export function escapeFishDescription(value: string): string {
|
||||
return value.replace(/'/g, "'\\''");
|
||||
}
|
||||
|
||||
function parseOptionFlags(flags: string): { long?: string; short?: string } {
|
||||
const parts = flags.split(/[ ,|]+/);
|
||||
const long = parts.find((flag) => flag.startsWith("--"))?.replace(/^--/, "");
|
||||
const short = parts
|
||||
.find((flag) => flag.startsWith("-") && !flag.startsWith("--"))
|
||||
?.replace(/^-/, "");
|
||||
return { long, short };
|
||||
}
|
||||
|
||||
export function buildFishSubcommandCompletionLine(params: {
|
||||
rootCmd: string;
|
||||
condition: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}): string {
|
||||
const desc = escapeFishDescription(params.description);
|
||||
return `complete -c ${params.rootCmd} -n "${params.condition}" -a "${params.name}" -d '${desc}'\n`;
|
||||
}
|
||||
|
||||
export function buildFishOptionCompletionLine(params: {
|
||||
rootCmd: string;
|
||||
condition: string;
|
||||
flags: string;
|
||||
description: string;
|
||||
}): string {
|
||||
const { short, long } = parseOptionFlags(params.flags);
|
||||
const desc = escapeFishDescription(params.description);
|
||||
let line = `complete -c ${params.rootCmd} -n "${params.condition}"`;
|
||||
if (short) {
|
||||
line += ` -s ${short}`;
|
||||
}
|
||||
if (long) {
|
||||
line += ` -l ${long}`;
|
||||
}
|
||||
line += ` -d '${desc}'\n`;
|
||||
return line;
|
||||
}
|
||||
Reference in New Issue
Block a user