@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 +1227 -291
- package/dist/cli/index.js.map +1 -1
- package/dist/party-server.js +389 -24
- package/dist/party-server.js.map +1 -1
- package/package.json +1 -1
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
|
|
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,
|
|
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((
|
|
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
|
|
3414
|
-
return new Promise((
|
|
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
|
|
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-
|
|
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">
|
|
4358
|
-
<div class="
|
|
4359
|
-
|
|
4360
|
-
|
|
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="
|
|
4363
|
-
<button class="modal-btn modal-btn-submit" id="
|
|
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
|
|
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',
|
|
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 = '
|
|
4715
|
-
btnJoin.
|
|
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 = '
|
|
4748
|
-
btnJoin.
|
|
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.
|
|
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
|
|
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">
|
|
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
|
-
|
|
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/
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
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
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
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
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
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
|
-
|
|
5365
|
-
|
|
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
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
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
|
-
|
|
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((
|
|
5429
|
-
child.on("close",
|
|
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
|
-
|
|
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
|
|
5742
|
+
async function authKeyLogin(binary) {
|
|
5444
5743
|
console.log("");
|
|
5445
|
-
console.log("
|
|
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
|
|
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
|
-
|
|
5462
|
-
|
|
5759
|
+
return true;
|
|
5760
|
+
}
|
|
5761
|
+
console.log(`
|
|
5463
5762
|
${red("\u274C Login failed:")}
|
|
5464
5763
|
${result.output}`);
|
|
5465
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
5536
|
-
const { resolve:
|
|
5537
|
-
const { fileURLToPath } = await import("url");
|
|
5538
|
-
const
|
|
5539
|
-
const serverScript =
|
|
5540
|
-
const serverProc =
|
|
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
|
|
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(`
|
|
5572
|
-
|
|
5573
|
-
|
|
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
|
}
|