tmux-weblink 1.0.0 → 1.0.2
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,7 +1,7 @@
|
|
|
1
|
-
import{a as F}from"./chunk-O3DBTEWH.js";var h=window.__TMUX_WEB_SHELL__,
|
|
2
|
-
<span>${
|
|
3
|
-
<button class="session-edit-btn" data-session="${
|
|
4
|
-
`,a.addEventListener("click",
|
|
1
|
+
import{a as F}from"./chunk-O3DBTEWH.js";var h=window.__TMUX_WEB_SHELL__,l=null,c=null,D="sessions",m="",B="",x="",H="",i=document.getElementById("sidebar-content"),g=document.getElementById("main-placeholder"),f=document.getElementById("terminal-container"),k=document.getElementById("file-editor"),P=document.getElementById("fe-path"),o=document.getElementById("fe-status"),E=document.getElementById("fe-content"),y=document.getElementById("fe-save"),R=document.getElementById("fe-delete"),V=document.getElementById("fe-back"),S=document.getElementById("fe-new-name"),$=document.getElementById("fe-new-btn"),T=document.getElementById("settings-backdrop"),N=document.getElementById("settings-popover"),j=document.querySelector(".sidebar");function J(){return window.matchMedia("(max-width: 640px)").matches}function O(){J()&&j.classList.add("collapsed")}function z(){j.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{z(),_("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{z(),_("files")});function _(e){D=e,document.querySelectorAll(".mode-btn").forEach(n=>n.classList.remove("active"));let t=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");t&&t.classList.add("active"),e==="sessions"?L():U()}async function L(){i.innerHTML='<div class="sidebar-section-label">Sessions</div><button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';try{let t=(await(await fetch("/api/sidebar/sessions")).json()).recent||[];for(let n of t){let a=document.createElement("div");a.className="session-item"+(n.name===c?" active":""),a.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${n.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
|
|
2
|
+
<span>${b(n.name)}</span>
|
|
3
|
+
<button class="session-edit-btn" data-session="${b(n.name)}" title="Edit session"><svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg></button>
|
|
4
|
+
`,a.addEventListener("click",p=>{p.target.closest(".session-edit-btn")||M(n.name)}),i.appendChild(a);let s=a.querySelector(".session-edit-btn");s&&s.addEventListener("click",p=>{p.stopPropagation(),Z(n.name,s)})}}catch{i.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}let e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>document.getElementById("new-session-modal")?.classList.add("open"))}function b(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}async function M(e){if(!(c===e&&l)){k.style.display="none",g.style.display="none",f.style.display="",f.classList.add("terminal-pending"),O(),l&&(l.destroy(),l=null),c=e;try{l=await F(f,e,{terminal:h.terminal,scrollback:h.scrollback,theme:h.theme,renderer:h.renderer})}catch(t){console.error("[shell] terminal init failed:",t);let n=t&&typeof t=="object"&&"message"in t?t.message:String(t);f.textContent="Failed to open terminal for "+e+": "+n,f.classList.remove("terminal-pending")}}}var I=[];function U(){if(I=h.fsRoots||[],I.length===0){if(c){q(c);return}i.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}i.innerHTML="";for(let e of I){let t=document.createElement("div");t.className="file-tree-item",t.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${b(e)}`,t.addEventListener("click",()=>u(e)),i.appendChild(t)}}async function q(e){i.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let t=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();t.path?u(t.path):i.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{i.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function u(e){m=e,i.innerHTML='<div class="file-tree-empty">Loading...</div>';try{let t=await(await fetch("/api/fs/list?path="+encodeURIComponent(e))).json();i.innerHTML="";let n=A(e);if(n&&!I.some(s=>e===s)){let s=document.createElement("div");s.className="file-tree-item",s.innerHTML="..",s.addEventListener("click",()=>u(n)),i.appendChild(s)}if(t.dirs)for(let s of t.dirs){let p=s.split("/").pop()||s.split("\\").pop(),d=document.createElement("div");d.className="file-tree-item",d.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${b(p)}`,d.addEventListener("click",()=>u(s)),i.appendChild(d)}else i.innerHTML="";let a=t.files||[];for(let s of a){let p=s.split("/").pop()||s.split("\\").pop(),d=document.createElement("div");d.className="file-tree-item file",d.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${b(p)}`,d.addEventListener("click",()=>W(s)),i.appendChild(d)}}catch{i.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function A(e){let t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function W(e){B=e,f.style.display="none",l&&(l.destroy(),l=null,c=null),O(),g.style.display="none",k.style.display="flex",P.textContent=e,o.textContent="Loading...",y.style.display="none";try{let t=await fetch("/api/file?path="+encodeURIComponent(e));if(!t.ok){let a=await t.json();o.textContent=a.error||"Failed to load",E.value="";return}let n=await t.json();E.value=n.content,x=n.content,H=n.content,o.textContent=n.size+" bytes",y.style.display=""}catch{o.textContent="Failed to load",E.value=""}}E.addEventListener("input",()=>{x=E.value}),y.addEventListener("click",async()=>{y.textContent="Saving...",y.disabled=!0;try{let e=await fetch("/api/file",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:B,content:x})});if(!e.ok){let t=await e.json();o.textContent=t.error||"Save failed";return}H=x,o.textContent="Saved"}catch{o.textContent="Save failed"}finally{y.disabled=!1,y.textContent="Save"}}),R.addEventListener("click",async()=>{if(confirm("Delete "+B+"?"))try{let e=await fetch("/api/file/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:B})});if(!e.ok){let t=await e.json();o.textContent=t.error||"Delete failed";return}k.style.display="none",g.style.display="flex",m&&u(m)}catch{o.textContent="Delete failed"}}),V.addEventListener("click",()=>{k.style.display="none",g.style.display="flex",m&&u(m)}),$.addEventListener("click",async()=>{let e=S.value.trim();if(!e)return;let t=m+"/"+e;try{let n=await fetch("/api/file/touch",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!n.ok){let a=await n.json();o.textContent=a.error||"Create failed";return}S.value="",m&&u(m)}catch{o.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{let e=N.classList.toggle("open");T.classList.toggle("open",e)}),T.addEventListener("click",()=>{N.classList.remove("open"),T.classList.remove("open")});var v="",w=document.createElement("div");w.className="session-popover-backdrop",document.body.appendChild(w);var r=document.createElement("div");r.id="session-popover",r.className="session-popover",r.style.display="none",r.innerHTML=`<div class="sp-field">
|
|
5
5
|
<label>Rename session</label>
|
|
6
6
|
<input type="text" id="sp-name" />
|
|
7
7
|
</div>
|
|
@@ -10,4 +10,4 @@ import{a as F}from"./chunk-O3DBTEWH.js";var h=window.__TMUX_WEB_SHELL__,i=null,p
|
|
|
10
10
|
<button class="btn" id="sp-cancel">Cancel</button>
|
|
11
11
|
</div>
|
|
12
12
|
<hr class="sp-divider" />
|
|
13
|
-
<button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(
|
|
13
|
+
<button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(r);function X(e){let t=e.getBoundingClientRect();r.style.top=t.bottom+4+"px",r.style.left=Math.max(8,Math.min(t.left,window.innerWidth-240))+"px"}function Z(e,t){v=e;let n=document.getElementById("sp-name");n.value=e,w.classList.add("open"),r.style.display="block",X(t),setTimeout(()=>n.focus(),50)}function C(){w.classList.remove("open"),r.style.display="none"}w.addEventListener("click",C),document.getElementById("sp-save").addEventListener("click",async()=>{let e=document.getElementById("sp-name").value.trim();if(!e||e===v){C();return}try{if(!(await fetch("/api/sessions/rename",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:v,newName:e})})).ok)return;C(),L(),c===v&&M(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+v+'"?'))try{if(!(await fetch("/api/sessions/kill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:v})})).ok)return;C(),c===v&&(l&&(l.destroy(),l=null),c=null,f.style.display="none",g.style.display="flex"),L()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",C);var G={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ","ctrl-c":"","ctrl-d":"",enter:"\r",exit:"exit\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{let t=e.target.closest("button[data-key]");if(!t||!l)return;let n=G[t.dataset.key];n&&(l.sendInput(n),l.focus())}),window.__openSession=M,window.__refreshSidebar=L,L();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import{initTerminal as z}from"./terminal-core.js";const
|
|
2
|
-
<span>${w(
|
|
3
|
-
<button class="session-edit-btn" data-session="${w(
|
|
4
|
-
`,
|
|
1
|
+
import{initTerminal as z}from"./terminal-core.js";const g=window.__TMUX_WEB_SHELL__;let a=null,r=null,R="sessions",f="",k="",B="",H="";const s=document.getElementById("sidebar-content"),b=document.getElementById("main-placeholder"),u=document.getElementById("terminal-container"),S=document.getElementById("file-editor"),_=document.getElementById("fe-path"),c=document.getElementById("fe-status"),E=document.getElementById("fe-content"),y=document.getElementById("fe-save"),V=document.getElementById("fe-delete"),$=document.getElementById("fe-back"),N=document.getElementById("fe-new-name"),q=document.getElementById("fe-new-btn"),T=document.getElementById("settings-backdrop"),F=document.getElementById("settings-popover"),j=document.querySelector(".sidebar");function J(){return window.matchMedia("(max-width: 640px)").matches}function O(){J()&&j.classList.add("collapsed")}function D(){j.classList.remove("collapsed")}document.getElementById("mode-sessions").addEventListener("click",()=>{D(),P("sessions")}),document.getElementById("mode-files").addEventListener("click",()=>{D(),P("files")});function P(e){R=e,document.querySelectorAll(".mode-btn").forEach(n=>n.classList.remove("active"));const t=document.getElementById(e==="sessions"?"mode-sessions":"mode-files");t&&t.classList.add("active"),e==="sessions"?L():U()}async function L(){s.innerHTML='<div class="sidebar-section-label">Sessions</div><button class="new-session-sidebar-btn" id="ns-btn">+ New Session</button>';try{const p=(await(await fetch("/api/sidebar/sessions")).json()).recent||[];for(const m of p){const i=document.createElement("div");i.className="session-item"+(m.name===r?" active":""),i.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor"><path d="${m.attached?"M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H5V6h14v12z":"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"}"></svg>
|
|
2
|
+
<span>${w(m.name)}</span>
|
|
3
|
+
<button class="session-edit-btn" data-session="${w(m.name)}" title="Edit session"><svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg></button>
|
|
4
|
+
`,i.addEventListener("click",l=>{l.target.closest(".session-edit-btn")||M(m.name)}),s.appendChild(i);const o=i.querySelector(".session-edit-btn");o&&o.addEventListener("click",l=>{l.stopPropagation(),G(m.name,o)})}}catch{s.innerHTML+='<div class="file-tree-empty">Failed to load sessions</div>'}const e=document.getElementById("ns-btn");e&&e.addEventListener("click",()=>document.getElementById("new-session-modal")?.classList.add("open"))}function w(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}async function M(e){if(!(r===e&&a)){S.style.display="none",b.style.display="none",u.style.display="",u.classList.add("terminal-pending"),O(),a&&(a.destroy(),a=null),r=e;try{a=await z(u,e,{terminal:g.terminal,scrollback:g.scrollback,theme:g.theme,renderer:g.renderer})}catch(t){console.error("[shell] terminal init failed:",t);const n=t&&typeof t=="object"&&"message"in t?t.message:String(t);u.textContent="Failed to open terminal for "+e+": "+n,u.classList.remove("terminal-pending")}}}let I=[];function U(){if(I=g.fsRoots||[],I.length===0){if(r){A(r);return}s.innerHTML='<div class="file-tree-info">Open a session first, then switch to Files to browse its directory.</div>';return}s.innerHTML="";for(const e of I){const t=document.createElement("div");t.className="file-tree-item",t.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${w(e)}`,t.addEventListener("click",()=>v(e)),s.appendChild(t)}}async function A(e){s.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const n=await(await fetch("/api/fs/session-path?session="+encodeURIComponent(e))).json();n.path?v(n.path):s.innerHTML='<div class="file-tree-info">Could not determine session directory.</div>'}catch{s.innerHTML='<div class="file-tree-error">Failed to get session directory</div>'}}async function v(e){f=e,s.innerHTML='<div class="file-tree-empty">Loading...</div>';try{const n=await(await fetch("/api/fs/list?path="+encodeURIComponent(e))).json();s.innerHTML="";const p=W(e);if(p&&!I.some(o=>e===o)){const o=document.createElement("div");o.className="file-tree-item",o.innerHTML="..",o.addEventListener("click",()=>v(p)),s.appendChild(o)}if(n.dirs)for(const i of n.dirs){const o=i.split("/").pop()||i.split("\\").pop(),l=document.createElement("div");l.className="file-tree-item",l.innerHTML=`<svg viewBox="0 0 24 24" fill="currentColor" class="file-icon"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg> ${w(o)}`,l.addEventListener("click",()=>v(i)),s.appendChild(l)}else s.innerHTML="";const m=n.files||[];for(const i of m){const o=i.split("/").pop()||i.split("\\").pop(),l=document.createElement("div");l.className="file-tree-item file",l.innerHTML=`<span class="file-icon">\u{1F4C4}</span> ${w(o)}`,l.addEventListener("click",()=>X(i)),s.appendChild(l)}}catch{s.innerHTML='<div class="file-tree-error">Failed to load directory</div>'}}function W(e){const t=e.lastIndexOf("/");return t<=0?null:e.substring(0,t)}async function X(e){k=e,u.style.display="none",a&&(a.destroy(),a=null,r=null),O(),b.style.display="none",S.style.display="flex",_.textContent=e,c.textContent="Loading...",y.style.display="none";try{const t=await fetch("/api/file?path="+encodeURIComponent(e));if(!t.ok){const p=await t.json();c.textContent=p.error||"Failed to load",E.value="";return}const n=await t.json();E.value=n.content,B=n.content,H=n.content,c.textContent=n.size+" bytes",y.style.display=""}catch{c.textContent="Failed to load",E.value=""}}E.addEventListener("input",()=>{B=E.value}),y.addEventListener("click",async()=>{y.textContent="Saving...",y.disabled=!0;try{const e=await fetch("/api/file",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:k,content:B})});if(!e.ok){const t=await e.json();c.textContent=t.error||"Save failed";return}H=B,c.textContent="Saved"}catch{c.textContent="Save failed"}finally{y.disabled=!1,y.textContent="Save"}}),V.addEventListener("click",async()=>{if(confirm("Delete "+k+"?"))try{const e=await fetch("/api/file/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:k})});if(!e.ok){const t=await e.json();c.textContent=t.error||"Delete failed";return}S.style.display="none",b.style.display="flex",f&&v(f)}catch{c.textContent="Delete failed"}}),$.addEventListener("click",()=>{S.style.display="none",b.style.display="flex",f&&v(f)}),q.addEventListener("click",async()=>{const e=N.value.trim();if(!e)return;const t=f+"/"+e;try{const n=await fetch("/api/file/touch",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!n.ok){const p=await n.json();c.textContent=p.error||"Create failed";return}N.value="",f&&v(f)}catch{c.textContent="Create failed"}}),document.getElementById("mode-settings").addEventListener("click",()=>{const e=F.classList.toggle("open");T.classList.toggle("open",e)}),T.addEventListener("click",()=>{F.classList.remove("open"),T.classList.remove("open")});let h="";const C=document.createElement("div");C.className="session-popover-backdrop",document.body.appendChild(C);const d=document.createElement("div");d.id="session-popover",d.className="session-popover",d.style.display="none",d.innerHTML=`<div class="sp-field">
|
|
5
5
|
<label>Rename session</label>
|
|
6
6
|
<input type="text" id="sp-name" />
|
|
7
7
|
</div>
|
|
@@ -10,4 +10,4 @@ import{initTerminal as z}from"./terminal-core.js";const h=window.__TMUX_WEB_SHEL
|
|
|
10
10
|
<button class="btn" id="sp-cancel">Cancel</button>
|
|
11
11
|
</div>
|
|
12
12
|
<hr class="sp-divider" />
|
|
13
|
-
<button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(d);function
|
|
13
|
+
<button class="btn danger" id="sp-delete">Delete session</button>`,document.body.appendChild(d);function Z(e){const t=e.getBoundingClientRect();d.style.top=t.bottom+4+"px",d.style.left=Math.max(8,Math.min(t.left,window.innerWidth-240))+"px"}function G(e,t){h=e;const n=document.getElementById("sp-name");n.value=e,C.classList.add("open"),d.style.display="block",Z(t),setTimeout(()=>n.focus(),50)}function x(){C.classList.remove("open"),d.style.display="none"}C.addEventListener("click",x),document.getElementById("sp-save").addEventListener("click",async()=>{const e=document.getElementById("sp-name").value.trim();if(!e||e===h){x();return}try{if(!(await fetch("/api/sessions/rename",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldName:h,newName:e})})).ok)return;x(),L(),r===h&&M(e)}catch{}}),document.getElementById("sp-delete").addEventListener("click",async()=>{if(confirm('Delete session "'+h+'"?'))try{if(!(await fetch("/api/sessions/kill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:h})})).ok)return;x(),r===h&&(a&&(a.destroy(),a=null),r=null,u.style.display="none",b.style.display="flex"),L()}catch{}}),document.getElementById("sp-cancel").addEventListener("click",x);const K={esc:"\x1B",tab:" ","s-tab":"\x1B[Z",up:"\x1B[A",down:"\x1B[B",left:"\x1B[D",right:"\x1B[C",space:" ","ctrl-c":"","ctrl-d":"",enter:"\r",exit:"exit\r"};document.getElementById("mobile-keys").addEventListener("click",e=>{const t=e.target.closest("button[data-key]");if(!t||!a)return;const n=K[t.dataset.key];n&&(a.sendInput(n),a.focus())}),window.__openSession=M,window.__refreshSidebar=L,L();
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{chmodSync as $e,existsSync as K,mkdirSync as Be,readdirSync as pe,readFileSync as Ve,statSync as D,unlinkSync as Ge,writeFileSync as ze}from"node:fs";import{readFile as Ke}from"node:fs/promises";import{createRequire as Qe}from"node:module";import g from"node:path";import{fileURLToPath as Xe}from"node:url";import{Hono as Ye}from"hono";import{serve as Je}from"@hono/node-server";import{WebSocketServer as Ze,WebSocket as fe}from"ws";import*as et from"node-pty";try{const e=g.dirname(Qe(import.meta.url).resolve("node-pty/package.json")),t=g.join(e,"prebuilds");if(K(t))for(const r of pe(t)){const n=g.join(t,r,"spawn-helper");K(n)&&!(D(n).mode&73)&&$e(n,493)}}catch{}import{listSessions as v}from"./sessions.js";import{renderTerminal as tt,renderLoginPage as rt,renderNotesIndex as nt,renderNotesPage as ot,renderSettings as st,renderThemeSettings as at,renderScheduleIndex as it,renderAgentsIndex as dt,renderHistoryIndex as ut,renderQuickCommandsPage as ct,renderFilesIndex as lt,renderShell as mt}from"./frontend.js";import{loadSecurityConfig as pt,saveSecurityConfig as he}from"./lib/security-config.js";import{hashPassword as ye,verifyPassword as we,validatePassword as ge,TokenStore as ft}from"./lib/auth.js";import{RateLimiter as ht}from"./lib/rateLimiter.js";import{atomicWriteFileSync as yt}from"./lib/atomicWrite.js";import{resolveFsPath as Q,resolveFsRoots as je,MAX_FILE_BYTES as ke,walkRecursive as wt}from"./lib/fs-access.js";import{audit as c}from"./lib/auditLog.js";import{db as j}from"./lib/db.js";import{recordSessionAccess as
|
|
2
|
+
import{chmodSync as $e,existsSync as K,mkdirSync as Be,readdirSync as pe,readFileSync as Ve,statSync as D,unlinkSync as Ge,writeFileSync as ze}from"node:fs";import{readFile as Ke}from"node:fs/promises";import{createRequire as Qe}from"node:module";import g from"node:path";import{fileURLToPath as Xe}from"node:url";import{Hono as Ye}from"hono";import{serve as Je}from"@hono/node-server";import{WebSocketServer as Ze,WebSocket as fe}from"ws";import*as et from"node-pty";try{const e=g.dirname(Qe(import.meta.url).resolve("node-pty/package.json")),t=g.join(e,"prebuilds");if(K(t))for(const r of pe(t)){const n=g.join(t,r,"spawn-helper");K(n)&&!(D(n).mode&73)&&$e(n,493)}}catch{}import{listSessions as v}from"./sessions.js";import{renderTerminal as tt,renderLoginPage as rt,renderNotesIndex as nt,renderNotesPage as ot,renderSettings as st,renderThemeSettings as at,renderScheduleIndex as it,renderAgentsIndex as dt,renderHistoryIndex as ut,renderQuickCommandsPage as ct,renderFilesIndex as lt,renderShell as mt}from"./frontend.js";import{loadSecurityConfig as pt,saveSecurityConfig as he}from"./lib/security-config.js";import{hashPassword as ye,verifyPassword as we,validatePassword as ge,TokenStore as ft}from"./lib/auth.js";import{RateLimiter as ht}from"./lib/rateLimiter.js";import{atomicWriteFileSync as yt}from"./lib/atomicWrite.js";import{resolveFsPath as Q,resolveFsRoots as je,MAX_FILE_BYTES as ke,walkRecursive as wt}from"./lib/fs-access.js";import{captureSessionWindowsWithPath as gt}from"./lib/tmux-windows.js";import{audit as c}from"./lib/auditLog.js";import{db as j}from"./lib/db.js";import{recordSessionAccess as jt,getSessionAccessMap as A}from"./lib/session-access.js";import{listWindowHistory as kt,clearWindowHistory as vt}from"./lib/window-history.js";import{loadExtensions as xt,spawnExtensionBackend as St,registerExtensionRoutes as Tt}from"./lib/ext-loader.js";import{SchedulerService as bt,isValidScheduleInput as qt,isValidRescheduleInput as At}from"./lib/scheduler.js";import{getScheduleDelayError as ve}from"./lib/schedule-delay.js";import{handleClientMessage as It}from"./lib/ws-message.js";import{loadDotEnv as _t}from"./lib/load-env.js";import{cmdAdd as Ct,cmdRemove as Rt,cmdList as Et,cmdSetup as Mt,cmdTheme as Ot,printUsage as xe,printVersion as Pt}from"./lib/cli.js";import{readSettings as ne,writeSettings as Lt}from"./lib/settings.js";import{readActiveTheme as Nt,setActiveThemeTemplate as Se}from"./lib/theme-store.js";import{isThemeTemplateId as Te,THEME_TEMPLATE_IDS as Dt}from"./lib/themes/index.js";import{installPlugin as Wt,uninstallPlugin as Ht}from"./lib/plugins.js";import{buildCommandbarSessions as C}from"./lib/commandbar.js";import{pinView as Ut,unpinView as Ft,listPinnedViews as $t}from"./lib/pinned-views.js";import{listWindowLabels as Bt,setWindowLabel as Vt}from"./lib/window-labels.js";import{captureAndStoreWindows as oe,getStoredWindows as be}from"./lib/session-windows.js";import{acquireControlClient as Gt,killAllControlClients as zt}from"./lib/tmux-control.js";import{buildSidebarSessions as Kt}from"./lib/sessions-sidebar.js";import{createQuickCommand as Qt,deleteQuickCommand as Xt,listQuickCommands as se,updateQuickCommand as Yt}from"./lib/quick-commands.js";import{getSessionPaneTarget as Jt,capturePaneTail as Zt,capturePaneHistoryChunk as er,isAlternateScreen as tr,toCrlf as qe}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as rr}from"./lib/terminal-config.js";import{ImageUploadError as nr,saveUploadedImage as or}from"./lib/image-upload.js";import{listSessionWindows as sr,selectSessionWindow as ar,newSessionWindow as ir,renameSessionWindow as dr,newTmuxSession as ur,renameSession as cr,killSession as lr,TmuxWindowsError as X}from"./lib/tmux-windows.js";import{getActivePaneInfo as mr}from"./lib/tmux-panes.js";import{recordWatchedPane as pr}from"./lib/watched-panes.js";import{probeWatchedPanes as fr,startBackgroundWatch as hr,getCachedAgentStatuses as yr,requestProbe as wr}from"./lib/agents-watch.js";_t();const B=rr(),l=pt(),h=new ft,E=new ht;let W=!1;const Y="tmux-web-token",gr=365;function Ae(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}function J(e){if(l.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 jr(e){if(l.security.trustProxy){const t=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(t)return t}return e.socket.remoteAddress||"unknown"}function ae(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*)${Y}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function Ie(e){const t=e.headers.authorization;if(typeof t=="string"&&t.startsWith("Bearer "))return t.slice(7);const r=e.headers.cookie;if(typeof r!="string")return null;const n=r.match(new RegExp(`(?:^|;\\s*)${Y}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function xn(e){const t=ae(e);return t?h.validateToken(t):null}function Sn(e){const t=Ie(e);return t?h.validateToken(t):null}function _e(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${Y}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${gr*86400}${r?"; Secure":""}`)}function kr(e){e.header("Set-Cookie",`${Y}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}function i(){return async(e,t)=>{const r=ae(e);return!r||!h.validateToken(r)?(c("http_unauthorized",{ip:J(e)}),e.json({error:"unauthorized"},401)):t()}}function vr(e){const t=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${t}`,302)}function I(){return async(e,t)=>{const r=ae(e);return!r||!h.validateToken(r)?vr(e):t()}}const H=new Map;function xr(e){let t=0;for(const r of H.values())r.ip===e&&t++;return t}function ie(e,t,r){try{e.close(t,r)}catch{}}function Tn(e,t){e.readyState===fe.OPEN&&e.send(JSON.stringify(t))}function Sr(e,t){let r="xterm";(t==="ghostty"||t==="xterm")&&(r=t);const n=process.env.TMUX_WEB_TERMINAL_RENDERER?.trim().toLowerCase();(n==="ghostty"||n==="xterm")&&(r=n);for(const a of e)a==="--ghostty"&&(r="ghostty"),a==="--xterm"&&(r="xterm");return r}const Tr=7;function Ce(e){return typeof e!="number"||!Number.isFinite(e)?Tr:Math.min(365,Math.max(1,Math.round(e)))}const Re=process.argv.slice(2);{const e=Re.filter(t=>t!=="--ghostty"&&t!=="--xterm");if(e.length>0){const[t,r]=e;switch(t){case"add":r||(console.error("usage: tmux-web add <package>"),process.exit(1)),await Ct(r),process.exit(0);case"remove":case"rm":r||(console.error("usage: tmux-web remove <package>"),process.exit(1)),await Rt(r),process.exit(0);case"list":case"ls":await Et(),process.exit(0);case"setup":await Mt(e),process.exit(0);case"theme":await Ot(e.slice(1)),process.exit(0);case"help":case"--help":case"-h":xe(),process.exit(0);case"-V":case"--version":case"-v":Pt(),process.exit(0);default:console.error(`unknown argument: ${t}`),xe(),process.exit(1)}}}function x(e,t){e.readyState===fe.OPEN&&e.send(JSON.stringify(t))}const U=new Set,Ee=[];await j.read(),j.data.sessionAccess??=[],j.data.pinnedViews??=[],j.data.watchedPanes??=[],j.data.triggeredTasks??=[],j.data.quickCommands??=[];const V=await ne();let y=await Nt();const p=V.commandbar===!0,_=V.agents===!0,Me=_&&V.agentsBackgroundWatch===!0,Z=Sr(Re,V.terminalRenderer),Oe=Ce(V.scheduleHistoryDays),Pe=g.join(process.cwd(),"extensions"),de=await xt(Pe);for(const e of de)e.start&&Ee.push(St(e.dir,e));const M=new bt({db:j,historyRetentionMs:Oe*864e5,onMissedTask:e=>console.warn(`[scheduler] dropped missed task ${e.id} (was due ${new Date(e.fireAt).toISOString()})`)});await M.restoreFromDb();function ee(e){if(_)try{const t=mr(e);if(!t)return;pr({paneId:t.paneId,sessionName:e,windowIndex:t.windowIndex,paneIndex:t.paneIndex}).catch(()=>{})}catch{}}const s=new Ye;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'","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,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()}),Tt(s,Pe,de);const br=g.dirname(Xe(import.meta.url)),qr=[g.join(br,"assets"),g.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 qr){const n=g.join(r,t);if(!K(n))continue;const a=await Ke(n),o=g.extname(t),d={".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":d[o]??"application/octet-stream"})}return e.notFound()});const Ar=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
3
3
|
<rect x="2.5" y="5.5" width="27" height="21" rx="4" fill="#0d1117" stroke="#7dd3fc" stroke-width="2"/>
|
|
4
4
|
<path d="M8 13l4.2 3.1L8 19.2" fill="none" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
5
|
<line x1="15.5" y1="19.6" x2="22.5" y2="19.6" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round"/>
|
|
6
|
-
</svg>`,Le=e=>e.body(Ar,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});o.get("/favicon.svg",Le),o.get("/favicon.ico",Le),o.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!l.passwordHash,n=e.req.query("error");return e.html(rt({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:y}))}),o.get("/",I(),e=>{const t=p?C(v(),q()):[],r=je();return e.html(mt({theme:y,commandbarEnabled:p,commandbarSessions:t,agentsEnabled:_,fsRoots:r,terminalCfg:B,renderer:Z,scrollback:B.initialLines+2*B.historyChunk}))}),o.get("/s/:session",I(),async e=>{const t=decodeURIComponent(e.req.param("session"));await gt(t),oe(t);const r=v(),n=q(),a=p?C(r,n):[];return e.html(tt(t,de,{commandbarEnabled:p,commandbarSessions:a,quickCommands:p?se():[],agentsEnabled:_,terminal:B,theme:y,renderer:Z}))}),o.get("/notes",I(),e=>{const t=p?C(v(),q()):[];return e.html(nt(j.data.notes,y,p,t,_))}),o.get("/notes/:session",I(),e=>{const t=decodeURIComponent(e.req.param("session")),r=p?C(v(),q()):[];return e.html(ot(t,y,p,r))}),o.get("/schedule",I(),e=>{const t=p?C(v(),q()):[];return e.html(it(M.list(),M.listTriggered(),y,Oe,p,t,_))}),o.get("/agents",I(),e=>{if(!_)return e.redirect("/settings",303);const t=p?C(v(),q()):[];return e.html(dt(y,p,t))}),o.get("/history",I(),e=>{const t=v(),r=p?C(t,q()):[],n=new Set(t.map(a=>a.name));return e.html(ut(jt(),y,p,r,_,n))}),o.get("/quick-commands",I(),e=>{const t=p?C(v(),q()):[];return e.html(ct(se(),y,p,t,_))}),o.get("/files",I(),e=>{const t=p?C(v(),q()):[],r=je();return e.html(lt(y,p,t,_,r))}),o.post("/api/history/clear",i(),async e=>(await kt(),e.json({ok:!0}))),o.get("/api/quick-commands",i(),e=>e.json(se())),o.post("/api/quick-commands",i(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Kt(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),o.patch("/api/quick-commands/:id",i(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Xt(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),o.delete("/api/quick-commands/:id",i(),async e=>await Qt(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),o.get("/api/agents",i(),async e=>{if(!_)return e.json({error:"agents page disabled"},403);const t=Me?hr():await pr();return e.json(t)}),o.post("/api/auth/password",async e=>{const t=J(e),r=E.check(t);if(!r.allowed)return c("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(!l.passwordHash){if(!l.security.allowRemoteSetup&&!qe(t))return c("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(U)return e.json({error:"Password setup in progress"},409);const f=ge(a);if(f)return e.json({error:f},400);U=!0;try{l.passwordHash=await ye(a),he(l),c("password_set",{ip:t})}finally{U=!1}const{plaintext:b}=h.createAccessToken("setup",l.security.tokenTtlDays);return _e(e,b),e.json({ok:!0,token:b,setupMode:!0})}if(!await we(a,l.passwordHash)){const f=E.recordFailure(t);return c("auth_failed",{ip:t,method:"password",failures:f.failures,permanentLock:f.permanentLock}),f.permanentLock?(h.revokeAll(),c("permanent_lock",{ip:t,failures:f.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):f.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:f.retryAfterMs},429)}E.recordSuccess(t);const d=`browser-${t}`,{plaintext:T}=h.createAccessToken(d,l.security.tokenTtlDays);return _e(e,T),c("auth_success",{ip:t,method:"password",tokenName:d}),e.json({ok:!0,token:T})}),o.post("/api/auth/token",i(),async e=>{const t=J(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:l.security.tokenTtlDays,{stored:s,plaintext:d}=h.createAccessToken(n,a);return c("token_created",{ip:t,name:s.name,tokenId:s.id}),e.json({id:s.id,name:s.name,token:d,expiresAt:s.expiresAt})}),o.get("/api/auth/tokens",i(),e=>e.json(h.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),o.delete("/api/auth/tokens/:id",i(),e=>h.revoke(e.req.param("id"))?(c("token_revoked",{ip:J(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),o.post("/api/auth/logout",i(),e=>(jr(e),e.json({ok:!0}))),o.get("/settings",I(),async e=>{const t=await ne(),r=t.terminalRenderer??"xterm";return e.html(st({settings:t,renderer:Z,rendererOverridden:Z!==r,theme:y,plugins:t.plugins??[],saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),o.post("/settings",i(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await ne(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",a=t.defaultView==="recent"?"recent":"default",s=Ce(typeof t.scheduleHistoryDays=="string"?Number(t.scheduleHistoryDays):void 0);return await Pt({...r,commandbar:t.commandbar!==void 0,agents:t.agents!==void 0,agentsBackgroundWatch:t.agentsBackgroundWatch!==void 0,terminalRenderer:n,defaultView:a,scheduleHistoryDays:s}),e.redirect("/settings?saved=1",303)}),o.post("/settings/plugins",i(),async e=>{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 a=r==="remove"?await Ut(n):r==="add"?await Dt(n):{ok:!1,output:"unknown action"};return a.ok?e.redirect("/settings?saved=1",303):e.redirect("/settings?error="+encodeURIComponent(a.output.slice(0,800)),303)}),o.get("/settings/theme",I(),e=>e.html(at({theme:y,saved:e.req.query("saved")==="1"}))),o.post("/settings/theme",i(),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"||!be(r)?e.redirect("/settings/theme",303):(y=await Te(r),e.redirect("/settings/theme?saved=1",303))});const qr={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};o.get("/api/theme",i(),e=>e.json({active:y.template,templates:Nt.map(t=>({id:t,name:qr[t]}))})),o.post("/api/theme",i(),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"||!be(r)?e.json({error:"invalid theme template"},400):(y=await Te(r),e.json({ok:!0,active:y.template}))}),o.get("/api/sessions",i(),e=>p?e.json(C(v(),q())):e.json({error:"commandbar disabled"},404)),o.post("/api/sessions/new",i(),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(v().some(s=>s.name===r))return e.json({error:"session already exists"},409);try{return dr(r,n),e.json({ok:!0})}catch(s){const d=s instanceof X?s.message:"failed to create session";return e.json({error:d},500)}}),o.post("/api/sessions/rename",i(),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)?(ur(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),o.post("/api/sessions/kill",i(),async e=>{try{const{name:t}=await e.req.json();return t?(cr(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),o.get("/api/fs/list",i(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=g.join(t,r));const n=e.req.query("recursive")==="true";let a=r,s="",d=r.endsWith("/");if(!d)try{d=D(r).isDirectory()}catch{}d||(a=g.dirname(r),s=g.basename(r).toLowerCase());try{const T=pe(a),f=[],b=[];for(const O of T)if(!O.startsWith(".")&&!(s&&!O.toLowerCase().startsWith(s))){try{const S=g.join(a,O);D(S).isDirectory()?(f.push(S),n&&wt(S,f,b,0)):b.push(S)}catch{}if(f.length+b.length>=5e3)break}return e.json({dirs:f,files:b})}catch{return e.json({dirs:[],files:[]})}}),o.get("/api/file",i(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=Q(t);if(!D(r).isFile())return e.json({error:"not a file"},400);const n=D(r).size;if(n>ke)return e.json({error:"file too large",size:n,maxBytes:ke},413);const a=Ve(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)}}),o.put("/api/file",i(),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=Q(t.path);return yt(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)}}),o.post("/api/file/delete",i(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=Q(t.path);return D(r).isFile()?(Ge(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):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"delete failed"},500)}}),o.post("/api/file/touch",i(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=Q(t.path);return K(r)?e.json({error:"file already exists"},409):(Be(g.dirname(r),{recursive:!0}),ze(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)}});function ue(e){return{...zt(v(),q(),Ht()),currentSession:e??null}}function Ne(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}}o.get("/api/sidebar/sessions",i(),e=>{const t=e.req.query("currentSession");return e.json(ue(typeof t=="string"&&t?t:void 0))}),o.get("/api/sidebar/session-windows/:session",i(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(Se(t))}),o.post("/api/pinned-views",i(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Ne(t);return"error"in r?e.json({error:r.error},400):(await Wt(r.sessionName,r.windowIndex),e.json(ue()))}),o.delete("/api/pinned-views",i(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Ne(t);return"error"in r?e.json({error:r.error},400):(await Ft(r.sessionName,r.windowIndex),e.json(ue()))}),o.get("/api/notes",i(),e=>{const t=[...j.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),o.get("/api/notes/:scope",i(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=j.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),o.put("/api/notes/:scope",i(),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=j.data.notes.findIndex(s=>s.scope===t);return a>=0?j.data.notes[a]=n:j.data.notes.push(n),await j.write(),e.json({ok:!0})}),o.post("/api/session/:session/upload",i(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!v().some(s=>s.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 a=n.file;if(!(a instanceof File))return e.json({error:"missing file field"},400);try{const s=await a.arrayBuffer(),d=Buffer.from(s),{path:T}=await nr(d,a.type||void 0,a.name||void 0);return e.json({path:T})}catch(s){return s instanceof rr?e.json({error:s.message},s.status):(console.error("[upload]",s),e.json({error:"upload failed"},500))}}),o.get("/api/session/:session/windows",i(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map($t(t).map(s=>[s.windowIndex,s.label])),n=new Map(Se(t).map(s=>[s.index,s])),a=or(t).map(s=>({...s,label:r.get(s.index)??null,worktree:n.get(s.index)?.worktree??!1}));return e.json(a)}),o.post("/api/session/:session/window-label",i(),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 a=typeof r.label=="string"?r.label:"",s=await Bt(t,n,a);return e.json(s)}),o.post("/api/session/:session/select-window",i(),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 sr(t,n),ee(t),e.json({ok:!0})}catch(a){return a instanceof X?e.json({error:a.message},a.status):(console.error("[select-window]",a),e.json({error:"select-window failed"},500))}}),o.post("/api/session/:session/rename-window",i(),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 ir(t,n,r.name),oe(t),e.json({ok:!0})}catch(a){return a instanceof X?e.json({error:a.message},a.status):(console.error("[rename-window]",a),e.json({error:"rename-window failed"},500))}}),o.post("/api/session/:session/new-window",i(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return ar(t),ee(t),oe(t),e.json({ok:!0})}catch(r){return r instanceof X?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),o.get("/api/schedule",i(),e=>e.json(M.list(e.req.query("session")))),o.post("/api/schedule",i(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ve(t);if(r)return e.json({error:r},400);if(!St(t))return e.json({error:"invalid body"},400);const n=await M.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),o.delete("/api/schedule/:id",i(),async e=>await M.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),o.patch("/api/schedule/:id",i(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ve(t);if(r)return e.json({error:r},400);if(!At(t))return e.json({error:"invalid body"},400);const n=await M.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)});const Ir=parseInt(process.env.PORT||"21000",10),_r=Je({fetch:o.fetch,port:Ir},e=>{console.log(`tmux-web running at http://localhost:${e.port}`)});Me&&fr();const ce=new Ze({noServer:!0});function De(e,t,r){e.write(`HTTP/1.1 ${t} ${r}\r
|
|
6
|
+
</svg>`,Le=e=>e.body(Ar,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});s.get("/favicon.svg",Le),s.get("/favicon.ico",Le),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!l.passwordHash,n=e.req.query("error");return e.html(rt({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:y}))}),s.get("/",I(),e=>{const t=p?C(v(),A()):[],r=je();return e.html(mt({theme:y,commandbarEnabled:p,commandbarSessions:t,agentsEnabled:_,fsRoots:r,terminalCfg:B,renderer:Z,scrollback:B.initialLines+2*B.historyChunk}))}),s.get("/s/:session",I(),async e=>{const t=decodeURIComponent(e.req.param("session"));await jt(t),oe(t);const r=v(),n=A(),a=p?C(r,n):[];return e.html(tt(t,de,{commandbarEnabled:p,commandbarSessions:a,quickCommands:p?se():[],agentsEnabled:_,terminal:B,theme:y,renderer:Z}))}),s.get("/notes",I(),e=>{const t=p?C(v(),A()):[];return e.html(nt(j.data.notes,y,p,t,_))}),s.get("/notes/:session",I(),e=>{const t=decodeURIComponent(e.req.param("session")),r=p?C(v(),A()):[];return e.html(ot(t,y,p,r))}),s.get("/schedule",I(),e=>{const t=p?C(v(),A()):[];return e.html(it(M.list(),M.listTriggered(),y,Oe,p,t,_))}),s.get("/agents",I(),e=>{if(!_)return e.redirect("/settings",303);const t=p?C(v(),A()):[];return e.html(dt(y,p,t))}),s.get("/history",I(),e=>{const t=v(),r=p?C(t,A()):[],n=new Set(t.map(a=>a.name));return e.html(ut(kt(),y,p,r,_,n))}),s.get("/quick-commands",I(),e=>{const t=p?C(v(),A()):[];return e.html(ct(se(),y,p,t,_))}),s.get("/files",I(),e=>{const t=p?C(v(),A()):[],r=je();return e.html(lt(y,p,t,_,r))}),s.post("/api/history/clear",i(),async e=>(await vt(),e.json({ok:!0}))),s.get("/api/quick-commands",i(),e=>e.json(se())),s.post("/api/quick-commands",i(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Qt(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",i(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Yt(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",i(),async e=>await Xt(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.get("/api/agents",i(),async e=>{if(!_)return e.json({error:"agents page disabled"},403);const t=Me?yr():await fr();return e.json(t)}),s.post("/api/auth/password",async e=>{const t=J(e),r=E.check(t);if(!r.allowed)return c("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(!l.passwordHash){if(!l.security.allowRemoteSetup&&!Ae(t))return c("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(W)return e.json({error:"Password setup in progress"},409);const f=ge(a);if(f)return e.json({error:f},400);W=!0;try{l.passwordHash=await ye(a),he(l),c("password_set",{ip:t})}finally{W=!1}const{plaintext:T}=h.createAccessToken("setup",l.security.tokenTtlDays);return _e(e,T),e.json({ok:!0,token:T,setupMode:!0})}if(!await we(a,l.passwordHash)){const f=E.recordFailure(t);return c("auth_failed",{ip:t,method:"password",failures:f.failures,permanentLock:f.permanentLock}),f.permanentLock?(h.revokeAll(),c("permanent_lock",{ip:t,failures:f.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):f.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:f.retryAfterMs},429)}E.recordSuccess(t);const d=`browser-${t}`,{plaintext:S}=h.createAccessToken(d,l.security.tokenTtlDays);return _e(e,S),c("auth_success",{ip:t,method:"password",tokenName:d}),e.json({ok:!0,token:S})}),s.post("/api/auth/token",i(),async e=>{const t=J(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:l.security.tokenTtlDays,{stored:o,plaintext:d}=h.createAccessToken(n,a);return c("token_created",{ip:t,name:o.name,tokenId:o.id}),e.json({id:o.id,name:o.name,token:d,expiresAt:o.expiresAt})}),s.get("/api/auth/tokens",i(),e=>e.json(h.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",i(),e=>h.revoke(e.req.param("id"))?(c("token_revoked",{ip:J(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",i(),e=>(kr(e),e.json({ok:!0}))),s.get("/settings",I(),async e=>{const t=await ne(),r=t.terminalRenderer??"xterm";return e.html(st({settings:t,renderer:Z,rendererOverridden:Z!==r,theme:y,plugins:t.plugins??[],saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",i(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await ne(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",a=t.defaultView==="recent"?"recent":"default",o=Ce(typeof t.scheduleHistoryDays=="string"?Number(t.scheduleHistoryDays):void 0);return await Lt({...r,commandbar:t.commandbar!==void 0,agents:t.agents!==void 0,agentsBackgroundWatch:t.agentsBackgroundWatch!==void 0,terminalRenderer:n,defaultView:a,scheduleHistoryDays:o}),e.redirect("/settings?saved=1",303)}),s.post("/settings/plugins",i(),async e=>{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 a=r==="remove"?await Ht(n):r==="add"?await Wt(n):{ok:!1,output:"unknown action"};return a.ok?e.redirect("/settings?saved=1",303):e.redirect("/settings?error="+encodeURIComponent(a.output.slice(0,800)),303)}),s.get("/settings/theme",I(),e=>e.html(at({theme:y,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",i(),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"||!Te(r)?e.redirect("/settings/theme",303):(y=await Se(r),e.redirect("/settings/theme?saved=1",303))});const Ir={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",i(),e=>e.json({active:y.template,templates:Dt.map(t=>({id:t,name:Ir[t]}))})),s.post("/api/theme",i(),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"||!Te(r)?e.json({error:"invalid theme template"},400):(y=await Se(r),e.json({ok:!0,active:y.template}))}),s.get("/api/sessions",i(),e=>p?e.json(C(v(),A())):e.json({error:"commandbar disabled"},404)),s.post("/api/sessions/new",i(),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(v().some(o=>o.name===r))return e.json({error:"session already exists"},409);try{return ur(r,n),e.json({ok:!0})}catch(o){const d=o instanceof X?o.message:"failed to create session";return e.json({error:d},500)}}),s.post("/api/sessions/rename",i(),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)?(cr(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",i(),async e=>{try{const{name:t}=await e.req.json();return t?(lr(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",i(),e=>{const t=e.req.query("session");if(!t)return e.json({error:"session is required"},400);try{const r=gt(t),a=r.find(o=>o.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",i(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=g.join(t,r));const n=e.req.query("recursive")==="true";let a=r,o="",d=r.endsWith("/");if(!d)try{d=D(r).isDirectory()}catch{}d||(a=g.dirname(r),o=g.basename(r).toLowerCase());try{const S=pe(a),f=[],T=[];for(const O of S)if(!O.startsWith(".")&&!(o&&!O.toLowerCase().startsWith(o))){try{const b=g.join(a,O);D(b).isDirectory()?(f.push(b),n&&wt(b,f,T,0)):T.push(b)}catch{}if(f.length+T.length>=5e3)break}return e.json({dirs:f,files:T})}catch{return e.json({dirs:[],files:[]})}}),s.get("/api/file",i(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=Q(t);if(!D(r).isFile())return e.json({error:"not a file"},400);const n=D(r).size;if(n>ke)return e.json({error:"file too large",size:n,maxBytes:ke},413);const a=Ve(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",i(),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=Q(t.path);return yt(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",i(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=Q(t.path);return D(r).isFile()?(Ge(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):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"delete failed"},500)}}),s.post("/api/file/touch",i(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=Q(t.path);return K(r)?e.json({error:"file already exists"},409):(Be(g.dirname(r),{recursive:!0}),ze(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)}});function ue(e){return{...Kt(v(),A(),$t()),currentSession:e??null}}function Ne(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}}s.get("/api/sidebar/sessions",i(),e=>{const t=e.req.query("currentSession");return e.json(ue(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",i(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(be(t))}),s.post("/api/pinned-views",i(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Ne(t);return"error"in r?e.json({error:r.error},400):(await Ut(r.sessionName,r.windowIndex),e.json(ue()))}),s.delete("/api/pinned-views",i(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Ne(t);return"error"in r?e.json({error:r.error},400):(await Ft(r.sessionName,r.windowIndex),e.json(ue()))}),s.get("/api/notes",i(),e=>{const t=[...j.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",i(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=j.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",i(),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=j.data.notes.findIndex(o=>o.scope===t);return a>=0?j.data.notes[a]=n:j.data.notes.push(n),await j.write(),e.json({ok:!0})}),s.post("/api/session/:session/upload",i(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!v().some(o=>o.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 a=n.file;if(!(a instanceof File))return e.json({error:"missing file field"},400);try{const o=await a.arrayBuffer(),d=Buffer.from(o),{path:S}=await or(d,a.type||void 0,a.name||void 0);return e.json({path:S})}catch(o){return o instanceof nr?e.json({error:o.message},o.status):(console.error("[upload]",o),e.json({error:"upload failed"},500))}}),s.get("/api/session/:session/windows",i(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map(Bt(t).map(o=>[o.windowIndex,o.label])),n=new Map(be(t).map(o=>[o.index,o])),a=sr(t).map(o=>({...o,label:r.get(o.index)??null,worktree:n.get(o.index)?.worktree??!1}));return e.json(a)}),s.post("/api/session/:session/window-label",i(),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 a=typeof r.label=="string"?r.label:"",o=await Vt(t,n,a);return e.json(o)}),s.post("/api/session/:session/select-window",i(),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 ar(t,n),ee(t),e.json({ok:!0})}catch(a){return a instanceof X?e.json({error:a.message},a.status):(console.error("[select-window]",a),e.json({error:"select-window failed"},500))}}),s.post("/api/session/:session/rename-window",i(),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 dr(t,n,r.name),oe(t),e.json({ok:!0})}catch(a){return a instanceof X?e.json({error:a.message},a.status):(console.error("[rename-window]",a),e.json({error:"rename-window failed"},500))}}),s.post("/api/session/:session/new-window",i(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return ir(t),ee(t),oe(t),e.json({ok:!0})}catch(r){return r instanceof X?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),s.get("/api/schedule",i(),e=>e.json(M.list(e.req.query("session")))),s.post("/api/schedule",i(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ve(t);if(r)return e.json({error:r},400);if(!qt(t))return e.json({error:"invalid body"},400);const n=await M.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),s.delete("/api/schedule/:id",i(),async e=>await M.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.patch("/api/schedule/:id",i(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ve(t);if(r)return e.json({error:r},400);if(!At(t))return e.json({error:"invalid body"},400);const n=await M.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)});const _r=parseInt(process.env.PORT||"21000",10),Cr=Je({fetch:s.fetch,port:_r,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)});Me&&hr();const ce=new Ze({noServer:!0});function De(e,t,r){e.write(`HTTP/1.1 ${t} ${r}\r
|
|
7
7
|
\r
|
|
8
|
-
`),e.destroy()}
|
|
8
|
+
`),e.destroy()}Cr.on("upgrade",(e,t,r)=>{const n=new URL(e.url||"/",`http://${e.headers.host}`),a=n.pathname.match(/^\/ws\/(.+)$/);if(!a){t.destroy();return}const o=jr(e),d=e.headers.origin,S=l.security.allowedOrigins;if(d&&!(d===`http://${e.headers.host}`||d===`https://${e.headers.host}`)&&(S.length===0||!S.includes(d))){c("ws_rejected_origin",{ip:o,origin:d}),De(t,403,"Origin not allowed");return}const f=xr(o);if(f>=l.security.maxConnectionsPerIp){c("ws_rejected_per_ip_cap",{ip:o,liveFromIp:f}),De(t,429,"Too many connections");return}const T=Ie(e)||n.searchParams.get("token")||"",O=T?h.validateToken(T):null;ce.handleUpgrade(e,t,r,b=>{ce.emit("connection",b,e,decodeURIComponent(a[1]),o,O)})}),ce.on("connection",(e,t,r,n,a)=>{const o={ws:e,ip:n,authenticated:!1,authTimeout:null};H.set(e,o),c("ws_connected",{ip:n});let d=null;const{initialLines:S,historyChunk:f,syncIdleMs:T,syncMaxMs:O}=B;let b=!0,N=null,G=null,F=r,P=null;const z=()=>{N&&(clearTimeout(N),N=null),G&&(clearTimeout(G),G=null)},le=()=>{if(b){z(),b=!1;try{F=Jt(r)}catch{F=r}if(ee(r),!tr(F))try{const w=Zt(F,S);x(e,{type:"snapshot",data:qe(w),lines:S})}catch{}}},He=()=>{b&&(N&&clearTimeout(N),N=setTimeout(le,T))};function me(){try{d=et.spawn("tmux",["attach-session","-t",r],{name:"xterm-256color",cols:80,rows:24,cwd:process.env.HOME||"/",env:process.env})}catch(u){x(e,{type:"data",data:`\r
|
|
9
9
|
\x1B[31mFailed to attach to tmux session "${r}": ${u.message}\x1B[0m\r
|
|
10
|
-
`}),ie(e,1011,"pty spawn failed");return}
|
|
10
|
+
`}),ie(e,1011,"pty spawn failed");return}U.add(d);let w=-1,m="";P=Gt(r,({activeIndex:u,windows:k})=>{x(e,{type:"window_changed",activeIndex:u,windows:k});const L=k.map(q=>q.index).join(","),$=u!==w||L!==m;w=u,m=L,$&&(ee(r),wr())}),G=setTimeout(le,O),d.onData(u=>{if(b){He();return}x(e,{type:"data",data:u})}),d.onExit(({exitCode:u})=>{z(),d&&U.delete(d),d=null,x(e,{type:"data",data:`\r
|
|
11
11
|
\x1B[2m--- tmux exited (code ${u}) ---\x1B[0m\r
|
|
12
|
-
`}),ie(e,1e3,"pty exited")})}function te(w,m){
|
|
12
|
+
`}),ie(e,1e3,"pty exited")})}function te(w,m){o.authenticated=!0,o.authTimeout&&(clearTimeout(o.authTimeout),o.authTimeout=null),c("auth_success",{ip:n,method:w,tokenName:m}),x(e,{type:"auth.ok",setupMode:!l.passwordHash}),me()}function R(w,m={}){x(e,{type:"auth.failed",message:w,...m})}async function Ue(w){let m;try{m=JSON.parse(w)}catch{return!1}if(m.type==="auth.token"&&typeof m.token=="string"){const u=h.validateToken(m.token);if(!u){c("token_auth_failed",{ip:n});const k=E.recordFailure(n);return k.permanentLock&&(h.revokeAll(),c("permanent_lock",{ip:n,failures:k.failures})),R("Invalid or expired token",{permanentLock:k.permanentLock,retryAfterMs:k.retryAfterMs}),!0}return h.touch(u.tokenHash),te("token",u.name),!0}if(m.type==="auth"&&typeof m.password=="string"){const u=E.check(n);if(!u.allowed)return c("rate_limited",{ip:n,retryAfterMs:u.retryAfterMs,permanentLock:u.permanentLock}),R("Too many attempts",{permanentLock:u.permanentLock,retryAfterMs:u.retryAfterMs}),!0;if(!l.passwordHash){if(!l.security.allowRemoteSetup&&!Ae(n))return c("setup_rejected_remote",{ip:n}),R("First-run setup must be performed from localhost"),!0;if(W)return R("Password setup in progress"),!0;const q=ge(m.password);if(q)return R(q),!0;W=!0;try{l.passwordHash=await ye(m.password),he(l),c("password_set",{ip:n})}finally{W=!1}const{stored:re,plaintext:Fe}=h.createAccessToken("setup",l.security.tokenTtlDays);return x(e,{type:"auth.ok",setupMode:!0,token:Fe}),c("token_created",{ip:n,name:re.name,tokenId:re.id}),c("auth_success",{ip:n,method:"password",tokenName:re.name}),me(),!0}if(!l.passwordHash)return R("Server not configured"),!0;if(!await we(m.password,l.passwordHash)){const q=E.recordFailure(n);return c("auth_failed",{ip:n,method:"password",failures:q.failures,permanentLock:q.permanentLock}),q.permanentLock&&(h.revokeAll(),c("permanent_lock",{ip:n,failures:q.failures})),R("Incorrect password",{permanentLock:q.permanentLock,retryAfterMs:q.retryAfterMs}),!0}E.recordSuccess(n);const L=`ws-${n}`,{plaintext:$}=h.createAccessToken(L,l.security.tokenTtlDays);return te("password",L),x(e,{type:"auth.ok",setupMode:!1,token:$}),!0}return!1}a?(h.touch(a.tokenHash),te("token",a.name)):(x(e,{type:"auth.required",setupMode:!l.passwordHash}),o.authTimeout=setTimeout(()=>{o.authenticated||(c("auth_timeout",{ip:n}),R("Authentication timeout"),ie(e,4e3,"Auth timeout"))},l.security.authTimeoutMs)),e.on("message",async w=>{const m=typeof w=="string"?w:w.toString("utf-8");if(!o.authenticated){if(m.length>1e6){x(e,{type:"auth.failed",message:"Message too large"});return}await Ue(m)||R("Authentication required");return}if(!d)return;try{if(It(m,d))return}catch{return}let u;try{u=JSON.parse(m)}catch{return}if(u.type==="load_history"&&typeof u.before=="number"){const k=Math.max(0,Math.floor(u.before));try{const{data:L,lines:$}=er(F,k,f);x(e,{type:"history",data:qe(L),before:k,lines:$})}catch{x(e,{type:"history",data:"",before:k,lines:0})}}}),e.on("close",()=>{o.authTimeout&&clearTimeout(o.authTimeout),H.delete(e),c("ws_disconnected",{ip:n}),z(),P&&(P(),P=null),d&&(U.delete(d),d.kill(),d=null)}),e.on("error",()=>{o.authTimeout&&clearTimeout(o.authTimeout),H.delete(e),z(),P&&(P(),P=null),d&&(U.delete(d),d.kill(),d=null)})});function We(){M.cleanup(),zt();for(const[e,t]of H){try{e.close(1001,"Server shutting down")}catch{}t.authTimeout&&clearTimeout(t.authTimeout)}H.clear();for(const e of U)try{e.kill()}catch{}U.clear();for(const e of Ee)try{e.kill("SIGTERM")}catch{}E.dispose(),process.exit(0)}process.on("SIGINT",We),process.on("SIGTERM",We),setInterval(()=>{h.purgeExpired()},3600*1e3).unref();
|
package/dist/lib/fs-access.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:path";import{readdirSync as
|
|
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};
|
package/dist/lib/pages/shell.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cssVarsStyle as g}from"../theme.js";import{commandbarCSS as x,commandbarHTML as m,commandbarScript as f}from"../commandbar.js";import{newSessionModalCSS as
|
|
1
|
+
import{cssVarsStyle as g}from"../theme.js";import{commandbarCSS as x,commandbarHTML as m,commandbarScript as f}from"../commandbar.js";import{newSessionModalCSS as v,newSessionModalHTML as h,newSessionModalScript as u,reducedMotion as y}from"../shared-layout.js";import{icon as o}from"../icons.js";function C(n){return n.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function a(n="var(--panel-accent)"){return`box-shadow: 0 0 0 2px ${n}; outline: none;`}function _(n){const{theme:e,commandbarEnabled:t,commandbarSessions:s,agentsEnabled:r,fsRoots:d,terminalCfg:l,renderer:i,scrollback:p}=n,c=`
|
|
2
2
|
*, *::before, *::after { box-sizing: border-box; }
|
|
3
3
|
html, body {
|
|
4
4
|
background: var(--page-bg); color: var(--page-fg);
|
|
@@ -28,6 +28,7 @@ import{cssVarsStyle as g}from"../theme.js";import{commandbarCSS as x,commandbarH
|
|
|
28
28
|
display: flex;
|
|
29
29
|
height: 100vh;
|
|
30
30
|
padding-top: 48px;
|
|
31
|
+
transition: height 0.05s;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/* \u2500\u2500 Sidebar \u2500\u2500 */
|
|
@@ -302,7 +303,7 @@ import{cssVarsStyle as g}from"../theme.js";import{commandbarCSS as x,commandbarH
|
|
|
302
303
|
/* \u2500\u2500 Commandbar \u2500\u2500 */
|
|
303
304
|
${t?x():""}
|
|
304
305
|
${y()}
|
|
305
|
-
${
|
|
306
|
+
${v()}
|
|
306
307
|
@media (max-width: 640px) {
|
|
307
308
|
.app-layout { flex-direction: column; }
|
|
308
309
|
.sidebar { flex: 0 0 auto; border-right: none; border-bottom: 1px solid var(--panel-border); transition: max-height 0.2s; }
|
|
@@ -443,7 +444,7 @@ import{cssVarsStyle as g}from"../theme.js";import{commandbarCSS as x,commandbarH
|
|
|
443
444
|
})();
|
|
444
445
|
</script>
|
|
445
446
|
|
|
446
|
-
${
|
|
447
|
+
${h()}
|
|
447
448
|
${t?m():""}
|
|
448
449
|
|
|
449
450
|
<script>
|
|
@@ -457,7 +458,18 @@ window.__onSessionCreated = async function(name) {
|
|
|
457
458
|
await window.__refreshSidebar();
|
|
458
459
|
window.__openSession(name);
|
|
459
460
|
};
|
|
460
|
-
${
|
|
461
|
+
${u("__onSessionCreated")}
|
|
462
|
+
</script>
|
|
463
|
+
<script>
|
|
464
|
+
// Sync app-layout height to visual viewport so mobile keyboard doesn't overlap
|
|
465
|
+
(function(){
|
|
466
|
+
var el = document.querySelector('.app-layout');
|
|
467
|
+
var vv = window.visualViewport;
|
|
468
|
+
if (!vv || !el) return;
|
|
469
|
+
function sync(){ el.style.height = vv.height + 'px'; }
|
|
470
|
+
vv.addEventListener('resize', sync);
|
|
471
|
+
sync();
|
|
472
|
+
})();
|
|
461
473
|
</script>
|
|
462
474
|
</body>
|
|
463
475
|
</html>`}export{_ as renderShell};
|