diff --git a/easel/desktop/build-status.cjs b/easel/desktop/build-status.cjs index 7646e5c809..5aabe35104 100644 --- a/easel/desktop/build-status.cjs +++ b/easel/desktop/build-status.cjs @@ -10,7 +10,8 @@ function devStatus(running,state){ function createBuildStatus({app,devHome,root,send,onDevReady,channel=app.isPackaged?'release':'local'}){ let running=devHome?JSON.parse(fs.readFileSync(path.join(root,'../build.json'))):null; let latest={channel,version:app.getVersion(),status:'unknown'}; - let requested='',timer; + let requested='',timer,pin=''; + if(devHome){const dir=path.join(devHome,'running');fs.mkdirSync(dir,{recursive:true});pin=path.join(dir,process.pid+'.json');fs.writeFileSync(pin,JSON.stringify({pid:process.pid,tree:running.tree}));process.once('exit',()=>{try{fs.unlinkSync(pin);}catch{}});} const emit=()=>send('build-status',latest); function poll(){ if(!devHome)return emit(); @@ -25,6 +26,6 @@ function createBuildStatus({app,devHome,root,send,onDevReady,channel=app.isPacka }catch{latest={...latest,status:'unknown'};emit();} } if(devHome){timer=setInterval(poll,3000);timer.unref();} - return {poll,emit,release(status,info={}){if(!devHome){latest={...latest,status,latest:info.version||latest.latest,checkedAt:new Date().toISOString()};emit();}},adopt(nextRoot){running=JSON.parse(fs.readFileSync(path.join(nextRoot,'../build.json')));requested='';poll();},close(){clearInterval(timer);}}; + return {poll,emit,release(status,info={}){if(!devHome){latest={...latest,status,latest:info.version||latest.latest,checkedAt:new Date().toISOString()};emit();}},adopt(nextRoot){running=JSON.parse(fs.readFileSync(path.join(nextRoot,'../build.json')));requested='';poll();},close(){clearInterval(timer);if(pin)try{fs.unlinkSync(pin);}catch{}}}; } module.exports={devStatus,createBuildStatus}; diff --git a/easel/desktop/main.cjs b/easel/desktop/main.cjs index b078f18aa5..e3916884ce 100644 --- a/easel/desktop/main.cjs +++ b/easel/desktop/main.cjs @@ -164,7 +164,7 @@ function start() { ? require('./mas-terminal.cjs').createMasTerminal(args, { ...terminalSize, cwd: workspace, env }) : pty.spawn(process.execPath, args, { name: 'xterm-256color', ...terminalSize, cwd: workspace, env }); agentReady=false;agentReadyBuffer=''; - terminal.onData(data => {send('output', data);if(!agentReady){agentReadyBuffer=(agentReadyBuffer+data).slice(-256);if(agentReadyBuffer.includes('\x1b]777;easel-agent-ready\x07')){agentReady=true;applyPendingDev();}}}); + terminal.onData(data => {send('output', data);if(!agentReady){agentReadyBuffer+=data;if(agentReadyBuffer.includes('\x1b]777;easel-agent-ready\x07')){agentReady=true;applyPendingDev();}agentReadyBuffer=agentReadyBuffer.slice(-64);}}); terminal.onExit(({ exitCode }) => { terminal = null; if (exitCode === 75 && existsSync(controlFile)) { diff --git a/easel/dev/sync.mjs b/easel/dev/sync.mjs index 6860448924..af375f224a 100644 --- a/easel/dev/sync.mjs +++ b/easel/dev/sync.mjs @@ -30,10 +30,13 @@ export function pruneBuilds(home, retain=3) { // Keep recent builds, live PTY roots, and any locally edited snapshot. let processes;try{processes=execFileSync('/bin/ps',['-axo','command='],{encoding:'utf8',maxBuffer:4*1024*1024});}catch{return;} const current=fs.realpathSync(path.join(home,'current')); + const pinned=new Set(); + const pins=path.join(home,'running'); + if(fs.existsSync(pins))for(const name of fs.readdirSync(pins)){if(!/^\d+\.json$/.test(name))continue;const file=path.join(pins,name);try{const pin=JSON.parse(fs.readFileSync(file));if(!Number.isInteger(pin.pid)||pin.pid<=0||!(/^[a-f0-9]{40}$/.test(pin.tree)))continue;process.kill(pin.pid,0);pinned.add(pin.tree);}catch(error){if(error.code==='ESRCH')fs.rmSync(file,{force:true});}} const versions=path.join(home,'versions'); const builds=fs.readdirSync(versions).filter(name=>/^[a-f0-9]{40}$/.test(name)).map(name=>path.join(versions,name)).filter(dir=>fs.existsSync(path.join(dir,'build.json'))).sort((a,b)=>fs.statSync(b).mtimeMs-fs.statSync(a).mtimeMs); for(const dir of builds.slice(retain)){ - if(dir===current||processes.includes(dir))continue; + if(dir===current||pinned.has(path.basename(dir))||processes.includes(dir))continue; try{const manifest=JSON.parse(fs.readFileSync(path.join(dir,'build.json')));if(!modifiedFiles(path.join(dir,'easel'),manifest).length)fs.rmSync(dir,{recursive:true});}catch{} } } diff --git a/easel/test/dev-sync.test.mjs b/easel/test/dev-sync.test.mjs index 480d5cadd0..6e3b236c72 100644 --- a/easel/test/dev-sync.test.mjs +++ b/easel/test/dev-sync.test.mjs @@ -17,6 +17,6 @@ test('failed staging keeps current build and marks freshness unknown',t=>{const test('retention removes only old unmodified snapshots',t=>{ const f=fixture(t),args={home:f.home,fetch:false,build:()=>{}};sync(args);const edited=fs.realpathSync(path.join(f.home,'current')); const next=i=>{fs.writeFileSync(path.join(f.repo,'easel/desktop/main.cjs'),'// revision '+i);f.commit();sync(args);return fs.realpathSync(path.join(f.home,'current'));}; - const old=next(1);fs.writeFileSync(path.join(edited,'easel/desktop/main.cjs'),'local edit');for(let i=2;i<=5;i++)next(i); - assert(fs.existsSync(edited));assert(!fs.existsSync(old));assert.equal(fs.readdirSync(path.join(f.home,'versions')).length,4); + const old=next(1);fs.writeFileSync(path.join(edited,'easel/desktop/main.cjs'),'local edit');const unpinned=next(2);fs.mkdirSync(path.join(f.home,'running'));fs.writeFileSync(path.join(f.home,'running',process.pid+'.json'),JSON.stringify({pid:process.pid,tree:path.basename(old)}));for(let i=3;i<=6;i++)next(i); + assert(fs.existsSync(edited));assert(fs.existsSync(old));assert(!fs.existsSync(unpinned));assert.equal(fs.readdirSync(path.join(f.home,'versions')).length,5); });