buildflow-dev 1.0.0 → 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
@@ -1,80 +1,91 @@
1
- #!/usr/bin/env node
2
-
3
- import { program } from 'commander'
4
- import { readFileSync } from 'fs'
5
- import { fileURLToPath } from 'url'
6
- import { dirname, join } from 'path'
7
-
8
- const __dirname = dirname(fileURLToPath(import.meta.url))
9
- const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'))
10
-
11
- const loadInit = () => import('../src/commands/init.js')
12
- const loadInstall = () => import('../src/commands/install.js')
13
- const loadAudit = () => import('../src/commands/audit.js')
14
- const loadStatus = () => import('../src/commands/status.js')
15
- const loadUpdate = () => import('../src/commands/update.js')
16
-
17
- program
18
- .name('buildflow')
19
- .description('Adaptive AI-powered development orchestration')
20
- .version(pkg.version)
21
-
22
- program
23
- .command('init')
24
- .description('Initialize BuildFlow in the current project')
25
- .option('-y, --yes', 'Skip prompts, use defaults')
26
- .option('--greenfield', 'Start a brand-new project')
27
- .option('--existing', 'Add BuildFlow to existing codebase')
28
- .action(async (opts) => {
29
- const { run } = await loadInit()
30
- await run(opts)
31
- })
32
-
33
- program
34
- .command('install')
35
- .description('Install BuildFlow slash commands into an AI tool')
36
- .option('--tool <name>', 'AI tool to install into (claude|gemini|codex|cursor|all)')
37
- .option('--global', 'Install globally (available in all projects)')
38
- .option('--local', 'Install locally (current project only)')
39
- .action(async (opts) => {
40
- const { run } = await loadInstall()
41
- await run(opts)
42
- })
43
-
44
- program
45
- .command('audit')
46
- .description('Run a security audit on the current project')
47
- .option('-q, --quick', 'Quick audit (recent changes only)')
48
- .option('-t, --target <path>', 'Audit specific file or directory')
49
- .option('-c, --category <name>', 'Check specific OWASP category (A01-A10)')
50
- .option('-r, --report', 'Show latest report')
51
- .action(async (opts) => {
52
- const { run } = await loadAudit()
53
- await run(opts)
54
- })
55
-
56
- program
57
- .command('status')
58
- .description('Show BuildFlow status for current project')
59
- .option('-v, --verbose', 'Show detailed status')
60
- .action(async (opts) => {
61
- const { run } = await loadStatus()
62
- await run(opts)
63
- })
64
-
65
- program
66
- .command('update')
67
- .description('Update BuildFlow commands and agents in current project')
68
- .option('--check', 'Check for updates without applying')
69
- .action(async (opts) => {
70
- const { run } = await loadUpdate()
71
- await run(opts)
72
- })
73
-
74
- if (process.argv.length <= 2) {
75
- const { showWelcome } = await import('../src/utils/welcome.js')
76
- await showWelcome()
77
- process.exit(0)
78
- }
79
-
80
- program.parse()
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander'
4
+ import { readFileSync } from 'fs'
5
+ import { fileURLToPath } from 'url'
6
+ import { dirname, join } from 'path'
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url))
9
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'))
10
+
11
+ const loadInit = () => import('../src/commands/init.js')
12
+ const loadInstall = () => import('../src/commands/install.js')
13
+ const loadAudit = () => import('../src/commands/audit.js')
14
+ const loadFix = () => import('../src/commands/fix.js')
15
+ const loadStatus = () => import('../src/commands/status.js')
16
+ const loadUpdate = () => import('../src/commands/update.js')
17
+
18
+ program
19
+ .name('buildflow')
20
+ .description('Adaptive AI-powered development orchestration')
21
+ .version(pkg.version)
22
+
23
+ program
24
+ .command('init')
25
+ .description('Initialize BuildFlow in the current project')
26
+ .option('-y, --yes', 'Skip prompts, use defaults')
27
+ .option('--greenfield', 'Start a brand-new project')
28
+ .option('--existing', 'Add BuildFlow to existing codebase')
29
+ .action(async (opts) => {
30
+ const { run } = await loadInit()
31
+ await run(opts)
32
+ })
33
+
34
+ program
35
+ .command('install')
36
+ .description('Install BuildFlow slash commands into an AI tool')
37
+ .option('-y, --yes', 'Skip prompts, use defaults')
38
+ .option('--tool <name>', 'AI tool to install into (claude|gemini|codex|cursor|all)')
39
+ .option('--global', 'Install globally (available in all projects)')
40
+ .option('--local', 'Install locally (current project only)')
41
+ .action(async (opts) => {
42
+ const { run } = await loadInstall()
43
+ await run(opts)
44
+ })
45
+
46
+ program
47
+ .command('audit')
48
+ .description('Run a security audit on the current project')
49
+ .option('-q, --quick', 'Quick audit (recent changes only)')
50
+ .option('-t, --target <path>', 'Audit specific file or directory')
51
+ .option('-c, --category <name>', 'Check specific OWASP category (A01-A10)')
52
+ .option('-r, --report', 'Show latest report')
53
+ .action(async (opts) => {
54
+ const { run } = await loadAudit()
55
+ await run(opts)
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
+
67
+ program
68
+ .command('status')
69
+ .description('Show BuildFlow status for current project')
70
+ .option('-v, --verbose', 'Show detailed status')
71
+ .action(async (opts) => {
72
+ const { run } = await loadStatus()
73
+ await run(opts)
74
+ })
75
+
76
+ program
77
+ .command('update')
78
+ .description('Update BuildFlow commands and agents in current project')
79
+ .option('--check', 'Check for updates without applying')
80
+ .action(async (opts) => {
81
+ const { run } = await loadUpdate()
82
+ await run(opts)
83
+ })
84
+
85
+ if (process.argv.length <= 2) {
86
+ const { showWelcome } = await import('../src/utils/welcome.js')
87
+ await showWelcome()
88
+ process.exit(0)
89
+ }
90
+
91
+ program.parse()
package/package.json CHANGED
@@ -1,60 +1,60 @@
1
- {
2
- "name": "buildflow-dev",
3
- "version": "1.0.0",
4
- "description": "Adaptive AI-powered development orchestration. Works with Claude Code, Gemini CLI, Codex CLI, Cursor, and more.",
5
- "keywords": [
6
- "ai",
7
- "claude",
8
- "gemini",
9
- "codex",
10
- "cursor",
11
- "developer-tools",
12
- "cli",
13
- "workflow",
14
- "scaffolding",
15
- "security-audit",
16
- "code-generation"
17
- ],
18
- "homepage": "https://github.com/Vikas-gurrapu/buildflow",
19
- "bugs": {
20
- "url": "https://github.com/Vikas-gurrapu/buildflow/issues"
21
- },
22
- "repository": {
23
- "type": "git",
24
- "url": "git+https://github.com/Vikas-gurrapu/buildflow.git"
25
- },
26
- "license": "MIT",
27
- "author": "Vikas Gurrapu <vikas.gurrapu@gmail.com>",
28
- "type": "module",
29
- "main": "src/index.js",
30
- "bin": {
31
- "buildflow": "./bin/buildflow.js",
32
- "bf": "./bin/buildflow.js"
33
- },
34
- "files": [
35
- "bin/",
36
- "src/",
37
- "templates/",
38
- "README.md",
39
- "LICENSE"
40
- ],
41
- "scripts": {
42
- "start": "node bin/buildflow.js",
43
- "test": "node --test src/**/*.test.js",
44
- "lint": "eslint src/ bin/",
45
- "prepublishOnly": "npm test"
46
- },
47
- "dependencies": {
48
- "chalk": "^5.3.0",
49
- "commander": "^11.1.0",
50
- "enquirer": "^2.4.1",
51
- "ora": "^8.0.1",
52
- "which": "^4.0.0"
53
- },
54
- "devDependencies": {
55
- "eslint": "^8.56.0"
56
- },
57
- "engines": {
58
- "node": ">=18.0.0"
59
- }
60
- }
1
+ {
2
+ "name": "buildflow-dev",
3
+ "version": "1.0.2",
4
+ "description": "Adaptive AI-powered development orchestration. Works with Claude Code, Gemini CLI, Codex CLI, Cursor, and more.",
5
+ "keywords": [
6
+ "ai",
7
+ "claude",
8
+ "gemini",
9
+ "codex",
10
+ "cursor",
11
+ "developer-tools",
12
+ "cli",
13
+ "workflow",
14
+ "scaffolding",
15
+ "security-audit",
16
+ "code-generation"
17
+ ],
18
+ "homepage": "https://github.com/Vikas-gurrapu/buildflow",
19
+ "bugs": {
20
+ "url": "https://github.com/Vikas-gurrapu/buildflow/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/Vikas-gurrapu/buildflow.git"
25
+ },
26
+ "license": "MIT",
27
+ "author": "Vikas Gurrapu <vikas.gurrapu@gmail.com>",
28
+ "type": "module",
29
+ "main": "src/index.js",
30
+ "bin": {
31
+ "buildflow": "./bin/buildflow.js",
32
+ "bf": "./bin/buildflow.js"
33
+ },
34
+ "files": [
35
+ "bin/",
36
+ "src/",
37
+ "templates/",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "scripts": {
42
+ "start": "node bin/buildflow.js",
43
+ "test": "node --test src/**/*.test.js",
44
+ "lint": "eslint src/ bin/",
45
+ "prepublishOnly": "npm test"
46
+ },
47
+ "dependencies": {
48
+ "chalk": "^5.3.0",
49
+ "commander": "^11.1.0",
50
+ "enquirer": "^2.4.1",
51
+ "ora": "^8.0.1",
52
+ "which": "^4.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "eslint": "^8.56.0"
56
+ },
57
+ "engines": {
58
+ "node": ">=18.0.0"
59
+ }
60
+ }
@@ -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
+ }