@o-town/local.dev 1.0.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.
@@ -0,0 +1 @@
1
+ {}
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # local.dev
2
+
3
+ > Expose your local website to the internet — instantly. No signup, no config.
4
+
5
+ ```
6
+ npx local.dev 3000
7
+ ```
8
+
9
+ ---
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g local.dev
15
+ ```
16
+
17
+ Or use without installing:
18
+
19
+ ```bash
20
+ npx local.dev 3000
21
+ ```
22
+
23
+ ---
24
+
25
+ ## CLI Usage
26
+
27
+ ```
28
+ local.dev [port] [options]
29
+ ```
30
+
31
+ ### Arguments
32
+
33
+ | Argument | Description | Default |
34
+ |----------|--------------------------|---------|
35
+ | `port` | Local port to expose | `3000` |
36
+
37
+ ### Options
38
+
39
+ | Flag | Alias | Description |
40
+ |-----------------------|-------|------------------------------------------|
41
+ | `--subdomain` | `-s` | Request a custom subdomain |
42
+ | `--host` | `-H` | Local host to tunnel (default: localhost)|
43
+ | `--qr` | `-q` | Show QR code for the public URL |
44
+ | `--open` | `-o` | Open tunnel URL in your browser |
45
+ | `--print-requests` | `-r` | Log incoming HTTP requests |
46
+ | `--help` | `-h` | Show help |
47
+ | `--version` | `-v` | Show version number |
48
+
49
+ ### Examples
50
+
51
+ ```bash
52
+ # Expose port 3000
53
+ local.dev 3000
54
+
55
+ # Expose port 8080 with a custom subdomain
56
+ local.dev 8080 --subdomain myapp
57
+
58
+ # Show QR code (great for mobile testing)
59
+ local.dev 3000 --qr
60
+
61
+ # Open tunnel URL in browser + log requests
62
+ local.dev 3000 --open --print-requests
63
+
64
+ # Expose a non-localhost server
65
+ local.dev 3000 --host 192.168.1.10
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Programmatic API
71
+
72
+ You can also use `local.dev` as a Node.js library:
73
+
74
+ ```js
75
+ const { tunnel } = require('local.dev');
76
+
77
+ async function main() {
78
+ // Simple: just pass a port
79
+ const t = await tunnel(3000);
80
+ console.log('Public URL:', t.url);
81
+
82
+ // Stop the tunnel when done
83
+ t.close();
84
+ }
85
+
86
+ main();
87
+ ```
88
+
89
+ ### With options
90
+
91
+ ```js
92
+ const t = await tunnel({
93
+ port: 8080,
94
+ subdomain: 'myapp',
95
+ host: 'localhost',
96
+ printRequests: true,
97
+ });
98
+
99
+ console.log(t.url); // https://myapp.loca.lt
100
+
101
+ t.on('request', (info) => {
102
+ console.log(info.method, info.path, info.statusCode);
103
+ });
104
+
105
+ t.on('close', () => {
106
+ console.log('Tunnel closed');
107
+ });
108
+ ```
109
+
110
+ ### API Reference
111
+
112
+ #### `tunnel(port, [options])` → `Promise<TunnelHandle>`
113
+ #### `tunnel(options)` → `Promise<TunnelHandle>`
114
+
115
+ **Options:**
116
+
117
+ | Property | Type | Description |
118
+ |-----------------|---------|--------------------------------------|
119
+ | `port` | number | **Required.** Local port to expose |
120
+ | `host` | string | Local host (default: `'localhost'`) |
121
+ | `subdomain` | string | Requested subdomain |
122
+ | `printRequests` | boolean | Log requests (default: `false`) |
123
+
124
+ #### `TunnelHandle`
125
+
126
+ | Property/Method | Description |
127
+ |-----------------|------------------------------|
128
+ | `.url` | The public HTTPS URL |
129
+ | `.close()` | Close the tunnel |
130
+ | `.on('request', cb)` | Fired on each HTTP request |
131
+ | `.on('close', cb)` | Fired when tunnel closes |
132
+ | `.on('error', cb)` | Fired on tunnel errors |
133
+
134
+ ---
135
+
136
+ ## How it works
137
+
138
+ `local.dev` creates a secure tunnel from a public URL to your local machine using the [localtunnel](https://github.com/localtunnel/localtunnel) protocol. Incoming requests hit the public URL and are proxied through to your local server — no port forwarding or firewall changes needed.
139
+
140
+ ---
141
+
142
+ ## License
143
+
144
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
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
+ const { createTunnel } = require('../lib/tunnel');
11
+ const { printBanner, printSuccess, printInfo, printError } = require('../lib/ui');
12
+ const updateNotifier = require('update-notifier');
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;
61
+
62
+ async function main() {
63
+ printBanner();
64
+
65
+ const port = argv.port || 3000;
66
+ const host = argv.host || 'localhost';
67
+
68
+ printInfo(`Tunneling ${chalk.yellow(`${host}:${port}`)} to the internet...`);
69
+
70
+ const spinner = ora({
71
+ text: 'Establishing secure tunnel...',
72
+ color: 'cyan',
73
+ }).start();
74
+
75
+ try {
76
+ const tunnel = await createTunnel({
77
+ port,
78
+ host,
79
+ subdomain: argv.subdomain,
80
+ printRequests: argv['print-requests'],
81
+ });
82
+
83
+ spinner.stop();
84
+
85
+ printSuccess(tunnel.url);
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
+ });
123
+
124
+ process.on('SIGTERM', async () => {
125
+ tunnel.close();
126
+ process.exit(0);
127
+ });
128
+
129
+ } catch (err) {
130
+ spinner.fail('Failed to create tunnel');
131
+ printError(err.message);
132
+ process.exit(1);
133
+ }
134
+ }
135
+
136
+ main();
package/example.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * local.dev — programmatic usage example
3
+ *
4
+ * Run: node example.js
5
+ * (Make sure you have a server running on port 3000 first)
6
+ */
7
+
8
+ const { tunnel } = require('./lib/index');
9
+
10
+ async function main() {
11
+ console.log('Opening tunnel to localhost:3000...');
12
+
13
+ const t = await tunnel(3000);
14
+
15
+ console.log('');
16
+ console.log('✔ Public URL:', t.url);
17
+ console.log('');
18
+ console.log('Listening for requests... (Ctrl+C to stop)');
19
+
20
+ t.on('request', (info) => {
21
+ console.log(` ${info.method} ${info.path} → ${info.statusCode}`);
22
+ });
23
+
24
+ t.on('close', () => {
25
+ console.log('Tunnel closed.');
26
+ });
27
+
28
+ process.on('SIGINT', () => {
29
+ t.close();
30
+ process.exit(0);
31
+ });
32
+ }
33
+
34
+ main().catch((err) => {
35
+ console.error('Error:', err.message);
36
+ process.exit(1);
37
+ });
package/lib/index.js ADDED
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * local.dev — Expose your local server to the internet.
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>}
25
+ *
26
+ * @example
27
+ * // Simple usage
28
+ * const t = await tunnel(3000);
29
+ * console.log(t.url); // https://xyz.loca.lt
30
+ * t.close();
31
+ *
32
+ * @example
33
+ * // With options
34
+ * const t = await tunnel({ port: 8080, subdomain: 'myapp' });
35
+ */
36
+ async function tunnel(portOrOptions, options = {}) {
37
+ let opts;
38
+
39
+ if (typeof portOrOptions === 'number') {
40
+ opts = { port: portOrOptions, ...options };
41
+ } else if (typeof portOrOptions === 'object' && portOrOptions !== null) {
42
+ opts = portOrOptions;
43
+ } else {
44
+ throw new TypeError('First argument must be a port number or options object');
45
+ }
46
+
47
+ return createTunnel(opts);
48
+ }
49
+
50
+ module.exports = {
51
+ tunnel,
52
+ createTunnel,
53
+ TunnelHandle,
54
+ };
package/lib/tunnel.js ADDED
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ const localtunnel = require('localtunnel');
4
+ const EventEmitter = require('events');
5
+
6
+ /**
7
+ * Creates a tunnel to expose a local port to the internet.
8
+ *
9
+ * @param {object} options
10
+ * @param {number} options.port - Local port to expose
11
+ * @param {string} [options.host] - Local host (default: localhost)
12
+ * @param {string} [options.subdomain] - Requested subdomain
13
+ * @param {boolean} [options.printRequests] - Log incoming requests
14
+ * @returns {Promise<TunnelHandle>}
15
+ */
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
+
28
+ const tunnelOptions = {
29
+ port,
30
+ local_host: host,
31
+ allow_invalid_cert: false,
32
+ };
33
+
34
+ if (subdomain) {
35
+ tunnelOptions.subdomain = subdomain;
36
+ }
37
+
38
+ const tunnel = await localtunnel(tunnelOptions);
39
+
40
+ const handle = new TunnelHandle(tunnel, { printRequests });
41
+ return handle;
42
+ }
43
+
44
+ class TunnelHandle extends EventEmitter {
45
+ constructor(tunnel, options = {}) {
46
+ super();
47
+ this._tunnel = tunnel;
48
+ this.url = tunnel.url;
49
+
50
+ // Proxy tunnel events
51
+ tunnel.on('close', () => {
52
+ this.emit('close');
53
+ });
54
+
55
+ tunnel.on('error', (err) => {
56
+ this.emit('error', err);
57
+ });
58
+
59
+ tunnel.on('request', (info) => {
60
+ this.emit('request', info);
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Close the tunnel and free resources.
66
+ */
67
+ close() {
68
+ this._tunnel.close();
69
+ }
70
+ }
71
+
72
+ module.exports = { createTunnel, TunnelHandle };
package/lib/ui.js ADDED
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+
5
+ const BRAND = chalk.bold.cyan('local') + chalk.bold.white('.dev');
6
+
7
+ function printBanner() {
8
+ console.log('');
9
+ console.log(` ${BRAND} ${chalk.gray('v' + require('../package.json').version)}`);
10
+ console.log(` ${chalk.gray('Expose your local server to the internet')}`);
11
+ console.log('');
12
+ }
13
+
14
+ function printSuccess(url) {
15
+ const line = chalk.gray('─'.repeat(50));
16
+ console.log(` ${line}`);
17
+ console.log('');
18
+ console.log(` ${chalk.green('✔')} Tunnel is ${chalk.green('live')}`);
19
+ console.log('');
20
+ console.log(` ${chalk.gray('URL')} ${chalk.bold.white(url)}`);
21
+ console.log('');
22
+ console.log(` ${line}`);
23
+ }
24
+
25
+ function printInfo(msg) {
26
+ console.log(` ${chalk.cyan('→')} ${chalk.gray(msg)}`);
27
+ }
28
+
29
+ function printError(msg) {
30
+ console.log('');
31
+ console.log(` ${chalk.red('✖')} ${chalk.red('Error:')} ${msg}`);
32
+ console.log('');
33
+ console.log(` ${chalk.gray('Troubleshooting:')}`);
34
+ console.log(` ${chalk.gray(' • Is your local server running?')}`);
35
+ console.log(` ${chalk.gray(' • Check the port number is correct')}`);
36
+ console.log(` ${chalk.gray(' • Try: local.dev --help')}`);
37
+ console.log('');
38
+ }
39
+
40
+ module.exports = { printBanner, printSuccess, printInfo, printError };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@o-town/local.dev",
3
+ "version": "1.0.0",
4
+ "description": "Expose your local website to the internet instantly — no config, no signup.",
5
+ "main": "lib/index.js",
6
+ "bin": {
7
+ "local.dev": "./bin/cli.js",
8
+ "localdev": "./bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node test.js"
12
+ },
13
+ "keywords": [
14
+ "tunnel",
15
+ "localhost",
16
+ "expose",
17
+ "ngrok",
18
+ "local",
19
+ "dev",
20
+ "proxy",
21
+ "public-url"
22
+ ],
23
+ "author": "local.dev contributors",
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "axios": "^1.6.0",
27
+ "chalk": "^4.1.2",
28
+ "http-proxy": "^1.18.1",
29
+ "localtunnel": "^1.8.3",
30
+ "ora": "^5.4.1",
31
+ "qrcode-terminal": "^0.12.0",
32
+ "update-notifier": "^7.3.1",
33
+ "ws": "^8.16.0",
34
+ "yargs": "^17.7.2"
35
+ },
36
+ "engines": {
37
+ "node": ">=14.0.0"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/yourusername/local.dev.git"
42
+ },
43
+ "homepage": "https://localdev.sh",
44
+ "bugs": {
45
+ "url": "https://github.com/yourusername/local.dev/issues"
46
+ }
47
+ }