@sma1lboy/rove 0.9.283-next.87 → 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-ycb9tyj3.js → chunk-sdtp05gh.js} +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/web/assets/index-e7XrOFSi.js +154 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- 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,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{ROVE_PRODUCT_NAME}from"./chunk-zt7wfnva.js";import"./chunk-nc46gddg.js";import{__require}from"./chunk-91sykzqq.js";import{existsSync}from"fs";import{join}from"path";function webRoot(){return[join(import.meta.dir,"..","web"),join(import.meta.dir,"..","..","..","rove-web","dist")].find((dir)=>existsSync(join(dir,"index.html")))??null}async function runWebSubcommand(args){let root=webRoot();if(!root)console.error(`${ROVE_PRODUCT_NAME} --web: the browser client is not in this build. From a checkout, run \`bun --filter rove-web build\` first; the npm package ships it in dist/web.`),process.exit(2);let{runBridge}=await import("./chunk-
|
|
2
|
+
import{ROVE_PRODUCT_NAME}from"./chunk-zt7wfnva.js";import"./chunk-nc46gddg.js";import{__require}from"./chunk-91sykzqq.js";import{existsSync}from"fs";import{join}from"path";function webRoot(){return[join(import.meta.dir,"..","web"),join(import.meta.dir,"..","..","..","rove-web","dist")].find((dir)=>existsSync(join(dir,"index.html")))??null}async function runWebSubcommand(args){let root=webRoot();if(!root)console.error(`${ROVE_PRODUCT_NAME} --web: the browser client is not in this build. From a checkout, run \`bun --filter rove-web build\` first; the npm package ships it in dist/web.`),process.exit(2);let{runBridge}=await import("./chunk-0sb22rnm.js");await runBridge(args,{name:`${ROVE_PRODUCT_NAME} --web`,web:{root}})}export{runWebSubcommand};
|
package/dist/cli/index.js
CHANGED
|
@@ -23,4 +23,4 @@ ${ADOPT_USAGE}
|
|
|
23
23
|
pass a path glob to adopt, e.g. ${CLI_NAME} adopt '${repo}/*' --yes`);return}let matched=worktrees.filter(isMatch);if(matched.length===0){console.log(`
|
|
24
24
|
no worktrees match glob ${JSON.stringify(glob)}`);return}if(!yes){console.log(`
|
|
25
25
|
${matched.length} worktree(s) match \u2014 re-run with --yes to adopt them`);return}await adoptWorktreesInto(orch,repo,matched,vendor),console.log(`done \u2014 adopted ${matched.length} worktree(s)`)}function printTopLevelUsage(out){out.write(`${topLevelUsage()}
|
|
26
|
-
`)}var COMMANDS=new Map([["add",runAddSubcommand],["remove",runRemoveSubcommand],["adopt",runAdoptSubcommand],...DYNAMIC_COMMANDS]);async function main(){let[,,...rawArgs]=process.argv,[subcommand,...rest]=rawArgs;if(subcommand==="--version"||subcommand==="-v"||subcommand==="version"){let{CURRENT_VERSION:CURRENT_VERSION2}=await import("./chunk-231mqmk5.js");console.log(`${CLI_NAME} ${CURRENT_VERSION2}`);return}if(subcommand==="--help"||subcommand==="-h"||subcommand==="help"){printTopLevelUsage(process.stdout);return}if(subcommand==="--skill"){let{runSkillSubcommand}=await import("./chunk-xb196wqg.js");await runSkillSubcommand(["print"]);return}if(subcommand==="--web"){let{runWebSubcommand}=await import("./chunk-
|
|
26
|
+
`)}var COMMANDS=new Map([["add",runAddSubcommand],["remove",runRemoveSubcommand],["adopt",runAdoptSubcommand],...DYNAMIC_COMMANDS]);async function main(){let[,,...rawArgs]=process.argv,[subcommand,...rest]=rawArgs;if(subcommand==="--version"||subcommand==="-v"||subcommand==="version"){let{CURRENT_VERSION:CURRENT_VERSION2}=await import("./chunk-231mqmk5.js");console.log(`${CLI_NAME} ${CURRENT_VERSION2}`);return}if(subcommand==="--help"||subcommand==="-h"||subcommand==="help"){printTopLevelUsage(process.stdout);return}if(subcommand==="--skill"){let{runSkillSubcommand}=await import("./chunk-xb196wqg.js");await runSkillSubcommand(["print"]);return}if(subcommand==="--web"){let{runWebSubcommand}=await import("./chunk-sdtp05gh.js");await runWebSubcommand(rest);return}let handler=subcommand!==void 0?COMMANDS.get(subcommand):void 0;if(handler){await handler(rest);return}if(subcommand!==void 0&&isPathLikeArg(subcommand)){let{runOpenDirectory}=await import("./chunk-eae2x886.js");await runOpenDirectory(subcommand);return}if(subcommand!==void 0)console.error(`${CLI_NAME}: unknown command '${subcommand}'`),printTopLevelUsage(process.stderr),process.exit(2);let{publishRoveTerminalTitle}=await import("./chunk-g8wrdp7n.js");publishRoveTerminalTitle();let{launchTui}=await import("./chunk-8ww1e4nk.js");await launchTui()}var cliDone=main().catch(async(err)=>{let{formatCliFailure}=await import("./chunk-tg4swhr1.js");console.error(formatCliFailure(err,{cliName:CLI_NAME,argv:process.argv,cwd:process.cwd(),env:process.env})),process.exit(1)});export{cliDone};
|