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>
490 lines
13 KiB
JavaScript
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);
|
|
});
|