apiblaze 0.1.12 → 0.1.16

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.
Files changed (2) hide show
  1. package/dist/index.js +164 -199
  2. package/package.json +5 -3
package/dist/index.js CHANGED
@@ -69,9 +69,6 @@ function getAccessToken() {
69
69
  }
70
70
  return creds.accessToken;
71
71
  }
72
- function getApiblazeDir() {
73
- return APIBLAZE_DIR;
74
- }
75
72
  var fs, os, path, APIBLAZE_DIR, CREDENTIALS_PATH;
76
73
  var init_auth = __esm({
77
74
  "src/lib/auth.ts"() {
@@ -95,9 +92,9 @@ __export(api_exports, {
95
92
  getTeams: () => getTeams,
96
93
  putDevTunnel: () => putDevTunnel
97
94
  });
98
- async function apiFetch(path3, options = {}) {
95
+ async function apiFetch(path2, options = {}) {
99
96
  const token = getAccessToken();
100
- const url = `${DASHBOARD_BASE}${path3}`;
97
+ const url = `${DASHBOARD_BASE}${path2}`;
101
98
  const res = await fetch(url, {
102
99
  ...options,
103
100
  headers: {
@@ -169,6 +166,11 @@ var init_api = __esm({
169
166
  // src/index.ts
170
167
  var import_commander = require("commander");
171
168
  var import_chalk7 = __toESM(require("chalk"));
169
+
170
+ // package.json
171
+ var version = "0.1.16";
172
+
173
+ // src/index.ts
172
174
  init_types();
173
175
 
174
176
  // src/commands/login.ts
@@ -288,125 +290,11 @@ ${import_chalk.default.cyan("\u2192")} Team: ${import_chalk.default.bold(teamNam
288
290
 
289
291
  // src/commands/dev.ts
290
292
  var import_chalk3 = __toESM(require("chalk"));
291
- var import_ora3 = __toESM(require("ora"));
293
+ var import_ora2 = __toESM(require("ora"));
292
294
  var import_inquirer = __toESM(require("inquirer"));
293
295
  init_auth();
294
296
  init_api();
295
297
 
296
- // src/lib/cloudflared.ts
297
- var fs2 = __toESM(require("fs"));
298
- var path2 = __toESM(require("path"));
299
- var readline = __toESM(require("readline"));
300
- var import_child_process = require("child_process");
301
- var import_ora2 = __toESM(require("ora"));
302
- init_auth();
303
- var TUNNEL_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
304
- var DOWNLOAD_TIMEOUT_MS = 3e4;
305
- var TUNNEL_START_TIMEOUT_MS = 3e4;
306
- function getCloudflaredFilename() {
307
- const platform = process.platform;
308
- const arch = process.arch;
309
- if (platform === "darwin" && arch === "arm64") return "cloudflared-darwin-arm64";
310
- if (platform === "darwin" && arch === "x64") return "cloudflared-darwin-amd64";
311
- if (platform === "linux" && arch === "arm64") return "cloudflared-linux-arm64";
312
- if (platform === "linux" && arch === "x64") return "cloudflared-linux-amd64";
313
- if (platform === "win32" && arch === "x64") return "cloudflared-windows-amd64.exe";
314
- throw new Error(`Unsupported platform: ${platform} ${arch}`);
315
- }
316
- function getCloudflaredBinPath() {
317
- const filename = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
318
- return path2.join(getApiblazeDir(), "bin", filename);
319
- }
320
- function isExecutable(filePath) {
321
- try {
322
- fs2.accessSync(filePath, fs2.constants.X_OK);
323
- return true;
324
- } catch {
325
- return false;
326
- }
327
- }
328
- async function downloadCloudflared(binPath) {
329
- const filename = getCloudflaredFilename();
330
- const url = `https://github.com/cloudflare/cloudflared/releases/latest/download/${filename}`;
331
- const res = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
332
- if (!res.ok || !res.body) {
333
- throw new Error(`Failed to download cloudflared: ${res.status} ${res.statusText}`);
334
- }
335
- fs2.mkdirSync(path2.dirname(binPath), { recursive: true });
336
- const tmpPath = `${binPath}.tmp`;
337
- const writer = fs2.createWriteStream(tmpPath);
338
- const reader = res.body.getReader();
339
- try {
340
- while (true) {
341
- const { done, value } = await reader.read();
342
- if (done) break;
343
- await new Promise((resolve, reject) => {
344
- writer.write(value, (err) => err ? reject(err) : resolve());
345
- });
346
- }
347
- } finally {
348
- reader.releaseLock();
349
- }
350
- await new Promise((resolve, reject) => {
351
- writer.end((err) => err ? reject(err) : resolve());
352
- });
353
- fs2.renameSync(tmpPath, binPath);
354
- if (process.platform !== "win32") {
355
- fs2.chmodSync(binPath, 493);
356
- }
357
- }
358
- async function ensureCloudflared() {
359
- const binPath = getCloudflaredBinPath();
360
- if (fs2.existsSync(binPath) && isExecutable(binPath)) {
361
- return;
362
- }
363
- const spinner = (0, import_ora2.default)("Downloading cloudflared...").start();
364
- try {
365
- await downloadCloudflared(binPath);
366
- spinner.succeed("cloudflared downloaded.");
367
- } catch (err) {
368
- spinner.fail("Failed to download cloudflared.");
369
- throw err;
370
- }
371
- }
372
- function spawnCloudflared(port) {
373
- return new Promise((resolve, reject) => {
374
- const binPath = getCloudflaredBinPath();
375
- const proc = (0, import_child_process.spawn)(binPath, ["tunnel", "--url", `http://localhost:${port}`], {
376
- stdio: ["ignore", "ignore", "pipe"]
377
- });
378
- const timer = setTimeout(() => {
379
- proc.kill();
380
- reject(new Error("Timed out waiting for cloudflared tunnel URL (30s). Is something already running on the port?"));
381
- }, TUNNEL_START_TIMEOUT_MS);
382
- const rl = readline.createInterface({ input: proc.stderr });
383
- rl.on("line", (line) => {
384
- const match = line.match(TUNNEL_URL_RE);
385
- if (match) {
386
- clearTimeout(timer);
387
- rl.close();
388
- resolve({ process: proc, tunnelUrl: match[0] });
389
- }
390
- });
391
- proc.on("error", (err) => {
392
- clearTimeout(timer);
393
- reject(new Error(`Failed to start cloudflared: ${err.message}`));
394
- });
395
- proc.on("exit", (code) => {
396
- clearTimeout(timer);
397
- if (code !== null && code !== 0) {
398
- reject(new Error(`cloudflared exited with code ${code}`));
399
- }
400
- });
401
- });
402
- }
403
- function killCloudflared(proc) {
404
- try {
405
- proc.kill("SIGTERM");
406
- } catch {
407
- }
408
- }
409
-
410
298
  // src/lib/traffic.ts
411
299
  var import_chalk2 = __toESM(require("chalk"));
412
300
  var METHOD_COLORS = {
@@ -441,50 +329,136 @@ function formatLogLine(entry) {
441
329
  return `${import_chalk2.default.gray(`[${ts}]`)} ${colorMethod(entry.method)} ${import_chalk2.default.white(entry.path)} ${import_chalk2.default.gray("\u2192")} ${colorStatus(entry.status)} ${import_chalk2.default.gray(`(${colorLatency(entry.latency)})`)}`;
442
330
  }
443
331
 
444
- // src/lib/proxy-logger.ts
445
- var import_http = __toESM(require("http"));
446
- var import_net = __toESM(require("net"));
447
- function startPassthroughProxy(targetPort, onEntry) {
448
- return new Promise((resolve, reject) => {
449
- const server = import_http.default.createServer((req, res) => {
450
- const start = Date.now();
451
- const method = req.method ?? "GET";
452
- const path3 = req.url ?? "/";
453
- const proxyReq = import_http.default.request(
454
- { host: "127.0.0.1", port: targetPort, method, path: path3, headers: req.headers },
455
- (proxyRes) => {
456
- onEntry({ method, path: path3, status: proxyRes.statusCode ?? 0, latency: Date.now() - start });
457
- res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
458
- proxyRes.pipe(res);
332
+ // src/lib/tunnel-client.ts
333
+ var import_ws = __toESM(require("ws"));
334
+ var CHUNK_BYTES = 512 * 1024;
335
+ var PING_INTERVAL_MS = 6e4;
336
+ var MAX_RECONNECT_DELAY_MS = 15e3;
337
+ var STRIP_HEADERS = /* @__PURE__ */ new Set([
338
+ "host",
339
+ "content-length",
340
+ "connection",
341
+ "keep-alive",
342
+ "transfer-encoding",
343
+ "upgrade",
344
+ "proxy-connection",
345
+ "expect"
346
+ ]);
347
+ function stripHeaders(headers) {
348
+ const out = {};
349
+ for (const key of Object.keys(headers)) {
350
+ if (!STRIP_HEADERS.has(key.toLowerCase())) out[key] = headers[key];
351
+ }
352
+ return out;
353
+ }
354
+ function startTunnelClient(opts) {
355
+ const target = `http://127.0.0.1:${opts.localPort}`;
356
+ const inflight = /* @__PURE__ */ new Map();
357
+ let ws = null;
358
+ let closed = false;
359
+ let reconnects = 0;
360
+ let pingTimer;
361
+ function connect() {
362
+ const url = `${opts.connectUrl}?project=${encodeURIComponent(opts.projectId)}&token=${encodeURIComponent(opts.token)}`;
363
+ const socket = new import_ws.default(url);
364
+ ws = socket;
365
+ socket.on("open", () => {
366
+ reconnects = 0;
367
+ opts.onStatus?.("connected");
368
+ pingTimer = setInterval(() => {
369
+ try {
370
+ socket.send("ping");
371
+ } catch {
459
372
  }
460
- );
461
- proxyReq.on("error", () => {
462
- onEntry({ method, path: path3, status: 502, latency: Date.now() - start });
463
- if (!res.headersSent) res.writeHead(502);
464
- res.end();
465
- });
466
- req.pipe(proxyReq);
373
+ }, PING_INTERVAL_MS);
467
374
  });
468
- server.on("upgrade", (req, clientSocket, head) => {
469
- const upstream = import_net.default.connect(targetPort, "127.0.0.1", () => {
470
- const headerLines = Object.entries(req.headers).map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(", ") : v}`).join("\r\n");
471
- upstream.write(`${req.method} ${req.url} HTTP/1.1\r
472
- ${headerLines}\r
473
- \r
474
- `);
475
- if (head && head.length) upstream.write(head);
476
- upstream.pipe(clientSocket);
477
- clientSocket.pipe(upstream);
478
- });
479
- upstream.on("error", () => clientSocket.destroy());
480
- clientSocket.on("error", () => upstream.destroy());
375
+ socket.on("message", (data) => handleFrame(socket, data.toString()));
376
+ socket.on("close", () => {
377
+ if (pingTimer) clearInterval(pingTimer);
378
+ if (!closed) scheduleReconnect();
481
379
  });
482
- server.on("error", reject);
483
- server.listen(0, "127.0.0.1", () => {
484
- const { port } = server.address();
485
- resolve({ port, close: () => server.close() });
380
+ socket.on("error", () => {
486
381
  });
487
- });
382
+ }
383
+ function scheduleReconnect() {
384
+ const delay = Math.min(1e3 * 2 ** reconnects, MAX_RECONNECT_DELAY_MS);
385
+ reconnects++;
386
+ opts.onStatus?.(`disconnected \u2014 reconnecting in ${Math.round(delay / 1e3)}s`);
387
+ setTimeout(() => {
388
+ if (!closed) connect();
389
+ }, delay);
390
+ }
391
+ function handleFrame(socket, raw) {
392
+ let frame;
393
+ try {
394
+ frame = JSON.parse(raw);
395
+ } catch {
396
+ return;
397
+ }
398
+ if (frame.type === "req") {
399
+ inflight.set(frame.id, {
400
+ method: frame.method,
401
+ path: frame.path,
402
+ headers: frame.headers ?? {},
403
+ chunks: [],
404
+ start: Date.now()
405
+ });
406
+ if (frame.bodyLen === 0) finish(socket, frame.id);
407
+ } else if (frame.type === "chunk") {
408
+ const f = inflight.get(frame.id);
409
+ if (!f) return;
410
+ f.chunks.push(Buffer.from(frame.data ?? "", "base64"));
411
+ if (frame.final) finish(socket, frame.id);
412
+ }
413
+ }
414
+ function finish(socket, id) {
415
+ const f = inflight.get(id);
416
+ if (!f) return;
417
+ inflight.delete(id);
418
+ void forward(socket, id, f);
419
+ }
420
+ async function forward(socket, id, f) {
421
+ const body = Buffer.concat(f.chunks);
422
+ const init = { method: f.method, headers: stripHeaders(f.headers) };
423
+ if (body.length) init.body = body;
424
+ let status = 502;
425
+ try {
426
+ const resp = await fetch(target + f.path, init);
427
+ status = resp.status;
428
+ const buf = Buffer.from(await resp.arrayBuffer());
429
+ const headers = {};
430
+ resp.headers.forEach((value, key) => {
431
+ if (!STRIP_HEADERS.has(key.toLowerCase())) headers[key] = value;
432
+ });
433
+ send(socket, { id, type: "res", status, headers, bodyLen: buf.length });
434
+ for (let off = 0, seq = 0; off < buf.length; off += CHUNK_BYTES) {
435
+ const slice = buf.subarray(off, Math.min(off + CHUNK_BYTES, buf.length));
436
+ send(socket, { id, type: "chunk", seq: seq++, data: slice.toString("base64"), final: off + CHUNK_BYTES >= buf.length });
437
+ }
438
+ } catch (err) {
439
+ const code = err?.cause?.code;
440
+ const message = code === "ECONNREFUSED" || code === "ECONNRESET" || code === "ENOTFOUND" || code === "EHOSTUNREACH" ? `No local server reachable at ${target} \u2014 is your dev server running on port ${opts.localPort}?` : err?.cause?.message || err?.message || String(err);
441
+ send(socket, { id, type: "err", message });
442
+ }
443
+ opts.onEntry({ method: f.method, path: f.path, status, latency: Date.now() - f.start });
444
+ }
445
+ function send(socket, frame) {
446
+ try {
447
+ socket.send(JSON.stringify(frame));
448
+ } catch {
449
+ }
450
+ }
451
+ connect();
452
+ return {
453
+ close() {
454
+ closed = true;
455
+ if (pingTimer) clearInterval(pingTimer);
456
+ try {
457
+ ws?.close();
458
+ } catch {
459
+ }
460
+ }
461
+ };
488
462
  }
489
463
 
490
464
  // src/commands/dev.ts
@@ -519,7 +493,7 @@ async function runDev(options) {
519
493
  }
520
494
  let targets;
521
495
  {
522
- const spinner = (0, import_ora3.default)("Fetching your localhost projects...").start();
496
+ const spinner = (0, import_ora2.default)("Fetching your localhost projects...").start();
523
497
  try {
524
498
  targets = await getLocalhostTargets(teamId);
525
499
  spinner.stop();
@@ -529,8 +503,8 @@ async function runDev(options) {
529
503
  }
530
504
  }
531
505
  if (targets.length === 0) {
532
- console.log(import_chalk3.default.yellow("No projects found with localhost targets."));
533
- console.log("Set a project's upstream target to localhost in your APIblaze dashboard, then try again.");
506
+ console.log(import_chalk3.default.yellow("No projects found with an internal target."));
507
+ console.log("Set a project's upstream target to localhost or a private IP in your APIblaze dashboard, then try again.");
534
508
  process.exit(0);
535
509
  }
536
510
  let selectedTargets;
@@ -538,7 +512,7 @@ async function runDev(options) {
538
512
  const { confirmed } = await import_inquirer.default.prompt([{
539
513
  type: "confirm",
540
514
  name: "confirmed",
541
- message: `Found 1 project targeting localhost \u2014 tunnel "${import_chalk3.default.bold(targets[0].projectName)}" (${targets[0].tenantName})?`,
515
+ message: `Found 1 project with an internal target \u2014 tunnel "${import_chalk3.default.bold(targets[0].projectName)}" (${targets[0].tenantName})?`,
542
516
  default: true
543
517
  }]);
544
518
  if (!confirmed) {
@@ -550,11 +524,11 @@ async function runDev(options) {
550
524
  const { chosen } = await import_inquirer.default.prompt([{
551
525
  type: "checkbox",
552
526
  name: "chosen",
553
- message: `Found ${targets.length} projects targeting localhost \u2014 select which to tunnel:`,
527
+ message: `Found ${targets.length} projects with an internal target \u2014 pick which to tunnel (space to select, a for all):`,
554
528
  choices: targets.map((t) => ({
555
529
  name: `${import_chalk3.default.bold(t.projectName)} (${t.tenantName}) \u2014 ${t.target}`,
556
530
  value: t,
557
- checked: true
531
+ checked: false
558
532
  }))
559
533
  }]);
560
534
  if (chosen.length === 0) {
@@ -568,40 +542,32 @@ async function runDev(options) {
568
542
  Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
569
543
  `)
570
544
  );
571
- await ensureCloudflared();
572
- const logger = await startPassthroughProxy(options.port, (entry) => {
573
- console.log(formatLogLine(entry));
574
- });
575
- let cfProcess;
576
- let tunnelUrl;
577
- {
578
- const spinner = (0, import_ora3.default)("Starting Cloudflare tunnel...").start();
579
- try {
580
- ({ process: cfProcess, tunnelUrl } = await spawnCloudflared(logger.port));
581
- spinner.succeed(`Tunnel active: ${import_chalk3.default.bold.cyan(tunnelUrl)}`);
582
- } catch (err) {
583
- spinner.fail("Failed to start cloudflared tunnel.");
584
- logger.close();
585
- throw err;
586
- }
587
- }
588
545
  let restore = [];
546
+ let connect;
589
547
  {
590
- const spinner = (0, import_ora3.default)("Registering tunnel with APIblaze...").start();
548
+ const spinner = (0, import_ora2.default)("Registering tunnel with APIblaze...").start();
591
549
  try {
592
550
  const result = await putDevTunnel({
593
- tunnelUrl,
594
551
  targets: selectedTargets.map((t) => ({ projectId: t.projectId, tenantId: t.tenantId }))
595
552
  });
596
553
  restore = result.restore ?? [];
597
- spinner.succeed("Tunnel registered. Proxying traffic.");
554
+ connect = result.connect;
555
+ spinner.succeed("Tunnel registered.");
598
556
  } catch (err) {
599
557
  spinner.fail("Failed to register tunnel.");
600
- logger.close();
601
- killCloudflared(cfProcess);
602
558
  throw err;
603
559
  }
604
560
  }
561
+ const clients = connect.projects.map(
562
+ (projectId) => startTunnelClient({
563
+ connectUrl: connect.url,
564
+ token: connect.token,
565
+ projectId,
566
+ localPort: options.port,
567
+ onEntry: (entry) => console.log(formatLogLine(entry)),
568
+ onStatus: (status) => console.log(import_chalk3.default.gray(`[${projectId}] ${status}`))
569
+ })
570
+ );
605
571
  console.log("\n" + import_chalk3.default.gray("\u2500".repeat(60)));
606
572
  console.log(import_chalk3.default.bold("Live traffic") + import_chalk3.default.gray(" (Ctrl+C to stop)"));
607
573
  console.log(import_chalk3.default.gray("\u2500".repeat(60)) + "\n");
@@ -610,10 +576,9 @@ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
610
576
  if (isCleaningUp) return;
611
577
  isCleaningUp = true;
612
578
  console.log(import_chalk3.default.gray("\n\nShutting down..."));
579
+ for (const client of clients) client.close();
613
580
  await deleteDevTunnel(restore).catch(() => {
614
581
  });
615
- logger.close();
616
- killCloudflared(cfProcess);
617
582
  console.log(import_chalk3.default.green("Tunnel stopped."));
618
583
  process.exit(0);
619
584
  }
@@ -625,7 +590,7 @@ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
625
590
 
626
591
  // src/commands/projects.ts
627
592
  var import_chalk4 = __toESM(require("chalk"));
628
- var import_ora4 = __toESM(require("ora"));
593
+ var import_ora3 = __toESM(require("ora"));
629
594
  init_auth();
630
595
  init_api();
631
596
  async function runProjects() {
@@ -662,7 +627,7 @@ async function runProjects() {
662
627
  }
663
628
  console.log(`${import_chalk4.default.cyan("\u2192")} Team: ${import_chalk4.default.bold(teamName ?? teamId)}
664
629
  `);
665
- const spinner = (0, import_ora4.default)("Fetching projects...").start();
630
+ const spinner = (0, import_ora3.default)("Fetching projects...").start();
666
631
  let projects;
667
632
  try {
668
633
  projects = await getProjects(teamId);
@@ -685,7 +650,7 @@ ${projects.length} project${projects.length === 1 ? "" : "s"}`));
685
650
 
686
651
  // src/commands/create.ts
687
652
  var import_chalk5 = __toESM(require("chalk"));
688
- var import_ora5 = __toESM(require("ora"));
653
+ var import_ora4 = __toESM(require("ora"));
689
654
  init_auth();
690
655
  init_api();
691
656
  function normalizeName(raw) {
@@ -764,7 +729,7 @@ async function runCreate(opts = {}) {
764
729
  console.log(import_chalk5.default.yellow(" Name must be at least 3 characters (letters and digits only).\n"));
765
730
  continue;
766
731
  }
767
- const spinner2 = (0, import_ora5.default)("Checking availability...").start();
732
+ const spinner2 = (0, import_ora4.default)("Checking availability...").start();
768
733
  try {
769
734
  const check = await checkProxyName(name, teamId);
770
735
  spinner2.stop();
@@ -820,7 +785,7 @@ async function runCreate(opts = {}) {
820
785
  return;
821
786
  }
822
787
  }
823
- const spinner = !opts.json ? (0, import_ora5.default)("Creating proxy (tenant, keys, dev portal)...").start() : null;
788
+ const spinner = !opts.json ? (0, import_ora4.default)("Creating proxy (tenant, keys, dev portal)...").start() : null;
824
789
  let result;
825
790
  try {
826
791
  result = await createProxy({ name, target_url: targetUrl, auth_type: auth, team_id: teamId });
@@ -829,15 +794,15 @@ async function runCreate(opts = {}) {
829
794
  spinner?.fail("Failed to create proxy.");
830
795
  throw err;
831
796
  }
832
- const version = result.api_version || "1.0.0";
797
+ const version2 = result.api_version || "1.0.0";
833
798
  const keys = result.api_keys ?? {};
834
799
  const adminKey = keys.dev ?? Object.values(keys)[0];
835
- const proxyUrl = `https://${name}.apiblaze.com/${version}/dev`;
800
+ const proxyUrl = `https://${name}.apiblaze.com/${version2}/dev`;
836
801
  const devPortal = result.devPortal ? stripTenantFromPortal(result.devPortal) : void 0;
837
802
  if (opts.json) {
838
803
  process.stdout.write(JSON.stringify({
839
804
  project_id: result.project_id,
840
- api_version: version,
805
+ api_version: version2,
841
806
  proxy_url: proxyUrl,
842
807
  dev_portal: devPortal,
843
808
  api_key: adminKey,
@@ -910,7 +875,7 @@ async function runTeam(arg) {
910
875
 
911
876
  // src/index.ts
912
877
  var program = new import_commander.Command();
913
- program.name("apiblaze").description("APIblaze dev tunnel CLI").version("0.1.11");
878
+ program.name("apiblaze").description("APIblaze dev tunnel CLI").version(version);
914
879
  program.command("login").description("Authenticate with APIblaze").action(async () => {
915
880
  try {
916
881
  await runLogin();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.1.12",
4
- "description": "Dev tunnel CLI for APIblaze — route localhost projects through Cloudflare tunnels",
3
+ "version": "0.1.16",
4
+ "description": "Dev tunnel CLI for APIblaze — route localhost projects through your APIblaze endpoints",
5
5
  "keywords": [
6
6
  "apiblaze",
7
7
  "dev-tunnel",
@@ -32,11 +32,13 @@
32
32
  "chalk": "^4.1.2",
33
33
  "commander": "^11.1.0",
34
34
  "inquirer": "^8.2.6",
35
- "ora": "^5.4.1"
35
+ "ora": "^5.4.1",
36
+ "ws": "^8.17.0"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@types/inquirer": "^8.2.10",
39
40
  "@types/node": "^20.0.0",
41
+ "@types/ws": "^8.5.10",
40
42
  "tsup": "^8.0.0",
41
43
  "typescript": "^5.4.0"
42
44
  }