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 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;