clever-tools 3.12.0 → 3.13.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 (42) hide show
  1. package/bin/clever.js +280 -6
  2. package/package.json +1 -1
  3. package/src/clever-client/auth-bridge.js +1 -1
  4. package/src/clever-client/operators.js +121 -0
  5. package/src/commands/addon.js +12 -7
  6. package/src/commands/cancel-deploy.js +3 -4
  7. package/src/commands/config.js +16 -20
  8. package/src/commands/console.js +4 -10
  9. package/src/commands/create.js +76 -9
  10. package/src/commands/delete.js +11 -10
  11. package/src/commands/deploy.js +35 -21
  12. package/src/commands/emails.js +174 -0
  13. package/src/commands/keycloak.js +138 -0
  14. package/src/commands/link.js +3 -1
  15. package/src/commands/makeDefault.js +2 -1
  16. package/src/commands/matomo.js +85 -0
  17. package/src/commands/metabase.js +112 -0
  18. package/src/commands/open.js +2 -5
  19. package/src/commands/otoroshi.js +138 -0
  20. package/src/commands/profile.js +2 -4
  21. package/src/commands/restart.js +1 -1
  22. package/src/commands/ssh-keys.js +159 -0
  23. package/src/commands/stop.js +1 -1
  24. package/src/commands/tcp-redirs.js +3 -3
  25. package/src/commands/tokens.js +16 -7
  26. package/src/commands/unlink.js +2 -1
  27. package/src/experimental-features.js +16 -15
  28. package/src/lib/operator-commands.js +281 -0
  29. package/src/lib/prompts.js +30 -0
  30. package/src/lib/slugify.js +10 -0
  31. package/src/models/addon.js +5 -2
  32. package/src/models/app_configuration.js +12 -9
  33. package/src/models/application.js +24 -12
  34. package/src/models/application_configuration.js +72 -72
  35. package/src/models/git.js +29 -1
  36. package/src/models/ids-resolver.js +37 -1
  37. package/src/models/interact.js +0 -24
  38. package/src/models/log-v4.js +13 -4
  39. package/src/models/operator.js +48 -0
  40. package/src/models/utils.js +21 -0
  41. package/src/parsers.js +14 -0
  42. package/src/prompt-password.js +0 -10
@@ -1,5 +1,4 @@
1
1
  import dedent from 'dedent';
2
- import { conf } from './models/configuration.js';
3
2
 
4
3
  export const EXPERIMENTAL_FEATURES = {
5
4
  kv: {
@@ -17,19 +16,6 @@ export const EXPERIMENTAL_FEATURES = {
17
16
  Learn more about Materia KV: https://www.clever-cloud.com/developers/doc/addons/materia-kv/
18
17
  `,
19
18
  },
20
- tokens: {
21
- status: 'beta',
22
- description: `Manage API tokens to query Clever Cloud API from ${conf.AUTH_BRIDGE_HOST}`,
23
- instructions: dedent`
24
- Create, list or revoke API tokens from a single command:
25
-
26
- clever tokens create myTokenName
27
- clever tokens --format json
28
- clever tokens revoke myTokenId
29
-
30
- Learn more about Clever Cloud API: https://www.clever-cloud.com/developers/api
31
- `,
32
- },
33
19
  ng: {
34
20
  status: 'beta',
35
21
  description: 'Manage Network Groups to manage applications, add-ons, external peers through a Wireguard network',
@@ -55,7 +41,22 @@ export const EXPERIMENTAL_FEATURES = {
55
41
  - Search Network Groups, members or peers:
56
42
  clever ng search myQuery
57
43
 
58
- Learn more about Network Groups: https://github.com/CleverCloud/clever-tools/blob/master/docs/ng.md
44
+ Learn more about Network Groups: https://www.clever-cloud.com/developers/doc/develop/network-groups/
45
+ `,
46
+ },
47
+ operators: {
48
+ status: 'beta',
49
+ description: 'Manage operators and their features such as Keycloak, Matomo, Metabase, Otoroshi',
50
+ instructions: dedent`
51
+ clever keycloak
52
+ clever keycloak get keycloak_xxx
53
+ clever keycloak ng enable myKeycloak
54
+
55
+ clever metabase version check myMetabase
56
+ clever metabase version update myMetabase 0.53
57
+
58
+ clever matomo open myMatomo
59
+ clever otoroshi open logs myOtoroshi
59
60
  `,
60
61
  },
61
62
  };
@@ -0,0 +1,281 @@
1
+ import colors from 'colors/safe.js';
2
+ import _ from 'lodash';
3
+ import dedent from 'dedent';
4
+
5
+ import * as Operator from '../models/operator.js';
6
+
7
+ import { Logger } from '../logger.js';
8
+ import { sendToApi } from '../models/send-to-api.js';
9
+ import {
10
+ ngDisableOperator,
11
+ ngEnableOperator,
12
+ rebootOperator,
13
+ rebuildOperator,
14
+ versionCheck,
15
+ versionUpdate,
16
+ } from '../clever-client/operators.js';
17
+ import { findAddonsByAddonProvider } from '../models/ids-resolver.js';
18
+ import { openBrowser } from '../models/utils.js';
19
+ import { confirm, selectAnswer } from './prompts.js';
20
+ import { conf } from '../models/configuration.js';
21
+
22
+ /**
23
+ * Check the version of an operator
24
+ * @param {string} provider The operator's provider
25
+ * @param {{ addon_name?: string, operator_id?: string, addon_id?: string }} addonIdOrName The operator's name or ID
26
+ * @param {string} params.options.format The output format
27
+ * @returns {Promise<void>}
28
+ */
29
+ export async function operatorCheckVersion (provider, addonIdOrName, format) {
30
+
31
+ const realId = await Operator.getSingleRealId(addonIdOrName);
32
+ const name = getDisplayName(addonIdOrName);
33
+ const versions = await versionCheck({ provider, realId }).then(sendToApi);
34
+
35
+ switch (format) {
36
+ case 'json':
37
+ Logger.printJson(versions);
38
+ break;
39
+ case 'human':
40
+ default:
41
+ if (!versions.needUpdate || (provider === 'metabase' && versions.installed === 'community-latest')) {
42
+ Logger.printSuccess(`${colors.green(name)} is up-to-date (${colors.green(versions.installed)})`);
43
+ }
44
+ else {
45
+ Logger.println(dedent`
46
+ 🔄 ${colors.red(name)} is outdated
47
+ • Installed version: ${colors.red(versions.installed)}
48
+ • Latest version: ${colors.green(versions.latest)}
49
+ `);
50
+ Logger.println();
51
+
52
+ await confirm(
53
+ `Do you want to update it to ${colors.green(versions.latest)} now?`,
54
+ 'No confirmation, aborting version update',
55
+ );
56
+
57
+ await versionUpdate({ provider, realId }, { targetVersion: versions.latest }).then(sendToApi);
58
+ Logger.printSuccess(`${colors.green(name)} is up-to-date and being rebuilt…`);
59
+ }
60
+ break;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Update the version of an operator
66
+ * @param {string} provider The operator's provider
67
+ * @param {string} askedVersion The version to update to
68
+ * @param {{ addon_name?: string, operator_id?: string, addon_id?: string }} addonIdOrName The operator's name or ID
69
+ * @returns {Promise<void>}
70
+ */
71
+ export async function operatorUpdateVersion (provider, askedVersion, addonIdOrName) {
72
+ const realId = await Operator.getSingleRealId(addonIdOrName);
73
+ const name = getDisplayName(addonIdOrName);
74
+
75
+ const versions = await versionCheck({ provider, realId }).then(sendToApi);
76
+
77
+ const targetVersion = askedVersion ?? await selectAnswer(
78
+ `Which version do you want to update ${colors.blue(name)} to, current is ${colors.blue(versions.installed)}?`,
79
+ versions.available.reverse(),
80
+ );
81
+
82
+ if (!versions.available.includes(targetVersion)) {
83
+ throw new Error(`Version ${colors.red(targetVersion)} is not available`);
84
+ }
85
+
86
+ if (versions.installed === targetVersion) {
87
+ Logger.printSuccess(`${colors.green(name)} is already at version ${colors.green(targetVersion)}`);
88
+ return;
89
+ }
90
+
91
+ await versionUpdate({ provider, realId }, { targetVersion }).then(sendToApi);
92
+ Logger.printSuccess(`${colors.green(name)} updated to ${colors.green(targetVersion)} and being rebuilt…`);
93
+ }
94
+
95
+ /**
96
+ * Unlink an operator from a Network Group
97
+ * @param {string} provider The operator's provider
98
+ * @param {{ addon_name?: string, operator_id?: string, addon_id?: string }} addonIdOrName The operator's name or ID
99
+ * @returns {Promise<void>}
100
+ * @throws {Error} If the Network Group feature is already disabled
101
+ */
102
+ export async function operatorNgDisable (provider, addonIdOrName) {
103
+ const name = getDisplayName(addonIdOrName);
104
+ const operator = await Operator.getDetails(provider, addonIdOrName);
105
+ if (!operator.features.networkGroup?.id) {
106
+ throw new Error(`Network Group is already disabled on ${colors.red(name)}`);
107
+ }
108
+
109
+ await ngDisableOperator({ provider, realId: operator.resourceId }).then(sendToApi);
110
+ Logger.println(`Disabling Network Group on ${colors.blue(name)}…`);
111
+
112
+ await operatorPrint(provider, addonIdOrName);
113
+ }
114
+
115
+ /**
116
+ * Link an operator to a Network Group
117
+ * @param {string} provider The operator's provider
118
+ * @param {{ addon_name?: string, operator_id?: string, addon_id?: string }} addonIdOrName The operator's name or ID
119
+ * @returns {Promise<void>}
120
+ * @throws {Error} If the Network Group feature is already enabled
121
+ */
122
+ export async function operatorNgEnable (provider, addonIdOrName) {
123
+ const name = getDisplayName(addonIdOrName);
124
+ const operator = await Operator.getDetails(provider, addonIdOrName);
125
+
126
+ if (operator.features.networkGroup?.id) {
127
+ throw new Error(`Network Group is already enabled on ${colors.red(name)}`);
128
+ }
129
+
130
+ await ngEnableOperator({ provider, realId: operator.resourceId }).then(sendToApi);
131
+ Logger.println(`Enabling Network Group on ${colors.blue(name)}…`);
132
+
133
+ await operatorPrint(provider, addonIdOrName);
134
+ }
135
+
136
+ /**
137
+ * List all operators for a given provider
138
+ * @param {string} provider The operator's provider
139
+ * @param {string} format The output format
140
+ * @returns {Promise<void>}
141
+ */
142
+ export async function operatorList (provider, format) {
143
+ const deployed = await findAddonsByAddonProvider(provider);
144
+ const providerName = _.capitalize(provider.replace('addon-', ''));
145
+ const operatorsPerOwner = _.groupBy(deployed, 'ownerId');
146
+
147
+ switch (format) {
148
+ case 'json':
149
+ Logger.printJson(operatorsPerOwner);
150
+ break;
151
+ case 'human':
152
+ default:
153
+
154
+ if (deployed.length === 0) {
155
+ Logger.println(`🔎 No ${providerName} found, create one with ${colors.blue(`clever addon create ${providerName.toLocaleLowerCase()}`)} command`);
156
+ return;
157
+ }
158
+
159
+ Logger.println(`🔎 Found ${deployed.length} ${providerName} operator${deployed.length > 1 ? 's' : ''}:`);
160
+ Logger.println();
161
+
162
+ Object.values(operatorsPerOwner).forEach((operators) => {
163
+ Logger.println(`• ${colors.bold(`${(operators[0].ownerId)} (${(operators[0].ownerName)})`)}`);
164
+ operators.forEach((operator) => {
165
+ Logger.println(` • ${operator.name} ${colors.grey(`(${operator.realId})`)}`);
166
+ });
167
+ Logger.println();
168
+ });
169
+ break;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Open an operator dashboard in the Clever Cloud Console in the browser
175
+ * @param {string} provider The operator's provider
176
+ * @param {{ addon_name?: string, operator_id?: string, addon_id?: string }} addonIdOrName The operator's name or ID
177
+ * @returns {Promise<void>}
178
+ */
179
+ export async function operatorOpen (provider, addonIdOrName) {
180
+ const operator = await Operator.getDetails(provider, addonIdOrName);
181
+ await openBrowser(`${conf.GOTO_URL}/${operator.addonId}`, `🌐 Opening ${colors.blue(operator.addonId)} in the browser…`);
182
+ }
183
+
184
+ /**
185
+ * Open the Logs section of an operator application in the Clever Cloud Console
186
+ * @param {string} provider The operator's provider
187
+ * @param {{ addon_name?: string, operator_id?: string, addon_id?: string }} addonIdOrName The operator's name or ID
188
+ * @returns {Promise<void>}
189
+ */
190
+ export async function operatorOpenLogs (provider, addonIdOrName) {
191
+ const operator = await Operator.getDetails(provider, addonIdOrName);
192
+ await openBrowser(
193
+ `/organisations/${operator.ownerId}/applications/${operator.resources.entrypoint}/logs`,
194
+ `🌐 Opening ${colors.blue(operator.addonId)} logs in the Clever Cloud Console…`,
195
+ );
196
+ }
197
+
198
+ /**
199
+ * Open an operator Web UI in the browser
200
+ * @param {string} provider The operator's provider
201
+ * @param {{ addon_name?: string, operator_id?: string, addon_id?: string }} addonIdOrName The operator's name or ID
202
+ * @returns {Promise<void>}
203
+ */
204
+ export async function operatorOpenWebUi (provider, addonIdOrName) {
205
+ const operator = await Operator.getDetails(provider, addonIdOrName);
206
+ await openBrowser(operator.accessUrl, `🌐 Opening ${colors.blue(operator.addonId)} Management interface in the browser…`);
207
+ }
208
+
209
+ /**
210
+ * Reboot an operator
211
+ * @param {object} params The command's parameters
212
+ * @param {string} provider The operator's provider
213
+ * @param {{ addon_name?: string, operator_id?: string, addon_id?: string }} addonIdOrName The operator's name or ID
214
+ * @returns {Promise<void>}
215
+ */
216
+ export async function operatorReboot (provider, addonIdOrName) {
217
+ const name = getDisplayName(addonIdOrName);
218
+ const realId = await Operator.getSingleRealId(addonIdOrName);
219
+ await rebootOperator({ provider, realId }).then(sendToApi);
220
+ Logger.println(`🔄 Restarting ${colors.blue(name)}…`);
221
+ }
222
+
223
+ /**
224
+ * Rebuild an operator
225
+ * @param {object} params The command's parameters
226
+ * @param {string} provider The operator's provider
227
+ * @param {{ addon_name?: string, operator_id?: string, addon_id?: string }} addonIdOrName The operator's name or ID
228
+ * @returns {Promise<void>}
229
+ */
230
+ export async function operatorRebuild (provider, addonIdOrName) {
231
+ const name = getDisplayName(addonIdOrName);
232
+ const realId = await Operator.getSingleRealId(addonIdOrName);
233
+ await rebuildOperator({ provider, realId }).then(sendToApi);
234
+ Logger.println(`🔄 Rebuilding ${colors.blue(name)}…`);
235
+ }
236
+
237
+ /**
238
+ * Print the details of an operator
239
+ * @param {string} provider The operator's provider
240
+ * @param {{ addon_name?: string, operator_id?: string, addon_id?: string }} addonIdOrName The operator's name or ID
241
+ * @param {string} format The output format
242
+ * @returns {void}
243
+ */
244
+ export async function operatorPrint (provider, addonIdOrName, format = 'human') {
245
+
246
+ const operator = await Operator.getDetails(provider, addonIdOrName);
247
+
248
+ const dataToPrint = {
249
+ Name: operator.name,
250
+ ID: operator.resourceId,
251
+ Owner: operator.ownerId,
252
+ };
253
+
254
+ dataToPrint.Version = provider === 'matomo'
255
+ ? `${operator.version} (PHP ${operator.phpVersion})`
256
+ : `${operator.version} (Java ${operator.javaVersion})`;
257
+
258
+ dataToPrint['Access URL'] = operator.accessUrl;
259
+
260
+ if (provider === 'otoroshi') {
261
+ dataToPrint['API URL'] = operator.api.url;
262
+ }
263
+
264
+ if (['otoroshi', 'keycloak'].includes(provider)) {
265
+ dataToPrint['Network Group'] = operator.features.networkGroup?.id ?? false;
266
+ }
267
+
268
+ switch (format) {
269
+ case 'json':
270
+ Logger.printJson(operator);
271
+ break;
272
+ case 'human':
273
+ default:
274
+ console.table(dataToPrint);
275
+ break;
276
+ }
277
+ }
278
+
279
+ function getDisplayName (addonIdOrName) {
280
+ return addonIdOrName.addon_name ?? addonIdOrName.operator_id ?? addonIdOrName.addon_id;
281
+ }
@@ -0,0 +1,30 @@
1
+ import { confirm as confirmPrompt, input, password, select } from '@inquirer/prompts';
2
+
3
+ export function promptSecret (message) {
4
+ return password({ message, mask: true }).catch(exitOnPromptError);
5
+ }
6
+
7
+ export async function confirm (message, rejectionMessage) {
8
+ const answer = await confirmPrompt({ message }).catch(exitOnPromptError);
9
+ if (!answer) {
10
+ throw new Error(rejectionMessage);
11
+ }
12
+ }
13
+
14
+ export async function confirmAnswer (message, rejectionMessage, expectedAnswer) {
15
+ const answer = await input({ message }).catch(exitOnPromptError);
16
+ if (answer !== expectedAnswer) {
17
+ throw new Error(rejectionMessage);
18
+ }
19
+ }
20
+
21
+ export function selectAnswer (message, choices) {
22
+ return select({ message, choices }).catch(exitOnPromptError);
23
+ }
24
+
25
+ function exitOnPromptError (error) {
26
+ if (error instanceof Error && error.name === 'ExitPromptError') {
27
+ process.exit(1);
28
+ }
29
+ throw error;
30
+ }
@@ -0,0 +1,10 @@
1
+ import slugifyRaw from 'slugify';
2
+
3
+ /**
4
+ * Converts a string to a slug using strict mode
5
+ * @param {string} string - The string to be converted to a slug
6
+ * @returns {string} The slugified string
7
+ */
8
+ export function slugify (string) {
9
+ return slugifyRaw(string, { strict: true });
10
+ }
@@ -5,7 +5,7 @@ import { getAllAddonProviders } from '@clevercloud/client/esm/api/v2/product.js'
5
5
  import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
6
6
  import { getAddonProvider } from '@clevercloud/client/esm/api/v4/addon-providers.js';
7
7
 
8
- import * as Interact from './interact.js';
8
+ import { confirm } from '../lib/prompts.js';
9
9
  import { Logger } from '../logger.js';
10
10
  import { sendToApi } from '../models/send-to-api.js';
11
11
  import { resolveOwnerId } from './ids-resolver.js';
@@ -203,7 +203,10 @@ export async function deleteAddon (ownerId, addonIdOrName, skipConfirmation) {
203
203
  const addonId = await getId(ownerId, addonIdOrName);
204
204
 
205
205
  if (!skipConfirmation) {
206
- await Interact.confirm('Deleting the addon can\'t be undone, are you sure? ', 'No confirmation, aborting addon deletion');
206
+ await confirm(
207
+ 'Deleting the add-on can\'t be undone, are you sure?',
208
+ 'No confirmation, aborting add-on deletion',
209
+ );
207
210
  }
208
211
 
209
212
  return removeAddon({ id: ownerId, addonId }).then(sendToApi);
@@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  import _ from 'lodash';
5
- import slugify from 'slugify';
5
+ import { slugify } from '../lib/slugify.js';
6
6
 
7
7
  import { Logger } from '../logger.js';
8
8
  import * as User from './user.js';
@@ -54,19 +54,22 @@ export async function addLinkedApplication (appData, alias, ignoreParentConfig)
54
54
 
55
55
  export async function removeLinkedApplication ({ appId, alias }) {
56
56
  const currentConfig = await loadApplicationConf();
57
+ const appToUnlink = currentConfig.apps.find((a) => a.app_id === appId || a.alias === alias);
58
+ if (appToUnlink == null) {
59
+ return false;
60
+ }
57
61
  const newConfig = {
58
62
  ...currentConfig,
59
- apps: currentConfig.apps.filter((appEntry) => {
60
- return appEntry.app_id !== appId && appEntry.alias !== alias;
61
- }),
63
+ apps: currentConfig.apps.filter((a) => a !== appToUnlink),
62
64
  };
63
65
 
64
- if (currentConfig.apps.length !== newConfig.apps.length) {
65
- await persistConfig(newConfig);
66
- return true;
66
+ const isDefault = currentConfig.default === appToUnlink.app_id;
67
+ if (isDefault) {
68
+ delete newConfig.default;
67
69
  }
68
70
 
69
- return false;
71
+ await persistConfig(newConfig);
72
+ return true;
70
73
  };
71
74
 
72
75
  export function findApp (config, alias) {
@@ -134,7 +137,7 @@ async function getAppDetailsForId (appId) {
134
137
  }
135
138
  if (secondAppById != null) {
136
139
  throw new Error(`There are several applications matching id '${appId}'.`
137
- + 'This should not happen, your `.clever.json` should be fixed.');
140
+ + 'This should not happen, your `.clever.json` should be fixed.');
138
141
  }
139
142
 
140
143
  return appById;
@@ -1,20 +1,21 @@
1
1
  import _ from 'lodash';
2
2
  import {
3
+ addDependency,
3
4
  create as createApplication,
4
- remove as removeApplication,
5
- getAll as getAllApplications,
6
5
  get as getApplication,
7
- redeploy as redeployApplication,
8
- update as updateApplication,
6
+ getAll as getAllApplications,
9
7
  getAllDependencies,
10
- addDependency,
8
+ redeploy as redeployApplication,
9
+ remove as removeApplication,
11
10
  removeDependency,
11
+ update as updateApplication,
12
12
  } from '@clevercloud/client/esm/api/v2/application.js';
13
13
  import cliparse from 'cliparse';
14
14
  import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
15
+ import colors from 'colors/safe.js';
15
16
 
16
17
  import * as AppConfiguration from './app_configuration.js';
17
- import * as Interact from './interact.js';
18
+ import { confirmAnswer } from '../lib/prompts.js';
18
19
  import { Logger } from '../logger.js';
19
20
  import * as Organisation from './organisation.js';
20
21
  import * as User from './user.js';
@@ -24,10 +25,10 @@ import { resolveOwnerId } from './ids-resolver.js';
24
25
  import { getAvailableInstances } from '@clevercloud/client/esm/api/v2/product.js';
25
26
 
26
27
  export function listAvailableTypes () {
27
- return cliparse.autocomplete.words(['docker', 'elixir', 'go', 'gradle', 'haskell', 'jar', 'maven', 'meteor', 'node', 'php', 'play1', 'play2', 'python', 'ruby', 'rust', 'sbt', 'static-apache', 'war']);
28
+ return cliparse.autocomplete.words(['docker', 'elixir', 'frankenphp', 'go', 'gradle', 'haskell', 'jar', 'linux', 'maven', 'meteor', 'node', 'php', 'play1', 'play2', 'python', 'ruby', 'rust', 'sbt', 'static', 'static-apache', 'v', 'war']);
28
29
  };
29
30
 
30
- export const AVAILABLE_ZONES = ['par', 'grahds', 'rbx', 'rbxhds', 'scw', 'mtl', 'sgp', 'syd', 'wsw'];
31
+ export const AVAILABLE_ZONES = ['par', 'parhds', 'grahds', 'rbx', 'rbxhds', 'scw', 'ldn', 'mtl', 'sgp', 'syd', 'wsw'];
31
32
 
32
33
  export function listAvailableZones () {
33
34
  return cliparse.autocomplete.words(AVAILABLE_ZONES);
@@ -100,10 +101,10 @@ export async function deleteApp (app, skipConfirmation) {
100
101
  Logger.debug('Deleting app: ' + app.name + ' (' + app.id + ')');
101
102
 
102
103
  if (!skipConfirmation) {
103
- await Interact.confirm(
104
- `Deleting the application ${app.name} can't be undone, please type '${app.name}' to confirm: `,
104
+ await confirmAnswer(
105
+ `Deleting an application can't be undone, please type ${colors.green(app.name)} to confirm:`,
105
106
  'No confirmation, aborting application deletion',
106
- [app.name],
107
+ app.name,
107
108
  );
108
109
  }
109
110
 
@@ -164,9 +165,20 @@ async function getByName (ownerId, name) {
164
165
  return getApplicationByName(apps, name);
165
166
  };
166
167
 
168
+ function addInstanceLifetime (app) {
169
+ // Patch to help config commands
170
+ app.instanceLifetime = app.instance.lifetime;
171
+ return app;
172
+ }
173
+
167
174
  export function get (ownerId, appId) {
168
175
  Logger.debug(`Get information for the app: ${appId}`);
169
- return getApplication({ id: ownerId, appId }).then(sendToApi);
176
+ return getApplication({ id: ownerId, appId }).then(sendToApi).then(addInstanceLifetime);
177
+ };
178
+
179
+ export function updateOptions (ownerId, appId, options) {
180
+ Logger.debug(`Update app: ${appId}`);
181
+ return updateApplication({ id: ownerId, appId }, options).then(sendToApi).then(addInstanceLifetime);
170
182
  };
171
183
 
172
184
  function getFromSelf (appId) {