fix(cli): replace inquirer with @clack/prompts for Windows compatibility
- Add new prompts.js wrapper around @clack/prompts to fix Windows arrow key navigation issues (libuv #852) - Fix validation logic in github-copilot.js that always returned true - Add support for primitive choice values (string/number) in select/multiselect - Add 'when' property support for conditional questions in prompt() - Update all IDE installers to use new prompts module Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
eeebf152af
commit
bd9728de2d
|
|
@ -34,6 +34,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/sitemap": "^3.6.0",
|
"@astrojs/sitemap": "^3.6.0",
|
||||||
"@astrojs/starlight": "^0.37.0",
|
"@astrojs/starlight": "^0.37.0",
|
||||||
|
"@clack/prompts": "^0.11.0",
|
||||||
"@eslint/js": "^9.33.0",
|
"@eslint/js": "^9.33.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"astro": "^5.16.0",
|
"astro": "^5.16.0",
|
||||||
|
|
@ -244,7 +245,6 @@
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
|
|
@ -756,6 +756,29 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@clack/core": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"sisteransi": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@clack/core": "0.5.0",
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"sisteransi": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@colors/colors": {
|
"node_modules/@colors/colors": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||||
|
|
@ -3643,7 +3666,6 @@
|
||||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
|
|
@ -3983,7 +4005,6 @@
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -4290,7 +4311,6 @@
|
||||||
"integrity": "sha512-6mF/YrvwwRxLTu+aMEa5pwzKUNl5ZetWbTyZCs9Um0F12HUmxUiF5UHiZPy4rifzU3gtpM3xP2DfdmkNX9eZRg==",
|
"integrity": "sha512-6mF/YrvwwRxLTu+aMEa5pwzKUNl5ZetWbTyZCs9Um0F12HUmxUiF5UHiZPy4rifzU3gtpM3xP2DfdmkNX9eZRg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^2.13.0",
|
"@astrojs/compiler": "^2.13.0",
|
||||||
"@astrojs/internal-helpers": "0.7.5",
|
"@astrojs/internal-helpers": "0.7.5",
|
||||||
|
|
@ -5358,7 +5378,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -6689,7 +6708,6 @@
|
||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
|
|
@ -10304,7 +10322,6 @@
|
||||||
"integrity": "sha512-p3JTemJJbkiMjXEMiFwgm0v6ym5g8K+b2oDny+6xdl300tUKySxvilJQLSea48C6OaYNmO30kH9KxpiAg5bWJw==",
|
"integrity": "sha512-p3JTemJJbkiMjXEMiFwgm0v6ym5g8K+b2oDny+6xdl300tUKySxvilJQLSea48C6OaYNmO30kH9KxpiAg5bWJw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"globby": "15.0.0",
|
"globby": "15.0.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
|
|
@ -12378,7 +12395,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
|
@ -12444,7 +12460,6 @@
|
||||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
|
|
@ -13273,7 +13288,6 @@
|
||||||
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
|
|
@ -14837,7 +14851,6 @@
|
||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
|
|
@ -15111,7 +15124,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"yaml": "bin.mjs"
|
"yaml": "bin.mjs"
|
||||||
},
|
},
|
||||||
|
|
@ -15303,7 +15315,6 @@
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/sitemap": "^3.6.0",
|
"@astrojs/sitemap": "^3.6.0",
|
||||||
"@astrojs/starlight": "^0.37.0",
|
"@astrojs/starlight": "^0.37.0",
|
||||||
|
"@clack/prompts": "^0.11.0",
|
||||||
"@eslint/js": "^9.33.0",
|
"@eslint/js": "^9.33.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"astro": "^5.16.0",
|
"astro": "^5.16.0",
|
||||||
|
|
|
||||||
|
|
@ -71,14 +71,10 @@ module.exports = {
|
||||||
console.log(chalk.dim(' • ElevenLabs AI (150+ premium voices)'));
|
console.log(chalk.dim(' • ElevenLabs AI (150+ premium voices)'));
|
||||||
console.log(chalk.dim(' • Piper TTS (50+ free voices)\n'));
|
console.log(chalk.dim(' • Piper TTS (50+ free voices)\n'));
|
||||||
|
|
||||||
const { default: inquirer } = await import('inquirer');
|
const prompts = require('../lib/prompts');
|
||||||
await inquirer.prompt([
|
await prompts.text({
|
||||||
{
|
message: chalk.green('Press Enter to start AgentVibes installer...'),
|
||||||
type: 'input',
|
});
|
||||||
name: 'continue',
|
|
||||||
message: chalk.green('Press Enter to start AgentVibes installer...'),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,7 @@ const yaml = require('yaml');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
|
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
|
||||||
const { CLIUtils } = require('../../../lib/cli-utils');
|
const { CLIUtils } = require('../../../lib/cli-utils');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
// Lazy-load inquirer (ESM module) to avoid ERR_REQUIRE_ESM
|
|
||||||
let _inquirer = null;
|
|
||||||
async function getInquirer() {
|
|
||||||
if (!_inquirer) {
|
|
||||||
_inquirer = (await import('inquirer')).default;
|
|
||||||
}
|
|
||||||
return _inquirer;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConfigCollector {
|
class ConfigCollector {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -183,7 +175,6 @@ class ConfigCollector {
|
||||||
* @returns {boolean} True if new fields were prompted, false if all fields existed
|
* @returns {boolean} True if new fields were prompted, false if all fields existed
|
||||||
*/
|
*/
|
||||||
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
this.currentProjectDir = projectDir;
|
this.currentProjectDir = projectDir;
|
||||||
|
|
||||||
// Load existing config if not already loaded
|
// Load existing config if not already loaded
|
||||||
|
|
@ -359,7 +350,7 @@ class ConfigCollector {
|
||||||
// Only show header if we actually have questions
|
// Only show header if we actually have questions
|
||||||
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
|
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
|
||||||
console.log(); // Line break before questions
|
console.log(); // Line break before questions
|
||||||
const promptedAnswers = await inquirer.prompt(questions);
|
const promptedAnswers = await prompts.prompt(questions);
|
||||||
|
|
||||||
// Merge prompted answers with static answers
|
// Merge prompted answers with static answers
|
||||||
Object.assign(allAnswers, promptedAnswers);
|
Object.assign(allAnswers, promptedAnswers);
|
||||||
|
|
@ -502,7 +493,6 @@ class ConfigCollector {
|
||||||
* @param {boolean} skipCompletion - Skip showing completion message (for early core collection)
|
* @param {boolean} skipCompletion - Skip showing completion message (for early core collection)
|
||||||
*/
|
*/
|
||||||
async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
|
async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
this.currentProjectDir = projectDir;
|
this.currentProjectDir = projectDir;
|
||||||
// Load existing config if needed and not already loaded
|
// Load existing config if needed and not already loaded
|
||||||
if (!skipLoadExisting && !this.existingConfig) {
|
if (!skipLoadExisting && !this.existingConfig) {
|
||||||
|
|
@ -597,7 +587,7 @@ class ConfigCollector {
|
||||||
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
||||||
let customize = true;
|
let customize = true;
|
||||||
if (moduleName !== 'core') {
|
if (moduleName !== 'core') {
|
||||||
const customizeAnswer = await inquirer.prompt([
|
const customizeAnswer = await prompts.prompt([
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'customize',
|
name: 'customize',
|
||||||
|
|
@ -614,7 +604,7 @@ class ConfigCollector {
|
||||||
|
|
||||||
if (questionsWithoutDefaults.length > 0) {
|
if (questionsWithoutDefaults.length > 0) {
|
||||||
console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
|
console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
|
||||||
const promptedAnswers = await inquirer.prompt(questionsWithoutDefaults);
|
const promptedAnswers = await prompts.prompt(questionsWithoutDefaults);
|
||||||
Object.assign(allAnswers, promptedAnswers);
|
Object.assign(allAnswers, promptedAnswers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -628,7 +618,7 @@ class ConfigCollector {
|
||||||
allAnswers[question.name] = question.default;
|
allAnswers[question.name] = question.default;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const promptedAnswers = await inquirer.prompt(questions);
|
const promptedAnswers = await prompts.prompt(questions);
|
||||||
Object.assign(allAnswers, promptedAnswers);
|
Object.assign(allAnswers, promptedAnswers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -750,7 +740,7 @@ class ConfigCollector {
|
||||||
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
||||||
|
|
||||||
// Ask user if they want to accept defaults or customize on the next line
|
// Ask user if they want to accept defaults or customize on the next line
|
||||||
const { customize } = await inquirer.prompt([
|
const { customize } = await prompts.prompt([
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'customize',
|
name: 'customize',
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const { CLIUtils } = require('../../../lib/cli-utils');
|
||||||
const { ManifestGenerator } = require('./manifest-generator');
|
const { ManifestGenerator } = require('./manifest-generator');
|
||||||
const { IdeConfigManager } = require('./ide-config-manager');
|
const { IdeConfigManager } = require('./ide-config-manager');
|
||||||
const { CustomHandler } = require('../custom/handler');
|
const { CustomHandler } = require('../custom/handler');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
// BMAD installation folder name - this is constant and should never change
|
// BMAD installation folder name - this is constant and should never change
|
||||||
const BMAD_FOLDER_NAME = '_bmad';
|
const BMAD_FOLDER_NAME = '_bmad';
|
||||||
|
|
@ -2139,15 +2140,11 @@ class Installer {
|
||||||
* Private: Prompt for update action
|
* Private: Prompt for update action
|
||||||
*/
|
*/
|
||||||
async promptUpdateAction() {
|
async promptUpdateAction() {
|
||||||
const { default: inquirer } = await import('inquirer');
|
const action = await prompts.select({
|
||||||
return await inquirer.prompt([
|
message: 'What would you like to do?',
|
||||||
{
|
choices: [{ name: 'Update existing installation', value: 'update' }],
|
||||||
type: 'list',
|
});
|
||||||
name: 'action',
|
return { action };
|
||||||
message: 'What would you like to do?',
|
|
||||||
choices: [{ name: 'Update existing installation', value: 'update' }],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -2156,8 +2153,6 @@ class Installer {
|
||||||
* @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version)
|
* @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version)
|
||||||
*/
|
*/
|
||||||
async handleLegacyV4Migration(_projectDir, _legacyV4) {
|
async handleLegacyV4Migration(_projectDir, _legacyV4) {
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(chalk.yellow.bold('⚠️ Legacy BMAD v4 detected'));
|
console.log(chalk.yellow.bold('⚠️ Legacy BMAD v4 detected'));
|
||||||
console.log(chalk.yellow('─'.repeat(80)));
|
console.log(chalk.yellow('─'.repeat(80)));
|
||||||
|
|
@ -2172,26 +2167,22 @@ class Installer {
|
||||||
console.log(chalk.dim('If your v4 installation set up rules or commands, you should remove those as well.'));
|
console.log(chalk.dim('If your v4 installation set up rules or commands, you should remove those as well.'));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
const { proceed } = await inquirer.prompt([
|
const proceed = await prompts.select({
|
||||||
{
|
message: 'What would you like to do?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'proceed',
|
{
|
||||||
message: 'What would you like to do?',
|
name: 'Exit and clean up manually (recommended)',
|
||||||
choices: [
|
value: 'exit',
|
||||||
{
|
hint: 'Exit installation',
|
||||||
name: 'Exit and clean up manually (recommended)',
|
},
|
||||||
value: 'exit',
|
{
|
||||||
short: 'Exit installation',
|
name: 'Continue with installation anyway',
|
||||||
},
|
value: 'continue',
|
||||||
{
|
hint: 'Continue',
|
||||||
name: 'Continue with installation anyway',
|
},
|
||||||
value: 'continue',
|
],
|
||||||
short: 'Continue',
|
default: 'exit',
|
||||||
},
|
});
|
||||||
],
|
|
||||||
default: 'exit',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (proceed === 'exit') {
|
if (proceed === 'exit') {
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
@ -2437,7 +2428,6 @@ class Installer {
|
||||||
|
|
||||||
console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
|
console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
|
||||||
|
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
let keptCount = 0;
|
let keptCount = 0;
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
let removedCount = 0;
|
let removedCount = 0;
|
||||||
|
|
@ -2451,12 +2441,12 @@ class Installer {
|
||||||
{
|
{
|
||||||
name: 'Keep installed (will not be processed)',
|
name: 'Keep installed (will not be processed)',
|
||||||
value: 'keep',
|
value: 'keep',
|
||||||
short: 'Keep',
|
hint: 'Keep',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Specify new source location',
|
name: 'Specify new source location',
|
||||||
value: 'update',
|
value: 'update',
|
||||||
short: 'Update',
|
hint: 'Update',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -2465,47 +2455,40 @@ class Installer {
|
||||||
choices.push({
|
choices.push({
|
||||||
name: '⚠️ REMOVE module completely (destructive!)',
|
name: '⚠️ REMOVE module completely (destructive!)',
|
||||||
value: 'remove',
|
value: 'remove',
|
||||||
short: 'Remove',
|
hint: 'Remove',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { action } = await inquirer.prompt([
|
const action = await prompts.select({
|
||||||
{
|
message: `How would you like to handle "${missing.name}"?`,
|
||||||
type: 'list',
|
choices,
|
||||||
name: 'action',
|
});
|
||||||
message: `How would you like to handle "${missing.name}"?`,
|
|
||||||
choices,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'update': {
|
case 'update': {
|
||||||
const { newSourcePath } = await inquirer.prompt([
|
// Use sync validation because @clack/prompts doesn't support async validate
|
||||||
{
|
const newSourcePath = await prompts.text({
|
||||||
type: 'input',
|
message: 'Enter the new path to the custom module:',
|
||||||
name: 'newSourcePath',
|
default: missing.sourcePath,
|
||||||
message: 'Enter the new path to the custom module:',
|
validate: (input) => {
|
||||||
default: missing.sourcePath,
|
if (!input || input.trim() === '') {
|
||||||
validate: async (input) => {
|
return 'Please enter a path';
|
||||||
if (!input || input.trim() === '') {
|
}
|
||||||
return 'Please enter a path';
|
const expandedPath = path.resolve(input.trim());
|
||||||
}
|
if (!fs.pathExistsSync(expandedPath)) {
|
||||||
const expandedPath = path.resolve(input.trim());
|
return 'Path does not exist';
|
||||||
if (!(await fs.pathExists(expandedPath))) {
|
}
|
||||||
return 'Path does not exist';
|
// Check if it looks like a valid module
|
||||||
}
|
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
||||||
// Check if it looks like a valid module
|
const agentsPath = path.join(expandedPath, 'agents');
|
||||||
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
const workflowsPath = path.join(expandedPath, 'workflows');
|
||||||
const agentsPath = path.join(expandedPath, 'agents');
|
|
||||||
const workflowsPath = path.join(expandedPath, 'workflows');
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(moduleYamlPath)) && !(await fs.pathExists(agentsPath)) && !(await fs.pathExists(workflowsPath))) {
|
if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) {
|
||||||
return 'Path does not appear to contain a valid custom module';
|
return 'Path does not appear to contain a valid custom module';
|
||||||
}
|
}
|
||||||
return true;
|
return; // clack expects undefined for valid input
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]);
|
});
|
||||||
|
|
||||||
// Update the source in manifest
|
// Update the source in manifest
|
||||||
const resolvedPath = path.resolve(newSourcePath.trim());
|
const resolvedPath = path.resolve(newSourcePath.trim());
|
||||||
|
|
@ -2531,29 +2514,21 @@ class Installer {
|
||||||
console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
|
console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
|
||||||
console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`));
|
console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`));
|
||||||
|
|
||||||
const { confirm } = await inquirer.prompt([
|
const confirmDelete = await prompts.confirm({
|
||||||
{
|
message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'confirm',
|
});
|
||||||
message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (confirm) {
|
if (confirmDelete) {
|
||||||
const { typedConfirm } = await inquirer.prompt([
|
const typedConfirm = await prompts.text({
|
||||||
{
|
message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
|
||||||
type: 'input',
|
validate: (input) => {
|
||||||
name: 'typedConfirm',
|
if (input !== 'DELETE') {
|
||||||
message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
|
return chalk.red('You must type "DELETE" exactly to proceed');
|
||||||
validate: (input) => {
|
}
|
||||||
if (input !== 'DELETE') {
|
return; // clack expects undefined for valid input
|
||||||
return chalk.red('You must type "DELETE" exactly to proceed');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]);
|
});
|
||||||
|
|
||||||
if (typedConfirm === 'DELETE') {
|
if (typedConfirm === 'DELETE') {
|
||||||
// Remove the module from filesystem and manifest
|
// Remove the module from filesystem and manifest
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const {
|
||||||
resolveSubagentFiles,
|
resolveSubagentFiles,
|
||||||
} = require('./shared/module-injections');
|
} = require('./shared/module-injections');
|
||||||
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Google Antigravity IDE setup handler
|
* Google Antigravity IDE setup handler
|
||||||
|
|
@ -58,20 +59,14 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
|
|
||||||
if (config.subagentChoices.install !== 'none') {
|
if (config.subagentChoices.install !== 'none') {
|
||||||
// Ask for installation location
|
// Ask for installation location
|
||||||
const { default: inquirer } = await import('inquirer');
|
config.installLocation = await prompts.select({
|
||||||
const locationAnswer = await inquirer.prompt([
|
message: 'Where would you like to install Antigravity subagents?',
|
||||||
{
|
choices: [
|
||||||
type: 'list',
|
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
||||||
name: 'location',
|
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
||||||
message: 'Where would you like to install Antigravity subagents?',
|
],
|
||||||
choices: [
|
default: 'project',
|
||||||
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
});
|
||||||
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
|
||||||
],
|
|
||||||
default: 'project',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
config.installLocation = locationAnswer.location;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -297,20 +292,14 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
choices = await this.promptSubagentInstallation(config.subagents);
|
choices = await this.promptSubagentInstallation(config.subagents);
|
||||||
|
|
||||||
if (choices.install !== 'none') {
|
if (choices.install !== 'none') {
|
||||||
const { default: inquirer } = await import('inquirer');
|
location = await prompts.select({
|
||||||
const locationAnswer = await inquirer.prompt([
|
message: 'Where would you like to install Antigravity subagents?',
|
||||||
{
|
choices: [
|
||||||
type: 'list',
|
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
||||||
name: 'location',
|
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
||||||
message: 'Where would you like to install Antigravity subagents?',
|
],
|
||||||
choices: [
|
default: 'project',
|
||||||
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
});
|
||||||
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
|
||||||
],
|
|
||||||
default: 'project',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
location = locationAnswer.location;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,22 +323,16 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
* Prompt user for subagent installation preferences
|
* Prompt user for subagent installation preferences
|
||||||
*/
|
*/
|
||||||
async promptSubagentInstallation(subagentConfig) {
|
async promptSubagentInstallation(subagentConfig) {
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
|
|
||||||
// First ask if they want to install subagents
|
// First ask if they want to install subagents
|
||||||
const { install } = await inquirer.prompt([
|
const install = await prompts.select({
|
||||||
{
|
message: 'Would you like to install Antigravity subagents for enhanced functionality?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'install',
|
{ name: 'Yes, install all subagents', value: 'all' },
|
||||||
message: 'Would you like to install Antigravity subagents for enhanced functionality?',
|
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
||||||
choices: [
|
{ name: 'No, skip subagent installation', value: 'none' },
|
||||||
{ name: 'Yes, install all subagents', value: 'all' },
|
],
|
||||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
default: 'all',
|
||||||
{ name: 'No, skip subagent installation', value: 'none' },
|
});
|
||||||
],
|
|
||||||
default: 'all',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (install === 'selective') {
|
if (install === 'selective') {
|
||||||
// Show list of available subagents with descriptions
|
// Show list of available subagents with descriptions
|
||||||
|
|
@ -361,18 +344,14 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
'document-reviewer.md': 'Document quality review',
|
'document-reviewer.md': 'Document quality review',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { selected } = await inquirer.prompt([
|
const selected = await prompts.multiselect({
|
||||||
{
|
message: 'Select subagents to install:',
|
||||||
type: 'checkbox',
|
choices: subagentConfig.files.map((file) => ({
|
||||||
name: 'selected',
|
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||||
message: 'Select subagents to install:',
|
value: file,
|
||||||
choices: subagentConfig.files.map((file) => ({
|
checked: true,
|
||||||
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
})),
|
||||||
value: file,
|
});
|
||||||
checked: true,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { install: 'selective', selected };
|
return { install: 'selective', selected };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const {
|
||||||
resolveSubagentFiles,
|
resolveSubagentFiles,
|
||||||
} = require('./shared/module-injections');
|
} = require('./shared/module-injections');
|
||||||
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claude Code IDE setup handler
|
* Claude Code IDE setup handler
|
||||||
|
|
@ -57,20 +58,14 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
|
|
||||||
if (config.subagentChoices.install !== 'none') {
|
if (config.subagentChoices.install !== 'none') {
|
||||||
// Ask for installation location
|
// Ask for installation location
|
||||||
const { default: inquirer } = await import('inquirer');
|
config.installLocation = await prompts.select({
|
||||||
const locationAnswer = await inquirer.prompt([
|
message: 'Where would you like to install Claude Code subagents?',
|
||||||
{
|
choices: [
|
||||||
type: 'list',
|
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
||||||
name: 'location',
|
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
||||||
message: 'Where would you like to install Claude Code subagents?',
|
],
|
||||||
choices: [
|
default: 'project',
|
||||||
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
});
|
||||||
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
|
||||||
],
|
|
||||||
default: 'project',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
config.installLocation = locationAnswer.location;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -305,20 +300,14 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
choices = await this.promptSubagentInstallation(config.subagents);
|
choices = await this.promptSubagentInstallation(config.subagents);
|
||||||
|
|
||||||
if (choices.install !== 'none') {
|
if (choices.install !== 'none') {
|
||||||
const { default: inquirer } = await import('inquirer');
|
location = await prompts.select({
|
||||||
const locationAnswer = await inquirer.prompt([
|
message: 'Where would you like to install Claude Code subagents?',
|
||||||
{
|
choices: [
|
||||||
type: 'list',
|
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
||||||
name: 'location',
|
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
||||||
message: 'Where would you like to install Claude Code subagents?',
|
],
|
||||||
choices: [
|
default: 'project',
|
||||||
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
});
|
||||||
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
|
||||||
],
|
|
||||||
default: 'project',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
location = locationAnswer.location;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -342,22 +331,16 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
* Prompt user for subagent installation preferences
|
* Prompt user for subagent installation preferences
|
||||||
*/
|
*/
|
||||||
async promptSubagentInstallation(subagentConfig) {
|
async promptSubagentInstallation(subagentConfig) {
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
|
|
||||||
// First ask if they want to install subagents
|
// First ask if they want to install subagents
|
||||||
const { install } = await inquirer.prompt([
|
const install = await prompts.select({
|
||||||
{
|
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'install',
|
{ name: 'Yes, install all subagents', value: 'all' },
|
||||||
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
|
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
||||||
choices: [
|
{ name: 'No, skip subagent installation', value: 'none' },
|
||||||
{ name: 'Yes, install all subagents', value: 'all' },
|
],
|
||||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
default: 'all',
|
||||||
{ name: 'No, skip subagent installation', value: 'none' },
|
});
|
||||||
],
|
|
||||||
default: 'all',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (install === 'selective') {
|
if (install === 'selective') {
|
||||||
// Show list of available subagents with descriptions
|
// Show list of available subagents with descriptions
|
||||||
|
|
@ -369,18 +352,14 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
'document-reviewer.md': 'Document quality review',
|
'document-reviewer.md': 'Document quality review',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { selected } = await inquirer.prompt([
|
const selected = await prompts.multiselect({
|
||||||
{
|
message: 'Select subagents to install:',
|
||||||
type: 'checkbox',
|
choices: subagentConfig.files.map((file) => ({
|
||||||
name: 'selected',
|
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||||
message: 'Select subagents to install:',
|
value: file,
|
||||||
choices: subagentConfig.files.map((file) => ({
|
checked: true,
|
||||||
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
})),
|
||||||
value: file,
|
});
|
||||||
checked: true,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { install: 'selective', selected };
|
return { install: 'selective', selected };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ const { BaseIdeSetup } = require('./_base-ide');
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||||
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
|
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Codex setup handler (CLI mode)
|
* Codex setup handler (CLI mode)
|
||||||
|
|
@ -21,32 +22,24 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
* @returns {Object} Collected configuration
|
* @returns {Object} Collected configuration
|
||||||
*/
|
*/
|
||||||
async collectConfiguration(options = {}) {
|
async collectConfiguration(options = {}) {
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
|
|
||||||
let confirmed = false;
|
let confirmed = false;
|
||||||
let installLocation = 'global';
|
let installLocation = 'global';
|
||||||
|
|
||||||
while (!confirmed) {
|
while (!confirmed) {
|
||||||
const { location } = await inquirer.prompt([
|
installLocation = await prompts.select({
|
||||||
{
|
message: 'Where would you like to install Codex CLI prompts?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'location',
|
{
|
||||||
message: 'Where would you like to install Codex CLI prompts?',
|
name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)',
|
||||||
choices: [
|
value: 'global',
|
||||||
{
|
},
|
||||||
name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)',
|
{
|
||||||
value: 'global',
|
name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>${path.sep}.codex)`,
|
||||||
},
|
value: 'project',
|
||||||
{
|
},
|
||||||
name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>${path.sep}.codex)`,
|
],
|
||||||
value: 'project',
|
default: 'global',
|
||||||
},
|
});
|
||||||
],
|
|
||||||
default: 'global',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
installLocation = location;
|
|
||||||
|
|
||||||
// Display detailed instructions for the chosen option
|
// Display detailed instructions for the chosen option
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
@ -57,16 +50,10 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm the choice
|
// Confirm the choice
|
||||||
const { proceed } = await inquirer.prompt([
|
confirmed = await prompts.confirm({
|
||||||
{
|
message: 'Proceed with this installation option?',
|
||||||
type: 'confirm',
|
default: true,
|
||||||
name: 'proceed',
|
});
|
||||||
message: 'Proceed with this installation option?',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
confirmed = proceed;
|
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
console.log(chalk.yellow("\n Let's choose a different installation option.\n"));
|
console.log(chalk.yellow("\n Let's choose a different installation option.\n"));
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ const path = require('node:path');
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
const { BaseIdeSetup } = require('./_base-ide');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GitHub Copilot setup handler
|
* GitHub Copilot setup handler
|
||||||
|
|
@ -21,29 +22,23 @@ class GitHubCopilotSetup extends BaseIdeSetup {
|
||||||
* @returns {Object} Collected configuration
|
* @returns {Object} Collected configuration
|
||||||
*/
|
*/
|
||||||
async collectConfiguration(options = {}) {
|
async collectConfiguration(options = {}) {
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
const config = {};
|
const config = {};
|
||||||
|
|
||||||
console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration'));
|
console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration'));
|
||||||
console.log(chalk.dim(' GitHub Copilot works best with specific settings\n'));
|
console.log(chalk.dim(' GitHub Copilot works best with specific settings\n'));
|
||||||
|
|
||||||
const response = await inquirer.prompt([
|
config.vsCodeConfig = await prompts.select({
|
||||||
{
|
message: 'How would you like to configure VS Code settings?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'configChoice',
|
{ name: 'Use recommended defaults (fastest)', value: 'defaults' },
|
||||||
message: 'How would you like to configure VS Code settings?',
|
{ name: 'Configure each setting manually', value: 'manual' },
|
||||||
choices: [
|
{ name: 'Skip settings configuration', value: 'skip' },
|
||||||
{ name: 'Use recommended defaults (fastest)', value: 'defaults' },
|
],
|
||||||
{ name: 'Configure each setting manually', value: 'manual' },
|
default: 'defaults',
|
||||||
{ name: 'Skip settings configuration', value: 'skip' },
|
});
|
||||||
],
|
|
||||||
default: 'defaults',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
config.vsCodeConfig = response.configChoice;
|
|
||||||
|
|
||||||
if (response.configChoice === 'manual') {
|
if (config.vsCodeConfig === 'manual') {
|
||||||
config.manualSettings = await inquirer.prompt([
|
config.manualSettings = await prompts.prompt([
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'maxRequests',
|
name: 'maxRequests',
|
||||||
|
|
@ -52,7 +47,8 @@ class GitHubCopilotSetup extends BaseIdeSetup {
|
||||||
validate: (input) => {
|
validate: (input) => {
|
||||||
const num = parseInt(input, 10);
|
const num = parseInt(input, 10);
|
||||||
if (isNaN(num)) return 'Enter a valid number 1-50';
|
if (isNaN(num)) return 'Enter a valid number 1-50';
|
||||||
return (num >= 1 && num <= 50) || 'Enter 1-50';
|
if (num < 1 || num > 50) return 'Enter a number between 1-50';
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,391 @@
|
||||||
|
/**
|
||||||
|
* @clack/prompts wrapper for BMAD CLI
|
||||||
|
*
|
||||||
|
* This module provides a unified interface for CLI prompts using @clack/prompts.
|
||||||
|
* It replaces Inquirer.js to fix Windows arrow key navigation issues (libuv #852).
|
||||||
|
*
|
||||||
|
* @module prompts
|
||||||
|
*/
|
||||||
|
|
||||||
|
let _clack = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy-load @clack/prompts (ESM module)
|
||||||
|
* @returns {Promise<Object>} The clack prompts module
|
||||||
|
*/
|
||||||
|
async function getClack() {
|
||||||
|
if (!_clack) {
|
||||||
|
_clack = await import('@clack/prompts');
|
||||||
|
}
|
||||||
|
return _clack;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle user cancellation gracefully
|
||||||
|
* @param {any} value - The value to check
|
||||||
|
* @param {string} [message='Operation cancelled'] - Message to display
|
||||||
|
* @returns {boolean} True if cancelled
|
||||||
|
*/
|
||||||
|
async function handleCancel(value, message = 'Operation cancelled') {
|
||||||
|
const clack = await getClack();
|
||||||
|
if (clack.isCancel(value)) {
|
||||||
|
clack.cancel(message);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display intro message
|
||||||
|
* @param {string} message - The intro message
|
||||||
|
*/
|
||||||
|
async function intro(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.intro(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display outro message
|
||||||
|
* @param {string} message - The outro message
|
||||||
|
*/
|
||||||
|
async function outro(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.outro(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a note/info box
|
||||||
|
* @param {string} message - The note content
|
||||||
|
* @param {string} [title] - Optional title
|
||||||
|
*/
|
||||||
|
async function note(message, title) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.note(message, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a spinner for async operations
|
||||||
|
* @returns {Object} Spinner controller with start, stop, message methods
|
||||||
|
*/
|
||||||
|
async function spinner() {
|
||||||
|
const clack = await getClack();
|
||||||
|
return clack.spinner();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single-select prompt (replaces Inquirer 'list' type)
|
||||||
|
* @param {Object} options - Prompt options
|
||||||
|
* @param {string} options.message - The question to ask
|
||||||
|
* @param {Array} options.choices - Array of choices [{name, value, hint?}]
|
||||||
|
* @param {any} [options.default] - Default selected value
|
||||||
|
* @returns {Promise<any>} Selected value
|
||||||
|
*/
|
||||||
|
async function select(options) {
|
||||||
|
const clack = await getClack();
|
||||||
|
|
||||||
|
// Convert Inquirer-style choices to clack format
|
||||||
|
// Handle both object choices {name, value, hint} and primitive choices (string/number)
|
||||||
|
const clackOptions = options.choices
|
||||||
|
.filter((c) => c.type !== 'separator') // Skip separators for now
|
||||||
|
.map((choice) => {
|
||||||
|
if (typeof choice === 'string' || typeof choice === 'number') {
|
||||||
|
return { value: choice, label: String(choice) };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: choice.value === undefined ? choice.name : choice.value,
|
||||||
|
label: choice.name || choice.label || String(choice.value),
|
||||||
|
hint: choice.hint || choice.description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find initial value
|
||||||
|
let initialValue;
|
||||||
|
if (options.default !== undefined) {
|
||||||
|
initialValue = options.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await clack.select({
|
||||||
|
message: options.message,
|
||||||
|
options: clackOptions,
|
||||||
|
initialValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleCancel(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-select prompt (replaces Inquirer 'checkbox' type)
|
||||||
|
* @param {Object} options - Prompt options
|
||||||
|
* @param {string} options.message - The question to ask
|
||||||
|
* @param {Array} options.choices - Array of choices [{name, value, checked?, hint?}]
|
||||||
|
* @param {boolean} [options.required=false] - Whether at least one must be selected
|
||||||
|
* @returns {Promise<Array>} Array of selected values
|
||||||
|
*/
|
||||||
|
async function multiselect(options) {
|
||||||
|
const clack = await getClack();
|
||||||
|
|
||||||
|
// Convert Inquirer-style choices to clack format
|
||||||
|
// Handle both object choices {name, value, hint} and primitive choices (string/number)
|
||||||
|
const clackOptions = options.choices
|
||||||
|
.filter((c) => c.type !== 'separator') // Skip separators
|
||||||
|
.map((choice) => {
|
||||||
|
if (typeof choice === 'string' || typeof choice === 'number') {
|
||||||
|
return { value: choice, label: String(choice) };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: choice.value === undefined ? choice.name : choice.value,
|
||||||
|
label: choice.name || choice.label || String(choice.value),
|
||||||
|
hint: choice.hint || choice.description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find initial values (pre-checked items)
|
||||||
|
const initialValues = options.choices
|
||||||
|
.filter((c) => c.checked && c.type !== 'separator')
|
||||||
|
.map((c) => (c.value === undefined ? c.name : c.value));
|
||||||
|
|
||||||
|
const result = await clack.multiselect({
|
||||||
|
message: options.message,
|
||||||
|
options: clackOptions,
|
||||||
|
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
||||||
|
required: options.required || false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleCancel(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm prompt (replaces Inquirer 'confirm' type)
|
||||||
|
* @param {Object} options - Prompt options
|
||||||
|
* @param {string} options.message - The question to ask
|
||||||
|
* @param {boolean} [options.default=true] - Default value
|
||||||
|
* @returns {Promise<boolean>} User's answer
|
||||||
|
*/
|
||||||
|
async function confirm(options) {
|
||||||
|
const clack = await getClack();
|
||||||
|
|
||||||
|
const result = await clack.confirm({
|
||||||
|
message: options.message,
|
||||||
|
initialValue: options.default === undefined ? true : options.default,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleCancel(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text input prompt (replaces Inquirer 'input' type)
|
||||||
|
* @param {Object} options - Prompt options
|
||||||
|
* @param {string} options.message - The question to ask
|
||||||
|
* @param {string} [options.default] - Default value
|
||||||
|
* @param {string} [options.placeholder] - Placeholder text (defaults to options.default if not provided)
|
||||||
|
* @param {Function} [options.validate] - Validation function
|
||||||
|
* @returns {Promise<string>} User's input
|
||||||
|
*/
|
||||||
|
async function text(options) {
|
||||||
|
const clack = await getClack();
|
||||||
|
|
||||||
|
// 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 result = await clack.text({
|
||||||
|
message: options.message,
|
||||||
|
defaultValue: options.default,
|
||||||
|
placeholder: typeof placeholder === 'string' ? placeholder : undefined,
|
||||||
|
validate: options.validate,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleCancel(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Password input prompt (replaces Inquirer 'password' type)
|
||||||
|
* @param {Object} options - Prompt options
|
||||||
|
* @param {string} options.message - The question to ask
|
||||||
|
* @param {Function} [options.validate] - Validation function
|
||||||
|
* @returns {Promise<string>} User's input
|
||||||
|
*/
|
||||||
|
async function password(options) {
|
||||||
|
const clack = await getClack();
|
||||||
|
|
||||||
|
const result = await clack.password({
|
||||||
|
message: options.message,
|
||||||
|
validate: options.validate,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleCancel(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group multiple prompts together
|
||||||
|
* @param {Object} prompts - Object of prompt functions
|
||||||
|
* @param {Object} [options] - Group options
|
||||||
|
* @returns {Promise<Object>} Object with all answers
|
||||||
|
*/
|
||||||
|
async function group(prompts, options = {}) {
|
||||||
|
const clack = await getClack();
|
||||||
|
|
||||||
|
const result = await clack.group(prompts, {
|
||||||
|
onCancel: () => {
|
||||||
|
clack.cancel('Operation cancelled');
|
||||||
|
process.exit(0);
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run tasks with spinner feedback
|
||||||
|
* @param {Array} tasks - Array of task objects [{title, task, enabled?}]
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function tasks(taskList) {
|
||||||
|
const clack = await getClack();
|
||||||
|
await clack.tasks(taskList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log messages with styling
|
||||||
|
*/
|
||||||
|
const log = {
|
||||||
|
async info(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.log.info(message);
|
||||||
|
},
|
||||||
|
async success(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.log.success(message);
|
||||||
|
},
|
||||||
|
async warn(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.log.warn(message);
|
||||||
|
},
|
||||||
|
async error(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.log.error(message);
|
||||||
|
},
|
||||||
|
async message(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.log.message(message);
|
||||||
|
},
|
||||||
|
async step(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.log.step(message);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute an array of Inquirer-style questions using @clack/prompts
|
||||||
|
* This provides compatibility with dynamic question arrays
|
||||||
|
* @param {Array} questions - Array of Inquirer-style question objects
|
||||||
|
* @returns {Promise<Object>} Object with answers keyed by question name
|
||||||
|
*/
|
||||||
|
async function prompt(questions) {
|
||||||
|
const answers = {};
|
||||||
|
|
||||||
|
for (const question of questions) {
|
||||||
|
const { type, name, message, choices, default: defaultValue, validate, when } = question;
|
||||||
|
|
||||||
|
// Handle conditional questions via 'when' property
|
||||||
|
if (when !== undefined) {
|
||||||
|
const shouldAsk = typeof when === 'function' ? await when(answers) : when;
|
||||||
|
if (!shouldAsk) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let answer;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'input': {
|
||||||
|
// Note: @clack/prompts doesn't support async validation, so validate must be sync
|
||||||
|
answer = await text({
|
||||||
|
message,
|
||||||
|
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
||||||
|
validate: validate
|
||||||
|
? (val) => {
|
||||||
|
const result = validate(val, answers);
|
||||||
|
return result === true ? undefined : result;
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'confirm': {
|
||||||
|
answer = await confirm({
|
||||||
|
message,
|
||||||
|
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'list': {
|
||||||
|
answer = await select({
|
||||||
|
message,
|
||||||
|
choices: choices || [],
|
||||||
|
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'checkbox': {
|
||||||
|
answer = await multiselect({
|
||||||
|
message,
|
||||||
|
choices: choices || [],
|
||||||
|
required: false,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'password': {
|
||||||
|
// Note: @clack/prompts doesn't support async validation, so validate must be sync
|
||||||
|
answer = await password({
|
||||||
|
message,
|
||||||
|
validate: validate
|
||||||
|
? (val) => {
|
||||||
|
const result = validate(val, answers);
|
||||||
|
return result === true ? undefined : result;
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
// Default to text input for unknown types
|
||||||
|
answer = await text({
|
||||||
|
message,
|
||||||
|
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
answers[name] = answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return answers;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getClack,
|
||||||
|
handleCancel,
|
||||||
|
intro,
|
||||||
|
outro,
|
||||||
|
note,
|
||||||
|
spinner,
|
||||||
|
select,
|
||||||
|
multiselect,
|
||||||
|
confirm,
|
||||||
|
text,
|
||||||
|
password,
|
||||||
|
group,
|
||||||
|
tasks,
|
||||||
|
log,
|
||||||
|
prompt,
|
||||||
|
};
|
||||||
|
|
@ -4,16 +4,21 @@ const os = require('node:os');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { CLIUtils } = require('./cli-utils');
|
const { CLIUtils } = require('./cli-utils');
|
||||||
const { CustomHandler } = require('../installers/lib/custom/handler');
|
const { CustomHandler } = require('../installers/lib/custom/handler');
|
||||||
|
const prompts = require('./prompts');
|
||||||
|
|
||||||
// Lazy-load inquirer (ESM module) to avoid ERR_REQUIRE_ESM
|
// Separator class for visual grouping in select/multiselect prompts
|
||||||
let _inquirer = null;
|
// Note: @clack/prompts doesn't support separators natively, they are filtered out
|
||||||
async function getInquirer() {
|
class Separator {
|
||||||
if (!_inquirer) {
|
constructor(text = '────────') {
|
||||||
_inquirer = (await import('inquirer')).default;
|
this.line = text;
|
||||||
|
this.name = text;
|
||||||
}
|
}
|
||||||
return _inquirer;
|
type = 'separator';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provide a compatible interface
|
||||||
|
const inquirer = { Separator };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI utilities for the installer
|
* UI utilities for the installer
|
||||||
*/
|
*/
|
||||||
|
|
@ -23,7 +28,6 @@ class UI {
|
||||||
* @returns {Object} Installation configuration
|
* @returns {Object} Installation configuration
|
||||||
*/
|
*/
|
||||||
async promptInstall() {
|
async promptInstall() {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
CLIUtils.displayLogo();
|
CLIUtils.displayLogo();
|
||||||
|
|
||||||
// Display version-specific start message from install-messages.yaml
|
// Display version-specific start message from install-messages.yaml
|
||||||
|
|
@ -113,26 +117,20 @@ class UI {
|
||||||
console.log(chalk.yellow('─'.repeat(80)));
|
console.log(chalk.yellow('─'.repeat(80)));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
const { proceed } = await inquirer.prompt([
|
const proceed = await prompts.select({
|
||||||
{
|
message: 'What would you like to do?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'proceed',
|
{
|
||||||
message: 'What would you like to do?',
|
name: 'Cancel and do a fresh install (recommended)',
|
||||||
choices: [
|
value: 'cancel',
|
||||||
{
|
},
|
||||||
name: 'Cancel and do a fresh install (recommended)',
|
{
|
||||||
value: 'cancel',
|
name: 'Proceed anyway (will attempt update, potentially may fail or have unstable behavior)',
|
||||||
short: 'Cancel installation',
|
value: 'proceed',
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
name: 'Proceed anyway (will attempt update, potentially may fail or have unstable behavior)',
|
default: 'cancel',
|
||||||
value: 'proceed',
|
});
|
||||||
short: 'Proceed with update',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'cancel',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (proceed === 'cancel') {
|
if (proceed === 'cancel') {
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
@ -188,14 +186,10 @@ class UI {
|
||||||
|
|
||||||
// If Claude Code was selected, ask about TTS
|
// If Claude Code was selected, ask about TTS
|
||||||
if (claudeCodeSelected) {
|
if (claudeCodeSelected) {
|
||||||
const { enableTts } = await inquirer.prompt([
|
const enableTts = await prompts.confirm({
|
||||||
{
|
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'enableTts',
|
});
|
||||||
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (enableTts) {
|
if (enableTts) {
|
||||||
agentVibesConfig = { enabled: true, alreadyInstalled: false };
|
agentVibesConfig = { enabled: true, alreadyInstalled: false };
|
||||||
|
|
@ -250,18 +244,11 @@ class UI {
|
||||||
// Common actions
|
// Common actions
|
||||||
choices.push({ name: 'Modify BMAD Installation', value: 'update' });
|
choices.push({ name: 'Modify BMAD Installation', value: 'update' });
|
||||||
|
|
||||||
const promptResult = await inquirer.prompt([
|
actionType = await prompts.select({
|
||||||
{
|
message: 'What would you like to do?',
|
||||||
type: 'list',
|
choices: choices,
|
||||||
name: 'actionType',
|
default: choices[0].value,
|
||||||
message: 'What would you like to do?',
|
});
|
||||||
choices: choices,
|
|
||||||
default: choices[0].value, // Use the first option as default
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Extract actionType from prompt result
|
|
||||||
actionType = promptResult.actionType;
|
|
||||||
|
|
||||||
// Handle quick update separately
|
// Handle quick update separately
|
||||||
if (actionType === 'quick-update') {
|
if (actionType === 'quick-update') {
|
||||||
|
|
@ -290,14 +277,10 @@ class UI {
|
||||||
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
||||||
|
|
||||||
console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`));
|
console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`));
|
||||||
const { changeModuleSelection } = await inquirer.prompt([
|
const changeModuleSelection = await prompts.confirm({
|
||||||
{
|
message: 'Modify official module selection (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'changeModuleSelection',
|
});
|
||||||
message: 'Modify official module selection (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
let selectedModules = [];
|
let selectedModules = [];
|
||||||
if (changeModuleSelection) {
|
if (changeModuleSelection) {
|
||||||
|
|
@ -310,14 +293,10 @@ class UI {
|
||||||
|
|
||||||
// After module selection, ask about custom modules
|
// After module selection, ask about custom modules
|
||||||
console.log('');
|
console.log('');
|
||||||
const { changeCustomModules } = await inquirer.prompt([
|
const changeCustomModules = await prompts.confirm({
|
||||||
{
|
message: 'Modify custom module selection (add, update, or remove custom modules/agents/workflows)?',
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'changeCustomModules',
|
});
|
||||||
message: 'Modify custom module selection (add, update, or remove custom modules/agents/workflows)?',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
|
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
|
||||||
if (changeCustomModules) {
|
if (changeCustomModules) {
|
||||||
|
|
@ -352,15 +331,10 @@ class UI {
|
||||||
let enableTts = false;
|
let enableTts = false;
|
||||||
|
|
||||||
if (hasClaudeCode) {
|
if (hasClaudeCode) {
|
||||||
const { enableTts: enable } = await inquirer.prompt([
|
enableTts = await prompts.confirm({
|
||||||
{
|
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'enableTts',
|
});
|
||||||
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
enableTts = enable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core config with existing defaults (ask after TTS)
|
// Core config with existing defaults (ask after TTS)
|
||||||
|
|
@ -385,14 +359,10 @@ class UI {
|
||||||
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
||||||
|
|
||||||
// Ask about official modules for new installations
|
// Ask about official modules for new installations
|
||||||
const { wantsOfficialModules } = await inquirer.prompt([
|
const wantsOfficialModules = await prompts.confirm({
|
||||||
{
|
message: 'Will you be installing any official BMad modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
||||||
type: 'confirm',
|
default: true,
|
||||||
name: 'wantsOfficialModules',
|
});
|
||||||
message: 'Will you be installing any official BMad modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
let selectedOfficialModules = [];
|
let selectedOfficialModules = [];
|
||||||
if (wantsOfficialModules) {
|
if (wantsOfficialModules) {
|
||||||
|
|
@ -401,14 +371,10 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask about custom content
|
// Ask about custom content
|
||||||
const { wantsCustomContent } = await inquirer.prompt([
|
const wantsCustomContent = await prompts.confirm({
|
||||||
{
|
message: 'Would you like to install a local custom module (this includes custom agents and workflows also)?',
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'wantsCustomContent',
|
});
|
||||||
message: 'Would you like to install a local custom module (this includes custom agents and workflows also)?',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (wantsCustomContent) {
|
if (wantsCustomContent) {
|
||||||
customContentConfig = await this.promptCustomContentSource();
|
customContentConfig = await this.promptCustomContentSource();
|
||||||
|
|
@ -459,7 +425,6 @@ class UI {
|
||||||
* @returns {Object} Tool configuration
|
* @returns {Object} Tool configuration
|
||||||
*/
|
*/
|
||||||
async promptToolSelection(projectDir, selectedModules) {
|
async promptToolSelection(projectDir, selectedModules) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
// Check for existing configured IDEs - use findBmadDir to detect custom folder names
|
// Check for existing configured IDEs - use findBmadDir to detect custom folder names
|
||||||
const { Detector } = require('../installers/lib/core/detector');
|
const { Detector } = require('../installers/lib/core/detector');
|
||||||
const { Installer } = require('../installers/lib/core/installer');
|
const { Installer } = require('../installers/lib/core/installer');
|
||||||
|
|
@ -536,41 +501,31 @@ class UI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let answers;
|
let selectedIdes = [];
|
||||||
let userConfirmedNoTools = false;
|
let userConfirmedNoTools = false;
|
||||||
|
|
||||||
// Loop until user selects at least one tool OR explicitly confirms no tools
|
// Loop until user selects at least one tool OR explicitly confirms no tools
|
||||||
while (!userConfirmedNoTools) {
|
while (!userConfirmedNoTools) {
|
||||||
answers = await inquirer.prompt([
|
selectedIdes = await prompts.multiselect({
|
||||||
{
|
message: 'Select tools to configure:',
|
||||||
type: 'checkbox',
|
choices: ideChoices,
|
||||||
name: 'ides',
|
required: false,
|
||||||
message: 'Select tools to configure:',
|
});
|
||||||
choices: ideChoices,
|
|
||||||
pageSize: 30,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// If tools were selected, we're done
|
// If tools were selected, we're done
|
||||||
if (answers.ides && answers.ides.length > 0) {
|
if (selectedIdes && selectedIdes.length > 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn that no tools were selected - users often miss the spacebar requirement
|
// Warn that no tools were selected
|
||||||
console.log();
|
console.log();
|
||||||
console.log(chalk.red.bold('⚠️ WARNING: No tools were selected!'));
|
console.log(chalk.red.bold('⚠️ WARNING: No tools were selected!'));
|
||||||
console.log(chalk.red(' You must press SPACEBAR to select items, then ENTER to confirm.'));
|
|
||||||
console.log(chalk.red(' Simply highlighting an item does NOT select it.'));
|
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
const { goBack } = await inquirer.prompt([
|
const goBack = await prompts.confirm({
|
||||||
{
|
message: chalk.yellow('Would you like to go back and select at least one tool?'),
|
||||||
type: 'confirm',
|
default: true,
|
||||||
name: 'goBack',
|
});
|
||||||
message: chalk.yellow('Would you like to go back and select at least one tool?'),
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (goBack) {
|
if (goBack) {
|
||||||
// Re-display a message before looping back
|
// Re-display a message before looping back
|
||||||
|
|
@ -582,8 +537,8 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ides: answers.ides || [],
|
ides: selectedIdes || [],
|
||||||
skipIde: !answers.ides || answers.ides.length === 0,
|
skipIde: !selectedIdes || selectedIdes.length === 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -592,23 +547,17 @@ class UI {
|
||||||
* @returns {Object} Update configuration
|
* @returns {Object} Update configuration
|
||||||
*/
|
*/
|
||||||
async promptUpdate() {
|
async promptUpdate() {
|
||||||
const inquirer = await getInquirer();
|
const backupFirst = await prompts.confirm({
|
||||||
const answers = await inquirer.prompt([
|
message: 'Create backup before updating?',
|
||||||
{
|
default: true,
|
||||||
type: 'confirm',
|
});
|
||||||
name: 'backupFirst',
|
|
||||||
message: 'Create backup before updating?',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'preserveCustomizations',
|
|
||||||
message: 'Preserve local customizations?',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return answers;
|
const preserveCustomizations = await prompts.confirm({
|
||||||
|
message: 'Preserve local customizations?',
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { backupFirst, preserveCustomizations };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -617,27 +566,17 @@ class UI {
|
||||||
* @returns {Array} Selected modules
|
* @returns {Array} Selected modules
|
||||||
*/
|
*/
|
||||||
async promptModules(modules) {
|
async promptModules(modules) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
const choices = modules.map((mod) => ({
|
const choices = modules.map((mod) => ({
|
||||||
name: `${mod.name} - ${mod.description}`,
|
name: `${mod.name} - ${mod.description}`,
|
||||||
value: mod.id,
|
value: mod.id,
|
||||||
checked: false,
|
checked: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { selectedModules } = await inquirer.prompt([
|
const selectedModules = await prompts.multiselect({
|
||||||
{
|
message: 'Select modules to add:',
|
||||||
type: 'checkbox',
|
choices,
|
||||||
name: 'selectedModules',
|
required: true,
|
||||||
message: 'Select modules to add:',
|
});
|
||||||
choices,
|
|
||||||
validate: (answer) => {
|
|
||||||
if (answer.length === 0) {
|
|
||||||
return 'You must choose at least one module.';
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return selectedModules;
|
return selectedModules;
|
||||||
}
|
}
|
||||||
|
|
@ -649,17 +588,10 @@ class UI {
|
||||||
* @returns {boolean} User confirmation
|
* @returns {boolean} User confirmation
|
||||||
*/
|
*/
|
||||||
async confirm(message, defaultValue = false) {
|
async confirm(message, defaultValue = false) {
|
||||||
const inquirer = await getInquirer();
|
return await prompts.confirm({
|
||||||
const { confirmed } = await inquirer.prompt([
|
message,
|
||||||
{
|
default: defaultValue,
|
||||||
type: 'confirm',
|
});
|
||||||
name: 'confirmed',
|
|
||||||
message,
|
|
||||||
default: defaultValue,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return confirmed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -756,7 +688,6 @@ class UI {
|
||||||
* @returns {Array} Module choices for inquirer
|
* @returns {Array} Module choices for inquirer
|
||||||
*/
|
*/
|
||||||
async getModuleChoices(installedModuleIds, customContentConfig = null) {
|
async getModuleChoices(installedModuleIds, customContentConfig = null) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
const moduleChoices = [];
|
const moduleChoices = [];
|
||||||
const isNewInstallation = installedModuleIds.size === 0;
|
const isNewInstallation = installedModuleIds.size === 0;
|
||||||
|
|
||||||
|
|
@ -837,20 +768,19 @@ class UI {
|
||||||
* @returns {Array} Selected module IDs
|
* @returns {Array} Selected module IDs
|
||||||
*/
|
*/
|
||||||
async selectModules(moduleChoices, defaultSelections = []) {
|
async selectModules(moduleChoices, defaultSelections = []) {
|
||||||
const inquirer = await getInquirer();
|
// Mark choices as checked based on defaultSelections
|
||||||
const moduleAnswer = await inquirer.prompt([
|
const choicesWithDefaults = moduleChoices.map((choice) => ({
|
||||||
{
|
...choice,
|
||||||
type: 'checkbox',
|
checked: defaultSelections.includes(choice.value),
|
||||||
name: 'modules',
|
}));
|
||||||
message: 'Select modules to install:',
|
|
||||||
choices: moduleChoices,
|
|
||||||
default: defaultSelections,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const selected = moduleAnswer.modules || [];
|
const selected = await prompts.multiselect({
|
||||||
|
message: 'Select modules to install:',
|
||||||
|
choices: choicesWithDefaults,
|
||||||
|
required: false,
|
||||||
|
});
|
||||||
|
|
||||||
return selected;
|
return selected || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -858,23 +788,23 @@ class UI {
|
||||||
* @returns {Object} Directory answer from inquirer
|
* @returns {Object} Directory answer from inquirer
|
||||||
*/
|
*/
|
||||||
async promptForDirectory() {
|
async promptForDirectory() {
|
||||||
const inquirer = await getInquirer();
|
// Use sync validation because @clack/prompts doesn't support async validate
|
||||||
return await inquirer.prompt([
|
const directory = await prompts.text({
|
||||||
{
|
message: 'Installation directory:',
|
||||||
type: 'input',
|
default: process.cwd(),
|
||||||
name: 'directory',
|
placeholder: process.cwd(),
|
||||||
message: `Installation directory:`,
|
validate: (input) => this.validateDirectorySync(input),
|
||||||
default: process.cwd(),
|
});
|
||||||
validate: async (input) => this.validateDirectory(input),
|
|
||||||
filter: (input) => {
|
// Apply filter logic
|
||||||
// If empty, use the default
|
let filteredDir = directory;
|
||||||
if (!input || input.trim() === '') {
|
if (!filteredDir || filteredDir.trim() === '') {
|
||||||
return process.cwd();
|
filteredDir = process.cwd();
|
||||||
}
|
} else {
|
||||||
return this.expandUserPath(input);
|
filteredDir = this.expandUserPath(filteredDir);
|
||||||
},
|
}
|
||||||
},
|
|
||||||
]);
|
return { directory: filteredDir };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -915,45 +845,92 @@ class UI {
|
||||||
* @returns {boolean} Whether user confirmed
|
* @returns {boolean} Whether user confirmed
|
||||||
*/
|
*/
|
||||||
async confirmDirectory(directory) {
|
async confirmDirectory(directory) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
const dirExists = await fs.pathExists(directory);
|
const dirExists = await fs.pathExists(directory);
|
||||||
|
|
||||||
if (dirExists) {
|
if (dirExists) {
|
||||||
const confirmAnswer = await inquirer.prompt([
|
const proceed = await prompts.confirm({
|
||||||
{
|
message: 'Install to this directory?',
|
||||||
type: 'confirm',
|
default: true,
|
||||||
name: 'proceed',
|
});
|
||||||
message: `Install to this directory?`,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!confirmAnswer.proceed) {
|
if (!proceed) {
|
||||||
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
|
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return confirmAnswer.proceed;
|
return proceed;
|
||||||
} else {
|
} else {
|
||||||
// Ask for confirmation to create the directory
|
// Ask for confirmation to create the directory
|
||||||
const createConfirm = await inquirer.prompt([
|
const create = await prompts.confirm({
|
||||||
{
|
message: `The directory '${directory}' doesn't exist. Would you like to create it?`,
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'create',
|
});
|
||||||
message: `The directory '${directory}' doesn't exist. Would you like to create it?`,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!createConfirm.create) {
|
if (!create) {
|
||||||
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
|
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return createConfirm.create;
|
return create;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate directory path for installation
|
* Validate directory path for installation (sync version for clack prompts)
|
||||||
|
* @param {string} input - User input path
|
||||||
|
* @returns {string|undefined} Error message or undefined if valid
|
||||||
|
*/
|
||||||
|
validateDirectorySync(input) {
|
||||||
|
// Allow empty input to use the default
|
||||||
|
if (!input || input.trim() === '') {
|
||||||
|
return; // Empty means use default, undefined = valid for clack
|
||||||
|
}
|
||||||
|
|
||||||
|
let expandedPath;
|
||||||
|
try {
|
||||||
|
expandedPath = this.expandUserPath(input.trim());
|
||||||
|
} catch (error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the path exists
|
||||||
|
const pathExists = fs.pathExistsSync(expandedPath);
|
||||||
|
|
||||||
|
if (!pathExists) {
|
||||||
|
// Find the first existing parent directory
|
||||||
|
const existingParent = this.findExistingParentSync(expandedPath);
|
||||||
|
|
||||||
|
if (!existingParent) {
|
||||||
|
return 'Cannot create directory: no existing parent directory found';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the existing parent is writable
|
||||||
|
try {
|
||||||
|
fs.accessSync(existingParent, fs.constants.W_OK);
|
||||||
|
// Path doesn't exist but can be created - will prompt for confirmation later
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// Provide a detailed error message explaining both issues
|
||||||
|
return `Directory '${expandedPath}' does not exist and cannot be created: parent directory '${existingParent}' is not writable`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it exists, validate it's a directory and writable
|
||||||
|
const stat = fs.statSync(expandedPath);
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
return `Path exists but is not a directory: ${expandedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check write permissions
|
||||||
|
try {
|
||||||
|
fs.accessSync(expandedPath, fs.constants.W_OK);
|
||||||
|
} catch {
|
||||||
|
return `Directory is not writable: ${expandedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate directory path for installation (async version)
|
||||||
* @param {string} input - User input path
|
* @param {string} input - User input path
|
||||||
* @returns {string|true} Error message or true if valid
|
* @returns {string|true} Error message or true if valid
|
||||||
*/
|
*/
|
||||||
|
|
@ -1009,7 +986,28 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the first existing parent directory
|
* Find the first existing parent directory (sync version)
|
||||||
|
* @param {string} targetPath - The path to check
|
||||||
|
* @returns {string|null} The first existing parent directory, or null if none found
|
||||||
|
*/
|
||||||
|
findExistingParentSync(targetPath) {
|
||||||
|
let currentPath = path.resolve(targetPath);
|
||||||
|
|
||||||
|
// Walk up the directory tree until we find an existing directory
|
||||||
|
while (currentPath !== path.dirname(currentPath)) {
|
||||||
|
// Stop at root
|
||||||
|
const parent = path.dirname(currentPath);
|
||||||
|
if (fs.pathExistsSync(parent)) {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
currentPath = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // No existing parent found (shouldn't happen in practice)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the first existing parent directory (async version)
|
||||||
* @param {string} targetPath - The path to check
|
* @param {string} targetPath - The path to check
|
||||||
* @returns {string|null} The first existing parent directory, or null if none found
|
* @returns {string|null} The first existing parent directory, or null if none found
|
||||||
*/
|
*/
|
||||||
|
|
@ -1102,7 +1100,6 @@ class UI {
|
||||||
* - GitHub Issue: paulpreibisch/AgentVibes#36
|
* - GitHub Issue: paulpreibisch/AgentVibes#36
|
||||||
*/
|
*/
|
||||||
async promptAgentVibes(projectDir) {
|
async promptAgentVibes(projectDir) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
CLIUtils.displaySection('🎤 Voice Features', 'Enable TTS for multi-agent conversations');
|
CLIUtils.displaySection('🎤 Voice Features', 'Enable TTS for multi-agent conversations');
|
||||||
|
|
||||||
// Check if AgentVibes is already installed
|
// Check if AgentVibes is already installed
|
||||||
|
|
@ -1114,23 +1111,19 @@ class UI {
|
||||||
console.log(chalk.dim(' AgentVibes not detected'));
|
console.log(chalk.dim(' AgentVibes not detected'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const answers = await inquirer.prompt([
|
const enableTts = await prompts.confirm({
|
||||||
{
|
message: 'Enable Agents to Speak Out loud (powered by Agent Vibes? Claude Code only currently)',
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'enableTts',
|
});
|
||||||
message: 'Enable Agents to Speak Out loud (powered by Agent Vibes? Claude Code only currently)',
|
|
||||||
default: false, // Default to yes - recommended for best experience
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (answers.enableTts && !agentVibesInstalled) {
|
if (enableTts && !agentVibesInstalled) {
|
||||||
console.log(chalk.yellow('\n ⚠️ AgentVibes not installed'));
|
console.log(chalk.yellow('\n ⚠️ AgentVibes not installed'));
|
||||||
console.log(chalk.dim(' Install AgentVibes separately to enable TTS:'));
|
console.log(chalk.dim(' Install AgentVibes separately to enable TTS:'));
|
||||||
console.log(chalk.dim(' https://github.com/paulpreibisch/AgentVibes\n'));
|
console.log(chalk.dim(' https://github.com/paulpreibisch/AgentVibes\n'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: answers.enableTts,
|
enabled: enableTts,
|
||||||
alreadyInstalled: agentVibesInstalled,
|
alreadyInstalled: agentVibesInstalled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1253,25 +1246,20 @@ class UI {
|
||||||
* @returns {Object} Custom content configuration
|
* @returns {Object} Custom content configuration
|
||||||
*/
|
*/
|
||||||
async promptCustomContentSource() {
|
async promptCustomContentSource() {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
const customContentConfig = { hasCustomContent: true, sources: [] };
|
const customContentConfig = { hasCustomContent: true, sources: [] };
|
||||||
|
|
||||||
// Keep asking for more sources until user is done
|
// Keep asking for more sources until user is done
|
||||||
while (true) {
|
while (true) {
|
||||||
// First ask if user wants to add another module or continue
|
// First ask if user wants to add another module or continue
|
||||||
if (customContentConfig.sources.length > 0) {
|
if (customContentConfig.sources.length > 0) {
|
||||||
const { action } = await inquirer.prompt([
|
const action = await prompts.select({
|
||||||
{
|
message: 'Would you like to:',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'action',
|
{ name: 'Add another custom module', value: 'add' },
|
||||||
message: 'Would you like to:',
|
{ name: 'Continue with installation', value: 'continue' },
|
||||||
choices: [
|
],
|
||||||
{ name: 'Add another custom module', value: 'add' },
|
default: 'continue',
|
||||||
{ name: 'Continue with installation', value: 'continue' },
|
});
|
||||||
],
|
|
||||||
default: 'continue',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (action === 'continue') {
|
if (action === 'continue') {
|
||||||
break;
|
break;
|
||||||
|
|
@ -1282,57 +1270,54 @@ class UI {
|
||||||
let isValid = false;
|
let isValid = false;
|
||||||
|
|
||||||
while (!isValid) {
|
while (!isValid) {
|
||||||
const { path: inputPath } = await inquirer.prompt([
|
// Use sync validation because @clack/prompts doesn't support async validate
|
||||||
{
|
const inputPath = await prompts.text({
|
||||||
type: 'input',
|
message: 'Enter the path to your custom content folder (or press Enter to cancel):',
|
||||||
name: 'path',
|
validate: (input) => {
|
||||||
message: 'Enter the path to your custom content folder (or press Enter to cancel):',
|
// Allow empty input to cancel
|
||||||
validate: async (input) => {
|
if (!input || input.trim() === '') {
|
||||||
// Allow empty input to cancel
|
return; // Allow empty to exit
|
||||||
if (!input || input.trim() === '') {
|
}
|
||||||
return true; // Allow empty to exit
|
|
||||||
|
try {
|
||||||
|
// Expand the path
|
||||||
|
const expandedPath = this.expandUserPath(input.trim());
|
||||||
|
|
||||||
|
// Check if path exists
|
||||||
|
if (!fs.pathExistsSync(expandedPath)) {
|
||||||
|
return 'Path does not exist';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if it's a directory
|
||||||
|
const stat = fs.statSync(expandedPath);
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
return 'Path must be a directory';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for module.yaml in the root
|
||||||
|
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
||||||
|
if (!fs.pathExistsSync(moduleYamlPath)) {
|
||||||
|
return 'Directory must contain a module.yaml file in the root';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse the module.yaml to get the module ID
|
||||||
try {
|
try {
|
||||||
// Expand the path
|
const yaml = require('yaml');
|
||||||
const expandedPath = this.expandUserPath(input.trim());
|
const content = fs.readFileSync(moduleYamlPath, 'utf8');
|
||||||
|
const moduleData = yaml.parse(content);
|
||||||
// Check if path exists
|
if (!moduleData.code) {
|
||||||
if (!(await fs.pathExists(expandedPath))) {
|
return 'module.yaml must contain a "code" field for the module ID';
|
||||||
return 'Path does not exist';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a directory
|
|
||||||
const stat = await fs.stat(expandedPath);
|
|
||||||
if (!stat.isDirectory()) {
|
|
||||||
return 'Path must be a directory';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for module.yaml in the root
|
|
||||||
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
||||||
if (!(await fs.pathExists(moduleYamlPath))) {
|
|
||||||
return 'Directory must contain a module.yaml file in the root';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse the module.yaml to get the module ID
|
|
||||||
try {
|
|
||||||
const yaml = require('yaml');
|
|
||||||
const content = await fs.readFile(moduleYamlPath, 'utf8');
|
|
||||||
const moduleData = yaml.parse(content);
|
|
||||||
if (!moduleData.code) {
|
|
||||||
return 'module.yaml must contain a "code" field for the module ID';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return 'Invalid module.yaml file: ' + error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return 'Error validating path: ' + error.message;
|
return 'Invalid module.yaml file: ' + error.message;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
return; // Valid
|
||||||
|
} catch (error) {
|
||||||
|
return 'Error validating path: ' + error.message;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
]);
|
});
|
||||||
|
|
||||||
// If user pressed Enter without typing anything, exit the loop
|
// If user pressed Enter without typing anything, exit the loop
|
||||||
if (!inputPath || inputPath.trim() === '') {
|
if (!inputPath || inputPath.trim() === '') {
|
||||||
|
|
@ -1364,14 +1349,10 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask if user wants to add these to the installation
|
// Ask if user wants to add these to the installation
|
||||||
const { shouldInstall } = await inquirer.prompt([
|
const shouldInstall = await prompts.confirm({
|
||||||
{
|
message: `Install ${customContentConfig.sources.length} custom module(s) now?`,
|
||||||
type: 'confirm',
|
default: true,
|
||||||
name: 'shouldInstall',
|
});
|
||||||
message: `Install ${customContentConfig.sources.length} custom module(s) now?`,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (shouldInstall) {
|
if (shouldInstall) {
|
||||||
customContentConfig.selected = true;
|
customContentConfig.selected = true;
|
||||||
|
|
@ -1391,7 +1372,6 @@ class UI {
|
||||||
* @returns {Object} Result with selected custom modules and custom content config
|
* @returns {Object} Result with selected custom modules and custom content config
|
||||||
*/
|
*/
|
||||||
async handleCustomModulesInModifyFlow(directory, selectedModules) {
|
async handleCustomModulesInModifyFlow(directory, selectedModules) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
// Get existing installation to find custom modules
|
// Get existing installation to find custom modules
|
||||||
const { existingInstall } = await this.getExistingInstallation(directory);
|
const { existingInstall } = await this.getExistingInstallation(directory);
|
||||||
|
|
||||||
|
|
@ -1451,16 +1431,11 @@ class UI {
|
||||||
choices.push({ name: 'Add new custom modules', value: 'add' }, { name: 'Cancel (no custom modules)', value: 'cancel' });
|
choices.push({ name: 'Add new custom modules', value: 'add' }, { name: 'Cancel (no custom modules)', value: 'cancel' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { customAction } = await inquirer.prompt([
|
const customAction = await prompts.select({
|
||||||
{
|
message: cachedCustomModules.length > 0 ? 'What would you like to do with custom modules?' : 'Would you like to add custom modules?',
|
||||||
type: 'list',
|
choices: choices,
|
||||||
name: 'customAction',
|
default: cachedCustomModules.length > 0 ? 'keep' : 'add',
|
||||||
message:
|
});
|
||||||
cachedCustomModules.length > 0 ? 'What would you like to do with custom modules?' : 'Would you like to add custom modules?',
|
|
||||||
choices: choices,
|
|
||||||
default: cachedCustomModules.length > 0 ? 'keep' : 'add',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
switch (customAction) {
|
switch (customAction) {
|
||||||
case 'keep': {
|
case 'keep': {
|
||||||
|
|
@ -1472,21 +1447,18 @@ class UI {
|
||||||
|
|
||||||
case 'select': {
|
case 'select': {
|
||||||
// Let user choose which to keep
|
// Let user choose which to keep
|
||||||
const choices = cachedCustomModules.map((m) => ({
|
const selectChoices = cachedCustomModules.map((m) => ({
|
||||||
name: `${m.name} ${chalk.gray(`(${m.id})`)}`,
|
name: `${m.name} ${chalk.gray(`(${m.id})`)}`,
|
||||||
value: m.id,
|
value: m.id,
|
||||||
|
checked: m.checked,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { keepModules } = await inquirer.prompt([
|
const keepModules = await prompts.multiselect({
|
||||||
{
|
message: 'Select custom modules to keep:',
|
||||||
type: 'checkbox',
|
choices: selectChoices,
|
||||||
name: 'keepModules',
|
required: false,
|
||||||
message: 'Select custom modules to keep:',
|
});
|
||||||
choices: choices,
|
result.selectedCustomModules = keepModules || [];
|
||||||
default: cachedCustomModules.filter((m) => m.checked).map((m) => m.id),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
result.selectedCustomModules = keepModules;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1586,7 +1558,6 @@ class UI {
|
||||||
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
|
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
|
||||||
*/
|
*/
|
||||||
async showOldAlphaVersionWarning(installedVersion, currentVersion, bmadFolderName) {
|
async showOldAlphaVersionWarning(installedVersion, currentVersion, bmadFolderName) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
const versionInfo = this.checkAlphaVersionAge(installedVersion, currentVersion);
|
const versionInfo = this.checkAlphaVersionAge(installedVersion, currentVersion);
|
||||||
|
|
||||||
// Also warn if version is unknown or can't be parsed (legacy/unsupported)
|
// Also warn if version is unknown or can't be parsed (legacy/unsupported)
|
||||||
|
|
@ -1627,26 +1598,20 @@ class UI {
|
||||||
console.log(chalk.yellow('─'.repeat(80)));
|
console.log(chalk.yellow('─'.repeat(80)));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
const { proceed } = await inquirer.prompt([
|
const proceed = await prompts.select({
|
||||||
{
|
message: 'What would you like to do?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'proceed',
|
{
|
||||||
message: 'What would you like to do?',
|
name: 'Proceed with update anyway (may have issues)',
|
||||||
choices: [
|
value: 'proceed',
|
||||||
{
|
},
|
||||||
name: 'Proceed with update anyway (may have issues)',
|
{
|
||||||
value: 'proceed',
|
name: 'Cancel (recommended - do a fresh install instead)',
|
||||||
short: 'Proceed with update',
|
value: 'cancel',
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
name: 'Cancel (recommended - do a fresh install instead)',
|
default: 'cancel',
|
||||||
value: 'cancel',
|
});
|
||||||
short: 'Cancel installation',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'cancel',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (proceed === 'cancel') {
|
if (proceed === 'cancel') {
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue