From 359cc91de92dd0d63091ad382cd68e48bac8f28a Mon Sep 17 00:00:00 2001 From: dadachi Date: Tue, 21 Apr 2026 20:09:35 +0900 Subject: [PATCH] Day 4 #2: Rails worker auto-pins generated project to local ruby Substrate pins ruby 4.0.2; user's machine may have a nearby version (e.g. 4.0.1 via mise). Pre-fix, `bundle install` on the generated project hard-failed until the user manually relaxed the pin. Now the rails worker detects the locally-installed ruby (via `ruby -e "print RUBY_VERSION"`) and rewrites .ruby-version + any literal `ruby "X.Y.Z"` in Gemfile to match. Substrate's Gemfile actually uses `ruby file: ".ruby-version"`, so rewriting .ruby-version alone is enough in practice; the Gemfile regex remains as a defensive no-op for substrates that hard-code. If ruby isn't on PATH at all, skip the pin rewrite and let Day 5 Layer 2 surface the missing-ruby error. Verified: generated clinic-queue rails pins to 4.0.1, bundle install succeeds out of the box without manual edits. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/agents/workers/rails.ts | 42 ++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/agents/workers/rails.ts b/src/agents/workers/rails.ts index d6363d6..1bd3bac 100644 --- a/src/agents/workers/rails.ts +++ b/src/agents/workers/rails.ts @@ -1,4 +1,4 @@ -import { cp, lstat, mkdir, rm, stat } from "node:fs/promises"; +import { cp, lstat, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import { spawn } from "node:child_process"; import { resolve } from "node:path"; import { trace } from "../../trace.js"; @@ -47,6 +47,7 @@ export async function runRailsWorker(domain: DomainSpec): Promise `scanned ${renameStats.files_scanned} files, changed ${renameStats.files_changed}, ${renameStats.substitutions} substitutions, ${renameStats.files_renamed} file/dir renames`, ); + await pinToLocalRuby(outDir); await initGit(outDir); trace("rails", `done (out/${domain.slug}/rails)`); @@ -93,6 +94,45 @@ async function copyFiltered(src: string, dest: string): Promise { }); } +async function pinToLocalRuby(outDir: string): Promise { + const localVersion = await detectLocalRubyVersion(); + if (!localVersion) { + trace("rails", "ruby not found on PATH — skipping version pin rewrite"); + return; + } + + const rubyVersionPath = resolve(outDir, ".ruby-version"); + const gemfilePath = resolve(outDir, "Gemfile"); + + const pinned = (await readFile(rubyVersionPath, "utf8")).trim(); + if (pinned === localVersion) { + trace("rails", `ruby ${localVersion} already matches pin`); + return; + } + + await writeFile(rubyVersionPath, `${localVersion}\n`); + const gemfile = await readFile(gemfilePath, "utf8"); + const updated = gemfile.replace(/ruby\s+"[\d.]+"/g, `ruby "${localVersion}"`); + if (updated !== gemfile) { + await writeFile(gemfilePath, updated); + } + + trace("rails", `pinned ruby ${pinned} -> ${localVersion} (local)`); +} + +async function detectLocalRubyVersion(): Promise { + return new Promise((resolvePromise) => { + const child = spawn("ruby", ["-e", "print RUBY_VERSION"]); + const chunks: Buffer[] = []; + child.stdout.on("data", (c: Buffer) => chunks.push(c)); + child.on("close", (code) => { + if (code === 0) resolvePromise(Buffer.concat(chunks).toString("utf8").trim()); + else resolvePromise(null); + }); + child.on("error", () => resolvePromise(null)); + }); +} + async function initGit(dir: string): Promise { await new Promise((resolvePromise, rejectPromise) => { const child = spawn("git", ["init", "-q", "-b", "main"], { cwd: dir });