@peerbit/server 6.0.41 → 7.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 (84) hide show
  1. package/dist/src/cli.d.ts.map +1 -1
  2. package/dist/src/cli.js +161 -352
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/client.d.ts +1 -1
  5. package/dist/src/client.d.ts.map +1 -1
  6. package/dist/src/client.js +2 -5
  7. package/dist/src/client.js.map +1 -1
  8. package/dist/src/docker.d.ts +70 -1
  9. package/dist/src/docker.d.ts.map +1 -1
  10. package/dist/src/docker.js +559 -14
  11. package/dist/src/docker.js.map +1 -1
  12. package/dist/src/domain-lease.d.ts +126 -0
  13. package/dist/src/domain-lease.d.ts.map +1 -0
  14. package/dist/src/domain-lease.js +800 -0
  15. package/dist/src/domain-lease.js.map +1 -0
  16. package/dist/src/domain.d.ts +50 -2
  17. package/dist/src/domain.d.ts.map +1 -1
  18. package/dist/src/domain.js +484 -67
  19. package/dist/src/domain.js.map +1 -1
  20. package/dist/src/hetzner.d.ts +4 -21
  21. package/dist/src/hetzner.d.ts.map +1 -1
  22. package/dist/src/hetzner.js +4 -116
  23. package/dist/src/hetzner.js.map +1 -1
  24. package/dist/src/nginx-template.conf +19 -1
  25. package/dist/src/remotes.browser.d.ts +4 -1
  26. package/dist/src/remotes.browser.d.ts.map +1 -1
  27. package/dist/src/remotes.browser.js +1 -2
  28. package/dist/src/remotes.browser.js.map +1 -1
  29. package/dist/src/remotes.d.ts +2 -1
  30. package/dist/src/remotes.d.ts.map +1 -1
  31. package/dist/src/remotes.js +1 -0
  32. package/dist/src/remotes.js.map +1 -1
  33. package/dist/src/server.d.ts.map +1 -1
  34. package/dist/src/server.js +3 -5
  35. package/dist/src/server.js.map +1 -1
  36. package/dist/test/api.spec.js +2 -1
  37. package/dist/test/api.spec.js.map +1 -1
  38. package/dist/test/cli.spec.js +48 -1
  39. package/dist/test/cli.spec.js.map +1 -1
  40. package/dist/test/docker.spec.d.ts +2 -0
  41. package/dist/test/docker.spec.d.ts.map +1 -0
  42. package/dist/test/docker.spec.js +822 -0
  43. package/dist/test/docker.spec.js.map +1 -0
  44. package/dist/test/domain-lease.spec.d.ts +2 -0
  45. package/dist/test/domain-lease.spec.d.ts.map +1 -0
  46. package/dist/test/domain-lease.spec.js +530 -0
  47. package/dist/test/domain-lease.spec.js.map +1 -0
  48. package/dist/test/domain.integration.spec.js +358 -75
  49. package/dist/test/domain.integration.spec.js.map +1 -1
  50. package/dist/test/remotes.spec.d.ts +2 -0
  51. package/dist/test/remotes.spec.d.ts.map +1 -0
  52. package/dist/test/remotes.spec.js +15 -0
  53. package/dist/test/remotes.spec.js.map +1 -0
  54. package/dist/tsconfig.tsbuildinfo +1 -1
  55. package/dist/ui/assets/index-CjZwCewh.js +10 -0
  56. package/dist/ui/assets/index-e3g42MbC.js +9 -0
  57. package/dist/ui/index.html +1 -1
  58. package/package.json +14 -19
  59. package/src/cli.ts +204 -408
  60. package/src/client.ts +2 -6
  61. package/src/docker.ts +841 -20
  62. package/src/domain-lease.ts +1118 -0
  63. package/src/domain.ts +650 -90
  64. package/src/hetzner.ts +4 -184
  65. package/src/nginx-template.conf +19 -1
  66. package/src/remotes.browser.ts +7 -1
  67. package/src/remotes.ts +6 -1
  68. package/src/server.ts +3 -5
  69. package/dist/src/aws.browser.d.ts +0 -2
  70. package/dist/src/aws.browser.d.ts.map +0 -1
  71. package/dist/src/aws.browser.js +0 -3
  72. package/dist/src/aws.browser.js.map +0 -1
  73. package/dist/src/aws.d.ts +0 -30
  74. package/dist/src/aws.d.ts.map +0 -1
  75. package/dist/src/aws.js +0 -240
  76. package/dist/src/aws.js.map +0 -1
  77. package/dist/test/launch.spec.d.ts +0 -2
  78. package/dist/test/launch.spec.d.ts.map +0 -1
  79. package/dist/test/launch.spec.js +0 -23
  80. package/dist/test/launch.spec.js.map +0 -1
  81. package/dist/ui/assets/index-BBTKOM0z.js +0 -6
  82. package/dist/ui/assets/index-DVtj_GOf.js +0 -13
  83. package/src/aws.browser.ts +0 -1
  84. package/src/aws.ts +0 -327
package/src/cli.ts CHANGED
@@ -7,17 +7,10 @@ import chalk from "chalk";
7
7
  import fs from "fs";
8
8
  import sodium from "libsodium-wrappers";
9
9
  import path from "path";
10
- import { exit } from "process";
11
10
  import readline from "readline";
12
11
  import Table from "tty-table";
13
12
  import type { Argv } from "yargs";
14
- import {
15
- AWS_LINUX_ARM_AMIs,
16
- createRecord,
17
- launchNodes,
18
- terminateNode,
19
- } from "./aws.js";
20
- import { createClient, waitForDomain } from "./client.js";
13
+ import { createClient } from "./client.js";
21
14
  import {
22
15
  getHomeConfigDir,
23
16
  getKeypair,
@@ -25,17 +18,24 @@ import {
25
18
  getRemotesPath,
26
19
  } from "./config.js";
27
20
  import {
28
- createTestDomain,
29
- getDomainFromConfig,
30
- loadConfig,
31
- startCertbot,
32
- } from "./domain.js";
21
+ DNS_LEASE_ACCESS_TOKEN_ENV,
22
+ DNS_LEASE_SERVICE_URL_ENV,
23
+ DNS_LEASE_STATE_FILE_ENV,
24
+ getDnsLeaseStatePath,
25
+ provisionDnsLease,
26
+ readDnsLeaseState,
27
+ releaseDnsLease,
28
+ renewDnsLease,
29
+ startDnsLeaseRenewal,
30
+ } from "./domain-lease.js";
31
+ import { getDomainFromConfig, loadConfig, startCertbot } from "./domain.js";
32
+ import { terminateNode as terminateHetznerNode } from "./hetzner.js";
33
33
  import {
34
- HETZNER_SERVER_TYPES,
35
- launchNodes as launchHetznerNodes,
36
- terminateNode as terminateHetznerNode,
37
- } from "./hetzner.js";
38
- import { DEFAULT_REMOTE_GROUP, type RemoteObject, Remotes } from "./remotes.js";
34
+ DEFAULT_REMOTE_GROUP,
35
+ type RemoteObject,
36
+ Remotes,
37
+ getRetiredAWSManagementError,
38
+ } from "./remotes.js";
39
39
  import { LOCAL_API_PORT } from "./routes.js";
40
40
  import { startServerWithNode } from "./server.js";
41
41
  import type { InstallDependency, StartProgram } from "./types.js";
@@ -178,20 +178,43 @@ export const cli = async (args?: string[]) => {
178
178
  "Set Libp2p listen port. Only modify this when testing locally, since NGINX config depends on the default value",
179
179
  type: "number",
180
180
  default: undefined,
181
+ })
182
+ .option("dns-lease-state-file", {
183
+ describe: `Managed DNS lease state file (or ${DNS_LEASE_STATE_FILE_ENV})`,
184
+ type: "string",
181
185
  });
182
186
  return yargs;
183
187
  },
184
188
  handler: async (args) => {
185
- await startServerWithNode({
186
- directory: args.directory,
187
- domain: await loadConfig().then((config) =>
188
- config ? getDomainFromConfig(config) : undefined,
189
- ),
190
- ports: { api: args["port-api"], node: args["port-node"] },
191
- bootstrap: args.bootstrap,
192
- newSession: args.reset,
193
- grantAccess: args["grant-access"],
194
- });
189
+ let stopDnsLeaseRenewal: () => void = () => undefined;
190
+ try {
191
+ stopDnsLeaseRenewal = await startDnsLeaseRenewal({
192
+ statePath:
193
+ (args["dns-lease-state-file"] as string | undefined) ||
194
+ process.env[DNS_LEASE_STATE_FILE_ENV],
195
+ onError: (message) => console.warn(message),
196
+ });
197
+ } catch {
198
+ console.warn(
199
+ "Managed DNS lease renewal could not start; use 'peerbit domain lease renew' to inspect it",
200
+ );
201
+ }
202
+ try {
203
+ const started = await startServerWithNode({
204
+ directory: args.directory,
205
+ domain: await loadConfig().then((config) =>
206
+ config ? getDomainFromConfig(config) : undefined,
207
+ ),
208
+ ports: { api: args["port-api"], node: args["port-node"] },
209
+ bootstrap: args.bootstrap,
210
+ newSession: args.reset,
211
+ grantAccess: args["grant-access"],
212
+ });
213
+ started.server.once("close", stopDnsLeaseRenewal);
214
+ } catch (error) {
215
+ stopDnsLeaseRenewal();
216
+ throw error;
217
+ }
195
218
  },
196
219
  })
197
220
  .command({
@@ -214,103 +237,153 @@ export const cli = async (args?: string[]) => {
214
237
  })
215
238
  .command(
216
239
  "domain",
217
- "Setup a domain and certificate for this node",
240
+ "Configure a domain and certificate for this node",
218
241
  (yargs) => {
219
242
  yargs
220
243
  .command({
221
- command: "test",
222
- describe:
223
- "Setup a testing domain with SSL (no guarantess on how long the domain will be available)",
224
- builder: {
225
- email: {
226
- describe: "Email for Lets security messages",
227
- type: "string",
228
- demandOption: true,
229
- },
230
- outdir: {
231
- describe: "Output path for Nginx config",
232
- type: "string",
233
- alias: "o",
234
- },
235
- wait: {
236
- alias: "w",
237
- describe: "Wait for setup to succeed (or fail)",
238
- type: "boolean",
239
- default: false,
240
- },
241
- },
244
+ command: "lease [operation]",
245
+ describe: "Manage a temporary peerchecker.com DNS lease",
246
+ builder: (leaseArgs: Argv) =>
247
+ leaseArgs
248
+ .positional("operation", {
249
+ choices: ["claim", "renew", "release", "status"] as const,
250
+ default: "claim",
251
+ describe: "Lease operation",
252
+ type: "string",
253
+ })
254
+ .option("service-url", {
255
+ describe: `Lease service URL (or ${DNS_LEASE_SERVICE_URL_ENV})`,
256
+ type: "string",
257
+ })
258
+ .option("access-token", {
259
+ describe: `Claim access token (prefer ${DNS_LEASE_ACCESS_TOKEN_ENV} to avoid shell history)`,
260
+ type: "string",
261
+ })
262
+ .option("address", {
263
+ describe: "Public IPv4 or IPv6 address for a new lease",
264
+ type: "string",
265
+ })
266
+ .option("email", {
267
+ describe: "Email for Let's Encrypt security messages",
268
+ type: "string",
269
+ })
270
+ .option("configure", {
271
+ default: true,
272
+ describe: "Configure NGINX and Let's Encrypt after claiming",
273
+ type: "boolean",
274
+ })
275
+ .option("wait", {
276
+ alias: "w",
277
+ default: true,
278
+ describe: "Wait for HTTPS setup to succeed",
279
+ type: "boolean",
280
+ })
281
+ .option("state-file", {
282
+ default: getDnsLeaseStatePath(),
283
+ describe: "Managed DNS lease state file",
284
+ type: "string",
285
+ }),
242
286
  handler: async (args) => {
243
- const domain = await createTestDomain();
244
- await startCertbot(domain, args.email, args.wait);
245
- exit();
287
+ const operation = args.operation || "claim";
288
+ const statePath = args["state-file"] as string;
289
+ const serviceUrl =
290
+ (args["service-url"] as string | undefined) ||
291
+ process.env[DNS_LEASE_SERVICE_URL_ENV];
292
+ if (operation === "status") {
293
+ const state = readDnsLeaseState(statePath);
294
+ console.log(
295
+ JSON.stringify(
296
+ state
297
+ ? {
298
+ address: state.address,
299
+ configuredAt: state.configuredAt,
300
+ domain: state.domain,
301
+ expiresAt: state.expiresAt,
302
+ status: state.status,
303
+ }
304
+ : { status: "none" },
305
+ undefined,
306
+ 2,
307
+ ),
308
+ );
309
+ return;
310
+ }
311
+ if (operation === "renew") {
312
+ const active = await renewDnsLease({ statePath, serviceUrl });
313
+ console.log(`DNS lease renewed until ${active.expiresAt}`);
314
+ return;
315
+ }
316
+ if (operation === "release") {
317
+ await releaseDnsLease({ statePath, serviceUrl });
318
+ console.log("DNS lease released");
319
+ return;
320
+ }
321
+ if (args.configure && !args.email) {
322
+ throw new Error(
323
+ "--email is required when configuring the leased domain",
324
+ );
325
+ }
326
+ const active = await provisionDnsLease({
327
+ accessToken:
328
+ (args["access-token"] as string | undefined) ||
329
+ process.env[DNS_LEASE_ACCESS_TOKEN_ENV],
330
+ address: args.address as string | undefined,
331
+ configure: args.configure
332
+ ? (domain) =>
333
+ startCertbot(domain, args.email as string, args.wait)
334
+ : undefined,
335
+ serviceUrl,
336
+ statePath,
337
+ });
338
+ console.log(`DNS lease active until ${active.expiresAt}`);
246
339
  },
247
340
  })
248
341
  .command({
249
- command: "aws",
342
+ command: "configure <domain>",
250
343
  describe:
251
- "Setup a domain with an AWS account. You either have to setup you AWS credentials in the .aws folder, or pass the credentials in the cli",
344
+ "Configure NGINX and Let's Encrypt for a domain you control",
252
345
  builder: {
253
346
  domain: {
254
- describe: "domain, e.g. abc.example.com, example.com",
255
- alias: "d",
347
+ describe:
348
+ "Domain whose DNS record already points to this server",
256
349
  type: "string",
257
350
  demandOption: true,
258
351
  },
259
- hostedZoneId: {
260
- describe: 'The id of the hosted zone "HostedZoneId"',
261
- alias: "hz",
262
- type: "string",
263
- require: true,
264
- },
265
- accessKeyId: {
266
- describe: "Access key id of the AWS user",
267
- alias: "ak",
268
- type: "string",
269
- },
270
- region: {
271
- describe: "AWS region",
272
- alias: "r",
273
- type: "string",
274
- },
275
- secretAccessKey: {
276
- describe: "Secret key id of the AWS user",
277
- alias: "sk",
278
- type: "string",
279
- },
280
- outdir: {
281
- describe: "Output path for Nginx config",
352
+ email: {
353
+ describe: "Email for Let's Encrypt security messages",
282
354
  type: "string",
283
- alias: "o",
355
+ demandOption: true,
284
356
  },
285
357
  wait: {
286
358
  alias: "w",
287
- describe: "Wait for setup to succeed (or fail)",
359
+ describe:
360
+ "Wait for HTTPS setup to succeed (use --no-wait to return early)",
288
361
  type: "boolean",
289
- default: false,
362
+ default: true,
290
363
  },
291
364
  },
292
365
  handler: async (args) => {
293
- if (
294
- !!args.accessKeyId !== !!args.secretAccessKey ||
295
- !!args.region !== !!args.secretAccessKey
296
- ) {
297
- throw new Error(
298
- "Expecting either all 'accessKeyId', 'region' and 'secretAccessKey' to be provided or none",
299
- );
300
- }
301
- await createRecord({
302
- domain: args.domain,
303
- hostedZoneId: args.hostedZoneId,
304
- region: args.region,
305
- credentials: args.accessKeyId
306
- ? {
307
- accessKeyId: args.accessKeyId,
308
- secretAccessKey: args.secretAccessKey,
309
- }
310
- : undefined,
311
- });
312
366
  await startCertbot(args.domain, args.email, args.wait);
313
- exit();
367
+ },
368
+ })
369
+ .command({
370
+ command: "test",
371
+ describe: false,
372
+ builder: (retiredArgs: Argv) => retiredArgs.strict(false),
373
+ handler: () => {
374
+ throw new Error(
375
+ "Automatic test domains have been retired. Point a domain you control to this server, then run 'peerbit domain configure <domain> --email <email>'.",
376
+ );
377
+ },
378
+ })
379
+ .command({
380
+ command: "aws",
381
+ describe: false,
382
+ builder: (retiredArgs: Argv) => retiredArgs.strict(false),
383
+ handler: () => {
384
+ throw new Error(
385
+ "Automatic AWS DNS configuration has been retired. Configure DNS with your provider, then run 'peerbit domain configure <domain> --email <email>'.",
386
+ );
314
387
  },
315
388
  })
316
389
  .strict()
@@ -319,301 +392,21 @@ export const cli = async (args?: string[]) => {
319
392
  )
320
393
  .command("remote", "Handle remote nodes", (innerYargs) => {
321
394
  innerYargs
322
- .command("spawn", "Spawn remote nodes", (spawnYargs) => {
323
- spawnYargs
324
- .command({
325
- command: "aws",
326
- describe: "Spawn remote nodes on AWS",
327
- builder: (awsArgs: Argv) => {
328
- awsArgs.option("count", {
329
- describe: "Amount of nodes to spawn",
330
- defaultDescription: "One node",
331
- type: "number",
332
- alias: "c",
333
- default: 1,
334
- });
335
- awsArgs.option("region", {
336
- describe: "Region",
337
- type: "string",
338
- defaultDescription: "Region defined in ~.aws/config",
339
- choices: Object.keys(AWS_LINUX_ARM_AMIs),
340
- });
341
- awsArgs.option("group", {
342
- describe: "Remote group to launch nodes in",
343
- type: "string",
344
- alias: "g",
345
- default: DEFAULT_REMOTE_GROUP,
346
- });
347
- awsArgs.option("size", {
348
- describe: "Instance size",
349
- type: "string",
350
- alias: "s",
351
- choices: [
352
- "micro",
353
- "small",
354
- "medium",
355
- "large",
356
- "xlarge",
357
- "2xlarge",
358
- ],
359
- default: "micro",
360
- });
361
-
362
- awsArgs.option("name", {
363
- describe: "Name prefix for spawned nodes",
364
- type: "string",
365
- alias: "n",
366
- default: "peerbit-node",
367
- });
368
- awsArgs.option("grant-access", {
369
- describe: "Grant access to public keys on start",
370
- defaultDescription:
371
- "The publickey of this device located in 'directory'",
372
- type: "string",
373
- array: true,
374
- alias: "ga",
375
- });
376
- awsArgs.option("directory", {
377
- describe: "Peerbit directory",
378
- defaultDescription: "~.peerbit",
379
- type: "string",
380
- alias: "d",
381
- default: getHomeConfigDir(),
382
- });
383
- awsArgs.option("email", {
384
- describe: "Email for Let's security messages",
385
- type: "string",
386
- alias: "e",
387
- demandOption: true,
388
- });
389
- awsArgs.option("server-version", {
390
- describe:
391
- "@peerbit/server version or tag to install on the instance (e.g. 5.7.0-58d3d09)",
392
- type: "string",
393
- alias: ["sv"],
394
- });
395
- return awsArgs;
396
- },
397
- handler: async (args) => {
398
- const self = (
399
- await getKeypair(args.directory)
400
- ).publicKey.toPeerId();
401
- const accessGrant: PeerId[] =
402
- args["grant-access"]?.length > 0
403
- ? (args["grant-access"] as string[]).map((x) =>
404
- peerIdFromString(x),
405
- )
406
- : [];
407
- accessGrant.push(self);
408
- const nodes = await launchNodes({
409
- email: args.email as string,
410
- count: args.count,
411
- namePrefix: args.name,
412
- region: args.region,
413
- grantAccess: accessGrant,
414
- size: args.size,
415
- serverVersion:
416
- (args["server-version"] as string) || undefined,
417
- });
418
-
419
- console.log(
420
- `Waiting for ${args.count} ${
421
- args.count > 1 ? "nodes" : "node"
422
- } to spawn. This might take a few minutes. You can watch the progress in your AWS console.`,
423
- );
424
- const twirlTimer = (function () {
425
- const P = ["\\", "|", "/", "-"];
426
- let x = 0;
427
- return setInterval(function () {
428
- process.stdout.write(
429
- "\r" + "Loading: " + chalk.hex(colors[x])(P[x++]),
430
- );
431
- x &= 3;
432
- }, 250);
433
- })();
434
- for (const node of nodes) {
435
- try {
436
- const domain = await waitForDomain(node.publicIp);
437
- const remotes = new Remotes(getRemotesPath(args.directory));
438
- remotes.add({
439
- name: node.name,
440
- address: domain,
441
- group: args.group,
442
- origin: {
443
- type: "aws",
444
- instanceId: node.instanceId,
445
- region: node.region,
446
- },
447
- });
448
- } catch (error: any) {
449
- process.stdout.write("\r");
450
- console.error(
451
- `Error waiting for domain for ip: ${
452
- node.publicIp
453
- } to be available: ${error?.toString()}`,
454
- );
455
- }
456
- }
457
- process.stdout.write("\r");
458
- clearInterval(twirlTimer);
459
- console.log(`New nodes available (${nodes.length}):`);
460
- for (const node of nodes) {
461
- console.log(chalk.green(node.name));
462
- }
463
- },
464
- })
465
- .command({
466
- command: "hetzner",
467
- describe: "Spawn remote nodes on Hetzner Cloud",
468
- builder: (hetznerArgs: Argv) => {
469
- hetznerArgs.option("count", {
470
- describe: "Amount of nodes to spawn",
471
- defaultDescription: "One node",
472
- type: "number",
473
- alias: "c",
474
- default: 1,
475
- });
476
- hetznerArgs.option("location", {
477
- describe: "Location (e.g. fsn1, nbg1, hel1, ash, hil)",
478
- type: "string",
479
- alias: "l",
480
- default: "fsn1",
481
- });
482
- hetznerArgs.option("group", {
483
- describe: "Remote group to launch nodes in",
484
- type: "string",
485
- alias: "g",
486
- default: DEFAULT_REMOTE_GROUP,
487
- });
488
- hetznerArgs.option("server-type", {
489
- describe: "Server type",
490
- type: "string",
491
- alias: "t",
492
- choices: [...HETZNER_SERVER_TYPES],
493
- default: "cx11",
494
- });
495
-
496
- hetznerArgs.option("name", {
497
- describe: "Name prefix for spawned nodes",
498
- type: "string",
499
- alias: "n",
500
- default: "peerbit-node",
501
- });
502
- hetznerArgs.option("grant-access", {
503
- describe: "Grant access to public keys on start",
504
- defaultDescription:
505
- "The publickey of this device located in 'directory'",
506
- type: "string",
507
- array: true,
508
- alias: "ga",
509
- });
510
- hetznerArgs.option("directory", {
511
- describe: "Peerbit directory",
512
- defaultDescription: "~.peerbit",
513
- type: "string",
514
- alias: "d",
515
- default: getHomeConfigDir(),
516
- });
517
- hetznerArgs.option("email", {
518
- describe: "Email for Let's security messages",
519
- type: "string",
520
- alias: "e",
521
- demandOption: true,
522
- });
523
- hetznerArgs.option("token", {
524
- describe: "Hetzner Cloud API token (or set HCLOUD_TOKEN)",
525
- type: "string",
526
- alias: ["tok"],
527
- });
528
- hetznerArgs.option("image", {
529
- describe: "Image to use (e.g. ubuntu-22.04)",
530
- type: "string",
531
- default: "ubuntu-22.04",
532
- });
533
- hetznerArgs.option("server-version", {
534
- describe:
535
- "@peerbit/server version or tag to install on the instance (e.g. 5.7.0-58d3d09)",
536
- type: "string",
537
- alias: ["sv"],
538
- });
539
- return hetznerArgs;
540
- },
541
- handler: async (args) => {
542
- const self = (
543
- await getKeypair(args.directory)
544
- ).publicKey.toPeerId();
545
- const accessGrant: PeerId[] =
546
- args["grant-access"]?.length > 0
547
- ? (args["grant-access"] as string[]).map((x) =>
548
- peerIdFromString(x),
549
- )
550
- : [];
551
- accessGrant.push(self);
552
- const nodes = await launchHetznerNodes({
553
- token: (args.token as string) || undefined,
554
- email: args.email as string,
555
- count: args.count,
556
- namePrefix: args.name,
557
- location: args.location,
558
- serverType: args["server-type"],
559
- grantAccess: accessGrant,
560
- image: args.image,
561
- serverVersion:
562
- (args["server-version"] as string) || undefined,
563
- });
564
-
565
- console.log(
566
- `Waiting for ${args.count} ${
567
- args.count > 1 ? "nodes" : "node"
568
- } to spawn. This might take a few minutes. You can watch the progress in your Hetzner Cloud console.`,
569
- );
570
- const twirlTimer = (function () {
571
- const P = ["\\", "|", "/", "-"];
572
- let x = 0;
573
- return setInterval(function () {
574
- process.stdout.write(
575
- "\r" + "Loading: " + chalk.hex(colors[x])(P[x++]),
576
- );
577
- x &= 3;
578
- }, 250);
579
- })();
580
- for (const node of nodes) {
581
- try {
582
- const domain = await waitForDomain(node.publicIp);
583
- const remotes = new Remotes(getRemotesPath(args.directory));
584
- remotes.add({
585
- name: node.name,
586
- address: domain,
587
- group: args.group,
588
- origin: {
589
- type: "hetzner",
590
- serverId: node.serverId,
591
- location: node.location,
592
- },
593
- });
594
- } catch (error: any) {
595
- process.stdout.write("\r");
596
- console.error(
597
- `Error waiting for domain for ip: ${
598
- node.publicIp
599
- } to be available: ${error?.toString()}`,
600
- );
601
- }
602
- }
603
- process.stdout.write("\r");
604
- clearInterval(twirlTimer);
605
- console.log(`New nodes available (${nodes.length}):`);
606
- for (const node of nodes) {
607
- console.log(chalk.green(node.name));
608
- }
609
- },
610
- })
611
- .strict()
612
- .demandCommand();
395
+ .command({
396
+ command: "spawn [provider]",
397
+ describe: false,
398
+ builder: (spawnArgs: Argv) =>
399
+ spawnArgs.positional("provider", { type: "string" }).strict(false),
400
+ handler: () => {
401
+ throw new Error(
402
+ "Automatic cloud provisioning has been retired. Provision a server with your provider, configure a domain you control, then register it with 'peerbit remote add <name> <address>'.",
403
+ );
404
+ },
613
405
  })
614
406
  .command({
615
407
  command: "terminate [name...]",
616
- describe: "Terminate remote instances that was previously spawned",
408
+ describe:
409
+ "Terminate legacy Hetzner instances recorded by older releases",
617
410
  builder: (killArgs: Argv) => {
618
411
  killArgs.option("all", {
619
412
  describe: "Kill all nodes",
@@ -644,19 +437,22 @@ export const cli = async (args?: string[]) => {
644
437
  handler: async (args) => {
645
438
  const remotes = new Remotes(getRemotesPath(args.directory));
646
439
  const allRemotes = await remotes.all();
647
- for (const remote of allRemotes) {
648
- if (args.all || args.name.includes(remote.name)) {
649
- if (remote.origin?.type === "aws") {
650
- await terminateNode({
651
- instanceId: remote.origin.instanceId,
652
- region: remote.origin.region,
653
- });
654
- } else if (remote.origin?.type === "hetzner") {
655
- await terminateHetznerNode({
656
- serverId: remote.origin.serverId,
657
- token: (args.token as string) || undefined,
658
- });
659
- }
440
+ const selectedRemotes = allRemotes.filter(
441
+ (remote) => args.all || args.name.includes(remote.name),
442
+ );
443
+ const retiredAWSRemote = selectedRemotes.find(
444
+ (remote) => remote.origin?.type === "aws",
445
+ );
446
+ if (retiredAWSRemote?.origin?.type === "aws") {
447
+ throw getRetiredAWSManagementError(retiredAWSRemote.origin);
448
+ }
449
+
450
+ for (const remote of selectedRemotes) {
451
+ if (remote.origin?.type === "hetzner") {
452
+ await terminateHetznerNode({
453
+ serverId: remote.origin.serverId,
454
+ token: (args.token as string) || undefined,
455
+ });
660
456
  }
661
457
  }
662
458
  },