Compare commits
4 Commits
e567326150
...
f7b3b5a2c6
| Author | SHA1 | Date |
|---|---|---|
|
|
f7b3b5a2c6 | |
|
|
ea99b7ece5 | |
|
|
eabcd03f65 | |
|
|
17da5ca8ca |
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
||||||
spec_file: '' # set at runtime for both routes before leaving this step
|
spec_file: '' # set at runtime for both routes before leaving this step
|
||||||
|
story_key: '' # set at runtime to the current story's full sprint-status key (e.g. 3-2-digest-delivery) when the intent is an epic story and sprint-status resolution succeeds
|
||||||
---
|
---
|
||||||
|
|
||||||
# Step 1: Clarify and Route
|
# Step 1: Clarify and Route
|
||||||
|
|
@ -20,7 +21,7 @@ Before listing artifacts or prompting the user, check whether you already know t
|
||||||
|
|
||||||
1. Explicit argument
|
1. Explicit argument
|
||||||
Did the user pass a specific file path, spec name, or clear instruction this message?
|
Did the user pass a specific file path, spec name, or clear instruction this message?
|
||||||
- If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, in-review, or done) → set `spec_file` and **EARLY EXIT** to the appropriate step (step-02 for draft, step-03 for ready/in-progress, step-04 for review). For `done`, ingest as context and proceed to INSTRUCTIONS — do not resume.
|
- If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, in-review, or done) → set `spec_file`. Before exiting, run **Story-key resolution** (below). Then **EARLY EXIT** to the appropriate step (step-02 for draft, step-03 for ready/in-progress, step-04 for review). For `done`, ingest as context and proceed to INSTRUCTIONS — do not resume.
|
||||||
- Anything else (intent files, external docs, plans, descriptions) → ingest it as starting intent and proceed to INSTRUCTIONS. Do not attempt to infer a workflow state from it.
|
- Anything else (intent files, external docs, plans, descriptions) → ingest it as starting intent and proceed to INSTRUCTIONS. Do not attempt to infer a workflow state from it.
|
||||||
|
|
||||||
2. Recent conversation
|
2. Recent conversation
|
||||||
|
|
@ -29,13 +30,19 @@ Before listing artifacts or prompting the user, check whether you already know t
|
||||||
|
|
||||||
3. Otherwise — scan artifacts and ask
|
3. Otherwise — scan artifacts and ask
|
||||||
- Active specs (`draft`, `ready-for-dev`, `in-progress`, `in-review`) in `{implementation_artifacts}`? → List them and HALT. Ask user which to resume (or `[N]` for new).
|
- Active specs (`draft`, `ready-for-dev`, `in-progress`, `in-review`) in `{implementation_artifacts}`? → List them and HALT. Ask user which to resume (or `[N]` for new).
|
||||||
- If `draft` selected: Set `spec_file`. **EARLY EXIT** → `./step-02-plan.md` (resume planning from the draft)
|
- If `draft` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-02-plan.md` (resume planning from the draft)
|
||||||
- If `ready-for-dev` or `in-progress` selected: Set `spec_file`. **EARLY EXIT** → `./step-03-implement.md`
|
- If `ready-for-dev` or `in-progress` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-03-implement.md`
|
||||||
- If `in-review` selected: Set `spec_file`. **EARLY EXIT** → `./step-04-review.md`
|
- If `in-review` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-04-review.md`
|
||||||
- Unformatted spec or intent file lacking `status` frontmatter? → Suggest treating its contents as the starting intent. Do NOT attempt to infer a state and resume it.
|
- Unformatted spec or intent file lacking `status` frontmatter? → Suggest treating its contents as the starting intent. Do NOT attempt to infer a state and resume it.
|
||||||
|
|
||||||
Never ask extra questions if you already understand what the user intends.
|
Never ask extra questions if you already understand what the user intends.
|
||||||
|
|
||||||
|
### Story-key resolution
|
||||||
|
|
||||||
|
This runs on ALL paths (early-exit and INSTRUCTIONS) whenever `spec_file` is set. Determine whether the spec is an epic story — use the spec's filename, frontmatter, and any loaded epics file to identify `{epic_num}` and `{story_num}`. If the spec is not an epic story, skip silently and leave `{story_key}` unset.
|
||||||
|
|
||||||
|
If the spec is an epic story and `{sprint_status}` exists: find the `development_status` key matching `{epic_num}-{story_num}` by exact numeric equality on the first two segments (so `1-1` never collides with `1-10`). Exactly one match → set `{story_key}` to that full key. Zero or multiple matches → leave `{story_key}` unset (warn on multiple).
|
||||||
|
|
||||||
## INSTRUCTIONS
|
## INSTRUCTIONS
|
||||||
|
|
||||||
1. Load context.
|
1. Load context.
|
||||||
|
|
@ -45,7 +52,7 @@ Never ask extra questions if you already understand what the user intends.
|
||||||
|
|
||||||
**A) Epic story path** — if the intent is clearly an epic story:
|
**A) Epic story path** — if the intent is clearly an epic story:
|
||||||
|
|
||||||
1. Identify the epic number and (if present) the story number. If you can't identify an epic number, use path B.
|
1. Identify the epic number `{epic_num}` and (if present) the story number `{story_num}`. If you can't identify an epic number, use path B.
|
||||||
|
|
||||||
2. **Check for a valid cached epic context.** Look for `{implementation_artifacts}/epic-<N>-context.md` (where `<N>` is the epic number). A file is **valid** when it exists, is non-empty, starts with `# Epic <N> Context:` (with the correct epic number), and no file in `{planning_artifacts}` is newer.
|
2. **Check for a valid cached epic context.** Look for `{implementation_artifacts}/epic-<N>-context.md` (where `<N>` is the epic number). A file is **valid** when it exists, is non-empty, starts with `# Epic <N> Context:` (with the correct epic number), and no file in `{planning_artifacts}` is newer.
|
||||||
- **If valid:** load it as the primary planning context. Do not load raw planning docs (PRD, architecture, UX, etc.). Skip to step 5.
|
- **If valid:** load it as the primary planning context. Do not load raw planning docs (PRD, architecture, UX, etc.). Skip to step 5.
|
||||||
|
|
@ -59,6 +66,8 @@ Never ask extra questions if you already understand what the user intends.
|
||||||
|
|
||||||
5. **Previous story continuity.** Regardless of which context source succeeded above, scan `{implementation_artifacts}` for specs from the same epic with `status: done` and a lower story number. Load the most recent one (highest story number below current). Extract its **Code Map**, **Design Notes**, **Spec Change Log**, and **task list** as continuity context for step-02 planning. If no `done` spec is found but an `in-review` spec exists for the same epic with a lower story number, note it to the user and ask whether to load it.
|
5. **Previous story continuity.** Regardless of which context source succeeded above, scan `{implementation_artifacts}` for specs from the same epic with `status: done` and a lower story number. Load the most recent one (highest story number below current). Extract its **Code Map**, **Design Notes**, **Spec Change Log**, and **task list** as continuity context for step-02 planning. If no `done` spec is found but an `in-review` spec exists for the same epic with a lower story number, note it to the user and ask whether to load it.
|
||||||
|
|
||||||
|
6. **Resolve `{story_key}`.** If not already set by an earlier early-exit path, run **Story-key resolution** (above) now.
|
||||||
|
|
||||||
**B) Freeform path** — if the intent is not an epic story:
|
**B) Freeform path** — if the intent is not an epic story:
|
||||||
- Planning artifacts are the output of BMAD phases 1-3. Typical files include:
|
- Planning artifacts are the output of BMAD phases 1-3. Typical files include:
|
||||||
- **PRD** (`*prd*`) — product requirements and success criteria
|
- **PRD** (`*prd*`) — product requirements and success criteria
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ Capture `baseline_commit` (current HEAD, or `NO_VCS` if version control is unava
|
||||||
|
|
||||||
Change `{spec_file}` status to `in-progress` in the frontmatter before starting implementation.
|
Change `{spec_file}` status to `in-progress` in the frontmatter before starting implementation.
|
||||||
|
|
||||||
|
Follow `./sync-sprint-status.md` with `{target_status}` = `in-progress`.
|
||||||
|
|
||||||
If `{spec_file}` has a non-empty `context:` list in its frontmatter, load those files before implementation begins. When handing to a sub-agent, include them in the sub-agent prompt so it has access to the referenced context.
|
If `{spec_file}` has a non-empty `context:` list in its frontmatter, load those files before implementation begins. When handing to a sub-agent, include them in the sub-agent prompt so it has access to the referenced context.
|
||||||
|
|
||||||
Hand `{spec_file}` to a sub-agent/task and let it implement. If no sub-agents are available, implement directly.
|
Hand `{spec_file}` to a sub-agent/task and let it implement. If no sub-agents are available, implement directly.
|
||||||
|
|
|
||||||
|
|
@ -48,16 +48,25 @@ Format each stop as framing first, link on the next indented line:
|
||||||
|
|
||||||
When there is only one concern, omit the bold label — just list the stops directly.
|
When there is only one concern, omit the bold label — just list the stops directly.
|
||||||
|
|
||||||
### Commit and Present
|
### Mark Spec Done
|
||||||
|
|
||||||
1. Change `{spec_file}` status to `done` in the frontmatter.
|
Change `{spec_file}` status to `done` in the frontmatter.
|
||||||
2. If version control is available and the tree is dirty, create a local commit with a conventional message derived from the spec title.
|
|
||||||
3. Open the spec in the user's editor so they can click through the Suggested Review Order:
|
Follow `./sync-sprint-status.md` with `{target_status}` = `review`.
|
||||||
|
|
||||||
|
### Commit and Open
|
||||||
|
|
||||||
|
1. If version control is available and the tree is dirty, create a local commit with a conventional message derived from the spec title.
|
||||||
|
2. Open the spec in the user's editor so they can click through the Suggested Review Order:
|
||||||
- Resolve two absolute paths: (1) the repository root (`git rev-parse --show-toplevel` — returns the worktree root when in a worktree, project root otherwise; if this fails, fall back to the current working directory), (2) `{spec_file}`. Run `code -r "{absolute-root}" "{absolute-spec-file}"` — the root first so VS Code opens in the right context, then the spec file. Always double-quote paths to handle spaces and special characters.
|
- Resolve two absolute paths: (1) the repository root (`git rev-parse --show-toplevel` — returns the worktree root when in a worktree, project root otherwise; if this fails, fall back to the current working directory), (2) `{spec_file}`. Run `code -r "{absolute-root}" "{absolute-spec-file}"` — the root first so VS Code opens in the right context, then the spec file. Always double-quote paths to handle spaces and special characters.
|
||||||
- If `code` is not available (command fails), skip gracefully and tell the user the spec file path instead.
|
- If `code` is not available (command fails), skip gracefully and tell the user the spec file path instead.
|
||||||
4. Display summary of your work to the user, including the commit hash if one was created. Any file paths shown in conversation/terminal output must use CWD-relative format (no leading `/`) with `:line` notation (e.g., `src/path/file.ts:42`) for terminal clickability — the goal is to make paths clickable in terminal emulators. Include:
|
|
||||||
- A note that the spec is open in their editor (or the file path if it couldn't be opened). Mention that `{spec_file}` now contains a Suggested Review Order.
|
### Display Summary
|
||||||
- **Navigation tip:** "Ctrl+click (Cmd+click on macOS) the links in the Suggested Review Order to jump to each stop."
|
|
||||||
- Offer to push and/or create a pull request.
|
Display summary of your work to the user, including the commit hash if one was created. Any file paths shown in conversation/terminal output must use CWD-relative format (no leading `/`) with `:line` notation (e.g., `src/path/file.ts:42`) for terminal clickability — the goal is to make paths clickable in terminal emulators. Include:
|
||||||
|
|
||||||
|
- A note that the spec is open in their editor (or the file path if it couldn't be opened). Mention that `{spec_file}` now contains a Suggested Review Order.
|
||||||
|
- **Navigation tip:** "Ctrl+click (Cmd+click on macOS) the links in the Suggested Review Order to jump to each stop."
|
||||||
|
- Offer to push and/or create a pull request.
|
||||||
|
|
||||||
Workflow complete.
|
Workflow complete.
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
||||||
|
|
||||||
### Implement
|
### Implement
|
||||||
|
|
||||||
|
Follow `./sync-sprint-status.md` with `{target_status}` = `in-progress`.
|
||||||
|
|
||||||
Implement the clarified intent directly.
|
Implement the clarified intent directly.
|
||||||
|
|
||||||
### Review
|
### Review
|
||||||
|
|
@ -39,6 +41,8 @@ Write `{spec_file}` using `./spec-template.md`. Fill only these sections — del
|
||||||
2. **Title and Intent** — `# {title}` heading and `## Intent` with **Problem** and **Approach** lines. Reuse the summary you already generated for the terminal.
|
2. **Title and Intent** — `# {title}` heading and `## Intent` with **Problem** and **Approach** lines. Reuse the summary you already generated for the terminal.
|
||||||
3. **Suggested Review Order** — append after Intent. Build using the same convention as `./step-05-present.md` § "Generate Suggested Review Order" (spec-file-relative links, concern-based ordering, ultra-concise framing).
|
3. **Suggested Review Order** — append after Intent. Build using the same convention as `./step-05-present.md` § "Generate Suggested Review Order" (spec-file-relative links, concern-based ordering, ultra-concise framing).
|
||||||
|
|
||||||
|
Follow `./sync-sprint-status.md` with `{target_status}` = `review`.
|
||||||
|
|
||||||
### Commit
|
### Commit
|
||||||
|
|
||||||
If version control is available and the tree is dirty, create a local commit with a conventional message derived from the intent. If VCS is unavailable, skip.
|
If version control is available and the tree is dirty, create a local commit with a conventional message derived from the intent. If VCS is unavailable, skip.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Sync Sprint Status
|
||||||
|
|
||||||
|
Shared sub-step for updating `sprint-status.yaml` during quick-dev. Called from any route (plan-code-review, one-shot, future routes) with a `{target_status}` parameter.
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
Skip this entire file (return to caller) if ANY of:
|
||||||
|
- `{story_key}` is unset
|
||||||
|
- `{sprint_status}` does not exist on disk
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. Load the FULL `{sprint_status}` file.
|
||||||
|
2. Find the `development_status` entry matching `{story_key}`. If not found, warn the user once (`"{story_key} not found in sprint-status; skipping sprint sync"`) and return to caller.
|
||||||
|
3. **Idempotency check.** If `development_status[{story_key}]` is already at `{target_status}` or a later state (`review` is later than `in-progress`; `done` is later than both), return to caller — no write needed. Never regress a story's status.
|
||||||
|
4. Set `development_status[{story_key}]` to `{target_status}`.
|
||||||
|
5. **Epic lift (only when `{target_status}` = `in-progress`).** Derive the parent epic key as `epic-{N}` from the leading numeric segment of `{story_key}` (e.g., `3-2-digest-delivery` → `epic-3`). If that entry exists and is `backlog`, set it to `in-progress`. Leave it alone otherwise. Skip this sub-step entirely when `{target_status}` is not `in-progress`.
|
||||||
|
6. Refresh `last_updated` to the current date.
|
||||||
|
7. Save the file, preserving ALL comments and structure including STATUS DEFINITIONS and WORKFLOW NOTES.
|
||||||
|
|
@ -65,6 +65,7 @@ Load and read full config from `{main_config}` and resolve:
|
||||||
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
|
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
|
||||||
- `communication_language`, `document_output_language`, `user_skill_level`
|
- `communication_language`, `document_output_language`, `user_skill_level`
|
||||||
- `date` as system-generated current datetime
|
- `date` as system-generated current datetime
|
||||||
|
- `sprint_status` = `{implementation_artifacts}/sprint-status.yaml`
|
||||||
- `project_context` = `**/project-context.md` (load if exists)
|
- `project_context` = `**/project-context.md` (load if exists)
|
||||||
- CLAUDE.md / memory files (load if exist)
|
- CLAUDE.md / memory files (load if exist)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1728,36 +1728,6 @@ async function runTests() {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
console.log(`${colors.yellow}Test Suite 33: Community & Custom Module Managers${colors.reset}\n`);
|
console.log(`${colors.yellow}Test Suite 33: Community & Custom Module Managers${colors.reset}\n`);
|
||||||
|
|
||||||
// --- CustomModuleManager.validateGitHubUrl ---
|
|
||||||
{
|
|
||||||
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
|
|
||||||
const mgr = new CustomModuleManager();
|
|
||||||
|
|
||||||
const https1 = mgr.validateGitHubUrl('https://github.com/owner/repo');
|
|
||||||
assert(https1.isValid === true, 'validateGitHubUrl accepts HTTPS URL');
|
|
||||||
assert(https1.owner === 'owner' && https1.repo === 'repo', 'validateGitHubUrl extracts owner/repo from HTTPS');
|
|
||||||
|
|
||||||
const https2 = mgr.validateGitHubUrl('https://github.com/owner/repo.git');
|
|
||||||
assert(https2.isValid === true, 'validateGitHubUrl accepts HTTPS URL with .git');
|
|
||||||
assert(https2.repo === 'repo', 'validateGitHubUrl strips .git suffix');
|
|
||||||
|
|
||||||
const ssh1 = mgr.validateGitHubUrl('git@github.com:owner/repo.git');
|
|
||||||
assert(ssh1.isValid === true, 'validateGitHubUrl accepts SSH URL');
|
|
||||||
assert(ssh1.owner === 'owner' && ssh1.repo === 'repo', 'validateGitHubUrl extracts owner/repo from SSH');
|
|
||||||
|
|
||||||
const bad1 = mgr.validateGitHubUrl('https://gitlab.com/owner/repo');
|
|
||||||
assert(bad1.isValid === false, 'validateGitHubUrl rejects non-GitHub URL');
|
|
||||||
|
|
||||||
const bad2 = mgr.validateGitHubUrl('');
|
|
||||||
assert(bad2.isValid === false, 'validateGitHubUrl rejects empty string');
|
|
||||||
|
|
||||||
const bad3 = mgr.validateGitHubUrl(null);
|
|
||||||
assert(bad3.isValid === false, 'validateGitHubUrl rejects null');
|
|
||||||
|
|
||||||
const bad4 = mgr.validateGitHubUrl('https://github.com/owner');
|
|
||||||
assert(bad4.isValid === false, 'validateGitHubUrl rejects URL without repo');
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- CustomModuleManager._normalizeCustomModule ---
|
// --- CustomModuleManager._normalizeCustomModule ---
|
||||||
{
|
{
|
||||||
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
|
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
|
||||||
|
|
@ -1954,25 +1924,6 @@ async function runTests() {
|
||||||
assert(notFound === null, 'getModuleByCode returns null for unknown code');
|
assert(notFound === null, 'getModuleByCode returns null for unknown code');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CustomModuleManager URL edge cases ---
|
|
||||||
{
|
|
||||||
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
|
|
||||||
const mgr = new CustomModuleManager();
|
|
||||||
|
|
||||||
// HTTP (not HTTPS) should work
|
|
||||||
const http = mgr.validateGitHubUrl('http://github.com/owner/repo');
|
|
||||||
assert(http.isValid === true, 'validateGitHubUrl accepts HTTP URL');
|
|
||||||
|
|
||||||
// Trailing slash should be rejected (strict matching)
|
|
||||||
const trailing = mgr.validateGitHubUrl('https://github.com/owner/repo/');
|
|
||||||
assert(trailing.isValid === false, 'validateGitHubUrl rejects trailing slash');
|
|
||||||
|
|
||||||
// SSH without .git should work
|
|
||||||
const sshNoDotGit = mgr.validateGitHubUrl('git@github.com:owner/repo');
|
|
||||||
assert(sshNoDotGit.isValid === true, 'validateGitHubUrl accepts SSH without .git');
|
|
||||||
assert(sshNoDotGit.repo === 'repo', 'validateGitHubUrl extracts repo from SSH without .git');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,6 @@
|
||||||
const path = require('node:path');
|
|
||||||
const os = require('node:os');
|
|
||||||
const prompts = require('./prompts');
|
const prompts = require('./prompts');
|
||||||
|
|
||||||
const CLIUtils = {
|
const CLIUtils = {
|
||||||
/**
|
|
||||||
* Get version from package.json
|
|
||||||
*/
|
|
||||||
getVersion() {
|
|
||||||
try {
|
|
||||||
const packageJson = require(path.join(__dirname, '..', '..', 'package.json'));
|
|
||||||
return packageJson.version || 'Unknown';
|
|
||||||
} catch {
|
|
||||||
return 'Unknown';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display BMAD logo and version using @clack intro + box
|
* Display BMAD logo and version using @clack intro + box
|
||||||
*/
|
*/
|
||||||
|
|
@ -52,37 +38,6 @@ const CLIUtils = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Display section header
|
|
||||||
* @param {string} title - Section title
|
|
||||||
* @param {string} subtitle - Optional subtitle
|
|
||||||
*/
|
|
||||||
async displaySection(title, subtitle = null) {
|
|
||||||
await prompts.note(subtitle || '', title);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display info box
|
|
||||||
* @param {string|Array} content - Content to display
|
|
||||||
* @param {Object} options - Box options
|
|
||||||
*/
|
|
||||||
async displayBox(content, options = {}) {
|
|
||||||
let text = content;
|
|
||||||
if (Array.isArray(content)) {
|
|
||||||
text = content.join('\n\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = await prompts.getColor();
|
|
||||||
const borderColor = options.borderColor || 'cyan';
|
|
||||||
const colorMap = { green: color.green, red: color.red, yellow: color.yellow, cyan: color.cyan, blue: color.blue };
|
|
||||||
const formatBorder = colorMap[borderColor] || color.cyan;
|
|
||||||
|
|
||||||
await prompts.box(text, options.title, {
|
|
||||||
rounded: options.borderStyle === 'round' || options.borderStyle === undefined,
|
|
||||||
formatBorder,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display module configuration header
|
* Display module configuration header
|
||||||
* @param {string} moduleName - Module name (fallback if no custom header)
|
* @param {string} moduleName - Module name (fallback if no custom header)
|
||||||
|
|
@ -93,98 +48,6 @@ const CLIUtils = {
|
||||||
const title = header || `Configuring ${moduleName.toUpperCase()} Module`;
|
const title = header || `Configuring ${moduleName.toUpperCase()} Module`;
|
||||||
await prompts.note(subheader || '', title);
|
await prompts.note(subheader || '', title);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Display module with no custom configuration
|
|
||||||
* @param {string} moduleName - Module name (fallback if no custom header)
|
|
||||||
* @param {string} header - Custom header from module.yaml
|
|
||||||
* @param {string} subheader - Custom subheader from module.yaml
|
|
||||||
*/
|
|
||||||
async displayModuleNoConfig(moduleName, header = null, subheader = null) {
|
|
||||||
const title = header || `${moduleName.toUpperCase()} Module - No Custom Configuration`;
|
|
||||||
await prompts.note(subheader || '', title);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display step indicator
|
|
||||||
* @param {number} current - Current step
|
|
||||||
* @param {number} total - Total steps
|
|
||||||
* @param {string} description - Step description
|
|
||||||
*/
|
|
||||||
async displayStep(current, total, description) {
|
|
||||||
const progress = `[${current}/${total}]`;
|
|
||||||
await prompts.log.step(`${progress} ${description}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display completion message
|
|
||||||
* @param {string} message - Completion message
|
|
||||||
*/
|
|
||||||
async displayComplete(message) {
|
|
||||||
const color = await prompts.getColor();
|
|
||||||
await prompts.box(`\u2728 ${message}`, 'Complete', {
|
|
||||||
rounded: true,
|
|
||||||
formatBorder: color.green,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display error message
|
|
||||||
* @param {string} message - Error message
|
|
||||||
*/
|
|
||||||
async displayError(message) {
|
|
||||||
const color = await prompts.getColor();
|
|
||||||
await prompts.box(`\u2717 ${message}`, 'Error', {
|
|
||||||
rounded: true,
|
|
||||||
formatBorder: color.red,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format list for display
|
|
||||||
* @param {Array} items - Items to display
|
|
||||||
* @param {string} prefix - Item prefix
|
|
||||||
*/
|
|
||||||
formatList(items, prefix = '\u2022') {
|
|
||||||
return items.map((item) => ` ${prefix} ${item}`).join('\n');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear previous lines
|
|
||||||
* @param {number} lines - Number of lines to clear
|
|
||||||
*/
|
|
||||||
clearLines(lines) {
|
|
||||||
for (let i = 0; i < lines; i++) {
|
|
||||||
process.stdout.moveCursor(0, -1);
|
|
||||||
process.stdout.clearLine(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display module completion message
|
|
||||||
* @param {string} moduleName - Name of the completed module
|
|
||||||
* @param {boolean} clearScreen - Whether to clear the screen first (deprecated, always false now)
|
|
||||||
*/
|
|
||||||
displayModuleComplete(moduleName, clearScreen = false) {
|
|
||||||
// No longer clear screen or show boxes - just a simple completion message
|
|
||||||
// This is deprecated but kept for backwards compatibility
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expand path with ~ expansion
|
|
||||||
* @param {string} inputPath - Path to expand
|
|
||||||
* @returns {string} Expanded path
|
|
||||||
*/
|
|
||||||
expandPath(inputPath) {
|
|
||||||
if (!inputPath) return inputPath;
|
|
||||||
|
|
||||||
// Expand ~ to home directory
|
|
||||||
if (inputPath.startsWith('~')) {
|
|
||||||
return path.join(os.homedir(), inputPath.slice(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
return inputPath;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { CLIUtils };
|
module.exports = { CLIUtils };
|
||||||
|
|
|
||||||
|
|
@ -107,117 +107,6 @@ class Manifest {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update existing manifest
|
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
|
||||||
* @param {Object} updates - Fields to update
|
|
||||||
* @param {Array} installedFiles - Updated list of installed files
|
|
||||||
*/
|
|
||||||
async update(bmadDir, updates, installedFiles = null) {
|
|
||||||
const yaml = require('yaml');
|
|
||||||
const manifest = (await this._readRaw(bmadDir)) || {
|
|
||||||
installation: {},
|
|
||||||
modules: [],
|
|
||||||
ides: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle module updates
|
|
||||||
if (updates.modules) {
|
|
||||||
// If modules is being updated, we need to preserve detailed module info
|
|
||||||
const existingDetailed = manifest.modules || [];
|
|
||||||
const incomingNames = updates.modules;
|
|
||||||
|
|
||||||
// Build updated modules array
|
|
||||||
const updatedModules = [];
|
|
||||||
for (const name of incomingNames) {
|
|
||||||
const existing = existingDetailed.find((m) => m.name === name);
|
|
||||||
if (existing) {
|
|
||||||
// Preserve existing details, update lastUpdated if this module is being updated
|
|
||||||
updatedModules.push({
|
|
||||||
...existing,
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// New module - add with minimal details
|
|
||||||
updatedModules.push({
|
|
||||||
name,
|
|
||||||
version: null,
|
|
||||||
installDate: new Date().toISOString(),
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
source: 'unknown',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest.modules = updatedModules;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge other updates
|
|
||||||
if (updates.version) {
|
|
||||||
manifest.installation.version = updates.version;
|
|
||||||
}
|
|
||||||
if (updates.installDate) {
|
|
||||||
manifest.installation.installDate = updates.installDate;
|
|
||||||
}
|
|
||||||
manifest.installation.lastUpdated = new Date().toISOString();
|
|
||||||
|
|
||||||
if (updates.ides) {
|
|
||||||
manifest.ides = updates.ides;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle per-module version updates
|
|
||||||
if (updates.moduleVersions) {
|
|
||||||
for (const [moduleName, versionInfo] of Object.entries(updates.moduleVersions)) {
|
|
||||||
const moduleIndex = manifest.modules.findIndex((m) => m.name === moduleName);
|
|
||||||
if (moduleIndex !== -1) {
|
|
||||||
manifest.modules[moduleIndex] = {
|
|
||||||
...manifest.modules[moduleIndex],
|
|
||||||
...versionInfo,
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle adding a new module with version info
|
|
||||||
if (updates.addModule) {
|
|
||||||
const { name, version, source, npmPackage, repoUrl, localPath } = updates.addModule;
|
|
||||||
const existing = manifest.modules.find((m) => m.name === name);
|
|
||||||
if (!existing) {
|
|
||||||
const entry = {
|
|
||||||
name,
|
|
||||||
version: version || null,
|
|
||||||
installDate: new Date().toISOString(),
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
source: source || 'external',
|
|
||||||
npmPackage: npmPackage || null,
|
|
||||||
repoUrl: repoUrl || null,
|
|
||||||
};
|
|
||||||
if (localPath) entry.localPath = localPath;
|
|
||||||
manifest.modules.push(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
|
|
||||||
await fs.ensureDir(path.dirname(manifestPath));
|
|
||||||
|
|
||||||
// Clean the manifest data to remove any non-serializable values
|
|
||||||
const cleanManifestData = structuredClone(manifest);
|
|
||||||
|
|
||||||
const yamlContent = yaml.stringify(cleanManifestData, {
|
|
||||||
indent: 2,
|
|
||||||
lineWidth: 0,
|
|
||||||
sortKeys: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure POSIX-compliant final newline
|
|
||||||
const content = yamlContent.endsWith('\n') ? yamlContent : yamlContent + '\n';
|
|
||||||
await fs.writeFile(manifestPath, content, 'utf8');
|
|
||||||
|
|
||||||
// Return the flattened format for compatibility
|
|
||||||
return this._flattenManifest(manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read raw manifest data without flattening
|
* Read raw manifest data without flattening
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
|
@ -310,62 +199,6 @@ class Manifest {
|
||||||
await this._writeRaw(bmadDir, manifest);
|
await this._writeRaw(bmadDir, manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a module from the manifest
|
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
|
||||||
* @param {string} moduleName - Module name to remove
|
|
||||||
*/
|
|
||||||
async removeModule(bmadDir, moduleName) {
|
|
||||||
const manifest = await this._readRaw(bmadDir);
|
|
||||||
if (!manifest || !manifest.modules) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = manifest.modules.findIndex((m) => m.name === moduleName);
|
|
||||||
if (index !== -1) {
|
|
||||||
manifest.modules.splice(index, 1);
|
|
||||||
await this._writeRaw(bmadDir, manifest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a single module's version info
|
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
|
||||||
* @param {string} moduleName - Module name
|
|
||||||
* @param {Object} versionInfo - Version info to update
|
|
||||||
*/
|
|
||||||
async updateModuleVersion(bmadDir, moduleName, versionInfo) {
|
|
||||||
const manifest = await this._readRaw(bmadDir);
|
|
||||||
if (!manifest || !manifest.modules) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = manifest.modules.findIndex((m) => m.name === moduleName);
|
|
||||||
if (index !== -1) {
|
|
||||||
manifest.modules[index] = {
|
|
||||||
...manifest.modules[index],
|
|
||||||
...versionInfo,
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
await this._writeRaw(bmadDir, manifest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get version info for a specific module
|
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
|
||||||
* @param {string} moduleName - Module name
|
|
||||||
* @returns {Object|null} Module version info or null
|
|
||||||
*/
|
|
||||||
async getModuleVersion(bmadDir, moduleName) {
|
|
||||||
const manifest = await this._readRaw(bmadDir);
|
|
||||||
if (!manifest || !manifest.modules) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return manifest.modules.find((m) => m.name === moduleName) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all modules with their version info
|
* Get all modules with their version info
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
|
@ -403,27 +236,6 @@ class Manifest {
|
||||||
await fs.writeFile(manifestPath, content, 'utf8');
|
await fs.writeFile(manifestPath, content, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an IDE configuration to the manifest
|
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
|
||||||
* @param {string} ideName - IDE name to add
|
|
||||||
*/
|
|
||||||
async addIde(bmadDir, ideName) {
|
|
||||||
const manifest = await this.read(bmadDir);
|
|
||||||
if (!manifest) {
|
|
||||||
throw new Error('No manifest found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!manifest.ides) {
|
|
||||||
manifest.ides = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!manifest.ides.includes(ideName)) {
|
|
||||||
manifest.ides.push(ideName);
|
|
||||||
await this.update(bmadDir, { ides: manifest.ides });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate SHA256 hash of a file
|
* Calculate SHA256 hash of a file
|
||||||
* @param {string} filePath - Path to file
|
* @param {string} filePath - Path to file
|
||||||
|
|
@ -438,354 +250,6 @@ class Manifest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse installed files to extract metadata
|
|
||||||
* @param {Array} installedFiles - List of installed file paths
|
|
||||||
* @param {string} bmadDir - Path to bmad directory for relative paths
|
|
||||||
* @returns {Array} Array of file metadata objects
|
|
||||||
*/
|
|
||||||
async parseInstalledFiles(installedFiles, bmadDir) {
|
|
||||||
const fileMetadata = [];
|
|
||||||
|
|
||||||
for (const filePath of installedFiles) {
|
|
||||||
const fileExt = path.extname(filePath).toLowerCase();
|
|
||||||
// Make path relative to parent of bmad directory, starting with 'bmad/'
|
|
||||||
const relativePath = 'bmad' + filePath.replace(bmadDir, '').replaceAll('\\', '/');
|
|
||||||
|
|
||||||
// Calculate file hash
|
|
||||||
const hash = await this.calculateFileHash(filePath);
|
|
||||||
|
|
||||||
// Handle markdown files - extract XML metadata if present
|
|
||||||
if (fileExt === '.md') {
|
|
||||||
try {
|
|
||||||
if (await fs.pathExists(filePath)) {
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
const metadata = this.extractXmlNodeAttributes(content, filePath, relativePath);
|
|
||||||
|
|
||||||
if (metadata) {
|
|
||||||
// Has XML metadata
|
|
||||||
metadata.hash = hash;
|
|
||||||
fileMetadata.push(metadata);
|
|
||||||
} else {
|
|
||||||
// No XML metadata - still track the file
|
|
||||||
fileMetadata.push({
|
|
||||||
file: relativePath,
|
|
||||||
type: 'md',
|
|
||||||
name: path.basename(filePath, fileExt),
|
|
||||||
title: null,
|
|
||||||
hash: hash,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
await prompts.log.warn(`Could not parse ${filePath}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Handle other file types (CSV, JSON, YAML, etc.)
|
|
||||||
else {
|
|
||||||
fileMetadata.push({
|
|
||||||
file: relativePath,
|
|
||||||
type: fileExt.slice(1), // Remove the dot
|
|
||||||
name: path.basename(filePath, fileExt),
|
|
||||||
title: null,
|
|
||||||
hash: hash,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract XML node attributes from MD file content
|
|
||||||
* @param {string} content - File content
|
|
||||||
* @param {string} filePath - File path for context
|
|
||||||
* @param {string} relativePath - Relative path starting with 'bmad/'
|
|
||||||
* @returns {Object|null} Extracted metadata or null
|
|
||||||
*/
|
|
||||||
extractXmlNodeAttributes(content, filePath, relativePath) {
|
|
||||||
// Look for XML blocks in code fences
|
|
||||||
const xmlBlockMatch = content.match(/```xml\s*([\s\S]*?)```/);
|
|
||||||
if (!xmlBlockMatch) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const xmlContent = xmlBlockMatch[1];
|
|
||||||
|
|
||||||
// Extract root XML node (agent, task, template, etc.)
|
|
||||||
const rootNodeMatch = xmlContent.match(/<(\w+)([^>]*)>/);
|
|
||||||
if (!rootNodeMatch) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeType = rootNodeMatch[1];
|
|
||||||
const attributes = rootNodeMatch[2];
|
|
||||||
|
|
||||||
// Extract name and title attributes (id not needed since we have path)
|
|
||||||
const nameMatch = attributes.match(/name="([^"]*)"/);
|
|
||||||
const titleMatch = attributes.match(/title="([^"]*)"/);
|
|
||||||
|
|
||||||
return {
|
|
||||||
file: relativePath,
|
|
||||||
type: nodeType,
|
|
||||||
name: nameMatch ? nameMatch[1] : null,
|
|
||||||
title: titleMatch ? titleMatch[1] : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate CSV manifest content
|
|
||||||
* @param {Object} data - Manifest data
|
|
||||||
* @param {Array} fileMetadata - File metadata array
|
|
||||||
* @param {Object} moduleConfigs - Module configuration data
|
|
||||||
* @returns {string} CSV content
|
|
||||||
*/
|
|
||||||
generateManifestCsv(data, fileMetadata, moduleConfigs = {}) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
let csv = [];
|
|
||||||
|
|
||||||
// Header section
|
|
||||||
csv.push(
|
|
||||||
'# BMAD Manifest',
|
|
||||||
`# Generated: ${timestamp}`,
|
|
||||||
'',
|
|
||||||
'## Installation Info',
|
|
||||||
'Property,Value',
|
|
||||||
`Version,${data.version}`,
|
|
||||||
`InstallDate,${data.installDate || timestamp}`,
|
|
||||||
`LastUpdated,${data.lastUpdated || timestamp}`,
|
|
||||||
);
|
|
||||||
if (data.language) {
|
|
||||||
csv.push(`Language,${data.language}`);
|
|
||||||
}
|
|
||||||
csv.push('');
|
|
||||||
|
|
||||||
// Modules section
|
|
||||||
if (data.modules && data.modules.length > 0) {
|
|
||||||
csv.push('## Modules', 'Name,Version,ShortTitle');
|
|
||||||
for (const moduleName of data.modules) {
|
|
||||||
const config = moduleConfigs[moduleName] || {};
|
|
||||||
csv.push([moduleName, config.version || '', config['short-title'] || ''].map((v) => this.escapeCsv(v)).join(','));
|
|
||||||
}
|
|
||||||
csv.push('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// IDEs section
|
|
||||||
if (data.ides && data.ides.length > 0) {
|
|
||||||
csv.push('## IDEs', 'IDE');
|
|
||||||
for (const ide of data.ides) {
|
|
||||||
csv.push(this.escapeCsv(ide));
|
|
||||||
}
|
|
||||||
csv.push('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Files section - NO LONGER USED
|
|
||||||
// Files are now tracked in files-manifest.csv by ManifestGenerator
|
|
||||||
|
|
||||||
return csv.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse CSV manifest content back to object
|
|
||||||
* @param {string} csvContent - CSV content to parse
|
|
||||||
* @returns {Object} Parsed manifest data
|
|
||||||
*/
|
|
||||||
parseManifestCsv(csvContent) {
|
|
||||||
const result = {
|
|
||||||
modules: [],
|
|
||||||
ides: [],
|
|
||||||
files: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const lines = csvContent.split('\n');
|
|
||||||
let section = '';
|
|
||||||
|
|
||||||
for (const line_ of lines) {
|
|
||||||
const line = line_.trim();
|
|
||||||
|
|
||||||
// Skip empty lines and comments
|
|
||||||
if (!line || line.startsWith('#')) {
|
|
||||||
// Check for section headers
|
|
||||||
if (line.startsWith('## ')) {
|
|
||||||
section = line.slice(3).toLowerCase();
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse based on current section
|
|
||||||
switch (section) {
|
|
||||||
case 'installation info': {
|
|
||||||
// Skip header row
|
|
||||||
if (line === 'Property,Value') continue;
|
|
||||||
|
|
||||||
const [property, ...valueParts] = line.split(',');
|
|
||||||
const value = this.unescapeCsv(valueParts.join(','));
|
|
||||||
|
|
||||||
switch (property) {
|
|
||||||
// Path no longer stored in manifest
|
|
||||||
case 'Version': {
|
|
||||||
result.version = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'InstallDate': {
|
|
||||||
result.installDate = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'LastUpdated': {
|
|
||||||
result.lastUpdated = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'Language': {
|
|
||||||
result.language = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'modules': {
|
|
||||||
// Skip header row
|
|
||||||
if (line === 'Name,Version,ShortTitle') continue;
|
|
||||||
|
|
||||||
const parts = this.parseCsvLine(line);
|
|
||||||
if (parts[0]) {
|
|
||||||
result.modules.push(parts[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ides': {
|
|
||||||
// Skip header row
|
|
||||||
if (line === 'IDE') continue;
|
|
||||||
|
|
||||||
result.ides.push(this.unescapeCsv(line));
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'files': {
|
|
||||||
// Skip header rows (support both old and new format)
|
|
||||||
if (line === 'Type,Path,Name,Title' || line === 'Type,Path,Name,Title,Hash') continue;
|
|
||||||
|
|
||||||
const parts = this.parseCsvLine(line);
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
result.files.push({
|
|
||||||
type: parts[0] || '',
|
|
||||||
file: parts[1] || '',
|
|
||||||
name: parts[2] || null,
|
|
||||||
title: parts[3] || null,
|
|
||||||
hash: parts[4] || null, // Hash column (may not exist in old manifests)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// No default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a CSV line handling quotes and commas
|
|
||||||
* @param {string} line - CSV line to parse
|
|
||||||
* @returns {Array} Array of values
|
|
||||||
*/
|
|
||||||
parseCsvLine(line) {
|
|
||||||
const result = [];
|
|
||||||
let current = '';
|
|
||||||
let inQuotes = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < line.length; i++) {
|
|
||||||
const char = line[i];
|
|
||||||
|
|
||||||
if (char === '"') {
|
|
||||||
if (inQuotes && line[i + 1] === '"') {
|
|
||||||
// Escaped quote
|
|
||||||
current += '"';
|
|
||||||
i++;
|
|
||||||
} else {
|
|
||||||
// Toggle quote state
|
|
||||||
inQuotes = !inQuotes;
|
|
||||||
}
|
|
||||||
} else if (char === ',' && !inQuotes) {
|
|
||||||
// Field separator
|
|
||||||
result.push(this.unescapeCsv(current));
|
|
||||||
current = '';
|
|
||||||
} else {
|
|
||||||
current += char;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the last field
|
|
||||||
result.push(this.unescapeCsv(current));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escape CSV special characters
|
|
||||||
* @param {string} text - Text to escape
|
|
||||||
* @returns {string} Escaped text
|
|
||||||
*/
|
|
||||||
escapeCsv(text) {
|
|
||||||
if (!text) return '';
|
|
||||||
const str = String(text);
|
|
||||||
|
|
||||||
// If contains comma, newline, or quote, wrap in quotes and escape quotes
|
|
||||||
if (str.includes(',') || str.includes('\n') || str.includes('"')) {
|
|
||||||
return '"' + str.replaceAll('"', '""') + '"';
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unescape CSV field
|
|
||||||
* @param {string} text - Text to unescape
|
|
||||||
* @returns {string} Unescaped text
|
|
||||||
*/
|
|
||||||
unescapeCsv(text) {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// Remove surrounding quotes if present
|
|
||||||
if (text.startsWith('"') && text.endsWith('"')) {
|
|
||||||
text = text.slice(1, -1);
|
|
||||||
// Unescape doubled quotes
|
|
||||||
text = text.replaceAll('""', '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load module configuration files
|
|
||||||
* @param {Array} modules - List of module names
|
|
||||||
* @returns {Object} Module configurations indexed by name
|
|
||||||
*/
|
|
||||||
async loadModuleConfigs(modules) {
|
|
||||||
const configs = {};
|
|
||||||
|
|
||||||
for (const moduleName of modules) {
|
|
||||||
// Handle core module differently - it's in src/core-skills not src/modules/core
|
|
||||||
const configPath =
|
|
||||||
moduleName === 'core'
|
|
||||||
? path.join(process.cwd(), 'src', 'core-skills', 'config.yaml')
|
|
||||||
: path.join(process.cwd(), 'src', 'modules', moduleName, 'config.yaml');
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (await fs.pathExists(configPath)) {
|
|
||||||
const yaml = require('yaml');
|
|
||||||
const content = await fs.readFile(configPath, 'utf8');
|
|
||||||
configs[moduleName] = yaml.parse(content);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
await prompts.log.warn(`Could not load config for module ${moduleName}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return configs;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Get module version info from source
|
* Get module version info from source
|
||||||
* @param {string} moduleName - Module name/code
|
* @param {string} moduleName - Module name/code
|
||||||
|
|
@ -986,47 +450,6 @@ class Manifest {
|
||||||
|
|
||||||
return updates;
|
return updates;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare two semantic versions
|
|
||||||
* @param {string} v1 - First version
|
|
||||||
* @param {string} v2 - Second version
|
|
||||||
* @returns {number} -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
|
|
||||||
*/
|
|
||||||
compareVersions(v1, v2) {
|
|
||||||
if (!v1 || !v2) return 0;
|
|
||||||
|
|
||||||
const normalize = (v) => {
|
|
||||||
// Remove leading 'v' if present
|
|
||||||
v = v.replace(/^v/, '');
|
|
||||||
// Handle prerelease tags
|
|
||||||
const parts = v.split('-');
|
|
||||||
const main = parts[0].split('.');
|
|
||||||
const prerelease = parts[1];
|
|
||||||
return { main, prerelease };
|
|
||||||
};
|
|
||||||
|
|
||||||
const n1 = normalize(v1);
|
|
||||||
const n2 = normalize(v2);
|
|
||||||
|
|
||||||
// Compare main version parts
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const num1 = parseInt(n1.main[i] || '0', 10);
|
|
||||||
const num2 = parseInt(n2.main[i] || '0', 10);
|
|
||||||
if (num1 !== num2) {
|
|
||||||
return num1 < num2 ? -1 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If main versions are equal, compare prerelease
|
|
||||||
if (n1.prerelease && n2.prerelease) {
|
|
||||||
return n1.prerelease < n2.prerelease ? -1 : n1.prerelease > n2.prerelease ? 1 : 0;
|
|
||||||
}
|
|
||||||
if (n1.prerelease) return -1; // Prerelease is older than stable
|
|
||||||
if (n2.prerelease) return 1; // Stable is newer than prerelease
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { Manifest };
|
module.exports = { Manifest };
|
||||||
|
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates launcher command files for each agent
|
|
||||||
*/
|
|
||||||
class AgentCommandGenerator {
|
|
||||||
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
|
||||||
this.templatePath = path.join(__dirname, '../templates/agent-command-template.md');
|
|
||||||
this.bmadFolderName = bmadFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect agent artifacts for IDE installation
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Array} selectedModules - Modules to include
|
|
||||||
* @returns {Object} Artifacts array with metadata
|
|
||||||
*/
|
|
||||||
async collectAgentArtifacts(bmadDir, selectedModules = []) {
|
|
||||||
const { getAgentsFromBmad } = require('./bmad-artifacts');
|
|
||||||
|
|
||||||
// Get agents from INSTALLED bmad/ directory
|
|
||||||
const agents = await getAgentsFromBmad(bmadDir, selectedModules);
|
|
||||||
|
|
||||||
const artifacts = [];
|
|
||||||
|
|
||||||
for (const agent of agents) {
|
|
||||||
const launcherContent = await this.generateLauncherContent(agent);
|
|
||||||
// Use relativePath if available (for nested agents), otherwise just name with .md
|
|
||||||
const agentPathInModule = agent.relativePath || `${agent.name}.md`;
|
|
||||||
// Calculate the relative agent path (e.g., bmm/agents/pm.md)
|
|
||||||
let agentRelPath = agent.path || '';
|
|
||||||
// Normalize path separators for cross-platform compatibility
|
|
||||||
agentRelPath = agentRelPath.replaceAll('\\', '/');
|
|
||||||
// Remove _bmad/ prefix if present to get relative path from project root
|
|
||||||
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
|
|
||||||
if (agentRelPath.includes('_bmad/')) {
|
|
||||||
const parts = agentRelPath.split(/_bmad\//);
|
|
||||||
if (parts.length > 1) {
|
|
||||||
agentRelPath = parts.slice(1).join('/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
artifacts.push({
|
|
||||||
type: 'agent-launcher',
|
|
||||||
name: agent.name,
|
|
||||||
description: agent.description || `${agent.name} agent`,
|
|
||||||
module: agent.module,
|
|
||||||
canonicalId: agent.canonicalId || '',
|
|
||||||
relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename
|
|
||||||
agentPath: agentRelPath, // Relative path to actual agent file
|
|
||||||
content: launcherContent,
|
|
||||||
sourcePath: agent.path,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
artifacts,
|
|
||||||
counts: {
|
|
||||||
agents: agents.length,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate launcher content for an agent
|
|
||||||
* @param {Object} agent - Agent metadata
|
|
||||||
* @returns {string} Launcher file content
|
|
||||||
*/
|
|
||||||
async generateLauncherContent(agent) {
|
|
||||||
// Load the template
|
|
||||||
const template = await fs.readFile(this.templatePath, 'utf8');
|
|
||||||
|
|
||||||
// Replace template variables
|
|
||||||
// Use relativePath if available (for nested agents), otherwise just name with .md
|
|
||||||
const agentPathInModule = agent.relativePath || `${agent.name}.md`;
|
|
||||||
return template
|
|
||||||
.replaceAll('{{name}}', agent.name)
|
|
||||||
.replaceAll('{{module}}', agent.module)
|
|
||||||
.replaceAll('{{path}}', agentPathInModule)
|
|
||||||
.replaceAll('{{description}}', agent.description || `${agent.name} agent`)
|
|
||||||
.replaceAll('_bmad', this.bmadFolderName)
|
|
||||||
.replaceAll('_bmad', '_bmad');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write agent launcher artifacts to IDE commands directory
|
|
||||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
|
||||||
* @param {Array} artifacts - Agent launcher artifacts
|
|
||||||
* @returns {number} Count of launchers written
|
|
||||||
*/
|
|
||||||
async writeAgentLaunchers(baseCommandsDir, artifacts) {
|
|
||||||
let writtenCount = 0;
|
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
|
||||||
if (artifact.type === 'agent-launcher') {
|
|
||||||
const moduleAgentsDir = path.join(baseCommandsDir, artifact.module, 'agents');
|
|
||||||
await fs.ensureDir(moduleAgentsDir);
|
|
||||||
|
|
||||||
const launcherPath = path.join(moduleAgentsDir, `${artifact.name}.md`);
|
|
||||||
await fs.writeFile(launcherPath, artifact.content);
|
|
||||||
writtenCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return writtenCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write agent launcher artifacts using underscore format (Windows-compatible)
|
|
||||||
* Creates flat files like: bmad_bmm_pm.md
|
|
||||||
*
|
|
||||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
|
||||||
* @param {Array} artifacts - Agent launcher artifacts
|
|
||||||
* @returns {number} Count of launchers written
|
|
||||||
*/
|
|
||||||
async writeColonArtifacts(baseCommandsDir, artifacts) {
|
|
||||||
let writtenCount = 0;
|
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
|
||||||
if (artifact.type === 'agent-launcher') {
|
|
||||||
// Convert relativePath to underscore format: bmm/agents/pm.md → bmad_bmm_pm.md
|
|
||||||
const flatName = toColonPath(artifact.relativePath);
|
|
||||||
const launcherPath = path.join(baseCommandsDir, flatName);
|
|
||||||
await fs.ensureDir(path.dirname(launcherPath));
|
|
||||||
await fs.writeFile(launcherPath, artifact.content);
|
|
||||||
writtenCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return writtenCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write agent launcher artifacts using dash format (NEW STANDARD)
|
|
||||||
* Creates flat files like: bmad-agent-bmm-pm.md
|
|
||||||
*
|
|
||||||
* The bmad-agent- prefix distinguishes agents from workflows/tasks/tools.
|
|
||||||
*
|
|
||||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
|
||||||
* @param {Array} artifacts - Agent launcher artifacts
|
|
||||||
* @returns {number} Count of launchers written
|
|
||||||
*/
|
|
||||||
async writeDashArtifacts(baseCommandsDir, artifacts) {
|
|
||||||
let writtenCount = 0;
|
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
|
||||||
if (artifact.type === 'agent-launcher') {
|
|
||||||
// Convert relativePath to dash format: bmm/agents/pm.md → bmad-agent-bmm-pm.md
|
|
||||||
const flatName = toDashPath(artifact.relativePath);
|
|
||||||
const launcherPath = path.join(baseCommandsDir, flatName);
|
|
||||||
await fs.ensureDir(path.dirname(launcherPath));
|
|
||||||
await fs.writeFile(launcherPath, artifact.content);
|
|
||||||
writtenCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return writtenCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the custom agent name in underscore format (Windows-compatible)
|
|
||||||
* @param {string} agentName - Custom agent name
|
|
||||||
* @returns {string} Underscore-formatted filename
|
|
||||||
*/
|
|
||||||
getCustomAgentColonName(agentName) {
|
|
||||||
return customAgentColonName(agentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the custom agent name in underscore format (Windows-compatible)
|
|
||||||
* @param {string} agentName - Custom agent name
|
|
||||||
* @returns {string} Underscore-formatted filename
|
|
||||||
*/
|
|
||||||
getCustomAgentDashName(agentName) {
|
|
||||||
return customAgentDashName(agentName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { AgentCommandGenerator };
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const { loadSkillManifest, getCanonicalId } = require('./skill-manifest');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helpers for gathering BMAD agents/tasks from the installed tree.
|
|
||||||
* Shared by installers that need Claude-style exports.
|
|
||||||
*
|
|
||||||
* TODO: Dead code cleanup — compiled XML agents are retired.
|
|
||||||
*
|
|
||||||
* All agents now use the SKILL.md directory format with bmad-skill-manifest.yaml
|
|
||||||
* (type: agent). The legacy pipeline below only discovers compiled .md files
|
|
||||||
* containing <agent> XML tags, which no longer exist. The following are dead:
|
|
||||||
*
|
|
||||||
* - getAgentsFromBmad() — scans {module}/agents/ for .md files with <agent> tags
|
|
||||||
* - getAgentsFromDir() — recursive helper for the above
|
|
||||||
* - AgentCommandGenerator — (agent-command-generator.js) generates launcher .md files
|
|
||||||
* that tell the LLM to load a compiled agent .md file
|
|
||||||
* - agent-command-template.md — (templates/) the launcher template with hardcoded
|
|
||||||
* {module}/agents/{{path}} reference
|
|
||||||
*
|
|
||||||
* Agent metadata for agent-manifest.csv is now handled entirely by
|
|
||||||
* ManifestGenerator.getAgentsFromDirRecursive() in manifest-generator.js,
|
|
||||||
* which walks the full module tree and finds type:agent directories.
|
|
||||||
*
|
|
||||||
* IDE installation of agents is handled by the native skill pipeline —
|
|
||||||
* each agent's SKILL.md directory is installed directly to the IDE's
|
|
||||||
* skills path, so no launcher intermediary is needed.
|
|
||||||
*
|
|
||||||
* Cleanup: remove getAgentsFromBmad, getAgentsFromDir, their exports,
|
|
||||||
* AgentCommandGenerator, agent-command-template.md, and all call sites
|
|
||||||
* in IDE installers that invoke collectAgentArtifacts / writeAgentLaunchers /
|
|
||||||
* writeColonArtifacts / writeDashArtifacts.
|
|
||||||
* getTasksFromBmad and getTasksFromDir may still be live — verify before removing.
|
|
||||||
*/
|
|
||||||
async function getAgentsFromBmad(bmadDir, selectedModules = []) {
|
|
||||||
const agents = [];
|
|
||||||
|
|
||||||
// Get core agents
|
|
||||||
if (await fs.pathExists(path.join(bmadDir, 'core', 'agents'))) {
|
|
||||||
const coreAgents = await getAgentsFromDir(path.join(bmadDir, 'core', 'agents'), 'core');
|
|
||||||
agents.push(...coreAgents);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get module agents
|
|
||||||
for (const moduleName of selectedModules) {
|
|
||||||
const agentsPath = path.join(bmadDir, moduleName, 'agents');
|
|
||||||
|
|
||||||
if (await fs.pathExists(agentsPath)) {
|
|
||||||
const moduleAgents = await getAgentsFromDir(agentsPath, moduleName);
|
|
||||||
agents.push(...moduleAgents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get standalone agents from bmad/agents/ directory
|
|
||||||
const standaloneAgentsDir = path.join(bmadDir, 'agents');
|
|
||||||
if (await fs.pathExists(standaloneAgentsDir)) {
|
|
||||||
const agentDirs = await fs.readdir(standaloneAgentsDir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const agentDir of agentDirs) {
|
|
||||||
if (!agentDir.isDirectory()) continue;
|
|
||||||
|
|
||||||
const agentDirPath = path.join(standaloneAgentsDir, agentDir.name);
|
|
||||||
const agentFiles = await fs.readdir(agentDirPath);
|
|
||||||
const skillManifest = await loadSkillManifest(agentDirPath);
|
|
||||||
|
|
||||||
for (const file of agentFiles) {
|
|
||||||
if (!file.endsWith('.md')) continue;
|
|
||||||
if (file.includes('.customize.')) continue;
|
|
||||||
|
|
||||||
const filePath = path.join(agentDirPath, file);
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
|
|
||||||
if (content.includes('localskip="true"')) continue;
|
|
||||||
|
|
||||||
agents.push({
|
|
||||||
path: filePath,
|
|
||||||
name: file.replace('.md', ''),
|
|
||||||
module: 'standalone', // Mark as standalone agent
|
|
||||||
canonicalId: getCanonicalId(skillManifest, file),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return agents;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getTasksFromBmad(bmadDir, selectedModules = []) {
|
|
||||||
const tasks = [];
|
|
||||||
|
|
||||||
if (await fs.pathExists(path.join(bmadDir, 'core', 'tasks'))) {
|
|
||||||
const coreTasks = await getTasksFromDir(path.join(bmadDir, 'core', 'tasks'), 'core');
|
|
||||||
tasks.push(...coreTasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const moduleName of selectedModules) {
|
|
||||||
const tasksPath = path.join(bmadDir, moduleName, 'tasks');
|
|
||||||
|
|
||||||
if (await fs.pathExists(tasksPath)) {
|
|
||||||
const moduleTasks = await getTasksFromDir(tasksPath, moduleName);
|
|
||||||
tasks.push(...moduleTasks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAgentsFromDir(dirPath, moduleName, relativePath = '') {
|
|
||||||
const agents = [];
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(dirPath))) {
|
|
||||||
return agents;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
||||||
const skillManifest = await loadSkillManifest(dirPath);
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
// Skip if entry.name is undefined or not a string
|
|
||||||
if (!entry.name || typeof entry.name !== 'string') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullPath = path.join(dirPath, entry.name);
|
|
||||||
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
// Recurse into subdirectories
|
|
||||||
const subDirAgents = await getAgentsFromDir(fullPath, moduleName, newRelativePath);
|
|
||||||
agents.push(...subDirAgents);
|
|
||||||
} else if (entry.name.endsWith('.md')) {
|
|
||||||
// Skip README files and other non-agent files
|
|
||||||
if (entry.name.toLowerCase() === 'readme.md' || entry.name.toLowerCase().startsWith('readme-')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.name.includes('.customize.')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await fs.readFile(fullPath, 'utf8');
|
|
||||||
|
|
||||||
if (content.includes('localskip="true"')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only include files that have agent-specific content (compiled agents have <agent> tag)
|
|
||||||
if (!content.includes('<agent')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
agents.push({
|
|
||||||
path: fullPath,
|
|
||||||
name: entry.name.replace('.md', ''),
|
|
||||||
module: moduleName,
|
|
||||||
relativePath: newRelativePath, // Keep the .md extension for the full path
|
|
||||||
canonicalId: getCanonicalId(skillManifest, entry.name),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return agents;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getTasksFromDir(dirPath, moduleName) {
|
|
||||||
const tasks = [];
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(dirPath))) {
|
|
||||||
return tasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = await fs.readdir(dirPath);
|
|
||||||
const skillManifest = await loadSkillManifest(dirPath);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
// Include both .md and .xml task files
|
|
||||||
if (!file.endsWith('.md') && !file.endsWith('.xml')) {
|
|
||||||
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({
|
|
||||||
path: filePath,
|
|
||||||
name: file.replace(ext, ''),
|
|
||||||
module: moduleName,
|
|
||||||
canonicalId: getCanonicalId(skillManifest, file),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return tasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getAgentsFromBmad,
|
|
||||||
getTasksFromBmad,
|
|
||||||
getAgentsFromDir,
|
|
||||||
getTasksFromDir,
|
|
||||||
};
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const yaml = require('yaml');
|
|
||||||
const { glob } = require('glob');
|
|
||||||
const { getSourcePath } = require('../../project-root');
|
|
||||||
|
|
||||||
async function loadModuleInjectionConfig(handler, moduleName) {
|
|
||||||
const sourceModulesPath = getSourcePath('modules');
|
|
||||||
const handlerBaseDir = path.join(sourceModulesPath, moduleName, 'sub-modules', handler);
|
|
||||||
const configPath = path.join(handlerBaseDir, 'injections.yaml');
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(configPath))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configContent = await fs.readFile(configPath, 'utf8');
|
|
||||||
const config = yaml.parse(configContent) || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
config,
|
|
||||||
handlerBaseDir,
|
|
||||||
configPath,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldApplyInjection(injection, subagentChoices) {
|
|
||||||
if (!subagentChoices || subagentChoices.install === 'none') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subagentChoices.install === 'all') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subagentChoices.install === 'selective') {
|
|
||||||
const selected = subagentChoices.selected || [];
|
|
||||||
|
|
||||||
if (injection.requires === 'any' && selected.length > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (injection.requires) {
|
|
||||||
const required = `${injection.requires}.md`;
|
|
||||||
return selected.includes(required);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (injection.point) {
|
|
||||||
const selectedNames = selected.map((file) => file.replace('.md', ''));
|
|
||||||
return selectedNames.some((name) => injection.point.includes(name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterAgentInstructions(content, selectedFiles) {
|
|
||||||
if (!selectedFiles || selectedFiles.length === 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedAgents = selectedFiles.map((file) => file.replace('.md', ''));
|
|
||||||
const lines = content.split('\n');
|
|
||||||
const filteredLines = [];
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.includes('<llm') || line.includes('</llm>')) {
|
|
||||||
filteredLines.push(line);
|
|
||||||
} else if (line.includes('subagent')) {
|
|
||||||
let shouldInclude = false;
|
|
||||||
for (const agent of selectedAgents) {
|
|
||||||
if (line.includes(agent)) {
|
|
||||||
shouldInclude = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldInclude) {
|
|
||||||
filteredLines.push(line);
|
|
||||||
}
|
|
||||||
} else if (line.includes('When creating PRDs') || line.includes('ACTIVELY delegate')) {
|
|
||||||
filteredLines.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filteredLines.length > 2) {
|
|
||||||
return filteredLines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveSubagentFiles(handlerBaseDir, subagentConfig, subagentChoices) {
|
|
||||||
if (!subagentConfig || !subagentConfig.files) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subagentChoices || subagentChoices.install === 'none') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let filesToCopy = subagentConfig.files;
|
|
||||||
|
|
||||||
if (subagentChoices.install === 'selective') {
|
|
||||||
filesToCopy = subagentChoices.selected || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceDir = path.join(handlerBaseDir, subagentConfig.source || '');
|
|
||||||
const resolved = [];
|
|
||||||
|
|
||||||
for (const file of filesToCopy) {
|
|
||||||
// Use forward slashes for glob pattern (works on both Windows and Unix)
|
|
||||||
// Convert backslashes to forward slashes for glob compatibility
|
|
||||||
const normalizedSourceDir = sourceDir.replaceAll('\\', '/');
|
|
||||||
const pattern = `${normalizedSourceDir}/**/${file}`;
|
|
||||||
const matches = await glob(pattern);
|
|
||||||
|
|
||||||
if (matches.length > 0) {
|
|
||||||
const absolutePath = matches[0];
|
|
||||||
resolved.push({
|
|
||||||
file,
|
|
||||||
absolutePath,
|
|
||||||
relativePath: path.relative(sourceDir, absolutePath),
|
|
||||||
sourceDir,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
loadModuleInjectionConfig,
|
|
||||||
shouldApplyInjection,
|
|
||||||
filterAgentInstructions,
|
|
||||||
resolveSubagentFiles,
|
|
||||||
};
|
|
||||||
|
|
@ -15,8 +15,6 @@
|
||||||
* - standalone/agents/fred.md → bmad-agent-standalone-fred.md
|
* - standalone/agents/fred.md → bmad-agent-standalone-fred.md
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Type segments - agents are included in naming, others are filtered out
|
|
||||||
const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
|
|
||||||
const AGENT_SEGMENT = 'agents';
|
const AGENT_SEGMENT = 'agents';
|
||||||
|
|
||||||
// BMAD installation folder name - centralized constant for all installers
|
// BMAD installation folder name - centralized constant for all installers
|
||||||
|
|
@ -194,125 +192,6 @@ function parseDashName(filename) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// LEGACY FUNCTIONS (underscore format) - kept for backward compatibility
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert hierarchical path to flat underscore-separated name (LEGACY)
|
|
||||||
* @deprecated Use toDashName instead
|
|
||||||
*/
|
|
||||||
function toUnderscoreName(module, type, name) {
|
|
||||||
const isAgent = type === AGENT_SEGMENT;
|
|
||||||
if (module === 'core') {
|
|
||||||
return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`;
|
|
||||||
}
|
|
||||||
if (module === 'standalone') {
|
|
||||||
return isAgent ? `bmad_agent_standalone_${name}.md` : `bmad_standalone_${name}.md`;
|
|
||||||
}
|
|
||||||
return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert relative path to flat underscore-separated name (LEGACY)
|
|
||||||
* @deprecated Use toDashPath instead
|
|
||||||
*/
|
|
||||||
function toUnderscorePath(relativePath) {
|
|
||||||
// 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 module = parts[0];
|
|
||||||
const type = parts[1];
|
|
||||||
const name = parts.slice(2).join('_');
|
|
||||||
|
|
||||||
return toUnderscoreName(module, type, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create custom agent underscore name (LEGACY)
|
|
||||||
* @deprecated Use customAgentDashName instead
|
|
||||||
*/
|
|
||||||
function customAgentUnderscoreName(agentName) {
|
|
||||||
return `bmad_custom_${agentName}.md`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a filename uses underscore format (LEGACY)
|
|
||||||
* @deprecated Use isDashFormat instead
|
|
||||||
*/
|
|
||||||
function isUnderscoreFormat(filename) {
|
|
||||||
return filename.startsWith('bmad_') && filename.includes('_');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract parts from an underscore-formatted filename (LEGACY)
|
|
||||||
* @deprecated Use parseDashName instead
|
|
||||||
*/
|
|
||||||
function parseUnderscoreName(filename) {
|
|
||||||
const withoutExt = filename.replace('.md', '');
|
|
||||||
const parts = withoutExt.split('_');
|
|
||||||
|
|
||||||
if (parts.length < 2 || parts[0] !== 'bmad') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentIndex = parts.indexOf('agent');
|
|
||||||
|
|
||||||
if (agentIndex !== -1) {
|
|
||||||
if (agentIndex === 1) {
|
|
||||||
// bmad_agent_... - check for standalone
|
|
||||||
if (parts.length >= 4 && parts[2] === 'standalone') {
|
|
||||||
return {
|
|
||||||
prefix: parts[0],
|
|
||||||
module: 'standalone',
|
|
||||||
type: 'agents',
|
|
||||||
name: parts.slice(3).join('_'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
prefix: parts[0],
|
|
||||||
module: 'core',
|
|
||||||
type: 'agents',
|
|
||||||
name: parts.slice(agentIndex + 1).join('_'),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
prefix: parts[0],
|
|
||||||
module: parts[1],
|
|
||||||
type: 'agents',
|
|
||||||
name: parts.slice(agentIndex + 1).join('_'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.length === 2) {
|
|
||||||
return {
|
|
||||||
prefix: parts[0],
|
|
||||||
module: 'core',
|
|
||||||
type: 'workflows',
|
|
||||||
name: parts[1],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for standalone non-agent: bmad_standalone_name
|
|
||||||
if (parts[1] === 'standalone') {
|
|
||||||
return {
|
|
||||||
prefix: parts[0],
|
|
||||||
module: 'standalone',
|
|
||||||
type: 'workflows',
|
|
||||||
name: parts.slice(2).join('_'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
prefix: parts[0],
|
|
||||||
module: parts[1],
|
|
||||||
type: 'workflows',
|
|
||||||
name: parts.slice(2).join('_'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the skill name for an artifact.
|
* Resolve the skill name for an artifact.
|
||||||
* Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available,
|
* Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available,
|
||||||
|
|
@ -328,37 +207,13 @@ function resolveSkillName(artifact) {
|
||||||
return toDashPath(artifact.relativePath);
|
return toDashPath(artifact.relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward compatibility aliases (colon format was same as underscore)
|
|
||||||
const toColonName = toUnderscoreName;
|
|
||||||
const toColonPath = toUnderscorePath;
|
|
||||||
const customAgentColonName = customAgentUnderscoreName;
|
|
||||||
const isColonFormat = isUnderscoreFormat;
|
|
||||||
const parseColonName = parseUnderscoreName;
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// New standard (dash-based)
|
|
||||||
toDashName,
|
toDashName,
|
||||||
toDashPath,
|
toDashPath,
|
||||||
resolveSkillName,
|
resolveSkillName,
|
||||||
customAgentDashName,
|
customAgentDashName,
|
||||||
isDashFormat,
|
isDashFormat,
|
||||||
parseDashName,
|
parseDashName,
|
||||||
|
|
||||||
// Legacy (underscore-based) - kept for backward compatibility
|
|
||||||
toUnderscoreName,
|
|
||||||
toUnderscorePath,
|
|
||||||
customAgentUnderscoreName,
|
|
||||||
isUnderscoreFormat,
|
|
||||||
parseUnderscoreName,
|
|
||||||
|
|
||||||
// Backward compatibility aliases
|
|
||||||
toColonName,
|
|
||||||
toColonPath,
|
|
||||||
customAgentColonName,
|
|
||||||
isColonFormat,
|
|
||||||
parseColonName,
|
|
||||||
|
|
||||||
TYPE_SEGMENTS,
|
|
||||||
AGENT_SEGMENT,
|
AGENT_SEGMENT,
|
||||||
BMAD_FOLDER_NAME,
|
BMAD_FOLDER_NAME,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
---
|
|
||||||
name: '{{name}}'
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from {project-root}/_bmad/{{module}}/agents/{{path}}
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. Execute ALL activation steps exactly as written in the agent file
|
|
||||||
4. Follow the agent's persona and menu system precisely
|
|
||||||
5. Stay in character throughout the session
|
|
||||||
</agent-activation>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
---
|
|
||||||
name: '{{name}}'
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
|
|
||||||
|
|
||||||
Follow all instructions in the workflow file exactly as written.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
default-agent.md
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
default-workflow.md
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
---
|
|
||||||
name: '{{name}}'
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from {project-root}/_bmad/{{path}}
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. FOLLOW every step in the <activation> section precisely
|
|
||||||
4. DISPLAY the welcome/greeting as instructed
|
|
||||||
5. PRESENT the numbered menu
|
|
||||||
6. WAIT for user input before proceeding
|
|
||||||
</agent-activation>
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
---
|
|
||||||
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.
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
---
|
|
||||||
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.
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
name: '{{name}}'
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL {project-root}/{{bmadFolderName}}/{{path}}, READ its entire contents and follow its directions exactly!
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
description = "Activates the {{name}} agent from the BMad Method."
|
|
||||||
prompt = """
|
|
||||||
CRITICAL: You are now the BMad '{{name}}' agent.
|
|
||||||
|
|
||||||
PRE-FLIGHT CHECKLIST:
|
|
||||||
1. [ ] IMMEDIATE ACTION: Load and parse {project-root}/{{bmadFolderName}}/{{module}}/config.yaml - store ALL config values in memory for use throughout the session.
|
|
||||||
2. [ ] IMMEDIATE ACTION: Read and internalize the full agent definition at {project-root}/{{bmadFolderName}}/{{path}}.
|
|
||||||
3. [ ] CONFIRM: The user's name from config is {user_name}.
|
|
||||||
|
|
||||||
Only after all checks are complete, greet the user by name and display the menu.
|
|
||||||
Acknowledge this checklist is complete in your first response.
|
|
||||||
|
|
||||||
AGENT DEFINITION: {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
"""
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
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}}
|
|
||||||
"""
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
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}}
|
|
||||||
"""
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
description = '{{description}}'
|
|
||||||
prompt = """
|
|
||||||
Execute the BMAD '{{name}}' workflow.
|
|
||||||
|
|
||||||
CRITICAL: This is a structured YAML workflow. Follow these steps precisely:
|
|
||||||
|
|
||||||
1. LOAD the workflow definition from {project-root}/{{bmadFolderName}}/{{workflow_path}}
|
|
||||||
2. PARSE the YAML structure to understand:
|
|
||||||
- Workflow phases and steps
|
|
||||||
- Required inputs and outputs
|
|
||||||
- Dependencies between steps
|
|
||||||
3. EXECUTE each step in order
|
|
||||||
4. VALIDATE outputs before proceeding to next step
|
|
||||||
|
|
||||||
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{workflow_path}}
|
|
||||||
"""
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
description = '{{description}}'
|
|
||||||
prompt = """
|
|
||||||
Execute the BMAD '{{name}}' workflow.
|
|
||||||
|
|
||||||
CRITICAL: You must load and follow the workflow definition exactly.
|
|
||||||
|
|
||||||
WORKFLOW INSTRUCTIONS:
|
|
||||||
1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{workflow_path}}
|
|
||||||
2. READ its entire contents
|
|
||||||
3. FOLLOW every step precisely as specified
|
|
||||||
4. DO NOT skip or modify any steps
|
|
||||||
|
|
||||||
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{workflow_path}}
|
|
||||||
"""
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
---
|
|
||||||
inclusion: manual
|
|
||||||
---
|
|
||||||
|
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from #[[file:{{bmadFolderName}}/{{path}}]]
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. FOLLOW every step in the <activation> section precisely
|
|
||||||
4. DISPLAY the welcome/greeting as instructed
|
|
||||||
5. PRESENT the numbered menu
|
|
||||||
6. WAIT for user input before proceeding
|
|
||||||
</agent-activation>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
inclusion: manual
|
|
||||||
---
|
|
||||||
|
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
Read the entire task file at: #[[file:{{bmadFolderName}}/{{path}}]]
|
|
||||||
|
|
||||||
Follow all instructions in the task file exactly as written.
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
inclusion: manual
|
|
||||||
---
|
|
||||||
|
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
Read the entire tool file at: #[[file:{{bmadFolderName}}/{{path}}]]
|
|
||||||
|
|
||||||
Follow all instructions in the tool file exactly as written.
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
inclusion: manual
|
|
||||||
---
|
|
||||||
|
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL #[[file:{{bmadFolderName}}/{{path}}]], READ its entire contents and follow its directions exactly!
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
---
|
|
||||||
mode: all
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. FOLLOW every step in the <activation> section precisely
|
|
||||||
4. DISPLAY the welcome/greeting as instructed
|
|
||||||
5. PRESENT the numbered menu
|
|
||||||
6. WAIT for user input before proceeding
|
|
||||||
</agent-activation>
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
---
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
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}}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
---
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
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}}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
---
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
Execute the BMAD '{{name}}' workflow.
|
|
||||||
|
|
||||||
CRITICAL: You must load and follow the workflow definition exactly.
|
|
||||||
|
|
||||||
WORKFLOW INSTRUCTIONS:
|
|
||||||
|
|
||||||
1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
2. READ its entire contents
|
|
||||||
3. FOLLOW every step precisely as specified
|
|
||||||
4. DO NOT skip or modify any steps
|
|
||||||
|
|
||||||
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
---
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
Execute the BMAD '{{name}}' workflow.
|
|
||||||
|
|
||||||
CRITICAL: You must load and follow the workflow definition exactly.
|
|
||||||
|
|
||||||
WORKFLOW INSTRUCTIONS:
|
|
||||||
|
|
||||||
1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
2. READ its entire contents
|
|
||||||
3. FOLLOW every step precisely as specified
|
|
||||||
4. DO NOT skip or modify any steps
|
|
||||||
|
|
||||||
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
{{description}}
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
|
|
||||||
|
|
||||||
Follow all instructions in the workflow file exactly as written.
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
{{description}}
|
|
||||||
|
|
||||||
## Instructions
|
|
||||||
|
|
||||||
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
|
|
||||||
|
|
||||||
Follow all instructions in the workflow file exactly as written.
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
---
|
|
||||||
description: '{{description}}'
|
|
||||||
auto_execution_mode: "iterate"
|
|
||||||
---
|
|
||||||
|
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
Read the entire workflow file at {project-root}/_bmad/{{workflow_path}}
|
|
||||||
|
|
||||||
Follow all instructions in the workflow file exactly as written.
|
|
||||||
|
|
@ -155,33 +155,6 @@ class CustomModuleManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use parseSource() instead. Kept for backward compatibility.
|
|
||||||
* Parse and validate a GitHub repository URL.
|
|
||||||
* @param {string} url - GitHub URL to validate
|
|
||||||
* @returns {Object} { owner, repo, isValid, error }
|
|
||||||
*/
|
|
||||||
validateGitHubUrl(url) {
|
|
||||||
if (!url || typeof url !== 'string') {
|
|
||||||
return { owner: null, repo: null, isValid: false, error: 'URL is required' };
|
|
||||||
}
|
|
||||||
const trimmed = url.trim();
|
|
||||||
|
|
||||||
// HTTPS format: https://github.com/owner/repo[.git] (strict, no trailing path)
|
|
||||||
const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
||||||
if (httpsMatch) {
|
|
||||||
return { owner: httpsMatch[1], repo: httpsMatch[2], isValid: true, error: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
// SSH format: git@github.com:owner/repo[.git]
|
|
||||||
const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
||||||
if (sshMatch) {
|
|
||||||
return { owner: sshMatch[1], repo: sshMatch[2], isValid: true, error: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { owner: null, repo: null, isValid: false, error: 'Not a valid GitHub URL (expected https://github.com/owner/repo)' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Marketplace JSON ─────────────────────────────────────────────────────
|
// ─── Marketplace JSON ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -109,46 +109,6 @@ class ExternalModuleManager {
|
||||||
return modules.find((m) => m.code === code) || null;
|
return modules.find((m) => m.code === code) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get module info by key
|
|
||||||
* @param {string} key - The module key (e.g., 'bmad-creative-intelligence-suite')
|
|
||||||
* @returns {Object|null} Module info or null if not found
|
|
||||||
*/
|
|
||||||
async getModuleByKey(key) {
|
|
||||||
const modules = await this.listAvailable();
|
|
||||||
return modules.find((m) => m.key === key) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a module code exists in external modules
|
|
||||||
* @param {string} code - The module code to check
|
|
||||||
* @returns {boolean} True if the module exists
|
|
||||||
*/
|
|
||||||
async hasModule(code) {
|
|
||||||
const module = await this.getModuleByCode(code);
|
|
||||||
return module !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the URL for a module by code
|
|
||||||
* @param {string} code - The module code
|
|
||||||
* @returns {string|null} The URL or null if not found
|
|
||||||
*/
|
|
||||||
async getModuleUrl(code) {
|
|
||||||
const module = await this.getModuleByCode(code);
|
|
||||||
return module ? module.url : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the module definition path for a module by code
|
|
||||||
* @param {string} code - The module code
|
|
||||||
* @returns {string|null} The module definition path or null if not found
|
|
||||||
*/
|
|
||||||
async getModuleDefinition(code) {
|
|
||||||
const module = await this.getModuleByCode(code);
|
|
||||||
return module ? module.moduleDefinition : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the cache directory for external modules
|
* Get the cache directory for external modules
|
||||||
* @returns {string} Path to the external modules cache directory
|
* @returns {string} Path to the external modules cache directory
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ class OfficialModules {
|
||||||
// Config collection state (merged from ConfigCollector)
|
// Config collection state (merged from ConfigCollector)
|
||||||
this.collectedConfig = {};
|
this.collectedConfig = {};
|
||||||
this._existingConfig = null;
|
this._existingConfig = null;
|
||||||
|
// Tracked during interactive config collection so {directory_name}
|
||||||
|
// placeholder defaults can be resolved in buildQuestion().
|
||||||
this.currentProjectDir = null;
|
this.currentProjectDir = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,32 +502,6 @@ class OfficialModules {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all .md agent files recursively in a directory
|
|
||||||
* @param {string} dir - Directory to search
|
|
||||||
* @returns {Array} List of .md agent file paths
|
|
||||||
*/
|
|
||||||
async findAgentMdFiles(dir) {
|
|
||||||
const agentFiles = [];
|
|
||||||
|
|
||||||
async function searchDirectory(searchDir) {
|
|
||||||
const entries = await fs.readdir(searchDir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(searchDir, entry.name);
|
|
||||||
|
|
||||||
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
||||||
agentFiles.push(fullPath);
|
|
||||||
} else if (entry.isDirectory()) {
|
|
||||||
await searchDirectory(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await searchDirectory(dir);
|
|
||||||
return agentFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create directories declared in module.yaml's `directories` key
|
* Create directories declared in module.yaml's `directories` key
|
||||||
* This replaces the security-risky module installer pattern with declarative config
|
* This replaces the security-risky module installer pattern with declarative config
|
||||||
|
|
@ -699,29 +675,6 @@ class OfficialModules {
|
||||||
return { createdDirs, movedDirs, createdWdsFolders };
|
return { createdDirs, movedDirs, createdWdsFolders };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Private: Process module configuration
|
|
||||||
* @param {string} modulePath - Path to installed module
|
|
||||||
* @param {string} moduleName - Module name
|
|
||||||
*/
|
|
||||||
async processModuleConfig(modulePath, moduleName) {
|
|
||||||
const configPath = path.join(modulePath, 'config.yaml');
|
|
||||||
|
|
||||||
if (await fs.pathExists(configPath)) {
|
|
||||||
try {
|
|
||||||
let configContent = await fs.readFile(configPath, 'utf8');
|
|
||||||
|
|
||||||
// Replace path placeholders
|
|
||||||
configContent = configContent.replaceAll('{project-root}', `bmad/${moduleName}`);
|
|
||||||
configContent = configContent.replaceAll('{module}', moduleName);
|
|
||||||
|
|
||||||
await fs.writeFile(configPath, configContent, 'utf8');
|
|
||||||
} catch (error) {
|
|
||||||
await prompts.log.warn(`Failed to process module config: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private: Sync module files (preserving user modifications)
|
* Private: Sync module files (preserving user modifications)
|
||||||
* @param {string} sourcePath - Source module path
|
* @param {string} sourcePath - Source module path
|
||||||
|
|
@ -1091,7 +1044,6 @@ class OfficialModules {
|
||||||
*/
|
*/
|
||||||
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
||||||
this.currentProjectDir = projectDir;
|
this.currentProjectDir = projectDir;
|
||||||
|
|
||||||
// Load existing config if not already loaded
|
// Load existing config if not already loaded
|
||||||
if (!this._existingConfig) {
|
if (!this._existingConfig) {
|
||||||
await this.loadExistingConfig(projectDir);
|
await this.loadExistingConfig(projectDir);
|
||||||
|
|
|
||||||
|
|
@ -50,17 +50,6 @@ class RegistryClient {
|
||||||
const content = await this.fetch(url, timeout);
|
const content = await this.fetch(url, timeout);
|
||||||
return yaml.parse(content);
|
return yaml.parse(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a URL and parse the response as JSON.
|
|
||||||
* @param {string} url - URL to fetch
|
|
||||||
* @param {number} [timeout] - Timeout in ms
|
|
||||||
* @returns {Promise<Object>} Parsed JSON content
|
|
||||||
*/
|
|
||||||
async fetchJson(url, timeout) {
|
|
||||||
const content = await this.fetch(url, timeout);
|
|
||||||
return JSON.parse(content);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { RegistryClient };
|
module.exports = { RegistryClient };
|
||||||
|
|
|
||||||
|
|
@ -498,26 +498,6 @@ async function password(options) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Group multiple prompts together
|
|
||||||
* @param {Object} prompts - Object of prompt functions
|
|
||||||
* @param {Object} [options] - Group options
|
|
||||||
* @returns {Promise<Object>} Object with all answers
|
|
||||||
*/
|
|
||||||
async function group(prompts, options = {}) {
|
|
||||||
const clack = await getClack();
|
|
||||||
|
|
||||||
const result = await clack.group(prompts, {
|
|
||||||
onCancel: () => {
|
|
||||||
clack.cancel('Operation cancelled');
|
|
||||||
process.exit(0);
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run tasks with spinner feedback
|
* Run tasks with spinner feedback
|
||||||
* @param {Array} tasks - Array of task objects [{title, task, enabled?}]
|
* @param {Array} tasks - Array of task objects [{title, task, enabled?}]
|
||||||
|
|
@ -578,42 +558,6 @@ async function box(content, title, options) {
|
||||||
clack.box(content, title, options);
|
clack.box(content, title, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a progress bar for visualizing task completion
|
|
||||||
* @param {Object} [options] - Progress options (max, style, etc.)
|
|
||||||
* @returns {Promise<Object>} Progress controller with start, advance, stop methods
|
|
||||||
*/
|
|
||||||
async function progress(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.progress(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a task log for displaying scrolling subprocess output
|
|
||||||
* @param {Object} options - TaskLog options (title, limit, retainLog)
|
|
||||||
* @returns {Promise<Object>} TaskLog controller with message, success, error methods
|
|
||||||
*/
|
|
||||||
async function taskLog(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.taskLog(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* File system path prompt with autocomplete
|
|
||||||
* @param {Object} options - Path options
|
|
||||||
* @param {string} options.message - The prompt message
|
|
||||||
* @param {string} [options.initialValue] - Initial path value
|
|
||||||
* @param {boolean} [options.directory=false] - Only allow directories
|
|
||||||
* @param {Function} [options.validate] - Validation function
|
|
||||||
* @returns {Promise<string>} Selected path
|
|
||||||
*/
|
|
||||||
async function pathPrompt(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
const result = await clack.path(options);
|
|
||||||
await handleCancel(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Autocomplete single-select prompt with type-ahead filtering
|
* Autocomplete single-select prompt with type-ahead filtering
|
||||||
* @param {Object} options - Autocomplete options
|
* @param {Object} options - Autocomplete options
|
||||||
|
|
@ -631,50 +575,6 @@ async function autocomplete(options) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Key-based instant selection prompt
|
|
||||||
* @param {Object} options - SelectKey options
|
|
||||||
* @param {string} options.message - The prompt message
|
|
||||||
* @param {Array} options.options - Array of choices [{value, label, hint?}]
|
|
||||||
* @returns {Promise<any>} Selected value
|
|
||||||
*/
|
|
||||||
async function selectKey(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
const result = await clack.selectKey(options);
|
|
||||||
await handleCancel(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stream messages with dynamic content (for LLMs, generators, etc.)
|
|
||||||
*/
|
|
||||||
const stream = {
|
|
||||||
async info(generator) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.stream.info(generator);
|
|
||||||
},
|
|
||||||
async success(generator) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.stream.success(generator);
|
|
||||||
},
|
|
||||||
async step(generator) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.stream.step(generator);
|
|
||||||
},
|
|
||||||
async warn(generator) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.stream.warn(generator);
|
|
||||||
},
|
|
||||||
async error(generator) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.stream.error(generator);
|
|
||||||
},
|
|
||||||
async message(generator, options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.stream.message(generator, options);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the color utility (picocolors instance from @clack/prompts)
|
* Get the color utility (picocolors instance from @clack/prompts)
|
||||||
* @returns {Promise<Object>} The color utility (picocolors)
|
* @returns {Promise<Object>} The color utility (picocolors)
|
||||||
|
|
@ -790,20 +690,14 @@ module.exports = {
|
||||||
note,
|
note,
|
||||||
box,
|
box,
|
||||||
spinner,
|
spinner,
|
||||||
progress,
|
|
||||||
taskLog,
|
|
||||||
select,
|
select,
|
||||||
multiselect,
|
multiselect,
|
||||||
autocompleteMultiselect,
|
autocompleteMultiselect,
|
||||||
autocomplete,
|
autocomplete,
|
||||||
selectKey,
|
|
||||||
confirm,
|
confirm,
|
||||||
text,
|
text,
|
||||||
path: pathPrompt,
|
|
||||||
password,
|
password,
|
||||||
group,
|
|
||||||
tasks,
|
tasks,
|
||||||
log,
|
log,
|
||||||
stream,
|
|
||||||
prompt,
|
prompt,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue