/ scripts / autofix-branch.js
autofix-branch.js
  1  #!/usr/bin/env node
  2  
  3  /**
  4   * Auto-Fix Branch Manager
  5   *
  6   * All automated maintenance tasks (sage, lint, deps, security, docs) commit
  7   * directly to main. The separate autofix branch workflow has been retired.
  8   *
  9   * ensureAutofixBranch() now simply ensures we are on main.
 10   */
 11  
 12  import { execSync } from 'child_process';
 13  import { existsSync, readFileSync } from 'fs';
 14  import { join } from 'path';
 15  import { fileURLToPath } from 'url';
 16  import { dirname } from 'path';
 17  
 18  const __filename = fileURLToPath(import.meta.url);
 19  const __dirname = dirname(__filename);
 20  const projectRoot = join(__dirname, '..');
 21  
 22  const BRANCH_NAME = 'main';
 23  
 24  /**
 25   * Check if we're in a git repository
 26   */
 27  export function isGitRepo() {
 28    try {
 29      execSync('git rev-parse --git-dir', { cwd: projectRoot, stdio: 'ignore' });
 30      return true;
 31    } catch {
 32      return false;
 33    }
 34  }
 35  
 36  /**
 37   * Get current git branch
 38   */
 39  export function getCurrentBranch() {
 40    try {
 41      return execSync('git branch --show-current', {
 42        cwd: projectRoot,
 43        encoding: 'utf-8',
 44      }).trim();
 45    } catch {
 46      return null;
 47    }
 48  }
 49  
 50  /**
 51   * Check if branch exists
 52   */
 53  export function branchExists(branchName) {
 54    try {
 55      execSync(`git rev-parse --verify ${branchName}`, { cwd: projectRoot, stdio: 'ignore' });
 56      return true;
 57    } catch {
 58      return false;
 59    }
 60  }
 61  
 62  /**
 63   * Get the main branch name (main or master)
 64   */
 65  export function getMainBranch() {
 66    try {
 67      // Check if 'main' exists
 68      execSync('git rev-parse --verify main', { cwd: projectRoot, stdio: 'ignore' });
 69      return 'main';
 70    } catch {
 71      try {
 72        // Fall back to 'master'
 73        execSync('git rev-parse --verify master', { cwd: projectRoot, stdio: 'ignore' });
 74        return 'master';
 75      } catch {
 76        return 'main'; // Default fallback
 77      }
 78    }
 79  }
 80  
 81  /**
 82   * Ensure we're on main, ready for automated commits.
 83   *
 84   * @returns {Object} - { branch: string, switched: boolean }
 85   */
 86  export function ensureAutofixBranch() {
 87    if (!isGitRepo()) {
 88      throw new Error('Not a git repository');
 89    }
 90  
 91    const currentBranch = getCurrentBranch();
 92    const mainBranch = getMainBranch();
 93  
 94    if (currentBranch !== mainBranch) {
 95      execSync(`git checkout ${mainBranch}`, {
 96        cwd: projectRoot,
 97        stdio: 'inherit',
 98      });
 99    }
100  
101    return {
102      branch: mainBranch,
103      switched: currentBranch !== mainBranch,
104      message: `On ${mainBranch} — ready for automated commits`,
105    };
106  }
107  
108  /**
109   * Commit changes with a standardized format
110   *
111   * @param {string} type - Type of fix (sage, deps, lint, security, docs, etc.)
112   * @param {string} summary - Brief summary of changes
113   * @param {Object} details - Additional details to include in commit body
114   */
115  export function commitAutofix(type, summary, details = {}) {
116    // Check if there are changes to commit
117    try {
118      const status = execSync('git status --porcelain', {
119        cwd: projectRoot,
120        encoding: 'utf-8',
121      });
122  
123      if (!status.trim()) {
124        return {
125          committed: false,
126          message: 'No changes to commit',
127        };
128      }
129    } catch (error) {
130      throw new Error(`Failed to check git status: ${error.message}`);
131    }
132  
133    // Stage all changes
134    execSync('git add -A', { cwd: projectRoot });
135  
136    // Build commit message
137    const detailsText = Object.entries(details)
138      .map(([key, value]) => `${key}: ${value}`)
139      .join('\n');
140  
141    const commitMessage = `fix(${type}): ${summary}
142  
143  ${detailsText}
144  
145  Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`;
146  
147    // Commit
148    execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, {
149      cwd: projectRoot,
150      stdio: 'inherit',
151    });
152  
153    return {
154      committed: true,
155      message: `Committed ${type} fixes to ${getMainBranch()} branch`,
156    };
157  }
158  
159  /**
160   * Get summary of recent commits on main (autofix branch no longer used).
161   */
162  export function getAutofixSummary() {
163    if (!isGitRepo()) {
164      return { exists: false, commits: [], filesChanged: 0 };
165    }
166  
167    const mainBranch = getMainBranch();
168  
169    try {
170      const commits = execSync(
171        `git log ${mainBranch} --pretty=format:"%h|%s|%ar" --no-merges -20`,
172        { cwd: projectRoot, encoding: 'utf-8' }
173      )
174        .trim()
175        .split('\n')
176        .filter(Boolean)
177        .map(line => {
178          const [hash, subject, date] = line.split('|');
179          return { hash, subject, date };
180        });
181  
182      return { exists: true, branch: mainBranch, commits };
183    } catch (error) {
184      return { exists: true, error: error.message };
185    }
186  }
187  
188  /**
189   * No-op: autofix branch no longer used. Commits go directly to main.
190   */
191  export function deleteAutofixBranch() {
192    return { deleted: false, message: 'autofix branch retired — commits go directly to main' };
193  }
194  
195  // CLI commands
196  if (import.meta.url === `file://${process.argv[1]}`) {
197    const command = process.argv[2];
198  
199    try {
200      switch (command) {
201        case 'ensure': {
202          console.log('📝 Ensuring we are on main...');
203          const result = ensureAutofixBranch();
204          console.log(`✅ ${result.message}`);
205          break;
206        }
207  
208        case 'summary': {
209          console.log('📊 Recent Commits on main\n');
210          const summary = getAutofixSummary();
211          if (!summary.exists) {
212            console.log('â„šī¸  Not a git repo');
213          } else if (summary.error) {
214            console.error(`❌ Error: ${summary.error}`);
215          } else {
216            console.log(`Branch: ${summary.branch}`);
217            if (summary.commits.length > 0) {
218              console.log('Recent Commits:');
219              summary.commits.forEach(commit => {
220                console.log(`  ${commit.hash} - ${commit.subject} (${commit.date})`);
221              });
222            }
223          }
224          break;
225        }
226  
227        case 'delete': {
228          const deleteResult = deleteAutofixBranch();
229          console.log(`â„šī¸  ${deleteResult.message}`);
230          break;
231        }
232  
233        case 'help':
234        default:
235          console.log(`
236  Usage: node scripts/autofix-branch.js <command>
237  
238  Commands:
239    ensure   - Ensure we are on main (autofix branch retired)
240    summary  - Show recent commits on main
241    delete   - No-op (autofix branch retired)
242    help     - Show this help message
243  
244  Examples:
245    node scripts/autofix-branch.js ensure
246    node scripts/autofix-branch.js summary
247          `);
248      }
249    } catch (error) {
250      console.error(`❌ Error: ${error.message}`);
251      process.exit(1);
252    }
253  }