spec-up-t-healthcheck 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/README.md +216 -0
- package/bin/cli.js +193 -0
- package/bin/demo-html.js +186 -0
- package/bin/simple-test.js +79 -0
- package/lib/checks/external-specs-urls.js +484 -0
- package/lib/checks/gitignore.js +350 -0
- package/lib/checks/package-json.js +518 -0
- package/lib/checks/spec-files.js +263 -0
- package/lib/checks/specsjson.js +361 -0
- package/lib/file-opener.js +127 -0
- package/lib/formatters.js +176 -0
- package/lib/health-check-orchestrator.js +413 -0
- package/lib/health-check-registry.js +396 -0
- package/lib/health-check-utils.js +234 -0
- package/lib/health-checker.js +145 -0
- package/lib/html-formatter.js +626 -0
- package/lib/index.js +123 -0
- package/lib/providers.js +184 -0
- package/lib/web.js +70 -0
- package/package.json +91 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview .gitignore health check module
|
|
3
|
+
*
|
|
4
|
+
* This module validates the existence and content of .gitignore files in
|
|
5
|
+
* specification repositories. It ensures that essential entries are present
|
|
6
|
+
* to prevent common files from being accidentally committed to version control.
|
|
7
|
+
*
|
|
8
|
+
* The check validates:
|
|
9
|
+
* - Existence of .gitignore file
|
|
10
|
+
* - Valid content (not empty, properly formatted)
|
|
11
|
+
* - Presence of required entries that should be ignored
|
|
12
|
+
*
|
|
13
|
+
* @author spec-up-t-healthcheck
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createHealthCheckResult, createErrorResult } from '../health-check-utils.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The identifier for this health check, used in reports and registries.
|
|
20
|
+
* @type {string}
|
|
21
|
+
*/
|
|
22
|
+
export const CHECK_ID = 'gitignore';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Human-readable name for this health check.
|
|
26
|
+
* @type {string}
|
|
27
|
+
*/
|
|
28
|
+
export const CHECK_NAME = '.gitignore Validation';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Description of what this health check validates.
|
|
32
|
+
* @type {string}
|
|
33
|
+
*/
|
|
34
|
+
export const CHECK_DESCRIPTION = 'Validates the existence and content of .gitignore file';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* GitHub URL for the boilerplate .gitignore file that defines the reference entries.
|
|
38
|
+
* This is used to fetch the latest required .gitignore entries dynamically.
|
|
39
|
+
* @type {string}
|
|
40
|
+
*/
|
|
41
|
+
const BOILERPLATE_GITIGNORE_URL = 'https://raw.githubusercontent.com/trustoverip/spec-up-t/master/src/install-from-boilerplate/boilerplate/gitignore';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Cache duration for fetched boilerplate .gitignore (in milliseconds).
|
|
45
|
+
* Set to 1 hour to avoid excessive network requests while keeping data reasonably fresh.
|
|
46
|
+
* @type {number}
|
|
47
|
+
*/
|
|
48
|
+
const CACHE_DURATION = 60 * 60 * 1000; // 1 hour
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* In-memory cache for required entries.
|
|
52
|
+
* @type {Object}
|
|
53
|
+
* @private
|
|
54
|
+
*/
|
|
55
|
+
let entriesCache = {
|
|
56
|
+
entries: null,
|
|
57
|
+
lastFetch: 0
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fallback required entries if the remote fetch fails.
|
|
62
|
+
* These entries prevent common files and directories from being committed.
|
|
63
|
+
*
|
|
64
|
+
* Each entry can appear as-is or with variations (e.g., /node_modules, node_modules/, etc.)
|
|
65
|
+
*
|
|
66
|
+
* @type {readonly string[]}
|
|
67
|
+
*/
|
|
68
|
+
const FALLBACK_REQUIRED_ENTRIES = Object.freeze([
|
|
69
|
+
'node_modules',
|
|
70
|
+
'*.log',
|
|
71
|
+
'dist',
|
|
72
|
+
'*.bak',
|
|
73
|
+
'*.tmp',
|
|
74
|
+
'.DS_Store',
|
|
75
|
+
'.env',
|
|
76
|
+
'coverage',
|
|
77
|
+
'build',
|
|
78
|
+
'.history',
|
|
79
|
+
'/.cache/'
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Fetches the latest required .gitignore entries from the boilerplate repository.
|
|
84
|
+
*
|
|
85
|
+
* This function retrieves the reference .gitignore file from the spec-up-t
|
|
86
|
+
* repository to determine the currently required entries. Results are cached
|
|
87
|
+
* to minimize network requests.
|
|
88
|
+
*
|
|
89
|
+
* The boilerplate .gitignore is parsed line by line, filtering out empty lines
|
|
90
|
+
* and comments to extract the actual entries.
|
|
91
|
+
*
|
|
92
|
+
* @returns {Promise<string[]>} Array of required .gitignore entries, or fallback entries if fetch fails
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
async function fetchRequiredEntries() {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
|
|
98
|
+
// Return cached entries if still valid
|
|
99
|
+
if (entriesCache.entries && (now - entriesCache.lastFetch) < CACHE_DURATION) {
|
|
100
|
+
return entriesCache.entries;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const response = await fetch(BOILERPLATE_GITIGNORE_URL);
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const content = await response.text();
|
|
110
|
+
|
|
111
|
+
// Parse the .gitignore content to extract entries
|
|
112
|
+
const entries = parseGitignoreContent(content);
|
|
113
|
+
|
|
114
|
+
if (entries.length === 0) {
|
|
115
|
+
throw new Error('No entries found in boilerplate .gitignore');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Cache the fetched entries
|
|
119
|
+
entriesCache.entries = entries;
|
|
120
|
+
entriesCache.lastFetch = now;
|
|
121
|
+
|
|
122
|
+
return entries;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// If fetch fails, return cached entries if available, otherwise use fallback
|
|
125
|
+
if (entriesCache.entries) {
|
|
126
|
+
return entriesCache.entries;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Use fallback entries as last resort
|
|
130
|
+
return Array.from(FALLBACK_REQUIRED_ENTRIES);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Normalizes a .gitignore line for comparison.
|
|
136
|
+
*
|
|
137
|
+
* This function removes leading/trailing slashes and whitespace to allow
|
|
138
|
+
* flexible matching of entries that may be written in different styles.
|
|
139
|
+
*
|
|
140
|
+
* Examples:
|
|
141
|
+
* - "/node_modules/" -> "node_modules"
|
|
142
|
+
* - "node_modules/" -> "node_modules"
|
|
143
|
+
* - " node_modules " -> "node_modules"
|
|
144
|
+
*
|
|
145
|
+
* @param {string} line - The line to normalize
|
|
146
|
+
* @returns {string} The normalized line
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
function normalizeLine(line) {
|
|
150
|
+
return line.trim().replace(/^\/+|\/+$/g, '');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Checks if a required entry is present in the .gitignore content.
|
|
155
|
+
*
|
|
156
|
+
* This function performs a flexible match that accounts for different ways
|
|
157
|
+
* the entry might be written (with or without slashes, wildcards, etc.)
|
|
158
|
+
*
|
|
159
|
+
* @param {string} requiredEntry - The required entry to look for
|
|
160
|
+
* @param {string[]} normalizedLines - Array of normalized .gitignore lines
|
|
161
|
+
* @returns {boolean} True if the entry is present
|
|
162
|
+
* @private
|
|
163
|
+
*/
|
|
164
|
+
function isEntryPresent(requiredEntry, normalizedLines) {
|
|
165
|
+
const normalizedRequired = normalizeLine(requiredEntry);
|
|
166
|
+
|
|
167
|
+
return normalizedLines.some(line => {
|
|
168
|
+
// Exact match after normalization
|
|
169
|
+
if (line === normalizedRequired) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// For wildcard patterns, check if the pattern is present
|
|
174
|
+
if (normalizedRequired.includes('*') && line === normalizedRequired) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// For paths with slashes, check if the base name matches
|
|
179
|
+
const requiredBase = normalizedRequired.split('/').pop();
|
|
180
|
+
const lineBase = line.split('/').pop();
|
|
181
|
+
|
|
182
|
+
if (requiredBase === lineBase) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return false;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Parses .gitignore content into an array of valid entries.
|
|
192
|
+
*
|
|
193
|
+
* This function:
|
|
194
|
+
* - Splits content by newlines
|
|
195
|
+
* - Removes empty lines
|
|
196
|
+
* - Removes comment lines (starting with #)
|
|
197
|
+
* - Trims whitespace
|
|
198
|
+
*
|
|
199
|
+
* @param {string} content - The raw .gitignore content
|
|
200
|
+
* @returns {string[]} Array of valid .gitignore entries
|
|
201
|
+
* @private
|
|
202
|
+
*/
|
|
203
|
+
function parseGitignoreContent(content) {
|
|
204
|
+
return content
|
|
205
|
+
.split('\n')
|
|
206
|
+
.map(line => line.trim())
|
|
207
|
+
.filter(line => line.length > 0 && !line.startsWith('#'));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validates the .gitignore file in a specification repository.
|
|
212
|
+
*
|
|
213
|
+
* This function performs a comprehensive check of the .gitignore file:
|
|
214
|
+
* 1. Verifies the file exists
|
|
215
|
+
* 2. Validates that it has content
|
|
216
|
+
* 3. Checks for required entries
|
|
217
|
+
* 4. Reports missing entries as warnings or failures
|
|
218
|
+
*
|
|
219
|
+
* @param {import('../providers.js').Provider} provider - The provider instance for repository access
|
|
220
|
+
* @returns {Promise<import('../health-check-utils.js').HealthCheckResult>} Result of the .gitignore validation
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```javascript
|
|
224
|
+
* const provider = createLocalProvider('/path/to/repo');
|
|
225
|
+
* const result = await checkGitignore(provider);
|
|
226
|
+
* console.log(result.status); // 'pass', 'warn', or 'fail'
|
|
227
|
+
* console.log(result.message);
|
|
228
|
+
* console.log(result.details.missingEntries);
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
export async function checkGitignore(provider) {
|
|
232
|
+
try {
|
|
233
|
+
// Fetch the required entries from the boilerplate repository
|
|
234
|
+
const requiredEntries = await fetchRequiredEntries();
|
|
235
|
+
|
|
236
|
+
// Check if .gitignore file exists
|
|
237
|
+
const exists = await provider.fileExists('.gitignore');
|
|
238
|
+
|
|
239
|
+
if (!exists) {
|
|
240
|
+
return createHealthCheckResult(
|
|
241
|
+
CHECK_NAME,
|
|
242
|
+
'fail',
|
|
243
|
+
'.gitignore file not found - repository should have a .gitignore file',
|
|
244
|
+
{
|
|
245
|
+
fileExists: false,
|
|
246
|
+
recommendation: 'Create a .gitignore file with common exclusion patterns',
|
|
247
|
+
boilerplateUrl: BOILERPLATE_GITIGNORE_URL
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Read .gitignore content
|
|
253
|
+
const content = await provider.readFile('.gitignore');
|
|
254
|
+
|
|
255
|
+
// Check if file has valid content
|
|
256
|
+
if (!content || content.trim().length === 0) {
|
|
257
|
+
return createHealthCheckResult(
|
|
258
|
+
CHECK_NAME,
|
|
259
|
+
'fail',
|
|
260
|
+
'.gitignore file is empty - should contain exclusion patterns',
|
|
261
|
+
{
|
|
262
|
+
fileExists: true,
|
|
263
|
+
isEmpty: true,
|
|
264
|
+
recommendation: 'Add common exclusion patterns to .gitignore',
|
|
265
|
+
boilerplateUrl: BOILERPLATE_GITIGNORE_URL
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Parse .gitignore content
|
|
271
|
+
const lines = parseGitignoreContent(content);
|
|
272
|
+
|
|
273
|
+
if (lines.length === 0) {
|
|
274
|
+
return createHealthCheckResult(
|
|
275
|
+
CHECK_NAME,
|
|
276
|
+
'fail',
|
|
277
|
+
'.gitignore file contains no valid entries (only comments or empty lines)',
|
|
278
|
+
{
|
|
279
|
+
fileExists: true,
|
|
280
|
+
hasOnlyComments: true,
|
|
281
|
+
recommendation: 'Add valid exclusion patterns to .gitignore',
|
|
282
|
+
boilerplateUrl: BOILERPLATE_GITIGNORE_URL
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Normalize lines for comparison
|
|
288
|
+
const normalizedLines = lines.map(normalizeLine);
|
|
289
|
+
|
|
290
|
+
// Check for missing required entries
|
|
291
|
+
const missingEntries = requiredEntries.filter(
|
|
292
|
+
required => !isEntryPresent(required, normalizedLines)
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Build details object
|
|
296
|
+
const details = {
|
|
297
|
+
fileExists: true,
|
|
298
|
+
totalEntries: lines.length,
|
|
299
|
+
requiredEntriesCount: requiredEntries.length,
|
|
300
|
+
presentEntriesCount: requiredEntries.length - missingEntries.length,
|
|
301
|
+
missingEntries: missingEntries.length > 0 ? missingEntries : undefined,
|
|
302
|
+
sample: lines.slice(0, 10), // Include first 10 entries as sample
|
|
303
|
+
boilerplateUrl: BOILERPLATE_GITIGNORE_URL,
|
|
304
|
+
usedFallback: entriesCache.entries === FALLBACK_REQUIRED_ENTRIES
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Determine status and message
|
|
308
|
+
if (missingEntries.length === 0) {
|
|
309
|
+
return createHealthCheckResult(
|
|
310
|
+
CHECK_NAME,
|
|
311
|
+
'pass',
|
|
312
|
+
'.gitignore file is valid and contains all required entries',
|
|
313
|
+
details
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// If some required entries are missing, it's a warning
|
|
318
|
+
const missingCount = missingEntries.length;
|
|
319
|
+
const message = `${missingCount} required ${missingCount === 1 ? 'entry' : 'entries'} missing from .gitignore: ${missingEntries.join(', ')}`;
|
|
320
|
+
|
|
321
|
+
return createHealthCheckResult(
|
|
322
|
+
CHECK_NAME,
|
|
323
|
+
'warn',
|
|
324
|
+
message,
|
|
325
|
+
details
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return createErrorResult(CHECK_NAME, error, {
|
|
330
|
+
context: 'checking .gitignore file',
|
|
331
|
+
provider: provider.type
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Clears the cached required entries.
|
|
338
|
+
* This is primarily useful for testing to force a fresh fetch.
|
|
339
|
+
*
|
|
340
|
+
* @private
|
|
341
|
+
*/
|
|
342
|
+
export function clearEntriesCache() {
|
|
343
|
+
entriesCache = {
|
|
344
|
+
entries: null,
|
|
345
|
+
lastFetch: 0
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Export the health check function as default for easy registration
|
|
350
|
+
export default checkGitignore;
|