@lifeonlars/prime-yggdrasil 0.2.6 ā 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.
- package/.ai/agents/accessibility.md +581 -0
- package/.ai/agents/block-composer.md +909 -0
- package/.ai/agents/drift-validator.md +784 -0
- package/.ai/agents/interaction-patterns.md +465 -0
- package/.ai/agents/primeflex-guard.md +815 -0
- package/.ai/agents/semantic-token-intent.md +739 -0
- package/README.md +139 -12
- package/cli/bin/yggdrasil.js +134 -0
- package/cli/commands/audit.js +425 -0
- package/cli/commands/init.js +288 -0
- package/cli/commands/validate.js +405 -0
- package/cli/templates/.ai/yggdrasil/README.md +308 -0
- package/docs/AESTHETICS.md +168 -0
- package/docs/PRIMEFLEX-POLICY.md +737 -0
- package/package.json +6 -1
- package/docs/Fixes.md +0 -258
- package/docs/archive/README.md +0 -27
- package/docs/archive/SEMANTIC-MIGRATION-PLAN.md +0 -177
- package/docs/archive/YGGDRASIL_THEME.md +0 -264
- package/docs/archive/agentic_policy.md +0 -216
- package/docs/contrast-report.md +0 -9
|
@@ -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
|
+
}
|