hetzner-robot-cli 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/dist/cli.js ADDED
@@ -0,0 +1,934 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from 'commander';
3
+ import { select, confirm, input, checkbox } from '@inquirer/prompts';
4
+ import { config } from 'dotenv';
5
+ import { readFileSync } from 'fs';
6
+ import { HetznerRobotClient } from './client.js';
7
+ import { requireCredentials, promptLogin, clearConfig, getCredentials, } from './config.js';
8
+ import * as fmt from './formatter.js';
9
+ config();
10
+ let client = null;
11
+ /**
12
+ * Get or create API client with credentials.
13
+ * Credential sources (in order): CLI flags, env vars, config file, interactive prompt.
14
+ */
15
+ async function getClient(options) {
16
+ if (client)
17
+ return client;
18
+ const { user } = options;
19
+ let password = options.password;
20
+ if (password === '-') {
21
+ try {
22
+ password = readFileSync(0, 'utf-8').trim();
23
+ }
24
+ catch {
25
+ throw new Error('Failed to read password from stdin');
26
+ }
27
+ }
28
+ if (user && password) {
29
+ client = new HetznerRobotClient(user, password);
30
+ return client;
31
+ }
32
+ const creds = await requireCredentials();
33
+ client = new HetznerRobotClient(creds.user, creds.password);
34
+ return client;
35
+ }
36
+ function asyncAction(fn) {
37
+ return async (...args) => {
38
+ const options = args[args.length - 1];
39
+ try {
40
+ const apiClient = await getClient(options);
41
+ await fn(apiClient, ...args.slice(0, -1));
42
+ }
43
+ catch (error) {
44
+ if (error instanceof Error) {
45
+ if (error.message.includes('ExitPromptError') || error.name === 'ExitPromptError') {
46
+ process.exit(0);
47
+ }
48
+ console.error(fmt.error(error.message));
49
+ }
50
+ else {
51
+ console.error(fmt.error('An unknown error occurred'));
52
+ }
53
+ process.exit(1);
54
+ }
55
+ };
56
+ }
57
+ /**
58
+ * Output data as JSON or formatted table based on options.
59
+ */
60
+ function output(data, formatter, options) {
61
+ console.log(options.json ? fmt.formatJson(data) : formatter(data));
62
+ }
63
+ /**
64
+ * Confirm destructive action unless --yes flag is set.
65
+ * Returns true if confirmed, false if aborted.
66
+ */
67
+ async function confirmAction(message, options, defaultValue = false) {
68
+ if (options.yes)
69
+ return true;
70
+ const confirmed = await confirm({ message, default: defaultValue });
71
+ if (!confirmed) {
72
+ console.log('Aborted.');
73
+ return false;
74
+ }
75
+ return true;
76
+ }
77
+ const program = new Command();
78
+ program
79
+ .name('hetzner')
80
+ .description('Feature-complete CLI for Hetzner Robot API (dedicated servers)')
81
+ .version('1.0.0')
82
+ .option('-u, --user <username>', 'Hetzner Robot web service username')
83
+ .option('-p, --password <password>', 'Hetzner Robot web service password (use - to read from stdin)')
84
+ .option('--json', 'Output raw JSON instead of formatted tables');
85
+ const auth = program.command('auth').description('Authentication management');
86
+ auth
87
+ .command('login')
88
+ .description('Interactively configure credentials')
89
+ .action(async () => {
90
+ try {
91
+ await promptLogin();
92
+ console.log('');
93
+ console.log(fmt.success('Authentication configured successfully.'));
94
+ }
95
+ catch (error) {
96
+ if (error instanceof Error && error.name === 'ExitPromptError') {
97
+ process.exit(0);
98
+ }
99
+ throw error;
100
+ }
101
+ });
102
+ auth
103
+ .command('logout')
104
+ .description('Clear saved credentials')
105
+ .action(() => {
106
+ clearConfig();
107
+ console.log(fmt.success('Credentials cleared.'));
108
+ });
109
+ auth
110
+ .command('status')
111
+ .description('Check authentication status')
112
+ .action(() => {
113
+ const creds = getCredentials();
114
+ if (creds) {
115
+ console.log(fmt.success(`Authenticated as: ${creds.user}`));
116
+ console.log(fmt.info('Source: ' + (process.env.HETZNER_ROBOT_USER ? 'environment variables' : 'config file')));
117
+ }
118
+ else {
119
+ console.log(fmt.warning('Not authenticated. Run: hetzner auth login'));
120
+ }
121
+ });
122
+ auth
123
+ .command('test')
124
+ .description('Test API credentials')
125
+ .action(asyncAction(async (client) => {
126
+ const servers = await client.listServers();
127
+ console.log(fmt.success(`Authenticated successfully. Found ${servers.length} server(s).`));
128
+ }));
129
+ const server = program.command('server').alias('servers').description('Server management');
130
+ server
131
+ .command('list')
132
+ .alias('ls')
133
+ .description('List all servers')
134
+ .action(asyncAction(async (client, options) => {
135
+ const servers = await client.listServers();
136
+ output(servers, fmt.formatServerList, options);
137
+ }));
138
+ server
139
+ .command('get <server>')
140
+ .alias('show')
141
+ .description('Get server details')
142
+ .action(asyncAction(async (client, serverIdOrIp, options) => {
143
+ const { server: srv } = await client.getServer(serverIdOrIp);
144
+ output(srv, fmt.formatServerDetails, options);
145
+ }));
146
+ server
147
+ .command('rename <server> <name>')
148
+ .description('Rename a server')
149
+ .action(asyncAction(async (client, serverIdOrIp, name) => {
150
+ await client.updateServerName(serverIdOrIp, name);
151
+ console.log(fmt.success(`Server renamed to: ${name}`));
152
+ }));
153
+ const reset = program.command('reset').description('Server reset operations');
154
+ reset
155
+ .command('options [server]')
156
+ .description('Show reset options for server(s)')
157
+ .action(asyncAction(async (client, serverIdOrIp, options) => {
158
+ if (serverIdOrIp) {
159
+ const { reset: rst } = await client.getResetOptions(serverIdOrIp);
160
+ output(rst, fmt.formatResetOptions, options);
161
+ }
162
+ else {
163
+ const resets = await client.listResetOptions();
164
+ if (options.json) {
165
+ console.log(fmt.formatJson(resets));
166
+ }
167
+ else {
168
+ for (const { reset: rst } of resets) {
169
+ console.log(fmt.formatResetOptions(rst));
170
+ console.log('');
171
+ }
172
+ }
173
+ }
174
+ }));
175
+ reset
176
+ .command('execute <servers...>')
177
+ .alias('run')
178
+ .description('Reset one or more servers')
179
+ .addOption(new Option('-t, --type <type>', 'Reset type')
180
+ .choices(['sw', 'hw', 'man', 'power', 'power_long'])
181
+ .default('sw'))
182
+ .option('-i, --interactive', 'Interactively select reset type')
183
+ .option('-y, --yes', 'Skip confirmation')
184
+ .action(asyncAction(async (client, servers, options) => {
185
+ let resetType = options.type;
186
+ if (options.interactive) {
187
+ resetType = (await select({
188
+ message: 'Select reset type:',
189
+ choices: [
190
+ { value: 'sw', name: 'Software reset (ACPI) - Recommended' },
191
+ { value: 'hw', name: 'Hardware reset (forced)' },
192
+ { value: 'power', name: 'Power cycle' },
193
+ { value: 'power_long', name: 'Long power cycle (10+ seconds)' },
194
+ { value: 'man', name: 'Manual reset (technician)' },
195
+ ],
196
+ }));
197
+ }
198
+ if (!options.yes) {
199
+ console.log('');
200
+ console.log(fmt.warning(`About to reset ${servers.length} server(s) with type: ${resetType}`));
201
+ console.log(`Servers: ${servers.join(', ')}`);
202
+ console.log('');
203
+ const confirmed = await confirm({
204
+ message: 'Are you sure you want to proceed?',
205
+ default: false,
206
+ });
207
+ if (!confirmed) {
208
+ console.log('Aborted.');
209
+ return;
210
+ }
211
+ }
212
+ console.log('');
213
+ for (const srv of servers) {
214
+ try {
215
+ const { reset: rst } = await client.resetServer(srv, resetType);
216
+ console.log(fmt.formatResetResult(rst, resetType));
217
+ }
218
+ catch (error) {
219
+ console.log(fmt.error(`Failed to reset ${srv}: ${error instanceof Error ? error.message : 'Unknown error'}`));
220
+ }
221
+ }
222
+ }));
223
+ const boot = program.command('boot').description('Boot configuration (rescue, linux, vnc, windows)');
224
+ boot
225
+ .command('status <server>')
226
+ .description('Show boot configuration status')
227
+ .action(asyncAction(async (client, serverIdOrIp, options) => {
228
+ const { boot: bootConfig } = await client.getBootConfig(serverIdOrIp);
229
+ output(bootConfig, (b) => fmt.formatBootConfig(b, parseInt(serverIdOrIp) || 0), options);
230
+ }));
231
+ const rescue = boot.command('rescue').description('Rescue system management');
232
+ rescue
233
+ .command('activate <server>')
234
+ .description('Activate rescue system')
235
+ .option('-o, --os <os>', 'Operating system (linux, linuxold, vkvm)', 'linux')
236
+ .option('-a, --arch <arch>', 'Architecture (64 or 32)', '64')
237
+ .option('-k, --keys <fingerprints...>', 'SSH key fingerprints')
238
+ .action(asyncAction(async (client, serverIdOrIp, options) => {
239
+ const { rescue: rsc } = await client.activateRescue(serverIdOrIp, options.os, parseInt(options.arch), options.keys);
240
+ console.log(fmt.formatRescueActivation(rsc));
241
+ }));
242
+ rescue
243
+ .command('deactivate <server>')
244
+ .description('Deactivate rescue system')
245
+ .action(asyncAction(async (client, serverIdOrIp) => {
246
+ await client.deactivateRescue(serverIdOrIp);
247
+ console.log(fmt.success('Rescue system deactivated.'));
248
+ }));
249
+ rescue
250
+ .command('last <server>')
251
+ .description('Show last rescue activation details')
252
+ .action(asyncAction(async (client, serverIdOrIp, options) => {
253
+ const { rescue: rsc } = await client.getLastRescue(serverIdOrIp);
254
+ output(rsc, fmt.formatRescueActivation, options);
255
+ }));
256
+ const linux = boot.command('linux').description('Linux installation management');
257
+ linux
258
+ .command('activate <server>')
259
+ .description('Activate Linux installation')
260
+ .requiredOption('-d, --dist <dist>', 'Distribution (e.g., Debian-1210-bookworm-amd64-base)')
261
+ .option('-a, --arch <arch>', 'Architecture (64 or 32)', '64')
262
+ .option('-l, --lang <lang>', 'Language', 'en')
263
+ .option('-k, --keys <fingerprints...>', 'SSH key fingerprints')
264
+ .action(asyncAction(async (client, serverIdOrIp, options) => {
265
+ const { linux: lnx } = await client.activateLinux(serverIdOrIp, options.dist, parseInt(options.arch), options.lang, options.keys);
266
+ console.log(fmt.formatLinuxActivation(lnx));
267
+ }));
268
+ linux
269
+ .command('deactivate <server>')
270
+ .description('Deactivate Linux installation')
271
+ .action(asyncAction(async (client, serverIdOrIp) => {
272
+ await client.deactivateLinux(serverIdOrIp);
273
+ console.log(fmt.success('Linux installation deactivated.'));
274
+ }));
275
+ linux
276
+ .command('options <server>')
277
+ .description('Show available Linux distributions')
278
+ .action(asyncAction(async (client, serverIdOrIp, options) => {
279
+ const { linux: lnx } = await client.getLinux(serverIdOrIp);
280
+ output(lnx, (l) => {
281
+ const lines = [fmt.heading('Available Linux Distributions'), ''];
282
+ for (const dist of l.dist) {
283
+ lines.push(` ${dist}`);
284
+ }
285
+ lines.push('', fmt.info(`Languages: ${l.lang.join(', ')}`));
286
+ lines.push(fmt.info(`Architectures: ${l.arch.join(', ')}-bit`));
287
+ return lines.join('\n');
288
+ }, options);
289
+ }));
290
+ const ip = program.command('ip').alias('ips').description('IP address management');
291
+ ip.command('list')
292
+ .alias('ls')
293
+ .description('List all IPs')
294
+ .action(asyncAction(async (client, options) => {
295
+ const ips = await client.listIps();
296
+ output(ips, fmt.formatIpList, options);
297
+ }));
298
+ ip.command('get <ip>')
299
+ .alias('show')
300
+ .description('Get IP details')
301
+ .action(asyncAction(async (client, ipAddr, options) => {
302
+ const { ip: ipData } = await client.getIp(ipAddr);
303
+ output(ipData, fmt.formatIpDetails, options);
304
+ }));
305
+ ip.command('update <ip>')
306
+ .description('Update IP traffic warning settings')
307
+ .option('--warnings <enabled>', 'Enable/disable traffic warnings (true/false)')
308
+ .option('--hourly <mb>', 'Hourly traffic limit in MB')
309
+ .option('--daily <mb>', 'Daily traffic limit in MB')
310
+ .option('--monthly <gb>', 'Monthly traffic limit in GB')
311
+ .action(asyncAction(async (client, ipAddr, options) => {
312
+ await client.updateIp(ipAddr, options.warnings ? options.warnings === 'true' : undefined, options.hourly ? parseInt(options.hourly) : undefined, options.daily ? parseInt(options.daily) : undefined, options.monthly ? parseInt(options.monthly) : undefined);
313
+ console.log(fmt.success(`IP ${ipAddr} updated.`));
314
+ }));
315
+ const mac = ip.command('mac').description('MAC address management');
316
+ mac
317
+ .command('get <ip>')
318
+ .description('Get separate MAC address for IP')
319
+ .action(asyncAction(async (client, ipAddr, options) => {
320
+ const { mac: macData } = await client.getIpMac(ipAddr);
321
+ output(macData, (m) => `IP: ${m.ip}\nMAC: ${m.mac}`, options);
322
+ }));
323
+ mac
324
+ .command('generate <ip>')
325
+ .description('Generate separate MAC address for IP')
326
+ .action(asyncAction(async (client, ipAddr) => {
327
+ const { mac: macData } = await client.generateIpMac(ipAddr);
328
+ console.log(fmt.success(`Generated MAC: ${macData.mac}`));
329
+ }));
330
+ mac
331
+ .command('delete <ip>')
332
+ .description('Delete separate MAC address')
333
+ .action(asyncAction(async (client, ipAddr) => {
334
+ await client.deleteIpMac(ipAddr);
335
+ console.log(fmt.success('MAC address deleted.'));
336
+ }));
337
+ const subnet = program.command('subnet').alias('subnets').description('Subnet management');
338
+ subnet
339
+ .command('list')
340
+ .alias('ls')
341
+ .description('List all subnets')
342
+ .action(asyncAction(async (client, options) => {
343
+ const subnets = await client.listSubnets();
344
+ output(subnets, fmt.formatSubnetList, options);
345
+ }));
346
+ subnet
347
+ .command('get <subnet>')
348
+ .alias('show')
349
+ .description('Get subnet details')
350
+ .action(asyncAction(async (client, netIp, options) => {
351
+ const { subnet: subnetData } = await client.getSubnet(netIp);
352
+ output(subnetData, (s) => fmt.formatSubnetList([{ subnet: s }]), options);
353
+ }));
354
+ const failover = program.command('failover').description('Failover IP management');
355
+ failover
356
+ .command('list')
357
+ .alias('ls')
358
+ .description('List all failover IPs')
359
+ .action(asyncAction(async (client, options) => {
360
+ const failovers = await client.listFailovers();
361
+ output(failovers, fmt.formatFailoverList, options);
362
+ }));
363
+ failover
364
+ .command('get <ip>')
365
+ .alias('show')
366
+ .description('Get failover IP details')
367
+ .action(asyncAction(async (client, failoverIp, options) => {
368
+ const { failover: fo } = await client.getFailover(failoverIp);
369
+ output(fo, (f) => fmt.formatFailoverList([{ failover: f }]), options);
370
+ }));
371
+ failover
372
+ .command('switch <failover-ip> <target-server-ip>')
373
+ .description('Switch failover IP routing to another server')
374
+ .option('-y, --yes', 'Skip confirmation')
375
+ .action(asyncAction(async (client, failoverIp, targetServerIp, options) => {
376
+ if (!await confirmAction(`Route ${failoverIp} to ${targetServerIp}?`, options, true))
377
+ return;
378
+ const { failover: fo } = await client.switchFailover(failoverIp, targetServerIp);
379
+ console.log(fmt.formatFailoverSwitch(fo));
380
+ }));
381
+ failover
382
+ .command('delete <ip>')
383
+ .description('Delete failover IP routing')
384
+ .option('-y, --yes', 'Skip confirmation')
385
+ .action(asyncAction(async (client, failoverIp, options) => {
386
+ if (!await confirmAction(`Delete routing for ${failoverIp}?`, options))
387
+ return;
388
+ await client.deleteFailoverRouting(failoverIp);
389
+ console.log(fmt.success('Failover routing deleted.'));
390
+ }));
391
+ const rdns = program.command('rdns').description('Reverse DNS management');
392
+ rdns
393
+ .command('list')
394
+ .alias('ls')
395
+ .description('List all reverse DNS entries')
396
+ .action(asyncAction(async (client, options) => {
397
+ const entries = await client.listRdns();
398
+ output(entries, fmt.formatRdnsList, options);
399
+ }));
400
+ rdns
401
+ .command('get <ip>')
402
+ .alias('show')
403
+ .description('Get reverse DNS entry for IP')
404
+ .action(asyncAction(async (client, ipAddr, options) => {
405
+ const { rdns: entry } = await client.getRdns(ipAddr);
406
+ output(entry, (e) => `IP: ${e.ip}\nPTR: ${e.ptr}`, options);
407
+ }));
408
+ rdns
409
+ .command('set <ip> <ptr>')
410
+ .description('Create or update reverse DNS entry')
411
+ .action(asyncAction(async (client, ipAddr, ptr) => {
412
+ try {
413
+ await client.createRdns(ipAddr, ptr);
414
+ }
415
+ catch {
416
+ await client.updateRdns(ipAddr, ptr);
417
+ }
418
+ console.log(fmt.success(`rDNS set: ${ipAddr} -> ${ptr}`));
419
+ }));
420
+ rdns
421
+ .command('delete <ip>')
422
+ .description('Delete reverse DNS entry')
423
+ .action(asyncAction(async (client, ipAddr) => {
424
+ await client.deleteRdns(ipAddr);
425
+ console.log(fmt.success('rDNS entry deleted.'));
426
+ }));
427
+ const key = program.command('key').alias('keys').description('SSH key management');
428
+ key
429
+ .command('list')
430
+ .alias('ls')
431
+ .description('List all SSH keys')
432
+ .action(asyncAction(async (client, options) => {
433
+ const keys = await client.listSshKeys();
434
+ output(keys, fmt.formatSshKeyList, options);
435
+ }));
436
+ key
437
+ .command('get <fingerprint>')
438
+ .alias('show')
439
+ .description('Get SSH key details')
440
+ .action(asyncAction(async (client, fingerprint, options) => {
441
+ const { key: keyData } = await client.getSshKey(fingerprint);
442
+ output(keyData, fmt.formatSshKeyDetails, options);
443
+ }));
444
+ key
445
+ .command('add <name>')
446
+ .description('Add a new SSH key')
447
+ .option('-f, --file <path>', 'Path to public key file')
448
+ .option('-d, --data <key>', 'Public key data')
449
+ .action(asyncAction(async (client, name, options) => {
450
+ let keyData;
451
+ if (options.file) {
452
+ keyData = readFileSync(options.file, 'utf-8').trim();
453
+ }
454
+ else if (options.data) {
455
+ keyData = options.data;
456
+ }
457
+ else {
458
+ keyData = await input({
459
+ message: 'Paste public key:',
460
+ validate: (v) => v.length > 0 || 'Key data is required',
461
+ });
462
+ }
463
+ const { key: newKey } = await client.createSshKey(name, keyData);
464
+ console.log(fmt.success(`SSH key added: ${newKey.fingerprint}`));
465
+ }));
466
+ key
467
+ .command('rename <fingerprint> <name>')
468
+ .description('Rename an SSH key')
469
+ .action(asyncAction(async (client, fingerprint, name) => {
470
+ await client.updateSshKey(fingerprint, name);
471
+ console.log(fmt.success(`SSH key renamed to: ${name}`));
472
+ }));
473
+ key
474
+ .command('delete <fingerprint>')
475
+ .alias('rm')
476
+ .description('Delete an SSH key')
477
+ .option('-y, --yes', 'Skip confirmation')
478
+ .action(asyncAction(async (client, fingerprint, options) => {
479
+ if (!await confirmAction(`Delete SSH key ${fingerprint}?`, options))
480
+ return;
481
+ await client.deleteSshKey(fingerprint);
482
+ console.log(fmt.success('SSH key deleted.'));
483
+ }));
484
+ const firewall = program.command('firewall').description('Firewall management');
485
+ firewall
486
+ .command('get <server>')
487
+ .alias('show')
488
+ .description('Get firewall configuration')
489
+ .action(asyncAction(async (client, serverIdOrIp, options) => {
490
+ const { firewall: fw } = await client.getFirewall(serverIdOrIp);
491
+ output(fw, fmt.formatFirewall, options);
492
+ }));
493
+ firewall
494
+ .command('enable <server>')
495
+ .description('Enable firewall')
496
+ .action(asyncAction(async (client, serverIdOrIp) => {
497
+ await client.updateFirewall(serverIdOrIp, 'active');
498
+ console.log(fmt.success('Firewall enabled.'));
499
+ }));
500
+ firewall
501
+ .command('disable <server>')
502
+ .description('Disable firewall')
503
+ .action(asyncAction(async (client, serverIdOrIp) => {
504
+ await client.updateFirewall(serverIdOrIp, 'disabled');
505
+ console.log(fmt.success('Firewall disabled.'));
506
+ }));
507
+ firewall
508
+ .command('delete <server>')
509
+ .description('Delete all firewall rules')
510
+ .option('-y, --yes', 'Skip confirmation')
511
+ .action(asyncAction(async (client, serverIdOrIp, options) => {
512
+ if (!await confirmAction('Delete all firewall rules?', options))
513
+ return;
514
+ await client.deleteFirewall(serverIdOrIp);
515
+ console.log(fmt.success('Firewall rules deleted.'));
516
+ }));
517
+ const fwTemplate = firewall.command('template').description('Firewall template management');
518
+ fwTemplate
519
+ .command('list')
520
+ .alias('ls')
521
+ .description('List firewall templates')
522
+ .action(asyncAction(async (client, options) => {
523
+ const templates = await client.listFirewallTemplates();
524
+ output(templates, fmt.formatFirewallTemplateList, options);
525
+ }));
526
+ fwTemplate
527
+ .command('get <id>')
528
+ .alias('show')
529
+ .description('Get firewall template details')
530
+ .action(asyncAction(async (client, templateId, options) => {
531
+ const { firewall_template: tmpl } = await client.getFirewallTemplate(parseInt(templateId));
532
+ output(tmpl, (t) => fmt.formatFirewallTemplateList([{ firewall_template: t }]), options);
533
+ }));
534
+ fwTemplate
535
+ .command('delete <id>')
536
+ .alias('rm')
537
+ .description('Delete firewall template')
538
+ .option('-y, --yes', 'Skip confirmation')
539
+ .action(asyncAction(async (client, templateId, options) => {
540
+ if (!await confirmAction(`Delete firewall template ${templateId}?`, options))
541
+ return;
542
+ await client.deleteFirewallTemplate(parseInt(templateId));
543
+ console.log(fmt.success('Firewall template deleted.'));
544
+ }));
545
+ const vswitch = program.command('vswitch').description('vSwitch management');
546
+ vswitch
547
+ .command('list')
548
+ .alias('ls')
549
+ .description('List all vSwitches')
550
+ .action(asyncAction(async (client, options) => {
551
+ const vswitches = await client.listVSwitches();
552
+ output(vswitches, fmt.formatVSwitchList, options);
553
+ }));
554
+ vswitch
555
+ .command('get <id>')
556
+ .alias('show')
557
+ .description('Get vSwitch details')
558
+ .action(asyncAction(async (client, vswitchId, options) => {
559
+ const { vswitch: vs } = await client.getVSwitch(parseInt(vswitchId));
560
+ output(vs, fmt.formatVSwitchDetails, options);
561
+ }));
562
+ vswitch
563
+ .command('create <name> <vlan>')
564
+ .description('Create a new vSwitch')
565
+ .action(asyncAction(async (client, name, vlan) => {
566
+ const { vswitch: vs } = await client.createVSwitch(name, parseInt(vlan));
567
+ console.log(fmt.success(`vSwitch created: ID ${vs.id}`));
568
+ }));
569
+ vswitch
570
+ .command('update <id>')
571
+ .description('Update vSwitch')
572
+ .option('-n, --name <name>', 'New name')
573
+ .option('-v, --vlan <vlan>', 'New VLAN ID')
574
+ .action(asyncAction(async (client, vswitchId, options) => {
575
+ await client.updateVSwitch(parseInt(vswitchId), options.name, options.vlan ? parseInt(options.vlan) : undefined);
576
+ console.log(fmt.success('vSwitch updated.'));
577
+ }));
578
+ vswitch
579
+ .command('delete <id>')
580
+ .alias('rm')
581
+ .description('Delete vSwitch')
582
+ .option('-y, --yes', 'Skip confirmation')
583
+ .option('--date <date>', 'Cancellation date (YYYY-MM-DD)')
584
+ .action(asyncAction(async (client, vswitchId, options) => {
585
+ if (!await confirmAction(`Delete vSwitch ${vswitchId}?`, options))
586
+ return;
587
+ await client.deleteVSwitch(parseInt(vswitchId), options.date);
588
+ console.log(fmt.success('vSwitch deleted.'));
589
+ }));
590
+ vswitch
591
+ .command('add-server <vswitch-id> <server>')
592
+ .description('Add server to vSwitch')
593
+ .action(asyncAction(async (client, vswitchId, serverIdOrIp) => {
594
+ await client.addServerToVSwitch(parseInt(vswitchId), serverIdOrIp);
595
+ console.log(fmt.success('Server added to vSwitch.'));
596
+ }));
597
+ vswitch
598
+ .command('remove-server <vswitch-id> <server>')
599
+ .description('Remove server from vSwitch')
600
+ .action(asyncAction(async (client, vswitchId, serverIdOrIp) => {
601
+ await client.removeServerFromVSwitch(parseInt(vswitchId), serverIdOrIp);
602
+ console.log(fmt.success('Server removed from vSwitch.'));
603
+ }));
604
+ const storagebox = program.command('storagebox').alias('storage').description('Storage Box management');
605
+ storagebox
606
+ .command('list')
607
+ .alias('ls')
608
+ .description('List all storage boxes')
609
+ .action(asyncAction(async (client, options) => {
610
+ const boxes = await client.listStorageBoxes();
611
+ output(boxes, fmt.formatStorageBoxList, options);
612
+ }));
613
+ storagebox
614
+ .command('get <id>')
615
+ .alias('show')
616
+ .description('Get storage box details')
617
+ .action(asyncAction(async (client, boxId, options) => {
618
+ const { storagebox: box } = await client.getStorageBox(parseInt(boxId));
619
+ output(box, fmt.formatStorageBoxDetails, options);
620
+ }));
621
+ storagebox
622
+ .command('update <id>')
623
+ .description('Update storage box settings')
624
+ .option('-n, --name <name>', 'Storage box name')
625
+ .option('--webdav <enabled>', 'Enable/disable WebDAV')
626
+ .option('--samba <enabled>', 'Enable/disable Samba/CIFS')
627
+ .option('--ssh <enabled>', 'Enable/disable SSH/SFTP')
628
+ .option('--external <enabled>', 'Enable/disable external reachability')
629
+ .option('--zfs <enabled>', 'Enable/disable ZFS')
630
+ .action(asyncAction(async (client, boxId, options) => {
631
+ await client.updateStorageBox(parseInt(boxId), options.name, options.webdav ? options.webdav === 'true' : undefined, options.samba ? options.samba === 'true' : undefined, options.ssh ? options.ssh === 'true' : undefined, options.external ? options.external === 'true' : undefined, options.zfs ? options.zfs === 'true' : undefined);
632
+ console.log(fmt.success('Storage box updated.'));
633
+ }));
634
+ storagebox
635
+ .command('reset-password <id>')
636
+ .description('Reset storage box password')
637
+ .action(asyncAction(async (client, boxId) => {
638
+ const { password } = await client.resetStorageBoxPassword(parseInt(boxId));
639
+ console.log(fmt.success('Password reset.'));
640
+ console.log(`New password: ${fmt.colorize(password, 'yellow')}`);
641
+ }));
642
+ const snapshot = storagebox.command('snapshot').description('Storage box snapshot management');
643
+ snapshot
644
+ .command('list <box-id>')
645
+ .alias('ls')
646
+ .description('List storage box snapshots')
647
+ .action(asyncAction(async (client, boxId, options) => {
648
+ const snapshots = await client.listStorageBoxSnapshots(parseInt(boxId));
649
+ output(snapshots, fmt.formatStorageBoxSnapshots, options);
650
+ }));
651
+ snapshot
652
+ .command('create <box-id>')
653
+ .description('Create a new snapshot')
654
+ .action(asyncAction(async (client, boxId) => {
655
+ const { snapshot: snap } = await client.createStorageBoxSnapshot(parseInt(boxId));
656
+ console.log(fmt.success(`Snapshot created: ${snap.name}`));
657
+ }));
658
+ snapshot
659
+ .command('delete <box-id> <name>')
660
+ .alias('rm')
661
+ .description('Delete a snapshot')
662
+ .option('-y, --yes', 'Skip confirmation')
663
+ .action(asyncAction(async (client, boxId, snapshotName, options) => {
664
+ if (!await confirmAction(`Delete snapshot ${snapshotName}?`, options))
665
+ return;
666
+ await client.deleteStorageBoxSnapshot(parseInt(boxId), snapshotName);
667
+ console.log(fmt.success('Snapshot deleted.'));
668
+ }));
669
+ snapshot
670
+ .command('revert <box-id> <name>')
671
+ .description('Revert to a snapshot')
672
+ .option('-y, --yes', 'Skip confirmation')
673
+ .action(asyncAction(async (client, boxId, snapshotName, options) => {
674
+ if (!options.yes) {
675
+ console.log(fmt.warning('This will revert your storage box to the snapshot state.'));
676
+ }
677
+ if (!await confirmAction(`Revert to snapshot ${snapshotName}?`, options))
678
+ return;
679
+ await client.revertStorageBoxSnapshot(parseInt(boxId), snapshotName);
680
+ console.log(fmt.success('Reverted to snapshot.'));
681
+ }));
682
+ const subaccount = storagebox.command('subaccount').description('Storage box subaccount management');
683
+ subaccount
684
+ .command('list <box-id>')
685
+ .alias('ls')
686
+ .description('List storage box subaccounts')
687
+ .action(asyncAction(async (client, boxId, options) => {
688
+ const subaccounts = await client.listStorageBoxSubaccounts(parseInt(boxId));
689
+ output(subaccounts, fmt.formatStorageBoxSubaccounts, options);
690
+ }));
691
+ subaccount
692
+ .command('create <box-id> <home-directory>')
693
+ .description('Create a new subaccount')
694
+ .option('--samba <enabled>', 'Enable Samba')
695
+ .option('--ssh <enabled>', 'Enable SSH')
696
+ .option('--webdav <enabled>', 'Enable WebDAV')
697
+ .option('--external <enabled>', 'Enable external reachability')
698
+ .option('--readonly <enabled>', 'Read-only access')
699
+ .option('--comment <comment>', 'Comment')
700
+ .action(asyncAction(async (client, boxId, homeDir, options) => {
701
+ const { subaccount: sub } = await client.createStorageBoxSubaccount(parseInt(boxId), homeDir, options.samba ? options.samba === 'true' : undefined, options.ssh ? options.ssh === 'true' : undefined, options.external ? options.external === 'true' : undefined, options.webdav ? options.webdav === 'true' : undefined, options.readonly ? options.readonly === 'true' : undefined, options.comment);
702
+ console.log(fmt.success(`Subaccount created: ${sub.username}`));
703
+ }));
704
+ subaccount
705
+ .command('delete <box-id> <username>')
706
+ .alias('rm')
707
+ .description('Delete a subaccount')
708
+ .option('-y, --yes', 'Skip confirmation')
709
+ .action(asyncAction(async (client, boxId, username, options) => {
710
+ if (!await confirmAction(`Delete subaccount ${username}?`, options))
711
+ return;
712
+ await client.deleteStorageBoxSubaccount(parseInt(boxId), username);
713
+ console.log(fmt.success('Subaccount deleted.'));
714
+ }));
715
+ const traffic = program.command('traffic').description('Traffic analytics');
716
+ traffic
717
+ .command('query')
718
+ .description('Query traffic data')
719
+ .option('-i, --ip <ips...>', 'IP addresses to query')
720
+ .option('-s, --subnet <subnets...>', 'Subnets to query')
721
+ .option('--from <date>', 'Start date (YYYY-MM-DD)')
722
+ .option('--to <date>', 'End date (YYYY-MM-DD)')
723
+ .option('-t, --type <type>', 'Query type (day, month, year)', 'month')
724
+ .action(asyncAction(async (client, options) => {
725
+ const now = new Date();
726
+ const from = options.from || new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0];
727
+ const to = options.to || now.toISOString().split('T')[0];
728
+ const { traffic: trafficData } = await client.getTraffic(options.ip || [], options.subnet || [], from, to, options.type);
729
+ output(trafficData, fmt.formatTraffic, options);
730
+ }));
731
+ const wol = program.command('wol').description('Wake on LAN');
732
+ wol
733
+ .command('status <server>')
734
+ .description('Check WoL status for server')
735
+ .action(asyncAction(async (client, serverIdOrIp, options) => {
736
+ const { wol: wolData } = await client.getWol(serverIdOrIp);
737
+ output(wolData, (w) => `Server: ${w.server_number} (${w.server_ip})\n${fmt.info('Wake on LAN is available for this server.')}`, options);
738
+ }));
739
+ wol
740
+ .command('send <server>')
741
+ .description('Send Wake on LAN packet')
742
+ .action(asyncAction(async (client, serverIdOrIp) => {
743
+ const { wol: wolData } = await client.sendWol(serverIdOrIp);
744
+ console.log(fmt.formatWolResult(wolData));
745
+ }));
746
+ const cancel = program.command('cancel').description('Server cancellation');
747
+ cancel
748
+ .command('status <server>')
749
+ .description('Get cancellation status')
750
+ .action(asyncAction(async (client, serverIdOrIp, options) => {
751
+ const { cancellation } = await client.getCancellation(serverIdOrIp);
752
+ output(cancellation, fmt.formatCancellation, options);
753
+ }));
754
+ cancel
755
+ .command('request <server>')
756
+ .description('Request server cancellation')
757
+ .option('--date <date>', 'Cancellation date (YYYY-MM-DD)')
758
+ .option('--reason <reasons...>', 'Cancellation reasons')
759
+ .option('-y, --yes', 'Skip confirmation')
760
+ .action(asyncAction(async (client, serverIdOrIp, options) => {
761
+ if (!options.yes) {
762
+ console.log(fmt.warning('This will cancel your server!'));
763
+ }
764
+ if (!await confirmAction('Are you sure you want to cancel this server?', options))
765
+ return;
766
+ const { cancellation } = await client.cancelServer(serverIdOrIp, options.date, options.reason);
767
+ console.log(fmt.success('Cancellation requested.'));
768
+ console.log(fmt.formatCancellation(cancellation));
769
+ }));
770
+ cancel
771
+ .command('revoke <server>')
772
+ .description('Revoke server cancellation')
773
+ .action(asyncAction(async (client, serverIdOrIp) => {
774
+ await client.revokeCancellation(serverIdOrIp);
775
+ console.log(fmt.success('Cancellation revoked.'));
776
+ }));
777
+ const order = program.command('order').description('Server ordering');
778
+ order
779
+ .command('products')
780
+ .description('List available server products')
781
+ .action(asyncAction(async (client, options) => {
782
+ const products = await client.listServerProducts();
783
+ output(products, fmt.formatServerProductList, options);
784
+ }));
785
+ order
786
+ .command('market')
787
+ .description('List server market (auction) products')
788
+ .action(asyncAction(async (client, options) => {
789
+ const products = await client.listServerMarketProducts();
790
+ output(products, fmt.formatServerMarketProductList, options);
791
+ }));
792
+ order
793
+ .command('transactions')
794
+ .description('List order transactions')
795
+ .action(asyncAction(async (client, options) => {
796
+ const transactions = await client.listServerTransactions();
797
+ output(transactions, fmt.formatTransactionList, options);
798
+ }));
799
+ order
800
+ .command('transaction <id>')
801
+ .description('Get order transaction details')
802
+ .action(asyncAction(async (client, transactionId, options) => {
803
+ const { transaction } = await client.getServerTransaction(transactionId);
804
+ output(transaction, (t) => fmt.formatTransactionList([{ transaction: t }]), options);
805
+ }));
806
+ program
807
+ .command('interactive')
808
+ .alias('i')
809
+ .description('Interactive mode for common operations')
810
+ .action(asyncAction(async (client) => {
811
+ while (true) {
812
+ const action = await select({
813
+ message: 'What would you like to do?',
814
+ choices: [
815
+ { value: 'servers', name: 'List servers' },
816
+ { value: 'reset', name: 'Reset a server' },
817
+ { value: 'rescue', name: 'Activate rescue mode' },
818
+ { value: 'failover', name: 'Switch failover IP' },
819
+ { value: 'keys', name: 'Manage SSH keys' },
820
+ { value: 'exit', name: 'Exit' },
821
+ ],
822
+ pageSize: 10,
823
+ });
824
+ if (action === 'exit') {
825
+ console.log('Goodbye!');
826
+ break;
827
+ }
828
+ if (action === 'servers') {
829
+ const servers = await client.listServers();
830
+ console.log('');
831
+ console.log(fmt.formatServerList(servers));
832
+ console.log('');
833
+ }
834
+ if (action === 'reset') {
835
+ const servers = await client.listServers();
836
+ if (servers.length === 0) {
837
+ console.log(fmt.info('No servers found.'));
838
+ continue;
839
+ }
840
+ const selected = await checkbox({
841
+ message: 'Select servers to reset:',
842
+ choices: servers.map(({ server }) => ({
843
+ value: server.server_ip,
844
+ name: `${server.server_number} - ${server.server_ip} (${server.server_name || 'unnamed'})`,
845
+ })),
846
+ });
847
+ if (selected.length === 0) {
848
+ console.log('No servers selected.');
849
+ continue;
850
+ }
851
+ const resetType = (await select({
852
+ message: 'Select reset type:',
853
+ choices: [
854
+ { value: 'sw', name: 'Software reset (ACPI)' },
855
+ { value: 'hw', name: 'Hardware reset (forced)' },
856
+ { value: 'power', name: 'Power cycle' },
857
+ ],
858
+ }));
859
+ const confirmed = await confirm({
860
+ message: `Reset ${selected.length} server(s) with ${resetType}?`,
861
+ default: false,
862
+ });
863
+ if (confirmed) {
864
+ for (const srv of selected) {
865
+ try {
866
+ const { reset: rst } = await client.resetServer(srv, resetType);
867
+ console.log(fmt.formatResetResult(rst, resetType));
868
+ }
869
+ catch (error) {
870
+ console.log(fmt.error(`Failed to reset ${srv}: ${error instanceof Error ? error.message : 'Unknown'}`));
871
+ }
872
+ }
873
+ }
874
+ }
875
+ if (action === 'rescue') {
876
+ const servers = await client.listServers();
877
+ if (servers.length === 0) {
878
+ console.log(fmt.info('No servers found.'));
879
+ continue;
880
+ }
881
+ const serverIp = await select({
882
+ message: 'Select server:',
883
+ choices: servers.map(({ server }) => ({
884
+ value: server.server_ip,
885
+ name: `${server.server_number} - ${server.server_ip} (${server.server_name || 'unnamed'})`,
886
+ })),
887
+ });
888
+ const os = await select({
889
+ message: 'Select rescue OS:',
890
+ choices: [
891
+ { value: 'linux', name: 'Linux (64-bit)' },
892
+ { value: 'linuxold', name: 'Linux (32-bit)' },
893
+ { value: 'vkvm', name: 'vKVM' },
894
+ ],
895
+ });
896
+ const { rescue } = await client.activateRescue(serverIp, os, 64);
897
+ console.log('');
898
+ console.log(fmt.formatRescueActivation(rescue));
899
+ }
900
+ if (action === 'failover') {
901
+ const failovers = await client.listFailovers();
902
+ if (failovers.length === 0) {
903
+ console.log(fmt.info('No failover IPs found.'));
904
+ continue;
905
+ }
906
+ const failoverIp = await select({
907
+ message: 'Select failover IP:',
908
+ choices: failovers.map(({ failover }) => ({
909
+ value: failover.ip,
910
+ name: `${failover.ip} -> ${failover.active_server_ip}`,
911
+ })),
912
+ });
913
+ const servers = await client.listServers();
914
+ const targetIp = await select({
915
+ message: 'Route to server:',
916
+ choices: servers.map(({ server }) => ({
917
+ value: server.server_ip,
918
+ name: `${server.server_number} - ${server.server_ip}`,
919
+ })),
920
+ });
921
+ const { failover: fo } = await client.switchFailover(failoverIp, targetIp);
922
+ console.log('');
923
+ console.log(fmt.formatFailoverSwitch(fo));
924
+ }
925
+ if (action === 'keys') {
926
+ const keys = await client.listSshKeys();
927
+ console.log('');
928
+ console.log(fmt.formatSshKeyList(keys));
929
+ console.log('');
930
+ }
931
+ console.log('');
932
+ }
933
+ }));
934
+ program.parse();