feat(lanes): add ccam lanes sync-base CLI (E2)

This commit is contained in:
2026-08-05 10:52:36 +07:00
parent 40b1d0e0fb
commit 5a127df709
+71 -1
View File
@@ -1900,6 +1900,71 @@ async function cmdLanesRuntime(sub, args) {
`lane #${laneId} running hook ${name} — follow it with: ccam lanes logs ${laneId} ${name}` `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 <resolved files> && 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", "[<id>]", "Slot, ports, service health and the last boot error"], ["lanes runtime", "[<id>]", "Slot, ports, service health and the last boot error"],
["lanes logs", "[<id>] <svc> [--tail N]", "Tail one of the lane's hook or service logs"], ["lanes logs", "[<id>] <svc> [--tail N]", "Tail one of the lane's hook or service logs"],
["lanes hook", "[<id>] <name> [args…]", "Run a profile hook (ci-gate, e2e, migrate, …)"], ["lanes hook", "[<id>] <name> [args…]", "Run a profile hook (ci-gate, e2e, migrate, …)"],
[
"lanes sync-base",
"[<id>] [--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", "lanes proof-link",
"[<id>]", "[<id>]",
@@ -3085,7 +3155,7 @@ async function runCommand(argv) {
if (["reset", "remove", "purge"].includes(rest[0])) { if (["reset", "remove", "purge"].includes(rest[0])) {
return cmdLanesLifecycle(rest[0], rest.slice(1)); 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)); return cmdLanesRuntime(rest[0], rest.slice(1));
} }
if (rest[0] === "proof-link") return cmdLanesProofLink(rest.slice(1)); if (rest[0] === "proof-link") return cmdLanesProofLink(rest.slice(1));