@feynmanzhang/open-party 0.1.3-beta.0 → 0.1.4

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.
@@ -28,6 +28,7 @@ var init_config = __esm({
28
28
  import { execFileSync, execSync } from "child_process";
29
29
  import { existsSync } from "fs";
30
30
  import { join } from "path";
31
+ import { spawn as nodeSpawn } from "child_process";
31
32
  function parsePossiblyNoisyJson(raw2) {
32
33
  const trimmed = raw2.trim();
33
34
  const start = trimmed.indexOf("{");
@@ -192,6 +193,62 @@ function getTailscaleInstallationStatus() {
192
193
  function resetTailscaleBinaryCache() {
193
194
  cachedBinary = null;
194
195
  }
196
+ function logoutTailscale(timeout = 15e3) {
197
+ const binary = getTailscaleBinary();
198
+ try {
199
+ const output = runExec([binary, "logout"], timeout);
200
+ resetTailscaleBinaryCache();
201
+ return { success: true, output: output.trim() };
202
+ } catch (e) {
203
+ const err = e;
204
+ return { success: false, output: (err.stderr ?? err.message ?? "").trim() };
205
+ }
206
+ }
207
+ function startInteractiveLogin() {
208
+ const binary = getTailscaleBinary();
209
+ const child = nodeSpawn(binary, ["login"], {
210
+ stdio: ["pipe", "pipe", "pipe"],
211
+ windowsHide: true
212
+ });
213
+ const urlRegex = /https:\/\/login\.tailscale\.com\/a\/[^\s]+/;
214
+ const promise = new Promise((resolve) => {
215
+ let stdout = "";
216
+ let resolved = false;
217
+ const done = (result) => {
218
+ if (resolved) return;
219
+ resolved = true;
220
+ resolve(result);
221
+ };
222
+ child.stdout?.on("data", (data) => {
223
+ stdout += data.toString();
224
+ const match2 = stdout.match(urlRegex);
225
+ if (match2) {
226
+ done({ success: true, url: match2[0], output: stdout.trim() });
227
+ }
228
+ });
229
+ child.stderr?.on("data", (data) => {
230
+ stdout += data.toString();
231
+ });
232
+ child.on("close", (code) => {
233
+ if (code === 0) {
234
+ done({ success: true, output: stdout.trim() });
235
+ } else {
236
+ done({ success: false, output: stdout.trim() || `Exited with code ${code}` });
237
+ }
238
+ });
239
+ child.on("error", (err) => {
240
+ done({ success: false, output: err.message });
241
+ });
242
+ setTimeout(() => {
243
+ done({ success: false, output: "Timeout waiting for login URL" });
244
+ try {
245
+ child.kill();
246
+ } catch {
247
+ }
248
+ }, 3e4);
249
+ });
250
+ return { promise, process: child };
251
+ }
195
252
  function getInstallInstructions(platform) {
196
253
  switch (platform) {
197
254
  case "linux":
@@ -700,6 +757,58 @@ var init_state = __esm({
700
757
  }
701
758
  });
702
759
 
760
+ // src/cli/tailscale-installer.ts
761
+ var tailscale_installer_exports = {};
762
+ __export(tailscale_installer_exports, {
763
+ installTailscale: () => installTailscale
764
+ });
765
+ import { spawn } from "child_process";
766
+ async function installTailscale(platform) {
767
+ const entry = INSTALL_COMMANDS[platform];
768
+ if (!entry) {
769
+ return {
770
+ success: false,
771
+ output: `Unsupported platform: ${platform}. Please install manually from https://tailscale.com/download`
772
+ };
773
+ }
774
+ const cmd = entry.needsSudo ? "sudo" : entry.cmd;
775
+ const args = entry.needsSudo ? [entry.cmd, ...entry.args] : entry.args;
776
+ console.log(`Running: ${cmd} ${args.join(" ")}
777
+ `);
778
+ return new Promise((resolve) => {
779
+ const child = spawn(cmd, args, {
780
+ stdio: "inherit",
781
+ windowsHide: true
782
+ });
783
+ let exited = false;
784
+ child.on("close", (code) => {
785
+ if (exited) return;
786
+ exited = true;
787
+ if (code === 0) {
788
+ resolve({ success: true, output: "Installation completed." });
789
+ } else {
790
+ resolve({ success: false, output: `Installation exited with code ${code}` });
791
+ }
792
+ });
793
+ child.on("error", (err) => {
794
+ if (exited) return;
795
+ exited = true;
796
+ resolve({ success: false, output: err.message });
797
+ });
798
+ });
799
+ }
800
+ var INSTALL_COMMANDS;
801
+ var init_tailscale_installer = __esm({
802
+ "src/cli/tailscale-installer.ts"() {
803
+ "use strict";
804
+ INSTALL_COMMANDS = {
805
+ linux: { cmd: "bash", args: ["-c", "curl -fsSL https://tailscale.com/install.sh | sh"], needsSudo: true },
806
+ darwin: { cmd: "brew", args: ["install", "tailscale"], needsSudo: false },
807
+ win32: { cmd: "winget", args: ["install", "Tailscale.Tailscale", "--accept-source-agreements"], needsSudo: false }
808
+ };
809
+ }
810
+ });
811
+
703
812
  // node_modules/hono/dist/compose.js
704
813
  var compose = (middleware, onError, onNotFound) => {
705
814
  return (context, next) => {
@@ -3869,7 +3978,21 @@ body::after{
3869
3978
  }
3870
3979
  .btn-join:hover{background:rgba(0,255,240,0.18);box-shadow:0 0 10px rgba(0,255,240,0.2)}
3871
3980
  .btn-join:active{transform:scale(0.97)}
3872
- .btn-join.connected{border-color:var(--green);color:var(--green);background:rgba(0,255,136,0.08)}
3981
+ .btn-logout{border-color:var(--red);color:var(--red);background:rgba(255,51,102,0.08)}
3982
+ .btn-logout:hover{background:rgba(255,51,102,0.18);box-shadow:0 0 10px rgba(255,51,102,0.2)}
3983
+ .btn-install{border-color:var(--yellow);color:var(--yellow);background:rgba(255,170,0,0.08)}
3984
+ .btn-install:hover{background:rgba(255,170,0,0.18);box-shadow:0 0 10px rgba(255,170,0,0.2)}
3985
+
3986
+ /* Tab bar inside modal */
3987
+ .tab-bar{display:flex;gap:0;margin-bottom:18px;border-bottom:1px solid var(--border)}
3988
+ .tab-bar .tab{
3989
+ font-family:var(--font-mono);font-size:0.8rem;padding:8px 16px;cursor:pointer;
3990
+ color:var(--muted);border-bottom:2px solid transparent;transition:all 0.2s;
3991
+ }
3992
+ .tab-bar .tab:hover{color:var(--text)}
3993
+ .tab-bar .tab.active{color:var(--cyan);border-bottom-color:var(--cyan)}
3994
+ .tab-content{display:none}
3995
+ .tab-content.active{display:block}
3873
3996
 
3874
3997
  /* Modal */
3875
3998
  .modal-overlay{
@@ -4042,16 +4165,47 @@ body::after{
4042
4165
 
4043
4166
  <div class="footer">OPEN PARTY v0.1 // DECENTRALIZED AGENT NETWORK</div>
4044
4167
 
4045
- <!-- Join Network Modal -->
4168
+ <!-- Join Network / Login Modal (two tabs: Interactive + Auth Key) -->
4046
4169
  <div class="modal-overlay" id="joinModal">
4047
4170
  <div class="modal">
4048
- <div class="modal-title">JOIN TAILNET</div>
4049
- <div class="modal-desc">Enter your Tailscale auth key to join the network.<br>You can generate one from the Tailscale admin console.</div>
4050
- <input type="password" class="modal-input" id="authKeyInput" placeholder="tskey-auth-xxxxx..." autocomplete="off" spellcheck="false" />
4051
- <div class="modal-status" id="joinStatus"></div>
4171
+ <div class="modal-title">CONNECT TO TAILNET</div>
4172
+ <div class="tab-bar" id="joinTabs">
4173
+ <div class="tab active" data-tab="interactive">Interactive</div>
4174
+ <div class="tab" data-tab="authkey">Auth Key</div>
4175
+ </div>
4176
+
4177
+ <!-- Interactive tab -->
4178
+ <div class="tab-content active" id="tabInteractive">
4179
+ <div class="modal-desc">Click the button below to open a browser authentication page.<br>Your Tailscale connection will be established once you authenticate.</div>
4180
+ <div class="modal-status" id="interactiveStatus"></div>
4181
+ <div class="modal-actions">
4182
+ <button class="modal-btn modal-btn-cancel" id="btnCancelJoin">Cancel</button>
4183
+ <button class="modal-btn modal-btn-submit" id="btnInteractiveLogin">Open Browser Login</button>
4184
+ </div>
4185
+ </div>
4186
+
4187
+ <!-- Auth Key tab -->
4188
+ <div class="tab-content" id="tabAuthkey">
4189
+ <div class="modal-desc">Enter your Tailscale auth key to join the network.<br>You can generate one from the Tailscale admin console.</div>
4190
+ <input type="password" class="modal-input" id="authKeyInput" placeholder="tskey-auth-xxxxx..." autocomplete="off" spellcheck="false" />
4191
+ <div class="modal-status" id="joinStatus"></div>
4192
+ <div class="modal-actions">
4193
+ <button class="modal-btn modal-btn-cancel" id="btnCancelAuthkey">Cancel</button>
4194
+ <button class="modal-btn modal-btn-submit" id="btnSubmitJoin">Connect</button>
4195
+ </div>
4196
+ </div>
4197
+ </div>
4198
+ </div>
4199
+
4200
+ <!-- Logout Confirmation Modal -->
4201
+ <div class="modal-overlay" id="logoutModal">
4202
+ <div class="modal">
4203
+ <div class="modal-title" style="color:var(--red)">LOG OUT OF TAILNET</div>
4204
+ <div class="modal-desc">This will disconnect from Tailscale and remove your credentials.<br>You will need to re-authenticate to reconnect.</div>
4205
+ <div class="modal-status" id="logoutStatus"></div>
4052
4206
  <div class="modal-actions">
4053
- <button class="modal-btn modal-btn-cancel" id="btnCancelJoin">Cancel</button>
4054
- <button class="modal-btn modal-btn-submit" id="btnSubmitJoin">Connect</button>
4207
+ <button class="modal-btn modal-btn-cancel" id="btnCancelLogout">Cancel</button>
4208
+ <button class="modal-btn modal-btn-submit" style="border-color:var(--red);color:var(--red);background:rgba(255,51,102,0.12)" id="btnConfirmLogout">Log Out</button>
4055
4209
  </div>
4056
4210
  </div>
4057
4211
  </div>
@@ -4351,28 +4505,63 @@ body::after{
4351
4505
  }
4352
4506
  }, 1000);
4353
4507
 
4354
- // ---- Join Network Modal ----
4508
+ // ---- Join Modal Tabs ----
4509
+ const joinTabs = $$('#joinTabs .tab');
4510
+ joinTabs.forEach(function(tab) {
4511
+ tab.addEventListener('click', function() {
4512
+ joinTabs.forEach(function(t) { t.classList.remove('active'); });
4513
+ tab.classList.add('active');
4514
+ // Toggle tab contents
4515
+ const target = tab.getAttribute('data-tab');
4516
+ $$('.tab-content').forEach(function(tc) { tc.classList.remove('active'); });
4517
+ if (target === 'interactive') {
4518
+ $('#tabInteractive').classList.add('active');
4519
+ } else {
4520
+ $('#tabAuthkey').classList.add('active');
4521
+ }
4522
+ });
4523
+ });
4524
+
4525
+ // ---- Join Network Modal (open/close) ----
4355
4526
  const joinModal = $('#joinModal');
4356
4527
  const btnJoin = $('#btnJoinNetwork');
4357
4528
  const btnCancel = $('#btnCancelJoin');
4529
+ const btnCancelAuthkey = $('#btnCancelAuthkey');
4358
4530
  const btnSubmit = $('#btnSubmitJoin');
4359
4531
  const authKeyInput = $('#authKeyInput');
4360
4532
  const joinStatus = $('#joinStatus');
4361
4533
 
4362
4534
  function openJoinModal() {
4535
+ // Reset both tabs
4363
4536
  joinStatus.className = 'modal-status';
4364
4537
  joinStatus.textContent = '';
4365
4538
  authKeyInput.value = '';
4539
+ $('#interactiveStatus').className = 'modal-status';
4540
+ $('#interactiveStatus').textContent = '';
4541
+ // Default to Interactive tab
4542
+ $$('#joinTabs .tab').forEach(function(t) { t.classList.remove('active'); });
4543
+ $$('#joinTabs .tab')[0].classList.add('active');
4544
+ $$('.tab-content').forEach(function(tc) { tc.classList.remove('active'); });
4545
+ $('#tabInteractive').classList.add('active');
4366
4546
  joinModal.classList.add('open');
4367
- setTimeout(function() { authKeyInput.focus(); }, 100);
4368
4547
  }
4369
4548
 
4370
4549
  function closeJoinModal() {
4371
4550
  joinModal.classList.remove('open');
4372
4551
  }
4373
4552
 
4374
- btnJoin.addEventListener('click', openJoinModal);
4553
+ btnJoin.addEventListener('click', function() {
4554
+ // Decide action based on Tailscale state
4555
+ if (tsState && tsState.state === 'connected') {
4556
+ openLogoutModal();
4557
+ } else if (tsState && tsState.state === 'not_installed') {
4558
+ doInstallTailscale();
4559
+ } else {
4560
+ openJoinModal();
4561
+ }
4562
+ });
4375
4563
  btnCancel.addEventListener('click', closeJoinModal);
4564
+ btnCancelAuthkey.addEventListener('click', closeJoinModal);
4376
4565
  joinModal.addEventListener('click', function(e) {
4377
4566
  if (e.target === joinModal) closeJoinModal();
4378
4567
  });
@@ -4381,6 +4570,7 @@ body::after{
4381
4570
  if (e.key === 'Escape') closeJoinModal();
4382
4571
  });
4383
4572
 
4573
+ // ---- Auth Key submit ----
4384
4574
  btnSubmit.addEventListener('click', async function() {
4385
4575
  const key = authKeyInput.value.trim();
4386
4576
  if (!key) {
@@ -4402,9 +4592,9 @@ body::after{
4402
4592
  if (data.success) {
4403
4593
  joinStatus.className = 'modal-status success';
4404
4594
  joinStatus.textContent = 'Successfully joined network!';
4405
- btnJoin.textContent = 'Connected';
4406
- btnJoin.classList.add('connected');
4407
- setTimeout(function() { closeJoinModal(); fullRefresh(); }, 1500);
4595
+ btnJoin.textContent = 'Logout';
4596
+ btnJoin.className = 'btn-join btn-logout';
4597
+ setTimeout(function() { closeJoinModal(); checkTailscaleStatus(); fullRefresh(); }, 1500);
4408
4598
  } else {
4409
4599
  joinStatus.className = 'modal-status error';
4410
4600
  joinStatus.textContent = data.output || 'Failed to join network';
@@ -4417,6 +4607,133 @@ body::after{
4417
4607
  btnSubmit.textContent = 'Connect';
4418
4608
  });
4419
4609
 
4610
+ // ---- Interactive Login ----
4611
+ const btnInteractiveLogin = $('#btnInteractiveLogin');
4612
+ btnInteractiveLogin.addEventListener('click', async function() {
4613
+ const statusEl = $('#interactiveStatus');
4614
+ statusEl.className = 'modal-status';
4615
+ statusEl.textContent = '';
4616
+ btnInteractiveLogin.disabled = true;
4617
+ btnInteractiveLogin.innerHTML = '<span class="spinner"></span>Opening browser...';
4618
+
4619
+ try {
4620
+ const r = await fetch('/dashboard/api/tailscale-login', { method: 'POST' });
4621
+ const data = await r.json();
4622
+
4623
+ if (data.success && data.url) {
4624
+ // Open the auth URL in a new tab
4625
+ window.open(data.url, '_blank');
4626
+ statusEl.className = 'modal-status success';
4627
+ statusEl.textContent = 'Authentication page opened in your browser. Waiting for connection...';
4628
+
4629
+ // Poll for connection
4630
+ var pollCount = 0;
4631
+ var pollInterval = setInterval(async function() {
4632
+ pollCount++;
4633
+ if (pollCount > 40) { // 2 minutes timeout
4634
+ clearInterval(pollInterval);
4635
+ statusEl.className = 'modal-status error';
4636
+ statusEl.textContent = 'Timed out waiting for authentication. Please try again.';
4637
+ btnInteractiveLogin.disabled = false;
4638
+ btnInteractiveLogin.textContent = 'Open Browser Login';
4639
+ return;
4640
+ }
4641
+ try {
4642
+ var sr = await fetch('/dashboard/api/tailscale-status');
4643
+ var sd = await sr.json();
4644
+ if (sd.state === 'connected') {
4645
+ clearInterval(pollInterval);
4646
+ btnJoin.textContent = 'Logout';
4647
+ btnJoin.className = 'btn-join btn-logout';
4648
+ closeJoinModal();
4649
+ checkTailscaleStatus();
4650
+ fullRefresh();
4651
+ return;
4652
+ }
4653
+ } catch { /* poll error, continue */ }
4654
+ }, 3000);
4655
+ } else {
4656
+ statusEl.className = 'modal-status error';
4657
+ statusEl.textContent = data.output || 'Failed to start interactive login';
4658
+ btnInteractiveLogin.disabled = false;
4659
+ btnInteractiveLogin.textContent = 'Open Browser Login';
4660
+ }
4661
+ } catch (e) {
4662
+ statusEl.className = 'modal-status error';
4663
+ statusEl.textContent = 'Network error: ' + (e.message || 'unknown');
4664
+ btnInteractiveLogin.disabled = false;
4665
+ btnInteractiveLogin.textContent = 'Open Browser Login';
4666
+ }
4667
+ });
4668
+
4669
+ // ---- Logout Modal ----
4670
+ const logoutModal = $('#logoutModal');
4671
+ const btnConfirmLogout = $('#btnConfirmLogout');
4672
+ const btnCancelLogout = $('#btnCancelLogout');
4673
+ const logoutStatus = $('#logoutStatus');
4674
+
4675
+ function openLogoutModal() {
4676
+ logoutStatus.className = 'modal-status';
4677
+ logoutStatus.textContent = '';
4678
+ logoutModal.classList.add('open');
4679
+ }
4680
+
4681
+ btnCancelLogout.addEventListener('click', function() { logoutModal.classList.remove('open'); });
4682
+ logoutModal.addEventListener('click', function(e) { if (e.target === logoutModal) logoutModal.classList.remove('open'); });
4683
+
4684
+ btnConfirmLogout.addEventListener('click', async function() {
4685
+ btnConfirmLogout.disabled = true;
4686
+ btnConfirmLogout.innerHTML = '<span class="spinner"></span>Logging out...';
4687
+ logoutStatus.className = 'modal-status';
4688
+ logoutStatus.textContent = '';
4689
+
4690
+ try {
4691
+ const r = await fetch('/dashboard/api/logout', { method: 'POST' });
4692
+ const data = await r.json();
4693
+ logoutModal.classList.remove('open');
4694
+ if (data.success) {
4695
+ checkTailscaleStatus();
4696
+ fullRefresh();
4697
+ } else {
4698
+ alert('Logout failed: ' + (data.output || 'unknown error'));
4699
+ }
4700
+ } catch (e) {
4701
+ logoutModal.classList.remove('open');
4702
+ alert('Network error: ' + (e.message || 'unknown'));
4703
+ }
4704
+ btnConfirmLogout.disabled = false;
4705
+ btnConfirmLogout.textContent = 'Log Out';
4706
+ });
4707
+
4708
+ // ---- Install Tailscale ----
4709
+ async function doInstallTailscale() {
4710
+ if (!confirm('Install Tailscale on this machine?')) return;
4711
+
4712
+ btnJoin.disabled = true;
4713
+ btnJoin.innerHTML = '<span class="spinner"></span>Installing...';
4714
+
4715
+ try {
4716
+ const r = await fetch('/dashboard/api/install-tailscale', { method: 'POST' });
4717
+ const data = await r.json();
4718
+ if (data.success) {
4719
+ btnJoin.textContent = 'Installed';
4720
+ btnJoin.disabled = false;
4721
+ checkTailscaleStatus();
4722
+ fullRefresh();
4723
+ } else {
4724
+ alert('Installation failed: ' + (data.output || 'unknown error'));
4725
+ btnJoin.textContent = 'Install Tailscale';
4726
+ btnJoin.className = 'btn-join btn-install';
4727
+ btnJoin.disabled = false;
4728
+ }
4729
+ } catch (e) {
4730
+ alert('Network error: ' + (e.message || 'unknown'));
4731
+ btnJoin.textContent = 'Install Tailscale';
4732
+ btnJoin.className = 'btn-join btn-install';
4733
+ btnJoin.disabled = false;
4734
+ }
4735
+ }
4736
+
4420
4737
  // Check initial Tailscale status (tri-state)
4421
4738
  let tsState = null;
4422
4739
  let tsInstallInfo = null;
@@ -4435,17 +4752,23 @@ body::after{
4435
4752
  if (tsState.state === 'connected') {
4436
4753
  dot.className = 'status-dot';
4437
4754
  text.textContent = 'ONLINE';
4438
- btnJoin.textContent = 'Connected';
4439
- btnJoin.classList.add('connected');
4755
+ btnJoin.textContent = 'Logout';
4756
+ btnJoin.className = 'btn-join btn-logout';
4757
+ btnJoin.style.display = '';
4440
4758
  panel.style.display = 'none';
4441
4759
  } else if (tsState.state === 'not_installed') {
4442
4760
  dot.className = 'status-dot not-installed';
4443
4761
  text.textContent = 'NOT INSTALLED';
4444
- btnJoin.style.display = 'none';
4762
+ btnJoin.textContent = 'Install Tailscale';
4763
+ btnJoin.className = 'btn-join btn-install';
4764
+ btnJoin.style.display = '';
4445
4765
  await renderNotInstalledPanel();
4446
4766
  } else {
4447
4767
  dot.className = 'status-dot not-connected';
4448
4768
  text.textContent = 'NOT CONNECTED';
4769
+ btnJoin.textContent = 'Join Network';
4770
+ btnJoin.className = 'btn-join';
4771
+ btnJoin.style.display = '';
4449
4772
  await renderNotConnectedPanel();
4450
4773
  }
4451
4774
  }
@@ -4479,7 +4802,7 @@ body::after{
4479
4802
  html += '</div>';
4480
4803
  }
4481
4804
 
4482
- html += '<div class="ts-setup-hint">Or run <code style="color:var(--cyan)">npx open-party setup</code> for guided installation</div>';
4805
+ html += '<div class="ts-setup-hint">Or click the <strong>Install Tailscale</strong> button above, or run <code style="color:var(--cyan)">npx open-party setup</code></div>';
4483
4806
  html += '<button class="btn-redetect" onclick="window.__redetectTailscale()">Re-detect</button>';
4484
4807
  panel.innerHTML = html;
4485
4808
  panel.style.display = 'block';
@@ -4487,14 +4810,10 @@ body::after{
4487
4810
 
4488
4811
  async function renderNotConnectedPanel() {
4489
4812
  const panel = $('#tsPanel');
4490
- const btnJoin = $('#btnJoinNetwork');
4491
- btnJoin.style.display = '';
4492
- btnJoin.textContent = 'Join Network';
4493
- btnJoin.classList.remove('connected');
4494
4813
 
4495
4814
  let html = '<div class="ts-panel-title not-connected">Tailscale Not Connected</div>';
4496
4815
  html += '<div class="ts-info-row"><span class="label">Status:</span><span class="value" style="color:var(--yellow)">Installed but not authenticated</span></div>';
4497
- html += '<div class="ts-setup-hint">Run <code style="color:var(--cyan)">npx open-party setup</code> to log in, or use the Join Network button to enter an Auth Key</div>';
4816
+ html += '<div class="ts-setup-hint">Use the <strong>Join Network</strong> button above to log in</div>';
4498
4817
  html += '<button class="btn-redetect" onclick="window.__redetectTailscale()">Re-detect</button>';
4499
4818
  panel.innerHTML = html;
4500
4819
  panel.style.display = 'block';
@@ -4617,6 +4936,37 @@ dashboardRoutes.post("/api/join-network", async (c) => {
4617
4936
  return c.json({ success: false, output: e.message }, 500);
4618
4937
  }
4619
4938
  });
4939
+ var activeLogin = null;
4940
+ dashboardRoutes.post("/api/logout", async (c) => {
4941
+ const result = logoutTailscale();
4942
+ if (result.success) {
4943
+ resetTailscaleBinaryCache();
4944
+ refreshSelfIp();
4945
+ }
4946
+ return c.json(result, result.success ? 200 : 500);
4947
+ });
4948
+ dashboardRoutes.post("/api/tailscale-login", async (c) => {
4949
+ if (activeLogin?.url) {
4950
+ return c.json({ success: true, url: activeLogin.url });
4951
+ }
4952
+ const { promise, process: process2 } = startInteractiveLogin();
4953
+ activeLogin = { process: process2 };
4954
+ const result = await promise;
4955
+ if (result.success && result.url) {
4956
+ activeLogin.url = result.url;
4957
+ return c.json({ success: true, url: result.url });
4958
+ }
4959
+ activeLogin = null;
4960
+ return c.json({ success: false, output: result.output }, 500);
4961
+ });
4962
+ dashboardRoutes.post("/api/install-tailscale", async (c) => {
4963
+ const { installTailscale: installTailscale2 } = await Promise.resolve().then(() => (init_tailscale_installer(), tailscale_installer_exports));
4964
+ const result = await installTailscale2(process.platform);
4965
+ if (result.success) {
4966
+ resetTailscaleBinaryCache();
4967
+ }
4968
+ return c.json(result, result.success ? 200 : 500);
4969
+ });
4620
4970
 
4621
4971
  // src/server/index.ts
4622
4972
  async function periodicCleanup() {