@feynmanzhang/open-party 0.1.3-beta.0 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude-code/open-party-0.1.1/BUILD_INFO.json +6 -0
- package/dist/claude-code/open-party-0.1.1/dist/hook-handler.js +555 -0
- package/dist/claude-code/open-party-0.1.1/dist/mcp-server.js +21408 -0
- package/dist/claude-code/open-party-0.1.1/dist/party-server.js +5017 -0
- package/dist/claude-code/open-party-0.1.1/hooks/hooks.json +50 -0
- package/dist/claude-code/open-party-0.1.1/package.json +6 -0
- package/dist/claude-code/open-party-0.1.1/skills/open-party/SKILL.md +71 -0
- package/dist/cli/index.js +895 -280
- package/dist/cli/index.js.map +1 -1
- package/dist/party-server.js +373 -23
- package/dist/party-server.js.map +1 -1
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -11,27 +11,10 @@ var __export = (target, all) => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
// src/infra/tailscale.ts
|
|
14
|
-
var tailscale_exports = {};
|
|
15
|
-
__export(tailscale_exports, {
|
|
16
|
-
WhoisIdentity: () => WhoisIdentity,
|
|
17
|
-
disableFunnel: () => disableFunnel,
|
|
18
|
-
disableServe: () => disableServe,
|
|
19
|
-
enableFunnel: () => enableFunnel,
|
|
20
|
-
enableServe: () => enableServe,
|
|
21
|
-
getInstallInstructions: () => getInstallInstructions,
|
|
22
|
-
getTailnetHostname: () => getTailnetHostname,
|
|
23
|
-
getTailscaleBinary: () => getTailscaleBinary,
|
|
24
|
-
getTailscaleConnectionStatus: () => getTailscaleConnectionStatus,
|
|
25
|
-
getTailscaleInstallationStatus: () => getTailscaleInstallationStatus,
|
|
26
|
-
getTailscaleIps: () => getTailscaleIps,
|
|
27
|
-
joinTailnet: () => joinTailnet,
|
|
28
|
-
readTailscaleStatus: () => readTailscaleStatus,
|
|
29
|
-
readWhoisIdentity: () => readWhoisIdentity,
|
|
30
|
-
resetTailscaleBinaryCache: () => resetTailscaleBinaryCache
|
|
31
|
-
});
|
|
32
14
|
import { execFileSync, execSync } from "child_process";
|
|
33
15
|
import { existsSync } from "fs";
|
|
34
16
|
import { join } from "path";
|
|
17
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
35
18
|
function parsePossiblyNoisyJson(raw2) {
|
|
36
19
|
const trimmed = raw2.trim();
|
|
37
20
|
const start = trimmed.indexOf("{");
|
|
@@ -137,35 +120,6 @@ function getTailscaleIps() {
|
|
|
137
120
|
}
|
|
138
121
|
return [];
|
|
139
122
|
}
|
|
140
|
-
function parseWhoisIdentity(payload) {
|
|
141
|
-
const userProfile = payload.UserProfile ?? payload.userProfile ?? payload.User ?? {};
|
|
142
|
-
const login = userProfile.LoginName || userProfile.Login || userProfile.login || payload.LoginName || payload.login;
|
|
143
|
-
if (typeof login !== "string" || !login.trim()) return null;
|
|
144
|
-
const rawName = userProfile.DisplayName || userProfile.Name || userProfile.displayName || payload.DisplayName || payload.name;
|
|
145
|
-
const name = typeof rawName === "string" ? rawName.trim() : void 0;
|
|
146
|
-
return new WhoisIdentity(login.trim(), name);
|
|
147
|
-
}
|
|
148
|
-
function readWhoisIdentity(ip, timeout = 5e3, cacheTtl = 60, errorTtl = 5) {
|
|
149
|
-
const normalized = ip.trim();
|
|
150
|
-
if (!normalized) return null;
|
|
151
|
-
const now = performance.now() / 1e3;
|
|
152
|
-
const cached = whoisCache.get(normalized);
|
|
153
|
-
if (cached) {
|
|
154
|
-
if (cached.expiresAt > now) return cached.value;
|
|
155
|
-
whoisCache.delete(normalized);
|
|
156
|
-
}
|
|
157
|
-
const binary = getTailscaleBinary();
|
|
158
|
-
let identity = null;
|
|
159
|
-
try {
|
|
160
|
-
const stdout = runExec([binary, "whois", "--json", normalized], timeout);
|
|
161
|
-
const parsed = stdout.trim() ? parsePossiblyNoisyJson(stdout) : {};
|
|
162
|
-
identity = parseWhoisIdentity(parsed);
|
|
163
|
-
} catch {
|
|
164
|
-
}
|
|
165
|
-
const ttl = identity ? cacheTtl : errorTtl;
|
|
166
|
-
whoisCache.set(normalized, { value: identity, expiresAt: now + ttl });
|
|
167
|
-
return identity;
|
|
168
|
-
}
|
|
169
123
|
function execWithSudoFallback(cmd, timeout = 15e3) {
|
|
170
124
|
try {
|
|
171
125
|
return runExec(cmd, timeout);
|
|
@@ -181,22 +135,6 @@ function execWithSudoFallback(cmd, timeout = 15e3) {
|
|
|
181
135
|
throw exc;
|
|
182
136
|
}
|
|
183
137
|
}
|
|
184
|
-
function enableServe(port, timeout = 15e3) {
|
|
185
|
-
const binary = getTailscaleBinary();
|
|
186
|
-
execWithSudoFallback([binary, "serve", "--bg", "--yes", String(port)], timeout);
|
|
187
|
-
}
|
|
188
|
-
function disableServe(timeout = 15e3) {
|
|
189
|
-
const binary = getTailscaleBinary();
|
|
190
|
-
execWithSudoFallback([binary, "serve", "reset"], timeout);
|
|
191
|
-
}
|
|
192
|
-
function enableFunnel(port, timeout = 15e3) {
|
|
193
|
-
const binary = getTailscaleBinary();
|
|
194
|
-
execWithSudoFallback([binary, "funnel", "--bg", "--yes", String(port)], timeout);
|
|
195
|
-
}
|
|
196
|
-
function disableFunnel(timeout = 15e3) {
|
|
197
|
-
const binary = getTailscaleBinary();
|
|
198
|
-
execWithSudoFallback([binary, "funnel", "reset"], timeout);
|
|
199
|
-
}
|
|
200
138
|
function joinTailnet(authKey, timeout = 3e4) {
|
|
201
139
|
const binary = getTailscaleBinary();
|
|
202
140
|
try {
|
|
@@ -210,27 +148,6 @@ function joinTailnet(authKey, timeout = 3e4) {
|
|
|
210
148
|
return { success: false, output: (err.stderr ?? err.message ?? "").trim() };
|
|
211
149
|
}
|
|
212
150
|
}
|
|
213
|
-
function getTailscaleConnectionStatus() {
|
|
214
|
-
try {
|
|
215
|
-
const status = readTailscaleStatus();
|
|
216
|
-
const self = status.Self ?? {};
|
|
217
|
-
const online = self.Online === true;
|
|
218
|
-
const ips = self.TailscaleIPs;
|
|
219
|
-
const dns = self.DNSName?.replace(/\.$/, "");
|
|
220
|
-
return {
|
|
221
|
-
connected: online,
|
|
222
|
-
tailscale_ip: ips?.[0] ?? null,
|
|
223
|
-
hostname: dns ?? null
|
|
224
|
-
};
|
|
225
|
-
} catch (e) {
|
|
226
|
-
return {
|
|
227
|
-
connected: false,
|
|
228
|
-
tailscale_ip: null,
|
|
229
|
-
hostname: null,
|
|
230
|
-
error: e.message
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
151
|
function getTailscaleInstallationStatus() {
|
|
235
152
|
const binary = findTailscaleBinary();
|
|
236
153
|
if (!binary) {
|
|
@@ -262,6 +179,62 @@ function getTailscaleInstallationStatus() {
|
|
|
262
179
|
function resetTailscaleBinaryCache() {
|
|
263
180
|
cachedBinary = null;
|
|
264
181
|
}
|
|
182
|
+
function logoutTailscale(timeout = 15e3) {
|
|
183
|
+
const binary = getTailscaleBinary();
|
|
184
|
+
try {
|
|
185
|
+
const output = runExec([binary, "logout"], timeout);
|
|
186
|
+
resetTailscaleBinaryCache();
|
|
187
|
+
return { success: true, output: output.trim() };
|
|
188
|
+
} catch (e) {
|
|
189
|
+
const err = e;
|
|
190
|
+
return { success: false, output: (err.stderr ?? err.message ?? "").trim() };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function startInteractiveLogin() {
|
|
194
|
+
const binary = getTailscaleBinary();
|
|
195
|
+
const child = nodeSpawn(binary, ["login"], {
|
|
196
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
197
|
+
windowsHide: true
|
|
198
|
+
});
|
|
199
|
+
const urlRegex = /https:\/\/login\.tailscale\.com\/a\/[^\s]+/;
|
|
200
|
+
const promise = new Promise((resolve4) => {
|
|
201
|
+
let stdout = "";
|
|
202
|
+
let resolved = false;
|
|
203
|
+
const done = (result) => {
|
|
204
|
+
if (resolved) return;
|
|
205
|
+
resolved = true;
|
|
206
|
+
resolve4(result);
|
|
207
|
+
};
|
|
208
|
+
child.stdout?.on("data", (data) => {
|
|
209
|
+
stdout += data.toString();
|
|
210
|
+
const match2 = stdout.match(urlRegex);
|
|
211
|
+
if (match2) {
|
|
212
|
+
done({ success: true, url: match2[0], output: stdout.trim() });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
child.stderr?.on("data", (data) => {
|
|
216
|
+
stdout += data.toString();
|
|
217
|
+
});
|
|
218
|
+
child.on("close", (code) => {
|
|
219
|
+
if (code === 0) {
|
|
220
|
+
done({ success: true, output: stdout.trim() });
|
|
221
|
+
} else {
|
|
222
|
+
done({ success: false, output: stdout.trim() || `Exited with code ${code}` });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
child.on("error", (err) => {
|
|
226
|
+
done({ success: false, output: err.message });
|
|
227
|
+
});
|
|
228
|
+
setTimeout(() => {
|
|
229
|
+
done({ success: false, output: "Timeout waiting for login URL" });
|
|
230
|
+
try {
|
|
231
|
+
child.kill();
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}, 3e4);
|
|
235
|
+
});
|
|
236
|
+
return { promise, process: child };
|
|
237
|
+
}
|
|
265
238
|
function getInstallInstructions(platform) {
|
|
266
239
|
switch (platform) {
|
|
267
240
|
case "linux":
|
|
@@ -294,20 +267,11 @@ function getInstallInstructions(platform) {
|
|
|
294
267
|
};
|
|
295
268
|
}
|
|
296
269
|
}
|
|
297
|
-
var cachedBinary,
|
|
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
|
}
|
|
@@ -3411,7 +3427,7 @@ function classifyFetchError(error) {
|
|
|
3411
3427
|
return null;
|
|
3412
3428
|
}
|
|
3413
3429
|
function sleep2(ms) {
|
|
3414
|
-
return new Promise((
|
|
3430
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
3415
3431
|
}
|
|
3416
3432
|
var UNKNOWN, PARTY_SERVER, DEGRADED, SUSPECT, DOWN, NOT_SERVER, MAYBE, MAYBE_MAX_RETRIES, BACKOFF_BASE, BACKOFF_CAP, FAILURE_SUSPECT, FAILURE_DOWN, PeerDiscovery;
|
|
3417
3433
|
var init_peer_discovery = __esm({
|
|
@@ -4178,7 +4194,21 @@ body::after{
|
|
|
4178
4194
|
}
|
|
4179
4195
|
.btn-join:hover{background:rgba(0,255,240,0.18);box-shadow:0 0 10px rgba(0,255,240,0.2)}
|
|
4180
4196
|
.btn-join:active{transform:scale(0.97)}
|
|
4181
|
-
.btn-
|
|
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,6 +5161,37 @@ var init_dashboard = __esm({
|
|
|
4935
5161
|
return c.json({ success: false, output: e.message }, 500);
|
|
4936
5162
|
}
|
|
4937
5163
|
});
|
|
5164
|
+
activeLogin = null;
|
|
5165
|
+
dashboardRoutes.post("/api/logout", async (c) => {
|
|
5166
|
+
const result = logoutTailscale();
|
|
5167
|
+
if (result.success) {
|
|
5168
|
+
resetTailscaleBinaryCache();
|
|
5169
|
+
refreshSelfIp();
|
|
5170
|
+
}
|
|
5171
|
+
return c.json(result, result.success ? 200 : 500);
|
|
5172
|
+
});
|
|
5173
|
+
dashboardRoutes.post("/api/tailscale-login", async (c) => {
|
|
5174
|
+
if (activeLogin?.url) {
|
|
5175
|
+
return c.json({ success: true, url: activeLogin.url });
|
|
5176
|
+
}
|
|
5177
|
+
const { promise, process: process2 } = startInteractiveLogin();
|
|
5178
|
+
activeLogin = { process: process2 };
|
|
5179
|
+
const result = await promise;
|
|
5180
|
+
if (result.success && result.url) {
|
|
5181
|
+
activeLogin.url = result.url;
|
|
5182
|
+
return c.json({ success: true, url: result.url });
|
|
5183
|
+
}
|
|
5184
|
+
activeLogin = null;
|
|
5185
|
+
return c.json({ success: false, output: result.output }, 500);
|
|
5186
|
+
});
|
|
5187
|
+
dashboardRoutes.post("/api/install-tailscale", async (c) => {
|
|
5188
|
+
const { installTailscale: installTailscale2 } = await Promise.resolve().then(() => (init_tailscale_installer(), tailscale_installer_exports));
|
|
5189
|
+
const result = await installTailscale2(process.platform);
|
|
5190
|
+
if (result.success) {
|
|
5191
|
+
resetTailscaleBinaryCache();
|
|
5192
|
+
}
|
|
5193
|
+
return c.json(result, result.success ? 200 : 500);
|
|
5194
|
+
});
|
|
4938
5195
|
}
|
|
4939
5196
|
});
|
|
4940
5197
|
|
|
@@ -5005,49 +5262,7 @@ var init_server = __esm({
|
|
|
5005
5262
|
|
|
5006
5263
|
// src/cli/setup.ts
|
|
5007
5264
|
init_tailscale();
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
// src/cli/tailscale-installer.ts
|
|
5011
|
-
import { spawn } from "child_process";
|
|
5012
|
-
var INSTALL_COMMANDS = {
|
|
5013
|
-
linux: { cmd: "bash", args: ["-c", "curl -fsSL https://tailscale.com/install.sh | sh"], needsSudo: true },
|
|
5014
|
-
darwin: { cmd: "brew", args: ["install", "tailscale"], needsSudo: false },
|
|
5015
|
-
win32: { cmd: "winget", args: ["install", "Tailscale.Tailscale", "--accept-source-agreements"], needsSudo: false }
|
|
5016
|
-
};
|
|
5017
|
-
async function installTailscale(platform) {
|
|
5018
|
-
const entry = INSTALL_COMMANDS[platform];
|
|
5019
|
-
if (!entry) {
|
|
5020
|
-
return {
|
|
5021
|
-
success: false,
|
|
5022
|
-
output: `Unsupported platform: ${platform}. Please install manually from https://tailscale.com/download`
|
|
5023
|
-
};
|
|
5024
|
-
}
|
|
5025
|
-
const cmd = entry.needsSudo ? "sudo" : entry.cmd;
|
|
5026
|
-
const args2 = entry.needsSudo ? [entry.cmd, ...entry.args] : entry.args;
|
|
5027
|
-
console.log(`Running: ${cmd} ${args2.join(" ")}
|
|
5028
|
-
`);
|
|
5029
|
-
return new Promise((resolve3) => {
|
|
5030
|
-
const child = spawn(cmd, args2, {
|
|
5031
|
-
stdio: "inherit",
|
|
5032
|
-
windowsHide: true
|
|
5033
|
-
});
|
|
5034
|
-
let exited = false;
|
|
5035
|
-
child.on("close", (code) => {
|
|
5036
|
-
if (exited) return;
|
|
5037
|
-
exited = true;
|
|
5038
|
-
if (code === 0) {
|
|
5039
|
-
resolve3({ success: true, output: "Installation completed." });
|
|
5040
|
-
} else {
|
|
5041
|
-
resolve3({ success: false, output: `Installation exited with code ${code}` });
|
|
5042
|
-
}
|
|
5043
|
-
});
|
|
5044
|
-
child.on("error", (err) => {
|
|
5045
|
-
if (exited) return;
|
|
5046
|
-
exited = true;
|
|
5047
|
-
resolve3({ success: false, output: err.message });
|
|
5048
|
-
});
|
|
5049
|
-
});
|
|
5050
|
-
}
|
|
5265
|
+
init_tailscale_installer();
|
|
5051
5266
|
|
|
5052
5267
|
// src/cli/agent-detector.ts
|
|
5053
5268
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -5332,11 +5547,12 @@ async function installPluginToAgent(agentType) {
|
|
|
5332
5547
|
}
|
|
5333
5548
|
}
|
|
5334
5549
|
|
|
5335
|
-
// src/cli/
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5550
|
+
// src/cli/tailscale-login.ts
|
|
5551
|
+
init_tailscale();
|
|
5552
|
+
import { spawn as spawn2 } from "child_process";
|
|
5553
|
+
|
|
5554
|
+
// src/cli/tty-utils.ts
|
|
5555
|
+
import { createInterface } from "readline";
|
|
5340
5556
|
function cyan(text) {
|
|
5341
5557
|
return `\x1B[36m${text}\x1B[0m`;
|
|
5342
5558
|
}
|
|
@@ -5352,10 +5568,222 @@ function red(text) {
|
|
|
5352
5568
|
function bold(text) {
|
|
5353
5569
|
return `\x1B[1m${text}\x1B[0m`;
|
|
5354
5570
|
}
|
|
5571
|
+
function dim(text) {
|
|
5572
|
+
return `\x1B[2m${text}\x1B[0m`;
|
|
5573
|
+
}
|
|
5574
|
+
function createRl() {
|
|
5575
|
+
return createInterface({ input: process.stdin, output: process.stdout });
|
|
5576
|
+
}
|
|
5577
|
+
function closeRl(rl) {
|
|
5578
|
+
rl.close();
|
|
5579
|
+
}
|
|
5580
|
+
async function prompt(question) {
|
|
5581
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5582
|
+
return new Promise((resolve4) => {
|
|
5583
|
+
rl.question(question, (answer) => {
|
|
5584
|
+
rl.close();
|
|
5585
|
+
resolve4(answer.trim());
|
|
5586
|
+
});
|
|
5587
|
+
});
|
|
5588
|
+
}
|
|
5589
|
+
async function select(options, opts) {
|
|
5590
|
+
if (options.length === 0) throw new Error("select() requires at least one option");
|
|
5591
|
+
if (options.length === 1) return options[0].value;
|
|
5592
|
+
const message = opts?.message ?? "";
|
|
5593
|
+
const wasRaw = process.stdin.isRaw;
|
|
5594
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
5595
|
+
try {
|
|
5596
|
+
let cursor = 0;
|
|
5597
|
+
const render = () => {
|
|
5598
|
+
const lines = options.length + (message ? 1 : 0);
|
|
5599
|
+
process.stdout.write(`\x1B[${lines}A\x1B[0J`);
|
|
5600
|
+
if (message) {
|
|
5601
|
+
process.stdout.write(`${message}
|
|
5602
|
+
`);
|
|
5603
|
+
}
|
|
5604
|
+
for (let i = 0; i < options.length; i++) {
|
|
5605
|
+
const opt = options[i];
|
|
5606
|
+
const prefix = i === cursor ? `${cyan("\u276F")} ` : " ";
|
|
5607
|
+
const label = i === cursor ? bold(opt.label) : opt.label;
|
|
5608
|
+
const hintStr = opt.hint ? ` ${dim(opt.hint)}` : "";
|
|
5609
|
+
process.stdout.write(`${prefix}${label}${hintStr}
|
|
5610
|
+
`);
|
|
5611
|
+
}
|
|
5612
|
+
};
|
|
5613
|
+
if (message) {
|
|
5614
|
+
process.stdout.write(`${message}
|
|
5615
|
+
`);
|
|
5616
|
+
}
|
|
5617
|
+
for (let i = 0; i < options.length; i++) {
|
|
5618
|
+
const opt = options[i];
|
|
5619
|
+
const prefix = i === cursor ? `${cyan("\u276F")} ` : " ";
|
|
5620
|
+
const label = i === cursor ? bold(opt.label) : opt.label;
|
|
5621
|
+
const hintStr = opt.hint ? ` ${dim(opt.hint)}` : "";
|
|
5622
|
+
process.stdout.write(`${prefix}${label}${hintStr}
|
|
5623
|
+
`);
|
|
5624
|
+
}
|
|
5625
|
+
return new Promise((resolve4) => {
|
|
5626
|
+
const onKey = (ch, key) => {
|
|
5627
|
+
if (key.name === "up" || key.sequence === "\x1B[A") {
|
|
5628
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
5629
|
+
render();
|
|
5630
|
+
} else if (key.name === "down" || key.sequence === "\x1B[B") {
|
|
5631
|
+
cursor = (cursor + 1) % options.length;
|
|
5632
|
+
render();
|
|
5633
|
+
} else if (key.name === "return" || key.sequence === "\r") {
|
|
5634
|
+
process.stdin.removeListener("keypress", onKey);
|
|
5635
|
+
process.stdout.write("\n");
|
|
5636
|
+
resolve4(options[cursor].value);
|
|
5637
|
+
}
|
|
5638
|
+
};
|
|
5639
|
+
process.stdin.on("keypress", onKey);
|
|
5640
|
+
if (process.stdin.isTTY) {
|
|
5641
|
+
process.stdin.resume();
|
|
5642
|
+
}
|
|
5643
|
+
});
|
|
5644
|
+
} finally {
|
|
5645
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw ?? false);
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
async function multiSelect(options, opts) {
|
|
5649
|
+
if (options.length === 0) return [];
|
|
5650
|
+
const message = opts?.message ?? "";
|
|
5651
|
+
const wasRaw = process.stdin.isRaw;
|
|
5652
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
5653
|
+
try {
|
|
5654
|
+
let cursor = 0;
|
|
5655
|
+
const selected = /* @__PURE__ */ new Set();
|
|
5656
|
+
const render = () => {
|
|
5657
|
+
const lines = options.length + (message ? 1 : 0) + 1;
|
|
5658
|
+
process.stdout.write(`\x1B[${lines}A\x1B[0J`);
|
|
5659
|
+
if (message) {
|
|
5660
|
+
process.stdout.write(`${message}
|
|
5661
|
+
`);
|
|
5662
|
+
}
|
|
5663
|
+
process.stdout.write(`${dim(" \u2191/\u2193 navigate, space to select, enter to confirm")}
|
|
5664
|
+
`);
|
|
5665
|
+
for (let i = 0; i < options.length; i++) {
|
|
5666
|
+
const opt = options[i];
|
|
5667
|
+
const prefix = i === cursor ? `${cyan("\u276F")} ` : " ";
|
|
5668
|
+
const check = selected.has(i) ? green("\u25C9") : "\u25CB";
|
|
5669
|
+
const label = i === cursor ? bold(opt.label) : opt.label;
|
|
5670
|
+
process.stdout.write(`${prefix}${check} ${label}
|
|
5671
|
+
`);
|
|
5672
|
+
}
|
|
5673
|
+
};
|
|
5674
|
+
if (message) {
|
|
5675
|
+
process.stdout.write(`${message}
|
|
5676
|
+
`);
|
|
5677
|
+
}
|
|
5678
|
+
process.stdout.write(`${dim(" \u2191/\u2193 navigate, space to select, enter to confirm")}
|
|
5679
|
+
`);
|
|
5680
|
+
for (let i = 0; i < options.length; i++) {
|
|
5681
|
+
const opt = options[i];
|
|
5682
|
+
const prefix = i === cursor ? `${cyan("\u276F")} ` : " ";
|
|
5683
|
+
const check = selected.has(i) ? green("\u25C9") : "\u25CB";
|
|
5684
|
+
const label = i === cursor ? bold(opt.label) : opt.label;
|
|
5685
|
+
process.stdout.write(`${prefix}${check} ${label}
|
|
5686
|
+
`);
|
|
5687
|
+
}
|
|
5688
|
+
return new Promise((resolve4) => {
|
|
5689
|
+
const onKey = (_ch, key) => {
|
|
5690
|
+
if (key.name === "up" || key.sequence === "\x1B[A") {
|
|
5691
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
5692
|
+
render();
|
|
5693
|
+
} else if (key.name === "down" || key.sequence === "\x1B[B") {
|
|
5694
|
+
cursor = (cursor + 1) % options.length;
|
|
5695
|
+
render();
|
|
5696
|
+
} else if (key.name === "space") {
|
|
5697
|
+
if (selected.has(cursor)) {
|
|
5698
|
+
selected.delete(cursor);
|
|
5699
|
+
} else {
|
|
5700
|
+
selected.add(cursor);
|
|
5701
|
+
}
|
|
5702
|
+
render();
|
|
5703
|
+
} else if (key.name === "return" || key.sequence === "\r") {
|
|
5704
|
+
process.stdin.removeListener("keypress", onKey);
|
|
5705
|
+
process.stdout.write("\n");
|
|
5706
|
+
const result = Array.from(selected).sort((a, b) => a - b).map((i) => options[i].value);
|
|
5707
|
+
resolve4(result);
|
|
5708
|
+
}
|
|
5709
|
+
};
|
|
5710
|
+
process.stdin.on("keypress", onKey);
|
|
5711
|
+
if (process.stdin.isTTY) {
|
|
5712
|
+
process.stdin.resume();
|
|
5713
|
+
}
|
|
5714
|
+
});
|
|
5715
|
+
} finally {
|
|
5716
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw ?? false);
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
|
|
5720
|
+
// src/cli/tailscale-login.ts
|
|
5721
|
+
async function interactiveLogin(binary) {
|
|
5722
|
+
console.log(`
|
|
5723
|
+
${cyan("Running interactive login...")}`);
|
|
5724
|
+
console.log("A browser window should open. Authenticate in the browser, then return here.\n");
|
|
5725
|
+
const child = spawn2(binary, ["login"], { stdio: "inherit" });
|
|
5726
|
+
const exitCode = await new Promise((resolve4) => {
|
|
5727
|
+
child.on("close", resolve4);
|
|
5728
|
+
});
|
|
5729
|
+
resetTailscaleBinaryCache();
|
|
5730
|
+
const status = getTailscaleInstallationStatus();
|
|
5731
|
+
if (exitCode === 0 && status.state === "connected") {
|
|
5732
|
+
console.log(`
|
|
5733
|
+
${green("\u2705 Login successful!")} IP: ${status.tailscale_ip}`);
|
|
5734
|
+
showAuthKeyTip();
|
|
5735
|
+
return true;
|
|
5736
|
+
}
|
|
5737
|
+
console.log(`
|
|
5738
|
+
${yellow("\u26A0\uFE0F Login may not have completed. Status: " + status.state)}`);
|
|
5739
|
+
console.log(" Try running: open-party login");
|
|
5740
|
+
return false;
|
|
5741
|
+
}
|
|
5742
|
+
async function authKeyLogin(binary) {
|
|
5743
|
+
console.log("");
|
|
5744
|
+
console.log("Ask the network creator to generate an Auth Key at:");
|
|
5745
|
+
console.log(`${cyan(" https://login.tailscale.com/admin/settings/keys")}
|
|
5746
|
+
`);
|
|
5747
|
+
const authKey = await prompt("Enter Auth Key: ");
|
|
5748
|
+
if (!authKey) {
|
|
5749
|
+
console.log(yellow("No auth key provided, skipping login."));
|
|
5750
|
+
return false;
|
|
5751
|
+
}
|
|
5752
|
+
const result = joinTailnet(authKey);
|
|
5753
|
+
if (result.success) {
|
|
5754
|
+
resetTailscaleBinaryCache();
|
|
5755
|
+
const status = getTailscaleInstallationStatus();
|
|
5756
|
+
console.log(`
|
|
5757
|
+
${green("\u2705 Login successful!")} IP: ${status.state === "connected" ? status.tailscale_ip : "unknown"}`);
|
|
5758
|
+
showAuthKeyTip();
|
|
5759
|
+
return true;
|
|
5760
|
+
}
|
|
5761
|
+
console.log(`
|
|
5762
|
+
${red("\u274C Login failed:")}
|
|
5763
|
+
${result.output}`);
|
|
5764
|
+
console.log(" Check your auth key and try again.");
|
|
5765
|
+
return false;
|
|
5766
|
+
}
|
|
5767
|
+
function showAuthKeyTip() {
|
|
5768
|
+
console.log("");
|
|
5769
|
+
console.log(`${bold("\u{1F4A1} To share network access with teammates:")}`);
|
|
5770
|
+
console.log(" 1. Go to https://login.tailscale.com/admin/settings/keys");
|
|
5771
|
+
console.log(" 2. Generate an Auth Key");
|
|
5772
|
+
console.log(" 3. Share it with teammates \u2014 they can run: open-party login");
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5775
|
+
// src/cli/setup.ts
|
|
5355
5776
|
async function stepTailscale() {
|
|
5356
5777
|
console.log(`
|
|
5357
|
-
${bold(cyan("\u{1F50D} Step 1:
|
|
5778
|
+
${bold(cyan("\u{1F50D} Step 1: Tailscale Network"))}
|
|
5358
5779
|
`);
|
|
5780
|
+
console.log(
|
|
5781
|
+
"Tailscale enables agents across different machines to discover and\ncommunicate with each other over a secure network.\n"
|
|
5782
|
+
);
|
|
5783
|
+
console.log(
|
|
5784
|
+
`${dim("Without Tailscale, Open Party runs in local mode \u2014 connecting only\nto agents on this machine.")}
|
|
5785
|
+
`
|
|
5786
|
+
);
|
|
5359
5787
|
const status = getTailscaleInstallationStatus();
|
|
5360
5788
|
if (status.state === "connected") {
|
|
5361
5789
|
console.log(`${green("\u2705 Tailscale is connected!")}`);
|
|
@@ -5363,12 +5791,10 @@ ${bold(cyan("\u{1F50D} Step 1: Detecting Tailscale..."))}
|
|
|
5363
5791
|
return;
|
|
5364
5792
|
}
|
|
5365
5793
|
if (status.state === "not_installed") {
|
|
5366
|
-
console.log(`${red("\u274C Tailscale is not installed.")}`);
|
|
5367
5794
|
await handleNotInstalled(status.platform);
|
|
5368
5795
|
const newStatus = getTailscaleInstallationStatus();
|
|
5369
5796
|
if (newStatus.state === "not_installed") {
|
|
5370
|
-
|
|
5371
|
-
${yellow("\u26A0\uFE0F Tailscale still not detected. Please install manually and re-run setup.")}`);
|
|
5797
|
+
showLocalModeNotice();
|
|
5372
5798
|
return;
|
|
5373
5799
|
}
|
|
5374
5800
|
if (newStatus.state === "connected") {
|
|
@@ -5383,6 +5809,7 @@ ${green("\u2705 Tailscale is connected!")} IP: ${newStatus.tailscale_ip}`);
|
|
|
5383
5809
|
}
|
|
5384
5810
|
async function handleNotInstalled(platform) {
|
|
5385
5811
|
const info = getInstallInstructions(platform);
|
|
5812
|
+
console.log(`${red("\u274C Tailscale is not installed.")}`);
|
|
5386
5813
|
console.log(`
|
|
5387
5814
|
Install Tailscale for ${bold(info.os)}:
|
|
5388
5815
|
`);
|
|
@@ -5393,99 +5820,61 @@ Install Tailscale for ${bold(info.os)}:
|
|
|
5393
5820
|
}
|
|
5394
5821
|
}
|
|
5395
5822
|
console.log(`
|
|
5396
|
-
Download: ${info.download_url}
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5823
|
+
Download: ${info.download_url}
|
|
5824
|
+
`);
|
|
5825
|
+
const options = [];
|
|
5826
|
+
if (info.commands.length > 0 && platform !== "win32") {
|
|
5827
|
+
options.push({ label: "Install Tailscale automatically", value: "auto", hint: "recommended" });
|
|
5828
|
+
}
|
|
5829
|
+
options.push({ label: "I've installed Tailscale, re-detect", value: "redetect" });
|
|
5830
|
+
options.push({ label: "Skip \u2014 use local mode only", value: "skip", hint: "agents on this machine only" });
|
|
5831
|
+
const choice = await select(options, { message: "Choose:" });
|
|
5832
|
+
if (choice === "skip") {
|
|
5833
|
+
return;
|
|
5834
|
+
}
|
|
5835
|
+
if (choice === "auto") {
|
|
5836
|
+
console.log("");
|
|
5837
|
+
const result = await installTailscale(platform);
|
|
5838
|
+
if (result.success) {
|
|
5839
|
+
console.log(`${green("\u2705 Tailscale installed successfully!")}`);
|
|
5840
|
+
} else {
|
|
5841
|
+
console.log(`${red("\u274C Installation failed:")}
|
|
5408
5842
|
${result.output}`);
|
|
5409
|
-
|
|
5410
|
-
Please install manually and re-run
|
|
5411
|
-
}
|
|
5412
|
-
return;
|
|
5843
|
+
console.log(`
|
|
5844
|
+
Please install manually and re-run: ${cyan("open-party setup")}`);
|
|
5413
5845
|
}
|
|
5846
|
+
return;
|
|
5414
5847
|
}
|
|
5415
|
-
|
|
5416
|
-
Or run: ${cyan("npx open-party setup")} (after installing manually)`);
|
|
5417
|
-
if (autoInstall) {
|
|
5418
|
-
const cont = await prompt('Press Enter when installation is complete, or type "skip" to skip...');
|
|
5419
|
-
if (cont.toLowerCase() === "skip") return;
|
|
5848
|
+
if (choice === "redetect") {
|
|
5420
5849
|
resetTailscaleBinaryCache();
|
|
5850
|
+
return;
|
|
5421
5851
|
}
|
|
5422
5852
|
}
|
|
5423
5853
|
async function handleNotConnected(binary) {
|
|
5424
|
-
console.log(`${yellow("\u{1F512} Tailscale is installed but not connected.")}
|
|
5425
|
-
console.log("");
|
|
5426
|
-
console.log("Choose a login method:\n");
|
|
5427
|
-
console.log(" 1. Interactive login (opens browser to authenticate)");
|
|
5428
|
-
console.log(" 2. Auth key (enter from Tailscale admin console)");
|
|
5429
|
-
console.log("");
|
|
5430
|
-
const choice = await prompt("Select [1/2]: ");
|
|
5431
|
-
if (choice === "1") {
|
|
5432
|
-
await handleInteractiveLogin(binary);
|
|
5433
|
-
} else {
|
|
5434
|
-
await handleAuthKeyLogin(binary);
|
|
5435
|
-
}
|
|
5436
|
-
}
|
|
5437
|
-
async function handleInteractiveLogin(binary) {
|
|
5438
|
-
console.log(`
|
|
5439
|
-
${cyan("Running interactive login...")}`);
|
|
5440
|
-
console.log("A browser window should open. Authenticate in the browser, then return here.\n");
|
|
5441
|
-
const { spawn: spawn3 } = await import("child_process");
|
|
5442
|
-
const child = spawn3(binary, ["login"], { stdio: "inherit" });
|
|
5443
|
-
const exitCode = await new Promise((resolve3) => {
|
|
5444
|
-
child.on("close", resolve3);
|
|
5445
|
-
});
|
|
5446
|
-
resetTailscaleBinaryCache();
|
|
5447
|
-
const status = getTailscaleInstallationStatus();
|
|
5448
|
-
if (exitCode === 0 && status.state === "connected") {
|
|
5449
|
-
console.log(`
|
|
5450
|
-
${green("\u2705 Login successful!")} IP: ${status.tailscale_ip}`);
|
|
5451
|
-
showAuthKeyTip();
|
|
5452
|
-
} else {
|
|
5453
|
-
console.log(`
|
|
5454
|
-
${yellow("\u26A0\uFE0F Login may not have completed. Status: " + status.state)}`);
|
|
5455
|
-
console.log(" Try running: npx open-party setup");
|
|
5456
|
-
}
|
|
5457
|
-
}
|
|
5458
|
-
async function handleAuthKeyLogin(binary) {
|
|
5459
|
-
console.log("");
|
|
5460
|
-
console.log("Ask the network creator to generate an Auth Key at:");
|
|
5461
|
-
console.log(`${cyan(" https://login.tailscale.com/admin/settings/keys")}
|
|
5854
|
+
console.log(`${yellow("\u{1F512} Tailscale is installed but not connected.")}
|
|
5462
5855
|
`);
|
|
5463
|
-
const
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
const
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
console.log(`
|
|
5474
|
-
${green("\u2705 Login successful!")} IP: ${status.state === "connected" ? status.tailscale_ip : "unknown"}`);
|
|
5475
|
-
showAuthKeyTip();
|
|
5856
|
+
const options = [
|
|
5857
|
+
{ label: "Interactive login", value: "interactive", hint: "opens browser to authenticate" },
|
|
5858
|
+
{ label: "Auth key", value: "authkey", hint: "from network creator" },
|
|
5859
|
+
{ label: "Skip", value: "skip", hint: "login later with: open-party login" }
|
|
5860
|
+
];
|
|
5861
|
+
const choice = await select(options, { message: "Choose a login method:" });
|
|
5862
|
+
if (choice === "interactive") {
|
|
5863
|
+
await interactiveLogin(binary);
|
|
5864
|
+
} else if (choice === "authkey") {
|
|
5865
|
+
await authKeyLogin(binary);
|
|
5476
5866
|
} else {
|
|
5477
5867
|
console.log(`
|
|
5478
|
-
${
|
|
5479
|
-
${
|
|
5480
|
-
console.log(" Check your auth key and try again.");
|
|
5868
|
+
${yellow("\u26A0\uFE0F Tailscale not connected. Running in local mode.")}`);
|
|
5869
|
+
console.log(` To connect later, run: ${cyan("open-party login")}`);
|
|
5481
5870
|
}
|
|
5482
5871
|
}
|
|
5483
|
-
function
|
|
5484
|
-
console.log(
|
|
5485
|
-
|
|
5486
|
-
console.log("
|
|
5487
|
-
console.log(
|
|
5488
|
-
console.log(
|
|
5872
|
+
function showLocalModeNotice() {
|
|
5873
|
+
console.log(`
|
|
5874
|
+
${yellow("\u26A0\uFE0F Running in local mode \u2014 connecting to agents on this machine only.")}`);
|
|
5875
|
+
console.log(" To enable cross-machine communication later:");
|
|
5876
|
+
console.log(` 1. Install Tailscale: ${cyan("https://tailscale.com/download")}`);
|
|
5877
|
+
console.log(` 2. Run: ${cyan("open-party login")}`);
|
|
5489
5878
|
}
|
|
5490
5879
|
async function stepAgentPlugin() {
|
|
5491
5880
|
console.log(`
|
|
@@ -5497,7 +5886,7 @@ ${bold(cyan("\u{1F50D} Step 2: Detecting AI agents in your environment..."))}
|
|
|
5497
5886
|
console.log(yellow("No supported AI agents detected in this environment."));
|
|
5498
5887
|
console.log(" Supported agents: Claude Code, Cursor, Gemini CLI");
|
|
5499
5888
|
console.log("");
|
|
5500
|
-
console.log(" Install one and re-run:
|
|
5889
|
+
console.log(" Install one and re-run: open-party setup");
|
|
5501
5890
|
return;
|
|
5502
5891
|
}
|
|
5503
5892
|
console.log("Detected agents:\n");
|
|
@@ -5505,7 +5894,10 @@ ${bold(cyan("\u{1F50D} Step 2: Detecting AI agents in your environment..."))}
|
|
|
5505
5894
|
console.log(` ${green("\u2713")} ${agent.name}`);
|
|
5506
5895
|
}
|
|
5507
5896
|
console.log("");
|
|
5508
|
-
const
|
|
5897
|
+
const options = detected.map((a) => ({ label: a.name, value: a }));
|
|
5898
|
+
const selected = await multiSelect(options, {
|
|
5899
|
+
message: "Select agents to install Open Party plugin:"
|
|
5900
|
+
});
|
|
5509
5901
|
if (selected.length === 0) {
|
|
5510
5902
|
console.log(yellow("No agents selected, skipping plugin installation."));
|
|
5511
5903
|
return;
|
|
@@ -5527,32 +5919,19 @@ Installing Open Party plugin for ${agent.name}...`);
|
|
|
5527
5919
|
}
|
|
5528
5920
|
}
|
|
5529
5921
|
}
|
|
5530
|
-
async function selectAgents(agents) {
|
|
5531
|
-
console.log("Select agents to install Open Party plugin:\n");
|
|
5532
|
-
for (let i = 0; i < agents.length; i++) {
|
|
5533
|
-
console.log(` [${i + 1}] ${agents[i].name}`);
|
|
5534
|
-
}
|
|
5535
|
-
console.log(` [a] All`);
|
|
5536
|
-
console.log(` [n] None (skip)`);
|
|
5537
|
-
console.log("");
|
|
5538
|
-
const answer = await prompt('Enter selection (e.g. "1 2" or "a" or "n"): ');
|
|
5539
|
-
if (answer.toLowerCase() === "n" || answer === "") return [];
|
|
5540
|
-
if (answer.toLowerCase() === "a") return agents;
|
|
5541
|
-
const indices = answer.split(/[\s,]+/).map((s) => parseInt(s, 10) - 1).filter((i) => i >= 0 && i < agents.length);
|
|
5542
|
-
return indices.map((i) => agents[i]);
|
|
5543
|
-
}
|
|
5544
5922
|
async function setupCommand() {
|
|
5545
5923
|
console.log(bold(cyan("\n\u{1F680} Open Party Setup Wizard\n")));
|
|
5924
|
+
const rl = createRl();
|
|
5546
5925
|
await stepTailscale();
|
|
5547
5926
|
await stepAgentPlugin();
|
|
5548
5927
|
console.log(`
|
|
5549
5928
|
${bold(cyan("\u{1F680} Starting Party Server..."))}`);
|
|
5550
|
-
const { spawn:
|
|
5551
|
-
const { resolve:
|
|
5552
|
-
const { fileURLToPath:
|
|
5553
|
-
const __dirname2 =
|
|
5554
|
-
const serverScript =
|
|
5555
|
-
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], {
|
|
5556
5935
|
detached: true,
|
|
5557
5936
|
stdio: "ignore",
|
|
5558
5937
|
windowsHide: true
|
|
@@ -5563,11 +5942,70 @@ ${bold(cyan("\u{1F680} Starting Party Server..."))}`);
|
|
|
5563
5942
|
${bold(green("\u{1F389} Setup complete!"))}`);
|
|
5564
5943
|
console.log(` Dashboard: http://127.0.0.1:8000/dashboard`);
|
|
5565
5944
|
console.log(" Other agents can join with: npx @feynmanzhang/open-party setup\n");
|
|
5566
|
-
rl
|
|
5945
|
+
closeRl(rl);
|
|
5946
|
+
}
|
|
5947
|
+
|
|
5948
|
+
// src/cli/login.ts
|
|
5949
|
+
init_tailscale();
|
|
5950
|
+
async function loginCommand() {
|
|
5951
|
+
const status = getTailscaleInstallationStatus();
|
|
5952
|
+
if (status.state === "connected") {
|
|
5953
|
+
console.log(`${green("\u2705 Tailscale is already connected!")}`);
|
|
5954
|
+
console.log(` IP: ${status.tailscale_ip} Hostname: ${status.hostname}`);
|
|
5955
|
+
return;
|
|
5956
|
+
}
|
|
5957
|
+
if (status.state === "not_installed") {
|
|
5958
|
+
console.log(`${red("\u274C Tailscale is not installed.")}`);
|
|
5959
|
+
console.log(" Install it first: https://tailscale.com/download");
|
|
5960
|
+
console.log(` Then run: ${cyan("open-party login")}`);
|
|
5961
|
+
return;
|
|
5962
|
+
}
|
|
5963
|
+
console.log(`${yellow("\u{1F512} Tailscale is installed but not connected.")}
|
|
5964
|
+
`);
|
|
5965
|
+
const options = [
|
|
5966
|
+
{ label: "Interactive login", value: "interactive", hint: "opens browser to authenticate" },
|
|
5967
|
+
{ label: "Auth key", value: "authkey", hint: "from network creator" }
|
|
5968
|
+
];
|
|
5969
|
+
const choice = await select(options, { message: "Choose a login method:" });
|
|
5970
|
+
if (choice === "interactive") {
|
|
5971
|
+
await interactiveLogin(status.binary);
|
|
5972
|
+
} else {
|
|
5973
|
+
await authKeyLogin(status.binary);
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
|
|
5977
|
+
// src/cli/logout.ts
|
|
5978
|
+
init_tailscale();
|
|
5979
|
+
async function logoutCommand() {
|
|
5980
|
+
const status = getTailscaleInstallationStatus();
|
|
5981
|
+
if (status.state === "not_installed") {
|
|
5982
|
+
console.log(red("\u274C Tailscale is not installed."));
|
|
5983
|
+
return;
|
|
5984
|
+
}
|
|
5985
|
+
if (status.state === "not_connected") {
|
|
5986
|
+
console.log(yellow("\u26A0\uFE0F Tailscale is not connected \u2014 nothing to log out from."));
|
|
5987
|
+
return;
|
|
5988
|
+
}
|
|
5989
|
+
const choice = await select(
|
|
5990
|
+
[
|
|
5991
|
+
{ label: "Log out (remove credentials)", value: "logout", hint: "need to re-authenticate next time" },
|
|
5992
|
+
{ label: "Cancel", value: "cancel" }
|
|
5993
|
+
],
|
|
5994
|
+
{ message: "Are you sure you want to log out?" }
|
|
5995
|
+
);
|
|
5996
|
+
if (choice === "cancel") return;
|
|
5997
|
+
console.log("Logging out of Tailscale...");
|
|
5998
|
+
const result = logoutTailscale();
|
|
5999
|
+
if (result.success) {
|
|
6000
|
+
console.log(green("\u2705 Logged out successfully."));
|
|
6001
|
+
console.log(" To reconnect, run: open-party login");
|
|
6002
|
+
} else {
|
|
6003
|
+
console.log(red("\u274C Logout failed:"), result.output);
|
|
6004
|
+
}
|
|
5567
6005
|
}
|
|
5568
6006
|
|
|
5569
6007
|
// src/cli/server-utils.ts
|
|
5570
|
-
import { spawn as
|
|
6008
|
+
import { spawn as spawn3, execSync as execSync3 } from "child_process";
|
|
5571
6009
|
import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2, openSync } from "fs";
|
|
5572
6010
|
import { join as join4, dirname as dirname2, resolve as resolve2 } from "path";
|
|
5573
6011
|
import { homedir as homedir3 } from "os";
|
|
@@ -5665,7 +6103,7 @@ async function spawnServerInBackground(port) {
|
|
|
5665
6103
|
mkdirSync2(dirname2(logPath), { recursive: true });
|
|
5666
6104
|
const logFd = openSync(logPath, "a");
|
|
5667
6105
|
const env = { ...process.env, PARTY_PORT: String(port) };
|
|
5668
|
-
const proc =
|
|
6106
|
+
const proc = spawn3(process.execPath, [script], {
|
|
5669
6107
|
stdio: ["ignore", logFd, logFd],
|
|
5670
6108
|
detached: true,
|
|
5671
6109
|
windowsHide: true,
|
|
@@ -5717,7 +6155,7 @@ function parseStartArgs(args2) {
|
|
|
5717
6155
|
return { daemon, port };
|
|
5718
6156
|
}
|
|
5719
6157
|
function sleep(ms) {
|
|
5720
|
-
return new Promise((
|
|
6158
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
5721
6159
|
}
|
|
5722
6160
|
|
|
5723
6161
|
// src/cli/start-server.ts
|
|
@@ -5845,6 +6283,151 @@ async function statusCommand() {
|
|
|
5845
6283
|
}
|
|
5846
6284
|
}
|
|
5847
6285
|
|
|
6286
|
+
// src/cli/version.ts
|
|
6287
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
6288
|
+
import { resolve as resolve3, dirname as dirname4 } from "path";
|
|
6289
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6290
|
+
function showVersion() {
|
|
6291
|
+
const pkgPath = resolve3(dirname4(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
|
|
6292
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
6293
|
+
console.log(`open-party v${pkg.version}`);
|
|
6294
|
+
}
|
|
6295
|
+
|
|
6296
|
+
// src/cli/agents.ts
|
|
6297
|
+
async function agentsCommand() {
|
|
6298
|
+
const port = resolvePort();
|
|
6299
|
+
if (!await isServerHealthy(port)) {
|
|
6300
|
+
console.log("Party Server is not running.");
|
|
6301
|
+
console.log(" Use 'open-party start' to start it.");
|
|
6302
|
+
return;
|
|
6303
|
+
}
|
|
6304
|
+
const overview = await getServerOverview(port);
|
|
6305
|
+
if (!overview) {
|
|
6306
|
+
console.log("Failed to get server overview.");
|
|
6307
|
+
return;
|
|
6308
|
+
}
|
|
6309
|
+
const agents = overview.agents;
|
|
6310
|
+
if (!agents) {
|
|
6311
|
+
console.log("No agent data available.");
|
|
6312
|
+
return;
|
|
6313
|
+
}
|
|
6314
|
+
const localAgents = agents.local_agents ?? [];
|
|
6315
|
+
const remoteAgents = agents.remote_agents ?? [];
|
|
6316
|
+
const localCount = agents.local_count ?? localAgents.length;
|
|
6317
|
+
const remoteCount = agents.remote_count ?? remoteAgents.length;
|
|
6318
|
+
if (localCount === 0) {
|
|
6319
|
+
console.log("Local agents: (none)");
|
|
6320
|
+
} else {
|
|
6321
|
+
console.log(`Local agents (${localCount}):`);
|
|
6322
|
+
for (const agent of localAgents) {
|
|
6323
|
+
const id = agent.agent_id ?? "?";
|
|
6324
|
+
const name = agent.display_name ?? id;
|
|
6325
|
+
const ago = formatTimeAgo(agent.last_heartbeat);
|
|
6326
|
+
console.log(` ${id.padEnd(20)} ${name.padEnd(16)} ${ago}`);
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
if (remoteCount > 0) {
|
|
6330
|
+
console.log(`
|
|
6331
|
+
Remote agents (${remoteCount}):`);
|
|
6332
|
+
for (const agent of remoteAgents) {
|
|
6333
|
+
const id = agent.agent_id ?? "?";
|
|
6334
|
+
const name = agent.display_name ?? id;
|
|
6335
|
+
const via = agent.source_peer_ip ?? "?";
|
|
6336
|
+
const ago = formatTimeAgo(agent.last_heartbeat);
|
|
6337
|
+
console.log(` ${id.padEnd(20)} ${name.padEnd(16)} (via ${via}) ${ago}`);
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
6340
|
+
if (localCount === 0 && remoteCount === 0) {
|
|
6341
|
+
console.log("\nNo agents connected yet.");
|
|
6342
|
+
}
|
|
6343
|
+
}
|
|
6344
|
+
function formatTimeAgo(timestamp) {
|
|
6345
|
+
if (!timestamp) return "\u2014";
|
|
6346
|
+
const diff = Date.now() / 1e3 - timestamp / 1e3;
|
|
6347
|
+
if (diff < 60) return "just now";
|
|
6348
|
+
if (diff < 3600) return `${Math.floor(diff / 60)} min ago`;
|
|
6349
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)} hr ago`;
|
|
6350
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
6351
|
+
}
|
|
6352
|
+
|
|
6353
|
+
// src/cli/peers.ts
|
|
6354
|
+
async function peersCommand() {
|
|
6355
|
+
const port = resolvePort();
|
|
6356
|
+
if (!await isServerHealthy(port)) {
|
|
6357
|
+
console.log("Party Server is not running.");
|
|
6358
|
+
console.log(" Use 'open-party start' to start it.");
|
|
6359
|
+
return;
|
|
6360
|
+
}
|
|
6361
|
+
const overview = await getServerOverview(port);
|
|
6362
|
+
if (!overview) {
|
|
6363
|
+
console.log("Failed to get server overview.");
|
|
6364
|
+
return;
|
|
6365
|
+
}
|
|
6366
|
+
const peers = overview.peers;
|
|
6367
|
+
if (!peers) {
|
|
6368
|
+
console.log("No peer data available.");
|
|
6369
|
+
return;
|
|
6370
|
+
}
|
|
6371
|
+
const details = peers.details ?? [];
|
|
6372
|
+
const remoteAgents = overview.agents?.remote_agents ?? [];
|
|
6373
|
+
const peerAgentCounts = /* @__PURE__ */ new Map();
|
|
6374
|
+
for (const agent of remoteAgents) {
|
|
6375
|
+
const ip = agent.source_peer_ip;
|
|
6376
|
+
peerAgentCounts.set(ip, (peerAgentCounts.get(ip) ?? 0) + 1);
|
|
6377
|
+
}
|
|
6378
|
+
const total = peers.total ?? details.length;
|
|
6379
|
+
if (details.length === 0) {
|
|
6380
|
+
console.log("No peers discovered yet.");
|
|
6381
|
+
return;
|
|
6382
|
+
}
|
|
6383
|
+
console.log(`Peers (${total}):
|
|
6384
|
+
`);
|
|
6385
|
+
for (const peer of details) {
|
|
6386
|
+
const agentCount = peerAgentCounts.get(peer.ip);
|
|
6387
|
+
const agentStr = agentCount != null ? String(agentCount) : "\u2014";
|
|
6388
|
+
const statusStr = formatStatus(peer.status);
|
|
6389
|
+
console.log(` ${peer.ip.padEnd(18)} ${statusStr.padEnd(16)} ${agentStr} agents`);
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
function formatStatus(status) {
|
|
6393
|
+
const map = {
|
|
6394
|
+
PARTY_SERVER: "Online",
|
|
6395
|
+
DEGRADED: "Degraded",
|
|
6396
|
+
SUSPECT: "Suspect",
|
|
6397
|
+
DOWN: "Down",
|
|
6398
|
+
UNKNOWN: "Unknown",
|
|
6399
|
+
MAYBE: "Probing",
|
|
6400
|
+
NOT_SERVER: "Not a server"
|
|
6401
|
+
};
|
|
6402
|
+
return map[status] ?? status;
|
|
6403
|
+
}
|
|
6404
|
+
|
|
6405
|
+
// src/cli/install.ts
|
|
6406
|
+
init_tailscale();
|
|
6407
|
+
init_tailscale_installer();
|
|
6408
|
+
async function installCommand() {
|
|
6409
|
+
const status = getTailscaleInstallationStatus();
|
|
6410
|
+
if (status.state === "connected" || status.state === "not_connected") {
|
|
6411
|
+
console.log(green("\u2705 Tailscale is already installed!"), `Binary: ${status.binary}`);
|
|
6412
|
+
if (status.state === "connected") {
|
|
6413
|
+
console.log(` IP: ${status.tailscale_ip} Hostname: ${status.hostname}`);
|
|
6414
|
+
}
|
|
6415
|
+
return;
|
|
6416
|
+
}
|
|
6417
|
+
const platform = process.platform;
|
|
6418
|
+
const info = getInstallInstructions(platform);
|
|
6419
|
+
console.log(bold(cyan("Installing Tailscale...\n")));
|
|
6420
|
+
console.log(` Platform: ${info.os}`);
|
|
6421
|
+
const result = await installTailscale(platform);
|
|
6422
|
+
if (result.success) {
|
|
6423
|
+
console.log(green("\n\u2705 Tailscale installed successfully!"));
|
|
6424
|
+
console.log(" Run: open-party login");
|
|
6425
|
+
} else {
|
|
6426
|
+
console.log(red("\n\u274C Installation failed:"), result.output);
|
|
6427
|
+
console.log(" Install manually: https://tailscale.com/download");
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
|
|
5848
6431
|
// src/cli/index.ts
|
|
5849
6432
|
function showHelp() {
|
|
5850
6433
|
console.log(`Usage: open-party <command> [options]
|
|
@@ -5854,28 +6437,54 @@ Commands:
|
|
|
5854
6437
|
stop Stop the Party Server
|
|
5855
6438
|
status Show server status
|
|
5856
6439
|
setup Interactive setup wizard (Tailscale + agent plugins)
|
|
6440
|
+
login Login to Tailscale network
|
|
6441
|
+
logout Log out of Tailscale network
|
|
6442
|
+
install Install Tailscale
|
|
6443
|
+
agents List connected agents
|
|
6444
|
+
peers List discovered peer nodes
|
|
5857
6445
|
help Show this help message
|
|
5858
6446
|
|
|
5859
6447
|
Options for 'start':
|
|
5860
6448
|
-d, --daemon Run in background (daemon mode)
|
|
5861
6449
|
-p, --port <port> Override port (default: 8000, env: PARTY_PORT)
|
|
5862
6450
|
|
|
6451
|
+
Global options:
|
|
6452
|
+
-v, --version Show version number
|
|
6453
|
+
|
|
5863
6454
|
Examples:
|
|
5864
6455
|
open-party Start server in foreground
|
|
5865
6456
|
open-party start Start server in foreground
|
|
5866
6457
|
open-party start -d Start server in background
|
|
5867
6458
|
open-party start -d -p 9000 Start server in background on port 9000
|
|
5868
6459
|
open-party stop Stop the server
|
|
5869
|
-
open-party status Check if the server is running
|
|
6460
|
+
open-party status Check if the server is running
|
|
6461
|
+
open-party login Login to Tailscale
|
|
6462
|
+
open-party logout Log out of Tailscale
|
|
6463
|
+
open-party install Install Tailscale
|
|
6464
|
+
open-party agents List connected agents
|
|
6465
|
+
open-party peers List discovered peer nodes`);
|
|
5870
6466
|
}
|
|
5871
6467
|
var args = process.argv.slice(2);
|
|
5872
6468
|
var command = args[0] ?? "start";
|
|
5873
6469
|
var commandArgs = args.slice(1);
|
|
5874
6470
|
async function main2() {
|
|
6471
|
+
if (command === "--version" || command === "-v") {
|
|
6472
|
+
showVersion();
|
|
6473
|
+
process.exit(0);
|
|
6474
|
+
}
|
|
5875
6475
|
switch (command) {
|
|
5876
6476
|
case "setup":
|
|
5877
6477
|
await setupCommand();
|
|
5878
6478
|
break;
|
|
6479
|
+
case "login":
|
|
6480
|
+
await loginCommand();
|
|
6481
|
+
break;
|
|
6482
|
+
case "logout":
|
|
6483
|
+
await logoutCommand();
|
|
6484
|
+
break;
|
|
6485
|
+
case "install":
|
|
6486
|
+
await installCommand();
|
|
6487
|
+
break;
|
|
5879
6488
|
case "start":
|
|
5880
6489
|
await startServer(commandArgs);
|
|
5881
6490
|
break;
|
|
@@ -5885,6 +6494,12 @@ async function main2() {
|
|
|
5885
6494
|
case "status":
|
|
5886
6495
|
await statusCommand();
|
|
5887
6496
|
break;
|
|
6497
|
+
case "agents":
|
|
6498
|
+
await agentsCommand();
|
|
6499
|
+
break;
|
|
6500
|
+
case "peers":
|
|
6501
|
+
await peersCommand();
|
|
6502
|
+
break;
|
|
5888
6503
|
case "help":
|
|
5889
6504
|
case "--help":
|
|
5890
6505
|
case "-h":
|