wgm 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/.claude/settings.local.json +16 -0
- package/README.md +57 -0
- package/bin/wgm +400 -0
- package/package.json +31 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"WebSearch",
|
|
5
|
+
"Bash(npm search:*)",
|
|
6
|
+
"Bash(git init)",
|
|
7
|
+
"Bash(git add:*)",
|
|
8
|
+
"Bash(git commit:*)",
|
|
9
|
+
"Bash(git reset:*)",
|
|
10
|
+
"Bash(git rm:*)",
|
|
11
|
+
"Bash(git remote:*)",
|
|
12
|
+
"Bash(git branch:*)",
|
|
13
|
+
"Bash(git push:*)"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# wgm - Simplified WireGuard Peer Management
|
|
2
|
+
|
|
3
|
+
A lightweight CLI tool for managing WireGuard peers with interactive prompts.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Interactive peer addition without manual config editing
|
|
8
|
+
- Automatic IP address allocation
|
|
9
|
+
- Automatic config backup
|
|
10
|
+
- Client config file generation with QR code
|
|
11
|
+
- Support for manual or auto key generation
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g inquirer qrcode ini
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or use directly with npx:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
sudo npx wgm add
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
wgm add # Add a new peer interactively
|
|
29
|
+
wgm list # List all peers
|
|
30
|
+
wgm rm <name> # Remove a peer
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Example
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
$ sudo wgm add
|
|
37
|
+
? Peer name: macbook
|
|
38
|
+
? Key generation method:
|
|
39
|
+
❯ I provide the client public key (recommended)
|
|
40
|
+
Auto-generate key pair
|
|
41
|
+
? Paste client publicKey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
42
|
+
Assigned IP: 10.0.0.3/24
|
|
43
|
+
✅ Peer "macbook" added successfully
|
|
44
|
+
📄 Config saved: macbook.conf
|
|
45
|
+
📱 QR Code:
|
|
46
|
+
[QR code displayed]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
- WireGuard installed (`wg` command available)
|
|
52
|
+
- Node.js >= 16
|
|
53
|
+
- Root privileges
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
package/bin/wgm
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* wgm - Simplified WireGuard Peer Management Tool
|
|
5
|
+
*
|
|
6
|
+
* A lightweight CLI tool for managing WireGuard peers with interactive prompts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import inquirer from 'inquirer';
|
|
11
|
+
import QRCode from 'qrcode';
|
|
12
|
+
import { parse, stringify } from 'ini';
|
|
13
|
+
import { readFileSync, writeFileSync, copyFileSync, existsSync } from 'fs';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { dirname, join } from 'path';
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = dirname(__filename);
|
|
19
|
+
|
|
20
|
+
const CONFIG_DIR = '/etc/wireguard';
|
|
21
|
+
|
|
22
|
+
let INTERFACE = null;
|
|
23
|
+
let CONFIG_PATH = null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get WireGuard interface from args, env, or auto-detect
|
|
27
|
+
* @returns {Promise<string>} - Interface name
|
|
28
|
+
*/
|
|
29
|
+
async function getInterface() {
|
|
30
|
+
// Check CLI arg: -i <name>
|
|
31
|
+
const argIndex = process.argv.indexOf('-i');
|
|
32
|
+
if (argIndex !== -1 && process.argv[argIndex + 1]) {
|
|
33
|
+
return process.argv[argIndex + 1];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// List available configs
|
|
37
|
+
try {
|
|
38
|
+
const output = execSync(`ls ${CONFIG_DIR}/*.conf 2>/dev/null | xargs -n1 basename`, { encoding: 'utf-8' });
|
|
39
|
+
const configs = output.trim().split('\n').filter(c => c && c.endsWith('.conf'));
|
|
40
|
+
if (configs.length === 0) {
|
|
41
|
+
console.log('[ERROR] No WireGuard configs found in ' + CONFIG_DIR);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const names = configs.map(c => c.replace('.conf', ''));
|
|
45
|
+
if (names.length === 1) {
|
|
46
|
+
return names[0];
|
|
47
|
+
}
|
|
48
|
+
const { interface: selected } = await inquirer.prompt([{
|
|
49
|
+
type: 'list',
|
|
50
|
+
name: 'interface',
|
|
51
|
+
message: 'Select WireGuard interface:',
|
|
52
|
+
choices: names
|
|
53
|
+
}]);
|
|
54
|
+
return selected;
|
|
55
|
+
} catch {
|
|
56
|
+
console.log('[ERROR] Cannot read ' + CONFIG_DIR);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Initialize interface and config path
|
|
63
|
+
*/
|
|
64
|
+
async function init() {
|
|
65
|
+
if (!INTERFACE) {
|
|
66
|
+
INTERFACE = await getInterface();
|
|
67
|
+
CONFIG_PATH = join(CONFIG_DIR, `${INTERFACE}.conf`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate WireGuard public key format
|
|
73
|
+
* @param {string} key - Public key to validate
|
|
74
|
+
* @returns {boolean} - Whether the key is valid
|
|
75
|
+
*/
|
|
76
|
+
function validateKey(key) {
|
|
77
|
+
return /^[A-Za-z0-9+/]{43}=?$/.test(key.trim());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get server's public key from config
|
|
82
|
+
* @returns {string|null} - Server public key or null if not found
|
|
83
|
+
*/
|
|
84
|
+
function getServerPublicKey() {
|
|
85
|
+
try {
|
|
86
|
+
const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
87
|
+
const privKey = config.Interface?.PrivateKey;
|
|
88
|
+
if (privKey) {
|
|
89
|
+
return execSync(`echo "${privKey}" | wg pubkey`, { encoding: 'utf-8' }).trim();
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get server endpoint (IP:Port)
|
|
99
|
+
* @returns {string|null} - Endpoint string or null
|
|
100
|
+
*/
|
|
101
|
+
function getEndpoint() {
|
|
102
|
+
try {
|
|
103
|
+
const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
104
|
+
const port = config.Interface?.ListenPort || '51820';
|
|
105
|
+
try {
|
|
106
|
+
const ip = execSync('curl -s ifconfig.me', { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
107
|
+
return `${ip}:${port}`;
|
|
108
|
+
} catch {
|
|
109
|
+
return `YOUR_SERVER_IP:${port}`;
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get next available IP address from the configured network range
|
|
118
|
+
* @returns {string|null} - Available IP with CIDR or null
|
|
119
|
+
*/
|
|
120
|
+
function getAvailableIP() {
|
|
121
|
+
const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
122
|
+
const network = config.Interface?.Address;
|
|
123
|
+
|
|
124
|
+
if (!network) return null;
|
|
125
|
+
|
|
126
|
+
const [baseIp, cidr] = network.split('/');
|
|
127
|
+
const prefix = baseIp.split('.').slice(0, 3).join('.');
|
|
128
|
+
|
|
129
|
+
// Collect used IPs
|
|
130
|
+
const used = new Set();
|
|
131
|
+
for (const [key, val] of Object.entries(config)) {
|
|
132
|
+
if (key.startsWith('Peer') && val.AllowedIPs) {
|
|
133
|
+
const ip = val.AllowedIPs.split('/')[0];
|
|
134
|
+
const octets = ip.split('.');
|
|
135
|
+
if (octets.length === 4) {
|
|
136
|
+
const lastOctet = parseInt(octets[3]);
|
|
137
|
+
if (!isNaN(lastOctet)) used.add(lastOctet);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Find first available IP (starting from .2)
|
|
143
|
+
for (let i = 2; i < 255; i++) {
|
|
144
|
+
if (!used.has(i)) {
|
|
145
|
+
return `${prefix}.${i}/${cidr}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Save configuration and reload WireGuard
|
|
153
|
+
* @param {Object} config - Parsed INI config object
|
|
154
|
+
*/
|
|
155
|
+
function saveConfig(config) {
|
|
156
|
+
// Create backup
|
|
157
|
+
const backupPath = `${CONFIG_PATH}.backup.${Date.now()}`;
|
|
158
|
+
copyFileSync(CONFIG_PATH, backupPath);
|
|
159
|
+
|
|
160
|
+
// Save new config
|
|
161
|
+
writeFileSync(CONFIG_PATH, stringify(config, { section: '=' }));
|
|
162
|
+
|
|
163
|
+
// Reload WireGuard
|
|
164
|
+
try {
|
|
165
|
+
execSync(`wg syncconf ${INTERFACE} <(wg-quick strip ${INTERFACE})`, { stdio: 'pipe' });
|
|
166
|
+
} catch {
|
|
167
|
+
execSync(`wg-quick down ${INTERFACE} 2>/dev/null; wg-quick up ${INTERFACE}`, { stdio: 'inherit' });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Add a new peer interactively
|
|
173
|
+
*/
|
|
174
|
+
async function addPeer() {
|
|
175
|
+
await init();
|
|
176
|
+
const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
177
|
+
|
|
178
|
+
// Prompt for peer name
|
|
179
|
+
const { name } = await inquirer.prompt([{
|
|
180
|
+
type: 'input',
|
|
181
|
+
name: 'name',
|
|
182
|
+
message: 'Peer name:',
|
|
183
|
+
validate: input => input.length > 0 || 'Name cannot be empty'
|
|
184
|
+
}]);
|
|
185
|
+
|
|
186
|
+
// Check for duplicate
|
|
187
|
+
if (config[`Peer #${name}`]) {
|
|
188
|
+
console.log(`[ERROR] Peer "${name}" already exists`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Choose key method
|
|
193
|
+
const { method } = await inquirer.prompt([{
|
|
194
|
+
type: 'list',
|
|
195
|
+
name: 'method',
|
|
196
|
+
message: 'Key generation method:',
|
|
197
|
+
choices: [
|
|
198
|
+
{ name: 'I provide the client public key (recommended)', value: 'manual' },
|
|
199
|
+
{ name: 'Auto-generate key pair', value: 'auto' }
|
|
200
|
+
]
|
|
201
|
+
}]);
|
|
202
|
+
|
|
203
|
+
let publicKey, privateKey;
|
|
204
|
+
if (method === 'manual') {
|
|
205
|
+
console.log('\nOn the client device, run:');
|
|
206
|
+
console.log(' wg genkey | tee privatekey | wg pubkey');
|
|
207
|
+
console.log('Send the PUBLIC KEY to the server admin.\n');
|
|
208
|
+
|
|
209
|
+
const { key } = await inquirer.prompt([{
|
|
210
|
+
type: 'input',
|
|
211
|
+
name: 'key',
|
|
212
|
+
message: 'Paste client public key:',
|
|
213
|
+
validate: input => validateKey(input) || 'Invalid key format (44 character base64)'
|
|
214
|
+
}]);
|
|
215
|
+
publicKey = key.trim();
|
|
216
|
+
} else {
|
|
217
|
+
privateKey = execSync('wg genkey', { encoding: 'utf-8' }).trim();
|
|
218
|
+
publicKey = execSync(`echo "${privateKey}" | wg pubkey`, { encoding: 'utf-8' }).trim();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Get available IP
|
|
222
|
+
const ip = getAvailableIP();
|
|
223
|
+
if (!ip) {
|
|
224
|
+
console.log('[ERROR] Unable to get available IP');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
console.log(`Assigned IP: ${ip}`);
|
|
228
|
+
|
|
229
|
+
// Get server's VPN IP for AllowedIPs
|
|
230
|
+
let serverIp = 'SERVER_VPN_IP';
|
|
231
|
+
try {
|
|
232
|
+
const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
233
|
+
if (config.Interface?.Address) {
|
|
234
|
+
serverIp = config.Interface.Address.split('/')[0];
|
|
235
|
+
}
|
|
236
|
+
} catch {}
|
|
237
|
+
|
|
238
|
+
const { allowedIPs } = await inquirer.prompt([{
|
|
239
|
+
type: 'input',
|
|
240
|
+
name: 'allowedIPs',
|
|
241
|
+
message: 'AllowedIPs (traffic to route through VPN):',
|
|
242
|
+
default: `${serverIp}/32`,
|
|
243
|
+
validate: input => input.length > 0 || 'AllowedIPs cannot be empty'
|
|
244
|
+
}]);
|
|
245
|
+
|
|
246
|
+
// Save config
|
|
247
|
+
config[`Peer #${name}`] = {
|
|
248
|
+
PublicKey: publicKey,
|
|
249
|
+
AllowedIPs: clientIp
|
|
250
|
+
};
|
|
251
|
+
saveConfig(config);
|
|
252
|
+
console.log(`[OK] Peer "${name}" added successfully`);
|
|
253
|
+
|
|
254
|
+
// Generate client config
|
|
255
|
+
const serverPubKey = getServerPublicKey();
|
|
256
|
+
const endpoint = getEndpoint();
|
|
257
|
+
const dns = config.Interface?.DNS || '1.1.1.1, 8.8.8.8';
|
|
258
|
+
|
|
259
|
+
const clientConfig = `[Interface]
|
|
260
|
+
${privateKey ? `PrivateKey = ${privateKey}
|
|
261
|
+
` : ''}Address = ${ip}
|
|
262
|
+
DNS = ${dns}
|
|
263
|
+
|
|
264
|
+
[Peer]
|
|
265
|
+
PublicKey = ${serverPubKey}
|
|
266
|
+
AllowedIPs = ${allowedIPs}
|
|
267
|
+
Endpoint = ${endpoint}
|
|
268
|
+
PersistentKeepalive = 25`;
|
|
269
|
+
|
|
270
|
+
// Generate client config
|
|
271
|
+
const serverPubKey = getServerPublicKey();
|
|
272
|
+
const endpoint = getEndpoint();
|
|
273
|
+
const dns = config.Interface?.DNS || '1.1.1.1, 8.8.8.8';
|
|
274
|
+
|
|
275
|
+
const clientConfig = `[Interface]
|
|
276
|
+
${privateKey ? `PrivateKey = ${privateKey}
|
|
277
|
+
` : ''}Address = ${ip}
|
|
278
|
+
DNS = ${dns}
|
|
279
|
+
|
|
280
|
+
[Peer]
|
|
281
|
+
PublicKey = ${serverPubKey}
|
|
282
|
+
AllowedIPs = 0.0.0.0/0, ::/0
|
|
283
|
+
Endpoint = ${endpoint}
|
|
284
|
+
PersistentKeepalive = 25`;
|
|
285
|
+
|
|
286
|
+
// Save to file
|
|
287
|
+
const filename = `${name}.conf`;
|
|
288
|
+
writeFileSync(filename, clientConfig);
|
|
289
|
+
console.log(`[FILE] Config saved: ${filename}`);
|
|
290
|
+
|
|
291
|
+
// Generate QR code
|
|
292
|
+
try {
|
|
293
|
+
const qr = await QRCode.toString(clientConfig, { type: 'terminal', small: true });
|
|
294
|
+
console.log('\n[QR] QR Code:');
|
|
295
|
+
console.log(qr);
|
|
296
|
+
} catch {}
|
|
297
|
+
|
|
298
|
+
if (privateKey) {
|
|
299
|
+
console.log('\n[WARN] Private key generated. Keep it safe!');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* List all peers with their status
|
|
305
|
+
*/
|
|
306
|
+
async function listPeers() {
|
|
307
|
+
await init();
|
|
308
|
+
const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
309
|
+
|
|
310
|
+
console.log(`\n Peers on ${INTERFACE}:\n`);
|
|
311
|
+
|
|
312
|
+
// Get online status
|
|
313
|
+
let onlinePeers = new Set();
|
|
314
|
+
try {
|
|
315
|
+
const status = execSync(`wg show ${INTERFACE}`, { encoding: 'utf-8' });
|
|
316
|
+
onlinePeers = new Set(status.match(/peer: ([^\s]+)/g)?.map(p => p.replace('peer: ', '')) || []);
|
|
317
|
+
} catch {}
|
|
318
|
+
|
|
319
|
+
const peers = Object.entries(config).filter(([k]) => k.startsWith('Peer'));
|
|
320
|
+
|
|
321
|
+
if (peers.length === 0) {
|
|
322
|
+
console.log(' No peers configured');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (const [key, val] of peers) {
|
|
327
|
+
const name = key.replace('Peer #', '');
|
|
328
|
+
const isOnline = onlinePeers.has(val.PublicKey);
|
|
329
|
+
console.log(` ${isOnline ? '[ONLINE]' : '[OFFLINE]'} ${name} ${val.AllowedIPs}`);
|
|
330
|
+
}
|
|
331
|
+
console.log('');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Remove a peer by name
|
|
336
|
+
* @param {string} name - Peer name to remove
|
|
337
|
+
*/
|
|
338
|
+
async function removePeer(name) {
|
|
339
|
+
await init();
|
|
340
|
+
const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
341
|
+
const peerKey = Object.keys(config).find(k => k === `Peer #${name}` || config[k]?.PublicKey === name);
|
|
342
|
+
|
|
343
|
+
if (!peerKey) {
|
|
344
|
+
console.log(`[ERROR] Peer not found: ${name}`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
delete config[peerKey];
|
|
349
|
+
saveConfig(config);
|
|
350
|
+
console.log(`[OK] Removed: ${name}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Show help message
|
|
355
|
+
*/
|
|
356
|
+
function showHelp() {
|
|
357
|
+
console.log(`
|
|
358
|
+
wgm - Simplified WireGuard Peer Management
|
|
359
|
+
|
|
360
|
+
Usage:
|
|
361
|
+
wgm add Add a new peer interactively
|
|
362
|
+
wgm list List all peers
|
|
363
|
+
wgm rm <name> Remove a peer
|
|
364
|
+
wgm -i <name> Specify interface (default: auto-detect)
|
|
365
|
+
|
|
366
|
+
Examples:
|
|
367
|
+
wgm add
|
|
368
|
+
wgm -i wg1 add
|
|
369
|
+
wgm list
|
|
370
|
+
wgm rm laptop
|
|
371
|
+
|
|
372
|
+
Requirements:
|
|
373
|
+
- WireGuard installed
|
|
374
|
+
- Node.js >= 16
|
|
375
|
+
- Root privileges
|
|
376
|
+
`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Main entry point
|
|
381
|
+
*/
|
|
382
|
+
async function main() {
|
|
383
|
+
const cmd = process.argv[2];
|
|
384
|
+
|
|
385
|
+
if (cmd === 'add') {
|
|
386
|
+
await addPeer();
|
|
387
|
+
} else if (cmd === 'list' || cmd === 'ls') {
|
|
388
|
+
await listPeers();
|
|
389
|
+
} else if (cmd === 'rm' || cmd === 'remove') {
|
|
390
|
+
const name = process.argv[3];
|
|
391
|
+
if (name) await removePeer(name);
|
|
392
|
+
else console.log('Usage: wgm rm <name>');
|
|
393
|
+
} else if (cmd === '-h' || cmd === '--help' || cmd === '-v' || cmd === '--version') {
|
|
394
|
+
showHelp();
|
|
395
|
+
} else {
|
|
396
|
+
showHelp();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wgm",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Simplified WireGuard Peer Management Tool",
|
|
5
|
+
"main": "bin/wgm",
|
|
6
|
+
"bin": {
|
|
7
|
+
"wgm": "./bin/wgm"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/wgm"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["wireguard", "vpn", "peer", "cli", "wireguard-manager"],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"inquirer": "^9.2.0",
|
|
17
|
+
"qrcode": "^1.5.3",
|
|
18
|
+
"ini": "^4.1.0"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=16.0.0"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/wgm/wgm"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/wgm/wgm/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/wgm/wgm#readme"
|
|
31
|
+
}
|