Merge branch 'main' into bmgd-changes

This commit is contained in:
Brian 2026-01-15 10:53:23 +08:00 committed by GitHub
commit e0586e2be8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1067 additions and 802 deletions

85
SECURITY.md Normal file
View File

@ -0,0 +1,85 @@
# Security Policy
## Supported Versions
We release security patches for the following versions:
| Version | Supported |
| ------- | ------------------ |
| Latest | :white_check_mark: |
| < Latest | :x: |
We recommend always using the latest version of BMad Method to ensure you have the most recent security updates.
## Reporting a Vulnerability
We take security vulnerabilities seriously. If you discover a security issue, please report it responsibly.
### How to Report
**Do NOT report security vulnerabilities through public GitHub issues.**
Instead, please report them via one of these methods:
1. **GitHub Security Advisories** (Preferred): Use [GitHub's private vulnerability reporting](https://github.com/bmad-code-org/BMAD-METHOD/security/advisories/new) to submit a confidential report.
2. **Discord**: Contact a maintainer directly via DM on our [Discord server](https://discord.gg/gk8jAdXWmj).
### What to Include
Please include as much of the following information as possible:
- Type of vulnerability (e.g., prompt injection, path traversal, etc.)
- Full paths of source file(s) related to the vulnerability
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if available)
- Impact assessment of the vulnerability
### Response Timeline
- **Initial Response**: Within 48 hours of receiving your report
- **Status Update**: Within 7 days with our assessment
- **Resolution Target**: Critical issues within 30 days; other issues within 90 days
### What to Expect
1. We will acknowledge receipt of your report
2. We will investigate and validate the vulnerability
3. We will work on a fix and coordinate disclosure timing with you
4. We will credit you in the security advisory (unless you prefer to remain anonymous)
## Security Scope
### In Scope
- Vulnerabilities in BMad Method core framework code
- Security issues in agent definitions or workflows that could lead to unintended behavior
- Path traversal or file system access issues
- Prompt injection vulnerabilities that bypass intended agent behavior
- Supply chain vulnerabilities in dependencies
### Out of Scope
- Security issues in user-created custom agents or modules
- Vulnerabilities in third-party AI providers (Claude, GPT, etc.)
- Issues that require physical access to a user's machine
- Social engineering attacks
- Denial of service attacks that don't exploit a specific vulnerability
## Security Best Practices for Users
When using BMad Method:
1. **Review Agent Outputs**: Always review AI-generated code before executing it
2. **Limit File Access**: Configure your AI IDE to limit file system access where possible
3. **Keep Updated**: Regularly update to the latest version
4. **Validate Dependencies**: Review any dependencies added by generated code
5. **Environment Isolation**: Consider running AI-assisted development in isolated environments
## Acknowledgments
We appreciate the security research community's efforts in helping keep BMad Method secure. Contributors who report valid security issues will be acknowledged in our security advisories.
---
Thank you for helping keep BMad Method and our community safe.

188
package-lock.json generated
View File

@ -19,7 +19,6 @@
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"glob": "^11.0.3", "glob": "^11.0.3",
"ignore": "^7.0.5", "ignore": "^7.0.5",
"inquirer": "^9.3.8",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"ora": "^5.4.1", "ora": "^5.4.1",
"semver": "^7.6.3", "semver": "^7.6.3",
@ -34,6 +33,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 +244,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 +755,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",
@ -1998,36 +2020,6 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
}, },
"node_modules/@inquirer/external-editor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
"integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
"license": "MIT",
"dependencies": {
"chardet": "^2.1.1",
"iconv-lite": "^0.7.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/figures": {
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
"integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@isaacs/balanced-match": { "node_modules/@isaacs/balanced-match": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
@ -3641,9 +3633,8 @@
"version": "25.0.3", "version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@ -3983,7 +3974,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"
}, },
@ -4031,6 +4021,7 @@
"version": "4.3.2", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"type-fest": "^0.21.3" "type-fest": "^0.21.3"
@ -4046,6 +4037,7 @@
"version": "0.21.3", "version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true,
"license": "(MIT OR CC0-1.0)", "license": "(MIT OR CC0-1.0)",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -4290,7 +4282,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 +5349,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",
@ -5601,12 +5591,6 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/chardet": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
"integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
"license": "MIT"
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@ -5787,15 +5771,6 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/cli-width": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
"license": "ISC",
"engines": {
"node": ">= 12"
}
},
"node_modules/cliui": { "node_modules/cliui": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@ -6689,7 +6664,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",
@ -8269,22 +8243,6 @@
"@babel/runtime": "^7.23.2" "@babel/runtime": "^7.23.2"
} }
}, },
"node_modules/iconv-lite": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/ieee754": { "node_modules/ieee754": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@ -8420,43 +8378,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/inquirer": {
"version": "9.3.8",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.8.tgz",
"integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==",
"license": "MIT",
"dependencies": {
"@inquirer/external-editor": "^1.0.2",
"@inquirer/figures": "^1.0.3",
"ansi-escapes": "^4.3.2",
"cli-width": "^4.1.0",
"mute-stream": "1.0.0",
"ora": "^5.4.1",
"run-async": "^3.0.0",
"rxjs": "^7.8.1",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^6.2.0",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/inquirer/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/iron-webcrypto": { "node_modules/iron-webcrypto": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
@ -10304,7 +10225,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",
@ -11576,15 +11496,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/mute-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
"integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
"license": "ISC",
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/nano-spawn": { "node_modules/nano-spawn": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
@ -12378,7 +12289,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 +12354,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 +13182,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"
}, },
@ -13310,15 +13218,6 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/run-async": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
"integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -13343,15 +13242,6 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -13372,12 +13262,6 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/sax": { "node_modules/sax": {
"version": "1.4.3", "version": "1.4.3",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
@ -14251,6 +14135,7 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/type-check": { "node_modules/type-check": {
@ -14335,7 +14220,7 @@
"version": "7.16.0", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"devOptional": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unicode-properties": { "node_modules/unicode-properties": {
@ -14837,7 +14722,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 +14995,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"
}, },
@ -15270,18 +15153,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/yoctocolors-cjs": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
"integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zip-stream": { "node_modules/zip-stream": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
@ -15303,7 +15174,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"
} }

View File

@ -67,6 +67,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@clack/prompts": "^0.11.0",
"@kayvan/markdown-tree-parser": "^1.6.1", "@kayvan/markdown-tree-parser": "^1.6.1",
"boxen": "^5.1.2", "boxen": "^5.1.2",
"chalk": "^4.1.2", "chalk": "^4.1.2",
@ -77,7 +78,6 @@
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"glob": "^11.0.3", "glob": "^11.0.3",
"ignore": "^7.0.5", "ignore": "^7.0.5",
"inquirer": "^9.3.8",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"ora": "^5.4.1", "ora": "^5.4.1",
"semver": "^7.6.3", "semver": "^7.6.3",

View File

@ -3,7 +3,7 @@ const path = require('node:path');
const fs = require('node:fs'); const fs = require('node:fs');
// Fix for stdin issues when running through npm on Windows // Fix for stdin issues when running through npm on Windows
// Ensures keyboard interaction works properly with inquirer prompts // Ensures keyboard interaction works properly with CLI prompts
if (process.stdin.isTTY) { if (process.stdin.isTTY) {
try { try {
process.stdin.resume(); process.stdin.resume();

View File

@ -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('');

View File

@ -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',
@ -845,7 +835,7 @@ class ConfigCollector {
} }
/** /**
* Build an inquirer question from a config item * Build a prompt question from a config item
* @param {string} moduleName - Module name * @param {string} moduleName - Module name
* @param {string} key - Config key * @param {string} key - Config key
* @param {Object} item - Config item definition * @param {Object} item - Config item definition
@ -1007,7 +997,7 @@ class ConfigCollector {
message: message, message: message,
}; };
// Set default - if it's dynamic, use a function that inquirer will evaluate with current answers // Set default - if it's dynamic, use a function that the prompt will evaluate with current answers
// But if we have an existing value, always use that instead // But if we have an existing value, always use that instead
if (existingValue !== null && existingValue !== undefined && questionType !== 'list') { if (existingValue !== null && existingValue !== undefined && questionType !== 'list') {
question.default = existingValue; question.default = existingValue;

View File

@ -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';
@ -758,6 +759,9 @@ class Installer {
config.skipIde = toolSelection.skipIde; config.skipIde = toolSelection.skipIde;
const ideConfigurations = toolSelection.configurations; const ideConfigurations = toolSelection.configurations;
// Add spacing after prompts before installation progress
console.log('');
if (spinner.isSpinning) { if (spinner.isSpinning) {
spinner.text = 'Continuing installation...'; spinner.text = 'Continuing installation...';
} else { } else {
@ -2139,15 +2143,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 +2156,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 +2170,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 +2431,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 +2444,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 +2458,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,46 +2517,38 @@ 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
const modulePath = path.join(bmadDir, moduleId); const modulePath = path.join(bmadDir, missing.id);
if (await fs.pathExists(modulePath)) { if (await fs.pathExists(modulePath)) {
const fsExtra = require('fs-extra'); const fsExtra = require('fs-extra');
await fsExtra.remove(modulePath); await fsExtra.remove(modulePath);
console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`)); console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`));
} }
await this.manifest.removeModule(bmadDir, moduleId); await this.manifest.removeModule(bmadDir, missing.id);
await this.manifest.removeCustomModule(bmadDir, moduleId); await this.manifest.removeCustomModule(bmadDir, missing.id);
console.log(chalk.yellow(` ✓ Removed from manifest`)); console.log(chalk.yellow(` ✓ Removed from manifest`));
// Also remove from installedModules list // Also remove from installedModules list
if (installedModules && installedModules.includes(moduleId)) { if (installedModules && installedModules.includes(missing.id)) {
const index = installedModules.indexOf(moduleId); const index = installedModules.indexOf(missing.id);
if (index !== -1) { if (index !== -1) {
installedModules.splice(index, 1); installedModules.splice(index, 1);
} }
@ -2591,7 +2569,7 @@ class Installer {
} }
case 'keep': { case 'keep': {
keptCount++; keptCount++;
keptModulesWithoutSources.push(moduleId); keptModulesWithoutSources.push(missing.id);
console.log(chalk.dim(` Module will be kept as-is`)); console.log(chalk.dim(` Module will be kept as-is`));
break; break;

View File

@ -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
@ -26,6 +27,21 @@ class AntigravitySetup extends BaseIdeSetup {
this.workflowsDir = 'workflows'; this.workflowsDir = 'workflows';
} }
/**
* Prompt for subagent installation location
* @returns {Promise<string>} Selected location ('project' or 'user')
*/
async _promptInstallLocation() {
return prompts.select({
message: 'Where would you like to install Antigravity subagents?',
choices: [
{ name: 'Project level (.agent/agents/)', value: 'project' },
{ name: 'User level (~/.agent/agents/)', value: 'user' },
],
default: 'project',
});
}
/** /**
* Collect configuration choices before installation * Collect configuration choices before installation
* @param {Object} options - Configuration options * @param {Object} options - Configuration options
@ -57,21 +73,7 @@ class AntigravitySetup extends BaseIdeSetup {
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents); config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
if (config.subagentChoices.install !== 'none') { if (config.subagentChoices.install !== 'none') {
// Ask for installation location config.installLocation = await this._promptInstallLocation();
const { default: inquirer } = await import('inquirer');
const locationAnswer = await inquirer.prompt([
{
type: 'list',
name: 'location',
message: 'Where would you like to install Antigravity subagents?',
choices: [
{ 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 +299,7 @@ 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 this._promptInstallLocation();
const locationAnswer = await inquirer.prompt([
{
type: 'list',
name: 'location',
message: 'Where would you like to install Antigravity subagents?',
choices: [
{ 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 ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
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 };
} }

View File

@ -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
@ -25,6 +26,21 @@ class ClaudeCodeSetup extends BaseIdeSetup {
this.agentsDir = 'agents'; this.agentsDir = 'agents';
} }
/**
* Prompt for subagent installation location
* @returns {Promise<string>} Selected location ('project' or 'user')
*/
async promptInstallLocation() {
return prompts.select({
message: 'Where would you like to install Claude Code subagents?',
choices: [
{ name: 'Project level (.claude/agents/)', value: 'project' },
{ name: 'User level (~/.claude/agents/)', value: 'user' },
],
default: 'project',
});
}
/** /**
* Collect configuration choices before installation * Collect configuration choices before installation
* @param {Object} options - Configuration options * @param {Object} options - Configuration options
@ -56,21 +72,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents); config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
if (config.subagentChoices.install !== 'none') { if (config.subagentChoices.install !== 'none') {
// Ask for installation location config.installLocation = await this.promptInstallLocation();
const { default: inquirer } = await import('inquirer');
const locationAnswer = await inquirer.prompt([
{
type: 'list',
name: 'location',
message: 'Where would you like to install Claude Code subagents?',
choices: [
{ 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 +307,7 @@ 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 this.promptInstallLocation();
const locationAnswer = await inquirer.prompt([
{
type: 'list',
name: 'location',
message: 'Where would you like to install Claude Code subagents?',
choices: [
{ 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 ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
type: 'checkbox', options: subagentConfig.files.map((file) => ({
name: 'selected', label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
message: 'Select subagents to install:', value: file,
choices: subagentConfig.files.map((file) => ({ })),
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`, initialValues: subagentConfig.files,
value: file, });
checked: true,
})),
},
]);
return { install: 'selective', selected }; return { install: 'selective', selected };
} }

View File

@ -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"));

View File

@ -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;
}, },
}, },
{ {

432
tools/cli/lib/prompts.js Normal file
View File

@ -0,0 +1,432 @@
/**
* @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();
// Support both clack-native (options) and Inquirer-style (choices) APIs
let clackOptions;
let initialValues;
if (options.options) {
// Native clack format: options with label/value
clackOptions = options.options;
initialValues = options.initialValues || [];
} else {
// Convert Inquirer-style choices to clack format
// Handle both object choices {name, value, hint} and primitive choices (string/number)
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)
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;
}
/**
* Grouped multi-select prompt for categorized options
* @param {Object} options - Prompt options
* @param {string} options.message - The question to ask
* @param {Object} options.options - Object mapping group names to arrays of choices
* @param {Array} [options.initialValues] - Array of initially selected values
* @param {boolean} [options.required=false] - Whether at least one must be selected
* @param {boolean} [options.selectableGroups=false] - Whether groups can be selected as a whole
* @returns {Promise<Array>} Array of selected values
*/
async function groupMultiselect(options) {
const clack = await getClack();
const result = await clack.groupMultiselect({
message: options.message,
options: options.options,
initialValues: options.initialValues,
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);
if (result instanceof Promise) {
throw new TypeError('Async validation is not supported by @clack/prompts. Please use synchronous validation.');
}
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);
if (result instanceof Promise) {
throw new TypeError('Async validation is not supported by @clack/prompts. Please use synchronous validation.');
}
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,
groupMultiselect,
confirm,
text,
password,
group,
tasks,
log,
prompt,
};

View File

@ -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';
} }
// Separator for choice lists (compatible interface)
const choiceUtils = { 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');
@ -477,13 +442,14 @@ class UI {
const preferredIdes = ideManager.getPreferredIdes(); const preferredIdes = ideManager.getPreferredIdes();
const otherIdes = ideManager.getOtherIdes(); const otherIdes = ideManager.getOtherIdes();
// Build IDE choices array with separators // Build grouped options object for groupMultiselect
const ideChoices = []; const groupedOptions = {};
const processedIdes = new Set(); const processedIdes = new Set();
const initialValues = [];
// First, add previously configured IDEs at the top, marked with ✅ // First, add previously configured IDEs at the top, marked with ✅
if (configuredIdes.length > 0) { if (configuredIdes.length > 0) {
ideChoices.push(new inquirer.Separator('── Previously Configured ──')); const configuredGroup = [];
for (const ideValue of configuredIdes) { for (const ideValue of configuredIdes) {
// Skip empty or invalid IDE values // Skip empty or invalid IDE values
if (!ideValue || typeof ideValue !== 'string') { if (!ideValue || typeof ideValue !== 'string') {
@ -496,81 +462,71 @@ class UI {
const ide = preferredIde || otherIde; const ide = preferredIde || otherIde;
if (ide) { if (ide) {
ideChoices.push({ configuredGroup.push({
name: `${ide.name}`, label: `${ide.name}`,
value: ide.value, value: ide.value,
checked: true, // Previously configured IDEs are checked by default
}); });
processedIdes.add(ide.value); processedIdes.add(ide.value);
initialValues.push(ide.value); // Pre-select configured IDEs
} else { } else {
// Warn about unrecognized IDE (but don't fail) // Warn about unrecognized IDE (but don't fail)
console.log(chalk.yellow(`⚠️ Previously configured IDE '${ideValue}' is no longer available`)); console.log(chalk.yellow(`⚠️ Previously configured IDE '${ideValue}' is no longer available`));
} }
} }
if (configuredGroup.length > 0) {
groupedOptions['Previously Configured'] = configuredGroup;
}
} }
// Add preferred tools (excluding already processed) // Add preferred tools (excluding already processed)
const remainingPreferred = preferredIdes.filter((ide) => !processedIdes.has(ide.value)); const remainingPreferred = preferredIdes.filter((ide) => !processedIdes.has(ide.value));
if (remainingPreferred.length > 0) { if (remainingPreferred.length > 0) {
ideChoices.push(new inquirer.Separator('── Recommended Tools ──')); groupedOptions['Recommended Tools'] = remainingPreferred.map((ide) => {
for (const ide of remainingPreferred) {
ideChoices.push({
name: `${ide.name}`,
value: ide.value,
checked: false,
});
processedIdes.add(ide.value); processedIdes.add(ide.value);
} return {
label: `${ide.name}`,
value: ide.value,
};
});
} }
// Add other tools (excluding already processed) // Add other tools (excluding already processed)
const remainingOther = otherIdes.filter((ide) => !processedIdes.has(ide.value)); const remainingOther = otherIdes.filter((ide) => !processedIdes.has(ide.value));
if (remainingOther.length > 0) { if (remainingOther.length > 0) {
ideChoices.push(new inquirer.Separator('── Additional Tools ──')); groupedOptions['Additional Tools'] = remainingOther.map((ide) => ({
for (const ide of remainingOther) { label: ide.name,
ideChoices.push({ value: ide.value,
name: ide.name, }));
value: ide.value,
checked: false,
});
}
} }
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.groupMultiselect({
{ message: `Select tools to configure ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
type: 'checkbox', options: groupedOptions,
name: 'ides', initialValues: initialValues.length > 0 ? initialValues : undefined,
message: 'Select tools to configure:', required: false,
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 - users often miss the spacebar requirement
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(' You must press SPACE to select items, then ENTER to confirm.'));
console.log(chalk.red(' Simply highlighting an item does NOT select it.')); console.log(chalk.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 +538,8 @@ class UI {
} }
return { return {
ides: answers.ides || [], ides: selectedIdes || [],
skipIde: !answers.ides || answers.ides.length === 0, skipIde: !selectedIdes || selectedIdes.length === 0,
}; };
} }
@ -592,23 +548,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 +567,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 ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
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 +589,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;
} }
/** /**
@ -753,10 +686,9 @@ class UI {
* Get module choices for selection * Get module choices for selection
* @param {Set} installedModuleIds - Currently installed module IDs * @param {Set} installedModuleIds - Currently installed module IDs
* @param {Object} customContentConfig - Custom content configuration * @param {Object} customContentConfig - Custom content configuration
* @returns {Array} Module choices for inquirer * @returns {Array} Module choices for prompt
*/ */
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;
@ -811,9 +743,9 @@ class UI {
if (allCustomModules.length > 0) { if (allCustomModules.length > 0) {
// Add separator for custom content, all custom modules, and official content separator // Add separator for custom content, all custom modules, and official content separator
moduleChoices.push( moduleChoices.push(
new inquirer.Separator('── Custom Content ──'), new choiceUtils.Separator('── Custom Content ──'),
...allCustomModules, ...allCustomModules,
new inquirer.Separator('── Official Content ──'), new choiceUtils.Separator('── Official Content ──'),
); );
} }
@ -837,44 +769,43 @@ 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 ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
choices: choicesWithDefaults,
required: false,
});
return selected; return selected || [];
} }
/** /**
* Prompt for directory selection * Prompt for directory selection
* @returns {Object} Directory answer from inquirer * @returns {Object} Directory answer from prompt
*/ */
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 +846,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 +987,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
*/ */
@ -1071,7 +1070,7 @@ class UI {
* @sideeffects None - pure user input collection, no files written * @sideeffects None - pure user input collection, no files written
* @edgecases Shows warning if user enables TTS but AgentVibes not detected * @edgecases Shows warning if user enables TTS but AgentVibes not detected
* @calledby promptInstall() during installation flow, after core config, before IDE selection * @calledby promptInstall() during installation flow, after core config, before IDE selection
* @calls checkAgentVibesInstalled(), inquirer.prompt(), chalk.green/yellow/dim() * @calls checkAgentVibesInstalled(), prompts.select(), chalk.green/yellow/dim()
* *
* AI NOTE: This prompt is strategically positioned in installation flow: * AI NOTE: This prompt is strategically positioned in installation flow:
* - AFTER core config (user_name, etc) * - AFTER core config (user_name, etc)
@ -1102,7 +1101,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 +1112,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,
}; };
} }
@ -1248,30 +1242,75 @@ class UI {
return existingInstall.ides || []; return existingInstall.ides || [];
} }
/**
* Validate custom content path synchronously
* @param {string} input - User input path
* @returns {string|undefined} Error message or undefined if valid
*/
validateCustomContentPathSync(input) {
// Allow empty input to cancel
if (!input || input.trim() === '') {
return; // 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 {
const yaml = require('yaml');
const content = fs.readFileSync(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; // Valid
} catch (error) {
return 'Error validating path: ' + error.message;
}
}
/** /**
* Prompt user for custom content source location * Prompt user for custom content source location
* @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 +1321,11 @@ 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) => this.validateCustomContentPathSync(input),
message: 'Enter the path to your custom content folder (or press Enter to cancel):', });
validate: async (input) => {
// Allow empty input to cancel
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 (!(await fs.pathExists(expandedPath))) {
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) {
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 +1357,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 +1380,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 +1439,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 +1455,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 ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
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 +1566,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 +1606,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('');