@redocly/cli 1.29.0 → 1.31.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 (88) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +36 -16
  3. package/lib/__tests__/commands/push-region.test.js +3 -3
  4. package/lib/auth/__tests__/device-flow.test.js +62 -0
  5. package/lib/auth/__tests__/oauth-client.test.js +93 -0
  6. package/lib/auth/device-flow.d.ts +26 -0
  7. package/lib/auth/device-flow.js +133 -0
  8. package/lib/auth/oauth-client.d.ts +14 -0
  9. package/lib/auth/oauth-client.js +93 -0
  10. package/lib/commands/auth.d.ts +13 -0
  11. package/lib/commands/auth.js +51 -0
  12. package/lib/commands/push.d.ts +1 -1
  13. package/lib/commands/push.js +4 -4
  14. package/lib/index.js +103 -15
  15. package/lib/otel.d.ts +10 -0
  16. package/lib/otel.js +47 -0
  17. package/lib/reunite/api/__tests__/domains.test.js +32 -0
  18. package/lib/{cms → reunite}/api/api-client.d.ts +9 -0
  19. package/lib/{cms → reunite}/api/api-client.js +2 -1
  20. package/lib/reunite/api/domains.d.ts +4 -0
  21. package/lib/reunite/api/domains.js +22 -0
  22. package/lib/reunite/commands/__tests__/push.test.d.ts +1 -0
  23. package/lib/reunite/commands/__tests__/utils.test.d.ts +1 -0
  24. package/lib/types.d.ts +5 -4
  25. package/lib/utils/miscellaneous.d.ts +5 -4
  26. package/lib/utils/miscellaneous.js +14 -14
  27. package/package.json +11 -4
  28. package/src/__tests__/commands/push-region.test.ts +2 -2
  29. package/src/auth/__tests__/device-flow.test.ts +73 -0
  30. package/src/auth/__tests__/oauth-client.test.ts +117 -0
  31. package/src/auth/device-flow.ts +175 -0
  32. package/src/auth/oauth-client.ts +111 -0
  33. package/src/commands/auth.ts +66 -0
  34. package/src/commands/push.ts +3 -3
  35. package/src/index.ts +115 -16
  36. package/src/otel.ts +59 -0
  37. package/src/reunite/api/__tests__/domains.test.ts +41 -0
  38. package/src/{cms → reunite}/api/api-client.ts +1 -1
  39. package/src/reunite/api/domains.ts +23 -0
  40. package/src/types.ts +8 -4
  41. package/src/utils/miscellaneous.ts +19 -18
  42. package/tsconfig.json +1 -1
  43. package/tsconfig.tsbuildinfo +1 -1
  44. package/lib/cms/api/__tests__/domains.test.js +0 -13
  45. package/lib/cms/api/domains.d.ts +0 -1
  46. package/lib/cms/api/domains.js +0 -11
  47. package/lib/commands/login.d.ts +0 -9
  48. package/lib/commands/login.js +0 -23
  49. package/src/cms/api/__tests__/domains.test.ts +0 -15
  50. package/src/cms/api/domains.ts +0 -11
  51. package/src/commands/login.ts +0 -34
  52. /package/lib/{cms/api/__tests__/api-keys.test.d.ts → auth/__tests__/device-flow.test.d.ts} +0 -0
  53. /package/lib/{cms/api/__tests__/api.client.test.d.ts → auth/__tests__/oauth-client.test.d.ts} +0 -0
  54. /package/lib/{cms/api/__tests__/domains.test.d.ts → reunite/api/__tests__/api-keys.test.d.ts} +0 -0
  55. /package/lib/{cms → reunite}/api/__tests__/api-keys.test.js +0 -0
  56. /package/lib/{cms/commands/__tests__/push-status.test.d.ts → reunite/api/__tests__/api.client.test.d.ts} +0 -0
  57. /package/lib/{cms → reunite}/api/__tests__/api.client.test.js +0 -0
  58. /package/lib/{cms/commands/__tests__/push.test.d.ts → reunite/api/__tests__/domains.test.d.ts} +0 -0
  59. /package/lib/{cms → reunite}/api/api-keys.d.ts +0 -0
  60. /package/lib/{cms → reunite}/api/api-keys.js +0 -0
  61. /package/lib/{cms → reunite}/api/index.d.ts +0 -0
  62. /package/lib/{cms → reunite}/api/index.js +0 -0
  63. /package/lib/{cms → reunite}/api/types.d.ts +0 -0
  64. /package/lib/{cms → reunite}/api/types.js +0 -0
  65. /package/lib/{cms/commands/__tests__/utils.test.d.ts → reunite/commands/__tests__/push-status.test.d.ts} +0 -0
  66. /package/lib/{cms → reunite}/commands/__tests__/push-status.test.js +0 -0
  67. /package/lib/{cms → reunite}/commands/__tests__/push.test.js +0 -0
  68. /package/lib/{cms → reunite}/commands/__tests__/utils.test.js +0 -0
  69. /package/lib/{cms → reunite}/commands/push-status.d.ts +0 -0
  70. /package/lib/{cms → reunite}/commands/push-status.js +0 -0
  71. /package/lib/{cms → reunite}/commands/push.d.ts +0 -0
  72. /package/lib/{cms → reunite}/commands/push.js +0 -0
  73. /package/lib/{cms → reunite}/commands/utils.d.ts +0 -0
  74. /package/lib/{cms → reunite}/commands/utils.js +0 -0
  75. /package/lib/{cms → reunite}/utils.d.ts +0 -0
  76. /package/lib/{cms → reunite}/utils.js +0 -0
  77. /package/src/{cms → reunite}/api/__tests__/api-keys.test.ts +0 -0
  78. /package/src/{cms → reunite}/api/__tests__/api.client.test.ts +0 -0
  79. /package/src/{cms → reunite}/api/api-keys.ts +0 -0
  80. /package/src/{cms → reunite}/api/index.ts +0 -0
  81. /package/src/{cms → reunite}/api/types.ts +0 -0
  82. /package/src/{cms → reunite}/commands/__tests__/push-status.test.ts +0 -0
  83. /package/src/{cms → reunite}/commands/__tests__/push.test.ts +0 -0
  84. /package/src/{cms → reunite}/commands/__tests__/utils.test.ts +0 -0
  85. /package/src/{cms → reunite}/commands/push-status.ts +0 -0
  86. /package/src/{cms → reunite}/commands/push.ts +0 -0
  87. /package/src/{cms → reunite}/commands/utils.ts +0 -0
  88. /package/src/{cms → reunite}/utils.ts +0 -0
@@ -0,0 +1,117 @@
1
+ import { RedoclyOAuthClient } from '../oauth-client';
2
+ import { RedoclyOAuthDeviceFlow } from '../device-flow';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import * as os from 'node:os';
6
+
7
+ jest.mock('node:fs');
8
+ jest.mock('node:os');
9
+ jest.mock('../device-flow');
10
+
11
+ describe('RedoclyOAuthClient', () => {
12
+ const mockClientName = 'test-client';
13
+ const mockVersion = '1.0.0';
14
+ const mockBaseUrl = 'https://test.redocly.com';
15
+ const mockHomeDir = '/mock/home/dir';
16
+ const mockRedoclyDir = path.join(mockHomeDir, '.redocly');
17
+ let client: RedoclyOAuthClient;
18
+
19
+ beforeEach(() => {
20
+ jest.resetAllMocks();
21
+ (os.homedir as jest.Mock).mockReturnValue(mockHomeDir);
22
+ process.env.HOME = mockHomeDir;
23
+ client = new RedoclyOAuthClient(mockClientName, mockVersion);
24
+ });
25
+
26
+ describe('login', () => {
27
+ it('successfully logs in and saves token', async () => {
28
+ const mockToken = { access_token: 'test-token' };
29
+ const mockDeviceFlow = {
30
+ run: jest.fn().mockResolvedValue(mockToken),
31
+ };
32
+ (RedoclyOAuthDeviceFlow as jest.Mock).mockImplementation(() => mockDeviceFlow);
33
+
34
+ await client.login(mockBaseUrl);
35
+
36
+ expect(mockDeviceFlow.run).toHaveBeenCalled();
37
+ expect(fs.writeFileSync).toHaveBeenCalled();
38
+ });
39
+
40
+ it('throws error when login fails', async () => {
41
+ const mockDeviceFlow = {
42
+ run: jest.fn().mockResolvedValue(null),
43
+ };
44
+ (RedoclyOAuthDeviceFlow as jest.Mock).mockImplementation(() => mockDeviceFlow);
45
+
46
+ await expect(client.login(mockBaseUrl)).rejects.toThrow('Failed to login');
47
+ });
48
+ });
49
+
50
+ describe('logout', () => {
51
+ it('removes token file if it exists', async () => {
52
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
53
+
54
+ await client.logout();
55
+
56
+ expect(fs.rmSync).toHaveBeenCalledWith(path.join(mockRedoclyDir, 'auth.json'));
57
+ });
58
+
59
+ it('silently fails if token file does not exist', async () => {
60
+ (fs.existsSync as jest.Mock).mockReturnValue(false);
61
+
62
+ await expect(client.logout()).resolves.not.toThrow();
63
+ expect(fs.rmSync).not.toHaveBeenCalled();
64
+ });
65
+ });
66
+
67
+ describe('isAuthorized', () => {
68
+ it('verifies API key if provided', async () => {
69
+ const mockDeviceFlow = {
70
+ verifyApiKey: jest.fn().mockResolvedValue(true),
71
+ };
72
+ (RedoclyOAuthDeviceFlow as jest.Mock).mockImplementation(() => mockDeviceFlow);
73
+
74
+ const result = await client.isAuthorized(mockBaseUrl, 'test-api-key');
75
+
76
+ expect(result).toBe(true);
77
+ expect(mockDeviceFlow.verifyApiKey).toHaveBeenCalledWith('test-api-key');
78
+ });
79
+
80
+ it('verifies access token if no API key provided', async () => {
81
+ const mockToken = { access_token: 'test-token' };
82
+ const mockDeviceFlow = {
83
+ verifyToken: jest.fn().mockResolvedValue(true),
84
+ };
85
+ (RedoclyOAuthDeviceFlow as jest.Mock).mockImplementation(() => mockDeviceFlow);
86
+ (fs.readFileSync as jest.Mock).mockReturnValue(
87
+ client['cipher'].update(JSON.stringify(mockToken), 'utf8', 'hex') +
88
+ client['cipher'].final('hex')
89
+ );
90
+
91
+ const result = await client.isAuthorized(mockBaseUrl);
92
+
93
+ expect(result).toBe(true);
94
+ expect(mockDeviceFlow.verifyToken).toHaveBeenCalledWith('test-token');
95
+ });
96
+
97
+ it('returns false if token refresh fails', async () => {
98
+ const mockToken = {
99
+ access_token: 'old-token',
100
+ refresh_token: 'refresh-token',
101
+ };
102
+ const mockDeviceFlow = {
103
+ verifyToken: jest.fn().mockResolvedValue(false),
104
+ refreshToken: jest.fn().mockRejectedValue(new Error('Refresh failed')),
105
+ };
106
+ (RedoclyOAuthDeviceFlow as jest.Mock).mockImplementation(() => mockDeviceFlow);
107
+ (fs.readFileSync as jest.Mock).mockReturnValue(
108
+ client['cipher'].update(JSON.stringify(mockToken), 'utf8', 'hex') +
109
+ client['cipher'].final('hex')
110
+ );
111
+
112
+ const result = await client.isAuthorized(mockBaseUrl);
113
+
114
+ expect(result).toBe(false);
115
+ });
116
+ });
117
+ });
@@ -0,0 +1,175 @@
1
+ import { blue, green } from 'colorette';
2
+ import * as childProcess from 'child_process';
3
+ import { ReuniteApiClient } from '../reunite/api/api-client';
4
+
5
+ export type AuthToken = {
6
+ access_token: string;
7
+ refresh_token?: string;
8
+ token_type?: string;
9
+ expires_in?: number;
10
+ };
11
+
12
+ export class RedoclyOAuthDeviceFlow {
13
+ private apiClient: ReuniteApiClient;
14
+
15
+ constructor(private baseUrl: string, private clientName: string, private version: string) {
16
+ this.apiClient = new ReuniteApiClient(this.version, 'login');
17
+ }
18
+
19
+ async run() {
20
+ const code = await this.getDeviceCode();
21
+ process.stdout.write(
22
+ 'Attempting to automatically open the SSO authorization page in your default browser.\n'
23
+ );
24
+ process.stdout.write(
25
+ 'If the browser does not open or you wish to use a different device to authorize this request, open the following URL:\n\n'
26
+ );
27
+ process.stdout.write(blue(code.verificationUri));
28
+ process.stdout.write(`\n\n`);
29
+ process.stdout.write(`Then enter the code:\n\n`);
30
+ process.stdout.write(blue(code.userCode));
31
+ process.stdout.write(`\n\n`);
32
+
33
+ this.openBrowser(code.verificationUriComplete);
34
+
35
+ const accessToken = await this.pollingAccessToken(
36
+ code.deviceCode,
37
+ code.interval,
38
+ code.expiresIn
39
+ );
40
+ process.stdout.write(green('✅ Logged in\n\n'));
41
+
42
+ return accessToken;
43
+ }
44
+
45
+ private openBrowser(url: string) {
46
+ try {
47
+ const cmd =
48
+ process.platform === 'win32'
49
+ ? `start ${url}`
50
+ : process.platform === 'darwin'
51
+ ? `open ${url}`
52
+ : `xdg-open ${url}`;
53
+
54
+ childProcess.execSync(cmd);
55
+ } catch {
56
+ // silently fail if browser cannot be opened
57
+ }
58
+ }
59
+
60
+ async verifyToken(accessToken: string) {
61
+ try {
62
+ const response = await this.sendRequest('/session', 'GET', undefined, {
63
+ Cookie: `accessToken=${accessToken};`,
64
+ });
65
+
66
+ return !!response.user;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ async verifyApiKey(apiKey: string) {
73
+ try {
74
+ const response = await this.sendRequest('/api-keys-verify', 'POST', {
75
+ apiKey,
76
+ });
77
+ return !!response.success;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ async refreshToken(refreshToken: string) {
84
+ const response = await this.sendRequest(`/device-rotate-token`, 'POST', {
85
+ grant_type: 'refresh_token',
86
+ client_name: this.clientName,
87
+ refresh_token: refreshToken,
88
+ });
89
+
90
+ if (!response.access_token) {
91
+ throw new Error('Failed to refresh token');
92
+ }
93
+ return {
94
+ access_token: response.access_token,
95
+ refresh_token: response.refresh_token,
96
+ expires_in: response.expires_in,
97
+ };
98
+ }
99
+
100
+ private async pollingAccessToken(
101
+ deviceCode: string,
102
+ interval: number,
103
+ expiresIn: number
104
+ ): Promise<AuthToken> {
105
+ return new Promise((resolve, reject) => {
106
+ const intervalId = setInterval(async () => {
107
+ const response = await this.getAccessToken(deviceCode);
108
+ if (response.access_token) {
109
+ clearInterval(intervalId);
110
+ clearTimeout(timeoutId);
111
+ resolve(response);
112
+ }
113
+ if (response.error && response.error !== 'authorization_pending') {
114
+ clearInterval(intervalId);
115
+ clearTimeout(timeoutId);
116
+ reject(response.error_description);
117
+ }
118
+ }, interval * 1000);
119
+
120
+ const timeoutId = setTimeout(async () => {
121
+ clearInterval(intervalId);
122
+ clearTimeout(timeoutId);
123
+ reject('Authorization has expired. Please try again.');
124
+ }, expiresIn * 1000);
125
+ });
126
+ }
127
+
128
+ private async getAccessToken(deviceCode: string) {
129
+ return await this.sendRequest('/device-token', 'POST', {
130
+ client_name: this.clientName,
131
+ device_code: deviceCode,
132
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
133
+ });
134
+ }
135
+
136
+ private async getDeviceCode() {
137
+ const {
138
+ device_code: deviceCode,
139
+ user_code: userCode,
140
+ verification_uri: verificationUri,
141
+ verification_uri_complete: verificationUriComplete,
142
+ interval = 10,
143
+ expires_in: expiresIn = 300,
144
+ } = await this.sendRequest('/device-authorize', 'POST', {
145
+ client_name: this.clientName,
146
+ });
147
+
148
+ return {
149
+ deviceCode,
150
+ userCode,
151
+ verificationUri,
152
+ verificationUriComplete,
153
+ interval,
154
+ expiresIn,
155
+ };
156
+ }
157
+
158
+ private async sendRequest(
159
+ url: string,
160
+ method: string = 'GET',
161
+ body: Record<string, unknown> | undefined = undefined,
162
+ headers: Record<string, string> = {}
163
+ ) {
164
+ url = `${this.baseUrl}${url}`;
165
+ const response = await this.apiClient.request(url, {
166
+ body: body ? JSON.stringify(body) : body,
167
+ method,
168
+ headers: { 'Content-Type': 'application/json', ...headers },
169
+ });
170
+ if (response.status === 204) {
171
+ return { success: true };
172
+ }
173
+ return await response.json();
174
+ }
175
+ }
@@ -0,0 +1,111 @@
1
+ import { homedir } from 'node:os';
2
+ import * as path from 'node:path';
3
+ import { mkdirSync, existsSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
4
+ import * as crypto from 'node:crypto';
5
+ import { Buffer } from 'node:buffer';
6
+ import { type AuthToken, RedoclyOAuthDeviceFlow } from './device-flow';
7
+
8
+ const SALT = '4618dbc9-8aed-4e27-aaf0-225f4603e5a4';
9
+ const CRYPTO_ALGORITHM = 'aes-256-cbc';
10
+
11
+ export class RedoclyOAuthClient {
12
+ private dir: string;
13
+ private cipher: crypto.Cipher;
14
+ private decipher: crypto.Decipher;
15
+
16
+ constructor(private clientName: string, private version: string) {
17
+ this.dir = path.join(homedir(), '.redocly');
18
+ if (!existsSync(this.dir)) {
19
+ mkdirSync(this.dir);
20
+ }
21
+
22
+ const homeDirPath = process.env.HOME as string;
23
+ const hash = crypto.createHash('sha256');
24
+ hash.update(`${homeDirPath}${SALT}`);
25
+ const hashHex = hash.digest('hex');
26
+
27
+ const key = Buffer.alloc(
28
+ 32,
29
+ Buffer.from(hashHex).toString('base64')
30
+ ).toString() as crypto.CipherKey;
31
+ const iv = Buffer.alloc(
32
+ 16,
33
+ Buffer.from(process.env.HOME as string).toString('base64')
34
+ ).toString() as crypto.BinaryLike;
35
+ this.cipher = crypto.createCipheriv(CRYPTO_ALGORITHM, key, iv);
36
+ this.decipher = crypto.createDecipheriv(CRYPTO_ALGORITHM, key, iv);
37
+ }
38
+
39
+ async login(baseUrl: string) {
40
+ const deviceFlow = new RedoclyOAuthDeviceFlow(baseUrl, this.clientName, this.version);
41
+
42
+ const token = await deviceFlow.run();
43
+ if (!token) {
44
+ throw new Error('Failed to login');
45
+ }
46
+ this.saveToken(token);
47
+ }
48
+
49
+ async logout() {
50
+ try {
51
+ this.removeToken();
52
+ } catch (err) {
53
+ // do nothing
54
+ }
55
+ }
56
+
57
+ async isAuthorized(baseUrl: string, apiKey?: string) {
58
+ const deviceFlow = new RedoclyOAuthDeviceFlow(baseUrl, this.clientName, this.version);
59
+
60
+ if (apiKey) {
61
+ return await deviceFlow.verifyApiKey(apiKey);
62
+ }
63
+
64
+ const token = await this.readToken();
65
+ if (!token) {
66
+ return false;
67
+ }
68
+
69
+ const isValidAccessToken = await deviceFlow.verifyToken(token.access_token);
70
+
71
+ if (isValidAccessToken) {
72
+ return true;
73
+ }
74
+
75
+ try {
76
+ const newToken = await deviceFlow.refreshToken(token.refresh_token);
77
+ await this.saveToken(newToken);
78
+ } catch {
79
+ return false;
80
+ }
81
+
82
+ return true;
83
+ }
84
+
85
+ private async saveToken(token: AuthToken) {
86
+ try {
87
+ const encrypted =
88
+ this.cipher.update(JSON.stringify(token), 'utf8', 'hex') + this.cipher.final('hex');
89
+ writeFileSync(path.join(this.dir, 'auth.json'), encrypted);
90
+ } catch (error) {
91
+ process.stderr.write('Error saving tokens:', error);
92
+ }
93
+ }
94
+
95
+ private async readToken() {
96
+ try {
97
+ const token = readFileSync(path.join(this.dir, 'auth.json'), 'utf8');
98
+ const decrypted = this.decipher.update(token, 'hex', 'utf8') + this.decipher.final('utf8');
99
+ return decrypted ? JSON.parse(decrypted) : null;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ private async removeToken() {
106
+ const tokenPath = path.join(this.dir, 'auth.json');
107
+ if (existsSync(tokenPath)) {
108
+ rmSync(tokenPath);
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,66 @@
1
+ import { blue, green, gray, yellow } from 'colorette';
2
+ import { RedoclyClient } from '@redocly/openapi-core';
3
+ import { exitWithError, promptUser } from '../utils/miscellaneous';
4
+ import { RedoclyOAuthClient } from '../auth/oauth-client';
5
+ import { getReuniteUrl } from '../reunite/api';
6
+
7
+ import type { CommandArgs } from '../wrapper';
8
+ import type { Region } from '@redocly/openapi-core';
9
+
10
+ export function promptClientToken(domain: string) {
11
+ return promptUser(
12
+ green(
13
+ `\n 🔑 Copy your API key from ${blue(`https://app.${domain}/profile`)} and paste it below`
14
+ ) + yellow(' (if you want to log in with Reunite, please run `redocly login --next` instead)'),
15
+ true
16
+ );
17
+ }
18
+
19
+ export type LoginOptions = {
20
+ verbose?: boolean;
21
+ residency?: string;
22
+ config?: string;
23
+ next?: boolean;
24
+ };
25
+
26
+ export async function handleLogin({ argv, config, version }: CommandArgs<LoginOptions>) {
27
+ if (argv.next) {
28
+ try {
29
+ const reuniteUrl = getReuniteUrl(argv.residency);
30
+ const oauthClient = new RedoclyOAuthClient('redocly-cli', version);
31
+ await oauthClient.login(reuniteUrl);
32
+ } catch {
33
+ if (argv.residency) {
34
+ const reuniteUrl = getReuniteUrl(argv.residency);
35
+ exitWithError(`❌ Connection to ${reuniteUrl} failed.`);
36
+ } else {
37
+ exitWithError(`❌ Login failed. Please check your credentials and try again.`);
38
+ }
39
+ }
40
+ } else {
41
+ try {
42
+ const region = (argv.residency as Region) || config.region;
43
+ const client = new RedoclyClient(region);
44
+ const clientToken = await promptClientToken(client.domain);
45
+ process.stdout.write(gray('\n Logging in...\n'));
46
+ await client.login(clientToken, argv.verbose);
47
+ process.stdout.write(green(' Authorization confirmed. ✅\n\n'));
48
+ } catch (err) {
49
+ exitWithError(' ' + err?.message);
50
+ }
51
+ }
52
+ }
53
+
54
+ export type LogoutOptions = {
55
+ config?: string;
56
+ };
57
+
58
+ export async function handleLogout({ version }: CommandArgs<LogoutOptions>) {
59
+ const client = new RedoclyClient();
60
+ client.logout();
61
+
62
+ const oauthClient = new RedoclyOAuthClient('redocly-cli', version);
63
+ oauthClient.logout();
64
+
65
+ process.stdout.write('Logged out from the Redocly account. ✋ \n');
66
+ }
@@ -19,9 +19,9 @@ import {
19
19
  getFallbackApisOrExit,
20
20
  dumpBundle,
21
21
  } from '../utils/miscellaneous';
22
- import { promptClientToken } from './login';
23
- import { handlePush as handleCMSPush } from '../cms/commands/push';
24
- import { streamToBuffer } from '../cms/api/api-client';
22
+ import { promptClientToken } from './auth';
23
+ import { handlePush as handleCMSPush } from '../reunite/commands/push';
24
+ import { streamToBuffer } from '../reunite/api/api-client';
25
25
 
26
26
  import type { Readable } from 'node:stream';
27
27
  import type { Agent } from 'node:http';
package/src/index.ts CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
-
2
+ import * as path from 'path';
3
+ import * as dotenv from 'dotenv';
3
4
  import './utils/assert-node-version';
4
5
  import * as yargs from 'yargs';
5
6
  import * as colors from 'colorette';
6
- import { RedoclyClient } from '@redocly/openapi-core';
7
7
  import { outputExtensions, regionChoices } from './types';
8
8
  import { previewDocs } from './commands/preview-docs';
9
9
  import { handleStats } from './commands/stats';
10
10
  import { handleSplit } from './commands/split';
11
11
  import { handleJoin } from './commands/join';
12
- import { handlePushStatus } from './cms/commands/push-status';
12
+ import { handlePushStatus } from './reunite/commands/push-status';
13
13
  import { handleLint } from './commands/lint';
14
14
  import { handleBundle } from './commands/bundle';
15
- import { handleLogin } from './commands/login';
15
+ import { handleLogin, handleLogout } from './commands/auth';
16
16
  import { handlerBuildCommand } from './commands/build-docs';
17
17
  import {
18
18
  cacheLatestVersion,
@@ -28,11 +28,14 @@ import { commonPushHandler } from './commands/push';
28
28
 
29
29
  import type { Arguments } from 'yargs';
30
30
  import type { OutputFormat, RuleSeverity } from '@redocly/openapi-core';
31
+ import type { GenerateArazzoFileOptions, RespectOptions } from '@redocly/respect-core';
31
32
  import type { BuildDocsArgv } from './commands/build-docs/types';
32
- import type { PushStatusOptions } from './cms/commands/push-status';
33
+ import type { PushStatusOptions } from './reunite/commands/push-status';
33
34
  import type { PushArguments } from './types';
34
35
  import type { EjectOptions } from './commands/eject';
35
36
 
37
+ dotenv.config({ path: path.resolve(process.cwd(), './.env') });
38
+
36
39
  if (!('replaceAll' in String.prototype)) {
37
40
  require('core-js/actual/string/replace-all');
38
41
  }
@@ -605,23 +608,27 @@ yargs
605
608
  )
606
609
  .command(
607
610
  'login',
608
- 'Login to the Redocly API registry with an access token.',
611
+ 'Log in to Redocly.',
609
612
  async (yargs) =>
610
613
  yargs.options({
611
614
  verbose: {
612
615
  description: 'Include additional output.',
613
616
  type: 'boolean',
614
617
  },
615
- region: {
616
- description: 'Specify a region.',
617
- alias: 'r',
618
- choices: regionChoices,
618
+ residency: {
619
+ description: 'Residency of the application. Defaults to `us`.',
620
+ alias: ['r', 'region'],
621
+ type: 'string',
619
622
  },
620
623
  config: {
621
624
  description: 'Path to the config file.',
622
625
  requiresArg: true,
623
626
  type: 'string',
624
627
  },
628
+ next: {
629
+ description: 'Use Reunite application to login.',
630
+ type: 'boolean',
631
+ },
625
632
  }),
626
633
  (argv) => {
627
634
  process.env.REDOCLY_CLI_COMMAND = 'login';
@@ -632,13 +639,9 @@ yargs
632
639
  'logout',
633
640
  'Clear your stored credentials for the Redocly API registry.',
634
641
  (yargs) => yargs,
635
- async (argv) => {
642
+ (argv) => {
636
643
  process.env.REDOCLY_CLI_COMMAND = 'logout';
637
- await commandWrapper(async () => {
638
- const client = new RedoclyClient();
639
- client.logout();
640
- process.stdout.write('Logged out from the Redocly account. ✋\n');
641
- })(argv);
644
+ commandWrapper(handleLogout)(argv);
642
645
  }
643
646
  )
644
647
  .command(
@@ -864,6 +867,102 @@ yargs
864
867
  commandWrapper(handleEject)(argv as Arguments<EjectOptions>);
865
868
  }
866
869
  )
870
+ .command(
871
+ 'respect [files...]',
872
+ 'Run Arazzo tests.',
873
+ (yargs) => {
874
+ return yargs
875
+ .positional('files', {
876
+ describe: 'Test files or glob pattern.',
877
+ type: 'string',
878
+ array: true,
879
+ default: [],
880
+ })
881
+ .env('REDOCLY_CLI_RESPECT')
882
+ .options({
883
+ input: {
884
+ alias: 'i',
885
+ describe: 'Input parameters.',
886
+ type: 'string',
887
+ },
888
+ server: {
889
+ alias: 'S',
890
+ describe: 'Server parameters.',
891
+ type: 'string',
892
+ },
893
+ workflow: {
894
+ alias: 'w',
895
+ describe: 'Workflow name.',
896
+ type: 'string',
897
+ array: true,
898
+ },
899
+ skip: {
900
+ alias: 's',
901
+ describe: 'Workflow to skip.',
902
+ type: 'string',
903
+ array: true,
904
+ },
905
+ verbose: {
906
+ alias: 'v',
907
+ describe: 'Apply verbose mode.',
908
+ type: 'boolean',
909
+ },
910
+ 'har-output': {
911
+ describe: 'Har file output name.',
912
+ type: 'string',
913
+ },
914
+ 'json-output': {
915
+ describe: 'JSON file output name.',
916
+ type: 'string',
917
+ },
918
+ 'client-cert': {
919
+ describe: 'Mutual TLS client certificate.',
920
+ type: 'string',
921
+ },
922
+ 'client-key': {
923
+ describe: 'Mutual TLS client key.',
924
+ type: 'string',
925
+ },
926
+ 'ca-cert': {
927
+ describe: 'Mutual TLS CA certificate.',
928
+ type: 'string',
929
+ },
930
+ severity: {
931
+ describe: 'Severity of the check.',
932
+ type: 'string',
933
+ },
934
+ });
935
+ },
936
+ async (argv) => {
937
+ process.env.REDOCLY_CLI_COMMAND = 'respect';
938
+ const { handleRun } = await import('@redocly/respect-core');
939
+ commandWrapper(handleRun)(argv as Arguments<RespectOptions>);
940
+ }
941
+ )
942
+ .command(
943
+ 'generate-arazzo <descriptionPath>',
944
+ 'Auto-generate arazzo description file from an API description.',
945
+ (yargs) => {
946
+ return yargs
947
+ .positional('descriptionPath', {
948
+ describe: 'Description file path.',
949
+ type: 'string',
950
+ })
951
+ .env('REDOCLY_CLI_RESPECT')
952
+ .options({
953
+ 'output-file': {
954
+ alias: 'o',
955
+ describe: 'Output File name.',
956
+ type: 'string',
957
+ },
958
+ });
959
+ },
960
+ async (argv) => {
961
+ process.env.REDOCLY_CLI_COMMAND = 'generate-arazzo';
962
+ const { handleGenerate } = await import('@redocly/respect-core');
963
+ commandWrapper(handleGenerate)(argv as Arguments<GenerateArazzoFileOptions>);
964
+ }
965
+ )
867
966
  .completion('completion', 'Generate autocomplete script for `redocly` command.')
868
967
  .demandCommand(1)
869
968
  .middleware([notifyUpdateCliVersion])