cistack 1.0.0 → 3.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,163 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Loads cistack.config.js (or .cjs / .mjs) from the project root.
8
+ * Returns an empty object if no config file is found.
9
+ *
10
+ * Supported keys:
11
+ * nodeVersion – override detected Node version e.g. '18'
12
+ * packageManager – override detected PM: 'npm'|'yarn'|'pnpm'|'bun'
13
+ * hosting – array of hosting names to force e.g. ['Firebase']
14
+ * branches – branches to run CI on e.g. ['main', 'staging']
15
+ * cache – { npm: bool, cargo: bool, pip: bool, ... } enable/disable caches
16
+ * monorepo – { perPackage: bool } generate one file per workspace
17
+ * release – { tool: 'semantic-release'|'changesets'|'standard-version'|'release-it' }
18
+ * secrets – extra secret names to document in workflow comments
19
+ * outputDir – override default '.github/workflows'
20
+ */
21
+ class ConfigLoader {
22
+ constructor(projectPath) {
23
+ this.projectPath = projectPath;
24
+ }
25
+
26
+ load() {
27
+ const candidates = [
28
+ 'cistack.config.js',
29
+ 'cistack.config.cjs',
30
+ 'cistack.config.mjs',
31
+ ];
32
+
33
+ for (const candidate of candidates) {
34
+ const fullPath = path.join(this.projectPath, candidate);
35
+ if (fs.existsSync(fullPath)) {
36
+ try {
37
+ // For .js and .cjs, we can use require (with cache clearing)
38
+ // For .mjs, we might need a different approach, but sticking to sync require for now where possible
39
+ // In a real CLI, we might use dynamic import() but that's async.
40
+ // Since this is a CLI, we can afford a bit of hackiness or just support CommonJS primarily.
41
+
42
+ 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);
47
+ } else {
48
+ delete require.cache[require.resolve(fullPath)];
49
+ cfg = require(fullPath);
50
+ }
51
+
52
+ // Handle both `module.exports = {}` and `export default {}`
53
+ const resolved = cfg && cfg.__esModule ? cfg.default : cfg;
54
+ if (resolved && typeof resolved === 'object') {
55
+ return resolved;
56
+ }
57
+ } catch (err) {
58
+ // If it fails, it might be because it's ESM.
59
+ // We don't want to crash, but we should inform the user if they have a config but it's broken.
60
+ console.warn(chalk.yellow(`[cistack] Warning: could not load ${candidate}: ${err.message}`));
61
+ if (err.message.includes('ERR_REQUIRE_ESM')) {
62
+ console.warn(chalk.dim(` Tip: Try renaming ${candidate} to ${candidate.replace('.js', '.cjs')} or use CommonJS syntax.`));
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ return {};
69
+ }
70
+
71
+ /**
72
+ * Deep-merge config file settings into detected settings.
73
+ * Config file always wins on scalar values; arrays overwrite entirely.
74
+ */
75
+ static merge(detected, override) {
76
+ if (!override || typeof override !== 'object') return detected;
77
+
78
+ const result = { ...detected };
79
+
80
+ for (const [key, val] of Object.entries(override)) {
81
+ if (val === null || val === undefined) continue;
82
+
83
+ if (Array.isArray(val)) {
84
+ result[key] = val; // arrays overwrite
85
+ } else if (typeof val === 'object' && !Array.isArray(detected[key])) {
86
+ result[key] = { ...(detected[key] || {}), ...val };
87
+ } else {
88
+ result[key] = val;
89
+ }
90
+ }
91
+
92
+ return result;
93
+ }
94
+
95
+ /**
96
+ * Apply config file overrides onto the full detected stack.
97
+ *
98
+ * @param {object} cfg - raw cistack.config.js export
99
+ * @param {object} detected - { hosting, frameworks, languages, testing, ... }
100
+ * @returns {object} - merged config ready for the generator
101
+ */
102
+ static applyToStack(cfg, detected) {
103
+ if (!cfg || Object.keys(cfg).length === 0) return detected;
104
+
105
+ const result = { ...detected };
106
+
107
+ // 1. Language overrides (Node version, package manager)
108
+ if (cfg.nodeVersion && result.languages && result.languages.length > 0) {
109
+ result.languages = result.languages.map((l, i) =>
110
+ i === 0 && (l.name === 'JavaScript' || l.name === 'TypeScript')
111
+ ? { ...l, nodeVersion: String(cfg.nodeVersion), manual: true }
112
+ : l
113
+ );
114
+ }
115
+
116
+ 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
119
+ );
120
+ }
121
+
122
+ // 2. Hosting overrides
123
+ if (cfg.hosting) {
124
+ const hostingNames = Array.isArray(cfg.hosting) ? cfg.hosting : [cfg.hosting];
125
+ result.hosting = hostingNames.map((name) => ({
126
+ name,
127
+ confidence: 1.0,
128
+ manual: true,
129
+ secrets: [],
130
+ notes: ['set via cistack.config.js'],
131
+ }));
132
+ }
133
+
134
+ // 3. Framework overrides
135
+ if (cfg.frameworks) {
136
+ const frameworkNames = Array.isArray(cfg.frameworks) ? cfg.frameworks : [cfg.frameworks];
137
+ result.frameworks = frameworkNames.map(name => ({
138
+ name,
139
+ confidence: 1.0,
140
+ manual: true
141
+ }));
142
+ }
143
+
144
+ // 4. Testing overrides
145
+ if (cfg.testing) {
146
+ const testNames = Array.isArray(cfg.testing) ? cfg.testing : [cfg.testing];
147
+ result.testing = testNames.map(name => ({
148
+ name,
149
+ confidence: 1.0,
150
+ manual: true,
151
+ type: 'unit', // default
152
+ command: `npm run test` // fallback
153
+ }));
154
+ }
155
+
156
+ // Pass through raw extras for generators to consume
157
+ result._config = { ...(result._config || {}), ...cfg };
158
+
159
+ return result;
160
+ }
161
+ }
162
+
163
+ module.exports = ConfigLoader;
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Detects environment variables documented in .env.example or .env.sample.
8
+ *
9
+ * Returns:
10
+ * {
11
+ * secrets: string[], – keys that look like secrets (TOKEN, KEY, SECRET, PASSWORD, PASS)
12
+ * public: string[], – other public env vars
13
+ * all: string[], – full list in file order
14
+ * sourceFile: string, – which file was read
15
+ * }
16
+ */
17
+ class EnvDetector {
18
+ constructor(projectPath, codebaseInfo) {
19
+ this.root = projectPath;
20
+ this.info = codebaseInfo;
21
+ }
22
+
23
+ async detect() {
24
+ const candidates = ['.env.example', '.env.sample', '.env.template', '.env.defaults'];
25
+
26
+ for (const candidate of candidates) {
27
+ const fullPath = path.join(this.root, candidate);
28
+ if (fs.existsSync(fullPath)) {
29
+ const content = fs.readFileSync(fullPath, 'utf8');
30
+ return this._parse(content, candidate);
31
+ }
32
+ }
33
+
34
+ return { secrets: [], public: [], all: [], sourceFile: null };
35
+ }
36
+
37
+ _parse(content, sourceFile) {
38
+ const all = [];
39
+ const secrets = [];
40
+ const publicVars = [];
41
+
42
+ const SECRET_PATTERN = /SECRET|TOKEN|KEY|PASSWORD|PASS|PRIVATE|AUTH|CREDENTIAL|CERT|PEM/i;
43
+
44
+ for (const rawLine of content.split('\n')) {
45
+ const line = rawLine.trim();
46
+
47
+ // Skip comments and blank lines
48
+ if (!line || line.startsWith('#')) continue;
49
+
50
+ // Match KEY=value or KEY= or just KEY
51
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)\s*(?:=.*)?$/i);
52
+ if (!match) continue;
53
+
54
+ const key = match[1].toUpperCase();
55
+ if (all.includes(key)) continue; // de-dupe
56
+
57
+ all.push(key);
58
+ if (SECRET_PATTERN.test(key)) {
59
+ secrets.push(key);
60
+ } else {
61
+ publicVars.push(key);
62
+ }
63
+ }
64
+
65
+ return { secrets, public: publicVars, all, sourceFile };
66
+ }
67
+ }
68
+
69
+ module.exports = EnvDetector;
@@ -64,29 +64,46 @@ class FrameworkDetector {
64
64
  // ── generic JS/TS checker ─────────────────────────────────────────────────
65
65
  _check(name, depKeys, configFiles, meta = {}) {
66
66
  let confidence = 0;
67
+ const reasons = [];
67
68
 
68
69
  for (const dep of depKeys) {
69
- if (this.deps[dep]) { confidence += 0.5; break; }
70
+ if (this.deps[dep]) {
71
+ confidence += 0.5;
72
+ reasons.push(`dependency: ${dep}`);
73
+ break;
74
+ }
70
75
  }
71
76
  for (const cfg of configFiles) {
72
- if (this.configs.has(cfg) || this.files.has(cfg)) { confidence += 0.4; break; }
77
+ if (this.configs.has(cfg) || this.files.has(cfg)) {
78
+ confidence += 0.4;
79
+ reasons.push(`config file: ${cfg}`);
80
+ break;
81
+ }
73
82
  }
74
83
 
75
- return { name, confidence: Math.min(confidence, 1), ...meta };
84
+ return { name, confidence: Math.min(confidence, 1), reasons, ...meta };
76
85
  }
77
86
 
78
87
  _checkPython(name, pkg, markerFile) {
79
88
  let confidence = 0;
89
+ const reasons = [];
80
90
  const reqFiles = ['requirements.txt', 'Pipfile', 'pyproject.toml'];
81
91
  for (const rf of reqFiles) {
82
92
  const fullPath = path.join(this.root, rf);
83
93
  if (fs.existsSync(fullPath)) {
84
94
  const content = fs.readFileSync(fullPath, 'utf8').toLowerCase();
85
- if (content.includes(pkg.toLowerCase())) { confidence += 0.7; break; }
95
+ if (content.includes(pkg.toLowerCase())) {
96
+ confidence += 0.7;
97
+ reasons.push(`found ${pkg} in ${rf}`);
98
+ break;
99
+ }
86
100
  }
87
101
  }
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;
102
+ if (markerFile && this.files.has(markerFile)) {
103
+ confidence += 0.2;
104
+ reasons.push(`found marker file ${markerFile}`);
105
+ }
106
+ return confidence > 0 ? { name, confidence: Math.min(confidence, 1), isServer: true, isPython: true, reasons } : null;
90
107
  }
91
108
 
92
109
  _checkRuby(name, gem) {
@@ -94,20 +111,27 @@ class FrameworkDetector {
94
111
  if (!fs.existsSync(gemfilePath)) return null;
95
112
  const content = fs.readFileSync(gemfilePath, 'utf8').toLowerCase();
96
113
  const confidence = content.includes(gem.toLowerCase()) ? 0.9 : 0;
97
- return confidence > 0 ? { name, confidence, isServer: true, isRuby: true } : null;
114
+ const reasons = confidence > 0 ? [`found ${gem} in Gemfile`] : [];
115
+ return confidence > 0 ? { name, confidence, isServer: true, isRuby: true, reasons } : null;
98
116
  }
99
117
 
100
118
  _checkJVM(name, keyword) {
101
119
  const gradlePath = path.join(this.root, 'build.gradle');
102
120
  const pomPath = path.join(this.root, 'pom.xml');
103
121
  let confidence = 0;
122
+ let foundIn = '';
104
123
  for (const p of [gradlePath, pomPath]) {
105
124
  if (fs.existsSync(p)) {
106
125
  const content = fs.readFileSync(p, 'utf8').toLowerCase();
107
- if (content.includes(keyword.toLowerCase())) { confidence = 0.9; break; }
126
+ if (content.includes(keyword.toLowerCase())) {
127
+ confidence = 0.9;
128
+ foundIn = path.basename(p);
129
+ break;
130
+ }
108
131
  }
109
132
  }
110
- return confidence > 0 ? { name, confidence, isServer: true, isJVM: true } : null;
133
+ const reasons = confidence > 0 ? [`found ${keyword} in ${foundIn}`] : [];
134
+ return confidence > 0 ? { name, confidence, isServer: true, isJVM: true, reasons } : null;
111
135
  }
112
136
 
113
137
  _checkComposer(name, pkg) {
@@ -117,20 +141,21 @@ class FrameworkDetector {
117
141
  const composer = JSON.parse(fs.readFileSync(composerPath, 'utf8'));
118
142
  const allDeps = { ...(composer.require || {}), ...(composer['require-dev'] || {}) };
119
143
  const confidence = allDeps[pkg] ? 0.9 : 0;
120
- return confidence > 0 ? { name, confidence, isServer: true, isPHP: true } : null;
144
+ const reasons = confidence > 0 ? [`found ${pkg} in composer.json`] : [];
145
+ return confidence > 0 ? { name, confidence, isServer: true, isPHP: true, reasons } : null;
121
146
  } catch (_) { return null; }
122
147
  }
123
148
 
124
149
  _checkGo(name) {
125
150
  const goMod = path.join(this.root, 'go.mod');
126
151
  if (!fs.existsSync(goMod)) return null;
127
- return { name, confidence: 0.9, isServer: true, isGo: true };
152
+ return { name, confidence: 0.9, isServer: true, isGo: true, reasons: ['go.mod found'] };
128
153
  }
129
154
 
130
155
  _checkRust(name) {
131
156
  const cargoToml = path.join(this.root, 'Cargo.toml');
132
157
  if (!fs.existsSync(cargoToml)) return null;
133
- return { name, confidence: 0.9, isServer: true, isRust: true };
158
+ return { name, confidence: 0.9, isServer: true, isRust: true, reasons: ['Cargo.toml found'] };
134
159
  }
135
160
  }
136
161
 
@@ -60,13 +60,13 @@ class HostingDetector {
60
60
 
61
61
  _checkFirebase() {
62
62
  let confidence = 0;
63
- const notes = [];
63
+ const reasons = [];
64
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; }
65
+ if (this.configs.has('firebase.json')) { confidence += 0.6; reasons.push('firebase.json found'); }
66
+ if (this.configs.has('.firebaserc')) { confidence += 0.3; reasons.push('.firebaserc found'); }
67
+ if (this.deps['firebase-tools'] || this.deps['firebase']) { confidence += 0.2; reasons.push('firebase dependency found'); }
68
+ if (Object.values(this.scripts).some((s) => s.includes('firebase deploy'))) { confidence += 0.3; reasons.push('firebase deploy script found'); }
69
+ if (this.info.srcStructure.hasFunctions) { confidence += 0.1; reasons.push('functions directory found'); }
70
70
 
71
71
  // Detect what Firebase services are used
72
72
  let deployTarget = 'hosting';
@@ -85,38 +85,38 @@ class HostingDetector {
85
85
  confidence: Math.min(confidence, 1),
86
86
  deployCommand: `firebase deploy --only ${deployTarget}`,
87
87
  secrets: ['FIREBASE_TOKEN'],
88
- notes,
88
+ reasons,
89
89
  buildStep: this._detectBuildScript(),
90
90
  };
91
91
  }
92
92
 
93
93
  _checkVercel() {
94
94
  let confidence = 0;
95
- const notes = [];
95
+ const reasons = [];
96
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'); }
97
+ if (this.configs.has('vercel.json')) { confidence += 0.7; reasons.push('vercel.json found'); }
98
+ if (this.configs.has('.vercel')) { confidence += 0.4; reasons.push('.vercel directory found'); }
99
+ if (this.deps['vercel']) { confidence += 0.3; reasons.push('vercel dependency found'); }
100
+ if (Object.values(this.scripts).some((s) => s.includes('vercel'))) { confidence += 0.3; reasons.push('vercel script found'); }
101
101
 
102
102
  return {
103
103
  name: 'Vercel',
104
104
  confidence: Math.min(confidence, 1),
105
105
  deployCommand: 'vercel --prod --token $VERCEL_TOKEN',
106
106
  secrets: ['VERCEL_TOKEN', 'VERCEL_ORG_ID', 'VERCEL_PROJECT_ID'],
107
- notes,
107
+ reasons,
108
108
  buildStep: this._detectBuildScript(),
109
109
  };
110
110
  }
111
111
 
112
112
  _checkNetlify() {
113
113
  let confidence = 0;
114
- const notes = [];
114
+ const reasons = [];
115
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'); }
116
+ if (this.configs.has('netlify.toml')) { confidence += 0.7; reasons.push('netlify.toml found'); }
117
+ if (this.configs.has('_redirects')) { confidence += 0.2; reasons.push('_redirects file found'); }
118
+ if (this.deps['netlify-cli'] || this.deps['netlify']) { confidence += 0.3; reasons.push('netlify dependency found'); }
119
+ if (Object.values(this.scripts).some((s) => s.includes('netlify'))) { confidence += 0.3; reasons.push('netlify script found'); }
120
120
 
121
121
  let publishDir = 'dist';
122
122
  try {
@@ -130,7 +130,7 @@ class HostingDetector {
130
130
  confidence: Math.min(confidence, 1),
131
131
  deployCommand: `netlify deploy --prod --dir=${publishDir}`,
132
132
  secrets: ['NETLIFY_AUTH_TOKEN', 'NETLIFY_SITE_ID'],
133
- notes,
133
+ reasons,
134
134
  publishDir,
135
135
  buildStep: this._detectBuildScript(),
136
136
  };
@@ -138,110 +138,118 @@ class HostingDetector {
138
138
 
139
139
  _checkRender() {
140
140
  let confidence = 0;
141
- if (this.configs.has('render.yaml')) { confidence += 0.8; }
141
+ const reasons = [];
142
+ if (this.configs.has('render.yaml')) { confidence += 0.8; reasons.push('render.yaml detected'); }
142
143
  return {
143
144
  name: 'Render',
144
145
  confidence,
145
146
  deployCommand: 'curl -X POST $RENDER_DEPLOY_HOOK_URL',
146
147
  secrets: ['RENDER_DEPLOY_HOOK_URL'],
147
- notes: ['render.yaml detected'],
148
+ reasons,
148
149
  };
149
150
  }
150
151
 
151
152
  _checkRailway() {
152
153
  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;
154
+ const reasons = [];
155
+ if (this.configs.has('railway.json') || this.configs.has('railway.toml')) { confidence += 0.8; reasons.push('railway config found'); }
156
+ if (this.deps['@railway/cli']) { confidence += 0.2; reasons.push('railway cli dependency found'); }
155
157
  return {
156
158
  name: 'Railway',
157
159
  confidence,
158
160
  deployCommand: 'railway up',
159
161
  secrets: ['RAILWAY_TOKEN'],
160
- notes: [],
162
+ reasons,
161
163
  };
162
164
  }
163
165
 
164
166
  _checkHeroku() {
165
167
  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; }
168
+ const reasons = [];
169
+ if (this.configs.has('Procfile')) { confidence += 0.5; reasons.push('Procfile found'); }
170
+ if (this.configs.has('heroku.yml')) { confidence += 0.5; reasons.push('heroku.yml found'); }
171
+ if (this.deps['heroku']) { confidence += 0.2; reasons.push('heroku dependency found'); }
169
172
  return {
170
173
  name: 'Heroku',
171
174
  confidence,
172
175
  deployCommand: 'git push heroku main',
173
176
  secrets: ['HEROKU_API_KEY', 'HEROKU_APP_NAME'],
174
- notes: [],
177
+ reasons,
175
178
  };
176
179
  }
177
180
 
178
181
  _checkGCPAppEngine() {
179
182
  let confidence = 0;
180
- if (this.configs.has('app.yaml')) { confidence += 0.7; }
181
- if (this.deps['@google-cloud/functions-framework']) confidence += 0.2;
183
+ const reasons = [];
184
+ if (this.configs.has('app.yaml')) { confidence += 0.7; reasons.push('app.yaml detected'); }
185
+ if (this.deps['@google-cloud/functions-framework']) { confidence += 0.2; reasons.push('gcp functions framework found'); }
182
186
  return {
183
187
  name: 'GCP App Engine',
184
188
  confidence,
185
189
  deployCommand: 'gcloud app deploy',
186
190
  secrets: ['GCP_PROJECT_ID', 'GCP_SA_KEY'],
187
- notes: [],
191
+ reasons,
188
192
  };
189
193
  }
190
194
 
191
195
  _checkAWS() {
192
196
  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
+ const reasons = [];
198
+ if (this.configs.has('appspec.yml')) { confidence += 0.5; reasons.push('appspec.yml found'); }
199
+ if (this.configs.has('serverless.yml') || this.configs.has('serverless.yaml')) { confidence += 0.6; reasons.push('serverless.yml found'); }
200
+ if (this.configs.has('cdk.json')) { confidence += 0.4; reasons.push('cdk.json found'); }
201
+ if (this.deps['aws-sdk'] || this.deps['@aws-sdk/client-s3']) { confidence += 0.15; reasons.push('aws-sdk found'); }
197
202
  return {
198
203
  name: 'AWS',
199
204
  confidence: Math.min(confidence, 1),
200
205
  deployCommand: 'aws s3 sync ./dist s3://$AWS_S3_BUCKET --delete',
201
206
  secrets: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'],
202
- notes: [],
207
+ reasons,
203
208
  };
204
209
  }
205
210
 
206
211
  _checkAzure() {
207
212
  let confidence = 0;
208
- if (this.files.has('.azure/pipelines.yml')) confidence += 0.5;
209
- if (this.deps['@azure/core-http']) confidence += 0.2;
213
+ const reasons = [];
214
+ if (this.files.has('.azure/pipelines.yml')) { confidence += 0.5; reasons.push('.azure/pipelines.yml found'); }
215
+ if (this.deps['@azure/core-http']) { confidence += 0.2; reasons.push('azure core-http found'); }
210
216
  return {
211
217
  name: 'Azure',
212
218
  confidence,
213
219
  deployCommand: 'az webapp up',
214
220
  secrets: ['AZURE_CREDENTIALS'],
215
- notes: [],
221
+ reasons,
216
222
  };
217
223
  }
218
224
 
219
225
  _checkGitHubPages() {
220
226
  let confidence = 0;
227
+ const reasons = [];
221
228
  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;
229
+ if (pkgHomepage.includes('github.io')) { confidence += 0.6; reasons.push('homepage contains github.io'); }
230
+ if (this.deps['gh-pages']) { confidence += 0.4; reasons.push('gh-pages dependency found'); }
231
+ if (Object.values(this.scripts).some((s) => s.includes('gh-pages'))) { confidence += 0.3; reasons.push('gh-pages script found'); }
225
232
  return {
226
233
  name: 'GitHub Pages',
227
234
  confidence: Math.min(confidence, 1),
228
235
  deployCommand: null, // handled by actions/deploy-pages
229
236
  secrets: [],
230
- notes: [],
237
+ reasons,
231
238
  buildStep: this._detectBuildScript(),
232
239
  };
233
240
  }
234
241
 
235
242
  _checkDocker() {
236
243
  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;
244
+ const reasons = [];
245
+ if (this.configs.has('Dockerfile')) { confidence += 0.5; reasons.push('Dockerfile found'); }
246
+ if (this.configs.has('docker-compose.yml') || this.configs.has('docker-compose.yaml')) { confidence += 0.3; reasons.push('docker-compose.yml found'); }
239
247
  return {
240
248
  name: 'Docker',
241
249
  confidence,
242
250
  deployCommand: 'docker push $DOCKER_IMAGE',
243
251
  secrets: ['DOCKER_USERNAME', 'DOCKER_PASSWORD'],
244
- notes: [],
252
+ reasons,
245
253
  };
246
254
  }
247
255