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,187 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Package.json merge utilities
|
|
5
|
+
* Shared between setup script and tests to avoid duplication
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Merge scripts into package.json, preserving existing ones
|
|
10
|
+
* @param {Object} initialScripts - Existing scripts object
|
|
11
|
+
* @param {Object} defaultScripts - Default scripts to add
|
|
12
|
+
* @returns {Object} Merged scripts object
|
|
13
|
+
*/
|
|
14
|
+
function mergeScripts(initialScripts = {}, defaultScripts) {
|
|
15
|
+
const scripts = { ...initialScripts }
|
|
16
|
+
Object.entries(defaultScripts).forEach(([name, command]) => {
|
|
17
|
+
if (!scripts[name]) {
|
|
18
|
+
scripts[name] = command
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// Ensure husky command is present in prepare script
|
|
23
|
+
const prepareScript = scripts.prepare
|
|
24
|
+
if (!prepareScript) {
|
|
25
|
+
scripts.prepare = 'husky'
|
|
26
|
+
} else if (prepareScript.includes('husky install')) {
|
|
27
|
+
scripts.prepare = prepareScript.replace(/husky install/g, 'husky')
|
|
28
|
+
} else if (!prepareScript.includes('husky')) {
|
|
29
|
+
scripts.prepare = `${prepareScript} && husky`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return scripts
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Merge devDependencies into package.json, preserving existing ones
|
|
37
|
+
* @param {Object} initialDevDeps - Existing devDependencies object
|
|
38
|
+
* @param {Object} defaultDevDeps - Default devDependencies to add
|
|
39
|
+
* @returns {Object} Merged devDependencies object
|
|
40
|
+
*/
|
|
41
|
+
function mergeDevDependencies(initialDevDeps = {}, defaultDevDeps) {
|
|
42
|
+
const devDeps = { ...initialDevDeps }
|
|
43
|
+
Object.entries(defaultDevDeps).forEach(([dependency, version]) => {
|
|
44
|
+
if (!devDeps[dependency]) {
|
|
45
|
+
devDeps[dependency] = version
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
return devDeps
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Merge lint-staged configuration, preserving existing patterns
|
|
53
|
+
* @param {Object} existing - Existing lint-staged config
|
|
54
|
+
* @param {Object} defaults - Default lint-staged config
|
|
55
|
+
* @param {Object} options - Merge options
|
|
56
|
+
* @param {Function} patternChecker - Function to check if a pattern matches certain criteria
|
|
57
|
+
* @returns {Object} Merged lint-staged config
|
|
58
|
+
*/
|
|
59
|
+
function mergeLintStaged(
|
|
60
|
+
existing = {},
|
|
61
|
+
defaults,
|
|
62
|
+
options = {},
|
|
63
|
+
patternChecker = null
|
|
64
|
+
) {
|
|
65
|
+
const merged = { ...existing }
|
|
66
|
+
const stylelintTargets = options.stylelintTargets || []
|
|
67
|
+
const stylelintTargetSet = new Set(stylelintTargets)
|
|
68
|
+
|
|
69
|
+
// Check if existing config has CSS patterns
|
|
70
|
+
const hasExistingCssPatterns =
|
|
71
|
+
patternChecker && Object.keys(existing).some(patternChecker)
|
|
72
|
+
|
|
73
|
+
Object.entries(defaults).forEach(([pattern, commands]) => {
|
|
74
|
+
const isStylelintPattern = stylelintTargetSet.has(pattern)
|
|
75
|
+
if (isStylelintPattern && hasExistingCssPatterns) {
|
|
76
|
+
return // Skip stylelint patterns if existing CSS patterns exist
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!merged[pattern]) {
|
|
80
|
+
merged[pattern] = commands
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Merge commands for existing patterns
|
|
85
|
+
|
|
86
|
+
const existingCommands = Array.isArray(merged[pattern])
|
|
87
|
+
? [...merged[pattern]]
|
|
88
|
+
: [merged[pattern]]
|
|
89
|
+
|
|
90
|
+
const newCommands = [...existingCommands]
|
|
91
|
+
commands.forEach(command => {
|
|
92
|
+
if (!newCommands.includes(command)) {
|
|
93
|
+
newCommands.push(command)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
merged[pattern] = newCommands
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return merged
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Detect which package manager is being used in the project
|
|
105
|
+
* @param {string} projectPath - Path to the project directory
|
|
106
|
+
* @returns {string} Package manager name: 'pnpm', 'yarn', 'bun', or 'npm'
|
|
107
|
+
*/
|
|
108
|
+
function detectPackageManager(projectPath = process.cwd()) {
|
|
109
|
+
const fs = require('fs')
|
|
110
|
+
const path = require('path')
|
|
111
|
+
|
|
112
|
+
// Check for lockfiles in order of preference
|
|
113
|
+
if (fs.existsSync(path.join(projectPath, 'pnpm-lock.yaml'))) {
|
|
114
|
+
return 'pnpm'
|
|
115
|
+
}
|
|
116
|
+
if (fs.existsSync(path.join(projectPath, 'yarn.lock'))) {
|
|
117
|
+
return 'yarn'
|
|
118
|
+
}
|
|
119
|
+
if (fs.existsSync(path.join(projectPath, 'bun.lockb'))) {
|
|
120
|
+
return 'bun'
|
|
121
|
+
}
|
|
122
|
+
if (fs.existsSync(path.join(projectPath, 'package-lock.json'))) {
|
|
123
|
+
return 'npm'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check package.json for packageManager field (Corepack)
|
|
127
|
+
const packageJsonPath = path.join(projectPath, 'package.json')
|
|
128
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
129
|
+
try {
|
|
130
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
131
|
+
if (packageJson.packageManager) {
|
|
132
|
+
// Format: "pnpm@8.0.0" or "yarn@3.0.0"
|
|
133
|
+
const pmName = packageJson.packageManager.split('@')[0]
|
|
134
|
+
if (['pnpm', 'yarn', 'bun', 'npm'].includes(pmName)) {
|
|
135
|
+
return pmName
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Ignore parse errors
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Default to npm if no lockfile found
|
|
144
|
+
return 'npm'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get install command for detected package manager
|
|
149
|
+
* @param {string} packageManager - Package manager name
|
|
150
|
+
* @param {boolean} frozen - Use frozen/immutable lockfile (CI mode)
|
|
151
|
+
* @returns {string} Install command
|
|
152
|
+
*/
|
|
153
|
+
function getInstallCommand(packageManager, frozen = true) {
|
|
154
|
+
const commands = {
|
|
155
|
+
pnpm: frozen ? 'pnpm install --frozen-lockfile' : 'pnpm install',
|
|
156
|
+
yarn: frozen ? 'yarn install --frozen-lockfile' : 'yarn install',
|
|
157
|
+
bun: frozen ? 'bun install --frozen-lockfile' : 'bun install',
|
|
158
|
+
npm: frozen ? 'npm ci' : 'npm install',
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return commands[packageManager] || 'npm install'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get audit command for detected package manager
|
|
166
|
+
* @param {string} packageManager - Package manager name
|
|
167
|
+
* @returns {string} Audit command
|
|
168
|
+
*/
|
|
169
|
+
function getAuditCommand(packageManager) {
|
|
170
|
+
const commands = {
|
|
171
|
+
pnpm: 'pnpm audit',
|
|
172
|
+
yarn: 'yarn audit',
|
|
173
|
+
bun: 'bun audit', // Bun has audit support
|
|
174
|
+
npm: 'npm audit',
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return commands[packageManager] || 'npm audit'
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
mergeScripts,
|
|
182
|
+
mergeDevDependencies,
|
|
183
|
+
mergeLintStaged,
|
|
184
|
+
detectPackageManager,
|
|
185
|
+
getInstallCommand,
|
|
186
|
+
getAuditCommand,
|
|
187
|
+
}
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Project Maturity Detection for Progressive Quality Automation
|
|
5
|
+
*
|
|
6
|
+
* Automatically detects project maturity level and recommends appropriate quality checks.
|
|
7
|
+
*
|
|
8
|
+
* Maturity Levels:
|
|
9
|
+
* - minimal: Just package.json, maybe README (< 5 files)
|
|
10
|
+
* - bootstrap: Some source files, no tests yet (1-2 source files)
|
|
11
|
+
* - development: Active development with tests (ā„ 3 source files, has tests)
|
|
12
|
+
* - production-ready: Full project (ā„ 10 source files, tests, docs)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs')
|
|
16
|
+
const path = require('path')
|
|
17
|
+
const {
|
|
18
|
+
MATURITY_THRESHOLDS,
|
|
19
|
+
SCAN_LIMITS,
|
|
20
|
+
EXCLUDE_DIRECTORIES,
|
|
21
|
+
} = require('../config/constants')
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maturity level definitions with check recommendations
|
|
25
|
+
*/
|
|
26
|
+
const MATURITY_LEVELS = {
|
|
27
|
+
minimal: {
|
|
28
|
+
name: 'Minimal',
|
|
29
|
+
description: 'Just getting started - basic setup only',
|
|
30
|
+
checks: {
|
|
31
|
+
required: ['prettier'],
|
|
32
|
+
optional: [],
|
|
33
|
+
disabled: [
|
|
34
|
+
'eslint',
|
|
35
|
+
'stylelint',
|
|
36
|
+
'tests',
|
|
37
|
+
'coverage',
|
|
38
|
+
'security-audit',
|
|
39
|
+
'documentation',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
message:
|
|
43
|
+
'ā” Minimal project - only basic formatting checks enabled. Add source files to enable linting.',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
bootstrap: {
|
|
47
|
+
name: 'Bootstrap',
|
|
48
|
+
description: 'Early development - has some source files',
|
|
49
|
+
checks: {
|
|
50
|
+
required: ['prettier', 'eslint'],
|
|
51
|
+
optional: ['stylelint'],
|
|
52
|
+
disabled: ['tests', 'coverage', 'security-audit', 'documentation'],
|
|
53
|
+
},
|
|
54
|
+
message:
|
|
55
|
+
'š Bootstrap project - linting enabled. Add tests to enable test coverage checks.',
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
development: {
|
|
59
|
+
name: 'Development',
|
|
60
|
+
description: 'Active development - has source files and tests',
|
|
61
|
+
checks: {
|
|
62
|
+
required: ['prettier', 'eslint', 'stylelint', 'tests'],
|
|
63
|
+
optional: ['coverage', 'security-audit'],
|
|
64
|
+
disabled: ['documentation'],
|
|
65
|
+
},
|
|
66
|
+
message:
|
|
67
|
+
'šØ Development project - most checks enabled. Add documentation to enable doc validation.',
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
'production-ready': {
|
|
71
|
+
name: 'Production Ready',
|
|
72
|
+
description: 'Mature project - full quality automation',
|
|
73
|
+
checks: {
|
|
74
|
+
required: [
|
|
75
|
+
'prettier',
|
|
76
|
+
'eslint',
|
|
77
|
+
'stylelint',
|
|
78
|
+
'tests',
|
|
79
|
+
'coverage',
|
|
80
|
+
'security-audit',
|
|
81
|
+
'documentation',
|
|
82
|
+
],
|
|
83
|
+
optional: ['lighthouse'],
|
|
84
|
+
disabled: [],
|
|
85
|
+
},
|
|
86
|
+
message: 'ā
Production-ready project - all quality checks enabled.',
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class ProjectMaturityDetector {
|
|
91
|
+
constructor(options = {}) {
|
|
92
|
+
this.verbose = options.verbose || false
|
|
93
|
+
this.projectPath = options.projectPath || process.cwd()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Detect project maturity level
|
|
98
|
+
* @returns {string} Maturity level: 'minimal', 'bootstrap', 'development', or 'production-ready'
|
|
99
|
+
*/
|
|
100
|
+
detect() {
|
|
101
|
+
const stats = this.analyzeProject()
|
|
102
|
+
|
|
103
|
+
if (this.verbose) {
|
|
104
|
+
console.log('š Project Analysis:')
|
|
105
|
+
console.log(` Source files: ${stats.totalSourceFiles}`)
|
|
106
|
+
console.log(` Test files: ${stats.testFiles}`)
|
|
107
|
+
console.log(` Has documentation: ${stats.hasDocumentation}`)
|
|
108
|
+
console.log(` Has dependencies: ${stats.hasDependencies}`)
|
|
109
|
+
console.log(` Has CSS files: ${stats.hasCssFiles}`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Determine maturity level based on project characteristics
|
|
113
|
+
let maturity = 'minimal'
|
|
114
|
+
|
|
115
|
+
if (stats.totalSourceFiles === 0) {
|
|
116
|
+
maturity = 'minimal'
|
|
117
|
+
} else if (
|
|
118
|
+
stats.totalSourceFiles < MATURITY_THRESHOLDS.MIN_BOOTSTRAP_FILES &&
|
|
119
|
+
stats.testFiles === 0
|
|
120
|
+
) {
|
|
121
|
+
maturity = 'bootstrap'
|
|
122
|
+
} else if (
|
|
123
|
+
stats.testFiles > 0 &&
|
|
124
|
+
stats.totalSourceFiles >= MATURITY_THRESHOLDS.MIN_BOOTSTRAP_FILES &&
|
|
125
|
+
stats.totalSourceFiles < MATURITY_THRESHOLDS.MIN_PRODUCTION_FILES
|
|
126
|
+
) {
|
|
127
|
+
maturity = 'development'
|
|
128
|
+
} else if (
|
|
129
|
+
stats.testFiles >= MATURITY_THRESHOLDS.MIN_PRODUCTION_TESTS &&
|
|
130
|
+
stats.totalSourceFiles >= MATURITY_THRESHOLDS.MIN_PRODUCTION_FILES &&
|
|
131
|
+
(stats.hasDocumentation || stats.hasDependencies)
|
|
132
|
+
) {
|
|
133
|
+
maturity = 'production-ready'
|
|
134
|
+
} else if (
|
|
135
|
+
stats.totalSourceFiles >= MATURITY_THRESHOLDS.MIN_BOOTSTRAP_FILES
|
|
136
|
+
) {
|
|
137
|
+
maturity = 'development'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (this.verbose) {
|
|
141
|
+
const level = MATURITY_LEVELS[maturity]
|
|
142
|
+
console.log(`\n${level.message}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return maturity
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get detailed project statistics
|
|
150
|
+
* @returns {Object} Project statistics
|
|
151
|
+
*/
|
|
152
|
+
analyzeProject() {
|
|
153
|
+
return {
|
|
154
|
+
totalSourceFiles: this.countSourceFiles(),
|
|
155
|
+
testFiles: this.countTestFiles(),
|
|
156
|
+
hasDocumentation: this.hasDocumentation(),
|
|
157
|
+
hasTests: this.hasTests(),
|
|
158
|
+
hasDependencies: this.hasDependencies(),
|
|
159
|
+
hasCssFiles: this.hasCssFiles(),
|
|
160
|
+
packageJsonExists: this.packageJsonExists(),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Count JavaScript/TypeScript source files (excluding tests)
|
|
166
|
+
* @returns {number} Number of source files
|
|
167
|
+
*/
|
|
168
|
+
countSourceFiles() {
|
|
169
|
+
const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']
|
|
170
|
+
const testPatterns = ['.test.', '.spec.', '__tests__', '__mocks__']
|
|
171
|
+
|
|
172
|
+
return this.countFilesRecursive(this.projectPath, {
|
|
173
|
+
extensions,
|
|
174
|
+
excludeDirs: EXCLUDE_DIRECTORIES.PROJECT_MATURITY,
|
|
175
|
+
excludePatterns: testPatterns,
|
|
176
|
+
maxDepth: 5,
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Count test files
|
|
182
|
+
* @returns {number} Number of test files
|
|
183
|
+
*/
|
|
184
|
+
countTestFiles() {
|
|
185
|
+
const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']
|
|
186
|
+
const testPatterns = ['.test.', '.spec.', '__tests__']
|
|
187
|
+
|
|
188
|
+
return this.countFilesRecursive(this.projectPath, {
|
|
189
|
+
extensions,
|
|
190
|
+
excludeDirs: EXCLUDE_DIRECTORIES.PROJECT_MATURITY,
|
|
191
|
+
includePatterns: testPatterns,
|
|
192
|
+
maxDepth: 5,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check if project has documentation
|
|
198
|
+
* @returns {boolean} True if documentation exists
|
|
199
|
+
*/
|
|
200
|
+
hasDocumentation() {
|
|
201
|
+
const docIndicators = [
|
|
202
|
+
'docs',
|
|
203
|
+
'documentation',
|
|
204
|
+
'doc',
|
|
205
|
+
'.github/CONTRIBUTING.md',
|
|
206
|
+
'.github/CODE_OF_CONDUCT.md',
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
// Check for docs directory
|
|
210
|
+
for (const indicator of docIndicators) {
|
|
211
|
+
const docPath = path.join(this.projectPath, indicator)
|
|
212
|
+
if (fs.existsSync(docPath)) {
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check for substantial README (> MIN_LINES_FOR_DOCS lines)
|
|
218
|
+
const readmePath = path.join(this.projectPath, 'README.md')
|
|
219
|
+
if (fs.existsSync(readmePath)) {
|
|
220
|
+
const content = fs.readFileSync(readmePath, 'utf8')
|
|
221
|
+
const lines = content.split('\n').length
|
|
222
|
+
if (lines > MATURITY_THRESHOLDS.README_MIN_LINES_FOR_DOCS) {
|
|
223
|
+
return true
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Check if project has tests directory or test files
|
|
232
|
+
* @returns {boolean} True if tests exist
|
|
233
|
+
*/
|
|
234
|
+
hasTests() {
|
|
235
|
+
const testDirs = [
|
|
236
|
+
'test',
|
|
237
|
+
'tests',
|
|
238
|
+
'__tests__',
|
|
239
|
+
'spec',
|
|
240
|
+
'specs',
|
|
241
|
+
'__specs__',
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
for (const dir of testDirs) {
|
|
245
|
+
const testPath = path.join(this.projectPath, dir)
|
|
246
|
+
if (fs.existsSync(testPath)) {
|
|
247
|
+
return true
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return this.countTestFiles() > 0
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if package.json has dependencies
|
|
256
|
+
* @returns {boolean} True if dependencies exist
|
|
257
|
+
*/
|
|
258
|
+
hasDependencies() {
|
|
259
|
+
const packageJsonPath = path.join(this.projectPath, 'package.json')
|
|
260
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
261
|
+
return false
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
266
|
+
const deps = packageJson.dependencies || {}
|
|
267
|
+
const devDeps = packageJson.devDependencies || {}
|
|
268
|
+
return Object.keys(deps).length > 0 || Object.keys(devDeps).length > 0
|
|
269
|
+
} catch {
|
|
270
|
+
return false
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check if project has CSS/SCSS files
|
|
276
|
+
* @returns {boolean} True if CSS files exist
|
|
277
|
+
*/
|
|
278
|
+
hasCssFiles() {
|
|
279
|
+
const extensions = ['.css', '.scss', '.sass', '.less', '.pcss']
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
this.countFilesRecursive(this.projectPath, {
|
|
283
|
+
extensions,
|
|
284
|
+
excludeDirs: EXCLUDE_DIRECTORIES.PROJECT_MATURITY,
|
|
285
|
+
maxDepth: 4,
|
|
286
|
+
}) > 0
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Check if package.json exists
|
|
292
|
+
* @returns {boolean} True if package.json exists
|
|
293
|
+
*/
|
|
294
|
+
packageJsonExists() {
|
|
295
|
+
return fs.existsSync(path.join(this.projectPath, 'package.json'))
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Recursively counts files in a directory tree with advanced filtering
|
|
300
|
+
*
|
|
301
|
+
* Provides flexible file counting with multiple filter options for project
|
|
302
|
+
* analysis. Supports extension filtering, directory exclusion, pattern
|
|
303
|
+
* matching, and depth limiting. Skips symlinks for safety.
|
|
304
|
+
*
|
|
305
|
+
* Algorithm:
|
|
306
|
+
* 1. Read directory entries (skip if depth > maxDepth)
|
|
307
|
+
* 2. Skip excluded directories and symbolic links
|
|
308
|
+
* 3. For subdirectories: recursively count (depth + 1)
|
|
309
|
+
* 4. For files: apply all filters (extension, include, exclude)
|
|
310
|
+
* 5. Return total count across all matching files
|
|
311
|
+
*
|
|
312
|
+
* Filter priority:
|
|
313
|
+
* 1. Extension filter (if specified, must match)
|
|
314
|
+
* 2. Include patterns (if specified, must match at least one)
|
|
315
|
+
* 3. Exclude patterns (if any match, file is excluded)
|
|
316
|
+
*
|
|
317
|
+
* @param {string} dir - Directory to search (absolute path)
|
|
318
|
+
* @param {Object} [options={}] - Search and filter options
|
|
319
|
+
* @param {string[]} [options.extensions=[]] - File extensions to count (e.g., ['.js', '.ts'])
|
|
320
|
+
* @param {string[]} [options.excludeDirs=[]] - Directory names to skip (e.g., ['node_modules'])
|
|
321
|
+
* @param {string[]} [options.includePatterns=[]] - Filename patterns to include (substring match)
|
|
322
|
+
* @param {string[]} [options.excludePatterns=[]] - Filename patterns to exclude (substring match)
|
|
323
|
+
* @param {number} [options.maxDepth=5] - Maximum recursion depth
|
|
324
|
+
* @param {number} [options.currentDepth=0] - Current depth (for internal recursion)
|
|
325
|
+
* @returns {number} Count of files matching all filters
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* // Count all JavaScript files
|
|
329
|
+
* const jsCount = maturity.countFilesRecursive('./src', {
|
|
330
|
+
* extensions: ['.js', '.jsx']
|
|
331
|
+
* })
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* // Count test files in src/, excluding node_modules
|
|
335
|
+
* const testCount = maturity.countFilesRecursive('./src', {
|
|
336
|
+
* includePatterns: ['.test.', '.spec.'],
|
|
337
|
+
* excludeDirs: ['node_modules', 'dist'],
|
|
338
|
+
* maxDepth: 10
|
|
339
|
+
* })
|
|
340
|
+
*/
|
|
341
|
+
countFilesRecursive(dir, options = {}) {
|
|
342
|
+
const {
|
|
343
|
+
extensions = [],
|
|
344
|
+
excludeDirs = [],
|
|
345
|
+
includePatterns = [],
|
|
346
|
+
excludePatterns = [],
|
|
347
|
+
maxDepth = SCAN_LIMITS.FILE_COUNT_MAX_DEPTH,
|
|
348
|
+
currentDepth = 0,
|
|
349
|
+
} = options
|
|
350
|
+
|
|
351
|
+
if (currentDepth > maxDepth) {
|
|
352
|
+
return 0
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let count = 0
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
359
|
+
|
|
360
|
+
for (const entry of entries) {
|
|
361
|
+
if (entry.isSymbolicLink()) {
|
|
362
|
+
continue
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const fullPath = path.join(dir, entry.name)
|
|
366
|
+
|
|
367
|
+
if (entry.isDirectory()) {
|
|
368
|
+
if (excludeDirs.includes(entry.name)) {
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
count += this.countFilesRecursive(fullPath, {
|
|
373
|
+
...options,
|
|
374
|
+
currentDepth: currentDepth + 1,
|
|
375
|
+
})
|
|
376
|
+
} else if (entry.isFile()) {
|
|
377
|
+
const ext = path.extname(entry.name).toLowerCase()
|
|
378
|
+
|
|
379
|
+
// Check extension
|
|
380
|
+
if (extensions.length > 0 && !extensions.includes(ext)) {
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Check include patterns
|
|
385
|
+
if (includePatterns.length > 0) {
|
|
386
|
+
const matches = includePatterns.some(pattern =>
|
|
387
|
+
entry.name.includes(pattern)
|
|
388
|
+
)
|
|
389
|
+
if (!matches) {
|
|
390
|
+
continue
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Check exclude patterns
|
|
395
|
+
if (excludePatterns.length > 0) {
|
|
396
|
+
const excluded = excludePatterns.some(pattern =>
|
|
397
|
+
entry.name.includes(pattern)
|
|
398
|
+
)
|
|
399
|
+
if (excluded) {
|
|
400
|
+
continue
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
count++
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} catch (error) {
|
|
408
|
+
// Silently ignore permission errors
|
|
409
|
+
if (this.verbose) {
|
|
410
|
+
console.warn(`Warning: Could not read directory ${dir}:`, error.message)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return count
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Get maturity level details
|
|
419
|
+
* @param {string} maturity - Maturity level
|
|
420
|
+
* @returns {Object} Maturity level details
|
|
421
|
+
*/
|
|
422
|
+
getMaturityDetails(maturity) {
|
|
423
|
+
return MATURITY_LEVELS[maturity] || MATURITY_LEVELS.minimal
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Get recommended checks for maturity level
|
|
428
|
+
* @param {string} maturity - Maturity level
|
|
429
|
+
* @returns {Object} Recommended checks
|
|
430
|
+
*/
|
|
431
|
+
getRecommendedChecks(maturity) {
|
|
432
|
+
const level = this.getMaturityDetails(maturity)
|
|
433
|
+
return level.checks
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Generate GitHub Actions outputs for maturity detection
|
|
438
|
+
* @returns {string} GitHub Actions output format
|
|
439
|
+
*/
|
|
440
|
+
generateGitHubActionsOutput() {
|
|
441
|
+
const maturity = this.detect()
|
|
442
|
+
const stats = this.analyzeProject()
|
|
443
|
+
const checks = this.getRecommendedChecks(maturity)
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
maturity,
|
|
447
|
+
sourceCount: stats.totalSourceFiles,
|
|
448
|
+
testCount: stats.testFiles,
|
|
449
|
+
hasDeps: stats.hasDependencies,
|
|
450
|
+
hasDocs: stats.hasDocumentation,
|
|
451
|
+
hasCss: stats.hasCssFiles,
|
|
452
|
+
requiredChecks: checks.required.join(','),
|
|
453
|
+
optionalChecks: checks.optional.join(','),
|
|
454
|
+
disabledChecks: checks.disabled.join(','),
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Print human-readable maturity report
|
|
460
|
+
*/
|
|
461
|
+
printReport() {
|
|
462
|
+
const maturity = this.detect()
|
|
463
|
+
const stats = this.analyzeProject()
|
|
464
|
+
const level = this.getMaturityDetails(maturity)
|
|
465
|
+
|
|
466
|
+
console.log('\nš Project Maturity Report\n')
|
|
467
|
+
console.log(`Maturity Level: ${level.name}`)
|
|
468
|
+
console.log(`Description: ${level.description}\n`)
|
|
469
|
+
|
|
470
|
+
console.log('Project Statistics:')
|
|
471
|
+
console.log(` ⢠Source files: ${stats.totalSourceFiles}`)
|
|
472
|
+
console.log(` ⢠Test files: ${stats.testFiles}`)
|
|
473
|
+
console.log(` ⢠Documentation: ${stats.hasDocumentation ? 'Yes' : 'No'}`)
|
|
474
|
+
console.log(` ⢠Dependencies: ${stats.hasDependencies ? 'Yes' : 'No'}`)
|
|
475
|
+
console.log(` ⢠CSS files: ${stats.hasCssFiles ? 'Yes' : 'No'}\n`)
|
|
476
|
+
|
|
477
|
+
console.log('Quality Checks:')
|
|
478
|
+
console.log(` ā
Required: ${level.checks.required.join(', ')}`)
|
|
479
|
+
if (level.checks.optional.length > 0) {
|
|
480
|
+
console.log(` šµ Optional: ${level.checks.optional.join(', ')}`)
|
|
481
|
+
}
|
|
482
|
+
if (level.checks.disabled.length > 0) {
|
|
483
|
+
console.log(` āļø Disabled: ${level.checks.disabled.join(', ')}`)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
console.log(`\n${level.message}\n`)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// CLI usage
|
|
491
|
+
if (require.main === module) {
|
|
492
|
+
const args = process.argv.slice(2)
|
|
493
|
+
const verbose = args.includes('--verbose') || args.includes('-v')
|
|
494
|
+
const githubActions = args.includes('--github-actions')
|
|
495
|
+
|
|
496
|
+
const detector = new ProjectMaturityDetector({ verbose })
|
|
497
|
+
|
|
498
|
+
if (githubActions) {
|
|
499
|
+
// Output for GitHub Actions
|
|
500
|
+
const output = detector.generateGitHubActionsOutput()
|
|
501
|
+
console.log(`maturity=${output.maturity}`)
|
|
502
|
+
console.log(`source-count=${output.sourceCount}`)
|
|
503
|
+
console.log(`test-count=${output.testCount}`)
|
|
504
|
+
console.log(`has-deps=${output.hasDeps}`)
|
|
505
|
+
console.log(`has-docs=${output.hasDocs}`)
|
|
506
|
+
console.log(`has-css=${output.hasCss}`)
|
|
507
|
+
console.log(`required-checks=${output.requiredChecks}`)
|
|
508
|
+
console.log(`optional-checks=${output.optionalChecks}`)
|
|
509
|
+
console.log(`disabled-checks=${output.disabledChecks}`)
|
|
510
|
+
} else {
|
|
511
|
+
// Human-readable report
|
|
512
|
+
detector.printReport()
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
module.exports = { ProjectMaturityDetector, MATURITY_LEVELS }
|