luxlabs 1.0.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/commands/up.js ADDED
@@ -0,0 +1,211 @@
1
+ const axios = require('axios');
2
+ const archiver = require('archiver');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const ora = require('ora');
6
+ const chalk = require('chalk');
7
+ const ignore = require('ignore');
8
+ const {
9
+ loadConfig,
10
+ loadInterfaceConfig,
11
+ saveInterfaceConfig,
12
+ getApiUrl,
13
+ getAuthHeaders,
14
+ isAuthenticated,
15
+ } = require('../lib/config');
16
+
17
+ async function up(options) {
18
+ // Check authentication
19
+ if (!isAuthenticated()) {
20
+ console.log(
21
+ chalk.red('❌ Not authenticated. Run'),
22
+ chalk.white('lux login'),
23
+ chalk.red('first.')
24
+ );
25
+ process.exit(1);
26
+ }
27
+
28
+ // Check if initialized
29
+ const interfaceConfig = loadInterfaceConfig();
30
+ if (!interfaceConfig) {
31
+ console.log(
32
+ chalk.red('❌ Not initialized. Run'),
33
+ chalk.white('lux init'),
34
+ chalk.red('first.')
35
+ );
36
+ process.exit(1);
37
+ }
38
+
39
+ const config = loadConfig();
40
+ const apiUrl = getApiUrl();
41
+ const spinner = ora('Preparing upload...').start();
42
+
43
+ try {
44
+ let interfaceId = interfaceConfig.id;
45
+
46
+ // Step 1: Create interface if it doesn't exist
47
+ if (!interfaceId) {
48
+ spinner.text = 'Creating interface...';
49
+
50
+ const { data } = await axios.post(
51
+ `${apiUrl}/api/interfaces`,
52
+ {
53
+ name: interfaceConfig.name,
54
+ description: interfaceConfig.description,
55
+ },
56
+ {
57
+ headers: getAuthHeaders(),
58
+ }
59
+ );
60
+
61
+ interfaceId = data.interface.id;
62
+ interfaceConfig.id = interfaceId;
63
+ interfaceConfig.githubRepoUrl = data.interface.github_repo_url;
64
+ saveInterfaceConfig(interfaceConfig);
65
+
66
+ spinner.succeed(chalk.green(`Created interface: ${interfaceId}`));
67
+ spinner.start();
68
+ }
69
+
70
+ // Step 2: Create zip file
71
+ spinner.text = 'Creating zip file...';
72
+ const zipPath = await createZip(process.cwd());
73
+ const stats = fs.statSync(zipPath);
74
+ spinner.text = `Created zip (${formatBytes(stats.size)})...`;
75
+
76
+ // Step 3: Get presigned upload URL
77
+ spinner.text = 'Getting upload URL...';
78
+ const { data: urlData } = await axios.post(
79
+ `${apiUrl}/api/interfaces/${interfaceId}/presigned-urls`,
80
+ { zip_upload: true },
81
+ { headers: getAuthHeaders() }
82
+ );
83
+
84
+ // Step 4: Upload zip to R2
85
+ spinner.text = 'Uploading files...';
86
+ await axios.put(urlData.upload_url, fs.readFileSync(zipPath), {
87
+ headers: { 'Content-Type': 'application/zip' },
88
+ maxBodyLength: Infinity,
89
+ maxContentLength: Infinity,
90
+ onUploadProgress: (progressEvent) => {
91
+ const percent = Math.round(
92
+ (progressEvent.loaded * 100) / progressEvent.total
93
+ );
94
+ spinner.text = `Uploading files... ${percent}%`;
95
+ },
96
+ });
97
+
98
+ // Step 5: Trigger extraction and GitHub sync
99
+ spinner.text = 'Syncing to GitHub...';
100
+ const { data: uploadResult } = await axios.post(
101
+ `${apiUrl}/api/interfaces/${interfaceId}/upload-files`,
102
+ { zip_path: urlData.storage_path },
103
+ { headers: getAuthHeaders() }
104
+ );
105
+
106
+ // Cleanup
107
+ fs.unlinkSync(zipPath);
108
+
109
+ spinner.succeed(
110
+ chalk.green(`✓ Uploaded ${uploadResult.files_uploaded} files`)
111
+ );
112
+
113
+ console.log(chalk.dim(`\n${uploadResult.message}\n`));
114
+ console.log(chalk.cyan('Files synced to dev branch. Ready to deploy!'));
115
+ console.log(
116
+ chalk.dim(`Run ${chalk.white('lux deploy')} to publish to production\n`)
117
+ );
118
+
119
+ if (interfaceConfig.githubRepoUrl) {
120
+ console.log(chalk.dim(`GitHub: ${interfaceConfig.githubRepoUrl}`));
121
+ }
122
+ } catch (error) {
123
+ spinner.fail('Upload failed');
124
+ console.error(
125
+ chalk.red('\n❌ Error:'),
126
+ error.response?.data?.error || error.message
127
+ );
128
+
129
+ if (error.response?.status === 401) {
130
+ console.log(
131
+ chalk.yellow('\nYour session may have expired. Try running:'),
132
+ chalk.white('lux login')
133
+ );
134
+ }
135
+
136
+ process.exit(1);
137
+ }
138
+ }
139
+
140
+ async function createZip(sourceDir) {
141
+ const output = fs.createWriteStream('/tmp/lux-upload.zip');
142
+ const archive = archiver('zip', { zlib: { level: 9 } });
143
+
144
+ // Load .gitignore patterns
145
+ const ig = ignore();
146
+ const gitignorePath = path.join(sourceDir, '.gitignore');
147
+
148
+ if (fs.existsSync(gitignorePath)) {
149
+ const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
150
+ ig.add(gitignoreContent);
151
+ }
152
+
153
+ // Add default ignores (same as server)
154
+ ig.add([
155
+ 'node_modules/',
156
+ '.next/',
157
+ '.git/',
158
+ '.env.local',
159
+ '.DS_Store',
160
+ 'dist/',
161
+ 'build/',
162
+ '.cache/',
163
+ 'coverage/',
164
+ '.lux/interface.json', // Don't upload local config
165
+ ]);
166
+
167
+ return new Promise((resolve, reject) => {
168
+ output.on('close', () => resolve('/tmp/lux-upload.zip'));
169
+ archive.on('error', reject);
170
+
171
+ archive.pipe(output);
172
+
173
+ // Recursively add files
174
+ function addDirectory(dirPath, baseDir = '') {
175
+ const files = fs.readdirSync(dirPath);
176
+
177
+ for (const file of files) {
178
+ const fullPath = path.join(dirPath, file);
179
+ const relativePath = path.join(baseDir, file);
180
+
181
+ // Check if ignored
182
+ if (ig.ignores(relativePath)) {
183
+ continue;
184
+ }
185
+
186
+ const stat = fs.statSync(fullPath);
187
+
188
+ if (stat.isDirectory()) {
189
+ addDirectory(fullPath, relativePath);
190
+ } else if (stat.isFile()) {
191
+ archive.file(fullPath, { name: relativePath });
192
+ }
193
+ }
194
+ }
195
+
196
+ addDirectory(sourceDir);
197
+ archive.finalize();
198
+ });
199
+ }
200
+
201
+ function formatBytes(bytes) {
202
+ if (bytes === 0) return '0 Bytes';
203
+ const k = 1024;
204
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
205
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
206
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
207
+ }
208
+
209
+ module.exports = {
210
+ up,
211
+ };
@@ -0,0 +1,502 @@
1
+ /**
2
+ * Validate data-lux Attributes CLI Command
3
+ *
4
+ * Scans interface component files and reports interactive elements
5
+ * that are missing data-lux attributes.
6
+ *
7
+ * Usage:
8
+ * node lux-cli.js validate-data-lux [interface-id]
9
+ *
10
+ * If interface-id is not provided, validates all interfaces.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const chalk = require('chalk');
16
+ const {
17
+ getInterfacesDir,
18
+ getOrgId,
19
+ getProjectId,
20
+ LUX_STUDIO_DIR,
21
+ } = require('../lib/config');
22
+ const { error, success, info, warn } = require('../lib/helpers');
23
+
24
+ // Interactive elements that should have data-lux
25
+ const INTERACTIVE_ELEMENTS = ['button', 'input', 'form', 'select', 'textarea'];
26
+
27
+ // Event handlers that indicate an element is interactive
28
+ const INTERACTIVE_HANDLERS = [
29
+ 'onClick',
30
+ 'onSubmit',
31
+ 'onChange',
32
+ 'onBlur',
33
+ 'onFocus',
34
+ 'onKeyDown',
35
+ 'onKeyUp',
36
+ 'onKeyPress',
37
+ ];
38
+
39
+ /**
40
+ * Find all component files in a directory recursively
41
+ */
42
+ function findComponentFiles(dir, files = []) {
43
+ if (!fs.existsSync(dir)) return files;
44
+
45
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
46
+
47
+ for (const entry of entries) {
48
+ const fullPath = path.join(dir, entry.name);
49
+
50
+ // Skip node_modules and hidden directories
51
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') {
52
+ continue;
53
+ }
54
+
55
+ if (entry.isDirectory()) {
56
+ findComponentFiles(fullPath, files);
57
+ } else if (entry.isFile() && /\.(tsx|jsx)$/.test(entry.name)) {
58
+ files.push(fullPath);
59
+ }
60
+ }
61
+
62
+ return files;
63
+ }
64
+
65
+ /**
66
+ * Find the end of a JSX element's opening tag, properly handling nested braces.
67
+ * Returns { endIndex, attributes } or null if not found.
68
+ *
69
+ * This handles cases like:
70
+ * <input onChange={(e) => setValue(e)} data-lux={lux('name')} />
71
+ * where the `>` in `=>` should not end the tag.
72
+ */
73
+ function findElementEnd(content, startIndex) {
74
+ let braceDepth = 0;
75
+ let i = startIndex;
76
+
77
+ while (i < content.length) {
78
+ const char = content[i];
79
+
80
+ if (char === '{') {
81
+ braceDepth++;
82
+ } else if (char === '}') {
83
+ braceDepth--;
84
+ } else if (char === '>' && braceDepth === 0) {
85
+ // Found the actual end of the opening tag
86
+ return {
87
+ endIndex: i,
88
+ attributes: content.substring(startIndex, i),
89
+ };
90
+ }
91
+
92
+ i++;
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * Find all matches of interactive elements with proper brace handling
100
+ */
101
+ function findElementMatches(content, element) {
102
+ const matches = [];
103
+ const tagPattern = new RegExp(`<${element}\\b`, 'gi');
104
+ let tagMatch;
105
+
106
+ while ((tagMatch = tagPattern.exec(content)) !== null) {
107
+ const tagStart = tagMatch.index;
108
+ const attrStart = tagStart + tagMatch[0].length;
109
+
110
+ const result = findElementEnd(content, attrStart);
111
+ if (result) {
112
+ matches.push({
113
+ index: tagStart,
114
+ fullMatch: content.substring(tagStart, result.endIndex + 1),
115
+ attributes: result.attributes,
116
+ });
117
+ }
118
+ }
119
+
120
+ return matches;
121
+ }
122
+
123
+ /**
124
+ * Find all elements that have a specific handler, with proper brace handling
125
+ */
126
+ function findElementsWithHandler(content, handler) {
127
+ const matches = [];
128
+ // Match any element opening tag
129
+ const tagPattern = /<(\w+)\b/g;
130
+ let tagMatch;
131
+
132
+ while ((tagMatch = tagPattern.exec(content)) !== null) {
133
+ const tagStart = tagMatch.index;
134
+ const elementName = tagMatch[1];
135
+ const attrStart = tagStart + tagMatch[0].length;
136
+
137
+ const result = findElementEnd(content, attrStart);
138
+ if (result) {
139
+ // Check if this element has the handler
140
+ const handlerPattern = new RegExp(`${handler}=`);
141
+ if (handlerPattern.test(result.attributes)) {
142
+ matches.push({
143
+ index: tagStart,
144
+ element: elementName.toLowerCase(),
145
+ fullMatch: content.substring(tagStart, result.endIndex + 1),
146
+ attributes: result.attributes,
147
+ });
148
+ }
149
+ }
150
+ }
151
+
152
+ return matches;
153
+ }
154
+
155
+ /**
156
+ * Check if an element is inside a .map() callback
157
+ * This is a heuristic check - looks for common patterns
158
+ */
159
+ function isInsideMapCallback(content, elementIndex) {
160
+ // Look backwards from the element to find .map( pattern
161
+ const beforeElement = content.substring(0, elementIndex);
162
+ const lastMapIndex = beforeElement.lastIndexOf('.map(');
163
+ const lastCloseParen = beforeElement.lastIndexOf(')');
164
+
165
+ // If there's a .map( and it's not closed before our element, we're likely inside it
166
+ if (lastMapIndex > -1 && lastMapIndex > lastCloseParen) {
167
+ return true;
168
+ }
169
+
170
+ return false;
171
+ }
172
+
173
+ /**
174
+ * Extract element information from a match
175
+ */
176
+ function extractElementInfo(content, elementStr, matchIndex) {
177
+ // Count line number
178
+ const beforeMatch = content.substring(0, matchIndex);
179
+ const lineNumber = (beforeMatch.match(/\n/g) || []).length + 1;
180
+
181
+ // Try to extract text content for buttons
182
+ let textContent = null;
183
+ const afterElement = content.substring(matchIndex);
184
+ const closingMatch = afterElement.match(/>([^<]*)</);
185
+ if (closingMatch && closingMatch[1].trim()) {
186
+ textContent = closingMatch[1].trim();
187
+ }
188
+
189
+ // Try to extract placeholder for inputs
190
+ const placeholderMatch = elementStr.match(/placeholder=["']([^"']+)["']/);
191
+ const placeholder = placeholderMatch ? placeholderMatch[1] : null;
192
+
193
+ // Try to extract name attribute
194
+ const nameMatch = elementStr.match(/name=["']([^"']+)["']/);
195
+ const name = nameMatch ? nameMatch[1] : null;
196
+
197
+ // Try to extract aria-label
198
+ const ariaMatch = elementStr.match(/aria-label=["']([^"']+)["']/);
199
+ const ariaLabel = ariaMatch ? ariaMatch[1] : null;
200
+
201
+ // Try to extract onClick handler name
202
+ const onClickMatch = elementStr.match(/onClick=\{(\w+)\}/);
203
+ const handlerName = onClickMatch ? onClickMatch[1] : null;
204
+
205
+ return {
206
+ lineNumber,
207
+ textContent,
208
+ placeholder,
209
+ name,
210
+ ariaLabel,
211
+ handlerName,
212
+ rawElement: elementStr.substring(0, 80) + (elementStr.length > 80 ? '...' : ''),
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Generate a suggested name for the element
218
+ */
219
+ function suggestName(element, info) {
220
+ // Priority: textContent > placeholder > name > ariaLabel > handlerName > generic
221
+ let suggestion = null;
222
+
223
+ if (info.textContent) {
224
+ suggestion = slugify(info.textContent);
225
+ } else if (info.placeholder) {
226
+ suggestion = slugify(info.placeholder) + '-input';
227
+ } else if (info.name) {
228
+ suggestion = slugify(info.name) + '-input';
229
+ } else if (info.ariaLabel) {
230
+ suggestion = slugify(info.ariaLabel);
231
+ } else if (info.handlerName) {
232
+ // Convert handleSubmit to submit, handleDeleteUser to delete-user
233
+ suggestion = slugify(info.handlerName.replace(/^handle/, ''));
234
+ }
235
+
236
+ // Fallback to generic name
237
+ if (!suggestion) {
238
+ suggestion = element + '-element';
239
+ }
240
+
241
+ // Append element type suffix if not present
242
+ if (element === 'button' && !suggestion.includes('button')) {
243
+ suggestion += '-button';
244
+ } else if (element === 'input' && !suggestion.includes('input')) {
245
+ suggestion += '-input';
246
+ } else if (element === 'form' && !suggestion.includes('form')) {
247
+ suggestion += '-form';
248
+ }
249
+
250
+ return suggestion;
251
+ }
252
+
253
+ /**
254
+ * Slugify a string for use as element name
255
+ */
256
+ function slugify(str) {
257
+ return str
258
+ .toLowerCase()
259
+ .replace(/[^a-z0-9]+/g, '-')
260
+ .replace(/^-|-$/g, '')
261
+ .substring(0, 30); // Limit length
262
+ }
263
+
264
+ /**
265
+ * Validate a single component file
266
+ */
267
+ function validateFile(filePath, repoDir) {
268
+ const content = fs.readFileSync(filePath, 'utf8');
269
+ const issues = [];
270
+ const relativePath = path.relative(repoDir, filePath);
271
+
272
+ // Check if file has 'use client' directive (only validate client components)
273
+ const isClientComponent = content.includes("'use client'") || content.includes('"use client"');
274
+
275
+ // Find interactive elements with proper brace handling
276
+ for (const element of INTERACTIVE_ELEMENTS) {
277
+ const matches = findElementMatches(content, element);
278
+
279
+ for (const match of matches) {
280
+ const attributes = match.attributes;
281
+ const matchIndex = match.index;
282
+
283
+ // Skip if already has data-lux
284
+ if (/data-lux/.test(attributes)) {
285
+ continue;
286
+ }
287
+
288
+ // Skip hidden inputs
289
+ if (element === 'input' && /type=["']hidden["']/.test(attributes)) {
290
+ continue;
291
+ }
292
+
293
+ // Skip if has spread props (can't determine)
294
+ if (/\{\s*\.\.\.\w+/.test(attributes)) {
295
+ continue;
296
+ }
297
+
298
+ // Skip if inside .map() callback
299
+ if (isInsideMapCallback(content, matchIndex)) {
300
+ continue;
301
+ }
302
+
303
+ const info = extractElementInfo(content, match.fullMatch, matchIndex);
304
+ const suggestedName = suggestName(element, info);
305
+
306
+ issues.push({
307
+ file: relativePath,
308
+ line: info.lineNumber,
309
+ element,
310
+ rawElement: info.rawElement,
311
+ suggestedName,
312
+ textContent: info.textContent,
313
+ placeholder: info.placeholder,
314
+ });
315
+ }
316
+ }
317
+
318
+ // Also check for any element with interactive handlers
319
+ for (const handler of INTERACTIVE_HANDLERS) {
320
+ const matches = findElementsWithHandler(content, handler);
321
+
322
+ for (const match of matches) {
323
+ const element = match.element;
324
+ const attributes = match.attributes;
325
+ const matchIndex = match.index;
326
+
327
+ // Skip if it's already an interactive element we checked
328
+ if (INTERACTIVE_ELEMENTS.includes(element)) {
329
+ continue;
330
+ }
331
+
332
+ // Skip if already has data-lux
333
+ if (/data-lux/.test(attributes)) {
334
+ continue;
335
+ }
336
+
337
+ // Skip if has spread props
338
+ if (/\{\s*\.\.\.\w+/.test(attributes)) {
339
+ continue;
340
+ }
341
+
342
+ // Skip if inside .map() callback
343
+ if (isInsideMapCallback(content, matchIndex)) {
344
+ continue;
345
+ }
346
+
347
+ const info = extractElementInfo(content, match.fullMatch, matchIndex);
348
+ const suggestedName = suggestName(element, info);
349
+
350
+ // Avoid duplicates
351
+ const isDuplicate = issues.some(
352
+ (issue) => issue.file === relativePath && issue.line === info.lineNumber
353
+ );
354
+ if (isDuplicate) continue;
355
+
356
+ issues.push({
357
+ file: relativePath,
358
+ line: info.lineNumber,
359
+ element,
360
+ rawElement: info.rawElement,
361
+ suggestedName,
362
+ handler,
363
+ });
364
+ }
365
+ }
366
+
367
+ return issues;
368
+ }
369
+
370
+ /**
371
+ * Validate an interface
372
+ */
373
+ function validateInterface(interfaceId, interfacesDir) {
374
+ const interfaceDir = path.join(interfacesDir, interfaceId);
375
+ const repoDir = path.join(interfaceDir, 'repo');
376
+
377
+ if (!fs.existsSync(repoDir)) {
378
+ return { interfaceId, error: 'Interface repo not found', issues: [] };
379
+ }
380
+
381
+ // Find all component files
382
+ const componentFiles = findComponentFiles(repoDir);
383
+ const allIssues = [];
384
+
385
+ for (const file of componentFiles) {
386
+ const issues = validateFile(file, repoDir);
387
+ allIssues.push(...issues);
388
+ }
389
+
390
+ return { interfaceId, issues: allIssues };
391
+ }
392
+
393
+ /**
394
+ * List all interfaces in the project
395
+ */
396
+ function listInterfaces(interfacesDir) {
397
+ if (!fs.existsSync(interfacesDir)) return [];
398
+
399
+ return fs.readdirSync(interfacesDir, { withFileTypes: true })
400
+ .filter((d) => d.isDirectory())
401
+ .map((d) => d.name);
402
+ }
403
+
404
+ /**
405
+ * Format and print results
406
+ */
407
+ function printResults(results, outputJson = false) {
408
+ if (outputJson) {
409
+ console.log(JSON.stringify(results, null, 2));
410
+ return;
411
+ }
412
+
413
+ let totalIssues = 0;
414
+
415
+ for (const result of results) {
416
+ if (result.error) {
417
+ warn(`${result.interfaceId}: ${result.error}`);
418
+ continue;
419
+ }
420
+
421
+ if (result.issues.length === 0) continue;
422
+
423
+ totalIssues += result.issues.length;
424
+
425
+ console.log('');
426
+ console.log(chalk.bold(`Interface: ${result.interfaceId}`));
427
+ console.log(chalk.gray('─'.repeat(50)));
428
+
429
+ for (const issue of result.issues) {
430
+ console.log('');
431
+ console.log(chalk.yellow(` ${issue.file}:${issue.line}`));
432
+ console.log(chalk.gray(` ${issue.rawElement}`));
433
+ console.log(chalk.cyan(` Suggested: data-lux={lux('${issue.suggestedName}')}`));
434
+ }
435
+ }
436
+
437
+ console.log('');
438
+
439
+ if (totalIssues === 0) {
440
+ success('All interactive elements have data-lux attributes!');
441
+ return { valid: true, totalIssues: 0 };
442
+ } else {
443
+ console.log(chalk.red(`\n❌ ${totalIssues} element(s) missing data-lux attributes`));
444
+ console.log('');
445
+ console.log(chalk.gray('To fix, add data-lux attributes using the createLux helper:'));
446
+ console.log(chalk.gray(" 1. import { createLux } from '@/lib/lux';"));
447
+ console.log(chalk.gray(" 2. const lux = createLux('ComponentName');"));
448
+ console.log(chalk.gray(" 3. <button data-lux={lux('button-name')}>..."));
449
+ console.log('');
450
+ return { valid: false, totalIssues };
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Main handler for validate-data-lux command
456
+ */
457
+ async function handleValidateDataLux(args) {
458
+ const interfaceId = args[0];
459
+ const outputJson = args.includes('--json');
460
+
461
+ // Get interfaces directory
462
+ const interfacesDir = getInterfacesDir();
463
+ if (!interfacesDir) {
464
+ error('Could not determine interfaces directory. Are you logged in?');
465
+ return;
466
+ }
467
+
468
+ if (!fs.existsSync(interfacesDir)) {
469
+ error(`Interfaces directory not found: ${interfacesDir}`);
470
+ return;
471
+ }
472
+
473
+ let results = [];
474
+
475
+ if (interfaceId && interfaceId !== '--json') {
476
+ // Validate specific interface
477
+ results.push(validateInterface(interfaceId, interfacesDir));
478
+ } else {
479
+ // Validate all interfaces
480
+ const interfaces = listInterfaces(interfacesDir);
481
+
482
+ if (interfaces.length === 0) {
483
+ info('No interfaces found in this project.');
484
+ return;
485
+ }
486
+
487
+ console.log(chalk.gray(`Validating ${interfaces.length} interface(s)...`));
488
+
489
+ for (const iface of interfaces) {
490
+ results.push(validateInterface(iface, interfacesDir));
491
+ }
492
+ }
493
+
494
+ const summary = printResults(results, outputJson);
495
+
496
+ // Exit with error code if issues found (useful for CI)
497
+ if (summary && !summary.valid) {
498
+ process.exit(1);
499
+ }
500
+ }
501
+
502
+ module.exports = { handleValidateDataLux };