spec-up-t-healthcheck 1.0.0 → 1.1.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 +5 -0
- package/lib/checks/console-messages.js +211 -0
- package/lib/checks/external-specs-urls.js +93 -16
- package/lib/checks/gitignore.js +1 -1
- package/lib/checks/link-checker.js +361 -0
- package/lib/checks/package-json.js +1 -1
- package/lib/checks/spec-directory-and-files.js +356 -0
- package/lib/checks/specsjson.js +261 -5
- package/lib/formatters/result-details-formatter.js +505 -0
- package/lib/health-check-registry.js +57 -3
- package/lib/health-checker.js +13 -3
- package/lib/html-formatter.js +139 -168
- package/lib/index.js +1 -1
- package/lib/providers-browser.js +73 -0
- package/lib/providers.js +24 -0
- package/lib/web.js +10 -6
- package/package.json +5 -17
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
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
package/lib/checks/gitignore.js
CHANGED
|
@@ -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
|
|
28
|
+
export const CHECK_NAME = '.gitignore';
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Description of what this health check validates.
|