@vaishnavkm/flutterbridge 0.1.0
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/README.md +99 -0
- package/index.js +475 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# FlutterBridge CLI
|
|
2
|
+
|
|
3
|
+
> Wireless Flutter development with QR code pairing
|
|
4
|
+
|
|
5
|
+
Bridge your Flutter code to your phone instantly. No USB cables. No complex ADB setup.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### Global Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Using npm
|
|
13
|
+
npm install -g flutterbridge
|
|
14
|
+
|
|
15
|
+
# Using pnpm
|
|
16
|
+
pnpm add -g flutterbridge
|
|
17
|
+
|
|
18
|
+
# Using bun
|
|
19
|
+
bun add -g flutterbridge
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### One-time Use (No Installation)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Using npm
|
|
26
|
+
npx flutterbridge
|
|
27
|
+
|
|
28
|
+
# Using pnpm
|
|
29
|
+
pnpm dlx flutterbridge
|
|
30
|
+
|
|
31
|
+
# Using bun
|
|
32
|
+
bunx flutterbridge
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Navigate to your Flutter project directory and run:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
flutterbridge
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A QR code will appear in your terminal. Scan it with the FlutterBridge companion app to connect.
|
|
44
|
+
|
|
45
|
+
## CLI Options
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Choose a specific device
|
|
49
|
+
flutterbridge --device <device-id>
|
|
50
|
+
flutterbridge -d <device-id>
|
|
51
|
+
|
|
52
|
+
# Print only the QR code (no extra logs)
|
|
53
|
+
flutterbridge --qr-only
|
|
54
|
+
|
|
55
|
+
# Print machine-readable JSON output
|
|
56
|
+
flutterbridge --json
|
|
57
|
+
|
|
58
|
+
# Pass additional Flutter flags
|
|
59
|
+
flutterbridge -- --release
|
|
60
|
+
flutterbridge -- --flavor production
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- Node.js >= 18
|
|
66
|
+
- Flutter >= 3.0
|
|
67
|
+
- Package manager: npm, pnpm, or bun
|
|
68
|
+
- Both PC and phone on the same WiFi network
|
|
69
|
+
|
|
70
|
+
## Troubleshooting
|
|
71
|
+
|
|
72
|
+
### "Flutter was not found on your PATH"
|
|
73
|
+
Install Flutter: https://flutter.dev/docs/get-started/install
|
|
74
|
+
|
|
75
|
+
### "No pubspec.yaml file found"
|
|
76
|
+
Run the command from the root of your Flutter project.
|
|
77
|
+
|
|
78
|
+
### "No devices found"
|
|
79
|
+
- Connect a physical device via USB
|
|
80
|
+
- Start an Android/iOS emulator
|
|
81
|
+
- Run `flutter devices` to verify
|
|
82
|
+
|
|
83
|
+
### "Found offline/unauthorized devices"
|
|
84
|
+
- Enable USB debugging on your device
|
|
85
|
+
- Run `adb devices` and authorize the device
|
|
86
|
+
- Reconnect your device
|
|
87
|
+
|
|
88
|
+
### Connection fails
|
|
89
|
+
- Ensure both devices are on the same WiFi network
|
|
90
|
+
- Check firewall settings
|
|
91
|
+
- Try disabling VPN
|
|
92
|
+
|
|
93
|
+
## Documentation
|
|
94
|
+
|
|
95
|
+
Full documentation: https://github.com/vaishnavkm/flutterbridge
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT © Vaishnav K M
|
package/index.js
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const qrcode = require('qrcode-terminal');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
const VM_URL_TIMEOUT_MS = 60000;
|
|
12
|
+
const SERVICE_URI_KEYS = [
|
|
13
|
+
'vmServiceUri',
|
|
14
|
+
'observatoryUri',
|
|
15
|
+
'debugServiceUri',
|
|
16
|
+
'debuggerUri',
|
|
17
|
+
'debugWsUri',
|
|
18
|
+
'wsUri',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
let deviceId = null;
|
|
23
|
+
let qrOnly = false;
|
|
24
|
+
let jsonOutput = false;
|
|
25
|
+
const passthrough = [];
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
28
|
+
const arg = argv[i];
|
|
29
|
+
|
|
30
|
+
if (arg === '--device' || arg === '-d') {
|
|
31
|
+
const next = argv[i + 1];
|
|
32
|
+
if (!next || next.startsWith('-')) {
|
|
33
|
+
throw new Error('Missing value for --device.');
|
|
34
|
+
}
|
|
35
|
+
deviceId = next;
|
|
36
|
+
i += 1;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (arg.startsWith('--device=')) {
|
|
41
|
+
const value = arg.split('=').slice(1).join('=');
|
|
42
|
+
if (!value) {
|
|
43
|
+
throw new Error('Missing value for --device.');
|
|
44
|
+
}
|
|
45
|
+
deviceId = value;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (arg === '--qr-only') {
|
|
50
|
+
qrOnly = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (arg === '--json') {
|
|
55
|
+
jsonOutput = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
passthrough.push(arg);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { deviceId, qrOnly, jsonOutput, passthrough };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseMachineLine(line) {
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
if (!trimmed) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(trimmed);
|
|
77
|
+
} catch (_) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isValidUri(value) {
|
|
83
|
+
return typeof value === 'string' && /^(ws|http)s?:\/\//.test(value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findUriByKeys(obj, keys) {
|
|
87
|
+
if (!obj || typeof obj !== 'object') {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const key of keys) {
|
|
92
|
+
if (isValidUri(obj[key])) {
|
|
93
|
+
return obj[key];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function findAnyUri(obj) {
|
|
101
|
+
if (!obj || typeof obj !== 'object') {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
106
|
+
if (/uri/i.test(key) && isValidUri(value)) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function extractVmServiceUri(event) {
|
|
115
|
+
if (!event || typeof event !== 'object') {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const params = event.params || event;
|
|
120
|
+
let uri = findUriByKeys(params, SERVICE_URI_KEYS);
|
|
121
|
+
if (uri) {
|
|
122
|
+
return uri;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const debuggingOptions = params.debuggingOptions || params.debugOptions;
|
|
126
|
+
uri = findUriByKeys(debuggingOptions, SERVICE_URI_KEYS);
|
|
127
|
+
if (uri) {
|
|
128
|
+
return uri;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return findAnyUri(params);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isLoopbackHost(hostname) {
|
|
135
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '0.0.0.0';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function scoreLanIp(ip) {
|
|
139
|
+
if (ip.startsWith('192.168.')) {
|
|
140
|
+
return 3;
|
|
141
|
+
}
|
|
142
|
+
if (ip.startsWith('10.')) {
|
|
143
|
+
return 2;
|
|
144
|
+
}
|
|
145
|
+
if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(ip)) {
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getLanIp() {
|
|
152
|
+
const interfaces = os.networkInterfaces();
|
|
153
|
+
const candidates = [];
|
|
154
|
+
|
|
155
|
+
for (const entries of Object.values(interfaces)) {
|
|
156
|
+
for (const net of entries || []) {
|
|
157
|
+
if (net.family !== 'IPv4' || net.internal) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
candidates.push(net.address);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (candidates.length === 0) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
candidates.sort((a, b) => scoreLanIp(b) - scoreLanIp(a));
|
|
169
|
+
return candidates[0];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function rewriteVmServiceUrl(originalUrl) {
|
|
173
|
+
try {
|
|
174
|
+
const parsed = new URL(originalUrl);
|
|
175
|
+
if (!isLoopbackHost(parsed.hostname)) {
|
|
176
|
+
return { url: originalUrl, replaced: false };
|
|
177
|
+
}
|
|
178
|
+
|
|
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 };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
parsed.hostname = lanIp;
|
|
188
|
+
return { url: parsed.toString(), replaced: true };
|
|
189
|
+
} catch (_) {
|
|
190
|
+
return { url: originalUrl, replaced: false };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function assertFlutterProject(cwd) {
|
|
195
|
+
const pubspecPath = path.join(cwd, 'pubspec.yaml');
|
|
196
|
+
if (!fs.existsSync(pubspecPath)) {
|
|
197
|
+
throw new Error('No pubspec.yaml file found. Run this command from the root of your Flutter project.');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function runFlutterDevices() {
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
const child = spawn('flutter', ['devices', '--machine'], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
204
|
+
let stdout = '';
|
|
205
|
+
let stderr = '';
|
|
206
|
+
|
|
207
|
+
child.stdout.on('data', (data) => {
|
|
208
|
+
stdout += data.toString();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
child.stderr.on('data', (data) => {
|
|
212
|
+
stderr += data.toString();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
child.on('error', (err) => {
|
|
216
|
+
reject(err);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
child.on('close', (code) => {
|
|
220
|
+
if (code !== 0) {
|
|
221
|
+
reject(new Error(stderr || `flutter devices exited with code ${code}`));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const devices = JSON.parse(stdout.trim());
|
|
227
|
+
if (!Array.isArray(devices)) {
|
|
228
|
+
reject(new Error('Unexpected output from flutter devices.'));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
resolve(devices);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
reject(err);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function formatDevice(device) {
|
|
240
|
+
const platform = device.platform || device.platformType || 'unknown';
|
|
241
|
+
return `${device.name} (${platform}) - id: ${device.id}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function promptForDevice(devices) {
|
|
245
|
+
if (!process.stdin.isTTY) {
|
|
246
|
+
throw new Error('Multiple devices detected. Use --device <id> to select one.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(chalk.yellow('\nAvailable devices:'));
|
|
250
|
+
devices.forEach((device, index) => {
|
|
251
|
+
console.log(` ${index + 1}) ${formatDevice(device)}`);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
255
|
+
|
|
256
|
+
const answer = await new Promise((resolve) => {
|
|
257
|
+
rl.question(chalk.cyan('\nSelect a device by number or id: '), resolve);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
rl.close();
|
|
261
|
+
|
|
262
|
+
const trimmed = answer.trim();
|
|
263
|
+
if (!trimmed) {
|
|
264
|
+
throw new Error('No device selected.');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const asIndex = Number(trimmed);
|
|
268
|
+
if (Number.isInteger(asIndex) && asIndex >= 1 && asIndex <= devices.length) {
|
|
269
|
+
return devices[asIndex - 1].id;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const match = devices.find((device) => device.id === trimmed);
|
|
273
|
+
if (match) {
|
|
274
|
+
return match.id;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
throw new Error(`Unknown device selection: ${trimmed}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function resolveDeviceId(deviceIdArg, options = {}) {
|
|
281
|
+
if (deviceIdArg) {
|
|
282
|
+
return deviceIdArg;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let devices = [];
|
|
286
|
+
try {
|
|
287
|
+
devices = await runFlutterDevices();
|
|
288
|
+
} catch (err) {
|
|
289
|
+
if (err.code === 'ENOENT') {
|
|
290
|
+
throw new Error('Flutter was not found on your PATH. Install Flutter and try again.');
|
|
291
|
+
}
|
|
292
|
+
throw err;
|
|
293
|
+
}
|
|
294
|
+
const supported = devices.filter((device) => device.isSupported !== false);
|
|
295
|
+
|
|
296
|
+
if (supported.length === 0) {
|
|
297
|
+
const offline = devices.filter((d) => !d.isSupported && (d.emulator === false || d.emulator === undefined));
|
|
298
|
+
if (offline.length > 0) {
|
|
299
|
+
const hints = offline.map((d) => ` - ${d.name} (${d.id})`).join('\n');
|
|
300
|
+
throw new Error(`No available devices. Found offline/unauthorized devices:\n${hints}\n\nTry:\n - Enable USB debugging on your device\n - Run 'adb devices' and authorize the device\n - Reconnect your device`);
|
|
301
|
+
}
|
|
302
|
+
throw new Error('No devices found. Connect a device or start an emulator.');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (supported.length === 1) {
|
|
306
|
+
if (!options.quiet) {
|
|
307
|
+
console.log(chalk.gray(`Using device: ${formatDevice(supported[0])}`));
|
|
308
|
+
}
|
|
309
|
+
return supported[0].id;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (options.jsonOutput) {
|
|
313
|
+
throw new Error('Multiple devices detected. Use --device <id> with --json.');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return promptForDevice(supported);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function handleFlutterError(err) {
|
|
320
|
+
if (err.code === 'ENOENT') {
|
|
321
|
+
console.error(chalk.red('Flutter was not found on your PATH. Install Flutter and try again.'));
|
|
322
|
+
} else {
|
|
323
|
+
console.error(chalk.red(`Failed to start Flutter: ${err.message}`));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
process.exitCode = 1;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function hasWebHostnameFlag(args) {
|
|
330
|
+
return args.some((arg) => arg === '--web-hostname' || arg.startsWith('--web-hostname='));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function main() {
|
|
334
|
+
let options = null;
|
|
335
|
+
try {
|
|
336
|
+
options = parseArgs(process.argv.slice(2));
|
|
337
|
+
} catch (err) {
|
|
338
|
+
console.error(chalk.red(err.message));
|
|
339
|
+
process.exitCode = 1;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const {
|
|
344
|
+
deviceId: deviceIdArg,
|
|
345
|
+
passthrough,
|
|
346
|
+
qrOnly,
|
|
347
|
+
jsonOutput,
|
|
348
|
+
} = options;
|
|
349
|
+
|
|
350
|
+
if (qrOnly && jsonOutput) {
|
|
351
|
+
console.error(chalk.red('Use either --qr-only or --json, not both.'));
|
|
352
|
+
process.exitCode = 1;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const quiet = qrOnly || jsonOutput;
|
|
357
|
+
|
|
358
|
+
if (!quiet) {
|
|
359
|
+
console.log(chalk.cyan.bold('\n🚀 FlutterBridge CLI'));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
assertFlutterProject(process.cwd());
|
|
364
|
+
} catch (err) {
|
|
365
|
+
console.error(chalk.red(err.message));
|
|
366
|
+
process.exitCode = 1;
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let deviceId = null;
|
|
371
|
+
try {
|
|
372
|
+
deviceId = await resolveDeviceId(deviceIdArg, { quiet, jsonOutput });
|
|
373
|
+
} catch (err) {
|
|
374
|
+
console.error(chalk.red(`Device selection failed: ${err.message}`));
|
|
375
|
+
process.exitCode = 1;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!quiet) {
|
|
380
|
+
console.log(chalk.gray('Starting Flutter...\n'));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const flutterArgs = ['run', '--machine'];
|
|
384
|
+
if (deviceId) {
|
|
385
|
+
flutterArgs.push('-d', deviceId);
|
|
386
|
+
}
|
|
387
|
+
if (deviceId === 'chrome' && !hasWebHostnameFlag(passthrough)) {
|
|
388
|
+
flutterArgs.push('--web-hostname', '0.0.0.0');
|
|
389
|
+
}
|
|
390
|
+
if (passthrough.length > 0) {
|
|
391
|
+
flutterArgs.push(...passthrough);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const flutter = spawn('flutter', flutterArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
395
|
+
let stdoutBuffer = '';
|
|
396
|
+
let stderrBuffer = '';
|
|
397
|
+
let vmServiceUrl = null;
|
|
398
|
+
|
|
399
|
+
const vmTimeout = setTimeout(() => {
|
|
400
|
+
if (!vmServiceUrl) {
|
|
401
|
+
console.error(chalk.red(`Timed out after ${VM_URL_TIMEOUT_MS / 1000}s waiting for VM service URL.`));
|
|
402
|
+
flutter.kill('SIGINT');
|
|
403
|
+
process.exitCode = 1;
|
|
404
|
+
}
|
|
405
|
+
}, VM_URL_TIMEOUT_MS);
|
|
406
|
+
|
|
407
|
+
flutter.on('error', handleFlutterError);
|
|
408
|
+
|
|
409
|
+
const handleMachineChunk = (buffer, data) => {
|
|
410
|
+
let nextBuffer = buffer + data.toString();
|
|
411
|
+
const lines = nextBuffer.split(/\r?\n/);
|
|
412
|
+
nextBuffer = lines.pop();
|
|
413
|
+
|
|
414
|
+
for (const line of lines) {
|
|
415
|
+
const json = parseMachineLine(line);
|
|
416
|
+
if (!json) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const events = Array.isArray(json) ? json : [json];
|
|
421
|
+
for (const event of events) {
|
|
422
|
+
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;
|
|
431
|
+
}
|
|
432
|
+
console.log(JSON.stringify(payload));
|
|
433
|
+
} else {
|
|
434
|
+
if (!qrOnly) {
|
|
435
|
+
console.log(chalk.yellow('\nScan this QR with FlutterBridge app:\n'));
|
|
436
|
+
}
|
|
437
|
+
qrcode.generate(vmServiceUrl, { small: true });
|
|
438
|
+
if (!qrOnly) {
|
|
439
|
+
if (rewritten.replaced) {
|
|
440
|
+
console.log(chalk.gray(`Rewrote VM URL for LAN access: ${vmServiceUrl}`));
|
|
441
|
+
console.log(chalk.gray(`Original VM URL: ${url}`));
|
|
442
|
+
}
|
|
443
|
+
console.log(chalk.green(`\nVM URL: ${vmServiceUrl}`));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return nextBuffer;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
flutter.stdout.on('data', (data) => {
|
|
454
|
+
stdoutBuffer = handleMachineChunk(stdoutBuffer, data);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
flutter.stderr.on('data', (data) => {
|
|
458
|
+
stderrBuffer = handleMachineChunk(stderrBuffer, data);
|
|
459
|
+
if (!quiet) {
|
|
460
|
+
process.stdout.write(chalk.gray(data.toString()));
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
flutter.on('close', (code) => {
|
|
465
|
+
clearTimeout(vmTimeout);
|
|
466
|
+
if (code && code !== 0) {
|
|
467
|
+
process.exitCode = code;
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
main().catch((err) => {
|
|
473
|
+
console.error(chalk.red(`Unexpected error: ${err.message}`));
|
|
474
|
+
process.exitCode = 1;
|
|
475
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vaishnavkm/flutterbridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bridge your Flutter code to your phone instantly. Wireless development with QR code pairing.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"flutterbridge": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"flutter",
|
|
17
|
+
"mobile",
|
|
18
|
+
"development",
|
|
19
|
+
"wireless",
|
|
20
|
+
"hot-reload",
|
|
21
|
+
"qr-code",
|
|
22
|
+
"cli",
|
|
23
|
+
"developer-tools",
|
|
24
|
+
"expo",
|
|
25
|
+
"flutter-tools"
|
|
26
|
+
],
|
|
27
|
+
"author": "Vaishnav K M",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/vaishnavkm/flutterbridge.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/vaishnavkm/flutterbridge/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/vaishnavkm/flutterbridge#readme",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"packageManager": "pnpm@10.33.0",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"chalk": "^4.1.2",
|
|
43
|
+
"qrcode-terminal": "^0.12.0"
|
|
44
|
+
}
|
|
45
|
+
}
|