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,352 @@
1
+ export interface Server {
2
+ server_ip: string;
3
+ server_ipv6_net: string;
4
+ server_number: number;
5
+ server_name: string;
6
+ product: string;
7
+ dc: string;
8
+ traffic: string;
9
+ status: 'ready' | 'installing' | 'maintenance';
10
+ cancelled: boolean;
11
+ paid_until: string;
12
+ ip: string[];
13
+ subnet: ServerSubnet[];
14
+ }
15
+ export interface ServerSubnet {
16
+ ip: string;
17
+ mask: string;
18
+ }
19
+ export interface ServerDetails extends Server {
20
+ reset: boolean;
21
+ rescue: boolean;
22
+ vnc: boolean;
23
+ windows: boolean;
24
+ plesk: boolean;
25
+ cpanel: boolean;
26
+ wol: boolean;
27
+ hot_swap: boolean;
28
+ }
29
+ export interface Cancellation {
30
+ server_ip: string;
31
+ server_ipv6_net: string;
32
+ server_number: number;
33
+ server_name: string;
34
+ earliest_cancellation_date: string;
35
+ cancelled: boolean;
36
+ cancellation_date: string | null;
37
+ cancellation_reason: string[] | null;
38
+ }
39
+ export type ResetType = 'sw' | 'hw' | 'man' | 'power' | 'power_long';
40
+ export interface Reset {
41
+ server_ip: string;
42
+ server_ipv6_net: string;
43
+ server_number: number;
44
+ type: ResetType[];
45
+ operating_status: string;
46
+ }
47
+ export interface BootConfig {
48
+ rescue: RescueConfig | null;
49
+ linux: LinuxConfig | null;
50
+ vnc: VncConfig | null;
51
+ windows: WindowsConfig | null;
52
+ plesk: PleskConfig | null;
53
+ cpanel: CpanelConfig | null;
54
+ }
55
+ interface BaseBootConfig {
56
+ server_ip: string;
57
+ server_ipv6_net: string;
58
+ server_number: number;
59
+ active: boolean;
60
+ password: string | null;
61
+ }
62
+ export interface RescueConfig extends BaseBootConfig {
63
+ os: string[];
64
+ arch: number[];
65
+ authorized_key: string[];
66
+ host_key: string[];
67
+ }
68
+ export interface LinuxConfig extends BaseBootConfig {
69
+ dist: string[];
70
+ arch: number[];
71
+ lang: string[];
72
+ authorized_key: string[];
73
+ host_key: string[];
74
+ }
75
+ export interface VncConfig extends BaseBootConfig {
76
+ dist: string[];
77
+ arch: number[];
78
+ lang: string[];
79
+ }
80
+ export interface WindowsConfig extends BaseBootConfig {
81
+ dist: string[];
82
+ lang: string[];
83
+ }
84
+ export interface PleskConfig extends BaseBootConfig {
85
+ dist: string[];
86
+ arch: number[];
87
+ lang: string[];
88
+ hostname: string | null;
89
+ }
90
+ export interface CpanelConfig extends BaseBootConfig {
91
+ dist: string[];
92
+ arch: number[];
93
+ lang: string[];
94
+ hostname: string | null;
95
+ }
96
+ export interface IP {
97
+ ip: string;
98
+ server_ip: string;
99
+ server_number: number;
100
+ locked: boolean;
101
+ separate_mac: string | null;
102
+ traffic_warnings: boolean;
103
+ traffic_hourly: number;
104
+ traffic_daily: number;
105
+ traffic_monthly: number;
106
+ }
107
+ export interface Mac {
108
+ ip: string;
109
+ mac: string;
110
+ }
111
+ export interface Subnet {
112
+ ip: string;
113
+ mask: string;
114
+ gateway: string;
115
+ server_ip: string;
116
+ server_number: number;
117
+ failover: boolean;
118
+ locked: boolean;
119
+ traffic_warnings: boolean;
120
+ traffic_hourly: number;
121
+ traffic_daily: number;
122
+ traffic_monthly: number;
123
+ }
124
+ export interface Failover {
125
+ ip: string;
126
+ netmask: string;
127
+ server_ip: string;
128
+ server_number: number;
129
+ active_server_ip: string;
130
+ }
131
+ export interface Rdns {
132
+ ip: string;
133
+ ptr: string;
134
+ }
135
+ export interface SshKey {
136
+ name: string;
137
+ fingerprint: string;
138
+ type: string;
139
+ size: number;
140
+ data: string;
141
+ }
142
+ export interface Firewall {
143
+ server_ip: string;
144
+ server_number: number;
145
+ status: 'active' | 'disabled' | 'in process';
146
+ filter_ipv6: boolean;
147
+ whitelist_hos: boolean;
148
+ port: 'main' | 'kvm';
149
+ rules: {
150
+ input: FirewallRule[];
151
+ output?: FirewallRule[];
152
+ };
153
+ }
154
+ export interface FirewallRule {
155
+ ip_version: string;
156
+ name: string;
157
+ dst_ip: string | null;
158
+ dst_port: string | null;
159
+ src_ip: string | null;
160
+ src_port: string | null;
161
+ protocol: string | null;
162
+ tcp_flags: string | null;
163
+ action: 'accept' | 'discard';
164
+ }
165
+ export interface FirewallTemplate {
166
+ id: number;
167
+ name: string;
168
+ filter_ipv6: boolean;
169
+ whitelist_hos: boolean;
170
+ is_default: boolean;
171
+ rules: {
172
+ input: FirewallRule[];
173
+ output?: FirewallRule[];
174
+ };
175
+ }
176
+ export interface VSwitch {
177
+ id: number;
178
+ name: string;
179
+ vlan: number;
180
+ cancelled: boolean;
181
+ server: VSwitchServer[];
182
+ subnet: VSwitchSubnet[];
183
+ cloud_network: VSwitchCloudNetwork[];
184
+ }
185
+ export interface VSwitchServer {
186
+ server_ip: string;
187
+ server_ipv6_net: string;
188
+ server_number: number;
189
+ status: 'ready' | 'in process' | 'failed';
190
+ }
191
+ export interface VSwitchSubnet {
192
+ ip: string;
193
+ mask: number;
194
+ gateway: string;
195
+ }
196
+ export interface VSwitchCloudNetwork {
197
+ id: number;
198
+ ip: string;
199
+ mask: number;
200
+ gateway: string;
201
+ }
202
+ export interface StorageBox {
203
+ id: number;
204
+ login: string;
205
+ name: string;
206
+ product: string;
207
+ cancelled: boolean;
208
+ locked: boolean;
209
+ location: string;
210
+ linked_server: number | null;
211
+ paid_until: string;
212
+ disk_quota: number;
213
+ disk_usage: number;
214
+ disk_usage_data: number;
215
+ disk_usage_snapshots: number;
216
+ webdav: boolean;
217
+ samba: boolean;
218
+ ssh: boolean;
219
+ external_reachability: boolean;
220
+ zfs: boolean;
221
+ server: string;
222
+ host_system: string;
223
+ }
224
+ export interface StorageBoxSnapshot {
225
+ name: string;
226
+ timestamp: string;
227
+ size: number;
228
+ size_formatted: string;
229
+ }
230
+ export interface StorageBoxSnapshotPlan {
231
+ status: 'enabled' | 'disabled';
232
+ minute: number;
233
+ hour: number;
234
+ day_of_week: number;
235
+ day_of_month: number;
236
+ max_snapshots: number;
237
+ }
238
+ export interface StorageBoxSubaccount {
239
+ username: string;
240
+ accountid: string;
241
+ server: string;
242
+ homedirectory: string;
243
+ samba: boolean;
244
+ ssh: boolean;
245
+ external_reachability: boolean;
246
+ webdav: boolean;
247
+ readonly: boolean;
248
+ createtime: string;
249
+ comment: string;
250
+ }
251
+ export interface Traffic {
252
+ ip: string;
253
+ type: 'day' | 'month' | 'year';
254
+ from: string;
255
+ to: string;
256
+ data: TrafficData[];
257
+ }
258
+ export interface TrafficData {
259
+ in: number;
260
+ out: number;
261
+ sum: number;
262
+ date?: string;
263
+ }
264
+ export interface Wol {
265
+ server_ip: string;
266
+ server_ipv6_net: string;
267
+ server_number: number;
268
+ }
269
+ export interface ServerProduct {
270
+ id: string;
271
+ name: string;
272
+ description: string[];
273
+ traffic: string;
274
+ dist: string[];
275
+ arch: number[];
276
+ lang: string[];
277
+ location: string[];
278
+ prices: ProductPrice[];
279
+ orderable_addons: string[];
280
+ }
281
+ export interface ProductPrice {
282
+ location: string;
283
+ price: {
284
+ net: string;
285
+ gross: string;
286
+ };
287
+ price_setup: {
288
+ net: string;
289
+ gross: string;
290
+ };
291
+ price_vat: {
292
+ net: string;
293
+ gross: string;
294
+ };
295
+ price_setup_vat: {
296
+ net: string;
297
+ gross: string;
298
+ };
299
+ }
300
+ export interface ServerMarketProduct {
301
+ id: number;
302
+ name: string;
303
+ description: string[];
304
+ traffic: string;
305
+ dist: string[];
306
+ arch: number[];
307
+ lang: string[];
308
+ cpu: string;
309
+ cpu_benchmark: number;
310
+ memory_size: number;
311
+ hdd_size: number;
312
+ hdd_text: string;
313
+ hdd_count: number;
314
+ datacenter: string;
315
+ network_speed: string;
316
+ price: string;
317
+ price_setup: string;
318
+ fixed_price: boolean;
319
+ next_reduce: number;
320
+ next_reduce_date: string;
321
+ orderable_addons: string[];
322
+ }
323
+ export interface ServerTransaction {
324
+ id: string;
325
+ date: string;
326
+ status: 'ready' | 'in process' | 'cancelled';
327
+ server_number: number | null;
328
+ server_ip: string | null;
329
+ authorized_key: string[];
330
+ host_key: string[];
331
+ comment: string;
332
+ product: ServerTransactionProduct;
333
+ }
334
+ export interface ServerTransactionProduct {
335
+ id: string;
336
+ name: string;
337
+ description: string[];
338
+ traffic: string;
339
+ dist: string;
340
+ arch: number;
341
+ lang: string;
342
+ location: string;
343
+ }
344
+ export type ApiResponse<T> = Record<string, T>;
345
+ export interface ApiError {
346
+ error: {
347
+ status: number;
348
+ code: string;
349
+ message: string;
350
+ };
351
+ }
352
+ export {};
@@ -0,0 +1,5 @@
1
+ // ============================================================================
2
+ // Hetzner Robot API Types
3
+ // Based on: https://robot.hetzner.com/doc/webservice/en.html
4
+ // ============================================================================
5
+ export {};
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Check if system keychain is available
3
+ */
4
+ export declare function hasKeychainSupport(): Promise<boolean>;
5
+ /**
6
+ * Get credentials from system keychain
7
+ */
8
+ export declare function getKeychainCredentials(): Promise<{
9
+ user: string;
10
+ password: string;
11
+ } | null>;
12
+ /**
13
+ * Save credentials to system keychain
14
+ * @returns true if saved successfully, false otherwise
15
+ */
16
+ export declare function saveToKeychain(user: string, password: string): Promise<boolean>;
17
+ /**
18
+ * Clear credentials from system keychain
19
+ */
20
+ export declare function clearKeychain(): Promise<void>;
21
+ export type CredentialSource = 'environment' | 'keychain' | 'file' | null;
22
+ export interface Config {
23
+ user?: string;
24
+ password?: string;
25
+ }
26
+ /**
27
+ * Load configuration from file
28
+ */
29
+ export declare function loadConfig(): Config;
30
+ /**
31
+ * Save configuration to file
32
+ */
33
+ export declare function saveConfig(cfg: Config): void;
34
+ /**
35
+ * Clear saved configuration from file
36
+ */
37
+ export declare function clearConfigFile(): void;
38
+ /**
39
+ * Clear saved configuration from all storage locations
40
+ */
41
+ export declare function clearConfig(): Promise<void>;
42
+ /**
43
+ * Get credentials from environment variables only (sync)
44
+ */
45
+ export declare function getCredentialsFromEnv(): {
46
+ user: string;
47
+ password: string;
48
+ } | null;
49
+ /**
50
+ * Get credentials from config file only (sync)
51
+ */
52
+ export declare function getCredentialsFromFile(): {
53
+ user: string;
54
+ password: string;
55
+ } | null;
56
+ /**
57
+ * Get credentials from environment variables, keychain, or config file
58
+ * Priority: env vars → keychain → config file
59
+ */
60
+ export declare function getCredentials(): Promise<{
61
+ user: string;
62
+ password: string;
63
+ source: CredentialSource;
64
+ } | null>;
65
+ /**
66
+ * Check if credentials are configured (sync check - env and file only)
67
+ */
68
+ export declare function hasCredentialsSync(): boolean;
69
+ /**
70
+ * Check if credentials are configured (async - includes keychain)
71
+ */
72
+ export declare function hasCredentials(): Promise<boolean>;
73
+ /**
74
+ * Interactive login prompt
75
+ */
76
+ export declare function promptLogin(): Promise<{
77
+ user: string;
78
+ password: string;
79
+ }>;
80
+ /**
81
+ * Get credentials, prompting if necessary
82
+ */
83
+ export declare function requireCredentials(): Promise<{
84
+ user: string;
85
+ password: string;
86
+ }>;
@@ -0,0 +1,273 @@
1
+ import { config } from 'dotenv';
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { input, password as passwordPrompt, confirm } from '@inquirer/prompts';
6
+ config();
7
+ const CONFIG_DIR = join(homedir(), '.hetzner-cli');
8
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
9
+ const KEYCHAIN_SERVICE = 'hetzner-cli';
10
+ const KEYCHAIN_ACCOUNT = 'robot-api';
11
+ // Lazy-loaded keytar module (optional dependency with native bindings)
12
+ let keytarModule = null;
13
+ let keytarLoadAttempted = false;
14
+ /**
15
+ * Try to load keytar module (may fail if native deps unavailable)
16
+ */
17
+ async function getKeytar() {
18
+ if (keytarLoadAttempted)
19
+ return keytarModule;
20
+ keytarLoadAttempted = true;
21
+ try {
22
+ keytarModule = await import('keytar');
23
+ return keytarModule;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ /**
30
+ * Check if system keychain is available
31
+ */
32
+ export async function hasKeychainSupport() {
33
+ const keytar = await getKeytar();
34
+ if (!keytar)
35
+ return false;
36
+ try {
37
+ // Try a read operation to verify keychain access
38
+ await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ /**
46
+ * Get credentials from system keychain
47
+ */
48
+ export async function getKeychainCredentials() {
49
+ const keytar = await getKeytar();
50
+ if (!keytar)
51
+ return null;
52
+ try {
53
+ const stored = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
54
+ if (!stored)
55
+ return null;
56
+ const parsed = JSON.parse(stored);
57
+ if (parsed.user && parsed.password) {
58
+ return { user: parsed.user, password: parsed.password };
59
+ }
60
+ return null;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ /**
67
+ * Save credentials to system keychain
68
+ * @returns true if saved successfully, false otherwise
69
+ */
70
+ export async function saveToKeychain(user, password) {
71
+ const keytar = await getKeytar();
72
+ if (!keytar)
73
+ return false;
74
+ try {
75
+ await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, JSON.stringify({ user, password }));
76
+ return true;
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
82
+ /**
83
+ * Clear credentials from system keychain
84
+ */
85
+ export async function clearKeychain() {
86
+ const keytar = await getKeytar();
87
+ if (!keytar)
88
+ return;
89
+ try {
90
+ await keytar.deletePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
91
+ }
92
+ catch {
93
+ // Ignore errors when clearing
94
+ }
95
+ }
96
+ /**
97
+ * Ensure config directory exists
98
+ */
99
+ function ensureConfigDir() {
100
+ if (!existsSync(CONFIG_DIR)) {
101
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
102
+ }
103
+ }
104
+ /**
105
+ * Load configuration from file
106
+ */
107
+ export function loadConfig() {
108
+ if (!existsSync(CONFIG_FILE)) {
109
+ return {};
110
+ }
111
+ try {
112
+ const data = readFileSync(CONFIG_FILE, 'utf-8');
113
+ return JSON.parse(data);
114
+ }
115
+ catch {
116
+ return {};
117
+ }
118
+ }
119
+ /**
120
+ * Save configuration to file
121
+ */
122
+ export function saveConfig(cfg) {
123
+ ensureConfigDir();
124
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
125
+ }
126
+ /**
127
+ * Clear saved configuration from file
128
+ */
129
+ export function clearConfigFile() {
130
+ if (existsSync(CONFIG_FILE)) {
131
+ writeFileSync(CONFIG_FILE, '{}', { mode: 0o600 });
132
+ }
133
+ }
134
+ /**
135
+ * Clear saved configuration from all storage locations
136
+ */
137
+ export async function clearConfig() {
138
+ await clearKeychain();
139
+ clearConfigFile();
140
+ }
141
+ /**
142
+ * Get credentials from environment variables only (sync)
143
+ */
144
+ export function getCredentialsFromEnv() {
145
+ const envUser = process.env.HETZNER_ROBOT_USER;
146
+ const envPassword = process.env.HETZNER_ROBOT_PASSWORD;
147
+ if (envUser && envPassword) {
148
+ return { user: envUser, password: envPassword };
149
+ }
150
+ return null;
151
+ }
152
+ /**
153
+ * Get credentials from config file only (sync)
154
+ */
155
+ export function getCredentialsFromFile() {
156
+ const cfg = loadConfig();
157
+ if (cfg.user && cfg.password) {
158
+ return { user: cfg.user, password: cfg.password };
159
+ }
160
+ return null;
161
+ }
162
+ /**
163
+ * Get credentials from environment variables, keychain, or config file
164
+ * Priority: env vars → keychain → config file
165
+ */
166
+ export async function getCredentials() {
167
+ // First try environment variables
168
+ const envCreds = getCredentialsFromEnv();
169
+ if (envCreds) {
170
+ return { ...envCreds, source: 'environment' };
171
+ }
172
+ // Then try keychain
173
+ const keychainCreds = await getKeychainCredentials();
174
+ if (keychainCreds) {
175
+ return { ...keychainCreds, source: 'keychain' };
176
+ }
177
+ // Finally try config file
178
+ const fileCreds = getCredentialsFromFile();
179
+ if (fileCreds) {
180
+ return { ...fileCreds, source: 'file' };
181
+ }
182
+ return null;
183
+ }
184
+ /**
185
+ * Check if credentials are configured (sync check - env and file only)
186
+ */
187
+ export function hasCredentialsSync() {
188
+ return getCredentialsFromEnv() !== null || getCredentialsFromFile() !== null;
189
+ }
190
+ /**
191
+ * Check if credentials are configured (async - includes keychain)
192
+ */
193
+ export async function hasCredentials() {
194
+ return (await getCredentials()) !== null;
195
+ }
196
+ /**
197
+ * Interactive login prompt
198
+ */
199
+ export async function promptLogin() {
200
+ console.log('');
201
+ console.log('Hetzner Robot API Authentication');
202
+ console.log('─'.repeat(40));
203
+ console.log('');
204
+ console.log('To get your API credentials:');
205
+ console.log('1. Go to https://robot.hetzner.com');
206
+ console.log('2. Navigate to: Settings > Web service settings');
207
+ console.log('3. Create a new web service user');
208
+ console.log('');
209
+ console.log('Note: This is separate from your main Hetzner login.');
210
+ console.log('');
211
+ // Check if we should offer migration from file to keychain
212
+ const keychainAvailable = await hasKeychainSupport();
213
+ const existingFileCreds = getCredentialsFromFile();
214
+ if (keychainAvailable && existingFileCreds) {
215
+ const migrate = await confirm({
216
+ message: 'Migrate existing credentials to secure keychain storage?',
217
+ default: true,
218
+ });
219
+ if (migrate) {
220
+ const saved = await saveToKeychain(existingFileCreds.user, existingFileCreds.password);
221
+ if (saved) {
222
+ clearConfigFile();
223
+ console.log('');
224
+ console.log('Credentials migrated to keychain.');
225
+ return existingFileCreds;
226
+ }
227
+ }
228
+ }
229
+ const user = await input({
230
+ message: 'Web service username:',
231
+ validate: (v) => v.length > 0 || 'Username is required',
232
+ });
233
+ const password = await passwordPrompt({
234
+ message: 'Web service password:',
235
+ validate: (v) => v.length > 0 || 'Password is required',
236
+ });
237
+ // Determine storage location based on keychain availability
238
+ const storageMessage = keychainAvailable
239
+ ? 'Save credentials to secure keychain?'
240
+ : 'Save credentials to ~/.hetzner-cli/config.json?';
241
+ const save = await confirm({
242
+ message: storageMessage,
243
+ default: true,
244
+ });
245
+ if (save) {
246
+ let savedToKeychain = false;
247
+ if (keychainAvailable) {
248
+ savedToKeychain = await saveToKeychain(user, password);
249
+ }
250
+ if (!savedToKeychain) {
251
+ // Fall back to file storage
252
+ saveConfig({ user, password });
253
+ console.log('');
254
+ console.warn('Warning: System keychain unavailable. Credentials stored in plaintext at ~/.hetzner-cli/config.json');
255
+ console.log('Credentials saved to config file.');
256
+ }
257
+ else {
258
+ console.log('');
259
+ console.log('Credentials saved to keychain.');
260
+ }
261
+ }
262
+ return { user, password };
263
+ }
264
+ /**
265
+ * Get credentials, prompting if necessary
266
+ */
267
+ export async function requireCredentials() {
268
+ const creds = await getCredentials();
269
+ if (creds) {
270
+ return { user: creds.user, password: creds.password };
271
+ }
272
+ return promptLogin();
273
+ }