feat: streamline wizard selection prompts
This commit is contained in:
@@ -35,10 +35,10 @@ import { resolveUserPath } from "../utils.js";
|
|||||||
import { createClackPrompter } from "../wizard/clack-prompter.js";
|
import { createClackPrompter } from "../wizard/clack-prompter.js";
|
||||||
import { WizardCancelledError } from "../wizard/prompts.js";
|
import { WizardCancelledError } from "../wizard/prompts.js";
|
||||||
import { applyAuthChoice, warnIfModelConfigLooksOff } from "./auth-choice.js";
|
import { applyAuthChoice, warnIfModelConfigLooksOff } from "./auth-choice.js";
|
||||||
import { buildAuthChoiceOptions } from "./auth-choice-options.js";
|
import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js";
|
||||||
import { ensureWorkspaceAndSessions, moveToTrash } from "./onboard-helpers.js";
|
import { ensureWorkspaceAndSessions, moveToTrash } from "./onboard-helpers.js";
|
||||||
import { setupProviders } from "./onboard-providers.js";
|
import { setupProviders } from "./onboard-providers.js";
|
||||||
import type { AuthChoice, ProviderChoice } from "./onboard-types.js";
|
import type { ProviderChoice } from "./onboard-types.js";
|
||||||
|
|
||||||
type AgentsListOptions = {
|
type AgentsListOptions = {
|
||||||
json?: boolean;
|
json?: boolean;
|
||||||
@@ -920,14 +920,12 @@ export async function agentsAddCommand(
|
|||||||
const authStore = ensureAuthProfileStore(agentDir, {
|
const authStore = ensureAuthProfileStore(agentDir, {
|
||||||
allowKeychainPrompt: false,
|
allowKeychainPrompt: false,
|
||||||
});
|
});
|
||||||
const authChoice = (await prompter.select({
|
const authChoice = await promptAuthChoiceGrouped({
|
||||||
message: "Model/auth choice",
|
prompter,
|
||||||
options: buildAuthChoiceOptions({
|
store: authStore,
|
||||||
store: authStore,
|
includeSkip: true,
|
||||||
includeSkip: true,
|
includeClaudeCliIfMissing: true,
|
||||||
includeClaudeCliIfMissing: true,
|
});
|
||||||
}),
|
|
||||||
})) as AuthChoice;
|
|
||||||
|
|
||||||
const authResult = await applyAuthChoice({
|
const authResult = await applyAuthChoice({
|
||||||
authChoice,
|
authChoice,
|
||||||
|
|||||||
@@ -12,6 +12,72 @@ export type AuthChoiceOption = {
|
|||||||
hint?: string;
|
hint?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AuthChoiceGroupId =
|
||||||
|
| "openai"
|
||||||
|
| "anthropic"
|
||||||
|
| "google"
|
||||||
|
| "openrouter"
|
||||||
|
| "zai"
|
||||||
|
| "opencode-zen"
|
||||||
|
| "minimax";
|
||||||
|
|
||||||
|
export type AuthChoiceGroup = {
|
||||||
|
value: AuthChoiceGroupId;
|
||||||
|
label: string;
|
||||||
|
hint?: string;
|
||||||
|
options: AuthChoiceOption[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const AUTH_CHOICE_GROUP_DEFS: {
|
||||||
|
value: AuthChoiceGroupId;
|
||||||
|
label: string;
|
||||||
|
hint?: string;
|
||||||
|
choices: AuthChoice[];
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
value: "openai",
|
||||||
|
label: "OpenAI",
|
||||||
|
hint: "Codex OAuth + API key",
|
||||||
|
choices: ["codex-cli", "openai-codex", "openai-api-key"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "anthropic",
|
||||||
|
label: "Anthropic",
|
||||||
|
hint: "Claude CLI + API key",
|
||||||
|
choices: ["claude-cli", "setup-token", "token", "apiKey"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "google",
|
||||||
|
label: "Google",
|
||||||
|
hint: "Antigravity + Gemini API key",
|
||||||
|
choices: ["antigravity", "gemini-api-key"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "openrouter",
|
||||||
|
label: "OpenRouter",
|
||||||
|
hint: "API key",
|
||||||
|
choices: ["openrouter-api-key"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "zai",
|
||||||
|
label: "Z.AI (GLM 4.7)",
|
||||||
|
hint: "API key",
|
||||||
|
choices: ["zai-api-key"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "opencode-zen",
|
||||||
|
label: "OpenCode Zen",
|
||||||
|
hint: "API key",
|
||||||
|
choices: ["opencode-zen"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "minimax",
|
||||||
|
label: "MiniMax",
|
||||||
|
hint: "Hosted + LM Studio + API",
|
||||||
|
choices: ["minimax-cloud", "minimax", "minimax-api"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function formatOAuthHint(
|
function formatOAuthHint(
|
||||||
expires?: number,
|
expires?: number,
|
||||||
opts?: { allowStale?: boolean },
|
opts?: { allowStale?: boolean },
|
||||||
@@ -98,7 +164,7 @@ export function buildAuthChoiceOptions(params: {
|
|||||||
label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)",
|
label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)",
|
||||||
});
|
});
|
||||||
options.push({ value: "gemini-api-key", label: "Google Gemini API key" });
|
options.push({ value: "gemini-api-key", label: "Google Gemini API key" });
|
||||||
options.push({ value: "zai-api-key", label: "Z.AI (GLM) API key" });
|
options.push({ value: "zai-api-key", label: "Z.AI (GLM 4.7) API key" });
|
||||||
options.push({ value: "apiKey", label: "Anthropic API key" });
|
options.push({ value: "apiKey", label: "Anthropic API key" });
|
||||||
// Token flow is currently Anthropic-only; use CLI for advanced providers.
|
// Token flow is currently Anthropic-only; use CLI for advanced providers.
|
||||||
options.push({
|
options.push({
|
||||||
@@ -118,3 +184,34 @@ export function buildAuthChoiceOptions(params: {
|
|||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildAuthChoiceGroups(params: {
|
||||||
|
store: AuthProfileStore;
|
||||||
|
includeSkip: boolean;
|
||||||
|
includeClaudeCliIfMissing?: boolean;
|
||||||
|
platform?: NodeJS.Platform;
|
||||||
|
}): {
|
||||||
|
groups: AuthChoiceGroup[];
|
||||||
|
skipOption?: AuthChoiceOption;
|
||||||
|
} {
|
||||||
|
const options = buildAuthChoiceOptions({
|
||||||
|
...params,
|
||||||
|
includeSkip: false,
|
||||||
|
});
|
||||||
|
const optionByValue = new Map<AuthChoice, AuthChoiceOption>(
|
||||||
|
options.map((opt) => [opt.value, opt]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const groups = AUTH_CHOICE_GROUP_DEFS.map((group) => ({
|
||||||
|
...group,
|
||||||
|
options: group.choices
|
||||||
|
.map((choice) => optionByValue.get(choice))
|
||||||
|
.filter((opt): opt is AuthChoiceOption => Boolean(opt)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const skipOption = params.includeSkip
|
||||||
|
? ({ value: "skip", label: "Skip for now" } satisfies AuthChoiceOption)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return { groups, skipOption };
|
||||||
|
}
|
||||||
|
|||||||
63
src/commands/auth-choice-prompt.ts
Normal file
63
src/commands/auth-choice-prompt.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
||||||
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
|
import { buildAuthChoiceGroups } from "./auth-choice-options.js";
|
||||||
|
import type { AuthChoice } from "./onboard-types.js";
|
||||||
|
|
||||||
|
const BACK_VALUE = "__back";
|
||||||
|
|
||||||
|
export async function promptAuthChoiceGrouped(params: {
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
store: AuthProfileStore;
|
||||||
|
includeSkip: boolean;
|
||||||
|
includeClaudeCliIfMissing?: boolean;
|
||||||
|
platform?: NodeJS.Platform;
|
||||||
|
}): Promise<AuthChoice> {
|
||||||
|
const { groups, skipOption } = buildAuthChoiceGroups(params);
|
||||||
|
const availableGroups = groups.filter((group) => group.options.length > 0);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const providerOptions = [
|
||||||
|
...availableGroups.map((group) => ({
|
||||||
|
value: group.value,
|
||||||
|
label: group.label,
|
||||||
|
hint: group.hint,
|
||||||
|
})),
|
||||||
|
...(skipOption ? [skipOption] : []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const providerSelection = (await params.prompter.select({
|
||||||
|
message: "Model/auth provider",
|
||||||
|
options: providerOptions,
|
||||||
|
})) as string;
|
||||||
|
|
||||||
|
if (providerSelection === "skip") {
|
||||||
|
return "skip";
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = availableGroups.find(
|
||||||
|
(candidate) => candidate.value === providerSelection,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!group || group.options.length === 0) {
|
||||||
|
await params.prompter.note(
|
||||||
|
"No auth methods available for that provider.",
|
||||||
|
"Model/auth choice",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodSelection = (await params.prompter.select({
|
||||||
|
message: `${group.label} auth method`,
|
||||||
|
options: [
|
||||||
|
...group.options,
|
||||||
|
{ value: BACK_VALUE, label: "Back" },
|
||||||
|
],
|
||||||
|
})) as string;
|
||||||
|
|
||||||
|
if (methodSelection === BACK_VALUE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return methodSelection as AuthChoice;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import path from "node:path";
|
|||||||
import {
|
import {
|
||||||
confirm as clackConfirm,
|
confirm as clackConfirm,
|
||||||
intro as clackIntro,
|
intro as clackIntro,
|
||||||
multiselect as clackMultiselect,
|
|
||||||
outro as clackOutro,
|
outro as clackOutro,
|
||||||
select as clackSelect,
|
select as clackSelect,
|
||||||
text as clackText,
|
text as clackText,
|
||||||
@@ -41,7 +40,7 @@ import {
|
|||||||
applyAuthChoice,
|
applyAuthChoice,
|
||||||
resolvePreferredProviderForAuthChoice,
|
resolvePreferredProviderForAuthChoice,
|
||||||
} from "./auth-choice.js";
|
} from "./auth-choice.js";
|
||||||
import { buildAuthChoiceOptions } from "./auth-choice-options.js";
|
import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||||
@@ -64,7 +63,6 @@ import {
|
|||||||
import { setupProviders } from "./onboard-providers.js";
|
import { setupProviders } from "./onboard-providers.js";
|
||||||
import { promptRemoteGatewayConfig } from "./onboard-remote.js";
|
import { promptRemoteGatewayConfig } from "./onboard-remote.js";
|
||||||
import { setupSkills } from "./onboard-skills.js";
|
import { setupSkills } from "./onboard-skills.js";
|
||||||
import type { AuthChoice } from "./onboard-types.js";
|
|
||||||
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
||||||
|
|
||||||
export const CONFIGURE_WIZARD_SECTIONS = [
|
export const CONFIGURE_WIZARD_SECTIONS = [
|
||||||
@@ -110,16 +108,92 @@ const select = <T>(params: Parameters<typeof clackSelect<T>>[0]) =>
|
|||||||
: { ...opt, hint: stylePromptHint(opt.hint) },
|
: { ...opt, hint: stylePromptHint(opt.hint) },
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
const multiselect = <T>(params: Parameters<typeof clackMultiselect<T>>[0]) =>
|
|
||||||
clackMultiselect({
|
const CONFIGURE_SECTION_OPTIONS: {
|
||||||
...params,
|
value: WizardSection;
|
||||||
message: stylePromptMessage(params.message),
|
label: string;
|
||||||
options: params.options.map((opt) =>
|
hint: string;
|
||||||
opt.hint === undefined
|
}[] = [
|
||||||
? opt
|
{
|
||||||
: { ...opt, hint: stylePromptHint(opt.hint) },
|
value: "workspace",
|
||||||
),
|
label: "Workspace",
|
||||||
});
|
hint: "Set default workspace + ensure sessions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "model",
|
||||||
|
label: "Model/auth",
|
||||||
|
hint: "Pick model + auth profile sources",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "gateway",
|
||||||
|
label: "Gateway config",
|
||||||
|
hint: "Port/bind/auth/control UI settings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "daemon",
|
||||||
|
label: "Gateway daemon",
|
||||||
|
hint: "Install/manage the background service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "providers",
|
||||||
|
label: "Providers",
|
||||||
|
hint: "Link WhatsApp/Telegram/etc and defaults",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "skills",
|
||||||
|
label: "Skills",
|
||||||
|
hint: "Install/enable workspace skills",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "health",
|
||||||
|
label: "Health check",
|
||||||
|
hint: "Run gateway + provider checks",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function promptConfigureSections(
|
||||||
|
runtime: RuntimeEnv,
|
||||||
|
): Promise<WizardSection[]> {
|
||||||
|
const selected: WizardSection[] = [];
|
||||||
|
let remaining = CONFIGURE_SECTION_OPTIONS.slice();
|
||||||
|
let addMore = true;
|
||||||
|
|
||||||
|
while (addMore && remaining.length > 0) {
|
||||||
|
const choice = guardCancel(
|
||||||
|
await select<WizardSection>({
|
||||||
|
message:
|
||||||
|
selected.length === 0
|
||||||
|
? "Select a section to configure"
|
||||||
|
: "Select another section to configure",
|
||||||
|
options: remaining,
|
||||||
|
initialValue: remaining[0]?.value,
|
||||||
|
}),
|
||||||
|
runtime,
|
||||||
|
) as WizardSection;
|
||||||
|
|
||||||
|
if (!selected.includes(choice)) {
|
||||||
|
selected.push(choice);
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining = CONFIGURE_SECTION_OPTIONS.filter(
|
||||||
|
(option) => !selected.includes(option.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (remaining.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMore = guardCancel(
|
||||||
|
await confirm({
|
||||||
|
message: "Configure another section?",
|
||||||
|
initialValue: false,
|
||||||
|
}),
|
||||||
|
runtime,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
async function promptGatewayConfig(
|
async function promptGatewayConfig(
|
||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
@@ -294,15 +368,13 @@ async function promptAuthConfig(
|
|||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
prompter: WizardPrompter,
|
prompter: WizardPrompter,
|
||||||
): Promise<ClawdbotConfig> {
|
): Promise<ClawdbotConfig> {
|
||||||
const authChoice: AuthChoice = await prompter.select({
|
const authChoice = await promptAuthChoiceGrouped({
|
||||||
message: "Model/auth choice",
|
prompter,
|
||||||
options: buildAuthChoiceOptions({
|
store: ensureAuthProfileStore(undefined, {
|
||||||
store: ensureAuthProfileStore(undefined, {
|
allowKeychainPrompt: false,
|
||||||
allowKeychainPrompt: false,
|
|
||||||
}),
|
|
||||||
includeSkip: true,
|
|
||||||
includeClaudeCliIfMissing: true,
|
|
||||||
}),
|
}),
|
||||||
|
includeSkip: true,
|
||||||
|
includeClaudeCliIfMissing: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let next = cfg;
|
let next = cfg;
|
||||||
@@ -596,49 +668,7 @@ export async function runConfigureWizard(
|
|||||||
|
|
||||||
const selected = opts.sections
|
const selected = opts.sections
|
||||||
? opts.sections
|
? opts.sections
|
||||||
: (guardCancel(
|
: await promptConfigureSections(runtime);
|
||||||
await multiselect({
|
|
||||||
message: "Select sections to configure",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: "workspace",
|
|
||||||
label: "Workspace",
|
|
||||||
hint: "Set default workspace + ensure sessions",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "model",
|
|
||||||
label: "Model/auth",
|
|
||||||
hint: "Pick model + auth profile sources",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "gateway",
|
|
||||||
label: "Gateway config",
|
|
||||||
hint: "Port/bind/auth/control UI settings",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "daemon",
|
|
||||||
label: "Gateway daemon",
|
|
||||||
hint: "Install/manage the background service",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "providers",
|
|
||||||
label: "Providers",
|
|
||||||
hint: "Link WhatsApp/Telegram/etc and defaults",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "skills",
|
|
||||||
label: "Skills",
|
|
||||||
hint: "Install/enable workspace skills",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "health",
|
|
||||||
label: "Health check",
|
|
||||||
hint: "Run gateway + provider checks",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
) as WizardSection[]);
|
|
||||||
|
|
||||||
if (!selected || selected.length === 0) {
|
if (!selected || selected.length === 0) {
|
||||||
outro("No changes selected.");
|
outro("No changes selected.");
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
resolvePreferredProviderForAuthChoice,
|
resolvePreferredProviderForAuthChoice,
|
||||||
warnIfModelConfigLooksOff,
|
warnIfModelConfigLooksOff,
|
||||||
} from "../commands/auth-choice.js";
|
} from "../commands/auth-choice.js";
|
||||||
import { buildAuthChoiceOptions } from "../commands/auth-choice-options.js";
|
import { promptAuthChoiceGrouped } from "../commands/auth-choice-prompt.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||||
@@ -37,7 +37,6 @@ import { setupProviders } from "../commands/onboard-providers.js";
|
|||||||
import { promptRemoteGatewayConfig } from "../commands/onboard-remote.js";
|
import { promptRemoteGatewayConfig } from "../commands/onboard-remote.js";
|
||||||
import { setupSkills } from "../commands/onboard-skills.js";
|
import { setupSkills } from "../commands/onboard-skills.js";
|
||||||
import type {
|
import type {
|
||||||
AuthChoice,
|
|
||||||
GatewayAuthChoice,
|
GatewayAuthChoice,
|
||||||
OnboardMode,
|
OnboardMode,
|
||||||
OnboardOptions,
|
OnboardOptions,
|
||||||
@@ -333,14 +332,12 @@ export async function runOnboardingWizard(
|
|||||||
const authChoiceFromPrompt = opts.authChoice === undefined;
|
const authChoiceFromPrompt = opts.authChoice === undefined;
|
||||||
const authChoice =
|
const authChoice =
|
||||||
opts.authChoice ??
|
opts.authChoice ??
|
||||||
((await prompter.select({
|
(await promptAuthChoiceGrouped({
|
||||||
message: "Model/auth choice",
|
prompter,
|
||||||
options: buildAuthChoiceOptions({
|
store: authStore,
|
||||||
store: authStore,
|
includeSkip: true,
|
||||||
includeSkip: true,
|
includeClaudeCliIfMissing: true,
|
||||||
includeClaudeCliIfMissing: true,
|
}));
|
||||||
}),
|
|
||||||
})) as AuthChoice);
|
|
||||||
|
|
||||||
const authResult = await applyAuthChoice({
|
const authResult = await applyAuthChoice({
|
||||||
authChoice,
|
authChoice,
|
||||||
|
|||||||
Reference in New Issue
Block a user