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.
package/README.md CHANGED
@@ -138,6 +138,11 @@ The HTML format generates beautiful, interactive reports with:
138
138
 
139
139
  - **package-json** - Validates package.json structure and required fields
140
140
  - **spec-files** - Finds and validates specification markdown files
141
+ - **specs-json** - Validates specs.json configuration file
142
+ - **external-specs-urls** - Validates external specification URLs
143
+ - **gitignore** - Validates .gitignore file
144
+ - **spec-directory-and-files** - Validates spec directory structure
145
+ - **link-checker** - Validates all links in generated HTML output (uses linkinator)
141
146
 
142
147
  Use `spec-up-t-healthcheck list-checks` for the complete list.
143
148
 
@@ -0,0 +1,211 @@
1
+ /**
2
+ * @fileoverview Health check for console messages captured during spec-up-t operations
3
+ *
4
+ * This check reads the `.cache/console-messages.json` file generated by spec-up-t's
5
+ * message collector during menu operations [1] (render) and [4] (collect external references).
6
+ * It analyzes the captured messages for errors, warnings, and other issues.
7
+ *
8
+ * The check provides insights into:
9
+ * - Errors that occurred during operations
10
+ * - Warnings that may indicate potential issues
11
+ * - Operation success rate
12
+ * - Message statistics and patterns
13
+ *
14
+ * @module checks/console-messages
15
+ */
16
+
17
+ /**
18
+ * Analyzes console messages captured from spec-up-t operations
19
+ *
20
+ * This function reads the console messages JSON file and provides detailed
21
+ * analysis of any errors, warnings, or issues found during render or
22
+ * external reference collection operations.
23
+ *
24
+ * @param {Object} provider - File system provider for accessing project files
25
+ * @param {Object} [options={}] - Check options
26
+ * @param {boolean} [options.verbose=false] - Include all messages in details
27
+ * @returns {Promise<Object>} Health check result with console message analysis
28
+ *
29
+ * @example
30
+ * const result = await checkConsoleMessages(provider);
31
+ * console.log(`Status: ${result.status}`);
32
+ * console.log(`Errors found: ${result.details.errorCount}`);
33
+ */
34
+ export async function checkConsoleMessages(provider, options = {}) {
35
+ const checkName = 'console-messages';
36
+ const messagePath = '.cache/console-messages.json';
37
+
38
+ try {
39
+ // Check if the console messages file exists
40
+ const fileExists = await provider.fileExists(messagePath);
41
+
42
+ if (!fileExists) {
43
+ return {
44
+ check: checkName,
45
+ status: 'skip',
46
+ message: 'Console messages file not found. Run "npm run render" or "npm run collectExternalReferences" to generate it.',
47
+ timestamp: new Date().toISOString(),
48
+ details: {
49
+ path: messagePath,
50
+ fileExists: false,
51
+ suggestion: 'Console message collection is available after running menu operations [1] or [4].'
52
+ }
53
+ };
54
+ }
55
+
56
+ // Read and parse the console messages file
57
+ const content = await provider.readFile(messagePath);
58
+ let consoleData;
59
+
60
+ try {
61
+ consoleData = JSON.parse(content);
62
+ } catch (parseError) {
63
+ return {
64
+ check: checkName,
65
+ status: 'fail',
66
+ message: 'Failed to parse console messages file',
67
+ timestamp: new Date().toISOString(),
68
+ details: {
69
+ path: messagePath,
70
+ error: parseError.message,
71
+ fileExists: true
72
+ }
73
+ };
74
+ }
75
+
76
+ // Extract metadata and messages
77
+ const metadata = consoleData.metadata || {};
78
+ const messages = consoleData.messages || [];
79
+
80
+ // Analyze messages by type
81
+ const errorMessages = messages.filter(m => m.type === 'error');
82
+ const warningMessages = messages.filter(m => m.type === 'warn');
83
+ const successMessages = messages.filter(m => m.type === 'success');
84
+
85
+ // Determine status based on message analysis
86
+ let status;
87
+ let message;
88
+
89
+ if (errorMessages.length > 0) {
90
+ status = 'fail';
91
+ message = `Found ${errorMessages.length} error(s) in console output`;
92
+ } else if (warningMessages.length > 0) {
93
+ status = 'warn';
94
+ message = `Found ${warningMessages.length} warning(s) in console output`;
95
+ } else if (messages.length === 0) {
96
+ status = 'skip';
97
+ message = 'No console messages captured (file is empty)';
98
+ } else {
99
+ status = 'pass';
100
+ message = `Console output looks healthy (${successMessages.length} successful operations)`;
101
+ }
102
+
103
+ // Build detailed information
104
+ const details = {
105
+ path: messagePath,
106
+ fileExists: true,
107
+ metadata: {
108
+ generatedAt: metadata.generatedAt,
109
+ totalMessages: metadata.totalMessages || 0,
110
+ operations: metadata.operations || [],
111
+ messagesByType: metadata.messagesByType || {}
112
+ },
113
+ analysis: {
114
+ errorCount: errorMessages.length,
115
+ warningCount: warningMessages.length,
116
+ successCount: successMessages.length,
117
+ totalMessages: messages.length
118
+ },
119
+ // Include ALL errors (no truncation)
120
+ errors: errorMessages.map(m => ({
121
+ timestamp: m.timestamp,
122
+ message: m.message,
123
+ operation: m.operation,
124
+ additionalData: m.additionalData
125
+ })),
126
+ warnings: warningMessages.slice(0, 10).map(m => ({
127
+ timestamp: m.timestamp,
128
+ message: m.message,
129
+ operation: m.operation,
130
+ additionalData: m.additionalData
131
+ }))
132
+ };
133
+
134
+ // Include all messages for display in HTML report
135
+ // This allows users to see the complete console output in a table format
136
+ // Always include all messages (regardless of status) so users can see full context
137
+ details.allMessages = messages;
138
+
139
+ // Add truncation notice only for warnings (errors are never truncated)
140
+ if (warningMessages.length > 10) {
141
+ details.warningsNote = `Showing first 10 of ${warningMessages.length} warnings`;
142
+ }
143
+
144
+ return {
145
+ check: checkName,
146
+ status,
147
+ message,
148
+ timestamp: new Date().toISOString(),
149
+ details
150
+ };
151
+
152
+ } catch (error) {
153
+ return {
154
+ check: checkName,
155
+ status: 'fail',
156
+ message: `Failed to check console messages: ${error.message}`,
157
+ timestamp: new Date().toISOString(),
158
+ details: {
159
+ path: messagePath,
160
+ error: error.message,
161
+ stack: error.stack
162
+ }
163
+ };
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Get statistics from console messages without performing a full check
169
+ *
170
+ * This is a utility function for quickly accessing message statistics
171
+ * without running the full health check.
172
+ *
173
+ * @param {Object} provider - File system provider
174
+ * @returns {Promise<Object|null>} Statistics object or null if file doesn't exist
175
+ */
176
+ export async function getConsoleMessageStats(provider) {
177
+ const messagePath = '.cache/console-messages.json';
178
+
179
+ try {
180
+ const fileExists = await provider.fileExists(messagePath);
181
+ if (!fileExists) {
182
+ return null;
183
+ }
184
+
185
+ const content = await provider.readFile(messagePath);
186
+ const consoleData = JSON.parse(content);
187
+ const messages = consoleData.messages || [];
188
+
189
+ return {
190
+ total: messages.length,
191
+ errors: messages.filter(m => m.type === 'error').length,
192
+ warnings: messages.filter(m => m.type === 'warn').length,
193
+ successes: messages.filter(m => m.type === 'success').length,
194
+ operations: [...new Set(messages.map(m => m.operation).filter(Boolean))],
195
+ generatedAt: consoleData.metadata?.generatedAt
196
+ };
197
+ } catch (error) {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Check metadata for this health check
204
+ */
205
+ export const checkConsoleMessagesMetadata = {
206
+ id: 'console-messages',
207
+ name: 'Console Messages',
208
+ description: 'Analyzes console output captured during spec-up-t operations',
209
+ category: 'operations',
210
+ tags: ['console', 'messages', 'errors', 'warnings', 'operations']
211
+ };
@@ -42,6 +42,22 @@ const HTTP_TIMEOUT = 10000;
42
42
  */
43
43
  const MAX_REDIRECTS = 5;
44
44
 
45
+ /**
46
+ * Proxy URL for browser environments (to bypass CORS)
47
+ * Assumes proxy.php is in the public root directory
48
+ * @type {string}
49
+ */
50
+ const PROXY_URL = './proxy.php';
51
+
52
+ /**
53
+ * Detects if code is running in a browser environment
54
+ *
55
+ * @returns {boolean} True if running in browser
56
+ */
57
+ function isBrowserEnvironment() {
58
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
59
+ }
60
+
45
61
  /**
46
62
  * Validates that a string is a properly formatted URL
47
63
  *
@@ -181,6 +197,8 @@ async function checkUrlAccessibility(url, fieldName) {
181
197
 
182
198
  /**
183
199
  * Attempts to check URL accessibility using HEAD request
200
+ * In browser environments, attempts to use proxy to bypass CORS restrictions.
201
+ * Falls back gracefully if proxy is unavailable (e.g., in dev environment).
184
202
  *
185
203
  * @param {string} url - The URL to check
186
204
  * @param {string} fieldName - Name of the field being checked
@@ -188,13 +206,31 @@ async function checkUrlAccessibility(url, fieldName) {
188
206
  */
189
207
  async function attemptHeadRequest(url, fieldName) {
190
208
  try {
191
- const response = await axios.head(url, {
192
- timeout: HTTP_TIMEOUT,
193
- maxRedirects: MAX_REDIRECTS,
194
- validateStatus: (status) => status < 500
195
- });
196
-
197
- return createAccessibilityResult(response.status, fieldName);
209
+ const isBrowser = isBrowserEnvironment();
210
+
211
+ if (isBrowser) {
212
+ // In browser, try using proxy first
213
+ try {
214
+ const proxyUrl = `${PROXY_URL}?url=${encodeURIComponent(url)}`;
215
+ const response = await axios.head(proxyUrl, {
216
+ timeout: HTTP_TIMEOUT,
217
+ validateStatus: (status) => status < 500
218
+ });
219
+ return createAccessibilityResult(response.status, fieldName);
220
+ } catch (proxyError) {
221
+ // Proxy not available (e.g., dev environment without PHP)
222
+ // Return null to skip this check gracefully
223
+ return null;
224
+ }
225
+ } else {
226
+ // In Node.js, make direct request
227
+ const response = await axios.head(url, {
228
+ timeout: HTTP_TIMEOUT,
229
+ maxRedirects: MAX_REDIRECTS,
230
+ validateStatus: (status) => status < 500
231
+ });
232
+ return createAccessibilityResult(response.status, fieldName);
233
+ }
198
234
  } catch (error) {
199
235
  // Return null to signal that GET should be attempted
200
236
  return null;
@@ -203,6 +239,8 @@ async function attemptHeadRequest(url, fieldName) {
203
239
 
204
240
  /**
205
241
  * Attempts to check URL accessibility using GET request
242
+ * In browser environments, attempts to use proxy to bypass CORS restrictions.
243
+ * Falls back gracefully if proxy is unavailable (e.g., in dev environment).
206
244
  *
207
245
  * @param {string} url - The URL to check
208
246
  * @param {string} fieldName - Name of the field being checked
@@ -210,13 +248,33 @@ async function attemptHeadRequest(url, fieldName) {
210
248
  */
211
249
  async function attemptGetRequest(url, fieldName) {
212
250
  try {
213
- const response = await axios.get(url, {
214
- timeout: HTTP_TIMEOUT,
215
- maxRedirects: MAX_REDIRECTS,
216
- validateStatus: (status) => status < 500
217
- });
218
-
219
- return createAccessibilityResult(response.status, fieldName);
251
+ const isBrowser = isBrowserEnvironment();
252
+
253
+ if (isBrowser) {
254
+ // In browser, try using proxy
255
+ try {
256
+ const proxyUrl = `${PROXY_URL}?url=${encodeURIComponent(url)}`;
257
+ const response = await axios.get(proxyUrl, {
258
+ timeout: HTTP_TIMEOUT,
259
+ validateStatus: (status) => status < 500
260
+ });
261
+ return createAccessibilityResult(response.status, fieldName);
262
+ } catch (proxyError) {
263
+ // Proxy not available (e.g., dev environment without PHP)
264
+ return {
265
+ isAccessible: false,
266
+ message: `${fieldName} accessibility check skipped (proxy unavailable in dev environment)`
267
+ };
268
+ }
269
+ } else {
270
+ // In Node.js, make direct request
271
+ const response = await axios.get(url, {
272
+ timeout: HTTP_TIMEOUT,
273
+ maxRedirects: MAX_REDIRECTS,
274
+ validateStatus: (status) => status < 500
275
+ });
276
+ return createAccessibilityResult(response.status, fieldName);
277
+ }
220
278
  } catch (error) {
221
279
  return {
222
280
  isAccessible: false,
@@ -261,6 +319,7 @@ async function validateExternalSpec(spec, index, checkAccessibility = true) {
261
319
  specId: spec.external_spec || `[spec ${index}]`,
262
320
  errors: [],
263
321
  warnings: [],
322
+ info: [],
264
323
  success: []
265
324
  };
266
325
 
@@ -280,7 +339,7 @@ async function validateExternalSpec(spec, index, checkAccessibility = true) {
280
339
  results.success.push('Field "gh_page" has valid URL structure');
281
340
 
282
341
  if (ghPageStructure.message) {
283
- results.warnings.push(ghPageStructure.message);
342
+ results.info.push(ghPageStructure.message);
284
343
  }
285
344
 
286
345
  // Check gh_page accessibility
@@ -336,6 +395,9 @@ async function validateExternalSpec(spec, index, checkAccessibility = true) {
336
395
  * 5. Validates url structure (proper format for GitHub repository)
337
396
  * 6. Checks if url is accessible (returns HTTP 200)
338
397
  *
398
+ * Note: In browser environments, accessibility checks use a proxy (/proxy.php)
399
+ * to bypass CORS restrictions. The proxy must be available for full validation.
400
+ *
339
401
  * @param {import('../providers.js').Provider} provider - The provider instance for file operations
340
402
  * @param {Object} options - Validation options
341
403
  * @param {boolean} options.checkAccessibility - Whether to check URL accessibility (default: true)
@@ -349,7 +411,10 @@ async function validateExternalSpec(spec, index, checkAccessibility = true) {
349
411
  * ```
350
412
  */
351
413
  export async function checkExternalSpecsUrls(provider, options = {}) {
352
- const { checkAccessibility = true } = options;
414
+ // Default to checking accessibility (proxy handles CORS in browser)
415
+ const checkAccessibility = options.checkAccessibility !== undefined
416
+ ? options.checkAccessibility
417
+ : true;
353
418
 
354
419
  try {
355
420
  // Check if specs.json exists
@@ -432,6 +497,7 @@ export async function checkExternalSpecsUrls(provider, options = {}) {
432
497
  const allResults = [];
433
498
  const totalErrors = [];
434
499
  const totalWarnings = [];
500
+ const totalInfo = [];
435
501
  const totalSuccess = [];
436
502
 
437
503
  for (let i = 0; i < spec.external_specs.length; i++) {
@@ -441,6 +507,7 @@ export async function checkExternalSpecsUrls(provider, options = {}) {
441
507
 
442
508
  totalErrors.push(...result.errors.map(err => `${result.specId}: ${err}`));
443
509
  totalWarnings.push(...result.warnings.map(warn => `${result.specId}: ${warn}`));
510
+ totalInfo.push(...result.info.map(inf => `${result.specId}: ${inf}`));
444
511
  totalSuccess.push(...result.success.map(succ => `${result.specId}: ${succ}`));
445
512
  }
446
513
 
@@ -454,6 +521,15 @@ export async function checkExternalSpecsUrls(provider, options = {}) {
454
521
  } else if (totalWarnings.length > 0) {
455
522
  status = 'warn';
456
523
  message = `External specs validated with ${totalWarnings.length} warning(s)`;
524
+ } else if (totalInfo.length > 0) {
525
+ // Info messages don't affect the pass status
526
+ message = `All ${spec.external_specs.length} external spec(s) validated successfully (${totalInfo.length} info note(s))`;
527
+ }
528
+
529
+ // Collect all informational messages
530
+ const infoMessages = [...totalInfo];
531
+ if (isBrowserEnvironment() && checkAccessibility) {
532
+ infoMessages.push('URL accessibility checks performed via proxy (browser environment)');
457
533
  }
458
534
 
459
535
  return createHealthCheckResult(
@@ -465,6 +541,7 @@ export async function checkExternalSpecsUrls(provider, options = {}) {
465
541
  errors: totalErrors,
466
542
  warnings: totalWarnings,
467
543
  success: totalSuccess,
544
+ info: infoMessages.length > 0 ? infoMessages : undefined,
468
545
  detailedResults: allResults,
469
546
  accessibilityChecked: checkAccessibility
470
547
  }
@@ -25,7 +25,7 @@ export const CHECK_ID = 'gitignore';
25
25
  * Human-readable name for this health check.
26
26
  * @type {string}
27
27
  */
28
- export const CHECK_NAME = '.gitignore Validation';
28
+ export const CHECK_NAME = '.gitignore';
29
29
 
30
30
  /**
31
31
  * Description of what this health check validates.