@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.
- package/LICENSE +21 -0
- package/README.md +1071 -2
- package/dist/auction/cache.d.ts +6 -0
- package/dist/auction/cache.js +55 -0
- package/dist/auction/client.d.ts +24 -0
- package/dist/auction/client.js +236 -0
- package/dist/auction/commands.d.ts +2 -0
- package/dist/auction/commands.js +440 -0
- package/dist/auction/diff-formatter.d.ts +3 -0
- package/dist/auction/diff-formatter.js +61 -0
- package/dist/auction/diff.d.ts +11 -0
- package/dist/auction/diff.js +28 -0
- package/dist/auction/formatter.d.ts +21 -0
- package/dist/auction/formatter.js +182 -0
- package/dist/auction/store.d.ts +24 -0
- package/dist/auction/store.js +177 -0
- package/dist/auction/types.d.ts +74 -0
- package/dist/auction/types.js +5 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +74 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +4 -0
- package/dist/cloud/browser-bootstrap.d.ts +28 -0
- package/dist/cloud/browser-bootstrap.js +480 -0
- package/dist/cloud/client.d.ts +520 -0
- package/dist/cloud/client.js +755 -0
- package/dist/cloud/commands/certificate.d.ts +2 -0
- package/dist/cloud/commands/certificate.js +108 -0
- package/dist/cloud/commands/context.d.ts +2 -0
- package/dist/cloud/commands/context.js +133 -0
- package/dist/cloud/commands/datacenter.d.ts +2 -0
- package/dist/cloud/commands/datacenter.js +24 -0
- package/dist/cloud/commands/firewall.d.ts +2 -0
- package/dist/cloud/commands/firewall.js +142 -0
- package/dist/cloud/commands/floating-ip.d.ts +2 -0
- package/dist/cloud/commands/floating-ip.js +138 -0
- package/dist/cloud/commands/image.d.ts +2 -0
- package/dist/cloud/commands/image.js +98 -0
- package/dist/cloud/commands/index.d.ts +2 -0
- package/dist/cloud/commands/index.js +47 -0
- package/dist/cloud/commands/inventory.d.ts +2 -0
- package/dist/cloud/commands/inventory.js +13 -0
- package/dist/cloud/commands/iso.d.ts +2 -0
- package/dist/cloud/commands/iso.js +27 -0
- package/dist/cloud/commands/load-balancer-type.d.ts +2 -0
- package/dist/cloud/commands/load-balancer-type.js +24 -0
- package/dist/cloud/commands/load-balancer.d.ts +2 -0
- package/dist/cloud/commands/load-balancer.js +269 -0
- package/dist/cloud/commands/location.d.ts +2 -0
- package/dist/cloud/commands/location.js +24 -0
- package/dist/cloud/commands/network.d.ts +2 -0
- package/dist/cloud/commands/network.js +157 -0
- package/dist/cloud/commands/placement-group.d.ts +2 -0
- package/dist/cloud/commands/placement-group.js +79 -0
- package/dist/cloud/commands/pricing.d.ts +2 -0
- package/dist/cloud/commands/pricing.js +15 -0
- package/dist/cloud/commands/primary-ip.d.ts +2 -0
- package/dist/cloud/commands/primary-ip.js +139 -0
- package/dist/cloud/commands/server-type.d.ts +2 -0
- package/dist/cloud/commands/server-type.js +24 -0
- package/dist/cloud/commands/server.d.ts +2 -0
- package/dist/cloud/commands/server.js +346 -0
- package/dist/cloud/commands/ssh-key.d.ts +2 -0
- package/dist/cloud/commands/ssh-key.js +94 -0
- package/dist/cloud/commands/volume.d.ts +2 -0
- package/dist/cloud/commands/volume.js +138 -0
- package/dist/cloud/context.d.ts +34 -0
- package/dist/cloud/context.js +215 -0
- package/dist/cloud/formatter.d.ts +40 -0
- package/dist/cloud/formatter.js +824 -0
- package/dist/cloud/helpers.d.ts +27 -0
- package/dist/cloud/helpers.js +67 -0
- package/dist/cloud/inventory.d.ts +100 -0
- package/dist/cloud/inventory.js +229 -0
- package/dist/cloud/pricing.d.ts +27 -0
- package/dist/cloud/pricing.js +103 -0
- package/dist/cloud/token-instructions.d.ts +2 -0
- package/dist/cloud/token-instructions.js +21 -0
- package/dist/cloud/types.d.ts +437 -0
- package/dist/cloud/types.js +5 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -0
- package/dist/formatter.d.ts +3 -0
- package/dist/formatter.js +6 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +21 -0
- package/dist/robot/client.d.ts +256 -0
- package/dist/robot/client.js +745 -0
- package/dist/robot/commands/auth.d.ts +2 -0
- package/dist/robot/commands/auth.js +54 -0
- package/dist/robot/commands/boot.d.ts +2 -0
- package/dist/robot/commands/boot.js +76 -0
- package/dist/robot/commands/cancel.d.ts +2 -0
- package/dist/robot/commands/cancel.js +37 -0
- package/dist/robot/commands/failover.d.ts +2 -0
- package/dist/robot/commands/failover.js +46 -0
- package/dist/robot/commands/firewall.d.ts +2 -0
- package/dist/robot/commands/firewall.js +72 -0
- package/dist/robot/commands/index.d.ts +2 -0
- package/dist/robot/commands/index.js +36 -0
- package/dist/robot/commands/interactive.d.ts +2 -0
- package/dist/robot/commands/interactive.js +152 -0
- package/dist/robot/commands/ip.d.ts +2 -0
- package/dist/robot/commands/ip.js +55 -0
- package/dist/robot/commands/key.d.ts +2 -0
- package/dist/robot/commands/key.js +68 -0
- package/dist/robot/commands/order.d.ts +2 -0
- package/dist/robot/commands/order.js +33 -0
- package/dist/robot/commands/rdns.d.ts +2 -0
- package/dist/robot/commands/rdns.js +41 -0
- package/dist/robot/commands/reset.d.ts +2 -0
- package/dist/robot/commands/reset.js +77 -0
- package/dist/robot/commands/server.d.ts +2 -0
- package/dist/robot/commands/server.js +32 -0
- package/dist/robot/commands/storagebox.d.ts +2 -0
- package/dist/robot/commands/storagebox.js +126 -0
- package/dist/robot/commands/subnet.d.ts +2 -0
- package/dist/robot/commands/subnet.js +24 -0
- package/dist/robot/commands/traffic.d.ts +2 -0
- package/dist/robot/commands/traffic.js +23 -0
- package/dist/robot/commands/vswitch.d.ts +2 -0
- package/dist/robot/commands/vswitch.js +81 -0
- package/dist/robot/commands/wol.d.ts +2 -0
- package/dist/robot/commands/wol.js +20 -0
- package/dist/robot/formatter.d.ts +57 -0
- package/dist/robot/formatter.js +582 -0
- package/dist/robot/types.d.ts +359 -0
- package/dist/robot/types.js +5 -0
- package/dist/shared/config.d.ts +86 -0
- package/dist/shared/config.js +289 -0
- package/dist/shared/formatter.d.ts +29 -0
- package/dist/shared/formatter.js +121 -0
- package/dist/shared/helpers.d.ts +39 -0
- package/dist/shared/helpers.js +86 -0
- package/dist/shared/init.d.ts +2 -0
- package/dist/shared/init.js +107 -0
- package/dist/shared/reference.d.ts +5 -0
- package/dist/shared/reference.js +689 -0
- package/dist/shared/schemas.d.ts +1 -0
- package/dist/shared/schemas.js +1031 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +2 -0
- package/package.json +127 -4
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { confirm, input, password as passwordPrompt } from "@inquirer/prompts";
|
|
5
|
+
import { config } from "dotenv";
|
|
6
|
+
// `quiet: true` suppresses dotenv 17's "[dotenv@…] injecting env" banner.
|
|
7
|
+
// See src/cli.ts for the full rationale (it breaks JSON-output consumers).
|
|
8
|
+
config({ quiet: true });
|
|
9
|
+
const CONFIG_DIR = join(homedir(), ".hctl");
|
|
10
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
11
|
+
const LEGACY_CONFIG_DIR = join(homedir(), ".hetzner-cli");
|
|
12
|
+
const LEGACY_CONFIG_FILE = join(LEGACY_CONFIG_DIR, "config.json");
|
|
13
|
+
const KEYCHAIN_SERVICE = "hctl";
|
|
14
|
+
const LEGACY_KEYCHAIN_SERVICE = "hetzner-cli";
|
|
15
|
+
const KEYCHAIN_ACCOUNT = "robot-api";
|
|
16
|
+
// Lazy-loaded keytar module (optional dependency with native bindings)
|
|
17
|
+
let keytarModule = null;
|
|
18
|
+
let keytarLoadAttempted = false;
|
|
19
|
+
/**
|
|
20
|
+
* Try to load keytar module (may fail if native deps unavailable)
|
|
21
|
+
*/
|
|
22
|
+
async function getKeytar() {
|
|
23
|
+
if (keytarLoadAttempted) {
|
|
24
|
+
return keytarModule;
|
|
25
|
+
}
|
|
26
|
+
keytarLoadAttempted = true;
|
|
27
|
+
try {
|
|
28
|
+
keytarModule = await import("keytar");
|
|
29
|
+
return keytarModule;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if system keychain is available
|
|
37
|
+
*/
|
|
38
|
+
export async function hasKeychainSupport() {
|
|
39
|
+
const keytar = await getKeytar();
|
|
40
|
+
if (!keytar) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
// Try a read operation to verify keychain access
|
|
45
|
+
await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get credentials from system keychain
|
|
54
|
+
*/
|
|
55
|
+
export async function getKeychainCredentials() {
|
|
56
|
+
const keytar = await getKeytar();
|
|
57
|
+
if (!keytar) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const stored = (await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)) ??
|
|
62
|
+
(await keytar.getPassword(LEGACY_KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT));
|
|
63
|
+
if (!stored) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const parsed = JSON.parse(stored);
|
|
67
|
+
if (parsed.user && parsed.password) {
|
|
68
|
+
return { user: parsed.user, password: parsed.password };
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Save credentials to system keychain
|
|
78
|
+
* @returns true if saved successfully, false otherwise
|
|
79
|
+
*/
|
|
80
|
+
export async function saveToKeychain(user, password) {
|
|
81
|
+
const keytar = await getKeytar();
|
|
82
|
+
if (!keytar) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, JSON.stringify({ user, password }));
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Clear credentials from system keychain
|
|
95
|
+
*/
|
|
96
|
+
export async function clearKeychain() {
|
|
97
|
+
const keytar = await getKeytar();
|
|
98
|
+
if (!keytar) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await keytar.deletePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
103
|
+
await keytar.deletePassword(LEGACY_KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Ignore errors when clearing
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Ensure config directory exists
|
|
111
|
+
*/
|
|
112
|
+
function ensureConfigDir() {
|
|
113
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
114
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Load configuration from file
|
|
119
|
+
*/
|
|
120
|
+
export function loadConfig() {
|
|
121
|
+
const file = existsSync(CONFIG_FILE) ? CONFIG_FILE : LEGACY_CONFIG_FILE;
|
|
122
|
+
if (!existsSync(file)) {
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const data = readFileSync(file, "utf-8");
|
|
127
|
+
return JSON.parse(data);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Save configuration to file
|
|
135
|
+
*/
|
|
136
|
+
export function saveConfig(cfg) {
|
|
137
|
+
ensureConfigDir();
|
|
138
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Clear saved configuration from file
|
|
142
|
+
*/
|
|
143
|
+
export function clearConfigFile() {
|
|
144
|
+
if (existsSync(CONFIG_FILE)) {
|
|
145
|
+
writeFileSync(CONFIG_FILE, "{}", { mode: 0o600 });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Clear saved configuration from all storage locations
|
|
150
|
+
*/
|
|
151
|
+
export async function clearConfig() {
|
|
152
|
+
await clearKeychain();
|
|
153
|
+
clearConfigFile();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get credentials from environment variables only (sync)
|
|
157
|
+
*/
|
|
158
|
+
export function getCredentialsFromEnv() {
|
|
159
|
+
const envUser = process.env.HETZNER_ROBOT_USER;
|
|
160
|
+
const envPassword = process.env.HETZNER_ROBOT_PASSWORD;
|
|
161
|
+
if (envUser && envPassword) {
|
|
162
|
+
return { user: envUser, password: envPassword };
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get credentials from config file only (sync)
|
|
168
|
+
*/
|
|
169
|
+
export function getCredentialsFromFile() {
|
|
170
|
+
const cfg = loadConfig();
|
|
171
|
+
if (cfg.user && cfg.password) {
|
|
172
|
+
return { user: cfg.user, password: cfg.password };
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get credentials from environment variables, keychain, or config file
|
|
178
|
+
* Priority: env vars → keychain → config file
|
|
179
|
+
*/
|
|
180
|
+
export async function getCredentials() {
|
|
181
|
+
// First try environment variables
|
|
182
|
+
const envCreds = getCredentialsFromEnv();
|
|
183
|
+
if (envCreds) {
|
|
184
|
+
return { ...envCreds, source: "environment" };
|
|
185
|
+
}
|
|
186
|
+
// Then try keychain
|
|
187
|
+
const keychainCreds = await getKeychainCredentials();
|
|
188
|
+
if (keychainCreds) {
|
|
189
|
+
return { ...keychainCreds, source: "keychain" };
|
|
190
|
+
}
|
|
191
|
+
// Finally try config file
|
|
192
|
+
const fileCreds = getCredentialsFromFile();
|
|
193
|
+
if (fileCreds) {
|
|
194
|
+
return { ...fileCreds, source: "file" };
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Check if credentials are configured (sync check - env and file only)
|
|
200
|
+
*/
|
|
201
|
+
export function hasCredentialsSync() {
|
|
202
|
+
return getCredentialsFromEnv() !== null || getCredentialsFromFile() !== null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Check if credentials are configured (async - includes keychain)
|
|
206
|
+
*/
|
|
207
|
+
export async function hasCredentials() {
|
|
208
|
+
return (await getCredentials()) !== null;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Interactive login prompt
|
|
212
|
+
*/
|
|
213
|
+
export async function promptLogin() {
|
|
214
|
+
console.log("");
|
|
215
|
+
console.log("Hetzner Robot API Authentication");
|
|
216
|
+
console.log("─".repeat(40));
|
|
217
|
+
console.log("");
|
|
218
|
+
console.log("To create Robot web service credentials:");
|
|
219
|
+
console.log(" 1. Open https://robot.hetzner.com");
|
|
220
|
+
console.log(" 2. Top-right menu: Settings > Web service settings");
|
|
221
|
+
console.log(' 3. Click "create access data"');
|
|
222
|
+
console.log(" 4. Choose a username and password");
|
|
223
|
+
console.log("");
|
|
224
|
+
console.log("Note: Robot web service credentials are created in the Robot UI, not in the Cloud Console.");
|
|
225
|
+
console.log("They are separate from your main Hetzner account login.");
|
|
226
|
+
console.log("");
|
|
227
|
+
// Check if we should offer migration from file to keychain
|
|
228
|
+
const keychainAvailable = await hasKeychainSupport();
|
|
229
|
+
const existingFileCreds = getCredentialsFromFile();
|
|
230
|
+
if (keychainAvailable && existingFileCreds) {
|
|
231
|
+
const migrate = await confirm({
|
|
232
|
+
message: "Migrate existing credentials to secure keychain storage?",
|
|
233
|
+
default: true,
|
|
234
|
+
});
|
|
235
|
+
if (migrate) {
|
|
236
|
+
const saved = await saveToKeychain(existingFileCreds.user, existingFileCreds.password);
|
|
237
|
+
if (saved) {
|
|
238
|
+
clearConfigFile();
|
|
239
|
+
console.log("");
|
|
240
|
+
console.log("Credentials migrated to keychain.");
|
|
241
|
+
return existingFileCreds;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const user = await input({
|
|
246
|
+
message: "Web service username:",
|
|
247
|
+
validate: (v) => v.length > 0 || "Username is required",
|
|
248
|
+
});
|
|
249
|
+
const password = await passwordPrompt({
|
|
250
|
+
message: "Web service password:",
|
|
251
|
+
validate: (v) => v.length > 0 || "Password is required",
|
|
252
|
+
});
|
|
253
|
+
// Determine storage location based on keychain availability
|
|
254
|
+
const storageMessage = keychainAvailable
|
|
255
|
+
? "Save credentials to secure keychain?"
|
|
256
|
+
: "Save credentials to ~/.hctl/config.json?";
|
|
257
|
+
const save = await confirm({
|
|
258
|
+
message: storageMessage,
|
|
259
|
+
default: true,
|
|
260
|
+
});
|
|
261
|
+
if (save) {
|
|
262
|
+
let savedToKeychain = false;
|
|
263
|
+
if (keychainAvailable) {
|
|
264
|
+
savedToKeychain = await saveToKeychain(user, password);
|
|
265
|
+
}
|
|
266
|
+
if (savedToKeychain) {
|
|
267
|
+
console.log("");
|
|
268
|
+
console.log("Credentials saved to keychain.");
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
// Fall back to file storage
|
|
272
|
+
saveConfig({ user, password });
|
|
273
|
+
console.log("");
|
|
274
|
+
console.warn("Warning: System keychain unavailable. Credentials stored in plaintext at ~/.hctl/config.json");
|
|
275
|
+
console.log("Credentials saved to config file.");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return { user, password };
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Get credentials, prompting if necessary
|
|
282
|
+
*/
|
|
283
|
+
export async function requireCredentials() {
|
|
284
|
+
const creds = await getCredentials();
|
|
285
|
+
if (creds) {
|
|
286
|
+
return { user: creds.user, password: creds.password };
|
|
287
|
+
}
|
|
288
|
+
return promptLogin();
|
|
289
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Table from "cli-table3";
|
|
2
|
+
export declare const colors: {
|
|
3
|
+
reset: string;
|
|
4
|
+
bold: string;
|
|
5
|
+
dim: string;
|
|
6
|
+
red: string;
|
|
7
|
+
green: string;
|
|
8
|
+
yellow: string;
|
|
9
|
+
blue: string;
|
|
10
|
+
magenta: string;
|
|
11
|
+
cyan: string;
|
|
12
|
+
white: string;
|
|
13
|
+
gray: string;
|
|
14
|
+
bgRed: string;
|
|
15
|
+
bgGreen: string;
|
|
16
|
+
bgYellow: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function colorize(text: string, color: keyof typeof colors): string;
|
|
19
|
+
export declare function success(message: string): string;
|
|
20
|
+
export declare function error(message: string): string;
|
|
21
|
+
export declare function warning(message: string): string;
|
|
22
|
+
export declare function info(message: string): string;
|
|
23
|
+
export declare function heading(text: string): string;
|
|
24
|
+
export declare function formatStatus(status: string): string;
|
|
25
|
+
export declare function formatBytes(bytes: number): string;
|
|
26
|
+
export declare function formatDate(dateStr: string): string;
|
|
27
|
+
export declare function formatDateTime(dateStr: string): string;
|
|
28
|
+
export declare function createTable(head: string[], colWidths?: number[]): Table.Table;
|
|
29
|
+
export declare function formatJson(data: unknown): string;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import Table from "cli-table3";
|
|
2
|
+
export const colors = {
|
|
3
|
+
reset: "\x1b[0m",
|
|
4
|
+
bold: "\x1b[1m",
|
|
5
|
+
dim: "\x1b[2m",
|
|
6
|
+
red: "\x1b[31m",
|
|
7
|
+
green: "\x1b[32m",
|
|
8
|
+
yellow: "\x1b[33m",
|
|
9
|
+
blue: "\x1b[34m",
|
|
10
|
+
magenta: "\x1b[35m",
|
|
11
|
+
cyan: "\x1b[36m",
|
|
12
|
+
white: "\x1b[37m",
|
|
13
|
+
gray: "\x1b[90m",
|
|
14
|
+
bgRed: "\x1b[41m",
|
|
15
|
+
bgGreen: "\x1b[42m",
|
|
16
|
+
bgYellow: "\x1b[43m",
|
|
17
|
+
};
|
|
18
|
+
export function colorize(text, color) {
|
|
19
|
+
return `${colors[color]}${text}${colors.reset}`;
|
|
20
|
+
}
|
|
21
|
+
export function success(message) {
|
|
22
|
+
return `${colors.green}✓${colors.reset} ${message}`;
|
|
23
|
+
}
|
|
24
|
+
export function error(message) {
|
|
25
|
+
return `${colors.red}✗${colors.reset} ${message}`;
|
|
26
|
+
}
|
|
27
|
+
export function warning(message) {
|
|
28
|
+
return `${colors.yellow}⚠${colors.reset} ${message}`;
|
|
29
|
+
}
|
|
30
|
+
export function info(message) {
|
|
31
|
+
return `${colors.blue}ℹ${colors.reset} ${message}`;
|
|
32
|
+
}
|
|
33
|
+
export function heading(text) {
|
|
34
|
+
return `\n${colors.bold}${colors.cyan}${text}${colors.reset}\n${"─".repeat(text.length)}`;
|
|
35
|
+
}
|
|
36
|
+
export function formatStatus(status) {
|
|
37
|
+
switch (status.toLowerCase()) {
|
|
38
|
+
case "ready":
|
|
39
|
+
case "active":
|
|
40
|
+
case "enabled":
|
|
41
|
+
case "success":
|
|
42
|
+
return colorize(status, "green");
|
|
43
|
+
case "installing":
|
|
44
|
+
case "in process":
|
|
45
|
+
case "running":
|
|
46
|
+
return colorize(status, "yellow");
|
|
47
|
+
case "maintenance":
|
|
48
|
+
case "disabled":
|
|
49
|
+
case "failed":
|
|
50
|
+
case "cancelled":
|
|
51
|
+
return colorize(status, "red");
|
|
52
|
+
default:
|
|
53
|
+
return status;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function formatBytes(bytes) {
|
|
57
|
+
if (bytes === 0) {
|
|
58
|
+
return "0 B";
|
|
59
|
+
}
|
|
60
|
+
const k = 1024;
|
|
61
|
+
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
62
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
63
|
+
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
|
64
|
+
}
|
|
65
|
+
export function formatDate(dateStr) {
|
|
66
|
+
if (!dateStr) {
|
|
67
|
+
return "-";
|
|
68
|
+
}
|
|
69
|
+
const date = new Date(dateStr);
|
|
70
|
+
return date.toLocaleDateString("en-US", {
|
|
71
|
+
year: "numeric",
|
|
72
|
+
month: "short",
|
|
73
|
+
day: "numeric",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function formatDateTime(dateStr) {
|
|
77
|
+
if (!dateStr) {
|
|
78
|
+
return "-";
|
|
79
|
+
}
|
|
80
|
+
const date = new Date(dateStr);
|
|
81
|
+
return date.toLocaleString("en-US", {
|
|
82
|
+
year: "numeric",
|
|
83
|
+
month: "short",
|
|
84
|
+
day: "numeric",
|
|
85
|
+
hour: "2-digit",
|
|
86
|
+
minute: "2-digit",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export function createTable(head, colWidths) {
|
|
90
|
+
const options = {
|
|
91
|
+
head: head.map((h) => colorize(h, "cyan")),
|
|
92
|
+
chars: {
|
|
93
|
+
top: "─",
|
|
94
|
+
"top-mid": "┬",
|
|
95
|
+
"top-left": "┌",
|
|
96
|
+
"top-right": "┐",
|
|
97
|
+
bottom: "─",
|
|
98
|
+
"bottom-mid": "┴",
|
|
99
|
+
"bottom-left": "└",
|
|
100
|
+
"bottom-right": "┘",
|
|
101
|
+
left: "│",
|
|
102
|
+
"left-mid": "├",
|
|
103
|
+
mid: "─",
|
|
104
|
+
"mid-mid": "┼",
|
|
105
|
+
right: "│",
|
|
106
|
+
"right-mid": "┤",
|
|
107
|
+
middle: "│",
|
|
108
|
+
},
|
|
109
|
+
style: {
|
|
110
|
+
"padding-left": 1,
|
|
111
|
+
"padding-right": 1,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
if (colWidths) {
|
|
115
|
+
options.colWidths = colWidths;
|
|
116
|
+
}
|
|
117
|
+
return new Table(options);
|
|
118
|
+
}
|
|
119
|
+
export function formatJson(data) {
|
|
120
|
+
return JSON.stringify(data, null, 2);
|
|
121
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { OptionValues } from "commander";
|
|
2
|
+
import { HetznerRobotClient } from "../robot/client.js";
|
|
3
|
+
export interface ActionOptions {
|
|
4
|
+
json?: boolean;
|
|
5
|
+
password?: string;
|
|
6
|
+
user?: string;
|
|
7
|
+
yes?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Handle errors from action wrappers consistently.
|
|
11
|
+
* Exits with code 0 for user-initiated prompt exits, code 1 for all other errors.
|
|
12
|
+
*/
|
|
13
|
+
export declare function handleActionError(err: unknown): never;
|
|
14
|
+
/**
|
|
15
|
+
* Split the arguments Commander passes to an action handler, which are
|
|
16
|
+
* `(...positionals, localOptions, command)`.
|
|
17
|
+
*
|
|
18
|
+
* Flags such as --json are declared on ancestor commands, so they are missing
|
|
19
|
+
* from `localOptions`. The returned `handlerArgs` carry the merged options in
|
|
20
|
+
* that slot instead.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveActionArgs(args: unknown[]): {
|
|
23
|
+
handlerArgs: unknown[];
|
|
24
|
+
options: OptionValues;
|
|
25
|
+
};
|
|
26
|
+
export declare function asyncAction<T extends unknown[]>(fn: (client: HetznerRobotClient, ...args: T) => Promise<void>): (...args: [...T, ...unknown[]]) => Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Output data as JSON or formatted table based on options.
|
|
29
|
+
*/
|
|
30
|
+
export declare function output<T>(data: T, formatter: (data: T) => string, options: {
|
|
31
|
+
json?: boolean;
|
|
32
|
+
}): void;
|
|
33
|
+
/**
|
|
34
|
+
* Confirm destructive action unless --yes flag is set.
|
|
35
|
+
* Returns true if confirmed, false if aborted.
|
|
36
|
+
*/
|
|
37
|
+
export declare function confirmAction(message: string, options: {
|
|
38
|
+
yes?: boolean;
|
|
39
|
+
}, defaultValue?: boolean): Promise<boolean>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { confirm } from "@inquirer/prompts";
|
|
3
|
+
import { HetznerRobotClient } from "../robot/client.js";
|
|
4
|
+
import { requireCredentials } from "./config.js";
|
|
5
|
+
import { error as fmtError, formatJson } from "./formatter.js";
|
|
6
|
+
/**
|
|
7
|
+
* Handle errors from action wrappers consistently.
|
|
8
|
+
* Exits with code 0 for user-initiated prompt exits, code 1 for all other errors.
|
|
9
|
+
*/
|
|
10
|
+
export function handleActionError(err) {
|
|
11
|
+
if (err instanceof Error) {
|
|
12
|
+
if (err.message.includes("ExitPromptError") ||
|
|
13
|
+
err.name === "ExitPromptError") {
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
console.error(fmtError(err.message));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.error(fmtError("An unknown error occurred"));
|
|
20
|
+
}
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Split the arguments Commander passes to an action handler, which are
|
|
25
|
+
* `(...positionals, localOptions, command)`.
|
|
26
|
+
*
|
|
27
|
+
* Flags such as --json are declared on ancestor commands, so they are missing
|
|
28
|
+
* from `localOptions`. The returned `handlerArgs` carry the merged options in
|
|
29
|
+
* that slot instead.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveActionArgs(args) {
|
|
32
|
+
const command = args.at(-1);
|
|
33
|
+
const options = command.optsWithGlobals();
|
|
34
|
+
return { handlerArgs: [...args.slice(0, -2), options], options };
|
|
35
|
+
}
|
|
36
|
+
export function asyncAction(fn) {
|
|
37
|
+
return async (...args) => {
|
|
38
|
+
const { handlerArgs, options: merged } = resolveActionArgs(args);
|
|
39
|
+
const options = merged;
|
|
40
|
+
try {
|
|
41
|
+
const { user } = options;
|
|
42
|
+
let password = options.password;
|
|
43
|
+
if (password === "-") {
|
|
44
|
+
try {
|
|
45
|
+
password = readFileSync(0, "utf-8").trim();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error("Failed to read password from stdin");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let apiClient;
|
|
52
|
+
if (user && password) {
|
|
53
|
+
apiClient = new HetznerRobotClient(user, password);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const creds = await requireCredentials();
|
|
57
|
+
apiClient = new HetznerRobotClient(creds.user, creds.password);
|
|
58
|
+
}
|
|
59
|
+
await fn(apiClient, ...handlerArgs);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
handleActionError(error);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Output data as JSON or formatted table based on options.
|
|
68
|
+
*/
|
|
69
|
+
export function output(data, formatter, options) {
|
|
70
|
+
console.log(options.json ? formatJson(data) : formatter(data));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Confirm destructive action unless --yes flag is set.
|
|
74
|
+
* Returns true if confirmed, false if aborted.
|
|
75
|
+
*/
|
|
76
|
+
export async function confirmAction(message, options, defaultValue = false) {
|
|
77
|
+
if (options.yes) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
const confirmed = await confirm({ message, default: defaultValue });
|
|
81
|
+
if (!confirmed) {
|
|
82
|
+
console.log("Aborted.");
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { confirm, input, password as passwordPrompt } from "@inquirer/prompts";
|
|
2
|
+
import { createContext, listContexts } from "../cloud/context.js";
|
|
3
|
+
import { HETZNER_CLOUD_CONSOLE_URL, printCloudTokenInstructions, } from "../cloud/token-instructions.js";
|
|
4
|
+
import { hasCredentials, promptLogin } from "./config.js";
|
|
5
|
+
import { heading, info, success } from "./formatter.js";
|
|
6
|
+
const ROBOT_URL = "https://robot.hetzner.com";
|
|
7
|
+
function printIntro() {
|
|
8
|
+
console.log("");
|
|
9
|
+
console.log(heading("hctl setup wizard"));
|
|
10
|
+
console.log("");
|
|
11
|
+
console.log("This wizard configures credentials for the two authenticated APIs:");
|
|
12
|
+
console.log("");
|
|
13
|
+
console.log(` Robot API ${ROBOT_URL}`);
|
|
14
|
+
console.log(" └─ dedicated server management; uses Robot web service credentials");
|
|
15
|
+
console.log(` Cloud API ${HETZNER_CLOUD_CONSOLE_URL}`);
|
|
16
|
+
console.log(" └─ cloud infrastructure; uses project-scoped Cloud API tokens");
|
|
17
|
+
console.log("");
|
|
18
|
+
console.log(info("Robot and Cloud credentials are created in different Hetzner UIs."));
|
|
19
|
+
console.log(info("Auction commands need no auth and are skipped by this wizard."));
|
|
20
|
+
}
|
|
21
|
+
async function setupRobot() {
|
|
22
|
+
if (await hasCredentials()) {
|
|
23
|
+
console.log("");
|
|
24
|
+
console.log(success("Robot API: credentials already configured — skipping. Run `hctl auth login` to replace."));
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
console.log("");
|
|
28
|
+
const proceed = await confirm({
|
|
29
|
+
message: "Set up Robot API credentials now?",
|
|
30
|
+
default: true,
|
|
31
|
+
});
|
|
32
|
+
if (!proceed) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
await promptLogin();
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
async function setupCloud() {
|
|
39
|
+
const existing = listContexts().map((c) => c.name);
|
|
40
|
+
if (existing.length > 0) {
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log(success(`Cloud API: ${existing.length} context(s) already configured — skipping. Run \`hctl cloud context create <name>\` to add another.`));
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
console.log("");
|
|
46
|
+
const proceed = await confirm({
|
|
47
|
+
message: "Set up Cloud API token now?",
|
|
48
|
+
default: true,
|
|
49
|
+
});
|
|
50
|
+
if (!proceed) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
printCloudTokenInstructions();
|
|
54
|
+
const name = await input({
|
|
55
|
+
message: "Context name (e.g. 'production', 'personal'):",
|
|
56
|
+
default: "default",
|
|
57
|
+
validate: (v) => {
|
|
58
|
+
if (v.length === 0) {
|
|
59
|
+
return "Context name is required";
|
|
60
|
+
}
|
|
61
|
+
if (existing.includes(v)) {
|
|
62
|
+
return `Context '${v}' already exists. Pick a different name.`;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const token = await passwordPrompt({
|
|
68
|
+
message: `Paste your Cloud API token for '${name}':`,
|
|
69
|
+
validate: (v) => v.length > 0 || "Token is required",
|
|
70
|
+
});
|
|
71
|
+
await createContext(name, token);
|
|
72
|
+
console.log("");
|
|
73
|
+
console.log(success(`Context '${name}' created and activated.`));
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
function printNextSteps() {
|
|
77
|
+
console.log("");
|
|
78
|
+
console.log(heading("Next steps"));
|
|
79
|
+
console.log("");
|
|
80
|
+
console.log(" hctl auth status # verify Robot auth");
|
|
81
|
+
console.log(" hctl auth test # test Robot API call");
|
|
82
|
+
console.log(" hctl cloud context active # show active Cloud context");
|
|
83
|
+
console.log(" hctl cloud server list # test Cloud API call");
|
|
84
|
+
console.log("");
|
|
85
|
+
}
|
|
86
|
+
export function registerInitCommand(parent) {
|
|
87
|
+
parent
|
|
88
|
+
.command("init")
|
|
89
|
+
.alias("setup")
|
|
90
|
+
.description("Walk through API credential setup for Robot and Cloud APIs")
|
|
91
|
+
.action(async () => {
|
|
92
|
+
try {
|
|
93
|
+
printIntro();
|
|
94
|
+
await setupRobot();
|
|
95
|
+
await setupCloud();
|
|
96
|
+
console.log("");
|
|
97
|
+
console.log(success("Setup complete."));
|
|
98
|
+
printNextSteps();
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (err instanceof Error && err.name === "ExitPromptError") {
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|