Merge branch 'main' into doc-updates
This commit is contained in:
commit
46c54c0313
|
|
@ -61,7 +61,7 @@
|
||||||
| -------------- | -------------- | ------------------------- | ----------------------------- |
|
| -------------- | -------------- | ------------------------- | ----------------------------- |
|
||||||
| claude-code | Claude Code | .claude/commands/ | .md with frontmatter |
|
| claude-code | Claude Code | .claude/commands/ | .md with frontmatter |
|
||||||
| codex | Codex | (varies) | .md |
|
| codex | Codex | (varies) | .md |
|
||||||
| cursor | Cursor | .cursor/rules/bmad/ | .mdc with MDC frontmatter |
|
| cursor | Cursor | .cursor/commands/bmad/ | .md with YAML frontmatter |
|
||||||
| github-copilot | GitHub Copilot | .github/ | .md |
|
| github-copilot | GitHub Copilot | .github/ | .md |
|
||||||
| opencode | OpenCode | .opencode/ | .md |
|
| opencode | OpenCode | .opencode/ | .md |
|
||||||
| windsurf | Windsurf | .windsurf/workflows/bmad/ | .md with workflow frontmatter |
|
| windsurf | Windsurf | .windsurf/workflows/bmad/ | .md with workflow frontmatter |
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ module.exports = { NewIdeSetup };
|
||||||
| IDE | Config Pattern | File Extension |
|
| IDE | Config Pattern | File Extension |
|
||||||
| -------------- | ------------------------- | -------------- |
|
| -------------- | ------------------------- | -------------- |
|
||||||
| Claude Code | .claude/commands/bmad/ | .md |
|
| Claude Code | .claude/commands/bmad/ | .md |
|
||||||
| Cursor | .cursor/rules/bmad/ | .mdc |
|
| Cursor | .cursor/commands/bmad/ | .md |
|
||||||
| Windsurf | .windsurf/workflows/bmad/ | .md |
|
| Windsurf | .windsurf/workflows/bmad/ | .md |
|
||||||
| GitHub Copilot | .github/ | .md |
|
| GitHub Copilot | .github/ | .md |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
|
||||||
const { Installer } = require('../installers/lib/core/installer');
|
const { Installer } = require('../installers/lib/core/installer');
|
||||||
const { UI } = require('../lib/ui');
|
const { UI } = require('../lib/ui');
|
||||||
|
|
||||||
|
|
@ -72,6 +71,7 @@ module.exports = {
|
||||||
console.log(chalk.dim(' • ElevenLabs AI (150+ premium voices)'));
|
console.log(chalk.dim(' • ElevenLabs AI (150+ premium voices)'));
|
||||||
console.log(chalk.dim(' • Piper TTS (50+ free voices)\n'));
|
console.log(chalk.dim(' • Piper TTS (50+ free voices)\n'));
|
||||||
|
|
||||||
|
const { default: inquirer } = await import('inquirer');
|
||||||
await inquirer.prompt([
|
await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,18 @@ const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
|
||||||
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
|
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
|
||||||
const { CLIUtils } = require('../../../lib/cli-utils');
|
const { CLIUtils } = require('../../../lib/cli-utils');
|
||||||
|
|
||||||
|
// Lazy-load inquirer (ESM module) to avoid ERR_REQUIRE_ESM
|
||||||
|
let _inquirer = null;
|
||||||
|
async function getInquirer() {
|
||||||
|
if (!_inquirer) {
|
||||||
|
_inquirer = (await import('inquirer')).default;
|
||||||
|
}
|
||||||
|
return _inquirer;
|
||||||
|
}
|
||||||
|
|
||||||
class ConfigCollector {
|
class ConfigCollector {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.collectedConfig = {};
|
this.collectedConfig = {};
|
||||||
|
|
@ -175,6 +183,7 @@ class ConfigCollector {
|
||||||
* @returns {boolean} True if new fields were prompted, false if all fields existed
|
* @returns {boolean} True if new fields were prompted, false if all fields existed
|
||||||
*/
|
*/
|
||||||
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
this.currentProjectDir = projectDir;
|
this.currentProjectDir = projectDir;
|
||||||
|
|
||||||
// Load existing config if not already loaded
|
// Load existing config if not already loaded
|
||||||
|
|
@ -493,6 +502,7 @@ class ConfigCollector {
|
||||||
* @param {boolean} skipCompletion - Skip showing completion message (for early core collection)
|
* @param {boolean} skipCompletion - Skip showing completion message (for early core collection)
|
||||||
*/
|
*/
|
||||||
async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
|
async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
this.currentProjectDir = projectDir;
|
this.currentProjectDir = projectDir;
|
||||||
// Load existing config if needed and not already loaded
|
// Load existing config if needed and not already loaded
|
||||||
if (!skipLoadExisting && !this.existingConfig) {
|
if (!skipLoadExisting && !this.existingConfig) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const ora = require('ora');
|
const ora = require('ora');
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
|
||||||
const { Detector } = require('./detector');
|
const { Detector } = require('./detector');
|
||||||
const { Manifest } = require('./manifest');
|
const { Manifest } = require('./manifest');
|
||||||
const { ModuleManager } = require('../modules/manager');
|
const { ModuleManager } = require('../modules/manager');
|
||||||
|
|
@ -2140,7 +2139,7 @@ class Installer {
|
||||||
* Private: Prompt for update action
|
* Private: Prompt for update action
|
||||||
*/
|
*/
|
||||||
async promptUpdateAction() {
|
async promptUpdateAction() {
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
const { default: inquirer } = await import('inquirer');
|
||||||
return await inquirer.prompt([
|
return await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'list',
|
type: 'list',
|
||||||
|
|
@ -2157,7 +2156,7 @@ class Installer {
|
||||||
* @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version)
|
* @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version)
|
||||||
*/
|
*/
|
||||||
async handleLegacyV4Migration(_projectDir, _legacyV4) {
|
async handleLegacyV4Migration(_projectDir, _legacyV4) {
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(chalk.yellow.bold('⚠️ Legacy BMAD v4 detected'));
|
console.log(chalk.yellow.bold('⚠️ Legacy BMAD v4 detected'));
|
||||||
|
|
@ -2438,7 +2437,7 @@ class Installer {
|
||||||
|
|
||||||
console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
|
console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
|
||||||
|
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
const { default: inquirer } = await import('inquirer');
|
||||||
let keptCount = 0;
|
let keptCount = 0;
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
let removedCount = 0;
|
let removedCount = 0;
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
|
|
||||||
if (config.subagentChoices.install !== 'none') {
|
if (config.subagentChoices.install !== 'none') {
|
||||||
// Ask for installation location
|
// Ask for installation location
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
const { default: inquirer } = await import('inquirer');
|
||||||
const locationAnswer = await inquirer.prompt([
|
const locationAnswer = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'list',
|
type: 'list',
|
||||||
|
|
@ -297,7 +297,7 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
choices = await this.promptSubagentInstallation(config.subagents);
|
choices = await this.promptSubagentInstallation(config.subagents);
|
||||||
|
|
||||||
if (choices.install !== 'none') {
|
if (choices.install !== 'none') {
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
const { default: inquirer } = await import('inquirer');
|
||||||
const locationAnswer = await inquirer.prompt([
|
const locationAnswer = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'list',
|
type: 'list',
|
||||||
|
|
@ -334,7 +334,7 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
* Prompt user for subagent installation preferences
|
* Prompt user for subagent installation preferences
|
||||||
*/
|
*/
|
||||||
async promptSubagentInstallation(subagentConfig) {
|
async promptSubagentInstallation(subagentConfig) {
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
|
||||||
// First ask if they want to install subagents
|
// First ask if they want to install subagents
|
||||||
const { install } = await inquirer.prompt([
|
const { install } = await inquirer.prompt([
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
|
|
||||||
if (config.subagentChoices.install !== 'none') {
|
if (config.subagentChoices.install !== 'none') {
|
||||||
// Ask for installation location
|
// Ask for installation location
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
const { default: inquirer } = await import('inquirer');
|
||||||
const locationAnswer = await inquirer.prompt([
|
const locationAnswer = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'list',
|
type: 'list',
|
||||||
|
|
@ -305,7 +305,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
choices = await this.promptSubagentInstallation(config.subagents);
|
choices = await this.promptSubagentInstallation(config.subagents);
|
||||||
|
|
||||||
if (choices.install !== 'none') {
|
if (choices.install !== 'none') {
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
const { default: inquirer } = await import('inquirer');
|
||||||
const locationAnswer = await inquirer.prompt([
|
const locationAnswer = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'list',
|
type: 'list',
|
||||||
|
|
@ -342,7 +342,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
* Prompt user for subagent installation preferences
|
* Prompt user for subagent installation preferences
|
||||||
*/
|
*/
|
||||||
async promptSubagentInstallation(subagentConfig) {
|
async promptSubagentInstallation(subagentConfig) {
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
|
||||||
// First ask if they want to install subagents
|
// First ask if they want to install subagents
|
||||||
const { install } = await inquirer.prompt([
|
const { install } = await inquirer.prompt([
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
* @returns {Object} Collected configuration
|
* @returns {Object} Collected configuration
|
||||||
*/
|
*/
|
||||||
async collectConfiguration(options = {}) {
|
async collectConfiguration(options = {}) {
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
|
||||||
let confirmed = false;
|
let confirmed = false;
|
||||||
let installLocation = 'global';
|
let installLocation = 'global';
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const { BaseIdeSetup } = require('./_base-ide');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||||
|
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cursor IDE setup handler
|
* Cursor IDE setup handler
|
||||||
|
|
@ -12,6 +13,7 @@ class CursorSetup extends BaseIdeSetup {
|
||||||
super('cursor', 'Cursor', true); // preferred IDE
|
super('cursor', 'Cursor', true); // preferred IDE
|
||||||
this.configDir = '.cursor';
|
this.configDir = '.cursor';
|
||||||
this.rulesDir = 'rules';
|
this.rulesDir = 'rules';
|
||||||
|
this.commandsDir = 'commands';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -21,11 +23,17 @@ class CursorSetup extends BaseIdeSetup {
|
||||||
async cleanup(projectDir) {
|
async cleanup(projectDir) {
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const bmadRulesDir = path.join(projectDir, this.configDir, this.rulesDir, 'bmad');
|
const bmadRulesDir = path.join(projectDir, this.configDir, this.rulesDir, 'bmad');
|
||||||
|
const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad');
|
||||||
|
|
||||||
if (await fs.pathExists(bmadRulesDir)) {
|
if (await fs.pathExists(bmadRulesDir)) {
|
||||||
await fs.remove(bmadRulesDir);
|
await fs.remove(bmadRulesDir);
|
||||||
console.log(chalk.dim(` Removed old BMAD rules from ${this.name}`));
|
console.log(chalk.dim(` Removed old BMAD rules from ${this.name}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await fs.pathExists(bmadCommandsDir)) {
|
||||||
|
await fs.remove(bmadCommandsDir);
|
||||||
|
console.log(chalk.dim(` Removed old BMAD commands from ${this.name}`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -40,330 +48,76 @@ class CursorSetup extends BaseIdeSetup {
|
||||||
// Clean up old BMAD installation first
|
// Clean up old BMAD installation first
|
||||||
await this.cleanup(projectDir);
|
await this.cleanup(projectDir);
|
||||||
|
|
||||||
// Create .cursor/rules directory structure
|
// Create .cursor/commands directory structure
|
||||||
const cursorDir = path.join(projectDir, this.configDir);
|
const cursorDir = path.join(projectDir, this.configDir);
|
||||||
const rulesDir = path.join(cursorDir, this.rulesDir);
|
const commandsDir = path.join(cursorDir, this.commandsDir);
|
||||||
const bmadRulesDir = path.join(rulesDir, 'bmad');
|
const bmadCommandsDir = path.join(commandsDir, 'bmad');
|
||||||
|
|
||||||
await this.ensureDir(bmadRulesDir);
|
await this.ensureDir(bmadCommandsDir);
|
||||||
|
|
||||||
// Generate agent launchers first
|
// Generate agent launchers using AgentCommandGenerator
|
||||||
|
// This creates small launcher files that reference the actual agents in _bmad/
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||||
|
|
||||||
// Convert artifacts to agent format for index creation
|
|
||||||
const agents = agentArtifacts.map((a) => ({ module: a.module, name: a.name }));
|
|
||||||
|
|
||||||
// Get tasks, tools, and workflows (ALL workflows now generate commands)
|
|
||||||
const tasks = await this.getTasks(bmadDir, true);
|
|
||||||
const tools = await this.getTools(bmadDir, true);
|
|
||||||
|
|
||||||
// Get ALL workflows using the new workflow command generator
|
|
||||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
|
||||||
|
|
||||||
// Convert artifacts to workflow objects for directory creation
|
|
||||||
const workflows = workflowArtifacts
|
|
||||||
.filter((artifact) => artifact.type === 'workflow-command')
|
|
||||||
.map((artifact) => ({
|
|
||||||
module: artifact.module,
|
|
||||||
name: path.basename(artifact.relativePath, '.md'),
|
|
||||||
path: artifact.sourcePath,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Create directories for each module
|
// Create directories for each module
|
||||||
const modules = new Set();
|
const modules = new Set();
|
||||||
for (const item of [...agents, ...tasks, ...tools, ...workflows]) modules.add(item.module);
|
for (const artifact of agentArtifacts) {
|
||||||
|
modules.add(artifact.module);
|
||||||
|
}
|
||||||
|
|
||||||
for (const module of modules) {
|
for (const module of modules) {
|
||||||
await this.ensureDir(path.join(bmadRulesDir, module));
|
await this.ensureDir(path.join(bmadCommandsDir, module));
|
||||||
await this.ensureDir(path.join(bmadRulesDir, module, 'agents'));
|
await this.ensureDir(path.join(bmadCommandsDir, module, 'agents'));
|
||||||
await this.ensureDir(path.join(bmadRulesDir, module, 'tasks'));
|
|
||||||
await this.ensureDir(path.join(bmadRulesDir, module, 'tools'));
|
|
||||||
await this.ensureDir(path.join(bmadRulesDir, module, 'workflows'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process and write agent launchers with MDC format
|
// Write agent launcher files
|
||||||
let agentCount = 0;
|
const agentCount = await agentGen.writeAgentLaunchers(bmadCommandsDir, agentArtifacts);
|
||||||
for (const artifact of agentArtifacts) {
|
|
||||||
// Add MDC metadata header to launcher (but don't call processContent which adds activation headers)
|
|
||||||
const content = this.wrapLauncherWithMDC(artifact.content, {
|
|
||||||
module: artifact.module,
|
|
||||||
name: artifact.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const targetPath = path.join(bmadRulesDir, artifact.module, 'agents', `${artifact.name}.mdc`);
|
// Generate workflow commands from manifest (if it exists)
|
||||||
|
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||||
|
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
||||||
|
|
||||||
await this.writeFile(targetPath, content);
|
// Write only workflow-command artifacts, skip workflow-launcher READMEs
|
||||||
agentCount++;
|
let workflowCommandCount = 0;
|
||||||
}
|
|
||||||
|
|
||||||
// Process and copy tasks
|
|
||||||
let taskCount = 0;
|
|
||||||
for (const task of tasks) {
|
|
||||||
const content = await this.readAndProcess(task.path, {
|
|
||||||
module: task.module,
|
|
||||||
name: task.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const targetPath = path.join(bmadRulesDir, task.module, 'tasks', `${task.name}.mdc`);
|
|
||||||
|
|
||||||
await this.writeFile(targetPath, content);
|
|
||||||
taskCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process and copy tools
|
|
||||||
let toolCount = 0;
|
|
||||||
for (const tool of tools) {
|
|
||||||
const content = await this.readAndProcess(tool.path, {
|
|
||||||
module: tool.module,
|
|
||||||
name: tool.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const targetPath = path.join(bmadRulesDir, tool.module, 'tools', `${tool.name}.mdc`);
|
|
||||||
|
|
||||||
await this.writeFile(targetPath, content);
|
|
||||||
toolCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process and copy workflow commands (generated, not raw workflows)
|
|
||||||
let workflowCount = 0;
|
|
||||||
for (const artifact of workflowArtifacts) {
|
for (const artifact of workflowArtifacts) {
|
||||||
if (artifact.type === 'workflow-command') {
|
if (artifact.type === 'workflow-command') {
|
||||||
// Add MDC metadata header to workflow command
|
const moduleWorkflowsDir = path.join(bmadCommandsDir, artifact.module, 'workflows');
|
||||||
const content = this.wrapLauncherWithMDC(artifact.content, {
|
await this.ensureDir(moduleWorkflowsDir);
|
||||||
module: artifact.module,
|
const commandPath = path.join(moduleWorkflowsDir, path.basename(artifact.relativePath));
|
||||||
name: path.basename(artifact.relativePath, '.md'),
|
await this.writeFile(commandPath, artifact.content);
|
||||||
});
|
workflowCommandCount++;
|
||||||
|
|
||||||
const targetPath = path.join(bmadRulesDir, artifact.module, 'workflows', `${path.basename(artifact.relativePath, '.md')}.mdc`);
|
|
||||||
|
|
||||||
await this.writeFile(targetPath, content);
|
|
||||||
workflowCount++;
|
|
||||||
}
|
}
|
||||||
|
// Skip workflow-launcher READMEs as they would be treated as slash commands
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create BMAD index file (but NOT .cursorrules - user manages that)
|
// Generate task and tool commands from manifests (if they exist)
|
||||||
await this.createBMADIndex(bmadRulesDir, agents, tasks, tools, workflows, modules);
|
const taskToolGen = new TaskToolCommandGenerator();
|
||||||
|
const taskToolResult = await taskToolGen.generateTaskToolCommands(projectDir, bmadDir, bmadCommandsDir);
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||||
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
||||||
console.log(chalk.dim(` - ${taskCount} tasks installed`));
|
if (workflowCommandCount > 0) {
|
||||||
console.log(chalk.dim(` - ${toolCount} tools installed`));
|
console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`));
|
||||||
console.log(chalk.dim(` - ${workflowCount} workflows installed`));
|
}
|
||||||
console.log(chalk.dim(` - Rules directory: ${path.relative(projectDir, bmadRulesDir)}`));
|
if (taskToolResult.generated > 0) {
|
||||||
|
console.log(
|
||||||
|
chalk.dim(
|
||||||
|
` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, bmadCommandsDir)}`));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
agents: agentCount,
|
agents: agentCount,
|
||||||
tasks: taskCount,
|
tasks: taskToolResult.tasks || 0,
|
||||||
tools: toolCount,
|
tools: taskToolResult.tools || 0,
|
||||||
workflows: workflowCount,
|
workflows: workflowCommandCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create BMAD index file for easy navigation
|
|
||||||
*/
|
|
||||||
async createBMADIndex(bmadRulesDir, agents, tasks, tools, workflows, modules) {
|
|
||||||
const indexPath = path.join(bmadRulesDir, 'index.mdc');
|
|
||||||
|
|
||||||
let content = `---
|
|
||||||
description: BMAD Method - Master Index
|
|
||||||
globs:
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# BMAD Method - Cursor Rules Index
|
|
||||||
|
|
||||||
This is the master index for all BMAD agents, tasks, tools, and workflows available in your project.
|
|
||||||
|
|
||||||
## Installation Complete!
|
|
||||||
|
|
||||||
BMAD rules have been installed to: \`.cursor/rules/bmad/\`
|
|
||||||
|
|
||||||
**Note:** BMAD does not modify your \`.cursorrules\` file. You manage that separately.
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
|
|
||||||
- Reference specific agents: @bmad/{module}/agents/{agent-name}
|
|
||||||
- Reference specific tasks: @bmad/{module}/tasks/{task-name}
|
|
||||||
- Reference specific tools: @bmad/{module}/tools/{tool-name}
|
|
||||||
- Reference specific workflows: @bmad/{module}/workflows/{workflow-name}
|
|
||||||
- Reference entire modules: @bmad/{module}
|
|
||||||
- Reference this index: @bmad/index
|
|
||||||
|
|
||||||
## Available Modules
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
for (const module of modules) {
|
|
||||||
content += `### ${module.toUpperCase()}\n\n`;
|
|
||||||
|
|
||||||
// List agents for this module
|
|
||||||
const moduleAgents = agents.filter((a) => a.module === module);
|
|
||||||
if (moduleAgents.length > 0) {
|
|
||||||
content += `**Agents:**\n`;
|
|
||||||
for (const agent of moduleAgents) {
|
|
||||||
content += `- @bmad/${module}/agents/${agent.name} - ${agent.name}\n`;
|
|
||||||
}
|
|
||||||
content += '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
// List tasks for this module
|
|
||||||
const moduleTasks = tasks.filter((t) => t.module === module);
|
|
||||||
if (moduleTasks.length > 0) {
|
|
||||||
content += `**Tasks:**\n`;
|
|
||||||
for (const task of moduleTasks) {
|
|
||||||
content += `- @bmad/${module}/tasks/${task.name} - ${task.name}\n`;
|
|
||||||
}
|
|
||||||
content += '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
// List tools for this module
|
|
||||||
const moduleTools = tools.filter((t) => t.module === module);
|
|
||||||
if (moduleTools.length > 0) {
|
|
||||||
content += `**Tools:**\n`;
|
|
||||||
for (const tool of moduleTools) {
|
|
||||||
content += `- @bmad/${module}/tools/${tool.name} - ${tool.name}\n`;
|
|
||||||
}
|
|
||||||
content += '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
// List workflows for this module
|
|
||||||
const moduleWorkflows = workflows.filter((w) => w.module === module);
|
|
||||||
if (moduleWorkflows.length > 0) {
|
|
||||||
content += `**Workflows:**\n`;
|
|
||||||
for (const workflow of moduleWorkflows) {
|
|
||||||
content += `- @bmad/${module}/workflows/${workflow.name} - ${workflow.name}\n`;
|
|
||||||
}
|
|
||||||
content += '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content += `
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
- All BMAD rules are Manual type - reference them explicitly when needed
|
|
||||||
- Agents provide persona-based assistance with specific expertise
|
|
||||||
- Tasks are reusable workflows for common operations
|
|
||||||
- Tools provide specialized functionality
|
|
||||||
- Workflows orchestrate multi-step processes
|
|
||||||
- Each agent includes an activation block for proper initialization
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
BMAD rules are configured as Manual rules (alwaysApply: false) to give you control
|
|
||||||
over when they're included in your context. Reference them explicitly when you need
|
|
||||||
specific agent expertise, task workflows, tools, or guided workflows.
|
|
||||||
`;
|
|
||||||
|
|
||||||
await this.writeFile(indexPath, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read and process file content
|
|
||||||
*/
|
|
||||||
async readAndProcess(filePath, metadata) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
return this.processContent(content, metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override processContent to add MDC metadata header for Cursor
|
|
||||||
* @param {string} content - File content
|
|
||||||
* @param {Object} metadata - File metadata
|
|
||||||
* @returns {string} Processed content with MDC header
|
|
||||||
*/
|
|
||||||
processContent(content, metadata = {}) {
|
|
||||||
// First apply base processing (includes activation injection for agents)
|
|
||||||
let processed = super.processContent(content, metadata);
|
|
||||||
|
|
||||||
// Strip any existing frontmatter from the processed content
|
|
||||||
// This prevents duplicate frontmatter blocks
|
|
||||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
|
||||||
if (frontmatterRegex.test(processed)) {
|
|
||||||
processed = processed.replace(frontmatterRegex, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the type and description based on content
|
|
||||||
const isAgent = content.includes('<agent');
|
|
||||||
const isTask = content.includes('<task');
|
|
||||||
const isTool = content.includes('<tool');
|
|
||||||
const isWorkflow = content.includes('workflow:') || content.includes('name:');
|
|
||||||
|
|
||||||
let description = '';
|
|
||||||
let globs = '';
|
|
||||||
|
|
||||||
if (isAgent) {
|
|
||||||
// Extract agent title if available
|
|
||||||
const titleMatch = content.match(/title="([^"]+)"/);
|
|
||||||
const title = titleMatch ? titleMatch[1] : metadata.name;
|
|
||||||
description = `BMAD ${metadata.module.toUpperCase()} Agent: ${title}`;
|
|
||||||
globs = '';
|
|
||||||
} else if (isTask) {
|
|
||||||
// Extract task name if available
|
|
||||||
const nameMatch = content.match(/name="([^"]+)"/);
|
|
||||||
const taskName = nameMatch ? nameMatch[1] : metadata.name;
|
|
||||||
description = `BMAD ${metadata.module.toUpperCase()} Task: ${taskName}`;
|
|
||||||
globs = '';
|
|
||||||
} else if (isTool) {
|
|
||||||
// Extract tool name if available
|
|
||||||
const nameMatch = content.match(/name="([^"]+)"/);
|
|
||||||
const toolName = nameMatch ? nameMatch[1] : metadata.name;
|
|
||||||
description = `BMAD ${metadata.module.toUpperCase()} Tool: ${toolName}`;
|
|
||||||
globs = '';
|
|
||||||
} else if (isWorkflow) {
|
|
||||||
// Workflow
|
|
||||||
description = `BMAD ${metadata.module.toUpperCase()} Workflow: ${metadata.name}`;
|
|
||||||
globs = '';
|
|
||||||
} else {
|
|
||||||
description = `BMAD ${metadata.module.toUpperCase()}: ${metadata.name}`;
|
|
||||||
globs = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create MDC metadata header
|
|
||||||
const mdcHeader = `---
|
|
||||||
description: ${description}
|
|
||||||
globs: ${globs}
|
|
||||||
alwaysApply: false
|
|
||||||
---
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add the MDC header to the processed content
|
|
||||||
return mdcHeader + processed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrap launcher content with MDC metadata (without base processing)
|
|
||||||
* Launchers are already complete and should not have activation headers injected
|
|
||||||
*/
|
|
||||||
wrapLauncherWithMDC(launcherContent, metadata = {}) {
|
|
||||||
// Strip the launcher's frontmatter - we'll replace it with MDC frontmatter
|
|
||||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
|
||||||
const contentWithoutFrontmatter = launcherContent.replace(frontmatterRegex, '');
|
|
||||||
|
|
||||||
// Extract metadata from launcher frontmatter for MDC description
|
|
||||||
const nameMatch = launcherContent.match(/name:\s*"([^"]+)"/);
|
|
||||||
const name = nameMatch ? nameMatch[1] : metadata.name;
|
|
||||||
|
|
||||||
const description = `BMAD ${metadata.module.toUpperCase()} Agent: ${name}`;
|
|
||||||
|
|
||||||
// Create MDC metadata header
|
|
||||||
const mdcHeader = `---
|
|
||||||
description: ${description}
|
|
||||||
globs:
|
|
||||||
alwaysApply: false
|
|
||||||
---
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Return MDC header + launcher content (without its original frontmatter)
|
|
||||||
return mdcHeader + contentWithoutFrontmatter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install a custom agent launcher for Cursor
|
* Install a custom agent launcher for Cursor
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
|
|
@ -373,7 +127,7 @@ alwaysApply: false
|
||||||
* @returns {Object|null} Info about created command
|
* @returns {Object|null} Info about created command
|
||||||
*/
|
*/
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||||
const customAgentsDir = path.join(projectDir, this.configDir, this.rulesDir, 'bmad', 'custom', 'agents');
|
const customAgentsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad', 'custom', 'agents');
|
||||||
|
|
||||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
||||||
return null; // IDE not configured for this project
|
return null; // IDE not configured for this project
|
||||||
|
|
@ -393,22 +147,21 @@ alwaysApply: false
|
||||||
</agent-activation>
|
</agent-activation>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Cursor uses MDC format with metadata header
|
// Cursor uses YAML frontmatter matching Claude Code format
|
||||||
const mdcContent = `---
|
const commandContent = `---
|
||||||
description: "${agentName} agent"
|
name: '${agentName}'
|
||||||
globs:
|
description: '${agentName} agent'
|
||||||
alwaysApply: false
|
|
||||||
---
|
---
|
||||||
|
|
||||||
${launcherContent}
|
${launcherContent}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const launcherPath = path.join(customAgentsDir, `${agentName}.mdc`);
|
const launcherPath = path.join(customAgentsDir, `${agentName}.md`);
|
||||||
await this.writeFile(launcherPath, mdcContent);
|
await this.writeFile(launcherPath, commandContent);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: launcherPath,
|
path: launcherPath,
|
||||||
command: `@${agentName}`,
|
command: `/bmad/custom/agents/${agentName}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
const { BaseIdeSetup } = require('./_base-ide');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -22,6 +21,7 @@ class GitHubCopilotSetup extends BaseIdeSetup {
|
||||||
* @returns {Object} Collected configuration
|
* @returns {Object} Collected configuration
|
||||||
*/
|
*/
|
||||||
async collectConfiguration(options = {}) {
|
async collectConfiguration(options = {}) {
|
||||||
|
const { default: inquirer } = await import('inquirer');
|
||||||
const config = {};
|
const config = {};
|
||||||
|
|
||||||
console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration'));
|
console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration'));
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const inquirer = require('inquirer').default || require('inquirer');
|
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { CLIUtils } = require('./cli-utils');
|
const { CLIUtils } = require('./cli-utils');
|
||||||
const { CustomHandler } = require('../installers/lib/custom/handler');
|
const { CustomHandler } = require('../installers/lib/custom/handler');
|
||||||
|
|
||||||
|
// Lazy-load inquirer (ESM module) to avoid ERR_REQUIRE_ESM
|
||||||
|
let _inquirer = null;
|
||||||
|
async function getInquirer() {
|
||||||
|
if (!_inquirer) {
|
||||||
|
_inquirer = (await import('inquirer')).default;
|
||||||
|
}
|
||||||
|
return _inquirer;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI utilities for the installer
|
* UI utilities for the installer
|
||||||
*/
|
*/
|
||||||
|
|
@ -15,6 +23,7 @@ class UI {
|
||||||
* @returns {Object} Installation configuration
|
* @returns {Object} Installation configuration
|
||||||
*/
|
*/
|
||||||
async promptInstall() {
|
async promptInstall() {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
CLIUtils.displayLogo();
|
CLIUtils.displayLogo();
|
||||||
|
|
||||||
// Display version-specific start message from install-messages.yaml
|
// Display version-specific start message from install-messages.yaml
|
||||||
|
|
@ -450,6 +459,7 @@ class UI {
|
||||||
* @returns {Object} Tool configuration
|
* @returns {Object} Tool configuration
|
||||||
*/
|
*/
|
||||||
async promptToolSelection(projectDir, selectedModules) {
|
async promptToolSelection(projectDir, selectedModules) {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
// Check for existing configured IDEs - use findBmadDir to detect custom folder names
|
// Check for existing configured IDEs - use findBmadDir to detect custom folder names
|
||||||
const { Detector } = require('../installers/lib/core/detector');
|
const { Detector } = require('../installers/lib/core/detector');
|
||||||
const { Installer } = require('../installers/lib/core/installer');
|
const { Installer } = require('../installers/lib/core/installer');
|
||||||
|
|
@ -582,6 +592,7 @@ class UI {
|
||||||
* @returns {Object} Update configuration
|
* @returns {Object} Update configuration
|
||||||
*/
|
*/
|
||||||
async promptUpdate() {
|
async promptUpdate() {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
const answers = await inquirer.prompt([
|
const answers = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
|
|
@ -606,6 +617,7 @@ class UI {
|
||||||
* @returns {Array} Selected modules
|
* @returns {Array} Selected modules
|
||||||
*/
|
*/
|
||||||
async promptModules(modules) {
|
async promptModules(modules) {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
const choices = modules.map((mod) => ({
|
const choices = modules.map((mod) => ({
|
||||||
name: `${mod.name} - ${mod.description}`,
|
name: `${mod.name} - ${mod.description}`,
|
||||||
value: mod.id,
|
value: mod.id,
|
||||||
|
|
@ -637,6 +649,7 @@ class UI {
|
||||||
* @returns {boolean} User confirmation
|
* @returns {boolean} User confirmation
|
||||||
*/
|
*/
|
||||||
async confirm(message, defaultValue = false) {
|
async confirm(message, defaultValue = false) {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
const { confirmed } = await inquirer.prompt([
|
const { confirmed } = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
|
|
@ -743,6 +756,7 @@ class UI {
|
||||||
* @returns {Array} Module choices for inquirer
|
* @returns {Array} Module choices for inquirer
|
||||||
*/
|
*/
|
||||||
async getModuleChoices(installedModuleIds, customContentConfig = null) {
|
async getModuleChoices(installedModuleIds, customContentConfig = null) {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
const moduleChoices = [];
|
const moduleChoices = [];
|
||||||
const isNewInstallation = installedModuleIds.size === 0;
|
const isNewInstallation = installedModuleIds.size === 0;
|
||||||
|
|
||||||
|
|
@ -823,6 +837,7 @@ class UI {
|
||||||
* @returns {Array} Selected module IDs
|
* @returns {Array} Selected module IDs
|
||||||
*/
|
*/
|
||||||
async selectModules(moduleChoices, defaultSelections = []) {
|
async selectModules(moduleChoices, defaultSelections = []) {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
const moduleAnswer = await inquirer.prompt([
|
const moduleAnswer = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
|
|
@ -843,6 +858,7 @@ class UI {
|
||||||
* @returns {Object} Directory answer from inquirer
|
* @returns {Object} Directory answer from inquirer
|
||||||
*/
|
*/
|
||||||
async promptForDirectory() {
|
async promptForDirectory() {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
return await inquirer.prompt([
|
return await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
|
|
@ -899,6 +915,7 @@ class UI {
|
||||||
* @returns {boolean} Whether user confirmed
|
* @returns {boolean} Whether user confirmed
|
||||||
*/
|
*/
|
||||||
async confirmDirectory(directory) {
|
async confirmDirectory(directory) {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
const dirExists = await fs.pathExists(directory);
|
const dirExists = await fs.pathExists(directory);
|
||||||
|
|
||||||
if (dirExists) {
|
if (dirExists) {
|
||||||
|
|
@ -1085,6 +1102,7 @@ class UI {
|
||||||
* - GitHub Issue: paulpreibisch/AgentVibes#36
|
* - GitHub Issue: paulpreibisch/AgentVibes#36
|
||||||
*/
|
*/
|
||||||
async promptAgentVibes(projectDir) {
|
async promptAgentVibes(projectDir) {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
CLIUtils.displaySection('🎤 Voice Features', 'Enable TTS for multi-agent conversations');
|
CLIUtils.displaySection('🎤 Voice Features', 'Enable TTS for multi-agent conversations');
|
||||||
|
|
||||||
// Check if AgentVibes is already installed
|
// Check if AgentVibes is already installed
|
||||||
|
|
@ -1235,6 +1253,7 @@ class UI {
|
||||||
* @returns {Object} Custom content configuration
|
* @returns {Object} Custom content configuration
|
||||||
*/
|
*/
|
||||||
async promptCustomContentSource() {
|
async promptCustomContentSource() {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
const customContentConfig = { hasCustomContent: true, sources: [] };
|
const customContentConfig = { hasCustomContent: true, sources: [] };
|
||||||
|
|
||||||
// Keep asking for more sources until user is done
|
// Keep asking for more sources until user is done
|
||||||
|
|
@ -1372,6 +1391,7 @@ class UI {
|
||||||
* @returns {Object} Result with selected custom modules and custom content config
|
* @returns {Object} Result with selected custom modules and custom content config
|
||||||
*/
|
*/
|
||||||
async handleCustomModulesInModifyFlow(directory, selectedModules) {
|
async handleCustomModulesInModifyFlow(directory, selectedModules) {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
// Get existing installation to find custom modules
|
// Get existing installation to find custom modules
|
||||||
const { existingInstall } = await this.getExistingInstallation(directory);
|
const { existingInstall } = await this.getExistingInstallation(directory);
|
||||||
|
|
||||||
|
|
@ -1566,6 +1586,7 @@ class UI {
|
||||||
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
|
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
|
||||||
*/
|
*/
|
||||||
async showOldAlphaVersionWarning(installedVersion, currentVersion, bmadFolderName) {
|
async showOldAlphaVersionWarning(installedVersion, currentVersion, bmadFolderName) {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
const versionInfo = this.checkAlphaVersionAge(installedVersion, currentVersion);
|
const versionInfo = this.checkAlphaVersionAge(installedVersion, currentVersion);
|
||||||
|
|
||||||
// Also warn if version is unknown or can't be parsed (legacy/unsupported)
|
// Also warn if version is unknown or can't be parsed (legacy/unsupported)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue