lsh-framework 1.2.0 → 1.3.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 (74) hide show
  1. package/README.md +40 -3
  2. package/dist/cli.js +104 -486
  3. package/dist/commands/doctor.js +427 -0
  4. package/dist/commands/init.js +371 -0
  5. package/dist/constants/api.js +94 -0
  6. package/dist/constants/commands.js +64 -0
  7. package/dist/constants/config.js +56 -0
  8. package/dist/constants/database.js +21 -0
  9. package/dist/constants/errors.js +79 -0
  10. package/dist/constants/index.js +28 -0
  11. package/dist/constants/paths.js +28 -0
  12. package/dist/constants/ui.js +73 -0
  13. package/dist/constants/validation.js +124 -0
  14. package/dist/daemon/lshd.js +11 -32
  15. package/dist/lib/daemon-client-helper.js +7 -4
  16. package/dist/lib/daemon-client.js +9 -2
  17. package/dist/lib/format-utils.js +163 -0
  18. package/dist/lib/fuzzy-match.js +123 -0
  19. package/dist/lib/job-manager.js +2 -1
  20. package/dist/lib/platform-utils.js +211 -0
  21. package/dist/lib/secrets-manager.js +11 -1
  22. package/dist/lib/string-utils.js +128 -0
  23. package/dist/services/daemon/daemon-registrar.js +3 -2
  24. package/dist/services/secrets/secrets.js +119 -59
  25. package/package.json +10 -74
  26. package/dist/app.js +0 -33
  27. package/dist/cicd/analytics.js +0 -261
  28. package/dist/cicd/auth.js +0 -269
  29. package/dist/cicd/cache-manager.js +0 -172
  30. package/dist/cicd/data-retention.js +0 -305
  31. package/dist/cicd/performance-monitor.js +0 -224
  32. package/dist/cicd/webhook-receiver.js +0 -640
  33. package/dist/commands/api.js +0 -346
  34. package/dist/commands/theme.js +0 -261
  35. package/dist/commands/zsh-import.js +0 -240
  36. package/dist/components/App.js +0 -1
  37. package/dist/components/Divider.js +0 -29
  38. package/dist/components/REPL.js +0 -43
  39. package/dist/components/Terminal.js +0 -232
  40. package/dist/components/UserInput.js +0 -30
  41. package/dist/daemon/api-server.js +0 -316
  42. package/dist/daemon/monitoring-api.js +0 -220
  43. package/dist/lib/api-error-handler.js +0 -185
  44. package/dist/lib/associative-arrays.js +0 -285
  45. package/dist/lib/base-api-server.js +0 -290
  46. package/dist/lib/brace-expansion.js +0 -160
  47. package/dist/lib/builtin-commands.js +0 -439
  48. package/dist/lib/executors/builtin-executor.js +0 -52
  49. package/dist/lib/extended-globbing.js +0 -411
  50. package/dist/lib/extended-parameter-expansion.js +0 -227
  51. package/dist/lib/interactive-shell.js +0 -460
  52. package/dist/lib/job-builtins.js +0 -582
  53. package/dist/lib/pathname-expansion.js +0 -216
  54. package/dist/lib/script-runner.js +0 -226
  55. package/dist/lib/shell-executor.js +0 -2504
  56. package/dist/lib/shell-parser.js +0 -958
  57. package/dist/lib/shell-types.js +0 -6
  58. package/dist/lib/shell.lib.js +0 -40
  59. package/dist/lib/theme-manager.js +0 -476
  60. package/dist/lib/variable-expansion.js +0 -385
  61. package/dist/lib/zsh-compatibility.js +0 -659
  62. package/dist/lib/zsh-import-manager.js +0 -707
  63. package/dist/lib/zsh-options.js +0 -328
  64. package/dist/pipeline/job-tracker.js +0 -491
  65. package/dist/pipeline/mcli-bridge.js +0 -309
  66. package/dist/pipeline/pipeline-service.js +0 -1119
  67. package/dist/pipeline/workflow-engine.js +0 -870
  68. package/dist/services/api/api.js +0 -58
  69. package/dist/services/api/auth.js +0 -35
  70. package/dist/services/api/config.js +0 -7
  71. package/dist/services/api/file.js +0 -22
  72. package/dist/services/shell/shell.js +0 -28
  73. package/dist/services/zapier.js +0 -16
  74. package/dist/simple-api-server.js +0 -148
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "1.2.0",
4
- "description": "Encrypted secrets manager with automatic rotation, team sync, and multi-environment support. Built on a powerful shell with daemon scheduling and CI/CD integration.",
3
+ "version": "1.3.0",
4
+ "description": "Simple, cross-platform encrypted secrets manager with automatic sync and multi-environment support. Just run lsh sync and start managing your secrets.",
5
5
  "main": "dist/app.js",
6
6
  "bin": {
7
7
  "lsh": "./dist/cli.js"
@@ -16,24 +16,13 @@
16
16
  },
17
17
  "scripts": {
18
18
  "build": "tsc",
19
- "compile-ts": "tsc -b",
20
- "compile-nexe": "node build.js",
21
- "build-bin": "npm run clean; npm run compile-ts; npm run compile-nexe",
22
- "start": "node dist/app.js",
23
- "start:pipeline": "npm run compile-ts && node dist/pipeline/pipeline-service.js",
24
19
  "watch": "tsc --watch",
25
20
  "test": "node --experimental-vm-modules ./node_modules/.bin/jest",
26
21
  "test:coverage": "node --experimental-vm-modules ./node_modules/.bin/jest --coverage",
27
- "test:integration": "npm run test -- --testMatch=\"**/*.integration.test.(js|ts)\"",
28
22
  "clean": "rm -rf ./build; rm -rf ./bin; rm -rf ./dist",
29
- "electron": "npm run compile-ts && electron src/electron/main.cjs",
30
- "electron-dev": "NODE_ENV=development npm run electron",
31
- "dashboard": "npm run electron",
32
- "app": "npm run electron",
33
23
  "lint": "eslint src --ext .js,.ts,.tsx",
34
24
  "lint:fix": "eslint src --ext .js,.ts,.tsx --fix",
35
- "typecheck": "tsc --noEmit",
36
- "audit:security": "npm audit --audit-level moderate"
25
+ "typecheck": "tsc --noEmit"
37
26
  },
38
27
  "keywords": [
39
28
  "secrets-manager",
@@ -44,17 +33,11 @@
44
33
  "encryption",
45
34
  "credential-management",
46
35
  "team-sync",
47
- "secrets-rotation",
48
36
  "multi-environment",
49
37
  "devops",
50
38
  "security",
51
- "shell",
52
- "automation",
53
- "cron",
54
- "daemon",
55
- "job-scheduler",
56
- "cicd",
57
- "cli"
39
+ "cli",
40
+ "cross-platform"
58
41
  ],
59
42
  "engines": {
60
43
  "node": ">=20.18.0",
@@ -74,76 +57,29 @@
74
57
  "package.json"
75
58
  ],
76
59
  "dependencies": {
77
- "@deck.gl/core": "^8.9.33",
78
- "@inkjs/ui": "^1.0.0",
79
- "@octokit/rest": "^20.1.0",
80
60
  "@supabase/supabase-js": "^2.57.4",
81
- "@types/ink": "^2.0.3",
82
- "@types/ioredis": "^4.28.10",
83
- "@types/socket.io": "^3.0.1",
84
- "@xstate/react": "^4.1.0",
85
- "async": "^3.2.5",
86
- "async-lock": "^1.4.0",
87
- "axios": "^1.5.1",
88
- "bcrypt": "^5.1.1",
89
61
  "chalk": "^5.3.0",
90
- "cheerio": "^1.0.0-rc.12",
91
62
  "chokidar": "^3.6.0",
92
63
  "commander": "^10.0.1",
93
- "cors": "^2.8.5",
94
- "csv": "^6.3.5",
95
64
  "dotenv": "^16.4.5",
96
- "express": "^5.1.0",
97
- "fprint": "^2.0.1",
98
65
  "glob": "^10.3.12",
99
- "gradstop": "^2.2.3",
100
- "helmet": "^8.1.0",
101
- "highlightjs": "^9.16.2",
102
- "http-proxy-middleware": "^3.0.5",
103
- "ink": "^4.4.1",
104
- "ink-text-input": "^5.0.1",
105
66
  "inquirer": "^9.2.12",
106
- "ioredis": "^5.8.0",
107
- "jsonwebtoken": "^9.0.2",
108
- "lodash": "^4.17.21",
109
- "lsh-framework": "^0.8.2",
67
+ "js-yaml": "^4.1.0",
110
68
  "node-cron": "^3.0.3",
111
- "node-fetch": "^3.3.2",
112
69
  "ora": "^8.0.1",
113
- "path": "^0.12.7",
114
70
  "pg": "^8.16.3",
115
- "react": "^18.2.0",
116
- "repl": "^0.1.3",
117
- "sendgrid": "^5.2.3",
118
- "socket.io": "^4.8.1",
119
- "uuid": "^10.0.0",
120
- "xstate": "^5.9.1",
121
- "zx": "^7.2.3"
71
+ "smol-toml": "^1.3.1",
72
+ "uuid": "^10.0.0"
122
73
  },
123
74
  "devDependencies": {
124
- "@babel/preset-env": "^7.23.2",
125
- "@babel/preset-react": "^7.24.1",
126
- "@types/async-lock": "^1.4.2",
127
75
  "@types/jest": "^30.0.0",
76
+ "@types/js-yaml": "^4.0.9",
128
77
  "@types/node": "^20.12.7",
129
- "@types/react": "^18.2.73",
130
- "@types/request": "^2.48.12",
131
- "@types/supertest": "^6.0.3",
132
78
  "@typescript-eslint/eslint-plugin": "^8.44.1",
133
79
  "@typescript-eslint/parser": "^8.44.1",
134
- "babel-jest": "^29.7.0",
135
- "electron": "^38.1.2",
136
80
  "eslint": "^9.36.0",
137
- "eslint-plugin-react": "^7.37.5",
138
- "eslint-plugin-react-hooks": "^5.2.0",
139
81
  "jest": "^29.7.0",
140
- "mocha": "^10.3.0",
141
- "ncc": "^0.3.6",
142
- "nexe": "^4.0.0-rc.2",
143
- "nodemon": "^3.0.1",
144
- "supertest": "^7.1.4",
145
82
  "ts-jest": "^29.2.5",
146
- "typescript": "^5.4.5",
147
- "zapier-platform-core": "15.4.1"
83
+ "typescript": "^5.4.5"
148
84
  }
149
85
  }
package/dist/app.js DELETED
@@ -1,33 +0,0 @@
1
- import { Command } from 'commander';
2
- import { init_lib } from './services/lib/lib.js';
3
- import { init_ishell } from './services/shell/shell.js';
4
- import { init_supabase } from './services/supabase/supabase.js';
5
- import { init_daemon } from './services/daemon/daemon.js';
6
- import { init_cron } from './services/cron/cron.js';
7
- const program = new Command();
8
- program
9
- .version('0.0.0')
10
- .description('lsh | extensible cli client.')
11
- .name('lsh');
12
- init_ishell(program);
13
- init_lib(program);
14
- init_supabase(program);
15
- init_daemon(program);
16
- init_cron(program);
17
- // Show help without error when no command is provided
18
- program.configureHelp({
19
- showGlobalOptions: true,
20
- });
21
- // Set exitOverride to prevent Commander from calling process.exit
22
- program.exitOverride((err) => {
23
- // If showing help, exit cleanly
24
- if (err.code === 'commander.helpDisplayed' || err.code === 'commander.help') {
25
- process.exit(0);
26
- }
27
- throw err;
28
- });
29
- program.parse(process.argv);
30
- // If no command was provided, show help and exit cleanly
31
- if (process.argv.length <= 2) {
32
- program.help({ error: false });
33
- }
@@ -1,261 +0,0 @@
1
- import { createClient } from '@supabase/supabase-js';
2
- import Redis from 'ioredis';
3
- const SUPABASE_URL = process.env.SUPABASE_URL;
4
- const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY;
5
- const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
6
- // Future use for Supabase integration - keeping for planned features
7
- const _supabase = SUPABASE_URL && SUPABASE_ANON_KEY ?
8
- createClient(SUPABASE_URL, SUPABASE_ANON_KEY) : null;
9
- const redis = new Redis(REDIS_URL);
10
- // Calculate moving average for trend smoothing - utility function for future features
11
- function _movingAverage(data, window) {
12
- const result = [];
13
- for (let i = 0; i < data.length; i++) {
14
- const start = Math.max(0, i - window + 1);
15
- const subset = data.slice(start, i + 1);
16
- const avg = subset.reduce((a, b) => a + b, 0) / subset.length;
17
- result.push(avg);
18
- }
19
- return result;
20
- }
21
- // Detect anomalies using Z-score
22
- function detectAnomalies(data, threshold = 2.5) {
23
- const mean = data.reduce((a, b) => a + b, 0) / data.length;
24
- const variance = data.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / data.length;
25
- const stdDev = Math.sqrt(variance);
26
- return data.map((value, index) => {
27
- const zScore = Math.abs((value - mean) / stdDev);
28
- return zScore > threshold ? index : -1;
29
- }).filter(index => index !== -1);
30
- }
31
- // Linear regression for predictions
32
- function linearRegression(data) {
33
- const n = data.length;
34
- const x = Array.from({ length: n }, (_, i) => i);
35
- const sumX = x.reduce((a, b) => a + b, 0);
36
- const sumY = data.reduce((a, b) => a + b, 0);
37
- const sumXY = x.reduce((sum, xi, i) => sum + xi * data[i], 0);
38
- const sumX2 = x.reduce((sum, xi) => sum + xi * xi, 0);
39
- const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
40
- const intercept = (sumY - slope * sumX) / n;
41
- return { slope, intercept };
42
- }
43
- export async function generateTrendAnalysis(days = 30) {
44
- const trends = [];
45
- const endDate = new Date();
46
- const startDate = new Date();
47
- startDate.setDate(endDate.getDate() - days);
48
- for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
49
- const dateStr = d.toISOString().split('T')[0];
50
- const key = `metrics:${dateStr}`;
51
- const metrics = await redis.hgetall(key);
52
- const durations = await redis.lrange(`durations:${dateStr}`, 0, -1);
53
- const totalBuilds = parseInt(metrics.total_builds || '0');
54
- const successfulBuilds = parseInt(metrics.successful_builds || '0');
55
- const failedBuilds = parseInt(metrics.failed_builds || '0');
56
- const avgDuration = durations.length > 0
57
- ? durations.reduce((sum, d) => sum + parseInt(d), 0) / durations.length
58
- : 0;
59
- trends.push({
60
- date: dateStr,
61
- totalBuilds,
62
- successRate: totalBuilds > 0 ? (successfulBuilds / totalBuilds) * 100 : 0,
63
- avgDuration: avgDuration / 1000 / 60, // Convert to minutes
64
- failureRate: totalBuilds > 0 ? (failedBuilds / totalBuilds) * 100 : 0
65
- });
66
- }
67
- return trends;
68
- }
69
- export async function detectBuildAnomalies(trends) {
70
- const anomalies = [];
71
- // Extract metrics
72
- const durations = trends.map(t => t.avgDuration);
73
- const failureRates = trends.map(t => t.failureRate);
74
- const _buildCounts = trends.map(t => t.totalBuilds);
75
- // Detect duration anomalies
76
- const durationAnomalies = detectAnomalies(durations);
77
- durationAnomalies.forEach(index => {
78
- const mean = durations.reduce((a, b) => a + b, 0) / durations.length;
79
- const stdDev = Math.sqrt(durations.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / durations.length);
80
- anomalies.push({
81
- timestamp: trends[index].date,
82
- type: 'duration',
83
- severity: durations[index] > mean + 3 * stdDev ? 'critical' : 'warning',
84
- description: `Build duration significantly higher than average`,
85
- value: durations[index],
86
- expectedRange: {
87
- min: Math.max(0, mean - 2 * stdDev),
88
- max: mean + 2 * stdDev
89
- }
90
- });
91
- });
92
- // Detect failure rate anomalies
93
- const failureAnomalies = detectAnomalies(failureRates, 2);
94
- failureAnomalies.forEach(index => {
95
- if (failureRates[index] > 20) { // Only flag if failure rate > 20%
96
- anomalies.push({
97
- timestamp: trends[index].date,
98
- type: 'failure_rate',
99
- severity: failureRates[index] > 50 ? 'critical' : 'warning',
100
- description: `High failure rate detected`,
101
- value: failureRates[index],
102
- expectedRange: { min: 0, max: 20 }
103
- });
104
- }
105
- });
106
- return anomalies;
107
- }
108
- export async function generateInsights(trends) {
109
- const insights = [];
110
- if (trends.length < 7)
111
- return insights;
112
- // Compare last 7 days with previous 7 days
113
- const recentWeek = trends.slice(-7);
114
- const previousWeek = trends.slice(-14, -7);
115
- const recentAvgSuccess = recentWeek.reduce((sum, t) => sum + t.successRate, 0) / 7;
116
- const prevAvgSuccess = previousWeek.reduce((sum, t) => sum + t.successRate, 0) / 7;
117
- const successChange = recentAvgSuccess - prevAvgSuccess;
118
- if (Math.abs(successChange) > 5) {
119
- insights.push({
120
- type: successChange > 0 ? 'improvement' : 'degradation',
121
- title: `Success Rate ${successChange > 0 ? 'Improved' : 'Degraded'}`,
122
- description: `Success rate changed by ${Math.abs(successChange).toFixed(1)}% compared to previous week`,
123
- metric: 'success_rate',
124
- change: successChange,
125
- impact: Math.abs(successChange) > 15 ? 'high' : Math.abs(successChange) > 10 ? 'medium' : 'low'
126
- });
127
- }
128
- // Analyze build duration trends
129
- const recentAvgDuration = recentWeek.reduce((sum, t) => sum + t.avgDuration, 0) / 7;
130
- const prevAvgDuration = previousWeek.reduce((sum, t) => sum + t.avgDuration, 0) / 7;
131
- const durationChange = ((recentAvgDuration - prevAvgDuration) / prevAvgDuration) * 100;
132
- if (Math.abs(durationChange) > 10) {
133
- insights.push({
134
- type: durationChange < 0 ? 'improvement' : 'degradation',
135
- title: `Build Duration ${durationChange < 0 ? 'Improved' : 'Increased'}`,
136
- description: `Average build duration changed by ${Math.abs(durationChange).toFixed(1)}%`,
137
- metric: 'duration',
138
- change: durationChange,
139
- impact: Math.abs(durationChange) > 30 ? 'high' : Math.abs(durationChange) > 20 ? 'medium' : 'low'
140
- });
141
- }
142
- // Identify patterns
143
- const dailyBuilds = trends.map(t => t.totalBuilds);
144
- const weekdays = trends.map(t => new Date(t.date).getDay());
145
- const weekdayAvg = Array(7).fill(0).map((_, day) => {
146
- const dayBuilds = dailyBuilds.filter((_, i) => weekdays[i] === day);
147
- return dayBuilds.reduce((a, b) => a + b, 0) / dayBuilds.length;
148
- });
149
- const peakDay = weekdayAvg.indexOf(Math.max(...weekdayAvg));
150
- const lowDay = weekdayAvg.indexOf(Math.min(...weekdayAvg));
151
- const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
152
- insights.push({
153
- type: 'pattern',
154
- title: 'Weekly Build Pattern Detected',
155
- description: `Most builds occur on ${dayNames[peakDay]}, least on ${dayNames[lowDay]}`,
156
- metric: 'frequency',
157
- change: 0,
158
- impact: 'low'
159
- });
160
- return insights;
161
- }
162
- export async function predictNextPeriod(trends) {
163
- const predictions = [];
164
- if (trends.length < 14)
165
- return predictions;
166
- // Predict success rate
167
- const successRates = trends.map(t => t.successRate);
168
- const successRegression = linearRegression(successRates.slice(-14));
169
- const predictedSuccess = successRegression.slope * successRates.length + successRegression.intercept;
170
- predictions.push({
171
- metric: 'success_rate',
172
- nextPeriod: 'next_7_days',
173
- predictedValue: Math.max(0, Math.min(100, predictedSuccess)),
174
- confidence: 0.75,
175
- trend: successRegression.slope > 1 ? 'improving' :
176
- successRegression.slope < -1 ? 'degrading' : 'stable'
177
- });
178
- // Predict build volume
179
- const buildCounts = trends.map(t => t.totalBuilds);
180
- const volumeRegression = linearRegression(buildCounts.slice(-14));
181
- const predictedVolume = volumeRegression.slope * buildCounts.length + volumeRegression.intercept;
182
- predictions.push({
183
- metric: 'build_volume',
184
- nextPeriod: 'next_day',
185
- predictedValue: Math.max(0, Math.round(predictedVolume)),
186
- confidence: 0.7,
187
- trend: volumeRegression.slope > 5 ? 'improving' :
188
- volumeRegression.slope < -5 ? 'degrading' : 'stable'
189
- });
190
- return predictions;
191
- }
192
- export async function calculateCostAnalysis(trends) {
193
- // Estimate costs based on build minutes (GitHub Actions pricing model)
194
- const COST_PER_MINUTE = 0.008; // $0.008 per minute for Linux runners
195
- const totalMinutes = trends.reduce((sum, t) => sum + (t.totalBuilds * t.avgDuration), 0);
196
- const totalCost = totalMinutes * COST_PER_MINUTE;
197
- const totalBuilds = trends.reduce((sum, t) => sum + t.totalBuilds, 0);
198
- const savingsOpportunities = [];
199
- // Identify savings opportunities
200
- const avgDuration = totalMinutes / totalBuilds;
201
- if (avgDuration > 10) {
202
- savingsOpportunities.push('Consider optimizing long-running builds (>10 minutes average)');
203
- }
204
- const avgFailureRate = trends.reduce((sum, t) => sum + t.failureRate, 0) / trends.length;
205
- if (avgFailureRate > 15) {
206
- const wastedCost = (totalCost * avgFailureRate / 100);
207
- savingsOpportunities.push(`Reduce failure rate to save ~$${wastedCost.toFixed(2)}/month`);
208
- }
209
- // Check for off-peak opportunities
210
- const peakHourBuilds = trends.filter(t => {
211
- const hour = new Date(t.date).getHours();
212
- return hour >= 9 && hour <= 17;
213
- });
214
- if (peakHourBuilds.length > trends.length * 0.7) {
215
- savingsOpportunities.push('Schedule non-critical builds during off-peak hours');
216
- }
217
- return {
218
- totalCost,
219
- costPerBuild: totalBuilds > 0 ? totalCost / totalBuilds : 0,
220
- costByPlatform: {
221
- github: totalCost * 0.6, // Estimate based on usage
222
- gitlab: totalCost * 0.25,
223
- jenkins: totalCost * 0.15
224
- },
225
- savingsOpportunities
226
- };
227
- }
228
- export async function generateAnalyticsReport(period = 'weekly') {
229
- const days = period === 'daily' ? 1 : period === 'weekly' ? 7 : 30;
230
- const trends = await generateTrendAnalysis(days);
231
- const [insights, anomalies, predictions, costAnalysis] = await Promise.all([
232
- generateInsights(trends),
233
- detectBuildAnomalies(trends),
234
- predictNextPeriod(trends),
235
- calculateCostAnalysis(trends)
236
- ]);
237
- return {
238
- period,
239
- startDate: trends[0]?.date || new Date().toISOString(),
240
- endDate: trends[trends.length - 1]?.date || new Date().toISOString(),
241
- trends,
242
- insights,
243
- anomalies,
244
- predictions,
245
- costAnalysis
246
- };
247
- }
248
- // Export functions for bottleneck detection
249
- export async function detectBottlenecks() {
250
- // Analyze stage durations to find slowest parts
251
- const stageData = await redis.hgetall('stage_durations');
252
- const bottlenecks = Object.entries(stageData)
253
- .map(([stage, duration]) => ({
254
- stage,
255
- avgDuration: parseInt(duration),
256
- impact: 'high'
257
- }))
258
- .sort((a, b) => b.avgDuration - a.avgDuration)
259
- .slice(0, 5);
260
- return bottlenecks;
261
- }