ai-or-die 0.1.5 → 0.1.6

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
@@ -122,57 +122,20 @@ async function main() {
122
122
  }
123
123
  }
124
124
 
125
- // Dev tunnel setup
126
- let tunnelProcess = null;
127
- let publicUrl = null;
125
+ // Dev tunnel or browser open
126
+ let tunnel = null;
128
127
  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);
128
+ const { TunnelManager } = require('../src/tunnel-manager');
129
+ tunnel = new TunnelManager({
130
+ port,
131
+ allowAnonymous: options.tunnelAllowAnonymous,
132
+ dev: options.dev,
133
+ onUrl: (tunnelUrl) => {
134
+ console.log(`\n \x1b[1m\x1b[32mTunnel ready:\x1b[0m \x1b[1m\x1b[4m${tunnelUrl}\x1b[0m\n`);
135
+ if (open && options.open) open(tunnelUrl).catch(() => {});
150
136
  }
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
- }
137
+ });
138
+ await tunnel.start();
176
139
  } else if (options.open) {
177
140
  try { if (open) await open(url); } catch (error) {
178
141
  console.warn(' Could not automatically open browser:', error.message);
@@ -183,8 +146,7 @@ async function main() {
183
146
 
184
147
  const shutdown = async () => {
185
148
  console.log('\nShutting down server...');
186
- // Close dev tunnel first if active
187
- if (tunnelProcess) { try { tunnelProcess.kill(); } catch (_) {} }
149
+ if (tunnel) await tunnel.stop();
188
150
  server.close(() => {
189
151
  console.log('Server closed');
190
152
  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.6",
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 };