diff --git a/bmad-copilot/.gitignore b/bmad-copilot/.gitignore new file mode 100644 index 00000000..edc37814 --- /dev/null +++ b/bmad-copilot/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +out/ +releases/ +*.vsix +*.tgz +.DS_Store +Thumbs.db diff --git a/bmad-copilot/.npmignore b/bmad-copilot/.npmignore new file mode 100644 index 00000000..503c414a --- /dev/null +++ b/bmad-copilot/.npmignore @@ -0,0 +1,24 @@ +# Source +src/ +*.ts +!dist/**/*.d.ts + +# Development +.vscode/ +.github/ +out/ +node_modules/ +scripts/ +releases/ + +# Testing +TESTING.md +*.vsix + +# Config +tsconfig.json +.vscodeignore +.eslintrc* +.prettierrc* +.gitignore +.editorconfig diff --git a/bmad-copilot/.vscode/launch.json b/bmad-copilot/.vscode/launch.json new file mode 100644 index 00000000..d047039c --- /dev/null +++ b/bmad-copilot/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ], + "preLaunchTask": "npm: compile" + }, + { + "name": "Run Extension (Watch)", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ], + "preLaunchTask": "npm: watch" + } + ] +} diff --git a/bmad-copilot/.vscode/tasks.json b/bmad-copilot/.vscode/tasks.json new file mode 100644 index 00000000..ec1b6449 --- /dev/null +++ b/bmad-copilot/.vscode/tasks.json @@ -0,0 +1,23 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "compile", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$tsc", + "label": "npm: compile" + }, + { + "type": "npm", + "script": "watch", + "group": "build", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "label": "npm: watch" + } + ] +} diff --git a/bmad-copilot/.vscodeignore b/bmad-copilot/.vscodeignore new file mode 100644 index 00000000..fb24a4e8 --- /dev/null +++ b/bmad-copilot/.vscodeignore @@ -0,0 +1,13 @@ +.vscode/** +.github/** +src/** +out/** +releases +releases/** +scripts/** +node_modules/** +tsconfig.json +.gitignore +.npmignore +TESTING.md +*.vsix diff --git a/bmad-copilot/ARCHITECTURE.md b/bmad-copilot/ARCHITECTURE.md new file mode 100644 index 00000000..c5cd4aee --- /dev/null +++ b/bmad-copilot/ARCHITECTURE.md @@ -0,0 +1,357 @@ +# BMAD Copilot Adapter — Architecture Document + +> **Version**: 0.2.4 +> **BMAD-METHOD Compatibility**: ≥ 6.0.0-Beta.1 +> **VS Code Engine**: ≥ 1.93.0 +> **Last updated**: 2026-02 + +--- + +## 1. Design Philosophy + +This extension is a **pure read-only adapter** — an Official BMAD GitHub +Copilot Prompt Executor. + +It is **not** a CLI runtime, **not** a workflow engine, **not** a prompt +pre-processor, and **not** a file generator. It reads the official +`.prompt.md` / `bmad-agent-*.md` files generated by `npx bmad-method install +--tools github-copilot` and passes them directly to the Copilot LLM — the +same files that native GitHub Copilot Chat would consume. + +### What This Extension Does + +- Registers the `@bmad` chat participant with slash commands +- Discovers and indexes available BMAD commands from `_bmad/_config/` CSVs +- Maps command names to `.github/prompts/*.prompt.md` and `.github/agents/bmad-agent-*.md` +- Reads the prompt file content and sends it to the Copilot LLM **as-is** +- Spawns the real BMAD CLI for `install` operations + +### What This Extension Does **NOT** Do + +- ❌ Pre-read or inline `{project-root}` file references — the LLM reads workspace files itself +- ❌ Parse, rewrite, or transform official prompt file content +- ❌ Mirror, convert, or generate prompt files from other IDE formats +- ❌ Manage token budgets or truncate prompts +- ❌ Merge configs or resolve YAML/XML workflows programmatically +- ❌ Replicate BMAD CLI execution logic +- ❌ Modify any file in `_bmad/` directory (read-only access) +- ❌ Create or overwrite files in `.github/prompts/` or `.github/agents/` + +### Why Not Inline? + +Official BMAD `.prompt.md` files contain instructions like: + +``` +Read the file at {project-root}/_bmad/bmm/config.yaml +``` + +GitHub Copilot's native prompt system resolves `{project-root}` and +gives the LLM access to workspace files. When the extension sends these +prompts via `request.model.sendRequest()`, the LLM can still access +workspace files through its context. Pre-reading and inlining these +files would duplicate content, waste tokens, and diverge from the +official BMAD prompt design. + +> **Note:** `request.model.sendRequest()` may not fully resolve +> `{project-root}` references in all cases. This is a known limitation +> of the Chat Participant API vs. native Copilot prompt files. The +> official prompts are still passed unmodified — the LLM does its best +> with the workspace context available. + +--- + +## 2. Overview + +``` +┌─────────────────────────────────────────────────┐ +│ VS Code Copilot Chat │ +│ │ +│ User types: @bmad /run bmad-bmm-create-prd │ +└──────────────────────┬──────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────┐ +│ extension.ts — Activation & Lifecycle │ +│ ┌────────────────────────────────────────────┐ │ +│ │ chatBridge.ts — Prompt Executor │ │ +│ │ ┌──────────────────────────────────────┐ │ │ +│ │ │ commandRegistry.ts — Command Index │ │ │ +│ │ │ cliBridge.ts — CLI Process Spawner │ │ │ +│ │ └──────────────────────────────────────┘ │ │ +│ └────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ Copilot LLM │ + │ (User's model) │ + └─────────────────┘ +``` + +--- + +## 3. Module Architecture + +### `extension.ts` — Lifecycle + +| Responsibility | Detail | +| --------------- | -------------------------------------------------------------------------------------- | +| Activation | Triggered by `onChatParticipant:bmad` or `onStartupFinished` | +| Participant | Registers `@bmad` via `vscode.chat.createChatParticipant()` | +| Initial Scan | Calls `CommandRegistry.scan()` at activation | +| Integrity Check | Warns if `_bmad/` exists but `.github/prompts/` and `.github/agents/` are both missing | +| File Watchers | Monitors `_bmad/**`, `.github/prompts/**`, `.github/agents/**` | +| Configuration | Reacts to `bmad.*` setting changes | +| Commands | Provides `rescan`, `update`, and `install` VS Code commands | + +### `chatBridge.ts` — Prompt Executor (Core) + +| Responsibility | Detail | +| --------------------- | --------------------------------------------------------------------------------------------------- | +| Routing | Maps slash commands (`/help`, `/run`, `/agents`, etc.) to handlers | +| Prompt File Execution | Reads `.prompt.md` / `.agent.md` → strips frontmatter → sends raw prompt to LLM (no ref resolution) | +| CLI Delegation | Delegates `/install` to `CliBridge.openTerminal()` | +| Free-text | Fuzzy-matches user input to known commands | +| LLM Streaming | Calls `request.model.sendRequest()` and streams fragments | + +### `commandRegistry.ts` — Command Index + +| Responsibility | Detail | +| ------------------- | ------------------------------------------------------------------------ | +| Discovery | Finds `_bmad/` directory and enumerates modules | +| CSV Parsing | Zero-dependency parser for 5 manifest CSVs (with UTF-8 BOM stripping) | +| Prompt File Mapping | Scans `.github/prompts/` and `.github/agents/` to link files to commands | +| Name Conversion | `cliToSlash()` / `slashToCli()` bidirectional mapping | +| Fuzzy Search | Case-insensitive substring matching across names/descriptions | +| Invalidation | `invalidate()` clears cache; `rescan()` combines invalidate + scan | + +### `cliBridge.ts` — CLI Process Spawner + +| Responsibility | Detail | +| -------------- | ---------------------------------------------------------- | +| Terminal | Opens VS Code terminal with `npx bmad-method install` | +| Detection | Checks for `_bmad/`, `.github/prompts/`, `.github/agents/` | +| Version | Queries `npx bmad-method --version` | + +### `promptMirror.ts` — DEPRECATED + +| Responsibility | Detail | +| -------------- | -------------------------------------------------------------- | +| **Status** | **@deprecated** — not imported by any active module | +| Original Role | Mirrored claude-code prompts to Copilot format | +| Current Role | Historical reference only; will be removed in a future version | + +### `bmadRuntime.ts` — DEPRECATED + +| Responsibility | Detail | +| -------------- | -------------------------------------------------------------- | +| **Status** | **@deprecated** — not imported by any active module | +| Original Role | Built prompts directly from `_bmad/` manifests as a fallback | +| Current Role | Historical reference only; will be removed in a future version | + +--- + +## 4. CLI Bootstrap Layer + +The adapter ships a CLI entry point at `bin/bmad-copilot-adapter.js` that +provides three commands for terminal-based project setup: + +| Command | Module | Purpose | +| ----------- | ------------------- | --------------------------------------------------- | +| `bootstrap` | `src/cli/bootstrap` | Full setup: check Node, detect prompts, install ext | +| `update` | `src/cli/update` | Rescan prompts, write sentinel, print summary | +| `status` | `src/cli/status` | Show full installation diagnostics | + +```bash +npx bmad-copilot-adapter bootstrap # first-time project setup +npx bmad-copilot-adapter update # after re-running bmad-method install +npx bmad-copilot-adapter status # diagnose problems +``` + +The CLI is a **diagnostic and setup tool only** — it never modifies prompt +files, never invokes mirroring logic, and never writes to `.github/`. + +--- + +## 5. Data Flow — `/run bmad-bmm-create-prd` + +``` +User: @bmad /run bmad-bmm-create-prd + │ + ▼ +extension.ts + │ chatBridge.handler(request, context, stream, token) + ▼ +chatBridge.ts :: handleRun() + │ parts = ["bmad-bmm-create-prd"] + │ command = registry.resolve("bmad-bmm-create-prd") + ▼ +chatBridge.ts :: executeCommand() + │ promptFilePath = .github/prompts/bmad-bmm-create-prd.prompt.md + │ exists? → YES + ▼ +chatBridge.ts :: buildFromPromptFile() + │ 1. Read raw file content + │ 2. Strip YAML frontmatter (---...---) + │ 3. Append user input (if any) + │ 4. Return body AS-IS (no inline, no file resolution) + ▼ +chatBridge.ts :: sendToLlm(prompt, request, stream, token) + │ messages = [LanguageModelChatMessage.User(prompt)] + │ chatResponse = request.model.sendRequest(messages, {}, token) + │ for await (fragment of chatResponse.text) + │ stream.markdown(fragment) + ▼ +User sees streamed response +``` + +### Missing Prompt File Path + +``` +chatBridge.ts :: executeCommand() + │ promptFilePath found? → NO + │ → Show ⚠️ error with guidance: + │ "Run: npx bmad-method install --tools github-copilot --yes" +``` + +--- + +## 6. Command Naming Convention + +### CLI ↔ Slash Conversion + +| CLI Syntax | Slash Syntax | Category | +| ------------------------ | ------------------------ | --------------- | +| `bmad:agent:bmm:pm` | `bmad-agent-bmm-pm` | Agent | +| `bmad:bmm:create-prd` | `bmad-bmm-create-prd` | Module Workflow | +| `bmad:help` | `bmad-help` | Core Task | +| `bmad:create-next-story` | `bmad-create-next-story` | Core Workflow | + +**Rule**: CLI→Slash: replace all `:` with `-`. Slash→CLI: structural +dashes only are converted back to `:` (see `slashToCli()` in +`commandRegistry.ts` for the heuristic). +--- + +## 7. Static Slash Commands (VS Code Limitation) + +The VS Code Chat Participant API **does not support dynamic slash command +registration**. All commands must be declared statically in `package.json`. + +We declare **8 static gateway commands** that delegate to the dynamic +registry at runtime: + +| Static Command | Mode | Behaviour | +| -------------- | --------------- | ------------------------------------------ | +| `/install` | CLI Bridge | Opens terminal → `npx bmad-method install` | +| `/status` | Built-in + CLI | Shows installation health table | +| `/update` | Built-in | Invalidate + rescan command registry | +| `/help` | Built-in | Lists commands + optional LLM help | +| `/run ` | Prompt Executor | Reads `.prompt.md` / `.agent.md` → LLM | +| `/agents` | Built-in | Lists installed agents | +| `/workflows` | Built-in | Lists installed workflows | +| `/tasks` | Built-in | Lists installed tasks and tools | + +### Free-text Fallback + +When the user types to `@bmad` without a slash command: + +1. If input starts with `bmad-`, attempt exact command match → execute. +2. Otherwise, inject BMAD context and forward to LLM as free chat. + +--- + +## 8. Risk Analysis + +### High Priority + +| Risk | Impact | Mitigation | +| ----------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------- | +| **Static command limitation** | Cannot register individual slash commands | Hybrid `/run ` gateway + fuzzy free-text matching | +| **LLM context scope** | `sendRequest()` may not resolve `{project-root}` like native Copilot | Documented limitation; prompts passed unmodified | +| **BMAD version drift** | CSV schema changes may break parsing | Defensive parsing with fallback defaults | +| **Config file exposure** | `config.yaml` content is sent to LLM; may leak sensitive data | **Never store secrets in config files**; use env vars | + +### Medium Priority + +| Risk | Impact | Mitigation | +| ----------------------------- | --------------------------------------------- | ------------------------------------ | +| **Prompt fidelity** | LLM interpretation differs from CLI execution | Pass official prompts unmodified | +| **Race conditions on rescan** | Rapid file changes trigger multiple scans | Debounce timer (2 s); scan is atomic | +| **Multi-root workspaces** | Extension only reads first workspace folder | Document limitation | + +### Low Priority + +| Risk | Impact | Mitigation | +| ------------------------ | ------------------------------------------------ | --------------------------------- | +| **No offline LLM** | Requires Copilot + internet | Not solvable at this layer | +| **Missing prompt files** | `--tools github-copilot` not used during install | Clear error with install guidance | + +--- + +## 9. File Manifest + +| File | Purpose | Status | +| ------------------------ | ---------------------------------------------- | --------------- | +| `src/extension.ts` | Activation, watchers, participant registration | Active | +| `src/chatBridge.ts` | Prompt executor, slash command routing | Active (core) | +| `src/commandRegistry.ts` | CSV scanning, prompt file mapping | Active | +| `src/cliBridge.ts` | CLI process spawner (`install`) | Active | +| `src/cli/index.ts` | CLI router (argv parsing) | Active | +| `src/cli/bootstrap.ts` | CLI bootstrap command | Active | +| `src/cli/update.ts` | CLI update command | Active | +| `src/cli/status.ts` | CLI status command | Active | +| `src/types.ts` | Shared interfaces and types | Active | +| `src/promptMirror.ts` | Historical claude-code mirror | **@deprecated** | +| `src/bmadRuntime.ts` | Legacy prompt builder from `_bmad/` manifests | **@deprecated** | +| `package.json` | Extension manifest | Active | +| `tsconfig.json` | TypeScript configuration | Active | + +--- + +## 10. Security Best Practices + +⚠️ **Configuration File Security** + +The extension reads BMAD configuration files (`_bmad/*/config.yaml`) and +includes their content in prompts sent to the GitHub Copilot LLM. This is +necessary for the LLM to understand project-specific settings and variables. + +**Important Guidelines:** + +1. **Never store sensitive data** in BMAD config files: + - ❌ API keys, tokens, passwords + - ❌ Database credentials + - ❌ Private keys or certificates + - ❌ Personal identifiable information (PII) + +2. **Use environment variables** for secrets: + + ```yaml + # ✅ Good - reference environment variables + api_endpoint: "https://api.example.com" + auth_method: "env:API_TOKEN" + + # ❌ Bad - hardcoded secrets + api_key: "sk-1234567890abcdef" + ``` + +3. **Config file scope:** + - The extension sends full `config.yaml` content to the LLM + - Status commands include 500-char excerpts from each module config + - Assume all config content is visible to the AI model + +4. **Recommended practices:** + - Store only non-sensitive configuration: file paths, flags, workflow settings + - Use `.gitignore` to exclude any local config overrides + - Review config files before committing to version control + +--- + +## 11. Future Roadmap + +1. **Multi-root workspace support** — scan all workspace folders. +2. **Follow-up message handling** — maintain conversation state per command. +3. **Proposed API migration** — when VS Code supports dynamic slash + commands, auto-register all discovered commands. +4. **Testing harness** — unit tests for CSV parsing and command resolution. +5. **Remove deprecated modules** — delete `promptMirror.ts` and `bmadRuntime.ts`. diff --git a/bmad-copilot/CHANGELOG.md b/bmad-copilot/CHANGELOG.md new file mode 100644 index 00000000..f3ef6aae --- /dev/null +++ b/bmad-copilot/CHANGELOG.md @@ -0,0 +1,77 @@ +# Changelog + +## [0.2.4] — 2026-02-11 + +### Fixed + +- **Critical: Agent file detection** — BMAD v6 generates `bmad-agent-*.md` files (not `*.agent.md`). Updated file pattern matching across all modules: + - `commandRegistry.ts` — agent scan now matches `bmad-agent-*.md` in `.github/agents/` + - `extension.ts` — integrity check now accepts `.github/agents/` (not just `.github/prompts/`) + - `chatBridge.ts` — error message now references both `.github/prompts/` and `.github/agents/` + - `cli/bootstrap.ts` — detects `bmad-agent-*.md` pattern + - `cli/update.ts` — counts agent files correctly + - `cli/status.ts` — lists agent files correctly + +### Changed + +- `README.md` — Updated for BMAD v6 structure (`.github/agents/` is primary, `.github/prompts/` is secondary) +- `ARCHITECTURE.md` — Updated version to 0.2.4, corrected agent file references +- Publisher changed to `evil9369` (extension ID: `evil9369.bmad-copilot-adapter`) +- DEP0190 deprecation warning fixed in CLI (Windows `child_process` shell commands) + +## [0.2.0] — 2026-02-11 + +### Added + +- **CLI Bootstrap Layer** (`bin/bmad-copilot-adapter.js`) + - `npx bmad-copilot-adapter bootstrap` — Full setup: check Node ≥18, verify prompts, verify VS Code extension is installed, validate registration + - `npx bmad-copilot-adapter update` — Rescan `.github/prompts`, count manifests, write sentinel, trigger extension refresh + - `npx bmad-copilot-adapter status` — Show full BMAD installation diagnostics + +- **`/update` Copilot Chat command** + - Invalidates cached command registry + - Triggers full rescan of prompt files + - Available via `@bmad /update` in Copilot Chat + +- **`bmad-copilot.update` VS Code command** + - Available from Command Palette: "BMAD Copilot: Update (Invalidate + Rescan)" + - Equivalent to CLI `npx bmad-copilot-adapter update` + +- **`CommandRegistry.invalidate()` and `CommandRegistry.rescan()`** + - `invalidate()` clears cached state immediately + - `rescan()` combines invalidate + scan in one call + +- **Prompt integrity auto-check on activation** + - Detects `_bmad/` present but `.github/prompts/` missing + - Shows actionable notification with "Run Update" button + +- **UTF-8 BOM stripping** in CSV parsing + - `loadCsv()` now strips BOM before parsing (fixes Windows-generated CSV headers) + +### Changed + +- **Pure adapter architecture** — all mirror/transform/conversion logic removed + - `chatBridge.ts` no longer imports `BmadRuntime` or `hasClaudeCodeSource` + - `executeCommand()` shows clear error with install guidance when prompt file is missing (no fallback) + - `handleStatus()` no longer reports claude-code mirror status + - `MISSING_COPILOT_FILES_MESSAGE` updated to point to official BMAD installer only +- `promptMirror.ts` — marked as `@deprecated` (no active imports) +- `bmadRuntime.ts` — marked as `@deprecated` (no active imports) +- Version bumped to 0.2.0 (minor) +- `package.json` now declares `bin` field for CLI entry +- `files` array includes `bin/bmad-copilot-adapter.js` +- Chat participant commands now include `/update` +- Installation guard allows `/update` to run even when state is null +- ARCHITECTURE.md and README.md fully rewritten for pure adapter model +- Removed GitHub Actions CI/CD workflow (`release.yml`) +- Removed all global install (`-g`) references from documentation +- README now recommends project-level installation only +- Added troubleshooting section for 0-command scenario +- Release process is now manual (`npm publish`) only + +## [0.1.2] — Initial release + +- Core Chat Participant (`@bmad`) +- Prompt File Executor +- Command Registry with CSV manifest parsing +- CLI Bridge for terminal operations diff --git a/bmad-copilot/LICENSE b/bmad-copilot/LICENSE new file mode 100644 index 00000000..d68af9d5 --- /dev/null +++ b/bmad-copilot/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 BMAD Code Organization + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bmad-copilot/README.md b/bmad-copilot/README.md new file mode 100644 index 00000000..edc06a35 --- /dev/null +++ b/bmad-copilot/README.md @@ -0,0 +1,452 @@ +# bmad-copilot-adapter + +> Official prompt executor that enables BMAD-METHOD workflows to run natively inside GitHub Copilot Chat. Not a fork — a pure read-only bridge. + +> **⚠️ Important:** This extension is distributed as **source code only**. You must build and install it yourself — it is not available on the VS Code Marketplace. + +[![npm version](https://img.shields.io/npm/v/bmad-copilot-adapter)](https://www.npmjs.com/package/bmad-copilot-adapter) +[![BMAD-METHOD](https://img.shields.io/badge/BMAD--METHOD-≥6.0.0-blue)](https://github.com/pi-docket/bmad-method-vscode) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) + +--- + +## Installation + +**Note:** This extension is not published to the VS Code Marketplace. You must build and install it manually from source. + +### Step 1 — Build and Install the Extension + +```bash +git clone https://github.com/pi-docket/bmad-method-vscode.git +cd bmad-method-vscode/bmad-copilot +npm install +npm run vsce:package +code --install-extension releases/bmad-copilot-adapter-0.2.5.vsix +``` + +### Step 2 — Install BMAD-METHOD in Your Project + +```bash +cd /path/to/your-project +npx bmad-method install # interactive menu — select GitHub Copilot as tool +npx bmad-copilot-adapter bootstrap # detect prompts and verify setup +``` + +> **Tip:** After building the extension, you can optionally add the CLI +> tool as a dev dependency in your projects: +> +> ```bash +> npm install --save-dev bmad-copilot-adapter +> ``` + +--- + +## Overview + +**bmad-copilot-adapter** is a VS Code extension that bridges the [BMAD-METHOD](https://github.com/pi-docket/bmad-method-vscode) command system into GitHub Copilot Chat. It reads the official `.prompt.md` and agent `.md` files generated by the BMAD installer and passes them **directly** to the Copilot LLM — no workflow parsing, no prompt reconstruction, no token management. + +``` +CLI: bmad:bmm:create-prd +Copilot: @bmad /run bmad-bmm-create-prd +``` + +### What It Does + +- Registers the `@bmad` chat participant with 8 slash commands +- Discovers and indexes BMAD commands from `_bmad/_config/` manifests +- Reads official prompt files and sends them **as-is** to the LLM +- Provides CLI bootstrap, update, and status commands +- Spawns the real BMAD CLI for `install` operations + +### What It Does NOT Do + +- ❌ Parse, rewrite, or transform prompt file content +- ❌ Mirror or convert prompts from other IDE formats +- ❌ Pre-read or inline `{project-root}` file references +- ❌ Manage token budgets or truncate prompts +- ❌ Bundle, modify, or replace any part of `bmad-method` +- ❌ Create or overwrite files in `.github/` or `_bmad/` +- ❌ Auto-install `bmad-method` — it only validates prompts + +--- + +## Quick Start + +**Prerequisites:** You must have already built and installed the VS Code extension (see Installation section above). + +### Step 1 — Install BMAD-METHOD in Your Project + +```bash +cd /path/to/your-project +npx bmad-method install +``` + +The interactive menu lets you pick modules (e.g. `bmm`, `bmb`, `tea`). +**Select GitHub Copilot as your tool** to generate `.github/prompts/`. + +This creates: + +- `_bmad/` — Core framework and module files +- `_bmad/_config/` — CSV command manifests +- `.github/agents/` — Copilot agent files (`bmad-agent-*.md`) +- `.github/prompts/` — Copilot prompt files (`.prompt.md`, if applicable) + +### Step 2 — Bootstrap the Adapter + +```bash +npx bmad-copilot-adapter bootstrap +``` + +This will: + +1. Check Node.js version (≥ 18) +2. Verify `.github/agents/` or `.github/prompts/` exists (warns if missing) +3. Verify the VS Code extension is installed +4. Print a summary of discovered commands + +### Step 3 — Verify in VS Code + +Open Copilot Chat (`Ctrl+Shift+I`) and type: + +``` +@bmad /status +``` + +You should see ✅ for `_bmad/` directory and GitHub Copilot prompt files. + +--- + +## Building from Source + +This extension is distributed as source code only. To build and install: + +```bash +# Clone the repository +git clone https://github.com/pi-docket/bmad-method-vscode.git +cd bmad-method-vscode/bmad-copilot + +# Install dependencies +npm install + +# Build the extension +npm run build + +# Package as VSIX +npm run vsce:package + +# Install in VS Code +code --install-extension releases/bmad-copilot-adapter-0.2.5.vsix +``` + +**Updating:** When new versions are released, pull the latest changes, rebuild, and reinstall: + +```bash +git pull +npm install +npm run vsce:package +code --install-extension releases/bmad-copilot-adapter-0.2.5.vsix --force +``` + +--- + +## CLI Commands + +The adapter provides a CLI for terminal-based project management: + +```bash +npx bmad-copilot-adapter bootstrap # Full first-time setup +npx bmad-copilot-adapter update # Rescan after re-installing BMAD +npx bmad-copilot-adapter status # Show full diagnostics +npx bmad-copilot-adapter --version # Show adapter version +npx bmad-copilot-adapter --help # Show CLI help +``` + +| Command | Purpose | +| ----------- | ---------------------------------------------------------------------------------- | +| `bootstrap` | Verify Node ≥18, check prompts, verify extension, print summary | +| `update` | Rescan `.github/agents/` & `prompts/`, count manifests, write sentinel for VS Code | +| `status` | Full diagnostic: Node, `_bmad/`, prompts, CSV health, VS Code | + +--- + +## Usage + +### Slash Commands + +| Command | Description | +| ------------------- | ------------------------------------ | +| `@bmad /status` | Check BMAD installation health | +| `@bmad /install` | Open terminal with BMAD installer | +| `@bmad /update` | Invalidate cache and rescan commands | +| `@bmad /help` | Show available commands and guidance | +| `@bmad /run ` | Execute any BMAD command by name | +| `@bmad /agents` | List installed BMAD agents | +| `@bmad /workflows` | List installed workflows by phase | +| `@bmad /tasks` | List installed tasks and tools | + +### Examples + +``` +@bmad /run bmad-bmm-create-prd +@bmad /run bmad-agent-bmm-pm +@bmad /help create prd +@bmad /agents +``` + +### Free-text + +You can also type naturally — the adapter matches your input to known commands: + +``` +@bmad I want to create a PRD +@bmad bmad-bmm-create-prd +``` + +--- + +## Troubleshooting + +### `@bmad` shows 0 commands + +If you open Copilot Chat and `@bmad /status` reports 0 commands: + +1. **Ensure `.github/agents/` exists** and contains `bmad-agent-*.md` files + (or `.github/prompts/` has `bmad*.prompt.md` files). + +2. **Rescan the command registry:** + + ```bash + npx bmad-copilot-adapter update + ``` + +3. **If still 0 commands**, the BMAD installer may not have generated + prompt files. Re-run: + + ```bash + npx bmad-method install --tools github-copilot + ``` + +4. **Reload the VS Code window** (`Ctrl+Shift+P` → `Developer: Reload Window`). + +**Why this happens:** The adapter discovers commands by scanning +`.github/agents/`, `.github/prompts/` and `_bmad/_config/` CSV manifests. If prompt/agent files +are missing (e.g. `--tools github-copilot` was not used during install) +or if the extension was activated before files existed, the registry +will be empty. Running `update` writes a sentinel file that triggers +the extension's file watcher to rescan. + +### Extension not activating + +- Ensure GitHub Copilot Chat is installed and active. +- The extension activates only when you type `@bmad` in Copilot Chat. +- Check the Output panel → "BMAD Copilot" for error logs. + +### `npx` commands not found + +If `npx bmad-copilot-adapter` fails: + +- Ensure you have **Node.js ≥ 18** and **npm ≥ 7** (npx is bundled). +- Try running with the full package name: `npx bmad-copilot-adapter@latest --version` +- Or install locally first: `npm install --save-dev bmad-copilot-adapter` + +--- + +## Command Name Mapping + +BMAD CLI uses colons (`:`), Copilot Chat uses dashes (`-`): + +| CLI Syntax | Copilot Chat | +| --------------------- | -------------------------------- | +| `bmad:bmm:create-prd` | `@bmad /run bmad-bmm-create-prd` | +| `bmad:agent:bmm:pm` | `@bmad /run bmad-agent-bmm-pm` | +| `bmad:help` | `@bmad /help` | + +--- + +## Configuration + +| Setting | Default | Description | +| --------------- | ------- | -------------------------------------------------- | +| `bmad.bmadDir` | `""` | Override path to `_bmad/` (auto-detected if empty) | +| `bmad.autoScan` | `true` | Scan commands on extension activation | +| `bmad.verbose` | `false` | Verbose logging to BMAD output channel | + +--- + +## Architecture + +This extension follows the **Official Prompt Executor** model — a pure +read-only adapter: + +``` +extension.ts → Lifecycle, activation, integrity check, watchers + └─ chatBridge.ts → Prompt executor, slash command routing + ├─ commandRegistry.ts → CSV scanning, prompt/agent file mapping + └─ cliBridge.ts → CLI process spawner (install) +``` + +### Execution Flow + +``` +@bmad /run X → find .github/prompts/X.prompt.md or .github/agents/X.md + → (if missing → show error with install guidance) + → read file, strip YAML frontmatter + → pass body AS-IS to Copilot LLM +``` + +### Design Principles + +1. **Pure adapter** — never modifies, generates, or converts files. +2. **Strict upstream alignment** — only reads files produced by the official BMAD installer. +3. **No prompt reconstruction** — prompt body is passed unmodified to the LLM. +4. **No workflow parsing** — the LLM interprets workflow instructions, matching native Copilot behavior. +5. **No bundling** — `bmad-method` is a peer dependency, never included in the package. + +See [ARCHITECTURE.md](ARCHITECTURE.md) for the full architecture document. + +--- + +## Development + +```bash +git clone https://github.com/pi-docket/bmad-method-vscode.git +cd bmad-method-vscode +npm install +npm run build +``` + +Press `F5` in VS Code to launch the Extension Development Host. + +### Scripts + +| Script | Description | +| ---------------------- | --------------------------------- | +| `npm run build` | Clean build to `dist/` | +| `npm run compile` | TypeScript compile only | +| `npm run watch` | Compile in watch mode | +| `npm run lint` | Type-check without emitting | +| `npm run vsce:package` | Build VSIX package to `releases/` | + +--- + +## Publishing & Versioning + +**Note:** The VS Code extension (VSIX) is not published to the Marketplace. Users must build it from source. The CLI tool may be available via npm for convenience. + +This project follows [Semantic Versioning](https://semver.org/): + +| Version Bump | When | +| ----------------------- | -------------------------------- | +| `patch` (0.2.0 → 0.2.1) | Bug fixes, documentation updates | +| `minor` (0.2.0 → 0.3.0) | New features, new slash commands | +| `major` (0.x → 1.0.0) | Breaking changes, API changes | + +### Release Process + +Version tags are created in git to mark releases: + +```bash +# Update version in package.json +npm version patch # or minor, major + +# Tag and push +git push origin main --follow-tags +``` + +Users can then clone the repository at a specific tag and build the VSIX: + +```bash +git clone --branch v0.2.5 https://github.com/pi-docket/bmad-method-vscode.git +cd bmad-method-vscode/bmad-copilot +npm install +npm run vsce:package +code --install-extension releases/bmad-copilot-adapter-0.2.5.vsix +``` + +--- + +## Folder Structure + +### Source (development) + +``` +bmad-copilot/ +├── bin/bmad-copilot-adapter.js CLI entry point +├── src/ +│ ├── extension.ts VS Code entry point +│ ├── chatBridge.ts Prompt executor (core) +│ ├── cliBridge.ts CLI process spawner +│ ├── commandRegistry.ts CSV scanning, command index +│ ├── types.ts Shared interfaces +│ ├── promptMirror.ts @deprecated +│ ├── bmadRuntime.ts @deprecated +│ └── cli/ +│ ├── index.ts CLI router +│ ├── bootstrap.ts Bootstrap command +│ ├── update.ts Update command +│ └── status.ts Status command +├── package.json +├── tsconfig.json +├── README.md +├── ARCHITECTURE.md +├── CHANGELOG.md +├── TESTING.md +└── LICENSE +``` + +### Published (npm / VSIX) + +``` +bmad-copilot-adapter/ +├── bin/ +│ └── bmad-copilot-adapter.js CLI entry +├── dist/ +│ ├── extension.js +│ ├── chatBridge.js +│ ├── cliBridge.js +│ ├── commandRegistry.js +│ ├── types.js +│ └── cli/ +│ ├── index.js +│ ├── bootstrap.js +│ ├── update.js +│ └── status.js +├── package.json +├── README.md +├── ARCHITECTURE.md +└── LICENSE +``` + +--- + +## Safety & Disclaimer + +- This package **does not modify** any files in `_bmad/`, `.github/prompts/`, or `.github/agents/`. +- It **does not bundle** any part of `bmad-method`. +- It **only reads** official prompt files and forwards them to the Copilot LLM. +- Users **must install** `bmad-method` separately — it is a peer dependency. +- No internal workflow logic, config merging, or CLI execution is replicated. +- No mirroring, conversion, or prompt transformation of any kind. + +--- + +## Compatibility + +| Requirement | Minimum Version | +| ------------------- | ----------------------------------- | +| VS Code | ≥ 1.93.0 | +| Node.js | ≥ 18.0.0 | +| BMAD-METHOD | ≥ 6.0.0-Beta.1 | +| GitHub Copilot Chat | Required (extension must be active) | + +**Platform support:** Windows, macOS, Linux. + +--- + +## License + +MIT — see [LICENSE](LICENSE) for details. + +This package is an independent adapter. It does not include, fork, or modify any +part of the upstream [BMAD-METHOD](https://github.com/pi-docket/bmad-method-vscode) +codebase. Refer to the BMAD-METHOD repository for its own license terms. diff --git a/bmad-copilot/TESTING.md b/bmad-copilot/TESTING.md new file mode 100644 index 00000000..61653f5f --- /dev/null +++ b/bmad-copilot/TESTING.md @@ -0,0 +1,306 @@ +# BMAD Copilot Adapter — Testing Guide + +> Minimum viable test flow: from zero to executing a BMAD command in Copilot Chat. + +--- + +## Prerequisites + +- **Node.js v20+** with npm +- **VS Code** ≥ 1.93.0 +- **GitHub Copilot Chat** extension installed and signed in + +--- + +## Step 0: Build the Extension + +```bash +cd bmad-copilot +npm install +npm run compile +``` + +Verify: no TypeScript errors. The `dist/` directory should contain compiled `.js` files. + +--- + +## Step 1: Launch Extension Development Host + +1. Open the `bmad-copilot` folder in VS Code. +2. Press **F5** (or Run → Start Debugging). +3. Select **"Run Extension"** if prompted. +4. A new VS Code window (Extension Development Host) opens. + +**Troubleshoot:** +- If F5 shows "select debugger" → ensure `.vscode/launch.json` exists with `type: extensionHost`. +- If compile errors → run `npm run compile` and check Output panel. + +--- + +## Step 2: Open a BMAD Test Project + +In the Extension Development Host window: + +1. Open a folder where you want to test (or create a new empty folder). +2. Open the integrated terminal. + +--- + +## Step 3: Install BMAD-METHOD + +In the test project terminal: + +**Option A — claude-code tool (tests prompt mirror):** + +```powershell +# Windows PowerShell +npx bmad-method install --modules bmm --tools claude-code --yes +``` + +```bash +# macOS / Linux +npx bmad-method install --modules bmm --tools claude-code --yes +``` + +This creates: +- `_bmad/` — BMAD core + module files +- `_bmad/_config/` — CSV manifests +- `_bmad/ide/claude-code/prompts/*.prompt.md` — claude-code prompt files +- `_bmad/ide/claude-code/agents/*.agent.md` — Agent persona files (if any) + +> The adapter will automatically mirror these to `.github/prompts/` and `.github/agents/` on activation. + +**Option B — github-copilot tool (native):** + +```bash +npx bmad-method install --modules bmm --tools github-copilot --yes +``` + +This creates: +- `_bmad/` — BMAD core + module files +- `_bmad/_config/` — CSV manifests +- `.github/prompts/*.prompt.md` — Copilot workflow prompts +- `.github/agents/*.agent.md` — Agent persona files + +**Or from Copilot Chat:** + +``` +@bmad /install +``` + +This opens a terminal with the installer. + +--- + +## Step 4: Test `/status` + +Open Copilot Chat (Ctrl+Shift+I) and type: + +``` +@bmad /status +``` + +**Expected:** A table showing: +- ✅ `_bmad/` directory found +- ✅ GitHub Copilot prompt files found (or mirrored from claude-code) +- ✅ claude-code prompt source (if installed with `--tools claude-code`) +- Installed modules (e.g., `core, bmm`) +- Number of commands discovered +- Number of prompt files mapped + +**If `_bmad/` is missing:** You'll see an installation guide with exact commands. + +--- + +## Step 5: Test `/help` + +``` +@bmad /help +``` + +**Expected:** A table of available commands grouped by phase, with run instructions. + +--- + +## Step 6: Test `/workflows` + +``` +@bmad /workflows +``` + +**Expected:** List of all installed workflows with `/run ` instructions. + +--- + +## Step 7: Test `/agents` + +``` +@bmad /agents +``` + +**Expected:** List of installed agents grouped by module. + +--- + +## Step 8: Test `/run` (Core Test) + +``` +@bmad /run bmad-bmm-create-prd +``` + +**Expected:** +1. Header: `🚀 Executing: bmad-bmm-create-prd → CLI: bmad:bmm:create-prd` +2. The LLM receives the official prompt from `.github/prompts/bmad-bmm-create-prd.prompt.md` + (or falls back to `_bmad/` manifest-based prompt if prompt files don't exist). +3. The LLM follows BMAD workflow instructions and begins creating a PRD. + +**Other commands to test:** +``` +@bmad /run bmad-agent-bmm-pm +@bmad /run bmad-help +@bmad bmad-bmm-create-prd +``` + +--- + +## Step 9: Test Free-text + +``` +@bmad What workflows are available for creating stories? +``` + +**Expected:** BMAD-context-aware response suggesting relevant commands. + +--- + +## Common Failure Points + +| Symptom | Cause | Fix | +|---------|-------|-----| +| "BMAD installation not found" | No `_bmad/` in workspace root | Run `npx bmad-method install --modules bmm --tools claude-code --yes` | +| "GitHub Copilot prompt files not found" | Installed without `--tools github-copilot` or `--tools claude-code` | Re-run installer with either tool flag | +| No commands discovered | `_bmad/_config/` missing CSVs | Reinstall BMAD | +| LLM doesn't follow workflow | Prompt file may reference files LLM can't access | Check that `_bmad/` files exist and are readable | +| F5 doesn't work | Missing `.vscode/launch.json` | Ensure the file exists with `type: extensionHost` | +| Mirror didn't run | `.github/prompts/` already had files | Delete `.github/prompts/` and rescan to re-trigger | + +--- + +## Debugging with Output Panel + +1. In the Extension Development Host, open **Output** panel (Ctrl+Shift+U). +2. Select **"BMAD Copilot"** from the dropdown. +3. You'll see: + - Activation logs + - **Prompt mirror** status (skipped / mirrored N files / failed) + - Scan results (how many commands, which modules) + - Request routing (`command=run prompt="bmad-bmm-create-prd"`) + - Which prompt file was used or if fallback was triggered + - LLM errors (rate limits, model issues) + +Set `"bmad.verbose": true` in Settings for additional detail. + +--- + +## Verifying Prompt File Usage + +When you run `/run `, the output channel will show either: + +``` +[ChatBridge] Using prompt file: C:\project\.github\prompts\bmad-bmm-create-prd.prompt.md +``` + +The prompt body is passed **as-is** to the Copilot LLM — no file inlining, +no `{project-root}` resolution. The LLM reads workspace files through its +own context. + +Or the legacy fallback (when `.github/prompts/` files are missing): + +``` +[ChatBridge] No prompt file for bmad-bmm-create-prd, falling back to BmadRuntime +``` + +> **Note:** The BmadRuntime fallback is deprecated. If you see this message, +> re-run `npx bmad-method install --tools claude-code --yes` to generate +> prompt files that the adapter can mirror. + +--- + +## Step 10: Test Prompt Mirror (claude-code → Copilot) + +This test verifies the auto-mirror feature works correctly. + +### 10a: Clean Slate + +```powershell +# Windows PowerShell — remove any existing Copilot prompt files +Remove-Item -Recurse -Force .github\prompts -ErrorAction SilentlyContinue +Remove-Item -Recurse -Force .github\agents -ErrorAction SilentlyContinue +``` + +```bash +# macOS / Linux +rm -rf .github/prompts .github/agents +``` + +### 10b: Install with claude-code + +```bash +npx bmad-method install --modules bmm --tools claude-code --yes +``` + +### 10c: Trigger Rescan + +In Copilot Chat: +``` +@bmad /status +``` + +Or use Command Palette: `BMAD Copilot: Rescan Commands` + +### 10d: Verify Mirror + +**Expected in Output → "BMAD Copilot":** +``` +Prompt mirror: N prompt(s), M agent(s) mirrored from claude-code → .github/ +Scan complete: X commands, Y prompt files, modules=[core, bmm] +``` + +**Expected in `/status`:** +- ✅ `_bmad/` directory found +- ✅ GitHub Copilot prompt files found +- ✅ claude-code prompt source: Available (auto-mirror) +- Prompt files mapped > 0 + +### 10e: Verify No Overwrite + +1. Create a custom file: `.github/prompts/bmad-bmm-create-prd.prompt.md` +2. Run rescan +3. Verify the custom file was **not** overwritten (mirror skips existing files) + +### 10f: Test /run After Mirror + +``` +@bmad /run bmad-bmm-create-prd +``` + +**Expected:** The LLM receives the mirrored prompt and begins the PRD workflow. + +--- + +## Architecture Summary + +``` +@bmad /install → CLI Bridge → opens VS Code terminal → npx bmad-method install +@bmad /status → Built-in → reads _bmad/ + .github/ state + mirror status → Markdown table +@bmad /run X → Prompt Executor → reads .prompt.md/.agent.md → passes AS-IS to LLM +@bmad /help → Built-in → command listing + optional LLM help +``` + +The adapter reads the **exact same prompt files** that native GitHub Copilot +would use (`.github/prompts/*.prompt.md`) and passes them directly to the +LLM without any transformation, inlining, or file pre-reading. + +If `.github/prompts/` is missing but `_bmad/ide/claude-code/prompts/` exists, +the adapter **automatically mirrors** the files with minimal transformation +(filename sanitisation + frontmatter `command:` → `name:` conversion). diff --git a/bmad-copilot/bin/bmad-copilot-adapter.js b/bmad-copilot/bin/bmad-copilot-adapter.js new file mode 100644 index 00000000..f8f2c79a --- /dev/null +++ b/bmad-copilot/bin/bmad-copilot-adapter.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node +// @ts-check +"use strict"; + +/** + * CLI entry point for bmad-copilot-adapter. + * + * Commands: + * bootstrap — Full setup: check Node, install prompts, install extension + * update — Rescan prompts and rebuild command map + * status — Show installation health + * + * Usage: + * npx bmad-copilot-adapter bootstrap + * npx bmad-copilot-adapter update + * npx bmad-copilot-adapter status + */ + +require("../dist/cli/index.js"); diff --git a/bmad-copilot/package-lock.json b/bmad-copilot/package-lock.json new file mode 100644 index 00000000..8436d253 --- /dev/null +++ b/bmad-copilot/package-lock.json @@ -0,0 +1,1453 @@ +{ + "name": "bmad-copilot-adapter", + "version": "0.2.6", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bmad-copilot-adapter", + "version": "0.2.6", + "license": "MIT", + "devDependencies": { + "@types/node": "^18.19.0", + "@types/vscode": "^1.93.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0", + "vscode": "^1.93.0" + }, + "peerDependencies": { + "bmad-method": ">=6.0.0-Beta.1" + } + }, + "node_modules/@clack/core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0.tgz", + "integrity": "sha512-Orf9Ltr5NeiEuVJS8Rk2XTw3IxNC2Bic3ash7GgYeA8LJ/zmSNpSQ/m5UAhe03lA6KFgklzZ5KTHs4OAMA/SAQ==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.0.0.tgz", + "integrity": "sha512-rWPXg9UaCFqErJVQ+MecOaWsozjaxol4yjnmYcGNipAWzdaWa2x+VJmKfGq7L0APwBohQOYdHC+9RO4qRXej+A==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.0.0", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@kayvan/markdown-tree-parser": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@kayvan/markdown-tree-parser/-/markdown-tree-parser-1.6.1.tgz", + "integrity": "sha512-a5HUxUxQup5fAkL5xqmDBmZBNHZVfBn7FNfAx9SYEaUANuoVHygGAxaaHdkHDTCerntNKB2UzNS4dGIYbfPiiQ==", + "license": "MIT", + "dependencies": { + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5", + "unist-util-find": "^3.0.0", + "unist-util-select": "^5.1.0", + "unist-util-visit": "^5.0.0" + }, + "bin": { + "md-tree": "bin/md-tree.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ksylvan" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.109.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz", + "integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bmad-method": { + "version": "6.0.0-Beta.8", + "resolved": "https://registry.npmjs.org/bmad-method/-/bmad-method-6.0.0-Beta.8.tgz", + "integrity": "sha512-vsESQtZr0BJ68k3AxAiktPiGNxA/ULPpssC9hgZJlVHWqH4b6e+xOKt4/yEnGXyWnTrcwXNGXu2tUrET2T2PcA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@clack/core": "^1.0.0", + "@clack/prompts": "^1.0.0", + "@kayvan/markdown-tree-parser": "^1.6.1", + "chalk": "^4.1.2", + "commander": "^14.0.0", + "csv-parse": "^6.1.0", + "fs-extra": "^11.3.0", + "glob": "^11.0.3", + "ignore": "^7.0.5", + "js-yaml": "^4.1.0", + "picocolors": "^1.1.1", + "semver": "^7.6.3", + "xml2js": "^0.6.2", + "yaml": "^2.7.0" + }, + "bin": { + "bmad": "tools/bmad-npx-wrapper.js", + "bmad-method": "tools/bmad-npx-wrapper.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-selector-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", + "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/csv-parse": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-6.1.0.tgz", + "integrity": "sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lodash.iteratee": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.iteratee/-/lodash.iteratee-4.7.0.tgz", + "integrity": "sha512-yv3cSQZmfpbIKo4Yo45B1taEvxjNvcpF1CEOc0Y6dEyvhPIfEJE3twDwPgWTPQubcSgXyBwBKG6wpQvWMDOf6Q==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find/-/unist-util-find-3.0.0.tgz", + "integrity": "sha512-T7ZqS7immLjYyC4FCp2hDo3ksZ1v+qcbb+e5+iWxc2jONgHOLXPCpms1L8VV4hVxCXgWTxmBHDztuEZFVwC+Gg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "lodash.iteratee": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-select/-/unist-util-select-5.1.0.tgz", + "integrity": "sha512-4A5mfokSHG/rNQ4g7gSbdEs+H586xyd24sdJqF1IWamqrLHvYb+DH48fzxowyOhOfK7YSqX+XlCojAyuuyyT2A==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.1.0", + "nth-check": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/bmad-copilot/package.json b/bmad-copilot/package.json new file mode 100644 index 00000000..08555e7e --- /dev/null +++ b/bmad-copilot/package.json @@ -0,0 +1,149 @@ +{ + "name": "bmad-copilot-adapter", + "displayName": "BMAD Method — Copilot Chat Adapter", + "description": "Official prompt executor that enables BMAD-METHOD workflows to run natively inside GitHub Copilot Chat. VS Code extension (VSIX) must be built from source — not available on Marketplace.", + "version": "0.2.6", + "publisher": "evil9369", + "license": "MIT", + "author": { + "name": "evil0119" + }, + "homepage": "https://github.com/pi-docket/bmad-method-vscode#readme", + "bugs": { + "url": "https://github.com/pi-docket/bmad-method-vscode/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/pi-docket/bmad-method-vscode.git" + }, + "engines": { + "vscode": "^1.93.0", + "node": ">=18.0.0" + }, + "categories": [ + "AI", + "Chat" + ], + "keywords": [ + "bmad", + "bmad-method", + "copilot", + "chat-participant", + "vscode", + "agile", + "ai-agents", + "workflow", + "prompt-executor" + ], + "activationEvents": [ + "onChatParticipant:bmad" + ], + "main": "./dist/extension.js", + "bin": { + "bmad-copilot-adapter": "bin/bmad-copilot-adapter.js" + }, + "peerDependencies": { + "bmad-method": ">=6.0.0-Beta.1" + }, + "peerDependenciesMeta": { + "bmad-method": { + "optional": true + } + }, + "devDependencies": { + "@types/node": "^18.19.0", + "@types/vscode": "^1.93.0", + "typescript": "^5.3.0" + }, + "scripts": { + "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"", + "compile": "tsc -p ./", + "build": "npm run clean && npm run compile", + "watch": "tsc -watch -p ./", + "lint": "tsc --noEmit", + "test": "npm run lint", + "prevsce": "npm run build", + "vsce:package": "npx @vscode/vsce package --out releases/", + "vsce:publish": "npx @vscode/vsce publish", + "prepublishOnly": "npm run build" + }, + "contributes": { + "chatParticipants": [ + { + "id": "bmad", + "fullName": "BMAD Method", + "name": "bmad", + "description": "Run BMAD-METHOD workflows, agents, and tasks inside Copilot Chat. Type /bmad then a slash-command to get started.", + "isSticky": true, + "commands": [ + { + "name": "help", + "description": "Show available BMAD workflows, agents, and next-step guidance." + }, + { + "name": "run", + "description": "Execute any BMAD command by name. Example: /run bmad-bmm-create-prd" + }, + { + "name": "agents", + "description": "List all installed BMAD agents with persona details." + }, + { + "name": "workflows", + "description": "List all installed BMAD workflows grouped by module and phase." + }, + { + "name": "tasks", + "description": "List all installed BMAD tasks and tools." + }, + { + "name": "status", + "description": "Show BMAD installation status, detected modules, and configuration." + }, + { + "name": "install", + "description": "Run the BMAD installer in a terminal. Sets up _bmad/ and Copilot prompt files." + }, + { + "name": "update", + "description": "Rescan prompts, rebuild command map, and refresh Copilot integration." + } + ] + } + ], + "commands": [ + { + "command": "bmad-copilot.rescan", + "title": "BMAD Copilot: Rescan Commands" + }, + { + "command": "bmad-copilot.update", + "title": "BMAD Copilot: Update (Invalidate + Rescan)" + }, + { + "command": "bmad-copilot.install", + "title": "BMAD Copilot: Run Installer" + } + ], + "configuration": { + "title": "BMAD Copilot Adapter", + "properties": { + "bmad.bmadDir": { + "type": "string", + "default": "", + "description": "Override path to _bmad installation directory. Leave empty for auto-detection from workspace root." + }, + "bmad.autoScan": { + "type": "boolean", + "default": true, + "description": "Automatically scan for BMAD installation on activation." + }, + "bmad.verbose": { + "type": "boolean", + "default": false, + "description": "Enable verbose logging to the BMAD output channel." + } + } + } + } +} diff --git a/bmad-copilot/scripts/release.ps1 b/bmad-copilot/scripts/release.ps1 new file mode 100644 index 00000000..32974f4f --- /dev/null +++ b/bmad-copilot/scripts/release.ps1 @@ -0,0 +1,50 @@ +# release.ps1 — BMAD Copilot Adapter release helper (Windows) +# Usage: .\scripts\release.ps1 [-Bump patch|minor|major] +param( + [ValidateSet("patch", "minor", "major")] + [string]$Bump = "patch" +) + +$ErrorActionPreference = "Stop" + +Write-Host "=== BMAD Copilot Adapter Release ===" -ForegroundColor Cyan +Write-Host "Bump type: $Bump" +Write-Host "" + +# Pre-flight checks +Write-Host "[1/6] Pre-flight checks..." -ForegroundColor Yellow +$status = git status --porcelain +if ($status) { + Write-Host "ERROR: Working tree has uncommitted changes. Commit or stash first." -ForegroundColor Red + exit 1 +} + +Write-Host "[2/6] Clean build..." -ForegroundColor Yellow +npm run build +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +Write-Host "[3/6] Type-check..." -ForegroundColor Yellow +npm run lint +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +Write-Host "[4/6] Bump version ($Bump)..." -ForegroundColor Yellow +npm version $Bump -m "release: %s" +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +Write-Host "[5/6] Push tag to remote..." -ForegroundColor Yellow +git push --follow-tags +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +Write-Host "[6/6] Publish to npm..." -ForegroundColor Yellow +npm publish --access public +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +Write-Host "" +Write-Host "=== Release complete ===" -ForegroundColor Green +Write-Host "npm: https://www.npmjs.com/package/bmad-copilot-adapter" +Write-Host "" +Write-Host "To publish VSIX to VS Code Marketplace:" +Write-Host " npx @vscode/vsce publish" +Write-Host "" +Write-Host "To build VSIX locally:" +Write-Host " npm run vsce:package" diff --git a/bmad-copilot/scripts/release.sh b/bmad-copilot/scripts/release.sh new file mode 100755 index 00000000..36ae1f10 --- /dev/null +++ b/bmad-copilot/scripts/release.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# release.sh — BMAD Copilot Adapter release helper +# Usage: ./scripts/release.sh [patch|minor|major] +set -euo pipefail + +BUMP="${1:-patch}" + +echo "=== BMAD Copilot Adapter Release ===" +echo "Bump type: $BUMP" +echo "" + +# Pre-flight checks +echo "[1/6] Pre-flight checks..." +if ! git diff --quiet; then + echo "ERROR: Working tree has uncommitted changes. Commit or stash first." + exit 1 +fi + +if ! git diff --cached --quiet; then + echo "ERROR: Staged changes detected. Commit first." + exit 1 +fi + +echo "[2/6] Clean build..." +npm run build + +echo "[3/6] Type-check..." +npm run lint + +echo "[4/6] Bump version ($BUMP)..." +npm version "$BUMP" -m "release: %s" + +echo "[5/6] Push tag to remote..." +git push --follow-tags + +echo "[6/6] Publish to npm..." +npm publish --access public + +echo "" +echo "=== Release complete ===" +echo "npm: https://www.npmjs.com/package/bmad-copilot-adapter" +echo "" +echo "To publish VSIX to VS Code Marketplace:" +echo " npx @vscode/vsce publish" +echo "" +echo "To build VSIX locally:" +echo " npm run vsce:package" diff --git a/bmad-copilot/src/bmadRuntime.ts b/bmad-copilot/src/bmadRuntime.ts new file mode 100644 index 00000000..23ea3a74 --- /dev/null +++ b/bmad-copilot/src/bmadRuntime.ts @@ -0,0 +1,655 @@ +/** + * @fileoverview BMAD Runtime — Legacy prompt builder (DEPRECATED). + * + * @deprecated This module is a **fallback only**. It is used when the + * workspace was installed without `--tools github-copilot`, meaning + * `.github/prompts/*.prompt.md` and `.github/agents/*.agent.md` files + * do not exist. + * + * The preferred path is the **Prompt File Executor** in + * {@link ChatBridge}, which reads official prompt files and passes + * them directly to the Copilot LLM without any transformation. + * + * When `.github/prompts/` files are present, this module is never + * invoked. + * + * **Original Responsibilities** (retained for fallback) + * 1. Read the target workflow / agent / task file from disk. + * 2. Assemble a prompt using patterns derived from the official BMAD + * GitHub Copilot installer (`github-copilot.js`). + * 3. Return the composed prompt string. + * + * @module bmadRuntime + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import type { BmadCommand, RegistryState } from './types.js'; + +/* ------------------------------------------------------------------ */ +/* Constants */ +/* ------------------------------------------------------------------ */ + +/** Placeholder used in BMAD files that resolves to workspace root. */ +const PROJECT_ROOT_TOKEN = '{project-root}'; + +/** + * Default BMAD folder name. The installer always creates `_bmad/`. + */ +const BMAD_FOLDER_NAME = '_bmad'; + +/* ------------------------------------------------------------------ */ +/* BmadRuntime */ +/* ------------------------------------------------------------------ */ + +/** + * Composes prompts that mirror the official BMAD Copilot prompt + * templates without modifying any BMAD files. + * + * Each prompt pattern corresponds to a pattern from the official + * `github-copilot.js` installer: + * + * | Pattern | Official pattern | When used | + * |------------------|------------------|------------------------------------| + * | md-workflow | A | `.md` workflow files | + * | yaml-workflow | B | `.yaml` workflows via engine | + * | task | A-variant | `.xml` / `.md` task files | + * | agent-only | C | Tech-writer style agent commands | + * | agent-activator | D | Load agent persona directly | + * + * @deprecated Use the Prompt File Executor path in {@link ChatBridge} + * instead. This class is only retained as a fallback for workspaces + * installed without `--tools github-copilot`. + */ +export class BmadRuntime { + /** The workspace root for `{project-root}` resolution. */ + private readonly workspaceRoot: string; + /** Reference to the registry state. */ + private readonly registryState: RegistryState; + + /** + * Create a BmadRuntime bound to a workspace and registry state. + * + * @param workspaceRoot - Absolute path to the VS Code workspace root. + * @param registryState - Fully populated {@link RegistryState} from the registry scan. + */ + constructor(workspaceRoot: string, registryState: RegistryState) { + this.workspaceRoot = workspaceRoot; + this.registryState = registryState; + } + + /* -------------------------------------------------------------- */ + /* Public API */ + /* -------------------------------------------------------------- */ + + /** + * Build the complete prompt for a given {@link BmadCommand}. + * + * The returned string is ready to be prepended to the LLM message + * array as a system/user prompt. + * + * @param command - The resolved BMAD command. + * @param userInput - The user's free-text from the chat input. + * @returns The assembled prompt string. + */ + buildPrompt(command: BmadCommand, userInput: string): string { + switch (command.pattern) { + case 'md-workflow': + return this.buildMdWorkflowPrompt(command, userInput); + case 'yaml-workflow': + return this.buildYamlWorkflowPrompt(command, userInput); + case 'task': + return this.buildTaskPrompt(command, userInput); + case 'agent-activator': + return this.buildAgentActivatorPrompt(command, userInput); + case 'agent-only': + return this.buildAgentOnlyPrompt(command, userInput); + default: + return this.buildFallbackPrompt(command, userInput); + } + } + + /** + * Build the contextual help prompt that mimics `/bmad-help`. + * + * Reads the BMAD help task and injects the bmad-help.csv catalog + * as context, matching the official help task execution flow. + * + * @param userInput - Optional topic or question from the user. + * @returns Assembled help prompt. + */ + buildHelpPrompt(userInput: string): string { + const bmadDir = this.registryState.bmadDir; + const helpTaskPath = path.join(bmadDir, 'core', 'tasks', 'help.md'); + const helpCsvPath = path.join(bmadDir, '_config', 'bmad-help.csv'); + + let helpTask = ''; + if (fs.existsSync(helpTaskPath)) { + helpTask = this.readAndResolve(helpTaskPath); + } + + let helpCsv = ''; + if (fs.existsSync(helpCsvPath)) { + helpCsv = fs.readFileSync(helpCsvPath, 'utf8'); + } + + const configPrompt = this.buildConfigLoadLine(); + const commandListing = this.buildCommandListing(); + + return [ + `# BMAD Help — Runtime Context`, + '', + configPrompt, + '', + '## Help Task Instructions', + '', + helpTask || '*Help task file not found. Providing catalog-only guidance.*', + '', + '## BMAD Help Catalog (bmad-help.csv)', + '', + '```csv', + helpCsv || 'No bmad-help.csv found.', + '```', + '', + '## Available Slash Commands', + '', + commandListing, + '', + '## User Query', + '', + userInput || 'Show me what workflows and next steps are available.', + ].join('\n'); + } + + /** + * Build a prompt listing all installed agents. + * + * @returns Formatted agent listing prompt. + */ + buildAgentListingPrompt(): string { + const { agents } = this.registryState; + if (agents.length === 0) { + return 'No BMAD agents are currently installed.'; + } + + const lines = ['# Installed BMAD Agents\n']; + const byModule = new Map(); + for (const a of agents) { + const mod = a.module || 'core'; + if (!byModule.has(mod)) { + byModule.set(mod, []); + } + byModule.get(mod)!.push(a); + } + + for (const [mod, group] of byModule) { + lines.push(`## Module: ${mod.toUpperCase()}\n`); + lines.push('| Agent | Persona | Title | Capabilities |'); + lines.push('|---|---|---|---|'); + for (const a of group) { + const icon = a.icon || ''; + lines.push( + `| ${a.name} | ${a.displayName} | ${icon} ${a.title} | ${a.capabilities} |`, + ); + } + lines.push(''); + } + + lines.push( + '\n> To activate an agent, use `/bmad-agent--` ' + + '(e.g. `/bmad-agent-bmm-pm`).', + ); + return lines.join('\n'); + } + + /** + * Build a prompt listing all installed workflows. + * + * @returns Formatted workflow listing prompt. + */ + buildWorkflowListingPrompt(): string { + const { workflows, helpEntries } = this.registryState; + if (workflows.length === 0 && helpEntries.length === 0) { + return 'No BMAD workflows are currently installed.'; + } + + const lines = ['# Installed BMAD Workflows\n']; + + // Use help entries for richer data when available + if (helpEntries.length > 0) { + const byPhase = new Map(); + for (const h of helpEntries) { + const phase = h.phase || 'anytime'; + if (!byPhase.has(phase)) { + byPhase.set(phase, []); + } + byPhase.get(phase)!.push(h); + } + + for (const [phase, group] of byPhase) { + lines.push(`## Phase: ${phase}\n`); + lines.push('| Command | Name | Agent | Required | Description |'); + lines.push('|---|---|---|---|---|'); + for (const h of group) { + const cmd = h.command + ? `\`/${h.command}\`` + : `*${h.code || 'n/a'}*`; + const req = h.required === 'true' ? '✅' : ''; + lines.push( + `| ${cmd} | ${h.name} | ${h['agent-title'] || h['agent-name'] || ''} | ${req} | ${h.description} |`, + ); + } + lines.push(''); + } + } else { + // Fallback to workflow-manifest only + lines.push('| Name | Module | Description |'); + lines.push('|---|---|---|'); + for (const w of workflows) { + lines.push(`| ${w.name} | ${w.module} | ${w.description} |`); + } + } + + return lines.join('\n'); + } + + /** + * Build a prompt listing all installed tasks and tools. + * + * @returns Formatted task/tool listing prompt. + */ + buildTaskListingPrompt(): string { + const { tasks, tools } = this.registryState; + if (tasks.length === 0 && tools.length === 0) { + return 'No BMAD tasks or tools are currently installed.'; + } + + const lines = ['# Installed BMAD Tasks & Tools\n']; + + if (tasks.length > 0) { + lines.push('## Tasks\n'); + lines.push('| Name | Module | Description |'); + lines.push('|---|---|---|'); + for (const t of tasks) { + lines.push(`| ${t.name} | ${t.module} | ${t.description} |`); + } + lines.push(''); + } + + if (tools.length > 0) { + lines.push('## Tools\n'); + lines.push('| Name | Module | Description |'); + lines.push('|---|---|---|'); + for (const t of tools) { + lines.push(`| ${t.name} | ${t.module} | ${t.description} |`); + } + } + + return lines.join('\n'); + } + + /** + * Build a status summary of the current BMAD installation. + * + * ⚠️ **SECURITY WARNING**: This function includes config.yaml excerpts + * (up to 500 chars per module) in the status output sent to the LLM. + * Ensure config files do not contain sensitive information. + * + * @returns Formatted status prompt. + */ + buildStatusPrompt(): string { + const s = this.registryState; + const lines = [ + '# BMAD Installation Status\n', + `- **BMAD Directory**: \`${s.bmadDir}\``, + `- **Modules installed**: ${s.modules.join(', ') || 'none'}`, + `- **Agents**: ${s.agents.length}`, + `- **Workflows**: ${s.workflows.length}`, + `- **Tasks**: ${s.tasks.length}`, + `- **Tools**: ${s.tools.length}`, + `- **Total commands**: ${s.commands.size}`, + `- **Last scan**: ${s.lastScan}`, + ]; + + // Module config preview + for (const mod of s.modules) { + const configPath = path.join(s.bmadDir, mod, 'config.yaml'); + if (fs.existsSync(configPath)) { + const excerpt = fs.readFileSync(configPath, 'utf8').slice(0, 500); + lines.push(`\n### ${mod}/config.yaml (excerpt)\n`); + lines.push('```yaml'); + lines.push(excerpt); + lines.push('```'); + } + } + + return lines.join('\n'); + } + + /* -------------------------------------------------------------- */ + /* Pattern-specific prompt builders */ + /* -------------------------------------------------------------- */ + + /** + * Pattern A — MD workflows: load config, then follow the `.md` file + * directly. Mirrors the official GitHub Copilot installer Pattern A. + * + * @param command - Resolved command. + * @param userInput - User's free text. + * @returns Composed prompt. + */ + private buildMdWorkflowPrompt(command: BmadCommand, userInput: string): string { + const configLine = this.buildConfigLoadLine(); + const workflowContent = this.tryReadWorkflowFile(command.filePath); + + return [ + `# BMAD Workflow: ${command.description}`, + `> Command: \`/${command.slashName}\` → CLI: \`${command.cliSyntax}\``, + '', + configLine, + '', + `## Instructions`, + '', + `Load and follow the workflow at ${this.resolveProjectRoot(command.filePath)}`, + '', + workflowContent + ? `## Workflow Content\n\n${workflowContent}` + : `*Could not read workflow file: ${command.filePath}*`, + '', + userInput ? `## User Input\n\n${userInput}` : '', + ].join('\n'); + } + + /** + * Pattern B — YAML workflows: load the workflow.xml engine, then + * pass the `.yaml` config to it. Mirrors the official Pattern B. + * + * @param command - Resolved command. + * @param userInput - User's free text. + * @returns Composed prompt. + */ + private buildYamlWorkflowPrompt(command: BmadCommand, userInput: string): string { + const configLine = this.buildConfigLoadLine(); + const bmadDir = this.registryState.bmadDir; + const enginePath = path.join(bmadDir, 'core', 'tasks', 'workflow.xml'); + let engineContent = ''; + if (fs.existsSync(enginePath)) { + engineContent = this.readAndResolve(enginePath); + } + const workflowConfig = this.tryReadWorkflowFile(command.filePath); + + return [ + `# BMAD Workflow (YAML Engine): ${command.description}`, + `> Command: \`/${command.slashName}\` → CLI: \`${command.cliSyntax}\``, + '', + configLine, + '', + '## Step 1: Workflow Engine', + '', + `Load the workflow engine at ${this.resolveProjectRoot('_bmad/core/tasks/workflow.xml')}`, + '', + engineContent + ? `### workflow.xml\n\n\`\`\`xml\n${engineContent}\n\`\`\`` + : '*Could not read workflow.xml engine.*', + '', + '## Step 2: Workflow Configuration', + '', + `Load and execute the workflow configuration at ${this.resolveProjectRoot(command.filePath)} using the engine from step 1.`, + '', + workflowConfig + ? `### Workflow YAML\n\n\`\`\`yaml\n${workflowConfig}\n\`\`\`` + : `*Could not read workflow config: ${command.filePath}*`, + '', + userInput ? `## User Input\n\n${userInput}` : '', + ].join('\n'); + } + + /** + * Pattern A-variant — Tasks (XML/MD): load and execute directly. + * Mirrors the task handling from the official installer. + * + * @param command - Resolved command. + * @param userInput - User's free text. + * @returns Composed prompt. + */ + private buildTaskPrompt(command: BmadCommand, userInput: string): string { + const configLine = this.buildConfigLoadLine(); + const taskContent = this.tryReadWorkflowFile(command.filePath); + + return [ + `# BMAD Task: ${command.description}`, + `> Command: \`/${command.slashName}\` → CLI: \`${command.cliSyntax}\``, + '', + configLine, + '', + `## Instructions`, + '', + `Load and execute the task at ${this.resolveProjectRoot(command.filePath)}`, + '', + taskContent + ? `## Task Content\n\n${taskContent}` + : `*Could not read task file: ${command.filePath}*`, + '', + userInput ? `## User Input\n\n${userInput}` : '', + ].join('\n'); + } + + /** + * Pattern D — Agent activator: load config, then activate the agent + * persona. Mirrors the official Pattern D. + * + * @param command - Resolved command. + * @param userInput - User's free text. + * @returns Composed prompt. + */ + private buildAgentActivatorPrompt(command: BmadCommand, userInput: string): string { + const configLine = this.buildConfigLoadLine(); + const agentContent = this.tryReadWorkflowFile(command.filePath); + + return [ + `# BMAD Agent Activation: ${command.agentTitle || command.agentName}`, + `> Command: \`/${command.slashName}\` → CLI: \`${command.cliSyntax}\``, + '', + configLine, + '', + '## Instructions', + '', + `Load the full agent file from ${this.resolveProjectRoot(command.filePath)} and activate the persona.`, + '', + agentContent + ? `## Agent Definition\n\n${agentContent}` + : `*Could not read agent file: ${command.filePath}*`, + '', + userInput + ? `## User Request\n\n${userInput}` + : `Greet the user and present your capabilities.`, + ].join('\n'); + } + + /** + * Pattern C — Agent-only commands: load agent, invoke by code. + * Used for tech-writer style commands with no standalone workflow file. + * + * @param command - Resolved command. + * @param userInput - User's free text. + * @returns Composed prompt. + */ + private buildAgentOnlyPrompt(command: BmadCommand, userInput: string): string { + const configLine = this.buildConfigLoadLine(); + + return [ + `# BMAD Agent Command: ${command.description}`, + `> Command: \`/${command.slashName}\` → CLI: \`${command.cliSyntax}\``, + '', + configLine, + '', + '## Instructions', + '', + `1. Activate agent: ${command.agentName}`, + `2. Execute the "${command.description}" action.`, + '', + userInput ? `## User Input\n\n${userInput}` : '', + ].join('\n'); + } + + /** + * Fallback prompt for commands whose pattern could not be determined. + * + * @param command - Resolved command. + * @param userInput - User's free text. + * @returns Composed prompt. + */ + private buildFallbackPrompt(command: BmadCommand, userInput: string): string { + const content = this.tryReadWorkflowFile(command.filePath); + + return [ + `# BMAD: ${command.description}`, + `> Command: \`/${command.slashName}\``, + '', + content || `*No file content available for: ${command.filePath}*`, + '', + userInput ? `## User Input\n\n${userInput}` : '', + ].join('\n'); + } + + /* -------------------------------------------------------------- */ + /* Utility methods */ + /* -------------------------------------------------------------- */ + + /** + * Build the standard config-load line that prefixes most prompts. + * Mirrors: "Load {project-root}/_bmad/bmm/config.yaml and store ALL + * fields as session variables". + * + * Checks multiple config paths in priority order. + * + * ⚠️ **SECURITY WARNING**: This function reads the entire config.yaml + * and includes it in the LLM prompt. **Never store sensitive data** + * (API keys, passwords, secrets) in BMAD config files. Use environment + * variables or secure credential stores instead. + * + * @returns Config-load instruction string. + */ + private buildConfigLoadLine(): string { + const bmadDir = this.registryState.bmadDir; + const candidates = ['bmm/config.yaml', 'core/config.yaml']; + + for (const rel of candidates) { + const abs = path.join(bmadDir, rel); + if (fs.existsSync(abs)) { + const content = fs.readFileSync(abs, 'utf8'); + return [ + `## Session Configuration`, + '', + `Load the following configuration and store ALL fields as session variables:`, + '', + '```yaml', + content, + '```', + ].join('\n'); + } + } + + return '> *No module config.yaml found. Session variables unavailable.*'; + } + + /** + * Attempt to read a workflow/agent/task file from the `_bmad` + * directory, resolving `{project-root}` tokens. + * + * @param relativePath - Path relative to workspace root (may start with `_bmad/`). + * @returns File content with tokens resolved, or empty string on failure. + */ + private tryReadWorkflowFile(relativePath: string): string { + if (!relativePath) { + return ''; + } + + // Normalise: strip leading {project-root}/ if present + let cleanPath = relativePath.replace(/^\{project-root\}\//, ''); + // Also strip leading _bmad/ to avoid double-pathing + // Check for _bmad/ or _bmad\ (not just _bmad prefix like _bmad_v2) + if (!cleanPath.startsWith(BMAD_FOLDER_NAME + '/') && !cleanPath.startsWith(BMAD_FOLDER_NAME + '\\')) { + cleanPath = path.join(BMAD_FOLDER_NAME, cleanPath); + } + + const absPath = path.join(this.workspaceRoot, cleanPath); + + if (!fs.existsSync(absPath)) { + // Try the raw path as fallback + const rawAbs = path.join(this.workspaceRoot, relativePath); + if (fs.existsSync(rawAbs)) { + return this.readAndResolve(rawAbs); + } + return ''; + } + + return this.readAndResolve(absPath); + } + + /** + * Read a file and replace `{project-root}` with the actual workspace + * root path. + * + * @param absPath - Absolute path to the file. + * @returns File content with tokens resolved. + */ + private readAndResolve(absPath: string): string { + const content = fs.readFileSync(absPath, 'utf8'); + return content.replaceAll(PROJECT_ROOT_TOKEN, this.workspaceRoot); + } + + /** + * Prepend `{project-root}/` to a relative path for display purposes. + * + * @param relativePath - Relative file path. + * @returns Path with `{project-root}` prefix. + */ + private resolveProjectRoot(relativePath: string): string { + if (relativePath.startsWith(PROJECT_ROOT_TOKEN)) { + return relativePath; + } + return `${PROJECT_ROOT_TOKEN}/${relativePath}`; + } + + /** + * Build a compact listing of all registered slash commands, + * grouped by category. + * + * @returns Formatted Markdown command listing. + */ + private buildCommandListing(): string { + const { commands } = this.registryState; + const byCategory = new Map(); + + for (const cmd of commands.values()) { + if (!byCategory.has(cmd.category)) { + byCategory.set(cmd.category, []); + } + byCategory.get(cmd.category)!.push(cmd); + } + + const lines: string[] = []; + const order: Array = [ + 'workflow', + 'agent', + 'task', + 'tool', + 'core', + ]; + + for (const cat of order) { + const group = byCategory.get(cat); + if (!group || group.length === 0) { + continue; + } + lines.push(`### ${cat.charAt(0).toUpperCase() + cat.slice(1)}s\n`); + for (const cmd of group) { + lines.push(`- \`/${cmd.slashName}\` — ${cmd.description}`); + } + lines.push(''); + } + + return lines.join('\n'); + } +} diff --git a/bmad-copilot/src/chatBridge.ts b/bmad-copilot/src/chatBridge.ts new file mode 100644 index 00000000..244c1e63 --- /dev/null +++ b/bmad-copilot/src/chatBridge.ts @@ -0,0 +1,847 @@ +/** + * @fileoverview Chat Bridge — Official BMAD GitHub Copilot Prompt Executor + * + * This module is the core of the BMAD Copilot Adapter. It reads + * official `.prompt.md` / `.agent.md` files generated by the BMAD + * installer and passes them **as-is** to the Copilot LLM. + * + * **Design Philosophy** + * + * - The adapter does NOT pre-read, inline, or transform file + * references inside prompt files. The official prompts contain + * `{project-root}` tokens that the LLM resolves through its + * own workspace context. + * - For `install` and `status`, the real BMAD CLI is spawned via + * {@link CliBridge}. + * - If `.github/prompts/` files are missing (i.e. BMAD was installed + * without `--tools github-copilot`), a clear error message guides + * the user to install prompts via the official CLI. + * + * @module chatBridge + */ + +import * as vscode from 'vscode'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { CommandRegistry } from './commandRegistry.js'; +import { CliBridge } from './cliBridge.js'; +import type { BmadCommand, RegistryState } from './types.js'; + +/* ------------------------------------------------------------------ */ +/* Constants */ +/* ------------------------------------------------------------------ */ + +/** The missing-installation message, modelled after the official CLI. */ +const MISSING_INSTALL_MESSAGE = ` +⚠️ **BMAD installation not found** + +No \`_bmad/\` directory detected in your workspace. + +**Quick setup:** + +\`\`\`bash +npx bmad-method install --tools github-copilot +\`\`\` + +**Prerequisites:** +- Node.js **v20+** and npm +- Run the command above from your **project root** + +**With specific modules:** + +\`\`\`bash +npx bmad-method install --modules bmm --tools github-copilot --yes +\`\`\` + +> Available modules: \`bmm\` (Business Model Master), \`bmb\` (BMad Builder), \`tea\` (Test Architect), \`gds\` (Game Dev Studio), \`cis\` (Creative Intelligence Suite) + +After installation, reload the VS Code window or use \`@bmad /status\`. +`.trim(); + +/** Message when Copilot prompt files are missing but _bmad/ exists. */ +const MISSING_COPILOT_FILES_MESSAGE = ` +⚠️ **GitHub Copilot prompt files not found** + +Your workspace has \`_bmad/\` but no \`.github/prompts/\` or \`.github/agents/\` BMAD files. + +Run the official BMAD installer to generate prompt files: + +\`\`\`bash +npx bmad-method install --tools github-copilot --yes +\`\`\` + +> **Note:** Cloud Code CLI is NOT required. Only the generated prompt files are needed. + +After installation, use \`@bmad /status\` to verify. +`.trim(); + +/* ------------------------------------------------------------------ */ +/* ChatBridge */ +/* ------------------------------------------------------------------ */ + +/** + * Bridges the VS Code Copilot Chat participant API with BMAD. + * + * ### Slash-command routing + * + * | Command | Mode | Behaviour | + * |--------------|--------------------|-------------------------------------------| + * | `/status` | Built-in + CLI | Show installation status | + * | `/install` | CLI Bridge | Open terminal with `npx bmad-method install` | + * | `/help` | Built-in | List commands and help guidance | + * | `/run ` | Prompt Executor | Read prompt file → resolve refs → LLM | + * | `/agents` | Built-in | List installed agents | + * | `/workflows` | Built-in | List installed workflows | + * | `/tasks` | Built-in | List installed tasks and tools | + * | *(free text)*| Fuzzy → Executor | Match to command or BMAD context chat | + */ +export class ChatBridge { + private readonly registry: CommandRegistry; + private readonly cliBridge: CliBridge; + private readonly outputChannel: vscode.OutputChannel; + + /** + * Create a Chat Bridge instance. + * + * @param registry - Command registry for resolving BMAD commands. + * @param cliBridge - CLI bridge for executing bmad-method commands. + * @param outputChannel - VS Code output channel for logging. + */ + constructor( + registry: CommandRegistry, + cliBridge: CliBridge, + outputChannel: vscode.OutputChannel, + ) { + this.registry = registry; + this.cliBridge = cliBridge; + this.outputChannel = outputChannel; + } + + /* -------------------------------------------------------------- */ + /* Main handler */ + /* -------------------------------------------------------------- */ + + /** The {@link vscode.ChatRequestHandler} implementation. */ + handler: vscode.ChatRequestHandler = async ( + request: vscode.ChatRequest, + _context: vscode.ChatContext, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken, + ): Promise => { + this.log(`command=${request.command ?? '(none)'} prompt="${request.prompt}"`); + + try { + const state = this.registry.state; + + // --- Installation guard --- + // Allow /status, /install, and /update to run even when state is null + if (!state && request.command !== 'install' && request.command !== 'status' && request.command !== 'update') { + stream.markdown(MISSING_INSTALL_MESSAGE); + return {}; + } + + switch (request.command) { + case 'install': + return this.handleInstall(stream); + case 'status': + return this.handleStatus(state, stream); + case 'update': + return this.handleUpdate(stream); + case 'help': + return this.handleHelp(state!, request, stream, token); + case 'run': + return this.handleRun(state!, request, stream, token); + case 'agents': + return this.handleAgents(state!, stream); + case 'workflows': + return this.handleWorkflows(state!, stream); + case 'tasks': + return this.handleTasks(state!, stream); + default: + return this.handleFreeText(state!, request, stream, token); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + this.log(`Error: ${msg}`); + stream.markdown(`❌ **Error**: ${msg}`); + return {}; + } + }; + + /* -------------------------------------------------------------- */ + /* /install — CLI Bridge */ + /* -------------------------------------------------------------- */ + + /** + * Handle `/install` — open a VS Code terminal running the real + * BMAD CLI installer. The installer uses `@clack/prompts` for + * interactive terminal UI, so it must run in a real TTY. + * + * @param stream - Chat response stream for sending messages to user. + * @returns Chat result. + */ + private handleInstall(stream: vscode.ChatResponseStream): vscode.ChatResult { + const workspaceRoot = this.getWorkspaceRoot(); + + stream.markdown( + '🚀 **Opening terminal to run BMAD installer…**\n\n' + + 'The installer will guide you through module and tool selection.\n\n' + + '> **Tip:** To install non-interactively, run:\n' + + '> ```bash\n' + + '> npx bmad-method install --modules bmm --tools github-copilot --yes\n' + + '> ```\n\n' + + 'After installation completes, use `@bmad /status` to verify.', + ); + + this.cliBridge.openTerminal( + ['install', '--tools', 'github-copilot'], + workspaceRoot, + ); + + return {}; + } + + /* -------------------------------------------------------------- */ + /* /update — Invalidate and rescan */ + /* -------------------------------------------------------------- */ + + /** + * Handle `/update` — invalidate the cached command registry and + * trigger a full rescan. This is the Copilot Chat counterpart + * of the CLI `npx bmad-copilot-adapter update` command. + * + * @param stream - Chat response stream for sending messages to user. + * @returns Chat result. + */ + private handleUpdate( + stream: vscode.ChatResponseStream, + ): vscode.ChatResult { + stream.markdown( + '🔄 **Updating BMAD command registry…**\n\n' + + 'Invalidating cache and triggering full rescan.\n\n', + ); + + // Fire the VS Code command which performs invalidate + mirror + rescan + vscode.commands.executeCommand('bmad-copilot.update').then( + () => { /* Command executed — UI notification handled by the command */ }, + (err) => { this.log(`Update command error: ${err}`); }, + ); + + stream.markdown( + '✅ Update triggered. The command registry will be refreshed momentarily.\n\n' + + '> Use `@bmad /status` to verify the updated state.', + ); + + return {}; + } + + /* -------------------------------------------------------------- */ + /* /status — Built-in + CLI version */ + /* -------------------------------------------------------------- */ + + /** + * Handle `/status` — show BMAD installation health. + * + * Works even when `state` is null (no `_bmad/`), so the user can + * diagnose installation problems. + * + * @param state - Current registry state, or null if BMAD not installed. + * @param stream - Chat response stream for sending messages to user. + * @returns Chat result. + */ + private handleStatus( + state: RegistryState | null, + stream: vscode.ChatResponseStream, + ): vscode.ChatResult { + const workspaceRoot = this.getWorkspaceRoot(); + const hasBmad = this.cliBridge.hasBmadInstallation(workspaceRoot); + const hasCopilot = this.cliBridge.hasCopilotPromptFiles(workspaceRoot); + + const lines: string[] = ['# BMAD Installation Status\n']; + + lines.push(`| Check | Status |`); + lines.push(`|---|---|`); + lines.push(`| \`_bmad/\` directory | ${hasBmad ? '✅ Found' : '❌ Missing'} |`); + lines.push(`| GitHub Copilot prompt files | ${hasCopilot ? '✅ Found' : '⚠️ Missing'} |`); + lines.push(`| Workspace root | \`${workspaceRoot}\` |`); + + if (state) { + lines.push(`| BMAD directory | \`${state.bmadDir}\` |`); + lines.push(`| Installed modules | ${state.modules.join(', ') || 'none'} |`); + lines.push(`| Discovered commands | ${state.commands.size} |`); + lines.push(`| Prompt files mapped | ${state.promptFiles.size} |`); + lines.push(`| Agents | ${state.agents.length} |`); + lines.push(`| Workflows | ${state.workflows.length} |`); + lines.push(`| Tasks | ${state.tasks.length} |`); + lines.push(`| Tools | ${state.tools.length} |`); + lines.push(`| Last scan | ${state.lastScan} |`); + } + + lines.push(''); + + if (!hasBmad) { + lines.push('---\n'); + lines.push(MISSING_INSTALL_MESSAGE); + } else if (!hasCopilot) { + lines.push('---\n'); + lines.push(MISSING_COPILOT_FILES_MESSAGE); + } + + stream.markdown(lines.join('\n')); + return {}; + } + + /* -------------------------------------------------------------- */ + /* /help — List commands and guidance */ + /* -------------------------------------------------------------- */ + + /** + * Handle `/help` — show available commands and usage guidance. + * If the user provides a topic, filter or focus the help output. + * + * @param state - Current registry state. + * @param request - The chat request object. + * @param stream - Chat response stream for sending messages to user. + * @param token - Cancellation token. + * @returns Chat result. + */ + private async handleHelp( + state: RegistryState, + request: vscode.ChatRequest, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken, + ): Promise { + const topic = request.prompt.trim(); + + // If user asks about a specific command, try to resolve it + if (topic && topic.startsWith('bmad')) { + const cmd = this.registry.resolve(topic); + if (cmd) { + stream.markdown(this.formatCommandDetail(cmd)); + return {}; + } + } + + // Build help listing + const lines: string[] = [ + '# BMAD Help\n', + '## Quick Commands\n', + '| Command | Description |', + '|---|---|', + '| `@bmad /status` | Check installation health |', + '| `@bmad /install` | Run the BMAD installer (opens terminal) |', + '| `@bmad /help` | This help message |', + '| `@bmad /run ` | Execute a BMAD command |', + '| `@bmad /agents` | List installed agents |', + '| `@bmad /workflows` | List installed workflows |', + '| `@bmad /tasks` | List installed tasks and tools |', + '', + ]; + + // Top commands from help entries + if (state.helpEntries.length > 0) { + lines.push('## Available BMAD Commands\n'); + const byPhase = new Map(); + for (const h of state.helpEntries) { + const phase = h.phase || 'anytime'; + if (!byPhase.has(phase)) byPhase.set(phase, []); + byPhase.get(phase)!.push(h); + } + + for (const [phase, entries] of byPhase) { + lines.push(`### ${phase}\n`); + lines.push('| Command | Name | Agent | Description |'); + lines.push('|---|---|---|---|'); + for (const h of entries) { + const cmd = h.command ? `\`/run ${h.command}\`` : `*${h.code || '—'}*`; + lines.push(`| ${cmd} | ${h.name} | ${h['agent-title'] || ''} | ${h.description} |`); + } + lines.push(''); + } + } + + lines.push( + '\n> **Tip:** You can also type a command name directly, e.g. ' + + '`@bmad bmad-bmm-create-prd`\n', + ); + + // If user had a topic query, also send to LLM for contextual help + if (topic) { + lines.push(`\n---\n\n*Searching for help on: "${topic}"…*\n\n`); + stream.markdown(lines.join('\n')); + // Use LLM for contextual help + const contextPrompt = [ + 'You are a BMAD-METHOD expert assistant.', + `Installed modules: ${state.modules.join(', ')}`, + `Available commands: ${state.commands.size}`, + `\nUser is asking for help about: ${topic}`, + '\nProvide concise, actionable guidance. Suggest specific commands where applicable.', + ].join('\n'); + return this.sendToLlm(contextPrompt, request, stream, token); + } + + stream.markdown(lines.join('\n')); + return {}; + } + + /* -------------------------------------------------------------- */ + /* /run — Prompt File Executor (core) */ + /* -------------------------------------------------------------- */ + + /** + * Handle `/run ` — execute a BMAD command. + * + * **Execution strategy:** + * 1. Resolve command from the registry. + * 2. If an official `.prompt.md` or `.agent.md` file exists → + * read it and send the prompt **as-is** to the LLM. The LLM + * resolves `{project-root}` references via its own workspace context. + * 3. If no prompt file → fall back to BmadRuntime prompt builder + * (reads from `_bmad/` directly). + * + * @param state - Current registry state. + * @param request - The chat request object. + * @param stream - Chat response stream for sending messages to user. + * @param token - Cancellation token. + * @returns Chat result. + */ + private async handleRun( + state: RegistryState, + request: vscode.ChatRequest, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken, + ): Promise { + const parts = request.prompt.trim().split(/\s+/); + const rawName = parts[0] || ''; + const userInput = parts.slice(1).join(' '); + + if (!rawName) { + stream.markdown( + '💡 **Usage:** `/run [input]`\n\n' + + 'Examples:\n' + + '- `/run bmad-bmm-create-prd`\n' + + '- `/run bmad-agent-bmm-pm`\n\n' + + 'Use `/help` or `/workflows` to see available commands.', + ); + return {}; + } + + // Normalise input + const normalised = rawName.startsWith('bmad-') ? rawName : `bmad-${rawName}`; + const command = this.registry.resolve(normalised) || this.registry.resolve(rawName); + + if (!command) { + return this.handleCommandNotFound(rawName, stream); + } + + return this.executeCommand(command, userInput, request, stream, token); + } + + /* -------------------------------------------------------------- */ + /* /agents, /workflows, /tasks — Listing views */ + /* -------------------------------------------------------------- */ + + /** + * Handle `/agents` — list installed agents. + * + * @param state - Current registry state. + * @param stream - Chat response stream for sending messages to user. + * @returns Chat result. + */ + private handleAgents( + state: RegistryState, + stream: vscode.ChatResponseStream, + ): vscode.ChatResult { + if (state.agents.length === 0) { + stream.markdown('No BMAD agents installed.'); + return {}; + } + + const lines = ['# Installed BMAD Agents\n']; + const byModule = new Map(); + for (const a of state.agents) { + const mod = a.module || 'core'; + if (!byModule.has(mod)) byModule.set(mod, []); + byModule.get(mod)!.push(a); + } + + for (const [mod, group] of byModule) { + lines.push(`## Module: \`${mod}\`\n`); + lines.push('| Agent | Persona | Title | Run Command |'); + lines.push('|---|---|---|---|'); + for (const a of group) { + const slashName = mod === 'core' + ? `bmad-agent-${a.name}` + : `bmad-agent-${mod}-${a.name}`; + lines.push( + `| ${a.name} | ${a.displayName || '—'} | ${a.icon || ''} ${a.title || ''} | \`/run ${slashName}\` |`, + ); + } + lines.push(''); + } + stream.markdown(lines.join('\n')); + return {}; + } + + /** + * Handle `/workflows` — list installed workflows. + * + * @param state - Current registry state. + * @param stream - Chat response stream for sending messages to user. + * @returns Chat result. + */ + private handleWorkflows( + state: RegistryState, + stream: vscode.ChatResponseStream, + ): vscode.ChatResult { + if (state.helpEntries.length === 0 && state.workflows.length === 0) { + stream.markdown('No BMAD workflows installed.'); + return {}; + } + + const lines = ['# Installed BMAD Workflows\n']; + + if (state.helpEntries.length > 0) { + const byPhase = new Map(); + for (const h of state.helpEntries) { + const phase = h.phase || 'anytime'; + if (!byPhase.has(phase)) byPhase.set(phase, []); + byPhase.get(phase)!.push(h); + } + + for (const [phase, entries] of byPhase) { + lines.push(`## ${phase}\n`); + lines.push('| Run Command | Name | Agent | Required | Description |'); + lines.push('|---|---|---|---|---|'); + for (const h of entries) { + const cmd = h.command ? `\`/run ${h.command}\`` : '*—*'; + const req = h.required === 'true' ? '✅' : ''; + lines.push(`| ${cmd} | ${h.name} | ${h['agent-title'] || ''} | ${req} | ${h.description} |`); + } + lines.push(''); + } + } else { + lines.push('| Name | Module | Description | Run Command |'); + lines.push('|---|---|---|---|'); + for (const w of state.workflows) { + const slashName = `bmad-${w.module}-${w.name}`; + lines.push(`| ${w.name} | ${w.module} | ${w.description} | \`/run ${slashName}\` |`); + } + } + stream.markdown(lines.join('\n')); + return {}; + } + + /** + * Handle `/tasks` — list installed tasks and tools. + * + * @param state - Current registry state. + * @param stream - Chat response stream for sending messages to user. + * @returns Chat result. + */ + private handleTasks( + state: RegistryState, + stream: vscode.ChatResponseStream, + ): vscode.ChatResult { + if (state.tasks.length === 0 && state.tools.length === 0) { + stream.markdown('No BMAD tasks or tools installed.'); + return {}; + } + + const lines = ['# Installed BMAD Tasks & Tools\n']; + + if (state.tasks.length > 0) { + lines.push('## Tasks\n'); + lines.push('| Name | Module | Description |'); + lines.push('|---|---|---|'); + for (const t of state.tasks) { + lines.push(`| ${t.name} | ${t.module} | ${t.description} |`); + } + lines.push(''); + } + + if (state.tools.length > 0) { + lines.push('## Tools\n'); + lines.push('| Name | Module | Description |'); + lines.push('|---|---|---|'); + for (const t of state.tools) { + lines.push(`| ${t.name} | ${t.module} | ${t.description} |`); + } + } + stream.markdown(lines.join('\n')); + return {}; + } + + /* -------------------------------------------------------------- */ + /* Free text handler */ + /* -------------------------------------------------------------- */ + + /** + * Handle free-text (no slash command). + * + * 1. If it starts with `bmad-`, try exact match → execute. + * 2. Otherwise, send to LLM with BMAD workspace context. + * + * @param state - Current registry state. + * @param request - The chat request object. + * @param stream - Chat response stream for sending messages to user. + * @param token - Cancellation token. + * @returns Chat result. + */ + private async handleFreeText( + state: RegistryState, + request: vscode.ChatRequest, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken, + ): Promise { + const prompt = request.prompt.trim(); + + // Try exact command match + if (prompt.startsWith('bmad-')) { + const firstWord = prompt.split(/\s+/)[0]; + const command = this.registry.resolve(firstWord); + if (command) { + const userInput = prompt.slice(firstWord.length).trim(); + return this.executeCommand(command, userInput, request, stream, token); + } + } + + // General BMAD-aware conversation + const contextPrompt = [ + 'You are an expert on the BMAD-METHOD framework.', + `This workspace has BMAD installed with modules: ${state.modules.join(', ')}.`, + `There are ${state.commands.size} commands available.`, + '', + 'The user is asking a question in the context of their BMAD project.', + 'If their question relates to a specific workflow, agent, or task,', + 'suggest the appropriate `@bmad /run ` command.', + '', + `User: ${prompt}`, + ].join('\n'); + + return this.sendToLlm(contextPrompt, request, stream, token); + } + + /* -------------------------------------------------------------- */ + /* Command execution (Prompt File Executor) */ + /* -------------------------------------------------------------- */ + + /** + * Execute a resolved BMAD command by reading its official prompt + * file and sending it directly to the LLM. + * + * **Resolution order:** + * 1. `command.promptFilePath` — official `.prompt.md` / `.agent.md` + * 2. `state.promptFiles` — prompt file map from scan + * 3. If no prompt file exists — show an error with install guidance. + * + * The prompt body is passed **as-is** — no file inlining, no + * `{project-root}` resolution. The LLM reads workspace files + * through its own context, matching native Copilot behaviour. + * + * @param command - The resolved BMAD command to execute. + * @param userInput - Additional input from the user. + * @param request - The chat request object. + * @param stream - Chat response stream for sending messages to user. + * @param token - Cancellation token. + * @returns Chat result. + */ + private async executeCommand( + command: BmadCommand, + userInput: string, + request: vscode.ChatRequest, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken, + ): Promise { + const workspaceRoot = this.getWorkspaceRoot(); + + stream.markdown( + `🚀 **Executing**: \`${command.slashName}\` → CLI: \`${command.cliSyntax}\`\n` + + `> ${command.description}\n\n---\n\n`, + ); + + // 1. Try official prompt file + const promptFilePath = command.promptFilePath + || this.registry.state?.promptFiles.get(command.slashName); + + if (promptFilePath && fs.existsSync(promptFilePath)) { + this.log(`Using prompt file: ${promptFilePath}`); + const compiledPrompt = this.buildFromPromptFile(promptFilePath, userInput); + return this.sendToLlm(compiledPrompt, request, stream, token); + } + + // 2. No prompt file — guide the user. + this.log(`No prompt file for ${command.slashName}`); + stream.markdown( + `⚠️ **No prompt file found** for \`${command.slashName}\`.\n\n` + + 'The official prompt/agent file is missing from `.github/prompts/` or `.github/agents/`.\n\n' + + 'Run the BMAD installer to generate it:\n' + + '```bash\nnpx bmad-method install --tools github-copilot --yes\n```\n\n' + + '> Then use `@bmad /update` to refresh the command registry.', + ); + return {}; + } + + /** + * Read an official `.prompt.md` or `.agent.md` file, strip YAML + * frontmatter, and return the body **as-is**. + * + * No file inlining, no `{project-root}` resolution, no token + * management. The official prompt is passed directly to the LLM, + * matching the behaviour of native GitHub Copilot. + * + * @param filePath - Absolute path to the prompt file. + * @param userInput - Additional user input to append. + * @returns Prompt body ready to send to the LLM. + */ + private buildFromPromptFile( + filePath: string, + userInput: string, + ): string { + let raw = fs.readFileSync(filePath, 'utf8'); + + // Strip UTF-8 BOM if present + if (raw.charCodeAt(0) === 0xFEFF) { + raw = raw.slice(1); + } + + // Strip YAML frontmatter (--- ... ---) + const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/); + const body = fmMatch ? fmMatch[2] : raw; + + // Compose final prompt — body as-is + optional user input + if (userInput) { + return `${body}\n\n## Additional User Input\n\n${userInput}`; + } + + return body; + } + + /* -------------------------------------------------------------- */ + /* Command not found */ + /* -------------------------------------------------------------- */ + + /** + * Show a "not found" message with fuzzy suggestions. + * + * @param rawName - The command name that was not found. + * @param stream - Chat response stream for sending messages to user. + * @returns Chat result. + */ + private handleCommandNotFound( + rawName: string, + stream: vscode.ChatResponseStream, + ): vscode.ChatResult { + const suggestions = this.registry.search(rawName, 5); + if (suggestions.length > 0) { + const list = suggestions + .map((s) => `- \`/run ${s.slashName}\` — ${s.description}`) + .join('\n'); + stream.markdown( + `❓ Command \`${rawName}\` not found.\n\nDid you mean:\n${list}`, + ); + } else { + stream.markdown( + `❓ Command \`${rawName}\` not found.\n\n` + + 'Use `/workflows` to see all available commands, or `/install` to set up BMAD.', + ); + } + return {}; + } + + /** + * Format detailed information for a single command. + * + * @param cmd - The command to format. + * @returns Formatted Markdown string. + */ + private formatCommandDetail(cmd: BmadCommand): string { + const lines = [ + `## \`${cmd.slashName}\`\n`, + `- **CLI syntax:** \`${cmd.cliSyntax}\``, + `- **Category:** ${cmd.category}`, + `- **Module:** ${cmd.module}`, + `- **Description:** ${cmd.description}`, + ]; + if (cmd.agentTitle) lines.push(`- **Agent:** ${cmd.agentTitle}`); + if (cmd.promptFilePath) lines.push(`- **Prompt file:** \`${cmd.promptFilePath}\``); + lines.push(`\n**Run:** \`@bmad /run ${cmd.slashName}\``); + return lines.join('\n'); + } + + /* -------------------------------------------------------------- */ + /* LLM interaction */ + /* -------------------------------------------------------------- */ + + /** + * Send a prompt to the Copilot LLM and stream the response. + * + * Uses `request.model.sendRequest()` to respect the user's + * selected model preference. + * + * @param prompt - The prompt text to send to the LLM. + * @param request - The chat request object. + * @param stream - Chat response stream for sending messages to user. + * @param token - Cancellation token. + * @returns Chat result. + */ + private async sendToLlm( + prompt: string, + request: vscode.ChatRequest, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken, + ): Promise { + const messages = [vscode.LanguageModelChatMessage.User(prompt)]; + + try { + const chatResponse = await request.model.sendRequest(messages, {}, token); + + for await (const fragment of chatResponse.text) { + if (token.isCancellationRequested) break; + stream.markdown(fragment); + } + } catch (err) { + if (err instanceof vscode.LanguageModelError) { + this.log(`LLM error: ${err.message} (code: ${err.code})`); + const msg = err.code === 'Blocked' + ? '\n\n⚠️ **Rate limit reached.** Please wait a moment and try again.' + : `\n\n⚠️ **Model error**: ${err.message}`; + stream.markdown(msg); + } else { + throw err; + } + } + + return {}; + } + + /* -------------------------------------------------------------- */ + /* Utilities */ + /* -------------------------------------------------------------- */ + + /** + * Get the first workspace folder path. + * + * @returns Absolute path to the workspace root. + * @throws Error if no workspace folder is open. + */ + private getWorkspaceRoot(): string { + const folders = vscode.workspace.workspaceFolders; + if (!folders || folders.length === 0) { + throw new Error('No workspace folder is open.'); + } + return folders[0].uri.fsPath; + } + + /** + * Write a timestamped log line to the output channel. + * + * @param message - Message to log. + */ + private log(message: string): void { + const ts = new Date().toISOString(); + this.outputChannel.appendLine(`[${ts}] [ChatBridge] ${message}`); + } +} + + diff --git a/bmad-copilot/src/cli/bootstrap.ts b/bmad-copilot/src/cli/bootstrap.ts new file mode 100644 index 00000000..9ee4dec0 --- /dev/null +++ b/bmad-copilot/src/cli/bootstrap.ts @@ -0,0 +1,426 @@ +/** + * @fileoverview Bootstrap command — full project setup for bmad-copilot-adapter. + * + * Performs a complete installation check and setup: + * 1. Verify Node.js version (>=18) + * 2. Check for `.github/prompts` directory (warn if missing — do NOT auto-install) + * 3. Detect VS Code CLI (`code` command) + * 4. Check if the BMAD Copilot extension is installed (provide build instructions if not) + * 5. Validate prompt directory readable + * 6. Print success summary with prompts/agents/commands found + * + * Note: The VS Code extension is NOT published to the Marketplace. + * Users must build and install it manually from source. + * + * This is a pure adapter — it never modifies, converts, or generates + * prompt files. The official `npx bmad-method install` is the only + * source of truth for `.github/prompts`. + * + * @module cli/bootstrap + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as cp from 'node:child_process'; + +/* ------------------------------------------------------------------ */ +/* Non-goal reminder */ +/* ------------------------------------------------------------------ */ +// This CLI does NOT: +// - Convert or mirror Cloud Code prompts +// - Rewrite or generate Copilot prompt files +// - Fork any BMAD runtime logic +// It is a pure bridge / adapter. + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +export interface BootstrapOptions { + /** Working directory (project root). */ + cwd: string; + /** Skip confirmation prompts. */ + yes: boolean; +} + +interface StepResult { + ok: boolean; + message: string; + skipped?: boolean; +} + +/* ------------------------------------------------------------------ */ +/* Pretty output helpers */ +/* ------------------------------------------------------------------ */ + +const FMT = { + green: (s: string) => `\x1b[32m${s}\x1b[0m`, + red: (s: string) => `\x1b[31m${s}\x1b[0m`, + yellow: (s: string) => `\x1b[33m${s}\x1b[0m`, + cyan: (s: string) => `\x1b[36m${s}\x1b[0m`, + bold: (s: string) => `\x1b[1m${s}\x1b[0m`, + dim: (s: string) => `\x1b[2m${s}\x1b[0m`, +}; + +/** + * Print a success step message with green checkmark. + * + * @param msg - Message to display. + */ +function stepOk(msg: string): void { + console.log(` ${FMT.green('✔')} ${msg}`); +} + +/** + * Print a skipped step message with yellow circle. + * + * @param msg - Message to display. + */ +function stepSkip(msg: string): void { + console.log(` ${FMT.yellow('◇')} ${msg}`); +} + +/** + * Print a failure step message with red X. + * + * @param msg - Message to display. + */ +function stepFail(msg: string): void { + console.log(` ${FMT.red('✖')} ${msg}`); +} + +/** + * Print an informational step message with cyan info icon. + * + * @param msg - Message to display. + */ +function stepInfo(msg: string): void { + console.log(` ${FMT.cyan('ℹ')} ${msg}`); +} + +/* ------------------------------------------------------------------ */ +/* Bootstrap */ +/* ------------------------------------------------------------------ */ + +export async function bootstrap(options: BootstrapOptions): Promise { + const { cwd, yes } = options; + const results: StepResult[] = []; + + console.log(''); + console.log(FMT.bold(' BMAD Copilot Adapter — Bootstrap')); + console.log(FMT.dim(' ─────────────────────────────────')); + console.log(''); + + // ── Step 1: Check Node version ────────────────────────────── + { + const result = checkNodeVersion(); + results.push(result); + if (result.ok) { + stepOk(result.message); + } else { + stepFail(result.message); + console.log(''); + console.error(FMT.red(' Bootstrap aborted: Node.js >= 18 is required.')); + process.exit(1); + } + } + + // ── Step 2: Check .github/prompts ─────────────────────────── + { + const promptsDir = path.join(cwd, '.github', 'prompts'); + const agentsDir = path.join(cwd, '.github', 'agents'); + const hasPrompts = fs.existsSync(promptsDir) && hasBmadFiles(promptsDir); + const hasAgents = fs.existsSync(agentsDir) && hasBmadAgentFiles(agentsDir); + + if (hasPrompts || hasAgents) { + const pCount = hasPrompts ? countBmadFiles(promptsDir) : 0; + const aCount = hasAgents ? countBmadAgentFiles(agentsDir) : 0; + stepOk(`Found ${pCount} prompt file(s), ${aCount} agent file(s)`); + results.push({ ok: true, message: 'Prompts/agents found' }); + } else { + // Warn — do NOT auto-install. User must run the official CLI. + stepFail('.github/prompts/ and .github/agents/ not found or contain no BMAD files'); + console.log(''); + console.log(FMT.yellow(' You must install prompts with the official BMAD CLI first:')); + console.log(FMT.dim(' npx bmad-method install')); + console.log(FMT.dim(' Select ✅ GitHub Copilot in the interactive menu.')); + console.log(''); + results.push({ + ok: false, + message: '.github/prompts/ or .github/agents/ missing — run: npx bmad-method install', + }); + } + } + + // ── Step 3: Detect VS Code CLI ────────────────────────────── + { + const result = detectVsCodeCli(); + results.push(result); + if (result.ok) { + stepOk(result.message); + } else { + stepSkip(result.message); + } + } + + // ── Step 4: Check VS Code extension ───────────────────────── + { + const vsCodeAvailable = results[results.length - 1].ok; + if (vsCodeAvailable) { + const result = await installExtension(); + results.push(result); + if (result.ok) { + stepOk(result.message); + } else if (result.skipped) { + stepSkip(result.message); + } else { + stepFail(result.message); + console.log(''); + console.log(FMT.yellow(' 📦 How to build and install the extension:')); + console.log(''); + console.log(FMT.dim(' git clone https://github.com/pi-docket/bmad-method-vscode.git')); + console.log(FMT.dim(' cd bmad-method-vscode/bmad-copilot')); + console.log(FMT.dim(' npm install')); + console.log(FMT.dim(' npm run vsce:package')); + console.log(FMT.dim(' code --install-extension releases/bmad-copilot-adapter-0.2.5.vsix')); + console.log(''); + console.log(FMT.cyan(' 📖 Full instructions: https://www.npmjs.com/package/bmad-copilot-adapter')); + console.log(''); + } + } else { + stepSkip('VS Code not detected — skipping extension check'); + results.push({ ok: false, message: 'VS Code not detected', skipped: true }); + } + } + + // ── Step 5: Validate Copilot participant ──────────────────── + { + const result = validateCopilotRegistration(cwd); + results.push(result); + if (result.ok) { + stepOk(result.message); + } else { + stepSkip(result.message); + } + } + + // ── Step 6: Summary ───────────────────────────────────────── + console.log(''); + console.log(FMT.dim(' ─────────────────────────────────')); + + const failures = results.filter((r) => !r.ok && !r.skipped); + const skipped = results.filter((r) => r.skipped); + + if (failures.length === 0) { + console.log(` ${FMT.green('✔')} ${FMT.bold('Bootstrap complete!')}`); + console.log(''); + console.log(' Next steps:'); + console.log(FMT.dim(' 1. Open this project in VS Code')); + console.log(FMT.dim(' 2. Open Copilot Chat (Ctrl+Shift+I)')); + console.log(FMT.dim(' 3. Type: @bmad /help')); + console.log(''); + + if (skipped.length > 0) { + console.log(FMT.yellow(` ${skipped.length} step(s) skipped (non-critical).`)); + console.log(FMT.dim(' Run `npx bmad-copilot-adapter status` for details.')); + } + } else { + console.log(` ${FMT.yellow('⚠')} ${FMT.bold('Bootstrap completed with warnings')}`); + console.log(''); + for (const f of failures) { + console.log(` ${FMT.red('•')} ${f.message}`); + } + console.log(''); + console.log(FMT.dim(' Run `npx bmad-copilot-adapter update` to retry sync.')); + } + + console.log(''); +} + +/* ------------------------------------------------------------------ */ +/* Step implementations */ +/* ------------------------------------------------------------------ */ + +/** + * Step 1: Check Node.js version >= 18. + */ +function checkNodeVersion(): StepResult { + const version = process.versions.node; + const major = parseInt(version.split('.')[0], 10); + + if (major >= 18) { + return { ok: true, message: `Node.js v${version} (>= 18 ✓)` }; + } + + return { + ok: false, + message: `Node.js v${version} detected — v18 or later required`, + }; +} + +/** + * Step 3: Detect VS Code CLI (`code` command). + */ +function detectVsCodeCli(): StepResult { + const isWindows = process.platform === 'win32'; + + try { + // Avoid DEP0190: don't pass args array when shell is true + const result = isWindows + ? cp.spawnSync('code.cmd --version', { timeout: 10000, shell: true, stdio: ['ignore', 'pipe', 'pipe'] }) + : cp.spawnSync('code', ['--version'], { timeout: 10000, stdio: ['ignore', 'pipe', 'pipe'] }); + + if (result.status === 0) { + const version = result.stdout?.toString().trim().split('\n')[0] ?? 'unknown'; + return { ok: true, message: `VS Code CLI detected (v${version})` }; + } + + return { + ok: false, + message: 'VS Code CLI (`code`) not found in PATH', + skipped: true, + }; + } catch { + return { + ok: false, + message: 'VS Code CLI (`code`) not found in PATH', + skipped: true, + }; + } +} + +/** + * Step 4: Check if VS Code extension is installed. + * + * Note: This extension is NOT published to the VS Code Marketplace. + * Users must build and install it manually from source. + */ +async function installExtension(): Promise { + const extensionId = 'evil9369.bmad-copilot-adapter'; + const isWindows = process.platform === 'win32'; + + // Check if already installed (avoid DEP0190: no args array with shell:true) + try { + const listResult = isWindows + ? cp.spawnSync('code.cmd --list-extensions', { timeout: 15000, shell: true, stdio: ['ignore', 'pipe', 'pipe'] }) + : cp.spawnSync('code', ['--list-extensions'], { timeout: 15000, stdio: ['ignore', 'pipe', 'pipe'] }); + + if (listResult.status === 0) { + const extensions = listResult.stdout?.toString() ?? ''; + if (extensions.toLowerCase().includes(extensionId.toLowerCase())) { + return { + ok: true, + message: `Extension ${extensionId} already installed`, + skipped: true, + }; + } + } + } catch { + // Continue to not-installed message + } + + // Extension not found — instruct user to build from source + return { + ok: false, + message: `Extension not installed. This extension must be built from source.`, + }; +} + +/** + * Step 6: Validate Copilot participant registration. + * + * This is a heuristic: we check that `.github/prompts/` has bmad files + * and the extension package.json declares the chat participant. + * Full runtime validation requires VS Code to be running. + */ +function validateCopilotRegistration(cwd: string): StepResult { + const promptsDir = path.join(cwd, '.github', 'prompts'); + const agentsDir = path.join(cwd, '.github', 'agents'); + + const hasPrompts = fs.existsSync(promptsDir) && hasBmadFiles(promptsDir); + const hasAgents = fs.existsSync(agentsDir) && hasBmadAgentFiles(agentsDir); + + if (hasPrompts || hasAgents) { + const count = (hasPrompts ? countBmadFiles(promptsDir) : 0) + (hasAgents ? countBmadAgentFiles(agentsDir) : 0); + return { + ok: true, + message: `Copilot integration ready (${count} BMAD file(s) detected)`, + }; + } + + // Check for _bmad/ as fallback (prompts can be generated via BMAD installer) + const hasBmadDir = fs.existsSync(path.join(cwd, '_bmad')); + if (hasBmadDir) { + return { + ok: false, + message: 'Copilot integration ready (_bmad/ found; run `npx bmad-method install --tools github-copilot` to generate prompts)', + skipped: true, + }; + } + + return { + ok: false, + message: 'No BMAD installation detected — Copilot participant cannot register', + skipped: true, + }; +} + +/* ------------------------------------------------------------------ */ +/* File helpers */ +/* ------------------------------------------------------------------ */ + +/** + * Check if a directory contains BMAD prompt files. + * + * @param dir - Absolute path to directory. + * @returns `true` if directory contains `bmad*.prompt.md` files. + */ +function hasBmadFiles(dir: string): boolean { + try { + return fs.readdirSync(dir).some((f) => f.startsWith('bmad') && f.endsWith('.prompt.md')); + } catch { + return false; + } +} + +/** + * Check if a directory contains BMAD agent files. + * + * @param dir - Absolute path to directory. + * @returns `true` if directory contains `bmad-agent*.md` files. + */ +function hasBmadAgentFiles(dir: string): boolean { + try { + return fs.readdirSync(dir).some((f) => f.startsWith('bmad-agent') && f.endsWith('.md')); + } catch { + return false; + } +} + +/** + * Count BMAD prompt files in a directory. + * + * @param dir - Absolute path to directory. + * @returns Number of `bmad*.prompt.md` files found. + */ +function countBmadFiles(dir: string): number { + try { + return fs.readdirSync(dir).filter((f) => f.startsWith('bmad') && f.endsWith('.prompt.md')).length; + } catch { + return 0; + } +} + +/** + * Count BMAD agent files in a directory. + * + * @param dir - Absolute path to directory. + * @returns Number of `bmad-agent*.md` files found. + */ +function countBmadAgentFiles(dir: string): number { + try { + return fs.readdirSync(dir).filter((f) => f.startsWith('bmad-agent') && f.endsWith('.md')).length; + } catch { + return 0; + } +} diff --git a/bmad-copilot/src/cli/index.ts b/bmad-copilot/src/cli/index.ts new file mode 100644 index 00000000..2d447294 --- /dev/null +++ b/bmad-copilot/src/cli/index.ts @@ -0,0 +1,134 @@ +/** + * @fileoverview CLI Router for bmad-copilot-adapter. + * + * Parses command-line arguments and dispatches to the appropriate + * command handler. Uses zero external dependencies — simple + * `process.argv` parsing only. + * + * @module cli/index + */ + +import { bootstrap } from './bootstrap.js'; +import { update } from './update.js'; +import { status } from './status.js'; + +/* ------------------------------------------------------------------ */ +/* Constants */ +/* ------------------------------------------------------------------ */ + +const VERSION = require('../../package.json').version; + +const HELP_TEXT = ` +bmad-copilot-adapter v${VERSION} + +USAGE + npx bmad-copilot-adapter [options] + +COMMANDS + bootstrap Full setup: check Node, install prompts, install VS Code extension + update Rescan .github/prompts and rebuild command map + status Show BMAD installation health and diagnostics + +OPTIONS + --cwd Override working directory (default: process.cwd()) + --yes, -y Skip confirmation prompts + --help, -h Show this help message + --version Show version number + +EXAMPLES + npx bmad-copilot-adapter bootstrap + npx bmad-copilot-adapter update + npx bmad-copilot-adapter status --cwd /path/to/project +`.trim(); + +/* ------------------------------------------------------------------ */ +/* Argument parsing */ +/* ------------------------------------------------------------------ */ + +interface ParsedArgs { + command: string; + cwd: string; + yes: boolean; + help: boolean; + version: boolean; +} + +/** + * Parse command-line arguments from process.argv. + * + * @param argv - Raw command-line arguments array. + * @returns Parsed arguments object. + */ +function parseArgs(argv: string[]): ParsedArgs { + const args = argv.slice(2); // Skip node + script path + const result: ParsedArgs = { + command: '', + cwd: process.cwd(), + yes: false, + help: false, + version: false, + }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === '--help' || arg === '-h') { + result.help = true; + } else if (arg === '--version') { + result.version = true; + } else if (arg === '--yes' || arg === '-y') { + result.yes = true; + } else if (arg === '--cwd' && i + 1 < args.length) { + result.cwd = args[++i]; + } else if (!arg.startsWith('-') && !result.command) { + result.command = arg; + } + } + + return result; +} + +/* ------------------------------------------------------------------ */ +/* Main */ +/* ------------------------------------------------------------------ */ + +/** + * CLI entry point — parses arguments and dispatches to command handlers. + */ +async function main(): Promise { + const args = parseArgs(process.argv); + + if (args.version) { + console.log(VERSION); + process.exit(0); + } + + if (args.help || !args.command) { + console.log(HELP_TEXT); + process.exit(args.help ? 0 : 1); + } + + try { + switch (args.command) { + case 'bootstrap': + await bootstrap({ cwd: args.cwd, yes: args.yes }); + break; + case 'update': + await update({ cwd: args.cwd }); + break; + case 'status': + await status({ cwd: args.cwd }); + break; + default: + console.error(`\x1b[31mUnknown command: ${args.command}\x1b[0m\n`); + console.log(HELP_TEXT); + process.exit(1); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error(`\x1b[31m✖ ${msg}\x1b[0m`); + process.exit(1); + } +} + +main(); diff --git a/bmad-copilot/src/cli/status.ts b/bmad-copilot/src/cli/status.ts new file mode 100644 index 00000000..501507e2 --- /dev/null +++ b/bmad-copilot/src/cli/status.ts @@ -0,0 +1,275 @@ +/** + * @fileoverview Status command — show BMAD installation diagnostics. + * + * Checks the workspace for: + * - `_bmad/` directory and modules + * - `.github/prompts/` and `.github/agents/` BMAD files + * - `claude-code` source availability (informational) + * - VS Code CLI and extension status + * - Manifest CSV health + * + * @module cli/status + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as cp from 'node:child_process'; + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +export interface StatusOptions { + /** Working directory (project root). */ + cwd: string; +} + +/* ------------------------------------------------------------------ */ +/* Pretty output helpers */ +/* ------------------------------------------------------------------ */ + +const FMT = { + green: (s: string) => `\x1b[32m${s}\x1b[0m`, + red: (s: string) => `\x1b[31m${s}\x1b[0m`, + yellow: (s: string) => `\x1b[33m${s}\x1b[0m`, + cyan: (s: string) => `\x1b[36m${s}\x1b[0m`, + bold: (s: string) => `\x1b[1m${s}\x1b[0m`, + dim: (s: string) => `\x1b[2m${s}\x1b[0m`, +}; + +/* ------------------------------------------------------------------ */ +/* Status command */ +/* ------------------------------------------------------------------ */ + +export async function status(options: StatusOptions): Promise { + const { cwd } = options; + + console.log(''); + console.log(FMT.bold(' BMAD Copilot Adapter — Status')); + console.log(FMT.dim(' ─────────────────────────────')); + console.log(''); + + // ── 1. Node.js ────────────────────────────────────────────── + const nodeVersion = process.versions.node; + const nodeMajor = parseInt(nodeVersion.split('.')[0], 10); + printCheck('Node.js', `v${nodeVersion}`, nodeMajor >= 18); + + // ── 2. Working directory ──────────────────────────────────── + console.log(` ${FMT.dim('Workspace:')} ${cwd}`); + console.log(''); + + // ── 3. _bmad/ directory ───────────────────────────────────── + const bmadDir = path.join(cwd, '_bmad'); + const hasBmad = fs.existsSync(bmadDir); + printCheck('_bmad/ directory', hasBmad ? 'Found' : 'Missing', hasBmad); + + if (hasBmad) { + // Modules + const modules = detectModules(bmadDir); + console.log(` ${FMT.dim(' Modules:')} ${modules.length > 0 ? modules.join(', ') : 'none'}`); + + // Config + const configDir = path.join(bmadDir, '_config'); + const hasConfig = fs.existsSync(configDir); + printCheck(' _config/ manifests', hasConfig ? 'Found' : 'Missing', hasConfig); + + if (hasConfig) { + const manifests = ['bmad-help.csv', 'agent-manifest.csv', 'workflow-manifest.csv', 'task-manifest.csv', 'tool-manifest.csv']; + for (const m of manifests) { + const mPath = path.join(configDir, m); + const exists = fs.existsSync(mPath); + const count = exists ? countCsvRows(mPath) : 0; + console.log(` ${FMT.dim(` ${m}:`)} ${exists ? `${count} entries` : FMT.yellow('not found')}`); + } + } + } + + console.log(''); + + // ── 4. .github/prompts/ ───────────────────────────────────── + const promptsDir = path.join(cwd, '.github', 'prompts'); + const promptFiles = listBmadFiles(promptsDir, '.prompt.md'); + printCheck('.github/prompts/', promptFiles.length > 0 ? `${promptFiles.length} file(s)` : 'Empty or missing', promptFiles.length > 0); + + // ── 5. .github/agents/ ────────────────────────────────────── + const agentsDir = path.join(cwd, '.github', 'agents'); + const agentFiles = listBmadAgentFiles(agentsDir); + printCheck('.github/agents/', agentFiles.length > 0 ? `${agentFiles.length} file(s)` : 'Empty or missing', agentFiles.length > 0); + + // ── 6. Claude-code source (informational only) ───────────── + if (hasBmad) { + const claudeSrc = path.join(bmadDir, 'ide', 'claude-code', 'prompts'); + const hasClaudeSrc = fs.existsSync(claudeSrc); + printCheck('claude-code source', hasClaudeSrc ? 'Present (informational)' : 'Not found', hasClaudeSrc, true); + } + + console.log(''); + + // ── 7. VS Code CLI ────────────────────────────────────────── + const vsCodeInfo = detectVsCode(); + printCheck('VS Code CLI', vsCodeInfo.version ?? 'Not found in PATH', vsCodeInfo.available, !vsCodeInfo.available); + + // ── 8. Extension installed ────────────────────────────────── + if (vsCodeInfo.available) { + const extInstalled = checkExtensionInstalled(); + printCheck('BMAD extension', extInstalled ? 'Installed' : 'Not installed', extInstalled, !extInstalled); + } + + // ── Summary ───────────────────────────────────────────────── + console.log(''); + console.log(FMT.dim(' ─────────────────────────────')); + + if (!hasBmad) { + console.log(''); + console.log(` ${FMT.yellow('⚠')} BMAD not installed. Run:`); + console.log(FMT.dim(' npx bmad-copilot-adapter bootstrap')); + } else if (promptFiles.length === 0 && agentFiles.length === 0) { + console.log(''); + console.log(` ${FMT.yellow('⚠')} No Copilot prompt files. Run:`); + console.log(FMT.dim(' npx bmad-copilot-adapter update')); + } else { + console.log(''); + console.log(` ${FMT.green('✔')} ${FMT.bold('BMAD Copilot Adapter ready')}`); + } + + console.log(''); +} + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +/** + * Print a status check line with icon. + * + * @param label - Check label. + * @param value - Check value/status. + * @param ok - Whether the check passed. + * @param isOptional - Whether this check is optional. + */ +function printCheck(label: string, value: string, ok: boolean, isOptional = false): void { + const icon = ok ? FMT.green('✔') : isOptional ? FMT.yellow('◇') : FMT.red('✖'); + console.log(` ${icon} ${label}: ${value}`); +} + +/** + * Detect installed BMAD modules in the _bmad directory. + * + * @param bmadDir - Absolute path to _bmad directory. + * @returns Array of module names. + */ +function detectModules(bmadDir: string): string[] { + try { + return fs + .readdirSync(bmadDir, { withFileTypes: true }) + .filter( + (e) => + e.isDirectory() && + e.name !== '_config' && + e.name !== '_memory' && + e.name !== 'docs' && + e.name !== 'ide' && + !e.name.startsWith('.'), + ) + .map((e) => e.name); + } catch { + return []; + } +} + +/** + * Count non-header rows in a CSV file. + * + * @param filePath - Absolute path to CSV file. + * @returns Number of data rows (excluding header). + */ +function countCsvRows(filePath: string): number { + try { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n').filter((l) => l.trim() && !l.startsWith('#')); + return Math.max(0, lines.length - 1); + } catch { + return 0; + } +} + +/** + * List BMAD files in a directory with a specific suffix. + * + * @param dir - Absolute path to directory. + * @param suffix - File suffix to filter by. + * @returns Array of matching filenames. + */ +function listBmadFiles(dir: string, suffix: string): string[] { + try { + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir).filter((f) => f.startsWith('bmad') && f.endsWith(suffix)); + } catch { + return []; + } +} + +/** + * List BMAD agent files in a directory. + * + * @param dir - Absolute path to directory. + * @returns Array of agent filenames. + */ +function listBmadAgentFiles(dir: string): string[] { + try { + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir).filter((f) => f.startsWith('bmad-agent') && f.endsWith('.md')); + } catch { + return []; + } +} + +/** + * Detect VS Code CLI availability and version. + * + * @returns Object with availability status and version string. + */ +function detectVsCode(): { available: boolean; version: string | null } { + const isWindows = process.platform === 'win32'; + + try { + // Avoid DEP0190: don't pass args array when shell is true + const result = isWindows + ? cp.spawnSync('code.cmd --version', { timeout: 10000, shell: true, stdio: ['ignore', 'pipe', 'pipe'] }) + : cp.spawnSync('code', ['--version'], { timeout: 10000, stdio: ['ignore', 'pipe', 'pipe'] }); + + if (result.status === 0) { + const version = result.stdout?.toString().trim().split('\n')[0] ?? null; + return { available: true, version }; + } + } catch { + // Fall through + } + + return { available: false, version: null }; +} + +/** + * Check if the BMAD Copilot Adapter extension is installed in VS Code. + * + * @returns `true` if extension is installed. + */ +function checkExtensionInstalled(): boolean { + const isWindows = process.platform === 'win32'; + const extensionId = 'evil9369.bmad-copilot-adapter'; + + try { + // Avoid DEP0190: don't pass args array when shell is true + const result = isWindows + ? cp.spawnSync('code.cmd --list-extensions', { timeout: 15000, shell: true, stdio: ['ignore', 'pipe', 'pipe'] }) + : cp.spawnSync('code', ['--list-extensions'], { timeout: 15000, stdio: ['ignore', 'pipe', 'pipe'] }); + + if (result.status === 0) { + return result.stdout?.toString().toLowerCase().includes(extensionId.toLowerCase()) ?? false; + } + } catch { + // Fall through + } + + return false; +} diff --git a/bmad-copilot/src/cli/update.ts b/bmad-copilot/src/cli/update.ts new file mode 100644 index 00000000..561527fe --- /dev/null +++ b/bmad-copilot/src/cli/update.ts @@ -0,0 +1,189 @@ +/** + * @fileoverview Update command — rescan prompts and rebuild command map. + * + * When called from the CLI, this command: + * 1. Verifies `_bmad/` exists + * 2. Scans `.github/prompts/` and `.github/agents/` for official BMAD files + * 3. Scans manifest CSV files for command counts + * 4. Writes a sentinel file to trigger VS Code extension rescan + * 5. Prints a summary of discovered commands + * + * This command does NOT mirror, convert, or rewrite prompt files. + * It reads only official files generated by `npx bmad-method install`. + * + * @module cli/update + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +export interface UpdateOptions { + /** Working directory (project root). */ + cwd: string; +} + +/* ------------------------------------------------------------------ */ +/* Pretty output helpers */ +/* ------------------------------------------------------------------ */ + +const FMT = { + green: (s: string) => `\x1b[32m${s}\x1b[0m`, + red: (s: string) => `\x1b[31m${s}\x1b[0m`, + yellow: (s: string) => `\x1b[33m${s}\x1b[0m`, + cyan: (s: string) => `\x1b[36m${s}\x1b[0m`, + bold: (s: string) => `\x1b[1m${s}\x1b[0m`, + dim: (s: string) => `\x1b[2m${s}\x1b[0m`, +}; + +/* ------------------------------------------------------------------ */ +/* Update command */ +/* ------------------------------------------------------------------ */ + +export async function update(options: UpdateOptions): Promise { + const { cwd } = options; + + console.log(''); + console.log(FMT.bold(' BMAD Copilot Adapter — Update')); + console.log(FMT.dim(' ─────────────────────────────')); + console.log(''); + + // ── 1. Verify _bmad/ exists ───────────────────────────────── + const bmadDir = path.join(cwd, '_bmad'); + if (!fs.existsSync(bmadDir)) { + console.log(` ${FMT.red('✖')} No _bmad/ directory found in ${cwd}`); + console.log(''); + console.log(FMT.dim(' Run: npx bmad-copilot-adapter bootstrap')); + process.exit(1); + } + console.log(` ${FMT.green('✔')} Found _bmad/ directory`); + + // ── 2. Scan .github/prompts/ ──────────────────────────────── + const promptsDir = path.join(cwd, '.github', 'prompts'); + const agentsDir = path.join(cwd, '.github', 'agents'); + + let promptCount = 0; + let agentCount = 0; + + if (fs.existsSync(promptsDir)) { + try { + const files = fs.readdirSync(promptsDir).filter( + (f) => f.startsWith('bmad') && f.endsWith('.prompt.md'), + ); + promptCount = files.length; + } catch { /* ignore */ } + } + + if (fs.existsSync(agentsDir)) { + try { + const files = fs.readdirSync(agentsDir).filter( + (f) => f.startsWith('bmad-agent') && f.endsWith('.md'), + ); + agentCount = files.length; + } catch { /* ignore */ } + } + + if (promptCount > 0 || agentCount > 0) { + console.log(` ${FMT.green('✔')} Found ${promptCount} prompt file(s), ${agentCount} agent file(s)`); + } else { + console.log(` ${FMT.yellow('◇')} No Copilot prompt files found in .github/`); + } + + // ── 3. Scan manifest CSV files for command count ───────────── + const configDir = path.join(bmadDir, '_config'); + let helpEntryCount = 0; + let manifestAgentCount = 0; + let taskCount = 0; + let toolCount = 0; + + if (fs.existsSync(configDir)) { + helpEntryCount = countCsvRows(path.join(configDir, 'bmad-help.csv')); + manifestAgentCount = countCsvRows(path.join(configDir, 'agent-manifest.csv')); + taskCount = countCsvRows(path.join(configDir, 'task-manifest.csv')); + toolCount = countCsvRows(path.join(configDir, 'tool-manifest.csv')); + } + + // ── 4. Detect installed modules ────────────────────────────── + const modules = detectModules(bmadDir); + + // ── 5. Write update sentinel for extension pickup ──────────── + // The extension watches _bmad/ for changes, so touching a + // sentinel file triggers its debounced rescan. + const sentinelPath = path.join(bmadDir, '.copilot-update'); + try { + fs.writeFileSync(sentinelPath, new Date().toISOString(), 'utf8'); + console.log(` ${FMT.green('✔')} Wrote update sentinel for VS Code extension`); + } catch { + console.log(` ${FMT.yellow('◇')} Could not write update sentinel (non-critical)`); + } + + // ── 6. Print summary ──────────────────────────────────────── + console.log(''); + console.log(FMT.dim(' ─────────────────────────────')); + console.log(` ${FMT.green('✔')} ${FMT.bold('Update complete')}`); + console.log(''); + console.log(` Modules: ${modules.length > 0 ? modules.join(', ') : 'none'}`); + console.log(` Help entries: ${helpEntryCount}`); + console.log(` Agents: ${manifestAgentCount}`); + console.log(` Tasks: ${taskCount}`); + console.log(` Tools: ${toolCount}`); + console.log(` Prompt files: ${promptCount}`); + console.log(` Agent files: ${agentCount}`); + console.log(''); + + if (promptCount === 0 && agentCount === 0) { + console.log(FMT.yellow(' ⚠ No Copilot prompt files found.')); + console.log(FMT.dim(' Run: npx bmad-method install --tools github-copilot --yes')); + } else { + console.log(FMT.dim(' Reload VS Code window or use @bmad /update to apply changes.')); + } + console.log(''); +} + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +/** + * Count non-header rows in a CSV file. + * + * @param filePath - Absolute path to CSV file. + * @returns Number of data rows (excluding header). + */ +function countCsvRows(filePath: string): number { + try { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n').filter((l) => l.trim() && !l.startsWith('#')); + return Math.max(0, lines.length - 1); // Minus header row + } catch { + return 0; + } +} + +/** + * Detect installed module directories under `_bmad/`. + * + * @param bmadDir - Absolute path to _bmad directory. + * @returns Array of module names. + */ +function detectModules(bmadDir: string): string[] { + try { + return fs + .readdirSync(bmadDir, { withFileTypes: true }) + .filter( + (e) => + e.isDirectory() && + e.name !== '_config' && + e.name !== '_memory' && + e.name !== 'docs' && + e.name !== 'ide' && + !e.name.startsWith('.'), + ) + .map((e) => e.name); + } catch { + return []; + } +} diff --git a/bmad-copilot/src/cliBridge.ts b/bmad-copilot/src/cliBridge.ts new file mode 100644 index 00000000..47cc432e --- /dev/null +++ b/bmad-copilot/src/cliBridge.ts @@ -0,0 +1,353 @@ +/** + * @fileoverview CLI Bridge — spawns `bmad-method` CLI as a child process + * and streams output to Copilot Chat or VS Code terminals. + * + * **Capabilities** + * - Resolve the bmad CLI executable (local `node_modules/.bin/bmad` or `npx`) + * - Check CLI availability and version + * - Spawn interactive commands in a VS Code terminal (for `install`) + * - Spawn non-interactive commands and stream stdout/stderr (for `status`) + * - Cancellation via CancellationToken (kills child process) + * + * **Windows Support** + * - Resolves `.cmd` executables on Windows + * - Uses `shell: true` on Windows for proper PATH resolution + * + * @module cliBridge + */ + +import * as vscode from 'vscode'; +import * as cp from 'node:child_process'; +import * as path from 'node:path'; +import * as fs from 'node:fs'; + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +/** Resolved CLI command and base arguments. */ +export interface CliResolution { + /** The executable command (absolute path or `npx`/`npx.cmd`). */ + command: string; + /** Base arguments prepended to every invocation (e.g. `['bmad-method']` for npx). */ + baseArgs: string[]; +} + +/** Result of a captured CLI execution. */ +export interface CliResult { + /** Process exit code (0 = success). */ + exitCode: number; + /** Combined stdout text. */ + stdout: string; + /** Combined stderr text. */ + stderr: string; +} + +/* ------------------------------------------------------------------ */ +/* CliBridge */ +/* ------------------------------------------------------------------ */ + +/** + * Bridges the BMAD CLI (`npx bmad-method`) into the VS Code environment. + * + * The bridge resolves the CLI executable, provides availability checks, + * and offers two execution modes: + * + * 1. **Terminal mode** — opens a VS Code integrated terminal for + * interactive commands (e.g. `install` with its @clack/prompts UI). + * 2. **Stream mode** — spawns a child process and pipes stdout/stderr + * into a Copilot Chat response stream. + */ +export class CliBridge { + private readonly outputChannel: vscode.OutputChannel; + + constructor(outputChannel: vscode.OutputChannel) { + this.outputChannel = outputChannel; + } + + /* -------------------------------------------------------------- */ + /* Resolution */ + /* -------------------------------------------------------------- */ + + /** + * Resolve the bmad CLI executable path. + * + * **Priority order:** + * 1. `/node_modules/.bin/bmad` (local install) + * 2. `npx bmad-method` (npx fallback) + * + * On Windows, `.cmd` extensions are checked first. + * + * @param workspaceRoot - Absolute path to workspace root. + * @returns Resolved command and base arguments. + */ + resolveCli(workspaceRoot: string): CliResolution { + const isWindows = process.platform === 'win32'; + + // 1. Try local node_modules/.bin/bmad + const localBinNames = isWindows + ? ['bmad.cmd', 'bmad.ps1', 'bmad'] + : ['bmad']; + + for (const binName of localBinNames) { + const localBin = path.join(workspaceRoot, 'node_modules', '.bin', binName); + if (fs.existsSync(localBin)) { + this.log(`Resolved CLI: local ${localBin}`); + return { command: localBin, baseArgs: [] }; + } + } + + // 2. Fallback: npx bmad-method + const npxCmd = isWindows ? 'npx.cmd' : 'npx'; + this.log(`Resolved CLI: ${npxCmd} bmad-method`); + return { command: npxCmd, baseArgs: ['bmad-method'] }; + } + + /* -------------------------------------------------------------- */ + /* Availability */ + /* -------------------------------------------------------------- */ + + /** + * Check whether `bmad-method` is reachable and return its version. + * + * @param workspaceRoot - Absolute workspace root path. + * @returns Version string (e.g. `"6.0.0-Beta.8"`) or `null` if unavailable. + */ + async getVersion(workspaceRoot: string): Promise { + try { + const cli = this.resolveCli(workspaceRoot); + const result = await this.spawnAndCapture( + ['--version'], + workspaceRoot, + cli, + 5000, + ); + if (result.exitCode === 0 && result.stdout.trim()) { + return result.stdout.trim(); + } + return null; + } catch { + return null; + } + } + + /** + * Quick check: is the `_bmad/` directory present? + * + * @param workspaceRoot - Absolute workspace root path. + * @returns `true` if `_bmad/` exists. + */ + hasBmadInstallation(workspaceRoot: string): boolean { + return fs.existsSync(path.join(workspaceRoot, '_bmad')); + } + + /** + * Quick check: are GitHub Copilot prompt files present? + * + * @param workspaceRoot - Workspace root. + * @returns `true` if `.github/prompts/` or `.github/agents/` has bmad files. + */ + hasCopilotPromptFiles(workspaceRoot: string): boolean { + const promptsDir = path.join(workspaceRoot, '.github', 'prompts'); + const agentsDir = path.join(workspaceRoot, '.github', 'agents'); + + const check = (dir: string) => { + if (!fs.existsSync(dir)) return false; + try { + return fs.readdirSync(dir).some((f) => f.startsWith('bmad')); + } catch { + return false; + } + }; + + return check(promptsDir) || check(agentsDir); + } + + /* -------------------------------------------------------------- */ + /* Terminal mode (interactive) */ + /* -------------------------------------------------------------- */ + + /** + * Open a VS Code integrated terminal and execute a bmad CLI command. + * + * Used for interactive commands like `install` that rely on + * `@clack/prompts` for rich terminal UI. + * + * @param args - CLI arguments (e.g. `['install', '--tools', 'github-copilot']`). + * @param workspaceRoot - Workspace root (used as cwd). + * @returns The created terminal instance. + */ + openTerminal(args: string[], workspaceRoot: string): vscode.Terminal { + const cli = this.resolveCli(workspaceRoot); + const fullArgs = [...cli.baseArgs, ...args]; + + // Quote command and args to prevent shell injection and handle spaces + const quotedCommand = cli.command.includes(' ') ? `"${cli.command}"` : cli.command; + const quotedArgs = fullArgs.map(arg => arg.includes(' ') ? `"${arg}"` : arg); + const cmdLine = `${quotedCommand} ${quotedArgs.join(' ')}`; + + const terminal = vscode.window.createTerminal({ + name: 'BMAD Install', + cwd: workspaceRoot, + }); + + terminal.show(); + terminal.sendText(cmdLine); + + this.log(`Opened terminal: ${cmdLine}`); + return terminal; + } + + /* -------------------------------------------------------------- */ + /* Stream mode (Copilot Chat output) */ + /* -------------------------------------------------------------- */ + + /** + * Spawn a bmad CLI command and stream its output into a Copilot Chat + * response stream. + * + * stdout lines are emitted as Markdown. stderr lines are emitted + * inside blockquotes with a warning prefix. + * + * ANSI escape codes are stripped automatically. + * + * @param args - CLI arguments. + * @param workspaceRoot - Workspace root (cwd). + * @param stream - Copilot Chat response stream. + * @param token - Cancellation token; kills the child process on cancel. + * @returns Process exit code. + */ + async spawnToChat( + args: string[], + workspaceRoot: string, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken, + ): Promise { + const cli = this.resolveCli(workspaceRoot); + const fullArgs = [...cli.baseArgs, ...args]; + + this.log(`Spawning: ${cli.command} ${fullArgs.join(' ')}`); + + return new Promise((resolve, reject) => { + const proc = cp.spawn(cli.command, fullArgs, { + cwd: workspaceRoot, + shell: process.platform === 'win32', + env: { + ...process.env, + FORCE_COLOR: '0', + NO_COLOR: '1', + CI: '1', // Suppress interactive prompts + }, + }); + + // Cancel → kill + const cancelSub = token.onCancellationRequested(() => { + this.log('Cancellation requested — killing process'); + proc.kill('SIGTERM'); + setTimeout(() => { + if (!proc.killed) proc.kill('SIGKILL'); + }, 3000); + }); + + proc.stdout?.on('data', (data: Buffer) => { + const text = stripAnsi(data.toString()); + if (text.trim()) { + stream.markdown(text); + } + }); + + proc.stderr?.on('data', (data: Buffer) => { + const text = stripAnsi(data.toString()).trim(); + if (text) { + stream.markdown(`\n> ⚠️ ${text}\n`); + } + }); + + let settled = false; + + proc.on('close', (code) => { + if (settled) return; + settled = true; + cancelSub.dispose(); + this.log(`Process exited with code ${code}`); + resolve(code ?? 1); + }); + + proc.on('error', (err) => { + if (settled) return; + settled = true; + cancelSub.dispose(); + this.log(`Process error: ${err.message}`); + reject(err); + }); + }); + } + + /* -------------------------------------------------------------- */ + /* Internal helpers */ + /* -------------------------------------------------------------- */ + + /** + * Spawn a command and capture its full output (no streaming). + * + * @param args - CLI arguments. + * @param workspaceRoot - Working directory. + * @param cli - Resolved CLI. + * @param timeout - Max wait in ms. + * @returns Captured result. + */ + private spawnAndCapture( + args: string[], + workspaceRoot: string, + cli: CliResolution, + timeout: number, + ): Promise { + const fullArgs = [...cli.baseArgs, ...args]; + + return new Promise((resolve, reject) => { + const proc = cp.spawn(cli.command, fullArgs, { + cwd: workspaceRoot, + shell: process.platform === 'win32', + timeout, + env: { + ...process.env, + FORCE_COLOR: '0', + NO_COLOR: '1', + }, + }); + + let stdout = ''; + let stderr = ''; + + proc.stdout?.on('data', (d: Buffer) => { stdout += d.toString(); }); + proc.stderr?.on('data', (d: Buffer) => { stderr += d.toString(); }); + + proc.on('close', (code) => { + resolve({ exitCode: code ?? 1, stdout, stderr }); + }); + + proc.on('error', (err) => reject(err)); + }); + } + + /** Write to the BMAD output channel. */ + private log(message: string): void { + const ts = new Date().toISOString(); + this.outputChannel.appendLine(`[${ts}] [CliBridge] ${message}`); + } +} + +/* ------------------------------------------------------------------ */ +/* Utilities */ +/* ------------------------------------------------------------------ */ + +/** + * Strip ANSI escape codes from a string. + * + * @param text - Input text possibly containing escape sequences. + * @returns Clean text. + */ +function stripAnsi(text: string): string { + // eslint-disable-next-line no-control-regex + return text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, ''); +} diff --git a/bmad-copilot/src/commandRegistry.ts b/bmad-copilot/src/commandRegistry.ts new file mode 100644 index 00000000..159b9e6f --- /dev/null +++ b/bmad-copilot/src/commandRegistry.ts @@ -0,0 +1,632 @@ +/** + * @fileoverview Command Registry — scans a BMAD installation and builds + * the canonical {@link RegistryState} that all other layers consume. + * + * **Responsibilities** + * 1. Locate the `_bmad/` directory in the workspace. + * 2. Parse every manifest CSV (`bmad-help`, `agent-manifest`, + * `workflow-manifest`, `task-manifest`, `tool-manifest`). + * 3. Derive a {@link BmadCommand} for each actionable entry. + * 4. Expose the state via {@link CommandRegistry.state}. + * + * **Design rules** + * - Zero hard-coded commands — everything is discovered at runtime. + * - Manifest schemas are defined in {@link types}. + * - The registry never mutates BMAD files. + * + * @module commandRegistry + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import type { + AgentManifestEntry, + BmadCommand, + BmadHelpEntry, + PromptPattern, + RegistryState, + TaskToolManifestEntry, + WorkflowManifestEntry, +} from './types.js'; + +/* ------------------------------------------------------------------ */ +/* CSV parsing (minimal, zero-dependency) */ +/* ------------------------------------------------------------------ */ + +/** + * Parse a CSV string with header row into an array of column→value objects. + * + * Handles double-quoted fields and embedded commas — the same subset + * that the core BMAD installer relies on. + * + * @param csv - Raw CSV text. + * @returns Array of records keyed by header column names. + */ +function parseCsv>(csv: string): T[] { + const lines = csv.split('\n').filter((l) => l.trim() && !l.startsWith('#')); + if (lines.length < 2) { + return []; + } + + // Trim headers to handle CRLF line endings on Windows + const headers = parseCsvLine(lines[0]).map(h => h.trim()); + const records: T[] = []; + + for (let i = 1; i < lines.length; i++) { + const values = parseCsvLine(lines[i]); + if (values.length === 0) { + continue; + } + const record: Record = {}; + for (let h = 0; h < headers.length; h++) { + record[headers[h]] = (values[h] ?? '').trim(); + } + records.push(record as T); + } + return records; +} + +/** + * Split a single CSV line into field values, respecting double-quoted + * fields that may contain commas. + * + * @param line - A single CSV line. + * @returns Array of unquoted field values. + */ +function parseCsvLine(line: string): string[] { + const fields: string[] = []; + let current = ''; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (inQuotes) { + if (ch === '"') { + if (i + 1 < line.length && line[i + 1] === '"') { + current += '"'; + i++; // skip escaped quote + } else { + inQuotes = false; + } + } else { + current += ch; + } + } else if (ch === '"') { + inQuotes = true; + } else if (ch === ',') { + fields.push(current); + current = ''; + } else { + current += ch; + } + } + fields.push(current); + return fields; +} + +/* ------------------------------------------------------------------ */ +/* Slash-name ↔ CLI syntax helpers */ +/* ------------------------------------------------------------------ */ + +/** + * Convert a BMAD CLI colon-style command to a Copilot-friendly slash + * name. + * + * @example + * cliToSlash('bmad:bmm:create-prd') // → 'bmad-bmm-create-prd' + * + * @param cli - Colon-delimited CLI command. + * @returns Dash-delimited slash name. + */ +export function cliToSlash(cli: string): string { + return cli.replaceAll(':', '-'); +} + +/** + * Convert a Copilot slash-name back to BMAD CLI colon syntax. + * + * The algorithm is intentionally conservative: it only replaces the + * structural dashes that separate `bmad`, module, type-prefix + * (`agent`), and the trailing name — everything else stays as-is. + * + * **Limitation:** For core agents with hyphenated names (e.g., `tech-writer`), + * the slash name `bmad-agent-tech-writer` is ambiguous and may be incorrectly + * parsed as module=tech, name=writer. This affects only the display `cliSyntax` + * field and does not impact command execution. + * + * @example + * slashToCli('bmad-bmm-create-prd') // → 'bmad:bmm:create-prd' + * slashToCli('bmad-agent-bmm-pm') // → 'bmad:agent:bmm:pm' + * slashToCli('bmad-help') // → 'bmad:help' + * + * @param slash - Dash-delimited slash name. + * @returns Colon-delimited CLI command. + */ +export function slashToCli(slash: string): string { + // Remove leading '/' if present + const clean = slash.startsWith('/') ? slash.slice(1) : slash; + + const parts = clean.split('-'); + if (parts[0] !== 'bmad') { + return clean; + } + + // Agent pattern: bmad-agent-- + if (parts[1] === 'agent') { + if (parts.length >= 4) { + const module = parts[2]; + const name = parts.slice(3).join('-'); + return `bmad:agent:${module}:${name}`; + } + // Core agent: bmad-agent- + return `bmad:agent:${parts.slice(2).join('-')}`; + } + + // Core task/tool with no module: bmad- + if (parts.length === 2) { + return `bmad:${parts[1]}`; + } + + // Module workflow: bmad-- + const module = parts[1]; + const rest = parts.slice(2).join('-'); + return `bmad:${module}:${rest}`; +} + +/* ------------------------------------------------------------------ */ +/* Prompt-pattern detection */ +/* ------------------------------------------------------------------ */ + +/** + * Determine the prompt-injection pattern for a workflow based on its + * file extension, matching the four patterns from the official + * `github-copilot.js` installer. + * + * | Extension | Pattern | + * |-----------|------------------| + * | `.md` | md-workflow | + * | `.yaml` | yaml-workflow | + * | `.xml` | task | + * + * @param workflowFile - Relative path to the workflow file. + * @returns The applicable {@link PromptPattern}. + */ +function detectPattern(workflowFile: string): PromptPattern { + if (workflowFile.endsWith('.yaml') || workflowFile.endsWith('.yml')) { + return 'yaml-workflow'; + } + if (workflowFile.endsWith('.xml')) { + return 'task'; + } + return 'md-workflow'; +} + +/* ------------------------------------------------------------------ */ +/* CommandRegistry */ +/* ------------------------------------------------------------------ */ + +/** + * Scans a BMAD installation directory and produces an immutable + * {@link RegistryState} containing every discoverable command. + * + * Usage: + * ```ts + * const reg = new CommandRegistry(); + * const state = await reg.scan('/path/to/project'); + * state.commands.forEach(cmd => console.log(cmd.slashName)); + * ``` + */ +export class CommandRegistry { + /** The last successful scan result. `null` until {@link scan} completes. */ + private _state: RegistryState | null = null; + + /** Read-only accessor for the current registry state. */ + get state(): RegistryState | null { + return this._state; + } + + /* -------------------------------------------------------------- */ + /* Public API */ + /* -------------------------------------------------------------- */ + + /** + * Perform a full scan of the workspace to discover the BMAD + * installation and build the command map. + * + * @param workspaceRoot - Absolute path to the VS Code workspace root. + * @param overrideBmadDir - Optional explicit `_bmad` path (from config). + * @returns The populated {@link RegistryState}, or `null` if no + * BMAD installation was found. + */ + async scan(workspaceRoot: string, overrideBmadDir?: string): Promise { + const bmadDir = overrideBmadDir || this.findBmadDir(workspaceRoot); + if (!bmadDir) { + return null; + } + + const configDir = path.join(bmadDir, '_config'); + if (!fs.existsSync(configDir)) { + return null; + } + + // --- Parse all manifests in parallel --- + const [helpEntries, agents, workflows, tasks, tools] = await Promise.all([ + this.loadCsv(path.join(configDir, 'bmad-help.csv')), + this.loadCsv(path.join(configDir, 'agent-manifest.csv')), + this.loadCsv(path.join(configDir, 'workflow-manifest.csv')), + this.loadCsv(path.join(configDir, 'task-manifest.csv')), + this.loadCsv(path.join(configDir, 'tool-manifest.csv')), + ]); + + // --- Detect installed modules --- + const modules = this.detectModules(bmadDir); + + // --- Build command map --- + const commands = new Map(); + + // 1. Commands from bmad-help.csv (primary source of truth) + for (const entry of helpEntries) { + this.registerHelpEntry(entry, commands); + } + + // 2. Agent activators — one per agent + for (const agent of agents) { + this.registerAgentActivator(agent, commands); + } + + // 3. Tasks and tools from their respective manifests + for (const task of tasks) { + this.registerTaskTool(task, 'task', commands); + } + for (const tool of tools) { + this.registerTaskTool(tool, 'tool', commands); + } + + this._state = { + bmadDir, + commands, + helpEntries, + agents, + workflows, + tasks, + tools, + modules, + promptFiles: new Map(), + hasCopilotFiles: false, + lastScan: new Date().toISOString(), + }; + + // --- Scan .github/prompts/ and .github/agents/ --- + // Use workspaceRoot directly instead of deriving from bmadDir + // to support overrideBmadDir pointing to non-standard locations + this.scanCopilotPromptFiles(workspaceRoot, commands); + + return this._state; + } + + /** + * Invalidate (clear) the cached registry state. + * + * After calling this, {@link state} returns `null` until the next + * {@link scan} completes. Use this before a forced rescan to ensure + * stale data is not served. + */ + invalidate(): void { + this._state = null; + } + + /** + * Convenience method: invalidate + scan in one call. + * + * Equivalent to: + * ```ts + * registry.invalidate(); + * await registry.scan(workspaceRoot, overrideBmadDir); + * ``` + * + * @param workspaceRoot - Absolute workspace root path. + * @param overrideBmadDir - Optional explicit `_bmad` path. + * @returns The new {@link RegistryState}, or `null`. + */ + async rescan(workspaceRoot: string, overrideBmadDir?: string): Promise { + this.invalidate(); + return this.scan(workspaceRoot, overrideBmadDir); + } + + /** + * Look up a command by its slash name. + * + * @param slashName - Command name (with or without leading `/`). + * @returns The matched {@link BmadCommand} or `undefined`. + */ + resolve(slashName: string): BmadCommand | undefined { + const key = slashName.startsWith('/') ? slashName.slice(1) : slashName; + return this._state?.commands.get(key); + } + + /** + * Fuzzy search across registered commands. + * + * Returns commands whose `slashName` or `description` contains the + * query string (case-insensitive). + * + * @param query - Free-text search string. + * @param limit - Maximum results to return (default 20). + * @returns Matching commands in Map iteration order (not scored by relevance). + */ + search(query: string, limit = 20): BmadCommand[] { + if (!this._state) { + return []; + } + const q = query.toLowerCase(); + const results: BmadCommand[] = []; + for (const cmd of this._state.commands.values()) { + if ( + cmd.slashName.toLowerCase().includes(q) || + cmd.description.toLowerCase().includes(q) + ) { + results.push(cmd); + } + if (results.length >= limit) { + break; + } + } + return results; + } + + /* -------------------------------------------------------------- */ + /* Private helpers */ + /* -------------------------------------------------------------- */ + + /** + * Locate the `_bmad` directory by walking up from the workspace root. + * + * @param startDir - Starting directory. + * @returns Absolute path to `_bmad`, or `null` if not found. + */ + private findBmadDir(startDir: string): string | null { + const candidate = path.join(startDir, '_bmad'); + if (fs.existsSync(candidate)) { + return candidate; + } + // Walk up one level (mono-repo scenario) + const parent = path.dirname(startDir); + if (parent !== startDir) { + const parentCandidate = path.join(parent, '_bmad'); + if (fs.existsSync(parentCandidate)) { + return parentCandidate; + } + } + return null; + } + + /** + * Read and parse a CSV manifest file. + * + * @param filePath - Absolute path to the CSV file. + * @returns Parsed records, or an empty array if the file is missing. + */ + private async loadCsv(filePath: string): Promise { + try { + let content = await fs.promises.readFile(filePath, 'utf8'); + // Strip UTF-8 BOM (common on Windows-generated CSV files) + if (content.charCodeAt(0) === 0xFEFF) { + content = content.slice(1); + } + return parseCsv(content); + } catch { + return []; + } + } + + /** + * Detect installed module directories under `_bmad/`. + * + * @param bmadDir - Absolute path to `_bmad`. + * @returns Array of module directory names. + */ + private detectModules(bmadDir: string): string[] { + try { + return fs + .readdirSync(bmadDir, { withFileTypes: true }) + .filter( + (e) => + e.isDirectory() && + e.name !== '_config' && + e.name !== '_memory' && + e.name !== 'docs' && + !e.name.startsWith('.'), + ) + .map((e) => e.name); + } catch { + return []; + } + } + + /** + * Register a command derived from a `bmad-help.csv` row. + * + * @param entry - Parsed CSV row. + * @param map - Target command map. + */ + private registerHelpEntry( + entry: BmadHelpEntry, + map: Map, + ): void { + const commandName = entry.command?.trim(); + if (!commandName) { + // Agent-only entry (e.g. tech-writer commands) — skip static + // registration; these are handled via agent activators or the + // catch-all `/run` command. + return; + } + + const workflowFile = entry['workflow-file']?.trim() || ''; + const pattern = workflowFile ? detectPattern(workflowFile) : 'md-workflow'; + + const cmd: BmadCommand = { + slashName: commandName, + cliSyntax: slashToCli(commandName), + description: entry.description || entry.name || commandName, + category: 'workflow', + module: entry.module || 'core', + filePath: workflowFile, + agentName: entry['agent-name'] || '', + agentTitle: entry['agent-title'] || '', + pattern, + }; + + // First write wins — bmad-help.csv is authoritative + if (!map.has(cmd.slashName)) { + map.set(cmd.slashName, cmd); + } + } + + /** + * Register an agent-activator command from `agent-manifest.csv`. + * + * The naming convention follows the BMAD standard: + * ``` + * bmad-agent-- (module agents) + * bmad-agent- (core agents) + * ``` + * + * @param agent - Parsed CSV row. + * @param map - Target command map. + */ + private registerAgentActivator( + agent: AgentManifestEntry, + map: Map, + ): void { + const mod = agent.module || 'core'; + const slashName = + mod === 'core' + ? `bmad-agent-${agent.name}` + : `bmad-agent-${mod}-${agent.name}`; + + const cmd: BmadCommand = { + slashName, + cliSyntax: slashToCli(slashName), + description: + `${agent.icon || ''} ${agent.displayName || agent.name} — ${agent.title || agent.role || 'Agent'}`.trim(), + category: 'agent', + module: mod, + filePath: agent.path || '', + agentName: agent.name, + agentTitle: `${agent.icon || ''} ${agent.title || ''}`.trim(), + pattern: 'agent-activator', + }; + + if (!map.has(cmd.slashName)) { + map.set(cmd.slashName, cmd); + } + } + + /** + * Register a standalone task or tool from its manifest CSV. + * + * Naming: `bmad-` (core) or `bmad--`. + * + * @param entry - Parsed CSV row. + * @param category - `'task'` or `'tool'`. + * @param map - Target command map. + */ + private registerTaskTool( + entry: TaskToolManifestEntry, + category: 'task' | 'tool', + map: Map, + ): void { + const mod = entry.module || 'core'; + const slashName = + mod === 'core' + ? `bmad-${entry.name}` + : `bmad-${mod}-${entry.name}`; + + const cmd: BmadCommand = { + slashName, + cliSyntax: slashToCli(slashName), + description: entry.description || entry.displayName || entry.name, + category, + module: mod, + filePath: entry.path || '', + agentName: '', + agentTitle: '', + pattern: 'task', + }; + + // Don't overwrite commands already registered from bmad-help.csv + if (!map.has(cmd.slashName)) { + map.set(cmd.slashName, cmd); + } + } + + /** + * Scan `.github/prompts/` and `.github/agents/` for official BMAD + * Copilot prompt files generated by `bmad-method install --tools github-copilot`. + * + * Discovered files are recorded in `_state.promptFiles` and linked + * back to matching commands via `BmadCommand.promptFilePath`. + * + * @param workspaceRoot - Absolute path to the workspace root. + * @param commands - The command map to annotate. + */ + private scanCopilotPromptFiles( + workspaceRoot: string, + commands: Map, + ): void { + if (!this._state) return; + + const promptFiles = this._state.promptFiles; + let foundAny = false; + + // Scan .github/prompts/*.prompt.md + const promptsDir = path.join(workspaceRoot, '.github', 'prompts'); + if (fs.existsSync(promptsDir)) { + try { + for (const file of fs.readdirSync(promptsDir)) { + if (file.startsWith('bmad') && file.endsWith('.prompt.md')) { + // "bmad-bmm-create-prd.prompt.md" → "bmad-bmm-create-prd" + const slashName = file.replace(/\.prompt\.md$/, ''); + const absPath = path.join(promptsDir, file); + promptFiles.set(slashName, absPath); + foundAny = true; + + // Link to existing command if present + const cmd = commands.get(slashName); + if (cmd) { + cmd.promptFilePath = absPath; + } + } + } + } catch { + // Ignore permission or access errors on .github/prompts/ + } + } + + // Scan .github/agents/bmad-agent-*.md + const agentsDir = path.join(workspaceRoot, '.github', 'agents'); + if (fs.existsSync(agentsDir)) { + try { + for (const file of fs.readdirSync(agentsDir)) { + if (file.startsWith('bmad-agent') && file.endsWith('.md')) { + // "bmad-agent-bmm-pm.md" → "bmad-agent-bmm-pm" + const slashName = file.replace(/\.md$/, ''); + const absPath = path.join(agentsDir, file); + promptFiles.set(slashName, absPath); + foundAny = true; + + // Link to existing agent command if present + const cmd = commands.get(slashName); + if (cmd) { + cmd.promptFilePath = absPath; + } + } + } + } catch { + // Ignore permission or access errors on .github/agents/ + } + } + + this._state.hasCopilotFiles = foundAny; + } +} diff --git a/bmad-copilot/src/extension.ts b/bmad-copilot/src/extension.ts new file mode 100644 index 00000000..3d31b6e2 --- /dev/null +++ b/bmad-copilot/src/extension.ts @@ -0,0 +1,311 @@ +/** + * @fileoverview Extension entry point for the BMAD Copilot Adapter. + * + * **Responsibilities** + * 1. Register the `@bmad` Chat Participant via the VS Code Chat API. + * 2. Create the {@link CliBridge} for real CLI process spawning. + * 3. Trigger an initial {@link CommandRegistry.scan} on activation. + * 4. Watch for `_bmad/` and `.github/` directory changes and re-scan. + * 5. Register `bmad-copilot.rescan` and `bmad-copilot.install` commands. + * + * **Debugging** + * - Open the Output panel → select "BMAD Copilot" to see all logs. + * - Set breakpoints in any `src/*.ts` file; they resolve via source maps. + * - Press F5 to launch the Extension Development Host. + * + * @module extension + */ + +import * as vscode from 'vscode'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { CommandRegistry } from './commandRegistry.js'; +import { CliBridge } from './cliBridge.js'; +import { ChatBridge } from './chatBridge.js'; +// NOTE: promptMirror.ts is DEPRECATED and not imported. +// The adapter is a pure read-only bridge. The official +// `npx bmad-method install` generates `.github/prompts` directly. + +/* ------------------------------------------------------------------ */ +/* Constants */ +/* ------------------------------------------------------------------ */ + +const PARTICIPANT_ID = 'bmad'; +const OUTPUT_CHANNEL_NAME = 'BMAD Copilot'; + +/* ------------------------------------------------------------------ */ +/* activate */ +/* ------------------------------------------------------------------ */ + +/** + * VS Code extension activation entry point. + * + * @param context - Extension context for managing disposables. + */ +export async function activate( + context: vscode.ExtensionContext, +): Promise { + const outputChannel = vscode.window.createOutputChannel(OUTPUT_CHANNEL_NAME); + context.subscriptions.push(outputChannel); + + log(outputChannel, 'Activating BMAD Copilot Adapter…'); + + /* -------------------------------------------------------------- */ + /* Core services */ + /* -------------------------------------------------------------- */ + + const registry = new CommandRegistry(); + const cliBridge = new CliBridge(outputChannel); + + // Initial scan — read official .github/prompts as-is (no mirroring) + const workspaceRoot = getWorkspaceRoot(); + if (workspaceRoot) { + await performScan(registry, workspaceRoot, outputChannel); + } else { + log(outputChannel, 'No workspace folder open — skipping initial scan.'); + } + + /* -------------------------------------------------------------- */ + /* Chat Participant */ + /* -------------------------------------------------------------- */ + + const chatBridge = new ChatBridge(registry, cliBridge, outputChannel); + + const participant = vscode.chat.createChatParticipant( + PARTICIPANT_ID, + chatBridge.handler, + ); + participant.iconPath = new vscode.ThemeIcon('rocket'); + context.subscriptions.push(participant); + + log(outputChannel, 'Chat participant @bmad registered.'); + + /* -------------------------------------------------------------- */ + /* Prompt integrity check on activation */ + /* -------------------------------------------------------------- */ + + if (workspaceRoot) { + const promptsDir = require('node:path').join(workspaceRoot, '.github', 'prompts'); + const agentsDir = require('node:path').join(workspaceRoot, '.github', 'agents'); + const bmadDirLocal = require('node:path').join(workspaceRoot, '_bmad'); + + const hasBmadDir = fs.existsSync(bmadDirLocal); + + let hasPrompts = false; + if (fs.existsSync(promptsDir)) { + try { + hasPrompts = fs.readdirSync(promptsDir).some((f: string) => f.startsWith('bmad') && f.endsWith('.prompt.md')); + } catch (err) { + log(outputChannel, `Failed to read ${promptsDir}: ${err instanceof Error ? err.message : String(err)}`); + hasPrompts = false; + } + } + + let hasAgents = false; + if (fs.existsSync(agentsDir)) { + try { + hasAgents = fs.readdirSync(agentsDir).some((f: string) => f.startsWith('bmad-agent') && f.endsWith('.md')); + } catch (err) { + log(outputChannel, `Failed to read ${agentsDir}: ${err instanceof Error ? err.message : String(err)}`); + hasAgents = false; + } + } + + if (hasBmadDir && !hasPrompts && !hasAgents) { + log(outputChannel, 'Prompt integrity check: _bmad/ exists but .github/prompts/ and .github/agents/ missing — notifying user.'); + vscode.window.showWarningMessage( + 'BMAD Copilot: Prompt files missing. Use @bmad /update or run `npx bmad-copilot-adapter update` to sync.', + 'Run Update', + ).then((choice) => { + if (choice === 'Run Update') { + vscode.commands.executeCommand('bmad-copilot.update'); + } + }); + } else if (!hasBmadDir) { + log(outputChannel, 'Prompt integrity check: No _bmad/ found. BMAD not installed.'); + } else { + log(outputChannel, 'Prompt integrity check: OK.'); + } + } + + /* -------------------------------------------------------------- */ + /* Registered commands */ + /* -------------------------------------------------------------- */ + + // Manual rescan + context.subscriptions.push( + vscode.commands.registerCommand('bmad-copilot.rescan', async () => { + const root = getWorkspaceRoot(); + if (!root) { + vscode.window.showWarningMessage('BMAD Copilot: No workspace folder is open.'); + return; + } + const count = await performScan(registry, root, outputChannel); + vscode.window.showInformationMessage(`BMAD Copilot: Scanned ${count} commands.`); + }), + ); + + // Update command — invalidate + rescan + notify + context.subscriptions.push( + vscode.commands.registerCommand('bmad-copilot.update', async () => { + const root = getWorkspaceRoot(); + if (!root) { + vscode.window.showWarningMessage('BMAD Copilot: No workspace folder is open.'); + return; + } + log(outputChannel, 'Update command triggered — invalidating and rescanning…'); + registry.invalidate(); + const count = await performScan(registry, root, outputChannel); + vscode.window.showInformationMessage(`BMAD Copilot: Updated — ${count} commands refreshed.`); + }), + ); + + // Open install terminal + context.subscriptions.push( + vscode.commands.registerCommand('bmad-copilot.install', () => { + const root = getWorkspaceRoot(); + if (!root) { + vscode.window.showWarningMessage('BMAD Copilot: No workspace folder is open.'); + return; + } + cliBridge.openTerminal(['install', '--tools', 'github-copilot'], root); + }), + ); + + /* -------------------------------------------------------------- */ + /* File watchers */ + /* -------------------------------------------------------------- */ + + if (workspaceRoot) { + // Watch _bmad/ and .github/ for changes + const patterns = [ + new vscode.RelativePattern(workspaceRoot, '_bmad/**'), + new vscode.RelativePattern(workspaceRoot, '.github/prompts/**'), + new vscode.RelativePattern(workspaceRoot, '.github/agents/**'), + ]; + + let scanTimer: ReturnType | undefined; + const debouncedScan = () => { + if (scanTimer) clearTimeout(scanTimer); + scanTimer = setTimeout(async () => { + log(outputChannel, 'File change detected — rescanning…'); + const root = getWorkspaceRoot(); + if (root) await performScan(registry, root, outputChannel); + }, 2000); + }; + + for (const pattern of patterns) { + const watcher = vscode.workspace.createFileSystemWatcher(pattern); + watcher.onDidChange(debouncedScan); + watcher.onDidCreate(debouncedScan); + watcher.onDidDelete(debouncedScan); + context.subscriptions.push(watcher); + } + + log(outputChannel, 'File watchers on _bmad/ and .github/ registered.'); + } + + /* -------------------------------------------------------------- */ + /* Configuration change listener */ + /* -------------------------------------------------------------- */ + + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(async (e) => { + if (e.affectsConfiguration('bmad')) { + log(outputChannel, 'Configuration changed — rescanning…'); + const root = getWorkspaceRoot(); + if (root) await performScan(registry, root, outputChannel); + } + }), + ); + + log(outputChannel, 'BMAD Copilot Adapter activated ✓'); +} + +/* ------------------------------------------------------------------ */ +/* deactivate */ +/* ------------------------------------------------------------------ */ + +/** Extension deactivation hook. */ +export function deactivate(): void { + // All disposables are managed via context.subscriptions. +} + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +/** + * Scan the workspace for official BMAD prompt files and build the + * command registry. + * + * This function reads `.github/prompts/` and `.github/agents/` as-is. + * It does NOT mirror, convert, or rewrite any files. The official + * `npx bmad-method install` is the sole source of truth. + */ +async function performScan( + registry: CommandRegistry, + workspaceRoot: string, + outputChannel: vscode.OutputChannel, +): Promise { + const config = vscode.workspace.getConfiguration('bmad'); + const configuredBmadDir = config.get('bmadDir'); + + try { + const state = await registry.scan(workspaceRoot, configuredBmadDir || undefined); + if (!state) { + log(outputChannel, 'Scan returned null — _bmad/ not found.'); + return 0; + } + log( + outputChannel, + `Scan complete: ${state.commands.size} commands, ` + + `${state.promptFiles.size} prompt files, ` + + `modules=[${state.modules.join(', ')}]`, + ); + return state.commands.size; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + log(outputChannel, `Scan failed: ${msg}`); + vscode.window.showWarningMessage(`BMAD Copilot: scan failed — ${msg}`); + return 0; + } +} + +/** + * Locate the `_bmad` directory in the workspace. + * + * @param workspaceRoot - Absolute path to the workspace root. + * @returns Absolute path to `_bmad`, or `null` if not found. + */ +function findBmadDir(workspaceRoot: string): string | null { + const candidate = path.join(workspaceRoot, '_bmad'); + if (fs.existsSync(candidate)) return candidate; + const parent = path.dirname(workspaceRoot); + if (parent !== workspaceRoot) { + const parentCandidate = path.join(parent, '_bmad'); + if (fs.existsSync(parentCandidate)) return parentCandidate; + } + return null; +} + +/** + * Get the first workspace folder's filesystem path. + * + * @returns Absolute path to the workspace root, or `undefined` if no workspace is open. + */ +function getWorkspaceRoot(): string | undefined { + const folders = vscode.workspace.workspaceFolders; + return folders && folders.length > 0 ? folders[0].uri.fsPath : undefined; +} + +/** + * Write a timestamped log message to the output channel. + * + * @param channel - VS Code output channel. + * @param message - Message to log. + */ +function log(channel: vscode.OutputChannel, message: string): void { + const ts = new Date().toISOString(); + channel.appendLine(`[${ts}] ${message}`); +} diff --git a/bmad-copilot/src/promptMirror.ts b/bmad-copilot/src/promptMirror.ts new file mode 100644 index 00000000..b8717212 --- /dev/null +++ b/bmad-copilot/src/promptMirror.ts @@ -0,0 +1,335 @@ +/** + * @fileoverview Prompt Mirror — DEPRECATED historical module. + * + * @deprecated **This module is retained for reference only.** The adapter + * is a pure read-only bridge and does NOT mirror, convert, or generate + * prompt files. The official `npx bmad-method install --tools github-copilot` + * is the sole source of truth for `.github/prompts/` and `.github/agents/`. + * + * No active code imports this module. It will be removed in a future version. + * + * --- Original description --- + * + * When a workspace has BMAD installed with `--tools claude-code` but + * **not** `--tools github-copilot`, the `.github/prompts/` and + * `.github/agents/` directories will be missing. + * + * This module reads the official prompt files generated by the + * claude-code tool under `_bmad/ide/claude-code/` and mirrors them + * into the GitHub Copilot locations with **minimal transformation**: + * + * 1. Filename colons (`:`) → dashes (`-`). + * 2. Frontmatter `command:` field → `name:` field (colons → dashes). + * 3. Body content is **never modified**. + * + * The mirrored files are standard `.prompt.md` / `.agent.md` that the + * Copilot prompt executor can consume directly. + * + * @module promptMirror + */ + +import * as fs from 'node:fs/promises'; +import * as fsSync from 'node:fs'; +import * as path from 'node:path'; + +/* ------------------------------------------------------------------ */ +/* Public types */ +/* ------------------------------------------------------------------ */ + +/** Result returned by {@link ensureCopilotPrompts}. */ +export interface MirrorResult { + /** Whether mirroring was actually performed. */ + performed: boolean; + /** `true` if Copilot prompt files already existed (mirror skipped). */ + alreadyExists: boolean; + /** Number of `.prompt.md` files written. */ + promptCount: number; + /** Number of `.agent.md` files written. */ + agentCount: number; + /** Number of files skipped (already existed at destination). */ + skippedCount: number; + /** Source directory used (absolute path), or `null` if not found. */ + sourceDir: string | null; + /** Destination base directory (`.github/`), or `null`. */ + destDir: string | null; + /** Human-readable summary message. */ + message: string; +} + +/** Parameters for {@link ensureCopilotPrompts}. */ +export interface MirrorParams { + /** Absolute path to the workspace root. */ + workspaceRoot: string; + /** Absolute path to the `_bmad` directory. */ + bmadDir: string; +} + +/* ------------------------------------------------------------------ */ +/* Main entry point */ +/* ------------------------------------------------------------------ */ + +/** + * Ensure that GitHub Copilot prompt files exist in the workspace. + * + * **Logic:** + * 1. If `.github/prompts/` already contains `bmad*.prompt.md` files + * → skip (return `alreadyExists: true`). + * 2. Look for `_bmad/ide/claude-code/prompts/` as mirror source. + * 3. Copy each file with minimal transformation. + * 4. Repeat for `_bmad/ide/claude-code/agents/` → `.github/agents/`. + * + * @param params - Workspace and BMAD directory paths. + * @returns Mirror result with counts and status message. + */ +export async function ensureCopilotPrompts( + params: MirrorParams, +): Promise { + const { workspaceRoot, bmadDir } = params; + + // ── 1. Check if Copilot prompt files already exist ────────── + const copilotPromptsDir = path.join(workspaceRoot, '.github', 'prompts'); + if (hasBmadPromptFiles(copilotPromptsDir)) { + return { + performed: false, + alreadyExists: true, + promptCount: 0, + agentCount: 0, + skippedCount: 0, + sourceDir: null, + destDir: null, + message: 'GitHub Copilot prompt files already exist — mirror skipped.', + }; + } + + // ── 2. Locate claude-code source directories ──────────────── + const claudeCodeBase = path.join(bmadDir, 'ide', 'claude-code'); + const srcPromptsDir = path.join(claudeCodeBase, 'prompts'); + const srcAgentsDir = path.join(claudeCodeBase, 'agents'); + + const hasPromptSource = fsSync.existsSync(srcPromptsDir); + const hasAgentSource = fsSync.existsSync(srcAgentsDir); + + if (!hasPromptSource) { + return { + performed: false, + alreadyExists: false, + promptCount: 0, + agentCount: 0, + skippedCount: 0, + sourceDir: null, + destDir: null, + message: + 'GitHub Copilot prompt files not found and claude-code prompts missing.\n' + + 'Install BMAD with claude-code tool:\n' + + ' npx bmad-method install --modules bmm --tools claude-code --yes', + }; + } + + // ── 3. Mirror prompts ─────────────────────────────────────── + const destPromptsDir = copilotPromptsDir; + const destAgentsDir = path.join(workspaceRoot, '.github', 'agents'); + + let promptCount = 0; + let agentCount = 0; + let skippedCount = 0; + + // 3a. Mirror *.prompt.md files + const promptResults = await mirrorDirectory( + srcPromptsDir, + destPromptsDir, + '.prompt.md', + ); + promptCount = promptResults.written; + skippedCount += promptResults.skipped; + + // 3b. Mirror *.agent.md files (optional — may not exist) + if (hasAgentSource) { + const agentResults = await mirrorDirectory( + srcAgentsDir, + destAgentsDir, + '.agent.md', + ); + agentCount = agentResults.written; + skippedCount += agentResults.skipped; + } + + const total = promptCount + agentCount; + const message = total > 0 + ? `Mirror complete: ${promptCount} prompt(s), ${agentCount} agent(s) mirrored from claude-code.` + : `Mirror found source files but wrote 0 new files (${skippedCount} already existed).`; + + return { + performed: total > 0, + alreadyExists: false, + promptCount, + agentCount, + skippedCount, + sourceDir: claudeCodeBase, + destDir: path.join(workspaceRoot, '.github'), + message, + }; +} + +/* ------------------------------------------------------------------ */ +/* Directory mirroring */ +/* ------------------------------------------------------------------ */ + +/** + * Mirror all matching files from `srcDir` to `destDir` with minimal + * transformation. + * + * @param srcDir - Source directory (e.g. `_bmad/ide/claude-code/prompts/`). + * @param destDir - Destination directory (e.g. `.github/prompts/`). + * @param suffix - File suffix filter (e.g. `.prompt.md`). + * @returns Counts of written and skipped files. + */ +async function mirrorDirectory( + srcDir: string, + destDir: string, + suffix: string, +): Promise<{ written: number; skipped: number }> { + let written = 0; + let skipped = 0; + + if (!fsSync.existsSync(srcDir)) { + return { written, skipped }; + } + + const entries = await fs.readdir(srcDir); + const matchingFiles = entries.filter((f) => f.endsWith(suffix)); + + if (matchingFiles.length === 0) { + return { written, skipped }; + } + + // Ensure destination directory exists + await fs.mkdir(destDir, { recursive: true }); + + for (const srcFile of matchingFiles) { + const destFileName = sanitiseFileName(srcFile); + const destPath = path.join(destDir, destFileName); + + // Never overwrite existing user files + if (fsSync.existsSync(destPath)) { + skipped++; + continue; + } + + const srcPath = path.join(srcDir, srcFile); + const content = await fs.readFile(srcPath, 'utf8'); + const transformed = transformContent(content); + + await fs.writeFile(destPath, transformed, 'utf8'); + written++; + } + + return { written, skipped }; +} + +/* ------------------------------------------------------------------ */ +/* Content transformation (minimal) */ +/* ------------------------------------------------------------------ */ + +/** + * Apply minimal frontmatter transformation to prompt file content. + * + * **Rules:** + * - If frontmatter has `command: bmad:bmm:create-prd` → add/replace + * with `name: bmad-bmm-create-prd` (colons → dashes). + * - If frontmatter already has `name:` → keep as-is. + * - Body content is **never modified**. + * + * @param raw - Raw file content (UTF-8). + * @returns Transformed content. + */ +function transformContent(raw: string): string { + const fmMatch = raw.match(/^(---\r?\n)([\s\S]*?\r?\n)(---\r?\n)([\s\S]*)$/); + if (!fmMatch) { + // No frontmatter — return as-is + return raw; + } + + const fmOpen = fmMatch[1]; // "---\n" + let fmBody = fmMatch[2]; // frontmatter fields + const fmClose = fmMatch[3]; // "---\n" + const body = fmMatch[4]; // everything after frontmatter + + // Check if `name:` already exists + if (/^name\s*:/m.test(fmBody)) { + // Already has a name field — no transformation needed + return raw; + } + + // Check for `command:` field and convert to `name:` + const commandMatch = fmBody.match(/^(command\s*:\s*)(.+)$/m); + if (commandMatch) { + const commandValue = commandMatch[2].trim(); + // Convert colon-style to dash-style: bmad:bmm:create-prd → bmad-bmm-create-prd + const nameValue = commandValue.replaceAll(':', '-'); + // Replace `command:` line with `name:` line + fmBody = fmBody.replace( + commandMatch[0], + `name: ${nameValue}`, + ); + } + + return fmOpen + fmBody + fmClose + body; +} + +/* ------------------------------------------------------------------ */ +/* Filename sanitisation */ +/* ------------------------------------------------------------------ */ + +/** + * Sanitise a filename for cross-platform compatibility. + * + * Converts colons (used in BMAD CLI syntax) to dashes. + * Example: `bmad:bmm:create-prd.prompt.md` → `bmad-bmm-create-prd.prompt.md` + * + * @param name - Original filename. + * @returns Sanitised filename safe for Windows/macOS/Linux. + */ +function sanitiseFileName(name: string): string { + return name.replaceAll(':', '-'); +} + +/* ------------------------------------------------------------------ */ +/* Detection helpers */ +/* ------------------------------------------------------------------ */ + +/** + * Check whether a directory contains at least one `bmad*.prompt.md` file. + * + * @param dir - Absolute path to the directory. + * @returns `true` if BMAD Copilot prompt files are present. + */ +function hasBmadPromptFiles(dir: string): boolean { + if (!fsSync.existsSync(dir)) { + return false; + } + try { + const files = fsSync.readdirSync(dir); + return files.some((f) => f.startsWith('bmad') && f.endsWith('.prompt.md')); + } catch { + return false; + } +} + +/** + * Check whether claude-code prompt source files exist under `_bmad/`. + * + * @param bmadDir - Absolute path to the `_bmad` directory. + * @returns `true` if `_bmad/ide/claude-code/prompts/` contains files. + */ +export function hasClaudeCodeSource(bmadDir: string): boolean { + const srcDir = path.join(bmadDir, 'ide', 'claude-code', 'prompts'); + if (!fsSync.existsSync(srcDir)) { + return false; + } + try { + const files = fsSync.readdirSync(srcDir); + return files.some((f) => f.endsWith('.prompt.md')); + } catch { + return false; + } +} diff --git a/bmad-copilot/src/types.ts b/bmad-copilot/src/types.ts new file mode 100644 index 00000000..f291a346 --- /dev/null +++ b/bmad-copilot/src/types.ts @@ -0,0 +1,243 @@ +/** + * @fileoverview Shared type definitions for the BMAD Copilot Adapter. + * + * All interfaces mirror the CSV manifest schemas produced by + * `bmad-method` CLI v6 — no fields are invented. + * + * @module types + */ + +/* ------------------------------------------------------------------ */ +/* bmad-help.csv row */ +/* ------------------------------------------------------------------ */ + +/** + * Represents a single row from the merged `bmad-help.csv` catalog. + * + * CSV header (16 columns): + * ``` + * module, phase, name, code, sequence, workflow-file, command, + * required, agent-name, agent-command, agent-display-name, + * agent-title, options, description, output-location, outputs + * ``` + */ +export interface BmadHelpEntry { + [key: string]: string; + /** Module code the workflow belongs to (e.g. "bmm", "bmb"). Empty for universal/core tools. */ + module: string; + /** Lifecycle phase (e.g. "1-discover", "2-define"). Empty for anytime tools. */ + phase: string; + /** Human-readable workflow name (e.g. "Create PRD"). */ + name: string; + /** Short mnemonic code (e.g. "CP"). */ + code: string; + /** Numeric ordering within the phase. */ + sequence: string; + /** Relative path to the workflow/task file (e.g. "_bmad/bmm/workflows/…"). */ + 'workflow-file': string; + /** Slash-command name without leading `/` (e.g. "bmad-bmm-create-prd"). Empty for agent-only commands. */ + command: string; + /** Whether this workflow is required before proceeding ("true" / "false"). */ + required: string; + /** Internal agent name (e.g. "pm"). */ + 'agent-name': string; + /** CLI-style agent command (e.g. "bmad:bmm:agent:pm"). */ + 'agent-command': string; + /** Display name of the agent persona (e.g. "Paige"). */ + 'agent-display-name': string; + /** Agent title with icon (e.g. "📋 Project Manager"). */ + 'agent-title': string; + /** Comma-separated option flags (e.g. "#yolo"). */ + options: string; + /** One-line description of the workflow. */ + description: string; + /** Variable path pattern for output artifacts. */ + 'output-location': string; + /** Expected output filenames/patterns. */ + outputs: string; +} + +/* ------------------------------------------------------------------ */ +/* agent-manifest.csv row */ +/* ------------------------------------------------------------------ */ + +/** + * Represents a single row from `agent-manifest.csv`. + * + * CSV header (11 columns): + * ``` + * name, displayName, title, icon, capabilities, role, + * identity, communicationStyle, principles, module, path + * ``` + */ +export interface AgentManifestEntry { + [key: string]: string; + /** Internal agent identifier (e.g. "pm"). */ + name: string; + /** Public-facing persona name (e.g. "Paige"). */ + displayName: string; + /** Short role title (e.g. "Project Manager"). */ + title: string; + /** Emoji icon (e.g. "📋"). */ + icon: string; + /** Comma-separated capability keywords. */ + capabilities: string; + /** Persona role description. */ + role: string; + /** Persona identity statement. */ + identity: string; + /** Communication style tags. */ + communicationStyle: string; + /** Core principles. */ + principles: string; + /** Owning module code (e.g. "bmm", "core"). */ + module: string; + /** Relative file path to compiled agent markdown. */ + path: string; +} + +/* ------------------------------------------------------------------ */ +/* workflow-manifest.csv row */ +/* ------------------------------------------------------------------ */ + +/** + * Represents a single row from `workflow-manifest.csv`. + * + * CSV header (4 columns): `name, description, module, path` + */ +export interface WorkflowManifestEntry { + [key: string]: string; + /** Workflow identifier (e.g. "create-prd"). */ + name: string; + /** One-line description. */ + description: string; + /** Owning module code. */ + module: string; + /** Relative file path to the workflow definition. */ + path: string; +} + +/* ------------------------------------------------------------------ */ +/* task-manifest.csv / tool-manifest.csv row */ +/* ------------------------------------------------------------------ */ + +/** + * Represents a single row from `task-manifest.csv` or `tool-manifest.csv`. + * + * CSV header (6 columns): `name, displayName, description, module, path, standalone` + */ +export interface TaskToolManifestEntry { + [key: string]: string; + /** Task/tool identifier. */ + name: string; + /** Human-readable display name. */ + displayName: string; + /** One-line description. */ + description: string; + /** Owning module code. */ + module: string; + /** Relative file path. */ + path: string; + /** "true" if the task/tool can be invoked standalone. */ + standalone: string; +} + +/* ------------------------------------------------------------------ */ +/* Internal command map */ +/* ------------------------------------------------------------------ */ + +/** + * Describes a single BMAD command that has been resolved from the + * manifests and is ready for Copilot Chat dispatch. + */ +export interface BmadCommand { + /** Slash-command name as typed in Copilot Chat (without `/`). Example: "bmad-bmm-create-prd". */ + slashName: string; + /** Original CLI colon-syntax. Example: "bmad:bmm:create-prd". */ + cliSyntax: string; + /** One-line description shown to the user. */ + description: string; + /** Category for grouping. */ + category: 'workflow' | 'agent' | 'task' | 'tool' | 'core'; + /** Module code ("bmm", "core", …). */ + module: string; + /** Relative path to the workflow/agent/task file. */ + filePath: string; + /** Agent name required to run this command (empty for standalone tasks). */ + agentName: string; + /** Agent display title with icon. */ + agentTitle: string; + /** The execution pattern to use. */ + pattern: PromptPattern; + /** + * Absolute path to the official `.prompt.md` or `.agent.md` file + * generated by `bmad-method install --tools github-copilot`. + * When present, this file is used as the primary execution source. + */ + promptFilePath?: string; +} + +/** + * Prompt injection patterns — mirrors the four patterns used by the + * official GitHub Copilot installer in `github-copilot.js`: + * + * - **A** — MD workflows: load and follow directly + * - **B** — YAML workflows: load workflow.xml engine first, then config + * - **C** — Agent-only (tech-writer style): load agent, invoke code + * - **D** — Agent activator: load config, then agent file + * - **task** — XML/MD tasks: load and execute directly + */ +export type PromptPattern = 'md-workflow' | 'yaml-workflow' | 'agent-only' | 'agent-activator' | 'task'; + +/* ------------------------------------------------------------------ */ +/* Registry state */ +/* ------------------------------------------------------------------ */ + +/** + * Snapshot of everything discovered during a BMAD scan. + * + * This is the canonical runtime state — all other layers read from it. + */ +export interface RegistryState { + /** Absolute path to the `_bmad` directory. */ + bmadDir: string; + /** Map of slash-name → BmadCommand. */ + commands: Map; + /** Parsed rows from bmad-help.csv. */ + helpEntries: BmadHelpEntry[]; + /** Parsed rows from agent-manifest.csv. */ + agents: AgentManifestEntry[]; + /** Parsed rows from workflow-manifest.csv. */ + workflows: WorkflowManifestEntry[]; + /** Parsed rows from task-manifest.csv. */ + tasks: TaskToolManifestEntry[]; + /** Parsed rows from tool-manifest.csv. */ + tools: TaskToolManifestEntry[]; + /** Modules detected. */ + modules: string[]; + /** + * Map of slash-command name → absolute path to the official + * `.prompt.md` or `.agent.md` file in `.github/`. + */ + promptFiles: Map; + /** `true` if `.github/prompts/` or `.github/agents/` contain BMAD files. */ + hasCopilotFiles: boolean; + /** ISO timestamp of last scan. */ + lastScan: string; +} + +/* ------------------------------------------------------------------ */ +/* Extension configuration */ +/* ------------------------------------------------------------------ */ + +/** + * User-facing configuration surface from `contributes.configuration`. + */ +export interface BmadConfig { + /** Override path to `_bmad` directory. Empty string = auto-detect. */ + bmadDir: string; + /** Auto-scan on activation. */ + autoScan: boolean; + /** Verbose logging. */ + verbose: boolean; +} diff --git a/bmad-copilot/tsconfig.json b/bmad-copilot/tsconfig.json new file mode 100644 index 00000000..a8782cf0 --- /dev/null +++ b/bmad-copilot/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "Node16", + "target": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "Node16", + "outDir": "dist", + "rootDir": "src", + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "resolveJsonModule": true, + "newLine": "lf" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "out"] +}