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
@@ -3,22 +3,49 @@
3
3
  const _ = require('lodash');
4
4
  const colors = require('colors/safe');
5
5
 
6
- const AppConfig = require('../models/app_configuration.js');
6
+ const Application = require('../models/application.js');
7
7
  const Logger = require('../logger.js');
8
8
 
9
9
  const { get: getApplication, getAllInstances } = require('@clevercloud/client/cjs/api/v2/application.js');
10
10
  const { sendToApi } = require('../models/send-to-api.js');
11
11
 
12
- function displayGroupInfo (instances, commit) {
13
- return `(${displayFlavors(instances)}, Commit: ${commit || 'N/A'})`;
12
+ async function status (params) {
13
+ const { alias, app: appIdOrName, format } = params.options;
14
+ const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
15
+
16
+ const instances = await getAllInstances({ id: ownerId, appId }).then(sendToApi);
17
+ const app = await getApplication({ id: ownerId, appId }).then(sendToApi);
18
+
19
+ const status = computeStatus(instances, app);
20
+
21
+ switch (format) {
22
+ case 'json': {
23
+ Logger.printJson(status);
24
+ break;
25
+ }
26
+ case 'human':
27
+ default: {
28
+ const statusMessage = status.status === 'running'
29
+ ? `${colors.bold.green('running')} ${displayInstances(status.instances, status.commit)}`
30
+ : colors.bold.red('stopped');
31
+
32
+ Logger.println(`${status.name}: ${statusMessage}`);
33
+ Logger.println(`Executed as: ${colors.bold(status.lifetime)}`);
34
+ if (status.deploymentInProgress) {
35
+ Logger.println(`Deployment in progress ${displayInstances(status.deploymentInProgress.instances, status.deploymentInProgress.commit)}`);
36
+ }
37
+ Logger.println();
38
+ Logger.println('Scalability:');
39
+ Logger.println(` Auto scalability: ${status.scalability.enabled ? colors.green('enabled') : colors.red('disabled')}`);
40
+ Logger.println(` Scalers: ${colors.bold(formatScalability(status.scalability.horizontal))}`);
41
+ Logger.println(` Sizes: ${colors.bold(formatScalability(status.scalability.vertical))}`);
42
+ Logger.println(` Dedicated build: ${status.separateBuild ? colors.bold(status.buildFlavor) : colors.red('disabled')}`);
43
+ }
44
+ }
14
45
  }
15
46
 
16
- function displayFlavors (instances) {
17
- return _(instances)
18
- .groupBy((i) => i.flavor.name)
19
- .map((instances, flavorName) => `${instances.length}*${flavorName}`)
20
- .value()
21
- .join(', ');
47
+ function displayInstances (instances, commit) {
48
+ return `(${instances.map((instance) => `${instance.count}*${instance.flavor}`)}, Commit: ${commit || 'N/A'})`;
22
49
  }
23
50
 
24
51
  function computeStatus (instances, app) {
@@ -30,50 +57,49 @@ function computeStatus (instances, app) {
30
57
  const isDeploying = !_.isEmpty(deployingInstances);
31
58
  const deployingCommit = _(deployingInstances).map('commit').head();
32
59
 
33
- const statusMessage = isUp
34
- ? `${colors.bold.green('running')} ${displayGroupInfo(upInstances, upCommit)}`
35
- : colors.bold.red('stopped');
36
-
37
- const statusLine = `${app.name}: ${statusMessage}`;
38
- const taskLine = `Executed as: ${colors.bold(app.instance.lifetime)}`;
39
- const deploymentLine = isDeploying
40
- ? `Deployment in progress ${displayGroupInfo(deployingInstances, deployingCommit)}`
41
- : '';
42
-
43
- return [statusLine, taskLine, deploymentLine].join('\n');
44
- }
45
-
46
- function displayScalability (app) {
47
-
48
60
  const { minFlavor, maxFlavor, minInstances, maxInstances } = app.instance;
49
61
 
50
- const vertical = (minFlavor.name === maxFlavor.name)
51
- ? minFlavor.name
52
- : `${minFlavor.name} to ${maxFlavor.name}`;
53
-
54
- const horizontal = (minInstances === maxInstances)
55
- ? minInstances
56
- : `${minInstances} to ${maxInstances}`;
57
-
58
- const enabled = (minFlavor.name !== maxFlavor.name)
62
+ const scalabilityEnabled = (minFlavor.name !== maxFlavor.name)
59
63
  || (minInstances !== maxInstances);
60
64
 
61
- return `Scalability:
62
- Auto scalability: ${enabled ? colors.green('enabled') : colors.red('disabled')}
63
- Scalers: ${colors.bold(horizontal)}
64
- Sizes: ${colors.bold(vertical)}
65
- Dedicated build: ${app.separateBuild ? colors.bold(app.buildFlavor.name) : colors.red('disabled')}`;
65
+ const status = {
66
+ id: app.id,
67
+ name: app.name,
68
+ lifetime: app.instance.lifetime,
69
+ status: isUp ? 'running' : 'stopped',
70
+ commit: upCommit,
71
+ instances: groupInstances(upInstances),
72
+ scalability: {
73
+ enabled: scalabilityEnabled,
74
+ vertical: { min: minFlavor.name, max: maxFlavor.name },
75
+ horizontal: { min: minInstances, max: maxInstances },
76
+ },
77
+ separateBuild: app.separateBuild,
78
+ buildFlavor: app.buildFlavor.name,
79
+ };
80
+
81
+ if (isDeploying) {
82
+ status.deploymentInProgress = {
83
+ commit: deployingCommit,
84
+ instances: groupInstances(deployingInstances),
85
+ };
86
+ }
87
+
88
+ return status;
66
89
  }
67
90
 
68
- async function status (params) {
69
- const { alias } = params.options;
70
- const { ownerId, appId } = await AppConfig.getAppDetails({ alias });
71
-
72
- const instances = await getAllInstances({ id: ownerId, appId }).then(sendToApi);
73
- const app = await getApplication({ id: ownerId, appId }).then(sendToApi);
91
+ function formatScalability ({ min, max }) {
92
+ return (min === max) ? min : `${min} to ${max}`;
93
+ }
74
94
 
75
- Logger.println(computeStatus(instances, app));
76
- Logger.println(displayScalability(app));
95
+ function groupInstances (instances) {
96
+ return _(instances)
97
+ .groupBy((i) => i.flavor.name)
98
+ .map((instances, flavorName) => ({
99
+ flavor: flavorName,
100
+ count: instances.length,
101
+ }))
102
+ .value();
77
103
  }
78
104
 
79
105
  module.exports = { status };
@@ -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 application = require('@clevercloud/client/cjs/api/v2/application.js');
5
5
  const Logger = require('../logger.js');
6
6
  const { sendToApi } = require('../models/send-to-api.js');
7
7
 
8
8
  async function stop (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
  await application.undeploy({ id: ownerId, appId }).then(sendToApi);
13
13
  Logger.println('App successfully stopped!');
@@ -2,32 +2,68 @@
2
2
 
3
3
  const colors = require('colors/safe');
4
4
 
5
- const AppConfig = require('../models/app_configuration.js');
6
- const Organisation = require('../models/organisation.js');
5
+ const Namespaces = require('../models/namespaces.js');
7
6
  const { sendToApi } = require('../models/send-to-api.js');
8
7
  const Interact = require('../models/interact.js');
9
8
  const Logger = require('../logger.js');
10
9
  const application = require('@clevercloud/client/cjs/api/v2/application.js');
10
+ const Application = require('../models/application.js');
11
11
 
12
12
  async function listNamespaces (params) {
13
- const namespaces = await Organisation.getNamespaces(params);
13
+ const { alias, app: appIdOrName, format } = params.options;
14
+ const { ownerId } = await Application.resolveId(appIdOrName, alias);
14
15
 
15
- Logger.println('Available namespaces: ' + namespaces.map(({ namespace }) => namespace).join(', '));
16
- };
16
+ const namespaces = await Namespaces.getNamespaces(ownerId);
17
+
18
+ namespaces.sort((a, b) => a.namespace.localeCompare(b.namespace));
19
+
20
+ switch (format) {
21
+ case 'json': {
22
+ Logger.printJson(namespaces);
23
+ break;
24
+ }
25
+ case 'human':
26
+ default: {
27
+ Logger.println('Available namespaces:');
28
+ namespaces.forEach(({ namespace }) => {
29
+ switch (namespace) {
30
+ case 'cleverapps':
31
+ Logger.println(`- ${namespace}: for redirections used with 'cleverapps.io' domain`);
32
+ break;
33
+ case 'default':
34
+ Logger.println(`- ${namespace}: for redirections used with custom domains`);
35
+ break;
36
+ default:
37
+ Logger.println(`- ${namespace}`);
38
+ }
39
+ });
40
+ break;
41
+ }
42
+ }
43
+ }
17
44
 
18
45
  async function list (params) {
19
- const { alias } = params.options;
20
- const { ownerId, appId } = await AppConfig.getAppDetails({ alias });
46
+ const { alias, app: appIdOrName, format } = params.options;
47
+ const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
21
48
 
22
49
  const redirs = await application.getTcpRedirs({ id: ownerId, appId }).then(sendToApi);
23
50
 
24
- if (redirs.length === 0) {
25
- Logger.println('No active TCP redirection for this application');
26
- }
27
- else {
28
- Logger.println('Enabled TCP redirections:');
29
- for (const { namespace, port } of redirs) {
30
- Logger.println(port + ' on ' + namespace);
51
+ switch (format) {
52
+ case 'json': {
53
+ Logger.printJson(redirs);
54
+ break;
55
+ }
56
+ case 'human':
57
+ default: {
58
+ if (redirs.length === 0) {
59
+ Logger.println('No active TCP redirection for this application');
60
+ }
61
+ else {
62
+ Logger.println('Enabled TCP redirections:');
63
+ for (const { namespace, port } of redirs) {
64
+ Logger.println(port + ' on ' + namespace);
65
+ }
66
+ }
31
67
  }
32
68
  }
33
69
  }
@@ -46,8 +82,8 @@ async function acceptPayment (result, skipConfirmation) {
46
82
  }
47
83
 
48
84
  async function add (params) {
49
- const { alias, namespace, yes: skipConfirmation } = params.options;
50
- const { ownerId, appId } = await AppConfig.getAppDetails({ alias });
85
+ const { alias, app: appIdOrName, namespace, yes: skipConfirmation } = params.options;
86
+ const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
51
87
 
52
88
  const { port } = await application.addTcpRedir({ id: ownerId, appId }, { namespace }).then(sendToApi).catch((error) => {
53
89
  if (error.status === 402) {
@@ -65,8 +101,8 @@ async function add (params) {
65
101
 
66
102
  async function remove (params) {
67
103
  const [port] = params.args;
68
- const { alias, namespace } = params.options;
69
- const { ownerId, appId } = await AppConfig.getAppDetails({ alias });
104
+ const { alias, app: appIdOrName, namespace } = params.options;
105
+ const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
70
106
 
71
107
  await application.removeTcpRedir({ id: ownerId, appId, sourcePort: port, namespace }).then(sendToApi);
72
108
 
@@ -8,29 +8,44 @@ const { getOwnerAndApp, getOrgaIdOrUserId } = require('../models/notification.js
8
8
  const { getWebhooks, createWebhook, deleteWebhook } = require('@clevercloud/client/cjs/api/v2/notification.js');
9
9
  const { sendToApi } = require('../models/send-to-api.js');
10
10
 
11
- function displayWebhook (hook) {
12
- Logger.println((hook.name && colors.bold(hook.name)) || hook.id);
13
- Logger.println(` id: ${hook.id}`);
14
- Logger.println(` services: ${(hook.scope && hook.scope.join(', ')) || hook.ownerId}`);
15
- Logger.println(` events: ${(hook.events && hook.events.join(', ')) || colors.bold('ALL')}`);
16
- Logger.println(' hooks:');
17
- hook.urls.forEach((url) => Logger.println(` ${url.url} (${url.format})`));
18
- Logger.println();
19
- }
20
-
21
11
  async function list (params) {
22
- const { org, 'list-all': listAll } = params.options;
12
+ const { org, 'list-all': listAll, format } = params.options;
23
13
 
24
14
  // TODO: fix alias option
25
15
  const { ownerId, appId } = await getOwnerAndApp(null, org, !listAll);
26
16
  const hooks = await getWebhooks({ ownerId }).then(sendToApi);
27
17
 
28
- hooks
18
+ const formattedHooks = hooks
29
19
  .filter((hook) => {
30
20
  const emptyScope = !hook.scope || hook.scope.length === 0;
31
21
  return !appId || emptyScope || hook.scope.includes(appId);
32
22
  })
33
- .forEach((hook) => displayWebhook(hook));
23
+ .map((hook) => ({
24
+ id: hook.id,
25
+ name: hook.name,
26
+ ownerId: hook.ownerId,
27
+ services: hook.scope ?? [hook.ownerId],
28
+ events: hook.events ?? ['ALL'],
29
+ urls: hook.urls,
30
+ }));
31
+
32
+ switch (format) {
33
+ case 'json': {
34
+ Logger.printJson(formattedHooks);
35
+ break;
36
+ }
37
+ case 'human':
38
+ default: {
39
+ formattedHooks.forEach((hook) => {
40
+ Logger.println(hook.name ? colors.bold(hook.name) : hook.id);
41
+ Logger.println(` id: ${hook.id}`);
42
+ Logger.println(` services: ${hook.services.join(', ')}`);
43
+ Logger.println(` events: ${hook.events.join(', ')}`);
44
+ Logger.println(' hooks:');
45
+ hook.urls.forEach((url) => Logger.println(` ${url.url} (${url.format})`));
46
+ });
47
+ }
48
+ }
34
49
  }
35
50
 
36
51
  async function add (params) {
package/src/logger.js CHANGED
@@ -28,20 +28,24 @@ function formatLines (prefixLength, lines) {
28
28
  .join('\n');
29
29
  }
30
30
 
31
+ function consoleErrorWithoutColor (line) {
32
+ process.stderr.write(line + '\n');
33
+ }
34
+
31
35
  const Logger = _(['debug', 'info', 'warn', 'error'])
32
36
  .map((severity) => {
33
37
  if (process.env.CLEVER_QUIET || (!process.env.CLEVER_VERBOSE && (severity === 'debug' || severity === 'info'))) {
34
38
  return [severity, _.noop];
35
39
  }
36
- const consoleFn = (severity === 'error') ? console.error : console.log;
40
+ const consoleFn = (severity === 'error') ? consoleErrorWithoutColor : console.log;
37
41
  const { prefix, prefixLength } = getPrefix(severity);
38
42
  return [severity, (err) => {
39
43
  const message = _.get(err, 'message', err);
40
44
  const formattedMsg = formatLines(prefixLength, processApiError(message));
41
45
  if (process.env.CLEVER_VERBOSE && severity === 'error') {
42
- console.error('[STACKTRACE]');
43
- console.error(err);
44
- console.error('[/STACKTRACE]');
46
+ consoleErrorWithoutColor('[STACKTRACE]');
47
+ consoleErrorWithoutColor(err);
48
+ consoleErrorWithoutColor('[/STACKTRACE]');
45
49
  }
46
50
  return consoleFn(`${prefix}${formattedMsg}`);
47
51
  }];
@@ -57,8 +61,7 @@ Logger.printJson = (obj) => {
57
61
  console.log(JSON.stringify(obj, null, 2));
58
62
  };
59
63
 
60
- // No decoration for Logger.printErrorLine
61
- Logger.printErrorLine = console.error;
64
+ Logger.printErrorLine = consoleErrorWithoutColor;
62
65
 
63
66
  // Only exported for testing, shouldn't be used directly
64
67
  Logger.processApiError = processApiError;
@@ -26,7 +26,7 @@ async function getProvider (providerName) {
26
26
  const providers = await listProviders();
27
27
  const provider = providers.find((p) => p.id === providerName);
28
28
  if (provider == null) {
29
- throw new Error('invalid provider name');
29
+ throw new Error(`Invalid provider name. Available providers: ${providers.map((p) => p.id).join(', ')}`);
30
30
  }
31
31
  return provider;
32
32
  }
@@ -136,22 +136,17 @@ function validateAddonVersionAndOptions (region, version, addonOptions, provider
136
136
  async function create ({ ownerId, name, providerName, planName, region, skipConfirmation, version, addonOptions }) {
137
137
 
138
138
  // TODO: We should be able to use it without {}
139
- const providers = await listProviders();
139
+ const provider = await getProvider(providerName);
140
140
 
141
- const provider = providers.find((p) => p.id === providerName);
142
- if (provider == null) {
143
- throw new Error('invalid provider name');
144
- }
145
141
  if (!provider.regions.includes(region)) {
146
- throw new Error(`invalid region name. Available regions: ${provider.regions.join(', ')}`);
142
+ throw new Error(`Invalid region name. Available regions: ${provider.regions.join(', ')}`);
147
143
  }
148
-
149
- const plan = provider.plans.find((p) => p.slug.toLowerCase() === planName.toLowerCase());
150
- if (plan == null) {
151
- const availablePlans = provider.plans.map((p) => p.slug);
152
- throw new Error(`invalid plan name. Available plans: ${availablePlans.join(', ')}`);
144
+ if (provider.plans.length === 0) {
145
+ throw new Error(`No plans available for provider ${providerName}`);
153
146
  }
154
147
 
148
+ const plan = getPlan(planName, provider.plans);
149
+
155
150
  const providerInfos = await getProviderInfos(provider.id);
156
151
  const planType = plan.features.find(({ name }) => name.toLowerCase() === 'type');
157
152
 
@@ -289,6 +284,20 @@ function parseAddonOptions (options) {
289
284
  }, {});
290
285
  }
291
286
 
287
+ function getPlan (planName, plans) {
288
+ // if no plan specified, pick the cheapest one
289
+ if (planName == null || planName === '') {
290
+ return plans.sort((p1, p2) => p1.price - p2.price)[0];
291
+ }
292
+
293
+ const plan = plans.find((p) => p.slug.toLowerCase() === planName.toLowerCase());
294
+ if (plan == null) {
295
+ const availablePlans = plans.map((p) => p.slug);
296
+ throw new Error(`Invalid plan name. Available plans: ${availablePlans.join(', ')}`);
297
+ }
298
+ return plan;
299
+ }
300
+
292
301
  module.exports = {
293
302
  completePlan,
294
303
  completeRegion,
@@ -54,13 +54,21 @@ async function addLinkedApplication (appData, alias, ignoreParentConfig) {
54
54
  return persistConfig(currentConfig);
55
55
  };
56
56
 
57
- async function removeLinkedApplication (alias) {
57
+ async function removeLinkedApplication ({ appId, alias }) {
58
58
  const currentConfig = await loadApplicationConf();
59
59
  const newConfig = {
60
60
  ...currentConfig,
61
- apps: currentConfig.apps.filter((appEntry) => appEntry.alias !== alias),
61
+ apps: currentConfig.apps.filter((appEntry) => {
62
+ return appEntry.app_id !== appId && appEntry.alias !== alias;
63
+ }),
62
64
  };
63
- return persistConfig(newConfig);
65
+
66
+ if (currentConfig.apps.length !== newConfig.apps.length) {
67
+ await persistConfig(newConfig);
68
+ return true;
69
+ }
70
+
71
+ return false;
64
72
  };
65
73
 
66
74
  function findApp (config, alias) {
@@ -4,6 +4,7 @@ const _ = require('lodash');
4
4
  const application = require('@clevercloud/client/cjs/api/v2/application.js');
5
5
  const autocomplete = require('cliparse').autocomplete;
6
6
  const product = require('@clevercloud/client/cjs/api/v2/product.js');
7
+ const { getSummary } = require('@clevercloud/client/cjs/api/v2/user.js');
7
8
 
8
9
  const AppConfiguration = require('./app_configuration.js');
9
10
  const Interact = require('./interact.js');
@@ -12,6 +13,8 @@ const Organisation = require('./organisation.js');
12
13
  const User = require('./user.js');
13
14
 
14
15
  const { sendToApi } = require('../models/send-to-api.js');
16
+ const AppConfig = require('./app_configuration.js');
17
+ const { resolveOwnerId } = require('./ids-resolver.js');
15
18
 
16
19
  function listAvailableTypes () {
17
20
  return autocomplete.words(['docker', 'elixir', 'go', 'gradle', 'haskell', 'jar', 'maven', 'meteor', 'node', 'php', 'play1', 'play2', 'python', 'ruby', 'rust', 'sbt', 'static-apache', 'war']);
@@ -86,20 +89,58 @@ async function create (name, typeName, region, orgaIdOrName, github, isTask, env
86
89
  return application.create({ id: ownerId }, newApp).then(sendToApi);
87
90
  };
88
91
 
89
- async function deleteApp (addDetails, skipConfirmation) {
90
- Logger.debug('Deleting app: ' + addDetails.name + ' (' + addDetails.appId + ')');
92
+ async function deleteApp (app, skipConfirmation) {
93
+ Logger.debug('Deleting app: ' + app.name + ' (' + app.id + ')');
91
94
 
92
95
  if (!skipConfirmation) {
93
96
  await Interact.confirm(
94
- `Deleting the application ${addDetails.name} can't be undone, please type '${addDetails.name}' to confirm: `,
97
+ `Deleting the application ${app.name} can't be undone, please type '${app.name}' to confirm: `,
95
98
  'No confirmation, aborting application deletion',
96
- [addDetails.name],
99
+ [app.name],
97
100
  );
98
101
  }
99
102
 
100
- return application.remove({ id: addDetails.ownerId, appId: addDetails.appId }).then(sendToApi);
103
+ return application.remove({ id: app.ownerId, appId: app.id }).then(sendToApi);
101
104
  };
102
105
 
106
+ async function getAllApps (ownerId) {
107
+
108
+ const summary = await getSummary().then(sendToApi);
109
+
110
+ const orgaWithApps = await Promise.all(
111
+ summary.organisations
112
+ // If owner ID is present, only keep the matching org
113
+ .filter((org) => ownerId == null || org.id === ownerId)
114
+ .sort((a, b) => a.name.localeCompare(b.name))
115
+ .map(async (org) => {
116
+ const applications = await getApplicationsForOwner(org.id);
117
+ return {
118
+ id: org.id,
119
+ name: org.name,
120
+ applications: applications.sort((a, b) => a.name.localeCompare(b.name)),
121
+ };
122
+ }),
123
+ );
124
+
125
+ return orgaWithApps;
126
+ };
127
+
128
+ async function getApplicationsForOwner (ownerId) {
129
+ const rawApplications = await application.getAll({ id: ownerId }).then(sendToApi);
130
+ return rawApplications.map((app) => {
131
+ return {
132
+ app_id: app.id,
133
+ org_id: ownerId,
134
+ name: app.name,
135
+ zone: app.zone,
136
+ type: app.instance.variant.slug,
137
+ createdAt: new Date(app.creationDate).toISOString(),
138
+ deploy_url: app.deployment.httpUrl,
139
+ git_ssh_url: app.deployment.url,
140
+ };
141
+ });
142
+ }
143
+
103
144
  function getApplicationByName (apps, name) {
104
145
  const filteredApps = apps.filter((app) => app.name === name);
105
146
  if (filteredApps.length === 1) {
@@ -129,6 +170,62 @@ function getFromSelf (appId) {
129
170
  return application.get({ appId }).then(sendToApi);
130
171
  };
131
172
 
173
+ /**
174
+ * @param {{app_id: string}|{app_name: string}} appIdOrName
175
+ * @param {string} alias
176
+ * @return {Promise<{appId: string, ownerId: string}>}
177
+ */
178
+ async function resolveId (appIdOrName, alias) {
179
+ if (appIdOrName != null && alias != null) {
180
+ throw new Error('Only one of the `--app` or `--alias` options can be set at a time');
181
+ }
182
+
183
+ // -- resolve by linked app
184
+
185
+ if (appIdOrName == null) {
186
+ const appDetails = await AppConfig.getAppDetails({ alias });
187
+ return { appId: appDetails.appId, ownerId: appDetails.ownerId };
188
+ }
189
+
190
+ // -- resolve by app id
191
+
192
+ if (appIdOrName.app_id != null) {
193
+ const ownerId = await resolveOwnerId(appIdOrName.app_id);
194
+ if (ownerId != null) {
195
+ return {
196
+ appId: appIdOrName.app_id,
197
+ ownerId,
198
+ };
199
+ }
200
+
201
+ throw new Error('Application not found');
202
+ }
203
+
204
+ // -- resolve by app name
205
+
206
+ const summary = await getSummary({}).then(sendToApi);
207
+
208
+ const candidates = [summary.user, ...summary.organisations]
209
+ .flatMap((owner) => owner.applications.map((app) => ({ app, owner })))
210
+ .filter((candidate) => candidate.app.name === appIdOrName.app_name);
211
+
212
+ if (candidates.length === 0) {
213
+ throw new Error('Application not found');
214
+ }
215
+ if (candidates.length === 1) {
216
+ return {
217
+ appId: candidates[0].app.id,
218
+ ownerId: candidates[0].owner.id,
219
+ };
220
+ }
221
+
222
+ Logger.printErrorLine(`The name '${appIdOrName.app_name}' refers to multiple applications:`);
223
+ candidates.forEach((candidate) => {
224
+ Logger.printErrorLine(`- ${candidate.owner.name}: ${candidate.app.id} (${candidate.app.variantSlug})`);
225
+ });
226
+ throw new Error('Ambiguous application name, use the `--app` option with one of the IDs above');
227
+ }
228
+
132
229
  async function linkRepo (app, orgaIdOrName, alias, ignoreParentConfig) {
133
230
  Logger.debug(`Linking current repository to the app: ${app.app_id || app.app_name}`);
134
231
 
@@ -145,7 +242,7 @@ async function linkRepo (app, orgaIdOrName, alias, ignoreParentConfig) {
145
242
 
146
243
  function unlinkRepo (alias) {
147
244
  Logger.debug(`Unlinking current repository from the app: ${alias}`);
148
- return AppConfiguration.removeLinkedApplication(alias);
245
+ return AppConfiguration.removeLinkedApplication({ alias });
149
246
  };
150
247
 
151
248
  function redeploy (ownerId, appId, commit, withoutCache) {
@@ -214,7 +311,7 @@ async function listDependencies (ownerId, appId, showAll) {
214
311
  const applicationDeps = await application.getAllDependencies({ id: ownerId, appId }).then(sendToApi);
215
312
 
216
313
  if (!showAll) {
217
- return applicationDeps;
314
+ return applicationDeps.map((app) => ({ ...app, isLinked: true }));
218
315
  }
219
316
 
220
317
  const allApps = await application.getAll({ id: ownerId }).then(sendToApi);
@@ -237,9 +334,11 @@ async function unlink (ownerId, appId, dependency) {
237
334
  };
238
335
 
239
336
  module.exports = {
337
+ resolveId,
240
338
  create,
241
339
  deleteApp,
242
340
  get,
341
+ getAllApps,
243
342
  link,
244
343
  linkRepo,
245
344
  listAvailableAliases,
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const colors = require('colors/safe');
4
+ const Logger = require('../logger.js');
5
+
6
+ function get (follow, exitOnDeploy) {
7
+ if (follow) {
8
+ if (exitOnDeploy === 'deploy-start') {
9
+ throw new Error('The `follow` and `exit-on` set to "deploy-start" options are not compatible');
10
+ }
11
+ Logger.println(colors.yellow('The `follow` option is deprecated and will be removed in an upcoming major, use --exit-on set to "never" instead'));
12
+ return 'never';
13
+ }
14
+ return exitOnDeploy;
15
+ }
16
+
17
+ // plotQuietWarning: If in quiet mode and exitStrategy set to never plot a warning to indicate that the command will end
18
+ function plotQuietWarning (exitStrategy, quiet) {
19
+ if (exitStrategy === 'never' && quiet) {
20
+ Logger.println(colors.bold.yellow('The "never" exit-on strategy is not compatible with the "quiet" mode, it will exit once the deployment ends'));
21
+ }
22
+ }
23
+
24
+ module.exports = { get, plotQuietWarning };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Helper to print a real JSON array with starting `[` and ending `]`
3
+ */
4
+ class JsonArray {
5
+ constructor () {
6
+ this._isFirst = true;
7
+ }
8
+
9
+ open () {
10
+ process.stdout.write('[\n');
11
+ }
12
+
13
+ push (log) {
14
+ if (this._isFirst) {
15
+ this._isFirst = false;
16
+ }
17
+ else {
18
+ process.stdout.write(',\n');
19
+ }
20
+ process.stdout.write(` ${JSON.stringify(log)}`);
21
+ }
22
+
23
+ close () {
24
+ process.stdout.write('\n]');
25
+ }
26
+ }
27
+
28
+ module.exports = { JsonArray };