@sma1lboy/rove 0.9.283-next.86 → 0.9.283-next.89
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/cli/{chunk-0xns9fat.js → chunk-0sb22rnm.js} +4 -4
- package/dist/cli/{chunk-6efsknfr.js → chunk-9snqpah0.js} +1 -1
- package/dist/cli/{chunk-c3xqh7vp.js → chunk-dznh9kx4.js} +1 -1
- package/dist/cli/{chunk-s7ygq6hr.js → chunk-f7a84zv4.js} +6 -6
- package/dist/cli/{chunk-5pp9pvcm.js → chunk-k77ysa7w.js} +1 -1
- package/dist/cli/chunk-rmnm3f9n.js +3 -0
- package/dist/cli/{chunk-ycb9tyj3.js → chunk-sdtp05gh.js} +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/pty-host-node.mjs +3 -1
- package/dist/web/assets/index-e7XrOFSi.js +154 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-tq0h3vy6.js +0 -3
- package/dist/web/assets/index-Bpal5sTS.js +0 -154
|
@@ -92,7 +92,7 @@ usage: rove-bridge [--preset tailscale|cf|relay] [--host <addr>]... [--port <n>]
|
|
|
92
92
|
--unlock lift a password-login lockout, then exit
|
|
93
93
|
|
|
94
94
|
--cf-team, --cf-aud, --public-host and --relay-server may also live in
|
|
95
|
-
<ROVE_HOME>/.rove/bridge/config.json as cfTeam, cfAud, publicHost, relayServer.`)}if(preset==="cf")publicHost??=config.publicHost??null;if(publicHost!==null&&!/^[A-Za-z0-9.-]+$/.test(publicHost))throw Error(`--public-host must be a bare hostname (no scheme, port or path), got ${publicHost}`);if(preset!=="none"&&hosts.length>0)throw Error("--host cannot be combined with --preset; the preset picks the address");let base={port,rotateToken,rotateRelay,relayShellDir,qr,device,action,relayServer:null};if(preset==="tailscale"){let ts=deps.tailscale?.()??{ipv4:null,dnsName:null};if(!ts.ipv4)throw Error("--preset tailscale: `tailscale ip -4` returned no address \u2014 is Tailscale installed and up?");return{...base,preset,hosts:[ts.ipv4],publicHost:publicHost??ts.dnsName,tls:publicHost!==null,cloudflare:null}}if(preset==="cf"){if(!cfTeam||!cfAud)throw Error("--preset cf needs --cf-team and --cf-aud (or cfTeam/cfAud in bridge/config.json)");if(!publicHost)throw Error("--preset cf needs --public-host: the tunnel hostname the phone dials");return{...base,preset,hosts:["127.0.0.1"],publicHost,tls:!0,cloudflare:{teamDomain:teamDomainOf(cfTeam),aud:cfAud.trim()}}}if(preset==="relay"){if(!relayServer)throw Error("--preset relay needs --relay-server <url> (or relayServer in bridge/config.json)");if(publicHost!==null)throw Error("--public-host cannot be combined with --preset relay");return{...base,preset,hosts:["127.0.0.1"],publicHost:null,tls:!1,cloudflare:null,relayServer:relayOriginOf(relayServer)}}return{...base,preset,hosts:hosts.length>0?[...new Set(hosts)]:["127.0.0.1"],publicHost,tls:publicHost!==null,cloudflare:null}}function isSnapshot(value){return!!value&&typeof value==="object"&&"rows"in value&&Array.isArray(value.rows)&&"rev"in value&&typeof value.rev==="number"}class DispatchFeed{client;snapshot=null;listeners=new Set;constructor(client){this.client=client;client.on("dispatch.snapshot",(frame)=>this.accept(frame.payload,!1))}current(){return this.snapshot}async resync(){this.accept(await this.client.request("dispatch.list").catch(()=>null),!0)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}accept(value,whole){if(!isSnapshot(value))return;if(!whole&&this.snapshot&&value.rev<=this.snapshot.rev)return;this.snapshot=value;for(let listener of this.listeners)listener(value)}}class TaskFeed{read;now;latest=null;latestKey="";listeners=new Set;timer=null;inflight=null;constructor(read,now=Date.now){this.read=read;this.now=now}get active(){return this.listeners.size>0}poke(){if(this.listeners.size===0||this.timer)return;this.timer=setTimeout(()=>{this.timer=null,this.refresh().catch(()=>{})},250)}refresh(){if(this.inflight)return this.inflight;let p=this.read().then((payload)=>{this.latest={payload,readAt:this.now()};let key=JSON.stringify(payload,(k,v)=>k==="forMs"?void 0:v);if(key!==this.latestKey){this.latestKey=key;for(let listener of this.listeners)listener(payload)}return payload});this.inflight=p;let clear=()=>{if(this.inflight===p)this.inflight=null};return p.then(clear,clear),p}current(){if(!this.latest)return this.refresh();let elapsed=Math.max(0,this.now()-this.latest.readAt),{payload}=this.latest;return Promise.resolve({...payload,tasks:payload.tasks.map((t)=>t.activity?{...t,activity:{...t.activity,forMs:t.activity.forMs+elapsed}}:t)})}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}}var STALE_NOTICE_MS=1e4;class NoticeFeed{now;listeners=new Set;lastAt=0;constructor(now=Date.now){this.now=now}push(notice){if(!notice||typeof notice.title!=="string"||typeof notice.at!=="number")return;if(notice.at<=this.lastAt)return;if(this.lastAt=notice.at,this.now()-notice.at>1e4)return;let{title,body,kind,taskId,source,at}=notice,clean={title,kind:typeof kind==="string"?kind:"done",at,...body?{body}:{},...taskId?{taskId}:{},...source?{source}:{}};for(let listener of this.listeners)listener(clean)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}}var BRIDGE_PROTOCOL_VERSION=1,OPS=["hello","api.invoke","tasks.subscribe","tasks.list","engines.list","repos.list","task.create","task.delete","task.land","task.tabs","tab.new","tab.close","term.attach","term.input","term.resize","term.detach","term.ack","term.revive","diff.files","diff.file","diff.refs","attention.dismiss","ui.viewing","push.key","push.status","push.subscribe","push.unsubscribe"];function isCoreOp(op){return OPS.includes(op)}class BridgeError extends Error{code;detail;constructor(code,message,detail){super(message);this.code=code;this.detail=detail;this.name="BridgeError"}}function parseRequest(raw,known=isCoreOp){let frame;try{frame=JSON.parse(raw)}catch{throw new BridgeError("BAD_FRAME","frame is not JSON")}if(!frame||typeof frame!=="object"||Array.isArray(frame))throw new BridgeError("BAD_FRAME","frame is not an object");let{id,op,args}=frame;if(typeof id!=="number"||!Number.isSafeInteger(id))throw new BridgeError("BAD_FRAME","frame needs an integer id");if(typeof op!=="string"||!known(op))throw Object.assign(new BridgeError("UNKNOWN_OP",`unknown op: ${String(op)}`),{requestId:id});if(args!==void 0&&(args===null||typeof args!=="object"||Array.isArray(args)))throw Object.assign(new BridgeError("BAD_ARGS","args must be an object"),{requestId:id});return{id,op,args:args??{}}}function requestIdOf(err){let id=err&&typeof err==="object"&&"requestId"in err?err.requestId:null;return typeof id==="number"?id:null}function str(args,name){let v=args[name];if(typeof v!=="string"||v.length===0)throw new BridgeError("BAD_ARGS",`${name} must be a non-empty string`);return v}function optStr(args,name){let v=args[name];if(v===void 0||v===null||v==="")return;if(typeof v!=="string")throw new BridgeError("BAD_ARGS",`${name} must be a string`);return v}function optInt(args,name,min,max){let v=args[name];if(v===void 0||v===null)return;if(typeof v!=="number"||!Number.isInteger(v)||v<min||v>max)throw new BridgeError("BAD_ARGS",`${name} must be an integer in ${min}..${max}`);return v}function optBool(args,name){let v=args[name];if(v===void 0||v===null)return!1;if(typeof v!=="boolean")throw new BridgeError("BAD_ARGS",`${name} must be a boolean`);return v}function responseOk(id,result){return JSON.stringify({id,ok:!0,result:result??{}})}function responseError(id,code,message,detail){return JSON.stringify({id,ok:!1,error:{...detail,code,message}})}function pushEvent(event,data){return JSON.stringify({event,data})}function asBridgeError(err,fallback){let code=err&&typeof err==="object"&&"code"in err?err.code:void 0,data=err instanceof ApiError?err.data:void 0;return new BridgeError(typeof code==="string"?code:fallback,err instanceof Error?err.message:String(err),data)}function taskIdOf(argv){for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--task-id")return argv[i+1];if(arg?.startsWith("--task-id="))return arg.slice(10)}return}function createBridgeApi(client,remote){return{machineOf:(taskId)=>remote?.machineOf(taskId),async verb(name,argv,machine){if(machine!==void 0&&machine!=="local"&&!remote)throw new BridgeError("UNKNOWN_MACHINE",`this bridge serves no machine named "${machine}"`);let taskId=taskIdOf(argv),alias=machine!==void 0&&machine!=="local"?machine:taskId===void 0?void 0:remote?.machineOf(taskId);if(alias!==void 0&&remote){let envelope;try{envelope=await remote.forward(alias,name,argvToParams(name,argv))}catch(err){throw asBridgeError(err,"RPC_ERROR")}if(envelope.ok)return envelope.result;let{code,message,...data}=envelope.error;throw new BridgeError(code,message,data)}try{return await invokeVerb(name,argv,{client})}catch(err){throw asBridgeError(err,"RPC_ERROR")}},async rpc(name,payload){try{return await client.request(name,payload)}catch(err){throw asBridgeError(err,"RPC_ERROR")}}}}var REASON_BY_CODE={MACHINE_UNREACHABLE:"offline",MACHINE_VERSION_SKEW:"outdated",UNKNOWN_VERB:"outdated",INBOUND_DENIED:"denied"};async function attempt(call){try{return{ok:!0,result:await call}}catch(err){if(!(err instanceof BridgeError))throw err;return{ok:!1,reason:Object.hasOwn(REASON_BY_CODE,err.code)?REASON_BY_CODE[err.code]:"failed",message:err.message}}}function machineOf(args){let machine=optStr(args,"machine");if(machine!==void 0&&machine.length>64)throw new BridgeError("BAD_ARGS","machine is too long");return machine==="local"?void 0:machine}function repoRows(raw){let rows=raw?.repos;if(!Array.isArray(rows))return[];return rows.flatMap((row)=>typeof row?.id==="string"&&Array.isArray(row.checkouts)?[{id:row.id,primary:typeof row.primary==="string"?row.primary:null,checkouts:row.checkouts.filter((c)=>typeof c==="string")}]:[])}class MachineStore{rows=[];set(statuses){this.rows=statuses.filter((s)=>!s.duplicateOf).map((s)=>({alias:s.alias,hostLabel:s.hostLabel,state:s.state,version:s.version??null,reason:s.state==="online"?null:s.reason??null}))}get(){return this.rows}}var machineStore=new MachineStore;function createMachineOps(store=machineStore){return{"machines.list":{kind:"read",destructive:!1,wraps:"MachineFeed.machinesSignal (the daemon's machines channel, as the TUI sidebar shows it)",async run(){return{machines:store.get()}}},"machines.repos":{kind:"read",destructive:!1,wraps:"repo-list (on `machine`): that machine's repos by id, with their checkouts",async run(args,ctx){let machine=machineOf(args);if(!machine)throw new BridgeError("BAD_ARGS","machine must name a registered machine");let state=store.get().find((row)=>row.alias===machine)?.state;if(state!==void 0&&state!=="online")return{repos:[],reason:state==="mismatch"||state==="unsupported"?"outdated":"offline",message:`machine "${machine}" is ${state}`};let out=await attempt(ctx.api.verb("repo-list",[],machine));return out.ok?{repos:repoRows(out.result)}:{repos:[],reason:out.reason,message:out.message}}}}}var machineOps=createMachineOps();var KEY_HINTS_ENABLED_KEY="hints.keyboard.enabled",PANE_HINT_USED_KEYS=["hints.sidebar.used","hints.files.used"],RAIL_FOLD_STYLES=appearanceChoices("railFold",[]).map((c)=>c.value),bool=(key,fallback,extra)=>({kind:"bool",read:(s)=>typeof s[key]==="boolean"?s[key]:fallback,write:(v)=>({[key]:v,...extra?.(v)})}),choice=(key,choices,read,stored=(v)=>v)=>({kind:"enum",choices:()=>choices,read:(s)=>read(s[key]),write:(v)=>({[key]:stored(v)})}),appearance=(setting)=>appearanceChoices(setting,[]).map((c)=>c.value),text=(raw)=>typeof raw==="string"?raw.trim():"";function worktreeKind(state){return worktreeBaseKindOf(text(state[WORKTREE_BASE_KEY]))}function worktreeCustomPath(state){return text(state[WORKTREE_BASE_CUSTOM_KEY])||(worktreeKind(state)==="custom"?text(state[WORKTREE_BASE_KEY]):"")}var WORKTREE_KINDS=["default","nextToProject","custom"],PREF_DEFS={locale:choice("locale",LOCALES.map((l)=>l.id),(raw)=>isLocaleId(raw)?raw:DEFAULT_LOCALE),theme:{kind:"enum",choices:(themes)=>themes,read:(s)=>typeof s.activeTheme==="string"&&s.activeTheme?s.activeTheme:DEFAULT_THEME,write:(v)=>({activeTheme:v})},themeMode:choice("themeMode",THEME_MODE_PREFERENCES,(raw)=>THEME_MODE_PREFERENCES.includes(raw)?raw:DEFAULT_THEME_MODE),transparent:bool("transparentBackground",defaultTransparentBackground()),focusAccent:choice("focusAccent",FOCUS_ACCENT_SLOTS,(raw)=>FOCUS_ACCENT_SLOTS.includes(raw)?raw:"primary"),splitStyle:choice(SPLIT_STYLE_KEY,appearance("splitStyle"),normalizeSplitStyle),railFold:choice(RAIL_FOLD_STYLE_KEY,RAIL_FOLD_STYLES,(raw)=>RAIL_FOLD_STYLES.includes(raw)?raw:"glyphs"),workingBorder:choice(WORKING_BORDER_KEY,appearance("workingBorder"),(raw)=>normalizeWorkingBorder(raw??DEFAULT_WORKING_BORDER)),taskColors:choice(TASK_COLORS_KEY,appearance("taskColors"),(raw)=>normalizeTaskColors(raw??DEFAULT_TASK_COLORS)),glyphSet:choice(GLYPH_SET_KEY,appearance("glyphSet"),(raw)=>normalizeGlyphSet(raw??DEFAULT_GLYPH_SET)),colorblind:choice(COLORBLIND_KEY,appearance("colorblind"),(raw)=>normalizeColorblind(raw??DEFAULT_COLORBLIND)),runningTitle:choice(RUNNING_TITLE_KEY,appearance("runningTitle"),(raw)=>normalizeRunningTitle(raw??DEFAULT_RUNNING_TITLE)),toast:bool("notifications.toast.enabled",!0),sound:bool("notifications.sound.enabled",!0),soundVolume:choice(SOUND_VOLUME_KEY,SOUND_VOLUME_STEPS,(raw)=>normalizeSoundVolume(raw??DEFAULT_SOUND_VOLUME)),crossTask:bool("notifications.crossTask.enabled",!0),finishedAlerts:bool(FINISHED_ALERTS_KEY,!0),keyHints:bool(KEY_HINTS_ENABLED_KEY,!0,(on)=>on?Object.fromEntries(PANE_HINT_USED_KEYS.map((k)=>[k,!1])):{}),zen:bool(ZEN_ACTIVE_KEY,ZEN_DEFAULT),editorKind:choice(EDITOR_KIND_KEY,EDITOR_KINDS,(raw)=>normalizeEditorKind(raw??DEFAULT_EDITOR_KIND)),worktreeBase:{kind:"enum",choices:()=>WORKTREE_KINDS,read:(s)=>worktreeKind(s),write:(kind,s)=>{let current=text(s[WORKTREE_BASE_KEY]),remember=worktreeKind(s)==="custom"&&kind!=="custom"?{[WORKTREE_BASE_CUSTOM_KEY]:current}:{};if(kind==="default")return{...remember,[WORKTREE_BASE_KEY]:""};if(kind==="nextToProject")return{...remember,[WORKTREE_BASE_KEY]:PROJECT_SIBLING_BASE};let custom=worktreeCustomPath(s);if(!custom)throw new BridgeError("BAD_ARGS","no custom worktree base is remembered; set one in the TUI");return{[WORKTREE_BASE_KEY]:custom}}},scrollbackRows:{kind:"int",min:SCROLLBACK_ROWS_MIN,max:SCROLLBACK_ROWS_MAX,read:(s)=>normalizeScrollbackRows(s[SCROLLBACK_ROWS_KEY]??DEFAULT_SCROLLBACK_ROWS),write:(v)=>({[SCROLLBACK_ROWS_KEY]:normalizeScrollbackRows(v)})},tabStrip:{kind:"enum",choices:()=>TAB_STRIP_MODES,read:(s)=>resolveTabStripMode(s[TAB_STRIP_MODE_KEY],s[TAB_STRIP_HIDE_SINGLE_KEY]),write:(v)=>({[TAB_STRIP_MODE_KEY]:v})},prefixTap:choice(PREFIX_TAP_PRESENTATION_KEY,PREFIX_TAP_PRESENTATIONS,(raw)=>normalizePrefixTapPresentation(raw??DEFAULT_PREFIX_TAP_PRESENTATION)),remoteProjects:bool("experimental.remoteProjects",!1),autoStatus:bool(AUTO_STATUS_KEY,!1),dispatcher:bool(DISPATCHER_KEY,!1),updateChannel:choice(UPDATE_CHANNEL_KEY,RELEASE_CHANNELS,(raw)=>normalizeReleaseChannel(raw)??channelOf()),updateNotify:bool(UPDATE_NOTIFY_KEY,!0)};function prefInfo(state){return{editorCustomSet:text(state[EDITOR_CUSTOM_KEY]).length>0,worktreeCustomPath:worktreeCustomPath(state)}}function checkPrefValue(name,def,value,themes){let bad=(why)=>{throw new BridgeError("BAD_ARGS",`${name} ${why}`)};if(def.kind==="bool")return typeof value==="boolean"?value:bad("must be a boolean");if(def.kind==="int"){if(typeof value!=="number"||!Number.isInteger(value))return bad("must be an integer");return value<def.min||value>def.max?bad(`must be ${def.min}\u2013${def.max}`):value}let choices=def.choices(themes);if((typeof value==="string"||typeof value==="number")&&choices.includes(value))return value;return bad(`must be one of ${choices.join("|")}`)}var VIEWS=["browser","files","diff"];class PanelFeed{now;listeners=new Set;lastKey="";lastAt=0;constructor(now=Date.now){this.now=now}push(panel){if(!panel||typeof panel.taskId!=="string"||typeof panel.at!=="number"||!VIEWS.includes(panel.view))return;let{taskId,view,url,path,at}=panel,key=JSON.stringify([at,taskId,view,url,path]);if(at<this.lastAt||key===this.lastKey)return;if(this.lastAt=at,this.lastKey=key,this.now()-at>STALE_NOTICE_MS)return;let clean={taskId,view,at,...typeof url==="string"?{url}:{},...typeof path==="string"?{path}:{}};for(let listener of this.listeners)listener(clean)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}}import{randomBytes as randomBytes2,scrypt,timingSafeEqual as timingSafeEqual3}from"crypto";import{chmodSync as chmodSync2,existsSync as existsSync2,mkdirSync as mkdirSync2,readFileSync as readFileSync2,renameSync,rmSync,writeFileSync as writeFileSync2}from"fs";import{dirname as dirname2,join}from"path";var COST={N:32768,r:8,p:1},KEY_LENGTH=32,MIN_PASSWORD_LENGTH=8,MAX_PASSWORD_LENGTH=1024;function webPasswordPaths(bridgeDir){return{password:join(bridgeDir,"web-password.json"),lockout:join(bridgeDir,"web-login-lock.json")}}function derive(password,salt,cost){return new Promise((resolve,reject)=>{scrypt(password,salt,KEY_LENGTH,{...cost,maxmem:256*cost.N*cost.r},(err,key)=>err?reject(err):resolve(key))})}async function hashPassword(password){let salt=randomBytes2(16),key=await derive(password,salt,COST);return{algo:"scrypt",...COST,salt:salt.toString("base64"),hash:key.toString("base64")}}async function verifyPassword(record,password){let expected=Buffer.from(record.hash,"base64"),key=await derive(password,Buffer.from(record.salt,"base64"),record);return key.length===expected.length&&timingSafeEqual3(key,expected)}function writeOwnerOnly(path,body){mkdirSync2(dirname2(path),{recursive:!0,mode:448}),chmodSync2(dirname2(path),448);let tmp=`${path}.${process.pid}.tmp`;writeFileSync2(tmp,body,{mode:384}),chmodSync2(tmp,384),renameSync(tmp,path)}function readPassword(path){if(!existsSync2(path))return null;try{let raw=JSON.parse(readFileSync2(path,"utf8")),pow2=(n)=>typeof n==="number"&&Number.isInteger(Math.log2(n));if(raw.algo!=="scrypt"||!pow2(raw.N)||raw.N<COST.N||raw.N>1048576)return null;if(typeof raw.r!=="number"||typeof raw.p!=="number")return null;if(typeof raw.salt!=="string"||typeof raw.hash!=="string")return null;return raw}catch{return null}}function writePassword(path,record){writeOwnerOnly(path,`${JSON.stringify(record,null,2)}
|
|
95
|
+
<ROVE_HOME>/.rove/bridge/config.json as cfTeam, cfAud, publicHost, relayServer.`)}if(preset==="cf")publicHost??=config.publicHost??null;if(publicHost!==null&&!/^[A-Za-z0-9.-]+$/.test(publicHost))throw Error(`--public-host must be a bare hostname (no scheme, port or path), got ${publicHost}`);if(preset!=="none"&&hosts.length>0)throw Error("--host cannot be combined with --preset; the preset picks the address");let base={port,rotateToken,rotateRelay,relayShellDir,qr,device,action,relayServer:null};if(preset==="tailscale"){let ts=deps.tailscale?.()??{ipv4:null,dnsName:null};if(!ts.ipv4)throw Error("--preset tailscale: `tailscale ip -4` returned no address \u2014 is Tailscale installed and up?");return{...base,preset,hosts:[ts.ipv4],publicHost:publicHost??ts.dnsName,tls:publicHost!==null,cloudflare:null}}if(preset==="cf"){if(!cfTeam||!cfAud)throw Error("--preset cf needs --cf-team and --cf-aud (or cfTeam/cfAud in bridge/config.json)");if(!publicHost)throw Error("--preset cf needs --public-host: the tunnel hostname the phone dials");return{...base,preset,hosts:["127.0.0.1"],publicHost,tls:!0,cloudflare:{teamDomain:teamDomainOf(cfTeam),aud:cfAud.trim()}}}if(preset==="relay"){if(!relayServer)throw Error("--preset relay needs --relay-server <url> (or relayServer in bridge/config.json)");if(publicHost!==null)throw Error("--public-host cannot be combined with --preset relay");return{...base,preset,hosts:["127.0.0.1"],publicHost:null,tls:!1,cloudflare:null,relayServer:relayOriginOf(relayServer)}}return{...base,preset,hosts:hosts.length>0?[...new Set(hosts)]:["127.0.0.1"],publicHost,tls:publicHost!==null,cloudflare:null}}class CredentialSockets{byCredential=new Map;add(ws){let set=this.byCredential.get(ws.data.credential)??new Set;set.add(ws),this.byCredential.set(ws.data.credential,set)}remove(ws){let set=this.byCredential.get(ws.data.credential);if(!set)return;if(set.delete(ws),set.size===0)this.byCredential.delete(ws.data.credential)}closeCredential(credential,reason="signed out"){let set=this.byCredential.get(credential);if(!set)return 0;this.byCredential.delete(credential);for(let ws of set)ws.close(4401,reason);return set.size}}function isSnapshot(value){return!!value&&typeof value==="object"&&"rows"in value&&Array.isArray(value.rows)&&"rev"in value&&typeof value.rev==="number"}class DispatchFeed{client;snapshot=null;listeners=new Set;constructor(client){this.client=client;client.on("dispatch.snapshot",(frame)=>this.accept(frame.payload,!1))}current(){return this.snapshot}async resync(){this.accept(await this.client.request("dispatch.list").catch(()=>null),!0)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}accept(value,whole){if(!isSnapshot(value))return;if(!whole&&this.snapshot&&value.rev<=this.snapshot.rev)return;this.snapshot=value;for(let listener of this.listeners)listener(value)}}class TaskFeed{read;now;latest=null;latestKey="";listeners=new Set;timer=null;inflight=null;constructor(read,now=Date.now){this.read=read;this.now=now}get active(){return this.listeners.size>0}poke(){if(this.listeners.size===0||this.timer)return;this.timer=setTimeout(()=>{this.timer=null,this.refresh().catch(()=>{})},250)}refresh(){if(this.inflight)return this.inflight;let p=this.read().then((payload)=>{this.latest={payload,readAt:this.now()};let key=JSON.stringify(payload,(k,v)=>k==="forMs"?void 0:v);if(key!==this.latestKey){this.latestKey=key;for(let listener of this.listeners)listener(payload)}return payload});this.inflight=p;let clear=()=>{if(this.inflight===p)this.inflight=null};return p.then(clear,clear),p}current(){if(!this.latest)return this.refresh();let elapsed=Math.max(0,this.now()-this.latest.readAt),{payload}=this.latest;return Promise.resolve({...payload,tasks:payload.tasks.map((t)=>t.activity?{...t,activity:{...t.activity,forMs:t.activity.forMs+elapsed}}:t)})}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}}var STALE_NOTICE_MS=1e4;class NoticeFeed{now;listeners=new Set;lastAt=0;constructor(now=Date.now){this.now=now}push(notice){if(!notice||typeof notice.title!=="string"||typeof notice.at!=="number")return;if(notice.at<=this.lastAt)return;if(this.lastAt=notice.at,this.now()-notice.at>1e4)return;let{title,body,kind,taskId,source,at}=notice,clean={title,kind:typeof kind==="string"?kind:"done",at,...body?{body}:{},...taskId?{taskId}:{},...source?{source}:{}};for(let listener of this.listeners)listener(clean)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}}var BRIDGE_PROTOCOL_VERSION=1,OPS=["hello","api.invoke","tasks.subscribe","tasks.list","engines.list","repos.list","task.create","task.delete","task.land","task.tabs","tab.new","tab.close","term.attach","term.input","term.resize","term.detach","term.ack","term.revive","diff.files","diff.file","diff.refs","attention.dismiss","ui.viewing","push.key","push.status","push.subscribe","push.unsubscribe"];function isCoreOp(op){return OPS.includes(op)}class BridgeError extends Error{code;detail;constructor(code,message,detail){super(message);this.code=code;this.detail=detail;this.name="BridgeError"}}function parseRequest(raw,known=isCoreOp){let frame;try{frame=JSON.parse(raw)}catch{throw new BridgeError("BAD_FRAME","frame is not JSON")}if(!frame||typeof frame!=="object"||Array.isArray(frame))throw new BridgeError("BAD_FRAME","frame is not an object");let{id,op,args}=frame;if(typeof id!=="number"||!Number.isSafeInteger(id))throw new BridgeError("BAD_FRAME","frame needs an integer id");if(typeof op!=="string"||!known(op))throw Object.assign(new BridgeError("UNKNOWN_OP",`unknown op: ${String(op)}`),{requestId:id});if(args!==void 0&&(args===null||typeof args!=="object"||Array.isArray(args)))throw Object.assign(new BridgeError("BAD_ARGS","args must be an object"),{requestId:id});return{id,op,args:args??{}}}function requestIdOf(err){let id=err&&typeof err==="object"&&"requestId"in err?err.requestId:null;return typeof id==="number"?id:null}function str(args,name){let v=args[name];if(typeof v!=="string"||v.length===0)throw new BridgeError("BAD_ARGS",`${name} must be a non-empty string`);return v}function optStr(args,name){let v=args[name];if(v===void 0||v===null||v==="")return;if(typeof v!=="string")throw new BridgeError("BAD_ARGS",`${name} must be a string`);return v}function optInt(args,name,min,max){let v=args[name];if(v===void 0||v===null)return;if(typeof v!=="number"||!Number.isInteger(v)||v<min||v>max)throw new BridgeError("BAD_ARGS",`${name} must be an integer in ${min}..${max}`);return v}function optBool(args,name){let v=args[name];if(v===void 0||v===null)return!1;if(typeof v!=="boolean")throw new BridgeError("BAD_ARGS",`${name} must be a boolean`);return v}function responseOk(id,result){return JSON.stringify({id,ok:!0,result:result??{}})}function responseError(id,code,message,detail){return JSON.stringify({id,ok:!1,error:{...detail,code,message}})}function pushEvent(event,data){return JSON.stringify({event,data})}function asBridgeError(err,fallback){let code=err&&typeof err==="object"&&"code"in err?err.code:void 0,data=err instanceof ApiError?err.data:void 0;return new BridgeError(typeof code==="string"?code:fallback,err instanceof Error?err.message:String(err),data)}function taskIdOf(argv){for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--task-id")return argv[i+1];if(arg?.startsWith("--task-id="))return arg.slice(10)}return}function createBridgeApi(client,remote){return{machineOf:(taskId)=>remote?.machineOf(taskId),async verb(name,argv,machine){if(machine!==void 0&&machine!=="local"&&!remote)throw new BridgeError("UNKNOWN_MACHINE",`this bridge serves no machine named "${machine}"`);let taskId=taskIdOf(argv),alias=machine!==void 0&&machine!=="local"?machine:taskId===void 0?void 0:remote?.machineOf(taskId);if(alias!==void 0&&remote){let envelope;try{envelope=await remote.forward(alias,name,argvToParams(name,argv))}catch(err){throw asBridgeError(err,"RPC_ERROR")}if(envelope.ok)return envelope.result;let{code,message,...data}=envelope.error;throw new BridgeError(code,message,data)}try{return await invokeVerb(name,argv,{client})}catch(err){throw asBridgeError(err,"RPC_ERROR")}},async rpc(name,payload){try{return await client.request(name,payload)}catch(err){throw asBridgeError(err,"RPC_ERROR")}}}}var REASON_BY_CODE={MACHINE_UNREACHABLE:"offline",MACHINE_VERSION_SKEW:"outdated",UNKNOWN_VERB:"outdated",INBOUND_DENIED:"denied"};async function attempt(call){try{return{ok:!0,result:await call}}catch(err){if(!(err instanceof BridgeError))throw err;return{ok:!1,reason:Object.hasOwn(REASON_BY_CODE,err.code)?REASON_BY_CODE[err.code]:"failed",message:err.message}}}function machineOf(args){let machine=optStr(args,"machine");if(machine!==void 0&&machine.length>64)throw new BridgeError("BAD_ARGS","machine is too long");return machine==="local"?void 0:machine}function repoRows(raw){let rows=raw?.repos;if(!Array.isArray(rows))return[];return rows.flatMap((row)=>typeof row?.id==="string"&&Array.isArray(row.checkouts)?[{id:row.id,primary:typeof row.primary==="string"?row.primary:null,checkouts:row.checkouts.filter((c)=>typeof c==="string")}]:[])}class MachineStore{rows=[];set(statuses){this.rows=statuses.filter((s)=>!s.duplicateOf).map((s)=>({alias:s.alias,hostLabel:s.hostLabel,state:s.state,version:s.version??null,reason:s.state==="online"?null:s.reason??null}))}get(){return this.rows}}var machineStore=new MachineStore;function createMachineOps(store=machineStore){return{"machines.list":{kind:"read",destructive:!1,wraps:"MachineFeed.machinesSignal (the daemon's machines channel, as the TUI sidebar shows it)",async run(){return{machines:store.get()}}},"machines.repos":{kind:"read",destructive:!1,wraps:"repo-list (on `machine`): that machine's repos by id, with their checkouts",async run(args,ctx){let machine=machineOf(args);if(!machine)throw new BridgeError("BAD_ARGS","machine must name a registered machine");let state=store.get().find((row)=>row.alias===machine)?.state;if(state!==void 0&&state!=="online")return{repos:[],reason:state==="mismatch"||state==="unsupported"?"outdated":"offline",message:`machine "${machine}" is ${state}`};let out=await attempt(ctx.api.verb("repo-list",[],machine));return out.ok?{repos:repoRows(out.result)}:{repos:[],reason:out.reason,message:out.message}}}}}var machineOps=createMachineOps();var KEY_HINTS_ENABLED_KEY="hints.keyboard.enabled",PANE_HINT_USED_KEYS=["hints.sidebar.used","hints.files.used"],RAIL_FOLD_STYLES=appearanceChoices("railFold",[]).map((c)=>c.value),bool=(key,fallback,extra)=>({kind:"bool",read:(s)=>typeof s[key]==="boolean"?s[key]:fallback,write:(v)=>({[key]:v,...extra?.(v)})}),choice=(key,choices,read,stored=(v)=>v)=>({kind:"enum",choices:()=>choices,read:(s)=>read(s[key]),write:(v)=>({[key]:stored(v)})}),appearance=(setting)=>appearanceChoices(setting,[]).map((c)=>c.value),text=(raw)=>typeof raw==="string"?raw.trim():"";function worktreeKind(state){return worktreeBaseKindOf(text(state[WORKTREE_BASE_KEY]))}function worktreeCustomPath(state){return text(state[WORKTREE_BASE_CUSTOM_KEY])||(worktreeKind(state)==="custom"?text(state[WORKTREE_BASE_KEY]):"")}var WORKTREE_KINDS=["default","nextToProject","custom"],PREF_DEFS={locale:choice("locale",LOCALES.map((l)=>l.id),(raw)=>isLocaleId(raw)?raw:DEFAULT_LOCALE),theme:{kind:"enum",choices:(themes)=>themes,read:(s)=>typeof s.activeTheme==="string"&&s.activeTheme?s.activeTheme:DEFAULT_THEME,write:(v)=>({activeTheme:v})},themeMode:choice("themeMode",THEME_MODE_PREFERENCES,(raw)=>THEME_MODE_PREFERENCES.includes(raw)?raw:DEFAULT_THEME_MODE),transparent:bool("transparentBackground",defaultTransparentBackground()),focusAccent:choice("focusAccent",FOCUS_ACCENT_SLOTS,(raw)=>FOCUS_ACCENT_SLOTS.includes(raw)?raw:"primary"),splitStyle:choice(SPLIT_STYLE_KEY,appearance("splitStyle"),normalizeSplitStyle),railFold:choice(RAIL_FOLD_STYLE_KEY,RAIL_FOLD_STYLES,(raw)=>RAIL_FOLD_STYLES.includes(raw)?raw:"glyphs"),workingBorder:choice(WORKING_BORDER_KEY,appearance("workingBorder"),(raw)=>normalizeWorkingBorder(raw??DEFAULT_WORKING_BORDER)),taskColors:choice(TASK_COLORS_KEY,appearance("taskColors"),(raw)=>normalizeTaskColors(raw??DEFAULT_TASK_COLORS)),glyphSet:choice(GLYPH_SET_KEY,appearance("glyphSet"),(raw)=>normalizeGlyphSet(raw??DEFAULT_GLYPH_SET)),colorblind:choice(COLORBLIND_KEY,appearance("colorblind"),(raw)=>normalizeColorblind(raw??DEFAULT_COLORBLIND)),runningTitle:choice(RUNNING_TITLE_KEY,appearance("runningTitle"),(raw)=>normalizeRunningTitle(raw??DEFAULT_RUNNING_TITLE)),toast:bool("notifications.toast.enabled",!0),sound:bool("notifications.sound.enabled",!0),soundVolume:choice(SOUND_VOLUME_KEY,SOUND_VOLUME_STEPS,(raw)=>normalizeSoundVolume(raw??DEFAULT_SOUND_VOLUME)),crossTask:bool("notifications.crossTask.enabled",!0),finishedAlerts:bool(FINISHED_ALERTS_KEY,!0),keyHints:bool(KEY_HINTS_ENABLED_KEY,!0,(on)=>on?Object.fromEntries(PANE_HINT_USED_KEYS.map((k)=>[k,!1])):{}),zen:bool(ZEN_ACTIVE_KEY,ZEN_DEFAULT),editorKind:choice(EDITOR_KIND_KEY,EDITOR_KINDS,(raw)=>normalizeEditorKind(raw??DEFAULT_EDITOR_KIND)),worktreeBase:{kind:"enum",choices:()=>WORKTREE_KINDS,read:(s)=>worktreeKind(s),write:(kind,s)=>{let current=text(s[WORKTREE_BASE_KEY]),remember=worktreeKind(s)==="custom"&&kind!=="custom"?{[WORKTREE_BASE_CUSTOM_KEY]:current}:{};if(kind==="default")return{...remember,[WORKTREE_BASE_KEY]:""};if(kind==="nextToProject")return{...remember,[WORKTREE_BASE_KEY]:PROJECT_SIBLING_BASE};let custom=worktreeCustomPath(s);if(!custom)throw new BridgeError("BAD_ARGS","no custom worktree base is remembered; set one in the TUI");return{[WORKTREE_BASE_KEY]:custom}}},scrollbackRows:{kind:"int",min:SCROLLBACK_ROWS_MIN,max:SCROLLBACK_ROWS_MAX,read:(s)=>normalizeScrollbackRows(s[SCROLLBACK_ROWS_KEY]??DEFAULT_SCROLLBACK_ROWS),write:(v)=>({[SCROLLBACK_ROWS_KEY]:normalizeScrollbackRows(v)})},tabStrip:{kind:"enum",choices:()=>TAB_STRIP_MODES,read:(s)=>resolveTabStripMode(s[TAB_STRIP_MODE_KEY],s[TAB_STRIP_HIDE_SINGLE_KEY]),write:(v)=>({[TAB_STRIP_MODE_KEY]:v})},prefixTap:choice(PREFIX_TAP_PRESENTATION_KEY,PREFIX_TAP_PRESENTATIONS,(raw)=>normalizePrefixTapPresentation(raw??DEFAULT_PREFIX_TAP_PRESENTATION)),remoteProjects:bool("experimental.remoteProjects",!1),autoStatus:bool(AUTO_STATUS_KEY,!1),dispatcher:bool(DISPATCHER_KEY,!1),updateChannel:choice(UPDATE_CHANNEL_KEY,RELEASE_CHANNELS,(raw)=>normalizeReleaseChannel(raw)??channelOf()),updateNotify:bool(UPDATE_NOTIFY_KEY,!0)};function prefInfo(state){return{editorCustomSet:text(state[EDITOR_CUSTOM_KEY]).length>0,worktreeCustomPath:worktreeCustomPath(state)}}function checkPrefValue(name,def,value,themes){let bad=(why)=>{throw new BridgeError("BAD_ARGS",`${name} ${why}`)};if(def.kind==="bool")return typeof value==="boolean"?value:bad("must be a boolean");if(def.kind==="int"){if(typeof value!=="number"||!Number.isInteger(value))return bad("must be an integer");return value<def.min||value>def.max?bad(`must be ${def.min}\u2013${def.max}`):value}let choices=def.choices(themes);if((typeof value==="string"||typeof value==="number")&&choices.includes(value))return value;return bad(`must be one of ${choices.join("|")}`)}var VIEWS=["browser","files","diff"];class PanelFeed{now;listeners=new Set;lastKey="";lastAt=0;constructor(now=Date.now){this.now=now}push(panel){if(!panel||typeof panel.taskId!=="string"||typeof panel.at!=="number"||!VIEWS.includes(panel.view))return;let{taskId,view,url,path,at}=panel,key=JSON.stringify([at,taskId,view,url,path]);if(at<this.lastAt||key===this.lastKey)return;if(this.lastAt=at,this.lastKey=key,this.now()-at>STALE_NOTICE_MS)return;let clean={taskId,view,at,...typeof url==="string"?{url}:{},...typeof path==="string"?{path}:{}};for(let listener of this.listeners)listener(clean)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}}import{randomBytes as randomBytes2,scrypt,timingSafeEqual as timingSafeEqual3}from"crypto";import{chmodSync as chmodSync2,existsSync as existsSync2,mkdirSync as mkdirSync2,readFileSync as readFileSync2,renameSync,rmSync,writeFileSync as writeFileSync2}from"fs";import{dirname as dirname2,join}from"path";var COST={N:32768,r:8,p:1},KEY_LENGTH=32,MIN_PASSWORD_LENGTH=8,MAX_PASSWORD_LENGTH=1024;function webPasswordPaths(bridgeDir){return{password:join(bridgeDir,"web-password.json"),lockout:join(bridgeDir,"web-login-lock.json")}}function derive(password,salt,cost){return new Promise((resolve,reject)=>{scrypt(password,salt,KEY_LENGTH,{...cost,maxmem:256*cost.N*cost.r},(err,key)=>err?reject(err):resolve(key))})}async function hashPassword(password){let salt=randomBytes2(16),key=await derive(password,salt,COST);return{algo:"scrypt",...COST,salt:salt.toString("base64"),hash:key.toString("base64")}}async function verifyPassword(record,password){let expected=Buffer.from(record.hash,"base64"),key=await derive(password,Buffer.from(record.salt,"base64"),record);return key.length===expected.length&&timingSafeEqual3(key,expected)}function writeOwnerOnly(path,body){mkdirSync2(dirname2(path),{recursive:!0,mode:448}),chmodSync2(dirname2(path),448);let tmp=`${path}.${process.pid}.tmp`;writeFileSync2(tmp,body,{mode:384}),chmodSync2(tmp,384),renameSync(tmp,path)}function readPassword(path){if(!existsSync2(path))return null;try{let raw=JSON.parse(readFileSync2(path,"utf8")),pow2=(n)=>typeof n==="number"&&Number.isInteger(Math.log2(n));if(raw.algo!=="scrypt"||!pow2(raw.N)||raw.N<COST.N||raw.N>1048576)return null;if(typeof raw.r!=="number"||typeof raw.p!=="number")return null;if(typeof raw.salt!=="string"||typeof raw.hash!=="string")return null;return raw}catch{return null}}function writePassword(path,record){writeOwnerOnly(path,`${JSON.stringify(record,null,2)}
|
|
96
96
|
`)}function clearPassword(path){if(!existsSync2(path))return!1;return rmSync(path,{force:!0}),!0}var IP_FAILURE_LIMIT=5,IP_LOCK_MS=900000,GLOBAL_FAILURE_LIMIT=20,GLOBAL_WINDOW_MS=86400000,EMPTY_LOCKOUT={ips:{},failures:[],globalLocked:!1};function lockOf(state,ip,now){if(state.globalLocked)return{kind:"global"};let until=state.ips[ip]?.lockedUntil;return until&&until>now?{kind:"ip",retryAfterMs:until-now}:null}function prune(state,now){let ips={};for(let[ip,rec]of Object.entries(state.ips))if(rec.lockedUntil!==null&&rec.lockedUntil>now||now-rec.last<GLOBAL_WINDOW_MS)ips[ip]=rec;return{...state,ips,failures:state.failures.filter((t)=>now-t<GLOBAL_WINDOW_MS)}}function recordFailure(prev,ip,now){let state=prune(prev,now),old=state.ips[ip],failures=(old&&old.lockedUntil!==null&&old.lockedUntil<=now?0:old?.failures??0)+1,lockedUntil=failures>=IP_FAILURE_LIMIT?now+IP_LOCK_MS:null,all=[...state.failures,now],next={ips:{...state.ips,[ip]:{failures,lockedUntil,last:now}},failures:all,globalLocked:state.globalLocked||all.length>=GLOBAL_FAILURE_LIMIT};return{state:next,lock:lockOf(next,ip,now)}}function recordSuccess(state,ip){let{[ip]:_,...rest}=state.ips;return{...state,ips:rest}}function readLockout(path){if(!existsSync2(path))return EMPTY_LOCKOUT;try{let raw=JSON.parse(readFileSync2(path,"utf8")),ips={};for(let[ip,rec]of Object.entries(raw.ips??{}))if(rec&&typeof rec.failures==="number"&&typeof rec.last==="number")ips[ip]={failures:rec.failures,last:rec.last,lockedUntil:rec.lockedUntil??null};let failures=Array.isArray(raw.failures)?raw.failures.filter((t)=>typeof t==="number"):[];return{ips,failures,globalLocked:raw.globalLocked===!0}}catch{return{...EMPTY_LOCKOUT,globalLocked:!0}}}function writeLockout(path,state){writeOwnerOnly(path,`${JSON.stringify(state)}
|
|
97
97
|
`)}function promptHidden(question){let stdin=process.stdin;if(!stdin.isTTY)return Promise.reject(Error("--set-password needs an interactive terminal"));return process.stdout.write(question),stdin.setRawMode(!0),stdin.setEncoding("utf8"),stdin.resume(),new Promise((resolve,reject)=>{let value="",finish=()=>{stdin.off("data",onData),stdin.setRawMode(!1),stdin.pause(),process.stdout.write(`
|
|
98
98
|
`)},onData=(chunk)=>{for(let ch of chunk){if(ch==="\r"||ch===`
|
|
@@ -125,13 +125,13 @@ ${updaterShellFailureHint()??""}`),onExit(1)}),child.on("close",(code)=>onExit(c
|
|
|
125
125
|
`,{status:400})}let target=resolve2(root,`.${pathname}`);if(target!==root&&!target.startsWith(root+sep2))return new Response(`not found
|
|
126
126
|
`,{status:404});let isFile=await stat(target).then((s)=>s.isFile(),()=>!1),path=target;if(!isFile){if(pathname.startsWith("/assets/"))return new Response(`not found
|
|
127
127
|
`,{status:404});if(path=resolve2(root,"index.html"),!await Bun.file(path).exists())return new Response(`web client not built
|
|
128
|
-
`,{status:503})}let file=Bun.file(path),cache2=target.startsWith(resolve2(root,"assets")+sep2)?"public, max-age=31536000, immutable":"no-cache",{"Content-Security-Policy":_,...withoutCsp}=SECURITY_HEADERS,headers={...isFile&&pathname.startsWith("/relay/")?withoutCsp:SECURITY_HEADERS,"Content-Type":file.type,"Cache-Control":cache2};if(!COMPRESSIBLE.has(extname2(path)))return new Response(req.method==="HEAD"?null:file,{headers});if(headers.Vary="Accept-Encoding",!acceptsGzip(req.headers.get("accept-encoding")??""))return new Response(req.method==="HEAD"?null:file,{headers});return headers["Content-Encoding"]="gzip",new Response(req.method==="HEAD"?null:await gzipFile(path),{headers})}function acceptsGzip(header){return header.split(",").some((part)=>{let[name2,...params]=part.trim().toLowerCase().split(";");return name2.trim()==="gzip"&&!params.some((p)=>/^\s*q=0(\.0*)?\s*$/.test(p))})}async function handle(deps,ws,req){let{ops}=deps,a=req.args,conn=ws.data,terminal=()=>{return conn.terminal??=new TerminalForwarder(deps.openPty(),(frame)=>ws.send(frame)),conn.terminal},terminalFor=async(taskId2)=>{let alias=ops.machineOf?.(taskId2);if(alias===void 0)return terminal();if(!deps.openMachinePty)throw new BridgeError("MACHINE_UNREACHABLE",`this bridge cannot reach machine ${alias}`);let open=deps.openMachinePty,pending=conn.machineTerminals.get(alias);if(!pending)pending=open(alias).then((pty)=>new TerminalForwarder(pty,(frame)=>ws.send(frame))),pending.catch(()=>conn.machineTerminals.delete(alias)),conn.machineTerminals.set(alias,pending);return pending},owner=(stream)=>conn.streamOwner.get(stream)??terminal();if(DESTRUCTIVE_CORE[req.op])logDestructive({log:deps.log,who:conn.who},req);switch(req.op){case"hello":return{protocol:BRIDGE_PROTOCOL_VERSION,apiVersion:ROVE_API_VERSION,roveVersion:deps.roveVersion,host:osHostname(),peer:conn.peer};case"api.invoke":{if(!deps.invoke)throw new BridgeError("UNKNOWN_OP","this bridge does not serve verbs");let verb=str(a,"verb"),params=a.params??{};if(typeof params!=="object"||params===null||Array.isArray(params))throw new BridgeError("BAD_ARGS","params must be an object keyed by flag name");if(Object.hasOwn(DESTRUCTIVE_VERBS,verb))logDestructiveVerb({log:deps.log,who:conn.who},verb,params);let envelope=await deps.invoke(verb,params,optStr(a,"machine"));if(envelope.ok)return envelope.result;throw new BridgeError(envelope.error.code,envelope.error.message,envelope.error)}case"tasks.subscribe":{let snapshot=await deps.feed.current();return conn.unsubscribeTasks??=deps.feed.subscribe((payload)=>ws.send(pushEvent("tasks",payload))),conn.unsubscribeTabs??=deps.tabs?.subscribe((taskIds)=>ws.send(pushEvent("tabs",{taskIds})))??null,snapshot}case"tasks.list":return deps.feed.refresh();case"engines.list":{let repo=a.repo===void 0?void 0:absPath(a,"repo");return{engines:await ops.engines(),preferred:taskOpDeps.preferredEngine(repo)}}case"repos.list":return{repos:await ops.repos()};case"task.create":return ops.createTask({repo:str(a,"repo"),engine:optStr(a,"engine"),prompt:optStr(a,"prompt"),title:optStr(a,"title")});case"task.delete":return ops.deleteTask(str(a,"taskId"),optBool(a,"force"),optBool(a,"cascade"));case"task.land":{let strategy=optStr(a,"strategy")??"merge";if(strategy!=="merge"&&strategy!=="squash")throw new BridgeError("BAD_ARGS","strategy is merge|squash");return ops.landTask(str(a,"taskId"),strategy)}case"task.tabs":return{tabs:await ops.tabs(str(a,"taskId"))};case"tab.new":{let prompt=optStr(a,"prompt"),kind=optStr(a,"kind")??"engine";if(kind!=="shell"&&kind!=="engine")throw new BridgeError("BAD_ARGS","kind is shell|engine");let engine=optStr(a,"engine"),latest=optBool(a,"latest"),replaceTab=optStr(a,"replaceTab");if(kind==="shell"&&(prompt!==void 0||engine!==void 0||latest||replaceTab!==void 0))throw new BridgeError("BAD_ARGS","a shell tab takes no prompt or engine");if(replaceTab!==void 0&&!/^tab-[A-Za-z0-9-]+$/.test(replaceTab))throw new BridgeError("BAD_ARGS","replaceTab is not a tab id");return ops.newTab(str(a,"taskId"),{kind,prompt,engine,...latest?{latest}:{},...replaceTab?{replaceTab}:{}})}case"tab.close":return await ops.closeTab(str(a,"taskId"),str(a,"tabId")),{};case"term.attach":{let taskId2=str(a,"taskId"),tabId2=str(a,"tabId"),leafId2=optStr(a,"leafId");if(leafId2!==void 0&&!/^leaf-\d{1,4}$/.test(leafId2))throw new BridgeError("BAD_ARGS","leafId is not a pane id");let cols=optInt(a,"cols",10,1000),rows=optInt(a,"rows",4,500);if(cols===void 0!==(rows===void 0))throw new BridgeError("BAD_ARGS","cols and rows go together");let remote=ops.machineOf?.(taskId2)!==void 0,shown=remote?null:ops.viewedSession(taskId2,tabId2),target=shown?.taskId??taskId2,forwarder=await terminalFor(target),cwd=shown?.cwd??await ops.worktreeOf(taskId2),shownTab=shown?.tabId??tabId2,size=cols!==void 0&&rows!==void 0?{cols,rows}:void 0,acks=optBool(a,"acks"),attach=()=>forwarder.attach(target,shownTab,cwd,size,leafId2,acks),autoRevive=!remote&&leafId2===void 0,attached=null,gone=null;try{attached=await attach()}catch(err){if(!autoRevive||!(err instanceof BridgeError&&err.code==="TAB_NOT_LIVE"))throw err;gone=err}if(autoRevive&&(gone!==null||attached?.alive===!1)){if(attached)await forwarder.detach(attached.stream);if(!await ops.reviveTab(target,shownTab).then(()=>!0,()=>!1)&&gone!==null)throw gone;attached=await attach()}if(!attached)throw gone;return conn.streamOwner.set(attached.stream,forwarder),attached}case"term.input":{let data=a.data;if(typeof data!=="string")throw new BridgeError("BAD_ARGS","data must be a string");return await owner(str(a,"stream")).input(str(a,"stream"),data),deps.rowStatus?.noteInput(str(a,"stream"),data),{}}case"term.resize":return await owner(str(a,"stream")).resize(str(a,"stream"),optInt(a,"cols",10,1000)??80,optInt(a,"rows",4,500)??24),{};case"term.ack":return conn.streamOwner.get(str(a,"stream"))?.ack(str(a,"stream"),optInt(a,"chars",0,1e9)??0),{};case"term.detach":{let stream=str(a,"stream"),forwarder=conn.streamOwner.get(stream)??conn.terminal;conn.streamOwner.delete(stream),await forwarder?.detach(stream)}return{};case"term.revive":{let taskId2=str(a,"taskId"),tabId2=str(a,"tabId"),shown=ops.machineOf?.(taskId2)!==void 0?null:ops.viewedSession(taskId2,tabId2);return ops.reviveTab(shown?.taskId??taskId2,shown?.tabId??tabId2,optBool(a,"restartExited"))}case"diff.files":return ops.diffFiles(str(a,"taskId"));case"diff.file":{let scope=oneOf(a,"scope",["branch","working","task"]),flags=[optBool(a,"ignoreWhitespace"),optStr(a,"base"),optBool(a,"content")];return ops.diffFile(str(a,"taskId"),str(a,"path"),scope,...flags)}case"diff.refs":return ops.diffRefs(str(a,"taskId"));case"attention.dismiss":{let taskId2=str(a,"taskId"),tabId2=optStr(a,"tabId"),deliveryId=optStr(a,"deliveryId");if(optStr(a,"gateId")||!deliveryId&&dismissHitsOnlyGates((await deps.feed.current()).attention,taskId2,tabId2))throw new BridgeError("GATE_NOT_DISMISSABLE","a decision is answered with gate.answer, not dismissed");return await ops.dismissAttention(taskId2,tabId2,deliveryId),{}}case"push.key":return{publicKey:requirePush(deps).publicKey()};case"push.status":return{subscribed:requirePush(deps).subscribed(a.endpoint,conn.credential)};case"push.subscribe":return requirePush(deps).subscribe(a.subscription,conn.credential),{};case"push.unsubscribe":return{removed:requirePush(deps).unsubscribe(a.endpoint,conn.credential)};case"ui.viewing":{let taskId2=a.taskId===null?null:str(a,"taskId"),tabId2=taskId2===null?null:optStr(a,"tabId")??null;return deps.push?.setVisible(conn.viewer,taskId2!==null),deps.rowStatus?.setViewing(conn.viewer,taskId2,tabId2,viewingFields(a,conn.peer)),{}}default:{let spec2=(deps.areaOps??AREA_OPS)[req.op];if(!spec2)throw new BridgeError("UNKNOWN_OP",`unknown op: ${req.op}`);if(spec2.destructive)logDestructive({log:deps.log,who:conn.who},req);return spec2.run(a,{api:deps.api,progress:(line)=>ws.send(pushEvent("op.progress",{id:req.id,line}))})}}}function requirePush(deps){if(!deps.push)throw new BridgeError("UNAVAILABLE","this bridge sends no push notifications");return deps.push}async function onMessage(deps,ws,raw){let req,area=deps.areaOps??AREA_OPS;try{req=parseRequest(raw,(op)=>isCoreOp(op)||Object.hasOwn(area,op))}catch(err){let e=err instanceof BridgeError?err:new BridgeError("BAD_FRAME",String(err));ws.send(responseError(requestIdOf(err),e.code,e.message));return}try{ws.send(responseOk(req.id,await handle(deps,ws,req)))}catch(err){let code=err instanceof BridgeError?err.code:"INTERNAL",detail=err instanceof BridgeError?err.detail:void 0;ws.send(responseError(req.id,code,err instanceof Error?err.message:String(err),detail))}}var nextViewer=0;function startBridgeServer(deps,listen){return Bun.serve({hostname:listen.hostname,port:listen.port,async fetch(req,server){let log=deps.log??((line)=>console.error(line)),from=req.headers.get("cf-connecting-ip")??server.requestIP(req)?.address??"?";if(deps.access)try{await deps.access.verify(req.headers.get("cf-access-jwt-assertion"))}catch(err){return log(`[rove-bridge] 401 from ${from}: Cloudflare Access \u2014 ${err instanceof Error?err.message:String(err)}`),new Response(`unauthorized
|
|
128
|
+
`,{status:503})}let file=Bun.file(path),cache2=target.startsWith(resolve2(root,"assets")+sep2)?"public, max-age=31536000, immutable":"no-cache",{"Content-Security-Policy":_,...withoutCsp}=SECURITY_HEADERS,headers={...isFile&&pathname.startsWith("/relay/")?withoutCsp:SECURITY_HEADERS,"Content-Type":file.type,"Cache-Control":cache2};if(!COMPRESSIBLE.has(extname2(path)))return new Response(req.method==="HEAD"?null:file,{headers});if(headers.Vary="Accept-Encoding",!acceptsGzip(req.headers.get("accept-encoding")??""))return new Response(req.method==="HEAD"?null:file,{headers});return headers["Content-Encoding"]="gzip",new Response(req.method==="HEAD"?null:await gzipFile(path),{headers})}function acceptsGzip(header){return header.split(",").some((part)=>{let[name2,...params]=part.trim().toLowerCase().split(";");return name2.trim()==="gzip"&&!params.some((p)=>/^\s*q=0(\.0*)?\s*$/.test(p))})}async function handle(deps,ws,req){let{ops}=deps,a=req.args,conn=ws.data,terminal=()=>{return conn.terminal??=new TerminalForwarder(deps.openPty(),(frame)=>ws.send(frame)),conn.terminal},terminalFor=async(taskId2)=>{let alias=ops.machineOf?.(taskId2);if(alias===void 0)return terminal();if(!deps.openMachinePty)throw new BridgeError("MACHINE_UNREACHABLE",`this bridge cannot reach machine ${alias}`);let open=deps.openMachinePty,pending=conn.machineTerminals.get(alias);if(!pending)pending=open(alias).then((pty)=>new TerminalForwarder(pty,(frame)=>ws.send(frame))),pending.catch(()=>conn.machineTerminals.delete(alias)),conn.machineTerminals.set(alias,pending);return pending},owner=(stream)=>conn.streamOwner.get(stream)??terminal();if(DESTRUCTIVE_CORE[req.op])logDestructive({log:deps.log,who:conn.who},req);switch(req.op){case"hello":return{protocol:BRIDGE_PROTOCOL_VERSION,apiVersion:ROVE_API_VERSION,roveVersion:deps.roveVersion,host:osHostname(),peer:conn.peer};case"api.invoke":{if(!deps.invoke)throw new BridgeError("UNKNOWN_OP","this bridge does not serve verbs");let verb=str(a,"verb"),params=a.params??{};if(typeof params!=="object"||params===null||Array.isArray(params))throw new BridgeError("BAD_ARGS","params must be an object keyed by flag name");if(Object.hasOwn(DESTRUCTIVE_VERBS,verb))logDestructiveVerb({log:deps.log,who:conn.who},verb,params);let envelope=await deps.invoke(verb,params,optStr(a,"machine"));if(envelope.ok)return envelope.result;throw new BridgeError(envelope.error.code,envelope.error.message,envelope.error)}case"tasks.subscribe":{let snapshot=await deps.feed.current();return conn.unsubscribeTasks??=deps.feed.subscribe((payload)=>ws.send(pushEvent("tasks",payload))),conn.unsubscribeTabs??=deps.tabs?.subscribe((taskIds)=>ws.send(pushEvent("tabs",{taskIds})))??null,snapshot}case"tasks.list":return deps.feed.refresh();case"engines.list":{let repo=a.repo===void 0?void 0:absPath(a,"repo");return{engines:await ops.engines(),preferred:taskOpDeps.preferredEngine(repo)}}case"repos.list":return{repos:await ops.repos()};case"task.create":return ops.createTask({repo:str(a,"repo"),engine:optStr(a,"engine"),prompt:optStr(a,"prompt"),title:optStr(a,"title")});case"task.delete":return ops.deleteTask(str(a,"taskId"),optBool(a,"force"),optBool(a,"cascade"));case"task.land":{let strategy=optStr(a,"strategy")??"merge";if(strategy!=="merge"&&strategy!=="squash")throw new BridgeError("BAD_ARGS","strategy is merge|squash");return ops.landTask(str(a,"taskId"),strategy)}case"task.tabs":return{tabs:await ops.tabs(str(a,"taskId"))};case"tab.new":{let prompt=optStr(a,"prompt"),kind=optStr(a,"kind")??"engine";if(kind!=="shell"&&kind!=="engine")throw new BridgeError("BAD_ARGS","kind is shell|engine");let engine=optStr(a,"engine"),latest=optBool(a,"latest"),replaceTab=optStr(a,"replaceTab");if(kind==="shell"&&(prompt!==void 0||engine!==void 0||latest||replaceTab!==void 0))throw new BridgeError("BAD_ARGS","a shell tab takes no prompt or engine");if(replaceTab!==void 0&&!/^tab-[A-Za-z0-9-]+$/.test(replaceTab))throw new BridgeError("BAD_ARGS","replaceTab is not a tab id");return ops.newTab(str(a,"taskId"),{kind,prompt,engine,...latest?{latest}:{},...replaceTab?{replaceTab}:{}})}case"tab.close":return await ops.closeTab(str(a,"taskId"),str(a,"tabId")),{};case"term.attach":{let taskId2=str(a,"taskId"),tabId2=str(a,"tabId"),leafId2=optStr(a,"leafId");if(leafId2!==void 0&&!/^leaf-\d{1,4}$/.test(leafId2))throw new BridgeError("BAD_ARGS","leafId is not a pane id");let cols=optInt(a,"cols",10,1000),rows=optInt(a,"rows",4,500);if(cols===void 0!==(rows===void 0))throw new BridgeError("BAD_ARGS","cols and rows go together");let remote=ops.machineOf?.(taskId2)!==void 0,shown=remote?null:ops.viewedSession(taskId2,tabId2),target=shown?.taskId??taskId2,forwarder=await terminalFor(target),cwd=shown?.cwd??await ops.worktreeOf(taskId2),shownTab=shown?.tabId??tabId2,size=cols!==void 0&&rows!==void 0?{cols,rows}:void 0,acks=optBool(a,"acks"),attach=()=>forwarder.attach(target,shownTab,cwd,size,leafId2,acks),autoRevive=!remote&&leafId2===void 0,attached=null,gone=null;try{attached=await attach()}catch(err){if(!autoRevive||!(err instanceof BridgeError&&err.code==="TAB_NOT_LIVE"))throw err;gone=err}if(autoRevive&&(gone!==null||attached?.alive===!1)){if(attached)await forwarder.detach(attached.stream);if(!await ops.reviveTab(target,shownTab).then(()=>!0,()=>!1)&&gone!==null)throw gone;attached=await attach()}if(!attached)throw gone;return conn.streamOwner.set(attached.stream,forwarder),attached}case"term.input":{let data=a.data;if(typeof data!=="string")throw new BridgeError("BAD_ARGS","data must be a string");return await owner(str(a,"stream")).input(str(a,"stream"),data),deps.rowStatus?.noteInput(str(a,"stream"),data),{}}case"term.resize":return await owner(str(a,"stream")).resize(str(a,"stream"),optInt(a,"cols",10,1000)??80,optInt(a,"rows",4,500)??24),{};case"term.ack":return conn.streamOwner.get(str(a,"stream"))?.ack(str(a,"stream"),optInt(a,"chars",0,1e9)??0),{};case"term.detach":{let stream=str(a,"stream"),forwarder=conn.streamOwner.get(stream)??conn.terminal;conn.streamOwner.delete(stream),await forwarder?.detach(stream)}return{};case"term.revive":{let taskId2=str(a,"taskId"),tabId2=str(a,"tabId"),shown=ops.machineOf?.(taskId2)!==void 0?null:ops.viewedSession(taskId2,tabId2);return ops.reviveTab(shown?.taskId??taskId2,shown?.tabId??tabId2,optBool(a,"restartExited"))}case"diff.files":return ops.diffFiles(str(a,"taskId"));case"diff.file":{let scope=oneOf(a,"scope",["branch","working","task"]),flags=[optBool(a,"ignoreWhitespace"),optStr(a,"base"),optBool(a,"content")];return ops.diffFile(str(a,"taskId"),str(a,"path"),scope,...flags)}case"diff.refs":return ops.diffRefs(str(a,"taskId"));case"attention.dismiss":{let taskId2=str(a,"taskId"),tabId2=optStr(a,"tabId"),deliveryId=optStr(a,"deliveryId");if(optStr(a,"gateId")||!deliveryId&&dismissHitsOnlyGates((await deps.feed.current()).attention,taskId2,tabId2))throw new BridgeError("GATE_NOT_DISMISSABLE","a decision is answered with gate.answer, not dismissed");return await ops.dismissAttention(taskId2,tabId2,deliveryId),{}}case"push.key":return{publicKey:requirePush(deps).publicKey()};case"push.status":return{subscribed:requirePush(deps).subscribed(a.endpoint,conn.credential)};case"push.subscribe":return requirePush(deps).subscribe(a.subscription,conn.credential),{};case"push.unsubscribe":return{removed:requirePush(deps).unsubscribe(a.endpoint,conn.credential)};case"ui.viewing":{let taskId2=a.taskId===null?null:str(a,"taskId"),tabId2=taskId2===null?null:optStr(a,"tabId")??null;return deps.push?.setVisible(conn.viewer,taskId2!==null),deps.rowStatus?.setViewing(conn.viewer,taskId2,tabId2,viewingFields(a,conn.peer)),{}}default:{let spec2=(deps.areaOps??AREA_OPS)[req.op];if(!spec2)throw new BridgeError("UNKNOWN_OP",`unknown op: ${req.op}`);if(spec2.destructive)logDestructive({log:deps.log,who:conn.who},req);return spec2.run(a,{api:deps.api,progress:(line)=>ws.send(pushEvent("op.progress",{id:req.id,line}))})}}}function requirePush(deps){if(!deps.push)throw new BridgeError("UNAVAILABLE","this bridge sends no push notifications");return deps.push}async function onMessage(deps,ws,raw){let req,area=deps.areaOps??AREA_OPS;try{req=parseRequest(raw,(op)=>isCoreOp(op)||Object.hasOwn(area,op))}catch(err){let e=err instanceof BridgeError?err:new BridgeError("BAD_FRAME",String(err));ws.send(responseError(requestIdOf(err),e.code,e.message));return}try{ws.send(responseOk(req.id,await handle(deps,ws,req)))}catch(err){let code=err instanceof BridgeError?err.code:"INTERNAL",detail=err instanceof BridgeError?err.detail:void 0;ws.send(responseError(req.id,code,err instanceof Error?err.message:String(err),detail))}}var nextViewer=0;function startBridgeServer(deps,listen){return Bun.serve({hostname:listen.hostname,port:listen.port,maxRequestBodySize:2097152,async fetch(req,server){let log=deps.log??((line)=>console.error(line)),from=req.headers.get("cf-connecting-ip")??server.requestIP(req)?.address??"?";if(deps.access)try{await deps.access.verify(req.headers.get("cf-access-jwt-assertion"))}catch(err){return log(`[rove-bridge] 401 from ${from}: Cloudflare Access \u2014 ${err instanceof Error?err.message:String(err)}`),new Response(`unauthorized
|
|
129
129
|
`,{status:401})}let upgrade=req.headers.get("upgrade")?.toLowerCase()==="websocket";if(!upgrade){let hook=await routineHook(deps.api,req,log);if(hook)return hook}if(!upgrade&&deps.login){let answer2=await deps.login.handle(req,server.requestIP(req)?.address??"?");if(answer2)return answer2}if(!upgrade&&deps.web){let file=await serveWebFile(deps.web,req);if(file)return file}let origin=req.headers.get("origin");if(origin!==null&&!browserOriginAllowed(origin,req.headers.get("host"),deps.publicHost??null))return log(`[rove-bridge] 403 from ${from}: cross-origin upgrade from ${origin}`),new Response(`forbidden
|
|
130
130
|
`,{status:403});let presented=presentedToken(req,deps.tokenInQuery===!0),device=tokenMatches(deps.token,presented)?null:deps.deviceOf?.(presented)??void 0;if(device===void 0)return log(`[rove-bridge] 401 from ${from}: missing or wrong bearer token`),new Response(`unauthorized
|
|
131
131
|
`,{status:401});if(device)log(`[rove-bridge] ${new Date().toISOString()} device ${device} connected from ${from}`);let data={peer:peerOf(req,server.requestIP(req)?.address,deps),viewer:`bridge-${++nextViewer}`,who:`device=${device??"shared-token"} from=${from}`,credential:device??sharedCredential(deps.token),terminal:null,machineTerminals:new Map,streamOwner:new Map,unsubscribeTasks:null,unsubscribeTabs:null,unsubscribeNotices:null,unsubscribeMods:null,unsubscribePrompts:null,unsubscribeDispatches:null,unsubscribePanels:null},headers=offersWebSubprotocol(req)?{"Sec-WebSocket-Protocol":WEB_SUBPROTOCOL}:void 0;if(server.upgrade(req,{data,headers}))return;return new Response(`rove-bridge speaks WebSocket only
|
|
132
|
-
`,{status:426})},websocket:{open(ws){if(ws.data.unsubscribeNotices=deps.notices?.subscribe((n)=>ws.send(pushEvent("notice",n)))??null,ws.data.unsubscribePrompts=deps.prompts?.subscribe((p)=>ws.send(pushEvent("prompt",p)))??null,ws.data.unsubscribePanels=deps.panels?.subscribe((p)=>ws.send(pushEvent("panel",p)))??null,deps.mods)ws.send(pushEvent("mods",{panes:deps.mods.current()})),ws.data.unsubscribeMods=deps.mods.subscribe((panes)=>ws.send(pushEvent("mods",{panes})));if(deps.dispatches){let now=deps.dispatches.current();if(now)ws.send(pushEvent("dispatch",now));ws.data.unsubscribeDispatches=deps.dispatches.subscribe((snapshot)=>ws.send(pushEvent("dispatch",snapshot)))}},maxPayloadLength:8388608,message(ws,message){onMessage(deps,ws,typeof message==="string"?message:message.toString("utf8"))},close(ws){deps.rowStatus?.setViewing(ws.data.viewer,null,null),deps.push?.setVisible(ws.data.viewer,!1),ws.data.unsubscribeTasks?.(),ws.data.unsubscribeTabs?.(),ws.data.unsubscribeNotices?.(),ws.data.unsubscribeMods?.(),ws.data.unsubscribePrompts?.(),ws.data.unsubscribeDispatches?.(),ws.data.unsubscribePanels?.(),ws.data.terminal?.dispose();for(let pending of ws.data.machineTerminals.values())pending.then((forwarder)=>forwarder.dispose(),()=>{})}}})}class SessionTitles{open;now;client=null;titles=new Map;signature="";readAt=Number.NEGATIVE_INFINITY;inflight=null;timer=null;constructor(open,now=Date.now){this.open=open;this.now=now}async current(){if(this.now()-this.readAt<2000)return this.titles;return this.poll()}watch(active,onChange,intervalMs=2000){if(this.timer)return;this.timer=setInterval(()=>{if(active())this.tick().then((moved)=>moved&&onChange())},intervalMs)}async tick(){let before=this.signature;return await this.poll(),this.signature!==before}stop(){clearInterval(this.timer??void 0),this.timer=null,this.client?.close(),this.client=null}poll(){return this.inflight??=this.read().finally(()=>{this.inflight=null}),this.inflight}async read(){let titles=new Map;try{this.client??=this.open();let{sessions=[]}=await this.client.request("pty.list",{});for(let session of sessions){let title=session.title?.trim();if(session.alive!==!1&&typeof session.key==="string"&&title)titles.set(session.key,title)}}catch{this.client?.close(),this.client=null}return this.titles=titles,this.readAt=this.now(),this.signature=JSON.stringify([...titles]),titles}}class TabFeed{read;latest;listeners=new Set;constructor(read2){this.read=read2;this.latest=this.snapshots()}check(){let next=this.snapshots(),changed=new Set;for(let[id,json]of next)if(this.latest.get(id)!==json)changed.add(id);for(let id of this.latest.keys())if(!next.has(id))changed.add(id);if(this.latest=next,changed.size===0)return;let taskIds=[...changed];for(let listener of this.listeners)listener(taskIds)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}snapshots(){let out=new Map;for(let[key,value]of Object.entries(this.read()))if(key.startsWith("terminalTabs."))out.set(key.slice(13),JSON.stringify(value));return out}}var MAX_BODY_BYTES=4096,LOOPBACK=/^(127\.|::1$|::ffff:127\.)/,json=(status2,body,headers={})=>new Response(JSON.stringify(body),{status:status2,headers:{"Content-Type":"application/json","Cache-Control":"no-store",...headers}});function loginDeviceName(devicesPath,now){let stamp=now.toISOString().replace(/[-:]/g,"").replace("T","-").slice(0,15),taken=new Set(listDevices(devicesPath).map((d)=>d.name)),base=`web-login-${stamp}`;if(!taken.has(base))return base;let n=2;while(taken.has(`${base}-${n}`))n++;return`${base}-${n}`}function createWebLogin(opts){let paths=webPasswordPaths(opts.bridgeDir),now=opts.now??Date.now,delay=opts.failureDelayMs??(()=>300+Math.floor(Math.random()*200)),queue=Promise.resolve(),serial=(fn)=>{let run=queue.then(fn,fn);return queue=run.catch(()=>{}),run},clientIp=(req,peer)=>{let forwarded=req.headers.get("cf-connecting-ip")?.trim();if(opts.trustForwardedIp&&forwarded&&LOOPBACK.test(peer)&&/^[0-9A-Fa-f:.]{2,45}$/.test(forwarded))return forwarded;return peer},sameOrigin=(req)=>{let origin=req.headers.get("origin");if(origin===null||!browserOriginAllowed(origin,req.headers.get("host"),opts.publicHost))return!1;let site=req.headers.get("sec-fetch-site");return site===null||site==="same-origin"},stamp=()=>new Date(now()).toISOString(),lockedResponse=(lock)=>lock.kind==="global"?json(429,{error:"locked; unlock on the host",locked:"global"}):json(429,{error:"locked",locked:"ip",retryAfterSeconds:Math.ceil(lock.retryAfterMs/1000)},{"Retry-After":String(Math.ceil(lock.retryAfterMs/1000))});async function login(req,peer){let ip=clientIp(req,peer);if(!sameOrigin(req))return opts.log(`[rove-bridge] ${stamp()} 403 from ${ip}: cross-origin /login from ${req.headers.get("origin")}`),json(403,{error:"forbidden"});if(!(req.headers.get("content-type")??"").toLowerCase().startsWith("application/json"))return json(415,{error:"expected application/json"});let body=await req.text();if(body.length>MAX_BODY_BYTES)return json(413,{error:"too large"});let password;try{password=JSON.parse(body)?.password}catch{password=void 0}if(typeof password!=="string"||password.length===0||password.length>MAX_PASSWORD_LENGTH)return json(400,{error:"expected {password}"});let entered=password,outcome=await serial(async()=>{let record=readPassword(paths.password);if(!record)return{kind:"gone"};let state=readLockout(paths.lockout),held=lockOf(state,ip,now());if(held)return{kind:"locked",lock:held};if(await verifyPassword(record,entered)){writeLockout(paths.lockout,recordSuccess(state,ip));let name2=loginDeviceName(opts.devicesPath,new Date(now()));return{kind:"ok",name:name2,token:addDevice(opts.devicesPath,name2,new Date(now()))}}let next=recordFailure(state,ip,now());return writeLockout(paths.lockout,next.state),{kind:"failed",lock:next.lock,count:next.state.failures.length}});switch(outcome.kind){case"gone":return json(404,{error:"not found"});case"locked":return opts.log(`[rove-bridge] ${stamp()} login refused from ${ip}: locked (${outcome.lock.kind})`),lockedResponse(outcome.lock);case"ok":return opts.log(`[rove-bridge] ${stamp()} login from ${ip}: minted device ${outcome.name}`),json(200,{token:outcome.token,device:outcome.name});case"failed":{if(opts.log(`[rove-bridge] ${stamp()} login failed from ${ip} (${outcome.count} in 24h)`),outcome.lock?.kind==="ip")opts.log(`[rove-bridge] ${stamp()} locked ${ip} for 15 minutes`);if(outcome.lock?.kind==="global")opts.log(`[rove-bridge] ${stamp()} password login locked for everyone; run \`rove --web --unlock\``);return await new Promise((resolve3)=>setTimeout(resolve3,delay())),outcome.lock?lockedResponse(outcome.lock):json(401,{error:"login failed"})}}}function logout(req,peer){if(!sameOrigin(req))return json(403,{error:"forbidden"});let device=deviceOf(opts.devicesPath,presentedToken(req));if(device&&revokeDevice(opts.devicesPath,device))opts.log(`[rove-bridge] ${stamp()} device ${device} signed out from ${clientIp(req,peer)}`);return new Response(null,{status:204,headers:{"Cache-Control":"no-store"}})}return{async handle(req,peer){let path=new URL(req.url).pathname;if(path==="/logout"&&req.method==="POST")return logout(req,peer);if(path!=="/login")return null;if(!readPassword(paths.password))return null;if(req.method==="GET")return json(200,{password:!0});if(req.method==="POST")return login(req,peer);return json(405,{error:"method not allowed"},{Allow:"GET, POST"})}}}var TICK_MS=15000,TABS_DEBOUNCE_MS=200,STATUS_RECONNECT_MS=1000,STATUS_RECONNECT_MAX_MS=30000,TAILSCALE_BINARIES=["tailscale","/Applications/Tailscale.app/Contents/MacOS/Tailscale"];function tailscaleInfo(){for(let bin of TAILSCALE_BINARIES){let ip=spawnSync(bin,["ip","-4"],{encoding:"utf8",timeout:5000}),ipv4=ip.status===0?ip.stdout.split(`
|
|
132
|
+
`,{status:426})},websocket:{open(ws){if(deps.sockets?.add(ws),ws.data.unsubscribeNotices=deps.notices?.subscribe((n)=>ws.send(pushEvent("notice",n)))??null,ws.data.unsubscribePrompts=deps.prompts?.subscribe((p)=>ws.send(pushEvent("prompt",p)))??null,ws.data.unsubscribePanels=deps.panels?.subscribe((p)=>ws.send(pushEvent("panel",p)))??null,deps.mods)ws.send(pushEvent("mods",{panes:deps.mods.current()})),ws.data.unsubscribeMods=deps.mods.subscribe((panes)=>ws.send(pushEvent("mods",{panes})));if(deps.dispatches){let now=deps.dispatches.current();if(now)ws.send(pushEvent("dispatch",now));ws.data.unsubscribeDispatches=deps.dispatches.subscribe((snapshot)=>ws.send(pushEvent("dispatch",snapshot)))}},maxPayloadLength:8388608,message(ws,message){onMessage(deps,ws,typeof message==="string"?message:message.toString("utf8"))},close(ws){deps.sockets?.remove(ws),deps.rowStatus?.setViewing(ws.data.viewer,null,null),deps.push?.setVisible(ws.data.viewer,!1),ws.data.unsubscribeTasks?.(),ws.data.unsubscribeTabs?.(),ws.data.unsubscribeNotices?.(),ws.data.unsubscribeMods?.(),ws.data.unsubscribePrompts?.(),ws.data.unsubscribeDispatches?.(),ws.data.unsubscribePanels?.(),ws.data.terminal?.dispose();for(let pending of ws.data.machineTerminals.values())pending.then((forwarder)=>forwarder.dispose(),()=>{})}}})}class SessionTitles{open;now;client=null;titles=new Map;signature="";readAt=Number.NEGATIVE_INFINITY;inflight=null;timer=null;constructor(open,now=Date.now){this.open=open;this.now=now}async current(){if(this.now()-this.readAt<2000)return this.titles;return this.poll()}watch(active,onChange,intervalMs=2000){if(this.timer)return;this.timer=setInterval(()=>{if(active())this.tick().then((moved)=>moved&&onChange())},intervalMs)}async tick(){let before=this.signature;return await this.poll(),this.signature!==before}stop(){clearInterval(this.timer??void 0),this.timer=null,this.client?.close(),this.client=null}poll(){return this.inflight??=this.read().finally(()=>{this.inflight=null}),this.inflight}async read(){let titles=new Map;try{this.client??=this.open();let{sessions=[]}=await this.client.request("pty.list",{});for(let session of sessions){let title=session.title?.trim();if(session.alive!==!1&&typeof session.key==="string"&&title)titles.set(session.key,title)}}catch{this.client?.close(),this.client=null}return this.titles=titles,this.readAt=this.now(),this.signature=JSON.stringify([...titles]),titles}}class TabFeed{read;latest;listeners=new Set;constructor(read2){this.read=read2;this.latest=this.snapshots()}check(){let next=this.snapshots(),changed=new Set;for(let[id,json]of next)if(this.latest.get(id)!==json)changed.add(id);for(let id of this.latest.keys())if(!next.has(id))changed.add(id);if(this.latest=next,changed.size===0)return;let taskIds=[...changed];for(let listener of this.listeners)listener(taskIds)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}snapshots(){let out=new Map;for(let[key,value]of Object.entries(this.read()))if(key.startsWith("terminalTabs."))out.set(key.slice(13),JSON.stringify(value));return out}}var MAX_BODY_BYTES=4096,LOOPBACK=/^(127\.|::1$|::ffff:127\.)/,json=(status2,body,headers={})=>new Response(JSON.stringify(body),{status:status2,headers:{"Content-Type":"application/json","Cache-Control":"no-store",...headers}});function loginDeviceName(devicesPath,now){let stamp=now.toISOString().replace(/[-:]/g,"").replace("T","-").slice(0,15),taken=new Set(listDevices(devicesPath).map((d)=>d.name)),base=`web-login-${stamp}`;if(!taken.has(base))return base;let n=2;while(taken.has(`${base}-${n}`))n++;return`${base}-${n}`}function createWebLogin(opts){let paths=webPasswordPaths(opts.bridgeDir),now=opts.now??Date.now,delay=opts.failureDelayMs??(()=>300+Math.floor(Math.random()*200)),queue=Promise.resolve(),serial=(fn)=>{let run=queue.then(fn,fn);return queue=run.catch(()=>{}),run},clientIp=(req,peer)=>{let forwarded=req.headers.get("cf-connecting-ip")?.trim();if(opts.trustForwardedIp&&forwarded&&LOOPBACK.test(peer)&&/^[0-9A-Fa-f:.]{2,45}$/.test(forwarded))return forwarded;return peer},sameOrigin=(req)=>{let origin=req.headers.get("origin");if(origin===null||!browserOriginAllowed(origin,req.headers.get("host"),opts.publicHost))return!1;let site=req.headers.get("sec-fetch-site");return site===null||site==="same-origin"},stamp=()=>new Date(now()).toISOString(),lockedResponse=(lock)=>lock.kind==="global"?json(429,{error:"locked; unlock on the host",locked:"global"}):json(429,{error:"locked",locked:"ip",retryAfterSeconds:Math.ceil(lock.retryAfterMs/1000)},{"Retry-After":String(Math.ceil(lock.retryAfterMs/1000))});async function login(req,peer){let ip=clientIp(req,peer);if(!sameOrigin(req))return opts.log(`[rove-bridge] ${stamp()} 403 from ${ip}: cross-origin /login from ${req.headers.get("origin")}`),json(403,{error:"forbidden"});if(!(req.headers.get("content-type")??"").toLowerCase().startsWith("application/json"))return json(415,{error:"expected application/json"});let body=await req.text();if(body.length>MAX_BODY_BYTES)return json(413,{error:"too large"});let password;try{password=JSON.parse(body)?.password}catch{password=void 0}if(typeof password!=="string"||password.length===0||password.length>MAX_PASSWORD_LENGTH)return json(400,{error:"expected {password}"});let entered=password,outcome=await serial(async()=>{let record=readPassword(paths.password);if(!record)return{kind:"gone"};let state=readLockout(paths.lockout),held=lockOf(state,ip,now());if(held)return{kind:"locked",lock:held};if(await verifyPassword(record,entered)){writeLockout(paths.lockout,recordSuccess(state,ip));let name2=loginDeviceName(opts.devicesPath,new Date(now()));return{kind:"ok",name:name2,token:addDevice(opts.devicesPath,name2,new Date(now()))}}let next=recordFailure(state,ip,now());return writeLockout(paths.lockout,next.state),{kind:"failed",lock:next.lock,count:next.state.failures.length}});switch(outcome.kind){case"gone":return json(404,{error:"not found"});case"locked":return opts.log(`[rove-bridge] ${stamp()} login refused from ${ip}: locked (${outcome.lock.kind})`),lockedResponse(outcome.lock);case"ok":return opts.log(`[rove-bridge] ${stamp()} login from ${ip}: minted device ${outcome.name}`),json(200,{token:outcome.token,device:outcome.name});case"failed":{if(opts.log(`[rove-bridge] ${stamp()} login failed from ${ip} (${outcome.count} in 24h)`),outcome.lock?.kind==="ip")opts.log(`[rove-bridge] ${stamp()} locked ${ip} for 15 minutes`);if(outcome.lock?.kind==="global")opts.log(`[rove-bridge] ${stamp()} password login locked for everyone; run \`rove --web --unlock\``);return await new Promise((resolve3)=>setTimeout(resolve3,delay())),outcome.lock?lockedResponse(outcome.lock):json(401,{error:"login failed"})}}}function logout(req,peer){if(!sameOrigin(req))return json(403,{error:"forbidden"});let device=deviceOf(opts.devicesPath,presentedToken(req));if(device&&revokeDevice(opts.devicesPath,device))opts.log(`[rove-bridge] ${stamp()} device ${device} signed out from ${clientIp(req,peer)}`),opts.onRevoked?.(device);return new Response(null,{status:204,headers:{"Cache-Control":"no-store"}})}return{async handle(req,peer){let path=new URL(req.url).pathname;if(path==="/logout"&&req.method==="POST")return logout(req,peer);if(path!=="/login")return null;if(!readPassword(paths.password))return null;if(req.method==="GET")return json(200,{password:!0});if(req.method==="POST")return login(req,peer);return json(405,{error:"method not allowed"},{Allow:"GET, POST"})}}}var TICK_MS=15000,TABS_DEBOUNCE_MS=200,STATUS_RECONNECT_MS=1000,STATUS_RECONNECT_MAX_MS=30000,TAILSCALE_BINARIES=["tailscale","/Applications/Tailscale.app/Contents/MacOS/Tailscale"];function tailscaleInfo(){for(let bin of TAILSCALE_BINARIES){let ip=spawnSync(bin,["ip","-4"],{encoding:"utf8",timeout:5000}),ipv4=ip.status===0?ip.stdout.split(`
|
|
133
133
|
`)[0]?.trim()??"":"";if(!ipv4)continue;let status2=spawnSync(bin,["status","--json"],{encoding:"utf8",timeout:5000}),dnsName=null;try{let self=JSON.parse(status2.stdout).Self;if(self&&typeof self==="object"&&"DNSName"in self&&typeof self.DNSName==="string")dnsName=self.DNSName.replace(/\.$/,"")||null}catch{}return{ipv4,dnsName}}return{ipv4:null,dnsName:null}}function readConfig2(path){if(!existsSync6(path))return{};let raw=JSON.parse(readFileSync6(path,"utf8"));if(!raw||typeof raw!=="object")throw Error(`${path} must hold a JSON object`);let fields=new Map(Object.entries(raw)),pick=(k)=>{let v=fields.get(k);return typeof v==="string"&&v.trim()?v.trim():void 0};return{cfTeam:pick("cfTeam"),cfAud:pick("cfAud"),publicHost:pick("publicHost"),relayServer:pick("relayServer")}}async function runBridge(argv,opts){let home=resolveProductHomeDir(),bridgeDir=join8(home,".rove","bridge"),args;try{args=parseBridgeArgs(argv,{config:readConfig2(join8(bridgeDir,"config.json")),tailscale:tailscaleInfo})}catch(err){console.error(`${opts.name}: ${err instanceof Error?err.message:String(err)}`),process.exit(2)}Reflect.deleteProperty(process.env,"ROVE_TASK_ID"),Reflect.deleteProperty(process.env,"ROVE_TAB_ID"),ensurePluginEnginesLoaded();let devicesPath=join8(bridgeDir,"devices.json");if(args.action==="list-devices"){let devices=listDevices(devicesPath);console.log(devices.length?devices.map((d)=>`${d.name} since ${d.createdAt}`).join(`
|
|
134
|
-
`):"no device tokens");return}if(args.action==="revoke-device"){let name2=args.device??"";console.log(revokeDevice(devicesPath,name2)?`revoked ${name2}`:`no device named ${name2}`);return}if(args.action==="setup"){printSetup(args.port);return}if((args.action==="relay-shell"||args.preset==="relay")&&!opts.web)console.error(`${opts.name}: the relay is for the browser client; run it as \`rove --web\``),process.exit(2);if(args.action==="relay-shell"&&opts.web){let shell=join8(opts.web.root,"relay","index.html");if(!existsSync6(shell))console.error(`${opts.name}: ${shell} is missing; build the web client first`),process.exit(1);let dir=args.relayShellDir??".";mkdirSync5(dir,{recursive:!0}),copyFileSync(shell,join8(dir,"index.html")),console.log(`wrote ${join8(dir,"index.html")}`);return}if(args.action==="set-password"||args.action==="clear-password"||args.action==="unlock"){try{process.exitCode=await runPasswordAction(args.action,bridgeDir)}catch(err){console.error(`${opts.name}: ${err instanceof Error?err.message:String(err)}`),process.exitCode=1}return}let{sharedToken,token,revoked}=startupCredentials({tokenPath:join8(bridgeDir,"token"),devicesPath,rotate:args.rotateToken,device:args.device});if(revoked>0)console.log(`${opts.name}: --rotate-token signed out ${revoked} device token(s)`);let daemonSocket=await ensureDaemonReachable(void 0,"autospawn");await ensurePtyHostReachable();let orchestrator=new RemoteOrchestrator(new RoveDaemonClient(daemonSocket),{role:"gui",graphicsOut:()=>{}});await orchestrator.init();let hub=new MachineFeed(orchestrator),machines=hub.machinesSignal();machineStore.set(machines.get()),machines.subscribe(()=>machineStore.set(machines.get()));let verbClient=new RoveDaemonClient(daemonSocket),machineOf3=(taskId2)=>{let alias=hub.tasksSignal().get().find((row)=>String(row.id)===taskId2)?.origin?.machineId;return alias&&alias!=="local"?alias:void 0},statusClient=new RoveDaemonClient(daemonSocket),rowStatus=new RowStatusFeed(statusClient),dispatches=new DispatchFeed(statusClient),panels=new PanelFeed;statusClient.onChannel("panel.show",(panel)=>panels.push(panel));let statusRetryMs=STATUS_RECONNECT_MS,syncStatus=async()=>{try{await statusClient.subscribe({channels:["row.status","dispatch.snapshot","panel.show"],role:"pane"}),await rowStatus.resync(),await dispatches.resync(),statusRetryMs=STATUS_RECONNECT_MS}catch{statusClient.forceDisconnect(),setTimeout(()=>void syncStatus(),statusRetryMs),statusRetryMs=Math.min(statusRetryMs*2,STATUS_RECONNECT_MAX_MS)}};statusClient.onLifecycle("close",()=>{setTimeout(()=>void syncStatus(),STATUS_RECONNECT_MS)}),await syncStatus();let api=createBridgeApi(verbClient,{machineOf:machineOf3,forward:(alias,verb,params)=>forwardVerb(requireMachine(alias),verb,params)}),sessionTitles=new SessionTitles(()=>new RoveDaemonClient(defaultPtyHostSocketPath())),ops=createRoveOps(verbClient,{changes:()=>orchestrator.worktreeChangesSignal()(),tokens:()=>orchestrator.rowTokensSignal()(),gates:()=>orchestrator.gatesSignal()(),remote:()=>hub.tasksSignal().get().filter((task)=>task.origin&&task.origin.machineId!=="local").map((task)=>({...task,kind:task.kind??"task",pinned:task.pinned??!1})),remoteLive:()=>hub.liveSignal().get(),rowStatus:()=>rowStatus.current(),titleSources:async()=>{let sessions=await sessionTitles.current(),state=loadStateFile();return{sessions,tabs:(taskId2)=>state[terminalTabsKey(taskId2)]}}},api),feed=new TaskFeed(()=>ops.tasks());for(let signal of[orchestrator.tasksSignal(),rowStatus,orchestrator.attentionInboxSignal(),orchestrator.worktreeChangesSignal(),orchestrator.rowTokensSignal(),orchestrator.gatesSignal(),hub.tasksSignal(),hub.liveSignal()])signal.subscribe(()=>feed.poke());let usage=orchestrator.usageSnapshotSignal();usageStore.set(usage()),usage.subscribe(()=>usageStore.set(usage()));let context=orchestrator.contextUsageSignal();usageStore.setContext(context()),context.subscribe(()=>usageStore.setContext(context())),setInterval(()=>feed.poke(),TICK_MS),sessionTitles.watch(()=>feed.active,()=>feed.poke());let notices=new NoticeFeed,noticeStore=orchestrator.noticeStore();noticeStore.subscribe(()=>notices.push(noticeStore.get()));let prompts=new PromptFeed,promptStore=orchestrator.uiPromptStore();promptStore.subscribe(()=>prompts.push(promptStore.get()));let modPanes=orchestrator.modPanesSignal(),mods={current:()=>modPanes.get(),subscribe:(listener)=>modPanes.subscribe(()=>listener(modPanes.get()))},statePath=defaultUiPrefsStatePath(),readState=()=>{try{let raw=JSON.parse(readFileSync6(statePath,"utf8"));return raw&&typeof raw==="object"?raw:{}}catch{return{}}},tabs=new TabFeed(readState);startFileWatchTrigger({filePath:statePath,debounceMs:TABS_DEBOUNCE_MS,onTrigger:()=>{tabs.check(),feed.poke()},onError:()=>{}});let ptySocket=defaultPtyHostSocketPath(),push=opts.web?new PushHub({path:join8(bridgeDir,"push.json"),feed,crossTask:()=>PREF_DEFS.crossTask.read(readState())===!0,credentialValid:(credential)=>credential===sharedCredential(token)||listDevices(devicesPath).some((d)=>d.name===credential),log:(line)=>console.error(line)}):null,deps={token:sharedToken,ops,feed,notices,prompts,panels,mods,tabs,rowStatus,dispatches,deviceOf:(presented)=>deviceOf(devicesPath,presented),openPty:()=>new RoveDaemonClient(ptySocket),openMachinePty:async(alias)=>{let entry=requireMachine(alias);if(!await machineDaemonSocket(entry))throw new BridgeError("MACHINE_UNREACHABLE",`machine "${alias}" did not answer`);return new RoveDaemonClient(localPtySocketPath(alias))},roveVersion:CURRENT_VERSION,api,invoke:(verb,params,machine)=>invokeRouted({verb,params,machine,isLocalTask:(id)=>verbClient.request("task.get",{taskId:id}).then(()=>!0,()=>!1),runHere:()=>executeVerb(verb,{params},{client:verbClient,caller:REMOTE_CALLER})}),...args.cloudflare?{access:createAccessVerifier(args.cloudflare)}:{},...opts.web?{web:opts.web}:{},...opts.web?{login:createWebLogin({bridgeDir,devicesPath,publicHost:args.publicHost,trustForwardedIp:args.preset==="cf"||args.preset==="none"&&args.publicHost!==null,log:(line)=>console.error(line)})}:{},publicHost:args.publicHost,...push?{push}:{}},relay=args.relayServer?startRelay(deps,args.relayServer,bridgeDir,args.rotateRelay):null,servers=args.hosts.map((hostname)=>{try{return startBridgeServer(deps,{hostname,port:args.port})}catch(err){if(!(err&&typeof err==="object"&&("code"in err)&&err.code==="EADDRINUSE"))throw err;console.error(`${opts.name}: ${hostname}:${args.port} is already in use (another rove-bridge?). Stop it or pass --port <n>.`),process.exit(2)}});console.log(`${opts.name} ${CURRENT_VERSION} \u2014 Rove home ${home}`);for(let server of servers){let host=server.hostname??"127.0.0.1",port=server.port??args.port;if(console.log(`listening on ${host}:${port}${args.preset==="none"?"":` (preset ${args.preset})`}`),host==="0.0.0.0"||host==="::")console.log(" WARNING: all interfaces, plain ws:// \u2014 prefer --preset tailscale or --preset cf");let publicHost=args.publicHost??host;if(opts.web)console.log(`
|
|
134
|
+
`):"no device tokens");return}if(args.action==="revoke-device"){let name2=args.device??"";console.log(revokeDevice(devicesPath,name2)?`revoked ${name2}`:`no device named ${name2}`);return}if(args.action==="setup"){printSetup(args.port);return}if((args.action==="relay-shell"||args.preset==="relay")&&!opts.web)console.error(`${opts.name}: the relay is for the browser client; run it as \`rove --web\``),process.exit(2);if(args.action==="relay-shell"&&opts.web){let shell=join8(opts.web.root,"relay","index.html");if(!existsSync6(shell))console.error(`${opts.name}: ${shell} is missing; build the web client first`),process.exit(1);let dir=args.relayShellDir??".";mkdirSync5(dir,{recursive:!0}),copyFileSync(shell,join8(dir,"index.html")),console.log(`wrote ${join8(dir,"index.html")}`);return}if(args.action==="set-password"||args.action==="clear-password"||args.action==="unlock"){try{process.exitCode=await runPasswordAction(args.action,bridgeDir)}catch(err){console.error(`${opts.name}: ${err instanceof Error?err.message:String(err)}`),process.exitCode=1}return}let{sharedToken,token,revoked}=startupCredentials({tokenPath:join8(bridgeDir,"token"),devicesPath,rotate:args.rotateToken,device:args.device});if(revoked>0)console.log(`${opts.name}: --rotate-token signed out ${revoked} device token(s)`);let daemonSocket=await ensureDaemonReachable(void 0,"autospawn");await ensurePtyHostReachable();let orchestrator=new RemoteOrchestrator(new RoveDaemonClient(daemonSocket),{role:"gui",graphicsOut:()=>{}});await orchestrator.init();let hub=new MachineFeed(orchestrator),machines=hub.machinesSignal();machineStore.set(machines.get()),machines.subscribe(()=>machineStore.set(machines.get()));let verbClient=new RoveDaemonClient(daemonSocket),machineOf3=(taskId2)=>{let alias=hub.tasksSignal().get().find((row)=>String(row.id)===taskId2)?.origin?.machineId;return alias&&alias!=="local"?alias:void 0},statusClient=new RoveDaemonClient(daemonSocket),rowStatus=new RowStatusFeed(statusClient),dispatches=new DispatchFeed(statusClient),panels=new PanelFeed;statusClient.onChannel("panel.show",(panel)=>panels.push(panel));let statusRetryMs=STATUS_RECONNECT_MS,syncStatus=async()=>{try{await statusClient.subscribe({channels:["row.status","dispatch.snapshot","panel.show"],role:"pane"}),await rowStatus.resync(),await dispatches.resync(),statusRetryMs=STATUS_RECONNECT_MS}catch{statusClient.forceDisconnect(),setTimeout(()=>void syncStatus(),statusRetryMs),statusRetryMs=Math.min(statusRetryMs*2,STATUS_RECONNECT_MAX_MS)}};statusClient.onLifecycle("close",()=>{setTimeout(()=>void syncStatus(),STATUS_RECONNECT_MS)}),await syncStatus();let api=createBridgeApi(verbClient,{machineOf:machineOf3,forward:(alias,verb,params)=>forwardVerb(requireMachine(alias),verb,params)}),sessionTitles=new SessionTitles(()=>new RoveDaemonClient(defaultPtyHostSocketPath())),ops=createRoveOps(verbClient,{changes:()=>orchestrator.worktreeChangesSignal()(),tokens:()=>orchestrator.rowTokensSignal()(),gates:()=>orchestrator.gatesSignal()(),remote:()=>hub.tasksSignal().get().filter((task)=>task.origin&&task.origin.machineId!=="local").map((task)=>({...task,kind:task.kind??"task",pinned:task.pinned??!1})),remoteLive:()=>hub.liveSignal().get(),rowStatus:()=>rowStatus.current(),titleSources:async()=>{let sessions=await sessionTitles.current(),state=loadStateFile();return{sessions,tabs:(taskId2)=>state[terminalTabsKey(taskId2)]}}},api),feed=new TaskFeed(()=>ops.tasks());for(let signal of[orchestrator.tasksSignal(),rowStatus,orchestrator.attentionInboxSignal(),orchestrator.worktreeChangesSignal(),orchestrator.rowTokensSignal(),orchestrator.gatesSignal(),hub.tasksSignal(),hub.liveSignal()])signal.subscribe(()=>feed.poke());let usage=orchestrator.usageSnapshotSignal();usageStore.set(usage()),usage.subscribe(()=>usageStore.set(usage()));let context=orchestrator.contextUsageSignal();usageStore.setContext(context()),context.subscribe(()=>usageStore.setContext(context())),setInterval(()=>feed.poke(),TICK_MS),sessionTitles.watch(()=>feed.active,()=>feed.poke());let notices=new NoticeFeed,noticeStore=orchestrator.noticeStore();noticeStore.subscribe(()=>notices.push(noticeStore.get()));let prompts=new PromptFeed,promptStore=orchestrator.uiPromptStore();promptStore.subscribe(()=>prompts.push(promptStore.get()));let modPanes=orchestrator.modPanesSignal(),mods={current:()=>modPanes.get(),subscribe:(listener)=>modPanes.subscribe(()=>listener(modPanes.get()))},statePath=defaultUiPrefsStatePath(),readState=()=>{try{let raw=JSON.parse(readFileSync6(statePath,"utf8"));return raw&&typeof raw==="object"?raw:{}}catch{return{}}},tabs=new TabFeed(readState);startFileWatchTrigger({filePath:statePath,debounceMs:TABS_DEBOUNCE_MS,onTrigger:()=>{tabs.check(),feed.poke()},onError:()=>{}});let ptySocket=defaultPtyHostSocketPath(),push=opts.web?new PushHub({path:join8(bridgeDir,"push.json"),feed,crossTask:()=>PREF_DEFS.crossTask.read(readState())===!0,credentialValid:(credential)=>credential===sharedCredential(token)||listDevices(devicesPath).some((d)=>d.name===credential),log:(line)=>console.error(line)}):null,sockets=new CredentialSockets,deps={token:sharedToken,sockets,ops,feed,notices,prompts,panels,mods,tabs,rowStatus,dispatches,deviceOf:(presented)=>deviceOf(devicesPath,presented),openPty:()=>new RoveDaemonClient(ptySocket),openMachinePty:async(alias)=>{let entry=requireMachine(alias);if(!await machineDaemonSocket(entry))throw new BridgeError("MACHINE_UNREACHABLE",`machine "${alias}" did not answer`);return new RoveDaemonClient(localPtySocketPath(alias))},roveVersion:CURRENT_VERSION,api,invoke:(verb,params,machine)=>invokeRouted({verb,params,machine,isLocalTask:(id)=>verbClient.request("task.get",{taskId:id}).then(()=>!0,()=>!1),runHere:()=>executeVerb(verb,{params},{client:verbClient,caller:REMOTE_CALLER})}),...args.cloudflare?{access:createAccessVerifier(args.cloudflare)}:{},...opts.web?{web:opts.web}:{},...opts.web?{login:createWebLogin({bridgeDir,devicesPath,publicHost:args.publicHost,trustForwardedIp:args.preset==="cf"||args.preset==="none"&&args.publicHost!==null,log:(line)=>console.error(line),onRevoked:(device)=>{let closed=sockets.closeCredential(device);if(closed>0)console.error(`[rove-bridge] closed ${closed} socket(s) of signed-out device ${device}`)}})}:{},publicHost:args.publicHost,...push?{push}:{}},relay=args.relayServer?startRelay(deps,args.relayServer,bridgeDir,args.rotateRelay):null,servers=args.hosts.map((hostname)=>{try{return startBridgeServer(deps,{hostname,port:args.port})}catch(err){if(!(err&&typeof err==="object"&&("code"in err)&&err.code==="EADDRINUSE"))throw err;console.error(`${opts.name}: ${hostname}:${args.port} is already in use (another rove-bridge?). Stop it or pass --port <n>.`),process.exit(2)}});console.log(`${opts.name} ${CURRENT_VERSION} \u2014 Rove home ${home}`);for(let server of servers){let host=server.hostname??"127.0.0.1",port=server.port??args.port;if(console.log(`listening on ${host}:${port}${args.preset==="none"?"":` (preset ${args.preset})`}`),host==="0.0.0.0"||host==="::")console.log(" WARNING: all interfaces, plain ws:// \u2014 prefer --preset tailscale or --preset cf");let publicHost=args.publicHost??host;if(opts.web)console.log(`
|
|
135
135
|
open: ${webUrl({host:publicHost,port,token,tls:args.tls})}`);if(opts.web&&readPassword(webPasswordPaths(bridgeDir).password))console.log(`password login: on at ${webUrl({host:publicHost,port,token:"",tls:args.tls}).split("#")[0]}`);if(relay)continue;let url=pairingUrl({host:publicHost,port,token,tls:args.tls,preset:args.preset});if(console.log(`${opts.web?"":`
|
|
136
136
|
`}pair: ${url}`),args.qr&&!opts.web)console.log(renderUnicodeCompact(url))}if(relay&&args.relayServer){let link=relayLink({server:args.relayServer,code:relay.code,token});if(console.log(`
|
|
137
137
|
relay (end-to-end encrypted through ${args.relayServer}):
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{formatBytes}from"./chunk-my20q579.js";import{hookConfigIssues}from"./chunk-c8qjy6rx.js";import{applyFixes,daemonRestartFix,defaultFixRuntime,engineTabsManualFix,humanOnlyFix,killOrphansManualFix,noEngineFix,reinstallManualFix,resetManualFix,skillInstallFix,spawnHelperFix}from"./chunk-qx6d4bbb.js";import{inspectLegacyTmux,legacyTmuxDoctorLines}from"./chunk-gkdfct7t.js";import"./chunk-
|
|
2
|
+
import{formatBytes}from"./chunk-my20q579.js";import{hookConfigIssues}from"./chunk-c8qjy6rx.js";import{applyFixes,daemonRestartFix,defaultFixRuntime,engineTabsManualFix,humanOnlyFix,killOrphansManualFix,noEngineFix,reinstallManualFix,resetManualFix,skillInstallFix,spawnHelperFix}from"./chunk-qx6d4bbb.js";import{inspectLegacyTmux,legacyTmuxDoctorLines}from"./chunk-gkdfct7t.js";import"./chunk-f7a84zv4.js";import"./chunk-xr117xs9.js";import"./chunk-a4hsqj1b.js";import"./chunk-6hcyvg0m.js";import{skillOverview}from"./chunk-h27064am.js";import"./chunk-rmnm3f9n.js";import"./chunk-cpgnm1xr.js";import"./chunk-r3s2t9af.js";import"./chunk-6xr1prk6.js";import{describeAccount,detectEngineStatuses,probeableEngineIds,summarizeEngines}from"./chunk-eyaaaprw.js";import"./chunk-q8ffd1pc.js";import"./chunk-4hestsjp.js";import"./chunk-qgrg9a5n.js";import"./chunk-2bgmby1d.js";import"./chunk-n0rdn2fn.js";import"./chunk-5mhvjytn.js";import"./chunk-atn06ncf.js";import"./chunk-vp29jgbt.js";import"./chunk-gq36nzg8.js";import{resolveNodeBinary}from"./chunk-d4zw1g83.js";import"./chunk-w77s2df8.js";import"./chunk-mqfvzdpr.js";import{formatDaemonInfo}from"./chunk-rxqdw6s5.js";import"./chunk-0m8javx8.js";import"./chunk-xfgqae9t.js";import"./chunk-j1z9kgyd.js";import"./chunk-g9ba25q5.js";import"./chunk-tn9gtf18.js";import{isStaleInstallError,resolveRoveSpawn}from"./chunk-cb80ft39.js";import"./chunk-mdwqpnde.js";import"./chunk-mv229800.js";import"./chunk-jnt3nf6w.js";import"./chunk-hgtf6yvg.js";import"./chunk-2tfnscxk.js";import"./chunk-betn5ma1.js";import"./chunk-w7zv47mp.js";import"./chunk-2w228s7t.js";import"./chunk-etfh8f4z.js";import"./chunk-gygdyf0s.js";import"./chunk-dmzg9zdc.js";import"./chunk-40dgv828.js";import"./chunk-kg9kmwt9.js";import{CURRENT_VERSION,package_default}from"./chunk-jxqhq9va.js";import"./chunk-zsfyj36g.js";import"./chunk-2brn8e1y.js";import"./chunk-7mwvvj46.js";import"./chunk-32j6y5d1.js";import{t}from"./chunk-4d5xp2p0.js";import"./chunk-b4egks20.js";import"./chunk-cmp64q4j.js";import{RoveDaemonClient,isForeignDaemonHome}from"./chunk-496egqmk.js";import"./chunk-96kvwzj8.js";import"./chunk-7bakcyes.js";import"./chunk-8qkyd589.js";import"./chunk-7kysxgbq.js";import{readableLegacyIndexPath}from"./chunk-hz1x3hak.js";import"./chunk-x1jxxmtc.js";import"./chunk-xy8be792.js";import"./chunk-h2m7em1e.js";import"./chunk-7ghwdjdc.js";import"./chunk-8t7qbr1e.js";import"./chunk-bejhca6z.js";import{readPidFile}from"./chunk-br8r72bf.js";import"./chunk-yt15y085.js";import{homeDir,kvStatePath,roveStateDir}from"./chunk-z1nj22va.js";import{ROVE_PRODUCT_NAME}from"./chunk-zt7wfnva.js";import"./chunk-0g4wc808.js";import"./chunk-pn5ygqn6.js";import{defaultDaemonLogPath,defaultDaemonPidPath,defaultDaemonSocketPath,defaultPtyHostLogPath,defaultPtyHostPidPath,defaultPtyHostSocketPath}from"./chunk-eqdbfgq5.js";import{preRenameStateDir}from"./chunk-f8gg8cqh.js";import"./chunk-nc46gddg.js";import"./chunk-pxk2c2vj.js";import{__require}from"./chunk-91sykzqq.js";import{existsSync,readFileSync as readFileSync2,statSync as statSync2}from"fs";import{join as join2}from"path";var MIN_BUN_VERSION=package_default.engines.bun.match(/\d+\.\d+\.\d+/)?.[0]??"0.0.0";function parseBunVersion(raw){return raw.trim().match(/^v?(\d+\.\d+\.\d+)/)?.[1]??null}function isBunAtLeast(raw,minimum=MIN_BUN_VERSION){let found=parseBunVersion(raw),floor=parseBunVersion(minimum);if(!found||!floor)return!0;let a=found.split(".").map(Number),b=floor.split(".").map(Number);for(let i=0;i<3;i++){let av=a[i]??0,bv=b[i]??0;if(av!==bv)return av>bv}return!0}function classifyHookChannel(input){let total=0,hooked=0;for(let tabs of Object.values(input.tabs))for(let entry of Object.values(tabs))if(total++,entry.source==="hook")hooked++;if(total===0)return{kind:"no-tabs"};if(hooked===0)return{kind:"down",totalTabs:total};return{kind:"live",hookTabs:hooked,totalTabs:total}}function hookChannelDoctorLines(verdict,input,cliName){let configLines=(input.configIssues??[]).flatMap((issue)=>[` \u26A0 hook install skipped: ${issue.file}`,` ${issue.reason} \u2014 fix the file, then relaunch Rove`]);if(verdict.kind==="no-tabs")return["hooks: \u2014 no engine tabs yet (nothing to check)",...configLines];if(verdict.kind==="live")return[`hooks: \u2713 engine hook channel live (${verdict.hookTabs}/${verdict.totalTabs} tab(s) hook-sourced)`,...configLines];let out=[`hooks: \u2717 NO hook events reaching the daemon (0/${verdict.totalTabs} tab(s) hook-sourced)`," badges fall back to a ~10s poll, so activity looks seconds late",` daemon socket: ${input.socketPath}`,...configLines];return out.push(' \u2192 compare with an engine tab\'s own path: `ps eww -p <engine-pid> | tr " " "\\n" | grep DAEMON_SOCKET_PATH`',` \u2192 restart the engine tabs (they may hold a stale socket path), or run \`${cliName} daemon restart\``,` \u2192 debug one hook directly: \`ROVE_HOOK_DEBUG=1 echo '{}' | ${cliName} hook turn-start --engine claude\``),out}import{readdirSync,statSync}from"fs";import{createRequire}from"module";import{dirname,join}from"path";function installedSpawnHelpers(){let pkg;try{pkg=dirname(createRequire(import.meta.url).resolve("node-pty/package.json"))}catch{return[]}let prebuilds=join(pkg,"prebuilds"),arches;try{arches=readdirSync(prebuilds).filter((name)=>name.startsWith("darwin-"))}catch{return[]}let helpers=[];for(let arch of arches){let path=join(prebuilds,arch,"spawn-helper");try{helpers.push({path,executable:(statSync(path).mode&64)!==0})}catch{}}return helpers}function spawnHelperDoctorLines(helpers){let broken=helpers.filter((helper)=>!helper.executable).map((helper)=>helper.path);if(helpers.length===0)return{lines:["node-pty: ? no darwin spawn-helper found next to node-pty"],broken};if(broken.length===0)return{lines:[`node-pty: \u2713 spawn-helper executable (${helpers.length} arch)`],broken};return{lines:["node-pty: \u2717 spawn-helper is not executable \u2014 every node-pty PTY spawn fails",...broken.map((path)=>` ${path}`),` \u2192 chmod 755 ${broken.join(" ")}`],broken}}import{appendFileSync,readFileSync}from"fs";var PTY_MARKER="ROVE_TERMINAL_PTY=1",run=async(argv)=>{try{let proc=Bun.spawn([...argv],{stdin:"ignore",stdout:"pipe",stderr:"ignore"}),[stdout,code]=await Promise.all([new Response(proc.stdout).text().catch(()=>""),proc.exited]);return{code,stdout}}catch{return{code:127,stdout:""}}};function parsePsRows(output){let rows=[];for(let line of output.split(`
|
|
3
3
|
`)){let parts=line.trim().split(/\s+/);if(parts.length<6)continue;let[pid,ppid,pgid]=[parts[0],parts[1],parts[2]].map((value)=>Number.parseInt(value??"",10)),rssKb=Number.parseInt(parts[4]??"",10);if(!Number.isFinite(pid)||!Number.isFinite(ppid)||!Number.isFinite(pgid)||!Number.isFinite(rssKb))continue;rows.push({pid,ppid,pgid,etime:parts[3]??"",rssKb,command:parts.slice(5).join(" ")})}return rows}function pidAlive(pid,rows){return rows.some((row)=>row.pid===pid)}function orphanCandidates(rows,selfPgid,liveSessionPids){return rows.filter((row)=>row.ppid===1&&row.pgid!==selfPgid&&!liveSessionPids.has(row.pgid)&&!pidAlive(row.pgid,rows))}async function markedPids(pids,deps={}){let runProbe=deps.run??run,readEnviron=deps.readEnviron??((pid)=>readFileSync(`/proc/${pid}/environ`,"utf8")),marked=new Set;if(pids.length===0)return{marked,failed:null};if((deps.platform??process.platform)==="linux"){let refused=null;for(let pid of pids)try{if(readEnviron(pid).split("\x00").includes(PTY_MARKER))marked.add(pid)}catch(err){let code=err.code;if(code!=="ENOENT"&&code!=="ESRCH")refused??=`/proc/${pid}/environ: ${code??"unreadable"}`}return{marked,failed:refused}}let result=await runProbe(["ps","eww","-o","pid=,command=","-p",pids.join(",")]);if(result.code!==0)return{marked,failed:`ps eww exited ${result.code}`};let marker=new RegExp(`(^|\\s)${PTY_MARKER}(\\s|$)`);for(let line of result.stdout.split(`
|
|
4
4
|
`)){let pid=Number.parseInt(line.trim().split(/\s+/)[0]??"",10);if(Number.isFinite(pid)&&marker.test(line))marked.add(pid)}return{marked,failed:null}}async function ownPgid(runProbe){let result=await runProbe(["ps","-o","pgid=","-p",String(process.pid)]),pgid=Number.parseInt(result.stdout.trim(),10);return Number.isFinite(pgid)?pgid:-1}async function collectOrphans(liveSessionPids,deps={}){let runProbe=deps.run??run;if((deps.platform??process.platform)==="win32")return{orphans:[],error:null};let ps=await runProbe(["ps","-A","-o","pid=,ppid=,pgid=,etime=,rss=,command="]);if(ps.code!==0)return{orphans:[],error:`could not read the process table \u2014 ps exited ${ps.code}`};let rows=parsePsRows(ps.stdout),candidates=orphanCandidates(rows,await ownPgid(runProbe),liveSessionPids),{marked,failed}=await markedPids(candidates.map((row)=>row.pid),deps);if(failed)return{orphans:[],error:`could not read process environments \u2014 ${failed}`};return{orphans:candidates.filter((row)=>marked.has(row.pid)),error:null}}function formatMb(rssKb){return`${(rssKb/1024).toFixed(0)} MB`}function orphanDoctorLines(orphans,error,cliName,killing=!1){if(error)return[`orphans: \u2717 ${error}`];if(orphans.length===0)return["orphans: \u2713 none \u2014 no processes left behind by a dead PTY session"];let totalMb=orphans.reduce((sum,row)=>sum+row.rssKb,0)/1024,lines=[`orphans: \u26A0 ${orphans.length} process(es) outlived the PTY session that spawned them (${totalMb.toFixed(0)} MB RSS)`," each is reparented to init, carries Rove's PTY marker, and its process"," group leader is gone \u2014 no live task owns them"];for(let row of[...orphans].sort((a,b)=>b.rssKb-a.rssKb))lines.push(` pid ${row.pid} (group ${row.pgid}) up ${row.etime}, ${formatMb(row.rssKb)}: ${row.command.slice(0,90)}`);if(!killing)lines.push(` \u2192 \`${cliName} doctor --kill-orphans\` ends those process groups (SIGTERM, then SIGKILL)`," read the list first: something you backgrounded from a Rove terminal and then"," closed the tab on looks exactly like a leak, and doctor cannot tell them apart");return lines}var GROUP_EXIT_GRACE_MS=2000,GROUP_POLL_MS=100;function logOrphanKill(pgid,signal,logPath=defaultDaemonLogPath()){try{appendFileSync(logPath,formatDaemonInfo("doctor-kill-orphans",`${signal} process group ${pgid}`))}catch{}}function groupAlive(pgid){try{return process.kill(-pgid,0),!0}catch(err){return err.code!=="ESRCH"}}async function killOrphanGroups(orphans){let groups=[...new Set(orphans.map((row)=>row.pgid))];for(let pgid of groups)try{process.kill(-pgid,"SIGTERM"),logOrphanKill(pgid,"SIGTERM")}catch{}let deadline=Date.now()+GROUP_EXIT_GRACE_MS,survivors=groups.filter(groupAlive);while(survivors.length>0&&Date.now()<deadline)await new Promise((resolve)=>setTimeout(resolve,GROUP_POLL_MS)),survivors=survivors.filter(groupAlive);for(let pgid of survivors)try{process.kill(-pgid,"SIGKILL"),logOrphanKill(pgid,"SIGKILL")}catch{}return await new Promise((resolve)=>setTimeout(resolve,GROUP_POLL_MS)),{groups,survivors:survivors.filter(groupAlive)}}function multiplexerLabel(env){if(env.TMUX)return"tmux";if(env.ZELLIJ)return"zellij";if(env.STY)return"screen";return"no"}function terminalEnvLines(env){let show=(v)=>v&&v.length>0?v:"(unset)",program=env.TERM_PROGRAM?`${env.TERM_PROGRAM}${env.TERM_PROGRAM_VERSION?` v${env.TERM_PROGRAM_VERSION}`:""}`:"(unset)";return[`terminal: TERM=${show(env.TERM)} TERM_PROGRAM=${program} COLORTERM=${show(env.COLORTERM)}`,` running inside a multiplexer: ${multiplexerLabel(env)}`]}function parseKittyProbeReply(data){let kitty=data.match(/\x1b\[\?(\d+)u/);if(kitty?.[1]!==void 0)return{kind:"supported",flags:Number.parseInt(kitty[1],10)};if(/\x1b\[\?[\d;]*c/.test(data))return{kind:"unsupported"};return null}function kittyProbeLine(result){switch(result.kind){case"supported":return` kitty keyboard protocol: \u2713 answered (flags=${result.flags})`;case"unsupported":return[" kitty keyboard protocol: \u2717 not supported \u2014 legacy key path"," (ctrl+h/ctrl+j arrive as C0 backspace/linefeed bytes; the"," split chords ctrl+\\ and ctrl+= cannot be encoded at all)"].join(`
|
|
5
5
|
`);case"no-response":return" kitty keyboard protocol: ? no reply (terminal ignored both the kitty query and DA1)";case"skipped":return` kitty keyboard protocol: skipped (${result.reason})`}}async function probeKittyKeyboard(timeoutMs=300){let stdin=process.stdin;if(!stdin.isTTY||!process.stdout.isTTY)return{kind:"skipped",reason:"not an interactive terminal"};let wasRaw=stdin.isRaw===!0,buffer="";return await new Promise((resolve)=>{let done=!1,finish=(result)=>{if(done)return;if(done=!0,clearTimeout(timer),stdin.off("data",onData),stdin.pause(),!wasRaw)stdin.setRawMode(!1);resolve(result)},onData=(chunk)=>{buffer+=chunk.toString("latin1");let decided=parseKittyProbeReply(buffer);if(decided)finish(decided)},timer=setTimeout(()=>finish({kind:"no-response"}),timeoutMs);stdin.setRawMode(!0),stdin.resume(),stdin.on("data",onData),process.stdout.write("\x1B[?u\x1B[c")})}async function terminalDoctorLines(){return[...terminalEnvLines(process.env),kittyProbeLine(await probeKittyKeyboard())]}async function probeGit(){try{let proc=Bun.spawn(["git","--version"],{stdin:"ignore",stdout:"pipe",stderr:"ignore"}),text=(await new Response(proc.stdout).text()).trim();if(await proc.exited===0&&text)return{line:`git: \u2713 ${text}`,found:!0}}catch{}return{line:"git: \u2717 not found on PATH",found:!1}}function binaryLabel(binary){return binary.found?`\u2713 ${binary.path}`:`\u2717 ${binary.error}`}async function probeEngines(){let statuses=await detectEngineStatuses(await probeableEngineIds()),lines=["engines:"];for(let status of statuses){let account=describeAccount(status.account),name=`${status.vendor.padEnd(7)} `;if(lines.push(` ${name}${binaryLabel(status.binary)}${status.binary.found?` \u2014 ${account}`:""}`),status.accountError)lines.push(` \u26A0 ${status.accountError}`)}let{usable,signedOut}=summarizeEngines(statuses);return{lines,anyUsable:usable.length>0,signedOut}}var CLI_NAME=ROVE_PRODUCT_NAME;function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch(err){return err.code==="EPERM"}}async function requestIfReachable(socketPath,name){let client=new RoveDaemonClient(socketPath);try{return await client.request(name,{})}catch{return null}finally{client.close()}}function fmtDuration(ms){let seconds=Math.floor(ms/1000);if(seconds<60)return`${seconds}s`;let minutes=Math.floor(seconds/60);if(minutes<60)return`${minutes}m ${seconds%60}s`;return`${Math.floor(minutes/60)}h ${minutes%60}m`}function describeFile(path){try{let stat=statSync2(path);return`present (${formatBytes(stat.size)}, modified ${stat.mtime.toISOString()})`}catch{return"absent"}}function taskCount(path){try{let parsed=JSON.parse(readFileSync2(path,"utf8"));return Array.isArray(parsed.tasks)?parsed.tasks.length:null}catch{return null}}function tailFile(path,count){try{return readFileSync2(path,"utf8").split(`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{deliverPromptToLiveEngineAdapter,deliverPromptToLiveEngineDetailedAdapter,deliverPromptToLiveEngineTabDetailedAdapter,deriveTitleFromSession,deriveTitleFromSessionId,ensureTaskSessionAdapter,startTaskSessionWithPromptAdapter,tearDownTaskSessionAdapter}from"./chunk-0annhyhx.js";import{buildCIPromptForWorktree}from"./chunk-vkz6kkmm.js";import{Orchestrator}from"./chunk-jn8fkzm7.js";import{GH_PR_VIEW_FIELDS,classifyGhFailure,mapGhPrView,nextPrPoll,samePrStatus}from"./chunk-qnv8ggxh.js";import"./chunk-c1dz3qg3.js";import{resolvePluginBinPath}from"./chunk-b0ssvpht.js";import{GitWorktreeManager,parseDirtyPaths}from"./chunk-jkssty8x.js";import{startChatChannels}from"./chunk-429zca0e.js";import{auditDeletionBranchKept,auditDeletionResidue,auditDeletionSalvaged,auditWorktreeResidue,auditWorktreeSalvaged,parseAheadBehind,startDaemonServer}from"./chunk-
|
|
2
|
+
import{deliverPromptToLiveEngineAdapter,deliverPromptToLiveEngineDetailedAdapter,deliverPromptToLiveEngineTabDetailedAdapter,deriveTitleFromSession,deriveTitleFromSessionId,ensureTaskSessionAdapter,startTaskSessionWithPromptAdapter,tearDownTaskSessionAdapter}from"./chunk-0annhyhx.js";import{buildCIPromptForWorktree}from"./chunk-vkz6kkmm.js";import{Orchestrator}from"./chunk-jn8fkzm7.js";import{GH_PR_VIEW_FIELDS,classifyGhFailure,mapGhPrView,nextPrPoll,samePrStatus}from"./chunk-qnv8ggxh.js";import"./chunk-c1dz3qg3.js";import{resolvePluginBinPath}from"./chunk-b0ssvpht.js";import{GitWorktreeManager,parseDirtyPaths}from"./chunk-jkssty8x.js";import{startChatChannels}from"./chunk-429zca0e.js";import{auditDeletionBranchKept,auditDeletionResidue,auditDeletionSalvaged,auditWorktreeResidue,auditWorktreeSalvaged,parseAheadBehind,startDaemonServer}from"./chunk-f7a84zv4.js";import"./chunk-xr117xs9.js";import"./chunk-a4hsqj1b.js";import"./chunk-6hcyvg0m.js";import{refreshOutdatedSkills}from"./chunk-h27064am.js";import"./chunk-4dt1gfb3.js";import"./chunk-rmnm3f9n.js";import"./chunk-cpgnm1xr.js";import"./chunk-r3s2t9af.js";import"./chunk-6xr1prk6.js";import{preferredReleaseChannel,updateDelayHours}from"./chunk-ydaj339x.js";import{cancelAllLogins}from"./chunk-pm91e38v.js";import{PLACEHOLDER_TASK_TITLE}from"./chunk-2xsxcdp6.js";import{affectsActivityState,isEngineActivityKind}from"./chunk-17e2tgyg.js";import"./chunk-ee8917py.js";import"./chunk-q8ffd1pc.js";import"./chunk-4hestsjp.js";import"./chunk-qgrg9a5n.js";import"./chunk-2bgmby1d.js";import"./chunk-n0rdn2fn.js";import"./chunk-5mhvjytn.js";import"./chunk-atn06ncf.js";import"./chunk-vp29jgbt.js";import"./chunk-gq36nzg8.js";import{RELAY_PREAMBLE}from"./chunk-sq6zj3fb.js";import{TaskIndexStore}from"./chunk-7kgcy89w.js";import"./chunk-qkehg2c1.js";import"./chunk-xehd2vx3.js";import"./chunk-d4zw1g83.js";import"./chunk-w77s2df8.js";import"./chunk-mqfvzdpr.js";import"./chunk-92hpkjkk.js";import{installDaemonCrashHandlers,logDaemonError,logDaemonInfo}from"./chunk-rxqdw6s5.js";import"./chunk-8ng1756g.js";import"./chunk-kxp04jnp.js";import{autoStatusEnabled,protocolUpgradeFromLiveSession,protocolWriteBackFromLiveSession}from"./chunk-ypfrkpe9.js";import{engineDisplayName,engineProtocolKey,protocolEntry,roveApiInvocation,sessionProtocol}from"./chunk-0m8javx8.js";import"./chunk-r5j8fycq.js";import"./chunk-xfgqae9t.js";import"./chunk-j1z9kgyd.js";import{teardownScope}from"./chunk-g9ba25q5.js";import"./chunk-paashxkd.js";import"./chunk-hpay0ax8.js";import"./chunk-catrr0z3.js";import{stateFileTabsPersistence}from"./chunk-mdc7b01e.js";import"./chunk-tn9gtf18.js";import{execHostForWorktreePath}from"./chunk-22emnsn5.js";import"./chunk-sak1tphp.js";import"./chunk-y1xnjx8x.js";import{connectOrStartDaemon,daemonSpawnReason}from"./chunk-cb80ft39.js";import{stopDaemonProcess}from"./chunk-mdwqpnde.js";import"./chunk-mv229800.js";import{foregroundEngineIn,parsePsSnapshot,psSnapshot}from"./chunk-jnt3nf6w.js";import{createEngineTurnDetector,engineEntry,engineTitleTurnHint,vendorsWithQuotaProbe}from"./chunk-hgtf6yvg.js";import{spawnCapture}from"./chunk-2tfnscxk.js";import{availableEngineIds}from"./chunk-betn5ma1.js";import"./chunk-w7zv47mp.js";import"./chunk-2w228s7t.js";import"./chunk-etfh8f4z.js";import"./chunk-8ttn73cv.js";import{backfillSavedReposFromProjects,getCustomEngineIds,getPersistedString,getRemoteRepoConfig,getSavedRepos,setPersistedString}from"./chunk-gygdyf0s.js";import{parsePorcelain}from"./chunk-0r1jf8m9.js";import"./chunk-t7j8g9f6.js";import"./chunk-dmzg9zdc.js";import"./chunk-40dgv828.js";import{DEFAULT_BASE_REF_CANDIDATES,readHeadSha,readRefSha,resolveGitDirs}from"./chunk-kg9kmwt9.js";import"./chunk-xmwp6je4.js";import{readOnlyGitProcessEnv}from"./chunk-6xppe9bj.js";import{CURRENT_VERSION,checkLatestVersion}from"./chunk-jxqhq9va.js";import{argvHasFlag,switchFlag}from"./chunk-zsfyj36g.js";import"./chunk-2brn8e1y.js";import"./chunk-7mwvvj46.js";import{SUBCOMMAND_VERBS}from"./chunk-nbcm8d58.js";import{loadStateFile}from"./chunk-32j6y5d1.js";import"./chunk-b4egks20.js";import"./chunk-cmp64q4j.js";import{RoveDaemonClient}from"./chunk-496egqmk.js";import"./chunk-96kvwzj8.js";import"./chunk-7bakcyes.js";import"./chunk-8qkyd589.js";import"./chunk-7kysxgbq.js";import"./chunk-hz1x3hak.js";import"./chunk-x1jxxmtc.js";import{DEFAULT_TASK_VENDOR,isTaskStatus}from"./chunk-xy8be792.js";import"./chunk-h2m7em1e.js";import"./chunk-7ghwdjdc.js";import{migrateRoveDaemonStateLayout}from"./chunk-8t7qbr1e.js";import{releaseSync}from"./chunk-bejhca6z.js";import{isProcessAlive,readPidFile}from"./chunk-br8r72bf.js";import"./chunk-yt15y085.js";import"./chunk-z1nj22va.js";import{ROVE_PRODUCT_NAME}from"./chunk-zt7wfnva.js";import{rotateLogIfNeeded}from"./chunk-0g4wc808.js";import"./chunk-pn5ygqn6.js";import{defaultDaemonLogPath,defaultDaemonPidPath,defaultDaemonSocketPath}from"./chunk-eqdbfgq5.js";import"./chunk-f8gg8cqh.js";import{readRoveHomeDirEnv}from"./chunk-nc46gddg.js";import"./chunk-pxk2c2vj.js";import{__require}from"./chunk-91sykzqq.js";async function latestTranscriptMtime(vendor,worktree){if(!worktree)return 0;return protocolEntry(vendor).history.latestTranscriptMtimeForWorktree(worktree)}async function maybeAutoStart(orch,taskId,enabled=autoStatusEnabled){if(!enabled())return"skipped";let task=orch.getTask(taskId);if(!task)return"skipped";if((task.kind??"task")==="main")return"skipped";if(task.status!=="backlog")return"skipped";return await orch.setStatus(taskId,"in_progress"),"moved"}var SYNC_TIMEOUT_MS=60000,SYNC_CONFLICT="SYNC_CONFLICT",SYNC_DIRTY="SYNC_WORKTREE_DIRTY";async function git(cwd,args,signal,write=!1){return spawnCapture("git",args,{cwd,env:write?process.env:readOnlyGitProcessEnv(),signal})}function parseConflictedPaths(stdout){return stdout.split(`
|
|
3
3
|
`).map((line)=>line.trim()).filter((line)=>line.length>0)}async function syncWorktreeWithBase(worktreePath,baseRef,signal){let status=await git(worktreePath,["status","--porcelain=v1"],signal);if(status.status!==0)throw Error("git status failed");let dirty=parseDirtyPaths(status.stdout);if(dirty.length>0)throw Error(`${SYNC_DIRTY}: ${dirty.join(", ")}`);let before=await git(worktreePath,["rev-parse","HEAD"],signal),merge=await git(worktreePath,["merge","--no-edit",baseRef],signal,!0);if(merge.status!==0){let conflicted=await git(worktreePath,["diff","--name-only","--diff-filter=U"],signal),paths=parseConflictedPaths(conflicted.stdout);if(paths.length>0)throw Error(`${SYNC_CONFLICT}: ${paths.join(", ")}`);let detail=merge.stderr?.trim();throw Error(detail?`git merge ${baseRef} failed \u2014 ${detail}`:`git merge ${baseRef} failed`)}let after=await git(worktreePath,["rev-parse","HEAD"],signal);return{baseRef,alreadyCurrent:before.status===0&&before.stdout.trim()===after.stdout.trim()}}import{readFileSync}from"fs";import{join}from"path";var BASE_REF_TTL_MS=300000,cache=new Map;async function git2(worktreePath,args,signal){return await spawnCapture("git",[...args],{cwd:worktreePath,env:readOnlyGitProcessEnv(),signal})}async function refExists(worktreePath,ref,signal){return(await git2(worktreePath,["rev-parse","--verify","--quiet",ref],signal)).status===0}async function sharesHistory(worktreePath,ref,signal){return(await git2(worktreePath,["merge-base",ref,"HEAD"],signal)).status===0}async function baseCheckoutBranchFromGit(worktreePath,signal){let out=await git2(worktreePath,["worktree","list","--porcelain"],signal);if(out.status!==0)return null;return(out.stdout.split(`
|
|
4
4
|
|
|
5
5
|
`,1)[0]??"").split(`
|