@feynmanzhang/open-party 0.1.2 → 0.1.3

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("{");
@@ -152,7 +153,7 @@ function joinTailnet(authKey, timeout = 3e4) {
152
153
  const binary = getTailscaleBinary();
153
154
  try {
154
155
  const output = execWithSudoFallback(
155
- [binary, "up", "--authkey", authKey, "--accept-routes"],
156
+ [binary, "up", "--authkey", authKey],
156
157
  timeout
157
158
  );
158
159
  return { success: true, output: output.trim() };
@@ -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) => {
@@ -3502,6 +3611,9 @@ var serve = (options, listeningListener) => {
3502
3611
  // src/server/index.ts
3503
3612
  init_config();
3504
3613
  init_state();
3614
+ import { mkdirSync, writeFileSync, unlinkSync } from "fs";
3615
+ import { join as join2, dirname } from "path";
3616
+ import { homedir } from "os";
3505
3617
 
3506
3618
  // src/server/models.ts
3507
3619
  function sanitizeAgentInfo(info) {
@@ -3866,7 +3978,21 @@ body::after{
3866
3978
  }
3867
3979
  .btn-join:hover{background:rgba(0,255,240,0.18);box-shadow:0 0 10px rgba(0,255,240,0.2)}
3868
3980
  .btn-join:active{transform:scale(0.97)}
3869
- .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}
3870
3996
 
3871
3997
  /* Modal */
3872
3998
  .modal-overlay{
@@ -4039,16 +4165,47 @@ body::after{
4039
4165
 
4040
4166
  <div class="footer">OPEN PARTY v0.1 // DECENTRALIZED AGENT NETWORK</div>
4041
4167
 
4042
- <!-- Join Network Modal -->
4168
+ <!-- Join Network / Login Modal (two tabs: Interactive + Auth Key) -->
4043
4169
  <div class="modal-overlay" id="joinModal">
4044
4170
  <div class="modal">
4045
- <div class="modal-title">JOIN TAILNET</div>
4046
- <div class="modal-desc">Enter your Tailscale auth key to join the network.<br>You can generate one from the Tailscale admin console.</div>
4047
- <input type="password" class="modal-input" id="authKeyInput" placeholder="tskey-auth-xxxxx..." autocomplete="off" spellcheck="false" />
4048
- <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>
4049
4206
  <div class="modal-actions">
4050
- <button class="modal-btn modal-btn-cancel" id="btnCancelJoin">Cancel</button>
4051
- <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>
4052
4209
  </div>
4053
4210
  </div>
4054
4211
  </div>
@@ -4348,28 +4505,63 @@ body::after{
4348
4505
  }
4349
4506
  }, 1000);
4350
4507
 
4351
- // ---- 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) ----
4352
4526
  const joinModal = $('#joinModal');
4353
4527
  const btnJoin = $('#btnJoinNetwork');
4354
4528
  const btnCancel = $('#btnCancelJoin');
4529
+ const btnCancelAuthkey = $('#btnCancelAuthkey');
4355
4530
  const btnSubmit = $('#btnSubmitJoin');
4356
4531
  const authKeyInput = $('#authKeyInput');
4357
4532
  const joinStatus = $('#joinStatus');
4358
4533
 
4359
4534
  function openJoinModal() {
4535
+ // Reset both tabs
4360
4536
  joinStatus.className = 'modal-status';
4361
4537
  joinStatus.textContent = '';
4362
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');
4363
4546
  joinModal.classList.add('open');
4364
- setTimeout(function() { authKeyInput.focus(); }, 100);
4365
4547
  }
4366
4548
 
4367
4549
  function closeJoinModal() {
4368
4550
  joinModal.classList.remove('open');
4369
4551
  }
4370
4552
 
4371
- 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
+ });
4372
4563
  btnCancel.addEventListener('click', closeJoinModal);
4564
+ btnCancelAuthkey.addEventListener('click', closeJoinModal);
4373
4565
  joinModal.addEventListener('click', function(e) {
4374
4566
  if (e.target === joinModal) closeJoinModal();
4375
4567
  });
@@ -4378,6 +4570,7 @@ body::after{
4378
4570
  if (e.key === 'Escape') closeJoinModal();
4379
4571
  });
4380
4572
 
4573
+ // ---- Auth Key submit ----
4381
4574
  btnSubmit.addEventListener('click', async function() {
4382
4575
  const key = authKeyInput.value.trim();
4383
4576
  if (!key) {
@@ -4399,9 +4592,9 @@ body::after{
4399
4592
  if (data.success) {
4400
4593
  joinStatus.className = 'modal-status success';
4401
4594
  joinStatus.textContent = 'Successfully joined network!';
4402
- btnJoin.textContent = 'Connected';
4403
- btnJoin.classList.add('connected');
4404
- setTimeout(function() { closeJoinModal(); fullRefresh(); }, 1500);
4595
+ btnJoin.textContent = 'Logout';
4596
+ btnJoin.className = 'btn-join btn-logout';
4597
+ setTimeout(function() { closeJoinModal(); checkTailscaleStatus(); fullRefresh(); }, 1500);
4405
4598
  } else {
4406
4599
  joinStatus.className = 'modal-status error';
4407
4600
  joinStatus.textContent = data.output || 'Failed to join network';
@@ -4414,6 +4607,133 @@ body::after{
4414
4607
  btnSubmit.textContent = 'Connect';
4415
4608
  });
4416
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
+
4417
4737
  // Check initial Tailscale status (tri-state)
4418
4738
  let tsState = null;
4419
4739
  let tsInstallInfo = null;
@@ -4432,17 +4752,23 @@ body::after{
4432
4752
  if (tsState.state === 'connected') {
4433
4753
  dot.className = 'status-dot';
4434
4754
  text.textContent = 'ONLINE';
4435
- btnJoin.textContent = 'Connected';
4436
- btnJoin.classList.add('connected');
4755
+ btnJoin.textContent = 'Logout';
4756
+ btnJoin.className = 'btn-join btn-logout';
4757
+ btnJoin.style.display = '';
4437
4758
  panel.style.display = 'none';
4438
4759
  } else if (tsState.state === 'not_installed') {
4439
4760
  dot.className = 'status-dot not-installed';
4440
4761
  text.textContent = 'NOT INSTALLED';
4441
- btnJoin.style.display = 'none';
4762
+ btnJoin.textContent = 'Install Tailscale';
4763
+ btnJoin.className = 'btn-join btn-install';
4764
+ btnJoin.style.display = '';
4442
4765
  await renderNotInstalledPanel();
4443
4766
  } else {
4444
4767
  dot.className = 'status-dot not-connected';
4445
4768
  text.textContent = 'NOT CONNECTED';
4769
+ btnJoin.textContent = 'Join Network';
4770
+ btnJoin.className = 'btn-join';
4771
+ btnJoin.style.display = '';
4446
4772
  await renderNotConnectedPanel();
4447
4773
  }
4448
4774
  }
@@ -4476,7 +4802,7 @@ body::after{
4476
4802
  html += '</div>';
4477
4803
  }
4478
4804
 
4479
- 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>';
4480
4806
  html += '<button class="btn-redetect" onclick="window.__redetectTailscale()">Re-detect</button>';
4481
4807
  panel.innerHTML = html;
4482
4808
  panel.style.display = 'block';
@@ -4484,14 +4810,10 @@ body::after{
4484
4810
 
4485
4811
  async function renderNotConnectedPanel() {
4486
4812
  const panel = $('#tsPanel');
4487
- const btnJoin = $('#btnJoinNetwork');
4488
- btnJoin.style.display = '';
4489
- btnJoin.textContent = 'Join Network';
4490
- btnJoin.classList.remove('connected');
4491
4813
 
4492
4814
  let html = '<div class="ts-panel-title not-connected">Tailscale Not Connected</div>';
4493
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>';
4494
- 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>';
4495
4817
  html += '<button class="btn-redetect" onclick="window.__redetectTailscale()">Re-detect</button>';
4496
4818
  panel.innerHTML = html;
4497
4819
  panel.style.display = 'block';
@@ -4614,6 +4936,37 @@ dashboardRoutes.post("/api/join-network", async (c) => {
4614
4936
  return c.json({ success: false, output: e.message }, 500);
4615
4937
  }
4616
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
+ });
4617
4970
 
4618
4971
  // src/server/index.ts
4619
4972
  async function periodicCleanup() {
@@ -4623,7 +4976,15 @@ app.use("*", cors());
4623
4976
  app.route("/agent", agentRoutes);
4624
4977
  app.route("/proxy", proxyRoutes);
4625
4978
  app.route("/dashboard", dashboardRoutes);
4979
+ function pidFilePath() {
4980
+ const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
4981
+ if (pluginData) return join2(pluginData, "server.pid");
4982
+ return join2(homedir(), ".open-party", "server.pid");
4983
+ }
4626
4984
  async function main() {
4985
+ const pidPath = pidFilePath();
4986
+ mkdirSync(dirname(pidPath), { recursive: true });
4987
+ writeFileSync(pidPath, String(process.pid));
4627
4988
  console.log(`Starting Party Server on port ${PARTY_PORT} (Tailscale IP: ${getSelfIp()})`);
4628
4989
  process.on("SIGHUP", () => {
4629
4990
  });
@@ -4632,6 +4993,10 @@ async function main() {
4632
4993
  const cleanupPromise = periodicCleanup();
4633
4994
  const shutdown = () => {
4634
4995
  console.log("\nShutting down Party Server...");
4996
+ try {
4997
+ unlinkSync(pidPath);
4998
+ } catch {
4999
+ }
4635
5000
  server.close();
4636
5001
  process.exit(0);
4637
5002
  };