polaris-vpn 0.1.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.1] - 2026-06-28
9
+
10
+ ### Fixed
11
+ - Gracefully handled interactive `sudo` password prompts during WireGuard configuration when running in `--json` mode.
12
+ - Improved robust error handling for `wg-quick` start/stop routines.
13
+
14
+ ## [0.3.0] - 2026-06-28
15
+
16
+ ### Added
17
+ - Automated system-wide VPN deployment command (`polaris deploy --server user@host`) for Ubuntu 22.04.
18
+ - Peer command namespace (`polaris peer add/list/remove/qr`) to manage multiple WireGuard client connections.
19
+ - Terminal QR code generation (`polaris peer qr <name>`) using `qrcode-terminal` for mobile client pairing.
20
+ - OS-level Network Kill Switch (`polaris config set kill-switch true`) for Linux (`iptables`) and macOS (`pfctl`) interfaces.
21
+ - Pure JavaScript base64 Curve25519/X25519 key derivation helper using standard Node.js `crypto` (no local binary `wg` CLI command dependency).
22
+ - Integrated smart auto-detection mode fallback sequence (`wireguard` -> `tls` -> `ssh`) for client tunnel startup.
23
+
24
+ ## [0.2.0] - 2026-06-28
25
+
26
+ ### Added
27
+ - Companion `polaris-server` dynamic HTTP/TLS forwarding proxy on port 8443.
28
+ - local DNS-over-HTTPS (DoH) resolver command namespace (`polaris dns start/stop/status`) on port 5354.
29
+ - TLS tunnel client option (`polaris start --mode tls`).
30
+ - Auto-reconnect with exponential backoff & keepalive monitoring.
31
+ - Desktop alerts/notifications via `node-notifier`.
32
+ - Refactored codebase to highly modular MVC structure with centralized configurations and daemon manager.
33
+ - Static runner scripts to ensure full compatibility with global NPM installation permissions.
34
+
8
35
  ## [0.1.0] - 2026-06-27
9
36
 
10
37
  ### Added
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "polaris-vpn",
3
- "version": "0.1.0",
3
+ "version": "0.3.1",
4
4
  "description": "Your True North in Digital Privacy. A self-hosted VPN CLI.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "polaris": "src/cli.js"
7
+ "polaris": "src/cli.js",
8
+ "polaris-server": "src/server/server.js"
8
9
  },
9
10
  "files": [
10
11
  "src",
@@ -35,8 +36,13 @@
35
36
  "cli-table3": "^0.6.5",
36
37
  "commander": "^15.0.0",
37
38
  "conf": "^15.1.0",
39
+ "dns2": "^3.0.0",
38
40
  "node-fetch": "^3.3.2",
41
+ "node-notifier": "^10.0.1",
39
42
  "ora": "^9.4.1",
40
- "socks-proxy-agent": "^10.1.0"
43
+ "qrcode-terminal": "^0.12.0",
44
+ "socks-proxy-agent": "^10.1.0",
45
+ "ssh2": "^1.17.0",
46
+ "wireguard-tools": "^0.1.0"
41
47
  }
42
48
  }
package/src/cli.js CHANGED
@@ -3,7 +3,7 @@ import { Command } from 'commander';
3
3
  import { readFileSync } from 'fs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import path from 'path';
6
- import { printBanner, printError } from './ui/display.js';
6
+ import { printBanner, printError } from './utils/display.js';
7
7
 
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = path.dirname(__filename);
@@ -22,9 +22,10 @@ const willPrintJson = () => process.argv.includes('--json');
22
22
 
23
23
  program
24
24
  .command('start')
25
- .description('Start the encrypted SSH tunnel')
26
- .option('-s, --server <user@host>', 'SSH server to connect to')
25
+ .description('Start the encrypted SSH, TLS or WireGuard tunnel')
26
+ .option('-s, --server <user@host>', 'SSH/TLS/WireGuard server to connect to')
27
27
  .option('-p, --port <number>', 'Local SOCKS5 port to bind', '1080')
28
+ .option('-m, --mode <type>', 'Tunnel mode: ssh, tls, wireguard or auto', 'auto')
28
29
  .action(async (options, cmd) => {
29
30
  if (!cmd.optsWithGlobals().json) printBanner();
30
31
  try {
@@ -121,6 +122,153 @@ program
121
122
  }
122
123
  });
123
124
 
125
+ const dnsCmd = program.command('dns').description('Manage local DNS-over-HTTPS resolver');
126
+
127
+ dnsCmd
128
+ .command('start')
129
+ .description('Start the local DoH resolver')
130
+ .option('-p, --port <number>', 'Local port to bind', '5354')
131
+ .option('-u, --upstream <provider>', 'Upstream DoH provider (cloudflare, google, or custom URL)', 'cloudflare')
132
+ .action(async (options, cmd) => {
133
+ if (!cmd.optsWithGlobals().json) printBanner();
134
+ try {
135
+ const { dnsStart } = await import('./commands/dns.js');
136
+ await dnsStart(cmd.optsWithGlobals());
137
+ } catch (err) {
138
+ if (!cmd.optsWithGlobals().json) printError('Command failed', err);
139
+ process.exit(1);
140
+ }
141
+ });
142
+
143
+ dnsCmd
144
+ .command('stop')
145
+ .description('Stop the local DoH resolver')
146
+ .action(async (options, cmd) => {
147
+ if (!cmd.optsWithGlobals().json) printBanner();
148
+ try {
149
+ const { dnsStop } = await import('./commands/dns.js');
150
+ await dnsStop(cmd.optsWithGlobals());
151
+ } catch (err) {
152
+ if (!cmd.optsWithGlobals().json) printError('Command failed', err);
153
+ process.exit(1);
154
+ }
155
+ });
156
+
157
+ dnsCmd
158
+ .command('status')
159
+ .description('Show local DoH resolver status')
160
+ .action(async (options, cmd) => {
161
+ if (!cmd.optsWithGlobals().json) printBanner();
162
+ try {
163
+ const { dnsStatus } = await import('./commands/dns.js');
164
+ await dnsStatus(cmd.optsWithGlobals());
165
+ } catch (err) {
166
+ if (!cmd.optsWithGlobals().json) printError('Command failed', err);
167
+ process.exit(1);
168
+ }
169
+ });
170
+
171
+ program
172
+ .command('deploy')
173
+ .description('Provision a fresh Ubuntu server as a WireGuard server')
174
+ .requiredOption('-s, --server <user@host>', 'SSH server to configure')
175
+ .option('-i, --identity <path>', 'SSH private key file path')
176
+ .option('-p, --password <password>', 'SSH password (alternative to identity)')
177
+ .action(async (options, cmd) => {
178
+ if (!cmd.optsWithGlobals().json) printBanner();
179
+ try {
180
+ const run = (await import('./commands/deploy.js')).default;
181
+ await run(cmd.optsWithGlobals());
182
+ } catch (err) {
183
+ if (!cmd.optsWithGlobals().json) printError('Command failed', err);
184
+ process.exit(1);
185
+ }
186
+ });
187
+
188
+ const peerCmd = program.command('peer').description('Manage WireGuard client peers');
189
+
190
+ peerCmd
191
+ .command('add <name>')
192
+ .description('Generate and add a new peer config')
193
+ .action(async (name, options, cmd) => {
194
+ if (!cmd.optsWithGlobals().json) printBanner();
195
+ try {
196
+ const { peerAdd } = await import('./commands/peer.js');
197
+ await peerAdd(name, cmd.optsWithGlobals());
198
+ } catch (err) {
199
+ if (!cmd.optsWithGlobals().json) printError('Command failed', err);
200
+ process.exit(1);
201
+ }
202
+ });
203
+
204
+ peerCmd
205
+ .command('list')
206
+ .description('List all active peer configurations')
207
+ .action(async (options, cmd) => {
208
+ if (!cmd.optsWithGlobals().json) printBanner();
209
+ try {
210
+ const { peerList } = await import('./commands/peer.js');
211
+ await peerList(cmd.optsWithGlobals());
212
+ } catch (err) {
213
+ if (!cmd.optsWithGlobals().json) printError('Command failed', err);
214
+ process.exit(1);
215
+ }
216
+ });
217
+
218
+ peerCmd
219
+ .command('remove <name>')
220
+ .description('Revoke a peer from the server')
221
+ .action(async (name, options, cmd) => {
222
+ if (!cmd.optsWithGlobals().json) printBanner();
223
+ try {
224
+ const { peerRemove } = await import('./commands/peer.js');
225
+ await peerRemove(name, cmd.optsWithGlobals());
226
+ } catch (err) {
227
+ if (!cmd.optsWithGlobals().json) printError('Command failed', err);
228
+ process.exit(1);
229
+ }
230
+ });
231
+
232
+ peerCmd
233
+ .command('qr <name>')
234
+ .description('Show peer configuration QR code')
235
+ .action(async (name, options, cmd) => {
236
+ try {
237
+ const { peerQr } = await import('./commands/peer.js');
238
+ await peerQr(name, cmd.optsWithGlobals());
239
+ } catch (err) {
240
+ if (!cmd.optsWithGlobals().json) printError('Command failed', err);
241
+ process.exit(1);
242
+ }
243
+ });
244
+
245
+ const configCmd = program.command('config').description('Configure polaris settings');
246
+ configCmd
247
+ .command('set <key> <value>')
248
+ .description('Set a configuration setting (e.g. kill-switch true)')
249
+ .action(async (key, value, options, cmd) => {
250
+ const isJson = cmd.optsWithGlobals().json;
251
+ if (!isJson) printBanner();
252
+ try {
253
+ if (key === 'kill-switch') {
254
+ const val = value === 'true';
255
+ const { setKillSwitchConfig } = await import('./utils/kill-switch.js');
256
+ setKillSwitchConfig(val);
257
+ const { printSuccess } = await import('./utils/display.js');
258
+ if (!isJson) {
259
+ printSuccess(`Set config 'kill-switch' to ${val}`);
260
+ } else {
261
+ console.log(JSON.stringify({ success: true, key, value: val }));
262
+ }
263
+ } else {
264
+ throw new Error(`Unknown config key '${key}'`);
265
+ }
266
+ } catch (err) {
267
+ if (!isJson) printError('Command failed', err);
268
+ process.exit(1);
269
+ }
270
+ });
271
+
124
272
  program.parseAsync(process.argv).catch(err => {
125
273
  if (!willPrintJson()) printError('Fatal error', err);
126
274
  process.exit(1);
@@ -1,11 +1,13 @@
1
1
  import chalk from 'chalk';
2
- import { getTunnelInfo } from '../tunnel/ssh.js';
3
- import { getPublicIp, getProxiedIp, checkDns, checkIpv6Leak } from '../net/ip.js';
4
- import { createTable, createSpinner, printError } from '../ui/display.js';
2
+ import { getActiveTunnel } from '../core/tunnel-service.js';
3
+ import { getDnsStatus } from '../core/dns-service.js';
4
+ import { getPublicIp, getProxiedIp, checkDns, checkIpv6Leak } from '../net/ip-check.js';
5
+ import { createTable, createSpinner, printError } from '../utils/display.js';
5
6
 
6
7
  export default async (options) => {
7
8
  const isJson = options.json;
8
- const info = getTunnelInfo();
9
+ const info = getActiveTunnel();
10
+ const dnsStatus = getDnsStatus();
9
11
 
10
12
  if (!isJson) {
11
13
  console.log(chalk.cyan.bold('\nRunning Privacy Check...\n'));
@@ -22,7 +24,7 @@ export default async (options) => {
22
24
  try {
23
25
  const tunnelIp = await getProxiedIp(info.port);
24
26
  results.ip = true;
25
- details.ip = `Tunnel active: ${tunnelIp}`;
27
+ details.ip = `Tunnel active (${(info.mode || 'ssh').toUpperCase()}): ${tunnelIp}`;
26
28
  } catch (e) {
27
29
  results.ip = false;
28
30
  details.ip = `Tunnel process running but proxy failed: ${e.message}`;
@@ -40,15 +42,14 @@ export default async (options) => {
40
42
  results.dns = false;
41
43
  details.dns = 'DNS resolution failed';
42
44
  } else {
43
- // In a real full VPN this would check if it's our VPN's DNS.
44
- // Since this is just a SOCKS5 proxy, system DNS is usually used.
45
- // We flag it as warning/fail if it's likely an ISP DNS, but for MVP
46
- // we'll just check if we can resolve and list the servers used.
47
- const servers = dnsResult.servers.join(', ');
48
- // SOCKS5 doesn't automatically tunnel DNS for all OS commands,
49
- // but browsers can be configured to use SOCKS5 for DNS.
50
- results.dns = true;
51
- details.dns = `Using system DNS: ${servers}`;
45
+ if (dnsStatus) {
46
+ results.dns = true;
47
+ details.dns = `DoH Resolver Active: 127.0.0.1:${dnsStatus.port} (Upstream: ${dnsStatus.upstream})`;
48
+ } else {
49
+ const servers = dnsResult.servers.join(', ');
50
+ results.dns = false;
51
+ details.dns = `Using system/ISP DNS: ${servers} (Potential DNS Leak)`;
52
+ }
52
53
  }
53
54
 
54
55
  // 3. IPv6 Leak Check
@@ -65,7 +66,12 @@ export default async (options) => {
65
66
  if (spinner) spinner.stop();
66
67
 
67
68
  if (isJson) {
68
- console.log(JSON.stringify({ results, details }));
69
+ console.log(JSON.stringify({
70
+ results,
71
+ details,
72
+ tunnelMode: info ? (info.mode || 'ssh') : 'none',
73
+ dnsResolver: dnsStatus ? 'doh' : 'system'
74
+ }));
69
75
  } else {
70
76
  const table = createTable(['Check', 'Status', 'Details']);
71
77
 
@@ -73,9 +79,9 @@ export default async (options) => {
73
79
 
74
80
  table.push(
75
81
  ['IP Address', formatResult(results.ip), details.ip],
76
- // If dns is "pass" but we know it's a SOCKS proxy, it's actually a warning that DNS isn't tunneled OS-wide
77
- ['DNS Leak', results.dns ? chalk.yellow('⚠ WARN') : formatResult(results.dns), details.dns],
78
- ['IPv6 Leak', formatResult(results.ipv6), details.ipv6]
82
+ ['DNS Leak', dnsStatus ? chalk.green('✓ PASS') : chalk.yellow(' WARN'), details.dns],
83
+ ['IPv6 Leak', formatResult(results.ipv6), details.ipv6],
84
+ ['WebRTC Leak', chalk.cyan('ⓘ INFO'), 'WebRTC can bypass proxies. Check browserleaks.com/webrtc']
79
85
  );
80
86
 
81
87
  console.log(table.toString());
@@ -85,7 +91,9 @@ export default async (options) => {
85
91
  console.log(chalk.yellow('Note: Your traffic is currently exposed. Run "polaris start" to connect.'));
86
92
  } else {
87
93
  console.log(chalk.cyan('Note: SOCKS5 proxies only tunnel apps configured to use them (like your browser).'));
88
- console.log(chalk.cyan(' System-wide DNS leaks are expected until v0.5 (WireGuard).'));
94
+ if (!dnsStatus) {
95
+ console.log(chalk.yellow(' Run "polaris dns start" to prevent DNS queries from leaking to your ISP.'));
96
+ }
89
97
  }
90
98
  console.log();
91
99
  }
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk';
2
+ import { printSuccess, printError, createSpinner, printInfo } from '../utils/display.js';
3
+ import { deployServer } from '../core/deploy-service.js';
4
+ import startCommand from './start.js';
5
+
6
+ export default async (options) => {
7
+ const isJson = options.json;
8
+ const server = options.server;
9
+
10
+ if (!server) {
11
+ if (isJson) {
12
+ console.log(JSON.stringify({ error: 'Missing --server argument' }));
13
+ } else {
14
+ printError('You must specify a server with --server <user@host>');
15
+ }
16
+ process.exitCode = 1;
17
+ return;
18
+ }
19
+
20
+ const spinner = isJson ? null : createSpinner('Initiating server deployment...').start();
21
+
22
+ try {
23
+ const res = await deployServer(server, {
24
+ privateKey: options.identity,
25
+ password: options.password,
26
+ onProgress: (msg) => {
27
+ if (spinner) spinner.text = msg;
28
+ }
29
+ });
30
+
31
+ if (spinner) {
32
+ spinner.succeed('Server provisioning completed successfully!');
33
+ }
34
+
35
+ if (isJson) {
36
+ console.log(JSON.stringify({ success: true, ...res }));
37
+ } else {
38
+ printSuccess(`WireGuard configured on remote VPS ${server}`);
39
+ printInfo(`Local Client Config: ${res.clientConfPath}`);
40
+ printInfo(`Client Public Key : ${res.clientPublicKey}`);
41
+ printInfo(`Server Public Key : ${res.serverPublicKey}`);
42
+ console.log(chalk.cyan('\nStarting WireGuard tunnel connection locally...\n'));
43
+ }
44
+
45
+ // Automatically trigger local connection
46
+ await startCommand({ mode: 'wireguard', json: isJson });
47
+
48
+ } catch (err) {
49
+ if (spinner) spinner.fail('Deployment failed');
50
+ if (isJson) {
51
+ console.log(JSON.stringify({ error: err.message }));
52
+ } else {
53
+ printError('Deployment failed', err);
54
+ }
55
+ process.exitCode = 1;
56
+ }
57
+ };
@@ -0,0 +1,85 @@
1
+ import chalk from 'chalk';
2
+ import { printError, printSuccess, printInfo, printWarning } from '../utils/display.js';
3
+ import { startDnsResolver, stopDnsResolver, getDnsStatus } from '../core/dns-service.js';
4
+
5
+ export const dnsStart = async (options) => {
6
+ const port = parseInt(options.port || '5354', 10);
7
+ const upstreamMap = {
8
+ 'cloudflare': 'https://cloudflare-dns.com/dns-query',
9
+ 'google': 'https://dns.google/resolve'
10
+ };
11
+
12
+ const provider = options.upstream || 'cloudflare';
13
+ const upstream = upstreamMap[provider.toLowerCase()] || provider;
14
+ const isJson = options.json;
15
+
16
+ try {
17
+ const res = startDnsResolver(port, upstream);
18
+ if (isJson) {
19
+ console.log(JSON.stringify({ success: true, ...res }));
20
+ } else {
21
+ printSuccess(`DNS-over-HTTPS resolver started on 127.0.0.1:${port}`);
22
+ printInfo(`Forwarding to: ${upstream}`);
23
+ }
24
+ } catch (err) {
25
+ if (isJson) {
26
+ console.log(JSON.stringify({ error: err.message }));
27
+ } else {
28
+ printError('Failed to start DNS resolver', err);
29
+ }
30
+ process.exitCode = 1;
31
+ }
32
+ };
33
+
34
+ export const dnsStop = async (options) => {
35
+ const isJson = options.json;
36
+
37
+ try {
38
+ const res = stopDnsResolver();
39
+ if (!res) {
40
+ if (isJson) {
41
+ console.log(JSON.stringify({ error: 'DNS resolver is not running' }));
42
+ } else {
43
+ printWarning('DNS resolver is not running.');
44
+ }
45
+ return;
46
+ }
47
+
48
+ if (isJson) {
49
+ console.log(JSON.stringify({ success: true }));
50
+ } else {
51
+ printSuccess('DNS-over-HTTPS resolver stopped.');
52
+ }
53
+ } catch (err) {
54
+ if (isJson) {
55
+ console.log(JSON.stringify({ error: err.message }));
56
+ } else {
57
+ printError('Failed to stop DNS resolver', err);
58
+ }
59
+ process.exitCode = 1;
60
+ }
61
+ };
62
+
63
+ export const dnsStatus = async (options) => {
64
+ const isJson = options.json;
65
+ const status = getDnsStatus();
66
+
67
+ if (!status) {
68
+ if (isJson) {
69
+ console.log(JSON.stringify({ running: false }));
70
+ } else {
71
+ console.log(`${chalk.red('✗')} DNS Resolver: ${chalk.red('Stopped')}`);
72
+ }
73
+ return;
74
+ }
75
+
76
+ if (isJson) {
77
+ console.log(JSON.stringify({ running: true, ...status }));
78
+ } else {
79
+ console.log(`${chalk.green('✓')} DNS Resolver: ${chalk.green('Running')}`);
80
+ console.log(` ${chalk.dim('PID :')} ${status.pid}`);
81
+ console.log(` ${chalk.dim('Port :')} ${status.port}`);
82
+ console.log(` ${chalk.dim('Upstream:')} ${status.upstream}`);
83
+ console.log(` ${chalk.dim('Uptime :')} ${Math.floor((Date.now() - new Date(status.startTime)) / 1000)}s`);
84
+ }
85
+ };
@@ -0,0 +1,121 @@
1
+ import fs from 'fs';
2
+ import chalk from 'chalk';
3
+ import qrcode from 'qrcode-terminal';
4
+ import { printSuccess, printError, createSpinner, printInfo, createTable } from '../utils/display.js';
5
+ import { addPeer, listPeers, removePeer, getLocalPeerConfPath } from '../core/peer-service.js';
6
+
7
+ export const peerAdd = async (name, options) => {
8
+ const isJson = options.json;
9
+ const spinner = isJson ? null : createSpinner(`Creating peer '${name}' on server...`).start();
10
+
11
+ try {
12
+ const res = await addPeer(name);
13
+ if (spinner) spinner.succeed(`Peer '${name}' created successfully!`);
14
+
15
+ if (isJson) {
16
+ console.log(JSON.stringify({ success: true, ...res }));
17
+ } else {
18
+ printSuccess(`Saved local peer profile: ${res.confPath}`);
19
+ printInfo(`IP Assigned: ${res.ip}`);
20
+ printInfo(`Public Key : ${res.publicKey}`);
21
+ console.log(chalk.cyan('\nScan this QR code with the WireGuard app on your mobile device:\n'));
22
+
23
+ const confString = fs.readFileSync(res.confPath, 'utf-8');
24
+ qrcode.generate(confString, { small: true });
25
+ }
26
+ } catch (err) {
27
+ if (spinner) spinner.fail('Failed to add peer');
28
+ if (isJson) {
29
+ console.log(JSON.stringify({ error: err.message }));
30
+ } else {
31
+ printError('Failed to add peer', err);
32
+ }
33
+ process.exitCode = 1;
34
+ }
35
+ };
36
+
37
+ export const peerList = async (options) => {
38
+ const isJson = options.json;
39
+ const spinner = isJson ? null : createSpinner('Fetching peer list...').start();
40
+
41
+ try {
42
+ const peers = await listPeers();
43
+ if (spinner) spinner.stop();
44
+
45
+ if (isJson) {
46
+ console.log(JSON.stringify({ success: true, peers }));
47
+ return;
48
+ }
49
+
50
+ if (peers.length === 0) {
51
+ printInfo('No peers configured on server.');
52
+ return;
53
+ }
54
+
55
+ const table = createTable(['Name', 'Assigned IP', 'Latest Handshake', 'Transfer Received/Sent']);
56
+
57
+ for (const p of peers) {
58
+ table.push([
59
+ chalk.green.bold(p.name),
60
+ p.ip,
61
+ p.handshake,
62
+ `${p.transferRx} / ${p.transferTx}`
63
+ ]);
64
+ }
65
+
66
+ console.log('\n' + table.toString() + '\n');
67
+ } catch (err) {
68
+ if (spinner) spinner.stop();
69
+ if (isJson) {
70
+ console.log(JSON.stringify({ error: err.message }));
71
+ } else {
72
+ printError('Failed to list peers', err);
73
+ }
74
+ process.exitCode = 1;
75
+ }
76
+ };
77
+
78
+ export const peerRemove = async (name, options) => {
79
+ const isJson = options.json;
80
+ const spinner = isJson ? null : createSpinner(`Revoking peer '${name}'...`).start();
81
+
82
+ try {
83
+ await removePeer(name);
84
+ if (spinner) spinner.succeed(`Peer '${name}' revoked from server.`);
85
+
86
+ if (isJson) {
87
+ console.log(JSON.stringify({ success: true }));
88
+ }
89
+ } catch (err) {
90
+ if (spinner) spinner.fail('Failed to remove peer');
91
+ if (isJson) {
92
+ console.log(JSON.stringify({ error: err.message }));
93
+ } else {
94
+ printError('Failed to remove peer', err);
95
+ }
96
+ process.exitCode = 1;
97
+ }
98
+ };
99
+
100
+ export const peerQr = async (name, options) => {
101
+ const isJson = options.json;
102
+
103
+ try {
104
+ const confPath = getLocalPeerConfPath(name);
105
+ const confString = fs.readFileSync(confPath, 'utf-8');
106
+
107
+ if (isJson) {
108
+ console.log(JSON.stringify({ success: true, config: confString }));
109
+ } else {
110
+ console.log(chalk.cyan(`\nQR code for peer configuration '${name}':\n`));
111
+ qrcode.generate(confString, { small: true });
112
+ }
113
+ } catch (err) {
114
+ if (isJson) {
115
+ console.log(JSON.stringify({ error: err.message }));
116
+ } else {
117
+ printError('Failed to retrieve QR code', err);
118
+ }
119
+ process.exitCode = 1;
120
+ }
121
+ };