Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
Complete implementation of enterprise design system validation: Phase 1 - @dss/rules npm package: - CLI with validate and init commands - 16 rules across 5 categories (colors, spacing, typography, components, a11y) - dss-ignore support (inline and next-line) - Break-glass [dss-skip] for emergency merges - CI workflow templates (Gitea, GitHub, GitLab) Phase 2 - Metrics dashboard: - FastAPI metrics API with SQLite storage - Portfolio-wide metrics aggregation - Project drill-down with file:line:column violations - Trend charts and history tracking Phase 3 - Local analysis cache: - LocalAnalysisCache for offline-capable validation - Mode detection (LOCAL/REMOTE/CI) - Stale cache warnings with recommendations Phase 4 - Project onboarding: - dss-init command for project setup - Creates ds.config.json, .dss/ folder structure - Updates .gitignore and package.json scripts - Optional CI workflow setup Architecture decisions: - No commit-back: CI uploads to dashboard, not git - Three-tier: Dashboard (read-only) → CI (authoritative) → Local (advisory) - Pull-based rules via npm for version control 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
362 lines
10 KiB
JavaScript
362 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* DSS Rules CLI
|
|
*
|
|
* Command-line tool for validating files against DSS design system rules.
|
|
* Used by CI pipelines, pre-commit hooks, and local development.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { glob } = require('glob');
|
|
const rules = require('../lib/index');
|
|
|
|
// ANSI colors
|
|
const c = {
|
|
red: '\x1b[31m',
|
|
yellow: '\x1b[33m',
|
|
green: '\x1b[32m',
|
|
blue: '\x1b[34m',
|
|
cyan: '\x1b[36m',
|
|
dim: '\x1b[2m',
|
|
reset: '\x1b[0m',
|
|
bold: '\x1b[1m'
|
|
};
|
|
|
|
/**
|
|
* Parse command line arguments
|
|
*/
|
|
function parseArgs(args) {
|
|
const options = {
|
|
files: [],
|
|
json: false,
|
|
baseline: null,
|
|
strict: false,
|
|
quiet: false,
|
|
help: false,
|
|
selfTest: false,
|
|
version: false,
|
|
ciMode: false,
|
|
fetchBaseline: null
|
|
};
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (arg === '--json') options.json = true;
|
|
else if (arg === '--strict') options.strict = true;
|
|
else if (arg === '--quiet' || arg === '-q') options.quiet = true;
|
|
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
else if (arg === '--self-test') options.selfTest = true;
|
|
else if (arg === '--version' || arg === '-v') options.version = true;
|
|
else if (arg === '--ci') options.ciMode = true;
|
|
else if (arg === '--baseline' && args[i + 1]) {
|
|
options.baseline = args[++i];
|
|
}
|
|
else if (arg === '--fetch-baseline' && args[i + 1]) {
|
|
options.fetchBaseline = args[++i];
|
|
}
|
|
else if (!arg.startsWith('-')) {
|
|
options.files.push(arg);
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
/**
|
|
* Print help message
|
|
*/
|
|
function printHelp() {
|
|
console.log(`
|
|
${c.bold}@dss/rules${c.reset} - Design System Rules Validator
|
|
|
|
${c.bold}Usage:${c.reset}
|
|
dss-rules <command> [options]
|
|
|
|
${c.bold}Commands:${c.reset}
|
|
init Initialize DSS in a new project
|
|
validate Validate files against design system rules (default)
|
|
|
|
${c.bold}Validate Options:${c.reset}
|
|
-h, --help Show this help message
|
|
-v, --version Show version
|
|
--json Output results as JSON
|
|
--quiet, -q Minimal output (errors only)
|
|
--strict Treat warnings as errors
|
|
--ci CI mode (auto-detects baseline, version drift warnings)
|
|
--baseline <file> Compare against baseline JSON to show only new violations
|
|
--fetch-baseline <url> Fetch baseline from dashboard API
|
|
--self-test Verify rules package installation
|
|
|
|
${c.bold}Init Options:${c.reset}
|
|
--force, -f Overwrite existing configuration
|
|
--ci <platform> Set up CI workflow (gitea, github, gitlab)
|
|
--yes, -y Skip interactive prompts, use defaults
|
|
|
|
${c.bold}Examples:${c.reset}
|
|
dss-rules init # Initialize new project
|
|
dss-rules init --ci github # Initialize with GitHub Actions
|
|
dss-rules validate src/**/*.tsx # Validate files
|
|
dss-rules --ci --strict src/ # CI mode validation
|
|
dss-rules --json src/ > report.json # JSON output
|
|
|
|
${c.bold}Ignore Comments:${c.reset}
|
|
// dss-ignore Ignore current line
|
|
// dss-ignore-next-line Ignore next line
|
|
/* dss-ignore */ Block ignore (CSS)
|
|
|
|
${c.bold}Skip CI Validation:${c.reset}
|
|
git commit -m "fix: hotfix [dss-skip]"
|
|
|
|
${c.bold}Exit Codes:${c.reset}
|
|
0 All checks passed
|
|
1 Validation errors found
|
|
2 Configuration error
|
|
`);
|
|
}
|
|
|
|
/**
|
|
* Print version info
|
|
*/
|
|
function printVersion() {
|
|
console.log(`@dss/rules v${rules.getVersion()}`);
|
|
const config = rules.getCIConfig();
|
|
console.log(` ${config.blockingRules.length} blocking rules`);
|
|
console.log(` ${config.advisoryRules.length} advisory rules`);
|
|
}
|
|
|
|
/**
|
|
* Run self-test
|
|
*/
|
|
function selfTest() {
|
|
console.log(`${c.bold}Running @dss/rules self-test...${c.reset}\n`);
|
|
|
|
const allRules = rules.loadRules();
|
|
let passed = true;
|
|
let totalRules = 0;
|
|
|
|
for (const [category, ruleSet] of Object.entries(allRules)) {
|
|
if (!ruleSet) {
|
|
console.log(`${c.red}✗${c.reset} ${category}: Failed to load`);
|
|
passed = false;
|
|
continue;
|
|
}
|
|
const count = ruleSet.rules?.length || 0;
|
|
totalRules += count;
|
|
console.log(`${c.green}✓${c.reset} ${category}: ${count} rules (v${ruleSet.version})`);
|
|
}
|
|
|
|
const config = rules.getCIConfig();
|
|
console.log(`\n${c.bold}Summary:${c.reset}`);
|
|
console.log(` Package version: ${config.version}`);
|
|
console.log(` Total rules: ${totalRules}`);
|
|
console.log(` Blocking (error): ${config.blockingRules.length}`);
|
|
console.log(` Advisory (warning): ${config.advisoryRules.length}`);
|
|
|
|
if (passed) {
|
|
console.log(`\n${c.green}${c.bold}Self-test passed!${c.reset}`);
|
|
process.exit(0);
|
|
} else {
|
|
console.log(`\n${c.red}${c.bold}Self-test failed!${c.reset}`);
|
|
process.exit(2);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expand glob patterns to file list
|
|
*/
|
|
async function expandGlobs(patterns) {
|
|
const files = [];
|
|
for (const pattern of patterns) {
|
|
if (pattern.includes('*')) {
|
|
const matches = await glob(pattern, { nodir: true });
|
|
files.push(...matches);
|
|
} else if (fs.existsSync(pattern)) {
|
|
const stat = fs.statSync(pattern);
|
|
if (stat.isDirectory()) {
|
|
const dirFiles = await glob(`${pattern}/**/*.{js,jsx,ts,tsx,css,scss,vue,svelte}`, { nodir: true });
|
|
files.push(...dirFiles);
|
|
} else {
|
|
files.push(pattern);
|
|
}
|
|
}
|
|
}
|
|
return [...new Set(files)];
|
|
}
|
|
|
|
/**
|
|
* Print validation results
|
|
*/
|
|
function printResults(results, options) {
|
|
if (!options.quiet) {
|
|
console.log(`\n${c.bold}=== DSS Rules Validation ===${c.reset}`);
|
|
console.log(`${c.dim}Rules version: ${results.rulesVersion}${c.reset}\n`);
|
|
}
|
|
|
|
for (const fileResult of results.fileResults) {
|
|
if (fileResult.errors.length === 0 && fileResult.warnings.length === 0) {
|
|
if (!options.quiet) {
|
|
console.log(`${c.green}✓${c.reset} ${fileResult.file}`);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const icon = fileResult.passed ? c.yellow + '⚠' : c.red + '✗';
|
|
console.log(`${icon}${c.reset} ${fileResult.file}`);
|
|
|
|
for (const error of fileResult.errors) {
|
|
console.log(` ${c.red}ERROR${c.reset} [${error.rule}] ${error.line}:${error.column}`);
|
|
console.log(` ${error.message}`);
|
|
console.log(` ${c.dim}Found: ${c.yellow}${error.match}${c.reset}`);
|
|
}
|
|
|
|
for (const warning of fileResult.warnings) {
|
|
if (!options.quiet) {
|
|
console.log(` ${c.yellow}WARN${c.reset} [${warning.rule}] ${warning.line}:${warning.column}`);
|
|
console.log(` ${warning.message}`);
|
|
}
|
|
}
|
|
|
|
if (fileResult.ignored.length > 0 && !options.quiet) {
|
|
console.log(` ${c.dim}(${fileResult.ignored.length} ignored)${c.reset}`);
|
|
}
|
|
}
|
|
|
|
// Summary
|
|
console.log(`\n${c.bold}=== Summary ===${c.reset}`);
|
|
console.log(`Files: ${results.passedFiles}/${results.totalFiles} passed`);
|
|
console.log(`Errors: ${c.red}${results.totalErrors}${c.reset}`);
|
|
console.log(`Warnings: ${c.yellow}${results.totalWarnings}${c.reset}`);
|
|
if (results.totalIgnored > 0) {
|
|
console.log(`Ignored: ${c.dim}${results.totalIgnored}${c.reset}`);
|
|
}
|
|
|
|
// New violations if baseline comparison
|
|
if (results.newErrors !== undefined) {
|
|
console.log(`\n${c.bold}New violations:${c.reset} ${results.newErrors.length} errors, ${results.newWarnings.length} warnings`);
|
|
console.log(`${c.dim}Existing:${c.reset} ${results.existingErrors.length} errors, ${results.existingWarnings.length} warnings`);
|
|
}
|
|
|
|
// Final status
|
|
if (results.totalErrors > 0) {
|
|
console.log(`\n${c.red}${c.bold}Validation failed!${c.reset}`);
|
|
} else if (results.totalWarnings > 0 && options.strict) {
|
|
console.log(`\n${c.yellow}${c.bold}Validation failed (strict mode)!${c.reset}`);
|
|
} else {
|
|
console.log(`\n${c.green}${c.bold}Validation passed!${c.reset}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for version drift and warn
|
|
*/
|
|
async function checkVersionDrift(options) {
|
|
if (!options.ciMode) return;
|
|
|
|
try {
|
|
// Check if there's a newer version available
|
|
const currentVersion = rules.getVersion();
|
|
// In real implementation, would check npm registry
|
|
// For now, just show the current version
|
|
console.log(`${c.cyan}[CI]${c.reset} Using @dss/rules v${currentVersion}`);
|
|
} catch (e) {
|
|
// Ignore version check errors
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load baseline for comparison
|
|
*/
|
|
function loadBaseline(baselinePath) {
|
|
if (!baselinePath) return null;
|
|
|
|
try {
|
|
if (fs.existsSync(baselinePath)) {
|
|
return JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
}
|
|
} catch (e) {
|
|
console.error(`${c.yellow}Warning: Could not load baseline: ${e.message}${c.reset}`);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Main entry point
|
|
*/
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
|
|
// Check for init command first
|
|
if (args[0] === 'init') {
|
|
// Delegate to init script
|
|
require('./init');
|
|
return;
|
|
}
|
|
|
|
// Check for validate command (default)
|
|
const validateArgs = args[0] === 'validate' ? args.slice(1) : args;
|
|
const options = parseArgs(validateArgs);
|
|
|
|
if (options.help) {
|
|
printHelp();
|
|
process.exit(0);
|
|
}
|
|
|
|
if (options.version) {
|
|
printVersion();
|
|
process.exit(0);
|
|
}
|
|
|
|
if (options.selfTest) {
|
|
selfTest();
|
|
return;
|
|
}
|
|
|
|
if (options.files.length === 0) {
|
|
console.error(`${c.red}Error: No files specified${c.reset}`);
|
|
console.log('Run with --help for usage information');
|
|
process.exit(2);
|
|
}
|
|
|
|
// Check version drift in CI mode
|
|
await checkVersionDrift(options);
|
|
|
|
// Expand globs
|
|
const files = await expandGlobs(options.files);
|
|
|
|
if (files.length === 0) {
|
|
console.error(`${c.yellow}Warning: No files matched the patterns${c.reset}`);
|
|
process.exit(0);
|
|
}
|
|
|
|
// Run validation
|
|
let results = rules.validateFiles(files);
|
|
|
|
// Compare with baseline if provided
|
|
const baseline = loadBaseline(options.baseline);
|
|
if (baseline) {
|
|
results = rules.compareWithBaseline(results, baseline);
|
|
}
|
|
|
|
// Output
|
|
if (options.json) {
|
|
console.log(JSON.stringify(results, null, 2));
|
|
} else {
|
|
printResults(results, options);
|
|
}
|
|
|
|
// Exit code
|
|
if (results.totalErrors > 0) {
|
|
process.exit(1);
|
|
}
|
|
if (options.strict && results.totalWarnings > 0) {
|
|
process.exit(1);
|
|
}
|
|
process.exit(0);
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error(`${c.red}Error: ${err.message}${c.reset}`);
|
|
process.exit(2);
|
|
});
|