@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 ADDED
@@ -0,0 +1,44 @@
1
+ # reshapr-cli
2
+
3
+ This is the command line interface for the Reshapr project.
4
+
5
+ ## Installing the CLI
6
+
7
+ ```shell
8
+ npm install -g @reshapr/reshapr-cli
9
+ ```
10
+
11
+ ## Running the CLI in dev mode
12
+
13
+ After cloning the repository, you can run the CLI in development mode using the following commands.
14
+
15
+ 1. First, ensure you have the required dependencies installed. You can do this by running:
16
+
17
+ ```shell
18
+ npm install
19
+ ```
20
+
21
+ 2. Next, you have to start the CLI in development mode using:
22
+
23
+ ```shell
24
+ npm run dev
25
+ ```
26
+
27
+ This will keep the CLI running and watch for changes in the source code, allowing you to develop
28
+ and test your changes in real-time.
29
+
30
+ 3. Finally, you have to link the `reshapr` binary to your JavaScript entrypoint for the CLI:
31
+
32
+ ```shell
33
+ npm link
34
+ ```
35
+
36
+ ### Executing some commands
37
+
38
+ ```shell
39
+ # Login to the reShapr local control-plane server
40
+ reshapr login --server http://localhost:5555
41
+
42
+ # Logout once your job is done.
43
+ reshapr logout
44
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright The Reshapr Authors.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { program } from 'commander';
18
+ import * as yaml from 'js-yaml';
19
+ import { loginCommand, infoCommand, logoutCommand, importCommand, attachCommand, quotasCommand } from './commands/index.js';
20
+ import { ConfigUtil } from './utils/config.js';
21
+ import { Logger } from './utils/logger.js';
22
+ import { Context } from './utils/context.js';
23
+ import { CLI_VERSION } from './version.js';
24
+ import { CLI_NAME, CLI_LABEL } from './constants.js';
25
+ program
26
+ .name(CLI_NAME)
27
+ .description(CLI_LABEL + ' CLI - A command line interface for ' + CLI_LABEL)
28
+ .version(CLI_VERSION)
29
+ .hook('preAction', (thisCommand, actionCommand) => {
30
+ ConfigUtil.readConfig();
31
+ if (actionCommand.name() != 'login' && actionCommand.name() != 'logout') {
32
+ if (!ConfigUtil.config.token) {
33
+ Logger.warn(`You are not logged in. Please login first using the \`${CLI_NAME} login\` command.`);
34
+ process.exit(1);
35
+ }
36
+ if (ConfigUtil.config.exp && ConfigUtil.config.exp < Date.now() / 1000) {
37
+ Logger.warn(`Your token has expired. Please login again using the \`${CLI_NAME} login\` command.`);
38
+ process.exit(1);
39
+ }
40
+ }
41
+ if (actionCommand.opts().output) {
42
+ Logger.mute();
43
+ }
44
+ })
45
+ .hook('postAction', (thisCommand, actionCommand) => {
46
+ if (actionCommand.opts().output) {
47
+ Logger.unmute();
48
+ // Check requested format.
49
+ const format = actionCommand.opts().output.toLowerCase();
50
+ if (format !== 'json' && format !== 'yaml') {
51
+ Logger.error('Invalid output format. Supported formats are json and yaml.');
52
+ process.exit(1);
53
+ }
54
+ if (!Context.isEmpty()) {
55
+ if (Context.size() === 1) {
56
+ const firstKey = Object.keys(Context.getAll())[0];
57
+ if (format === 'json') {
58
+ Logger.log(convertToJson(Context.get(firstKey)));
59
+ }
60
+ else if (format === 'yaml') {
61
+ Logger.log(convertToYaml(Context.get(firstKey)));
62
+ }
63
+ }
64
+ else {
65
+ if (format === 'json') {
66
+ Logger.log(convertToJson(Context.getAll()));
67
+ }
68
+ else if (format === 'yaml') {
69
+ Logger.log(convertToYaml(Context.getAll()));
70
+ }
71
+ }
72
+ }
73
+ }
74
+ });
75
+ function convertToJson(data) {
76
+ return JSON.stringify(data, null, 2);
77
+ }
78
+ function convertToYaml(data) {
79
+ return yaml.dump(data);
80
+ }
81
+ process.on('uncaughtException', (error) => {
82
+ console.error('Uncaught Exception:', error);
83
+ process.exit(1);
84
+ });
85
+ process.on('unhandledRejection', (reason, promise) => {
86
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
87
+ process.exit(1);
88
+ });
89
+ // Load commands.
90
+ program.addCommand(loginCommand);
91
+ program.addCommand(infoCommand);
92
+ program.addCommand(logoutCommand);
93
+ program.addCommand(importCommand);
94
+ program.addCommand(attachCommand);
95
+ program.addCommand(quotasCommand);
96
+ program.parse(process.argv);
@@ -0,0 +1,123 @@
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 { Option, program } from "commander";
17
+ import inquirer from "inquirer";
18
+ import { Logger } from "../utils/logger.js";
19
+ import { ConfigUtil } from "../utils/config.js";
20
+ import { Context } from "../utils/context.js";
21
+ import { CLI_LABEL } from '../constants.js';
22
+ export const tokenCommand = program.command('api-token')
23
+ .description(`Manage API tokens in ${CLI_LABEL}`);
24
+ /* List all api tokens */
25
+ tokenCommand.command('list')
26
+ .description('List all API tokens')
27
+ .action(async () => {
28
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/tokens/apiTokens`, {
29
+ method: 'GET',
30
+ headers: {
31
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
32
+ }
33
+ });
34
+ if (!response.ok) {
35
+ Logger.error(`Failed to fetch API tokens: ${response.statusText}`);
36
+ process.exit(1);
37
+ }
38
+ const data = await response.json().catch(err => {
39
+ Logger.error('Error parsing secrets response: ' + err.message);
40
+ });
41
+ if (data != null) {
42
+ if (data.length === 0) {
43
+ Logger.info('No API tokens found.');
44
+ }
45
+ else {
46
+ Context.put('tokens', data);
47
+ const longestName = longestTokenName(data) + 1; // +1 for padding
48
+ Logger.log(`${'ID'.padEnd(13, ' ')} ${'NAME'.padEnd(longestName, ' ')} VALID UNTIL`);
49
+ data.forEach((token) => {
50
+ Logger.log(`${token.id.padEnd(13, ' ')} ${token.name.padEnd(longestName, ' ')} ${new Date(token.validUntil).toUTCString()}`);
51
+ });
52
+ }
53
+ }
54
+ });
55
+ function longestTokenName(tokens) {
56
+ return tokens.reduce((max, token) => {
57
+ return Math.max(max, token.name.length);
58
+ }, 0);
59
+ }
60
+ /* Create a new api token */
61
+ tokenCommand.command('create <name>')
62
+ .description('Create a new API token')
63
+ .addOption(new Option('-v, --validity-days <days>', 'Number of days the token is valid for').choices(['1', '7', '30', '90']))
64
+ .action(async (name, options) => {
65
+ // Initialize the token request object.
66
+ let tokenRequest = {
67
+ name: name,
68
+ validityDays: 30
69
+ };
70
+ // Populate validityDays if provided.
71
+ if (options.validityDays) {
72
+ tokenRequest.validityDays = parseInt(options.validityDays, 10);
73
+ }
74
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/tokens/apiTokens`, {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json',
78
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
79
+ },
80
+ body: JSON.stringify(tokenRequest)
81
+ });
82
+ if (!response.ok) {
83
+ Logger.error(`Creating API token failed: ${response.statusText}`);
84
+ process.exit(1);
85
+ }
86
+ const token = await response.json().catch(err => {
87
+ Logger.error('Error parsing create token response: ' + err.message);
88
+ });
89
+ Context.put('token', token);
90
+ if (token != null) {
91
+ Logger.warn(`The API Token to register Gateway is: ${token.organizationId}-${token.token}`);
92
+ Logger.warn('Make sure to store it securely, as it will not be shown again.');
93
+ }
94
+ });
95
+ /* Delete an api token */
96
+ tokenCommand.command('delete <tokenId>')
97
+ .description('Delete an API token by ID')
98
+ .option('-f, --force', 'Skip confirmation prompt')
99
+ .action(async (tokenId, options) => {
100
+ if (!options.force) {
101
+ const confirm = await inquirer.prompt({
102
+ type: 'confirm',
103
+ name: 'confirm',
104
+ message: `Deleting this API token will prevent Gateways that use it to connect to ${CLI_LABEL}. Are you sure you want to proceed?`,
105
+ default: false
106
+ });
107
+ if (!confirm.confirm) {
108
+ Logger.info('Deletion cancelled.');
109
+ return;
110
+ }
111
+ }
112
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/tokens/apiTokens/${tokenId}`, {
113
+ method: 'DELETE',
114
+ headers: {
115
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`
116
+ }
117
+ });
118
+ if (!response.ok) {
119
+ Logger.error(`Failed to delete API token: ${response.statusText}`);
120
+ return;
121
+ }
122
+ Logger.info(`API token with ID ${tokenId} deleted successfully.`);
123
+ });
@@ -0,0 +1,72 @@
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 yoctoSpinner from 'yocto-spinner';
18
+ import { Command } from "commander";
19
+ import { Logger } from "../utils/logger.js";
20
+ import { ConfigUtil } from "../utils/config.js";
21
+ import { Context } from "../utils/context.js";
22
+ import { CLI_LABEL } from '../constants.js';
23
+ export const attachCommand = new Command('attach')
24
+ .description(`Attach an artifact to a ${CLI_LABEL} Service`)
25
+ .option('-f, --file <file>', 'Path to the artifact file to atttach')
26
+ .option('-u, --url <url>', 'URL of the artifact to atttach')
27
+ .option('-s, --secret <artifactSecret>', 'Use a secret to authenticate the artifact to atttach')
28
+ .option('-o, --output <format>', 'Output format (json, yaml)')
29
+ .action(async (options) => {
30
+ if (!options.file && !options.url) {
31
+ Logger.error('You must provide either a file path or a URL to import.');
32
+ process.exit(1);
33
+ }
34
+ let body;
35
+ if (options.file) {
36
+ if (!fs.existsSync(options.file)) {
37
+ Logger.error(`File not found: ${options.file}`);
38
+ process.exit(1);
39
+ }
40
+ // We should encode in multipart/form-data
41
+ body = new FormData();
42
+ body.append('file', new Blob([fs.readFileSync(options.file)]), options.file.split('/').pop());
43
+ }
44
+ else if (options.url) {
45
+ // We should encode in application/x-www-form-urlencoded
46
+ body = new URLSearchParams();
47
+ body.append('url', options.url);
48
+ if (options.secret) {
49
+ body.append('secretName', options.secret);
50
+ }
51
+ }
52
+ const spinner = yoctoSpinner({ text: 'Attaching artifact...' }).start();
53
+ const response = await fetch(`${ConfigUtil.config.server}/api/v1/artifacts/attach`, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Authorization': `Bearer ${ConfigUtil.config.token}`,
57
+ },
58
+ body: body
59
+ });
60
+ if (!response.ok) {
61
+ Logger.error('Attach failed: ' + response.statusText);
62
+ process.exit(1);
63
+ }
64
+ const data = await response.json().catch(err => {
65
+ Logger.error('Failed to parse response: ' + err.message);
66
+ process.exit(1);
67
+ });
68
+ spinner.stop();
69
+ Context.put('artifact', data);
70
+ Logger.success('Attachment successful!');
71
+ Logger.info(`Discovered Artifact ${data.name} with ID: ${data.id}`);
72
+ });