hetzner-cli 2.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 (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +907 -0
  3. package/dist/auction/client.d.ts +4 -0
  4. package/dist/auction/client.js +103 -0
  5. package/dist/auction/commands.d.ts +2 -0
  6. package/dist/auction/commands.js +138 -0
  7. package/dist/auction/formatter.d.ts +3 -0
  8. package/dist/auction/formatter.js +87 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +39 -0
  11. package/dist/client.d.ts +2 -0
  12. package/dist/client.js +4 -0
  13. package/dist/cloud/client.d.ts +511 -0
  14. package/dist/cloud/client.js +706 -0
  15. package/dist/cloud/commands/certificate.d.ts +2 -0
  16. package/dist/cloud/commands/certificate.js +77 -0
  17. package/dist/cloud/commands/context.d.ts +2 -0
  18. package/dist/cloud/commands/context.js +78 -0
  19. package/dist/cloud/commands/datacenter.d.ts +2 -0
  20. package/dist/cloud/commands/datacenter.js +20 -0
  21. package/dist/cloud/commands/firewall.d.ts +2 -0
  22. package/dist/cloud/commands/firewall.js +77 -0
  23. package/dist/cloud/commands/floating-ip.d.ts +2 -0
  24. package/dist/cloud/commands/floating-ip.js +83 -0
  25. package/dist/cloud/commands/image.d.ts +2 -0
  26. package/dist/cloud/commands/image.js +60 -0
  27. package/dist/cloud/commands/index.d.ts +2 -0
  28. package/dist/cloud/commands/index.js +41 -0
  29. package/dist/cloud/commands/iso.d.ts +2 -0
  30. package/dist/cloud/commands/iso.js +22 -0
  31. package/dist/cloud/commands/load-balancer-type.d.ts +2 -0
  32. package/dist/cloud/commands/load-balancer-type.js +20 -0
  33. package/dist/cloud/commands/load-balancer.d.ts +2 -0
  34. package/dist/cloud/commands/load-balancer.js +177 -0
  35. package/dist/cloud/commands/location.d.ts +2 -0
  36. package/dist/cloud/commands/location.js +20 -0
  37. package/dist/cloud/commands/network.d.ts +2 -0
  38. package/dist/cloud/commands/network.js +96 -0
  39. package/dist/cloud/commands/placement-group.d.ts +2 -0
  40. package/dist/cloud/commands/placement-group.js +53 -0
  41. package/dist/cloud/commands/primary-ip.d.ts +2 -0
  42. package/dist/cloud/commands/primary-ip.js +83 -0
  43. package/dist/cloud/commands/server-type.d.ts +2 -0
  44. package/dist/cloud/commands/server-type.js +20 -0
  45. package/dist/cloud/commands/server.d.ts +2 -0
  46. package/dist/cloud/commands/server.js +260 -0
  47. package/dist/cloud/commands/ssh-key.d.ts +2 -0
  48. package/dist/cloud/commands/ssh-key.js +63 -0
  49. package/dist/cloud/commands/volume.d.ts +2 -0
  50. package/dist/cloud/commands/volume.js +92 -0
  51. package/dist/cloud/context.d.ts +28 -0
  52. package/dist/cloud/context.js +172 -0
  53. package/dist/cloud/formatter.d.ts +37 -0
  54. package/dist/cloud/formatter.js +413 -0
  55. package/dist/cloud/helpers.d.ts +18 -0
  56. package/dist/cloud/helpers.js +48 -0
  57. package/dist/cloud/types.d.ts +398 -0
  58. package/dist/cloud/types.js +5 -0
  59. package/dist/config.d.ts +1 -0
  60. package/dist/config.js +2 -0
  61. package/dist/formatter.d.ts +3 -0
  62. package/dist/formatter.js +6 -0
  63. package/dist/index.d.ts +10 -0
  64. package/dist/index.js +17 -0
  65. package/dist/robot/client.d.ts +256 -0
  66. package/dist/robot/client.js +656 -0
  67. package/dist/robot/commands/auth.d.ts +2 -0
  68. package/dist/robot/commands/auth.js +54 -0
  69. package/dist/robot/commands/boot.d.ts +2 -0
  70. package/dist/robot/commands/boot.js +72 -0
  71. package/dist/robot/commands/cancel.d.ts +2 -0
  72. package/dist/robot/commands/cancel.js +36 -0
  73. package/dist/robot/commands/failover.d.ts +2 -0
  74. package/dist/robot/commands/failover.js +42 -0
  75. package/dist/robot/commands/firewall.d.ts +2 -0
  76. package/dist/robot/commands/firewall.js +66 -0
  77. package/dist/robot/commands/index.d.ts +2 -0
  78. package/dist/robot/commands/index.js +36 -0
  79. package/dist/robot/commands/interactive.d.ts +2 -0
  80. package/dist/robot/commands/interactive.js +134 -0
  81. package/dist/robot/commands/ip.d.ts +2 -0
  82. package/dist/robot/commands/ip.js +52 -0
  83. package/dist/robot/commands/key.d.ts +2 -0
  84. package/dist/robot/commands/key.js +64 -0
  85. package/dist/robot/commands/order.d.ts +2 -0
  86. package/dist/robot/commands/order.js +33 -0
  87. package/dist/robot/commands/rdns.d.ts +2 -0
  88. package/dist/robot/commands/rdns.js +41 -0
  89. package/dist/robot/commands/reset.d.ts +2 -0
  90. package/dist/robot/commands/reset.js +77 -0
  91. package/dist/robot/commands/server.d.ts +2 -0
  92. package/dist/robot/commands/server.js +29 -0
  93. package/dist/robot/commands/storagebox.d.ts +2 -0
  94. package/dist/robot/commands/storagebox.js +116 -0
  95. package/dist/robot/commands/subnet.d.ts +2 -0
  96. package/dist/robot/commands/subnet.js +21 -0
  97. package/dist/robot/commands/traffic.d.ts +2 -0
  98. package/dist/robot/commands/traffic.js +20 -0
  99. package/dist/robot/commands/vswitch.d.ts +2 -0
  100. package/dist/robot/commands/vswitch.js +64 -0
  101. package/dist/robot/commands/wol.d.ts +2 -0
  102. package/dist/robot/commands/wol.js +20 -0
  103. package/dist/robot/formatter.d.ts +58 -0
  104. package/dist/robot/formatter.js +500 -0
  105. package/dist/robot/types.d.ts +352 -0
  106. package/dist/robot/types.js +5 -0
  107. package/dist/shared/config.d.ts +86 -0
  108. package/dist/shared/config.js +273 -0
  109. package/dist/shared/formatter.d.ts +29 -0
  110. package/dist/shared/formatter.js +118 -0
  111. package/dist/shared/helpers.d.ts +17 -0
  112. package/dist/shared/helpers.js +72 -0
  113. package/dist/shared/reference.d.ts +2 -0
  114. package/dist/shared/reference.js +626 -0
  115. package/dist/types.d.ts +75 -0
  116. package/dist/types.js +1 -0
  117. package/package.json +112 -0
@@ -0,0 +1,260 @@
1
+ import { cloudAction, cloudOutput, cloudConfirm } from '../helpers.js';
2
+ import * as fmt from '../../shared/formatter.js';
3
+ import * as cloudFmt from '../formatter.js';
4
+ export function registerCloudServerCommands(parent) {
5
+ const server = parent.command('server').description('Cloud server management');
6
+ server
7
+ .command('list')
8
+ .alias('ls')
9
+ .description('List all servers')
10
+ .option('-l, --label-selector <selector>', 'Label selector')
11
+ .option('-n, --name <name>', 'Filter by name')
12
+ .option('-s, --sort <field>', 'Sort by field')
13
+ .option('--status <status>', 'Filter by status')
14
+ .action(cloudAction(async (client, options) => {
15
+ const servers = await client.listServers({ label_selector: options.labelSelector, name: options.name, sort: options.sort, status: options.status });
16
+ cloudOutput(servers, cloudFmt.formatCloudServerList, options);
17
+ }));
18
+ server
19
+ .command('describe <id>')
20
+ .description('Show server details')
21
+ .action(cloudAction(async (client, id, options) => {
22
+ const srv = await client.getServer(parseInt(id));
23
+ cloudOutput(srv, cloudFmt.formatCloudServerDetails, options);
24
+ }));
25
+ server
26
+ .command('create')
27
+ .description('Create a new server')
28
+ .requiredOption('--name <name>', 'Server name')
29
+ .requiredOption('--type <type>', 'Server type')
30
+ .requiredOption('--image <image>', 'Image to use')
31
+ .option('--location <location>', 'Location')
32
+ .option('--datacenter <dc>', 'Datacenter')
33
+ .option('--ssh-key <keys...>', 'SSH key IDs or names')
34
+ .option('--user-data <data>', 'Cloud-init user data')
35
+ .option('--start-after-create', 'Start server after creation', true)
36
+ .option('--no-start-after-create', 'Do not start server after creation')
37
+ .action(cloudAction(async (client, options) => {
38
+ const result = await client.createServer({
39
+ name: options.name,
40
+ server_type: options.type,
41
+ image: options.image,
42
+ location: options.location,
43
+ datacenter: options.datacenter,
44
+ ssh_keys: options.sshKey,
45
+ user_data: options.userData,
46
+ start_after_create: options.startAfterCreate,
47
+ });
48
+ console.log(fmt.success(`Server '${result.server.name}' created (ID: ${result.server.id})`));
49
+ if (result.root_password) {
50
+ console.log(`Root password: ${fmt.colorize(result.root_password, 'yellow')}`);
51
+ }
52
+ console.log(fmt.info(`IPv4: ${result.server.public_net.ipv4?.ip || 'pending'}`));
53
+ }));
54
+ server
55
+ .command('delete <id>')
56
+ .description('Delete a server')
57
+ .option('-y, --yes', 'Skip confirmation')
58
+ .action(cloudAction(async (client, id, options) => {
59
+ if (!await cloudConfirm(`Delete server ${id}?`, options))
60
+ return;
61
+ await client.deleteServer(parseInt(id));
62
+ console.log(fmt.success(`Server ${id} deleted.`));
63
+ }));
64
+ server
65
+ .command('update <id>')
66
+ .description('Update server')
67
+ .option('--name <name>', 'New name')
68
+ .action(cloudAction(async (client, id, options) => {
69
+ const { server: srv } = await client.updateServer(parseInt(id), { name: options.name });
70
+ console.log(fmt.success(`Server '${srv.name}' updated.`));
71
+ }));
72
+ server
73
+ .command('poweron <id>')
74
+ .description('Power on a server')
75
+ .action(cloudAction(async (client, id) => {
76
+ await client.powerOnServer(parseInt(id));
77
+ console.log(fmt.success(`Server ${id} powered on.`));
78
+ }));
79
+ server
80
+ .command('poweroff <id>')
81
+ .description('Power off a server (hard)')
82
+ .action(cloudAction(async (client, id) => {
83
+ await client.powerOffServer(parseInt(id));
84
+ console.log(fmt.success(`Server ${id} powered off.`));
85
+ }));
86
+ server
87
+ .command('reboot <id>')
88
+ .description('Soft reboot a server')
89
+ .action(cloudAction(async (client, id) => {
90
+ await client.rebootServer(parseInt(id));
91
+ console.log(fmt.success(`Server ${id} rebooted.`));
92
+ }));
93
+ server
94
+ .command('reset <id>')
95
+ .description('Hard reset a server')
96
+ .action(cloudAction(async (client, id) => {
97
+ await client.resetServer(parseInt(id));
98
+ console.log(fmt.success(`Server ${id} reset.`));
99
+ }));
100
+ server
101
+ .command('shutdown <id>')
102
+ .description('Gracefully shutdown a server (ACPI)')
103
+ .action(cloudAction(async (client, id) => {
104
+ await client.shutdownServer(parseInt(id));
105
+ console.log(fmt.success(`Server ${id} shutdown initiated.`));
106
+ }));
107
+ server
108
+ .command('rebuild <id>')
109
+ .description('Rebuild a server with a new image')
110
+ .requiredOption('--image <image>', 'Image to rebuild with')
111
+ .action(cloudAction(async (client, id, options) => {
112
+ const result = await client.rebuildServer(parseInt(id), options.image);
113
+ console.log(fmt.success(`Server ${id} rebuilding.`));
114
+ if (result.root_password) {
115
+ console.log(`Root password: ${fmt.colorize(result.root_password, 'yellow')}`);
116
+ }
117
+ }));
118
+ server
119
+ .command('change-type <id>')
120
+ .description('Change server type (resize)')
121
+ .requiredOption('--type <type>', 'New server type')
122
+ .option('--upgrade-disk', 'Upgrade disk size (irreversible)', false)
123
+ .action(cloudAction(async (client, id, options) => {
124
+ await client.changeServerType(parseInt(id), options.type, !!options.upgradeDisk);
125
+ console.log(fmt.success(`Server ${id} type change initiated.`));
126
+ }));
127
+ server
128
+ .command('enable-rescue <id>')
129
+ .description('Enable rescue mode')
130
+ .option('--type <type>', 'Rescue system type', 'linux64')
131
+ .option('--ssh-key <keys...>', 'SSH key IDs')
132
+ .action(cloudAction(async (client, id, options) => {
133
+ const result = await client.enableServerRescue(parseInt(id), options.type, options.sshKey?.map(Number));
134
+ console.log(fmt.success('Rescue mode enabled.'));
135
+ console.log(`Root password: ${fmt.colorize(result.root_password, 'yellow')}`);
136
+ }));
137
+ server
138
+ .command('disable-rescue <id>')
139
+ .description('Disable rescue mode')
140
+ .action(cloudAction(async (client, id) => {
141
+ await client.disableServerRescue(parseInt(id));
142
+ console.log(fmt.success('Rescue mode disabled.'));
143
+ }));
144
+ server
145
+ .command('enable-backup <id>')
146
+ .description('Enable automatic backups')
147
+ .action(cloudAction(async (client, id) => {
148
+ await client.enableServerBackup(parseInt(id));
149
+ console.log(fmt.success(`Backups enabled for server ${id}.`));
150
+ }));
151
+ server
152
+ .command('disable-backup <id>')
153
+ .description('Disable automatic backups')
154
+ .action(cloudAction(async (client, id) => {
155
+ await client.disableServerBackup(parseInt(id));
156
+ console.log(fmt.success(`Backups disabled for server ${id}.`));
157
+ }));
158
+ server
159
+ .command('create-image <id>')
160
+ .description('Create an image (snapshot) from server')
161
+ .option('--description <desc>', 'Image description')
162
+ .option('--type <type>', 'Image type (snapshot or backup)', 'snapshot')
163
+ .action(cloudAction(async (client, id, options) => {
164
+ const result = await client.createServerImage(parseInt(id), { description: options.description, type: options.type });
165
+ console.log(fmt.success(`Image created (ID: ${result.image.id})`));
166
+ }));
167
+ server
168
+ .command('attach-iso <id>')
169
+ .description('Attach an ISO to a server')
170
+ .requiredOption('--iso <iso>', 'ISO name or ID')
171
+ .action(cloudAction(async (client, id, options) => {
172
+ await client.attachIsoToServer(parseInt(id), options.iso);
173
+ console.log(fmt.success(`ISO attached to server ${id}.`));
174
+ }));
175
+ server
176
+ .command('detach-iso <id>')
177
+ .description('Detach ISO from a server')
178
+ .action(cloudAction(async (client, id) => {
179
+ await client.detachIsoFromServer(parseInt(id));
180
+ console.log(fmt.success(`ISO detached from server ${id}.`));
181
+ }));
182
+ server
183
+ .command('reset-password <id>')
184
+ .description('Reset server root password')
185
+ .action(cloudAction(async (client, id) => {
186
+ const result = await client.resetServerPassword(parseInt(id));
187
+ console.log(fmt.success('Root password reset.'));
188
+ console.log(`New password: ${fmt.colorize(result.root_password, 'yellow')}`);
189
+ }));
190
+ server
191
+ .command('set-rdns <id>')
192
+ .description('Set reverse DNS for server')
193
+ .requiredOption('--ip <ip>', 'IP address')
194
+ .requiredOption('--dns-ptr <ptr>', 'DNS pointer')
195
+ .action(cloudAction(async (client, id, options) => {
196
+ await client.setServerRdns(parseInt(id), options.ip, options.dnsPtr);
197
+ console.log(fmt.success(`rDNS set: ${options.ip} -> ${options.dnsPtr}`));
198
+ }));
199
+ server
200
+ .command('enable-protection <id>')
201
+ .description('Enable server protection')
202
+ .option('--delete', 'Enable delete protection', true)
203
+ .option('--rebuild', 'Enable rebuild protection', false)
204
+ .action(cloudAction(async (client, id, options) => {
205
+ await client.enableServerProtection(parseInt(id), { delete: options.delete, rebuild: options.rebuild });
206
+ console.log(fmt.success(`Protection enabled for server ${id}.`));
207
+ }));
208
+ server
209
+ .command('disable-protection <id>')
210
+ .description('Disable server protection')
211
+ .action(cloudAction(async (client, id) => {
212
+ await client.enableServerProtection(parseInt(id), { delete: false, rebuild: false });
213
+ console.log(fmt.success(`Protection disabled for server ${id}.`));
214
+ }));
215
+ server
216
+ .command('request-console <id>')
217
+ .description('Request a WebSocket VNC console')
218
+ .action(cloudAction(async (client, id) => {
219
+ const result = await client.requestServerConsole(parseInt(id));
220
+ console.log(fmt.success('Console ready.'));
221
+ console.log(`WebSocket URL: ${result.wss_url}`);
222
+ console.log(`Password: ${fmt.colorize(result.password, 'yellow')}`);
223
+ }));
224
+ server
225
+ .command('attach-to-network <id>')
226
+ .description('Attach server to a network')
227
+ .requiredOption('--network <network>', 'Network ID')
228
+ .option('--ip <ip>', 'IP address in network')
229
+ .action(cloudAction(async (client, id, options) => {
230
+ await client.attachServerToNetwork(parseInt(id), parseInt(options.network), options.ip);
231
+ console.log(fmt.success(`Server ${id} attached to network ${options.network}.`));
232
+ }));
233
+ server
234
+ .command('detach-from-network <id>')
235
+ .description('Detach server from a network')
236
+ .requiredOption('--network <network>', 'Network ID')
237
+ .action(cloudAction(async (client, id, options) => {
238
+ await client.detachServerFromNetwork(parseInt(id), parseInt(options.network));
239
+ console.log(fmt.success(`Server ${id} detached from network ${options.network}.`));
240
+ }));
241
+ server
242
+ .command('add-label <id> <label>')
243
+ .description('Add a label (key=value)')
244
+ .action(cloudAction(async (client, id, label) => {
245
+ const srv = await client.getServer(parseInt(id));
246
+ const [key, value] = label.split('=');
247
+ const labels = { ...srv.labels, [key]: value || '' };
248
+ await client.updateServer(parseInt(id), { labels });
249
+ console.log(fmt.success(`Label '${key}' added to server ${id}.`));
250
+ }));
251
+ server
252
+ .command('remove-label <id> <key>')
253
+ .description('Remove a label by key')
254
+ .action(cloudAction(async (client, id, key) => {
255
+ const srv = await client.getServer(parseInt(id));
256
+ const labels = Object.fromEntries(Object.entries(srv.labels).filter(([k]) => k !== key));
257
+ await client.updateServer(parseInt(id), { labels });
258
+ console.log(fmt.success(`Label '${key}' removed from server ${id}.`));
259
+ }));
260
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerCloudSshKeyCommands(parent: Command): void;
@@ -0,0 +1,63 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { cloudAction, cloudOutput, cloudConfirm } from '../helpers.js';
3
+ import * as fmt from '../../shared/formatter.js';
4
+ import * as cloudFmt from '../formatter.js';
5
+ export function registerCloudSshKeyCommands(parent) {
6
+ const sshKey = parent.command('ssh-key').description('SSH key management');
7
+ sshKey.command('list').alias('ls').description('List all SSH keys')
8
+ .option('-l, --label-selector <selector>', 'Label selector')
9
+ .option('-s, --sort <field>', 'Sort by field')
10
+ .action(cloudAction(async (client, options) => {
11
+ const keys = await client.listSshKeys({ label_selector: options.labelSelector, sort: options.sort });
12
+ cloudOutput(keys, cloudFmt.formatCloudSshKeyList, options);
13
+ }));
14
+ sshKey.command('describe <id>').description('Show SSH key details')
15
+ .action(cloudAction(async (client, id, options) => {
16
+ const key = await client.getSshKey(parseInt(id));
17
+ cloudOutput(key, cloudFmt.formatCloudSshKeyDetails, options);
18
+ }));
19
+ sshKey.command('create').description('Create an SSH key')
20
+ .requiredOption('--name <name>', 'Key name')
21
+ .option('--public-key <key>', 'Public key string')
22
+ .option('--public-key-from-file <path>', 'Read public key from file')
23
+ .action(cloudAction(async (client, options) => {
24
+ let pubKey = options.publicKey;
25
+ if (options.publicKeyFromFile) {
26
+ pubKey = readFileSync(options.publicKeyFromFile, 'utf-8').trim();
27
+ }
28
+ if (!pubKey) {
29
+ console.error(fmt.error('Provide --public-key or --public-key-from-file'));
30
+ process.exit(1);
31
+ }
32
+ const { ssh_key: key } = await client.createSshKey({ name: options.name, public_key: pubKey });
33
+ console.log(fmt.success(`SSH key '${key.name}' created (ID: ${key.id})`));
34
+ }));
35
+ sshKey.command('delete <id>').description('Delete an SSH key')
36
+ .option('-y, --yes', 'Skip confirmation')
37
+ .action(cloudAction(async (client, id, options) => {
38
+ if (!await cloudConfirm(`Delete SSH key ${id}?`, options))
39
+ return;
40
+ await client.deleteSshKey(parseInt(id));
41
+ console.log(fmt.success(`SSH key ${id} deleted.`));
42
+ }));
43
+ sshKey.command('update <id>').description('Update SSH key')
44
+ .option('--name <name>', 'New name')
45
+ .action(cloudAction(async (client, id, options) => {
46
+ await client.updateSshKey(parseInt(id), { name: options.name });
47
+ console.log(fmt.success(`SSH key ${id} updated.`));
48
+ }));
49
+ sshKey.command('add-label <id> <label>').description('Add a label (key=value)')
50
+ .action(cloudAction(async (client, id, label) => {
51
+ const key = await client.getSshKey(parseInt(id));
52
+ const [k, v] = label.split('=');
53
+ await client.updateSshKey(parseInt(id), { labels: { ...key.labels, [k]: v || '' } });
54
+ console.log(fmt.success(`Label '${k}' added.`));
55
+ }));
56
+ sshKey.command('remove-label <id> <key>').description('Remove a label')
57
+ .action(cloudAction(async (client, id, key) => {
58
+ const sshKeyData = await client.getSshKey(parseInt(id));
59
+ const labels = Object.fromEntries(Object.entries(sshKeyData.labels).filter(([k]) => k !== key));
60
+ await client.updateSshKey(parseInt(id), { labels });
61
+ console.log(fmt.success(`Label '${key}' removed.`));
62
+ }));
63
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerVolumeCommands(parent: Command): void;
@@ -0,0 +1,92 @@
1
+ import { cloudAction, cloudOutput, cloudConfirm } from '../helpers.js';
2
+ import * as fmt from '../../shared/formatter.js';
3
+ import * as cloudFmt from '../formatter.js';
4
+ export function registerVolumeCommands(parent) {
5
+ const volume = parent.command('volume').description('Volume management');
6
+ volume.command('list').alias('ls').description('List all volumes')
7
+ .option('-l, --label-selector <selector>', 'Label selector')
8
+ .option('-s, --sort <field>', 'Sort by field')
9
+ .option('--status <status>', 'Filter by status')
10
+ .action(cloudAction(async (client, options) => {
11
+ const volumes = await client.listVolumes({ label_selector: options.labelSelector, sort: options.sort, status: options.status });
12
+ cloudOutput(volumes, cloudFmt.formatVolumeList, options);
13
+ }));
14
+ volume.command('describe <id>').description('Show volume details')
15
+ .action(cloudAction(async (client, id, options) => {
16
+ const vol = await client.getVolume(parseInt(id));
17
+ cloudOutput(vol, cloudFmt.formatVolumeDetails, options);
18
+ }));
19
+ volume.command('create').description('Create a volume')
20
+ .requiredOption('--name <name>', 'Volume name')
21
+ .requiredOption('--size <size>', 'Size in GB')
22
+ .option('--location <loc>', 'Location')
23
+ .option('--server <id>', 'Server to attach to')
24
+ .option('--format <format>', 'Filesystem format (ext4, xfs)')
25
+ .option('--automount', 'Auto-mount on server')
26
+ .action(cloudAction(async (client, options) => {
27
+ const { volume: vol } = await client.createVolume({
28
+ name: options.name,
29
+ size: parseInt(options.size),
30
+ location: options.location,
31
+ server: options.server ? parseInt(options.server) : undefined,
32
+ format: options.format,
33
+ automount: options.automount,
34
+ });
35
+ console.log(fmt.success(`Volume '${vol.name}' created (ID: ${vol.id}, ${vol.size} GB)`));
36
+ }));
37
+ volume.command('delete <id>').description('Delete a volume')
38
+ .option('-y, --yes', 'Skip confirmation')
39
+ .action(cloudAction(async (client, id, options) => {
40
+ if (!await cloudConfirm(`Delete volume ${id}?`, options))
41
+ return;
42
+ await client.deleteVolume(parseInt(id));
43
+ console.log(fmt.success(`Volume ${id} deleted.`));
44
+ }));
45
+ volume.command('update <id>').description('Update volume')
46
+ .option('--name <name>', 'New name')
47
+ .action(cloudAction(async (client, id, options) => {
48
+ await client.updateVolume(parseInt(id), { name: options.name });
49
+ console.log(fmt.success(`Volume ${id} updated.`));
50
+ }));
51
+ volume.command('attach <id> <server>').description('Attach volume to server')
52
+ .option('--automount', 'Auto-mount on server')
53
+ .action(cloudAction(async (client, id, server, options) => {
54
+ await client.attachVolume(parseInt(id), parseInt(server), options.automount);
55
+ console.log(fmt.success(`Volume ${id} attached to server ${server}.`));
56
+ }));
57
+ volume.command('detach <id>').description('Detach volume from server')
58
+ .action(cloudAction(async (client, id) => {
59
+ await client.detachVolume(parseInt(id));
60
+ console.log(fmt.success(`Volume ${id} detached.`));
61
+ }));
62
+ volume.command('resize <id>').description('Resize a volume')
63
+ .requiredOption('--size <size>', 'New size in GB')
64
+ .action(cloudAction(async (client, id, options) => {
65
+ await client.resizeVolume(parseInt(id), parseInt(options.size));
66
+ console.log(fmt.success(`Volume ${id} resized to ${options.size} GB.`));
67
+ }));
68
+ volume.command('enable-protection <id>').description('Enable delete protection')
69
+ .action(cloudAction(async (client, id) => {
70
+ await client.changeVolumeProtection(parseInt(id), true);
71
+ console.log(fmt.success(`Protection enabled for volume ${id}.`));
72
+ }));
73
+ volume.command('disable-protection <id>').description('Disable delete protection')
74
+ .action(cloudAction(async (client, id) => {
75
+ await client.changeVolumeProtection(parseInt(id), false);
76
+ console.log(fmt.success(`Protection disabled for volume ${id}.`));
77
+ }));
78
+ volume.command('add-label <id> <label>').description('Add a label (key=value)')
79
+ .action(cloudAction(async (client, id, label) => {
80
+ const vol = await client.getVolume(parseInt(id));
81
+ const [key, value] = label.split('=');
82
+ await client.updateVolume(parseInt(id), { labels: { ...vol.labels, [key]: value || '' } });
83
+ console.log(fmt.success(`Label '${key}' added.`));
84
+ }));
85
+ volume.command('remove-label <id> <key>').description('Remove a label')
86
+ .action(cloudAction(async (client, id, key) => {
87
+ const vol = await client.getVolume(parseInt(id));
88
+ const labels = Object.fromEntries(Object.entries(vol.labels).filter(([k]) => k !== key));
89
+ await client.updateVolume(parseInt(id), { labels });
90
+ console.log(fmt.success(`Label '${key}' removed.`));
91
+ }));
92
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Create a new cloud context.
3
+ */
4
+ export declare function createContext(name: string, token: string): Promise<void>;
5
+ /**
6
+ * Set the active context.
7
+ */
8
+ export declare function useContext(name: string): void;
9
+ /**
10
+ * Delete a context.
11
+ */
12
+ export declare function deleteContext(name: string): Promise<void>;
13
+ /**
14
+ * List all contexts.
15
+ */
16
+ export declare function listContexts(): {
17
+ name: string;
18
+ active: boolean;
19
+ }[];
20
+ /**
21
+ * Get the active context name.
22
+ */
23
+ export declare function getActiveContext(): string | null;
24
+ /**
25
+ * Resolve the cloud API token.
26
+ * Priority: --token flag > HETZNER_CLOUD_TOKEN env > active context
27
+ */
28
+ export declare function resolveToken(flagToken?: string): Promise<string>;
@@ -0,0 +1,172 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const CONFIG_DIR = join(homedir(), '.hetzner-cli');
5
+ const CONTEXTS_FILE = join(CONFIG_DIR, 'cloud-contexts.json');
6
+ const KEYCHAIN_SERVICE = 'hetzner-cli';
7
+ // Lazy-loaded keytar module
8
+ let keytarModule = null;
9
+ let keytarLoadAttempted = false;
10
+ async function getKeytar() {
11
+ if (keytarLoadAttempted)
12
+ return keytarModule;
13
+ keytarLoadAttempted = true;
14
+ try {
15
+ keytarModule = await import('keytar');
16
+ return keytarModule;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ function ensureConfigDir() {
23
+ if (!existsSync(CONFIG_DIR)) {
24
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
25
+ }
26
+ }
27
+ function loadContexts() {
28
+ if (!existsSync(CONTEXTS_FILE)) {
29
+ return { active: null, contexts: {} };
30
+ }
31
+ try {
32
+ const data = readFileSync(CONTEXTS_FILE, 'utf-8');
33
+ return JSON.parse(data);
34
+ }
35
+ catch {
36
+ return { active: null, contexts: {} };
37
+ }
38
+ }
39
+ function saveContexts(config) {
40
+ ensureConfigDir();
41
+ writeFileSync(CONTEXTS_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
42
+ }
43
+ function keychainAccount(contextName) {
44
+ return `cloud-token:${contextName}`;
45
+ }
46
+ async function storeToken(contextName, token) {
47
+ const keytar = await getKeytar();
48
+ if (keytar) {
49
+ try {
50
+ await keytar.setPassword(KEYCHAIN_SERVICE, keychainAccount(contextName), token);
51
+ return true;
52
+ }
53
+ catch {
54
+ // Fall through to file storage
55
+ }
56
+ }
57
+ console.warn('Warning: System keychain unavailable. Cloud token will be stored in plaintext at ~/.hetzner-cli/cloud-contexts.json');
58
+ return false;
59
+ }
60
+ async function retrieveToken(contextName) {
61
+ // Try keytar first
62
+ const keytar = await getKeytar();
63
+ if (keytar) {
64
+ try {
65
+ const token = await keytar.getPassword(KEYCHAIN_SERVICE, keychainAccount(contextName));
66
+ if (token)
67
+ return token;
68
+ }
69
+ catch {
70
+ // Fall through
71
+ }
72
+ }
73
+ // Fall back to file
74
+ const config = loadContexts();
75
+ return config.contexts[contextName]?.token ?? null;
76
+ }
77
+ async function deleteToken(contextName) {
78
+ const keytar = await getKeytar();
79
+ if (keytar) {
80
+ try {
81
+ await keytar.deletePassword(KEYCHAIN_SERVICE, keychainAccount(contextName));
82
+ }
83
+ catch {
84
+ // Ignore
85
+ }
86
+ }
87
+ }
88
+ /**
89
+ * Create a new cloud context.
90
+ */
91
+ export async function createContext(name, token) {
92
+ const config = loadContexts();
93
+ const storedInKeychain = await storeToken(name, token);
94
+ config.contexts[name] = {
95
+ name,
96
+ ...(storedInKeychain ? {} : { token }),
97
+ };
98
+ // Auto-activate if first context
99
+ if (!config.active) {
100
+ config.active = name;
101
+ }
102
+ saveContexts(config);
103
+ }
104
+ /**
105
+ * Set the active context.
106
+ */
107
+ export function useContext(name) {
108
+ const config = loadContexts();
109
+ if (!config.contexts[name]) {
110
+ throw new Error(`Context '${name}' not found. Use 'hetzner cloud context list' to see available contexts.`);
111
+ }
112
+ config.active = name;
113
+ saveContexts(config);
114
+ }
115
+ /**
116
+ * Delete a context.
117
+ */
118
+ export async function deleteContext(name) {
119
+ const config = loadContexts();
120
+ if (!config.contexts[name]) {
121
+ throw new Error(`Context '${name}' not found.`);
122
+ }
123
+ await deleteToken(name);
124
+ const { [name]: _, ...rest } = config.contexts;
125
+ config.contexts = rest;
126
+ if (config.active === name) {
127
+ const remaining = Object.keys(config.contexts);
128
+ config.active = remaining.length > 0 ? remaining[0] : null;
129
+ }
130
+ saveContexts(config);
131
+ }
132
+ /**
133
+ * List all contexts.
134
+ */
135
+ export function listContexts() {
136
+ const config = loadContexts();
137
+ return Object.values(config.contexts).map((ctx) => ({
138
+ name: ctx.name,
139
+ active: ctx.name === config.active,
140
+ }));
141
+ }
142
+ /**
143
+ * Get the active context name.
144
+ */
145
+ export function getActiveContext() {
146
+ const config = loadContexts();
147
+ return config.active;
148
+ }
149
+ /**
150
+ * Resolve the cloud API token.
151
+ * Priority: --token flag > HETZNER_CLOUD_TOKEN env > active context
152
+ */
153
+ export async function resolveToken(flagToken) {
154
+ // 1. CLI flag
155
+ if (flagToken)
156
+ return flagToken;
157
+ // 2. Environment variable
158
+ const envToken = process.env.HETZNER_CLOUD_TOKEN;
159
+ if (envToken)
160
+ return envToken;
161
+ // 3. Active context
162
+ const config = loadContexts();
163
+ if (config.active) {
164
+ const token = await retrieveToken(config.active);
165
+ if (token)
166
+ return token;
167
+ }
168
+ throw new Error('No cloud token found. Use one of:\n' +
169
+ ' --token <token> Pass token directly\n' +
170
+ ' HETZNER_CLOUD_TOKEN=<token> Set environment variable\n' +
171
+ ' hetzner cloud context create Configure a named context');
172
+ }
@@ -0,0 +1,37 @@
1
+ import type { Datacenter, Location, ServerType, LoadBalancerType, ISO, CloudServer, Network, CloudFirewall, FloatingIp, PrimaryIp, Volume, LoadBalancer, Image, CloudSshKey, Certificate, PlacementGroup } from './types.js';
2
+ export declare function formatContextList(contexts: {
3
+ name: string;
4
+ active: boolean;
5
+ }[]): string;
6
+ export declare function formatDatacenterList(datacenters: Datacenter[]): string;
7
+ export declare function formatDatacenterDetails(dc: Datacenter): string;
8
+ export declare function formatLocationList(locations: Location[]): string;
9
+ export declare function formatLocationDetails(loc: Location): string;
10
+ export declare function formatServerTypeList(types: ServerType[]): string;
11
+ export declare function formatServerTypeDetails(st: ServerType): string;
12
+ export declare function formatLoadBalancerTypeList(types: LoadBalancerType[]): string;
13
+ export declare function formatLoadBalancerTypeDetails(lbt: LoadBalancerType): string;
14
+ export declare function formatIsoList(isos: ISO[]): string;
15
+ export declare function formatIsoDetails(iso: ISO): string;
16
+ export declare function formatCloudServerList(servers: CloudServer[]): string;
17
+ export declare function formatCloudServerDetails(srv: CloudServer): string;
18
+ export declare function formatNetworkList(networks: Network[]): string;
19
+ export declare function formatNetworkDetails(net: Network): string;
20
+ export declare function formatCloudFirewallList(firewalls: CloudFirewall[]): string;
21
+ export declare function formatCloudFirewallDetails(fw: CloudFirewall): string;
22
+ export declare function formatFloatingIpList(ips: FloatingIp[]): string;
23
+ export declare function formatFloatingIpDetails(ip: FloatingIp): string;
24
+ export declare function formatPrimaryIpList(ips: PrimaryIp[]): string;
25
+ export declare function formatPrimaryIpDetails(ip: PrimaryIp): string;
26
+ export declare function formatVolumeList(volumes: Volume[]): string;
27
+ export declare function formatVolumeDetails(vol: Volume): string;
28
+ export declare function formatLoadBalancerList(lbs: LoadBalancer[]): string;
29
+ export declare function formatLoadBalancerDetails(lb: LoadBalancer): string;
30
+ export declare function formatImageList(images: Image[]): string;
31
+ export declare function formatImageDetails(img: Image): string;
32
+ export declare function formatCloudSshKeyList(keys: CloudSshKey[]): string;
33
+ export declare function formatCloudSshKeyDetails(key: CloudSshKey): string;
34
+ export declare function formatCertificateList(certs: Certificate[]): string;
35
+ export declare function formatCertificateDetails(cert: Certificate): string;
36
+ export declare function formatPlacementGroupList(groups: PlacementGroup[]): string;
37
+ export declare function formatPlacementGroupDetails(pg: PlacementGroup): string;