#!/usr/bin/env node // --prepare is read-only. --deploy / --rollback require an explicit release decision. import {readFileSync,writeFileSync,mkdirSync,chmodSync} from 'node:fs'; import {createHash} from 'node:crypto'; import {homedir} from 'node:os'; import {join} from 'node:path'; import {patchAdminReadHelpers,optimizedReadHelpers} from './admin-read-performance-patch.mjs'; const mode=process.argv.includes('--deploy')?'deploy':process.argv.includes('--rollback')?'rollback':'prepare'; const dir=process.env.XIAOSHU_READ_RELEASE_DIR||join(homedir(),'.local/state/xiaoshu/cloud-releases/2026-10-15-admin-read-performance'); const base=process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse',app=process.env.XIAOSHU_PARSE_APP_ID,master=process.env.XIAOSHU_MASTER_KEY,token=process.env.XIAOSHU_SYNC_OPERATOR_TOKEN; if(!app||!master||!token)throw Error('Missing authorized release configuration'); const hash=s=>createHash('sha256').update(s).digest('hex'),headers={'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master}; async function parse(path,method='GET',body){const r=await fetch(base+path,{method,headers,body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(30000)}),d=await r.json();if(!r.ok||d.error)throw Error('Release API HTTP '+r.status);return d;} mkdirSync(dir,{recursive:true,mode:0o700});chmodSync(dir,0o700); if(mode==='prepare'){ const rows=(await parse('/classes/Function?'+new URLSearchParams({where:JSON.stringify({path:'xiaoshu/ops/gateway-v3'}),limit:'2'}))).results; if(rows.length!==1)throw Error('Ambiguous operations gateway');const original=rows[0],code=patchAdminReadHelpers(original.code); writeFileSync(join(dir,'original.json'),JSON.stringify(original),{mode:0o600});writeFileSync(join(dir,'candidate.js'),code,{mode:0o600}); const manifest={preparedAt:new Date().toISOString(),objectId:original.objectId,path:original.path,beforeHash:hash(original.code),afterHash:hash(code),helpers:optimizedReadHelpers}; writeFileSync(join(dir,'manifest.json'),JSON.stringify(manifest,null,2),{mode:0o600});console.log(JSON.stringify({mode,dir,...manifest})); }else{ const manifest=JSON.parse(readFileSync(join(dir,'manifest.json'),'utf8')),original=JSON.parse(readFileSync(join(dir,'original.json'),'utf8')),candidate=readFileSync(join(dir,'candidate.js'),'utf8'); if(hash(original.code)!==manifest.beforeHash||hash(candidate)!==manifest.afterHash)throw Error('Prepared release has changed'); const current=await parse('/classes/Function/'+manifest.objectId),expected=mode==='deploy'?manifest.beforeHash:manifest.afterHash,target=mode==='deploy'?candidate:original.code; if(hash(current.code)===hash(target)){console.log(JSON.stringify({mode,unchanged:true}));process.exit(0);} if(hash(current.code)!==expected)throw Error('Online gateway changed after preparation; reprepare and revalidate'); const sync=await fetch(base.replace(/\/parse\/?$/,'/api/functions/xiaoshu/sync/direct-v1'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{action:'status'}}),signal:AbortSignal.timeout(30000)}),status=await sync.json(); if(!sync.ok||status.success===false||!Array.isArray(status.data?.runs))throw Error('Unable to verify synchronization is idle'); if(status.data.runs.some(r=>r.status==='running'))throw Error('Synchronization is running; retry when idle'); await parse('/classes/Function/'+manifest.objectId,'PUT',{code:target}); const verified=await parse('/classes/Function/'+manifest.objectId);if(hash(verified.code)!==hash(target))throw Error('Release readback mismatch'); writeFileSync(join(dir,mode+'-receipt.json'),JSON.stringify({at:new Date().toISOString(),hash:hash(target)}),{mode:0o600});console.log(JSON.stringify({mode,verified:true,hash:hash(target)})); }