feat: add detailed JSDoc comments for functions in bmadIndex, chatHandler, commandParser, extension, and logger modules

This commit is contained in:
Your Name 2026-02-07 21:34:57 +08:00 committed by evil0119
parent 2b13abbfe1
commit 4b631f6dcb
5 changed files with 162 additions and 0 deletions

View File

@ -33,6 +33,10 @@ export interface BmadIndex {
// ─── Detection ──────────────────────────────────────────
/**
* Return the filesystem path of the first open workspace folder.
* @returns Absolute path, or `undefined` if no folder is open.
*/
function workspaceRoot(): string | undefined {
return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
}
@ -146,6 +150,12 @@ function extractWorkflowMeta(content: string): { name?: string; description?: st
// ─── Recursive file walker ──────────────────────────────
/**
* Recursively collect all file paths under {@link dir}.
* Silently skips directories that cannot be read.
* @param dir - Root directory to walk.
* @returns Array of absolute file paths.
*/
function walkAll(dir: string): string[] {
const results: string[] = [];
function walk(current: string): void {
@ -170,20 +180,43 @@ function walkAll(dir: string): string[] {
// ─── Name derivation ────────────────────────────────────
/**
* Test whether a filename matches the BMAD agent naming convention (`*.agent.yaml` / `*.agent.md`).
* @param filename - Base filename (no directory component).
* @returns `true` if the file is an agent definition.
*/
function isAgentFile(filename: string): boolean {
return /\.agent\.(yaml|md)$/i.test(filename);
}
/**
* Test whether a filename matches the BMAD workflow naming convention
* (`workflow.yaml`, `workflow.md`, `workflow-*.yaml`, `workflow-*.md`).
* @param filename - Base filename (no directory component).
* @returns `true` if the file is a workflow definition.
*/
function isWorkflowFile(filename: string): boolean {
// Matches: workflow.yaml, workflow.md, workflow-*.md, workflow-*.yaml
return /^workflow(-[\w-]+)?\.(yaml|md)$/i.test(filename);
}
/**
* Derive a short display name from an agent file path by stripping the `.agent.yaml/.md` suffix.
* @param filePath - Absolute path to the agent file.
* @returns Display name (e.g. `"analyst"`).
*/
function deriveAgentName(filePath: string): string {
const base = path.basename(filePath);
return base.replace(/\.agent\.(yaml|md)$/i, '');
}
/**
* Derive a display name for a workflow. Prefers the `name:` key from YAML
* metadata; falls back to stripping the `workflow-` prefix and extension.
* @param filePath - Absolute path to the workflow file.
* @param content - Raw file content (used for metadata extraction).
* @returns Display name (e.g. `"create-prd"`).
*/
function deriveWorkflowName(filePath: string, content: string): string {
// Prefer name from YAML metadata
const meta = extractWorkflowMeta(content);
@ -209,6 +242,12 @@ function inferModule(filePath: string, bmadRoot: string): string | undefined {
// ─── Build index ────────────────────────────────────────
/**
* Walk the BMAD root directory and build a complete index of agents and workflows.
* Reads each matching file to extract metadata. Deduplicates workflows by name.
* @param bmadRoot - Absolute path to the detected BMAD root directory.
* @returns A {@link BmadIndex} with sorted agent and workflow lists.
*/
export function buildIndex(bmadRoot: string): BmadIndex {
const wsRoot = workspaceRoot() ?? bmadRoot;
const allFiles = walkAll(bmadRoot);
@ -267,10 +306,18 @@ export function buildIndex(bmadRoot: string): BmadIndex {
let _index: BmadIndex | undefined;
let _watcher: vscode.FileSystemWatcher | undefined;
/**
* Return the current in-memory BMAD index without rebuilding.
* @returns The cached index, or `undefined` if not yet built.
*/
export function getIndex(): BmadIndex | undefined {
return _index;
}
/**
* Re-detect the BMAD root and rebuild the index from disk.
* @returns The newly built index, or `undefined` if no BMAD root was found.
*/
export function refreshIndex(): BmadIndex | undefined {
const root = detectBmadRoot();
if (!root) {
@ -281,6 +328,11 @@ export function refreshIndex(): BmadIndex | undefined {
return _index;
}
/**
* Register a file-system watcher that rebuilds the BMAD index when
* YAML/Markdown files in the workspace change. Changes are debounced (500 ms).
* @param ctx - Extension context for disposable management.
*/
export function startWatching(ctx: vscode.ExtensionContext): void {
const wsRoot = workspaceRoot();
if (!wsRoot) { return; }

View File

@ -6,6 +6,11 @@ import { logInfo, logWarn, logError } from './logger';
// ─── Dynamic help builder ───────────────────────────────
/**
* Build dynamic help Markdown that lists all slash commands and currently installed items.
* @param idx - Current BMAD index (may be `undefined` if not yet detected).
* @returns Formatted Markdown string.
*/
function buildHelpText(idx: BmadIndex | undefined): string {
let md = `## BMAD Copilot — Available Commands\n\n`;
md += `| Command | Description |\n`;
@ -42,6 +47,11 @@ function buildHelpText(idx: BmadIndex | undefined): string {
const KNOWN_SUBS = ['help', 'doctor', 'list', 'run', 'agents', 'workflows'];
/**
* Suggest the closest known slash command for a mistyped input using positional character overlap.
* @param input - The unrecognised command string the user typed.
* @returns The best-matching known command name (falls back to `"help"`).
*/
function suggestCommand(input: string): string {
let best = '';
let bestScore = 0;
@ -57,11 +67,19 @@ function suggestCommand(input: string): string {
// ─── Sub-command handlers ───────────────────────────────
/**
* Handle the `/help` command render dynamic help text.
* @param stream - Chat response stream to write Markdown into.
*/
function handleHelp(stream: vscode.ChatResponseStream): void {
const idx = getIndex() ?? refreshIndex();
stream.markdown(buildHelpText(idx));
}
/**
* Handle the `/doctor` command render BMAD installation diagnostics.
* @param stream - Chat response stream to write Markdown into.
*/
function handleDoctor(stream: vscode.ChatResponseStream): void {
const wsRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '(no workspace)';
const config = vscode.workspace.getConfiguration('bmad');
@ -102,6 +120,11 @@ function handleDoctor(stream: vscode.ChatResponseStream): void {
stream.markdown(md);
}
/**
* Handle the `/list` command display agents or workflows in a table.
* @param stream - Chat response stream.
* @param args - Remaining tokens (expects `["agents"]` or `["workflows"]`).
*/
function handleList(stream: vscode.ChatResponseStream, args: string[]): void {
const idx = getIndex() ?? refreshIndex();
if (!idx) {
@ -119,6 +142,13 @@ function handleList(stream: vscode.ChatResponseStream, args: string[]): void {
}
}
/**
* Render a Markdown table of BMAD items (agents or workflows).
* @param stream - Chat response stream.
* @param title - Section heading (e.g. `"Agents"`).
* @param items - Items to display.
* @param isAgent - `true` to show agent-specific columns (Title, Icon); `false` for workflow columns (Description).
*/
function listItems(stream: vscode.ChatResponseStream, title: string, items: BmadItem[], isAgent: boolean): void {
if (items.length === 0) {
stream.markdown(`## ${title}\n\n_No items found._\n`);
@ -141,6 +171,12 @@ function listItems(stream: vscode.ChatResponseStream, title: string, items: Bmad
stream.markdown(md);
}
/**
* Truncate a string to at most {@link max} characters, appending `'…'` if shortened.
* @param s - Input string.
* @param max - Maximum allowed length.
* @returns The (possibly truncated) string.
*/
function truncate(s: string, max: number): string {
return s.length > max ? s.slice(0, max - 1) + '…' : s;
}
@ -169,6 +205,14 @@ function safeFence(content: string): string {
// ─── Run agent / workflow ───────────────────────────────
/**
* Execute a resolved run target by injecting its definition into the LLM context.
* Falls back to displaying a copyable prompt if the Language Model API is unavailable.
* @param request - Original chat request (provides the language model).
* @param stream - Chat response stream for output.
* @param token - Cancellation token.
* @param resolved - The agent/workflow and task to execute.
*/
async function executeRun(
request: vscode.ChatRequest,
stream: vscode.ChatResponseStream,
@ -221,6 +265,15 @@ async function executeRun(
fallbackPrompt(stream, item, kind, fileContent, task);
}
/**
* Handle the `/run` command resolve args to an agent/workflow and execute it.
* Shows helpful feedback with closest-name suggestions on resolution failure.
* @param request - Original chat request.
* @param stream - Chat response stream.
* @param token - Cancellation token.
* @param args - Parsed tokens after `/run`.
* @returns A promise that resolves when execution is complete.
*/
function handleRun(
request: vscode.ChatRequest,
stream: vscode.ChatResponseStream,
@ -253,6 +306,15 @@ function handleRun(
return executeRun(request, stream, token, resolved);
}
/**
* Display the assembled prompt as a copyable code block when the LLM API is unavailable.
* Includes a "Copy Prompt" button bound to the clipboard command.
* @param stream - Chat response stream.
* @param item - The agent or workflow item.
* @param kind - `"agent"` or `"workflow"`.
* @param fileContent - Raw content of the definition file.
* @param task - User-supplied task description.
*/
function fallbackPrompt(
stream: vscode.ChatResponseStream,
item: BmadItem,
@ -275,6 +337,10 @@ function fallbackPrompt(
// ─── Main handler ───────────────────────────────────────
/**
* Main `@bmad` chat participant request handler.
* Routes incoming commands to the appropriate sub-handler and returns an empty result.
*/
export const chatHandler: vscode.ChatRequestHandler = async (
request: vscode.ChatRequest,
context: vscode.ChatContext,

View File

@ -128,10 +128,24 @@ export function resolveDirectKind(kind: ResolvedKind, args: string[], index: Bma
// ── Helpers ──
/**
* Test whether a token looks like a BMAD module name (e.g. `"bmm"`, `"core"`).
* Rejects known command keywords so they are not confused with modules.
* @param s - Lowercased token to check.
* @returns `true` if the token is a plausible module name.
*/
function isModuleName(s: string): boolean {
return /^[a-z][a-z0-9_-]*$/.test(s) && !['agent', 'workflow', 'a', 'w', 'agents', 'workflows'].includes(s);
}
/**
* Look up a named item of the given kind in the index.
* @param kind - `'agent'` or `'workflow'`.
* @param args - Remaining tokens: `[name, ...taskWords]`.
* @param index - Current BMAD index.
* @param moduleFilter - Optional module name to narrow the search.
* @returns Resolved target, or `undefined` if no match is found.
*/
function findItem(kind: ResolvedKind, args: string[], index: BmadIndex, moduleFilter?: string): ResolvedRunTarget | undefined {
if (args.length === 0) { return undefined; }
const name = args[0].toLowerCase();
@ -146,6 +160,12 @@ function findItem(kind: ResolvedKind, args: string[], index: BmadIndex, moduleFi
return { kind, item, task };
}
/**
* Auto-resolve a name to an agent or workflow (agent takes priority).
* @param name - Item name to look up (case-insensitive).
* @param index - Current BMAD index.
* @returns The matched kind and item, or `undefined`.
*/
function findByName(name: string, index: BmadIndex): { kind: ResolvedKind; item: BmadItem } | undefined {
const n = name.toLowerCase();
const agent = index.agents.find(a => a.name.toLowerCase() === n);

View File

@ -3,6 +3,11 @@ import { initLogger, logInfo } from './logger';
import { refreshIndex, startWatching } from './bmadIndex';
import { chatHandler } from './chatHandler';
/**
* Extension entry point registers the `@bmad` chat participant,
* builds the initial BMAD index, and wires up file/config watchers.
* @param context - VS Code extension context for lifecycle management.
*/
export function activate(context: vscode.ExtensionContext): void {
// ── Logger ──
initLogger(context);
@ -45,6 +50,7 @@ export function activate(context: vscode.ExtensionContext): void {
logInfo('BMAD Copilot extension activated.');
}
/** Extension teardown. Cleanup is handled automatically by `context.subscriptions`. */
export function deactivate(): void {
// Cleanup handled by context.subscriptions
}

View File

@ -3,24 +3,42 @@ import * as vscode from 'vscode';
const CHANNEL_NAME = 'BMAD Copilot';
let _channel: vscode.OutputChannel | undefined;
/**
* Initialise the shared output channel for extension logging.
* @param ctx - Extension context whose subscriptions manage the channel lifecycle.
* @returns The created {@link vscode.OutputChannel}.
*/
export function initLogger(ctx: vscode.ExtensionContext): vscode.OutputChannel {
_channel = vscode.window.createOutputChannel(CHANNEL_NAME);
ctx.subscriptions.push(_channel);
return _channel;
}
/** @returns Current time as `HH:MM:SS.mmm` for log line prefixes. */
function ts(): string {
return new Date().toISOString().slice(11, 23);
}
/**
* Write an INFO-level message to the output channel.
* @param msg - The message to log.
*/
export function logInfo(msg: string): void {
_channel?.appendLine(`[${ts()}] INFO ${msg}`);
}
/**
* Write a WARN-level message to the output channel.
* @param msg - The message to log.
*/
export function logWarn(msg: string): void {
_channel?.appendLine(`[${ts()}] WARN ${msg}`);
}
/**
* Write an ERROR-level message to the output channel.
* @param msg - The message to log.
*/
export function logError(msg: string): void {
_channel?.appendLine(`[${ts()}] ERROR ${msg}`);
}