cistack 1.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/README.md +173 -0
- package/bin/ciflow.js +35 -0
- package/package.json +36 -0
- package/src/analyzers/codebase.js +205 -0
- package/src/detectors/framework.js +137 -0
- package/src/detectors/hosting.js +256 -0
- package/src/detectors/language.js +116 -0
- package/src/detectors/testing.js +137 -0
- package/src/generators/workflow.js +670 -0
- package/src/index.js +166 -0
- package/src/utils/helpers.js +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# cistack
|
|
2
|
+
|
|
3
|
+
> Automatically generate GitHub Actions CI/CD pipelines by analysing your codebase
|
|
4
|
+
|
|
5
|
+
`cistack` scans your project directory and produces production-grade GitHub Actions workflow YAML files. It detects your language, framework, testing tools, and hosting platform — then writes the best pipeline for your stack.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🔍 **Deep codebase analysis** — reads `package.json`, lock files, config files, and directory structure
|
|
12
|
+
- 🧠 **Smart detection** — identifies 30+ frameworks, 12 languages, 12+ testing tools, and 10+ hosting platforms
|
|
13
|
+
- 🚀 **Hosting auto-detect** — Firebase, Vercel, Netlify, AWS, GCP, Azure, Heroku, Render, Railway, GitHub Pages, Docker
|
|
14
|
+
- 🏗️ **Multi-workflow output** — generates separate `ci.yml`, `deploy.yml`, `docker.yml`, and `security.yml` as appropriate
|
|
15
|
+
- 🔒 **Security built-in** — CodeQL analysis + dependency auditing on every pipeline
|
|
16
|
+
- 📦 **Monorepo aware** — detects Turborepo, Nx, Lerna, pnpm workspaces
|
|
17
|
+
- ✅ **Interactive mode** — confirms detected settings before writing files
|
|
18
|
+
- 🎯 **Zero config** — works out of the box with no configuration needed
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Run without installing (recommended for one-off use)
|
|
26
|
+
npx cistack
|
|
27
|
+
|
|
28
|
+
# Install globally
|
|
29
|
+
npm install -g cistack
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# In your project directory
|
|
38
|
+
npx cistack
|
|
39
|
+
|
|
40
|
+
# Specify a project path
|
|
41
|
+
npx cistack --path /path/to/project
|
|
42
|
+
|
|
43
|
+
# Custom output directory
|
|
44
|
+
npx cistack --output .github/workflows
|
|
45
|
+
|
|
46
|
+
# Dry run (print YAML without writing files)
|
|
47
|
+
npx cistack --dry-run
|
|
48
|
+
|
|
49
|
+
# Skip interactive prompts
|
|
50
|
+
npx cistack --no-prompt
|
|
51
|
+
|
|
52
|
+
# Verbose output
|
|
53
|
+
npx cistack --verbose
|
|
54
|
+
|
|
55
|
+
# Force overwrite existing files
|
|
56
|
+
npx cistack --force
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Detected Hosting Platforms
|
|
62
|
+
|
|
63
|
+
| Platform | Detection Signal |
|
|
64
|
+
|---|---|
|
|
65
|
+
| **Firebase** | `firebase.json`, `.firebaserc`, `firebase-tools` dep |
|
|
66
|
+
| **Vercel** | `vercel.json`, `.vercel` dir, `vercel` dep |
|
|
67
|
+
| **Netlify** | `netlify.toml`, `_redirects`, `netlify-cli` dep |
|
|
68
|
+
| **GitHub Pages** | `gh-pages` dep, `github.io` homepage in `package.json` |
|
|
69
|
+
| **AWS** | `serverless.yml`, `appspec.yml`, `cdk.json`, `aws-sdk` dep |
|
|
70
|
+
| **GCP App Engine** | `app.yaml` |
|
|
71
|
+
| **Azure** | `azure/pipelines.yml`, `@azure/*` deps |
|
|
72
|
+
| **Heroku** | `Procfile`, `heroku.yml` |
|
|
73
|
+
| **Render** | `render.yaml` |
|
|
74
|
+
| **Railway** | `railway.json`, `railway.toml` |
|
|
75
|
+
| **Docker** | `Dockerfile`, `docker-compose.yml` |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Detected Frameworks
|
|
80
|
+
|
|
81
|
+
Next.js, Nuxt, SvelteKit, Remix, Astro, Vite, React, Vue, Angular, Svelte, Gatsby,
|
|
82
|
+
Express, Fastify, NestJS, Hono, Koa, tRPC,
|
|
83
|
+
Django, Flask, FastAPI,
|
|
84
|
+
Ruby on Rails,
|
|
85
|
+
Spring Boot,
|
|
86
|
+
Laravel,
|
|
87
|
+
Go (gin), Rust (Cargo)
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Detected Testing Tools
|
|
92
|
+
|
|
93
|
+
| Tool | Type |
|
|
94
|
+
|---|---|
|
|
95
|
+
| Jest, Vitest, Mocha | Unit |
|
|
96
|
+
| Cypress, Playwright | E2E |
|
|
97
|
+
| Pytest | Python unit |
|
|
98
|
+
| RSpec | Ruby unit |
|
|
99
|
+
| Go Test | Go unit |
|
|
100
|
+
| Cargo Test | Rust unit |
|
|
101
|
+
| PHPUnit | PHP unit |
|
|
102
|
+
| JUnit/Maven | JVM unit |
|
|
103
|
+
| Storybook | Visual |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Generated Workflows
|
|
108
|
+
|
|
109
|
+
### `ci.yml` — Continuous Integration
|
|
110
|
+
Runs on every push and pull request:
|
|
111
|
+
1. **Lint** — ESLint, TypeScript type-check, formatter check
|
|
112
|
+
2. **Test** — unit tests with coverage upload (matrix across Node versions)
|
|
113
|
+
3. **Build** — production build, artifact upload
|
|
114
|
+
4. **E2E** — Cypress / Playwright (if detected)
|
|
115
|
+
|
|
116
|
+
### `deploy.yml` — Continuous Deployment
|
|
117
|
+
Triggers on push to `main`/`master` + manual dispatch:
|
|
118
|
+
- Platform-specific deploy using the best available GitHub Action
|
|
119
|
+
- Proper secret references documented in the file header
|
|
120
|
+
|
|
121
|
+
### `docker.yml` — Docker Build & Push
|
|
122
|
+
Triggers on push to `main` and version tags:
|
|
123
|
+
- Multi-platform build via Docker Buildx
|
|
124
|
+
- Pushes to GitHub Container Registry (GHCR)
|
|
125
|
+
- Build cache via GitHub Actions cache
|
|
126
|
+
|
|
127
|
+
### `security.yml` — Security Audit
|
|
128
|
+
Runs on push, PRs, and weekly schedule:
|
|
129
|
+
- Dependency vulnerability audit (npm audit / safety / etc.)
|
|
130
|
+
- GitHub CodeQL analysis for the detected language
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Required Secrets
|
|
135
|
+
|
|
136
|
+
After generating, add the required secrets to your repository at:
|
|
137
|
+
`Settings → Secrets and variables → Actions`
|
|
138
|
+
|
|
139
|
+
Each generated `deploy.yml` has a comment at the top listing the exact secrets needed.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Examples
|
|
144
|
+
|
|
145
|
+
**Next.js + Vercel project:**
|
|
146
|
+
```
|
|
147
|
+
npx cistack
|
|
148
|
+
# → .github/workflows/ci.yml (lint, test, build)
|
|
149
|
+
# → .github/workflows/deploy.yml (vercel deploy)
|
|
150
|
+
# → .github/workflows/security.yml
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Firebase + React project:**
|
|
154
|
+
```
|
|
155
|
+
npx cistack
|
|
156
|
+
# → .github/workflows/ci.yml
|
|
157
|
+
# → .github/workflows/deploy.yml (firebase deploy --only hosting)
|
|
158
|
+
# → .github/workflows/security.yml
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Node.js API + Docker:**
|
|
162
|
+
```
|
|
163
|
+
npx cistack
|
|
164
|
+
# → .github/workflows/ci.yml
|
|
165
|
+
# → .github/workflows/docker.yml (GHCR push)
|
|
166
|
+
# → .github/workflows/security.yml
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
package/bin/ciflow.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const { program } = require('commander');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const CIFlow = require('../src/index');
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('cistack')
|
|
11
|
+
.description('Generate GitHub Actions CI/CD pipelines by analysing your codebase')
|
|
12
|
+
.version('1.0.0');
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command('generate', { isDefault: true })
|
|
16
|
+
.description('Analyse codebase and generate GitHub Actions workflow(s)')
|
|
17
|
+
.option('-p, --path <dir>', 'Path to the project root', process.cwd())
|
|
18
|
+
.option('-o, --output <dir>', 'Output directory for workflow files', '.github/workflows')
|
|
19
|
+
.option('--dry-run', 'Print the generated YAML without writing files')
|
|
20
|
+
.option('--force', 'Overwrite existing workflow files without prompting')
|
|
21
|
+
.option('--no-prompt', 'Skip interactive prompts and use detected settings')
|
|
22
|
+
.option('--verbose', 'Show detailed analysis output')
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
const ciflow = new CIFlow({
|
|
25
|
+
projectPath: path.resolve(options.path),
|
|
26
|
+
outputDir: options.output,
|
|
27
|
+
dryRun: options.dryRun,
|
|
28
|
+
force: options.force,
|
|
29
|
+
prompt: options.prompt,
|
|
30
|
+
verbose: options.verbose,
|
|
31
|
+
});
|
|
32
|
+
await ciflow.run();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cistack",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Automatically generate GitHub Actions CI/CD pipelines by analysing your codebase",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cistack": "./bin/ciflow.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/ciflow.js",
|
|
11
|
+
"test": "node bin/ciflow.js --dry-run"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"github-actions",
|
|
15
|
+
"ci-cd",
|
|
16
|
+
"pipeline",
|
|
17
|
+
"automation",
|
|
18
|
+
"devops",
|
|
19
|
+
"firebase",
|
|
20
|
+
"vercel",
|
|
21
|
+
"netlify"
|
|
22
|
+
],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"chalk": "^4.1.2",
|
|
27
|
+
"commander": "^11.1.0",
|
|
28
|
+
"glob": "^10.3.10",
|
|
29
|
+
"inquirer": "^8.2.6",
|
|
30
|
+
"js-yaml": "^4.1.0",
|
|
31
|
+
"ora": "^5.4.1"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=16.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { globSync } = require('glob');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Scans the project root and collects all the raw signals that detectors need.
|
|
9
|
+
*/
|
|
10
|
+
class CodebaseAnalyzer {
|
|
11
|
+
constructor(projectPath, options = {}) {
|
|
12
|
+
this.root = projectPath;
|
|
13
|
+
this.verbose = options.verbose || false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async analyse() {
|
|
17
|
+
const info = {
|
|
18
|
+
root: this.root,
|
|
19
|
+
files: [],
|
|
20
|
+
packageJson: null,
|
|
21
|
+
lockFiles: [],
|
|
22
|
+
configFiles: [],
|
|
23
|
+
dockerFiles: [],
|
|
24
|
+
envFiles: [],
|
|
25
|
+
srcStructure: {},
|
|
26
|
+
hasMonorepo: false,
|
|
27
|
+
workspaces: [],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ── gather all file paths (ignore node_modules, .git, dist, build) ────
|
|
31
|
+
const allFiles = globSync('**/*', {
|
|
32
|
+
cwd: this.root,
|
|
33
|
+
ignore: [
|
|
34
|
+
'node_modules/**',
|
|
35
|
+
'.git/**',
|
|
36
|
+
'dist/**',
|
|
37
|
+
'build/**',
|
|
38
|
+
'.next/**',
|
|
39
|
+
'.nuxt/**',
|
|
40
|
+
'coverage/**',
|
|
41
|
+
'*.min.js',
|
|
42
|
+
'*.min.css',
|
|
43
|
+
],
|
|
44
|
+
nodir: true,
|
|
45
|
+
dot: true,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
info.files = allFiles;
|
|
49
|
+
|
|
50
|
+
// ── parse package.json ────────────────────────────────────────────────
|
|
51
|
+
const pkgPath = path.join(this.root, 'package.json');
|
|
52
|
+
if (fs.existsSync(pkgPath)) {
|
|
53
|
+
try {
|
|
54
|
+
info.packageJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
55
|
+
} catch (_) {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── detect lock files ─────────────────────────────────────────────────
|
|
59
|
+
const lockCandidates = [
|
|
60
|
+
'package-lock.json',
|
|
61
|
+
'yarn.lock',
|
|
62
|
+
'pnpm-lock.yaml',
|
|
63
|
+
'bun.lockb',
|
|
64
|
+
'Pipfile.lock',
|
|
65
|
+
'poetry.lock',
|
|
66
|
+
'Gemfile.lock',
|
|
67
|
+
'go.sum',
|
|
68
|
+
'Cargo.lock',
|
|
69
|
+
'composer.lock',
|
|
70
|
+
];
|
|
71
|
+
info.lockFiles = lockCandidates.filter((f) =>
|
|
72
|
+
fs.existsSync(path.join(this.root, f))
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// ── detect notable config files ───────────────────────────────────────
|
|
76
|
+
const configCandidates = [
|
|
77
|
+
// Hosting
|
|
78
|
+
'firebase.json',
|
|
79
|
+
'.firebaserc',
|
|
80
|
+
'vercel.json',
|
|
81
|
+
'.vercel',
|
|
82
|
+
'netlify.toml',
|
|
83
|
+
'_redirects',
|
|
84
|
+
'render.yaml',
|
|
85
|
+
'railway.json',
|
|
86
|
+
'railway.toml',
|
|
87
|
+
'heroku.yml',
|
|
88
|
+
'Procfile',
|
|
89
|
+
'app.yaml', // GCP App Engine
|
|
90
|
+
'serverless.yml',
|
|
91
|
+
'serverless.yaml',
|
|
92
|
+
'amplify.yml',
|
|
93
|
+
'appspec.yml', // AWS CodeDeploy
|
|
94
|
+
// Docker
|
|
95
|
+
'Dockerfile',
|
|
96
|
+
'Dockerfile.prod',
|
|
97
|
+
'docker-compose.yml',
|
|
98
|
+
'docker-compose.yaml',
|
|
99
|
+
'.dockerignore',
|
|
100
|
+
// IaC
|
|
101
|
+
'terraform.tf',
|
|
102
|
+
'main.tf',
|
|
103
|
+
'pulumi.yaml',
|
|
104
|
+
'cdk.json',
|
|
105
|
+
// Lang-specific
|
|
106
|
+
'go.mod',
|
|
107
|
+
'Cargo.toml',
|
|
108
|
+
'pyproject.toml',
|
|
109
|
+
'setup.py',
|
|
110
|
+
'setup.cfg',
|
|
111
|
+
'Pipfile',
|
|
112
|
+
'requirements.txt',
|
|
113
|
+
'Gemfile',
|
|
114
|
+
'pom.xml',
|
|
115
|
+
'build.gradle',
|
|
116
|
+
'build.gradle.kts',
|
|
117
|
+
'settings.gradle',
|
|
118
|
+
'composer.json',
|
|
119
|
+
// Build tools
|
|
120
|
+
'vite.config.js',
|
|
121
|
+
'vite.config.ts',
|
|
122
|
+
'webpack.config.js',
|
|
123
|
+
'webpack.config.ts',
|
|
124
|
+
'rollup.config.js',
|
|
125
|
+
'turbo.json',
|
|
126
|
+
'nx.json',
|
|
127
|
+
'lerna.json',
|
|
128
|
+
'rush.json',
|
|
129
|
+
// Test
|
|
130
|
+
'jest.config.js',
|
|
131
|
+
'jest.config.ts',
|
|
132
|
+
'vitest.config.js',
|
|
133
|
+
'vitest.config.ts',
|
|
134
|
+
'cypress.config.js',
|
|
135
|
+
'cypress.config.ts',
|
|
136
|
+
'playwright.config.js',
|
|
137
|
+
'playwright.config.ts',
|
|
138
|
+
'.mocharc.js',
|
|
139
|
+
'.mocharc.yml',
|
|
140
|
+
'phpunit.xml',
|
|
141
|
+
'pytest.ini',
|
|
142
|
+
'conftest.py',
|
|
143
|
+
// Lint / Format
|
|
144
|
+
'.eslintrc',
|
|
145
|
+
'.eslintrc.js',
|
|
146
|
+
'.eslintrc.json',
|
|
147
|
+
'.prettierrc',
|
|
148
|
+
'.stylelintrc',
|
|
149
|
+
'biome.json',
|
|
150
|
+
// CI already present
|
|
151
|
+
'.travis.yml',
|
|
152
|
+
'circle.ci/config.yml',
|
|
153
|
+
'Jenkinsfile',
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
info.configFiles = configCandidates.filter((f) =>
|
|
157
|
+
fs.existsSync(path.join(this.root, f))
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
info.dockerFiles = info.configFiles.filter((f) =>
|
|
161
|
+
f.toLowerCase().includes('docker')
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// ── .env files ────────────────────────────────────────────────────────
|
|
165
|
+
info.envFiles = allFiles.filter((f) => path.basename(f).startsWith('.env'));
|
|
166
|
+
|
|
167
|
+
// ── src structure hints ───────────────────────────────────────────────
|
|
168
|
+
const topDirs = fs
|
|
169
|
+
.readdirSync(this.root, { withFileTypes: true })
|
|
170
|
+
.filter((d) => d.isDirectory())
|
|
171
|
+
.map((d) => d.name)
|
|
172
|
+
.filter(
|
|
173
|
+
(d) =>
|
|
174
|
+
!['node_modules', '.git', '.github', 'coverage', 'dist', 'build'].includes(d)
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
info.srcStructure.topDirs = topDirs;
|
|
178
|
+
info.srcStructure.hasPages =
|
|
179
|
+
topDirs.includes('pages') || topDirs.includes('app');
|
|
180
|
+
info.srcStructure.hasPublic = topDirs.includes('public');
|
|
181
|
+
info.srcStructure.hasSrc = topDirs.includes('src');
|
|
182
|
+
info.srcStructure.hasFunctions =
|
|
183
|
+
topDirs.includes('functions') || topDirs.includes('api');
|
|
184
|
+
|
|
185
|
+
// ── monorepo detection ────────────────────────────────────────────────
|
|
186
|
+
const hasMonorepoMarker =
|
|
187
|
+
fs.existsSync(path.join(this.root, 'pnpm-workspace.yaml')) ||
|
|
188
|
+
fs.existsSync(path.join(this.root, 'turbo.json')) ||
|
|
189
|
+
fs.existsSync(path.join(this.root, 'nx.json')) ||
|
|
190
|
+
fs.existsSync(path.join(this.root, 'lerna.json')) ||
|
|
191
|
+
(info.packageJson &&
|
|
192
|
+
(info.packageJson.workspaces ||
|
|
193
|
+
info.packageJson.private === true));
|
|
194
|
+
|
|
195
|
+
info.hasMonorepo = !!hasMonorepoMarker;
|
|
196
|
+
if (info.packageJson && info.packageJson.workspaces) {
|
|
197
|
+
const ws = info.packageJson.workspaces;
|
|
198
|
+
info.workspaces = Array.isArray(ws) ? ws : ws.packages || [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return info;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = CodebaseAnalyzer;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
class FrameworkDetector {
|
|
7
|
+
constructor(projectPath, codebaseInfo) {
|
|
8
|
+
this.root = projectPath;
|
|
9
|
+
this.info = codebaseInfo;
|
|
10
|
+
this.pkg = codebaseInfo.packageJson || {};
|
|
11
|
+
this.deps = {
|
|
12
|
+
...(this.pkg.dependencies || {}),
|
|
13
|
+
...(this.pkg.devDependencies || {}),
|
|
14
|
+
};
|
|
15
|
+
this.configs = new Set(codebaseInfo.configFiles);
|
|
16
|
+
this.files = new Set(codebaseInfo.files);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async detect() {
|
|
20
|
+
const results = [];
|
|
21
|
+
|
|
22
|
+
const checks = [
|
|
23
|
+
// JS / TS frontend
|
|
24
|
+
this._check('Next.js', ['next'], ['next.config.js', 'next.config.ts', 'next.config.mjs'], { buildDir: '.next', nodeVersion: '20' }),
|
|
25
|
+
this._check('Nuxt', ['nuxt', 'nuxt3'], ['nuxt.config.js', 'nuxt.config.ts'], { buildDir: '.nuxt', nodeVersion: '20' }),
|
|
26
|
+
this._check('SvelteKit', ['@sveltejs/kit'], ['svelte.config.js'], { buildDir: '.svelte-kit', nodeVersion: '20' }),
|
|
27
|
+
this._check('Remix', ['@remix-run/react', '@remix-run/node'], [], { nodeVersion: '20' }),
|
|
28
|
+
this._check('Astro', ['astro'], ['astro.config.mjs', 'astro.config.ts'], { buildDir: 'dist', nodeVersion: '20' }),
|
|
29
|
+
this._check('Vite', ['vite'], ['vite.config.js', 'vite.config.ts'], { buildDir: 'dist' }),
|
|
30
|
+
this._check('React', ['react', 'react-dom'], [], { buildDir: 'build' }),
|
|
31
|
+
this._check('Vue', ['vue'], [], { buildDir: 'dist' }),
|
|
32
|
+
this._check('Angular', ['@angular/core'], [], { buildDir: 'dist', nodeVersion: '20' }),
|
|
33
|
+
this._check('Svelte', ['svelte'], ['svelte.config.js'], { buildDir: 'public' }),
|
|
34
|
+
this._check('Gatsby', ['gatsby'], [], { buildDir: 'public', nodeVersion: '20' }),
|
|
35
|
+
this._check('Ember', ['ember-cli'], [], {}),
|
|
36
|
+
// Node / backend
|
|
37
|
+
this._check('Express', ['express'], [], { isServer: true }),
|
|
38
|
+
this._check('Fastify', ['fastify'], [], { isServer: true }),
|
|
39
|
+
this._check('NestJS', ['@nestjs/core'], [], { isServer: true, nodeVersion: '20' }),
|
|
40
|
+
this._check('Hono', ['hono'], [], { isServer: true }),
|
|
41
|
+
this._check('Koa', ['koa'], [], { isServer: true }),
|
|
42
|
+
this._check('tRPC', ['@trpc/server', '@trpc/client'], [], { isServer: true }),
|
|
43
|
+
// Python
|
|
44
|
+
this._checkPython('Django', 'django', 'manage.py'),
|
|
45
|
+
this._checkPython('Flask', 'flask'),
|
|
46
|
+
this._checkPython('FastAPI', 'fastapi'),
|
|
47
|
+
// Ruby
|
|
48
|
+
this._checkRuby('Rails', 'rails'),
|
|
49
|
+
// Java / Kotlin
|
|
50
|
+
this._checkJVM('Spring Boot', 'spring-boot'),
|
|
51
|
+
// PHP
|
|
52
|
+
this._checkComposer('Laravel', 'laravel/framework'),
|
|
53
|
+
// Go
|
|
54
|
+
this._checkGo('Go', 'gin-gonic/gin'),
|
|
55
|
+
// Rust
|
|
56
|
+
this._checkRust('Rust'),
|
|
57
|
+
].filter(Boolean);
|
|
58
|
+
|
|
59
|
+
return checks
|
|
60
|
+
.filter((r) => r.confidence > 0)
|
|
61
|
+
.sort((a, b) => b.confidence - a.confidence);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── generic JS/TS checker ─────────────────────────────────────────────────
|
|
65
|
+
_check(name, depKeys, configFiles, meta = {}) {
|
|
66
|
+
let confidence = 0;
|
|
67
|
+
|
|
68
|
+
for (const dep of depKeys) {
|
|
69
|
+
if (this.deps[dep]) { confidence += 0.5; break; }
|
|
70
|
+
}
|
|
71
|
+
for (const cfg of configFiles) {
|
|
72
|
+
if (this.configs.has(cfg) || this.files.has(cfg)) { confidence += 0.4; break; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { name, confidence: Math.min(confidence, 1), ...meta };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_checkPython(name, pkg, markerFile) {
|
|
79
|
+
let confidence = 0;
|
|
80
|
+
const reqFiles = ['requirements.txt', 'Pipfile', 'pyproject.toml'];
|
|
81
|
+
for (const rf of reqFiles) {
|
|
82
|
+
const fullPath = path.join(this.root, rf);
|
|
83
|
+
if (fs.existsSync(fullPath)) {
|
|
84
|
+
const content = fs.readFileSync(fullPath, 'utf8').toLowerCase();
|
|
85
|
+
if (content.includes(pkg.toLowerCase())) { confidence += 0.7; break; }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (markerFile && this.files.has(markerFile)) confidence += 0.2;
|
|
89
|
+
return confidence > 0 ? { name, confidence: Math.min(confidence, 1), isServer: true, isPython: true } : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_checkRuby(name, gem) {
|
|
93
|
+
const gemfilePath = path.join(this.root, 'Gemfile');
|
|
94
|
+
if (!fs.existsSync(gemfilePath)) return null;
|
|
95
|
+
const content = fs.readFileSync(gemfilePath, 'utf8').toLowerCase();
|
|
96
|
+
const confidence = content.includes(gem.toLowerCase()) ? 0.9 : 0;
|
|
97
|
+
return confidence > 0 ? { name, confidence, isServer: true, isRuby: true } : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_checkJVM(name, keyword) {
|
|
101
|
+
const gradlePath = path.join(this.root, 'build.gradle');
|
|
102
|
+
const pomPath = path.join(this.root, 'pom.xml');
|
|
103
|
+
let confidence = 0;
|
|
104
|
+
for (const p of [gradlePath, pomPath]) {
|
|
105
|
+
if (fs.existsSync(p)) {
|
|
106
|
+
const content = fs.readFileSync(p, 'utf8').toLowerCase();
|
|
107
|
+
if (content.includes(keyword.toLowerCase())) { confidence = 0.9; break; }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return confidence > 0 ? { name, confidence, isServer: true, isJVM: true } : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_checkComposer(name, pkg) {
|
|
114
|
+
const composerPath = path.join(this.root, 'composer.json');
|
|
115
|
+
if (!fs.existsSync(composerPath)) return null;
|
|
116
|
+
try {
|
|
117
|
+
const composer = JSON.parse(fs.readFileSync(composerPath, 'utf8'));
|
|
118
|
+
const allDeps = { ...(composer.require || {}), ...(composer['require-dev'] || {}) };
|
|
119
|
+
const confidence = allDeps[pkg] ? 0.9 : 0;
|
|
120
|
+
return confidence > 0 ? { name, confidence, isServer: true, isPHP: true } : null;
|
|
121
|
+
} catch (_) { return null; }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_checkGo(name) {
|
|
125
|
+
const goMod = path.join(this.root, 'go.mod');
|
|
126
|
+
if (!fs.existsSync(goMod)) return null;
|
|
127
|
+
return { name, confidence: 0.9, isServer: true, isGo: true };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_checkRust(name) {
|
|
131
|
+
const cargoToml = path.join(this.root, 'Cargo.toml');
|
|
132
|
+
if (!fs.existsSync(cargoToml)) return null;
|
|
133
|
+
return { name, confidence: 0.9, isServer: true, isRust: true };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = FrameworkDetector;
|