@lifeonlars/prime-yggdrasil 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,288 @@
1
+ import { existsSync, mkdirSync, copyFileSync, writeFileSync, readFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import readline from 'readline';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ const rl = readline.createInterface({
10
+ input: process.stdin,
11
+ output: process.stdout
12
+ });
13
+
14
+ function question(query) {
15
+ return new Promise((resolve) => rl.question(query, resolve));
16
+ }
17
+
18
+ export async function initCommand() {
19
+ console.log(`
20
+ 🌳 Yggdrasil Agent Infrastructure Setup
21
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22
+
23
+ This will set up AI agents in your project to guide design
24
+ system usage and prevent drift.
25
+ `);
26
+
27
+ try {
28
+ // Detect project type
29
+ const projectType = detectProjectType();
30
+ console.log(`šŸ“¦ Detected project type: ${projectType}`);
31
+ console.log('');
32
+
33
+ // Ask user preferences
34
+ const copyAgents = await askYesNo('Copy agent documentation to .ai/yggdrasil/? (Recommended)', true);
35
+ const installESLint = await askYesNo('Add ESLint config reference? (Available in Phase 3)', false);
36
+ const addScripts = await askYesNo('Add npm scripts to package.json?', true);
37
+
38
+ console.log('');
39
+ console.log('šŸ“‹ Installation Plan:');
40
+ console.log('');
41
+ if (copyAgents) console.log(' āœ“ Copy 4 agent specifications to .ai/yggdrasil/');
42
+ if (installESLint) console.log(' āœ“ Add ESLint configuration (placeholder for Phase 3)');
43
+ if (addScripts) console.log(' āœ“ Add npm scripts for validation');
44
+ console.log('');
45
+
46
+ const proceed = await askYesNo('Proceed with installation?', true);
47
+ if (!proceed) {
48
+ console.log('āŒ Installation cancelled.');
49
+ rl.close();
50
+ return;
51
+ }
52
+
53
+ console.log('');
54
+ console.log('šŸš€ Installing...');
55
+ console.log('');
56
+
57
+ // Copy agents
58
+ if (copyAgents) {
59
+ await copyAgentFiles();
60
+ }
61
+
62
+ // Add ESLint config
63
+ if (installESLint) {
64
+ await createESLintConfig();
65
+ }
66
+
67
+ // Add scripts
68
+ if (addScripts) {
69
+ await addNpmScripts();
70
+ }
71
+
72
+ // Print success message
73
+ printSuccessMessage(copyAgents, installESLint);
74
+
75
+ rl.close();
76
+ } catch (error) {
77
+ console.error('āŒ Error during installation:', error.message);
78
+ rl.close();
79
+ process.exit(1);
80
+ }
81
+ }
82
+
83
+ function detectProjectType() {
84
+ const cwd = process.cwd();
85
+
86
+ if (existsSync(join(cwd, 'next.config.js')) || existsSync(join(cwd, 'next.config.mjs'))) {
87
+ return 'Next.js';
88
+ }
89
+ if (existsSync(join(cwd, 'vite.config.js')) || existsSync(join(cwd, 'vite.config.ts'))) {
90
+ return 'Vite';
91
+ }
92
+ if (existsSync(join(cwd, 'package.json'))) {
93
+ return 'React/Node.js';
94
+ }
95
+ return 'Unknown';
96
+ }
97
+
98
+ async function askYesNo(question, defaultYes = true) {
99
+ const prompt = `${question} ${defaultYes ? '[Y/n]' : '[y/N]'}: `;
100
+ const answer = await questionPromise(prompt);
101
+ const normalized = answer.toLowerCase().trim();
102
+
103
+ if (normalized === '') return defaultYes;
104
+ return normalized === 'y' || normalized === 'yes';
105
+ }
106
+
107
+ function questionPromise(query) {
108
+ return new Promise((resolve) => rl.question(query, resolve));
109
+ }
110
+
111
+ async function copyAgentFiles() {
112
+ const cwd = process.cwd();
113
+ const targetDir = join(cwd, '.ai', 'yggdrasil');
114
+ const sourceDir = join(__dirname, '../templates/.ai/yggdrasil');
115
+
116
+ // Create target directory
117
+ if (!existsSync(targetDir)) {
118
+ mkdirSync(targetDir, { recursive: true });
119
+ }
120
+
121
+ // Copy agent files from main .ai/agents directory
122
+ const mainAgentsDir = join(__dirname, '../../.ai/agents');
123
+ const agentFiles = [
124
+ 'block-composer.md',
125
+ 'primeflex-guard.md',
126
+ 'semantic-token-intent.md',
127
+ 'drift-validator.md'
128
+ ];
129
+
130
+ console.log(' šŸ“„ Copying agent specifications...');
131
+ for (const file of agentFiles) {
132
+ const source = join(mainAgentsDir, file);
133
+ const target = join(targetDir, file);
134
+
135
+ if (existsSync(source)) {
136
+ copyFileSync(source, target);
137
+ console.log(` āœ“ ${file}`);
138
+ } else {
139
+ console.log(` āš ļø ${file} not found (will be available after build)`);
140
+ }
141
+ }
142
+
143
+ // Copy README
144
+ const readmePath = join(sourceDir, 'README.md');
145
+ if (existsSync(readmePath)) {
146
+ copyFileSync(readmePath, join(targetDir, 'README.md'));
147
+ console.log(' āœ“ README.md');
148
+ }
149
+
150
+ // Copy PrimeFlex policy
151
+ const policySource = join(__dirname, '../../docs/PRIMEFLEX-POLICY.md');
152
+ if (existsSync(policySource)) {
153
+ copyFileSync(policySource, join(targetDir, 'PRIMEFLEX-POLICY.md'));
154
+ console.log(' āœ“ PRIMEFLEX-POLICY.md');
155
+ }
156
+
157
+ console.log('');
158
+ console.log(` āœ… Copied agents to ${targetDir}`);
159
+ console.log('');
160
+ }
161
+
162
+ async function createESLintConfig() {
163
+ const cwd = process.cwd();
164
+ const configPath = join(cwd, '.eslintrc.yggdrasil.js');
165
+
166
+ const configContent = `// Yggdrasil ESLint Configuration
167
+ // This will be functional in Phase 3 when the ESLint plugin is published
168
+
169
+ module.exports = {
170
+ extends: [
171
+ // Uncomment when @lifeonlars/eslint-plugin-yggdrasil is available (Phase 3)
172
+ // 'plugin:@lifeonlars/yggdrasil/recommended'
173
+ ],
174
+ rules: {
175
+ // Phase 3: ESLint rules will enforce:
176
+ // - No hardcoded colors (use semantic tokens)
177
+ // - No PrimeFlex on PrimeReact components
178
+ // - PrimeFlex allowlist (layout/spacing only)
179
+ // - No Tailwind classes
180
+ // - Valid spacing (4px grid)
181
+ // - Semantic tokens only
182
+ // - Consistent PrimeReact imports
183
+ }
184
+ };
185
+ `;
186
+
187
+ writeFileSync(configPath, configContent);
188
+ console.log(` āœ… Created ${configPath}`);
189
+ console.log(' āš ļø ESLint plugin will be available in Phase 3');
190
+ console.log('');
191
+ }
192
+
193
+ async function addNpmScripts() {
194
+ const cwd = process.cwd();
195
+ const packageJsonPath = join(cwd, 'package.json');
196
+
197
+ if (!existsSync(packageJsonPath)) {
198
+ console.log(' āš ļø package.json not found, skipping script installation');
199
+ return;
200
+ }
201
+
202
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
203
+
204
+ if (!packageJson.scripts) {
205
+ packageJson.scripts = {};
206
+ }
207
+
208
+ // Add scripts (placeholders for Phase 4)
209
+ const scriptsToAdd = {
210
+ 'yggdrasil:validate': 'echo "āš ļø Validation command available in Phase 4. Use ESLint for now."',
211
+ 'yggdrasil:audit': 'echo "āš ļø Audit command available in Phase 4. Use ESLint for now."'
212
+ };
213
+
214
+ let added = false;
215
+ for (const [key, value] of Object.entries(scriptsToAdd)) {
216
+ if (!packageJson.scripts[key]) {
217
+ packageJson.scripts[key] = value;
218
+ console.log(` āœ“ Added script: ${key}`);
219
+ added = true;
220
+ }
221
+ }
222
+
223
+ if (added) {
224
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
225
+ console.log('');
226
+ console.log(' āœ… Updated package.json');
227
+ } else {
228
+ console.log(' ā„¹ļø Scripts already exist, skipping');
229
+ }
230
+ console.log('');
231
+ }
232
+
233
+ function printSuccessMessage(copiedAgents, addedESLint) {
234
+ console.log('');
235
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
236
+ console.log('āœ… Yggdrasil Agent Infrastructure Installed!');
237
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
238
+ console.log('');
239
+
240
+ if (copiedAgents) {
241
+ console.log('šŸ“š Agent Documentation:');
242
+ console.log(' .ai/yggdrasil/block-composer.md - Composition planning');
243
+ console.log(' .ai/yggdrasil/primeflex-guard.md - Layout constraints');
244
+ console.log(' .ai/yggdrasil/semantic-token-intent.md - Token selection');
245
+ console.log(' .ai/yggdrasil/drift-validator.md - Policy enforcement');
246
+ console.log(' .ai/yggdrasil/PRIMEFLEX-POLICY.md - PrimeFlex usage rules');
247
+ console.log('');
248
+ }
249
+
250
+ console.log('šŸ¤– Using Agents with AI Tools:');
251
+ console.log('');
252
+ console.log(' Claude Code:');
253
+ console.log(' "Before implementing UI, read .ai/yggdrasil/block-composer.md');
254
+ console.log(' and suggest the appropriate PrimeReact components."');
255
+ console.log('');
256
+ console.log(' Cursor/Copilot:');
257
+ console.log(' Add .ai/yggdrasil/ to your AI context, then ask:');
258
+ console.log(' "Help me implement a user profile form following Yggdrasil agents."');
259
+ console.log('');
260
+
261
+ console.log('šŸ“– Next Steps:');
262
+ console.log('');
263
+ console.log(' 1. Read the agent documentation in .ai/yggdrasil/');
264
+ console.log(' 2. Reference agents when implementing UI features');
265
+ console.log(' 3. Phase 3: Install ESLint plugin for code-time validation');
266
+ console.log(' npm install --save-dev @lifeonlars/eslint-plugin-yggdrasil');
267
+ console.log(' 4. Phase 4: Use validation commands');
268
+ console.log(' npm run yggdrasil:validate');
269
+ console.log(' npm run yggdrasil:audit');
270
+ console.log('');
271
+
272
+ console.log('šŸ“‹ Agent Workflow:');
273
+ console.log('');
274
+ console.log(' Planning UI: Block Composer Agent');
275
+ console.log(' Choosing layout: PrimeFlex Guard Agent');
276
+ console.log(' Selecting colors: Semantic Token Intent Agent');
277
+ console.log(' Validating code: Drift Validator Agent');
278
+ console.log('');
279
+
280
+ console.log('šŸ”— Resources:');
281
+ console.log('');
282
+ console.log(' Documentation: https://github.com/lifeonlars/prime-yggdrasil');
283
+ console.log(' PrimeReact: https://primereact.org/');
284
+ console.log(' PrimeFlex: https://primeflex.org/');
285
+ console.log('');
286
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
287
+ console.log('');
288
+ }
@@ -0,0 +1,405 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+
4
+ /**
5
+ * Validate command - Report-only mode
6
+ *
7
+ * Scans the project for design system violations and reports them.
8
+ * Does NOT block builds - use for analysis and adoption phase.
9
+ *
10
+ * For autofix suggestions, use the audit command instead.
11
+ */
12
+
13
+ // Import validation logic from ESLint rules
14
+ // Since we can't directly import ESLint rules into CLI without ESLint dependency,
15
+ // we'll implement simplified validation logic here that shares the same concepts
16
+
17
+ const PRIMEREACT_COMPONENTS = [
18
+ 'AutoComplete', 'Calendar', 'CascadeSelect', 'Checkbox', 'Chips', 'ColorPicker',
19
+ 'Dropdown', 'Editor', 'FloatLabel', 'IconField', 'InputGroup', 'InputMask',
20
+ 'InputNumber', 'InputOtp', 'InputSwitch', 'InputText', 'InputTextarea',
21
+ 'Knob', 'Listbox', 'MultiSelect', 'Password', 'RadioButton', 'Rating',
22
+ 'SelectButton', 'Slider', 'TreeSelect', 'TriStateCheckbox', 'ToggleButton',
23
+ 'Button', 'SpeedDial', 'SplitButton',
24
+ 'DataTable', 'DataView', 'OrderList', 'OrganizationChart', 'Paginator',
25
+ 'PickList', 'Timeline', 'Tree', 'TreeTable', 'VirtualScroller',
26
+ 'Accordion', 'AccordionTab', 'Card', 'DeferredContent', 'Divider',
27
+ 'Fieldset', 'Panel', 'ScrollPanel', 'Splitter', 'SplitterPanel',
28
+ 'Stepper', 'StepperPanel', 'TabView', 'TabPanel', 'Toolbar',
29
+ 'ConfirmDialog', 'ConfirmPopup', 'Dialog', 'DynamicDialog', 'OverlayPanel',
30
+ 'Sidebar', 'Tooltip', 'FileUpload',
31
+ 'Breadcrumb', 'ContextMenu', 'Dock', 'Menu', 'Menubar', 'MegaMenu',
32
+ 'PanelMenu', 'Steps', 'TabMenu', 'TieredMenu', 'Chart',
33
+ 'Message', 'Messages', 'Toast',
34
+ 'Carousel', 'Galleria', 'Image',
35
+ 'Avatar', 'AvatarGroup', 'Badge', 'BlockUI', 'Chip', 'Inplace',
36
+ 'MeterGroup', 'ProgressBar', 'ProgressSpinner', 'ScrollTop', 'Skeleton',
37
+ 'Tag', 'Terminal'
38
+ ];
39
+
40
+ const UTILITY_PATTERNS = [
41
+ /^bg-[a-z]+-\d+$/, /^text-[a-z]+-\d+$/, /^border-[a-z]+-\d+$/,
42
+ /^rounded(-[a-z]+)?(-\d+)?$/, /^shadow(-\d+)?$/,
43
+ /^p-\d+$/, /^m-\d+$/, /^gap-\d+$/
44
+ ];
45
+
46
+ const TAILWIND_PATTERNS = [
47
+ /\w+-\[.+\]/, // Arbitrary values
48
+ /^(hover|focus|dark|sm|md|lg|xl):/, // Modifiers
49
+ /^space-(x|y)-\d+$/, /^ring(-\d+)?$/, /^backdrop-/
50
+ ];
51
+
52
+ const HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;
53
+ const RGB_COLOR = /rgba?\(/;
54
+ const HSL_COLOR = /hsla?\(/;
55
+
56
+ /**
57
+ * Validation rules
58
+ */
59
+ const RULES = {
60
+ 'no-utility-on-components': {
61
+ name: 'No PrimeFlex on PrimeReact Components',
62
+ severity: 'error',
63
+ check: (content, filePath) => {
64
+ const violations = [];
65
+ const lines = content.split('\n');
66
+
67
+ lines.forEach((line, index) => {
68
+ // Find JSX tags with className
69
+ PRIMEREACT_COMPONENTS.forEach(component => {
70
+ const regex = new RegExp(`<${component}[^>]*className=["'\`]([^"'\`]+)["'\`]`, 'g');
71
+ let match;
72
+
73
+ while ((match = regex.exec(line)) !== null) {
74
+ const classes = match[1].split(/\s+/);
75
+ const utilityClasses = classes.filter(c =>
76
+ UTILITY_PATTERNS.some(p => p.test(c))
77
+ );
78
+
79
+ if (utilityClasses.length > 0) {
80
+ violations.push({
81
+ line: index + 1,
82
+ column: match.index,
83
+ message: `PrimeFlex utility classes "${utilityClasses.join(', ')}" on <${component}>`,
84
+ suggestion: `Remove utility classes. The theme handles component styling.`
85
+ });
86
+ }
87
+ }
88
+ });
89
+ });
90
+
91
+ return violations;
92
+ }
93
+ },
94
+
95
+ 'no-tailwind': {
96
+ name: 'No Tailwind CSS Classes',
97
+ severity: 'error',
98
+ check: (content, filePath) => {
99
+ const violations = [];
100
+ const lines = content.split('\n');
101
+
102
+ lines.forEach((line, index) => {
103
+ // Find className attributes
104
+ const regex = /className=["'`]([^"'`]+)["'`]/g;
105
+ let match;
106
+
107
+ while ((match = regex.exec(line)) !== null) {
108
+ const classes = match[1].split(/\s+/);
109
+ const tailwindClasses = classes.filter(c =>
110
+ TAILWIND_PATTERNS.some(p => p.test(c))
111
+ );
112
+
113
+ if (tailwindClasses.length > 0) {
114
+ violations.push({
115
+ line: index + 1,
116
+ column: match.index,
117
+ message: `Tailwind classes "${tailwindClasses.join(', ')}" detected`,
118
+ suggestion: `Use PrimeFlex for layout or semantic tokens for design.`
119
+ });
120
+ }
121
+ }
122
+ });
123
+
124
+ return violations;
125
+ }
126
+ },
127
+
128
+ 'no-hardcoded-colors': {
129
+ name: 'No Hardcoded Colors',
130
+ severity: 'error',
131
+ check: (content, filePath) => {
132
+ const violations = [];
133
+ const lines = content.split('\n');
134
+
135
+ lines.forEach((line, index) => {
136
+ // Check for hex colors
137
+ const hexMatches = line.match(HEX_COLOR);
138
+ if (hexMatches) {
139
+ violations.push({
140
+ line: index + 1,
141
+ column: line.indexOf(hexMatches[0]),
142
+ message: `Hardcoded hex color "${hexMatches[0]}"`,
143
+ suggestion: `Use semantic token: var(--surface-neutral-primary) or var(--text-neutral-default)`
144
+ });
145
+ }
146
+
147
+ // Check for rgb/rgba
148
+ if (RGB_COLOR.test(line)) {
149
+ violations.push({
150
+ line: index + 1,
151
+ column: line.search(RGB_COLOR),
152
+ message: `Hardcoded RGB color detected`,
153
+ suggestion: `Use semantic token: var(--surface-neutral-primary)`
154
+ });
155
+ }
156
+
157
+ // Check for hsl/hsla
158
+ if (HSL_COLOR.test(line)) {
159
+ violations.push({
160
+ line: index + 1,
161
+ column: line.search(HSL_COLOR),
162
+ message: `Hardcoded HSL color detected`,
163
+ suggestion: `Use semantic token: var(--surface-neutral-primary)`
164
+ });
165
+ }
166
+ });
167
+
168
+ return violations;
169
+ }
170
+ },
171
+
172
+ 'semantic-tokens-only': {
173
+ name: 'Semantic Tokens Only',
174
+ severity: 'warning',
175
+ check: (content, filePath) => {
176
+ const violations = [];
177
+ const lines = content.split('\n');
178
+
179
+ lines.forEach((line, index) => {
180
+ // Check for foundation tokens
181
+ const foundationTokens = line.match(/var\(--(blue|green|red|yellow|gray|purple|pink|indigo|teal|orange|cyan|bluegray)-\d+\)/g);
182
+ if (foundationTokens) {
183
+ foundationTokens.forEach(token => {
184
+ violations.push({
185
+ line: index + 1,
186
+ column: line.indexOf(token),
187
+ message: `Foundation token "${token}" in app code`,
188
+ suggestion: `Use semantic token: var(--surface-brand-primary) or var(--text-neutral-default)`
189
+ });
190
+ });
191
+ }
192
+ });
193
+
194
+ return violations;
195
+ }
196
+ },
197
+
198
+ 'valid-spacing': {
199
+ name: 'Valid 4px Grid Spacing',
200
+ severity: 'warning',
201
+ check: (content, filePath) => {
202
+ const violations = [];
203
+ const lines = content.split('\n');
204
+
205
+ const validPx = [0, 4, 8, 12, 16, 20, 24, 28, 32];
206
+ const spacingProps = ['padding', 'margin', 'gap'];
207
+
208
+ lines.forEach((line, index) => {
209
+ spacingProps.forEach(prop => {
210
+ // Check for px values
211
+ const regex = new RegExp(`${prop}[^:]*:\\s*['"]?(\\d+)px`, 'g');
212
+ let match;
213
+
214
+ while ((match = regex.exec(line)) !== null) {
215
+ const value = parseInt(match[1], 10);
216
+ if (!validPx.includes(value) && value !== 1) {
217
+ const nearest = validPx.reduce((prev, curr) =>
218
+ Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev
219
+ );
220
+ violations.push({
221
+ line: index + 1,
222
+ column: match.index,
223
+ message: `Off-grid spacing: ${value}px`,
224
+ suggestion: `Use nearest 4px grid value: ${nearest}px (${nearest / 16}rem)`
225
+ });
226
+ }
227
+ }
228
+ });
229
+
230
+ // Check for invalid PrimeFlex classes (p-9, p-10, etc.)
231
+ const invalidClasses = line.match(/[pm][trblxy]?-([9-9]\d+)/g);
232
+ if (invalidClasses) {
233
+ violations.push({
234
+ line: index + 1,
235
+ column: line.indexOf(invalidClasses[0]),
236
+ message: `Invalid PrimeFlex spacing: ${invalidClasses.join(', ')}`,
237
+ suggestion: `Use p-0 through p-8 (0-32px in 4px increments)`
238
+ });
239
+ }
240
+ });
241
+
242
+ return violations;
243
+ }
244
+ }
245
+ };
246
+
247
+ /**
248
+ * Find all relevant files in directory
249
+ */
250
+ function findFiles(dir, extensions = ['.tsx', '.jsx', '.ts', '.js']) {
251
+ const files = [];
252
+
253
+ function walk(currentDir) {
254
+ try {
255
+ const entries = readdirSync(currentDir);
256
+
257
+ entries.forEach(entry => {
258
+ const fullPath = join(currentDir, entry);
259
+
260
+ // Skip node_modules, .git, dist, build
261
+ if (['node_modules', '.git', 'dist', 'build', '.next'].includes(entry)) {
262
+ return;
263
+ }
264
+
265
+ try {
266
+ const stat = statSync(fullPath);
267
+
268
+ if (stat.isDirectory()) {
269
+ walk(fullPath);
270
+ } else if (extensions.includes(extname(fullPath))) {
271
+ files.push(fullPath);
272
+ }
273
+ } catch (err) {
274
+ // Skip files we can't access
275
+ }
276
+ });
277
+ } catch (err) {
278
+ // Skip directories we can't access
279
+ }
280
+ }
281
+
282
+ walk(dir);
283
+ return files;
284
+ }
285
+
286
+ /**
287
+ * Run validation on a file
288
+ */
289
+ function validateFile(filePath, rules) {
290
+ try {
291
+ const content = readFileSync(filePath, 'utf8');
292
+ const results = {};
293
+
294
+ Object.entries(rules).forEach(([ruleId, rule]) => {
295
+ const violations = rule.check(content, filePath);
296
+ if (violations.length > 0) {
297
+ results[ruleId] = {
298
+ rule: rule.name,
299
+ severity: rule.severity,
300
+ violations
301
+ };
302
+ }
303
+ });
304
+
305
+ return results;
306
+ } catch (err) {
307
+ return { error: err.message };
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Format validation results
313
+ */
314
+ function formatResults(results, format = 'cli') {
315
+ if (format === 'json') {
316
+ return JSON.stringify(results, null, 2);
317
+ }
318
+
319
+ // CLI format
320
+ let output = '\n';
321
+ let totalViolations = 0;
322
+ let errorCount = 0;
323
+ let warningCount = 0;
324
+
325
+ Object.entries(results).forEach(([filePath, fileResults]) => {
326
+ if (fileResults.error) {
327
+ output += `āŒ Error reading ${filePath}: ${fileResults.error}\n`;
328
+ return;
329
+ }
330
+
331
+ const violations = Object.values(fileResults);
332
+ if (violations.length === 0) return;
333
+
334
+ output += `\nšŸ“„ ${filePath}\n`;
335
+
336
+ Object.entries(fileResults).forEach(([ruleId, result]) => {
337
+ result.violations.forEach(violation => {
338
+ totalViolations++;
339
+ const icon = result.severity === 'error' ? 'āŒ' : 'āš ļø';
340
+ if (result.severity === 'error') errorCount++;
341
+ else warningCount++;
342
+
343
+ output += ` ${icon} ${result.rule}\n`;
344
+ output += ` Line ${violation.line}, Col ${violation.column}\n`;
345
+ output += ` ${violation.message}\n`;
346
+ output += ` šŸ’” ${violation.suggestion}\n\n`;
347
+ });
348
+ });
349
+ });
350
+
351
+ if (totalViolations === 0) {
352
+ output += 'āœ… No violations found!\n\n';
353
+ } else {
354
+ output += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
355
+ output += `šŸ“Š Summary: ${totalViolations} violations found\n`;
356
+ output += ` ${errorCount} errors, ${warningCount} warnings\n\n`;
357
+ }
358
+
359
+ return output;
360
+ }
361
+
362
+ /**
363
+ * Main validate command
364
+ */
365
+ export async function validateCommand(options = {}) {
366
+ const cwd = options.cwd || process.cwd();
367
+ const format = options.format || 'cli';
368
+ const rulesFilter = options.rules ? options.rules.split(',') : Object.keys(RULES);
369
+
370
+ console.log(`
371
+ 🌳 Yggdrasil Design System Validation
372
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
373
+
374
+ šŸ“‚ Scanning directory: ${cwd}
375
+ šŸ” Active rules: ${rulesFilter.length}
376
+ `);
377
+
378
+ // Filter rules
379
+ const activeRules = {};
380
+ rulesFilter.forEach(ruleId => {
381
+ if (RULES[ruleId]) {
382
+ activeRules[ruleId] = RULES[ruleId];
383
+ }
384
+ });
385
+
386
+ // Find files
387
+ const files = findFiles(cwd);
388
+ console.log(`šŸ“ Found ${files.length} files to check\n`);
389
+
390
+ // Validate files
391
+ const results = {};
392
+ files.forEach(file => {
393
+ const fileResults = validateFile(file, activeRules);
394
+ if (Object.keys(fileResults).length > 0) {
395
+ results[file] = fileResults;
396
+ }
397
+ });
398
+
399
+ // Output results
400
+ const output = formatResults(results, format);
401
+ console.log(output);
402
+
403
+ // Return results for programmatic use
404
+ return results;
405
+ }