tmux-weblink 1.1.8 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3 -3
- package/dist/lib/agents-watch.js +2 -2
- package/dist/lib/pages/settings.js +16 -22
- package/dist/lib/pages/shell.js +63 -10
- package/dist/lib/system-monitor.js +1 -0
- package/dist/lib/theme-store.js +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{chmodSync as Fe,existsSync as V,mkdirSync as Ue,readdirSync as de,readFileSync as We,statSync as N,unlinkSync as $e,writeFileSync as Be}from"node:fs";import{readFile as Ve}from"node:fs/promises";import{createRequire as Ge}from"node:module";import w from"node:path";import{fileURLToPath as ze}from"node:url";import{Hono as Ke}from"hono";import{serve as Qe}from"@hono/node-server";import{WebSocketServer as Xe,WebSocket as ue}from"ws";import*as Ye from"node-pty";try{const e=w.dirname(Ge(import.meta.url).resolve("node-pty/package.json")),t=w.join(e,"prebuilds");if(V(t))for(const r of de(t)){const n=w.join(t,r,"spawn-helper");V(n)&&!(N(n).mode&73)&&Fe(n,493)}}catch{}import{listSessions as q}from"./sessions.js";import{renderLoginPage as Je,renderNotesIndex as Ze,renderNotesPage as et,renderSettings as tt,renderThemeSettings as rt,renderScheduleIndex as nt,renderHistoryIndex as ot,renderQuickCommandsPage as st,renderFilesIndex as it,renderShell as at}from"./frontend.js";import{loadSecurityConfig as dt,saveSecurityConfig as ce}from"./lib/security-config.js";import{hashPassword as le,verifyPassword as me,validatePassword as pe,TokenStore as ut}from"./lib/auth.js";import{RateLimiter as ct}from"./lib/rateLimiter.js";import{atomicWriteFileSync as lt}from"./lib/atomicWrite.js";import{resolveFsPath as G,resolveFsRoots as fe,MAX_FILE_BYTES as he,walkRecursive as mt}from"./lib/fs-access.js";import{captureSessionWindowsWithPath as pt}from"./lib/tmux-windows.js";import{audit as c}from"./lib/auditLog.js";import{db as g}from"./lib/db.js";import{getSessionAccessMap as I}from"./lib/session-access.js";import{listWindowHistory as ft,clearWindowHistory as ht}from"./lib/window-history.js";import{loadExtensions as yt,spawnExtensionBackend as wt,registerExtensionRoutes as gt}from"./lib/ext-loader.js";import{SchedulerService as jt,isValidScheduleInput as kt,isValidRescheduleInput as vt}from"./lib/scheduler.js";import{getScheduleDelayError as ye}from"./lib/schedule-delay.js";import{handleClientMessage as xt}from"./lib/ws-message.js";import{loadDotEnv as Tt}from"./lib/load-env.js";import{cmdAdd as St,cmdRemove as bt,cmdList as qt,cmdSetup as At,cmdTheme as It,printUsage as we,printVersion as _t}from"./lib/cli.js";import{readSettings as Z,writeSettings as Ct}from"./lib/settings.js";import{readActiveTheme as Rt,setActiveThemeTemplate as ge}from"./lib/theme-store.js";import{isThemeTemplateId as je,THEME_TEMPLATE_IDS as Et}from"./lib/themes/index.js";import{installPlugin as Mt,uninstallPlugin as Ot}from"./lib/plugins.js";import{buildCommandbarSessions as C}from"./lib/commandbar.js";import{pinView as Lt,unpinView as Pt,listPinnedViews as Nt}from"./lib/pinned-views.js";import{listWindowLabels as Dt,setWindowLabel as Ht}from"./lib/window-labels.js";import{captureAndStoreWindows as ke,getStoredWindows as ve}from"./lib/session-windows.js";import{acquireControlClient as Ft,killAllControlClients as Ut}from"./lib/tmux-control.js";import{buildSidebarSessions as Wt}from"./lib/sessions-sidebar.js";import{createQuickCommand as $t,deleteQuickCommand as Bt,listQuickCommands as xe,updateQuickCommand as Vt}from"./lib/quick-commands.js";import{getSessionPaneTarget as Gt,capturePaneTail as zt,capturePaneHistoryChunk as Kt,isAlternateScreen as Qt,toCrlf as Te}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as Xt}from"./lib/terminal-config.js";import{ImageUploadError as Yt,saveUploadedImage as Jt}from"./lib/image-upload.js";import{listSessionWindows as
|
|
2
|
+
import{chmodSync as Fe,existsSync as V,mkdirSync as Ue,readdirSync as de,readFileSync as We,statSync as N,unlinkSync as $e,writeFileSync as Be}from"node:fs";import{readFile as Ve}from"node:fs/promises";import{createRequire as Ge}from"node:module";import w from"node:path";import{fileURLToPath as ze}from"node:url";import{Hono as Ke}from"hono";import{serve as Qe}from"@hono/node-server";import{WebSocketServer as Xe,WebSocket as ue}from"ws";import*as Ye from"node-pty";try{const e=w.dirname(Ge(import.meta.url).resolve("node-pty/package.json")),t=w.join(e,"prebuilds");if(V(t))for(const r of de(t)){const n=w.join(t,r,"spawn-helper");V(n)&&!(N(n).mode&73)&&Fe(n,493)}}catch{}import{listSessions as q}from"./sessions.js";import{renderLoginPage as Je,renderNotesIndex as Ze,renderNotesPage as et,renderSettings as tt,renderThemeSettings as rt,renderScheduleIndex as nt,renderHistoryIndex as ot,renderQuickCommandsPage as st,renderFilesIndex as it,renderShell as at}from"./frontend.js";import{loadSecurityConfig as dt,saveSecurityConfig as ce}from"./lib/security-config.js";import{hashPassword as le,verifyPassword as me,validatePassword as pe,TokenStore as ut}from"./lib/auth.js";import{RateLimiter as ct}from"./lib/rateLimiter.js";import{atomicWriteFileSync as lt}from"./lib/atomicWrite.js";import{resolveFsPath as G,resolveFsRoots as fe,MAX_FILE_BYTES as he,walkRecursive as mt}from"./lib/fs-access.js";import{captureSessionWindowsWithPath as pt}from"./lib/tmux-windows.js";import{audit as c}from"./lib/auditLog.js";import{db as g}from"./lib/db.js";import{getSessionAccessMap as I}from"./lib/session-access.js";import{listWindowHistory as ft,clearWindowHistory as ht}from"./lib/window-history.js";import{loadExtensions as yt,spawnExtensionBackend as wt,registerExtensionRoutes as gt}from"./lib/ext-loader.js";import{SchedulerService as jt,isValidScheduleInput as kt,isValidRescheduleInput as vt}from"./lib/scheduler.js";import{getScheduleDelayError as ye}from"./lib/schedule-delay.js";import{handleClientMessage as xt}from"./lib/ws-message.js";import{loadDotEnv as Tt}from"./lib/load-env.js";import{cmdAdd as St,cmdRemove as bt,cmdList as qt,cmdSetup as At,cmdTheme as It,printUsage as we,printVersion as _t}from"./lib/cli.js";import{readSettings as Z,writeSettings as Ct}from"./lib/settings.js";import{readActiveTheme as Rt,setActiveThemeTemplate as ge}from"./lib/theme-store.js";import{isThemeTemplateId as je,THEME_TEMPLATE_IDS as Et}from"./lib/themes/index.js";import{installPlugin as Mt,uninstallPlugin as Ot}from"./lib/plugins.js";import{buildCommandbarSessions as C}from"./lib/commandbar.js";import{pinView as Lt,unpinView as Pt,listPinnedViews as Nt}from"./lib/pinned-views.js";import{listWindowLabels as Dt,setWindowLabel as Ht}from"./lib/window-labels.js";import{captureAndStoreWindows as ke,getStoredWindows as ve}from"./lib/session-windows.js";import{acquireControlClient as Ft,killAllControlClients as Ut}from"./lib/tmux-control.js";import{buildSidebarSessions as Wt}from"./lib/sessions-sidebar.js";import{createQuickCommand as $t,deleteQuickCommand as Bt,listQuickCommands as xe,updateQuickCommand as Vt}from"./lib/quick-commands.js";import{getSessionPaneTarget as Gt,capturePaneTail as zt,capturePaneHistoryChunk as Kt,isAlternateScreen as Qt,toCrlf as Te}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as Xt}from"./lib/terminal-config.js";import{ImageUploadError as Yt,saveUploadedImage as Jt}from"./lib/image-upload.js";import{getSystemStatus as Zt}from"./lib/system-monitor.js";import{listSessionWindows as er,selectSessionWindow as tr,newSessionWindow as rr,renameSessionWindow as nr,newTmuxSession as or,renameSession as sr,killSession as ir,TmuxWindowsError as z}from"./lib/tmux-windows.js";Tt();const K=Xt(),l=dt(),f=new ut,R=new ct;let D=!1;const Q="tmux-web-token",ar=365;function Se(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}function X(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 dr(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 ee(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*)${Q}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function be(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*)${Q}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function un(e){const t=ee(e);return t?f.validateToken(t):null}function cn(e){const t=be(e);return t?f.validateToken(t):null}function qe(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${Q}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ar*86400}${r?"; Secure":""}`)}function ur(e){e.header("Set-Cookie",`${Q}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}function a(){return async(e,t)=>{const r=ee(e);return!r||!f.validateToken(r)?(c("http_unauthorized",{ip:X(e)}),e.json({error:"unauthorized"},401)):t()}}function cr(e){const t=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${t}`,302)}function _(){return async(e,t)=>{const r=ee(e);return!r||!f.validateToken(r)?cr(e):t()}}const H=new Map;function lr(e){let t=0;for(const r of H.values())r.ip===e&&t++;return t}function te(e,t,r){try{e.close(t,r)}catch{}}function ln(e,t){e.readyState===ue.OPEN&&e.send(JSON.stringify(t))}function mr(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 i of e)i==="--ghostty"&&(r="ghostty"),i==="--xterm"&&(r="xterm");return r}const pr=7;function Ae(e){return typeof e!="number"||!Number.isFinite(e)?pr:Math.min(365,Math.max(1,Math.round(e)))}const Ie=process.argv.slice(2);{const e=Ie.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 St(r),process.exit(0);case"remove":case"rm":r||(console.error("usage: tmux-web remove <package>"),process.exit(1)),await bt(r),process.exit(0);case"list":case"ls":await qt(),process.exit(0);case"setup":await At(e),process.exit(0);case"theme":await It(e.slice(1)),process.exit(0);case"help":case"--help":case"-h":we(),process.exit(0);case"-V":case"--version":case"-v":_t(),process.exit(0);default:console.error(`unknown argument: ${t}`),we(),process.exit(1)}}}function v(e,t){e.readyState===ue.OPEN&&e.send(JSON.stringify(t))}const F=new Set,_e=[];await g.read(),g.data.sessionAccess??=[],g.data.pinnedViews??=[],g.data.watchedPanes??=[],g.data.triggeredTasks??=[],g.data.quickCommands??=[];const re=await Z();let j=await Rt();const h=re.commandbar===!0,ne=mr(Ie,re.terminalRenderer),Ce=Ae(re.scheduleHistoryDays),Re=w.join(process.cwd(),"extensions"),Ee=await yt(Re);for(const e of Ee)e.start&&_e.push(wt(e.dir,e));const E=new jt({db:g,historyRetentionMs:Ce*864e5,onMissedTask:e=>console.warn(`[scheduler] dropped missed task ${e.id} (was due ${new Date(e.fireAt).toISOString()})`)});await E.restoreFromDb();const s=new Ke;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()}),gt(s,Re,Ee);const fr=w.dirname(ze(import.meta.url)),hr=[w.join(fr,"assets"),w.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 hr){const n=w.join(r,t);if(!V(n))continue;const i=await Ve(n),o=w.extname(t),d={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(i,200,{"Content-Type":d[o]??"application/octet-stream"})}return e.notFound()});const yr=`<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>`,Me=e=>e.body(hr,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});s.get("/favicon.svg",Me),s.get("/favicon.ico",Me),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!l.passwordHash,n=e.req.query("error");return e.html(Je({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:j}))}),s.get("/",_(),e=>{const t=h?C(q(),I()):[],r=fe();return e.html(at({theme:j,commandbarEnabled:h,commandbarSessions:t,fsRoots:r,terminalCfg:K,renderer:ne,scrollback:K.initialLines+2*K.historyChunk}))}),s.get("/notes",_(),e=>{const t=h?C(q(),I()):[];return e.html(Ze(g.data.notes,j,h,t))}),s.get("/notes/:session",_(),e=>{const t=decodeURIComponent(e.req.param("session")),r=h?C(q(),I()):[];return e.html(et(t,j,h,r))}),s.get("/schedule",_(),e=>{const t=h?C(q(),I()):[];return e.html(nt(E.list(),E.listTriggered(),j,Ce,h,t))}),s.get("/history",_(),e=>{const t=q(),r=h?C(t,I()):[],n=new Set(t.map(i=>i.name));return e.html(ot(ft(),j,h,r,n))}),s.get("/quick-commands",_(),e=>{const t=h?C(q(),I()):[];return e.html(st(xe(),j,h,t))}),s.get("/files",_(),e=>{const t=h?C(q(),I()):[],r=fe();return e.html(it(j,h,t,r))}),s.post("/api/history/clear",a(),async e=>(await ht(),e.json({ok:!0}))),s.get("/api/quick-commands",a(),e=>e.json(xe())),s.post("/api/quick-commands",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await $t(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Vt(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),s.delete("/api/quick-commands/:id",a(),async e=>await Bt(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{const t=X(e),r=R.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 i=typeof n.password=="string"?n.password:"";if(!l.passwordHash){if(!l.security.allowRemoteSetup&&!Se(t))return c("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(D)return e.json({error:"Password setup in progress"},409);const p=pe(i);if(p)return e.json({error:p},400);D=!0;try{l.passwordHash=await le(i),ce(l),c("password_set",{ip:t})}finally{D=!1}const{plaintext:T}=f.createAccessToken("setup",l.security.tokenTtlDays);return qe(e,T),e.json({ok:!0,token:T,setupMode:!0})}if(!await me(i,l.passwordHash)){const p=R.recordFailure(t);return c("auth_failed",{ip:t,method:"password",failures:p.failures,permanentLock:p.permanentLock}),p.permanentLock?(f.revokeAll(),c("permanent_lock",{ip:t,failures:p.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):p.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:p.retryAfterMs},429)}R.recordSuccess(t);const d=`browser-${t}`,{plaintext:x}=f.createAccessToken(d,l.security.tokenTtlDays);return qe(e,x),c("auth_success",{ip:t,method:"password",tokenName:d}),e.json({ok:!0,token:x})}),s.post("/api/auth/token",a(),async e=>{const t=X(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}`,i=typeof r.ttlDays=="number"?r.ttlDays:l.security.tokenTtlDays,{stored:o,plaintext:d}=f.createAccessToken(n,i);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",a(),e=>e.json(f.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",a(),e=>f.revoke(e.req.param("id"))?(c("token_revoked",{ip:X(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",a(),e=>(dr(e),e.json({ok:!0}))),s.get("/settings",_(),async e=>{const t=await Z(),r=t.terminalRenderer??"xterm";return e.html(tt({settings:t,renderer:ne,rendererOverridden:ne!==r,theme:j,plugins:t.plugins??[],saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await Z(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",i=t.defaultView==="recent"?"recent":"default",o=Ae(typeof t.scheduleHistoryDays=="string"?Number(t.scheduleHistoryDays):void 0);return await Ct({...r,commandbar:t.commandbar!==void 0,terminalRenderer:n,defaultView:i,scheduleHistoryDays:o}),e.redirect("/settings?saved=1",303)}),s.post("/settings/plugins",a(),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 i=r==="remove"?await Ot(n):r==="add"?await Mt(n):{ok:!1,output:"unknown action"};return i.ok?e.redirect("/settings?saved=1",303):e.redirect("/settings?error="+encodeURIComponent(i.output.slice(0,800)),303)}),s.get("/settings/theme",_(),e=>e.html(rt({theme:j,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const r=t.template;return typeof r!="string"||!je(r)?e.redirect("/settings/theme",303):(j=await ge(r),e.redirect("/settings/theme?saved=1",303))});const yr={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",a(),e=>e.json({active:j.template,templates:Et.map(t=>({id:t,name:yr[t]}))})),s.post("/api/theme",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=t.template;return typeof r!="string"||!je(r)?e.json({error:"invalid theme template"},400):(j=await ge(r),e.json({ok:!0,active:j.template}))}),s.get("/api/sessions",a(),e=>h?e.json(C(q(),I())):e.json({error:"commandbar disabled"},404)),s.post("/api/sessions/new",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(r))return e.json({error:"name contains invalid characters"},400);const n=typeof t.dir=="string"&&t.dir.trim()?t.dir.trim():void 0;if(q().some(o=>o.name===r))return e.json({error:"session already exists"},409);try{return nr(r,n),e.json({ok:!0})}catch(o){const d=o instanceof z?o.message:"failed to create session";return e.json({error:d},500)}}),s.post("/api/sessions/rename",a(),async e=>{try{const{oldName:t,newName:r}=await e.req.json();return!t||!r?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(r)?(or(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),s.post("/api/sessions/kill",a(),async e=>{try{const{name:t}=await e.req.json();return t?(sr(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.get("/api/fs/session-path",a(),e=>{const t=e.req.query("session");if(!t)return e.json({error:"session is required"},400);try{const r=pt(t),i=r.find(o=>o.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:i})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",a(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=w.join(t,r));const n=e.req.query("recursive")==="true";let i=r,o="",d=r.endsWith("/");if(!d)try{d=N(r).isDirectory()}catch{}d||(i=w.dirname(r),o=w.basename(r).toLowerCase());try{const x=de(i),p=[],T=[];for(const M of x)if(!M.startsWith(".")&&!(o&&!M.toLowerCase().startsWith(o))){try{const S=w.join(i,M);N(S).isDirectory()?(p.push(S),n&&mt(S,p,T,0)):T.push(S)}catch{}if(p.length+T.length>=5e3)break}return e.json({dirs:p,files:T})}catch{return e.json({dirs:[],files:[]})}}),s.get("/api/file",a(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=G(t);if(!N(r).isFile())return e.json({error:"not a file"},400);const n=N(r).size;if(n>he)return e.json({error:"file too large",size:n,maxBytes:he},413);const i=We(r,"utf-8");return e.json({path:r,content:i,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",a(),async e=>{try{const t=await e.req.json();if(!t.path||typeof t.content!="string")return e.json({error:"path and content are required"},400);const r=G(t.path);return lt(r,t.content),e.json({ok:!0})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),s.post("/api/file/delete",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=G(t.path);return N(r).isFile()?($e(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",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=G(t.path);return V(r)?e.json({error:"file already exists"},409):(Ue(w.dirname(r),{recursive:!0}),Be(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 oe(e){return{...Wt(q(),I(),Nt()),currentSession:e??null}}function Oe(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",a(),e=>{const t=e.req.query("currentSession");return e.json(oe(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",a(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(ve(t))}),s.post("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Oe(t);return"error"in r?e.json({error:r.error},400):(await Lt(r.sessionName,r.windowIndex),e.json(oe()))}),s.delete("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Oe(t);return"error"in r?e.json({error:r.error},400):(await Pt(r.sessionName,r.windowIndex),e.json(oe()))}),s.get("/api/notes",a(),e=>{const t=[...g.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",a(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=g.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",a(),async e=>{const t=decodeURIComponent(e.req.param("scope"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}if(typeof r.content!="string")return e.json({error:"content must be string"},400);const n={scope:t,content:r.content,updatedAt:Date.now()},i=g.data.notes.findIndex(o=>o.scope===t);return i>=0?g.data.notes[i]=n:g.data.notes.push(n),await g.write(),e.json({ok:!0})}),s.post("/api/session/:session/upload",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!q().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 i=n.file;if(!(i instanceof File))return e.json({error:"missing file field"},400);try{const o=await i.arrayBuffer(),d=Buffer.from(o),{path:x}=await Jt(d,i.type||void 0,i.name||void 0);return e.json({path:x})}catch(o){return o instanceof Yt?e.json({error:o.message},o.status):(console.error("[upload]",o),e.json({error:"upload failed"},500))}}),s.get("/api/session/:session/windows",a(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map(Dt(t).map(o=>[o.windowIndex,o.label])),n=new Map(ve(t).map(o=>[o.index,o])),i=Zt(t).map(o=>({...o,label:r.get(o.index)??null,worktree:n.get(o.index)?.worktree??!1}));return e.json(i)}),s.post("/api/session/:session/window-label",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);const i=typeof r.label=="string"?r.label:"",o=await Ht(t,n,i);return e.json(o)}),s.post("/api/session/:session/select-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);try{return er(t,n),e.json({ok:!0})}catch(i){return i instanceof z?e.json({error:i.message},i.status):(console.error("[select-window]",i),e.json({error:"select-window failed"},500))}}),s.post("/api/session/:session/rename-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);if(typeof r.name!="string"||!r.name.trim())return e.json({error:"name is required"},400);try{return rr(t,n,r.name),ke(t),e.json({ok:!0})}catch(i){return i instanceof z?e.json({error:i.message},i.status):(console.error("[rename-window]",i),e.json({error:"rename-window failed"},500))}}),s.post("/api/session/:session/new-window",a(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return tr(t),ke(t),e.json({ok:!0})}catch(r){return r instanceof z?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),s.get("/api/schedule",a(),e=>e.json(E.list(e.req.query("session")))),s.post("/api/schedule",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ye(t);if(r)return e.json({error:r},400);if(!kt(t))return e.json({error:"invalid body"},400);const n=await E.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),s.delete("/api/schedule/:id",a(),async e=>await E.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.patch("/api/schedule/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ye(t);if(r)return e.json({error:r},400);if(!vt(t))return e.json({error:"invalid body"},400);const n=await E.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)});const wr=parseInt(process.env.PORT||"21000",10),gr=Qe({fetch:s.fetch,port:wr,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),se=new Xe({noServer:!0});function Le(e,t,r){e.write(`HTTP/1.1 ${t} ${r}\r
|
|
6
|
+
</svg>`,Me=e=>e.body(yr,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});s.get("/favicon.svg",Me),s.get("/favicon.ico",Me),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!l.passwordHash,n=e.req.query("error");return e.html(Je({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:j}))}),s.get("/",_(),e=>{const t=h?C(q(),I()):[],r=fe();return e.html(at({theme:j,commandbarEnabled:h,commandbarSessions:t,fsRoots:r,terminalCfg:K,renderer:ne,scrollback:K.initialLines+2*K.historyChunk}))}),s.get("/notes",_(),e=>{const t=h?C(q(),I()):[];return e.html(Ze(g.data.notes,j,h,t))}),s.get("/notes/:session",_(),e=>{const t=decodeURIComponent(e.req.param("session")),r=h?C(q(),I()):[];return e.html(et(t,j,h,r))}),s.get("/schedule",_(),e=>{const t=h?C(q(),I()):[];return e.html(nt(E.list(),E.listTriggered(),j,Ce,h,t))}),s.get("/history",_(),e=>{const t=q(),r=h?C(t,I()):[],n=new Set(t.map(i=>i.name));return e.html(ot(ft(),j,h,r,n))}),s.get("/quick-commands",_(),e=>{const t=h?C(q(),I()):[];return e.html(st(xe(),j,h,t))}),s.get("/files",_(),e=>{const t=h?C(q(),I()):[],r=fe();return e.html(it(j,h,t,r))}),s.post("/api/history/clear",a(),async e=>(await ht(),e.json({ok:!0}))),s.get("/api/quick-commands",a(),e=>e.json(xe())),s.post("/api/quick-commands",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await $t(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Vt(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),s.delete("/api/quick-commands/:id",a(),async e=>await Bt(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{const t=X(e),r=R.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 i=typeof n.password=="string"?n.password:"";if(!l.passwordHash){if(!l.security.allowRemoteSetup&&!Se(t))return c("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(D)return e.json({error:"Password setup in progress"},409);const p=pe(i);if(p)return e.json({error:p},400);D=!0;try{l.passwordHash=await le(i),ce(l),c("password_set",{ip:t})}finally{D=!1}const{plaintext:T}=f.createAccessToken("setup",l.security.tokenTtlDays);return qe(e,T),e.json({ok:!0,token:T,setupMode:!0})}if(!await me(i,l.passwordHash)){const p=R.recordFailure(t);return c("auth_failed",{ip:t,method:"password",failures:p.failures,permanentLock:p.permanentLock}),p.permanentLock?(f.revokeAll(),c("permanent_lock",{ip:t,failures:p.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):p.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:p.retryAfterMs},429)}R.recordSuccess(t);const d=`browser-${t}`,{plaintext:x}=f.createAccessToken(d,l.security.tokenTtlDays);return qe(e,x),c("auth_success",{ip:t,method:"password",tokenName:d}),e.json({ok:!0,token:x})}),s.post("/api/auth/token",a(),async e=>{const t=X(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}`,i=typeof r.ttlDays=="number"?r.ttlDays:l.security.tokenTtlDays,{stored:o,plaintext:d}=f.createAccessToken(n,i);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",a(),e=>e.json(f.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",a(),e=>f.revoke(e.req.param("id"))?(c("token_revoked",{ip:X(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",a(),e=>(ur(e),e.json({ok:!0}))),s.get("/settings",_(),async e=>{const t=await Z(),r=t.terminalRenderer??"xterm";return e.html(tt({settings:t,renderer:ne,rendererOverridden:ne!==r,theme:j,plugins:t.plugins??[],saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await Z(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",i=t.defaultView==="recent"?"recent":"default",o=Ae(typeof t.scheduleHistoryDays=="string"?Number(t.scheduleHistoryDays):void 0);return await Ct({...r,commandbar:t.commandbar!==void 0,terminalRenderer:n,defaultView:i,scheduleHistoryDays:o}),e.redirect("/settings?saved=1",303)}),s.post("/settings/plugins",a(),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 i=r==="remove"?await Ot(n):r==="add"?await Mt(n):{ok:!1,output:"unknown action"};return i.ok?e.redirect("/settings?saved=1",303):e.redirect("/settings?error="+encodeURIComponent(i.output.slice(0,800)),303)}),s.get("/settings/theme",_(),e=>e.html(rt({theme:j,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const r=t.template;return typeof r!="string"||!je(r)?e.redirect("/settings/theme",303):(j=await ge(r),e.redirect("/settings/theme?saved=1",303))});const wr={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",a(),e=>e.json({active:j.template,templates:Et.map(t=>({id:t,name:wr[t]}))})),s.post("/api/theme",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=t.template;return typeof r!="string"||!je(r)?e.json({error:"invalid theme template"},400):(j=await ge(r),e.json({ok:!0,active:j.template}))}),s.get("/api/system/status",a(),e=>e.json(Zt())),s.get("/api/sessions",a(),e=>h?e.json(C(q(),I())):e.json({error:"commandbar disabled"},404)),s.post("/api/sessions/new",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(r))return e.json({error:"name contains invalid characters"},400);const n=typeof t.dir=="string"&&t.dir.trim()?t.dir.trim():void 0;if(q().some(o=>o.name===r))return e.json({error:"session already exists"},409);try{return or(r,n),e.json({ok:!0})}catch(o){const d=o instanceof z?o.message:"failed to create session";return e.json({error:d},500)}}),s.post("/api/sessions/rename",a(),async e=>{try{const{oldName:t,newName:r}=await e.req.json();return!t||!r?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(r)?(sr(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),s.post("/api/sessions/kill",a(),async e=>{try{const{name:t}=await e.req.json();return t?(ir(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.get("/api/fs/session-path",a(),e=>{const t=e.req.query("session");if(!t)return e.json({error:"session is required"},400);try{const r=pt(t),i=r.find(o=>o.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:i})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",a(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=w.join(t,r));const n=e.req.query("recursive")==="true";let i=r,o="",d=r.endsWith("/");if(!d)try{d=N(r).isDirectory()}catch{}d||(i=w.dirname(r),o=w.basename(r).toLowerCase());try{const x=de(i),p=[],T=[];for(const M of x)if(!M.startsWith(".")&&!(o&&!M.toLowerCase().startsWith(o))){try{const S=w.join(i,M);N(S).isDirectory()?(p.push(S),n&&mt(S,p,T,0)):T.push(S)}catch{}if(p.length+T.length>=5e3)break}return e.json({dirs:p,files:T})}catch{return e.json({dirs:[],files:[]})}}),s.get("/api/file",a(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=G(t);if(!N(r).isFile())return e.json({error:"not a file"},400);const n=N(r).size;if(n>he)return e.json({error:"file too large",size:n,maxBytes:he},413);const i=We(r,"utf-8");return e.json({path:r,content:i,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",a(),async e=>{try{const t=await e.req.json();if(!t.path||typeof t.content!="string")return e.json({error:"path and content are required"},400);const r=G(t.path);return lt(r,t.content),e.json({ok:!0})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),s.post("/api/file/delete",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=G(t.path);return N(r).isFile()?($e(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",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=G(t.path);return V(r)?e.json({error:"file already exists"},409):(Ue(w.dirname(r),{recursive:!0}),Be(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 oe(e){return{...Wt(q(),I(),Nt()),currentSession:e??null}}function Oe(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",a(),e=>{const t=e.req.query("currentSession");return e.json(oe(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",a(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(ve(t))}),s.post("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Oe(t);return"error"in r?e.json({error:r.error},400):(await Lt(r.sessionName,r.windowIndex),e.json(oe()))}),s.delete("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Oe(t);return"error"in r?e.json({error:r.error},400):(await Pt(r.sessionName,r.windowIndex),e.json(oe()))}),s.get("/api/notes",a(),e=>{const t=[...g.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",a(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=g.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",a(),async e=>{const t=decodeURIComponent(e.req.param("scope"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}if(typeof r.content!="string")return e.json({error:"content must be string"},400);const n={scope:t,content:r.content,updatedAt:Date.now()},i=g.data.notes.findIndex(o=>o.scope===t);return i>=0?g.data.notes[i]=n:g.data.notes.push(n),await g.write(),e.json({ok:!0})}),s.post("/api/session/:session/upload",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!q().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 i=n.file;if(!(i instanceof File))return e.json({error:"missing file field"},400);try{const o=await i.arrayBuffer(),d=Buffer.from(o),{path:x}=await Jt(d,i.type||void 0,i.name||void 0);return e.json({path:x})}catch(o){return o instanceof Yt?e.json({error:o.message},o.status):(console.error("[upload]",o),e.json({error:"upload failed"},500))}}),s.get("/api/session/:session/windows",a(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map(Dt(t).map(o=>[o.windowIndex,o.label])),n=new Map(ve(t).map(o=>[o.index,o])),i=er(t).map(o=>({...o,label:r.get(o.index)??null,worktree:n.get(o.index)?.worktree??!1}));return e.json(i)}),s.post("/api/session/:session/window-label",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);const i=typeof r.label=="string"?r.label:"",o=await Ht(t,n,i);return e.json(o)}),s.post("/api/session/:session/select-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);try{return tr(t,n),e.json({ok:!0})}catch(i){return i instanceof z?e.json({error:i.message},i.status):(console.error("[select-window]",i),e.json({error:"select-window failed"},500))}}),s.post("/api/session/:session/rename-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);if(typeof r.name!="string"||!r.name.trim())return e.json({error:"name is required"},400);try{return nr(t,n,r.name),ke(t),e.json({ok:!0})}catch(i){return i instanceof z?e.json({error:i.message},i.status):(console.error("[rename-window]",i),e.json({error:"rename-window failed"},500))}}),s.post("/api/session/:session/new-window",a(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return rr(t),ke(t),e.json({ok:!0})}catch(r){return r instanceof z?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),s.get("/api/schedule",a(),e=>e.json(E.list(e.req.query("session")))),s.post("/api/schedule",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ye(t);if(r)return e.json({error:r},400);if(!kt(t))return e.json({error:"invalid body"},400);const n=await E.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),s.delete("/api/schedule/:id",a(),async e=>await E.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.patch("/api/schedule/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ye(t);if(r)return e.json({error:r},400);if(!vt(t))return e.json({error:"invalid body"},400);const n=await E.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)});const gr=parseInt(process.env.PORT||"21000",10),jr=Qe({fetch:s.fetch,port:gr,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),se=new Xe({noServer:!0});function Le(e,t,r){e.write(`HTTP/1.1 ${t} ${r}\r
|
|
7
7
|
\r
|
|
8
|
-
`),e.destroy()}
|
|
8
|
+
`),e.destroy()}jr.on("upgrade",(e,t,r)=>{const n=new URL(e.url||"/",`http://${e.headers.host}`),i=n.pathname.match(/^\/ws\/(.+)$/);if(!i){t.destroy();return}const o=dr(e),d=e.headers.origin,x=l.security.allowedOrigins;if(d&&!(d===`http://${e.headers.host}`||d===`https://${e.headers.host}`)&&(x.length===0||!x.includes(d))){c("ws_rejected_origin",{ip:o,origin:d}),Le(t,403,"Origin not allowed");return}const p=lr(o);if(p>=l.security.maxConnectionsPerIp){c("ws_rejected_per_ip_cap",{ip:o,liveFromIp:p}),Le(t,429,"Too many connections");return}const T=be(e)||n.searchParams.get("token")||"",M=T?f.validateToken(T):null;se.handleUpgrade(e,t,r,S=>{se.emit("connection",S,e,decodeURIComponent(i[1]),o,M)})}),se.on("connection",(e,t,r,n,i)=>{const o={ws:e,ip:n,authenticated:!1,authTimeout:null};H.set(e,o),c("ws_connected",{ip:n});let d=null;const{initialLines:x,historyChunk:p,syncIdleMs:T,syncMaxMs:M}=K;let S=!0,P=null,$=null,U=r,O=null;const B=()=>{P&&(clearTimeout(P),P=null),$&&(clearTimeout($),$=null)},ie=()=>{if(S){B(),S=!1;try{U=Gt(r)}catch{U=r}if(!Qt(U))try{const y=zt(U,x);v(e,{type:"snapshot",data:Te(y),lines:x})}catch{}}},Ne=()=>{S&&(P&&clearTimeout(P),P=setTimeout(ie,T))};function ae(){try{d=Ye.spawn("tmux",["attach-session","-t",r],{name:"xterm-256color",cols:80,rows:24,cwd:process.env.HOME||"/",env:process.env})}catch(u){v(e,{type:"data",data:`\r
|
|
9
9
|
\x1B[31mFailed to attach to tmux session "${r}": ${u.message}\x1B[0m\r
|
|
10
10
|
`}),te(e,1011,"pty spawn failed");return}F.add(d);let y=-1,m="";O=Ft(r,({activeIndex:u,windows:k})=>{v(e,{type:"window_changed",activeIndex:u,windows:k});const L=k.map(b=>b.index).join(","),W=u!==y||L!==m;y=u,m=L}),$=setTimeout(ie,M),d.onData(u=>{if(S){Ne();return}v(e,{type:"data",data:u})}),d.onExit(({exitCode:u})=>{B(),d&&F.delete(d),d=null,v(e,{type:"data",data:`\r
|
|
11
11
|
\x1B[2m--- tmux exited (code ${u}) ---\x1B[0m\r
|
package/dist/lib/agents-watch.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{execFileSync as k}from"node:child_process";import{capturePaneLines as v}from"./tmux-capture.js";import{listSessionPanes as S}from"./tmux-panes.js";import{getWatchedPanes as B,pruneWatchedPanes as M,recordWatchedPane as D}from"./watched-panes.js";import{identifyAgent as y,detectState as b}from"./agent-detect.js";function
|
|
2
|
-
`)){const c=a.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);c&&e.set(parseInt(c[1],10),{ppid:parseInt(c[2],10),command:c[3]})}}catch{}return e}function A(e,o){if(o.size===0||e<=0)return[];const a=new Map;for(const[n,t]of o){const s=a.get(t.ppid);s?s.push(n):a.set(t.ppid,[n])}const c=[],f=[...a.get(e)??[]],p=o.get(e);p&&c.push(p.command);let m=0;for(;f.length&&m<200;){m++;const n=f.shift(),t=o.get(n);t&&c.push(t.command);const s=a.get(n);s&&f.push(...s)}return c}const
|
|
1
|
+
import{execFileSync as k}from"node:child_process";import{capturePaneLines as v}from"./tmux-capture.js";import{listSessionPanes as S}from"./tmux-panes.js";import{getWatchedPanes as B,pruneWatchedPanes as M,recordWatchedPane as D}from"./watched-panes.js";import{identifyAgent as y,detectState as b}from"./agent-detect.js";function C(){const e=new Map;try{const o=k("ps",["-axo","pid=,ppid=,command="],{encoding:"utf-8",timeout:3e3,maxBuffer:8388608});for(const a of o.split(`
|
|
2
|
+
`)){const c=a.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);c&&e.set(parseInt(c[1],10),{ppid:parseInt(c[2],10),command:c[3]})}}catch{}return e}function A(e,o){if(o.size===0||e<=0)return[];const a=new Map;for(const[n,t]of o){const s=a.get(t.ppid);s?s.push(n):a.set(t.ppid,[n])}const c=[],f=[...a.get(e)??[]],p=o.get(e);p&&c.push(p.command);let m=0;for(;f.length&&m<200;){m++;const n=f.shift(),t=o.get(n);t&&c.push(t.command);const s=a.get(n);s&&f.push(...s)}return c}const l=new Map;function T(e){let o=5381;for(let a=0;a<e.length;a++)o=(o<<5)+o+e.charCodeAt(a)>>>0;return o}async function N(){const e=B();if(e.length===0)return[];const o=new Map;for(const n of e)o.has(n.sessionName)||o.set(n.sessionName,S(n.sessionName));const a=new Set;for(const n of o.values())for(const t of n)a.add(t.paneId);await M(a);for(const n of l.keys())a.has(n)||l.delete(n);const c=C(),f=new Set(e.map(n=>n.paneId)),p=[...e];for(const[n,t]of o){const s=t.find(d=>d.active&&d.windowActive);if(!s||f.has(s.paneId)||!y(s.command,A(s.pid,c)))continue;const u={paneId:s.paneId,sessionName:n,windowIndex:s.windowIndex,paneIndex:s.paneIndex};await D(u),f.add(s.paneId),p.unshift({...u,watchedAt:Date.now()})}const m=[];for(const n of p){const t=o.get(n.sessionName)?.find(h=>h.paneId===n.paneId);if(!t)continue;const s=A(t.pid,c),u=y(t.command,s);if(!u)continue;let d="unknown";try{const h=v(t.target,0);d=b(u,h);const g=T(h),x=l.get(t.paneId);l.set(t.paneId,g),d==="idle"&&x!==void 0&&x!==g&&(d="working")}catch{}m.push({paneId:t.paneId,sessionName:n.sessionName,windowIndex:t.windowIndex,paneIndex:t.paneIndex,agent:u,state:d,watchedAt:n.watchedAt})}return m}let i=null,I=[],P=0,r=null;const W=1500;function w(){P=Date.now(),N().then(e=>{I=e}).catch(()=>{})}function q(){return I}function z(){return i!==null}function F(){if(!i||r)return;const e=Date.now()-P;if(e>=W){w();return}r=setTimeout(()=>{r=null,w()},W-e),typeof r.unref=="function"&&r.unref()}function L(e=3500){i||(w(),i=setInterval(w,e),typeof i.unref=="function"&&i.unref())}function H(){i&&(clearInterval(i),i=null),r&&(clearTimeout(r),r=null),I=[]}export{q as getCachedAgentStatuses,z as isBackgroundWatchRunning,N as probeWatchedPanes,F as requestProbe,L as startBackgroundWatch,H as stopBackgroundWatch};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cssVarsStyle as
|
|
1
|
+
import{cssVarsStyle as b}from"../theme.js";import{getThemeTemplates as h,THEME_TEMPLATE_IDS as x}from"../themes/index.js";function n(a){return a.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function g(a,e){return`<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
@@ -6,7 +6,7 @@ import{cssVarsStyle as h}from"../theme.js";import{getThemeTemplates as x,THEME_T
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>${n(a)} - tmux-web</title>
|
|
8
8
|
<style>
|
|
9
|
-
${
|
|
9
|
+
${b(e.shell)}
|
|
10
10
|
html, body { background: var(--page-bg); color: var(--page-fg); min-height: 100%; font-family: var(--font-sans); }
|
|
11
11
|
.container { max-width: 680px; margin: 80px auto; padding: 0 24px; }
|
|
12
12
|
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px; }
|
|
@@ -87,7 +87,7 @@ import{cssVarsStyle as h}from"../theme.js";import{getThemeTemplates as x,THEME_T
|
|
|
87
87
|
.num-input { width: 100%; }
|
|
88
88
|
}
|
|
89
89
|
</style>
|
|
90
|
-
</head>`}function
|
|
90
|
+
</head>`}function f(a,e){let r="";return a&&(r+='<div class="saved-flash">\u2713 Saved. Restart tmux-web to apply.</div>'),e&&(r+=`<div class="error-flash">${n(e)}</div>`),r}function T(a){const{settings:e,renderer:r,rendererOverridden:i,theme:l,plugins:t,saved:d=!1,error:s}=a,c=e.commandbar===!0,o=e.terminalRenderer??"xterm",m=e.defaultView??"default",u=e.scheduleHistoryDays??7,v=t.length?t.map(p=>`<div class="plugin-row">
|
|
91
91
|
<span class="pkg">${n(p)}</span>
|
|
92
92
|
<form method="POST" action="/settings/plugins" onsubmit="return confirm('Remove ${n(p)}?');">
|
|
93
93
|
<input type="hidden" name="action" value="remove" />
|
|
@@ -95,7 +95,7 @@ import{cssVarsStyle as h}from"../theme.js";import{getThemeTemplates as x,THEME_T
|
|
|
95
95
|
<button type="submit" class="btn danger">Remove</button>
|
|
96
96
|
</form>
|
|
97
97
|
</div>`).join(`
|
|
98
|
-
`):'<p class="desc" style="margin:0">No plugins enabled.</p>';return`${
|
|
98
|
+
`):'<p class="desc" style="margin:0">No plugins enabled.</p>';return`${g("Settings",l)}
|
|
99
99
|
<body>
|
|
100
100
|
<div class="container">
|
|
101
101
|
<div class="page-header">
|
|
@@ -108,7 +108,7 @@ import{cssVarsStyle as h}from"../theme.js";import{getThemeTemplates as x,THEME_T
|
|
|
108
108
|
<strong>restart the tmux-web process</strong> after making changes for them to take effect.
|
|
109
109
|
</div>
|
|
110
110
|
|
|
111
|
-
${
|
|
111
|
+
${f(d,s)}
|
|
112
112
|
|
|
113
113
|
<form method="POST" action="/settings">
|
|
114
114
|
<div class="section">
|
|
@@ -117,18 +117,12 @@ import{cssVarsStyle as h}from"../theme.js";import{getThemeTemplates as x,THEME_T
|
|
|
117
117
|
<label class="row"><input type="checkbox" name="commandbar" ${c?"checked":""} /> Enable command bar</label>
|
|
118
118
|
</div>
|
|
119
119
|
|
|
120
|
-
<div class="section">
|
|
121
|
-
<h2>Agents page</h2>
|
|
122
|
-
<p class="desc">Watch AI agents (Claude, Codex, OpenCode, Cursor) running in the last 10 panes you viewed, at <code>/agents</code>. Off by default.</p>
|
|
123
|
-
<label class="row"><input type="checkbox" name="agents" ${s?"checked":""} /> Enable agents page</label>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
120
|
<div class="section">
|
|
127
121
|
<h2>Terminal library</h2>
|
|
128
122
|
<p class="desc">Rendering engine for the terminal view.</p>
|
|
129
123
|
<div class="radios">
|
|
130
|
-
<label class="row"><input type="radio" name="terminalRenderer" value="xterm" ${
|
|
131
|
-
<label class="row"><input type="radio" name="terminalRenderer" value="ghostty" ${
|
|
124
|
+
<label class="row"><input type="radio" name="terminalRenderer" value="xterm" ${o==="xterm"?"checked":""} /> xterm.js (default)</label>
|
|
125
|
+
<label class="row"><input type="radio" name="terminalRenderer" value="ghostty" ${o==="ghostty"?"checked":""} /> ghostty-web</label>
|
|
132
126
|
</div>
|
|
133
127
|
${i?`<p class="override-note">\u26A0 A CLI flag or <code>TMUX_WEB_TERMINAL_RENDERER</code> env var is currently forcing <strong>${r}</strong>, overriding this setting for the running process.</p>`:""}
|
|
134
128
|
</div>
|
|
@@ -137,15 +131,15 @@ import{cssVarsStyle as h}from"../theme.js";import{getThemeTemplates as x,THEME_T
|
|
|
137
131
|
<h2>Default home tab</h2>
|
|
138
132
|
<p class="desc">Which tab the home page opens on.</p>
|
|
139
133
|
<div class="radios">
|
|
140
|
-
<label class="row"><input type="radio" name="defaultView" value="default" ${
|
|
141
|
-
<label class="row"><input type="radio" name="defaultView" value="recent" ${
|
|
134
|
+
<label class="row"><input type="radio" name="defaultView" value="default" ${m==="default"?"checked":""} /> Default</label>
|
|
135
|
+
<label class="row"><input type="radio" name="defaultView" value="recent" ${m==="recent"?"checked":""} /> Last Updated</label>
|
|
142
136
|
</div>
|
|
143
137
|
</div>
|
|
144
138
|
|
|
145
139
|
<div class="section">
|
|
146
140
|
<h2>Schedule history</h2>
|
|
147
141
|
<p class="desc">Days to keep the <code>/schedule</code> "Recently Triggered" history (fired & missed tasks). 1\u2013365, default 7.</p>
|
|
148
|
-
<label class="row"><input type="number" class="num-input" name="scheduleHistoryDays" min="1" max="365" value="${
|
|
142
|
+
<label class="row"><input type="number" class="num-input" name="scheduleHistoryDays" min="1" max="365" value="${u}" /> days</label>
|
|
149
143
|
</div>
|
|
150
144
|
|
|
151
145
|
<div class="form-actions">
|
|
@@ -156,7 +150,7 @@ import{cssVarsStyle as h}from"../theme.js";import{getThemeTemplates as x,THEME_T
|
|
|
156
150
|
<div class="section" style="margin-top:16px">
|
|
157
151
|
<h2>Plugins</h2>
|
|
158
152
|
<p class="desc">Installed via npm into the tmux-web data dir. Add/remove runs npm and may take a few seconds.</p>
|
|
159
|
-
${
|
|
153
|
+
${v}
|
|
160
154
|
<form method="POST" action="/settings/plugins" class="plugin-add">
|
|
161
155
|
<input type="hidden" name="action" value="add" />
|
|
162
156
|
<input type="text" id="pkg-input" name="pkg" placeholder="npm package name" autocomplete="off" />
|
|
@@ -171,14 +165,14 @@ import{cssVarsStyle as h}from"../theme.js";import{getThemeTemplates as x,THEME_T
|
|
|
171
165
|
</div>
|
|
172
166
|
</div>
|
|
173
167
|
</body>
|
|
174
|
-
</html>`}const
|
|
168
|
+
</html>`}const y={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"},w=["background","foreground","red","green","yellow","blue","magenta","cyan"];function S(a){const{theme:e,saved:r=!1}=a,i=h(),l=x.map(t=>{const d=i[t],s=e.template===t,c=w.map(o=>`<span class="swatch" style="background:${n(d.terminal[o])}" title="${o}"></span>`).join("");return`<label class="theme-card${s?" active":""}">
|
|
175
169
|
<div class="tname">
|
|
176
|
-
<input type="radio" name="template" value="${t}" ${
|
|
177
|
-
${n(
|
|
170
|
+
<input type="radio" name="template" value="${t}" ${s?"checked":""} style="accent-color:var(--panel-success)" />
|
|
171
|
+
${n(y[t]??t)}
|
|
178
172
|
</div>
|
|
179
173
|
<div class="swatches">${c}</div>
|
|
180
174
|
</label>`}).join(`
|
|
181
|
-
`);return`${
|
|
175
|
+
`);return`${g("Theme",e)}
|
|
182
176
|
<body>
|
|
183
177
|
<div class="container">
|
|
184
178
|
<div class="page-header">
|
|
@@ -202,4 +196,4 @@ import{cssVarsStyle as h}from"../theme.js";import{getThemeTemplates as x,THEME_T
|
|
|
202
196
|
</form>
|
|
203
197
|
</div>
|
|
204
198
|
</body>
|
|
205
|
-
</html>`}export{
|
|
199
|
+
</html>`}export{T as renderSettings,S as renderThemeSettings};
|
package/dist/lib/pages/shell.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cssVarsStyle as b}from"../theme.js";import{commandbarCSS as
|
|
1
|
+
import{cssVarsStyle as b}from"../theme.js";import{commandbarCSS as m,commandbarHTML as x,commandbarScript as g}from"../commandbar.js";import{newSessionModalCSS as f,newSessionModalHTML as u,newSessionModalScript as h,reducedMotion as v}from"../shared-layout.js";import{icon as n}from"../icons.js";function $(a){return a.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function o(a="var(--panel-accent)"){return`box-shadow: 0 0 0 2px ${a}; outline: none;`}function C(a){const{theme:e,commandbarEnabled:t,commandbarSessions:i,fsRoots:s,terminalCfg:l,renderer:r,scrollback:d}=a,p=`
|
|
2
2
|
*, *::before, *::after { box-sizing: border-box; }
|
|
3
3
|
html, body {
|
|
4
4
|
background: var(--page-bg); color: var(--page-fg);
|
|
@@ -23,6 +23,34 @@ import{cssVarsStyle as b}from"../theme.js";import{commandbarCSS as x,commandbarH
|
|
|
23
23
|
.fixed-header .brand a { color: inherit; text-decoration: none; }
|
|
24
24
|
.header-actions { display: flex; align-items: center; gap: 4px; }
|
|
25
25
|
|
|
26
|
+
/* \u2500\u2500 System status in header \u2500\u2500 */
|
|
27
|
+
.sys-status {
|
|
28
|
+
display: flex; align-items: center; gap: 8px;
|
|
29
|
+
font-size: 11px; font-family: var(--font-mono);
|
|
30
|
+
color: var(--panel-muted); cursor: default;
|
|
31
|
+
margin: 0 12px;
|
|
32
|
+
}
|
|
33
|
+
.sys-status .mem-bar {
|
|
34
|
+
width: 40px; height: 6px;
|
|
35
|
+
background: color-mix(in srgb, var(--panel-muted) 20%, transparent);
|
|
36
|
+
border-radius: 3px; overflow: hidden;
|
|
37
|
+
}
|
|
38
|
+
.sys-status .mem-bar-fill {
|
|
39
|
+
height: 100%; border-radius: 3px;
|
|
40
|
+
transition: width 0.5s ease;
|
|
41
|
+
}
|
|
42
|
+
.sys-status .mem-bar-fill.low { background: var(--panel-success, #16a34a); }
|
|
43
|
+
.sys-status .mem-bar-fill.mid { background: #eab308; }
|
|
44
|
+
.sys-status .mem-bar-fill.high { background: #ef4444; }
|
|
45
|
+
.sys-status .mem-text { min-width: 3ch; text-align: right; }
|
|
46
|
+
.sys-status .cpu-text { color: color-mix(in srgb, var(--panel-muted) 70%, transparent); }
|
|
47
|
+
@media (max-width: 720px) {
|
|
48
|
+
.sys-status .cpu-text { display: none; }
|
|
49
|
+
}
|
|
50
|
+
@media (max-width: 540px) {
|
|
51
|
+
.sys-status { display: none; }
|
|
52
|
+
}
|
|
53
|
+
|
|
26
54
|
/* \u2500\u2500 App layout \u2500\u2500 */
|
|
27
55
|
.app-layout {
|
|
28
56
|
display: flex;
|
|
@@ -63,7 +91,7 @@ import{cssVarsStyle as b}from"../theme.js";import{commandbarCSS as x,commandbarH
|
|
|
63
91
|
}
|
|
64
92
|
.sidebar-footer .mode-btn:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
|
|
65
93
|
.sidebar-footer .mode-btn.active { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 10%, transparent); }
|
|
66
|
-
.sidebar-footer .mode-btn:focus-visible { ${
|
|
94
|
+
.sidebar-footer .mode-btn:focus-visible { ${o()} }
|
|
67
95
|
|
|
68
96
|
/* \u2500\u2500 Sidebar session mode \u2500\u2500 */
|
|
69
97
|
.session-item {
|
|
@@ -85,7 +113,7 @@ import{cssVarsStyle as b}from"../theme.js";import{commandbarCSS as x,commandbarH
|
|
|
85
113
|
}
|
|
86
114
|
.session-item:hover .session-edit-btn { display: flex; align-items: center; }
|
|
87
115
|
.session-edit-btn:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
|
|
88
|
-
.session-edit-btn:focus-visible { ${
|
|
116
|
+
.session-edit-btn:focus-visible { ${o()} }
|
|
89
117
|
|
|
90
118
|
.sidebar-section-label {
|
|
91
119
|
font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.05em;
|
|
@@ -102,7 +130,7 @@ import{cssVarsStyle as b}from"../theme.js";import{commandbarCSS as x,commandbarH
|
|
|
102
130
|
color: var(--panel-accent); border-color: var(--panel-accent);
|
|
103
131
|
background: color-mix(in srgb, var(--panel-accent) 6%, transparent);
|
|
104
132
|
}
|
|
105
|
-
.new-session-sidebar-btn:focus-visible { ${
|
|
133
|
+
.new-session-sidebar-btn:focus-visible { ${o()} }
|
|
106
134
|
.new-session-sidebar-btn svg { width: 16px; height: 16px; fill: currentColor; }
|
|
107
135
|
|
|
108
136
|
/* \u2500\u2500 Sidebar files mode \u2500\u2500 */
|
|
@@ -176,7 +204,7 @@ import{cssVarsStyle as b}from"../theme.js";import{commandbarCSS as x,commandbarH
|
|
|
176
204
|
color: var(--page-fg); transition: opacity 0.15s; text-decoration: none;
|
|
177
205
|
}
|
|
178
206
|
.btn:hover { opacity: 0.85; }
|
|
179
|
-
.btn:focus-visible { ${
|
|
207
|
+
.btn:focus-visible { ${o()} }
|
|
180
208
|
.btn.primary { background: var(--panel-accent); border-color: var(--panel-accent); color: var(--panel-accent-on); font-weight: 500; }
|
|
181
209
|
.btn.primary:hover { opacity: 0.9; }
|
|
182
210
|
.btn.danger { color: #b91c1c; border-color: color-mix(in srgb, #b91c1c 30%, transparent); }
|
|
@@ -328,8 +356,8 @@ import{cssVarsStyle as b}from"../theme.js";import{commandbarCSS as x,commandbarH
|
|
|
328
356
|
.file-roots-list li svg { width: 20px; height: 20px; flex-shrink: 0; fill: var(--panel-accent); }
|
|
329
357
|
|
|
330
358
|
/* \u2500\u2500 Commandbar \u2500\u2500 */
|
|
331
|
-
${t?
|
|
332
|
-
${
|
|
359
|
+
${t?m():""}
|
|
360
|
+
${v()}
|
|
333
361
|
${f()}
|
|
334
362
|
@media (max-width: 640px) {
|
|
335
363
|
.app-layout { flex-direction: column; }
|
|
@@ -370,6 +398,11 @@ import{cssVarsStyle as b}from"../theme.js";import{commandbarCSS as x,commandbarH
|
|
|
370
398
|
|
|
371
399
|
<header class="fixed-header">
|
|
372
400
|
<div class="brand"><a href="/">tmux<span>-weblink</span></a></div>
|
|
401
|
+
<div class="sys-status" id="sys-status">
|
|
402
|
+
<span class="mem-text" id="sys-mem-text">--</span>
|
|
403
|
+
<span class="mem-bar"><span class="mem-bar-fill" id="sys-mem-bar" style="width:0%"></span></span>
|
|
404
|
+
<span class="cpu-text" id="sys-cpu-text">--</span>
|
|
405
|
+
</div>
|
|
373
406
|
<div class="header-actions">
|
|
374
407
|
${t?`<button class="header-btn" id="cmdbar-btn" title="Search" aria-label="Search">${n("search")}</button>`:""}
|
|
375
408
|
</div>
|
|
@@ -473,20 +506,20 @@ import{cssVarsStyle as b}from"../theme.js";import{commandbarCSS as x,commandbarH
|
|
|
473
506
|
</script>
|
|
474
507
|
|
|
475
508
|
${u()}
|
|
476
|
-
${t?
|
|
509
|
+
${t?x():""}
|
|
477
510
|
|
|
478
511
|
<script>
|
|
479
512
|
window.__TMUX_WEB_SHELL__ = ${c};
|
|
480
513
|
</script>
|
|
481
514
|
<script type="module">
|
|
482
515
|
await import('/assets/shell-client.js');
|
|
483
|
-
${t?
|
|
516
|
+
${t?g(i,[]):""}
|
|
484
517
|
// Wrapper: refresh sidebar list, then open session
|
|
485
518
|
window.__onSessionCreated = async function(name) {
|
|
486
519
|
await window.__refreshSidebar();
|
|
487
520
|
window.__openSession(name);
|
|
488
521
|
};
|
|
489
|
-
${
|
|
522
|
+
${h("__onSessionCreated")}
|
|
490
523
|
</script>
|
|
491
524
|
<script>
|
|
492
525
|
// Sync app-layout height to visual viewport so mobile keyboard doesn't overlap
|
|
@@ -499,5 +532,25 @@ ${v("__onSessionCreated")}
|
|
|
499
532
|
sync();
|
|
500
533
|
})();
|
|
501
534
|
</script>
|
|
535
|
+
<script>
|
|
536
|
+
(function(){
|
|
537
|
+
var memText = document.getElementById('sys-mem-text');
|
|
538
|
+
var memBar = document.getElementById('sys-mem-bar');
|
|
539
|
+
var cpuText = document.getElementById('sys-cpu-text');
|
|
540
|
+
if (!memText) return;
|
|
541
|
+
function poll(){
|
|
542
|
+
fetch('/api/system/status').then(function(r){ return r.json(); }).then(function(s){
|
|
543
|
+
var p = s.memory.percent;
|
|
544
|
+
memText.textContent = p + '%';
|
|
545
|
+
memBar.style.width = p + '%';
|
|
546
|
+
memBar.className = 'mem-bar-fill ' + (p >= 85 ? 'high' : p >= 70 ? 'mid' : 'low');
|
|
547
|
+
var load = s.cpu.loadAvg[0];
|
|
548
|
+
cpuText.textContent = 'CPU ' + (load < 10 ? load.toFixed(1) : Math.round(load));
|
|
549
|
+
}).catch(function(){});
|
|
550
|
+
}
|
|
551
|
+
poll();
|
|
552
|
+
setInterval(poll, 5000);
|
|
553
|
+
})();
|
|
554
|
+
</script>
|
|
502
555
|
</body>
|
|
503
556
|
</html>`}export{C as renderShell};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import*as t from"node:os";function m(){const o=t.totalmem(),r=t.freemem(),s=o-r;return{memory:{total:o,free:r,used:s,percent:o>0?Math.round(s/o*100):0},cpu:{loadAvg:t.loadavg(),cores:t.cpus().length},uptime:{system:t.uptime(),process:process.uptime()},process:{rss:process.memoryUsage().rss,heapTotal:process.memoryUsage().heapTotal,heapUsed:process.memoryUsage().heapUsed},hostname:t.hostname(),platform:`${t.platform()} ${t.release()}`}}function p(o){return o<1024?`${o} B`:o<1024*1024?`${(o/1024).toFixed(1)} KB`:o<1024*1024*1024?`${(o/(1024*1024)).toFixed(1)} MB`:`${(o/(1024*1024*1024)).toFixed(1)} GB`}function n(o){const r=Math.floor(o/86400),s=Math.floor(o%86400/3600),a=Math.floor(o%3600/60),e=[];return r>0&&e.push(`${r}d`),s>0&&e.push(`${s}h`),a>0&&e.push(`${a}m`),e.length===0&&e.push(`${Math.floor(o)}s`),e.join(" ")}export{p as formatBytes,n as formatDuration,m as getSystemStatus};
|
package/dist/lib/theme-store.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{readFile as a,writeFile as o,mkdir as l}from"node:fs/promises";import m from"node:path";import{getThemePath as
|
|
2
|
-
`)}async function w(){const e=n("
|
|
1
|
+
import{readFile as a,writeFile as o,mkdir as l}from"node:fs/promises";import m from"node:path";import{getThemePath as c}from"./state-paths.js";import{isThemeTemplateId as f,resolveTheme as n}from"./themes/index.js";const i=c();function s(e){if(!e||typeof e!="object")return!1;const t=e;return f(t.template)&&typeof t.shell=="object"&&t.shell!==null&&typeof t.shell.pageBg=="string"&&typeof t.terminal=="object"&&t.terminal!==null&&typeof t.terminal.background=="string"}function u(e,t){return JSON.stringify(e)===JSON.stringify(t)}async function r(e){await l(m.dirname(i),{recursive:!0}),await o(i,JSON.stringify(e,null,2)+`
|
|
2
|
+
`)}async function w(){const e=n("warm-clay");let t;try{t=JSON.parse(await a(i,"utf-8"))}catch{return await r(e),e}return s(t)?u(t,e)?t:(await r(e),e):(await r(e),e)}async function g(e){const t=n(e);return await r(t),t}export{w as readActiveTheme,g as setActiveThemeTemplate,r as writeActiveTheme};
|