polaris-vpn 1.0.0 → 1.0.1
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 +4 -0
- package/docs/marketing-drafts.md +68 -0
- package/package.json +25 -7
- package/src/cli.js +18 -34
- package/src/commands/server.js +3 -7
- package/src/commands/start.js +5 -19
- package/src/core/deploy-service.js +11 -4
- package/src/utils/error-handler.js +19 -0
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
+
<div align="center">
|
|
6
|
+
<img src="./docs/demo.gif" alt="Polaris VPN Terminal Demo" width="800">
|
|
7
|
+
</div>
|
|
8
|
+
|
|
5
9
|
Your True North in Digital Privacy. `polaris` is a production-quality, open-source, self-hosted VPN CLI tool. It supports SSH dynamic port forwarding, WireGuard, and AmneziaWG (Stealth) tunnels.
|
|
6
10
|
|
|
7
11
|
### Prerequisites
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Marketing Drafts for Polaris VPN
|
|
2
|
+
|
|
3
|
+
Use these templates to post to various communities to drive your npm downloads.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Hacker News (Show HN)
|
|
8
|
+
**Title:** Show HN: I built a free, self-hosted stealth VPN CLI for Oracle Cloud
|
|
9
|
+
**Link / Text:**
|
|
10
|
+
|
|
11
|
+
I got tired of paying for commercial VPNs that harvest data, so I built Polaris. It’s an open-source CLI tool that automatically provisions a WireGuard or AmneziaWG (Stealth Mode) VPN on any Linux VPS in about 30 seconds.
|
|
12
|
+
|
|
13
|
+
I optimized it specifically for Oracle Cloud's "Always Free" ARM tier (2 OCPUs, 12GB RAM, 10TB bandwidth), so you can effectively get a high-performance personal VPN completely for free, forever.
|
|
14
|
+
|
|
15
|
+
Features:
|
|
16
|
+
- Native WireGuard & Stealth Mode (AmneziaWG for DPI bypass)
|
|
17
|
+
- Zero-Log Self-Hosted DNS (Unbound is automatically configured on the server)
|
|
18
|
+
- SOCKS5 / SSH tunnel fallback modes
|
|
19
|
+
- Built-in live bandwidth monitoring & terminal TUI dashboard
|
|
20
|
+
- System-wide Kill Switch
|
|
21
|
+
- Terminal QR codes for mobile setup
|
|
22
|
+
|
|
23
|
+
Would love for you to try it out and tear the code apart.
|
|
24
|
+
|
|
25
|
+
Repo: https://github.com/Divyo/polaris-vpn
|
|
26
|
+
`npm install -g polaris-vpn`
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. Reddit (r/selfhosted, r/privacy, r/homelab)
|
|
31
|
+
**Title:** I built a CLI tool to automate deploying a free, zero-log stealth VPN on Oracle Cloud in 30 seconds
|
|
32
|
+
|
|
33
|
+
**Body:**
|
|
34
|
+
Hey r/selfhosted!
|
|
35
|
+
|
|
36
|
+
I’ve been working on a project called **Polaris VPN**. The goal was to make spinning up a secure, self-hosted VPN as easy as typing a single command, without having to manually mess with `iptables` routing, DNS leaks, or WireGuard configs.
|
|
37
|
+
|
|
38
|
+
It works perfectly with the Oracle Cloud Free Tier. If you have an empty Ubuntu VM sitting there, you can run:
|
|
39
|
+
|
|
40
|
+
`polaris deploy --server ubuntu@<your-ip>`
|
|
41
|
+
|
|
42
|
+
And it will automatically:
|
|
43
|
+
1. Install WireGuard (or AmneziaWG for stealth/DPI bypass)
|
|
44
|
+
2. Setup UFW and Fail2Ban for security
|
|
45
|
+
3. Install and configure **Unbound** so your DNS queries never hit Cloudflare or Google (Zero-Log DNS)
|
|
46
|
+
4. Configure your local machine to route all traffic through it.
|
|
47
|
+
|
|
48
|
+
It also has a cool terminal dashboard (`polaris dashboard`) to monitor your bandwidth and peers, and can generate QR codes in the terminal (`polaris peer qr mobile`) to instantly connect your phone.
|
|
49
|
+
|
|
50
|
+
It's completely open-source (Apache 2.0).
|
|
51
|
+
Repo: https://github.com/Divyo/polaris-vpn
|
|
52
|
+
|
|
53
|
+
If anyone has an Oracle Cloud account or a spare VPS, I'd love to hear your feedback!
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 3. Dev.to / Medium Tutorial
|
|
58
|
+
**Title:** How to host your own Stealth VPN for free (and never pay for a VPN again)
|
|
59
|
+
|
|
60
|
+
**(Outline for your blog post):**
|
|
61
|
+
1. **The Problem:** Commercial VPNs are honeypots. They log your data, get blocked by Netflix/DPI, and cost $5-10/month.
|
|
62
|
+
2. **The Solution:** Oracle Cloud Always Free tier + Polaris VPN.
|
|
63
|
+
3. **Step 1:** Sign up for Oracle Cloud and launch an Ubuntu ARM VM.
|
|
64
|
+
4. **Step 2:** Run `npm install -g polaris-vpn`
|
|
65
|
+
5. **Step 3:** Run `polaris deploy --server ubuntu@your-ip`
|
|
66
|
+
6. **Step 4:** Run `polaris start --server ubuntu@your-ip`
|
|
67
|
+
7. **Explain the Tech:** Talk about AmneziaWG obfuscation, Unbound local DNS for privacy, and how it bypasses DPI (Deep Packet Inspection) which blocks normal WireGuard.
|
|
68
|
+
8. **Call to Action:** Ask them to star the repo and try it out!
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polaris-vpn",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Your True North in Digital Privacy. A self-hosted VPN CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"polaris": "src/cli.js",
|
|
8
|
-
"polaris-server": "src/
|
|
8
|
+
"polaris-server": "src/commands/server.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"src",
|
|
@@ -16,13 +16,23 @@
|
|
|
16
16
|
],
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
|
-
"url": "https://github.com/Divyo/polaris-vpn.git"
|
|
19
|
+
"url": "git+https://github.com/Divyo/polaris-vpn.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/Divyo/polaris-vpn#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/Divyo/polaris-vpn/issues"
|
|
24
|
+
},
|
|
25
|
+
"author": "Divyo",
|
|
26
|
+
"funding": {
|
|
27
|
+
"type": "github",
|
|
28
|
+
"url": "https://github.com/sponsors/Divyo"
|
|
20
29
|
},
|
|
21
30
|
"engines": {
|
|
22
31
|
"node": ">=18"
|
|
23
32
|
},
|
|
24
33
|
"scripts": {
|
|
25
|
-
"start": "node ./src/cli.js"
|
|
34
|
+
"start": "node ./src/cli.js",
|
|
35
|
+
"test": "node --test"
|
|
26
36
|
},
|
|
27
37
|
"license": "Apache-2.0",
|
|
28
38
|
"keywords": [
|
|
@@ -34,7 +44,16 @@
|
|
|
34
44
|
"cli",
|
|
35
45
|
"wireguard",
|
|
36
46
|
"self-hosted",
|
|
37
|
-
"oracle-cloud"
|
|
47
|
+
"oracle-cloud",
|
|
48
|
+
"free-vpn",
|
|
49
|
+
"ssh-tunnel",
|
|
50
|
+
"amneziawg",
|
|
51
|
+
"stealth-vpn",
|
|
52
|
+
"dpi-bypass",
|
|
53
|
+
"zero-trust",
|
|
54
|
+
"kill-switch",
|
|
55
|
+
"peer-to-peer",
|
|
56
|
+
"terminal"
|
|
38
57
|
],
|
|
39
58
|
"dependencies": {
|
|
40
59
|
"blessed": "^0.1.81",
|
|
@@ -51,7 +70,6 @@
|
|
|
51
70
|
"qrcode-terminal": "^0.12.0",
|
|
52
71
|
"socks-proxy-agent": "^10.1.0",
|
|
53
72
|
"ssh2": "^1.17.0",
|
|
54
|
-
"update-notifier": "^7.3.1"
|
|
55
|
-
"wireguard-tools": "^0.1.0"
|
|
73
|
+
"update-notifier": "^7.3.1"
|
|
56
74
|
}
|
|
57
75
|
}
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import updateNotifier from 'update-notifier';
|
|
7
7
|
import { spawnSync } from 'child_process';
|
|
8
8
|
import { printBanner, printError, printSuccess } from './utils/display.js';
|
|
9
|
+
import { handleError } from './utils/error-handler.js';
|
|
9
10
|
import chalk from 'chalk';
|
|
10
11
|
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -51,8 +52,7 @@ program
|
|
|
51
52
|
const run = (await import('./commands/start.js')).default;
|
|
52
53
|
await run(cmd.optsWithGlobals());
|
|
53
54
|
} catch (err) {
|
|
54
|
-
|
|
55
|
-
process.exit(1);
|
|
55
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
|
|
@@ -65,8 +65,7 @@ program
|
|
|
65
65
|
const run = (await import('./commands/stop.js')).default;
|
|
66
66
|
await run(cmd.optsWithGlobals());
|
|
67
67
|
} catch (err) {
|
|
68
|
-
|
|
69
|
-
process.exit(1);
|
|
68
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
70
69
|
}
|
|
71
70
|
});
|
|
72
71
|
|
|
@@ -80,8 +79,7 @@ program
|
|
|
80
79
|
const run = (await import('./commands/status.js')).default;
|
|
81
80
|
await run(cmd.optsWithGlobals());
|
|
82
81
|
} catch (err) {
|
|
83
|
-
|
|
84
|
-
process.exit(1);
|
|
82
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
85
83
|
}
|
|
86
84
|
});
|
|
87
85
|
|
|
@@ -107,8 +105,7 @@ program
|
|
|
107
105
|
const run = (await import('./commands/check.js')).default;
|
|
108
106
|
await run(cmd.optsWithGlobals());
|
|
109
107
|
} catch (err) {
|
|
110
|
-
|
|
111
|
-
process.exit(1);
|
|
108
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
112
109
|
}
|
|
113
110
|
});
|
|
114
111
|
|
|
@@ -122,8 +119,7 @@ program
|
|
|
122
119
|
const { addServer } = await import('./commands/servers.js');
|
|
123
120
|
await addServer(alias, cmd.optsWithGlobals());
|
|
124
121
|
} catch (err) {
|
|
125
|
-
|
|
126
|
-
process.exit(1);
|
|
122
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
127
123
|
}
|
|
128
124
|
});
|
|
129
125
|
|
|
@@ -136,8 +132,7 @@ program
|
|
|
136
132
|
const { listServers } = await import('./commands/servers.js');
|
|
137
133
|
await listServers(cmd.optsWithGlobals());
|
|
138
134
|
} catch (err) {
|
|
139
|
-
|
|
140
|
-
process.exit(1);
|
|
135
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
141
136
|
}
|
|
142
137
|
});
|
|
143
138
|
|
|
@@ -150,8 +145,7 @@ program
|
|
|
150
145
|
const { useServer } = await import('./commands/servers.js');
|
|
151
146
|
await useServer(alias, cmd.optsWithGlobals());
|
|
152
147
|
} catch (err) {
|
|
153
|
-
|
|
154
|
-
process.exit(1);
|
|
148
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
155
149
|
}
|
|
156
150
|
});
|
|
157
151
|
|
|
@@ -168,8 +162,7 @@ dnsCmd
|
|
|
168
162
|
const { dnsStart } = await import('./commands/dns.js');
|
|
169
163
|
await dnsStart(cmd.optsWithGlobals());
|
|
170
164
|
} catch (err) {
|
|
171
|
-
|
|
172
|
-
process.exit(1);
|
|
165
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
173
166
|
}
|
|
174
167
|
});
|
|
175
168
|
|
|
@@ -182,8 +175,7 @@ dnsCmd
|
|
|
182
175
|
const { dnsStop } = await import('./commands/dns.js');
|
|
183
176
|
await dnsStop(cmd.optsWithGlobals());
|
|
184
177
|
} catch (err) {
|
|
185
|
-
|
|
186
|
-
process.exit(1);
|
|
178
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
187
179
|
}
|
|
188
180
|
});
|
|
189
181
|
|
|
@@ -196,8 +188,7 @@ dnsCmd
|
|
|
196
188
|
const { dnsStatus } = await import('./commands/dns.js');
|
|
197
189
|
await dnsStatus(cmd.optsWithGlobals());
|
|
198
190
|
} catch (err) {
|
|
199
|
-
|
|
200
|
-
process.exit(1);
|
|
191
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
201
192
|
}
|
|
202
193
|
});
|
|
203
194
|
|
|
@@ -214,8 +205,7 @@ program
|
|
|
214
205
|
const run = (await import('./commands/deploy.js')).default;
|
|
215
206
|
await run(cmd.optsWithGlobals());
|
|
216
207
|
} catch (err) {
|
|
217
|
-
|
|
218
|
-
process.exit(1);
|
|
208
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
219
209
|
}
|
|
220
210
|
});
|
|
221
211
|
|
|
@@ -231,8 +221,7 @@ ksCmd
|
|
|
231
221
|
setKillSwitchConfig(true);
|
|
232
222
|
printSuccess('Kill switch enabled. All non-VPN traffic will be blocked when the tunnel is active.');
|
|
233
223
|
} catch (err) {
|
|
234
|
-
|
|
235
|
-
process.exit(1);
|
|
224
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
236
225
|
}
|
|
237
226
|
});
|
|
238
227
|
|
|
@@ -246,8 +235,7 @@ ksCmd
|
|
|
246
235
|
setKillSwitchConfig(false);
|
|
247
236
|
printSuccess('Kill switch disabled.');
|
|
248
237
|
} catch (err) {
|
|
249
|
-
|
|
250
|
-
process.exit(1);
|
|
238
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
251
239
|
}
|
|
252
240
|
});
|
|
253
241
|
|
|
@@ -262,8 +250,7 @@ peerCmd
|
|
|
262
250
|
const { peerAdd } = await import('./commands/peer.js');
|
|
263
251
|
await peerAdd(name, cmd.optsWithGlobals());
|
|
264
252
|
} catch (err) {
|
|
265
|
-
|
|
266
|
-
process.exit(1);
|
|
253
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
267
254
|
}
|
|
268
255
|
});
|
|
269
256
|
|
|
@@ -276,8 +263,7 @@ peerCmd
|
|
|
276
263
|
const { peerList } = await import('./commands/peer.js');
|
|
277
264
|
await peerList(cmd.optsWithGlobals());
|
|
278
265
|
} catch (err) {
|
|
279
|
-
|
|
280
|
-
process.exit(1);
|
|
266
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
281
267
|
}
|
|
282
268
|
});
|
|
283
269
|
|
|
@@ -290,8 +276,7 @@ peerCmd
|
|
|
290
276
|
const { peerRemove } = await import('./commands/peer.js');
|
|
291
277
|
await peerRemove(name, cmd.optsWithGlobals());
|
|
292
278
|
} catch (err) {
|
|
293
|
-
|
|
294
|
-
process.exit(1);
|
|
279
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
295
280
|
}
|
|
296
281
|
});
|
|
297
282
|
|
|
@@ -303,8 +288,7 @@ peerCmd
|
|
|
303
288
|
const { peerQr } = await import('./commands/peer.js');
|
|
304
289
|
await peerQr(name, cmd.optsWithGlobals());
|
|
305
290
|
} catch (err) {
|
|
306
|
-
|
|
307
|
-
process.exit(1);
|
|
291
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
308
292
|
}
|
|
309
293
|
});
|
|
310
294
|
|
package/src/commands/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { printSuccess,
|
|
1
|
+
import { printSuccess, printInfo } from '../utils/display.js';
|
|
2
|
+
import { handleError } from '../utils/error-handler.js';
|
|
2
3
|
import { startApiServer } from '../server/api.js';
|
|
3
4
|
|
|
4
5
|
export const serverStart = async (options) => {
|
|
@@ -25,11 +26,6 @@ Endpoints:
|
|
|
25
26
|
`);
|
|
26
27
|
}
|
|
27
28
|
} catch (err) {
|
|
28
|
-
|
|
29
|
-
console.log(JSON.stringify({ error: err.message }));
|
|
30
|
-
} else {
|
|
31
|
-
printError('Failed to start local API', err);
|
|
32
|
-
}
|
|
33
|
-
process.exitCode = 1;
|
|
29
|
+
handleError('Failed to start local API', err, isJson);
|
|
34
30
|
}
|
|
35
31
|
};
|
package/src/commands/start.js
CHANGED
|
@@ -3,7 +3,8 @@ import tls from 'tls';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { spawnSync } from 'child_process';
|
|
6
|
-
import {
|
|
6
|
+
import { printSuccess, createSpinner, printInfo } from '../utils/display.js';
|
|
7
|
+
import { handleError } from '../utils/error-handler.js';
|
|
7
8
|
import { getPublicIp, getProxiedIp } from '../net/ip-check.js';
|
|
8
9
|
import { getProfiles } from '../core/profile-service.js';
|
|
9
10
|
import { getActiveTunnel, startTunnel } from '../core/tunnel-service.js';
|
|
@@ -54,12 +55,7 @@ export default async (options) => {
|
|
|
54
55
|
if (active && profiles[active]) {
|
|
55
56
|
server = profiles[active];
|
|
56
57
|
} else {
|
|
57
|
-
|
|
58
|
-
console.log(JSON.stringify({ error: 'No server specified' }));
|
|
59
|
-
} else {
|
|
60
|
-
printError('No server specified. Use --server <user@host> or set an active profile with "polaris use".');
|
|
61
|
-
}
|
|
62
|
-
process.exitCode = 1;
|
|
58
|
+
handleError('No server specified. Use --server <user@host> or set an active profile with "polaris use".', null, isJson);
|
|
63
59
|
return;
|
|
64
60
|
}
|
|
65
61
|
}
|
|
@@ -67,12 +63,7 @@ export default async (options) => {
|
|
|
67
63
|
// Check if already running
|
|
68
64
|
const info = getActiveTunnel();
|
|
69
65
|
if (info) {
|
|
70
|
-
|
|
71
|
-
console.log(JSON.stringify({ error: 'Tunnel already running', pid: info.pid }));
|
|
72
|
-
} else {
|
|
73
|
-
printError(`Tunnel is already running connected to ${info.server}. Run "polaris stop" first.`);
|
|
74
|
-
}
|
|
75
|
-
process.exitCode = 1;
|
|
66
|
+
handleError(`Tunnel is already running connected to ${info.server}. Run "polaris stop" first.`, null, isJson);
|
|
76
67
|
return;
|
|
77
68
|
}
|
|
78
69
|
|
|
@@ -167,11 +158,6 @@ export default async (options) => {
|
|
|
167
158
|
|
|
168
159
|
} catch (err) {
|
|
169
160
|
if (!isJson && spinner) spinner.stop();
|
|
170
|
-
|
|
171
|
-
console.log(JSON.stringify({ error: err.message }));
|
|
172
|
-
} else {
|
|
173
|
-
printError('Failed to start tunnel', err);
|
|
174
|
-
}
|
|
175
|
-
process.exitCode = 1;
|
|
161
|
+
handleError('Failed to start tunnel', err, isJson);
|
|
176
162
|
}
|
|
177
163
|
};
|
|
@@ -39,9 +39,15 @@ export const deployServer = async (serverStr, options = {}) => {
|
|
|
39
39
|
const host = parts.length > 1 ? parts[1] : parts[0];
|
|
40
40
|
const isAwg = options.mode === 'amneziawg';
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
let privateKey = null;
|
|
43
|
+
if (options.privateKey) {
|
|
44
|
+
if (!fs.existsSync(options.privateKey)) {
|
|
45
|
+
throw new Error(`SSH private key file not found: ${options.privateKey}`);
|
|
46
|
+
}
|
|
47
|
+
privateKey = fs.readFileSync(options.privateKey);
|
|
48
|
+
} else {
|
|
49
|
+
privateKey = getDefaultPrivateKey();
|
|
50
|
+
}
|
|
45
51
|
|
|
46
52
|
if (!privateKey && !options.password) {
|
|
47
53
|
throw new Error('No SSH authentication method found. Please configure default SSH keys or specify key path/password.');
|
|
@@ -189,7 +195,8 @@ PersistentKeepalive = 25
|
|
|
189
195
|
port: options.port || 22,
|
|
190
196
|
username,
|
|
191
197
|
privateKey,
|
|
192
|
-
password: options.password
|
|
198
|
+
password: options.password,
|
|
199
|
+
readyTimeout: 10000 // 10 second timeout for SSH connection
|
|
193
200
|
});
|
|
194
201
|
});
|
|
195
202
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { printError } from './display.js';
|
|
2
|
+
|
|
3
|
+
export const handleError = (msg, err, isJson = false) => {
|
|
4
|
+
const isDebug = process.argv.includes('--debug');
|
|
5
|
+
|
|
6
|
+
if (isJson) {
|
|
7
|
+
const errorOutput = { error: msg, details: err ? err.message : undefined };
|
|
8
|
+
if (isDebug && err && err.stack) {
|
|
9
|
+
errorOutput.stack = err.stack;
|
|
10
|
+
}
|
|
11
|
+
console.log(JSON.stringify(errorOutput));
|
|
12
|
+
} else {
|
|
13
|
+
printError(msg, err);
|
|
14
|
+
if (isDebug && err && err.stack) {
|
|
15
|
+
console.error('\n' + err.stack);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
};
|