@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.
- package/dist/commands/analytics.d.ts +3 -0
- package/dist/commands/analytics.d.ts.map +1 -0
- package/dist/commands/analytics.js +228 -0
- package/dist/commands/analytics.js.map +1 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +214 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/product.d.ts +3 -0
- package/dist/commands/product.d.ts.map +1 -0
- package/dist/commands/product.js +199 -0
- package/dist/commands/product.js.map +1 -0
- package/dist/commands/sales.d.ts +3 -0
- package/dist/commands/sales.d.ts.map +1 -0
- package/dist/commands/sales.js +85 -0
- package/dist/commands/sales.js.map +1 -0
- package/dist/commands/trends.d.ts +3 -0
- package/dist/commands/trends.d.ts.map +1 -0
- package/dist/commands/trends.js +224 -0
- package/dist/commands/trends.js.map +1 -0
- package/dist/config/index.d.ts +21 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +39 -0
- package/dist/config/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/output.d.ts +11 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +45 -0
- package/dist/utils/output.js.map +1 -0
- package/package.json +39 -0
- package/src/commands/analytics.ts +352 -0
- package/src/commands/auth.ts +277 -0
- package/src/commands/product.ts +327 -0
- package/src/commands/sales.ts +125 -0
- package/src/commands/trends.ts +355 -0
- package/src/config/index.ts +50 -0
- package/src/index.ts +64 -0
- package/src/utils/output.ts +52 -0
- package/test-auth.js +63 -0
- package/test-sales.js +45 -0
- package/tsconfig.json +13 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { createAuthCommand } from './commands/auth.js';
|
|
4
|
+
import { createSalesCommand } from './commands/sales.js';
|
|
5
|
+
import { createProductCommand } from './commands/product.js';
|
|
6
|
+
import { createTrendsCommand } from './commands/trends.js';
|
|
7
|
+
import { createAnalyticsCommand } from './commands/analytics.js';
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program
|
|
10
|
+
.name('bi-cli')
|
|
11
|
+
.description('Optima BI CLI - AI-friendly business intelligence tool')
|
|
12
|
+
.version('0.2.0');
|
|
13
|
+
// Auth commands
|
|
14
|
+
program.addCommand(createAuthCommand());
|
|
15
|
+
// Sales commands
|
|
16
|
+
program.addCommand(createSalesCommand());
|
|
17
|
+
// Product commands
|
|
18
|
+
program.addCommand(createProductCommand());
|
|
19
|
+
// Trends commands
|
|
20
|
+
program.addCommand(createTrendsCommand());
|
|
21
|
+
// Analytics commands
|
|
22
|
+
program.addCommand(createAnalyticsCommand());
|
|
23
|
+
// Config commands (placeholder)
|
|
24
|
+
program
|
|
25
|
+
.command('config')
|
|
26
|
+
.description('Manage configuration')
|
|
27
|
+
.action(() => {
|
|
28
|
+
console.log('Config management coming soon...');
|
|
29
|
+
});
|
|
30
|
+
// Customer commands (placeholder)
|
|
31
|
+
program
|
|
32
|
+
.command('customer')
|
|
33
|
+
.description('Customer analytics')
|
|
34
|
+
.action(() => {
|
|
35
|
+
console.log('Customer analytics coming soon...');
|
|
36
|
+
});
|
|
37
|
+
// Inventory commands (placeholder)
|
|
38
|
+
program
|
|
39
|
+
.command('inventory')
|
|
40
|
+
.description('Inventory analytics')
|
|
41
|
+
.action(() => {
|
|
42
|
+
console.log('Inventory analytics coming soon...');
|
|
43
|
+
});
|
|
44
|
+
// Platform commands (admin only - placeholder)
|
|
45
|
+
program
|
|
46
|
+
.command('platform')
|
|
47
|
+
.description('Platform analytics (admin only)')
|
|
48
|
+
.action(() => {
|
|
49
|
+
console.log('Platform analytics coming soon...');
|
|
50
|
+
});
|
|
51
|
+
program.parse();
|
|
52
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,QAAQ,CAAC;KACd,WAAW,CAAC,wDAAwD,CAAC;KACrE,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,gBAAgB;AAChB,OAAO,CAAC,UAAU,CAAC,iBAAiB,EAAE,CAAC,CAAC;AAExC,iBAAiB;AACjB,OAAO,CAAC,UAAU,CAAC,kBAAkB,EAAE,CAAC,CAAC;AAEzC,mBAAmB;AACnB,OAAO,CAAC,UAAU,CAAC,oBAAoB,EAAE,CAAC,CAAC;AAE3C,kBAAkB;AAClB,OAAO,CAAC,UAAU,CAAC,mBAAmB,EAAE,CAAC,CAAC;AAE1C,qBAAqB;AACrB,OAAO,CAAC,UAAU,CAAC,sBAAsB,EAAE,CAAC,CAAC;AAE7C,gCAAgC;AAChC,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,sBAAsB,CAAC;KACnC,MAAM,CAAC,GAAG,EAAE;IACX,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;AAClD,CAAC,CAAC,CAAC;AAEL,kCAAkC;AAClC,OAAO;KACJ,OAAO,CAAC,UAAU,CAAC;KACnB,WAAW,CAAC,oBAAoB,CAAC;KACjC,MAAM,CAAC,GAAG,EAAE;IACX,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;AACnD,CAAC,CAAC,CAAC;AAEL,mCAAmC;AACnC,OAAO;KACJ,OAAO,CAAC,WAAW,CAAC;KACpB,WAAW,CAAC,qBAAqB,CAAC;KAClC,MAAM,CAAC,GAAG,EAAE;IACX,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEL,+CAA+C;AAC/C,OAAO;KACJ,OAAO,CAAC,UAAU,CAAC;KACnB,WAAW,CAAC,iCAAiC,CAAC;KAC9C,MAAM,CAAC,GAAG,EAAE;IACX,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;AACnD,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare enum OutputFormat {
|
|
2
|
+
JSON = "json",
|
|
3
|
+
PRETTY = "pretty"
|
|
4
|
+
}
|
|
5
|
+
export declare function outputJson(data: any): void;
|
|
6
|
+
export declare function outputPretty(data: any, headers?: string[]): void;
|
|
7
|
+
export declare function success(message: string): void;
|
|
8
|
+
export declare function error(message: string): void;
|
|
9
|
+
export declare function info(message: string): void;
|
|
10
|
+
export declare function warn(message: string): void;
|
|
11
|
+
//# sourceMappingURL=output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/utils/output.ts"],"names":[],"mappings":"AAGA,oBAAY,YAAY;IACtB,IAAI,SAAS;IACb,MAAM,WAAW;CAClB;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAE1C;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAuBhE;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE3C;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { table } from 'table';
|
|
3
|
+
export var OutputFormat;
|
|
4
|
+
(function (OutputFormat) {
|
|
5
|
+
OutputFormat["JSON"] = "json";
|
|
6
|
+
OutputFormat["PRETTY"] = "pretty";
|
|
7
|
+
})(OutputFormat || (OutputFormat = {}));
|
|
8
|
+
export function outputJson(data) {
|
|
9
|
+
console.log(JSON.stringify(data, null, 2));
|
|
10
|
+
}
|
|
11
|
+
export function outputPretty(data, headers) {
|
|
12
|
+
if (Array.isArray(data)) {
|
|
13
|
+
if (data.length === 0) {
|
|
14
|
+
console.log(chalk.yellow('No data found.'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const keys = headers || Object.keys(data[0]);
|
|
18
|
+
const rows = [keys.map((k) => chalk.bold(k))];
|
|
19
|
+
data.forEach((item) => {
|
|
20
|
+
rows.push(keys.map((k) => String(item[k] || '')));
|
|
21
|
+
});
|
|
22
|
+
console.log(table(rows));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// Single object
|
|
26
|
+
const rows = [];
|
|
27
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
28
|
+
rows.push([chalk.bold(key), String(value)]);
|
|
29
|
+
});
|
|
30
|
+
console.log(table(rows));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function success(message) {
|
|
34
|
+
console.log(chalk.green('✓ ' + message));
|
|
35
|
+
}
|
|
36
|
+
export function error(message) {
|
|
37
|
+
console.error(chalk.red('✗ ' + message));
|
|
38
|
+
}
|
|
39
|
+
export function info(message) {
|
|
40
|
+
console.log(chalk.blue('ℹ ' + message));
|
|
41
|
+
}
|
|
42
|
+
export function warn(message) {
|
|
43
|
+
console.log(chalk.yellow('⚠ ' + message));
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/utils/output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAE9B,MAAM,CAAN,IAAY,YAGX;AAHD,WAAY,YAAY;IACtB,6BAAa,CAAA;IACb,iCAAiB,CAAA;AACnB,CAAC,EAHW,YAAY,KAAZ,YAAY,QAGvB;AAED,MAAM,UAAU,UAAU,CAAC,IAAS;IAClC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAS,EAAE,OAAkB;IACxD,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC;YAC5C,OAAO;QACT,CAAC;QAED,MAAM,IAAI,GAAG,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAE9C,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;YACpB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3B,CAAC;SAAM,CAAC;QACN,gBAAgB;QAChB,MAAM,IAAI,GAAe,EAAE,CAAC;QAC5B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YAC5C,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3B,CAAC;AACH,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,OAAe;IACrC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,OAAe;IACnC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,OAAe;IAClC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,OAAe;IAClC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC;AAC5C,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@optima-chat/bi-cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Optima BI CLI - AI-friendly business intelligence tool",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"bi-cli": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"lint": "eslint src --ext .ts",
|
|
15
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
16
|
+
"test": "echo \"Error: no test specified\" && exit 0"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"axios": "^1.6.5",
|
|
20
|
+
"chalk": "^4.1.2",
|
|
21
|
+
"commander": "^11.1.0",
|
|
22
|
+
"conf": "^12.0.0",
|
|
23
|
+
"open": "^8.4.2",
|
|
24
|
+
"ora": "^5.4.1",
|
|
25
|
+
"table": "^6.8.1"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^18.19.50",
|
|
29
|
+
"tsx": "^4.7.0",
|
|
30
|
+
"typescript": "^5.3.3"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"bi",
|
|
34
|
+
"business-intelligence",
|
|
35
|
+
"analytics",
|
|
36
|
+
"cli",
|
|
37
|
+
"optima"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
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 CompareResponse {
|
|
7
|
+
success: boolean;
|
|
8
|
+
data: {
|
|
9
|
+
merchant_id: string;
|
|
10
|
+
period: { days: number; compare_to: string };
|
|
11
|
+
current: { revenue: number; orders: number; avg_order_value: number; customers: number };
|
|
12
|
+
previous: { revenue: number; orders: number; avg_order_value: number; customers: number };
|
|
13
|
+
changes: {
|
|
14
|
+
revenue: { absolute: number; percentage: number };
|
|
15
|
+
orders: { absolute: number; percentage: number };
|
|
16
|
+
avg_order_value: { absolute: number; percentage: number };
|
|
17
|
+
customers: { absolute: number; percentage: number };
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface GrowthResponse {
|
|
23
|
+
success: boolean;
|
|
24
|
+
data: {
|
|
25
|
+
merchant_id: string;
|
|
26
|
+
period_type: string;
|
|
27
|
+
trends: Array<{
|
|
28
|
+
period: string;
|
|
29
|
+
revenue: number;
|
|
30
|
+
orders: number;
|
|
31
|
+
avg_order_value: number;
|
|
32
|
+
customers: number;
|
|
33
|
+
revenue_growth: number | null;
|
|
34
|
+
}>;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface CohortResponse {
|
|
39
|
+
success: boolean;
|
|
40
|
+
data: {
|
|
41
|
+
merchant_id: string;
|
|
42
|
+
cohorts: Array<{
|
|
43
|
+
month: string;
|
|
44
|
+
customers: number;
|
|
45
|
+
avg_lifetime_value: number;
|
|
46
|
+
avg_orders: number;
|
|
47
|
+
total_revenue: number;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface FunnelResponse {
|
|
53
|
+
success: boolean;
|
|
54
|
+
data: {
|
|
55
|
+
merchant_id: string;
|
|
56
|
+
period: { days: number };
|
|
57
|
+
total_orders: number;
|
|
58
|
+
funnel: Array<{
|
|
59
|
+
stage: string;
|
|
60
|
+
count: number;
|
|
61
|
+
amount: number;
|
|
62
|
+
percentage: number;
|
|
63
|
+
}>;
|
|
64
|
+
dropoffs: Array<{
|
|
65
|
+
status: string;
|
|
66
|
+
count: number;
|
|
67
|
+
amount: number;
|
|
68
|
+
percentage: number;
|
|
69
|
+
}>;
|
|
70
|
+
conversion_rate: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createAnalyticsCommand(): Command {
|
|
75
|
+
const analytics = new Command('analytics').description('Advanced analytics');
|
|
76
|
+
|
|
77
|
+
// analytics compare
|
|
78
|
+
analytics
|
|
79
|
+
.command('compare')
|
|
80
|
+
.description('Compare current period with previous period')
|
|
81
|
+
.option('--days <number>', 'Number of days', '30')
|
|
82
|
+
.option(
|
|
83
|
+
'--compare-to <type>',
|
|
84
|
+
'Compare to: previous_period or previous_year',
|
|
85
|
+
'previous_period'
|
|
86
|
+
)
|
|
87
|
+
.option('--pretty', 'Output in pretty table format')
|
|
88
|
+
.action(async (options) => {
|
|
89
|
+
const cfg = getConfig();
|
|
90
|
+
if (!cfg.accessToken) {
|
|
91
|
+
error('Not logged in. Run: bi-cli auth login');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const response = await axios.get<CompareResponse>(
|
|
99
|
+
`${cfg.backendUrl}/api/v1/analytics/compare`,
|
|
100
|
+
{
|
|
101
|
+
params: { days: options.days, compare_to: options.compareTo },
|
|
102
|
+
headers: { Authorization: `Bearer ${cfg.accessToken}` },
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (!response.data.success) {
|
|
107
|
+
error('Failed to fetch comparison data');
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { current, previous, changes, period } = response.data.data;
|
|
112
|
+
|
|
113
|
+
if (format === OutputFormat.JSON) {
|
|
114
|
+
outputJson(response.data.data);
|
|
115
|
+
} else {
|
|
116
|
+
console.log(
|
|
117
|
+
`\n📊 Period Comparison (${period.days} days vs ${period.compare_to.replace('_', ' ')})\n`
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const formatChange = (pct: number | null) => {
|
|
121
|
+
if (pct === null) return '-';
|
|
122
|
+
const icon = pct > 0 ? '↑' : pct < 0 ? '↓' : '→';
|
|
123
|
+
const color = pct > 0 ? '+' : '';
|
|
124
|
+
return `${icon} ${color}${pct.toFixed(1)}%`;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const formatCurrency = (val: number | null) => {
|
|
128
|
+
if (val === null) return '-';
|
|
129
|
+
return `¥${val.toFixed(2)}`;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
outputPretty(
|
|
133
|
+
[
|
|
134
|
+
{
|
|
135
|
+
Metric: 'Revenue',
|
|
136
|
+
Current: formatCurrency(current.revenue),
|
|
137
|
+
Previous: formatCurrency(previous.revenue),
|
|
138
|
+
Change: formatChange(changes.revenue.percentage),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
Metric: 'Orders',
|
|
142
|
+
Current: current.orders,
|
|
143
|
+
Previous: previous.orders,
|
|
144
|
+
Change: formatChange(changes.orders.percentage),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
Metric: 'Avg Order Value',
|
|
148
|
+
Current: formatCurrency(current.avg_order_value),
|
|
149
|
+
Previous: formatCurrency(previous.avg_order_value),
|
|
150
|
+
Change: formatChange(changes.avg_order_value.percentage),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
Metric: 'Customers',
|
|
154
|
+
Current: current.customers,
|
|
155
|
+
Previous: previous.customers,
|
|
156
|
+
Change: formatChange(changes.customers.percentage),
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
['Metric', 'Current', 'Previous', 'Change']
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
} catch (err: unknown) {
|
|
163
|
+
handleApiError(err);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// analytics growth
|
|
168
|
+
analytics
|
|
169
|
+
.command('growth')
|
|
170
|
+
.description('Get growth trends over time')
|
|
171
|
+
.option('--period <type>', 'Period type: daily, weekly, monthly', 'daily')
|
|
172
|
+
.option('--limit <number>', 'Number of periods', '30')
|
|
173
|
+
.option('--pretty', 'Output in pretty table format')
|
|
174
|
+
.action(async (options) => {
|
|
175
|
+
const cfg = getConfig();
|
|
176
|
+
if (!cfg.accessToken) {
|
|
177
|
+
error('Not logged in. Run: bi-cli auth login');
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const response = await axios.get<GrowthResponse>(
|
|
185
|
+
`${cfg.backendUrl}/api/v1/analytics/growth`,
|
|
186
|
+
{
|
|
187
|
+
params: { period: options.period, limit: options.limit },
|
|
188
|
+
headers: { Authorization: `Bearer ${cfg.accessToken}` },
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (!response.data.success) {
|
|
193
|
+
error('Failed to fetch growth data');
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const { trends, period_type } = response.data.data;
|
|
198
|
+
|
|
199
|
+
if (format === OutputFormat.JSON) {
|
|
200
|
+
outputJson(response.data.data);
|
|
201
|
+
} else {
|
|
202
|
+
console.log(`\n📈 Growth Trends (${period_type})\n`);
|
|
203
|
+
|
|
204
|
+
const recentTrends = trends.slice(-15);
|
|
205
|
+
outputPretty(
|
|
206
|
+
recentTrends.map((t) => ({
|
|
207
|
+
Period: t.period.split('T')[0],
|
|
208
|
+
Revenue: `¥${t.revenue.toFixed(2)}`,
|
|
209
|
+
Orders: t.orders,
|
|
210
|
+
Customers: t.customers,
|
|
211
|
+
Growth: t.revenue_growth !== null ? `${t.revenue_growth.toFixed(1)}%` : '-',
|
|
212
|
+
})),
|
|
213
|
+
['Period', 'Revenue', 'Orders', 'Customers', 'Growth']
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
} catch (err: unknown) {
|
|
217
|
+
handleApiError(err);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// analytics cohort
|
|
222
|
+
analytics
|
|
223
|
+
.command('cohort')
|
|
224
|
+
.description('Customer cohort analysis')
|
|
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<CohortResponse>(
|
|
237
|
+
`${cfg.backendUrl}/api/v1/analytics/cohort`,
|
|
238
|
+
{
|
|
239
|
+
headers: { Authorization: `Bearer ${cfg.accessToken}` },
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (!response.data.success) {
|
|
244
|
+
error('Failed to fetch cohort data');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const { cohorts } = response.data.data;
|
|
249
|
+
|
|
250
|
+
if (format === OutputFormat.JSON) {
|
|
251
|
+
outputJson(response.data.data);
|
|
252
|
+
} else {
|
|
253
|
+
console.log('\n👥 Customer Cohort Analysis\n');
|
|
254
|
+
outputPretty(
|
|
255
|
+
cohorts.map((c) => ({
|
|
256
|
+
Cohort: c.month.split('T')[0].slice(0, 7),
|
|
257
|
+
Customers: c.customers,
|
|
258
|
+
'Avg LTV': `¥${c.avg_lifetime_value.toFixed(2)}`,
|
|
259
|
+
'Avg Orders': c.avg_orders.toFixed(1),
|
|
260
|
+
'Total Revenue': `¥${c.total_revenue.toFixed(2)}`,
|
|
261
|
+
})),
|
|
262
|
+
['Cohort', 'Customers', 'Avg LTV', 'Avg Orders', 'Total Revenue']
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
} catch (err: unknown) {
|
|
266
|
+
handleApiError(err);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// analytics funnel
|
|
271
|
+
analytics
|
|
272
|
+
.command('funnel')
|
|
273
|
+
.description('Order status funnel analysis')
|
|
274
|
+
.option('--days <number>', 'Number of days', '30')
|
|
275
|
+
.option('--pretty', 'Output in pretty table format')
|
|
276
|
+
.action(async (options) => {
|
|
277
|
+
const cfg = getConfig();
|
|
278
|
+
if (!cfg.accessToken) {
|
|
279
|
+
error('Not logged in. Run: bi-cli auth login');
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const response = await axios.get<FunnelResponse>(
|
|
287
|
+
`${cfg.backendUrl}/api/v1/analytics/order-funnel`,
|
|
288
|
+
{
|
|
289
|
+
params: { days: options.days },
|
|
290
|
+
headers: { Authorization: `Bearer ${cfg.accessToken}` },
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (!response.data.success) {
|
|
295
|
+
error('Failed to fetch funnel data');
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const { funnel, dropoffs, total_orders, conversion_rate } = response.data.data;
|
|
300
|
+
|
|
301
|
+
if (format === OutputFormat.JSON) {
|
|
302
|
+
outputJson(response.data.data);
|
|
303
|
+
} else {
|
|
304
|
+
console.log('\n🔄 Order Funnel\n');
|
|
305
|
+
outputPretty(
|
|
306
|
+
funnel.map((f) => ({
|
|
307
|
+
Stage: f.stage,
|
|
308
|
+
Count: f.count,
|
|
309
|
+
Amount: `¥${f.amount.toFixed(2)}`,
|
|
310
|
+
Percentage: `${f.percentage.toFixed(1)}%`,
|
|
311
|
+
})),
|
|
312
|
+
['Stage', 'Count', 'Amount', 'Percentage']
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
if (dropoffs.length > 0) {
|
|
316
|
+
console.log('\n❌ Dropoffs\n');
|
|
317
|
+
outputPretty(
|
|
318
|
+
dropoffs.map((d) => ({
|
|
319
|
+
Status: d.status,
|
|
320
|
+
Count: d.count,
|
|
321
|
+
Amount: `¥${d.amount.toFixed(2)}`,
|
|
322
|
+
Percentage: `${d.percentage.toFixed(1)}%`,
|
|
323
|
+
})),
|
|
324
|
+
['Status', 'Count', 'Amount', 'Percentage']
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
info(`Total Orders: ${total_orders} | Conversion Rate: ${conversion_rate.toFixed(1)}%`);
|
|
329
|
+
}
|
|
330
|
+
} catch (err: unknown) {
|
|
331
|
+
handleApiError(err);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return analytics;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function handleApiError(err: unknown): never {
|
|
339
|
+
const axiosError = err as {
|
|
340
|
+
response?: { status?: number; data?: { error?: { message?: string } } };
|
|
341
|
+
};
|
|
342
|
+
const errorObj = err as Error;
|
|
343
|
+
|
|
344
|
+
if (axiosError.response?.status === 401) {
|
|
345
|
+
error('Authentication failed. Please login again: bi-cli auth login');
|
|
346
|
+
} else if (axiosError.response?.data?.error) {
|
|
347
|
+
error(`Error: ${axiosError.response.data.error.message}`);
|
|
348
|
+
} else {
|
|
349
|
+
error(`Request failed: ${errorObj.message || 'Unknown error'}`);
|
|
350
|
+
}
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|