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 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 prominently
95
- if (noAuth) {
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 url = `${protocol}://localhost:${port}`;
113
-
114
- console.log(`\n🚀 ai-or-die is running at: ${url}`);
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
- if (!noAuth) {
117
- console.log('\n📋 Authentication Required:');
118
- if (options.auth) {
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 setup
126
- let tunnelProcess = null;
127
- let publicUrl = null;
115
+ // Dev tunnel or browser open
116
+ let tunnel = null;
128
117
  if (options.tunnel) {
129
- console.log('\n\x1b[36m Connecting dev tunnel...\x1b[0m');
130
- try {
131
- const { spawn: cpSpawn, execFileSync } = require('child_process');
132
-
133
- // Check if devtunnel CLI is available
134
- const devtunnelCmd = process.platform === 'win32' ? 'where' : 'which';
135
- try {
136
- execFileSync(devtunnelCmd, ['devtunnel'], { stdio: 'ignore' });
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
- const tunnelArgs = ['host', '-p', String(port)];
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
- // Close dev tunnel first if active
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-or-die",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Universal AI coding terminal — Claude, Copilot, Gemini & more in your browser",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -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 };