create-qa-architect 5.0.1 → 5.0.7

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.
Files changed (40) hide show
  1. package/.github/RELEASE_CHECKLIST.md +2 -4
  2. package/.github/workflows/daily-deploy-check.yml +136 -0
  3. package/.github/workflows/dependabot-auto-merge.yml +32 -0
  4. package/.github/workflows/nightly-gitleaks-verification.yml +1 -1
  5. package/.github/workflows/release.yml +12 -10
  6. package/.github/workflows/weekly-audit.yml +173 -0
  7. package/LICENSE +3 -3
  8. package/README.md +11 -17
  9. package/config/defaults.js +22 -1
  10. package/config/quality-config.schema.json +1 -1
  11. package/create-saas-monetization.js +65 -27
  12. package/docs/ARCHITECTURE.md +16 -13
  13. package/docs/DEPLOYMENT.md +1 -2
  14. package/docs/PREFLIGHT_REPORT.md +100 -0
  15. package/docs/TESTING.md +4 -6
  16. package/lib/billing-dashboard.html +6 -12
  17. package/lib/config-validator.js +8 -2
  18. package/lib/dependency-monitoring-premium.js +21 -19
  19. package/lib/github-api.js +249 -0
  20. package/lib/interactive/questions.js +4 -0
  21. package/lib/license-validator.js +1 -1
  22. package/lib/licensing.js +16 -18
  23. package/lib/package-utils.js +9 -8
  24. package/lib/project-maturity.js +1 -1
  25. package/lib/template-loader.js +2 -0
  26. package/lib/ui-helpers.js +2 -1
  27. package/lib/validation/base-validator.js +5 -1
  28. package/lib/validation/cache-manager.js +1 -0
  29. package/lib/validation/config-security.js +9 -4
  30. package/lib/validation/validation-factory.js +1 -1
  31. package/lib/validation/workflow-validation.js +27 -22
  32. package/lib/yaml-utils.js +15 -10
  33. package/package.json +17 -14
  34. package/scripts/check-docs.sh +63 -0
  35. package/scripts/smart-test-strategy.sh +98 -0
  36. package/scripts/test-e2e-package.sh +283 -0
  37. package/scripts/validate-command-patterns.js +112 -0
  38. package/setup.js +38 -9
  39. package/templates/QUALITY_TROUBLESHOOTING.md +32 -33
  40. package/templates/scripts/smart-test-strategy.sh +1 -1
@@ -0,0 +1,249 @@
1
+ /**
2
+ * GitHub API Integration for QA Architect
3
+ * Enables Dependabot alerts and security features via GitHub API
4
+ */
5
+
6
+ const https = require('https')
7
+ const { execSync } = require('child_process')
8
+
9
+ /**
10
+ * Get GitHub token from environment or gh CLI
11
+ */
12
+ function getGitHubToken() {
13
+ // Check environment variable first
14
+ if (process.env.GITHUB_TOKEN) {
15
+ return process.env.GITHUB_TOKEN
16
+ }
17
+
18
+ // Try to get from gh CLI
19
+ try {
20
+ const token = execSync('gh auth token', { encoding: 'utf8' }).trim()
21
+ if (token) return token
22
+ } catch {
23
+ // gh CLI not available or not authenticated
24
+ }
25
+
26
+ return null
27
+ }
28
+
29
+ /**
30
+ * Get repository info from git remote
31
+ */
32
+ function getRepoInfo(projectPath = '.') {
33
+ try {
34
+ const remoteUrl = execSync('git remote get-url origin', {
35
+ cwd: projectPath,
36
+ encoding: 'utf8',
37
+ }).trim()
38
+
39
+ // Parse GitHub URL (https or ssh format)
40
+ const httpsMatch = remoteUrl.match(
41
+ /github\.com[/:]([^/]+)\/([^/.]+)(\.git)?$/
42
+ )
43
+ if (httpsMatch) {
44
+ return { owner: httpsMatch[1], repo: httpsMatch[2] }
45
+ }
46
+
47
+ return null
48
+ } catch {
49
+ return null
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Make GitHub API request
55
+ */
56
+ function githubRequest(method, path, token, data = null) {
57
+ return new Promise((resolve, reject) => {
58
+ const options = {
59
+ hostname: 'api.github.com',
60
+ port: 443,
61
+ path: path,
62
+ method: method,
63
+ headers: {
64
+ Authorization: `Bearer ${token}`,
65
+ Accept: 'application/vnd.github+json',
66
+ 'User-Agent': 'qa-architect',
67
+ 'X-GitHub-Api-Version': '2022-11-28',
68
+ },
69
+ }
70
+
71
+ if (data) {
72
+ options.headers['Content-Type'] = 'application/json'
73
+ }
74
+
75
+ const req = https.request(options, res => {
76
+ let body = ''
77
+ res.on('data', chunk => (body += chunk))
78
+ res.on('end', () => {
79
+ if (res.statusCode >= 200 && res.statusCode < 300) {
80
+ resolve({
81
+ status: res.statusCode,
82
+ data: body ? JSON.parse(body) : null,
83
+ })
84
+ } else if (res.statusCode === 204) {
85
+ resolve({ status: 204, data: null })
86
+ } else {
87
+ reject(
88
+ new Error(
89
+ `GitHub API error: ${res.statusCode} - ${body || res.statusMessage}`
90
+ )
91
+ )
92
+ }
93
+ })
94
+ })
95
+
96
+ req.on('error', reject)
97
+
98
+ if (data) {
99
+ req.write(JSON.stringify(data))
100
+ }
101
+ req.end()
102
+ })
103
+ }
104
+
105
+ /**
106
+ * Check if Dependabot alerts are enabled
107
+ */
108
+ async function checkDependabotStatus(owner, repo, token) {
109
+ try {
110
+ await githubRequest(
111
+ 'GET',
112
+ `/repos/${owner}/${repo}/vulnerability-alerts`,
113
+ token
114
+ )
115
+ return true // 204 means enabled
116
+ } catch (error) {
117
+ if (error.message.includes('404')) {
118
+ return false // Not enabled
119
+ }
120
+ throw error
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Enable Dependabot alerts for a repository
126
+ */
127
+ async function enableDependabotAlerts(owner, repo, token) {
128
+ try {
129
+ await githubRequest(
130
+ 'PUT',
131
+ `/repos/${owner}/${repo}/vulnerability-alerts`,
132
+ token
133
+ )
134
+ return { success: true, message: 'Dependabot alerts enabled' }
135
+ } catch (error) {
136
+ return { success: false, message: error.message }
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Enable Dependabot security updates
142
+ */
143
+ async function enableDependabotSecurityUpdates(owner, repo, token) {
144
+ try {
145
+ await githubRequest(
146
+ 'PUT',
147
+ `/repos/${owner}/${repo}/automated-security-fixes`,
148
+ token
149
+ )
150
+ return { success: true, message: 'Dependabot security updates enabled' }
151
+ } catch (error) {
152
+ return { success: false, message: error.message }
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Full setup: Enable all Dependabot features
158
+ */
159
+ async function setupDependabot(projectPath = '.', options = {}) {
160
+ const { verbose = false } = options
161
+ const results = {
162
+ success: false,
163
+ repoInfo: null,
164
+ alerts: null,
165
+ securityUpdates: null,
166
+ errors: [],
167
+ }
168
+
169
+ // Get token
170
+ const token = getGitHubToken()
171
+ if (!token) {
172
+ results.errors.push(
173
+ 'No GitHub token found. Set GITHUB_TOKEN env var or run `gh auth login`'
174
+ )
175
+ return results
176
+ }
177
+
178
+ // Get repo info
179
+ const repoInfo = getRepoInfo(projectPath)
180
+ if (!repoInfo) {
181
+ results.errors.push('Could not determine GitHub repository from git remote')
182
+ return results
183
+ }
184
+ results.repoInfo = repoInfo
185
+
186
+ if (verbose) {
187
+ console.log(`📦 Repository: ${repoInfo.owner}/${repoInfo.repo}`)
188
+ }
189
+
190
+ // Check current status
191
+ try {
192
+ const isEnabled = await checkDependabotStatus(
193
+ repoInfo.owner,
194
+ repoInfo.repo,
195
+ token
196
+ )
197
+ if (isEnabled) {
198
+ if (verbose) console.log('✅ Dependabot alerts already enabled')
199
+ results.alerts = { success: true, message: 'Already enabled' }
200
+ } else {
201
+ // Enable alerts
202
+ results.alerts = await enableDependabotAlerts(
203
+ repoInfo.owner,
204
+ repoInfo.repo,
205
+ token
206
+ )
207
+ if (verbose) {
208
+ console.log(
209
+ results.alerts.success
210
+ ? '✅ Dependabot alerts enabled'
211
+ : `❌ Failed to enable alerts: ${results.alerts.message}`
212
+ )
213
+ }
214
+ }
215
+ } catch (error) {
216
+ results.errors.push(`Alerts check failed: ${error.message}`)
217
+ }
218
+
219
+ // Enable security updates
220
+ try {
221
+ results.securityUpdates = await enableDependabotSecurityUpdates(
222
+ repoInfo.owner,
223
+ repoInfo.repo,
224
+ token
225
+ )
226
+ if (verbose) {
227
+ console.log(
228
+ results.securityUpdates.success
229
+ ? '✅ Dependabot security updates enabled'
230
+ : `⚠️ Security updates: ${results.securityUpdates.message}`
231
+ )
232
+ }
233
+ } catch (error) {
234
+ results.errors.push(`Security updates failed: ${error.message}`)
235
+ }
236
+
237
+ results.success = results.alerts?.success && results.errors.length === 0
238
+
239
+ return results
240
+ }
241
+
242
+ module.exports = {
243
+ getGitHubToken,
244
+ getRepoInfo,
245
+ checkDependabotStatus,
246
+ enableDependabotAlerts,
247
+ enableDependabotSecurityUpdates,
248
+ setupDependabot,
249
+ }
@@ -1,5 +1,9 @@
1
1
  'use strict'
2
2
 
3
+ /**
4
+ * @typedef {import('./prompt').InteractivePrompt} InteractivePrompt
5
+ */
6
+
3
7
  /**
4
8
  * Question definitions and answer parsing for interactive mode
5
9
  */
@@ -26,7 +26,7 @@ class LicenseValidator {
26
26
  // Allow enterprises to host their own registry
27
27
  this.licenseDbUrl =
28
28
  process.env.QAA_LICENSE_DB_URL ||
29
- 'https://license.aibuilderlab.com/qaa/legitimate-licenses.json'
29
+ 'https://vibebuildlab.com/api/licenses/qa-architect.json'
30
30
  }
31
31
 
32
32
  ensureLicenseDir() {
package/lib/licensing.js CHANGED
@@ -36,11 +36,11 @@ Object.defineProperty(exports, 'LICENSE_FILE', {
36
36
  * Standardized to use SCREAMING_SNAKE_CASE for both keys and values
37
37
  * for consistency with ErrorCategory and other enums in the codebase.
38
38
  *
39
- * Pricing (effective Jan 15, 2026 - founder pricing retired):
39
+ * Pricing:
40
40
  * - FREE: $0 (Hobby/OSS - capped)
41
- * - PRO: $59/mo or $590/yr (Solo Devs/Small Teams)
42
- * - TEAM: $15/user/mo, 5-seat minimum (Organizations)
43
- * - ENTERPRISE: $249/mo annual + $499 onboarding (Large Orgs)
41
+ * - PRO: $19/mo or $190/yr (Solo Devs/Small Teams)
42
+ * - TEAM: Contact us (Organizations) - coming soon
43
+ * - ENTERPRISE: Contact us (Large Orgs) - coming soon
44
44
  */
45
45
  const LICENSE_TIERS = {
46
46
  FREE: 'FREE',
@@ -216,7 +216,7 @@ function isDeveloperMode() {
216
216
  if (fs.existsSync(developerMarkerFile)) {
217
217
  return true
218
218
  }
219
- } catch (_error) {
219
+ } catch {
220
220
  // Ignore errors checking for marker file
221
221
  }
222
222
 
@@ -338,7 +338,7 @@ function verifyLicenseSignature(payload, signature) {
338
338
  Buffer.from(signature),
339
339
  Buffer.from(expectedSignature)
340
340
  )
341
- } catch (_error) {
341
+ } catch {
342
342
  // If signature verification fails, treat as invalid
343
343
  return false
344
344
  }
@@ -384,7 +384,7 @@ function showUpgradeMessage(feature) {
384
384
  if (license.tier === LICENSE_TIERS.FREE) {
385
385
  console.log('\n🚀 Upgrade to PRO')
386
386
  console.log('')
387
- console.log(' 💰 $59/month or $590/year (save $118)')
387
+ console.log(' 💰 $19/month or $190/year (save $38)')
388
388
  console.log('')
389
389
  console.log(' ✅ Unlimited repos, LOC, and runs')
390
390
  console.log(' ✅ Smart Test Strategy (70% faster pre-push)')
@@ -396,16 +396,14 @@ function showUpgradeMessage(feature) {
396
396
  console.log('')
397
397
  console.log(' 🎁 Start 14-day free trial - no credit card required')
398
398
  console.log('')
399
- console.log('🚀 Upgrade: https://vibebuildlab.com/qaa')
399
+ console.log('🚀 Upgrade: https://vibebuildlab.com/qa-architect')
400
400
  console.log(
401
401
  '🔑 Activate: npx create-qa-architect@latest --activate-license'
402
402
  )
403
403
  } else if (license.tier === LICENSE_TIERS.PRO) {
404
404
  console.log('\n👥 Upgrade to TEAM')
405
405
  console.log('')
406
- console.log(
407
- ' 💰 $15/user/month (5-seat min) or $150/user/year (save $30/user)'
408
- )
406
+ console.log(' 💰 Contact us for Team pricing')
409
407
  console.log('')
410
408
  console.log(' ✅ All PRO features included')
411
409
  console.log(' ✅ Per-seat licensing for your org')
@@ -414,9 +412,9 @@ function showUpgradeMessage(feature) {
414
412
  console.log(' ✅ Slack/email alerts for failures')
415
413
  console.log(' ✅ Priority support (business hours)')
416
414
  console.log('')
417
- console.log('👥 Upgrade: https://vibebuildlab.com/qaa/team')
415
+ console.log('👥 Upgrade: https://vibebuildlab.com/qa-architect')
418
416
  } else if (license.tier === LICENSE_TIERS.TEAM) {
419
- console.log('\n🏢 Upgrade to ENTERPRISE - $249/month (annual) + onboarding')
417
+ console.log('\n🏢 Upgrade to ENTERPRISE - Contact us for pricing')
420
418
  console.log('')
421
419
  console.log(' ✅ All TEAM features included')
422
420
  console.log(' ✅ SSO/SAML integration')
@@ -553,7 +551,7 @@ async function addLegitimateKey(
553
551
  if (fs.existsSync(legitimateDBFile)) {
554
552
  try {
555
553
  database = JSON.parse(fs.readFileSync(legitimateDBFile, 'utf8'))
556
- } catch (_error) {
554
+ } catch {
557
555
  console.error(
558
556
  'Warning: Could not parse existing database, creating new one'
559
557
  )
@@ -654,7 +652,7 @@ async function promptLicenseActivation() {
654
652
  console.log(
655
653
  ' If you purchased this license, please contact support at:'
656
654
  )
657
- console.log(' Email: support@aibuilderlab.com')
655
+ console.log(' Email: support@vibebuildlab.com')
658
656
  console.log(
659
657
  ' Include your license key and purchase email for verification.'
660
658
  )
@@ -745,7 +743,7 @@ function loadUsage() {
745
743
 
746
744
  return data
747
745
  }
748
- } catch (_error) {
746
+ } catch {
749
747
  // Ignore errors reading usage file
750
748
  }
751
749
 
@@ -770,7 +768,7 @@ function saveUsage(usage) {
770
768
  }
771
769
  fs.writeFileSync(getUsageFile(), JSON.stringify(usage, null, 2))
772
770
  return true
773
- } catch (_error) {
771
+ } catch {
774
772
  return false
775
773
  }
776
774
  }
@@ -958,7 +956,7 @@ function showLicenseStatus() {
958
956
  // Show upgrade path
959
957
  if (license.tier === LICENSE_TIERS.FREE) {
960
958
  console.log('\n💡 Upgrade to PRO for unlimited access + security scanning')
961
- console.log(' → https://vibebuildlab.com/qaa')
959
+ console.log(' → https://vibebuildlab.com/qa-architect')
962
960
  }
963
961
  }
964
962
 
@@ -50,17 +50,17 @@ function mergeDevDependencies(initialDevDeps = {}, defaultDevDeps) {
50
50
 
51
51
  /**
52
52
  * Merge lint-staged configuration, preserving existing patterns
53
- * @param {Object} existing - Existing lint-staged config
54
- * @param {Object} defaults - Default lint-staged config
55
- * @param {Object} options - Merge options
56
- * @param {Function} patternChecker - Function to check if a pattern matches certain criteria
57
- * @returns {Object} Merged lint-staged config
53
+ * @param {Record<string, string|string[]>} [existing] - Existing lint-staged config
54
+ * @param {Record<string, string|string[]>} defaults - Default lint-staged config
55
+ * @param {{stylelintTargets?: string[]}} [options] - Merge options
56
+ * @param {(pattern: string) => boolean} [patternChecker] - Function to check if a pattern matches certain criteria
57
+ * @returns {Record<string, string|string[]>} Merged lint-staged config
58
58
  */
59
59
  function mergeLintStaged(
60
+ defaults = {},
60
61
  existing = {},
61
- defaults,
62
62
  options = {},
63
- patternChecker = null
63
+ patternChecker = _pattern => false
64
64
  ) {
65
65
  const merged = { ...existing }
66
66
  const stylelintTargets = options.stylelintTargets || []
@@ -88,7 +88,8 @@ function mergeLintStaged(
88
88
  : [merged[pattern]]
89
89
 
90
90
  const newCommands = [...existingCommands]
91
- commands.forEach(command => {
91
+ const commandList = Array.isArray(commands) ? commands : [commands]
92
+ commandList.forEach(command => {
92
93
  if (!newCommands.includes(command)) {
93
94
  newCommands.push(command)
94
95
  }
@@ -435,7 +435,7 @@ class ProjectMaturityDetector {
435
435
 
436
436
  /**
437
437
  * Generate GitHub Actions outputs for maturity detection
438
- * @returns {string} GitHub Actions output format
438
+ * @returns {{maturity: string, sourceCount: number, testCount: number, hasDeps: boolean, hasDocs: boolean, hasCss: boolean, requiredChecks: string, optionalChecks: string, disabledChecks: string}} GitHub Actions output format
439
439
  */
440
440
  generateGitHubActionsOutput() {
441
441
  const maturity = this.detect()
@@ -126,6 +126,7 @@ class TemplateLoader {
126
126
  * // Only loads from whitelisted dirs: config/, .github/, lib/
127
127
  */
128
128
  async loadTemplates(dir, baseDir = dir, isPackageDir = false) {
129
+ /** @type {Record<string, string>} */
129
130
  const templates = {}
130
131
 
131
132
  try {
@@ -177,6 +178,7 @@ class TemplateLoader {
177
178
  * @returns {Promise<Object>} Merged template map
178
179
  */
179
180
  async mergeTemplates(customDir, defaultsDir) {
181
+ /** @type {Record<string, string>} */
180
182
  const merged = {}
181
183
 
182
184
  // Load defaults first (from package directory - restrict to known template dirs)
package/lib/ui-helpers.js CHANGED
@@ -55,7 +55,8 @@ function showProgress(message) {
55
55
 
56
56
  // Try to use ora for interactive terminals
57
57
  try {
58
- const ora = require('ora')
58
+ const oraImport = require('ora')
59
+ const ora = /** @type {any} */ (oraImport.default || oraImport)
59
60
  return ora(message).start()
60
61
  } catch {
61
62
  // Fallback if ora not installed (graceful degradation)
@@ -1,5 +1,9 @@
1
1
  'use strict'
2
2
 
3
+ /**
4
+ * @typedef {Error & {code?: string}} ErrWithCode
5
+ */
6
+
3
7
  /**
4
8
  * Base Validator Class
5
9
  * Provides common error handling, state management, and validation patterns
@@ -81,7 +85,7 @@ class BaseValidator {
81
85
 
82
86
  /**
83
87
  * Format error message for user
84
- * @param {Error} error - The error object
88
+ * @param {ErrWithCode} error - The error object
85
89
  * @param {string} context - The context
86
90
  * @returns {string} Formatted error message
87
91
  */
@@ -14,6 +14,7 @@ class CacheManager {
14
14
  options.cacheDir || path.join(process.cwd(), '.create-qa-architect-cache')
15
15
  this.ttl = options.ttl || 24 * 60 * 60 * 1000 // Default: 24 hours in milliseconds
16
16
  this.enabled = options.enabled !== false // Enable by default
17
+ this.verbose = Boolean(options.verbose)
17
18
 
18
19
  // Ensure cache directory exists
19
20
  if (this.enabled) {
@@ -10,16 +10,19 @@ const { showProgress } = require('../ui-helpers')
10
10
 
11
11
  // Pinned gitleaks version for reproducible security scanning
12
12
  const GITLEAKS_VERSION = '8.28.0'
13
- // Real SHA256 checksums from https://github.com/gitleaks/gitleaks/releases/tag/v8.28.0
13
+ // SHA256 checksums of EXTRACTED BINARIES from gitleaks v8.28.0 release
14
+ // Note: These are checksums of the binary itself, not the tarball/zip archives
14
15
  const GITLEAKS_CHECKSUMS = {
15
16
  'linux-x64':
16
- 'a65b5253807a68ac0cafa4414031fd740aeb55f54fb7e55f386acb52e6a840eb',
17
+ '5fd1b3b0073269484d40078662e921d07427340ab9e6ed526ccd215a565b3298',
18
+ 'linux-arm64':
19
+ '3770c7ebeb625e3e96c183525ca18285a01aedef2d75a2c41ceb3e141af2e8b7',
17
20
  'darwin-x64':
18
- 'edf5a507008b0d2ef4959575772772770586409c1f6f74dabf19cbe7ec341ced',
21
+ 'cf09ad7a85683d90221db8324f036f23c8c29107145e1fc4a0dffbfa9e89c09a',
19
22
  'darwin-arm64':
20
23
  '5588b5d942dffa048720f7e6e1d274283219fb5722a2c7564d22e83ba39087d7',
21
24
  'win32-x64':
22
- 'da6458e8864af553807de1c46a7a8eac0880bd6b99ba56288e87e86a45af884f',
25
+ '54230c22688d19939f316cd3e2e040cd067ece40a3a8c5b684e5110c62ecbf52',
23
26
  }
24
27
 
25
28
  /**
@@ -199,6 +202,7 @@ class ConfigSecurityScanner {
199
202
  'darwin-x64': 'darwin_x64',
200
203
  'darwin-arm64': 'darwin_arm64',
201
204
  'linux-x64': 'linux_x64',
205
+ 'linux-arm64': 'linux_arm64',
202
206
  'win32-x64': 'windows_x64',
203
207
  }
204
208
 
@@ -349,6 +353,7 @@ class ConfigSecurityScanner {
349
353
  timeout: 60000, // 60 second timeout for audit operations
350
354
  encoding: 'utf8',
351
355
  })
356
+ spinner.succeed('npm audit completed - no high/critical vulnerabilities')
352
357
  } catch (error) {
353
358
  if (error.signal === 'SIGTERM') {
354
359
  // Timeout occurred
@@ -75,7 +75,7 @@ class ValidationFactory {
75
75
 
76
76
  /**
77
77
  * Run all validators
78
- * @returns {object} Validation results
78
+ * @returns {Promise<object>} Validation results
79
79
  */
80
80
  async validateAll() {
81
81
  const results = {
@@ -2,7 +2,6 @@
2
2
 
3
3
  const fs = require('fs')
4
4
  const path = require('path')
5
- const { execSync } = require('child_process')
6
5
  const { showProgress } = require('../ui-helpers')
7
6
 
8
7
  /**
@@ -85,32 +84,38 @@ class WorkflowValidator {
85
84
  const spinner = showProgress('Running actionlint on workflow files...')
86
85
 
87
86
  try {
88
- // Run actionlint via npx (works with local and global installs, cross-platform)
89
- execSync('npx actionlint', { stdio: 'pipe', cwd: process.cwd() })
90
- spinner.succeed('actionlint validation passed')
91
- } catch (error) {
92
- if (error.stdout || error.stderr) {
93
- const output = error.stdout
94
- ? error.stdout.toString()
95
- : error.stderr.toString()
96
- const lines = output
97
- .trim()
98
- .split('\n')
99
- .filter(line => line.trim())
100
-
101
- if (lines.length > 0) {
102
- spinner.fail(`actionlint found ${lines.length} issue(s)`)
103
- lines.forEach(line => {
104
- if (line.trim()) {
105
- this.issues.push(`actionlint: ${line.trim()}`)
106
- }
87
+ const { createLinter } = require('actionlint')
88
+ const workflowFiles = fs
89
+ .readdirSync(workflowDir)
90
+ .filter(file => file.endsWith('.yml') || file.endsWith('.yaml'))
91
+
92
+ const linter = await createLinter()
93
+ let issueCount = 0
94
+
95
+ for (const file of workflowFiles) {
96
+ const filePath = path.join(workflowDir, file)
97
+ const content = fs.readFileSync(filePath, 'utf8')
98
+ const results = linter(content, filePath) || []
99
+
100
+ if (Array.isArray(results) && results.length > 0) {
101
+ issueCount += results.length
102
+ results.forEach(result => {
103
+ this.issues.push(
104
+ `actionlint: ${result.file}:${result.line}:${result.column} ${result.kind} - ${result.message}`
105
+ )
107
106
  })
108
- } else {
109
- spinner.succeed('actionlint validation passed')
110
107
  }
108
+ }
109
+
110
+ if (issueCount > 0) {
111
+ spinner.fail(`actionlint found ${issueCount} issue(s)`)
111
112
  } else {
112
113
  spinner.succeed('actionlint validation passed')
113
114
  }
115
+ } catch (error) {
116
+ spinner.fail('actionlint failed to run')
117
+ const reason = error?.message || 'Unknown error'
118
+ this.issues.push(`actionlint: Failed to run - ${reason}`)
114
119
  }
115
120
  }
116
121
 
package/lib/yaml-utils.js CHANGED
@@ -82,17 +82,22 @@ function convertToYaml(obj, indent = 0) {
82
82
  if (Array.isArray(obj)) {
83
83
  obj.forEach(item => {
84
84
  if (typeof item === 'object' && item !== null) {
85
- // For objects in arrays, handle first property inline with dash, rest indented
86
- const itemYaml = convertToYaml(item, indent + 2)
87
- const lines = itemYaml.split('\n').filter(line => line.trim())
88
- if (lines.length > 0) {
89
- // First property goes inline with the dash
90
- yaml += `${spaces}- ${lines[0].trim()}\n`
91
- // Additional properties are properly indented
92
- for (let i = 1; i < lines.length; i++) {
93
- yaml += `${spaces} ${lines[i].trim()}\n`
85
+ // For objects in arrays, prefix first line with "- " and indent rest
86
+ const entries = Object.entries(item)
87
+ entries.forEach(([key, value], idx) => {
88
+ const safeKey = formatYamlValue(key)
89
+ const prefix = idx === 0 ? `${spaces}- ` : `${spaces} `
90
+
91
+ if (Array.isArray(value)) {
92
+ yaml += `${prefix}${safeKey}:\n`
93
+ yaml += convertToYaml(value, indent + 4)
94
+ } else if (typeof value === 'object' && value !== null) {
95
+ yaml += `${prefix}${safeKey}:\n`
96
+ yaml += convertToYaml(value, indent + 4)
97
+ } else {
98
+ yaml += `${prefix}${safeKey}: ${formatYamlValue(value)}\n`
94
99
  }
95
- }
100
+ })
96
101
  } else {
97
102
  yaml += `${spaces}- ${formatYamlValue(item)}\n`
98
103
  }