clever-tools 3.11.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 (57) hide show
  1. package/bin/clever.js +416 -3
  2. package/package.json +4 -3
  3. package/src/clever-client/auth-bridge.js +61 -0
  4. package/src/clever-client/ng.js +18 -0
  5. package/src/clever-client/operators.js +121 -0
  6. package/src/commands/addon.js +34 -28
  7. package/src/commands/cancel-deploy.js +3 -4
  8. package/src/commands/config.js +16 -20
  9. package/src/commands/console.js +4 -10
  10. package/src/commands/create.js +76 -9
  11. package/src/commands/curl.js +17 -17
  12. package/src/commands/database.js +9 -9
  13. package/src/commands/delete.js +11 -10
  14. package/src/commands/deploy.js +35 -21
  15. package/src/commands/domain.js +13 -16
  16. package/src/commands/emails.js +174 -0
  17. package/src/commands/env.js +15 -8
  18. package/src/commands/keycloak.js +138 -0
  19. package/src/commands/kv.js +1 -1
  20. package/src/commands/link.js +7 -3
  21. package/src/commands/makeDefault.js +2 -1
  22. package/src/commands/matomo.js +85 -0
  23. package/src/commands/metabase.js +112 -0
  24. package/src/commands/ng.js +202 -0
  25. package/src/commands/open.js +2 -5
  26. package/src/commands/otoroshi.js +138 -0
  27. package/src/commands/profile.js +11 -10
  28. package/src/commands/published-config.js +7 -7
  29. package/src/commands/restart.js +1 -1
  30. package/src/commands/ssh-keys.js +159 -0
  31. package/src/commands/stop.js +3 -3
  32. package/src/commands/tcp-redirs.js +8 -8
  33. package/src/commands/tokens.js +146 -0
  34. package/src/commands/unlink.js +2 -1
  35. package/src/experimental-features.js +47 -2
  36. package/src/lib/ng-print.js +195 -0
  37. package/src/lib/operator-commands.js +281 -0
  38. package/src/lib/prompts.js +30 -0
  39. package/src/lib/slugify.js +10 -0
  40. package/src/logger.js +3 -0
  41. package/src/models/activity.js +2 -3
  42. package/src/models/addon.js +9 -6
  43. package/src/models/app_configuration.js +12 -9
  44. package/src/models/application.js +44 -22
  45. package/src/models/application_configuration.js +72 -72
  46. package/src/models/configuration.js +1 -0
  47. package/src/models/git.js +29 -1
  48. package/src/models/ids-resolver.js +37 -1
  49. package/src/models/interact.js +0 -24
  50. package/src/models/log-v4.js +13 -4
  51. package/src/models/namespaces.js +3 -2
  52. package/src/models/ng-resources.js +270 -0
  53. package/src/models/ng.js +276 -0
  54. package/src/models/operator.js +48 -0
  55. package/src/models/send-to-api.js +18 -0
  56. package/src/models/utils.js +21 -0
  57. package/src/parsers.js +71 -3
@@ -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
+ }
package/src/logger.js CHANGED
@@ -55,6 +55,9 @@ export const Logger = _(['debug', 'info', 'warn', 'error'])
55
55
  // No decoration for Logger.println
56
56
  Logger.println = console.log;
57
57
 
58
+ // Logger for success with a green check before the message
59
+ Logger.printSuccess = (message) => console.log(`${colors.bold.green('✓')} ${message}`);
60
+
58
61
  // No decoration for Logger.println
59
62
  Logger.printJson = (obj) => {
60
63
  console.log(JSON.stringify(obj, null, 2));
@@ -1,8 +1,7 @@
1
- import * as application from '@clevercloud/client/esm/api/v2/application.js';
2
-
3
1
  import { sendToApi } from './send-to-api.js';
2
+ import { getAllDeployments } from '@clevercloud/client/esm/api/v2/application.js';
4
3
 
5
4
  export function list (ownerId, appId, showAll) {
6
5
  const limit = showAll ? null : 10;
7
- return application.getAllDeployments({ id: ownerId, appId, limit }).then(sendToApi);
6
+ return getAllDeployments({ id: ownerId, appId, limit }).then(sendToApi);
8
7
  };
@@ -1,4 +1,3 @@
1
- import * as application from '@clevercloud/client/esm/api/v2/application.js';
2
1
  import cliparse from 'cliparse';
3
2
 
4
3
  import { get as getAddon, getAll as getAllAddons, getAllEnvVars, remove as removeAddon, create as createAddon, update as updateAddon } from '@clevercloud/client/esm/api/v2/addon.js';
@@ -6,10 +5,11 @@ import { getAllAddonProviders } from '@clevercloud/client/esm/api/v2/product.js'
6
5
  import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
7
6
  import { getAddonProvider } from '@clevercloud/client/esm/api/v4/addon-providers.js';
8
7
 
9
- import * as Interact from './interact.js';
8
+ import { confirm } from '../lib/prompts.js';
10
9
  import { Logger } from '../logger.js';
11
10
  import { sendToApi } from '../models/send-to-api.js';
12
11
  import { resolveOwnerId } from './ids-resolver.js';
12
+ import { getAllLinkedAddons, linkAddon, unlinkAddon } from '@clevercloud/client/esm/api/v2/application.js';
13
13
 
14
14
  export function listProviders () {
15
15
  return getAllAddonProviders({}).then(sendToApi);
@@ -42,7 +42,7 @@ export async function list (ownerId, appId, showAll) {
42
42
  return allAddons;
43
43
  }
44
44
 
45
- const myAddons = await application.getAllLinkedAddons({ id: ownerId, appId }).then(sendToApi);
45
+ const myAddons = await getAllLinkedAddons({ id: ownerId, appId }).then(sendToApi);
46
46
 
47
47
  if (showAll == null) {
48
48
  return myAddons;
@@ -191,19 +191,22 @@ async function getId (ownerId, addon) {
191
191
 
192
192
  export async function link (ownerId, appId, addon) {
193
193
  const addonId = await getId(ownerId, addon);
194
- return application.linkAddon({ id: ownerId, appId }, JSON.stringify(addonId)).then(sendToApi);
194
+ return linkAddon({ id: ownerId, appId }, JSON.stringify(addonId)).then(sendToApi);
195
195
  }
196
196
 
197
197
  export async function unlink (ownerId, appId, addon) {
198
198
  const addonId = await getId(ownerId, addon);
199
- return application.unlinkAddon({ id: ownerId, appId, addonId }).then(sendToApi);
199
+ return unlinkAddon({ id: ownerId, appId, addonId }).then(sendToApi);
200
200
  }
201
201
 
202
202
  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,23 +1,34 @@
1
1
  import _ from 'lodash';
2
- import * as application from '@clevercloud/client/esm/api/v2/application.js';
2
+ import {
3
+ addDependency,
4
+ create as createApplication,
5
+ get as getApplication,
6
+ getAll as getAllApplications,
7
+ getAllDependencies,
8
+ redeploy as redeployApplication,
9
+ remove as removeApplication,
10
+ removeDependency,
11
+ update as updateApplication,
12
+ } from '@clevercloud/client/esm/api/v2/application.js';
3
13
  import cliparse from 'cliparse';
4
- import * as product from '@clevercloud/client/esm/api/v2/product.js';
5
14
  import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
15
+ import colors from 'colors/safe.js';
6
16
 
7
17
  import * as AppConfiguration from './app_configuration.js';
8
- import * as Interact from './interact.js';
18
+ import { confirmAnswer } from '../lib/prompts.js';
9
19
  import { Logger } from '../logger.js';
10
20
  import * as Organisation from './organisation.js';
11
21
  import * as User from './user.js';
12
22
 
13
23
  import { sendToApi } from '../models/send-to-api.js';
14
24
  import { resolveOwnerId } from './ids-resolver.js';
25
+ import { getAvailableInstances } from '@clevercloud/client/esm/api/v2/product.js';
15
26
 
16
27
  export function listAvailableTypes () {
17
- 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']);
18
29
  };
19
30
 
20
- 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'];
21
32
 
22
33
  export function listAvailableZones () {
23
34
  return cliparse.autocomplete.words(AVAILABLE_ZONES);
@@ -42,7 +53,7 @@ async function getId (ownerId, dependency) {
42
53
  async function getInstanceType (type) {
43
54
 
44
55
  // TODO: We should be able to use it without {}
45
- const types = await product.getAvailableInstances({}).then(sendToApi);
56
+ const types = await getAvailableInstances({}).then(sendToApi);
46
57
 
47
58
  const enabledTypes = types.filter((t) => t.enabled);
48
59
  const matchingVariants = enabledTypes.filter((t) => t.variant != null && t.variant.slug === type);
@@ -83,21 +94,21 @@ export async function create (name, typeName, region, orgaIdOrName, github, isTa
83
94
  newApp.oauthApp = github;
84
95
  }
85
96
 
86
- return application.create({ id: ownerId }, newApp).then(sendToApi);
97
+ return createApplication({ id: ownerId }, newApp).then(sendToApi);
87
98
  };
88
99
 
89
100
  export async function deleteApp (app, skipConfirmation) {
90
101
  Logger.debug('Deleting app: ' + app.name + ' (' + app.id + ')');
91
102
 
92
103
  if (!skipConfirmation) {
93
- await Interact.confirm(
94
- `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:`,
95
106
  'No confirmation, aborting application deletion',
96
- [app.name],
107
+ app.name,
97
108
  );
98
109
  }
99
110
 
100
- return application.remove({ id: app.ownerId, appId: app.id }).then(sendToApi);
111
+ return removeApplication({ id: app.ownerId, appId: app.id }).then(sendToApi);
101
112
  };
102
113
 
103
114
  export async function getAllApps (ownerId) {
@@ -123,7 +134,7 @@ export async function getAllApps (ownerId) {
123
134
  };
124
135
 
125
136
  async function getApplicationsForOwner (ownerId) {
126
- const rawApplications = await application.getAll({ id: ownerId }).then(sendToApi);
137
+ const rawApplications = await getAllApplications({ id: ownerId }).then(sendToApi);
127
138
  return rawApplications.map((app) => {
128
139
  return {
129
140
  app_id: app.id,
@@ -150,13 +161,24 @@ function getApplicationByName (apps, name) {
150
161
  };
151
162
 
152
163
  async function getByName (ownerId, name) {
153
- const apps = await application.getAll({ id: ownerId }).then(sendToApi);
164
+ const apps = await getAllApplications({ id: ownerId }).then(sendToApi);
154
165
  return getApplicationByName(apps, name);
155
166
  };
156
167
 
168
+ function addInstanceLifetime (app) {
169
+ // Patch to help config commands
170
+ app.instanceLifetime = app.instance.lifetime;
171
+ return app;
172
+ }
173
+
157
174
  export function get (ownerId, appId) {
158
175
  Logger.debug(`Get information for the app: ${appId}`);
159
- return application.get({ 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);
160
182
  };
161
183
 
162
184
  function getFromSelf (appId) {
@@ -164,7 +186,7 @@ function getFromSelf (appId) {
164
186
  // /self differs from /organisations only for this one:
165
187
  // it fallbacks to the organisations of which the user
166
188
  // is a member, if it doesn't belong to Personal Space.
167
- return application.get({ appId }).then(sendToApi);
189
+ return getApplication({ appId }).then(sendToApi);
168
190
  };
169
191
 
170
192
  /**
@@ -245,7 +267,7 @@ export function unlinkRepo (alias) {
245
267
  export function redeploy (ownerId, appId, commit, withoutCache) {
246
268
  Logger.debug(`Redeploying the app: ${appId}`);
247
269
  const useCache = (withoutCache) ? 'no' : null;
248
- return application.redeploy({ id: ownerId, appId, commit, useCache }).then(sendToApi);
270
+ return redeployApplication({ id: ownerId, appId, commit, useCache }).then(sendToApi);
249
271
  };
250
272
 
251
273
  export function mergeScalabilityParameters (scalabilityParameters, instance) {
@@ -283,7 +305,7 @@ export function mergeScalabilityParameters (scalabilityParameters, instance) {
283
305
  export async function setScalability (appId, ownerId, scalabilityParameters, buildFlavor) {
284
306
  Logger.info('Scaling the app: ' + appId);
285
307
 
286
- const app = await application.get({ id: ownerId, appId }).then(sendToApi);
308
+ const app = await getApplication({ id: ownerId, appId }).then(sendToApi);
287
309
  const instance = _.cloneDeep(app.instance);
288
310
 
289
311
  instance.minFlavor = instance.minFlavor.name;
@@ -301,17 +323,17 @@ export async function setScalability (appId, ownerId, scalabilityParameters, bui
301
323
  }
302
324
  }
303
325
 
304
- return application.update({ id: ownerId, appId }, newConfig).then(sendToApi);
326
+ return updateApplication({ id: ownerId, appId }, newConfig).then(sendToApi);
305
327
  };
306
328
 
307
329
  export async function listDependencies (ownerId, appId, showAll) {
308
- const applicationDeps = await application.getAllDependencies({ id: ownerId, appId }).then(sendToApi);
330
+ const applicationDeps = await getAllDependencies({ id: ownerId, appId }).then(sendToApi);
309
331
 
310
332
  if (!showAll) {
311
333
  return applicationDeps.map((app) => ({ ...app, isLinked: true }));
312
334
  }
313
335
 
314
- const allApps = await application.getAll({ id: ownerId }).then(sendToApi);
336
+ const allApps = await getAllApplications({ id: ownerId }).then(sendToApi);
315
337
 
316
338
  const applicationDepsIds = applicationDeps.map((app) => app.id);
317
339
  return allApps.map((app) => {
@@ -322,10 +344,10 @@ export async function listDependencies (ownerId, appId, showAll) {
322
344
 
323
345
  export async function link (ownerId, appId, dependency) {
324
346
  const dependencyId = await getId(ownerId, dependency);
325
- return application.addDependency({ id: ownerId, appId, dependencyId }).then(sendToApi);
347
+ return addDependency({ id: ownerId, appId, dependencyId }).then(sendToApi);
326
348
  };
327
349
 
328
350
  export async function unlink (ownerId, appId, dependency) {
329
351
  const dependencyId = await getId(ownerId, dependency);
330
- return application.removeDependency({ id: ownerId, appId, dependencyId }).then(sendToApi);
352
+ return removeDependency({ id: ownerId, appId, dependencyId }).then(sendToApi);
331
353
  };