import { execFile } from "node:child_process"; import { access, readFile } from "node:fs/promises"; import { join } from "node:path"; import { categorizeRubyLspError } from "./workspace.js"; export type RailsRouteEntry = { name: string; verb: string; path: string; controller: string; action: string; }; export type RailsRoutesPlan = { command: string[]; cwd: string; reason: string; }; async function fileExists(path: string): Promise { try { await access(path); return true; } catch { return false; } } async function hasRailsGem(root: string): Promise { try { const lockfile = await readFile(join(root, "Gemfile.lock"), "utf8"); return /^\s{4}(rails|railties) \(/m.test(lockfile); } catch { return false; } } export function parseRailsRoutes(output: string): RailsRouteEntry[] { return output.split(/\r?\n/).flatMap((line) => { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("Prefix") || trimmed.startsWith("--")) return []; const match = trimmed.match(/^(?:(\S+)\s+)?(GET|POST|PATCH|PUT|DELETE|OPTIONS|HEAD|TRACE|CONNECT)\s+(\S+)\s+(?:\S+\s+)?([^#\s]+)#([^\s]+)$/); if (!match) return []; return [{ name: match[1] ?? "", verb: match[2], path: match[3], controller: match[4], action: match[5] }]; }); } export async function planRailsRoutesCommand(cwd: string, filter?: string): Promise { const hasBinRails = await fileExists(join(cwd, "bin", "rails")); const hasRailsMarker = hasBinRails || await fileExists(join(cwd, "config", "application.rb")) || await fileExists(join(cwd, "config", "routes.rb")) || await hasRailsGem(cwd); if (!hasRailsMarker) throw new Error("Rails project not detected. Expected bin/rails, config/application.rb, config/routes.rb, or Rails gems in Gemfile.lock."); const command = hasBinRails ? ["bin/rails", "routes"] : ["bundle", "exec", "rails", "routes"]; if (filter) command.push("-g", filter); return { command, cwd, reason: hasBinRails ? "bin/rails detected" : "Rails markers detected without bin/rails", }; } export async function runRailsRoutes(cwd: string, filter?: string): Promise { const plan = await planRailsRoutesCommand(cwd, filter); const [command, ...args] = plan.command; return new Promise((resolve, reject) => { const child = execFile(command, args, { cwd, timeout: Number(process.env.PI_RUBY_LSP_RAILS_TIMEOUT_MS ?? 15000), maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => { if (error) { const errorInfo = categorizeRubyLspError({ error: `Rails routes failed running '${plan.command.join(" ")}': ${error.message}`, root: cwd, command: plan.command, cwd, stderr }); reject(new Error(`${errorInfo.message}\n${errorInfo.suggestions.join("\n")}${stderr ? `\nstderr:\n${stderr}` : ""}`)); return; } const routes = parseRailsRoutes(stdout); resolve({ command: plan.command, filter, routes, raw: stdout, parseWarning: routes.length === 0 ? "Could not parse structured routes from output; inspect raw output." : undefined }); }); child.on("error", reject); }); }