@automagik/genie 4.260503.5 → 4.260503.7

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.
package/dist/genie.js CHANGED
@@ -625,7 +625,7 @@ ${bin} set-option -w pane-active-border-style "fg=$COLOR"
625
625
  ${a.repoPath??null}, ${null},
626
626
  ${a.startedAt??new Date().toISOString()}
627
627
  ) ON CONFLICT (id) DO NOTHING
628
- `,await sql`UPDATE agents SET current_executor_id = ${executorId} WHERE id = ${a.id} AND current_executor_id IS NULL`}async function seedWorkers(sql){let filePath=workersJsonPath();if(!needsMigration(filePath))return{agents:0,templates:0};let data=await readJson(filePath);if(!data)return{agents:0,templates:0};let agentCount=0;for(let agent of Object.values(data.workers??{})){if(!agent.id)continue;await upsertAgent(sql,agent),await upsertExecutorFromAgent(sql,agent),agentCount++}let templateCount=0;for(let tpl of Object.values(data.templates??{})){if(!tpl.id)continue;await upsertTemplate(sql,tpl),templateCount++}return{agents:agentCount,templates:templateCount}}function deriveLeader(cfg){if(!cfg.leadAgentId)return null;let at=cfg.leadAgentId.indexOf("@");return at===-1?cfg.leadAgentId:cfg.leadAgentId.slice(0,at)}async function upsertNativeTeam(sql,c){let memberNames=(c.members??[]).map((m)=>m.name).filter((n)=>typeof n==="string"&&n.length>0);await sql`
628
+ `,await sql`UPDATE agents SET current_executor_id = ${executorId} WHERE id = ${a.id} AND current_executor_id IS NULL`}async function seedWorkers(sql){let filePath=workersJsonPath();if(!needsMigration(filePath))return{agents:0,templates:0};let data=await readJson(filePath);if(!data)return{agents:0,templates:0};let agentCount=0;for(let agent of Object.values(data.workers??{})){if(!agent.id)continue;await upsertAgent(sql,agent),await upsertExecutorFromAgent(sql,agent),agentCount++}let templateCount=0;for(let tpl of Object.values(data.templates??{})){if(!tpl.id)continue;await upsertTemplate(sql,tpl),templateCount++}return{agents:agentCount,templates:templateCount}}function deriveLeader(cfg){if(!cfg.leadAgentId)return null;let at=cfg.leadAgentId.indexOf("@");return at===-1?cfg.leadAgentId:cfg.leadAgentId.slice(0,at)}function isValidTeamMember(s){return MEMBER_UUID_RE.test(s)||s.startsWith("dir:")}async function upsertNativeTeam(sql,c){let rawMemberNames=(c.members??[]).map((m)=>m.name).filter((n)=>typeof n==="string"&&n.length>0),memberNames=rawMemberNames.filter(isValidTeamMember),dropped=rawMemberNames.length-memberNames.length;if(dropped>0)console.warn(`[pg-seed] team "${c.name}": dropped ${dropped} legacy member name(s) failing migration 061 constraint (UUID or dir: prefix required); kept ${memberNames.length}`);await sql`
629
629
  INSERT INTO teams (
630
630
  name, repo, base_branch, worktree_path, leader,
631
631
  members, status, native_team_parent_session_id,
@@ -656,7 +656,7 @@ ${bin} set-option -w pane-active-border-style "fg=$COLOR"
656
656
  ${msg.timestamp??new Date().toISOString()}
657
657
  ) ON CONFLICT (id) DO NOTHING
658
658
  `}function parseJsonlRecords(content){let records=[];for(let line of content.trim().split(`
659
- `).filter(Boolean))try{let msg=JSON.parse(line);if(msg.id)records.push(msg)}catch{}return records}async function seedTeamChat(sql,repoPath){let dir=join16(repoPath,".genie","chat");if(!existsSync12(dir))return 0;let files;try{files=await readdir3(dir)}catch{return 0}let count=0;for(let file of files){if(!file.endsWith(".jsonl")||file.endsWith(".migrated"))continue;if(!needsMigration(join16(dir,file)))continue;let teamName=file.replace(".jsonl","").replace(/--/g,"/"),content;try{content=await readFile4(join16(dir,file),"utf-8")}catch{continue}for(let msg of parseJsonlRecords(content))await upsertChatMessage(sql,msg,teamName,repoPath),count++}return count}async function markMigrated(repoPath){let workersPath=workersJsonPath();if(needsMigration(workersPath))await rename(workersPath,`${workersPath}.migrated`);if(!repoPath)return;await renameMatchingFiles(join16(repoPath,".genie","mailbox"),isMailboxFile),await renameMatchingFiles(join16(repoPath,".genie","chat"),(f)=>f.endsWith(".jsonl")&&!f.endsWith(".migrated"))}async function runSeed(sql,repoPath){let result2={agents:0,templates:0,teams:0,mailboxMessages:0,chatMessages:0},workers=await seedWorkers(sql);result2.agents=workers.agents,result2.templates=workers.templates;let teams=await seedTeams(sql);if(result2.teams=teams.count,repoPath)result2.mailboxMessages=await seedMailbox(sql,repoPath),result2.chatMessages=await seedTeamChat(sql,repoPath);if(await markMigrated(repoPath),!teams.hadFailures)await writeTeamsSeedMarker(teams.teamNames);return result2}var init_pg_seed=__esm(()=>{init_claude_native_teams()});import{execSync as execSync2}from"child_process";import{readFileSync as readFileSync8}from"fs";function getProcessStartTime(pid){if(!Number.isInteger(pid)||pid<=0)return null;try{if(process.platform==="darwin"){let raw=execSync2(`ps -o lstart= -p ${pid}`,{encoding:"utf8",timeout:1000,stdio:["ignore","pipe","ignore"]}).trim();return raw===""?null:raw}if(process.platform==="linux"){let raw=readFileSync8(`/proc/${pid}/stat`,"utf-8"),closeParen=raw.lastIndexOf(")");if(closeParen<0)return null;let starttime=raw.slice(closeParen+1).trim().split(/\s+/)[19];return starttime&&starttime.length>0?starttime:null}}catch{return null}return null}var init_process_identity=()=>{};function cachedError(xs){if(originCache.has(xs))return originCache.get(xs);let x=Error.stackTraceLimit;return Error.stackTraceLimit=4,originCache.set(xs,Error()),Error.stackTraceLimit=x,originCache.get(xs)}var originCache,originStackCache,originError,CLOSE,Query;var init_query=__esm(()=>{originCache=new Map,originStackCache=new Map,originError=Symbol("OriginError"),CLOSE={};Query=class Query extends Promise{constructor(strings,args,handler,canceller,options={}){let resolve3,reject;super((a,b)=>{resolve3=a,reject=b});this.tagged=Array.isArray(strings.raw),this.strings=strings,this.args=args,this.handler=handler,this.canceller=canceller,this.options=options,this.state=null,this.statement=null,this.resolve=(x)=>(this.active=!1,resolve3(x)),this.reject=(x)=>(this.active=!1,reject(x)),this.active=!1,this.cancelled=null,this.executed=!1,this.signature="",this[originError]=this.handler.debug?Error():this.tagged&&cachedError(this.strings)}get origin(){return(this.handler.debug?this[originError].stack:this.tagged&&originStackCache.has(this.strings)?originStackCache.get(this.strings):originStackCache.set(this.strings,this[originError].stack).get(this.strings))||""}static get[Symbol.species](){return Promise}cancel(){return this.canceller&&(this.canceller(this),this.canceller=null)}simple(){return this.options.simple=!0,this.options.prepare=!1,this}async readable(){return this.simple(),this.streaming=!0,this}async writable(){return this.simple(),this.streaming=!0,this}cursor(rows=1,fn){if(this.options.simple=!1,typeof rows==="function")fn=rows,rows=1;if(this.cursorRows=rows,typeof fn==="function")return this.cursorFn=fn,this;let prev;return{[Symbol.asyncIterator]:()=>({next:()=>{if(this.executed&&!this.active)return{done:!0};prev&&prev();let promise=new Promise((resolve3,reject)=>{this.cursorFn=(value)=>{return resolve3({value,done:!1}),new Promise((r)=>prev=r)},this.resolve=()=>(this.active=!1,resolve3({done:!0})),this.reject=(x)=>(this.active=!1,reject(x))});return this.execute(),promise},return(){return prev&&prev(CLOSE),{done:!0}}})}}describe(){return this.options.simple=!1,this.onlyDescribe=this.options.prepare=!0,this}stream(){throw Error(".stream has been renamed to .forEach")}forEach(fn){return this.forEachFn=fn,this.handle(),this}raw(){return this.isRaw=!0,this}values(){return this.isRaw="values",this}async handle(){!this.executed&&(this.executed=!0)&&await 1&&this.handler(this)}execute(){return this.handle(),this}then(){return this.handle(),super.then.apply(this,arguments)}catch(){return this.handle(),super.catch.apply(this,arguments)}finally(){return this.handle(),super.finally.apply(this,arguments)}}});function connection(x,options,socket){let{host,port}=socket||options,error=Object.assign(Error("write "+x+" "+(options.path||host+":"+port)),{code:x,errno:x,address:options.path||host},options.path?{}:{port});return Error.captureStackTrace(error,connection),error}function postgres(x){let error=new PostgresError(x);return Error.captureStackTrace(error,postgres),error}function generic(code,message){let error=Object.assign(Error(code+": "+message),{code});return Error.captureStackTrace(error,generic),error}function notSupported(x){let error=Object.assign(Error(x+" (B) is not supported"),{code:"MESSAGE_NOT_SUPPORTED",name:x});return Error.captureStackTrace(error,notSupported),error}var PostgresError,Errors;var init_errors2=__esm(()=>{PostgresError=class PostgresError extends Error{constructor(x){super(x.message);this.name=this.constructor.name,Object.assign(this,x)}};Errors={connection,postgres,generic,notSupported}});class NotTagged{then(){notTagged()}catch(){notTagged()}finally(){notTagged()}}function handleValue(x,parameters,types4,options){let value=x instanceof Parameter?x.value:x;if(value===void 0){if(x instanceof Parameter?x.value=options.transform.undefined:value=x=options.transform.undefined,value===void 0)throw Errors.generic("UNDEFINED_VALUE","Undefined values are not allowed")}return"$"+types4.push(x instanceof Parameter?(parameters.push(x.value),x.array?x.array[x.type||inferType(x.value)]||x.type||firstIsString(x.value):x.type):(parameters.push(x),inferType(x)))}function stringify(q,string,value,parameters,types4,options){for(let i2=1;i2<q.strings.length;i2++)string+=stringifyValue(string,value,parameters,types4,options)+q.strings[i2],value=q.args[i2];return string}function stringifyValue(string,value,parameters,types4,o){return value instanceof Builder?value.build(string,parameters,types4,o):value instanceof Query?fragment(value,parameters,types4,o):value instanceof Identifier?value.value:value&&value[0]instanceof Query?value.reduce((acc,x)=>acc+" "+fragment(x,parameters,types4,o),""):handleValue(value,parameters,types4,o)}function fragment(q,parameters,types4,options){return q.fragment=!0,stringify(q,q.strings[0],q.args[0],parameters,types4,options)}function valuesBuilder(first,parameters,types4,columns,options){return first.map((row)=>"("+columns.map((column)=>stringifyValue("values",row[column],parameters,types4,options)).join(",")+")").join(",")}function values(first,rest,parameters,types4,options){let multi=Array.isArray(first[0]),columns=rest.length?rest.flat():Object.keys(multi?first[0]:first);return valuesBuilder(multi?first:[first],parameters,types4,columns,options)}function select(first,rest,parameters,types4,options){if(typeof first==="string"&&(first=[first].concat(rest)),Array.isArray(first))return escapeIdentifiers(first,options);let value;return(rest.length?rest.flat():Object.keys(first)).map((x)=>{return value=first[x],(value instanceof Query?fragment(value,parameters,types4,options):value instanceof Identifier?value.value:handleValue(value,parameters,types4,options))+" as "+escapeIdentifier(options.transform.column.to?options.transform.column.to(x):x)}).join(",")}function notTagged(){throw Errors.generic("NOT_TAGGED_CALL","Query not called as a tagged template literal")}function firstIsString(x){if(Array.isArray(x))return firstIsString(x[0]);return typeof x==="string"?1009:0}function typeHandlers(types4){return Object.keys(types4).reduce((acc,k)=>{if(types4[k].from&&[].concat(types4[k].from).forEach((x)=>acc.parsers[x]=types4[k].parse),types4[k].serialize)acc.serializers[types4[k].to]=types4[k].serialize,types4[k].from&&[].concat(types4[k].from).forEach((x)=>acc.serializers[x]=types4[k].serialize);return acc},{parsers:{},serializers:{}})}function escapeIdentifiers(xs,{transform:{column}}){return xs.map((x)=>escapeIdentifier(column.to?column.to(x):x)).join(",")}function arrayEscape(x){return x.replace(escapeBackslash,"\\\\").replace(escapeQuote,"\\\"")}function arrayParserLoop(s,x,parser,typarray){let xs=[],delimiter=typarray===1020?";":",";for(;s.i<x.length;s.i++){if(s.char=x[s.i],s.quoted)if(s.char==="\\")s.str+=x[++s.i];else if(s.char==='"')xs.push(parser?parser(s.str):s.str),s.str="",s.quoted=x[s.i+1]==='"',s.last=s.i+2;else s.str+=s.char;else if(s.char==='"')s.quoted=!0;else if(s.char==="{")s.last=++s.i,xs.push(arrayParserLoop(s,x,parser,typarray));else if(s.char==="}"){s.quoted=!1,s.last<s.i&&xs.push(parser?parser(x.slice(s.last,s.i)):x.slice(s.last,s.i)),s.last=s.i+1;break}else if(s.char===delimiter&&s.p!=="}"&&s.p!=='"')xs.push(parser?parser(x.slice(s.last,s.i)):x.slice(s.last,s.i)),s.last=s.i+1;s.p=s.char}return s.last<s.i&&xs.push(parser?parser(x.slice(s.last,s.i+1)):x.slice(s.last,s.i+1)),xs}function createJsonTransform(fn){return function jsonTransform(x,column){return typeof x==="object"&&x!==null&&(column.type===114||column.type===3802)?Array.isArray(x)?x.map((x2)=>jsonTransform(x2,column)):Object.entries(x).reduce((acc,[k,v])=>Object.assign(acc,{[fn(k)]:jsonTransform(v,column)}),{}):x}}var types3,Identifier,Parameter,Builder,defaultHandlers,builders,serializers,parsers,mergeUserTypes=function(types4){let user=typeHandlers(types4||{});return{serializers:Object.assign({},serializers,user.serializers),parsers:Object.assign({},parsers,user.parsers)}},escapeIdentifier=function(str2){return'"'+str2.replace(/"/g,'""').replace(/\./g,'"."')+'"'},inferType=function inferType2(x){return x instanceof Parameter?x.type:x instanceof Date?1184:x instanceof Uint8Array?17:x===!0||x===!1?16:typeof x==="bigint"?20:Array.isArray(x)?inferType2(x[0]):0},escapeBackslash,escapeQuote,arraySerializer=function arraySerializer2(xs,serializer,options,typarray){if(Array.isArray(xs)===!1)return xs;if(!xs.length)return"{}";let first=xs[0],delimiter=typarray===1020?";":",";if(Array.isArray(first)&&!first.type)return"{"+xs.map((x)=>arraySerializer2(x,serializer,options,typarray)).join(delimiter)+"}";return"{"+xs.map((x)=>{if(x===void 0){if(x=options.transform.undefined,x===void 0)throw Errors.generic("UNDEFINED_VALUE","Undefined values are not allowed")}return x===null?"null":'"'+arrayEscape(serializer?serializer(x.type?x.value:x):""+x)+'"'}).join(delimiter)+"}"},arrayParserState,arrayParser=function(x,parser,typarray){return arrayParserState.i=arrayParserState.last=0,arrayParserLoop(arrayParserState,x,parser,typarray)},toCamel=(x)=>{let str2=x[0];for(let i2=1;i2<x.length;i2++)str2+=x[i2]==="_"?x[++i2].toUpperCase():x[i2];return str2},toPascal=(x)=>{let str2=x[0].toUpperCase();for(let i2=1;i2<x.length;i2++)str2+=x[i2]==="_"?x[++i2].toUpperCase():x[i2];return str2},toKebab=(x)=>x.replace(/_/g,"-"),fromCamel=(x)=>x.replace(/([A-Z])/g,"_$1").toLowerCase(),fromPascal=(x)=>(x.slice(0,1)+x.slice(1).replace(/([A-Z])/g,"_$1")).toLowerCase(),fromKebab=(x)=>x.replace(/-/g,"_"),camel,pascal,kebab;var init_types3=__esm(()=>{init_query();init_errors2();types3={string:{to:25,from:null,serialize:(x)=>""+x},number:{to:0,from:[21,23,26,700,701],serialize:(x)=>""+x,parse:(x)=>+x},json:{to:114,from:[114,3802],serialize:(x)=>JSON.stringify(x),parse:(x)=>JSON.parse(x)},boolean:{to:16,from:16,serialize:(x)=>x===!0?"t":"f",parse:(x)=>x==="t"},date:{to:1184,from:[1082,1114,1184],serialize:(x)=>(x instanceof Date?x:new Date(x)).toISOString(),parse:(x)=>new Date(x)},bytea:{to:17,from:17,serialize:(x)=>"\\x"+Buffer.from(x).toString("hex"),parse:(x)=>Buffer.from(x.slice(2),"hex")}};Identifier=class Identifier extends NotTagged{constructor(value){super();this.value=escapeIdentifier(value)}};Parameter=class Parameter extends NotTagged{constructor(value,type2,array){super();this.value=value,this.type=type2,this.array=array}};Builder=class Builder extends NotTagged{constructor(first,rest){super();this.first=first,this.rest=rest}build(before,parameters,types4,options){let keyword=builders.map(([x,fn])=>({fn,i:before.search(x)})).sort((a,b)=>a.i-b.i).pop();return keyword.i===-1?escapeIdentifiers(this.first,options):keyword.fn(this.first,this.rest,parameters,types4,options)}};defaultHandlers=typeHandlers(types3);builders=Object.entries({values,in:(...xs)=>{let x=values(...xs);return x==="()"?"(null)":x},select,as:select,returning:select,"\\(":select,update(first,rest,parameters,types4,options){return(rest.length?rest.flat():Object.keys(first)).map((x)=>escapeIdentifier(options.transform.column.to?options.transform.column.to(x):x)+"="+stringifyValue("values",first[x],parameters,types4,options))},insert(first,rest,parameters,types4,options){let columns=rest.length?rest.flat():Object.keys(Array.isArray(first)?first[0]:first);return"("+escapeIdentifiers(columns,options)+")values"+valuesBuilder(Array.isArray(first)?first:[first],parameters,types4,columns,options)}}).map(([x,fn])=>[new RegExp("((?:^|[\\s(])"+x+"(?:$|[\\s(]))(?![\\s\\S]*\\1)","i"),fn]);serializers=defaultHandlers.serializers,parsers=defaultHandlers.parsers;escapeBackslash=/\\/g,escapeQuote=/"/g;arrayParserState={i:0,char:null,str:"",quoted:!1,last:0};toCamel.column={from:toCamel};toCamel.value={from:createJsonTransform(toCamel)};fromCamel.column={to:fromCamel};camel={...toCamel};camel.column.to=fromCamel;toPascal.column={from:toPascal};toPascal.value={from:createJsonTransform(toPascal)};fromPascal.column={to:fromPascal};pascal={...toPascal};pascal.column.to=fromPascal;toKebab.column={from:toKebab};toKebab.value={from:createJsonTransform(toKebab)};fromKebab.column={to:fromKebab};kebab={...toKebab};kebab.column.to=fromKebab});var Result;var init_result=__esm(()=>{Result=class Result extends Array{constructor(){super();Object.defineProperties(this,{count:{value:null,writable:!0},state:{value:null,writable:!0},command:{value:null,writable:!0},columns:{value:null,writable:!0},statement:{value:null,writable:!0}})}static get[Symbol.species](){return Array}}});function Queue(initial=[]){let xs=initial.slice(),index=0;return{get length(){return xs.length-index},remove:(x)=>{let index2=xs.indexOf(x);return index2===-1?null:(xs.splice(index2,1),x)},push:(x)=>(xs.push(x),x),shift:()=>{let out=xs[index++];if(index===xs.length)index=0,xs=[];else xs[index-1]=void 0;return out}}}var queue_default;var init_queue=__esm(()=>{queue_default=Queue});function fit(x){if(buffer.length-b.i<x){let prev=buffer,length=prev.length;buffer=Buffer.allocUnsafe(length+(length>>1)+x),prev.copy(buffer)}}function reset(){return b.i=0,b}var buffer,messages,b,bytes_default;var init_bytes=__esm(()=>{buffer=Buffer.allocUnsafe(256),messages="BCcDdEFfHPpQSX".split("").reduce((acc,x)=>{let v=x.charCodeAt(0);return acc[x]=()=>{return buffer[0]=v,b.i=5,b},acc},{}),b=Object.assign(reset,messages,{N:String.fromCharCode(0),i:0,inc(x){return b.i+=x,b},str(x){let length=Buffer.byteLength(x);return fit(length),b.i+=buffer.write(x,b.i,length,"utf8"),b},i16(x){return fit(2),buffer.writeUInt16BE(x,b.i),b.i+=2,b},i32(x,i2){if(i2||i2===0)return buffer.writeUInt32BE(x,i2),b;return fit(4),buffer.writeUInt32BE(x,b.i),b.i+=4,b},z(x){return fit(x),buffer.fill(0,b.i,b.i+x),b.i+=x,b},raw(x){return buffer=Buffer.concat([buffer.subarray(0,b.i),x]),b.i=buffer.length,b},end(at=1){buffer.writeUInt32BE(b.i-at,at);let out=buffer.subarray(0,b.i);return b.i=0,buffer=Buffer.allocUnsafe(256),out}}),bytes_default=b});import net from"net";import tls from"tls";import crypto2 from"crypto";import Stream from"stream";import{performance as performance2}from"perf_hooks";function Connection(options,queues={},{onopen=noop,onend=noop,onclose=noop}={}){let{sslnegotiation,ssl,max,user,host,port,database,parsers:parsers2,transform,onnotice,onnotify,onparameter,max_pipeline,keep_alive,backoff,target_session_attrs}=options,sent=queue_default(),id=uid++,backend={pid:null,secret:null},idleTimer=timer(end,options.idle_timeout),lifeTimer=timer(end,options.max_lifetime),connectTimer=timer(connectTimedOut,options.connect_timeout),socket=null,cancelMessage,errorResponse=null,result2=new Result,incoming=Buffer.alloc(0),needsTypes=options.fetch_types,backendParameters={},statements={},statementId=Math.random().toString(36).slice(2),statementCount=1,closedTime=0,remaining=0,hostIndex=0,retries=0,length=0,delay=0,rows=0,serverSignature=null,nextWriteTimer=null,terminated=!1,incomings=null,results=null,initial=null,ending=null,stream=null,chunk=null,ended=null,nonce=null,query=null,final=null,connection2={queue:queues.closed,idleTimer,connect(query2){initial=query2,reconnect()},terminate,execute,cancel,end,count:0,id};return queues.closed&&queues.closed.push(connection2),connection2;async function createSocket(){let x;try{x=options.socket?await Promise.resolve(options.socket(options)):new net.Socket}catch(e){error(e);return}return x.on("error",error),x.on("close",closed),x.on("drain",drain),x}async function cancel({pid,secret},resolve3,reject){try{cancelMessage=bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16),await connect(),socket.once("error",reject),socket.once("close",resolve3)}catch(error2){reject(error2)}}function execute(q){if(terminated)return queryError(q,Errors.connection("CONNECTION_DESTROYED",options));if(stream)return queryError(q,Errors.generic("COPY_IN_PROGRESS","You cannot execute queries during copy"));if(q.cancelled)return;try{return q.state=backend,query?sent.push(q):(query=q,query.active=!0),build(q),write(toBuffer(q))&&!q.describeFirst&&!q.cursorFn&&sent.length<max_pipeline&&(!q.options.onexecute||q.options.onexecute(connection2))}catch(error2){return sent.length===0&&write(Sync),errored(error2),!0}}function toBuffer(q){if(q.parameters.length>=65534)throw Errors.generic("MAX_PARAMETERS_EXCEEDED","Max number of parameters (65534) exceeded");return q.options.simple?bytes_default().Q().str(q.statement.string+bytes_default.N).end():q.describeFirst?Buffer.concat([describe(q),Flush]):q.prepare?q.prepared?prepared(q):Buffer.concat([describe(q),prepared(q)]):unnamed(q)}function describe(q){return Buffer.concat([Parse(q.statement.string,q.parameters,q.statement.types,q.statement.name),Describe("S",q.statement.name)])}function prepared(q){return Buffer.concat([Bind(q.parameters,q.statement.types,q.statement.name,q.cursorName),q.cursorFn?Execute("",q.cursorRows):ExecuteUnnamed])}function unnamed(q){return Buffer.concat([Parse(q.statement.string,q.parameters,q.statement.types),DescribeUnnamed,prepared(q)])}function build(q){let parameters=[],types4=[],string=stringify(q,q.strings[0],q.args[0],parameters,types4,options);!q.tagged&&q.args.forEach((x)=>handleValue(x,parameters,types4,options)),q.prepare=options.prepare&&("prepare"in q.options?q.options.prepare:!0),q.string=string,q.signature=q.prepare&&types4+string,q.onlyDescribe&&delete statements[q.signature],q.parameters=q.parameters||parameters,q.prepared=q.prepare&&q.signature in statements,q.describeFirst=q.onlyDescribe||parameters.length&&!q.prepared,q.statement=q.prepared?statements[q.signature]:{string,types:types4,name:q.prepare?statementId+statementCount++:""},typeof options.debug==="function"&&options.debug(id,string,parameters,types4)}function write(x,fn){if(chunk=chunk?Buffer.concat([chunk,x]):Buffer.from(x),fn||chunk.length>=1024)return nextWrite(fn);return nextWriteTimer===null&&(nextWriteTimer=setImmediate(nextWrite)),!0}function nextWrite(fn){let x=socket.write(chunk,fn);return nextWriteTimer!==null&&clearImmediate(nextWriteTimer),chunk=nextWriteTimer=null,x}function connectTimedOut(){errored(Errors.connection("CONNECT_TIMEOUT",options,socket)),socket.destroy()}async function secure(){if(sslnegotiation!=="direct"){if(write(SSLRequest),!await new Promise((r)=>socket.once("data",(x)=>r(x[0]===83)))&&ssl==="prefer")return connected()}let options2={socket,servername:net.isIP(socket.host)?void 0:socket.host};if(sslnegotiation==="direct")options2.ALPNProtocols=["postgresql"];if(ssl==="require"||ssl==="allow"||ssl==="prefer")options2.rejectUnauthorized=!1;else if(typeof ssl==="object")Object.assign(options2,ssl);socket.removeAllListeners(),socket=tls.connect(options2),socket.on("secureConnect",connected),socket.on("error",error),socket.on("close",closed),socket.on("drain",drain)}function drain(){!query&&onopen(connection2)}function data(x){if(incomings){if(incomings.push(x),remaining-=x.length,remaining>0)return}incoming=incomings?Buffer.concat(incomings,length-remaining):incoming.length===0?x:Buffer.concat([incoming,x],incoming.length+x.length);while(incoming.length>4){if(length=incoming.readUInt32BE(1),length>=incoming.length){remaining=length-incoming.length,incomings=[incoming];break}try{handle(incoming.subarray(0,length+1))}catch(e){query&&(query.cursorFn||query.describeFirst)&&write(Sync),errored(e)}incoming=incoming.subarray(length+1),remaining=0,incomings=null}}async function connect(){if(terminated=!1,backendParameters={},socket||(socket=await createSocket()),!socket)return;if(connectTimer.start(),options.socket)return ssl?secure():connected();if(socket.on("connect",ssl?secure:connected),options.path)return socket.connect(options.path);socket.ssl=ssl,socket.connect(port[hostIndex],host[hostIndex]),socket.host=host[hostIndex],socket.port=port[hostIndex],hostIndex=(hostIndex+1)%port.length}function reconnect(){setTimeout(connect,closedTime?Math.max(0,closedTime+delay-performance2.now()):0)}function connected(){try{statements={},needsTypes=options.fetch_types,statementId=Math.random().toString(36).slice(2),statementCount=1,lifeTimer.start(),socket.on("data",data),keep_alive&&socket.setKeepAlive&&socket.setKeepAlive(!0,1000*keep_alive);let s=StartupMessage();write(s)}catch(err){error(err)}}function error(err){if(connection2.queue===queues.connecting&&options.host[retries+1])return;errored(err);while(sent.length)queryError(sent.shift(),err)}function errored(err){stream&&(stream.destroy(err),stream=null),query&&queryError(query,err),initial&&(queryError(initial,err),initial=null)}function queryError(query2,err){if(query2.reserve)return query2.reject(err);if(!err||typeof err!=="object")err=Error(err);"query"in err||"parameters"in err||Object.defineProperties(err,{stack:{value:err.stack+query2.origin.replace(/.*\n/,`
659
+ `).filter(Boolean))try{let msg=JSON.parse(line);if(msg.id)records.push(msg)}catch{}return records}async function seedTeamChat(sql,repoPath){let dir=join16(repoPath,".genie","chat");if(!existsSync12(dir))return 0;let files;try{files=await readdir3(dir)}catch{return 0}let count=0;for(let file of files){if(!file.endsWith(".jsonl")||file.endsWith(".migrated"))continue;if(!needsMigration(join16(dir,file)))continue;let teamName=file.replace(".jsonl","").replace(/--/g,"/"),content;try{content=await readFile4(join16(dir,file),"utf-8")}catch{continue}for(let msg of parseJsonlRecords(content))await upsertChatMessage(sql,msg,teamName,repoPath),count++}return count}async function markMigrated(repoPath){let workersPath=workersJsonPath();if(needsMigration(workersPath))await rename(workersPath,`${workersPath}.migrated`);if(!repoPath)return;await renameMatchingFiles(join16(repoPath,".genie","mailbox"),isMailboxFile),await renameMatchingFiles(join16(repoPath,".genie","chat"),(f)=>f.endsWith(".jsonl")&&!f.endsWith(".migrated"))}async function runSeed(sql,repoPath){let result2={agents:0,templates:0,teams:0,mailboxMessages:0,chatMessages:0},workers=await seedWorkers(sql);result2.agents=workers.agents,result2.templates=workers.templates;let teams=await seedTeams(sql);if(result2.teams=teams.count,repoPath)result2.mailboxMessages=await seedMailbox(sql,repoPath),result2.chatMessages=await seedTeamChat(sql,repoPath);if(await markMigrated(repoPath),!teams.hadFailures)await writeTeamsSeedMarker(teams.teamNames);return result2}var MEMBER_UUID_RE;var init_pg_seed=__esm(()=>{init_claude_native_teams();MEMBER_UUID_RE=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/});import{execSync as execSync2}from"child_process";import{readFileSync as readFileSync8}from"fs";function getProcessStartTime(pid){if(!Number.isInteger(pid)||pid<=0)return null;try{if(process.platform==="darwin"){let raw=execSync2(`ps -o lstart= -p ${pid}`,{encoding:"utf8",timeout:1000,stdio:["ignore","pipe","ignore"]}).trim();return raw===""?null:raw}if(process.platform==="linux"){let raw=readFileSync8(`/proc/${pid}/stat`,"utf-8"),closeParen=raw.lastIndexOf(")");if(closeParen<0)return null;let starttime=raw.slice(closeParen+1).trim().split(/\s+/)[19];return starttime&&starttime.length>0?starttime:null}}catch{return null}return null}var init_process_identity=()=>{};function cachedError(xs){if(originCache.has(xs))return originCache.get(xs);let x=Error.stackTraceLimit;return Error.stackTraceLimit=4,originCache.set(xs,Error()),Error.stackTraceLimit=x,originCache.get(xs)}var originCache,originStackCache,originError,CLOSE,Query;var init_query=__esm(()=>{originCache=new Map,originStackCache=new Map,originError=Symbol("OriginError"),CLOSE={};Query=class Query extends Promise{constructor(strings,args,handler,canceller,options={}){let resolve3,reject;super((a,b)=>{resolve3=a,reject=b});this.tagged=Array.isArray(strings.raw),this.strings=strings,this.args=args,this.handler=handler,this.canceller=canceller,this.options=options,this.state=null,this.statement=null,this.resolve=(x)=>(this.active=!1,resolve3(x)),this.reject=(x)=>(this.active=!1,reject(x)),this.active=!1,this.cancelled=null,this.executed=!1,this.signature="",this[originError]=this.handler.debug?Error():this.tagged&&cachedError(this.strings)}get origin(){return(this.handler.debug?this[originError].stack:this.tagged&&originStackCache.has(this.strings)?originStackCache.get(this.strings):originStackCache.set(this.strings,this[originError].stack).get(this.strings))||""}static get[Symbol.species](){return Promise}cancel(){return this.canceller&&(this.canceller(this),this.canceller=null)}simple(){return this.options.simple=!0,this.options.prepare=!1,this}async readable(){return this.simple(),this.streaming=!0,this}async writable(){return this.simple(),this.streaming=!0,this}cursor(rows=1,fn){if(this.options.simple=!1,typeof rows==="function")fn=rows,rows=1;if(this.cursorRows=rows,typeof fn==="function")return this.cursorFn=fn,this;let prev;return{[Symbol.asyncIterator]:()=>({next:()=>{if(this.executed&&!this.active)return{done:!0};prev&&prev();let promise=new Promise((resolve3,reject)=>{this.cursorFn=(value)=>{return resolve3({value,done:!1}),new Promise((r)=>prev=r)},this.resolve=()=>(this.active=!1,resolve3({done:!0})),this.reject=(x)=>(this.active=!1,reject(x))});return this.execute(),promise},return(){return prev&&prev(CLOSE),{done:!0}}})}}describe(){return this.options.simple=!1,this.onlyDescribe=this.options.prepare=!0,this}stream(){throw Error(".stream has been renamed to .forEach")}forEach(fn){return this.forEachFn=fn,this.handle(),this}raw(){return this.isRaw=!0,this}values(){return this.isRaw="values",this}async handle(){!this.executed&&(this.executed=!0)&&await 1&&this.handler(this)}execute(){return this.handle(),this}then(){return this.handle(),super.then.apply(this,arguments)}catch(){return this.handle(),super.catch.apply(this,arguments)}finally(){return this.handle(),super.finally.apply(this,arguments)}}});function connection(x,options,socket){let{host,port}=socket||options,error=Object.assign(Error("write "+x+" "+(options.path||host+":"+port)),{code:x,errno:x,address:options.path||host},options.path?{}:{port});return Error.captureStackTrace(error,connection),error}function postgres(x){let error=new PostgresError(x);return Error.captureStackTrace(error,postgres),error}function generic(code,message){let error=Object.assign(Error(code+": "+message),{code});return Error.captureStackTrace(error,generic),error}function notSupported(x){let error=Object.assign(Error(x+" (B) is not supported"),{code:"MESSAGE_NOT_SUPPORTED",name:x});return Error.captureStackTrace(error,notSupported),error}var PostgresError,Errors;var init_errors2=__esm(()=>{PostgresError=class PostgresError extends Error{constructor(x){super(x.message);this.name=this.constructor.name,Object.assign(this,x)}};Errors={connection,postgres,generic,notSupported}});class NotTagged{then(){notTagged()}catch(){notTagged()}finally(){notTagged()}}function handleValue(x,parameters,types4,options){let value=x instanceof Parameter?x.value:x;if(value===void 0){if(x instanceof Parameter?x.value=options.transform.undefined:value=x=options.transform.undefined,value===void 0)throw Errors.generic("UNDEFINED_VALUE","Undefined values are not allowed")}return"$"+types4.push(x instanceof Parameter?(parameters.push(x.value),x.array?x.array[x.type||inferType(x.value)]||x.type||firstIsString(x.value):x.type):(parameters.push(x),inferType(x)))}function stringify(q,string,value,parameters,types4,options){for(let i2=1;i2<q.strings.length;i2++)string+=stringifyValue(string,value,parameters,types4,options)+q.strings[i2],value=q.args[i2];return string}function stringifyValue(string,value,parameters,types4,o){return value instanceof Builder?value.build(string,parameters,types4,o):value instanceof Query?fragment(value,parameters,types4,o):value instanceof Identifier?value.value:value&&value[0]instanceof Query?value.reduce((acc,x)=>acc+" "+fragment(x,parameters,types4,o),""):handleValue(value,parameters,types4,o)}function fragment(q,parameters,types4,options){return q.fragment=!0,stringify(q,q.strings[0],q.args[0],parameters,types4,options)}function valuesBuilder(first,parameters,types4,columns,options){return first.map((row)=>"("+columns.map((column)=>stringifyValue("values",row[column],parameters,types4,options)).join(",")+")").join(",")}function values(first,rest,parameters,types4,options){let multi=Array.isArray(first[0]),columns=rest.length?rest.flat():Object.keys(multi?first[0]:first);return valuesBuilder(multi?first:[first],parameters,types4,columns,options)}function select(first,rest,parameters,types4,options){if(typeof first==="string"&&(first=[first].concat(rest)),Array.isArray(first))return escapeIdentifiers(first,options);let value;return(rest.length?rest.flat():Object.keys(first)).map((x)=>{return value=first[x],(value instanceof Query?fragment(value,parameters,types4,options):value instanceof Identifier?value.value:handleValue(value,parameters,types4,options))+" as "+escapeIdentifier(options.transform.column.to?options.transform.column.to(x):x)}).join(",")}function notTagged(){throw Errors.generic("NOT_TAGGED_CALL","Query not called as a tagged template literal")}function firstIsString(x){if(Array.isArray(x))return firstIsString(x[0]);return typeof x==="string"?1009:0}function typeHandlers(types4){return Object.keys(types4).reduce((acc,k)=>{if(types4[k].from&&[].concat(types4[k].from).forEach((x)=>acc.parsers[x]=types4[k].parse),types4[k].serialize)acc.serializers[types4[k].to]=types4[k].serialize,types4[k].from&&[].concat(types4[k].from).forEach((x)=>acc.serializers[x]=types4[k].serialize);return acc},{parsers:{},serializers:{}})}function escapeIdentifiers(xs,{transform:{column}}){return xs.map((x)=>escapeIdentifier(column.to?column.to(x):x)).join(",")}function arrayEscape(x){return x.replace(escapeBackslash,"\\\\").replace(escapeQuote,"\\\"")}function arrayParserLoop(s,x,parser,typarray){let xs=[],delimiter=typarray===1020?";":",";for(;s.i<x.length;s.i++){if(s.char=x[s.i],s.quoted)if(s.char==="\\")s.str+=x[++s.i];else if(s.char==='"')xs.push(parser?parser(s.str):s.str),s.str="",s.quoted=x[s.i+1]==='"',s.last=s.i+2;else s.str+=s.char;else if(s.char==='"')s.quoted=!0;else if(s.char==="{")s.last=++s.i,xs.push(arrayParserLoop(s,x,parser,typarray));else if(s.char==="}"){s.quoted=!1,s.last<s.i&&xs.push(parser?parser(x.slice(s.last,s.i)):x.slice(s.last,s.i)),s.last=s.i+1;break}else if(s.char===delimiter&&s.p!=="}"&&s.p!=='"')xs.push(parser?parser(x.slice(s.last,s.i)):x.slice(s.last,s.i)),s.last=s.i+1;s.p=s.char}return s.last<s.i&&xs.push(parser?parser(x.slice(s.last,s.i+1)):x.slice(s.last,s.i+1)),xs}function createJsonTransform(fn){return function jsonTransform(x,column){return typeof x==="object"&&x!==null&&(column.type===114||column.type===3802)?Array.isArray(x)?x.map((x2)=>jsonTransform(x2,column)):Object.entries(x).reduce((acc,[k,v])=>Object.assign(acc,{[fn(k)]:jsonTransform(v,column)}),{}):x}}var types3,Identifier,Parameter,Builder,defaultHandlers,builders,serializers,parsers,mergeUserTypes=function(types4){let user=typeHandlers(types4||{});return{serializers:Object.assign({},serializers,user.serializers),parsers:Object.assign({},parsers,user.parsers)}},escapeIdentifier=function(str2){return'"'+str2.replace(/"/g,'""').replace(/\./g,'"."')+'"'},inferType=function inferType2(x){return x instanceof Parameter?x.type:x instanceof Date?1184:x instanceof Uint8Array?17:x===!0||x===!1?16:typeof x==="bigint"?20:Array.isArray(x)?inferType2(x[0]):0},escapeBackslash,escapeQuote,arraySerializer=function arraySerializer2(xs,serializer,options,typarray){if(Array.isArray(xs)===!1)return xs;if(!xs.length)return"{}";let first=xs[0],delimiter=typarray===1020?";":",";if(Array.isArray(first)&&!first.type)return"{"+xs.map((x)=>arraySerializer2(x,serializer,options,typarray)).join(delimiter)+"}";return"{"+xs.map((x)=>{if(x===void 0){if(x=options.transform.undefined,x===void 0)throw Errors.generic("UNDEFINED_VALUE","Undefined values are not allowed")}return x===null?"null":'"'+arrayEscape(serializer?serializer(x.type?x.value:x):""+x)+'"'}).join(delimiter)+"}"},arrayParserState,arrayParser=function(x,parser,typarray){return arrayParserState.i=arrayParserState.last=0,arrayParserLoop(arrayParserState,x,parser,typarray)},toCamel=(x)=>{let str2=x[0];for(let i2=1;i2<x.length;i2++)str2+=x[i2]==="_"?x[++i2].toUpperCase():x[i2];return str2},toPascal=(x)=>{let str2=x[0].toUpperCase();for(let i2=1;i2<x.length;i2++)str2+=x[i2]==="_"?x[++i2].toUpperCase():x[i2];return str2},toKebab=(x)=>x.replace(/_/g,"-"),fromCamel=(x)=>x.replace(/([A-Z])/g,"_$1").toLowerCase(),fromPascal=(x)=>(x.slice(0,1)+x.slice(1).replace(/([A-Z])/g,"_$1")).toLowerCase(),fromKebab=(x)=>x.replace(/-/g,"_"),camel,pascal,kebab;var init_types3=__esm(()=>{init_query();init_errors2();types3={string:{to:25,from:null,serialize:(x)=>""+x},number:{to:0,from:[21,23,26,700,701],serialize:(x)=>""+x,parse:(x)=>+x},json:{to:114,from:[114,3802],serialize:(x)=>JSON.stringify(x),parse:(x)=>JSON.parse(x)},boolean:{to:16,from:16,serialize:(x)=>x===!0?"t":"f",parse:(x)=>x==="t"},date:{to:1184,from:[1082,1114,1184],serialize:(x)=>(x instanceof Date?x:new Date(x)).toISOString(),parse:(x)=>new Date(x)},bytea:{to:17,from:17,serialize:(x)=>"\\x"+Buffer.from(x).toString("hex"),parse:(x)=>Buffer.from(x.slice(2),"hex")}};Identifier=class Identifier extends NotTagged{constructor(value){super();this.value=escapeIdentifier(value)}};Parameter=class Parameter extends NotTagged{constructor(value,type2,array){super();this.value=value,this.type=type2,this.array=array}};Builder=class Builder extends NotTagged{constructor(first,rest){super();this.first=first,this.rest=rest}build(before,parameters,types4,options){let keyword=builders.map(([x,fn])=>({fn,i:before.search(x)})).sort((a,b)=>a.i-b.i).pop();return keyword.i===-1?escapeIdentifiers(this.first,options):keyword.fn(this.first,this.rest,parameters,types4,options)}};defaultHandlers=typeHandlers(types3);builders=Object.entries({values,in:(...xs)=>{let x=values(...xs);return x==="()"?"(null)":x},select,as:select,returning:select,"\\(":select,update(first,rest,parameters,types4,options){return(rest.length?rest.flat():Object.keys(first)).map((x)=>escapeIdentifier(options.transform.column.to?options.transform.column.to(x):x)+"="+stringifyValue("values",first[x],parameters,types4,options))},insert(first,rest,parameters,types4,options){let columns=rest.length?rest.flat():Object.keys(Array.isArray(first)?first[0]:first);return"("+escapeIdentifiers(columns,options)+")values"+valuesBuilder(Array.isArray(first)?first:[first],parameters,types4,columns,options)}}).map(([x,fn])=>[new RegExp("((?:^|[\\s(])"+x+"(?:$|[\\s(]))(?![\\s\\S]*\\1)","i"),fn]);serializers=defaultHandlers.serializers,parsers=defaultHandlers.parsers;escapeBackslash=/\\/g,escapeQuote=/"/g;arrayParserState={i:0,char:null,str:"",quoted:!1,last:0};toCamel.column={from:toCamel};toCamel.value={from:createJsonTransform(toCamel)};fromCamel.column={to:fromCamel};camel={...toCamel};camel.column.to=fromCamel;toPascal.column={from:toPascal};toPascal.value={from:createJsonTransform(toPascal)};fromPascal.column={to:fromPascal};pascal={...toPascal};pascal.column.to=fromPascal;toKebab.column={from:toKebab};toKebab.value={from:createJsonTransform(toKebab)};fromKebab.column={to:fromKebab};kebab={...toKebab};kebab.column.to=fromKebab});var Result;var init_result=__esm(()=>{Result=class Result extends Array{constructor(){super();Object.defineProperties(this,{count:{value:null,writable:!0},state:{value:null,writable:!0},command:{value:null,writable:!0},columns:{value:null,writable:!0},statement:{value:null,writable:!0}})}static get[Symbol.species](){return Array}}});function Queue(initial=[]){let xs=initial.slice(),index=0;return{get length(){return xs.length-index},remove:(x)=>{let index2=xs.indexOf(x);return index2===-1?null:(xs.splice(index2,1),x)},push:(x)=>(xs.push(x),x),shift:()=>{let out=xs[index++];if(index===xs.length)index=0,xs=[];else xs[index-1]=void 0;return out}}}var queue_default;var init_queue=__esm(()=>{queue_default=Queue});function fit(x){if(buffer.length-b.i<x){let prev=buffer,length=prev.length;buffer=Buffer.allocUnsafe(length+(length>>1)+x),prev.copy(buffer)}}function reset(){return b.i=0,b}var buffer,messages,b,bytes_default;var init_bytes=__esm(()=>{buffer=Buffer.allocUnsafe(256),messages="BCcDdEFfHPpQSX".split("").reduce((acc,x)=>{let v=x.charCodeAt(0);return acc[x]=()=>{return buffer[0]=v,b.i=5,b},acc},{}),b=Object.assign(reset,messages,{N:String.fromCharCode(0),i:0,inc(x){return b.i+=x,b},str(x){let length=Buffer.byteLength(x);return fit(length),b.i+=buffer.write(x,b.i,length,"utf8"),b},i16(x){return fit(2),buffer.writeUInt16BE(x,b.i),b.i+=2,b},i32(x,i2){if(i2||i2===0)return buffer.writeUInt32BE(x,i2),b;return fit(4),buffer.writeUInt32BE(x,b.i),b.i+=4,b},z(x){return fit(x),buffer.fill(0,b.i,b.i+x),b.i+=x,b},raw(x){return buffer=Buffer.concat([buffer.subarray(0,b.i),x]),b.i=buffer.length,b},end(at=1){buffer.writeUInt32BE(b.i-at,at);let out=buffer.subarray(0,b.i);return b.i=0,buffer=Buffer.allocUnsafe(256),out}}),bytes_default=b});import net from"net";import tls from"tls";import crypto2 from"crypto";import Stream from"stream";import{performance as performance2}from"perf_hooks";function Connection(options,queues={},{onopen=noop,onend=noop,onclose=noop}={}){let{sslnegotiation,ssl,max,user,host,port,database,parsers:parsers2,transform,onnotice,onnotify,onparameter,max_pipeline,keep_alive,backoff,target_session_attrs}=options,sent=queue_default(),id=uid++,backend={pid:null,secret:null},idleTimer=timer(end,options.idle_timeout),lifeTimer=timer(end,options.max_lifetime),connectTimer=timer(connectTimedOut,options.connect_timeout),socket=null,cancelMessage,errorResponse=null,result2=new Result,incoming=Buffer.alloc(0),needsTypes=options.fetch_types,backendParameters={},statements={},statementId=Math.random().toString(36).slice(2),statementCount=1,closedTime=0,remaining=0,hostIndex=0,retries=0,length=0,delay=0,rows=0,serverSignature=null,nextWriteTimer=null,terminated=!1,incomings=null,results=null,initial=null,ending=null,stream=null,chunk=null,ended=null,nonce=null,query=null,final=null,connection2={queue:queues.closed,idleTimer,connect(query2){initial=query2,reconnect()},terminate,execute,cancel,end,count:0,id};return queues.closed&&queues.closed.push(connection2),connection2;async function createSocket(){let x;try{x=options.socket?await Promise.resolve(options.socket(options)):new net.Socket}catch(e){error(e);return}return x.on("error",error),x.on("close",closed),x.on("drain",drain),x}async function cancel({pid,secret},resolve3,reject){try{cancelMessage=bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16),await connect(),socket.once("error",reject),socket.once("close",resolve3)}catch(error2){reject(error2)}}function execute(q){if(terminated)return queryError(q,Errors.connection("CONNECTION_DESTROYED",options));if(stream)return queryError(q,Errors.generic("COPY_IN_PROGRESS","You cannot execute queries during copy"));if(q.cancelled)return;try{return q.state=backend,query?sent.push(q):(query=q,query.active=!0),build(q),write(toBuffer(q))&&!q.describeFirst&&!q.cursorFn&&sent.length<max_pipeline&&(!q.options.onexecute||q.options.onexecute(connection2))}catch(error2){return sent.length===0&&write(Sync),errored(error2),!0}}function toBuffer(q){if(q.parameters.length>=65534)throw Errors.generic("MAX_PARAMETERS_EXCEEDED","Max number of parameters (65534) exceeded");return q.options.simple?bytes_default().Q().str(q.statement.string+bytes_default.N).end():q.describeFirst?Buffer.concat([describe(q),Flush]):q.prepare?q.prepared?prepared(q):Buffer.concat([describe(q),prepared(q)]):unnamed(q)}function describe(q){return Buffer.concat([Parse(q.statement.string,q.parameters,q.statement.types,q.statement.name),Describe("S",q.statement.name)])}function prepared(q){return Buffer.concat([Bind(q.parameters,q.statement.types,q.statement.name,q.cursorName),q.cursorFn?Execute("",q.cursorRows):ExecuteUnnamed])}function unnamed(q){return Buffer.concat([Parse(q.statement.string,q.parameters,q.statement.types),DescribeUnnamed,prepared(q)])}function build(q){let parameters=[],types4=[],string=stringify(q,q.strings[0],q.args[0],parameters,types4,options);!q.tagged&&q.args.forEach((x)=>handleValue(x,parameters,types4,options)),q.prepare=options.prepare&&("prepare"in q.options?q.options.prepare:!0),q.string=string,q.signature=q.prepare&&types4+string,q.onlyDescribe&&delete statements[q.signature],q.parameters=q.parameters||parameters,q.prepared=q.prepare&&q.signature in statements,q.describeFirst=q.onlyDescribe||parameters.length&&!q.prepared,q.statement=q.prepared?statements[q.signature]:{string,types:types4,name:q.prepare?statementId+statementCount++:""},typeof options.debug==="function"&&options.debug(id,string,parameters,types4)}function write(x,fn){if(chunk=chunk?Buffer.concat([chunk,x]):Buffer.from(x),fn||chunk.length>=1024)return nextWrite(fn);return nextWriteTimer===null&&(nextWriteTimer=setImmediate(nextWrite)),!0}function nextWrite(fn){let x=socket.write(chunk,fn);return nextWriteTimer!==null&&clearImmediate(nextWriteTimer),chunk=nextWriteTimer=null,x}function connectTimedOut(){errored(Errors.connection("CONNECT_TIMEOUT",options,socket)),socket.destroy()}async function secure(){if(sslnegotiation!=="direct"){if(write(SSLRequest),!await new Promise((r)=>socket.once("data",(x)=>r(x[0]===83)))&&ssl==="prefer")return connected()}let options2={socket,servername:net.isIP(socket.host)?void 0:socket.host};if(sslnegotiation==="direct")options2.ALPNProtocols=["postgresql"];if(ssl==="require"||ssl==="allow"||ssl==="prefer")options2.rejectUnauthorized=!1;else if(typeof ssl==="object")Object.assign(options2,ssl);socket.removeAllListeners(),socket=tls.connect(options2),socket.on("secureConnect",connected),socket.on("error",error),socket.on("close",closed),socket.on("drain",drain)}function drain(){!query&&onopen(connection2)}function data(x){if(incomings){if(incomings.push(x),remaining-=x.length,remaining>0)return}incoming=incomings?Buffer.concat(incomings,length-remaining):incoming.length===0?x:Buffer.concat([incoming,x],incoming.length+x.length);while(incoming.length>4){if(length=incoming.readUInt32BE(1),length>=incoming.length){remaining=length-incoming.length,incomings=[incoming];break}try{handle(incoming.subarray(0,length+1))}catch(e){query&&(query.cursorFn||query.describeFirst)&&write(Sync),errored(e)}incoming=incoming.subarray(length+1),remaining=0,incomings=null}}async function connect(){if(terminated=!1,backendParameters={},socket||(socket=await createSocket()),!socket)return;if(connectTimer.start(),options.socket)return ssl?secure():connected();if(socket.on("connect",ssl?secure:connected),options.path)return socket.connect(options.path);socket.ssl=ssl,socket.connect(port[hostIndex],host[hostIndex]),socket.host=host[hostIndex],socket.port=port[hostIndex],hostIndex=(hostIndex+1)%port.length}function reconnect(){setTimeout(connect,closedTime?Math.max(0,closedTime+delay-performance2.now()):0)}function connected(){try{statements={},needsTypes=options.fetch_types,statementId=Math.random().toString(36).slice(2),statementCount=1,lifeTimer.start(),socket.on("data",data),keep_alive&&socket.setKeepAlive&&socket.setKeepAlive(!0,1000*keep_alive);let s=StartupMessage();write(s)}catch(err){error(err)}}function error(err){if(connection2.queue===queues.connecting&&options.host[retries+1])return;errored(err);while(sent.length)queryError(sent.shift(),err)}function errored(err){stream&&(stream.destroy(err),stream=null),query&&queryError(query,err),initial&&(queryError(initial,err),initial=null)}function queryError(query2,err){if(query2.reserve)return query2.reject(err);if(!err||typeof err!=="object")err=Error(err);"query"in err||"parameters"in err||Object.defineProperties(err,{stack:{value:err.stack+query2.origin.replace(/.*\n/,`
660
660
  `),enumerable:options.debug},query:{value:query2.string,enumerable:options.debug},parameters:{value:query2.parameters,enumerable:options.debug},args:{value:query2.args,enumerable:options.debug},types:{value:query2.statement&&query2.statement.types,enumerable:options.debug}}),query2.reject(err)}function end(){return ending||(!connection2.reserved&&onend(connection2),!connection2.reserved&&!initial&&!query&&sent.length===0?(terminate(),new Promise((r)=>socket&&socket.readyState!=="closed"?socket.once("close",r):r())):ending=new Promise((r)=>ended=r))}function terminate(){if(terminated=!0,stream||query||initial||sent.length)error(Errors.connection("CONNECTION_DESTROYED",options));if(clearImmediate(nextWriteTimer),socket)socket.removeListener("data",data),socket.removeListener("connect",connected),socket.readyState==="open"&&socket.end(bytes_default().X().end());ended&&(ended(),ending=ended=null)}async function closed(hadError){if(incoming=Buffer.alloc(0),remaining=0,incomings=null,clearImmediate(nextWriteTimer),socket.removeListener("data",data),socket.removeListener("connect",connected),idleTimer.cancel(),lifeTimer.cancel(),connectTimer.cancel(),socket.removeAllListeners(),socket=null,initial)return reconnect();!hadError&&(query||sent.length)&&error(Errors.connection("CONNECTION_CLOSED",options,socket)),closedTime=performance2.now(),hadError&&options.shared.retries++,delay=(typeof backoff==="function"?backoff(options.shared.retries):backoff)*1000,onclose(connection2,Errors.connection("CONNECTION_CLOSED",options,socket))}function handle(xs,x=xs[0]){(x===68?DataRow:x===100?CopyData:x===65?NotificationResponse:x===83?ParameterStatus:x===90?ReadyForQuery:x===67?CommandComplete:x===50?BindComplete:x===49?ParseComplete:x===116?ParameterDescription:x===84?RowDescription:x===82?Authentication:x===110?NoData:x===75?BackendKeyData:x===69?ErrorResponse:x===115?PortalSuspended:x===51?CloseComplete:x===71?CopyInResponse:x===78?NoticeResponse:x===72?CopyOutResponse:x===99?CopyDone:x===73?EmptyQueryResponse:x===86?FunctionCallResponse:x===118?NegotiateProtocolVersion:x===87?CopyBothResponse:UnknownMessage)(xs)}function DataRow(x){let index=7,length2,column,value,row=query.isRaw?Array(query.statement.columns.length):{};for(let i2=0;i2<query.statement.columns.length;i2++)column=query.statement.columns[i2],length2=x.readInt32BE(index),index+=4,value=length2===-1?null:query.isRaw===!0?x.subarray(index,index+=length2):column.parser===void 0?x.toString("utf8",index,index+=length2):column.parser.array===!0?column.parser(x.toString("utf8",index+1,index+=length2)):column.parser(x.toString("utf8",index,index+=length2)),query.isRaw?row[i2]=query.isRaw===!0?value:transform.value.from?transform.value.from(value,column):value:row[column.name]=transform.value.from?transform.value.from(value,column):value;query.forEachFn?query.forEachFn(transform.row.from?transform.row.from(row):row,result2):result2[rows++]=transform.row.from?transform.row.from(row):row}function ParameterStatus(x){let[k,v]=x.toString("utf8",5,x.length-1).split(bytes_default.N);if(backendParameters[k]=v,options.parameters[k]!==v)options.parameters[k]=v,onparameter&&onparameter(k,v)}function ReadyForQuery(x){if(query)if(errorResponse)query.retried?errored(query.retried):query.prepared&&retryRoutines.has(errorResponse.routine)?retry(query,errorResponse):errored(errorResponse);else query.resolve(results||result2);else if(errorResponse)errored(errorResponse);if(query=results=errorResponse=null,result2=new Result,connectTimer.cancel(),initial){if(target_session_attrs){if(!backendParameters.in_hot_standby||!backendParameters.default_transaction_read_only)return fetchState();else if(tryNext(target_session_attrs,backendParameters))return terminate()}if(needsTypes)return initial.reserve&&(initial=null),fetchArrayTypes();initial&&!initial.reserve&&execute(initial),options.shared.retries=retries=0,initial=null;return}while(sent.length&&(query=sent.shift())&&(query.active=!0,query.cancelled))Connection(options).cancel(query.state,query.cancelled.resolve,query.cancelled.reject);if(query)return;connection2.reserved?!connection2.reserved.release&&x[5]===73?ending?terminate():(connection2.reserved=null,onopen(connection2)):connection2.reserved():ending?terminate():onopen(connection2)}function CommandComplete(x){rows=0;for(let i2=x.length-1;i2>0;i2--){if(x[i2]===32&&x[i2+1]<58&&result2.count===null)result2.count=+x.toString("utf8",i2+1,x.length-1);if(x[i2-1]>=65){result2.command=x.toString("utf8",5,i2),result2.state=backend;break}}if(final&&(final(),final=null),result2.command==="BEGIN"&&max!==1&&!connection2.reserved)return errored(Errors.generic("UNSAFE_TRANSACTION","Only use sql.begin, sql.reserved or max: 1"));if(query.options.simple)return BindComplete();if(query.cursorFn)result2.count&&query.cursorFn(result2),write(Sync)}function ParseComplete(){query.parsing=!1}function BindComplete(){!result2.statement&&(result2.statement=query.statement),result2.columns=query.statement.columns}function ParameterDescription(x){let length2=x.readUInt16BE(5);for(let i2=0;i2<length2;++i2)!query.statement.types[i2]&&(query.statement.types[i2]=x.readUInt32BE(7+i2*4));query.prepare&&(statements[query.signature]=query.statement),query.describeFirst&&!query.onlyDescribe&&(write(prepared(query)),query.describeFirst=!1)}function RowDescription(x){if(result2.command)results=results||[result2],results.push(result2=new Result),result2.count=null,query.statement.columns=null;let length2=x.readUInt16BE(5),index=7,start;query.statement.columns=Array(length2);for(let i2=0;i2<length2;++i2){start=index;while(x[index++]!==0);let table=x.readUInt32BE(index),number=x.readUInt16BE(index+4),type2=x.readUInt32BE(index+6);query.statement.columns[i2]={name:transform.column.from?transform.column.from(x.toString("utf8",start,index-1)):x.toString("utf8",start,index-1),parser:parsers2[type2],table,number,type:type2},index+=18}if(result2.statement=query.statement,query.onlyDescribe)return query.resolve(query.statement),write(Sync)}async function Authentication(x,type2=x.readUInt32BE(5)){(type2===3?AuthenticationCleartextPassword:type2===5?AuthenticationMD5Password:type2===10?SASL:type2===11?SASLContinue:type2===12?SASLFinal:type2!==0?UnknownAuth:noop)(x,type2)}async function AuthenticationCleartextPassword(){let payload=await Pass();write(bytes_default().p().str(payload).z(1).end())}async function AuthenticationMD5Password(x){let payload="md5"+await md5(Buffer.concat([Buffer.from(await md5(await Pass()+user)),x.subarray(9)]));write(bytes_default().p().str(payload).z(1).end())}async function SASL(){nonce=(await crypto2.randomBytes(18)).toString("base64"),bytes_default().p().str("SCRAM-SHA-256"+bytes_default.N);let i2=bytes_default.i;write(bytes_default.inc(4).str("n,,n=*,r="+nonce).i32(bytes_default.i-i2-4,i2).end())}async function SASLContinue(x){let res=x.toString("utf8",9).split(",").reduce((acc,x2)=>(acc[x2[0]]=x2.slice(2),acc),{}),saltedPassword=await crypto2.pbkdf2Sync(await Pass(),Buffer.from(res.s,"base64"),parseInt(res.i),32,"sha256"),clientKey=await hmac(saltedPassword,"Client Key"),auth="n=*,r="+nonce+",r="+res.r+",s="+res.s+",i="+res.i+",c=biws,r="+res.r;serverSignature=(await hmac(await hmac(saltedPassword,"Server Key"),auth)).toString("base64");let payload="c=biws,r="+res.r+",p="+xor(clientKey,Buffer.from(await hmac(await sha256(clientKey),auth))).toString("base64");write(bytes_default().p().str(payload).end())}function SASLFinal(x){if(x.toString("utf8",9).split(bytes_default.N,1)[0].slice(2)===serverSignature)return;errored(Errors.generic("SASL_SIGNATURE_MISMATCH","The server did not return the correct signature")),socket.destroy()}function Pass(){return Promise.resolve(typeof options.pass==="function"?options.pass():options.pass)}function NoData(){if(result2.statement=query.statement,result2.statement.columns=[],query.onlyDescribe)return query.resolve(query.statement),write(Sync)}function BackendKeyData(x){backend.pid=x.readUInt32BE(5),backend.secret=x.readUInt32BE(9)}async function fetchArrayTypes(){needsTypes=!1,(await new Query([`
661
661
  select b.oid, b.typarray
662
662
  from pg_catalog.pg_type a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/genie",
3
- "version": "4.260503.5",
3
+ "version": "4.260503.7",
4
4
  "description": "Collaborative terminal toolkit for human + AI workflows. NOTE: the npm distribution is being soft-deprecated — the canonical install is `curl -fsSL https://get.automagik.dev/genie | bash` (cosign + SLSA verified). See https://automagik.dev/genie/security/distribution-sovereignty",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,11 +12,13 @@
12
12
  "plugins/genie/",
13
13
  "scripts/postinstall-tmux.js",
14
14
  "scripts/postinstall-hook-binary.js",
15
+ "scripts/postinstall-migrations.js",
15
16
  "scripts/sec-scan.cjs",
16
17
  "scripts/sec-remediate.cjs",
17
18
  "scripts/sec-fix.cjs",
18
19
  "scripts/tmux/",
19
20
  "src/db/migrations/",
21
+ "src/migrations/",
20
22
  "templates/",
21
23
  "README.md",
22
24
  "LICENSE"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie",
3
- "version": "4.260503.5",
3
+ "version": "4.260503.7",
4
4
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
5
5
  "author": {
6
6
  "name": "Namastex Labs"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie-plugin",
3
- "version": "4.260503.5",
3
+ "version": "4.260503.7",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for genie bundled CLIs",
6
6
  "type": "module",
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * genie host-migrations postinstall hook.
4
+ *
5
+ * Runs `genie migrate --quiet` after `bun add -g @automagik/genie@<latest>`
6
+ * so users get host-state self-heal transparently.
7
+ *
8
+ * Behavior:
9
+ * - GENIE_SKIP_MIGRATIONS=1 → exit 0 immediately (CI / containers)
10
+ * - No ~/.genie/ directory → fresh install, exit 0 silently
11
+ * - genie binary not callable yet → exit 0 (other postinstalls may run first)
12
+ * - Otherwise: invoke `genie migrate --quiet` with timeout
13
+ * - Soft-fail: any error logs warning, exits 0 (never breaks bun install)
14
+ *
15
+ * The escape hatch for forced re-runs is `genie migrate` (manual).
16
+ */
17
+
18
+ import { spawnSync } from 'node:child_process';
19
+ import fs from 'node:fs';
20
+ import os from 'node:os';
21
+ import path from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = path.dirname(__filename);
26
+
27
+ function main() {
28
+ if (process.env.GENIE_SKIP_MIGRATIONS === '1') return;
29
+
30
+ const genieRoot = path.join(os.homedir(), '.genie');
31
+ if (!fs.existsSync(genieRoot)) return; // fresh install
32
+
33
+ // Try locating the genie binary in this package
34
+ const candidate = path.join(__dirname, '..', 'dist', 'genie.js');
35
+ if (!fs.existsSync(candidate)) {
36
+ process.stderr.write(`[genie-postinstall-migrations] dist not built yet at ${candidate}, skipping\n`);
37
+ return;
38
+ }
39
+
40
+ const result = spawnSync(process.execPath, [candidate, 'migrate', '--quiet'], {
41
+ stdio: ['ignore', 'inherit', 'inherit'],
42
+ timeout: 60_000,
43
+ });
44
+
45
+ if (result.error) {
46
+ process.stderr.write(`[genie-postinstall-migrations] WARNING: invocation failed: ${result.error.message}\n`);
47
+ process.stderr.write('[genie-postinstall-migrations] Run `genie migrate` manually to retry.\n');
48
+ return;
49
+ }
50
+ if (result.status !== 0) {
51
+ process.stderr.write(`[genie-postinstall-migrations] WARNING: \`genie migrate\` exited ${result.status}\n`);
52
+ process.stderr.write('[genie-postinstall-migrations] Run `genie migrate` manually to investigate.\n');
53
+ }
54
+ }
55
+
56
+ try {
57
+ main();
58
+ } catch (err) {
59
+ process.stderr.write(`[genie-postinstall-migrations] WARNING: unexpected error: ${err.message}\n`);
60
+ }
61
+ process.exit(0);
@@ -0,0 +1,71 @@
1
+ # genie host-migrations
2
+
3
+ `genie migrate` applies versioned host-state migrations that fix drift
4
+ between current code expectations and persisted host state (pm2 env
5
+ blocks, embedded pgserve fantasmas, config drifts). Same pattern as DB
6
+ migrations (drizzle, alembic) but for HOST state.
7
+
8
+ ## Lifecycle
9
+
10
+ ```
11
+ bun add -g @automagik/genie
12
+ └─ postinstall hook → genie migrate --quiet
13
+ └─ for each step:
14
+ ├─ check(ctx) — needs apply?
15
+ ├─ apply(ctx) — make it so
16
+ ├─ validate(ctx) — confirm it stuck
17
+ └─ record APPLIED in ~/.genie/migrations.json
18
+ ```
19
+
20
+ ## Subcommands
21
+
22
+ | Command | Behavior |
23
+ |---------|----------|
24
+ | `genie migrate` | Apply all pending in alphabetical order |
25
+ | `genie migrate --dry-run` | List pending without executing |
26
+ | `genie migrate --quiet` | Suppress per-step OK lines (used by postinstall) |
27
+ | `genie migrate --status` | Show applied / pending / failed table |
28
+
29
+ ## Tracking store
30
+
31
+ `~/.genie/migrations.json` — atomic-write JSON. Override path via `GENIE_MIGRATIONS_STORE`.
32
+
33
+ ## Writing a new migration
34
+
35
+ 1. Pick the next 3-digit ID — alphabetical = apply order
36
+ 2. Create file `src/migrations/steps/<NNN>-<kebab-case-name>.ts`
37
+ 3. Export the contract:
38
+
39
+ ```typescript
40
+ import type { MigrationContext } from '../discover.js';
41
+ export const id = 'NNN-kebab-case-name';
42
+ export const description = 'One-line operator-facing description';
43
+ export async function check(ctx: MigrationContext): Promise<boolean> { /* return true if needs apply */ }
44
+ export async function apply(ctx: MigrationContext): Promise<void> { /* write side */ }
45
+ export async function validate(ctx: MigrationContext): Promise<void> { /* throw on fail */ }
46
+ ```
47
+
48
+ ## Idempotency requirement
49
+
50
+ Every migration MUST be idempotent at the apply level. Prefer "set X to Y" over "increment X by 1". Re-runs after partial failure must never corrupt state.
51
+
52
+ ## Failure semantics
53
+
54
+ - A migration that throws is recorded as `FAILED` with error message
55
+ - Subsequent `genie migrate` runs RETRY the failed migration
56
+ - `genie migrate --status` surfaces FAILED rows
57
+ - Postinstall hook soft-fails (warn + exit 0) — `bun install` never breaks
58
+
59
+ ## Override / escape hatch
60
+
61
+ | Env var | Effect |
62
+ |---------|--------|
63
+ | `GENIE_SKIP_MIGRATIONS=1` | Postinstall exits 0 without invoking migrate |
64
+ | `GENIE_MIGRATIONS_STORE` | Override `~/.genie/migrations.json` path |
65
+ | `GENIE_KEEP_LEGACY_PG=1` | Migration 002 will not stop the legacy embedded |
66
+
67
+ ## See also
68
+
69
+ - Wish: `.genie/wishes/genie-host-migrations/WISH.md`
70
+ - Sibling: `pgserve/autopg-upgrade-command` (same self-heal pattern, pgserve subsystem)
71
+ - Drizzle DB migrations (separate concern): `src/db/migrations/`
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Smoke tests for the migrations orchestrator.
3
+ *
4
+ * Validates: dry-run lists discoverable steps, status read works, store
5
+ * read/write atomic, no-op on already-applied. Does NOT exercise actual
6
+ * migration apply paths (that would touch pm2/processes — left for
7
+ * integration tests).
8
+ */
9
+
10
+ import { afterEach, beforeEach, expect, test } from 'bun:test';
11
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
12
+ import { tmpdir } from 'node:os';
13
+ import { join } from 'node:path';
14
+
15
+ let tmpStore: string;
16
+ const ORIGINAL_STORE = process.env.GENIE_MIGRATIONS_STORE;
17
+
18
+ beforeEach(() => {
19
+ const dir = mkdtempSync(join(tmpdir(), 'genie-mig-test-'));
20
+ tmpStore = join(dir, 'migrations.json');
21
+ process.env.GENIE_MIGRATIONS_STORE = tmpStore;
22
+ });
23
+
24
+ afterEach(() => {
25
+ if (existsSync(tmpStore)) rmSync(tmpStore, { force: true });
26
+ if (ORIGINAL_STORE) process.env.GENIE_MIGRATIONS_STORE = ORIGINAL_STORE;
27
+ else process.env.GENIE_MIGRATIONS_STORE = undefined;
28
+ });
29
+
30
+ test('store: load empty when file missing', async () => {
31
+ const { loadStore } = await import('../store.js');
32
+ const s = loadStore();
33
+ expect(s.applied).toEqual([]);
34
+ });
35
+
36
+ test('store: recordApplied + load round-trip', async () => {
37
+ const { recordApplied, loadStore } = await import('../store.js');
38
+ recordApplied('999-test', '4.0.0', 'test detail');
39
+ const s = loadStore();
40
+ expect(s.applied.length).toBe(1);
41
+ expect(s.applied[0].id).toBe('999-test');
42
+ expect(s.applied[0].status).toBe('APPLIED');
43
+ expect(s.applied[0].appliedFrom).toBe('4.0.0');
44
+ });
45
+
46
+ test('store: recordFailed then recordApplied replaces FAILED', async () => {
47
+ const { recordFailed, recordApplied, loadStore } = await import('../store.js');
48
+ recordFailed('999-test', '4.0.0', 'first attempt err');
49
+ expect(loadStore().applied[0].status).toBe('FAILED');
50
+ recordApplied('999-test', '4.0.1', 'second attempt ok');
51
+ const s = loadStore();
52
+ expect(s.applied.length).toBe(1);
53
+ expect(s.applied[0].status).toBe('APPLIED');
54
+ });
55
+
56
+ test('store: atomic write prevents partial JSON', async () => {
57
+ const { recordApplied } = await import('../store.js');
58
+ recordApplied('999-atomic', '4.0.0');
59
+ const raw = readFileSync(tmpStore, 'utf8');
60
+ // file should be valid JSON in one piece
61
+ expect(() => JSON.parse(raw)).not.toThrow();
62
+ });
63
+
64
+ test('discover: returns sorted by id (lexical)', async () => {
65
+ const { discoverMigrations } = await import('../discover.js');
66
+ const list = discoverMigrations();
67
+ // current shipped: 001-* and 002-*
68
+ expect(list.length).toBeGreaterThanOrEqual(2);
69
+ for (let i = 1; i < list.length; i++) {
70
+ expect(list[i].id > list[i - 1].id).toBe(true);
71
+ }
72
+ });
73
+
74
+ test('orchestrator: dry-run does not write store for synthetic check=true mig', async () => {
75
+ // We can't easily inject a synthetic migration without filesystem ceremony;
76
+ // instead verify dry-run on the real shipped migrations doesn't record APPLIED.
77
+ const { migrate } = await import('../index.js');
78
+ const _before = existsSync(tmpStore);
79
+ const r = await migrate({ dryRun: true, quiet: true });
80
+ expect(r.results.length).toBeGreaterThanOrEqual(2);
81
+ // Each result should be DRY-RUN, NO-OP, or SKIP — never APPLIED on dry-run
82
+ for (const x of r.results) {
83
+ expect(['DRY-RUN', 'NO-OP', 'SKIP', 'FAIL'].includes(x.status)).toBe(true);
84
+ }
85
+ // If file exists, it should not contain entries from this dry-run
86
+ if (existsSync(tmpStore)) {
87
+ const s = JSON.parse(readFileSync(tmpStore, 'utf8'));
88
+ // dry-run never records APPLIED (NO-OP could record though, that's allowed for short-circuit)
89
+ expect(s.applied.every((r: any) => r.status !== 'APPLIED' || r.detail === 'no-op (check returned false)')).toBe(
90
+ true,
91
+ );
92
+ }
93
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Discover migrations: scan src/migrations/steps/ for files matching
3
+ * `^\d{3}-.+\.(ts|js)$`. Filename = id (sans extension). Alphabetical
4
+ * sort = strict apply order.
5
+ */
6
+
7
+ import { existsSync, readdirSync } from 'node:fs';
8
+ import { dirname, join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+
13
+ const FILE_PATTERN = /^(\d{3}-[a-z0-9-]+)\.(ts|js)$/;
14
+
15
+ export interface MigrationModule {
16
+ id: string;
17
+ description: string;
18
+ check: (ctx: MigrationContext) => Promise<boolean>;
19
+ apply: (ctx: MigrationContext) => Promise<void>;
20
+ validate: (ctx: MigrationContext) => Promise<void>;
21
+ }
22
+
23
+ export interface MigrationContext {
24
+ log: (msg: string) => void;
25
+ warn: (msg: string) => void;
26
+ dryRun: boolean;
27
+ }
28
+
29
+ export interface DiscoveredMigration {
30
+ id: string;
31
+ filePath: string;
32
+ }
33
+
34
+ export function discoverMigrations(): DiscoveredMigration[] {
35
+ const stepsDir = join(__dirname, 'steps');
36
+ if (!existsSync(stepsDir)) return [];
37
+ const files = readdirSync(stepsDir);
38
+ const matched: DiscoveredMigration[] = [];
39
+ for (const file of files) {
40
+ const m = file.match(FILE_PATTERN);
41
+ if (!m) continue;
42
+ matched.push({ id: m[1], filePath: join(stepsDir, file) });
43
+ }
44
+ matched.sort((a, b) => a.id.localeCompare(b.id));
45
+ return matched;
46
+ }
47
+
48
+ export async function loadMigrationModule(filePath: string): Promise<MigrationModule> {
49
+ const mod = await import(filePath);
50
+ if (typeof mod.id !== 'string' || typeof mod.description !== 'string') {
51
+ throw new Error(`migration ${filePath}: missing id/description exports`);
52
+ }
53
+ if (typeof mod.check !== 'function' || typeof mod.apply !== 'function' || typeof mod.validate !== 'function') {
54
+ throw new Error(`migration ${filePath}: missing check/apply/validate exports`);
55
+ }
56
+ return mod as MigrationModule;
57
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Genie host migrations — versioned, applied-once-per-host.
3
+ *
4
+ * Discover migrations in steps/ → filter pending vs already-applied →
5
+ * run check → apply → validate per step, record to store. Same pattern
6
+ * as DB migrations (drizzle, alembic) but for HOST state (pm2 env,
7
+ * embedded pgserve, config drifts).
8
+ *
9
+ * Auto-runs on `bun add -g @automagik/genie@latest` via postinstall hook
10
+ * (scripts/postinstall-migrations.js). Manual `genie migrate` is the
11
+ * explicit escape hatch.
12
+ *
13
+ * See: .genie/wishes/genie-host-migrations/WISH.md
14
+ */
15
+
16
+ import { readFileSync } from 'node:fs';
17
+ import { dirname, join } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ import { type MigrationContext, type MigrationModule, discoverMigrations, loadMigrationModule } from './discover.js';
21
+ import { type StepResult, runMigration } from './runner.js';
22
+ import { type MigrationRecord, getApplied } from './store.js';
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+
26
+ export interface MigrateOptions {
27
+ quiet?: boolean;
28
+ dryRun?: boolean;
29
+ }
30
+
31
+ export interface MigrateResult {
32
+ ok: boolean;
33
+ results: StepResult[];
34
+ summary: string;
35
+ }
36
+
37
+ function getGenieVersion(): string {
38
+ try {
39
+ // genie cli installed structure: <root>/dist/genie.js + <root>/package.json
40
+ const pkgPath = join(__dirname, '..', '..', 'package.json');
41
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
42
+ return pkg.version || 'unknown';
43
+ } catch {
44
+ return 'unknown';
45
+ }
46
+ }
47
+
48
+ export async function migrate(options: MigrateOptions = {}): Promise<MigrateResult> {
49
+ const { quiet = false, dryRun = false } = options;
50
+ const log = (msg: string) => {
51
+ if (!quiet) process.stderr.write(`${msg}\n`);
52
+ };
53
+ const warn = (msg: string) => process.stderr.write(`${msg}\n`);
54
+ const ctx: MigrationContext = { log, warn, dryRun };
55
+
56
+ const version = getGenieVersion();
57
+ log(`genie host-migrations starting (version=${version}, dryRun=${dryRun})`);
58
+
59
+ const discovered = discoverMigrations();
60
+ if (discovered.length === 0) {
61
+ return { ok: true, results: [], summary: 'no migrations discovered' };
62
+ }
63
+
64
+ const results: StepResult[] = [];
65
+ for (const item of discovered) {
66
+ let mod: MigrationModule;
67
+ try {
68
+ mod = await loadMigrationModule(item.filePath);
69
+ } catch (err) {
70
+ const msg = (err as Error).message;
71
+ warn(`[${item.id}] FAIL during load: ${msg}`);
72
+ results.push({ id: item.id, status: 'FAIL', detail: `load threw: ${msg}` });
73
+ continue;
74
+ }
75
+ const r = await runMigration(mod, ctx, version);
76
+ results.push(r);
77
+ }
78
+
79
+ const failed = results.filter((r) => r.status === 'FAIL');
80
+ const summary = `genie host-migrations complete: ${results.length - failed.length}/${results.length} OK`;
81
+ log(summary);
82
+ if (failed.length > 0) {
83
+ warn(`Failed migrations: ${failed.map((r) => r.id).join(', ')}`);
84
+ warn('Re-run `genie migrate` after addressing the above.');
85
+ return { ok: false, results, summary };
86
+ }
87
+ return { ok: true, results, summary };
88
+ }
89
+
90
+ export interface StatusEntry {
91
+ id: string;
92
+ status: 'APPLIED' | 'PENDING' | 'FAILED';
93
+ appliedAt?: string;
94
+ appliedFrom?: string;
95
+ detail?: string;
96
+ }
97
+
98
+ export function status(): StatusEntry[] {
99
+ const discovered = discoverMigrations();
100
+ const applied = getApplied();
101
+ const out: StatusEntry[] = [];
102
+ for (const item of discovered) {
103
+ const rec: MigrationRecord | undefined = applied.get(item.id);
104
+ if (!rec) {
105
+ out.push({ id: item.id, status: 'PENDING' });
106
+ } else {
107
+ out.push({
108
+ id: item.id,
109
+ status: rec.status === 'APPLIED' ? 'APPLIED' : 'FAILED',
110
+ appliedAt: rec.appliedAt,
111
+ appliedFrom: rec.appliedFrom,
112
+ detail: rec.detail,
113
+ });
114
+ }
115
+ }
116
+ return out;
117
+ }
118
+
119
+ export { discoverMigrations, loadMigrationModule } from './discover.js';
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Per-step runner — enforces the check → apply → validate contract,
3
+ * records outcome to the store, returns structured result.
4
+ */
5
+
6
+ import type { MigrationContext, MigrationModule } from './discover.js';
7
+ import { getApplied, recordApplied, recordFailed } from './store.js';
8
+
9
+ export type StepStatus = 'APPLIED' | 'SKIP' | 'NO-OP' | 'FAIL' | 'DRY-RUN';
10
+
11
+ export interface StepResult {
12
+ id: string;
13
+ status: StepStatus;
14
+ detail: string;
15
+ }
16
+
17
+ export async function runMigration(mod: MigrationModule, ctx: MigrationContext, version: string): Promise<StepResult> {
18
+ const applied = getApplied();
19
+ const prior = applied.get(mod.id);
20
+ if (prior?.status === 'APPLIED') {
21
+ ctx.log(`[${mod.id}] SKIP: already applied at ${prior.appliedAt}`);
22
+ return { id: mod.id, status: 'SKIP', detail: `already applied at ${prior.appliedAt}` };
23
+ }
24
+
25
+ let needsApply: boolean;
26
+ try {
27
+ needsApply = await mod.check(ctx);
28
+ } catch (err) {
29
+ const msg = (err as Error).message;
30
+ ctx.warn(`[${mod.id}] FAIL during check: ${msg}`);
31
+ recordFailed(mod.id, version, `check threw: ${msg}`);
32
+ return { id: mod.id, status: 'FAIL', detail: `check threw: ${msg}` };
33
+ }
34
+
35
+ if (!needsApply) {
36
+ ctx.log(`[${mod.id}] NO-OP: check returned false (host already in target state)`);
37
+ recordApplied(mod.id, version, 'no-op (check returned false)');
38
+ return { id: mod.id, status: 'NO-OP', detail: 'check returned false' };
39
+ }
40
+
41
+ if (ctx.dryRun) {
42
+ ctx.log(`[${mod.id}] DRY-RUN: would apply — ${mod.description}`);
43
+ return { id: mod.id, status: 'DRY-RUN', detail: mod.description };
44
+ }
45
+
46
+ try {
47
+ await mod.apply(ctx);
48
+ } catch (err) {
49
+ const msg = (err as Error).message;
50
+ ctx.warn(`[${mod.id}] FAIL during apply: ${msg}`);
51
+ recordFailed(mod.id, version, `apply threw: ${msg}`);
52
+ return { id: mod.id, status: 'FAIL', detail: `apply threw: ${msg}` };
53
+ }
54
+
55
+ try {
56
+ await mod.validate(ctx);
57
+ } catch (err) {
58
+ const msg = (err as Error).message;
59
+ ctx.warn(`[${mod.id}] FAIL during validate: ${msg}`);
60
+ recordFailed(mod.id, version, `validate threw: ${msg}`);
61
+ return { id: mod.id, status: 'FAIL', detail: `validate threw: ${msg}` };
62
+ }
63
+
64
+ recordApplied(mod.id, version, mod.description);
65
+ ctx.log(`[${mod.id}] APPLIED: ${mod.description}`);
66
+ return { id: mod.id, status: 'APPLIED', detail: mod.description };
67
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Migration 001 — bake DATABASE_URL into pm2 genie-serve env block.
3
+ *
4
+ * Closes the upgrade-path hole left by commit 5567e202 (`fix(install):
5
+ * bake DATABASE_URL env into ecosystem config when canonical pgserve is
6
+ * detected`). That fix only kicks in on fresh `genie install`. Hosts
7
+ * installed before the fix have a pm2 process `genie-serve` with no
8
+ * env block and silently spawn their own embedded pgserve instead of
9
+ * connecting to the canonical one.
10
+ *
11
+ * Detection: pm2 process `genie-serve` exists AND its env lacks
12
+ * `DATABASE_URL` AND canonical pgserve is registered (port 8432
13
+ * reachable).
14
+ *
15
+ * Fix: set DATABASE_URL via `pm2 set genie-serve:DATABASE_URL <url>` then
16
+ * `pm2 restart genie-serve --update-env`. Genie-serve at next boot
17
+ * connects to canonical and stops spawning the legacy embedded.
18
+ */
19
+
20
+ import { execSync } from 'node:child_process';
21
+
22
+ import type { MigrationContext } from '../discover.js';
23
+
24
+ export const id = '001-pm2-env-databaseurl-bake';
25
+ export const description = 'Bake DATABASE_URL into pm2 genie-serve env when canonical pgserve registered';
26
+
27
+ const CANONICAL_PORT = 8432;
28
+
29
+ interface Pm2Process {
30
+ pm_id: number;
31
+ name: string;
32
+ pm2_env?: { env?: Record<string, string> };
33
+ }
34
+
35
+ function pm2ListJson(): Pm2Process[] {
36
+ try {
37
+ const out = execSync('pm2 jlist', { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
38
+ return JSON.parse(out) as Pm2Process[];
39
+ } catch {
40
+ return [];
41
+ }
42
+ }
43
+
44
+ function findGenieServe(): Pm2Process | undefined {
45
+ return pm2ListJson().find((p) => p.name === 'genie-serve');
46
+ }
47
+
48
+ function canonicalPgserveReachable(): boolean {
49
+ try {
50
+ execSync(`pg_isready -h 127.0.0.1 -p ${CANONICAL_PORT}`, { stdio: 'pipe' });
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ function buildCanonicalUrl(): string {
58
+ // Mirrors the URL shape baked by `genie install` post-5567e202:
59
+ // postgresql://postgres:postgres@127.0.0.1:<port>/<db>
60
+ // The DB name is per-app fingerprint; genie defaults to 'postgres' when
61
+ // the host hasn't run autopg create-app — install sets the real name.
62
+ // For the migration we use the pgserve discovery URL.
63
+ return `postgresql://postgres:postgres@127.0.0.1:${CANONICAL_PORT}/postgres`;
64
+ }
65
+
66
+ export async function check(_ctx: MigrationContext): Promise<boolean> {
67
+ const proc = findGenieServe();
68
+ if (!proc) return false; // no genie-serve under pm2 → nothing to fix
69
+ const envHas = proc.pm2_env?.env?.DATABASE_URL;
70
+ if (envHas) return false; // already baked
71
+ if (!canonicalPgserveReachable()) return false; // canonical not up → can't safely set URL
72
+ return true;
73
+ }
74
+
75
+ export async function apply(ctx: MigrationContext): Promise<void> {
76
+ const url = buildCanonicalUrl();
77
+ ctx.log(`setting pm2 env genie-serve:DATABASE_URL = ${url}`);
78
+ execSync(`pm2 set genie-serve:DATABASE_URL ${JSON.stringify(url)}`, { stdio: 'pipe' });
79
+ ctx.log('restarting genie-serve --update-env');
80
+ execSync('pm2 restart genie-serve --update-env', { stdio: 'pipe' });
81
+ }
82
+
83
+ export async function validate(_ctx: MigrationContext): Promise<void> {
84
+ const proc = findGenieServe();
85
+ if (!proc) throw new Error('genie-serve missing from pm2 after restart');
86
+ const url = proc.pm2_env?.env?.DATABASE_URL;
87
+ if (!url) throw new Error('DATABASE_URL still not baked into pm2 env after apply');
88
+ if (!url.includes(`:${CANONICAL_PORT}/`)) {
89
+ throw new Error(`DATABASE_URL points to non-canonical port: ${url}`);
90
+ }
91
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Migration 002 — kill legacy embedded pgserve listening on a non-canonical port.
3
+ *
4
+ * Detection: a postgres process owned by the current user is listening on
5
+ * a port other than canonical 8432, AND canonical pgserve responds on
6
+ * 8432, AND no obvious user-intent override (env var GENIE_KEEP_LEGACY_PG=1).
7
+ *
8
+ * Fix: send graceful pg_ctl stop to the legacy process; if that fails,
9
+ * SIGTERM. Migration 001 (must run first) ensures genie-serve no longer
10
+ * spawns it, so it stays dead.
11
+ */
12
+
13
+ import { execSync } from 'node:child_process';
14
+
15
+ import type { MigrationContext } from '../discover.js';
16
+
17
+ export const id = '002-kill-embedded-pgserve-legacy';
18
+ export const description = 'Stop legacy embedded pgserve on non-canonical ports when canonical is healthy';
19
+
20
+ const CANONICAL_PORT = 8432;
21
+
22
+ interface ListeningPg {
23
+ pid: number;
24
+ port: number;
25
+ }
26
+
27
+ function listListeningPgserve(): ListeningPg[] {
28
+ try {
29
+ const out = execSync('ss -tlnp', { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
30
+ const out_lines = out.split('\n');
31
+ const found: ListeningPg[] = [];
32
+ const seen = new Set<number>();
33
+ for (const line of out_lines) {
34
+ // Match "127.0.0.1:<port>" with users:(("postgres",pid=<n>,...))
35
+ const portMatch = line.match(/127\.0\.0\.1:(\d+)\s/);
36
+ const procMatch = line.match(/users:\(\("postgres",pid=(\d+)/);
37
+ if (portMatch && procMatch) {
38
+ const pid = Number.parseInt(procMatch[1], 10);
39
+ const port = Number.parseInt(portMatch[1], 10);
40
+ if (!seen.has(pid)) {
41
+ seen.add(pid);
42
+ found.push({ pid, port });
43
+ }
44
+ }
45
+ }
46
+ return found;
47
+ } catch {
48
+ return [];
49
+ }
50
+ }
51
+
52
+ function canonicalReachable(): boolean {
53
+ try {
54
+ execSync(`pg_isready -h 127.0.0.1 -p ${CANONICAL_PORT}`, { stdio: 'pipe' });
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ function findLegacyEmbedded(): ListeningPg | undefined {
62
+ if (process.env.GENIE_KEEP_LEGACY_PG === '1') return undefined;
63
+ if (!canonicalReachable()) return undefined;
64
+ return listListeningPgserve().find((p) => p.port !== CANONICAL_PORT);
65
+ }
66
+
67
+ export async function check(_ctx: MigrationContext): Promise<boolean> {
68
+ return findLegacyEmbedded() !== undefined;
69
+ }
70
+
71
+ export async function apply(ctx: MigrationContext): Promise<void> {
72
+ const target = findLegacyEmbedded();
73
+ if (!target) {
74
+ ctx.log('no legacy embedded found at apply time (race resolved)');
75
+ return;
76
+ }
77
+ ctx.log(`stopping legacy embedded pgserve PID ${target.pid} (port ${target.port})`);
78
+ // Try pg_ctl stop via discovered data dir from the process
79
+ try {
80
+ // Process cmdline to find -D <dataDir>
81
+ const cmdline = execSync(`cat /proc/${target.pid}/cmdline | tr '\\0' ' '`, {
82
+ stdio: ['ignore', 'pipe', 'pipe'],
83
+ }).toString();
84
+ const dataMatch = cmdline.match(/-D\s+(\S+)/);
85
+ if (dataMatch) {
86
+ execSync(`pg_ctl -D ${dataMatch[1]} -m fast stop`, { stdio: 'pipe' });
87
+ ctx.log(`pg_ctl stop OK (data dir: ${dataMatch[1]})`);
88
+ return;
89
+ }
90
+ } catch (err) {
91
+ ctx.warn(`pg_ctl stop failed: ${(err as Error).message} — falling back to SIGTERM`);
92
+ }
93
+ // Fallback: SIGTERM the master process
94
+ try {
95
+ process.kill(target.pid, 'SIGTERM');
96
+ ctx.log(`SIGTERM sent to PID ${target.pid}`);
97
+ } catch (err) {
98
+ throw new Error(`could not stop legacy embedded PID ${target.pid}: ${(err as Error).message}`);
99
+ }
100
+ }
101
+
102
+ export async function validate(_ctx: MigrationContext): Promise<void> {
103
+ // Allow up to 5s for shutdown
104
+ const deadline = Date.now() + 5000;
105
+ while (Date.now() < deadline) {
106
+ if (findLegacyEmbedded() === undefined) return;
107
+ // small sleep
108
+ Bun.sleepSync(200);
109
+ }
110
+ const remaining = findLegacyEmbedded();
111
+ if (remaining) {
112
+ throw new Error(`legacy embedded still listening on port ${remaining.port} (PID ${remaining.pid}) after 5s`);
113
+ }
114
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Migration tracking store — atomic read/write of ~/.genie/migrations.json.
3
+ *
4
+ * Records which migrations have been applied to this host, when, and from
5
+ * which genie cli version. Used by the orchestrator to filter pending vs
6
+ * already-applied. File-based (not PG) because migrations may need to RUN
7
+ * before genie-serve / canonical pgserve are healthy.
8
+ */
9
+
10
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { dirname } from 'node:path';
13
+
14
+ export type MigrationStatus = 'APPLIED' | 'FAILED';
15
+
16
+ export interface MigrationRecord {
17
+ id: string;
18
+ status: MigrationStatus;
19
+ appliedAt: string; // ISO timestamp
20
+ appliedFrom: string; // genie cli version at apply time
21
+ detail?: string; // FAILED reason or APPLIED note
22
+ }
23
+
24
+ interface StoreFile {
25
+ applied: MigrationRecord[];
26
+ }
27
+
28
+ export function getStorePath(): string {
29
+ return process.env.GENIE_MIGRATIONS_STORE || `${homedir()}/.genie/migrations.json`;
30
+ }
31
+
32
+ export function loadStore(): StoreFile {
33
+ const p = getStorePath();
34
+ if (!existsSync(p)) return { applied: [] };
35
+ try {
36
+ const raw = readFileSync(p, 'utf8');
37
+ const parsed = JSON.parse(raw);
38
+ if (!parsed || !Array.isArray(parsed.applied)) return { applied: [] };
39
+ return parsed as StoreFile;
40
+ } catch {
41
+ return { applied: [] };
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Atomic write: tmp file + rename so a crash mid-write never leaves
47
+ * partial JSON on disk.
48
+ */
49
+ export function saveStore(store: StoreFile): void {
50
+ const p = getStorePath();
51
+ mkdirSync(dirname(p), { recursive: true });
52
+ const tmp = `${p}.tmp.${process.pid}.${Date.now()}`;
53
+ writeFileSync(tmp, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o644 });
54
+ renameSync(tmp, p);
55
+ }
56
+
57
+ export function recordApplied(id: string, version: string, detail?: string): void {
58
+ const store = loadStore();
59
+ // Strip any prior FAILED record for this id; record APPLIED authoritatively.
60
+ store.applied = store.applied.filter((r) => r.id !== id);
61
+ store.applied.push({
62
+ id,
63
+ status: 'APPLIED',
64
+ appliedAt: new Date().toISOString(),
65
+ appliedFrom: version,
66
+ detail,
67
+ });
68
+ saveStore(store);
69
+ }
70
+
71
+ export function recordFailed(id: string, version: string, reason: string): void {
72
+ const store = loadStore();
73
+ store.applied = store.applied.filter((r) => r.id !== id);
74
+ store.applied.push({
75
+ id,
76
+ status: 'FAILED',
77
+ appliedAt: new Date().toISOString(),
78
+ appliedFrom: version,
79
+ detail: reason,
80
+ });
81
+ saveStore(store);
82
+ }
83
+
84
+ export function getApplied(): Map<string, MigrationRecord> {
85
+ const store = loadStore();
86
+ return new Map(store.applied.map((r) => [r.id, r]));
87
+ }