#!/usr/bin/env bun /** * Analyze upstream changes without merging * * Generates a detailed conflict report for a target upstream version. * * Usage: * bun run script/upstream/analyze.ts --version v1.1.49 * bun run script/upstream/analyze.ts --commit abc123 * bun run script/upstream/analyze.ts --version v1.1.49 --base-branch catrielmuller/kilo-opencode-v1.1.44 */ import { $ } from "bun" import * as git from "./utils/git" import * as version from "./utils/version" import * as report from "./utils/report" import { defaultConfig, loadConfig } from "./utils/config" import { header, info, success, warn, error, divider, list } from "./utils/logger" interface AnalyzeOptions { version?: string commit?: string output?: string baseBranch?: string } function parseArgs(): AnalyzeOptions { const args = process.argv.slice(2) const options: AnalyzeOptions = {} const versionIdx = args.indexOf("--version") if (versionIdx !== -1 && args[versionIdx + 1]) { options.version = args[versionIdx + 1] } const commitIdx = args.indexOf("--commit") if (commitIdx !== -1 && args[commitIdx + 1]) { options.commit = args[commitIdx + 1] } const outputIdx = args.indexOf("--output") if (outputIdx !== -1 && args[outputIdx + 1]) { options.output = args[outputIdx + 1] } const baseBranchIdx = args.indexOf("--base-branch") if (baseBranchIdx !== -1 && args[baseBranchIdx + 1]) { options.baseBranch = args[baseBranchIdx + 1] } return options } async function main() { const options = parseArgs() const config = loadConfig(options.baseBranch ? { baseBranch: options.baseBranch } : undefined) header("Upstream Change Analysis") // Check upstream remote if (!(await git.hasUpstreamRemote())) { error("No 'upstream' remote found. Please add it:") info(" git remote add upstream git@github.com:anomalyco/opencode.git") process.exit(1) } // Fetch upstream info("Fetching upstream...") await git.fetchUpstream() // Determine target let target: version.VersionInfo | null = null if (options.commit) { target = await version.getVersionForCommit(options.commit) if (!target) { target = { version: "unknown", tag: "unknown", commit: options.commit } } } else if (options.version) { const versions = await version.getAvailableUpstreamVersions() target = versions.find((v) => v.version === options.version || v.tag === options.version) || null if (!target) { error(`Version ${options.version} not found`) info("Use 'bun run script/upstream/list-versions.ts' to see available versions") process.exit(1) } } else { target = await version.getLatestUpstreamVersion() } if (!target) { error("Could not determine target version") process.exit(1) } success(`Analyzing: ${target.tag} (${target.commit.slice(0, 8)})`) divider() // Analyze conflicts info("Analyzing changes...") // Use commit hash directly since we may not have the tag fetched locally const conflicts = await report.analyzeConflicts(target.commit, config.baseBranch, config.keepOurs) // Group by type const byType = new Map() for (const c of conflicts) { const list = byType.get(c.type) || [] list.push(c) byType.set(c.type, list) } // Group by recommendation const byRec = new Map() for (const c of conflicts) { const list = byRec.get(c.recommendation) || [] list.push(c) byRec.set(c.recommendation, list) } console.log() success(`Total files changed: ${conflicts.length}`) console.log() // By type info("Changes by type:") for (const [type, files] of byType) { console.log(` ${type.padEnd(12)} ${files.length}`) } console.log() // By recommendation info("Changes by recommendation:") for (const [rec, files] of byRec) { const label = rec === "keep-ours" ? "Keep Kilo's" : rec === "codemod" ? "Auto-transform" : rec === "keep-theirs" ? "Take upstream" : "Manual review" console.log(` ${label.padEnd(16)} ${files.length}`) } // Show manual review files const manual = byRec.get("manual") || [] if (manual.length > 0 && manual.length <= 20) { console.log() warn("Files requiring manual review:") list(manual.map((f) => f.path)) } // Generate report const conflictReport: report.ConflictReport = { timestamp: new Date().toISOString(), upstreamVersion: target.version, upstreamCommit: target.commit, baseBranch: config.baseBranch, mergeBranch: "", totalConflicts: conflicts.length, conflicts, recommendations: [], } if (manual.length > 0) { conflictReport.recommendations.push(`${manual.length} files require manual review`) } const codemodFiles = byRec.get("codemod") || [] if (codemodFiles.length > 0) { conflictReport.recommendations.push(`${codemodFiles.length} files will be auto-transformed`) } const keepOursFiles = byRec.get("keep-ours") || [] if (keepOursFiles.length > 0) { conflictReport.recommendations.push(`${keepOursFiles.length} files will keep Kilo's version`) } // Save report const outputPath = options.output || `upstream-analysis-${target.version}.md` await report.saveReport(conflictReport, outputPath) divider() success(`Report saved to ${outputPath}`) console.log() info("Next steps:") info(" 1. Review the report") info(" 2. Run merge with: bun run script/upstream/merge.ts --version " + target.tag) } main().catch((err) => { error(`Error: ${err}`) process.exit(1) })