tmux-weblink 2.0.16 → 2.0.18
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/README.md +4 -4
- package/dist/lib/build-app.js +4 -4
- package/dist/lib/cli.js +1 -1
- package/dist/lib/pages/federation.js +12 -12
- package/dist/lib/pages/settings.js +7 -7
- package/dist/lib/pages/shell.js +13 -3
- package/dist/lib/terminal-config.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,15 +32,15 @@ tmux-weblink
|
|
|
32
32
|
# Optional: tail-first buffer loading (see docs/architecture.md)
|
|
33
33
|
TMUX_WEB_INITIAL_LINES=1000 TMUX_WEB_HISTORY_CHUNK=500 tmux-weblink
|
|
34
34
|
|
|
35
|
-
#
|
|
35
|
+
# Machines: join this machine to a hub (no ports opened)
|
|
36
36
|
tmux-weblink agent --hub wss://hub.example.com --token <agent-token> --name laptop
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
> **Every machine uses the same start command.** To join a hub, open
|
|
40
|
-
> `http://localhost:21000/settings/
|
|
40
|
+
> `http://localhost:21000/settings/machines` on the machine you want to
|
|
41
41
|
> attach, paste the hub URL + token, and press **Save & Connect** — the agent
|
|
42
42
|
> client runs in-process next to the normal server. The hub's own
|
|
43
|
-
> `/settings/
|
|
43
|
+
> `/settings/machines` page creates/revokes tokens.
|
|
44
44
|
|
|
45
45
|
Then open `http://localhost:21000` in your browser. You'll see a list of active tmux sessions — click one to attach.
|
|
46
46
|
|
|
@@ -49,7 +49,7 @@ Then open `http://localhost:21000` in your browser. You'll see a list of active
|
|
|
49
49
|
- [Documentation hub](docs/index.md)
|
|
50
50
|
- [Notes](docs/notes.md) — per-session Markdown scratchpad
|
|
51
51
|
- **Windows drawer** — switch tmux windows from the terminal header (mobile-friendly tab picker)
|
|
52
|
-
- [
|
|
52
|
+
- [Machines / Agents](docs/agents.md) — join NAT'd machines to a public hub
|
|
53
53
|
- [Architecture](docs/architecture.md) — how the server, terminal, and sidebar connect
|
|
54
54
|
|
|
55
55
|
## Prerequisites
|
package/dist/lib/build-app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{existsSync as P,readdirSync as ie,statSync as A,unlinkSync as ae,writeFileSync as de,mkdirSync as ue,readFileSync as $}from"node:fs";import{readFile as ce}from"node:fs/promises";import{execSync as _,execFileSync as G}from"node:child_process";import p from"node:path";import{fileURLToPath as le}from"node:url";import{Hono as
|
|
1
|
+
import{existsSync as P,readdirSync as ie,statSync as A,unlinkSync as ae,writeFileSync as de,mkdirSync as ue,readFileSync as $}from"node:fs";import{readFile as ce}from"node:fs/promises";import{execSync as _,execFileSync as G}from"node:child_process";import p from"node:path";import{fileURLToPath as le}from"node:url";import{Hono as me}from"hono";import{listSessions as L}from"../sessions.js";import{renderLoginPage as fe,renderNotesIndex as pe,renderNotesPage as he,renderSettings as ge,renderThemeSettings as ye,renderHistoryIndex as je,renderQuickCommandsPage as we,renderFilesIndex as ke,renderShell as ve}from"../frontend.js";import{renderFederationPage as qe}from"./pages/federation.js";import{createAgentToken as Se,removeAgentToken as Te,listAgentTokens as z}from"./agent-tokens.js";import{hashPassword as Ae,verifyPassword as be,validatePassword as xe}from"./auth.js";import{atomicWriteFileSync as _e}from"./atomicWrite.js";import{resolveFsPath as b,resolveFsRoots as B,MAX_FILE_BYTES as V,walkRecursive as Ce}from"./fs-access.js";import{captureSessionWindowsWithPath as Oe}from"./tmux-windows.js";import{audit as j}from"./auditLog.js";import{db as S}from"./db.js";import{getSessionAccessMap as Ee}from"./session-access.js";import{listWindowHistory as Ie,clearWindowHistory as Ne}from"./window-history.js";import{saveSecurityConfig as Re}from"./security-config.js";import{readSettings as Q,writeSettings as Pe}from"./settings.js";import{setActiveThemeTemplate as K}from"./theme-store.js";import{isThemeTemplateId as X,THEME_TEMPLATE_IDS as Le}from"./themes/index.js";import{pinView as De,unpinView as Fe,listPinnedViews as He}from"./pinned-views.js";import{getStoredWindows as Ue}from"./session-windows.js";import{buildSidebarSessions as Me}from"./sessions-sidebar.js";import{createQuickCommand as We,deleteQuickCommand as $e,listQuickCommands as Y,updateQuickCommand as Ge}from"./quick-commands.js";import{getSystemStatus as ze,getTopProcesses as Be,killProcess as Ve}from"./system-monitor.js";import{newTmuxSession as Qe,renameSession as Ke,killSession as Xe,TmuxWindowsError as Ye}from"./tmux-windows.js";import{registerAgentTunnel as Ze}from"./agent-tunnel.js";import{injectScopeScript as Je}from"./scope-script.js";const D="tmux-web-token",et=365;function tt(x){return x==="127.0.0.1"||x==="::1"||x==="::ffff:127.0.0.1"}function Rt(x){const{mode:Z,securityConfig:h,tokenStore:y,rateLimiter:O,settings:rt,terminalBufferConfig:E,getAgentId:F,state:u,federation:w,hub:q}=x,k=Z==="agent";function C(e){if(h.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 I(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*)${D}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function nt(e){const t=I(e);return t?y.validateToken(t):null}function H(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${D}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${et*86400}${r?"; Secure":""}`)}function J(e){e.header("Set-Cookie",`${D}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}const o=()=>k?async(e,t)=>t():async(e,t)=>{const r=I(e);return!r||!y.validateToken(r)?(j("http_unauthorized",{ip:C(e)}),e.json({error:"unauthorized"},401)):t()},g=()=>k?async(e,t)=>t():async(e,t)=>{const r=I(e);if(!r||!y.validateToken(r)){const n=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${n}`,302)}return t()};function N(e){const t=Me(L(),Ee(),He()),r=[];if(q)for(const n of q.agents.list()){const i=q.agents.get(n.agentId);r.push({agentId:n.agentId,agentName:n.name,online:n.online,sessions:i?i.sessions:[]})}return{...t,currentSession:e??null,agents:r}}function U(e){const t=typeof e.sessionName=="string"?e.sessionName.trim():"";if(!t)return{error:"sessionName is required"};if(e.windowIndex===void 0)return{sessionName:t};const r=e.windowIndex;return typeof r!="number"||!Number.isInteger(r)||r<0?{error:"windowIndex must be a non-negative integer"}:{sessionName:t,windowIndex:r}}const s=new me;s.use("*",async(e,t)=>{await t(),e.header("X-Content-Type-Options","nosniff"),e.header("X-Frame-Options","DENY"),e.header("Content-Security-Policy",["default-src 'self'","script-src 'self' 'unsafe-inline'","style-src 'self' 'unsafe-inline' https://fonts.googleapis.com","connect-src 'self' ws: wss: http: https:","img-src 'self' data:","font-src 'self' https://fonts.gstatic.com","worker-src 'self' blob:"].join("; ")),e.header("Referrer-Policy","strict-origin-when-cross-origin")}),s.use("*",async(e,t)=>{const r=e.req.method;if(r==="GET"||r==="HEAD"||r==="OPTIONS")return t();const n=e.req.header("sec-fetch-site");return n&&n!=="same-origin"&&n!=="none"?e.text("cross-site request blocked",403):t()}),k||s.use("*",async(e,t)=>{if(await t(),e.req.method!=="GET"||e.req.path==="/login"||!(e.res.headers.get("content-type")??"").includes("text/html"))return;const n=await e.res.text();e.res=new Response(Je(n,{prefix:"",agentId:null}),e.res)});const M=p.dirname(le(import.meta.url)),ee=[p.join(M,"..","assets"),p.join(M,"assets"),p.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 ee){const n=p.join(r,t);if(!P(n))continue;const i=await ce(n),a=p.extname(t),c={".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":c[a]??"application/octet-stream","Cache-Control":a===".js"||a===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const te=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
2
|
<rect x="2.5" y="5.5" width="27" height="21" rx="4" fill="#0d1117" stroke="#7dd3fc" stroke-width="2"/>
|
|
3
3
|
<path d="M8 13l4.2 3.1L8 19.2" fill="none" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
4
|
<line x1="15.5" y1="19.6" x2="22.5" y2="19.6" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round"/>
|
|
@@ -13,7 +13,7 @@ self.addEventListener("activate", (e) => { e.waitUntil(clients.claim()); });
|
|
|
13
13
|
self.addEventListener("fetch", (e) => {
|
|
14
14
|
e.respondWith(fetch(e.request).catch(() => caches.match(e.request)));
|
|
15
15
|
});
|
|
16
|
-
`;s.get("/sw.js",e=>e.body(ne,200,{"Content-Type":"application/javascript; charset=utf-8"})),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!h.passwordHash,n=e.req.query("error");return e.html(
|
|
17
|
-
`)){if(!d.trim())continue;const
|
|
18
|
-
`)){if(!
|
|
16
|
+
`;s.get("/sw.js",e=>e.body(ne,200,{"Content-Type":"application/javascript; charset=utf-8"})),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!h.passwordHash,n=e.req.query("error");return e.html(fe({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:u.activeTheme}))}),s.get("/",g(),e=>{const t=B();return e.html(ve({theme:u.activeTheme,fsRoots:t,terminalCfg:E,scrollback:E.initialLines+2*E.historyChunk,wsBase:k&&F?.()?`/ws/a/${F()}`:void 0}))}),s.get("/notes",g(),e=>e.html(pe(S.data.notes,u.activeTheme))),s.get("/notes/:session",g(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.html(he(t,u.activeTheme))}),s.get("/history",g(),e=>{const t=L(),r=new Set(t.map(n=>n.name));return e.html(je(Ie(),u.activeTheme,r))}),s.get("/quick-commands",g(),e=>e.html(we(Y(),u.activeTheme))),s.get("/files",g(),e=>{const t=B();return e.html(ke(u.activeTheme,t))}),s.post("/api/history/clear",o(),async e=>(await Ne(),e.json({ok:!0}))),s.get("/api/quick-commands",o(),e=>e.json(Y())),s.post("/api/quick-commands",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await We(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Ge(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),s.delete("/api/quick-commands/:id",o(),e=>$e(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{if(k)return e.json({error:"hub only"},403);const t=C(e),r=O.check(t);if(!r.allowed)return j("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(!h.passwordHash){if(!h.security.allowRemoteSetup&&!tt(t))return j("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(u.settingUpPassword)return e.json({error:"Password setup in progress"},409);const l=xe(i);if(l)return e.json({error:l},400);u.settingUpPassword=!0;try{h.passwordHash=await Ae(i),Re(h),j("password_set",{ip:t})}finally{u.settingUpPassword=!1}const{plaintext:d}=y.createAccessToken("setup",h.security.tokenTtlDays);return H(e,d),e.json({ok:!0,token:d,setupMode:!0})}if(!await be(i,h.passwordHash)){const l=O.recordFailure(t);return j("auth_failed",{ip:t,method:"password",failures:l.failures,permanentLock:l.permanentLock}),l.permanentLock?(y.revokeAll(),j("permanent_lock",{ip:t,failures:l.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):l.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:l.retryAfterMs},429)}O.recordSuccess(t);const c=`browser-${t}`,{plaintext:v}=y.createAccessToken(c,h.security.tokenTtlDays);return H(e,v),j("auth_success",{ip:t,method:"password",tokenName:c}),e.json({ok:!0,token:v})}),s.post("/api/auth/token",o(),async e=>{if(k)return e.json({error:"hub only"},403);const t=C(e);let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const n=typeof r.name=="string"?r.name:`api-${t}`,i=typeof r.ttlDays=="number"?r.ttlDays:h.security.tokenTtlDays,{stored:a,plaintext:c}=y.createAccessToken(n,i);return j("token_created",{ip:t,name:a.name,tokenId:a.id}),e.json({id:a.id,name:a.name,token:c,expiresAt:a.expiresAt})}),s.get("/api/auth/tokens",o(),e=>k?e.json({error:"hub only"},403):e.json(y.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",o(),e=>k?e.json({error:"hub only"},403):y.revoke(e.req.param("id"))?(j("token_revoked",{ip:C(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",o(),e=>(J(e),e.json({ok:!0}))),s.get("/settings",g(),async e=>{const t=await Q();return e.html(ge({settings:t,theme:u.activeTheme,saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",o(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await Q(),n=t.defaultView==="recent"?"recent":"default";return await Pe({...r,defaultView:n}),e.redirect("/settings?saved=1",303)}),s.get("/settings/theme",g(),e=>e.html(ye({theme:u.activeTheme,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",o(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const r=t.template;return typeof r!="string"||!X(r)?e.redirect("/settings/theme",303):(u.activeTheme=await K(r),e.redirect("/settings/theme?saved=1",303))});const se={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",o(),e=>e.json({active:u.activeTheme.template,templates:Le.map(t=>({id:t,name:se[t]}))})),s.post("/api/theme",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=t.template;return typeof r!="string"||!X(r)?e.json({error:"invalid theme template"},400):(u.activeTheme=await K(r),e.json({ok:!0,active:u.activeTheme.template}))}),s.get("/api/system/status",o(),e=>e.json(ze())),s.get("/api/system/processes",o(),e=>e.json(Be())),w&&(s.get("/settings/federation",g(),e=>e.redirect("/settings/machines",301)),s.get("/settings/machines",g(),async e=>e.html(qe({config:w.getConfig(),status:w.status(),tokens:z().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})),hostname:w.hostname},u.activeTheme))),s.get("/api/machines",o(),e=>e.json({config:w.getConfig(),status:w.status()})),s.post("/api/machines",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.hub=="string"?t.hub.trim():"",n=typeof t.token=="string"?t.token.trim():"",i=typeof t.name=="string"?t.name.trim().slice(0,64):"",a=t.enabled===!0;if(a){if(!r)return e.json({error:"hub URL is required"},400);if(!/^wss?:\/\//i.test(r))return e.json({error:"hub URL must start with ws:// or wss://"},400);if(!n)return e.json({error:"registration token is required"},400)}const c={hub:r||void 0,token:n||void 0,name:i||void 0,enabled:a};return await w.setConfig(c),e.json({ok:!0,status:w.status()})}),s.get("/api/agent-tokens",o(),e=>e.json(z().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})))),s.post("/api/agent-tokens",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim().slice(0,64):"";if(!r)return e.json({error:"name is required"},400);const{id:n,token:i}=Se(r);return e.json({ok:!0,id:n,name:r,token:i})}),s.delete("/api/agent-tokens/:id",o(),e=>{const t=e.req.param("id");return Te(t)?e.json({ok:!0}):e.json({error:"no such token"},404)})),s.post("/api/system/kill",o(),async e=>{const{pid:t}=await e.req.json();if(typeof t!="number"||!Number.isInteger(t)||t<=0)return e.json({error:"invalid pid"},400);const r=Ve(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),s.get("/api/agents",o(),e=>q?e.json(q.agents.list()):e.json([])),s.post("/api/sessions/new",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(r))return e.json({error:"name contains invalid characters"},400);const n=typeof t.dir=="string"&&t.dir.trim()?t.dir.trim():void 0;if(L().some(a=>a.name===r))return e.json({error:"session already exists"},409);try{return Qe(r,n),e.json({ok:!0})}catch(a){const c=a instanceof Ye?a.message:"failed to create session";return e.json({error:c},500)}}),s.post("/api/sessions/rename",o(),async e=>{try{const{oldName:t,newName:r}=await e.req.json();return!t||!r?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(r)?(Ke(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),s.post("/api/sessions/kill",o(),async e=>{try{const{name:t}=await e.req.json();return t?(Xe(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",o(),e=>{const t=e.req.query("session");if(!t)return e.json({error:"session is required"},400);try{const r=Oe(t),i=r.find(a=>a.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",o(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=p.join(t,r));const n=e.req.query("recursive")==="true";let i=r,a="",c=r.endsWith("/");if(!c)try{c=A(r).isDirectory()}catch{}c||(i=p.dirname(r),a=p.basename(r).toLowerCase());try{const v=ie(i),l=[],d=[];for(const m of v)if(!m.startsWith(".")&&!(a&&!m.toLowerCase().startsWith(a))){try{const f=p.join(i,m);A(f).isDirectory()?(l.push(f),n&&Ce(f,l,d,0)):d.push(f)}catch{}if(l.length+d.length>=5e3)break}return e.json({dirs:l,files:d})}catch{return e.json({dirs:[],files:[]})}});function oe(e){const t={repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0};try{const r=_("git rev-parse --show-toplevel 2>/dev/null",{cwd:e,encoding:"utf-8",timeout:3e3}).trim();if(!r)return t;let n;try{n=_("git branch --show-current 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}catch{n=_("git rev-parse --short HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}n||(n="HEAD");let i="";try{i=_("git status --porcelain 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3})}catch{}const a=[];for(const d of i.split(`
|
|
17
|
+
`)){if(!d.trim())continue;const m=d[0],f=d[1],T=d.substring(3).trim(),R=f!==" "?f:m;a.push({path:T,status:R,staged:m,unstaged:f,additions:0,deletions:0})}const c=new Map;try{const d=_("git diff --numstat HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3});for(const m of d.split(`
|
|
18
|
+
`)){if(!m.trim())continue;const f=m.split(" ");if(f.length>=3){const T=parseInt(f[0],10)||0,R=parseInt(f[1],10)||0;c.set(f[2],{added:T,deleted:R})}}}catch{}for(const d of a){const m=c.get(d.path);m&&(d.additions=m.added,d.deletions=m.deleted)}let v=0,l=0;for(const d of a)v+=d.additions,l+=d.deletions;for(const d of a)if(d.status==="?"&&d.additions===0)try{const m=p.join(r,d.path),T=$(m,"utf-8").split(`
|
|
19
19
|
`).length;d.additions=T,v+=T}catch{}return{repoRoot:r,branch:n,files:a,linesAdded:v,linesRemoved:l}}catch{return t}}return s.get("/api/git/status",o(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=b(t),n=oe(r);return e.json(n)}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0})}}),s.get("/api/git/diff",o(),e=>{const t=e.req.query("path"),r=e.req.query("file");if(!t||!r)return e.json({error:"path and file are required"},400);try{const n=b(t);if(!P(n)||!A(n).isDirectory())return e.json({error:"invalid repository path: "+n},400);try{const i=G("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),a=G("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:i,stagedDiff:a})}catch(i){throw console.error("[git/diff] execFileSync failed:",i?.code,i.message),i}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),s.get("/api/file",o(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=b(t);if(!A(r).isFile())return e.json({error:"not a file"},400);const n=A(r).size;if(n>V)return e.json({error:"file too large",size:n,maxBytes:V},413);const i=$(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",o(),async e=>{try{const t=await e.req.json();if(!t.path||typeof t.content!="string")return e.json({error:"path and content are required"},400);const r=b(t.path);return _e(r,t.content),e.json({ok:!0})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),s.post("/api/file/delete",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=b(t.path);return A(r).isFile()?(ae(r),e.json({ok:!0})):e.json({error:"not a file"},400)}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"delete failed"},500)}}),s.post("/api/file/touch",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=b(t.path);return P(r)?e.json({error:"file already exists"},409):(ue(p.dirname(r),{recursive:!0}),de(r,"","utf-8"),e.json({ok:!0,path:r}))}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"touch failed"},500)}}),s.get("/api/sidebar/sessions",o(),e=>{const t=e.req.query("currentSession");return e.json(N(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",o(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(Ue(t))}),s.post("/api/pinned-views",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=U(t);return"error"in r?e.json({error:r.error},400):(await De(r.sessionName,r.windowIndex),e.json(N()))}),s.delete("/api/pinned-views",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=U(t);return"error"in r?e.json({error:r.error},400):(await Fe(r.sessionName,r.windowIndex),e.json(N()))}),s.get("/api/notes",o(),e=>{const t=[...S.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",o(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=S.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",o(),async e=>{const t=decodeURIComponent(e.req.param("scope"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}if(typeof r.content!="string")return e.json({error:"content must be string"},400);const n={scope:t,content:r.content,updatedAt:Date.now()},i=S.data.notes.findIndex(a=>a.scope===t);return i>=0?S.data.notes[i]=n:S.data.notes.push(n),await S.write(),e.json({ok:!0})}),q&&(s.use("/a/:agentId/*",o()),Ze(s,{getChannel:q.getChannel})),s}export{Rt as buildApp};
|
package/dist/lib/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ Usage:
|
|
|
18
18
|
tmux-web agent --hub <url> --token <t> [--name <n>] Run as a remote agent
|
|
19
19
|
|
|
20
20
|
Prefer the web UI: every machine runs the same "tmux-web" command; use
|
|
21
|
-
/settings/
|
|
21
|
+
/settings/machines to join a hub or to create/revoke agent tokens.
|
|
22
22
|
|
|
23
23
|
Files:
|
|
24
24
|
${u} settings (agents)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import{cssVarsStyle as c}from"../theme.js";function e(o){return o.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function b(o,i){const{config:n,status:
|
|
1
|
+
import{cssVarsStyle as c}from"../theme.js";function e(o){return o.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function b(o,i){const{config:n,status:a,tokens:r,hostname:s}=o,l=a.state==="connected"?"Connected":a.state==="connecting"?"Connecting\u2026":a.state==="error"?"Error":"Off",d=r.length?r.map(t=>`<div class="token-row" data-id="${e(t.id)}">
|
|
2
2
|
<div class="tok-info">
|
|
3
|
-
<div class="tok-name">${e(
|
|
4
|
-
<div class="tok-meta">${e(
|
|
3
|
+
<div class="tok-name">${e(t.name)}</div>
|
|
4
|
+
<div class="tok-meta">${e(t.id)} \xB7 created ${e(new Date(t.createdAt).toISOString().slice(0,16).replace("T"," "))} UTC</div>
|
|
5
5
|
</div>
|
|
6
|
-
<button class="btn danger" data-revoke="${e(
|
|
6
|
+
<button class="btn danger" data-revoke="${e(t.id)}">Revoke</button>
|
|
7
7
|
</div>`).join(`
|
|
8
8
|
`):'<p class="desc" style="margin:0">No agent tokens yet.</p>';return`<!DOCTYPE html>
|
|
9
9
|
<html lang="en">
|
|
@@ -11,7 +11,7 @@ import{cssVarsStyle as c}from"../theme.js";function e(o){return o.replace(/&/g,"
|
|
|
11
11
|
<meta charset="UTF-8" />
|
|
12
12
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
13
13
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
14
|
-
<title>
|
|
14
|
+
<title>Machines - tmux-web</title>
|
|
15
15
|
<style>
|
|
16
16
|
${c(i.shell)}
|
|
17
17
|
html, body { background: var(--page-bg); color: var(--page-fg); min-height: 100%; font-family: var(--font-sans); }
|
|
@@ -81,7 +81,7 @@ import{cssVarsStyle as c}from"../theme.js";function e(o){return o.replace(/&/g,"
|
|
|
81
81
|
<body>
|
|
82
82
|
<div class="container">
|
|
83
83
|
<div class="page-header">
|
|
84
|
-
<h1>
|
|
84
|
+
<h1>Machines</h1>
|
|
85
85
|
<a href="/settings" class="back-link">Back</a>
|
|
86
86
|
</div>
|
|
87
87
|
|
|
@@ -95,11 +95,11 @@ import{cssVarsStyle as c}from"../theme.js";function e(o){return o.replace(/&/g,"
|
|
|
95
95
|
No ports need to be opened. Get a token on the hub with <code>tmux-web agent-token add --name ${e(s)}</code>.</p>
|
|
96
96
|
|
|
97
97
|
<div class="status-line">
|
|
98
|
-
<span class="dot ${
|
|
98
|
+
<span class="dot ${a.state==="connected"?"connected":a.state==="connecting"?"connecting":a.state==="error"?"error":"off"}"></span>
|
|
99
99
|
<span class="stext">${e(l)}</span>
|
|
100
|
-
${
|
|
101
|
-
${
|
|
102
|
-
${
|
|
100
|
+
${a.agentId?`<span class="sub">agentId ${e(a.agentId)}</span>`:""}
|
|
101
|
+
${a.hub?`<span class="sub">\u2192 ${e(a.hub)}</span>`:""}
|
|
102
|
+
${a.lastError?`<span class="err">${e(a.lastError)}</span>`:""}
|
|
103
103
|
</div>
|
|
104
104
|
|
|
105
105
|
<div class="field">
|
|
@@ -159,7 +159,7 @@ import{cssVarsStyle as c}from"../theme.js";function e(o){return o.replace(/&/g,"
|
|
|
159
159
|
if (body.enabled && !body.hub) return flash('Hub URL is required.');
|
|
160
160
|
if (body.enabled && !body.token) return flash('Registration token is required.');
|
|
161
161
|
try {
|
|
162
|
-
const res = await fetch('/api/
|
|
162
|
+
const res = await fetch('/api/machines', {
|
|
163
163
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
|
164
164
|
});
|
|
165
165
|
const data = await res.json();
|
|
@@ -172,7 +172,7 @@ import{cssVarsStyle as c}from"../theme.js";function e(o){return o.replace(/&/g,"
|
|
|
172
172
|
$('f-disconnect').addEventListener('click', async () => {
|
|
173
173
|
clearFlash();
|
|
174
174
|
try {
|
|
175
|
-
const res = await fetch('/api/
|
|
175
|
+
const res = await fetch('/api/machines', {
|
|
176
176
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
177
177
|
body: JSON.stringify({ hub: $('f-hub').value.trim(), token: $('f-token').value.trim(), name: $('f-name').value.trim(), enabled: false }),
|
|
178
178
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cssVarsStyle as m}from"../theme.js";import{getThemeTemplates as g,THEME_TEMPLATE_IDS as
|
|
1
|
+
import{cssVarsStyle as m}from"../theme.js";import{getThemeTemplates as g,THEME_TEMPLATE_IDS as h}from"../themes/index.js";function o(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function l(e,a){return`<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
@@ -77,7 +77,7 @@ import{cssVarsStyle as m}from"../theme.js";import{getThemeTemplates as g,THEME_T
|
|
|
77
77
|
.num-input { width: 100%; }
|
|
78
78
|
}
|
|
79
79
|
</style>
|
|
80
|
-
</head>`}function
|
|
80
|
+
</head>`}function v(e,a){let r="";return e&&(r+='<div class="saved-flash">\u2713 Saved. Restart tmux-web to apply.</div>'),a&&(r+=`<div class="error-flash">${o(a)}</div>`),r}function y(e){const{settings:a,theme:r,saved:n=!1,error:s}=e,t=a.defaultView??"default";return`${l("Settings",r)}
|
|
81
81
|
<body>
|
|
82
82
|
<div class="container">
|
|
83
83
|
<div class="page-header">
|
|
@@ -90,7 +90,7 @@ import{cssVarsStyle as m}from"../theme.js";import{getThemeTemplates as g,THEME_T
|
|
|
90
90
|
<strong>restart the tmux-web process</strong> after making changes for them to take effect.
|
|
91
91
|
</div>
|
|
92
92
|
|
|
93
|
-
${
|
|
93
|
+
${v(n,s)}
|
|
94
94
|
|
|
95
95
|
<form method="POST" action="/settings">
|
|
96
96
|
<div class="section">
|
|
@@ -114,20 +114,20 @@ import{cssVarsStyle as m}from"../theme.js";import{getThemeTemplates as g,THEME_T
|
|
|
114
114
|
</div>
|
|
115
115
|
|
|
116
116
|
<div class="section">
|
|
117
|
-
<h2>
|
|
117
|
+
<h2>Machines</h2>
|
|
118
118
|
<p class="desc">Join another tmux-web as an agent (same start command), or manage tokens so other machines can join this one.</p>
|
|
119
|
-
<a href="/settings/
|
|
119
|
+
<a href="/settings/machines" class="btn" style="display:inline-block;text-decoration:none">Machines settings \u2192</a>
|
|
120
120
|
</div>
|
|
121
121
|
</div>
|
|
122
122
|
</body>
|
|
123
|
-
</html>`}const x={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"},b=["background","foreground","red","green","yellow","blue","magenta","cyan"];function w(e){const{theme:a,saved:r=!1}=e,n=g(),s=
|
|
123
|
+
</html>`}const x={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"},b=["background","foreground","red","green","yellow","blue","magenta","cyan"];function w(e){const{theme:a,saved:r=!1}=e,n=g(),s=h.map(t=>{const d=n[t],i=a.template===t,p=b.map(c=>`<span class="swatch" style="background:${o(d.terminal[c])}" title="${c}"></span>`).join("");return`<label class="theme-card${i?" active":""}">
|
|
124
124
|
<div class="tname">
|
|
125
125
|
<input type="radio" name="template" value="${t}" ${i?"checked":""} style="accent-color:var(--panel-success)" />
|
|
126
126
|
${o(x[t]??t)}
|
|
127
127
|
</div>
|
|
128
128
|
<div class="swatches">${p}</div>
|
|
129
129
|
</label>`}).join(`
|
|
130
|
-
`);return`${
|
|
130
|
+
`);return`${l("Theme",a)}
|
|
131
131
|
<body>
|
|
132
132
|
<div class="container">
|
|
133
133
|
<div class="page-header">
|
package/dist/lib/pages/shell.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cssVarsStyle as d}from"../theme.js";import{newSessionModalCSS as c,newSessionModalHTML as f,newSessionModalScript as g,reducedMotion as
|
|
1
|
+
import{cssVarsStyle as d}from"../theme.js";import{newSessionModalCSS as c,newSessionModalHTML as f,newSessionModalScript as g,reducedMotion as b}from"../shared-layout.js";import{icon as i}from"../icons.js";function h(t){return t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function o(t="var(--panel-accent)"){return`box-shadow: 0 0 0 2px ${t}; outline: none;`}function y(t){const{theme:e,fsRoots:n,terminalCfg:r,scrollback:a,wsBase:s}=t,p=`
|
|
2
2
|
*, *::before, *::after { box-sizing: border-box; }
|
|
3
3
|
html, body {
|
|
4
4
|
background: var(--page-bg); color: var(--page-fg);
|
|
@@ -499,6 +499,16 @@ import{cssVarsStyle as d}from"../theme.js";import{newSessionModalCSS as c,newSes
|
|
|
499
499
|
flex: 1; overflow: hidden; position: relative;
|
|
500
500
|
}
|
|
501
501
|
#terminal-container.terminal-pending { visibility: hidden; }
|
|
502
|
+
#terminal-container.terminal-pending::before {
|
|
503
|
+
content: "Connecting\u2026";
|
|
504
|
+
visibility: visible;
|
|
505
|
+
position: absolute; inset: 0; z-index: 5;
|
|
506
|
+
display: flex; align-items: center; justify-content: center;
|
|
507
|
+
color: var(--panel-muted); font-size: 13px;
|
|
508
|
+
background: var(--panel-bg);
|
|
509
|
+
animation: tw-pulse 1.2s ease-in-out infinite;
|
|
510
|
+
}
|
|
511
|
+
@keyframes tw-pulse { 50% { opacity: 0.35; } }
|
|
502
512
|
#terminal-container .terminal-drag-over {
|
|
503
513
|
outline: 2px dashed var(--panel-accent);
|
|
504
514
|
outline-offset: -2px;
|
|
@@ -580,7 +590,7 @@ import{cssVarsStyle as d}from"../theme.js";import{newSessionModalCSS as c,newSes
|
|
|
580
590
|
.file-roots-list li:hover { border-color: var(--panel-accent); }
|
|
581
591
|
.file-roots-list li svg { width: 20px; height: 20px; flex-shrink: 0; fill: var(--panel-accent); }
|
|
582
592
|
|
|
583
|
-
${
|
|
593
|
+
${b()}
|
|
584
594
|
${c()}
|
|
585
595
|
@media (max-width: 640px) {
|
|
586
596
|
.app-layout { flex-direction: column; }
|
|
@@ -592,7 +602,7 @@ import{cssVarsStyle as d}from"../theme.js";import{newSessionModalCSS as c,newSes
|
|
|
592
602
|
.mobile-keys .mk-buttons { display: none; }
|
|
593
603
|
.mobile-keys.mk-focused .mk-buttons { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
594
604
|
}
|
|
595
|
-
`,
|
|
605
|
+
`,x=JSON.stringify(e.terminal).replace(/</g,"\\u003c"),l=JSON.stringify({terminal:r,scrollback:a,theme:e.terminal,fsRoots:n,wsBase:s||""}).replace(/</g,"\\u003c");return`<!DOCTYPE html>
|
|
596
606
|
<html lang="en">
|
|
597
607
|
<head>
|
|
598
608
|
<meta charset="UTF-8" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function n(s,r){if(!s)return r;const e=parseInt(s,10);return Number.isFinite(e)&&e>0?e:r}function i(){return{initialLines:n(process.env.TMUX_WEB_INITIAL_LINES,
|
|
1
|
+
function n(s,r){if(!s)return r;const e=parseInt(s,10);return Number.isFinite(e)&&e>0?e:r}function i(){return{initialLines:n(process.env.TMUX_WEB_INITIAL_LINES,400),historyChunk:n(process.env.TMUX_WEB_HISTORY_CHUNK,500),syncIdleMs:n(process.env.TMUX_WEB_SYNC_IDLE_MS,20),syncMaxMs:n(process.env.TMUX_WEB_SYNC_MAX_MS,200)}}export{i as readTerminalBufferConfig};
|