cistack 3.2.0 → 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.
@@ -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,8 +13,10 @@ 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
19
+ * workflowLayout – 'single' | 'split' (default: 'single')
17
20
  * release – { tool: 'semantic-release'|'changesets'|'standard-version'|'release-it' }
18
21
  * secrets – extra secret names to document in workflow comments
19
22
  * outputDir – override default '.github/workflows'
@@ -23,7 +26,7 @@ class ConfigLoader {
23
26
  this.projectPath = projectPath;
24
27
  }
25
28
 
26
- load() {
29
+ async load() {
27
30
  const candidates = [
28
31
  'cistack.config.js',
29
32
  'cistack.config.cjs',
@@ -40,10 +43,11 @@ class ConfigLoader {
40
43
  // Since this is a CLI, we can afford a bit of hackiness or just support CommonJS primarily.
41
44
 
42
45
  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);
46
+ if (candidate.endsWith('.mjs') || (candidate.endsWith('.js') && !candidate.endsWith('.cjs'))) {
47
+ // Dynamic import() for ESM support
48
+ const modulePath = path.resolve(this.projectPath, candidate);
49
+ const imported = await import(`file://${modulePath}`);
50
+ cfg = imported.default || imported;
47
51
  } else {
48
52
  delete require.cache[require.resolve(fullPath)];
49
53
  cfg = require(fullPath);
@@ -103,6 +107,30 @@ class ConfigLoader {
103
107
  if (!cfg || Object.keys(cfg).length === 0) return detected;
104
108
 
105
109
  const result = { ...detected };
110
+ const runScript = (scriptName) => {
111
+ const packageManager =
112
+ (result.languages && result.languages[0] && result.languages[0].packageManager) ||
113
+ cfg.packageManager ||
114
+ 'npm';
115
+ if (packageManager === 'yarn') return `yarn run ${scriptName}`;
116
+ if (packageManager === 'pnpm') return `pnpm run ${scriptName}`;
117
+ if (packageManager === 'bun') return `bun run ${scriptName}`;
118
+ return `npm run ${scriptName}`;
119
+ };
120
+ const canonicalHostingNames = {
121
+ firebase: 'Firebase',
122
+ vercel: 'Vercel',
123
+ netlify: 'Netlify',
124
+ aws: 'AWS',
125
+ 'gcp app engine': 'GCP App Engine',
126
+ gcp: 'GCP App Engine',
127
+ azure: 'Azure',
128
+ heroku: 'Heroku',
129
+ render: 'Render',
130
+ railway: 'Railway',
131
+ 'github pages': 'GitHub Pages',
132
+ 'github-pages': 'GitHub Pages',
133
+ };
106
134
 
107
135
  // 1. Language overrides (Node version, package manager)
108
136
  if (cfg.nodeVersion && result.languages && result.languages.length > 0) {
@@ -114,23 +142,41 @@ class ConfigLoader {
114
142
  }
115
143
 
116
144
  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
145
+ result.languages = result.languages.map((l) =>
146
+ ({ ...l, packageManager: cfg.packageManager, manual: true })
119
147
  );
120
148
  }
121
149
 
122
150
  // 2. Hosting overrides
123
151
  if (cfg.hosting) {
124
152
  const hostingNames = Array.isArray(cfg.hosting) ? cfg.hosting : [cfg.hosting];
153
+ const hostingSecrets = {
154
+ Firebase: ['FIREBASE_SERVICE_ACCOUNT'],
155
+ Vercel: ['VERCEL_TOKEN', 'VERCEL_ORG_ID', 'VERCEL_PROJECT_ID'],
156
+ Netlify: ['NETLIFY_AUTH_TOKEN', 'NETLIFY_SITE_ID'],
157
+ AWS: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'AWS_S3_BUCKET', 'CLOUDFRONT_DISTRIBUTION_ID'],
158
+ 'GCP App Engine': ['GCP_SA_KEY'],
159
+ Azure: ['AZURE_APP_NAME', 'AZURE_WEBAPP_PUBLISH_PROFILE'],
160
+ Heroku: ['HEROKU_API_KEY', 'HEROKU_APP_NAME', 'HEROKU_EMAIL'],
161
+ Render: ['RENDER_DEPLOY_HOOK_URL'],
162
+ Railway: ['RAILWAY_TOKEN'],
163
+ 'GitHub Pages': [],
164
+ };
125
165
  result.hosting = hostingNames.map((name) => ({
126
- name,
166
+ name: canonicalHostingNames[String(name).toLowerCase()] || name,
127
167
  confidence: 1.0,
128
168
  manual: true,
129
- secrets: [],
169
+ secrets: hostingSecrets[canonicalHostingNames[String(name).toLowerCase()] || name] || [],
130
170
  notes: ['set via cistack.config.js'],
131
171
  }));
132
172
  }
133
173
 
174
+ // 2b. Release override
175
+ if (cfg.release) {
176
+ const releaseOverride = typeof cfg.release === 'string' ? { tool: cfg.release } : cfg.release;
177
+ result.releaseInfo = { ...(result.releaseInfo || {}), ...releaseOverride };
178
+ }
179
+
134
180
  // 3. Framework overrides
135
181
  if (cfg.frameworks) {
136
182
  const frameworkNames = Array.isArray(cfg.frameworks) ? cfg.frameworks : [cfg.frameworks];
@@ -149,10 +195,20 @@ class ConfigLoader {
149
195
  confidence: 1.0,
150
196
  manual: true,
151
197
  type: 'unit', // default
152
- command: `npm run test` // fallback
198
+ command: runScript('test') // fallback
153
199
  }));
154
200
  }
155
201
 
202
+ // 5. Extra documented secrets
203
+ if (cfg.secrets) {
204
+ const extraSecrets = Array.isArray(cfg.secrets) ? cfg.secrets : [cfg.secrets];
205
+ const envVars = result.envVars || { secrets: [], public: [], all: [], sourceFile: null };
206
+ result.envVars = {
207
+ ...envVars,
208
+ secrets: [...new Set([...(envVars.secrets || []), ...extraSecrets])],
209
+ };
210
+ }
211
+
156
212
  // Pass through raw extras for generators to consume
157
213
  result._config = { ...(result._config || {}), ...cfg };
158
214
 
@@ -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({