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,159 @@
1
+ import { confirm } from '../lib/prompts.js';
2
+ import colors from 'colors/safe.js';
3
+ import { Logger } from '../logger.js';
4
+ import fs from 'node:fs';
5
+ import { sendToApi } from '../models/send-to-api.js';
6
+ import { openBrowser } from '../models/utils.js';
7
+ import dedent from 'dedent';
8
+ import {
9
+ todo_addSshKey as addSshKey,
10
+ todo_getSshKeys as getSshKeys,
11
+ todo_removeSshKey as removeSshKey,
12
+ } from '@clevercloud/client/esm/api/v2/user.js';
13
+
14
+ /**
15
+ * List SSH keys of the current user
16
+ * @param {object} params The command parameters
17
+ * @param {string} params.options.format The output format
18
+ */
19
+ export async function list (params) {
20
+ const { format } = params.options;
21
+
22
+ const keys = await getUserSshKeys();
23
+
24
+ switch (format) {
25
+ case 'json': {
26
+ Logger.printJson(keys);
27
+ break;
28
+ }
29
+ case 'human':
30
+ default: {
31
+ if (keys.length === 0) {
32
+ Logger.println(dedent`
33
+ ${colors.blue('🔐 No SSH keys')}
34
+
35
+ To list the SSH keys on your local system, use the following command:
36
+ ${colors.grey('ssh-add -l -E sha256')}
37
+
38
+ To create a new key pair, use the following command:
39
+ ${colors.grey('ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_clever -C "An optional comment"')}
40
+
41
+ Then add the public key to your Clever Cloud account:
42
+ ${colors.grey('clever ssh-keys add myNewKey ~/.ssh/id_ed25519_clever.pub')}
43
+ `);
44
+ return;
45
+ }
46
+
47
+ Logger.println(`🔐 ${keys.length} SSH key(s):`);
48
+ keys.forEach((key) => {
49
+ Logger.println(` • ${colors.blue(key.name)}`, colors.grey(`(${key.fingerprint})`));
50
+ });
51
+ }
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Add a SSH key to the current user
57
+ * @param {object} params The command parameters
58
+ * @param {Array<string>} params.args
59
+ */
60
+ export async function add (params) {
61
+ const [keyName, filePath] = params.args;
62
+
63
+ if (!fs.existsSync(filePath)) {
64
+ throw new Error(`File ${filePath} does not exist`);
65
+ }
66
+
67
+ const pubKeyContent = fs.readFileSync(filePath, 'utf8').trim();
68
+ Logger.debug(`SSH key file content: ${pubKeyContent}`);
69
+
70
+ try {
71
+ await addSshKey({ key: encodeURIComponent(keyName) }, JSON.stringify(pubKeyContent)).then(sendToApi);
72
+ }
73
+ catch (e) {
74
+ console.log(e?.responseBody?.id);
75
+ if (e?.responseBody?.id === 505) {
76
+ throw new Error('This SSH key is not valid, please make sure you\'re pointing to the public key file');
77
+ }
78
+ }
79
+
80
+ Logger.printSuccess(`SSH key ${keyName} added successfully`);
81
+ }
82
+
83
+ /**
84
+ * Remove a SSH key from the current user
85
+ * @param {object} params The command parameters
86
+ * @param {Array<string>} params.args
87
+ */
88
+ export async function remove (params) {
89
+ const [keyName] = params.args;
90
+
91
+ const keys = await getUserSshKeys();
92
+
93
+ if (keys.find((key) => key.name === keyName) == null) {
94
+ throw new Error(`SSH key ${colors.red(keyName)} not found`);
95
+ }
96
+
97
+ const keyNameEncoded = encodeURIComponent(keyName);
98
+ await removeSshKey({ key: keyNameEncoded }).then(sendToApi);
99
+
100
+ Logger.printSuccess(`SSH key ${keyName} removed successfully`);
101
+ }
102
+
103
+ /**
104
+ * Remove all SSH keys from the current user
105
+ * @param {object} params The command parameters
106
+ * @param {object} params.options The command options
107
+ * @param {boolean} params.options.yes The user confirmation
108
+ */
109
+ export async function removeAll (params) {
110
+ if (!params.options.yes) {
111
+ await confirm(
112
+ 'Are you sure you want to remove all your SSH keys?',
113
+ 'No SSH keys removed',
114
+ );
115
+ }
116
+
117
+ const keys = await getUserSshKeys();
118
+
119
+ if (keys.length === 0) {
120
+ Logger.println('No SSH keys to remove');
121
+ return;
122
+ }
123
+
124
+ const results = await Promise.all(
125
+ keys.map((key) => {
126
+ const keyNameEncoded = encodeURIComponent(key.name);
127
+ return removeSshKey({ key: keyNameEncoded }).then(sendToApi)
128
+ .then(() => [true, key.name])
129
+ .catch(() => [false, key.name]);
130
+ }),
131
+ );
132
+
133
+ if (results.every(([isRemoved]) => isRemoved)) {
134
+ Logger.printSuccess('All SSH keys were removed successfully');
135
+ }
136
+ else {
137
+ const keyNamesWithErrors = results
138
+ .filter(([isRemoved]) => !isRemoved)
139
+ .map(([_, keyName]) => keyName)
140
+ .join(', ');
141
+ throw new Error(`Some errors occured while removing these SSH keys: ${keyNamesWithErrors}`);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Open the SSH keys management page of the Console in your browser
147
+ * @returns {Promise<void>} A promise that resolves when the page is opened
148
+ */
149
+ export function openConsole () {
150
+ return openBrowser('/users/me/ssh-keys', 'Opening the SSH keys management page of the Console in your browser');
151
+ }
152
+
153
+ /**
154
+ * @return {Promise<Array<{ name: string, key: string, fingerprint: string }>>}
155
+ */
156
+ async function getUserSshKeys () {
157
+ const rawKeys = await getSshKeys().then(sendToApi);
158
+ return rawKeys.sort((a, b) => a.name.localeCompare(b.name));
159
+ }
@@ -1,12 +1,12 @@
1
1
  import * as Application from '../models/application.js';
2
- import * as application from '@clevercloud/client/esm/api/v2/application.js';
3
2
  import { Logger } from '../logger.js';
4
3
  import { sendToApi } from '../models/send-to-api.js';
4
+ import { undeploy as stopApplication } from '@clevercloud/client/esm/api/v2/application.js';
5
5
 
6
6
  export async function stop (params) {
7
7
  const { alias, app: appIdOrName } = params.options;
8
8
  const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
9
9
 
10
- await application.undeploy({ id: ownerId, appId }).then(sendToApi);
11
- Logger.println('App successfully stopped!');
10
+ await stopApplication({ id: ownerId, appId }).then(sendToApi);
11
+ Logger.printSuccess('Application successfully stopped!');
12
12
  }
@@ -2,10 +2,10 @@ import colors from 'colors/safe.js';
2
2
 
3
3
  import * as Namespaces from '../models/namespaces.js';
4
4
  import { sendToApi } from '../models/send-to-api.js';
5
- import * as Interact from '../models/interact.js';
5
+ import { confirm } from '../lib/prompts.js';
6
6
  import { Logger } from '../logger.js';
7
- import * as application from '@clevercloud/client/esm/api/v2/application.js';
8
7
  import * as Application from '../models/application.js';
8
+ import { addTcpRedir, getTcpRedirs, removeTcpRedir } from '@clevercloud/client/esm/api/v2/application.js';
9
9
 
10
10
  export async function listNamespaces (params) {
11
11
  const { alias, app: appIdOrName, format } = params.options;
@@ -44,7 +44,7 @@ export async function list (params) {
44
44
  const { alias, app: appIdOrName, format } = params.options;
45
45
  const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
46
46
 
47
- const redirs = await application.getTcpRedirs({ id: ownerId, appId }).then(sendToApi);
47
+ const redirs = await getTcpRedirs({ id: ownerId, appId }).then(sendToApi);
48
48
 
49
49
  switch (format) {
50
50
  case 'json': {
@@ -72,8 +72,8 @@ async function acceptPayment (result, skipConfirmation) {
72
72
  Logger.println(`Total (without taxes): ${result.totalHT}€`);
73
73
  Logger.println(colors.bold(`Total (with taxes): ${result.totalTTC}€`));
74
74
 
75
- await Interact.confirm(
76
- `You're about to pay ${result.totalTTC}€, confirm? (yes or no) `,
75
+ await confirm(
76
+ `You're about to pay ${result.totalTTC}€, confirm?`,
77
77
  'No confirmation, aborting TCP redirection creation',
78
78
  );
79
79
  }
@@ -83,10 +83,10 @@ export async function add (params) {
83
83
  const { alias, app: appIdOrName, namespace, yes: skipConfirmation } = params.options;
84
84
  const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
85
85
 
86
- const { port } = await application.addTcpRedir({ id: ownerId, appId }, { namespace }).then(sendToApi).catch((error) => {
86
+ const { port } = await addTcpRedir({ id: ownerId, appId }, { namespace }).then(sendToApi).catch((error) => {
87
87
  if (error.status === 402) {
88
88
  return acceptPayment(error.response.body, skipConfirmation).then(() => {
89
- return application.addTcpRedir({ id: ownerId, appId, payment: 'accepted' }, { namespace }).then(sendToApi);
89
+ return addTcpRedir({ id: ownerId, appId, payment: 'accepted' }, { namespace }).then(sendToApi);
90
90
  });
91
91
  }
92
92
  else {
@@ -102,7 +102,7 @@ export async function remove (params) {
102
102
  const { alias, app: appIdOrName, namespace } = params.options;
103
103
  const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
104
104
 
105
- await application.removeTcpRedir({ id: ownerId, appId, sourcePort: port, namespace }).then(sendToApi);
105
+ await removeTcpRedir({ id: ownerId, appId, sourcePort: port, namespace }).then(sendToApi);
106
106
 
107
107
  Logger.println('Successfully removed tcp redirection.');
108
108
  };
@@ -0,0 +1,146 @@
1
+ import colors from 'colors/safe.js';
2
+ import { Logger } from '../logger.js';
3
+ import { createApiToken, deleteApiToken, listApiTokens } from '../clever-client/auth-bridge.js';
4
+ import { sendToAuthBridge } from '../models/send-to-api.js';
5
+ import { getCurrent as getCurrentUser } from '../models/user.js';
6
+ import { conf } from '../models/configuration.js';
7
+ import dedent from 'dedent';
8
+ import { promptSecret } from '../lib/prompts.js';
9
+
10
+ /**
11
+ * Create a new API token
12
+ * @param {Object} params - Function parameters
13
+ * @param {[string]} params.args - Command line args
14
+ * @param {Object} params.options - Command line options
15
+ * @param {'json'|'human'} params.options.format - Output format
16
+ * @param {number} params.options.expiration - Expiration date as timestamp
17
+ */
18
+ export async function create (params) {
19
+ const [apiTokenName] = params.args;
20
+ const { expiration, format } = params.options;
21
+ const user = await getCurrentUser();
22
+
23
+ if (!user.hasPassword) {
24
+ const apiTokenListHref = new URL('/users/me/api-tokens', conf.CONSOLE_URL).href;
25
+ throw new Error(dedent`
26
+ ${colors.yellow('!')} Your Clever Cloud account is linked via GitHub and has no password. Setting one is required to create API tokens.
27
+ ${colors.blue('→')} To do so, go to the following URL: ${colors.blue(apiTokenListHref)}
28
+ `);
29
+ }
30
+
31
+ // Expire in 1 year
32
+ const dateObject = new Date();
33
+ dateObject.setFullYear(dateObject.getFullYear() + 1);
34
+ const maxExpirationDate = dateObject;
35
+
36
+ let expirationDate;
37
+ if (expiration != null) {
38
+ if (expiration > maxExpirationDate.getTime()) {
39
+ throw new Error('You cannot set an expiration date greater than 1 year');
40
+ }
41
+ expirationDate = new Date(expiration);
42
+ }
43
+ else {
44
+ expirationDate = maxExpirationDate;
45
+ }
46
+
47
+ const password = await promptSecret('Enter your password:');
48
+
49
+ let mfaCode;
50
+ if (user.preferredMFA === 'TOTP') {
51
+ mfaCode = await promptSecret('Enter your 2FA code:');
52
+ }
53
+
54
+ const tokenData = {
55
+ email: user.email,
56
+ password,
57
+ mfaCode,
58
+ name: apiTokenName,
59
+ expirationDate: expirationDate.toISOString(),
60
+ };
61
+ const createdToken = await createApiToken(tokenData).then(sendToAuthBridge).catch((error) => {
62
+ const errorCode = error?.cause?.responseBody?.code;
63
+ if (errorCode === 'invalid-credential') {
64
+ throw new Error('Invalid credentials, check your password');
65
+ }
66
+ if (errorCode === 'invalid-mfa-code') {
67
+ throw new Error('Invalid credentials, check your 2FA code');
68
+ }
69
+ throw error;
70
+ });
71
+
72
+ switch (format) {
73
+ case 'json':
74
+ Logger.printJson(createdToken);
75
+ break;
76
+ case 'human':
77
+ default:
78
+ Logger.println(dedent`
79
+ ${colors.green('✔')} API token successfully created! Store it securely, you won't able to print it again.
80
+
81
+ - API token ID : ${colors.grey(createdToken.apiTokenId)}
82
+ - API token : ${colors.grey(createdToken.apiToken)}
83
+ - Expiration : ${colors.grey(formatDate(createdToken.expirationDate))}
84
+
85
+ Export this token and use it to make authenticated requests to the Clever Cloud API through the Auth Bridge:
86
+
87
+ export CC_API_TOKEN=${createdToken.apiToken}
88
+ curl -H "Authorization: Bearer $CC_API_TOKEN" ${conf.AUTH_BRIDGE_HOST}/v2/self
89
+
90
+ Then, to revoke this token, run:
91
+ clever tokens revoke ${createdToken.apiTokenId}
92
+ `);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get information about an API token
98
+ * @param {Object} params - Function parameters
99
+ * @param {Object} params.options - Command line options
100
+ * @param {Object} params.options.format - Output format
101
+ * @returns {Promise<void>}
102
+ */
103
+ export async function list (params) {
104
+ const { format } = params.options;
105
+
106
+ const tokens = await listApiTokens().then(sendToAuthBridge);
107
+
108
+ if (format === 'json') {
109
+ Logger.printJson(tokens);
110
+ }
111
+ else {
112
+ if (tokens.length === 0) {
113
+ Logger.println(`ℹ️ No API token found, create one with ${colors.blue('clever tokens create')} command`);
114
+ }
115
+ else {
116
+ console.table(tokens.map((token) => {
117
+ return {
118
+ 'API token ID': token.apiTokenId,
119
+ Name: token.name,
120
+ 'Creation IP address': token.ip,
121
+ Creation: formatDate(token.creationDate),
122
+ Expiration: formatDate(token.expirationDate),
123
+ State: token.state,
124
+ };
125
+ }));
126
+ }
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Revoke an API token
132
+ * @param {Object} params - Function parameters
133
+ * @param {string[]} params.args - Command line arguments, token ID to revoke is expected as first argument
134
+ * @returns {Promise<void>}
135
+ */
136
+ export async function revoke (params) {
137
+ const [apiTokenId] = params.args;
138
+
139
+ await deleteApiToken(apiTokenId).then(sendToAuthBridge);
140
+
141
+ Logger.println(colors.green('✔'), 'API token successfully revoked!');
142
+ }
143
+
144
+ function formatDate (dateInput) {
145
+ return new Date(dateInput).toISOString().substring(0, 16).replace('T', ' ');
146
+ }
@@ -1,11 +1,12 @@
1
1
  import * as AppConfig from '../models/app_configuration.js';
2
2
  import * as Application from '../models/application.js';
3
3
  import { Logger } from '../logger.js';
4
+ import colors from 'colors/safe.js';
4
5
 
5
6
  export async function unlink (params) {
6
7
  const [alias] = params.args;
7
8
  const app = await AppConfig.getAppDetails({ alias });
8
9
 
9
10
  await Application.unlinkRepo(app.alias);
10
- Logger.println('Your application has been successfully unlinked!');
11
+ Logger.printSuccess(`Application ${colors.green(app.appId)} has been successfully unlinked from local alias ${colors.green(app.alias)}!`);
11
12
  };
@@ -1,8 +1,10 @@
1
+ import dedent from 'dedent';
2
+
1
3
  export const EXPERIMENTAL_FEATURES = {
2
4
  kv: {
3
5
  status: 'alpha',
4
6
  description: 'Send commands to databases such as Materia KV or Redis® directly from Clever Tools, without other dependencies',
5
- instructions: `
7
+ instructions: dedent`
6
8
  Target any compatible add-on by its name or ID (with an org ID if needed) and send commands to it:
7
9
 
8
10
  clever kv myMateriaKV SET myKey myValue
@@ -12,6 +14,49 @@ export const EXPERIMENTAL_FEATURES = {
12
14
  clever kv redis_xxxxx --org org_xxxxx PING
13
15
 
14
16
  Learn more about Materia KV: https://www.clever-cloud.com/developers/doc/addons/materia-kv/
15
- `,
17
+ `,
18
+ },
19
+ ng: {
20
+ status: 'beta',
21
+ description: 'Manage Network Groups to manage applications, add-ons, external peers through a Wireguard network',
22
+ instructions: dedent`
23
+ - Create a Network Group:
24
+ clever ng create myNG
25
+ - Create a Network Group with members (application, database add-on):
26
+ clever ng create myNG --link app_xxx,addon_xxx
27
+ - List Network Groups:
28
+ clever ng
29
+ - Delete a Network Group:
30
+ clever ng delete myNG
31
+ - (Un)Link an application or a database add-on to an existing Network Group:
32
+ clever ng link app_xxx myNG
33
+ clever ng unlink addon_xxx myNG
34
+ - Get the Wireguard configuration of a peer:
35
+ clever ng get-config peerIdOrLabel myNG
36
+ - Get details about a Network Group, a member or a peer:
37
+ clever ng get myNg
38
+ clever ng get app_xxx
39
+ clever ng get peerId
40
+ clever ng get memberLabel
41
+ - Search Network Groups, members or peers:
42
+ clever ng search myQuery
43
+
44
+ Learn more about Network Groups: https://www.clever-cloud.com/developers/doc/develop/network-groups/
45
+ `,
46
+ },
47
+ operators: {
48
+ status: 'beta',
49
+ description: 'Manage operators and their features such as Keycloak, Matomo, Metabase, Otoroshi',
50
+ instructions: dedent`
51
+ clever keycloak
52
+ clever keycloak get keycloak_xxx
53
+ clever keycloak ng enable myKeycloak
54
+
55
+ clever metabase version check myMetabase
56
+ clever metabase version update myMetabase 0.53
57
+
58
+ clever matomo open myMatomo
59
+ clever otoroshi open logs myOtoroshi
60
+ `,
16
61
  },
17
62
  };
@@ -0,0 +1,195 @@
1
+ import colors from 'colors/safe.js';
2
+ import * as networkGroup from '../models/ng.js';
3
+
4
+ import { Logger } from '../logger.js';
5
+
6
+ /**
7
+ * Print a Network Group
8
+ * @param {Object} ng The Network Group to print
9
+ * @param {string} format Output format
10
+ * @param {boolean} full If true, get more details about the Network Group (default: false)
11
+ */
12
+ function printNg (ng, format, full = false) {
13
+
14
+ switch (format) {
15
+ case 'json': {
16
+ Logger.printJson(ng);
17
+ break;
18
+ }
19
+ case 'human':
20
+ default: {
21
+ const ngData = {
22
+ ID: ng.id,
23
+ Label: ng.label,
24
+ Description: ng.description,
25
+ Network: `${ng.networkIp}`,
26
+ 'Members/Peers': `${Object.keys(ng.members)?.length}/${Object.keys(ng.peers)?.length}`,
27
+ };
28
+
29
+ console.table(ngData);
30
+
31
+ if (full) {
32
+ const members = Object.entries(ng.members)
33
+ .sort((a, b) => a[1].domainName.localeCompare(b[1].domainName))
34
+ .map(([id, member]) => ({
35
+ Domain: member.domainName,
36
+ }));
37
+ if (members.length > 0) {
38
+ Logger.println(colors.bold(' • Members:'));
39
+ console.table(members);
40
+ }
41
+
42
+ const peers = Object.entries(ng.peers)
43
+ .sort((a, b) => a[1].parentMember.localeCompare(b[1].parentMember))
44
+ .map(([id, peer]) => formatPeer(peer));
45
+ if (peers.length > 0) {
46
+ Logger.println(colors.bold(' • Peers:'));
47
+ console.table(peers);
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Print a Network Group member
56
+ * @param {Object} member The Network Group member to print
57
+ * @param {string} format Output format
58
+ */
59
+ function printMember (member, format) {
60
+
61
+ switch (format) {
62
+ case 'json': {
63
+ Logger.printJson(member);
64
+ break;
65
+ }
66
+ case 'human':
67
+ default: {
68
+ console.table({
69
+ Label: member.label,
70
+ Domain: member.domainName,
71
+ });
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Print a Network Group peer
78
+ * @param {Object} peer The Network Group peer to print
79
+ * @param {string} format Output format
80
+ * @param {boolean} full If true, get more details about the peer (default: false)
81
+ */
82
+ function printPeer (peer, format, full = false) {
83
+ switch (format) {
84
+ case 'json': {
85
+ Logger.printJson(peer);
86
+ break;
87
+ }
88
+ case 'human':
89
+ default: {
90
+ console.table(formatPeer(peer, full));
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Format a peer to print
97
+ * @param {Object} peer
98
+ * @param {boolean} full If true, get more details about the peer (default: false)
99
+ */
100
+ function formatPeer (peer, full = false) {
101
+ const peerToPrint = {
102
+ 'Parent Member': peer.parentMember,
103
+ ID: peer.id,
104
+ Label: peer.label,
105
+ Type: peer.type,
106
+ };
107
+
108
+ if (full) {
109
+ if (peer.endpoint.ngTerm != null) {
110
+ peerToPrint['Host:IP'] = `${peer.endpoint.ngTerm.host}:${peer.endpoint.ngTerm.port}`;
111
+ }
112
+ else {
113
+ peerToPrint.Host = peer.endpoint.ngIp;
114
+ }
115
+ if (peer.endpoint.publicTerm != null) {
116
+ peerToPrint['Public Term'] = `${peer.endpoint.publicTerm.host}:${peer.endpoint.publicTerm.port}`;
117
+ }
118
+ peerToPrint['Public Key'] = peer.publicKey;
119
+ }
120
+
121
+ return peerToPrint;
122
+ }
123
+
124
+ /** Print the results of a search or get action
125
+ * @param {object} idOrLabel ID or label of the Network Group, a member or a peer
126
+ * @param {object} org Organisation ID or name
127
+ * @param {string} format Output format
128
+ * @param {string} action Action to perform (search or get)
129
+ * @param {string} type Type of item to search (NetworkGroup, Member, Peer)
130
+ */
131
+ export async function printResults (idOrLabel, org, format, action, type) {
132
+
133
+ const exactMatch = action === 'get';
134
+ const toLookFor = type ?? (action === 'search' ? 'all' : 'single');
135
+
136
+ const found = await networkGroup.searchNgOrResource(idOrLabel, org, toLookFor, exactMatch);
137
+
138
+ if (!found.length) {
139
+ const searchString = idOrLabel.ngId
140
+ ?? idOrLabel.memberId
141
+ ?? idOrLabel.ngResourceLabel;
142
+ Logger.println(`${colors.blue('!')} No Network Group or resource found for ${colors.blue(searchString)}`);
143
+ return;
144
+ }
145
+
146
+ if (found.length === 1) {
147
+ switch (found[0].type) {
148
+ case 'NetworkGroup':
149
+ return printNg(found[0], format, true);
150
+ case 'Member':
151
+ return printMember(found[0], format);
152
+ case 'CleverPeer':
153
+ case 'ExternalPeer':
154
+ return printPeer(found[0], format, true);
155
+ default:
156
+ throw new Error(`Unknown item type: ${found[0].type}`);
157
+ }
158
+ }
159
+
160
+ if (action === 'search') {
161
+ // Group found items by type in a new object
162
+ const grouped = found.reduce((acc, item) => {
163
+ if (!acc[item.type]) {
164
+ acc[item.type] = [];
165
+ }
166
+ acc[item.type].push(item);
167
+ return acc;
168
+ }, {});
169
+
170
+ switch (format) {
171
+ case 'json': {
172
+ Logger.printJson(grouped);
173
+ break;
174
+ }
175
+ case 'human':
176
+ default: {
177
+ if (grouped.NetworkGroup) {
178
+ Logger.println(`${colors.bold(` • Found ${grouped.NetworkGroup.length} Network Group(s):`)}`);
179
+ grouped.NetworkGroup?.forEach((item) => printNg(item, format));
180
+ }
181
+
182
+ if (grouped.Member) {
183
+ Logger.println(`${colors.bold(` • Found ${grouped.Member.length} Member(s):`)}`);
184
+ grouped.Member?.forEach((item) => printMember(item, format));
185
+ }
186
+
187
+ if (grouped.ExternalPeer || grouped.CleverPeer) {
188
+ Logger.println(`${colors.bold(` • Found ${grouped.ExternalPeer.length + grouped.CleverPeer.length} Peer(s):`)}`);
189
+ grouped.CleverPeer?.forEach((item) => printPeer(item, format));
190
+ grouped.ExternalPeer?.forEach((item) => printPeer(item, format));
191
+ }
192
+ }
193
+ }
194
+ }
195
+ }