apertodns 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.
Files changed (4) hide show
  1. package/README.md +75 -0
  2. package/index.js +1125 -0
  3. package/package.json +51 -0
  4. package/utils.js +35 -0
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # ApertoDNS CLI
2
+
3
+ Dynamic DNS management from your terminal. Manage domains, tokens, and DNS updates with style.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g apertodns
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # Setup (login or register)
15
+ apertodns --setup
16
+
17
+ # View dashboard
18
+ apertodns --dashboard
19
+
20
+ # List your domains
21
+ apertodns --domains
22
+
23
+ # Add a new domain
24
+ apertodns --add-domain myserver.apertodns.com
25
+
26
+ # Test DNS resolution
27
+ apertodns --test google.com
28
+ ```
29
+
30
+ ## Commands
31
+
32
+ ### Main Commands
33
+ | Command | Description |
34
+ |---------|-------------|
35
+ | `--dashboard` | Complete dashboard with all info |
36
+ | `--domains` | List all your domains |
37
+ | `--tokens` | List all your tokens |
38
+ | `--stats` | Statistics and metrics |
39
+ | `--logs` | Recent activity logs |
40
+
41
+ ### Domain Management
42
+ | Command | Description |
43
+ |---------|-------------|
44
+ | `--add-domain <name>` | Create a new domain |
45
+ | `--delete-domain` | Delete a domain (interactive) |
46
+ | `--test <domain>` | Test DNS resolution |
47
+
48
+ ### Token Management
49
+ | Command | Description |
50
+ |---------|-------------|
51
+ | `--enable <id>` | Enable a token |
52
+ | `--disable <id>` | Disable a token |
53
+ | `--toggle <id>` | Toggle token state |
54
+
55
+ ### Configuration
56
+ | Command | Description |
57
+ |---------|-------------|
58
+ | `--setup` | Guided setup (login/register) |
59
+ | `--status` | Show current status |
60
+ | `--force` | Force DNS update |
61
+
62
+ ## Interactive Mode
63
+
64
+ Run `apertodns` without arguments for interactive menu.
65
+
66
+ ## Cron Setup
67
+
68
+ ```bash
69
+ # Every 5 minutes
70
+ */5 * * * * apertodns --cron
71
+ ```
72
+
73
+ ## License
74
+
75
+ MIT - Aperto Network
package/index.js ADDED
@@ -0,0 +1,1125 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import fetch from "node-fetch";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ import readline from "readline";
8
+ import inquirer from "inquirer";
9
+ import { log, getCurrentIP, getCurrentIPv6, loadLastIP, saveCurrentIP } from "./utils.js";
10
+ import chalk from "chalk";
11
+ import figlet from "figlet";
12
+ import Table from "cli-table3";
13
+ import ora from "ora";
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const CONFIG_PATH = path.resolve(__dirname, "config.json");
17
+ const UPDATE_CACHE_PATH = path.resolve(__dirname, ".update-check");
18
+ const API_BASE = "https://api.apertodns.com/api";
19
+
20
+ // Leggi versione da package.json
21
+ const getPackageVersion = () => {
22
+ try {
23
+ const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, "package.json"), "utf-8"));
24
+ return pkg.version;
25
+ } catch {
26
+ return "0.0.0";
27
+ }
28
+ };
29
+
30
+ const CURRENT_VERSION = getPackageVersion();
31
+ const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 ore in ms
32
+
33
+ // Confronto semver robusto
34
+ const compareVersions = (v1, v2) => {
35
+ const parts1 = v1.split('.').map(Number);
36
+ const parts2 = v2.split('.').map(Number);
37
+
38
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
39
+ const p1 = parts1[i] || 0;
40
+ const p2 = parts2[i] || 0;
41
+ if (p1 > p2) return 1;
42
+ if (p1 < p2) return -1;
43
+ }
44
+ return 0;
45
+ };
46
+
47
+ // Check aggiornamenti con cache
48
+ const checkForUpdates = async () => {
49
+ try {
50
+ // Controlla cache
51
+ if (fs.existsSync(UPDATE_CACHE_PATH)) {
52
+ const cache = JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, "utf-8"));
53
+ const cacheAge = Date.now() - (cache.timestamp || 0);
54
+
55
+ // Se cache recente, usa il risultato cachato
56
+ if (cacheAge < UPDATE_CHECK_INTERVAL) {
57
+ if (cache.latestVersion && compareVersions(cache.latestVersion, CURRENT_VERSION) > 0) {
58
+ return cache.latestVersion;
59
+ }
60
+ return null;
61
+ }
62
+ }
63
+
64
+ // Fetch da npm registry
65
+ const controller = new AbortController();
66
+ const timeout = setTimeout(() => controller.abort(), 3000);
67
+
68
+ const res = await fetch("https://registry.npmjs.org/apertodns/latest", {
69
+ signal: controller.signal
70
+ });
71
+ clearTimeout(timeout);
72
+
73
+ if (!res.ok) return null;
74
+ const data = await res.json();
75
+ const latestVersion = data.version;
76
+
77
+ // Salva in cache
78
+ fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify({
79
+ timestamp: Date.now(),
80
+ latestVersion,
81
+ checkedVersion: CURRENT_VERSION
82
+ }));
83
+
84
+ // Confronta versioni
85
+ if (latestVersion && compareVersions(latestVersion, CURRENT_VERSION) > 0) {
86
+ return latestVersion;
87
+ }
88
+ return null;
89
+ } catch {
90
+ return null; // Silenzioso se offline o errore
91
+ }
92
+ };
93
+
94
+ // Colors
95
+ const orange = chalk.hex('#f97316');
96
+ const green = chalk.hex('#22c55e');
97
+ const red = chalk.hex('#ef4444');
98
+ const blue = chalk.hex('#3b82f6');
99
+ const purple = chalk.hex('#a855f7');
100
+ const yellow = chalk.hex('#eab308');
101
+ const gray = chalk.hex('#71717a');
102
+ const cyan = chalk.hex('#06b6d4');
103
+
104
+ const showBanner = async () => {
105
+ console.clear();
106
+ const banner = figlet.textSync("ApertoDNS", { font: "Standard" });
107
+ console.log(orange(banner));
108
+ console.log(gray(" ╔════════════════════════════════════════════════════════╗"));
109
+ console.log(gray(" ā•‘") + cyan(` ApertoDNS CLI v${CURRENT_VERSION}`) + gray(" - Dynamic DNS Reinvented ā•‘"));
110
+ console.log(gray(" ā•‘") + gray(" Gestisci domini, token e DNS dalla tua shell. ā•‘"));
111
+ console.log(gray(" ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n"));
112
+
113
+ // Check aggiornamenti in background
114
+ const newVersion = await checkForUpdates();
115
+ if (newVersion) {
116
+ console.log(yellow(" ╔════════════════════════════════════════════════════════╗"));
117
+ console.log(yellow(" ā•‘") + red(" āš ļø Nuova versione disponibile: ") + green.bold(`v${newVersion}`) + yellow(" ā•‘"));
118
+ console.log(yellow(" ā•‘") + gray(` Tu hai: v${CURRENT_VERSION}`) + yellow(" ā•‘"));
119
+ console.log(yellow(" ā•‘") + cyan(" Aggiorna: ") + chalk.white("npm update -g apertodns-cli") + yellow(" ā•‘"));
120
+ console.log(yellow(" ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n"));
121
+ }
122
+ };
123
+
124
+ const promptInput = (question) => {
125
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
126
+ return new Promise((resolve) => rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); }));
127
+ };
128
+
129
+ // Parse args
130
+ const args = process.argv.slice(2);
131
+ const isCron = args.includes("--cron");
132
+ if (isCron) {
133
+ process.argv.push("--quiet");
134
+ process.argv.push("--json");
135
+ }
136
+
137
+ const isQuiet = args.includes("--quiet");
138
+ const showHelp = args.includes("--help");
139
+ const showVersion = args.includes("--version");
140
+ const showJson = args.includes("--json");
141
+ const runVerify = args.includes("--verify");
142
+ const runSetup = args.includes("--setup");
143
+ const showStatus = args.includes("--status") || args.includes("--show");
144
+ const forceUpdate = args.includes("--force");
145
+ const enableTokenId = args.includes("--enable") ? args[args.indexOf("--enable") + 1] : null;
146
+ const disableTokenId = args.includes("--disable") ? args[args.indexOf("--disable") + 1] : null;
147
+ const toggleTokenId = args.includes("--toggle") ? args[args.indexOf("--toggle") + 1] : null;
148
+ const runConfigEdit = args.includes("--config");
149
+ const listDomains = args.includes("--domains");
150
+ const listTokens = args.includes("--tokens");
151
+ const addDomainArg = args.includes("--add-domain") ? args[args.indexOf("--add-domain") + 1] : null;
152
+ const deleteDomainArg = args.includes("--delete-domain") ? args[args.indexOf("--delete-domain") + 1] : null;
153
+ const showStats = args.includes("--stats");
154
+ const showLogs = args.includes("--logs");
155
+ const testDns = args.includes("--test") ? args[args.indexOf("--test") + 1] : null;
156
+ const showDashboard = args.includes("--dashboard");
157
+ const listWebhooks = args.includes("--webhooks");
158
+ const listApiKeys = args.includes("--api-keys");
159
+ const runInteractive = args.length === 0;
160
+
161
+ // Show help
162
+ if (showHelp) {
163
+ console.log(`
164
+ ${orange.bold("ApertoDNS CLI")} - Gestisci il tuo DNS dinamico
165
+
166
+ ${chalk.bold("USAGE:")}
167
+ apertodns [command] [options]
168
+
169
+ ${chalk.bold("COMANDI PRINCIPALI:")}
170
+ ${cyan("--dashboard")} Dashboard completa con tutte le info
171
+ ${cyan("--domains")} Lista tutti i tuoi domini (tabella)
172
+ ${cyan("--tokens")} Lista tutti i tuoi token (tabella)
173
+ ${cyan("--stats")} Statistiche e metriche
174
+ ${cyan("--logs")} Ultimi log di attivitĆ 
175
+
176
+ ${chalk.bold("GESTIONE DOMINI:")}
177
+ ${cyan("--add-domain")} <name> Crea un nuovo dominio
178
+ ${cyan("--delete-domain")} Elimina un dominio (interattivo)
179
+ ${cyan("--test")} <domain> Testa risoluzione DNS di un dominio
180
+
181
+ ${chalk.bold("GESTIONE TOKEN:")}
182
+ ${cyan("--enable")} <id> Attiva un token
183
+ ${cyan("--disable")} <id> Disattiva un token
184
+ ${cyan("--toggle")} <id> Inverte stato token (ON/OFF)
185
+ ${cyan("--verify")} Verifica validitĆ  token
186
+
187
+ ${chalk.bold("INTEGRAZIONI:")}
188
+ ${cyan("--webhooks")} Lista webhook configurati
189
+ ${cyan("--api-keys")} Lista API keys
190
+
191
+ ${chalk.bold("CONFIGURAZIONE:")}
192
+ ${cyan("--setup")} Configurazione guidata (login/registrazione)
193
+ ${cyan("--status")} Mostra stato attuale
194
+ ${cyan("--config")} Modifica configurazione
195
+ ${cyan("--force")} Forza aggiornamento DNS
196
+
197
+ ${chalk.bold("OPZIONI:")}
198
+ ${cyan("--cron")} ModalitĆ  silenziosa per cronjob
199
+ ${cyan("--quiet")} Nasconde banner
200
+ ${cyan("--json")} Output JSON
201
+ ${cyan("--version")} Mostra versione
202
+ ${cyan("--help")} Mostra questo help
203
+
204
+ ${chalk.bold("MODALITƀ INTERATTIVA:")}
205
+ Esegui ${cyan("apertodns")} senza argomenti per il menu interattivo.
206
+
207
+ ${gray("Esempi:")}
208
+ ${gray("$")} apertodns --dashboard
209
+ ${gray("$")} apertodns --domains
210
+ ${gray("$")} apertodns --add-domain mioserver.apertodns.com
211
+ ${gray("$")} apertodns --test mioserver.apertodns.com
212
+ ${gray("$")} apertodns --stats
213
+ `);
214
+ process.exit(0);
215
+ }
216
+
217
+ if (showVersion) {
218
+ const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, "package.json"), "utf-8"));
219
+ console.log(`ApertoDNS CLI v${pkg.version}`);
220
+ process.exit(0);
221
+ }
222
+
223
+ if (!isQuiet && !isCron) showBanner();
224
+
225
+ // Load config
226
+ let config = {};
227
+ if (fs.existsSync(CONFIG_PATH)) {
228
+ try {
229
+ config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
230
+ } catch (err) {
231
+ console.error(red("Errore lettura config.json:"), err.message);
232
+ }
233
+ }
234
+
235
+ // Helper: get JWT token (per API: domains, tokens, dashboard...)
236
+ const getAuthToken = async () => {
237
+ if (config.jwtToken) return config.jwtToken;
238
+ if (config.apiToken) return config.apiToken; // backward compatibility
239
+ return await promptInput(cyan("šŸ”‘ Token JWT: "));
240
+ };
241
+
242
+ // Helper: get CLI token (per DDNS: status, force, update...)
243
+ const getCliToken = async () => {
244
+ if (config.cliToken) return config.cliToken;
245
+ if (config.apiToken) return config.apiToken; // backward compatibility
246
+ return await promptInput(cyan("šŸ”‘ Token CLI: "));
247
+ };
248
+
249
+ // Helper: create spinner
250
+ const spinner = (text) => ora({ text, spinner: "dots", color: "yellow" });
251
+
252
+ // ==================== DOMAINS ====================
253
+
254
+ const fetchDomains = async () => {
255
+ const token = await getAuthToken();
256
+ const spin = spinner("Caricamento domini...").start();
257
+ try {
258
+ const res = await fetch(`${API_BASE}/domains`, {
259
+ headers: { Authorization: `Bearer ${token}` }
260
+ });
261
+ spin.stop();
262
+ if (!res.ok) throw new Error("Errore fetch domini");
263
+ return await res.json();
264
+ } catch (err) {
265
+ spin.fail("Errore caricamento domini");
266
+ return [];
267
+ }
268
+ };
269
+
270
+ const showDomainsList = async () => {
271
+ const domains = await fetchDomains();
272
+ if (domains.length === 0) {
273
+ console.log(yellow("\nāš ļø Nessun dominio trovato.\n"));
274
+ return;
275
+ }
276
+
277
+ const table = new Table({
278
+ head: [
279
+ gray('STATO'),
280
+ orange.bold('DOMINIO'),
281
+ cyan('IP ATTUALE'),
282
+ gray('TTL'),
283
+ gray('ULTIMO UPDATE')
284
+ ],
285
+ style: { head: [], border: ['gray'] },
286
+ chars: {
287
+ 'top': '─', 'top-mid': '┬', 'top-left': 'ā”Œ', 'top-right': '┐',
288
+ 'bottom': '─', 'bottom-mid': '┓', 'bottom-left': 'ā””', 'bottom-right': 'ā”˜',
289
+ 'left': '│', 'left-mid': 'ā”œ', 'mid': '─', 'mid-mid': '┼',
290
+ 'right': '│', 'right-mid': '┤', 'middle': '│'
291
+ }
292
+ });
293
+
294
+ domains.forEach(d => {
295
+ const status = d.currentIp ? green('ā— ONLINE') : red('ā— OFFLINE');
296
+ const lastUpdate = d.lastUpdated
297
+ ? new Date(d.lastUpdated).toLocaleString("it-IT", { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
298
+ : gray('Mai');
299
+
300
+ table.push([
301
+ status,
302
+ chalk.bold(d.name),
303
+ d.currentIp || gray('N/D'),
304
+ `${d.ttl}s`,
305
+ lastUpdate
306
+ ]);
307
+ });
308
+
309
+ console.log(`\nšŸ“‹ ${chalk.bold('I tuoi domini')} (${domains.length})\n`);
310
+ console.log(table.toString());
311
+ console.log();
312
+ };
313
+
314
+ const addDomain = async (name) => {
315
+ const token = await getAuthToken();
316
+ const domainName = name || await promptInput(cyan("šŸ“ Nome dominio (es. mioserver.apertodns.com): "));
317
+
318
+ if (!domainName) {
319
+ console.log(red("Nome dominio richiesto."));
320
+ return;
321
+ }
322
+
323
+ const spin = spinner(`Creazione dominio ${domainName}...`).start();
324
+ try {
325
+ const res = await fetch(`${API_BASE}/domains/standard`, {
326
+ method: "POST",
327
+ headers: {
328
+ "Content-Type": "application/json",
329
+ Authorization: `Bearer ${token}`
330
+ },
331
+ body: JSON.stringify({ name: domainName })
332
+ });
333
+ const data = await res.json();
334
+
335
+ if (res.ok) {
336
+ spin.succeed(`Dominio "${domainName}" creato!`);
337
+ if (data.token) {
338
+ console.log(yellow("\nšŸ” Token generato:"), chalk.bold.white(data.token));
339
+ console.log(gray(" (Salvalo subito, non sarà più visibile)\n"));
340
+ }
341
+ } else {
342
+ spin.fail(`Errore: ${data.error || data.message}`);
343
+ }
344
+ } catch (err) {
345
+ spin.fail(err.message);
346
+ }
347
+ };
348
+
349
+ const deleteDomain = async (name) => {
350
+ const token = await getAuthToken();
351
+ const domains = await fetchDomains();
352
+
353
+ let domainName = name;
354
+ if (!domainName) {
355
+ if (domains.length === 0) {
356
+ console.log(yellow("Nessun dominio da eliminare."));
357
+ return;
358
+ }
359
+ const { selected } = await inquirer.prompt([{
360
+ type: "list",
361
+ name: "selected",
362
+ message: "Quale dominio vuoi eliminare?",
363
+ choices: domains.map(d => ({
364
+ name: `${d.currentIp ? green('ā—') : red('ā—')} ${d.name}`,
365
+ value: d.name
366
+ }))
367
+ }]);
368
+ domainName = selected;
369
+ }
370
+
371
+ const domain = domains.find(d => d.name === domainName);
372
+ if (!domain) {
373
+ console.log(red(`Dominio "${domainName}" non trovato.`));
374
+ return;
375
+ }
376
+
377
+ const { confirm } = await inquirer.prompt([{
378
+ type: "confirm",
379
+ name: "confirm",
380
+ message: red(`āš ļø Eliminare definitivamente "${domainName}"?`),
381
+ default: false
382
+ }]);
383
+
384
+ if (!confirm) {
385
+ console.log(gray("Operazione annullata."));
386
+ return;
387
+ }
388
+
389
+ const spin = spinner(`Eliminazione ${domainName}...`).start();
390
+ try {
391
+ const res = await fetch(`${API_BASE}/domains/${domain.id}`, {
392
+ method: "DELETE",
393
+ headers: { Authorization: `Bearer ${token}` }
394
+ });
395
+
396
+ if (res.ok) {
397
+ spin.succeed(`Dominio "${domainName}" eliminato.`);
398
+ } else {
399
+ const data = await res.json();
400
+ spin.fail(`Errore: ${data.error || data.message}`);
401
+ }
402
+ } catch (err) {
403
+ spin.fail(err.message);
404
+ }
405
+ };
406
+
407
+ // ==================== DNS TEST ====================
408
+
409
+ const testDnsResolution = async (domain) => {
410
+ const domainToTest = domain || await promptInput(cyan("🌐 Dominio da testare: "));
411
+ if (!domainToTest) return;
412
+
413
+ // Validazione dominio per prevenire command injection
414
+ const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-_.]+[a-zA-Z0-9]$/;
415
+ if (!domainRegex.test(domainToTest) || domainToTest.includes('..')) {
416
+ console.log(red("\nāŒ Nome dominio non valido.\n"));
417
+ return;
418
+ }
419
+
420
+ const spin = spinner(`Testing DNS per ${domainToTest}...`).start();
421
+
422
+ try {
423
+ const { execFileSync } = await import('child_process');
424
+ const result = execFileSync('dig', ['+short', domainToTest, 'A'], { encoding: 'utf-8' }).trim();
425
+ const result6 = execFileSync('dig', ['+short', domainToTest, 'AAAA'], { encoding: 'utf-8' }).trim();
426
+
427
+ spin.stop();
428
+ console.log(`\nšŸ” ${chalk.bold('Risultati DNS per')} ${cyan(domainToTest)}\n`);
429
+
430
+ const table = new Table({
431
+ style: { head: [], border: ['gray'] }
432
+ });
433
+
434
+ table.push(
435
+ [gray('Record A (IPv4)'), result || red('Non trovato')],
436
+ [gray('Record AAAA (IPv6)'), result6 || gray('Non configurato')]
437
+ );
438
+
439
+ console.log(table.toString());
440
+
441
+ // Propagation check
442
+ console.log(`\n${gray('Propagazione DNS:')}`);
443
+ const dnsServers = ['8.8.8.8', '1.1.1.1'];
444
+ for (const dns of dnsServers) {
445
+ try {
446
+ const check = execFileSync('dig', ['+short', `@${dns}`, domainToTest, 'A'], { encoding: 'utf-8' }).trim();
447
+ console.log(` ${dns}: ${check ? green('āœ“ ' + check) : red('āœ— Non trovato')}`);
448
+ } catch {
449
+ console.log(` ${dns}: ${red('āœ— Errore')}`);
450
+ }
451
+ }
452
+ console.log();
453
+ } catch (err) {
454
+ spin.fail("Errore nel test DNS");
455
+ console.log(gray(" (Assicurati che 'dig' sia installato)\n"));
456
+ }
457
+ };
458
+
459
+ // ==================== TOKENS ====================
460
+
461
+ const fetchTokens = async () => {
462
+ const token = await getAuthToken();
463
+ const spin = spinner("Caricamento token...").start();
464
+ try {
465
+ const res = await fetch(`${API_BASE}/tokens`, {
466
+ headers: { Authorization: `Bearer ${token}` }
467
+ });
468
+ spin.stop();
469
+ if (!res.ok) throw new Error("Errore fetch token");
470
+ return await res.json();
471
+ } catch (err) {
472
+ spin.fail("Errore caricamento token");
473
+ return [];
474
+ }
475
+ };
476
+
477
+ const showTokensList = async () => {
478
+ const tokens = await fetchTokens();
479
+ if (tokens.length === 0) {
480
+ console.log(yellow("\nāš ļø Nessun token trovato.\n"));
481
+ return;
482
+ }
483
+
484
+ const table = new Table({
485
+ head: [
486
+ gray('STATO'),
487
+ orange.bold('ETICHETTA'),
488
+ cyan('DOMINIO'),
489
+ gray('ID'),
490
+ gray('ULTIMO USO')
491
+ ],
492
+ style: { head: [], border: ['gray'] }
493
+ });
494
+
495
+ tokens.forEach(t => {
496
+ const status = t.active ? green('ā— ATTIVO') : red('ā— OFF');
497
+ const lastUsed = t.lastUsed
498
+ ? new Date(t.lastUsed).toLocaleString("it-IT", { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
499
+ : gray('Mai');
500
+
501
+ table.push([
502
+ status,
503
+ chalk.bold(t.label || 'N/D'),
504
+ t.domain?.name || gray('N/D'),
505
+ gray(t.id),
506
+ lastUsed
507
+ ]);
508
+ });
509
+
510
+ console.log(`\nšŸ”‘ ${chalk.bold('I tuoi token')} (${tokens.length})\n`);
511
+ console.log(table.toString());
512
+ console.log();
513
+ };
514
+
515
+ const updateTokenState = async (tokenId, desiredState = null) => {
516
+ const apiToken = await getAuthToken();
517
+ if (!tokenId) {
518
+ console.error(red("Devi specificare un tokenId"));
519
+ return;
520
+ }
521
+
522
+ let finalState = desiredState;
523
+ if (desiredState === null) {
524
+ const spin = spinner("Caricamento...").start();
525
+ const res = await fetch(`${API_BASE}/tokens`, {
526
+ headers: { Authorization: `Bearer ${apiToken}` }
527
+ });
528
+ const all = await res.json();
529
+ spin.stop();
530
+
531
+ const token = all.find(t => t.id === parseInt(tokenId));
532
+ if (!token) {
533
+ console.error(red(`Token ID ${tokenId} non trovato.`));
534
+ return;
535
+ }
536
+ finalState = !token.active;
537
+ }
538
+
539
+ const spin = spinner(`${finalState ? 'Attivazione' : 'Disattivazione'} token...`).start();
540
+ const res = await fetch(`${API_BASE}/tokens/${tokenId}`, {
541
+ method: "PATCH",
542
+ headers: {
543
+ "Content-Type": "application/json",
544
+ Authorization: `Bearer ${apiToken}`
545
+ },
546
+ body: JSON.stringify({ active: finalState })
547
+ });
548
+
549
+ if (res.ok) {
550
+ spin.succeed(`Token ${tokenId} ${finalState ? green('attivato') : red('disattivato')}`);
551
+ } else {
552
+ const data = await res.json();
553
+ spin.fail(`Errore: ${data.error || data.message}`);
554
+ }
555
+ };
556
+
557
+ // ==================== STATS ====================
558
+
559
+ const showStatsCommand = async () => {
560
+ const token = await getAuthToken();
561
+ const spin = spinner("Caricamento statistiche...").start();
562
+
563
+ try {
564
+ const [domainsRes, tokensRes, statsRes] = await Promise.all([
565
+ fetch(`${API_BASE}/domains`, { headers: { Authorization: `Bearer ${token}` } }),
566
+ fetch(`${API_BASE}/tokens`, { headers: { Authorization: `Bearer ${token}` } }),
567
+ fetch(`${API_BASE}/stats/daily?days=7`, { headers: { Authorization: `Bearer ${token}` } }).catch(() => null)
568
+ ]);
569
+
570
+ const domains = await domainsRes.json();
571
+ const tokens = await tokensRes.json();
572
+ const stats = statsRes?.ok ? await statsRes.json() : [];
573
+
574
+ spin.stop();
575
+
576
+ console.log(`\nšŸ“Š ${chalk.bold('Statistiche ApertoDNS')}\n`);
577
+
578
+ // Summary boxes
579
+ const box1 = `ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
580
+ │ ${orange.bold(String(domains.length).padStart(2))} Domini │
581
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜`;
582
+
583
+ const box2 = `ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
584
+ │ ${green.bold(String(tokens.filter(t => t.active).length).padStart(2))} Token attivi│
585
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜`;
586
+
587
+ const box3 = `ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
588
+ │ ${cyan.bold(String(domains.filter(d => d.currentIp).length).padStart(2))} Online │
589
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜`;
590
+
591
+ console.log(gray(box1.split('\n')[0] + ' ' + box2.split('\n')[0] + ' ' + box3.split('\n')[0]));
592
+ console.log(gray(box1.split('\n')[1] + ' ' + box2.split('\n')[1] + ' ' + box3.split('\n')[1]));
593
+ console.log(gray(box1.split('\n')[2] + ' ' + box2.split('\n')[2] + ' ' + box3.split('\n')[2]));
594
+
595
+ // Weekly chart
596
+ if (stats.length > 0) {
597
+ console.log(`\n${gray('Aggiornamenti ultimi 7 giorni:')}`);
598
+ const maxUpdates = Math.max(...stats.map(s => s.updates || 0), 1);
599
+
600
+ stats.forEach(day => {
601
+ const barLength = Math.round((day.updates || 0) / maxUpdates * 20);
602
+ const bar = 'ā–ˆ'.repeat(barLength) + 'ā–‘'.repeat(20 - barLength);
603
+ const date = new Date(day.date).toLocaleDateString('it-IT', { weekday: 'short', day: '2-digit' });
604
+ console.log(` ${gray(date)} ${orange(bar)} ${day.updates || 0}`);
605
+ });
606
+ }
607
+
608
+ console.log();
609
+ } catch (err) {
610
+ spin.fail("Errore caricamento statistiche");
611
+ }
612
+ };
613
+
614
+ // ==================== LOGS ====================
615
+
616
+ const showLogsCommand = async () => {
617
+ const token = await getAuthToken();
618
+ const spin = spinner("Caricamento log...").start();
619
+
620
+ try {
621
+ const res = await fetch(`${API_BASE}/logs?limit=10`, {
622
+ headers: { Authorization: `Bearer ${token}` }
623
+ });
624
+
625
+ if (!res.ok) throw new Error("Errore fetch logs");
626
+ const data = await res.json();
627
+ const logs = data.logs || data;
628
+
629
+ spin.stop();
630
+
631
+ if (logs.length === 0) {
632
+ console.log(yellow("\nāš ļø Nessun log recente.\n"));
633
+ return;
634
+ }
635
+
636
+ console.log(`\nšŸ“œ ${chalk.bold('Ultimi log')}\n`);
637
+
638
+ logs.forEach(l => {
639
+ const time = new Date(l.createdAt).toLocaleString('it-IT', {
640
+ day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'
641
+ });
642
+ const actionColor = l.action === 'UPDATE' ? green :
643
+ l.action === 'CREATE' ? blue :
644
+ l.action === 'DELETE' ? red : gray;
645
+
646
+ console.log(` ${gray(time)} ${actionColor(l.action.padEnd(8))} ${l.token?.label || 'N/D'} ${gray('→')} ${l.token?.domain?.name || 'N/D'}`);
647
+ });
648
+
649
+ console.log();
650
+ } catch (err) {
651
+ spin.fail("Errore caricamento log");
652
+ }
653
+ };
654
+
655
+ // ==================== WEBHOOKS ====================
656
+
657
+ const showWebhooksList = async () => {
658
+ const token = await getAuthToken();
659
+ const spin = spinner("Caricamento webhook...").start();
660
+
661
+ try {
662
+ const res = await fetch(`${API_BASE}/webhooks`, {
663
+ headers: { Authorization: `Bearer ${token}` }
664
+ });
665
+
666
+ if (!res.ok) throw new Error("Errore fetch webhooks");
667
+ const webhooks = await res.json();
668
+
669
+ spin.stop();
670
+
671
+ if (webhooks.length === 0) {
672
+ console.log(yellow("\nāš ļø Nessun webhook configurato.\n"));
673
+ return;
674
+ }
675
+
676
+ const table = new Table({
677
+ head: [gray('STATO'), purple.bold('URL'), gray('EVENTI'), gray('DOMINIO')],
678
+ style: { head: [], border: ['gray'] }
679
+ });
680
+
681
+ webhooks.forEach(w => {
682
+ const status = w.active ? green('ā— ON') : red('ā— OFF');
683
+ const url = w.url.length > 40 ? w.url.substring(0, 37) + '...' : w.url;
684
+ table.push([status, url, w.events?.join(', ') || 'ALL', w.domain?.name || 'Tutti']);
685
+ });
686
+
687
+ console.log(`\nšŸ”— ${chalk.bold('Webhooks')} (${webhooks.length})\n`);
688
+ console.log(table.toString());
689
+ console.log();
690
+ } catch (err) {
691
+ spin.fail("Errore caricamento webhooks");
692
+ }
693
+ };
694
+
695
+ // ==================== API KEYS ====================
696
+
697
+ const showApiKeysList = async () => {
698
+ const token = await getAuthToken();
699
+ const spin = spinner("Caricamento API keys...").start();
700
+
701
+ try {
702
+ const res = await fetch(`${API_BASE}/api-keys`, {
703
+ headers: { Authorization: `Bearer ${token}` }
704
+ });
705
+
706
+ if (!res.ok) throw new Error("Errore fetch API keys");
707
+ const keys = await res.json();
708
+
709
+ spin.stop();
710
+
711
+ if (keys.length === 0) {
712
+ console.log(yellow("\nāš ļø Nessuna API key trovata.\n"));
713
+ return;
714
+ }
715
+
716
+ const table = new Table({
717
+ head: [gray('STATO'), blue.bold('NOME'), gray('PREFIX'), gray('SCOPES'), gray('RATE')],
718
+ style: { head: [], border: ['gray'] }
719
+ });
720
+
721
+ keys.forEach(k => {
722
+ const status = k.active ? green('ā— ON') : red('ā— OFF');
723
+ const scopes = k.scopes?.length > 3 ? `${k.scopes.length} scopes` : k.scopes?.join(', ') || 'N/D';
724
+ table.push([status, chalk.bold(k.name), gray(k.keyPrefix + '...'), scopes, `${k.rateLimit}/h`]);
725
+ });
726
+
727
+ console.log(`\nšŸ” ${chalk.bold('API Keys')} (${keys.length})\n`);
728
+ console.log(table.toString());
729
+ console.log();
730
+ } catch (err) {
731
+ spin.fail("Errore caricamento API keys");
732
+ }
733
+ };
734
+
735
+ // ==================== DASHBOARD ====================
736
+
737
+ const showDashboardCommand = async () => {
738
+ const token = await getAuthToken();
739
+ const spin = spinner("Caricamento dashboard...").start();
740
+
741
+ try {
742
+ const [domainsRes, tokensRes, ipRes] = await Promise.all([
743
+ fetch(`${API_BASE}/domains`, { headers: { Authorization: `Bearer ${token}` } }),
744
+ fetch(`${API_BASE}/tokens`, { headers: { Authorization: `Bearer ${token}` } }),
745
+ getCurrentIP('https://api.ipify.org').catch(() => 'N/D')
746
+ ]);
747
+
748
+ const domains = await domainsRes.json();
749
+ const tokens = await tokensRes.json();
750
+
751
+ spin.stop();
752
+
753
+ console.log(`\n${orange.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`);
754
+ console.log(`${orange.bold(' DASHBOARD ')}`);
755
+ console.log(`${orange.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}\n`);
756
+
757
+ // Current IP
758
+ console.log(` 🌐 ${gray('IP Attuale:')} ${green.bold(ipRes)}`);
759
+ console.log();
760
+
761
+ // Stats row
762
+ const onlineDomains = domains.filter(d => d.currentIp).length;
763
+ const activeTokens = tokens.filter(t => t.active).length;
764
+
765
+ console.log(` ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”`);
766
+ console.log(` │ ${orange.bold('DOMINI')} │ ${green.bold('ONLINE')} │ ${cyan.bold('TOKEN')} │ ${purple.bold('ATTIVI')} │`);
767
+ console.log(` │ ${chalk.bold(String(domains.length).padStart(3))} │ ${chalk.bold(String(onlineDomains).padStart(3))} │ ${chalk.bold(String(tokens.length).padStart(3))} │ ${chalk.bold(String(activeTokens).padStart(3))} │`);
768
+ console.log(` ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜`);
769
+ console.log();
770
+
771
+ // Domains preview
772
+ if (domains.length > 0) {
773
+ console.log(` ${gray('Ultimi domini:')}`);
774
+ domains.slice(0, 5).forEach(d => {
775
+ const status = d.currentIp ? green('ā—') : red('ā—');
776
+ console.log(` ${status} ${chalk.bold(d.name)} ${gray('→')} ${d.currentIp || gray('N/D')}`);
777
+ });
778
+ if (domains.length > 5) console.log(` ${gray(`... e altri ${domains.length - 5}`)}`);
779
+ }
780
+
781
+ console.log();
782
+ console.log(` ${gray('─'.repeat(60))}`);
783
+ console.log(` ${gray('Usa')} ${cyan('--help')} ${gray('per vedere tutti i comandi disponibili')}`);
784
+ console.log();
785
+ } catch (err) {
786
+ spin.fail("Errore caricamento dashboard");
787
+ }
788
+ };
789
+
790
+ // ==================== EXISTING FUNCTIONS ====================
791
+
792
+ const fetchRemoteConfig = async (token) => {
793
+ try {
794
+ const res = await fetch(`${API_BASE}/cli-config/from-token`, {
795
+ method: "POST",
796
+ headers: { "Content-Type": "application/json" },
797
+ body: JSON.stringify({ token })
798
+ });
799
+ const data = await res.json();
800
+ if (!res.ok || !data.config || !data.config.id) throw new Error(data.error || "Configurazione non valida");
801
+ return data.config;
802
+ } catch (err) {
803
+ return null;
804
+ }
805
+ };
806
+
807
+ const setup = async () => {
808
+ console.log(cyan("\nšŸ”§ Configurazione ApertoDNS CLI\n"));
809
+
810
+ const { hasAccount } = await inquirer.prompt([{
811
+ type: "list",
812
+ name: "hasAccount",
813
+ message: "Seleziona un'opzione:",
814
+ choices: [
815
+ { name: green('šŸ”“ Ho giĆ  un account - Login'), value: true },
816
+ { name: blue('šŸ“ Sono nuovo - Registrati sul sito'), value: false }
817
+ ]
818
+ }]);
819
+
820
+ let apiToken;
821
+
822
+ if (hasAccount) {
823
+ const { email, password } = await inquirer.prompt([
824
+ { type: "input", name: "email", message: "šŸ“§ Email:" },
825
+ { type: "password", name: "password", message: "šŸ”‘ Password:", mask: "ā—" }
826
+ ]);
827
+
828
+ const spin = spinner("Login in corso...").start();
829
+ const res = await fetch(`${API_BASE}/auth/login`, {
830
+ method: "POST",
831
+ headers: { "Content-Type": "application/json" },
832
+ body: JSON.stringify({ email, password })
833
+ });
834
+ const data = await res.json();
835
+
836
+ if (!res.ok || !data.cliToken) {
837
+ spin.fail("Login fallito: " + (data.message || "Errore"));
838
+ return;
839
+ }
840
+ apiToken = data.cliToken;
841
+ spin.succeed("Login effettuato!");
842
+ } else {
843
+ // Registrazione solo via web per sicurezza (captcha)
844
+ console.log(cyan("\nšŸ“ Per registrarti, visita: ") + orange.bold("https://apertodns.com/register"));
845
+ console.log(gray(" Dopo la registrazione, torna qui per fare il login.\n"));
846
+ return;
847
+ }
848
+
849
+ const remoteConfig = await fetchRemoteConfig(apiToken);
850
+ config = remoteConfig ? { ...remoteConfig, apiToken } : { apiToken };
851
+
852
+ const { save } = await inquirer.prompt([{
853
+ type: "confirm",
854
+ name: "save",
855
+ message: "šŸ’¾ Salvare la configurazione su questo computer?",
856
+ default: true
857
+ }]);
858
+
859
+ if (save) {
860
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
861
+ console.log(green(`\nāœ… Configurazione salvata!`));
862
+ console.log(yellow("āš ļø Non condividere config.json - contiene il tuo token.\n"));
863
+ }
864
+ };
865
+
866
+ const verifyToken = async () => {
867
+ const apiToken = await promptInput(cyan("šŸ” Token da verificare: "));
868
+ const spin = spinner("Verifica in corso...").start();
869
+
870
+ try {
871
+ const res = await fetch(`${API_BASE}/tokens/verify`, {
872
+ method: "POST",
873
+ headers: { "Content-Type": "application/json" },
874
+ body: JSON.stringify({ token: apiToken })
875
+ });
876
+ const data = await res.json();
877
+
878
+ if (data.valid) {
879
+ spin.succeed("Token valido!");
880
+ console.log(` ${gray('Etichetta:')} ${data.label}`);
881
+ console.log(` ${gray('Creato:')} ${new Date(data.createdAt).toLocaleString("it-IT")}\n`);
882
+ } else {
883
+ spin.fail("Token non valido");
884
+ }
885
+ } catch (err) {
886
+ spin.fail("Errore nella verifica");
887
+ }
888
+ };
889
+
890
+ const showCurrentStatus = async () => {
891
+ const cliToken = await getCliToken();
892
+ const spin = spinner("Caricamento stato...").start();
893
+
894
+ const remote = await fetchRemoteConfig(cliToken);
895
+ if (!remote) {
896
+ spin.fail("Impossibile caricare la configurazione");
897
+ return;
898
+ }
899
+
900
+ const currentIP = await getCurrentIP(remote.ipService).catch(() => null);
901
+ const lastIP = loadLastIP();
902
+
903
+ spin.stop();
904
+
905
+ console.log(`\nšŸ“Š ${chalk.bold('Stato Attuale')}\n`);
906
+
907
+ const table = new Table({ style: { border: ['gray'] } });
908
+ table.push(
909
+ [gray('Dominio'), cyan.bold(remote.domain)],
910
+ [gray('TTL'), `${remote.ttl}s`],
911
+ [gray('IP Attuale'), green.bold(currentIP || 'N/D')],
912
+ [gray('Ultimo IP'), lastIP || gray('N/D')],
913
+ [gray('IPv6'), remote.useIPv6 ? green('Attivo') : gray('Disattivo')]
914
+ );
915
+
916
+ console.log(table.toString());
917
+ console.log();
918
+ };
919
+
920
+ const editConfig = async () => {
921
+ const apiToken = await getAuthToken();
922
+ const remote = await fetchRemoteConfig(apiToken);
923
+ if (!remote) {
924
+ console.log(red("Impossibile caricare la configurazione."));
925
+ return;
926
+ }
927
+
928
+ const answers = await inquirer.prompt([
929
+ { type: "input", name: "ttl", message: "ā±ļø TTL (secondi):", default: String(remote.ttl) },
930
+ { type: "input", name: "ipService", message: "🌐 Servizio IP:", default: remote.ipService },
931
+ { type: "confirm", name: "useIPv6", message: "6ļøāƒ£ Usare IPv6?", default: remote.useIPv6 }
932
+ ]);
933
+
934
+ const spin = spinner("Salvataggio...").start();
935
+ const res = await fetch(`${API_BASE}/cli-config/${remote.id}`, {
936
+ method: "PATCH",
937
+ headers: {
938
+ "Content-Type": "application/json",
939
+ Authorization: `Bearer ${apiToken}`
940
+ },
941
+ body: JSON.stringify({
942
+ ttl: parseInt(answers.ttl),
943
+ ipService: answers.ipService,
944
+ useIPv6: answers.useIPv6
945
+ })
946
+ });
947
+
948
+ if (res.ok) {
949
+ spin.succeed("Configurazione aggiornata!");
950
+ config = { ...remote, ...answers, apiToken };
951
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
952
+ } else {
953
+ const data = await res.json();
954
+ spin.fail("Errore: " + (data.error || data.message));
955
+ }
956
+ };
957
+
958
+ const runUpdate = async () => {
959
+ let apiToken = config.apiToken;
960
+ if (!apiToken) {
961
+ apiToken = await promptInput(cyan("šŸ”‘ Token API: "));
962
+ const remoteConfig = await fetchRemoteConfig(apiToken);
963
+ if (!remoteConfig) {
964
+ console.log(red("Configurazione non trovata."));
965
+ return;
966
+ }
967
+ config = { ...remoteConfig, apiToken };
968
+ }
969
+
970
+ const spin = spinner("Rilevamento IP...").start();
971
+ const currentIP = await getCurrentIP(config.ipService).catch(() => null);
972
+ const currentIPv6 = config.useIPv6 ? await getCurrentIPv6(config.ipv6Service).catch(() => null) : null;
973
+
974
+ if (!currentIP && !currentIPv6) {
975
+ spin.fail("Nessun IP rilevato");
976
+ return;
977
+ }
978
+
979
+ spin.text = `IP rilevato: ${currentIP}`;
980
+
981
+ const lastIP = loadLastIP();
982
+ if (!forceUpdate && lastIP === currentIP) {
983
+ spin.succeed(`IP invariato (${currentIP})`);
984
+ return;
985
+ }
986
+
987
+ spin.text = `Aggiornamento DNS per ${config.domain}...`;
988
+
989
+ const body = {
990
+ name: config.domain,
991
+ ip: currentIP,
992
+ ttl: config.ttl,
993
+ };
994
+ if (currentIPv6) body.ipv6 = currentIPv6;
995
+
996
+ const res = await fetch(`${API_BASE}/update-dns`, {
997
+ method: "POST",
998
+ headers: {
999
+ Authorization: `Bearer ${config.apiToken}`,
1000
+ "Content-Type": "application/json",
1001
+ },
1002
+ body: JSON.stringify(body),
1003
+ });
1004
+
1005
+ const data = await res.json();
1006
+ if (res.ok && data.results) {
1007
+ saveCurrentIP(currentIP);
1008
+ spin.succeed(`DNS aggiornato! ${config.domain} → ${currentIP}`);
1009
+ if (showJson) console.log(JSON.stringify(data.results[0], null, 2));
1010
+ } else {
1011
+ spin.fail(`Errore: ${data.error || data.details}`);
1012
+ }
1013
+ };
1014
+
1015
+ // ==================== INTERACTIVE MODE ====================
1016
+
1017
+ const interactiveMode = async () => {
1018
+ console.log(gray(" Premi Ctrl+C per uscire\n"));
1019
+
1020
+ while (true) {
1021
+ const { action } = await inquirer.prompt([{
1022
+ type: "list",
1023
+ name: "action",
1024
+ message: orange("Cosa vuoi fare?"),
1025
+ pageSize: 15,
1026
+ choices: [
1027
+ { name: `${orange('šŸ“Š')} Dashboard`, value: "dashboard" },
1028
+ new inquirer.Separator(gray('─── Domini ───')),
1029
+ { name: `${cyan('šŸ“‹')} Lista domini`, value: "domains" },
1030
+ { name: `${green('āž•')} Aggiungi dominio`, value: "add-domain" },
1031
+ { name: `${red('šŸ—‘ļø ')} Elimina dominio`, value: "delete-domain" },
1032
+ { name: `${blue('šŸ”')} Test DNS`, value: "test-dns" },
1033
+ new inquirer.Separator(gray('─── Token ───')),
1034
+ { name: `${purple('šŸ”‘')} Lista token`, value: "tokens" },
1035
+ { name: `${yellow('šŸ”„')} Toggle token`, value: "toggle-token" },
1036
+ new inquirer.Separator(gray('─── Altro ───')),
1037
+ { name: `${cyan('šŸ“ˆ')} Statistiche`, value: "stats" },
1038
+ { name: `${gray('šŸ“œ')} Log attivitĆ `, value: "logs" },
1039
+ { name: `${purple('šŸ”—')} Webhooks`, value: "webhooks" },
1040
+ { name: `${blue('šŸ”')} API Keys`, value: "api-keys" },
1041
+ new inquirer.Separator(gray('─── Config ───')),
1042
+ { name: `${gray('šŸ“Š')} Stato attuale`, value: "status" },
1043
+ { name: `${green('šŸ”„')} Aggiorna DNS`, value: "update" },
1044
+ { name: `${yellow('āš™ļø ')} Configurazione`, value: "config" },
1045
+ { name: `${blue('šŸ”§')} Setup`, value: "setup" },
1046
+ new inquirer.Separator(),
1047
+ { name: red("āŒ Esci"), value: "exit" }
1048
+ ]
1049
+ }]);
1050
+
1051
+ console.log();
1052
+
1053
+ switch (action) {
1054
+ case "dashboard": await showDashboardCommand(); break;
1055
+ case "domains": await showDomainsList(); break;
1056
+ case "add-domain": await addDomain(); break;
1057
+ case "delete-domain": await deleteDomain(); break;
1058
+ case "test-dns": await testDnsResolution(); break;
1059
+ case "tokens": await showTokensList(); break;
1060
+ case "toggle-token":
1061
+ const tokens = await fetchTokens();
1062
+ if (tokens.length === 0) {
1063
+ console.log(yellow("Nessun token disponibile."));
1064
+ break;
1065
+ }
1066
+ const { tokenId } = await inquirer.prompt([{
1067
+ type: "list",
1068
+ name: "tokenId",
1069
+ message: "Seleziona token:",
1070
+ choices: tokens.map(t => ({
1071
+ name: `${t.active ? green('ā—') : red('ā—')} ${t.label} ${gray('→')} ${t.domain?.name || 'N/D'}`,
1072
+ value: t.id
1073
+ }))
1074
+ }]);
1075
+ await updateTokenState(tokenId, null);
1076
+ break;
1077
+ case "stats": await showStatsCommand(); break;
1078
+ case "logs": await showLogsCommand(); break;
1079
+ case "webhooks": await showWebhooksList(); break;
1080
+ case "api-keys": await showApiKeysList(); break;
1081
+ case "status": await showCurrentStatus(); break;
1082
+ case "update": await runUpdate(); break;
1083
+ case "config": await editConfig(); break;
1084
+ case "setup": await setup(); break;
1085
+ case "exit":
1086
+ console.log(gray("Arrivederci! šŸ‘‹\n"));
1087
+ process.exit(0);
1088
+ }
1089
+
1090
+ console.log();
1091
+ }
1092
+ };
1093
+
1094
+ // ==================== MAIN ====================
1095
+
1096
+ const main = async () => {
1097
+ try {
1098
+ if (enableTokenId) await updateTokenState(enableTokenId, true);
1099
+ else if (disableTokenId) await updateTokenState(disableTokenId, false);
1100
+ else if (toggleTokenId) await updateTokenState(toggleTokenId, null);
1101
+ else if (showDashboard) await showDashboardCommand();
1102
+ else if (listDomains) await showDomainsList();
1103
+ else if (addDomainArg) await addDomain(addDomainArg);
1104
+ else if (deleteDomainArg) await deleteDomain(deleteDomainArg);
1105
+ else if (testDns) await testDnsResolution(testDns);
1106
+ else if (listTokens) await showTokensList();
1107
+ else if (showStats) await showStatsCommand();
1108
+ else if (showLogs) await showLogsCommand();
1109
+ else if (listWebhooks) await showWebhooksList();
1110
+ else if (listApiKeys) await showApiKeysList();
1111
+ else if (runSetup) await setup();
1112
+ else if (runVerify) await verifyToken();
1113
+ else if (showStatus) await showCurrentStatus();
1114
+ else if (runConfigEdit) await editConfig();
1115
+ else if (runInteractive) await interactiveMode();
1116
+ else await runUpdate();
1117
+ } catch (err) {
1118
+ if (err.message !== 'User force closed the prompt') {
1119
+ console.error(red("\nāŒ Errore:"), err.message);
1120
+ }
1121
+ process.exit(1);
1122
+ }
1123
+ };
1124
+
1125
+ main();
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "apertodns",
3
+ "version": "1.0.0",
4
+ "description": "ApertoDNS CLI - Dynamic DNS management from your terminal. Manage domains, tokens, and DNS updates.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "apertodns": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js"
12
+ },
13
+ "keywords": [
14
+ "ddns",
15
+ "cli",
16
+ "dns",
17
+ "apertodns",
18
+ "dynamic-dns",
19
+ "ipv4",
20
+ "ipv6",
21
+ "domain",
22
+ "terminal"
23
+ ],
24
+ "author": "Aperto Network <info@apertodns.com>",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/apertonetwork/apertodns.git"
29
+ },
30
+ "homepage": "https://apertodns.com",
31
+ "bugs": {
32
+ "url": "https://github.com/apertonetwork/apertodns/issues"
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
37
+ "files": [
38
+ "index.js",
39
+ "utils.js",
40
+ "README.md"
41
+ ],
42
+ "dependencies": {
43
+ "chalk": "^5.3.0",
44
+ "cli-spinners": "^3.3.0",
45
+ "cli-table3": "^0.6.5",
46
+ "figlet": "^1.6.0",
47
+ "inquirer": "^9.3.8",
48
+ "node-fetch": "^3.3.2",
49
+ "ora": "^9.0.0"
50
+ }
51
+ }
package/utils.js ADDED
@@ -0,0 +1,35 @@
1
+ import fetch from "node-fetch";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const dataDir = path.resolve(__dirname, ".data");
8
+ const ipPath = path.join(dataDir, "last_ip_ironDNS.txt");
9
+
10
+ if (!fs.existsSync(dataDir)) {
11
+ fs.mkdirSync(dataDir, { recursive: true });
12
+ }
13
+
14
+ export const log = (msg) => {
15
+ const time = new Date().toISOString().replace("T", " ").substring(0, 19);
16
+ console.log(`[${time}] ${msg}`);
17
+ };
18
+
19
+ export const getCurrentIP = async (ipService) => {
20
+ const res = await fetch(ipService);
21
+ return res.text();
22
+ };
23
+
24
+ export const loadLastIP = () => {
25
+ return fs.existsSync(ipPath) ? fs.readFileSync(ipPath, "utf-8") : null;
26
+ };
27
+
28
+ export const saveCurrentIP = (ip) => {
29
+ fs.writeFileSync(ipPath, ip);
30
+ };
31
+
32
+ export const getCurrentIPv6 = async (ipService6) => {
33
+ const res = await fetch(ipService6);
34
+ return res.text();
35
+ };