Compare commits
5 Commits
1c6c72e874
...
be2f51b899
| Author | SHA1 | Date |
|---|---|---|
|
|
be2f51b899 | |
|
|
df176d4206 | |
|
|
2d9ebcaf2f | |
|
|
5b80649d3a | |
|
|
bd58d06d1c |
|
|
@ -0,0 +1,241 @@
|
||||||
|
# Augment Code Review Guidelines for BMad-CORE
|
||||||
|
# https://docs.augmentcode.com/codereview/overview
|
||||||
|
# Focus: Workflow validation and quality
|
||||||
|
|
||||||
|
file_paths_to_ignore:
|
||||||
|
- "node_modules/**"
|
||||||
|
- "build/**"
|
||||||
|
- "dist/**"
|
||||||
|
- "**/*.min.js"
|
||||||
|
- "**/*.generated.*"
|
||||||
|
- "**/*.bundle.md"
|
||||||
|
- "**/validation-report-*.md"
|
||||||
|
- "package-lock.json"
|
||||||
|
- "*.png"
|
||||||
|
- "*.jpg"
|
||||||
|
- "*.svg"
|
||||||
|
- "CHANGELOG.md"
|
||||||
|
|
||||||
|
areas:
|
||||||
|
# ============================================
|
||||||
|
# WORKFLOW STRUCTURE RULES
|
||||||
|
# ============================================
|
||||||
|
workflow_structure:
|
||||||
|
description: "Workflow folder organization and required components"
|
||||||
|
globs:
|
||||||
|
- "src/**/workflows/**"
|
||||||
|
rules:
|
||||||
|
- id: "workflow_entry_point_required"
|
||||||
|
description: "Every workflow folder must have workflow.yaml, workflow.md, or workflow.xml as entry point"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
- id: "sharded_workflow_steps_folder"
|
||||||
|
description: "Sharded workflows (using workflow.md) must have steps/ folder with numbered files (step-01-*.md, step-02-*.md)"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
- id: "standard_workflow_instructions"
|
||||||
|
description: "Standard workflows using workflow.yaml must include instructions.md for execution guidance"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "workflow_step_limit"
|
||||||
|
description: "Workflows should have 5-10 steps maximum to prevent context loss in LLM execution"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# WORKFLOW ENTRY FILE RULES
|
||||||
|
# ============================================
|
||||||
|
workflow_definitions:
|
||||||
|
description: "Workflow entry files (workflow.yaml, workflow.md, workflow.xml)"
|
||||||
|
globs:
|
||||||
|
- "src/**/workflows/**/workflow.yaml"
|
||||||
|
- "src/**/workflows/**/workflow.md"
|
||||||
|
- "src/**/workflows/**/workflow.xml"
|
||||||
|
rules:
|
||||||
|
- id: "workflow_name_required"
|
||||||
|
description: "Workflow entry files must define 'name' field in frontmatter or root element"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
- id: "workflow_description_required"
|
||||||
|
description: "Workflow entry files must include 'description' explaining the workflow's purpose"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
- id: "workflow_config_source"
|
||||||
|
description: "Workflows should reference config_source for variable resolution (e.g., {project-root}/_bmad/module/config.yaml)"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "workflow_installed_path"
|
||||||
|
description: "Workflows should define installed_path for relative file references within the workflow"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "valid_step_references"
|
||||||
|
description: "Step file references in workflow entry must point to existing files"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# SHARDED WORKFLOW STEP RULES
|
||||||
|
# ============================================
|
||||||
|
workflow_steps:
|
||||||
|
description: "Individual step files in sharded workflows"
|
||||||
|
globs:
|
||||||
|
- "src/**/workflows/**/steps/step-*.md"
|
||||||
|
rules:
|
||||||
|
- id: "step_goal_required"
|
||||||
|
description: "Each step must clearly state its goal (## STEP GOAL, ## YOUR TASK, or step n='X' goal='...')"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
- id: "step_mandatory_rules"
|
||||||
|
description: "Step files should include MANDATORY EXECUTION RULES section with universal agent behavior rules"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "step_context_boundaries"
|
||||||
|
description: "Step files should define CONTEXT BOUNDARIES explaining available context and limits"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "step_success_metrics"
|
||||||
|
description: "Step files should include SUCCESS METRICS section with ✅ checkmarks for validation criteria"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "step_failure_modes"
|
||||||
|
description: "Step files should include FAILURE MODES section with ❌ marks for anti-patterns to avoid"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "step_next_step_reference"
|
||||||
|
description: "Step files should reference the next step file path for sequential execution"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "step_no_forward_loading"
|
||||||
|
description: "Steps must NOT load future step files until current step completes - just-in-time loading only"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
- id: "valid_file_references"
|
||||||
|
description: "File path references using {variable}/filename.md must point to existing files"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
- id: "step_naming"
|
||||||
|
description: "Step files must be named step-NN-description.md (e.g., step-01-init.md, step-02-context.md)"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "halt_before_menu"
|
||||||
|
description: "Steps presenting user menus ([C] Continue, [a] Advanced, etc.) must HALT and wait for response"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# XML WORKFLOW/TASK RULES
|
||||||
|
# ============================================
|
||||||
|
xml_workflows:
|
||||||
|
description: "XML-based workflows and tasks"
|
||||||
|
globs:
|
||||||
|
- "src/**/workflows/**/*.xml"
|
||||||
|
- "src/**/tasks/**/*.xml"
|
||||||
|
rules:
|
||||||
|
- id: "xml_task_id_required"
|
||||||
|
description: "XML tasks must have unique 'id' attribute on root task element"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
- id: "xml_llm_instructions"
|
||||||
|
description: "XML workflows should include <llm> section with critical execution instructions for the agent"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "xml_step_numbering"
|
||||||
|
description: "XML steps should use n='X' attribute for sequential numbering"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "xml_action_tags"
|
||||||
|
description: "Use <action> for required actions, <ask> for user input (must HALT), <goto> for jumps, <check if='...'> for conditionals"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "xml_ask_must_halt"
|
||||||
|
description: "<ask> tags require agent to HALT and wait for user response before continuing"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# WORKFLOW CONTENT QUALITY
|
||||||
|
# ============================================
|
||||||
|
workflow_content:
|
||||||
|
description: "Content quality and consistency rules for all workflow files"
|
||||||
|
globs:
|
||||||
|
- "src/**/workflows/**/*.md"
|
||||||
|
- "src/**/workflows/**/*.yaml"
|
||||||
|
rules:
|
||||||
|
- id: "communication_language_variable"
|
||||||
|
description: "Workflows should use {communication_language} variable for agent output language consistency"
|
||||||
|
severity: "low"
|
||||||
|
|
||||||
|
- id: "path_placeholders_required"
|
||||||
|
description: "Use path placeholders ({project-root}, {installed_path}, {output_folder}) instead of hardcoded paths"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "no_time_estimates"
|
||||||
|
description: "Workflows should NOT include time estimates - AI development speed varies significantly"
|
||||||
|
severity: "low"
|
||||||
|
|
||||||
|
- id: "facilitator_not_generator"
|
||||||
|
description: "Workflow agents should act as facilitators (guide user input) not content generators (create without input)"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "no_skip_optimization"
|
||||||
|
description: "Workflows must execute steps sequentially - no skipping or 'optimizing' step order"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# AGENT DEFINITIONS
|
||||||
|
# ============================================
|
||||||
|
agent_definitions:
|
||||||
|
description: "Agent YAML configuration files"
|
||||||
|
globs:
|
||||||
|
- "src/**/*.agent.yaml"
|
||||||
|
rules:
|
||||||
|
- id: "agent_metadata_required"
|
||||||
|
description: "Agent files must have metadata section with id, name, title, icon, and module"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
- id: "agent_persona_required"
|
||||||
|
description: "Agent files must define persona with role, identity, communication_style, and principles"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
- id: "agent_menu_valid_workflows"
|
||||||
|
description: "Menu triggers must reference valid workflow paths that exist"
|
||||||
|
severity: "high"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# TEMPLATES
|
||||||
|
# ============================================
|
||||||
|
templates:
|
||||||
|
description: "Template files for workflow outputs"
|
||||||
|
globs:
|
||||||
|
- "src/**/template*.md"
|
||||||
|
- "src/**/templates/**/*.md"
|
||||||
|
rules:
|
||||||
|
- id: "placeholder_syntax"
|
||||||
|
description: "Use {variable_name} or {{variable_name}} syntax consistently for placeholders"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
- id: "template_sections_marked"
|
||||||
|
description: "Template sections that need generation should be clearly marked (e.g., <!-- GENERATE: section_name -->)"
|
||||||
|
severity: "low"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# DOCUMENTATION
|
||||||
|
# ============================================
|
||||||
|
documentation:
|
||||||
|
description: "Documentation files"
|
||||||
|
globs:
|
||||||
|
- "docs/**/*.md"
|
||||||
|
- "README.md"
|
||||||
|
- "CONTRIBUTING.md"
|
||||||
|
rules:
|
||||||
|
- id: "valid_internal_links"
|
||||||
|
description: "Internal markdown links must point to existing files"
|
||||||
|
severity: "medium"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# BUILD TOOLS
|
||||||
|
# ============================================
|
||||||
|
build_tools:
|
||||||
|
description: "Build scripts and tooling"
|
||||||
|
globs:
|
||||||
|
- "tools/**"
|
||||||
|
rules:
|
||||||
|
- id: "script_error_handling"
|
||||||
|
description: "Scripts should handle errors gracefully with proper exit codes"
|
||||||
|
severity: "medium"
|
||||||
|
|
@ -21,17 +21,22 @@ reviews:
|
||||||
path_instructions:
|
path_instructions:
|
||||||
- path: "**/*"
|
- path: "**/*"
|
||||||
instructions: |
|
instructions: |
|
||||||
Focus on inconsistencies, contradictions, edge cases and serious issues.
|
You are a cynical, jaded reviewer with zero patience for sloppy work.
|
||||||
Avoid commenting on minor issues such as linting, formatting and style issues.
|
This PR was submitted by a clueless weasel and you expect to find problems.
|
||||||
When providing code suggestions, use GitHub's suggestion format:
|
Be skeptical of everything.
|
||||||
```suggestion
|
Look for what's missing, not just what's wrong.
|
||||||
<code changes>
|
Use a precise, professional tone — no profanity or personal attacks.
|
||||||
```
|
|
||||||
- path: "**/*.js"
|
Review with extreme skepticism — assume problems exist.
|
||||||
instructions: |
|
Find at least 10 issues to fix or improve.
|
||||||
CLI tooling code. Check for: missing error handling on fs operations,
|
|
||||||
path.join vs string concatenation, proper cleanup in error paths.
|
Do NOT:
|
||||||
Flag any process.exit() without error message.
|
- Comment on formatting, linting, or style
|
||||||
|
- Give "looks good" passes
|
||||||
|
- Anchor on any specific ruleset — reason freely
|
||||||
|
|
||||||
|
If you find zero issues, re-analyze — this is suspicious.
|
||||||
|
Some findings may be noise — that's acceptable. The human will filter.
|
||||||
chat:
|
chat:
|
||||||
auto_reply: true # Response to mentions in comments, a la @coderabbit review
|
auto_reply: true # Response to mentions in comments, a la @coderabbit review
|
||||||
issue_enrichment:
|
issue_enrichment:
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@ z*/
|
||||||
_bmad
|
_bmad
|
||||||
_bmad-output
|
_bmad-output
|
||||||
.clinerules
|
.clinerules
|
||||||
.augment
|
.augment/*
|
||||||
|
!.augment/code_review_guidelines.yaml
|
||||||
.crush
|
.crush
|
||||||
.cursor
|
.cursor
|
||||||
.iflow
|
.iflow
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ export default [
|
||||||
'tools/template-test-generator/test-scenarios/**',
|
'tools/template-test-generator/test-scenarios/**',
|
||||||
'src/modules/*/sub-modules/**',
|
'src/modules/*/sub-modules/**',
|
||||||
'.bundler-temp/**',
|
'.bundler-temp/**',
|
||||||
|
// Augment Code Review config uses underscores per their spec
|
||||||
|
'.augment/**',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@
|
||||||
"version": "6.0.0-Beta.5",
|
"version": "6.0.0-Beta.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.11.0",
|
"@clack/core": "^1.0.0",
|
||||||
|
"@clack/prompts": "^1.0.0",
|
||||||
"@kayvan/markdown-tree-parser": "^1.6.1",
|
"@kayvan/markdown-tree-parser": "^1.6.1",
|
||||||
"boxen": "^5.1.2",
|
"boxen": "^5.1.2",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
|
|
@ -22,6 +23,7 @@
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"ora": "^5.4.1",
|
"ora": "^5.4.1",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"wrap-ansi": "^7.0.0",
|
"wrap-ansi": "^7.0.0",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
|
|
@ -756,9 +758,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@clack/core": {
|
"node_modules/@clack/core": {
|
||||||
"version": "0.5.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0.tgz",
|
||||||
"integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==",
|
"integrity": "sha512-Orf9Ltr5NeiEuVJS8Rk2XTw3IxNC2Bic3ash7GgYeA8LJ/zmSNpSQ/m5UAhe03lA6KFgklzZ5KTHs4OAMA/SAQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
|
|
@ -766,12 +768,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@clack/prompts": {
|
"node_modules/@clack/prompts": {
|
||||||
"version": "0.11.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.0.0.tgz",
|
||||||
"integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==",
|
"integrity": "sha512-rWPXg9UaCFqErJVQ+MecOaWsozjaxol4yjnmYcGNipAWzdaWa2x+VJmKfGq7L0APwBohQOYdHC+9RO4qRXej+A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/core": "0.5.0",
|
"@clack/core": "1.0.0",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
"sisteransi": "^1.0.5"
|
"sisteransi": "^1.0.5"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,8 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.11.0",
|
"@clack/core": "^1.0.0",
|
||||||
|
"@clack/prompts": "^1.0.0",
|
||||||
"@kayvan/markdown-tree-parser": "^1.6.1",
|
"@kayvan/markdown-tree-parser": "^1.6.1",
|
||||||
"boxen": "^5.1.2",
|
"boxen": "^5.1.2",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
|
|
@ -82,6 +83,7 @@
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"ora": "^5.4.1",
|
"ora": "^5.4.1",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"wrap-ansi": "^7.0.0",
|
"wrap-ansi": "^7.0.0",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<task id="_bmad/core/tasks/workflow.xml" name="Execute Workflow" standalone="false">
|
<task id="_bmad/core/tasks/workflow.xml" name="Execute Workflow" standalone="false" internal="true">
|
||||||
<objective>Execute given workflow by loading its configuration, following instructions, and producing output</objective>
|
<objective>Execute given workflow by loading its configuration, following instructions, and producing output</objective>
|
||||||
|
|
||||||
<llm critical="true">
|
<llm critical="true">
|
||||||
|
|
|
||||||
|
|
@ -586,7 +586,11 @@ class ConfigCollector {
|
||||||
console.log();
|
console.log();
|
||||||
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
||||||
let customize = true;
|
let customize = true;
|
||||||
if (moduleName !== 'core') {
|
if (moduleName === 'core') {
|
||||||
|
// 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([
|
const customizeAnswer = await prompts.prompt([
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ class DependencyResolver {
|
||||||
const content = await fs.readFile(file.path, 'utf8');
|
const content = await fs.readFile(file.path, 'utf8');
|
||||||
|
|
||||||
// Parse YAML frontmatter for explicit dependencies
|
// Parse YAML frontmatter for explicit dependencies
|
||||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||||
if (frontmatterMatch) {
|
if (frontmatterMatch) {
|
||||||
try {
|
try {
|
||||||
// Pre-process to handle backticks in YAML values
|
// Pre-process to handle backticks in YAML values
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,7 @@ const { ManifestGenerator } = require('./manifest-generator');
|
||||||
const { IdeConfigManager } = require('./ide-config-manager');
|
const { IdeConfigManager } = require('./ide-config-manager');
|
||||||
const { CustomHandler } = require('../custom/handler');
|
const { CustomHandler } = require('../custom/handler');
|
||||||
const prompts = require('../../../lib/prompts');
|
const prompts = require('../../../lib/prompts');
|
||||||
|
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
||||||
// BMAD installation folder name - this is constant and should never change
|
|
||||||
const BMAD_FOLDER_NAME = '_bmad';
|
|
||||||
|
|
||||||
class Installer {
|
class Installer {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -697,9 +695,6 @@ class Installer {
|
||||||
config.skipIde = toolSelection.skipIde;
|
config.skipIde = toolSelection.skipIde;
|
||||||
const ideConfigurations = toolSelection.configurations;
|
const ideConfigurations = toolSelection.configurations;
|
||||||
|
|
||||||
// Add spacing after prompts before installation progress
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
if (spinner.isSpinning) {
|
if (spinner.isSpinning) {
|
||||||
spinner.text = 'Continuing installation...';
|
spinner.text = 'Continuing installation...';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const crypto = require('node:crypto');
|
const crypto = require('node:crypto');
|
||||||
|
const csv = require('csv-parse/sync');
|
||||||
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||||
|
|
||||||
// Load package.json for version info
|
// Load package.json for version info
|
||||||
|
|
@ -21,6 +22,19 @@ class ManifestGenerator {
|
||||||
this.selectedIdes = [];
|
this.selectedIdes = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean text for CSV output by normalizing whitespace and escaping quotes
|
||||||
|
* @param {string} text - Text to clean
|
||||||
|
* @returns {string} Cleaned text safe for CSV
|
||||||
|
*/
|
||||||
|
cleanForCSV(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
return text
|
||||||
|
.trim()
|
||||||
|
.replaceAll(/\s+/g, ' ') // Normalize all whitespace (including newlines) to single space
|
||||||
|
.replaceAll('"', '""'); // Escape quotes for CSV
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate all manifests for the installation
|
* Generate all manifests for the installation
|
||||||
* @param {string} bmadDir - _bmad
|
* @param {string} bmadDir - _bmad
|
||||||
|
|
@ -161,7 +175,7 @@ class ManifestGenerator {
|
||||||
workflow = yaml.parse(content);
|
workflow = yaml.parse(content);
|
||||||
} else {
|
} else {
|
||||||
// Parse MD workflow with YAML frontmatter
|
// Parse MD workflow with YAML frontmatter
|
||||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||||
if (!frontmatterMatch) {
|
if (!frontmatterMatch) {
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`);
|
console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`);
|
||||||
|
|
@ -201,7 +215,7 @@ class ManifestGenerator {
|
||||||
// Workflows with standalone: false are filtered out above
|
// Workflows with standalone: false are filtered out above
|
||||||
workflows.push({
|
workflows.push({
|
||||||
name: workflow.name,
|
name: workflow.name,
|
||||||
description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV
|
description: this.cleanForCSV(workflow.description),
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
});
|
});
|
||||||
|
|
@ -319,24 +333,15 @@ class ManifestGenerator {
|
||||||
|
|
||||||
const agentName = entry.name.replace('.md', '');
|
const agentName = entry.name.replace('.md', '');
|
||||||
|
|
||||||
// Helper function to clean and escape CSV content
|
|
||||||
const cleanForCSV = (text) => {
|
|
||||||
if (!text) return '';
|
|
||||||
return text
|
|
||||||
.trim()
|
|
||||||
.replaceAll(/\s+/g, ' ') // Normalize whitespace
|
|
||||||
.replaceAll('"', '""'); // Escape quotes for CSV
|
|
||||||
};
|
|
||||||
|
|
||||||
agents.push({
|
agents.push({
|
||||||
name: agentName,
|
name: agentName,
|
||||||
displayName: nameMatch ? nameMatch[1] : agentName,
|
displayName: nameMatch ? nameMatch[1] : agentName,
|
||||||
title: titleMatch ? titleMatch[1] : '',
|
title: titleMatch ? titleMatch[1] : '',
|
||||||
icon: iconMatch ? iconMatch[1] : '',
|
icon: iconMatch ? iconMatch[1] : '',
|
||||||
role: roleMatch ? cleanForCSV(roleMatch[1]) : '',
|
role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '',
|
||||||
identity: identityMatch ? cleanForCSV(identityMatch[1]) : '',
|
identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '',
|
||||||
communicationStyle: styleMatch ? cleanForCSV(styleMatch[1]) : '',
|
communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '',
|
||||||
principles: principlesMatch ? cleanForCSV(principlesMatch[1]) : '',
|
principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '',
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
});
|
});
|
||||||
|
|
@ -385,6 +390,11 @@ class ManifestGenerator {
|
||||||
const filePath = path.join(dirPath, file);
|
const filePath = path.join(dirPath, file);
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
const content = await fs.readFile(filePath, 'utf8');
|
||||||
|
|
||||||
|
// Skip internal/engine files (not user-facing tasks)
|
||||||
|
if (content.includes('internal="true"')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let name = file.replace(/\.(xml|md)$/, '');
|
let name = file.replace(/\.(xml|md)$/, '');
|
||||||
let displayName = name;
|
let displayName = name;
|
||||||
let description = '';
|
let description = '';
|
||||||
|
|
@ -392,13 +402,13 @@ class ManifestGenerator {
|
||||||
|
|
||||||
if (file.endsWith('.md')) {
|
if (file.endsWith('.md')) {
|
||||||
// Parse YAML frontmatter for .md tasks
|
// Parse YAML frontmatter for .md tasks
|
||||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||||
if (frontmatterMatch) {
|
if (frontmatterMatch) {
|
||||||
try {
|
try {
|
||||||
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
||||||
name = frontmatter.name || name;
|
name = frontmatter.name || name;
|
||||||
displayName = frontmatter.displayName || frontmatter.name || name;
|
displayName = frontmatter.displayName || frontmatter.name || name;
|
||||||
description = frontmatter.description || '';
|
description = this.cleanForCSV(frontmatter.description || '');
|
||||||
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
|
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
|
||||||
} catch {
|
} catch {
|
||||||
// If YAML parsing fails, use defaults
|
// If YAML parsing fails, use defaults
|
||||||
|
|
@ -411,7 +421,7 @@ class ManifestGenerator {
|
||||||
|
|
||||||
const descMatch = content.match(/description="([^"]+)"/);
|
const descMatch = content.match(/description="([^"]+)"/);
|
||||||
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
|
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
|
||||||
description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '';
|
description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '');
|
||||||
|
|
||||||
const standaloneMatch = content.match(/<task[^>]+standalone="true"/);
|
const standaloneMatch = content.match(/<task[^>]+standalone="true"/);
|
||||||
standalone = !!standaloneMatch;
|
standalone = !!standaloneMatch;
|
||||||
|
|
@ -424,7 +434,7 @@ class ManifestGenerator {
|
||||||
tasks.push({
|
tasks.push({
|
||||||
name: name,
|
name: name,
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
description: description.replaceAll('"', '""'),
|
description: description,
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
standalone: standalone,
|
standalone: standalone,
|
||||||
|
|
@ -474,6 +484,11 @@ class ManifestGenerator {
|
||||||
const filePath = path.join(dirPath, file);
|
const filePath = path.join(dirPath, file);
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
const content = await fs.readFile(filePath, 'utf8');
|
||||||
|
|
||||||
|
// Skip internal tools (same as tasks)
|
||||||
|
if (content.includes('internal="true"')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let name = file.replace(/\.(xml|md)$/, '');
|
let name = file.replace(/\.(xml|md)$/, '');
|
||||||
let displayName = name;
|
let displayName = name;
|
||||||
let description = '';
|
let description = '';
|
||||||
|
|
@ -481,13 +496,13 @@ class ManifestGenerator {
|
||||||
|
|
||||||
if (file.endsWith('.md')) {
|
if (file.endsWith('.md')) {
|
||||||
// Parse YAML frontmatter for .md tools
|
// Parse YAML frontmatter for .md tools
|
||||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||||
if (frontmatterMatch) {
|
if (frontmatterMatch) {
|
||||||
try {
|
try {
|
||||||
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
||||||
name = frontmatter.name || name;
|
name = frontmatter.name || name;
|
||||||
displayName = frontmatter.displayName || frontmatter.name || name;
|
displayName = frontmatter.displayName || frontmatter.name || name;
|
||||||
description = frontmatter.description || '';
|
description = this.cleanForCSV(frontmatter.description || '');
|
||||||
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
|
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
|
||||||
} catch {
|
} catch {
|
||||||
// If YAML parsing fails, use defaults
|
// If YAML parsing fails, use defaults
|
||||||
|
|
@ -500,7 +515,7 @@ class ManifestGenerator {
|
||||||
|
|
||||||
const descMatch = content.match(/description="([^"]+)"/);
|
const descMatch = content.match(/description="([^"]+)"/);
|
||||||
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
|
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
|
||||||
description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '';
|
description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '');
|
||||||
|
|
||||||
const standaloneMatch = content.match(/<tool[^>]+standalone="true"/);
|
const standaloneMatch = content.match(/<tool[^>]+standalone="true"/);
|
||||||
standalone = !!standaloneMatch;
|
standalone = !!standaloneMatch;
|
||||||
|
|
@ -513,7 +528,7 @@ class ManifestGenerator {
|
||||||
tools.push({
|
tools.push({
|
||||||
name: name,
|
name: name,
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
description: description.replaceAll('"', '""'),
|
description: description,
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
standalone: standalone,
|
standalone: standalone,
|
||||||
|
|
@ -773,30 +788,23 @@ class ManifestGenerator {
|
||||||
*/
|
*/
|
||||||
async writeAgentManifest(cfgDir) {
|
async writeAgentManifest(cfgDir) {
|
||||||
const csvPath = path.join(cfgDir, 'agent-manifest.csv');
|
const csvPath = path.join(cfgDir, 'agent-manifest.csv');
|
||||||
|
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||||
|
|
||||||
// Read existing manifest to preserve entries
|
// Read existing manifest to preserve entries
|
||||||
const existingEntries = new Map();
|
const existingEntries = new Map();
|
||||||
if (await fs.pathExists(csvPath)) {
|
if (await fs.pathExists(csvPath)) {
|
||||||
const content = await fs.readFile(csvPath, 'utf8');
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
const lines = content.split('\n').filter((line) => line.trim());
|
const records = csv.parse(content, {
|
||||||
|
columns: true,
|
||||||
// Skip header
|
skip_empty_lines: true,
|
||||||
for (let i = 1; i < lines.length; i++) {
|
});
|
||||||
const line = lines[i];
|
for (const record of records) {
|
||||||
if (line) {
|
existingEntries.set(`${record.module}:${record.name}`, record);
|
||||||
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
|
||||||
const parts = line.split('","');
|
|
||||||
if (parts.length >= 11) {
|
|
||||||
const name = parts[0].replace(/^"/, '');
|
|
||||||
const module = parts[8];
|
|
||||||
existingEntries.set(`${module}:${name}`, line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create CSV header with persona fields
|
// Create CSV header with persona fields
|
||||||
let csv = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n';
|
let csvContent = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n';
|
||||||
|
|
||||||
// Combine existing and new agents, preferring new data for duplicates
|
// Combine existing and new agents, preferring new data for duplicates
|
||||||
const allAgents = new Map();
|
const allAgents = new Map();
|
||||||
|
|
@ -809,18 +817,38 @@ class ManifestGenerator {
|
||||||
// Add/update new agents
|
// Add/update new agents
|
||||||
for (const agent of this.agents) {
|
for (const agent of this.agents) {
|
||||||
const key = `${agent.module}:${agent.name}`;
|
const key = `${agent.module}:${agent.name}`;
|
||||||
allAgents.set(
|
allAgents.set(key, {
|
||||||
key,
|
name: agent.name,
|
||||||
`"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"`,
|
displayName: agent.displayName,
|
||||||
);
|
title: agent.title,
|
||||||
|
icon: agent.icon,
|
||||||
|
role: agent.role,
|
||||||
|
identity: agent.identity,
|
||||||
|
communicationStyle: agent.communicationStyle,
|
||||||
|
principles: agent.principles,
|
||||||
|
module: agent.module,
|
||||||
|
path: agent.path,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write all agents
|
// Write all agents
|
||||||
for (const [, value] of allAgents) {
|
for (const [, record] of allAgents) {
|
||||||
csv += value + '\n';
|
const row = [
|
||||||
|
escapeCsv(record.name),
|
||||||
|
escapeCsv(record.displayName),
|
||||||
|
escapeCsv(record.title),
|
||||||
|
escapeCsv(record.icon),
|
||||||
|
escapeCsv(record.role),
|
||||||
|
escapeCsv(record.identity),
|
||||||
|
escapeCsv(record.communicationStyle),
|
||||||
|
escapeCsv(record.principles),
|
||||||
|
escapeCsv(record.module),
|
||||||
|
escapeCsv(record.path),
|
||||||
|
].join(',');
|
||||||
|
csvContent += row + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(csvPath, csv);
|
await fs.writeFile(csvPath, csvContent);
|
||||||
return csvPath;
|
return csvPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -830,30 +858,23 @@ class ManifestGenerator {
|
||||||
*/
|
*/
|
||||||
async writeTaskManifest(cfgDir) {
|
async writeTaskManifest(cfgDir) {
|
||||||
const csvPath = path.join(cfgDir, 'task-manifest.csv');
|
const csvPath = path.join(cfgDir, 'task-manifest.csv');
|
||||||
|
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||||
|
|
||||||
// Read existing manifest to preserve entries
|
// Read existing manifest to preserve entries
|
||||||
const existingEntries = new Map();
|
const existingEntries = new Map();
|
||||||
if (await fs.pathExists(csvPath)) {
|
if (await fs.pathExists(csvPath)) {
|
||||||
const content = await fs.readFile(csvPath, 'utf8');
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
const lines = content.split('\n').filter((line) => line.trim());
|
const records = csv.parse(content, {
|
||||||
|
columns: true,
|
||||||
// Skip header
|
skip_empty_lines: true,
|
||||||
for (let i = 1; i < lines.length; i++) {
|
});
|
||||||
const line = lines[i];
|
for (const record of records) {
|
||||||
if (line) {
|
existingEntries.set(`${record.module}:${record.name}`, record);
|
||||||
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
|
||||||
const parts = line.split('","');
|
|
||||||
if (parts.length >= 6) {
|
|
||||||
const name = parts[0].replace(/^"/, '');
|
|
||||||
const module = parts[3];
|
|
||||||
existingEntries.set(`${module}:${name}`, line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create CSV header with standalone column
|
// Create CSV header with standalone column
|
||||||
let csv = 'name,displayName,description,module,path,standalone\n';
|
let csvContent = 'name,displayName,description,module,path,standalone\n';
|
||||||
|
|
||||||
// Combine existing and new tasks
|
// Combine existing and new tasks
|
||||||
const allTasks = new Map();
|
const allTasks = new Map();
|
||||||
|
|
@ -866,15 +887,30 @@ class ManifestGenerator {
|
||||||
// Add/update new tasks
|
// Add/update new tasks
|
||||||
for (const task of this.tasks) {
|
for (const task of this.tasks) {
|
||||||
const key = `${task.module}:${task.name}`;
|
const key = `${task.module}:${task.name}`;
|
||||||
allTasks.set(key, `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"`);
|
allTasks.set(key, {
|
||||||
|
name: task.name,
|
||||||
|
displayName: task.displayName,
|
||||||
|
description: task.description,
|
||||||
|
module: task.module,
|
||||||
|
path: task.path,
|
||||||
|
standalone: task.standalone,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write all tasks
|
// Write all tasks
|
||||||
for (const [, value] of allTasks) {
|
for (const [, record] of allTasks) {
|
||||||
csv += value + '\n';
|
const row = [
|
||||||
|
escapeCsv(record.name),
|
||||||
|
escapeCsv(record.displayName),
|
||||||
|
escapeCsv(record.description),
|
||||||
|
escapeCsv(record.module),
|
||||||
|
escapeCsv(record.path),
|
||||||
|
escapeCsv(record.standalone),
|
||||||
|
].join(',');
|
||||||
|
csvContent += row + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(csvPath, csv);
|
await fs.writeFile(csvPath, csvContent);
|
||||||
return csvPath;
|
return csvPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -884,30 +920,23 @@ class ManifestGenerator {
|
||||||
*/
|
*/
|
||||||
async writeToolManifest(cfgDir) {
|
async writeToolManifest(cfgDir) {
|
||||||
const csvPath = path.join(cfgDir, 'tool-manifest.csv');
|
const csvPath = path.join(cfgDir, 'tool-manifest.csv');
|
||||||
|
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||||
|
|
||||||
// Read existing manifest to preserve entries
|
// Read existing manifest to preserve entries
|
||||||
const existingEntries = new Map();
|
const existingEntries = new Map();
|
||||||
if (await fs.pathExists(csvPath)) {
|
if (await fs.pathExists(csvPath)) {
|
||||||
const content = await fs.readFile(csvPath, 'utf8');
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
const lines = content.split('\n').filter((line) => line.trim());
|
const records = csv.parse(content, {
|
||||||
|
columns: true,
|
||||||
// Skip header
|
skip_empty_lines: true,
|
||||||
for (let i = 1; i < lines.length; i++) {
|
});
|
||||||
const line = lines[i];
|
for (const record of records) {
|
||||||
if (line) {
|
existingEntries.set(`${record.module}:${record.name}`, record);
|
||||||
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
|
||||||
const parts = line.split('","');
|
|
||||||
if (parts.length >= 6) {
|
|
||||||
const name = parts[0].replace(/^"/, '');
|
|
||||||
const module = parts[3];
|
|
||||||
existingEntries.set(`${module}:${name}`, line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create CSV header with standalone column
|
// Create CSV header with standalone column
|
||||||
let csv = 'name,displayName,description,module,path,standalone\n';
|
let csvContent = 'name,displayName,description,module,path,standalone\n';
|
||||||
|
|
||||||
// Combine existing and new tools
|
// Combine existing and new tools
|
||||||
const allTools = new Map();
|
const allTools = new Map();
|
||||||
|
|
@ -920,15 +949,30 @@ class ManifestGenerator {
|
||||||
// Add/update new tools
|
// Add/update new tools
|
||||||
for (const tool of this.tools) {
|
for (const tool of this.tools) {
|
||||||
const key = `${tool.module}:${tool.name}`;
|
const key = `${tool.module}:${tool.name}`;
|
||||||
allTools.set(key, `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"`);
|
allTools.set(key, {
|
||||||
|
name: tool.name,
|
||||||
|
displayName: tool.displayName,
|
||||||
|
description: tool.description,
|
||||||
|
module: tool.module,
|
||||||
|
path: tool.path,
|
||||||
|
standalone: tool.standalone,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write all tools
|
// Write all tools
|
||||||
for (const [, value] of allTools) {
|
for (const [, record] of allTools) {
|
||||||
csv += value + '\n';
|
const row = [
|
||||||
|
escapeCsv(record.name),
|
||||||
|
escapeCsv(record.displayName),
|
||||||
|
escapeCsv(record.description),
|
||||||
|
escapeCsv(record.module),
|
||||||
|
escapeCsv(record.path),
|
||||||
|
escapeCsv(record.standalone),
|
||||||
|
].join(',');
|
||||||
|
csvContent += row + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(csvPath, csv);
|
await fs.writeFile(csvPath, csvContent);
|
||||||
return csvPath;
|
return csvPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,7 @@ class CustomHandler {
|
||||||
const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']);
|
const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']);
|
||||||
|
|
||||||
for (const agentFile of agentFiles) {
|
for (const agentFile of agentFiles) {
|
||||||
const relativePath = path.relative(sourceAgentsPath, agentFile);
|
const relativePath = path.relative(sourceAgentsPath, agentFile).split(path.sep).join('/');
|
||||||
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
|
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
|
||||||
|
|
||||||
await fs.ensureDir(targetDir);
|
await fs.ensureDir(targetDir);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const fs = require('fs-extra');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { XmlHandler } = require('../../../lib/xml-handler');
|
const { XmlHandler } = require('../../../lib/xml-handler');
|
||||||
const { getSourcePath } = require('../../../lib/project-root');
|
const { getSourcePath } = require('../../../lib/project-root');
|
||||||
|
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for IDE-specific setup
|
* Base class for IDE-specific setup
|
||||||
|
|
@ -18,7 +19,7 @@ class BaseIdeSetup {
|
||||||
this.configFile = null; // Override in subclasses when detection is file-based
|
this.configFile = null; // Override in subclasses when detection is file-based
|
||||||
this.detectionPaths = []; // Additional paths that indicate the IDE is configured
|
this.detectionPaths = []; // Additional paths that indicate the IDE is configured
|
||||||
this.xmlHandler = new XmlHandler();
|
this.xmlHandler = new XmlHandler();
|
||||||
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,7 +58,7 @@ class BaseIdeSetup {
|
||||||
if (this.configDir) {
|
if (this.configDir) {
|
||||||
const configPath = path.join(projectDir, this.configDir);
|
const configPath = path.join(projectDir, this.configDir);
|
||||||
if (await fs.pathExists(configPath)) {
|
if (await fs.pathExists(configPath)) {
|
||||||
const bmadRulesPath = path.join(configPath, 'bmad');
|
const bmadRulesPath = path.join(configPath, BMAD_FOLDER_NAME);
|
||||||
if (await fs.pathExists(bmadRulesPath)) {
|
if (await fs.pathExists(bmadRulesPath)) {
|
||||||
await fs.remove(bmadRulesPath);
|
await fs.remove(bmadRulesPath);
|
||||||
console.log(chalk.dim(`Removed ${this.name} BMAD configuration`));
|
console.log(chalk.dim(`Removed ${this.name} BMAD configuration`));
|
||||||
|
|
@ -445,6 +446,11 @@ class BaseIdeSetup {
|
||||||
try {
|
try {
|
||||||
const content = await fs.readFile(fullPath, 'utf8');
|
const content = await fs.readFile(fullPath, 'utf8');
|
||||||
|
|
||||||
|
// Skip internal/engine files (not user-facing tasks/tools)
|
||||||
|
if (content.includes('internal="true"')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for standalone="true" in XML files
|
// Check for standalone="true" in XML files
|
||||||
if (entry.name.endsWith('.xml')) {
|
if (entry.name.endsWith('.xml')) {
|
||||||
// Look for standalone="true" in the opening tag (task or tool)
|
// Look for standalone="true" in the opening tag (task or tool)
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
*/
|
*/
|
||||||
async installToTarget(projectDir, bmadDir, config, options) {
|
async installToTarget(projectDir, bmadDir, config, options) {
|
||||||
const { target_dir, template_type, artifact_types } = config;
|
const { target_dir, template_type, artifact_types } = config;
|
||||||
|
|
||||||
|
// Skip targets with explicitly empty artifact_types array
|
||||||
|
// This prevents creating empty directories when no artifacts will be written
|
||||||
|
if (Array.isArray(artifact_types) && artifact_types.length === 0) {
|
||||||
|
return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0 } };
|
||||||
|
}
|
||||||
|
|
||||||
const targetPath = path.join(projectDir, target_dir);
|
const targetPath = path.join(projectDir, target_dir);
|
||||||
await this.ensureDir(targetPath);
|
await this.ensureDir(targetPath);
|
||||||
|
|
||||||
|
|
@ -86,10 +93,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config);
|
results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install tasks and tools
|
// Install tasks and tools using template system (supports TOML for Gemini, MD for others)
|
||||||
if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) {
|
if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) {
|
||||||
const taskToolGen = new TaskToolCommandGenerator();
|
const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName);
|
||||||
const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, targetPath);
|
const { artifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
|
||||||
|
const taskToolResult = await this.writeTaskToolArtifacts(targetPath, artifacts, template_type, config);
|
||||||
results.tasks = taskToolResult.tasks || 0;
|
results.tasks = taskToolResult.tasks || 0;
|
||||||
results.tools = taskToolResult.tools || 0;
|
results.tools = taskToolResult.tools || 0;
|
||||||
}
|
}
|
||||||
|
|
@ -180,6 +188,53 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write task/tool artifacts to target directory using templates
|
||||||
|
* @param {string} targetPath - Target directory path
|
||||||
|
* @param {Array} artifacts - Task/tool artifacts
|
||||||
|
* @param {string} templateType - Template type to use
|
||||||
|
* @param {Object} config - Installation configuration
|
||||||
|
* @returns {Promise<Object>} Counts of tasks and tools written
|
||||||
|
*/
|
||||||
|
async writeTaskToolArtifacts(targetPath, artifacts, templateType, config = {}) {
|
||||||
|
let taskCount = 0;
|
||||||
|
let toolCount = 0;
|
||||||
|
|
||||||
|
// Pre-load templates to avoid repeated file I/O in the loop
|
||||||
|
const taskTemplate = await this.loadTemplate(templateType, 'task', config, 'default-task');
|
||||||
|
const toolTemplate = await this.loadTemplate(templateType, 'tool', config, 'default-tool');
|
||||||
|
|
||||||
|
const { artifact_types } = config;
|
||||||
|
|
||||||
|
for (const artifact of artifacts) {
|
||||||
|
if (artifact.type !== 'task' && artifact.type !== 'tool') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if the specific artifact type is not requested in config
|
||||||
|
if (artifact_types) {
|
||||||
|
if (artifact.type === 'task' && !artifact_types.includes('tasks')) continue;
|
||||||
|
if (artifact.type === 'tool' && !artifact_types.includes('tools')) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use pre-loaded template based on artifact type
|
||||||
|
const { content: template, extension } = artifact.type === 'task' ? taskTemplate : toolTemplate;
|
||||||
|
|
||||||
|
const content = this.renderTemplate(template, artifact);
|
||||||
|
const filename = this.generateFilename(artifact, artifact.type, extension);
|
||||||
|
const filePath = path.join(targetPath, filename);
|
||||||
|
await this.writeFile(filePath, content);
|
||||||
|
|
||||||
|
if (artifact.type === 'task') {
|
||||||
|
taskCount++;
|
||||||
|
} else {
|
||||||
|
toolCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tasks: taskCount, tools: toolCount };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load template based on type and configuration
|
* Load template based on type and configuration
|
||||||
* @param {string} templateType - Template type (claude, windsurf, etc.)
|
* @param {string} templateType - Template type (claude, windsurf, etc.)
|
||||||
|
|
@ -316,10 +371,24 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
renderTemplate(template, artifact) {
|
renderTemplate(template, artifact) {
|
||||||
// Use the appropriate path property based on artifact type
|
// Use the appropriate path property based on artifact type
|
||||||
let pathToUse = artifact.relativePath || '';
|
let pathToUse = artifact.relativePath || '';
|
||||||
if (artifact.type === 'agent-launcher') {
|
switch (artifact.type) {
|
||||||
|
case 'agent-launcher': {
|
||||||
pathToUse = artifact.agentPath || artifact.relativePath || '';
|
pathToUse = artifact.agentPath || artifact.relativePath || '';
|
||||||
} else if (artifact.type === 'workflow-command') {
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'workflow-command': {
|
||||||
pathToUse = artifact.workflowPath || artifact.relativePath || '';
|
pathToUse = artifact.workflowPath || artifact.relativePath || '';
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'task':
|
||||||
|
case 'tool': {
|
||||||
|
pathToUse = artifact.path || artifact.relativePath || '';
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// No default
|
||||||
}
|
}
|
||||||
|
|
||||||
let rendered = template
|
let rendered = template
|
||||||
|
|
@ -351,8 +420,9 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
// Reuse central logic to ensure consistent naming conventions
|
// Reuse central logic to ensure consistent naming conventions
|
||||||
const standardName = toDashPath(artifact.relativePath);
|
const standardName = toDashPath(artifact.relativePath);
|
||||||
|
|
||||||
// Clean up potential double extensions from source files (e.g. .yaml.md -> .md)
|
// Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md)
|
||||||
const baseName = standardName.replace(/\.(yaml|yml)\.md$/, '.md');
|
// This handles any extensions that might slip through toDashPath()
|
||||||
|
const baseName = standardName.replace(/\.(md|yaml|yml|json|xml|toml)\.md$/i, '.md');
|
||||||
|
|
||||||
// If using default markdown, preserve the bmad-agent- prefix for agents
|
// If using default markdown, preserve the bmad-agent- prefix for agents
|
||||||
if (extension === '.md') {
|
if (extension === '.md') {
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,10 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
);
|
);
|
||||||
taskArtifacts.push({
|
taskArtifacts.push({
|
||||||
type: 'task',
|
type: 'task',
|
||||||
|
name: task.name,
|
||||||
|
displayName: task.name,
|
||||||
module: task.module,
|
module: task.module,
|
||||||
|
path: task.path,
|
||||||
sourcePath: task.path,
|
sourcePath: task.path,
|
||||||
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
|
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
|
||||||
content,
|
content,
|
||||||
|
|
@ -116,7 +119,7 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
const workflowCount = await workflowGenerator.writeDashArtifacts(destDir, workflowArtifacts);
|
const workflowCount = await workflowGenerator.writeDashArtifacts(destDir, workflowArtifacts);
|
||||||
|
|
||||||
// Also write tasks using underscore format
|
// Also write tasks using underscore format
|
||||||
const ttGen = new TaskToolCommandGenerator();
|
const ttGen = new TaskToolCommandGenerator(this.bmadFolderName);
|
||||||
const tasksWritten = await ttGen.writeDashArtifacts(destDir, taskArtifacts);
|
const tasksWritten = await ttGen.writeDashArtifacts(destDir, taskArtifacts);
|
||||||
|
|
||||||
const written = agentCount + workflowCount + tasksWritten;
|
const written = agentCount + workflowCount + tasksWritten;
|
||||||
|
|
@ -214,7 +217,10 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
|
|
||||||
artifacts.push({
|
artifacts.push({
|
||||||
type: 'task',
|
type: 'task',
|
||||||
|
name: task.name,
|
||||||
|
displayName: task.name,
|
||||||
module: task.module,
|
module: task.module,
|
||||||
|
path: task.path,
|
||||||
sourcePath: task.path,
|
sourcePath: task.path,
|
||||||
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
|
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
|
||||||
content,
|
content,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
|
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IDE Manager - handles IDE-specific setup
|
* IDE Manager - handles IDE-specific setup
|
||||||
|
|
@ -14,7 +15,7 @@ class IdeManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.handlers = new Map();
|
this.handlers = new Map();
|
||||||
this._initialized = false;
|
this._initialized = false;
|
||||||
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -73,6 +74,9 @@ class IdeManager {
|
||||||
if (HandlerClass) {
|
if (HandlerClass) {
|
||||||
const instance = new HandlerClass();
|
const instance = new HandlerClass();
|
||||||
if (instance.name && typeof instance.name === 'string') {
|
if (instance.name && typeof instance.name === 'string') {
|
||||||
|
if (typeof instance.setBmadFolderName === 'function') {
|
||||||
|
instance.setBmadFolderName(this.bmadFolderName);
|
||||||
|
}
|
||||||
this.handlers.set(instance.name, instance);
|
this.handlers.set(instance.name, instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +104,9 @@ class IdeManager {
|
||||||
if (!platformInfo.installer) continue;
|
if (!platformInfo.installer) continue;
|
||||||
|
|
||||||
const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo);
|
const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo);
|
||||||
|
if (typeof handler.setBmadFolderName === 'function') {
|
||||||
handler.setBmadFolderName(this.bmadFolderName);
|
handler.setBmadFolderName(this.bmadFolderName);
|
||||||
|
}
|
||||||
this.handlers.set(platformCode, handler);
|
this.handlers.set(platformCode, handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,6 @@ platforms:
|
||||||
- target_dir: .github/agents
|
- target_dir: .github/agents
|
||||||
template_type: copilot_agents
|
template_type: copilot_agents
|
||||||
artifact_types: [agents]
|
artifact_types: [agents]
|
||||||
- target_dir: .vscode
|
|
||||||
template_type: vscode_settings
|
|
||||||
artifact_types: []
|
|
||||||
|
|
||||||
iflow:
|
iflow:
|
||||||
name: "iFlow"
|
name: "iFlow"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils');
|
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates launcher command files for each agent
|
* Generates launcher command files for each agent
|
||||||
* Similar to WorkflowCommandGenerator but for agents
|
* Similar to WorkflowCommandGenerator but for agents
|
||||||
*/
|
*/
|
||||||
class AgentCommandGenerator {
|
class AgentCommandGenerator {
|
||||||
constructor(bmadFolderName = 'bmad') {
|
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
||||||
this.templatePath = path.join(__dirname, '../templates/agent-command-template.md');
|
this.templatePath = path.join(__dirname, '../templates/agent-command-template.md');
|
||||||
this.bmadFolderName = bmadFolderName;
|
this.bmadFolderName = bmadFolderName;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,13 +141,24 @@ async function getTasksFromDir(dirPath, moduleName) {
|
||||||
const files = await fs.readdir(dirPath);
|
const files = await fs.readdir(dirPath);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!file.endsWith('.md')) {
|
// Include both .md and .xml task files
|
||||||
|
if (!file.endsWith('.md') && !file.endsWith('.xml')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(dirPath, file);
|
||||||
|
const content = await fs.readFile(filePath, 'utf8');
|
||||||
|
|
||||||
|
// Skip internal/engine files (not user-facing tasks)
|
||||||
|
if (content.includes('internal="true"')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove extension to get task name
|
||||||
|
const ext = file.endsWith('.xml') ? '.xml' : '.md';
|
||||||
tasks.push({
|
tasks.push({
|
||||||
path: path.join(dirPath, file),
|
path: filePath,
|
||||||
name: file.replace('.md', ''),
|
name: file.replace(ext, ''),
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@
|
||||||
const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
|
const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
|
||||||
const AGENT_SEGMENT = 'agents';
|
const AGENT_SEGMENT = 'agents';
|
||||||
|
|
||||||
|
// BMAD installation folder name - centralized constant for all installers
|
||||||
|
const BMAD_FOLDER_NAME = '_bmad';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert hierarchical path to flat dash-separated name (NEW STANDARD)
|
* Convert hierarchical path to flat dash-separated name (NEW STANDARD)
|
||||||
* Converts: 'bmm', 'agents', 'pm' → 'bmad-agent-bmm-pm.md'
|
* Converts: 'bmm', 'agents', 'pm' → 'bmad-agent-bmm-pm.md'
|
||||||
|
|
@ -59,7 +62,9 @@ function toDashPath(relativePath) {
|
||||||
return 'bmad-unknown.md';
|
return 'bmad-unknown.md';
|
||||||
}
|
}
|
||||||
|
|
||||||
const withoutExt = relativePath.replace('.md', '');
|
// Strip common file extensions to avoid double extensions in generated filenames
|
||||||
|
// e.g., 'create-story.xml' → 'create-story', 'workflow.yaml' → 'workflow'
|
||||||
|
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
|
||||||
const parts = withoutExt.split(/[/\\]/);
|
const parts = withoutExt.split(/[/\\]/);
|
||||||
|
|
||||||
const module = parts[0];
|
const module = parts[0];
|
||||||
|
|
@ -183,7 +188,8 @@ function toUnderscoreName(module, type, name) {
|
||||||
* @deprecated Use toDashPath instead
|
* @deprecated Use toDashPath instead
|
||||||
*/
|
*/
|
||||||
function toUnderscorePath(relativePath) {
|
function toUnderscorePath(relativePath) {
|
||||||
const withoutExt = relativePath.replace('.md', '');
|
// Strip common file extensions (same as toDashPath for consistency)
|
||||||
|
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
|
||||||
const parts = withoutExt.split(/[/\\]/);
|
const parts = withoutExt.split(/[/\\]/);
|
||||||
|
|
||||||
const module = parts[0];
|
const module = parts[0];
|
||||||
|
|
@ -289,4 +295,5 @@ module.exports = {
|
||||||
|
|
||||||
TYPE_SEGMENTS,
|
TYPE_SEGMENTS,
|
||||||
AGENT_SEGMENT,
|
AGENT_SEGMENT,
|
||||||
|
BMAD_FOLDER_NAME,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,98 @@ const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const csv = require('csv-parse/sync');
|
const csv = require('csv-parse/sync');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { toColonName, toColonPath, toDashPath } = require('./path-utils');
|
const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = require('./path-utils');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates command files for standalone tasks and tools
|
* Generates command files for standalone tasks and tools
|
||||||
*/
|
*/
|
||||||
class TaskToolCommandGenerator {
|
class TaskToolCommandGenerator {
|
||||||
|
/**
|
||||||
|
* @param {string} bmadFolderName - Name of the BMAD folder for template rendering (default: '_bmad')
|
||||||
|
* Note: This parameter is accepted for API consistency with AgentCommandGenerator and
|
||||||
|
* WorkflowCommandGenerator, but is not used for path stripping. The manifest always stores
|
||||||
|
* filesystem paths with '_bmad/' prefix (the actual folder name), while bmadFolderName is
|
||||||
|
* used for template placeholder rendering ({{bmadFolderName}}).
|
||||||
|
*/
|
||||||
|
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
||||||
|
this.bmadFolderName = bmadFolderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect task and tool artifacts for IDE installation
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @returns {Promise<Object>} Artifacts array with metadata
|
||||||
|
*/
|
||||||
|
async collectTaskToolArtifacts(bmadDir) {
|
||||||
|
const tasks = await this.loadTaskManifest(bmadDir);
|
||||||
|
const tools = await this.loadToolManifest(bmadDir);
|
||||||
|
|
||||||
|
// Filter to only standalone items
|
||||||
|
const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
|
||||||
|
const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
|
||||||
|
|
||||||
|
const artifacts = [];
|
||||||
|
const bmadPrefix = `${BMAD_FOLDER_NAME}/`;
|
||||||
|
|
||||||
|
// Collect task artifacts
|
||||||
|
for (const task of standaloneTasks) {
|
||||||
|
let taskPath = (task.path || '').replaceAll('\\', '/');
|
||||||
|
// Convert absolute paths to relative paths
|
||||||
|
if (path.isAbsolute(taskPath)) {
|
||||||
|
taskPath = path.relative(bmadDir, taskPath).replaceAll('\\', '/');
|
||||||
|
}
|
||||||
|
// Remove _bmad/ prefix if present to get relative path within bmad folder
|
||||||
|
if (taskPath.startsWith(bmadPrefix)) {
|
||||||
|
taskPath = taskPath.slice(bmadPrefix.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskExt = path.extname(taskPath) || '.md';
|
||||||
|
artifacts.push({
|
||||||
|
type: 'task',
|
||||||
|
name: task.name,
|
||||||
|
displayName: task.displayName || task.name,
|
||||||
|
description: task.description || `Execute ${task.displayName || task.name}`,
|
||||||
|
module: task.module,
|
||||||
|
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
|
||||||
|
relativePath: `${task.module}/tasks/${task.name}${taskExt}`,
|
||||||
|
path: taskPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect tool artifacts
|
||||||
|
for (const tool of standaloneTools) {
|
||||||
|
let toolPath = (tool.path || '').replaceAll('\\', '/');
|
||||||
|
// Convert absolute paths to relative paths
|
||||||
|
if (path.isAbsolute(toolPath)) {
|
||||||
|
toolPath = path.relative(bmadDir, toolPath).replaceAll('\\', '/');
|
||||||
|
}
|
||||||
|
// Remove _bmad/ prefix if present to get relative path within bmad folder
|
||||||
|
if (toolPath.startsWith(bmadPrefix)) {
|
||||||
|
toolPath = toolPath.slice(bmadPrefix.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolExt = path.extname(toolPath) || '.md';
|
||||||
|
artifacts.push({
|
||||||
|
type: 'tool',
|
||||||
|
name: tool.name,
|
||||||
|
displayName: tool.displayName || tool.name,
|
||||||
|
description: tool.description || `Execute ${tool.displayName || tool.name}`,
|
||||||
|
module: tool.module,
|
||||||
|
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
|
||||||
|
relativePath: `${tool.module}/tools/${tool.name}${toolExt}`,
|
||||||
|
path: toolPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
artifacts,
|
||||||
|
counts: {
|
||||||
|
tasks: standaloneTasks.length,
|
||||||
|
tools: standaloneTools.length,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate task and tool commands from manifest CSVs
|
* Generate task and tool commands from manifest CSVs
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
|
|
@ -65,9 +151,35 @@ class TaskToolCommandGenerator {
|
||||||
const description = item.description || `Execute ${item.displayName || item.name}`;
|
const description = item.description || `Execute ${item.displayName || item.name}`;
|
||||||
|
|
||||||
// Convert path to use {project-root} placeholder
|
// Convert path to use {project-root} placeholder
|
||||||
|
// Handle undefined/missing path by constructing from module and name
|
||||||
let itemPath = item.path;
|
let itemPath = item.path;
|
||||||
if (itemPath && typeof itemPath === 'string' && itemPath.startsWith('bmad/')) {
|
if (!itemPath || typeof itemPath !== 'string') {
|
||||||
itemPath = `{project-root}/${itemPath}`;
|
// Fallback: construct path from module and name if path is missing
|
||||||
|
const typePlural = type === 'task' ? 'tasks' : 'tools';
|
||||||
|
itemPath = `{project-root}/${this.bmadFolderName}/${item.module}/${typePlural}/${item.name}.md`;
|
||||||
|
} else {
|
||||||
|
// Normalize path separators to forward slashes
|
||||||
|
itemPath = itemPath.replaceAll('\\', '/');
|
||||||
|
|
||||||
|
// Extract relative path from absolute paths (Windows or Unix)
|
||||||
|
// Look for _bmad/ or bmad/ in the path and extract everything after it
|
||||||
|
// Match patterns like: /_bmad/core/tasks/... or /bmad/core/tasks/...
|
||||||
|
// Use [/\\] to handle both Unix forward slashes and Windows backslashes,
|
||||||
|
// and also paths without a leading separator (e.g., C:/_bmad/...)
|
||||||
|
const bmadMatch = itemPath.match(/[/\\]_bmad[/\\](.+)$/) || itemPath.match(/[/\\]bmad[/\\](.+)$/);
|
||||||
|
if (bmadMatch) {
|
||||||
|
// Found /_bmad/ or /bmad/ - use relative path after it
|
||||||
|
itemPath = `{project-root}/${this.bmadFolderName}/${bmadMatch[1]}`;
|
||||||
|
} else if (itemPath.startsWith(`${BMAD_FOLDER_NAME}/`)) {
|
||||||
|
// Relative path starting with _bmad/
|
||||||
|
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(BMAD_FOLDER_NAME.length + 1)}`;
|
||||||
|
} else if (itemPath.startsWith('bmad/')) {
|
||||||
|
// Relative path starting with bmad/
|
||||||
|
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(5)}`;
|
||||||
|
} else if (!itemPath.startsWith('{project-root}')) {
|
||||||
|
// For other relative paths, prefix with project root and bmad folder
|
||||||
|
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return `---
|
return `---
|
||||||
|
|
@ -187,7 +299,7 @@ Follow all instructions in the ${type} file exactly as written.
|
||||||
// Generate command files for tasks
|
// Generate command files for tasks
|
||||||
for (const task of standaloneTasks) {
|
for (const task of standaloneTasks) {
|
||||||
const commandContent = this.generateCommandContent(task, 'task');
|
const commandContent = this.generateCommandContent(task, 'task');
|
||||||
// Use underscore format: bmad_bmm_name.md
|
// Use dash format: bmad-bmm-name.md
|
||||||
const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`);
|
const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`);
|
||||||
const commandPath = path.join(baseCommandsDir, flatName);
|
const commandPath = path.join(baseCommandsDir, flatName);
|
||||||
await fs.ensureDir(path.dirname(commandPath));
|
await fs.ensureDir(path.dirname(commandPath));
|
||||||
|
|
@ -198,7 +310,7 @@ Follow all instructions in the ${type} file exactly as written.
|
||||||
// Generate command files for tools
|
// Generate command files for tools
|
||||||
for (const tool of standaloneTools) {
|
for (const tool of standaloneTools) {
|
||||||
const commandContent = this.generateCommandContent(tool, 'tool');
|
const commandContent = this.generateCommandContent(tool, 'tool');
|
||||||
// Use underscore format: bmad_bmm_name.md
|
// Use dash format: bmad-bmm-name.md
|
||||||
const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`);
|
const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`);
|
||||||
const commandPath = path.join(baseCommandsDir, flatName);
|
const commandPath = path.join(baseCommandsDir, flatName);
|
||||||
await fs.ensureDir(path.dirname(commandPath));
|
await fs.ensureDir(path.dirname(commandPath));
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const csv = require('csv-parse/sync');
|
const csv = require('csv-parse/sync');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils');
|
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates command files for each workflow in the manifest
|
* Generates command files for each workflow in the manifest
|
||||||
*/
|
*/
|
||||||
class WorkflowCommandGenerator {
|
class WorkflowCommandGenerator {
|
||||||
constructor(bmadFolderName = 'bmad') {
|
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
||||||
this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md');
|
this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md');
|
||||||
this.bmadFolderName = bmadFolderName;
|
this.bmadFolderName = bmadFolderName;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
name: '{{name}}'
|
||||||
|
description: '{{description}}'
|
||||||
|
---
|
||||||
|
|
||||||
|
# {{name}}
|
||||||
|
|
||||||
|
Read the entire task file at: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
|
|
||||||
|
Follow all instructions in the task file exactly as written.
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
name: '{{name}}'
|
||||||
|
description: '{{description}}'
|
||||||
|
---
|
||||||
|
|
||||||
|
# {{name}}
|
||||||
|
|
||||||
|
Read the entire tool file at: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
|
|
||||||
|
Follow all instructions in the tool file exactly as written.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
description = "Executes the {{name}} task from the BMAD Method."
|
||||||
|
prompt = """
|
||||||
|
Execute the BMAD '{{name}}' task.
|
||||||
|
|
||||||
|
TASK INSTRUCTIONS:
|
||||||
|
1. LOAD the task file from {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
|
2. READ its entire contents
|
||||||
|
3. FOLLOW every instruction precisely as specified
|
||||||
|
|
||||||
|
TASK FILE: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
|
"""
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
description = "Executes the {{name}} tool from the BMAD Method."
|
||||||
|
prompt = """
|
||||||
|
Execute the BMAD '{{name}}' tool.
|
||||||
|
|
||||||
|
TOOL INSTRUCTIONS:
|
||||||
|
1. LOAD the tool file from {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
|
2. READ its entire contents
|
||||||
|
3. FOLLOW every instruction precisely as specified
|
||||||
|
|
||||||
|
TOOL FILE: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
|
"""
|
||||||
|
|
@ -7,6 +7,7 @@ const { XmlHandler } = require('../../../lib/xml-handler');
|
||||||
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||||
const { filterCustomizationData } = require('../../../lib/agent/compiler');
|
const { filterCustomizationData } = require('../../../lib/agent/compiler');
|
||||||
const { ExternalModuleManager } = require('./external-manager');
|
const { ExternalModuleManager } = require('./external-manager');
|
||||||
|
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the installation, updating, and removal of BMAD modules.
|
* Manages the installation, updating, and removal of BMAD modules.
|
||||||
|
|
@ -27,7 +28,7 @@ const { ExternalModuleManager } = require('./external-manager');
|
||||||
class ModuleManager {
|
class ModuleManager {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.xmlHandler = new XmlHandler();
|
this.xmlHandler = new XmlHandler();
|
||||||
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden
|
||||||
this.customModulePaths = new Map(); // Initialize custom module paths
|
this.customModulePaths = new Map(); // Initialize custom module paths
|
||||||
this.externalModuleManager = new ExternalModuleManager(); // For external official modules
|
this.externalModuleManager = new ExternalModuleManager(); // For external official modules
|
||||||
}
|
}
|
||||||
|
|
@ -870,7 +871,7 @@ class ModuleManager {
|
||||||
for (const agentFile of agentFiles) {
|
for (const agentFile of agentFiles) {
|
||||||
if (!agentFile.endsWith('.agent.yaml')) continue;
|
if (!agentFile.endsWith('.agent.yaml')) continue;
|
||||||
|
|
||||||
const relativePath = path.relative(sourceAgentsPath, agentFile);
|
const relativePath = path.relative(sourceAgentsPath, agentFile).split(path.sep).join('/');
|
||||||
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
|
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
|
||||||
|
|
||||||
await fs.ensureDir(targetDir);
|
await fs.ensureDir(targetDir);
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ function findBmadConfig(startPath = process.cwd()) {
|
||||||
* @returns {string} Resolved path
|
* @returns {string} Resolved path
|
||||||
*/
|
*/
|
||||||
function resolvePath(pathStr, context) {
|
function resolvePath(pathStr, context) {
|
||||||
return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context_bmadFolder);
|
return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context.bmadFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let _clack = null;
|
let _clack = null;
|
||||||
|
let _clackCore = null;
|
||||||
|
let _picocolors = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazy-load @clack/prompts (ESM module)
|
* Lazy-load @clack/prompts (ESM module)
|
||||||
|
|
@ -20,6 +22,28 @@ async function getClack() {
|
||||||
return _clack;
|
return _clack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy-load @clack/core (ESM module)
|
||||||
|
* @returns {Promise<Object>} The clack core module
|
||||||
|
*/
|
||||||
|
async function getClackCore() {
|
||||||
|
if (!_clackCore) {
|
||||||
|
_clackCore = await import('@clack/core');
|
||||||
|
}
|
||||||
|
return _clackCore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy-load picocolors
|
||||||
|
* @returns {Promise<Object>} The picocolors module
|
||||||
|
*/
|
||||||
|
async function getPicocolors() {
|
||||||
|
if (!_picocolors) {
|
||||||
|
_picocolors = (await import('picocolors')).default;
|
||||||
|
}
|
||||||
|
return _picocolors;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle user cancellation gracefully
|
* Handle user cancellation gracefully
|
||||||
* @param {any} value - The value to check
|
* @param {any} value - The value to check
|
||||||
|
|
@ -191,6 +215,118 @@ async function groupMultiselect(options) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default filter function for autocomplete - case-insensitive label matching
|
||||||
|
* @param {string} search - Search string
|
||||||
|
* @param {Object} option - Option object with label
|
||||||
|
* @returns {boolean} Whether the option matches
|
||||||
|
*/
|
||||||
|
function defaultAutocompleteFilter(search, option) {
|
||||||
|
const label = option.label ?? String(option.value ?? '');
|
||||||
|
return label.toLowerCase().includes(search.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Autocomplete multi-select prompt with type-ahead filtering
|
||||||
|
* Custom implementation that always shows "Space/Tab:" in the hint
|
||||||
|
* @param {Object} options - Prompt options
|
||||||
|
* @param {string} options.message - The question to ask
|
||||||
|
* @param {Array} options.options - Array of choices [{label, value, hint?}]
|
||||||
|
* @param {string} [options.placeholder] - Placeholder text for search input
|
||||||
|
* @param {Array} [options.initialValues] - Array of initially selected values
|
||||||
|
* @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
|
||||||
|
* @returns {Promise<Array>} Array of selected values
|
||||||
|
*/
|
||||||
|
async function autocompleteMultiselect(options) {
|
||||||
|
const core = await getClackCore();
|
||||||
|
const clack = await getClack();
|
||||||
|
const color = await getPicocolors();
|
||||||
|
|
||||||
|
const filterFn = options.filter ?? defaultAutocompleteFilter;
|
||||||
|
|
||||||
|
const prompt = new core.AutocompletePrompt({
|
||||||
|
options: options.options,
|
||||||
|
multiple: true,
|
||||||
|
filter: filterFn,
|
||||||
|
validate: () => {
|
||||||
|
if (options.required && prompt.selectedValues.length === 0) {
|
||||||
|
return 'Please select at least one item';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initialValue: options.initialValues,
|
||||||
|
render() {
|
||||||
|
const barColor = this.state === 'error' ? color.yellow : color.cyan;
|
||||||
|
const bar = barColor(clack.S_BAR);
|
||||||
|
const barEnd = barColor(clack.S_BAR_END);
|
||||||
|
|
||||||
|
const title = `${color.gray(clack.S_BAR)}\n${clack.symbol(this.state)} ${options.message}\n`;
|
||||||
|
|
||||||
|
const userInput = this.userInput;
|
||||||
|
const placeholder = options.placeholder || 'Type to search...';
|
||||||
|
const hasPlaceholder = userInput === '' && placeholder !== undefined;
|
||||||
|
|
||||||
|
// Show placeholder or user input with cursor
|
||||||
|
const searchDisplay =
|
||||||
|
this.isNavigating || hasPlaceholder ? color.dim(hasPlaceholder ? placeholder : userInput) : this.userInputWithCursor;
|
||||||
|
|
||||||
|
const allOptions = this.options;
|
||||||
|
const matchCount =
|
||||||
|
this.filteredOptions.length === allOptions.length
|
||||||
|
? ''
|
||||||
|
: color.dim(` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`);
|
||||||
|
|
||||||
|
// Render option with checkbox
|
||||||
|
const renderOption = (opt, isHighlighted) => {
|
||||||
|
const isSelected = this.selectedValues.includes(opt.value);
|
||||||
|
const label = opt.label ?? String(opt.value ?? '');
|
||||||
|
const hintText = opt.hint && opt.value === this.focusedValue ? color.dim(` (${opt.hint})`) : '';
|
||||||
|
const checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE);
|
||||||
|
return isHighlighted ? `${checkbox} ${label}${hintText}` : `${checkbox} ${color.dim(label)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (this.state) {
|
||||||
|
case 'submit': {
|
||||||
|
return `${title}${color.gray(clack.S_BAR)} ${color.dim(`${this.selectedValues.length} items selected`)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'cancel': {
|
||||||
|
return `${title}${color.gray(clack.S_BAR)} ${color.strikethrough(color.dim(userInput))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
// Always show "SPACE:" regardless of isNavigating state
|
||||||
|
const hints = [`${color.dim('↑/↓')} to navigate`, `${color.dim('TAB/SPACE:')} select`, `${color.dim('ENTER:')} confirm`];
|
||||||
|
|
||||||
|
const noMatchesLine = this.filteredOptions.length === 0 && userInput ? [`${bar} ${color.yellow('No matches found')}`] : [];
|
||||||
|
|
||||||
|
const errorLine = this.state === 'error' ? [`${bar} ${color.yellow(this.error)}`] : [];
|
||||||
|
|
||||||
|
const headerLines = [...`${title}${bar}`.split('\n'), `${bar} ${searchDisplay}${matchCount}`, ...noMatchesLine, ...errorLine];
|
||||||
|
|
||||||
|
const footerLines = [`${bar} ${color.dim(hints.join(' • '))}`, `${barEnd}`];
|
||||||
|
|
||||||
|
const optionLines = clack.limitOptions({
|
||||||
|
cursor: this.cursor,
|
||||||
|
options: this.filteredOptions,
|
||||||
|
style: renderOption,
|
||||||
|
maxItems: options.maxItems || 5,
|
||||||
|
output: options.output,
|
||||||
|
rowPadding: headerLines.length + footerLines.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...headerLines, ...optionLines.map((line) => `${bar} ${line}`), ...footerLines].join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await prompt.prompt();
|
||||||
|
await handleCancel(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm prompt (replaces Inquirer 'confirm' type)
|
* Confirm prompt (replaces Inquirer 'confirm' type)
|
||||||
* @param {Object} options - Prompt options
|
* @param {Object} options - Prompt options
|
||||||
|
|
@ -211,7 +347,12 @@ async function confirm(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Text input prompt (replaces Inquirer 'input' type)
|
* Text input prompt with Tab-to-fill-placeholder support (replaces Inquirer 'input' type)
|
||||||
|
*
|
||||||
|
* This custom implementation restores the Tab-to-fill-placeholder behavior that was
|
||||||
|
* intentionally removed in @clack/prompts v1.0.0 (placeholder became purely visual).
|
||||||
|
* Uses @clack/core's TextPrompt primitive with custom key handling.
|
||||||
|
*
|
||||||
* @param {Object} options - Prompt options
|
* @param {Object} options - Prompt options
|
||||||
* @param {string} options.message - The question to ask
|
* @param {string} options.message - The question to ask
|
||||||
* @param {string} [options.default] - Default value
|
* @param {string} [options.default] - Default value
|
||||||
|
|
@ -220,20 +361,64 @@ async function confirm(options) {
|
||||||
* @returns {Promise<string>} User's input
|
* @returns {Promise<string>} User's input
|
||||||
*/
|
*/
|
||||||
async function text(options) {
|
async function text(options) {
|
||||||
const clack = await getClack();
|
const core = await getClackCore();
|
||||||
|
const color = await getPicocolors();
|
||||||
|
|
||||||
// Use default as placeholder if placeholder not explicitly provided
|
// Use default as placeholder if placeholder not explicitly provided
|
||||||
// This shows the default value as grayed-out hint text
|
// This shows the default value as grayed-out hint text
|
||||||
const placeholder = options.placeholder === undefined ? options.default : options.placeholder;
|
const placeholder = options.placeholder === undefined ? options.default : options.placeholder;
|
||||||
|
const defaultValue = options.default;
|
||||||
|
|
||||||
const result = await clack.text({
|
const prompt = new core.TextPrompt({
|
||||||
message: options.message,
|
defaultValue,
|
||||||
defaultValue: options.default,
|
|
||||||
placeholder: typeof placeholder === 'string' ? placeholder : undefined,
|
|
||||||
validate: options.validate,
|
validate: options.validate,
|
||||||
|
render() {
|
||||||
|
const title = `${color.gray('◆')} ${options.message}`;
|
||||||
|
|
||||||
|
// Show placeholder as dim text when input is empty
|
||||||
|
let valueDisplay;
|
||||||
|
if (this.state === 'error') {
|
||||||
|
valueDisplay = color.yellow(this.userInputWithCursor);
|
||||||
|
} else if (this.userInput) {
|
||||||
|
valueDisplay = this.userInputWithCursor;
|
||||||
|
} else if (placeholder) {
|
||||||
|
// Show placeholder with cursor indicator when empty
|
||||||
|
valueDisplay = `${color.inverse(color.hidden('_'))}${color.dim(placeholder)}`;
|
||||||
|
} else {
|
||||||
|
valueDisplay = color.inverse(color.hidden('_'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const bar = color.gray('│');
|
||||||
|
|
||||||
|
// Handle different states
|
||||||
|
if (this.state === 'submit') {
|
||||||
|
return `${color.gray('◇')} ${options.message}\n${bar} ${color.dim(this.value || defaultValue || '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state === 'cancel') {
|
||||||
|
return `${color.gray('◇')} ${options.message}\n${bar} ${color.strikethrough(color.dim(this.userInput || ''))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state === 'error') {
|
||||||
|
return `${color.yellow('▲')} ${options.message}\n${bar} ${valueDisplay}\n${color.yellow('│')} ${color.yellow(this.error)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${title}\n${bar} ${valueDisplay}\n${bar}`;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add Tab key handler to fill placeholder into input
|
||||||
|
prompt.on('key', (char) => {
|
||||||
|
if (char === '\t' && placeholder && !prompt.userInput) {
|
||||||
|
// Use _setUserInput with write=true to populate the readline and update internal state
|
||||||
|
prompt._setUserInput(placeholder, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await prompt.prompt();
|
||||||
await handleCancel(result);
|
await handleCancel(result);
|
||||||
|
|
||||||
|
// TextPrompt's finalize handler already applies defaultValue for empty input
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -423,6 +608,7 @@ module.exports = {
|
||||||
select,
|
select,
|
||||||
multiselect,
|
multiselect,
|
||||||
groupMultiselect,
|
groupMultiselect,
|
||||||
|
autocompleteMultiselect,
|
||||||
confirm,
|
confirm,
|
||||||
text,
|
text,
|
||||||
password,
|
password,
|
||||||
|
|
|
||||||
|
|
@ -344,6 +344,9 @@ class UI {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompt for tool/IDE selection (called after module configuration)
|
* Prompt for tool/IDE selection (called after module configuration)
|
||||||
|
* Uses a split prompt approach:
|
||||||
|
* 1. Recommended tools - standard multiselect for 3 preferred tools
|
||||||
|
* 2. Additional tools - autocompleteMultiselect with search capability
|
||||||
* @param {string} projectDir - Project directory to check for existing IDEs
|
* @param {string} projectDir - Project directory to check for existing IDEs
|
||||||
* @returns {Object} Tool configuration
|
* @returns {Object} Tool configuration
|
||||||
*/
|
*/
|
||||||
|
|
@ -366,95 +369,123 @@ class UI {
|
||||||
const preferredIdes = ideManager.getPreferredIdes();
|
const preferredIdes = ideManager.getPreferredIdes();
|
||||||
const otherIdes = ideManager.getOtherIdes();
|
const otherIdes = ideManager.getOtherIdes();
|
||||||
|
|
||||||
// Build grouped options object for groupMultiselect
|
// Determine which configured IDEs are in "preferred" vs "other" categories
|
||||||
const groupedOptions = {};
|
const configuredPreferred = configuredIdes.filter((id) => preferredIdes.some((ide) => ide.value === id));
|
||||||
const processedIdes = new Set();
|
const configuredOther = configuredIdes.filter((id) => otherIdes.some((ide) => ide.value === id));
|
||||||
const initialValues = [];
|
|
||||||
|
|
||||||
// First, add previously configured IDEs, marked with ✅
|
// Warn about previously configured tools that are no longer available
|
||||||
|
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) {
|
||||||
|
console.log(chalk.yellow(`⚠️ Previously configured tools are no longer available: ${unknownTools.join(', ')}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// UPGRADE PATH: If tools already configured, show all tools with configured at top
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
if (configuredIdes.length > 0) {
|
if (configuredIdes.length > 0) {
|
||||||
const configuredGroup = [];
|
const allTools = [...preferredIdes, ...otherIdes];
|
||||||
for (const ideValue of configuredIdes) {
|
|
||||||
// Skip empty or invalid IDE values
|
|
||||||
if (!ideValue || typeof ideValue !== 'string') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the IDE in either preferred or other lists
|
// Sort: configured tools first, then preferred, then others
|
||||||
const preferredIde = preferredIdes.find((ide) => ide.value === ideValue);
|
const sortedTools = [
|
||||||
const otherIde = otherIdes.find((ide) => ide.value === ideValue);
|
...allTools.filter((ide) => configuredIdes.includes(ide.value)),
|
||||||
const ide = preferredIde || otherIde;
|
...allTools.filter((ide) => !configuredIdes.includes(ide.value)),
|
||||||
|
];
|
||||||
|
|
||||||
if (ide) {
|
const upgradeOptions = sortedTools.map((ide) => {
|
||||||
configuredGroup.push({
|
const isConfigured = configuredIdes.includes(ide.value);
|
||||||
label: `${ide.name} ✅`,
|
const isPreferred = preferredIdes.some((p) => p.value === ide.value);
|
||||||
value: ide.value,
|
let label = ide.name;
|
||||||
|
if (isPreferred) label += ' ⭐';
|
||||||
|
if (isConfigured) label += ' ✅';
|
||||||
|
return { label, value: ide.value };
|
||||||
});
|
});
|
||||||
processedIdes.add(ide.value);
|
|
||||||
initialValues.push(ide.value); // Pre-select configured IDEs
|
// Sort initialValues to match display order
|
||||||
} else {
|
const sortedInitialValues = sortedTools.filter((ide) => configuredIdes.includes(ide.value)).map((ide) => ide.value);
|
||||||
// Warn about unrecognized IDE (but don't fail)
|
|
||||||
console.log(chalk.yellow(`⚠️ Previously configured IDE '${ideValue}' is no longer available`));
|
const upgradeSelected = await prompts.autocompleteMultiselect({
|
||||||
}
|
message: 'Integrate with',
|
||||||
}
|
options: upgradeOptions,
|
||||||
if (configuredGroup.length > 0) {
|
initialValues: sortedInitialValues,
|
||||||
groupedOptions['Previously Configured'] = configuredGroup;
|
required: false,
|
||||||
}
|
maxItems: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedIdes = upgradeSelected || [];
|
||||||
|
|
||||||
|
if (selectedIdes.length === 0) {
|
||||||
|
console.log('');
|
||||||
|
const confirmNoTools = await prompts.confirm({
|
||||||
|
message: 'No tools selected. Continue without installing any tools?',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmNoTools) {
|
||||||
|
return this.promptToolSelection(projectDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add preferred tools (excluding already processed)
|
return { ides: [], skipIde: true };
|
||||||
const remainingPreferred = preferredIdes.filter((ide) => !processedIdes.has(ide.value));
|
}
|
||||||
if (remainingPreferred.length > 0) {
|
|
||||||
groupedOptions['Recommended Tools'] = remainingPreferred.map((ide) => {
|
// Display selected tools
|
||||||
processedIdes.add(ide.value);
|
this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||||
|
|
||||||
|
return { ides: selectedIdes, skipIde: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// NEW INSTALL: Show all tools with search
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
const allTools = [...preferredIdes, ...otherIdes];
|
||||||
|
|
||||||
|
const allToolOptions = allTools.map((ide) => {
|
||||||
|
const isPreferred = preferredIdes.some((p) => p.value === ide.value);
|
||||||
|
let label = ide.name;
|
||||||
|
if (isPreferred) label += ' ⭐';
|
||||||
return {
|
return {
|
||||||
label: `${ide.name} ⭐`,
|
label,
|
||||||
value: ide.value,
|
value: ide.value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Add other tools (excluding already processed)
|
const selectedIdes = await prompts.autocompleteMultiselect({
|
||||||
const remainingOther = otherIdes.filter((ide) => !processedIdes.has(ide.value));
|
message: 'Select tools:',
|
||||||
if (remainingOther.length > 0) {
|
options: allToolOptions,
|
||||||
groupedOptions['Additional Tools'] = remainingOther.map((ide) => ({
|
initialValues: configuredIdes.length > 0 ? configuredIdes : undefined,
|
||||||
label: ide.name,
|
required: false,
|
||||||
value: ide.value,
|
maxItems: 8,
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add standalone "None" option at the end
|
|
||||||
groupedOptions[' '] = [
|
|
||||||
{
|
|
||||||
label: '⚠ None - I am not installing any tools',
|
|
||||||
value: '__NONE__',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let selectedIdes = [];
|
|
||||||
|
|
||||||
selectedIdes = await prompts.groupMultiselect({
|
|
||||||
message: `Select tools to configure ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
|
|
||||||
options: groupedOptions,
|
|
||||||
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
|
||||||
required: true,
|
|
||||||
selectableGroups: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If user selected both "__NONE__" and other tools, honor the "None" choice
|
const allSelectedIdes = selectedIdes || [];
|
||||||
if (selectedIdes && selectedIdes.includes('__NONE__') && selectedIdes.length > 1) {
|
|
||||||
console.log();
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
console.log(chalk.yellow('⚠️ "None - I am not installing any tools" was selected, so no tools will be configured.'));
|
// STEP 3: Confirm if no tools selected
|
||||||
console.log();
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
selectedIdes = [];
|
if (allSelectedIdes.length === 0) {
|
||||||
} else if (selectedIdes && selectedIdes.includes('__NONE__')) {
|
console.log('');
|
||||||
// Only "__NONE__" was selected
|
const confirmNoTools = await prompts.confirm({
|
||||||
selectedIdes = [];
|
message: 'No tools selected. Continue without installing any tools?',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmNoTools) {
|
||||||
|
// User wants to select tools - recurse
|
||||||
|
return this.promptToolSelection(projectDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ides: selectedIdes || [],
|
ides: [],
|
||||||
skipIde: !selectedIdes || selectedIdes.length === 0,
|
skipIde: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display selected tools
|
||||||
|
this.displaySelectedTools(allSelectedIdes, preferredIdes, allTools);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ides: allSelectedIdes,
|
||||||
|
skipIde: allSelectedIdes.length === 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1655,6 +1686,27 @@ class UI {
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display list of selected tools after IDE selection
|
||||||
|
* @param {Array} selectedIdes - Array of selected IDE values
|
||||||
|
* @param {Array} preferredIdes - Array of preferred IDE objects
|
||||||
|
* @param {Array} allTools - Array of all tool objects
|
||||||
|
*/
|
||||||
|
displaySelectedTools(selectedIdes, preferredIdes, allTools) {
|
||||||
|
if (selectedIdes.length === 0) return;
|
||||||
|
|
||||||
|
const preferredValues = new Set(preferredIdes.map((ide) => ide.value));
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(chalk.dim(' Selected tools:'));
|
||||||
|
for (const ideValue of selectedIdes) {
|
||||||
|
const tool = allTools.find((t) => t.value === ideValue);
|
||||||
|
const name = tool?.name || ideValue;
|
||||||
|
const marker = preferredValues.has(ideValue) ? ' ⭐' : '';
|
||||||
|
console.log(chalk.dim(` • ${name}${marker}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { UI };
|
module.exports = { UI };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue