@kb-labs/commit-core 2.81.0 → 2.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -175,11 +175,7 @@ ${commit.body}`;
175
175
  return message;
176
176
  }
177
177
  async function checkStaleness(cwd, plan, _scope) {
178
- const planFiles = /* @__PURE__ */ new Set([
179
- ...plan.gitStatus.staged,
180
- ...plan.gitStatus.unstaged,
181
- ...plan.gitStatus.untracked
182
- ]);
178
+ const planFiles = new Set(plan.commits.flatMap((c) => c.files));
183
179
  if (planFiles.size === 0) {
184
180
  return { isStale: false, reason: "" };
185
181
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/analyzer/git-status.ts","../../src/applier/apply.ts","../../src/applier/push.ts"],"names":["simpleGit"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Git status analysis\n */\n\nimport { simpleGit, type SimpleGit, type StatusResult } from \"simple-git\";\nimport type { GitStatus } from \"@kb-labs/commit-contracts\";\n\n/**\n * Get current git status (staged, unstaged, untracked files).\n * cwd must already point to the resolved scope directory.\n */\nexport async function getGitStatus(cwd: string): Promise<GitStatus> {\n // --ignore-submodules=all: exclude submodule pointer drift in worktrees\n const git: SimpleGit = simpleGit(cwd);\n const status: StatusResult = await git.status(['--ignore-submodules=all']);\n\n return {\n staged: status.staged.filter((f) => !shouldIgnoreFile(f)),\n unstaged: [...status.modified, ...status.deleted]\n .filter((f) => !status.staged.includes(f))\n .filter((f) => !shouldIgnoreFile(f)),\n untracked: status.not_added.filter((f) => !shouldIgnoreFile(f)),\n };\n}\n\n/**\n * Check if file should be ignored (node_modules, dist, etc.)\n */\nfunction shouldIgnoreFile(file: string): boolean {\n const ignoredPaths = [\n \"node_modules/\",\n \".git/\",\n \"dist/\",\n \"build/\",\n \".next/\",\n \".turbo/\",\n \"coverage/\",\n \".cache/\",\n \".temp/\",\n \"tmp/\",\n ];\n\n return ignoredPaths.some((path) => file.includes(path));\n}\n\n/**\n * Get all changed files (staged + unstaged + untracked)\n * Filters out node_modules and other build artifacts\n */\nexport function getAllChangedFiles(status: GitStatus): string[] {\n const allFiles = [\n ...new Set([...status.staged, ...status.unstaged, ...status.untracked]),\n ];\n return allFiles.filter((file) => !shouldIgnoreFile(file));\n}\n\n/**\n * Check if there are any changes\n */\nexport function hasChanges(status: GitStatus): boolean {\n return (\n status.staged.length > 0 ||\n status.unstaged.length > 0 ||\n status.untracked.length > 0\n );\n}\n\n/**\n * Get current branch name\n */\nexport async function getCurrentBranch(cwd: string): Promise<string> {\n const git: SimpleGit = simpleGit(cwd);\n const branch = await git.revparse([\"--abbrev-ref\", \"HEAD\"]);\n return branch.trim();\n}\n\n/**\n * Check if branch is protected (main/master)\n */\nexport function isProtectedBranch(branch: string): boolean {\n const protectedBranches = [\n \"main\",\n \"master\",\n \"develop\",\n \"release\",\n \"production\",\n ];\n return protectedBranches.includes(branch.toLowerCase());\n}\n","/**\n * Commit plan applier\n */\n\n/* eslint-disable no-await-in-loop -- Sequential git commits required: must stage files and commit one group at a time */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { simpleGit, type SimpleGit } from \"simple-git\";\nimport type {\n CommitPlan,\n ApplyResult,\n CommitGroup,\n} from \"@kb-labs/commit-contracts\";\nimport type { ApplyOptions } from \"../types\";\nimport { getGitStatus, getAllChangedFiles } from \"../analyzer/git-status\";\n\n/**\n * Apply a commit plan - creates local git commits\n *\n * @param cwd - Working directory (repo root)\n * @param plan - Commit plan to apply\n * @param options - Apply options\n * @returns Apply result with created commit SHAs\n */\nexport async function applyCommitPlan(\n cwd: string,\n plan: CommitPlan,\n options?: ApplyOptions,\n): Promise<ApplyResult> {\n const appliedCommits: ApplyResult[\"appliedCommits\"] = [];\n const errors: string[] = [];\n\n // 0. Validate plan integrity before touching git state.\n const integrityErrors = validatePlanIntegrity(plan);\n if (integrityErrors.length > 0) {\n return {\n success: false,\n appliedCommits: [],\n errors: integrityErrors,\n };\n }\n\n // 1. Check for staleness (only for files in the plan, not entire repo)\n if (!options?.force) {\n const staleness = await checkStaleness(cwd, plan, options?.scope);\n if (staleness.isStale) {\n return {\n success: false,\n appliedCommits: [],\n errors: [staleness.reason],\n };\n }\n }\n\n // 2. Apply each commit in order\n for (const commit of plan.commits) {\n try {\n const sha = await applyCommit(cwd, commit);\n appliedCommits.push({\n groupId: commit.id,\n sha,\n message: formatCommitMessage(commit),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n errors.push(`Failed to apply commit ${commit.id}: ${message}`);\n\n // Stop on first error - don't leave repo in inconsistent state\n break;\n }\n }\n\n return {\n success: errors.length === 0,\n appliedCommits,\n errors,\n };\n}\n\nfunction validatePlanIntegrity(plan: CommitPlan): string[] {\n const errors: string[] = [];\n const seenInCommit = new Map<string, string>();\n\n for (const commit of plan.commits) {\n if (commit.files.length === 0) {\n errors.push(`Commit ${commit.id} has no files`);\n continue;\n }\n\n for (const file of commit.files) {\n const firstCommit = seenInCommit.get(file);\n if (firstCommit) {\n errors.push(\n `File appears in multiple commits: ${file} (first: ${firstCommit}, duplicate: ${commit.id})`,\n );\n } else {\n seenInCommit.set(file, commit.id);\n }\n }\n }\n\n return errors;\n}\n\n/**\n * Apply a single commit\n * Supports nested git repositories - files with 'nested-repo/...' prefix will be committed in the nested repo\n */\nasync function applyCommit(cwd: string, commit: CommitGroup): Promise<string> {\n // Group files by repository (root vs nested)\n const filesByRepo = groupFilesByRepo(cwd, commit.files);\n\n // For now, we only support commits within a single repo\n // If files span multiple repos, we need to handle that differently\n const repos = Array.from(filesByRepo.keys());\n\n if (repos.length > 1) {\n throw new Error(\n \"Commit spans multiple repositories. Split into separate commits.\",\n );\n }\n\n const [repoPath, fileInfos] = Array.from(filesByRepo.entries())[0]!;\n const git: SimpleGit = simpleGit(repoPath);\n\n // CRITICAL: Reset staging area first to ensure we only commit the specific files for this commit\n // Without this, git.commit() would commit ALL staged files, not just the ones we want\n await git.reset([\"--\"]);\n\n // Stage only the files for this specific commit (using paths relative to the repo)\n for (const { relativePath } of fileInfos) {\n await git.add(relativePath);\n }\n\n // Create commit with only the staged files\n const message = formatCommitMessage(commit);\n const result = await git.commit(message);\n\n return result.commit;\n}\n\n/**\n * Group files by their git repository (root or nested)\n */\nfunction groupFilesByRepo(\n cwd: string,\n files: string[],\n): Map<string, { relativePath: string; originalPath: string }[]> {\n const filesByRepo = new Map<\n string,\n { relativePath: string; originalPath: string }[]\n >();\n\n for (const file of files) {\n // Check if file is in a nested repo (first segment might be a git repo)\n const segments = file.split(\"/\");\n const potentialRepoDir = segments[0];\n\n // Handle edge case: empty file path or no segments\n if (!potentialRepoDir) {\n const group = filesByRepo.get(cwd) ?? [];\n group.push({ relativePath: file, originalPath: file });\n filesByRepo.set(cwd, group);\n continue;\n }\n\n const potentialRepoPath = join(cwd, potentialRepoDir);\n const potentialGitDir = join(potentialRepoPath, \".git\");\n\n // Check if it's actually a nested git repo\n const isNestedRepo = existsSync(potentialGitDir);\n\n if (isNestedRepo) {\n // Use nested repo as git root, strip first segment from path\n const relativePath = segments.slice(1).join(\"/\");\n const group = filesByRepo.get(potentialRepoPath) ?? [];\n group.push({ relativePath, originalPath: file });\n filesByRepo.set(potentialRepoPath, group);\n } else {\n // Use cwd as git root\n const group = filesByRepo.get(cwd) ?? [];\n group.push({ relativePath: file, originalPath: file });\n filesByRepo.set(cwd, group);\n }\n }\n\n return filesByRepo;\n}\n\n/** Commit footer for branding */\nconst COMMIT_FOOTER = \"\\n\\nšŸ¤– Generated by kb-labs-commit-plugin\";\n\n/**\n * Format commit message following conventional commits\n */\nexport function formatCommitMessage(\n commit: CommitGroup,\n options?: { includeFooter?: boolean },\n): string {\n const type = commit.type;\n const scope = commit.scope ? `(${commit.scope})` : \"\";\n const breaking = commit.breaking ? \"!\" : \"\";\n const subject = commit.message;\n\n let message = `${type}${scope}${breaking}: ${subject}`;\n\n // Add body if present\n if (commit.body) {\n message += `\\n\\n${commit.body}`;\n }\n\n // Add branding footer (default: true)\n if (options?.includeFooter !== false) {\n message += COMMIT_FOOTER;\n }\n\n return message;\n}\n\n/**\n * Check if files in the plan have changed since plan generation\n * Only checks files that are part of the plan, ignoring other changes in the repo\n */\nasync function checkStaleness(\n cwd: string,\n plan: CommitPlan,\n _scope?: string,\n): Promise<{ isStale: boolean; reason: string }> {\n const planFiles = new Set([\n ...plan.gitStatus.staged,\n ...plan.gitStatus.unstaged,\n ...plan.gitStatus.untracked,\n ]);\n\n // If no files in plan, nothing to check\n if (planFiles.size === 0) {\n return { isStale: false, reason: \"\" };\n }\n\n // Determine which repo(s) we need to check\n const filesByRepo = groupFilesByRepo(cwd, [...planFiles]);\n\n // Check each repo for staleness\n for (const [repoPath, fileInfos] of filesByRepo) {\n // Get current git status from the repo\n // Note: repoPath already points to the correct git repository root\n const currentStatus = await getGitStatus(repoPath);\n const currentFiles = new Set(getAllChangedFiles(currentStatus));\n\n // Check that all expected files are still changed\n for (const { relativePath, originalPath } of fileInfos) {\n if (!currentFiles.has(relativePath)) {\n return {\n isStale: true,\n reason: `File no longer has changes: ${originalPath}. Regenerate plan or use --force.`,\n };\n }\n }\n }\n\n return { isStale: false, reason: \"\" };\n}\n","/**\n * Git push operations\n */\n\nimport { simpleGit, type SimpleGit } from 'simple-git';\nimport type { PushResult } from '@kb-labs/commit-contracts';\nimport type { PushOptions } from '../types';\nimport { getCurrentBranch, isProtectedBranch } from '../analyzer/git-status';\n\n/**\n * Push commits to remote repository\n *\n * @param cwd - Working directory (repo root)\n * @param options - Push options\n * @returns Push result\n */\nexport async function pushCommits(cwd: string, options?: PushOptions): Promise<PushResult> {\n const git: SimpleGit = simpleGit(cwd);\n const remote = options?.remote || 'origin';\n\n try {\n // Get current branch\n const branch = await getCurrentBranch(cwd);\n\n // Warn about protected branches with force push\n if (options?.force && isProtectedBranch(branch)) {\n return {\n success: false,\n remote,\n branch,\n commitsPushed: 0,\n error: `Refusing to force push to protected branch '${branch}'. This is dangerous and disabled by default.`,\n };\n }\n\n // Check how many commits ahead of remote\n const commitsToPush = await countCommitsToPush(git, remote, branch);\n\n if (commitsToPush === 0) {\n return {\n success: true,\n remote,\n branch,\n commitsPushed: 0,\n };\n }\n\n // Push to remote\n const pushOptions = options?.force ? ['--force'] : [];\n await git.push(remote, branch, pushOptions);\n\n return {\n success: true,\n remote,\n branch,\n commitsPushed: commitsToPush,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n const branch = await getCurrentBranch(cwd).catch(() => 'unknown');\n\n return {\n success: false,\n remote,\n branch,\n commitsPushed: 0,\n error: message,\n };\n }\n}\n\n/**\n * Count commits ahead of remote\n */\nasync function countCommitsToPush(\n git: SimpleGit,\n remote: string,\n branch: string\n): Promise<number> {\n try {\n // First fetch to ensure we have latest remote refs\n await git.fetch(remote, branch);\n\n // Count commits between remote and local\n const result = await git.raw([\n 'rev-list',\n '--count',\n `${remote}/${branch}..HEAD`,\n ]);\n\n return parseInt(result.trim(), 10) || 0;\n } catch {\n // If remote doesn't have the branch, all local commits need pushing\n try {\n const result = await git.raw(['rev-list', '--count', 'HEAD']);\n return parseInt(result.trim(), 10) || 0;\n } catch {\n return 0;\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/analyzer/git-status.ts","../../src/applier/apply.ts","../../src/applier/push.ts"],"names":["simpleGit"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Git status analysis\n */\n\nimport { simpleGit, type SimpleGit, type StatusResult } from \"simple-git\";\nimport type { GitStatus } from \"@kb-labs/commit-contracts\";\n\n/**\n * Get current git status (staged, unstaged, untracked files).\n * cwd must already point to the resolved scope directory.\n */\nexport async function getGitStatus(cwd: string): Promise<GitStatus> {\n // --ignore-submodules=all: exclude submodule pointer drift in worktrees\n const git: SimpleGit = simpleGit(cwd);\n const status: StatusResult = await git.status(['--ignore-submodules=all']);\n\n return {\n staged: status.staged.filter((f) => !shouldIgnoreFile(f)),\n unstaged: [...status.modified, ...status.deleted]\n .filter((f) => !status.staged.includes(f))\n .filter((f) => !shouldIgnoreFile(f)),\n untracked: status.not_added.filter((f) => !shouldIgnoreFile(f)),\n };\n}\n\n/**\n * Check if file should be ignored (node_modules, dist, etc.)\n */\nfunction shouldIgnoreFile(file: string): boolean {\n const ignoredPaths = [\n \"node_modules/\",\n \".git/\",\n \"dist/\",\n \"build/\",\n \".next/\",\n \".turbo/\",\n \"coverage/\",\n \".cache/\",\n \".temp/\",\n \"tmp/\",\n ];\n\n return ignoredPaths.some((path) => file.includes(path));\n}\n\n/**\n * Get all changed files (staged + unstaged + untracked)\n * Filters out node_modules and other build artifacts\n */\nexport function getAllChangedFiles(status: GitStatus): string[] {\n const allFiles = [\n ...new Set([...status.staged, ...status.unstaged, ...status.untracked]),\n ];\n return allFiles.filter((file) => !shouldIgnoreFile(file));\n}\n\n/**\n * Check if there are any changes\n */\nexport function hasChanges(status: GitStatus): boolean {\n return (\n status.staged.length > 0 ||\n status.unstaged.length > 0 ||\n status.untracked.length > 0\n );\n}\n\n/**\n * Get current branch name\n */\nexport async function getCurrentBranch(cwd: string): Promise<string> {\n const git: SimpleGit = simpleGit(cwd);\n const branch = await git.revparse([\"--abbrev-ref\", \"HEAD\"]);\n return branch.trim();\n}\n\n/**\n * Check if branch is protected (main/master)\n */\nexport function isProtectedBranch(branch: string): boolean {\n const protectedBranches = [\n \"main\",\n \"master\",\n \"develop\",\n \"release\",\n \"production\",\n ];\n return protectedBranches.includes(branch.toLowerCase());\n}\n","/**\n * Commit plan applier\n */\n\n/* eslint-disable no-await-in-loop -- Sequential git commits required: must stage files and commit one group at a time */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { simpleGit, type SimpleGit } from \"simple-git\";\nimport type {\n CommitPlan,\n ApplyResult,\n CommitGroup,\n} from \"@kb-labs/commit-contracts\";\nimport type { ApplyOptions } from \"../types\";\nimport { getGitStatus, getAllChangedFiles } from \"../analyzer/git-status\";\n\n/**\n * Apply a commit plan - creates local git commits\n *\n * @param cwd - Working directory (repo root)\n * @param plan - Commit plan to apply\n * @param options - Apply options\n * @returns Apply result with created commit SHAs\n */\nexport async function applyCommitPlan(\n cwd: string,\n plan: CommitPlan,\n options?: ApplyOptions,\n): Promise<ApplyResult> {\n const appliedCommits: ApplyResult[\"appliedCommits\"] = [];\n const errors: string[] = [];\n\n // 0. Validate plan integrity before touching git state.\n const integrityErrors = validatePlanIntegrity(plan);\n if (integrityErrors.length > 0) {\n return {\n success: false,\n appliedCommits: [],\n errors: integrityErrors,\n };\n }\n\n // 1. Check for staleness (only for files in the plan, not entire repo)\n if (!options?.force) {\n const staleness = await checkStaleness(cwd, plan, options?.scope);\n if (staleness.isStale) {\n return {\n success: false,\n appliedCommits: [],\n errors: [staleness.reason],\n };\n }\n }\n\n // 2. Apply each commit in order\n for (const commit of plan.commits) {\n try {\n const sha = await applyCommit(cwd, commit);\n appliedCommits.push({\n groupId: commit.id,\n sha,\n message: formatCommitMessage(commit),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n errors.push(`Failed to apply commit ${commit.id}: ${message}`);\n\n // Stop on first error - don't leave repo in inconsistent state\n break;\n }\n }\n\n return {\n success: errors.length === 0,\n appliedCommits,\n errors,\n };\n}\n\nfunction validatePlanIntegrity(plan: CommitPlan): string[] {\n const errors: string[] = [];\n const seenInCommit = new Map<string, string>();\n\n for (const commit of plan.commits) {\n if (commit.files.length === 0) {\n errors.push(`Commit ${commit.id} has no files`);\n continue;\n }\n\n for (const file of commit.files) {\n const firstCommit = seenInCommit.get(file);\n if (firstCommit) {\n errors.push(\n `File appears in multiple commits: ${file} (first: ${firstCommit}, duplicate: ${commit.id})`,\n );\n } else {\n seenInCommit.set(file, commit.id);\n }\n }\n }\n\n return errors;\n}\n\n/**\n * Apply a single commit\n * Supports nested git repositories - files with 'nested-repo/...' prefix will be committed in the nested repo\n */\nasync function applyCommit(cwd: string, commit: CommitGroup): Promise<string> {\n // Group files by repository (root vs nested)\n const filesByRepo = groupFilesByRepo(cwd, commit.files);\n\n // For now, we only support commits within a single repo\n // If files span multiple repos, we need to handle that differently\n const repos = Array.from(filesByRepo.keys());\n\n if (repos.length > 1) {\n throw new Error(\n \"Commit spans multiple repositories. Split into separate commits.\",\n );\n }\n\n const [repoPath, fileInfos] = Array.from(filesByRepo.entries())[0]!;\n const git: SimpleGit = simpleGit(repoPath);\n\n // CRITICAL: Reset staging area first to ensure we only commit the specific files for this commit\n // Without this, git.commit() would commit ALL staged files, not just the ones we want\n await git.reset([\"--\"]);\n\n // Stage only the files for this specific commit (using paths relative to the repo)\n for (const { relativePath } of fileInfos) {\n await git.add(relativePath);\n }\n\n // Create commit with only the staged files\n const message = formatCommitMessage(commit);\n const result = await git.commit(message);\n\n return result.commit;\n}\n\n/**\n * Group files by their git repository (root or nested)\n */\nfunction groupFilesByRepo(\n cwd: string,\n files: string[],\n): Map<string, { relativePath: string; originalPath: string }[]> {\n const filesByRepo = new Map<\n string,\n { relativePath: string; originalPath: string }[]\n >();\n\n for (const file of files) {\n // Check if file is in a nested repo (first segment might be a git repo)\n const segments = file.split(\"/\");\n const potentialRepoDir = segments[0];\n\n // Handle edge case: empty file path or no segments\n if (!potentialRepoDir) {\n const group = filesByRepo.get(cwd) ?? [];\n group.push({ relativePath: file, originalPath: file });\n filesByRepo.set(cwd, group);\n continue;\n }\n\n const potentialRepoPath = join(cwd, potentialRepoDir);\n const potentialGitDir = join(potentialRepoPath, \".git\");\n\n // Check if it's actually a nested git repo\n const isNestedRepo = existsSync(potentialGitDir);\n\n if (isNestedRepo) {\n // Use nested repo as git root, strip first segment from path\n const relativePath = segments.slice(1).join(\"/\");\n const group = filesByRepo.get(potentialRepoPath) ?? [];\n group.push({ relativePath, originalPath: file });\n filesByRepo.set(potentialRepoPath, group);\n } else {\n // Use cwd as git root\n const group = filesByRepo.get(cwd) ?? [];\n group.push({ relativePath: file, originalPath: file });\n filesByRepo.set(cwd, group);\n }\n }\n\n return filesByRepo;\n}\n\n/** Commit footer for branding */\nconst COMMIT_FOOTER = \"\\n\\nšŸ¤– Generated by kb-labs-commit-plugin\";\n\n/**\n * Format commit message following conventional commits\n */\nexport function formatCommitMessage(\n commit: CommitGroup,\n options?: { includeFooter?: boolean },\n): string {\n const type = commit.type;\n const scope = commit.scope ? `(${commit.scope})` : \"\";\n const breaking = commit.breaking ? \"!\" : \"\";\n const subject = commit.message;\n\n let message = `${type}${scope}${breaking}: ${subject}`;\n\n // Add body if present\n if (commit.body) {\n message += `\\n\\n${commit.body}`;\n }\n\n // Add branding footer (default: true)\n if (options?.includeFooter !== false) {\n message += COMMIT_FOOTER;\n }\n\n return message;\n}\n\n/**\n * Check if files in the plan have changed since plan generation\n * Only checks files that are part of the plan, ignoring other changes in the repo\n */\nasync function checkStaleness(\n cwd: string,\n plan: CommitPlan,\n _scope?: string,\n): Promise<{ isStale: boolean; reason: string }> {\n const planFiles = new Set(plan.commits.flatMap((c) => c.files));\n\n // If no files in plan, nothing to check\n if (planFiles.size === 0) {\n return { isStale: false, reason: \"\" };\n }\n\n // Determine which repo(s) we need to check\n const filesByRepo = groupFilesByRepo(cwd, [...planFiles]);\n\n // Check each repo for staleness\n for (const [repoPath, fileInfos] of filesByRepo) {\n // Get current git status from the repo\n // Note: repoPath already points to the correct git repository root\n const currentStatus = await getGitStatus(repoPath);\n const currentFiles = new Set(getAllChangedFiles(currentStatus));\n\n // Check that all expected files are still changed\n for (const { relativePath, originalPath } of fileInfos) {\n if (!currentFiles.has(relativePath)) {\n return {\n isStale: true,\n reason: `File no longer has changes: ${originalPath}. Regenerate plan or use --force.`,\n };\n }\n }\n }\n\n return { isStale: false, reason: \"\" };\n}\n","/**\n * Git push operations\n */\n\nimport { simpleGit, type SimpleGit } from 'simple-git';\nimport type { PushResult } from '@kb-labs/commit-contracts';\nimport type { PushOptions } from '../types';\nimport { getCurrentBranch, isProtectedBranch } from '../analyzer/git-status';\n\n/**\n * Push commits to remote repository\n *\n * @param cwd - Working directory (repo root)\n * @param options - Push options\n * @returns Push result\n */\nexport async function pushCommits(cwd: string, options?: PushOptions): Promise<PushResult> {\n const git: SimpleGit = simpleGit(cwd);\n const remote = options?.remote || 'origin';\n\n try {\n // Get current branch\n const branch = await getCurrentBranch(cwd);\n\n // Warn about protected branches with force push\n if (options?.force && isProtectedBranch(branch)) {\n return {\n success: false,\n remote,\n branch,\n commitsPushed: 0,\n error: `Refusing to force push to protected branch '${branch}'. This is dangerous and disabled by default.`,\n };\n }\n\n // Check how many commits ahead of remote\n const commitsToPush = await countCommitsToPush(git, remote, branch);\n\n if (commitsToPush === 0) {\n return {\n success: true,\n remote,\n branch,\n commitsPushed: 0,\n };\n }\n\n // Push to remote\n const pushOptions = options?.force ? ['--force'] : [];\n await git.push(remote, branch, pushOptions);\n\n return {\n success: true,\n remote,\n branch,\n commitsPushed: commitsToPush,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n const branch = await getCurrentBranch(cwd).catch(() => 'unknown');\n\n return {\n success: false,\n remote,\n branch,\n commitsPushed: 0,\n error: message,\n };\n }\n}\n\n/**\n * Count commits ahead of remote\n */\nasync function countCommitsToPush(\n git: SimpleGit,\n remote: string,\n branch: string\n): Promise<number> {\n try {\n // First fetch to ensure we have latest remote refs\n await git.fetch(remote, branch);\n\n // Count commits between remote and local\n const result = await git.raw([\n 'rev-list',\n '--count',\n `${remote}/${branch}..HEAD`,\n ]);\n\n return parseInt(result.trim(), 10) || 0;\n } catch {\n // If remote doesn't have the branch, all local commits need pushing\n try {\n const result = await git.raw(['rev-list', '--count', 'HEAD']);\n return parseInt(result.trim(), 10) || 0;\n } catch {\n return 0;\n }\n }\n}\n"]}
package/dist/index.js CHANGED
@@ -2889,11 +2889,7 @@ ${commit.body}`;
2889
2889
  return message;
2890
2890
  }
2891
2891
  async function checkStaleness(cwd, plan, _scope) {
2892
- const planFiles = /* @__PURE__ */ new Set([
2893
- ...plan.gitStatus.staged,
2894
- ...plan.gitStatus.unstaged,
2895
- ...plan.gitStatus.untracked
2896
- ]);
2892
+ const planFiles = new Set(plan.commits.flatMap((c) => c.files));
2897
2893
  if (planFiles.size === 0) {
2898
2894
  return { isStale: false, reason: "" };
2899
2895
  }