medesign 1.2.0 → 1.4.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/bin/medesign.js CHANGED
@@ -1,200 +1,691 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * ME-DESIGN CLI — Autonomous Design Engineering Toolkit & Linter
5
- *
6
- * Commands:
7
- * medesign lint <file> [--format text|json|sarif] [--preset <name>] [--config <path>]
8
- * medesign audit <file> [--json]
9
- * medesign tokens [--preset <name>] [--format tailwind|dtcg|css|all] [--out <dir>]
10
- * medesign presets [list | get <name>]
11
- * medesign test [--browser]
12
- * medesign doctor
13
- */
14
-
15
- const fs = require('fs');
16
- const path = require('path');
17
- const { spawnSync } = require('child_process');
18
-
19
- const rootDir = path.join(__dirname, '..');
20
- const args = process.argv.slice(2);
21
- const command = args[0] || 'help';
22
-
23
- const ANSI_RESET = '\x1b[0m';
24
- const ANSI_BOLD = '\x1b[1m';
25
- const ANSI_CYAN = '\x1b[36m';
26
- const ANSI_GREEN = '\x1b[32m';
27
- const ANSI_YELLOW = '\x1b[33m';
28
- const ANSI_RED = '\x1b[31m';
29
- const ANSI_GRAY = '\x1b[90m';
30
-
31
- function printHelp() {
32
- console.log(`
33
- ${ANSI_BOLD}ME-DESIGN CLI v1.2.0${ANSI_RESET}
34
- Autonomous Design Engineering Toolkit & Automated Linter for AI Coding Assistants
35
-
36
- ${ANSI_BOLD}USAGE:${ANSI_RESET}
37
- medesign <command> [options]
38
-
39
- ${ANSI_BOLD}COMMANDS:${ANSI_RESET}
40
- ${ANSI_CYAN}lint${ANSI_RESET} <file> Lint HTML/CSS/JSX file with static AST & semantic rules
41
- Options: --format <text|json|sarif>
42
- --preset <apple|tesla|metha>
43
- --config <path>
44
-
45
- ${ANSI_CYAN}audit${ANSI_RESET} <file> Run headless Chrome DevTools Protocol verification matrix
46
- (Checks computed contrast, tab focus outline, 44px hit-targets across viewports)
47
- Options: --json
48
-
49
- ${ANSI_CYAN}tokens${ANSI_RESET} [export] Export preset design tokens to Tailwind v4 @theme, W3C DTCG, or CSS
50
- Options: --preset <apple|tesla|metha|all>
51
- --format <tailwind|dtcg|css|all>
52
- --out <directory>
53
-
54
- ${ANSI_CYAN}presets${ANSI_RESET} [list|get] Inspect available design system presets
55
- Usage: medesign presets list
56
- medesign presets get <preset-name>
57
-
58
- ${ANSI_CYAN}test${ANSI_RESET} [options] Run deterministic test suite across fixtures
59
- Options: --browser (runs both static AST and headless browser matrix)
60
-
61
- ${ANSI_CYAN}doctor${ANSI_RESET} Verify local environment, Node.js, and Chrome/CDP availability
62
-
63
- ${ANSI_CYAN}help${ANSI_RESET} Display this help message
64
- `);
65
- }
66
-
67
- switch (command) {
68
- case 'lint': {
69
- const linterScript = path.join(rootDir, 'tests', 'linter.js');
70
- const child = spawnSync(process.execPath, [linterScript, ...args.slice(1)], {
71
- stdio: 'inherit',
72
- cwd: process.cwd()
73
- });
74
- process.exit(child.status ?? 0);
75
- }
76
-
77
- case 'audit': {
78
- const auditScript = path.join(rootDir, 'tests', 'browser-audit.js');
79
- const child = spawnSync(process.execPath, [auditScript, ...args.slice(1)], {
80
- stdio: 'inherit',
81
- cwd: process.cwd()
82
- });
83
- process.exit(child.status ?? 0);
84
- }
85
-
86
- case 'tokens': {
87
- const exportScript = path.join(rootDir, 'scripts', 'export-tokens.js');
88
- // Filter out 'export' sub-command word if passed (e.g. medesign tokens export)
89
- const exportArgs = args.slice(1).filter(a => a !== 'export');
90
- const child = spawnSync(process.execPath, [exportScript, ...exportArgs], {
91
- stdio: 'inherit',
92
- cwd: process.cwd()
93
- });
94
- process.exit(child.status ?? 0);
95
- }
96
-
97
- case 'presets': {
98
- const sub = args[1] || 'list';
99
- const presetsDir = path.join(rootDir, 'presets');
100
- if (sub === 'list') {
101
- console.log(`\n${ANSI_BOLD}AVAILABLE ME-DESIGN PRESETS:${ANSI_RESET}\n`);
102
- const folders = fs.readdirSync(presetsDir, { withFileTypes: true })
103
- .filter(d => d.isDirectory())
104
- .map(d => d.name);
105
- folders.forEach(f => {
106
- const pJson = path.join(presetsDir, f, 'preset.json');
107
- if (fs.existsSync(pJson)) {
108
- const data = JSON.parse(fs.readFileSync(pJson, 'utf8'));
109
- console.log(` ${ANSI_CYAN}${data.id.padEnd(10)}${ANSI_RESET} ${data.name}`);
110
- console.log(` ${ANSI_GRAY}${data.archetype}${ANSI_RESET}`);
111
- }
112
- });
113
- console.log('');
114
- } else if (sub === 'get') {
115
- const name = args[2];
116
- if (!name) {
117
- console.error('Error: Please specify preset name (e.g. medesign presets get apple)');
118
- process.exit(1);
119
- }
120
- const pJson = path.join(presetsDir, name.toLowerCase(), 'preset.json');
121
- if (!fs.existsSync(pJson)) {
122
- console.error(`Error: Preset '${name}' not found.`);
123
- process.exit(1);
124
- }
125
- console.log(fs.readFileSync(pJson, 'utf8'));
126
- }
127
- break;
128
- }
129
-
130
- case 'test': {
131
- const runnerScript = path.join(rootDir, 'tests', 'runner.js');
132
- const child = spawnSync(process.execPath, [runnerScript, ...args.slice(1)], {
133
- stdio: 'inherit',
134
- cwd: process.cwd()
135
- });
136
- process.exit(child.status ?? 0);
137
- }
138
-
139
- case 'doctor': {
140
- console.log(`\n${ANSI_BOLD}ME-DESIGN System Health & Environment Doctor${ANSI_RESET}`);
141
- console.log(`${ANSI_GRAY}────────────────────────────────────────${ANSI_RESET}\n`);
142
-
143
- // 1. Node.js Version Check
144
- const nodeVer = process.version;
145
- const major = parseInt(nodeVer.slice(1).split('.')[0], 10);
146
- const nodeOk = major >= 18;
147
- console.log(`Node.js Runtime: ${nodeOk ? ANSI_GREEN + nodeVer + ' (Supported)' : ANSI_RED + nodeVer + ' (Requires >= 18.0.0)'}${ANSI_RESET}`);
148
-
149
- const chromeCandidates = [
150
- process.env.CHROME_PATH,
151
- process.env.CHROME_BIN,
152
- 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
153
- 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
154
- 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
155
- path.join(process.env.LOCALAPPDATA || '', 'ms-playwright', 'chromium-1248', 'chrome-win64', 'chrome.exe'),
156
- '/usr/bin/google-chrome',
157
- '/usr/bin/google-chrome-stable',
158
- '/usr/bin/chromium',
159
- '/usr/bin/chromium-browser',
160
- '/snap/bin/chromium',
161
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
162
- '/Applications/Chromium.app/Contents/MacOS/Chromium'
163
- ].filter(Boolean);
164
-
165
- let foundChrome = null;
166
- for (const c of chromeCandidates) {
167
- if (fs.existsSync(c)) {
168
- foundChrome = c;
169
- break;
170
- }
171
- }
172
- console.log(`Headless Browser: ${foundChrome ? ANSI_GREEN + 'Found (' + path.basename(foundChrome) + ')' : ANSI_YELLOW + 'Not found in default paths'}${ANSI_RESET}`);
173
- if (foundChrome) {
174
- console.log(` ${ANSI_GRAY}${foundChrome}${ANSI_RESET}`);
175
- }
176
-
177
- // 3. Presets Check
178
- const presetsDir = path.join(rootDir, 'presets');
179
- const presets = fs.readdirSync(presetsDir, { withFileTypes: true })
180
- .filter(d => d.isDirectory() && fs.existsSync(path.join(presetsDir, d.name, 'preset.json')));
181
- console.log(`Installed Presets: ${ANSI_GREEN}${presets.length} valid presets (${presets.map(p => p.name).join(', ')})${ANSI_RESET}`);
182
-
183
- // 4. Test Suite Fixtures
184
- const fixturesDir = path.join(rootDir, 'tests', 'fixtures');
185
- const validCount = fs.existsSync(path.join(fixturesDir, 'valid')) ? fs.readdirSync(path.join(fixturesDir, 'valid')).length : 0;
186
- const invalidCount = fs.existsSync(path.join(fixturesDir, 'invalid')) ? fs.readdirSync(path.join(fixturesDir, 'invalid')).length : 0;
187
- console.log(`Test Fixtures: ${ANSI_GREEN}${validCount} valid, ${invalidCount} invalid test fixtures${ANSI_RESET}\n`);
188
-
189
- console.log(`${ANSI_GRAY}────────────────────────────────────────${ANSI_RESET}`);
190
- console.log(`${ANSI_GREEN}${ANSI_BOLD}STATUS: ALL SYSTEMS OPERATIONAL${ANSI_RESET}\n`);
191
- break;
192
- }
193
-
194
- case 'help':
195
- case '--help':
196
- case '-h':
197
- default:
198
- printHelp();
199
- break;
200
- }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ME-DESIGN CLI — Autonomous Design Engineering Toolkit & Linter
5
+ *
6
+ * Commands:
7
+ * medesign lint <file> [--format text|json|sarif] [--preset <name>] [--config <path>]
8
+ * medesign audit <file> [--json]
9
+ * medesign search <query> [--json]
10
+ * polish <file>
11
+ * bolder <file>
12
+ * quiet <file>
13
+ * medesign tokens [--preset <name>] [--format tailwind|dtcg|css|all] [--out <dir>]
14
+ * medesign presets [list | get <name>]
15
+ * medesign test [--browser]
16
+ * medesign doctor
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { spawnSync } = require('child_process');
22
+
23
+ const rootDir = path.join(__dirname, '..');
24
+ const args = process.argv.slice(2);
25
+ const command = args[0] || 'help';
26
+
27
+ const ANSI_RESET = '\x1b[0m';
28
+ const ANSI_BOLD = '\x1b[1m';
29
+ const ANSI_CYAN = '\x1b[36m';
30
+ const ANSI_GREEN = '\x1b[32m';
31
+ const ANSI_YELLOW = '\x1b[33m';
32
+ const ANSI_RED = '\x1b[31m';
33
+ const ANSI_GRAY = '\x1b[90m';
34
+
35
+ function printHelp() {
36
+ console.log(`
37
+ ${ANSI_BOLD}ME-DESIGN CLI v1.4.0${ANSI_RESET}
38
+ Autonomous Design Engineering Toolkit & Automated Linter for AI Coding Assistants
39
+
40
+ ${ANSI_BOLD}USAGE:${ANSI_RESET}
41
+ medesign <command> [options]
42
+
43
+ ${ANSI_BOLD}COMMANDS:${ANSI_RESET}
44
+ ${ANSI_CYAN}search${ANSI_RESET} <query> Search 38 Archetypes & presets with discovery engine
45
+ Options: --json
46
+
47
+ ${ANSI_CYAN}polish${ANSI_RESET} <file> Self-healing repair for Hard Invariants (MD005, MD104, em-dash)
48
+
49
+ ${ANSI_CYAN}bolder${ANSI_RESET} <file> Boost visual density, font weights, and border contrast
50
+
51
+ ${ANSI_CYAN}quiet${ANSI_RESET} <file> Softens visual density, font weights, and border treatments
52
+
53
+ ${ANSI_CYAN}lint${ANSI_RESET} <file> Lint HTML/CSS/JSX file with static AST & semantic rules
54
+ Options: --format <text|json|sarif>
55
+ --preset <apple|tesla|metha>
56
+ --config <path>
57
+
58
+ ${ANSI_CYAN}audit${ANSI_RESET} <file> Run headless Chrome DevTools Protocol verification matrix
59
+ (Checks computed contrast, tab focus outline, 44px hit-targets across viewports)
60
+ Options: --json
61
+
62
+ ${ANSI_CYAN}tokens${ANSI_RESET} [export] Export preset design tokens to Tailwind v4 @theme, W3C DTCG, or CSS
63
+ Options: --preset <apple|tesla|metha|all>
64
+ --format <tailwind|dtcg|css|all>
65
+ --out <directory>
66
+
67
+ ${ANSI_CYAN}presets${ANSI_RESET} [list|get] Inspect available design system presets
68
+ Usage: medesign presets list
69
+ medesign presets get <preset-name>
70
+
71
+ ${ANSI_CYAN}test${ANSI_RESET} [options] Run deterministic test suite across fixtures
72
+ Options: --browser (runs both static AST and headless browser matrix)
73
+
74
+ ${ANSI_CYAN}doctor${ANSI_RESET} Verify local environment, Node.js, and Chrome/CDP availability
75
+
76
+ ${ANSI_CYAN}help${ANSI_RESET} Display this help message
77
+ `);
78
+ }
79
+
80
+ switch (command) {
81
+ case 'search': {
82
+ const isJson = args.includes('--json');
83
+ const query = args.slice(1).filter(a => !a.startsWith('--')).join(' ').trim();
84
+
85
+ if (!query) {
86
+ if (isJson) {
87
+ console.log(JSON.stringify({ error: 'Missing search query', usage: 'medesign search <query> [--json]' }));
88
+ } else {
89
+ console.log(`\n${ANSI_YELLOW}Usage:${ANSI_RESET} medesign search "<query>" [--json]\nExample: medesign search "cadastre survey"\n`);
90
+ }
91
+ process.exit(1);
92
+ }
93
+
94
+ const catalogPath = path.join(rootDir, 'references', 'catalog.json');
95
+ if (!fs.existsSync(catalogPath)) {
96
+ console.error('Error: references/catalog.json not found.');
97
+ process.exit(1);
98
+ }
99
+ const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
100
+
101
+ const presetsDir = path.join(rootDir, 'presets');
102
+ const presetFolders = fs.existsSync(presetsDir)
103
+ ? fs.readdirSync(presetsDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name)
104
+ : [];
105
+ const presets = [];
106
+ presetFolders.forEach(name => {
107
+ const pPath = path.join(presetsDir, name, 'preset.json');
108
+ if (fs.existsSync(pPath)) {
109
+ try {
110
+ presets.push(JSON.parse(fs.readFileSync(pPath, 'utf8')));
111
+ } catch (e) {}
112
+ }
113
+ });
114
+
115
+ const queryTerms = query.toLowerCase().split(/[\s,+/]+/).filter(Boolean);
116
+
117
+ // Score archetypes based on query relevance
118
+ const scored = catalog.archetypes.map(arch => {
119
+ let score = 0;
120
+ const nameLower = (arch.name || '').toLowerCase();
121
+ const titleLower = (arch.archetypeTitle || '').toLowerCase();
122
+ const groupLower = (arch.group || '').toLowerCase();
123
+ const vibeLower = (arch.vibe || '').toLowerCase();
124
+ const spatialLower = (arch.spatialModel || '').toLowerCase();
125
+ const colorLower = (arch.primaryColor || '').toLowerCase();
126
+ const typoLower = (arch.typography || '').toLowerCase();
127
+ const keywords = (arch.keywords || []).map(k => k.toLowerCase());
128
+ const presetId = (arch.presetId || '').toLowerCase();
129
+ const presetLink = (arch.presetLink || '').toLowerCase();
130
+
131
+ // Check entire query phrase match
132
+ const qLower = query.toLowerCase();
133
+ if (nameLower.includes(qLower) || titleLower.includes(qLower)) {
134
+ score += 40;
135
+ }
136
+
137
+ let titleTermsMatched = 0;
138
+ queryTerms.forEach(term => {
139
+ const stem = term.length > 4 ? term.replace(/(?:al|ic|ing|e|s)$/i, '') : term;
140
+ // Name / Title match
141
+ if (nameLower.includes(term) || titleLower.includes(term)) {
142
+ score += 20;
143
+ titleTermsMatched++;
144
+ } else if (stem.length >= 4 && (nameLower.includes(stem) || titleLower.includes(stem))) {
145
+ score += 16;
146
+ titleTermsMatched++;
147
+ }
148
+ // Keywords exact or substring match
149
+ keywords.forEach(kw => {
150
+ if (kw === term) score += 15;
151
+ else if (kw.includes(term) || term.includes(kw)) score += 10;
152
+ else if (stem.length >= 4 && (kw.includes(stem) || stem.includes(kw))) score += 8;
153
+ });
154
+ // Vibe match
155
+ if (vibeLower.includes(term) || (stem.length >= 4 && vibeLower.includes(stem))) score += 6;
156
+ // Spatial / color / typography / group match
157
+ if (spatialLower.includes(term)) score += 5;
158
+ if (colorLower.includes(term)) score += 5;
159
+ if (typoLower.includes(term)) score += 5;
160
+ if (groupLower.includes(term)) score += 4;
161
+ // Preset id / link match
162
+ if (presetId.includes(term) || presetLink.includes(term)) score += 12;
163
+ });
164
+
165
+ // Bonus if multiple query terms directly match the title
166
+ if (titleTermsMatched > 1) {
167
+ score += titleTermsMatched * 15;
168
+ }
169
+
170
+ // Match against presets definitions
171
+ presets.forEach(p => {
172
+ const pName = (p.name || '').toLowerCase();
173
+ const pDesc = (p.description || '').toLowerCase();
174
+ const pId = (p.id || '').toLowerCase();
175
+ const pArch = (p.archetype || '').toLowerCase();
176
+ let presetMatches = false;
177
+ queryTerms.forEach(term => {
178
+ if (pName.includes(term) || pDesc.includes(term) || pId === term || pArch.includes(term)) {
179
+ presetMatches = true;
180
+ }
181
+ });
182
+ if (presetMatches && (arch.presetId === p.id || (arch.presetLink && arch.presetLink.includes(p.id)))) {
183
+ score += 25;
184
+ }
185
+ });
186
+
187
+ return { ...arch, score };
188
+ });
189
+
190
+ scored.sort((a, b) => b.score - a.score);
191
+ const best = scored[0] || catalog.archetypes[0];
192
+
193
+ if (isJson) {
194
+ console.log(JSON.stringify({
195
+ query,
196
+ bestMatch: {
197
+ id: best.id,
198
+ number: best.number,
199
+ name: best.name,
200
+ archetypeTitle: best.archetypeTitle,
201
+ group: best.group,
202
+ presetLink: best.presetLink,
203
+ spatialModel: best.spatialModel,
204
+ primaryColor: best.primaryColor,
205
+ secondaryColor: best.secondaryColor,
206
+ surfaceContrast: best.surfaceContrast,
207
+ typography: best.typography,
208
+ motionProfile: best.motionProfile,
209
+ score: best.score
210
+ },
211
+ topMatches: scored.slice(0, 5).map(a => ({
212
+ number: a.number,
213
+ name: a.name,
214
+ score: a.score,
215
+ presetLink: a.presetLink
216
+ }))
217
+ }, null, 2));
218
+ process.exit(0);
219
+ }
220
+
221
+ console.log(`
222
+ ${ANSI_BOLD}ME-DESIGN Discovery Engine${ANSI_RESET}
223
+ ${ANSI_GRAY}────────────────────────────────────────${ANSI_RESET}
224
+ ${ANSI_BOLD}Query${ANSI_RESET} "${ANSI_CYAN}${query}${ANSI_RESET}"
225
+ ${ANSI_BOLD}Best Match${ANSI_RESET} Archetype ${best.number < 10 ? '0' + best.number : best.number}: ${ANSI_GREEN}${ANSI_BOLD}${best.name}${ANSI_RESET}
226
+ ${ANSI_BOLD}Preset Link${ANSI_RESET} ${best.presetLink ? ANSI_CYAN + best.presetLink + ANSI_RESET : ANSI_GRAY + 'Standard Catalog' + ANSI_RESET}
227
+
228
+ ${ANSI_BOLD}DESIGN ARCHETYPE RECOMMENDATION${ANSI_RESET}
229
+ ${ANSI_BOLD}Archetype${ANSI_RESET} ${best.archetypeTitle || best.name}
230
+ ${ANSI_BOLD}Spatial Model${ANSI_RESET} ${best.spatialModel || '16:10 / 100dvh Map-Centric with Split Data Grid'}
231
+ ${ANSI_BOLD}Primary Color${ANSI_RESET} ${best.primaryColor || 'Electric Cyan (oklch(0.85 0.18 200))'}
232
+ ${ANSI_BOLD}Secondary Color${ANSI_RESET} ${best.secondaryColor || 'Passed Emerald (oklch(0.72 0.18 145))'}
233
+ ${ANSI_BOLD}Surface Contrast${ANSI_RESET} ${best.surfaceContrast || 'Ultra-Dark Obsidian (#0a0d14)'}
234
+ ${ANSI_BOLD}Typography${ANSI_RESET} ${best.typography || 'JetBrains Mono + IBM Plex Sans Thai'}
235
+ ${ANSI_BOLD}Motion Profile${ANSI_RESET} ${best.motionProfile || 'Precision Linear (120ms max)'}
236
+ ${ANSI_GRAY}────────────────────────────────────────${ANSI_RESET}
237
+ `);
238
+ break;
239
+ }
240
+
241
+ case 'polish': {
242
+ const targetFile = args[1];
243
+ if (!targetFile) {
244
+ console.error(`${ANSI_RED}Error:${ANSI_RESET} Please specify target file to polish (e.g. medesign polish index.html)`);
245
+ process.exit(1);
246
+ }
247
+ const resolvedPath = path.resolve(process.cwd(), targetFile);
248
+ if (!fs.existsSync(resolvedPath)) {
249
+ console.error(`${ANSI_RED}Error:${ANSI_RESET} File not found: ${resolvedPath}`);
250
+ process.exit(1);
251
+ }
252
+
253
+ const linterScript = path.join(rootDir, 'tests', 'linter.js');
254
+ let beforeQ = 80.0;
255
+ try {
256
+ const beforeRes = spawnSync(process.execPath, [linterScript, resolvedPath, '--json'], { encoding: 'utf8' });
257
+ const parsed = JSON.parse(beforeRes.stdout);
258
+ beforeQ = parsed.qScore || 80.0;
259
+ } catch (e) {}
260
+
261
+ let content = fs.readFileSync(resolvedPath, 'utf8');
262
+ const fixes = [];
263
+
264
+ // 1. RULE MD104: Dynamic Viewport Units (Convert raw 100vh to 100dvh fallback)
265
+ let count104 = 0;
266
+ content = content.replace(/min-height\s*:\s*100vh\s*;?(?!\s*min-height\s*:\s*100[ds]vh)/gi, () => {
267
+ count104++;
268
+ return 'min-height: 100vh; min-height: 100dvh;';
269
+ });
270
+ content = content.replace(/(?<!min-)(?<!max-)height\s*:\s*100vh\s*;?(?!\s*(?:min-height|height)\s*:\s*100[ds]vh)/gi, () => {
271
+ count104++;
272
+ return 'min-height: 100vh; min-height: 100dvh;';
273
+ });
274
+ content = content.replace(/max-height\s*:\s*100vh\s*;?(?!\s*max-height\s*:\s*100[ds]vh)/gi, () => {
275
+ count104++;
276
+ return 'max-height: 100vh; max-height: 100dvh;';
277
+ });
278
+ if (count104 > 0) {
279
+ fixes.push(`Replaced ${count104} raw 100vh with 100dvh fallback (MD104)`);
280
+ }
281
+
282
+ // 2. RULE MD005: Focus Ring High Contrast & Physical Outline Visibility
283
+ const focusBlockRegex = /((?:[a-zA-Z0-9_\-\.\#\:\*\,\[\]\="'>\s]+)?:focus(?:-visible)?\s*\{)([^}]+)(\})/gi;
284
+ let md005Applied = false;
285
+ content = content.replace(focusBlockRegex, (match, prefix, body, suffix) => {
286
+ let updatedBody = body;
287
+ if (/outline\s*:\s*(?:none|0px?)\s*(?:;|\})/i.test(updatedBody)) {
288
+ updatedBody = updatedBody.replace(/outline\s*:\s*(?:none|0px?)\s*;?/gi, 'outline: 2px solid Highlight; outline-offset: 2px;');
289
+ md005Applied = true;
290
+ } else if (/box-shadow\s*:/i.test(updatedBody) && !/outline\s*:\s*(?!none|0px?)/i.test(updatedBody)) {
291
+ updatedBody += ' outline: 2px solid Highlight; outline-offset: 2px;';
292
+ md005Applied = true;
293
+ }
294
+ return prefix + updatedBody + suffix;
295
+ });
296
+
297
+ // Ensure forced-colors high contrast outline media query is injected
298
+ if (!/@media\s*\(forced-colors:\s*active\)/i.test(content)) {
299
+ const forcedColorsRule = `\n @media (forced-colors: active) {\n :focus-visible, button:focus-visible, [class*="btn"]:focus-visible, a:focus-visible {\n outline: 2px solid CanvasText !important;\n outline-offset: 2px !important;\n }\n }\n`;
300
+ if (content.includes('</style>')) {
301
+ content = content.replace('</style>', `${forcedColorsRule} </style>`);
302
+ md005Applied = true;
303
+ } else if (resolvedPath.endsWith('.css') || !/<html|<body/i.test(content)) {
304
+ content = content.trimEnd() + '\n' + forcedColorsRule;
305
+ md005Applied = true;
306
+ }
307
+ }
308
+ if (md005Applied) {
309
+ fixes.push('Added physical focus outline & forced-colors high contrast (MD005)');
310
+ }
311
+
312
+ // 3. RULE MD004: Broken State Contract (Dead Buttons)
313
+ const hasButtons = /<button/i.test(content) || /class="[^"]*btn[^"]*"/i.test(content) || /\.btn\b/i.test(content);
314
+ const hasInteractive = /:hover/i.test(content) || /:active/i.test(content);
315
+ if (hasButtons && !hasInteractive) {
316
+ const buttonStates = `\n button:hover, [class*="btn"]:hover, .btn:hover { filter: brightness(1.1); }\n button:active, [class*="btn"]:active, .btn:active { transform: scale(0.98); }\n button:focus-visible, [class*="btn"]:focus-visible, .btn:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }\n`;
317
+ if (content.includes('</style>')) {
318
+ content = content.replace('</style>', `${buttonStates} </style>`);
319
+ fixes.push('Injected hover and active interactive states for buttons (MD004)');
320
+ } else if (resolvedPath.endsWith('.css') || !/<html|<body/i.test(content)) {
321
+ content = content.trimEnd() + '\n' + buttonStates;
322
+ fixes.push('Injected hover and active interactive states for buttons (MD004)');
323
+ }
324
+ }
325
+
326
+ // 4. RULE MD002: Reduced Motion Compliance
327
+ const hasAnimations = /animation:|transition:/i.test(content) && !/transition:\s*none/i.test(content);
328
+ const hasReducedMotion = /prefers-reduced-motion/i.test(content);
329
+ if (hasAnimations && !hasReducedMotion) {
330
+ const reducedMotionBlock = `\n @media (prefers-reduced-motion: reduce) {\n *, *::before, *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n }\n`;
331
+ if (content.includes('</style>')) {
332
+ content = content.replace('</style>', `${reducedMotionBlock} </style>`);
333
+ fixes.push('Added prefers-reduced-motion accessibility gate (MD002)');
334
+ } else if (resolvedPath.endsWith('.css') || !/<html|<body/i.test(content)) {
335
+ content = content.trimEnd() + '\n' + reducedMotionBlock;
336
+ fixes.push('Added prefers-reduced-motion accessibility gate (MD002)');
337
+ }
338
+ }
339
+
340
+ // 5. Anti-AI Tells: Em-Dash / En-Dash & HTML Entities Cleanup
341
+ const emDashRegex = /\s*(?:[—–]|&mdash;|&ndash;|&#8212;|&#8211;|&#x2014;|&#x2013;)\s*/g;
342
+ const emDashMatches = content.match(emDashRegex);
343
+ if (emDashMatches && emDashMatches.length > 0) {
344
+ const count = emDashMatches.length;
345
+ content = content.replace(emDashRegex, ' - ');
346
+ fixes.push(`Cleaned ${count} AI-tell em-dash punctuation & entities`);
347
+ }
348
+
349
+ fs.writeFileSync(resolvedPath, content, 'utf8');
350
+
351
+ let afterQ = beforeQ;
352
+ let isPassed = true;
353
+ try {
354
+ const afterRes = spawnSync(process.execPath, [linterScript, resolvedPath, '--json'], { encoding: 'utf8' });
355
+ const parsed = JSON.parse(afterRes.stdout);
356
+ afterQ = parsed.qScore !== undefined ? parsed.qScore : beforeQ;
357
+ isPassed = parsed.passed !== undefined ? parsed.passed : (afterQ >= 75);
358
+ } catch (e) {}
359
+
360
+ console.log(`\n${ANSI_GREEN}${ANSI_BOLD}[POLISH COMPLETED]${ANSI_RESET} ${fixes.length} issues automatically repaired in ${ANSI_CYAN}${path.basename(resolvedPath)}${ANSI_RESET}`);
361
+ fixes.forEach(f => console.log(` ${ANSI_GREEN}Fixed:${ANSI_RESET} ${f}`));
362
+ console.log(`${ANSI_BOLD}Q-Score:${ANSI_RESET} ${beforeQ.toFixed(1)} -> ${ANSI_GREEN}${ANSI_BOLD}${afterQ.toFixed(1)}${ANSI_RESET} [${isPassed ? ANSI_GREEN + 'PASS' + ANSI_RESET : ANSI_YELLOW + 'WARNING' + ANSI_RESET}]\n`);
363
+ process.exit(0);
364
+ }
365
+
366
+ case 'bolder': {
367
+ const targetFile = args[1];
368
+ if (!targetFile) {
369
+ console.error(`${ANSI_RED}Error:${ANSI_RESET} Please specify target file (e.g. medesign bolder index.html)`);
370
+ process.exit(1);
371
+ }
372
+ const resolvedPath = path.resolve(process.cwd(), targetFile);
373
+ if (!fs.existsSync(resolvedPath)) {
374
+ console.error(`${ANSI_RED}Error:${ANSI_RESET} File not found: ${resolvedPath}`);
375
+ process.exit(1);
376
+ }
377
+
378
+ let content = fs.readFileSync(resolvedPath, 'utf8');
379
+ let fontCount = 0;
380
+ let borderCount = 0;
381
+ let accentCount = 0;
382
+
383
+ // 1. Single-pass atomic elevation of font weights (prevents cascading replacement)
384
+ const bolderFontMap = {
385
+ 'font-normal': 'font-medium',
386
+ 'font-medium': 'font-semibold',
387
+ 'font-semibold': 'font-bold'
388
+ };
389
+ content = content.replace(/\b(font-normal|font-medium|font-semibold)\b/g, (m) => {
390
+ fontCount++;
391
+ return bolderFontMap[m] || m;
392
+ });
393
+
394
+ content = content.replace(/font-weight\s*:\s*(400|500|normal)\b/gi, (match, val) => {
395
+ fontCount++;
396
+ if (val === '400' || val.toLowerCase() === 'normal') return 'font-weight: 600';
397
+ if (val === '500') return 'font-weight: 700';
398
+ return match;
399
+ });
400
+
401
+ // 2. Deepen border contrast & opacity (single pass)
402
+ const bolderBorderMap = {
403
+ 'border-white/10': 'border-white/20',
404
+ 'border-white/5': 'border-white/15',
405
+ 'border-neutral-200': 'border-neutral-400',
406
+ 'border-gray-200': 'border-gray-400'
407
+ };
408
+ content = content.replace(/\b(border-white\/10|border-white\/5|border-neutral-200|border-gray-200)\b/g, (m) => {
409
+ borderCount++;
410
+ return bolderBorderMap[m] || m;
411
+ });
412
+
413
+ content = content.replace(/rgba\(\s*255\s*,\s*255\s*,\s*255\s*,\s*0\.0[5-8]\s*\)/gi, () => { borderCount++; return 'rgba(255, 255, 255, 0.18)'; });
414
+ content = content.replace(/rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\.0[5-8]\s*\)/gi, () => { borderCount++; return 'rgba(0, 0, 0, 0.18)'; });
415
+
416
+ // 3. Boost Accent Saturation & Vibrancy
417
+ // OKLCH chroma boost (increase C by ~25% for chromatic colors C >= 0.04)
418
+ content = content.replace(/oklch\(\s*([\d\.]+)\s+([\d\.]+)(\s+[\d\.]+(?:\s*(?:\/|\s)\s*[\d\.\%]+)?)\s*\)/gi, (match, l, c, rest) => {
419
+ const cVal = parseFloat(c);
420
+ if (cVal >= 0.04) {
421
+ accentCount++;
422
+ const boostedC = Math.min(0.35, parseFloat((cVal * 1.25).toFixed(3)));
423
+ return `oklch(${l} ${boostedC}${rest})`;
424
+ }
425
+ return match;
426
+ });
427
+
428
+ // HSL saturation boost (+15% saturation)
429
+ content = content.replace(/hsl\(\s*([\d\.]+)\s*,\s*([\d\.]+)%\s*,\s*([\d\.]+)%\s*\)/gi, (match, h, s, l) => {
430
+ const sVal = parseFloat(s);
431
+ if (sVal > 0 && sVal <= 85) {
432
+ accentCount++;
433
+ const boostedS = Math.min(100, Math.round(sVal + 15));
434
+ return `hsl(${h}, ${boostedS}%, ${l}%)`;
435
+ }
436
+ return match;
437
+ });
438
+
439
+ // Tailwind accent vibrancy elevation
440
+ const bolderTailwindMap = {
441
+ 'saturate-50': 'saturate-100',
442
+ 'saturate-100': 'saturate-150'
443
+ };
444
+ content = content.replace(/\b(saturate-50|saturate-100)\b/g, (m) => {
445
+ accentCount++;
446
+ return bolderTailwindMap[m] || m;
447
+ });
448
+ content = content.replace(/\b(bg|text|border)-(blue|cyan|indigo|emerald|sky|violet|amber|rose|teal)-500\b/g, (match, prop, col) => {
449
+ accentCount++;
450
+ return `${prop}-${col}-600`;
451
+ });
452
+
453
+ fs.writeFileSync(resolvedPath, content, 'utf8');
454
+
455
+ console.log(`\n${ANSI_GREEN}${ANSI_BOLD}[BOLDER APPLIED]${ANSI_RESET} Visual density boosted in ${ANSI_CYAN}${path.basename(resolvedPath)}${ANSI_RESET}`);
456
+ console.log(` ${ANSI_BOLD}Font Weights:${ANSI_RESET} ${fontCount} font weight declarations elevated`);
457
+ console.log(` ${ANSI_BOLD}Border Contrast:${ANSI_RESET} ${borderCount} border treatments deepened`);
458
+ console.log(` ${ANSI_BOLD}Accents:${ANSI_RESET} ${accentCount} accent saturation & vibrancy rules boosted\n`);
459
+ process.exit(0);
460
+ }
461
+
462
+ case 'quiet': {
463
+ const targetFile = args[1];
464
+ if (!targetFile) {
465
+ console.error(`${ANSI_RED}Error:${ANSI_RESET} Please specify target file (e.g. medesign quiet index.html)`);
466
+ process.exit(1);
467
+ }
468
+ const resolvedPath = path.resolve(process.cwd(), targetFile);
469
+ if (!fs.existsSync(resolvedPath)) {
470
+ console.error(`${ANSI_RED}Error:${ANSI_RESET} File not found: ${resolvedPath}`);
471
+ process.exit(1);
472
+ }
473
+
474
+ let content = fs.readFileSync(resolvedPath, 'utf8');
475
+ let fontCount = 0;
476
+ let borderCount = 0;
477
+ let toneCount = 0;
478
+
479
+ // 1. Single-pass atomic softening of font weights (prevents cascading replacement)
480
+ const quietFontMap = {
481
+ 'font-extrabold': 'font-bold',
482
+ 'font-bold': 'font-semibold',
483
+ 'font-semibold': 'font-medium',
484
+ 'font-medium': 'font-normal'
485
+ };
486
+ content = content.replace(/\b(font-extrabold|font-bold|font-semibold|font-medium)\b/g, (m) => {
487
+ fontCount++;
488
+ return quietFontMap[m] || m;
489
+ });
490
+
491
+ content = content.replace(/font-weight\s*:\s*(700|600|bold)\b/gi, (match, val) => {
492
+ fontCount++;
493
+ if (val === '700' || val.toLowerCase() === 'bold') return 'font-weight: 500';
494
+ if (val === '600') return 'font-weight: 400';
495
+ return match;
496
+ });
497
+
498
+ // 2. Soften borders to minimalist hairlines (single pass)
499
+ const quietBorderMap = {
500
+ 'border-2': 'border',
501
+ 'border-white/20': 'border-white/10',
502
+ 'border-white/15': 'border-white/8',
503
+ 'border-neutral-400': 'border-neutral-200',
504
+ 'border-gray-400': 'border-gray-200'
505
+ };
506
+ content = content.replace(/\b(border-2|border-white\/20|border-white\/15|border-neutral-400|border-gray-400)\b/g, (m) => {
507
+ borderCount++;
508
+ return quietBorderMap[m] || m;
509
+ });
510
+
511
+ content = content.replace(/rgba\(\s*255\s*,\s*255\s*,\s*255\s*,\s*0\.1[5-8]\s*\)/gi, () => { borderCount++; return 'rgba(255, 255, 255, 0.08)'; });
512
+ content = content.replace(/rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\.1[5-8]\s*\)/gi, () => { borderCount++; return 'rgba(0, 0, 0, 0.08)'; });
513
+
514
+ // 3. Shift Tone to Minimalist (Mute chroma & saturation)
515
+ // OKLCH chroma reduction (decrease C by ~30% for chromatic colors C >= 0.04)
516
+ content = content.replace(/oklch\(\s*([\d\.]+)\s+([\d\.]+)(\s+[\d\.]+(?:\s*(?:\/|\s)\s*[\d\.\%]+)?)\s*\)/gi, (match, l, c, rest) => {
517
+ const cVal = parseFloat(c);
518
+ if (cVal >= 0.04) {
519
+ toneCount++;
520
+ const softenedC = Math.max(0.01, parseFloat((cVal * 0.70).toFixed(3)));
521
+ return `oklch(${l} ${softenedC}${rest})`;
522
+ }
523
+ return match;
524
+ });
525
+
526
+ // HSL saturation reduction (-25% saturation)
527
+ content = content.replace(/hsl\(\s*([\d\.]+)\s*,\s*([\d\.]+)%\s*,\s*([\d\.]+)%\s*\)/gi, (match, h, s, l) => {
528
+ const sVal = parseFloat(s);
529
+ if (sVal > 15) {
530
+ toneCount++;
531
+ const softenedS = Math.max(10, Math.round(sVal - 25));
532
+ return `hsl(${h}, ${softenedS}%, ${l}%)`;
533
+ }
534
+ return match;
535
+ });
536
+
537
+ // Tailwind accent softening
538
+ const quietTailwindMap = {
539
+ 'saturate-150': 'saturate-100',
540
+ 'saturate-100': 'saturate-50'
541
+ };
542
+ content = content.replace(/\b(saturate-150|saturate-100)\b/g, (m) => {
543
+ toneCount++;
544
+ return quietTailwindMap[m] || m;
545
+ });
546
+ content = content.replace(/\b(bg|text|border)-(blue|cyan|indigo|emerald|sky|violet|amber|rose|teal)-600\b/g, (match, prop, col) => {
547
+ toneCount++;
548
+ return `${prop}-${col}-500`;
549
+ });
550
+
551
+ fs.writeFileSync(resolvedPath, content, 'utf8');
552
+
553
+ console.log(`\n${ANSI_GREEN}${ANSI_BOLD}[QUIET APPLIED]${ANSI_RESET} Visual density softened in ${ANSI_CYAN}${path.basename(resolvedPath)}${ANSI_RESET}`);
554
+ console.log(` ${ANSI_BOLD}Font Weights:${ANSI_RESET} ${fontCount} font weight declarations softened`);
555
+ console.log(` ${ANSI_BOLD}Borders:${ANSI_RESET} ${borderCount} border treatments converted to minimalist hairlines`);
556
+ console.log(` ${ANSI_BOLD}Tone:${ANSI_RESET} ${toneCount} color tone declarations softened to minimalist palette\n`);
557
+ process.exit(0);
558
+ }
559
+
560
+ case 'lint': {
561
+ const linterScript = path.join(rootDir, 'tests', 'linter.js');
562
+ const child = spawnSync(process.execPath, [linterScript, ...args.slice(1)], {
563
+ stdio: 'inherit',
564
+ cwd: process.cwd()
565
+ });
566
+ process.exit(child.status ?? 0);
567
+ }
568
+
569
+ case 'audit': {
570
+ const auditScript = path.join(rootDir, 'tests', 'browser-audit.js');
571
+ const child = spawnSync(process.execPath, [auditScript, ...args.slice(1)], {
572
+ stdio: 'inherit',
573
+ cwd: process.cwd()
574
+ });
575
+ process.exit(child.status ?? 0);
576
+ }
577
+
578
+ case 'tokens': {
579
+ const exportScript = path.join(rootDir, 'scripts', 'export-tokens.js');
580
+ const exportArgs = args.slice(1).filter(a => a !== 'export');
581
+ const child = spawnSync(process.execPath, [exportScript, ...exportArgs], {
582
+ stdio: 'inherit',
583
+ cwd: process.cwd()
584
+ });
585
+ process.exit(child.status ?? 0);
586
+ }
587
+
588
+ case 'presets': {
589
+ const sub = args[1] || 'list';
590
+ const presetsDir = path.join(rootDir, 'presets');
591
+ if (sub === 'list') {
592
+ console.log(`\n${ANSI_BOLD}AVAILABLE ME-DESIGN PRESETS:${ANSI_RESET}\n`);
593
+ const folders = fs.readdirSync(presetsDir, { withFileTypes: true })
594
+ .filter(d => d.isDirectory())
595
+ .map(d => d.name);
596
+ folders.forEach(f => {
597
+ const pJson = path.join(presetsDir, f, 'preset.json');
598
+ if (fs.existsSync(pJson)) {
599
+ const data = JSON.parse(fs.readFileSync(pJson, 'utf8'));
600
+ console.log(` ${ANSI_CYAN}${data.id.padEnd(10)}${ANSI_RESET} ${data.name}`);
601
+ console.log(` ${ANSI_GRAY}${data.archetype}${ANSI_RESET}`);
602
+ }
603
+ });
604
+ console.log('');
605
+ } else if (sub === 'get') {
606
+ const name = args[2];
607
+ if (!name) {
608
+ console.error('Error: Please specify preset name (e.g. medesign presets get apple)');
609
+ process.exit(1);
610
+ }
611
+ const pJson = path.join(presetsDir, name.toLowerCase(), 'preset.json');
612
+ if (!fs.existsSync(pJson)) {
613
+ console.error(`Error: Preset '${name}' not found.`);
614
+ process.exit(1);
615
+ }
616
+ console.log(fs.readFileSync(pJson, 'utf8'));
617
+ }
618
+ break;
619
+ }
620
+
621
+ case 'test': {
622
+ const runnerScript = path.join(rootDir, 'tests', 'runner.js');
623
+ const child = spawnSync(process.execPath, [runnerScript, ...args.slice(1)], {
624
+ stdio: 'inherit',
625
+ cwd: process.cwd()
626
+ });
627
+ process.exit(child.status ?? 0);
628
+ }
629
+
630
+ case 'doctor': {
631
+ console.log(`\n${ANSI_BOLD}ME-DESIGN System Health & Environment Doctor${ANSI_RESET}`);
632
+ console.log(`${ANSI_GRAY}────────────────────────────────────────${ANSI_RESET}\n`);
633
+
634
+ // 1. Node.js Version Check
635
+ const nodeVer = process.version;
636
+ const major = parseInt(nodeVer.slice(1).split('.')[0], 10);
637
+ const nodeOk = major >= 18;
638
+ console.log(`Node.js Runtime: ${nodeOk ? ANSI_GREEN + nodeVer + ' (Supported)' : ANSI_RED + nodeVer + ' (Requires >= 18.0.0)'}${ANSI_RESET}`);
639
+
640
+ const chromeCandidates = [
641
+ process.env.CHROME_PATH,
642
+ process.env.CHROME_BIN,
643
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
644
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
645
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
646
+ path.join(process.env.LOCALAPPDATA || '', 'ms-playwright', 'chromium-1248', 'chrome-win64', 'chrome.exe'),
647
+ '/usr/bin/google-chrome',
648
+ '/usr/bin/google-chrome-stable',
649
+ '/usr/bin/chromium',
650
+ '/usr/bin/chromium-browser',
651
+ '/snap/bin/chromium',
652
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
653
+ '/Applications/Chromium.app/Contents/MacOS/Chromium'
654
+ ].filter(Boolean);
655
+
656
+ let foundChrome = null;
657
+ for (const c of chromeCandidates) {
658
+ if (fs.existsSync(c)) {
659
+ foundChrome = c;
660
+ break;
661
+ }
662
+ }
663
+ console.log(`Headless Browser: ${foundChrome ? ANSI_GREEN + 'Found (' + path.basename(foundChrome) + ')' : ANSI_YELLOW + 'Not found in default paths'}${ANSI_RESET}`);
664
+ if (foundChrome) {
665
+ console.log(` ${ANSI_GRAY}${foundChrome}${ANSI_RESET}`);
666
+ }
667
+
668
+ // 3. Presets Check
669
+ const presetsDir = path.join(rootDir, 'presets');
670
+ const presets = fs.readdirSync(presetsDir, { withFileTypes: true })
671
+ .filter(d => d.isDirectory() && fs.existsSync(path.join(presetsDir, d.name, 'preset.json')));
672
+ console.log(`Installed Presets: ${ANSI_GREEN}${presets.length} valid presets (${presets.map(p => p.name).join(', ')})${ANSI_RESET}`);
673
+
674
+ // 4. Test Suite Fixtures
675
+ const fixturesDir = path.join(rootDir, 'tests', 'fixtures');
676
+ const validCount = fs.existsSync(path.join(fixturesDir, 'valid')) ? fs.readdirSync(path.join(fixturesDir, 'valid')).length : 0;
677
+ const invalidCount = fs.existsSync(path.join(fixturesDir, 'invalid')) ? fs.readdirSync(path.join(fixturesDir, 'invalid')).length : 0;
678
+ console.log(`Test Fixtures: ${ANSI_GREEN}${validCount} valid, ${invalidCount} invalid test fixtures${ANSI_RESET}\n`);
679
+
680
+ console.log(`${ANSI_GRAY}────────────────────────────────────────${ANSI_RESET}`);
681
+ console.log(`${ANSI_GREEN}${ANSI_BOLD}STATUS: ALL SYSTEMS OPERATIONAL${ANSI_RESET}\n`);
682
+ break;
683
+ }
684
+
685
+ case 'help':
686
+ case '--help':
687
+ case '-h':
688
+ default:
689
+ printHelp();
690
+ break;
691
+ }