@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.
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,27 +135,11 @@ 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 {
203
141
  const output = execWithSudoFallback(
204
- [binary, "up", "--authkey", authKey, "--accept-routes"],
142
+ [binary, "up", "--authkey", authKey],
205
143
  timeout
206
144
  );
207
145
  return { success: true, output: output.trim() };
@@ -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((resolve2) => setTimeout(resolve2));
3190
+ await new Promise((resolve4) => setTimeout(resolve4));
3175
3191
  maxReadCount = 3;
3176
3192
  continue;
3177
3193
  }
@@ -3410,8 +3426,8 @@ function classifyFetchError(error) {
3410
3426
  if (error instanceof DOMException && error.name === "AbortError") return null;
3411
3427
  return null;
3412
3428
  }
3413
- function sleep(ms) {
3414
- return new Promise((resolve2) => setTimeout(resolve2, ms));
3429
+ function sleep2(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({
@@ -3487,7 +3503,7 @@ var init_peer_discovery = __esm({
3487
3503
  } catch (e) {
3488
3504
  console.error("[Discovery] Cycle failed:", e);
3489
3505
  }
3490
- await sleep(DISCOVERY_INTERVAL * 1e3);
3506
+ await sleep2(DISCOVERY_INTERVAL * 1e3);
3491
3507
  }
3492
3508
  }
3493
3509
  async discoveryCycle() {
@@ -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,14 +5161,56 @@ 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
 
4941
5198
  // src/server/index.ts
4942
5199
  var server_exports = {};
5200
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
5201
+ import { join as join5, dirname as dirname3 } from "path";
5202
+ import { homedir as homedir4 } from "os";
4943
5203
  async function periodicCleanup() {
4944
5204
  }
5205
+ function pidFilePath2() {
5206
+ const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
5207
+ if (pluginData) return join5(pluginData, "server.pid");
5208
+ return join5(homedir4(), ".open-party", "server.pid");
5209
+ }
4945
5210
  async function main() {
5211
+ const pidPath = pidFilePath2();
5212
+ mkdirSync3(dirname3(pidPath), { recursive: true });
5213
+ writeFileSync3(pidPath, String(process.pid));
4946
5214
  console.log(`Starting Party Server on port ${PARTY_PORT} (Tailscale IP: ${getSelfIp()})`);
4947
5215
  process.on("SIGHUP", () => {
4948
5216
  });
@@ -4951,6 +5219,10 @@ async function main() {
4951
5219
  const cleanupPromise = periodicCleanup();
4952
5220
  const shutdown = () => {
4953
5221
  console.log("\nShutting down Party Server...");
5222
+ try {
5223
+ unlinkSync2(pidPath);
5224
+ } catch {
5225
+ }
4954
5226
  server.close();
4955
5227
  process.exit(0);
4956
5228
  };
@@ -4990,49 +5262,7 @@ var init_server = __esm({
4990
5262
 
4991
5263
  // src/cli/setup.ts
4992
5264
  init_tailscale();
4993
- import { createInterface } from "readline";
4994
-
4995
- // src/cli/tailscale-installer.ts
4996
- import { spawn } from "child_process";
4997
- var INSTALL_COMMANDS = {
4998
- linux: { cmd: "bash", args: ["-c", "curl -fsSL https://tailscale.com/install.sh | sh"], needsSudo: true },
4999
- darwin: { cmd: "brew", args: ["install", "tailscale"], needsSudo: false },
5000
- win32: { cmd: "winget", args: ["install", "Tailscale.Tailscale", "--accept-source-agreements"], needsSudo: false }
5001
- };
5002
- async function installTailscale(platform) {
5003
- const entry = INSTALL_COMMANDS[platform];
5004
- if (!entry) {
5005
- return {
5006
- success: false,
5007
- output: `Unsupported platform: ${platform}. Please install manually from https://tailscale.com/download`
5008
- };
5009
- }
5010
- const cmd = entry.needsSudo ? "sudo" : entry.cmd;
5011
- const args2 = entry.needsSudo ? [entry.cmd, ...entry.args] : entry.args;
5012
- console.log(`Running: ${cmd} ${args2.join(" ")}
5013
- `);
5014
- return new Promise((resolve2) => {
5015
- const child = spawn(cmd, args2, {
5016
- stdio: "inherit",
5017
- windowsHide: true
5018
- });
5019
- let exited = false;
5020
- child.on("close", (code) => {
5021
- if (exited) return;
5022
- exited = true;
5023
- if (code === 0) {
5024
- resolve2({ success: true, output: "Installation completed." });
5025
- } else {
5026
- resolve2({ success: false, output: `Installation exited with code ${code}` });
5027
- }
5028
- });
5029
- child.on("error", (err) => {
5030
- if (exited) return;
5031
- exited = true;
5032
- resolve2({ success: false, output: err.message });
5033
- });
5034
- });
5035
- }
5265
+ init_tailscale_installer();
5036
5266
 
5037
5267
  // src/cli/agent-detector.ts
5038
5268
  import { existsSync as existsSync2 } from "fs";
@@ -5317,11 +5547,12 @@ async function installPluginToAgent(agentType) {
5317
5547
  }
5318
5548
  }
5319
5549
 
5320
- // src/cli/setup.ts
5321
- var rl = createInterface({ input: process.stdin, output: process.stdout });
5322
- function prompt(question) {
5323
- return new Promise((resolve2) => rl.question(question, (answer) => resolve2(answer.trim())));
5324
- }
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";
5325
5556
  function cyan(text) {
5326
5557
  return `\x1B[36m${text}\x1B[0m`;
5327
5558
  }
@@ -5337,96 +5568,163 @@ function red(text) {
5337
5568
  function bold(text) {
5338
5569
  return `\x1B[1m${text}\x1B[0m`;
5339
5570
  }
5340
- async function stepTailscale() {
5341
- console.log(`
5342
- ${bold(cyan("\u{1F50D} Step 1: Detecting Tailscale..."))}
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}
5343
5615
  `);
5344
- const status = getTailscaleInstallationStatus();
5345
- if (status.state === "connected") {
5346
- console.log(`${green("\u2705 Tailscale is connected!")}`);
5347
- console.log(` IP: ${status.tailscale_ip} Hostname: ${status.hostname}`);
5348
- return;
5349
- }
5350
- if (status.state === "not_installed") {
5351
- console.log(`${red("\u274C Tailscale is not installed.")}`);
5352
- await handleNotInstalled(status.platform);
5353
- const newStatus = getTailscaleInstallationStatus();
5354
- if (newStatus.state === "not_installed") {
5355
- console.log(`
5356
- ${yellow("\u26A0\uFE0F Tailscale still not detected. Please install manually and re-run setup.")}`);
5357
- return;
5358
5616
  }
5359
- if (newStatus.state === "connected") {
5360
- console.log(`
5361
- ${green("\u2705 Tailscale is connected!")} IP: ${newStatus.tailscale_ip}`);
5362
- return;
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
+ `);
5363
5624
  }
5364
- await handleNotConnected(newStatus.binary);
5365
- return;
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);
5366
5646
  }
5367
- await handleNotConnected(status.binary);
5368
5647
  }
5369
- async function handleNotInstalled(platform) {
5370
- const info = getInstallInstructions(platform);
5371
- console.log(`
5372
- Install Tailscale for ${bold(info.os)}:
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}
5373
5661
  `);
5374
- if (info.commands.length > 0) {
5375
- for (const cmd of info.commands) {
5376
- const prefix = info.needs_sudo ? "sudo " : "";
5377
- console.log(` ${cyan(prefix + cmd)}`);
5378
- }
5379
- }
5380
- console.log(`
5381
- Download: ${info.download_url}`);
5382
- const autoInstall = info.commands.length > 0 && platform !== "win32";
5383
- if (autoInstall) {
5384
- const answer = await prompt(`
5385
- ${bold("Install Tailscale automatically?")} [yes/no]: `);
5386
- if (answer.toLowerCase() === "yes" || answer.toLowerCase() === "y") {
5387
- console.log("");
5388
- const result = await installTailscale(platform);
5389
- if (result.success) {
5390
- console.log(`${green("\u2705 Tailscale installed successfully!")}`);
5391
- } else {
5392
- console.log(`${red("\u274C Installation failed:")}
5393
- ${result.output}`);
5394
- console.log(`
5395
- Please install manually and re-run \x1B[36mnpx open-party setup\x1B[0m`);
5396
5662
  }
5397
- return;
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
+ `);
5398
5677
  }
5399
- }
5400
- console.log(`
5401
- Or run: ${cyan("npx open-party setup")} (after installing manually)`);
5402
- if (autoInstall) {
5403
- const cont = await prompt('Press Enter when installation is complete, or type "skip" to skip...');
5404
- if (cont.toLowerCase() === "skip") return;
5405
- resetTailscaleBinaryCache();
5406
- }
5407
- }
5408
- async function handleNotConnected(binary) {
5409
- console.log(`${yellow("\u{1F512} Tailscale is installed but not connected.")}`);
5410
- console.log("");
5411
- console.log("Choose a login method:\n");
5412
- console.log(" 1. Interactive login (opens browser to authenticate)");
5413
- console.log(" 2. Auth key (enter from Tailscale admin console)");
5414
- console.log("");
5415
- const choice = await prompt("Select [1/2]: ");
5416
- if (choice === "1") {
5417
- await handleInteractiveLogin(binary);
5418
- } else {
5419
- await handleAuthKeyLogin(binary);
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);
5420
5717
  }
5421
5718
  }
5422
- async function handleInteractiveLogin(binary) {
5719
+
5720
+ // src/cli/tailscale-login.ts
5721
+ async function interactiveLogin(binary) {
5423
5722
  console.log(`
5424
5723
  ${cyan("Running interactive login...")}`);
5425
5724
  console.log("A browser window should open. Authenticate in the browser, then return here.\n");
5426
- const { spawn: spawn2 } = await import("child_process");
5427
5725
  const child = spawn2(binary, ["login"], { stdio: "inherit" });
5428
- const exitCode = await new Promise((resolve2) => {
5429
- child.on("close", resolve2);
5726
+ const exitCode = await new Promise((resolve4) => {
5727
+ child.on("close", resolve4);
5430
5728
  });
5431
5729
  resetTailscaleBinaryCache();
5432
5730
  const status = getTailscaleInstallationStatus();
@@ -5434,43 +5732,149 @@ ${cyan("Running interactive login...")}`);
5434
5732
  console.log(`
5435
5733
  ${green("\u2705 Login successful!")} IP: ${status.tailscale_ip}`);
5436
5734
  showAuthKeyTip();
5437
- } else {
5438
- console.log(`
5439
- ${yellow("\u26A0\uFE0F Login may not have completed. Status: " + status.state)}`);
5440
- console.log(" Try running: npx open-party setup");
5735
+ return true;
5441
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;
5442
5741
  }
5443
- async function handleAuthKeyLogin(binary) {
5742
+ async function authKeyLogin(binary) {
5444
5743
  console.log("");
5445
- console.log("You can generate an Auth Key at:");
5744
+ console.log("Ask the network creator to generate an Auth Key at:");
5446
5745
  console.log(`${cyan(" https://login.tailscale.com/admin/settings/keys")}
5447
5746
  `);
5448
5747
  const authKey = await prompt("Enter Auth Key: ");
5449
5748
  if (!authKey) {
5450
5749
  console.log(yellow("No auth key provided, skipping login."));
5451
- return;
5750
+ return false;
5452
5751
  }
5453
- const { joinTailnet: joinTailnet2 } = await Promise.resolve().then(() => (init_tailscale(), tailscale_exports));
5454
- const result = joinTailnet2(authKey);
5752
+ const result = joinTailnet(authKey);
5455
5753
  if (result.success) {
5456
5754
  resetTailscaleBinaryCache();
5457
5755
  const status = getTailscaleInstallationStatus();
5458
5756
  console.log(`
5459
5757
  ${green("\u2705 Login successful!")} IP: ${status.state === "connected" ? status.tailscale_ip : "unknown"}`);
5460
5758
  showAuthKeyTip();
5461
- } else {
5462
- console.log(`
5759
+ return true;
5760
+ }
5761
+ console.log(`
5463
5762
  ${red("\u274C Login failed:")}
5464
5763
  ${result.output}`);
5465
- console.log(" Check your auth key and try again.");
5466
- }
5764
+ console.log(" Check your auth key and try again.");
5765
+ return false;
5467
5766
  }
5468
5767
  function showAuthKeyTip() {
5469
5768
  console.log("");
5470
5769
  console.log(`${bold("\u{1F4A1} To share network access with teammates:")}`);
5471
5770
  console.log(" 1. Go to https://login.tailscale.com/admin/settings/keys");
5472
5771
  console.log(" 2. Generate an Auth Key");
5473
- console.log(" 3. Share it with teammates \u2014 they can run: npx open-party setup");
5772
+ console.log(" 3. Share it with teammates \u2014 they can run: open-party login");
5773
+ }
5774
+
5775
+ // src/cli/setup.ts
5776
+ async function stepTailscale() {
5777
+ console.log(`
5778
+ ${bold(cyan("\u{1F50D} Step 1: Tailscale Network"))}
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
+ );
5787
+ const status = getTailscaleInstallationStatus();
5788
+ if (status.state === "connected") {
5789
+ console.log(`${green("\u2705 Tailscale is connected!")}`);
5790
+ console.log(` IP: ${status.tailscale_ip} Hostname: ${status.hostname}`);
5791
+ return;
5792
+ }
5793
+ if (status.state === "not_installed") {
5794
+ await handleNotInstalled(status.platform);
5795
+ const newStatus = getTailscaleInstallationStatus();
5796
+ if (newStatus.state === "not_installed") {
5797
+ showLocalModeNotice();
5798
+ return;
5799
+ }
5800
+ if (newStatus.state === "connected") {
5801
+ console.log(`
5802
+ ${green("\u2705 Tailscale is connected!")} IP: ${newStatus.tailscale_ip}`);
5803
+ return;
5804
+ }
5805
+ await handleNotConnected(newStatus.binary);
5806
+ return;
5807
+ }
5808
+ await handleNotConnected(status.binary);
5809
+ }
5810
+ async function handleNotInstalled(platform) {
5811
+ const info = getInstallInstructions(platform);
5812
+ console.log(`${red("\u274C Tailscale is not installed.")}`);
5813
+ console.log(`
5814
+ Install Tailscale for ${bold(info.os)}:
5815
+ `);
5816
+ if (info.commands.length > 0) {
5817
+ for (const cmd of info.commands) {
5818
+ const prefix = info.needs_sudo ? "sudo " : "";
5819
+ console.log(` ${cyan(prefix + cmd)}`);
5820
+ }
5821
+ }
5822
+ console.log(`
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:")}
5842
+ ${result.output}`);
5843
+ console.log(`
5844
+ Please install manually and re-run: ${cyan("open-party setup")}`);
5845
+ }
5846
+ return;
5847
+ }
5848
+ if (choice === "redetect") {
5849
+ resetTailscaleBinaryCache();
5850
+ return;
5851
+ }
5852
+ }
5853
+ async function handleNotConnected(binary) {
5854
+ console.log(`${yellow("\u{1F512} Tailscale is installed but not connected.")}
5855
+ `);
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);
5866
+ } else {
5867
+ console.log(`
5868
+ ${yellow("\u26A0\uFE0F Tailscale not connected. Running in local mode.")}`);
5869
+ console.log(` To connect later, run: ${cyan("open-party login")}`);
5870
+ }
5871
+ }
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")}`);
5474
5878
  }
5475
5879
  async function stepAgentPlugin() {
5476
5880
  console.log(`
@@ -5482,7 +5886,7 @@ ${bold(cyan("\u{1F50D} Step 2: Detecting AI agents in your environment..."))}
5482
5886
  console.log(yellow("No supported AI agents detected in this environment."));
5483
5887
  console.log(" Supported agents: Claude Code, Cursor, Gemini CLI");
5484
5888
  console.log("");
5485
- console.log(" Install one and re-run: npx open-party setup");
5889
+ console.log(" Install one and re-run: open-party setup");
5486
5890
  return;
5487
5891
  }
5488
5892
  console.log("Detected agents:\n");
@@ -5490,7 +5894,10 @@ ${bold(cyan("\u{1F50D} Step 2: Detecting AI agents in your environment..."))}
5490
5894
  console.log(` ${green("\u2713")} ${agent.name}`);
5491
5895
  }
5492
5896
  console.log("");
5493
- 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
+ });
5494
5901
  if (selected.length === 0) {
5495
5902
  console.log(yellow("No agents selected, skipping plugin installation."));
5496
5903
  return;
@@ -5512,32 +5919,19 @@ Installing Open Party plugin for ${agent.name}...`);
5512
5919
  }
5513
5920
  }
5514
5921
  }
5515
- async function selectAgents(agents) {
5516
- console.log("Select agents to install Open Party plugin:\n");
5517
- for (let i = 0; i < agents.length; i++) {
5518
- console.log(` [${i + 1}] ${agents[i].name}`);
5519
- }
5520
- console.log(` [a] All`);
5521
- console.log(` [n] None (skip)`);
5522
- console.log("");
5523
- const answer = await prompt('Enter selection (e.g. "1 2" or "a" or "n"): ');
5524
- if (answer.toLowerCase() === "n" || answer === "") return [];
5525
- if (answer.toLowerCase() === "a") return agents;
5526
- const indices = answer.split(/[\s,]+/).map((s) => parseInt(s, 10) - 1).filter((i) => i >= 0 && i < agents.length);
5527
- return indices.map((i) => agents[i]);
5528
- }
5529
5922
  async function setupCommand() {
5530
5923
  console.log(bold(cyan("\n\u{1F680} Open Party Setup Wizard\n")));
5924
+ const rl = createRl();
5531
5925
  await stepTailscale();
5532
5926
  await stepAgentPlugin();
5533
5927
  console.log(`
5534
5928
  ${bold(cyan("\u{1F680} Starting Party Server..."))}`);
5535
- const { spawn: spawn2 } = await import("child_process");
5536
- const { resolve: resolve2, dirname: dirname2 } = await import("path");
5537
- const { fileURLToPath } = await import("url");
5538
- const __dirname = dirname2(fileURLToPath(import.meta.url));
5539
- const serverScript = resolve2(__dirname, "..", "party-server.js");
5540
- const serverProc = spawn2(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], {
5541
5935
  detached: true,
5542
5936
  stdio: "ignore",
5543
5937
  windowsHide: true
@@ -5548,31 +5942,573 @@ ${bold(cyan("\u{1F680} Starting Party Server..."))}`);
5548
5942
  ${bold(green("\u{1F389} Setup complete!"))}`);
5549
5943
  console.log(` Dashboard: http://127.0.0.1:8000/dashboard`);
5550
5944
  console.log(" Other agents can join with: npx @feynmanzhang/open-party setup\n");
5551
- 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
+ }
6005
+ }
6006
+
6007
+ // src/cli/server-utils.ts
6008
+ import { spawn as spawn3, execSync as execSync3 } from "child_process";
6009
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2, openSync } from "fs";
6010
+ import { join as join4, dirname as dirname2, resolve as resolve2 } from "path";
6011
+ import { homedir as homedir3 } from "os";
6012
+ import { fileURLToPath } from "url";
6013
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
6014
+ function pidFilePath() {
6015
+ const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
6016
+ if (pluginData) return join4(pluginData, "server.pid");
6017
+ return join4(homedir3(), ".open-party", "server.pid");
6018
+ }
6019
+ function logFilePath() {
6020
+ const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
6021
+ if (pluginData) return join4(pluginData, "server.log");
6022
+ return join4(homedir3(), ".open-party", "server.log");
6023
+ }
6024
+ function serverScriptPath() {
6025
+ return resolve2(__dirname, "..", "party-server.js");
6026
+ }
6027
+ function readPid() {
6028
+ const path = pidFilePath();
6029
+ if (!existsSync4(path)) return null;
6030
+ try {
6031
+ return parseInt(readFileSync2(path, "utf-8").trim(), 10);
6032
+ } catch {
6033
+ return null;
6034
+ }
6035
+ }
6036
+ function writePid(pid) {
6037
+ const path = pidFilePath();
6038
+ const dir = dirname2(path);
6039
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
6040
+ writeFileSync2(path, String(pid));
6041
+ }
6042
+ function removePidFile() {
6043
+ try {
6044
+ unlinkSync(pidFilePath());
6045
+ } catch {
6046
+ }
6047
+ }
6048
+ function isProcessRunning(pid) {
6049
+ if (process.platform === "win32") {
6050
+ try {
6051
+ const output = execSync3(`tasklist /FI "PID eq ${pid}" /NH`, {
6052
+ encoding: "utf-8",
6053
+ windowsHide: true,
6054
+ stdio: ["pipe", "pipe", "pipe"]
6055
+ });
6056
+ return output.includes(String(pid));
6057
+ } catch {
6058
+ return false;
6059
+ }
6060
+ }
6061
+ try {
6062
+ process.kill(pid, 0);
6063
+ return true;
6064
+ } catch {
6065
+ return false;
6066
+ }
6067
+ }
6068
+ function resolvePort() {
6069
+ return parseInt(process.env.PARTY_PORT || "8000", 10);
6070
+ }
6071
+ async function fetchJson(url, timeoutMs = 2e3) {
6072
+ try {
6073
+ const controller = new AbortController();
6074
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
6075
+ const resp = await fetch(url, { signal: controller.signal });
6076
+ clearTimeout(timer);
6077
+ if (!resp.ok) return null;
6078
+ return await resp.json();
6079
+ } catch {
6080
+ return null;
6081
+ }
6082
+ }
6083
+ async function isServerHealthy(port) {
6084
+ const p = port ?? resolvePort();
6085
+ const data = await fetchJson(`http://127.0.0.1:${p}/proxy/health`);
6086
+ return data !== null && data.status === "ok";
6087
+ }
6088
+ async function getServerHealth(port) {
6089
+ const p = port ?? resolvePort();
6090
+ return fetchJson(`http://127.0.0.1:${p}/proxy/health`);
6091
+ }
6092
+ async function getServerOverview(port) {
6093
+ const p = port ?? resolvePort();
6094
+ return fetchJson(`http://127.0.0.1:${p}/dashboard/api/overview`, 3e3);
6095
+ }
6096
+ async function spawnServerInBackground(port) {
6097
+ const script = serverScriptPath();
6098
+ if (!existsSync4(script)) {
6099
+ console.error(`Server script not found: ${script}`);
6100
+ return { pid: 0, ok: false };
6101
+ }
6102
+ const logPath = logFilePath();
6103
+ mkdirSync2(dirname2(logPath), { recursive: true });
6104
+ const logFd = openSync(logPath, "a");
6105
+ const env = { ...process.env, PARTY_PORT: String(port) };
6106
+ const proc = spawn3(process.execPath, [script], {
6107
+ stdio: ["ignore", logFd, logFd],
6108
+ detached: true,
6109
+ windowsHide: true,
6110
+ env
6111
+ });
6112
+ proc.unref();
6113
+ const pid = proc.pid;
6114
+ writePid(pid);
6115
+ proc.on("error", (err) => {
6116
+ console.error(`Failed to start server: ${err.message}`);
6117
+ });
6118
+ return { pid, ok: true };
6119
+ }
6120
+ async function waitForServerReady(port, timeoutMs = 1e4) {
6121
+ const deadline = Date.now() + timeoutMs;
6122
+ while (Date.now() < deadline) {
6123
+ if (await isServerHealthy(port)) return true;
6124
+ const pid = readPid();
6125
+ if (pid !== null && !isProcessRunning(pid)) {
6126
+ return false;
6127
+ }
6128
+ await sleep(500);
6129
+ }
6130
+ return false;
6131
+ }
6132
+ function killServer(pid) {
6133
+ try {
6134
+ if (process.platform === "win32") {
6135
+ execSync3(`taskkill /F /T /PID ${pid}`, { stdio: "ignore", windowsHide: true });
6136
+ } else {
6137
+ process.kill(pid, "SIGTERM");
6138
+ }
6139
+ } catch {
6140
+ }
6141
+ }
6142
+ function parseStartArgs(args2) {
6143
+ let daemon = false;
6144
+ let port = null;
6145
+ for (let i = 0; i < args2.length; i++) {
6146
+ if (args2[i] === "-d" || args2[i] === "--daemon") {
6147
+ daemon = true;
6148
+ } else if (args2[i] === "-p" || args2[i] === "--port") {
6149
+ const val = args2[++i];
6150
+ if (val) port = parseInt(val, 10);
6151
+ } else if (args2[i].startsWith("--port=")) {
6152
+ port = parseInt(args2[i].split("=")[1], 10);
6153
+ }
6154
+ }
6155
+ return { daemon, port };
6156
+ }
6157
+ function sleep(ms) {
6158
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
5552
6159
  }
5553
6160
 
5554
6161
  // src/cli/start-server.ts
5555
- async function startServer() {
6162
+ async function startServer(args2 = []) {
6163
+ const opts = parseStartArgs(args2);
6164
+ const port = opts.port ?? resolvePort();
6165
+ if (opts.daemon) {
6166
+ await startDaemon(port);
6167
+ } else {
6168
+ await startForeground();
6169
+ }
6170
+ }
6171
+ async function startForeground() {
6172
+ writePid(process.pid);
6173
+ process.on("exit", () => {
6174
+ removePidFile();
6175
+ });
5556
6176
  await Promise.resolve().then(() => (init_server(), server_exports));
5557
6177
  }
6178
+ async function startDaemon(port) {
6179
+ if (await isServerHealthy(port)) {
6180
+ const pid2 = readPid();
6181
+ console.log(`Party Server is already running (PID ${pid2 ?? "unknown"}, port ${port}).`);
6182
+ process.exit(0);
6183
+ }
6184
+ const existingPid = readPid();
6185
+ if (existingPid !== null && !isProcessRunning(existingPid)) {
6186
+ removePidFile();
6187
+ }
6188
+ const { pid, ok } = await spawnServerInBackground(port);
6189
+ if (!ok) {
6190
+ process.exit(1);
6191
+ }
6192
+ console.log(`Starting Party Server in background (PID ${pid})...`);
6193
+ const ready = await waitForServerReady(port);
6194
+ if (ready) {
6195
+ console.log(`Party Server is running on port ${port}.`);
6196
+ console.log(` Dashboard: http://127.0.0.1:${port}/dashboard`);
6197
+ console.log(` Logs: ${logFilePath()}`);
6198
+ console.log(` Use 'open-party stop' to stop the server.`);
6199
+ } else {
6200
+ console.error("Party Server failed to start within timeout.");
6201
+ console.error(`Check logs: ${logFilePath()}`);
6202
+ process.exit(1);
6203
+ }
6204
+ }
6205
+
6206
+ // src/cli/stop-server.ts
6207
+ async function stopServer() {
6208
+ const pid = readPid();
6209
+ if (pid === null) {
6210
+ const port2 = resolvePort();
6211
+ const healthy = await isServerHealthy(port2);
6212
+ if (healthy) {
6213
+ console.log(`No PID file found, but a server is responding on port ${port2}.`);
6214
+ console.log("It may have been started manually. Kill it by port or process name.");
6215
+ } else {
6216
+ console.log("Party Server is not running (no PID file found).");
6217
+ }
6218
+ return;
6219
+ }
6220
+ if (!isProcessRunning(pid)) {
6221
+ console.log(`Stale PID file found (PID ${pid} is not running). Cleaning up.`);
6222
+ removePidFile();
6223
+ return;
6224
+ }
6225
+ console.log(`Stopping Party Server (PID ${pid})...`);
6226
+ killServer(pid);
6227
+ removePidFile();
6228
+ const port = resolvePort();
6229
+ const stillUp = await isServerHealthy(port);
6230
+ if (stillUp) {
6231
+ console.warn(`Process ${pid} was killed, but port ${port} is still responding.`);
6232
+ console.warn("Another process may be using this port.");
6233
+ } else {
6234
+ console.log("Party Server stopped.");
6235
+ }
6236
+ }
6237
+
6238
+ // src/cli/status.ts
6239
+ async function statusCommand() {
6240
+ const port = resolvePort();
6241
+ const pid = readPid();
6242
+ let processAlive = false;
6243
+ if (pid !== null) {
6244
+ processAlive = isProcessRunning(pid);
6245
+ }
6246
+ const healthy = await isServerHealthy(port);
6247
+ if (healthy) {
6248
+ const health = await getServerHealth(port);
6249
+ const overview = await getServerOverview(port);
6250
+ console.log("Party Server is running.");
6251
+ console.log(` PID: ${pid ?? "unknown (no PID file)"}`);
6252
+ console.log(` Port: ${port}`);
6253
+ console.log(` Tailscale IP: ${health?.tailscale_ip ?? "N/A"}`);
6254
+ console.log(` Hostname: ${health?.hostname ?? "N/A"}`);
6255
+ if (overview) {
6256
+ const server = overview.server;
6257
+ const agents = overview.agents;
6258
+ if (server?.uptime_seconds != null) {
6259
+ const uptime = server.uptime_seconds;
6260
+ const mins = Math.floor(uptime / 60);
6261
+ const secs = Math.floor(uptime % 60);
6262
+ console.log(` Uptime: ${mins}m ${secs}s`);
6263
+ }
6264
+ console.log(` Local agents: ${agents?.local_count ?? "N/A"}`);
6265
+ console.log(` Remote agents: ${agents?.remote_count ?? "N/A"}`);
6266
+ } else {
6267
+ console.log(` Local agents: ${health?.agent_count ?? "N/A"}`);
6268
+ }
6269
+ console.log(` Dashboard: http://127.0.0.1:${port}/dashboard`);
6270
+ } else if (processAlive && pid !== null) {
6271
+ console.log("Party Server process exists but is not responding on health endpoint.");
6272
+ console.log(` PID: ${pid}`);
6273
+ console.log(" The server may be starting up or has crashed.");
6274
+ console.log(` Logs: ~/.open-party/server.log`);
6275
+ } else if (pid !== null) {
6276
+ console.log("Party Server is NOT running (stale PID file).");
6277
+ console.log(` PID file references PID ${pid}, which is not a live process.`);
6278
+ console.log(" Use: open-party start to start the server.");
6279
+ } else {
6280
+ console.log("Party Server is NOT running.");
6281
+ console.log(" No PID file found.");
6282
+ console.log(" Use: open-party start to start the server.");
6283
+ }
6284
+ }
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
+ }
5558
6430
 
5559
6431
  // src/cli/index.ts
6432
+ function showHelp() {
6433
+ console.log(`Usage: open-party <command> [options]
6434
+
6435
+ Commands:
6436
+ start Start the Party Server (default when no command given)
6437
+ stop Stop the Party Server
6438
+ status Show server status
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
6445
+ help Show this help message
6446
+
6447
+ Options for 'start':
6448
+ -d, --daemon Run in background (daemon mode)
6449
+ -p, --port <port> Override port (default: 8000, env: PARTY_PORT)
6450
+
6451
+ Global options:
6452
+ -v, --version Show version number
6453
+
6454
+ Examples:
6455
+ open-party Start server in foreground
6456
+ open-party start Start server in foreground
6457
+ open-party start -d Start server in background
6458
+ open-party start -d -p 9000 Start server in background on port 9000
6459
+ open-party stop Stop the server
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`);
6466
+ }
5560
6467
  var args = process.argv.slice(2);
5561
6468
  var command = args[0] ?? "start";
6469
+ var commandArgs = args.slice(1);
5562
6470
  async function main2() {
6471
+ if (command === "--version" || command === "-v") {
6472
+ showVersion();
6473
+ process.exit(0);
6474
+ }
5563
6475
  switch (command) {
5564
6476
  case "setup":
5565
6477
  await setupCommand();
5566
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;
5567
6488
  case "start":
5568
- await startServer();
6489
+ await startServer(commandArgs);
6490
+ break;
6491
+ case "stop":
6492
+ await stopServer();
6493
+ break;
6494
+ case "status":
6495
+ await statusCommand();
6496
+ break;
6497
+ case "agents":
6498
+ await agentsCommand();
6499
+ break;
6500
+ case "peers":
6501
+ await peersCommand();
6502
+ break;
6503
+ case "help":
6504
+ case "--help":
6505
+ case "-h":
6506
+ showHelp();
5569
6507
  break;
5570
6508
  default:
5571
- console.log(`Usage: npx open-party [setup|start]`);
5572
- console.log("");
5573
- console.log("Commands:");
5574
- console.log(" setup Interactive setup wizard (Tailscale + agent plugins)");
5575
- console.log(" start Start the Party Server (default)");
6509
+ console.log(`Unknown command: ${command}
6510
+ `);
6511
+ showHelp();
5576
6512
  process.exit(1);
5577
6513
  }
5578
6514
  }