From 1fa7529aef44886ee2896f5469c1881950112daa Mon Sep 17 00:00:00 2001 From: dadachi Date: Tue, 21 Apr 2026 17:14:44 +0900 Subject: [PATCH] Day 3b complete: Android worker finishes the three-platform pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/agents/workers/android.ts: copies $NATEMPLATE_ANDROID -> ./out//android, runs rename.rb, git init. Uses segment-based skip (Set-membership over each path segment) rather than startsWith, because Android has nested build/ dirs in every module (app/build, model/build, datastore-proto/build) that startsWith("/build/") wouldn't catch. - scripts/ruby/rename.rb: - TEXT_EXTS += .kt .kts .xml .gradle .pro .toml .properties .cfg - TEXT_BASENAMES += gradlew gradlew.bat gradle.properties local.properties - SKIP_DIR_SEGMENTS += build .gradle .idea .kotlin captures Verified — all three workers running in parallel via dispatch's Promise.all: clinic-queue (adapt, 2-pair rename): rails: 305 scanned, 134 changed, 2459 subs, 49 renames ios: 252 scanned, 101 changed, 2227 subs, 44 renames android: 271 scanned, 116 changed, 1854 subs, 49 renames task-tracker (replace, 3-pair rename, ItemTag->Task): rails: 305 scanned, 139 changed, 3110 subs, 65 renames ios: 252 scanned, 120 changed, 2950 subs, 77 renames android: 271 scanned, 131 changed, 2828 subs, 89 renames Zero [Ss]hop / [Ss]hopkeeper / ItemTag leftovers across all platforms in both specs (content + filename grep). Parallel wall time ~1s per worker. Build green-check (gradlew / xcodebuild / bin/dev) deferred to Day 5 Layer 2. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/ruby/rename.rb | 4 +- src/agents/workers/android.ts | 106 +++++++++++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/scripts/ruby/rename.rb b/scripts/ruby/rename.rb index 5f1287c..31e3875 100755 --- a/scripts/ruby/rename.rb +++ b/scripts/ruby/rename.rb @@ -17,18 +17,20 @@ stats = { files_scanned: 0, files_changed: 0, substitutions: 0, files_renamed: 0 } -SKIP_DIR_SEGMENTS = %w[.git node_modules tmp log DerivedData Pods Carthage xcuserdata .build].freeze +SKIP_DIR_SEGMENTS = %w[.git node_modules tmp log DerivedData Pods Carthage xcuserdata .build build .gradle .idea .kotlin captures].freeze SKIP_SUBPATHS = %w[vendor/bundle].freeze TEXT_EXTS = %w[ .rb .erb .yml .yaml .json .md .gemspec .rake .ru .txt .sample .example .conf .html .css .scss .js .mjs .tt .lock .swift .plist .strings .xcconfig .entitlements .pbxproj .xcworkspacedata .modulemap + .kt .kts .xml .gradle .pro .toml .properties .cfg ].freeze TEXT_BASENAMES = %w[ Gemfile Gemfile.lock Rakefile Procfile Procfile.dev .gitignore .env.sample .ruby-version .node-version config.ru Dockerfile Podfile Podfile.lock Package.swift Cartfile Makefile + gradlew gradlew.bat gradle.properties local.properties ].freeze def pluralize(word) diff --git a/src/agents/workers/android.ts b/src/agents/workers/android.ts index cdb6ab7..95a6175 100644 --- a/src/agents/workers/android.ts +++ b/src/agents/workers/android.ts @@ -1,10 +1,112 @@ +import { cp, lstat, mkdir, rm, stat } from "node:fs/promises"; +import { spawn } from "node:child_process"; +import { resolve } from "node:path"; import { trace } from "../../trace.js"; -import type { DomainSpec, WorkerResult } from "../types.js"; +import { isStub } from "../../stub.js"; +import { runRuby } from "../../ruby.js"; +import type { DomainSpec, RenamePair, WorkerResult } from "../types.js"; -const delay = (ms: number): Promise => new Promise((r) => { setTimeout(r, ms); }); +type RenameStats = { + files_scanned: number; + files_changed: number; + substitutions: number; + files_renamed: number; +}; + +const SKIP_SEGMENTS = new Set([ + ".git", + "build", + ".gradle", + ".idea", + ".kotlin", + "captures", + "node_modules", +]); export async function runAndroidWorker(domain: DomainSpec): Promise { + if (isStub("android")) { + return runStubAndroidWorker(domain); + } + + const substrate = process.env['NATEMPLATE_ANDROID']; + if (!substrate) { + throw new Error("android worker: NATEMPLATE_ANDROID env var is not set; see CLAUDE.md Substrate section"); + } + + const outDir = resolve(process.cwd(), "out", domain.slug, "android"); + + trace("android", `copying substrate from ${substrate} to ${outDir}`); + await prepareFresh(outDir); + await copyFiltered(substrate, outDir); + + const plan = domain.renamePlan.map((p) => `${p.from}->${p.to}`).join(", "); + trace("android", `running scripts/ruby/rename.rb: ${plan}`); + + const renameStats = await runRuby<{ renamePlan: readonly RenamePair[]; root: string }, RenameStats>( + "rename.rb", + { renamePlan: domain.renamePlan, root: outDir }, + ); + + trace( + "android", + `scanned ${renameStats.files_scanned} files, changed ${renameStats.files_changed}, ${renameStats.substitutions} substitutions, ${renameStats.files_renamed} file/dir renames`, + ); + + await initGit(outDir); + trace("android", `done (out/${domain.slug}/android)`); + + return { + platform: "android", + outDir: `./out/${domain.slug}/android`, + filesTouched: renameStats.files_changed + renameStats.files_renamed, + }; +} + +async function prepareFresh(dir: string): Promise { + try { + await stat(dir); + await rm(dir, { recursive: true, force: true }); + } catch { + // dest doesn't exist + } + await mkdir(dir, { recursive: true }); +} + +async function copyFiltered(src: string, dest: string): Promise { + await cp(src, dest, { + recursive: true, + force: true, + filter: async (source: string) => { + const rel = source.slice(src.length); + const segments = rel.split("/").filter(Boolean); + if (segments.some((seg) => SKIP_SEGMENTS.has(seg))) return false; + try { + const s = await lstat(source); + if (s.isSocket() || s.isFIFO() || s.isBlockDevice() || s.isCharacterDevice()) return false; + } catch { + return false; + } + return true; + }, + }); +} + +async function initGit(dir: string): Promise { + await new Promise((resolvePromise, rejectPromise) => { + const child = spawn("git", ["init", "-q", "-b", "main"], { cwd: dir }); + child.on("close", (code) => { + if (code === 0) resolvePromise(); + else rejectPromise(new Error(`git init exited ${code}`)); + }); + child.on("error", rejectPromise); + }); +} + +const delay = (ms: number): Promise => new Promise((r) => { setTimeout(r, ms); }); + +async function runStubAndroidWorker(domain: DomainSpec): Promise { const plan = domain.renamePlan.map((p) => `${p.from}->${p.to}`).join(", "); + trace("android", "(stub mode)"); trace("android", `copying Jetpack Compose substrate for ${domain.displayName}`); await delay(200); trace("android", `renaming Kotlin symbols: ${plan}`);