#!/usr/bin/env bun /** * detect.ts - Detect available project tools * * Quick pre-flight check for sitrep service availability. * Returns JSON with boolean availability for each tool. * * Usage: * ./detect.ts # JSON output * ./detect.ts --format=text # Human-readable output */ import { parseArgs } from "node:util"; /** * Result of detecting available project tools. */ interface DetectResult { /** Whether Graphite CLI is available and initialized */ graphite: boolean; /** Whether GitHub CLI is available and authenticated */ github: boolean; /** Whether Linear MCP is available */ linear: boolean; /** Whether Beads issue tracking is initialized */ beads: boolean; /** Human-readable status details for each tool */ details: { graphite?: string; github?: string; linear?: string; beads?: string; }; } const { values } = parseArgs({ args: Bun.argv.slice(2), options: { format: { type: "string", short: "f", default: "json" }, help: { type: "boolean", short: "h" }, }, }); if (values.help) { console.log(` detect.ts - Detect available project tools for sitrep Usage: ./detect.ts [options] Options: -f, --format Output format: json, text [default: json] -h, --help Show this help Output: JSON object with boolean availability for each service: - graphite: gt CLI installed and initialized - github: gh CLI installed and authenticated - linear: Linear MCP available (checks for mcp tools) - beads: .beads/ directory exists in current project `); process.exit(0); } /** * Checks if a command exists in PATH. * @param cmd - Command name to check * @returns True if command is available */ async function commandExists(cmd: string): Promise { const proc = Bun.spawn(["command", "-v", cmd], { stdout: "pipe", stderr: "pipe", }); await proc.exited; return proc.exitCode === 0; } /** * Runs a command and captures output. * @param cmd - Command and arguments array * @returns Object with success status and combined output */ async function runCommand( cmd: string[], ): Promise<{ success: boolean; output: string }> { const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe", }); const stdout = await new Response(proc.stdout).text(); const stderr = await new Response(proc.stderr).text(); await proc.exited; return { success: proc.exitCode === 0, output: stdout || stderr, }; } /** * Detects Graphite CLI availability and initialization status. * @returns Availability and detail message */ async function detectGraphite(): Promise<{ available: boolean; detail?: string }> { if (!(await commandExists("gt"))) { return { available: false, detail: "gt CLI not installed" }; } // Check if initialized in this repo const { success } = await runCommand(["gt", "state"]); if (!success) { return { available: false, detail: "gt not initialized in this repo" }; } return { available: true, detail: "gt CLI ready" }; } /** * Detects GitHub CLI availability and authentication status. * @returns Availability and detail message */ async function detectGitHub(): Promise<{ available: boolean; detail?: string }> { if (!(await commandExists("gh"))) { return { available: false, detail: "gh CLI not installed" }; } // Check auth status const { success, output } = await runCommand(["gh", "auth", "status"]); if (!success) { return { available: false, detail: "gh not authenticated" }; } // Extract account info if available const match = output.match(/Logged in to .+ as (\S+)/); const user = match ? match[1] : "authenticated"; return { available: true, detail: `gh CLI ready (${user})` }; } /** * Detects Linear MCP availability. * @returns Availability and detail message */ async function detectLinear(): Promise<{ available: boolean; detail?: string }> { // Linear detection is tricky - we check for the MCP tool availability // In Claude Code context, this would be detected via tool availability // For script context, we check if claude CLI exists and has linear configured if (!(await commandExists("claude"))) { return { available: false, detail: "claude CLI not installed" }; } // We can't easily detect MCP availability from a script // Return unknown/check-at-runtime return { available: false, detail: "Linear MCP - check at runtime" }; } /** * Detects Beads issue tracking initialization. * @returns Availability and detail message */ async function detectBeads(): Promise<{ available: boolean; detail?: string }> { const beadsDir = Bun.file(".beads/metadata.json"); const exists = await beadsDir.exists(); if (!exists) { return { available: false, detail: ".beads/ not initialized" }; } return { available: true, detail: "beads initialized" }; } /** * Detects all available project tools in parallel. * @returns Detection results for all tools */ async function detect(): Promise { const [graphite, github, linear, beads] = await Promise.all([ detectGraphite(), detectGitHub(), detectLinear(), detectBeads(), ]); return { graphite: graphite.available, github: github.available, linear: linear.available, beads: beads.available, details: { graphite: graphite.detail, github: github.detail, linear: linear.detail, beads: beads.detail, }, }; } /** * Formats detection results as human-readable text. * @param result - Detection results to format * @returns Formatted text output */ function formatText(result: DetectResult): string { const lines: string[] = ["PROJECT TOOLS", ""]; const status = (available: boolean) => (available ? "✓" : "✗"); lines.push(`${status(result.graphite)} Graphite: ${result.details.graphite}`); lines.push(`${status(result.github)} GitHub: ${result.details.github}`); lines.push(`${status(result.linear)} Linear: ${result.details.linear}`); lines.push(`${status(result.beads)} Beads: ${result.details.beads}`); const available = [ result.graphite && "graphite", result.github && "github", result.linear && "linear", result.beads && "beads", ].filter(Boolean); lines.push(""); lines.push( available.length > 0 ? `Available: ${available.join(", ")}` : "No tools detected", ); return lines.join("\n"); } async function main() { const result = await detect(); if (values.format === "text") { console.log(formatText(result)); } else { console.log(JSON.stringify(result, null, 2)); } } main().catch((err) => { console.error("Error:", err.message); process.exit(1); });