tmux-weblink 2.2.24 → 2.2.25

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 P,readdirSync as le,statSync as T,unlinkSync as me,writeFileSync as fe,mkdirSync as pe,readFileSync as B}from"node:fs";import{readFile as he}from"node:fs/promises";import{execSync as _,execFileSync as L}from"node:child_process";import{tmuxEnv as ge}from"./tmux-env.js";import h from"node:path";import{fileURLToPath as ye}from"node:url";import{Hono as je}from"hono";import{listSessions as D}from"../sessions.js";import{renderLoginPage as we,renderNotesIndex as ve,renderNotesPage as ke,renderSettings as qe,renderThemeSettings as Se,renderHistoryIndex as be,renderQuickCommandsPage as Te,renderFilesIndex as Ae,renderShell as xe}from"../frontend.js";import{renderFederationPage as _e}from"./pages/federation.js";import{createAgentToken as Ie,removeAgentToken as Ce,listAgentTokens as G}from"./agent-tokens.js";import{hashPassword as Ee,verifyPassword as Oe,validatePassword as Ne}from"./auth.js";import{atomicWriteFileSync as Re}from"./atomicWrite.js";import{resolveFsPath as A,resolveFsRoots as z,MAX_FILE_BYTES as V,walkRecursive as Pe}from"./fs-access.js";import{captureSessionWindowsWithPath as Le}from"./tmux-windows.js";import{audit as v}from"./auditLog.js";import{db as S}from"./db.js";import{getSessionAccessMap as De}from"./session-access.js";import{listWindowHistory as Fe,clearWindowHistory as He}from"./window-history.js";import{saveSecurityConfig as Me}from"./security-config.js";import{ImageUploadError as Ue,saveUploadedImage as We}from"./image-upload.js";import{readSettings as Q,writeSettings as $e}from"./settings.js";import{setActiveThemeTemplate as K}from"./theme-store.js";import{isThemeTemplateId as X,THEME_TEMPLATE_IDS as Be}from"./themes/index.js";import{pinView as Ge,unpinView as ze,listPinnedViews as Ve}from"./pinned-views.js";import{getStoredWindows as Qe}from"./session-windows.js";import{buildSidebarSessions as Ke}from"./sessions-sidebar.js";import{upsertSessionState as Z,removeSessionState as Y,renameSessionState as J,computeTombstones as Xe,getSessionState as Ze}from"./session-state.js";import{createQuickCommand as Ye,deleteQuickCommand as Je,listQuickCommands as ee,updateQuickCommand as et}from"./quick-commands.js";import{getSystemStatus as tt,getTopProcesses as rt,killProcess as nt}from"./system-monitor.js";import{newTmuxSession as te,renameSession as st,killSession as ot,sessionExists as it,TmuxWindowsError as re}from"./tmux-windows.js";import{registerAgentTunnel as at}from"./agent-tunnel.js";import{injectScopeScript as dt}from"./scope-script.js";const F="tmux-web-token",ut=365;function ct(x){return x==="127.0.0.1"||x==="::1"||x==="::ffff:127.0.0.1"}function zt(x){const{mode:ne,securityConfig:g,tokenStore:j,rateLimiter:C,settings:lt,terminalBufferConfig:E,getAgentId:H,state:l,federation:k,hub:w}=x,q=ne==="agent";function I(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 O(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*)${F}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function mt(e){const t=O(e);return t?j.validateToken(t):null}function M(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${F}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ut*86400}${r?"; Secure":""}`)}function se(e){e.header("Set-Cookie",`${F}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}const o=()=>q?async(e,t)=>t():async(e,t)=>{const r=O(e);return!r||!j.validateToken(r)?(v("http_unauthorized",{ip:I(e)}),e.json({error:"unauthorized"},401)):t()},y=()=>q?async(e,t)=>t():async(e,t)=>{const r=O(e);if(!r||!j.validateToken(r)){const n=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${n}`,302)}return t()};function N(e){const t=D(),r=new Set(t.map(c=>c.name)),n=Ke(t,De(),Ve()),a=[],i=new Map,u=new Map;if(w)for(const c of w.agents.list()){const d=w.agents.get(c.agentId);i.set(c.agentId,new Set((d?d.sessions:[]).map(m=>m.name))),u.set(c.agentId,c.online),a.push({agentId:c.agentId,agentName:c.name,online:c.online,sessions:d?d.sessions:[]})}const p=Xe(r,i,u);return{...n,currentSession:e??null,agents:a,tombstones:p}}function U(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 je;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()}),q||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(dt(n,{prefix:"",agentId:null}),e.res)});const W=h.dirname(ye(import.meta.url)),oe=[h.join(W,"..","assets"),h.join(W,"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 oe){const n=h.join(r,t);if(!P(n))continue;const a=await he(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(a,200,{"Content-Type":u[i]??"application/octet-stream","Cache-Control":i===".js"||i===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const ie=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
1
+ import{existsSync as P,readdirSync as me,statSync as T,unlinkSync as fe,writeFileSync as pe,mkdirSync as he,readFileSync as B}from"node:fs";import{readFile as ge}from"node:fs/promises";import{execSync as _,execFileSync as L}from"node:child_process";import{tmuxEnv as ye}from"./tmux-env.js";import h from"node:path";import{fileURLToPath as je}from"node:url";import{Hono as we}from"hono";import{listSessions as D}from"../sessions.js";import{renderLoginPage as ve,renderNotesIndex as ke,renderNotesPage as qe,renderSettings as Se,renderThemeSettings as be,renderHistoryIndex as Te,renderQuickCommandsPage as Ae,renderFilesIndex as xe,renderShell as _e}from"../frontend.js";import{renderFederationPage as Ce}from"./pages/federation.js";import{createAgentToken as Ie,removeAgentToken as Ee,listAgentTokens as G}from"./agent-tokens.js";import{hashPassword as Oe,verifyPassword as Ne,validatePassword as Re}from"./auth.js";import{atomicWriteFileSync as Pe}from"./atomicWrite.js";import{resolveFsPath as A,resolveFsRoots as z,MAX_FILE_BYTES as V,walkRecursive as Le,fsEntryCmp as Q}from"./fs-access.js";import{captureSessionWindowsWithPath as De}from"./tmux-windows.js";import{audit as v}from"./auditLog.js";import{db as S}from"./db.js";import{getSessionAccessMap as Fe}from"./session-access.js";import{listWindowHistory as He,clearWindowHistory as Me}from"./window-history.js";import{saveSecurityConfig as Ue}from"./security-config.js";import{ImageUploadError as We,saveUploadedImage as $e}from"./image-upload.js";import{readSettings as K,writeSettings as Be}from"./settings.js";import{setActiveThemeTemplate as X}from"./theme-store.js";import{isThemeTemplateId as Z,THEME_TEMPLATE_IDS as Ge}from"./themes/index.js";import{pinView as ze,unpinView as Ve,listPinnedViews as Qe}from"./pinned-views.js";import{getStoredWindows as Ke}from"./session-windows.js";import{buildSidebarSessions as Xe}from"./sessions-sidebar.js";import{upsertSessionState as Y,removeSessionState as J,renameSessionState as ee,computeTombstones as Ze,getSessionState as Ye}from"./session-state.js";import{createQuickCommand as Je,deleteQuickCommand as et,listQuickCommands as te,updateQuickCommand as tt}from"./quick-commands.js";import{getSystemStatus as rt,getTopProcesses as nt,killProcess as st}from"./system-monitor.js";import{newTmuxSession as re,renameSession as ot,killSession as it,sessionExists as at,TmuxWindowsError as ne}from"./tmux-windows.js";import{registerAgentTunnel as dt}from"./agent-tunnel.js";import{injectScopeScript as ut}from"./scope-script.js";const F="tmux-web-token",ct=365;function lt(x){return x==="127.0.0.1"||x==="::1"||x==="::ffff:127.0.0.1"}function Vt(x){const{mode:se,securityConfig:g,tokenStore:j,rateLimiter:I,settings:mt,terminalBufferConfig:E,getAgentId:H,state:l,federation:k,hub:w}=x,q=se==="agent";function C(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 O(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*)${F}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function ft(e){const t=O(e);return t?j.validateToken(t):null}function M(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${F}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ct*86400}${r?"; Secure":""}`)}function oe(e){e.header("Set-Cookie",`${F}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}const o=()=>q?async(e,t)=>t():async(e,t)=>{const r=O(e);return!r||!j.validateToken(r)?(v("http_unauthorized",{ip:C(e)}),e.json({error:"unauthorized"},401)):t()},y=()=>q?async(e,t)=>t():async(e,t)=>{const r=O(e);if(!r||!j.validateToken(r)){const n=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${n}`,302)}return t()};function N(e){const t=D(),r=new Set(t.map(c=>c.name)),n=Xe(t,Fe(),Qe()),a=[],i=new Map,u=new Map;if(w)for(const c of w.agents.list()){const d=w.agents.get(c.agentId);i.set(c.agentId,new Set((d?d.sessions:[]).map(m=>m.name))),u.set(c.agentId,c.online),a.push({agentId:c.agentId,agentName:c.name,online:c.online,sessions:d?d.sessions:[]})}const p=Ze(r,i,u);return{...n,currentSession:e??null,agents:a,tombstones:p}}function U(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 we;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()}),q||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(ut(n,{prefix:"",agentId:null}),e.res)});const W=h.dirname(je(import.meta.url)),ie=[h.join(W,"..","assets"),h.join(W,"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 ie){const n=h.join(r,t);if(!P(n))continue;const a=await ge(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(a,200,{"Content-Type":u[i]??"application/octet-stream","Cache-Control":i===".js"||i===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const ae=`<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>`,$=e=>e.body(ie,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});s.get("/favicon.svg",$),s.get("/favicon.ico",$);const ae={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(ae,200,{"Cache-Control":"public, max-age=3600"}));const de=`// tmux-weblink Service Worker
5
+ </svg>`,$=e=>e.body(ae,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});s.get("/favicon.svg",$),s.get("/favicon.ico",$);const de={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(de,200,{"Cache-Control":"public, max-age=3600"}));const ue=`// 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(de,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(we({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:l.activeTheme}))}),s.get("/",y(),e=>{const t=z();return e.html(xe({theme:l.activeTheme,fsRoots:t,terminalCfg:E,scrollback:E.initialLines+2*E.historyChunk,wsBase:q&&H?.()?`/ws/a/${H()}`:void 0}))}),s.get("/notes",y(),e=>e.html(ve(S.data.notes,l.activeTheme))),s.get("/notes/:session",y(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.html(ke(t,l.activeTheme))}),s.get("/history",y(),e=>{const t=D(),r=new Set(t.map(n=>n.name));return e.html(be(Fe(),l.activeTheme,r))}),s.get("/quick-commands",y(),e=>e.html(Te(ee(),l.activeTheme))),s.get("/files",y(),e=>{const t=z();return e.html(Ae(l.activeTheme,t))}),s.post("/api/history/clear",o(),async e=>(await He(),e.json({ok:!0}))),s.get("/api/quick-commands",o(),e=>e.json(ee())),s.post("/api/quick-commands",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Ye(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await et(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",o(),e=>Je(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{if(q)return e.json({error:"hub only"},403);const t=I(e),r=C.check(t);if(!r.allowed)return v("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 a=typeof n.password=="string"?n.password:"";if(!g.passwordHash){if(!g.security.allowRemoteSetup&&!ct(t))return v("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(l.settingUpPassword)return e.json({error:"Password setup in progress"},409);const c=Ne(a);if(c)return e.json({error:c},400);l.settingUpPassword=!0;try{g.passwordHash=await Ee(a),Me(g),v("password_set",{ip:t})}finally{l.settingUpPassword=!1}const{plaintext:d}=j.createAccessToken("setup",g.security.tokenTtlDays);return M(e,d),e.json({ok:!0,token:d,setupMode:!0})}if(!await Oe(a,g.passwordHash)){const c=C.recordFailure(t);return v("auth_failed",{ip:t,method:"password",failures:c.failures,permanentLock:c.permanentLock}),c.permanentLock?(j.revokeAll(),v("permanent_lock",{ip:t,failures:c.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):c.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:c.retryAfterMs},429)}C.recordSuccess(t);const u=`browser-${t}`,{plaintext:p}=j.createAccessToken(u,g.security.tokenTtlDays);return M(e,p),v("auth_success",{ip:t,method:"password",tokenName:u}),e.json({ok:!0,token:p})}),s.post("/api/auth/token",o(),async e=>{if(q)return e.json({error:"hub only"},403);const t=I(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}`,a=typeof r.ttlDays=="number"?r.ttlDays:g.security.tokenTtlDays,{stored:i,plaintext:u}=j.createAccessToken(n,a);return v("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",o(),e=>q?e.json({error:"hub only"},403):e.json(j.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",o(),e=>q?e.json({error:"hub only"},403):j.revoke(e.req.param("id"))?(v("token_revoked",{ip:I(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",o(),e=>(se(e),e.json({ok:!0}))),s.get("/settings",y(),async e=>{const t=await Q();return e.html(qe({settings:t,theme:l.activeTheme,saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",o(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await Q(),n=t.defaultView==="recent"?"recent":"default";return await $e({...r,defaultView:n}),e.redirect("/settings?saved=1",303)}),s.get("/settings/theme",y(),e=>e.html(Se({theme:l.activeTheme,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",o(),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"||!X(r)?e.redirect("/settings/theme",303):(l.activeTheme=await K(r),e.redirect("/settings/theme?saved=1",303))});const ue={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",o(),e=>e.json({active:l.activeTheme.template,templates:Be.map(t=>({id:t,name:ue[t]}))})),s.post("/api/theme",o(),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"||!X(r)?e.json({error:"invalid theme template"},400):(l.activeTheme=await K(r),e.json({ok:!0,active:l.activeTheme.template}))}),s.get("/api/system/status",o(),e=>e.json(tt())),s.get("/api/system/processes",o(),e=>e.json(rt())),k&&(s.get("/settings/federation",y(),e=>e.redirect("/settings/machines",301)),s.get("/settings/machines",y(),async e=>e.html(_e({config:k.getConfig(),status:k.status(),tokens:G().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})),hostname:k.hostname},l.activeTheme))),s.get("/api/machines",o(),e=>e.json({config:k.getConfig(),status:k.status()})),s.post("/api/machines",o(),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():"",a=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:a||void 0,enabled:i};return await k.setConfig(u),e.json({ok:!0,status:k.status()})}),s.get("/api/agent-tokens",o(),e=>e.json(G().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})))),s.post("/api/agent-tokens",o(),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:a}=Ie(r);return e.json({ok:!0,id:n,name:r,token:a})}),s.delete("/api/agent-tokens/:id",o(),e=>{const t=e.req.param("id");return Ce(t)?e.json({ok:!0}):e.json({error:"no such token"},404)})),s.post("/api/system/kill",o(),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=nt(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),s.get("/api/agents",o(),e=>w?e.json(w.agents.list()):e.json([])),s.post("/api/sessions/new",o(),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(D().some(i=>i.name===r))return e.json({error:"session already exists"},409);try{return te(r,n),Z(r,{path:n}),e.json({ok:!0})}catch(i){const u=i instanceof re?i.message:"failed to create session";return e.json({error:u},500)}}),s.post("/api/sessions/rename",o(),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)?(st(t,r),J(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",o(),async e=>{try{const{name:t}=await e.req.json();return t?(ot(t),Y(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.post("/api/sessions/rebuild",o(),async e=>{try{const t=await e.req.json(),r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name required"},400);const n=typeof t.agentId=="string"?t.agentId:void 0,i=Ze().find(u=>u.name===r&&(u.agentId??null)===(n??null))?.path;if(n){if(!w)return e.json({error:"not a hub"},400);const u=w.getChannel(n);return u?u.rebuildSession(r,i)?e.json({ok:!0,async:!0}):e.json({error:"agent offline"},409):e.json({error:"agent offline"},409)}if(it(r))return e.json({error:"session already exists"},409);try{te(r,i)}catch(u){const p=u instanceof re?u.message:"rebuild failed";return e.json({error:p},500)}return Z(r,{path:i}),e.json({ok:!0})}catch{return e.json({error:"rebuild failed"},500)}}),s.post("/api/sessions/dismiss",o(),async e=>{try{const t=await e.req.json(),r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name required"},400);const n=typeof t.agentId=="string"?t.agentId:void 0;return Y(r,n),e.json({ok:!0})}catch{return e.json({error:"dismiss failed"},500)}}),s.post("/api/sessions/rename-tombstone",o(),async e=>{try{const t=await e.req.json(),r=typeof t.oldName=="string"?t.oldName.trim():"",n=typeof t.newName=="string"?t.newName.trim():"";if(!r||!n)return e.json({error:"oldName and newName required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(n))return e.json({error:"invalid characters in name"},400);const a=typeof t.agentId=="string"?t.agentId:void 0;return J(r,n,a),e.json({ok:!0})}catch{return e.json({error:"rename failed"},500)}}),s.get("/api/session-program",o(),e=>{const t=e.req.query("session");if(!t)return e.json({program:null});try{const n=L("tmux",["display-message","-p","-t",t,"#{pane_current_command}"],{encoding:"utf-8",timeout:3e3,env:ge()}).trim()||null;return e.json({program:n})}catch{return e.json({program:null})}}),s.post("/api/session/:name/upload",o(),async e=>{try{const r=(await e.req.parseBody()).file;if(!(r instanceof File))return e.json({error:'expected a file field named "file"'},400);const n=Buffer.from(await r.arrayBuffer()),{path:a}=await We(n,r.type,r.name);return e.json({path:a})}catch(t){return t instanceof Ue?e.json({error:t.message},t.status):e.json({error:"upload failed"},500)}}),s.get("/api/fs/session-path",o(),e=>{const t=e.req.query("session");if(!t)return e.json({path:process.env.HOME??"/"});try{const r=Le(t),a=r.find(i=>i.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:a})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",o(),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 a=r,i="",u=r.endsWith("/");if(!u)try{u=T(r).isDirectory()}catch{}u||(a=h.dirname(r),i=h.basename(r).toLowerCase());try{const p=le(a),c=[],d=[];for(const m of p)if(!m.startsWith(".")&&!(i&&!m.toLowerCase().startsWith(i))){try{const f=h.join(a,m);T(f).isDirectory()?(c.push(f),n&&Pe(f,c,d,0)):d.push(f)}catch{}if(c.length+d.length>=5e3)break}return e.json({dirs:c,files:d})}catch{return e.json({dirs:[],files:[]})}});function ce(e){const t={repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0};try{const r=_("git rev-parse --show-toplevel 2>/dev/null",{cwd:e,encoding:"utf-8",timeout:3e3}).trim();if(!r)return t;let n;try{n=_("git branch --show-current 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}catch{n=_("git rev-parse --short HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}n||(n="HEAD");let a="";try{a=_("git status --porcelain 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3})}catch{}const i=[];for(const d of a.split(`
16
+ `;s.get("/sw.js",e=>e.body(ue,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(ve({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:l.activeTheme}))}),s.get("/",y(),e=>{const t=z();return e.html(_e({theme:l.activeTheme,fsRoots:t,terminalCfg:E,scrollback:E.initialLines+2*E.historyChunk,wsBase:q&&H?.()?`/ws/a/${H()}`:void 0}))}),s.get("/notes",y(),e=>e.html(ke(S.data.notes,l.activeTheme))),s.get("/notes/:session",y(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.html(qe(t,l.activeTheme))}),s.get("/history",y(),e=>{const t=D(),r=new Set(t.map(n=>n.name));return e.html(Te(He(),l.activeTheme,r))}),s.get("/quick-commands",y(),e=>e.html(Ae(te(),l.activeTheme))),s.get("/files",y(),e=>{const t=z();return e.html(xe(l.activeTheme,t))}),s.post("/api/history/clear",o(),async e=>(await Me(),e.json({ok:!0}))),s.get("/api/quick-commands",o(),e=>e.json(te())),s.post("/api/quick-commands",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Je(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await tt(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",o(),e=>et(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{if(q)return e.json({error:"hub only"},403);const t=C(e),r=I.check(t);if(!r.allowed)return v("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 a=typeof n.password=="string"?n.password:"";if(!g.passwordHash){if(!g.security.allowRemoteSetup&&!lt(t))return v("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(l.settingUpPassword)return e.json({error:"Password setup in progress"},409);const c=Re(a);if(c)return e.json({error:c},400);l.settingUpPassword=!0;try{g.passwordHash=await Oe(a),Ue(g),v("password_set",{ip:t})}finally{l.settingUpPassword=!1}const{plaintext:d}=j.createAccessToken("setup",g.security.tokenTtlDays);return M(e,d),e.json({ok:!0,token:d,setupMode:!0})}if(!await Ne(a,g.passwordHash)){const c=I.recordFailure(t);return v("auth_failed",{ip:t,method:"password",failures:c.failures,permanentLock:c.permanentLock}),c.permanentLock?(j.revokeAll(),v("permanent_lock",{ip:t,failures:c.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):c.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:c.retryAfterMs},429)}I.recordSuccess(t);const u=`browser-${t}`,{plaintext:p}=j.createAccessToken(u,g.security.tokenTtlDays);return M(e,p),v("auth_success",{ip:t,method:"password",tokenName:u}),e.json({ok:!0,token:p})}),s.post("/api/auth/token",o(),async e=>{if(q)return e.json({error:"hub only"},403);const t=C(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}`,a=typeof r.ttlDays=="number"?r.ttlDays:g.security.tokenTtlDays,{stored:i,plaintext:u}=j.createAccessToken(n,a);return v("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",o(),e=>q?e.json({error:"hub only"},403):e.json(j.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",o(),e=>q?e.json({error:"hub only"},403):j.revoke(e.req.param("id"))?(v("token_revoked",{ip:C(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",o(),e=>(oe(e),e.json({ok:!0}))),s.get("/settings",y(),async e=>{const t=await K();return e.html(Se({settings:t,theme:l.activeTheme,saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",o(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await K(),n=t.defaultView==="recent"?"recent":"default";return await Be({...r,defaultView:n}),e.redirect("/settings?saved=1",303)}),s.get("/settings/theme",y(),e=>e.html(be({theme:l.activeTheme,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",o(),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"||!Z(r)?e.redirect("/settings/theme",303):(l.activeTheme=await X(r),e.redirect("/settings/theme?saved=1",303))});const ce={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",o(),e=>e.json({active:l.activeTheme.template,templates:Ge.map(t=>({id:t,name:ce[t]}))})),s.post("/api/theme",o(),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"||!Z(r)?e.json({error:"invalid theme template"},400):(l.activeTheme=await X(r),e.json({ok:!0,active:l.activeTheme.template}))}),s.get("/api/system/status",o(),e=>e.json(rt())),s.get("/api/system/processes",o(),e=>e.json(nt())),k&&(s.get("/settings/federation",y(),e=>e.redirect("/settings/machines",301)),s.get("/settings/machines",y(),async e=>e.html(Ce({config:k.getConfig(),status:k.status(),tokens:G().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})),hostname:k.hostname},l.activeTheme))),s.get("/api/machines",o(),e=>e.json({config:k.getConfig(),status:k.status()})),s.post("/api/machines",o(),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():"",a=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:a||void 0,enabled:i};return await k.setConfig(u),e.json({ok:!0,status:k.status()})}),s.get("/api/agent-tokens",o(),e=>e.json(G().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})))),s.post("/api/agent-tokens",o(),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:a}=Ie(r);return e.json({ok:!0,id:n,name:r,token:a})}),s.delete("/api/agent-tokens/:id",o(),e=>{const t=e.req.param("id");return Ee(t)?e.json({ok:!0}):e.json({error:"no such token"},404)})),s.post("/api/system/kill",o(),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=st(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),s.get("/api/agents",o(),e=>w?e.json(w.agents.list()):e.json([])),s.post("/api/sessions/new",o(),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(D().some(i=>i.name===r))return e.json({error:"session already exists"},409);try{return re(r,n),Y(r,{path:n}),e.json({ok:!0})}catch(i){const u=i instanceof ne?i.message:"failed to create session";return e.json({error:u},500)}}),s.post("/api/sessions/rename",o(),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)?(ot(t,r),ee(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",o(),async e=>{try{const{name:t}=await e.req.json();return t?(it(t),J(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.post("/api/sessions/rebuild",o(),async e=>{try{const t=await e.req.json(),r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name required"},400);const n=typeof t.agentId=="string"?t.agentId:void 0,i=Ye().find(u=>u.name===r&&(u.agentId??null)===(n??null))?.path;if(n){if(!w)return e.json({error:"not a hub"},400);const u=w.getChannel(n);return u?u.rebuildSession(r,i)?e.json({ok:!0,async:!0}):e.json({error:"agent offline"},409):e.json({error:"agent offline"},409)}if(at(r))return e.json({error:"session already exists"},409);try{re(r,i)}catch(u){const p=u instanceof ne?u.message:"rebuild failed";return e.json({error:p},500)}return Y(r,{path:i}),e.json({ok:!0})}catch{return e.json({error:"rebuild failed"},500)}}),s.post("/api/sessions/dismiss",o(),async e=>{try{const t=await e.req.json(),r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name required"},400);const n=typeof t.agentId=="string"?t.agentId:void 0;return J(r,n),e.json({ok:!0})}catch{return e.json({error:"dismiss failed"},500)}}),s.post("/api/sessions/rename-tombstone",o(),async e=>{try{const t=await e.req.json(),r=typeof t.oldName=="string"?t.oldName.trim():"",n=typeof t.newName=="string"?t.newName.trim():"";if(!r||!n)return e.json({error:"oldName and newName required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(n))return e.json({error:"invalid characters in name"},400);const a=typeof t.agentId=="string"?t.agentId:void 0;return ee(r,n,a),e.json({ok:!0})}catch{return e.json({error:"rename failed"},500)}}),s.get("/api/session-program",o(),e=>{const t=e.req.query("session");if(!t)return e.json({program:null});try{const n=L("tmux",["display-message","-p","-t",t,"#{pane_current_command}"],{encoding:"utf-8",timeout:3e3,env:ye()}).trim()||null;return e.json({program:n})}catch{return e.json({program:null})}}),s.post("/api/session/:name/upload",o(),async e=>{try{const r=(await e.req.parseBody()).file;if(!(r instanceof File))return e.json({error:'expected a file field named "file"'},400);const n=Buffer.from(await r.arrayBuffer()),{path:a}=await $e(n,r.type,r.name);return e.json({path:a})}catch(t){return t instanceof We?e.json({error:t.message},t.status):e.json({error:"upload failed"},500)}}),s.get("/api/fs/session-path",o(),e=>{const t=e.req.query("session");if(!t)return e.json({path:process.env.HOME??"/"});try{const r=De(t),a=r.find(i=>i.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:a})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",o(),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 a=r,i="",u=r.endsWith("/");if(!u)try{u=T(r).isDirectory()}catch{}u||(a=h.dirname(r),i=h.basename(r).toLowerCase());try{const p=me(a),c=[],d=[];for(const m of p)if(!(i&&!m.toLowerCase().startsWith(i))){try{const f=h.join(a,m);T(f).isDirectory()?(c.push(f),n&&Le(f,c,d,0)):d.push(f)}catch{}if(c.length+d.length>=5e3)break}return c.sort(Q),d.sort(Q),e.json({dirs:c,files:d})}catch{return e.json({dirs:[],files:[]})}});function le(e){const t={repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0};try{const r=_("git rev-parse --show-toplevel 2>/dev/null",{cwd:e,encoding:"utf-8",timeout:3e3}).trim();if(!r)return t;let n;try{n=_("git branch --show-current 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}catch{n=_("git rev-parse --short HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}n||(n="HEAD");let a="";try{a=_("git status --porcelain 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3})}catch{}const i=[];for(const d of a.split(`
17
17
  `)){if(!d.trim())continue;const m=d[0],f=d[1],b=d.substring(3).trim(),R=f!==" "?f:m;i.push({path:b,status:R,staged:m,unstaged:f,additions:0,deletions:0})}const u=new Map;try{const d=_("git diff --numstat HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3});for(const m of d.split(`
18
18
  `)){if(!m.trim())continue;const f=m.split(" ");if(f.length>=3){const b=parseInt(f[0],10)||0,R=parseInt(f[1],10)||0;u.set(f[2],{added:b,deleted:R})}}}catch{}for(const d of i){const m=u.get(d.path);m&&(d.additions=m.added,d.deletions=m.deleted)}let p=0,c=0;for(const d of i)p+=d.additions,c+=d.deletions;for(const d of i)if(d.status==="?"&&d.additions===0)try{const m=h.join(r,d.path),b=B(m,"utf-8").split(`
19
- `).length;d.additions=b,p+=b}catch{}return{repoRoot:r,branch:n,files:i,linesAdded:p,linesRemoved:c}}catch{return t}}return s.get("/api/git/status",o(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=A(t),n=ce(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",o(),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=A(t);if(!P(n)||!T(n).isDirectory())return e.json({error:"invalid repository path: "+n},400);try{const a=L("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),i=L("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:a,stagedDiff:i})}catch(a){throw console.error("[git/diff] execFileSync failed:",a?.code,a.message),a}}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",o(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=A(t);if(!T(r).isFile())return e.json({error:"not a file"},400);const n=T(r).size;if(n>V)return e.json({error:"file too large",size:n,maxBytes:V},413);const a=B(r,"utf-8");return e.json({path:r,content:a,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",o(),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=A(t.path);return Re(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",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=A(t.path);return T(r).isFile()?(me(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",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=A(t.path);return P(r)?e.json({error:"file already exists"},409):(pe(h.dirname(r),{recursive:!0}),fe(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",o(),e=>{const t=e.req.query("currentSession");return e.json(N(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",o(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(Qe(t))}),s.post("/api/pinned-views",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=U(t);return"error"in r?e.json({error:r.error},400):(await Ge(r.sessionName,r.windowIndex),e.json(N()))}),s.delete("/api/pinned-views",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=U(t);return"error"in r?e.json({error:r.error},400):(await ze(r.sessionName,r.windowIndex),e.json(N()))}),s.get("/api/notes",o(),e=>{const t=[...S.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",o(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=S.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",o(),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()},a=S.data.notes.findIndex(i=>i.scope===t);return a>=0?S.data.notes[a]=n:S.data.notes.push(n),await S.write(),e.json({ok:!0})}),w&&(s.use("/a/:agentId/*",o()),at(s,{getChannel:w.getChannel})),s}export{zt as buildApp};
19
+ `).length;d.additions=b,p+=b}catch{}return{repoRoot:r,branch:n,files:i,linesAdded:p,linesRemoved:c}}catch{return t}}return s.get("/api/git/status",o(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=A(t),n=le(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",o(),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=A(t);if(!P(n)||!T(n).isDirectory())return e.json({error:"invalid repository path: "+n},400);try{const a=L("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),i=L("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:a,stagedDiff:i})}catch(a){throw console.error("[git/diff] execFileSync failed:",a?.code,a.message),a}}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",o(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=A(t);if(!T(r).isFile())return e.json({error:"not a file"},400);const n=T(r).size;if(n>V)return e.json({error:"file too large",size:n,maxBytes:V},413);const a=B(r,"utf-8");return e.json({path:r,content:a,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",o(),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=A(t.path);return Pe(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",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=A(t.path);return T(r).isFile()?(fe(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",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=A(t.path);return P(r)?e.json({error:"file already exists"},409):(he(h.dirname(r),{recursive:!0}),pe(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",o(),e=>{const t=e.req.query("currentSession");return e.json(N(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",o(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(Ke(t))}),s.post("/api/pinned-views",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=U(t);return"error"in r?e.json({error:r.error},400):(await ze(r.sessionName,r.windowIndex),e.json(N()))}),s.delete("/api/pinned-views",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=U(t);return"error"in r?e.json({error:r.error},400):(await Ve(r.sessionName,r.windowIndex),e.json(N()))}),s.get("/api/notes",o(),e=>{const t=[...S.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",o(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=S.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",o(),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()},a=S.data.notes.findIndex(i=>i.scope===t);return a>=0?S.data.notes[a]=n:S.data.notes.push(n),await S.write(),e.json({ok:!0})}),w&&(s.use("/a/:agentId/*",o()),dt(s,{getChannel:w.getChannel})),s}export{Vt as buildApp};
@@ -1 +1 @@
1
- import e from"node:path";import{readdirSync as f,statSync as l}from"node:fs";const v=1048576;function a(){const o=process.env.TMUX_WEB_FS_ROOTS;return o?o.split(":").map(r=>(r=r.trim(),r.startsWith("~")&&(r=e.join(process.env.HOME||"/",r.slice(1))),e.resolve(r))):[]}function m(o){const r=a();let t=o;if(t.startsWith("~")&&(t=e.join(process.env.HOME||"/",t.slice(1))),e.isAbsolute(t)||(t=e.resolve(t)),t=e.normalize(t),r.length===0)return t;if(!r.some(s=>t===s||t.startsWith(s+e.sep)))throw new Error("PATH_NOT_ALLOWED");return t}function h(o,r,t,i,s=5e3){if(i>8)return;let c;try{c=f(o)}catch{return}for(const u of c){if(u.startsWith("."))continue;const n=e.join(o,u);try{l(n).isDirectory()?(r.push(n),h(n,r,t,i+1,s)):t.push(n)}catch{}if(r.length+t.length>=s)return}}export{v as MAX_FILE_BYTES,m as resolveFsPath,a as resolveFsRoots,h as walkRecursive};
1
+ import n from"node:path";import{readdirSync as u,statSync as f}from"node:fs";const _=1048576;function v(e,r){const t=n.basename(e).startsWith("."),s=n.basename(r).startsWith(".");return t!==s?t?1:-1:e.localeCompare(r)}function l(){const e=process.env.TMUX_WEB_FS_ROOTS;return e?e.split(":").map(r=>(r=r.trim(),r.startsWith("~")&&(r=n.join(process.env.HOME||"/",r.slice(1))),n.resolve(r))):[]}function E(e){const r=l();let t=e;if(t.startsWith("~")&&(t=n.join(process.env.HOME||"/",t.slice(1))),n.isAbsolute(t)||(t=n.resolve(t)),t=n.normalize(t),r.length===0)return t;if(!r.some(o=>t===o||t.startsWith(o+n.sep)))throw new Error("PATH_NOT_ALLOWED");return t}function h(e,r,t,s,o=5e3){if(s>8)return;let c;try{c=u(e)}catch{return}for(const a of c){if(a.startsWith("."))continue;const i=n.join(e,a);try{f(i).isDirectory()?(r.push(i),h(i,r,t,s+1,o)):t.push(i)}catch{}if(r.length+t.length>=o)return}}export{_ as MAX_FILE_BYTES,v as fsEntryCmp,E as resolveFsPath,l as resolveFsRoots,h as walkRecursive};
@@ -250,7 +250,7 @@ import{escapeHtml as c}from"./html.js";import{icon as i,iconPath as r}from"./ico
250
250
  <script>${y()}</script>`}function A(e){const{activePage:o,refreshHref:a}=e;function t(l,f,b,x,g=""){return`<a href="${f}" class="${l!==null&&l===o?"sidebar-btn current":"sidebar-btn"}"${g}>
251
251
  <svg viewBox="0 0 24 24" fill="currentColor">${b}</svg>
252
252
  ${x}
253
- </a>`}const s=r("sessions"),p=r("notes"),h=r("history"),m=r("quick-commands"),u=r("file"),k=r("refresh");return`<aside class="action-sidebar">
253
+ </a>`}const s=r("sessions"),p=r("notes"),h=r("history"),u=r("quick-commands"),m=r("file"),k=r("refresh");return`<aside class="action-sidebar">
254
254
  <p class="sidebar-label">Actions</p>
255
255
  <button class="sidebar-btn primary" id="new-session-btn">
256
256
  ${i("add")}
@@ -259,9 +259,9 @@ import{escapeHtml as c}from"./html.js";import{icon as i,iconPath as r}from"./ico
259
259
  <hr class="sidebar-divider">
260
260
  ${t("home","/",s,"Sessions")}
261
261
  ${t("notes","/notes",p,"All Notes")}
262
- ${t("files","/files",u,"Files")}
262
+ ${t("files","/files",m,"Files")}
263
263
  ${t("history","/history",h,"History")}
264
- ${t("quickCommands","/quick-commands",m,"Quick Commands")}
264
+ ${t("quickCommands","/quick-commands",u,"Quick Commands")}
265
265
  <hr class="sidebar-divider">
266
266
  <a href="${a}" class="sidebar-btn">
267
267
  ${i("refresh")}
@@ -498,7 +498,12 @@ import{escapeHtml as c}from"./html.js";import{icon as i,iconPath as r}from"./ico
498
498
  try {
499
499
  const res = await fetch(selectedBase() + '/api/fs/list?path=' + encodeURIComponent(path));
500
500
  const data = await res.json();
501
- const dirs = (data.dirs || []).filter(d => d !== path).sort((a, b) => a.localeCompare(b));
501
+ const dirs = (data.dirs || []).filter(d => d !== path).sort((a, b) => {
502
+ const ah = a.split('/').filter(Boolean).pop().startsWith('.');
503
+ const bh = b.split('/').filter(Boolean).pop().startsWith('.');
504
+ if (ah !== bh) return ah ? 1 : -1;
505
+ return a.localeCompare(b);
506
+ });
502
507
  treeCache[path] = dirs;
503
508
  return dirs;
504
509
  } catch { treeCache[path] = []; return []; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-weblink",
3
- "version": "2.2.24",
3
+ "version": "2.2.25",
4
4
  "description": "Access your tmux sessions from the browser. A continuation of tmux-web and persalink.",
5
5
  "type": "module",
6
6
  "bin": {