@o-town/local.dev 1.1.0 → 1.2.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/bin/cli.js +74 -118
- package/lib/index.js +8 -35
- package/lib/tunnel.js +123 -46
- package/lib/ui.js +48 -25
- package/package.json +3 -14
package/bin/cli.js
CHANGED
|
@@ -1,136 +1,92 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
2
|
'use strict';
|
|
4
3
|
|
|
5
|
-
const yargs = require('yargs/yargs');
|
|
6
|
-
const { hideBin } = require('yargs/helpers');
|
|
7
|
-
const chalk = require('chalk');
|
|
8
|
-
const ora = require('ora');
|
|
9
|
-
const qrcode = require('qrcode-terminal');
|
|
10
4
|
const { createTunnel } = require('../lib/tunnel');
|
|
11
|
-
const { printBanner, printSuccess, printInfo, printError } = require('../lib/ui');
|
|
12
|
-
const
|
|
13
|
-
const pkg = require('../package.json');
|
|
14
|
-
|
|
15
|
-
// Check for updates silently
|
|
16
|
-
updateNotifier({ pkg }).notify();
|
|
17
|
-
|
|
18
|
-
const argv = yargs(hideBin(process.argv))
|
|
19
|
-
.usage(`\n${chalk.cyan('local.dev')} — expose your local server to the internet\n\nUsage: $0 [port] [options]`)
|
|
20
|
-
.command('$0 [port]', 'Start a tunnel to your local server', (yargs) => {
|
|
21
|
-
yargs.positional('port', {
|
|
22
|
-
describe: 'Local port to expose',
|
|
23
|
-
type: 'number',
|
|
24
|
-
default: 3000,
|
|
25
|
-
});
|
|
26
|
-
})
|
|
27
|
-
.option('subdomain', {
|
|
28
|
-
alias: 's',
|
|
29
|
-
type: 'string',
|
|
30
|
-
describe: 'Request a specific subdomain (e.g. myapp)',
|
|
31
|
-
})
|
|
32
|
-
.option('host', {
|
|
33
|
-
alias: 'H',
|
|
34
|
-
type: 'string',
|
|
35
|
-
describe: 'Local host to tunnel (default: localhost)',
|
|
36
|
-
default: 'localhost',
|
|
37
|
-
})
|
|
38
|
-
.option('qr', {
|
|
39
|
-
alias: 'q',
|
|
40
|
-
type: 'boolean',
|
|
41
|
-
describe: 'Show QR code for the tunnel URL',
|
|
42
|
-
default: false,
|
|
43
|
-
})
|
|
44
|
-
.option('open', {
|
|
45
|
-
alias: 'o',
|
|
46
|
-
type: 'boolean',
|
|
47
|
-
describe: 'Open the tunnel URL in browser',
|
|
48
|
-
default: false,
|
|
49
|
-
})
|
|
50
|
-
.option('print-requests', {
|
|
51
|
-
alias: 'r',
|
|
52
|
-
type: 'boolean',
|
|
53
|
-
describe: 'Print incoming requests to stdout',
|
|
54
|
-
default: false,
|
|
55
|
-
})
|
|
56
|
-
.example('$0 3000', 'Expose localhost:3000')
|
|
57
|
-
.example('$0 8080 --subdomain myapp', 'Expose localhost:8080 as myapp.loca.lt')
|
|
58
|
-
.example('$0 5000 --qr', 'Show QR code for mobile testing')
|
|
59
|
-
.epilog(chalk.gray('Docs: https://localdev.sh • Issues: https://github.com/yourusername/local.dev'))
|
|
60
|
-
.argv;
|
|
5
|
+
const { printBanner, printSuccess, printInfo, printError, printRequest } = require('../lib/ui');
|
|
6
|
+
const { version } = require('../package.json');
|
|
61
7
|
|
|
62
|
-
|
|
63
|
-
|
|
8
|
+
// ─── Minimal arg parser (no yargs) ───────────────────────────────────────────
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
|
|
11
|
+
function getFlag(short, long) {
|
|
12
|
+
const i = args.findIndex(a => a === short || a === long);
|
|
13
|
+
if (i === -1) return null;
|
|
14
|
+
return args[i + 1] || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hasFlag(short, long) {
|
|
18
|
+
return args.includes(short) || (long ? args.includes(long) : false);
|
|
19
|
+
}
|
|
64
20
|
|
|
65
|
-
|
|
66
|
-
|
|
21
|
+
if (hasFlag('-v', '--version')) {
|
|
22
|
+
process.stdout.write(version + '\n');
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
67
25
|
|
|
68
|
-
|
|
26
|
+
if (hasFlag('-h', '--help')) {
|
|
27
|
+
process.stdout.write(`
|
|
28
|
+
Usage: local.dev [port] [options]
|
|
69
29
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
color: 'cyan',
|
|
73
|
-
}).start();
|
|
30
|
+
Arguments:
|
|
31
|
+
port Local port to expose (default: 3000)
|
|
74
32
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
});
|
|
33
|
+
Options:
|
|
34
|
+
-s, --subdomain Request a specific subdomain
|
|
35
|
+
-H, --host Local host to tunnel (default: localhost)
|
|
36
|
+
-r, --requests Print incoming requests
|
|
37
|
+
-v, --version Show version
|
|
38
|
+
-h, --help Show this help
|
|
82
39
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (argv.qr) {
|
|
88
|
-
console.log('\n' + chalk.gray(' Scan to open on mobile:'));
|
|
89
|
-
qrcode.generate(tunnel.url, { small: true }, (qr) => {
|
|
90
|
-
qr.split('\n').forEach(line => console.log(' ' + line));
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (argv.open) {
|
|
95
|
-
const { exec } = require('child_process');
|
|
96
|
-
const openCmd = process.platform === 'darwin' ? 'open' :
|
|
97
|
-
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
98
|
-
exec(`${openCmd} ${tunnel.url}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
console.log('');
|
|
102
|
-
console.log(chalk.gray(' Press Ctrl+C to stop the tunnel'));
|
|
103
|
-
console.log('');
|
|
104
|
-
|
|
105
|
-
// Handle incoming request logging
|
|
106
|
-
if (argv['print-requests']) {
|
|
107
|
-
tunnel.on('request', (info) => {
|
|
108
|
-
const time = new Date().toLocaleTimeString();
|
|
109
|
-
const method = chalk.cyan(info.method.padEnd(6));
|
|
110
|
-
const status = info.statusCode >= 400
|
|
111
|
-
? chalk.red(info.statusCode)
|
|
112
|
-
: chalk.green(info.statusCode);
|
|
113
|
-
console.log(` ${chalk.gray(time)} ${method} ${status} ${chalk.white(info.path)}`);
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Graceful shutdown
|
|
118
|
-
process.on('SIGINT', async () => {
|
|
119
|
-
console.log('\n' + chalk.gray(' Closing tunnel...'));
|
|
120
|
-
tunnel.close();
|
|
121
|
-
process.exit(0);
|
|
122
|
-
});
|
|
40
|
+
Examples:
|
|
41
|
+
local.dev 3000
|
|
42
|
+
local.dev 8080 --subdomain myapp
|
|
43
|
+
local.dev 5000 --requests
|
|
123
44
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
45
|
+
`);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const port = parseInt(args.find(a => /^\d+$/.test(a)), 10) || 3000;
|
|
50
|
+
const subdomain = getFlag('-s', '--subdomain');
|
|
51
|
+
const host = getFlag('-H', '--host') || 'localhost';
|
|
52
|
+
const printRequests = hasFlag('-r', '--requests');
|
|
53
|
+
|
|
54
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
55
|
+
async function main() {
|
|
56
|
+
printBanner(version);
|
|
57
|
+
printInfo(`Tunneling ${host}:${port} to the internet...`);
|
|
128
58
|
|
|
59
|
+
let tunnel;
|
|
60
|
+
try {
|
|
61
|
+
tunnel = await createTunnel({ port, host, subdomain });
|
|
129
62
|
} catch (err) {
|
|
130
|
-
spinner.fail('Failed to create tunnel');
|
|
131
63
|
printError(err.message);
|
|
132
64
|
process.exit(1);
|
|
133
65
|
}
|
|
66
|
+
|
|
67
|
+
printSuccess(tunnel.url);
|
|
68
|
+
process.stdout.write(' Press Ctrl+C to stop\n\n');
|
|
69
|
+
|
|
70
|
+
if (printRequests) {
|
|
71
|
+
tunnel.on('request', (info) => {
|
|
72
|
+
printRequest(info.method || 'GET', info.path || '/', info.statusCode || 200);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
tunnel.on('error', (err) => {
|
|
77
|
+
// Surface non-fatal errors quietly
|
|
78
|
+
process.stderr.write(` warn: ${err.message}\n`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Graceful shutdown
|
|
82
|
+
const shutdown = () => {
|
|
83
|
+
process.stdout.write('\n Closing tunnel...\n');
|
|
84
|
+
tunnel.close();
|
|
85
|
+
process.exit(0);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
process.on('SIGINT', shutdown);
|
|
89
|
+
process.on('SIGTERM', shutdown);
|
|
134
90
|
}
|
|
135
91
|
|
|
136
|
-
main();
|
|
92
|
+
main();
|
package/lib/index.js
CHANGED
|
@@ -1,54 +1,27 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* local.dev —
|
|
5
|
-
*
|
|
6
|
-
* Programmatic API:
|
|
7
|
-
*
|
|
8
|
-
* const { tunnel } = require('local.dev');
|
|
9
|
-
*
|
|
10
|
-
* const t = await tunnel(3000);
|
|
11
|
-
* console.log('Public URL:', t.url);
|
|
12
|
-
*
|
|
13
|
-
* // Later:
|
|
14
|
-
* t.close();
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const { createTunnel, TunnelHandle } = require('./tunnel');
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Open a tunnel to a local port.
|
|
21
|
-
*
|
|
22
|
-
* @param {number|object} portOrOptions - Port number, or options object
|
|
23
|
-
* @param {object} [options] - Options (if first arg is a port)
|
|
24
|
-
* @returns {Promise<TunnelHandle>}
|
|
4
|
+
* @o-town/local.dev — programmatic API
|
|
25
5
|
*
|
|
26
6
|
* @example
|
|
27
|
-
*
|
|
7
|
+
* const { tunnel } = require('@o-town/local.dev');
|
|
28
8
|
* const t = await tunnel(3000);
|
|
29
|
-
* console.log(t.url);
|
|
9
|
+
* console.log(t.url);
|
|
30
10
|
* t.close();
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* // With options
|
|
34
|
-
* const t = await tunnel({ port: 8080, subdomain: 'myapp' });
|
|
35
11
|
*/
|
|
12
|
+
|
|
13
|
+
const { createTunnel, Tunnel } = require('./tunnel');
|
|
14
|
+
|
|
36
15
|
async function tunnel(portOrOptions, options = {}) {
|
|
37
16
|
let opts;
|
|
38
|
-
|
|
39
17
|
if (typeof portOrOptions === 'number') {
|
|
40
18
|
opts = { port: portOrOptions, ...options };
|
|
41
|
-
} else if (typeof portOrOptions === 'object'
|
|
19
|
+
} else if (portOrOptions && typeof portOrOptions === 'object') {
|
|
42
20
|
opts = portOrOptions;
|
|
43
21
|
} else {
|
|
44
22
|
throw new TypeError('First argument must be a port number or options object');
|
|
45
23
|
}
|
|
46
|
-
|
|
47
24
|
return createTunnel(opts);
|
|
48
25
|
}
|
|
49
26
|
|
|
50
|
-
module.exports = {
|
|
51
|
-
tunnel,
|
|
52
|
-
createTunnel,
|
|
53
|
-
TunnelHandle,
|
|
54
|
-
};
|
|
27
|
+
module.exports = { tunnel, createTunnel, Tunnel };
|
package/lib/tunnel.js
CHANGED
|
@@ -1,72 +1,149 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const localtunnel = require('localtunnel');
|
|
4
|
-
const EventEmitter = require('events');
|
|
5
|
-
|
|
6
3
|
/**
|
|
7
|
-
*
|
|
4
|
+
* Core tunnel logic — zero deps beyond Node built-ins + ws.
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* @returns {Promise<TunnelHandle>}
|
|
6
|
+
* How it works:
|
|
7
|
+
* 1. POST to loca.lt API to get an assigned subdomain + WebSocket URL
|
|
8
|
+
* 2. Open a WebSocket to the tunnel server
|
|
9
|
+
* 3. For each incoming tunnel request, open a TCP socket to localhost:port
|
|
10
|
+
* and pipe data bidirectionally through the WebSocket message channel
|
|
15
11
|
*/
|
|
16
|
-
async function createTunnel(options = {}) {
|
|
17
|
-
const {
|
|
18
|
-
port,
|
|
19
|
-
host = 'localhost',
|
|
20
|
-
subdomain,
|
|
21
|
-
printRequests = false,
|
|
22
|
-
} = options;
|
|
23
|
-
|
|
24
|
-
if (!port || typeof port !== 'number' || port < 1 || port > 65535) {
|
|
25
|
-
throw new Error(`Invalid port: ${port}. Must be a number between 1 and 65535.`);
|
|
26
|
-
}
|
|
27
12
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
13
|
+
const https = require('https');
|
|
14
|
+
const http = require('http');
|
|
15
|
+
const net = require('net');
|
|
16
|
+
const { EventEmitter } = require('events');
|
|
17
|
+
const WebSocket = require('ws');
|
|
33
18
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
19
|
+
const TUNNEL_HOST = 'localtunnel.me';
|
|
20
|
+
const API_URL = `https://${TUNNEL_HOST}`;
|
|
37
21
|
|
|
38
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Request a tunnel assignment from the loca.lt API.
|
|
24
|
+
* Returns { id, url, cached_url, ws_connections, port }
|
|
25
|
+
*/
|
|
26
|
+
function assignTunnel(subdomain) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const path = subdomain ? `/api/tunnels/${subdomain}` : '/api/tunnels';
|
|
29
|
+
const options = {
|
|
30
|
+
hostname: TUNNEL_HOST,
|
|
31
|
+
path,
|
|
32
|
+
method: 'GET',
|
|
33
|
+
headers: { 'bypass-tunnel-reminder': '1' },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const req = https.request(options, (res) => {
|
|
37
|
+
let body = '';
|
|
38
|
+
res.on('data', (chunk) => (body += chunk));
|
|
39
|
+
res.on('end', () => {
|
|
40
|
+
try {
|
|
41
|
+
const data = JSON.parse(body);
|
|
42
|
+
if (data.error) return reject(new Error(data.error));
|
|
43
|
+
resolve(data);
|
|
44
|
+
} catch {
|
|
45
|
+
reject(new Error('Invalid response from tunnel server'));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
req.on('error', reject);
|
|
51
|
+
req.end();
|
|
52
|
+
});
|
|
42
53
|
}
|
|
43
54
|
|
|
44
|
-
class
|
|
45
|
-
constructor(
|
|
55
|
+
class Tunnel extends EventEmitter {
|
|
56
|
+
constructor(opts = {}) {
|
|
46
57
|
super();
|
|
47
|
-
this.
|
|
48
|
-
this.
|
|
58
|
+
this.port = opts.port;
|
|
59
|
+
this.host = opts.host || 'localhost';
|
|
60
|
+
this.subdomain = opts.subdomain || null;
|
|
61
|
+
this.url = null;
|
|
62
|
+
this._ws = null;
|
|
63
|
+
this._closed = false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async open() {
|
|
67
|
+
const info = await assignTunnel(this.subdomain);
|
|
68
|
+
this.url = info.url;
|
|
69
|
+
this._connect(info);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
49
72
|
|
|
50
|
-
|
|
51
|
-
|
|
73
|
+
_connect(info) {
|
|
74
|
+
// loca.lt uses a WebSocket-based multiplexed tunnel
|
|
75
|
+
const wsUrl = this.url.replace('https://', 'wss://').replace('http://', 'ws://');
|
|
76
|
+
const ws = new WebSocket(wsUrl + '/?new', {
|
|
77
|
+
headers: { 'bypass-tunnel-reminder': '1' },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this._ws = ws;
|
|
81
|
+
|
|
82
|
+
ws.on('open', () => {
|
|
83
|
+
this.emit('open', this.url);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
ws.on('message', (data) => {
|
|
87
|
+
// Parse incoming tunnel request metadata, then proxy it
|
|
88
|
+
try {
|
|
89
|
+
const meta = JSON.parse(data);
|
|
90
|
+
if (meta.type === 'request') {
|
|
91
|
+
this._handleRequest(meta, ws);
|
|
92
|
+
this.emit('request', meta);
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// binary / passthrough frame — ignore at this level
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
ws.on('close', () => {
|
|
100
|
+
if (!this._closed) {
|
|
101
|
+
// Auto-reconnect after a short delay
|
|
102
|
+
setTimeout(() => this._connect(info), 1000);
|
|
103
|
+
}
|
|
52
104
|
this.emit('close');
|
|
53
105
|
});
|
|
54
106
|
|
|
55
|
-
|
|
107
|
+
ws.on('error', (err) => {
|
|
56
108
|
this.emit('error', err);
|
|
57
109
|
});
|
|
110
|
+
}
|
|
58
111
|
|
|
59
|
-
|
|
60
|
-
|
|
112
|
+
_handleRequest(meta, ws) {
|
|
113
|
+
// Open a raw TCP connection to the local server and pipe via WebSocket
|
|
114
|
+
const socket = net.createConnection({ host: this.host, port: this.port });
|
|
115
|
+
|
|
116
|
+
socket.on('error', (err) => {
|
|
117
|
+
this.emit('error', err);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Forward local response back through the WebSocket
|
|
121
|
+
socket.on('data', (chunk) => {
|
|
122
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
123
|
+
ws.send(JSON.stringify({ id: meta.id, data: chunk.toString('base64') }));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Send the original request down to localhost
|
|
128
|
+
socket.write(Buffer.from(meta.data || '', 'base64'));
|
|
129
|
+
|
|
130
|
+
socket.on('end', () => {
|
|
131
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
132
|
+
ws.send(JSON.stringify({ id: meta.id, end: true }));
|
|
133
|
+
}
|
|
61
134
|
});
|
|
62
135
|
}
|
|
63
136
|
|
|
64
|
-
/**
|
|
65
|
-
* Close the tunnel and free resources.
|
|
66
|
-
*/
|
|
67
137
|
close() {
|
|
68
|
-
this.
|
|
138
|
+
this._closed = true;
|
|
139
|
+
if (this._ws) this._ws.terminate();
|
|
69
140
|
}
|
|
70
141
|
}
|
|
71
142
|
|
|
72
|
-
|
|
143
|
+
async function createTunnel(opts = {}) {
|
|
144
|
+
const tunnel = new Tunnel(opts);
|
|
145
|
+
await tunnel.open();
|
|
146
|
+
return tunnel;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { createTunnel, Tunnel };
|
package/lib/ui.js
CHANGED
|
@@ -1,40 +1,63 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// ANSI color codes — no chalk needed
|
|
4
|
+
const c = {
|
|
5
|
+
reset: '\x1b[0m',
|
|
6
|
+
bold: '\x1b[1m',
|
|
7
|
+
dim: '\x1b[2m',
|
|
8
|
+
cyan: '\x1b[36m',
|
|
9
|
+
green: '\x1b[32m',
|
|
10
|
+
red: '\x1b[31m',
|
|
11
|
+
white: '\x1b[37m',
|
|
12
|
+
yellow: '\x1b[33m',
|
|
13
|
+
};
|
|
4
14
|
|
|
5
|
-
const
|
|
15
|
+
const fmt = (codes, text) => `${codes}${text}${c.reset}`;
|
|
6
16
|
|
|
7
|
-
function printBanner() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
function printBanner(version) {
|
|
18
|
+
const brand = fmt(c.bold + c.cyan, 'local') + fmt(c.bold + c.white, '.dev');
|
|
19
|
+
process.stdout.write(`
|
|
20
|
+
${brand} ${fmt(c.dim, 'v' + version)}
|
|
21
|
+
${fmt(c.dim, 'Expose your local server to the internet')}
|
|
22
|
+
|
|
23
|
+
`);
|
|
12
24
|
}
|
|
13
25
|
|
|
14
26
|
function printSuccess(url) {
|
|
15
|
-
const line =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
const line = fmt(c.dim, '─'.repeat(48));
|
|
28
|
+
process.stdout.write(`
|
|
29
|
+
${line}
|
|
30
|
+
|
|
31
|
+
${fmt(c.green, '✔')} Tunnel is ${fmt(c.green, 'live')}
|
|
32
|
+
|
|
33
|
+
${fmt(c.dim, 'URL')} ${fmt(c.bold + c.white, url)}
|
|
34
|
+
|
|
35
|
+
${line}
|
|
36
|
+
|
|
37
|
+
`);
|
|
23
38
|
}
|
|
24
39
|
|
|
25
40
|
function printInfo(msg) {
|
|
26
|
-
|
|
41
|
+
process.stdout.write(` ${fmt(c.cyan, '→')} ${fmt(c.dim, msg)}\n`);
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
function printError(msg) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
process.stderr.write(`
|
|
46
|
+
${fmt(c.red, '✖ Error:')} ${msg}
|
|
47
|
+
|
|
48
|
+
${fmt(c.dim, 'Troubleshooting:')}
|
|
49
|
+
${fmt(c.dim, ' • Is your local server running?')}
|
|
50
|
+
${fmt(c.dim, ' • Check the port number is correct')}
|
|
51
|
+
${fmt(c.dim, ' • Run: local.dev --help')}
|
|
52
|
+
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function printRequest(method, path, status) {
|
|
57
|
+
const time = new Date().toLocaleTimeString();
|
|
58
|
+
const statusFmt = status >= 400 ? fmt(c.red, status) : fmt(c.green, status);
|
|
59
|
+
const methodFmt = fmt(c.cyan, method.padEnd(6));
|
|
60
|
+
process.stdout.write(` ${fmt(c.dim, time)} ${methodFmt} ${statusFmt} ${path}\n`);
|
|
38
61
|
}
|
|
39
62
|
|
|
40
|
-
module.exports = { printBanner, printSuccess, printInfo, printError };
|
|
63
|
+
module.exports = { printBanner, printSuccess, printInfo, printError, printRequest };
|
package/package.json
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o-town/local.dev",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Expose your local website to the internet instantly — no config, no signup.",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"local.dev": "./bin/cli.js",
|
|
8
8
|
"localdev": "./bin/cli.js"
|
|
9
9
|
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"test": "node test.js"
|
|
12
|
-
},
|
|
13
10
|
"keywords": [
|
|
14
11
|
"tunnel",
|
|
15
12
|
"localhost",
|
|
@@ -23,15 +20,7 @@
|
|
|
23
20
|
"author": "local.dev contributors",
|
|
24
21
|
"license": "MIT",
|
|
25
22
|
"dependencies": {
|
|
26
|
-
"
|
|
27
|
-
"chalk": "^4.1.2",
|
|
28
|
-
"ws": "^8.16.0",
|
|
29
|
-
"http-proxy": "^1.18.1",
|
|
30
|
-
"localtunnel": "^2.0.2",
|
|
31
|
-
"ora": "^5.4.1",
|
|
32
|
-
"qrcode-terminal": "^0.12.0",
|
|
33
|
-
"update-notifier": "^5.1.0",
|
|
34
|
-
"yargs": "^17.7.2"
|
|
23
|
+
"ws": "^8.16.0"
|
|
35
24
|
},
|
|
36
25
|
"engines": {
|
|
37
26
|
"node": ">=14.0.0"
|