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.
@@ -0,0 +1,256 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Detects hosting platforms from config files, package.json deps, and directory structure.
8
+ * Each result: { name, confidence (0–1), deployCommand, secrets, notes }
9
+ */
10
+ class HostingDetector {
11
+ constructor(projectPath, codebaseInfo) {
12
+ this.root = projectPath;
13
+ this.info = codebaseInfo;
14
+ this.pkg = codebaseInfo.packageJson || {};
15
+ this.deps = {
16
+ ...((this.pkg.dependencies) || {}),
17
+ ...((this.pkg.devDependencies) || {}),
18
+ };
19
+ this.scripts = this.pkg.scripts || {};
20
+ this.configs = new Set(codebaseInfo.configFiles);
21
+ this.files = new Set(codebaseInfo.files);
22
+ }
23
+
24
+ async detect() {
25
+ const results = [];
26
+
27
+ const checks = [
28
+ this._checkFirebase(),
29
+ this._checkVercel(),
30
+ this._checkNetlify(),
31
+ this._checkRender(),
32
+ this._checkRailway(),
33
+ this._checkHeroku(),
34
+ this._checkGCPAppEngine(),
35
+ this._checkAWS(),
36
+ this._checkAzure(),
37
+ this._checkGitHubPages(),
38
+ this._checkDocker(),
39
+ ];
40
+
41
+ for (const result of checks) {
42
+ if (result && result.confidence > 0) {
43
+ results.push(result);
44
+ }
45
+ }
46
+
47
+ // Sort by confidence descending
48
+ results.sort((a, b) => b.confidence - a.confidence);
49
+
50
+ // De-duplicate by name
51
+ const seen = new Set();
52
+ return results.filter((r) => {
53
+ if (seen.has(r.name)) return false;
54
+ seen.add(r.name);
55
+ return true;
56
+ });
57
+ }
58
+
59
+ // ── individual checks ─────────────────────────────────────────────────────
60
+
61
+ _checkFirebase() {
62
+ let confidence = 0;
63
+ const notes = [];
64
+
65
+ if (this.configs.has('firebase.json')) { confidence += 0.6; notes.push('firebase.json found'); }
66
+ if (this.configs.has('.firebaserc')) { confidence += 0.3; notes.push('.firebaserc found'); }
67
+ if (this.deps['firebase-tools'] || this.deps['firebase']) { confidence += 0.2; notes.push('firebase dep'); }
68
+ if (Object.values(this.scripts).some((s) => s.includes('firebase deploy'))) { confidence += 0.3; notes.push('deploy script'); }
69
+ if (this.info.srcStructure.hasFunctions) { confidence += 0.1; }
70
+
71
+ // Detect what Firebase services are used
72
+ let deployTarget = 'hosting';
73
+ try {
74
+ const fbJson = JSON.parse(fs.readFileSync(path.join(this.root, 'firebase.json'), 'utf8'));
75
+ const services = [];
76
+ if (fbJson.hosting) services.push('hosting');
77
+ if (fbJson.functions) services.push('functions');
78
+ if (fbJson.firestore) services.push('firestore');
79
+ if (fbJson.storage) services.push('storage');
80
+ deployTarget = services.join(',') || 'hosting';
81
+ } catch (_) {}
82
+
83
+ return {
84
+ name: 'Firebase',
85
+ confidence: Math.min(confidence, 1),
86
+ deployCommand: `firebase deploy --only ${deployTarget}`,
87
+ secrets: ['FIREBASE_TOKEN'],
88
+ notes,
89
+ buildStep: this._detectBuildScript(),
90
+ };
91
+ }
92
+
93
+ _checkVercel() {
94
+ let confidence = 0;
95
+ const notes = [];
96
+
97
+ if (this.configs.has('vercel.json')) { confidence += 0.7; notes.push('vercel.json found'); }
98
+ if (this.configs.has('.vercel')) { confidence += 0.4; notes.push('.vercel dir found'); }
99
+ if (this.deps['vercel']) { confidence += 0.3; notes.push('vercel dep'); }
100
+ if (Object.values(this.scripts).some((s) => s.includes('vercel'))) { confidence += 0.3; notes.push('vercel script'); }
101
+
102
+ return {
103
+ name: 'Vercel',
104
+ confidence: Math.min(confidence, 1),
105
+ deployCommand: 'vercel --prod --token $VERCEL_TOKEN',
106
+ secrets: ['VERCEL_TOKEN', 'VERCEL_ORG_ID', 'VERCEL_PROJECT_ID'],
107
+ notes,
108
+ buildStep: this._detectBuildScript(),
109
+ };
110
+ }
111
+
112
+ _checkNetlify() {
113
+ let confidence = 0;
114
+ const notes = [];
115
+
116
+ if (this.configs.has('netlify.toml')) { confidence += 0.7; notes.push('netlify.toml found'); }
117
+ if (this.configs.has('_redirects')) { confidence += 0.2; notes.push('_redirects found'); }
118
+ if (this.deps['netlify-cli'] || this.deps['netlify']) { confidence += 0.3; notes.push('netlify dep'); }
119
+ if (Object.values(this.scripts).some((s) => s.includes('netlify'))) { confidence += 0.3; notes.push('netlify script'); }
120
+
121
+ let publishDir = 'dist';
122
+ try {
123
+ const toml = fs.readFileSync(path.join(this.root, 'netlify.toml'), 'utf8');
124
+ const match = toml.match(/publish\s*=\s*["']?([^"'\n]+)/);
125
+ if (match) publishDir = match[1].trim();
126
+ } catch (_) {}
127
+
128
+ return {
129
+ name: 'Netlify',
130
+ confidence: Math.min(confidence, 1),
131
+ deployCommand: `netlify deploy --prod --dir=${publishDir}`,
132
+ secrets: ['NETLIFY_AUTH_TOKEN', 'NETLIFY_SITE_ID'],
133
+ notes,
134
+ publishDir,
135
+ buildStep: this._detectBuildScript(),
136
+ };
137
+ }
138
+
139
+ _checkRender() {
140
+ let confidence = 0;
141
+ if (this.configs.has('render.yaml')) { confidence += 0.8; }
142
+ return {
143
+ name: 'Render',
144
+ confidence,
145
+ deployCommand: 'curl -X POST $RENDER_DEPLOY_HOOK_URL',
146
+ secrets: ['RENDER_DEPLOY_HOOK_URL'],
147
+ notes: ['render.yaml detected'],
148
+ };
149
+ }
150
+
151
+ _checkRailway() {
152
+ let confidence = 0;
153
+ if (this.configs.has('railway.json') || this.configs.has('railway.toml')) confidence += 0.8;
154
+ if (this.deps['@railway/cli']) confidence += 0.2;
155
+ return {
156
+ name: 'Railway',
157
+ confidence,
158
+ deployCommand: 'railway up',
159
+ secrets: ['RAILWAY_TOKEN'],
160
+ notes: [],
161
+ };
162
+ }
163
+
164
+ _checkHeroku() {
165
+ let confidence = 0;
166
+ if (this.configs.has('Procfile')) { confidence += 0.5; }
167
+ if (this.configs.has('heroku.yml')) { confidence += 0.5; }
168
+ if (this.deps['heroku']) { confidence += 0.2; }
169
+ return {
170
+ name: 'Heroku',
171
+ confidence,
172
+ deployCommand: 'git push heroku main',
173
+ secrets: ['HEROKU_API_KEY', 'HEROKU_APP_NAME'],
174
+ notes: [],
175
+ };
176
+ }
177
+
178
+ _checkGCPAppEngine() {
179
+ let confidence = 0;
180
+ if (this.configs.has('app.yaml')) { confidence += 0.7; }
181
+ if (this.deps['@google-cloud/functions-framework']) confidence += 0.2;
182
+ return {
183
+ name: 'GCP App Engine',
184
+ confidence,
185
+ deployCommand: 'gcloud app deploy',
186
+ secrets: ['GCP_PROJECT_ID', 'GCP_SA_KEY'],
187
+ notes: [],
188
+ };
189
+ }
190
+
191
+ _checkAWS() {
192
+ let confidence = 0;
193
+ if (this.configs.has('appspec.yml')) confidence += 0.5;
194
+ if (this.configs.has('serverless.yml') || this.configs.has('serverless.yaml')) confidence += 0.6;
195
+ if (this.configs.has('cdk.json')) confidence += 0.4;
196
+ if (this.deps['aws-sdk'] || this.deps['@aws-sdk/client-s3']) confidence += 0.15;
197
+ return {
198
+ name: 'AWS',
199
+ confidence: Math.min(confidence, 1),
200
+ deployCommand: 'aws s3 sync ./dist s3://$AWS_S3_BUCKET --delete',
201
+ secrets: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'],
202
+ notes: [],
203
+ };
204
+ }
205
+
206
+ _checkAzure() {
207
+ let confidence = 0;
208
+ if (this.files.has('.azure/pipelines.yml')) confidence += 0.5;
209
+ if (this.deps['@azure/core-http']) confidence += 0.2;
210
+ return {
211
+ name: 'Azure',
212
+ confidence,
213
+ deployCommand: 'az webapp up',
214
+ secrets: ['AZURE_CREDENTIALS'],
215
+ notes: [],
216
+ };
217
+ }
218
+
219
+ _checkGitHubPages() {
220
+ let confidence = 0;
221
+ const pkgHomepage = this.pkg.homepage || '';
222
+ if (pkgHomepage.includes('github.io')) { confidence += 0.6; }
223
+ if (this.deps['gh-pages']) { confidence += 0.4; }
224
+ if (Object.values(this.scripts).some((s) => s.includes('gh-pages'))) confidence += 0.3;
225
+ return {
226
+ name: 'GitHub Pages',
227
+ confidence: Math.min(confidence, 1),
228
+ deployCommand: null, // handled by actions/deploy-pages
229
+ secrets: [],
230
+ notes: [],
231
+ buildStep: this._detectBuildScript(),
232
+ };
233
+ }
234
+
235
+ _checkDocker() {
236
+ let confidence = 0;
237
+ if (this.configs.has('Dockerfile')) confidence += 0.5;
238
+ if (this.configs.has('docker-compose.yml') || this.configs.has('docker-compose.yaml')) confidence += 0.3;
239
+ return {
240
+ name: 'Docker',
241
+ confidence,
242
+ deployCommand: 'docker push $DOCKER_IMAGE',
243
+ secrets: ['DOCKER_USERNAME', 'DOCKER_PASSWORD'],
244
+ notes: [],
245
+ };
246
+ }
247
+
248
+ _detectBuildScript() {
249
+ const scripts = this.pkg.scripts || {};
250
+ if (scripts.build) return `npm run build`;
251
+ if (scripts['build:prod']) return `npm run build:prod`;
252
+ return null;
253
+ }
254
+ }
255
+
256
+ module.exports = HostingDetector;
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ const EXT_MAP = {
7
+ '.ts': 'TypeScript',
8
+ '.tsx': 'TypeScript',
9
+ '.js': 'JavaScript',
10
+ '.jsx': 'JavaScript',
11
+ '.mjs': 'JavaScript',
12
+ '.py': 'Python',
13
+ '.rb': 'Ruby',
14
+ '.go': 'Go',
15
+ '.rs': 'Rust',
16
+ '.java': 'Java',
17
+ '.kt': 'Kotlin',
18
+ '.scala': 'Scala',
19
+ '.php': 'PHP',
20
+ '.cs': 'C#',
21
+ '.cpp': 'C++',
22
+ '.c': 'C',
23
+ '.swift': 'Swift',
24
+ '.dart': 'Dart',
25
+ '.ex': 'Elixir',
26
+ '.exs': 'Elixir',
27
+ '.hs': 'Haskell',
28
+ '.clj': 'Clojure',
29
+ '.r': 'R',
30
+ '.R': 'R',
31
+ '.lua': 'Lua',
32
+ '.jl': 'Julia',
33
+ };
34
+
35
+ class LanguageDetector {
36
+ constructor(projectPath, codebaseInfo) {
37
+ this.root = projectPath;
38
+ this.info = codebaseInfo;
39
+ }
40
+
41
+ async detect() {
42
+ const counts = {};
43
+
44
+ for (const file of this.info.files) {
45
+ const ext = path.extname(file).toLowerCase();
46
+ const lang = EXT_MAP[ext] || EXT_MAP[path.extname(file)];
47
+ if (lang) {
48
+ counts[lang] = (counts[lang] || 0) + 1;
49
+ }
50
+ }
51
+
52
+ // Package manager hints
53
+ const pkg = this.info.packageJson;
54
+ if (pkg) counts['JavaScript'] = (counts['JavaScript'] || 0) + 5;
55
+
56
+ const results = Object.entries(counts)
57
+ .map(([name, fileCount]) => ({
58
+ name,
59
+ fileCount,
60
+ confidence: Math.min(fileCount / 10, 1),
61
+ packageManager: this._packageManager(name),
62
+ nodeVersion: this._nodeVersion(name),
63
+ }))
64
+ .sort((a, b) => b.fileCount - a.fileCount);
65
+
66
+ // Normalise: if TS is present, suppress JS unless JS files are dominant
67
+ const hasTS = results.find((r) => r.name === 'TypeScript');
68
+ const hasJS = results.find((r) => r.name === 'JavaScript');
69
+ if (hasTS && hasJS && hasTS.fileCount >= hasJS.fileCount * 0.3) {
70
+ return results.filter((r) => r.name !== 'JavaScript');
71
+ }
72
+
73
+ return results;
74
+ }
75
+
76
+ _packageManager(lang) {
77
+ const lockFiles = this.info.lockFiles;
78
+ if (lang === 'JavaScript' || lang === 'TypeScript') {
79
+ if (lockFiles.includes('pnpm-lock.yaml')) return 'pnpm';
80
+ if (lockFiles.includes('yarn.lock')) return 'yarn';
81
+ if (lockFiles.includes('bun.lockb')) return 'bun';
82
+ return 'npm';
83
+ }
84
+ if (lang === 'Python') {
85
+ if (lockFiles.includes('poetry.lock')) return 'poetry';
86
+ if (lockFiles.includes('Pipfile.lock')) return 'pipenv';
87
+ return 'pip';
88
+ }
89
+ if (lang === 'Ruby') return 'bundler';
90
+ if (lang === 'Go') return 'go mod';
91
+ if (lang === 'Rust') return 'cargo';
92
+ if (lang === 'Java') {
93
+ if (fs.existsSync(path.join(this.root, 'pom.xml'))) return 'maven';
94
+ return 'gradle';
95
+ }
96
+ if (lang === 'PHP') return 'composer';
97
+ return null;
98
+ }
99
+
100
+ _nodeVersion(lang) {
101
+ if (lang !== 'JavaScript' && lang !== 'TypeScript') return null;
102
+ const pkg = this.info.packageJson;
103
+ if (pkg && pkg.engines && pkg.engines.node) {
104
+ const match = pkg.engines.node.match(/(\d+)/);
105
+ if (match) return match[1];
106
+ }
107
+ try {
108
+ const nvmrc = fs.readFileSync(path.join(this.root, '.nvmrc'), 'utf8').trim();
109
+ const match = nvmrc.match(/(\d+)/);
110
+ if (match) return match[1];
111
+ } catch (_) {}
112
+ return '20';
113
+ }
114
+ }
115
+
116
+ module.exports = LanguageDetector;
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ class TestingDetector {
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.scripts = this.pkg.scripts || {};
16
+ this.configs = new Set(codebaseInfo.configFiles);
17
+ this.files = new Set(codebaseInfo.files);
18
+ }
19
+
20
+ async detect() {
21
+ const results = [];
22
+
23
+ const checks = [
24
+ this._checkJest(),
25
+ this._checkVitest(),
26
+ this._checkMocha(),
27
+ this._checkCypress(),
28
+ this._checkPlaywright(),
29
+ this._checkStorybook(),
30
+ this._checkPytest(),
31
+ this._checkRspec(),
32
+ this._checkGo(),
33
+ this._checkCargo(),
34
+ this._checkPHPUnit(),
35
+ this._checkJUnit(),
36
+ ];
37
+
38
+ for (const c of checks) {
39
+ if (c && c.confidence > 0) results.push(c);
40
+ }
41
+
42
+ return results.sort((a, b) => b.confidence - a.confidence);
43
+ }
44
+
45
+ _checkJest() {
46
+ let conf = 0;
47
+ if (this.deps['jest'] || this.deps['@jest/core']) conf += 0.5;
48
+ if (this.configs.has('jest.config.js') || this.configs.has('jest.config.ts')) conf += 0.4;
49
+ if (this.scripts.test && this.scripts.test.includes('jest')) conf += 0.2;
50
+ return { name: 'Jest', confidence: Math.min(conf, 1), command: this._testScript('jest') || 'npx jest --coverage', type: 'unit' };
51
+ }
52
+
53
+ _checkVitest() {
54
+ let conf = 0;
55
+ if (this.deps['vitest']) conf += 0.6;
56
+ if (this.configs.has('vitest.config.js') || this.configs.has('vitest.config.ts')) conf += 0.4;
57
+ return { name: 'Vitest', confidence: Math.min(conf, 1), command: this._testScript('vitest') || 'npx vitest run --coverage', type: 'unit' };
58
+ }
59
+
60
+ _checkMocha() {
61
+ let conf = 0;
62
+ if (this.deps['mocha']) conf += 0.5;
63
+ if (this.configs.has('.mocharc.js') || this.configs.has('.mocharc.yml')) conf += 0.4;
64
+ return { name: 'Mocha', confidence: Math.min(conf, 1), command: 'npx mocha', type: 'unit' };
65
+ }
66
+
67
+ _checkCypress() {
68
+ let conf = 0;
69
+ if (this.deps['cypress']) conf += 0.6;
70
+ if (this.configs.has('cypress.config.js') || this.configs.has('cypress.config.ts')) conf += 0.4;
71
+ if (this.files.has('cypress/e2e') || [...this.files].some((f) => f.startsWith('cypress/'))) conf += 0.2;
72
+ return { name: 'Cypress', confidence: Math.min(conf, 1), command: 'npx cypress run', type: 'e2e' };
73
+ }
74
+
75
+ _checkPlaywright() {
76
+ let conf = 0;
77
+ if (this.deps['@playwright/test']) conf += 0.6;
78
+ if (this.configs.has('playwright.config.js') || this.configs.has('playwright.config.ts')) conf += 0.4;
79
+ return { name: 'Playwright', confidence: Math.min(conf, 1), command: 'npx playwright test', type: 'e2e' };
80
+ }
81
+
82
+ _checkStorybook() {
83
+ let conf = 0;
84
+ if (this.deps['@storybook/react'] || this.deps['@storybook/vue3'] || this.deps['storybook']) conf += 0.6;
85
+ const hasStories = [...this.files].some((f) => f.endsWith('.stories.tsx') || f.endsWith('.stories.jsx') || f.endsWith('.stories.ts') || f.endsWith('.stories.js'));
86
+ if (hasStories) conf += 0.3;
87
+ return { name: 'Storybook', confidence: Math.min(conf, 1), command: 'npx storybook build', type: 'visual' };
88
+ }
89
+
90
+ _checkPytest() {
91
+ let conf = 0;
92
+ const reqPath = path.join(this.root, 'requirements.txt');
93
+ if (fs.existsSync(reqPath) && fs.readFileSync(reqPath, 'utf8').toLowerCase().includes('pytest')) conf += 0.6;
94
+ if (this.files.has('pytest.ini') || this.files.has('conftest.py') || this.configs.has('conftest.py')) conf += 0.3;
95
+ const pyprojectPath = path.join(this.root, 'pyproject.toml');
96
+ if (fs.existsSync(pyprojectPath) && fs.readFileSync(pyprojectPath, 'utf8').includes('pytest')) conf += 0.3;
97
+ return { name: 'Pytest', confidence: Math.min(conf, 1), command: 'pytest --cov', type: 'unit', isPython: true };
98
+ }
99
+
100
+ _checkRspec() {
101
+ const gemfilePath = path.join(this.root, 'Gemfile');
102
+ if (!fs.existsSync(gemfilePath)) return { name: 'RSpec', confidence: 0 };
103
+ const content = fs.readFileSync(gemfilePath, 'utf8').toLowerCase();
104
+ const conf = content.includes('rspec') ? 0.9 : 0;
105
+ return { name: 'RSpec', confidence: conf, command: 'bundle exec rspec', type: 'unit', isRuby: true };
106
+ }
107
+
108
+ _checkGo() {
109
+ const conf = this.info.lockFiles.includes('go.sum') ? 0.9 : 0;
110
+ return { name: 'Go Test', confidence: conf, command: 'go test ./... -coverprofile=coverage.out', type: 'unit', isGo: true };
111
+ }
112
+
113
+ _checkCargo() {
114
+ const cargoPath = path.join(this.root, 'Cargo.toml');
115
+ const conf = fs.existsSync(cargoPath) ? 0.9 : 0;
116
+ return { name: 'Cargo Test', confidence: conf, command: 'cargo test', type: 'unit', isRust: true };
117
+ }
118
+
119
+ _checkPHPUnit() {
120
+ const conf = this.configs.has('phpunit.xml') ? 0.9 : 0;
121
+ return { name: 'PHPUnit', confidence: conf, command: 'vendor/bin/phpunit', type: 'unit', isPHP: true };
122
+ }
123
+
124
+ _checkJUnit() {
125
+ const conf = fs.existsSync(path.join(this.root, 'pom.xml')) ? 0.6 : 0;
126
+ return { name: 'JUnit', confidence: conf, command: 'mvn test', type: 'unit', isJVM: true };
127
+ }
128
+
129
+ _testScript(tool) {
130
+ const scripts = this.pkg.scripts || {};
131
+ if (scripts.test && scripts.test.includes(tool)) return 'npm test';
132
+ if (scripts['test:ci']) return 'npm run test:ci';
133
+ return null;
134
+ }
135
+ }
136
+
137
+ module.exports = TestingDetector;