antigravity-mobile-proxy 0.1.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/README.md +362 -0
- package/app/api/v1/artifacts/[convId]/[filename]/route.ts +75 -0
- package/app/api/v1/artifacts/[convId]/route.ts +47 -0
- package/app/api/v1/artifacts/active/[filename]/route.ts +50 -0
- package/app/api/v1/artifacts/active/route.ts +89 -0
- package/app/api/v1/artifacts/route.ts +43 -0
- package/app/api/v1/chat/action/route.ts +30 -0
- package/app/api/v1/chat/approve/route.ts +21 -0
- package/app/api/v1/chat/history/route.ts +23 -0
- package/app/api/v1/chat/mode/route.ts +59 -0
- package/app/api/v1/chat/new/route.ts +21 -0
- package/app/api/v1/chat/reject/route.ts +21 -0
- package/app/api/v1/chat/route.ts +105 -0
- package/app/api/v1/chat/state/route.ts +23 -0
- package/app/api/v1/chat/stream/route.ts +258 -0
- package/app/api/v1/conversations/active/route.ts +117 -0
- package/app/api/v1/conversations/route.ts +189 -0
- package/app/api/v1/conversations/select/route.ts +114 -0
- package/app/api/v1/debug/dom/route.ts +30 -0
- package/app/api/v1/debug/scrape/route.ts +56 -0
- package/app/api/v1/health/route.ts +13 -0
- package/app/api/v1/windows/cdp-start/route.ts +32 -0
- package/app/api/v1/windows/cdp-status/route.ts +32 -0
- package/app/api/v1/windows/close/route.ts +67 -0
- package/app/api/v1/windows/open/route.ts +49 -0
- package/app/api/v1/windows/recent/route.ts +25 -0
- package/app/api/v1/windows/route.ts +27 -0
- package/app/api/v1/windows/select/route.ts +35 -0
- package/app/debug/page.tsx +228 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +1234 -0
- package/app/layout.tsx +42 -0
- package/app/page.tsx +10 -0
- package/bin/cli.js +698 -0
- package/components/agent-message.tsx +63 -0
- package/components/artifact-panel.tsx +133 -0
- package/components/chat-container.tsx +82 -0
- package/components/chat-input.tsx +92 -0
- package/components/conversation-selector.tsx +97 -0
- package/components/header.tsx +302 -0
- package/components/hitl-dialog.tsx +23 -0
- package/components/message-list.tsx +41 -0
- package/components/thinking-block.tsx +14 -0
- package/components/tool-call-card.tsx +75 -0
- package/components/typing-indicator.tsx +11 -0
- package/components/user-message.tsx +13 -0
- package/components/welcome-screen.tsx +38 -0
- package/hooks/use-artifacts.ts +85 -0
- package/hooks/use-chat.ts +278 -0
- package/hooks/use-conversations.ts +190 -0
- package/lib/actions/hitl.ts +113 -0
- package/lib/actions/new-chat.ts +116 -0
- package/lib/actions/send-message.ts +31 -0
- package/lib/actions/switch-conversation.ts +92 -0
- package/lib/cdp/connection.ts +95 -0
- package/lib/cdp/process-manager.ts +327 -0
- package/lib/cdp/recent-projects.ts +137 -0
- package/lib/cdp/selectors.ts +11 -0
- package/lib/context.ts +38 -0
- package/lib/init.ts +48 -0
- package/lib/logger.ts +32 -0
- package/lib/scraper/agent-mode.ts +122 -0
- package/lib/scraper/agent-state.ts +756 -0
- package/lib/scraper/chat-history.ts +138 -0
- package/lib/scraper/ide-conversations.ts +124 -0
- package/lib/sse/diff-states.ts +141 -0
- package/lib/types.ts +146 -0
- package/lib/utils.ts +7 -0
- package/next.config.ts +7 -0
- package/package.json +50 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +34 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn, execSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
// ── Colors & Formatting ─────────────────────────────────────────────────
|
|
10
|
+
const c = {
|
|
11
|
+
reset: '\x1b[0m',
|
|
12
|
+
bold: '\x1b[1m',
|
|
13
|
+
dim: '\x1b[2m',
|
|
14
|
+
italic: '\x1b[3m',
|
|
15
|
+
underline: '\x1b[4m',
|
|
16
|
+
cyan: '\x1b[36m',
|
|
17
|
+
green: '\x1b[32m',
|
|
18
|
+
yellow: '\x1b[33m',
|
|
19
|
+
red: '\x1b[31m',
|
|
20
|
+
magenta: '\x1b[35m',
|
|
21
|
+
blue: '\x1b[34m',
|
|
22
|
+
white: '\x1b[37m',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const fmt = {
|
|
26
|
+
bold: (s) => `${c.bold}${s}${c.reset}`,
|
|
27
|
+
dim: (s) => `${c.dim}${s}${c.reset}`,
|
|
28
|
+
cyan: (s) => `${c.cyan}${s}${c.reset}`,
|
|
29
|
+
green: (s) => `${c.green}${s}${c.reset}`,
|
|
30
|
+
yellow: (s) => `${c.yellow}${s}${c.reset}`,
|
|
31
|
+
red: (s) => `${c.red}${s}${c.reset}`,
|
|
32
|
+
magenta: (s) => `${c.magenta}${s}${c.reset}`,
|
|
33
|
+
blue: (s) => `${c.blue}${s}${c.reset}`,
|
|
34
|
+
link: (s) => `${c.cyan}${c.underline}${s}${c.reset}`,
|
|
35
|
+
success: (s) => `${c.green}✔${c.reset} ${s}`,
|
|
36
|
+
error: (s) => `${c.red}✖${c.reset} ${s}`,
|
|
37
|
+
warn: (s) => `${c.yellow}⚠${c.reset} ${s}`,
|
|
38
|
+
info: (s) => `${c.cyan}ℹ${c.reset} ${s}`,
|
|
39
|
+
step: (n, s) => `${c.dim}[${n}]${c.reset} ${s}`,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ── Config file path ────────────────────────────────────────────────────
|
|
43
|
+
const CONFIG_DIR = path.join(os.homedir(), '.antigravity-mobile-proxy');
|
|
44
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
45
|
+
const APP_DIR = path.join(CONFIG_DIR, 'app');
|
|
46
|
+
|
|
47
|
+
function loadConfig() {
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
50
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function saveConfig(config) {
|
|
57
|
+
try {
|
|
58
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
59
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Readline helpers ────────────────────────────────────────────────────
|
|
66
|
+
const rl = readline.createInterface({
|
|
67
|
+
input: process.stdin,
|
|
68
|
+
output: process.stdout,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function ask(question) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function askWithDefault(question, defaultVal) {
|
|
78
|
+
const hint = defaultVal ? ` ${c.dim}(${defaultVal})${c.reset}` : '';
|
|
79
|
+
const answer = await ask(`${question}${hint}: `);
|
|
80
|
+
return answer || defaultVal || '';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function askYesNo(question, defaultYes = true) {
|
|
84
|
+
const hint = defaultYes ? `${c.dim}(Y/n)${c.reset}` : `${c.dim}(y/N)${c.reset}`;
|
|
85
|
+
const answer = await ask(`${question} ${hint}: `);
|
|
86
|
+
if (!answer) return defaultYes;
|
|
87
|
+
return answer.toLowerCase().startsWith('y');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function askPassword(question) {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
process.stdout.write(question);
|
|
93
|
+
const stdin = process.stdin;
|
|
94
|
+
const wasRaw = stdin.isRaw;
|
|
95
|
+
|
|
96
|
+
if (stdin.isTTY) {
|
|
97
|
+
stdin.setRawMode(true);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let input = '';
|
|
101
|
+
const onData = (char) => {
|
|
102
|
+
const s = char.toString();
|
|
103
|
+
|
|
104
|
+
if (s === '\n' || s === '\r') {
|
|
105
|
+
stdin.removeListener('data', onData);
|
|
106
|
+
if (stdin.isTTY) {
|
|
107
|
+
stdin.setRawMode(wasRaw || false);
|
|
108
|
+
}
|
|
109
|
+
process.stdout.write('\n');
|
|
110
|
+
resolve(input.trim());
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (s === '\u0003') {
|
|
115
|
+
process.stdout.write('\n');
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (s === '\u007f' || s === '\b') {
|
|
120
|
+
if (input.length > 0) {
|
|
121
|
+
input = input.slice(0, -1);
|
|
122
|
+
process.stdout.write('\b \b');
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
input += s;
|
|
128
|
+
process.stdout.write('•');
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
stdin.resume();
|
|
132
|
+
stdin.on('data', onData);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function printSeparator() {
|
|
137
|
+
console.log(c.dim + ' ─────────────────────────────────────────────' + c.reset);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function clearLine() {
|
|
141
|
+
process.stdout.write('\x1b[2K\r');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Banner ──────────────────────────────────────────────────────────────
|
|
145
|
+
function printBanner() {
|
|
146
|
+
console.log('');
|
|
147
|
+
console.log(` ${c.bold}${c.cyan}╔═══════════════════════════════════════════════════════╗${c.reset}`);
|
|
148
|
+
console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}🚀 Antigravity Mobile Proxy${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
|
|
149
|
+
console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.dim}Secure tunnel to your IDE with Google OAuth${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
|
|
150
|
+
console.log(` ${c.bold}${c.cyan}╚═══════════════════════════════════════════════════════╝${c.reset}`);
|
|
151
|
+
console.log('');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── CLI Arg Parsing ─────────────────────────────────────────────────────
|
|
155
|
+
function parseArgs() {
|
|
156
|
+
const args = process.argv.slice(2);
|
|
157
|
+
const parsed = {};
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < args.length; i++) {
|
|
160
|
+
if (args[i] === '--email' && args[i + 1]) parsed.email = args[++i];
|
|
161
|
+
else if (args[i] === '--port' && args[i + 1]) parsed.port = args[++i];
|
|
162
|
+
else if (args[i] === '--authtoken' && args[i + 1]) parsed.authtoken = args[++i];
|
|
163
|
+
else if (args[i] === '--no-tunnel') parsed.noTunnel = true;
|
|
164
|
+
else if (args[i] === '--help') parsed.help = true;
|
|
165
|
+
else if (args[i] === '--reset') parsed.reset = true;
|
|
166
|
+
else if (args[i] === '--non-interactive') parsed.nonInteractive = true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return parsed;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function printHelp() {
|
|
173
|
+
printBanner();
|
|
174
|
+
console.log(` ${fmt.bold('Usage:')}`);
|
|
175
|
+
console.log(` ${fmt.cyan('npx antigravity-mobile-proxy')} ${fmt.dim('# Interactive setup wizard')}`);
|
|
176
|
+
console.log(` ${fmt.cyan('npx antigravity-mobile-proxy --email me@gmail.com')} ${fmt.dim('# Skip wizard')}`);
|
|
177
|
+
console.log('');
|
|
178
|
+
console.log(` ${fmt.bold('Options:')}`);
|
|
179
|
+
console.log(` ${fmt.cyan('--email')} <email> Google email to allow access`);
|
|
180
|
+
console.log(` ${fmt.cyan('--port')} <number> Local port (default: 5555)`);
|
|
181
|
+
console.log(` ${fmt.cyan('--authtoken')} <token> ngrok authtoken`);
|
|
182
|
+
console.log(` ${fmt.cyan('--no-tunnel')} Run locally without ngrok`);
|
|
183
|
+
console.log(` ${fmt.cyan('--reset')} Reset saved configuration`);
|
|
184
|
+
console.log(` ${fmt.cyan('--help')} Show this help`);
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log(` ${fmt.bold('Environment Variables:')}`);
|
|
187
|
+
console.log(` ${fmt.cyan('NGROK_AUTHTOKEN')} Your ngrok authtoken`);
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log(` ${fmt.bold('First-time setup?')} Just run ${fmt.cyan('npx antigravity-mobile-proxy')} and follow the wizard!`);
|
|
190
|
+
console.log('');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Check if ngrok authtoken exists ─────────────────────────────────────
|
|
194
|
+
function detectAuthtoken() {
|
|
195
|
+
if (process.env.NGROK_AUTHTOKEN) {
|
|
196
|
+
return { token: process.env.NGROK_AUTHTOKEN, source: 'environment variable' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const config = loadConfig();
|
|
200
|
+
if (config.authtoken) {
|
|
201
|
+
return { token: config.authtoken, source: 'saved config' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const ngrokConfigPaths = [
|
|
205
|
+
path.join(os.homedir(), '.config', 'ngrok', 'ngrok.yml'),
|
|
206
|
+
path.join(os.homedir(), '.ngrok2', 'ngrok.yml'),
|
|
207
|
+
path.join(os.homedir(), 'Library', 'Application Support', 'ngrok', 'ngrok.yml'),
|
|
208
|
+
// Windows
|
|
209
|
+
path.join(os.homedir(), 'AppData', 'Local', 'ngrok', 'ngrok.yml'),
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
for (const configPath of ngrokConfigPaths) {
|
|
213
|
+
try {
|
|
214
|
+
if (fs.existsSync(configPath)) {
|
|
215
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
216
|
+
const match = content.match(/authtoken:\s*(.+)/);
|
|
217
|
+
if (match && match[1].trim()) {
|
|
218
|
+
return { token: match[1].trim(), source: `ngrok config (${configPath})` };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} catch {}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Interactive Setup Wizard ────────────────────────────────────────────
|
|
228
|
+
async function runWizard(cliArgs) {
|
|
229
|
+
printBanner();
|
|
230
|
+
|
|
231
|
+
const config = loadConfig();
|
|
232
|
+
const isFirstRun = !config.email && !config.port;
|
|
233
|
+
|
|
234
|
+
if (isFirstRun) {
|
|
235
|
+
console.log(` ${fmt.info('Welcome! Let\'s set up your Antigravity Mobile Proxy.')}`);
|
|
236
|
+
console.log(` ${fmt.dim('This wizard will guide you through the configuration.')}`);
|
|
237
|
+
console.log(` ${fmt.dim('Your settings will be saved for next time.')}`);
|
|
238
|
+
console.log('');
|
|
239
|
+
} else {
|
|
240
|
+
console.log(` ${fmt.info('Welcome back! Loading your saved settings.')}`);
|
|
241
|
+
console.log(` ${fmt.dim('Press Enter to keep defaults, or type new values.')}`);
|
|
242
|
+
console.log('');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
printSeparator();
|
|
246
|
+
|
|
247
|
+
// ── Step 1: ngrok Authtoken ───────────────────────────────────────
|
|
248
|
+
console.log('');
|
|
249
|
+
console.log(` ${fmt.step('1/3', fmt.bold('ngrok Authentication'))}`);
|
|
250
|
+
console.log('');
|
|
251
|
+
|
|
252
|
+
const existingAuth = detectAuthtoken();
|
|
253
|
+
let authtoken = null;
|
|
254
|
+
|
|
255
|
+
if (existingAuth) {
|
|
256
|
+
const masked = existingAuth.token.slice(0, 8) + '••••••••' + existingAuth.token.slice(-4);
|
|
257
|
+
console.log(` ${fmt.success(`Found authtoken from ${fmt.dim(existingAuth.source)}`)}`);
|
|
258
|
+
console.log(` ${fmt.dim(' Token:')} ${masked}`);
|
|
259
|
+
console.log('');
|
|
260
|
+
|
|
261
|
+
const useExisting = await askYesNo(` Use this token?`);
|
|
262
|
+
if (useExisting) {
|
|
263
|
+
authtoken = existingAuth.token;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!authtoken) {
|
|
268
|
+
console.log('');
|
|
269
|
+
console.log(` ${fmt.warn('ngrok authtoken is required for tunneling.')}`);
|
|
270
|
+
console.log('');
|
|
271
|
+
console.log(` ${fmt.bold('How to get your authtoken:')}`);
|
|
272
|
+
console.log(` ${fmt.cyan('1.')} Go to ${fmt.link('https://dashboard.ngrok.com/signup')}`);
|
|
273
|
+
console.log(` ${fmt.cyan('2.')} Sign up (it's free) or log in`);
|
|
274
|
+
console.log(` ${fmt.cyan('3.')} Go to ${fmt.link('https://dashboard.ngrok.com/authtokens')}`);
|
|
275
|
+
console.log(` ${fmt.cyan('4.')} Copy your authtoken`);
|
|
276
|
+
console.log('');
|
|
277
|
+
|
|
278
|
+
authtoken = await askPassword(` ${fmt.cyan('?')} Paste your authtoken: `);
|
|
279
|
+
|
|
280
|
+
if (!authtoken) {
|
|
281
|
+
console.log('');
|
|
282
|
+
console.log(` ${fmt.error('Authtoken is required. Exiting.')}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log(` ${fmt.success('Authtoken received')}`);
|
|
287
|
+
|
|
288
|
+
const shouldSave = await askYesNo(` ${fmt.cyan('?')} Save authtoken for future use?`);
|
|
289
|
+
if (shouldSave) {
|
|
290
|
+
config.authtoken = authtoken;
|
|
291
|
+
saveConfig(config);
|
|
292
|
+
console.log(` ${fmt.success(`Saved to ${fmt.dim(CONFIG_FILE)}`)}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Step 2: Email ─────────────────────────────────────────────────
|
|
297
|
+
console.log('');
|
|
298
|
+
printSeparator();
|
|
299
|
+
console.log('');
|
|
300
|
+
console.log(` ${fmt.step('2/3', fmt.bold('Access Control'))}`);
|
|
301
|
+
console.log(` ${fmt.dim(' Only the specified Google email will be able to access your proxy.')}`);
|
|
302
|
+
console.log('');
|
|
303
|
+
|
|
304
|
+
const email = await askWithDefault(
|
|
305
|
+
` ${fmt.cyan('?')} Google email to allow`,
|
|
306
|
+
config.email || ''
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (!email || !email.includes('@')) {
|
|
310
|
+
console.log('');
|
|
311
|
+
console.log(` ${fmt.error('A valid email address is required.')}`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.log(` ${fmt.success(`Access restricted to ${fmt.cyan(email)}`)}`);
|
|
316
|
+
|
|
317
|
+
// ── Step 3: Port ──────────────────────────────────────────────────
|
|
318
|
+
console.log('');
|
|
319
|
+
printSeparator();
|
|
320
|
+
console.log('');
|
|
321
|
+
console.log(` ${fmt.step('3/3', fmt.bold('Server Configuration'))}`);
|
|
322
|
+
console.log('');
|
|
323
|
+
|
|
324
|
+
const port = await askWithDefault(
|
|
325
|
+
` ${fmt.cyan('?')} Local port for the server`,
|
|
326
|
+
config.port || '5555'
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
console.log(` ${fmt.success(`Server will run on port ${fmt.cyan(port)}`)}`);
|
|
330
|
+
|
|
331
|
+
// ── Save config ───────────────────────────────────────────────────
|
|
332
|
+
config.email = email;
|
|
333
|
+
config.port = port;
|
|
334
|
+
saveConfig(config);
|
|
335
|
+
|
|
336
|
+
// ── Summary ───────────────────────────────────────────────────────
|
|
337
|
+
console.log('');
|
|
338
|
+
printSeparator();
|
|
339
|
+
console.log('');
|
|
340
|
+
console.log(` ${fmt.bold('📋 Configuration Summary')}`);
|
|
341
|
+
console.log('');
|
|
342
|
+
console.log(` ${fmt.dim('Server Port')} ${fmt.cyan(port)}`);
|
|
343
|
+
console.log(` ${fmt.dim('Tunnel')} ${fmt.green('ngrok + Google OAuth')}`);
|
|
344
|
+
console.log(` ${fmt.dim('Allowed Email')} ${fmt.cyan(email)}`);
|
|
345
|
+
console.log(` ${fmt.dim('Authtoken')} ${authtoken.slice(0, 8)}••••••••${authtoken.slice(-4)}`);
|
|
346
|
+
console.log('');
|
|
347
|
+
|
|
348
|
+
const proceed = await askYesNo(` ${fmt.cyan('?')} Start the server?`);
|
|
349
|
+
|
|
350
|
+
if (!proceed) {
|
|
351
|
+
console.log('');
|
|
352
|
+
console.log(` ${fmt.dim('Settings saved. Run again anytime!')}`);
|
|
353
|
+
rl.close();
|
|
354
|
+
process.exit(0);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
rl.close();
|
|
358
|
+
|
|
359
|
+
return { email, port, authtoken };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── Project Setup (handles npx case) ────────────────────────────────────
|
|
363
|
+
function getPackageRoot() {
|
|
364
|
+
return path.resolve(__dirname, '..');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isInsideNodeModules(dir) {
|
|
368
|
+
return dir.includes('node_modules');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function copyDirSync(src, dest) {
|
|
372
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
373
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
374
|
+
|
|
375
|
+
for (const entry of entries) {
|
|
376
|
+
const srcPath = path.join(src, entry.name);
|
|
377
|
+
const destPath = path.join(dest, entry.name);
|
|
378
|
+
|
|
379
|
+
// Skip node_modules, .next, .git
|
|
380
|
+
if (['node_modules', '.next', '.git', '.env.local'].includes(entry.name)) continue;
|
|
381
|
+
|
|
382
|
+
if (entry.isDirectory()) {
|
|
383
|
+
copyDirSync(srcPath, destPath);
|
|
384
|
+
} else {
|
|
385
|
+
fs.copyFileSync(srcPath, destPath);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function getPackageVersion() {
|
|
391
|
+
try {
|
|
392
|
+
const pkgPath = path.join(getPackageRoot(), 'package.json');
|
|
393
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
394
|
+
return pkg.version || '0.0.0';
|
|
395
|
+
} catch {
|
|
396
|
+
return '0.0.0';
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function prepareWorkingDirectory() {
|
|
401
|
+
const packageRoot = getPackageRoot();
|
|
402
|
+
|
|
403
|
+
// If running from the actual project (not node_modules), use it directly
|
|
404
|
+
if (!isInsideNodeModules(packageRoot)) {
|
|
405
|
+
return packageRoot;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Running from npx/node_modules — copy to a working directory
|
|
409
|
+
const currentVersion = getPackageVersion();
|
|
410
|
+
const versionFile = path.join(APP_DIR, '.version');
|
|
411
|
+
|
|
412
|
+
// Check if already set up with the same version
|
|
413
|
+
let existingVersion = null;
|
|
414
|
+
try {
|
|
415
|
+
if (fs.existsSync(versionFile)) {
|
|
416
|
+
existingVersion = fs.readFileSync(versionFile, 'utf-8').trim();
|
|
417
|
+
}
|
|
418
|
+
} catch {}
|
|
419
|
+
|
|
420
|
+
if (existingVersion === currentVersion && fs.existsSync(path.join(APP_DIR, 'package.json'))) {
|
|
421
|
+
console.log(` ${fmt.success(`App v${currentVersion} already installed`)}`);
|
|
422
|
+
return APP_DIR;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Copy files to working directory
|
|
426
|
+
console.log(` ${fmt.dim('▸ Setting up app directory...')}`);
|
|
427
|
+
|
|
428
|
+
// Clean old version if exists
|
|
429
|
+
if (fs.existsSync(APP_DIR)) {
|
|
430
|
+
fs.rmSync(APP_DIR, { recursive: true, force: true });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
copyDirSync(packageRoot, APP_DIR);
|
|
434
|
+
|
|
435
|
+
// Write version marker
|
|
436
|
+
fs.writeFileSync(versionFile, currentVersion);
|
|
437
|
+
|
|
438
|
+
console.log(` ${fmt.success(`App files copied to ${fmt.dim(APP_DIR)}`)}`);
|
|
439
|
+
|
|
440
|
+
// Install dependencies
|
|
441
|
+
console.log(` ${fmt.dim('▸ Installing dependencies (this may take a minute)...')}`);
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
execSync('npm install --omit=dev', {
|
|
445
|
+
cwd: APP_DIR,
|
|
446
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
447
|
+
timeout: 120000,
|
|
448
|
+
});
|
|
449
|
+
console.log(` ${fmt.success('Dependencies installed')}`);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
console.log(` ${fmt.error('Failed to install dependencies')}`);
|
|
452
|
+
console.log(` ${err.stderr ? err.stderr.toString() : err.message}`);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return APP_DIR;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Spawn helper (cross-platform, no shell) ────────────────────────────
|
|
460
|
+
function spawnNext(args, cwd, stdio) {
|
|
461
|
+
// Always use node + module path — works on Windows, macOS, and Linux
|
|
462
|
+
// (node_modules/.bin/next is a .cmd on Windows, can't spawn without shell)
|
|
463
|
+
const nextEntry = path.join(cwd, 'node_modules', 'next', 'dist', 'bin', 'next');
|
|
464
|
+
return spawn(process.execPath, [nextEntry, ...args], { cwd, stdio });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ── Server Startup ──────────────────────────────────────────────────────
|
|
468
|
+
function startServer({ email, port, authtoken, noTunnel }) {
|
|
469
|
+
console.log('');
|
|
470
|
+
printSeparator();
|
|
471
|
+
console.log('');
|
|
472
|
+
console.log(` ${fmt.bold('🔧 Starting up...')}`);
|
|
473
|
+
console.log('');
|
|
474
|
+
|
|
475
|
+
// Prepare working directory (handles npx case)
|
|
476
|
+
const projectRoot = prepareWorkingDirectory();
|
|
477
|
+
|
|
478
|
+
// ── Build ─────────────────────────────────────────────────────────
|
|
479
|
+
process.stdout.write(` ${fmt.dim('▸ Building Next.js app...')}`);
|
|
480
|
+
|
|
481
|
+
const build = spawnNext(['build'], projectRoot, ['ignore', 'pipe', 'pipe']);
|
|
482
|
+
|
|
483
|
+
let buildOutput = '';
|
|
484
|
+
build.stdout.on('data', (data) => { buildOutput += data.toString(); });
|
|
485
|
+
build.stderr.on('data', (data) => { buildOutput += data.toString(); });
|
|
486
|
+
|
|
487
|
+
build.on('close', (code) => {
|
|
488
|
+
clearLine();
|
|
489
|
+
|
|
490
|
+
if (code !== 0) {
|
|
491
|
+
console.log(` ${fmt.error('Build failed!')}`);
|
|
492
|
+
console.log('');
|
|
493
|
+
console.log(buildOutput);
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
console.log(` ${fmt.success('Build complete')}`);
|
|
498
|
+
|
|
499
|
+
// ── Start Next.js ─────────────────────────────────────────────
|
|
500
|
+
process.stdout.write(` ${fmt.dim('▸ Starting server on port ' + port + '...')}`);
|
|
501
|
+
|
|
502
|
+
const nextServer = spawnNext(['start', '-p', port], projectRoot, ['ignore', 'pipe', 'pipe']);
|
|
503
|
+
|
|
504
|
+
let serverStarted = false;
|
|
505
|
+
|
|
506
|
+
nextServer.stdout.on('data', (data) => {
|
|
507
|
+
const line = data.toString();
|
|
508
|
+
if (!serverStarted && (line.includes('Ready') || line.includes('started') || line.includes(port))) {
|
|
509
|
+
serverStarted = true;
|
|
510
|
+
clearLine();
|
|
511
|
+
console.log(` ${fmt.success('Server running on port ' + port)}`);
|
|
512
|
+
|
|
513
|
+
if (!noTunnel) {
|
|
514
|
+
startTunnel({ port, email, authtoken, projectRoot });
|
|
515
|
+
} else {
|
|
516
|
+
printLocalOnly(port);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
nextServer.stderr.on('data', (data) => {
|
|
522
|
+
const line = data.toString().trim();
|
|
523
|
+
if (line && line.toLowerCase().includes('error')) {
|
|
524
|
+
console.log(` ${fmt.dim('[next]')} ${line}`);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
nextServer.on('close', (exitCode) => {
|
|
529
|
+
console.log(`\n ${fmt.dim('Next.js server stopped.')}`);
|
|
530
|
+
process.exit(exitCode);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// ── Graceful shutdown ─────────────────────────────────────────
|
|
534
|
+
const cleanup = () => {
|
|
535
|
+
console.log('');
|
|
536
|
+
console.log(` ${fmt.dim('👋 Shutting down...')}`);
|
|
537
|
+
nextServer.kill();
|
|
538
|
+
process.exit(0);
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
process.on('SIGINT', cleanup);
|
|
542
|
+
process.on('SIGTERM', cleanup);
|
|
543
|
+
|
|
544
|
+
// Fallback: if we don't detect "Ready", start tunnel after timeout
|
|
545
|
+
setTimeout(() => {
|
|
546
|
+
if (!serverStarted) {
|
|
547
|
+
serverStarted = true;
|
|
548
|
+
clearLine();
|
|
549
|
+
console.log(` ${fmt.success('Server likely running on port ' + port)}`);
|
|
550
|
+
if (!noTunnel) {
|
|
551
|
+
startTunnel({ port, email, authtoken, projectRoot });
|
|
552
|
+
} else {
|
|
553
|
+
printLocalOnly(port);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}, 8000);
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ── Start ngrok Tunnel ──────────────────────────────────────────────────
|
|
561
|
+
async function startTunnel({ port, email, authtoken, projectRoot }) {
|
|
562
|
+
process.stdout.write(` ${fmt.dim('▸ Opening ngrok tunnel...')}`);
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
// Try loading ngrok from the project's node_modules first
|
|
566
|
+
let ngrok;
|
|
567
|
+
const localNgrok = path.join(projectRoot, 'node_modules', '@ngrok', 'ngrok');
|
|
568
|
+
try {
|
|
569
|
+
ngrok = require(localNgrok);
|
|
570
|
+
} catch {
|
|
571
|
+
ngrok = require('@ngrok/ngrok');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const listener = await ngrok.forward({
|
|
575
|
+
addr: parseInt(port, 10),
|
|
576
|
+
authtoken: authtoken,
|
|
577
|
+
oauth_provider: 'google',
|
|
578
|
+
oauth_allow_emails: email,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
clearLine();
|
|
582
|
+
console.log(` ${fmt.success('ngrok tunnel established')}`);
|
|
583
|
+
|
|
584
|
+
const url = listener.url();
|
|
585
|
+
|
|
586
|
+
console.log('');
|
|
587
|
+
console.log(` ${c.bold}${c.cyan}╔═══════════════════════════════════════════════════════╗${c.reset}`);
|
|
588
|
+
console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
|
|
589
|
+
console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.green}${c.bold}🌐 Your app is live!${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
|
|
590
|
+
console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
|
|
591
|
+
console.log(` ${c.bold}${c.cyan}║${c.reset} ${url} ${' '.repeat(Math.max(0, 39 - url.length))}${c.bold}${c.cyan}║${c.reset}`);
|
|
592
|
+
console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
|
|
593
|
+
console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.dim}🔒 Google OAuth → ${email}${' '.repeat(Math.max(0, 23 - email.length))}${c.reset}${c.bold}${c.cyan}║${c.reset}`);
|
|
594
|
+
console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
|
|
595
|
+
console.log(` ${c.bold}${c.cyan}╚═══════════════════════════════════════════════════════╝${c.reset}`);
|
|
596
|
+
console.log('');
|
|
597
|
+
console.log(` ${fmt.dim('Press Ctrl+C to stop.')}`);
|
|
598
|
+
console.log('');
|
|
599
|
+
|
|
600
|
+
} catch (err) {
|
|
601
|
+
clearLine();
|
|
602
|
+
console.log(` ${fmt.error('ngrok tunnel failed!')}`);
|
|
603
|
+
console.log('');
|
|
604
|
+
console.log(` ${fmt.red(err.message)}`);
|
|
605
|
+
|
|
606
|
+
if (err.message.includes('authtoken') || err.message.includes('ERR_NGROK_')) {
|
|
607
|
+
console.log('');
|
|
608
|
+
console.log(` ${fmt.warn('Your authtoken may be invalid or expired.')}`);
|
|
609
|
+
console.log(` ${fmt.dim('Get a new one at:')} ${fmt.link('https://dashboard.ngrok.com/authtokens')}`);
|
|
610
|
+
console.log(` ${fmt.dim('Then run:')} ${fmt.cyan('npx antigravity-mobile-proxy --reset')}`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
console.log('');
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function printLocalOnly(port) {
|
|
618
|
+
console.log('');
|
|
619
|
+
console.log(` ${c.bold}${c.cyan}╔═══════════════════════════════════════════════════════╗${c.reset}`);
|
|
620
|
+
console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
|
|
621
|
+
console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.green}${c.bold}🖥 Running locally${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
|
|
622
|
+
console.log(` ${c.bold}${c.cyan}║${c.reset} ${fmt.cyan(`http://localhost:${port}`)} ${c.bold}${c.cyan}║${c.reset}`);
|
|
623
|
+
console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
|
|
624
|
+
console.log(` ${c.bold}${c.cyan}╚═══════════════════════════════════════════════════════╝${c.reset}`);
|
|
625
|
+
console.log('');
|
|
626
|
+
console.log(` ${fmt.dim('Press Ctrl+C to stop.')}`);
|
|
627
|
+
console.log('');
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ── Main ────────────────────────────────────────────────────────────────
|
|
631
|
+
async function main() {
|
|
632
|
+
const args = parseArgs();
|
|
633
|
+
|
|
634
|
+
if (args.help) {
|
|
635
|
+
printHelp();
|
|
636
|
+
process.exit(0);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (args.reset) {
|
|
640
|
+
try {
|
|
641
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
642
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
643
|
+
console.log(fmt.success('Configuration reset. Run again to reconfigure.'));
|
|
644
|
+
} else {
|
|
645
|
+
console.log(fmt.info('No saved configuration found.'));
|
|
646
|
+
}
|
|
647
|
+
} catch {}
|
|
648
|
+
process.exit(0);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (args.noTunnel) {
|
|
652
|
+
printBanner();
|
|
653
|
+
startServer({
|
|
654
|
+
email: null,
|
|
655
|
+
port: args.port || '5555',
|
|
656
|
+
authtoken: null,
|
|
657
|
+
noTunnel: true,
|
|
658
|
+
});
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (args.nonInteractive || (args.email && (args.authtoken || process.env.NGROK_AUTHTOKEN))) {
|
|
663
|
+
printBanner();
|
|
664
|
+
const authtoken = args.authtoken || process.env.NGROK_AUTHTOKEN;
|
|
665
|
+
const port = args.port || '5555';
|
|
666
|
+
|
|
667
|
+
console.log(` ${fmt.dim('Port:')} ${fmt.cyan(port)}`);
|
|
668
|
+
console.log(` ${fmt.dim('Email:')} ${fmt.cyan(args.email)}`);
|
|
669
|
+
console.log(` ${fmt.dim('Tunnel:')} ${fmt.green('ngrok + Google OAuth')}`);
|
|
670
|
+
console.log('');
|
|
671
|
+
|
|
672
|
+
startServer({
|
|
673
|
+
email: args.email,
|
|
674
|
+
port,
|
|
675
|
+
authtoken,
|
|
676
|
+
noTunnel: false,
|
|
677
|
+
});
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
const settings = await runWizard(args);
|
|
683
|
+
startServer({
|
|
684
|
+
email: settings.email,
|
|
685
|
+
port: settings.port,
|
|
686
|
+
authtoken: settings.authtoken,
|
|
687
|
+
noTunnel: false,
|
|
688
|
+
});
|
|
689
|
+
} catch (err) {
|
|
690
|
+
if (err.message === 'readline was closed') {
|
|
691
|
+
process.exit(0);
|
|
692
|
+
}
|
|
693
|
+
console.error(`\n ${fmt.error(err.message)}`);
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
main();
|