Initial commit
This commit is contained in:
325
skills/fluxwing-validator/README.md
Normal file
325
skills/fluxwing-validator/README.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# Fluxwing Validator
|
||||
|
||||
Deterministic validation for uxscii components and screens.
|
||||
|
||||
## Two Modes of Operation
|
||||
|
||||
### Interactive Mode (User Invocation)
|
||||
|
||||
When users say "Validate my components", the skill:
|
||||
1. Presents menu (everything/components/screens/custom)
|
||||
2. Runs appropriate validators
|
||||
3. Shows minimal summary with optional details
|
||||
|
||||
**Example:**
|
||||
```
|
||||
✓ 12/14 components valid
|
||||
✗ 2/14 components failed
|
||||
|
||||
Show error details? (yes/no)
|
||||
```
|
||||
|
||||
### Direct Mode (Skill-to-Skill)
|
||||
|
||||
Other skills call validator scripts directly:
|
||||
```bash
|
||||
node {SKILL_ROOT}/../fluxwing-validator/validate-component.js \
|
||||
./fluxwing/components/button.uxm \
|
||||
{SKILL_ROOT}/schemas/uxm-component.schema.json
|
||||
```
|
||||
|
||||
Output: Always verbose (full errors, warnings, stats)
|
||||
Exit codes: 0 = valid, 1 = errors, 2 = invalid args
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This skill contains validation scripts that can be called from Claude Code skills to validate `.uxm` component files against JSON Schema with additional uxscii-specific checks.
|
||||
|
||||
**Key features:**
|
||||
- ✅ **Deterministic** - Locked dependencies via `package-lock.json`
|
||||
- ✅ **Zero setup** - `node_modules` bundled in repository
|
||||
- ✅ **Fast** - ~80ms execution time with ajv
|
||||
- ✅ **Comprehensive** - Schema validation + uxscii-specific rules
|
||||
- ✅ **Structured output** - JSON and human-readable formats
|
||||
|
||||
## Installation
|
||||
|
||||
This skill is bundled with fluxwing-skills plugin.
|
||||
|
||||
**Plugin install:**
|
||||
```bash
|
||||
/plugin install fluxwing-skills
|
||||
```
|
||||
|
||||
**Script install (development):**
|
||||
```bash
|
||||
./scripts/install.sh
|
||||
```
|
||||
|
||||
No npm install required - dependencies bundled with skill.
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
skills/fluxwing-validator/
|
||||
├── SKILL.md # Interactive validation workflow
|
||||
├── validate-component.js # Component validator
|
||||
├── validate-screen.js # Screen validator
|
||||
├── validate-batch.js # Batch validator
|
||||
├── test-validator.js # Component test suite
|
||||
├── test-screen-validator.js # Screen test suite
|
||||
├── package.json # Dependencies (ajv 8.12.0)
|
||||
├── package-lock.json # Locked versions
|
||||
├── node_modules/ # Bundled dependencies (~7.4 MB)
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### From Command Line
|
||||
|
||||
```bash
|
||||
# From project root
|
||||
node skills/fluxwing-validator/validate-component.js \
|
||||
./fluxwing/components/component.uxm \
|
||||
skills/fluxwing-component-creator/schemas/uxm-component.schema.json
|
||||
|
||||
# JSON output (for programmatic use)
|
||||
node skills/fluxwing-validator/validate-component.js \
|
||||
./fluxwing/components/component.uxm \
|
||||
skills/fluxwing-component-creator/schemas/uxm-component.schema.json \
|
||||
--json
|
||||
```
|
||||
|
||||
### From Other Skills (using SKILL_ROOT)
|
||||
|
||||
```bash
|
||||
# In SKILL.md - Direct mode validation
|
||||
node {SKILL_ROOT}/../fluxwing-validator/validate-component.js \
|
||||
./fluxwing/components/button.uxm \
|
||||
{SKILL_ROOT}/schemas/uxm-component.schema.json
|
||||
```
|
||||
|
||||
### Exit Codes
|
||||
|
||||
- `0` - Component is valid
|
||||
- `1` - Validation errors found
|
||||
- `2` - Missing dependencies or invalid arguments
|
||||
|
||||
## Validation Checks
|
||||
|
||||
### 1. JSON Schema Validation
|
||||
|
||||
Validates against `uxm-component.schema.json` using ajv (Draft 7):
|
||||
- Required fields (id, type, version, metadata, props, ascii)
|
||||
- Field types and formats
|
||||
- Enum constraints (type, category, fidelity)
|
||||
- Pattern matching (id format, version format)
|
||||
- Array and object structures
|
||||
|
||||
### 2. ASCII Template File
|
||||
|
||||
- Checks that corresponding `.md` file exists
|
||||
- Validates path: `component.uxm` → `component.md`
|
||||
|
||||
### 3. Template Variables
|
||||
|
||||
- Extracts `{{variables}}` from `.md` template
|
||||
- Compares against `ascii.variables` in `.uxm`
|
||||
- Warns if variables in `.md` are not defined in `.uxm`
|
||||
- Supports both formats:
|
||||
- Array of strings: `["text", "value"]`
|
||||
- Array of objects: `[{"name": "text", "type": "string"}]`
|
||||
|
||||
### 4. Accessibility
|
||||
|
||||
For interactive components (with `behavior.interactions`):
|
||||
- Warns if missing `behavior.accessibility.role`
|
||||
- Warns if `focusable` is not set
|
||||
|
||||
### 5. ASCII Dimensions
|
||||
|
||||
- Warns if `ascii.width` > 120 (terminal width limit)
|
||||
- Warns if `ascii.height` > 50 (single viewport limit)
|
||||
|
||||
### 6. State Properties
|
||||
|
||||
- Warns if states exist but have no properties defined
|
||||
|
||||
## Output Format
|
||||
|
||||
### Human-Readable
|
||||
|
||||
```
|
||||
✓ Valid: primary-button
|
||||
Type: button
|
||||
Version: 1.0.0
|
||||
States: 4
|
||||
Props: 5
|
||||
|
||||
Warnings: 1
|
||||
1. Interactive component should have ARIA role
|
||||
Location: behavior → accessibility → role
|
||||
```
|
||||
|
||||
### JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"errors": [],
|
||||
"warnings": [
|
||||
{
|
||||
"path": ["behavior", "accessibility", "role"],
|
||||
"message": "Interactive component should have ARIA role",
|
||||
"type": "accessibility"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"id": "primary-button",
|
||||
"type": "button",
|
||||
"version": "1.0.0",
|
||||
"states": 4,
|
||||
"props": 5,
|
||||
"interactive": true,
|
||||
"hasAccessibility": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
Only 6 packages, 2.7 MB total:
|
||||
|
||||
- `ajv@8.12.0` - JSON Schema validator (industry standard)
|
||||
- `ajv-formats@2.1.1` - Format validation (date-time, etc.)
|
||||
- Supporting packages (fast-deep-equal, fast-uri, json-schema-traverse, require-from-string)
|
||||
|
||||
**All dependencies are bundled in `node_modules/` and checked into git.**
|
||||
|
||||
## Node.js Version
|
||||
|
||||
- **Minimum:** Node.js 14.0.0
|
||||
- **Recommended:** Node.js 18+ or 20+ (LTS)
|
||||
- **Tested:** Node.js 18, 20, 22
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run test suite against bundled templates
|
||||
npm test
|
||||
|
||||
# Or directly
|
||||
node test-validator.js
|
||||
```
|
||||
|
||||
Tests validate components from:
|
||||
- `skills/fluxwing-component-creator/templates/*.uxm`
|
||||
|
||||
## Development
|
||||
|
||||
### Updating Validation Logic
|
||||
|
||||
1. Edit `validate-component.js`
|
||||
2. Test changes: `npm test`
|
||||
3. Commit (no build step needed)
|
||||
|
||||
### Updating Dependencies
|
||||
|
||||
```bash
|
||||
npm update
|
||||
npm audit fix
|
||||
npm test
|
||||
git add package-lock.json node_modules/
|
||||
git commit -m "Update dependencies"
|
||||
```
|
||||
|
||||
### Adding New Validators
|
||||
|
||||
Create new scripts following the same pattern:
|
||||
- `validate-screen.js` - For screen validation
|
||||
- `validate-variable.js` - For variable substitution
|
||||
|
||||
## Why Node.js?
|
||||
|
||||
Compared to Python or native binaries:
|
||||
|
||||
**Advantages:**
|
||||
- ✅ No build step (unlike Go/Rust/Deno compiled)
|
||||
- ✅ Readable source code (easier debugging)
|
||||
- ✅ Small diffs (text, not binaries)
|
||||
- ✅ Likely installed (most developers have Node.js)
|
||||
- ✅ Mature ecosystem (ajv is industry standard)
|
||||
- ✅ Fast enough (~80ms including startup)
|
||||
|
||||
**Tradeoffs:**
|
||||
- ⚠️ Requires Node.js runtime (not zero-dependency)
|
||||
- ⚠️ Slower than native (~80ms vs ~5ms for Go)
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Component Creator Skill
|
||||
|
||||
```bash
|
||||
# After creating component (from SKILL.md)
|
||||
node {SKILL_ROOT}/../fluxwing-validator/validate-component.js \
|
||||
./fluxwing/components/${COMPONENT_ID}.uxm \
|
||||
{SKILL_ROOT}/schemas/uxm-component.schema.json
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✓ Component validated successfully"
|
||||
else
|
||||
echo "✗ Validation failed - see errors above"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### Pre-commit Hook
|
||||
|
||||
```bash
|
||||
# .claude/hooks/pre-commit.sh
|
||||
for uxm_file in fluxwing/components/*.uxm; do
|
||||
node skills/fluxwing-validator/validate-component.js \
|
||||
"$uxm_file" \
|
||||
skills/fluxwing-component-creator/schemas/uxm-component.schema.json \
|
||||
|| exit 1
|
||||
done
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "ajv libraries not found"
|
||||
|
||||
The validator couldn't find the ajv dependency. This should never happen since `node_modules/` is bundled, but if it does:
|
||||
|
||||
```bash
|
||||
cd skills/fluxwing-validator
|
||||
npm install
|
||||
```
|
||||
|
||||
### "Component file not found"
|
||||
|
||||
Check that the path to the `.uxm` file is correct. Paths are relative to the current working directory.
|
||||
|
||||
### "Invalid JSON"
|
||||
|
||||
The `.uxm` file has syntax errors. Use a JSON validator to find the issue:
|
||||
|
||||
```bash
|
||||
cat component.uxm | jq .
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [x] Screen validation (`validate-screen.js`)
|
||||
- [x] Batch validation (`validate-batch.js`)
|
||||
- [ ] Variable substitution validation
|
||||
- [ ] Watch mode for development
|
||||
- [ ] Integration with CI/CD pipelines
|
||||
- [ ] VSCode extension integration
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
354
skills/fluxwing-validator/SKILL.md
Normal file
354
skills/fluxwing-validator/SKILL.md
Normal file
@@ -0,0 +1,354 @@
|
||||
---
|
||||
name: fluxwing-validator
|
||||
description: Validate uxscii components and screens against schema with interactive menu or direct script calls
|
||||
version: 1.0.0
|
||||
author: Fluxwing Team
|
||||
allowed-tools: Read, Bash, AskUserQuestion, TodoWrite
|
||||
---
|
||||
|
||||
# Fluxwing Validator
|
||||
|
||||
Validate uxscii components and screens against JSON Schema with interactive workflows.
|
||||
|
||||
## Overview
|
||||
|
||||
This skill provides two modes of operation:
|
||||
|
||||
1. **Interactive Mode**: User invocation with menu and minimal output
|
||||
2. **Direct Mode**: Script calls from other skills with verbose output
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
**User says:**
|
||||
- "Validate my components"
|
||||
- "Check if everything is valid"
|
||||
- "Run validation on my screens"
|
||||
- "Validate the project"
|
||||
|
||||
**Other skills:** Call validator scripts directly (see Technical Reference below)
|
||||
|
||||
## Interactive Validation Workflow
|
||||
|
||||
### Step 1: Present Validation Options
|
||||
|
||||
Use AskUserQuestion to present menu:
|
||||
|
||||
```
|
||||
What would you like to validate?
|
||||
```
|
||||
|
||||
**Options:**
|
||||
1. **Everything in this project** - Validates all components and screens
|
||||
2. **Just components** - Validates `./fluxwing/components/*.uxm`
|
||||
3. **Just screens** - Validates `./fluxwing/screens/*.uxm`
|
||||
4. **Let me specify a file or pattern** - Custom path/glob pattern
|
||||
|
||||
### Step 2: Check What Exists
|
||||
|
||||
Before running validation, check if directories exist:
|
||||
|
||||
```bash
|
||||
# Check for components
|
||||
test -d ./fluxwing/components
|
||||
|
||||
# Check for screens
|
||||
test -d ./fluxwing/screens
|
||||
```
|
||||
|
||||
**If neither exists:**
|
||||
- Inform user: "No components or screens found. Create some first!"
|
||||
- Exit cleanly
|
||||
|
||||
**If option 4 (custom) selected:**
|
||||
- Ask user for the pattern/file path
|
||||
- Validate it's not empty
|
||||
- Proceed with user-provided pattern
|
||||
|
||||
### Step 3: Run Validation
|
||||
|
||||
Based on user selection:
|
||||
|
||||
**Option 1: Everything**
|
||||
```bash
|
||||
# Validate components
|
||||
node {SKILL_ROOT}/validate-batch.js \
|
||||
"./fluxwing/components/*.uxm" \
|
||||
"{SKILL_ROOT}/../fluxwing-component-creator/schemas/uxm-component.schema.json" \
|
||||
--json
|
||||
|
||||
# Validate screens
|
||||
node {SKILL_ROOT}/validate-batch.js \
|
||||
"./fluxwing/screens/*.uxm" \
|
||||
"{SKILL_ROOT}/../fluxwing-component-creator/schemas/uxm-component.schema.json" \
|
||||
--screens \
|
||||
--json
|
||||
```
|
||||
|
||||
**Option 2: Just components**
|
||||
```bash
|
||||
node {SKILL_ROOT}/validate-batch.js \
|
||||
"./fluxwing/components/*.uxm" \
|
||||
"{SKILL_ROOT}/../fluxwing-component-creator/schemas/uxm-component.schema.json" \
|
||||
--json
|
||||
```
|
||||
|
||||
**Option 3: Just screens**
|
||||
```bash
|
||||
node {SKILL_ROOT}/validate-batch.js \
|
||||
"./fluxwing/screens/*.uxm" \
|
||||
"{SKILL_ROOT}/../fluxwing-component-creator/schemas/uxm-component.schema.json" \
|
||||
--screens \
|
||||
--json
|
||||
```
|
||||
|
||||
**Option 4: Custom pattern**
|
||||
```bash
|
||||
# Use user-provided pattern
|
||||
node {SKILL_ROOT}/validate-batch.js \
|
||||
"${userPattern}" \
|
||||
"{SKILL_ROOT}/../fluxwing-component-creator/schemas/uxm-component.schema.json" \
|
||||
--json
|
||||
```
|
||||
|
||||
### Step 4: Parse Results and Show Minimal Summary
|
||||
|
||||
Parse JSON output from validator to extract:
|
||||
- `total`: Total files validated
|
||||
- `passed`: Number of valid files
|
||||
- `failed`: Number of failed files
|
||||
- `warnings`: Total warning count
|
||||
|
||||
**Display minimal summary:**
|
||||
|
||||
```
|
||||
✓ 12/14 components valid
|
||||
✗ 2/14 components failed
|
||||
⚠ 3 warnings total
|
||||
```
|
||||
|
||||
**If all passed:**
|
||||
```
|
||||
✓ All 14 components valid
|
||||
⚠ 3 warnings
|
||||
```
|
||||
|
||||
**If everything failed:**
|
||||
```
|
||||
✗ All 14 components failed
|
||||
```
|
||||
|
||||
**If nothing to validate:**
|
||||
```
|
||||
No files found matching pattern
|
||||
```
|
||||
|
||||
### Step 5: Ask About Details
|
||||
|
||||
Use AskUserQuestion to ask:
|
||||
|
||||
```
|
||||
Show error details?
|
||||
```
|
||||
|
||||
**Options:**
|
||||
1. **Yes** - Display full validation output
|
||||
2. **No** - Clean exit
|
||||
|
||||
### Step 6: Display Details (if requested)
|
||||
|
||||
If user selects "Yes", show full validation output including:
|
||||
|
||||
**Failed files section:**
|
||||
```
|
||||
Failed Files:
|
||||
|
||||
✗ broken-button (./fluxwing/components/broken-button.uxm)
|
||||
Errors: 2
|
||||
1. must have required property 'fidelity'
|
||||
2. ASCII template file not found
|
||||
|
||||
✗ old-card (./fluxwing/components/old-card.uxm)
|
||||
Errors: 1
|
||||
1. invalid version format
|
||||
```
|
||||
|
||||
**Passed with warnings section:**
|
||||
```
|
||||
Passed with Warnings:
|
||||
|
||||
✓ login-screen (2 warnings)
|
||||
✓ dashboard (1 warning)
|
||||
```
|
||||
|
||||
**Fully passed section (optional, only if not too many):**
|
||||
```
|
||||
Fully Passed:
|
||||
|
||||
✓ primary-button
|
||||
✓ secondary-button
|
||||
✓ email-input
|
||||
...
|
||||
```
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### No fluxwing directory exists
|
||||
```
|
||||
No components or screens found. Create some first!
|
||||
```
|
||||
|
||||
### Empty directories
|
||||
```
|
||||
✓ 0/0 components valid
|
||||
```
|
||||
|
||||
### Invalid glob pattern (option 4)
|
||||
```
|
||||
No files found matching pattern: ${pattern}
|
||||
```
|
||||
|
||||
### Validation script fails to execute
|
||||
```
|
||||
Error: Cannot execute validator. Node.js required.
|
||||
```
|
||||
|
||||
## Technical Reference (For Other Skills)
|
||||
|
||||
### Direct Script Calls
|
||||
|
||||
Other skills (component-creator, screen-scaffolder) call validator scripts directly:
|
||||
|
||||
**Validate single component:**
|
||||
```bash
|
||||
node {SKILL_ROOT}/../fluxwing-validator/validate-component.js \
|
||||
./fluxwing/components/button.uxm \
|
||||
{SKILL_ROOT}/schemas/uxm-component.schema.json
|
||||
```
|
||||
|
||||
**Validate single screen:**
|
||||
```bash
|
||||
node {SKILL_ROOT}/../fluxwing-validator/validate-screen.js \
|
||||
./fluxwing/screens/login-screen.uxm \
|
||||
{SKILL_ROOT}/schemas/uxm-component.schema.json
|
||||
```
|
||||
|
||||
**Batch validate:**
|
||||
```bash
|
||||
node {SKILL_ROOT}/../fluxwing-validator/validate-batch.js \
|
||||
"./fluxwing/components/*.uxm" \
|
||||
{SKILL_ROOT}/schemas/uxm-component.schema.json
|
||||
```
|
||||
|
||||
**Output modes:**
|
||||
- Default: Human-readable (verbose, full errors/warnings)
|
||||
- `--json`: Machine-readable JSON
|
||||
|
||||
**Exit codes:**
|
||||
- `0`: All files valid
|
||||
- `1`: One or more files invalid
|
||||
- `2`: Missing dependencies or invalid arguments
|
||||
|
||||
### Validator Scripts
|
||||
|
||||
**Available scripts:**
|
||||
- `validate-component.js` - Validate single component file
|
||||
- `validate-screen.js` - Validate single screen file (with screen-specific checks)
|
||||
- `validate-batch.js` - Validate multiple files with glob patterns
|
||||
- `test-validator.js` - Test component templates
|
||||
- `test-screen-validator.js` - Test screen templates
|
||||
|
||||
### npm Scripts (for testing)
|
||||
|
||||
```bash
|
||||
cd {SKILL_ROOT}
|
||||
|
||||
# Run tests
|
||||
npm test # Test component templates
|
||||
npm run test:screens # Test screen templates
|
||||
|
||||
# Batch validation
|
||||
npm run validate:components # Validate all components
|
||||
npm run validate:screens # Validate all screens
|
||||
npm run validate:all # Validate everything
|
||||
```
|
||||
|
||||
## Example Interactions
|
||||
|
||||
### Example 1: Validate Everything
|
||||
|
||||
**User:** "Validate my components"
|
||||
|
||||
**Skill:**
|
||||
```
|
||||
What would you like to validate?
|
||||
|
||||
1. Everything in this project
|
||||
2. Just components
|
||||
3. Just screens
|
||||
4. Let me specify a file or pattern
|
||||
```
|
||||
|
||||
**User selects:** Option 1
|
||||
|
||||
**Skill runs validation and shows:**
|
||||
```
|
||||
✓ 12/14 components valid
|
||||
✗ 2/14 components failed
|
||||
⚠ 3 warnings total
|
||||
|
||||
✓ 2/2 screens valid
|
||||
⚠ 1 warning
|
||||
|
||||
Show error details?
|
||||
```
|
||||
|
||||
**User:** "yes"
|
||||
|
||||
**Skill shows full error details for failed files**
|
||||
|
||||
### Example 2: Validate Specific Pattern
|
||||
|
||||
**User:** "Validate my components"
|
||||
|
||||
**Skill:** (presents menu)
|
||||
|
||||
**User selects:** Option 4 (custom pattern)
|
||||
|
||||
**Skill:** "What file or pattern would you like to validate?"
|
||||
|
||||
**User:** "./fluxwing/components/*-button.uxm"
|
||||
|
||||
**Skill validates and shows:**
|
||||
```
|
||||
✓ 3/3 files valid
|
||||
|
||||
Show error details?
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
**Parse JSON output:**
|
||||
```javascript
|
||||
const result = JSON.parse(bashOutput);
|
||||
const total = result.total;
|
||||
const passed = result.passed;
|
||||
const failed = result.failed;
|
||||
const warnings = result.warnings;
|
||||
```
|
||||
|
||||
**Summary formatting:**
|
||||
- Show passed/failed ratio for quick scan
|
||||
- Highlight failures prominently
|
||||
- Warnings shown but not intrusive
|
||||
- Clean, minimal output by default
|
||||
|
||||
**Error detail formatting:**
|
||||
- Group by status (failed, warnings, passed)
|
||||
- Show file path and error count
|
||||
- Display first 2-3 errors per file
|
||||
- Indicate if more errors exist
|
||||
|
||||
---
|
||||
|
||||
**Skill Status:** Ready for use
|
||||
**Version:** 1.0.0
|
||||
23
skills/fluxwing-validator/package.json
Normal file
23
skills/fluxwing-validator/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "fluxwing-validators",
|
||||
"version": "1.0.0",
|
||||
"description": "Deterministic Node.js validators for uxscii components and screens",
|
||||
"main": "validate-component.js",
|
||||
"scripts": {
|
||||
"test": "node test-validator.js",
|
||||
"test:screens": "node test-screen-validator.js",
|
||||
"validate:components": "node validate-batch.js '../fluxwing/components/*.uxm' '../skills/fluxwing-component-creator/schemas/uxm-component.schema.json'",
|
||||
"validate:screens": "node validate-batch.js '../fluxwing/screens/*.uxm' '../skills/fluxwing-component-creator/schemas/uxm-component.schema.json' --screens",
|
||||
"validate:all": "npm run validate:components && npm run validate:screens"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"glob": "^10.3.10"
|
||||
},
|
||||
"author": "Fluxwing",
|
||||
"license": "MIT"
|
||||
}
|
||||
89
skills/fluxwing-validator/test-screen-validator.js
Executable file
89
skills/fluxwing-validator/test-screen-validator.js
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test suite for screen validator
|
||||
* Tests bundled screen templates against the schema
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { validateScreen } = require('./validate-screen.js');
|
||||
|
||||
// ANSI color codes
|
||||
const GREEN = '\x1b[32m';
|
||||
const RED = '\x1b[31m';
|
||||
const YELLOW = '\x1b[33m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
console.log('Testing Fluxwing Screen Validator\n');
|
||||
console.log('==================================================\n');
|
||||
|
||||
// Test configuration
|
||||
const schemaPath = path.join(__dirname, '..', 'fluxwing-component-creator', 'schemas', 'uxm-component.schema.json');
|
||||
const templatesDir = path.join(__dirname, '..', 'fluxwing-screen-scaffolder', 'templates');
|
||||
|
||||
// Find all .uxm files in templates directory
|
||||
const screenFiles = fs.readdirSync(templatesDir)
|
||||
.filter(file => file.endsWith('.uxm'))
|
||||
.map(file => path.join(templatesDir, file));
|
||||
|
||||
if (screenFiles.length === 0) {
|
||||
console.log(`${YELLOW}⚠ No screen templates found in ${templatesDir}${RESET}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Test results
|
||||
const results = {
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
warnings: 0
|
||||
};
|
||||
|
||||
// Run tests
|
||||
for (const screenFile of screenFiles) {
|
||||
const screenName = path.basename(screenFile, '.uxm');
|
||||
console.log(`Testing: ${screenName}`);
|
||||
|
||||
try {
|
||||
const result = validateScreen(screenFile, schemaPath);
|
||||
|
||||
if (result.valid) {
|
||||
console.log(` ${GREEN}✓ PASS${RESET} - ${screenName}`);
|
||||
results.passed++;
|
||||
|
||||
if (result.warnings.length > 0) {
|
||||
console.log(` (${result.warnings.length} warnings)`);
|
||||
results.warnings += result.warnings.length;
|
||||
}
|
||||
} else {
|
||||
console.log(` ${RED}✗ FAIL${RESET} - ${screenName}`);
|
||||
console.log(` Errors: ${result.errors.length}`);
|
||||
results.failed++;
|
||||
|
||||
// Show first few errors
|
||||
result.errors.slice(0, 3).forEach((error, i) => {
|
||||
console.log(` ${i + 1}. ${error.message}`);
|
||||
});
|
||||
|
||||
if (result.errors.length > 3) {
|
||||
console.log(` ... and ${result.errors.length - 3} more errors`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ${RED}✗ ERROR${RESET} - ${screenName}`);
|
||||
console.log(` ${error.message}`);
|
||||
results.failed++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Print summary
|
||||
console.log('==================================================');
|
||||
console.log(`Results: ${GREEN}${results.passed} passed${RESET}, ${results.failed > 0 ? RED : ''}${results.failed} failed${RESET}`);
|
||||
|
||||
if (results.warnings > 0) {
|
||||
console.log(`Warnings: ${YELLOW}${results.warnings} total${RESET}`);
|
||||
}
|
||||
|
||||
// Exit with appropriate code
|
||||
process.exit(results.failed > 0 ? 1 : 0);
|
||||
68
skills/fluxwing-validator/test-validator.js
Executable file
68
skills/fluxwing-validator/test-validator.js
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test script for validate-component.js
|
||||
* Tests against bundled template components
|
||||
*/
|
||||
|
||||
const { validateComponent } = require('./validate-component.js');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Test components from templates
|
||||
const testComponents = [
|
||||
'../fluxwing-component-creator/templates/primary-button.uxm',
|
||||
'../fluxwing-component-creator/templates/email-input.uxm',
|
||||
'../fluxwing-component-creator/templates/card.uxm',
|
||||
'../fluxwing-component-creator/templates/badge.uxm',
|
||||
'../fluxwing-component-creator/templates/alert.uxm'
|
||||
];
|
||||
|
||||
const schemaPath = path.join(__dirname, '../fluxwing-component-creator/schemas/uxm-component.schema.json');
|
||||
|
||||
console.log('Testing Fluxwing Component Validator\n');
|
||||
console.log('='.repeat(50));
|
||||
console.log('');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const componentRelPath of testComponents) {
|
||||
const componentPath = path.join(__dirname, componentRelPath);
|
||||
const componentName = path.basename(componentPath);
|
||||
|
||||
if (!fs.existsSync(componentPath)) {
|
||||
console.log(`⊘ SKIP: ${componentName} (file not found)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Testing: ${componentName}`);
|
||||
|
||||
try {
|
||||
const result = validateComponent(componentPath, schemaPath);
|
||||
|
||||
if (result.valid) {
|
||||
passed++;
|
||||
console.log(` ✓ PASS - ${result.stats.id}`);
|
||||
if (result.warnings.length > 0) {
|
||||
console.log(` (${result.warnings.length} warnings)`);
|
||||
}
|
||||
} else {
|
||||
failed++;
|
||||
console.log(` ✗ FAIL - ${result.errors.length} errors`);
|
||||
result.errors.slice(0, 2).forEach(err => {
|
||||
console.log(` • ${err.message}`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
failed++;
|
||||
console.log(` ✗ ERROR: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('='.repeat(50));
|
||||
console.log(`Results: ${passed} passed, ${failed} failed`);
|
||||
console.log('');
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
208
skills/fluxwing-validator/validate-batch.js
Executable file
208
skills/fluxwing-validator/validate-batch.js
Executable file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Batch validation for multiple .uxm files
|
||||
* Validates multiple component or screen files in one command
|
||||
*
|
||||
* Usage:
|
||||
* node validate-batch.js "./fluxwing/components/FILE.uxm" <schema.json>
|
||||
* node validate-batch.js "./fluxwing/screens/FILE.uxm" <schema.json> --json
|
||||
* node validate-batch.js "./fluxwing/ALL/FILE.uxm" <schema.json> --screens
|
||||
*
|
||||
* Options:
|
||||
* --json Output results as JSON
|
||||
* --screens Use screen validator instead of component validator
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 - All files valid
|
||||
* 1 - One or more files invalid
|
||||
* 2 - Missing dependencies or invalid arguments
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Check for glob dependency
|
||||
let glob;
|
||||
try {
|
||||
glob = require('glob');
|
||||
} catch (error) {
|
||||
console.error('Error: glob library not found');
|
||||
console.error('Install with: npm install glob');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// Import validators
|
||||
const { validateComponent } = require('./validate-component.js');
|
||||
const { validateScreen } = require('./validate-screen.js');
|
||||
|
||||
// ANSI color codes
|
||||
const GREEN = '\x1b[32m';
|
||||
const RED = '\x1b[31m';
|
||||
const YELLOW = '\x1b[33m';
|
||||
const BLUE = '\x1b[34m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
/**
|
||||
* Validate multiple files matching a glob pattern
|
||||
* @param {string} pattern - Glob pattern for files to validate
|
||||
* @param {string} schemaPath - Path to JSON schema file
|
||||
* @param {Object} options - Validation options
|
||||
* @returns {Object} Batch validation results
|
||||
*/
|
||||
function validateBatch(pattern, schemaPath, options = {}) {
|
||||
const useScreenValidator = options.screens || false;
|
||||
const validator = useScreenValidator ? validateScreen : validateComponent;
|
||||
|
||||
// Find all matching files
|
||||
const files = glob.sync(pattern);
|
||||
|
||||
if (files.length === 0) {
|
||||
return {
|
||||
total: 0,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
warnings: 0,
|
||||
files: [],
|
||||
message: `No files found matching pattern: ${pattern}`
|
||||
};
|
||||
}
|
||||
|
||||
const results = {
|
||||
total: files.length,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
warnings: 0,
|
||||
files: []
|
||||
};
|
||||
|
||||
// Validate each file
|
||||
for (const file of files) {
|
||||
const result = validator(file, schemaPath);
|
||||
|
||||
const fileResult = {
|
||||
file: file,
|
||||
id: result.stats?.id || path.basename(file, '.uxm'),
|
||||
valid: result.valid,
|
||||
errors: result.errors.length,
|
||||
warnings: result.warnings.length,
|
||||
errorDetails: result.errors,
|
||||
warningDetails: result.warnings
|
||||
};
|
||||
|
||||
results.files.push(fileResult);
|
||||
|
||||
if (result.valid) {
|
||||
results.passed++;
|
||||
} else {
|
||||
results.failed++;
|
||||
}
|
||||
|
||||
results.warnings += result.warnings.length;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print human-readable batch results
|
||||
* @param {Object} results - Batch validation results
|
||||
*/
|
||||
function printHumanReadable(results) {
|
||||
if (results.total === 0) {
|
||||
console.log(`${YELLOW}${results.message}${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${BLUE}Batch Validation Results${RESET}\n`);
|
||||
console.log(`Total files: ${results.total}`);
|
||||
console.log(`${GREEN}Passed: ${results.passed}${RESET}`);
|
||||
|
||||
if (results.failed > 0) {
|
||||
console.log(`${RED}Failed: ${results.failed}${RESET}`);
|
||||
}
|
||||
|
||||
if (results.warnings > 0) {
|
||||
console.log(`${YELLOW}Total warnings: ${results.warnings}${RESET}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Show failed files
|
||||
const failedFiles = results.files.filter(f => !f.valid);
|
||||
if (failedFiles.length > 0) {
|
||||
console.log(`${RED}Failed Files:${RESET}\n`);
|
||||
failedFiles.forEach(file => {
|
||||
console.log(` ${RED}✗${RESET} ${file.id} (${file.file})`);
|
||||
console.log(` Errors: ${file.errors}`);
|
||||
|
||||
// Show first 2 errors
|
||||
file.errorDetails.slice(0, 2).forEach((error, i) => {
|
||||
console.log(` ${i + 1}. ${error.message}`);
|
||||
});
|
||||
|
||||
if (file.errors > 2) {
|
||||
console.log(` ... and ${file.errors - 2} more errors`);
|
||||
}
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
// Show passed files with warnings
|
||||
const passedWithWarnings = results.files.filter(f => f.valid && f.warnings > 0);
|
||||
if (passedWithWarnings.length > 0) {
|
||||
console.log(`${YELLOW}Passed with Warnings:${RESET}\n`);
|
||||
passedWithWarnings.forEach(file => {
|
||||
console.log(` ${GREEN}✓${RESET} ${file.id} ${YELLOW}(${file.warnings} warnings)${RESET}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Show fully passed files (compact)
|
||||
const fullPassed = results.files.filter(f => f.valid && f.warnings === 0);
|
||||
if (fullPassed.length > 0) {
|
||||
console.log(`${GREEN}Fully Passed:${RESET}\n`);
|
||||
fullPassed.forEach(file => {
|
||||
console.log(` ${GREEN}✓${RESET} ${file.id}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print JSON results
|
||||
* @param {Object} results - Batch validation results
|
||||
*/
|
||||
function printJSON(results) {
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 2) {
|
||||
console.error('Usage: node validate-batch.js <pattern> <schema.json> [--json] [--screens]');
|
||||
console.error('');
|
||||
console.error('Examples:');
|
||||
console.error(' node validate-batch.js "./fluxwing/components/*.uxm" schema.json');
|
||||
console.error(' node validate-batch.js "./fluxwing/**/*.uxm" schema.json --json');
|
||||
console.error(' node validate-batch.js "./fluxwing/screens/*.uxm" schema.json --screens');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const pattern = args[0];
|
||||
const schemaPath = args[1];
|
||||
const jsonOutput = args.includes('--json');
|
||||
const useScreens = args.includes('--screens');
|
||||
|
||||
const results = validateBatch(pattern, schemaPath, { screens: useScreens });
|
||||
|
||||
if (jsonOutput) {
|
||||
printJSON(results);
|
||||
} else {
|
||||
printHumanReadable(results);
|
||||
}
|
||||
|
||||
process.exit(results.failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
module.exports = { validateBatch };
|
||||
359
skills/fluxwing-validator/validate-component.js
Executable file
359
skills/fluxwing-validator/validate-component.js
Executable file
@@ -0,0 +1,359 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Fast, deterministic component validation using JSON Schema (ajv).
|
||||
* Validates .uxm component files against schema with uxscii-specific checks.
|
||||
*
|
||||
* Usage:
|
||||
* node validate-component.js <component.uxm> <schema.json>
|
||||
* node validate-component.js <component.uxm> <schema.json> --json
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 - Valid component
|
||||
* 1 - Validation errors found
|
||||
* 2 - Missing dependencies or invalid arguments
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Check for ajv dependency
|
||||
let Ajv, addFormats;
|
||||
try {
|
||||
Ajv = require('ajv');
|
||||
addFormats = require('ajv-formats');
|
||||
} catch (error) {
|
||||
console.error('Error: ajv libraries not found');
|
||||
console.error('Install with: npm install ajv ajv-formats');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a .uxm component file against the schema
|
||||
* @param {string} uxmFilePath - Path to .uxm file
|
||||
* @param {string} schemaPath - Path to JSON schema file
|
||||
* @returns {Object} Validation result object
|
||||
*/
|
||||
function validateComponent(uxmFilePath, schemaPath) {
|
||||
const result = {
|
||||
valid: true,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
stats: {}
|
||||
};
|
||||
|
||||
// Load component file
|
||||
let component;
|
||||
try {
|
||||
const uxmContent = fs.readFileSync(uxmFilePath, 'utf8');
|
||||
component = JSON.parse(uxmContent);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
result.valid = false;
|
||||
result.errors.push({
|
||||
path: [],
|
||||
message: `Component file not found: ${uxmFilePath}`,
|
||||
type: 'file_not_found'
|
||||
});
|
||||
return result;
|
||||
}
|
||||
if (error instanceof SyntaxError) {
|
||||
result.valid = false;
|
||||
result.errors.push({
|
||||
path: [],
|
||||
message: `Invalid JSON: ${error.message}`,
|
||||
type: 'json_error'
|
||||
});
|
||||
return result;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Load schema file
|
||||
let schema;
|
||||
try {
|
||||
const schemaContent = fs.readFileSync(schemaPath, 'utf8');
|
||||
schema = JSON.parse(schemaContent);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
result.valid = false;
|
||||
result.errors.push({
|
||||
path: [],
|
||||
message: `Schema file not found: ${schemaPath}`,
|
||||
type: 'file_not_found'
|
||||
});
|
||||
return result;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Validate against JSON schema
|
||||
const ajv = new Ajv({ allErrors: true, strict: false });
|
||||
addFormats(ajv);
|
||||
|
||||
const validate = ajv.compile(schema);
|
||||
const valid = validate(component);
|
||||
|
||||
if (!valid) {
|
||||
for (const error of validate.errors) {
|
||||
result.errors.push({
|
||||
path: error.instancePath.split('/').filter(p => p),
|
||||
message: error.message,
|
||||
type: 'schema_violation',
|
||||
details: error.params
|
||||
});
|
||||
}
|
||||
result.valid = false;
|
||||
}
|
||||
|
||||
// uxscii-specific validation checks
|
||||
performUxsciiChecks(component, uxmFilePath, result);
|
||||
|
||||
// Collect stats
|
||||
result.stats = {
|
||||
id: component.id,
|
||||
type: component.type,
|
||||
version: component.version,
|
||||
states: component.behavior?.states?.length || 0,
|
||||
props: Object.keys(component.props || {}).length,
|
||||
interactive: component.behavior?.interactions?.length > 0,
|
||||
hasAccessibility: !!component.behavior?.accessibility
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform uxscii-specific validation checks
|
||||
* @param {Object} component - Parsed component object
|
||||
* @param {string} uxmFilePath - Path to .uxm file
|
||||
* @param {Object} result - Result object to populate
|
||||
*/
|
||||
function performUxsciiChecks(component, uxmFilePath, result) {
|
||||
// Check 1: ASCII template file exists
|
||||
const mdFilePath = uxmFilePath.replace('.uxm', '.md');
|
||||
let mdContent = null;
|
||||
|
||||
try {
|
||||
mdContent = fs.readFileSync(mdFilePath, 'utf8');
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
result.valid = false;
|
||||
result.errors.push({
|
||||
path: ['ascii', 'templateFile'],
|
||||
message: `ASCII template file not found: ${mdFilePath}`,
|
||||
type: 'missing_file'
|
||||
});
|
||||
return; // Can't do variable checks without .md file
|
||||
}
|
||||
}
|
||||
|
||||
// Check 2: Template variables match
|
||||
if (mdContent) {
|
||||
checkTemplateVariables(component, mdContent, result);
|
||||
}
|
||||
|
||||
// Check 3: Accessibility requirements for interactive components
|
||||
checkAccessibility(component, result);
|
||||
|
||||
// Check 4: ASCII dimensions
|
||||
checkAsciiDimensions(component, result);
|
||||
|
||||
// Check 5: States have properties
|
||||
checkStates(component, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that template variables in .md match those defined in .uxm
|
||||
* @param {Object} component - Component object
|
||||
* @param {string} mdContent - Content of .md file
|
||||
* @param {Object} result - Result object
|
||||
*/
|
||||
function checkTemplateVariables(component, mdContent, result) {
|
||||
// Extract {{variables}} from markdown template
|
||||
const varPattern = /\{\{(\w+)\}\}/g;
|
||||
const mdVars = new Set();
|
||||
let match;
|
||||
while ((match = varPattern.exec(mdContent)) !== null) {
|
||||
mdVars.add(match[1]);
|
||||
}
|
||||
|
||||
// Get variables from .uxm (handle both array of strings and array of objects)
|
||||
const asciiVars = component.ascii?.variables || [];
|
||||
const uxmVars = new Set();
|
||||
|
||||
if (asciiVars.length > 0) {
|
||||
if (typeof asciiVars[0] === 'object') {
|
||||
// Array of objects format: [{"name": "text", "type": "string"}]
|
||||
asciiVars.forEach(v => {
|
||||
if (v && typeof v === 'object' && v.name) {
|
||||
uxmVars.add(v.name);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Array of strings format: ["text", "value"]
|
||||
asciiVars.forEach(v => uxmVars.add(v));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for variables in .md but not defined in .uxm
|
||||
const missing = [...mdVars].filter(v => !uxmVars.has(v));
|
||||
if (missing.length > 0) {
|
||||
result.warnings.push({
|
||||
path: ['ascii', 'variables'],
|
||||
message: `Variables in .md but not defined in .uxm: ${missing.sort().join(', ')}`,
|
||||
type: 'variable_mismatch'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check accessibility requirements
|
||||
* @param {Object} component - Component object
|
||||
* @param {Object} result - Result object
|
||||
*/
|
||||
function checkAccessibility(component, result) {
|
||||
const hasInteractions = component.behavior?.interactions?.length > 0;
|
||||
|
||||
if (hasInteractions) {
|
||||
const accessibility = component.behavior?.accessibility || {};
|
||||
|
||||
if (!accessibility.role) {
|
||||
result.warnings.push({
|
||||
path: ['behavior', 'accessibility', 'role'],
|
||||
message: 'Interactive component should have ARIA role',
|
||||
type: 'accessibility'
|
||||
});
|
||||
}
|
||||
|
||||
if (!accessibility.focusable) {
|
||||
result.warnings.push({
|
||||
path: ['behavior', 'accessibility', 'focusable'],
|
||||
message: 'Interactive component should be focusable',
|
||||
type: 'accessibility'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check ASCII dimensions are within recommended limits
|
||||
* @param {Object} component - Component object
|
||||
* @param {Object} result - Result object
|
||||
*/
|
||||
function checkAsciiDimensions(component, result) {
|
||||
const ascii = component.ascii || {};
|
||||
const width = ascii.width || 0;
|
||||
const height = ascii.height || 0;
|
||||
|
||||
if (width > 120) {
|
||||
result.warnings.push({
|
||||
path: ['ascii', 'width'],
|
||||
message: `Width ${width} exceeds recommended max of 120`,
|
||||
type: 'dimensions'
|
||||
});
|
||||
}
|
||||
|
||||
if (height > 50) {
|
||||
result.warnings.push({
|
||||
path: ['ascii', 'height'],
|
||||
message: `Height ${height} exceeds recommended max of 50`,
|
||||
type: 'dimensions'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that states have properties defined
|
||||
* @param {Object} component - Component object
|
||||
* @param {Object} result - Result object
|
||||
*/
|
||||
function checkStates(component, result) {
|
||||
const states = component.behavior?.states || [];
|
||||
|
||||
for (const state of states) {
|
||||
if (!state.properties || Object.keys(state.properties).length === 0) {
|
||||
result.warnings.push({
|
||||
path: ['behavior', 'states', state.name || 'unknown'],
|
||||
message: `State '${state.name || 'unknown'}' has no properties defined`,
|
||||
type: 'incomplete_state'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print results in human-readable format
|
||||
* @param {Object} result - Validation result
|
||||
*/
|
||||
function printHumanReadable(result) {
|
||||
if (result.valid) {
|
||||
console.log(`✓ Valid: ${result.stats.id}`);
|
||||
console.log(` Type: ${result.stats.type}`);
|
||||
console.log(` Version: ${result.stats.version}`);
|
||||
console.log(` States: ${result.stats.states}`);
|
||||
console.log(` Props: ${result.stats.props}`);
|
||||
|
||||
if (result.warnings.length > 0) {
|
||||
console.log(`\n Warnings: ${result.warnings.length}`);
|
||||
result.warnings.forEach((warning, i) => {
|
||||
const pathStr = warning.path.join(' → ') || 'root';
|
||||
console.log(` ${i + 1}. ${warning.message}`);
|
||||
console.log(` Location: ${pathStr}`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(`✗ Validation Failed`);
|
||||
console.log('');
|
||||
|
||||
const maxErrors = 5;
|
||||
result.errors.slice(0, maxErrors).forEach((error, i) => {
|
||||
const pathStr = error.path.join(' → ') || 'root';
|
||||
console.log(` Error ${i + 1}: ${error.message}`);
|
||||
console.log(` Location: ${pathStr}`);
|
||||
console.log('');
|
||||
});
|
||||
|
||||
if (result.errors.length > maxErrors) {
|
||||
console.log(` ... and ${result.errors.length - maxErrors} more errors`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
*/
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 2) {
|
||||
console.log('Usage: validate-component.js <component.uxm> <schema.json> [--json]');
|
||||
console.log('');
|
||||
console.log('Validates a uxscii component against the JSON schema.');
|
||||
console.log('Returns JSON with validation results and exits 0 if valid, 1 if invalid.');
|
||||
console.log('');
|
||||
console.log('Options:');
|
||||
console.log(' --json Output results as JSON instead of human-readable format');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const uxmFile = args[0];
|
||||
const schemaFile = args[1];
|
||||
const jsonOutput = args.includes('--json');
|
||||
|
||||
const result = validateComponent(uxmFile, schemaFile);
|
||||
|
||||
if (jsonOutput) {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} else {
|
||||
printHumanReadable(result);
|
||||
}
|
||||
|
||||
process.exit(result.valid ? 0 : 1);
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { validateComponent };
|
||||
222
skills/fluxwing-validator/validate-screen.js
Executable file
222
skills/fluxwing-validator/validate-screen.js
Executable file
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Screen validation using component validator + screen-specific checks.
|
||||
* Validates .uxm screen files with additional checks for rendered examples and composed components.
|
||||
*
|
||||
* Usage:
|
||||
* node validate-screen.js <screen.uxm> <schema.json>
|
||||
* node validate-screen.js <screen.uxm> <schema.json> --json
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 - Valid screen
|
||||
* 1 - Validation errors found
|
||||
* 2 - Missing dependencies or invalid arguments
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Import component validator
|
||||
const { validateComponent } = require('./validate-component.js');
|
||||
|
||||
/**
|
||||
* Validate a .uxm screen file with screen-specific checks
|
||||
* @param {string} uxmFilePath - Path to .uxm file
|
||||
* @param {string} schemaPath - Path to JSON schema file
|
||||
* @returns {Object} Validation result object
|
||||
*/
|
||||
function validateScreen(uxmFilePath, schemaPath) {
|
||||
// 1. Run standard component validation
|
||||
const result = validateComponent(uxmFilePath, schemaPath);
|
||||
|
||||
// If component validation failed completely, return early
|
||||
if (!fs.existsSync(uxmFilePath)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Load screen data for screen-specific checks
|
||||
let screen;
|
||||
try {
|
||||
const uxmContent = fs.readFileSync(uxmFilePath, 'utf8');
|
||||
screen = JSON.parse(uxmContent);
|
||||
} catch (error) {
|
||||
// Already handled by validateComponent
|
||||
return result;
|
||||
}
|
||||
|
||||
// 2. Screen-specific checks
|
||||
checkRenderedFile(uxmFilePath, result);
|
||||
checkComposedComponents(screen, uxmFilePath, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if .rendered.md file exists (recommended for screens)
|
||||
* @param {string} uxmFilePath - Path to .uxm file
|
||||
* @param {Object} result - Result object to populate
|
||||
*/
|
||||
function checkRenderedFile(uxmFilePath, result) {
|
||||
const renderedFilePath = uxmFilePath.replace('.uxm', '.rendered.md');
|
||||
|
||||
if (!fs.existsSync(renderedFilePath)) {
|
||||
result.warnings.push({
|
||||
path: ['screen'],
|
||||
message: `Rendered example file recommended for screens: ${renderedFilePath}`,
|
||||
type: 'missing_rendered',
|
||||
severity: 'info'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if components referenced in screen exist
|
||||
* @param {Object} screen - Parsed screen object
|
||||
* @param {string} uxmFilePath - Path to .uxm file
|
||||
* @param {Object} result - Result object to populate
|
||||
*/
|
||||
function checkComposedComponents(screen, uxmFilePath, result) {
|
||||
// Extract component references from screen
|
||||
const composedComponents = extractComponentReferences(screen);
|
||||
|
||||
if (composedComponents.length === 0) {
|
||||
return; // No composed components to check
|
||||
}
|
||||
|
||||
// Determine base path for component lookups
|
||||
const screenDir = path.dirname(uxmFilePath);
|
||||
const projectRoot = findProjectRoot(screenDir);
|
||||
const componentsDir = path.join(projectRoot, 'fluxwing', 'components');
|
||||
|
||||
for (const componentId of composedComponents) {
|
||||
const componentPath = path.join(componentsDir, `${componentId}.uxm`);
|
||||
|
||||
if (!fs.existsSync(componentPath)) {
|
||||
result.warnings.push({
|
||||
path: ['composed'],
|
||||
message: `Referenced component not found: ${componentId} (expected at ${componentPath})`,
|
||||
type: 'missing_component',
|
||||
severity: 'warning'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract component IDs referenced in screen
|
||||
* @param {Object} screen - Parsed screen object
|
||||
* @returns {Array<string>} Array of component IDs
|
||||
*/
|
||||
function extractComponentReferences(screen) {
|
||||
const componentIds = new Set();
|
||||
|
||||
// Check if screen extends another component
|
||||
if (screen.extends) {
|
||||
componentIds.add(screen.extends);
|
||||
}
|
||||
|
||||
// Check slots for component references
|
||||
if (screen.slots) {
|
||||
for (const slot of Object.values(screen.slots)) {
|
||||
if (slot.component) {
|
||||
componentIds.add(slot.component);
|
||||
}
|
||||
if (Array.isArray(slot.components)) {
|
||||
slot.components.forEach(c => componentIds.add(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check ASCII template content for component references (basic pattern matching)
|
||||
// Pattern: {{component:component-id}}
|
||||
const mdFilePath = screen.ascii?.templateFile;
|
||||
if (mdFilePath) {
|
||||
try {
|
||||
const screenDir = path.dirname(screen.id);
|
||||
const mdContent = fs.readFileSync(mdFilePath, 'utf8');
|
||||
const componentPattern = /\{\{component:([a-z0-9-]+)\}\}/g;
|
||||
let match;
|
||||
while ((match = componentPattern.exec(mdContent)) !== null) {
|
||||
componentIds.add(match[1]);
|
||||
}
|
||||
} catch (error) {
|
||||
// Template file not found - already reported by component validator
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(componentIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find project root directory (containing fluxwing/ directory)
|
||||
* @param {string} startDir - Starting directory
|
||||
* @returns {string} Project root path
|
||||
*/
|
||||
function findProjectRoot(startDir) {
|
||||
let currentDir = startDir;
|
||||
|
||||
while (currentDir !== path.dirname(currentDir)) {
|
||||
const fluxwingDir = path.join(currentDir, 'fluxwing');
|
||||
if (fs.existsSync(fluxwingDir)) {
|
||||
return currentDir;
|
||||
}
|
||||
currentDir = path.dirname(currentDir);
|
||||
}
|
||||
|
||||
// Fallback to current working directory
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 2) {
|
||||
console.error('Usage: node validate-screen.js <screen.uxm> <schema.json> [--json]');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const uxmFilePath = args[0];
|
||||
const schemaPath = args[1];
|
||||
const jsonOutput = args.includes('--json');
|
||||
|
||||
const result = validateScreen(uxmFilePath, schemaPath);
|
||||
|
||||
if (jsonOutput) {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} else {
|
||||
if (result.valid) {
|
||||
console.log(`✓ Valid: ${result.stats.id || 'screen'}`);
|
||||
console.log(` Type: ${result.stats.type || 'unknown'}`);
|
||||
console.log(` Version: ${result.stats.version || 'unknown'}`);
|
||||
console.log(` States: ${result.stats.states || 0}`);
|
||||
console.log(` Props: ${result.stats.props || 0}`);
|
||||
|
||||
if (result.warnings.length > 0) {
|
||||
console.log(`\n⚠ Warnings:`);
|
||||
result.warnings.forEach((warning, i) => {
|
||||
console.log(`\n Warning ${i + 1}: ${warning.message}`);
|
||||
if (warning.path.length > 0) {
|
||||
console.log(` Location: ${warning.path.join(' → ')}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error('✗ Validation Failed\n');
|
||||
result.errors.forEach((error, i) => {
|
||||
console.error(` Error ${i + 1}: ${error.message}`);
|
||||
if (error.path.length > 0) {
|
||||
console.error(` Location: ${error.path.join(' → ')}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (result.errors.length > 8) {
|
||||
console.error(`\n ... and ${result.errors.length - 8} more errors`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(result.valid ? 0 : 1);
|
||||
}
|
||||
|
||||
module.exports = { validateScreen };
|
||||
Reference in New Issue
Block a user