Files
dss/packages/dss-rules/bin/init.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

490 lines
13 KiB
JavaScript

#!/usr/bin/env node
/**
* DSS Project Initialization CLI
*
* Sets up a new project for DSS validation:
* - Creates ds.config.json
* - Sets up .dss/ folder with .gitignore
* - Configures package.json scripts
* - Optionally sets up CI workflow
*
* Usage:
* npx @dss/rules init
* npx @dss/rules init --ci gitea
* npx @dss/rules init --force
*/
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const PACKAGE_VERSION = require('../package.json').version;
// Template paths
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
// Default config template
const DEFAULT_CONFIG = {
"$schema": "https://dss.overbits.luz.uy/schemas/ds.config.json",
"project": {
"id": "",
"name": "",
"description": "Design system validation for this project"
},
"extends": {
"skin": "classic"
},
"validation": {
"rules": ["colors", "spacing", "typography", "components", "accessibility"],
"severity": {
"colors": "error",
"spacing": "warning",
"typography": "warning",
"components": "error",
"accessibility": "warning"
}
},
"overrides": {
"tokens": {}
}
};
// CI platform configurations
const CI_PLATFORMS = {
gitea: {
name: 'Gitea Actions',
template: 'gitea-workflow.yml',
destDir: '.gitea/workflows',
destFile: 'dss-validate.yml'
},
github: {
name: 'GitHub Actions',
template: 'github-workflow.yml',
destDir: '.github/workflows',
destFile: 'dss-validate.yml'
},
gitlab: {
name: 'GitLab CI',
template: 'gitlab-ci.yml',
destDir: '',
destFile: '.gitlab-ci.yml'
}
};
async function main() {
const args = process.argv.slice(2);
const options = parseArgs(args);
console.log('\n🎨 DSS Project Initialization\n');
console.log(`Version: ${PACKAGE_VERSION}`);
console.log('─'.repeat(40) + '\n');
const projectRoot = process.cwd();
// Check if already initialized
const configPath = path.join(projectRoot, 'ds.config.json');
if (fs.existsSync(configPath) && !options.force) {
console.log('⚠️ Project already initialized (ds.config.json exists)');
console.log(' Use --force to reinitialize\n');
process.exit(1);
}
// Interactive mode if not all options provided
const config = options.interactive
? await interactiveSetup(projectRoot)
: await autoSetup(projectRoot, options);
// 1. Create ds.config.json
console.log('📝 Creating ds.config.json...');
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
console.log(' ✓ Configuration file created\n');
// 2. Create .dss/ folder structure
console.log('📁 Setting up .dss/ folder...');
setupDssFolder(projectRoot);
console.log(' ✓ Local cache folder ready\n');
// 3. Update .gitignore
console.log('📋 Updating .gitignore...');
updateGitignore(projectRoot);
console.log(' ✓ .gitignore updated\n');
// 4. Add npm scripts
console.log('📦 Adding npm scripts...');
addNpmScripts(projectRoot);
console.log(' ✓ Package.json updated\n');
// 5. Setup CI if requested
if (options.ci) {
console.log(`🔧 Setting up ${CI_PLATFORMS[options.ci]?.name || options.ci} CI...`);
setupCI(projectRoot, options.ci);
console.log(' ✓ CI workflow created\n');
}
// Success message
console.log('─'.repeat(40));
console.log('\n✅ DSS initialization complete!\n');
console.log('Next steps:');
console.log(' 1. Review ds.config.json and customize rules');
console.log(' 2. Run: npx dss-rules validate');
console.log(' 3. Fix any violations found\n');
if (!options.ci) {
console.log('💡 Tip: Set up CI validation with:');
console.log(' npx @dss/rules init --ci gitea\n');
}
}
function parseArgs(args) {
const options = {
force: false,
ci: null,
interactive: true,
projectId: null,
projectName: null
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--force' || arg === '-f') {
options.force = true;
} else if (arg === '--ci') {
options.ci = args[++i] || 'gitea';
} else if (arg === '--yes' || arg === '-y') {
options.interactive = false;
} else if (arg === '--id') {
options.projectId = args[++i];
} else if (arg === '--name') {
options.projectName = args[++i];
} else if (arg === '--help' || arg === '-h') {
showHelp();
process.exit(0);
}
}
return options;
}
function showHelp() {
console.log(`
DSS Project Initialization
Usage:
npx @dss/rules init [options]
Options:
--force, -f Overwrite existing configuration
--ci <platform> Set up CI workflow (gitea, github, gitlab)
--yes, -y Skip interactive prompts, use defaults
--id <id> Project ID (default: directory name)
--name <name> Project display name
--help, -h Show this help message
Examples:
npx @dss/rules init
npx @dss/rules init --ci gitea
npx @dss/rules init -y --ci github
npx @dss/rules init --id my-app --name "My Application"
`);
}
async function interactiveSetup(projectRoot) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
try {
const dirName = path.basename(projectRoot);
const projectId = await question(`Project ID [${dirName}]: `) || dirName;
const projectName = await question(`Project Name [${projectId}]: `) || projectId;
const skin = await question('Base skin [classic]: ') || 'classic';
rl.close();
const config = { ...DEFAULT_CONFIG };
config.project.id = projectId;
config.project.name = projectName;
config.extends.skin = skin;
return config;
} catch (e) {
rl.close();
throw e;
}
}
async function autoSetup(projectRoot, options) {
const dirName = path.basename(projectRoot);
const config = { ...DEFAULT_CONFIG };
config.project.id = options.projectId || dirName;
config.project.name = options.projectName || config.project.id;
return config;
}
function setupDssFolder(projectRoot) {
const dssDir = path.join(projectRoot, '.dss');
const cacheDir = path.join(dssDir, 'cache');
// Create directories
if (!fs.existsSync(dssDir)) {
fs.mkdirSync(dssDir, { recursive: true });
}
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
// Create .gitignore in .dss/
const gitignorePath = path.join(dssDir, '.gitignore');
const gitignoreContent = `# DSS local cache - do not commit
*
!.gitignore
`;
fs.writeFileSync(gitignorePath, gitignoreContent);
// Create metadata.json
const metadataPath = path.join(dssDir, 'metadata.json');
const metadata = {
initialized_at: new Date().toISOString(),
rules_version: PACKAGE_VERSION,
last_updated: null
};
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
}
function updateGitignore(projectRoot) {
const gitignorePath = path.join(projectRoot, '.gitignore');
// Entries to add
const entries = [
'',
'# DSS local analysis cache',
'.dss/',
'!.dss/.gitignore'
];
let content = '';
if (fs.existsSync(gitignorePath)) {
content = fs.readFileSync(gitignorePath, 'utf-8');
// Check if already configured
if (content.includes('.dss/')) {
console.log(' (already configured)');
return;
}
}
// Append entries
const newContent = content + entries.join('\n') + '\n';
fs.writeFileSync(gitignorePath, newContent);
}
function addNpmScripts(projectRoot) {
const packagePath = path.join(projectRoot, 'package.json');
if (!fs.existsSync(packagePath)) {
console.log(' (no package.json found, skipping)');
return;
}
try {
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
// Add scripts
pkg.scripts = pkg.scripts || {};
if (!pkg.scripts['dss:validate']) {
pkg.scripts['dss:validate'] = 'dss-rules validate';
}
if (!pkg.scripts['dss:validate:ci']) {
pkg.scripts['dss:validate:ci'] = 'dss-rules validate --ci --strict --json > .dss/results.json';
}
if (!pkg.scripts['dss:baseline']) {
pkg.scripts['dss:baseline'] = 'dss-rules validate --baseline';
}
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n');
} catch (e) {
console.log(` ⚠️ Failed to update package.json: ${e.message}`);
}
}
function setupCI(projectRoot, platform) {
const ciConfig = CI_PLATFORMS[platform];
if (!ciConfig) {
console.log(` ⚠️ Unknown CI platform: ${platform}`);
console.log(` Supported: ${Object.keys(CI_PLATFORMS).join(', ')}`);
return;
}
// Read template
const templatePath = path.join(TEMPLATES_DIR, ciConfig.template);
if (!fs.existsSync(templatePath)) {
// Create a default template inline
const template = getDefaultCITemplate(platform);
writeCIFile(projectRoot, ciConfig, template);
return;
}
const template = fs.readFileSync(templatePath, 'utf-8');
writeCIFile(projectRoot, ciConfig, template);
}
function writeCIFile(projectRoot, ciConfig, content) {
const destDir = path.join(projectRoot, ciConfig.destDir);
const destPath = path.join(destDir, ciConfig.destFile);
// Create directory
if (ciConfig.destDir && !fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
fs.writeFileSync(destPath, content);
}
function getDefaultCITemplate(platform) {
if (platform === 'github') {
return `# DSS Design System Validation
name: DSS Validate
on:
push:
branches: [main, master, develop]
pull_request:
branches: [main, master]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Check for [dss-skip]
id: skip-check
run: |
if git log -1 --pretty=%B | grep -q '\\[dss-skip\\]'; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "⚠️ DSS validation skipped via [dss-skip] commit message"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Run DSS validation
if: steps.skip-check.outputs.skip != 'true'
run: npm run dss:validate:ci
- name: Upload metrics to dashboard
if: steps.skip-check.outputs.skip != 'true'
run: |
curl -X POST "\${DSS_DASHBOARD_URL}/api/metrics/upload" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer \${DSS_API_TOKEN}" \\
-d @.dss/results.json
env:
DSS_DASHBOARD_URL: \${{ secrets.DSS_DASHBOARD_URL }}
DSS_API_TOKEN: \${{ secrets.DSS_API_TOKEN }}
`;
}
if (platform === 'gitlab') {
return `# DSS Design System Validation
stages:
- validate
dss-validate:
stage: validate
image: node:20
script:
- npm ci
- |
if git log -1 --pretty=%B | grep -q '\\[dss-skip\\]'; then
echo "⚠️ DSS validation skipped via [dss-skip] commit message"
exit 0
fi
- npm run dss:validate:ci
- |
curl -X POST "\${DSS_DASHBOARD_URL}/api/metrics/upload" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer \${DSS_API_TOKEN}" \\
-d @.dss/results.json
only:
- main
- master
- develop
- merge_requests
`;
}
// Default to gitea template (most similar to the one in templates/)
return `# DSS Design System Validation
name: DSS Validate
on:
push:
branches: [main, master, develop]
pull_request:
branches: [main, master]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Check for [dss-skip]
id: skip-check
run: |
if git log -1 --pretty=%B | grep -q '\\[dss-skip\\]'; then
echo "skip=true" >> \$GITHUB_OUTPUT
echo "::warning::DSS validation skipped via [dss-skip] commit message"
else
echo "skip=false" >> \$GITHUB_OUTPUT
fi
- name: Run DSS validation
if: steps.skip-check.outputs.skip != 'true'
run: npm run dss:validate:ci
- name: Upload metrics to dashboard
if: steps.skip-check.outputs.skip != 'true' && always()
run: |
curl -X POST "\${DSS_DASHBOARD_URL}/api/metrics/upload" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer \${DSS_API_TOKEN}" \\
-d @.dss/results.json
env:
DSS_DASHBOARD_URL: \${{ secrets.DSS_DASHBOARD_URL }}
DSS_API_TOKEN: \${{ secrets.DSS_API_TOKEN }}
`;
}
// Run main
main().catch(err => {
console.error('\n❌ Initialization failed:', err.message);
process.exit(1);
});