Compare commits

..

No commits in common. "8c4058bca3b568a2e6e4da783462a5777131bf94" and "1ac25c2a7af495b961d12e86f733d3d161102a92" have entirely different histories.

24 changed files with 1362 additions and 1047 deletions

390
package-lock.json generated
View File

@ -12,15 +12,20 @@
"@clack/core": "^1.0.0",
"@clack/prompts": "^1.0.0",
"@kayvan/markdown-tree-parser": "^1.6.1",
"boxen": "^5.1.2",
"chalk": "^4.1.2",
"cli-table3": "^0.6.5",
"commander": "^14.0.0",
"csv-parse": "^6.1.0",
"figlet": "^1.8.0",
"fs-extra": "^11.3.0",
"glob": "^11.0.3",
"ignore": "^7.0.5",
"js-yaml": "^4.1.0",
"ora": "^5.4.1",
"picocolors": "^1.1.1",
"semver": "^7.6.3",
"wrap-ansi": "^7.0.0",
"xml2js": "^0.6.2",
"yaml": "^2.7.0"
},
@ -772,6 +777,16 @@
"sisteransi": "^1.0.5"
}
},
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
@ -2014,9 +2029,9 @@
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
@ -3978,7 +3993,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
"integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.1.0"
@ -4971,6 +4985,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
@ -5008,12 +5042,59 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/bl/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/boxen": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
"integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
"license": "MIT",
"dependencies": {
"ansi-align": "^3.0.0",
"camelcase": "^6.2.0",
"chalk": "^4.1.0",
"cli-boxes": "^2.2.1",
"string-width": "^4.2.2",
"type-fest": "^0.20.2",
"widest-line": "^3.1.0",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@ -5082,6 +5163,30 @@
"node-int64": "^0.4.0"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -5150,7 +5255,6 @@
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@ -5329,6 +5433,18 @@
"node": ">=0.8.0"
}
},
"node_modules/cli-boxes": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
"integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
@ -5345,6 +5461,33 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-spinners": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-table3": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz",
"integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==",
"license": "MIT",
"dependencies": {
"string-width": "^4.2.0"
},
"engines": {
"node": "10.* || >= 12.*"
},
"optionalDependencies": {
"@colors/colors": "1.5.0"
}
},
"node_modules/cli-truncate": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz",
@ -5417,6 +5560,15 @@
"node": ">=8"
}
},
"node_modules/clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@ -5790,6 +5942,18 @@
"node": ">=0.10.0"
}
},
"node_modules/defaults": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
"integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
"license": "MIT",
"dependencies": {
"clone": "^1.0.2"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
@ -6870,6 +7034,21 @@
}
}
},
"node_modules/figlet": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/figlet/-/figlet-1.10.0.tgz",
"integrity": "sha512-aktIwEZZ6Gp9AWdMXW4YCi0J2Ahuxo67fNJRUIWD81w8pQ0t9TS8FFpbl27ChlTLF06VkwjDesZSzEVzN75rzA==",
"license": "MIT",
"dependencies": {
"commander": "^14.0.0"
},
"bin": {
"figlet": "bin/index.js"
},
"engines": {
"node": ">= 17.0.0"
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@ -7727,6 +7906,26 @@
"@babel/runtime": "^7.23.2"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
@ -7823,7 +8022,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ini": {
@ -8008,6 +8206,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-interactive": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@ -8043,6 +8250,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-wsl": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
@ -9304,6 +9523,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/log-symbols": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
"license": "MIT",
"dependencies": {
"chalk": "^4.1.0",
"is-unicode-supported": "^0.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
@ -10750,7 +10985,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -11062,7 +11296,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-fn": "^2.1.0"
@ -11111,6 +11344,81 @@
"node": ">= 0.8.0"
}
},
"node_modules/ora": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
"integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.1.0",
"chalk": "^4.1.0",
"cli-cursor": "^3.1.0",
"cli-spinners": "^2.5.0",
"is-interactive": "^1.0.0",
"is-unicode-supported": "^0.1.0",
"log-symbols": "^4.1.0",
"strip-ansi": "^6.0.0",
"wcwidth": "^1.0.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ora/node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"license": "MIT",
"dependencies": {
"restore-cursor": "^3.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ora/node_modules/restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"license": "MIT",
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ora/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/ora/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-limit": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz",
@ -12418,6 +12726,26 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/sax": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
@ -12770,6 +13098,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-argv": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
@ -13354,6 +13691,18 @@
"node": ">=4"
}
},
"node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
@ -13803,7 +14152,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/v8-to-istanbul": {
@ -13969,6 +14317,15 @@
"makeerror": "1.0.12"
}
},
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
"integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
"license": "MIT",
"dependencies": {
"defaults": "^1.0.3"
}
},
"node_modules/web-namespaces": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
@ -14005,6 +14362,18 @@
"node": ">=4"
}
},
"node_modules/widest-line": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
"integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
"license": "MIT",
"dependencies": {
"string-width": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@ -14019,7 +14388,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
@ -14076,7 +14444,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -14086,7 +14453,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"

View File

@ -69,15 +69,20 @@
"@clack/core": "^1.0.0",
"@clack/prompts": "^1.0.0",
"@kayvan/markdown-tree-parser": "^1.6.1",
"boxen": "^5.1.2",
"chalk": "^4.1.2",
"cli-table3": "^0.6.5",
"commander": "^14.0.0",
"csv-parse": "^6.1.0",
"figlet": "^1.8.0",
"fs-extra": "^11.3.0",
"glob": "^11.0.3",
"ignore": "^7.0.5",
"js-yaml": "^4.1.0",
"ora": "^5.4.1",
"picocolors": "^1.1.1",
"semver": "^7.6.3",
"wrap-ansi": "^7.0.0",
"xml2js": "^0.6.2",
"yaml": "^2.7.0"
},

View File

@ -2,14 +2,6 @@ const { program } = require('commander');
const path = require('node:path');
const fs = require('node:fs');
const { execSync } = require('node:child_process');
const prompts = require('./lib/prompts');
// The installer flow uses many sequential @clack/prompts, each adding keypress
// listeners to stdin. Raise the limit to avoid spurious EventEmitter warnings.
if (process.stdin?.setMaxListeners) {
const currentLimit = process.stdin.getMaxListeners();
process.stdin.setMaxListeners(Math.max(currentLimit, 50));
}
// Check for updates - do this asynchronously so it doesn't block startup
const packageJson = require('../../package.json');
@ -35,17 +27,17 @@ async function checkForUpdate() {
}).trim();
if (result && result !== packageJson.version) {
const color = await prompts.getColor();
const updateMsg = [
`You are using version ${packageJson.version} but ${result} is available.`,
'',
'To update, exit and first run:',
` npm cache clean --force && npx bmad-method@${tag} install`,
].join('\n');
await prompts.box(updateMsg, 'Update Available', {
rounded: true,
formatBorder: color.yellow,
});
console.warn('');
console.warn(' ╔═══════════════════════════════════════════════════════════════════════════════╗');
console.warn(' ║ UPDATE AVAILABLE ║');
console.warn(' ║ ║');
console.warn(` ║ You are using version ${packageJson.version} but ${result} is available. ║`);
console.warn(' ║ ║');
console.warn(' ║ To update,exir and first run: ║');
console.warn(` ║ npm cache clean --force && npx bmad-method@${tag} install ║`);
console.warn(' ║ ║');
console.warn(' ╚═══════════════════════════════════════════════════════════════════════════════╝');
console.warn('');
}
} catch {
// Silently fail - network issues or npm not available

View File

@ -1,5 +1,5 @@
const chalk = require('chalk');
const path = require('node:path');
const prompts = require('../lib/prompts');
const { Installer } = require('../installers/lib/core/installer');
const { UI } = require('../lib/ui');
@ -30,14 +30,14 @@ module.exports = {
// Set debug flag as environment variable for all components
if (options.debug) {
process.env.BMAD_DEBUG_MANIFEST = 'true';
await prompts.log.info('Debug mode enabled');
console.log(chalk.cyan('Debug mode enabled\n'));
}
const config = await ui.promptInstall(options);
// Handle cancel
if (config.actionType === 'cancel') {
await prompts.log.warn('Installation cancelled.');
console.log(chalk.yellow('Installation cancelled.'));
process.exit(0);
return;
}
@ -45,13 +45,13 @@ module.exports = {
// Handle quick update separately
if (config.actionType === 'quick-update') {
const result = await installer.quickUpdate(config);
await prompts.log.success('Quick update complete!');
await prompts.log.info(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`);
console.log(chalk.green('\n✨ Quick update complete!'));
console.log(chalk.cyan(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`));
// Display version-specific end message
const { MessageLoader } = require('../installers/lib/message-loader');
const messageLoader = new MessageLoader();
await messageLoader.displayEndMessage();
messageLoader.displayEndMessage();
process.exit(0);
return;
@ -60,8 +60,8 @@ module.exports = {
// Handle compile agents separately
if (config.actionType === 'compile-agents') {
const result = await installer.compileAgents(config);
await prompts.log.success('Agent recompilation complete!');
await prompts.log.info(`Recompiled ${result.agentCount} agents with customizations applied`);
console.log(chalk.green('\n✨ Agent recompilation complete!'));
console.log(chalk.cyan(`Recompiled ${result.agentCount} agents with customizations applied`));
process.exit(0);
return;
}
@ -80,22 +80,21 @@ module.exports = {
// Display version-specific end message from install-messages.yaml
const { MessageLoader } = require('../installers/lib/message-loader');
const messageLoader = new MessageLoader();
await messageLoader.displayEndMessage();
messageLoader.displayEndMessage();
process.exit(0);
}
} catch (error) {
try {
if (error.fullMessage) {
await prompts.log.error(error.fullMessage);
} else {
await prompts.log.error(`Installation failed: ${error.message}`);
}
// Check if error has a complete formatted message
if (error.fullMessage) {
console.error(error.fullMessage);
if (error.stack) {
await prompts.log.message(error.stack);
console.error('\n' + chalk.dim(error.stack));
}
} catch {
console.error(error.fullMessage || error.message || error);
} else {
// Generic error handling for all other errors
console.error(chalk.red('Installation failed:'), error.message);
console.error(chalk.dim(error.stack));
}
process.exit(1);
}

View File

@ -1,5 +1,5 @@
const chalk = require('chalk');
const path = require('node:path');
const prompts = require('../lib/prompts');
const { Installer } = require('../installers/lib/core/installer');
const { Manifest } = require('../installers/lib/core/manifest');
const { UI } = require('../lib/ui');
@ -21,9 +21,9 @@ module.exports = {
// Check if bmad directory exists
const fs = require('fs-extra');
if (!(await fs.pathExists(bmadDir))) {
await prompts.log.warn('No BMAD installation found in the current directory.');
await prompts.log.message(`Expected location: ${bmadDir}`);
await prompts.log.message('Run "bmad install" to set up a new installation.');
console.log(chalk.yellow('No BMAD installation found in the current directory.'));
console.log(chalk.dim(`Expected location: ${bmadDir}`));
console.log(chalk.dim('\nRun "bmad install" to set up a new installation.'));
process.exit(0);
return;
}
@ -32,8 +32,8 @@ module.exports = {
const manifestData = await manifest._readRaw(bmadDir);
if (!manifestData) {
await prompts.log.warn('No BMAD installation manifest found.');
await prompts.log.message('Run "bmad install" to set up a new installation.');
console.log(chalk.yellow('No BMAD installation manifest found.'));
console.log(chalk.dim('\nRun "bmad install" to set up a new installation.'));
process.exit(0);
return;
}
@ -46,7 +46,7 @@ module.exports = {
const availableUpdates = await manifest.checkForUpdates(bmadDir);
// Display status
await ui.displayStatus({
ui.displayStatus({
installation,
modules,
availableUpdates,
@ -55,9 +55,9 @@ module.exports = {
process.exit(0);
} catch (error) {
await prompts.log.error(`Status check failed: ${error.message}`);
console.error(chalk.red('Status check failed:'), error.message);
if (process.env.BMAD_DEBUG) {
await prompts.log.message(error.stack);
console.error(chalk.dim(error.stack));
}
process.exit(1);
}

View File

@ -1,6 +1,7 @@
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('yaml');
const chalk = require('chalk');
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
const { CLIUtils } = require('../../../lib/cli-utils');
const prompts = require('../../../lib/prompts');
@ -259,9 +260,15 @@ class ConfigCollector {
// If module has no config keys at all, handle it specially
if (hasNoConfig && moduleConfig.subheader) {
// Add blank line for better readability (matches other modules)
console.log();
const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
await prompts.log.step(moduleDisplayName);
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
// Display the module name in color first (matches other modules)
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
// Show the subheader since there's no configuration to ask about
console.log(chalk.dim(`${moduleConfig.subheader}`));
return false; // No new fields
}
@ -315,7 +322,7 @@ class ConfigCollector {
}
// Show "no config" message for modules with no new questions (that have config keys)
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module already up to date`);
console.log(chalk.dim(` ${moduleName.toUpperCase()} module already up to date`));
return false; // No new fields
}
@ -343,15 +350,15 @@ class ConfigCollector {
if (questions.length > 0) {
// Only show header if we actually have questions
await CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
await prompts.log.message('');
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
console.log(); // Line break before questions
const promptedAnswers = await prompts.prompt(questions);
// Merge prompted answers with static answers
Object.assign(allAnswers, promptedAnswers);
} else if (newStaticKeys.length > 0) {
// Only static fields, no questions - show no config message
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configuration updated`);
console.log(chalk.dim(` ${moduleName.toUpperCase()} module configuration updated`));
}
// Store all answers for cross-referencing
@ -581,7 +588,7 @@ class ConfigCollector {
// Skip prompts mode: use all defaults without asking
if (this.skipPrompts) {
await prompts.log.info(`Using default configuration for ${moduleDisplayName}`);
console.log(chalk.cyan('Using default configuration for'), chalk.magenta(moduleDisplayName));
// Use defaults for all questions
for (const question of questions) {
const hasDefault = question.default !== undefined && question.default !== null && question.default !== '';
@ -590,10 +597,12 @@ class ConfigCollector {
}
}
} else {
await prompts.log.step(moduleDisplayName);
console.log();
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
let customize = true;
if (moduleName === 'core') {
// Core module: no confirm prompt, continues directly
// Core module: no confirm prompt, so add spacing manually to match visual style
console.log(chalk.gray('│'));
} else {
// Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing)
const customizeAnswer = await prompts.prompt([
@ -612,7 +621,7 @@ class ConfigCollector {
const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === '');
if (questionsWithoutDefaults.length > 0) {
await prompts.log.message(` Asking required questions for ${moduleName.toUpperCase()}...`);
console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
const promptedAnswers = await prompts.prompt(questionsWithoutDefaults);
Object.assign(allAnswers, promptedAnswers);
}
@ -738,15 +747,32 @@ class ConfigCollector {
const hasNoConfig = actualConfigKeys.length === 0;
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
await prompts.log.step(moduleDisplayName);
// Module explicitly has no configuration - show with special styling
// Add blank line for better readability (matches other modules)
console.log();
// Display the module name in color first (matches other modules)
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
// Ask user if they want to accept defaults or customize on the next line
const { customize } = await prompts.prompt([
{
type: 'confirm',
name: 'customize',
message: 'Accept Defaults (no to customize)?',
default: true,
},
]);
// Show the subheader if available, otherwise show a default message
if (moduleConfig.subheader) {
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
console.log(chalk.dim(` ${moduleConfig.subheader}`));
} else {
await prompts.log.message(` \u2713 No custom configuration required`);
console.log(chalk.dim(` ✓ No custom configuration required`));
}
} else {
// Module has config but just no questions to ask
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
console.log(chalk.dim(` ${moduleName.toUpperCase()} module configured`));
}
}
@ -955,15 +981,14 @@ class ConfigCollector {
}
// Add current value indicator for existing configs
const color = await prompts.getColor();
if (existingValue !== null && existingValue !== undefined) {
if (typeof existingValue === 'boolean') {
message += color.dim(` (current: ${existingValue ? 'true' : 'false'})`);
message += chalk.dim(` (current: ${existingValue ? 'true' : 'false'})`);
} else if (Array.isArray(existingValue)) {
message += color.dim(` (current: ${existingValue.join(', ')})`);
message += chalk.dim(` (current: ${existingValue.join(', ')})`);
} else if (questionType !== 'list') {
// Show the cleaned value (without {project-root}/) for display
message += color.dim(` (current: ${existingValue})`);
message += chalk.dim(` (current: ${existingValue})`);
}
} else if (item.example && questionType === 'input') {
// Show example for input fields
@ -973,7 +998,7 @@ class ConfigCollector {
exampleText = this.replacePlaceholders(exampleText, moduleName, moduleConfig);
exampleText = exampleText.replace('{project-root}/', '');
}
message += color.dim(` (e.g., ${exampleText})`);
message += chalk.dim(` (e.g., ${exampleText})`);
}
// Build the question object

View File

@ -1,8 +1,8 @@
const fs = require('fs-extra');
const path = require('node:path');
const glob = require('glob');
const chalk = require('chalk');
const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
/**
* Dependency Resolver for BMAD modules
@ -24,7 +24,7 @@ class DependencyResolver {
*/
async resolve(bmadDir, selectedModules = [], options = {}) {
if (options.verbose) {
await prompts.log.info('Resolving module dependencies...');
console.log(chalk.cyan('Resolving module dependencies...'));
}
// Always include core as base
@ -50,7 +50,7 @@ class DependencyResolver {
// Report results (only in verbose mode)
if (options.verbose) {
await this.reportResults(organizedFiles, selectedModules);
this.reportResults(organizedFiles, selectedModules);
}
return {
@ -90,12 +90,8 @@ class DependencyResolver {
}
}
if (!moduleDir) {
continue;
}
if (!(await fs.pathExists(moduleDir))) {
await prompts.log.warn('Module directory not found: ' + moduleDir);
console.warn(chalk.yellow(`Module directory not found: ${moduleDir}`));
continue;
}
@ -183,7 +179,7 @@ class DependencyResolver {
}
}
} catch (error) {
await prompts.log.warn('Failed to parse frontmatter in ' + file.name + ': ' + error.message);
console.warn(chalk.yellow(`Failed to parse frontmatter in ${file.name}: ${error.message}`));
}
}
@ -662,8 +658,8 @@ class DependencyResolver {
/**
* Report resolution results
*/
async reportResults(organized, selectedModules) {
await prompts.log.success('Dependency resolution complete');
reportResults(organized, selectedModules) {
console.log(chalk.green('\n✓ Dependency resolution complete'));
for (const [module, files] of Object.entries(organized)) {
const isSelected = selectedModules.includes(module) || module === 'core';
@ -671,31 +667,31 @@ class DependencyResolver {
files.agents.length + files.tasks.length + files.tools.length + files.templates.length + files.data.length + files.other.length;
if (totalFiles > 0) {
await prompts.log.info(` ${module.toUpperCase()} module:`);
await prompts.log.message(` Status: ${isSelected ? 'Selected' : 'Dependencies only'}`);
console.log(chalk.cyan(`\n ${module.toUpperCase()} module:`));
console.log(chalk.dim(` Status: ${isSelected ? 'Selected' : 'Dependencies only'}`));
if (files.agents.length > 0) {
await prompts.log.message(` Agents: ${files.agents.length}`);
console.log(chalk.dim(` Agents: ${files.agents.length}`));
}
if (files.tasks.length > 0) {
await prompts.log.message(` Tasks: ${files.tasks.length}`);
console.log(chalk.dim(` Tasks: ${files.tasks.length}`));
}
if (files.templates.length > 0) {
await prompts.log.message(` Templates: ${files.templates.length}`);
console.log(chalk.dim(` Templates: ${files.templates.length}`));
}
if (files.data.length > 0) {
await prompts.log.message(` Data files: ${files.data.length}`);
console.log(chalk.dim(` Data files: ${files.data.length}`));
}
if (files.other.length > 0) {
await prompts.log.message(` Other files: ${files.other.length}`);
console.log(chalk.dim(` Other files: ${files.other.length}`));
}
}
}
if (this.missingDependencies.size > 0) {
await prompts.log.warn('Missing dependencies:');
console.log(chalk.yellow('\n ⚠ Missing dependencies:'));
for (const missing of this.missingDependencies) {
await prompts.log.warn(` - ${missing}`);
console.log(chalk.yellow(` - ${missing}`));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
const { FileOps } = require('../../../lib/file-ops');
const { XmlHandler } = require('../../../lib/xml-handler');
@ -88,7 +88,7 @@ class CustomHandler {
try {
config = yaml.parse(configContent);
} catch (parseError) {
await prompts.log.warn('YAML parse error in ' + configPath + ': ' + parseError.message);
console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message));
return null;
}
@ -111,7 +111,7 @@ class CustomHandler {
isInstallConfig: isInstallConfig, // Track which type this is
};
} catch (error) {
await prompts.log.warn('Failed to read ' + configPath + ': ' + error.message);
console.warn(chalk.yellow(`Warning: Failed to read ${configPath}:`, error.message));
return null;
}
}
@ -268,13 +268,14 @@ class CustomHandler {
}
results.filesCopied++;
if (entry.name.endsWith('.md')) {
results.workflowsInstalled++;
}
if (fileTrackingCallback) {
fileTrackingCallback(targetPath);
}
}
if (entry.name.endsWith('.md')) {
results.workflowsInstalled++;
}
} catch (error) {
results.errors.push(`Failed to copy ${entry.name}: ${error.message}`);
}
@ -321,7 +322,7 @@ class CustomHandler {
await fs.writeFile(customizePath, templateContent, 'utf8');
// Only show customize creation in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(' Created customize: custom-' + agentName + '.customize.yaml');
console.log(chalk.dim(` Created customize: custom-${agentName}.customize.yaml`));
}
}
}
@ -345,10 +346,14 @@ class CustomHandler {
// Only show compilation details in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(' Compiled agent: ' + agentName + ' -> ' + path.relative(targetAgentsPath, targetMdPath));
console.log(
chalk.dim(
` Compiled agent: ${agentName} -> ${path.relative(targetAgentsPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
),
);
}
} catch (error) {
await prompts.log.warn(' Failed to compile agent ' + agentName + ': ' + error.message);
console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
results.errors.push(`Failed to compile agent ${agentName}: ${error.message}`);
}
}

View File

@ -1,7 +1,7 @@
const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
const { XmlHandler } = require('../../../lib/xml-handler');
const prompts = require('../../../lib/prompts');
const { getSourcePath } = require('../../../lib/project-root');
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
@ -53,7 +53,7 @@ class BaseIdeSetup {
* Cleanup IDE configuration
* @param {string} projectDir - Project directory
*/
async cleanup(projectDir, options = {}) {
async cleanup(projectDir) {
// Default implementation - can be overridden
if (this.configDir) {
const configPath = path.join(projectDir, this.configDir);
@ -61,7 +61,7 @@ class BaseIdeSetup {
const bmadRulesPath = path.join(configPath, BMAD_FOLDER_NAME);
if (await fs.pathExists(bmadRulesPath)) {
await fs.remove(bmadRulesPath);
if (!options.silent) await prompts.log.message(`Removed ${this.name} BMAD configuration`);
console.log(chalk.dim(`Removed ${this.name} BMAD configuration`));
}
}
}

View File

@ -1,7 +1,7 @@
const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
const { BaseIdeSetup } = require('./_base-ide');
const prompts = require('../../../lib/prompts');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
@ -34,10 +34,10 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
* @returns {Promise<Object>} Setup result
*/
async setup(projectDir, bmadDir, options = {}) {
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Clean up any old BMAD installation first
await this.cleanup(projectDir, options);
await this.cleanup(projectDir);
if (!this.installerConfig) {
return { success: false, reason: 'no-config' };
@ -102,7 +102,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
results.tools = taskToolResult.tools || 0;
}
await this.printSummary(results, target_dir, options);
this.printSummary(results, target_dir);
return { success: true, results };
}
@ -439,28 +439,32 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
* @param {Object} results - Installation results
* @param {string} targetDir - Target directory (relative)
*/
async printSummary(results, targetDir, options = {}) {
if (options.silent) return;
const parts = [];
if (results.agents > 0) parts.push(`${results.agents} agents`);
if (results.workflows > 0) parts.push(`${results.workflows} workflows`);
if (results.tasks > 0) parts.push(`${results.tasks} tasks`);
if (results.tools > 0) parts.push(`${results.tools} tools`);
await prompts.log.success(`${this.name} configured: ${parts.join(', ')}${targetDir}`);
printSummary(results, targetDir) {
console.log(chalk.green(`\n${this.name} configured:`));
if (results.agents > 0) {
console.log(chalk.dim(` - ${results.agents} agents installed`));
}
if (results.workflows > 0) {
console.log(chalk.dim(` - ${results.workflows} workflow commands generated`));
}
if (results.tasks > 0 || results.tools > 0) {
console.log(chalk.dim(` - ${results.tasks + results.tools} task/tool commands generated`));
}
console.log(chalk.dim(` - Destination: ${targetDir}`));
}
/**
* Cleanup IDE configuration
* @param {string} projectDir - Project directory
*/
async cleanup(projectDir, options = {}) {
async cleanup(projectDir) {
// Clean all target directories
if (this.installerConfig?.targets) {
for (const target of this.installerConfig.targets) {
await this.cleanupTarget(projectDir, target.target_dir, options);
await this.cleanupTarget(projectDir, target.target_dir);
}
} else if (this.installerConfig?.target_dir) {
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options);
await this.cleanupTarget(projectDir, this.installerConfig.target_dir);
}
}
@ -469,7 +473,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
* @param {string} projectDir - Project directory
* @param {string} targetDir - Target directory to clean
*/
async cleanupTarget(projectDir, targetDir, options = {}) {
async cleanupTarget(projectDir, targetDir) {
const targetPath = path.join(projectDir, targetDir);
if (!(await fs.pathExists(targetPath))) {
@ -492,22 +496,25 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
let removedCount = 0;
for (const entry of entries) {
// Skip non-strings or undefined entries
if (!entry || typeof entry !== 'string') {
continue;
}
if (entry.startsWith('bmad')) {
const entryPath = path.join(targetPath, entry);
try {
const stat = await fs.stat(entryPath);
if (stat.isFile()) {
await fs.remove(entryPath);
removedCount++;
} else if (stat.isDirectory()) {
await fs.remove(entryPath);
removedCount++;
} catch {
// Skip entries that can't be removed (broken symlinks, permission errors)
}
}
}
if (removedCount > 0 && !options.silent) {
await prompts.log.message(` Cleaned ${removedCount} BMAD files from ${targetDir}`);
if (removedCount > 0) {
console.log(chalk.dim(` Cleaned ${removedCount} BMAD files from ${targetDir}`));
}
}
}

View File

@ -1,6 +1,7 @@
const path = require('node:path');
const fs = require('fs-extra');
const os = require('node:os');
const chalk = require('chalk');
const { BaseIdeSetup } = require('./_base-ide');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
@ -42,11 +43,12 @@ class CodexSetup extends BaseIdeSetup {
default: 'global',
});
// Show brief confirmation hint (detailed instructions available via verbose)
// Display detailed instructions for the chosen option
console.log('');
if (installLocation === 'project') {
await prompts.log.info('Prompts installed to: <project>/.codex/prompts (requires CODEX_HOME)');
console.log(this.getProjectSpecificInstructions());
} else {
await prompts.log.info('Prompts installed to: ~/.codex/prompts');
console.log(this.getGlobalInstructions());
}
// Confirm the choice
@ -56,7 +58,7 @@ class CodexSetup extends BaseIdeSetup {
});
if (!confirmed) {
await prompts.log.warn("Let's choose a different installation option.");
console.log(chalk.yellow("\n Let's choose a different installation option.\n"));
}
}
@ -70,7 +72,7 @@ class CodexSetup extends BaseIdeSetup {
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Always use CLI mode
const mode = 'cli';
@ -82,7 +84,7 @@ class CodexSetup extends BaseIdeSetup {
const destDir = this.getCodexPromptDir(projectDir, installLocation);
await fs.ensureDir(destDir);
await this.clearOldBmadFiles(destDir, options);
await this.clearOldBmadFiles(destDir);
// Collect artifacts and write using underscore format
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
@ -122,11 +124,16 @@ class CodexSetup extends BaseIdeSetup {
const written = agentCount + workflowCount + tasksWritten;
if (!options.silent) {
await prompts.log.success(
`${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} files → ${destDir}`,
);
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - Mode: CLI`));
console.log(chalk.dim(` - ${counts.agents} agents exported`));
console.log(chalk.dim(` - ${counts.tasks} tasks exported`));
console.log(chalk.dim(` - ${counts.workflows} workflow commands exported`));
if (counts.workflowLaunchers > 0) {
console.log(chalk.dim(` - ${counts.workflowLaunchers} workflow launchers exported`));
}
console.log(chalk.dim(` - ${written} Codex prompt files written`));
console.log(chalk.dim(` - Destination: ${destDir}`));
return {
success: true,
@ -255,7 +262,7 @@ class CodexSetup extends BaseIdeSetup {
return written;
}
async clearOldBmadFiles(destDir, options = {}) {
async clearOldBmadFiles(destDir) {
if (!(await fs.pathExists(destDir))) {
return;
}
@ -265,7 +272,7 @@ class CodexSetup extends BaseIdeSetup {
entries = await fs.readdir(destDir);
} catch (error) {
// Directory exists but can't be read - skip cleanup
if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`);
console.warn(chalk.yellow(`Warning: Could not read directory ${destDir}: ${error.message}`));
return;
}
@ -284,11 +291,15 @@ class CodexSetup extends BaseIdeSetup {
const entryPath = path.join(destDir, entry);
try {
await fs.remove(entryPath);
} catch (error) {
if (!options.silent) {
await prompts.log.message(` Skipping ${entry}: ${error.message}`);
const stat = await fs.stat(entryPath);
if (stat.isFile()) {
await fs.remove(entryPath);
} else if (stat.isDirectory()) {
await fs.remove(entryPath);
}
} catch (error) {
// Skip files that can't be processed
console.warn(chalk.dim(` Skipping ${entry}: ${error.message}`));
}
}
}
@ -304,16 +315,22 @@ class CodexSetup extends BaseIdeSetup {
*/
getGlobalInstructions(destDir) {
const lines = [
'IMPORTANT: Codex Configuration',
'',
'/prompts installed globally to your HOME DIRECTORY.',
chalk.bold.cyan('═'.repeat(70)),
chalk.bold.yellow(' IMPORTANT: Codex Configuration'),
chalk.bold.cyan('═'.repeat(70)),
'',
'These prompts reference a specific _bmad path.',
"To use with other projects, you'd need to copy the _bmad dir.",
chalk.white(' /prompts installed globally to your HOME DIRECTORY.'),
'',
chalk.yellow(' ⚠️ These prompts reference a specific _bmad path'),
chalk.dim(" To use with other projects, you'd need to copy the _bmad dir"),
'',
chalk.green(' ✓ You can now use /commands in Codex CLI'),
chalk.dim(' Example: /bmad_bmm_pm'),
chalk.dim(' Type / to see all available commands'),
'',
chalk.bold.cyan('═'.repeat(70)),
'',
'You can now use /commands in Codex CLI',
' Example: /bmad_bmm_pm',
' Type / to see all available commands',
];
return lines.join('\n');
}
@ -328,34 +345,43 @@ class CodexSetup extends BaseIdeSetup {
const isWindows = os.platform() === 'win32';
const commonLines = [
'Project-Specific Codex Configuration',
'',
`Prompts will be installed to: ${destDir || '<project>/.codex/prompts'}`,
chalk.bold.cyan('═'.repeat(70)),
chalk.bold.yellow(' Project-Specific Codex Configuration'),
chalk.bold.cyan('═'.repeat(70)),
'',
'REQUIRED: You must set CODEX_HOME to use these prompts',
chalk.white(' Prompts will be installed to: ') + chalk.cyan(destDir || '<project>/.codex/prompts'),
'',
chalk.bold.yellow(' ⚠️ REQUIRED: You must set CODEX_HOME to use these prompts'),
'',
];
const windowsLines = [
'Create a codex.cmd file in your project root:',
chalk.bold(' Create a codex.cmd file in your project root:'),
'',
' @echo off',
' set CODEX_HOME=%~dp0.codex',
' codex %*',
chalk.green(' @echo off'),
chalk.green(' set CODEX_HOME=%~dp0.codex'),
chalk.green(' codex %*'),
'',
String.raw`Then run: .\codex instead of codex`,
'(The %~dp0 gets the directory of the .cmd file)',
chalk.dim(String.raw` Then run: .\codex instead of codex`),
chalk.dim(' (The %~dp0 gets the directory of the .cmd file)'),
];
const unixLines = [
'Add this alias to your ~/.bashrc or ~/.zshrc:',
chalk.bold(' Add this alias to your ~/.bashrc or ~/.zshrc:'),
'',
' alias codex=\'CODEX_HOME="$PWD/.codex" codex\'',
chalk.green(' alias codex=\'CODEX_HOME="$PWD/.codex" codex\''),
'',
chalk.dim(' After adding, run: source ~/.bashrc (or source ~/.zshrc)'),
chalk.dim(' (The $PWD uses your current working directory)'),
];
const closingLines = [
'',
chalk.dim(' This tells Codex CLI to use prompts from this project instead of ~/.codex'),
'',
chalk.bold.cyan('═'.repeat(70)),
'',
'After adding, run: source ~/.bashrc (or source ~/.zshrc)',
'(The $PWD uses your current working directory)',
];
const closingLines = ['', 'This tells Codex CLI to use prompts from this project instead of ~/.codex'];
const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines];

View File

@ -1,7 +1,7 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
@ -23,10 +23,10 @@ class KiloSetup extends BaseIdeSetup {
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Clean up any old BMAD installation first
await this.cleanup(projectDir, options);
await this.cleanup(projectDir);
// Load existing config (may contain non-BMAD modes and other settings)
const kiloModesPath = path.join(projectDir, this.configFile);
@ -38,7 +38,7 @@ class KiloSetup extends BaseIdeSetup {
config = yaml.parse(existingContent) || {};
} catch {
// If parsing fails, start fresh but warn user
await prompts.log.warn('Warning: Could not parse existing .kilocodemodes, starting fresh');
console.log(chalk.yellow('Warning: Could not parse existing .kilocodemodes, starting fresh'));
config = {};
}
}
@ -88,11 +88,14 @@ class KiloSetup extends BaseIdeSetup {
const taskCount = taskToolCounts.tasks || 0;
const toolCount = taskToolCounts.tools || 0;
if (!options.silent) {
await prompts.log.success(
`${this.name} configured: ${addedCount} modes, ${workflowCount} workflows, ${taskCount} tasks, ${toolCount} tools → ${this.configFile}`,
);
}
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${addedCount} modes added`));
console.log(chalk.dim(` - ${workflowCount} workflows exported`));
console.log(chalk.dim(` - ${taskCount} tasks exported`));
console.log(chalk.dim(` - ${toolCount} tools exported`));
console.log(chalk.dim(` - Configuration file: ${this.configFile}`));
console.log(chalk.dim(` - Workflows directory: .kilocode/workflows/`));
console.log(chalk.dim('\n Modes will be available when you open this project in KiloCode'));
return {
success: true,
@ -171,7 +174,7 @@ class KiloSetup extends BaseIdeSetup {
/**
* Cleanup KiloCode configuration
*/
async cleanup(projectDir, options = {}) {
async cleanup(projectDir) {
const fs = require('fs-extra');
const kiloModesPath = path.join(projectDir, this.configFile);
@ -189,12 +192,12 @@ class KiloSetup extends BaseIdeSetup {
if (removedCount > 0) {
await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 }));
if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .kilocodemodes`);
console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .kilocodemodes`));
}
}
} catch {
// If parsing fails, leave file as-is
if (!options.silent) await prompts.log.warn('Warning: Could not parse .kilocodemodes for cleanup');
console.log(chalk.yellow('Warning: Could not parse .kilocodemodes for cleanup'));
}
}

View File

@ -1,7 +1,7 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const fs = require('fs-extra');
const prompts = require('../../../lib/prompts');
const yaml = require('yaml');
/**
@ -18,7 +18,7 @@ class KiroCliSetup extends BaseIdeSetup {
* Cleanup old BMAD installation before reinstalling
* @param {string} projectDir - Project directory
*/
async cleanup(projectDir, options = {}) {
async cleanup(projectDir) {
const bmadAgentsDir = path.join(projectDir, this.configDir, this.agentsDir);
if (await fs.pathExists(bmadAgentsDir)) {
@ -29,7 +29,7 @@ class KiroCliSetup extends BaseIdeSetup {
await fs.remove(path.join(bmadAgentsDir, file));
}
}
if (!options.silent) await prompts.log.message(` Cleaned old BMAD agents from ${this.name}`);
console.log(chalk.dim(` Cleaned old BMAD agents from ${this.name}`));
}
}
@ -40,9 +40,9 @@ class KiroCliSetup extends BaseIdeSetup {
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
console.log(chalk.cyan(`Setting up ${this.name}...`));
await this.cleanup(projectDir, options);
await this.cleanup(projectDir);
const kiroDir = path.join(projectDir, this.configDir);
const agentsDir = path.join(kiroDir, this.agentsDir);
@ -52,7 +52,7 @@ class KiroCliSetup extends BaseIdeSetup {
// Create BMad agents from source YAML files
await this.createBmadAgentsFromSource(agentsDir, projectDir);
if (!options.silent) await prompts.log.success(`${this.name} configured with BMad agents`);
console.log(chalk.green(`${this.name} configured with BMad agents`));
}
/**
@ -70,7 +70,7 @@ class KiroCliSetup extends BaseIdeSetup {
try {
await this.processAgentFile(agentFile, agentsDir, projectDir);
} catch (error) {
await prompts.log.warn(`Failed to process ${agentFile}: ${error.message}`);
console.warn(chalk.yellow(`⚠️ Failed to process ${agentFile}: ${error.message}`));
}
}
}

View File

@ -1,7 +1,7 @@
const fs = require('fs-extra');
const path = require('node:path');
const chalk = require('chalk');
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
const prompts = require('../../../lib/prompts');
/**
* IDE Manager - handles IDE-specific setup
@ -49,7 +49,7 @@ class IdeManager {
*/
async loadHandlers() {
// Load custom installer files
await this.loadCustomInstallerFiles();
this.loadCustomInstallerFiles();
// Load config-driven handlers from platform-codes.yaml
await this.loadConfigDrivenHandlers();
@ -59,7 +59,7 @@ class IdeManager {
* Load custom installer files (unique installation logic)
* These files have special installation patterns that don't fit the config-driven model
*/
async loadCustomInstallerFiles() {
loadCustomInstallerFiles() {
const ideDir = __dirname;
const customFiles = ['codex.js', 'kilo.js', 'kiro-cli.js'];
@ -81,7 +81,7 @@ class IdeManager {
}
}
} catch (error) {
await prompts.log.warn(`Warning: Could not load ${file}: ${error.message}`);
console.log(chalk.yellow(` Warning: Could not load ${file}: ${error.message}`));
}
}
}
@ -171,45 +171,17 @@ class IdeManager {
const handler = this.handlers.get(ideName.toLowerCase());
if (!handler) {
await prompts.log.warn(`IDE '${ideName}' is not yet supported`);
await prompts.log.message(`Supported IDEs: ${[...this.handlers.keys()].join(', ')}`);
return { success: false, ide: ideName, error: 'unsupported IDE' };
console.warn(chalk.yellow(`⚠️ IDE '${ideName}' is not yet supported`));
console.log(chalk.dim('Supported IDEs:', [...this.handlers.keys()].join(', ')));
return { success: false, reason: 'unsupported' };
}
try {
const handlerResult = await handler.setup(projectDir, bmadDir, options);
// Build detail string from handler-returned data
let detail = '';
if (handlerResult && handlerResult.results) {
// Config-driven handlers return { success, results: { agents, workflows, tasks, tools } }
const r = handlerResult.results;
const parts = [];
if (r.agents > 0) parts.push(`${r.agents} agents`);
if (r.workflows > 0) parts.push(`${r.workflows} workflows`);
if (r.tasks > 0) parts.push(`${r.tasks} tasks`);
if (r.tools > 0) parts.push(`${r.tools} tools`);
detail = parts.join(', ');
} else if (handlerResult && handlerResult.counts) {
// Codex handler returns { success, counts: { agents, workflows, tasks }, written }
const c = handlerResult.counts;
const parts = [];
if (c.agents > 0) parts.push(`${c.agents} agents`);
if (c.workflows > 0) parts.push(`${c.workflows} workflows`);
if (c.tasks > 0) parts.push(`${c.tasks} tasks`);
detail = parts.join(', ');
} else if (handlerResult && handlerResult.modes !== undefined) {
// Kilo handler returns { success, modes, workflows, tasks, tools }
const parts = [];
if (handlerResult.modes > 0) parts.push(`${handlerResult.modes} modes`);
if (handlerResult.workflows > 0) parts.push(`${handlerResult.workflows} workflows`);
if (handlerResult.tasks > 0) parts.push(`${handlerResult.tasks} tasks`);
if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`);
detail = parts.join(', ');
}
return { success: true, ide: ideName, detail, handlerResult };
await handler.setup(projectDir, bmadDir, options);
return { success: true, ide: ideName };
} catch (error) {
await prompts.log.error(`Failed to setup ${ideName}: ${error.message}`);
return { success: false, ide: ideName, error: error.message };
console.error(chalk.red(`Failed to setup ${ideName}:`), error.message);
return { success: false, error: error.message };
}
}
@ -282,7 +254,7 @@ class IdeManager {
const handler = this.handlers.get(ideName.toLowerCase());
if (!handler) {
await prompts.log.warn(`IDE '${ideName}' is not yet supported for custom agent installation`);
console.warn(chalk.yellow(`⚠️ IDE '${ideName}' is not yet supported for custom agent installation`));
continue;
}
@ -294,7 +266,7 @@ class IdeManager {
}
}
} catch (error) {
await prompts.log.warn(`Failed to install ${ideName} launcher: ${error.message}`);
console.warn(chalk.yellow(`⚠️ Failed to install ${ideName} launcher: ${error.message}`));
}
}

View File

@ -1,5 +1,6 @@
const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
/**

View File

@ -1,6 +1,7 @@
const path = require('node:path');
const fs = require('fs-extra');
const csv = require('csv-parse/sync');
const chalk = require('chalk');
const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = require('./path-utils');
/**

View File

@ -1,7 +1,7 @@
const path = require('node:path');
const fs = require('fs-extra');
const csv = require('csv-parse/sync');
const prompts = require('../../../../lib/prompts');
const chalk = require('chalk');
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
/**
@ -22,7 +22,7 @@ class WorkflowCommandGenerator {
const workflows = await this.loadWorkflowManifest(bmadDir);
if (!workflows) {
await prompts.log.warn('Workflow manifest not found. Skipping command generation.');
console.log(chalk.yellow('Workflow manifest not found. Skipping command generation.'));
return { generated: 0 };
}
@ -157,7 +157,8 @@ class WorkflowCommandGenerator {
.replaceAll('{{module}}', workflow.module)
.replaceAll('{{description}}', workflow.description)
.replaceAll('{{workflow_path}}', workflowPath)
.replaceAll('_bmad', this.bmadFolderName);
.replaceAll('_bmad', this.bmadFolderName)
.replaceAll('_bmad', '_bmad');
}
/**
@ -237,15 +238,15 @@ When running any workflow:
const match = workflowPath.match(/\/src\/bmm\/(.+)/);
if (match) {
transformed = `{project-root}/${this.bmadFolderName}/bmm/${match[1]}`;
} else if (workflowPath.includes('/src/core/')) {
const match = workflowPath.match(/\/src\/core\/(.+)/);
if (match) {
transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`;
}
}
} else if (workflowPath.includes('/src/core/')) {
const match = workflowPath.match(/\/src\/core\/(.+)/);
if (match) {
transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`;
}
}
return transformed;
return transformed;
}
}
async loadWorkflowManifest(bmadDir) {

View File

@ -1,7 +1,7 @@
const fs = require('fs-extra');
const path = require('node:path');
const yaml = require('yaml');
const prompts = require('../../lib/prompts');
const chalk = require('chalk');
/**
* Load and display installer messages from messages.yaml
@ -51,20 +51,22 @@ class MessageLoader {
/**
* Display the start message (after logo, before prompts)
*/
async displayStartMessage() {
displayStartMessage() {
const message = this.getStartMessage();
if (message) {
await prompts.log.info(message);
console.log(chalk.cyan(message));
console.log();
}
}
/**
* Display the end message (after installation completes)
*/
async displayEndMessage() {
displayEndMessage() {
const message = this.getEndMessage();
if (message) {
await prompts.log.info(message);
console.log();
console.log(chalk.cyan(message));
}
}

View File

@ -1,7 +1,8 @@
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
const chalk = require('chalk');
const ora = require('ora');
const { XmlHandler } = require('../../../lib/xml-handler');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { filterCustomizationData } = require('../../../lib/agent/compiler');
@ -16,7 +17,7 @@ const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
* @class ModuleManager
* @requires fs-extra
* @requires yaml
* @requires prompts
* @requires chalk
* @requires XmlHandler
*
* @example
@ -151,26 +152,26 @@ class ModuleManager {
// File hasn't been modified by user, safe to update
await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true);
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(` Updated sidecar file: ${relativeToBmad}`);
console.log(chalk.dim(` Updated sidecar file: ${relativeToBmad}`));
}
} else {
// User has modified the file, preserve it
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(` Preserving user-modified file: ${relativeToBmad}`);
console.log(chalk.dim(` Preserving user-modified file: ${relativeToBmad}`));
}
}
} else {
// First time seeing this file in manifest, copy it
await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true);
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(` Added new sidecar file: ${relativeToBmad}`);
console.log(chalk.dim(` Added new sidecar file: ${relativeToBmad}`));
}
}
} else {
// New installation
await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true);
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(` Copied sidecar file: ${relativeToBmad}`);
console.log(chalk.dim(` Copied sidecar file: ${relativeToBmad}`));
}
}
@ -287,7 +288,7 @@ class ModuleManager {
moduleInfo.dependencies = config.dependencies || [];
moduleInfo.defaultSelected = config.default_selected === undefined ? false : config.default_selected;
} catch (error) {
await prompts.log.warn(`Failed to read config for ${defaultName}: ${error.message}`);
console.warn(`Failed to read config for ${defaultName}:`, error.message);
}
return moduleInfo;
@ -298,7 +299,7 @@ class ModuleManager {
* @param {string} moduleCode - Code of the module to find (from module.yaml)
* @returns {string|null} Path to the module source or null if not found
*/
async findModuleSource(moduleCode, options = {}) {
async findModuleSource(moduleCode) {
const projectRoot = getProjectRoot();
// First check custom module paths if they exist
@ -315,7 +316,7 @@ class ModuleManager {
}
// Check external official modules
const externalSource = await this.findExternalModuleSource(moduleCode, options);
const externalSource = await this.findExternalModuleSource(moduleCode);
if (externalSource) {
return externalSource;
}
@ -347,7 +348,7 @@ class ModuleManager {
* @param {string} moduleCode - Code of the external module
* @returns {string} Path to the cloned repository
*/
async cloneExternalModule(moduleCode, options = {}) {
async cloneExternalModule(moduleCode) {
const { execSync } = require('node:child_process');
const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode);
@ -357,32 +358,10 @@ class ModuleManager {
const cacheDir = this.getExternalCacheDir();
const moduleCacheDir = path.join(cacheDir, moduleCode);
const silent = options.silent || false;
// Create cache directory if it doesn't exist
await fs.ensureDir(cacheDir);
// Helper to create a spinner or a no-op when silent
const createSpinner = async () => {
if (silent) {
return {
start() {},
stop() {},
error() {},
message() {},
cancel() {},
clear() {},
get isSpinning() {
return false;
},
get isCancelled() {
return false;
},
};
}
return await prompts.spinner();
};
// Track if we need to install dependencies
let needsDependencyInstall = false;
let wasNewClone = false;
@ -390,30 +369,21 @@ class ModuleManager {
// Check if already cloned
if (await fs.pathExists(moduleCacheDir)) {
// Try to update if it's a git repo
const fetchSpinner = await createSpinner();
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start();
try {
const currentRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
// Fetch and reset to remote - works better with shallow clones than pull
execSync('git fetch origin --depth 1', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
execSync('git reset --hard origin/HEAD', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
execSync('git fetch origin --depth 1', { cwd: moduleCacheDir, stdio: 'pipe' });
execSync('git reset --hard origin/HEAD', { cwd: moduleCacheDir, stdio: 'pipe' });
const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
fetchSpinner.stop(`Fetched ${moduleInfo.name}`);
fetchSpinner.succeed(`Fetched ${moduleInfo.name}`);
// Force dependency install if we got new code
if (currentRef !== newRef) {
needsDependencyInstall = true;
}
} catch {
fetchSpinner.error(`Fetch failed, re-downloading ${moduleInfo.name}`);
fetchSpinner.warn(`Fetch failed, re-downloading ${moduleInfo.name}`);
// If update fails, remove and re-clone
await fs.remove(moduleCacheDir);
wasNewClone = true;
@ -424,16 +394,14 @@ class ModuleManager {
// Clone if not exists or was removed
if (wasNewClone) {
const fetchSpinner = await createSpinner();
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start();
try {
execSync(`git clone --depth 1 "${moduleInfo.url}" "${moduleCacheDir}"`, {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
stdio: 'pipe',
});
fetchSpinner.stop(`Fetched ${moduleInfo.name}`);
fetchSpinner.succeed(`Fetched ${moduleInfo.name}`);
} catch (error) {
fetchSpinner.error(`Failed to fetch ${moduleInfo.name}`);
fetchSpinner.fail(`Failed to fetch ${moduleInfo.name}`);
throw new Error(`Failed to clone external module '${moduleCode}': ${error.message}`);
}
}
@ -447,18 +415,17 @@ class ModuleManager {
// Force install if we updated or cloned new
if (needsDependencyInstall || wasNewClone || nodeModulesMissing) {
const installSpinner = await createSpinner();
installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`);
const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start();
try {
execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
stdio: 'pipe',
timeout: 120_000, // 2 minute timeout
});
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
installSpinner.succeed(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) {
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` Warning: ${error.message}`);
installSpinner.warn(`Failed to install dependencies for ${moduleInfo.name}`);
console.warn(chalk.yellow(` Warning: ${error.message}`));
}
} else {
// Check if package.json is newer than node_modules
@ -473,18 +440,17 @@ class ModuleManager {
}
if (packageJsonNewer) {
const installSpinner = await createSpinner();
installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`);
const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start();
try {
execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
stdio: 'pipe',
timeout: 120_000, // 2 minute timeout
});
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
installSpinner.succeed(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) {
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` Warning: ${error.message}`);
installSpinner.warn(`Failed to install dependencies for ${moduleInfo.name}`);
console.warn(chalk.yellow(` Warning: ${error.message}`));
}
}
}
@ -498,7 +464,7 @@ class ModuleManager {
* @param {string} moduleCode - Code of the external module
* @returns {string|null} Path to the module source or null if not found
*/
async findExternalModuleSource(moduleCode, options = {}) {
async findExternalModuleSource(moduleCode) {
const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode);
if (!moduleInfo) {
@ -506,7 +472,7 @@ class ModuleManager {
}
// Clone the external module repo
const cloneDir = await this.cloneExternalModule(moduleCode, options);
const cloneDir = await this.cloneExternalModule(moduleCode);
// The module-definition specifies the path to module.yaml relative to repo root
// We need to return the directory containing module.yaml
@ -527,7 +493,7 @@ class ModuleManager {
* @param {Object} options.logger - Logger instance for output
*/
async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) {
const sourcePath = await this.findModuleSource(moduleName, { silent: options.silent });
const sourcePath = await this.findModuleSource(moduleName);
const targetPath = path.join(bmadDir, moduleName);
// Check if source module exists
@ -548,14 +514,14 @@ class ModuleManager {
const customContent = await fs.readFile(rootCustomConfigPath, 'utf8');
customConfig = yaml.parse(customContent);
} catch (error) {
await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`);
console.warn(chalk.yellow(`Warning: Failed to read custom.yaml for ${moduleName}:`, error.message));
}
} else if (await fs.pathExists(moduleInstallerCustomPath)) {
try {
const customContent = await fs.readFile(moduleInstallerCustomPath, 'utf8');
customConfig = yaml.parse(customContent);
} catch (error) {
await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`);
console.warn(chalk.yellow(`Warning: Failed to read custom.yaml for ${moduleName}:`, error.message));
}
}
@ -563,7 +529,7 @@ class ModuleManager {
if (customConfig) {
options.moduleConfig = { ...options.moduleConfig, ...customConfig };
if (options.logger) {
options.logger.log(` Merged custom configuration for ${moduleName}`);
options.logger.log(chalk.cyan(` Merged custom configuration for ${moduleName}`));
}
}
@ -616,7 +582,7 @@ class ModuleManager {
* @param {string} bmadDir - Target bmad directory
* @param {boolean} force - Force update (overwrite modifications)
*/
async update(moduleName, bmadDir, force = false, options = {}) {
async update(moduleName, bmadDir, force = false) {
const sourcePath = await this.findModuleSource(moduleName);
const targetPath = path.join(bmadDir, moduleName);
@ -633,7 +599,7 @@ class ModuleManager {
if (force) {
// Force update - remove and reinstall
await fs.remove(targetPath);
return await this.install(moduleName, bmadDir, null, { installer: options.installer });
return await this.install(moduleName, bmadDir);
} else {
// Selective update - preserve user modifications
await this.syncModule(sourcePath, targetPath);
@ -707,7 +673,7 @@ class ModuleManager {
const config = yaml.parse(configContent);
Object.assign(moduleInfo, config);
} catch (error) {
await prompts.log.warn(`Failed to read installed module config: ${error.message}`);
console.warn(`Failed to read installed module config:`, error.message);
}
}
@ -769,7 +735,7 @@ class ModuleManager {
// Check for localskip="true" in the agent tag
const agentMatch = content.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
if (agentMatch) {
await prompts.log.message(` Skipping web-only agent: ${path.basename(file)}`);
console.log(chalk.dim(` Skipping web-only agent: ${path.basename(file)}`));
continue; // Skip this agent
}
}
@ -802,6 +768,7 @@ class ModuleManager {
// IMPORTANT: Replace escape sequence and placeholder BEFORE parsing YAML
// Otherwise parsing will fail on the placeholder
yamlContent = yamlContent.replaceAll('_bmad', '_bmad');
yamlContent = yamlContent.replaceAll('_bmad', this.bmadFolderName);
try {
@ -871,7 +838,7 @@ class ModuleManager {
await fs.writeFile(targetFile, strippedYaml, 'utf8');
} catch {
// If anything fails, just copy the file as-is
await prompts.log.warn(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`);
console.warn(chalk.yellow(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`));
await fs.copy(sourceFile, targetFile, { overwrite: true });
}
}
@ -923,7 +890,7 @@ class ModuleManager {
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath);
// Only show customize creation in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(` Created customize: ${moduleName}-${agentName}.customize.yaml`);
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
}
// Store original hash for modification detection
@ -1023,10 +990,10 @@ class ModuleManager {
const copiedFiles = await this.copySidecarToMemory(sourceSidecarPath, agentName, bmadMemoryPath, isUpdate, bmadDir, installer);
if (process.env.BMAD_VERBOSE_INSTALL === 'true' && copiedFiles.length > 0) {
await prompts.log.message(` Sidecar files processed: ${copiedFiles.length} files`);
console.log(chalk.dim(` Sidecar files processed: ${copiedFiles.length} files`));
}
} else if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.warn(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`);
console.log(chalk.yellow(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`));
}
}
@ -1045,12 +1012,14 @@ class ModuleManager {
// Only show compilation details in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(
` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
console.log(
chalk.dim(
` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
),
);
}
} catch (error) {
await prompts.log.warn(` Failed to compile agent ${agentName}: ${error.message}`);
console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
}
}
}
@ -1170,11 +1139,11 @@ class ModuleManager {
}
if (!workflowsVendored) {
await prompts.log.info(`\n Vendoring cross-module workflows for ${moduleName}...`);
console.log(chalk.cyan(`\n Vendoring cross-module workflows for ${moduleName}...`));
workflowsVendored = true;
}
await prompts.log.message(` Processing: ${agentFile}`);
console.log(chalk.dim(` Processing: ${agentFile}`));
for (const item of workflowInstallItems) {
const sourceWorkflowPath = item.workflow; // Where to copy FROM
@ -1186,7 +1155,7 @@ class ModuleManager {
// Or: {project-root}/bmad/bmm/workflows/4-implementation/create-story/workflow.yaml
const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:_bmad)\/([^/]+)\/workflows\/(.+)/);
if (!sourceMatch) {
await prompts.log.warn(` Could not parse workflow path: ${sourceWorkflowPath}`);
console.warn(chalk.yellow(` Could not parse workflow path: ${sourceWorkflowPath}`));
continue;
}
@ -1197,7 +1166,7 @@ class ModuleManager {
// Example: {project-root}/_bmad/bmgd/workflows/4-production/create-story/workflow.yaml
const installMatch = installWorkflowPath.match(/\{project-root\}\/(_bmad)\/([^/]+)\/workflows\/(.+)/);
if (!installMatch) {
await prompts.log.warn(` Could not parse workflow-install path: ${installWorkflowPath}`);
console.warn(chalk.yellow(` Could not parse workflow-install path: ${installWorkflowPath}`));
continue;
}
@ -1210,13 +1179,15 @@ class ModuleManager {
// Check if source workflow exists
if (!(await fs.pathExists(actualSourceWorkflowPath))) {
await prompts.log.warn(` Source workflow not found: ${actualSourceWorkflowPath}`);
console.warn(chalk.yellow(` Source workflow not found: ${actualSourceWorkflowPath}`));
continue;
}
// Copy the entire workflow folder
await prompts.log.message(
` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}`,
console.log(
chalk.dim(
` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}`,
),
);
await fs.ensureDir(path.dirname(actualDestWorkflowPath));
@ -1232,7 +1203,7 @@ class ModuleManager {
}
if (workflowsVendored) {
await prompts.log.success(` Workflow vendoring complete\n`);
console.log(chalk.green(` ✓ Workflow vendoring complete\n`));
}
}
@ -1254,7 +1225,7 @@ class ModuleManager {
if (updatedYaml !== yamlContent) {
await fs.writeFile(workflowYamlPath, updatedYaml, 'utf8');
await prompts.log.message(` Updated config_source to: ${this.bmadFolderName}/${newModuleName}/config.yaml`);
console.log(chalk.dim(` Updated config_source to: ${this.bmadFolderName}/${newModuleName}/config.yaml`));
}
}
@ -1270,7 +1241,7 @@ class ModuleManager {
if (moduleName === 'core') {
sourcePath = getSourcePath('core');
} else {
sourcePath = await this.findModuleSource(moduleName, { silent: options.silent });
sourcePath = await this.findModuleSource(moduleName);
if (!sourcePath) {
// No source found, skip module installer
return;
@ -1309,11 +1280,11 @@ class ModuleManager {
});
if (!result) {
await prompts.log.warn(`Module installer for ${moduleName} returned false`);
console.warn(chalk.yellow(`Module installer for ${moduleName} returned false`));
}
}
} catch (error) {
await prompts.log.error(`Error running module installer for ${moduleName}: ${error.message}`);
console.error(chalk.red(`Error running module installer for ${moduleName}: ${error.message}`));
}
}
@ -1335,7 +1306,7 @@ class ModuleManager {
await fs.writeFile(configPath, configContent, 'utf8');
} catch (error) {
await prompts.log.warn(`Failed to process module config: ${error.message}`);
console.warn(`Failed to process module config:`, error.message);
}
}
}

View File

@ -6,7 +6,7 @@
const fs = require('node:fs');
const path = require('node:path');
const yaml = require('yaml');
const prompts = require('../prompts');
const readline = require('node:readline');
const { compileAgent, compileAgentFile } = require('./compiler');
const { extractInstallConfig, getDefaultValues } = require('./template-engine');
@ -149,47 +149,83 @@ async function promptInstallQuestions(installConfig, defaults, presetAnswers = {
return { ...defaults, ...presetAnswers };
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const question = (prompt) =>
new Promise((resolve) => {
rl.question(prompt, resolve);
});
const answers = { ...defaults, ...presetAnswers };
await prompts.note(installConfig.description || '', 'Agent Configuration');
console.log('\n📝 Agent Configuration\n');
if (installConfig.description) {
console.log(` ${installConfig.description}\n`);
}
for (const q of installConfig.questions) {
// Skip questions for variables that are already set (e.g., custom_name set upfront)
if (answers[q.var] !== undefined && answers[q.var] !== defaults[q.var]) {
await prompts.log.message(` ${q.var}: ${answers[q.var]} (already set)`);
console.log(chalk.dim(` ${q.var}: ${answers[q.var]} (already set)`));
continue;
}
let response;
switch (q.type) {
case 'text': {
const response = await prompts.text({
message: q.prompt,
default: q.default ?? '',
});
answers[q.var] = response ?? q.default ?? '';
const defaultHint = q.default ? ` (default: ${q.default})` : '';
response = await question(` ${q.prompt}${defaultHint}: `);
answers[q.var] = response || q.default || '';
break;
}
case 'boolean': {
const response = await prompts.confirm({
message: q.prompt,
default: q.default,
});
answers[q.var] = response;
const defaultHint = q.default ? ' [Y/n]' : ' [y/N]';
response = await question(` ${q.prompt}${defaultHint}: `);
if (response === '') {
answers[q.var] = q.default;
} else {
answers[q.var] = response.toLowerCase().startsWith('y');
}
break;
}
case 'choice': {
const response = await prompts.select({
message: q.prompt,
options: q.options.map((o) => ({ value: o.value, label: o.label })),
initialValue: q.default,
});
answers[q.var] = response;
console.log(` ${q.prompt}`);
for (const [idx, opt] of q.options.entries()) {
const marker = opt.value === q.default ? '* ' : ' ';
console.log(` ${marker}${idx + 1}. ${opt.label}`);
}
const defaultIdx = q.options.findIndex((o) => o.value === q.default) + 1;
let validChoice = false;
let choiceIdx;
while (!validChoice) {
response = await question(` Choice (default: ${defaultIdx}): `);
if (response) {
choiceIdx = parseInt(response, 10) - 1;
if (isNaN(choiceIdx) || choiceIdx < 0 || choiceIdx >= q.options.length) {
console.log(` Invalid choice. Please enter 1-${q.options.length}`);
} else {
validChoice = true;
}
} else {
choiceIdx = defaultIdx - 1;
validChoice = true;
}
}
answers[q.var] = q.options[choiceIdx].value;
break;
}
// No default
}
}
rl.close();
return answers;
}

View File

@ -1,6 +1,9 @@
const chalk = require('chalk');
const boxen = require('boxen');
const wrapAnsi = require('wrap-ansi');
const figlet = require('figlet');
const path = require('node:path');
const os = require('node:os');
const prompts = require('./prompts');
const CLIUtils = {
/**
@ -16,32 +19,27 @@ const CLIUtils = {
},
/**
* Display BMAD logo using @clack intro + box
* @param {boolean} _clearScreen - Deprecated, ignored (no longer clears screen)
* Display BMAD logo
* @param {boolean} clearScreen - Whether to clear the screen first (default: true for initial display only)
*/
async displayLogo(_clearScreen = true) {
displayLogo(clearScreen = true) {
if (clearScreen) {
console.clear();
}
const version = this.getVersion();
const color = await prompts.getColor();
// ASCII art logo
const logo = [
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™',
' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗',
' ██████╔╝██╔████╔██║███████║██║ ██║',
' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║',
' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝',
' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝',
]
.map((line) => color.yellow(line))
.join('\n');
const logo = `
`;
const tagline = ' Build More, Architect Dreams';
await prompts.box(`${logo}\n${tagline}`, `v${version}`, {
contentAlign: 'center',
rounded: true,
formatBorder: color.blue,
});
console.log(chalk.cyan(logo));
console.log(chalk.dim(` Build More, Architect Dreams`) + chalk.cyan.bold(` v${version}`) + '\n');
},
/**
@ -49,8 +47,13 @@ const CLIUtils = {
* @param {string} title - Section title
* @param {string} subtitle - Optional subtitle
*/
async displaySection(title, subtitle = null) {
await prompts.note(subtitle || '', title);
displaySection(title, subtitle = null) {
console.log('\n' + chalk.cyan('═'.repeat(80)));
console.log(chalk.cyan.bold(` ${title}`));
if (subtitle) {
console.log(chalk.dim(` ${subtitle}`));
}
console.log(chalk.cyan('═'.repeat(80)) + '\n');
},
/**
@ -58,21 +61,25 @@ const CLIUtils = {
* @param {string|Array} content - Content to display
* @param {Object} options - Box options
*/
async displayBox(content, options = {}) {
displayBox(content, options = {}) {
const defaultOptions = {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'cyan',
...options,
};
// Handle array content
let text = content;
if (Array.isArray(content)) {
text = content.join('\n\n');
}
const color = await prompts.getColor();
const borderColor = options.borderColor || 'cyan';
const colorMap = { green: color.green, red: color.red, yellow: color.yellow, cyan: color.cyan, blue: color.blue };
const formatBorder = colorMap[borderColor] || color.cyan;
// Wrap text to prevent overflow
const wrapped = wrapAnsi(text, 76, { hard: true, wordWrap: true });
await prompts.box(text, options.title, {
rounded: options.borderStyle === 'round' || options.borderStyle === undefined,
formatBorder,
});
console.log(boxen(wrapped, defaultOptions));
},
/**
@ -81,9 +88,14 @@ const CLIUtils = {
* @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from module.yaml
*/
async displayModuleConfigHeader(moduleName, header = null, subheader = null) {
const title = header || `Configuring ${moduleName.toUpperCase()} Module`;
await prompts.note(subheader || '', title);
displayModuleConfigHeader(moduleName, header = null, subheader = null) {
// Simple blue banner with custom header/subheader if provided
console.log('\n' + chalk.cyan('─'.repeat(80)));
console.log(chalk.cyan(header || `Configuring ${moduleName.toUpperCase()} Module`));
if (subheader) {
console.log(chalk.dim(`${subheader}`));
}
console.log(chalk.cyan('─'.repeat(80)) + '\n');
},
/**
@ -92,9 +104,14 @@ const CLIUtils = {
* @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from module.yaml
*/
async displayModuleNoConfig(moduleName, header = null, subheader = null) {
const title = header || `${moduleName.toUpperCase()} Module - No Custom Configuration`;
await prompts.note(subheader || '', title);
displayModuleNoConfig(moduleName, header = null, subheader = null) {
// Show full banner with header/subheader, just like modules with config
console.log('\n' + chalk.cyan('─'.repeat(80)));
console.log(chalk.cyan(header || `${moduleName.toUpperCase()} Module - No Custom Configuration`));
if (subheader) {
console.log(chalk.dim(`${subheader}`));
}
console.log(chalk.cyan('─'.repeat(80)) + '\n');
},
/**
@ -103,33 +120,42 @@ const CLIUtils = {
* @param {number} total - Total steps
* @param {string} description - Step description
*/
async displayStep(current, total, description) {
displayStep(current, total, description) {
const progress = `[${current}/${total}]`;
await prompts.log.step(`${progress} ${description}`);
console.log('\n' + chalk.cyan(progress) + ' ' + chalk.bold(description));
console.log(chalk.dim('─'.repeat(80 - progress.length - 1)) + '\n');
},
/**
* Display completion message
* @param {string} message - Completion message
*/
async displayComplete(message) {
const color = await prompts.getColor();
await prompts.box(`\u2728 ${message}`, 'Complete', {
rounded: true,
formatBorder: color.green,
});
displayComplete(message) {
console.log(
'\n' +
boxen(chalk.green('✨ ' + message), {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'green',
}),
);
},
/**
* Display error message
* @param {string} message - Error message
*/
async displayError(message) {
const color = await prompts.getColor();
await prompts.box(`\u2717 ${message}`, 'Error', {
rounded: true,
formatBorder: color.red,
});
displayError(message) {
console.log(
'\n' +
boxen(chalk.red('✗ ' + message), {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'red',
}),
);
},
/**
@ -137,7 +163,7 @@ const CLIUtils = {
* @param {Array} items - Items to display
* @param {string} prefix - Item prefix
*/
formatList(items, prefix = '\u2022') {
formatList(items, prefix = '') {
return items.map((item) => ` ${prefix} ${item}`).join('\n');
},
@ -152,6 +178,25 @@ const CLIUtils = {
}
},
/**
* Display table
* @param {Array} data - Table data
* @param {Object} options - Table options
*/
displayTable(data, options = {}) {
const Table = require('cli-table3');
const table = new Table({
style: {
head: ['cyan'],
border: ['dim'],
},
...options,
});
for (const row of data) table.push(row);
console.log(table.toString());
},
/**
* Display module completion message
* @param {string} moduleName - Name of the completed module

View File

@ -89,51 +89,11 @@ async function note(message, title) {
/**
* Display a spinner for async operations
* Wraps @clack/prompts spinner with isSpinning state tracking
* @returns {Object} Spinner controller with start, stop, message, error, cancel, clear, isSpinning
* @returns {Object} Spinner controller with start, stop, message methods
*/
async function spinner() {
const clack = await getClack();
const s = clack.spinner();
let spinning = false;
return {
start: (msg) => {
if (spinning) {
s.message(msg);
} else {
spinning = true;
s.start(msg);
}
},
stop: (msg) => {
if (spinning) {
spinning = false;
s.stop(msg);
}
},
message: (msg) => {
if (spinning) s.message(msg);
},
error: (msg) => {
spinning = false;
s.error(msg);
},
cancel: (msg) => {
spinning = false;
s.cancel(msg);
},
clear: () => {
spinning = false;
s.clear();
},
get isSpinning() {
return spinning;
},
get isCancelled() {
return s.isCancelled;
},
};
return clack.spinner();
}
/**
@ -230,6 +190,31 @@ async function multiselect(options) {
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,
selectableGroups: options.selectableGroups || false,
});
await handleCancel(result);
return result;
}
/**
* Default filter function for autocomplete - case-insensitive label matching
* @param {string} search - Search string
@ -252,7 +237,6 @@ function defaultAutocompleteFilter(search, option) {
* @param {boolean} [options.required=false] - Whether at least one must be selected
* @param {number} [options.maxItems=5] - Maximum visible items in scrollable list
* @param {Function} [options.filter] - Custom filter function (search, option) => boolean
* @param {Array} [options.lockedValues] - Values that are always selected and cannot be toggled off
* @returns {Promise<Array>} Array of selected values
*/
async function autocompleteMultiselect(options) {
@ -261,7 +245,6 @@ async function autocompleteMultiselect(options) {
const color = await getPicocolors();
const filterFn = options.filter ?? defaultAutocompleteFilter;
const lockedSet = new Set(options.lockedValues || []);
const prompt = new core.AutocompletePrompt({
options: options.options,
@ -272,7 +255,7 @@ async function autocompleteMultiselect(options) {
return 'Please select at least one item';
}
},
initialValue: [...new Set([...(options.initialValues || []), ...(options.lockedValues || [])])],
initialValue: options.initialValues,
render() {
const barColor = this.state === 'error' ? color.yellow : color.cyan;
const bar = barColor(clack.S_BAR);
@ -297,17 +280,9 @@ async function autocompleteMultiselect(options) {
// Render option with checkbox
const renderOption = (opt, isHighlighted) => {
const isSelected = this.selectedValues.includes(opt.value);
const isLocked = lockedSet.has(opt.value);
const label = opt.label ?? String(opt.value ?? '');
const hintText = opt.hint && isHighlighted ? color.dim(` (${opt.hint})`) : '';
let checkbox;
if (isLocked) {
checkbox = color.green(clack.S_CHECKBOX_SELECTED);
const lockHint = color.dim(' (always installed)');
return isHighlighted ? `${checkbox} ${label}${lockHint}` : `${checkbox} ${color.dim(label)}${lockHint}`;
}
checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE);
const hintText = opt.hint && opt.value === this.focusedValue ? color.dim(` (${opt.hint})`) : '';
const checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE);
return isHighlighted ? `${checkbox} ${label}${hintText}` : `${checkbox} ${color.dim(label)}`;
};
@ -347,18 +322,6 @@ async function autocompleteMultiselect(options) {
},
});
// Prevent locked values from being toggled off
if (lockedSet.size > 0) {
const originalToggle = prompt.toggleSelected.bind(prompt);
prompt.toggleSelected = function (value) {
// If locked and already selected, skip the toggle (would deselect)
if (lockedSet.has(value) && this.selectedValues.includes(value)) {
return;
}
originalToggle(value);
};
}
// === FIX: Make SPACE always act as selection key (not search input) ===
// Override _isActionKey to treat SPACE like TAB - always an action key
// This prevents SPACE from being added to the search input
@ -372,9 +335,8 @@ async function autocompleteMultiselect(options) {
// Handle SPACE toggle when NOT navigating (internal code only handles it when isNavigating=true)
prompt.on('key', (char, key) => {
if (key && key.name === 'space' && !prompt.isNavigating) {
const focused = prompt.filteredOptions[prompt.cursor];
if (focused) prompt.toggleSelected(focused.value);
if (key && key.name === 'space' && !prompt.isNavigating && prompt.focusedValue !== undefined) {
prompt.toggleSelected(prompt.focusedValue);
}
});
// === END FIX ===
@ -558,131 +520,6 @@ const log = {
},
};
/**
* Display cancellation message
* @param {string} [message='Operation cancelled'] - The cancellation message
*/
async function cancel(message = 'Operation cancelled') {
const clack = await getClack();
clack.cancel(message);
}
/**
* Display content in a styled box
* @param {string} content - The box content
* @param {string} [title] - Optional title
* @param {Object} [options] - Box options (contentAlign, titleAlign, width, rounded, formatBorder, etc.)
*/
async function box(content, title, options) {
const clack = await getClack();
clack.box(content, title, options);
}
/**
* Create a progress bar for visualizing task completion
* @param {Object} [options] - Progress options (max, style, etc.)
* @returns {Promise<Object>} Progress controller with start, advance, stop methods
*/
async function progress(options) {
const clack = await getClack();
return clack.progress(options);
}
/**
* Create a task log for displaying scrolling subprocess output
* @param {Object} options - TaskLog options (title, limit, retainLog)
* @returns {Promise<Object>} TaskLog controller with message, success, error methods
*/
async function taskLog(options) {
const clack = await getClack();
return clack.taskLog(options);
}
/**
* File system path prompt with autocomplete
* @param {Object} options - Path options
* @param {string} options.message - The prompt message
* @param {string} [options.initialValue] - Initial path value
* @param {boolean} [options.directory=false] - Only allow directories
* @param {Function} [options.validate] - Validation function
* @returns {Promise<string>} Selected path
*/
async function pathPrompt(options) {
const clack = await getClack();
const result = await clack.path(options);
await handleCancel(result);
return result;
}
/**
* Autocomplete single-select prompt with type-ahead filtering
* @param {Object} options - Autocomplete options
* @param {string} options.message - The prompt message
* @param {Array} options.options - Array of choices [{value, label, hint?}]
* @param {string} [options.placeholder] - Placeholder text
* @param {number} [options.maxItems] - Maximum visible items
* @param {Function} [options.filter] - Custom filter function
* @returns {Promise<any>} Selected value
*/
async function autocomplete(options) {
const clack = await getClack();
const result = await clack.autocomplete(options);
await handleCancel(result);
return result;
}
/**
* Key-based instant selection prompt
* @param {Object} options - SelectKey options
* @param {string} options.message - The prompt message
* @param {Array} options.options - Array of choices [{value, label, hint?}]
* @returns {Promise<any>} Selected value
*/
async function selectKey(options) {
const clack = await getClack();
const result = await clack.selectKey(options);
await handleCancel(result);
return result;
}
/**
* Stream messages with dynamic content (for LLMs, generators, etc.)
*/
const stream = {
async info(generator) {
const clack = await getClack();
return clack.stream.info(generator);
},
async success(generator) {
const clack = await getClack();
return clack.stream.success(generator);
},
async step(generator) {
const clack = await getClack();
return clack.stream.step(generator);
},
async warn(generator) {
const clack = await getClack();
return clack.stream.warn(generator);
},
async error(generator) {
const clack = await getClack();
return clack.stream.error(generator);
},
async message(generator, options) {
const clack = await getClack();
return clack.stream.message(generator, options);
},
};
/**
* Get the color utility (picocolors instance from @clack/prompts)
* @returns {Promise<Object>} The color utility (picocolors)
*/
async function getColor() {
return await getPicocolors();
}
/**
* Execute an array of Inquirer-style questions using @clack/prompts
* This provides compatibility with dynamic question arrays
@ -782,28 +619,20 @@ async function prompt(questions) {
module.exports = {
getClack,
getColor,
handleCancel,
intro,
outro,
cancel,
note,
box,
spinner,
progress,
taskLog,
select,
multiselect,
groupMultiselect,
autocompleteMultiselect,
autocomplete,
selectKey,
confirm,
text,
path: pathPrompt,
password,
group,
tasks,
log,
stream,
prompt,
};

View File

@ -1,3 +1,4 @@
const chalk = require('chalk');
const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra');
@ -29,12 +30,12 @@ class UI {
* @returns {Object} Installation configuration
*/
async promptInstall(options = {}) {
await CLIUtils.displayLogo();
CLIUtils.displayLogo();
// Display version-specific start message from install-messages.yaml
const { MessageLoader } = require('../installers/lib/message-loader');
const messageLoader = new MessageLoader();
await messageLoader.displayStartMessage();
messageLoader.displayStartMessage();
// Get directory from options or prompt
let confirmedDirectory;
@ -46,7 +47,7 @@ class UI {
throw new Error(`Invalid directory: ${validation}`);
}
confirmedDirectory = expandedDir;
await prompts.log.info(`Using directory from command-line: ${confirmedDirectory}`);
console.log(chalk.cyan('Using directory from command-line:'), chalk.bold(confirmedDirectory));
} else {
confirmedDirectory = await this.getConfirmedDirectory();
}
@ -74,7 +75,7 @@ class UI {
for (const entry of entries) {
if (entry.isDirectory() && (entry.name === '.bmad' || entry.name === 'bmad')) {
hasLegacyBmadFolder = true;
legacyBmadPath = path.join(confirmedDirectory, entry.name);
legacyBmadPath = path.join(confirmedDirectory, '.bmad');
bmadDir = legacyBmadPath;
// Check if it has _cfg folder
@ -97,30 +98,38 @@ class UI {
// Handle legacy .bmad or _cfg folder - these are very old (v4 or alpha)
// Show version warning instead of offering conversion
if (hasLegacyBmadFolder || hasLegacyCfg) {
await prompts.log.warn('LEGACY INSTALLATION DETECTED');
await prompts.note(
'Found a ".bmad"/"bmad" folder, or a legacy "_cfg" folder under the bmad folder -\n' +
'this is from an old BMAD version that is out of date for automatic upgrade,\n' +
'manual intervention required.\n\n' +
'You have a legacy version installed (v4 or alpha).\n' +
'Legacy installations may have compatibility issues.\n\n' +
'For the best experience, we strongly recommend:\n' +
' 1. Delete your current BMAD installation folder (.bmad or bmad)\n' +
' 2. Run a fresh installation\n\n' +
'If you do not want to start fresh, you can attempt to proceed beyond this\n' +
'point IF you have ensured the bmad folder is named _bmad, and under it there\n' +
'is a _config folder. If you have a folder under your bmad folder named _cfg,\n' +
'you would need to rename it _config, and then restart the installer.\n\n' +
'Benefits of a fresh install:\n' +
' \u2022 Cleaner configuration without legacy artifacts\n' +
' \u2022 All new features properly configured\n' +
' \u2022 Fewer potential conflicts\n\n' +
'If you have already produced output from an earlier alpha version, you can\n' +
'still retain those artifacts. After installation, ensure you configured during\n' +
'install the proper file locations for artifacts depending on the module you\n' +
'are using, or move the files to the proper locations.',
'Legacy Installation Detected',
console.log('');
console.log(chalk.yellow.bold('⚠️ LEGACY INSTALLATION DETECTED'));
console.log(chalk.yellow('─'.repeat(80)));
console.log(
chalk.yellow(
'Found a ".bmad"/"bmad" folder, or a legacy "_cfg" folder under the bmad folder - this is from a old BMAD version that is out of date for automatic upgrade, manual intervention required.',
),
);
console.log(chalk.yellow('You have a legacy version installed (v4 or alpha).'));
console.log('');
console.log(chalk.dim('Legacy installations may have compatibility issues.'));
console.log('');
console.log(chalk.dim('For the best experience, we strongly recommend:'));
console.log(chalk.dim(' 1. Delete your current BMAD installation folder (.bmad or bmad)'));
console.log(
chalk.dim(
' 2. Run a fresh installation\n\nIf you do not want to start fresh, you can attempt to proceed beyond this point IF you have ensured the bmad folder is named _bmad, and under it there is a _config folder. If you have a folder under your bmad folder named _cfg, you would need to rename it _config, and then restart the installer.',
),
);
console.log('');
console.log(chalk.dim('Benefits of a fresh install:'));
console.log(chalk.dim(' • Cleaner configuration without legacy artifacts'));
console.log(chalk.dim(' • All new features properly configured'));
console.log(chalk.dim(' • Fewer potential conflicts'));
console.log(chalk.dim(''));
console.log(
chalk.dim(
'If you have already produced output from an earlier alpha version, you can still retain those artifacts. After installation, ensure you configured during install the proper file locations for artifacts depending on the module you are using, or move the files to the proper locations.',
),
);
console.log(chalk.yellow('─'.repeat(80)));
console.log('');
const proceed = await prompts.select({
message: 'How would you like to proceed?',
@ -138,33 +147,37 @@ class UI {
});
if (proceed === 'cancel') {
await prompts.note('1. Delete the existing bmad folder in your project\n' + "2. Run 'bmad install' again", 'To do a fresh install');
console.log('');
console.log(chalk.cyan('To do a fresh install:'));
console.log(chalk.dim(' 1. Delete the existing bmad folder in your project'));
console.log(chalk.dim(" 2. Run 'bmad install' again"));
console.log('');
process.exit(0);
return;
}
const s = await prompts.spinner();
s.start('Updating folder structure...');
const ora = require('ora');
const spinner = ora('Updating folder structure...').start();
try {
// Handle .bmad folder
if (hasLegacyBmadFolder) {
const newBmadPath = path.join(confirmedDirectory, '_bmad');
await fs.move(legacyBmadPath, newBmadPath);
bmadDir = newBmadPath;
s.stop(`Renamed "${path.basename(legacyBmadPath)}" to "_bmad"`);
spinner.succeed('Renamed ".bmad" to "_bmad"');
}
// Handle _cfg folder (either from .bmad or standalone)
const cfgPath = path.join(bmadDir, '_cfg');
if (await fs.pathExists(cfgPath)) {
s.start('Renaming configuration folder...');
spinner.start('Renaming configuration folder...');
const newCfgPath = path.join(bmadDir, '_config');
await fs.move(cfgPath, newCfgPath);
s.stop('Renamed "_cfg" to "_config"');
spinner.succeed('Renamed "_cfg" to "_config"');
}
} catch (error) {
s.stop('Failed to update folder structure');
await prompts.log.error(`Error: ${error.message}`);
spinner.fail('Failed to update folder structure');
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
}
@ -226,7 +239,7 @@ class UI {
throw new Error(`Invalid action: ${options.action}. Valid actions: ${validActions.join(', ')}`);
}
actionType = options.action;
await prompts.log.info(`Using action from command-line: ${actionType}`);
console.log(chalk.cyan('Using action from command-line:'), chalk.bold(actionType));
} else {
actionType = await prompts.select({
message: 'How would you like to proceed?',
@ -261,7 +274,7 @@ class UI {
// Get existing installation info
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
await prompts.log.message(`Found existing modules: ${[...installedModuleIds].join(', ')}`);
console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`));
// Unified module selection - all modules in one grouped multiselect
let selectedModules;
@ -271,13 +284,13 @@ class UI {
.split(',')
.map((m) => m.trim())
.filter(Boolean);
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
console.log(chalk.cyan('Using modules from command-line:'), chalk.bold(selectedModules.join(', ')));
} else {
selectedModules = await this.selectAllModules(installedModuleIds);
selectedModules = selectedModules.filter((m) => m !== 'core');
}
// After module selection, ask about custom modules
console.log('');
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
if (options.customContent) {
@ -286,7 +299,7 @@ class UI {
.split(',')
.map((p) => p.trim())
.filter(Boolean);
await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
console.log(chalk.cyan('Using custom content from command-line:'), chalk.bold(paths.join(', ')));
// Build custom content config similar to promptCustomContentSource
const customPaths = [];
@ -296,7 +309,7 @@ class UI {
const expandedPath = this.expandUserPath(customPath);
const validation = this.validateCustomContentPathSync(expandedPath);
if (validation) {
await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
console.log(chalk.yellow(`⚠️ Skipping invalid custom content path: ${customPath} - ${validation}`));
continue;
}
@ -308,12 +321,12 @@ class UI {
const yaml = require('yaml');
moduleMeta = yaml.parse(moduleYaml);
} catch (error) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`));
continue;
}
if (!moduleMeta.code) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - module.yaml missing 'code' field`));
continue;
}
@ -391,11 +404,11 @@ class UI {
.split(',')
.map((m) => m.trim())
.filter(Boolean);
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
console.log(chalk.cyan('Using modules from command-line:'), chalk.bold(selectedModules.join(', ')));
} else if (options.yes) {
// Use default modules when --yes flag is set
selectedModules = await this.getDefaultModules(installedModuleIds);
await prompts.log.info(`Using default modules (--yes flag): ${selectedModules.join(', ')}`);
console.log(chalk.cyan('Using default modules (--yes flag):'), chalk.bold(selectedModules.join(', ')));
} else {
selectedModules = await this.selectAllModules(installedModuleIds);
}
@ -407,7 +420,7 @@ class UI {
.split(',')
.map((p) => p.trim())
.filter(Boolean);
await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
console.log(chalk.cyan('Using custom content from command-line:'), chalk.bold(paths.join(', ')));
// Build custom content config similar to promptCustomContentSource
const customPaths = [];
@ -417,7 +430,7 @@ class UI {
const expandedPath = this.expandUserPath(customPath);
const validation = this.validateCustomContentPathSync(expandedPath);
if (validation) {
await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
console.log(chalk.yellow(`⚠️ Skipping invalid custom content path: ${customPath} - ${validation}`));
continue;
}
@ -429,12 +442,12 @@ class UI {
const yaml = require('yaml');
moduleMeta = yaml.parse(moduleYaml);
} catch (error) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`));
continue;
}
if (!moduleMeta.code) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - module.yaml missing 'code' field`));
continue;
}
@ -518,7 +531,7 @@ class UI {
const allKnownValues = new Set([...preferredIdes, ...otherIdes].map((ide) => ide.value));
const unknownTools = configuredIdes.filter((id) => id && typeof id === 'string' && !allKnownValues.has(id));
if (unknownTools.length > 0) {
await prompts.log.warn(`Previously configured tools are no longer available: ${unknownTools.join(', ')}`);
console.log(chalk.yellow(`⚠️ Previously configured tools are no longer available: ${unknownTools.join(', ')}`));
}
// ─────────────────────────────────────────────────────────────────────────────
@ -556,20 +569,21 @@ class UI {
const selectedIdes = upgradeSelected || [];
if (selectedIdes.length === 0) {
console.log('');
const confirmNoTools = await prompts.confirm({
message: 'No tools selected. Continue without installing any tools?',
default: false,
});
if (!confirmNoTools) {
return this.promptToolSelection(projectDir, options);
return this.promptToolSelection(projectDir);
}
return { ides: [], skipIde: true };
}
// Display selected tools
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return { ides: selectedIdes, skipIde: false };
}
@ -595,25 +609,25 @@ class UI {
if (options.tools) {
// Check for explicit "none" value to skip tool installation
if (options.tools.toLowerCase() === 'none') {
await prompts.log.info('Skipping tool configuration (--tools none)');
console.log(chalk.cyan('Skipping tool configuration (--tools none)'));
return { ides: [], skipIde: true };
} else {
selectedIdes = options.tools
.split(',')
.map((t) => t.trim())
.filter(Boolean);
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
console.log(chalk.cyan('Using tools from command-line:'), chalk.bold(selectedIdes.join(', ')));
this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return { ides: selectedIdes, skipIde: false };
}
} else if (options.yes) {
// If --yes flag is set, skip tool prompt and use previously configured tools or empty
if (configuredIdes.length > 0) {
await prompts.log.info(`Using previously configured tools (--yes flag): ${configuredIdes.join(', ')}`);
await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
console.log(chalk.cyan('Using previously configured tools (--yes flag):'), chalk.bold(configuredIdes.join(', ')));
this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
return { ides: configuredIdes, skipIde: false };
} else {
await prompts.log.info('Skipping tool configuration (--yes flag, no previous tools)');
console.log(chalk.cyan('Skipping tool configuration (--yes flag, no previous tools)'));
return { ides: [], skipIde: true };
}
}
@ -633,6 +647,7 @@ class UI {
// STEP 3: Confirm if no tools selected
// ─────────────────────────────────────────────────────────────────────────────
if (selectedIdes.length === 0) {
console.log('');
const confirmNoTools = await prompts.confirm({
message: 'No tools selected. Continue without installing any tools?',
default: false,
@ -640,7 +655,7 @@ class UI {
if (!confirmNoTools) {
// User wants to select tools - recurse
return this.promptToolSelection(projectDir, options);
return this.promptToolSelection(projectDir);
}
return {
@ -650,7 +665,7 @@ class UI {
}
// Display selected tools
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return {
ides: selectedIdes,
@ -693,12 +708,15 @@ class UI {
* Display installation summary
* @param {Object} result - Installation result
*/
async showInstallSummary(result) {
let summary = `Installed to: ${result.path}`;
showInstallSummary(result) {
// Clean, simple completion message
console.log('\n' + chalk.green.bold('✨ BMAD is ready to use!'));
// Show installation summary in a simple format
console.log(chalk.dim(`Installed to: ${result.path}`));
if (result.modules && result.modules.length > 0) {
summary += `\nModules: ${result.modules.join(', ')}`;
console.log(chalk.dim(`Modules: ${result.modules.join(', ')}`));
}
await prompts.note(summary, 'BMAD is ready to use!');
}
/**
@ -751,19 +769,19 @@ class UI {
const coreConfig = {};
if (options.userName) {
coreConfig.user_name = options.userName;
await prompts.log.info(`Using user name from command-line: ${options.userName}`);
console.log(chalk.cyan('Using user name from command-line:'), chalk.bold(options.userName));
}
if (options.communicationLanguage) {
coreConfig.communication_language = options.communicationLanguage;
await prompts.log.info(`Using communication language from command-line: ${options.communicationLanguage}`);
console.log(chalk.cyan('Using communication language from command-line:'), chalk.bold(options.communicationLanguage));
}
if (options.documentOutputLanguage) {
coreConfig.document_output_language = options.documentOutputLanguage;
await prompts.log.info(`Using document output language from command-line: ${options.documentOutputLanguage}`);
console.log(chalk.cyan('Using document output language from command-line:'), chalk.bold(options.documentOutputLanguage));
}
if (options.outputFolder) {
coreConfig.output_folder = options.outputFolder;
await prompts.log.info(`Using output folder from command-line: ${options.outputFolder}`);
console.log(chalk.cyan('Using output folder from command-line:'), chalk.bold(options.outputFolder));
}
// Load existing config to merge with provided options
@ -800,7 +818,7 @@ class UI {
document_output_language: 'English',
output_folder: '_bmad-output',
};
await prompts.log.info('Using default configuration (--yes flag)');
console.log(chalk.cyan('Using default configuration (--yes flag)'));
}
} else {
// Load existing configs first if they exist
@ -821,11 +839,11 @@ class UI {
* @returns {Array} Module choices for prompt
*/
async getModuleChoices(installedModuleIds, customContentConfig = null) {
const color = await prompts.getColor();
const moduleChoices = [];
const isNewInstallation = installedModuleIds.size === 0;
const customContentItems = [];
const hasCustomContentItems = false;
// Add custom content items
if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
@ -837,7 +855,7 @@ class UI {
const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo) {
customContentItems.push({
name: `${color.cyan('\u2713')} ${customInfo.name} ${color.dim(`(${customInfo.relativePath})`)}`,
name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`,
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
checked: true, // Default to selected since user chose to provide custom content
path: customInfo.path, // Track path to avoid duplicates
@ -865,7 +883,7 @@ class UI {
if (!isDuplicate) {
allCustomModules.push({
name: `${color.cyan('\u2713')} ${mod.name} ${color.dim('(cached)')}`,
name: `${chalk.cyan('✓')} ${mod.name} ${chalk.gray(`(cached)`)}`,
value: mod.id,
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
hint: mod.description || undefined,
@ -916,20 +934,22 @@ class UI {
...choicesWithDefaults,
{
value: '__NONE__',
label: '\u26A0 None / I changed my mind - skip module installation',
label: ' None / I changed my mind - skip module installation',
checked: false,
},
];
const selected = await prompts.multiselect({
message: 'Select modules to install (use arrow keys, space to toggle):',
message: `Select modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
choices: choicesWithSkipOption,
required: true,
});
// If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None / I changed my mind" was selected, so no modules will be installed.');
console.log();
console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no modules will be installed.'));
console.log();
return [];
}
@ -962,7 +982,8 @@ class UI {
*/
async selectExternalModules(externalModuleChoices, defaultSelections = []) {
// Build a message showing available modules
const message = 'Select official BMad modules to install (use arrow keys, space to toggle):';
const availableNames = externalModuleChoices.map((c) => c.name).join(', ');
const message = `Select official BMad modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`;
// Mark choices as checked based on defaultSelections
const choicesWithDefaults = externalModuleChoices.map((choice) => ({
@ -988,7 +1009,9 @@ class UI {
// If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None / I changed my mind" was selected, so no external modules will be installed.');
console.log();
console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no external modules will be installed.'));
console.log();
return [];
}
@ -1010,98 +1033,100 @@ class UI {
const externalManager = new ExternalModuleManager();
const externalModules = await externalManager.listAvailable();
// Build flat options list with group hints for autocompleteMultiselect
const allOptions = [];
// Build grouped options
const groupedOptions = {};
const initialValues = [];
const lockedValues = ['core'];
// Core module is always installed — show it locked at the top
allOptions.push({ label: 'BMad Core Module', value: 'core', hint: 'Core configuration and shared resources' });
initialValues.push('core');
// Helper to build module entry with proper sorting and selection
const buildModuleEntry = (mod, value, group) => {
const buildModuleEntry = (mod, value) => {
const isInstalled = installedModuleIds.has(value);
const isDefault = mod.defaultSelected === true;
return {
label: mod.name,
label: mod.description ? `${mod.name}${mod.description}` : mod.name,
value,
hint: mod.description || group,
// Pre-select only if already installed (not on fresh install)
selected: isInstalled,
// For sorting: defaultSelected=0, others=1
sortKey: isDefault ? 0 : 1,
// Pre-select if default selected OR already installed
selected: isDefault || isInstalled,
};
};
// Local modules (BMM, BMB, etc.)
const localEntries = [];
// Group 1: BMad Core (BMM, BMB)
const coreModules = [];
for (const mod of localModules) {
if (!mod.isCustom && mod.id !== 'core') {
const entry = buildModuleEntry(mod, mod.id, 'Local');
localEntries.push(entry);
if (!mod.isCustom && (mod.id === 'bmm' || mod.id === 'bmb')) {
const entry = buildModuleEntry(mod, mod.id);
coreModules.push(entry);
if (entry.selected) {
initialValues.push(mod.id);
}
}
}
allOptions.push(...localEntries.map(({ label, value, hint }) => ({ label, value, hint })));
// Sort: defaultSelected first, then others
coreModules.sort((a, b) => a.sortKey - b.sortKey);
// Remove sortKey from final entries
if (coreModules.length > 0) {
groupedOptions['BMad Core'] = coreModules.map(({ label, value }) => ({ label, value }));
}
// Group 2: BMad Official Modules (type: bmad-org)
const officialModules = [];
for (const mod of externalModules) {
if (mod.type === 'bmad-org') {
const entry = buildModuleEntry(mod, mod.code, 'Official');
const entry = buildModuleEntry(mod, mod.code);
officialModules.push(entry);
if (entry.selected) {
initialValues.push(mod.code);
}
}
}
allOptions.push(...officialModules.map(({ label, value, hint }) => ({ label, value, hint })));
officialModules.sort((a, b) => a.sortKey - b.sortKey);
if (officialModules.length > 0) {
groupedOptions['BMad Official Modules'] = officialModules.map(({ label, value }) => ({ label, value }));
}
// Group 3: Community Modules (type: community)
const communityModules = [];
for (const mod of externalModules) {
if (mod.type === 'community') {
const entry = buildModuleEntry(mod, mod.code, 'Community');
const entry = buildModuleEntry(mod, mod.code);
communityModules.push(entry);
if (entry.selected) {
initialValues.push(mod.code);
}
}
}
allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })), {
// "None" option at the end
label: '\u26A0 None - Skip module installation',
value: '__NONE__',
});
communityModules.sort((a, b) => a.sortKey - b.sortKey);
if (communityModules.length > 0) {
groupedOptions['Community Modules'] = communityModules.map(({ label, value }) => ({ label, value }));
}
const selected = await prompts.autocompleteMultiselect({
message: 'Select modules to install:',
options: allOptions,
// Add "None" option at the end
groupedOptions[' '] = [
{
label: '⚠ None - Skip module installation',
value: '__NONE__',
},
];
const selected = await prompts.groupMultiselect({
message: `Select modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
options: groupedOptions,
initialValues: initialValues.length > 0 ? initialValues : undefined,
lockedValues,
required: true,
maxItems: allOptions.length,
selectableGroups: false,
});
// If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None" was selected, so no modules will be installed.');
console.log();
console.log(chalk.yellow('⚠️ "None" was selected, so no modules will be installed.'));
console.log();
return [];
}
// Filter out the special '__NONE__' value
const result = selected ? selected.filter((m) => m !== '__NONE__') : [];
// Display selected modules as bulleted list
if (result.length > 0) {
const moduleLines = result.map((moduleId) => {
const opt = allOptions.find((o) => o.value === moduleId);
return ` \u2022 ${opt?.label || moduleId}`;
});
await prompts.log.message('Selected modules:\n' + moduleLines.join('\n'));
}
return result;
return selected ? selected.filter((m) => m !== '__NONE__') : [];
}
/**
@ -1160,7 +1185,7 @@ class UI {
* @param {string} directory - The directory path
*/
async displayDirectoryInfo(directory) {
await prompts.log.info(`Resolved installation path: ${directory}`);
console.log(chalk.cyan('\nResolved installation path:'), chalk.bold(directory));
const dirExists = await fs.pathExists(directory);
if (dirExists) {
@ -1176,10 +1201,12 @@ class UI {
const hasBmadInstall =
(await fs.pathExists(bmadResult.bmadDir)) && (await fs.pathExists(path.join(bmadResult.bmadDir, '_config', 'manifest.yaml')));
const bmadNote = hasBmadInstall ? ` including existing BMAD installation (${path.basename(bmadResult.bmadDir)})` : '';
await prompts.log.message(`Directory exists and contains ${files.length} item(s)${bmadNote}`);
console.log(
chalk.gray(`Directory exists and contains ${files.length} item(s)`) +
(hasBmadInstall ? chalk.yellow(` including existing BMAD installation (${path.basename(bmadResult.bmadDir)})`) : ''),
);
} else {
await prompts.log.message('Directory exists and is empty');
console.log(chalk.gray('Directory exists and is empty'));
}
}
}
@ -1200,7 +1227,7 @@ class UI {
});
if (!proceed) {
await prompts.log.warn("Let's try again with a different path.");
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
}
return proceed;
@ -1212,7 +1239,7 @@ class UI {
});
if (!create) {
await prompts.log.warn("Let's try again with a different path.");
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
}
return create;
@ -1432,7 +1459,7 @@ class UI {
return configs;
} catch {
// If loading fails, return empty configs
await prompts.log.warn('Could not load existing configurations');
console.warn('Warning: Could not load existing configurations');
return configs;
}
}
@ -1563,7 +1590,7 @@ class UI {
name: moduleData.name || moduleData.code,
});
await prompts.log.success(`Confirmed local custom module: ${moduleData.name || moduleData.code}`);
console.log(chalk.green(`Confirmed local custom module: ${moduleData.name || moduleData.code}`));
}
// Ask if user wants to add these to the installation
@ -1629,11 +1656,11 @@ class UI {
};
// Ask user about custom modules
await prompts.log.info('Custom Modules');
console.log(chalk.cyan('\n⚙ Custom Modules'));
if (cachedCustomModules.length > 0) {
await prompts.log.message('Found custom modules in your installation:');
console.log(chalk.dim('Found custom modules in your installation:'));
} else {
await prompts.log.message('No custom modules currently installed.');
console.log(chalk.dim('No custom modules currently installed.'));
}
// Build choices dynamically based on whether we have existing modules
@ -1659,14 +1686,14 @@ class UI {
case 'keep': {
// Keep all existing custom modules
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
await prompts.log.message(`Keeping ${result.selectedCustomModules.length} custom module(s)`);
console.log(chalk.dim(`Keeping ${result.selectedCustomModules.length} custom module(s)`));
break;
}
case 'select': {
// Let user choose which to keep
const selectChoices = cachedCustomModules.map((m) => ({
name: `${m.name} (${m.id})`,
name: `${m.name} ${chalk.gray(`(${m.id})`)}`,
value: m.id,
checked: m.checked,
}));
@ -1682,14 +1709,16 @@ class UI {
];
const keepModules = await prompts.multiselect({
message: 'Select custom modules to keep (use arrow keys, space to toggle):',
message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
choices: choicesWithSkip,
required: true,
});
// If user selected both "__NONE__" and other modules, honor the "None" choice
if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) {
await prompts.log.warn('"None / I changed my mind" was selected, so no custom modules will be kept.');
console.log();
console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no custom modules will be kept.'));
console.log();
result.selectedCustomModules = [];
} else {
// Filter out the special '__NONE__' value
@ -1714,13 +1743,13 @@ class UI {
case 'remove': {
// Remove all custom modules
await prompts.log.warn('All custom modules will be removed from the installation');
console.log(chalk.yellow('All custom modules will be removed from the installation'));
break;
}
case 'cancel': {
// User cancelled - no custom modules
await prompts.log.message('No custom modules will be added');
console.log(chalk.dim('No custom modules will be added'));
break;
}
}
@ -1753,26 +1782,30 @@ class UI {
return true; // Not legacy, proceed
}
let warningContent;
console.log('');
console.log(chalk.yellow.bold('⚠️ VERSION WARNING'));
console.log(chalk.yellow('─'.repeat(80)));
if (installedVersion === 'unknown') {
warningContent = 'Unable to detect your installed BMAD version.\n' + 'This appears to be a legacy or unsupported installation.';
console.log(chalk.yellow('Unable to detect your installed BMAD version.'));
console.log(chalk.yellow('This appears to be a legacy or unsupported installation.'));
} else {
warningContent =
`You are updating from ${installedVersion} to ${currentVersion}.\n` + 'You have a legacy version installed (v4 or alpha).';
console.log(chalk.yellow(`You are updating from ${installedVersion} to ${currentVersion}.`));
console.log(chalk.yellow('You have a legacy version installed (v4 or alpha).'));
}
warningContent +=
'\n\nFor the best experience, we recommend:\n' +
' 1. Delete your current BMAD installation folder\n' +
` (the "${bmadFolderName}/" folder in your project)\n` +
' 2. Run a fresh installation\n\n' +
'Benefits of a fresh install:\n' +
' \u2022 Cleaner configuration without legacy artifacts\n' +
' \u2022 All new features properly configured\n' +
' \u2022 Fewer potential conflicts';
await prompts.log.warn('VERSION WARNING');
await prompts.note(warningContent, 'Version Warning');
console.log('');
console.log(chalk.dim('For the best experience, we recommend:'));
console.log(chalk.dim(' 1. Delete your current BMAD installation folder'));
console.log(chalk.dim(` (the "${bmadFolderName}/" folder in your project)`));
console.log(chalk.dim(' 2. Run a fresh installation'));
console.log('');
console.log(chalk.dim('Benefits of a fresh install:'));
console.log(chalk.dim(' • Cleaner configuration without legacy artifacts'));
console.log(chalk.dim(' • All new features properly configured'));
console.log(chalk.dim(' • Fewer potential conflicts'));
console.log(chalk.yellow('─'.repeat(80)));
console.log('');
const proceed = await prompts.select({
message: 'How would you like to proceed?',
@ -1790,10 +1823,11 @@ class UI {
});
if (proceed === 'cancel') {
await prompts.note(
`1. Delete the "${bmadFolderName}/" folder in your project\n` + "2. Run 'bmad install' again",
'To do a fresh install',
);
console.log('');
console.log(chalk.cyan('To do a fresh install:'));
console.log(chalk.dim(` 1. Delete the "${bmadFolderName}/" folder in your project`));
console.log(chalk.dim(" 2. Run 'bmad install' again"));
console.log('');
}
return proceed === 'proceed';
@ -1804,34 +1838,41 @@ class UI {
* @param {Array} modules - Array of module info objects with version info
* @param {Array} availableUpdates - Array of available updates
*/
async displayModuleVersions(modules, availableUpdates = []) {
displayModuleVersions(modules, availableUpdates = []) {
console.log('');
console.log(chalk.cyan.bold('📦 Module Versions'));
console.log(chalk.gray('─'.repeat(80)));
// Group modules by source
const builtIn = modules.filter((m) => m.source === 'built-in');
const external = modules.filter((m) => m.source === 'external');
const custom = modules.filter((m) => m.source === 'custom');
const unknown = modules.filter((m) => m.source === 'unknown');
const lines = [];
const formatGroup = (group, title) => {
const displayGroup = (group, title) => {
if (group.length === 0) return;
lines.push(title);
for (const mod of group) {
const updateInfo = availableUpdates.find((u) => u.name === mod.name);
const versionDisplay = mod.version || 'unknown';
console.log(chalk.yellow(`\n${title}`));
for (const module of group) {
const updateInfo = availableUpdates.find((u) => u.name === module.name);
const versionDisplay = module.version || chalk.gray('unknown');
if (updateInfo) {
lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2192 ${updateInfo.latestVersion} \u2191`);
console.log(
` ${chalk.cyan(module.name.padEnd(20))} ${versionDisplay}${chalk.green(updateInfo.latestVersion)} ${chalk.green('↑')}`,
);
} else {
lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2713`);
console.log(` ${chalk.cyan(module.name.padEnd(20))} ${versionDisplay} ${chalk.gray('✓')}`);
}
}
};
formatGroup(builtIn, 'Built-in Modules');
formatGroup(external, 'External Modules (Official)');
formatGroup(custom, 'Custom Modules');
formatGroup(unknown, 'Other Modules');
displayGroup(builtIn, 'Built-in Modules');
displayGroup(external, 'External Modules (Official)');
displayGroup(custom, 'Custom Modules');
displayGroup(unknown, 'Other Modules');
await prompts.note(lines.join('\n'), 'Module Versions');
console.log('');
}
/**
@ -1844,10 +1885,12 @@ class UI {
return [];
}
await prompts.log.info('Available Updates');
console.log('');
console.log(chalk.cyan.bold('🔄 Available Updates'));
console.log(chalk.gray('─'.repeat(80)));
const choices = availableUpdates.map((update) => ({
name: `${update.name} (v${update.installedVersion} \u2192 v${update.latestVersion})`,
name: `${update.name} ${chalk.dim(`(v${update.installedVersion} v${update.latestVersion})`)}`,
value: update.name,
checked: true, // Default to selecting all updates
}));
@ -1873,7 +1916,7 @@ class UI {
// Allow specific selection
const selected = await prompts.multiselect({
message: 'Select modules to update (use arrow keys, space to toggle):',
message: `Select modules to update ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
choices: choices,
required: true,
});
@ -1885,29 +1928,34 @@ class UI {
* Display status of all installed modules
* @param {Object} statusData - Status data with modules, installation info, and available updates
*/
async displayStatus(statusData) {
displayStatus(statusData) {
const { installation, modules, availableUpdates, bmadDir } = statusData;
// Installation info
const infoLines = [
`Version: ${installation.version || 'unknown'}`,
`Location: ${bmadDir}`,
`Installed: ${new Date(installation.installDate).toLocaleDateString()}`,
`Last Updated: ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : 'unknown'}`,
];
console.log('');
console.log(chalk.cyan.bold('📋 BMAD Status'));
console.log(chalk.gray('─'.repeat(80)));
await prompts.note(infoLines.join('\n'), 'BMAD Status');
// Installation info
console.log(chalk.yellow('\nInstallation'));
console.log(` ${chalk.gray('Version:'.padEnd(20))} ${installation.version || chalk.gray('unknown')}`);
console.log(` ${chalk.gray('Location:'.padEnd(20))} ${bmadDir}`);
console.log(` ${chalk.gray('Installed:'.padEnd(20))} ${new Date(installation.installDate).toLocaleDateString()}`);
console.log(
` ${chalk.gray('Last Updated:'.padEnd(20))} ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : chalk.gray('unknown')}`,
);
// Module versions
await this.displayModuleVersions(modules, availableUpdates);
this.displayModuleVersions(modules, availableUpdates);
// Update summary
if (availableUpdates.length > 0) {
await prompts.log.warn(`${availableUpdates.length} update(s) available`);
await prompts.log.message('Run \'bmad install\' and select "Quick Update" to update');
console.log(chalk.yellow.bold(`\n⚠️ ${availableUpdates.length} update(s) available`));
console.log(chalk.dim(` Run 'bmad install' and select "Quick Update" to update`));
} else {
await prompts.log.success('All modules are up to date');
console.log(chalk.green.bold('\n✓ All modules are up to date'));
}
console.log('');
}
/**
@ -1916,17 +1964,19 @@ class UI {
* @param {Array} preferredIdes - Array of preferred IDE objects
* @param {Array} allTools - Array of all tool objects
*/
async displaySelectedTools(selectedIdes, preferredIdes, allTools) {
displaySelectedTools(selectedIdes, preferredIdes, allTools) {
if (selectedIdes.length === 0) return;
const preferredValues = new Set(preferredIdes.map((ide) => ide.value));
const toolLines = selectedIdes.map((ideValue) => {
console.log('');
console.log(chalk.dim(' Selected tools:'));
for (const ideValue of selectedIdes) {
const tool = allTools.find((t) => t.value === ideValue);
const name = tool?.name || ideValue;
const marker = preferredValues.has(ideValue) ? ' \u2B50' : '';
return ` \u2022 ${name}${marker}`;
});
await prompts.log.message('Selected tools:\n' + toolLines.join('\n'));
const marker = preferredValues.has(ideValue) ? ' ⭐' : '';
console.log(chalk.dim(`${name}${marker}`));
}
}
}