buildflow-dev 1.0.1 → 1.0.2

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.
package/bin/buildflow.js CHANGED
@@ -11,6 +11,7 @@ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'))
11
11
  const loadInit = () => import('../src/commands/init.js')
12
12
  const loadInstall = () => import('../src/commands/install.js')
13
13
  const loadAudit = () => import('../src/commands/audit.js')
14
+ const loadFix = () => import('../src/commands/fix.js')
14
15
  const loadStatus = () => import('../src/commands/status.js')
15
16
  const loadUpdate = () => import('../src/commands/update.js')
16
17
 
@@ -54,6 +55,15 @@ program
54
55
  await run(opts)
55
56
  })
56
57
 
58
+ program
59
+ .command('fix')
60
+ .description('Scan for issues and fix them interactively')
61
+ .option('-t, --target <path>', 'Scan specific file or directory')
62
+ .action(async (opts) => {
63
+ const { run } = await loadFix()
64
+ await run(opts)
65
+ })
66
+
57
67
  program
58
68
  .command('status')
59
69
  .description('Show BuildFlow status for current project')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buildflow-dev",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Adaptive AI-powered development orchestration. Works with Claude Code, Gemini CLI, Codex CLI, Cursor, and more.",
5
5
  "keywords": [
6
6
  "ai",
@@ -0,0 +1,368 @@
1
+ import chalk from 'chalk'
2
+ import ora from 'ora'
3
+ import enquirer from 'enquirer'
4
+ import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, mkdirSync } from 'fs'
5
+ import { join, relative, resolve } from 'path'
6
+ import { execSync } from 'child_process'
7
+
8
+ const { prompt } = enquirer
9
+
10
+ // ─── Scan patterns (same source as audit.js) ─────────────────────────────────
11
+
12
+ const SECRET_PATTERNS = [
13
+ { pattern: /(?<![a-zA-Z])(sk|pk|rk)[-_][a-zA-Z0-9]{20,}/g, label: 'API Key (sk/pk/rk)' },
14
+ { pattern: /AKIA[0-9A-Z]{16}/g, label: 'AWS Access Key' },
15
+ { pattern: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY/g, label: 'Private Key' },
16
+ { pattern: /['"]\s*(password|passwd|pwd|secret|api.?key|auth.?token)\s*['"]?\s*[:=]\s*['"][^'"]{4,}/gi, label: 'Hardcoded credential' },
17
+ { pattern: /postgres:\/\/[^@]+:[^@]+@/g, label: 'DB URL with credentials' },
18
+ { pattern: /mongodb(\+srv)?:\/\/[^@]+:[^@]+@/g, label: 'MongoDB URL with credentials' },
19
+ ]
20
+
21
+ const VULN_PATTERNS = [
22
+ {
23
+ pattern: /\.query\s*\(\s*[`'"]\s*SELECT.*?\$\{|\.query\s*\(\s*["'`].*?\+\s*\w/gs,
24
+ label: 'Possible SQL Injection',
25
+ severity: 'CRITICAL',
26
+ owasp: 'A03',
27
+ autoFix: null,
28
+ },
29
+ {
30
+ pattern: /eval\s*\(/g,
31
+ label: 'eval() usage',
32
+ severity: 'HIGH',
33
+ owasp: 'A03',
34
+ autoFix: null,
35
+ },
36
+ {
37
+ pattern: /exec\s*\(\s*[`'"]\s*.*?\$\{|execSync\s*\(\s*[`'"]\s*.*?\$\{/g,
38
+ label: 'Command injection risk',
39
+ severity: 'CRITICAL',
40
+ owasp: 'A03',
41
+ autoFix: null,
42
+ },
43
+ {
44
+ pattern: /Math\.random\s*\(\)/g,
45
+ label: 'Math.random() used for tokens (not cryptographically secure)',
46
+ severity: 'HIGH',
47
+ owasp: 'A07',
48
+ autoFix: {
49
+ description: 'Replace Math.random() with crypto.randomUUID() or crypto.getRandomValues()',
50
+ apply: (content) => content.replace(/Math\.random\s*\(\)/g, 'crypto.randomUUID()'),
51
+ note: 'Replaced Math.random() with crypto.randomUUID(). Review usage — randomUUID() returns a string, not a float.',
52
+ },
53
+ },
54
+ {
55
+ pattern: /console\.log\s*\([^)]*(?:password|token|secret|key|user)[^)]*\)/gi,
56
+ label: 'Sensitive data may be logged',
57
+ severity: 'MEDIUM',
58
+ owasp: 'A09',
59
+ autoFix: null,
60
+ },
61
+ ]
62
+
63
+ const CODE_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java'])
64
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', 'coverage'])
65
+
66
+ function* walkFiles(dir) {
67
+ for (const entry of readdirSync(dir)) {
68
+ if (SKIP_DIRS.has(entry)) continue
69
+ const full = join(dir, entry)
70
+ if (statSync(full).isDirectory()) {
71
+ yield* walkFiles(full)
72
+ } else if (CODE_EXTENSIONS.has(entry.slice(entry.lastIndexOf('.')))) {
73
+ yield full
74
+ }
75
+ }
76
+ }
77
+
78
+ function scanFile(filePath) {
79
+ let content
80
+ try { content = readFileSync(filePath, 'utf8') } catch { return [] }
81
+
82
+ const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath) ||
83
+ filePath.includes('__tests__') ||
84
+ filePath.includes('fixtures')
85
+
86
+ const findings = []
87
+ const lines = content.split('\n')
88
+
89
+ if (!isTestFile) {
90
+ for (const { pattern, label } of SECRET_PATTERNS) {
91
+ for (let i = 0; i < lines.length; i++) {
92
+ pattern.lastIndex = 0
93
+ if (pattern.test(lines[i])) {
94
+ findings.push({
95
+ type: 'SECRET',
96
+ severity: 'CRITICAL',
97
+ label,
98
+ file: filePath,
99
+ line: i + 1,
100
+ snippet: lines[i].trim().slice(0, 80),
101
+ autoFix: null,
102
+ })
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ for (const { pattern, label, severity, owasp, autoFix } of VULN_PATTERNS) {
109
+ for (let i = 0; i < lines.length; i++) {
110
+ pattern.lastIndex = 0
111
+ if (pattern.test(lines[i])) {
112
+ findings.push({
113
+ type: 'VULN',
114
+ severity,
115
+ label,
116
+ owasp,
117
+ file: filePath,
118
+ line: i + 1,
119
+ snippet: lines[i].trim().slice(0, 80),
120
+ autoFix,
121
+ })
122
+ }
123
+ }
124
+ }
125
+
126
+ return findings
127
+ }
128
+
129
+ // ─── Config / structural checks ───────────────────────────────────────────────
130
+
131
+ function checkConfigIssues(cwd) {
132
+ const issues = []
133
+
134
+ // .env not in .gitignore
135
+ if (existsSync(join(cwd, '.env'))) {
136
+ const gitignore = existsSync(join(cwd, '.gitignore'))
137
+ ? readFileSync(join(cwd, '.gitignore'), 'utf8')
138
+ : ''
139
+ if (!gitignore.includes('.env')) {
140
+ issues.push({
141
+ type: 'CONFIG',
142
+ severity: 'CRITICAL',
143
+ label: '.env file is not in .gitignore — secrets could be committed',
144
+ file: join(cwd, '.gitignore'),
145
+ autoFix: {
146
+ description: 'Add .env to .gitignore',
147
+ apply: () => {
148
+ const existing = existsSync(join(cwd, '.gitignore'))
149
+ ? readFileSync(join(cwd, '.gitignore'), 'utf8')
150
+ : ''
151
+ writeFileSync(join(cwd, '.gitignore'), existing + '\n.env\n.env.local\n.env.*.local\n')
152
+ },
153
+ },
154
+ })
155
+ }
156
+ }
157
+
158
+ // Missing package-lock.json in a Node project
159
+ if (existsSync(join(cwd, 'package.json')) && !existsSync(join(cwd, 'package-lock.json')) && !existsSync(join(cwd, 'yarn.lock')) && !existsSync(join(cwd, 'pnpm-lock.yaml'))) {
160
+ issues.push({
161
+ type: 'CONFIG',
162
+ severity: 'MEDIUM',
163
+ label: 'No lockfile found (package-lock.json / yarn.lock / pnpm-lock.yaml) — dependency versions not pinned',
164
+ file: join(cwd, 'package.json'),
165
+ autoFix: {
166
+ description: 'Run npm install to generate package-lock.json',
167
+ apply: () => execSync('npm install', { cwd, stdio: 'ignore' }),
168
+ },
169
+ })
170
+ }
171
+
172
+ return issues
173
+ }
174
+
175
+ // ─── Fix application ──────────────────────────────────────────────────────────
176
+
177
+ function applyFileFix(filePath, autoFix) {
178
+ const content = readFileSync(filePath, 'utf8')
179
+ const fixed = autoFix.apply(content)
180
+ writeFileSync(filePath, fixed)
181
+ }
182
+
183
+ // ─── Severity colour helper ───────────────────────────────────────────────────
184
+
185
+ function severityColor(s) {
186
+ if (s === 'CRITICAL') return chalk.red
187
+ if (s === 'HIGH') return chalk.yellow
188
+ return chalk.hex('#FFA500')
189
+ }
190
+
191
+ function severityIcon(s) {
192
+ if (s === 'CRITICAL') return '🔴'
193
+ if (s === 'HIGH') return '🟡'
194
+ return '🟠'
195
+ }
196
+
197
+ // ─── Main ─────────────────────────────────────────────────────────────────────
198
+
199
+ export async function run(opts = {}) {
200
+ const cwd = process.cwd()
201
+
202
+ console.log('\n' + chalk.bold.white(' BuildFlow — Fix Mode') + '\n')
203
+ console.log(chalk.dim(' Scans for issues, auto-fixes safe ones, asks about the rest.\n'))
204
+
205
+ // 1. Scan
206
+ const spinner = ora('Scanning project...').start()
207
+ const target = opts.target ? resolve(opts.target) : cwd
208
+
209
+ const allFindings = []
210
+ let fileCount = 0
211
+
212
+ for (const filePath of walkFiles(target)) {
213
+ fileCount++
214
+ allFindings.push(...scanFile(filePath))
215
+ }
216
+
217
+ allFindings.push(...checkConfigIssues(cwd))
218
+ spinner.stop()
219
+
220
+ if (allFindings.length === 0) {
221
+ console.log(chalk.green(' ✓ No issues found. Nothing to fix.\n'))
222
+ return
223
+ }
224
+
225
+ // Sort: CRITICAL first, then HIGH, then MEDIUM
226
+ const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2 }
227
+ allFindings.sort((a, b) => (order[a.severity] ?? 3) - (order[b.severity] ?? 3))
228
+
229
+ console.log(chalk.dim(` Found ${allFindings.length} issue(s) across ${fileCount} files.\n`))
230
+
231
+ // 2. Separate auto-fixable from prompt-required
232
+ const autoFixable = allFindings.filter(f => f.autoFix !== null)
233
+ const needsPrompt = allFindings.filter(f => f.autoFix === null)
234
+
235
+ // ── Auto-fixes ──────────────────────────────────────────────────────────────
236
+ if (autoFixable.length > 0) {
237
+ console.log(chalk.bold(' Auto-fixable issues:\n'))
238
+
239
+ for (const f of autoFixable) {
240
+ const color = severityColor(f.severity)
241
+ const icon = severityIcon(f.severity)
242
+ const rel = f.file ? relative(cwd, f.file) : ''
243
+
244
+ console.log(color(` ${icon} [${f.severity}] ${f.label}`))
245
+ if (rel) console.log(chalk.dim(` File: ${rel}${f.line ? `:${f.line}` : ''}`))
246
+ console.log(chalk.dim(` Fix: ${f.autoFix.description}`))
247
+ console.log('')
248
+ }
249
+
250
+ const { confirmAuto } = await prompt({
251
+ type: 'confirm',
252
+ name: 'confirmAuto',
253
+ message: `Apply ${autoFixable.length} auto-fix(es) now?`,
254
+ initial: true,
255
+ })
256
+
257
+ if (confirmAuto) {
258
+ for (const f of autoFixable) {
259
+ const sp = ora(chalk.dim(` Fixing: ${f.label}...`)).start()
260
+ try {
261
+ if (f.type === 'VULN' && f.file && f.autoFix.apply.length === 1) {
262
+ // File-content fix
263
+ applyFileFix(f.file, f.autoFix)
264
+ } else {
265
+ // Side-effect fix (gitignore, npm install, etc.)
266
+ f.autoFix.apply()
267
+ }
268
+ sp.succeed(chalk.green(` ✓ Fixed: ${f.label}`))
269
+ if (f.autoFix.note) console.log(chalk.dim(` Note: ${f.autoFix.note}`))
270
+ } catch (err) {
271
+ sp.fail(chalk.red(` ✗ Failed: ${f.label} — ${err.message}`))
272
+ }
273
+ }
274
+ console.log('')
275
+ } else {
276
+ console.log(chalk.dim(' Skipped auto-fixes.\n'))
277
+ }
278
+ }
279
+
280
+ // ── Prompt-required issues ───────────────────────────────────────────────────
281
+ if (needsPrompt.length > 0) {
282
+ console.log(chalk.bold(` ${needsPrompt.length} issue(s) require your decision:\n`))
283
+
284
+ let fixed = 0
285
+ let skipped = 0
286
+
287
+ for (let i = 0; i < needsPrompt.length; i++) {
288
+ const f = needsPrompt[i]
289
+ const color = severityColor(f.severity)
290
+ const icon = severityIcon(f.severity)
291
+ const rel = f.file ? relative(cwd, f.file) : ''
292
+
293
+ console.log(chalk.dim(` [${i + 1}/${needsPrompt.length}]`))
294
+ console.log(color(` ${icon} [${f.severity}] ${f.label}`))
295
+ if (rel) console.log(chalk.dim(` File: ${rel}${f.line ? `:${f.line}` : ''}`))
296
+ if (f.snippet) console.log(chalk.dim(` Code: ${f.snippet}`))
297
+ if (f.owasp) console.log(chalk.dim(` OWASP: ${f.owasp}`))
298
+ console.log('')
299
+
300
+ const { action } = await prompt({
301
+ type: 'select',
302
+ name: 'action',
303
+ message: 'What do you want to do?',
304
+ choices: [
305
+ { name: 'skip', message: 'Skip — leave as-is for now' },
306
+ { name: 'debt', message: 'Log to security debt — track but do not fix now' },
307
+ { name: 'open', message: 'Open in editor — I will fix it manually' },
308
+ { name: 'stop', message: 'Stop — exit fix mode' },
309
+ ],
310
+ })
311
+
312
+ if (action === 'stop') {
313
+ console.log(chalk.dim(`\n Stopped at issue ${i + 1}/${needsPrompt.length}.\n`))
314
+ break
315
+ }
316
+
317
+ if (action === 'debt') {
318
+ logSecurityDebt(cwd, f)
319
+ console.log(chalk.dim(' Logged to .buildflow/security/DEBT.md\n'))
320
+ fixed++
321
+ } else if (action === 'open') {
322
+ openInEditor(f.file, f.line)
323
+ console.log(chalk.dim(' Opened in editor. Fix and save, then re-run: buildflow fix\n'))
324
+ skipped++
325
+ } else {
326
+ skipped++
327
+ console.log('')
328
+ }
329
+ }
330
+
331
+ console.log(chalk.bold('\n Summary:'))
332
+ console.log(chalk.dim(` Logged to debt: ${fixed}`))
333
+ console.log(chalk.dim(` Skipped/manual: ${skipped}`))
334
+ }
335
+
336
+ console.log(chalk.dim('\n Re-run `buildflow fix` anytime to re-check.\n'))
337
+ }
338
+
339
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
340
+
341
+ function logSecurityDebt(cwd, finding) {
342
+ const debtPath = join(cwd, '.buildflow', 'security', 'DEBT.md')
343
+ if (!existsSync(debtPath)) return
344
+
345
+ const existing = readFileSync(debtPath, 'utf8')
346
+ const today = new Date().toISOString().split('T')[0]
347
+ const rel = finding.file ? relative(cwd, finding.file) : 'unknown'
348
+ const entry = `\n### ${today} — [${finding.severity}] ${finding.label}\n- File: ${rel}${finding.line ? `:${finding.line}` : ''}\n- OWASP: ${finding.owasp ?? 'N/A'}\n- Status: Deferred\n`
349
+
350
+ // Insert under "## Active" section
351
+ if (existing.includes('## Active')) {
352
+ writeFileSync(debtPath, existing.replace('## Active', `## Active\n${entry}`))
353
+ } else {
354
+ writeFileSync(debtPath, existing + '\n' + entry)
355
+ }
356
+ }
357
+
358
+ function openInEditor(filePath, line) {
359
+ if (!filePath) return
360
+ const editor = process.env.EDITOR || process.env.VISUAL || 'code'
361
+ try {
362
+ const target = line ? `${filePath}:${line}` : filePath
363
+ // VS Code and many editors support file:line syntax
364
+ execSync(`${editor} "${target}"`, { stdio: 'ignore' })
365
+ } catch {
366
+ console.log(chalk.dim(` Path: ${filePath}${line ? `:${line}` : ''}`))
367
+ }
368
+ }