@reshapr/reshapr-cli 0.0.1

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.
@@ -0,0 +1,387 @@
1
+ /*
2
+ * Copyright The Reshapr Authors.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { program } from "commander";
17
+ import inquirer from 'inquirer';
18
+ import { Logger } from "../utils/logger.js";
19
+ import { ConfigUtil } from "../utils/config.js";
20
+ import { openUpdateEditor } from "../utils/editor.js";
21
+ import { Context } from "../utils/context.js";
22
+ import { CLI_LABEL } from '../constants.js';
23
+ export const configCommand = program.command('config')
24
+ .description(`Manage configuration plans in ${CLI_LABEL}`);
25
+ /** List all configuration plans */
26
+ configCommand.command('list')
27
+ .description('List all configuration plans')
28
+ .option('-s, --serviceId <id>', 'Filter by service ID')
29
+ .option('-o, --output <format>', 'Output format (json, yaml)')
30
+ .action(async (options) => {
31
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/configurationPlans`, {
32
+ method: 'GET',
33
+ headers: {
34
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
35
+ }
36
+ });
37
+ if (!response.ok) {
38
+ Logger.error('Fetching configuration plans failed: ' + response.statusText);
39
+ process.exit(1);
40
+ }
41
+ const data = await response.json().catch(err => {
42
+ Logger.error('Error parsing configuration plans response: ' + err.message);
43
+ });
44
+ if (data != null) {
45
+ if (data.length === 0) {
46
+ Logger.info('No configuration plans found.');
47
+ }
48
+ else {
49
+ Context.put('configurationPlans', data);
50
+ const longestName = longestCPName(data); // +1 for padding
51
+ const longestEndpoint = Math.max(...data.map((config) => config.backendEndpoint.length)) + 1; // +1 for padding
52
+ Logger.log(`${'ID'.padEnd(13, ' ')} ${'NAME'.padEnd(longestName, ' ')} ${'SERVICE'.padEnd(14, ' ')} ${'BACKEND'.padEnd(longestEndpoint, ' ')} API_KEY OAUTH2_CONFIG`);
53
+ data.forEach((config) => {
54
+ Logger.log(`${config.id} ${config.name.padEnd(longestName, ' ')} ${config.serviceId.padEnd(14, ' ')} ${config.backendEndpoint.padEnd(longestEndpoint, ' ')} ${(config.apiKey != undefined ? 'Yes' : 'No').padEnd(8, ' ')} ${(config.oauth2Configuration != undefined ? 'Yes' : 'No').padEnd(13, ' ')}`);
55
+ });
56
+ }
57
+ }
58
+ });
59
+ function longestCPName(expos) {
60
+ return expos.reduce((max, config) => {
61
+ return Math.max(max, config.name.length + 1);
62
+ }, 0);
63
+ }
64
+ /** Get configuration plan by ID */
65
+ configCommand.command('get <id>')
66
+ .description('Get configuration plan by ID')
67
+ .option('-o, --output <format>', 'Output format (json, yaml)')
68
+ .action(async (id) => {
69
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/configurationPlans/${id}`, {
70
+ method: 'GET',
71
+ headers: {
72
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
73
+ }
74
+ });
75
+ if (!response.ok) {
76
+ Logger.error('Fetching configuration plan failed: ' + response.statusText);
77
+ process.exit(1);
78
+ }
79
+ const config = await response.json();
80
+ Context.put('configurationPlan', config);
81
+ Logger.info('Configuration plan details');
82
+ Logger.log(`ID : ${config.id}`);
83
+ Logger.log(`Name : ${config.name}`);
84
+ Logger.log(`Organization : ${config.organizationId}`);
85
+ Logger.log(`Description : ${config.description}`);
86
+ Logger.log(`Service ID : ${config.serviceId}`);
87
+ Logger.log(`Backend Endpoint: ${config.backendEndpoint}`);
88
+ Logger.log(`Included Ops. : ${JSON.stringify(config.includedOperations || [])}`);
89
+ Logger.log(`Excluded Ops. : ${JSON.stringify(config.excludedOperations || [])}`);
90
+ Logger.log(`Backend Secret : ${config.backendSecretId != undefined ? config.backendSecretId : 'No'}`);
91
+ Logger.log(`API Key : ${config.apiKey != undefined ? config.apiKey : 'No'}`);
92
+ if (config.oauth2Configuration) {
93
+ Logger.bold('OAuth2:');
94
+ Logger.log(` Authorization Servers: ${config.oauth2Configuration.authorizationServers.join(', ')}`);
95
+ Logger.log(` JKWS URI : ${config.oauth2Configuration.jwksUri}`);
96
+ if (config.oauth2Configuration.scopes) {
97
+ Logger.log(` Scopes : ${config.oauth2Configuration.scopes.join(', ')}`);
98
+ }
99
+ }
100
+ else {
101
+ Logger.log(`OAuth2 : No`);
102
+ }
103
+ });
104
+ /** Create a new configuration plan */
105
+ configCommand.command('create <name>')
106
+ .description('Create a new configuration plan')
107
+ .requiredOption('-s, --serviceId <serviceId>', 'ID of the service')
108
+ .option('-d, --description <text>', 'Description of the configuration plan')
109
+ .requiredOption('--be, --backendEndpoint <backendEndpointURL>', 'Backend endpoint URL')
110
+ .option('--bs, --backendSecret <backendSecretId>', 'ID of the secret to authenticate with the backend endpoint')
111
+ .option('--filter', 'Filter operations to include or exclude in the configuration plan')
112
+ .option('--io, --includedOperations [<operation1>, <operation2>]', 'Include these operations when importing service artifact (JSON array). Takes precedence over excludedOperations.')
113
+ .option('--eo, --excludedOperations [<operation1>, <operation2>]', 'Exclude these operations when importing service artifact (JSON array). Only considered if no includedOperations.')
114
+ .option('--apiKey', 'Generate an API key for this configuration plan to secure the MCP endpoint')
115
+ .option('-o, --output <format>', 'Output format (json, yaml)')
116
+ .action(async (name, options) => {
117
+ if (!options.serviceId) {
118
+ Logger.error('Service ID is required to create a configuration plan.');
119
+ process.exit(1);
120
+ }
121
+ // Manage filter, included and excluded operations.
122
+ await manageInclusionsAndExclusions(options);
123
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/configurationPlans`, {
124
+ method: 'POST',
125
+ headers: {
126
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`,
127
+ 'Content-Type': 'application/json'
128
+ },
129
+ body: JSON.stringify({
130
+ name: name,
131
+ serviceId: options.serviceId,
132
+ description: options.description,
133
+ backendEndpoint: options.backendEndpoint,
134
+ backendSecretId: options.backendSecret || undefined,
135
+ includedOperations: options.includedOps || undefined,
136
+ excludedOperations: options.excludedOps || undefined,
137
+ apiKey: (options.apiKey ? 'generate-me' : undefined),
138
+ initialAccessToken: (options.internalOAuth2 ? 'generate-me' : undefined)
139
+ })
140
+ });
141
+ if (!response.ok) {
142
+ Logger.error('Creating configuration plan failed: ' + response.statusText);
143
+ process.exit(1);
144
+ }
145
+ const config = await response.json();
146
+ Logger.success(`Configuration plan '${config.name}' created successfully with ID: ${config.id}`);
147
+ Context.put('configurationPlan', config);
148
+ if (options.apiKey) {
149
+ Logger.warn(`The API Key to access future expositions is: ${config.apiKey}`);
150
+ Logger.warn('Make sure to store it securely, as it will not be shown again.');
151
+ }
152
+ });
153
+ /** Create a new configuration plan with oauth */
154
+ configCommand.command('create-oauth <name>')
155
+ .description('Create a new configuration plan with custom OAuth2 authorization')
156
+ .requiredOption('-s, --serviceId <serviceId>', 'ID of the service')
157
+ .option('-d, --description <text>', 'Description of the configuration plan')
158
+ .requiredOption('--be, --backendEndpoint <backendEndpointURL>', 'Backend endpoint URL')
159
+ .option('--bs, --backendSecret <backendSecretId>', 'ID of the secret to authenticate with the backend endpoint')
160
+ .option('--filter', 'Filter operations to include or exclude in the configuration plan')
161
+ .option('--io, --includedOperations [<operation1>, <operation2>]', 'Include these operations when importing service artifact (JSON array). Takes precedence over excludedOperations.')
162
+ .option('--eo, --excludedOperations [<operation1>, <operation2>]', 'Exclude these operations when importing service artifact (JSON array). Only considered if no includedOperations.')
163
+ .requiredOption('--oas, --oauth2AuthorizationServers [<authorizationServer1>, <authorizationServer2>]', 'A list of OAuth2 authorization server URLs to accept tokens from')
164
+ .requiredOption('--oju, --oauth2jwksUri <jwksUri>', 'The JWKS URI to validate OAuth2 tokens')
165
+ .option('--osc, --oauth2Scopes [<scope1>, <scope2>]', 'A list of OAuth2 scopes to enforce presence in the access token')
166
+ .option('-o, --output <format>', 'Output format (json, yaml)')
167
+ .action(async (name, options) => {
168
+ if (!options.serviceId) {
169
+ Logger.error('Service ID is required to create a configuration plan.');
170
+ process.exit(1);
171
+ }
172
+ // Manage filter, included and excluded operations.
173
+ await manageInclusionsAndExclusions(options);
174
+ options.oauth2Configuration = {
175
+ authorizationServers: getArrayOfStrings(options.oauth2AuthorizationServers, 'oauth2AuthorizationServers'),
176
+ jwksUri: options.oauth2jwksUri,
177
+ scopes: options.oauth2Scopes ? getArrayOfStrings(options.oauth2Scopes, 'oauth2Scopes') : undefined
178
+ };
179
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/configurationPlans`, {
180
+ method: 'POST',
181
+ headers: {
182
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`,
183
+ 'Content-Type': 'application/json'
184
+ },
185
+ body: JSON.stringify({
186
+ name: name,
187
+ serviceId: options.serviceId,
188
+ description: options.description,
189
+ backendEndpoint: options.backendEndpoint,
190
+ backendSecretId: options.backendSecret || undefined,
191
+ includedOperations: options.includedOps || undefined,
192
+ excludedOperations: options.excludedOps || undefined,
193
+ oauth2Configuration: options.oauth2Configuration
194
+ })
195
+ });
196
+ if (!response.ok) {
197
+ Logger.error('Creating configuration plan failed: ' + response.statusText);
198
+ process.exit(1);
199
+ }
200
+ const config = await response.json();
201
+ Logger.success(`Configuration plan '${config.name}' created successfully with ID: ${config.id}`);
202
+ Context.put('configurationPlan', config);
203
+ if (options.apiKey) {
204
+ Logger.warn(`The API Key to access future expositions is: ${config.apiKey}`);
205
+ Logger.warn('Make sure to store it securely, as it will not be shown again.');
206
+ }
207
+ });
208
+ configCommand.command('update <id>')
209
+ .description('Update configuration plan by ID')
210
+ .action(async (id) => {
211
+ try {
212
+ // First, fetch the current configuration plan
213
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/configurationPlans/${id}`, {
214
+ method: 'GET',
215
+ headers: {
216
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
217
+ }
218
+ });
219
+ if (!response.ok) {
220
+ Logger.error('Fetching configuration plan failed: ' + response.statusText);
221
+ process.exit(1);
222
+ }
223
+ const config = await response.json();
224
+ Logger.info(`Opening editor for configuration plan: ${config.name}`);
225
+ await openUpdateEditor(config, async (modifiedConfig) => {
226
+ // Enforce properties that are immutable.
227
+ modifiedConfig.id = config.id; // Ensure the ID remains the same.
228
+ modifiedConfig.organizationId = config.organizationId; // Ensure the organization ID remains the same.
229
+ const updateResponse = await fetch(`${ConfigUtil.config.server}/api/v1/configurationPlans/${id}`, {
230
+ method: 'PUT',
231
+ headers: {
232
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`,
233
+ 'Content-Type': 'application/json'
234
+ },
235
+ body: JSON.stringify(modifiedConfig)
236
+ });
237
+ if (!updateResponse.ok) {
238
+ Logger.error('Updating configuration plan failed: ' + updateResponse.statusText);
239
+ process.exit(1);
240
+ }
241
+ Logger.success(`Configuration plan ${id} updated successfully.`);
242
+ });
243
+ }
244
+ catch (error) {
245
+ Logger.error('Updating configuration plan failed: ' + error.message);
246
+ process.exit(1);
247
+ }
248
+ });
249
+ /** Renew or add ApiKey on configuration plan by ID */
250
+ configCommand.command('renew-api-key <id>')
251
+ .description('Renew or add ApiKey on configuration plan by ID')
252
+ .option('-o, --output <format>', 'Output format (json, yaml)')
253
+ .action(async (id) => {
254
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/configurationPlans/${id}/renewApiKey`, {
255
+ method: 'PUT',
256
+ headers: {
257
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
258
+ }
259
+ });
260
+ if (!response.ok) {
261
+ Logger.error('Renewing API key failed: ' + response.statusText);
262
+ process.exit(1);
263
+ }
264
+ const config = await response.json();
265
+ Logger.warn(`The API Key to access future expositions is: ${config.apiKey}`);
266
+ Logger.warn('Make sure to store it securely, as it will not be shown again.');
267
+ Context.put('configurationPlan', config);
268
+ });
269
+ /** Delete configuration plan by ID */
270
+ configCommand.command('delete <id>')
271
+ .option('-f, --force', 'Skip confirmation prompt')
272
+ .description('Delete configuration plan by ID')
273
+ .action(async (id, options) => {
274
+ if (!options.force) {
275
+ const confirm = await inquirer.prompt({
276
+ type: 'confirm',
277
+ name: 'confirm',
278
+ message: 'Deleting this config plan may also remove associated expositions. Are you sure you want to proceed?',
279
+ default: false
280
+ });
281
+ if (!confirm.confirm) {
282
+ Logger.info('Deletion cancelled.');
283
+ return;
284
+ }
285
+ }
286
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/configurationPlans/${id}`, {
287
+ method: 'DELETE',
288
+ headers: {
289
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
290
+ }
291
+ });
292
+ if (!response.ok) {
293
+ Logger.error('Deleting configuration plan failed: ' + response.statusText);
294
+ process.exit(1);
295
+ }
296
+ Logger.success(`Configuration plan ${id} deleted successfully.`);
297
+ });
298
+ async function manageInclusionsAndExclusions(options) {
299
+ if (options.filter) {
300
+ // We must retrieve the available operations to filter
301
+ const opsResponse = await fetch(`${ConfigUtil.config.server}/api/v1/services/${options.serviceId}`, {
302
+ method: 'GET',
303
+ headers: {
304
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
305
+ }
306
+ });
307
+ if (!opsResponse.ok) {
308
+ Logger.error('Fetching service operations failed: ' + opsResponse.statusText);
309
+ process.exit(1);
310
+ }
311
+ const service = await opsResponse.json();
312
+ console.log(`The service ${service.name} has ${service.operations.length} operation(s) available. You can filter them to include or exclude specific operations.`);
313
+ const includeOrExclude = await inquirer.prompt({
314
+ type: 'list',
315
+ name: 'includeOrExclude',
316
+ message: 'Do you want to include or exclude operations?',
317
+ choices: [
318
+ { name: 'No', value: 'no' },
319
+ { name: 'Include operations', value: 'include' },
320
+ { name: 'Exclude operations', value: 'exclude' }
321
+ ]
322
+ });
323
+ if (includeOrExclude.includeOrExclude === 'no') {
324
+ options.includedOps = [];
325
+ options.excludedOps = [];
326
+ }
327
+ else {
328
+ const opsChoices = service.operations.map((op) => ({
329
+ name: op.name,
330
+ value: op.name
331
+ }));
332
+ // Sort by operation path if OpenAPI.
333
+ if (service.type === 'REST') {
334
+ opsChoices.sort(function (x, y) {
335
+ const pathX = x.value.split('/')[1];
336
+ const pathY = y.value.split('/')[1];
337
+ return pathX.localeCompare(pathY);
338
+ });
339
+ }
340
+ else {
341
+ // Sort alphabetically for other types.
342
+ opsChoices.sort();
343
+ }
344
+ const selectedOps = await inquirer.prompt({
345
+ type: 'checkbox',
346
+ name: includeOrExclude.includeOrExclude === 'include' ? 'includedOps' : 'excludedOps',
347
+ message: `Select operations to ${includeOrExclude.includeOrExclude}:`,
348
+ choices: opsChoices,
349
+ loop: false,
350
+ pageSize: 10
351
+ });
352
+ options[includeOrExclude.includeOrExclude === 'include' ? 'includedOps' : 'excludedOps'] =
353
+ selectedOps[includeOrExclude.includeOrExclude === 'include' ? 'includedOps' : 'excludedOps'];
354
+ }
355
+ }
356
+ else {
357
+ if (options.includedOperations) {
358
+ let operations = getArrayOfStrings(options.includedOperations, 'includedOperations');
359
+ options.includedOps = operations;
360
+ }
361
+ if (!options.includedOperations && options.excludedOperations) {
362
+ let operations = getArrayOfStrings(options.excludedOperations, 'excludedOperations');
363
+ options.excludedOps = operations;
364
+ }
365
+ }
366
+ }
367
+ function getArrayOfStrings(input, name) {
368
+ if (Array.isArray(input)) {
369
+ return input;
370
+ }
371
+ else {
372
+ try {
373
+ const parsed = JSON.parse(input);
374
+ if (Array.isArray(parsed)) {
375
+ return parsed;
376
+ }
377
+ else {
378
+ throw new Error('Not an array');
379
+ }
380
+ }
381
+ catch (err) {
382
+ Logger.error(`Input must be a JSON array of strings for ${name}.`);
383
+ process.exit(1);
384
+ }
385
+ }
386
+ return [];
387
+ }
@@ -0,0 +1,222 @@
1
+ /*
2
+ * Copyright The Reshapr Authors.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { program } from "commander";
17
+ import { Logger } from "../utils/logger.js";
18
+ import { ConfigUtil } from "../utils/config.js";
19
+ import { ageFrom } from "../utils/age.js";
20
+ import { formatEndpoint } from "../utils/format.js";
21
+ import { Context } from "../utils/context.js";
22
+ import { CLI_LABEL } from '../constants.js';
23
+ export const expoCommand = program.command('expo')
24
+ .description(`Manage expositions in ${CLI_LABEL}`);
25
+ /* List all expositions */
26
+ expoCommand.command('list')
27
+ .description('List all expositions')
28
+ .option('-a, --all', 'Display also inactive expositions')
29
+ .option('-o, --output <format>', 'Output format (json, yaml)')
30
+ .action(async (options) => {
31
+ if (options.all) {
32
+ // If --all option is provided, list all expositions (active and inactive)
33
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/expositions`, {
34
+ method: 'GET',
35
+ headers: {
36
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
37
+ }
38
+ });
39
+ if (!response.ok) {
40
+ Logger.error('Fetching expositions failed: ' + response.statusText);
41
+ process.exit(1);
42
+ }
43
+ const data = await response.json().catch(err => {
44
+ Logger.error('Error parsing expositions response: ' + err.message);
45
+ });
46
+ if (data.length === 0) {
47
+ Logger.info('No expositions found.');
48
+ }
49
+ else {
50
+ Context.put('expositions', data);
51
+ const longestName = longestServiceName(data) + 1; // +1 for padding
52
+ const longestBackend = longestBackendEndpoint(data) + 1; // +1 for padding
53
+ Logger.log(`${'ID'.padEnd(13, ' ')} ${'SERVICE'.padEnd(longestName, ' ')} ${'BACKEND'.padEnd(longestBackend, ' ')} AGE`);
54
+ data.forEach((expo) => {
55
+ Logger.log(`${expo.id} ${(expo.service.name + ':' + expo.service.version).padEnd(longestName, ' ')} ${expo.configurationPlan.backendEndpoint.padEnd(longestBackend, ' ')} ${ageFrom(expo.createdOn)}`);
56
+ });
57
+ }
58
+ }
59
+ else {
60
+ // Default is to list only active expositions
61
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/expositions/active`, {
62
+ method: 'GET',
63
+ headers: {
64
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
65
+ }
66
+ });
67
+ if (!response.ok) {
68
+ Logger.error('Fetching expositions failed: ' + response.statusText);
69
+ process.exit(1);
70
+ }
71
+ const data = await response.json().catch(err => {
72
+ Logger.error('Error parsing expositions response: ' + err.message);
73
+ });
74
+ if (data.length === 0) {
75
+ Logger.info('No active expositions found. Check inactive expositions with the --all option.');
76
+ }
77
+ else {
78
+ Context.put('expositions', data);
79
+ const longestName = longestServiceName(data) + 1; // +1 for padding
80
+ const longestBackend = longestBackendEndpoint(data) + 1; // +1 for padding
81
+ const longestEndpoints = longestGatewaysFQDNs(data) + 1; // +1 for padding
82
+ Logger.log(`${'ID'.padEnd(13, ' ')} ${'SERVICE'.padEnd(longestName, ' ')} ${'BACKEND'.padEnd(longestBackend, ' ')} ${'ENDPOINTS'.padEnd(longestEndpoints, ' ')} AGE`);
83
+ data.forEach((expo) => {
84
+ let allFqdns = uniqueFQDNs(expo.gateways);
85
+ Logger.log(`${expo.id} ${(expo.service.name + ':' + expo.service.version).padEnd(longestName, ' ')} ${expo.configurationPlan.backendEndpoint.padEnd(longestBackend, ' ')} ${allFqdns.join(',').padEnd(longestEndpoints, ' ')} ${ageFrom(expo.createdOn)}`);
86
+ });
87
+ }
88
+ }
89
+ });
90
+ function longestServiceName(expos) {
91
+ return expos.reduce((max, expo) => {
92
+ return Math.max(max, expo.service.name.length + expo.service.version.length + 1);
93
+ }, 0);
94
+ }
95
+ function longestBackendEndpoint(expos) {
96
+ return expos.reduce((max, expo) => {
97
+ return Math.max(max, expo.configurationPlan.backendEndpoint ? expo.configurationPlan.backendEndpoint.length : 0);
98
+ }, 0);
99
+ }
100
+ function uniqueFQDNs(gateways) {
101
+ let allFqdns = [];
102
+ gateways.forEach(gateway => {
103
+ gateway.fqdns.filter(fqdn => !allFqdns.includes(fqdn)).forEach(fqdn => allFqdns.push(fqdn));
104
+ });
105
+ return allFqdns;
106
+ }
107
+ function longestGatewaysFQDNs(expos) {
108
+ return expos.reduce((max, expo) => {
109
+ return Math.max(max, uniqueFQDNs(expo.gateways).join(',').length);
110
+ }, 0);
111
+ }
112
+ /** Get exposition by ID */
113
+ expoCommand.command('get <id>')
114
+ .description('Get details of an exposition by ID')
115
+ .option('-o, --output <format>', 'Output format (json, yaml)')
116
+ .action(async (id) => {
117
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/expositions/${id}`, {
118
+ method: 'GET',
119
+ headers: {
120
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
121
+ }
122
+ });
123
+ if (!response.ok) {
124
+ Logger.error('Fetching exposition failed: ' + response.statusText);
125
+ process.exit(1);
126
+ }
127
+ const exposition = await response.json();
128
+ Context.put('exposition', exposition);
129
+ await displayExpositionDetails(exposition);
130
+ });
131
+ /** Create a new exposition */
132
+ expoCommand.command('create')
133
+ .description('Create a new exposition')
134
+ .requiredOption('-c, --configuration <id>', 'Configuration Plan ID to use')
135
+ .requiredOption('-g, --gateway-group <id>', 'Gateway Group ID to use')
136
+ .option('-o, --output <format>', 'Output format (json, yaml)')
137
+ .action(async (options) => {
138
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/expositions`, {
139
+ method: 'POST',
140
+ headers: {
141
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`,
142
+ 'Content-Type': 'application/json'
143
+ },
144
+ body: JSON.stringify({
145
+ configurationPlanId: options.configuration,
146
+ gatewayGroupId: options.gatewayGroup
147
+ })
148
+ });
149
+ if (!response.ok) {
150
+ Logger.error('Failed to create exposition: ' + response.statusText);
151
+ if (response.status === 429) {
152
+ Logger.error('Exposition creation quota exceeded. Check your quotas.');
153
+ }
154
+ process.exit(1);
155
+ }
156
+ const data = await response.json().catch(err => {
157
+ Logger.error('Error parsing exposition creation response: ' + err.message);
158
+ process.exit(1);
159
+ });
160
+ Context.put('exposition', data);
161
+ Logger.success(`Exposition created successfully with ID: ${data.id}`);
162
+ await displayExpositionDetails(data);
163
+ });
164
+ /** Delete exposition by ID */
165
+ expoCommand.command('delete <id>')
166
+ .description('Delete an exposition by ID')
167
+ .action(async (id) => {
168
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/expositions/${id}`, {
169
+ method: 'DELETE',
170
+ headers: {
171
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
172
+ }
173
+ });
174
+ if (!response.ok) {
175
+ Logger.error('Deleting exposition failed: ' + response.statusText);
176
+ process.exit(1);
177
+ }
178
+ Logger.success(`Exposition ${id} deleted successfully.`);
179
+ });
180
+ async function displayExpositionDetails(exposition) {
181
+ Logger.info('Exposition details');
182
+ Logger.log(`ID : ${exposition.id}`);
183
+ Logger.log(`Created on : ${exposition.createdOn}`);
184
+ Logger.log(`Organization: ${exposition.organizationId}`);
185
+ Logger.bold('Service:');
186
+ Logger.log(` ID : ${exposition.service.id}`);
187
+ Logger.log(` Name : ${exposition.service.name}`);
188
+ Logger.log(` Version: ${exposition.service.version}`);
189
+ Logger.log(` Type : ${exposition.service.type}`);
190
+ Logger.bold('Configuration Plan');
191
+ Logger.log(` ID : ${exposition.configurationPlan.id}`);
192
+ Logger.log(` Name : ${exposition.configurationPlan.name}`);
193
+ Logger.log(` BackendEndpoint: ${exposition.configurationPlan.backendEndpoint}`);
194
+ Logger.log(` Included Ops. : ${JSON.stringify(exposition.configurationPlan.includedOperations || [])}`);
195
+ Logger.log(` Excluded Ops. : ${JSON.stringify(exposition.configurationPlan.excludedOperations || [])}`);
196
+ Logger.bold('Gateway Group');
197
+ Logger.log(` ID : ${exposition.gatewayGroup.id}`);
198
+ Logger.log(` Name : ${exposition.gatewayGroup.name}`);
199
+ Logger.log(` Labels: ${JSON.stringify(exposition.gatewayGroup.labels)}`);
200
+ // Now check if acctive and print active endpoints if any.
201
+ const activeResponse = await fetch(`${ConfigUtil.config.server}/api/v1/expositions/active/${exposition.id}`, {
202
+ method: 'GET',
203
+ headers: {
204
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
205
+ }
206
+ });
207
+ if (activeResponse.status === 404) {
208
+ Logger.warn(`No active exposition found for the Exposition ${exposition.id}. Maybe there's no running Gateway at the moment?`);
209
+ process.exit(0);
210
+ }
211
+ const activeExpositions = await activeResponse.json();
212
+ Context.put('gateways', activeExpositions.gateways);
213
+ let allFqdns = uniqueFQDNs(activeExpositions.gateways);
214
+ Context.put('endpoints', allFqdns.map(fqdn => formatEndpoint(fqdn, exposition.organizationId, exposition.service.name, exposition.service.version)));
215
+ Logger.bold('Gateway Endpoints');
216
+ activeExpositions.gateways.forEach((gateway) => {
217
+ Logger.log(` - ID : ${gateway.id}`);
218
+ Logger.log(` Name : ${gateway.name}`);
219
+ Logger.log(` Endpoints: ${gateway.fqdns.map((fqdn) => formatEndpoint(fqdn, exposition.organizationId, exposition.service.name, exposition.service.version))
220
+ .join(', ')}`);
221
+ });
222
+ }