@rollupdate/hctl 0.0.0-stage → 3.1.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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1071 -2
  3. package/dist/auction/cache.d.ts +6 -0
  4. package/dist/auction/cache.js +55 -0
  5. package/dist/auction/client.d.ts +24 -0
  6. package/dist/auction/client.js +236 -0
  7. package/dist/auction/commands.d.ts +2 -0
  8. package/dist/auction/commands.js +440 -0
  9. package/dist/auction/diff-formatter.d.ts +3 -0
  10. package/dist/auction/diff-formatter.js +61 -0
  11. package/dist/auction/diff.d.ts +11 -0
  12. package/dist/auction/diff.js +28 -0
  13. package/dist/auction/formatter.d.ts +21 -0
  14. package/dist/auction/formatter.js +182 -0
  15. package/dist/auction/store.d.ts +24 -0
  16. package/dist/auction/store.js +177 -0
  17. package/dist/auction/types.d.ts +74 -0
  18. package/dist/auction/types.js +5 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +74 -0
  21. package/dist/client.d.ts +2 -0
  22. package/dist/client.js +4 -0
  23. package/dist/cloud/browser-bootstrap.d.ts +28 -0
  24. package/dist/cloud/browser-bootstrap.js +480 -0
  25. package/dist/cloud/client.d.ts +520 -0
  26. package/dist/cloud/client.js +755 -0
  27. package/dist/cloud/commands/certificate.d.ts +2 -0
  28. package/dist/cloud/commands/certificate.js +108 -0
  29. package/dist/cloud/commands/context.d.ts +2 -0
  30. package/dist/cloud/commands/context.js +133 -0
  31. package/dist/cloud/commands/datacenter.d.ts +2 -0
  32. package/dist/cloud/commands/datacenter.js +24 -0
  33. package/dist/cloud/commands/firewall.d.ts +2 -0
  34. package/dist/cloud/commands/firewall.js +142 -0
  35. package/dist/cloud/commands/floating-ip.d.ts +2 -0
  36. package/dist/cloud/commands/floating-ip.js +138 -0
  37. package/dist/cloud/commands/image.d.ts +2 -0
  38. package/dist/cloud/commands/image.js +98 -0
  39. package/dist/cloud/commands/index.d.ts +2 -0
  40. package/dist/cloud/commands/index.js +47 -0
  41. package/dist/cloud/commands/inventory.d.ts +2 -0
  42. package/dist/cloud/commands/inventory.js +13 -0
  43. package/dist/cloud/commands/iso.d.ts +2 -0
  44. package/dist/cloud/commands/iso.js +27 -0
  45. package/dist/cloud/commands/load-balancer-type.d.ts +2 -0
  46. package/dist/cloud/commands/load-balancer-type.js +24 -0
  47. package/dist/cloud/commands/load-balancer.d.ts +2 -0
  48. package/dist/cloud/commands/load-balancer.js +269 -0
  49. package/dist/cloud/commands/location.d.ts +2 -0
  50. package/dist/cloud/commands/location.js +24 -0
  51. package/dist/cloud/commands/network.d.ts +2 -0
  52. package/dist/cloud/commands/network.js +157 -0
  53. package/dist/cloud/commands/placement-group.d.ts +2 -0
  54. package/dist/cloud/commands/placement-group.js +79 -0
  55. package/dist/cloud/commands/pricing.d.ts +2 -0
  56. package/dist/cloud/commands/pricing.js +15 -0
  57. package/dist/cloud/commands/primary-ip.d.ts +2 -0
  58. package/dist/cloud/commands/primary-ip.js +139 -0
  59. package/dist/cloud/commands/server-type.d.ts +2 -0
  60. package/dist/cloud/commands/server-type.js +24 -0
  61. package/dist/cloud/commands/server.d.ts +2 -0
  62. package/dist/cloud/commands/server.js +346 -0
  63. package/dist/cloud/commands/ssh-key.d.ts +2 -0
  64. package/dist/cloud/commands/ssh-key.js +94 -0
  65. package/dist/cloud/commands/volume.d.ts +2 -0
  66. package/dist/cloud/commands/volume.js +138 -0
  67. package/dist/cloud/context.d.ts +34 -0
  68. package/dist/cloud/context.js +215 -0
  69. package/dist/cloud/formatter.d.ts +40 -0
  70. package/dist/cloud/formatter.js +824 -0
  71. package/dist/cloud/helpers.d.ts +27 -0
  72. package/dist/cloud/helpers.js +67 -0
  73. package/dist/cloud/inventory.d.ts +100 -0
  74. package/dist/cloud/inventory.js +229 -0
  75. package/dist/cloud/pricing.d.ts +27 -0
  76. package/dist/cloud/pricing.js +103 -0
  77. package/dist/cloud/token-instructions.d.ts +2 -0
  78. package/dist/cloud/token-instructions.js +21 -0
  79. package/dist/cloud/types.d.ts +437 -0
  80. package/dist/cloud/types.js +5 -0
  81. package/dist/config.d.ts +1 -0
  82. package/dist/config.js +2 -0
  83. package/dist/formatter.d.ts +3 -0
  84. package/dist/formatter.js +6 -0
  85. package/dist/index.d.ts +12 -0
  86. package/dist/index.js +21 -0
  87. package/dist/robot/client.d.ts +256 -0
  88. package/dist/robot/client.js +745 -0
  89. package/dist/robot/commands/auth.d.ts +2 -0
  90. package/dist/robot/commands/auth.js +54 -0
  91. package/dist/robot/commands/boot.d.ts +2 -0
  92. package/dist/robot/commands/boot.js +76 -0
  93. package/dist/robot/commands/cancel.d.ts +2 -0
  94. package/dist/robot/commands/cancel.js +37 -0
  95. package/dist/robot/commands/failover.d.ts +2 -0
  96. package/dist/robot/commands/failover.js +46 -0
  97. package/dist/robot/commands/firewall.d.ts +2 -0
  98. package/dist/robot/commands/firewall.js +72 -0
  99. package/dist/robot/commands/index.d.ts +2 -0
  100. package/dist/robot/commands/index.js +36 -0
  101. package/dist/robot/commands/interactive.d.ts +2 -0
  102. package/dist/robot/commands/interactive.js +152 -0
  103. package/dist/robot/commands/ip.d.ts +2 -0
  104. package/dist/robot/commands/ip.js +55 -0
  105. package/dist/robot/commands/key.d.ts +2 -0
  106. package/dist/robot/commands/key.js +68 -0
  107. package/dist/robot/commands/order.d.ts +2 -0
  108. package/dist/robot/commands/order.js +33 -0
  109. package/dist/robot/commands/rdns.d.ts +2 -0
  110. package/dist/robot/commands/rdns.js +41 -0
  111. package/dist/robot/commands/reset.d.ts +2 -0
  112. package/dist/robot/commands/reset.js +77 -0
  113. package/dist/robot/commands/server.d.ts +2 -0
  114. package/dist/robot/commands/server.js +32 -0
  115. package/dist/robot/commands/storagebox.d.ts +2 -0
  116. package/dist/robot/commands/storagebox.js +126 -0
  117. package/dist/robot/commands/subnet.d.ts +2 -0
  118. package/dist/robot/commands/subnet.js +24 -0
  119. package/dist/robot/commands/traffic.d.ts +2 -0
  120. package/dist/robot/commands/traffic.js +23 -0
  121. package/dist/robot/commands/vswitch.d.ts +2 -0
  122. package/dist/robot/commands/vswitch.js +81 -0
  123. package/dist/robot/commands/wol.d.ts +2 -0
  124. package/dist/robot/commands/wol.js +20 -0
  125. package/dist/robot/formatter.d.ts +57 -0
  126. package/dist/robot/formatter.js +582 -0
  127. package/dist/robot/types.d.ts +359 -0
  128. package/dist/robot/types.js +5 -0
  129. package/dist/shared/config.d.ts +86 -0
  130. package/dist/shared/config.js +289 -0
  131. package/dist/shared/formatter.d.ts +29 -0
  132. package/dist/shared/formatter.js +121 -0
  133. package/dist/shared/helpers.d.ts +39 -0
  134. package/dist/shared/helpers.js +86 -0
  135. package/dist/shared/init.d.ts +2 -0
  136. package/dist/shared/init.js +107 -0
  137. package/dist/shared/reference.d.ts +5 -0
  138. package/dist/shared/reference.js +689 -0
  139. package/dist/shared/schemas.d.ts +1 -0
  140. package/dist/shared/schemas.js +1031 -0
  141. package/dist/types.d.ts +2 -0
  142. package/dist/types.js +2 -0
  143. package/package.json +127 -4
package/README.md CHANGED
@@ -1,3 +1,1072 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="logo.svg" alt="hctl logo" width="128" height="128">
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ # hctl
6
+
7
+ [![npm version](https://badge.fury.io/js/%40rollupdate%2Fhctl.svg)](https://www.npmjs.com/package/@rollupdate/hctl)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ **[Website](https://hctl.dev/)** · **[npm](https://www.npmjs.com/package/@rollupdate/hctl)** · **[GitHub](https://github.com/p8-public/hetzner-cli)**
11
+
12
+ Unified CLI and Node.js library for Hetzner's three APIs:
13
+
14
+ - **[Robot API](https://robot.hetzner.com)** — dedicated server management (servers, IPs, firewall, storage boxes, etc.)
15
+ - **[Cloud API](https://console.hetzner.cloud)** — cloud infrastructure (servers, networks, volumes, load balancers, etc.)
16
+ - **[Server Auction](https://www.hetzner.com/sb/)** — public server auction browser with rich filtering (no auth required)
17
+
18
+ > **Note:** This is an unofficial, community-built CLI. The official Hetzner Cloud CLI is [`hcloud`](https://github.com/hetznercloud/cli). See [how they differ](#comparison-with-official-cli).
19
+
20
+ ---
21
+
22
+ ## Table of Contents
23
+
24
+ - [Comparison with Official CLI](#comparison-with-official-cli)
25
+ - [Installation](#installation)
26
+ - [Quick Start](#quick-start)
27
+ - [Authentication](#authentication)
28
+ - [Robot API Credentials](#robot-api-credentials)
29
+ - [Cloud API Token](#cloud-api-token)
30
+ - [Auction API](#auction-api-no-auth)
31
+ - [CLI Reference](#cli-reference)
32
+ - [Global Options](#global-options)
33
+ - [Robot Commands](#robot-commands)
34
+ - [Cloud Commands](#cloud-commands)
35
+ - [Auction Commands](#auction-commands)
36
+ - [Built-in Reference](#built-in-reference)
37
+ - [Library Usage](#library-usage)
38
+ - [Robot Client](#robot-client)
39
+ - [Cloud Client](#cloud-client)
40
+ - [Auction Client](#auction-client)
41
+ - [Type Exports](#type-exports)
42
+ - [API Reference](#api-reference)
43
+ - [Configuration Files](#configuration-files)
44
+ - [Development](#development)
45
+ - [License](#license)
46
+
47
+ ---
48
+
49
+ ## Comparison with Official CLI
50
+
51
+ Hetzner maintains an official Cloud CLI called [`hcloud`](https://github.com/hetznercloud/cli), written in Go. This project (`hctl`) is a separate, community-built tool. Here's how they compare:
52
+
53
+ | Feature | [`hcloud`](https://github.com/hetznercloud/cli) (official) | `hctl` (this project) |
54
+ |---|---|---|
55
+ | **API coverage** | Cloud only | Cloud + Robot + Server Auction |
56
+ | **Language** | Go (single binary) | TypeScript / Node.js |
57
+ | **Install** | Homebrew, apt, binary download | `npm install -g @rollupdate/hctl` |
58
+ | **Library usage** | CLI only | Also usable as a Node.js library (`import { HetznerRobotClient } from '@rollupdate/hctl'`) |
59
+ | **Interactive mode** | No | Yes (`hctl interactive`) |
60
+ | **LLM reference** | No | Yes (`hctl reference` — structured output for LLM context) |
61
+
62
+ **When to use `hcloud`:** You only need Cloud API access and prefer a native binary with no runtime dependency.
63
+
64
+ **When to use `hctl`:** You manage dedicated servers (Robot), browse the server auction, or want a single tool that covers all three Hetzner APIs. Also useful if you need a Node.js library for automation scripts.
65
+
66
+ ---
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ # Global CLI
72
+ npm install -g @rollupdate/hctl
73
+
74
+ # Or as a project dependency
75
+ npm install @rollupdate/hctl
76
+ ```
77
+
78
+ Requires Node.js 20.17+, 22.13+, or 23.5+.
79
+
80
+ ---
81
+
82
+ ## Quick Start
83
+
84
+ ```bash
85
+ # First-time setup wizard for Robot + Cloud credentials
86
+ hctl init
87
+
88
+ # List your dedicated servers
89
+ hctl server list
90
+
91
+ # List your cloud servers
92
+ hctl cloud server list
93
+
94
+ # Browse the server auction (no login needed)
95
+ hctl auction list --max-price 60 --ecc --disk-type nvme --sort price
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Authentication
101
+
102
+ ### Robot API Credentials
103
+
104
+ The Robot API uses a **web service username and password** (separate from your main Hetzner login).
105
+ These credentials are created in the Robot UI at [robot.hetzner.com](https://robot.hetzner.com), not in the Hetzner Cloud Console.
106
+
107
+ **To create credentials:**
108
+
109
+ 1. Go to [robot.hetzner.com](https://robot.hetzner.com)
110
+ 2. Navigate to **Settings > Web service settings**
111
+ 3. Create a new web service user
112
+
113
+ **Credential resolution order** (first match wins):
114
+
115
+ | Priority | Method | Details |
116
+ |----------|--------|---------|
117
+ | 1 | CLI flags | `--user <username> --password <password>` |
118
+ | 2 | Environment variables | `HETZNER_ROBOT_USER` and `HETZNER_ROBOT_PASSWORD` |
119
+ | 3 | System keychain | Stored by `hctl auth login` (uses native keytar) |
120
+ | 4 | Config file | `~/.hctl/config.json` |
121
+ | 5 | Interactive prompt | Asks for username/password at runtime |
122
+
123
+ **Secure password passing** (keeps password out of shell history):
124
+
125
+ ```bash
126
+ # From stdin
127
+ echo "$PASSWORD" | hctl server list -u myuser -p -
128
+
129
+ # With 1Password
130
+ op read "op://vault/item/password" | hctl server list -u $(op read "op://vault/item/user") -p -
131
+
132
+ # From environment
133
+ export HETZNER_ROBOT_USER=myuser
134
+ export HETZNER_ROBOT_PASSWORD=mypassword
135
+ hctl server list
136
+ ```
137
+
138
+ ### Cloud API Token
139
+
140
+ The Cloud API uses a **bearer token** from the Hetzner Cloud Console.
141
+ Cloud tokens are project-scoped and are created in the Cloud Console at [console.hetzner.cloud](https://console.hetzner.cloud), not in the Robot UI.
142
+
143
+ **To create a token:**
144
+
145
+ 1. Go to [console.hetzner.cloud](https://console.hetzner.cloud)
146
+ 2. Select your project, or create one
147
+ 3. In the left menu, open **Security**
148
+ 4. In the top menu, open **API Tokens**
149
+ 5. Click **Generate API Token**
150
+ 6. Name the token, for example `hctl`
151
+ 7. Choose **Read & Write** for full `hctl` management, or **Read** for list/describe-only usage
152
+ 8. Copy the token immediately; Hetzner only shows it once
153
+
154
+ **Token resolution order:**
155
+
156
+ | Priority | Method | Details |
157
+ |----------|--------|---------|
158
+ | 1 | CLI flag | `--token <token>` per command |
159
+ | 2 | Environment variable | `HETZNER_CLOUD_TOKEN` |
160
+ | 3 | Active context | Set via `hctl cloud context use <name>` |
161
+
162
+ ```bash
163
+ # Interactive Robot + Cloud setup
164
+ hctl init
165
+
166
+ # Set up a named context (saves token)
167
+ hctl cloud context create production -t hcloud_xxxxxx
168
+
169
+ # Verify the resolved token before provisioning
170
+ hctl cloud context test
171
+
172
+ # Explicit browser-assisted token setup
173
+ hctl cloud context bootstrap placeswhere \
174
+ --project Placeswhere \
175
+ --create-project
176
+
177
+ # Switch contexts
178
+ hctl cloud context use production
179
+
180
+ # Or use inline
181
+ hctl cloud server list --token hcloud_xxxxxx
182
+ ```
183
+
184
+ ### Auction API (No Auth)
185
+
186
+ The auction commands use the hosted 15-minute Cloudflare cache by default. No credentials required, and normal CLI usage does not hit Hetzner's public JSON endpoint directly.
187
+
188
+ ```bash
189
+ hctl auction list
190
+ hctl auction list --direct # bypass the hosted cache and fetch from Hetzner
191
+ ```
192
+
193
+ Hosted JSON files:
194
+
195
+ - `https://auction.hctl.dev/live_data_sb_EUR.json`
196
+ - `https://auction.hctl.dev/live_data_sb_USD.json`
197
+ - `https://auction.hctl.dev/metadata.json`
198
+ - `https://auction.hctl.dev/health.json`
199
+
200
+ If Hetzner's public JSON endpoint is temporarily unavailable, the Cloudflare Worker keeps serving the last good R2 snapshot and records the refresh failure in `metadata.json` and `health.json`. If the cache is empty and Hetzner cannot be reached, the Worker returns `502` with `Retry-After: 300`.
201
+
202
+ The CLI also keeps a local copy of the most recent successful auction response. If your internet connection is unavailable, `hctl auction list` and `hctl auction show` fall back to that local data and mark it as stale. JSON output remains valid JSON on stdout; stale-cache warnings are written to stderr.
203
+
204
+ `hctl auction status --json` is the stable machine-readable way to check freshness before automation. `auction diff` and `auction watch` require a current fetch and will not create change snapshots from stale local fallback data.
205
+
206
+ The Worker stores the current public files in R2 under `latest/` and keeps timestamped copies under `history/YYYY/MM/DD/HHmm/`. Those historical JSON snapshots are intended for later trend analysis in DuckDB or a database.
207
+
208
+ ---
209
+
210
+ ## CLI Reference
211
+
212
+ ### Global Options
213
+
214
+ ```
215
+ -u, --user <username> Robot API username
216
+ -p, --password <password> Robot API password (use "-" to read from stdin)
217
+ --json Output raw JSON instead of formatted tables
218
+ --output-schema Print TypeScript type for --json output and exit
219
+ -V, --version Show version number
220
+ -h, --help Show help for any command
221
+ ```
222
+
223
+ All commands support `--json` for machine-readable output. Pipe to `jq` for filtering:
224
+
225
+ ```bash
226
+ hctl server list --json | jq '.[].server.server_ip'
227
+ hctl auction list --max-price 50 --json | jq '.[].id'
228
+ ```
229
+
230
+ ### Robot Commands
231
+
232
+ Commands for managing Hetzner dedicated servers via the Robot API.
233
+
234
+ #### Authentication
235
+
236
+ ```bash
237
+ hctl auth login # Interactive credential setup
238
+ hctl auth logout # Clear saved credentials
239
+ hctl auth status # Show current auth source
240
+ hctl auth test # Verify credentials work
241
+ ```
242
+
243
+ #### Servers
244
+
245
+ ```bash
246
+ hctl server list # List all servers
247
+ hctl server get <server> # Server details (by ID or IP)
248
+ hctl server rename <server> <name> # Rename a server
249
+ ```
250
+
251
+ #### Reset
252
+
253
+ ```bash
254
+ hctl reset options [server] # Show available reset types
255
+ hctl reset execute <servers...> # Reset servers
256
+ -t, --type <type> # sw|hw|man|power|power_long (default: sw)
257
+ -i, --interactive # Select type interactively
258
+ -y, --yes # Skip confirmation
259
+ ```
260
+
261
+ #### Boot Configuration
262
+
263
+ ```bash
264
+ hctl boot status <server> # Show all boot config
265
+
266
+ # Rescue system
267
+ hctl boot rescue activate <server> [-o linux|linuxold|vkvm] [-a 64|32] [-k <fingerprints...>]
268
+ hctl boot rescue deactivate <server>
269
+ hctl boot rescue last <server> # Show last rescue (includes password)
270
+
271
+ # Linux installation
272
+ hctl boot linux options <server> # Show available distributions
273
+ hctl boot linux activate <server> -d <dist> [-a 64|32] [-l en] [-k <fingerprints...>]
274
+ hctl boot linux deactivate <server>
275
+ ```
276
+
277
+ #### IP & Networking
278
+
279
+ ```bash
280
+ hctl ip list # List all IPs
281
+ hctl ip get <ip> # IP details
282
+ hctl ip update <ip> [--warnings true|false] [--hourly <mb>] [--daily <mb>] [--monthly <gb>]
283
+ hctl ip mac get|generate|delete <ip> # Separate MAC management
284
+
285
+ hctl subnet list # List subnets
286
+ hctl subnet get <subnet>
287
+
288
+ hctl failover list # List failover IPs
289
+ hctl failover get <ip>
290
+ hctl failover switch <failover-ip> <target-server-ip> [-y]
291
+ hctl failover delete <ip> [-y]
292
+
293
+ hctl rdns list # List reverse DNS entries
294
+ hctl rdns get <ip>
295
+ hctl rdns set <ip> <ptr> # Create/update
296
+ hctl rdns delete <ip>
297
+ ```
298
+
299
+ #### SSH Keys
300
+
301
+ ```bash
302
+ hctl key list # List all keys
303
+ hctl key get <fingerprint>
304
+ hctl key add <name> [-f <path>] [-d <data>]
305
+ hctl key rename <fingerprint> <name>
306
+ hctl key delete <fingerprint> [-y]
307
+ ```
308
+
309
+ #### Firewall
310
+
311
+ ```bash
312
+ hctl firewall get <server> # Show config
313
+ hctl firewall enable|disable <server>
314
+ hctl firewall delete <server> [-y] # Delete all rules
315
+
316
+ hctl firewall template list
317
+ hctl firewall template get|delete <id>
318
+ ```
319
+
320
+ #### vSwitch
321
+
322
+ ```bash
323
+ hctl vswitch list
324
+ hctl vswitch get <id>
325
+ hctl vswitch servers [id]
326
+ hctl vswitch create <name> <vlan>
327
+ hctl vswitch update <id> [-n <name>] [-v <vlan>]
328
+ hctl vswitch delete <id> [-y] [--date <YYYY-MM-DD>]
329
+ hctl vswitch add-server <vswitch-id> <server>
330
+ hctl vswitch remove-server <vswitch-id> <server>
331
+ ```
332
+
333
+ #### Storage Box
334
+
335
+ ```bash
336
+ hctl storagebox list # (alias: storage)
337
+ hctl storagebox get <id>
338
+ hctl storagebox update <id> [-n <name>] [--webdav|--samba|--ssh|--external|--zfs true|false]
339
+ hctl storagebox reset-password <id>
340
+
341
+ # Snapshots
342
+ hctl storagebox snapshot list|create|delete|revert <box-id> [<name>]
343
+
344
+ # Subaccounts
345
+ hctl storagebox subaccount list <box-id>
346
+ hctl storagebox subaccount create <box-id> <home-directory> [--samba|--ssh|--webdav|--external|--readonly true|false]
347
+ hctl storagebox subaccount delete <box-id> <username> [-y]
348
+ ```
349
+
350
+ #### Traffic, WoL, Cancellation, Ordering
351
+
352
+ ```bash
353
+ hctl traffic query [-i <ips...>] [-s <subnets...>] [--from <date>] [--to <date>] [-t day|month|year]
354
+ hctl wol status|send <server>
355
+ hctl cancel status|request|revoke <server>
356
+ hctl order products|market|transactions
357
+ hctl order transaction <id>
358
+ ```
359
+
360
+ #### Interactive Mode
361
+
362
+ ```bash
363
+ hctl interactive # or: hctl i
364
+ ```
365
+
366
+ Menu-driven interface for common operations (list servers, reset, rescue, failover, SSH keys).
367
+
368
+ ### Cloud Commands
369
+
370
+ All cloud commands live under `hctl cloud <resource> <action>`. Add `--token <token>` to any command to override the active context.
371
+
372
+ #### Context Management
373
+
374
+ ```bash
375
+ hctl cloud context create <name> [-t <token>] [--no-verify]
376
+ hctl cloud context test [-t <token>]
377
+ hctl cloud context bootstrap <name> --project <name> [--create-project] [--write-env [path]]
378
+ hctl cloud context use <name>
379
+ hctl cloud context delete <name>
380
+ hctl cloud context list
381
+ hctl cloud context active
382
+ ```
383
+
384
+ `hctl cloud context bootstrap` is an explicit opt-in assisted browser flow. It
385
+ opens a visible local real-browser session so you can log in to the Hetzner Cloud
386
+ Console yourself, including 2FA. `hctl` attaches to the browser only after you
387
+ press Enter, so no automation runs during login. It does not ask for or store
388
+ your console password and does not scrape the one-time API token from the page.
389
+ After Hetzner
390
+ prepares the token form, `hctl` asks for confirmation again before generating
391
+ the credential. Copy the resulting token yourself and paste it into the hidden
392
+ CLI prompt. The token is then validated and saved as a normal `hctl` cloud
393
+ context. Use `--write-env` only when you also want `HETZNER_CLOUD_TOKEN` written
394
+ to a local env file.
395
+
396
+ #### Cloud Servers
397
+
398
+ ```bash
399
+ hctl cloud server list [-l <label-selector>] [-n <name>] [-s <sort>] [--status <status>]
400
+ hctl cloud server describe <id>
401
+ hctl cloud server create --name <name> --type <type> --image <image> [--location <loc>] [--ssh-key <keys...>]
402
+ hctl cloud server delete <id> [-y]
403
+ hctl cloud server update <id> [--name <name>]
404
+
405
+ # Power management
406
+ hctl cloud server poweron|poweroff|reboot|reset|shutdown <id>
407
+
408
+ # Maintenance
409
+ hctl cloud server rebuild <id> --image <image>
410
+ hctl cloud server change-type <id> --type <type> [--upgrade-disk]
411
+ hctl cloud server enable-rescue <id> [--type linux64] [--ssh-key <ids...>]
412
+ hctl cloud server disable-rescue <id>
413
+ hctl cloud server enable-backup|disable-backup <id>
414
+ hctl cloud server create-image <id> [--description <desc>]
415
+ hctl cloud server attach-iso|detach-iso <id> [--iso <iso>]
416
+ hctl cloud server reset-password <id>
417
+
418
+ # Networking
419
+ hctl cloud server set-rdns <id> --ip <ip> --dns-ptr <ptr>
420
+ hctl cloud server attach-to-network <id> --network <id> [--ip <ip>]
421
+ hctl cloud server detach-from-network <id> --network <id>
422
+
423
+ # Protection & labels
424
+ hctl cloud server enable-protection|disable-protection <id>
425
+ hctl cloud server add-label <id> <key=value>
426
+ hctl cloud server remove-label <id> <key>
427
+ hctl cloud server request-console <id>
428
+ ```
429
+
430
+ #### Other Cloud Resources
431
+
432
+ Each follows the pattern: `hctl cloud <resource> list|describe|create|delete`
433
+
434
+ ```bash
435
+ hctl cloud network list|describe|create|delete
436
+ hctl cloud firewall list|describe|create|delete
437
+ hctl cloud floating-ip list|describe|create|delete
438
+ hctl cloud primary-ip list|describe|create|delete
439
+ hctl cloud volume list|describe|create|delete
440
+ hctl cloud load-balancer list|describe|create|delete
441
+ hctl cloud image list|describe|update|delete
442
+ hctl cloud ssh-key list|describe|create|delete
443
+ hctl cloud certificate list|describe|create|delete
444
+ hctl cloud placement-group list|describe|create|delete
445
+
446
+ # Read-only reference data
447
+ hctl cloud datacenter list|describe
448
+ hctl cloud location list|describe
449
+ hctl cloud server-type list|describe
450
+ hctl cloud load-balancer-type list|describe
451
+ hctl cloud iso list|describe
452
+ ```
453
+
454
+ ### Auction Commands
455
+
456
+ Browse Hetzner's server auction — no authentication required. Data is fetched from the hosted 15-minute cache and filtered/sorted client-side. Add `--direct` when you want to bypass the cache and fetch directly from Hetzner.
457
+
458
+ ```bash
459
+ hctl auction list [options] # List and filter auction servers
460
+ hctl auction show <id> # Detailed view of a single server
461
+ hctl auction status # Show hosted/local cache freshness
462
+ hctl auction diff # Compare current data with the last local snapshot
463
+ hctl auction watch # Poll for local snapshot changes
464
+ ```
465
+
466
+ #### Auction List — Full Option Reference
467
+
468
+ **Price filters:**
469
+
470
+ ```bash
471
+ --min-price <n> # Minimum monthly price
472
+ --max-price <n> # Maximum monthly price
473
+ --max-hourly-price <n> # Maximum hourly price
474
+ --max-setup-price <n> # Maximum setup fee
475
+ --no-setup-fee # Only free setup (shorthand for --max-setup-price 0)
476
+ --fixed-price # Only fixed-price servers
477
+ --auction-only # Only auction servers (price decreases over time)
478
+ ```
479
+
480
+ **Hardware filters:**
481
+
482
+ ```bash
483
+ --cpu <text> # CPU model substring, case-insensitive (e.g. "Ryzen", "EPYC", "i7-6700")
484
+ --min-cpu-count <n> # Minimum CPU/socket count
485
+ --max-cpu-count <n> # Maximum CPU/socket count
486
+ --min-ram <gb> # Minimum RAM in GB
487
+ --max-ram <gb> # Maximum RAM in GB
488
+ --ecc # Only ECC RAM servers
489
+ ```
490
+
491
+ **Disk filters:**
492
+
493
+ ```bash
494
+ --min-disk-size <gb> # Minimum total disk capacity (sum of all drives)
495
+ --max-disk-size <gb> # Maximum total disk capacity
496
+ --min-disk-count <n> # Minimum number of physical drives
497
+ --max-disk-count <n> # Maximum number of physical drives
498
+ --disk-type <type> # Must have this disk type: nvme, sata, hdd
499
+ ```
500
+
501
+ **Network & feature filters:**
502
+
503
+ ```bash
504
+ --datacenter <text> # Datacenter substring, case-insensitive (e.g. "FSN", "HEL1-DC2", "NBG")
505
+ --min-bandwidth <mbit> # Minimum bandwidth in Mbit/s
506
+ --gpu # Only GPU servers
507
+ --inic # Only Intel NIC servers
508
+ --highio # Only high I/O servers
509
+ --specials <text> # Any special feature, substring match (e.g. "GPU", "ECC")
510
+ --search <text> # Free-text search across description
511
+ ```
512
+
513
+ **Sorting:**
514
+
515
+ ```bash
516
+ --sort <field> # Sort field (default: price). Choices:
517
+ # price, hourly, setup, ram, disk, disk_count,
518
+ # cpu, cpu_count, datacenter, bandwidth, next_reduce
519
+ --desc # Sort descending (default: ascending)
520
+ ```
521
+
522
+ **Output:**
523
+
524
+ ```bash
525
+ --currency <EUR|USD> # Price currency (default: EUR)
526
+ --direct # Fetch directly from Hetzner instead of hosted cache
527
+ --limit <n> # Limit number of results
528
+ --json # Output raw JSON
529
+ ```
530
+
531
+ #### Auction Status
532
+
533
+ ```bash
534
+ hctl auction status
535
+ hctl auction status --json
536
+ hctl auction status --currency USD
537
+ ```
538
+
539
+ `auction status --json` is the recommended freshness check for scripts and LLM agents. It includes `updatedAt`, `ageSeconds`, `stale`, `source`, `serverCount`, `endpointUrl`, `usingLocalFallback`, and local cache state.
540
+
541
+ #### Auction Examples
542
+
543
+ ```bash
544
+ # Browse all servers, cheapest first
545
+ hctl auction list
546
+
547
+ # Cheap AMD EPYC with NVMe + ECC in Helsinki
548
+ hctl auction list --cpu epyc --disk-type nvme --ecc --datacenter HEL --sort price
549
+
550
+ # GPU servers under 150 EUR
551
+ hctl auction list --gpu --max-price 150
552
+
553
+ # High-RAM servers with lots of drives
554
+ hctl auction list --min-ram 256 --min-disk-count 4 --sort ram --desc
555
+
556
+ # Cheapest 10 fixed-price NVMe servers
557
+ hctl auction list --fixed-price --disk-type nvme --sort price --limit 10
558
+
559
+ # Auction servers about to drop in price
560
+ hctl auction list --auction-only --sort next_reduce --limit 20
561
+
562
+ # Export to JSON for scripting
563
+ hctl auction list --max-price 60 --ecc --json | jq '.[].id'
564
+
565
+ # Check freshness before automation
566
+ hctl auction status --json
567
+
568
+ # Bypass the hosted cache
569
+ hctl auction list --direct --max-price 60
570
+ ```
571
+
572
+ ### Built-in Reference
573
+
574
+ For a complete, structured reference optimized for LLM context windows:
575
+
576
+ ```bash
577
+ hctl reference # or: hctl ref
578
+ hctl reference --section auction
579
+ hctl auction status --output-schema
580
+ ```
581
+
582
+ This prints every command, option, and example in a structured plaintext format designed for easy parsing by language models. Pipe it into your LLM context:
583
+
584
+ ```bash
585
+ hctl reference | pbcopy # Copy to clipboard (macOS)
586
+ hctl reference > /tmp/hctl-ref.txt # Save to file
587
+ ```
588
+
589
+ ---
590
+
591
+ ## Library Usage
592
+
593
+ ### Robot Client
594
+
595
+ ```typescript
596
+ import { HetznerRobotClient } from '@rollupdate/hctl';
597
+
598
+ const client = new HetznerRobotClient('username', 'password');
599
+
600
+ // List servers
601
+ const servers = await client.listServers();
602
+
603
+ // Get server details
604
+ const { server } = await client.getServer(123456);
605
+ console.log(server.server_name, server.server_ip);
606
+
607
+ // Reset a server
608
+ await client.resetServer(123456, 'sw');
609
+
610
+ // Activate rescue mode
611
+ const rescue = await client.activateRescue(123456, 'linux', 64, ['ssh-fingerprint']);
612
+ console.log('Password:', rescue.rescue.password);
613
+
614
+ // Linux installation
615
+ await client.activateLinux(123456, 'Debian-1210-bookworm-amd64-base', 64, 'en', ['ssh-fingerprint']);
616
+
617
+ // Failover
618
+ await client.switchFailover('1.2.3.4', '5.6.7.8');
619
+
620
+ // SSH keys
621
+ const keys = await client.listSshKeys();
622
+ await client.createSshKey('my-key', 'ssh-rsa AAAA...');
623
+
624
+ // Storage box
625
+ const boxes = await client.listStorageBoxes();
626
+ await client.createStorageBoxSnapshot(123);
627
+
628
+ // Firewall
629
+ const fw = await client.getFirewall(123456);
630
+ await client.updateFirewall(123456, 'active');
631
+ ```
632
+
633
+ ### Cloud Client
634
+
635
+ ```typescript
636
+ import { HetznerCloudClient } from '@rollupdate/hctl';
637
+
638
+ const client = new HetznerCloudClient('hcloud_xxxxxx');
639
+
640
+ // List servers
641
+ const { servers } = await client.listServers();
642
+
643
+ // Create a server
644
+ const result = await client.createServer({
645
+ name: 'web1',
646
+ server_type: 'cx22',
647
+ image: 'ubuntu-22.04',
648
+ location: 'fsn1',
649
+ });
650
+
651
+ // Networks
652
+ const { networks } = await client.listNetworks();
653
+ await client.createNetwork({ name: 'my-net', ip_range: '10.0.0.0/16' });
654
+
655
+ // Volumes
656
+ const { volumes } = await client.listVolumes();
657
+ await client.createVolume({ name: 'data', size: 50, location: 'fsn1' });
658
+
659
+ // Load Balancers
660
+ const { load_balancers } = await client.listLoadBalancers();
661
+
662
+ // Firewalls, Floating IPs, SSH Keys, Images, etc.
663
+ const { firewalls } = await client.listFirewalls();
664
+ const { floating_ips } = await client.listFloatingIps();
665
+ const { ssh_keys } = await client.listSshKeys();
666
+ ```
667
+
668
+ ### Auction Client
669
+
670
+ ```typescript
671
+ import {
672
+ fetchAuctionData,
673
+ fetchAuctionServers,
674
+ filterAuctionServers,
675
+ getAuctionDataUrl,
676
+ sortAuctionServers,
677
+ } from '@rollupdate/hctl';
678
+
679
+ // Fetch all servers from the hosted 15-minute cache (EUR pricing)
680
+ const { server: servers } = await fetchAuctionServers('EUR');
681
+ const { metadata } = await fetchAuctionData('EUR');
682
+ console.log(metadata.updatedAt, metadata.ageSeconds, metadata.source);
683
+
684
+ // Or bypass the cache and fetch directly from Hetzner
685
+ const live = await fetchAuctionServers('EUR', { source: 'direct' });
686
+
687
+ console.log(getAuctionDataUrl('EUR'), live.serverCount);
688
+
689
+ // Filter: cheap ECC NVMe servers in Helsinki
690
+ const filtered = filterAuctionServers(servers, {
691
+ maxPrice: 60,
692
+ ecc: true,
693
+ diskType: 'nvme',
694
+ datacenter: 'HEL',
695
+ });
696
+
697
+ // Sort by price ascending
698
+ const sorted = sortAuctionServers(filtered, 'price', false);
699
+
700
+ console.log(`Found ${sorted.length} servers`);
701
+ for (const s of sorted) {
702
+ console.log(`${s.id}: ${s.cpu} | ${s.ram_size}GB | €${s.price}/mo | ${s.datacenter}`);
703
+ }
704
+ ```
705
+
706
+ ### Type Exports
707
+
708
+ ```typescript
709
+ import type {
710
+ // Robot types
711
+ Server, ServerDetails, Reset, ResetType,
712
+ RescueConfig, LinuxConfig,
713
+ IP, Subnet, Failover, Rdns,
714
+ SshKey, Firewall, FirewallRule,
715
+ VSwitch, StorageBox,
716
+ Traffic, Wol,
717
+ ServerProduct, ServerMarketProduct,
718
+
719
+ // Cloud types
720
+ CloudServer, CloudAction, CloudFirewall, CloudFirewallRule,
721
+ CloudSshKey, Network, NetworkSubnet, NetworkRoute,
722
+ FloatingIp, PrimaryIp, Volume, Image,
723
+ LoadBalancer, LoadBalancerType, LoadBalancerTarget, LoadBalancerService,
724
+ Certificate, PlacementGroup, Datacenter, Location,
725
+ ServerType, ISO, Labels,
726
+
727
+ // Auction types
728
+ AuctionServer, AuctionDiskData, AuctionIpPrice,
729
+ AuctionFilterOptions, AuctionResponse,
730
+ } from '@rollupdate/hctl';
731
+ ```
732
+
733
+ ---
734
+
735
+ ## API Reference
736
+
737
+ ### HetznerRobotClient Methods
738
+
739
+ #### Servers
740
+
741
+ | Method | Description |
742
+ |--------|-------------|
743
+ | `listServers()` | List all servers |
744
+ | `getServer(id)` | Get server details |
745
+ | `updateServerName(id, name)` | Rename server |
746
+ | `getCancellation(id)` | Get cancellation status |
747
+ | `cancelServer(id, date?, reasons?)` | Cancel server |
748
+ | `revokeCancellation(id)` | Revoke cancellation |
749
+
750
+ #### Reset
751
+
752
+ | Method | Description |
753
+ |--------|-------------|
754
+ | `listResetOptions()` | List all reset options |
755
+ | `getResetOptions(id)` | Get reset options for server |
756
+ | `resetServer(id, type?)` | Reset server (sw/hw/man/power/power_long) |
757
+
758
+ #### Boot
759
+
760
+ | Method | Description |
761
+ |--------|-------------|
762
+ | `getBootConfig(id)` | Get all boot configs |
763
+ | `activateRescue(id, os, arch?, keys?)` | Activate rescue |
764
+ | `deactivateRescue(id)` | Deactivate rescue |
765
+ | `getLastRescue(id)` | Get last rescue activation |
766
+ | `getLinux(id)` | Get available Linux distributions |
767
+ | `activateLinux(id, dist, arch?, lang?, keys?)` | Activate Linux install |
768
+ | `deactivateLinux(id)` | Deactivate Linux |
769
+ | `activateVnc(id, dist, arch?, lang?)` | Activate VNC install |
770
+ | `activateWindows(id, dist, lang?)` | Activate Windows install |
771
+
772
+ #### IPs & Networking
773
+
774
+ | Method | Description |
775
+ |--------|-------------|
776
+ | `listIps()` | List all IPs |
777
+ | `getIp(ip)` | Get IP details |
778
+ | `updateIp(ip, warnings?, hourly?, daily?, monthly?)` | Update IP settings |
779
+ | `generateIpMac(ip)` | Generate separate MAC |
780
+ | `deleteIpMac(ip)` | Delete MAC |
781
+ | `listSubnets()` | List subnets |
782
+ | `getSubnet(ip)` | Get subnet details |
783
+ | `listFailovers()` | List failover IPs |
784
+ | `getFailover(ip)` | Get failover details |
785
+ | `switchFailover(ip, targetIp)` | Switch routing |
786
+ | `deleteFailoverRouting(ip)` | Delete routing |
787
+ | `listRdns()` | List reverse DNS |
788
+ | `getRdns(ip)` | Get rDNS for IP |
789
+ | `createRdns(ip, ptr)` | Create/update rDNS |
790
+ | `deleteRdns(ip)` | Delete rDNS |
791
+
792
+ #### SSH Keys
793
+
794
+ | Method | Description |
795
+ |--------|-------------|
796
+ | `listSshKeys()` | List SSH keys |
797
+ | `getSshKey(fingerprint)` | Get key details |
798
+ | `createSshKey(name, data)` | Add SSH key |
799
+ | `updateSshKey(fingerprint, name)` | Rename key |
800
+ | `deleteSshKey(fingerprint)` | Delete key |
801
+
802
+ #### Firewall
803
+
804
+ | Method | Description |
805
+ |--------|-------------|
806
+ | `getFirewall(id)` | Get firewall config |
807
+ | `updateFirewall(id, status, rules?)` | Update firewall |
808
+ | `deleteFirewall(id)` | Delete all rules |
809
+ | `listFirewallTemplates()` | List templates |
810
+ | `getFirewallTemplate(id)` | Get template |
811
+ | `deleteFirewallTemplate(id)` | Delete template |
812
+
813
+ #### vSwitch
814
+
815
+ | Method | Description |
816
+ |--------|-------------|
817
+ | `listVSwitches()` | List vSwitches |
818
+ | `getVSwitch(id)` | Get details |
819
+ | `listVSwitchesWithServers()` | Get details for every vSwitch, including servers |
820
+ | `addVSwitchServerNames(vswitches)` | Add server names to vSwitch details |
821
+ | `createVSwitch(name, vlan)` | Create |
822
+ | `updateVSwitch(id, name?, vlan?)` | Update |
823
+ | `deleteVSwitch(id, date?)` | Delete |
824
+ | `addServerToVSwitch(vswitchId, serverId)` | Add server |
825
+ | `removeServerFromVSwitch(vswitchId, serverId)` | Remove server |
826
+
827
+ #### Storage Box
828
+
829
+ | Method | Description |
830
+ |--------|-------------|
831
+ | `listStorageBoxes()` | List storage boxes |
832
+ | `getStorageBox(id)` | Get details |
833
+ | `updateStorageBox(id, ...)` | Update settings |
834
+ | `resetStorageBoxPassword(id)` | Reset password |
835
+ | `listStorageBoxSnapshots(id)` | List snapshots |
836
+ | `createStorageBoxSnapshot(id)` | Create snapshot |
837
+ | `deleteStorageBoxSnapshot(id, name)` | Delete snapshot |
838
+ | `revertStorageBoxSnapshot(id, name)` | Revert to snapshot |
839
+ | `listStorageBoxSubaccounts(id)` | List subaccounts |
840
+ | `createStorageBoxSubaccount(id, ...)` | Create subaccount |
841
+ | `deleteStorageBoxSubaccount(id, username)` | Delete subaccount |
842
+
843
+ #### Traffic, WoL, Ordering
844
+
845
+ | Method | Description |
846
+ |--------|-------------|
847
+ | `getTraffic(ips, subnets, from, to, type)` | Query traffic |
848
+ | `getWol(id)` | Get WoL status |
849
+ | `sendWol(id)` | Send Wake-on-LAN |
850
+ | `listServerProducts()` | List products |
851
+ | `listServerMarketProducts()` | List auction servers |
852
+ | `getServerTransaction(id)` | Get transaction |
853
+ | `listServerTransactions()` | List transactions |
854
+
855
+ ### HetznerCloudClient Methods
856
+
857
+ #### Context Management
858
+
859
+ | Function | Description |
860
+ |----------|-------------|
861
+ | `createContext(name, token)` | Create a named context (stores token) |
862
+ | `useContext(name)` | Switch active context |
863
+ | `deleteContext(name)` | Delete a context |
864
+ | `listContexts()` | List all contexts |
865
+ | `getActiveContext()` | Get active context name |
866
+ | `resolveToken(opts)` | Resolve token (flag > env > context) |
867
+
868
+ #### Servers
869
+
870
+ | Method | Description |
871
+ |--------|-------------|
872
+ | `listServers(params?)` | List cloud servers |
873
+ | `getServer(id)` | Get server details |
874
+ | `createServer(data)` | Create a server |
875
+ | `deleteServer(id)` | Delete a server |
876
+ | `updateServer(id, data)` | Update server name/labels |
877
+ | `poweronServer(id)` | Power on |
878
+ | `poweroffServer(id)` | Hard power off |
879
+ | `rebootServer(id)` | Soft reboot |
880
+ | `resetServer(id)` | Hard reset |
881
+ | `shutdownServer(id)` | Graceful shutdown |
882
+ | `rebuildServer(id, image)` | Rebuild with image |
883
+ | `changeServerType(id, type, disk?)` | Resize server |
884
+ | `enableRescue(id, type?, keys?)` | Enable rescue mode |
885
+ | `disableRescue(id)` | Disable rescue mode |
886
+ | `enableBackup(id)` | Enable backups |
887
+ | `disableBackup(id)` | Disable backups |
888
+ | `createImage(id, desc?, type?)` | Create snapshot |
889
+ | `attachIso(id, iso)` | Attach ISO |
890
+ | `detachIso(id)` | Detach ISO |
891
+ | `resetPassword(id)` | Reset root password |
892
+ | `setRdns(id, ip, ptr)` | Set reverse DNS |
893
+ | `changeProtection(id, opts)` | Change protection |
894
+ | `requestConsole(id)` | Get VNC console URL |
895
+ | `attachToNetwork(id, network, ip?)` | Attach to network |
896
+ | `detachFromNetwork(id, network)` | Detach from network |
897
+
898
+ #### Networks
899
+
900
+ | Method | Description |
901
+ |--------|-------------|
902
+ | `listNetworks()` | List networks |
903
+ | `getNetwork(id)` | Get network details |
904
+ | `createNetwork(data)` | Create a network |
905
+ | `deleteNetwork(id)` | Delete a network |
906
+ | `addSubnet(id, subnet)` | Add subnet |
907
+ | `deleteSubnet(id, subnet)` | Delete subnet |
908
+ | `addRoute(id, route)` | Add route |
909
+ | `deleteRoute(id, route)` | Delete route |
910
+ | `changeIpRange(id, range)` | Change IP range |
911
+
912
+ #### Firewalls, IPs, Volumes, Load Balancers
913
+
914
+ | Method | Description |
915
+ |--------|-------------|
916
+ | `listFirewalls()` / `getFirewall(id)` / `createFirewall(data)` / `deleteFirewall(id)` | Firewall CRUD |
917
+ | `listFloatingIps()` / `getFloatingIp(id)` / `createFloatingIp(data)` / `deleteFloatingIp(id)` | Floating IP CRUD |
918
+ | `listPrimaryIps()` / `getPrimaryIp(id)` / `createPrimaryIp(data)` / `deletePrimaryIp(id)` | Primary IP CRUD |
919
+ | `listVolumes()` / `getVolume(id)` / `createVolume(data)` / `deleteVolume(id)` | Volume CRUD |
920
+ | `listLoadBalancers()` / `getLoadBalancer(id)` / `createLoadBalancer(data)` / `deleteLoadBalancer(id)` | Load Balancer CRUD |
921
+ | `listImages()` / `getImage(id)` / `updateImage(id, data)` / `deleteImage(id)` | Image management |
922
+ | `listSshKeys()` / `getSshKey(id)` / `createSshKey(data)` / `deleteSshKey(id)` | SSH key CRUD |
923
+ | `listCertificates()` / `getCertificate(id)` / `createCertificate(data)` / `deleteCertificate(id)` | Certificate CRUD |
924
+ | `listPlacementGroups()` / `getPlacementGroup(id)` / `createPlacementGroup(data)` / `deletePlacementGroup(id)` | Placement group CRUD |
925
+
926
+ #### Reference Data (Read-Only)
927
+
928
+ | Method | Description |
929
+ |--------|-------------|
930
+ | `listDatacenters()` / `getDatacenter(id)` | Datacenter info |
931
+ | `listLocations()` / `getLocation(id)` | Location info |
932
+ | `listServerTypes()` / `getServerType(id)` | Server type info |
933
+ | `listLoadBalancerTypes()` / `getLoadBalancerType(id)` | LB type info |
934
+ | `listIsos(params?)` / `getIso(id)` | ISO image info |
935
+
936
+ ### Auction Functions
937
+
938
+ | Function | Description |
939
+ |----------|-------------|
940
+ | `fetchAuctionData(currency?, options?)` | Fetch servers plus freshness metadata (`updatedAt`, `ageSeconds`, `source`, `stale`, `url`) |
941
+ | `fetchAuctionServers(currency?, options?)` | Fetch all servers from hosted cache by default, or direct from Hetzner with `{ source: "direct" }` |
942
+ | `filterAuctionServers(servers, filters)` | Apply filter criteria to server array |
943
+ | `getAuctionDataUrl(currency?, options?)` | Resolve the JSON URL used for a cache or direct fetch |
944
+ | `sortAuctionServers(servers, field, desc)` | Sort servers by field |
945
+
946
+ ---
947
+
948
+ ## Configuration Files
949
+
950
+ | File | Purpose |
951
+ |------|---------|
952
+ | `~/.hctl/config.json` | Robot API credentials (file-based fallback) |
953
+ | `~/.hctl/cloud-contexts.json` | Cloud API contexts and tokens |
954
+
955
+ System keychain (via keytar):
956
+
957
+ | Key | Value |
958
+ |-----|-------|
959
+ | Service | `hctl` |
960
+ | Account | `robot-api` |
961
+
962
+ Environment variables:
963
+
964
+ | Variable | Purpose |
965
+ |----------|---------|
966
+ | `HETZNER_ROBOT_USER` | Robot API username |
967
+ | `HETZNER_ROBOT_PASSWORD` | Robot API password |
968
+ | `HETZNER_CLOUD_TOKEN` | Cloud API token (overrides active context) |
969
+ | `HCTL_AUCTION_SOURCE` | Set to `direct` to bypass the hosted auction cache |
970
+ | `HCTL_AUCTION_CACHE_URL` | Override the hosted auction cache base URL |
971
+
972
+ ### Cloudflare Deployment
973
+
974
+ The website deploys to Cloudflare Pages via `.github/workflows/deploy-website.yml`. The auction cache deploys as a separate Cloudflare Worker via `.github/workflows/deploy-auction-worker.yml`; it refreshes EUR and USD auction JSON into R2 every 15 minutes with the Cron Trigger in `wrangler.auction.toml`.
975
+
976
+ The Worker validates that Hetzner returns JSON with a `server` array before updating R2. Refresh failures do not overwrite the last good snapshot; they are recorded under the metadata and health endpoints so monitors can detect degraded refreshes.
977
+
978
+ R2 object layout:
979
+
980
+ | Prefix | Purpose |
981
+ |--------|---------|
982
+ | `latest/live_data_sb_EUR.json` | Current EUR auction JSON |
983
+ | `latest/live_data_sb_USD.json` | Current USD auction JSON |
984
+ | `history/YYYY/MM/DD/HHmm/live_data_sb_EUR.json` | Historical EUR snapshots |
985
+ | `history/YYYY/MM/DD/HHmm/live_data_sb_USD.json` | Historical USD snapshots |
986
+ | `failures/EUR.json` | Last EUR refresh failure, when present |
987
+ | `failures/USD.json` | Last USD refresh failure, when present |
988
+
989
+ Required GitHub secret:
990
+
991
+ | Secret | Purpose |
992
+ |--------|---------|
993
+ | `CLOUDFLARE_API_TOKEN` | Token with Pages, Workers, Workers KV, and Workers R2 edit access |
994
+
995
+ Optional GitHub variables:
996
+
997
+ | Variable | Default |
998
+ |----------|---------|
999
+ | `CLOUDFLARE_ACCOUNT_ID` | `fffa2cbba41f8b0e145472fe76c90065` |
1000
+ | `CLOUDFLARE_PAGES_PROJECT_NAME` | `hctl` |
1001
+ | `WEBSITE_ORIGIN` | `https://hctl.dev` |
1002
+
1003
+ Initial setup:
1004
+
1005
+ ```bash
1006
+ npx wrangler pages project create hctl --production-branch main
1007
+ npx wrangler r2 bucket create hctl-auction-data
1008
+ npx wrangler deploy --config wrangler.auction.toml
1009
+ ```
1010
+
1011
+ Manual deployments:
1012
+
1013
+ ```bash
1014
+ gh workflow run deploy-website.yml
1015
+ gh workflow run deploy-auction-worker.yml
1016
+ ```
1017
+
1018
+ ---
1019
+
1020
+ ## Development
1021
+
1022
+ ```bash
1023
+ npm install # Install dependencies
1024
+ npm run build # Compile TypeScript
1025
+ npm test # Run tests
1026
+ npm run test:coverage # Run with coverage report
1027
+ npm run test:watch # Watch mode
1028
+ ```
1029
+
1030
+ ### Project Structure
1031
+
1032
+ ```
1033
+ src/
1034
+ ├── cli.ts # CLI entry point
1035
+ ├── index.ts # Library exports
1036
+ ├── shared/
1037
+ │ ├── config.ts # Credential management
1038
+ │ ├── formatter.ts # Colors, tables, output formatting
1039
+ │ ├── helpers.ts # asyncAction, output, confirmAction
1040
+ │ └── reference.ts # Built-in reference documentation
1041
+ ├── robot/
1042
+ │ ├── client.ts # Robot API client (HetznerRobotClient)
1043
+ │ ├── types.ts # Robot type definitions
1044
+ │ ├── formatter.ts # Robot-specific formatters
1045
+ │ └── commands/ # CLI command modules (17 files)
1046
+ ├── cloud/
1047
+ │ ├── client.ts # Cloud API client
1048
+ │ ├── context.ts # Context/token management
1049
+ │ ├── types.ts # Cloud type definitions
1050
+ │ ├── formatter.ts # Cloud-specific formatters
1051
+ │ ├── helpers.ts # Cloud action wrappers
1052
+ │ └── commands/ # CLI command modules (17 files)
1053
+ └── auction/
1054
+ ├── client.ts # Auction fetch/filter/sort
1055
+ ├── formatter.ts # Auction formatters
1056
+ └── commands.ts # Auction CLI commands
1057
+ ```
1058
+
1059
+ ### AI Agent Instructions
1060
+
1061
+ This repo is developed with help from multiple coding agents. Each reads its own instruction file at the repo root:
1062
+
1063
+ - **Claude Code** — `.claude/CLAUDE.md` (project-scoped instructions)
1064
+ - **OpenAI Codex / generic agents** — `AGENTS.md`
1065
+
1066
+ Both files describe the same code-quality conventions (Ultracite + Biome), so agents produce consistent output regardless of which one is driving.
1067
+
1068
+ ---
1069
+
1070
+ ## License
1071
+
1072
+ MIT