clever-tools 4.5.2 → 4.6.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 (69) hide show
  1. package/README.md +10 -0
  2. package/bin/clever.js +25 -22
  3. package/package.json +2 -2
  4. package/src/clever-client/auth-bridge.js +0 -25
  5. package/src/commands/README.md +1 -0
  6. package/src/commands/accesslogs/accesslogs.command.js +4 -1
  7. package/src/commands/accesslogs/accesslogs.docs.md +1 -1
  8. package/src/commands/addon/addon.create.command.js +23 -20
  9. package/src/commands/config/config.set.command.js +3 -3
  10. package/src/commands/config-provider/config-provider.args.js +8 -0
  11. package/src/commands/config-provider/config-provider.command.js +10 -0
  12. package/src/commands/config-provider/config-provider.docs.md +109 -0
  13. package/src/commands/config-provider/config-provider.get.command.js +36 -0
  14. package/src/commands/config-provider/config-provider.import.command.js +40 -0
  15. package/src/commands/config-provider/config-provider.list.command.js +45 -0
  16. package/src/commands/config-provider/config-provider.open.command.js +17 -0
  17. package/src/commands/config-provider/config-provider.rm.command.js +25 -0
  18. package/src/commands/config-provider/config-provider.set.command.js +37 -0
  19. package/src/commands/create/create.command.js +7 -6
  20. package/src/commands/curl/curl.command.js +18 -19
  21. package/src/commands/deploy/deploy.command.js +14 -8
  22. package/src/commands/deploy/deploy.docs.md +20 -0
  23. package/src/commands/diag/diag.command.js +52 -39
  24. package/src/commands/features/features.disable.command.js +1 -2
  25. package/src/commands/features/features.enable.command.js +1 -2
  26. package/src/commands/features/features.info.command.js +1 -1
  27. package/src/commands/features/features.list.command.js +1 -2
  28. package/src/commands/global.commands.js +22 -0
  29. package/src/commands/global.options.js +1 -1
  30. package/src/commands/login/login.command.js +119 -20
  31. package/src/commands/login/login.docs.md +9 -2
  32. package/src/commands/logout/logout.command.js +39 -7
  33. package/src/commands/logout/logout.docs.md +7 -1
  34. package/src/commands/logs/logs.command.js +7 -8
  35. package/src/commands/logs/logs.docs.md +1 -1
  36. package/src/commands/ng/ng.get-config.command.js +3 -3
  37. package/src/commands/profile/profile.command.js +13 -37
  38. package/src/commands/profile/profile.docs.md +28 -0
  39. package/src/commands/profile/profile.list.command.js +44 -0
  40. package/src/commands/profile/profile.switch.command.js +80 -0
  41. package/src/commands/restart/restart.command.js +3 -2
  42. package/src/commands/ssh/ssh.command.js +2 -2
  43. package/src/commands/tokens/tokens.command.js +3 -3
  44. package/src/commands/tokens/tokens.create.command.js +4 -4
  45. package/src/config/cache.js +60 -0
  46. package/src/config/config.js +233 -0
  47. package/src/config/features.js +167 -0
  48. package/src/config/paths.js +23 -0
  49. package/src/lib/date-utils.js +48 -0
  50. package/src/lib/fs.js +49 -0
  51. package/src/lib/operator-commands.js +4 -4
  52. package/src/lib/profile.js +116 -0
  53. package/src/logger.js +123 -69
  54. package/src/logger.types.d.ts +5 -0
  55. package/src/models/app_configuration.js +42 -39
  56. package/src/models/application_configuration.js +30 -30
  57. package/src/models/config-provider.js +34 -0
  58. package/src/models/git-isomorphic.js +153 -0
  59. package/src/models/git-system.js +182 -0
  60. package/src/models/git.js +150 -128
  61. package/src/models/ids-resolver.js +1 -1
  62. package/src/models/log.js +141 -90
  63. package/src/models/send-to-api.js +58 -33
  64. package/src/models/user.js +0 -11
  65. package/src/models/utils.js +2 -2
  66. package/src/experimental-features.js +0 -91
  67. package/src/lib/format-date.js +0 -3
  68. package/src/models/configuration.js +0 -140
  69. package/src/models/log-v4.js +0 -189
@@ -0,0 +1,116 @@
1
+ import { get as getUser } from '@clevercloud/client/esm/api/v2/organisation.js';
2
+ import { getCurrentTokenInfo } from '@clevercloud/client/esm/api/v2/self.js';
3
+ import { baseConfig } from '../config/config.js';
4
+ import { sendToApiWithConfig } from '../models/send-to-api.js';
5
+ import { formatDateLocalized, toDate } from './date-utils.js';
6
+ import { styleText } from './style-text.js';
7
+
8
+ /**
9
+ * @typedef {import('../config/config.js').Profile} Profile
10
+ */
11
+
12
+ /**
13
+ * @typedef {object} ProfileDetails
14
+ * @property {string} alias
15
+ * @property {string} email
16
+ * @property {string | null | undefined} name
17
+ * @property {string} [id]
18
+ * @property {Date} [tokenExpiration]
19
+ * @property {boolean} has2FA
20
+ * @property {string} [avatar]
21
+ * @property {Date} [creationDate]
22
+ * @property {string} [lang]
23
+ * @property {boolean} isProfileActive
24
+ * @property {boolean} isTokenValid
25
+ * @property {Record<string, string>} [overrides]
26
+ */
27
+
28
+ /**
29
+ * Formats a profile for display.
30
+ * @param {Profile | ProfileDetails} profile
31
+ * @returns {string}
32
+ */
33
+ export function formatProfile(profile) {
34
+ return [
35
+ profile.alias,
36
+ profile.email != null ? `(${profile.email})` : null,
37
+ profile.isProfileActive ? styleText('green', '[active]') : null,
38
+ ]
39
+ .filter(Boolean)
40
+ .join(' ');
41
+ }
42
+
43
+ /**
44
+ * Fetch full profile details using the profile's credentials.
45
+ * @param {object} params
46
+ * @param {Profile} params.profile
47
+ * @param {boolean} params.isActive
48
+ * @returns {Promise<ProfileDetails>}
49
+ */
50
+ export async function getProfileDetails({ profile, isActive }) {
51
+ const sendWithCredentials = sendToApiWithConfig({
52
+ token: profile.token,
53
+ secret: profile.secret,
54
+ apiHost: profile.overrides?.API_HOST ?? baseConfig.API_HOST,
55
+ consumerKey: profile.overrides?.OAUTH_CONSUMER_KEY ?? baseConfig.OAUTH_CONSUMER_KEY,
56
+ consumerSecret: profile.overrides?.OAUTH_CONSUMER_SECRET ?? baseConfig.OAUTH_CONSUMER_SECRET,
57
+ });
58
+
59
+ const [user, token] = await Promise.all([
60
+ getUser({}).then(sendWithCredentials),
61
+ getCurrentTokenInfo().then(sendWithCredentials),
62
+ ]).catch(() => [null, null]);
63
+
64
+ return {
65
+ id: user?.id ?? profile.userId,
66
+ email: user?.email ?? profile.email,
67
+ name: user?.name,
68
+ avatar: user?.avatar,
69
+ creationDate: toDate(user?.creationDate),
70
+ tokenExpiration: toDate(token?.expirationDate ?? profile.expirationDate),
71
+ lang: user?.lang,
72
+ has2FA: user != null ? user.preferredMFA != null && user.preferredMFA !== 'NONE' : undefined,
73
+ alias: profile.alias,
74
+ isProfileActive: isActive,
75
+ isTokenValid: user != null,
76
+ overrides: profile.overrides,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Formats profile details for CLI display.
82
+ * @param {ProfileDetails} profile
83
+ */
84
+ export function formatProfileDetails(profile) {
85
+ const lines = [];
86
+
87
+ lines.push(styleText('bold', formatProfile(profile)));
88
+ lines.push(profile.id);
89
+
90
+ if (!profile.isTokenValid) {
91
+ lines.push(styleText('red', 'Invalid or expired token'));
92
+ return lines.map((line, index) => (index === 0 ? line : ` ${line}`)).join('\n');
93
+ }
94
+
95
+ lines.push(profile.name ? profile.name : '[unknown]');
96
+
97
+ const expiresAtFormatted = formatDateLocalized(profile.tokenExpiration);
98
+ if (expiresAtFormatted) {
99
+ lines.push(`Expires on ${styleText('gray', expiresAtFormatted)}`);
100
+ }
101
+
102
+ lines.push(`2FA ${profile.has2FA ? styleText('green', 'enabled ✓') : styleText('red', 'disabled ✗')}`);
103
+
104
+ if (profile.overrides != null) {
105
+ const overrideEntries = Object.entries(profile.overrides).filter(([, v]) => v != null);
106
+ for (const [key, value] of overrideEntries) {
107
+ lines.push(`${key}: ${styleText('gray', value)}`);
108
+ }
109
+ }
110
+
111
+ return lines
112
+ .map((line, index) => {
113
+ return index === 0 ? line : ` ${line}`;
114
+ })
115
+ .join('\n');
116
+ }
package/src/logger.js CHANGED
@@ -1,85 +1,139 @@
1
- import _ from 'lodash';
2
1
  import { format } from 'node:util';
3
2
  import { styleText } from './lib/style-text.js';
4
3
 
5
- function getPrefix(severity) {
6
- const prefix = `[${severity.toUpperCase()}] `;
7
- const prefixLength = prefix.length;
8
- if (severity === 'error') {
9
- return { prefix: styleText(['bold', 'red'], prefix), prefixLength };
10
- }
11
- return { prefix, prefixLength };
12
- }
4
+ /**
5
+ * @typedef {import('./logger.types.js').ApiError} ApiError
6
+ */
13
7
 
14
- function processApiError(error) {
15
- if (error.id == null || error.message == null) {
16
- return error;
17
- }
18
- const fields = _.map(error.fields, (msg, field) => `${field}: ${msg}`);
19
- return [`${error.message} [${error.id}]`, ...fields].join('\n');
20
- }
8
+ const IS_QUIET = Boolean(process.env.CLEVER_QUIET);
9
+ const IS_VERBOSE = Boolean(process.env.CLEVER_VERBOSE);
21
10
 
22
- function formatLines(prefixLength, lines) {
23
- const blankPrefix = _.repeat(' ', prefixLength);
24
- return (lines || '')
25
- .split('\n')
26
- .map((line, i) => (i === 0 ? line : `${blankPrefix}${line}`))
27
- .join('\n');
28
- }
11
+ export const Logger = {
12
+ /**
13
+ * @param {string} message
14
+ */
15
+ debug(message) {
16
+ consoleLog('debug', message);
17
+ },
29
18
 
30
- function consoleErrorWithoutColor(line) {
31
- process.stderr.write(format(line) + '\n');
32
- }
19
+ /**
20
+ * @param {string} message
21
+ */
22
+ info(message) {
23
+ consoleLog('info', message);
24
+ },
25
+
26
+ /**
27
+ * @param {string} message
28
+ */
29
+ warn(message) {
30
+ consoleLog('warn', message);
31
+ },
32
+
33
+ /**
34
+ * @param {Error|string} error
35
+ */
36
+ error(error) {
37
+ if (IS_QUIET) {
38
+ return;
39
+ }
40
+
41
+ const prefix = '[ERROR] ';
42
+ const styledPrefix = styleText(['bold', 'red'], prefix);
43
+ const message = error instanceof Error ? error.message : error;
44
+ const formatted = formatLines(prefix.length, processApiError(message));
33
45
 
34
- export const Logger = _(['debug', 'info', 'warn', 'error'])
35
- .map((severity) => {
36
- if (process.env.CLEVER_QUIET || (!process.env.CLEVER_VERBOSE && (severity === 'debug' || severity === 'info'))) {
37
- return [severity, _.noop];
46
+ if (IS_VERBOSE) {
47
+ writeStderr('[STACKTRACE]');
48
+ writeStderr(error);
49
+ writeStderr('[/STACKTRACE]');
38
50
  }
39
- const consoleFn = severity === 'error' ? consoleErrorWithoutColor : console.log;
40
- const { prefix, prefixLength } = getPrefix(severity);
41
- return [
42
- severity,
43
- (err) => {
44
- const message = _.get(err, 'message', err);
45
- const formattedMsg = formatLines(prefixLength, processApiError(message));
46
- if (process.env.CLEVER_VERBOSE && severity === 'error') {
47
- consoleErrorWithoutColor('[STACKTRACE]');
48
- consoleErrorWithoutColor(err);
49
- consoleErrorWithoutColor('[/STACKTRACE]');
50
- }
51
- return consoleFn(`${prefix}${formattedMsg}`);
52
- },
53
- ];
54
- })
55
- .fromPairs()
56
- .value();
57
-
58
- // No decoration for Logger.println
59
- Logger.println = console.log;
51
+ writeStderr(`${styledPrefix}${formatted}`);
52
+ },
53
+
54
+ println: console.log,
55
+
56
+ /**
57
+ * @param {string} text
58
+ * @param {number} indentLevel
59
+ */
60
+ printlnWithIndent(text, indentLevel) {
61
+ console.log(' '.repeat(indentLevel) + text);
62
+ },
63
+
64
+ /** @param {string} message */
65
+ printSuccess(message) {
66
+ console.log(`${styleText(['bold', 'green'], '✓')} ${message}`);
67
+ },
68
+
69
+ /** @param {string} message */
70
+ printInfo(message) {
71
+ console.log(`${styleText('blue', 'i')} ${message}`);
72
+ },
73
+
74
+ /** @param {unknown} obj */
75
+ printJson(obj) {
76
+ console.log(JSON.stringify(obj, null, 2));
77
+ },
78
+
79
+ printErrorLine: writeStderr,
80
+ };
60
81
 
61
82
  /**
62
- * Prints a line of text with specified indentation.
63
- *
64
- * @param {string} text - The text to be printed.
65
- * @param {number} indentLevel - The number of spaces to indent the text.
83
+ * Logs a message to the console with severity prefix.
84
+ * @param {'debug'|'info'|'warn'} severity
85
+ * @param {string} message
86
+ * @returns {void}
66
87
  */
67
- Logger.printlnWithIndent = (text, indentLevel) => {
68
- Logger.println(' '.repeat(indentLevel) + text);
69
- };
88
+ function consoleLog(severity, message) {
89
+ if (IS_QUIET) {
90
+ return;
91
+ }
92
+ if (!IS_VERBOSE && severity !== 'warn') {
93
+ return;
94
+ }
95
+ const prefix = `[${severity.toUpperCase()}] `;
96
+ console.log(`${prefix}${formatLines(prefix.length, message)}`);
97
+ }
70
98
 
71
- // Logger for success with a green check before the message
72
- Logger.printSuccess = (message) => console.log(`${styleText(['bold', 'green'], '✓')} ${message}`);
99
+ /**
100
+ * Writes a formatted line to stderr.
101
+ * @param {Error|string} value
102
+ * @returns {void}
103
+ */
104
+ function writeStderr(value) {
105
+ process.stderr.write(format(value) + '\n');
106
+ }
73
107
 
74
- // Logger for information with a blue 'i' before the message
75
- Logger.printInfo = (message) => console.log(`${styleText('blue', 'i')} ${message}`);
108
+ /**
109
+ * Formats a multiline message with indentation for continuation lines.
110
+ * @param {number} prefixLength
111
+ * @param {string} message
112
+ * @returns {string}
113
+ */
114
+ function formatLines(prefixLength, message) {
115
+ const indent = ' '.repeat(prefixLength);
116
+ return message
117
+ .split('\n')
118
+ .map((line, i) => (i === 0 ? line : indent + line))
119
+ .join('\n');
120
+ }
76
121
 
77
- // No decoration for Logger.println
78
- Logger.printJson = (obj) => {
79
- console.log(JSON.stringify(obj, null, 2));
80
- };
122
+ /**
123
+ * Transforms an API error object into a formatted message string.
124
+ * @param {ApiError|string} error
125
+ * @returns {string}
126
+ */
127
+ function processApiError(error) {
128
+ if (typeof error === 'string') {
129
+ return error;
130
+ }
81
131
 
82
- Logger.printErrorLine = consoleErrorWithoutColor;
132
+ const { id, message, fields } = error;
133
+ if (id == null || message == null) {
134
+ return String(error);
135
+ }
83
136
 
84
- // Only exported for testing, shouldn't be used directly
85
- Logger.processApiError = processApiError;
137
+ const fieldLines = Object.entries(fields ?? {}).map(([name, msg]) => `${name}: ${msg}`);
138
+ return [`${message} [${id}]`, ...fieldLines].join('\n');
139
+ }
@@ -0,0 +1,5 @@
1
+ export interface ApiError {
2
+ id?: string;
3
+ message?: string;
4
+ fields?: Record<string, string>;
5
+ }
@@ -1,25 +1,24 @@
1
1
  import _ from 'lodash';
2
- import { promises as fs } from 'node:fs';
3
2
  import path from 'node:path';
3
+ import { config } from '../config/config.js';
4
+ import { readJson, writeJson } from '../lib/fs.js';
4
5
  import { slugify } from '../lib/slugify.js';
5
6
  import { styleText } from '../lib/style-text.js';
6
7
  import { Logger } from '../logger.js';
7
- import { conf } from './configuration.js';
8
8
  import * as User from './user.js';
9
9
 
10
10
  // TODO: Maybe use fs-utils findPath()
11
11
  export async function loadApplicationConf(ignoreParentConfig = false, pathToFolder) {
12
12
  if (pathToFolder == null) {
13
- pathToFolder = path.dirname(conf.APP_CONFIGURATION_FILE);
13
+ pathToFolder = path.dirname(config.APP_CONFIGURATION_FILE);
14
14
  }
15
- const fileName = path.basename(conf.APP_CONFIGURATION_FILE);
15
+ const fileName = path.basename(config.APP_CONFIGURATION_FILE);
16
16
  const fullPath = path.join(pathToFolder, fileName);
17
17
  Logger.debug('Loading app configuration from ' + fullPath);
18
18
  try {
19
- const contents = await fs.readFile(fullPath);
20
- return JSON.parse(contents);
19
+ return await readJson(fullPath);
21
20
  } catch (error) {
22
- Logger.info('Cannot load app configuration from ' + conf.APP_CONFIGURATION_FILE + ' (' + error + ')');
21
+ Logger.info('Cannot load app configuration from ' + config.APP_CONFIGURATION_FILE + ' (' + error + ')');
23
22
  if (ignoreParentConfig || path.parse(pathToFolder).root === pathToFolder) {
24
23
  return { apps: [] };
25
24
  }
@@ -30,25 +29,34 @@ export async function loadApplicationConf(ignoreParentConfig = false, pathToFold
30
29
  export async function addLinkedApplication(appData, alias, ignoreParentConfig) {
31
30
  const currentConfig = await loadApplicationConf(ignoreParentConfig);
32
31
 
32
+ const generatedAlias = alias || slugify(appData.name);
33
+
34
+ const existingApp = currentConfig.apps.find((app) => app.app_id === appData.id);
35
+ if (existingApp != null) {
36
+ throw new Error(
37
+ `Application ${styleText('red', appData.id)} is already linked with alias ${styleText('red', existingApp.alias)}`,
38
+ );
39
+ }
40
+
41
+ const aliasConflict = currentConfig.apps.find((app) => app.alias === generatedAlias);
42
+ if (aliasConflict != null) {
43
+ throw new Error(
44
+ `An application with alias ${styleText('red', generatedAlias)} is already linked. Please specify a different alias with ${styleText('blue', '--alias')}.`,
45
+ );
46
+ }
47
+
33
48
  const appEntry = {
34
49
  app_id: appData.id,
35
50
  org_id: appData.ownerId,
36
51
  deploy_url: appData.deployment.httpUrl || appData.deployment.url,
37
52
  git_ssh_url: appData.deployment.url,
38
53
  name: appData.name,
39
- alias: alias || slugify(appData.name),
54
+ alias: generatedAlias,
40
55
  };
41
56
 
42
- const isPresent = currentConfig.apps.find((app) => app.app_id === appEntry.app_id) != null;
43
- if (isPresent) {
44
- throw new Error(
45
- `Application ${styleText('red', appEntry.app_id)} is already linked with alias ${styleText('red', appEntry.alias)}`,
46
- );
47
- }
48
-
49
57
  currentConfig.apps.push(appEntry);
50
58
 
51
- return persistConfig(currentConfig).then(() => {
59
+ return writeJson(config.APP_CONFIGURATION_FILE, currentConfig).then(() => {
52
60
  return appEntry;
53
61
  });
54
62
  }
@@ -69,17 +77,17 @@ export async function removeLinkedApplication({ appId, alias }) {
69
77
  delete newConfig.default;
70
78
  }
71
79
 
72
- await persistConfig(newConfig);
80
+ await writeJson(config.APP_CONFIGURATION_FILE, newConfig);
73
81
  return true;
74
82
  }
75
83
 
76
- export function findApp(config, alias) {
77
- if (_.isEmpty(config.apps)) {
84
+ export function findApp(appConfig, alias) {
85
+ if (_.isEmpty(appConfig.apps)) {
78
86
  throw new Error('There is no linked or targeted application. Use `--app` option or `clever link` command.');
79
87
  }
80
88
 
81
89
  if (alias != null) {
82
- const [appByAlias, secondAppByAlias] = _.filter(config.apps, { alias });
90
+ const [appByAlias, secondAppByAlias] = _.filter(appConfig.apps, { alias });
83
91
  if (appByAlias == null) {
84
92
  throw new Error(`There are no applications matching alias ${alias}`);
85
93
  }
@@ -91,7 +99,7 @@ export function findApp(config, alias) {
91
99
  return appByAlias;
92
100
  }
93
101
 
94
- return findDefaultApp(config);
102
+ return findDefaultApp(appConfig);
95
103
  }
96
104
 
97
105
  export function checkAlreadyLinked(apps, name, alias) {
@@ -106,13 +114,13 @@ export function checkAlreadyLinked(apps, name, alias) {
106
114
  }
107
115
  }
108
116
 
109
- function findDefaultApp(config) {
110
- if (_.isEmpty(config.apps)) {
117
+ function findDefaultApp(appConfig) {
118
+ if (_.isEmpty(appConfig.apps)) {
111
119
  throw new Error('There is no linked or targeted application. Use `--app` option or `clever link` command.');
112
120
  }
113
121
 
114
- if (config.default != null) {
115
- const defaultApp = _.find(config.apps, { app_id: config.default });
122
+ if (appConfig.default != null) {
123
+ const defaultApp = _.find(appConfig.apps, { app_id: appConfig.default });
116
124
  if (defaultApp == null) {
117
125
  throw new Error(
118
126
  'The default application is not listed anymore. This should not happen, your `.clever.json` should be fixed.',
@@ -121,19 +129,19 @@ function findDefaultApp(config) {
121
129
  return defaultApp;
122
130
  }
123
131
 
124
- if (config.apps.length === 1) {
125
- return config.apps[0];
132
+ if (appConfig.apps.length === 1) {
133
+ return appConfig.apps[0];
126
134
  }
127
135
 
128
- const aliases = _.map(config.apps, 'alias').join(', ');
136
+ const aliases = _.map(appConfig.apps, 'alias').join(', ');
129
137
  throw new Error(
130
138
  `Several applications are linked. You can specify one with the "--alias" option. Run "clever applications" to list linked applications. Available aliases: ${aliases}`,
131
139
  );
132
140
  }
133
141
 
134
142
  export async function getAppDetails({ alias }) {
135
- const config = await loadApplicationConf();
136
- const app = findApp(config, alias);
143
+ const appConfig = await loadApplicationConf();
144
+ const app = findApp(appConfig, alias);
137
145
  const ownerId = app.org_id != null ? app.org_id : await User.getCurrentId();
138
146
  return {
139
147
  appId: app.app_id,
@@ -144,14 +152,9 @@ export async function getAppDetails({ alias }) {
144
152
  };
145
153
  }
146
154
 
147
- function persistConfig(modifiedConfig) {
148
- const jsonContents = JSON.stringify(modifiedConfig, null, 2);
149
- return fs.writeFile(conf.APP_CONFIGURATION_FILE, jsonContents);
150
- }
151
-
152
155
  export async function setDefault(alias) {
153
- const config = await loadApplicationConf();
154
- const app = findApp(config, alias);
155
- const newConfig = { ...config, default: app.app_id };
156
- return persistConfig(newConfig);
156
+ const appConfig = await loadApplicationConf();
157
+ const app = findApp(appConfig, alias);
158
+ const newConfig = { ...appConfig, default: app.app_id };
159
+ return writeJson(config.APP_CONFIGURATION_FILE, newConfig);
157
160
  }
@@ -12,7 +12,7 @@ const CONFIG_KEYS = [
12
12
  ];
13
13
 
14
14
  export function listAvailableIds(asText = false) {
15
- const ids = CONFIG_KEYS.map((config) => config.id);
15
+ const ids = CONFIG_KEYS.map((configKey) => configKey.id);
16
16
  if (asText) {
17
17
  return new Intl.ListFormat('en', { style: 'short', type: 'disjunction' }).format(ids);
18
18
  }
@@ -20,9 +20,9 @@ export function listAvailableIds(asText = false) {
20
20
  }
21
21
 
22
22
  export function getById(id) {
23
- const config = CONFIG_KEYS.find((config) => config.id === id);
24
- if (config != null) {
25
- return config;
23
+ const configKey = CONFIG_KEYS.find((ck) => ck.id === id);
24
+ if (configKey != null) {
25
+ return configKey;
26
26
  }
27
27
  throw new Error(dedent`
28
28
  Invalid configuration name: ${id}.
@@ -30,8 +30,8 @@ export function getById(id) {
30
30
  `);
31
31
  }
32
32
 
33
- export function formatValue(config, value) {
34
- switch (config.kind) {
33
+ export function formatValue(configKey, value) {
34
+ switch (configKey.kind) {
35
35
  case 'bool': {
36
36
  return value;
37
37
  }
@@ -50,8 +50,8 @@ export function formatValue(config, value) {
50
50
  }
51
51
  }
52
52
 
53
- export function parse(config, value) {
54
- switch (config.kind) {
53
+ export function parse(configKey, value) {
54
+ switch (configKey.kind) {
55
55
  case 'bool':
56
56
  case 'inverted-bool':
57
57
  case 'force-https':
@@ -59,16 +59,16 @@ export function parse(config, value) {
59
59
  if (value !== 'true' && value !== 'false') {
60
60
  throw new Error('Invalid configuration value, it must be a boolean (true or false)');
61
61
  }
62
- if (config.kind === 'bool') {
62
+ if (configKey.kind === 'bool') {
63
63
  return value === 'true';
64
64
  }
65
- if (config.kind === 'inverted-bool') {
65
+ if (configKey.kind === 'inverted-bool') {
66
66
  return value === 'false';
67
67
  }
68
- if (config.kind === 'force-https') {
68
+ if (configKey.kind === 'force-https') {
69
69
  return value === 'true' ? 'ENABLED' : 'DISABLED';
70
70
  }
71
- if (config.kind === 'task') {
71
+ if (configKey.kind === 'task') {
72
72
  return value === 'false' ? 'REGULAR' : 'TASK';
73
73
  }
74
74
  return;
@@ -80,54 +80,54 @@ export function parse(config, value) {
80
80
  }
81
81
 
82
82
  export function parseOptions(options) {
83
- const newOptions = CONFIG_KEYS.map((config) => {
84
- return parseConfigOption(config, options);
83
+ const newOptions = CONFIG_KEYS.map((configKey) => {
84
+ return parseConfigOption(configKey, options);
85
85
  }).filter((a) => {
86
86
  return a != null && a[1] != null;
87
87
  });
88
88
  return Object.fromEntries(newOptions);
89
89
  }
90
90
 
91
- function parseConfigOption(config, options) {
92
- switch (config.kind) {
91
+ function parseConfigOption(configKey, options) {
92
+ switch (configKey.kind) {
93
93
  case 'bool':
94
94
  case 'inverted-bool':
95
95
  case 'force-https':
96
96
  case 'task': {
97
- const enable = options[`enable-${config.id}`];
98
- const disable = options[`disable-${config.id}`];
97
+ const enable = options[`enable-${configKey.id}`];
98
+ const disable = options[`disable-${configKey.id}`];
99
99
  if (enable && disable) {
100
- throw new Error(`You cannot use both --enable-${config.id} and --disable-${config.id} at the same time`);
100
+ throw new Error(`You cannot use both --enable-${configKey.id} and --disable-${configKey.id} at the same time`);
101
101
  }
102
102
  if (enable || disable) {
103
- if (config.kind === 'bool') {
104
- return [config.name, enable];
103
+ if (configKey.kind === 'bool') {
104
+ return [configKey.name, enable];
105
105
  }
106
- if (config.kind === 'inverted-bool') {
107
- return [config.name, disable];
106
+ if (configKey.kind === 'inverted-bool') {
107
+ return [configKey.name, disable];
108
108
  }
109
- if (config.kind === 'force-https' || config.kind === 'task') {
110
- return [config.name, parse(config, String(enable))];
109
+ if (configKey.kind === 'force-https' || configKey.kind === 'task') {
110
+ return [configKey.name, parse(configKey, String(enable))];
111
111
  }
112
112
  }
113
113
  return;
114
114
  }
115
115
  default: {
116
- return [config.name, options[config.id]];
116
+ return [configKey.name, options[configKey.id]];
117
117
  }
118
118
  }
119
119
  }
120
120
 
121
121
  export function printValue(app, id) {
122
- const config = getById(id);
123
- Logger.println(formatValue(config, app[config.name]));
122
+ const configKey = getById(id);
123
+ Logger.println(formatValue(configKey, app[configKey.name]));
124
124
  }
125
125
 
126
126
  export function printAllValues(app) {
127
127
  console.table(
128
128
  Object.fromEntries(
129
- CONFIG_KEYS.map((config) => {
130
- return [config.id, formatValue(config, app[config.name])];
129
+ CONFIG_KEYS.map((configKey) => {
130
+ return [configKey.id, formatValue(configKey, app[configKey.name])];
131
131
  }),
132
132
  ),
133
133
  );
@@ -0,0 +1,34 @@
1
+ import { get as getAddon } from '@clevercloud/client/esm/api/v2/addon.js';
2
+ import { findAddonsByNameOrId } from './ids-resolver.js';
3
+ import { sendToApi } from './send-to-api.js';
4
+
5
+ /**
6
+ * Resolve a config provider ID from a name, ID or real ID
7
+ * @param {string} addonIdOrRealIdOrName The add-on ID (addon_xxx), real ID (config_xxx) or name
8
+ * @returns {Promise<{ ownerId: string, realId: string, addonId: string }>} The owner ID, real ID and addon ID
9
+ * @throws {Error} If the add-on is not found
10
+ * @throws {Error} If multiple add-ons are found with the same name
11
+ * @throws {Error} If the add-on is not a configuration provider
12
+ */
13
+ export async function resolveConfigProviderId(addonIdOrRealIdOrName) {
14
+ const candidates = await findAddonsByNameOrId(addonIdOrRealIdOrName);
15
+
16
+ if (candidates.length === 0) {
17
+ throw new Error(`Config provider not found: ${addonIdOrRealIdOrName}`);
18
+ }
19
+
20
+ if (candidates.length > 1) {
21
+ throw new Error(`Ambiguous config provider name '${addonIdOrRealIdOrName}', please use the ID`);
22
+ }
23
+
24
+ const { ownerId, addonId, realId } = candidates[0];
25
+
26
+ // Verify that the addon is a config provider
27
+ const addon = await getAddon({ id: ownerId, addonId }).then(sendToApi);
28
+
29
+ if (addon.provider.id !== 'config-provider') {
30
+ throw new Error(`The add-on '${addonIdOrRealIdOrName}' is not a configuration provider`);
31
+ }
32
+
33
+ return { ownerId, realId, addonId };
34
+ }