tmux-weblink 2.0.0 → 2.0.1

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.
@@ -1,8 +1,8 @@
1
- import{existsSync as W,readdirSync as je,statSync as C,unlinkSync as ke,writeFileSync as ve,mkdirSync as be,readFileSync as Q}from"node:fs";import{readFile as qe}from"node:fs/promises";import{execSync as E,execFileSync as Y}from"node:child_process";import h from"node:path";import{fileURLToPath as Se}from"node:url";import{Hono as Te}from"hono";import{listSessions as R}from"../sessions.js";import{renderLoginPage as Ae,renderNotesIndex as xe,renderNotesPage as Ie,renderSettings as Ce,renderThemeSettings as _e,renderScheduleIndex as Ee,renderHistoryIndex as Re,renderQuickCommandsPage as Oe,renderFilesIndex as Ne,renderShell as De}from"../frontend.js";import{renderFederationPage as Le}from"./pages/federation.js";import{createAgentToken as Pe,removeAgentToken as Ue,listAgentTokens as K}from"./agent-tokens.js";import{hashPassword as Fe,verifyPassword as He,validatePassword as We}from"./auth.js";import{atomicWriteFileSync as Me}from"./atomicWrite.js";import{resolveFsPath as _,resolveFsRoots as X,MAX_FILE_BYTES as Z,walkRecursive as Be}from"./fs-access.js";import{captureSessionWindowsWithPath as Ve}from"./tmux-windows.js";import{audit as q}from"./auditLog.js";import{db as A}from"./db.js";import{getSessionAccessMap as M}from"./session-access.js";import{listWindowHistory as $e,clearWindowHistory as Ge}from"./window-history.js";import{registerExtensionRoutes as ze}from"./ext-loader.js";import{saveSecurityConfig as Qe}from"./security-config.js";import{isValidScheduleInput as Ye,isValidRescheduleInput as Ke}from"./scheduler.js";import{ImageUploadError as Xe,saveUploadedImage as Ze}from"./image-upload.js";import{getScheduleDelayError as J}from"./schedule-delay.js";import{readSettings as ee,writeSettings as Je}from"./settings.js";import{setActiveThemeTemplate as re}from"./theme-store.js";import{isThemeTemplateId as te,THEME_TEMPLATE_IDS as er}from"./themes/index.js";import{installPlugin as rr,uninstallPlugin as tr}from"./plugins.js";import{buildCommandbarSessions as ne}from"./commandbar.js";import{pinView as nr,unpinView as sr,listPinnedViews as or}from"./pinned-views.js";import{listWindowLabels as ir,setWindowLabel as ar}from"./window-labels.js";import{captureAndStoreWindows as se,getStoredWindows as oe}from"./session-windows.js";import{buildSidebarSessions as dr}from"./sessions-sidebar.js";import{createQuickCommand as ur,deleteQuickCommand as cr,listQuickCommands as ie,updateQuickCommand as lr}from"./quick-commands.js";import{getSystemStatus as mr,getTopProcesses as fr,killProcess as pr}from"./system-monitor.js";import{listSessionWindows as hr,selectSessionWindow as gr,newSessionWindow as yr,renameSessionWindow as wr,newTmuxSession as jr,renameSession as kr,killSession as vr,TmuxWindowsError as N}from"./tmux-windows.js";import{registerAgentTunnel as br}from"./agent-tunnel.js";import{injectScopeScript as qr}from"./scope-script.js";const B="tmux-web-token",Sr=365;function Tr(k){return k==="127.0.0.1"||k==="::1"||k==="::ffff:127.0.0.1"}function ft(k){const{mode:ae,securityConfig:g,tokenStore:v,rateLimiter:D,scheduler:x,settings:Ir,commandbarEnabled:m,terminalRenderer:L,scheduleHistoryDays:de,extsDir:ue,extensions:ce,terminalBufferConfig:P,getAgentId:V,state:c,federation:S,hub:y}=k,b=ae==="agent";function O(e){if(g.security.trustProxy){const r=(e.req.header("x-forwarded-for")||"").split(",")[0].trim();if(r)return r}return e.env?.incoming?.socket?.remoteAddress||"unknown"}function U(e){const r=e.req.header("authorization");if(r?.startsWith("Bearer "))return r.slice(7);const t=e.req.header("cookie");if(!t)return null;const n=t.match(new RegExp(`(?:^|;\\s*)${B}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function Cr(e){const r=U(e);return r?v.validateToken(r):null}function $(e,r){const t=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${B}=${encodeURIComponent(r)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${Sr*86400}${t?"; Secure":""}`)}function le(e){e.header("Set-Cookie",`${B}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}const a=()=>b?async(e,r)=>r():async(e,r)=>{const t=U(e);return!t||!v.validateToken(t)?(q("http_unauthorized",{ip:O(e)}),e.json({error:"unauthorized"},401)):r()},w=()=>b?async(e,r)=>r():async(e,r)=>{const t=U(e);if(!t||!v.validateToken(t)){const n=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${n}`,302)}return r()};function T(){const e=ne(R(),M());if(!y)return e;for(const r of y.agents.list()){if(!r.online)continue;const t=y.agents.get(r.agentId);if(t)for(const n of t.sessions)e.push({...n,agentId:r.agentId,agentName:r.name,remote:!0})}return e}function F(e){const r=dr(R(),M(),or()),t=[];if(y)for(const n of y.agents.list()){const o=y.agents.get(n.agentId);t.push({agentId:n.agentId,agentName:n.name,online:n.online,sessions:o?o.sessions:[]})}return{...r,currentSession:e??null,agents:t}}function G(e){const r=typeof e.sessionName=="string"?e.sessionName.trim():"";if(!r)return{error:"sessionName is required"};if(e.windowIndex===void 0)return{sessionName:r};const t=e.windowIndex;return typeof t!="number"||!Number.isInteger(t)||t<0?{error:"windowIndex must be a non-negative integer"}:{sessionName:r,windowIndex:t}}const s=new Te;s.use("*",async(e,r)=>{await r(),e.header("X-Content-Type-Options","nosniff"),e.header("X-Frame-Options","DENY"),e.header("Content-Security-Policy",["default-src 'self'","script-src 'self' 'unsafe-inline'","style-src 'self' 'unsafe-inline'","connect-src 'self' ws: wss: http: https:","img-src 'self' data:","font-src 'self'","worker-src 'self' blob:"].join("; ")),e.header("Referrer-Policy","strict-origin-when-cross-origin")}),s.use("*",async(e,r)=>{const t=e.req.method;if(t==="GET"||t==="HEAD"||t==="OPTIONS")return r();const n=e.req.header("sec-fetch-site");return n&&n!=="same-origin"&&n!=="none"?e.text("cross-site request blocked",403):r()}),b||s.use("*",async(e,r)=>{if(await r(),e.req.method!=="GET"||e.req.path==="/login"||!(e.res.headers.get("content-type")??"").includes("text/html"))return;const n=await e.res.text();e.res=new Response(qr(n,{prefix:"",agentId:null}),e.res)}),ze(s,ue,ce);const me=h.dirname(Se(import.meta.url)),fe=[h.join(me,"assets"),h.join(process.cwd(),"dist","assets")];s.get("/assets/:file",async e=>{const r=e.req.param("file");if(!/^[a-zA-Z0-9._-]+$/.test(r))return e.notFound();for(const t of fe){const n=h.join(t,r);if(!W(n))continue;const o=await qe(n),i=h.extname(r),u={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(o,200,{"Content-Type":u[i]??"application/octet-stream","Cache-Control":i===".js"||i===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const pe=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
1
+ import{existsSync as W,readdirSync as je,statSync as C,unlinkSync as ke,writeFileSync as ve,mkdirSync as be,readFileSync as Y}from"node:fs";import{readFile as qe}from"node:fs/promises";import{execSync as E,execFileSync as K}from"node:child_process";import h from"node:path";import{fileURLToPath as Se}from"node:url";import{Hono as Te}from"hono";import{listSessions as R}from"../sessions.js";import{renderLoginPage as Ae,renderNotesIndex as xe,renderNotesPage as Ie,renderSettings as Ce,renderThemeSettings as _e,renderScheduleIndex as Ee,renderHistoryIndex as Re,renderQuickCommandsPage as Oe,renderFilesIndex as Ne,renderShell as De}from"../frontend.js";import{renderFederationPage as Le}from"./pages/federation.js";import{createAgentToken as Pe,removeAgentToken as Ue,listAgentTokens as X}from"./agent-tokens.js";import{hashPassword as Fe,verifyPassword as He,validatePassword as We}from"./auth.js";import{atomicWriteFileSync as Me}from"./atomicWrite.js";import{resolveFsPath as _,resolveFsRoots as Z,MAX_FILE_BYTES as J,walkRecursive as Be}from"./fs-access.js";import{captureSessionWindowsWithPath as Ve}from"./tmux-windows.js";import{audit as q}from"./auditLog.js";import{db as A}from"./db.js";import{getSessionAccessMap as M}from"./session-access.js";import{listWindowHistory as $e,clearWindowHistory as Ge}from"./window-history.js";import{registerExtensionRoutes as ze}from"./ext-loader.js";import{saveSecurityConfig as Qe}from"./security-config.js";import{isValidScheduleInput as Ye,isValidRescheduleInput as Ke}from"./scheduler.js";import{ImageUploadError as Xe,saveUploadedImage as Ze}from"./image-upload.js";import{getScheduleDelayError as ee}from"./schedule-delay.js";import{readSettings as te,writeSettings as Je}from"./settings.js";import{setActiveThemeTemplate as re}from"./theme-store.js";import{isThemeTemplateId as ne,THEME_TEMPLATE_IDS as et}from"./themes/index.js";import{installPlugin as tt,uninstallPlugin as rt}from"./plugins.js";import{buildCommandbarSessions as se}from"./commandbar.js";import{pinView as nt,unpinView as st,listPinnedViews as ot}from"./pinned-views.js";import{listWindowLabels as it,setWindowLabel as at}from"./window-labels.js";import{captureAndStoreWindows as oe,getStoredWindows as ie}from"./session-windows.js";import{buildSidebarSessions as dt}from"./sessions-sidebar.js";import{createQuickCommand as ut,deleteQuickCommand as ct,listQuickCommands as ae,updateQuickCommand as lt}from"./quick-commands.js";import{getSystemStatus as mt,getTopProcesses as ft,killProcess as pt}from"./system-monitor.js";import{listSessionWindows as ht,selectSessionWindow as gt,newSessionWindow as yt,renameSessionWindow as wt,newTmuxSession as jt,renameSession as kt,killSession as vt,TmuxWindowsError as N}from"./tmux-windows.js";import{registerAgentTunnel as bt}from"./agent-tunnel.js";import{injectScopeScript as qt}from"./scope-script.js";const B="tmux-web-token",St=365;function Tt(k){return k==="127.0.0.1"||k==="::1"||k==="::ffff:127.0.0.1"}function fr(k){const{mode:de,securityConfig:g,tokenStore:v,rateLimiter:D,scheduler:x,settings:It,commandbarEnabled:m,terminalRenderer:L,scheduleHistoryDays:ue,extsDir:ce,extensions:le,terminalBufferConfig:P,getAgentId:V,state:c,federation:S,hub:y}=k,b=de==="agent";function O(e){if(g.security.trustProxy){const t=(e.req.header("x-forwarded-for")||"").split(",")[0].trim();if(t)return t}return e.env?.incoming?.socket?.remoteAddress||"unknown"}function U(e){const t=e.req.header("authorization");if(t?.startsWith("Bearer "))return t.slice(7);const r=e.req.header("cookie");if(!r)return null;const n=r.match(new RegExp(`(?:^|;\\s*)${B}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function Ct(e){const t=U(e);return t?v.validateToken(t):null}function $(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${B}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${St*86400}${r?"; Secure":""}`)}function me(e){e.header("Set-Cookie",`${B}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}const a=()=>b?async(e,t)=>t():async(e,t)=>{const r=U(e);return!r||!v.validateToken(r)?(q("http_unauthorized",{ip:O(e)}),e.json({error:"unauthorized"},401)):t()},w=()=>b?async(e,t)=>t():async(e,t)=>{const r=U(e);if(!r||!v.validateToken(r)){const n=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${n}`,302)}return t()};function T(){const e=se(R(),M());if(!y)return e;for(const t of y.agents.list()){if(!t.online)continue;const r=y.agents.get(t.agentId);if(r)for(const n of r.sessions)e.push({...n,agentId:t.agentId,agentName:t.name,remote:!0})}return e}function F(e){const t=dt(R(),M(),ot()),r=[];if(y)for(const n of y.agents.list()){const o=y.agents.get(n.agentId);r.push({agentId:n.agentId,agentName:n.name,online:n.online,sessions:o?o.sessions:[]})}return{...t,currentSession:e??null,agents:r}}function G(e){const t=typeof e.sessionName=="string"?e.sessionName.trim():"";if(!t)return{error:"sessionName is required"};if(e.windowIndex===void 0)return{sessionName:t};const r=e.windowIndex;return typeof r!="number"||!Number.isInteger(r)||r<0?{error:"windowIndex must be a non-negative integer"}:{sessionName:t,windowIndex:r}}const s=new Te;s.use("*",async(e,t)=>{await t(),e.header("X-Content-Type-Options","nosniff"),e.header("X-Frame-Options","DENY"),e.header("Content-Security-Policy",["default-src 'self'","script-src 'self' 'unsafe-inline'","style-src 'self' 'unsafe-inline' https://fonts.googleapis.com","connect-src 'self' ws: wss: http: https:","img-src 'self' data:","font-src 'self' https://fonts.gstatic.com","worker-src 'self' blob:"].join("; ")),e.header("Referrer-Policy","strict-origin-when-cross-origin")}),s.use("*",async(e,t)=>{const r=e.req.method;if(r==="GET"||r==="HEAD"||r==="OPTIONS")return t();const n=e.req.header("sec-fetch-site");return n&&n!=="same-origin"&&n!=="none"?e.text("cross-site request blocked",403):t()}),b||s.use("*",async(e,t)=>{if(await t(),e.req.method!=="GET"||e.req.path==="/login"||!(e.res.headers.get("content-type")??"").includes("text/html"))return;const n=await e.res.text();e.res=new Response(qt(n,{prefix:"",agentId:null}),e.res)}),ze(s,ce,le);const z=h.dirname(Se(import.meta.url)),fe=[h.join(z,"..","assets"),h.join(z,"assets"),h.join(process.cwd(),"dist","assets")];s.get("/assets/:file",async e=>{const t=e.req.param("file");if(!/^[a-zA-Z0-9._-]+$/.test(t))return e.notFound();for(const r of fe){const n=h.join(r,t);if(!W(n))continue;const o=await qe(n),i=h.extname(t),u={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(o,200,{"Content-Type":u[i]??"application/octet-stream","Cache-Control":i===".js"||i===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const pe=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
2
  <rect x="2.5" y="5.5" width="27" height="21" rx="4" fill="#0d1117" stroke="#7dd3fc" stroke-width="2"/>
3
3
  <path d="M8 13l4.2 3.1L8 19.2" fill="none" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
4
4
  <line x1="15.5" y1="19.6" x2="22.5" y2="19.6" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round"/>
5
- </svg>`,z=e=>e.body(pe,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});s.get("/favicon.svg",z),s.get("/favicon.ico",z);const he={name:"tmux-weblink",short_name:"tmux-web",description:"Access your tmux sessions from the browser",start_url:"/",display:"standalone",background_color:"#0d1117",theme_color:"#0d1117",icons:[{src:"/assets/icon-192.png",sizes:"192x192",type:"image/png"},{src:"/assets/icon-512.png",sizes:"512x512",type:"image/png"}]};s.get("/manifest.json",e=>e.json(he,200,{"Cache-Control":"public, max-age=3600"}));const ge=`// tmux-weblink Service Worker
5
+ </svg>`,Q=e=>e.body(pe,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});s.get("/favicon.svg",Q),s.get("/favicon.ico",Q);const he={name:"tmux-weblink",short_name:"tmux-web",description:"Access your tmux sessions from the browser",start_url:"/",display:"standalone",background_color:"#0d1117",theme_color:"#0d1117",icons:[{src:"/assets/icon-192.png",sizes:"192x192",type:"image/png"},{src:"/assets/icon-512.png",sizes:"512x512",type:"image/png"}]};s.get("/manifest.json",e=>e.json(he,200,{"Cache-Control":"public, max-age=3600"}));const ge=`// tmux-weblink Service Worker
6
6
  const CACHE = "tmux-weblink-v1";
7
7
  const ASSETS = ["/", "/favicon.svg"];
8
8
  self.addEventListener("install", (e) => {
@@ -13,7 +13,7 @@ self.addEventListener("activate", (e) => { e.waitUntil(clients.claim()); });
13
13
  self.addEventListener("fetch", (e) => {
14
14
  e.respondWith(fetch(e.request).catch(() => caches.match(e.request)));
15
15
  });
16
- `;s.get("/sw.js",e=>e.body(ge,200,{"Content-Type":"application/javascript; charset=utf-8"})),s.get("/login",e=>{const t=e.req.query("setup")==="1"?!0:!g.passwordHash,n=e.req.query("error");return e.html(Ae({setupMode:t,error:n?decodeURIComponent(n):void 0,theme:c.activeTheme}))}),s.get("/",w(),e=>{const r=m?T():[],t=X();return e.html(De({theme:c.activeTheme,commandbarEnabled:m,commandbarSessions:r,fsRoots:t,terminalCfg:P,renderer:L,scrollback:P.initialLines+2*P.historyChunk,wsBase:b&&V?.()?`/ws/a/${V()}`:void 0}))}),s.get("/notes",w(),e=>{const r=m?T():[];return e.html(xe(A.data.notes,c.activeTheme,m,r))}),s.get("/notes/:session",w(),e=>{const r=decodeURIComponent(e.req.param("session")),t=m?T():[];return e.html(Ie(r,c.activeTheme,m,t))}),s.get("/schedule",w(),e=>{const r=m?T():[];return e.html(Ee(x.list(),x.listTriggered(),c.activeTheme,de,m,r))}),s.get("/history",w(),e=>{const r=R(),t=m?ne(r,M()):[],n=new Set(r.map(o=>o.name));return e.html(Re($e(),c.activeTheme,m,t,n))}),s.get("/quick-commands",w(),e=>{const r=m?T():[];return e.html(Oe(ie(),c.activeTheme,m,r))}),s.get("/files",w(),e=>{const r=m?T():[],t=X();return e.html(Ne(c.activeTheme,m,r,t))}),s.post("/api/history/clear",a(),async e=>(await Ge(),e.json({ok:!0}))),s.get("/api/quick-commands",a(),e=>e.json(ie())),s.post("/api/quick-commands",a(),async e=>{let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const t=await ur(r);return"error"in t?e.json({error:t.error},400):e.json(t,201)}),s.patch("/api/quick-commands/:id",a(),async e=>{let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const t=await lr(e.req.param("id"),r);return"error"in t?e.json({error:t.error},t.status):e.json(t)}),s.delete("/api/quick-commands/:id",a(),e=>cr(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{if(b)return e.json({error:"hub only"},403);const r=O(e),t=D.check(r);if(!t.allowed)return q("rate_limited",{ip:r,retryAfterMs:t.retryAfterMs,permanentLock:t.permanentLock}),t.permanentLock?e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403):e.json({error:"Too many attempts",retryAfterMs:t.retryAfterMs},429);let n;try{n=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const o=typeof n.password=="string"?n.password:"";if(!g.passwordHash){if(!g.security.allowRemoteSetup&&!Tr(r))return q("setup_rejected_remote",{ip:r}),e.json({error:"First-run setup must be performed from localhost"},403);if(c.settingUpPassword)return e.json({error:"Password setup in progress"},409);const l=We(o);if(l)return e.json({error:l},400);c.settingUpPassword=!0;try{g.passwordHash=await Fe(o),Qe(g),q("password_set",{ip:r})}finally{c.settingUpPassword=!1}const{plaintext:d}=v.createAccessToken("setup",g.security.tokenTtlDays);return $(e,d),e.json({ok:!0,token:d,setupMode:!0})}if(!await He(o,g.passwordHash)){const l=D.recordFailure(r);return q("auth_failed",{ip:r,method:"password",failures:l.failures,permanentLock:l.permanentLock}),l.permanentLock?(v.revokeAll(),q("permanent_lock",{ip:r,failures:l.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):l.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:l.retryAfterMs},429)}D.recordSuccess(r);const u=`browser-${r}`,{plaintext:j}=v.createAccessToken(u,g.security.tokenTtlDays);return $(e,j),q("auth_success",{ip:r,method:"password",tokenName:u}),e.json({ok:!0,token:j})}),s.post("/api/auth/token",a(),async e=>{if(b)return e.json({error:"hub only"},403);const r=O(e);let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const n=typeof t.name=="string"?t.name:`api-${r}`,o=typeof t.ttlDays=="number"?t.ttlDays:g.security.tokenTtlDays,{stored:i,plaintext:u}=v.createAccessToken(n,o);return q("token_created",{ip:r,name:i.name,tokenId:i.id}),e.json({id:i.id,name:i.name,token:u,expiresAt:i.expiresAt})}),s.get("/api/auth/tokens",a(),e=>b?e.json({error:"hub only"},403):e.json(v.list().map(r=>({id:r.id,name:r.name,createdAt:r.createdAt,lastUsedAt:r.lastUsedAt,expiresAt:r.expiresAt})))),s.delete("/api/auth/tokens/:id",a(),e=>b?e.json({error:"hub only"},403):v.revoke(e.req.param("id"))?(q("token_revoked",{ip:O(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",a(),e=>(le(e),e.json({ok:!0}))),s.get("/settings",w(),async e=>{const r=await ee(),t=r.terminalRenderer??"xterm";return e.html(Ce({settings:r,renderer:L,rendererOverridden:L!==t,theme:c.activeTheme,plugins:r.plugins??[],saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",a(),async e=>{let r;try{r=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const t=await ee(),n=r.terminalRenderer==="ghostty"?"ghostty":"xterm",o=r.defaultView==="recent"?"recent":"default",i=xr(typeof r.scheduleHistoryDays=="string"?Number(r.scheduleHistoryDays):void 0);return await Je({...t,commandbar:r.commandbar!==void 0,terminalRenderer:n,defaultView:o,scheduleHistoryDays:i}),e.redirect("/settings?saved=1",303)}),s.post("/settings/plugins",a(),async e=>{if(b)return e.json({error:"plugin management is hub-only"},403);let r;try{r=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const t=r.action,n=typeof r.pkg=="string"?r.pkg.trim():"";if(!n)return e.redirect("/settings?error="+encodeURIComponent("missing package name"),303);const o=t==="remove"?await tr(n):t==="add"?await rr(n):{ok:!1,output:"unknown action"};return o.ok?e.redirect("/settings?saved=1",303):e.redirect("/settings?error="+encodeURIComponent(o.output.slice(0,800)),303)}),s.get("/settings/theme",w(),e=>e.html(_e({theme:c.activeTheme,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",a(),async e=>{let r;try{r=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const t=r.template;return typeof t!="string"||!te(t)?e.redirect("/settings/theme",303):(c.activeTheme=await re(t),e.redirect("/settings/theme?saved=1",303))});const ye={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",a(),e=>e.json({active:c.activeTheme.template,templates:er.map(r=>({id:r,name:ye[r]}))})),s.post("/api/theme",a(),async e=>{let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const t=r.template;return typeof t!="string"||!te(t)?e.json({error:"invalid theme template"},400):(c.activeTheme=await re(t),e.json({ok:!0,active:c.activeTheme.template}))}),s.get("/api/system/status",a(),e=>e.json(mr())),s.get("/api/system/processes",a(),e=>e.json(fr())),S&&(s.get("/settings/federation",w(),async e=>e.html(Le({config:S.getConfig(),status:S.status(),tokens:K().map(r=>({id:r.id,name:r.name,createdAt:r.createdAt})),hostname:S.hostname},c.activeTheme))),s.get("/api/federation",a(),e=>e.json({config:S.getConfig(),status:S.status()})),s.post("/api/federation",a(),async e=>{let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const t=typeof r.hub=="string"?r.hub.trim():"",n=typeof r.token=="string"?r.token.trim():"",o=typeof r.name=="string"?r.name.trim().slice(0,64):"",i=r.enabled===!0;if(i){if(!t)return e.json({error:"hub URL is required"},400);if(!/^wss?:\/\//i.test(t))return e.json({error:"hub URL must start with ws:// or wss://"},400);if(!n)return e.json({error:"registration token is required"},400)}const u={hub:t||void 0,token:n||void 0,name:o||void 0,enabled:i};return await S.setConfig(u),e.json({ok:!0,status:S.status()})}),s.get("/api/agent-tokens",a(),e=>e.json(K().map(r=>({id:r.id,name:r.name,createdAt:r.createdAt})))),s.post("/api/agent-tokens",a(),async e=>{let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const t=typeof r.name=="string"?r.name.trim().slice(0,64):"";if(!t)return e.json({error:"name is required"},400);const{id:n,token:o}=Pe(t);return e.json({ok:!0,id:n,name:t,token:o})}),s.delete("/api/agent-tokens/:id",a(),e=>{const r=e.req.param("id");return Ue(r)?e.json({ok:!0}):e.json({error:"no such token"},404)})),s.post("/api/system/kill",a(),async e=>{const{pid:r}=await e.req.json();if(typeof r!="number"||!Number.isInteger(r)||r<=0)return e.json({error:"invalid pid"},400);const t=pr(r);return t.ok?e.json({ok:!0}):e.json({error:t.error},500)}),s.get("/api/agents",a(),e=>y?e.json(y.agents.list()):e.json([])),s.get("/api/sessions",a(),e=>m?e.json(T()):e.json({error:"commandbar disabled"},404)),s.post("/api/sessions/new",a(),async e=>{let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const t=typeof r.name=="string"?r.name.trim():"";if(!t)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(t))return e.json({error:"name contains invalid characters"},400);const n=typeof r.dir=="string"&&r.dir.trim()?r.dir.trim():void 0;if(R().some(i=>i.name===t))return e.json({error:"session already exists"},409);try{return jr(t,n),e.json({ok:!0})}catch(i){const u=i instanceof N?i.message:"failed to create session";return e.json({error:u},500)}}),s.post("/api/sessions/rename",a(),async e=>{try{const{oldName:r,newName:t}=await e.req.json();return!r||!t?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(t)?(kr(r,t),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),s.post("/api/sessions/kill",a(),async e=>{try{const{name:r}=await e.req.json();return r?(vr(r),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.get("/api/fs/session-path",a(),e=>{const r=e.req.query("session");if(!r)return e.json({error:"session is required"},400);try{const t=Ve(r),o=t.find(i=>i.active)?.path??t[0]?.path??process.env.HOME??"/";return e.json({path:o})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",a(),e=>{const r=process.env.HOME??"/";let t=e.req.query("path")??r;t.startsWith("~")&&(t=r+t.slice(1)),t.startsWith("/")||(t=h.join(r,t));const n=e.req.query("recursive")==="true";let o=t,i="",u=t.endsWith("/");if(!u)try{u=C(t).isDirectory()}catch{}u||(o=h.dirname(t),i=h.basename(t).toLowerCase());try{const j=je(o),l=[],d=[];for(const f of j)if(!f.startsWith(".")&&!(i&&!f.toLowerCase().startsWith(i))){try{const p=h.join(o,f);C(p).isDirectory()?(l.push(p),n&&Be(p,l,d,0)):d.push(p)}catch{}if(l.length+d.length>=5e3)break}return e.json({dirs:l,files:d})}catch{return e.json({dirs:[],files:[]})}});function we(e){const r={repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0};try{const t=E("git rev-parse --show-toplevel 2>/dev/null",{cwd:e,encoding:"utf-8",timeout:3e3}).trim();if(!t)return r;let n;try{n=E("git branch --show-current 2>/dev/null",{cwd:t,encoding:"utf-8",timeout:3e3}).trim()}catch{n=E("git rev-parse --short HEAD 2>/dev/null",{cwd:t,encoding:"utf-8",timeout:3e3}).trim()}n||(n="HEAD");let o="";try{o=E("git status --porcelain 2>/dev/null",{cwd:t,encoding:"utf-8",timeout:3e3})}catch{}const i=[];for(const d of o.split(`
17
- `)){if(!d.trim())continue;const f=d[0],p=d[1],I=d.substring(3).trim(),H=p!==" "?p:f;i.push({path:I,status:H,staged:f,unstaged:p,additions:0,deletions:0})}const u=new Map;try{const d=E("git diff --numstat HEAD 2>/dev/null",{cwd:t,encoding:"utf-8",timeout:3e3});for(const f of d.split(`
18
- `)){if(!f.trim())continue;const p=f.split(" ");if(p.length>=3){const I=parseInt(p[0],10)||0,H=parseInt(p[1],10)||0;u.set(p[2],{added:I,deleted:H})}}}catch{}for(const d of i){const f=u.get(d.path);f&&(d.additions=f.added,d.deletions=f.deleted)}let j=0,l=0;for(const d of i)j+=d.additions,l+=d.deletions;for(const d of i)if(d.status==="?"&&d.additions===0)try{const f=h.join(t,d.path),I=Q(f,"utf-8").split(`
19
- `).length;d.additions=I,j+=I}catch{}return{repoRoot:t,branch:n,files:i,linesAdded:j,linesRemoved:l}}catch{return r}}return s.get("/api/git/status",a(),e=>{const r=e.req.query("path");if(!r)return e.json({error:"path is required"},400);try{const t=_(r),n=we(t);return e.json(n)}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0})}}),s.get("/api/git/diff",a(),e=>{const r=e.req.query("path"),t=e.req.query("file");if(!r||!t)return e.json({error:"path and file are required"},400);try{const n=_(r);if(!W(n)||!C(n).isDirectory())return e.json({error:"invalid repository path: "+n},400);try{const o=Y("/usr/bin/git",["diff","HEAD","--",t],{cwd:n,encoding:"utf-8",timeout:5e3}),i=Y("/usr/bin/git",["diff","--cached","--",t],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:o,stagedDiff:i})}catch(o){throw console.error("[git/diff] execFileSync failed:",o?.code,o.message),o}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),s.get("/api/file",a(),e=>{try{const r=e.req.query("path");if(!r)return e.json({error:"path is required"},400);const t=_(r);if(!C(t).isFile())return e.json({error:"not a file"},400);const n=C(t).size;if(n>Z)return e.json({error:"file too large",size:n,maxBytes:Z},413);const o=Q(t);return e.json({path:t,content:o,size:n})}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):r.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",a(),async e=>{try{const r=await e.req.json();if(!r.path||typeof r.content!="string")return e.json({error:"path and content are required"},400);const t=_(r.path);return Me(t,r.content),e.json({ok:!0})}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),s.post("/api/file/delete",a(),async e=>{try{const r=await e.req.json();if(!r.path)return e.json({error:"path is required"},400);const t=_(r.path);return C(t).isFile()?(ke(t),e.json({ok:!0})):e.json({error:"not a file"},400)}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"delete failed"},500)}}),s.post("/api/file/touch",a(),async e=>{try{const r=await e.req.json();if(!r.path)return e.json({error:"path is required"},400);const t=_(r.path);return W(t)?e.json({error:"file already exists"},409):(be(h.dirname(t),{recursive:!0}),ve(t,"","utf-8"),e.json({ok:!0,path:t}))}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"touch failed"},500)}}),s.get("/api/sidebar/sessions",a(),e=>{const r=e.req.query("currentSession");return e.json(F(typeof r=="string"&&r?r:void 0))}),s.get("/api/sidebar/session-windows/:session",a(),e=>{const r=decodeURIComponent(e.req.param("session"));return e.json(oe(r))}),s.post("/api/pinned-views",a(),async e=>{let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const t=G(r);return"error"in t?e.json({error:t.error},400):(await nr(t.sessionName,t.windowIndex),e.json(F()))}),s.delete("/api/pinned-views",a(),async e=>{let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const t=G(r);return"error"in t?e.json({error:t.error},400):(await sr(t.sessionName,t.windowIndex),e.json(F()))}),s.get("/api/notes",a(),e=>{const r=[...A.data.notes].sort((t,n)=>(n.updatedAt??0)-(t.updatedAt??0));return e.json(r)}),s.get("/api/notes/:scope",a(),e=>{const r=decodeURIComponent(e.req.param("scope")),t=A.data.notes.find(n=>n.scope===r);return t?e.json(t):e.json(null,404)}),s.put("/api/notes/:scope",a(),async e=>{const r=decodeURIComponent(e.req.param("scope"));let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}if(typeof t.content!="string")return e.json({error:"content must be string"},400);const n={scope:r,content:t.content,updatedAt:Date.now()},o=A.data.notes.findIndex(i=>i.scope===r);return o>=0?A.data.notes[o]=n:A.data.notes.push(n),await A.write(),e.json({ok:!0})}),s.post("/api/session/:session/upload",a(),async e=>{const r=decodeURIComponent(e.req.param("session"));if(!R().some(i=>i.name===r))return e.json({error:"session not found"},404);let n;try{n=await e.req.parseBody()}catch{return e.json({error:"invalid multipart body"},400)}const o=n.file;if(!(o instanceof File))return e.json({error:"missing file field"},400);try{const i=await o.arrayBuffer(),u=Buffer.from(i),{path:j}=await Ze(u,o.type||void 0,o.name||void 0);return e.json({path:j})}catch(i){return i instanceof Xe?e.json({error:i.message},i.status):(console.error("[upload]",i),e.json({error:"upload failed"},500))}}),s.get("/api/session/:session/windows",a(),e=>{const r=decodeURIComponent(e.req.param("session")),t=new Map(ir(r).map(i=>[i.windowIndex,i.label])),n=new Map(oe(r).map(i=>[i.index,i])),o=hr(r).map(i=>({...i,label:t.get(i.index)??null,worktree:n.get(i.index)?.worktree??!1}));return e.json(o)}),s.post("/api/session/:session/window-label",a(),async e=>{const r=decodeURIComponent(e.req.param("session"));let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=t;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);const o=typeof t.label=="string"?t.label:"",i=await ar(r,n,o);return e.json(i)}),s.post("/api/session/:session/select-window",a(),async e=>{const r=decodeURIComponent(e.req.param("session"));let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=t;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);try{return gr(r,n),e.json({ok:!0})}catch(o){return o instanceof N?e.json({error:o.message},o.status):(console.error("[select-window]",o),e.json({error:"select-window failed"},500))}}),s.post("/api/session/:session/rename-window",a(),async e=>{const r=decodeURIComponent(e.req.param("session"));let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=t;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);if(typeof t.name!="string"||!t.name.trim())return e.json({error:"name is required"},400);try{return wr(r,n,t.name),se(r),e.json({ok:!0})}catch(o){return o instanceof N?e.json({error:o.message},o.status):(console.error("[rename-window]",o),e.json({error:"rename-window failed"},500))}}),s.post("/api/session/:session/new-window",a(),e=>{const r=decodeURIComponent(e.req.param("session"));try{return yr(r),se(r),e.json({ok:!0})}catch(t){return t instanceof N?e.json({error:t.message},t.status):(console.error("[new-window]",t),e.json({error:"new-window failed"},500))}}),s.get("/api/schedule",a(),e=>e.json(x.list(e.req.query("session")))),s.post("/api/schedule",a(),async e=>{let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const t=J(r);if(t)return e.json({error:t},400);if(!Ye(r))return e.json({error:"invalid body"},400);const n=await x.create(r);return e.json({id:n.id,fireAt:n.fireAt})}),s.delete("/api/schedule/:id",a(),async e=>await x.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.patch("/api/schedule/:id",a(),async e=>{let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const t=J(r);if(t)return e.json({error:t},400);if(!Ke(r))return e.json({error:"invalid body"},400);const n=await x.reschedule(e.req.param("id"),r.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)}),y&&(s.use("/a/:agentId/*",a()),br(s,{getChannel:y.getChannel})),s}const Ar=7;function xr(k){return typeof k!="number"||!Number.isFinite(k)?Ar:Math.min(365,Math.max(1,Math.round(k)))}export{ft as buildApp};
16
+ `;s.get("/sw.js",e=>e.body(ge,200,{"Content-Type":"application/javascript; charset=utf-8"})),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!g.passwordHash,n=e.req.query("error");return e.html(Ae({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:c.activeTheme}))}),s.get("/",w(),e=>{const t=m?T():[],r=Z();return e.html(De({theme:c.activeTheme,commandbarEnabled:m,commandbarSessions:t,fsRoots:r,terminalCfg:P,renderer:L,scrollback:P.initialLines+2*P.historyChunk,wsBase:b&&V?.()?`/ws/a/${V()}`:void 0}))}),s.get("/notes",w(),e=>{const t=m?T():[];return e.html(xe(A.data.notes,c.activeTheme,m,t))}),s.get("/notes/:session",w(),e=>{const t=decodeURIComponent(e.req.param("session")),r=m?T():[];return e.html(Ie(t,c.activeTheme,m,r))}),s.get("/schedule",w(),e=>{const t=m?T():[];return e.html(Ee(x.list(),x.listTriggered(),c.activeTheme,ue,m,t))}),s.get("/history",w(),e=>{const t=R(),r=m?se(t,M()):[],n=new Set(t.map(o=>o.name));return e.html(Re($e(),c.activeTheme,m,r,n))}),s.get("/quick-commands",w(),e=>{const t=m?T():[];return e.html(Oe(ae(),c.activeTheme,m,t))}),s.get("/files",w(),e=>{const t=m?T():[],r=Z();return e.html(Ne(c.activeTheme,m,t,r))}),s.post("/api/history/clear",a(),async e=>(await Ge(),e.json({ok:!0}))),s.get("/api/quick-commands",a(),e=>e.json(ae())),s.post("/api/quick-commands",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await ut(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await lt(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),s.delete("/api/quick-commands/:id",a(),e=>ct(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{if(b)return e.json({error:"hub only"},403);const t=O(e),r=D.check(t);if(!r.allowed)return q("rate_limited",{ip:t,retryAfterMs:r.retryAfterMs,permanentLock:r.permanentLock}),r.permanentLock?e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403):e.json({error:"Too many attempts",retryAfterMs:r.retryAfterMs},429);let n;try{n=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const o=typeof n.password=="string"?n.password:"";if(!g.passwordHash){if(!g.security.allowRemoteSetup&&!Tt(t))return q("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(c.settingUpPassword)return e.json({error:"Password setup in progress"},409);const l=We(o);if(l)return e.json({error:l},400);c.settingUpPassword=!0;try{g.passwordHash=await Fe(o),Qe(g),q("password_set",{ip:t})}finally{c.settingUpPassword=!1}const{plaintext:d}=v.createAccessToken("setup",g.security.tokenTtlDays);return $(e,d),e.json({ok:!0,token:d,setupMode:!0})}if(!await He(o,g.passwordHash)){const l=D.recordFailure(t);return q("auth_failed",{ip:t,method:"password",failures:l.failures,permanentLock:l.permanentLock}),l.permanentLock?(v.revokeAll(),q("permanent_lock",{ip:t,failures:l.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):l.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:l.retryAfterMs},429)}D.recordSuccess(t);const u=`browser-${t}`,{plaintext:j}=v.createAccessToken(u,g.security.tokenTtlDays);return $(e,j),q("auth_success",{ip:t,method:"password",tokenName:u}),e.json({ok:!0,token:j})}),s.post("/api/auth/token",a(),async e=>{if(b)return e.json({error:"hub only"},403);const t=O(e);let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const n=typeof r.name=="string"?r.name:`api-${t}`,o=typeof r.ttlDays=="number"?r.ttlDays:g.security.tokenTtlDays,{stored:i,plaintext:u}=v.createAccessToken(n,o);return q("token_created",{ip:t,name:i.name,tokenId:i.id}),e.json({id:i.id,name:i.name,token:u,expiresAt:i.expiresAt})}),s.get("/api/auth/tokens",a(),e=>b?e.json({error:"hub only"},403):e.json(v.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",a(),e=>b?e.json({error:"hub only"},403):v.revoke(e.req.param("id"))?(q("token_revoked",{ip:O(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",a(),e=>(me(e),e.json({ok:!0}))),s.get("/settings",w(),async e=>{const t=await te(),r=t.terminalRenderer??"xterm";return e.html(Ce({settings:t,renderer:L,rendererOverridden:L!==r,theme:c.activeTheme,plugins:t.plugins??[],saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await te(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",o=t.defaultView==="recent"?"recent":"default",i=xt(typeof t.scheduleHistoryDays=="string"?Number(t.scheduleHistoryDays):void 0);return await Je({...r,commandbar:t.commandbar!==void 0,terminalRenderer:n,defaultView:o,scheduleHistoryDays:i}),e.redirect("/settings?saved=1",303)}),s.post("/settings/plugins",a(),async e=>{if(b)return e.json({error:"plugin management is hub-only"},403);let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=t.action,n=typeof t.pkg=="string"?t.pkg.trim():"";if(!n)return e.redirect("/settings?error="+encodeURIComponent("missing package name"),303);const o=r==="remove"?await rt(n):r==="add"?await tt(n):{ok:!1,output:"unknown action"};return o.ok?e.redirect("/settings?saved=1",303):e.redirect("/settings?error="+encodeURIComponent(o.output.slice(0,800)),303)}),s.get("/settings/theme",w(),e=>e.html(_e({theme:c.activeTheme,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const r=t.template;return typeof r!="string"||!ne(r)?e.redirect("/settings/theme",303):(c.activeTheme=await re(r),e.redirect("/settings/theme?saved=1",303))});const ye={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",a(),e=>e.json({active:c.activeTheme.template,templates:et.map(t=>({id:t,name:ye[t]}))})),s.post("/api/theme",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=t.template;return typeof r!="string"||!ne(r)?e.json({error:"invalid theme template"},400):(c.activeTheme=await re(r),e.json({ok:!0,active:c.activeTheme.template}))}),s.get("/api/system/status",a(),e=>e.json(mt())),s.get("/api/system/processes",a(),e=>e.json(ft())),S&&(s.get("/settings/federation",w(),async e=>e.html(Le({config:S.getConfig(),status:S.status(),tokens:X().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})),hostname:S.hostname},c.activeTheme))),s.get("/api/federation",a(),e=>e.json({config:S.getConfig(),status:S.status()})),s.post("/api/federation",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.hub=="string"?t.hub.trim():"",n=typeof t.token=="string"?t.token.trim():"",o=typeof t.name=="string"?t.name.trim().slice(0,64):"",i=t.enabled===!0;if(i){if(!r)return e.json({error:"hub URL is required"},400);if(!/^wss?:\/\//i.test(r))return e.json({error:"hub URL must start with ws:// or wss://"},400);if(!n)return e.json({error:"registration token is required"},400)}const u={hub:r||void 0,token:n||void 0,name:o||void 0,enabled:i};return await S.setConfig(u),e.json({ok:!0,status:S.status()})}),s.get("/api/agent-tokens",a(),e=>e.json(X().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})))),s.post("/api/agent-tokens",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim().slice(0,64):"";if(!r)return e.json({error:"name is required"},400);const{id:n,token:o}=Pe(r);return e.json({ok:!0,id:n,name:r,token:o})}),s.delete("/api/agent-tokens/:id",a(),e=>{const t=e.req.param("id");return Ue(t)?e.json({ok:!0}):e.json({error:"no such token"},404)})),s.post("/api/system/kill",a(),async e=>{const{pid:t}=await e.req.json();if(typeof t!="number"||!Number.isInteger(t)||t<=0)return e.json({error:"invalid pid"},400);const r=pt(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),s.get("/api/agents",a(),e=>y?e.json(y.agents.list()):e.json([])),s.get("/api/sessions",a(),e=>m?e.json(T()):e.json({error:"commandbar disabled"},404)),s.post("/api/sessions/new",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(r))return e.json({error:"name contains invalid characters"},400);const n=typeof t.dir=="string"&&t.dir.trim()?t.dir.trim():void 0;if(R().some(i=>i.name===r))return e.json({error:"session already exists"},409);try{return jt(r,n),e.json({ok:!0})}catch(i){const u=i instanceof N?i.message:"failed to create session";return e.json({error:u},500)}}),s.post("/api/sessions/rename",a(),async e=>{try{const{oldName:t,newName:r}=await e.req.json();return!t||!r?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(r)?(kt(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),s.post("/api/sessions/kill",a(),async e=>{try{const{name:t}=await e.req.json();return t?(vt(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.get("/api/fs/session-path",a(),e=>{const t=e.req.query("session");if(!t)return e.json({error:"session is required"},400);try{const r=Ve(t),o=r.find(i=>i.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:o})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",a(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=h.join(t,r));const n=e.req.query("recursive")==="true";let o=r,i="",u=r.endsWith("/");if(!u)try{u=C(r).isDirectory()}catch{}u||(o=h.dirname(r),i=h.basename(r).toLowerCase());try{const j=je(o),l=[],d=[];for(const f of j)if(!f.startsWith(".")&&!(i&&!f.toLowerCase().startsWith(i))){try{const p=h.join(o,f);C(p).isDirectory()?(l.push(p),n&&Be(p,l,d,0)):d.push(p)}catch{}if(l.length+d.length>=5e3)break}return e.json({dirs:l,files:d})}catch{return e.json({dirs:[],files:[]})}});function we(e){const t={repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0};try{const r=E("git rev-parse --show-toplevel 2>/dev/null",{cwd:e,encoding:"utf-8",timeout:3e3}).trim();if(!r)return t;let n;try{n=E("git branch --show-current 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}catch{n=E("git rev-parse --short HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}n||(n="HEAD");let o="";try{o=E("git status --porcelain 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3})}catch{}const i=[];for(const d of o.split(`
17
+ `)){if(!d.trim())continue;const f=d[0],p=d[1],I=d.substring(3).trim(),H=p!==" "?p:f;i.push({path:I,status:H,staged:f,unstaged:p,additions:0,deletions:0})}const u=new Map;try{const d=E("git diff --numstat HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3});for(const f of d.split(`
18
+ `)){if(!f.trim())continue;const p=f.split(" ");if(p.length>=3){const I=parseInt(p[0],10)||0,H=parseInt(p[1],10)||0;u.set(p[2],{added:I,deleted:H})}}}catch{}for(const d of i){const f=u.get(d.path);f&&(d.additions=f.added,d.deletions=f.deleted)}let j=0,l=0;for(const d of i)j+=d.additions,l+=d.deletions;for(const d of i)if(d.status==="?"&&d.additions===0)try{const f=h.join(r,d.path),I=Y(f,"utf-8").split(`
19
+ `).length;d.additions=I,j+=I}catch{}return{repoRoot:r,branch:n,files:i,linesAdded:j,linesRemoved:l}}catch{return t}}return s.get("/api/git/status",a(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=_(t),n=we(r);return e.json(n)}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0})}}),s.get("/api/git/diff",a(),e=>{const t=e.req.query("path"),r=e.req.query("file");if(!t||!r)return e.json({error:"path and file are required"},400);try{const n=_(t);if(!W(n)||!C(n).isDirectory())return e.json({error:"invalid repository path: "+n},400);try{const o=K("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),i=K("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:o,stagedDiff:i})}catch(o){throw console.error("[git/diff] execFileSync failed:",o?.code,o.message),o}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),s.get("/api/file",a(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=_(t);if(!C(r).isFile())return e.json({error:"not a file"},400);const n=C(r).size;if(n>J)return e.json({error:"file too large",size:n,maxBytes:J},413);const o=Y(r);return e.json({path:r,content:o,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",a(),async e=>{try{const t=await e.req.json();if(!t.path||typeof t.content!="string")return e.json({error:"path and content are required"},400);const r=_(t.path);return Me(r,t.content),e.json({ok:!0})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),s.post("/api/file/delete",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=_(t.path);return C(r).isFile()?(ke(r),e.json({ok:!0})):e.json({error:"not a file"},400)}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"delete failed"},500)}}),s.post("/api/file/touch",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=_(t.path);return W(r)?e.json({error:"file already exists"},409):(be(h.dirname(r),{recursive:!0}),ve(r,"","utf-8"),e.json({ok:!0,path:r}))}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"touch failed"},500)}}),s.get("/api/sidebar/sessions",a(),e=>{const t=e.req.query("currentSession");return e.json(F(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",a(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(ie(t))}),s.post("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=G(t);return"error"in r?e.json({error:r.error},400):(await nt(r.sessionName,r.windowIndex),e.json(F()))}),s.delete("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=G(t);return"error"in r?e.json({error:r.error},400):(await st(r.sessionName,r.windowIndex),e.json(F()))}),s.get("/api/notes",a(),e=>{const t=[...A.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",a(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=A.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",a(),async e=>{const t=decodeURIComponent(e.req.param("scope"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}if(typeof r.content!="string")return e.json({error:"content must be string"},400);const n={scope:t,content:r.content,updatedAt:Date.now()},o=A.data.notes.findIndex(i=>i.scope===t);return o>=0?A.data.notes[o]=n:A.data.notes.push(n),await A.write(),e.json({ok:!0})}),s.post("/api/session/:session/upload",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!R().some(i=>i.name===t))return e.json({error:"session not found"},404);let n;try{n=await e.req.parseBody()}catch{return e.json({error:"invalid multipart body"},400)}const o=n.file;if(!(o instanceof File))return e.json({error:"missing file field"},400);try{const i=await o.arrayBuffer(),u=Buffer.from(i),{path:j}=await Ze(u,o.type||void 0,o.name||void 0);return e.json({path:j})}catch(i){return i instanceof Xe?e.json({error:i.message},i.status):(console.error("[upload]",i),e.json({error:"upload failed"},500))}}),s.get("/api/session/:session/windows",a(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map(it(t).map(i=>[i.windowIndex,i.label])),n=new Map(ie(t).map(i=>[i.index,i])),o=ht(t).map(i=>({...i,label:r.get(i.index)??null,worktree:n.get(i.index)?.worktree??!1}));return e.json(o)}),s.post("/api/session/:session/window-label",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);const o=typeof r.label=="string"?r.label:"",i=await at(t,n,o);return e.json(i)}),s.post("/api/session/:session/select-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);try{return gt(t,n),e.json({ok:!0})}catch(o){return o instanceof N?e.json({error:o.message},o.status):(console.error("[select-window]",o),e.json({error:"select-window failed"},500))}}),s.post("/api/session/:session/rename-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);if(typeof r.name!="string"||!r.name.trim())return e.json({error:"name is required"},400);try{return wt(t,n,r.name),oe(t),e.json({ok:!0})}catch(o){return o instanceof N?e.json({error:o.message},o.status):(console.error("[rename-window]",o),e.json({error:"rename-window failed"},500))}}),s.post("/api/session/:session/new-window",a(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return yt(t),oe(t),e.json({ok:!0})}catch(r){return r instanceof N?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),s.get("/api/schedule",a(),e=>e.json(x.list(e.req.query("session")))),s.post("/api/schedule",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ee(t);if(r)return e.json({error:r},400);if(!Ye(t))return e.json({error:"invalid body"},400);const n=await x.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),s.delete("/api/schedule/:id",a(),async e=>await x.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.patch("/api/schedule/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ee(t);if(r)return e.json({error:r},400);if(!Ke(t))return e.json({error:"invalid body"},400);const n=await x.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)}),y&&(s.use("/a/:agentId/*",a()),bt(s,{getChannel:y.getChannel})),s}const At=7;function xt(k){return typeof k!="number"||!Number.isFinite(k)?At:Math.min(365,Math.max(1,Math.round(k)))}export{fr as buildApp};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-weblink",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Access your tmux sessions from the browser. A continuation of tmux-web and persalink.",
5
5
  "type": "module",
6
6
  "bin": {