Compare commits
4 Commits
7243be195d
...
010c861d86
| Author | SHA1 | Date |
|---|---|---|
|
|
010c861d86 | |
|
|
1ff3323c87 | |
|
|
3e2da9c728 | |
|
|
7a3f623b83 |
|
|
@ -215,8 +215,20 @@ async function groupMultiselect(options) {
|
||||||
return result;
|
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
|
* Autocomplete multi-select prompt with type-ahead filtering
|
||||||
|
* Custom implementation that always shows "Space/Tab:" in the hint
|
||||||
* @param {Object} options - Prompt options
|
* @param {Object} options - Prompt options
|
||||||
* @param {string} options.message - The question to ask
|
* @param {string} options.message - The question to ask
|
||||||
* @param {Array} options.options - Array of choices [{label, value, hint?}]
|
* @param {Array} options.options - Array of choices [{label, value, hint?}]
|
||||||
|
|
@ -228,18 +240,94 @@ async function groupMultiselect(options) {
|
||||||
* @returns {Promise<Array>} Array of selected values
|
* @returns {Promise<Array>} Array of selected values
|
||||||
*/
|
*/
|
||||||
async function autocompleteMultiselect(options) {
|
async function autocompleteMultiselect(options) {
|
||||||
|
const core = await getClackCore();
|
||||||
const clack = await getClack();
|
const clack = await getClack();
|
||||||
|
const color = await getPicocolors();
|
||||||
|
|
||||||
const result = await clack.autocompleteMultiselect({
|
const filterFn = options.filter ?? defaultAutocompleteFilter;
|
||||||
message: options.message,
|
|
||||||
|
const prompt = new core.AutocompletePrompt({
|
||||||
options: options.options,
|
options: options.options,
|
||||||
placeholder: options.placeholder || 'Type to search...',
|
multiple: true,
|
||||||
initialValues: options.initialValues,
|
filter: filterFn,
|
||||||
required: options.required || false,
|
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 || 'Start typing to find your IDE...';
|
||||||
|
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} ${color.dim('Search:')} ${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,
|
maxItems: options.maxItems || 5,
|
||||||
filter: options.filter,
|
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);
|
await handleCancel(result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -381,7 +381,61 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// STEP 1: Recommended Tools (multiselect) - optional, user can skip
|
// 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: 'Select tools to install:',
|
||||||
|
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 recommendedOptions = preferredIdes.map((ide) => {
|
||||||
const isConfigured = configuredPreferred.includes(ide.value);
|
const isConfigured = configuredPreferred.includes(ide.value);
|
||||||
|
|
@ -391,11 +445,20 @@ class UI {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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__',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Pre-select previously configured preferred tools
|
// Pre-select previously configured preferred tools
|
||||||
const recommendedInitialValues = configuredPreferred.length > 0 ? configuredPreferred : undefined;
|
const recommendedInitialValues = configuredPreferred.length > 0 ? configuredPreferred : undefined;
|
||||||
|
|
||||||
const recommendedSelected = await prompts.multiselect({
|
const recommendedSelected = await prompts.multiselect({
|
||||||
message: `Select recommended tools ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
|
message: `Select tools to install ${chalk.dim('(↑/↓ to navigate • SPACE: select • ENTER: confirm)')}:`,
|
||||||
options: recommendedOptions,
|
options: recommendedOptions,
|
||||||
initialValues: recommendedInitialValues,
|
initialValues: recommendedInitialValues,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -404,51 +467,63 @@ class UI {
|
||||||
const selectedRecommended = recommendedSelected || [];
|
const selectedRecommended = recommendedSelected || [];
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// STEP 2: Additional Tools - show if user has configured "other" tools,
|
// STEP 2: Handle "Browse All" selection - show additional tools if requested
|
||||||
// selected no recommended tools, or wants to add more
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
let showAdditionalPrompt = configuredOther.length > 0 || selectedRecommended.length === 0;
|
const wantsBrowseAll = selectedRecommended.includes('__BROWSE_ALL__');
|
||||||
|
const filteredRecommended = selectedRecommended.filter((v) => v !== '__BROWSE_ALL__');
|
||||||
|
|
||||||
if (!showAdditionalPrompt && otherIdes.length > 0) {
|
// Show additional tools if:
|
||||||
console.log('');
|
// 1. User explicitly chose "Browse All", OR
|
||||||
showAdditionalPrompt = await prompts.confirm({
|
// 2. User has previously configured "other" tools, OR
|
||||||
message: 'Add more tools from the extended list?',
|
// 3. User selected no recommended tools (allow them to pick from other tools)
|
||||||
default: false,
|
const showAdditionalTools = wantsBrowseAll || configuredOther.length > 0 || filteredRecommended.length === 0;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedAdditional = [];
|
let selectedAdditionalOrAll = [];
|
||||||
|
|
||||||
if (showAdditionalPrompt && otherIdes.length > 0) {
|
if (showAdditionalTools) {
|
||||||
// Build options for additional tools, excluding any already selected in recommended
|
// Show ALL tools if:
|
||||||
const additionalOptions = otherIdes
|
// - User explicitly chose "Browse All", OR
|
||||||
.filter((ide) => !selectedRecommended.includes(ide.value))
|
// - User selected nothing from recommended (so they can pick from everything)
|
||||||
.map((ide) => {
|
// Otherwise, show only "other" tools as additional options
|
||||||
const isConfigured = configuredOther.includes(ide.value);
|
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 {
|
return {
|
||||||
label: isConfigured ? `${ide.name} ✅` : ide.name,
|
label,
|
||||||
value: ide.value,
|
value: ide.value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pre-select previously configured other tools
|
// Pre-select: previously configured tools + any recommended tools already selected
|
||||||
const additionalInitialValues = configuredOther.length > 0 ? configuredOther : undefined;
|
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('');
|
console.log('');
|
||||||
const additionalSelected = await prompts.autocompleteMultiselect({
|
const selected = await prompts.autocompleteMultiselect({
|
||||||
message: `Select additional tools ${chalk.dim('(type to search, SPACE toggles, ENTER to confirm)')}:`,
|
message: isAdditional ? 'Select additional tools:' : 'Select tools:',
|
||||||
options: additionalOptions,
|
options: allToolOptions,
|
||||||
initialValues: additionalInitialValues,
|
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
||||||
required: false,
|
required: false,
|
||||||
maxItems: 6,
|
maxItems: 8,
|
||||||
placeholder: 'Type to search...',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
selectedAdditional = additionalSelected || [];
|
selectedAdditionalOrAll = selected || [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine selections
|
// Combine selections:
|
||||||
const allSelectedIdes = [...selectedRecommended, ...selectedAdditional];
|
// - 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
|
// STEP 3: Confirm if no tools selected
|
||||||
|
|
@ -471,6 +546,10 @@ class UI {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display selected tools
|
||||||
|
const allTools = [...preferredIdes, ...otherIdes];
|
||||||
|
this.displaySelectedTools(allSelectedIdes, preferredIdes, allTools);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ides: allSelectedIdes,
|
ides: allSelectedIdes,
|
||||||
skipIde: allSelectedIdes.length === 0,
|
skipIde: allSelectedIdes.length === 0,
|
||||||
|
|
@ -1674,6 +1753,27 @@ class UI {
|
||||||
|
|
||||||
console.log('');
|
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 };
|
module.exports = { UI };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue