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.
- package/cli/src/subcommands/menu.ts +224 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
+
}
|