clever-tools 3.6.1 → 3.8.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 (45) hide show
  1. package/README.md +1 -7
  2. package/bin/clever.js +99 -47
  3. package/package.json +48 -50
  4. package/src/command-options.js +22 -3
  5. package/src/commands/accesslogs.js +114 -34
  6. package/src/commands/activity.js +70 -16
  7. package/src/commands/addon.js +153 -75
  8. package/src/commands/applications.js +83 -1
  9. package/src/commands/cancel-deploy.js +3 -3
  10. package/src/commands/config.js +6 -7
  11. package/src/commands/console.js +16 -4
  12. package/src/commands/curl.js +13 -16
  13. package/src/commands/delete.js +16 -6
  14. package/src/commands/deploy.js +9 -6
  15. package/src/commands/diag.js +68 -28
  16. package/src/commands/domain.js +13 -13
  17. package/src/commands/drain.js +42 -27
  18. package/src/commands/env.js +53 -24
  19. package/src/commands/logs.js +3 -3
  20. package/src/commands/notify-email.js +33 -18
  21. package/src/commands/open.js +3 -3
  22. package/src/commands/profile.js +29 -9
  23. package/src/commands/published-config.js +26 -15
  24. package/src/commands/restart.js +12 -5
  25. package/src/commands/scale.js +2 -3
  26. package/src/commands/service.js +42 -15
  27. package/src/commands/ssh.js +3 -3
  28. package/src/commands/status.js +72 -46
  29. package/src/commands/stop.js +3 -3
  30. package/src/commands/tcp-redirs.js +54 -18
  31. package/src/commands/webhooks.js +28 -13
  32. package/src/logger.js +9 -6
  33. package/src/models/addon.js +21 -12
  34. package/src/models/app_configuration.js +11 -3
  35. package/src/models/application.js +106 -7
  36. package/src/models/exit-strategy-option.js +24 -0
  37. package/src/models/json-array.js +28 -0
  38. package/src/models/log-v4.js +9 -29
  39. package/src/models/log.js +9 -2
  40. package/src/models/namespaces.js +20 -0
  41. package/src/models/organisation.js +0 -20
  42. package/src/models/utils.js +8 -1
  43. package/src/models/accesslogs.js +0 -54
  44. package/vendors/README_VENDORS.md +0 -18
  45. package/vendors/curlconverter-parse.js +0 -3709
@@ -1,53 +1,133 @@
1
1
  'use strict';
2
2
 
3
- const { getAccessLogsFromWarp10InBatches, getContinuousAccessLogsFromWarp10 } = require('@clevercloud/client/cjs/access-logs.js');
4
- const { getWarp10AccessLogsToken } = require('@clevercloud/client/cjs/api/v2/warp-10.js');
5
- const { ONE_HOUR_MICROS, ONE_SECOND_MICROS, toMicroTimestamp } = require('@clevercloud/client/cjs/utils/date.js');
6
-
7
- const Addon = require('../models/addon.js');
8
- const AppConfig = require('../models/app_configuration.js');
3
+ const Application = require('../models/application.js');
9
4
  const Logger = require('../logger.js');
10
- const { getFormatter } = require('../models/accesslogs.js');
11
- const { sendToApi, sendToWarp10 } = require('../models/send-to-api.js');
5
+ const { getHostAndTokens } = require('../models/send-to-api.js');
6
+ const { ApplicationAccessLogStream } = require('@clevercloud/client/cjs/streams/access-logs.js');
7
+ const { JsonArray } = require('../models/json-array.js');
8
+ const colors = require('colors/safe');
9
+ const formatTable = require('../format-table.js');
10
+ const { truncateWithEllipsis } = require('../models/utils.js');
12
11
 
13
- const CONTINUOUS_DELAY = ONE_SECOND_MICROS * 5;
12
+ // 2000 logs per 100ms maximum
13
+ const THROTTLE_ELEMENTS = 2000;
14
+ const THROTTLE_PER_IN_MILLISECONDS = 100;
15
+ const CITY_MAX_LENGTH = 20;
14
16
 
15
17
  async function accessLogs (params) {
16
- const { alias, format, before, after, addon: addonId, follow } = params.options;
17
18
 
18
- const { ownerId, appId, realAddonId } = await getIds(addonId, alias);
19
- const to = (before != null) ? toMicroTimestamp(before.toISOString()) : toMicroTimestamp();
20
- const from = (after != null) ? toMicroTimestamp(after.toISOString()) : to - ONE_HOUR_MICROS;
21
- const warpToken = await getWarp10AccessLogsToken({ orgaId: ownerId }).then(sendToApi);
19
+ // TODO: drop when add-ons are supported in API
20
+ if (params.options.addon) {
21
+ throw new Error('Access Logs are not available for add-ons yet');
22
+ }
23
+
24
+ const { apiHost, tokens } = await getHostAndTokens();
25
+ const { alias, app: appIdOrName, format, before: until, after: since } = params.options;
26
+ const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
27
+
28
+ const stream = new ApplicationAccessLogStream({
29
+ apiHost,
30
+ tokens,
31
+ ownerId,
32
+ appId,
33
+ since,
34
+ until,
35
+ throttleElements: THROTTLE_ELEMENTS,
36
+ throttlePerInMilliseconds: THROTTLE_PER_IN_MILLISECONDS,
37
+ });
38
+
39
+ if (format === 'human') {
40
+ Logger.warn(colors.yellow('/!\\ Access Logs feature is in Alpha testing phase'));
41
+ }
22
42
 
23
- if (follow && (before != null || after != null)) {
24
- Logger.warn('Access logs are displayed continuously with -f/--follow therefore --before and --after are ignored.');
43
+ if (format === 'json' && (!until)) {
44
+ throw new Error('JSON format only works with a limiting parameter such as `before`');
25
45
  }
26
46
 
27
- const emitter = follow
28
- ? getContinuousAccessLogsFromWarp10({ appId, realAddonId, warpToken, delay: CONTINUOUS_DELAY }, sendToWarp10)
29
- : getAccessLogsFromWarp10InBatches({ appId, realAddonId, from, to, warpToken }, sendToWarp10);
47
+ // used for 'json' format
48
+ const jsonArray = new JsonArray();
30
49
 
31
- const formatLogLine = getFormatter(format, addonId != null);
50
+ stream
51
+ .on('open', (event) => {
52
+ Logger.debug(colors.blue(`Logs stream (open) ${JSON.stringify({ appId })}`));
53
+ if (format === 'json') {
54
+ jsonArray.open();
55
+ }
56
+ })
57
+ .on('error', (event) => {
58
+ Logger.debug(colors.red(`Logs stream (error) ${event.error.message}`));
59
+ })
60
+ .onLog((log) => {
61
+ switch (format) {
62
+ case 'json':
63
+ jsonArray.push(log);
64
+ break;
65
+ case 'json-stream':
66
+ Logger.printJson(log);
67
+ break;
68
+ case 'human':
69
+ default:
70
+ // when the connection is cut too early, or for TCP redirections, we don't have HTTP section
71
+ if (log.http == null) {
72
+ break;
73
+ }
32
74
 
33
- emitter.on('data', (data) => {
34
- data.forEach((l) => Logger.println(formatLogLine(l)));
35
- });
75
+ Logger.println(formatHuman(log));
76
+ break;
77
+ }
78
+ });
36
79
 
37
- return new Promise((resolve, reject) => {
38
- emitter.on('error', reject);
39
- });
80
+ // Properly close the stream
81
+ process.once('SIGINT', (signal) => stream.close(signal));
82
+
83
+ const closeReason = await stream.start();
84
+
85
+ if (format === 'json') {
86
+ jsonArray.close();
87
+ }
88
+
89
+ Logger.debug(`stream closed: ${closeReason?.type}`);
90
+ }
91
+
92
+ function formatHuman (log) {
93
+ const { date, http, source } = log;
94
+ const country = source.countryCode ?? '(unknown)';
95
+ const hasSourceCity = source.city ?? '';
96
+
97
+ return row([[
98
+ colors.grey(date.toISOString(date)),
99
+ source.ip,
100
+ `${country}${hasSourceCity ? '/' + truncateWithEllipsis(CITY_MAX_LENGTH, source.city) : ''}`,
101
+ colorStatusCode(http.response.statusCode),
102
+ http.request.method.toString().padEnd(4, ' ') + ' ' + http.request.path,
103
+ ]]);
40
104
  }
41
105
 
42
- async function getIds (addonId, alias) {
43
- if (addonId != null) {
44
- const addon = await Addon.findById(addonId);
45
- return {
46
- ownerId: addon.orgaId,
47
- realAddonId: addon.realId,
48
- };
106
+ const row = formatTable([
107
+ '2024-06-24T08:05:43.880Z',
108
+ '255.255.255.255',
109
+ // country / city
110
+ 2 + 1 + CITY_MAX_LENGTH,
111
+ 'XXX',
112
+ // longest method name
113
+ 'OPTIONS',
114
+ // path
115
+ ]);
116
+
117
+ function colorStatusCode (code) {
118
+ if (code >= 500) {
119
+ return colors.red(code);
120
+ }
121
+ if (code >= 400) {
122
+ return colors.yellow(code);
123
+ }
124
+ if (code >= 300) {
125
+ return colors.blue(code);
126
+ }
127
+ if (code >= 200) {
128
+ return colors.green(code);
49
129
  }
50
- return AppConfig.getAppDetails({ alias });
130
+ return code;
51
131
  }
52
132
 
53
133
  module.exports = { accessLogs };
@@ -4,12 +4,12 @@ const colors = require('colors/safe');
4
4
  const moment = require('moment');
5
5
 
6
6
  const Activity = require('../models/activity.js');
7
- const AppConfig = require('../models/app_configuration.js');
8
7
  const formatTable = require('../format-table');
9
8
  const Logger = require('../logger.js');
10
9
  const { Deferred } = require('../models/utils.js');
11
10
  const { EventsStream } = require('@clevercloud/client/cjs/streams/events.node.js');
12
11
  const { getHostAndTokens } = require('../models/send-to-api.js');
12
+ const Application = require('../models/application.js');
13
13
 
14
14
  function getColoredState (state, isLast) {
15
15
  if (state === 'OK') {
@@ -31,24 +31,37 @@ function getColoredState (state, isLast) {
31
31
  // We use examples of maximum width text to have a clean display
32
32
  const formatActivityTable = formatTable([
33
33
  moment().format(),
34
+ 47,
34
35
  'IN PROGRESS',
35
36
  'downscale',
36
- // a git commit id is 40 chars long
37
- 40,
37
+ // a git commit id is 8 chars long
38
+ 8,
38
39
  0,
39
40
  ]);
40
41
 
42
+ function convertEventToJson (event) {
43
+ return {
44
+ uuid: event.uuid,
45
+ date: event.date,
46
+ state: event.state,
47
+ action: event.action,
48
+ commit: event.commit,
49
+ cause: event.cause,
50
+ };
51
+ }
52
+
41
53
  function formatActivityLine (event) {
42
54
  return formatActivityTable([
43
55
  [
44
56
  moment(event.date).format(),
57
+ event.uuid,
45
58
  getColoredState(event.state, event.isLast),
46
59
  event.action,
47
- event.commit || 'not specified',
60
+ event?.commit?.substring(0, 8) ?? 'N/A',
48
61
  event.cause,
49
62
  ],
50
63
  ]);
51
- };
64
+ }
52
65
 
53
66
  function isTemporaryEvent (ev) {
54
67
  if (ev == null) {
@@ -65,28 +78,65 @@ function clearPreviousLine () {
65
78
  }
66
79
  }
67
80
 
68
- function handleEvent (previousEvent, event) {
81
+ function handleEvent (previousEvent, event, handler) {
69
82
  if (isTemporaryEvent(previousEvent)) {
70
- clearPreviousLine();
83
+ handler.pop();
71
84
  }
72
85
 
73
- const activityLine = formatActivityLine(event);
74
- Logger.println(activityLine);
86
+ handler.print(event);
75
87
 
76
88
  return event;
77
89
  }
78
90
 
79
- function onEvent (previousEvent, newEvent) {
80
- const { event, date, data: { state, action, commit, cause } } = newEvent;
91
+ function onEvent (previousEvent, newEvent, handler) {
92
+ const { event, date, data: { uuid, state, action, commit, cause } } = newEvent;
81
93
  if (event !== 'DEPLOYMENT_ACTION_BEGIN' && event !== 'DEPLOYMENT_ACTION_END') {
82
94
  return previousEvent;
83
95
  }
84
- return handleEvent(previousEvent, { date, state, action, commit, cause, isLast: true });
96
+ return handleEvent(previousEvent, { date, uuid, state, action, commit, cause, isLast: true }, handler);
97
+ }
98
+
99
+ function getEventHandler (format, follow) {
100
+ switch (format) {
101
+ case 'json': {
102
+ if (follow) {
103
+ throw new Error('The `follow` option and "json" format are not compatible. Use "json-stream" format instead.');
104
+ }
105
+ else {
106
+ const buf = [];
107
+
108
+ return {
109
+ print: (event) => buf.push(convertEventToJson(event)),
110
+ pop: () => buf.pop(),
111
+ end: () => {
112
+ Logger.printJson(buf);
113
+ },
114
+ };
115
+ }
116
+ }
117
+ case 'json-stream': {
118
+ return {
119
+ print: (event) => {
120
+ Logger.println(JSON.stringify(convertEventToJson(event)));
121
+ },
122
+ pop: clearPreviousLine,
123
+ end: () => {},
124
+ };
125
+ }
126
+ case 'human':
127
+ default: {
128
+ return {
129
+ print: (event) => Logger.println(formatActivityLine(event)),
130
+ pop: clearPreviousLine,
131
+ end: () => {},
132
+ };
133
+ }
134
+ }
85
135
  }
86
136
 
87
137
  async function activity (params) {
88
- const { alias, 'show-all': showAll, follow } = params.options;
89
- const { ownerId, appId } = await AppConfig.getAppDetails({ alias });
138
+ const { alias, app: appIdOrName, 'show-all': showAll, follow, format } = params.options;
139
+ const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
90
140
  const events = await Activity.list(ownerId, appId, showAll);
91
141
  const reversedArrayWithIndex = events
92
142
  .reverse()
@@ -94,9 +144,13 @@ async function activity (params) {
94
144
  const isLast = index === all.length - 1;
95
145
  return ({ ...event, isLast });
96
146
  });
97
- let lastEvent = reversedArrayWithIndex.reduce(handleEvent, {});
147
+
148
+ const handler = getEventHandler(format, follow);
149
+
150
+ let lastEvent = reversedArrayWithIndex.reduce((previousEvent, newEvent) => handleEvent(previousEvent, newEvent, handler), {});
98
151
 
99
152
  if (!follow) {
153
+ handler.end();
100
154
  return lastEvent;
101
155
  }
102
156
 
@@ -108,7 +162,7 @@ async function activity (params) {
108
162
  eventsStream
109
163
  .on('open', () => Logger.debug('WS for events (open) ' + JSON.stringify({ appId })))
110
164
  .on('event', (event) => {
111
- lastEvent = onEvent(lastEvent, event);
165
+ lastEvent = onEvent(lastEvent, event, handler);
112
166
  return lastEvent;
113
167
  })
114
168
  .on('ping', () => Logger.debug('WS for events (ping)'))
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const _ = require('lodash');
4
3
  const colors = require('colors/safe');
5
4
 
6
5
  const Addon = require('../models/addon.js');
@@ -15,22 +14,6 @@ const { sendToApi } = require('../models/send-to-api.js');
15
14
  const { toNameEqualsValueString } = require('@clevercloud/client/cjs/utils/env-vars.js');
16
15
  const { resolveAddonId } = require('../models/ids-resolver.js');
17
16
 
18
- function getPlan (inputPlan, providerName) {
19
-
20
- // Return user plan
21
- if (inputPlan !== '') {
22
- return inputPlan;
23
- }
24
-
25
- // Choose a default plan based on the add-on provider
26
- switch (providerName) {
27
- case 'kv':
28
- return 'alpha';
29
- default:
30
- return 'dev';
31
- }
32
- }
33
-
34
17
  async function list (params) {
35
18
  const { org: orgaIdOrName, format } = params.options;
36
19
 
@@ -74,7 +57,7 @@ async function create (params) {
74
57
  const [providerName, name] = params.args;
75
58
  const {
76
59
  link: linkedAppAlias,
77
- plan: inputPlan,
60
+ plan: planName,
78
61
  region,
79
62
  yes: skipConfirmation,
80
63
  org: orgaIdOrName,
@@ -82,7 +65,6 @@ async function create (params) {
82
65
  } = params.options;
83
66
  const version = params.options['addon-version'];
84
67
  const addonOptions = parseAddonOptions(params.options.option);
85
- const planName = getPlan(inputPlan, providerName);
86
68
 
87
69
  const ownerId = (orgaIdOrName != null)
88
70
  ? await Organisation.getId(orgaIdOrName)
@@ -118,6 +100,45 @@ async function create (params) {
118
100
  }
119
101
 
120
102
  function displayAddon (format, addon, providerName, message) {
103
+
104
+ const WIP_PROVIDERS = {
105
+ keycloak: {
106
+ statut: 'alpha',
107
+ postCreateInstructions: [
108
+ 'Learn more about Keycloak on Clever Cloud: https://developers.clever-cloud.com/doc/addons/keycloak/',
109
+ ].join('\n'),
110
+ },
111
+ kv: {
112
+ status: 'alpha',
113
+ postCreateInstructions: [
114
+ colors.yellow('You can easily use Materia KV with \'redis-cli\', with such commands:'),
115
+ colors.blue(`source <(clever addon env ${addon.id} -F shell)`),
116
+ colors.blue('redis-cli -h $KV_HOST -p $KV_PORT --tls'),
117
+ 'Learn more about Materia KV on Clever Cloud: https://developers.clever-cloud.com/doc/addons/materia-kv/',
118
+ ].join('\n'),
119
+ },
120
+ 'addon-pulsar': {
121
+ status: 'beta',
122
+ postCreateInstructions: [
123
+ 'Learn more about Pulsar on Clever Cloud: https://developers.clever-cloud.com/doc/addons/pulsar/',
124
+ ].join('\n'),
125
+ },
126
+ };
127
+
128
+ let providerNameToShow = '';
129
+ let statusMessage = '';
130
+ if (providerName in WIP_PROVIDERS) {
131
+
132
+ providerNameToShow = providerName === 'kv'
133
+ ? 'Materia KV'
134
+ : providerName;
135
+
136
+ statusMessage = `The ${providerNameToShow} provider is in ${WIP_PROVIDERS[providerName].status} testing phase`;
137
+ statusMessage += WIP_PROVIDERS[providerName].status === 'alpha'
138
+ ? '. Don\'t store sensitive or production grade data.'
139
+ : '';
140
+ }
141
+
121
142
  switch (format) {
122
143
 
123
144
  case 'json': {
@@ -126,8 +147,9 @@ function displayAddon (format, addon, providerName, message) {
126
147
  realId: addon.realId,
127
148
  name: addon.name,
128
149
  };
129
- Logger.printJson((providerName === 'kv')
130
- ? { ...jsonAddon, availability: 'alpha', warning: 'Don\'t store sensitive or production grade data' }
150
+
151
+ Logger.printJson((WIP_PROVIDERS[providerName] != null)
152
+ ? { ...jsonAddon, availability: WIP_PROVIDERS[providerName].status, message: statusMessage }
131
153
  : jsonAddon);
132
154
  break;
133
155
  }
@@ -140,15 +162,12 @@ function displayAddon (format, addon, providerName, message) {
140
162
  `Real ID: ${addon.realId}`,
141
163
  `Name: ${addon.name}`,
142
164
  ].join('\n'));
143
- if (providerName === 'kv') {
144
- const materiaMessage = [
145
- '',
146
- colors.yellow(`/!\\ The MateriaDB ${providerName.toUpperCase()} provider is in Alpha testing phase, don't store sensitive or production grade data`),
147
- 'You can easily use MateriaDB KV with \'redis-cli\', with such commands:',
148
- colors.blue(`source <(clever addon env ${addon.id} -F shell)`),
149
- colors.blue('redis-cli -h $KV_HOST -p $KV_PORT --tls'),
150
- ].join('\n');
151
- Logger.println(materiaMessage);
165
+
166
+ if (providerName in WIP_PROVIDERS) {
167
+
168
+ Logger.println();
169
+ Logger.println(colors.yellow(`/!\\ ${statusMessage}`));
170
+ Logger.println(WIP_PROVIDERS[providerName].postCreateInstructions);
152
171
  }
153
172
  }
154
173
  }
@@ -173,64 +192,123 @@ async function rename (params) {
173
192
  Logger.println(`Addon ${addon.addon_id || addon.addon_name} successfully renamed to ${newName}`);
174
193
  }
175
194
 
176
- async function listProviders () {
195
+ async function listProviders (params) {
196
+ const { format } = params.options;
177
197
 
178
198
  const providers = await Addon.listProviders();
179
199
 
180
- const formattedProviders = providers.map((provider) => {
181
- return [
182
- colors.bold(provider.id),
183
- provider.name,
184
- provider.shortDesc || '',
185
- ];
186
- });
187
- Logger.println(formatTable(formattedProviders));
200
+ switch (format) {
201
+ case 'json': {
202
+ const formattedProviders = providers.map((provider) => ({
203
+ id: provider.id,
204
+ name: provider.name,
205
+ shortDesc: provider.shortDesc,
206
+ regions: provider.regions,
207
+ plans: provider.plans.map((plan) => ({
208
+ id: plan.id,
209
+ name: plan.name,
210
+ slug: plan.slug,
211
+ })),
212
+ }));
213
+ Logger.printJson(formattedProviders);
214
+ break;
215
+ }
216
+ case 'human':
217
+ default: {
218
+ const formattedProviders = providers.map((provider) => {
219
+ return [
220
+ colors.bold(provider.id),
221
+ provider.name,
222
+ provider.shortDesc || '',
223
+ ];
224
+ });
225
+ Logger.println(formatTable(formattedProviders));
226
+ }
227
+ }
188
228
  }
189
229
 
190
230
  async function showProvider (params) {
191
231
  const [providerName] = params.args;
232
+ const { format } = params.options;
192
233
 
193
234
  const provider = await Addon.getProvider(providerName);
194
235
  const providerInfos = await Addon.getProviderInfos(providerName);
195
- const providerPlans = provider.plans.sort((a, b) => a.price - b.price);
196
-
197
- Logger.println(colors.bold(provider.id));
198
- Logger.println(`${provider.name}: ${provider.shortDesc}`);
199
- Logger.println();
200
- Logger.println(`Available regions: ${provider.regions.join(', ')}`);
201
- Logger.println();
202
- Logger.println('Available plans');
203
-
204
- providerPlans.forEach((plan) => {
205
- Logger.println(`Plan ${colors.bold(plan.slug)}`);
206
- _(plan.features)
207
- .sortBy('name')
208
- .forEach(({ name, value }) => Logger.println(` ${name}: ${value}`));
209
-
210
- if (providerInfos != null) {
211
- const planType = plan.features.find(({ name }) => name.toLowerCase() === 'type');
212
- if (planType != null && planType.value.toLowerCase() === 'dedicated') {
213
- const planVersions = Object.keys(providerInfos.dedicated);
214
- const versions = planVersions.map((version) => {
215
- if (version === providerInfos.defaultDedicatedVersion) {
216
- return `${version} (default)`;
217
- }
218
- else {
219
- return version;
220
- }
221
- });
222
- Logger.println(` Available versions: ${versions.join(', ')}`);
223
-
224
- planVersions.forEach((version) => {
225
- const features = providerInfos.dedicated[version].features;
226
- Logger.println(` Options for version ${version}:`);
227
- features.forEach(({ name, enabled }) => {
228
- Logger.println(` ${name}: default=${enabled}`);
236
+
237
+ const formattedPlans = [...provider.plans]
238
+ .sort((a, b) => a.price - b.price)
239
+ .map((plan) => {
240
+ const formattedFeatures = plan.features
241
+ .map((feature) => ({
242
+ name: feature.name,
243
+ value: feature.value,
244
+ }))
245
+ .sort((a, b) => a.name.localeCompare(b.name));
246
+
247
+ const formattedPlan = {
248
+ id: plan.id,
249
+ name: plan.name,
250
+ slug: plan.slug,
251
+ features: formattedFeatures,
252
+ };
253
+
254
+ if (providerInfos != null) {
255
+ const planType = plan.features.find(({ name }) => name.toLowerCase() === 'type');
256
+ if (planType != null && planType.value.toLowerCase() === 'dedicated') {
257
+ const planVersions = Object.keys(providerInfos.dedicated);
258
+ formattedPlan.versions = planVersions.map((version) => {
259
+ return {
260
+ version,
261
+ isDefault: version === providerInfos.defaultDedicatedVersion,
262
+ options: providerInfos.dedicated[version].features.map((feature) => ({
263
+ name: feature.name,
264
+ enabledByDefault: feature.enabled,
265
+ })),
266
+ };
229
267
  });
230
- });
268
+ }
231
269
  }
270
+
271
+ return formattedPlan;
272
+ });
273
+
274
+ const formattedProvider = {
275
+ id: provider.id,
276
+ name: provider.name,
277
+ shortDesc: provider.shortDesc,
278
+ regions: provider.regions,
279
+ plans: formattedPlans,
280
+ };
281
+
282
+ switch (format) {
283
+ case 'json': {
284
+ Logger.printJson(formattedProvider);
285
+ break;
232
286
  }
233
- });
287
+ case 'human':
288
+ default: {
289
+ Logger.println(colors.bold(formattedProvider.id));
290
+ Logger.println(`${formattedProvider.name}: ${formattedProvider.shortDesc}`);
291
+ Logger.println();
292
+ Logger.println(`Available regions: ${formattedProvider.regions.join(', ')}`);
293
+ Logger.println();
294
+ Logger.println('Available plans');
295
+
296
+ formattedProvider.plans.forEach((plan) => {
297
+ Logger.println(`Plan ${colors.bold(plan.slug)}`);
298
+ plan.features.forEach(({ name, value }) => Logger.println(` ${name}: ${value}`));
299
+
300
+ if (plan.versions != null) {
301
+ Logger.println(` Available versions: ${plan.versions.map(({ version, isDefault }) => isDefault ? `${version} (default)` : version).join(', ')}`);
302
+ plan.versions.forEach(({ version, options }) => {
303
+ Logger.println(` Options for version ${version}:`);
304
+ options.forEach(({ name, enabledByDefault }) => {
305
+ Logger.println(` ${name}: default=${enabledByDefault}`);
306
+ });
307
+ });
308
+ }
309
+ });
310
+ }
311
+ }
234
312
  }
235
313
 
236
314
  async function env (params) {