clever-tools 4.7.0 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/bin/clever.js +11 -1
  2. package/package.json +2 -2
  3. package/src/clever-client/drains.js +18 -18
  4. package/src/commands/README.md +1 -0
  5. package/src/commands/accesslogs/accesslogs.command.js +2 -2
  6. package/src/commands/activity/activity.command.js +1 -1
  7. package/src/commands/addon/addon.create.command.js +2 -3
  8. package/src/commands/addon/addon.delete.command.js +2 -2
  9. package/src/commands/addon/addon.docs.md +2 -2
  10. package/src/commands/addon/addon.env.command.js +4 -6
  11. package/src/commands/config-provider/config-provider.command.js +0 -4
  12. package/src/commands/config-provider/config-provider.import.command.js +1 -1
  13. package/src/commands/config-provider/config-provider.list.command.js +8 -19
  14. package/src/commands/config-provider/config-provider.open.command.js +1 -1
  15. package/src/commands/console/console.command.js +3 -2
  16. package/src/commands/curl/curl.command.js +0 -1
  17. package/src/commands/database/database.backups.command.js +4 -7
  18. package/src/commands/database/database.backups.download.command.js +5 -7
  19. package/src/commands/database/database.command.js +0 -4
  20. package/src/commands/database/database.docs.md +2 -2
  21. package/src/commands/deploy/deploy.command.js +1 -1
  22. package/src/commands/deploy/deploy.docs.md +1 -1
  23. package/src/commands/drain/drain.command.js +14 -9
  24. package/src/commands/drain/drain.create.command.js +9 -13
  25. package/src/commands/drain/drain.disable.command.js +7 -7
  26. package/src/commands/drain/drain.docs.md +7 -1
  27. package/src/commands/drain/drain.enable.command.js +7 -7
  28. package/src/commands/drain/drain.get.command.js +12 -8
  29. package/src/commands/drain/drain.remove.command.js +7 -7
  30. package/src/commands/emails/emails.open.command.js +1 -4
  31. package/src/commands/global.commands.js +18 -0
  32. package/src/commands/global.options.js +7 -7
  33. package/src/commands/help/help.command.js +0 -3
  34. package/src/commands/k8s/k8s.command.js +0 -4
  35. package/src/commands/k8s/k8s.delete.command.js +2 -2
  36. package/src/commands/k8s/k8s.docs.md +1 -1
  37. package/src/commands/logs/logs.command.js +4 -5
  38. package/src/commands/ng/ng.docs.md +0 -1
  39. package/src/commands/ng/ng.get-config.command.js +3 -14
  40. package/src/commands/ng/ng.options.js +1 -1
  41. package/src/commands/notify-email/notify-email.remove.command.js +2 -2
  42. package/src/commands/oauth-consumers/oauth-consumers.args.js +8 -0
  43. package/src/commands/oauth-consumers/oauth-consumers.command.js +6 -0
  44. package/src/commands/oauth-consumers/oauth-consumers.create.command.js +64 -0
  45. package/src/commands/oauth-consumers/oauth-consumers.delete.command.js +33 -0
  46. package/src/commands/oauth-consumers/oauth-consumers.docs.md +130 -0
  47. package/src/commands/oauth-consumers/oauth-consumers.get.command.js +67 -0
  48. package/src/commands/oauth-consumers/oauth-consumers.list.command.js +34 -0
  49. package/src/commands/oauth-consumers/oauth-consumers.open.command.js +18 -0
  50. package/src/commands/oauth-consumers/oauth-consumers.options.js +48 -0
  51. package/src/commands/oauth-consumers/oauth-consumers.update.command.js +83 -0
  52. package/src/commands/open/open.command.js +1 -1
  53. package/src/commands/profile/profile.open.command.js +1 -1
  54. package/src/commands/restart/restart.docs.md +1 -1
  55. package/src/commands/ssh-keys/ssh-keys.open.command.js +1 -1
  56. package/src/commands/webhooks/webhooks.remove.command.js +2 -2
  57. package/src/lib/cliparse-patched.js +2 -0
  58. package/src/lib/get-command-info.js +3 -1
  59. package/src/lib/get-command-info.types.d.ts +4 -0
  60. package/src/lib/operator-commands.js +11 -25
  61. package/src/lib/print-items-by-owner.js +38 -0
  62. package/src/lib/prompts.js +23 -1
  63. package/src/lib/zod-utils.js +51 -0
  64. package/src/models/drain.js +17 -0
  65. package/src/models/ids-resolver.js +38 -20
  66. package/src/models/ng.js +2 -2
  67. package/src/models/notification.js +0 -4
  68. package/src/models/oauth-consumer.js +77 -0
  69. package/src/models/utils.js +1 -1
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @typedef {Object} ZodSchemaLike
3
3
  * @property {{ type?: string, innerType?: ZodSchemaLike, defaultValue?: unknown, typeName?: string, in?: ZodSchemaLike }} [_def]
4
+ * @property {string[]} [options]
4
5
  */
5
6
 
6
7
  /**
@@ -37,6 +38,56 @@ export function isRequired(schema) {
37
38
  return true;
38
39
  }
39
40
 
41
+ /**
42
+ * Extracts enum values from a Zod schema.
43
+ * Handles wrapped schemas (default, optional, nullable, pipe).
44
+ * @param {ZodSchemaLike} schema
45
+ * @return {string[] | null}
46
+ */
47
+ export function getEnumValues(schema) {
48
+ /** @type {ZodSchemaLike | undefined} */
49
+ let current = schema;
50
+ while (current) {
51
+ const schemaType = current._def?.type;
52
+ if (schemaType === 'enum') {
53
+ return current.options ?? null;
54
+ }
55
+ if (schemaType === 'default' || schemaType === 'optional' || schemaType === 'nullable') {
56
+ current = current._def?.innerType;
57
+ continue;
58
+ }
59
+ if (schemaType === 'pipe') {
60
+ current = current._def?.in;
61
+ continue;
62
+ }
63
+ break;
64
+ }
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * Unwraps optional/nullable/default/pipe wrappers to get the inner schema.
70
+ * @param {ZodSchemaLike} schema
71
+ * @return {ZodSchemaLike}
72
+ */
73
+ export function unwrapSchema(schema) {
74
+ /** @type {ZodSchemaLike | undefined} */
75
+ let current = schema;
76
+ while (current) {
77
+ const schemaType = current._def?.type;
78
+ if (schemaType === 'optional' || schemaType === 'nullable' || schemaType === 'default') {
79
+ current = current._def?.innerType;
80
+ continue;
81
+ }
82
+ if (schemaType === 'pipe') {
83
+ current = current._def?.in;
84
+ continue;
85
+ }
86
+ break;
87
+ }
88
+ return /** @type {ZodSchemaLike} */ (current);
89
+ }
90
+
40
91
  /**
41
92
  * Extracts the default value from a Zod schema.
42
93
  * Handles wrapped schemas (optional, nullable, pipe).
@@ -1,3 +1,20 @@
1
+ import * as Application from './application.js';
2
+ import { resolveAddon } from './ids-resolver.js';
3
+
4
+ export async function resolveDrainResource(alias, appIdOrName, addonIdOrRealId) {
5
+ if (addonIdOrRealId != null && (appIdOrName != null || alias != null)) {
6
+ throw new Error('`--addon` cannot be combined with `--app` or `--alias`');
7
+ }
8
+
9
+ if (addonIdOrRealId != null) {
10
+ const { ownerId, realId } = await resolveAddon(addonIdOrRealId);
11
+ return { ownerId, resourceId: realId };
12
+ }
13
+
14
+ const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
15
+ return { ownerId, resourceId: appId };
16
+ }
17
+
1
18
  export const DRAIN_TYPES = {
2
19
  DATADOG: { apiCode: 'DATADOG', cliCode: 'datadog', label: 'Datadog' },
3
20
  ELASTICSEARCH: { apiCode: 'ELASTICSEARCH', cliCode: 'elasticsearch', label: 'Elasticsearch' },
@@ -31,28 +31,17 @@ export async function resolveOwnerId(id) {
31
31
  return getIdFromCacheOrSummary((ids) => ids.owners[id]);
32
32
  }
33
33
 
34
- export async function resolveAddonId(id) {
35
- const addonId = await getIdFromCacheOrSummary((ids) => {
36
- return ids.addons[id] != null ? ids.addons[id].addonId : null;
34
+ export async function resolveAddon(addonIdOrRealId) {
35
+ const result = await getIdFromCacheOrSummary((ids) => {
36
+ const addon = ids.addons[addonIdOrRealId];
37
+ if (addon == null) return null;
38
+ const ownerId = ids.owners[addon.realId];
39
+ return { ownerId, addonId: addon.addonId, realId: addon.realId };
37
40
  });
38
-
39
- if (addonId != null) {
40
- return addonId;
41
- }
42
-
43
- throw new Error(`Add-on ${id} does not exist`);
44
- }
45
-
46
- export async function resolveRealId(id) {
47
- const realId = await getIdFromCacheOrSummary((ids) => {
48
- return ids.addons[id] != null ? ids.addons[id].realId : null;
49
- });
50
-
51
- if (realId != null) {
52
- return realId;
41
+ if (result == null) {
42
+ throw new Error(`Add-on ${addonIdOrRealId} does not exist`);
53
43
  }
54
-
55
- throw new Error(`Add-on ${id} does not exist foo`);
44
+ return result;
56
45
  }
57
46
 
58
47
  async function getIdFromCacheOrSummary(callback) {
@@ -94,6 +83,9 @@ async function getIdsFromSummary() {
94
83
  ids.addons[addon.id] = addonIds;
95
84
  ids.addons[addon.realId] = addonIds;
96
85
  }
86
+ for (const consumer of owner.consumers) {
87
+ ids.owners[consumer.key] = owner.id;
88
+ }
97
89
  }
98
90
 
99
91
  return ids;
@@ -174,6 +166,32 @@ export async function findAddonsByAddonProvider(provider) {
174
166
  return candidates;
175
167
  }
176
168
 
169
+ /**
170
+ * Find OAuth consumers by key or name.
171
+ * Fast path: tries the IDs cache first (works for keys).
172
+ * Fallback: fetches the summary and searches by key or name.
173
+ * @param {string} keyOrName
174
+ * @returns {Promise<Array<{ ownerId: string, key: string, name: string }>>}
175
+ */
176
+ export async function findOauthConsumersByKeyOrName(keyOrName) {
177
+ const ownerIdFromCache = await resolveOwnerId(keyOrName);
178
+ if (ownerIdFromCache != null) {
179
+ return [{ ownerId: ownerIdFromCache, key: keyOrName }];
180
+ }
181
+
182
+ const summary = await getSummary().then(sendToApi);
183
+ const consumers = [summary.user, ...summary.organisations].flatMap((owner) => {
184
+ return owner.consumers.map((c) => ({ ownerId: owner.id, name: c.name, key: c.key }));
185
+ });
186
+
187
+ const matchByKey = consumers.find((c) => c.key === keyOrName);
188
+ if (matchByKey) {
189
+ return [matchByKey];
190
+ }
191
+
192
+ return consumers.filter((c) => c.name === keyOrName);
193
+ }
194
+
177
195
  /**
178
196
  * Get the owner ID from an Organisation ID or name
179
197
  * @param {object} orgIdOrName The Organisation ID or name
package/src/models/ng.js CHANGED
@@ -79,7 +79,7 @@ export async function destroy(ngIdOrLabel, orgaIdOrName) {
79
79
  * @param {object} peerIdOrLabel The Peer ID or Label
80
80
  * @param {object} ngIdOrLabel The Network Group ID or Label
81
81
  * @param {object} orgaIdOrName The owner ID or name
82
- * @returns {Promise<Object>} The Peer WireGuard configuration
82
+ * @returns {Promise<string>} The Peer WireGuard configuration
83
83
  * @throws {Error} If the Peer is not found
84
84
  * @throws {Error} If the Network Group is not found
85
85
  * @throws {Error} If the Peer is not in the Network Group
@@ -113,7 +113,7 @@ export async function getPeerConfig(peerIdOrLabel, ngIdOrLabel, orgaIdOrName) {
113
113
  networkGroupId: parentNg.id,
114
114
  peerId: peer.id,
115
115
  }).then(sendToApi);
116
- Logger.debug(`Received from API:\n${JSON.stringify(result, null, 2)}`);
116
+ Logger.debug(`Received from API:\n${result}`);
117
117
 
118
118
  return result;
119
119
  }
@@ -6,10 +6,6 @@ export function listMetaEvents() {
6
6
  return ['META_SERVICE_LIFECYCLE', 'META_DEPLOYMENT_RESULT', 'META_SERVICE_MANAGEMENT', 'META_CREDITS'];
7
7
  }
8
8
 
9
- export function getOrgaIdOrUserId(orgIdOrName) {
10
- return orgIdOrName == null ? User.getCurrentId() : Organisation.getId(orgIdOrName);
11
- }
12
-
13
9
  export async function getOwnerAndApp(org, useLinkedApp) {
14
10
  if (org != null) {
15
11
  return { ownerId: await Organisation.getId(org) };
@@ -0,0 +1,77 @@
1
+ import { get as getOauthConsumer } from '@clevercloud/client/esm/api/v2/oauth-consumer.js';
2
+ import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
3
+ import { promptCheckbox } from '../lib/prompts.js';
4
+ import { styleText } from '../lib/style-text.js';
5
+ import { findOauthConsumersByKeyOrName } from './ids-resolver.js';
6
+ import { sendToApi } from './send-to-api.js';
7
+
8
+ /* eslint-disable camelcase */
9
+ export const OAUTH_RIGHTS = {
10
+ access_organisations: 'access-organisations',
11
+ access_organisations_bills: 'access-organisations-bills',
12
+ access_organisations_consumption_statistics: 'access-organisations-consumption-statistics',
13
+ access_organisations_credit_count: 'access-organisations-credit-count',
14
+ access_personal_information: 'access-personal-information',
15
+ manage_organisations: 'manage-organisations',
16
+ manage_organisations_applications: 'manage-organisations-applications',
17
+ manage_organisations_members: 'manage-organisations-members',
18
+ manage_organisations_services: 'manage-organisations-services',
19
+ manage_personal_information: 'manage-personal-information',
20
+ manage_ssh_keys: 'manage-ssh-keys',
21
+ };
22
+ /* eslint-enable camelcase */
23
+
24
+ export function rightsFromList(requestedRights = []) {
25
+ const hasAll = requestedRights.includes('all');
26
+ return Object.fromEntries(
27
+ Object.entries(OAUTH_RIGHTS).map(([apiName, cliName]) => {
28
+ return [apiName, hasAll || requestedRights.includes(cliName)];
29
+ }),
30
+ );
31
+ }
32
+
33
+ const READONLY_RIGHTS = ['almighty'];
34
+
35
+ export function removeReadonlyRights(rights) {
36
+ return Object.fromEntries(
37
+ Object.entries(rights).filter(([key]) => {
38
+ return !READONLY_RIGHTS.includes(key);
39
+ }),
40
+ );
41
+ }
42
+
43
+ export async function promptRights(existingRights) {
44
+ const choices = Object.entries(OAUTH_RIGHTS).map(([apiName, cliName]) => ({
45
+ name: cliName,
46
+ value: cliName,
47
+ checked: existingRights?.[apiName] ?? false,
48
+ }));
49
+
50
+ const selected = await promptCheckbox('Select rights', choices);
51
+
52
+ return rightsFromList(selected);
53
+ }
54
+
55
+ export async function getAllConsumers() {
56
+ const summary = await getSummary().then(sendToApi);
57
+ return [summary.user, ...summary.organisations].flatMap((owner) => {
58
+ return owner.consumers.map((c) => ({ ownerId: owner.id, ownerName: owner.name, ...c }));
59
+ });
60
+ }
61
+
62
+ export async function resolveOauthConsumer(keyOrName) {
63
+ const candidates = await findOauthConsumersByKeyOrName(keyOrName);
64
+
65
+ if (candidates.length === 0) {
66
+ throw new Error(`OAuth consumer not found: ${styleText('red', keyOrName)}`);
67
+ }
68
+
69
+ if (candidates.length > 1) {
70
+ const list = candidates.map((c) => ` - ${c.name} ${styleText('grey', `(${c.key})`)}`).join('\n');
71
+ throw new Error(`Ambiguous name ${styleText('red', keyOrName)}, use the key instead:\n${list}`);
72
+ }
73
+
74
+ const { ownerId, key } = candidates[0];
75
+ const oauthConsumer = await getOauthConsumer({ id: ownerId, key }).then(sendToApi);
76
+ return { ownerId, ...oauthConsumer };
77
+ }
@@ -26,7 +26,7 @@ export function openBrowser(urlOrPath, message) {
26
26
  const url = urlOrPath.startsWith('/') ? `${config.CONSOLE_URL}${urlOrPath}` : urlOrPath;
27
27
 
28
28
  Logger.debug(`Opening URL "${url}" in browser`);
29
- Logger.println(message);
29
+ Logger.println(`🌐 ${message}`);
30
30
 
31
31
  return openPage(url, { wait: false });
32
32
  }