UNPKG

relaycode

Version:

A developer assistant that automates applying code changes from LLMs.

8 lines 8.37 kB
import {applyOperations,calculateLineChanges,normalizeGitCommitMsg}from'relaycode-core';import {getErrorMessage,logger}from'../utils/logger';import {executeShellCommand,getErrorCount}from'../utils/shell';import {readFileContent,renameFile,deleteFile,writeFileContent,removeEmptyParentDirectories}from'../utils/fs';import $ from'path';import a from'chalk';import {hasBeenProcessed,writePendingState,updatePendingState,commitState,deletePendingState}from'./state';import {createConfirmationHandler}from'../utils/prompt';import {notifyPatchDetected,notifySuccess,notifyRollbackFailure,notifyFailure,requestApprovalWithNotification}from'../utils/notifier';const j=new Map,Z=async(t,o=process.cwd())=>{const s={};return await Promise.all(t.map(async r=>{s[r]=await readFileContent(r,o);})),s},ee=async(t,o,s=process.cwd())=>{const r=await applyOperations(t,o);if(!r.success)throw new Error(`Failed to calculate state changes: ${r.error}`);const{newFileStates:p}=r,c=t.filter(d=>d.type==="rename");for(const d of c)await renameFile(d.from,d.to,s);const i=[],n=new Set([...o.keys(),...p.keys()]),m=new Set(c.map(d=>d.from));for(const d of n){if(m.has(d))continue;const w=o.get(d),h=p.get(d);w!==h&&(h==null?i.push(deleteFile(d,s)):i.push(writeFileContent(d,h,s)));}return await Promise.all(i),p},te=async(t,o=process.cwd())=>{const s=$.resolve(o),r=Object.entries(t),p=new Set,c=[];await Promise.all(r.map(async([n,m])=>{const d=$.resolve(o,n);try{m===null?(await deleteFile(n,o),p.add($.dirname(d))):await writeFileContent(n,m,o);}catch(w){c.push({path:n,error:w});}}));const i=Array.from(p).sort((n,m)=>m.split($.sep).length-n.split($.sep).length);for(const n of i)await removeEmptyParentDirectories(n,s);if(c.length>0){const n=c.map(m=>` - ${m.path}: ${getErrorMessage(m.error)}`).join(` `);throw new Error(`Rollback failed for ${c.length} file(s): ${n}`)}},re=(t,o,s)=>{const r=performance.now()-o;logger.log(a.bold(` Summary:`)),logger.log(`Applied ${a.cyan(s.length)} file operation(s) successfully.`),logger.log(`Total time from start to commit: ${a.gray(`${r.toFixed(2)}ms`)}`),logger.success(`\u2705 Transaction ${a.gray(t)} committed successfully!`);},E=async(t,o,s,r,p=true,c=true)=>{if(!o||typeof o!="string"||!o.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)){logger.error(`Fatal: Invalid UUID provided for rollback: ${o}`);return}c&&logger.warn(`Rolling back changes: ${r}`);let i=false;try{await te(s,t),logger.success(" - Files restored to original state."),i=!0;}catch(n){logger.error(`Fatal: Rollback failed: ${getErrorMessage(n)}`),notifyRollbackFailure(o,p);}finally{try{await deletePendingState(t,o),logger.info(`\u21A9\uFE0F Transaction ${a.gray(o)} rolled back.`),c&&i&&notifyFailure(o,p);}catch(n){logger.error(`Fatal: Could not clean up pending state for ${a.gray(o)}: ${getErrorMessage(n)}`);}}},ae=async({config:t,cwd:o,getConfirmation:s})=>{const r=await getErrorCount(t.patch.linter,o);logger.log(` - Final linter error count: ${r>0?a.red(r):a.green(r)}`);const p=async i=>{logger.warn(i);const n=await requestApprovalWithNotification(t.projectId,t.core.enableNotifications);return n==="approved"?(logger.info("Approved via notification."),true):n==="rejected"?(logger.info("Rejected via notification."),false):(n==="timeout"&&logger.info("Notification timed out or was dismissed. Please use the terminal to respond."),await s("Changes applied. Do you want to approve and commit them? (y/N)"))};return t.patch.approvalMode==="manual"?await p("Manual approval mode is enabled."):r<=t.patch.approvalOnErrorCount?(logger.success(" - Changes automatically approved based on your configuration."),true):await p(`Manual approval required: Linter found ${r} error(s) (threshold is ${t.patch.approvalOnErrorCount}).`)},oe=async(t,o,s)=>{const r=s?.cwd||process.cwd(),p=createConfirmationHandler({yes:s?.yes},s?.prompter),{control:c,operations:i,reasoning:n}=o,{uuid:m,projectId:d}=c,w=performance.now();if(d!==t.projectId){logger.warn(`Skipping patch: projectId mismatch (expected '${a.cyan(t.projectId)}', got '${a.cyan(d)}').`);return}if(await hasBeenProcessed(r,m)){logger.info(`Skipping patch: uuid '${a.gray(m)}' has already been processed.`);return}const{minFileChanges:h,maxFileChanges:b}=t.patch,C=i.length;if(h>0&&C<h){logger.warn(`Skipping patch: Not enough file changes (expected at least ${h}, got ${C}).`);return}if(b&&C>b){logger.warn(`Skipping patch: Too many file changes (expected at most ${b}, got ${C}).`);return}if(s?.notifyOnStart&&(notifyPatchDetected(t.projectId,t.core.enableNotifications),logger.success(`Valid patch detected for project '${a.cyan(t.projectId)}'. Processing...`)),t.patch.preCommand){logger.log(` - Running pre-command: ${a.magenta(t.patch.preCommand)}`);const{exitCode:g,stderr:f}=await executeShellCommand(t.patch.preCommand,r);if(g!==0){logger.error(`Pre-command failed with exit code ${a.red(g)}, aborting transaction.`),f&&logger.error(`Stderr: ${f}`);return}}logger.info(`\u{1F680} Starting transaction for patch ${a.gray(m)}...`),logger.log(`${a.bold("Reasoning:")} ${n.join(` `)}`);const A=i.reduce((g,f)=>(f.type==="rename"?g.push(f.from,f.to):g.push(f.path),g),[]),P=await Z(A,r),y={uuid:m,projectId:d,createdAt:new Date().toISOString(),gitCommitMsg:c.gitCommitMsg,promptSummary:c.promptSummary,reasoning:n,operations:i,snapshot:P,approved:false};try{await writePendingState(r,y),logger.success(" - Staged changes to .pending.yml file.");const g=new Map;A.forEach(l=>g.set(l,P[l]??null)),logger.log(" - Applying file operations...");const f=await ee(i,g,r);logger.success(" - File operations complete.");const v=i.map(l=>{const u=calculateLineChanges(l,g,f);return l.type==="write"?logger.success(`\u2714 Written: ${a.cyan(l.path)} (${a.green(`+${u.added}`)}, ${a.red(`-${u.removed}`)})`):l.type==="delete"?logger.success(`\u2714 Deleted: ${a.cyan(l.path)}`):l.type==="rename"&&logger.success(`\u2714 Renamed: ${a.cyan(l.from)} -> ${a.cyan(l.to)}`),u});if(t.patch.postCommand){logger.log(` - Running post-command: ${a.magenta(t.patch.postCommand)}`);const l=await executeShellCommand(t.patch.postCommand,r);if(l.exitCode!==0)throw logger.error(`Post-command failed with exit code ${a.red(l.exitCode)}.`),l.stderr&&logger.error(`Stderr: ${l.stderr}`),new Error("Post-command failed, forcing rollback.")}const L=performance.now()-w,k=v.reduce((l,u)=>l+u.added,0),x=v.reduce((l,u)=>l+u.removed,0),O=v.reduce((l,u)=>l+u.difference,0);logger.log(a.bold(` Pre-flight summary:`)),logger.success(`Lines changed: ${a.green(`+${k}`)}, ${a.red(`-${x}`)} (${a.yellow(`${O} total`)})`),logger.log(`Checks completed in ${a.gray(`${L.toFixed(2)}ms`)}`),await ae({config:t,cwd:r,getConfirmation:p})?(y.approved=!0,y.linesAdded=k,y.linesRemoved=x,y.linesDifference=O,await updatePendingState(r,y),await commitState(r,m),re(m,w,i),notifySuccess(m,t.core.enableNotifications),await ne(t,y,r)):(logger.warn("Operation cancelled by user. Rolling back changes..."),await E(r,m,P,"User cancellation",t.core.enableNotifications,!1));}catch(g){const f=getErrorMessage(g);await E(r,m,P,f,t.core.enableNotifications,true);}},ye=async(t,o,s)=>{const r=s?.cwd||process.cwd(),p=()=>oe(t,o,s),i=(j.get(r)??Promise.resolve()).finally(p);j.set(r,i),await i;},ne=async(t,o,s)=>{if(!t.git.autoGitBranch)return;let r="";t.git.gitBranchTemplate==="gitCommitMsg"&&o.gitCommitMsg?r=normalizeGitCommitMsg(o.gitCommitMsg)??"":r=o.uuid;const p=r.trim().toLowerCase().replace(/[^\w\s-]/g,"").replace(/[\s_]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").slice(0,70);if(p){const c=`${t.git.gitBranchPrefix}${p}`;logger.info(`Creating and switching to new git branch: ${a.magenta(c)}`);const i=`git checkout -b "${c}"`,n=await executeShellCommand(i,s);n.exitCode===0?logger.success(`Successfully created and switched to branch '${a.magenta(c)}'.`):(n.exitCode===128&&n.stderr.includes("already exists")?logger.warn(`Could not create branch '${a.magenta(c)}' because it already exists.`):logger.warn(`Could not create git branch '${a.magenta(c)}'.`),logger.debug(`'${i}' failed with: ${n.stderr}`));}else logger.warn("Could not generate a branch name segment from commit message or UUID. Skipping git branch creation.");};export{ee as applyOperations,Z as createSnapshot,ye as processPatch,te as restoreSnapshot};//# sourceMappingURL=transaction.js.map //# sourceMappingURL=transaction.js.map