clever-tools 3.10.0 → 3.11.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.
package/README.md CHANGED
@@ -56,7 +56,7 @@ Discover how to use Clever Tools through [our documentation](docs/).
56
56
 
57
57
  ## Examples
58
58
 
59
- Discover how to deploy many applications on Clever Cloud within [our guides](https://developers.clever-cloud.com/guides/).
59
+ Discover how to deploy many applications on Clever Cloud within [our guides](https://www.clever-cloud.com/developers/guides/).
60
60
 
61
61
  ## How to send feedback?
62
62
 
package/bin/clever.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import '../src/initial-setup.js';
5
5
 
6
6
  import cliparse from 'cliparse';
7
+ import colors from 'colors/safe.js';
7
8
  import cliparseCommands from 'cliparse/src/command.js';
8
9
  import _sortBy from 'lodash/sortBy.js';
9
10
 
@@ -13,7 +14,9 @@ import * as Parsers from '../src/parsers.js';
13
14
  import { handleCommandPromise } from '../src/command-promise-handler.js';
14
15
  import * as Application from '../src/models/application.js';
15
16
  import { AVAILABLE_ZONES } from '../src/models/application.js';
17
+ import { EXPERIMENTAL_FEATURES } from '../src/experimental-features.js';
16
18
  import { getExitOnOption, getOutputFormatOption, getSameCommitPolicyOption } from '../src/command-options.js';
19
+ import { getFeatures } from '../src/models/configuration.js';
17
20
 
18
21
  import * as Addon from '../src/models/addon.js';
19
22
  import * as ApplicationConfiguration from '../src/models/application_configuration.js';
@@ -34,6 +37,8 @@ import * as diag from '../src/commands/diag.js';
34
37
  import * as domain from '../src/commands/domain.js';
35
38
  import * as drain from '../src/commands/drain.js';
36
39
  import * as env from '../src/commands/env.js';
40
+ import * as features from '../src/commands/features.js';
41
+ import * as kv from '../src/commands/kv.js';
37
42
  import * as link from '../src/commands/link.js';
38
43
  import * as login from '../src/commands/login.js';
39
44
  import * as logout from '../src/commands/logout.js';
@@ -74,10 +79,21 @@ cliparse.command = function (name, options, commandFunction) {
74
79
  });
75
80
  };
76
81
 
77
- function run () {
82
+ // Add a yellow color and status tag to the description of an experimental command
83
+ function colorizeExperimentalCommand (command, id) {
84
+ const status = EXPERIMENTAL_FEATURES[id].status;
85
+ command.description = colors.yellow(command.description + ' [' + status.toUpperCase() + ']');
86
+ return command;
87
+ }
88
+
89
+ async function run () {
78
90
 
79
91
  // ARGUMENTS
80
92
  const args = {
93
+ kvRawCommand: cliparse.argument('command', { description: 'The raw command to send to the Materia KV or Redis® add-on' }),
94
+ kvIdOrName: cliparse.argument('kv-id', {
95
+ description: 'Add-on/Real ID (or name, if unambiguous) of a Materia KV or Redis® add-on',
96
+ }),
81
97
  addonIdOrName: cliparse.argument('addon-id', {
82
98
  description: 'Add-on ID (or name, if unambiguous)',
83
99
  parser: Parsers.addonIdOrName,
@@ -102,6 +118,11 @@ function run () {
102
118
  }),
103
119
  drainUrl: cliparse.argument('drain-url', { description: 'Drain URL' }),
104
120
  fqdn: cliparse.argument('fqdn', { description: 'Domain name of the application' }),
121
+ features: cliparse.argument('features', {
122
+ description: 'Comma-separated list of experimental features to manage',
123
+ parser: Parsers.commaSeparated,
124
+ }),
125
+ featureId: cliparse.argument('feature', { description: 'Experimental feature to manage' }),
105
126
  notificationName: cliparse.argument('name', { description: 'Notification name' }),
106
127
  notificationId: cliparse.argument('notification-id', { description: 'Notification ID' }),
107
128
  webhookUrl: cliparse.argument('url', { description: 'Webhook URL' }),
@@ -666,6 +687,35 @@ function run () {
666
687
  commands: [envSetCommand, envRemoveCommand, envImportCommand, envImportVarsFromLocalEnvCommand],
667
688
  }, env.list);
668
689
 
690
+ // EXPERIMENTAL FEATURES COMMAND
691
+ const listFeaturesCommand = cliparse.command('list', {
692
+ description: 'List available experimental features',
693
+ options: [opts.humanJsonOutputFormat],
694
+ }, features.list);
695
+ const infoFeaturesCommand = cliparse.command('info', {
696
+ description: 'Display info about an experimental feature',
697
+ args: [args.featureId],
698
+ }, features.info);
699
+ const enableFeatureCommand = cliparse.command('enable', {
700
+ description: 'Enable experimental features',
701
+ args: [args.features],
702
+ }, features.enable);
703
+ const disableFeatureCommand = cliparse.command('disable', {
704
+ description: 'Disable experimental features',
705
+ args: [args.features],
706
+ }, features.disable);
707
+ const featuresCommands = cliparse.command('features', {
708
+ description: 'Manage Clever Tools experimental features',
709
+ commands: [enableFeatureCommand, disableFeatureCommand, listFeaturesCommand, infoFeaturesCommand],
710
+ }, features.list);
711
+
712
+ // KV COMMAND
713
+ const kvRawCommand = cliparse.command('kv', {
714
+ description: 'Send a raw command to a Materia KV or Redis® add-on',
715
+ args: [args.kvIdOrName, args.kvRawCommand],
716
+ options: [opts.orgaIdOrName, opts.humanJsonOutputFormat],
717
+ }, kv.sendRawCommand);
718
+
669
719
  // LINK COMMAND
670
720
  const appLinkCommand = cliparse.command('link', {
671
721
  description: 'Link this repo to an existing application',
@@ -715,7 +765,7 @@ function run () {
715
765
 
716
766
  // OPEN COMMAND
717
767
  const openCommand = cliparse.command('open', {
718
- description: 'Open an application in your browser',
768
+ description: 'Open an application in the Console',
719
769
  options: [opts.alias, opts.appIdOrName],
720
770
  }, open.open);
721
771
 
@@ -726,9 +776,13 @@ function run () {
726
776
  }, consoleModule.openConsole);
727
777
 
728
778
  // PROFILE COMMAND
779
+ const profileOpenCommand = cliparse.command('open', {
780
+ description: 'Open your profile in the Console',
781
+ }, profile.openProfile);
729
782
  const profileCommand = cliparse.command('profile', {
730
783
  description: 'Display the profile of the current user',
731
784
  options: [opts.humanJsonOutputFormat],
785
+ commands: [profileOpenCommand],
732
786
  }, profile.profile);
733
787
 
734
788
  // PUBLISHED CONFIG COMMANDS
@@ -881,7 +935,7 @@ function run () {
881
935
  // Patch help command description
882
936
  cliparseCommands.helpCommand.description = 'Display help about the Clever Cloud CLI';
883
937
 
884
- const commands = _sortBy([
938
+ const commands = [
885
939
  accesslogsCommand,
886
940
  activityCommand,
887
941
  addonCommands,
@@ -900,6 +954,7 @@ function run () {
900
954
  drainCommands,
901
955
  emailNotificationsCommand,
902
956
  envCommands,
957
+ featuresCommands,
903
958
  cliparseCommands.helpCommand,
904
959
  loginCommand,
905
960
  logoutCommand,
@@ -918,7 +973,14 @@ function run () {
918
973
  tcpRedirsCommands,
919
974
  versionCommand,
920
975
  webhooksCommand,
921
- ], 'name');
976
+ ];
977
+
978
+ // Add experimental features only if they are enabled through the configuration file
979
+ const featuresFromConf = await getFeatures();
980
+
981
+ if (featuresFromConf.kv) {
982
+ commands.push(colorizeExperimentalCommand(kvRawCommand, 'kv'));
983
+ }
922
984
 
923
985
  // CLI PARSER
924
986
  const cliParser = cliparse.cli({
@@ -927,7 +989,7 @@ function run () {
927
989
  version: getPackageJson().version,
928
990
  options: [opts.color, opts.updateNotifier, opts.verbose],
929
991
  helpCommand: false,
930
- commands,
992
+ commands: _sortBy(commands, 'name'),
931
993
  });
932
994
 
933
995
  // Make sure argv[0] is always "node"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clever-tools",
3
- "version": "3.10.0",
3
+ "version": "3.11.0",
4
4
  "description": "Command Line Interface for Clever Cloud.",
5
5
  "main": "bin/clever.js",
6
6
  "type": "module",
@@ -33,6 +33,7 @@
33
33
  "curlconverter": "3.21.0",
34
34
  "duration-js": "4.0.0",
35
35
  "eventsource": "1.1.0",
36
+ "ioredis": "5.4.1",
36
37
  "iso8601-duration": "2.1.2",
37
38
  "isomorphic-git": "1.25.3",
38
39
  "linux-release-info": "3.0.0",
@@ -11,6 +11,7 @@ import { getAllEnvVars } from '@clevercloud/client/esm/api/v2/addon.js';
11
11
  import { sendToApi } from '../models/send-to-api.js';
12
12
  import { toNameEqualsValueString } from '@clevercloud/client/esm/utils/env-vars.js';
13
13
  import { resolveAddonId } from '../models/ids-resolver.js';
14
+ import { conf } from '../models/configuration.js';
14
15
 
15
16
  const formatTable = initFormatTable();
16
17
 
@@ -114,13 +115,17 @@ function displayAddon (format, addon, providerName, message) {
114
115
  name: 'Metabase',
115
116
  urlEnv: 'METABASE_URL',
116
117
  },
118
+ otoroshi: {
119
+ name: 'Otoroshi with LLM',
120
+ urlEnv: 'CC_OTOROSHI_URL',
121
+ },
117
122
  };
118
123
 
119
124
  const WIP_PROVIDERS = {
120
125
  keycloak: {
121
126
  status: 'beta',
122
127
  postCreateInstructions: [
123
- 'Learn more about Keycloak on Clever Cloud: https://developers.clever-cloud.com/doc/addons/keycloak/',
128
+ `Learn more about Keycloak on Clever Cloud: ${conf.DOC_URL}/addons/keycloak/`,
124
129
  ].join('\n'),
125
130
  },
126
131
  kv: {
@@ -129,25 +134,31 @@ function displayAddon (format, addon, providerName, message) {
129
134
  colors.yellow('You can easily use Materia KV with \'redis-cli\', with such commands:'),
130
135
  colors.blue(`source <(clever addon env ${addon.id} -F shell)`),
131
136
  colors.blue('redis-cli -h $KV_HOST -p $KV_PORT --tls'),
132
- 'Learn more about Materia KV on Clever Cloud: https://developers.clever-cloud.com/doc/addons/materia-kv/',
137
+ `Learn more about Materia KV on Clever Cloud: ${conf.DOC_URL}/addons/materia-kv/`,
133
138
  ].join('\n'),
134
139
  },
135
140
  'addon-matomo': {
136
141
  status: 'beta',
137
142
  postCreateInstructions: [
138
- 'Learn more about Matomo on Clever Cloud: https://developers.clever-cloud.com/doc/addons/matomo/',
143
+ `Learn more about Matomo on Clever Cloud: ${conf.DOC_URL}/addons/matomo/`,
139
144
  ].join('\n'),
140
145
  },
141
146
  metabase: {
142
147
  status: 'beta',
143
148
  postCreateInstructions: [
144
- 'Learn more about Metabase on Clever Cloud: https://developers.clever-cloud.com/doc/addons/metabase/',
149
+ `Learn more about Metabase on Clever Cloud: ${conf.DOC_URL}/addons/metabase/`,
150
+ ].join('\n'),
151
+ },
152
+ otoroshi: {
153
+ status: 'beta',
154
+ postCreateInstructions: [
155
+ `Learn more about Otoroshi with LLM on Clever Cloud: ${conf.DOC_URL}/addons/otoroshi/`,
145
156
  ].join('\n'),
146
157
  },
147
158
  'addon-pulsar': {
148
159
  status: 'beta',
149
160
  postCreateInstructions: [
150
- 'Learn more about Pulsar on Clever Cloud: https://developers.clever-cloud.com/doc/addons/pulsar/',
161
+ `Learn more about Pulsar on Clever Cloud: ${conf.DOC_URL}/addons/pulsar/`,
151
162
  ].join('\n'),
152
163
  },
153
164
  };
@@ -200,7 +211,7 @@ function displayAddon (format, addon, providerName, message) {
200
211
  Logger.println();
201
212
  Logger.println(`Your ${provider.name} is starting:`);
202
213
  Logger.println(` - Access it: ${urlToShow.startsWith('http') ? urlToShow : `https://${urlToShow}`}`);
203
- Logger.println(` - Manage it: https://console.clever-cloud.com/goto/${addon.id}`);
214
+ Logger.println(` - Manage it: ${conf.GOTO_URL}/${addon.id}`);
204
215
  }
205
216
 
206
217
  if (providerName === 'keycloak') {
@@ -209,6 +220,13 @@ function displayAddon (format, addon, providerName, message) {
209
220
  Logger.println(` - Admin user name: ${addon.env.find((e) => e.name === 'CC_KEYCLOAK_ADMIN').value}`);
210
221
  Logger.println(` - Temporary password: ${addon.env.find((e) => e.name === 'CC_KEYCLOAK_ADMIN_DEFAULT_PASSWORD').value}`);
211
222
  }
223
+
224
+ if (providerName === 'otoroshi') {
225
+ Logger.println();
226
+ Logger.println('An initial account has been created, change the password at first login (Security -> Administrators -> Edit user):');
227
+ Logger.println(` - Admin user name: ${addon.env.find((e) => e.name === 'CC_OTOROSHI_INITIAL_ADMIN_LOGIN').value}`);
228
+ Logger.println(` - Initial password: ${addon.env.find((e) => e.name === 'CC_OTOROSHI_INITIAL_ADMIN_PASSWORD').value}`);
229
+ }
212
230
  }
213
231
 
214
232
  if (providerName in WIP_PROVIDERS) {
@@ -2,23 +2,22 @@ import * as Application from '../models/application.js';
2
2
  import * as AppConfig from '../models/app_configuration.js';
3
3
  import { Logger } from '../logger.js';
4
4
  import openPage from 'open';
5
+ import { conf } from '../models/configuration.js';
5
6
 
6
7
  export async function openConsole (params) {
7
8
  const { alias, app: appIdOrName } = params.options;
8
9
 
9
- const baseUrl = 'https://console.clever-cloud.com';
10
-
11
10
  const { apps } = await AppConfig.loadApplicationConf();
12
11
  // If no app is linked or asked, open the Console without any context
13
12
  if (apps.length === 0 && !appIdOrName) {
14
13
  Logger.println('Opening the Console in your browser');
15
- await openPage(baseUrl, { wait: false });
14
+ await openPage(conf.CONSOLE_URL, { wait: false });
16
15
  return;
17
16
  }
18
17
 
19
18
  const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
20
19
  const prefixPath = (ownerId.startsWith('user_')) ? 'users/me' : `organisations/${ownerId}`;
21
- const url = `${baseUrl}/${prefixPath}/applications/${appId}`;
20
+ const url = `${conf.CONSOLE_URL}/${prefixPath}/applications/${appId}`;
22
21
 
23
22
  Logger.debug(`URL: ${url}`);
24
23
  Logger.println(`Opening the Console in your browser for application ${appId}`);
@@ -16,11 +16,11 @@ async function loadTokens () {
16
16
  }
17
17
 
18
18
  function printCleverCurlHelp () {
19
- const apiDocUrlv2 = 'https://developers.clever-cloud.com/api/v2/';
20
- const apiDocUrlv4 = 'https://developers.clever-cloud.com/api/v4/';
19
+ const apiDocUrlv2 = `${conf.API_DOC_URL}/v2/`;
20
+ const apiDocUrlv4 = `${conf.API_DOC_URL}/v4/`;
21
21
 
22
22
  Logger.println(`Usage: clever curl
23
- Query Clever Cloud's API using Clever Tools credentials. For example:
23
+ Query Clever Cloud's API using Clever Tools credentials. For example:
24
24
 
25
25
  clever curl ${conf.API_HOST}/v2/self
26
26
  clever curl ${conf.API_HOST}/v2/summary
@@ -28,7 +28,7 @@ Query Clever Cloud's API using Clever Tools credentials. For example:
28
28
  clever curl ${conf.API_HOST}/v2/organisations/<ORGANISATION_ID>/applications | jq '.[].id'
29
29
  clever curl ${conf.API_HOST}/v4/billing/organisations/<ORGANISATION_ID>/<INVOICE_NUMBER>.pdf > invoice.pdf
30
30
 
31
- Our API documentation is available here :
31
+ Our API documentation is available here :
32
32
 
33
33
  ${apiDocUrlv2}
34
34
  ${apiDocUrlv4}`);
@@ -0,0 +1,93 @@
1
+ import { getFeatures, setFeature } from '../models/configuration.js';
2
+ import { EXPERIMENTAL_FEATURES } from '../experimental-features.js';
3
+ import { formatTable as initFormatTable } from '../format-table.js';
4
+ import { Logger } from '../logger.js';
5
+
6
+ const formatTable = initFormatTable();
7
+
8
+ export async function list (params) {
9
+ const { format } = params.options;
10
+
11
+ const featuresConf = await getFeatures();
12
+ // Add status from configuration file and remove instructions
13
+ const features = Object.entries(EXPERIMENTAL_FEATURES).map(([id, feature]) => {
14
+ const enabled = featuresConf[id] === true;
15
+ return {
16
+ id,
17
+ status: feature.status,
18
+ description: feature.description,
19
+ enabled,
20
+ };
21
+ });
22
+
23
+ // For each feature, print the object with the id, status, description and enabled
24
+ switch (format) {
25
+ case 'json': {
26
+ Logger.printJson(features);
27
+ break;
28
+ }
29
+ case 'human':
30
+ default: {
31
+ const headers = ['ID', 'STATUS', 'DESCRIPTION', 'ENABLED'];
32
+
33
+ Logger.println(formatTable([
34
+ headers,
35
+ ...features.map((feature) => [
36
+ feature.id,
37
+ feature.status,
38
+ feature.description,
39
+ feature.enabled,
40
+ ]),
41
+ ]));
42
+ }
43
+ }
44
+ }
45
+
46
+ export async function info (params) {
47
+ const { feature } = params.namedArgs;
48
+ const availableFeatures = Object.keys(EXPERIMENTAL_FEATURES);
49
+
50
+ if (!availableFeatures.includes(feature)) {
51
+ throw new Error(`Unavailable feature: ${feature}`);
52
+ }
53
+
54
+ Logger.println(EXPERIMENTAL_FEATURES[feature].instructions);
55
+ }
56
+
57
+ export async function enable (params) {
58
+ const { features } = params.namedArgs;
59
+ const availableFeatures = Object.keys(EXPERIMENTAL_FEATURES);
60
+
61
+ const unknownFeatures = features.filter((feature) => !availableFeatures.includes(feature));
62
+ if (unknownFeatures.length > 0) {
63
+ throw new Error(`Unavailable feature(s): ${unknownFeatures.join(', ')}`);
64
+ }
65
+
66
+ for (const featureName of features) {
67
+ await setFeature(featureName, true);
68
+ Logger.println(`Experimental feature '${featureName}' enabled`);
69
+ }
70
+
71
+ if (features.length === 1) {
72
+ Logger.println(EXPERIMENTAL_FEATURES[features[0]].instructions);
73
+ }
74
+ else {
75
+ Logger.println();
76
+ Logger.println("To learn more about these experimental features, use 'clever features info FEATURE_NAME'");
77
+ }
78
+ }
79
+
80
+ export async function disable (params) {
81
+ const { features } = params.namedArgs;
82
+ const availableFeatures = Object.keys(EXPERIMENTAL_FEATURES);
83
+
84
+ const unknownFeatures = features.filter((feature) => !availableFeatures.includes(feature));
85
+ if (unknownFeatures.length > 0) {
86
+ throw new Error(`Unavailable feature(s): ${unknownFeatures.join(', ')}`);
87
+ }
88
+
89
+ for (const featureName of features) {
90
+ await setFeature(featureName, false);
91
+ Logger.println(`Experimental feature '${featureName}' disabled`);
92
+ }
93
+ }
@@ -0,0 +1,93 @@
1
+ import Redis from 'ioredis';
2
+ import colors from 'colors/safe.js';
3
+ import { Logger } from '../logger.js';
4
+ import { sendToApi } from '../models/send-to-api.js';
5
+ import { getAllEnvVars } from '@clevercloud/client/cjs/api/v2/addon.js';
6
+ import { findAddonsByNameOrId } from '../models/ids-resolver.js';
7
+
8
+ const URL_ENV_KEY = 'REDIS_URL';
9
+ const MAX_RETRIES_PER_REQUEST = 1;
10
+
11
+ /**
12
+ * Send a raw command to a compatible KV database
13
+ * @param {Object} params
14
+ * @param {Array<string>} params.args
15
+ * @param {Object} params.options
16
+ * @param {string} params.options.format
17
+ * @returns {Promise<void>}
18
+ */
19
+ export async function sendRawCommand (params) {
20
+ const [addonIdOrRealIdOrName] = params.args;
21
+ const { org, format } = params.options;
22
+
23
+ const addons = await findAddonsByNameOrId(addonIdOrRealIdOrName, org);
24
+
25
+ if (addons.length === 0) {
26
+ throw new Error(`Add-on ${addonIdOrRealIdOrName} not found`);
27
+ }
28
+
29
+ if (addons.length > 1) {
30
+ const formattedAddons = addons
31
+ .map(({ addonId, ownerId }) => `\n${colors.grey(`- ${addonId} (${ownerId})`)}`)
32
+ .join('');
33
+ throw new Error(`Several add-ons found for '${addonIdOrRealIdOrName}', use ID instead:${formattedAddons}`);
34
+ }
35
+
36
+ const { addonId, ownerId } = addons[0];
37
+
38
+ const url = await getAddonUrl(ownerId, addonId);
39
+
40
+ Logger.debug(`Extracted command: ${params.args.join(' ')}`);
41
+ const command = params.args.slice(1);
42
+
43
+ const result = await sendCommand(url, command);
44
+
45
+ switch (format) {
46
+ case 'json': {
47
+ Logger.printJson(result);
48
+ break;
49
+ }
50
+ case 'human':
51
+ default: {
52
+ Logger.println(result);
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get the URL of the compatible KV database
59
+ * @param {string} ownerId
60
+ * @param {string} addonId
61
+ * @returns {Promise<string>} the URL of the compatible KV database
62
+ */
63
+ async function getAddonUrl (ownerId, addonId) {
64
+
65
+ const envVars = await getAllEnvVars({ id: ownerId, addonId }).then(sendToApi);
66
+ const redisUrl = envVars.find((env) => env.name === URL_ENV_KEY)?.value;
67
+
68
+ if (!redisUrl) {
69
+ throw new Error(`Environment variable ${colors.red(URL_ENV_KEY)} not found, is it a Materia KV or Redis® add-on?`);
70
+ }
71
+
72
+ return redisUrl;
73
+ }
74
+
75
+ /**
76
+ * Send a command to a compatible KV database
77
+ * @param {string} url
78
+ * @param {Array<string>} command
79
+ * @returns {Promise<string>} the command result
80
+ */
81
+ async function sendCommand (url, command) {
82
+ Logger.debug(`Sending command '${command.join(' ')}' to ${url}`);
83
+ const client = new Redis(url, { maxRetriesPerRequest: MAX_RETRIES_PER_REQUEST });
84
+ try {
85
+ const result = await client.call(...command);
86
+ Logger.debug(`Command result: ${result}`);
87
+ return result;
88
+ }
89
+ finally {
90
+ await client.disconnect();
91
+ Logger.debug('Disconnected from server');
92
+ }
93
+ }
@@ -1,5 +1,5 @@
1
1
  import colors from 'colors/safe.js';
2
-
2
+ import openPage from 'open';
3
3
  import { Logger } from '../logger.js';
4
4
  import * as User from '../models/user.js';
5
5
 
@@ -46,3 +46,9 @@ export async function profile (params) {
46
46
  }
47
47
  }
48
48
  };
49
+
50
+ export async function openProfile () {
51
+ const URL = 'https://console.clever-cloud.com/users/me/information';
52
+ Logger.debug('Opening the profile page in your browser');
53
+ await openPage(URL, { wait: false });
54
+ }
@@ -0,0 +1,17 @@
1
+ export const EXPERIMENTAL_FEATURES = {
2
+ kv: {
3
+ status: 'alpha',
4
+ description: 'Send commands to databases such as Materia KV or Redis® directly from Clever Tools, without other dependencies',
5
+ instructions: `
6
+ Target any compatible add-on by its name or ID (with an org ID if needed) and send commands to it:
7
+
8
+ clever kv myMateriaKV SET myKey myValue
9
+ clever kv kv_xxxxxxxx GET myKey -F json
10
+ clever kv addon_xxxxx SET myTempKey myTempValue EX 120
11
+ clever kv myMateriaKV -o myOrg TTL myTempKey
12
+ clever kv redis_xxxxx --org org_xxxxx PING
13
+
14
+ Learn more about Materia KV: https://www.clever-cloud.com/developers/doc/addons/materia-kv/
15
+ `,
16
+ },
17
+ };
@@ -11,13 +11,22 @@ const env = commonEnv(Logger);
11
11
  const CONFIG_FILES = {
12
12
  MAIN: 'clever-tools.json',
13
13
  IDS_CACHE: 'ids-cache.json',
14
+ EXPERIMENTAL_FEATURES_FILE: 'clever-tools-experimental-features.json',
14
15
  };
15
16
 
16
- function getConfigPath (configFile) {
17
- const configDir = (process.platform === 'win32')
17
+ function getConfigDir () {
18
+ return (process.platform === 'win32')
18
19
  ? path.resolve(process.env.APPDATA, 'clever-cloud')
19
20
  : xdg.basedir.configPath('clever-cloud');
20
- return path.resolve(configDir, configFile);
21
+ }
22
+
23
+ function getConfigPath (configFile) {
24
+ return path.resolve(getConfigDir(), configFile);
25
+ }
26
+
27
+ // Every function which need 'clever-cloud' directory, need to call it before
28
+ async function ensureConfigDirExists () {
29
+ await mkdirp(getConfigDir(), { mode: 0o700 });
21
30
  }
22
31
 
23
32
  export async function loadOAuthConf () {
@@ -49,9 +58,8 @@ export async function loadOAuthConf () {
49
58
 
50
59
  export async function writeOAuthConf (oauthData) {
51
60
  Logger.debug('Write the tokens in the configuration file…');
52
- const configDir = path.dirname(conf.CONFIGURATION_FILE);
53
61
  try {
54
- await mkdirp(configDir, { mode: 0o700 });
62
+ await ensureConfigDirExists();
55
63
  await fs.writeFile(conf.CONFIGURATION_FILE, JSON.stringify(oauthData));
56
64
  }
57
65
  catch (error) {
@@ -78,6 +86,7 @@ export async function writeIdsCache (ids) {
78
86
  const cachePath = getConfigPath(CONFIG_FILES.IDS_CACHE);
79
87
  const idsJson = JSON.stringify(ids);
80
88
  try {
89
+ await ensureConfigDirExists();
81
90
  await fs.writeFile(cachePath, idsJson);
82
91
  }
83
92
  catch (error) {
@@ -85,22 +94,48 @@ export async function writeIdsCache (ids) {
85
94
  }
86
95
  }
87
96
 
97
+ export async function getFeatures () {
98
+ Logger.debug('Get features configuration from ' + conf.EXPERIMENTAL_FEATURES_FILE);
99
+ try {
100
+ const rawFile = await fs.readFile(conf.EXPERIMENTAL_FEATURES_FILE);
101
+ return JSON.parse(rawFile);
102
+ }
103
+ catch (error) {
104
+ if (error.code !== 'ENOENT') {
105
+ throw new Error(`Cannot get experimental features configuration from ${conf.EXPERIMENTAL_FEATURES_FILE}`);
106
+ }
107
+ return {};
108
+ }
109
+ }
110
+
111
+ export async function setFeature (feature, value) {
112
+ const currentFeatures = await getFeatures();
113
+ const newFeatures = { ...currentFeatures, ...{ [feature]: value } };
114
+
115
+ try {
116
+ await ensureConfigDirExists();
117
+ await fs.writeFile(conf.EXPERIMENTAL_FEATURES_FILE, JSON.stringify(newFeatures, null, 2));
118
+ }
119
+ catch (error) {
120
+ throw new Error(`Cannot write experimental features configuration to ${conf.EXPERIMENTAL_FEATURES_FILE}`);
121
+ }
122
+ }
123
+
88
124
  export const conf = env.getOrElseAll({
89
125
  API_HOST: 'https://api.clever-cloud.com',
90
- // API_HOST: 'https://ccapi-preprod.cleverapps.io',
91
- LOG_WS_URL: 'wss://api.clever-cloud.com/v2/logs/logs-socket/<%- appId %>?since=<%- timestamp %>',
92
- LOG_HTTP_URL: 'https://api.clever-cloud.com/v2/logs/<%- appId %>',
93
- EVENT_URL: 'wss://api.clever-cloud.com/v2/events/event-socket',
94
- WARP_10_EXEC_URL: 'https://c1-warp10-clevercloud-customers.services.clever-cloud.com/api/v0/exec',
126
+ SSH_GATEWAY: 'ssh@sshgateway-clevercloud-customers.services.clever-cloud.com',
127
+
95
128
  // the disclosure of these tokens is not considered as a vulnerability. Do not report this to our security service.
96
129
  OAUTH_CONSUMER_KEY: 'T5nFjKeHH4AIlEveuGhB5S3xg8T19e',
97
130
  OAUTH_CONSUMER_SECRET: 'MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT',
98
- SSH_GATEWAY: 'ssh@sshgateway-clevercloud-customers.services.clever-cloud.com',
99
131
 
132
+ APP_CONFIGURATION_FILE: path.resolve('.', '.clever.json'),
100
133
  CONFIGURATION_FILE: getConfigPath(CONFIG_FILES.MAIN),
101
- CONSOLE_TOKEN_URL: 'https://console.clever-cloud.com/cli-oauth',
102
- // CONSOLE_TOKEN_URL: 'https://next-console.cleverapps.io/cli-oauth',
134
+ EXPERIMENTAL_FEATURES_FILE: getConfigPath(CONFIG_FILES.EXPERIMENTAL_FEATURES_FILE),
103
135
 
104
- CLEVER_CONFIGURATION_DIR: path.resolve('.', 'clevercloud'),
105
- APP_CONFIGURATION_FILE: path.resolve('.', '.clever.json'),
136
+ API_DOC_URL: 'https://www.clever-cloud.com/developers/api',
137
+ DOC_URL: 'https://www.clever-cloud.com/developers/doc',
138
+ CONSOLE_URL: 'https://console.clever-cloud.com',
139
+ CONSOLE_TOKEN_URL: 'https://console.clever-cloud.com/cli-oauth',
140
+ GOTO_URL: 'https://console.clever-cloud.com/goto',
106
141
  });
@@ -1,5 +1,6 @@
1
- import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
1
+ import { Logger } from '../logger.js';
2
2
  import { sendToApi } from './send-to-api.js';
3
+ import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
3
4
  import { loadIdsCache, writeIdsCache } from './configuration.js';
4
5
 
5
6
  /*
@@ -102,3 +103,39 @@ async function getIdsFromSummary () {
102
103
 
103
104
  return ids;
104
105
  }
106
+
107
+ /**
108
+ * Get the IDs and owners of found add-ons from a name, ID or real ID
109
+ * @param {string} addonIdOrRealIdOrName
110
+ * @param {{ orga_name?: string, orga_id?: string }} ownerNameOrId
111
+ * @throws {Error} if no add-on is found
112
+ * @throws {Error} if several add-ons are found
113
+ * @returns {Object} The ID and owner ID of the add-on { addonId, ownerId }
114
+ */
115
+ export async function findAddonsByNameOrId (addonIdOrRealIdOrName, ownerNameOrId) {
116
+ const summary = await getSummary().then(sendToApi);
117
+
118
+ Logger.debug(`Searching for add-on '${addonIdOrRealIdOrName}' in ${summary.user.id} and ${summary.organisations.map((org) => org.id).join(', ')}`);
119
+ const candidates = [summary.user, ...summary.organisations]
120
+ .flatMap((owner) => owner.addons.map((addon) => ({ addon, owner })))
121
+ .filter(({ addon, owner }) => {
122
+ const matchOwner = ownerNameOrId == null
123
+ || owner.id === ownerNameOrId.orga_id
124
+ || owner.name === ownerNameOrId.orga_name;
125
+ const matchAddon = addon.name === addonIdOrRealIdOrName
126
+ || addon.realId === addonIdOrRealIdOrName
127
+ || addon.id === addonIdOrRealIdOrName;
128
+ return matchOwner && matchAddon;
129
+ })
130
+ .map(({ addon, owner }) => ({
131
+ addonId: addon.id,
132
+ ownerId: owner.id,
133
+ }));
134
+
135
+ Logger.debug(`Found ${candidates.length} candidate(s):`);
136
+ for (const candidate of candidates) {
137
+ Logger.debug(` - ${candidate.addonId} (${candidate.ownerId})`);
138
+ }
139
+
140
+ return candidates;
141
+ }
@@ -1,7 +1,6 @@
1
1
  import { Logger } from '../logger.js';
2
2
  import { addOauthHeader } from '@clevercloud/client/esm/oauth.js';
3
- import { conf, loadOAuthConf } from '../models/configuration.js';
4
- import { execWarpscript } from '@clevercloud/client/esm/request-warp10.superagent.js';
3
+ import { conf, loadOAuthConf } from './configuration.js';
5
4
  import { prefixUrl } from '@clevercloud/client/esm/prefix-url.js';
6
5
  import { request } from '@clevercloud/client/esm/request.fetch.js';
7
6
  import { subtle as cryptoSuble } from 'node:crypto';
@@ -47,12 +46,6 @@ export function processError (error) {
47
46
  throw error;
48
47
  }
49
48
 
50
- export function sendToWarp10 (requestParams) {
51
- return Promise.resolve(requestParams)
52
- .then(prefixUrl(conf.WARP_10_EXEC_URL))
53
- .then((requestParams) => execWarpscript(requestParams, { retry: 1 }));
54
- }
55
-
56
49
  export async function getHostAndTokens () {
57
50
  const tokens = await loadTokens();
58
51
  return {
package/src/parsers.js CHANGED
@@ -4,15 +4,16 @@ import * as Application from './models/application.js';
4
4
  import ISO8601 from 'iso8601-duration';
5
5
  import Duration from 'duration-js';
6
6
 
7
- const addonOptionsRegex = /^\w+=.+$/;
7
+ const addonOptionsRegex = /^[\w-]+=.+$/;
8
8
 
9
9
  export function addonOptions (options) {
10
- for (const option of options) {
10
+ const optionsArray = typeof options === 'string' ? [options] : options;
11
+ for (const option of optionsArray) {
11
12
  if (!option.match(addonOptionsRegex)) {
12
13
  return cliparse.parsers.error('Invalid option: ' + option);
13
14
  }
14
15
  }
15
- return cliparse.parsers.success(options.join(','));
16
+ return cliparse.parsers.success(optionsArray.join(','));
16
17
  }
17
18
 
18
19
  export function flavor (flavor) {