@reshapr/reshapr-cli 0.0.1 → 0.0.4

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/dist/cli.js CHANGED
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { program } from 'commander';
18
18
  import * as yaml from 'js-yaml';
19
- import { loginCommand, infoCommand, logoutCommand, importCommand, attachCommand, quotasCommand } from './commands/index.js';
19
+ import { loginCommand, infoCommand, logoutCommand, importCommand, attachCommand, quotasCommand, runCommand, statusCommand, stopCommand } from './commands/index.js';
20
20
  import { ConfigUtil } from './utils/config.js';
21
21
  import { Logger } from './utils/logger.js';
22
22
  import { Context } from './utils/context.js';
@@ -28,7 +28,8 @@ program
28
28
  .version(CLI_VERSION)
29
29
  .hook('preAction', (thisCommand, actionCommand) => {
30
30
  ConfigUtil.readConfig();
31
- if (actionCommand.name() != 'login' && actionCommand.name() != 'logout') {
31
+ const noAuthCommands = ['login', 'logout', 'run', 'status', 'stop'];
32
+ if (!noAuthCommands.includes(actionCommand.name())) {
32
33
  if (!ConfigUtil.config.token) {
33
34
  Logger.warn(`You are not logged in. Please login first using the \`${CLI_NAME} login\` command.`);
34
35
  process.exit(1);
@@ -93,4 +94,7 @@ program.addCommand(logoutCommand);
93
94
  program.addCommand(importCommand);
94
95
  program.addCommand(attachCommand);
95
96
  program.addCommand(quotasCommand);
97
+ program.addCommand(runCommand);
98
+ program.addCommand(statusCommand);
99
+ program.addCommand(stopCommand);
96
100
  program.parse(process.argv);
@@ -25,3 +25,6 @@ export { configCommand } from './config.js';
25
25
  export { gatewayGroupCommand } from './gateway-group.js';
26
26
  export { tokenCommand } from './api-token.js';
27
27
  export { quotasCommand } from './quotas.js';
28
+ export { runCommand } from './run.js';
29
+ export { statusCommand } from './status.js';
30
+ export { stopCommand } from './stop.js';
@@ -115,50 +115,96 @@ async function handleOnPremisesLogin(options) {
115
115
  async function handleSaaSLogin(options) {
116
116
  // Prepare a token for reception.
117
117
  let token = null;
118
- // Start starting a lightweight web server to handle OAuth2 login.
118
+ // Start a lightweight web server to receive the OAuth2 callback.
119
119
  const server = http.createServer((req, res) => {
120
- // Handle Authentication callback here. Parse the URL.
120
+ // Parse the URL and extract query parameters.
121
121
  const parsedUrl = url.parse(req.url || '', true);
122
- // Get query parts of the URL.
123
122
  const query = parsedUrl.query;
124
123
  if (query.token && query.token.length > 0) {
125
124
  token = query.token;
125
+ // The SaaS also sends the control plane URL for subsequent CLI calls.
126
+ const ctrlUrl = query.ctrl_url || options.server;
126
127
  Logger.success('Login successful!');
127
- const tokenPayload = token.split('.')[1];
128
- const decodedPayload = Buffer.from(tokenPayload, 'base64').toString('utf8');
129
- const username = JSON.parse(decodedPayload).sub || 'unknown';
130
- Logger.info(`Welcome, ${username}!`);
131
- // Here you would typically save the authentication token or session.
132
- let config = {
133
- username: username,
134
- server: options.server,
135
- insecure: options.insecure,
136
- token: token
137
- };
138
- ConfigUtil.writeConfig(config);
139
- res.writeHead(200, { 'Content-Type': 'text/plain' });
140
- res.end('Login successful! You can close this window now.');
128
+ // Decode the JWT payload to extract username and org.
129
+ try {
130
+ const tokenPayload = token.split('.')[1];
131
+ const decodedPayload = Buffer.from(tokenPayload, 'base64').toString('utf8');
132
+ const payload = JSON.parse(decodedPayload);
133
+ const username = payload.sub || 'unknown';
134
+ const org = payload.org || '';
135
+ Logger.info(`Welcome, ${username}!`);
136
+ if (org) {
137
+ Logger.info(`Organization: ${org}`);
138
+ }
139
+ // Save the configuration — server points to the control plane, not the SaaS.
140
+ const config = {
141
+ username: username,
142
+ server: ctrlUrl,
143
+ insecure: options.insecure,
144
+ token: token
145
+ };
146
+ ConfigUtil.writeConfig(config);
147
+ }
148
+ catch (err) {
149
+ Logger.warn('Could not decode token payload: ' + err);
150
+ // Still save the config with what we have.
151
+ const config = {
152
+ username: 'unknown',
153
+ server: ctrlUrl,
154
+ insecure: options.insecure,
155
+ token: token
156
+ };
157
+ ConfigUtil.writeConfig(config);
158
+ }
159
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
160
+ res.end(`
161
+ <html>
162
+ <head><meta charset="utf-8"></head>
163
+ <body style="font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #f8fafc;">
164
+ <div style="text-align: center;">
165
+ <h1 style="color: #16a34a;">&#10003; Login successful!</h1>
166
+ <p style="color: #64748b;">You can close this window and return to your terminal.</p>
167
+ </div>
168
+ </body>
169
+ </html>
170
+ `);
171
+ // Close the server and exit after a short delay.
172
+ setTimeout(() => {
173
+ server.close();
174
+ process.exit(0);
175
+ }, 500);
141
176
  }
142
177
  else {
143
- Logger.error('Login failed: No token received.');
144
- res.writeHead(400, { 'Content-Type': 'text/plain' });
145
- res.end(`Login failed: No token received. Please login on ${options.server} before trying again.`);
178
+ // We may receive other requests to this server that don't have a token, so just respond with an error page.
179
+ if (!token) {
180
+ Logger.error('Login failed: No token received.');
181
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
182
+ res.end(`
183
+ <html>
184
+ <head><meta charset="utf-8"></head>
185
+ <body style="font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #f8fafc;">
186
+ <div style="text-align: center;">
187
+ <h1 style="color: #dc2626;">&#10007; Login failed</h1>
188
+ <p style="color: #64748b;">No token received. Please try again.</p>
189
+ </div>
190
+ </body>
191
+ </html>
192
+ `);
193
+ }
146
194
  }
147
- process.exit(0);
148
195
  });
149
196
  server.on('error', (err) => {
150
- Logger.error('Failed to start server for OAuth2 login: ' + err.message);
197
+ Logger.error('Failed to start local server for authentication: ' + err.message);
151
198
  process.exit(1);
152
199
  });
200
+ // Find an available port.
153
201
  const localPort = await getPort({ port: portNumbers(5556, 5599) });
154
202
  server.listen(localPort, () => {
155
- Logger.info(`Listening for OAuth2 callback on http://localhost:${localPort}`);
203
+ Logger.info(`Listening for authentication callback on http://localhost:${localPort}`);
156
204
  });
205
+ // Open the browser to the SaaS CLI login page.
206
+ const loginUrl = `${options.server}/cli/login?redirect_uri=http://localhost:${localPort}`;
207
+ Logger.info(`Opening browser: ${loginUrl}`);
157
208
  // Opens the URL in the default browser.
158
- await open(`${options.server}/auth/login/saas?redirect_uri=http://localhost:${localPort}`, {
159
- wait: true
160
- });
161
- server.close(() => {
162
- Logger.info('Server closed after handling OAuth2 callback.');
163
- });
209
+ await open(loginUrl, { wait: false });
164
210
  }
@@ -0,0 +1,114 @@
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 * as path from 'node:path';
19
+ import { Command } from "commander";
20
+ import { Logger } from "../utils/logger.js";
21
+ import { runDockerCompose } from '../utils/containers.js';
22
+ import { CLI_NAME, CLI_LABEL } from '../constants.js';
23
+ const GITHUB_REPO = 'reshaprio/reshapr';
24
+ const GITHUB_RAW_BASE = `https://raw.githubusercontent.com/${GITHUB_REPO}`;
25
+ const GITHUB_API_BASE = `https://api.github.com/repos/${GITHUB_REPO}`;
26
+ const COMPOSE_FILE_NAME = 'docker-compose-all-in-one.yml';
27
+ const COMPOSE_REMOTE_PATH = `install/${COMPOSE_FILE_NAME}`;
28
+ const RESHAPR_DIR = path.join(os.homedir(), `.${CLI_NAME}`);
29
+ const RUN_STATE_FILE = path.join(RESHAPR_DIR, 'run-state.json');
30
+ export const runCommand = new Command('run')
31
+ .description(`Start ${CLI_LABEL} locally using Docker Compose`)
32
+ .option('-r, --release <release>', 'Release of the containers to run', 'latest')
33
+ .action(async (options) => {
34
+ let release = options.release;
35
+ // Resolve 'latest' to the actual latest GitHub release tag.
36
+ if (release === 'latest') {
37
+ release = await resolveLatestRelease();
38
+ Logger.info(`Resolved 'latest' to release '${release}'.`);
39
+ }
40
+ const composeFile = getComposeFilePath(release);
41
+ // Download compose file if not already cached for this release.
42
+ if (!fs.existsSync(composeFile)) {
43
+ await downloadComposeFile(release, composeFile);
44
+ }
45
+ else {
46
+ Logger.info(`Using cached compose file for release '${release}'.`);
47
+ }
48
+ Logger.info(`Starting ${CLI_LABEL} containers (release: ${release})...`);
49
+ const exitCode = await runDockerCompose(['up', '-d'], composeFile);
50
+ if (exitCode !== 0) {
51
+ Logger.error(`docker compose exited with code ${exitCode}.`);
52
+ process.exit(exitCode);
53
+ }
54
+ saveRunState(release, composeFile);
55
+ Logger.success(`${CLI_LABEL} containers started successfully.`);
56
+ });
57
+ function getComposeFilePath(release) {
58
+ return path.join(RESHAPR_DIR, `docker-compose-${release}.yml`);
59
+ }
60
+ async function resolveLatestRelease() {
61
+ const url = `${GITHUB_API_BASE}/releases/latest`;
62
+ const response = await fetch(url, {
63
+ headers: { 'Accept': 'application/vnd.github+json' },
64
+ });
65
+ if (!response.ok) {
66
+ Logger.error(`Failed to fetch latest release from GitHub: ${response.status} ${response.statusText}`);
67
+ process.exit(1);
68
+ }
69
+ const data = await response.json();
70
+ return data.tag_name;
71
+ }
72
+ function getGitHubRef(release) {
73
+ if (release === 'nightly') {
74
+ return 'refs/heads/main';
75
+ }
76
+ return `refs/tags/${release}`;
77
+ }
78
+ async function downloadComposeFile(release, destPath) {
79
+ const ref = getGitHubRef(release);
80
+ const url = `${GITHUB_RAW_BASE}/${ref}/${COMPOSE_REMOTE_PATH}`;
81
+ Logger.info(`Downloading compose file from ${url}...`);
82
+ const response = await fetch(url);
83
+ if (!response.ok) {
84
+ Logger.error(`Failed to download compose file: ${response.status} ${response.statusText}`);
85
+ Logger.error(`Make sure the release '${release}' exists on the GitHub repository.`);
86
+ process.exit(1);
87
+ }
88
+ let content = await response.text();
89
+ // Replace image tags with the requested release.
90
+ content = content.replace(/(quay\.io\/reshapr\/[^:]+):[\w.-]+/g, `$1:${release}`);
91
+ fs.mkdirSync(RESHAPR_DIR, { recursive: true });
92
+ fs.writeFileSync(destPath, content, 'utf-8');
93
+ Logger.success(`Compose file saved to ${destPath}`);
94
+ }
95
+ function saveRunState(release, composeFile) {
96
+ const state = { release, composeFile, startedAt: new Date().toISOString() };
97
+ fs.writeFileSync(RUN_STATE_FILE, JSON.stringify(state, null, 2), 'utf-8');
98
+ }
99
+ export function readRunState() {
100
+ if (!fs.existsSync(RUN_STATE_FILE)) {
101
+ return null;
102
+ }
103
+ try {
104
+ return JSON.parse(fs.readFileSync(RUN_STATE_FILE, 'utf-8'));
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ export function removeRunState() {
111
+ if (fs.existsSync(RUN_STATE_FILE)) {
112
+ fs.unlinkSync(RUN_STATE_FILE);
113
+ }
114
+ }
@@ -0,0 +1,35 @@
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 { Command } from "commander";
17
+ import { Logger } from "../utils/logger.js";
18
+ import { runDockerCompose } from '../utils/containers.js';
19
+ import { CLI_LABEL } from '../constants.js';
20
+ import { readRunState } from './run.js';
21
+ export const statusCommand = new Command('status')
22
+ .description(`Show the status of locally running ${CLI_LABEL}`)
23
+ .action(async () => {
24
+ const state = readRunState();
25
+ if (!state) {
26
+ Logger.warn(`No ${CLI_LABEL} containers have been started. Use \`reshapr run\` to start them.`);
27
+ process.exit(0);
28
+ }
29
+ Logger.info(`${CLI_LABEL} containers (release: ${state.release}, started at: ${state.startedAt})`);
30
+ const exitCode = await runDockerCompose(['ps'], state.composeFile);
31
+ if (exitCode !== 0) {
32
+ Logger.error(`docker compose exited with code ${exitCode}.`);
33
+ process.exit(exitCode);
34
+ }
35
+ });
@@ -0,0 +1,37 @@
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 { Command } from "commander";
17
+ import { Logger } from "../utils/logger.js";
18
+ import { runDockerCompose } from '../utils/containers.js';
19
+ import { CLI_LABEL } from '../constants.js';
20
+ import { readRunState, removeRunState } from './run.js';
21
+ export const stopCommand = new Command('stop')
22
+ .description(`Stop locally running ${CLI_LABEL} containers`)
23
+ .action(async () => {
24
+ const state = readRunState();
25
+ if (!state) {
26
+ Logger.warn(`No ${CLI_LABEL} containers have been started. Nothing to stop.`);
27
+ process.exit(0);
28
+ }
29
+ Logger.info(`Stopping ${CLI_LABEL} containers (release: ${state.release})...`);
30
+ const exitCode = await runDockerCompose(['down'], state.composeFile);
31
+ if (exitCode !== 0) {
32
+ Logger.error(`docker compose exited with code ${exitCode}.`);
33
+ process.exit(exitCode);
34
+ }
35
+ removeRunState();
36
+ Logger.success(`${CLI_LABEL} containers stopped successfully.`);
37
+ });
@@ -0,0 +1,30 @@
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 { spawn } from 'node:child_process';
17
+ import { Logger } from "./logger.js";
18
+ export function runDockerCompose(args, composeFile) {
19
+ return new Promise((resolve, reject) => {
20
+ const proc = spawn('docker', ['compose', '-f', composeFile, ...args], {
21
+ stdio: 'inherit',
22
+ shell: false,
23
+ });
24
+ proc.on('close', (code) => resolve(code ?? 1));
25
+ proc.on('error', (err) => {
26
+ Logger.error(`Failed to execute docker compose: ${err.message}`);
27
+ reject(err);
28
+ });
29
+ });
30
+ }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.0.1";
1
+ export const CLI_VERSION = "0.0.4";
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@reshapr/reshapr-cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.4",
4
4
  "description": "CLI for reshapr.io - The MCP Gateway for AI-Native API Access!",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
7
7
  "bin": {
8
8
  "reshapr": "dist/cli.js"
9
9
  },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/reshaprio/reshapr.git"
13
+ },
10
14
  "license": "Apache-2.0",
11
15
  "publishConfig": {
12
16
  "access": "public"
package/src/cli.ts CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { program } from 'commander';
19
19
  import * as yaml from 'js-yaml';
20
- import { loginCommand, infoCommand, logoutCommand, importCommand, attachCommand, quotasCommand } from './commands/index.js';
20
+ import { loginCommand, infoCommand, logoutCommand, importCommand, attachCommand, quotasCommand, runCommand, statusCommand, stopCommand } from './commands/index.js';
21
21
  import { ConfigUtil } from './utils/config.js';
22
22
  import { Logger } from './utils/logger.js';
23
23
  import { Context } from './utils/context.js';
@@ -30,7 +30,8 @@ program
30
30
  .version(CLI_VERSION)
31
31
  .hook('preAction', (thisCommand, actionCommand) => {
32
32
  ConfigUtil.readConfig();
33
- if (actionCommand.name() != 'login' && actionCommand.name() != 'logout') {
33
+ const noAuthCommands = ['login', 'logout', 'run', 'status', 'stop'];
34
+ if (!noAuthCommands.includes(actionCommand.name())) {
34
35
  if (!ConfigUtil.config.token) {
35
36
  Logger.warn(`You are not logged in. Please login first using the \`${CLI_NAME} login\` command.`);
36
37
  process.exit(1);
@@ -100,5 +101,8 @@ program.addCommand(logoutCommand);
100
101
  program.addCommand(importCommand);
101
102
  program.addCommand(attachCommand);
102
103
  program.addCommand(quotasCommand);
104
+ program.addCommand(runCommand);
105
+ program.addCommand(statusCommand);
106
+ program.addCommand(stopCommand);
103
107
 
104
108
  program.parse(process.argv);