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/README.md +223 -129
- package/bin/medesign.js +691 -200
- package/package.json +69 -64
- package/references/catalog.json +1023 -108
- package/tests/linter.js +412 -412
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
${ANSI_CYAN}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
${ANSI_CYAN}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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*(?:[—–]|—|–|—|–|—|–)\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
|
+
}
|