mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-23 22:41:20 +00:00
refactor(cli): use native Clack option types
This commit is contained in:
@@ -683,7 +683,6 @@ describe("migrateApplyCommand", () => {
|
||||
const selectionPrompt = multiselectPrompt();
|
||||
expect(String(selectionPrompt.message)).toContain("Select Codex skills");
|
||||
expect(selectionPrompt.initialValues).toStrictEqual(["skill:alpha", "skill:beta"]);
|
||||
expect(selectionPrompt.required).toBe(false);
|
||||
expect(selectionPrompt.options?.map(({ label, value }) => ({ label, value }))).toStrictEqual([
|
||||
{ value: MIGRATION_SELECTION_ACCEPT, label: "Accept recommended" },
|
||||
{ value: "skill:alpha", label: "alpha" },
|
||||
@@ -743,7 +742,6 @@ describe("migrateApplyCommand", () => {
|
||||
const pluginPrompt = multiselectPrompt(1);
|
||||
expect(String(pluginPrompt.message)).toContain("Select native Codex plugins");
|
||||
expect(pluginPrompt.initialValues).toStrictEqual(["plugin:google-calendar", "plugin:gmail"]);
|
||||
expect(pluginPrompt.required).toBe(false);
|
||||
expect(pluginPrompt.options?.map(({ label, value }) => ({ label, value }))).toStrictEqual([
|
||||
{ value: MIGRATION_SELECTION_ACCEPT, label: "Accept recommended" },
|
||||
{ value: "plugin:google-calendar", label: "google-calendar" },
|
||||
|
||||
@@ -203,7 +203,6 @@ async function promptCodexMigrationSkillSelection(
|
||||
},
|
||||
],
|
||||
initialValues: getDefaultMigrationSkillSelectionValues(skillItems),
|
||||
required: false,
|
||||
selectableValues: skillItems.map(getMigrationSkillSelectionValue),
|
||||
cursorAt: MIGRATION_SELECTION_ACCEPT,
|
||||
});
|
||||
@@ -264,7 +263,6 @@ async function promptCodexMigrationPluginSelection(
|
||||
},
|
||||
],
|
||||
initialValues: getDefaultMigrationPluginSelectionValues(pluginItems),
|
||||
required: false,
|
||||
selectableValues: pluginItems.map(getMigrationPluginSelectionValue),
|
||||
cursorAt: MIGRATION_SELECTION_ACCEPT,
|
||||
});
|
||||
|
||||
@@ -37,7 +37,6 @@ async function runPromptWithKeys(params: {
|
||||
{ value: MIGRATION_SELECTION_TOGGLE_ALL_OFF, label: "Toggle all off" },
|
||||
],
|
||||
initialValues: params.initialValues,
|
||||
required: false,
|
||||
cursorAt: params.cursorAt,
|
||||
selectableValues: ["skill:alpha", "skill:beta"],
|
||||
input,
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
S_CHECKBOX_SELECTED,
|
||||
symbol,
|
||||
symbolBar,
|
||||
type MultiSelectOptions,
|
||||
type Option,
|
||||
} from "@clack/prompts";
|
||||
import {
|
||||
MIGRATION_SELECTION_ACCEPT,
|
||||
@@ -18,30 +20,13 @@ import {
|
||||
reconcileInteractiveMigrationSkillToggleValues,
|
||||
} from "./selection.js";
|
||||
|
||||
type MigrationSkillSelectionOption = {
|
||||
value: string;
|
||||
label?: string;
|
||||
hint?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
/** Options for the migration selection prompt, including testable IO streams. */
|
||||
type MigrationSkillSelectionPromptOptions = {
|
||||
message: string;
|
||||
options: MigrationSkillSelectionOption[];
|
||||
initialValues?: string[];
|
||||
maxItems?: number;
|
||||
required?: boolean;
|
||||
cursorAt?: string;
|
||||
input?: NodeJS.ReadStream;
|
||||
output?: NodeJS.WriteStream;
|
||||
signal?: AbortSignal;
|
||||
withGuide?: boolean;
|
||||
type MigrationSkillSelectionPromptOptions = Omit<MultiSelectOptions<string>, "required"> & {
|
||||
selectableValues: readonly string[];
|
||||
};
|
||||
|
||||
function formatOption(
|
||||
option: MigrationSkillSelectionOption,
|
||||
option: Option<string>,
|
||||
state:
|
||||
| "active"
|
||||
| "active-selected"
|
||||
@@ -78,21 +63,13 @@ function formatOption(
|
||||
export function promptMigrationSkillSelectionValues(
|
||||
opts: MigrationSkillSelectionPromptOptions,
|
||||
): Promise<string[] | symbol | undefined> {
|
||||
const required = opts.required ?? true;
|
||||
const prompt = new MultiSelectPrompt<MigrationSkillSelectionOption>({
|
||||
const prompt = new MultiSelectPrompt<Option<string>>({
|
||||
options: opts.options,
|
||||
signal: opts.signal,
|
||||
input: opts.input,
|
||||
output: opts.output,
|
||||
initialValues: opts.initialValues,
|
||||
required,
|
||||
cursorAt: opts.cursorAt,
|
||||
validate(value) {
|
||||
if (required && (value === undefined || value.length === 0)) {
|
||||
return "Please select at least one option.";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
render() {
|
||||
const withGuide = opts.withGuide ?? settings.withGuide;
|
||||
const message = wrapTextWithPrefix(
|
||||
@@ -103,7 +80,7 @@ export function promptMigrationSkillSelectionValues(
|
||||
);
|
||||
const header = `${withGuide ? `${styleText("gray", S_BAR)}\n` : ""}${message}\n`;
|
||||
const value = this.value ?? [];
|
||||
const optionState = (option: MigrationSkillSelectionOption, active: boolean) => {
|
||||
const optionState = (option: Option<string>, active: boolean) => {
|
||||
if (option.disabled) {
|
||||
return formatOption(option, "disabled");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Clack prompt wrappers that add onboarding navigation footers.
|
||||
import type { Writable } from "node:stream";
|
||||
import { styleText } from "node:util";
|
||||
import {
|
||||
AutocompletePrompt,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
type AutocompleteOptions,
|
||||
type ConfirmOptions,
|
||||
type MultiSelectOptions,
|
||||
type Option,
|
||||
type PasswordOptions,
|
||||
type SelectOptions,
|
||||
type TextOptions,
|
||||
@@ -34,20 +34,11 @@ import {
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import type { WizardPromptNavigation } from "./prompts.js";
|
||||
|
||||
type PromptOption<Value> = {
|
||||
value: Value;
|
||||
label?: string;
|
||||
hint?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type NavigationPromptOptions = {
|
||||
navigation?: WizardPromptNavigation;
|
||||
withGuide?: boolean;
|
||||
output?: Writable;
|
||||
};
|
||||
|
||||
function getOptionLabel<Value>(option: PromptOption<Value>): string {
|
||||
function getOptionLabel<Value>(option: Option<Value>): string {
|
||||
return option.label ?? String(option.value ?? "");
|
||||
}
|
||||
|
||||
@@ -61,7 +52,7 @@ function computeLabel(label: string, format: (text: string) => string): string {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function getFilteredOption<Value>(searchText: string, option: PromptOption<Value>): boolean {
|
||||
function getFilteredOption<Value>(searchText: string, option: Option<Value>): boolean {
|
||||
if (!searchText) {
|
||||
return true;
|
||||
}
|
||||
@@ -73,19 +64,6 @@ function getFilteredOption<Value>(searchText: string, option: PromptOption<Value
|
||||
);
|
||||
}
|
||||
|
||||
function getSelectedOptions<Value>(
|
||||
values: Value[],
|
||||
options: Array<PromptOption<Value>>,
|
||||
): Array<PromptOption<Value>> {
|
||||
return options.filter((option) => values.includes(option.value));
|
||||
}
|
||||
|
||||
function adaptOptionFilter<Value>(
|
||||
filter: AutocompleteOptions<Value>["filter"] | undefined,
|
||||
): ((search: string, option: PromptOption<Value>) => boolean) | undefined {
|
||||
return filter ? (search, option) => filter(search, option as never) : undefined;
|
||||
}
|
||||
|
||||
function formatNavigationFooter(navigation: WizardPromptNavigation | undefined): string {
|
||||
if (!navigation || (!navigation.canGoBack && !navigation.canGoForward)) {
|
||||
return "";
|
||||
@@ -117,7 +95,7 @@ function hasGuide(opts: { withGuide?: boolean }): boolean {
|
||||
return opts.withGuide ?? clackSettings.withGuide;
|
||||
}
|
||||
|
||||
function selectOptionRenderer<Value>(option: PromptOption<Value>, state: string): string {
|
||||
function selectOptionRenderer<Value>(option: Option<Value>, state: string): string {
|
||||
const label = getOptionLabel(option);
|
||||
switch (state) {
|
||||
case "disabled":
|
||||
@@ -143,7 +121,7 @@ export function selectWithNavigationFooter<Value>(
|
||||
opts: SelectOptions<Value> & NavigationPromptOptions,
|
||||
): Promise<Value | symbol> {
|
||||
return new SelectPrompt({
|
||||
options: opts.options as Array<PromptOption<Value>>,
|
||||
options: opts.options as Array<Option<Value>>,
|
||||
signal: opts.signal,
|
||||
input: opts.input,
|
||||
output: opts.output,
|
||||
@@ -217,12 +195,12 @@ export function selectWithNavigationFooter<Value>(
|
||||
export function autocompleteWithNavigationFooter<Value>(
|
||||
opts: AutocompleteOptions<Value> & NavigationPromptOptions,
|
||||
): Promise<Value | symbol> {
|
||||
const prompt = new AutocompletePrompt<PromptOption<Value>>({
|
||||
options: opts.options as Array<PromptOption<Value>>,
|
||||
const prompt = new AutocompletePrompt<Option<Value>>({
|
||||
options: opts.options as Array<Option<Value>>,
|
||||
initialValue: opts.initialValue === undefined ? undefined : [opts.initialValue],
|
||||
initialUserInput: opts.initialUserInput,
|
||||
placeholder: opts.placeholder,
|
||||
filter: adaptOptionFilter(opts.filter) ?? getFilteredOption,
|
||||
filter: opts.filter ?? getFilteredOption,
|
||||
signal: opts.signal,
|
||||
input: opts.input,
|
||||
output: opts.output,
|
||||
@@ -235,10 +213,7 @@ export function autocompleteWithNavigationFooter<Value>(
|
||||
const userInput = this.userInput;
|
||||
const options = this.options;
|
||||
const showPlaceholder = userInput === "" && opts.placeholder !== undefined;
|
||||
const opt = (
|
||||
option: PromptOption<Value>,
|
||||
state: "inactive" | "active" | "disabled",
|
||||
): string => {
|
||||
const opt = (option: Option<Value>, state: "inactive" | "active" | "disabled"): string => {
|
||||
const label = getOptionLabel(option);
|
||||
const hint =
|
||||
option.hint && option.value === this.focusedValue
|
||||
@@ -260,7 +235,7 @@ export function autocompleteWithNavigationFooter<Value>(
|
||||
|
||||
switch (this.state) {
|
||||
case "submit": {
|
||||
const selected = getSelectedOptions(this.selectedValues, options);
|
||||
const selected = options.filter((option) => this.selectedValues.includes(option.value));
|
||||
const label =
|
||||
selected.length > 0
|
||||
? ` ${styleText("dim", selected.map(getOptionLabel).join(", "))}`
|
||||
@@ -457,7 +432,7 @@ export function passwordWithNavigationFooter(
|
||||
}
|
||||
|
||||
function multiselectOptionRenderer<Value>(
|
||||
option: PromptOption<Value>,
|
||||
option: Option<Value>,
|
||||
state:
|
||||
| "inactive"
|
||||
| "active"
|
||||
@@ -504,12 +479,11 @@ export function multiselectWithNavigationFooter<Value>(
|
||||
): Promise<Value[] | symbol> {
|
||||
const required = opts.required ?? true;
|
||||
return new MultiSelectPrompt({
|
||||
options: opts.options as Array<PromptOption<Value>>,
|
||||
options: opts.options as Array<Option<Value>>,
|
||||
signal: opts.signal,
|
||||
input: opts.input,
|
||||
output: opts.output,
|
||||
initialValues: opts.initialValues,
|
||||
required,
|
||||
cursorAt: opts.cursorAt,
|
||||
validate(selected: Value[] | undefined) {
|
||||
if (required && (selected === undefined || selected.length === 0)) {
|
||||
@@ -536,7 +510,7 @@ export function multiselectWithNavigationFooter<Value>(
|
||||
);
|
||||
const title = `${showGuide ? `${styleText("gray", S_BAR)}\n` : ""}${wrappedMessage}\n`;
|
||||
const value = this.value ?? [];
|
||||
const styleOption = (option: PromptOption<Value>, active: boolean) => {
|
||||
const styleOption = (option: Option<Value>, active: boolean) => {
|
||||
if (option.disabled) {
|
||||
return multiselectOptionRenderer(option, "disabled");
|
||||
}
|
||||
@@ -634,7 +608,7 @@ export function autocompleteMultiselectWithNavigationFooter<Value>(
|
||||
opts: AutocompleteMultiSelectOptions<Value> & NavigationPromptOptions,
|
||||
): Promise<Value[] | symbol> {
|
||||
const formatOption = (
|
||||
option: PromptOption<Value>,
|
||||
option: Option<Value>,
|
||||
active: boolean,
|
||||
selectedValues: Value[],
|
||||
focusedValue: Value | undefined,
|
||||
@@ -661,11 +635,11 @@ export function autocompleteMultiselectWithNavigationFooter<Value>(
|
||||
return `${checkbox} ${styleText("dim", label)}`;
|
||||
};
|
||||
|
||||
const prompt = new AutocompletePrompt<PromptOption<Value>>({
|
||||
options: opts.options as Array<PromptOption<Value>>,
|
||||
const prompt = new AutocompletePrompt<Option<Value>>({
|
||||
options: opts.options as Array<Option<Value>>,
|
||||
multiple: true,
|
||||
placeholder: opts.placeholder,
|
||||
filter: adaptOptionFilter(opts.filter) ?? getFilteredOption,
|
||||
filter: opts.filter ?? getFilteredOption,
|
||||
validate: () => {
|
||||
if (opts.required && prompt.selectedValues.length === 0) {
|
||||
return "Please select at least one item";
|
||||
|
||||
Reference in New Issue
Block a user