@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.
- package/README.md +44 -0
- package/dist/cli.js +96 -0
- package/dist/commands/api-token.js +123 -0
- package/dist/commands/attach.js +72 -0
- package/dist/commands/config.js +387 -0
- package/dist/commands/expo.js +222 -0
- package/dist/commands/gateway-group.js +108 -0
- package/dist/commands/import.js +230 -0
- package/dist/commands/index.js +27 -0
- package/dist/commands/info.js +47 -0
- package/dist/commands/login.js +164 -0
- package/dist/commands/logout.js +25 -0
- package/dist/commands/quotas.js +60 -0
- package/dist/commands/secret.js +280 -0
- package/dist/commands/service.js +145 -0
- package/dist/constants.js +17 -0
- package/dist/utils/age.js +49 -0
- package/dist/utils/config.js +80 -0
- package/dist/utils/context.js +36 -0
- package/dist/utils/editor.js +70 -0
- package/dist/utils/format.js +21 -0
- package/dist/utils/logger.js +53 -0
- package/dist/version.js +1 -0
- package/package.json +43 -0
- package/src/cli.ts +104 -0
|
@@ -0,0 +1,280 @@
|
|
|
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 fs from 'node:fs';
|
|
17
|
+
import { program } from "commander";
|
|
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 secretCommand = program.command('secret')
|
|
24
|
+
.description(`Manage secrets in ${CLI_LABEL}`);
|
|
25
|
+
/* List all secrets */
|
|
26
|
+
secretCommand.command('list')
|
|
27
|
+
.description('List all secrets')
|
|
28
|
+
.option('-o, --output <format>', 'Output format (json, yaml)')
|
|
29
|
+
.action(async () => {
|
|
30
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/secrets/refs`, {
|
|
31
|
+
method: 'GET',
|
|
32
|
+
headers: {
|
|
33
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
Logger.error('Fetching secrets failed: ' + response.statusText);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const data = await response.json().catch(err => {
|
|
41
|
+
Logger.error('Error parsing secrets response: ' + err.message);
|
|
42
|
+
});
|
|
43
|
+
if (data != null) {
|
|
44
|
+
if (data.length === 0) {
|
|
45
|
+
Logger.info('No secrets found.');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
Context.put('secrets', data);
|
|
49
|
+
const longestName = longestSecretName(data) + 1; // +1 for padding
|
|
50
|
+
Logger.log(`${'ID'.padEnd(13, ' ')} ${'NAME'.padEnd(longestName, ' ')} TYPE DESCRIPTION`);
|
|
51
|
+
data.forEach((secret) => {
|
|
52
|
+
Logger.log(`${secret.id} ${secret.name.padEnd(longestName, ' ')} ${secret.type} ${secret.description || ''}`);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
function longestSecretName(secrets) {
|
|
58
|
+
return secrets.reduce((max, secret) => {
|
|
59
|
+
return Math.max(max, secret.name.length);
|
|
60
|
+
}, 0);
|
|
61
|
+
}
|
|
62
|
+
/* Get secret by ID */
|
|
63
|
+
secretCommand.command('get <id>')
|
|
64
|
+
.description('Get details of a secret by ID')
|
|
65
|
+
.option('-o, --output <format>', 'Output format (json, yaml)')
|
|
66
|
+
.action(async (id) => {
|
|
67
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/secrets/${id}`, {
|
|
68
|
+
method: 'GET',
|
|
69
|
+
headers: {
|
|
70
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
Logger.error('Fetching secret failed: ' + response.statusText);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const data = await response.json();
|
|
78
|
+
Context.put('secret', data);
|
|
79
|
+
Logger.info('Secret details');
|
|
80
|
+
Logger.log(`ID : ${data.id}`);
|
|
81
|
+
Logger.log(`Name : ${data.name}`);
|
|
82
|
+
Logger.log(`Organization: ${data.organizationId}`);
|
|
83
|
+
Logger.log(`Type : ${data.type}`);
|
|
84
|
+
if (data.username) {
|
|
85
|
+
Logger.log(`Username : ${data.username}`);
|
|
86
|
+
}
|
|
87
|
+
if (data.password) {
|
|
88
|
+
Logger.log(`Password : ${data.password}`);
|
|
89
|
+
}
|
|
90
|
+
if (data.token) {
|
|
91
|
+
Logger.log(`Token : ${data.token}`);
|
|
92
|
+
}
|
|
93
|
+
if (!data.useElicitation && data.tokenHeader) {
|
|
94
|
+
Logger.log(`Token Header: ${data.tokenHeader}`);
|
|
95
|
+
}
|
|
96
|
+
if (data.certPem) {
|
|
97
|
+
Logger.log(`Certificate :`);
|
|
98
|
+
Logger.log(data.certPem);
|
|
99
|
+
}
|
|
100
|
+
Logger.log(`Description : ${data.description || ''}`);
|
|
101
|
+
if (data.useElicitation) {
|
|
102
|
+
if (data.tokenHeader) {
|
|
103
|
+
Logger.bold(`3rd Party Token:`);
|
|
104
|
+
Logger.log(` Sensitive Header: ${data.tokenHeader}`);
|
|
105
|
+
}
|
|
106
|
+
if (data.thirdPartyOauth2Configuration) {
|
|
107
|
+
Logger.bold(`3rd Party OAuth2:`);
|
|
108
|
+
Logger.log(` OAuth2 Client ID : ${data.oauth2ClientConfiguration.clientId}`);
|
|
109
|
+
Logger.log(` OAuth2 Authorization Endpoint: ${data.oauth2ClientConfiguration.authorizationEndpoint}`);
|
|
110
|
+
Logger.log(` OAuth2 Token Endpoint : ${data.oauth2ClientConfiguration.tokenEndpoint}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
/* Create a secret with name */
|
|
115
|
+
secretCommand.command('create <name>')
|
|
116
|
+
.description('Create a new secret')
|
|
117
|
+
.option('-A, --artifact', 'Secret of ARTIFACT type')
|
|
118
|
+
.option('-B, --backend', 'Secret of BACKEND type')
|
|
119
|
+
.option('-d, --description <description>', 'Description for the secret')
|
|
120
|
+
.option('-u, --username <username>', 'Username for the secret (if applicable)')
|
|
121
|
+
.option('-p, --password <password>', 'Password for the secret (if applicable)')
|
|
122
|
+
.option('-t, --token <token>', 'Token for the secret (if applicable)')
|
|
123
|
+
.option('-h, --tokenHeader <tokenHeader>', 'Token Header for the secret (if not Authorization Bearer)')
|
|
124
|
+
.option('-c, --certificate <path>', 'Path to the certificate file in PEM format (if applicable)')
|
|
125
|
+
.option('-o, --output <format>', 'Output format (json, yaml)')
|
|
126
|
+
.action(async (name, options) => {
|
|
127
|
+
// Initialize the secret object.
|
|
128
|
+
let secret = {
|
|
129
|
+
name: name,
|
|
130
|
+
description: options.description || '',
|
|
131
|
+
};
|
|
132
|
+
// Populate the secret object based on the options provided.
|
|
133
|
+
if (options.artifact) {
|
|
134
|
+
secret.type = 'ARTIFACT';
|
|
135
|
+
}
|
|
136
|
+
else if (options.backend) {
|
|
137
|
+
secret.type = 'ENDPOINT';
|
|
138
|
+
}
|
|
139
|
+
if (options.username) {
|
|
140
|
+
secret.username = options.username;
|
|
141
|
+
}
|
|
142
|
+
if (options.password) {
|
|
143
|
+
secret.password = options.password;
|
|
144
|
+
}
|
|
145
|
+
if (options.token) {
|
|
146
|
+
secret.token = options.token;
|
|
147
|
+
}
|
|
148
|
+
if (options.tokenHeader) {
|
|
149
|
+
secret.tokenHeader = options.tokenHeader;
|
|
150
|
+
}
|
|
151
|
+
if (options.certificate) {
|
|
152
|
+
// Read the certificate file and put it into a string.
|
|
153
|
+
if (!fs.existsSync(options.certificate)) {
|
|
154
|
+
Logger.error(`Certificate file not found: ${options.certificate}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
secret.certPem = fs.readFileSync(options.certificate, 'utf8');
|
|
158
|
+
}
|
|
159
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/secrets`, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: {
|
|
162
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`,
|
|
163
|
+
'Content-Type': 'application/json'
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify(secret)
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
Logger.error('Creating secret failed: ' + response.statusText);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
const data = await response.json();
|
|
172
|
+
Context.put('secret', data);
|
|
173
|
+
Logger.success(`Secret ${data.name} created successfully with ID: ${data.id}`);
|
|
174
|
+
});
|
|
175
|
+
/* Create an elicitation secret with name */
|
|
176
|
+
secretCommand.command('create-elicitation <name>')
|
|
177
|
+
.description('Create a new Elicitation secret')
|
|
178
|
+
.option('-d, --description <description>', 'Description for the Elicitationsecret')
|
|
179
|
+
.option('-t, --token <token>', 'Token for the Elicitation secret (if sensitive data access is needed)')
|
|
180
|
+
.option('--oc, --oauth2ClientID <oauth2ClientID>', 'The ClientID for the backend Authorization service (if OAuth2 is used)')
|
|
181
|
+
.option('--oae, --oauth2AuthorizationEndpoint <authorizationEndpoint>', 'Authorization Endpoint for the backend authentication (including query parameters without clientID and redirect_uri)')
|
|
182
|
+
.option('--ote, --oauth2TokenEndpoint <tokenEndpoint>', 'Token exchange Endpoint for backend authentication (if OAuth2 is used)')
|
|
183
|
+
.option('-o, --output <format>', 'Output format (json, yaml)')
|
|
184
|
+
.action(async (name, options) => {
|
|
185
|
+
// Initialize the secret object.
|
|
186
|
+
let secret = {
|
|
187
|
+
name: name,
|
|
188
|
+
description: options.description || '',
|
|
189
|
+
type: 'ENDPOINT',
|
|
190
|
+
useElicitation: true,
|
|
191
|
+
};
|
|
192
|
+
if (options.token) {
|
|
193
|
+
secret.tokenHeader = options.token;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// OAuth2 based elicitation.
|
|
197
|
+
if (!options.oauth2ClientID || !options.oauth2AuthorizationEndpoint || !options.oauth2TokenEndpoint) {
|
|
198
|
+
Logger.error('OAuth2 based elicitation requires oauth2ClientID, oauth2AuthorizationEndpoint and oauth2TokenEndpoint to be provided.');
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
secret.oauth2ClientConfiguration = {
|
|
202
|
+
clientId: options.oauth2ClientID,
|
|
203
|
+
authorizationEndpoint: options.oauth2AuthorizationEndpoint,
|
|
204
|
+
tokenEndpoint: options.oauth2TokenEndpoint
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/secrets`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: {
|
|
210
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`,
|
|
211
|
+
'Content-Type': 'application/json'
|
|
212
|
+
},
|
|
213
|
+
body: JSON.stringify(secret)
|
|
214
|
+
});
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
Logger.error('Creating secret failed: ' + response.statusText);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
const data = await response.json();
|
|
220
|
+
Context.put('secret', data);
|
|
221
|
+
Logger.success(`Elicitation secret ${data.name} created successfully with ID: ${data.id}`);
|
|
222
|
+
});
|
|
223
|
+
/** Update secret by ID */
|
|
224
|
+
secretCommand.command('update <id>')
|
|
225
|
+
.description('Update a secret by ID')
|
|
226
|
+
.action(async (id) => {
|
|
227
|
+
try {
|
|
228
|
+
// First, fetch the current secret
|
|
229
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/secrets/${id}`, {
|
|
230
|
+
method: 'GET',
|
|
231
|
+
headers: {
|
|
232
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
Logger.error('Fetching secret failed: ' + response.statusText);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
const secret = await response.json();
|
|
240
|
+
Logger.info(`Opening editor for secret: ${secret.name}`);
|
|
241
|
+
await openUpdateEditor(secret, async (modifiedSecret) => {
|
|
242
|
+
// Enforce properties that are immutable.
|
|
243
|
+
modifiedSecret.id = secret.id; // Ensure the ID remains the same.
|
|
244
|
+
modifiedSecret.organizationId = secret.organizationId; // Ensure the organization ID remains the same.
|
|
245
|
+
const updateResponse = await fetch(`${ConfigUtil.config.server}/api/v1/secrets/${id}`, {
|
|
246
|
+
method: 'PUT',
|
|
247
|
+
headers: {
|
|
248
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`,
|
|
249
|
+
'Content-Type': 'application/json'
|
|
250
|
+
},
|
|
251
|
+
body: JSON.stringify(modifiedSecret)
|
|
252
|
+
});
|
|
253
|
+
if (!updateResponse.ok) {
|
|
254
|
+
Logger.error('Updating secret failed: ' + updateResponse.statusText);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
Logger.success(`Secret updated successfully: ${id}`);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
Logger.error('Updating secret failed: ' + error.message);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
/** Delete secret by ID */
|
|
266
|
+
secretCommand.command('delete <id>')
|
|
267
|
+
.description('Delete a secret by ID')
|
|
268
|
+
.action(async (id) => {
|
|
269
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/secrets/${id}`, {
|
|
270
|
+
method: 'DELETE',
|
|
271
|
+
headers: {
|
|
272
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
Logger.error('Deleting secret failed: ' + response.statusText);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
Logger.success(`Secret deleted successfully: ${id}`);
|
|
280
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
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 inquirer from "inquirer";
|
|
17
|
+
import { program } from "commander";
|
|
18
|
+
import { Logger } from "../utils/logger.js";
|
|
19
|
+
import { ConfigUtil } from "../utils/config.js";
|
|
20
|
+
import { ageFrom } from "../utils/age.js";
|
|
21
|
+
import { Context } from "../utils/context.js";
|
|
22
|
+
import { CLI_LABEL } from '../constants.js';
|
|
23
|
+
export const serviceCommand = program.command('service')
|
|
24
|
+
.description(`Manage services in ${CLI_LABEL}`);
|
|
25
|
+
/* List all secrets */
|
|
26
|
+
serviceCommand.command('list')
|
|
27
|
+
.description('List all services')
|
|
28
|
+
.option('-o, --output <format>', 'Output format (json, yaml)')
|
|
29
|
+
.action(async () => {
|
|
30
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/services`, {
|
|
31
|
+
method: 'GET',
|
|
32
|
+
headers: {
|
|
33
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
Logger.error('Fetching services failed: ' + response.statusText);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const data = await response.json().catch(err => {
|
|
41
|
+
Logger.error('Failed to parse response: ' + err.message);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
|
44
|
+
if (data.length === 0) {
|
|
45
|
+
Logger.info('No services found.');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
Context.put('services', data);
|
|
49
|
+
const longestName = longestServiceName(data) + 1; // +1 for padding
|
|
50
|
+
const longestVersion = longestServiceVersion(data) + 1; // +1 for padding
|
|
51
|
+
const longestType = longestServiceType(data) + 1; // +1 for padding
|
|
52
|
+
Logger.log(`${'ID'.padEnd(13, ' ')} ${'NAME'.padEnd(longestName, ' ')} ${'VERSION'.padEnd(Math.max(longestVersion, 7), ' ')} ${'TYPE'.padEnd(longestType, ' ')} AGE`);
|
|
53
|
+
data.forEach((service) => {
|
|
54
|
+
Logger.log(`${service.id} ${service.name.padEnd(longestName, ' ')} ${service.version.padEnd(Math.max(longestVersion, 7), ' ')} ${service.type.padEnd(longestType, ' ')} ${ageFrom(service.createdOn)}`);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
function longestServiceName(services) {
|
|
59
|
+
return services.reduce((max, service) => {
|
|
60
|
+
return Math.max(max, service.name.length);
|
|
61
|
+
}, 0);
|
|
62
|
+
}
|
|
63
|
+
function longestServiceVersion(services) {
|
|
64
|
+
return services.reduce((max, service) => {
|
|
65
|
+
return Math.max(max, service.version.length);
|
|
66
|
+
}, 0);
|
|
67
|
+
}
|
|
68
|
+
function longestServiceType(services) {
|
|
69
|
+
return services.reduce((max, service) => {
|
|
70
|
+
return Math.max(max, service.type.length);
|
|
71
|
+
}, 0);
|
|
72
|
+
}
|
|
73
|
+
/** Get service by ID */
|
|
74
|
+
serviceCommand.command('get <id>')
|
|
75
|
+
.description('Get service by ID')
|
|
76
|
+
.option('-o, --output <format>', 'Output format (json, yaml)')
|
|
77
|
+
.action(async (id) => {
|
|
78
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/services/${id}`, {
|
|
79
|
+
method: 'GET',
|
|
80
|
+
headers: {
|
|
81
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
Logger.error('Fetching service failed: ' + response.statusText);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const data = await response.json().catch(err => {
|
|
89
|
+
Logger.error('Error parsing service response: ' + err.message);
|
|
90
|
+
});
|
|
91
|
+
if (data != null) {
|
|
92
|
+
Context.put('service', data);
|
|
93
|
+
Logger.info('Service details');
|
|
94
|
+
Logger.log(`ID : ${data.id}`);
|
|
95
|
+
Logger.log(`Name : ${data.name}`);
|
|
96
|
+
Logger.log(`Version : ${data.version}`);
|
|
97
|
+
Logger.log(`Organization: ${data.organizationId}`);
|
|
98
|
+
Logger.log(`Type : ${data.type}`);
|
|
99
|
+
Logger.log(`Created : ${data.createdOn}`);
|
|
100
|
+
Logger.bold('Operations :');
|
|
101
|
+
if (data.operations && data.operations.length > 0) {
|
|
102
|
+
data.operations.forEach((op) => {
|
|
103
|
+
Logger.log(` - Name: ${op.name}`);
|
|
104
|
+
if (op.inputName) {
|
|
105
|
+
Logger.log(` Input: ${op.inputName}`);
|
|
106
|
+
}
|
|
107
|
+
if (op.outputName) {
|
|
108
|
+
Logger.log(` Output ${op.outputName}`);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
Logger.log(' No operations defined for this service.');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
/** Delete service by ID */
|
|
118
|
+
serviceCommand.command('delete <id>')
|
|
119
|
+
.description('Delete service by ID')
|
|
120
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
121
|
+
.action(async (id, options) => {
|
|
122
|
+
if (!options.force) {
|
|
123
|
+
const confirm = await inquirer.prompt({
|
|
124
|
+
type: 'confirm',
|
|
125
|
+
name: 'confirm',
|
|
126
|
+
message: 'Deleting this service will also remove associated artifacts, config plans & expositions. Are you sure you want to proceed?',
|
|
127
|
+
default: false
|
|
128
|
+
});
|
|
129
|
+
if (!confirm.confirm) {
|
|
130
|
+
Logger.info('Deletion cancelled.');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/services/${id}`, {
|
|
135
|
+
method: 'DELETE',
|
|
136
|
+
headers: {
|
|
137
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
Logger.error('Deleting service failed: ' + response.statusText);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
Logger.success(`Service ${id} deleted successfully.`);
|
|
145
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
export const CLI_NAME = "reshapr";
|
|
17
|
+
export const CLI_LABEL = "Reshapr";
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
export function ageFrom(value) {
|
|
17
|
+
const d = new Date(value);
|
|
18
|
+
const now = new Date();
|
|
19
|
+
const seconds = Math.round(Math.abs((now.getTime() - d.getTime()) / 1000));
|
|
20
|
+
const minutes = Math.round(Math.abs(seconds / 60));
|
|
21
|
+
const hours = Math.round(Math.abs(minutes / 60));
|
|
22
|
+
const days = Math.round(Math.abs(hours / 24));
|
|
23
|
+
const months = Math.round(Math.abs(days / 30.416));
|
|
24
|
+
const years = Math.round(Math.abs(days / 365));
|
|
25
|
+
if (Number.isNaN(seconds)) {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
else if (seconds <= 90) {
|
|
29
|
+
return `${seconds}s`;
|
|
30
|
+
}
|
|
31
|
+
else if (minutes <= 90) {
|
|
32
|
+
return `${minutes}m`;
|
|
33
|
+
}
|
|
34
|
+
else if (hours <= 23) {
|
|
35
|
+
return `${hours}h`;
|
|
36
|
+
}
|
|
37
|
+
else if (days <= 29) {
|
|
38
|
+
return `${days}d`;
|
|
39
|
+
}
|
|
40
|
+
else if (days <= 45) {
|
|
41
|
+
return `${months}m`;
|
|
42
|
+
}
|
|
43
|
+
else if (days <= 545) {
|
|
44
|
+
return '1y';
|
|
45
|
+
}
|
|
46
|
+
else { // (days > 545)
|
|
47
|
+
return `${years}y`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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 * as os from 'node:os';
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import { Logger } from './logger.js';
|
|
19
|
+
import { CLI_NAME } from '../constants.js';
|
|
20
|
+
export class ConfigUtil {
|
|
21
|
+
static configPath = `${os.homedir()}/.${CLI_NAME}/config`;
|
|
22
|
+
static config;
|
|
23
|
+
static writeConfig(config) {
|
|
24
|
+
if (!fs.existsSync(ConfigUtil.configPath)) {
|
|
25
|
+
try {
|
|
26
|
+
fs.mkdirSync(`${os.homedir()}/.${CLI_NAME}`, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
Logger.error('Failed to create config directory: ' + err);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Extract expiration time and org claim from the token if it exists.
|
|
34
|
+
if (config.token) {
|
|
35
|
+
try {
|
|
36
|
+
const tokenParts = config.token.split('.');
|
|
37
|
+
if (tokenParts.length === 3) {
|
|
38
|
+
const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString('utf-8'));
|
|
39
|
+
if (payload.exp) {
|
|
40
|
+
config.exp = payload.exp.toString();
|
|
41
|
+
}
|
|
42
|
+
if (payload.org) {
|
|
43
|
+
config.org = payload.org;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
Logger.error('Failed to parse token: ' + err);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
fs.writeFileSync(ConfigUtil.configPath, JSON.stringify(config, null, 2));
|
|
52
|
+
Logger.success(`Configuration saved to ${ConfigUtil.configPath}`);
|
|
53
|
+
ConfigUtil.config = config;
|
|
54
|
+
}
|
|
55
|
+
static readConfig() {
|
|
56
|
+
if (fs.existsSync(ConfigUtil.configPath)) {
|
|
57
|
+
try {
|
|
58
|
+
let configData = fs.readFileSync(ConfigUtil.configPath, 'utf-8');
|
|
59
|
+
ConfigUtil.config = JSON.parse(configData);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
Logger.error('Failed to read config file: ' + err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
static deleteConfig() {
|
|
67
|
+
if (fs.existsSync(ConfigUtil.configPath)) {
|
|
68
|
+
try {
|
|
69
|
+
fs.unlinkSync(ConfigUtil.configPath);
|
|
70
|
+
Logger.success('Configuration deleted successfully.');
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
Logger.error('Failed to delete config file: ' + err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
Logger.warn('No configuration file found to delete.');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
export class Context {
|
|
17
|
+
static currentContext = {};
|
|
18
|
+
static put(key, value) {
|
|
19
|
+
Context.currentContext[key] = value;
|
|
20
|
+
}
|
|
21
|
+
static isEmpty() {
|
|
22
|
+
return Context.size() === 0;
|
|
23
|
+
}
|
|
24
|
+
static size() {
|
|
25
|
+
return Object.keys(Context.currentContext).length;
|
|
26
|
+
}
|
|
27
|
+
static get(key) {
|
|
28
|
+
return Context.currentContext[key];
|
|
29
|
+
}
|
|
30
|
+
static getAll() {
|
|
31
|
+
return Context.currentContext;
|
|
32
|
+
}
|
|
33
|
+
static clear() {
|
|
34
|
+
Context.currentContext = {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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 * as os from 'os';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { spawn } from 'child_process';
|
|
19
|
+
import { promises as fs } from 'fs';
|
|
20
|
+
import { Logger } from "../utils/logger.js";
|
|
21
|
+
import { CLI_NAME } from '../constants.js';
|
|
22
|
+
/** Open a text editor to change initialContent. Execute onChanged() if updated. */
|
|
23
|
+
export async function openUpdateEditor(initialContent, onChanged) {
|
|
24
|
+
// Create a temporary file with the current configuration
|
|
25
|
+
const tempDir = os.tmpdir();
|
|
26
|
+
const tempFile = path.join(tempDir, `${CLI_NAME}-update.json`);
|
|
27
|
+
const initialData = JSON.stringify(initialContent, null, 2);
|
|
28
|
+
await fs.writeFile(tempFile, initialData, 'utf8');
|
|
29
|
+
// Get the initial modification time
|
|
30
|
+
const statsBefore = await fs.stat(tempFile);
|
|
31
|
+
const mtimeBefore = statsBefore.mtime.getTime();
|
|
32
|
+
// Open vi editor with the temporary file
|
|
33
|
+
const editorProcess = spawn('vi', [tempFile], {
|
|
34
|
+
stdio: 'inherit', // This allows vi to take control of the terminal
|
|
35
|
+
shell: false
|
|
36
|
+
});
|
|
37
|
+
// Wait for the editor to close
|
|
38
|
+
await new Promise((resolve, reject) => {
|
|
39
|
+
editorProcess.on('close', (code) => {
|
|
40
|
+
if (code === 0) {
|
|
41
|
+
resolve();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
reject(new Error(`Editor exited with code ${code}`));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
editorProcess.on('error', (error) => {
|
|
48
|
+
reject(error);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
// Check if the file was modified.
|
|
52
|
+
const statsAfter = await fs.stat(tempFile);
|
|
53
|
+
const mtimeAfter = statsAfter.mtime.getTime();
|
|
54
|
+
if (mtimeAfter !== mtimeBefore) {
|
|
55
|
+
let modifiedContent;
|
|
56
|
+
try {
|
|
57
|
+
modifiedContent = JSON.parse(await fs.readFile(tempFile, 'utf8'));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
Logger.error('Error reading modified content: ' + error.message);
|
|
61
|
+
}
|
|
62
|
+
// Notify the caller of the changes.
|
|
63
|
+
await onChanged(modifiedContent);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
Logger.info('No changes detected. Object not modified.');
|
|
67
|
+
}
|
|
68
|
+
// Clean up the temporary file
|
|
69
|
+
await fs.unlink(tempFile);
|
|
70
|
+
}
|