itwillsync 1.2.1 → 1.3.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.
@@ -0,0 +1,2 @@
1
+ (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))o(a);new MutationObserver(a=>{for(const s of a)if(s.type==="childList")for(const i of s.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&o(i)}).observe(document,{childList:!0,subtree:!0});function n(a){const s={};return a.integrity&&(s.integrity=a.integrity),a.referrerPolicy&&(s.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?s.credentials="include":a.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function o(a){if(a.ep)return;a.ep=!0;const s=n(a);fetch(a.href,s)}})();function B(e){const t=Math.floor(e/1e3);if(t<60)return`${t}s`;const n=Math.floor(t/60);if(n<60)return`${n}m`;const o=Math.floor(n/60),a=n%60;return`${o}h ${a}m`}function z(e){const t=e.match(/^\/(?:Users|home)\/[^/]+/)?.[0];return t?"~"+e.slice(t.length):e}function X(e){return e<1024?`${e} KB`:`${(e/1024).toFixed(1)} MB`}function H(e,t,n){const o=document.createElement("div");o.className=`session-card${e.status==="attention"?" attention":""}`,o.dataset.sessionId=e.id;const a=B(Date.now()-e.connectedAt),s=z(e.cwd),i=document.createElement("div");i.className="card-header";const c=document.createElement("div");c.className="card-agent";const p=document.createElement("div");p.className=`agent-dot ${e.status}`;const P=document.createElement("span");P.className="agent-name",P.textContent=e.name||e.agent,c.appendChild(p),c.appendChild(P);const k=document.createElement("span");k.className="card-uptime",k.textContent=a,i.appendChild(c),i.appendChild(k);const I=document.createElement("div");I.className="card-cwd",I.textContent=s;const h=document.createElement("div");h.className="card-preview";const $=document.createElement("pre");$.className="card-preview-text",$.textContent="Waiting for output...",h.appendChild($),h.addEventListener("click",l=>{l.stopPropagation(),n.onOpen(e)});const g=document.createElement("div");g.className="card-status";const E=document.createElement("span");E.className=`status-badge ${e.status}`,E.textContent=e.status,e.status==="attention"&&(E.style.display="none");const v=document.createElement("span");v.className="attention-badge",v.textContent="Needs your attention",e.status!=="attention"&&(v.style.display="none"),g.appendChild(E),g.appendChild(v);const f=document.createElement("div");f.className="card-actions";const C=document.createElement("button");C.className="action-btn stop",C.textContent="Stop",C.addEventListener("click",l=>{l.stopPropagation(),Q(o,e.id,n.onStop)});const w=document.createElement("button");w.className="action-btn rename",w.textContent="Rename",w.addEventListener("click",l=>{l.stopPropagation(),Z(o,e.id,n.onRename)});const N=document.createElement("button");N.className="action-btn info",N.textContent="Info",N.addEventListener("click",l=>{l.stopPropagation(),n.onInfo(e.id)});const b=document.createElement("button");b.className="action-btn open",b.textContent="Open",b.addEventListener("click",l=>{l.stopPropagation(),n.onOpen(e)}),f.appendChild(b),f.appendChild(w),f.appendChild(N),f.appendChild(C);const R=document.createElement("div");return R.className="card-metadata hidden",o.addEventListener("click",()=>{n.onOpen(e)}),o.appendChild(i),o.appendChild(I),o.appendChild(h),o.appendChild(g),o.appendChild(f),o.appendChild(R),o}function Q(e,t,n){e.querySelector(".confirm-overlay")?.remove();const o=document.createElement("div");o.className="confirm-overlay";const a=document.createElement("span");a.className="confirm-msg",a.textContent="Stop this session?";const s=document.createElement("button");s.className="confirm-btn yes",s.textContent="Yes",s.addEventListener("click",c=>{c.stopPropagation(),n(t),o.remove()});const i=document.createElement("button");i.className="confirm-btn no",i.textContent="No",i.addEventListener("click",c=>{c.stopPropagation(),o.remove()}),o.addEventListener("click",c=>c.stopPropagation()),o.appendChild(a),o.appendChild(s),o.appendChild(i),e.appendChild(o)}function Z(e,t,n){const o=e.querySelector(".agent-name");if(!o)return;const a=o.textContent||"",s=document.createElement("input");s.className="rename-input",s.type="text",s.value=a;const i=()=>{const c=s.value.trim();c&&c!==a&&n(t,c),o.textContent=c||a,o.style.display="",s.remove()};s.addEventListener("keydown",c=>{c.stopPropagation(),c.key==="Enter"?i():c.key==="Escape"&&(o.textContent=a,o.style.display="",s.remove())}),s.addEventListener("blur",i),s.addEventListener("click",c=>c.stopPropagation()),o.style.display="none",o.parentElement?.insertBefore(s,o.nextSibling),s.focus(),s.select()}function G(e,t){const n=e.querySelector(".agent-dot"),o=e.querySelector(".status-badge"),a=e.querySelector(".card-uptime"),s=e.querySelector(".agent-name");n&&(n.className=`agent-dot ${t.status}`),o&&(o.className=`status-badge ${t.status}`,o.textContent=t.status),a&&(a.textContent=B(Date.now()-t.connectedAt)),s&&!s.style.display&&(s.textContent=t.name||t.agent);const i=t.status==="attention";o&&(o.style.display=i?"none":"");const c=e.querySelector(".attention-badge");c&&(c.style.display=i?"":"none"),t.status==="attention"?e.classList.add("attention"):e.classList.remove("attention")}function ee(e,t){const n=e.querySelector(".card-preview-text");n&&(t.length>0?(n.textContent=t.join(`
2
+ `),n.classList.remove("empty")):(n.textContent="Waiting for output...",n.classList.add("empty")))}function te(e,t){const n=e.querySelector(".card-metadata");if(!n)return;for(;n.firstChild;)n.removeChild(n.firstChild);const o=[["PID",String(t.pid)],["Agent",t.agent],["Port",String(t.port)],["Directory",t.cwd],["Memory",X(t.memoryKB)],["Uptime",B(t.uptimeMs)]];for(const[a,s]of o){const i=document.createElement("div");i.className="meta-row";const c=document.createElement("span");c.className="meta-label",c.textContent=a;const p=document.createElement("span");p.className="meta-value",p.textContent=s,i.appendChild(c),i.appendChild(p),n.appendChild(i)}n.classList.toggle("hidden")}let r=null,q=!1;function ne(){if(!q){q=!0;try{r=new AudioContext,r.state==="suspended"&&r.resume()}catch{}}}function U(){if(!r)return;r.state==="suspended"&&r.resume();const e=r.currentTime,t=r.createOscillator(),n=r.createGain();t.frequency.value=587.33,t.type="sine",n.gain.setValueAtTime(.3,e),n.gain.exponentialRampToValueAtTime(.01,e+.3),t.connect(n),n.connect(r.destination),t.start(e),t.stop(e+.3);const o=r.createOscillator(),a=r.createGain();o.frequency.value=880,o.type="sine",a.gain.setValueAtTime(.3,e+.15),a.gain.exponentialRampToValueAtTime(.01,e+.45),o.connect(a),a.connect(r.destination),o.start(e+.15),o.stop(e+.45)}const D=120*1e3,u=new Map;function _(e){if(u.has(e))return;U();const t=setTimeout(function n(){U();const o=u.get(e);o&&(o.timerId=setTimeout(n,D))},D);u.set(e,{timerId:t})}function A(e){const t=u.get(e);t&&(clearTimeout(t.timerId),u.delete(e))}function V(){for(const e of u.values())clearTimeout(e.timerId);u.clear()}const oe=new URLSearchParams(window.location.search),J=oe.get("token");if(!J)throw document.body.textContent="Missing authentication token.",new Error("No token in URL");const ae=window.location.protocol==="https:"?"wss:":"ws:",se=`${ae}//${window.location.host}?token=${J}`,Y=window.location.hostname,y=new Map,m=new Map,ce=document.getElementById("session-list"),O=document.getElementById("empty-state"),ie=document.getElementById("session-count"),W=document.getElementById("status-dot");let M=null;function x(){ne(),document.removeEventListener("click",x),document.removeEventListener("touchstart",x)}document.addEventListener("click",x);document.addEventListener("touchstart",x);function S(e){d&&d.readyState===WebSocket.OPEN&&d.send(JSON.stringify(e))}const re={onOpen(e){const t=y.get(e.id)||e;t.status==="attention"&&(S({type:"clear-attention",sessionId:t.id}),A(t.id));const n=window.location.href,o=`http://${Y}:${t.port}?token=${t.token}&hub=${encodeURIComponent(n)}`;window.open(o,"_blank")},onStop(e){S({type:"stop-session",sessionId:e})},onRename(e,t){S({type:"rename-session",sessionId:e,name:t})},onInfo(e){S({type:"get-metadata",sessionId:e})}};function j(){const e=y.size;ie.textContent=`${e} session${e!==1?"s":""}`,e===0?O.style.display="flex":O.style.display="none"}function F(e){y.set(e.id,e);const t=H(e,Y,re);m.set(e.id,t),ce.insertBefore(t,O),j()}function K(e){y.delete(e);const t=m.get(e);t&&(t.remove(),m.delete(e)),j()}function de(e){y.set(e.id,e);const t=m.get(e.id);t&&G(t,e)}function le(){for(const[e,t]of y){const n=m.get(e);n&&G(n,t)}}let d=null,L=0;const me=1e4;function T(){d=new WebSocket(se),d.onopen=()=>{W.className="connected",L=0,M&&clearInterval(M),M=setInterval(le,1e4)},d.onmessage=e=>{try{const t=JSON.parse(e.data);switch(t.type){case"sessions":{V();for(const n of m.keys())K(n);for(const n of t.sessions)F(n),n.status==="attention"&&_(n.id);break}case"session-added":{F(t.session);break}case"session-removed":{const n=t.sessionId;A(n),K(n);break}case"session-updated":{const n=t.session;de(n),n.status==="attention"?_(n.id):A(n.id);break}case"preview":{const n=m.get(t.sessionId);n&&ee(n,t.lines);break}case"metadata":{const n=m.get(t.sessionId);n&&te(n,t.metadata);break}case"operation-error":{console.warn(`Operation "${t.operation}" failed for session ${t.sessionId}: ${t.error}`);break}}}catch{}},d.onclose=()=>{W.className="reconnecting",V(),ue()},d.onerror=()=>{d?.close()}}function ue(){const e=Math.min(1e3*Math.pow(1.5,L),me);L++,setTimeout(T,e)}document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&d?.readyState!==WebSocket.OPEN&&(L=0,T())});T();
@@ -0,0 +1 @@
1
+ *{margin:0;padding:0;box-sizing:border-box}html,body{height:100%;width:100%;background:#1a1a2e;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;color:#e0e0e0;overflow-x:hidden;-webkit-overflow-scrolling:touch}#header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#16213e;border-bottom:1px solid #0f3460;position:sticky;top:0;z-index:10}.header-left{display:flex;align-items:center;gap:10px}.logo{font-size:16px;font-weight:700;color:#e94560;letter-spacing:-.5px}#status-dot{width:8px;height:8px;border-radius:50%;background:#e74c3c;transition:background .3s ease}#status-dot.connected{background:#2ecc71}#status-dot.reconnecting{background:#f39c12;animation:pulse 1s infinite}#session-count{font-size:13px;color:#a0a0b0}#session-list{padding:12px 12px 80px;display:flex;flex-direction:column;gap:12px}#empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:60vh;color:#606080;text-align:center;gap:8px}#empty-state .empty-icon{font-size:48px;margin-bottom:8px;opacity:.5}#empty-state p{font-size:16px}#empty-state .empty-hint{font-size:13px;color:#505070}#empty-state code{background:#252540;padding:2px 8px;border-radius:4px;font-family:Cascadia Code,Fira Code,JetBrains Mono,monospace;font-size:12px;color:#e94560}.session-card{background:#16213e;border:1px solid #0f3460;border-radius:12px;padding:14px 16px;cursor:pointer;transition:transform .15s ease,border-color .2s ease;-webkit-tap-highlight-color:transparent}.session-card:active{transform:scale(.98)}.session-card:hover{border-color:#e94560}.session-card.attention{border-color:#e94560;animation:attention-glow 2s ease-in-out infinite}@keyframes attention-glow{0%,to{box-shadow:0 0 #e9456000}50%{box-shadow:0 0 12px 2px #e945604d}}.card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.card-agent{display:flex;align-items:center;gap:8px}.agent-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}.agent-dot.active{background:#2ecc71}.agent-dot.idle{background:#f39c12}.agent-dot.attention{background:#e94560;animation:pulse 1s infinite}.agent-name{font-size:15px;font-weight:600;color:#f0f0f0}.card-uptime{font-size:12px;color:#707090;font-variant-numeric:tabular-nums}.card-cwd{font-size:12px;color:#808098;margin-bottom:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:Cascadia Code,Fira Code,JetBrains Mono,monospace}.card-preview{background:#112;border-radius:6px;padding:8px 10px;margin-bottom:10px;overflow:hidden;min-height:24px;max-height:90px}.card-preview-text{font-family:Cascadia Code,Fira Code,JetBrains Mono,monospace;font-size:11px;line-height:1.4;color:#a0a0b0;white-space:pre;overflow:hidden;text-overflow:ellipsis;margin:0}.card-preview-text.empty{color:#505070;font-style:italic}.card-status{display:flex;align-items:center;gap:6px;font-size:12px;color:#a0a0b0}.status-badge{padding:2px 8px;border-radius:10px;font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:.5px}.status-badge.active{background:#2ecc7126;color:#2ecc71}.status-badge.idle{background:#f39c1226;color:#f39c12}.status-badge.attention{background:#e9456026;color:#e94560}.attention-badge{padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;background:#e9456033;color:#e94560;animation:attention-pulse 2s ease-in-out infinite}@keyframes attention-pulse{0%,to{opacity:1}50%{opacity:.6}}.card-actions{display:flex;gap:6px;margin-top:10px;padding-top:10px;border-top:1px solid #0f3460}.action-btn{flex:1;padding:6px 0;border:1px solid #2a2a44;border-radius:6px;background:#1e1e36;color:#a0a0b0;font-size:12px;font-weight:500;font-family:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent;transition:background .15s,color .15s}.action-btn:active{background:#2a2a44}.action-btn.open{color:#2ecc71;border-color:#2ecc714d}.action-btn.stop{color:#e94560;border-color:#e945604d}.action-btn.stop:active{background:#e9456026}.confirm-overlay{display:flex;align-items:center;justify-content:center;gap:10px;padding:10px;margin-top:8px;background:#2d1810;border:1px solid #e94560;border-radius:8px;animation:fadeIn .15s ease}.confirm-msg{font-size:13px;color:#f0f0f0;flex:1}.confirm-btn{padding:5px 14px;border:none;border-radius:5px;font-size:12px;font-weight:600;font-family:inherit;cursor:pointer}.confirm-btn.yes{background:#e94560;color:#fff}.confirm-btn.no{background:#2a2a44;color:#a0a0b0}.rename-input{background:#112;border:1px solid #e94560;border-radius:4px;color:#f0f0f0;font-size:15px;font-weight:600;font-family:inherit;padding:2px 6px;outline:none;width:120px}.card-metadata{margin-top:8px;padding:8px 10px;background:#112;border-radius:6px;animation:fadeIn .2s ease}.card-metadata.hidden{display:none}.meta-row{display:flex;justify-content:space-between;padding:3px 0;font-size:11px}.meta-label{color:#707090;font-weight:500}.meta-value{color:#c0c0d0;font-family:Cascadia Code,Fira Code,JetBrains Mono,monospace;font-size:11px;text-align:right;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media(min-width:768px){#session-list{max-width:600px;margin:0 auto}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.4}}@keyframes fadeIn{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.session-card{animation:fadeIn .3s ease}#reconnect-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:#1a1a2ef2;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:100;color:#e0e0e0;font-size:16px;gap:12px}#reconnect-overlay .spinner{width:32px;height:32px;border:3px solid #0f3460;border-top-color:#e94560;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}
@@ -0,0 +1,31 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no" />
6
+ <meta name="apple-mobile-web-app-capable" content="yes" />
7
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
8
+ <meta name="theme-color" content="#1a1a2e" />
9
+ <title>itwillsync Dashboard</title>
10
+ <script type="module" crossorigin src="/assets/index-DgUZUPW_.js"></script>
11
+ <link rel="stylesheet" crossorigin href="/assets/index-Erqx_a0N.css">
12
+ </head>
13
+ <body>
14
+ <header id="header">
15
+ <div class="header-left">
16
+ <span class="logo">itwillsync</span>
17
+ <span id="status-dot"></span>
18
+ </div>
19
+ <span id="session-count">0 sessions</span>
20
+ </header>
21
+
22
+ <main id="session-list">
23
+ <div id="empty-state">
24
+ <div class="empty-icon">&#x1F4BB;</div>
25
+ <p>No active sessions</p>
26
+ <p class="empty-hint">Start an agent with <code>itwillsync -- claude</code></p>
27
+ </div>
28
+ </main>
29
+
30
+ </body>
31
+ </html>
package/dist/index.js CHANGED
@@ -2766,7 +2766,7 @@ var require_websocket = __commonJS({
2766
2766
  }
2767
2767
  const defaultPort = isSecure ? 443 : 80;
2768
2768
  const key = randomBytes2(16).toString("base64");
2769
- const request = isSecure ? https.request : http.request;
2769
+ const request2 = isSecure ? https.request : http.request;
2770
2770
  const protocolSet = /* @__PURE__ */ new Set();
2771
2771
  let perMessageDeflate;
2772
2772
  opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
@@ -2843,12 +2843,12 @@ var require_websocket = __commonJS({
2843
2843
  if (opts.auth && !options.headers.authorization) {
2844
2844
  options.headers.authorization = "Basic " + Buffer.from(opts.auth).toString("base64");
2845
2845
  }
2846
- req = websocket._req = request(opts);
2846
+ req = websocket._req = request2(opts);
2847
2847
  if (websocket._redirects) {
2848
2848
  websocket.emit("redirect", websocket.url, req);
2849
2849
  }
2850
2850
  } else {
2851
- req = websocket._req = request(opts);
2851
+ req = websocket._req = request2(opts);
2852
2852
  }
2853
2853
  if (opts.timeout) {
2854
2854
  req.on("timeout", () => {
@@ -4149,8 +4149,233 @@ async function runSetupWizard() {
4149
4149
  return config;
4150
4150
  }
4151
4151
 
4152
+ // src/hub-client.ts
4153
+ import { spawn as spawn2 } from "child_process";
4154
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
4155
+ import { homedir as homedir2 } from "os";
4156
+ import { join as join4, dirname as dirname2 } from "path";
4157
+ import { fileURLToPath as fileURLToPath2 } from "url";
4158
+ import { request } from "http";
4159
+ var HUB_INTERNAL_PORT = 7963;
4160
+ var HUB_EXTERNAL_PORT = 7962;
4161
+ var SESSION_PORT_START = 7964;
4162
+ function getHubDir() {
4163
+ return process.env.ITWILLSYNC_CONFIG_DIR || join4(homedir2(), ".itwillsync");
4164
+ }
4165
+ function getHubConfigPath() {
4166
+ return join4(getHubDir(), "hub.json");
4167
+ }
4168
+ async function discoverHub() {
4169
+ return new Promise((resolve) => {
4170
+ const req = request(
4171
+ {
4172
+ hostname: "127.0.0.1",
4173
+ port: HUB_INTERNAL_PORT,
4174
+ path: "/api/health",
4175
+ method: "GET",
4176
+ timeout: 2e3
4177
+ },
4178
+ (res) => {
4179
+ let data = "";
4180
+ res.on("data", (chunk) => {
4181
+ data += chunk;
4182
+ });
4183
+ res.on("end", () => {
4184
+ try {
4185
+ const json = JSON.parse(data);
4186
+ resolve(json.status === "ok");
4187
+ } catch {
4188
+ resolve(false);
4189
+ }
4190
+ });
4191
+ }
4192
+ );
4193
+ req.on("error", () => resolve(false));
4194
+ req.on("timeout", () => {
4195
+ req.destroy();
4196
+ resolve(false);
4197
+ });
4198
+ req.end();
4199
+ });
4200
+ }
4201
+ async function spawnHub() {
4202
+ const __dirname = dirname2(fileURLToPath2(import.meta.url));
4203
+ const hubPath = join4(__dirname, "hub", "daemon.js");
4204
+ return new Promise((resolve, reject) => {
4205
+ const child = spawn2("node", [hubPath], {
4206
+ detached: true,
4207
+ stdio: ["ignore", "pipe", "ignore"],
4208
+ env: {
4209
+ ...process.env,
4210
+ // Pass config dir to hub
4211
+ ITWILLSYNC_CONFIG_DIR: process.env.ITWILLSYNC_CONFIG_DIR || ""
4212
+ }
4213
+ });
4214
+ child.unref();
4215
+ let output = "";
4216
+ const timeout = setTimeout(() => {
4217
+ reject(new Error("Hub daemon startup timed out"));
4218
+ }, 1e4);
4219
+ child.stdout?.on("data", (data) => {
4220
+ output += data.toString();
4221
+ if (output.includes("hub:ready:")) {
4222
+ clearTimeout(timeout);
4223
+ child.stdout?.destroy();
4224
+ resolve();
4225
+ }
4226
+ });
4227
+ child.on("error", (err) => {
4228
+ clearTimeout(timeout);
4229
+ reject(new Error(`Failed to spawn hub daemon: ${err.message}`));
4230
+ });
4231
+ child.on("exit", (code) => {
4232
+ clearTimeout(timeout);
4233
+ if (code !== null && code !== 0) {
4234
+ reject(new Error(`Hub daemon exited with code ${code}`));
4235
+ }
4236
+ });
4237
+ });
4238
+ }
4239
+ function getHubConfig() {
4240
+ const configPath = getHubConfigPath();
4241
+ if (!existsSync2(configPath)) {
4242
+ return null;
4243
+ }
4244
+ try {
4245
+ const raw = readFileSync2(configPath, "utf-8");
4246
+ return JSON.parse(raw);
4247
+ } catch {
4248
+ return null;
4249
+ }
4250
+ }
4251
+ async function registerSession(registration) {
4252
+ const body = JSON.stringify(registration);
4253
+ return new Promise((resolve, reject) => {
4254
+ const req = request(
4255
+ {
4256
+ hostname: "127.0.0.1",
4257
+ port: HUB_INTERNAL_PORT,
4258
+ path: "/api/sessions",
4259
+ method: "POST",
4260
+ headers: {
4261
+ "Content-Type": "application/json",
4262
+ "Content-Length": Buffer.byteLength(body)
4263
+ },
4264
+ timeout: 5e3
4265
+ },
4266
+ (res) => {
4267
+ let data = "";
4268
+ res.on("data", (chunk) => {
4269
+ data += chunk;
4270
+ });
4271
+ res.on("end", () => {
4272
+ try {
4273
+ const json = JSON.parse(data);
4274
+ if (res.statusCode === 201 && json.session) {
4275
+ resolve(json.session);
4276
+ } else {
4277
+ reject(new Error(`Registration failed: ${json.error || "Unknown error"}`));
4278
+ }
4279
+ } catch {
4280
+ reject(new Error("Invalid response from hub"));
4281
+ }
4282
+ });
4283
+ }
4284
+ );
4285
+ req.on("error", (err) => reject(new Error(`Failed to register with hub: ${err.message}`)));
4286
+ req.on("timeout", () => {
4287
+ req.destroy();
4288
+ reject(new Error("Registration request timed out"));
4289
+ });
4290
+ req.end(body);
4291
+ });
4292
+ }
4293
+ async function unregisterSession(sessionId) {
4294
+ return new Promise((resolve) => {
4295
+ const req = request(
4296
+ {
4297
+ hostname: "127.0.0.1",
4298
+ port: HUB_INTERNAL_PORT,
4299
+ path: `/api/sessions/${sessionId}`,
4300
+ method: "DELETE",
4301
+ timeout: 3e3
4302
+ },
4303
+ () => resolve()
4304
+ );
4305
+ req.on("error", () => resolve());
4306
+ req.on("timeout", () => {
4307
+ req.destroy();
4308
+ resolve();
4309
+ });
4310
+ req.end();
4311
+ });
4312
+ }
4313
+ async function listSessions() {
4314
+ return new Promise((resolve) => {
4315
+ const req = request(
4316
+ {
4317
+ hostname: "127.0.0.1",
4318
+ port: HUB_INTERNAL_PORT,
4319
+ path: "/api/sessions",
4320
+ method: "GET",
4321
+ timeout: 3e3
4322
+ },
4323
+ (res) => {
4324
+ let data = "";
4325
+ res.on("data", (chunk) => {
4326
+ data += chunk;
4327
+ });
4328
+ res.on("end", () => {
4329
+ try {
4330
+ const json = JSON.parse(data);
4331
+ resolve(json.sessions || []);
4332
+ } catch {
4333
+ resolve([]);
4334
+ }
4335
+ });
4336
+ }
4337
+ );
4338
+ req.on("error", () => resolve([]));
4339
+ req.on("timeout", () => {
4340
+ req.destroy();
4341
+ resolve([]);
4342
+ });
4343
+ req.end();
4344
+ });
4345
+ }
4346
+ function stopHub() {
4347
+ const config = getHubConfig();
4348
+ if (!config) return false;
4349
+ try {
4350
+ process.kill(config.pid, "SIGTERM");
4351
+ return true;
4352
+ } catch {
4353
+ return false;
4354
+ }
4355
+ }
4356
+ async function sendHeartbeat(sessionId) {
4357
+ return new Promise((resolve) => {
4358
+ const req = request(
4359
+ {
4360
+ hostname: "127.0.0.1",
4361
+ port: HUB_INTERNAL_PORT,
4362
+ path: `/api/sessions/${sessionId}/heartbeat`,
4363
+ method: "PUT",
4364
+ timeout: 2e3
4365
+ },
4366
+ () => resolve()
4367
+ );
4368
+ req.on("error", () => resolve());
4369
+ req.on("timeout", () => {
4370
+ req.destroy();
4371
+ resolve();
4372
+ });
4373
+ req.end();
4374
+ });
4375
+ }
4376
+
4152
4377
  // src/cli-options.ts
4153
- var DEFAULT_PORT = 3456;
4378
+ var DEFAULT_PORT = SESSION_PORT_START;
4154
4379
  function parseArgs(argv) {
4155
4380
  const options = {
4156
4381
  port: DEFAULT_PORT,
@@ -4159,13 +4384,24 @@ function parseArgs(argv) {
4159
4384
  command: [],
4160
4385
  subcommand: null,
4161
4386
  tailscale: false,
4162
- local: false
4387
+ local: false,
4388
+ hubInfo: false,
4389
+ hubStop: false,
4390
+ hubStatus: false
4163
4391
  };
4164
4392
  const args = argv.slice(2);
4165
4393
  if (args.length > 0 && args[0] === "setup") {
4166
4394
  options.subcommand = "setup";
4167
4395
  return options;
4168
4396
  }
4397
+ if (args.length > 0 && args[0] === "hub") {
4398
+ const hubAction = args[1] || "info";
4399
+ if (hubAction === "info") options.hubInfo = true;
4400
+ else if (hubAction === "stop") options.hubStop = true;
4401
+ else if (hubAction === "status") options.hubStatus = true;
4402
+ else options.hubInfo = true;
4403
+ return options;
4404
+ }
4169
4405
  let i = 0;
4170
4406
  while (i < args.length) {
4171
4407
  const arg = args[i];
@@ -4187,6 +4423,15 @@ function parseArgs(argv) {
4187
4423
  } else if (arg === "--no-qr") {
4188
4424
  options.noQr = true;
4189
4425
  i++;
4426
+ } else if (arg === "--hub-info") {
4427
+ options.hubInfo = true;
4428
+ i++;
4429
+ } else if (arg === "--hub-stop") {
4430
+ options.hubStop = true;
4431
+ i++;
4432
+ } else if (arg === "--hub-status") {
4433
+ options.hubStatus = true;
4434
+ i++;
4190
4435
  } else if (arg === "--help" || arg === "-h") {
4191
4436
  printHelp();
4192
4437
  process.exit(0);
@@ -4208,6 +4453,7 @@ Usage:
4208
4453
  itwillsync [options] -- <command> [args...]
4209
4454
  itwillsync [options] <command> [args...]
4210
4455
  itwillsync setup
4456
+ itwillsync hub [info|stop|status]
4211
4457
 
4212
4458
  Examples:
4213
4459
  itwillsync -- claude
@@ -4216,9 +4462,14 @@ Examples:
4216
4462
  itwillsync --port 8080 -- claude
4217
4463
  itwillsync --tailscale -- claude
4218
4464
  itwillsync setup
4465
+ itwillsync hub info
4466
+ itwillsync hub stop
4219
4467
 
4220
4468
  Commands:
4221
4469
  setup Run the setup wizard (configure networking mode)
4470
+ hub info Show dashboard URL, QR code, and hub status
4471
+ hub stop Stop the hub daemon and all sessions
4472
+ hub status List all active sessions
4222
4473
 
4223
4474
  Options:
4224
4475
  --port <number> Port to listen on (default: ${DEFAULT_PORT})
@@ -4228,24 +4479,29 @@ Options:
4228
4479
  --no-qr Don't display QR code
4229
4480
  -h, --help Show this help
4230
4481
  -v, --version Show version
4482
+
4483
+ Hub Management:
4484
+ --hub-info Show dashboard URL, QR code, and hub status
4485
+ --hub-stop Stop the hub daemon and all sessions
4486
+ --hub-status List all active sessions
4231
4487
  `);
4232
4488
  }
4233
4489
 
4234
4490
  // src/index.ts
4235
- import { fileURLToPath as fileURLToPath2 } from "url";
4236
- import { join as join4, dirname as dirname2 } from "path";
4237
- import { spawn as spawn2 } from "child_process";
4491
+ import { fileURLToPath as fileURLToPath3 } from "url";
4492
+ import { join as join5, dirname as dirname3 } from "path";
4493
+ import { spawn as spawn3 } from "child_process";
4238
4494
  function preventSleep() {
4239
4495
  try {
4240
4496
  if (process.platform === "darwin") {
4241
- const child = spawn2("caffeinate", ["-i", "-w", String(process.pid)], {
4497
+ const child = spawn3("caffeinate", ["-i", "-w", String(process.pid)], {
4242
4498
  stdio: "ignore",
4243
4499
  detached: true
4244
4500
  });
4245
4501
  child.unref();
4246
4502
  return child;
4247
4503
  } else if (process.platform === "linux") {
4248
- return spawn2("systemd-inhibit", [
4504
+ return spawn3("systemd-inhibit", [
4249
4505
  "--what=idle",
4250
4506
  "--who=itwillsync",
4251
4507
  "--why=Terminal sync session active",
@@ -4257,12 +4513,96 @@ function preventSleep() {
4257
4513
  }
4258
4514
  return null;
4259
4515
  }
4516
+ async function ensureHub() {
4517
+ const hubRunning = await discoverHub();
4518
+ if (hubRunning) {
4519
+ return false;
4520
+ }
4521
+ try {
4522
+ await spawnHub();
4523
+ await new Promise((resolve) => setTimeout(resolve, 500));
4524
+ return true;
4525
+ } catch (err) {
4526
+ console.warn(`
4527
+ Warning: Could not start hub daemon: ${err.message}`);
4528
+ console.warn(" Running in standalone mode (no dashboard).\n");
4529
+ return true;
4530
+ }
4531
+ }
4532
+ async function handleHubCommand(options) {
4533
+ const hubConfig = getHubConfig();
4534
+ const hubRunning = await discoverHub();
4535
+ if (options.hubStop) {
4536
+ if (!hubRunning || !hubConfig) {
4537
+ console.log("\n No hub daemon is running.\n");
4538
+ return;
4539
+ }
4540
+ const stopped = stopHub();
4541
+ if (stopped) {
4542
+ console.log("\n Hub daemon stopped.\n");
4543
+ } else {
4544
+ console.log("\n Failed to stop hub daemon.\n");
4545
+ }
4546
+ return;
4547
+ }
4548
+ if (options.hubStatus) {
4549
+ if (!hubRunning) {
4550
+ console.log("\n No hub daemon is running.\n");
4551
+ return;
4552
+ }
4553
+ const sessions = await listSessions();
4554
+ console.log(`
4555
+ Hub is running. ${sessions.length} active session(s).
4556
+ `);
4557
+ if (sessions.length > 0) {
4558
+ for (const s of sessions) {
4559
+ const uptime = Math.floor((Date.now() - s.connectedAt) / 6e4);
4560
+ console.log(` ${s.name || s.agent} (${s.status}, ${uptime}m, port ${s.port})`);
4561
+ }
4562
+ console.log("");
4563
+ }
4564
+ return;
4565
+ }
4566
+ if (options.hubInfo) {
4567
+ if (!hubRunning || !hubConfig) {
4568
+ console.log("\n No hub daemon is running.");
4569
+ console.log(" Start a session with: itwillsync -- <agent>\n");
4570
+ return;
4571
+ }
4572
+ let networkingMode = "local";
4573
+ if (options.tailscale) {
4574
+ networkingMode = "tailscale";
4575
+ } else if (options.local) {
4576
+ networkingMode = "local";
4577
+ } else if (configExists()) {
4578
+ networkingMode = loadConfig().networkingMode;
4579
+ }
4580
+ const ip = await resolveSessionIP(networkingMode, false);
4581
+ const dashboardUrl = `http://${ip}:${HUB_EXTERNAL_PORT}?token=${hubConfig.masterToken}`;
4582
+ displayQR(dashboardUrl);
4583
+ console.log(` Dashboard: ${dashboardUrl}`);
4584
+ const sessions = await listSessions();
4585
+ console.log(` Sessions: ${sessions.length} active`);
4586
+ if (sessions.length > 0) {
4587
+ for (const s of sessions) {
4588
+ const uptime = Math.floor((Date.now() - s.connectedAt) / 6e4);
4589
+ console.log(` ${s.name || s.agent} (${s.status}, ${uptime}m)`);
4590
+ }
4591
+ }
4592
+ console.log("");
4593
+ return;
4594
+ }
4595
+ }
4260
4596
  async function main() {
4261
4597
  const options = parseArgs(process.argv);
4262
4598
  if (options.subcommand === "setup") {
4263
4599
  await runSetupWizard();
4264
4600
  return;
4265
4601
  }
4602
+ if (options.hubInfo || options.hubStop || options.hubStatus) {
4603
+ await handleHubCommand(options);
4604
+ return;
4605
+ }
4266
4606
  if (options.tailscale && options.local) {
4267
4607
  console.error("Error: Cannot use both --tailscale and --local.\n");
4268
4608
  process.exit(1);
@@ -4285,13 +4625,14 @@ async function main() {
4285
4625
  networkingMode = config.networkingMode;
4286
4626
  }
4287
4627
  const [cmd, ...cmdArgs] = options.command;
4628
+ const isFirstSession = await ensureHub();
4629
+ const hubConfig = getHubConfig();
4288
4630
  const token = generateToken();
4289
4631
  const port = await findAvailablePort(options.port);
4290
4632
  const host = options.localhost ? "127.0.0.1" : "0.0.0.0";
4291
4633
  const ip = await resolveSessionIP(networkingMode, options.localhost);
4292
- const url = `http://${ip}:${port}?token=${token}`;
4293
- const __dirname = dirname2(fileURLToPath2(import.meta.url));
4294
- const webClientPath = join4(__dirname, "web-client");
4634
+ const __dirname = dirname3(fileURLToPath3(import.meta.url));
4635
+ const webClientPath = join5(__dirname, "web-client");
4295
4636
  const ptyManager = new PtyManager(cmd, cmdArgs);
4296
4637
  const server = createSyncServer({
4297
4638
  ptyManager,
@@ -4300,12 +4641,42 @@ async function main() {
4300
4641
  host,
4301
4642
  port
4302
4643
  });
4303
- if (!options.noQr) {
4304
- displayQR(url);
4305
- } else {
4644
+ let registeredSession = null;
4645
+ let heartbeatInterval = null;
4646
+ if (hubConfig) {
4647
+ try {
4648
+ registeredSession = await registerSession({
4649
+ name: cmd,
4650
+ port,
4651
+ token,
4652
+ agent: cmd,
4653
+ cwd: process.cwd(),
4654
+ pid: ptyManager.pid
4655
+ });
4656
+ heartbeatInterval = setInterval(() => {
4657
+ if (registeredSession) {
4658
+ sendHeartbeat(registeredSession.id);
4659
+ }
4660
+ }, 1e4);
4661
+ } catch (err) {
4662
+ console.warn(` Warning: Failed to register with hub: ${err.message}`);
4663
+ }
4664
+ }
4665
+ const dashboardUrl = hubConfig ? `http://${ip}:${HUB_EXTERNAL_PORT}?token=${hubConfig.masterToken}` : null;
4666
+ if (isFirstSession && dashboardUrl && !options.noQr) {
4667
+ displayQR(dashboardUrl);
4668
+ console.log(` Dashboard: ${dashboardUrl}`);
4669
+ } else if (isFirstSession && !options.noQr) {
4670
+ const directUrl = `http://${ip}:${port}?token=${token}`;
4671
+ displayQR(directUrl);
4672
+ } else if (dashboardUrl) {
4306
4673
  console.log(`
4307
- Connect at: ${url}
4308
- `);
4674
+ Session "${cmd}" registered with hub.`);
4675
+ console.log(` Dashboard: ${dashboardUrl}`);
4676
+ console.log("");
4677
+ } else if (!options.noQr) {
4678
+ const directUrl = `http://${ip}:${port}?token=${token}`;
4679
+ displayQR(directUrl);
4309
4680
  }
4310
4681
  const sleepGuard = preventSleep();
4311
4682
  console.log(` Server listening on ${host}:${port}`);
@@ -4331,26 +4702,32 @@ async function main() {
4331
4702
  }
4332
4703
  process.stdout.on("resize", handleResize);
4333
4704
  handleResize();
4334
- function cleanup() {
4705
+ async function cleanup() {
4335
4706
  if (process.stdin.isTTY) {
4336
4707
  process.stdin.setRawMode(false);
4337
4708
  }
4709
+ if (heartbeatInterval) {
4710
+ clearInterval(heartbeatInterval);
4711
+ }
4712
+ if (registeredSession) {
4713
+ await unregisterSession(registeredSession.id);
4714
+ }
4338
4715
  sleepGuard?.kill();
4339
4716
  server.close();
4340
4717
  ptyManager.kill();
4341
4718
  }
4342
- ptyManager.onExit((exitCode) => {
4719
+ ptyManager.onExit(async (exitCode) => {
4343
4720
  console.log(`
4344
4721
  Agent exited with code ${exitCode}`);
4345
- cleanup();
4722
+ await cleanup();
4346
4723
  process.exit(exitCode);
4347
4724
  });
4348
- process.on("SIGINT", () => {
4349
- cleanup();
4725
+ process.on("SIGINT", async () => {
4726
+ await cleanup();
4350
4727
  process.exit(0);
4351
4728
  });
4352
- process.on("SIGTERM", () => {
4353
- cleanup();
4729
+ process.on("SIGTERM", async () => {
4730
+ await cleanup();
4354
4731
  process.exit(0);
4355
4732
  });
4356
4733
  }