/** * start CLI execute — Execution logic, no validation */ import type { StartSpec, StartResult } from './types.js'; import { readConfig, resolveConfigPath } from '../lib/config.js'; import { buildFullBranchName, getBaseBranch, normalizeNameForType } from '../lib/branch.js'; import { getWorktreePath } from '../lib/worktree.js'; import { branchExists, fetch, checkoutNew, push, execGit } from '../lib/git.js'; import { writeAndCommitVersion } from '../lib/version.js'; import { isCompliantMigrationName } from '../lib/efcore.js'; /** * 4.x start-efcore-preflight (non-blocking): for release/hotfix, list tracked * migration files whose names don't follow the convention so the caller can * suggest `/efcore squash`. Returns the offending basenames. */ async function scanNonCompliantMigrations(cwd?: string): Promise { const r = await execGit(['ls-files'], cwd); if (r.exitCode !== 0) return []; const files = r.stdout .split('\n') .filter( (f) => /\/Migrations\//i.test(f) && f.endsWith('.cs') && !/\.Designer\.cs$/i.test(f) && !/ModelSnapshot\.cs$/i.test(f), ); return files.filter((f) => !isCompliantMigrationName(f)).map((f) => f.split(/[/\\]/).pop() || f); } export async function execute(spec: StartSpec, cwd?: string): Promise { try { const config = await readConfig((await resolveConfigPath(cwd)) ?? undefined); // Per-type naming: feature names → kebab; release/hotfix versions keep dots // (e.g. `3.53.0` stays `release/3.53.0`, never `release/3530`). const normalized = normalizeNameForType(spec.name, spec.type); const fullBranch = buildFullBranchName(normalized, spec.type, config); const baseBranch = getBaseBranch(spec.type, config); const warnings: string[] = []; if (await branchExists(fullBranch, false, cwd)) { return { success: false, error: `Branch ${fullBranch} already exists locally`, branch: fullBranch, branchType: spec.type, baseBranch, pushed: false, }; } if (await branchExists(fullBranch, true, cwd)) { return { success: false, error: `Branch ${fullBranch} already exists on remote`, branch: fullBranch, branchType: spec.type, baseBranch, pushed: false, }; } // EF Core migration-naming preflight (release/hotfix) — restored from 4.x, // surfaced as a non-blocking warning (the skill can offer /efcore squash). if (spec.type === 'release' || spec.type === 'hotfix') { const bad = await scanNonCompliantMigrations(cwd); if (bad.length > 0) { const shown = bad.slice(0, 5).join(', '); warnings.push( `EF Core: ${bad.length} migration(s) with non-compliant names — run /efcore squash before this ${spec.type}: ${shown}${bad.length > 5 ? ', …' : ''}`, ); } } // A release/hotfix carries the version it will release — materialized below. const releaseVersion = spec.type === 'release' || spec.type === 'hotfix' ? normalized : undefined; let versionSet = false; if (!spec.dryRun) { await fetch('origin', cwd); // Create the branch (worktree when enabled+resolvable, else flat checkout) // and resolve the working dir where it is checked out, so the version // commit lands ON the new branch. let workdir = cwd ?? process.cwd(); let worktreePath: string | undefined; const wtPath = config.worktrees.enabled ? getWorktreePath(fullBranch, config, cwd) : ''; if (wtPath) { const { createWorktree } = await import('../lib/worktree.js'); await createWorktree(fullBranch, wtPath, baseBranch, cwd); workdir = wtPath; worktreePath = wtPath; } else { // Create AND check out the branch in one op: `git checkout -b `. await checkoutNew(fullBranch, baseBranch, cwd); } // Model B: materialize the release/hotfix version in the sources (+ tracked // config) as the FIRST commit, so the branch carries exactly the version it // will tag at finish. Without this, the tag and package.json diverge and the // publish pipeline ships the wrong version. if (releaseVersion) { const set = await writeAndCommitVersion( workdir, releaseVersion, config.versioning.sources, `chore: set version to ${releaseVersion}`, ); versionSet = set.committed; if (set.warning) warnings.push(set.warning); if (set.error) warnings.push(`Could not set version ${releaseVersion}: ${set.error}`); else if (!set.committed) warnings.push(`Sources already at ${releaseVersion} — no version commit needed.`); } if (!spec.noPush) { await push(fullBranch, workdir, true); } return { success: true, branch: fullBranch, branchType: spec.type, baseBranch, worktreePath, pushed: !spec.noPush, versionSet, version: releaseVersion, warnings: warnings.length > 0 ? warnings : undefined, }; } return { success: true, branch: fullBranch, branchType: spec.type, baseBranch, pushed: false, versionSet: false, version: releaseVersion, warnings: warnings.length > 0 ? warnings : undefined, }; } catch (err: unknown) { return { success: false, error: (err as Error).message, branch: '', branchType: spec.type, baseBranch: '', pushed: false, }; } }