create-qa-architect 5.0.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/.editorconfig +12 -0
- package/.github/CLAUDE_MD_AUTOMATION.md +248 -0
- package/.github/PROGRESSIVE_QUALITY_IMPLEMENTATION.md +408 -0
- package/.github/PROGRESSIVE_QUALITY_PROPOSAL.md +443 -0
- package/.github/RELEASE_CHECKLIST.md +100 -0
- package/.github/dependabot.yml +50 -0
- package/.github/git-sync.sh +48 -0
- package/.github/workflows/claude-md-validation.yml +82 -0
- package/.github/workflows/nightly-gitleaks-verification.yml +176 -0
- package/.github/workflows/pnpm-ci.yml.example +53 -0
- package/.github/workflows/python-ci.yml.example +69 -0
- package/.github/workflows/quality-legacy.yml.backup +165 -0
- package/.github/workflows/quality-progressive.yml.example +291 -0
- package/.github/workflows/quality.yml +436 -0
- package/.github/workflows/release.yml +53 -0
- package/.nvmrc +1 -0
- package/.prettierignore +14 -0
- package/.prettierrc +9 -0
- package/.stylelintrc.json +5 -0
- package/README.md +212 -0
- package/config/.lighthouserc.js +45 -0
- package/config/.pre-commit-config.yaml +66 -0
- package/config/constants.js +128 -0
- package/config/defaults.js +124 -0
- package/config/pyproject.toml +124 -0
- package/config/quality-config.schema.json +97 -0
- package/config/quality-python.yml +89 -0
- package/config/requirements-dev.txt +15 -0
- package/create-saas-monetization.js +1465 -0
- package/eslint.config.cjs +117 -0
- package/eslint.config.ts.cjs +99 -0
- package/legal/README.md +106 -0
- package/legal/copyright.md +76 -0
- package/legal/disclaimer.md +146 -0
- package/legal/privacy-policy.html +324 -0
- package/legal/privacy-policy.md +196 -0
- package/legal/terms-of-service.md +224 -0
- package/lib/billing-dashboard.html +645 -0
- package/lib/config-validator.js +163 -0
- package/lib/dependency-monitoring-basic.js +185 -0
- package/lib/dependency-monitoring-premium.js +1490 -0
- package/lib/error-reporter.js +444 -0
- package/lib/interactive/prompt.js +128 -0
- package/lib/interactive/questions.js +146 -0
- package/lib/license-validator.js +403 -0
- package/lib/licensing.js +989 -0
- package/lib/package-utils.js +187 -0
- package/lib/project-maturity.js +516 -0
- package/lib/security-enhancements.js +340 -0
- package/lib/setup-enhancements.js +317 -0
- package/lib/smart-strategy-generator.js +344 -0
- package/lib/telemetry.js +323 -0
- package/lib/template-loader.js +252 -0
- package/lib/typescript-config-generator.js +210 -0
- package/lib/ui-helpers.js +74 -0
- package/lib/validation/base-validator.js +174 -0
- package/lib/validation/cache-manager.js +158 -0
- package/lib/validation/config-security.js +741 -0
- package/lib/validation/documentation.js +326 -0
- package/lib/validation/index.js +186 -0
- package/lib/validation/validation-factory.js +153 -0
- package/lib/validation/workflow-validation.js +172 -0
- package/lib/yaml-utils.js +120 -0
- package/marketing/beta-user-email-campaign.md +372 -0
- package/marketing/landing-page.html +721 -0
- package/package.json +165 -0
- package/setup.js +2076 -0
|
@@ -0,0 +1,1490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Premium Dependency Monitoring Library (Pro/Team/Enterprise Tiers)
|
|
3
|
+
* Framework-aware dependency grouping with intelligent batching
|
|
4
|
+
*
|
|
5
|
+
* @module dependency-monitoring-premium
|
|
6
|
+
* @requires lib/licensing.js - License tier validation
|
|
7
|
+
* @requires lib/dependency-monitoring-basic.js - Fallback for free tier
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
const { getLicenseInfo } = require('./licensing')
|
|
13
|
+
const {
|
|
14
|
+
generateBasicDependabotConfig,
|
|
15
|
+
} = require('./dependency-monitoring-basic')
|
|
16
|
+
const yaml = require('js-yaml')
|
|
17
|
+
const { DEPENDENCY_MONITORING } = require('../config/constants')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* npm Framework signature patterns for detection
|
|
21
|
+
* Maps framework names to dependency patterns that indicate their presence
|
|
22
|
+
*/
|
|
23
|
+
const NPM_FRAMEWORK_SIGNATURES = {
|
|
24
|
+
react: {
|
|
25
|
+
core: ['react', 'react-dom'],
|
|
26
|
+
routing: ['react-router', 'react-router-dom', '@tanstack/react-router'],
|
|
27
|
+
state: ['zustand', 'jotai', 'redux', '@reduxjs/toolkit'],
|
|
28
|
+
query: ['@tanstack/react-query', 'swr'],
|
|
29
|
+
forms: ['react-hook-form', 'formik'],
|
|
30
|
+
ui: [
|
|
31
|
+
'@mui/material',
|
|
32
|
+
'@chakra-ui/react',
|
|
33
|
+
'@radix-ui/react-*',
|
|
34
|
+
'@headlessui/react',
|
|
35
|
+
],
|
|
36
|
+
metaFrameworks: ['next', 'remix', 'gatsby'],
|
|
37
|
+
},
|
|
38
|
+
vue: {
|
|
39
|
+
core: ['vue'],
|
|
40
|
+
routing: ['vue-router'],
|
|
41
|
+
state: ['pinia', 'vuex'],
|
|
42
|
+
ecosystem: ['@vue/*', 'vueuse'],
|
|
43
|
+
ui: ['vuetify', 'element-plus', '@vueuse/core'],
|
|
44
|
+
metaFrameworks: ['nuxt'],
|
|
45
|
+
},
|
|
46
|
+
angular: {
|
|
47
|
+
core: ['@angular/core', '@angular/common', '@angular/platform-browser'],
|
|
48
|
+
routing: ['@angular/router'],
|
|
49
|
+
forms: ['@angular/forms'],
|
|
50
|
+
http: ['@angular/common/http'],
|
|
51
|
+
state: ['@ngrx/*', '@ngxs/*'],
|
|
52
|
+
ui: ['@angular/material', '@ng-bootstrap/ng-bootstrap'],
|
|
53
|
+
cli: ['@angular/cli', '@angular-devkit/*'],
|
|
54
|
+
},
|
|
55
|
+
svelte: {
|
|
56
|
+
core: ['svelte'],
|
|
57
|
+
metaFrameworks: ['@sveltejs/kit'],
|
|
58
|
+
},
|
|
59
|
+
testing: {
|
|
60
|
+
frameworks: [
|
|
61
|
+
'jest',
|
|
62
|
+
'vitest',
|
|
63
|
+
'@testing-library/*',
|
|
64
|
+
'playwright',
|
|
65
|
+
'@playwright/test',
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
build: {
|
|
69
|
+
tools: ['vite', 'webpack', 'turbo', 'nx', '@nx/*', 'esbuild', 'rollup'],
|
|
70
|
+
},
|
|
71
|
+
storybook: {
|
|
72
|
+
core: ['@storybook/*'],
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Python Framework signature patterns for pip ecosystem
|
|
78
|
+
*/
|
|
79
|
+
const PYTHON_FRAMEWORK_SIGNATURES = {
|
|
80
|
+
django: {
|
|
81
|
+
core: ['django'],
|
|
82
|
+
rest: ['djangorestframework', 'django-rest-framework'],
|
|
83
|
+
async: ['channels', 'django-channels'],
|
|
84
|
+
cms: ['wagtail', 'django-cms'],
|
|
85
|
+
},
|
|
86
|
+
flask: {
|
|
87
|
+
core: ['flask'],
|
|
88
|
+
extensions: ['flask-sqlalchemy', 'flask-restful', 'flask-cors'],
|
|
89
|
+
},
|
|
90
|
+
fastapi: {
|
|
91
|
+
core: ['fastapi'],
|
|
92
|
+
async: ['uvicorn', 'starlette'],
|
|
93
|
+
validation: ['pydantic'],
|
|
94
|
+
},
|
|
95
|
+
datascience: {
|
|
96
|
+
core: ['numpy', 'pandas', 'scipy'],
|
|
97
|
+
ml: ['scikit-learn', 'tensorflow', 'torch', 'pytorch'],
|
|
98
|
+
viz: ['matplotlib', 'seaborn', 'plotly'],
|
|
99
|
+
},
|
|
100
|
+
testing: {
|
|
101
|
+
frameworks: ['pytest', 'unittest2', 'nose2'],
|
|
102
|
+
helpers: ['pytest-*', 'coverage'],
|
|
103
|
+
},
|
|
104
|
+
web: {
|
|
105
|
+
servers: ['gunicorn', 'uwsgi'],
|
|
106
|
+
async: ['aiohttp', 'tornado'],
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Rust Framework signature patterns for cargo ecosystem
|
|
112
|
+
*/
|
|
113
|
+
const RUST_FRAMEWORK_SIGNATURES = {
|
|
114
|
+
actix: {
|
|
115
|
+
core: ['actix-web', 'actix-rt'],
|
|
116
|
+
middleware: ['actix-cors', 'actix-session'],
|
|
117
|
+
},
|
|
118
|
+
rocket: {
|
|
119
|
+
core: ['rocket'],
|
|
120
|
+
features: ['rocket_contrib'],
|
|
121
|
+
},
|
|
122
|
+
async: {
|
|
123
|
+
runtime: ['tokio', 'async-std'],
|
|
124
|
+
helpers: ['futures'],
|
|
125
|
+
},
|
|
126
|
+
serde: {
|
|
127
|
+
core: ['serde', 'serde_json'],
|
|
128
|
+
formats: ['serde_yaml', 'serde_derive'],
|
|
129
|
+
},
|
|
130
|
+
testing: {
|
|
131
|
+
frameworks: ['criterion', 'proptest'],
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Ruby Framework signature patterns for bundler ecosystem
|
|
137
|
+
*/
|
|
138
|
+
const RUBY_FRAMEWORK_SIGNATURES = {
|
|
139
|
+
rails: {
|
|
140
|
+
core: ['rails'],
|
|
141
|
+
database: ['activerecord', 'pg', 'mysql2'],
|
|
142
|
+
testing: ['rspec-rails', 'factory_bot_rails'],
|
|
143
|
+
frontend: ['webpacker', 'importmap-rails'],
|
|
144
|
+
},
|
|
145
|
+
sinatra: {
|
|
146
|
+
core: ['sinatra'],
|
|
147
|
+
extensions: ['sinatra-contrib'],
|
|
148
|
+
},
|
|
149
|
+
testing: {
|
|
150
|
+
frameworks: ['rspec', 'rspec-*', 'minitest'],
|
|
151
|
+
helpers: ['capybara', 'factory_bot', 'factory_bot_*'],
|
|
152
|
+
},
|
|
153
|
+
utilities: {
|
|
154
|
+
async: ['sidekiq', 'delayed_job'],
|
|
155
|
+
http: ['faraday', 'httparty'],
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Detect frameworks and libraries present in a project
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} packageJson - Parsed package.json content
|
|
163
|
+
* @returns {Object} Framework detection results
|
|
164
|
+
* @example
|
|
165
|
+
* {
|
|
166
|
+
* primary: 'react',
|
|
167
|
+
* detected: {
|
|
168
|
+
* react: { present: true, packages: ['react', 'react-dom'], version: '^18.0.0' },
|
|
169
|
+
* testing: { present: true, packages: ['jest', '@testing-library/react'] }
|
|
170
|
+
* }
|
|
171
|
+
* }
|
|
172
|
+
*/
|
|
173
|
+
function detectFrameworks(packageJson) {
|
|
174
|
+
const allDependencies = {
|
|
175
|
+
...(packageJson.dependencies || {}),
|
|
176
|
+
...(packageJson.devDependencies || {}),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const detectionResults = {
|
|
180
|
+
primary: null,
|
|
181
|
+
detected: {},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// OPTIMIZED: Build reverse lookup map ONCE (instead of nested loops)
|
|
185
|
+
// This reduces complexity from O(F×C×P×D) to O(F×C×P + D×patterns_checked)
|
|
186
|
+
const patternMap = new Map() // pattern → { framework, category, pattern }
|
|
187
|
+
|
|
188
|
+
for (const [frameworkName, categories] of Object.entries(
|
|
189
|
+
NPM_FRAMEWORK_SIGNATURES
|
|
190
|
+
)) {
|
|
191
|
+
for (const [categoryName, categoryPackages] of Object.entries(categories)) {
|
|
192
|
+
for (const pattern of categoryPackages) {
|
|
193
|
+
if (!patternMap.has(pattern)) {
|
|
194
|
+
patternMap.set(pattern, [])
|
|
195
|
+
}
|
|
196
|
+
patternMap.get(pattern).push({
|
|
197
|
+
framework: frameworkName,
|
|
198
|
+
category: categoryName,
|
|
199
|
+
pattern: pattern,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Track matched packages per framework
|
|
206
|
+
const frameworkMatches = {} // frameworkName → { packages: [], version: null }
|
|
207
|
+
|
|
208
|
+
// Single pass through dependencies (O(D) instead of O(F×C×P×D))
|
|
209
|
+
for (const [depName, depVersion] of Object.entries(allDependencies)) {
|
|
210
|
+
// Check each pattern (typically 50-100 patterns total)
|
|
211
|
+
for (const [pattern, frameworkInfos] of patternMap) {
|
|
212
|
+
if (matchesPattern(depName, pattern)) {
|
|
213
|
+
// Add to all frameworks that use this pattern
|
|
214
|
+
for (const { framework, category } of frameworkInfos) {
|
|
215
|
+
if (!frameworkMatches[framework]) {
|
|
216
|
+
frameworkMatches[framework] = { packages: [], version: null }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
frameworkMatches[framework].packages.push(depName)
|
|
220
|
+
|
|
221
|
+
// Capture version from core package
|
|
222
|
+
if (!frameworkMatches[framework].version && category === 'core') {
|
|
223
|
+
frameworkMatches[framework].version = depVersion
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Build detection results from matched frameworks
|
|
231
|
+
for (const [frameworkName, matches] of Object.entries(frameworkMatches)) {
|
|
232
|
+
if (matches.packages.length > 0) {
|
|
233
|
+
detectionResults.detected[frameworkName] = {
|
|
234
|
+
present: true,
|
|
235
|
+
packages: matches.packages,
|
|
236
|
+
version: matches.version,
|
|
237
|
+
count: matches.packages.length,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Set primary framework (first UI framework detected)
|
|
241
|
+
if (
|
|
242
|
+
!detectionResults.primary &&
|
|
243
|
+
['react', 'vue', 'angular', 'svelte'].includes(frameworkName)
|
|
244
|
+
) {
|
|
245
|
+
detectionResults.primary = frameworkName
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return detectionResults
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Regex pattern cache for performance optimization
|
|
255
|
+
*
|
|
256
|
+
* Caches compiled RegExp objects to avoid recompiling the same patterns
|
|
257
|
+
* repeatedly. Implements size-limited cache with FIFO eviction to prevent
|
|
258
|
+
* memory bloat in projects with many unique patterns.
|
|
259
|
+
*
|
|
260
|
+
* @type {Map<string, RegExp>}
|
|
261
|
+
*/
|
|
262
|
+
const patternCache = new Map()
|
|
263
|
+
const MAX_PATTERN_CACHE_SIZE = DEPENDENCY_MONITORING.MAX_PATTERN_CACHE_SIZE
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Check if a dependency name matches a pattern (supports wildcards)
|
|
267
|
+
*
|
|
268
|
+
* Uses an in-memory cache to avoid recompiling regex patterns on every
|
|
269
|
+
* invocation. For wildcard patterns (e.g., '@babel/*'), compiles the regex
|
|
270
|
+
* once and reuses it. For exact patterns, uses direct string comparison.
|
|
271
|
+
*
|
|
272
|
+
* Performance: O(1) for cached patterns, O(n) for first compilation
|
|
273
|
+
* Cache eviction: FIFO when size exceeds MAX_PATTERN_CACHE_SIZE
|
|
274
|
+
*
|
|
275
|
+
* @param {string} depName - Dependency name to check (e.g., '@babel/core')
|
|
276
|
+
* @param {string} pattern - Pattern to match (supports * wildcard, e.g., '@babel/*')
|
|
277
|
+
* @returns {boolean} True if depName matches pattern
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* matchesPattern('@babel/core', '@babel/*') // true (wildcard)
|
|
281
|
+
* matchesPattern('react', 'react') // true (exact)
|
|
282
|
+
* matchesPattern('vue', 'react') // false
|
|
283
|
+
*/
|
|
284
|
+
function matchesPattern(depName, pattern) {
|
|
285
|
+
if (pattern.includes('*')) {
|
|
286
|
+
// Check cache first
|
|
287
|
+
let regex = patternCache.get(pattern)
|
|
288
|
+
|
|
289
|
+
if (!regex) {
|
|
290
|
+
// Cache miss - compile and store
|
|
291
|
+
const regexPattern = pattern.replace(/\*/g, '.*')
|
|
292
|
+
// eslint-disable-next-line security/detect-non-literal-regexp
|
|
293
|
+
regex = new RegExp(`^${regexPattern}$`)
|
|
294
|
+
|
|
295
|
+
// Implement size-limited cache with FIFO eviction
|
|
296
|
+
if (patternCache.size >= MAX_PATTERN_CACHE_SIZE) {
|
|
297
|
+
// Remove oldest entry (first key in Map)
|
|
298
|
+
const firstKey = patternCache.keys().next().value
|
|
299
|
+
patternCache.delete(firstKey)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
patternCache.set(pattern, regex)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return regex.test(depName)
|
|
306
|
+
}
|
|
307
|
+
return depName === pattern
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Generate dependency groups for React ecosystem
|
|
312
|
+
*
|
|
313
|
+
* @param {Object} _frameworkInfo - React detection results (unused, reserved for future enhancements)
|
|
314
|
+
* @returns {Object} Dependabot groups configuration
|
|
315
|
+
*/
|
|
316
|
+
function generateReactGroups(_frameworkInfo) {
|
|
317
|
+
const groups = {}
|
|
318
|
+
|
|
319
|
+
// React core group - highest priority, most critical updates
|
|
320
|
+
groups['react-core'] = {
|
|
321
|
+
patterns: ['react', 'react-dom', 'react-router*'],
|
|
322
|
+
'update-types': ['minor', 'patch'],
|
|
323
|
+
'dependency-type': 'production',
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// React ecosystem - state management, data fetching
|
|
327
|
+
groups['react-ecosystem'] = {
|
|
328
|
+
patterns: ['@tanstack/*', 'zustand', 'jotai', 'swr', '@reduxjs/*'],
|
|
329
|
+
'update-types': ['patch'],
|
|
330
|
+
'dependency-type': 'production',
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// React UI libraries
|
|
334
|
+
groups['react-ui'] = {
|
|
335
|
+
patterns: ['@mui/*', '@chakra-ui/*', '@radix-ui/*', '@headlessui/react'],
|
|
336
|
+
'update-types': ['patch'],
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// React forms
|
|
340
|
+
groups['react-forms'] = {
|
|
341
|
+
patterns: ['react-hook-form', 'formik'],
|
|
342
|
+
'update-types': ['minor', 'patch'],
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return groups
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Generate dependency groups for Vue ecosystem
|
|
350
|
+
*
|
|
351
|
+
* @param {Object} frameworkInfo - Vue detection results
|
|
352
|
+
* @returns {Object} Dependabot groups configuration
|
|
353
|
+
*/
|
|
354
|
+
function generateVueGroups(_frameworkInfo) {
|
|
355
|
+
const groups = {}
|
|
356
|
+
|
|
357
|
+
groups['vue-core'] = {
|
|
358
|
+
patterns: ['vue', 'vue-router', 'pinia'],
|
|
359
|
+
'update-types': ['minor', 'patch'],
|
|
360
|
+
'dependency-type': 'production',
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
groups['vue-ecosystem'] = {
|
|
364
|
+
patterns: ['@vue/*', '@vueuse/*', 'vueuse'],
|
|
365
|
+
'update-types': ['patch'],
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
groups['vue-ui'] = {
|
|
369
|
+
patterns: ['vuetify', 'element-plus'],
|
|
370
|
+
'update-types': ['patch'],
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return groups
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Generate dependency groups for Angular ecosystem
|
|
378
|
+
*
|
|
379
|
+
* @param {Object} frameworkInfo - Angular detection results
|
|
380
|
+
* @returns {Object} Dependabot groups configuration
|
|
381
|
+
*/
|
|
382
|
+
function generateAngularGroups(_frameworkInfo) {
|
|
383
|
+
const groups = {}
|
|
384
|
+
|
|
385
|
+
groups['angular-core'] = {
|
|
386
|
+
patterns: ['@angular/core', '@angular/common', '@angular/platform-*'],
|
|
387
|
+
'update-types': ['minor', 'patch'],
|
|
388
|
+
'dependency-type': 'production',
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
groups['angular-ecosystem'] = {
|
|
392
|
+
patterns: ['@angular/*', '@ngrx/*', '@ngxs/*'],
|
|
393
|
+
'update-types': ['patch'],
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
groups['angular-ui'] = {
|
|
397
|
+
patterns: ['@angular/material', '@ng-bootstrap/*'],
|
|
398
|
+
'update-types': ['patch'],
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return groups
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Generate dependency groups for testing frameworks
|
|
406
|
+
*
|
|
407
|
+
* @param {Object} frameworkInfo - Testing framework detection results
|
|
408
|
+
* @returns {Object} Dependabot groups configuration
|
|
409
|
+
*/
|
|
410
|
+
function generateTestingGroups(_frameworkInfo) {
|
|
411
|
+
const groups = {}
|
|
412
|
+
|
|
413
|
+
groups['testing-frameworks'] = {
|
|
414
|
+
patterns: [
|
|
415
|
+
'jest',
|
|
416
|
+
'vitest',
|
|
417
|
+
'@testing-library/*',
|
|
418
|
+
'playwright',
|
|
419
|
+
'@playwright/*',
|
|
420
|
+
],
|
|
421
|
+
'update-types': ['minor', 'patch'],
|
|
422
|
+
'dependency-type': 'development',
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return groups
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Generate dependency groups for build tools
|
|
430
|
+
*
|
|
431
|
+
* @param {Object} frameworkInfo - Build tool detection results
|
|
432
|
+
* @returns {Object} Dependabot groups configuration
|
|
433
|
+
*/
|
|
434
|
+
function generateBuildToolGroups(_frameworkInfo) {
|
|
435
|
+
const groups = {}
|
|
436
|
+
|
|
437
|
+
groups['build-tools'] = {
|
|
438
|
+
patterns: ['vite', 'webpack', 'turbo', '@nx/*', 'esbuild', 'rollup'],
|
|
439
|
+
'update-types': ['patch'],
|
|
440
|
+
'dependency-type': 'development',
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return groups
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Generate Storybook dependency groups
|
|
448
|
+
*
|
|
449
|
+
* @param {Object} frameworkInfo - Storybook detection results
|
|
450
|
+
* @returns {Object} Dependabot groups configuration
|
|
451
|
+
*/
|
|
452
|
+
function generateStorybookGroups(_frameworkInfo) {
|
|
453
|
+
const groups = {}
|
|
454
|
+
|
|
455
|
+
groups['storybook'] = {
|
|
456
|
+
patterns: ['@storybook/*'],
|
|
457
|
+
'update-types': ['minor', 'patch'],
|
|
458
|
+
'dependency-type': 'development',
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return groups
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* ============================================================================
|
|
466
|
+
* PYTHON/PIP ECOSYSTEM SUPPORT
|
|
467
|
+
* ============================================================================
|
|
468
|
+
*/
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Check if project has Python dependencies
|
|
472
|
+
*/
|
|
473
|
+
function hasPythonProject(projectPath) {
|
|
474
|
+
return (
|
|
475
|
+
fs.existsSync(path.join(projectPath, 'requirements.txt')) ||
|
|
476
|
+
fs.existsSync(path.join(projectPath, 'Pipfile')) ||
|
|
477
|
+
fs.existsSync(path.join(projectPath, 'pyproject.toml')) ||
|
|
478
|
+
fs.existsSync(path.join(projectPath, 'setup.py'))
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Parse requirements.txt for Python dependencies
|
|
484
|
+
*
|
|
485
|
+
* Parses a Python requirements.txt file to extract dependency names and versions.
|
|
486
|
+
* Supports dotted package names (zope.interface), extras (fastapi[all]), and
|
|
487
|
+
* various version specifiers (==, >=, <, etc.).
|
|
488
|
+
*
|
|
489
|
+
* Security: Validates file size before reading to prevent memory exhaustion
|
|
490
|
+
* attacks with maliciously large requirements files.
|
|
491
|
+
*
|
|
492
|
+
* @param {string} requirementsPath - Path to requirements.txt file
|
|
493
|
+
* @returns {Object<string, string>} Map of package names to version specifiers
|
|
494
|
+
* @throws {Error} If file exceeds MAX_REQUIREMENTS_FILE_SIZE
|
|
495
|
+
*
|
|
496
|
+
* @example
|
|
497
|
+
* parsePipRequirements('./requirements.txt')
|
|
498
|
+
* // Returns: { 'flask': '==2.0.1', 'pytest': '>=7.0.0', 'requests': '*' }
|
|
499
|
+
*/
|
|
500
|
+
function parsePipRequirements(requirementsPath) {
|
|
501
|
+
// Validate file size before reading to prevent memory issues
|
|
502
|
+
const stats = fs.statSync(requirementsPath)
|
|
503
|
+
const MAX_FILE_SIZE = DEPENDENCY_MONITORING.MAX_REQUIREMENTS_FILE_SIZE
|
|
504
|
+
|
|
505
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
506
|
+
const sizeMB = (stats.size / (1024 * 1024)).toFixed(2)
|
|
507
|
+
const limitMB = (MAX_FILE_SIZE / (1024 * 1024)).toFixed(0)
|
|
508
|
+
throw new Error(
|
|
509
|
+
`requirements.txt file too large (${sizeMB} MB). Maximum allowed size is ${limitMB} MB. ` +
|
|
510
|
+
`This prevents memory exhaustion. Please split into multiple files or remove unnecessary dependencies.`
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const content = fs.readFileSync(requirementsPath, 'utf8')
|
|
515
|
+
const dependencies = {}
|
|
516
|
+
|
|
517
|
+
content.split('\n').forEach(line => {
|
|
518
|
+
// Remove inline comments (everything after #)
|
|
519
|
+
const commentIndex = line.indexOf('#')
|
|
520
|
+
if (commentIndex !== -1) {
|
|
521
|
+
line = line.substring(0, commentIndex)
|
|
522
|
+
}
|
|
523
|
+
line = line.trim()
|
|
524
|
+
|
|
525
|
+
// Skip empty lines
|
|
526
|
+
if (!line) return
|
|
527
|
+
|
|
528
|
+
// Parse: package==1.2.3 or package>=1.2.3
|
|
529
|
+
// Support dotted names (zope.interface), hyphens (pytest-cov), underscores (google_cloud)
|
|
530
|
+
// Also handle extras like fastapi[all] by capturing everything before the bracket
|
|
531
|
+
// Fixed: Replaced (.*) with ([^\s]*) to prevent catastrophic backtracking
|
|
532
|
+
// eslint-disable-next-line security/detect-unsafe-regex
|
|
533
|
+
const match = line.match(/^([\w.-]+)(\[[\w,\s-]+\])?([><=!~]+)?([^\s]*)$/)
|
|
534
|
+
if (match) {
|
|
535
|
+
const [, name, _extras, operator, version] = match
|
|
536
|
+
// Store package name without extras
|
|
537
|
+
|
|
538
|
+
dependencies[name] =
|
|
539
|
+
operator && version ? `${operator}${version.trim()}` : '*'
|
|
540
|
+
}
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
return dependencies
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Parse pyproject.toml for Python dependencies (PEP 621 + legacy formats)
|
|
548
|
+
* Supports:
|
|
549
|
+
* - PEP 621: dependencies = ["package>=1.0.0", ...]
|
|
550
|
+
* - PEP 621: [project.optional-dependencies] dev = ["package>=1.0.0"]
|
|
551
|
+
* - Legacy: package = "^1.2.3" (Poetry, setuptools)
|
|
552
|
+
*/
|
|
553
|
+
function parsePyprojectToml(pyprojectPath) {
|
|
554
|
+
const content = fs.readFileSync(pyprojectPath, 'utf8')
|
|
555
|
+
const dependencies = {}
|
|
556
|
+
|
|
557
|
+
// Parse PEP 621 list-style dependencies: dependencies = ["package>=1.0.0", ...]
|
|
558
|
+
// Match main dependencies array: dependencies = [...]
|
|
559
|
+
// Allow optional whitespace/comments after ] to handle: ] # end of deps
|
|
560
|
+
// eslint-disable-next-line security/detect-unsafe-regex
|
|
561
|
+
const mainDepPattern = /^dependencies\s*=\s*\[([\s\S]*?)\]\s*(?:#.*)?$/m
|
|
562
|
+
const mainMatch = mainDepPattern.exec(content)
|
|
563
|
+
|
|
564
|
+
if (mainMatch) {
|
|
565
|
+
const depList = mainMatch[1]
|
|
566
|
+
// Extract individual package lines: "package>=1.0.0"
|
|
567
|
+
const packagePattern = /["']([^"'\n]+)["']/g
|
|
568
|
+
let pkgMatch
|
|
569
|
+
|
|
570
|
+
while ((pkgMatch = packagePattern.exec(depList)) !== null) {
|
|
571
|
+
const depString = pkgMatch[1].trim()
|
|
572
|
+
|
|
573
|
+
// Parse: package>=1.0.0 or package[extra]>=1.0.0
|
|
574
|
+
// Support dotted names, hyphens, underscores, and extras
|
|
575
|
+
// Fixed: Replaced ($|.*) with ([^\s]*) to prevent catastrophic backtracking
|
|
576
|
+
const match = depString.match(
|
|
577
|
+
/^([\w.-]+)(\[[\w,\s-]+\])?([><=!~]+)?([^\s]*)$/ // eslint-disable-line security/detect-unsafe-regex
|
|
578
|
+
)
|
|
579
|
+
if (match) {
|
|
580
|
+
const [, name, _extras, operator, version] = match
|
|
581
|
+
|
|
582
|
+
dependencies[name] =
|
|
583
|
+
operator && version ? `${operator}${version.trim()}` : '*'
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Parse optional-dependencies sections: [project.optional-dependencies] dev = [...]
|
|
589
|
+
const optionalDepPattern =
|
|
590
|
+
/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/
|
|
591
|
+
const optionalMatch = optionalDepPattern.exec(content)
|
|
592
|
+
|
|
593
|
+
if (optionalMatch) {
|
|
594
|
+
const optionalSection = optionalMatch[1]
|
|
595
|
+
// Match each optional group: dev = [...], test-suite = [...], lint-tools = [...]
|
|
596
|
+
// Support hyphens, underscores, and dots in group names per PEP 621
|
|
597
|
+
const groupPattern = /([\w.-]+)\s*=\s*\[([\s\S]*?)\]/g
|
|
598
|
+
let groupMatch
|
|
599
|
+
|
|
600
|
+
while ((groupMatch = groupPattern.exec(optionalSection)) !== null) {
|
|
601
|
+
const depList = groupMatch[2]
|
|
602
|
+
const packagePattern = /["']([^"'\n]+)["']/g
|
|
603
|
+
let pkgMatch
|
|
604
|
+
|
|
605
|
+
while ((pkgMatch = packagePattern.exec(depList)) !== null) {
|
|
606
|
+
const depString = pkgMatch[1].trim()
|
|
607
|
+
|
|
608
|
+
const match = depString.match(
|
|
609
|
+
/^([\w.-]+)(\[[\w,\s-]+\])?([><=!~]+)?($|.*)$/ // eslint-disable-line security/detect-unsafe-regex
|
|
610
|
+
)
|
|
611
|
+
if (match) {
|
|
612
|
+
const [, name, _extras, operator, version] = match
|
|
613
|
+
|
|
614
|
+
dependencies[name] =
|
|
615
|
+
operator && version ? `${operator}${version.trim()}` : '*'
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Parse legacy key-value style: package = "^1.2.3"
|
|
622
|
+
// This handles Poetry and old setuptools formats
|
|
623
|
+
// ONLY parse within [tool.poetry.dependencies] and [project.dependencies] sections
|
|
624
|
+
const poetryDepSection = /\[tool\.poetry\.dependencies\]([\s\S]*?)(?=\n\[|$)/
|
|
625
|
+
const projectDepSection = /\[project\.dependencies\]([\s\S]*?)(?=\n\[|$)/
|
|
626
|
+
|
|
627
|
+
const sections = [poetryDepSection, projectDepSection]
|
|
628
|
+
|
|
629
|
+
for (const sectionPattern of sections) {
|
|
630
|
+
const sectionMatch = sectionPattern.exec(content)
|
|
631
|
+
if (sectionMatch) {
|
|
632
|
+
const sectionContent = sectionMatch[1]
|
|
633
|
+
const kvPattern = /([\w.-]+)\s*=\s*["']([^"']+)["']/g
|
|
634
|
+
let kvMatch
|
|
635
|
+
|
|
636
|
+
while ((kvMatch = kvPattern.exec(sectionContent)) !== null) {
|
|
637
|
+
const [, name, version] = kvMatch
|
|
638
|
+
// Skip Python version specifier
|
|
639
|
+
if (name === 'python') {
|
|
640
|
+
continue
|
|
641
|
+
}
|
|
642
|
+
// Only add if not already found in list-style dependencies
|
|
643
|
+
|
|
644
|
+
if (!dependencies[name]) {
|
|
645
|
+
dependencies[name] = version
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return dependencies
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Detect Python frameworks similar to npm framework detection
|
|
656
|
+
*/
|
|
657
|
+
function detectPythonFrameworks(projectPath) {
|
|
658
|
+
let dependencies = {}
|
|
659
|
+
|
|
660
|
+
// Try requirements.txt first
|
|
661
|
+
const reqPath = path.join(projectPath, 'requirements.txt')
|
|
662
|
+
if (fs.existsSync(reqPath)) {
|
|
663
|
+
dependencies = { ...dependencies, ...parsePipRequirements(reqPath) }
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Try pyproject.toml
|
|
667
|
+
const pyprojectPath = path.join(projectPath, 'pyproject.toml')
|
|
668
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
669
|
+
dependencies = { ...dependencies, ...parsePyprojectToml(pyprojectPath) }
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const detectionResults = {
|
|
673
|
+
primary: null,
|
|
674
|
+
detected: {},
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Use same detection logic as npm frameworks
|
|
678
|
+
for (const [frameworkName, categories] of Object.entries(
|
|
679
|
+
PYTHON_FRAMEWORK_SIGNATURES
|
|
680
|
+
)) {
|
|
681
|
+
const matchedPackages = []
|
|
682
|
+
|
|
683
|
+
for (const categoryPackages of Object.values(categories)) {
|
|
684
|
+
for (const pattern of categoryPackages) {
|
|
685
|
+
for (const [depName] of Object.entries(dependencies)) {
|
|
686
|
+
if (matchesPattern(depName, pattern)) {
|
|
687
|
+
matchedPackages.push(depName)
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (matchedPackages.length > 0) {
|
|
694
|
+
detectionResults.detected[frameworkName] = {
|
|
695
|
+
present: true,
|
|
696
|
+
packages: matchedPackages,
|
|
697
|
+
count: matchedPackages.length,
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Set primary framework
|
|
701
|
+
if (
|
|
702
|
+
!detectionResults.primary &&
|
|
703
|
+
['django', 'flask', 'fastapi'].includes(frameworkName)
|
|
704
|
+
) {
|
|
705
|
+
detectionResults.primary = frameworkName
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return detectionResults
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Generate Django dependency groups
|
|
715
|
+
*/
|
|
716
|
+
function generateDjangoGroups(_frameworkInfo) {
|
|
717
|
+
return {
|
|
718
|
+
'django-core': {
|
|
719
|
+
patterns: ['django', 'djangorestframework'],
|
|
720
|
+
'update-types': ['minor', 'patch'],
|
|
721
|
+
},
|
|
722
|
+
'django-extensions': {
|
|
723
|
+
patterns: ['django-*'],
|
|
724
|
+
'update-types': ['patch'],
|
|
725
|
+
},
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Generate FastAPI dependency groups
|
|
731
|
+
*/
|
|
732
|
+
function generateFastAPIGroups(_frameworkInfo) {
|
|
733
|
+
return {
|
|
734
|
+
'fastapi-core': {
|
|
735
|
+
patterns: ['fastapi', 'uvicorn', 'starlette', 'pydantic'],
|
|
736
|
+
'update-types': ['minor', 'patch'],
|
|
737
|
+
},
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Generate Flask dependency groups
|
|
743
|
+
*/
|
|
744
|
+
function generateFlaskGroups(_frameworkInfo) {
|
|
745
|
+
return {
|
|
746
|
+
'flask-core': {
|
|
747
|
+
patterns: ['flask', 'flask-*'],
|
|
748
|
+
'update-types': ['minor', 'patch'],
|
|
749
|
+
},
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Generate Data Science dependency groups
|
|
755
|
+
*/
|
|
756
|
+
function generateDataScienceGroups(_frameworkInfo) {
|
|
757
|
+
return {
|
|
758
|
+
'data-core': {
|
|
759
|
+
patterns: ['numpy', 'pandas', 'scipy'],
|
|
760
|
+
'update-types': ['minor', 'patch'],
|
|
761
|
+
},
|
|
762
|
+
'ml-frameworks': {
|
|
763
|
+
patterns: ['scikit-learn', 'tensorflow', 'torch', 'pytorch'],
|
|
764
|
+
'update-types': ['patch'],
|
|
765
|
+
},
|
|
766
|
+
visualization: {
|
|
767
|
+
patterns: ['matplotlib', 'seaborn', 'plotly'],
|
|
768
|
+
'update-types': ['patch'],
|
|
769
|
+
},
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Generate Python testing groups
|
|
775
|
+
*/
|
|
776
|
+
function generatePythonTestingGroups(_frameworkInfo) {
|
|
777
|
+
return {
|
|
778
|
+
'testing-frameworks': {
|
|
779
|
+
patterns: ['pytest', 'pytest-*', 'coverage'],
|
|
780
|
+
'update-types': ['minor', 'patch'],
|
|
781
|
+
},
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* ============================================================================
|
|
787
|
+
* RUST/CARGO ECOSYSTEM SUPPORT
|
|
788
|
+
* ============================================================================
|
|
789
|
+
*/
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Check if project has Rust dependencies
|
|
793
|
+
*/
|
|
794
|
+
function hasRustProject(projectPath) {
|
|
795
|
+
return fs.existsSync(path.join(projectPath, 'Cargo.toml'))
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Parse Cargo.toml for Rust dependencies (simple regex-based)
|
|
800
|
+
*/
|
|
801
|
+
function parseCargoToml(cargoPath) {
|
|
802
|
+
const content = fs.readFileSync(cargoPath, 'utf8')
|
|
803
|
+
const dependencies = {}
|
|
804
|
+
|
|
805
|
+
// Find [dependencies] section - extract until next section or end of file
|
|
806
|
+
const depsMatch = content.match(/\[dependencies\]([\s\S]*?)(?:\n\s*\[|$)/)
|
|
807
|
+
if (!depsMatch) return dependencies
|
|
808
|
+
|
|
809
|
+
const depsSection = depsMatch[1]
|
|
810
|
+
|
|
811
|
+
// Handle multi-line inline tables by joining continuation lines
|
|
812
|
+
let processedContent = depsSection
|
|
813
|
+
// Join lines that are part of inline tables (lines ending with incomplete braces)
|
|
814
|
+
processedContent = processedContent.replace(/\{[^}]*$/gm, match => {
|
|
815
|
+
// Find the closing brace
|
|
816
|
+
const startIdx = depsSection.indexOf(match)
|
|
817
|
+
const restContent = depsSection.slice(startIdx)
|
|
818
|
+
const closeBraceIdx = restContent.indexOf('}')
|
|
819
|
+
if (closeBraceIdx !== -1) {
|
|
820
|
+
return restContent.slice(0, closeBraceIdx + 1).replace(/\n/g, ' ')
|
|
821
|
+
}
|
|
822
|
+
return match
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
// Split by lines and process each line
|
|
826
|
+
const lines = processedContent.split('\n')
|
|
827
|
+
for (const line of lines) {
|
|
828
|
+
const trimmed = line.trim()
|
|
829
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
830
|
+
|
|
831
|
+
// Match simple pattern: name = "version"
|
|
832
|
+
// eslint-disable-next-line security/detect-unsafe-regex
|
|
833
|
+
const simpleMatch = trimmed.match(/^(\w+(?:-\w+)*)\s*=\s*["']([^"']+)["']/)
|
|
834
|
+
if (simpleMatch) {
|
|
835
|
+
const [, name, version] = simpleMatch
|
|
836
|
+
|
|
837
|
+
dependencies[name] = version
|
|
838
|
+
continue
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Match complex pattern: name = { version = "...", ... }
|
|
842
|
+
const complexMatch = trimmed.match(
|
|
843
|
+
// eslint-disable-next-line security/detect-unsafe-regex
|
|
844
|
+
/^(\w+(?:-\w+)*)\s*=\s*\{[^}]*version\s*=\s*["']([^"']+)["']/
|
|
845
|
+
)
|
|
846
|
+
if (complexMatch) {
|
|
847
|
+
const [, name, version] = complexMatch
|
|
848
|
+
|
|
849
|
+
dependencies[name] = version
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return dependencies
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Detect Rust frameworks
|
|
858
|
+
*/
|
|
859
|
+
function detectRustFrameworks(projectPath) {
|
|
860
|
+
const cargoPath = path.join(projectPath, 'Cargo.toml')
|
|
861
|
+
if (!fs.existsSync(cargoPath)) {
|
|
862
|
+
return { primary: null, detected: {} }
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const dependencies = parseCargoToml(cargoPath)
|
|
866
|
+
const detectionResults = {
|
|
867
|
+
primary: null,
|
|
868
|
+
detected: {},
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
for (const [frameworkName, categories] of Object.entries(
|
|
872
|
+
RUST_FRAMEWORK_SIGNATURES
|
|
873
|
+
)) {
|
|
874
|
+
const matchedPackages = []
|
|
875
|
+
|
|
876
|
+
for (const categoryPackages of Object.values(categories)) {
|
|
877
|
+
for (const pattern of categoryPackages) {
|
|
878
|
+
for (const [depName] of Object.entries(dependencies)) {
|
|
879
|
+
if (matchesPattern(depName, pattern)) {
|
|
880
|
+
matchedPackages.push(depName)
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (matchedPackages.length > 0) {
|
|
887
|
+
detectionResults.detected[frameworkName] = {
|
|
888
|
+
present: true,
|
|
889
|
+
packages: matchedPackages,
|
|
890
|
+
count: matchedPackages.length,
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (
|
|
894
|
+
!detectionResults.primary &&
|
|
895
|
+
['actix', 'rocket'].includes(frameworkName)
|
|
896
|
+
) {
|
|
897
|
+
detectionResults.primary = frameworkName
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return detectionResults
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Generate Actix dependency groups
|
|
907
|
+
*/
|
|
908
|
+
function generateActixGroups(_frameworkInfo) {
|
|
909
|
+
return {
|
|
910
|
+
'actix-core': {
|
|
911
|
+
patterns: ['actix-web', 'actix-rt'],
|
|
912
|
+
'update-types': ['minor', 'patch'],
|
|
913
|
+
},
|
|
914
|
+
'actix-ecosystem': {
|
|
915
|
+
patterns: ['actix-*'],
|
|
916
|
+
'update-types': ['patch'],
|
|
917
|
+
},
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Generate Async runtime dependency groups
|
|
923
|
+
*/
|
|
924
|
+
function generateAsyncRuntimeGroups(_frameworkInfo) {
|
|
925
|
+
return {
|
|
926
|
+
'async-runtime': {
|
|
927
|
+
patterns: ['tokio', 'async-std', 'futures'],
|
|
928
|
+
'update-types': ['patch'],
|
|
929
|
+
},
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Generate Serde dependency groups
|
|
935
|
+
*/
|
|
936
|
+
function generateSerdeGroups(_frameworkInfo) {
|
|
937
|
+
return {
|
|
938
|
+
'serde-ecosystem': {
|
|
939
|
+
patterns: ['serde', 'serde_json', 'serde_*'],
|
|
940
|
+
'update-types': ['minor', 'patch'],
|
|
941
|
+
},
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* ============================================================================
|
|
947
|
+
* RUBY/BUNDLER ECOSYSTEM SUPPORT
|
|
948
|
+
* ============================================================================
|
|
949
|
+
*/
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Check if project has Ruby dependencies
|
|
953
|
+
*/
|
|
954
|
+
function hasRubyProject(projectPath) {
|
|
955
|
+
return fs.existsSync(path.join(projectPath, 'Gemfile'))
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Parse Gemfile for Ruby dependencies
|
|
960
|
+
*/
|
|
961
|
+
function parseGemfile(gemfilePath) {
|
|
962
|
+
const content = fs.readFileSync(gemfilePath, 'utf8')
|
|
963
|
+
const dependencies = {}
|
|
964
|
+
|
|
965
|
+
// Process line by line to avoid newline issues
|
|
966
|
+
const lines = content.split('\n')
|
|
967
|
+
for (const line of lines) {
|
|
968
|
+
const trimmed = line.trim()
|
|
969
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
970
|
+
|
|
971
|
+
// Match: gem 'rails', '~> 7.0' or gem 'rails'
|
|
972
|
+
const gemMatch = trimmed.match(
|
|
973
|
+
// eslint-disable-next-line security/detect-unsafe-regex
|
|
974
|
+
/gem\s+['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]+)['"])?/
|
|
975
|
+
)
|
|
976
|
+
if (gemMatch) {
|
|
977
|
+
const [, name, version] = gemMatch
|
|
978
|
+
|
|
979
|
+
dependencies[name] = version || '*'
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
return dependencies
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Detect Ruby frameworks
|
|
988
|
+
*/
|
|
989
|
+
function detectRubyFrameworks(projectPath) {
|
|
990
|
+
const gemfilePath = path.join(projectPath, 'Gemfile')
|
|
991
|
+
if (!fs.existsSync(gemfilePath)) {
|
|
992
|
+
return { primary: null, detected: {} }
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const dependencies = parseGemfile(gemfilePath)
|
|
996
|
+
const detectionResults = {
|
|
997
|
+
primary: null,
|
|
998
|
+
detected: {},
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
for (const [frameworkName, categories] of Object.entries(
|
|
1002
|
+
RUBY_FRAMEWORK_SIGNATURES
|
|
1003
|
+
)) {
|
|
1004
|
+
const matchedPackages = []
|
|
1005
|
+
|
|
1006
|
+
for (const categoryPackages of Object.values(categories)) {
|
|
1007
|
+
for (const pattern of categoryPackages) {
|
|
1008
|
+
for (const [depName] of Object.entries(dependencies)) {
|
|
1009
|
+
if (matchesPattern(depName, pattern)) {
|
|
1010
|
+
matchedPackages.push(depName)
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (matchedPackages.length > 0) {
|
|
1017
|
+
detectionResults.detected[frameworkName] = {
|
|
1018
|
+
present: true,
|
|
1019
|
+
packages: matchedPackages,
|
|
1020
|
+
count: matchedPackages.length,
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (
|
|
1024
|
+
!detectionResults.primary &&
|
|
1025
|
+
['rails', 'sinatra'].includes(frameworkName)
|
|
1026
|
+
) {
|
|
1027
|
+
detectionResults.primary = frameworkName
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return detectionResults
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Generate Rails dependency groups
|
|
1037
|
+
*/
|
|
1038
|
+
function generateRailsGroups(_frameworkInfo) {
|
|
1039
|
+
return {
|
|
1040
|
+
'rails-core': {
|
|
1041
|
+
patterns: ['rails', 'activerecord', 'actionpack'],
|
|
1042
|
+
'update-types': ['minor', 'patch'],
|
|
1043
|
+
},
|
|
1044
|
+
'rails-ecosystem': {
|
|
1045
|
+
patterns: ['rails-*', 'active*'],
|
|
1046
|
+
'update-types': ['patch'],
|
|
1047
|
+
},
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Generate RSpec dependency groups
|
|
1053
|
+
*/
|
|
1054
|
+
function generateRSpecGroups(_frameworkInfo) {
|
|
1055
|
+
return {
|
|
1056
|
+
'testing-frameworks': {
|
|
1057
|
+
patterns: ['rspec', 'rspec-*', 'capybara', 'factory_bot'],
|
|
1058
|
+
'update-types': ['minor', 'patch'],
|
|
1059
|
+
},
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Detect all ecosystems present in project
|
|
1065
|
+
*/
|
|
1066
|
+
function detectAllEcosystems(projectPath) {
|
|
1067
|
+
const ecosystems = {}
|
|
1068
|
+
|
|
1069
|
+
// npm detection (existing)
|
|
1070
|
+
const packageJsonPath = path.join(projectPath, 'package.json')
|
|
1071
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
1072
|
+
try {
|
|
1073
|
+
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8')
|
|
1074
|
+
|
|
1075
|
+
// Validate JSON content before parsing
|
|
1076
|
+
if (!packageJsonContent || packageJsonContent.trim().length === 0) {
|
|
1077
|
+
console.error('❌ package.json is empty')
|
|
1078
|
+
console.log(
|
|
1079
|
+
'Please add valid JSON content to package.json and try again.'
|
|
1080
|
+
)
|
|
1081
|
+
process.exit(1)
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const packageJson = JSON.parse(packageJsonContent)
|
|
1085
|
+
|
|
1086
|
+
// Validate package.json structure
|
|
1087
|
+
if (typeof packageJson !== 'object' || packageJson === null) {
|
|
1088
|
+
console.error('❌ package.json must contain a valid JSON object')
|
|
1089
|
+
console.log('Please fix the package.json structure and try again.')
|
|
1090
|
+
process.exit(1)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
ecosystems.npm = detectFrameworks(packageJson)
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
console.error(`❌ Error parsing package.json: ${error.message}`)
|
|
1096
|
+
console.log('\nPlease fix the JSON syntax in package.json and try again.')
|
|
1097
|
+
console.log(
|
|
1098
|
+
'Common issues: trailing commas, missing quotes, unclosed brackets\n'
|
|
1099
|
+
)
|
|
1100
|
+
process.exit(1)
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Python detection
|
|
1105
|
+
if (hasPythonProject(projectPath)) {
|
|
1106
|
+
ecosystems.pip = detectPythonFrameworks(projectPath)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Rust detection
|
|
1110
|
+
if (hasRustProject(projectPath)) {
|
|
1111
|
+
ecosystems.cargo = detectRustFrameworks(projectPath)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Ruby detection
|
|
1115
|
+
if (hasRubyProject(projectPath)) {
|
|
1116
|
+
ecosystems.bundler = detectRubyFrameworks(projectPath)
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return ecosystems
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Generate dependency groups for npm ecosystem
|
|
1124
|
+
*/
|
|
1125
|
+
function generateNpmGroups(npmFrameworks) {
|
|
1126
|
+
let allGroups = {}
|
|
1127
|
+
|
|
1128
|
+
if (npmFrameworks.detected.react) {
|
|
1129
|
+
allGroups = {
|
|
1130
|
+
...allGroups,
|
|
1131
|
+
...generateReactGroups(npmFrameworks.detected.react),
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (npmFrameworks.detected.vue) {
|
|
1136
|
+
allGroups = {
|
|
1137
|
+
...allGroups,
|
|
1138
|
+
...generateVueGroups(npmFrameworks.detected.vue),
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
if (npmFrameworks.detected.angular) {
|
|
1143
|
+
allGroups = {
|
|
1144
|
+
...allGroups,
|
|
1145
|
+
...generateAngularGroups(npmFrameworks.detected.angular),
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
if (npmFrameworks.detected.testing) {
|
|
1150
|
+
allGroups = {
|
|
1151
|
+
...allGroups,
|
|
1152
|
+
...generateTestingGroups(npmFrameworks.detected.testing),
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if (npmFrameworks.detected.build) {
|
|
1157
|
+
allGroups = {
|
|
1158
|
+
...allGroups,
|
|
1159
|
+
...generateBuildToolGroups(npmFrameworks.detected.build),
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (npmFrameworks.detected.storybook) {
|
|
1164
|
+
allGroups = {
|
|
1165
|
+
...allGroups,
|
|
1166
|
+
...generateStorybookGroups(npmFrameworks.detected.storybook),
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return allGroups
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Generate dependency groups for pip ecosystem
|
|
1175
|
+
*/
|
|
1176
|
+
function generatePipGroups(pipFrameworks) {
|
|
1177
|
+
let allGroups = {}
|
|
1178
|
+
|
|
1179
|
+
if (pipFrameworks.detected.django) {
|
|
1180
|
+
allGroups = {
|
|
1181
|
+
...allGroups,
|
|
1182
|
+
...generateDjangoGroups(pipFrameworks.detected.django),
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
if (pipFrameworks.detected.flask) {
|
|
1187
|
+
allGroups = {
|
|
1188
|
+
...allGroups,
|
|
1189
|
+
...generateFlaskGroups(pipFrameworks.detected.flask),
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (pipFrameworks.detected.fastapi) {
|
|
1194
|
+
allGroups = {
|
|
1195
|
+
...allGroups,
|
|
1196
|
+
...generateFastAPIGroups(pipFrameworks.detected.fastapi),
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
if (pipFrameworks.detected.datascience) {
|
|
1201
|
+
allGroups = {
|
|
1202
|
+
...allGroups,
|
|
1203
|
+
...generateDataScienceGroups(pipFrameworks.detected.datascience),
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (pipFrameworks.detected.testing) {
|
|
1208
|
+
allGroups = {
|
|
1209
|
+
...allGroups,
|
|
1210
|
+
...generatePythonTestingGroups(pipFrameworks.detected.testing),
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
return allGroups
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Generate dependency groups for cargo ecosystem
|
|
1219
|
+
*/
|
|
1220
|
+
function generateCargoGroups(cargoFrameworks) {
|
|
1221
|
+
let allGroups = {}
|
|
1222
|
+
|
|
1223
|
+
if (cargoFrameworks.detected.actix) {
|
|
1224
|
+
allGroups = {
|
|
1225
|
+
...allGroups,
|
|
1226
|
+
...generateActixGroups(cargoFrameworks.detected.actix),
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
if (cargoFrameworks.detected.async) {
|
|
1231
|
+
allGroups = {
|
|
1232
|
+
...allGroups,
|
|
1233
|
+
...generateAsyncRuntimeGroups(cargoFrameworks.detected.async),
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (cargoFrameworks.detected.serde) {
|
|
1238
|
+
allGroups = {
|
|
1239
|
+
...allGroups,
|
|
1240
|
+
...generateSerdeGroups(cargoFrameworks.detected.serde),
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
return allGroups
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Generate dependency groups for bundler ecosystem
|
|
1249
|
+
*/
|
|
1250
|
+
function generateBundlerGroups(bundlerFrameworks) {
|
|
1251
|
+
let allGroups = {}
|
|
1252
|
+
|
|
1253
|
+
if (bundlerFrameworks.detected.rails) {
|
|
1254
|
+
allGroups = {
|
|
1255
|
+
...allGroups,
|
|
1256
|
+
...generateRailsGroups(bundlerFrameworks.detected.rails),
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (bundlerFrameworks.detected.testing) {
|
|
1261
|
+
allGroups = {
|
|
1262
|
+
...allGroups,
|
|
1263
|
+
...generateRSpecGroups(bundlerFrameworks.detected.testing),
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
return allGroups
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Generate premium Dependabot configuration with multi-language framework-aware grouping
|
|
1272
|
+
*
|
|
1273
|
+
* @param {Object} options - Configuration options
|
|
1274
|
+
* @param {string} options.projectPath - Path to project directory
|
|
1275
|
+
* @param {string} options.schedule - Update schedule (daily, weekly, monthly)
|
|
1276
|
+
* @param {string} options.day - Day of week for updates
|
|
1277
|
+
* @param {string} options.time - Time for updates
|
|
1278
|
+
* @returns {Object|null} Dependabot configuration object or null if not licensed
|
|
1279
|
+
*/
|
|
1280
|
+
function generatePremiumDependabotConfig(options = {}) {
|
|
1281
|
+
const license = getLicenseInfo()
|
|
1282
|
+
|
|
1283
|
+
// Premium features require Pro, Team, or Enterprise tier
|
|
1284
|
+
// FREE tier users get basic npm-only config
|
|
1285
|
+
const isPremiumTier =
|
|
1286
|
+
license.tier === 'PRO' ||
|
|
1287
|
+
license.tier === 'TEAM' ||
|
|
1288
|
+
license.tier === 'ENTERPRISE'
|
|
1289
|
+
|
|
1290
|
+
if (!isPremiumTier) {
|
|
1291
|
+
console.log(
|
|
1292
|
+
'💡 Multi-language monitoring requires Pro, Team, or Enterprise tier. Generating basic config...'
|
|
1293
|
+
)
|
|
1294
|
+
return generateBasicDependabotConfig(options)
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const {
|
|
1298
|
+
projectPath = '.',
|
|
1299
|
+
schedule = 'weekly',
|
|
1300
|
+
day = 'monday',
|
|
1301
|
+
time = '09:00',
|
|
1302
|
+
} = options
|
|
1303
|
+
|
|
1304
|
+
// Detect all ecosystems
|
|
1305
|
+
const ecosystems = detectAllEcosystems(projectPath)
|
|
1306
|
+
|
|
1307
|
+
// If no ecosystems detected, return null
|
|
1308
|
+
if (Object.keys(ecosystems).length === 0) {
|
|
1309
|
+
return null
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const updates = []
|
|
1313
|
+
|
|
1314
|
+
// npm ecosystem (if present)
|
|
1315
|
+
if (ecosystems.npm) {
|
|
1316
|
+
const npmGroups = generateNpmGroups(ecosystems.npm)
|
|
1317
|
+
updates.push({
|
|
1318
|
+
'package-ecosystem': 'npm',
|
|
1319
|
+
directory: '/',
|
|
1320
|
+
schedule: { interval: schedule, day, time },
|
|
1321
|
+
'open-pull-requests-limit': 10,
|
|
1322
|
+
labels: ['dependencies', 'npm'],
|
|
1323
|
+
'commit-message': { prefix: 'deps(npm)', include: 'scope' },
|
|
1324
|
+
...(Object.keys(npmGroups).length > 0 && { groups: npmGroups }),
|
|
1325
|
+
})
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// pip ecosystem (if present)
|
|
1329
|
+
if (ecosystems.pip) {
|
|
1330
|
+
const pipGroups = generatePipGroups(ecosystems.pip)
|
|
1331
|
+
updates.push({
|
|
1332
|
+
'package-ecosystem': 'pip',
|
|
1333
|
+
directory: '/',
|
|
1334
|
+
schedule: { interval: schedule, day, time },
|
|
1335
|
+
'open-pull-requests-limit': 10,
|
|
1336
|
+
labels: ['dependencies', 'python'],
|
|
1337
|
+
'commit-message': { prefix: 'deps(python)' },
|
|
1338
|
+
...(Object.keys(pipGroups).length > 0 && { groups: pipGroups }),
|
|
1339
|
+
})
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// cargo ecosystem (if present)
|
|
1343
|
+
if (ecosystems.cargo) {
|
|
1344
|
+
const cargoGroups = generateCargoGroups(ecosystems.cargo)
|
|
1345
|
+
updates.push({
|
|
1346
|
+
'package-ecosystem': 'cargo',
|
|
1347
|
+
directory: '/',
|
|
1348
|
+
schedule: { interval: schedule, day, time },
|
|
1349
|
+
'open-pull-requests-limit': 10,
|
|
1350
|
+
labels: ['dependencies', 'rust'],
|
|
1351
|
+
'commit-message': { prefix: 'deps(rust)' },
|
|
1352
|
+
...(Object.keys(cargoGroups).length > 0 && { groups: cargoGroups }),
|
|
1353
|
+
})
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// bundler ecosystem (if present)
|
|
1357
|
+
if (ecosystems.bundler) {
|
|
1358
|
+
const bundlerGroups = generateBundlerGroups(ecosystems.bundler)
|
|
1359
|
+
updates.push({
|
|
1360
|
+
'package-ecosystem': 'bundler',
|
|
1361
|
+
directory: '/',
|
|
1362
|
+
schedule: { interval: schedule, day, time },
|
|
1363
|
+
'open-pull-requests-limit': 10,
|
|
1364
|
+
labels: ['dependencies', 'ruby'],
|
|
1365
|
+
'commit-message': { prefix: 'deps(ruby)' },
|
|
1366
|
+
...(Object.keys(bundlerGroups).length > 0 && { groups: bundlerGroups }),
|
|
1367
|
+
})
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// GitHub Actions monitoring (always included)
|
|
1371
|
+
updates.push({
|
|
1372
|
+
'package-ecosystem': 'github-actions',
|
|
1373
|
+
directory: '/',
|
|
1374
|
+
schedule: { interval: schedule, day, time },
|
|
1375
|
+
labels: ['dependencies', 'github-actions'],
|
|
1376
|
+
'commit-message': { prefix: 'deps(actions)' },
|
|
1377
|
+
})
|
|
1378
|
+
|
|
1379
|
+
const config = {
|
|
1380
|
+
version: 2,
|
|
1381
|
+
updates,
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
return { config, ecosystems }
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Write premium Dependabot configuration to file (multi-language support)
|
|
1389
|
+
*
|
|
1390
|
+
* @param {Object} configData - Config and ecosystem detection results
|
|
1391
|
+
* @param {string} outputPath - Path to write config file
|
|
1392
|
+
*/
|
|
1393
|
+
function writePremiumDependabotConfig(configData, outputPath) {
|
|
1394
|
+
const { config, ecosystems } = configData
|
|
1395
|
+
|
|
1396
|
+
// Build header with all detected ecosystems
|
|
1397
|
+
const _detectedLanguages = Object.keys(ecosystems).filter(e => e !== 'npm')
|
|
1398
|
+
const languageList = Object.keys(ecosystems).join(', ')
|
|
1399
|
+
|
|
1400
|
+
let frameworkSummary = ''
|
|
1401
|
+
Object.entries(ecosystems).forEach(([ecosystem, data]) => {
|
|
1402
|
+
const frameworks = Object.keys(data.detected || {}).join(', ')
|
|
1403
|
+
if (frameworks) {
|
|
1404
|
+
frameworkSummary += `# ${ecosystem}: ${frameworks}\n`
|
|
1405
|
+
}
|
|
1406
|
+
})
|
|
1407
|
+
|
|
1408
|
+
const yamlContent = `# Premium Dependabot configuration (Pro Tier)
|
|
1409
|
+
# Auto-generated by create-qa-architect
|
|
1410
|
+
# Multi-language framework-aware dependency grouping
|
|
1411
|
+
#
|
|
1412
|
+
# Detected ecosystems: ${languageList}
|
|
1413
|
+
${frameworkSummary}#
|
|
1414
|
+
# This configuration groups dependencies by framework to reduce PR volume
|
|
1415
|
+
# and make dependency updates more manageable across all languages.
|
|
1416
|
+
#
|
|
1417
|
+
# Learn more: https://create-qa-architect.dev/docs/multi-language-grouping
|
|
1418
|
+
|
|
1419
|
+
${yaml.dump(config, { indent: 2, lineWidth: 120, sortKeys: false })}`
|
|
1420
|
+
|
|
1421
|
+
const configDir = path.dirname(outputPath)
|
|
1422
|
+
if (!fs.existsSync(configDir)) {
|
|
1423
|
+
fs.mkdirSync(configDir, { recursive: true })
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
fs.writeFileSync(outputPath, yamlContent)
|
|
1427
|
+
|
|
1428
|
+
// Post-write validation: verify the file can be parsed
|
|
1429
|
+
try {
|
|
1430
|
+
const writtenContent = fs.readFileSync(outputPath, 'utf8')
|
|
1431
|
+
yaml.load(writtenContent)
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
throw new Error(
|
|
1434
|
+
`Generated Premium Dependabot configuration is invalid YAML: ${error.message}`
|
|
1435
|
+
)
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
module.exports = {
|
|
1440
|
+
// npm ecosystem
|
|
1441
|
+
detectFrameworks,
|
|
1442
|
+
generateReactGroups,
|
|
1443
|
+
generateVueGroups,
|
|
1444
|
+
generateAngularGroups,
|
|
1445
|
+
generateTestingGroups,
|
|
1446
|
+
generateBuildToolGroups,
|
|
1447
|
+
generateStorybookGroups,
|
|
1448
|
+
|
|
1449
|
+
// Python ecosystem
|
|
1450
|
+
detectPythonFrameworks,
|
|
1451
|
+
generateDjangoGroups,
|
|
1452
|
+
generateFlaskGroups,
|
|
1453
|
+
generateFastAPIGroups,
|
|
1454
|
+
generateDataScienceGroups,
|
|
1455
|
+
generatePythonTestingGroups,
|
|
1456
|
+
|
|
1457
|
+
// Rust ecosystem
|
|
1458
|
+
detectRustFrameworks,
|
|
1459
|
+
generateActixGroups,
|
|
1460
|
+
generateAsyncRuntimeGroups,
|
|
1461
|
+
generateSerdeGroups,
|
|
1462
|
+
|
|
1463
|
+
// Ruby ecosystem
|
|
1464
|
+
detectRubyFrameworks,
|
|
1465
|
+
generateRailsGroups,
|
|
1466
|
+
generateRSpecGroups,
|
|
1467
|
+
|
|
1468
|
+
// Multi-language support
|
|
1469
|
+
detectAllEcosystems,
|
|
1470
|
+
generateNpmGroups,
|
|
1471
|
+
generatePipGroups,
|
|
1472
|
+
generateCargoGroups,
|
|
1473
|
+
generateBundlerGroups,
|
|
1474
|
+
|
|
1475
|
+
// Main config generation
|
|
1476
|
+
generatePremiumDependabotConfig,
|
|
1477
|
+
writePremiumDependabotConfig,
|
|
1478
|
+
|
|
1479
|
+
// Parsing functions (for testing)
|
|
1480
|
+
parsePyprojectToml,
|
|
1481
|
+
parsePipRequirements,
|
|
1482
|
+
parseCargoToml,
|
|
1483
|
+
parseGemfile,
|
|
1484
|
+
|
|
1485
|
+
// Framework signatures (for testing)
|
|
1486
|
+
NPM_FRAMEWORK_SIGNATURES,
|
|
1487
|
+
PYTHON_FRAMEWORK_SIGNATURES,
|
|
1488
|
+
RUST_FRAMEWORK_SIGNATURES,
|
|
1489
|
+
RUBY_FRAMEWORK_SIGNATURES,
|
|
1490
|
+
}
|