@vaishnavkm/flutterbridge 0.1.0 → 0.1.1

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 (3) hide show
  1. package/README.md +9 -6
  2. package/index.js +137 -28
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -10,26 +10,26 @@ Bridge your Flutter code to your phone instantly. No USB cables. No complex ADB
10
10
 
11
11
  ```bash
12
12
  # Using npm
13
- npm install -g flutterbridge
13
+ npm install -g @vaishnavkm/flutterbridge
14
14
 
15
15
  # Using pnpm
16
- pnpm add -g flutterbridge
16
+ pnpm add -g @vaishnavkm/flutterbridge
17
17
 
18
18
  # Using bun
19
- bun add -g flutterbridge
19
+ bun add -g @vaishnavkm/flutterbridge
20
20
  ```
21
21
 
22
22
  ### One-time Use (No Installation)
23
23
 
24
24
  ```bash
25
25
  # Using npm
26
- npx flutterbridge
26
+ npx @vaishnavkm/flutterbridge
27
27
 
28
28
  # Using pnpm
29
- pnpm dlx flutterbridge
29
+ pnpm dlx @vaishnavkm/flutterbridge
30
30
 
31
31
  # Using bun
32
- bunx flutterbridge
32
+ bunx @vaishnavkm/flutterbridge
33
33
  ```
34
34
 
35
35
  ## Usage
@@ -42,6 +42,9 @@ flutterbridge
42
42
 
43
43
  A QR code will appear in your terminal. Scan it with the FlutterBridge companion app to connect.
44
44
 
45
+ If the VM service is bound to localhost, FlutterBridge will start a LAN proxy
46
+ and encode that address in the QR so phones can connect over WiFi.
47
+
45
48
  ## CLI Options
46
49
 
47
50
  ```bash
package/index.js CHANGED
@@ -7,6 +7,7 @@ const readline = require('readline');
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
  const os = require('os');
10
+ const net = require('net');
10
11
 
11
12
  const VM_URL_TIMEOUT_MS = 60000;
12
13
  const SERVICE_URI_KEYS = [
@@ -18,6 +19,8 @@ const SERVICE_URI_KEYS = [
18
19
  'wsUri',
19
20
  ];
20
21
 
22
+ const activeProxies = new Set();
23
+
21
24
  function parseArgs(argv) {
22
25
  let deviceId = null;
23
26
  let qrOnly = false;
@@ -169,25 +172,103 @@ function getLanIp() {
169
172
  return candidates[0];
170
173
  }
171
174
 
172
- function rewriteVmServiceUrl(originalUrl) {
173
- try {
174
- const parsed = new URL(originalUrl);
175
- if (!isLoopbackHost(parsed.hostname)) {
176
- return { url: originalUrl, replaced: false };
177
- }
175
+ function getDefaultPort(protocol) {
176
+ if (protocol === 'wss:' || protocol === 'https:') {
177
+ return 443;
178
+ }
179
+ if (protocol === 'ws:' || protocol === 'http:') {
180
+ return 80;
181
+ }
182
+ return null;
183
+ }
184
+
185
+ function registerProxy(server) {
186
+ activeProxies.add(server);
187
+ server.on('close', () => activeProxies.delete(server));
188
+ }
178
189
 
179
- const lanIp = getLanIp();
180
- if (!lanIp) {
181
- console.warn(chalk.yellow('\n⚠️ Warning: Could not detect LAN IP address.'));
182
- console.warn(chalk.yellow('Make sure your PC and phone are on the same WiFi network.'));
183
- console.warn(chalk.yellow('Connection may fail with localhost URL.\n'));
184
- return { url: originalUrl, replaced: false };
190
+ function closeAllProxies() {
191
+ for (const server of activeProxies) {
192
+ try {
193
+ server.close();
194
+ } catch (_) {
195
+ // Ignore errors while shutting down.
185
196
  }
197
+ }
198
+ activeProxies.clear();
199
+ }
186
200
 
187
- parsed.hostname = lanIp;
188
- return { url: parsed.toString(), replaced: true };
201
+ process.on('exit', closeAllProxies);
202
+
203
+ function startTcpProxy(targetHost, targetPort) {
204
+ return new Promise((resolve, reject) => {
205
+ const server = net.createServer((client) => {
206
+ const upstream = net.connect({ host: targetHost, port: targetPort });
207
+
208
+ const closeBoth = () => {
209
+ client.destroy();
210
+ upstream.destroy();
211
+ };
212
+
213
+ client.on('error', closeBoth);
214
+ upstream.on('error', closeBoth);
215
+
216
+ client.pipe(upstream);
217
+ upstream.pipe(client);
218
+ });
219
+
220
+ server.on('error', reject);
221
+
222
+ server.listen(0, '0.0.0.0', () => {
223
+ registerProxy(server);
224
+ const address = server.address();
225
+ resolve({ server, port: address.port });
226
+ });
227
+ });
228
+ }
229
+
230
+ async function prepareVmServiceUrl(originalUrl) {
231
+ let parsed;
232
+ try {
233
+ parsed = new URL(originalUrl);
189
234
  } catch (_) {
190
- return { url: originalUrl, replaced: false };
235
+ return { url: originalUrl, originalUrl, replaced: false, proxied: false };
236
+ }
237
+
238
+ if (!isLoopbackHost(parsed.hostname)) {
239
+ return { url: originalUrl, originalUrl, replaced: false, proxied: false };
240
+ }
241
+
242
+ const lanIp = getLanIp();
243
+ if (!lanIp) {
244
+ console.warn(chalk.yellow('\n⚠️ Warning: Could not detect LAN IP address.'));
245
+ console.warn(chalk.yellow('Make sure your PC and phone are on the same WiFi network.'));
246
+ console.warn(chalk.yellow('Connection may fail with localhost URL.\n'));
247
+ return { url: originalUrl, originalUrl, replaced: false, proxied: false };
248
+ }
249
+
250
+ const port = parsed.port ? Number(parsed.port) : getDefaultPort(parsed.protocol);
251
+ if (!port) {
252
+ return { url: originalUrl, originalUrl, replaced: false, proxied: false };
253
+ }
254
+
255
+ const targetHost = parsed.hostname === '0.0.0.0' ? '127.0.0.1' : parsed.hostname;
256
+ try {
257
+ const proxy = await startTcpProxy(targetHost, port);
258
+ parsed.hostname = lanIp;
259
+ parsed.port = String(proxy.port);
260
+ return {
261
+ url: parsed.toString(),
262
+ originalUrl,
263
+ replaced: true,
264
+ proxied: true,
265
+ proxyHost: lanIp,
266
+ proxyPort: proxy.port,
267
+ };
268
+ } catch (err) {
269
+ console.warn(chalk.yellow(`\n⚠️ Warning: Failed to start LAN proxy (${err.message}).`));
270
+ parsed.hostname = lanIp;
271
+ return { url: parsed.toString(), originalUrl, replaced: true, proxied: false };
191
272
  }
192
273
  }
193
274
 
@@ -395,6 +476,7 @@ async function main() {
395
476
  let stdoutBuffer = '';
396
477
  let stderrBuffer = '';
397
478
  let vmServiceUrl = null;
479
+ let publishPending = false;
398
480
 
399
481
  const vmTimeout = setTimeout(() => {
400
482
  if (!vmServiceUrl) {
@@ -420,29 +502,56 @@ async function main() {
420
502
  const events = Array.isArray(json) ? json : [json];
421
503
  for (const event of events) {
422
504
  const url = extractVmServiceUri(event);
423
- if (url && !vmServiceUrl) {
424
- const rewritten = rewriteVmServiceUrl(url);
425
- vmServiceUrl = rewritten.url;
426
- clearTimeout(vmTimeout);
427
- if (jsonOutput) {
428
- const payload = { vmServiceUri: vmServiceUrl, deviceId };
429
- if (rewritten.replaced) {
430
- payload.originalVmServiceUri = url;
505
+ if (url && !vmServiceUrl && !publishPending) {
506
+ publishPending = true;
507
+
508
+ const publishUrl = (result) => {
509
+ if (vmServiceUrl) {
510
+ return;
431
511
  }
432
- console.log(JSON.stringify(payload));
433
- } else {
512
+ vmServiceUrl = result.url;
513
+ clearTimeout(vmTimeout);
514
+
515
+ if (jsonOutput) {
516
+ const payload = { vmServiceUri: vmServiceUrl, deviceId };
517
+ if (result.replaced) {
518
+ payload.originalVmServiceUri = result.originalUrl;
519
+ }
520
+ if (result.proxied) {
521
+ payload.proxy = { host: result.proxyHost, port: result.proxyPort };
522
+ }
523
+ console.log(JSON.stringify(payload));
524
+ return;
525
+ }
526
+
434
527
  if (!qrOnly) {
435
528
  console.log(chalk.yellow('\nScan this QR with FlutterBridge app:\n'));
436
529
  }
437
530
  qrcode.generate(vmServiceUrl, { small: true });
438
531
  if (!qrOnly) {
439
- if (rewritten.replaced) {
532
+ if (result.proxied) {
533
+ console.log(chalk.gray(`LAN proxy running at ws://${result.proxyHost}:${result.proxyPort}`));
534
+ console.log(chalk.gray(`Proxy target: ${result.originalUrl}`));
535
+ } else if (result.replaced) {
440
536
  console.log(chalk.gray(`Rewrote VM URL for LAN access: ${vmServiceUrl}`));
441
- console.log(chalk.gray(`Original VM URL: ${url}`));
537
+ console.log(chalk.gray(`Original VM URL: ${result.originalUrl}`));
442
538
  }
443
539
  console.log(chalk.green(`\nVM URL: ${vmServiceUrl}`));
444
540
  }
445
- }
541
+ };
542
+
543
+ prepareVmServiceUrl(url)
544
+ .then((result) => {
545
+ publishPending = false;
546
+ publishUrl(result);
547
+ })
548
+ .catch((err) => {
549
+ publishPending = false;
550
+ if (!quiet) {
551
+ console.error(chalk.red(`Failed to prepare VM service URL: ${err.message}`));
552
+ }
553
+ publishUrl({ url, originalUrl: url, replaced: false, proxied: false });
554
+ });
446
555
  }
447
556
  }
448
557
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@vaishnavkm/flutterbridge",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Bridge your Flutter code to your phone instantly. Wireless development with QR code pairing.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "flutterbridge": "./index.js"
7
+ "flutterbridge": "index.js"
8
8
  },
9
9
  "publishConfig": {
10
10
  "access": "public"
@@ -28,7 +28,7 @@
28
28
  "license": "MIT",
29
29
  "repository": {
30
30
  "type": "git",
31
- "url": "https://github.com/vaishnavkm/flutterbridge.git"
31
+ "url": "git+https://github.com/vaishnavkm/flutterbridge.git"
32
32
  },
33
33
  "bugs": {
34
34
  "url": "https://github.com/vaishnavkm/flutterbridge/issues"