Files
dss/packages/dss-rules/bin/cli.js
DSS 9dbd56271e
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
feat: Enterprise DSS architecture implementation
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>
2025-12-11 09:41:36 -03:00

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);
});