cistack 3.1.0 → 4.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.
@@ -43,17 +43,17 @@ class MonorepoAnalyzer {
43
43
 
44
44
  for (const relDir of matches) {
45
45
  if (seen.has(relDir)) continue;
46
- seen.add(relDir);
47
46
 
48
47
  const absPath = path.join(this.root, relDir);
49
48
  const pkgJsonPath = path.join(absPath, 'package.json');
50
- let pkgJson = null;
49
+ if (!fs.existsSync(pkgJsonPath)) continue;
51
50
 
52
- if (fs.existsSync(pkgJsonPath)) {
53
- try {
54
- pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
55
- } catch (_) {}
56
- }
51
+ seen.add(relDir);
52
+
53
+ let pkgJson = null;
54
+ try {
55
+ pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
56
+ } catch (_) {}
57
57
 
58
58
  const name = (pkgJson && pkgJson.name) || path.basename(relDir);
59
59
 
@@ -25,6 +25,7 @@ class WorkflowAnalyzer {
25
25
  'docker/build-push-action': 'v5',
26
26
  'docker/metadata-action': 'v5',
27
27
  'pnpm/action-setup': 'v3',
28
+ 'oven-sh/setup-bun': 'v2',
28
29
  'codecov/codecov-action': 'v4',
29
30
  'github/codeql-action/init': 'v3',
30
31
  'github/codeql-action/analyze': 'v3',
@@ -70,6 +71,15 @@ class WorkflowAnalyzer {
70
71
 
71
72
  _auditFile(filename, parsed, rawContent) {
72
73
  const issues = [];
74
+ if (!parsed || typeof parsed !== 'object') {
75
+ issues.push({
76
+ type: 'invalid_workflow',
77
+ severity: 'high',
78
+ message: 'Workflow file does not parse to a valid YAML object',
79
+ fix: 'Ensure the workflow root is a YAML mapping with top-level workflow keys',
80
+ });
81
+ return issues;
82
+ }
73
83
 
74
84
  // 1. Check for concurrency
75
85
  if (!parsed.concurrency) {
@@ -85,7 +95,6 @@ class WorkflowAnalyzer {
85
95
  const actionRegex = /uses:\s*([\w\-\/]+)@([\w\.]+)/g;
86
96
  let match;
87
97
  while ((match = actionRegex.exec(rawContent)) !== null) {
88
- const fullAction = match[0];
89
98
  const actionName = match[1];
90
99
  const currentVersion = match[2];
91
100
 
@@ -148,7 +157,6 @@ class WorkflowAnalyzer {
148
157
  for (const filename of files) {
149
158
  const filePath = path.join(this.workflowsDir, filename);
150
159
  let content = fs.readFileSync(filePath, 'utf8');
151
- let originalContent = content;
152
160
  let fileChanges = 0;
153
161
 
154
162
  for (const [action, latest] of Object.entries(this.latestVersions)) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const chalk = require('chalk');
5
6
 
6
7
  /**
7
8
  * Loads cistack.config.js (or .cjs / .mjs) from the project root.
@@ -12,6 +13,7 @@ const path = require('path');
12
13
  * packageManager – override detected PM: 'npm'|'yarn'|'pnpm'|'bun'
13
14
  * hosting – array of hosting names to force e.g. ['Firebase']
14
15
  * branches – branches to run CI on e.g. ['main', 'staging']
16
+ * (default: detected git default branch, then main/master/develop)
15
17
  * cache – { npm: bool, cargo: bool, pip: bool, ... } enable/disable caches
16
18
  * monorepo – { perPackage: bool } generate one file per workspace
17
19
  * release – { tool: 'semantic-release'|'changesets'|'standard-version'|'release-it' }
@@ -23,7 +25,7 @@ class ConfigLoader {
23
25
  this.projectPath = projectPath;
24
26
  }
25
27
 
26
- load() {
28
+ async load() {
27
29
  const candidates = [
28
30
  'cistack.config.js',
29
31
  'cistack.config.cjs',
@@ -40,10 +42,11 @@ class ConfigLoader {
40
42
  // Since this is a CLI, we can afford a bit of hackiness or just support CommonJS primarily.
41
43
 
42
44
  let cfg;
43
- if (candidate.endsWith('.mjs')) {
44
- // Very basic support for .mjs if the environment supports it, but require usually fails.
45
- // We'll try to use the fact that many environments now support require('.mjs') or just warn.
46
- cfg = require(fullPath);
45
+ if (candidate.endsWith('.mjs') || (candidate.endsWith('.js') && !candidate.endsWith('.cjs'))) {
46
+ // Dynamic import() for ESM support
47
+ const modulePath = path.resolve(this.projectPath, candidate);
48
+ const imported = await import(`file://${modulePath}`);
49
+ cfg = imported.default || imported;
47
50
  } else {
48
51
  delete require.cache[require.resolve(fullPath)];
49
52
  cfg = require(fullPath);
@@ -103,6 +106,30 @@ class ConfigLoader {
103
106
  if (!cfg || Object.keys(cfg).length === 0) return detected;
104
107
 
105
108
  const result = { ...detected };
109
+ const runScript = (scriptName) => {
110
+ const packageManager =
111
+ (result.languages && result.languages[0] && result.languages[0].packageManager) ||
112
+ cfg.packageManager ||
113
+ 'npm';
114
+ if (packageManager === 'yarn') return `yarn run ${scriptName}`;
115
+ if (packageManager === 'pnpm') return `pnpm run ${scriptName}`;
116
+ if (packageManager === 'bun') return `bun run ${scriptName}`;
117
+ return `npm run ${scriptName}`;
118
+ };
119
+ const canonicalHostingNames = {
120
+ firebase: 'Firebase',
121
+ vercel: 'Vercel',
122
+ netlify: 'Netlify',
123
+ aws: 'AWS',
124
+ 'gcp app engine': 'GCP App Engine',
125
+ gcp: 'GCP App Engine',
126
+ azure: 'Azure',
127
+ heroku: 'Heroku',
128
+ render: 'Render',
129
+ railway: 'Railway',
130
+ 'github pages': 'GitHub Pages',
131
+ 'github-pages': 'GitHub Pages',
132
+ };
106
133
 
107
134
  // 1. Language overrides (Node version, package manager)
108
135
  if (cfg.nodeVersion && result.languages && result.languages.length > 0) {
@@ -114,23 +141,41 @@ class ConfigLoader {
114
141
  }
115
142
 
116
143
  if (cfg.packageManager && result.languages && result.languages.length > 0) {
117
- result.languages = result.languages.map((l, i) =>
118
- i === 0 ? { ...l, packageManager: cfg.packageManager, manual: true } : l
144
+ result.languages = result.languages.map((l) =>
145
+ ({ ...l, packageManager: cfg.packageManager, manual: true })
119
146
  );
120
147
  }
121
148
 
122
149
  // 2. Hosting overrides
123
150
  if (cfg.hosting) {
124
151
  const hostingNames = Array.isArray(cfg.hosting) ? cfg.hosting : [cfg.hosting];
152
+ const hostingSecrets = {
153
+ Firebase: ['FIREBASE_SERVICE_ACCOUNT'],
154
+ Vercel: ['VERCEL_TOKEN', 'VERCEL_ORG_ID', 'VERCEL_PROJECT_ID'],
155
+ Netlify: ['NETLIFY_AUTH_TOKEN', 'NETLIFY_SITE_ID'],
156
+ AWS: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'AWS_S3_BUCKET', 'CLOUDFRONT_DISTRIBUTION_ID'],
157
+ 'GCP App Engine': ['GCP_SA_KEY'],
158
+ Azure: ['AZURE_APP_NAME', 'AZURE_WEBAPP_PUBLISH_PROFILE'],
159
+ Heroku: ['HEROKU_API_KEY', 'HEROKU_APP_NAME', 'HEROKU_EMAIL'],
160
+ Render: ['RENDER_DEPLOY_HOOK_URL'],
161
+ Railway: ['RAILWAY_TOKEN'],
162
+ 'GitHub Pages': [],
163
+ };
125
164
  result.hosting = hostingNames.map((name) => ({
126
- name,
165
+ name: canonicalHostingNames[String(name).toLowerCase()] || name,
127
166
  confidence: 1.0,
128
167
  manual: true,
129
- secrets: [],
168
+ secrets: hostingSecrets[canonicalHostingNames[String(name).toLowerCase()] || name] || [],
130
169
  notes: ['set via cistack.config.js'],
131
170
  }));
132
171
  }
133
172
 
173
+ // 2b. Release override
174
+ if (cfg.release) {
175
+ const releaseOverride = typeof cfg.release === 'string' ? { tool: cfg.release } : cfg.release;
176
+ result.releaseInfo = { ...(result.releaseInfo || {}), ...releaseOverride };
177
+ }
178
+
134
179
  // 3. Framework overrides
135
180
  if (cfg.frameworks) {
136
181
  const frameworkNames = Array.isArray(cfg.frameworks) ? cfg.frameworks : [cfg.frameworks];
@@ -149,10 +194,20 @@ class ConfigLoader {
149
194
  confidence: 1.0,
150
195
  manual: true,
151
196
  type: 'unit', // default
152
- command: `npm run test` // fallback
197
+ command: runScript('test') // fallback
153
198
  }));
154
199
  }
155
200
 
201
+ // 5. Extra documented secrets
202
+ if (cfg.secrets) {
203
+ const extraSecrets = Array.isArray(cfg.secrets) ? cfg.secrets : [cfg.secrets];
204
+ const envVars = result.envVars || { secrets: [], public: [], all: [], sourceFile: null };
205
+ result.envVars = {
206
+ ...envVars,
207
+ secrets: [...new Set([...(envVars.secrets || []), ...extraSecrets])],
208
+ };
209
+ }
210
+
156
211
  // Pass through raw extras for generators to consume
157
212
  result._config = { ...(result._config || {}), ...cfg };
158
213
 
@@ -17,26 +17,24 @@ class FrameworkDetector {
17
17
  }
18
18
 
19
19
  async detect() {
20
- const results = [];
21
-
22
- const checks = [
20
+ const results = [
23
21
  // 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'], [], {}),
22
+ this._check('Next.js', ['next'], ['next.config.js', 'next.config.ts', 'next.config.mjs'], { buildDir: '.next', priority: 10 }),
23
+ this._check('Nuxt', ['nuxt', 'nuxt3'], ['nuxt.config.js', 'nuxt.config.ts'], { buildDir: '.nuxt', priority: 10 }),
24
+ this._check('SvelteKit', ['@sveltejs/kit'], ['svelte.config.js'], { buildDir: '.svelte-kit', priority: 10 }),
25
+ this._check('Remix', ['@remix-run/react', '@remix-run/node'], [], { priority: 10 }),
26
+ this._check('Astro', ['astro'], ['astro.config.mjs', 'astro.config.ts'], { buildDir: 'dist', priority: 10 }),
27
+ this._check('Vite', ['vite'], ['vite.config.js', 'vite.config.ts'], { buildDir: 'dist', priority: 5 }),
28
+ this._check('React', ['react', 'react-dom'], [], { buildDir: 'build', priority: 1 }),
29
+ this._check('Vue', ['vue'], [], { buildDir: 'dist', priority: 1 }),
30
+ this._check('Angular', ['@angular/core'], [], { buildDir: 'dist', priority: 1 }),
31
+ this._check('Svelte', ['svelte'], ['svelte.config.js'], { buildDir: 'public', priority: 1 }),
32
+ this._check('Gatsby', ['gatsby'], [], { buildDir: 'public', priority: 1 }),
33
+ this._check('Ember', ['ember-cli'], [], { priority: 1 }),
36
34
  // Node / backend
37
35
  this._check('Express', ['express'], [], { isServer: true }),
38
36
  this._check('Fastify', ['fastify'], [], { isServer: true }),
39
- this._check('NestJS', ['@nestjs/core'], [], { isServer: true, nodeVersion: '20' }),
37
+ this._check('NestJS', ['@nestjs/core'], [], { isServer: true }),
40
38
  this._check('Hono', ['hono'], [], { isServer: true }),
41
39
  this._check('Koa', ['koa'], [], { isServer: true }),
42
40
  this._check('tRPC', ['@trpc/server', '@trpc/client'], [], { isServer: true }),
@@ -47,7 +45,7 @@ class FrameworkDetector {
47
45
  // Ruby
48
46
  this._checkRuby('Rails', 'rails'),
49
47
  // Java / Kotlin
50
- this._checkJVM('Spring Boot', 'spring-boot'),
48
+ this._checkJVM('Spring Boot', ['spring-boot', 'org.springframework.boot']),
51
49
  // PHP
52
50
  this._checkComposer('Laravel', 'laravel/framework'),
53
51
  // Go
@@ -56,8 +54,12 @@ class FrameworkDetector {
56
54
  this._checkRust('Rust'),
57
55
  ].filter(Boolean);
58
56
 
59
- return checks
60
- .filter((r) => r.confidence > 0)
57
+ // Filter by confidence and priority
58
+ const filtered = results.filter((r) => r.confidence > 0);
59
+ const hasMeta = filtered.some(r => (r.priority || 0) >= 10);
60
+
61
+ return filtered
62
+ .filter(r => !hasMeta || (r.priority || 0) >= 10)
61
63
  .sort((a, b) => b.confidence - a.confidence);
62
64
  }
63
65
 
@@ -115,22 +117,24 @@ class FrameworkDetector {
115
117
  return confidence > 0 ? { name, confidence, isServer: true, isRuby: true, reasons } : null;
116
118
  }
117
119
 
118
- _checkJVM(name, keyword) {
120
+ _checkJVM(name, keywords) {
119
121
  const gradlePath = path.join(this.root, 'build.gradle');
122
+ const gradleKtsPath = path.join(this.root, 'build.gradle.kts');
120
123
  const pomPath = path.join(this.root, 'pom.xml');
121
124
  let confidence = 0;
122
125
  let foundIn = '';
123
- for (const p of [gradlePath, pomPath]) {
126
+ const candidates = Array.isArray(keywords) ? keywords : [keywords];
127
+ for (const p of [gradlePath, gradleKtsPath, pomPath]) {
124
128
  if (fs.existsSync(p)) {
125
129
  const content = fs.readFileSync(p, 'utf8').toLowerCase();
126
- if (content.includes(keyword.toLowerCase())) {
127
- confidence = 0.9;
130
+ if (candidates.some((keyword) => content.includes(keyword.toLowerCase()))) {
131
+ confidence = 0.9;
128
132
  foundIn = path.basename(p);
129
- break;
133
+ break;
130
134
  }
131
135
  }
132
136
  }
133
- const reasons = confidence > 0 ? [`found ${keyword} in ${foundIn}`] : [];
137
+ const reasons = confidence > 0 ? [`found ${name} markers in ${foundIn}`] : [];
134
138
  return confidence > 0 ? { name, confidence, isServer: true, isJVM: true, reasons } : null;
135
139
  }
136
140
 
@@ -84,7 +84,7 @@ class HostingDetector {
84
84
  name: 'Firebase',
85
85
  confidence: Math.min(confidence, 1),
86
86
  deployCommand: `firebase deploy --only ${deployTarget}`,
87
- secrets: ['FIREBASE_TOKEN'],
87
+ secrets: ['FIREBASE_SERVICE_ACCOUNT'],
88
88
  reasons,
89
89
  buildStep: this._detectBuildScript(),
90
90
  };
@@ -102,7 +102,7 @@ class HostingDetector {
102
102
  return {
103
103
  name: 'Vercel',
104
104
  confidence: Math.min(confidence, 1),
105
- deployCommand: 'vercel --prod --token $VERCEL_TOKEN',
105
+ deployCommand: 'vercel deploy --prod',
106
106
  secrets: ['VERCEL_TOKEN', 'VERCEL_ORG_ID', 'VERCEL_PROJECT_ID'],
107
107
  reasons,
108
108
  buildStep: this._detectBuildScript(),
@@ -118,7 +118,7 @@ class HostingDetector {
118
118
  if (this.deps['netlify-cli'] || this.deps['netlify']) { confidence += 0.3; reasons.push('netlify dependency found'); }
119
119
  if (Object.values(this.scripts).some((s) => s.includes('netlify'))) { confidence += 0.3; reasons.push('netlify script found'); }
120
120
 
121
- let publishDir = 'dist';
121
+ let publishDir = null;
122
122
  try {
123
123
  const toml = fs.readFileSync(path.join(this.root, 'netlify.toml'), 'utf8');
124
124
  const match = toml.match(/publish\s*=\s*["']?([^"'\n]+)/);
@@ -173,7 +173,7 @@ class HostingDetector {
173
173
  name: 'Heroku',
174
174
  confidence,
175
175
  deployCommand: 'git push heroku main',
176
- secrets: ['HEROKU_API_KEY', 'HEROKU_APP_NAME'],
176
+ secrets: ['HEROKU_API_KEY', 'HEROKU_APP_NAME', 'HEROKU_EMAIL'],
177
177
  reasons,
178
178
  };
179
179
  }
@@ -187,7 +187,7 @@ class HostingDetector {
187
187
  name: 'GCP App Engine',
188
188
  confidence,
189
189
  deployCommand: 'gcloud app deploy',
190
- secrets: ['GCP_PROJECT_ID', 'GCP_SA_KEY'],
190
+ secrets: ['GCP_SA_KEY'],
191
191
  reasons,
192
192
  };
193
193
  }
@@ -199,11 +199,12 @@ class HostingDetector {
199
199
  if (this.configs.has('serverless.yml') || this.configs.has('serverless.yaml')) { confidence += 0.6; reasons.push('serverless.yml found'); }
200
200
  if (this.configs.has('cdk.json')) { confidence += 0.4; reasons.push('cdk.json found'); }
201
201
  if (this.deps['aws-sdk'] || this.deps['@aws-sdk/client-s3']) { confidence += 0.15; reasons.push('aws-sdk found'); }
202
+ const buildDir = this._detectBuildDir();
202
203
  return {
203
204
  name: 'AWS',
204
205
  confidence: Math.min(confidence, 1),
205
- deployCommand: 'aws s3 sync ./dist s3://$AWS_S3_BUCKET --delete',
206
- secrets: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'],
206
+ deployCommand: `aws s3 sync ./${buildDir} s3://$AWS_S3_BUCKET --delete`,
207
+ secrets: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'AWS_S3_BUCKET', 'CLOUDFRONT_DISTRIBUTION_ID'],
207
208
  reasons,
208
209
  };
209
210
  }
@@ -211,13 +212,16 @@ class HostingDetector {
211
212
  _checkAzure() {
212
213
  let confidence = 0;
213
214
  const reasons = [];
214
- if (this.files.has('.azure/pipelines.yml')) { confidence += 0.5; reasons.push('.azure/pipelines.yml found'); }
215
+ if (this.files.has('.azure/pipelines.yml') || this.files.has('azure/pipelines.yml')) {
216
+ confidence += 0.5;
217
+ reasons.push('azure/pipelines.yml found');
218
+ }
215
219
  if (this.deps['@azure/core-http']) { confidence += 0.2; reasons.push('azure core-http found'); }
216
220
  return {
217
221
  name: 'Azure',
218
222
  confidence,
219
223
  deployCommand: 'az webapp up',
220
- secrets: ['AZURE_CREDENTIALS'],
224
+ secrets: ['AZURE_APP_NAME', 'AZURE_WEBAPP_PUBLISH_PROFILE'],
221
225
  reasons,
222
226
  };
223
227
  }
@@ -242,7 +246,10 @@ class HostingDetector {
242
246
  _checkDocker() {
243
247
  let confidence = 0;
244
248
  const reasons = [];
245
- if (this.configs.has('Dockerfile')) { confidence += 0.5; reasons.push('Dockerfile found'); }
249
+ if (this.configs.has('Dockerfile') || this.configs.has('Dockerfile.prod')) {
250
+ confidence += 0.5;
251
+ reasons.push(this.configs.has('Dockerfile') ? 'Dockerfile found' : 'Dockerfile.prod found');
252
+ }
246
253
  if (this.configs.has('docker-compose.yml') || this.configs.has('docker-compose.yaml')) { confidence += 0.3; reasons.push('docker-compose.yml found'); }
247
254
  return {
248
255
  name: 'Docker',
@@ -259,6 +266,14 @@ class HostingDetector {
259
266
  if (scripts['build:prod']) return `npm run build:prod`;
260
267
  return null;
261
268
  }
269
+
270
+ _detectBuildDir() {
271
+ // If codebase has common build dirs
272
+ const dirs = this.info.srcStructure.topDirs || [];
273
+ if (dirs.includes('dist')) return 'dist';
274
+ if (dirs.includes('build')) return 'build';
275
+ return 'dist'; // fallback
276
+ }
262
277
  }
263
278
 
264
279
  module.exports = HostingDetector;
@@ -78,7 +78,7 @@ class LanguageDetector {
78
78
  if (lang === 'JavaScript' || lang === 'TypeScript') {
79
79
  if (lockFiles.includes('pnpm-lock.yaml')) return 'pnpm';
80
80
  if (lockFiles.includes('yarn.lock')) return 'yarn';
81
- if (lockFiles.includes('bun.lockb')) return 'bun';
81
+ if (lockFiles.includes('bun.lock') || lockFiles.includes('bun.lockb')) return 'bun';
82
82
  return 'npm';
83
83
  }
84
84
  if (lang === 'Python') {
@@ -89,7 +89,7 @@ class LanguageDetector {
89
89
  if (lang === 'Ruby') return 'bundler';
90
90
  if (lang === 'Go') return 'go mod';
91
91
  if (lang === 'Rust') return 'cargo';
92
- if (lang === 'Java') {
92
+ if (lang === 'Java' || lang === 'Kotlin') {
93
93
  if (fs.existsSync(path.join(this.root, 'pom.xml'))) return 'maven';
94
94
  return 'gradle';
95
95
  }
@@ -24,6 +24,7 @@ class ReleaseDetector {
24
24
  ...(this.pkg.devDependencies || {}),
25
25
  };
26
26
  this.scripts = this.pkg.scripts || {};
27
+ this.packageManager = this._detectPackageManager();
27
28
  }
28
29
 
29
30
  async detect() {
@@ -55,12 +56,13 @@ class ReleaseDetector {
55
56
  // --- release-it ---
56
57
  if (this.deps['release-it']) {
57
58
  const config = this._loadReleaseItConfig();
59
+ const publishToNpm = !!(config && config.npm && config.npm.publish !== false);
58
60
  return {
59
61
  tool: 'release-it',
60
62
  command: 'npx release-it --ci',
61
63
  config,
62
- publishToNpm: !!(config && config.npm && config.npm.publish !== false),
63
- requiresNpmToken: true,
64
+ publishToNpm,
65
+ requiresNpmToken: publishToNpm,
64
66
  };
65
67
  }
66
68
 
@@ -77,9 +79,10 @@ class ReleaseDetector {
77
79
  // --- fallback: check scripts ---
78
80
  const releaseScript = this.scripts['release'] || this.scripts['version'];
79
81
  if (releaseScript) {
82
+ const scriptName = this.scripts['release'] ? 'release' : 'version';
80
83
  return {
81
84
  tool: 'custom',
82
- command: 'npm run release',
85
+ command: this._runScript(scriptName),
83
86
  publishToNpm: releaseScript.includes('publish'),
84
87
  requiresNpmToken: releaseScript.includes('publish'),
85
88
  };
@@ -90,14 +93,15 @@ class ReleaseDetector {
90
93
 
91
94
  _loadSemanticReleaseConfig() {
92
95
  // Try .releaserc, .releaserc.json, .releaserc.js, package.json#release
93
- const candidates = ['.releaserc', '.releaserc.json', '.releaserc.js', '.releaserc.yaml'];
96
+ const candidates = ['.releaserc', '.releaserc.json', '.releaserc.js', '.releaserc.cjs', '.releaserc.yaml', '.releaserc.yml'];
94
97
  for (const c of candidates) {
95
98
  const p = path.join(this.root, c);
96
99
  if (fs.existsSync(p)) {
97
100
  try {
101
+ if (c.endsWith('.js') || c.endsWith('.cjs')) return require(p);
98
102
  const raw = fs.readFileSync(p, 'utf8');
99
- if (c.endsWith('.js')) return require(p);
100
- return JSON.parse(raw);
103
+ const yaml = require('js-yaml');
104
+ return yaml.load(raw);
101
105
  } catch (_) {}
102
106
  }
103
107
  }
@@ -106,19 +110,36 @@ class ReleaseDetector {
106
110
  }
107
111
 
108
112
  _loadReleaseItConfig() {
109
- const candidates = ['.release-it.json', '.release-it.js', '.release-it.yaml'];
113
+ const candidates = ['.release-it.json', '.release-it.js', '.release-it.cjs', '.release-it.yaml', '.release-it.yml'];
110
114
  for (const c of candidates) {
111
115
  const p = path.join(this.root, c);
112
116
  if (fs.existsSync(p)) {
113
117
  try {
118
+ if (c.endsWith('.js') || c.endsWith('.cjs')) return require(p);
114
119
  const raw = fs.readFileSync(p, 'utf8');
115
- return JSON.parse(raw);
120
+ const yaml = require('js-yaml');
121
+ return yaml.load(raw);
116
122
  } catch (_) {}
117
123
  }
118
124
  }
119
125
  if (this.pkg['release-it']) return this.pkg['release-it'];
120
126
  return {};
121
127
  }
128
+
129
+ _detectPackageManager() {
130
+ const lockFiles = this.info.lockFiles || [];
131
+ if (lockFiles.includes('pnpm-lock.yaml')) return 'pnpm';
132
+ if (lockFiles.includes('yarn.lock')) return 'yarn';
133
+ if (lockFiles.includes('bun.lock') || lockFiles.includes('bun.lockb')) return 'bun';
134
+ return 'npm';
135
+ }
136
+
137
+ _runScript(name) {
138
+ if (this.packageManager === 'yarn') return `yarn run ${name}`;
139
+ if (this.packageManager === 'pnpm') return `pnpm run ${name}`;
140
+ if (this.packageManager === 'bun') return `bun run ${name}`;
141
+ return `npm run ${name}`;
142
+ }
122
143
  }
123
144
 
124
145
  module.exports = ReleaseDetector;
@@ -15,6 +15,7 @@ class TestingDetector {
15
15
  this.scripts = this.pkg.scripts || {};
16
16
  this.configs = new Set(codebaseInfo.configFiles);
17
17
  this.files = new Set(codebaseInfo.files);
18
+ this.packageManager = this._detectPackageManager();
18
19
  }
19
20
 
20
21
  async detect() {
@@ -128,10 +129,25 @@ class TestingDetector {
128
129
 
129
130
  _testScript(tool) {
130
131
  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';
132
+ if (scripts.test && scripts.test.includes(tool)) return this._runScript('test');
133
+ if (scripts['test:ci']) return this._runScript('test:ci');
133
134
  return null;
134
135
  }
136
+
137
+ _detectPackageManager() {
138
+ const lockFiles = this.info.lockFiles || [];
139
+ if (lockFiles.includes('pnpm-lock.yaml')) return 'pnpm';
140
+ if (lockFiles.includes('yarn.lock')) return 'yarn';
141
+ if (lockFiles.includes('bun.lock') || lockFiles.includes('bun.lockb')) return 'bun';
142
+ return 'npm';
143
+ }
144
+
145
+ _runScript(name) {
146
+ if (this.packageManager === 'yarn') return `yarn run ${name}`;
147
+ if (this.packageManager === 'pnpm') return `pnpm run ${name}`;
148
+ if (this.packageManager === 'bun') return `bun run ${name}`;
149
+ return `npm run ${name}`;
150
+ }
135
151
  }
136
152
 
137
153
  module.exports = TestingDetector;
@@ -6,7 +6,7 @@ const yaml = require('js-yaml');
6
6
  * Generates a .github/dependabot.yml based on detected ecosystems.
7
7
  *
8
8
  * Supported ecosystems:
9
- * npm, pip, cargo, bundler, go, maven, gradle, github-actions, composer, docker
9
+ * bun, npm, pip, cargo, bundler, go, maven, gradle, github-actions, composer, docker
10
10
  */
11
11
  class DependabotGenerator {
12
12
  constructor(codebaseInfo) {
@@ -18,12 +18,32 @@ class DependabotGenerator {
18
18
 
19
19
  generate() {
20
20
  const updates = [];
21
+ const hasDeps =
22
+ Object.keys(this.pkg.dependencies || {}).length > 0 ||
23
+ Object.keys(this.pkg.devDependencies || {}).length > 0;
24
+ const hasBunLock = this.lockFiles.has('bun.lock');
25
+
26
+ // ── bun ────────────────────────────────────────────────────────────────
27
+ if (hasBunLock) {
28
+ updates.push({
29
+ 'package-ecosystem': 'bun',
30
+ directory: '/',
31
+ schedule: { interval: 'weekly', day: 'monday' },
32
+ 'open-pull-requests-limit': 10,
33
+ groups: {
34
+ 'dev-dependencies': {
35
+ 'dependency-type': 'development',
36
+ 'update-types': ['minor', 'patch'],
37
+ },
38
+ },
39
+ });
40
+ }
21
41
 
22
42
  // ── npm ────────────────────────────────────────────────────────────────
23
- if (this.pkg.dependencies || this.pkg.devDependencies ||
43
+ if (!hasBunLock && (hasDeps ||
24
44
  this.lockFiles.has('package-lock.json') ||
25
45
  this.lockFiles.has('yarn.lock') ||
26
- this.lockFiles.has('pnpm-lock.yaml')) {
46
+ this.lockFiles.has('pnpm-lock.yaml'))) {
27
47
  updates.push({
28
48
  'package-ecosystem': 'npm',
29
49
  directory: '/',
@@ -112,6 +132,7 @@ class DependabotGenerator {
112
132
 
113
133
  // ── Docker ─────────────────────────────────────────────────────────────
114
134
  if (this.configFiles.has('Dockerfile') ||
135
+ this.configFiles.has('Dockerfile.prod') ||
115
136
  this.configFiles.has('docker-compose.yml') ||
116
137
  this.configFiles.has('docker-compose.yaml')) {
117
138
  updates.push({