bypass-vpn 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # bypass-vpn
2
+
3
+ Route AI service traffic (Claude, ChatGPT, Firebase, Google Auth) through your Wi-Fi gateway to bypass VPN routing.
4
+
5
+ Works on **macOS** and **Windows**. Zero dependencies.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g bypass-vpn
11
+ ```
12
+
13
+ Or run directly without installing:
14
+
15
+ ```bash
16
+ # macOS
17
+ sudo npx bypass-vpn
18
+
19
+ # Windows (run from elevated PowerShell)
20
+ npx bypass-vpn
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ # Route all AI services through Wi-Fi
27
+ sudo bypass-vpn
28
+
29
+ # Route specific services only
30
+ sudo bypass-vpn --service claude --service chatgpt
31
+
32
+ # Remove routes
33
+ sudo bypass-vpn --remove
34
+
35
+ # Preview without executing
36
+ sudo bypass-vpn --dry-run
37
+
38
+ # List available services
39
+ bypass-vpn --list
40
+ ```
41
+
42
+ ## Supported Services
43
+
44
+ | Service | Domains |
45
+ |---------|---------|
46
+ | Claude | api.anthropic.com |
47
+ | ChatGPT | chatgpt.com, chat.openai.com, api.openai.com, + 5 more |
48
+ | Firebase | firestore.googleapis.com, securetoken.googleapis.com, + 3 more |
49
+ | Google Auth | accounts.google.com, oauth2.googleapis.com, + 2 more |
50
+
51
+ Run `bypass-vpn --list` for the full domain list.
52
+
53
+ ## How It Works
54
+
55
+ 1. Detects your Wi-Fi gateway IP
56
+ 2. Resolves each service domain to its current IP addresses
57
+ 3. Adds host-specific routes through the Wi-Fi gateway, bypassing VPN's default route
58
+
59
+ Routes are ephemeral — they reset on reboot or network change. Re-run as needed.
60
+
61
+ ## Requirements
62
+
63
+ - Node.js >= 16
64
+ - macOS or Windows
65
+ - Root/Administrator access (needed to modify routing table)
66
+
67
+ ## Uninstall
68
+
69
+ ```bash
70
+ npm uninstall -g bypass-vpn
71
+ ```
72
+
73
+ ## License
74
+
75
+ MIT
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { showBanner, showSummary, c, Spinner } = require('../src/ui');
4
+ const { ensureAdmin } = require('../src/platform');
5
+ const { detect } = require('../src/gateway');
6
+ const { resolveAll } = require('../src/resolver');
7
+ const { addRoute, removeRoute } = require('../src/router');
8
+ const services = require('../src/services');
9
+ const { version } = require('../package.json');
10
+
11
+ // ── Arg parsing ────────────────────────────────────────────────
12
+
13
+ const args = process.argv.slice(2);
14
+ const flags = {
15
+ help: args.includes('--help') || args.includes('-h'),
16
+ version: args.includes('--version') || args.includes('-v'),
17
+ remove: args.includes('--remove') || args.includes('-r'),
18
+ dryRun: args.includes('--dry-run'),
19
+ list: args.includes('--list'),
20
+ services: [],
21
+ };
22
+
23
+ for (let i = 0; i < args.length; i++) {
24
+ if (args[i] === '--service' && args[i + 1]) {
25
+ flags.services.push(args[i + 1].toLowerCase());
26
+ i++;
27
+ }
28
+ }
29
+
30
+ // ── Help ───────────────────────────────────────────────────────
31
+
32
+ if (flags.help) {
33
+ console.log(`
34
+ ${c.bold('bypass-vpn')} v${version}
35
+ Route AI service traffic through Wi-Fi gateway to bypass VPN.
36
+
37
+ ${c.bold('Usage:')}
38
+ ${c.cyan('sudo')} bypass-vpn Add routes (macOS)
39
+ bypass-vpn Add routes (Windows, elevated)
40
+ bypass-vpn ${c.dim('--remove')} Remove previously added routes
41
+ bypass-vpn ${c.dim('--dry-run')} Show commands without executing
42
+ bypass-vpn ${c.dim('--service claude')} Route specific service(s) only
43
+ bypass-vpn ${c.dim('--list')} List available services
44
+
45
+ ${c.bold('Flags:')}
46
+ -h, --help Show this help
47
+ -v, --version Show version
48
+ -r, --remove Remove routes instead of adding
49
+ --dry-run Print route commands without executing
50
+ --service <name> Route only specified service (repeatable)
51
+ --list List services and their domains
52
+
53
+ ${c.bold('Services:')} claude, chatgpt, firebase, googleauth
54
+
55
+ ${c.bold('Examples:')}
56
+ sudo npx bypass-vpn
57
+ sudo bypass-vpn --service claude --service chatgpt
58
+ sudo bypass-vpn --remove
59
+ `);
60
+ process.exit(0);
61
+ }
62
+
63
+ // ── Version ────────────────────────────────────────────────────
64
+
65
+ if (flags.version) {
66
+ console.log(version);
67
+ process.exit(0);
68
+ }
69
+
70
+ // ── List ───────────────────────────────────────────────────────
71
+
72
+ if (flags.list) {
73
+ console.log('');
74
+ for (const [key, svc] of Object.entries(services)) {
75
+ console.log(` ${c.bold(svc.name)} ${c.dim(`(--service ${key})`)}`);
76
+ for (const d of svc.domains) {
77
+ console.log(` ${c.dim('-')} ${d}`);
78
+ }
79
+ console.log('');
80
+ }
81
+ process.exit(0);
82
+ }
83
+
84
+ // ── Main ───────────────────────────────────────────────────────
85
+
86
+ async function main() {
87
+ showBanner();
88
+
89
+ if (!flags.dryRun) {
90
+ ensureAdmin();
91
+ }
92
+
93
+ // Detect gateway
94
+ const spinner = new Spinner();
95
+ spinner.start('Detecting Wi-Fi gateway...');
96
+
97
+ const gateway = detect();
98
+ if (!gateway) {
99
+ spinner.stop(c.red('x'), c.red('No Wi-Fi gateway found — connect to Wi-Fi first!'));
100
+ process.exit(1);
101
+ }
102
+ spinner.stop(c.green('OK'), `Gateway: ${c.bold(gateway)}`);
103
+
104
+ // Select services
105
+ let selectedServices;
106
+ if (flags.services.length > 0) {
107
+ selectedServices = {};
108
+ for (const key of flags.services) {
109
+ if (!services[key]) {
110
+ console.error(c.red(` Unknown service: ${key}`));
111
+ console.error(c.dim(` Available: ${Object.keys(services).join(', ')}`));
112
+ process.exit(1);
113
+ }
114
+ selectedServices[key] = services[key];
115
+ }
116
+ } else {
117
+ selectedServices = services;
118
+ }
119
+
120
+ // Process each service
121
+ const allIps = new Set();
122
+ let totalRouted = 0;
123
+ let totalSkipped = 0;
124
+ let totalFailed = 0;
125
+
126
+ for (const [, svc] of Object.entries(selectedServices)) {
127
+ console.log('');
128
+ console.log(` ${c.bold(svc.name)} ${c.dim(`(${svc.domains.length} domain${svc.domains.length > 1 ? 's' : ''})`)}`);
129
+
130
+ const { resolved, failed } = await resolveAll(svc.domains);
131
+
132
+ for (const domain of failed) {
133
+ console.log(` ${c.yellow('--')} ${c.dim(domain)} ${c.dim('— no A records')}`);
134
+ totalSkipped++;
135
+ }
136
+
137
+ for (const [domain, ips] of resolved) {
138
+ const newIps = ips.filter((ip) => !allIps.has(ip));
139
+ const dupeCount = ips.length - newIps.length;
140
+
141
+ if (newIps.length === 0) {
142
+ console.log(` ${c.yellow('--')} ${c.dim(domain)} ${c.dim('— IPs already routed')}`);
143
+ totalSkipped++;
144
+ continue;
145
+ }
146
+
147
+ let hostFailed = false;
148
+ for (const ip of newIps) {
149
+ if (flags.remove) {
150
+ const result = removeRoute(ip);
151
+ if (!result.success) hostFailed = true;
152
+ } else {
153
+ const result = addRoute(ip, gateway, { dryRun: flags.dryRun });
154
+ if (flags.dryRun) {
155
+ console.log(` ${c.dim('$')} ${c.dim(result.cmd)}`);
156
+ }
157
+ if (!result.success) hostFailed = true;
158
+ }
159
+ allIps.add(ip);
160
+ }
161
+
162
+ if (!flags.dryRun) {
163
+ const action = flags.remove ? 'removed' : 'routed';
164
+ const ipStr = newIps.join(', ');
165
+ const dupeNote = dupeCount > 0 ? c.dim(` (+${dupeCount} dupes)`) : '';
166
+ if (hostFailed) {
167
+ console.log(` ${c.red('x')} ${domain} — failed`);
168
+ totalFailed++;
169
+ } else {
170
+ console.log(` ${c.green('OK')} ${domain} ${c.dim(`-> ${ipStr}`)}${dupeNote}`);
171
+ totalRouted++;
172
+ }
173
+ } else {
174
+ totalRouted++;
175
+ }
176
+ }
177
+ }
178
+
179
+ if (!flags.dryRun) {
180
+ showSummary({ routed: totalRouted, skipped: totalSkipped, failed: totalFailed });
181
+ if (!flags.remove) {
182
+ console.log(` ${c.dim('To remove these routes later:')} sudo bypass-vpn --remove`);
183
+ console.log('');
184
+ }
185
+ } else {
186
+ console.log('');
187
+ console.log(` ${c.cyan(c.bold('Dry run complete.'))} No routes were modified.`);
188
+ console.log(` ${c.dim(`${totalRouted} domain(s) would be routed, ${totalSkipped} skipped.`)}`);
189
+ console.log('');
190
+ }
191
+ }
192
+
193
+ main().catch((err) => {
194
+ console.error(c.red(` Error: ${err.message}`));
195
+ process.exit(1);
196
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "bypass-vpn",
3
+ "version": "1.0.0",
4
+ "description": "Route AI service traffic (Claude, ChatGPT, Firebase) through Wi-Fi gateway to bypass VPN",
5
+ "bin": {
6
+ "bypass-vpn": "./bin/bypass-vpn.js"
7
+ },
8
+ "engines": {
9
+ "node": ">=16.0.0"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "src/"
14
+ ],
15
+ "keywords": [
16
+ "vpn",
17
+ "bypass",
18
+ "route",
19
+ "ai",
20
+ "claude",
21
+ "chatgpt",
22
+ "firebase",
23
+ "networking",
24
+ "wifi"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/ProjectAJ14/bypass-ai-vpn"
31
+ }
32
+ }
package/src/gateway.js ADDED
@@ -0,0 +1,62 @@
1
+ const { execSync } = require('child_process');
2
+ const { getPlatform } = require('./platform');
3
+
4
+ function detect() {
5
+ const platform = getPlatform();
6
+
7
+ if (platform === 'darwin') {
8
+ return detectMac();
9
+ }
10
+ if (platform === 'win32') {
11
+ return detectWindows();
12
+ }
13
+ return null;
14
+ }
15
+
16
+ function detectMac() {
17
+ try {
18
+ const output = execSync('netstat -nr', { encoding: 'utf8', timeout: 5000 });
19
+ for (const line of output.split('\n')) {
20
+ const parts = line.trim().split(/\s+/);
21
+ if (parts[0] === 'default' && parts[parts.length - 1] === 'en0') {
22
+ return parts[1];
23
+ }
24
+ }
25
+ } catch {}
26
+
27
+ // Fallback: networksetup
28
+ try {
29
+ const output = execSync('networksetup -getinfo Wi-Fi', { encoding: 'utf8', timeout: 5000 });
30
+ const match = output.match(/^Router:\s+(.+)$/m);
31
+ if (match) return match[1].trim();
32
+ } catch {}
33
+
34
+ return null;
35
+ }
36
+
37
+ function detectWindows() {
38
+ // Try PowerShell Get-NetRoute
39
+ try {
40
+ const cmd = `powershell -NoProfile -Command "Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Where-Object { $_.InterfaceAlias -like '*Wi-Fi*' -or $_.InterfaceAlias -like '*Wireless*' } | Select-Object -First 1 -ExpandProperty NextHop"`;
41
+ const output = execSync(cmd, { encoding: 'utf8', timeout: 10000 }).trim();
42
+ if (output && /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(output)) {
43
+ return output;
44
+ }
45
+ } catch {}
46
+
47
+ // Fallback: parse ipconfig
48
+ try {
49
+ const output = execSync('ipconfig', { encoding: 'utf8', timeout: 5000 });
50
+ const sections = output.split(/\r?\n\r?\n/);
51
+ for (const section of sections) {
52
+ if (/Wi-Fi|Wireless/i.test(section)) {
53
+ const match = section.match(/Default Gateway[.\s]*:\s*([\d.]+)/);
54
+ if (match) return match[1];
55
+ }
56
+ }
57
+ } catch {}
58
+
59
+ return null;
60
+ }
61
+
62
+ module.exports = { detect };
@@ -0,0 +1,37 @@
1
+ const { execSync } = require('child_process');
2
+ const { c } = require('./ui');
3
+
4
+ function getPlatform() {
5
+ const p = process.platform;
6
+ if (p === 'darwin' || p === 'win32') return p;
7
+ return 'unsupported';
8
+ }
9
+
10
+ function ensureAdmin() {
11
+ const platform = getPlatform();
12
+
13
+ if (platform === 'unsupported') {
14
+ console.error(c.red(' Unsupported platform. Only macOS and Windows are supported.'));
15
+ process.exit(1);
16
+ }
17
+
18
+ if (platform === 'darwin') {
19
+ if (process.getuid() !== 0) {
20
+ console.error(c.yellow(' This tool needs root privileges to modify routes.'));
21
+ console.error(c.dim(' Run: sudo bypass-vpn'));
22
+ process.exit(1);
23
+ }
24
+ }
25
+
26
+ if (platform === 'win32') {
27
+ try {
28
+ execSync('net session', { stdio: 'ignore' });
29
+ } catch {
30
+ console.error(c.yellow(' This tool needs Administrator privileges to modify routes.'));
31
+ console.error(c.dim(' Run from an elevated Command Prompt or PowerShell.'));
32
+ process.exit(1);
33
+ }
34
+ }
35
+ }
36
+
37
+ module.exports = { getPlatform, ensureAdmin };
@@ -0,0 +1,42 @@
1
+ const dns = require('dns');
2
+ const dnsPromises = dns.promises;
3
+
4
+ async function resolveAll(domains) {
5
+ const resolved = new Map();
6
+ const failed = [];
7
+
8
+ const results = await Promise.allSettled(
9
+ domains.map(async (domain) => {
10
+ const ips = await withTimeout(dnsPromises.resolve4(domain), 5000);
11
+ return { domain, ips };
12
+ })
13
+ );
14
+
15
+ for (const result of results) {
16
+ if (result.status === 'fulfilled') {
17
+ const { domain, ips } = result.value;
18
+ if (ips && ips.length > 0) {
19
+ resolved.set(domain, ips);
20
+ } else {
21
+ failed.push(domain);
22
+ }
23
+ } else {
24
+ // Extract domain from the original array by index
25
+ const idx = results.indexOf(result);
26
+ failed.push(domains[idx]);
27
+ }
28
+ }
29
+
30
+ return { resolved, failed };
31
+ }
32
+
33
+ function withTimeout(promise, ms) {
34
+ return new Promise((resolve, reject) => {
35
+ const timer = setTimeout(() => reject(new Error('DNS timeout')), ms);
36
+ promise
37
+ .then((val) => { clearTimeout(timer); resolve(val); })
38
+ .catch((err) => { clearTimeout(timer); reject(err); });
39
+ });
40
+ }
41
+
42
+ module.exports = { resolveAll };
package/src/router.js ADDED
@@ -0,0 +1,65 @@
1
+ const { execSync } = require('child_process');
2
+ const { getPlatform } = require('./platform');
3
+
4
+ const IP_REGEX = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
5
+
6
+ function validateIp(ip) {
7
+ if (!IP_REGEX.test(ip)) {
8
+ throw new Error(`Invalid IP address: ${ip}`);
9
+ }
10
+ }
11
+
12
+ function addRoute(ip, gateway, { dryRun = false } = {}) {
13
+ validateIp(ip);
14
+ validateIp(gateway);
15
+
16
+ const platform = getPlatform();
17
+ let cmd;
18
+
19
+ if (platform === 'darwin') {
20
+ // Remove existing route first (ignore errors)
21
+ const delCmd = `route -n delete -host ${ip} 2>/dev/null || true`;
22
+ cmd = `route -n add -host ${ip} ${gateway}`;
23
+ if (dryRun) {
24
+ return { success: true, ip, cmd };
25
+ }
26
+ try { execSync(delCmd, { stdio: 'ignore' }); } catch {}
27
+ } else {
28
+ // Windows: remove existing then add
29
+ const delCmd = `route delete ${ip}`;
30
+ cmd = `route add ${ip} mask 255.255.255.255 ${gateway}`;
31
+ if (dryRun) {
32
+ return { success: true, ip, cmd };
33
+ }
34
+ try { execSync(delCmd, { stdio: 'ignore' }); } catch {}
35
+ }
36
+
37
+ try {
38
+ execSync(cmd, { stdio: 'ignore' });
39
+ return { success: true, ip };
40
+ } catch (err) {
41
+ return { success: false, ip, error: err.message };
42
+ }
43
+ }
44
+
45
+ function removeRoute(ip) {
46
+ validateIp(ip);
47
+
48
+ const platform = getPlatform();
49
+ let cmd;
50
+
51
+ if (platform === 'darwin') {
52
+ cmd = `route -n delete -host ${ip}`;
53
+ } else {
54
+ cmd = `route delete ${ip}`;
55
+ }
56
+
57
+ try {
58
+ execSync(cmd, { stdio: 'ignore' });
59
+ return { success: true, ip };
60
+ } catch (err) {
61
+ return { success: false, ip, error: err.message };
62
+ }
63
+ }
64
+
65
+ module.exports = { addRoute, removeRoute };
@@ -0,0 +1,38 @@
1
+ module.exports = {
2
+ claude: {
3
+ name: 'Claude',
4
+ domains: ['api.anthropic.com'],
5
+ },
6
+ chatgpt: {
7
+ name: 'ChatGPT',
8
+ domains: [
9
+ 'chatgpt.com',
10
+ 'ab.chatgpt.com',
11
+ 'chat.openai.com',
12
+ 'api.openai.com',
13
+ 'auth0.openai.com',
14
+ 'cdn.oaistatic.com',
15
+ 'files.oaiusercontent.com',
16
+ 'events.statsigapi.net',
17
+ ],
18
+ },
19
+ firebase: {
20
+ name: 'Firebase',
21
+ domains: [
22
+ 'firestore.googleapis.com',
23
+ 'securetoken.googleapis.com',
24
+ 'identitytoolkit.googleapis.com',
25
+ 'narad-muni-14.firebaseapp.com',
26
+ 'narad-muni-14.firebasestorage.app',
27
+ ],
28
+ },
29
+ googleauth: {
30
+ name: 'Google Auth',
31
+ domains: [
32
+ 'accounts.google.com',
33
+ 'oauth2.googleapis.com',
34
+ 'www.googleapis.com',
35
+ 'iamcredentials.googleapis.com',
36
+ ],
37
+ },
38
+ };
package/src/ui.js ADDED
@@ -0,0 +1,66 @@
1
+ const { version } = require('../package.json');
2
+
3
+ const c = {
4
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
5
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
6
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
7
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
8
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
9
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
10
+ };
11
+
12
+ const SPIN_FRAMES = ['|', '/', '-', '\\'];
13
+
14
+ class Spinner {
15
+ constructor() {
16
+ this._interval = null;
17
+ this._frame = 0;
18
+ }
19
+
20
+ start(text) {
21
+ this._frame = 0;
22
+ this._interval = setInterval(() => {
23
+ const frame = c.cyan(SPIN_FRAMES[this._frame % SPIN_FRAMES.length]);
24
+ process.stderr.write(`\r ${frame} ${text}`);
25
+ this._frame++;
26
+ }, 80);
27
+ }
28
+
29
+ stop(symbol, text) {
30
+ if (this._interval) {
31
+ clearInterval(this._interval);
32
+ this._interval = null;
33
+ }
34
+ process.stderr.write(`\r ${symbol} ${text}\x1b[K\n`);
35
+ }
36
+ }
37
+
38
+ function showBanner() {
39
+ console.log('');
40
+ console.log(c.bold(c.cyan(' ╭─────────────────────────────────────╮')));
41
+ console.log(c.bold(c.cyan(` │ bypass-vpn v${version.padEnd(18)}│`)));
42
+ console.log(c.bold(c.cyan(' │ Route AI traffic around your VPN │')));
43
+ console.log(c.bold(c.cyan(' ╰─────────────────────────────────────╯')));
44
+ console.log('');
45
+ }
46
+
47
+ function showSummary({ routed, skipped, failed }) {
48
+ console.log('');
49
+ console.log(c.bold(c.cyan(' ╭─────────────────────────────────────╮')));
50
+ console.log(c.bold(c.cyan(' │ Results │')));
51
+ console.log(c.bold(c.cyan(' ├─────────────────────────────────────┤')));
52
+ console.log(c.bold(c.cyan(' │')) + ` ${c.green('Routed:')} ${String(routed + ' host(s)').padEnd(26)}` + c.bold(c.cyan('│')));
53
+ console.log(c.bold(c.cyan(' │')) + ` ${c.yellow('Skipped:')} ${String(skipped + ' host(s)').padEnd(26)}` + c.bold(c.cyan('│')));
54
+ console.log(c.bold(c.cyan(' │')) + ` ${c.red('Failed:')} ${String(failed + ' host(s)').padEnd(26)}` + c.bold(c.cyan('│')));
55
+ console.log(c.bold(c.cyan(' ╰─────────────────────────────────────╯')));
56
+ console.log('');
57
+
58
+ if (failed === 0) {
59
+ console.log(` ${c.green(c.bold('All clear!'))} VPN bypassed for AI services.`);
60
+ } else {
61
+ console.log(` ${c.yellow(c.bold('Partial success.'))} Some routes failed — see above.`);
62
+ }
63
+ console.log('');
64
+ }
65
+
66
+ module.exports = { c, Spinner, showBanner, showSummary };