ai-or-die 0.1.5 → 0.1.7
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/ai-or-die.js +25 -73
- package/package.json +1 -1
- package/src/tunnel-manager.js +215 -0
package/bin/ai-or-die.js
CHANGED
|
@@ -54,15 +54,14 @@ async function main() {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// Handle authentication logic
|
|
57
|
+
// Tunnel mode disables auth — the tunnel itself controls access
|
|
57
58
|
let authToken = null;
|
|
58
|
-
let noAuth = options.disableAuth === true;
|
|
59
|
+
let noAuth = options.disableAuth === true || options.tunnel === true;
|
|
59
60
|
|
|
60
61
|
if (!noAuth) {
|
|
61
62
|
if (options.auth) {
|
|
62
|
-
// Use provided token
|
|
63
63
|
authToken = options.auth;
|
|
64
64
|
} else {
|
|
65
|
-
// Generate random token
|
|
66
65
|
authToken = generateRandomToken();
|
|
67
66
|
}
|
|
68
67
|
}
|
|
@@ -91,88 +90,42 @@ async function main() {
|
|
|
91
90
|
console.log(`Plan: ${options.plan}`);
|
|
92
91
|
console.log(`Aliases: Claude → "${serverOptions.claudeAlias}", Codex → "${serverOptions.codexAlias}", Copilot → "${serverOptions.copilotAlias}", Gemini → "${serverOptions.geminiAlias}", Terminal → "${serverOptions.terminalAlias}"`);
|
|
93
92
|
|
|
94
|
-
// Display authentication status
|
|
95
|
-
if (
|
|
93
|
+
// Display authentication status
|
|
94
|
+
if (options.tunnel) {
|
|
95
|
+
console.log('\n🌍 TUNNEL MODE — authentication disabled (tunnel controls access)');
|
|
96
|
+
} else if (noAuth) {
|
|
96
97
|
console.log('\n⚠️ AUTHENTICATION DISABLED - Server is accessible without a token');
|
|
97
98
|
console.log(' (Use without --disable-auth flag for security in production)');
|
|
98
99
|
} else {
|
|
99
100
|
console.log('\n🔐 AUTHENTICATION ENABLED');
|
|
100
|
-
if (options.auth) {
|
|
101
|
-
console.log(' Using provided authentication token');
|
|
102
|
-
} else {
|
|
103
|
-
console.log(' Generated random authentication token:');
|
|
104
|
-
console.log(` \x1b[1m\x1b[33m${authToken}\x1b[0m`);
|
|
105
|
-
console.log(' \x1b[2mSave this token - you\'ll need it to access the interface\x1b[0m');
|
|
106
|
-
}
|
|
107
101
|
}
|
|
108
102
|
|
|
109
103
|
const server = await startServer(serverOptions);
|
|
110
104
|
|
|
111
105
|
const protocol = options.https ? 'https' : 'http';
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
106
|
+
const baseUrl = `${protocol}://localhost:${port}`;
|
|
107
|
+
// For localhost with auth, embed token in URL so user can just click it
|
|
108
|
+
const url = authToken ? `${baseUrl}?token=${authToken}` : baseUrl;
|
|
115
109
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.log(' Use your provided authentication token to access the interface');
|
|
120
|
-
} else {
|
|
121
|
-
console.log(` Enter this token when prompted: \x1b[1m\x1b[33m${authToken}\x1b[0m`);
|
|
122
|
-
}
|
|
110
|
+
console.log(`\n🚀 ai-or-die is running at: \x1b[1m\x1b[4m${url}\x1b[0m`);
|
|
111
|
+
if (authToken) {
|
|
112
|
+
console.log(` Auth token: \x1b[1m\x1b[33m${authToken}\x1b[0m`);
|
|
123
113
|
}
|
|
124
114
|
|
|
125
|
-
// Dev tunnel
|
|
126
|
-
let
|
|
127
|
-
let publicUrl = null;
|
|
115
|
+
// Dev tunnel or browser open
|
|
116
|
+
let tunnel = null;
|
|
128
117
|
if (options.tunnel) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
} catch (_) {
|
|
138
|
-
const isWin = process.platform === 'win32';
|
|
139
|
-
console.error('\n\x1b[31m devtunnel CLI not found.\x1b[0m\n');
|
|
140
|
-
console.error(' Install it with a single command:');
|
|
141
|
-
if (isWin) {
|
|
142
|
-
console.error(' \x1b[1mwinget install Microsoft.devtunnel\x1b[0m');
|
|
143
|
-
} else if (process.platform === 'darwin') {
|
|
144
|
-
console.error(' \x1b[1mbrew install --cask devtunnel\x1b[0m');
|
|
145
|
-
} else {
|
|
146
|
-
console.error(' \x1b[1mcurl -sL https://aka.ms/DevTunnelCliInstall | bash\x1b[0m');
|
|
147
|
-
}
|
|
148
|
-
console.error('\n Then run: \x1b[1mdevtunnel user login\x1b[0m (one-time)\n');
|
|
149
|
-
process.exit(1);
|
|
118
|
+
const { TunnelManager } = require('../src/tunnel-manager');
|
|
119
|
+
tunnel = new TunnelManager({
|
|
120
|
+
port,
|
|
121
|
+
allowAnonymous: options.tunnelAllowAnonymous,
|
|
122
|
+
dev: options.dev,
|
|
123
|
+
onUrl: (tunnelUrl) => {
|
|
124
|
+
console.log(`\n \x1b[1m\x1b[32mTunnel ready:\x1b[0m \x1b[1m\x1b[4m${tunnelUrl}\x1b[0m\n`);
|
|
125
|
+
if (open && options.open) open(tunnelUrl).catch(() => {});
|
|
150
126
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (options.tunnelAllowAnonymous) tunnelArgs.push('--allow-anonymous');
|
|
154
|
-
tunnelProcess = cpSpawn('devtunnel', tunnelArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
155
|
-
|
|
156
|
-
tunnelProcess.stdout.on('data', (data) => {
|
|
157
|
-
const match = data.toString().match(/https:\/\/[\w.-]+\.devtunnels\.ms\S*/);
|
|
158
|
-
if (match && !publicUrl) {
|
|
159
|
-
publicUrl = match[0].trim();
|
|
160
|
-
console.log(`\n \x1b[1m\x1b[32mTunnel ready:\x1b[0m \x1b[1m\x1b[4m${publicUrl}\x1b[0m\n`);
|
|
161
|
-
if (options.open) {
|
|
162
|
-
if (open) open(publicUrl).catch(() => {});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
tunnelProcess.stderr.on('data', (data) => {
|
|
167
|
-
const output = data.toString().trim();
|
|
168
|
-
if (output && options.dev) console.log(` [devtunnel] ${output}`);
|
|
169
|
-
});
|
|
170
|
-
tunnelProcess.on('error', () => {
|
|
171
|
-
console.error('\n \x1b[31mDev tunnel process failed to start.\x1b[0m');
|
|
172
|
-
});
|
|
173
|
-
} catch (error) {
|
|
174
|
-
console.error(' Failed to start dev tunnel:', error.message);
|
|
175
|
-
}
|
|
127
|
+
});
|
|
128
|
+
await tunnel.start();
|
|
176
129
|
} else if (options.open) {
|
|
177
130
|
try { if (open) await open(url); } catch (error) {
|
|
178
131
|
console.warn(' Could not automatically open browser:', error.message);
|
|
@@ -183,8 +136,7 @@ async function main() {
|
|
|
183
136
|
|
|
184
137
|
const shutdown = async () => {
|
|
185
138
|
console.log('\nShutting down server...');
|
|
186
|
-
|
|
187
|
-
if (tunnelProcess) { try { tunnelProcess.kill(); } catch (_) {} }
|
|
139
|
+
if (tunnel) await tunnel.stop();
|
|
188
140
|
server.close(() => {
|
|
189
141
|
console.log('Server closed');
|
|
190
142
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn, execFile } = require('child_process');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
const MAX_RETRIES = 3;
|
|
7
|
+
const URL_TIMEOUT_MS = 30000;
|
|
8
|
+
|
|
9
|
+
class TunnelManager {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.port = options.port || 7777;
|
|
12
|
+
this.allowAnonymous = options.allowAnonymous || false;
|
|
13
|
+
this.dev = options.dev || false;
|
|
14
|
+
this.onUrl = options.onUrl || (() => {});
|
|
15
|
+
|
|
16
|
+
this.process = null;
|
|
17
|
+
this.publicUrl = null;
|
|
18
|
+
this.stopping = false;
|
|
19
|
+
this.retryCount = 0;
|
|
20
|
+
this.tunnelId = `aiordie-${os.hostname().toLowerCase().replace(/[^a-z0-9-]/g, '')}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Full lifecycle: check CLI → check login → spawn → wait for URL.
|
|
25
|
+
* Never throws — logs errors and continues with localhost if tunnel fails.
|
|
26
|
+
*/
|
|
27
|
+
async start() {
|
|
28
|
+
console.log('\n Connecting dev tunnel...');
|
|
29
|
+
|
|
30
|
+
const hasCli = await this._checkCli();
|
|
31
|
+
if (!hasCli) return;
|
|
32
|
+
|
|
33
|
+
const loggedIn = await this._checkLogin();
|
|
34
|
+
if (!loggedIn) return;
|
|
35
|
+
|
|
36
|
+
await this._spawn();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Kill the tunnel process and wait for it to exit.
|
|
41
|
+
*/
|
|
42
|
+
async stop() {
|
|
43
|
+
this.stopping = true;
|
|
44
|
+
if (!this.process) return;
|
|
45
|
+
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const timeout = setTimeout(() => {
|
|
48
|
+
// Force kill after 5s if it hasn't exited
|
|
49
|
+
try { this.process.kill('SIGKILL'); } catch {}
|
|
50
|
+
resolve();
|
|
51
|
+
}, 5000);
|
|
52
|
+
|
|
53
|
+
this.process.once('exit', () => {
|
|
54
|
+
clearTimeout(timeout);
|
|
55
|
+
resolve();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
try { this.process.kill(); } catch {}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if devtunnel CLI is installed.
|
|
64
|
+
*/
|
|
65
|
+
async _checkCli() {
|
|
66
|
+
const checker = process.platform === 'win32' ? 'where' : 'which';
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
execFile(checker, ['devtunnel'], { timeout: 5000 }, (err) => {
|
|
69
|
+
if (err) {
|
|
70
|
+
const isWin = process.platform === 'win32';
|
|
71
|
+
console.error('\n \x1b[31mdevtunnel CLI not found.\x1b[0m\n');
|
|
72
|
+
console.error(' Install it with a single command:');
|
|
73
|
+
if (isWin) {
|
|
74
|
+
console.error(' \x1b[1mwinget install Microsoft.devtunnel\x1b[0m');
|
|
75
|
+
} else if (process.platform === 'darwin') {
|
|
76
|
+
console.error(' \x1b[1mbrew install --cask devtunnel\x1b[0m');
|
|
77
|
+
} else {
|
|
78
|
+
console.error(' \x1b[1mcurl -sL https://aka.ms/DevTunnelCliInstall | bash\x1b[0m');
|
|
79
|
+
}
|
|
80
|
+
console.error('\n Server will continue on localhost only.\n');
|
|
81
|
+
resolve(false);
|
|
82
|
+
} else {
|
|
83
|
+
resolve(true);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if user is logged in. If not, attempt interactive login.
|
|
91
|
+
*/
|
|
92
|
+
async _checkLogin() {
|
|
93
|
+
const isLoggedIn = await new Promise((resolve) => {
|
|
94
|
+
execFile('devtunnel', ['user', 'show'], { timeout: 10000 }, (err) => {
|
|
95
|
+
resolve(!err);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (isLoggedIn) return true;
|
|
100
|
+
|
|
101
|
+
console.log(' DevTunnel requires authentication. Launching login...\n');
|
|
102
|
+
|
|
103
|
+
// Run interactive login (inherits stdio so user can interact with browser auth)
|
|
104
|
+
const loginOk = await new Promise((resolve) => {
|
|
105
|
+
const loginProc = spawn('devtunnel', ['user', 'login'], {
|
|
106
|
+
stdio: 'inherit'
|
|
107
|
+
});
|
|
108
|
+
loginProc.on('exit', (code) => resolve(code === 0));
|
|
109
|
+
loginProc.on('error', () => resolve(false));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (loginOk) {
|
|
113
|
+
console.log('\n Login successful. Connecting tunnel...');
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.error('\n \x1b[33mLogin failed or cancelled. Server will continue on localhost only.\x1b[0m\n');
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Spawn the devtunnel host process and wait for the public URL.
|
|
123
|
+
*/
|
|
124
|
+
async _spawn() {
|
|
125
|
+
const args = ['host', '-p', String(this.port), '--tunnel-id', this.tunnelId];
|
|
126
|
+
if (this.allowAnonymous) args.push('--allow-anonymous');
|
|
127
|
+
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
this.process = spawn('devtunnel', args, {
|
|
130
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
let urlResolved = false;
|
|
134
|
+
|
|
135
|
+
// Timeout: if no URL appears within 30s, warn and continue
|
|
136
|
+
const urlTimeout = setTimeout(() => {
|
|
137
|
+
if (!urlResolved) {
|
|
138
|
+
urlResolved = true;
|
|
139
|
+
console.warn(' \x1b[33mTunnel started but no public URL detected within 30s.\x1b[0m');
|
|
140
|
+
console.warn(' The tunnel may still be connecting. Check devtunnel status manually.\n');
|
|
141
|
+
resolve();
|
|
142
|
+
}
|
|
143
|
+
}, URL_TIMEOUT_MS);
|
|
144
|
+
|
|
145
|
+
this.process.stdout.on('data', (data) => {
|
|
146
|
+
const output = data.toString();
|
|
147
|
+
if (this.dev) process.stdout.write(` [devtunnel] ${output}`);
|
|
148
|
+
|
|
149
|
+
const match = output.match(/https:\/\/[\w.-]+\.devtunnels\.ms\S*/);
|
|
150
|
+
if (match && !this.publicUrl) {
|
|
151
|
+
this.publicUrl = match[0].trim();
|
|
152
|
+
urlResolved = true;
|
|
153
|
+
clearTimeout(urlTimeout);
|
|
154
|
+
this.onUrl(this.publicUrl);
|
|
155
|
+
resolve();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
this.process.stderr.on('data', (data) => {
|
|
160
|
+
const output = data.toString().trim();
|
|
161
|
+
if (output) {
|
|
162
|
+
// Always log stderr — auth errors, rate limits come through here
|
|
163
|
+
console.error(` [devtunnel] ${output}`);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.process.on('error', (err) => {
|
|
168
|
+
clearTimeout(urlTimeout);
|
|
169
|
+
console.error(` \x1b[31mDev tunnel failed to start: ${err.message}\x1b[0m`);
|
|
170
|
+
if (!urlResolved) {
|
|
171
|
+
urlResolved = true;
|
|
172
|
+
resolve();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
this.process.on('exit', (code, signal) => {
|
|
177
|
+
clearTimeout(urlTimeout);
|
|
178
|
+
this.process = null;
|
|
179
|
+
|
|
180
|
+
if (!urlResolved) {
|
|
181
|
+
urlResolved = true;
|
|
182
|
+
resolve();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Auto-restart if not intentionally stopped
|
|
186
|
+
if (!this.stopping && code !== 0) {
|
|
187
|
+
this._restart();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Auto-restart with exponential backoff.
|
|
195
|
+
*/
|
|
196
|
+
async _restart() {
|
|
197
|
+
this.retryCount++;
|
|
198
|
+
if (this.retryCount > MAX_RETRIES) {
|
|
199
|
+
console.error(` \x1b[31mTunnel crashed ${MAX_RETRIES} times. Giving up. Server continues on localhost.\x1b[0m\n`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const delay = Math.pow(2, this.retryCount - 1) * 1000; // 1s, 2s, 4s
|
|
204
|
+
console.log(` Tunnel exited unexpectedly. Restarting in ${delay / 1000}s (attempt ${this.retryCount}/${MAX_RETRIES})...`);
|
|
205
|
+
|
|
206
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
207
|
+
|
|
208
|
+
if (this.stopping) return;
|
|
209
|
+
|
|
210
|
+
this.publicUrl = null;
|
|
211
|
+
await this._spawn();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = { TunnelManager };
|