From 5a127df709bcdbca4787fe59719348e524769a38 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Wed, 5 Aug 2026 10:52:36 +0700 Subject: [PATCH] feat(lanes): add ccam lanes sync-base CLI (E2) --- bin/ccam.js | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/bin/ccam.js b/bin/ccam.js index ebb3155..756476a 100755 --- a/bin/ccam.js +++ b/bin/ccam.js @@ -1900,6 +1900,71 @@ async function cmdLanesRuntime(sub, args) { `lane #${laneId} running hook ${name} — follow it with: ccam lanes logs ${laneId} ${name}` ); } + + if (sub === "sync-base") { + const mode = laneArgs.includes("--check") + ? "check" + : laneArgs.includes("--continue") + ? "continue" + : "merge"; + const branch = laneArgs.find((arg) => !arg.startsWith("--")); + const result = await post( + `/api/lanes/${laneId}/sync-base`, + { mode, branch }, + { allowError: true } + ); + if (result.status) { + console.error(`✖ sync-base → ${result.data?.error?.message || result.status}`); + process.exitCode = 1; + return; + } + + if (result.code === 5) { + console.error(`✖ lane #${laneId} — MIGRATION NUMBER COLLISION (nothing merged):`); + for (const c of result.collisions) { + console.error(` ${c.file} collides with ${c.collidesWith} — rename to ${c.suggestion}`); + } + process.exitCode = 5; + return; + } + + if (result.code === 4) { + console.error(`✖ lane #${laneId} — MERGE CONFLICT (left in place).`); + console.error(` conflicted: ${result.conflictedFiles.join(", ")}`); + console.error(" resolve, then: git add && git commit --no-edit"); + console.error( + ` then: ccam lanes sync-base --continue ${branch ? branch + " " : ""}${laneId}` + ); + process.exitCode = 4; + return; + } + + if (mode === "check") { + if (result.devDelta === null) { + console.log("DEV_DELTA: unknown (no merge-base with origin/development)"); + } else { + console.log( + `DEV_DELTA: ${result.devDelta.length} file(s) changed on origin/development since merge-base` + ); + for (const f of result.devDelta) console.log(` ${f}`); + if (result.overlap.length) { + console.log( + `DEV_OVERLAP: ${result.overlap.length} file(s) — the upstream delta touches the feature's files:` + ); + for (const f of result.overlap) console.log(` ${f}`); + } else { + console.log("DEV_OVERLAP: none"); + } + } + console.log(`lane #${laneId} preflight vs origin/development: OK`); + return; + } + + console.log( + `lane #${laneId} — synced with origin/development (re-enter the pipeline at the gates)` + ); + return; + } } /** @@ -2266,6 +2331,11 @@ const COMMAND_GROUPS = [ ["lanes runtime", "[]", "Slot, ports, service health and the last boot error"], ["lanes logs", "[] [--tail N]", "Tail one of the lane's hook or service logs"], ["lanes hook", "[] [args…]", "Run a profile hook (ci-gate, e2e, migrate, …)"], + [ + "lanes sync-base", + "[] [--check|--continue] [branch]", + "Fetch + migration-collision preflight, or merge origin/development into the feature branch (--check: read-only; --continue: finish after a resolved conflict; bare: merge, branch defaults to the lane's current branch)", + ], [ "lanes proof-link", "[]", @@ -3085,7 +3155,7 @@ async function runCommand(argv) { if (["reset", "remove", "purge"].includes(rest[0])) { return cmdLanesLifecycle(rest[0], rest.slice(1)); } - if (["up", "down", "runtime", "logs", "hook"].includes(rest[0])) { + if (["up", "down", "runtime", "logs", "hook", "sync-base"].includes(rest[0])) { return cmdLanesRuntime(rest[0], rest.slice(1)); } if (rest[0] === "proof-link") return cmdLanesProofLink(rest.slice(1));