spec-up-t-healthcheck 1.0.0 → 1.1.1

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.
@@ -233,6 +233,8 @@ export class HealthCheckRegistry {
233
233
  *
234
234
  * This method dynamically imports health check modules and registers them
235
235
  * with the registry. It's designed to work with the standard module structure.
236
+ *
237
+ * In browser environments, Node.js-only checks (like link-checker) are skipped.
236
238
  */
237
239
  async autoDiscover() {
238
240
  if (this.autoDiscovered) {
@@ -240,18 +242,35 @@ export class HealthCheckRegistry {
240
242
  }
241
243
 
242
244
  try {
245
+ // Detect if we're in a browser environment
246
+ const isBrowser = typeof window !== 'undefined' && typeof process === 'undefined';
247
+
243
248
  // Import the built-in health checks
244
249
  const packageJsonModule = await import('./checks/package-json.js');
245
250
  const specFilesModule = await import('./checks/spec-files.js');
246
251
  const specsJsonModule = await import('./checks/specsjson.js');
247
252
  const externalSpecsUrlsModule = await import('./checks/external-specs-urls.js');
248
253
  const gitignoreModule = await import('./checks/gitignore.js');
254
+ const specDirectoryAndFilesModule = await import('./checks/spec-directory-and-files.js');
255
+ const consoleMessagesModule = await import('./checks/console-messages.js');
256
+ const markdownTablesModule = await import('./checks/markdown-tables.js');
257
+
258
+ // Only import link-checker in Node.js environments (not browsers)
259
+ // Link checker requires linkinator which uses Node.js streams
260
+ let linkCheckerModule = null;
261
+ if (!isBrowser) {
262
+ try {
263
+ linkCheckerModule = await import('./checks/link-checker.js');
264
+ } catch (error) {
265
+ console.warn('Link checker module could not be loaded (Node.js only):', error.message);
266
+ }
267
+ }
249
268
 
250
269
  // Register package.json check
251
270
  if (packageJsonModule.checkPackageJson && packageJsonModule.CHECK_ID) {
252
271
  this.register({
253
272
  id: packageJsonModule.CHECK_ID,
254
- name: packageJsonModule.CHECK_NAME || 'Package.json Check',
273
+ name: packageJsonModule.CHECK_NAME || 'package.json Check',
255
274
  description: packageJsonModule.CHECK_DESCRIPTION || 'Validates package.json file',
256
275
  checkFunction: packageJsonModule.checkPackageJson,
257
276
  category: 'configuration',
@@ -275,7 +294,7 @@ export class HealthCheckRegistry {
275
294
  if (specsJsonModule.checkSpecsJson && specsJsonModule.CHECK_ID) {
276
295
  this.register({
277
296
  id: specsJsonModule.CHECK_ID,
278
- name: specsJsonModule.CHECK_NAME || 'Specs.json Check',
297
+ name: specsJsonModule.CHECK_NAME || 'specs.json Check',
279
298
  description: specsJsonModule.CHECK_DESCRIPTION || 'Validates specs.json file',
280
299
  checkFunction: specsJsonModule.checkSpecsJson,
281
300
  category: 'configuration',
@@ -299,7 +318,7 @@ export class HealthCheckRegistry {
299
318
  if (gitignoreModule.checkGitignore && gitignoreModule.CHECK_ID) {
300
319
  this.register({
301
320
  id: gitignoreModule.CHECK_ID,
302
- name: gitignoreModule.CHECK_NAME || '.gitignore Validation',
321
+ name: gitignoreModule.CHECK_NAME || '.gitignore validation',
303
322
  description: gitignoreModule.CHECK_DESCRIPTION || 'Validates .gitignore file',
304
323
  checkFunction: gitignoreModule.checkGitignore,
305
324
  category: 'configuration',
@@ -307,6 +326,54 @@ export class HealthCheckRegistry {
307
326
  });
308
327
  }
309
328
 
329
+ // Register spec directory and files check
330
+ if (specDirectoryAndFilesModule.checkSpecDirectoryAndFiles && specDirectoryAndFilesModule.CHECK_ID) {
331
+ this.register({
332
+ id: specDirectoryAndFilesModule.CHECK_ID,
333
+ name: specDirectoryAndFilesModule.CHECK_NAME || 'Spec Directory and Files Check',
334
+ description: specDirectoryAndFilesModule.CHECK_DESCRIPTION || 'Validates spec directories and required files',
335
+ checkFunction: specDirectoryAndFilesModule.checkSpecDirectoryAndFiles,
336
+ category: 'content',
337
+ priority: 16 // After specs.json validation, before general spec files discovery
338
+ });
339
+ }
340
+
341
+ // Register console messages check
342
+ if (consoleMessagesModule.checkConsoleMessages) {
343
+ this.register({
344
+ id: consoleMessagesModule.checkConsoleMessagesMetadata?.id || 'console-messages',
345
+ name: consoleMessagesModule.checkConsoleMessagesMetadata?.name || 'Console Messages',
346
+ description: consoleMessagesModule.checkConsoleMessagesMetadata?.description || 'Analyzes console output from operations',
347
+ checkFunction: consoleMessagesModule.checkConsoleMessages,
348
+ category: consoleMessagesModule.checkConsoleMessagesMetadata?.category || 'operations',
349
+ priority: 50 // Run after most other checks as it analyzes operation results
350
+ });
351
+ }
352
+
353
+ // Register markdown tables check
354
+ if (markdownTablesModule.checkMarkdownTables && markdownTablesModule.CHECK_ID) {
355
+ this.register({
356
+ id: markdownTablesModule.CHECK_ID,
357
+ name: markdownTablesModule.CHECK_NAME || 'Markdown Table Validation',
358
+ description: markdownTablesModule.CHECK_DESCRIPTION || 'Validates markdown table structure and syntax',
359
+ checkFunction: markdownTablesModule.checkMarkdownTables,
360
+ category: 'content',
361
+ priority: 25 // Run after spec files discovery, before external validations
362
+ });
363
+ }
364
+
365
+ // Register link checker (Node.js only - not available in browsers)
366
+ if (linkCheckerModule && linkCheckerModule.checkLinks && linkCheckerModule.CHECK_ID) {
367
+ this.register({
368
+ id: linkCheckerModule.CHECK_ID,
369
+ name: linkCheckerModule.CHECK_NAME || 'Link Checker',
370
+ description: linkCheckerModule.CHECK_DESCRIPTION || 'Validates all links in the generated HTML output',
371
+ checkFunction: linkCheckerModule.checkLinks,
372
+ category: 'quality',
373
+ priority: 40 // Run after all file validations complete
374
+ });
375
+ }
376
+
310
377
  this.autoDiscovered = true;
311
378
  } catch (error) {
312
379
  console.warn('Failed to auto-discover some health checks:', error.message);
@@ -37,6 +37,10 @@ import { checkSpecFiles } from './checks/spec-files.js';
37
37
  import { checkSpecsJson } from './checks/specsjson.js';
38
38
  import { checkExternalSpecsUrls } from './checks/external-specs-urls.js';
39
39
  import { checkGitignore } from './checks/gitignore.js';
40
+ import { checkSpecDirectoryAndFiles } from './checks/spec-directory-and-files.js';
41
+ import { checkConsoleMessages } from './checks/console-messages.js';
42
+ // Note: checkLinks (link-checker) is NOT imported here to avoid Node.js dependencies in browsers
43
+ // It is dynamically imported by the health-check-registry during auto-discovery in Node.js environments only
40
44
 
41
45
  // Re-export types for backward compatibility
42
46
  /**
@@ -74,9 +78,6 @@ export function createHealthCheckResult(check, status, message, details = {}) {
74
78
  return utilsCreateHealthCheckResult(check, status, message, details);
75
79
  }
76
80
 
77
- // Re-export the actual check functions directly
78
- export { checkPackageJson, checkSpecFiles, checkSpecsJson, checkExternalSpecsUrls, checkGitignore };
79
-
80
81
  /**
81
82
  * Runs a comprehensive health check suite on a specification repository.
82
83
  *
@@ -123,6 +124,15 @@ export async function runHealthChecks(provider, options = {}) {
123
124
 
124
125
  // Re-export modular components for advanced usage
125
126
  export {
127
+ // Individual check functions
128
+ checkPackageJson,
129
+ checkSpecFiles,
130
+ checkSpecsJson,
131
+ checkExternalSpecsUrls,
132
+ checkGitignore,
133
+ checkSpecDirectoryAndFiles,
134
+ checkConsoleMessages,
135
+
126
136
  // Utils
127
137
  calculateSummary,
128
138
  isValidHealthCheckResult,
@@ -7,9 +7,23 @@
7
7
  * responsive design elements.
8
8
  *
9
9
  * @author spec-up-t-healthcheck
10
-
11
10
  */
12
11
 
12
+ // Import shared formatting functions
13
+ import {
14
+ formatResultDetails,
15
+ getStatusDisplay,
16
+ getRowClass,
17
+ escapeHtml,
18
+ linkifyUrls
19
+ } from './formatters/result-details-formatter.js';
20
+
21
+ // Import console message formatting functions
22
+ import {
23
+ formatConsoleMessageList,
24
+ formatConsoleMessagesTable
25
+ } from './formatters/result-details-formatter.js';
26
+
13
27
  /**
14
28
  * Generates a complete HTML report from health check results.
15
29
  *
@@ -147,7 +161,7 @@ export function generateHtmlReport(healthCheckOutput, options = {}) {
147
161
  }
148
162
  .table td {
149
163
  padding: 0.75rem;
150
- vertical-align: middle;
164
+ vertical-align: top;
151
165
  }
152
166
  .table-striped tbody tr:nth-of-type(odd) {
153
167
  background-color: rgba(0, 0, 0, 0.02);
@@ -181,17 +195,22 @@ export function generateHtmlReport(healthCheckOutput, options = {}) {
181
195
  .text-warning {
182
196
  color: #fd7e14 !important;
183
197
  }
198
+ .text-info {
199
+ color: #17a2b8 !important;
200
+ }
184
201
  .text-muted {
185
202
  color: #6c757d !important;
186
203
  }
187
204
  .detail-errors ul,
188
205
  .detail-warnings ul,
206
+ .detail-info ul,
189
207
  .detail-success ul {
190
208
  padding-left: 1.25rem;
191
209
  margin-top: 0.5rem;
192
210
  }
193
211
  .detail-errors li,
194
212
  .detail-warnings li,
213
+ .detail-info li,
195
214
  .detail-success li {
196
215
  margin-bottom: 0.25rem;
197
216
  }
@@ -295,7 +314,7 @@ function generateSummarySection(summary, showPassingByDefault) {
295
314
  Health Check Summary
296
315
  </h5>
297
316
  <div class="filter-toggle form-check form-switch">
298
- <input class="form-check-input" type="checkbox" id="togglePassingChecks" ${showPassingByDefault ? 'checked' : ''}>
317
+ <input class="form-check-input" type="checkbox" id="togglePassingChecks">
299
318
  <label class="form-check-label" for="togglePassingChecks">
300
319
  Show passing checks
301
320
  </label>
@@ -362,6 +381,12 @@ function generateResultsSection(results) {
362
381
  }
363
382
 
364
383
  const resultsHtml = results.map((result, index) => {
384
+ // Special handling for console-messages check: split into two rows
385
+ if (result.check === 'console-messages' && result.details && result.details.analysis) {
386
+ return generateConsoleMessagesRows(result);
387
+ }
388
+
389
+ // Regular handling for other checks
365
390
  const { statusClass, statusIcon, statusText } = getStatusDisplay(result);
366
391
  const rowClass = getRowClass(result);
367
392
 
@@ -407,6 +432,117 @@ function generateResultsSection(results) {
407
432
  </div>`;
408
433
  }
409
434
 
435
+ /**
436
+ * Generates separate rows for console-messages check:
437
+ * 1. Fail row with errors (if any)
438
+ * 2. Warning row with warnings (if any)
439
+ * 3. Pass row with successful operations
440
+ *
441
+ * @param {import('./health-checker.js').HealthCheckResult} result - Console messages result
442
+ * @returns {string} HTML string with table rows
443
+ */
444
+ function generateConsoleMessagesRows(result) {
445
+ const { analysis, errors, warnings, allMessages } = result.details;
446
+
447
+ let html = '';
448
+
449
+ // Row 1: Errors (if any)
450
+ if (errors && errors.length > 0) {
451
+ const errorDetailsHtml = formatConsoleMessageList('Errors', errors, 'danger');
452
+ const errorNote = result.details.errorsNote ? `<div class="mt-1"><small class="text-muted">${escapeHtml(result.details.errorsNote)}</small></div>` : '';
453
+
454
+ html += `<tr data-status="fail" class="check-row table-danger">
455
+ <td class="text-danger status-badge">
456
+ <i class="bi bi-x-circle-fill status-icon"></i>
457
+ <span>Fail</span>
458
+ </td>
459
+ <td>Terms & Definitions (errors)</td>
460
+ <td>
461
+ Found ${errors.length} error(s) in console output
462
+ ${errorDetailsHtml}
463
+ ${errorNote}
464
+ </td>
465
+ </tr>`;
466
+ }
467
+
468
+ // Row 2: Warnings (if any)
469
+ if (warnings && warnings.length > 0) {
470
+ const warningDetailsHtml = formatConsoleMessageList('Warnings', warnings, 'warning');
471
+ const warningNote = result.details.warningsNote ? `<div class="mt-1"><small class="text-muted">${escapeHtml(result.details.warningsNote)}</small></div>` : '';
472
+
473
+ html += `<tr data-status="warn" class="check-row table-warning">
474
+ <td class="text-warning status-badge">
475
+ <i class="bi bi-exclamation-triangle-fill status-icon"></i>
476
+ <span>Warning</span>
477
+ </td>
478
+ <td>Terms & Definitions (warnings)</td>
479
+ <td>
480
+ Found ${warnings.length} warning(s) in console output
481
+ ${warningDetailsHtml}
482
+ ${warningNote}
483
+ </td>
484
+ </tr>`;
485
+ }
486
+
487
+ // Row 3: Successful operations (pass)
488
+ if (allMessages && allMessages.length > 0) {
489
+ // Filter out error and warning messages AND exclude separator, process, and info types
490
+ const valuableMessages = allMessages.filter(m =>
491
+ m.type !== 'error' &&
492
+ m.type !== 'warn' &&
493
+ m.type !== 'separator' &&
494
+ m.type !== 'process' &&
495
+ m.type !== 'info'
496
+ );
497
+
498
+ if (valuableMessages.length === 0) {
499
+ // If no valuable messages after filtering, don't show the Pass row
500
+ return html;
501
+ }
502
+
503
+ const valuableCount = valuableMessages.length;
504
+
505
+ // Count by type for display (only valuable types)
506
+ const typeCounts = {};
507
+ valuableMessages.forEach(m => {
508
+ typeCounts[m.type] = (typeCounts[m.type] || 0) + 1;
509
+ });
510
+
511
+ const typesList = Object.entries(typeCounts)
512
+ .map(([type, count]) => `${count} ${type}`)
513
+ .join(', ');
514
+
515
+ // Create modified details object with only valuable messages
516
+ const filteredDetails = {
517
+ ...result.details,
518
+ allMessages: valuableMessages,
519
+ analysis: {
520
+ ...result.details.analysis,
521
+ totalMessages: valuableMessages.length,
522
+ errorCount: 0, // No errors in valuable messages (they're filtered out)
523
+ warningCount: 0, // No warnings in valuable messages (they're filtered out)
524
+ successCount: valuableMessages.filter(m => m.type === 'success').length
525
+ }
526
+ };
527
+
528
+ const allMessagesHtml = formatConsoleMessagesTable(filteredDetails);
529
+
530
+ html += `<tr data-status="pass" class="check-row table-success">
531
+ <td class="text-success status-badge">
532
+ <i class="bi bi-check-circle-fill status-icon"></i>
533
+ <span>Pass</span>
534
+ </td>
535
+ <td>Terms & Definitions (operations)</td>
536
+ <td>
537
+ ${valuableCount} successful operations (${typesList})
538
+ ${allMessagesHtml}
539
+ </td>
540
+ </tr>`;
541
+ }
542
+
543
+ return html;
544
+ }
545
+
410
546
  /**
411
547
  * Generates the JavaScript section for interactive functionality.
412
548
  *
@@ -458,169 +594,4 @@ function generateScriptsSection() {
458
594
  // Initialize the filter state
459
595
  document.getElementById('togglePassingChecks').dispatchEvent(new Event('change'));
460
596
  </script>`;
461
- }
462
-
463
- /**
464
- * Gets the appropriate display properties for a health check result status.
465
- *
466
- * @param {import('./health-checker.js').HealthCheckResult} result - The health check result
467
- * @returns {Object} Object containing statusClass, statusIcon, and statusText
468
- */
469
- function getStatusDisplay(result) {
470
- switch (result.status) {
471
- case 'pass':
472
- return {
473
- statusClass: 'text-success',
474
- statusIcon: 'bi-check-circle-fill',
475
- statusText: 'Pass'
476
- };
477
- case 'fail':
478
- return {
479
- statusClass: 'text-danger',
480
- statusIcon: 'bi-x-circle-fill',
481
- statusText: 'Fail'
482
- };
483
- case 'warn':
484
- return {
485
- statusClass: 'text-warning',
486
- statusIcon: 'bi-exclamation-triangle-fill',
487
- statusText: 'Warning'
488
- };
489
- case 'skip':
490
- return {
491
- statusClass: 'text-muted',
492
- statusIcon: 'bi-dash-circle',
493
- statusText: 'Skipped'
494
- };
495
- default:
496
- return {
497
- statusClass: 'text-secondary',
498
- statusIcon: 'bi-question-circle',
499
- statusText: 'Unknown'
500
- };
501
- }
502
- }
503
-
504
- /**
505
- * Gets the appropriate CSS class for a table row based on the result status.
506
- *
507
- * @param {import('./health-checker.js').HealthCheckResult} result - The health check result
508
- * @returns {string} CSS class name
509
- */
510
- function getRowClass(result) {
511
- switch (result.status) {
512
- case 'fail':
513
- return 'table-danger';
514
- case 'warn':
515
- return 'table-warning';
516
- case 'pass':
517
- return 'table-success';
518
- default:
519
- return '';
520
- }
521
- }
522
-
523
- /**
524
- * Formats result details into HTML.
525
- *
526
- * @param {Object} details - The details object from a health check result
527
- * @returns {string} Formatted HTML string
528
- */
529
- function formatResultDetails(details) {
530
- let html = '';
531
-
532
- // Display errors array with clickable URLs
533
- if (details.errors && details.errors.length > 0) {
534
- html += `<div class="mt-2 detail-errors"><strong class="text-danger">Errors:</strong><ul class="mb-0 mt-1">`;
535
- details.errors.forEach(error => {
536
- html += `<li class="text-danger">${linkifyUrls(error)}</li>`;
537
- });
538
- html += `</ul></div>`;
539
- }
540
-
541
- // Display warnings array with clickable URLs
542
- if (details.warnings && details.warnings.length > 0) {
543
- html += `<div class="mt-2 detail-warnings"><strong class="text-warning">Warnings:</strong><ul class="mb-0 mt-1">`;
544
- details.warnings.forEach(warning => {
545
- html += `<li class="text-warning">${linkifyUrls(warning)}</li>`;
546
- });
547
- html += `</ul></div>`;
548
- }
549
-
550
- // Display success messages array with clickable URLs
551
- // Add detail-success class so these can be hidden when "Show passing checks" is disabled
552
- if (details.success && details.success.length > 0) {
553
- html += `<div class="mt-2 detail-success"><strong class="text-success">Success:</strong><ul class="mb-0 mt-1">`;
554
- details.success.forEach(success => {
555
- html += `<li class="text-success">${linkifyUrls(success)}</li>`;
556
- });
557
- html += `</ul></div>`;
558
- }
559
-
560
- // Display missing fields (existing functionality)
561
- if (details.missingFields && details.missingFields.length > 0) {
562
- html += `<br><small class="text-muted">Missing fields: ${details.missingFields.map(escapeHtml).join(', ')}</small>`;
563
- }
564
-
565
- // Display count (existing functionality)
566
- if (details.count !== undefined) {
567
- html += `<br><small class="text-muted">Files found: ${details.count}</small>`;
568
- }
569
-
570
- // Display package data (existing functionality)
571
- if (details.packageData) {
572
- html += `<br><small class="text-muted">Package: ${escapeHtml(details.packageData.name)}@${escapeHtml(details.packageData.version)}</small>`;
573
- }
574
-
575
- return html;
576
- }
577
-
578
- /**
579
- * Escapes HTML special characters to prevent XSS attacks.
580
- *
581
- * @param {string} text - Text to escape
582
- * @returns {string} HTML-escaped text
583
- */
584
- function escapeHtml(text) {
585
- if (typeof text !== 'string') {
586
- return String(text);
587
- }
588
-
589
- const map = {
590
- '&': '&amp;',
591
- '<': '&lt;',
592
- '>': '&gt;',
593
- '"': '&quot;',
594
- "'": '&#039;'
595
- };
596
-
597
- return text.replace(/[&<>"']/g, (m) => map[m]);
598
- }
599
-
600
- /**
601
- * Converts URLs in text to clickable links that open in a new tab.
602
- * The text is first escaped for HTML safety, then URLs are converted to links.
603
- *
604
- * @param {string} text - Text potentially containing URLs
605
- * @returns {string} HTML string with clickable links
606
- */
607
- function linkifyUrls(text) {
608
- if (typeof text !== 'string') {
609
- return escapeHtml(String(text));
610
- }
611
-
612
- // First escape the text for HTML safety
613
- const escaped = escapeHtml(text);
614
-
615
- // URL regex pattern that matches http://, https://, and www. URLs
616
- const urlPattern = /(https?:\/\/[^\s<>"]+|www\.[^\s<>"]+)/g;
617
-
618
- // Replace URLs with clickable links
619
- return escaped.replace(urlPattern, (url) => {
620
- // Ensure the URL has a protocol for the href attribute
621
- const href = url.startsWith('http') ? url : `https://${url}`;
622
-
623
- // Create an anchor tag that opens in a new tab with security attributes
624
- return `<a href="${href}" target="_blank" rel="noopener noreferrer" class="text-decoration-underline">${url}</a>`;
625
- });
626
597
  }
package/lib/index.js CHANGED
@@ -14,7 +14,7 @@
14
14
  export { createProvider, createLocalProvider } from './providers.js';
15
15
 
16
16
  // Re-export health checking functionality
17
- export { runHealthChecks, createHealthCheckResult, checkPackageJson, checkSpecFiles, checkSpecsJson, checkExternalSpecsUrls, checkGitignore } from './health-checker.js';
17
+ export { runHealthChecks, createHealthCheckResult, checkPackageJson, checkSpecFiles, checkSpecsJson, checkExternalSpecsUrls, checkGitignore, checkSpecDirectoryAndFiles, checkConsoleMessages } from './health-checker.js';
18
18
 
19
19
  // Re-export formatting functionality
20
20
  export { formatResultsAsText, formatResultsAsJson, formatResultsAsHtml } from './formatters.js';
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @fileoverview Browser-compatible providers module
3
+ *
4
+ * This is a browser-safe version of providers.js that doesn't import Node.js modules.
5
+ * It only exports the createProvider function interface without the Node.js-specific
6
+ * LocalFileProvider.
7
+ *
8
+ * In browser environments, providers must be created by the consuming application
9
+ * (e.g., GitHubUi creates a GitHub API provider).
10
+ *
11
+ * @author spec-up-t-healthcheck
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} FileEntry
16
+ * @property {string} name - The name of the file or directory
17
+ * @property {string} path - The relative path from the repository root
18
+ * @property {boolean} isDirectory - Whether this entry is a directory
19
+ * @property {boolean} isFile - Whether this entry is a file
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} Provider
24
+ * @property {string} type - The type of provider ('local', 'remote', etc.)
25
+ * @property {string} repoPath - The base path or URL for the repository
26
+ * @property {function(string): Promise<string>} readFile - Read a file and return its content
27
+ * @property {function(string): Promise<boolean>} fileExists - Check if a file exists
28
+ * @property {function(string): Promise<FileEntry[]>} listFiles - List files in a directory
29
+ */
30
+
31
+ /**
32
+ * Creates a provider based on the given configuration.
33
+ *
34
+ * In browser environments, only custom providers are supported.
35
+ * You must pass a pre-configured provider object.
36
+ *
37
+ * @param {Object|string} config - Provider configuration or custom provider
38
+ * @returns {Provider} The configured provider
39
+ * @throws {Error} If trying to create a local file provider in browser
40
+ *
41
+ * @example
42
+ * ```javascript
43
+ * // Browser: Pass a custom provider (e.g., GitHub API provider)
44
+ * const provider = createProvider({
45
+ * type: 'github',
46
+ * repoPath: 'owner/repo/branch',
47
+ * readFile: async (path) => { ... },
48
+ * fileExists: async (path) => { ... },
49
+ * listFiles: async (dir) => { ... }
50
+ * });
51
+ * ```
52
+ */
53
+ export function createProvider(config) {
54
+ // If config is already a provider object with required methods, return it
55
+ if (typeof config === 'object' &&
56
+ config.readFile &&
57
+ config.fileExists) {
58
+ return config;
59
+ }
60
+
61
+ // If config is a string (file path), this is not supported in browser
62
+ if (typeof config === 'string') {
63
+ throw new Error(
64
+ 'Local file system providers are not available in browser environments. ' +
65
+ 'Please provide a custom provider object with readFile, fileExists, and listFiles methods.'
66
+ );
67
+ }
68
+
69
+ throw new Error(
70
+ 'Invalid provider configuration. In browser environments, you must provide a ' +
71
+ 'custom provider object with readFile, fileExists, and listFiles methods.'
72
+ );
73
+ }
package/lib/providers.js CHANGED
@@ -115,6 +115,30 @@ export function createLocalProvider(repoPath) {
115
115
  }
116
116
  },
117
117
 
118
+ /**
119
+ * Checks whether a directory exists in the local filesystem.
120
+ *
121
+ * @param {string} dirPath - The relative path to the directory from the repository root
122
+ * @returns {Promise<boolean>} True if the directory exists, false otherwise
123
+ *
124
+ * @example
125
+ * ```javascript
126
+ * const exists = await provider.directoryExists('spec');
127
+ * if (exists) {
128
+ * console.log('Spec directory found');
129
+ * }
130
+ * ```
131
+ */
132
+ async directoryExists(dirPath) {
133
+ const fullPath = path.join(repoPath, dirPath);
134
+ try {
135
+ const stats = await fs.stat(fullPath);
136
+ return stats.isDirectory();
137
+ } catch {
138
+ return false;
139
+ }
140
+ },
141
+
118
142
  /**
119
143
  * Lists all files and directories in the specified directory.
120
144
  *
package/lib/web.js CHANGED
@@ -1,15 +1,19 @@
1
1
  /**
2
2
  * @fileoverview Browser-compatible entry point for spec-up-t-healthcheck
3
3
  *
4
- * This module provides a browser-safe API that excludes Node.js-specific modules
5
- * like file-opener which depend on child_process. This allows the health check
6
- * functionality to work in browser environments like Vite.
4
+ * This module provides a browser-safe API that excludes Node.js-specific modules:
5
+ * - file-opener (depends on child_process)
6
+ * - link-checker (depends on linkinator with Node.js streams)
7
+ * - providers (uses providers-browser.js instead of providers.js)
8
+ *
9
+ * The auto-discovery system will automatically skip Node.js-only checks
10
+ * when running in a browser environment.
7
11
  *
8
12
  * @author spec-up-t-healthcheck
9
13
  */
10
14
 
11
- // Re-export provider functionality (browser-compatible)
12
- export { createProvider } from './providers.js';
15
+ // Re-export provider functionality (browser-compatible version without Node.js imports)
16
+ export { createProvider } from './providers-browser.js';
13
17
 
14
18
  // Re-export health checking functionality (browser-compatible)
15
19
  export {
@@ -28,7 +32,7 @@ export {
28
32
  } from './formatters.js';
29
33
 
30
34
  // Import functions for internal use
31
- import { createProvider } from './providers.js';
35
+ import { createProvider } from './providers-browser.js';
32
36
  import { runHealthChecks } from './health-checker.js';
33
37
 
34
38
  /**