feat: Update @clack/prompts to v1.0.0 and Add autocompleteMultiselect prompt (#1514)
* feat: Update @clack/prompts to v1.0.0 and Add autocompleteMultiselect prompt * fix(cli): flexible tool selection (skip recommended or additional) + fix spacing * feat(cli): improve tool selection UX with autocomplete and upgrade path * feat(cli): display selected tools after IDE selection with preferred markers * fix: formatting * fix: make selection message more clear * fix: formatting * fix: Remove redundant colon --------- Co-authored-by: Brian <bmadcode@gmail.com>
This commit is contained in:
parent
5b80649d3a
commit
2d9ebcaf2f
|
|
@ -9,7 +9,8 @@
|
|||
"version": "6.0.0-Beta.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@clack/core": "^1.0.0",
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@kayvan/markdown-tree-parser": "^1.6.1",
|
||||
"boxen": "^5.1.2",
|
||||
"chalk": "^4.1.2",
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
"ignore": "^7.0.5",
|
||||
"js-yaml": "^4.1.0",
|
||||
"ora": "^5.4.1",
|
||||
"picocolors": "^1.1.1",
|
||||
"semver": "^7.6.3",
|
||||
"wrap-ansi": "^7.0.0",
|
||||
"xml2js": "^0.6.2",
|
||||
|
|
@ -756,9 +758,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@clack/core": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz",
|
||||
"integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0.tgz",
|
||||
"integrity": "sha512-Orf9Ltr5NeiEuVJS8Rk2XTw3IxNC2Bic3ash7GgYeA8LJ/zmSNpSQ/m5UAhe03lA6KFgklzZ5KTHs4OAMA/SAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picocolors": "^1.0.0",
|
||||
|
|
@ -766,12 +768,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@clack/prompts": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz",
|
||||
"integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.0.0.tgz",
|
||||
"integrity": "sha512-rWPXg9UaCFqErJVQ+MecOaWsozjaxol4yjnmYcGNipAWzdaWa2x+VJmKfGq7L0APwBohQOYdHC+9RO4qRXej+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@clack/core": "0.5.0",
|
||||
"@clack/core": "1.0.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"sisteransi": "^1.0.5"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@clack/core": "^1.0.0",
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@kayvan/markdown-tree-parser": "^1.6.1",
|
||||
"boxen": "^5.1.2",
|
||||
"chalk": "^4.1.2",
|
||||
|
|
@ -82,6 +83,7 @@
|
|||
"ignore": "^7.0.5",
|
||||
"js-yaml": "^4.1.0",
|
||||
"ora": "^5.4.1",
|
||||
"picocolors": "^1.1.1",
|
||||
"semver": "^7.6.3",
|
||||
"wrap-ansi": "^7.0.0",
|
||||
"xml2js": "^0.6.2",
|
||||
|
|
|
|||
|
|
@ -586,7 +586,11 @@ class ConfigCollector {
|
|||
console.log();
|
||||
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
||||
let customize = true;
|
||||
if (moduleName !== 'core') {
|
||||
if (moduleName === 'core') {
|
||||
// Core module: no confirm prompt, so add spacing manually to match visual style
|
||||
console.log(chalk.gray('│'));
|
||||
} else {
|
||||
// Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing)
|
||||
const customizeAnswer = await prompts.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
|
|
|
|||
|
|
@ -695,9 +695,6 @@ class Installer {
|
|||
config.skipIde = toolSelection.skipIde;
|
||||
const ideConfigurations = toolSelection.configurations;
|
||||
|
||||
// Add spacing after prompts before installation progress
|
||||
console.log('');
|
||||
|
||||
if (spinner.isSpinning) {
|
||||
spinner.text = 'Continuing installation...';
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
*/
|
||||
|
||||
let _clack = null;
|
||||
let _clackCore = null;
|
||||
let _picocolors = null;
|
||||
|
||||
/**
|
||||
* Lazy-load @clack/prompts (ESM module)
|
||||
|
|
@ -20,6 +22,28 @@ async function getClack() {
|
|||
return _clack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy-load @clack/core (ESM module)
|
||||
* @returns {Promise<Object>} The clack core module
|
||||
*/
|
||||
async function getClackCore() {
|
||||
if (!_clackCore) {
|
||||
_clackCore = await import('@clack/core');
|
||||
}
|
||||
return _clackCore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy-load picocolors
|
||||
* @returns {Promise<Object>} The picocolors module
|
||||
*/
|
||||
async function getPicocolors() {
|
||||
if (!_picocolors) {
|
||||
_picocolors = (await import('picocolors')).default;
|
||||
}
|
||||
return _picocolors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user cancellation gracefully
|
||||
* @param {any} value - The value to check
|
||||
|
|
@ -191,6 +215,118 @@ async function groupMultiselect(options) {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default filter function for autocomplete - case-insensitive label matching
|
||||
* @param {string} search - Search string
|
||||
* @param {Object} option - Option object with label
|
||||
* @returns {boolean} Whether the option matches
|
||||
*/
|
||||
function defaultAutocompleteFilter(search, option) {
|
||||
const label = option.label ?? String(option.value ?? '');
|
||||
return label.toLowerCase().includes(search.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Autocomplete multi-select prompt with type-ahead filtering
|
||||
* Custom implementation that always shows "Space/Tab:" in the hint
|
||||
* @param {Object} options - Prompt options
|
||||
* @param {string} options.message - The question to ask
|
||||
* @param {Array} options.options - Array of choices [{label, value, hint?}]
|
||||
* @param {string} [options.placeholder] - Placeholder text for search input
|
||||
* @param {Array} [options.initialValues] - Array of initially selected values
|
||||
* @param {boolean} [options.required=false] - Whether at least one must be selected
|
||||
* @param {number} [options.maxItems=5] - Maximum visible items in scrollable list
|
||||
* @param {Function} [options.filter] - Custom filter function (search, option) => boolean
|
||||
* @returns {Promise<Array>} Array of selected values
|
||||
*/
|
||||
async function autocompleteMultiselect(options) {
|
||||
const core = await getClackCore();
|
||||
const clack = await getClack();
|
||||
const color = await getPicocolors();
|
||||
|
||||
const filterFn = options.filter ?? defaultAutocompleteFilter;
|
||||
|
||||
const prompt = new core.AutocompletePrompt({
|
||||
options: options.options,
|
||||
multiple: true,
|
||||
filter: filterFn,
|
||||
validate: () => {
|
||||
if (options.required && prompt.selectedValues.length === 0) {
|
||||
return 'Please select at least one item';
|
||||
}
|
||||
},
|
||||
initialValue: options.initialValues,
|
||||
render() {
|
||||
const barColor = this.state === 'error' ? color.yellow : color.cyan;
|
||||
const bar = barColor(clack.S_BAR);
|
||||
const barEnd = barColor(clack.S_BAR_END);
|
||||
|
||||
const title = `${color.gray(clack.S_BAR)}\n${clack.symbol(this.state)} ${options.message}\n`;
|
||||
|
||||
const userInput = this.userInput;
|
||||
const placeholder = options.placeholder || 'Type to search...';
|
||||
const hasPlaceholder = userInput === '' && placeholder !== undefined;
|
||||
|
||||
// Show placeholder or user input with cursor
|
||||
const searchDisplay =
|
||||
this.isNavigating || hasPlaceholder ? color.dim(hasPlaceholder ? placeholder : userInput) : this.userInputWithCursor;
|
||||
|
||||
const allOptions = this.options;
|
||||
const matchCount =
|
||||
this.filteredOptions.length === allOptions.length
|
||||
? ''
|
||||
: color.dim(` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`);
|
||||
|
||||
// Render option with checkbox
|
||||
const renderOption = (opt, isHighlighted) => {
|
||||
const isSelected = this.selectedValues.includes(opt.value);
|
||||
const label = opt.label ?? String(opt.value ?? '');
|
||||
const hintText = opt.hint && opt.value === this.focusedValue ? color.dim(` (${opt.hint})`) : '';
|
||||
const checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE);
|
||||
return isHighlighted ? `${checkbox} ${label}${hintText}` : `${checkbox} ${color.dim(label)}`;
|
||||
};
|
||||
|
||||
switch (this.state) {
|
||||
case 'submit': {
|
||||
return `${title}${color.gray(clack.S_BAR)} ${color.dim(`${this.selectedValues.length} items selected`)}`;
|
||||
}
|
||||
|
||||
case 'cancel': {
|
||||
return `${title}${color.gray(clack.S_BAR)} ${color.strikethrough(color.dim(userInput))}`;
|
||||
}
|
||||
|
||||
default: {
|
||||
// Always show "SPACE:" regardless of isNavigating state
|
||||
const hints = [`${color.dim('↑/↓')} to navigate`, `${color.dim('TAB/SPACE:')} select`, `${color.dim('ENTER:')} confirm`];
|
||||
|
||||
const noMatchesLine = this.filteredOptions.length === 0 && userInput ? [`${bar} ${color.yellow('No matches found')}`] : [];
|
||||
|
||||
const errorLine = this.state === 'error' ? [`${bar} ${color.yellow(this.error)}`] : [];
|
||||
|
||||
const headerLines = [...`${title}${bar}`.split('\n'), `${bar} ${searchDisplay}${matchCount}`, ...noMatchesLine, ...errorLine];
|
||||
|
||||
const footerLines = [`${bar} ${color.dim(hints.join(' • '))}`, `${barEnd}`];
|
||||
|
||||
const optionLines = clack.limitOptions({
|
||||
cursor: this.cursor,
|
||||
options: this.filteredOptions,
|
||||
style: renderOption,
|
||||
maxItems: options.maxItems || 5,
|
||||
output: options.output,
|
||||
rowPadding: headerLines.length + footerLines.length,
|
||||
});
|
||||
|
||||
return [...headerLines, ...optionLines.map((line) => `${bar} ${line}`), ...footerLines].join('\n');
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const result = await prompt.prompt();
|
||||
await handleCancel(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm prompt (replaces Inquirer 'confirm' type)
|
||||
* @param {Object} options - Prompt options
|
||||
|
|
@ -211,7 +347,12 @@ async function confirm(options) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Text input prompt (replaces Inquirer 'input' type)
|
||||
* Text input prompt with Tab-to-fill-placeholder support (replaces Inquirer 'input' type)
|
||||
*
|
||||
* This custom implementation restores the Tab-to-fill-placeholder behavior that was
|
||||
* intentionally removed in @clack/prompts v1.0.0 (placeholder became purely visual).
|
||||
* Uses @clack/core's TextPrompt primitive with custom key handling.
|
||||
*
|
||||
* @param {Object} options - Prompt options
|
||||
* @param {string} options.message - The question to ask
|
||||
* @param {string} [options.default] - Default value
|
||||
|
|
@ -220,20 +361,64 @@ async function confirm(options) {
|
|||
* @returns {Promise<string>} User's input
|
||||
*/
|
||||
async function text(options) {
|
||||
const clack = await getClack();
|
||||
const core = await getClackCore();
|
||||
const color = await getPicocolors();
|
||||
|
||||
// Use default as placeholder if placeholder not explicitly provided
|
||||
// This shows the default value as grayed-out hint text
|
||||
const placeholder = options.placeholder === undefined ? options.default : options.placeholder;
|
||||
const defaultValue = options.default;
|
||||
|
||||
const result = await clack.text({
|
||||
message: options.message,
|
||||
defaultValue: options.default,
|
||||
placeholder: typeof placeholder === 'string' ? placeholder : undefined,
|
||||
const prompt = new core.TextPrompt({
|
||||
defaultValue,
|
||||
validate: options.validate,
|
||||
render() {
|
||||
const title = `${color.gray('◆')} ${options.message}`;
|
||||
|
||||
// Show placeholder as dim text when input is empty
|
||||
let valueDisplay;
|
||||
if (this.state === 'error') {
|
||||
valueDisplay = color.yellow(this.userInputWithCursor);
|
||||
} else if (this.userInput) {
|
||||
valueDisplay = this.userInputWithCursor;
|
||||
} else if (placeholder) {
|
||||
// Show placeholder with cursor indicator when empty
|
||||
valueDisplay = `${color.inverse(color.hidden('_'))}${color.dim(placeholder)}`;
|
||||
} else {
|
||||
valueDisplay = color.inverse(color.hidden('_'));
|
||||
}
|
||||
|
||||
const bar = color.gray('│');
|
||||
|
||||
// Handle different states
|
||||
if (this.state === 'submit') {
|
||||
return `${color.gray('◇')} ${options.message}\n${bar} ${color.dim(this.value || defaultValue || '')}`;
|
||||
}
|
||||
|
||||
if (this.state === 'cancel') {
|
||||
return `${color.gray('◇')} ${options.message}\n${bar} ${color.strikethrough(color.dim(this.userInput || ''))}`;
|
||||
}
|
||||
|
||||
if (this.state === 'error') {
|
||||
return `${color.yellow('▲')} ${options.message}\n${bar} ${valueDisplay}\n${color.yellow('│')} ${color.yellow(this.error)}`;
|
||||
}
|
||||
|
||||
return `${title}\n${bar} ${valueDisplay}\n${bar}`;
|
||||
},
|
||||
});
|
||||
|
||||
// Add Tab key handler to fill placeholder into input
|
||||
prompt.on('key', (char) => {
|
||||
if (char === '\t' && placeholder && !prompt.userInput) {
|
||||
// Use _setUserInput with write=true to populate the readline and update internal state
|
||||
prompt._setUserInput(placeholder, true);
|
||||
}
|
||||
});
|
||||
|
||||
const result = await prompt.prompt();
|
||||
await handleCancel(result);
|
||||
|
||||
// TextPrompt's finalize handler already applies defaultValue for empty input
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -423,6 +608,7 @@ module.exports = {
|
|||
select,
|
||||
multiselect,
|
||||
groupMultiselect,
|
||||
autocompleteMultiselect,
|
||||
confirm,
|
||||
text,
|
||||
password,
|
||||
|
|
|
|||
|
|
@ -344,6 +344,9 @@ class UI {
|
|||
|
||||
/**
|
||||
* Prompt for tool/IDE selection (called after module configuration)
|
||||
* Uses a split prompt approach:
|
||||
* 1. Recommended tools - standard multiselect for 3 preferred tools
|
||||
* 2. Additional tools - autocompleteMultiselect with search capability
|
||||
* @param {string} projectDir - Project directory to check for existing IDEs
|
||||
* @returns {Object} Tool configuration
|
||||
*/
|
||||
|
|
@ -366,95 +369,190 @@ class UI {
|
|||
const preferredIdes = ideManager.getPreferredIdes();
|
||||
const otherIdes = ideManager.getOtherIdes();
|
||||
|
||||
// Build grouped options object for groupMultiselect
|
||||
const groupedOptions = {};
|
||||
const processedIdes = new Set();
|
||||
const initialValues = [];
|
||||
// Determine which configured IDEs are in "preferred" vs "other" categories
|
||||
const configuredPreferred = configuredIdes.filter((id) => preferredIdes.some((ide) => ide.value === id));
|
||||
const configuredOther = configuredIdes.filter((id) => otherIdes.some((ide) => ide.value === id));
|
||||
|
||||
// First, add previously configured IDEs, marked with ✅
|
||||
if (configuredIdes.length > 0) {
|
||||
const configuredGroup = [];
|
||||
for (const ideValue of configuredIdes) {
|
||||
// Skip empty or invalid IDE values
|
||||
if (!ideValue || typeof ideValue !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the IDE in either preferred or other lists
|
||||
const preferredIde = preferredIdes.find((ide) => ide.value === ideValue);
|
||||
const otherIde = otherIdes.find((ide) => ide.value === ideValue);
|
||||
const ide = preferredIde || otherIde;
|
||||
|
||||
if (ide) {
|
||||
configuredGroup.push({
|
||||
label: `${ide.name} ✅`,
|
||||
value: ide.value,
|
||||
});
|
||||
processedIdes.add(ide.value);
|
||||
initialValues.push(ide.value); // Pre-select configured IDEs
|
||||
} else {
|
||||
// Warn about unrecognized IDE (but don't fail)
|
||||
console.log(chalk.yellow(`⚠️ Previously configured IDE '${ideValue}' is no longer available`));
|
||||
}
|
||||
}
|
||||
if (configuredGroup.length > 0) {
|
||||
groupedOptions['Previously Configured'] = configuredGroup;
|
||||
}
|
||||
// Warn about previously configured tools that are no longer available
|
||||
const allKnownValues = new Set([...preferredIdes, ...otherIdes].map((ide) => ide.value));
|
||||
const unknownTools = configuredIdes.filter((id) => id && typeof id === 'string' && !allKnownValues.has(id));
|
||||
if (unknownTools.length > 0) {
|
||||
console.log(chalk.yellow(`⚠️ Previously configured tools are no longer available: ${unknownTools.join(', ')}`));
|
||||
}
|
||||
|
||||
// Add preferred tools (excluding already processed)
|
||||
const remainingPreferred = preferredIdes.filter((ide) => !processedIdes.has(ide.value));
|
||||
if (remainingPreferred.length > 0) {
|
||||
groupedOptions['Recommended Tools'] = remainingPreferred.map((ide) => {
|
||||
processedIdes.add(ide.value);
|
||||
return {
|
||||
label: `${ide.name} ⭐`,
|
||||
value: ide.value,
|
||||
};
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// UPGRADE PATH: If tools already configured, show all tools with configured at top
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
if (configuredIdes.length > 0) {
|
||||
const allTools = [...preferredIdes, ...otherIdes];
|
||||
|
||||
// Sort: configured tools first, then preferred, then others
|
||||
const sortedTools = [
|
||||
...allTools.filter((ide) => configuredIdes.includes(ide.value)),
|
||||
...allTools.filter((ide) => !configuredIdes.includes(ide.value)),
|
||||
];
|
||||
|
||||
const upgradeOptions = sortedTools.map((ide) => {
|
||||
const isConfigured = configuredIdes.includes(ide.value);
|
||||
const isPreferred = preferredIdes.some((p) => p.value === ide.value);
|
||||
let label = ide.name;
|
||||
if (isPreferred) label += ' ⭐';
|
||||
if (isConfigured) label += ' ✅';
|
||||
return { label, value: ide.value };
|
||||
});
|
||||
|
||||
// Sort initialValues to match display order
|
||||
const sortedInitialValues = sortedTools.filter((ide) => configuredIdes.includes(ide.value)).map((ide) => ide.value);
|
||||
|
||||
const upgradeSelected = await prompts.autocompleteMultiselect({
|
||||
message: 'Integrate with',
|
||||
options: upgradeOptions,
|
||||
initialValues: sortedInitialValues,
|
||||
required: false,
|
||||
maxItems: 8,
|
||||
});
|
||||
|
||||
const selectedIdes = upgradeSelected || [];
|
||||
|
||||
if (selectedIdes.length === 0) {
|
||||
console.log('');
|
||||
const confirmNoTools = await prompts.confirm({
|
||||
message: 'No tools selected. Continue without installing any tools?',
|
||||
default: false,
|
||||
});
|
||||
|
||||
if (!confirmNoTools) {
|
||||
return this.promptToolSelection(projectDir);
|
||||
}
|
||||
|
||||
return { ides: [], skipIde: true };
|
||||
}
|
||||
|
||||
// Display selected tools
|
||||
this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||
|
||||
return { ides: selectedIdes, skipIde: false };
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// NEW INSTALL: Show recommended tools first with "Browse All" option
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
const recommendedOptions = preferredIdes.map((ide) => {
|
||||
const isConfigured = configuredPreferred.includes(ide.value);
|
||||
return {
|
||||
label: isConfigured ? `${ide.name} ⭐ ✅` : `${ide.name} ⭐`,
|
||||
value: ide.value,
|
||||
};
|
||||
});
|
||||
|
||||
// Add "browse all" option at the end if there are additional tools
|
||||
if (otherIdes.length > 0) {
|
||||
const totalTools = preferredIdes.length + otherIdes.length;
|
||||
recommendedOptions.push({
|
||||
label: `→ Browse all supported tools (${totalTools} total)...`,
|
||||
value: '__BROWSE_ALL__',
|
||||
});
|
||||
}
|
||||
|
||||
// Add other tools (excluding already processed)
|
||||
const remainingOther = otherIdes.filter((ide) => !processedIdes.has(ide.value));
|
||||
if (remainingOther.length > 0) {
|
||||
groupedOptions['Additional Tools'] = remainingOther.map((ide) => ({
|
||||
label: ide.name,
|
||||
value: ide.value,
|
||||
}));
|
||||
}
|
||||
// Pre-select previously configured preferred tools
|
||||
const recommendedInitialValues = configuredPreferred.length > 0 ? configuredPreferred : undefined;
|
||||
|
||||
// Add standalone "None" option at the end
|
||||
groupedOptions[' '] = [
|
||||
{
|
||||
label: '⚠ None - I am not installing any tools',
|
||||
value: '__NONE__',
|
||||
},
|
||||
];
|
||||
|
||||
let selectedIdes = [];
|
||||
|
||||
selectedIdes = await prompts.groupMultiselect({
|
||||
message: `Select tools to configure ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
|
||||
options: groupedOptions,
|
||||
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
||||
required: true,
|
||||
selectableGroups: false,
|
||||
const recommendedSelected = await prompts.multiselect({
|
||||
message: `Integrate with ${chalk.dim('(↑/↓ to navigate • SPACE: select • ENTER: confirm)')}:`,
|
||||
options: recommendedOptions,
|
||||
initialValues: recommendedInitialValues,
|
||||
required: false,
|
||||
});
|
||||
|
||||
// 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.yellow('⚠️ "None - I am not installing any tools" was selected, so no tools will be configured.'));
|
||||
console.log();
|
||||
selectedIdes = [];
|
||||
} else if (selectedIdes && selectedIdes.includes('__NONE__')) {
|
||||
// Only "__NONE__" was selected
|
||||
selectedIdes = [];
|
||||
const selectedRecommended = recommendedSelected || [];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// STEP 2: Handle "Browse All" selection - show additional tools if requested
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
const wantsBrowseAll = selectedRecommended.includes('__BROWSE_ALL__');
|
||||
const filteredRecommended = selectedRecommended.filter((v) => v !== '__BROWSE_ALL__');
|
||||
|
||||
// Show additional tools if:
|
||||
// 1. User explicitly chose "Browse All", OR
|
||||
// 2. User has previously configured "other" tools, OR
|
||||
// 3. User selected no recommended tools (allow them to pick from other tools)
|
||||
const showAdditionalTools = wantsBrowseAll || configuredOther.length > 0 || filteredRecommended.length === 0;
|
||||
|
||||
let selectedAdditionalOrAll = [];
|
||||
|
||||
if (showAdditionalTools) {
|
||||
// Show ALL tools if:
|
||||
// - User explicitly chose "Browse All", OR
|
||||
// - User selected nothing from recommended (so they can pick from everything)
|
||||
// Otherwise, show only "other" tools as additional options
|
||||
const showAllTools = wantsBrowseAll || filteredRecommended.length === 0;
|
||||
const toolsToShow = showAllTools ? [...preferredIdes, ...otherIdes] : otherIdes;
|
||||
|
||||
if (toolsToShow.length > 0) {
|
||||
const allToolOptions = toolsToShow.map((ide) => {
|
||||
const isConfigured = configuredIdes.includes(ide.value);
|
||||
const isPreferred = preferredIdes.some((p) => p.value === ide.value);
|
||||
let label = ide.name;
|
||||
if (isPreferred) label += ' ⭐';
|
||||
if (isConfigured) label += ' ✅';
|
||||
return {
|
||||
label,
|
||||
value: ide.value,
|
||||
};
|
||||
});
|
||||
|
||||
// Pre-select: previously configured tools + any recommended tools already selected
|
||||
const initialValues = [...configuredIdes, ...filteredRecommended].filter((v, i, arr) => arr.indexOf(v) === i); // dedupe
|
||||
|
||||
// Use "additional" only if user already selected some recommended tools
|
||||
const isAdditional = !wantsBrowseAll && filteredRecommended.length > 0;
|
||||
|
||||
console.log('');
|
||||
const selected = await prompts.autocompleteMultiselect({
|
||||
message: isAdditional ? 'Select additional tools:' : 'Select tools:',
|
||||
options: allToolOptions,
|
||||
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
||||
required: false,
|
||||
maxItems: 8,
|
||||
});
|
||||
|
||||
selectedAdditionalOrAll = selected || [];
|
||||
}
|
||||
}
|
||||
|
||||
// Combine selections:
|
||||
// - If "Browse All" was used, the second prompt contains ALL selections
|
||||
// - Otherwise, combine recommended + additional
|
||||
const allSelectedIdes = wantsBrowseAll ? selectedAdditionalOrAll : [...filteredRecommended, ...selectedAdditionalOrAll];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// STEP 3: Confirm if no tools selected
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
if (allSelectedIdes.length === 0) {
|
||||
console.log('');
|
||||
const confirmNoTools = await prompts.confirm({
|
||||
message: 'No tools selected. Continue without installing any tools?',
|
||||
default: false,
|
||||
});
|
||||
|
||||
if (!confirmNoTools) {
|
||||
// User wants to select tools - recurse
|
||||
return this.promptToolSelection(projectDir);
|
||||
}
|
||||
|
||||
return {
|
||||
ides: [],
|
||||
skipIde: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Display selected tools
|
||||
const allTools = [...preferredIdes, ...otherIdes];
|
||||
this.displaySelectedTools(allSelectedIdes, preferredIdes, allTools);
|
||||
|
||||
return {
|
||||
ides: selectedIdes || [],
|
||||
skipIde: !selectedIdes || selectedIdes.length === 0,
|
||||
ides: allSelectedIdes,
|
||||
skipIde: allSelectedIdes.length === 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1655,6 +1753,27 @@ class UI {
|
|||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display list of selected tools after IDE selection
|
||||
* @param {Array} selectedIdes - Array of selected IDE values
|
||||
* @param {Array} preferredIdes - Array of preferred IDE objects
|
||||
* @param {Array} allTools - Array of all tool objects
|
||||
*/
|
||||
displaySelectedTools(selectedIdes, preferredIdes, allTools) {
|
||||
if (selectedIdes.length === 0) return;
|
||||
|
||||
const preferredValues = new Set(preferredIdes.map((ide) => ide.value));
|
||||
|
||||
console.log('');
|
||||
console.log(chalk.dim(' Selected tools:'));
|
||||
for (const ideValue of selectedIdes) {
|
||||
const tool = allTools.find((t) => t.value === ideValue);
|
||||
const name = tool?.name || ideValue;
|
||||
const marker = preferredValues.has(ideValue) ? ' ⭐' : '';
|
||||
console.log(chalk.dim(` • ${name}${marker}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UI };
|
||||
|
|
|
|||
Loading…
Reference in New Issue