@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,125 @@
1
+ import { Command } from 'commander';
2
+ import axios from 'axios';
3
+ import { getConfig } from '../config/index.js';
4
+ import { outputJson, outputPretty, error, OutputFormat } from '../utils/output.js';
5
+
6
+ interface SalesSummary {
7
+ total_revenue: number;
8
+ total_orders: number;
9
+ avg_order_value: number;
10
+ unique_customers: number;
11
+ }
12
+
13
+ interface DailySales {
14
+ merchant_id: string;
15
+ date: string;
16
+ total_revenue: number;
17
+ order_count: number;
18
+ avg_order_value: number;
19
+ unique_customers: number;
20
+ }
21
+
22
+ interface SalesResponse {
23
+ success: boolean;
24
+ data: {
25
+ summary: SalesSummary;
26
+ daily: DailySales[];
27
+ };
28
+ meta: {
29
+ cached: boolean;
30
+ days: number;
31
+ query_time_ms?: number;
32
+ };
33
+ }
34
+
35
+ export function createSalesCommand(): Command {
36
+ const sales = new Command('sales').description('Sales analytics');
37
+
38
+ // sales get
39
+ sales
40
+ .command('get')
41
+ .description('Get sales data')
42
+ .option('--days <number>', 'Number of days to fetch', '7')
43
+ .option('--pretty', 'Output in pretty table format (default: JSON)')
44
+ .action(async (options) => {
45
+ const cfg = getConfig();
46
+
47
+ if (!cfg.accessToken) {
48
+ error('Not logged in. Run: bi-cli auth login');
49
+ process.exit(1);
50
+ }
51
+
52
+ const days = parseInt(options.days, 10);
53
+ if (isNaN(days) || days < 1 || days > 365) {
54
+ error('Days must be a number between 1 and 365');
55
+ process.exit(1);
56
+ }
57
+
58
+ const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
59
+
60
+ try {
61
+ const response = await axios.get<SalesResponse>(`${cfg.backendUrl}/api/v1/sales`, {
62
+ params: { days },
63
+ headers: {
64
+ Authorization: `Bearer ${cfg.accessToken}`,
65
+ },
66
+ });
67
+
68
+ if (!response.data.success) {
69
+ error('Failed to fetch sales data');
70
+ process.exit(1);
71
+ }
72
+
73
+ const { summary, daily } = response.data.data;
74
+
75
+ if (format === OutputFormat.JSON) {
76
+ outputJson(response.data.data);
77
+ } else {
78
+ // Pretty format with tables
79
+ console.log('\n📊 Sales Summary\n');
80
+ outputPretty({
81
+ 'Total Revenue': `¥${summary.total_revenue.toFixed(2)}`,
82
+ 'Total Orders': summary.total_orders,
83
+ 'Average Order Value': `¥${summary.avg_order_value.toFixed(2)}`,
84
+ 'Unique Customers': summary.unique_customers,
85
+ });
86
+
87
+ if (daily.length > 0) {
88
+ console.log('\n📅 Daily Breakdown\n');
89
+ outputPretty(
90
+ daily.map((d) => ({
91
+ Date: d.date,
92
+ Revenue: `¥${Number(d.total_revenue).toFixed(2)}`,
93
+ Orders: d.order_count,
94
+ AOV: `¥${Number(d.avg_order_value).toFixed(2)}`,
95
+ Customers: d.unique_customers,
96
+ })),
97
+ ['Date', 'Revenue', 'Orders', 'AOV', 'Customers']
98
+ );
99
+ }
100
+
101
+ // Show metadata
102
+ if (response.data.meta.cached) {
103
+ console.log('ℹ️ Data from cache');
104
+ } else if (response.data.meta.query_time_ms) {
105
+ console.log(`⏱️ Query time: ${response.data.meta.query_time_ms}ms`);
106
+ }
107
+ }
108
+ } catch (err: unknown) {
109
+ const axiosError = err as {
110
+ response?: { status?: number; data?: { error?: { message?: string } } };
111
+ };
112
+ const errorObj = err as Error;
113
+ if (axiosError.response?.status === 401) {
114
+ error('Authentication failed. Please login again: bi-cli auth login');
115
+ } else if (axiosError.response?.data?.error) {
116
+ error(`Error: ${axiosError.response.data.error.message}`);
117
+ } else {
118
+ error(`Failed to fetch sales data: ${errorObj.message || 'Unknown error'}`);
119
+ }
120
+ process.exit(1);
121
+ }
122
+ });
123
+
124
+ return sales;
125
+ }
@@ -0,0 +1,355 @@
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 RevenueTrendResponse {
7
+ success: boolean;
8
+ data: {
9
+ merchant_id: string;
10
+ period: { days: number; granularity: string };
11
+ statistics: {
12
+ total_revenue: number;
13
+ avg_revenue: number;
14
+ trend_direction: string;
15
+ data_points: number;
16
+ };
17
+ trend: Array<{
18
+ period: string;
19
+ revenue: number;
20
+ orders: number;
21
+ moving_avg_7: number | null;
22
+ }>;
23
+ };
24
+ }
25
+
26
+ interface HeatmapResponse {
27
+ success: boolean;
28
+ data: {
29
+ merchant_id: string;
30
+ period: { days: number };
31
+ heatmap: Array<{
32
+ day: string;
33
+ day_index: number;
34
+ hours: Array<{ hour: number; orders: number; revenue: number }>;
35
+ }>;
36
+ peak_times: Array<{ day: string; hour: string; orders: number }>;
37
+ };
38
+ }
39
+
40
+ interface SeasonalityResponse {
41
+ success: boolean;
42
+ data: {
43
+ merchant_id: string;
44
+ monthly_pattern: Array<{
45
+ month: number;
46
+ month_name: string;
47
+ revenue: number;
48
+ orders: number;
49
+ avg_order_value: number;
50
+ index: number;
51
+ }>;
52
+ insights: {
53
+ peak_months: string[];
54
+ low_months: string[];
55
+ avg_monthly_revenue: number;
56
+ };
57
+ };
58
+ }
59
+
60
+ interface ForecastResponse {
61
+ success: boolean;
62
+ data: {
63
+ merchant_id: string;
64
+ model: string;
65
+ baseline_days: number;
66
+ trend: {
67
+ direction: string;
68
+ daily_change: number;
69
+ };
70
+ forecast: Array<{
71
+ date: string;
72
+ day_of_week: string;
73
+ predicted_revenue: number;
74
+ confidence: string;
75
+ }>;
76
+ disclaimer: string;
77
+ };
78
+ }
79
+
80
+ export function createTrendsCommand(): Command {
81
+ const trends = new Command('trends').description('Trend analytics');
82
+
83
+ // trends revenue
84
+ trends
85
+ .command('revenue')
86
+ .description('Get revenue trend over time')
87
+ .option('--days <number>', 'Number of days', '30')
88
+ .option('--granularity <type>', 'Granularity: hourly, daily, weekly', 'daily')
89
+ .option('--pretty', 'Output in pretty table format')
90
+ .action(async (options) => {
91
+ const cfg = getConfig();
92
+ if (!cfg.accessToken) {
93
+ error('Not logged in. Run: bi-cli auth login');
94
+ process.exit(1);
95
+ }
96
+
97
+ const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
98
+
99
+ try {
100
+ const response = await axios.get<RevenueTrendResponse>(
101
+ `${cfg.backendUrl}/api/v1/trends/revenue`,
102
+ {
103
+ params: { days: options.days, granularity: options.granularity },
104
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
105
+ }
106
+ );
107
+
108
+ if (!response.data.success) {
109
+ error('Failed to fetch revenue trend');
110
+ process.exit(1);
111
+ }
112
+
113
+ const { statistics, trend, period } = response.data.data;
114
+
115
+ if (format === OutputFormat.JSON) {
116
+ outputJson(response.data.data);
117
+ } else {
118
+ console.log(`\n📈 Revenue Trend (Last ${period.days} days, ${period.granularity})\n`);
119
+
120
+ // Statistics
121
+ const trendIcon =
122
+ statistics.trend_direction === 'up'
123
+ ? '↑'
124
+ : statistics.trend_direction === 'down'
125
+ ? '↓'
126
+ : '→';
127
+ outputPretty({
128
+ 'Total Revenue': `¥${statistics.total_revenue.toFixed(2)}`,
129
+ 'Average Revenue': `¥${statistics.avg_revenue.toFixed(2)}`,
130
+ 'Trend Direction': `${trendIcon} ${statistics.trend_direction}`,
131
+ 'Data Points': statistics.data_points,
132
+ });
133
+
134
+ // Recent data points
135
+ console.log('\n📅 Recent Data\n');
136
+ const recentTrend = trend.slice(-10);
137
+ outputPretty(
138
+ recentTrend.map((t) => ({
139
+ Period: t.period.split('T')[0],
140
+ Revenue: `¥${t.revenue.toFixed(2)}`,
141
+ Orders: t.orders,
142
+ 'MA(7)': t.moving_avg_7 ? `¥${t.moving_avg_7.toFixed(2)}` : '-',
143
+ })),
144
+ ['Period', 'Revenue', 'Orders', 'MA(7)']
145
+ );
146
+ }
147
+ } catch (err: unknown) {
148
+ handleApiError(err);
149
+ }
150
+ });
151
+
152
+ // trends heatmap
153
+ trends
154
+ .command('heatmap')
155
+ .description('Orders heatmap by day and hour')
156
+ .option('--days <number>', 'Number of days', '30')
157
+ .option('--pretty', 'Output in pretty table format')
158
+ .action(async (options) => {
159
+ const cfg = getConfig();
160
+ if (!cfg.accessToken) {
161
+ error('Not logged in. Run: bi-cli auth login');
162
+ process.exit(1);
163
+ }
164
+
165
+ const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
166
+
167
+ try {
168
+ const response = await axios.get<HeatmapResponse>(
169
+ `${cfg.backendUrl}/api/v1/trends/orders-heatmap`,
170
+ {
171
+ params: { days: options.days },
172
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
173
+ }
174
+ );
175
+
176
+ if (!response.data.success) {
177
+ error('Failed to fetch heatmap data');
178
+ process.exit(1);
179
+ }
180
+
181
+ const { peak_times, heatmap } = response.data.data;
182
+
183
+ if (format === OutputFormat.JSON) {
184
+ outputJson(response.data.data);
185
+ } else {
186
+ console.log('\n🔥 Peak Order Times\n');
187
+ outputPretty(
188
+ peak_times.map((p, i) => ({
189
+ Rank: i + 1,
190
+ Day: p.day,
191
+ Hour: p.hour,
192
+ Orders: p.orders,
193
+ })),
194
+ ['Rank', 'Day', 'Hour', 'Orders']
195
+ );
196
+
197
+ console.log('\n📊 Daily Summary\n');
198
+ outputPretty(
199
+ heatmap.map((d) => {
200
+ const totalOrders = d.hours.reduce((sum, h) => sum + h.orders, 0);
201
+ const totalRevenue = d.hours.reduce((sum, h) => sum + h.revenue, 0);
202
+ const peakHour = d.hours.reduce(
203
+ (max, h) => (h.orders > max.orders ? h : max),
204
+ d.hours[0]
205
+ );
206
+ return {
207
+ Day: d.day,
208
+ 'Total Orders': totalOrders,
209
+ 'Total Revenue': `¥${totalRevenue.toFixed(2)}`,
210
+ 'Peak Hour': `${peakHour.hour}:00`,
211
+ };
212
+ }),
213
+ ['Day', 'Total Orders', 'Total Revenue', 'Peak Hour']
214
+ );
215
+ }
216
+ } catch (err: unknown) {
217
+ handleApiError(err);
218
+ }
219
+ });
220
+
221
+ // trends seasonality
222
+ trends
223
+ .command('seasonality')
224
+ .description('Monthly/seasonal patterns')
225
+ .option('--pretty', 'Output in pretty table format')
226
+ .action(async (options) => {
227
+ const cfg = getConfig();
228
+ if (!cfg.accessToken) {
229
+ error('Not logged in. Run: bi-cli auth login');
230
+ process.exit(1);
231
+ }
232
+
233
+ const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
234
+
235
+ try {
236
+ const response = await axios.get<SeasonalityResponse>(
237
+ `${cfg.backendUrl}/api/v1/trends/seasonality`,
238
+ {
239
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
240
+ }
241
+ );
242
+
243
+ if (!response.data.success) {
244
+ error('Failed to fetch seasonality data');
245
+ process.exit(1);
246
+ }
247
+
248
+ const { monthly_pattern, insights } = response.data.data;
249
+
250
+ if (format === OutputFormat.JSON) {
251
+ outputJson(response.data.data);
252
+ } else {
253
+ console.log('\n📅 Monthly Performance Pattern\n');
254
+ outputPretty(
255
+ monthly_pattern.map((m) => ({
256
+ Month: m.month_name.slice(0, 3),
257
+ Revenue: `¥${m.revenue.toFixed(2)}`,
258
+ Orders: m.orders,
259
+ 'Avg Order': `¥${m.avg_order_value.toFixed(2)}`,
260
+ Index: `${m.index.toFixed(0)}%`,
261
+ })),
262
+ ['Month', 'Revenue', 'Orders', 'Avg Order', 'Index']
263
+ );
264
+
265
+ console.log('\n💡 Insights\n');
266
+ outputPretty({
267
+ 'Peak Months': insights.peak_months.join(', ') || 'N/A',
268
+ 'Low Months': insights.low_months.join(', ') || 'N/A',
269
+ 'Avg Monthly Revenue': `¥${insights.avg_monthly_revenue.toFixed(2)}`,
270
+ });
271
+ }
272
+ } catch (err: unknown) {
273
+ handleApiError(err);
274
+ }
275
+ });
276
+
277
+ // trends forecast
278
+ trends
279
+ .command('forecast')
280
+ .description('Revenue forecast')
281
+ .option('--days <number>', 'Number of days to forecast', '7')
282
+ .option('--pretty', 'Output in pretty table format')
283
+ .action(async (options) => {
284
+ const cfg = getConfig();
285
+ if (!cfg.accessToken) {
286
+ error('Not logged in. Run: bi-cli auth login');
287
+ process.exit(1);
288
+ }
289
+
290
+ const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
291
+
292
+ try {
293
+ const response = await axios.get<ForecastResponse>(
294
+ `${cfg.backendUrl}/api/v1/trends/forecast`,
295
+ {
296
+ params: { forecast_days: options.days },
297
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
298
+ }
299
+ );
300
+
301
+ if (!response.data.success) {
302
+ error('Failed to generate forecast');
303
+ process.exit(1);
304
+ }
305
+
306
+ const { trend, forecast, baseline_days, disclaimer } = response.data.data;
307
+
308
+ if (format === OutputFormat.JSON) {
309
+ outputJson(response.data.data);
310
+ } else {
311
+ console.log('\n🔮 Revenue Forecast\n');
312
+
313
+ const trendIcon = trend.direction === 'up' ? '↑' : trend.direction === 'down' ? '↓' : '→';
314
+ outputPretty({
315
+ 'Trend Direction': `${trendIcon} ${trend.direction}`,
316
+ 'Daily Change': `¥${trend.daily_change.toFixed(2)}`,
317
+ 'Based on': `${baseline_days} days of data`,
318
+ });
319
+
320
+ console.log('\n📅 Predicted Revenue\n');
321
+ outputPretty(
322
+ forecast.map((f) => ({
323
+ Date: f.date,
324
+ Day: f.day_of_week.slice(0, 3),
325
+ 'Predicted Revenue': `¥${f.predicted_revenue.toFixed(2)}`,
326
+ Confidence: f.confidence,
327
+ })),
328
+ ['Date', 'Day', 'Predicted Revenue', 'Confidence']
329
+ );
330
+
331
+ info(disclaimer);
332
+ }
333
+ } catch (err: unknown) {
334
+ handleApiError(err);
335
+ }
336
+ });
337
+
338
+ return trends;
339
+ }
340
+
341
+ function handleApiError(err: unknown): never {
342
+ const axiosError = err as {
343
+ response?: { status?: number; data?: { error?: { message?: string } } };
344
+ };
345
+ const errorObj = err as Error;
346
+
347
+ if (axiosError.response?.status === 401) {
348
+ error('Authentication failed. Please login again: bi-cli auth login');
349
+ } else if (axiosError.response?.data?.error) {
350
+ error(`Error: ${axiosError.response.data.error.message}`);
351
+ } else {
352
+ error(`Request failed: ${errorObj.message || 'Unknown error'}`);
353
+ }
354
+ process.exit(1);
355
+ }
@@ -0,0 +1,50 @@
1
+ import Conf from 'conf';
2
+
3
+ export interface CliConfig {
4
+ environment: 'production' | 'stage' | 'development';
5
+ authUrl: string;
6
+ backendUrl: string;
7
+ accessToken?: string;
8
+ refreshToken?: string;
9
+ }
10
+
11
+ export const config = new Conf<CliConfig>({
12
+ projectName: 'optima-bi-cli',
13
+ defaults: {
14
+ environment: 'production',
15
+ authUrl: 'https://auth.optima.chat',
16
+ backendUrl: 'https://bi-api.optima.chat',
17
+ },
18
+ encryptionKey: 'optima-bi-cli-secret-key-change-in-production',
19
+ });
20
+
21
+ /**
22
+ * Get CLI configuration.
23
+ *
24
+ * Supports environment variables for CI/development:
25
+ * - BI_CLI_TOKEN: Access token (overrides stored token)
26
+ * - BI_CLI_BACKEND_URL: Backend URL (overrides stored URL)
27
+ * - BI_CLI_AUTH_URL: Auth URL (overrides stored URL)
28
+ */
29
+ export function getConfig(): CliConfig {
30
+ return {
31
+ environment: config.get('environment'),
32
+ authUrl: process.env.BI_CLI_AUTH_URL || config.get('authUrl'),
33
+ backendUrl: process.env.BI_CLI_BACKEND_URL || config.get('backendUrl'),
34
+ accessToken: process.env.BI_CLI_TOKEN || config.get('accessToken'),
35
+ refreshToken: config.get('refreshToken'),
36
+ };
37
+ }
38
+
39
+ export function setConfig(key: keyof CliConfig, value: any): void {
40
+ // Clear the key first if it exists and value is an object
41
+ if (config.has(key) && typeof value === 'object' && value !== null) {
42
+ config.delete(key);
43
+ }
44
+ config.set(key, value);
45
+ }
46
+
47
+ export function clearAuth(): void {
48
+ config.delete('accessToken');
49
+ config.delete('refreshToken');
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { createAuthCommand } from './commands/auth.js';
5
+ import { createSalesCommand } from './commands/sales.js';
6
+ import { createProductCommand } from './commands/product.js';
7
+ import { createTrendsCommand } from './commands/trends.js';
8
+ import { createAnalyticsCommand } from './commands/analytics.js';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('bi-cli')
14
+ .description('Optima BI CLI - AI-friendly business intelligence tool')
15
+ .version('0.2.0');
16
+
17
+ // Auth commands
18
+ program.addCommand(createAuthCommand());
19
+
20
+ // Sales commands
21
+ program.addCommand(createSalesCommand());
22
+
23
+ // Product commands
24
+ program.addCommand(createProductCommand());
25
+
26
+ // Trends commands
27
+ program.addCommand(createTrendsCommand());
28
+
29
+ // Analytics commands
30
+ program.addCommand(createAnalyticsCommand());
31
+
32
+ // Config commands (placeholder)
33
+ program
34
+ .command('config')
35
+ .description('Manage configuration')
36
+ .action(() => {
37
+ console.log('Config management coming soon...');
38
+ });
39
+
40
+ // Customer commands (placeholder)
41
+ program
42
+ .command('customer')
43
+ .description('Customer analytics')
44
+ .action(() => {
45
+ console.log('Customer analytics coming soon...');
46
+ });
47
+
48
+ // Inventory commands (placeholder)
49
+ program
50
+ .command('inventory')
51
+ .description('Inventory analytics')
52
+ .action(() => {
53
+ console.log('Inventory analytics coming soon...');
54
+ });
55
+
56
+ // Platform commands (admin only - placeholder)
57
+ program
58
+ .command('platform')
59
+ .description('Platform analytics (admin only)')
60
+ .action(() => {
61
+ console.log('Platform analytics coming soon...');
62
+ });
63
+
64
+ program.parse();
@@ -0,0 +1,52 @@
1
+ import chalk from 'chalk';
2
+ import { table } from 'table';
3
+
4
+ export enum OutputFormat {
5
+ JSON = 'json',
6
+ PRETTY = 'pretty',
7
+ }
8
+
9
+ export function outputJson(data: any): void {
10
+ console.log(JSON.stringify(data, null, 2));
11
+ }
12
+
13
+ export function outputPretty(data: any, headers?: string[]): void {
14
+ if (Array.isArray(data)) {
15
+ if (data.length === 0) {
16
+ console.log(chalk.yellow('No data found.'));
17
+ return;
18
+ }
19
+
20
+ const keys = headers || Object.keys(data[0]);
21
+ const rows = [keys.map((k) => chalk.bold(k))];
22
+
23
+ data.forEach((item) => {
24
+ rows.push(keys.map((k) => String(item[k] || '')));
25
+ });
26
+
27
+ console.log(table(rows));
28
+ } else {
29
+ // Single object
30
+ const rows: string[][] = [];
31
+ Object.entries(data).forEach(([key, value]) => {
32
+ rows.push([chalk.bold(key), String(value)]);
33
+ });
34
+ console.log(table(rows));
35
+ }
36
+ }
37
+
38
+ export function success(message: string): void {
39
+ console.log(chalk.green('✓ ' + message));
40
+ }
41
+
42
+ export function error(message: string): void {
43
+ console.error(chalk.red('✗ ' + message));
44
+ }
45
+
46
+ export function info(message: string): void {
47
+ console.log(chalk.blue('ℹ ' + message));
48
+ }
49
+
50
+ export function warn(message: string): void {
51
+ console.log(chalk.yellow('⚠ ' + message));
52
+ }