clever-tools 3.7.0 → 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 (41) hide show
  1. package/README.md +1 -1
  2. package/bin/clever.js +99 -47
  3. package/package.json +47 -47
  4. package/src/command-options.js +19 -0
  5. package/src/commands/accesslogs.js +114 -34
  6. package/src/commands/activity.js +64 -12
  7. package/src/commands/addon.js +152 -57
  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/delete.js +16 -6
  13. package/src/commands/deploy.js +9 -6
  14. package/src/commands/diag.js +68 -28
  15. package/src/commands/domain.js +13 -13
  16. package/src/commands/drain.js +42 -27
  17. package/src/commands/env.js +53 -24
  18. package/src/commands/logs.js +3 -3
  19. package/src/commands/notify-email.js +33 -18
  20. package/src/commands/open.js +3 -3
  21. package/src/commands/profile.js +29 -9
  22. package/src/commands/published-config.js +26 -15
  23. package/src/commands/restart.js +12 -5
  24. package/src/commands/scale.js +2 -3
  25. package/src/commands/service.js +42 -15
  26. package/src/commands/ssh.js +3 -3
  27. package/src/commands/status.js +72 -46
  28. package/src/commands/stop.js +3 -3
  29. package/src/commands/tcp-redirs.js +54 -18
  30. package/src/commands/webhooks.js +28 -13
  31. package/src/logger.js +9 -6
  32. package/src/models/app_configuration.js +11 -3
  33. package/src/models/application.js +106 -7
  34. package/src/models/exit-strategy-option.js +24 -0
  35. package/src/models/json-array.js +28 -0
  36. package/src/models/log-v4.js +9 -29
  37. package/src/models/log.js +9 -2
  38. package/src/models/namespaces.js +20 -0
  39. package/src/models/organisation.js +0 -20
  40. package/src/models/utils.js +8 -1
  41. package/src/models/accesslogs.js +0 -54
@@ -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') {
@@ -39,6 +39,17 @@ const formatActivityTable = formatTable([
39
39
  0,
40
40
  ]);
41
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
+
42
53
  function formatActivityLine (event) {
43
54
  return formatActivityTable([
44
55
  [
@@ -50,7 +61,7 @@ function formatActivityLine (event) {
50
61
  event.cause,
51
62
  ],
52
63
  ]);
53
- };
64
+ }
54
65
 
55
66
  function isTemporaryEvent (ev) {
56
67
  if (ev == null) {
@@ -67,28 +78,65 @@ function clearPreviousLine () {
67
78
  }
68
79
  }
69
80
 
70
- function handleEvent (previousEvent, event) {
81
+ function handleEvent (previousEvent, event, handler) {
71
82
  if (isTemporaryEvent(previousEvent)) {
72
- clearPreviousLine();
83
+ handler.pop();
73
84
  }
74
85
 
75
- const activityLine = formatActivityLine(event);
76
- Logger.println(activityLine);
86
+ handler.print(event);
77
87
 
78
88
  return event;
79
89
  }
80
90
 
81
- function onEvent (previousEvent, newEvent) {
91
+ function onEvent (previousEvent, newEvent, handler) {
82
92
  const { event, date, data: { uuid, state, action, commit, cause } } = newEvent;
83
93
  if (event !== 'DEPLOYMENT_ACTION_BEGIN' && event !== 'DEPLOYMENT_ACTION_END') {
84
94
  return previousEvent;
85
95
  }
86
- return handleEvent(previousEvent, { date, uuid, 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
+ }
87
135
  }
88
136
 
89
137
  async function activity (params) {
90
- const { alias, 'show-all': showAll, follow } = params.options;
91
- 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);
92
140
  const events = await Activity.list(ownerId, appId, showAll);
93
141
  const reversedArrayWithIndex = events
94
142
  .reverse()
@@ -96,9 +144,13 @@ async function activity (params) {
96
144
  const isLast = index === all.length - 1;
97
145
  return ({ ...event, isLast });
98
146
  });
99
- let lastEvent = reversedArrayWithIndex.reduce(handleEvent, {});
147
+
148
+ const handler = getEventHandler(format, follow);
149
+
150
+ let lastEvent = reversedArrayWithIndex.reduce((previousEvent, newEvent) => handleEvent(previousEvent, newEvent, handler), {});
100
151
 
101
152
  if (!follow) {
153
+ handler.end();
102
154
  return lastEvent;
103
155
  }
104
156
 
@@ -110,7 +162,7 @@ async function activity (params) {
110
162
  eventsStream
111
163
  .on('open', () => Logger.debug('WS for events (open) ' + JSON.stringify({ appId })))
112
164
  .on('event', (event) => {
113
- lastEvent = onEvent(lastEvent, event);
165
+ lastEvent = onEvent(lastEvent, event, handler);
114
166
  return lastEvent;
115
167
  })
116
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');
@@ -101,6 +100,45 @@ async function create (params) {
101
100
  }
102
101
 
103
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
+
104
142
  switch (format) {
105
143
 
106
144
  case 'json': {
@@ -109,8 +147,9 @@ function displayAddon (format, addon, providerName, message) {
109
147
  realId: addon.realId,
110
148
  name: addon.name,
111
149
  };
112
- Logger.printJson((providerName === 'kv')
113
- ? { ...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 }
114
153
  : jsonAddon);
115
154
  break;
116
155
  }
@@ -123,15 +162,12 @@ function displayAddon (format, addon, providerName, message) {
123
162
  `Real ID: ${addon.realId}`,
124
163
  `Name: ${addon.name}`,
125
164
  ].join('\n'));
126
- if (providerName === 'kv') {
127
- const materiaMessage = [
128
- '',
129
- colors.yellow(`/!\\ The Materia ${providerName.toUpperCase()} provider is in Alpha testing phase, don't store sensitive or production grade data`),
130
- 'You can easily use Materia KV with \'redis-cli\', with such commands:',
131
- colors.blue(`source <(clever addon env ${addon.id} -F shell)`),
132
- colors.blue('redis-cli -h $KV_HOST -p $KV_PORT --tls'),
133
- ].join('\n');
134
- 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);
135
171
  }
136
172
  }
137
173
  }
@@ -156,64 +192,123 @@ async function rename (params) {
156
192
  Logger.println(`Addon ${addon.addon_id || addon.addon_name} successfully renamed to ${newName}`);
157
193
  }
158
194
 
159
- async function listProviders () {
195
+ async function listProviders (params) {
196
+ const { format } = params.options;
160
197
 
161
198
  const providers = await Addon.listProviders();
162
199
 
163
- const formattedProviders = providers.map((provider) => {
164
- return [
165
- colors.bold(provider.id),
166
- provider.name,
167
- provider.shortDesc || '',
168
- ];
169
- });
170
- 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
+ }
171
228
  }
172
229
 
173
230
  async function showProvider (params) {
174
231
  const [providerName] = params.args;
232
+ const { format } = params.options;
175
233
 
176
234
  const provider = await Addon.getProvider(providerName);
177
235
  const providerInfos = await Addon.getProviderInfos(providerName);
178
- const providerPlans = provider.plans.sort((a, b) => a.price - b.price);
179
-
180
- Logger.println(colors.bold(provider.id));
181
- Logger.println(`${provider.name}: ${provider.shortDesc}`);
182
- Logger.println();
183
- Logger.println(`Available regions: ${provider.regions.join(', ')}`);
184
- Logger.println();
185
- Logger.println('Available plans');
186
-
187
- providerPlans.forEach((plan) => {
188
- Logger.println(`Plan ${colors.bold(plan.slug)}`);
189
- _(plan.features)
190
- .sortBy('name')
191
- .forEach(({ name, value }) => Logger.println(` ${name}: ${value}`));
192
-
193
- if (providerInfos != null) {
194
- const planType = plan.features.find(({ name }) => name.toLowerCase() === 'type');
195
- if (planType != null && planType.value.toLowerCase() === 'dedicated') {
196
- const planVersions = Object.keys(providerInfos.dedicated);
197
- const versions = planVersions.map((version) => {
198
- if (version === providerInfos.defaultDedicatedVersion) {
199
- return `${version} (default)`;
200
- }
201
- else {
202
- return version;
203
- }
204
- });
205
- Logger.println(` Available versions: ${versions.join(', ')}`);
206
-
207
- planVersions.forEach((version) => {
208
- const features = providerInfos.dedicated[version].features;
209
- Logger.println(` Options for version ${version}:`);
210
- features.forEach(({ name, enabled }) => {
211
- 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
+ };
212
267
  });
213
- });
268
+ }
214
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;
215
286
  }
216
- });
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
+ }
217
312
  }
218
313
 
219
314
  async function env (params) {
@@ -3,7 +3,11 @@
3
3
  const colors = require('colors/safe');
4
4
 
5
5
  const AppConfig = require('../models/app_configuration.js');
6
+ const Application = require('../models/application.js');
7
+ const Organisation = require('../models/organisation.js');
6
8
  const Logger = require('../logger.js');
9
+ const formatTable = require('../format-table.js');
10
+ const { truncateWithEllipsis } = require('../models/utils.js');
7
11
 
8
12
  async function list (params) {
9
13
  const { 'only-aliases': onlyAliases, json } = params.options;
@@ -43,4 +47,82 @@ function formatApps (apps, onlyAliases, json) {
43
47
  }
44
48
  }
45
49
 
46
- module.exports = { list };
50
+ async function listAll (params) {
51
+ const { org: orgaIdOrName, format } = params.options;
52
+
53
+ const linkedApps = await AppConfig.loadApplicationConf().then((conf) => conf.apps);
54
+
55
+ const ownerId = (orgaIdOrName != null && orgaIdOrName.orga_name !== '')
56
+ ? await Organisation.getId(orgaIdOrName)
57
+ : null;
58
+
59
+ const allAppsPerOrg = await Application.getAllApps(ownerId);
60
+
61
+ // For each app, we try to find a corresponding local alias in the configuration file
62
+ const allAppsWithAliasPerOrg = allAppsPerOrg.map((org) => {
63
+ const applicationsWithAlias = org.applications.map((app) => {
64
+ const foundAlias = linkedApps.find((a) => a.app_id === app.app_id)?.alias;
65
+ return { ...app, alias: foundAlias ?? '' };
66
+ });
67
+ return { ...org, applications: applicationsWithAlias };
68
+ });
69
+
70
+ switch (format) {
71
+ case 'json': {
72
+ Logger.printJson(allAppsWithAliasPerOrg);
73
+ break;
74
+ }
75
+ case 'human':
76
+ default: {
77
+
78
+ const headers = ['APPLICATION ID', 'NAME', 'TYPE', 'ZONE', 'LOCAL ALIAS'];
79
+ const appNameWidth = 42;
80
+
81
+ // We calculate the maximum width of each column to format the table
82
+ const allApps = allAppsWithAliasPerOrg.flatMap((org) => org.applications);
83
+ const columnWidths = [
84
+ getPropertyMaxWidth(allApps, 'app_id'),
85
+ appNameWidth,
86
+ getPropertyMaxWidth(allApps, 'type'),
87
+ getPropertyMaxWidth(allApps, 'zone'),
88
+ getPropertyMaxWidth(allApps, 'alias'),
89
+ ];
90
+ const formatTableWithColumnWidth = formatTable(columnWidths);
91
+
92
+ allAppsWithAliasPerOrg.forEach((org) => {
93
+
94
+ Logger.println();
95
+
96
+ const applicationsPlural = org.applications.length !== 1 ? 'applications' : 'application';
97
+ const punctuation = org.applications.length > 0 ? ':' : '';
98
+
99
+ Logger.println(colors.blue(`• Organization '${org.name}' (${org.id}) with ${org.applications.length} ${applicationsPlural}${punctuation}`));
100
+
101
+ if (org.applications.length > 0) {
102
+ Logger.println();
103
+ Logger.println(formatTableWithColumnWidth([
104
+ headers,
105
+ ...org.applications.map((app) => [
106
+ app.app_id,
107
+ truncateWithEllipsis(appNameWidth, app.name),
108
+ app.type,
109
+ app.zone,
110
+ app.alias,
111
+ ]),
112
+ ]));
113
+ }
114
+ });
115
+
116
+ break;
117
+ }
118
+ }
119
+ }
120
+
121
+ function getPropertyMaxWidth (array, propertyName) {
122
+ return Math.max(...array.map((o) => o[propertyName].length));
123
+ }
124
+
125
+ module.exports = {
126
+ list,
127
+ listAll,
128
+ };
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const AppConfig = require('../models/app_configuration.js');
3
+ const Application = require('../models/application.js');
4
4
  const Logger = require('../logger.js');
5
5
  const { getAllDeployments, cancelDeployment } = require('@clevercloud/client/cjs/api/v2/application.js');
6
6
  const { sendToApi } = require('../models/send-to-api.js');
7
7
 
8
8
  async function cancelDeploy (params) {
9
- const { alias } = params.options;
10
- const { ownerId, appId } = await AppConfig.getAppDetails({ alias });
9
+ const { alias, app: appIdOrName } = params.options;
10
+ const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
11
11
 
12
12
  const deployments = await getAllDeployments({ id: ownerId, appId, limit: 1 }).then(sendToApi);
13
13