uplink-cli 0.1.0-alpha.1 → 0.1.0-alpha.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.
@@ -652,6 +652,43 @@ export const menuCommand = new Command("menu")
652
652
  }
653
653
  },
654
654
  },
655
+ {
656
+ label: "View Connected (with IPs)",
657
+ action: async () => {
658
+ try {
659
+ // Use the API endpoint which proxies to the relay
660
+ const data = await apiRequest("GET", "/v1/admin/relay-status") as {
661
+ connectedTunnels?: number;
662
+ tunnels?: Array<{ token: string; clientIp: string; targetPort: number; connectedAt: string; connectedFor: string }>;
663
+ timestamp?: string;
664
+ error?: string;
665
+ message?: string;
666
+ };
667
+
668
+ if (data.error) {
669
+ return `❌ Relay error: ${data.error}${data.message ? ` - ${data.message}` : ""}`;
670
+ }
671
+
672
+ if (!data.tunnels || data.tunnels.length === 0) {
673
+ return "No tunnels currently connected to the relay.";
674
+ }
675
+
676
+ const lines = data.tunnels.map((t) =>
677
+ `${truncate(t.token, 12).padEnd(14)} ${t.clientIp.padEnd(16)} ${String(t.targetPort).padEnd(6)} ${t.connectedFor.padEnd(10)} ${truncate(t.connectedAt, 19)}`
678
+ );
679
+
680
+ return [
681
+ `Connected Tunnels: ${data.connectedTunnels}`,
682
+ "",
683
+ "Token Client IP Port Uptime Connected At",
684
+ "-".repeat(75),
685
+ ...lines,
686
+ ].join("\n");
687
+ } catch (err: any) {
688
+ return `❌ Failed to get relay status: ${err.message}`;
689
+ }
690
+ },
691
+ },
655
692
  ],
656
693
  });
657
694
 
@@ -1204,12 +1241,26 @@ function findTunnelClients(): Array<{ pid: number; port: number; token: string }
1204
1241
 
1205
1242
  function runSmoke(script: "smoke:tunnel" | "smoke:db" | "smoke:all" | "test:comprehensive") {
1206
1243
  return new Promise<void>((resolve, reject) => {
1244
+ const projectRoot = join(__dirname, "../../..");
1207
1245
  const env = {
1208
1246
  ...process.env,
1209
1247
  AGENTCLOUD_API_BASE: process.env.AGENTCLOUD_API_BASE ?? "https://api.uplink.spot",
1210
1248
  AGENTCLOUD_TOKEN: process.env.AGENTCLOUD_TOKEN ?? "dev-token",
1211
1249
  };
1212
- const child = spawn("npm", ["run", script], { stdio: "inherit", env });
1250
+
1251
+ // For test:comprehensive, run inline (no subprocess)
1252
+ if (script === "test:comprehensive") {
1253
+ runComprehensiveTest(env).then(resolve).catch(reject);
1254
+ return;
1255
+ }
1256
+
1257
+ // For other scripts, use npm run with shell mode from project root
1258
+ const child = spawn("npm", ["run", script], {
1259
+ stdio: "inherit",
1260
+ env,
1261
+ cwd: projectRoot,
1262
+ shell: true
1263
+ });
1213
1264
  child.on("close", (code) => {
1214
1265
  if (code === 0) {
1215
1266
  resolve();
@@ -1220,3 +1271,175 @@ function runSmoke(script: "smoke:tunnel" | "smoke:db" | "smoke:all" | "test:comp
1220
1271
  child.on("error", (err) => reject(err));
1221
1272
  });
1222
1273
  }
1274
+
1275
+ // Inline comprehensive test (no subprocess needed)
1276
+ async function runComprehensiveTest(env: Record<string, string | undefined>) {
1277
+ const API_BASE = env.AGENTCLOUD_API_BASE || "https://api.uplink.spot";
1278
+ const ADMIN_TOKEN = env.AGENTCLOUD_TOKEN || "";
1279
+
1280
+ const c = {
1281
+ reset: "\x1b[0m",
1282
+ red: "\x1b[31m",
1283
+ green: "\x1b[32m",
1284
+ yellow: "\x1b[33m",
1285
+ blue: "\x1b[34m",
1286
+ };
1287
+
1288
+ let PASSED = 0;
1289
+ let FAILED = 0;
1290
+ let SKIPPED = 0;
1291
+
1292
+ const logPass = (msg: string) => { console.log(`${c.green}✅ PASS${c.reset}: ${msg}`); PASSED++; };
1293
+ const logFail = (msg: string) => { console.log(`${c.red}❌ FAIL${c.reset}: ${msg}`); FAILED++; };
1294
+ const logSkip = (msg: string) => { console.log(`${c.yellow}⏭️ SKIP${c.reset}: ${msg}`); SKIPPED++; };
1295
+ const logInfo = (msg: string) => { console.log(`${c.blue}ℹ️ INFO${c.reset}: ${msg}`); };
1296
+ const logSection = (title: string) => {
1297
+ console.log(`\n${c.blue}═══════════════════════════════════════════════════════════${c.reset}`);
1298
+ console.log(`${c.blue} ${title}${c.reset}`);
1299
+ console.log(`${c.blue}═══════════════════════════════════════════════════════════${c.reset}`);
1300
+ };
1301
+
1302
+ const api = async (method: string, path: string, body?: object, token?: string) => {
1303
+ try {
1304
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
1305
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1306
+
1307
+ const res = await fetch(`${API_BASE}${path}`, {
1308
+ method,
1309
+ headers,
1310
+ body: body ? JSON.stringify(body) : undefined,
1311
+ });
1312
+
1313
+ let responseBody: any;
1314
+ try { responseBody = await res.json(); } catch { responseBody = {}; }
1315
+ return { status: res.status, body: responseBody };
1316
+ } catch (err: any) {
1317
+ return { status: 0, body: { error: err.message } };
1318
+ }
1319
+ };
1320
+
1321
+ console.log("");
1322
+ console.log("╔═══════════════════════════════════════════════════════════╗");
1323
+ console.log("║ UPLINK COMPREHENSIVE TEST SUITE ║");
1324
+ console.log("╚═══════════════════════════════════════════════════════════╝");
1325
+ console.log(`\nAPI Base: ${API_BASE}\n`);
1326
+
1327
+ if (!ADMIN_TOKEN) {
1328
+ console.log(`${c.red}ERROR: AGENTCLOUD_TOKEN not set.${c.reset}`);
1329
+ throw new Error("AGENTCLOUD_TOKEN not set");
1330
+ }
1331
+
1332
+ // 1. Health
1333
+ logSection("1. HEALTH CHECKS");
1334
+ let res = await api("GET", "/health");
1335
+ if (res.status === 200 && res.body?.status === "ok") logPass("GET /health returns 200");
1336
+ else logFail(`GET /health - got ${res.status}`);
1337
+
1338
+ res = await api("GET", "/health/live");
1339
+ if (res.status === 200) logPass("GET /health/live returns 200");
1340
+ else logFail(`GET /health/live - got ${res.status}`);
1341
+
1342
+ // 2. Auth
1343
+ logSection("2. AUTHENTICATION");
1344
+ res = await api("GET", "/v1/me");
1345
+ if (res.status === 401) logPass("Missing token returns 401");
1346
+ else logFail(`Missing token - got ${res.status}`);
1347
+
1348
+ res = await api("GET", "/v1/me", undefined, "invalid-token");
1349
+ if (res.status === 401) logPass("Invalid token returns 401");
1350
+ else logFail(`Invalid token - got ${res.status}`);
1351
+
1352
+ res = await api("GET", "/v1/me", undefined, ADMIN_TOKEN);
1353
+ if (res.status === 200 && res.body?.role === "admin") logPass("Valid admin token works");
1354
+ else logFail(`Admin token - got ${res.status}`);
1355
+
1356
+ // 3. Signup
1357
+ logSection("3. SIGNUP FLOW");
1358
+ let USER_TOKEN = "";
1359
+ let USER_TOKEN_ID = "";
1360
+ res = await api("POST", "/v1/signup", { label: `test-${Date.now()}` });
1361
+ if (res.status === 201 && res.body?.token) {
1362
+ USER_TOKEN = res.body.token;
1363
+ USER_TOKEN_ID = res.body.id;
1364
+ logPass("POST /v1/signup creates token");
1365
+ if (res.body.role === "user") logPass("Signup creates user role");
1366
+ else logFail(`Signup role: ${res.body.role}`);
1367
+ } else if (res.status === 429) {
1368
+ logSkip("Signup rate limited");
1369
+ } else {
1370
+ logFail(`Signup - got ${res.status}`);
1371
+ }
1372
+
1373
+ // 4. Authorization
1374
+ logSection("4. AUTHORIZATION");
1375
+ if (USER_TOKEN) {
1376
+ res = await api("GET", "/v1/admin/stats", undefined, USER_TOKEN);
1377
+ if (res.status === 403) logPass("User blocked from admin endpoint");
1378
+ else logFail(`User accessed admin - got ${res.status}`);
1379
+ } else {
1380
+ logSkip("No user token for auth tests");
1381
+ }
1382
+
1383
+ // 5. Tunnels
1384
+ logSection("5. TUNNEL API");
1385
+ res = await api("GET", "/v1/tunnels", undefined, ADMIN_TOKEN);
1386
+ if (res.status === 200) logPass("GET /v1/tunnels works");
1387
+ else logFail(`Tunnels list - got ${res.status}`);
1388
+
1389
+ res = await api("POST", "/v1/tunnels", { port: 3000 }, ADMIN_TOKEN);
1390
+ if (res.status === 201) {
1391
+ logPass("POST /v1/tunnels creates tunnel");
1392
+ if (res.body?.id) {
1393
+ const delRes = await api("DELETE", `/v1/tunnels/${res.body.id}`, undefined, ADMIN_TOKEN);
1394
+ if (delRes.status === 200) logPass("DELETE tunnel works");
1395
+ else logFail(`Delete tunnel - got ${delRes.status}`);
1396
+ }
1397
+ } else {
1398
+ logFail(`Create tunnel - got ${res.status}`);
1399
+ }
1400
+
1401
+ // 6. Databases
1402
+ logSection("6. DATABASE API");
1403
+ res = await api("GET", "/v1/dbs", undefined, ADMIN_TOKEN);
1404
+ if (res.status === 200) logPass("GET /v1/dbs works");
1405
+ else logFail(`Databases list - got ${res.status}`);
1406
+ logInfo("Skipping DB creation (provisions real resources)");
1407
+
1408
+ // 7. Admin Stats
1409
+ logSection("7. ADMIN STATS");
1410
+ res = await api("GET", "/v1/admin/stats", undefined, ADMIN_TOKEN);
1411
+ if (res.status === 200) {
1412
+ logPass("GET /v1/admin/stats works");
1413
+ if (res.body?.tunnels !== undefined) logPass("Stats include tunnels");
1414
+ if (res.body?.databases !== undefined) logPass("Stats include databases");
1415
+ } else {
1416
+ logFail(`Admin stats - got ${res.status}`);
1417
+ }
1418
+
1419
+ // Cleanup
1420
+ logSection("8. CLEANUP");
1421
+ if (USER_TOKEN_ID) {
1422
+ res = await api("DELETE", `/v1/admin/tokens/${USER_TOKEN_ID}`, undefined, ADMIN_TOKEN);
1423
+ if (res.status === 200) logPass("Cleaned up test token");
1424
+ else logInfo("Could not clean up token");
1425
+ } else {
1426
+ logInfo("No test token to clean up");
1427
+ }
1428
+
1429
+ // Summary
1430
+ logSection("TEST SUMMARY");
1431
+ console.log(`\n ${c.green}Passed${c.reset}: ${PASSED}`);
1432
+ console.log(` ${c.red}Failed${c.reset}: ${FAILED}`);
1433
+ console.log(` ${c.yellow}Skipped${c.reset}: ${SKIPPED}\n`);
1434
+
1435
+ if (FAILED === 0) {
1436
+ console.log(`${c.green}═══════════════════════════════════════════════════════════${c.reset}`);
1437
+ console.log(`${c.green} ✅ ALL TESTS PASSED (${PASSED}/${PASSED + FAILED})${c.reset}`);
1438
+ console.log(`${c.green}═══════════════════════════════════════════════════════════${c.reset}`);
1439
+ } else {
1440
+ console.log(`${c.red}═══════════════════════════════════════════════════════════${c.reset}`);
1441
+ console.log(`${c.red} ❌ SOME TESTS FAILED (${FAILED}/${PASSED + FAILED})${c.reset}`);
1442
+ console.log(`${c.red}═══════════════════════════════════════════════════════════${c.reset}`);
1443
+ throw new Error(`${FAILED} tests failed`);
1444
+ }
1445
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uplink-cli",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.3",
4
4
  "description": "Localhost to public URL in seconds. No signup forms, no browser - everything in your terminal.",
5
5
  "keywords": [
6
6
  "tunnel",