@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.
package/dist/cli/index.js CHANGED
@@ -11,27 +11,10 @@ var __export = (target, all) => {
11
11
  };
12
12
 
13
13
  // src/infra/tailscale.ts
14
- var tailscale_exports = {};
15
- __export(tailscale_exports, {
16
- WhoisIdentity: () => WhoisIdentity,
17
- disableFunnel: () => disableFunnel,
18
- disableServe: () => disableServe,
19
- enableFunnel: () => enableFunnel,
20
- enableServe: () => enableServe,
21
- getInstallInstructions: () => getInstallInstructions,
22
- getTailnetHostname: () => getTailnetHostname,
23
- getTailscaleBinary: () => getTailscaleBinary,
24
- getTailscaleConnectionStatus: () => getTailscaleConnectionStatus,
25
- getTailscaleInstallationStatus: () => getTailscaleInstallationStatus,
26
- getTailscaleIps: () => getTailscaleIps,
27
- joinTailnet: () => joinTailnet,
28
- readTailscaleStatus: () => readTailscaleStatus,
29
- readWhoisIdentity: () => readWhoisIdentity,
30
- resetTailscaleBinaryCache: () => resetTailscaleBinaryCache
31
- });
32
14
  import { execFileSync, execSync } from "child_process";
33
15
  import { existsSync } from "fs";
34
16
  import { join } from "path";
17
+ import { spawn as nodeSpawn } from "child_process";
35
18
  function parsePossiblyNoisyJson(raw2) {
36
19
  const trimmed = raw2.trim();
37
20
  const start = trimmed.indexOf("{");
@@ -137,35 +120,6 @@ function getTailscaleIps() {
137
120
  }
138
121
  return [];
139
122
  }
140
- function parseWhoisIdentity(payload) {
141
- const userProfile = payload.UserProfile ?? payload.userProfile ?? payload.User ?? {};
142
- const login = userProfile.LoginName || userProfile.Login || userProfile.login || payload.LoginName || payload.login;
143
- if (typeof login !== "string" || !login.trim()) return null;
144
- const rawName = userProfile.DisplayName || userProfile.Name || userProfile.displayName || payload.DisplayName || payload.name;
145
- const name = typeof rawName === "string" ? rawName.trim() : void 0;
146
- return new WhoisIdentity(login.trim(), name);
147
- }
148
- function readWhoisIdentity(ip, timeout = 5e3, cacheTtl = 60, errorTtl = 5) {
149
- const normalized = ip.trim();
150
- if (!normalized) return null;
151
- const now = performance.now() / 1e3;
152
- const cached = whoisCache.get(normalized);
153
- if (cached) {
154
- if (cached.expiresAt > now) return cached.value;
155
- whoisCache.delete(normalized);
156
- }
157
- const binary = getTailscaleBinary();
158
- let identity = null;
159
- try {
160
- const stdout = runExec([binary, "whois", "--json", normalized], timeout);
161
- const parsed = stdout.trim() ? parsePossiblyNoisyJson(stdout) : {};
162
- identity = parseWhoisIdentity(parsed);
163
- } catch {
164
- }
165
- const ttl = identity ? cacheTtl : errorTtl;
166
- whoisCache.set(normalized, { value: identity, expiresAt: now + ttl });
167
- return identity;
168
- }
169
123
  function execWithSudoFallback(cmd, timeout = 15e3) {
170
124
  try {
171
125
  return runExec(cmd, timeout);
@@ -181,22 +135,6 @@ function execWithSudoFallback(cmd, timeout = 15e3) {
181
135
  throw exc;
182
136
  }
183
137
  }
184
- function enableServe(port, timeout = 15e3) {
185
- const binary = getTailscaleBinary();
186
- execWithSudoFallback([binary, "serve", "--bg", "--yes", String(port)], timeout);
187
- }
188
- function disableServe(timeout = 15e3) {
189
- const binary = getTailscaleBinary();
190
- execWithSudoFallback([binary, "serve", "reset"], timeout);
191
- }
192
- function enableFunnel(port, timeout = 15e3) {
193
- const binary = getTailscaleBinary();
194
- execWithSudoFallback([binary, "funnel", "--bg", "--yes", String(port)], timeout);
195
- }
196
- function disableFunnel(timeout = 15e3) {
197
- const binary = getTailscaleBinary();
198
- execWithSudoFallback([binary, "funnel", "reset"], timeout);
199
- }
200
138
  function joinTailnet(authKey, timeout = 3e4) {
201
139
  const binary = getTailscaleBinary();
202
140
  try {
@@ -210,27 +148,6 @@ function joinTailnet(authKey, timeout = 3e4) {
210
148
  return { success: false, output: (err.stderr ?? err.message ?? "").trim() };
211
149
  }
212
150
  }
213
- function getTailscaleConnectionStatus() {
214
- try {
215
- const status = readTailscaleStatus();
216
- const self = status.Self ?? {};
217
- const online = self.Online === true;
218
- const ips = self.TailscaleIPs;
219
- const dns = self.DNSName?.replace(/\.$/, "");
220
- return {
221
- connected: online,
222
- tailscale_ip: ips?.[0] ?? null,
223
- hostname: dns ?? null
224
- };
225
- } catch (e) {
226
- return {
227
- connected: false,
228
- tailscale_ip: null,
229
- hostname: null,
230
- error: e.message
231
- };
232
- }
233
- }
234
151
  function getTailscaleInstallationStatus() {
235
152
  const binary = findTailscaleBinary();
236
153
  if (!binary) {
@@ -262,6 +179,62 @@ function getTailscaleInstallationStatus() {
262
179
  function resetTailscaleBinaryCache() {
263
180
  cachedBinary = null;
264
181
  }
182
+ function logoutTailscale(timeout = 15e3) {
183
+ const binary = getTailscaleBinary();
184
+ try {
185
+ const output = runExec([binary, "logout"], timeout);
186
+ resetTailscaleBinaryCache();
187
+ return { success: true, output: output.trim() };
188
+ } catch (e) {
189
+ const err = e;
190
+ return { success: false, output: (err.stderr ?? err.message ?? "").trim() };
191
+ }
192
+ }
193
+ function startInteractiveLogin() {
194
+ const binary = getTailscaleBinary();
195
+ const child = nodeSpawn(binary, ["login"], {
196
+ stdio: ["pipe", "pipe", "pipe"],
197
+ windowsHide: true
198
+ });
199
+ const urlRegex = /https:\/\/login\.tailscale\.com\/a\/[^\s]+/;
200
+ const promise = new Promise((resolve4) => {
201
+ let stdout = "";
202
+ let resolved = false;
203
+ const done = (result) => {
204
+ if (resolved) return;
205
+ resolved = true;
206
+ resolve4(result);
207
+ };
208
+ child.stdout?.on("data", (data) => {
209
+ stdout += data.toString();
210
+ const match2 = stdout.match(urlRegex);
211
+ if (match2) {
212
+ done({ success: true, url: match2[0], output: stdout.trim() });
213
+ }
214
+ });
215
+ child.stderr?.on("data", (data) => {
216
+ stdout += data.toString();
217
+ });
218
+ child.on("close", (code) => {
219
+ if (code === 0) {
220
+ done({ success: true, output: stdout.trim() });
221
+ } else {
222
+ done({ success: false, output: stdout.trim() || `Exited with code ${code}` });
223
+ }
224
+ });
225
+ child.on("error", (err) => {
226
+ done({ success: false, output: err.message });
227
+ });
228
+ setTimeout(() => {
229
+ done({ success: false, output: "Timeout waiting for login URL" });
230
+ try {
231
+ child.kill();
232
+ } catch {
233
+ }
234
+ }, 3e4);
235
+ });
236
+ return { promise, process: child };
237
+ }
265
238
  function getInstallInstructions(platform) {
266
239
  switch (platform) {
267
240
  case "linux":
@@ -294,20 +267,11 @@ function getInstallInstructions(platform) {
294
267
  };
295
268
  }
296
269
  }
297
- var cachedBinary, WhoisIdentity, whoisCache, PERMISSION_KEYWORDS;
270
+ var cachedBinary, PERMISSION_KEYWORDS;
298
271
  var init_tailscale = __esm({
299
272
  "src/infra/tailscale.ts"() {
300
273
  "use strict";
301
274
  cachedBinary = null;
302
- WhoisIdentity = class {
303
- constructor(login, name) {
304
- this.login = login;
305
- this.name = name;
306
- }
307
- login;
308
- name;
309
- };
310
- whoisCache = /* @__PURE__ */ new Map();
311
275
  PERMISSION_KEYWORDS = [
312
276
  "permission denied",
313
277
  "access denied",
@@ -322,6 +286,58 @@ var init_tailscale = __esm({
322
286
  }
323
287
  });
324
288
 
289
+ // src/cli/tailscale-installer.ts
290
+ var tailscale_installer_exports = {};
291
+ __export(tailscale_installer_exports, {
292
+ installTailscale: () => installTailscale
293
+ });
294
+ import { spawn } from "child_process";
295
+ async function installTailscale(platform) {
296
+ const entry = INSTALL_COMMANDS[platform];
297
+ if (!entry) {
298
+ return {
299
+ success: false,
300
+ output: `Unsupported platform: ${platform}. Please install manually from https://tailscale.com/download`
301
+ };
302
+ }
303
+ const cmd = entry.needsSudo ? "sudo" : entry.cmd;
304
+ const args2 = entry.needsSudo ? [entry.cmd, ...entry.args] : entry.args;
305
+ console.log(`Running: ${cmd} ${args2.join(" ")}
306
+ `);
307
+ return new Promise((resolve4) => {
308
+ const child = spawn(cmd, args2, {
309
+ stdio: "inherit",
310
+ windowsHide: true
311
+ });
312
+ let exited = false;
313
+ child.on("close", (code) => {
314
+ if (exited) return;
315
+ exited = true;
316
+ if (code === 0) {
317
+ resolve4({ success: true, output: "Installation completed." });
318
+ } else {
319
+ resolve4({ success: false, output: `Installation exited with code ${code}` });
320
+ }
321
+ });
322
+ child.on("error", (err) => {
323
+ if (exited) return;
324
+ exited = true;
325
+ resolve4({ success: false, output: err.message });
326
+ });
327
+ });
328
+ }
329
+ var INSTALL_COMMANDS;
330
+ var init_tailscale_installer = __esm({
331
+ "src/cli/tailscale-installer.ts"() {
332
+ "use strict";
333
+ INSTALL_COMMANDS = {
334
+ linux: { cmd: "bash", args: ["-c", "curl -fsSL https://tailscale.com/install.sh | sh"], needsSudo: true },
335
+ darwin: { cmd: "brew", args: ["install", "tailscale"], needsSudo: false },
336
+ win32: { cmd: "winget", args: ["install", "Tailscale.Tailscale", "--accept-source-agreements"], needsSudo: false }
337
+ };
338
+ }
339
+ });
340
+
325
341
  // node_modules/hono/dist/compose.js
326
342
  var compose;
327
343
  var init_compose = __esm({
@@ -3171,7 +3187,7 @@ var init_dist2 = __esm({
3171
3187
  });
3172
3188
  if (!chunk) {
3173
3189
  if (i === 1) {
3174
- await new Promise((resolve3) => setTimeout(resolve3));
3190
+ await new Promise((resolve4) => setTimeout(resolve4));
3175
3191
  maxReadCount = 3;
3176
3192
  continue;
3177
3193
  }
@@ -3411,7 +3427,7 @@ function classifyFetchError(error) {
3411
3427
  return null;
3412
3428
  }
3413
3429
  function sleep2(ms) {
3414
- return new Promise((resolve3) => setTimeout(resolve3, ms));
3430
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
3415
3431
  }
3416
3432
  var UNKNOWN, PARTY_SERVER, DEGRADED, SUSPECT, DOWN, NOT_SERVER, MAYBE, MAYBE_MAX_RETRIES, BACKOFF_BASE, BACKOFF_CAP, FAILURE_SUSPECT, FAILURE_DOWN, PeerDiscovery;
3417
3433
  var init_peer_discovery = __esm({
@@ -4178,7 +4194,21 @@ body::after{
4178
4194
  }
4179
4195
  .btn-join:hover{background:rgba(0,255,240,0.18);box-shadow:0 0 10px rgba(0,255,240,0.2)}
4180
4196
  .btn-join:active{transform:scale(0.97)}
4181
- .btn-join.connected{border-color:var(--green);color:var(--green);background:rgba(0,255,136,0.08)}
4197
+ .btn-logout{border-color:var(--red);color:var(--red);background:rgba(255,51,102,0.08)}
4198
+ .btn-logout:hover{background:rgba(255,51,102,0.18);box-shadow:0 0 10px rgba(255,51,102,0.2)}
4199
+ .btn-install{border-color:var(--yellow);color:var(--yellow);background:rgba(255,170,0,0.08)}
4200
+ .btn-install:hover{background:rgba(255,170,0,0.18);box-shadow:0 0 10px rgba(255,170,0,0.2)}
4201
+
4202
+ /* Tab bar inside modal */
4203
+ .tab-bar{display:flex;gap:0;margin-bottom:18px;border-bottom:1px solid var(--border)}
4204
+ .tab-bar .tab{
4205
+ font-family:var(--font-mono);font-size:0.8rem;padding:8px 16px;cursor:pointer;
4206
+ color:var(--muted);border-bottom:2px solid transparent;transition:all 0.2s;
4207
+ }
4208
+ .tab-bar .tab:hover{color:var(--text)}
4209
+ .tab-bar .tab.active{color:var(--cyan);border-bottom-color:var(--cyan)}
4210
+ .tab-content{display:none}
4211
+ .tab-content.active{display:block}
4182
4212
 
4183
4213
  /* Modal */
4184
4214
  .modal-overlay{
@@ -4351,16 +4381,47 @@ body::after{
4351
4381
 
4352
4382
  <div class="footer">OPEN PARTY v0.1 // DECENTRALIZED AGENT NETWORK</div>
4353
4383
 
4354
- <!-- Join Network Modal -->
4384
+ <!-- Join Network / Login Modal (two tabs: Interactive + Auth Key) -->
4355
4385
  <div class="modal-overlay" id="joinModal">
4356
4386
  <div class="modal">
4357
- <div class="modal-title">JOIN TAILNET</div>
4358
- <div class="modal-desc">Enter your Tailscale auth key to join the network.<br>You can generate one from the Tailscale admin console.</div>
4359
- <input type="password" class="modal-input" id="authKeyInput" placeholder="tskey-auth-xxxxx..." autocomplete="off" spellcheck="false" />
4360
- <div class="modal-status" id="joinStatus"></div>
4387
+ <div class="modal-title">CONNECT TO TAILNET</div>
4388
+ <div class="tab-bar" id="joinTabs">
4389
+ <div class="tab active" data-tab="interactive">Interactive</div>
4390
+ <div class="tab" data-tab="authkey">Auth Key</div>
4391
+ </div>
4392
+
4393
+ <!-- Interactive tab -->
4394
+ <div class="tab-content active" id="tabInteractive">
4395
+ <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>
4396
+ <div class="modal-status" id="interactiveStatus"></div>
4397
+ <div class="modal-actions">
4398
+ <button class="modal-btn modal-btn-cancel" id="btnCancelJoin">Cancel</button>
4399
+ <button class="modal-btn modal-btn-submit" id="btnInteractiveLogin">Open Browser Login</button>
4400
+ </div>
4401
+ </div>
4402
+
4403
+ <!-- Auth Key tab -->
4404
+ <div class="tab-content" id="tabAuthkey">
4405
+ <div class="modal-desc">Enter your Tailscale auth key to join the network.<br>You can generate one from the Tailscale admin console.</div>
4406
+ <input type="password" class="modal-input" id="authKeyInput" placeholder="tskey-auth-xxxxx..." autocomplete="off" spellcheck="false" />
4407
+ <div class="modal-status" id="joinStatus"></div>
4408
+ <div class="modal-actions">
4409
+ <button class="modal-btn modal-btn-cancel" id="btnCancelAuthkey">Cancel</button>
4410
+ <button class="modal-btn modal-btn-submit" id="btnSubmitJoin">Connect</button>
4411
+ </div>
4412
+ </div>
4413
+ </div>
4414
+ </div>
4415
+
4416
+ <!-- Logout Confirmation Modal -->
4417
+ <div class="modal-overlay" id="logoutModal">
4418
+ <div class="modal">
4419
+ <div class="modal-title" style="color:var(--red)">LOG OUT OF TAILNET</div>
4420
+ <div class="modal-desc">This will disconnect from Tailscale and remove your credentials.<br>You will need to re-authenticate to reconnect.</div>
4421
+ <div class="modal-status" id="logoutStatus"></div>
4361
4422
  <div class="modal-actions">
4362
- <button class="modal-btn modal-btn-cancel" id="btnCancelJoin">Cancel</button>
4363
- <button class="modal-btn modal-btn-submit" id="btnSubmitJoin">Connect</button>
4423
+ <button class="modal-btn modal-btn-cancel" id="btnCancelLogout">Cancel</button>
4424
+ <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>
4364
4425
  </div>
4365
4426
  </div>
4366
4427
  </div>
@@ -4660,28 +4721,63 @@ body::after{
4660
4721
  }
4661
4722
  }, 1000);
4662
4723
 
4663
- // ---- Join Network Modal ----
4724
+ // ---- Join Modal Tabs ----
4725
+ const joinTabs = $$('#joinTabs .tab');
4726
+ joinTabs.forEach(function(tab) {
4727
+ tab.addEventListener('click', function() {
4728
+ joinTabs.forEach(function(t) { t.classList.remove('active'); });
4729
+ tab.classList.add('active');
4730
+ // Toggle tab contents
4731
+ const target = tab.getAttribute('data-tab');
4732
+ $$('.tab-content').forEach(function(tc) { tc.classList.remove('active'); });
4733
+ if (target === 'interactive') {
4734
+ $('#tabInteractive').classList.add('active');
4735
+ } else {
4736
+ $('#tabAuthkey').classList.add('active');
4737
+ }
4738
+ });
4739
+ });
4740
+
4741
+ // ---- Join Network Modal (open/close) ----
4664
4742
  const joinModal = $('#joinModal');
4665
4743
  const btnJoin = $('#btnJoinNetwork');
4666
4744
  const btnCancel = $('#btnCancelJoin');
4745
+ const btnCancelAuthkey = $('#btnCancelAuthkey');
4667
4746
  const btnSubmit = $('#btnSubmitJoin');
4668
4747
  const authKeyInput = $('#authKeyInput');
4669
4748
  const joinStatus = $('#joinStatus');
4670
4749
 
4671
4750
  function openJoinModal() {
4751
+ // Reset both tabs
4672
4752
  joinStatus.className = 'modal-status';
4673
4753
  joinStatus.textContent = '';
4674
4754
  authKeyInput.value = '';
4755
+ $('#interactiveStatus').className = 'modal-status';
4756
+ $('#interactiveStatus').textContent = '';
4757
+ // Default to Interactive tab
4758
+ $$('#joinTabs .tab').forEach(function(t) { t.classList.remove('active'); });
4759
+ $$('#joinTabs .tab')[0].classList.add('active');
4760
+ $$('.tab-content').forEach(function(tc) { tc.classList.remove('active'); });
4761
+ $('#tabInteractive').classList.add('active');
4675
4762
  joinModal.classList.add('open');
4676
- setTimeout(function() { authKeyInput.focus(); }, 100);
4677
4763
  }
4678
4764
 
4679
4765
  function closeJoinModal() {
4680
4766
  joinModal.classList.remove('open');
4681
4767
  }
4682
4768
 
4683
- btnJoin.addEventListener('click', openJoinModal);
4769
+ btnJoin.addEventListener('click', function() {
4770
+ // Decide action based on Tailscale state
4771
+ if (tsState && tsState.state === 'connected') {
4772
+ openLogoutModal();
4773
+ } else if (tsState && tsState.state === 'not_installed') {
4774
+ doInstallTailscale();
4775
+ } else {
4776
+ openJoinModal();
4777
+ }
4778
+ });
4684
4779
  btnCancel.addEventListener('click', closeJoinModal);
4780
+ btnCancelAuthkey.addEventListener('click', closeJoinModal);
4685
4781
  joinModal.addEventListener('click', function(e) {
4686
4782
  if (e.target === joinModal) closeJoinModal();
4687
4783
  });
@@ -4690,6 +4786,7 @@ body::after{
4690
4786
  if (e.key === 'Escape') closeJoinModal();
4691
4787
  });
4692
4788
 
4789
+ // ---- Auth Key submit ----
4693
4790
  btnSubmit.addEventListener('click', async function() {
4694
4791
  const key = authKeyInput.value.trim();
4695
4792
  if (!key) {
@@ -4711,9 +4808,9 @@ body::after{
4711
4808
  if (data.success) {
4712
4809
  joinStatus.className = 'modal-status success';
4713
4810
  joinStatus.textContent = 'Successfully joined network!';
4714
- btnJoin.textContent = 'Connected';
4715
- btnJoin.classList.add('connected');
4716
- setTimeout(function() { closeJoinModal(); fullRefresh(); }, 1500);
4811
+ btnJoin.textContent = 'Logout';
4812
+ btnJoin.className = 'btn-join btn-logout';
4813
+ setTimeout(function() { closeJoinModal(); checkTailscaleStatus(); fullRefresh(); }, 1500);
4717
4814
  } else {
4718
4815
  joinStatus.className = 'modal-status error';
4719
4816
  joinStatus.textContent = data.output || 'Failed to join network';
@@ -4726,6 +4823,133 @@ body::after{
4726
4823
  btnSubmit.textContent = 'Connect';
4727
4824
  });
4728
4825
 
4826
+ // ---- Interactive Login ----
4827
+ const btnInteractiveLogin = $('#btnInteractiveLogin');
4828
+ btnInteractiveLogin.addEventListener('click', async function() {
4829
+ const statusEl = $('#interactiveStatus');
4830
+ statusEl.className = 'modal-status';
4831
+ statusEl.textContent = '';
4832
+ btnInteractiveLogin.disabled = true;
4833
+ btnInteractiveLogin.innerHTML = '<span class="spinner"></span>Opening browser...';
4834
+
4835
+ try {
4836
+ const r = await fetch('/dashboard/api/tailscale-login', { method: 'POST' });
4837
+ const data = await r.json();
4838
+
4839
+ if (data.success && data.url) {
4840
+ // Open the auth URL in a new tab
4841
+ window.open(data.url, '_blank');
4842
+ statusEl.className = 'modal-status success';
4843
+ statusEl.textContent = 'Authentication page opened in your browser. Waiting for connection...';
4844
+
4845
+ // Poll for connection
4846
+ var pollCount = 0;
4847
+ var pollInterval = setInterval(async function() {
4848
+ pollCount++;
4849
+ if (pollCount > 40) { // 2 minutes timeout
4850
+ clearInterval(pollInterval);
4851
+ statusEl.className = 'modal-status error';
4852
+ statusEl.textContent = 'Timed out waiting for authentication. Please try again.';
4853
+ btnInteractiveLogin.disabled = false;
4854
+ btnInteractiveLogin.textContent = 'Open Browser Login';
4855
+ return;
4856
+ }
4857
+ try {
4858
+ var sr = await fetch('/dashboard/api/tailscale-status');
4859
+ var sd = await sr.json();
4860
+ if (sd.state === 'connected') {
4861
+ clearInterval(pollInterval);
4862
+ btnJoin.textContent = 'Logout';
4863
+ btnJoin.className = 'btn-join btn-logout';
4864
+ closeJoinModal();
4865
+ checkTailscaleStatus();
4866
+ fullRefresh();
4867
+ return;
4868
+ }
4869
+ } catch { /* poll error, continue */ }
4870
+ }, 3000);
4871
+ } else {
4872
+ statusEl.className = 'modal-status error';
4873
+ statusEl.textContent = data.output || 'Failed to start interactive login';
4874
+ btnInteractiveLogin.disabled = false;
4875
+ btnInteractiveLogin.textContent = 'Open Browser Login';
4876
+ }
4877
+ } catch (e) {
4878
+ statusEl.className = 'modal-status error';
4879
+ statusEl.textContent = 'Network error: ' + (e.message || 'unknown');
4880
+ btnInteractiveLogin.disabled = false;
4881
+ btnInteractiveLogin.textContent = 'Open Browser Login';
4882
+ }
4883
+ });
4884
+
4885
+ // ---- Logout Modal ----
4886
+ const logoutModal = $('#logoutModal');
4887
+ const btnConfirmLogout = $('#btnConfirmLogout');
4888
+ const btnCancelLogout = $('#btnCancelLogout');
4889
+ const logoutStatus = $('#logoutStatus');
4890
+
4891
+ function openLogoutModal() {
4892
+ logoutStatus.className = 'modal-status';
4893
+ logoutStatus.textContent = '';
4894
+ logoutModal.classList.add('open');
4895
+ }
4896
+
4897
+ btnCancelLogout.addEventListener('click', function() { logoutModal.classList.remove('open'); });
4898
+ logoutModal.addEventListener('click', function(e) { if (e.target === logoutModal) logoutModal.classList.remove('open'); });
4899
+
4900
+ btnConfirmLogout.addEventListener('click', async function() {
4901
+ btnConfirmLogout.disabled = true;
4902
+ btnConfirmLogout.innerHTML = '<span class="spinner"></span>Logging out...';
4903
+ logoutStatus.className = 'modal-status';
4904
+ logoutStatus.textContent = '';
4905
+
4906
+ try {
4907
+ const r = await fetch('/dashboard/api/logout', { method: 'POST' });
4908
+ const data = await r.json();
4909
+ logoutModal.classList.remove('open');
4910
+ if (data.success) {
4911
+ checkTailscaleStatus();
4912
+ fullRefresh();
4913
+ } else {
4914
+ alert('Logout failed: ' + (data.output || 'unknown error'));
4915
+ }
4916
+ } catch (e) {
4917
+ logoutModal.classList.remove('open');
4918
+ alert('Network error: ' + (e.message || 'unknown'));
4919
+ }
4920
+ btnConfirmLogout.disabled = false;
4921
+ btnConfirmLogout.textContent = 'Log Out';
4922
+ });
4923
+
4924
+ // ---- Install Tailscale ----
4925
+ async function doInstallTailscale() {
4926
+ if (!confirm('Install Tailscale on this machine?')) return;
4927
+
4928
+ btnJoin.disabled = true;
4929
+ btnJoin.innerHTML = '<span class="spinner"></span>Installing...';
4930
+
4931
+ try {
4932
+ const r = await fetch('/dashboard/api/install-tailscale', { method: 'POST' });
4933
+ const data = await r.json();
4934
+ if (data.success) {
4935
+ btnJoin.textContent = 'Installed';
4936
+ btnJoin.disabled = false;
4937
+ checkTailscaleStatus();
4938
+ fullRefresh();
4939
+ } else {
4940
+ alert('Installation failed: ' + (data.output || 'unknown error'));
4941
+ btnJoin.textContent = 'Install Tailscale';
4942
+ btnJoin.className = 'btn-join btn-install';
4943
+ btnJoin.disabled = false;
4944
+ }
4945
+ } catch (e) {
4946
+ alert('Network error: ' + (e.message || 'unknown'));
4947
+ btnJoin.textContent = 'Install Tailscale';
4948
+ btnJoin.className = 'btn-join btn-install';
4949
+ btnJoin.disabled = false;
4950
+ }
4951
+ }
4952
+
4729
4953
  // Check initial Tailscale status (tri-state)
4730
4954
  let tsState = null;
4731
4955
  let tsInstallInfo = null;
@@ -4744,17 +4968,23 @@ body::after{
4744
4968
  if (tsState.state === 'connected') {
4745
4969
  dot.className = 'status-dot';
4746
4970
  text.textContent = 'ONLINE';
4747
- btnJoin.textContent = 'Connected';
4748
- btnJoin.classList.add('connected');
4971
+ btnJoin.textContent = 'Logout';
4972
+ btnJoin.className = 'btn-join btn-logout';
4973
+ btnJoin.style.display = '';
4749
4974
  panel.style.display = 'none';
4750
4975
  } else if (tsState.state === 'not_installed') {
4751
4976
  dot.className = 'status-dot not-installed';
4752
4977
  text.textContent = 'NOT INSTALLED';
4753
- btnJoin.style.display = 'none';
4978
+ btnJoin.textContent = 'Install Tailscale';
4979
+ btnJoin.className = 'btn-join btn-install';
4980
+ btnJoin.style.display = '';
4754
4981
  await renderNotInstalledPanel();
4755
4982
  } else {
4756
4983
  dot.className = 'status-dot not-connected';
4757
4984
  text.textContent = 'NOT CONNECTED';
4985
+ btnJoin.textContent = 'Join Network';
4986
+ btnJoin.className = 'btn-join';
4987
+ btnJoin.style.display = '';
4758
4988
  await renderNotConnectedPanel();
4759
4989
  }
4760
4990
  }
@@ -4788,7 +5018,7 @@ body::after{
4788
5018
  html += '</div>';
4789
5019
  }
4790
5020
 
4791
- html += '<div class="ts-setup-hint">Or run <code style="color:var(--cyan)">npx open-party setup</code> for guided installation</div>';
5021
+ 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>';
4792
5022
  html += '<button class="btn-redetect" onclick="window.__redetectTailscale()">Re-detect</button>';
4793
5023
  panel.innerHTML = html;
4794
5024
  panel.style.display = 'block';
@@ -4796,14 +5026,10 @@ body::after{
4796
5026
 
4797
5027
  async function renderNotConnectedPanel() {
4798
5028
  const panel = $('#tsPanel');
4799
- const btnJoin = $('#btnJoinNetwork');
4800
- btnJoin.style.display = '';
4801
- btnJoin.textContent = 'Join Network';
4802
- btnJoin.classList.remove('connected');
4803
5029
 
4804
5030
  let html = '<div class="ts-panel-title not-connected">Tailscale Not Connected</div>';
4805
5031
  html += '<div class="ts-info-row"><span class="label">Status:</span><span class="value" style="color:var(--yellow)">Installed but not authenticated</span></div>';
4806
- 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>';
5032
+ html += '<div class="ts-setup-hint">Use the <strong>Join Network</strong> button above to log in</div>';
4807
5033
  html += '<button class="btn-redetect" onclick="window.__redetectTailscale()">Re-detect</button>';
4808
5034
  panel.innerHTML = html;
4809
5035
  panel.style.display = 'block';
@@ -4829,7 +5055,7 @@ body::after{
4829
5055
  });
4830
5056
 
4831
5057
  // src/server/routes/dashboard.ts
4832
- var dashboardRoutes;
5058
+ var dashboardRoutes, activeLogin;
4833
5059
  var init_dashboard = __esm({
4834
5060
  "src/server/routes/dashboard.ts"() {
4835
5061
  "use strict";
@@ -4935,6 +5161,37 @@ var init_dashboard = __esm({
4935
5161
  return c.json({ success: false, output: e.message }, 500);
4936
5162
  }
4937
5163
  });
5164
+ activeLogin = null;
5165
+ dashboardRoutes.post("/api/logout", async (c) => {
5166
+ const result = logoutTailscale();
5167
+ if (result.success) {
5168
+ resetTailscaleBinaryCache();
5169
+ refreshSelfIp();
5170
+ }
5171
+ return c.json(result, result.success ? 200 : 500);
5172
+ });
5173
+ dashboardRoutes.post("/api/tailscale-login", async (c) => {
5174
+ if (activeLogin?.url) {
5175
+ return c.json({ success: true, url: activeLogin.url });
5176
+ }
5177
+ const { promise, process: process2 } = startInteractiveLogin();
5178
+ activeLogin = { process: process2 };
5179
+ const result = await promise;
5180
+ if (result.success && result.url) {
5181
+ activeLogin.url = result.url;
5182
+ return c.json({ success: true, url: result.url });
5183
+ }
5184
+ activeLogin = null;
5185
+ return c.json({ success: false, output: result.output }, 500);
5186
+ });
5187
+ dashboardRoutes.post("/api/install-tailscale", async (c) => {
5188
+ const { installTailscale: installTailscale2 } = await Promise.resolve().then(() => (init_tailscale_installer(), tailscale_installer_exports));
5189
+ const result = await installTailscale2(process.platform);
5190
+ if (result.success) {
5191
+ resetTailscaleBinaryCache();
5192
+ }
5193
+ return c.json(result, result.success ? 200 : 500);
5194
+ });
4938
5195
  }
4939
5196
  });
4940
5197
 
@@ -5005,49 +5262,7 @@ var init_server = __esm({
5005
5262
 
5006
5263
  // src/cli/setup.ts
5007
5264
  init_tailscale();
5008
- import { createInterface } from "readline";
5009
-
5010
- // src/cli/tailscale-installer.ts
5011
- import { spawn } from "child_process";
5012
- var INSTALL_COMMANDS = {
5013
- linux: { cmd: "bash", args: ["-c", "curl -fsSL https://tailscale.com/install.sh | sh"], needsSudo: true },
5014
- darwin: { cmd: "brew", args: ["install", "tailscale"], needsSudo: false },
5015
- win32: { cmd: "winget", args: ["install", "Tailscale.Tailscale", "--accept-source-agreements"], needsSudo: false }
5016
- };
5017
- async function installTailscale(platform) {
5018
- const entry = INSTALL_COMMANDS[platform];
5019
- if (!entry) {
5020
- return {
5021
- success: false,
5022
- output: `Unsupported platform: ${platform}. Please install manually from https://tailscale.com/download`
5023
- };
5024
- }
5025
- const cmd = entry.needsSudo ? "sudo" : entry.cmd;
5026
- const args2 = entry.needsSudo ? [entry.cmd, ...entry.args] : entry.args;
5027
- console.log(`Running: ${cmd} ${args2.join(" ")}
5028
- `);
5029
- return new Promise((resolve3) => {
5030
- const child = spawn(cmd, args2, {
5031
- stdio: "inherit",
5032
- windowsHide: true
5033
- });
5034
- let exited = false;
5035
- child.on("close", (code) => {
5036
- if (exited) return;
5037
- exited = true;
5038
- if (code === 0) {
5039
- resolve3({ success: true, output: "Installation completed." });
5040
- } else {
5041
- resolve3({ success: false, output: `Installation exited with code ${code}` });
5042
- }
5043
- });
5044
- child.on("error", (err) => {
5045
- if (exited) return;
5046
- exited = true;
5047
- resolve3({ success: false, output: err.message });
5048
- });
5049
- });
5050
- }
5265
+ init_tailscale_installer();
5051
5266
 
5052
5267
  // src/cli/agent-detector.ts
5053
5268
  import { existsSync as existsSync2 } from "fs";
@@ -5332,11 +5547,12 @@ async function installPluginToAgent(agentType) {
5332
5547
  }
5333
5548
  }
5334
5549
 
5335
- // src/cli/setup.ts
5336
- var rl = createInterface({ input: process.stdin, output: process.stdout });
5337
- function prompt(question) {
5338
- return new Promise((resolve3) => rl.question(question, (answer) => resolve3(answer.trim())));
5339
- }
5550
+ // src/cli/tailscale-login.ts
5551
+ init_tailscale();
5552
+ import { spawn as spawn2 } from "child_process";
5553
+
5554
+ // src/cli/tty-utils.ts
5555
+ import { createInterface } from "readline";
5340
5556
  function cyan(text) {
5341
5557
  return `\x1B[36m${text}\x1B[0m`;
5342
5558
  }
@@ -5352,10 +5568,222 @@ function red(text) {
5352
5568
  function bold(text) {
5353
5569
  return `\x1B[1m${text}\x1B[0m`;
5354
5570
  }
5571
+ function dim(text) {
5572
+ return `\x1B[2m${text}\x1B[0m`;
5573
+ }
5574
+ function createRl() {
5575
+ return createInterface({ input: process.stdin, output: process.stdout });
5576
+ }
5577
+ function closeRl(rl) {
5578
+ rl.close();
5579
+ }
5580
+ async function prompt(question) {
5581
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
5582
+ return new Promise((resolve4) => {
5583
+ rl.question(question, (answer) => {
5584
+ rl.close();
5585
+ resolve4(answer.trim());
5586
+ });
5587
+ });
5588
+ }
5589
+ async function select(options, opts) {
5590
+ if (options.length === 0) throw new Error("select() requires at least one option");
5591
+ if (options.length === 1) return options[0].value;
5592
+ const message = opts?.message ?? "";
5593
+ const wasRaw = process.stdin.isRaw;
5594
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
5595
+ try {
5596
+ let cursor = 0;
5597
+ const render = () => {
5598
+ const lines = options.length + (message ? 1 : 0);
5599
+ process.stdout.write(`\x1B[${lines}A\x1B[0J`);
5600
+ if (message) {
5601
+ process.stdout.write(`${message}
5602
+ `);
5603
+ }
5604
+ for (let i = 0; i < options.length; i++) {
5605
+ const opt = options[i];
5606
+ const prefix = i === cursor ? `${cyan("\u276F")} ` : " ";
5607
+ const label = i === cursor ? bold(opt.label) : opt.label;
5608
+ const hintStr = opt.hint ? ` ${dim(opt.hint)}` : "";
5609
+ process.stdout.write(`${prefix}${label}${hintStr}
5610
+ `);
5611
+ }
5612
+ };
5613
+ if (message) {
5614
+ process.stdout.write(`${message}
5615
+ `);
5616
+ }
5617
+ for (let i = 0; i < options.length; i++) {
5618
+ const opt = options[i];
5619
+ const prefix = i === cursor ? `${cyan("\u276F")} ` : " ";
5620
+ const label = i === cursor ? bold(opt.label) : opt.label;
5621
+ const hintStr = opt.hint ? ` ${dim(opt.hint)}` : "";
5622
+ process.stdout.write(`${prefix}${label}${hintStr}
5623
+ `);
5624
+ }
5625
+ return new Promise((resolve4) => {
5626
+ const onKey = (ch, key) => {
5627
+ if (key.name === "up" || key.sequence === "\x1B[A") {
5628
+ cursor = (cursor - 1 + options.length) % options.length;
5629
+ render();
5630
+ } else if (key.name === "down" || key.sequence === "\x1B[B") {
5631
+ cursor = (cursor + 1) % options.length;
5632
+ render();
5633
+ } else if (key.name === "return" || key.sequence === "\r") {
5634
+ process.stdin.removeListener("keypress", onKey);
5635
+ process.stdout.write("\n");
5636
+ resolve4(options[cursor].value);
5637
+ }
5638
+ };
5639
+ process.stdin.on("keypress", onKey);
5640
+ if (process.stdin.isTTY) {
5641
+ process.stdin.resume();
5642
+ }
5643
+ });
5644
+ } finally {
5645
+ if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw ?? false);
5646
+ }
5647
+ }
5648
+ async function multiSelect(options, opts) {
5649
+ if (options.length === 0) return [];
5650
+ const message = opts?.message ?? "";
5651
+ const wasRaw = process.stdin.isRaw;
5652
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
5653
+ try {
5654
+ let cursor = 0;
5655
+ const selected = /* @__PURE__ */ new Set();
5656
+ const render = () => {
5657
+ const lines = options.length + (message ? 1 : 0) + 1;
5658
+ process.stdout.write(`\x1B[${lines}A\x1B[0J`);
5659
+ if (message) {
5660
+ process.stdout.write(`${message}
5661
+ `);
5662
+ }
5663
+ process.stdout.write(`${dim(" \u2191/\u2193 navigate, space to select, enter to confirm")}
5664
+ `);
5665
+ for (let i = 0; i < options.length; i++) {
5666
+ const opt = options[i];
5667
+ const prefix = i === cursor ? `${cyan("\u276F")} ` : " ";
5668
+ const check = selected.has(i) ? green("\u25C9") : "\u25CB";
5669
+ const label = i === cursor ? bold(opt.label) : opt.label;
5670
+ process.stdout.write(`${prefix}${check} ${label}
5671
+ `);
5672
+ }
5673
+ };
5674
+ if (message) {
5675
+ process.stdout.write(`${message}
5676
+ `);
5677
+ }
5678
+ process.stdout.write(`${dim(" \u2191/\u2193 navigate, space to select, enter to confirm")}
5679
+ `);
5680
+ for (let i = 0; i < options.length; i++) {
5681
+ const opt = options[i];
5682
+ const prefix = i === cursor ? `${cyan("\u276F")} ` : " ";
5683
+ const check = selected.has(i) ? green("\u25C9") : "\u25CB";
5684
+ const label = i === cursor ? bold(opt.label) : opt.label;
5685
+ process.stdout.write(`${prefix}${check} ${label}
5686
+ `);
5687
+ }
5688
+ return new Promise((resolve4) => {
5689
+ const onKey = (_ch, key) => {
5690
+ if (key.name === "up" || key.sequence === "\x1B[A") {
5691
+ cursor = (cursor - 1 + options.length) % options.length;
5692
+ render();
5693
+ } else if (key.name === "down" || key.sequence === "\x1B[B") {
5694
+ cursor = (cursor + 1) % options.length;
5695
+ render();
5696
+ } else if (key.name === "space") {
5697
+ if (selected.has(cursor)) {
5698
+ selected.delete(cursor);
5699
+ } else {
5700
+ selected.add(cursor);
5701
+ }
5702
+ render();
5703
+ } else if (key.name === "return" || key.sequence === "\r") {
5704
+ process.stdin.removeListener("keypress", onKey);
5705
+ process.stdout.write("\n");
5706
+ const result = Array.from(selected).sort((a, b) => a - b).map((i) => options[i].value);
5707
+ resolve4(result);
5708
+ }
5709
+ };
5710
+ process.stdin.on("keypress", onKey);
5711
+ if (process.stdin.isTTY) {
5712
+ process.stdin.resume();
5713
+ }
5714
+ });
5715
+ } finally {
5716
+ if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw ?? false);
5717
+ }
5718
+ }
5719
+
5720
+ // src/cli/tailscale-login.ts
5721
+ async function interactiveLogin(binary) {
5722
+ console.log(`
5723
+ ${cyan("Running interactive login...")}`);
5724
+ console.log("A browser window should open. Authenticate in the browser, then return here.\n");
5725
+ const child = spawn2(binary, ["login"], { stdio: "inherit" });
5726
+ const exitCode = await new Promise((resolve4) => {
5727
+ child.on("close", resolve4);
5728
+ });
5729
+ resetTailscaleBinaryCache();
5730
+ const status = getTailscaleInstallationStatus();
5731
+ if (exitCode === 0 && status.state === "connected") {
5732
+ console.log(`
5733
+ ${green("\u2705 Login successful!")} IP: ${status.tailscale_ip}`);
5734
+ showAuthKeyTip();
5735
+ return true;
5736
+ }
5737
+ console.log(`
5738
+ ${yellow("\u26A0\uFE0F Login may not have completed. Status: " + status.state)}`);
5739
+ console.log(" Try running: open-party login");
5740
+ return false;
5741
+ }
5742
+ async function authKeyLogin(binary) {
5743
+ console.log("");
5744
+ console.log("Ask the network creator to generate an Auth Key at:");
5745
+ console.log(`${cyan(" https://login.tailscale.com/admin/settings/keys")}
5746
+ `);
5747
+ const authKey = await prompt("Enter Auth Key: ");
5748
+ if (!authKey) {
5749
+ console.log(yellow("No auth key provided, skipping login."));
5750
+ return false;
5751
+ }
5752
+ const result = joinTailnet(authKey);
5753
+ if (result.success) {
5754
+ resetTailscaleBinaryCache();
5755
+ const status = getTailscaleInstallationStatus();
5756
+ console.log(`
5757
+ ${green("\u2705 Login successful!")} IP: ${status.state === "connected" ? status.tailscale_ip : "unknown"}`);
5758
+ showAuthKeyTip();
5759
+ return true;
5760
+ }
5761
+ console.log(`
5762
+ ${red("\u274C Login failed:")}
5763
+ ${result.output}`);
5764
+ console.log(" Check your auth key and try again.");
5765
+ return false;
5766
+ }
5767
+ function showAuthKeyTip() {
5768
+ console.log("");
5769
+ console.log(`${bold("\u{1F4A1} To share network access with teammates:")}`);
5770
+ console.log(" 1. Go to https://login.tailscale.com/admin/settings/keys");
5771
+ console.log(" 2. Generate an Auth Key");
5772
+ console.log(" 3. Share it with teammates \u2014 they can run: open-party login");
5773
+ }
5774
+
5775
+ // src/cli/setup.ts
5355
5776
  async function stepTailscale() {
5356
5777
  console.log(`
5357
- ${bold(cyan("\u{1F50D} Step 1: Detecting Tailscale..."))}
5778
+ ${bold(cyan("\u{1F50D} Step 1: Tailscale Network"))}
5358
5779
  `);
5780
+ console.log(
5781
+ "Tailscale enables agents across different machines to discover and\ncommunicate with each other over a secure network.\n"
5782
+ );
5783
+ console.log(
5784
+ `${dim("Without Tailscale, Open Party runs in local mode \u2014 connecting only\nto agents on this machine.")}
5785
+ `
5786
+ );
5359
5787
  const status = getTailscaleInstallationStatus();
5360
5788
  if (status.state === "connected") {
5361
5789
  console.log(`${green("\u2705 Tailscale is connected!")}`);
@@ -5363,12 +5791,10 @@ ${bold(cyan("\u{1F50D} Step 1: Detecting Tailscale..."))}
5363
5791
  return;
5364
5792
  }
5365
5793
  if (status.state === "not_installed") {
5366
- console.log(`${red("\u274C Tailscale is not installed.")}`);
5367
5794
  await handleNotInstalled(status.platform);
5368
5795
  const newStatus = getTailscaleInstallationStatus();
5369
5796
  if (newStatus.state === "not_installed") {
5370
- console.log(`
5371
- ${yellow("\u26A0\uFE0F Tailscale still not detected. Please install manually and re-run setup.")}`);
5797
+ showLocalModeNotice();
5372
5798
  return;
5373
5799
  }
5374
5800
  if (newStatus.state === "connected") {
@@ -5383,6 +5809,7 @@ ${green("\u2705 Tailscale is connected!")} IP: ${newStatus.tailscale_ip}`);
5383
5809
  }
5384
5810
  async function handleNotInstalled(platform) {
5385
5811
  const info = getInstallInstructions(platform);
5812
+ console.log(`${red("\u274C Tailscale is not installed.")}`);
5386
5813
  console.log(`
5387
5814
  Install Tailscale for ${bold(info.os)}:
5388
5815
  `);
@@ -5393,99 +5820,61 @@ Install Tailscale for ${bold(info.os)}:
5393
5820
  }
5394
5821
  }
5395
5822
  console.log(`
5396
- Download: ${info.download_url}`);
5397
- const autoInstall = info.commands.length > 0 && platform !== "win32";
5398
- if (autoInstall) {
5399
- const answer = await prompt(`
5400
- ${bold("Install Tailscale automatically?")} [yes/no]: `);
5401
- if (answer.toLowerCase() === "yes" || answer.toLowerCase() === "y") {
5402
- console.log("");
5403
- const result = await installTailscale(platform);
5404
- if (result.success) {
5405
- console.log(`${green("\u2705 Tailscale installed successfully!")}`);
5406
- } else {
5407
- console.log(`${red("\u274C Installation failed:")}
5823
+ Download: ${info.download_url}
5824
+ `);
5825
+ const options = [];
5826
+ if (info.commands.length > 0 && platform !== "win32") {
5827
+ options.push({ label: "Install Tailscale automatically", value: "auto", hint: "recommended" });
5828
+ }
5829
+ options.push({ label: "I've installed Tailscale, re-detect", value: "redetect" });
5830
+ options.push({ label: "Skip \u2014 use local mode only", value: "skip", hint: "agents on this machine only" });
5831
+ const choice = await select(options, { message: "Choose:" });
5832
+ if (choice === "skip") {
5833
+ return;
5834
+ }
5835
+ if (choice === "auto") {
5836
+ console.log("");
5837
+ const result = await installTailscale(platform);
5838
+ if (result.success) {
5839
+ console.log(`${green("\u2705 Tailscale installed successfully!")}`);
5840
+ } else {
5841
+ console.log(`${red("\u274C Installation failed:")}
5408
5842
  ${result.output}`);
5409
- console.log(`
5410
- Please install manually and re-run \x1B[36mnpx open-party setup\x1B[0m`);
5411
- }
5412
- return;
5843
+ console.log(`
5844
+ Please install manually and re-run: ${cyan("open-party setup")}`);
5413
5845
  }
5846
+ return;
5414
5847
  }
5415
- console.log(`
5416
- Or run: ${cyan("npx open-party setup")} (after installing manually)`);
5417
- if (autoInstall) {
5418
- const cont = await prompt('Press Enter when installation is complete, or type "skip" to skip...');
5419
- if (cont.toLowerCase() === "skip") return;
5848
+ if (choice === "redetect") {
5420
5849
  resetTailscaleBinaryCache();
5850
+ return;
5421
5851
  }
5422
5852
  }
5423
5853
  async function handleNotConnected(binary) {
5424
- console.log(`${yellow("\u{1F512} Tailscale is installed but not connected.")}`);
5425
- console.log("");
5426
- console.log("Choose a login method:\n");
5427
- console.log(" 1. Interactive login (opens browser to authenticate)");
5428
- console.log(" 2. Auth key (enter from Tailscale admin console)");
5429
- console.log("");
5430
- const choice = await prompt("Select [1/2]: ");
5431
- if (choice === "1") {
5432
- await handleInteractiveLogin(binary);
5433
- } else {
5434
- await handleAuthKeyLogin(binary);
5435
- }
5436
- }
5437
- async function handleInteractiveLogin(binary) {
5438
- console.log(`
5439
- ${cyan("Running interactive login...")}`);
5440
- console.log("A browser window should open. Authenticate in the browser, then return here.\n");
5441
- const { spawn: spawn3 } = await import("child_process");
5442
- const child = spawn3(binary, ["login"], { stdio: "inherit" });
5443
- const exitCode = await new Promise((resolve3) => {
5444
- child.on("close", resolve3);
5445
- });
5446
- resetTailscaleBinaryCache();
5447
- const status = getTailscaleInstallationStatus();
5448
- if (exitCode === 0 && status.state === "connected") {
5449
- console.log(`
5450
- ${green("\u2705 Login successful!")} IP: ${status.tailscale_ip}`);
5451
- showAuthKeyTip();
5452
- } else {
5453
- console.log(`
5454
- ${yellow("\u26A0\uFE0F Login may not have completed. Status: " + status.state)}`);
5455
- console.log(" Try running: npx open-party setup");
5456
- }
5457
- }
5458
- async function handleAuthKeyLogin(binary) {
5459
- console.log("");
5460
- console.log("Ask the network creator to generate an Auth Key at:");
5461
- console.log(`${cyan(" https://login.tailscale.com/admin/settings/keys")}
5854
+ console.log(`${yellow("\u{1F512} Tailscale is installed but not connected.")}
5462
5855
  `);
5463
- const authKey = await prompt("Enter Auth Key: ");
5464
- if (!authKey) {
5465
- console.log(yellow("No auth key provided, skipping login."));
5466
- return;
5467
- }
5468
- const { joinTailnet: joinTailnet2 } = await Promise.resolve().then(() => (init_tailscale(), tailscale_exports));
5469
- const result = joinTailnet2(authKey);
5470
- if (result.success) {
5471
- resetTailscaleBinaryCache();
5472
- const status = getTailscaleInstallationStatus();
5473
- console.log(`
5474
- ${green("\u2705 Login successful!")} IP: ${status.state === "connected" ? status.tailscale_ip : "unknown"}`);
5475
- showAuthKeyTip();
5856
+ const options = [
5857
+ { label: "Interactive login", value: "interactive", hint: "opens browser to authenticate" },
5858
+ { label: "Auth key", value: "authkey", hint: "from network creator" },
5859
+ { label: "Skip", value: "skip", hint: "login later with: open-party login" }
5860
+ ];
5861
+ const choice = await select(options, { message: "Choose a login method:" });
5862
+ if (choice === "interactive") {
5863
+ await interactiveLogin(binary);
5864
+ } else if (choice === "authkey") {
5865
+ await authKeyLogin(binary);
5476
5866
  } else {
5477
5867
  console.log(`
5478
- ${red("\u274C Login failed:")}
5479
- ${result.output}`);
5480
- console.log(" Check your auth key and try again.");
5868
+ ${yellow("\u26A0\uFE0F Tailscale not connected. Running in local mode.")}`);
5869
+ console.log(` To connect later, run: ${cyan("open-party login")}`);
5481
5870
  }
5482
5871
  }
5483
- function showAuthKeyTip() {
5484
- console.log("");
5485
- console.log(`${bold("\u{1F4A1} To share network access with teammates:")}`);
5486
- console.log(" 1. Go to https://login.tailscale.com/admin/settings/keys");
5487
- console.log(" 2. Generate an Auth Key");
5488
- console.log(" 3. Share it with teammates \u2014 they can run: npx open-party setup");
5872
+ function showLocalModeNotice() {
5873
+ console.log(`
5874
+ ${yellow("\u26A0\uFE0F Running in local mode \u2014 connecting to agents on this machine only.")}`);
5875
+ console.log(" To enable cross-machine communication later:");
5876
+ console.log(` 1. Install Tailscale: ${cyan("https://tailscale.com/download")}`);
5877
+ console.log(` 2. Run: ${cyan("open-party login")}`);
5489
5878
  }
5490
5879
  async function stepAgentPlugin() {
5491
5880
  console.log(`
@@ -5497,7 +5886,7 @@ ${bold(cyan("\u{1F50D} Step 2: Detecting AI agents in your environment..."))}
5497
5886
  console.log(yellow("No supported AI agents detected in this environment."));
5498
5887
  console.log(" Supported agents: Claude Code, Cursor, Gemini CLI");
5499
5888
  console.log("");
5500
- console.log(" Install one and re-run: npx open-party setup");
5889
+ console.log(" Install one and re-run: open-party setup");
5501
5890
  return;
5502
5891
  }
5503
5892
  console.log("Detected agents:\n");
@@ -5505,7 +5894,10 @@ ${bold(cyan("\u{1F50D} Step 2: Detecting AI agents in your environment..."))}
5505
5894
  console.log(` ${green("\u2713")} ${agent.name}`);
5506
5895
  }
5507
5896
  console.log("");
5508
- const selected = await selectAgents(detected);
5897
+ const options = detected.map((a) => ({ label: a.name, value: a }));
5898
+ const selected = await multiSelect(options, {
5899
+ message: "Select agents to install Open Party plugin:"
5900
+ });
5509
5901
  if (selected.length === 0) {
5510
5902
  console.log(yellow("No agents selected, skipping plugin installation."));
5511
5903
  return;
@@ -5527,32 +5919,19 @@ Installing Open Party plugin for ${agent.name}...`);
5527
5919
  }
5528
5920
  }
5529
5921
  }
5530
- async function selectAgents(agents) {
5531
- console.log("Select agents to install Open Party plugin:\n");
5532
- for (let i = 0; i < agents.length; i++) {
5533
- console.log(` [${i + 1}] ${agents[i].name}`);
5534
- }
5535
- console.log(` [a] All`);
5536
- console.log(` [n] None (skip)`);
5537
- console.log("");
5538
- const answer = await prompt('Enter selection (e.g. "1 2" or "a" or "n"): ');
5539
- if (answer.toLowerCase() === "n" || answer === "") return [];
5540
- if (answer.toLowerCase() === "a") return agents;
5541
- const indices = answer.split(/[\s,]+/).map((s) => parseInt(s, 10) - 1).filter((i) => i >= 0 && i < agents.length);
5542
- return indices.map((i) => agents[i]);
5543
- }
5544
5922
  async function setupCommand() {
5545
5923
  console.log(bold(cyan("\n\u{1F680} Open Party Setup Wizard\n")));
5924
+ const rl = createRl();
5546
5925
  await stepTailscale();
5547
5926
  await stepAgentPlugin();
5548
5927
  console.log(`
5549
5928
  ${bold(cyan("\u{1F680} Starting Party Server..."))}`);
5550
- const { spawn: spawn3 } = await import("child_process");
5551
- const { resolve: resolve3, dirname: dirname4 } = await import("path");
5552
- const { fileURLToPath: fileURLToPath2 } = await import("url");
5553
- const __dirname2 = dirname4(fileURLToPath2(import.meta.url));
5554
- const serverScript = resolve3(__dirname2, "..", "party-server.js");
5555
- const serverProc = spawn3(process.execPath, [serverScript], {
5929
+ const { spawn: spawn4 } = await import("child_process");
5930
+ const { resolve: resolve4, dirname: dirname5 } = await import("path");
5931
+ const { fileURLToPath: fileURLToPath3 } = await import("url");
5932
+ const __dirname2 = dirname5(fileURLToPath3(import.meta.url));
5933
+ const serverScript = resolve4(__dirname2, "..", "party-server.js");
5934
+ const serverProc = spawn4(process.execPath, [serverScript], {
5556
5935
  detached: true,
5557
5936
  stdio: "ignore",
5558
5937
  windowsHide: true
@@ -5563,11 +5942,70 @@ ${bold(cyan("\u{1F680} Starting Party Server..."))}`);
5563
5942
  ${bold(green("\u{1F389} Setup complete!"))}`);
5564
5943
  console.log(` Dashboard: http://127.0.0.1:8000/dashboard`);
5565
5944
  console.log(" Other agents can join with: npx @feynmanzhang/open-party setup\n");
5566
- rl.close();
5945
+ closeRl(rl);
5946
+ }
5947
+
5948
+ // src/cli/login.ts
5949
+ init_tailscale();
5950
+ async function loginCommand() {
5951
+ const status = getTailscaleInstallationStatus();
5952
+ if (status.state === "connected") {
5953
+ console.log(`${green("\u2705 Tailscale is already connected!")}`);
5954
+ console.log(` IP: ${status.tailscale_ip} Hostname: ${status.hostname}`);
5955
+ return;
5956
+ }
5957
+ if (status.state === "not_installed") {
5958
+ console.log(`${red("\u274C Tailscale is not installed.")}`);
5959
+ console.log(" Install it first: https://tailscale.com/download");
5960
+ console.log(` Then run: ${cyan("open-party login")}`);
5961
+ return;
5962
+ }
5963
+ console.log(`${yellow("\u{1F512} Tailscale is installed but not connected.")}
5964
+ `);
5965
+ const options = [
5966
+ { label: "Interactive login", value: "interactive", hint: "opens browser to authenticate" },
5967
+ { label: "Auth key", value: "authkey", hint: "from network creator" }
5968
+ ];
5969
+ const choice = await select(options, { message: "Choose a login method:" });
5970
+ if (choice === "interactive") {
5971
+ await interactiveLogin(status.binary);
5972
+ } else {
5973
+ await authKeyLogin(status.binary);
5974
+ }
5975
+ }
5976
+
5977
+ // src/cli/logout.ts
5978
+ init_tailscale();
5979
+ async function logoutCommand() {
5980
+ const status = getTailscaleInstallationStatus();
5981
+ if (status.state === "not_installed") {
5982
+ console.log(red("\u274C Tailscale is not installed."));
5983
+ return;
5984
+ }
5985
+ if (status.state === "not_connected") {
5986
+ console.log(yellow("\u26A0\uFE0F Tailscale is not connected \u2014 nothing to log out from."));
5987
+ return;
5988
+ }
5989
+ const choice = await select(
5990
+ [
5991
+ { label: "Log out (remove credentials)", value: "logout", hint: "need to re-authenticate next time" },
5992
+ { label: "Cancel", value: "cancel" }
5993
+ ],
5994
+ { message: "Are you sure you want to log out?" }
5995
+ );
5996
+ if (choice === "cancel") return;
5997
+ console.log("Logging out of Tailscale...");
5998
+ const result = logoutTailscale();
5999
+ if (result.success) {
6000
+ console.log(green("\u2705 Logged out successfully."));
6001
+ console.log(" To reconnect, run: open-party login");
6002
+ } else {
6003
+ console.log(red("\u274C Logout failed:"), result.output);
6004
+ }
5567
6005
  }
5568
6006
 
5569
6007
  // src/cli/server-utils.ts
5570
- import { spawn as spawn2, execSync as execSync3 } from "child_process";
6008
+ import { spawn as spawn3, execSync as execSync3 } from "child_process";
5571
6009
  import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2, openSync } from "fs";
5572
6010
  import { join as join4, dirname as dirname2, resolve as resolve2 } from "path";
5573
6011
  import { homedir as homedir3 } from "os";
@@ -5665,7 +6103,7 @@ async function spawnServerInBackground(port) {
5665
6103
  mkdirSync2(dirname2(logPath), { recursive: true });
5666
6104
  const logFd = openSync(logPath, "a");
5667
6105
  const env = { ...process.env, PARTY_PORT: String(port) };
5668
- const proc = spawn2(process.execPath, [script], {
6106
+ const proc = spawn3(process.execPath, [script], {
5669
6107
  stdio: ["ignore", logFd, logFd],
5670
6108
  detached: true,
5671
6109
  windowsHide: true,
@@ -5717,7 +6155,7 @@ function parseStartArgs(args2) {
5717
6155
  return { daemon, port };
5718
6156
  }
5719
6157
  function sleep(ms) {
5720
- return new Promise((resolve3) => setTimeout(resolve3, ms));
6158
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
5721
6159
  }
5722
6160
 
5723
6161
  // src/cli/start-server.ts
@@ -5845,6 +6283,151 @@ async function statusCommand() {
5845
6283
  }
5846
6284
  }
5847
6285
 
6286
+ // src/cli/version.ts
6287
+ import { readFileSync as readFileSync3 } from "fs";
6288
+ import { resolve as resolve3, dirname as dirname4 } from "path";
6289
+ import { fileURLToPath as fileURLToPath2 } from "url";
6290
+ function showVersion() {
6291
+ const pkgPath = resolve3(dirname4(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
6292
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
6293
+ console.log(`open-party v${pkg.version}`);
6294
+ }
6295
+
6296
+ // src/cli/agents.ts
6297
+ async function agentsCommand() {
6298
+ const port = resolvePort();
6299
+ if (!await isServerHealthy(port)) {
6300
+ console.log("Party Server is not running.");
6301
+ console.log(" Use 'open-party start' to start it.");
6302
+ return;
6303
+ }
6304
+ const overview = await getServerOverview(port);
6305
+ if (!overview) {
6306
+ console.log("Failed to get server overview.");
6307
+ return;
6308
+ }
6309
+ const agents = overview.agents;
6310
+ if (!agents) {
6311
+ console.log("No agent data available.");
6312
+ return;
6313
+ }
6314
+ const localAgents = agents.local_agents ?? [];
6315
+ const remoteAgents = agents.remote_agents ?? [];
6316
+ const localCount = agents.local_count ?? localAgents.length;
6317
+ const remoteCount = agents.remote_count ?? remoteAgents.length;
6318
+ if (localCount === 0) {
6319
+ console.log("Local agents: (none)");
6320
+ } else {
6321
+ console.log(`Local agents (${localCount}):`);
6322
+ for (const agent of localAgents) {
6323
+ const id = agent.agent_id ?? "?";
6324
+ const name = agent.display_name ?? id;
6325
+ const ago = formatTimeAgo(agent.last_heartbeat);
6326
+ console.log(` ${id.padEnd(20)} ${name.padEnd(16)} ${ago}`);
6327
+ }
6328
+ }
6329
+ if (remoteCount > 0) {
6330
+ console.log(`
6331
+ Remote agents (${remoteCount}):`);
6332
+ for (const agent of remoteAgents) {
6333
+ const id = agent.agent_id ?? "?";
6334
+ const name = agent.display_name ?? id;
6335
+ const via = agent.source_peer_ip ?? "?";
6336
+ const ago = formatTimeAgo(agent.last_heartbeat);
6337
+ console.log(` ${id.padEnd(20)} ${name.padEnd(16)} (via ${via}) ${ago}`);
6338
+ }
6339
+ }
6340
+ if (localCount === 0 && remoteCount === 0) {
6341
+ console.log("\nNo agents connected yet.");
6342
+ }
6343
+ }
6344
+ function formatTimeAgo(timestamp) {
6345
+ if (!timestamp) return "\u2014";
6346
+ const diff = Date.now() / 1e3 - timestamp / 1e3;
6347
+ if (diff < 60) return "just now";
6348
+ if (diff < 3600) return `${Math.floor(diff / 60)} min ago`;
6349
+ if (diff < 86400) return `${Math.floor(diff / 3600)} hr ago`;
6350
+ return `${Math.floor(diff / 86400)}d ago`;
6351
+ }
6352
+
6353
+ // src/cli/peers.ts
6354
+ async function peersCommand() {
6355
+ const port = resolvePort();
6356
+ if (!await isServerHealthy(port)) {
6357
+ console.log("Party Server is not running.");
6358
+ console.log(" Use 'open-party start' to start it.");
6359
+ return;
6360
+ }
6361
+ const overview = await getServerOverview(port);
6362
+ if (!overview) {
6363
+ console.log("Failed to get server overview.");
6364
+ return;
6365
+ }
6366
+ const peers = overview.peers;
6367
+ if (!peers) {
6368
+ console.log("No peer data available.");
6369
+ return;
6370
+ }
6371
+ const details = peers.details ?? [];
6372
+ const remoteAgents = overview.agents?.remote_agents ?? [];
6373
+ const peerAgentCounts = /* @__PURE__ */ new Map();
6374
+ for (const agent of remoteAgents) {
6375
+ const ip = agent.source_peer_ip;
6376
+ peerAgentCounts.set(ip, (peerAgentCounts.get(ip) ?? 0) + 1);
6377
+ }
6378
+ const total = peers.total ?? details.length;
6379
+ if (details.length === 0) {
6380
+ console.log("No peers discovered yet.");
6381
+ return;
6382
+ }
6383
+ console.log(`Peers (${total}):
6384
+ `);
6385
+ for (const peer of details) {
6386
+ const agentCount = peerAgentCounts.get(peer.ip);
6387
+ const agentStr = agentCount != null ? String(agentCount) : "\u2014";
6388
+ const statusStr = formatStatus(peer.status);
6389
+ console.log(` ${peer.ip.padEnd(18)} ${statusStr.padEnd(16)} ${agentStr} agents`);
6390
+ }
6391
+ }
6392
+ function formatStatus(status) {
6393
+ const map = {
6394
+ PARTY_SERVER: "Online",
6395
+ DEGRADED: "Degraded",
6396
+ SUSPECT: "Suspect",
6397
+ DOWN: "Down",
6398
+ UNKNOWN: "Unknown",
6399
+ MAYBE: "Probing",
6400
+ NOT_SERVER: "Not a server"
6401
+ };
6402
+ return map[status] ?? status;
6403
+ }
6404
+
6405
+ // src/cli/install.ts
6406
+ init_tailscale();
6407
+ init_tailscale_installer();
6408
+ async function installCommand() {
6409
+ const status = getTailscaleInstallationStatus();
6410
+ if (status.state === "connected" || status.state === "not_connected") {
6411
+ console.log(green("\u2705 Tailscale is already installed!"), `Binary: ${status.binary}`);
6412
+ if (status.state === "connected") {
6413
+ console.log(` IP: ${status.tailscale_ip} Hostname: ${status.hostname}`);
6414
+ }
6415
+ return;
6416
+ }
6417
+ const platform = process.platform;
6418
+ const info = getInstallInstructions(platform);
6419
+ console.log(bold(cyan("Installing Tailscale...\n")));
6420
+ console.log(` Platform: ${info.os}`);
6421
+ const result = await installTailscale(platform);
6422
+ if (result.success) {
6423
+ console.log(green("\n\u2705 Tailscale installed successfully!"));
6424
+ console.log(" Run: open-party login");
6425
+ } else {
6426
+ console.log(red("\n\u274C Installation failed:"), result.output);
6427
+ console.log(" Install manually: https://tailscale.com/download");
6428
+ }
6429
+ }
6430
+
5848
6431
  // src/cli/index.ts
5849
6432
  function showHelp() {
5850
6433
  console.log(`Usage: open-party <command> [options]
@@ -5854,28 +6437,54 @@ Commands:
5854
6437
  stop Stop the Party Server
5855
6438
  status Show server status
5856
6439
  setup Interactive setup wizard (Tailscale + agent plugins)
6440
+ login Login to Tailscale network
6441
+ logout Log out of Tailscale network
6442
+ install Install Tailscale
6443
+ agents List connected agents
6444
+ peers List discovered peer nodes
5857
6445
  help Show this help message
5858
6446
 
5859
6447
  Options for 'start':
5860
6448
  -d, --daemon Run in background (daemon mode)
5861
6449
  -p, --port <port> Override port (default: 8000, env: PARTY_PORT)
5862
6450
 
6451
+ Global options:
6452
+ -v, --version Show version number
6453
+
5863
6454
  Examples:
5864
6455
  open-party Start server in foreground
5865
6456
  open-party start Start server in foreground
5866
6457
  open-party start -d Start server in background
5867
6458
  open-party start -d -p 9000 Start server in background on port 9000
5868
6459
  open-party stop Stop the server
5869
- open-party status Check if the server is running`);
6460
+ open-party status Check if the server is running
6461
+ open-party login Login to Tailscale
6462
+ open-party logout Log out of Tailscale
6463
+ open-party install Install Tailscale
6464
+ open-party agents List connected agents
6465
+ open-party peers List discovered peer nodes`);
5870
6466
  }
5871
6467
  var args = process.argv.slice(2);
5872
6468
  var command = args[0] ?? "start";
5873
6469
  var commandArgs = args.slice(1);
5874
6470
  async function main2() {
6471
+ if (command === "--version" || command === "-v") {
6472
+ showVersion();
6473
+ process.exit(0);
6474
+ }
5875
6475
  switch (command) {
5876
6476
  case "setup":
5877
6477
  await setupCommand();
5878
6478
  break;
6479
+ case "login":
6480
+ await loginCommand();
6481
+ break;
6482
+ case "logout":
6483
+ await logoutCommand();
6484
+ break;
6485
+ case "install":
6486
+ await installCommand();
6487
+ break;
5879
6488
  case "start":
5880
6489
  await startServer(commandArgs);
5881
6490
  break;
@@ -5885,6 +6494,12 @@ async function main2() {
5885
6494
  case "status":
5886
6495
  await statusCommand();
5887
6496
  break;
6497
+ case "agents":
6498
+ await agentsCommand();
6499
+ break;
6500
+ case "peers":
6501
+ await peersCommand();
6502
+ break;
5888
6503
  case "help":
5889
6504
  case "--help":
5890
6505
  case "-h":