clever-tools 4.0.2 → 4.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/bin/clever.js CHANGED
@@ -103,10 +103,6 @@ async function run() {
103
103
  }),
104
104
  apiTokenId: cliparse.argument('api-token-id', { description: 'API token ID' }),
105
105
  apiTokenName: cliparse.argument('api-token-name', { description: 'API token name' }),
106
- ngId: cliparse.argument('id', {
107
- description: 'Network Group ID',
108
- parser: Parsers.ngResourceType,
109
- }),
110
106
  ngLabel: cliparse.argument('ng-label', {
111
107
  description: 'Network Group label',
112
108
  parser: Parsers.ngResourceType,
@@ -115,9 +111,6 @@ async function run() {
115
111
  description: 'Network Group ID or label',
116
112
  parser: Parsers.ngResourceType,
117
113
  }),
118
- ngDescription: cliparse.argument('ng-description', {
119
- description: 'Network Group description',
120
- }),
121
114
  ngExternalPeerLabel: cliparse.argument('external-peer-label', {
122
115
  description: 'External peer label',
123
116
  parser: Parsers.ngResourceType,
@@ -216,15 +209,6 @@ async function run() {
216
209
  "Comma separated list of members IDs to link to a Network Group ('app_xxx', 'addon_xxx', 'external_xxx')",
217
210
  parser: Parsers.commaSeparated,
218
211
  }),
219
- ngMemberLabel: cliparse.option('label', {
220
- required: false,
221
- metavar: 'member_label',
222
- description: 'The member label',
223
- parser: Parsers.ngResourceType,
224
- }),
225
- ngPeerGetConfig: cliparse.flag('config', {
226
- description: 'Get the Wireguard configuration of an external peer',
227
- }),
228
212
  ngResourceType: cliparse.option('type', {
229
213
  metavar: 'type',
230
214
  description: 'Type of resource to look for (NetworkGroup, Member, CleverPeer, ExternalPeer)',
@@ -265,10 +249,6 @@ async function run() {
265
249
  metavar: 'TEXT',
266
250
  description: 'Get only domains containing the provided text',
267
251
  }),
268
- naturalName: cliparse.flag('natural-name', {
269
- aliases: ['n'],
270
- description: 'Show the application names or aliases if possible',
271
- }),
272
252
  before: cliparse.option('before', {
273
253
  metavar: 'before',
274
254
  aliases: ['until'],
@@ -294,10 +274,6 @@ async function run() {
294
274
  metavar: 'tag',
295
275
  description: 'Tag to push (none by default)',
296
276
  }),
297
- databaseId: cliparse.option('database-id', {
298
- metavar: 'database_id',
299
- description: 'The Database ID (e.g.: postgresql_xxx)',
300
- }),
301
277
  deploymentId: cliparse.option('deployment-id', {
302
278
  metavar: 'deployment_id',
303
279
  description: 'Fetch logs for a given deployment',
@@ -534,45 +510,17 @@ async function run() {
534
510
  aliases: ['y'],
535
511
  description: 'Skip confirmation and remove all SSH keys directly',
536
512
  }),
537
- confirmTcpRedirCreation: cliparse.flag('yes', {
538
- aliases: ['y'],
539
- description: 'Skip confirmation even if the TCP redirection is not free',
540
- }),
541
513
  yes: cliparse.flag('yes', {
542
514
  aliases: ['y'],
543
515
  description: 'Skip confirmation',
544
516
  }),
545
517
  jsonFormat: cliparse.flag('json', { aliases: ['j'], description: 'Show result in JSON format' }),
546
518
  humanJsonOutputFormat: getOutputFormatOption(),
547
- tag: cliparse.option('tag', {
548
- required: true,
549
- metavar: 'tag',
550
- description: 'A tag',
551
- parser: Parsers.tag,
552
- }),
553
- tags: cliparse.option('tags', {
554
- required: true,
555
- metavar: 'tags',
556
- description: 'List of tags, separated by a comma',
557
- parser: Parsers.tags,
558
- }),
559
519
  optTags: cliparse.option('tags', {
560
520
  metavar: 'tags',
561
521
  description: 'List of tags, separated by a comma',
562
522
  parser: Parsers.tags,
563
523
  }),
564
- optIpAddress: cliparse.option('ip', {
565
- required: false,
566
- metavar: 'ip_address',
567
- description: 'An IP address',
568
- parser: Parsers.ipAddress,
569
- }),
570
- optPortNumber: cliparse.option('port', {
571
- required: false,
572
- metavar: 'port_number',
573
- description: 'A port number',
574
- parser: Parsers.portNumber,
575
- }),
576
524
  };
577
525
 
578
526
  // ACCESSLOGS COMMAND
@@ -855,6 +803,7 @@ async function run() {
855
803
  {
856
804
  description: 'Manage the favourite domain name for an application',
857
805
  options: [opts.alias, opts.appIdOrName],
806
+ privateOptions: [opts.humanJsonOutputFormat],
858
807
  commands: [domainSetFavouriteCommand, domainUnsetFavouriteCommand],
859
808
  },
860
809
  domain.getFavourite,
@@ -879,7 +828,7 @@ async function run() {
879
828
  'domain',
880
829
  {
881
830
  description: 'Manage domain names for an application',
882
- privateOptions: [opts.alias, opts.appIdOrName],
831
+ privateOptions: [opts.alias, opts.appIdOrName, opts.humanJsonOutputFormat],
883
832
  commands: [
884
833
  domainCreateCommand,
885
834
  domainFavouriteCommands,
@@ -1907,7 +1856,7 @@ async function run() {
1907
1856
  'add',
1908
1857
  {
1909
1858
  description: 'Add a new TCP redirection to the application',
1910
- options: [opts.namespace, opts.confirmTcpRedirCreation],
1859
+ options: [opts.namespace],
1911
1860
  },
1912
1861
  tcpRedirs.add,
1913
1862
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clever-tools",
3
- "version": "4.0.2",
3
+ "version": "4.1.0",
4
4
  "description": "Command Line Interface for Clever Cloud.",
5
5
  "keywords": [
6
6
  "cli",
@@ -47,7 +47,7 @@
47
47
  "typecheck": "tsc -p tsconfig.json"
48
48
  },
49
49
  "dependencies": {
50
- "@clevercloud/client": "11.0.1",
50
+ "@clevercloud/client": "11.2.0",
51
51
  "@inquirer/prompts": "7.8.4",
52
52
  "char-regex": "2.0.2",
53
53
  "cliparse": "0.5.0",
@@ -34,7 +34,7 @@ export async function accessLogs(params) {
34
34
  });
35
35
 
36
36
  if (format === 'human') {
37
- Logger.warn(styleText('yellow', '/!\\ Access Logs feature is in Alpha testing phase'));
37
+ Logger.println(styleText('yellow', '/!\\ This feature is in Beta testing phase'));
38
38
  }
39
39
 
40
40
  if (format === 'json' && !until) {
@@ -39,15 +39,24 @@ function getFavouriteDomain({ ownerId, appId }) {
39
39
  }
40
40
 
41
41
  export async function list(params) {
42
- const { alias, app: appIdOrName } = params.options;
42
+ const { alias, app: appIdOrName, format } = params.options;
43
43
  const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
44
44
 
45
45
  const app = await getApp({ id: ownerId, appId }).then(sendToApi);
46
46
  const favouriteDomain = await getFavouriteDomain({ ownerId, appId });
47
- return app.vhosts.forEach(({ fqdn }) => {
48
- const prefix = fqdn === favouriteDomain ? '* ' : ' ';
49
- Logger.println(prefix + fqdn);
50
- });
47
+
48
+ const domains = app.vhosts.map((vhost) => getDomainObject(vhost.fqdn, favouriteDomain));
49
+
50
+ switch (format) {
51
+ case 'json':
52
+ Logger.printJson(domains);
53
+ break;
54
+ default:
55
+ domains.forEach((domain) => {
56
+ Logger.println(`${domain.isFavourite ? '* ' : ' '}${domain.domainWithPathPrefix}`);
57
+ });
58
+ break;
59
+ }
51
60
  }
52
61
 
53
62
  export async function add(params) {
@@ -61,16 +70,23 @@ export async function add(params) {
61
70
  }
62
71
 
63
72
  export async function getFavourite(params) {
64
- const { alias, app: appIdOrName } = params.options;
73
+ const { alias, app: appIdOrName, format } = params.options;
65
74
  const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
66
75
 
67
76
  const favouriteDomain = await getFavouriteDomain({ ownerId, appId });
68
77
 
69
- if (favouriteDomain == null) {
70
- return Logger.println('No favourite domain set');
78
+ switch (format) {
79
+ case 'json':
80
+ const domain = getDomainObject(favouriteDomain, favouriteDomain);
81
+ Logger.printJson(domain);
82
+ break;
83
+ default:
84
+ if (favouriteDomain == null) {
85
+ return Logger.println('No favourite domain set');
86
+ }
87
+ Logger.println(favouriteDomain);
88
+ break;
71
89
  }
72
-
73
- return Logger.println(favouriteDomain);
74
90
  }
75
91
 
76
92
  export async function setFavourite(params) {
@@ -258,6 +274,21 @@ export async function overview(params) {
258
274
  }
259
275
  }
260
276
 
277
+ function getDomainObject(domainWithPathPrefix, favouriteDomain) {
278
+ const parsed = parseDomain(domainWithPathPrefix, { validateHostname: false });
279
+ return {
280
+ domainWithPathPrefix,
281
+ domain: parsed.domain,
282
+ domainWithoutSuffix: parsed.domainWithoutSuffix,
283
+ hostname: parsed.hostname,
284
+ publicSuffix: parsed.publicSuffix,
285
+ subdomain: parsed.subdomain,
286
+ isApex: parsed.subdomain === '',
287
+ pathPrefix: new URL('https://' + domainWithPathPrefix).pathname,
288
+ isFavourite: domainWithPathPrefix === favouriteDomain,
289
+ };
290
+ }
291
+
261
292
  /** @param {DomainDiag & { resolvedDnsConfig: ResolveDnsResult }} domainDiag */
262
293
  function reportDomainDiagnostics({ hostname, pathPrefix, resolvedDnsConfig, diagDetails, diagSummary }) {
263
294
  const validDiags = diagDetails.filter((diag) => diag.code === 'valid-a');
@@ -274,20 +305,20 @@ function reportDomainDiagnostics({ hostname, pathPrefix, resolvedDnsConfig, diag
274
305
 
275
306
  switch (diagSummary) {
276
307
  case 'managed':
277
- printlnWithIndent(styleText('green', '✔ Managed by Clever Cloud'), 2);
278
- printlnWithIndent('ⓘ cleverapps.io domains should only be used for testing purposes', 2);
308
+ Logger.printlnWithIndent(styleText('green', '✔ Managed by Clever Cloud'), 2);
309
+ Logger.printlnWithIndent('ⓘ cleverapps.io domains should only be used for testing purposes', 2);
279
310
  break;
280
311
  case 'no-config':
281
- printlnWithIndent(styleText('red', '✘ No DNS configuration found'), 2);
312
+ Logger.printlnWithIndent(styleText('red', '✘ No DNS configuration found'), 2);
282
313
  break;
283
314
  case 'valid':
284
- printlnWithIndent(styleText('green', '✔ Your configuration is valid'), 2);
315
+ Logger.printlnWithIndent(styleText('green', '✔ Your configuration is valid'), 2);
285
316
  break;
286
317
  case 'invalid':
287
- printlnWithIndent(styleText('red', '✘ Something is wrong with your configuration'), 2);
318
+ Logger.printlnWithIndent(styleText('red', '✘ Something is wrong with your configuration'), 2);
288
319
  break;
289
320
  case 'incomplete':
290
- printlnWithIndent(styleText('yellow', '⚠ Your configuration is incomplete'), 2);
321
+ Logger.printlnWithIndent(styleText('yellow', '⚠ Your configuration is incomplete'), 2);
291
322
  break;
292
323
  }
293
324
 
@@ -296,15 +327,15 @@ function reportDomainDiagnostics({ hostname, pathPrefix, resolvedDnsConfig, diag
296
327
  Logger.println('');
297
328
  validDiags.forEach((diag) => {
298
329
  const source = hasCnameRecord ? `(from CNAME ${resolvedDnsConfig.cnameRecords[0]}.)` : '';
299
- printlnWithIndent(`${diag.record.value} ${styleText('green', '✔ A Record OK')} ${source}`, 2);
330
+ Logger.printlnWithIndent(`${diag.record.value} ${styleText('green', '✔ A Record OK')} ${source}`, 2);
300
331
  });
301
332
  }
302
333
 
303
334
  // Replace A with CNAME
304
335
  if (diagSummary === 'valid' && suggestedCname != null) {
305
336
  Logger.println('');
306
- printlnWithIndent('ⓘ You can replace your A records with this CNAME:', 2);
307
- printlnWithIndent(suggestedCname.record.value, 6);
337
+ Logger.printlnWithIndent('ⓘ You can replace your A records with this CNAME:', 2);
338
+ Logger.printlnWithIndent(suggestedCname.record.value, 6);
308
339
  }
309
340
 
310
341
  // Replace A Records with CNAME
@@ -312,51 +343,51 @@ function reportDomainDiagnostics({ hostname, pathPrefix, resolvedDnsConfig, diag
312
343
  const cnameToUse = suggestedCname != null ? suggestedCname.record.value : missingCname.record.value;
313
344
 
314
345
  Logger.println('');
315
- printlnWithIndent('⇄ Replace your A records with this CNAME:', 2);
316
- printlnWithIndent(cnameToUse, 6);
346
+ Logger.printlnWithIndent('⇄ Replace your A records with this CNAME:', 2);
347
+ Logger.printlnWithIndent(cnameToUse, 6);
317
348
  Logger.println('');
318
- printlnWithIndent('or:', 2);
349
+ Logger.printlnWithIndent('or:', 2);
319
350
  }
320
351
 
321
352
  // Replace unknown CNAME with missing CNAME
322
353
  if (diagSummary === 'invalid' && missingCname != null && unknownCname != null) {
323
354
  Logger.println('');
324
- printlnWithIndent('➖Remove this CNAME record:', 2);
325
- printlnWithIndent(unknownCname.record.value + '. ', 6);
355
+ Logger.printlnWithIndent('➖Remove this CNAME record:', 2);
356
+ Logger.printlnWithIndent(unknownCname.record.value + '. ', 6);
326
357
  Logger.println('');
327
- printlnWithIndent('➕Add this CNAME record instead:', 2);
328
- printlnWithIndent(missingCname.record.value, 6);
358
+ Logger.printlnWithIndent('➕Add this CNAME record instead:', 2);
359
+ Logger.printlnWithIndent(missingCname.record.value, 6);
329
360
 
330
361
  if (hasARecords) {
331
362
  Logger.println('');
332
- printlnWithIndent('or:', 2);
363
+ Logger.printlnWithIndent('or:', 2);
333
364
  }
334
365
  }
335
366
 
336
367
  // Add CNAME
337
368
  if (diagSummary === 'no-config' && missingCname != null) {
338
369
  Logger.println('');
339
- printlnWithIndent('➕Add this CNAME record:', 2);
340
- printlnWithIndent(missingCname.record.value, 6);
370
+ Logger.printlnWithIndent('➕Add this CNAME record:', 2);
371
+ Logger.printlnWithIndent(missingCname.record.value, 6);
341
372
  }
342
373
 
343
374
  // Remove Unknown records
344
375
  if (unknownDiags.length > 0) {
345
376
  Logger.println('');
346
- printlnWithIndent('➖Remove these A records:', 2);
377
+ Logger.printlnWithIndent('➖Remove these A records:', 2);
347
378
 
348
379
  unknownDiags.forEach((diag) => {
349
380
  const source = hasCnameRecord ? `(from CNAME ${resolvedDnsConfig.cnameRecords[0]}.)` : '';
350
- printlnWithIndent(`${diag.record.value} ${source}`, 6);
381
+ Logger.printlnWithIndent(`${diag.record.value} ${source}`, 6);
351
382
  });
352
383
  }
353
384
 
354
385
  // Add missing A records
355
386
  if (missingDiags.length > 0) {
356
387
  Logger.println('');
357
- printlnWithIndent('➕Add these A records:', 2);
388
+ Logger.printlnWithIndent('➕Add these A records:', 2);
358
389
 
359
- missingDiags.forEach((missingDiag) => printlnWithIndent(missingDiag.record.value, 6));
390
+ missingDiags.forEach((missingDiag) => Logger.printlnWithIndent(missingDiag.record.value, 6));
360
391
  }
361
392
  }
362
393
 
@@ -375,16 +406,6 @@ function getParsedDomains(vhosts) {
375
406
  });
376
407
  }
377
408
 
378
- /**
379
- * Prints a line of text with specified indentation.
380
- *
381
- * @param {string} text - The text to be printed.
382
- * @param {number} indentLevel - The number of spaces to indent the text.
383
- */
384
- function printlnWithIndent(text, indentLevel) {
385
- Logger.println(' '.repeat(indentLevel) + text);
386
- }
387
-
388
409
  function recursiveSort(obj) {
389
410
  if (typeof obj === 'object' && obj.appId != null) {
390
411
  return obj;
@@ -407,15 +428,15 @@ function recursiveSort(obj) {
407
428
 
408
429
  function recursiveDisplay(obj, indentLevel = 0) {
409
430
  if (typeof obj === 'object' && obj.appId != null) {
410
- printlnWithIndent(`${obj.ownerName} | ${obj.appName} (${obj.appVariantSlug})`, indentLevel);
411
- printlnWithIndent(styleText('blue', obj.appConsoleUrl), indentLevel);
431
+ Logger.printlnWithIndent(`${obj.ownerName} | ${obj.appName} (${obj.appVariantSlug})`, indentLevel);
432
+ Logger.printlnWithIndent(styleText('blue', obj.appConsoleUrl), indentLevel);
412
433
  return;
413
434
  }
414
435
 
415
436
  for (const [propertyPath, subObj] of Object.entries(obj)) {
416
437
  if (propertyPath !== '/') {
417
438
  Logger.println('');
418
- printlnWithIndent(styleText('yellow', propertyPath), indentLevel);
439
+ Logger.printlnWithIndent(styleText('yellow', propertyPath), indentLevel);
419
440
  recursiveDisplay(subObj, indentLevel + 2);
420
441
  } else {
421
442
  recursiveDisplay(subObj, indentLevel);
@@ -1,13 +1,12 @@
1
1
  import {
2
2
  todo_addEmailAddress as addEmailAddress,
3
- todo_getEmailAddresses as getEmailAddresses,
4
3
  todo_removeEmailAddress as removeEmailAddress,
5
4
  } from '@clevercloud/client/esm/api/v2/user.js';
6
5
  import { confirm } from '../lib/prompts.js';
7
6
  import { styleText } from '../lib/style-text.js';
8
7
  import { Logger } from '../logger.js';
8
+ import { getUserEmailAddresses } from '../models/emails.js';
9
9
  import { sendToApi } from '../models/send-to-api.js';
10
- import * as User from '../models/user.js';
11
10
  import { openBrowser } from '../models/utils.js';
12
11
 
13
12
  /**
@@ -155,16 +154,3 @@ export async function removeAllSecondary(params) {
155
154
  export function openConsole() {
156
155
  return openBrowser('/users/me/emails', 'Opening the email addresses management page of the Console in your browser');
157
156
  }
158
-
159
- /**
160
- * Get the primary and secondary email addresses of the current user
161
- * @returns {Promise<{ primary: string, secondary: string[] }>} The primary and secondary email addresses of the current user
162
- */
163
- async function getUserEmailAddresses() {
164
- const currentUser = await User.getCurrent();
165
- const secondaryAddresses = await getEmailAddresses().then(sendToApi);
166
- return {
167
- primary: currentUser.email,
168
- secondary: secondaryAddresses.sort(),
169
- };
170
- }
@@ -6,15 +6,15 @@ export async function link(params) {
6
6
  const [app] = params.args;
7
7
  const { org: orgaIdOrName, alias } = params.options;
8
8
 
9
+ let appConfigEntry;
9
10
  if (app.app_id != null && orgaIdOrName != null) {
10
11
  Logger.warn("You've specified a unique application ID, organisation option will be ignored");
11
- await Application.linkRepo(app, null, alias);
12
+ appConfigEntry = await Application.linkRepo(app, null, alias);
12
13
  } else {
13
- await Application.linkRepo(app, orgaIdOrName, alias);
14
+ appConfigEntry = await Application.linkRepo(app, orgaIdOrName, alias);
14
15
  }
15
16
 
16
- const linkedMessage = alias ? ` to local alias ${styleText('green', alias)}` : '';
17
17
  Logger.printSuccess(
18
- `Application ${styleText('green', app.app_name || app.app_id)} has been successfully linked${linkedMessage}!`,
18
+ `Application ${styleText('green', appConfigEntry.app_id)} has been successfully linked to local alias ${styleText('green', appConfigEntry.alias)}!`,
19
19
  );
20
20
  }
@@ -1,14 +1,11 @@
1
- import {
2
- todo_addSshKey as addSshKey,
3
- todo_getSshKeys as getSshKeys,
4
- todo_removeSshKey as removeSshKey,
5
- } from '@clevercloud/client/esm/api/v2/user.js';
1
+ import { todo_addSshKey as addSshKey, todo_removeSshKey as removeSshKey } from '@clevercloud/client/esm/api/v2/user.js';
6
2
  import dedent from 'dedent';
7
3
  import fs from 'node:fs';
8
4
  import { confirm } from '../lib/prompts.js';
9
5
  import { styleText } from '../lib/style-text.js';
10
6
  import { Logger } from '../logger.js';
11
7
  import { sendToApi } from '../models/send-to-api.js';
8
+ import { getUserSshKeys } from '../models/ssh-keys.js';
12
9
  import { openBrowser } from '../models/utils.js';
13
10
 
14
11
  /**
@@ -145,11 +142,3 @@ export async function removeAll(params) {
145
142
  export function openConsole() {
146
143
  return openBrowser('/users/me/ssh-keys', 'Opening the SSH keys management page of the Console in your browser');
147
144
  }
148
-
149
- /**
150
- * @return {Promise<Array<{ name: string, key: string, fingerprint: string }>>}
151
- */
152
- async function getUserSshKeys() {
153
- const rawKeys = await getSshKeys().then(sendToApi);
154
- return rawKeys.sort((a, b) => a.name.localeCompare(b.name));
155
- }
@@ -27,6 +27,7 @@ export async function status(params) {
27
27
  : styleText(['bold', 'red'], 'stopped');
28
28
 
29
29
  Logger.println(`${status.name}: ${statusMessage}`);
30
+ Logger.println(`Type: ${status.type.name}`);
30
31
  Logger.println(`Executed as: ${styleText('bold', status.lifetime)}`);
31
32
  if (status.deploymentInProgress) {
32
33
  Logger.println(
@@ -67,6 +68,10 @@ function computeStatus(instances, app) {
67
68
  const status = {
68
69
  id: app.id,
69
70
  name: app.name,
71
+ type: {
72
+ name: app.instance.variant.name,
73
+ slug: app.instance.variant.slug,
74
+ },
70
75
  lifetime: app.instance.lifetime,
71
76
  status: isUp ? 'running' : 'stopped',
72
77
  commit: upCommit,
@@ -1,6 +1,4 @@
1
1
  import { addTcpRedir, getTcpRedirs, removeTcpRedir } from '@clevercloud/client/esm/api/v2/application.js';
2
- import { confirm } from '../lib/prompts.js';
3
- import { styleText } from '../lib/style-text.js';
4
2
  import { Logger } from '../logger.js';
5
3
  import * as Application from '../models/application.js';
6
4
  import * as Namespaces from '../models/namespaces.js';
@@ -64,37 +62,10 @@ export async function list(params) {
64
62
  }
65
63
  }
66
64
 
67
- async function acceptPayment(result, skipConfirmation) {
68
- if (!skipConfirmation) {
69
- result.lines.forEach(({ description, VAT, price }) =>
70
- Logger.println(`${description}\tVAT: ${VAT}%\tPrice: ${price}€`),
71
- );
72
- Logger.println(`Total (without taxes): ${result.totalHT}€`);
73
- Logger.println(styleText('bold', `Total (with taxes): ${result.totalTTC}€`));
74
-
75
- await confirm(
76
- `You're about to pay ${result.totalTTC}€, confirm?`,
77
- 'No confirmation, aborting TCP redirection creation',
78
- );
79
- }
80
- }
81
-
82
65
  export async function add(params) {
83
- const { alias, app: appIdOrName, namespace, yes: skipConfirmation } = params.options;
66
+ const { alias, app: appIdOrName, namespace } = params.options;
84
67
  const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
85
-
86
- const { port } = await addTcpRedir({ id: ownerId, appId }, { namespace })
87
- .then(sendToApi)
88
- .catch((error) => {
89
- if (error.status === 402) {
90
- return acceptPayment(error.response.body, skipConfirmation).then(() => {
91
- return addTcpRedir({ id: ownerId, appId, payment: 'accepted' }, { namespace }).then(sendToApi);
92
- });
93
- } else {
94
- throw error;
95
- }
96
- });
97
-
68
+ const { port } = await addTcpRedir({ id: ownerId, appId }, { namespace }).then(sendToApi);
98
69
  Logger.println('Successfully added tcp redirection on port: ' + port);
99
70
  }
100
71
 
@@ -1,5 +1,6 @@
1
1
  import dedent from 'dedent';
2
2
  import { createApiToken, deleteApiToken, listApiTokens } from '../clever-client/auth-bridge.js';
3
+ import { formatDate } from '../lib/format-date.js';
3
4
  import { promptSecret } from '../lib/prompts.js';
4
5
  import { styleText } from '../lib/style-text.js';
5
6
  import { Logger } from '../logger.js';
@@ -141,7 +142,3 @@ export async function revoke(params) {
141
142
 
142
143
  Logger.println(styleText('green', '✔'), 'API token successfully revoked!');
143
144
  }
144
-
145
- function formatDate(dateInput) {
146
- return new Date(dateInput).toISOString().substring(0, 16).replace('T', ' ');
147
- }
@@ -0,0 +1,3 @@
1
+ export function formatDate(dateInput) {
2
+ return new Date(dateInput).toISOString().substring(0, 16).replace('T', ' ');
3
+ }
package/src/logger.js CHANGED
@@ -58,6 +58,16 @@ export const Logger = _(['debug', 'info', 'warn', 'error'])
58
58
  // No decoration for Logger.println
59
59
  Logger.println = console.log;
60
60
 
61
+ /**
62
+ * Prints a line of text with specified indentation.
63
+ *
64
+ * @param {string} text - The text to be printed.
65
+ * @param {number} indentLevel - The number of spaces to indent the text.
66
+ */
67
+ Logger.printlnWithIndent = (text, indentLevel) => {
68
+ Logger.println(' '.repeat(indentLevel) + text);
69
+ };
70
+
61
71
  // Logger for success with a green check before the message
62
72
  Logger.printSuccess = (message) => console.log(`${styleText(['bold', 'green'], '✓')} ${message}`);
63
73
 
@@ -2,6 +2,7 @@ import _ from 'lodash';
2
2
  import { promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { slugify } from '../lib/slugify.js';
5
+ import { styleText } from '../lib/style-text.js';
5
6
  import { Logger } from '../logger.js';
6
7
  import { conf } from './configuration.js';
7
8
  import * as User from './user.js';
@@ -39,14 +40,17 @@ export async function addLinkedApplication(appData, alias, ignoreParentConfig) {
39
40
  };
40
41
 
41
42
  const isPresent = currentConfig.apps.find((app) => app.app_id === appEntry.app_id) != null;
42
-
43
- // ToDo see what to do when there is a conflict between an existing entry
44
- // and the entry we want to add (same app_id, different other values)
45
- if (!isPresent) {
46
- currentConfig.apps.push(appEntry);
43
+ if (isPresent) {
44
+ throw new Error(
45
+ `Application ${styleText('red', appEntry.app_id)} is already linked with alias ${styleText('red', appEntry.alias)}`,
46
+ );
47
47
  }
48
48
 
49
- return persistConfig(currentConfig);
49
+ currentConfig.apps.push(appEntry);
50
+
51
+ return persistConfig(currentConfig).then(() => {
52
+ return appEntry;
53
+ });
50
54
  }
51
55
 
52
56
  export async function removeLinkedApplication({ appId, alias }) {
@@ -0,0 +1,16 @@
1
+ import { todo_getEmailAddresses as getEmailAddresses } from '@clevercloud/client/esm/api/v2/user.js';
2
+ import { sendToApi } from './send-to-api.js';
3
+ import * as User from './user.js';
4
+
5
+ /**
6
+ * Get the primary and secondary email addresses of the current user
7
+ * @returns {Promise<{ primary: string, secondary: string[] }>} The primary and secondary email addresses of the current user
8
+ */
9
+ export async function getUserEmailAddresses() {
10
+ const currentUser = await User.getCurrent();
11
+ const secondaryAddresses = await getEmailAddresses().then(sendToApi);
12
+ return {
13
+ primary: currentUser.email,
14
+ secondary: secondaryAddresses.sort(),
15
+ };
16
+ }
@@ -17,7 +17,8 @@ const THROTTLE_PER_IN_MILLISECONDS = 100;
17
17
 
18
18
  const retryConfiguration = {
19
19
  enabled: true,
20
- maxRetryCount: 6,
20
+ initRetryTimeout: 3000,
21
+ maxRetryCount: 10,
21
22
  };
22
23
 
23
24
  export async function displayLogs(params) {
@@ -0,0 +1,10 @@
1
+ import { todo_getSshKeys as getSshKeys } from '@clevercloud/client/esm/api/v2/user.js';
2
+ import { sendToApi } from './send-to-api.js';
3
+
4
+ /**
5
+ * @return {Promise<Array<{ name: string, key: string, fingerprint: string }>>}
6
+ */
7
+ export async function getUserSshKeys() {
8
+ const rawKeys = await getSshKeys().then(sendToApi);
9
+ return rawKeys.sort((a, b) => a.name.localeCompare(b.name));
10
+ }