agileflow 2.90.6 → 2.91.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/CHANGELOG.md +10 -0
- package/README.md +6 -6
- package/lib/codebase-indexer.js +810 -0
- package/lib/validate-names.js +3 -3
- package/package.json +4 -1
- package/scripts/obtain-context.js +238 -0
- package/scripts/precompact-context.sh +13 -1
- package/scripts/query-codebase.js +430 -0
- package/scripts/tui/blessed/data/watcher.js +175 -0
- package/scripts/tui/blessed/index.js +244 -0
- package/scripts/tui/blessed/panels/output.js +95 -0
- package/scripts/tui/blessed/panels/sessions.js +143 -0
- package/scripts/tui/blessed/panels/trace.js +91 -0
- package/scripts/tui/blessed/ui/help.js +77 -0
- package/scripts/tui/blessed/ui/screen.js +52 -0
- package/scripts/tui/blessed/ui/statusbar.js +51 -0
- package/scripts/tui/blessed/ui/tabbar.js +99 -0
- package/scripts/tui/index.js +38 -32
- package/scripts/tui/simple-tui.js +8 -5
- package/scripts/validators/README.md +143 -0
- package/scripts/validators/component-validator.js +212 -0
- package/scripts/validators/json-schema-validator.js +179 -0
- package/scripts/validators/markdown-validator.js +153 -0
- package/scripts/validators/migration-validator.js +117 -0
- package/scripts/validators/security-validator.js +276 -0
- package/scripts/validators/story-format-validator.js +176 -0
- package/scripts/validators/test-result-validator.js +99 -0
- package/scripts/validators/workflow-validator.js +240 -0
- package/src/core/agents/accessibility.md +6 -0
- package/src/core/agents/adr-writer.md +6 -0
- package/src/core/agents/analytics.md +6 -0
- package/src/core/agents/api.md +6 -0
- package/src/core/agents/ci.md +6 -0
- package/src/core/agents/codebase-query.md +237 -0
- package/src/core/agents/compliance.md +6 -0
- package/src/core/agents/configuration-damage-control.md +6 -0
- package/src/core/agents/configuration-visual-e2e.md +6 -0
- package/src/core/agents/database.md +10 -0
- package/src/core/agents/datamigration.md +6 -0
- package/src/core/agents/design.md +6 -0
- package/src/core/agents/devops.md +6 -0
- package/src/core/agents/documentation.md +6 -0
- package/src/core/agents/epic-planner.md +6 -0
- package/src/core/agents/integrations.md +6 -0
- package/src/core/agents/mentor.md +6 -0
- package/src/core/agents/mobile.md +6 -0
- package/src/core/agents/monitoring.md +6 -0
- package/src/core/agents/multi-expert.md +6 -0
- package/src/core/agents/performance.md +6 -0
- package/src/core/agents/product.md +6 -0
- package/src/core/agents/qa.md +6 -0
- package/src/core/agents/readme-updater.md +6 -0
- package/src/core/agents/refactor.md +6 -0
- package/src/core/agents/research.md +6 -0
- package/src/core/agents/security.md +6 -0
- package/src/core/agents/testing.md +10 -0
- package/src/core/agents/ui.md +6 -0
- package/src/core/commands/audit.md +401 -0
- package/src/core/commands/board.md +1 -0
- package/src/core/commands/epic.md +92 -1
- package/src/core/commands/help.md +1 -0
- package/src/core/commands/metrics.md +1 -0
- package/src/core/commands/research/analyze.md +1 -0
- package/src/core/commands/research/ask.md +2 -0
- package/src/core/commands/research/import.md +1 -0
- package/src/core/commands/research/list.md +2 -0
- package/src/core/commands/research/synthesize.md +584 -0
- package/src/core/commands/research/view.md +2 -0
- package/src/core/commands/status.md +126 -1
- package/src/core/commands/story/list.md +9 -9
- package/src/core/commands/story/view.md +1 -0
- package/src/core/experts/codebase-query/expertise.yaml +190 -0
- package/src/core/experts/codebase-query/question.md +73 -0
- package/src/core/experts/codebase-query/self-improve.md +105 -0
- package/tools/cli/commands/tui.js +40 -271
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const blessed = require('blessed');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create the status bar at the bottom with always-visible key hints
|
|
7
|
+
* nano-style: users can always see what keys are available
|
|
8
|
+
*/
|
|
9
|
+
module.exports = function createStatusBar(screen, state) {
|
|
10
|
+
const statusBar = blessed.box({
|
|
11
|
+
parent: screen,
|
|
12
|
+
bottom: 0,
|
|
13
|
+
left: 0,
|
|
14
|
+
width: '100%',
|
|
15
|
+
height: 1,
|
|
16
|
+
tags: true,
|
|
17
|
+
style: {
|
|
18
|
+
fg: 'white',
|
|
19
|
+
bg: 'blue'
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Always-visible key hints (nano-style for user-friendliness)
|
|
24
|
+
const hints = [
|
|
25
|
+
'{bold}1-3{/bold}:Tab',
|
|
26
|
+
'{bold}Tab{/bold}:Next',
|
|
27
|
+
'{bold}j/k{/bold}:Nav',
|
|
28
|
+
'{bold}r{/bold}:Refresh',
|
|
29
|
+
'{bold}?{/bold}:Help',
|
|
30
|
+
'{bold}q{/bold}:Quit'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const hintText = ' ' + hints.join(' ');
|
|
34
|
+
statusBar.setContent(hintText);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
element: statusBar,
|
|
38
|
+
setStatus(text) {
|
|
39
|
+
// Show custom status with key hints
|
|
40
|
+
const shortHints = [
|
|
41
|
+
'{bold}r{/bold}:Refresh',
|
|
42
|
+
'{bold}?{/bold}:Help',
|
|
43
|
+
'{bold}q{/bold}:Quit'
|
|
44
|
+
];
|
|
45
|
+
statusBar.setContent(` ${text} | ${shortHints.join(' ')}`);
|
|
46
|
+
},
|
|
47
|
+
resetHints() {
|
|
48
|
+
statusBar.setContent(hintText);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const blessed = require('blessed');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a proper styled tab bar with visual distinction
|
|
7
|
+
*/
|
|
8
|
+
module.exports = function createTabBar(screen, state) {
|
|
9
|
+
// Header bar background
|
|
10
|
+
const header = blessed.box({
|
|
11
|
+
parent: screen,
|
|
12
|
+
top: 0,
|
|
13
|
+
left: 0,
|
|
14
|
+
width: '100%',
|
|
15
|
+
height: 3,
|
|
16
|
+
style: {
|
|
17
|
+
bg: 'black'
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Logo/title
|
|
22
|
+
blessed.box({
|
|
23
|
+
parent: header,
|
|
24
|
+
top: 0,
|
|
25
|
+
left: 0,
|
|
26
|
+
width: 20,
|
|
27
|
+
height: 3,
|
|
28
|
+
content: '{bold}{#e8683a-fg}▄▀▄ AgileFlow{/}',
|
|
29
|
+
tags: true,
|
|
30
|
+
style: {
|
|
31
|
+
bg: 'black'
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Tab container
|
|
36
|
+
const tabContainer = blessed.box({
|
|
37
|
+
parent: header,
|
|
38
|
+
top: 0,
|
|
39
|
+
left: 20,
|
|
40
|
+
width: '100%-20',
|
|
41
|
+
height: 3,
|
|
42
|
+
style: {
|
|
43
|
+
bg: 'black'
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Create styled tabs
|
|
48
|
+
const tabs = state.tabs.map((name, i) => {
|
|
49
|
+
const tab = blessed.box({
|
|
50
|
+
parent: tabContainer,
|
|
51
|
+
top: 1,
|
|
52
|
+
left: i * 18,
|
|
53
|
+
width: 16,
|
|
54
|
+
height: 1,
|
|
55
|
+
content: `[${i + 1}] ${name}`,
|
|
56
|
+
tags: true,
|
|
57
|
+
style: {
|
|
58
|
+
fg: 'white',
|
|
59
|
+
bg: 'black'
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return tab;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Version info on right
|
|
66
|
+
blessed.box({
|
|
67
|
+
parent: header,
|
|
68
|
+
top: 1,
|
|
69
|
+
right: 1,
|
|
70
|
+
width: 12,
|
|
71
|
+
height: 1,
|
|
72
|
+
content: '{gray-fg}v2.90.7{/}',
|
|
73
|
+
tags: true,
|
|
74
|
+
style: {
|
|
75
|
+
bg: 'black'
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
element: header,
|
|
81
|
+
setTab(index) {
|
|
82
|
+
tabs.forEach((tab, i) => {
|
|
83
|
+
if (i === index) {
|
|
84
|
+
// Active tab - cyan background, black text, with brackets
|
|
85
|
+
tab.style.fg = 'black';
|
|
86
|
+
tab.style.bg = 'cyan';
|
|
87
|
+
tab.style.bold = true;
|
|
88
|
+
tab.setContent(`▶ ${state.tabs[i]} ◀`);
|
|
89
|
+
} else {
|
|
90
|
+
// Inactive tabs
|
|
91
|
+
tab.style.fg = 'gray';
|
|
92
|
+
tab.style.bg = 'black';
|
|
93
|
+
tab.style.bold = false;
|
|
94
|
+
tab.setContent(`[${i + 1}] ${state.tabs[i]}`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
};
|
package/scripts/tui/index.js
CHANGED
|
@@ -4,52 +4,58 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* AgileFlow TUI - Terminal User Interface
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* and interactive
|
|
7
|
+
* Full-screen, flicker-free dashboard for session monitoring, multi-agent
|
|
8
|
+
* orchestration, and interactive workflow control.
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
11
|
* node scripts/tui/index.js
|
|
12
12
|
* npx agileflow tui
|
|
13
|
+
* npx agileflow tui --fallback (use simple ANSI version)
|
|
13
14
|
*
|
|
14
15
|
* Key bindings:
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* r
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* 1-3 Switch tabs (Sessions, Output, Trace)
|
|
17
|
+
* Tab Next tab
|
|
18
|
+
* j/k Navigate list items
|
|
19
|
+
* r Refresh data
|
|
20
|
+
* ?/h Toggle help overlay
|
|
21
|
+
* q Quit TUI
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
const useInk = false;
|
|
24
|
+
// Check for --fallback flag
|
|
25
|
+
const useFallback = process.argv.includes('--fallback') || process.argv.includes('--simple');
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Main entry point
|
|
29
29
|
*/
|
|
30
30
|
async function main() {
|
|
31
|
-
if (
|
|
32
|
-
// Use
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// console.log('Action:', action);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Render the dashboard
|
|
44
|
-
const { waitUntilExit } = render(React.createElement(Dashboard, { onAction: handleAction }));
|
|
45
|
-
|
|
46
|
-
// Wait for exit
|
|
47
|
-
await waitUntilExit();
|
|
31
|
+
if (useFallback) {
|
|
32
|
+
// Use simple TUI (pure Node.js ANSI codes, no dependencies)
|
|
33
|
+
try {
|
|
34
|
+
const { main: simpleTuiMain } = require('./simple-tui');
|
|
35
|
+
simpleTuiMain();
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error('Simple TUI Error:', err.message);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
48
40
|
} else {
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
// Use blessed TUI (professional full-screen interface)
|
|
42
|
+
try {
|
|
43
|
+
const { main: blessedMain } = require('./blessed');
|
|
44
|
+
blessedMain();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
// If blessed fails (missing deps, terminal issues), fall back to simple
|
|
47
|
+
console.error('Blessed TUI failed to load:', err.message);
|
|
48
|
+
console.error('Falling back to simple TUI...\n');
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const { main: simpleTuiMain } = require('./simple-tui');
|
|
52
|
+
simpleTuiMain();
|
|
53
|
+
} catch (fallbackErr) {
|
|
54
|
+
console.error('Fallback TUI also failed:', fallbackErr.message);
|
|
55
|
+
console.error('\nTry running with: npx agileflow status');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
|
|
@@ -55,6 +55,7 @@ const ANSI = {
|
|
|
55
55
|
showCursor: '\x1b[?25h',
|
|
56
56
|
saveCursor: '\x1b[s',
|
|
57
57
|
restoreCursor: '\x1b[u',
|
|
58
|
+
clearLine: '\x1b[K', // Clear from cursor to end of line
|
|
58
59
|
};
|
|
59
60
|
|
|
60
61
|
// Get project root
|
|
@@ -213,7 +214,8 @@ class SimpleTUI {
|
|
|
213
214
|
this.render();
|
|
214
215
|
});
|
|
215
216
|
|
|
216
|
-
//
|
|
217
|
+
// Clear screen once at start, then render
|
|
218
|
+
process.stdout.write(ANSI.clear + ANSI.home);
|
|
217
219
|
this.render();
|
|
218
220
|
|
|
219
221
|
// Update loop
|
|
@@ -290,8 +292,8 @@ class SimpleTUI {
|
|
|
290
292
|
const height = process.stdout.rows || 24;
|
|
291
293
|
const output = [];
|
|
292
294
|
|
|
293
|
-
//
|
|
294
|
-
output.push(ANSI.
|
|
295
|
+
// Move to home position (don't clear - prevents bouncing)
|
|
296
|
+
output.push(ANSI.home);
|
|
295
297
|
|
|
296
298
|
// Determine layout mode based on terminal width
|
|
297
299
|
const isWide = width >= 100;
|
|
@@ -317,8 +319,9 @@ class SimpleTUI {
|
|
|
317
319
|
this.renderStacked(output, width, height, sessions, loopStatus, agentEvents, isNarrow);
|
|
318
320
|
}
|
|
319
321
|
|
|
320
|
-
// Output everything
|
|
321
|
-
|
|
322
|
+
// Output everything with line clearing to prevent artifacts
|
|
323
|
+
const outputWithClear = output.map(line => line + ANSI.clearLine);
|
|
324
|
+
process.stdout.write(outputWithClear.join('\n'));
|
|
322
325
|
}
|
|
323
326
|
|
|
324
327
|
renderSideBySide(output, width, height, sessions, loopStatus, agentEvents) {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Validators
|
|
2
|
+
|
|
3
|
+
Specialized self-validation scripts for AgileFlow agents.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Validators are Node.js scripts that run via hooks to verify agent output. They enable **closed-loop prompts** where validation is guaranteed, not optional.
|
|
8
|
+
|
|
9
|
+
**Research**: See [ADR-0009](../../../docs/03-decisions/adr-0009-specialized-self-validating-agents.md)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Exit Codes
|
|
14
|
+
|
|
15
|
+
| Code | Meaning | Behavior |
|
|
16
|
+
|------|---------|----------|
|
|
17
|
+
| `0` | Success | Proceed normally |
|
|
18
|
+
| `2` | Error (blocking) | Stderr sent to Claude for self-correction |
|
|
19
|
+
| Other | Warning | Log but continue |
|
|
20
|
+
|
|
21
|
+
**Exit code 2 is special**: Claude receives stderr output and automatically attempts to fix the issue.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Input Format
|
|
26
|
+
|
|
27
|
+
Validators receive JSON via stdin with tool context:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"tool_name": "Write",
|
|
32
|
+
"tool_input": {
|
|
33
|
+
"file_path": "/path/to/file.json",
|
|
34
|
+
"content": "..."
|
|
35
|
+
},
|
|
36
|
+
"result": "File written successfully"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Validator Template
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
#!/usr/bin/env node
|
|
46
|
+
const fs = require('fs');
|
|
47
|
+
|
|
48
|
+
let input = '';
|
|
49
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
50
|
+
process.stdin.on('end', () => {
|
|
51
|
+
try {
|
|
52
|
+
const context = JSON.parse(input);
|
|
53
|
+
const filePath = context.tool_input?.file_path;
|
|
54
|
+
|
|
55
|
+
if (!filePath) {
|
|
56
|
+
console.log('No file path in context, skipping');
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Your validation logic here
|
|
61
|
+
const issues = validate(filePath);
|
|
62
|
+
|
|
63
|
+
if (issues.length > 0) {
|
|
64
|
+
// Exit 2 = Claude will try to fix these
|
|
65
|
+
console.error(`Resolve these issues in ${filePath}:`);
|
|
66
|
+
issues.forEach(i => console.error(` - ${i}`));
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`Validation passed: ${filePath}`);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// Exit 1 = warning, don't block
|
|
74
|
+
console.error(`Validator error: ${e.message}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
function validate(filePath) {
|
|
80
|
+
const issues = [];
|
|
81
|
+
// Add your checks here
|
|
82
|
+
return issues;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Hook Configuration
|
|
89
|
+
|
|
90
|
+
Add hooks to agent/command frontmatter:
|
|
91
|
+
|
|
92
|
+
### PostToolUse (after each tool call)
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
hooks:
|
|
96
|
+
PostToolUse:
|
|
97
|
+
- matcher: "Write"
|
|
98
|
+
hooks:
|
|
99
|
+
- type: command
|
|
100
|
+
command: "node .agileflow/hooks/validators/your-validator.js"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Stop (when agent finishes)
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
hooks:
|
|
107
|
+
Stop:
|
|
108
|
+
- hooks:
|
|
109
|
+
- type: command
|
|
110
|
+
command: "node .agileflow/hooks/validators/final-validator.js"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Best Practices
|
|
116
|
+
|
|
117
|
+
1. **Keep validators fast** (< 100ms) - they run on every matching tool call
|
|
118
|
+
2. **Use specific matchers** - `"Write"` not `"Write|Edit|Read"`
|
|
119
|
+
3. **Return helpful errors** - Claude uses stderr to fix issues
|
|
120
|
+
4. **Test standalone first** - `echo '{"tool_input":{"file_path":"test.json"}}' | node validator.js`
|
|
121
|
+
5. **Log success too** - helps debugging hook chains
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Available Validators
|
|
126
|
+
|
|
127
|
+
| Validator | Purpose | Matcher |
|
|
128
|
+
|-----------|---------|---------|
|
|
129
|
+
| `json-schema-validator.js` | Validate JSON structure | Write (*.json) |
|
|
130
|
+
| `markdown-validator.js` | Validate markdown format | Write (*.md) |
|
|
131
|
+
| `story-format-validator.js` | Validate story structure | Write (status.json) |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Testing Validators
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Test with sample input
|
|
139
|
+
echo '{"tool_name":"Write","tool_input":{"file_path":"test.json","content":"{}"}}' | node json-schema-validator.js
|
|
140
|
+
|
|
141
|
+
# Check exit code
|
|
142
|
+
echo $?
|
|
143
|
+
```
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Component Validator
|
|
4
|
+
*
|
|
5
|
+
* Validates React/Vue/Svelte component files for common issues.
|
|
6
|
+
*
|
|
7
|
+
* Exit codes:
|
|
8
|
+
* 0 = Success
|
|
9
|
+
* 2 = Error (Claude will attempt to fix)
|
|
10
|
+
* 1 = Warning (logged but not blocking)
|
|
11
|
+
*
|
|
12
|
+
* Usage in agent hooks:
|
|
13
|
+
* hooks:
|
|
14
|
+
* PostToolUse:
|
|
15
|
+
* - matcher: "Write"
|
|
16
|
+
* hooks:
|
|
17
|
+
* - type: command
|
|
18
|
+
* command: "node .agileflow/hooks/validators/component-validator.js"
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
let input = '';
|
|
25
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
26
|
+
process.stdin.on('end', () => {
|
|
27
|
+
try {
|
|
28
|
+
const context = JSON.parse(input);
|
|
29
|
+
const filePath = context.tool_input?.file_path;
|
|
30
|
+
|
|
31
|
+
// Only validate component files
|
|
32
|
+
if (!filePath || !isComponentFile(filePath)) {
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Skip if file doesn't exist
|
|
37
|
+
if (!fs.existsSync(filePath)) {
|
|
38
|
+
console.log(`File not found: ${filePath} (skipping validation)`);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const issues = validateComponent(filePath);
|
|
43
|
+
|
|
44
|
+
if (issues.length > 0) {
|
|
45
|
+
console.error(`Fix these component issues in ${filePath}:`);
|
|
46
|
+
issues.forEach(i => console.error(` - ${i}`));
|
|
47
|
+
process.exit(2); // Claude will fix
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(`Component validation passed: ${filePath}`);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error(`Validator error: ${e.message}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function isComponentFile(filePath) {
|
|
59
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
60
|
+
const componentExtensions = ['.jsx', '.tsx', '.vue', '.svelte'];
|
|
61
|
+
|
|
62
|
+
// Also check for .js/.ts files in component directories
|
|
63
|
+
if (['.js', '.ts'].includes(ext)) {
|
|
64
|
+
const normalizedPath = filePath.toLowerCase();
|
|
65
|
+
return normalizedPath.includes('/components/') ||
|
|
66
|
+
normalizedPath.includes('/pages/') ||
|
|
67
|
+
normalizedPath.includes('/views/');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return componentExtensions.includes(ext);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateComponent(filePath) {
|
|
74
|
+
const issues = [];
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
78
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
79
|
+
const fileName = path.basename(filePath, ext);
|
|
80
|
+
|
|
81
|
+
// Check for empty component
|
|
82
|
+
if (!content.trim()) {
|
|
83
|
+
issues.push('Component file is empty');
|
|
84
|
+
return issues;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// React/JSX/TSX validation
|
|
88
|
+
if (['.jsx', '.tsx'].includes(ext) || content.includes('React')) {
|
|
89
|
+
issues.push(...validateReactComponent(content, fileName));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Vue validation
|
|
93
|
+
if (ext === '.vue') {
|
|
94
|
+
issues.push(...validateVueComponent(content));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Svelte validation
|
|
98
|
+
if (ext === '.svelte') {
|
|
99
|
+
issues.push(...validateSvelteComponent(content));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// General accessibility checks
|
|
103
|
+
issues.push(...validateAccessibility(content));
|
|
104
|
+
|
|
105
|
+
} catch (e) {
|
|
106
|
+
issues.push(`Read error: ${e.message}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return issues;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function validateReactComponent(content, fileName) {
|
|
113
|
+
const issues = [];
|
|
114
|
+
|
|
115
|
+
// Check for component export
|
|
116
|
+
if (!content.includes('export default') && !content.includes('export function') && !content.includes('export const')) {
|
|
117
|
+
issues.push('Component should have an export (export default, export function, or export const)');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check for React import in JSX files
|
|
121
|
+
if (content.includes('React.') && !content.includes("from 'react'") && !content.includes('from "react"')) {
|
|
122
|
+
issues.push('Using React. prefix but React is not imported');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check for proper function/component naming (should match filename for default exports)
|
|
126
|
+
const pascalCaseRegex = /^[A-Z][a-zA-Z0-9]*$/;
|
|
127
|
+
if (!pascalCaseRegex.test(fileName) && !fileName.startsWith('use')) {
|
|
128
|
+
// Only warn, don't block - could be a utility file
|
|
129
|
+
console.log(`Note: Component filename "${fileName}" should be PascalCase`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check for inline styles (prefer CSS modules or styled-components)
|
|
133
|
+
const inlineStyleCount = (content.match(/style=\{\{/g) || []).length;
|
|
134
|
+
if (inlineStyleCount > 5) {
|
|
135
|
+
issues.push(`Too many inline styles (${inlineStyleCount}) - consider using CSS modules or styled-components`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check for console.log in production code
|
|
139
|
+
if (content.includes('console.log') && !content.includes('// debug') && !content.includes('// DEBUG')) {
|
|
140
|
+
console.log('Note: console.log found - ensure it\'s removed before production');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check for missing key prop in map
|
|
144
|
+
if (content.includes('.map(') && content.includes('<') && !content.includes('key=')) {
|
|
145
|
+
issues.push('Array .map() rendering elements should include key prop');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return issues;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function validateVueComponent(content) {
|
|
152
|
+
const issues = [];
|
|
153
|
+
|
|
154
|
+
// Check for required template section
|
|
155
|
+
if (!content.includes('<template>') && !content.includes('<template ')) {
|
|
156
|
+
issues.push('Vue component must have a <template> section');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check for script section
|
|
160
|
+
if (!content.includes('<script') && !content.includes('<script>')) {
|
|
161
|
+
// Not strictly required but recommended
|
|
162
|
+
console.log('Note: Vue component has no <script> section');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check for scoped styles (recommended)
|
|
166
|
+
if (content.includes('<style>') && !content.includes('<style scoped') && !content.includes('scoped>')) {
|
|
167
|
+
console.log('Note: Consider using scoped styles to prevent CSS leaks');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return issues;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function validateSvelteComponent(content) {
|
|
174
|
+
const issues = [];
|
|
175
|
+
|
|
176
|
+
// Check for script section
|
|
177
|
+
if (!content.includes('<script') && !content.includes('<script>')) {
|
|
178
|
+
console.log('Note: Svelte component has no <script> section');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return issues;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function validateAccessibility(content) {
|
|
185
|
+
const issues = [];
|
|
186
|
+
|
|
187
|
+
// Check for images without alt
|
|
188
|
+
const imgWithoutAlt = content.match(/<img[^>]*(?!alt=)[^>]*>/gi) || [];
|
|
189
|
+
const imgWithEmptyAlt = content.match(/<img[^>]*alt=["']["'][^>]*>/gi) || [];
|
|
190
|
+
|
|
191
|
+
if (imgWithoutAlt.length > 0) {
|
|
192
|
+
issues.push(`Found ${imgWithoutAlt.length} <img> tag(s) without alt attribute`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check for button without type
|
|
196
|
+
if (content.includes('<button') && !content.includes('type=')) {
|
|
197
|
+
console.log('Note: <button> elements should have explicit type attribute');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check for click handlers on non-interactive elements
|
|
201
|
+
const clickOnDiv = content.match(/onClick[^>]*>[^<]*<\/div>/gi) || [];
|
|
202
|
+
if (clickOnDiv.length > 0) {
|
|
203
|
+
issues.push('onClick on <div> detected - use <button> for interactive elements (accessibility)');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check for form inputs without labels
|
|
207
|
+
if (content.includes('<input') && !content.includes('<label') && !content.includes('aria-label')) {
|
|
208
|
+
console.log('Note: <input> elements should have associated <label> or aria-label');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return issues;
|
|
212
|
+
}
|