crushdataai 1.2.12 → 1.2.14

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.
@@ -114,6 +114,29 @@ print(f"Missing values:\n{df.isnull().sum()}")
114
114
 
115
115
  ---
116
116
 
117
+ ## Step 5: Generate Dashboard Output
118
+
119
+ After analysis, save for visualization:
120
+ ```python
121
+ from pathlib import Path
122
+ import json
123
+ from datetime import datetime
124
+
125
+ Path("reports/dashboards").mkdir(parents=True, exist_ok=True)
126
+
127
+ dashboard = {
128
+ "metadata": {"title": "Analysis Dashboard", "generatedAt": datetime.now().isoformat()},
129
+ "kpis": [{"id": "kpi-1", "label": "Total", "value": "$50K", "trend": "+12%", "trendDirection": "up"}],
130
+ "charts": [{"id": "chart-1", "type": "line", "title": "Trend", "data": {"labels": ["Jan","Feb"], "datasets": [{"label": "Revenue", "values": [10000,20000]}]}}]
131
+ }
132
+
133
+ with open("reports/dashboards/dashboard.json", "w") as f:
134
+ json.dump(dashboard, f, indent=2)
135
+ ```
136
+ Tell user: "Run `npx crushdataai dashboard` to view."
137
+
138
+ ---
139
+
117
140
  ## Pre-Delivery Checklist
118
141
 
119
142
  - [ ] Business question answered
@@ -170,6 +170,54 @@ FROM table;
170
170
 
171
171
  ---
172
172
 
173
+ ## Step 5: Generate Dashboard Output
174
+
175
+ **After analysis, save results for dashboard visualization:**
176
+
177
+ 1. **Search for best chart type:**
178
+ ```bash
179
+ python3 .claude/skills/data-analyst/scripts/search.py "<metric description>" --domain chart
180
+ ```
181
+
182
+ 2. **Create dashboard JSON:**
183
+ ```python
184
+ from pathlib import Path
185
+ import json
186
+ from datetime import datetime
187
+
188
+ Path("reports/dashboards").mkdir(parents=True, exist_ok=True)
189
+
190
+ dashboard = {
191
+ "metadata": {
192
+ "title": "Analysis Dashboard",
193
+ "generatedAt": datetime.now().isoformat(),
194
+ "dataRange": f"{start_date} to {end_date}"
195
+ },
196
+ "kpis": [
197
+ {"id": "kpi-1", "label": "Total Revenue", "value": "$50,000", "trend": "+12%", "trendDirection": "up"}
198
+ ],
199
+ "charts": [
200
+ {
201
+ "id": "chart-1",
202
+ "type": "line", # line, bar, pie, area, scatter, donut, table
203
+ "title": "Monthly Trend",
204
+ "data": {
205
+ "labels": ["Jan", "Feb", "Mar"],
206
+ "datasets": [{"label": "Revenue", "values": [10000, 15000, 25000]}]
207
+ }
208
+ }
209
+ ]
210
+ }
211
+
212
+ with open("reports/dashboards/dashboard.json", "w") as f:
213
+ json.dump(dashboard, f, indent=2)
214
+ ```
215
+
216
+ 3. **Tell user:**
217
+ > "Dashboard ready! Run `npx crushdataai dashboard` to view."
218
+
219
+ ---
220
+
173
221
  ## Pre-Delivery Checklist
174
222
 
175
223
  Before presenting final results:
@@ -60,6 +60,26 @@ Report findings and ask user for confirmation.
60
60
  - Compare to benchmarks
61
61
  - Present for user validation
62
62
 
63
+ ### 5. Generate Dashboard Output
64
+ After analysis, save for visualization:
65
+ ```python
66
+ from pathlib import Path
67
+ import json
68
+ from datetime import datetime
69
+
70
+ Path("reports/dashboards").mkdir(parents=True, exist_ok=True)
71
+
72
+ dashboard = {
73
+ "metadata": {"title": "Analysis", "generatedAt": datetime.now().isoformat()},
74
+ "kpis": [{"id": "kpi-1", "label": "Total", "value": "$50K", "trend": "+12%"}],
75
+ "charts": [{"id": "chart-1", "type": "line", "title": "Trend", "data": {"labels": ["Jan","Feb"], "datasets": [{"label": "Revenue", "values": [10000,20000]}]}}]
76
+ }
77
+
78
+ with open("reports/dashboards/dashboard.json", "w") as f:
79
+ json.dump(dashboard, f, indent=2)
80
+ ```
81
+ Tell user: "Run `npx crushdataai dashboard` to view."
82
+
63
83
  ---
64
84
 
65
85
  ## Quick Reference
@@ -58,3 +58,21 @@ Report and confirm before proceeding.
58
58
  - Check totals
59
59
  - Compare benchmarks
60
60
  - User validation
61
+
62
+ ### 5. Generate Dashboard Output
63
+ After analysis, save for visualization:
64
+ ```python
65
+ from pathlib import Path
66
+ import json
67
+ from datetime import datetime
68
+
69
+ Path("reports/dashboards").mkdir(parents=True, exist_ok=True)
70
+ dashboard = {
71
+ "metadata": {"title": "Analysis", "generatedAt": datetime.now().isoformat()},
72
+ "kpis": [{"id": "kpi-1", "label": "Total", "value": "$50K", "trend": "+12%"}],
73
+ "charts": [{"id": "chart-1", "type": "line", "title": "Trend", "data": {"labels": ["Jan","Feb"], "datasets": [{"label": "Revenue", "values": [10000,20000]}]}}]
74
+ }
75
+ with open("reports/dashboards/dashboard.json", "w") as f:
76
+ json.dump(dashboard, f, indent=2)
77
+ ```
78
+ Tell user: "Run `npx crushdataai dashboard` to view."
@@ -60,3 +60,21 @@ Ask: "Does this match your expectation?"
60
60
  - Compare to benchmarks
61
61
  - Document assumptions
62
62
  - Present for user confirmation
63
+
64
+ ### 5. Generate Dashboard Output
65
+ After analysis, save for visualization:
66
+ ```python
67
+ from pathlib import Path
68
+ import json
69
+ from datetime import datetime
70
+
71
+ Path("reports/dashboards").mkdir(parents=True, exist_ok=True)
72
+ dashboard = {
73
+ "metadata": {"title": "Analysis", "generatedAt": datetime.now().isoformat()},
74
+ "kpis": [{"id": "kpi-1", "label": "Total", "value": "$50K", "trend": "+12%"}],
75
+ "charts": [{"id": "chart-1", "type": "line", "title": "Trend", "data": {"labels": ["Jan","Feb"], "datasets": [{"label": "Revenue", "values": [10000,20000]}]}}]
76
+ }
77
+ with open("reports/dashboards/dashboard.json", "w") as f:
78
+ json.dump(dashboard, f, indent=2)
79
+ ```
80
+ Tell user: "Run `npx crushdataai dashboard` to view."
@@ -58,3 +58,21 @@ print(f"Shape: {df.shape}, Dates: {df['date'].min()} to {df['date'].max()}")
58
58
  - Check totals are reasonable
59
59
  - Compare to benchmarks
60
60
  - Present for user validation
61
+
62
+ ### 5. Generate Dashboard Output
63
+ After analysis, save for visualization:
64
+ ```python
65
+ from pathlib import Path
66
+ import json
67
+ from datetime import datetime
68
+
69
+ Path("reports/dashboards").mkdir(parents=True, exist_ok=True)
70
+ dashboard = {
71
+ "metadata": {"title": "Analysis", "generatedAt": datetime.now().isoformat()},
72
+ "kpis": [{"id": "kpi-1", "label": "Total", "value": "$50K", "trend": "+12%"}],
73
+ "charts": [{"id": "chart-1", "type": "line", "title": "Trend", "data": {"labels": ["Jan","Feb"], "datasets": [{"label": "Revenue", "values": [10000,20000]}]}}]
74
+ }
75
+ with open("reports/dashboards/dashboard.json", "w") as f:
76
+ json.dump(dashboard, f, indent=2)
77
+ ```
78
+ Tell user: "Run `npx crushdataai dashboard` to view."
package/dist/commands.js CHANGED
@@ -79,17 +79,25 @@ function getAssetsDir() {
79
79
  function copySharedFiles(targetDir, force) {
80
80
  const sharedSource = path.join(getAssetsDir(), '.shared', 'data-analyst');
81
81
  const sharedTarget = path.join(targetDir, SHARED_DIR);
82
- if (fs.existsSync(sharedTarget) && !force) {
83
- console.log(` ⏭️ ${SHARED_DIR} already exists (use --force to overwrite)`);
84
- return;
82
+ if (fs.existsSync(sharedTarget)) {
83
+ if (!force) {
84
+ console.log(` ⏭️ ${SHARED_DIR} already exists (use --force to overwrite)`);
85
+ return;
86
+ }
87
+ // Clean reinstall: remove existing folder first
88
+ fs.removeSync(sharedTarget);
85
89
  }
86
- fs.copySync(sharedSource, sharedTarget, { overwrite: force });
90
+ fs.copySync(sharedSource, sharedTarget);
87
91
  console.log(` ✅ Created ${SHARED_DIR}/`);
88
92
  }
89
93
  function copyAIFiles(aiType, targetDir, force) {
90
94
  const config = AI_PATHS[aiType];
91
95
  const sourceDir = path.join(getAssetsDir(), config.sourceDir);
92
96
  const targetPath = path.join(targetDir, config.dir);
97
+ // Clean reinstall: remove entire folder first when force=true
98
+ if (fs.existsSync(targetPath) && force) {
99
+ fs.removeSync(targetPath);
100
+ }
93
101
  // Ensure directory exists
94
102
  fs.ensureDirSync(targetPath);
95
103
  for (const file of config.files) {
@@ -104,14 +112,18 @@ function copyAIFiles(aiType, targetDir, force) {
104
112
  console.log(` ✅ Created ${config.dir}/${file}`);
105
113
  }
106
114
  }
107
- // For Claude, also create symlinks to shared scripts and data
115
+ // For Claude, also copy shared scripts and data
108
116
  if (aiType === 'claude') {
109
117
  const scriptsLink = path.join(targetPath, 'scripts');
110
118
  const dataLink = path.join(targetPath, 'data');
111
- const sharedScripts = path.relative(targetPath, path.join(targetDir, SHARED_DIR, 'scripts'));
112
- const sharedData = path.relative(targetPath, path.join(targetDir, SHARED_DIR, 'data'));
113
- // Note: Symlinks may require admin on Windows. Copy as fallback.
114
119
  try {
120
+ // Clean reinstall: remove existing folders first when force=true
121
+ if (force) {
122
+ if (fs.existsSync(scriptsLink))
123
+ fs.removeSync(scriptsLink);
124
+ if (fs.existsSync(dataLink))
125
+ fs.removeSync(dataLink);
126
+ }
115
127
  if (!fs.existsSync(scriptsLink)) {
116
128
  fs.copySync(path.join(targetDir, SHARED_DIR, 'scripts'), scriptsLink);
117
129
  console.log(` ✅ Copied scripts to ${config.dir}/scripts/`);
@@ -0,0 +1 @@
1
+ export declare function startDashboardServer(port: number): Promise<void>;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.startDashboardServer = startDashboardServer;
40
+ const express_1 = __importDefault(require("express"));
41
+ const path = __importStar(require("path"));
42
+ const open_1 = __importDefault(require("open"));
43
+ const dashboard_1 = __importDefault(require("./routes/dashboard"));
44
+ async function startDashboardServer(port) {
45
+ const app = (0, express_1.default)();
46
+ // Serve static files from the built dashboard UI
47
+ const uiPath = path.join(__dirname, '..', 'ui-dashboard-dist');
48
+ app.use(express_1.default.static(uiPath));
49
+ // API routes
50
+ app.use('/api', dashboard_1.default);
51
+ // SPA fallback - serve index.html for client-side routing
52
+ app.get('*', (_req, res) => {
53
+ res.sendFile(path.join(uiPath, 'index.html'));
54
+ });
55
+ return new Promise((resolve, reject) => {
56
+ const server = app.listen(port, async () => {
57
+ console.log(`\n📊 Dashboard UI running at http://localhost:${port}\n`);
58
+ console.log(' Looking for dashboards in: reports/dashboards/');
59
+ console.log(' Press Ctrl+C to stop\n');
60
+ // Open browser
61
+ try {
62
+ await (0, open_1.default)(`http://localhost:${port}`);
63
+ }
64
+ catch (err) {
65
+ // Ignore open errors
66
+ }
67
+ resolve();
68
+ });
69
+ server.on('error', (err) => {
70
+ if (err.code === 'EADDRINUSE') {
71
+ reject(new Error(`Port ${port} is already in use. Try a different port with --port`));
72
+ }
73
+ else {
74
+ reject(err);
75
+ }
76
+ });
77
+ });
78
+ }
package/dist/index.js CHANGED
@@ -37,12 +37,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
37
37
  const commander_1 = require("commander");
38
38
  const commands_1 = require("./commands");
39
39
  const server_1 = require("./server");
40
+ const dashboard_server_1 = require("./dashboard-server");
40
41
  const connections_1 = require("./connections");
41
42
  const program = new commander_1.Command();
42
43
  program
43
44
  .name('crushdataai')
44
45
  .description('CLI to install CrushData AI data analyst skill for AI coding assistants')
45
- .version('1.2.1');
46
+ .version('1.2.14');
46
47
  program
47
48
  .command('init')
48
49
  .description('Initialize CrushData AI skill in current project')
@@ -125,4 +126,19 @@ program
125
126
  const { schema } = await Promise.resolve().then(() => __importStar(require('./commands/schema')));
126
127
  await schema(connection, table);
127
128
  });
129
+ program
130
+ .command('dashboard')
131
+ .description('Open the dashboard visualization UI')
132
+ .option('-p, --port <port>', 'Server port', '3002')
133
+ .action(async (options) => {
134
+ const port = parseInt(options.port);
135
+ console.log('\n📊 Starting CrushData AI Dashboard...\n');
136
+ try {
137
+ await (0, dashboard_server_1.startDashboardServer)(port);
138
+ }
139
+ catch (error) {
140
+ console.error(`❌ Error: ${error.message}`);
141
+ process.exit(1);
142
+ }
143
+ });
128
144
  program.parse();
@@ -0,0 +1,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const express_1 = require("express");
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const router = (0, express_1.Router)();
40
+ // Get reports/dashboards directory path relative to current working directory
41
+ function getDashboardsDir() {
42
+ return path.join(process.cwd(), 'reports', 'dashboards');
43
+ }
44
+ // List all available dashboards
45
+ router.get('/dashboards', (_req, res) => {
46
+ try {
47
+ const dashboardsDir = getDashboardsDir();
48
+ // Check if directory exists
49
+ if (!fs.existsSync(dashboardsDir)) {
50
+ return res.json([]);
51
+ }
52
+ // Read all JSON files in the directory
53
+ const files = fs.readdirSync(dashboardsDir)
54
+ .filter(file => file.endsWith('.json'));
55
+ const dashboards = files.map(file => {
56
+ const filePath = path.join(dashboardsDir, file);
57
+ const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
58
+ // Use filename without extension as ID
59
+ const id = path.basename(file, '.json');
60
+ return {
61
+ id,
62
+ title: content.metadata?.title || id,
63
+ generatedAt: content.metadata?.generatedAt || '',
64
+ chartCount: content.charts?.length || 0,
65
+ kpiCount: content.kpis?.length || 0
66
+ };
67
+ });
68
+ res.json(dashboards);
69
+ }
70
+ catch (error) {
71
+ console.error('Error listing dashboards:', error);
72
+ res.status(500).json({ error: 'Failed to list dashboards' });
73
+ }
74
+ });
75
+ // Get a specific dashboard by ID
76
+ router.get('/dashboards/:id', (req, res) => {
77
+ try {
78
+ const { id } = req.params;
79
+ const dashboardsDir = getDashboardsDir();
80
+ const filePath = path.join(dashboardsDir, `${id}.json`);
81
+ if (!fs.existsSync(filePath)) {
82
+ return res.status(404).json({ error: 'Dashboard not found' });
83
+ }
84
+ const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
85
+ res.json(content);
86
+ }
87
+ catch (error) {
88
+ console.error('Error reading dashboard:', error);
89
+ res.status(500).json({ error: 'Failed to read dashboard' });
90
+ }
91
+ });
92
+ // Refresh a chart's data (placeholder for now - would re-run query)
93
+ router.post('/charts/:id/refresh', (req, res) => {
94
+ try {
95
+ const { id } = req.params;
96
+ // For now, just return a success message
97
+ // In the future, this would:
98
+ // 1. Find the chart in a dashboard
99
+ // 2. Re-run its query against the data source
100
+ // 3. Update the chart data
101
+ // 4. Save the updated dashboard
102
+ res.json({
103
+ message: 'Refresh not yet implemented',
104
+ chartId: id,
105
+ lastRefreshed: new Date().toISOString()
106
+ });
107
+ }
108
+ catch (error) {
109
+ console.error('Error refreshing chart:', error);
110
+ res.status(500).json({ error: 'Failed to refresh chart' });
111
+ }
112
+ });
113
+ exports.default = router;
@@ -0,0 +1,48 @@
1
+ export interface DashboardMetadata {
2
+ title: string;
3
+ generatedAt: string;
4
+ dataRange?: string;
5
+ recordCount?: number;
6
+ }
7
+ export interface KPI {
8
+ id: string;
9
+ label: string;
10
+ value: string;
11
+ trend?: string;
12
+ trendDirection?: 'up' | 'down' | 'neutral';
13
+ }
14
+ export interface ChartQuery {
15
+ type: 'sql' | 'api';
16
+ connection?: string;
17
+ sql?: string;
18
+ }
19
+ export interface ChartDataset {
20
+ label: string;
21
+ values: number[];
22
+ color?: string;
23
+ }
24
+ export interface ChartData {
25
+ labels: string[];
26
+ datasets: ChartDataset[];
27
+ }
28
+ export interface Chart {
29
+ id: string;
30
+ type: string;
31
+ title: string;
32
+ description?: string;
33
+ query?: ChartQuery;
34
+ data: ChartData;
35
+ lastRefreshed?: string;
36
+ }
37
+ export interface Dashboard {
38
+ metadata: DashboardMetadata;
39
+ kpis: KPI[];
40
+ charts: Chart[];
41
+ }
42
+ export interface DashboardListItem {
43
+ id: string;
44
+ title: string;
45
+ generatedAt: string;
46
+ chartCount: number;
47
+ kpiCount: number;
48
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ // Dashboard types for the API layer
3
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "crushdataai",
3
- "version": "1.2.12",
3
+ "version": "1.2.14",
4
4
  "description": "CLI to install CrushData AI data analyst skill for AI coding assistants",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "crushdataai": "./dist/index.js"
8
8
  },
9
9
  "scripts": {
10
- "build": "tsc && cd ui-react && npm install && npm run build",
10
+ "build": "tsc && cd ui-react && npm install && npm run build && cd ../ui-dashboard && npm install && npm run build",
11
11
  "dev": "ts-node src/index.ts",
12
12
  "prepublishOnly": "npm run build"
13
13
  },
@@ -47,7 +47,8 @@
47
47
  "files": [
48
48
  "dist",
49
49
  "assets",
50
- "ui"
50
+ "ui",
51
+ "ui-dashboard-dist"
51
52
  ],
52
53
  "repository": {
53
54
  "type": "git",