diff --git a/tools/cli/installers/lib/ide/antigravity.js b/tools/cli/installers/lib/ide/antigravity.js index 57071cdc..9de0cfbf 100644 --- a/tools/cli/installers/lib/ide/antigravity.js +++ b/tools/cli/installers/lib/ide/antigravity.js @@ -345,7 +345,7 @@ class AntigravitySetup extends BaseIdeSetup { }; const selected = await prompts.multiselect({ - message: `Select subagents to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`, + message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`, choices: subagentConfig.files.map((file) => ({ name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`, value: file, diff --git a/tools/cli/installers/lib/ide/claude-code.js b/tools/cli/installers/lib/ide/claude-code.js index 02c65b40..1da64f81 100644 --- a/tools/cli/installers/lib/ide/claude-code.js +++ b/tools/cli/installers/lib/ide/claude-code.js @@ -353,7 +353,7 @@ class ClaudeCodeSetup extends BaseIdeSetup { }; const selected = await prompts.multiselect({ - message: `Select subagents to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`, + message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`, options: subagentConfig.files.map((file) => ({ label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`, value: file, diff --git a/tools/cli/lib/prompts.js b/tools/cli/lib/prompts.js index 96b80ba1..5d85e2b4 100644 --- a/tools/cli/lib/prompts.js +++ b/tools/cli/lib/prompts.js @@ -184,6 +184,7 @@ async function groupMultiselect(options) { options: options.options, initialValues: options.initialValues, required: options.required || false, + selectableGroups: options.selectableGroups || false, }); await handleCancel(result); diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 2080e868..f14a77cb 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -395,7 +395,7 @@ class UI { const processedIdes = new Set(); const initialValues = []; - // First, add previously configured IDEs at the top, marked with ✅ + // First, add previously configured IDEs, marked with ✅ if (configuredIdes.length > 0) { const configuredGroup = []; for (const ideValue of configuredIdes) { @@ -447,42 +447,33 @@ class UI { })); } + // Add standalone "None" option at the end + groupedOptions[' '] = [ + { + label: '⚠ None - I am not installing any tools', + value: '__NONE__', + }, + ]; + let selectedIdes = []; - let userConfirmedNoTools = false; - // Loop until user selects at least one tool OR explicitly confirms no tools - while (!userConfirmedNoTools) { - selectedIdes = await prompts.groupMultiselect({ - message: `Select tools to configure ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`, - options: groupedOptions, - initialValues: initialValues.length > 0 ? initialValues : undefined, - required: false, - }); + selectedIdes = await prompts.groupMultiselect({ + message: `Select tools to configure ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`, + options: groupedOptions, + initialValues: initialValues.length > 0 ? initialValues : undefined, + required: true, + selectableGroups: false, + }); - // If tools were selected, we're done - if (selectedIdes && selectedIdes.length > 0) { - break; - } - - // Warn that no tools were selected - users often miss the spacebar requirement + // If user selected both "__NONE__" and other tools, honor the "None" choice + if (selectedIdes && selectedIdes.includes('__NONE__') && selectedIdes.length > 1) { console.log(); - console.log(chalk.red.bold('⚠️ WARNING: No tools were selected!')); - console.log(chalk.red(' You must press SPACE to select items, then ENTER to confirm.')); - console.log(chalk.red(' Simply highlighting an item does NOT select it.')); + console.log(chalk.yellow('⚠️ "None - I am not installing any tools" was selected, so no tools will be configured.')); console.log(); - - const goBack = await prompts.confirm({ - message: chalk.yellow('Would you like to go back and select at least one tool?'), - default: true, - }); - - if (goBack) { - // Re-display a message before looping back - console.log(); - } else { - // User explicitly chose to proceed without tools - userConfirmedNoTools = true; - } + selectedIdes = []; + } else if (selectedIdes && selectedIdes.includes('__NONE__')) { + // Only "__NONE__" was selected + selectedIdes = []; } return { @@ -509,27 +500,6 @@ class UI { return { backupFirst, preserveCustomizations }; } - /** - * Prompt for module selection - * @param {Array} modules - Available modules - * @returns {Array} Selected modules - */ - async promptModules(modules) { - const choices = modules.map((mod) => ({ - name: `${mod.name} - ${mod.description}`, - value: mod.id, - checked: false, - })); - - const selectedModules = await prompts.multiselect({ - message: `Select modules to add ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`, - choices, - required: true, - }); - - return selectedModules; - } - /** * Confirm action * @param {string} message - Confirmation message @@ -697,20 +667,40 @@ class UI { * @param {Array} moduleChoices - Available module choices * @returns {Array} Selected module IDs */ - async selectModules(moduleChoices, defaultSelections = []) { - // Mark choices as checked based on defaultSelections + async selectModules(moduleChoices, defaultSelections = null) { + // If defaultSelections is provided, use it to override checked state + // Otherwise preserve the checked state from moduleChoices (set by getModuleChoices) const choicesWithDefaults = moduleChoices.map((choice) => ({ ...choice, - checked: defaultSelections.includes(choice.value), + ...(defaultSelections === null ? {} : { checked: defaultSelections.includes(choice.value) }), })); + // Add a "None" option at the end for users who changed their mind + const choicesWithSkipOption = [ + ...choicesWithDefaults, + { + value: '__NONE__', + label: '⚠ None / I changed my mind - skip module installation', + checked: false, + }, + ]; + const selected = await prompts.multiselect({ - message: `Select modules to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`, - choices: choicesWithDefaults, - required: false, + message: `Select modules to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`, + choices: choicesWithSkipOption, + required: true, }); - return selected || []; + // If user selected both "__NONE__" and other items, honor the "None" choice + if (selected && selected.includes('__NONE__') && selected.length > 1) { + console.log(); + console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no modules will be installed.')); + console.log(); + return []; + } + + // Filter out the special '__NONE__' value + return selected ? selected.filter((m) => m !== '__NONE__') : []; } /** @@ -1255,12 +1245,32 @@ class UI { checked: m.checked, })); + // Add "None / I changed my mind" option at the end + const choicesWithSkip = [ + ...selectChoices, + { + name: '⚠ None / I changed my mind - keep no custom modules', + value: '__NONE__', + checked: false, + }, + ]; + const keepModules = await prompts.multiselect({ - message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`, - choices: selectChoices, - required: false, + message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`, + choices: choicesWithSkip, + required: true, }); - result.selectedCustomModules = keepModules || []; + + // If user selected both "__NONE__" and other modules, honor the "None" choice + if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) { + console.log(); + console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no custom modules will be kept.')); + console.log(); + result.selectedCustomModules = []; + } else { + // Filter out the special '__NONE__' value + result.selectedCustomModules = keepModules ? keepModules.filter((m) => m !== '__NONE__') : []; + } break; }