@optima-chat/bi-cli 0.2.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 (44) hide show
  1. package/dist/commands/analytics.d.ts +3 -0
  2. package/dist/commands/analytics.d.ts.map +1 -0
  3. package/dist/commands/analytics.js +228 -0
  4. package/dist/commands/analytics.js.map +1 -0
  5. package/dist/commands/auth.d.ts +3 -0
  6. package/dist/commands/auth.d.ts.map +1 -0
  7. package/dist/commands/auth.js +214 -0
  8. package/dist/commands/auth.js.map +1 -0
  9. package/dist/commands/product.d.ts +3 -0
  10. package/dist/commands/product.d.ts.map +1 -0
  11. package/dist/commands/product.js +199 -0
  12. package/dist/commands/product.js.map +1 -0
  13. package/dist/commands/sales.d.ts +3 -0
  14. package/dist/commands/sales.d.ts.map +1 -0
  15. package/dist/commands/sales.js +85 -0
  16. package/dist/commands/sales.js.map +1 -0
  17. package/dist/commands/trends.d.ts +3 -0
  18. package/dist/commands/trends.d.ts.map +1 -0
  19. package/dist/commands/trends.js +224 -0
  20. package/dist/commands/trends.js.map +1 -0
  21. package/dist/config/index.d.ts +21 -0
  22. package/dist/config/index.d.ts.map +1 -0
  23. package/dist/config/index.js +39 -0
  24. package/dist/config/index.js.map +1 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +52 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/utils/output.d.ts +11 -0
  30. package/dist/utils/output.d.ts.map +1 -0
  31. package/dist/utils/output.js +45 -0
  32. package/dist/utils/output.js.map +1 -0
  33. package/package.json +39 -0
  34. package/src/commands/analytics.ts +352 -0
  35. package/src/commands/auth.ts +277 -0
  36. package/src/commands/product.ts +327 -0
  37. package/src/commands/sales.ts +125 -0
  38. package/src/commands/trends.ts +355 -0
  39. package/src/config/index.ts +50 -0
  40. package/src/index.ts +64 -0
  41. package/src/utils/output.ts +52 -0
  42. package/test-auth.js +63 -0
  43. package/test-sales.js +45 -0
  44. package/tsconfig.json +13 -0
@@ -0,0 +1,277 @@
1
+ import { Command } from 'commander';
2
+ import axios from 'axios';
3
+ import ora from 'ora';
4
+ import chalk from 'chalk';
5
+ import { setConfig, clearAuth, getConfig } from '../config/index.js';
6
+ import { success, error, info } from '../utils/output.js';
7
+
8
+ interface DeviceCodeResponse {
9
+ device_code: string;
10
+ user_code: string;
11
+ verification_uri: string;
12
+ verification_uri_complete?: string;
13
+ expires_in: number;
14
+ interval: number;
15
+ }
16
+
17
+ interface TokenResponse {
18
+ access_token: string;
19
+ refresh_token: string;
20
+ expires_in: number;
21
+ token_type: string;
22
+ }
23
+
24
+ interface UserInfo {
25
+ id: string;
26
+ email: string;
27
+ role: 'merchant' | 'admin';
28
+ merchant_id?: string;
29
+ }
30
+
31
+ export function createAuthCommand(): Command {
32
+ const auth = new Command('auth').description('Authentication commands');
33
+
34
+ // auth login
35
+ auth
36
+ .command('login')
37
+ .description('Login with OAuth 2.0 Device Flow')
38
+ .option('--env <environment>', 'Environment (production|stage|development)', 'production')
39
+ .action(async (options) => {
40
+ const { env } = options;
41
+
42
+ // Set URLs based on environment
43
+ const authUrls = {
44
+ production: 'https://auth.optima.chat',
45
+ stage: 'https://auth-stage.optima.chat',
46
+ development: 'http://localhost:4000',
47
+ };
48
+
49
+ const backendUrls = {
50
+ production: 'https://bi-api.optima.chat',
51
+ stage: 'https://bi-api-stage.optima.chat',
52
+ development: 'http://localhost:3001',
53
+ };
54
+
55
+ const authUrl = authUrls[env as keyof typeof authUrls];
56
+ const backendUrl = backendUrls[env as keyof typeof backendUrls];
57
+
58
+ if (!authUrl || !backendUrl) {
59
+ error(`Invalid environment: ${env}`);
60
+ process.exit(1);
61
+ }
62
+
63
+ setConfig('environment', env);
64
+ setConfig('authUrl', authUrl);
65
+ setConfig('backendUrl', backendUrl);
66
+
67
+ info(`Logging in to ${env} environment...`);
68
+
69
+ try {
70
+ // Step 1: Request device code
71
+ const spinner = ora('Requesting device code...').start();
72
+ const deviceCodeRes = await axios.post<DeviceCodeResponse>(
73
+ `${authUrl}/api/v1/oauth/device/authorize`,
74
+ { client_id: 'bi-cli-aqkutatj' }
75
+ );
76
+ spinner.succeed('Device code received');
77
+
78
+ const {
79
+ device_code,
80
+ user_code,
81
+ verification_uri,
82
+ verification_uri_complete,
83
+ expires_in,
84
+ interval,
85
+ } = deviceCodeRes.data;
86
+
87
+ // Use verification_uri_complete if available (includes code pre-filled)
88
+ const browserUrl = verification_uri_complete || verification_uri;
89
+
90
+ // Step 2: Display authorization instructions
91
+ console.log(chalk.bold('\n📋 Authorization Required:\n'));
92
+ if (verification_uri_complete) {
93
+ console.log(` Opening browser with pre-filled code: ${chalk.yellow.bold(user_code)}`);
94
+ console.log(` URL: ${chalk.cyan(verification_uri_complete)}\n`);
95
+ } else {
96
+ console.log(` 1. Visit: ${chalk.cyan(verification_uri)}`);
97
+ console.log(` 2. Enter code: ${chalk.yellow.bold(user_code)}\n`);
98
+ }
99
+
100
+ // Step 3: Open browser automatically
101
+ const { default: open } = await import('open');
102
+ await open(browserUrl);
103
+ info('Browser opened automatically');
104
+
105
+ // Step 4: Poll for token
106
+ const pollSpinner = ora('Waiting for authorization...').start();
107
+ const startTime = Date.now();
108
+ const expiresAt = startTime + expires_in * 1000;
109
+
110
+ let token: TokenResponse | null = null;
111
+ let pollCount = 0;
112
+
113
+ while (Date.now() < expiresAt) {
114
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
115
+ pollCount++;
116
+
117
+ try {
118
+ const tokenRes = await axios.post<
119
+ TokenResponse | { error: string; error_description: string }
120
+ >(
121
+ `${authUrl}/api/v1/oauth/device/token`,
122
+ new URLSearchParams({
123
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
124
+ client_id: 'bi-cli-aqkutatj',
125
+ device_code,
126
+ }),
127
+ {
128
+ headers: {
129
+ 'Content-Type': 'application/x-www-form-urlencoded',
130
+ },
131
+ }
132
+ );
133
+
134
+ // Check if response contains an error (auth server returns 200 with error object)
135
+ if ('error' in tokenRes.data) {
136
+ const errorCode = tokenRes.data.error;
137
+ if (errorCode === 'authorization_pending') {
138
+ pollSpinner.text = `Waiting for authorization... (attempt ${pollCount})`;
139
+ continue;
140
+ } else if (errorCode === 'slow_down') {
141
+ pollSpinner.text = `Slowing down polling... (attempt ${pollCount})`;
142
+ await new Promise((resolve) => setTimeout(resolve, 5000));
143
+ continue;
144
+ } else {
145
+ pollSpinner.fail(`Polling failed: ${errorCode}`);
146
+ throw new Error(tokenRes.data.error_description);
147
+ }
148
+ }
149
+
150
+ token = tokenRes.data as TokenResponse;
151
+ pollSpinner.text = `Authorization successful after ${pollCount} attempts`;
152
+ break;
153
+ } catch (err: unknown) {
154
+ const error = err as { response?: { data?: { error?: string } } };
155
+ const errorCode = error.response?.data?.error;
156
+
157
+ if (errorCode === 'authorization_pending') {
158
+ // Continue polling
159
+ pollSpinner.text = `Waiting for authorization... (attempt ${pollCount})`;
160
+ continue;
161
+ } else if (errorCode === 'slow_down') {
162
+ // Increase interval
163
+ pollSpinner.text = `Slowing down polling... (attempt ${pollCount})`;
164
+ await new Promise((resolve) => setTimeout(resolve, 5000));
165
+ continue;
166
+ } else {
167
+ // Unexpected error
168
+ pollSpinner.fail(`Polling failed: ${errorCode || 'unknown error'}`);
169
+ throw err;
170
+ }
171
+ }
172
+ }
173
+
174
+ if (!token) {
175
+ pollSpinner.fail('Authorization timeout');
176
+ error('Please try again');
177
+ process.exit(1);
178
+ }
179
+
180
+ pollSpinner.succeed();
181
+
182
+ // Step 5: Save tokens
183
+ setConfig('accessToken', token.access_token);
184
+ setConfig('refreshToken', token.refresh_token);
185
+
186
+ // Step 6: Fetch user info (optional - just for display)
187
+ try {
188
+ const userInfo = await axios.get<UserInfo>(`${authUrl}/api/v1/users/me`, {
189
+ headers: { Authorization: `Bearer ${token.access_token}` },
190
+ });
191
+ success(`Logged in as ${chalk.bold(userInfo.data.email)} (${userInfo.data.role})`);
192
+ } catch (userInfoErr) {
193
+ // If fetching user info fails, still consider login successful
194
+ success('Login successful! Token saved.');
195
+ }
196
+ } catch (err: unknown) {
197
+ const errorMsg = err instanceof Error ? err.message : 'Unknown error';
198
+ error(`Login failed: ${errorMsg}`);
199
+ process.exit(1);
200
+ }
201
+ });
202
+
203
+ // auth logout
204
+ auth
205
+ .command('logout')
206
+ .description('Logout and clear stored credentials')
207
+ .action(() => {
208
+ clearAuth();
209
+ success('Logged out successfully');
210
+ });
211
+
212
+ // auth whoami
213
+ auth
214
+ .command('whoami')
215
+ .description('Show current user information')
216
+ .action(async () => {
217
+ const cfg = getConfig();
218
+
219
+ if (!cfg.accessToken) {
220
+ error('Not logged in. Run: bi-cli auth login');
221
+ process.exit(1);
222
+ }
223
+
224
+ try {
225
+ const userInfo = await axios.get<UserInfo>(`${cfg.authUrl}/api/v1/users/me`, {
226
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
227
+ });
228
+
229
+ console.log(chalk.bold('\n👤 Current User:\n'));
230
+ console.log(` Email: ${chalk.cyan(userInfo.data.email)}`);
231
+ console.log(` Role: ${chalk.yellow(userInfo.data.role)}`);
232
+ if (userInfo.data.merchant_id) {
233
+ console.log(` Merchant ID: ${chalk.gray(userInfo.data.merchant_id)}`);
234
+ }
235
+ console.log(` Environment: ${chalk.green(cfg.environment)}\n`);
236
+ } catch (err: unknown) {
237
+ const axiosError = err as { response?: { status?: number } };
238
+ if (axiosError.response?.status === 401) {
239
+ error('Token expired. Please login again: bi-cli auth login');
240
+ } else {
241
+ const errorMsg = err instanceof Error ? err.message : 'Unknown error';
242
+ error(`Failed to fetch user info: ${errorMsg}`);
243
+ }
244
+ process.exit(1);
245
+ }
246
+ });
247
+
248
+ // auth switch
249
+ auth
250
+ .command('switch')
251
+ .description('Switch environment')
252
+ .option('--env <environment>', 'Environment (production|stage|development)', 'production')
253
+ .action((options) => {
254
+ const { env } = options;
255
+
256
+ const backendUrls = {
257
+ production: 'https://bi-api.optima.chat',
258
+ stage: 'https://bi-api-stage.optima.chat',
259
+ development: 'http://localhost:3001',
260
+ };
261
+
262
+ const backendUrl = backendUrls[env as keyof typeof backendUrls];
263
+ if (!backendUrl) {
264
+ error(`Invalid environment: ${env}`);
265
+ process.exit(1);
266
+ }
267
+
268
+ setConfig('environment', env);
269
+ setConfig('backendUrl', backendUrl);
270
+ clearAuth(); // Clear tokens when switching environment
271
+
272
+ success(`Switched to ${env} environment`);
273
+ info('Please login again: bi-cli auth login');
274
+ });
275
+
276
+ return auth;
277
+ }
@@ -0,0 +1,327 @@
1
+ import { Command } from 'commander';
2
+ import axios from 'axios';
3
+ import { getConfig } from '../config/index.js';
4
+ import { outputJson, outputPretty, error, OutputFormat, info } from '../utils/output.js';
5
+
6
+ interface BestSellersResponse {
7
+ success: boolean;
8
+ data: {
9
+ merchant_id: string;
10
+ sort_by: string;
11
+ products: Array<{
12
+ rank: number;
13
+ title: string;
14
+ revenue: number;
15
+ units_sold: number;
16
+ orders: number;
17
+ avg_price: number;
18
+ revenue_share: number;
19
+ }>;
20
+ total_revenue: number;
21
+ };
22
+ }
23
+
24
+ interface ABCAnalysisResponse {
25
+ success: boolean;
26
+ data: {
27
+ merchant_id: string;
28
+ total_revenue: number;
29
+ summary: {
30
+ A: { description: string; count: number; revenue: number; percent_of_products: number };
31
+ B: { description: string; count: number; revenue: number; percent_of_products: number };
32
+ C: { description: string; count: number; revenue: number; percent_of_products: number };
33
+ };
34
+ products: Array<{
35
+ title: string;
36
+ revenue: number;
37
+ units: number;
38
+ revenue_percent: number;
39
+ cumulative_percent: number;
40
+ category: 'A' | 'B' | 'C';
41
+ }>;
42
+ };
43
+ }
44
+
45
+ interface PriceAnalysisResponse {
46
+ success: boolean;
47
+ data: {
48
+ merchant_id: string;
49
+ price_ranges: Array<{
50
+ range: string;
51
+ products: number;
52
+ revenue: number;
53
+ units: number;
54
+ avg_price: number;
55
+ revenue_share: number;
56
+ units_share: number;
57
+ }>;
58
+ totals: {
59
+ revenue: number;
60
+ units: number;
61
+ };
62
+ };
63
+ }
64
+
65
+ interface PerformanceResponse {
66
+ success: boolean;
67
+ data: {
68
+ merchant_id: string;
69
+ period: { days: number };
70
+ products: Array<{
71
+ title: string;
72
+ price: number;
73
+ cost: number | null;
74
+ inventory: number;
75
+ status: string;
76
+ sales: {
77
+ units: number;
78
+ revenue: number;
79
+ orders: number;
80
+ };
81
+ metrics: {
82
+ margin_percent: number;
83
+ days_of_stock: number;
84
+ velocity: number;
85
+ };
86
+ }>;
87
+ };
88
+ }
89
+
90
+ export function createProductCommand(): Command {
91
+ const product = new Command('product').description('Product analytics');
92
+
93
+ // product best-sellers
94
+ product
95
+ .command('best-sellers')
96
+ .description('Get best selling products')
97
+ .option('--limit <number>', 'Number of products to return', '10')
98
+ .option('--sort <field>', 'Sort by: revenue, quantity, orders', 'revenue')
99
+ .option('--pretty', 'Output in pretty table format')
100
+ .action(async (options) => {
101
+ const cfg = getConfig();
102
+ if (!cfg.accessToken) {
103
+ error('Not logged in. Run: bi-cli auth login');
104
+ process.exit(1);
105
+ }
106
+
107
+ const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
108
+
109
+ try {
110
+ const response = await axios.get<BestSellersResponse>(
111
+ `${cfg.backendUrl}/api/v1/products/best-sellers`,
112
+ {
113
+ params: { limit: options.limit, sort_by: options.sort },
114
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
115
+ }
116
+ );
117
+
118
+ if (!response.data.success) {
119
+ error('Failed to fetch best sellers');
120
+ process.exit(1);
121
+ }
122
+
123
+ const { products, total_revenue } = response.data.data;
124
+
125
+ if (format === OutputFormat.JSON) {
126
+ outputJson(response.data.data);
127
+ } else {
128
+ console.log('\n🏆 Best Selling Products\n');
129
+ outputPretty(
130
+ products.map((p) => ({
131
+ Rank: p.rank,
132
+ Title: p.title.length > 30 ? p.title.slice(0, 27) + '...' : p.title,
133
+ Revenue: `¥${p.revenue.toFixed(2)}`,
134
+ Units: p.units_sold,
135
+ Orders: p.orders,
136
+ Share: `${p.revenue_share.toFixed(1)}%`,
137
+ })),
138
+ ['Rank', 'Title', 'Revenue', 'Units', 'Orders', 'Share']
139
+ );
140
+ info(`Total Revenue: ¥${total_revenue.toFixed(2)}`);
141
+ }
142
+ } catch (err: unknown) {
143
+ handleApiError(err);
144
+ }
145
+ });
146
+
147
+ // product abc-analysis
148
+ product
149
+ .command('abc-analysis')
150
+ .description('ABC inventory analysis')
151
+ .option('--pretty', 'Output in pretty table format')
152
+ .action(async (options) => {
153
+ const cfg = getConfig();
154
+ if (!cfg.accessToken) {
155
+ error('Not logged in. Run: bi-cli auth login');
156
+ process.exit(1);
157
+ }
158
+
159
+ const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
160
+
161
+ try {
162
+ const response = await axios.get<ABCAnalysisResponse>(
163
+ `${cfg.backendUrl}/api/v1/products/abc-analysis`,
164
+ {
165
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
166
+ }
167
+ );
168
+
169
+ if (!response.data.success) {
170
+ error('Failed to fetch ABC analysis');
171
+ process.exit(1);
172
+ }
173
+
174
+ const { summary, products, total_revenue } = response.data.data;
175
+
176
+ if (format === OutputFormat.JSON) {
177
+ outputJson(response.data.data);
178
+ } else {
179
+ console.log('\n📊 ABC Analysis Summary\n');
180
+ outputPretty({
181
+ 'Category A (High Value)': `${summary.A.count} products (${summary.A.percent_of_products.toFixed(1)}%) - ¥${summary.A.revenue.toFixed(2)}`,
182
+ 'Category B (Medium Value)': `${summary.B.count} products (${summary.B.percent_of_products.toFixed(1)}%) - ¥${summary.B.revenue.toFixed(2)}`,
183
+ 'Category C (Low Value)': `${summary.C.count} products (${summary.C.percent_of_products.toFixed(1)}%) - ¥${summary.C.revenue.toFixed(2)}`,
184
+ 'Total Revenue': `¥${total_revenue.toFixed(2)}`,
185
+ });
186
+
187
+ console.log('\n📦 Top Products by Category\n');
188
+ const topProducts = products.slice(0, 15);
189
+ outputPretty(
190
+ topProducts.map((p) => ({
191
+ Category: p.category,
192
+ Title: p.title.length > 25 ? p.title.slice(0, 22) + '...' : p.title,
193
+ Revenue: `¥${p.revenue.toFixed(2)}`,
194
+ Units: p.units,
195
+ 'Revenue %': `${p.revenue_percent.toFixed(1)}%`,
196
+ })),
197
+ ['Category', 'Title', 'Revenue', 'Units', 'Revenue %']
198
+ );
199
+ }
200
+ } catch (err: unknown) {
201
+ handleApiError(err);
202
+ }
203
+ });
204
+
205
+ // product price-analysis
206
+ product
207
+ .command('price-analysis')
208
+ .description('Price point analysis')
209
+ .option('--pretty', 'Output in pretty table format')
210
+ .action(async (options) => {
211
+ const cfg = getConfig();
212
+ if (!cfg.accessToken) {
213
+ error('Not logged in. Run: bi-cli auth login');
214
+ process.exit(1);
215
+ }
216
+
217
+ const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
218
+
219
+ try {
220
+ const response = await axios.get<PriceAnalysisResponse>(
221
+ `${cfg.backendUrl}/api/v1/products/price-analysis`,
222
+ {
223
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
224
+ }
225
+ );
226
+
227
+ if (!response.data.success) {
228
+ error('Failed to fetch price analysis');
229
+ process.exit(1);
230
+ }
231
+
232
+ const { price_ranges, totals } = response.data.data;
233
+
234
+ if (format === OutputFormat.JSON) {
235
+ outputJson(response.data.data);
236
+ } else {
237
+ console.log('\n💰 Price Distribution Analysis\n');
238
+ outputPretty(
239
+ price_ranges.map((r) => ({
240
+ Range: r.range,
241
+ Products: r.products,
242
+ Revenue: `¥${r.revenue.toFixed(2)}`,
243
+ Units: r.units,
244
+ 'Avg Price': `¥${r.avg_price.toFixed(2)}`,
245
+ 'Revenue Share': `${r.revenue_share.toFixed(1)}%`,
246
+ })),
247
+ ['Range', 'Products', 'Revenue', 'Units', 'Avg Price', 'Revenue Share']
248
+ );
249
+ info(`Total Revenue: ¥${totals.revenue.toFixed(2)} | Total Units: ${totals.units}`);
250
+ }
251
+ } catch (err: unknown) {
252
+ handleApiError(err);
253
+ }
254
+ });
255
+
256
+ // product performance
257
+ product
258
+ .command('performance')
259
+ .description('Get product performance metrics')
260
+ .option('--days <number>', 'Number of days', '30')
261
+ .option('--limit <number>', 'Number of products', '20')
262
+ .option('--pretty', 'Output in pretty table format')
263
+ .action(async (options) => {
264
+ const cfg = getConfig();
265
+ if (!cfg.accessToken) {
266
+ error('Not logged in. Run: bi-cli auth login');
267
+ process.exit(1);
268
+ }
269
+
270
+ const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
271
+
272
+ try {
273
+ const response = await axios.get<PerformanceResponse>(
274
+ `${cfg.backendUrl}/api/v1/products/performance`,
275
+ {
276
+ params: { days: options.days, limit: options.limit },
277
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
278
+ }
279
+ );
280
+
281
+ if (!response.data.success) {
282
+ error('Failed to fetch product performance');
283
+ process.exit(1);
284
+ }
285
+
286
+ const { products, period } = response.data.data;
287
+
288
+ if (format === OutputFormat.JSON) {
289
+ outputJson(response.data.data);
290
+ } else {
291
+ console.log(`\n📈 Product Performance (Last ${period.days} days)\n`);
292
+ outputPretty(
293
+ products.map((p) => ({
294
+ Title: p.title.length > 20 ? p.title.slice(0, 17) + '...' : p.title,
295
+ Price: `¥${p.price.toFixed(2)}`,
296
+ Revenue: `¥${p.sales.revenue.toFixed(2)}`,
297
+ Units: p.sales.units,
298
+ Stock: p.inventory,
299
+ 'Days Stock': p.metrics.days_of_stock.toFixed(0),
300
+ 'Margin %': `${p.metrics.margin_percent.toFixed(1)}%`,
301
+ })),
302
+ ['Title', 'Price', 'Revenue', 'Units', 'Stock', 'Days Stock', 'Margin %']
303
+ );
304
+ }
305
+ } catch (err: unknown) {
306
+ handleApiError(err);
307
+ }
308
+ });
309
+
310
+ return product;
311
+ }
312
+
313
+ function handleApiError(err: unknown): never {
314
+ const axiosError = err as {
315
+ response?: { status?: number; data?: { error?: { message?: string } } };
316
+ };
317
+ const errorObj = err as Error;
318
+
319
+ if (axiosError.response?.status === 401) {
320
+ error('Authentication failed. Please login again: bi-cli auth login');
321
+ } else if (axiosError.response?.data?.error) {
322
+ error(`Error: ${axiosError.response.data.error.message}`);
323
+ } else {
324
+ error(`Request failed: ${errorObj.message || 'Unknown error'}`);
325
+ }
326
+ process.exit(1);
327
+ }