@girardmedia/bootspring 2.0.21 → 2.0.23

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 (159) hide show
  1. package/bin/bootspring.js +5 -0
  2. package/cli/org.js +474 -0
  3. package/cli/preseed/index.js +16 -0
  4. package/cli/preseed/interactive.js +143 -0
  5. package/cli/preseed/templates.js +227 -0
  6. package/cli/preseed.js +9 -301
  7. package/cli/seed/builders/ai-context-builder.js +85 -0
  8. package/cli/seed/builders/index.js +13 -0
  9. package/cli/seed/builders/seed-builder.js +272 -0
  10. package/cli/seed/extractors/content-extractors.js +383 -0
  11. package/cli/seed/extractors/index.js +47 -0
  12. package/cli/seed/extractors/metadata-extractors.js +167 -0
  13. package/cli/seed/extractors/section-extractor.js +54 -0
  14. package/cli/seed/extractors/stack-extractors.js +228 -0
  15. package/cli/seed/index.js +18 -0
  16. package/cli/seed/utils/folder-structure.js +84 -0
  17. package/cli/seed/utils/index.js +11 -0
  18. package/cli/seed.js +23 -1074
  19. package/core/api-client.js +77 -0
  20. package/core/entitlements.js +36 -0
  21. package/core/organizations.js +223 -0
  22. package/core/policies.js +51 -6
  23. package/core/policy-matrix.js +303 -0
  24. package/core/project-context.js +1 -0
  25. package/dist/cli/index.d.ts +3 -0
  26. package/dist/cli/index.js +3220 -0
  27. package/dist/cli/index.js.map +1 -0
  28. package/dist/context-McpJQa_2.d.ts +5710 -0
  29. package/dist/core/index.d.ts +635 -0
  30. package/dist/core/index.js +2593 -0
  31. package/dist/core/index.js.map +1 -0
  32. package/dist/index-QqbeEiDm.d.ts +857 -0
  33. package/dist/index-UiYCgwiH.d.ts +174 -0
  34. package/dist/index.d.ts +453 -0
  35. package/dist/index.js +44228 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/mcp/index.d.ts +1 -0
  38. package/dist/mcp/index.js +41173 -0
  39. package/dist/mcp/index.js.map +1 -0
  40. package/generators/index.ts +82 -0
  41. package/intelligence/orchestrator/config/failure-signatures.js +48 -0
  42. package/intelligence/orchestrator/config/index.js +23 -0
  43. package/intelligence/orchestrator/config/pack-lifecycle.js +262 -0
  44. package/intelligence/orchestrator/config/phases.js +111 -0
  45. package/intelligence/orchestrator/config/remediation.js +150 -0
  46. package/intelligence/orchestrator/config/workflows.js +168 -0
  47. package/intelligence/orchestrator/core/index.js +16 -0
  48. package/intelligence/orchestrator/core/state-manager.js +88 -0
  49. package/intelligence/orchestrator/core/telemetry.js +24 -0
  50. package/intelligence/orchestrator/index.js +17 -0
  51. package/intelligence/orchestrator.js +17 -512
  52. package/mcp/contracts/mcp-contract.v1.json +1 -1
  53. package/package.json +16 -3
  54. package/src/cli/agent.ts +703 -0
  55. package/src/cli/analyze.ts +640 -0
  56. package/src/cli/audit.ts +707 -0
  57. package/src/cli/auth.ts +930 -0
  58. package/src/cli/billing.ts +364 -0
  59. package/src/cli/build.ts +1089 -0
  60. package/src/cli/business.ts +508 -0
  61. package/src/cli/checkpoint-utils.ts +236 -0
  62. package/src/cli/checkpoint.ts +757 -0
  63. package/src/cli/cloud-sync.ts +534 -0
  64. package/src/cli/content.ts +273 -0
  65. package/src/cli/context.ts +667 -0
  66. package/src/cli/dashboard.ts +133 -0
  67. package/src/cli/deploy.ts +704 -0
  68. package/src/cli/doctor.ts +480 -0
  69. package/src/cli/fundraise.ts +494 -0
  70. package/src/cli/generate.ts +346 -0
  71. package/src/cli/github-cmd.ts +566 -0
  72. package/src/cli/health.ts +599 -0
  73. package/src/cli/index.ts +113 -0
  74. package/src/cli/init.ts +838 -0
  75. package/src/cli/legal.ts +495 -0
  76. package/src/cli/log.ts +316 -0
  77. package/src/cli/loop.ts +1660 -0
  78. package/src/cli/manager.ts +878 -0
  79. package/src/cli/mcp.ts +275 -0
  80. package/src/cli/memory.ts +346 -0
  81. package/src/cli/metrics.ts +590 -0
  82. package/src/cli/monitor.ts +960 -0
  83. package/src/cli/mvp.ts +662 -0
  84. package/src/cli/onboard.ts +663 -0
  85. package/src/cli/orchestrator.ts +622 -0
  86. package/src/cli/plugin.ts +483 -0
  87. package/src/cli/prd.ts +671 -0
  88. package/src/cli/preseed-start.ts +1633 -0
  89. package/src/cli/preseed.ts +2434 -0
  90. package/src/cli/project.ts +526 -0
  91. package/src/cli/quality.ts +885 -0
  92. package/src/cli/security.ts +1079 -0
  93. package/src/cli/seed.ts +1224 -0
  94. package/src/cli/skill.ts +537 -0
  95. package/src/cli/suggest.ts +1225 -0
  96. package/src/cli/switch.ts +518 -0
  97. package/src/cli/task.ts +780 -0
  98. package/src/cli/telemetry.ts +172 -0
  99. package/src/cli/todo.ts +627 -0
  100. package/src/cli/types.ts +15 -0
  101. package/src/cli/update.ts +334 -0
  102. package/src/cli/visualize.ts +609 -0
  103. package/src/cli/watch.ts +895 -0
  104. package/src/cli/workspace.ts +709 -0
  105. package/src/core/action-recorder.ts +673 -0
  106. package/src/core/analyze-workflow.ts +1453 -0
  107. package/src/core/api-client.ts +1120 -0
  108. package/src/core/audit-workflow.ts +1681 -0
  109. package/src/core/auth.ts +471 -0
  110. package/src/core/build-orchestrator.ts +509 -0
  111. package/src/core/build-state.ts +621 -0
  112. package/src/core/checkpoint-engine.ts +482 -0
  113. package/src/core/config.ts +1285 -0
  114. package/src/core/context-loader.ts +694 -0
  115. package/src/core/context.ts +410 -0
  116. package/src/core/deploy-workflow.ts +1085 -0
  117. package/src/core/entitlements.ts +322 -0
  118. package/src/core/github-sync.ts +720 -0
  119. package/src/core/index.ts +981 -0
  120. package/src/core/ingest.ts +1186 -0
  121. package/src/core/metrics-engine.ts +886 -0
  122. package/src/core/mvp.ts +847 -0
  123. package/src/core/onboard-workflow.ts +1293 -0
  124. package/src/core/policies.ts +81 -0
  125. package/src/core/preseed-workflow.ts +1163 -0
  126. package/src/core/preseed.ts +1826 -0
  127. package/src/core/project-context.ts +380 -0
  128. package/src/core/project-state.ts +699 -0
  129. package/src/core/r2-sync.ts +691 -0
  130. package/src/core/scaffold.ts +1715 -0
  131. package/src/core/session.ts +286 -0
  132. package/src/core/task-extractor.ts +799 -0
  133. package/src/core/telemetry.ts +371 -0
  134. package/src/core/tier-enforcement.ts +737 -0
  135. package/src/core/utils.ts +437 -0
  136. package/src/index.ts +29 -0
  137. package/src/intelligence/agent-collab.ts +2376 -0
  138. package/src/intelligence/auto-suggest.ts +713 -0
  139. package/src/intelligence/content-gen.ts +1351 -0
  140. package/src/intelligence/cross-project.ts +1692 -0
  141. package/src/intelligence/git-memory.ts +529 -0
  142. package/src/intelligence/index.ts +318 -0
  143. package/src/intelligence/orchestrator.ts +534 -0
  144. package/src/intelligence/prd.ts +466 -0
  145. package/src/intelligence/recommendations.ts +982 -0
  146. package/src/intelligence/workflow-composer.ts +1472 -0
  147. package/src/mcp/capabilities.ts +233 -0
  148. package/src/mcp/index.ts +37 -0
  149. package/src/mcp/registry.ts +1268 -0
  150. package/src/mcp/response-formatter.ts +797 -0
  151. package/src/mcp/server.ts +240 -0
  152. package/src/types/agent.ts +69 -0
  153. package/src/types/config.ts +86 -0
  154. package/src/types/context.ts +77 -0
  155. package/src/types/index.ts +53 -0
  156. package/src/types/mcp.ts +91 -0
  157. package/src/types/skills.ts +47 -0
  158. package/src/types/workflow.ts +155 -0
  159. package/generators/index.js +0 -18
@@ -0,0 +1,1079 @@
1
+ /**
2
+ * Bootspring Security Command
3
+ * Run security scans and store scores
4
+ *
5
+ * @package bootspring
6
+ * @command security
7
+ */
8
+
9
+ import { execSync } from 'child_process';
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+
13
+ // Interfaces
14
+ interface ConfigModule {
15
+ load: () => { _projectRoot: string };
16
+ }
17
+
18
+ interface UtilsModule {
19
+ COLORS: {
20
+ cyan: string;
21
+ bold: string;
22
+ reset: string;
23
+ dim: string;
24
+ green: string;
25
+ yellow: string;
26
+ red: string;
27
+ };
28
+ createSpinner: (text: string) => {
29
+ start: () => { succeed: (text: string) => void; fail: (text: string) => void; warn: (text: string) => void };
30
+ succeed: (text: string) => void;
31
+ fail: (text: string) => void;
32
+ warn: (text: string) => void;
33
+ };
34
+ createProgressBar?: (value: number, max: number, width: number) => string;
35
+ parseArgs: (args: string[]) => ParsedArgs;
36
+ print: {
37
+ error: (msg: string) => void;
38
+ success: (msg: string) => void;
39
+ warn: (msg: string) => void;
40
+ dim: (msg: string) => void;
41
+ };
42
+ }
43
+
44
+ interface ProjectStateModule {
45
+ getOrCreateState: (projectRoot: string) => ProjectState;
46
+ loadState: (projectRoot: string) => ProjectState | null;
47
+ saveState: (projectRoot: string, state: ProjectState) => void;
48
+ }
49
+
50
+ interface ParsedArgs {
51
+ _: string[];
52
+ skip?: string;
53
+ quiet?: boolean;
54
+ json?: boolean;
55
+ [key: string]: string | boolean | string[] | undefined;
56
+ }
57
+
58
+ interface SeveritySummary {
59
+ critical: number;
60
+ high: number;
61
+ moderate: number;
62
+ low: number;
63
+ info?: number;
64
+ }
65
+
66
+ interface CheckResult {
67
+ score: number;
68
+ issues: Issue[];
69
+ summary: SeveritySummary | HeadersSummary | EnvSummary | GitignoreSummary;
70
+ message?: string | undefined;
71
+ error?: string | undefined;
72
+ }
73
+
74
+ interface HeadersSummary {
75
+ missing: number;
76
+ configured: number;
77
+ }
78
+
79
+ interface EnvSummary {
80
+ warnings: number;
81
+ good: number;
82
+ }
83
+
84
+ interface GitignoreSummary {
85
+ missing: number;
86
+ covered: number;
87
+ }
88
+
89
+ interface Issue {
90
+ severity: string;
91
+ title: string;
92
+ package?: string | undefined;
93
+ fixAvailable?: boolean | undefined;
94
+ message?: string | undefined;
95
+ file?: string | undefined;
96
+ count?: number | undefined;
97
+ current?: string | undefined;
98
+ recommended?: string | undefined;
99
+ fix?: string | undefined;
100
+ recommendation?: string | undefined;
101
+ }
102
+
103
+ interface SecurityCheck {
104
+ name: string;
105
+ description: string;
106
+ weight: number;
107
+ run: (projectRoot: string) => Promise<CheckResult>;
108
+ }
109
+
110
+ interface ScanResults {
111
+ timestamp: string;
112
+ overallScore: number;
113
+ checks: Record<string, CheckResult>;
114
+ summary: SeveritySummary;
115
+ recommendations: Recommendation[];
116
+ }
117
+
118
+ interface Recommendation {
119
+ priority: string;
120
+ title: string;
121
+ action: string;
122
+ }
123
+
124
+ interface ProjectState {
125
+ security?: SecurityState | undefined;
126
+ health?: { breakdown?: { security?: number | undefined } | undefined } | undefined;
127
+ }
128
+
129
+ interface SecurityState {
130
+ score: number;
131
+ lastScan: string | null;
132
+ history: Array<{ score: number; date: string }>;
133
+ summary?: SeveritySummary | undefined;
134
+ checks?: Record<string, { score: number; issues: number }> | undefined;
135
+ }
136
+
137
+ interface ScanOptions {
138
+ skip?: string[] | undefined;
139
+ quiet?: boolean | undefined;
140
+ }
141
+
142
+ interface AuditMetadata {
143
+ vulnerabilities?: {
144
+ critical?: number | undefined;
145
+ high?: number | undefined;
146
+ moderate?: number | undefined;
147
+ low?: number | undefined;
148
+ info?: number | undefined;
149
+ } | undefined;
150
+ }
151
+
152
+ interface VulnerabilityData {
153
+ severity: string;
154
+ via?: Array<{ title?: string | undefined }> | undefined;
155
+ fixAvailable?: boolean | undefined;
156
+ }
157
+
158
+ interface AuditOutput {
159
+ metadata?: AuditMetadata | undefined;
160
+ vulnerabilities?: Record<string, VulnerabilityData> | undefined;
161
+ }
162
+
163
+ // Lazy load modules
164
+ const config = require('../core/config') as ConfigModule;
165
+ const utils = require('../core/utils') as UtilsModule;
166
+ const projectState = require('../core/project-state') as ProjectStateModule;
167
+
168
+ // ============================================================================
169
+ // Security Check Definitions
170
+ // ============================================================================
171
+
172
+ const SECURITY_CHECKS: Record<string, SecurityCheck> = {
173
+ dependencies: {
174
+ name: 'Dependency Audit',
175
+ description: 'Check for vulnerable npm packages',
176
+ weight: 30,
177
+ run: runDependencyAudit
178
+ },
179
+ secrets: {
180
+ name: 'Secrets Detection',
181
+ description: 'Scan for exposed secrets and API keys',
182
+ weight: 25,
183
+ run: runSecretsDetection
184
+ },
185
+ permissions: {
186
+ name: 'File Permissions',
187
+ description: 'Check for insecure file permissions',
188
+ weight: 10,
189
+ run: runPermissionsCheck
190
+ },
191
+ headers: {
192
+ name: 'Security Headers',
193
+ description: 'Verify security headers configuration',
194
+ weight: 15,
195
+ run: runHeadersCheck
196
+ },
197
+ env: {
198
+ name: 'Environment Variables',
199
+ description: 'Check .env files for best practices',
200
+ weight: 10,
201
+ run: runEnvCheck
202
+ },
203
+ gitignore: {
204
+ name: 'Gitignore Coverage',
205
+ description: 'Verify sensitive files are ignored',
206
+ weight: 10,
207
+ run: runGitignoreCheck
208
+ }
209
+ };
210
+
211
+ // Severity weights for scoring
212
+ const SEVERITY_WEIGHTS: Record<string, number> = {
213
+ critical: 40,
214
+ high: 25,
215
+ moderate: 10,
216
+ low: 5,
217
+ info: 1
218
+ };
219
+
220
+ // ============================================================================
221
+ // Security Check Implementations
222
+ // ============================================================================
223
+
224
+ /**
225
+ * Run npm audit for dependency vulnerabilities
226
+ */
227
+ async function runDependencyAudit(projectRoot: string): Promise<CheckResult> {
228
+ const result: CheckResult = {
229
+ score: 100,
230
+ issues: [],
231
+ summary: { critical: 0, high: 0, moderate: 0, low: 0, info: 0 }
232
+ };
233
+
234
+ try {
235
+ // Check if package-lock.json exists
236
+ const lockFile = path.join(projectRoot, 'package-lock.json');
237
+ if (!fs.existsSync(lockFile)) {
238
+ return {
239
+ score: 100,
240
+ issues: [],
241
+ summary: result.summary,
242
+ message: 'No package-lock.json found'
243
+ };
244
+ }
245
+
246
+ const output = execSync('npm audit --json 2>/dev/null || true', {
247
+ cwd: projectRoot,
248
+ encoding: 'utf-8',
249
+ maxBuffer: 10 * 1024 * 1024
250
+ });
251
+
252
+ if (output.trim()) {
253
+ const audit = JSON.parse(output) as AuditOutput;
254
+
255
+ if (audit.metadata?.vulnerabilities) {
256
+ const vulns = audit.metadata.vulnerabilities;
257
+ const summary = result.summary as SeveritySummary;
258
+ summary.critical = vulns.critical || 0;
259
+ summary.high = vulns.high || 0;
260
+ summary.moderate = vulns.moderate || 0;
261
+ summary.low = vulns.low || 0;
262
+ summary.info = vulns.info || 0;
263
+
264
+ // Calculate score based on vulnerabilities
265
+ const deductions =
266
+ (summary.critical * (SEVERITY_WEIGHTS.critical || 0)) +
267
+ (summary.high * (SEVERITY_WEIGHTS.high || 0)) +
268
+ (summary.moderate * (SEVERITY_WEIGHTS.moderate || 0)) +
269
+ (summary.low * (SEVERITY_WEIGHTS.low || 0));
270
+
271
+ result.score = Math.max(0, 100 - deductions);
272
+
273
+ // Add top issues
274
+ if (audit.vulnerabilities) {
275
+ const vulnList = Object.entries(audit.vulnerabilities).slice(0, 5);
276
+ for (const [name, data] of vulnList) {
277
+ result.issues.push({
278
+ severity: data.severity,
279
+ package: name,
280
+ title: data.via?.[0]?.title || 'Vulnerability found',
281
+ fixAvailable: data.fixAvailable
282
+ });
283
+ }
284
+ }
285
+ }
286
+ }
287
+ } catch (error) {
288
+ result.issues.push({
289
+ severity: 'info',
290
+ title: 'Could not run npm audit',
291
+ message: (error as Error).message
292
+ });
293
+ }
294
+
295
+ return result;
296
+ }
297
+
298
+ /**
299
+ * Scan for exposed secrets
300
+ */
301
+ async function runSecretsDetection(projectRoot: string): Promise<CheckResult> {
302
+ const result: CheckResult = {
303
+ score: 100,
304
+ issues: [],
305
+ summary: { critical: 0, high: 0, moderate: 0, low: 0 }
306
+ };
307
+
308
+ // Patterns to detect secrets
309
+ const secretPatterns: Array<{ pattern: RegExp; name: string; severity: string }> = [
310
+ { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][a-zA-Z0-9]{20,}['"]/gi, name: 'API Key', severity: 'high' },
311
+ { pattern: /(?:secret|password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/gi, name: 'Password/Secret', severity: 'critical' },
312
+ { pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, name: 'Private Key', severity: 'critical' },
313
+ { pattern: /(?:aws[_-]?(?:access|secret)[_-]?(?:key|id))\s*[:=]\s*['"][A-Z0-9]{16,}['"]/gi, name: 'AWS Credentials', severity: 'critical' },
314
+ { pattern: /ghp_[a-zA-Z0-9]{36}/g, name: 'GitHub Token', severity: 'high' },
315
+ { pattern: /sk-[a-zA-Z0-9]{48}/g, name: 'OpenAI API Key', severity: 'high' },
316
+ { pattern: /xox[baprs]-[a-zA-Z0-9-]{10,}/g, name: 'Slack Token', severity: 'high' },
317
+ { pattern: /AKIA[0-9A-Z]{16}/g, name: 'AWS Access Key ID', severity: 'critical' },
318
+ { pattern: /stripe[_-]?(?:sk|pk)_(?:live|test)_[a-zA-Z0-9]{24}/gi, name: 'Stripe Key', severity: 'high' }
319
+ ];
320
+
321
+ // Files to scan (exclude node_modules, .git, etc.)
322
+ const excludeDirs = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];
323
+ const includeExts = ['.js', '.ts', '.jsx', '.tsx', '.json', '.yaml', '.yml', '.env.example', '.config.js'];
324
+
325
+ function scanDir(dir: string, depth = 0): void {
326
+ if (depth > 5) return; // Limit depth
327
+
328
+ try {
329
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
330
+
331
+ for (const entry of entries) {
332
+ const fullPath = path.join(dir, entry.name);
333
+
334
+ if (entry.isDirectory()) {
335
+ if (!excludeDirs.includes(entry.name) && !entry.name.startsWith('.')) {
336
+ scanDir(fullPath, depth + 1);
337
+ }
338
+ } else if (entry.isFile()) {
339
+ const ext = path.extname(entry.name);
340
+
341
+ // Skip .env files (they're supposed to have secrets but should be gitignored)
342
+ if (entry.name === '.env' || entry.name.endsWith('.env.local')) {
343
+ continue;
344
+ }
345
+
346
+ if (includeExts.includes(ext) || entry.name.includes('.env.')) {
347
+ scanFile(fullPath);
348
+ }
349
+ }
350
+ }
351
+ } catch {
352
+ // Ignore permission errors
353
+ }
354
+ }
355
+
356
+ function scanFile(filePath: string): void {
357
+ try {
358
+ const content = fs.readFileSync(filePath, 'utf-8');
359
+ const relativePath = path.relative(projectRoot, filePath);
360
+
361
+ for (const { pattern, name, severity } of secretPatterns) {
362
+ pattern.lastIndex = 0; // Reset regex
363
+ const matches = content.match(pattern);
364
+
365
+ if (matches) {
366
+ const summary = result.summary as SeveritySummary;
367
+ const currentCount = (summary[severity as keyof SeveritySummary] as number | undefined) || 0;
368
+ (summary as unknown as Record<string, number>)[severity] = currentCount + matches.length;
369
+ result.issues.push({
370
+ severity,
371
+ title: `Potential ${name} exposed`,
372
+ file: relativePath,
373
+ count: matches.length
374
+ });
375
+ }
376
+ }
377
+ } catch {
378
+ // Ignore read errors
379
+ }
380
+ }
381
+
382
+ scanDir(projectRoot);
383
+
384
+ // Calculate score
385
+ const summary = result.summary as SeveritySummary;
386
+ const deductions =
387
+ (summary.critical * (SEVERITY_WEIGHTS.critical || 0)) +
388
+ (summary.high * (SEVERITY_WEIGHTS.high || 0)) +
389
+ (summary.moderate * (SEVERITY_WEIGHTS.moderate || 0)) +
390
+ (summary.low * (SEVERITY_WEIGHTS.low || 0));
391
+
392
+ result.score = Math.max(0, 100 - deductions);
393
+
394
+ return result;
395
+ }
396
+
397
+ /**
398
+ * Check file permissions
399
+ */
400
+ async function runPermissionsCheck(projectRoot: string): Promise<CheckResult> {
401
+ const result: CheckResult = {
402
+ score: 100,
403
+ issues: [],
404
+ summary: { critical: 0, high: 0, moderate: 0, low: 0 }
405
+ };
406
+
407
+ // Check common sensitive files
408
+ const sensitiveFiles: Array<{ path: string; recommended: string; severity: string }> = [
409
+ { path: '.env', recommended: '600', severity: 'high' },
410
+ { path: '.env.local', recommended: '600', severity: 'high' },
411
+ { path: 'id_rsa', recommended: '600', severity: 'critical' },
412
+ { path: '.ssh/id_rsa', recommended: '600', severity: 'critical' },
413
+ { path: 'credentials.json', recommended: '600', severity: 'high' },
414
+ { path: 'serviceAccount.json', recommended: '600', severity: 'high' }
415
+ ];
416
+
417
+ for (const { path: filePath, recommended, severity } of sensitiveFiles) {
418
+ const fullPath = path.join(projectRoot, filePath);
419
+
420
+ if (fs.existsSync(fullPath)) {
421
+ try {
422
+ const stats = fs.statSync(fullPath);
423
+ const mode = (stats.mode & parseInt('777', 8)).toString(8);
424
+
425
+ // Check if file is world-readable
426
+ if (stats.mode & parseInt('004', 8)) {
427
+ const summary = result.summary as SeveritySummary;
428
+ const currentVal = (summary as unknown as Record<string, number>)[severity] || 0;
429
+ (summary as unknown as Record<string, number>)[severity] = currentVal + 1;
430
+ result.issues.push({
431
+ severity,
432
+ title: `${filePath} is world-readable`,
433
+ current: mode,
434
+ recommended,
435
+ fix: `chmod ${recommended} ${filePath}`
436
+ });
437
+ }
438
+ } catch {
439
+ // Ignore stat errors
440
+ }
441
+ }
442
+ }
443
+
444
+ const summary = result.summary as SeveritySummary;
445
+ const deductions =
446
+ (summary.critical * 20) +
447
+ (summary.high * 10) +
448
+ (summary.moderate * 5);
449
+
450
+ result.score = Math.max(0, 100 - deductions);
451
+
452
+ return result;
453
+ }
454
+
455
+ /**
456
+ * Check security headers configuration
457
+ */
458
+ async function runHeadersCheck(projectRoot: string): Promise<CheckResult> {
459
+ const result: CheckResult = {
460
+ score: 100,
461
+ issues: [],
462
+ summary: { missing: 0, configured: 0 } as HeadersSummary
463
+ };
464
+
465
+ // Check for Next.js security headers
466
+ const nextConfig = path.join(projectRoot, 'next.config.js');
467
+ const nextConfigMjs = path.join(projectRoot, 'next.config.mjs');
468
+ const vercelJson = path.join(projectRoot, 'vercel.json');
469
+
470
+ const requiredHeaders = [
471
+ 'X-Content-Type-Options',
472
+ 'X-Frame-Options',
473
+ 'X-XSS-Protection',
474
+ 'Strict-Transport-Security',
475
+ 'Content-Security-Policy'
476
+ ];
477
+
478
+ let hasHeaderConfig = false;
479
+ const summary = result.summary as HeadersSummary;
480
+
481
+ // Check next.config.js
482
+ for (const configPath of [nextConfig, nextConfigMjs]) {
483
+ if (fs.existsSync(configPath)) {
484
+ const content = fs.readFileSync(configPath, 'utf-8');
485
+ if (content.includes('headers') || content.includes('securityHeaders')) {
486
+ hasHeaderConfig = true;
487
+
488
+ // Check which headers are configured
489
+ for (const header of requiredHeaders) {
490
+ if (content.includes(header)) {
491
+ summary.configured++;
492
+ } else {
493
+ summary.missing++;
494
+ result.issues.push({
495
+ severity: 'moderate',
496
+ title: `Missing ${header}`,
497
+ file: path.basename(configPath)
498
+ });
499
+ }
500
+ }
501
+ }
502
+ }
503
+ }
504
+
505
+ // Check vercel.json
506
+ if (fs.existsSync(vercelJson)) {
507
+ const content = fs.readFileSync(vercelJson, 'utf-8');
508
+ if (content.includes('headers')) {
509
+ hasHeaderConfig = true;
510
+ }
511
+ }
512
+
513
+ if (!hasHeaderConfig) {
514
+ result.score = 50;
515
+ result.issues.push({
516
+ severity: 'moderate',
517
+ title: 'No security headers configured',
518
+ recommendation: 'Add security headers to next.config.js or vercel.json'
519
+ });
520
+ } else {
521
+ result.score = Math.max(0, 100 - (summary.missing * 10));
522
+ }
523
+
524
+ return result;
525
+ }
526
+
527
+ /**
528
+ * Check environment variable best practices
529
+ */
530
+ async function runEnvCheck(projectRoot: string): Promise<CheckResult> {
531
+ const result: CheckResult = {
532
+ score: 100,
533
+ issues: [],
534
+ summary: { warnings: 0, good: 0 } as EnvSummary
535
+ };
536
+
537
+ const envExample = path.join(projectRoot, '.env.example');
538
+ const envLocal = path.join(projectRoot, '.env.local');
539
+ const envFile = path.join(projectRoot, '.env');
540
+ const summary = result.summary as EnvSummary;
541
+
542
+ // Check if .env.example exists
543
+ if (!fs.existsSync(envExample)) {
544
+ result.issues.push({
545
+ severity: 'low',
546
+ title: 'Missing .env.example',
547
+ recommendation: 'Create .env.example to document required environment variables'
548
+ });
549
+ summary.warnings++;
550
+ } else {
551
+ summary.good++;
552
+ }
553
+
554
+ // Check if .env is in .gitignore
555
+ const gitignore = path.join(projectRoot, '.gitignore');
556
+ if (fs.existsSync(gitignore)) {
557
+ const content = fs.readFileSync(gitignore, 'utf-8');
558
+
559
+ if (!content.includes('.env') || content.includes('.env.example')) {
560
+ if (fs.existsSync(envFile) || fs.existsSync(envLocal)) {
561
+ result.issues.push({
562
+ severity: 'high',
563
+ title: '.env files may not be properly gitignored',
564
+ recommendation: 'Ensure .env and .env.local are in .gitignore'
565
+ });
566
+ summary.warnings++;
567
+ }
568
+ } else {
569
+ summary.good++;
570
+ }
571
+ }
572
+
573
+ // Check for hardcoded values that should be env vars
574
+ const configFiles = ['next.config.js', 'next.config.mjs', 'config.js'];
575
+ const hardcodedPatterns = [
576
+ /https?:\/\/[a-z0-9-]+\.(supabase|neon|planetscale)\.co/gi,
577
+ /mongodb\+srv:\/\//gi,
578
+ /postgres:\/\//gi
579
+ ];
580
+
581
+ for (const configFile of configFiles) {
582
+ const filePath = path.join(projectRoot, configFile);
583
+ if (fs.existsSync(filePath)) {
584
+ const content = fs.readFileSync(filePath, 'utf-8');
585
+
586
+ for (const pattern of hardcodedPatterns) {
587
+ pattern.lastIndex = 0;
588
+ if (pattern.test(content) && !content.includes('process.env')) {
589
+ result.issues.push({
590
+ severity: 'moderate',
591
+ title: `Potential hardcoded URL in ${configFile}`,
592
+ recommendation: 'Use environment variables for database/service URLs'
593
+ });
594
+ summary.warnings++;
595
+ }
596
+ }
597
+ }
598
+ }
599
+
600
+ result.score = Math.max(0, 100 - (summary.warnings * 15));
601
+
602
+ return result;
603
+ }
604
+
605
+ /**
606
+ * Check gitignore for sensitive file coverage
607
+ */
608
+ async function runGitignoreCheck(projectRoot: string): Promise<CheckResult> {
609
+ const result: CheckResult = {
610
+ score: 100,
611
+ issues: [],
612
+ summary: { missing: 0, covered: 0 } as GitignoreSummary
613
+ };
614
+
615
+ const gitignore = path.join(projectRoot, '.gitignore');
616
+
617
+ if (!fs.existsSync(gitignore)) {
618
+ return {
619
+ score: 50,
620
+ issues: [{ severity: 'high', title: 'No .gitignore file found' }],
621
+ summary: { missing: 1, covered: 0 } as GitignoreSummary
622
+ };
623
+ }
624
+
625
+ const content = fs.readFileSync(gitignore, 'utf-8');
626
+ const summary = result.summary as GitignoreSummary;
627
+
628
+ // Sensitive patterns that should be gitignored
629
+ const requiredPatterns: Array<{ pattern: string; severity: string; name: string }> = [
630
+ { pattern: '.env', severity: 'high', name: 'Environment files' },
631
+ { pattern: '.env.local', severity: 'high', name: 'Local env files' },
632
+ { pattern: 'node_modules', severity: 'moderate', name: 'Node modules' },
633
+ { pattern: '*.pem', severity: 'critical', name: 'PEM certificates' },
634
+ { pattern: '*.key', severity: 'critical', name: 'Key files' },
635
+ { pattern: '.DS_Store', severity: 'low', name: 'macOS files' },
636
+ { pattern: 'coverage', severity: 'low', name: 'Coverage reports' }
637
+ ];
638
+
639
+ for (const { pattern, severity, name } of requiredPatterns) {
640
+ if (content.includes(pattern)) {
641
+ summary.covered++;
642
+ } else {
643
+ // Check if file exists before warning
644
+ const patternPath = path.join(projectRoot, pattern.replace('*', ''));
645
+ if (fs.existsSync(patternPath) || pattern.includes('*')) {
646
+ summary.missing++;
647
+ result.issues.push({
648
+ severity,
649
+ title: `${name} (${pattern}) not in .gitignore`,
650
+ recommendation: `Add ${pattern} to .gitignore`
651
+ });
652
+ }
653
+ }
654
+ }
655
+
656
+ const deductions =
657
+ (result.issues.filter(i => i.severity === 'critical').length * 20) +
658
+ (result.issues.filter(i => i.severity === 'high').length * 15) +
659
+ (result.issues.filter(i => i.severity === 'moderate').length * 5);
660
+
661
+ result.score = Math.max(0, 100 - deductions);
662
+
663
+ return result;
664
+ }
665
+
666
+ // ============================================================================
667
+ // Main Security Functions
668
+ // ============================================================================
669
+
670
+ /**
671
+ * Run all security checks and calculate score
672
+ */
673
+ async function runFullScan(projectRoot: string, options: ScanOptions = {}): Promise<ScanResults> {
674
+ const results: ScanResults = {
675
+ timestamp: new Date().toISOString(),
676
+ overallScore: 0,
677
+ checks: {},
678
+ summary: {
679
+ critical: 0,
680
+ high: 0,
681
+ moderate: 0,
682
+ low: 0,
683
+ info: 0
684
+ },
685
+ recommendations: []
686
+ };
687
+
688
+ console.log(`
689
+ ${utils.COLORS.cyan}${utils.COLORS.bold}🔒 Security Scan${utils.COLORS.reset}
690
+ ${utils.COLORS.dim}Running comprehensive security checks...${utils.COLORS.reset}
691
+ `);
692
+
693
+ let totalWeight = 0;
694
+ let weightedScore = 0;
695
+
696
+ for (const [id, check] of Object.entries(SECURITY_CHECKS)) {
697
+ if (options.skip && options.skip.includes(id)) {
698
+ console.log(`${utils.COLORS.dim}○${utils.COLORS.reset} ${check.name}: skipped`);
699
+ continue;
700
+ }
701
+
702
+ const spinner = utils.createSpinner(`${check.name}: ${check.description}`).start();
703
+
704
+ try {
705
+ const result = await check.run(projectRoot);
706
+ results.checks[id] = result;
707
+
708
+ // Update summary
709
+ if (result.summary) {
710
+ for (const [severity, count] of Object.entries(result.summary)) {
711
+ if (results.summary[severity as keyof SeveritySummary] !== undefined) {
712
+ results.summary[severity as keyof SeveritySummary] += count as number;
713
+ }
714
+ }
715
+ }
716
+
717
+ // Calculate weighted score
718
+ totalWeight += check.weight;
719
+ weightedScore += result.score * check.weight;
720
+
721
+ // Show result
722
+ if (result.score >= 90) {
723
+ spinner.succeed(`${check.name}: ${utils.COLORS.green}${result.score}%${utils.COLORS.reset}`);
724
+ } else if (result.score >= 70) {
725
+ spinner.warn(`${check.name}: ${utils.COLORS.yellow}${result.score}%${utils.COLORS.reset}`);
726
+ } else {
727
+ spinner.fail(`${check.name}: ${utils.COLORS.red}${result.score}%${utils.COLORS.reset}`);
728
+ }
729
+
730
+ // Show critical issues
731
+ if (!options.quiet && result.issues.length > 0) {
732
+ const criticalIssues = result.issues.filter(i =>
733
+ i.severity === 'critical' || i.severity === 'high'
734
+ );
735
+ for (const issue of criticalIssues.slice(0, 3)) {
736
+ const color = issue.severity === 'critical' ? utils.COLORS.red : utils.COLORS.yellow;
737
+ console.log(` ${color}└ ${issue.title}${utils.COLORS.reset}`);
738
+ }
739
+ }
740
+ } catch (error) {
741
+ spinner.fail(`${check.name}: error`);
742
+ results.checks[id] = { score: 0, error: (error as Error).message, issues: [], summary: { critical: 0, high: 0, moderate: 0, low: 0 } };
743
+ }
744
+ }
745
+
746
+ // Calculate overall score
747
+ results.overallScore = totalWeight > 0
748
+ ? Math.round(weightedScore / totalWeight)
749
+ : 0;
750
+
751
+ // Generate recommendations
752
+ results.recommendations = generateRecommendations(results);
753
+
754
+ return results;
755
+ }
756
+
757
+ /**
758
+ * Generate recommendations based on scan results
759
+ */
760
+ function generateRecommendations(results: ScanResults): Recommendation[] {
761
+ const recommendations: Recommendation[] = [];
762
+
763
+ // Priority: critical issues first
764
+ if (results.summary.critical > 0) {
765
+ recommendations.push({
766
+ priority: 'critical',
767
+ title: `Fix ${results.summary.critical} critical security issue(s)`,
768
+ action: 'Review critical findings and address immediately'
769
+ });
770
+ }
771
+
772
+ if (results.summary.high > 0) {
773
+ recommendations.push({
774
+ priority: 'high',
775
+ title: `Address ${results.summary.high} high priority issue(s)`,
776
+ action: 'Run `npm audit fix` for dependency issues'
777
+ });
778
+ }
779
+
780
+ // Check-specific recommendations
781
+ const secretsScore = results.checks.secrets?.score;
782
+ if (secretsScore !== undefined && secretsScore < 100) {
783
+ recommendations.push({
784
+ priority: 'high',
785
+ title: 'Remove exposed secrets from codebase',
786
+ action: 'Move secrets to .env files and rotate compromised credentials'
787
+ });
788
+ }
789
+
790
+ const headersScore = results.checks.headers?.score;
791
+ if (headersScore !== undefined && headersScore < 70) {
792
+ recommendations.push({
793
+ priority: 'moderate',
794
+ title: 'Configure security headers',
795
+ action: 'Add security headers to next.config.js or vercel.json'
796
+ });
797
+ }
798
+
799
+ const gitignoreScore = results.checks.gitignore?.score;
800
+ if (gitignoreScore !== undefined && gitignoreScore < 90) {
801
+ recommendations.push({
802
+ priority: 'moderate',
803
+ title: 'Update .gitignore',
804
+ action: 'Ensure sensitive files are excluded from version control'
805
+ });
806
+ }
807
+
808
+ return recommendations;
809
+ }
810
+
811
+ /**
812
+ * Store security score in project state
813
+ */
814
+ function storeSecurityScore(projectRoot: string, results: ScanResults): SecurityState {
815
+ const state = projectState.getOrCreateState(projectRoot);
816
+
817
+ // Add security section if not exists
818
+ if (!state.security) {
819
+ state.security = {
820
+ score: 0,
821
+ lastScan: null,
822
+ history: []
823
+ };
824
+ }
825
+
826
+ // Update security data
827
+ state.security = {
828
+ score: results.overallScore,
829
+ lastScan: results.timestamp,
830
+ summary: results.summary,
831
+ checks: Object.fromEntries(
832
+ Object.entries(results.checks).map(([id, check]) => [
833
+ id,
834
+ { score: check.score, issues: check.issues?.length || 0 }
835
+ ])
836
+ ),
837
+ history: [
838
+ { score: results.overallScore, date: results.timestamp },
839
+ ...(state.security.history || []).slice(0, 9) // Keep last 10
840
+ ]
841
+ };
842
+
843
+ // Update health breakdown if exists
844
+ if (state.health?.breakdown) {
845
+ state.health.breakdown.security = Math.round(results.overallScore * 0.25); // 25% of overall health
846
+ }
847
+
848
+ projectState.saveState(projectRoot, state);
849
+
850
+ return state.security;
851
+ }
852
+
853
+ /**
854
+ * Show security status
855
+ */
856
+ function showStatus(projectRoot: string): void {
857
+ const state = projectState.loadState(projectRoot);
858
+
859
+ console.log(`
860
+ ${utils.COLORS.cyan}${utils.COLORS.bold}🔒 Security Status${utils.COLORS.reset}
861
+ `);
862
+
863
+ if (!state?.security?.lastScan) {
864
+ console.log(`${utils.COLORS.dim}No security scan has been run yet.${utils.COLORS.reset}`);
865
+ console.log(`${utils.COLORS.dim}Run 'bootspring security scan' to perform a scan.${utils.COLORS.reset}`);
866
+ return;
867
+ }
868
+
869
+ const sec = state.security;
870
+ const scoreColor = sec.score >= 90 ? utils.COLORS.green :
871
+ sec.score >= 70 ? utils.COLORS.yellow : utils.COLORS.red;
872
+
873
+ console.log(`${utils.COLORS.bold}Overall Score${utils.COLORS.reset}`);
874
+ console.log(` ${scoreColor}${utils.COLORS.bold}${sec.score}%${utils.COLORS.reset}`);
875
+ const lastScanDate = sec.lastScan ? new Date(sec.lastScan).toLocaleString() : 'Unknown';
876
+ console.log(` ${utils.COLORS.dim}Last scan: ${lastScanDate}${utils.COLORS.reset}`);
877
+ console.log();
878
+
879
+ if (sec.summary) {
880
+ console.log(`${utils.COLORS.bold}Issues Found${utils.COLORS.reset}`);
881
+ console.log(` ${utils.COLORS.red}Critical: ${sec.summary.critical || 0}${utils.COLORS.reset}`);
882
+ console.log(` ${utils.COLORS.yellow}High: ${sec.summary.high || 0}${utils.COLORS.reset}`);
883
+ console.log(` ${utils.COLORS.cyan}Moderate: ${sec.summary.moderate || 0}${utils.COLORS.reset}`);
884
+ console.log(` ${utils.COLORS.dim}Low: ${sec.summary.low || 0}${utils.COLORS.reset}`);
885
+ console.log();
886
+ }
887
+
888
+ if (sec.checks) {
889
+ console.log(`${utils.COLORS.bold}Check Scores${utils.COLORS.reset}`);
890
+ for (const [id, check] of Object.entries(sec.checks)) {
891
+ const name = SECURITY_CHECKS[id]?.name || id;
892
+ const checkColor = check.score >= 90 ? utils.COLORS.green :
893
+ check.score >= 70 ? utils.COLORS.yellow : utils.COLORS.red;
894
+ const bar = utils.createProgressBar ?
895
+ utils.createProgressBar(check.score, 100, 20) :
896
+ `${check.score}%`;
897
+ console.log(` ${name.padEnd(20)} ${checkColor}${bar}${utils.COLORS.reset}`);
898
+ }
899
+ console.log();
900
+ }
901
+
902
+ if (sec.history && sec.history.length > 1) {
903
+ console.log(`${utils.COLORS.bold}Score History${utils.COLORS.reset}`);
904
+ const firstHistory = sec.history[0];
905
+ const secondHistory = sec.history[1];
906
+ if (firstHistory && secondHistory) {
907
+ const trend = firstHistory.score - secondHistory.score;
908
+ const trendIcon = trend > 0 ? '↑' : trend < 0 ? '↓' : '→';
909
+ const trendColor = trend > 0 ? utils.COLORS.green : trend < 0 ? utils.COLORS.red : utils.COLORS.dim;
910
+ console.log(` ${trendColor}${trendIcon} ${Math.abs(trend)} points from last scan${utils.COLORS.reset}`);
911
+ console.log(` ${utils.COLORS.dim}${sec.history.slice(0, 5).map(h => h.score).join(' → ')}${utils.COLORS.reset}`);
912
+ }
913
+ }
914
+ }
915
+
916
+ /**
917
+ * Show detailed findings
918
+ */
919
+ function showFindings(projectRoot: string): void {
920
+ const state = projectState.loadState(projectRoot);
921
+
922
+ if (!state?.security?.lastScan) {
923
+ console.log(`${utils.COLORS.dim}No security scan results. Run 'bootspring security scan' first.${utils.COLORS.reset}`);
924
+ return;
925
+ }
926
+
927
+ // Re-run scan to get detailed findings
928
+ console.log(`${utils.COLORS.dim}Re-scanning for detailed findings...${utils.COLORS.reset}\n`);
929
+ }
930
+
931
+ /**
932
+ * Quick security check
933
+ */
934
+ async function runQuickCheck(projectRoot: string): Promise<void> {
935
+ console.log(`
936
+ ${utils.COLORS.cyan}${utils.COLORS.bold}🔒 Quick Security Check${utils.COLORS.reset}
937
+ `);
938
+
939
+ // Just run dependency audit and secrets detection
940
+ const results = {
941
+ dependencies: await runDependencyAudit(projectRoot),
942
+ secrets: await runSecretsDetection(projectRoot)
943
+ };
944
+
945
+ const depSummary = results.dependencies.summary as SeveritySummary;
946
+ const secretsSummary = results.secrets.summary as SeveritySummary;
947
+
948
+ const issues =
949
+ (depSummary.critical || 0) +
950
+ (depSummary.high || 0) +
951
+ (secretsSummary.critical || 0) +
952
+ (secretsSummary.high || 0);
953
+
954
+ if (issues === 0) {
955
+ utils.print.success('No critical security issues found');
956
+ } else {
957
+ utils.print.warn(`Found ${issues} high/critical security issue(s)`);
958
+ console.log(`${utils.COLORS.dim}Run 'bootspring security scan' for full details${utils.COLORS.reset}`);
959
+ }
960
+ }
961
+
962
+ /**
963
+ * Show help
964
+ */
965
+ function showHelp(): void {
966
+ console.log(`
967
+ ${utils.COLORS.cyan}${utils.COLORS.bold}🔒 Bootspring Security${utils.COLORS.reset}
968
+ ${utils.COLORS.dim}Security scanning and score tracking${utils.COLORS.reset}
969
+
970
+ ${utils.COLORS.bold}Usage:${utils.COLORS.reset}
971
+ bootspring security <command> [options]
972
+
973
+ ${utils.COLORS.bold}Commands:${utils.COLORS.reset}
974
+ ${utils.COLORS.cyan}scan${utils.COLORS.reset} Run full security scan and store score
975
+ ${utils.COLORS.cyan}quick${utils.COLORS.reset} Quick check (deps + secrets only)
976
+ ${utils.COLORS.cyan}status${utils.COLORS.reset} Show current security score and history
977
+ ${utils.COLORS.cyan}findings${utils.COLORS.reset} Show detailed findings from last scan
978
+
979
+ ${utils.COLORS.bold}Options:${utils.COLORS.reset}
980
+ --skip <check> Skip specific check (deps, secrets, headers, etc.)
981
+ --quiet Suppress detailed output
982
+ --json Output results as JSON
983
+
984
+ ${utils.COLORS.bold}Checks Performed:${utils.COLORS.reset}
985
+ ${utils.COLORS.dim}dependencies${utils.COLORS.reset} npm audit for vulnerable packages
986
+ ${utils.COLORS.dim}secrets${utils.COLORS.reset} Scan for exposed API keys and passwords
987
+ ${utils.COLORS.dim}permissions${utils.COLORS.reset} Check file permissions on sensitive files
988
+ ${utils.COLORS.dim}headers${utils.COLORS.reset} Verify security headers configuration
989
+ ${utils.COLORS.dim}env${utils.COLORS.reset} Check .env best practices
990
+ ${utils.COLORS.dim}gitignore${utils.COLORS.reset} Verify sensitive files are ignored
991
+
992
+ ${utils.COLORS.bold}Examples:${utils.COLORS.reset}
993
+ bootspring security scan
994
+ bootspring security scan --skip headers
995
+ bootspring security quick
996
+ bootspring security status
997
+ `);
998
+ }
999
+
1000
+ /**
1001
+ * Main run function
1002
+ */
1003
+ export async function run(args: string[]): Promise<void> {
1004
+ const parsedArgs = utils.parseArgs(args);
1005
+ const subcommand = parsedArgs._[0];
1006
+ const cfg = config.load();
1007
+ const projectRoot = cfg._projectRoot;
1008
+
1009
+ switch (subcommand) {
1010
+ case 'scan':
1011
+ case 'full': {
1012
+ const results = await runFullScan(projectRoot, {
1013
+ skip: parsedArgs.skip ? [parsedArgs.skip] : [],
1014
+ quiet: parsedArgs.quiet
1015
+ });
1016
+
1017
+ // Store score
1018
+ storeSecurityScore(projectRoot, results);
1019
+
1020
+ // Show summary
1021
+ console.log();
1022
+ const scoreColor = results.overallScore >= 90 ? utils.COLORS.green :
1023
+ results.overallScore >= 70 ? utils.COLORS.yellow : utils.COLORS.red;
1024
+
1025
+ console.log(`${utils.COLORS.bold}Security Score: ${scoreColor}${results.overallScore}%${utils.COLORS.reset}`);
1026
+
1027
+ if (results.recommendations.length > 0) {
1028
+ console.log(`\n${utils.COLORS.bold}Top Recommendations:${utils.COLORS.reset}`);
1029
+ for (const rec of results.recommendations.slice(0, 3)) {
1030
+ const icon = rec.priority === 'critical' ? '🔴' :
1031
+ rec.priority === 'high' ? '🟡' : '🔵';
1032
+ console.log(` ${icon} ${rec.title}`);
1033
+ }
1034
+ }
1035
+
1036
+ console.log(`\n${utils.COLORS.dim}Score saved to planning/PROJECT_STATE.json${utils.COLORS.reset}`);
1037
+
1038
+ if (parsedArgs.json) {
1039
+ console.log(JSON.stringify(results, null, 2));
1040
+ }
1041
+
1042
+ // Exit with error if critical issues
1043
+ if (results.summary.critical > 0) {
1044
+ process.exitCode = 1;
1045
+ }
1046
+ break;
1047
+ }
1048
+
1049
+ case 'quick':
1050
+ case 'check':
1051
+ await runQuickCheck(projectRoot);
1052
+ break;
1053
+
1054
+ case 'status':
1055
+ showStatus(projectRoot);
1056
+ break;
1057
+
1058
+ case 'findings':
1059
+ case 'details':
1060
+ showFindings(projectRoot);
1061
+ break;
1062
+
1063
+ case 'help':
1064
+ case '-h':
1065
+ case '--help':
1066
+ showHelp();
1067
+ break;
1068
+
1069
+ default:
1070
+ if (!subcommand) {
1071
+ showHelp();
1072
+ } else {
1073
+ utils.print.error(`Unknown command: ${subcommand}`);
1074
+ showHelp();
1075
+ }
1076
+ }
1077
+ }
1078
+
1079
+ export { runFullScan, storeSecurityScore, SECURITY_CHECKS };