playbook/outfitter-agents/plugins/outfitter/skills/check-status/scripts/gatherers/beads-gatherer.ts

196 lines
4.5 KiB
TypeScript
Executable File

#!/usr/bin/env bun
/**
* Beads gatherer for status
*
* Collects local issue data from .beads/ directory
* - Stats overview
* - In-progress work
* - Ready items (unblocked)
* - Blocked items with dependencies
* - Recently closed (filtered by time)
*/
import { parseArgs } from "node:util";
import { filterByTime, parseTimeConstraint } from "../lib/time";
import type {
BeadsData,
BeadsIssue,
BeadsStats,
GathererResult,
} from "../lib/types";
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
time: { type: "string", short: "t", default: "24h" },
workspace: { type: "string", short: "w" },
help: { type: "boolean", short: "h" },
},
});
if (values.help) {
console.log(`
beads-gatherer.ts - Gather beads issue data
Usage:
./beads-gatherer.ts [options]
Options:
-t, --time <constraint> Time constraint (24h, 7d, 2w) [default: 24h]
-w, --workspace <path> Workspace root [default: current directory]
-h, --help Show this help
Output:
JSON GathererResult with BeadsData
`);
process.exit(0);
}
/**
* Result of running a bd CLI command.
*/
interface BdOutput<T> {
success: boolean;
data?: T;
error?: string;
}
/**
* Runs a bd CLI command and parses JSON output.
* @param args - Arguments to pass to bd
* @returns Parsed output or error
*/
async function runBd<T>(args: string[]): Promise<BdOutput<T>> {
const workspaceArgs = values.workspace
? ["--workspace-root", values.workspace]
: [];
const proc = Bun.spawn(["bd", ...workspaceArgs, ...args, "--json"], {
stdout: "pipe",
stderr: "pipe",
});
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
const exitCode = await proc.exited;
if (exitCode !== 0) {
return {
success: false,
error: stderr || `bd exited with code ${exitCode}`,
};
}
try {
const data = JSON.parse(stdout);
return { success: true, data };
} catch {
return { success: false, error: `Failed to parse bd output: ${stdout}` };
}
}
/**
* Checks if Beads is initialized in workspace.
* @returns True if .beads directory exists
*/
async function checkBeadsAvailable(): Promise<boolean> {
// Check if .beads directory exists
const beadsDir = values.workspace ? `${values.workspace}/.beads` : ".beads";
const file = Bun.file(`${beadsDir}/issues.db`);
return file.exists();
}
/**
* Gathers Beads issue tracking data.
* @returns Gatherer result with Beads data
*/
async function gatherBeadsData(): Promise<GathererResult<BeadsData>> {
const timestamp = new Date().toISOString();
// Check if beads is available
const available = await checkBeadsAvailable();
if (!available) {
return {
source: "beads",
status: "unavailable",
reason: "Beads not initialized (.beads/ directory not found)",
timestamp,
};
}
// Parse time constraint
const timeValue = values.time ?? "24h";
let timeMs: number;
try {
timeMs = parseTimeConstraint(timeValue);
} catch (e) {
return {
source: "beads",
status: "error",
error: e instanceof Error ? e.message : "Invalid time constraint",
timestamp,
};
}
// Gather data in parallel
const [
statsResult,
inProgressResult,
readyResult,
blockedResult,
closedResult,
] = await Promise.all([
runBd<BeadsStats>(["stats"]),
runBd<BeadsIssue[]>(["list", "--status=in_progress", "--limit=10"]),
runBd<BeadsIssue[]>(["ready", "--limit=10"]),
runBd<BeadsIssue[]>(["blocked"]),
runBd<BeadsIssue[]>(["list", "--status=closed", "--limit=20"]),
]);
// Check for fatal errors (stats should always work if beads is available)
if (!statsResult.success) {
return {
source: "beads",
status: "error",
error: statsResult.error || "Failed to get beads stats",
timestamp,
};
}
// Build result, handling partial failures gracefully
const stats = statsResult.data ?? {
total: 0,
open: 0,
in_progress: 0,
blocked: 0,
closed: 0,
};
const inProgress = inProgressResult.success
? (inProgressResult.data ?? [])
: [];
const ready = readyResult.success ? (readyResult.data ?? []) : [];
const blocked = blockedResult.success ? (blockedResult.data ?? []) : [];
const closed = closedResult.success ? (closedResult.data ?? []) : [];
// Filter closed issues by time constraint (client-side)
const recentlyClosed = filterByTime(closed, timeMs);
return {
source: "beads",
status: "success",
data: {
stats,
inProgress,
ready,
blocked,
recentlyClosed,
},
timestamp,
};
}
// Main execution
const result = await gatherBeadsData();
console.log(JSON.stringify(result, null, 2));