wogiflow 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,1177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Verification Gates
|
|
5
|
+
*
|
|
6
|
+
* Enhanced verification system with:
|
|
7
|
+
* - Structured gate results
|
|
8
|
+
* - Auto-capture of stderr for LLM analysis
|
|
9
|
+
* - Rich error context for self-healing
|
|
10
|
+
* - Retry with fix suggestions
|
|
11
|
+
*
|
|
12
|
+
* Usage as module:
|
|
13
|
+
* const { runGate, runGates, GateResult } = require('./flow-verify');
|
|
14
|
+
* const result = await runGate('lint');
|
|
15
|
+
*
|
|
16
|
+
* Usage as CLI:
|
|
17
|
+
* flow verify lint # Run single gate
|
|
18
|
+
* flow verify all # Run all configured gates
|
|
19
|
+
* flow verify --json # Output JSON for CI
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { spawn, execSync } = require('child_process');
|
|
25
|
+
const { getProjectRoot, getConfig, colors: c } = require('./flow-utils');
|
|
26
|
+
const { recordCommandResult } = require('./flow-metrics');
|
|
27
|
+
|
|
28
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
29
|
+
const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Gate result structure
|
|
33
|
+
*/
|
|
34
|
+
class GateResult {
|
|
35
|
+
constructor(name) {
|
|
36
|
+
this.name = name;
|
|
37
|
+
this.passed = false;
|
|
38
|
+
this.exitCode = null;
|
|
39
|
+
this.duration = 0;
|
|
40
|
+
this.stdout = '';
|
|
41
|
+
this.stderr = '';
|
|
42
|
+
this.command = '';
|
|
43
|
+
this.errors = [];
|
|
44
|
+
this.warnings = [];
|
|
45
|
+
this.fixSuggestions = [];
|
|
46
|
+
this.timestamp = new Date().toISOString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
toJSON() {
|
|
50
|
+
return {
|
|
51
|
+
name: this.name,
|
|
52
|
+
passed: this.passed,
|
|
53
|
+
exitCode: this.exitCode,
|
|
54
|
+
duration: this.duration,
|
|
55
|
+
command: this.command,
|
|
56
|
+
errors: this.errors,
|
|
57
|
+
warnings: this.warnings,
|
|
58
|
+
fixSuggestions: this.fixSuggestions,
|
|
59
|
+
timestamp: this.timestamp,
|
|
60
|
+
// Only include output if there are errors
|
|
61
|
+
...(this.errors.length > 0 && { stdout: this.stdout, stderr: this.stderr })
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format for LLM consumption with rich error context
|
|
67
|
+
*/
|
|
68
|
+
toLLMContext() {
|
|
69
|
+
if (this.passed) {
|
|
70
|
+
return `✅ Gate "${this.name}" passed`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let context = `❌ Gate "${this.name}" FAILED\n\n`;
|
|
74
|
+
context += `Command: ${this.command}\n`;
|
|
75
|
+
context += `Exit Code: ${this.exitCode}\n`;
|
|
76
|
+
context += `Duration: ${this.duration}ms\n\n`;
|
|
77
|
+
|
|
78
|
+
if (this.errors.length > 0) {
|
|
79
|
+
context += `## Errors (${this.errors.length})\n`;
|
|
80
|
+
for (const err of this.errors) {
|
|
81
|
+
context += `\n### ${err.file || 'Unknown'}:${err.line || '?'}\n`;
|
|
82
|
+
context += `**Message**: ${err.message}\n`;
|
|
83
|
+
if (err.code) context += `**Code**: ${err.code}\n`;
|
|
84
|
+
if (err.rule) context += `**Rule**: ${err.rule}\n`;
|
|
85
|
+
if (err.snippet) context += `\`\`\`\n${err.snippet}\n\`\`\`\n`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (this.fixSuggestions.length > 0) {
|
|
90
|
+
context += `\n## Fix Suggestions\n`;
|
|
91
|
+
for (const fix of this.fixSuggestions) {
|
|
92
|
+
context += `- ${fix}\n`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.stderr && this.errors.length === 0) {
|
|
97
|
+
context += `\n## Raw stderr\n\`\`\`\n${this.stderr.slice(0, 3000)}\n\`\`\`\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return context;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Default gate configurations
|
|
106
|
+
*/
|
|
107
|
+
const DEFAULT_GATES = {
|
|
108
|
+
lint: {
|
|
109
|
+
name: 'Lint',
|
|
110
|
+
commands: [
|
|
111
|
+
{ cmd: 'npx', args: ['eslint', '.', '--ext', '.ts,.tsx,.js,.jsx'], detect: 'eslint' },
|
|
112
|
+
{ cmd: 'npx', args: ['biome', 'check', '.'], detect: '@biomejs/biome' }
|
|
113
|
+
],
|
|
114
|
+
parser: 'eslint',
|
|
115
|
+
autoFix: { cmd: 'npx', args: ['eslint', '.', '--fix'] }
|
|
116
|
+
},
|
|
117
|
+
typecheck: {
|
|
118
|
+
name: 'TypeCheck',
|
|
119
|
+
commands: [
|
|
120
|
+
{ cmd: 'npx', args: ['tsc', '--noEmit'], detect: 'typescript' }
|
|
121
|
+
],
|
|
122
|
+
parser: 'typescript'
|
|
123
|
+
},
|
|
124
|
+
test: {
|
|
125
|
+
name: 'Test',
|
|
126
|
+
commands: [
|
|
127
|
+
{ cmd: 'npx', args: ['vitest', 'run'], detect: 'vitest' },
|
|
128
|
+
{ cmd: 'npx', args: ['jest'], detect: 'jest' },
|
|
129
|
+
{ cmd: 'npm', args: ['test'], detect: null }
|
|
130
|
+
],
|
|
131
|
+
parser: 'jest'
|
|
132
|
+
},
|
|
133
|
+
build: {
|
|
134
|
+
name: 'Build',
|
|
135
|
+
commands: [
|
|
136
|
+
{ cmd: 'npm', args: ['run', 'build'], detect: null }
|
|
137
|
+
],
|
|
138
|
+
parser: 'generic'
|
|
139
|
+
},
|
|
140
|
+
format: {
|
|
141
|
+
name: 'Format Check',
|
|
142
|
+
commands: [
|
|
143
|
+
{ cmd: 'npx', args: ['prettier', '--check', '.'], detect: 'prettier' },
|
|
144
|
+
{ cmd: 'npx', args: ['biome', 'format', '--check', '.'], detect: '@biomejs/biome' }
|
|
145
|
+
],
|
|
146
|
+
parser: 'prettier',
|
|
147
|
+
autoFix: { cmd: 'npx', args: ['prettier', '--write', '.'] }
|
|
148
|
+
},
|
|
149
|
+
securityScan: {
|
|
150
|
+
name: 'Security Scan',
|
|
151
|
+
commands: [
|
|
152
|
+
{ cmd: 'npm', args: ['audit', '--json'], detect: null }
|
|
153
|
+
],
|
|
154
|
+
parser: 'security',
|
|
155
|
+
customChecks: ['secrets', 'injection', 'evalExec']
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Error parsers for different tools
|
|
161
|
+
*/
|
|
162
|
+
const ERROR_PARSERS = {
|
|
163
|
+
eslint: (output) => {
|
|
164
|
+
const errors = [];
|
|
165
|
+
// ESLint output: /path/file.ts:line:col: message (rule)
|
|
166
|
+
const lines = output.split('\n');
|
|
167
|
+
const errorRegex = /^(.+):(\d+):(\d+):\s*(.+?)\s*(\([\w\/@-]+\))?$/;
|
|
168
|
+
const warningRegex = /warning/i;
|
|
169
|
+
|
|
170
|
+
for (const line of lines) {
|
|
171
|
+
const match = line.match(errorRegex);
|
|
172
|
+
if (match) {
|
|
173
|
+
const [, file, lineNum, col, message, rule] = match;
|
|
174
|
+
errors.push({
|
|
175
|
+
file: path.relative(PROJECT_ROOT, file),
|
|
176
|
+
line: parseInt(lineNum),
|
|
177
|
+
column: parseInt(col),
|
|
178
|
+
message: message.trim(),
|
|
179
|
+
rule: rule ? rule.replace(/[()]/g, '') : null,
|
|
180
|
+
severity: warningRegex.test(line) ? 'warning' : 'error'
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return errors;
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
typescript: (output) => {
|
|
188
|
+
const errors = [];
|
|
189
|
+
// TypeScript output: file.ts(line,col): error TS1234: message
|
|
190
|
+
const lines = output.split('\n');
|
|
191
|
+
const errorRegex = /^(.+)\((\d+),(\d+)\):\s*(error|warning)\s+(TS\d+):\s*(.+)$/;
|
|
192
|
+
|
|
193
|
+
for (const line of lines) {
|
|
194
|
+
const match = line.match(errorRegex);
|
|
195
|
+
if (match) {
|
|
196
|
+
const [, file, lineNum, col, severity, code, message] = match;
|
|
197
|
+
errors.push({
|
|
198
|
+
file: path.relative(PROJECT_ROOT, file),
|
|
199
|
+
line: parseInt(lineNum),
|
|
200
|
+
column: parseInt(col),
|
|
201
|
+
message,
|
|
202
|
+
code,
|
|
203
|
+
severity: severity.toLowerCase()
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return errors;
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
jest: (output) => {
|
|
211
|
+
const errors = [];
|
|
212
|
+
// Jest failure patterns
|
|
213
|
+
const failedTestRegex = /✕\s+(.+)/g;
|
|
214
|
+
const fileRegex = /at\s+(?:Object\.<anonymous>|.*)\s+\((.+):(\d+):(\d+)\)/g;
|
|
215
|
+
|
|
216
|
+
let match;
|
|
217
|
+
while ((match = failedTestRegex.exec(output)) !== null) {
|
|
218
|
+
errors.push({
|
|
219
|
+
message: `Test failed: ${match[1]}`,
|
|
220
|
+
severity: 'error'
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
while ((match = fileRegex.exec(output)) !== null) {
|
|
225
|
+
const [, file, line, col] = match;
|
|
226
|
+
if (!file.includes('node_modules')) {
|
|
227
|
+
errors.push({
|
|
228
|
+
file: path.relative(PROJECT_ROOT, file),
|
|
229
|
+
line: parseInt(line),
|
|
230
|
+
column: parseInt(col),
|
|
231
|
+
message: 'Test assertion failed',
|
|
232
|
+
severity: 'error'
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return errors;
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
prettier: (output) => {
|
|
241
|
+
const errors = [];
|
|
242
|
+
// Prettier check output: Checking formatting...
|
|
243
|
+
// [warn] file.ts
|
|
244
|
+
const warnRegex = /\[warn\]\s+(.+)/g;
|
|
245
|
+
|
|
246
|
+
let match;
|
|
247
|
+
while ((match = warnRegex.exec(output)) !== null) {
|
|
248
|
+
errors.push({
|
|
249
|
+
file: match[1],
|
|
250
|
+
message: 'File needs formatting',
|
|
251
|
+
severity: 'warning'
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return errors;
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
generic: (output) => {
|
|
259
|
+
const errors = [];
|
|
260
|
+
// Generic error detection
|
|
261
|
+
const errorLines = output.split('\n').filter(line =>
|
|
262
|
+
/error|failed|fatal|exception/i.test(line) &&
|
|
263
|
+
!/warning/i.test(line)
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
for (const line of errorLines.slice(0, 10)) {
|
|
267
|
+
errors.push({
|
|
268
|
+
message: line.trim(),
|
|
269
|
+
severity: 'error'
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return errors;
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
security: (output) => {
|
|
277
|
+
const errors = [];
|
|
278
|
+
|
|
279
|
+
// Parse npm audit JSON output
|
|
280
|
+
try {
|
|
281
|
+
const audit = JSON.parse(output);
|
|
282
|
+
|
|
283
|
+
// Validate structure before accessing nested properties
|
|
284
|
+
if (!audit || typeof audit !== 'object') {
|
|
285
|
+
throw new Error('Invalid audit JSON structure');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const vulns = audit.metadata?.vulnerabilities || {};
|
|
289
|
+
|
|
290
|
+
if (vulns.critical > 0) {
|
|
291
|
+
errors.push({
|
|
292
|
+
message: `${vulns.critical} critical vulnerability(s) found`,
|
|
293
|
+
severity: 'error',
|
|
294
|
+
code: 'CRITICAL_VULN'
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (vulns.high > 0) {
|
|
298
|
+
errors.push({
|
|
299
|
+
message: `${vulns.high} high severity vulnerability(s) found`,
|
|
300
|
+
severity: 'error',
|
|
301
|
+
code: 'HIGH_VULN'
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (vulns.moderate > 0) {
|
|
305
|
+
errors.push({
|
|
306
|
+
message: `${vulns.moderate} moderate vulnerability(s) found`,
|
|
307
|
+
severity: 'warning',
|
|
308
|
+
code: 'MODERATE_VULN'
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (vulns.low > 0) {
|
|
312
|
+
errors.push({
|
|
313
|
+
message: `${vulns.low} low severity vulnerability(s) found`,
|
|
314
|
+
severity: 'warning',
|
|
315
|
+
code: 'LOW_VULN'
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
// Not JSON, try line-based parsing
|
|
320
|
+
if (output.includes('found 0 vulnerabilities')) {
|
|
321
|
+
// Clean
|
|
322
|
+
} else if (/found \d+ vulnerabilities/.test(output)) {
|
|
323
|
+
const match = output.match(/(\d+) (critical|high|moderate|low)/gi);
|
|
324
|
+
if (match) {
|
|
325
|
+
for (const m of match) {
|
|
326
|
+
const [count, severity] = m.split(' ');
|
|
327
|
+
errors.push({
|
|
328
|
+
message: `${count} ${severity} vulnerability(s)`,
|
|
329
|
+
severity: severity === 'critical' || severity === 'high' ? 'error' : 'warning',
|
|
330
|
+
code: `${severity.toUpperCase()}_VULN`
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return errors;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Detect which command to use based on installed packages
|
|
344
|
+
*/
|
|
345
|
+
function detectCommand(gate) {
|
|
346
|
+
const packageJsonPath = path.join(PROJECT_ROOT, 'package.json');
|
|
347
|
+
let deps = {};
|
|
348
|
+
|
|
349
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
350
|
+
try {
|
|
351
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
352
|
+
deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
353
|
+
} catch {
|
|
354
|
+
// Ignore parse errors
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const cmdConfig of gate.commands) {
|
|
359
|
+
if (cmdConfig.detect === null) {
|
|
360
|
+
// No detection needed, use this command
|
|
361
|
+
return cmdConfig;
|
|
362
|
+
}
|
|
363
|
+
if (deps[cmdConfig.detect]) {
|
|
364
|
+
return cmdConfig;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Run a command and capture output
|
|
373
|
+
*/
|
|
374
|
+
const MAX_OUTPUT_SIZE = 1024 * 1024; // 1MB limit per stream
|
|
375
|
+
|
|
376
|
+
function runCommand(cmd, args, timeout = 120000) {
|
|
377
|
+
return new Promise((resolve) => {
|
|
378
|
+
const startTime = Date.now();
|
|
379
|
+
let stdout = '';
|
|
380
|
+
let stderr = '';
|
|
381
|
+
let stdoutTruncated = false;
|
|
382
|
+
let stderrTruncated = false;
|
|
383
|
+
|
|
384
|
+
const proc = spawn(cmd, args, {
|
|
385
|
+
cwd: PROJECT_ROOT,
|
|
386
|
+
timeout,
|
|
387
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
proc.stdout.on('data', (data) => {
|
|
391
|
+
if (stdout.length < MAX_OUTPUT_SIZE) {
|
|
392
|
+
stdout += data.toString();
|
|
393
|
+
} else if (!stdoutTruncated) {
|
|
394
|
+
stdout += '\n[OUTPUT TRUNCATED - exceeded 1MB]';
|
|
395
|
+
stdoutTruncated = true;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
proc.stderr.on('data', (data) => {
|
|
400
|
+
if (stderr.length < MAX_OUTPUT_SIZE) {
|
|
401
|
+
stderr += data.toString();
|
|
402
|
+
} else if (!stderrTruncated) {
|
|
403
|
+
stderr += '\n[OUTPUT TRUNCATED - exceeded 1MB]';
|
|
404
|
+
stderrTruncated = true;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
proc.on('close', (code) => {
|
|
409
|
+
resolve({
|
|
410
|
+
exitCode: code,
|
|
411
|
+
stdout,
|
|
412
|
+
stderr,
|
|
413
|
+
duration: Date.now() - startTime
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
proc.on('error', (err) => {
|
|
418
|
+
resolve({
|
|
419
|
+
exitCode: 1,
|
|
420
|
+
stdout,
|
|
421
|
+
stderr: stderr + '\n' + err.message,
|
|
422
|
+
duration: Date.now() - startTime
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* TypeScript error code suggestions with self-correction guidance
|
|
430
|
+
*/
|
|
431
|
+
const TS_ERROR_SUGGESTIONS = {
|
|
432
|
+
// Module/Import errors
|
|
433
|
+
TS2307: {
|
|
434
|
+
pattern: /Cannot find module '(.+)'/,
|
|
435
|
+
suggest: (match, file) => {
|
|
436
|
+
const moduleName = match[1];
|
|
437
|
+
if (moduleName.startsWith('.')) {
|
|
438
|
+
return `Check file path: Does "${moduleName}" exist relative to ${file}? Common issues: wrong extension (.js vs .ts), missing index file, case sensitivity`;
|
|
439
|
+
}
|
|
440
|
+
if (moduleName.startsWith('@')) {
|
|
441
|
+
return `Install missing package: \`npm install ${moduleName}\` or \`npm install -D @types/${moduleName.replace('@', '').split('/')[0]}\``;
|
|
442
|
+
}
|
|
443
|
+
return `Install missing package: \`npm install ${moduleName}\` or for types: \`npm install -D @types/${moduleName}\``;
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
TS2305: {
|
|
447
|
+
pattern: /Module '"(.+)"' has no exported member '(.+)'/,
|
|
448
|
+
suggest: (match) => {
|
|
449
|
+
const [, modulePath, memberName] = match;
|
|
450
|
+
return `"${memberName}" is not exported from "${modulePath}". Check: 1) Correct export name (case-sensitive), 2) Named vs default export, 3) Re-export in index.ts`;
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
TS2614: {
|
|
454
|
+
pattern: /Module '"(.+)"' has no default export/,
|
|
455
|
+
suggest: (match) => {
|
|
456
|
+
const modulePath = match[1];
|
|
457
|
+
return `Use named import: \`import { something } from "${modulePath}"\` instead of default import. Or add \`export default\` to the source file.`;
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
// Type mismatch errors
|
|
462
|
+
TS2322: {
|
|
463
|
+
pattern: /Type '(.+)' is not assignable to type '(.+)'/,
|
|
464
|
+
suggest: (match) => {
|
|
465
|
+
const [, sourceType, targetType] = match;
|
|
466
|
+
if (sourceType === 'undefined' || sourceType.includes('undefined')) {
|
|
467
|
+
return `Handle undefined case: Use optional chaining (?.), nullish coalescing (??), or add a type guard. The value might be undefined but "${targetType}" doesn't allow it.`;
|
|
468
|
+
}
|
|
469
|
+
if (targetType === 'never') {
|
|
470
|
+
return 'Check your type narrowing logic - TypeScript determined this code path is impossible. Verify your conditional checks.';
|
|
471
|
+
}
|
|
472
|
+
return `Type mismatch: "${sourceType}" → "${targetType}". Options: 1) Cast with \`as ${targetType}\` if safe, 2) Add type guard, 3) Update the expected type, 4) Transform the value`;
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
TS2345: {
|
|
476
|
+
pattern: /Argument of type '(.+)' is not assignable to parameter of type '(.+)'/,
|
|
477
|
+
suggest: (match) => {
|
|
478
|
+
const [, argType, paramType] = match;
|
|
479
|
+
return `Wrong argument type. Expected "${paramType}" but got "${argType}". Check: 1) Correct function signature, 2) Transform argument before passing, 3) Update function to accept both types`;
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
TS2339: {
|
|
483
|
+
pattern: /Property '(.+)' does not exist on type '(.+)'/,
|
|
484
|
+
suggest: (match) => {
|
|
485
|
+
const [, propName, typeName] = match;
|
|
486
|
+
if (typeName.includes('|')) {
|
|
487
|
+
return `"${propName}" doesn't exist on all union members. Use type narrowing: \`if ('${propName}' in obj)\` or discriminated unions.`;
|
|
488
|
+
}
|
|
489
|
+
return `Property "${propName}" not in type "${typeName}". Options: 1) Add property to interface, 2) Use \`(obj as any).${propName}\` (not recommended), 3) Check for typo in property name`;
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
// Declaration errors
|
|
494
|
+
TS2304: {
|
|
495
|
+
pattern: /Cannot find name '(.+)'/,
|
|
496
|
+
suggest: (match) => {
|
|
497
|
+
const name = match[1];
|
|
498
|
+
const builtins = ['console', 'setTimeout', 'Promise', 'Array', 'Object', 'JSON'];
|
|
499
|
+
if (builtins.includes(name)) {
|
|
500
|
+
return `Add "dom" and/or "es2020" to compilerOptions.lib in tsconfig.json, or check that @types/node is installed.`;
|
|
501
|
+
}
|
|
502
|
+
return `"${name}" is not defined. Check: 1) Import the symbol, 2) Typo in name, 3) Declaration in scope, 4) Missing type definition`;
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
TS2451: {
|
|
506
|
+
pattern: /Cannot redeclare block-scoped variable '(.+)'/,
|
|
507
|
+
suggest: (match) => {
|
|
508
|
+
const varName = match[1];
|
|
509
|
+
return `"${varName}" is declared multiple times. This often happens with global declarations or missing export statements. Wrap in a module: add \`export {}\` to make file a module.`;
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
// Async/Promise errors
|
|
514
|
+
TS2705: {
|
|
515
|
+
pattern: /An async function/,
|
|
516
|
+
suggest: () => 'Async function needs Promise. Add "es2017" or higher to compilerOptions.lib in tsconfig.json.'
|
|
517
|
+
},
|
|
518
|
+
TS1064: {
|
|
519
|
+
pattern: /The return type of an async function/,
|
|
520
|
+
suggest: () => 'Async functions must return a Promise. Use `Promise<YourType>` as return type or let TypeScript infer it.'
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
// Generic/inference errors
|
|
524
|
+
TS2558: {
|
|
525
|
+
pattern: /Expected (\d+) type arguments?, but got (\d+)/,
|
|
526
|
+
suggest: (match) => {
|
|
527
|
+
const [, expected, got] = match;
|
|
528
|
+
return `Generic type expects ${expected} type argument(s), got ${got}. Check the generic definition and provide correct number of types.`;
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
TS7006: {
|
|
532
|
+
pattern: /Parameter '(.+)' implicitly has an 'any' type/,
|
|
533
|
+
suggest: (match) => {
|
|
534
|
+
const paramName = match[1];
|
|
535
|
+
return `Add type annotation to "${paramName}". Example: \`${paramName}: string\` or \`${paramName}: SomeType\`. If truly unknown, use \`${paramName}: unknown\` (safer than any).`;
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
// Object literal errors
|
|
540
|
+
TS2353: {
|
|
541
|
+
pattern: /Object literal may only specify known properties/,
|
|
542
|
+
suggest: () => 'Extra property in object literal. Either: 1) Remove the property, 2) Add it to the type definition, 3) Use type assertion to bypass (not recommended)'
|
|
543
|
+
},
|
|
544
|
+
TS2741: {
|
|
545
|
+
pattern: /Property '(.+)' is missing in type/,
|
|
546
|
+
suggest: (match) => {
|
|
547
|
+
const propName = match[1];
|
|
548
|
+
return `Required property "${propName}" is missing. Add it to the object, or make it optional in the type with \`${propName}?: Type\``;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Generate fix suggestions based on errors
|
|
555
|
+
*/
|
|
556
|
+
function generateFixSuggestions(gateName, errors) {
|
|
557
|
+
const suggestions = [];
|
|
558
|
+
|
|
559
|
+
if (gateName === 'lint') {
|
|
560
|
+
const hasAutoFix = errors.some(e => e.rule);
|
|
561
|
+
if (hasAutoFix) {
|
|
562
|
+
suggestions.push('Run `npx eslint . --fix` to auto-fix some issues');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Group by rule for specific suggestions
|
|
566
|
+
const ruleCount = {};
|
|
567
|
+
for (const err of errors) {
|
|
568
|
+
if (err.rule) {
|
|
569
|
+
ruleCount[err.rule] = (ruleCount[err.rule] || 0) + 1;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
for (const [rule, count] of Object.entries(ruleCount)) {
|
|
574
|
+
if (count > 3) {
|
|
575
|
+
suggestions.push(`Rule "${rule}" has ${count} violations - consider reviewing this pattern`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (gateName === 'typecheck') {
|
|
581
|
+
// Enhanced TypeScript suggestions with self-correction guidance
|
|
582
|
+
const seenSuggestions = new Set();
|
|
583
|
+
|
|
584
|
+
for (const err of errors) {
|
|
585
|
+
if (err.code && TS_ERROR_SUGGESTIONS[err.code]) {
|
|
586
|
+
const suggestionDef = TS_ERROR_SUGGESTIONS[err.code];
|
|
587
|
+
const match = err.message.match(suggestionDef.pattern);
|
|
588
|
+
|
|
589
|
+
if (match) {
|
|
590
|
+
const suggestion = suggestionDef.suggest(match, err.file);
|
|
591
|
+
// Dedupe similar suggestions
|
|
592
|
+
const key = `${err.code}:${suggestion.slice(0, 50)}`;
|
|
593
|
+
if (!seenSuggestions.has(key)) {
|
|
594
|
+
seenSuggestions.add(key);
|
|
595
|
+
suggestions.push(`[${err.code}] ${suggestion}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Fallback generic suggestions if no specific ones matched
|
|
602
|
+
if (suggestions.length === 0) {
|
|
603
|
+
const missingTypes = errors.filter(e => e.code === 'TS2307');
|
|
604
|
+
if (missingTypes.length > 0) {
|
|
605
|
+
suggestions.push('Some type definitions may be missing - check @types packages');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const anyErrors = errors.filter(e => e.message.includes('any'));
|
|
609
|
+
if (anyErrors.length > 0) {
|
|
610
|
+
suggestions.push('Consider adding proper type annotations instead of `any`');
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Limit suggestions to most relevant
|
|
615
|
+
if (suggestions.length > 5) {
|
|
616
|
+
const limited = suggestions.slice(0, 5);
|
|
617
|
+
limited.push(`... and ${suggestions.length - 5} more suggestions`);
|
|
618
|
+
return limited;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (gateName === 'test') {
|
|
623
|
+
suggestions.push('Review failing tests and update assertions or implementation');
|
|
624
|
+
if (errors.some(e => e.message.includes('timeout'))) {
|
|
625
|
+
suggestions.push('Some tests timed out - check for async issues or increase timeout');
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (gateName === 'format') {
|
|
630
|
+
suggestions.push('Run `npx prettier --write .` to fix formatting');
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (gateName === 'securityScan') {
|
|
634
|
+
const hasCritical = errors.some(e => e.code === 'CRITICAL_VULN');
|
|
635
|
+
const hasHigh = errors.some(e => e.code === 'HIGH_VULN');
|
|
636
|
+
const hasSecrets = errors.some(e => e.code === 'SECRET_DETECTED');
|
|
637
|
+
const hasInjection = errors.some(e => e.code === 'INJECTION_RISK');
|
|
638
|
+
|
|
639
|
+
if (hasCritical || hasHigh) {
|
|
640
|
+
suggestions.push('Run `npm audit fix` to auto-fix vulnerabilities');
|
|
641
|
+
suggestions.push('Run `npm audit fix --force` for breaking changes (review carefully)');
|
|
642
|
+
}
|
|
643
|
+
if (hasSecrets) {
|
|
644
|
+
suggestions.push('Remove hardcoded secrets and use environment variables');
|
|
645
|
+
suggestions.push('Add secrets to .gitignore and rotate any exposed credentials');
|
|
646
|
+
}
|
|
647
|
+
if (hasInjection) {
|
|
648
|
+
suggestions.push('Use parameterized queries for database operations');
|
|
649
|
+
suggestions.push('Avoid eval(), new Function(), and exec() with user input');
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return suggestions;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ============================================================
|
|
657
|
+
// Security Check Functions
|
|
658
|
+
// ============================================================
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Get staged files for security scanning
|
|
662
|
+
*/
|
|
663
|
+
function getStagedFiles() {
|
|
664
|
+
try {
|
|
665
|
+
const output = execSync('git diff --cached --name-only', {
|
|
666
|
+
cwd: PROJECT_ROOT,
|
|
667
|
+
encoding: 'utf-8'
|
|
668
|
+
});
|
|
669
|
+
return output.split('\n').filter(f => f.trim() && /\.(ts|tsx|js|jsx|json|env)$/i.test(f));
|
|
670
|
+
} catch {
|
|
671
|
+
return [];
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Find source files recursively (safe alternative to shell find)
|
|
677
|
+
* @param {string} dir - Directory to search
|
|
678
|
+
* @param {string[]} extensions - File extensions to match
|
|
679
|
+
* @param {number} limit - Max files to return
|
|
680
|
+
* @returns {string[]} Array of relative file paths
|
|
681
|
+
*/
|
|
682
|
+
function findSourceFiles(dir, extensions, limit) {
|
|
683
|
+
const results = [];
|
|
684
|
+
|
|
685
|
+
function walk(currentDir, depth = 0) {
|
|
686
|
+
// Prevent infinite recursion
|
|
687
|
+
if (depth > 10 || results.length >= limit) return;
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
691
|
+
|
|
692
|
+
for (const entry of entries) {
|
|
693
|
+
if (results.length >= limit) break;
|
|
694
|
+
|
|
695
|
+
// Skip hidden files/dirs and node_modules
|
|
696
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
697
|
+
|
|
698
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
699
|
+
const relativePath = path.relative(PROJECT_ROOT, fullPath);
|
|
700
|
+
|
|
701
|
+
if (entry.isDirectory()) {
|
|
702
|
+
walk(fullPath, depth + 1);
|
|
703
|
+
} else if (entry.isFile()) {
|
|
704
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
705
|
+
if (extensions.includes(ext)) {
|
|
706
|
+
results.push(relativePath);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
// Skip unreadable directories
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
walk(dir);
|
|
716
|
+
return results;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Check for hardcoded secrets in files
|
|
721
|
+
*/
|
|
722
|
+
function checkForSecrets(files) {
|
|
723
|
+
const errors = [];
|
|
724
|
+
const config = getConfig();
|
|
725
|
+
const ignorePatterns = config.security?.ignoreFiles || ['*.test.ts', '*.spec.ts'];
|
|
726
|
+
|
|
727
|
+
const secretPatterns = [
|
|
728
|
+
{ pattern: /password\s*[:=]\s*['"][^'"]{8,}['"]/gi, name: 'Hardcoded password' },
|
|
729
|
+
{ pattern: /api[_-]?key\s*[:=]\s*['"][^'"]{10,}['"]/gi, name: 'Hardcoded API key' },
|
|
730
|
+
{ pattern: /secret\s*[:=]\s*['"][^'"]{8,}['"]/gi, name: 'Hardcoded secret' },
|
|
731
|
+
{ pattern: /-----BEGIN (RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----/g, name: 'Private key' },
|
|
732
|
+
{ pattern: /sk_live_[a-zA-Z0-9]{24,}/g, name: 'Stripe live key' },
|
|
733
|
+
{ pattern: /sk_test_[a-zA-Z0-9]{24,}/g, name: 'Stripe test key' },
|
|
734
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/g, name: 'GitHub personal token' },
|
|
735
|
+
{ pattern: /gho_[a-zA-Z0-9]{36}/g, name: 'GitHub OAuth token' },
|
|
736
|
+
{ pattern: /xox[baprs]-[a-zA-Z0-9-]{10,}/g, name: 'Slack token' },
|
|
737
|
+
{ pattern: /AKIA[0-9A-Z]{16}/g, name: 'AWS access key' },
|
|
738
|
+
{ pattern: /mongodb(\+srv)?:\/\/[^:]+:[^@]+@/g, name: 'MongoDB connection string with password' }
|
|
739
|
+
];
|
|
740
|
+
|
|
741
|
+
for (const file of files) {
|
|
742
|
+
// Skip ignored patterns
|
|
743
|
+
const shouldIgnore = ignorePatterns.some(pattern => {
|
|
744
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
745
|
+
return regex.test(file);
|
|
746
|
+
});
|
|
747
|
+
if (shouldIgnore) continue;
|
|
748
|
+
|
|
749
|
+
const filePath = path.join(PROJECT_ROOT, file);
|
|
750
|
+
if (!fs.existsSync(filePath)) continue;
|
|
751
|
+
|
|
752
|
+
try {
|
|
753
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
754
|
+
|
|
755
|
+
for (const { pattern, name } of secretPatterns) {
|
|
756
|
+
const matches = content.match(pattern);
|
|
757
|
+
if (matches) {
|
|
758
|
+
errors.push({
|
|
759
|
+
file,
|
|
760
|
+
message: `${name} detected`,
|
|
761
|
+
severity: 'error',
|
|
762
|
+
code: 'SECRET_DETECTED',
|
|
763
|
+
count: matches.length
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
} catch {
|
|
768
|
+
// Skip unreadable files
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return errors;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Check for injection vulnerabilities
|
|
777
|
+
*/
|
|
778
|
+
function checkForInjection(files) {
|
|
779
|
+
const errors = [];
|
|
780
|
+
const config = getConfig();
|
|
781
|
+
const ignorePatterns = config.security?.ignoreFiles || ['*.test.ts', '*.spec.ts'];
|
|
782
|
+
|
|
783
|
+
const injectionPatterns = [
|
|
784
|
+
{ pattern: /eval\s*\([^)]*\$\{/g, name: 'eval() with template literal' },
|
|
785
|
+
{ pattern: /new\s+Function\s*\([^)]*\$\{/g, name: 'new Function() with template literal' },
|
|
786
|
+
{ pattern: /exec\s*\([^)]*\$\{/g, name: 'exec() with template literal' },
|
|
787
|
+
{ pattern: /innerHTML\s*=\s*[^;]*\$\{/g, name: 'innerHTML with template literal (XSS risk)' },
|
|
788
|
+
{ pattern: /dangerouslySetInnerHTML/g, name: 'dangerouslySetInnerHTML usage' },
|
|
789
|
+
{ pattern: /\$\{[^}]+\}.*(?:SELECT|INSERT|UPDATE|DELETE|DROP)\s/gi, name: 'Potential SQL injection' }
|
|
790
|
+
];
|
|
791
|
+
|
|
792
|
+
for (const file of files) {
|
|
793
|
+
// Skip ignored patterns
|
|
794
|
+
const shouldIgnore = ignorePatterns.some(pattern => {
|
|
795
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
796
|
+
return regex.test(file);
|
|
797
|
+
});
|
|
798
|
+
if (shouldIgnore) continue;
|
|
799
|
+
|
|
800
|
+
const filePath = path.join(PROJECT_ROOT, file);
|
|
801
|
+
if (!fs.existsSync(filePath)) continue;
|
|
802
|
+
|
|
803
|
+
try {
|
|
804
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
805
|
+
|
|
806
|
+
const lines = content.split('\n');
|
|
807
|
+
for (const { pattern, name } of injectionPatterns) {
|
|
808
|
+
// Create fresh regex for each pattern to avoid stateful lastIndex issues
|
|
809
|
+
for (let i = 0; i < lines.length; i++) {
|
|
810
|
+
const freshPattern = new RegExp(pattern.source, pattern.flags);
|
|
811
|
+
if (freshPattern.test(lines[i])) {
|
|
812
|
+
errors.push({
|
|
813
|
+
file,
|
|
814
|
+
line: i + 1,
|
|
815
|
+
message: name,
|
|
816
|
+
severity: 'warning',
|
|
817
|
+
code: 'INJECTION_RISK'
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
} catch {
|
|
823
|
+
// Skip unreadable files
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return errors;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Run all security checks
|
|
832
|
+
*/
|
|
833
|
+
async function runSecurityChecks(gateResult) {
|
|
834
|
+
const config = getConfig();
|
|
835
|
+
const securityConfig = config.security || {};
|
|
836
|
+
const checkPatterns = securityConfig.checkPatterns || {};
|
|
837
|
+
|
|
838
|
+
// Get files to scan
|
|
839
|
+
let files = getStagedFiles();
|
|
840
|
+
if (files.length === 0) {
|
|
841
|
+
// Fall back to src directory if no staged files
|
|
842
|
+
// Use fs.readdirSync instead of shell find to prevent command injection
|
|
843
|
+
try {
|
|
844
|
+
const srcDir = path.join(PROJECT_ROOT, 'src');
|
|
845
|
+
if (fs.existsSync(srcDir)) {
|
|
846
|
+
files = findSourceFiles(srcDir, ['.ts', '.tsx', '.js', '.jsx'], 100);
|
|
847
|
+
}
|
|
848
|
+
} catch {
|
|
849
|
+
files = [];
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Run secret detection
|
|
854
|
+
if (checkPatterns.secrets !== false) {
|
|
855
|
+
const secretErrors = checkForSecrets(files);
|
|
856
|
+
gateResult.errors.push(...secretErrors);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Run injection detection
|
|
860
|
+
if (checkPatterns.injection !== false) {
|
|
861
|
+
const injectionErrors = checkForInjection(files);
|
|
862
|
+
gateResult.errors.push(...injectionErrors.filter(e => e.severity === 'error'));
|
|
863
|
+
gateResult.warnings.push(...injectionErrors.filter(e => e.severity === 'warning'));
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Run a single verification gate
|
|
869
|
+
*/
|
|
870
|
+
async function runGate(gateName, options = {}) {
|
|
871
|
+
const config = getConfig();
|
|
872
|
+
const gateConfig = config.verifyGates?.[gateName] || DEFAULT_GATES[gateName];
|
|
873
|
+
|
|
874
|
+
if (!gateConfig) {
|
|
875
|
+
const result = new GateResult(gateName);
|
|
876
|
+
result.errors = [{ message: `Unknown gate: ${gateName}` }];
|
|
877
|
+
return result;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const result = new GateResult(gateName);
|
|
881
|
+
const cmdConfig = detectCommand(gateConfig);
|
|
882
|
+
|
|
883
|
+
if (!cmdConfig) {
|
|
884
|
+
result.passed = true;
|
|
885
|
+
result.warnings = [{ message: `No tool detected for ${gateName}, skipping` }];
|
|
886
|
+
return result;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
result.command = `${cmdConfig.cmd} ${cmdConfig.args.join(' ')}`;
|
|
890
|
+
|
|
891
|
+
if (!options.quiet) {
|
|
892
|
+
console.log(`${c.cyan}▶${c.reset} Running ${gateConfig.name}...`);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const output = await runCommand(cmdConfig.cmd, cmdConfig.args, options.timeout || 120000);
|
|
896
|
+
|
|
897
|
+
result.exitCode = output.exitCode;
|
|
898
|
+
result.duration = output.duration;
|
|
899
|
+
result.stdout = output.stdout;
|
|
900
|
+
result.stderr = output.stderr;
|
|
901
|
+
result.passed = output.exitCode === 0;
|
|
902
|
+
|
|
903
|
+
// Parse errors
|
|
904
|
+
const parser = ERROR_PARSERS[gateConfig.parser] || ERROR_PARSERS.generic;
|
|
905
|
+
const combinedOutput = output.stdout + '\n' + output.stderr;
|
|
906
|
+
const parsedErrors = parser(combinedOutput);
|
|
907
|
+
|
|
908
|
+
result.errors = parsedErrors.filter(e => e.severity === 'error');
|
|
909
|
+
result.warnings = parsedErrors.filter(e => e.severity === 'warning');
|
|
910
|
+
|
|
911
|
+
// Run custom security checks for securityScan gate
|
|
912
|
+
if (gateName === 'securityScan' && gateConfig.customChecks) {
|
|
913
|
+
await runSecurityChecks(result);
|
|
914
|
+
// Update passed status based on errors
|
|
915
|
+
const config = getConfig();
|
|
916
|
+
const blockOnHigh = config.security?.blockOnHigh !== false;
|
|
917
|
+
const hasBlockingErrors = result.errors.some(e =>
|
|
918
|
+
e.code === 'SECRET_DETECTED' ||
|
|
919
|
+
e.code === 'CRITICAL_VULN' ||
|
|
920
|
+
(blockOnHigh && e.code === 'HIGH_VULN')
|
|
921
|
+
);
|
|
922
|
+
if (hasBlockingErrors) {
|
|
923
|
+
result.passed = false;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Generate fix suggestions
|
|
928
|
+
if (!result.passed) {
|
|
929
|
+
result.fixSuggestions = generateFixSuggestions(gateName, result.errors);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Record metrics
|
|
933
|
+
recordCommandResult(result.command, {
|
|
934
|
+
success: result.passed,
|
|
935
|
+
duration: result.duration,
|
|
936
|
+
exitCode: result.exitCode,
|
|
937
|
+
errorType: result.errors[0]?.code || (result.passed ? null : 'UNKNOWN')
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
if (!options.quiet) {
|
|
941
|
+
if (result.passed) {
|
|
942
|
+
console.log(`${c.green}✅ ${gateConfig.name} passed${c.reset} (${result.duration}ms)`);
|
|
943
|
+
} else {
|
|
944
|
+
console.log(`${c.red}❌ ${gateConfig.name} failed${c.reset} (${result.duration}ms)`);
|
|
945
|
+
console.log(` ${result.errors.length} error(s), ${result.warnings.length} warning(s)`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return result;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Run multiple gates
|
|
954
|
+
*/
|
|
955
|
+
async function runGates(gateNames, options = {}) {
|
|
956
|
+
const config = getConfig();
|
|
957
|
+
|
|
958
|
+
// Use configured gates if 'all' specified
|
|
959
|
+
if (gateNames.includes('all')) {
|
|
960
|
+
gateNames = Object.keys(config.verifyGates || DEFAULT_GATES);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// If stopOnFailure is set or only one gate, run sequentially
|
|
964
|
+
if (options.stopOnFailure || gateNames.length <= 1) {
|
|
965
|
+
const results = [];
|
|
966
|
+
for (const gateName of gateNames) {
|
|
967
|
+
const result = await runGate(gateName, options);
|
|
968
|
+
results.push(result);
|
|
969
|
+
if (!result.passed && options.stopOnFailure) {
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return results;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Run gates with limited concurrency (max 4 parallel) to avoid resource exhaustion
|
|
977
|
+
const MAX_CONCURRENT = 4;
|
|
978
|
+
const results = [];
|
|
979
|
+
|
|
980
|
+
for (let i = 0; i < gateNames.length; i += MAX_CONCURRENT) {
|
|
981
|
+
const batch = gateNames.slice(i, i + MAX_CONCURRENT);
|
|
982
|
+
const batchResults = await Promise.all(
|
|
983
|
+
batch.map(gateName => runGate(gateName, options))
|
|
984
|
+
);
|
|
985
|
+
results.push(...batchResults);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return results;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Get summary of gate results
|
|
993
|
+
*/
|
|
994
|
+
function getSummary(results) {
|
|
995
|
+
const passed = results.filter(r => r.passed).length;
|
|
996
|
+
const failed = results.filter(r => !r.passed).length;
|
|
997
|
+
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
998
|
+
|
|
999
|
+
return {
|
|
1000
|
+
passed,
|
|
1001
|
+
failed,
|
|
1002
|
+
total: results.length,
|
|
1003
|
+
allPassed: failed === 0,
|
|
1004
|
+
duration: totalDuration,
|
|
1005
|
+
results: results.map(r => r.toJSON())
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Format results for terminal display
|
|
1011
|
+
*/
|
|
1012
|
+
function formatResults(results, options = {}) {
|
|
1013
|
+
let output = '';
|
|
1014
|
+
|
|
1015
|
+
output += `\n${c.cyan}${'═'.repeat(60)}${c.reset}\n`;
|
|
1016
|
+
output += `${c.bold}Verification Gate Results${c.reset}\n`;
|
|
1017
|
+
output += `${c.cyan}${'═'.repeat(60)}${c.reset}\n\n`;
|
|
1018
|
+
|
|
1019
|
+
for (const result of results) {
|
|
1020
|
+
const icon = result.passed ? `${c.green}✅${c.reset}` : `${c.red}❌${c.reset}`;
|
|
1021
|
+
output += `${icon} ${c.bold}${result.name}${c.reset}`;
|
|
1022
|
+
output += ` ${c.dim}(${result.duration}ms)${c.reset}\n`;
|
|
1023
|
+
|
|
1024
|
+
if (!result.passed && result.errors.length > 0) {
|
|
1025
|
+
const showCount = options.verbose ? result.errors.length : Math.min(5, result.errors.length);
|
|
1026
|
+
for (let i = 0; i < showCount; i++) {
|
|
1027
|
+
const err = result.errors[i];
|
|
1028
|
+
output += ` ${c.red}•${c.reset} ${err.file || ''}`;
|
|
1029
|
+
if (err.line) output += `:${err.line}`;
|
|
1030
|
+
output += ` ${err.message}\n`;
|
|
1031
|
+
}
|
|
1032
|
+
if (result.errors.length > showCount) {
|
|
1033
|
+
output += ` ${c.dim}... and ${result.errors.length - showCount} more${c.reset}\n`;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (!result.passed && result.fixSuggestions.length > 0) {
|
|
1038
|
+
output += ` ${c.yellow}💡 Suggestions:${c.reset}\n`;
|
|
1039
|
+
for (const fix of result.fixSuggestions) {
|
|
1040
|
+
output += ` ${fix}\n`;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
output += '\n';
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Summary
|
|
1048
|
+
const summary = getSummary(results);
|
|
1049
|
+
output += `${c.cyan}${'─'.repeat(60)}${c.reset}\n`;
|
|
1050
|
+
|
|
1051
|
+
if (summary.allPassed) {
|
|
1052
|
+
output += `${c.green}✅ All ${summary.total} gates passed${c.reset}`;
|
|
1053
|
+
} else {
|
|
1054
|
+
output += `${c.red}❌ ${summary.failed}/${summary.total} gates failed${c.reset}`;
|
|
1055
|
+
}
|
|
1056
|
+
output += ` ${c.dim}(${summary.duration}ms total)${c.reset}\n`;
|
|
1057
|
+
|
|
1058
|
+
return output;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Save results to run artifacts
|
|
1063
|
+
*/
|
|
1064
|
+
function saveResults(runId, results) {
|
|
1065
|
+
const runDir = path.join(WORKFLOW_DIR, 'runs', runId);
|
|
1066
|
+
if (!fs.existsSync(runDir)) {
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const artifactsDir = path.join(runDir, 'artifacts');
|
|
1071
|
+
if (!fs.existsSync(artifactsDir)) {
|
|
1072
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
fs.writeFileSync(
|
|
1076
|
+
path.join(artifactsDir, 'verify-results.json'),
|
|
1077
|
+
JSON.stringify(getSummary(results), null, 2)
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
// Generate LLM context for failures
|
|
1081
|
+
const failures = results.filter(r => !r.passed);
|
|
1082
|
+
if (failures.length > 0) {
|
|
1083
|
+
let llmContext = '# Verification Failures\n\n';
|
|
1084
|
+
llmContext += 'The following verification gates failed. Please analyze and fix:\n\n';
|
|
1085
|
+
for (const failure of failures) {
|
|
1086
|
+
llmContext += failure.toLLMContext() + '\n\n---\n\n';
|
|
1087
|
+
}
|
|
1088
|
+
fs.writeFileSync(
|
|
1089
|
+
path.join(artifactsDir, 'verify-failures-context.md'),
|
|
1090
|
+
llmContext
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Module exports
|
|
1098
|
+
module.exports = {
|
|
1099
|
+
GateResult,
|
|
1100
|
+
runGate,
|
|
1101
|
+
runGates,
|
|
1102
|
+
getSummary,
|
|
1103
|
+
formatResults,
|
|
1104
|
+
saveResults,
|
|
1105
|
+
DEFAULT_GATES,
|
|
1106
|
+
ERROR_PARSERS
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
// CLI Handler
|
|
1110
|
+
if (require.main === module) {
|
|
1111
|
+
const args = process.argv.slice(2);
|
|
1112
|
+
|
|
1113
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
1114
|
+
console.log(`
|
|
1115
|
+
${c.cyan}Wogi Flow - Verification Gates${c.reset}
|
|
1116
|
+
|
|
1117
|
+
${c.bold}Usage:${c.reset}
|
|
1118
|
+
flow verify <gate> Run a single gate (lint, typecheck, test, build, format)
|
|
1119
|
+
flow verify all Run all configured gates
|
|
1120
|
+
flow verify lint typecheck Run multiple gates
|
|
1121
|
+
|
|
1122
|
+
${c.bold}Options:${c.reset}
|
|
1123
|
+
--json Output results as JSON
|
|
1124
|
+
--verbose Show all errors (not just first 5)
|
|
1125
|
+
--stop-on-failure Stop at first failing gate
|
|
1126
|
+
--quiet Suppress progress output
|
|
1127
|
+
--llm-context Output LLM-friendly error context
|
|
1128
|
+
|
|
1129
|
+
${c.bold}Available Gates:${c.reset}
|
|
1130
|
+
lint Run ESLint/Biome
|
|
1131
|
+
typecheck Run TypeScript compiler
|
|
1132
|
+
test Run test suite (Jest/Vitest)
|
|
1133
|
+
build Run build script
|
|
1134
|
+
format Check code formatting
|
|
1135
|
+
|
|
1136
|
+
${c.bold}Exit Codes:${c.reset}
|
|
1137
|
+
0 All gates passed
|
|
1138
|
+
1 One or more gates failed
|
|
1139
|
+
2 Configuration error
|
|
1140
|
+
`);
|
|
1141
|
+
process.exit(0);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const jsonOutput = args.includes('--json');
|
|
1145
|
+
const verbose = args.includes('--verbose');
|
|
1146
|
+
const stopOnFailure = args.includes('--stop-on-failure');
|
|
1147
|
+
const quiet = args.includes('--quiet') || jsonOutput;
|
|
1148
|
+
const llmContext = args.includes('--llm-context');
|
|
1149
|
+
|
|
1150
|
+
const gateNames = args.filter(a => !a.startsWith('--'));
|
|
1151
|
+
|
|
1152
|
+
if (gateNames.length === 0) {
|
|
1153
|
+
gateNames.push('all');
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
runGates(gateNames, { verbose, stopOnFailure, quiet })
|
|
1157
|
+
.then(results => {
|
|
1158
|
+
if (jsonOutput) {
|
|
1159
|
+
console.log(JSON.stringify(getSummary(results), null, 2));
|
|
1160
|
+
} else if (llmContext) {
|
|
1161
|
+
const failures = results.filter(r => !r.passed);
|
|
1162
|
+
for (const failure of failures) {
|
|
1163
|
+
console.log(failure.toLLMContext());
|
|
1164
|
+
console.log('\n---\n');
|
|
1165
|
+
}
|
|
1166
|
+
} else {
|
|
1167
|
+
console.log(formatResults(results, { verbose }));
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const summary = getSummary(results);
|
|
1171
|
+
process.exit(summary.allPassed ? 0 : 1);
|
|
1172
|
+
})
|
|
1173
|
+
.catch(err => {
|
|
1174
|
+
console.error(`${c.red}Error: ${err.message}${c.reset}`);
|
|
1175
|
+
process.exit(2);
|
|
1176
|
+
});
|
|
1177
|
+
}
|