spec-up-t 1.2.7 → 1.2.8
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/.github/copilot-instructions.md +3 -0
- package/index.js +11 -1
- package/package.json +1 -1
- package/src/collect-external-references.js +10 -23
- package/src/escape-mechanism.js +57 -0
- package/src/health-check/specs-configuration-checker.js +178 -144
- package/src/health-check.js +199 -10
- package/src/collectExternalReferences/fetchTermsFromIndex.1.js +0 -340
package/index.js
CHANGED
|
@@ -35,6 +35,7 @@ module.exports = async function (options = {}) {
|
|
|
35
35
|
createVersionsIndex(config.specs[0].output_path);
|
|
36
36
|
|
|
37
37
|
const { fixMarkdownFiles } = require('./src/fix-markdown-files.js');
|
|
38
|
+
const { processEscapedTags, restoreEscapedTags } = require('./src/escape-mechanism.js');
|
|
38
39
|
|
|
39
40
|
let template = fs.readFileSync(path.join(modulePath, 'templates/template.html'), 'utf8');
|
|
40
41
|
let assets = fs.readJsonSync(modulePath + '/config/asset-map.json');
|
|
@@ -528,7 +529,12 @@ module.exports = async function (options = {}) {
|
|
|
528
529
|
|
|
529
530
|
let doc = docs.join("\n");
|
|
530
531
|
|
|
531
|
-
//
|
|
532
|
+
// Handles backslash escape mechanism for substitution tags
|
|
533
|
+
// Phase 1: Pre-processing - Handle escaped tags
|
|
534
|
+
doc = processEscapedTags(doc);
|
|
535
|
+
|
|
536
|
+
// Handles backslash escape mechanism for substitution tags
|
|
537
|
+
// Phase 2: Tag Processing - Apply normal substitution logic
|
|
532
538
|
doc = applyReplacers(doc);
|
|
533
539
|
|
|
534
540
|
md[spec.katex ? "enable" : "disable"](katexRules);
|
|
@@ -542,6 +548,10 @@ module.exports = async function (options = {}) {
|
|
|
542
548
|
// Sort definition terms case-insensitively before final rendering
|
|
543
549
|
renderedHtml = sortDefinitionTermsInHtml(renderedHtml);
|
|
544
550
|
|
|
551
|
+
// Handles backslash escape mechanism for substitution tags
|
|
552
|
+
// Phase 3: Post-processing - Restore escaped sequences as literals
|
|
553
|
+
renderedHtml = restoreEscapedTags(renderedHtml);
|
|
554
|
+
|
|
545
555
|
// Process external references to ensure they are inserted as raw HTML, not as JSON string
|
|
546
556
|
const externalReferencesHtml = Array.isArray(externalReferences)
|
|
547
557
|
? externalReferences.join('')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-up-t",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"description": "Technical specification drafting tool that generates rich specification documents from markdown. Forked from https://github.com/decentralized-identity/spec-up by Daniel Buchner (https://github.com/csuwildcat)",
|
|
5
5
|
"main": "./index",
|
|
6
6
|
"repository": {
|
|
@@ -270,13 +270,17 @@ function processExternalReferences(config, GITHUB_API_TOKEN, options) {
|
|
|
270
270
|
*
|
|
271
271
|
* @description
|
|
272
272
|
* This function performs several key operations:
|
|
273
|
-
* 1.
|
|
273
|
+
* 1. Optionally uses GitHub PAT for better API performance and higher rate limits
|
|
274
274
|
* 2. Checks validity of repository URLs
|
|
275
275
|
* 3. Extracts xref/tref patterns from markdown content
|
|
276
276
|
* 4. Extends references with repository metadata
|
|
277
277
|
* 5. Processes references to fetch commit information
|
|
278
278
|
* 6. Generates output files in both JS and JSON formats
|
|
279
279
|
*
|
|
280
|
+
* Note: The function will run without a GitHub token but may encounter rate limits.
|
|
281
|
+
* For better performance, provide a GitHub Personal Access Token via environment
|
|
282
|
+
* variable or the options parameter.
|
|
283
|
+
*
|
|
280
284
|
* @example
|
|
281
285
|
* // Basic usage
|
|
282
286
|
* collectExternalReferences();
|
|
@@ -289,17 +293,6 @@ function collectExternalReferences(options = {}) {
|
|
|
289
293
|
const externalSpecsRepos = config.specs[0].external_specs;
|
|
290
294
|
const GITHUB_API_TOKEN = options.pat || process.env.GITHUB_API_TOKEN;
|
|
291
295
|
|
|
292
|
-
const explanationPAT =
|
|
293
|
-
`❌ No GitHub Personal Access Token (PAT) was found.
|
|
294
|
-
|
|
295
|
-
GitHub requires you to set up a PAT to retrieve external references.
|
|
296
|
-
|
|
297
|
-
There is no point in continuing without a PAT, so we stop here.
|
|
298
|
-
|
|
299
|
-
Find instructions on how to get a PAT at https://blockchainbird.github.io/spec-up-t-website/docs/getting-started/github-token
|
|
300
|
-
|
|
301
|
-
`;
|
|
302
|
-
|
|
303
296
|
const explanationNoExternalReferences =
|
|
304
297
|
`❌ No external references were found in the specs.json file.
|
|
305
298
|
|
|
@@ -310,19 +303,13 @@ function collectExternalReferences(options = {}) {
|
|
|
310
303
|
`;
|
|
311
304
|
|
|
312
305
|
// First do some checks
|
|
313
|
-
|
|
314
|
-
// Do not run the script if the GitHub API token is not set
|
|
306
|
+
// Show informational message if no token is available
|
|
315
307
|
if (!GITHUB_API_TOKEN) {
|
|
316
|
-
console.log(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// React to user pressing any key
|
|
320
|
-
if (userInput.trim() !== '') {
|
|
321
|
-
console.log('ℹ️ Stopping...');
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
308
|
+
console.log('ℹ️ No GitHub Personal Access Token (PAT) found. Running without authentication (may hit rate limits).');
|
|
309
|
+
console.log('💡 For better performance, set up a PAT: https://blockchainbird.github.io/spec-up-t-website/docs/getting-started/github-token\n');
|
|
324
310
|
}
|
|
325
|
-
|
|
311
|
+
|
|
312
|
+
if (externalSpecsRepos.length === 0) {
|
|
326
313
|
// Check if the URLs for the external specs repositories are valid, and prompt the user to abort if they are not.
|
|
327
314
|
console.log(explanationNoExternalReferences);
|
|
328
315
|
const userInput = readlineSync.question('Press any key');
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape Mechanism Module for Spec-Up Substitution Tags
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions to handle backslash escape sequences for substitution tags,
|
|
5
|
+
* allowing users to display tag syntax literally in their documentation.
|
|
6
|
+
*
|
|
7
|
+
* The escape mechanism works in three phases:
|
|
8
|
+
* 1. Pre-processing: Convert escaped sequences to temporary placeholders
|
|
9
|
+
* 2. Tag processing: Normal substitution logic (handled elsewhere)
|
|
10
|
+
* 3. Post-processing: Restore escaped sequences as literals
|
|
11
|
+
*
|
|
12
|
+
* Supported escape pattern:
|
|
13
|
+
* - \[[tag: content]] → displays as literal [[tag: content]]
|
|
14
|
+
*
|
|
15
|
+
* @version 1.0.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Handles backslash escape mechanism for substitution tags
|
|
20
|
+
*
|
|
21
|
+
* Use backslash escape sequences to allow literal [[ tags in markdown
|
|
22
|
+
*
|
|
23
|
+
* Phase 1: Pre-processing - Convert escaped sequences to temporary placeholders
|
|
24
|
+
*
|
|
25
|
+
* @param {string} doc - The markdown document to process
|
|
26
|
+
* @returns {string} - Document with escaped sequences converted to placeholders
|
|
27
|
+
*/
|
|
28
|
+
function processEscapedTags(doc) {
|
|
29
|
+
// Replace \[[ with escape placeholder for literal display
|
|
30
|
+
// In markdown: \[[def: term]] should become [[def: term]] (literal tag syntax)
|
|
31
|
+
doc = doc.replace(/\\(\[\[)/g, '__SPEC_UP_ESCAPED_TAG__');
|
|
32
|
+
|
|
33
|
+
return doc;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Handles backslash escape mechanism for substitution tags
|
|
38
|
+
*
|
|
39
|
+
* Use backslash escape sequences to allow literal [[ tags in markdown
|
|
40
|
+
*
|
|
41
|
+
* Phase 3: Post-processing - Restore escaped sequences as literals
|
|
42
|
+
* Converts placeholders back to literal [[ characters
|
|
43
|
+
*
|
|
44
|
+
* @param {string} renderedHtml - The rendered HTML to process
|
|
45
|
+
* @returns {string} - HTML with placeholders restored to literal [[ tags
|
|
46
|
+
*/
|
|
47
|
+
function restoreEscapedTags(renderedHtml) {
|
|
48
|
+
// Replace escaped tag placeholders with literal [[
|
|
49
|
+
renderedHtml = renderedHtml.replace(/__SPEC_UP_ESCAPED_TAG__/g, '[[');
|
|
50
|
+
|
|
51
|
+
return renderedHtml;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
processEscapedTags,
|
|
56
|
+
restoreEscapedTags
|
|
57
|
+
};
|
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Spec-Up-T specs.json Configuration Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates specs.json configuration files by comparing project configurations
|
|
5
|
+
* against default templates to ensure proper setup and catch common issues.
|
|
6
|
+
*
|
|
7
|
+
* **Validation Flow:**
|
|
8
|
+
* 1. File existence check (project specs.json + default template)
|
|
9
|
+
* 2. Field categorization (required vs optional fields)
|
|
10
|
+
* 3. Required field validation (presence + configuration status)
|
|
11
|
+
* 4. Optional field validation (configuration warnings)
|
|
12
|
+
* 5. Unexpected field detection (typo prevention)
|
|
13
|
+
* 6. Summary report generation
|
|
14
|
+
*
|
|
15
|
+
* **Field Categories:**
|
|
16
|
+
* - **Required fields**: Must be present (e.g., title, author, source)
|
|
17
|
+
* - **Optional fields**: Can be omitted (e.g., logo, external_specs)
|
|
18
|
+
* - **Must-change fields**: Cannot use default values (title, author, etc.)
|
|
19
|
+
* - **Allow-default fields**: Can keep default values (spec_directory, etc.)
|
|
20
|
+
* - **Deprecated fields**: Legacy fields ignored during validation
|
|
21
|
+
*
|
|
22
|
+
* **Output:**
|
|
23
|
+
* Returns structured validation results with pass/fail/warning status,
|
|
24
|
+
* detailed messages, and actionable feedback for configuration improvements.
|
|
25
|
+
*
|
|
26
|
+
* @author Spec-Up-T Team
|
|
27
|
+
* @since 2025-06-06
|
|
28
|
+
*/
|
|
29
|
+
|
|
1
30
|
const fs = require('fs');
|
|
2
31
|
const path = require('path');
|
|
3
32
|
|
|
@@ -51,9 +80,18 @@ const knownOptionalFields = [
|
|
|
51
80
|
'external_specs',
|
|
52
81
|
'logo_link',
|
|
53
82
|
'favicon',
|
|
54
|
-
'katex'
|
|
83
|
+
'katex',
|
|
84
|
+
'spec_directory',
|
|
85
|
+
'spec_terms_directory',
|
|
86
|
+
'output_path',
|
|
87
|
+
'markdown_paths'
|
|
55
88
|
];
|
|
56
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Deprecated fields that should not be flagged as unexpected
|
|
92
|
+
*/
|
|
93
|
+
const deprecatedFields = [];
|
|
94
|
+
|
|
57
95
|
/**
|
|
58
96
|
* Check if the files needed for configuration check exist
|
|
59
97
|
* @param {string} projectSpecsPath - Path to project specs.json
|
|
@@ -80,32 +118,87 @@ function checkFilesExist(projectSpecsPath, defaultSpecsPath) {
|
|
|
80
118
|
return null;
|
|
81
119
|
}
|
|
82
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Get all valid field names (required + optional + deprecated). Creates a comprehensive list of all field names that should not be flagged as "unexpected"
|
|
123
|
+
* @param {Array} defaultSpecKeys - Keys from default specs
|
|
124
|
+
* @returns {Array} - All valid field names
|
|
125
|
+
*/
|
|
126
|
+
function getAllValidFields(defaultSpecKeys) {
|
|
127
|
+
// Get all field names from descriptions (these are the canonical field names)
|
|
128
|
+
const canonicalFields = Object.keys(fieldDescriptions);
|
|
129
|
+
|
|
130
|
+
// Combine with known optional fields and deprecated fields
|
|
131
|
+
const allValidFields = [
|
|
132
|
+
...canonicalFields,
|
|
133
|
+
...knownOptionalFields,
|
|
134
|
+
...deprecatedFields,
|
|
135
|
+
...defaultSpecKeys
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
// Remove duplicates
|
|
139
|
+
return [...new Set(allValidFields)];
|
|
140
|
+
}
|
|
141
|
+
|
|
83
142
|
/**
|
|
84
143
|
* Categorize fields into required and optional
|
|
85
144
|
* @param {Array} defaultSpecKeys - Keys from default specs
|
|
86
145
|
* @returns {Object} - Object containing required and optional fields
|
|
87
146
|
*/
|
|
88
147
|
function categorizeFields(defaultSpecKeys) {
|
|
148
|
+
const createFieldObject = key => ({
|
|
149
|
+
key,
|
|
150
|
+
description: fieldDescriptions[key] || `${key.replace(/_/g, ' ')} field`,
|
|
151
|
+
allowDefaultValue: allowDefaultValueFields.includes(key),
|
|
152
|
+
mustChange: mustChangeFields.includes(key)
|
|
153
|
+
});
|
|
154
|
+
|
|
89
155
|
const requiredFields = defaultSpecKeys
|
|
90
156
|
.filter(key => !knownOptionalFields.includes(key))
|
|
91
|
-
.map(
|
|
92
|
-
key,
|
|
93
|
-
description: fieldDescriptions[key] || `${key.replace(/_/g, ' ')} field`,
|
|
94
|
-
allowDefaultValue: allowDefaultValueFields.includes(key),
|
|
95
|
-
mustChange: mustChangeFields.includes(key)
|
|
96
|
-
}));
|
|
157
|
+
.map(createFieldObject);
|
|
97
158
|
|
|
98
159
|
const optionalFields = defaultSpecKeys
|
|
99
160
|
.filter(key => knownOptionalFields.includes(key))
|
|
100
|
-
.map(
|
|
101
|
-
key,
|
|
102
|
-
description: fieldDescriptions[key] || `${key.replace(/_/g, ' ')} field`,
|
|
103
|
-
allowDefaultValue: allowDefaultValueFields.includes(key)
|
|
104
|
-
}));
|
|
161
|
+
.map(createFieldObject);
|
|
105
162
|
|
|
106
163
|
return { requiredFields, optionalFields };
|
|
107
164
|
}
|
|
108
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Process field validation results. Orchestrates the validation of all fields in the specs.json
|
|
168
|
+
* @param {Object} projectSpecs - Project specs object
|
|
169
|
+
* @param {Object} defaultSpecs - Default specs object
|
|
170
|
+
* @param {Array} defaultSpecKeys - Keys from default specs
|
|
171
|
+
* @returns {Object} - Object with results and missingRequiredKeys
|
|
172
|
+
*/
|
|
173
|
+
function processFieldValidation(projectSpecs, defaultSpecs, defaultSpecKeys) {
|
|
174
|
+
const { requiredFields, optionalFields } = categorizeFields(defaultSpecKeys);
|
|
175
|
+
|
|
176
|
+
const requiredResults = requiredFields.map(field => evaluateRequiredField(field, projectSpecs, defaultSpecs));
|
|
177
|
+
const optionalResults = optionalFields.map(field => evaluateOptionalField(field, projectSpecs, defaultSpecs));
|
|
178
|
+
|
|
179
|
+
const missingRequiredKeys = requiredResults
|
|
180
|
+
.filter(result => !result.success && result.details.includes('missing'))
|
|
181
|
+
.map((_, index) => requiredFields[index].key);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
results: [...requiredResults, ...optionalResults],
|
|
185
|
+
missingRequiredKeys
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check for unexpected fields in project specs
|
|
191
|
+
* @param {Object} projectSpecs - Project specs object
|
|
192
|
+
* @param {Array} defaultSpecKeys - Keys from default specs
|
|
193
|
+
* @returns {Array} - Array of unexpected field names
|
|
194
|
+
*/
|
|
195
|
+
function findUnexpectedFields(projectSpecs, defaultSpecKeys) {
|
|
196
|
+
const projectKeys = Object.keys(projectSpecs.specs?.[0] || {});
|
|
197
|
+
const allValidFields = getAllValidFields(defaultSpecKeys);
|
|
198
|
+
|
|
199
|
+
return projectKeys.filter(key => !allValidFields.includes(key));
|
|
200
|
+
}
|
|
201
|
+
|
|
109
202
|
/**
|
|
110
203
|
* Check if a field value has been configured
|
|
111
204
|
* @param {any} projectValue - Value from project specs
|
|
@@ -120,61 +213,58 @@ function isFieldConfigured(projectValue, defaultValue) {
|
|
|
120
213
|
}
|
|
121
214
|
|
|
122
215
|
/**
|
|
123
|
-
* Evaluate a
|
|
216
|
+
* Evaluate a field and generate result (unified for required/optional)
|
|
124
217
|
* @param {Object} field - Field definition
|
|
125
218
|
* @param {Object} projectSpecs - Project specs object
|
|
126
219
|
* @param {Object} defaultSpecs - Default specs object
|
|
220
|
+
* @param {boolean} isRequired - Whether field is required
|
|
127
221
|
* @returns {Object} - Check result
|
|
128
222
|
*/
|
|
129
|
-
function
|
|
223
|
+
function evaluateField(field, projectSpecs, defaultSpecs, isRequired) {
|
|
130
224
|
const hasField = projectSpecs.specs?.[0]?.hasOwnProperty(field.key);
|
|
131
225
|
|
|
132
226
|
if (!hasField) {
|
|
133
227
|
return {
|
|
134
228
|
name: `${field.description} configuration`,
|
|
135
|
-
success:
|
|
136
|
-
details:
|
|
229
|
+
success: !isRequired,
|
|
230
|
+
details: isRequired
|
|
231
|
+
? `Required "${field.key}" key is missing in specs.json`
|
|
232
|
+
: `Optional "${field.key}" key is not present (this is not required)`
|
|
137
233
|
};
|
|
138
234
|
}
|
|
139
235
|
|
|
140
236
|
const projectValue = projectSpecs.specs[0][field.key];
|
|
141
237
|
const defaultValue = defaultSpecs.specs?.[0]?.[field.key];
|
|
142
|
-
|
|
238
|
+
const isConfigured = field.allowDefaultValue || isFieldConfigured(projectValue, defaultValue);
|
|
143
239
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
configured = true;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
let status;
|
|
150
|
-
let success = true;
|
|
240
|
+
// Show warning when fields haven't been configured from their default values
|
|
241
|
+
const status = isConfigured ? undefined : 'warning';
|
|
151
242
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
status = undefined; // No status means it shows as failure
|
|
155
|
-
success = false;
|
|
156
|
-
} else {
|
|
157
|
-
status = 'warning';
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
let details = '';
|
|
162
|
-
if (configured) {
|
|
163
|
-
details = (projectValue === defaultValue && field.allowDefaultValue)
|
|
243
|
+
const details = isConfigured
|
|
244
|
+
? (projectValue === defaultValue && field.allowDefaultValue)
|
|
164
245
|
? `Default value for ${field.description} is acceptable`
|
|
165
|
-
: `${field.description} has been changed from default
|
|
166
|
-
|
|
167
|
-
details = `${field.description} is still set to default value${['title', 'author'].includes(field.key) ? `: \"${defaultValue}\"` : ''}`;
|
|
168
|
-
}
|
|
246
|
+
: `${field.description} has been changed from default`
|
|
247
|
+
: `${field.description} is still set to default value${mustChangeFields.includes(field.key) ? `: \"${defaultValue}\"` : ''}`;
|
|
169
248
|
|
|
170
249
|
return {
|
|
171
250
|
name: `${field.description} configuration`,
|
|
172
251
|
status,
|
|
173
|
-
success,
|
|
252
|
+
success: true,
|
|
174
253
|
details
|
|
175
254
|
};
|
|
176
255
|
}
|
|
177
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Evaluate a required field and generate result
|
|
259
|
+
* @param {Object} field - Field definition
|
|
260
|
+
* @param {Object} projectSpecs - Project specs object
|
|
261
|
+
* @param {Object} defaultSpecs - Default specs object
|
|
262
|
+
* @returns {Object} - Check result
|
|
263
|
+
*/
|
|
264
|
+
function evaluateRequiredField(field, projectSpecs, defaultSpecs) {
|
|
265
|
+
return evaluateField(field, projectSpecs, defaultSpecs, true);
|
|
266
|
+
}
|
|
267
|
+
|
|
178
268
|
/**
|
|
179
269
|
* Evaluate an optional field and generate result
|
|
180
270
|
* @param {Object} field - Field definition
|
|
@@ -183,39 +273,7 @@ function evaluateRequiredField(field, projectSpecs, defaultSpecs) {
|
|
|
183
273
|
* @returns {Object} - Check result
|
|
184
274
|
*/
|
|
185
275
|
function evaluateOptionalField(field, projectSpecs, defaultSpecs) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (!hasField) {
|
|
189
|
-
return {
|
|
190
|
-
name: `${field.description} configuration`,
|
|
191
|
-
success: true,
|
|
192
|
-
details: `Optional "${field.key}" key is not present (this is not required)`
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const projectValue = projectSpecs.specs[0][field.key];
|
|
197
|
-
const defaultValue = defaultSpecs.specs?.[0]?.[field.key];
|
|
198
|
-
let configured = isFieldConfigured(projectValue, defaultValue);
|
|
199
|
-
|
|
200
|
-
if (field.allowDefaultValue) {
|
|
201
|
-
configured = true;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
let details = '';
|
|
205
|
-
if (configured) {
|
|
206
|
-
details = (projectValue === defaultValue && field.allowDefaultValue)
|
|
207
|
-
? `Default value for ${field.description} is acceptable`
|
|
208
|
-
: `${field.description} has been changed from default`;
|
|
209
|
-
} else {
|
|
210
|
-
details = `${field.description} is still set to default value`;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
name: `${field.description} configuration`,
|
|
215
|
-
status: configured ? undefined : 'warning',
|
|
216
|
-
success: true, // Always true for optional fields
|
|
217
|
-
details
|
|
218
|
-
};
|
|
276
|
+
return evaluateField(field, projectSpecs, defaultSpecs, false);
|
|
219
277
|
}
|
|
220
278
|
|
|
221
279
|
/**
|
|
@@ -228,22 +286,16 @@ function evaluateOptionalField(field, projectSpecs, defaultSpecs) {
|
|
|
228
286
|
function generateSummaryResults(results, missingRequiredKeys, unexpectedKeys) {
|
|
229
287
|
const summaryResults = [];
|
|
230
288
|
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
summaryResults.push({
|
|
240
|
-
name: 'Required fields check',
|
|
241
|
-
success: true,
|
|
242
|
-
details: 'All required fields are present'
|
|
243
|
-
});
|
|
244
|
-
}
|
|
289
|
+
// Required fields summary
|
|
290
|
+
summaryResults.push({
|
|
291
|
+
name: 'Required fields check',
|
|
292
|
+
success: missingRequiredKeys.length === 0,
|
|
293
|
+
details: missingRequiredKeys.length > 0
|
|
294
|
+
? `Missing required fields: ${missingRequiredKeys.join(', ')}`
|
|
295
|
+
: 'All required fields are present'
|
|
296
|
+
});
|
|
245
297
|
|
|
246
|
-
//
|
|
298
|
+
// Unexpected fields check
|
|
247
299
|
if (unexpectedKeys.length > 0) {
|
|
248
300
|
summaryResults.push({
|
|
249
301
|
name: 'Unexpected fields check',
|
|
@@ -253,24 +305,44 @@ function generateSummaryResults(results, missingRequiredKeys, unexpectedKeys) {
|
|
|
253
305
|
}
|
|
254
306
|
|
|
255
307
|
// Overall configuration status
|
|
256
|
-
const fieldResults = results.filter(r =>
|
|
257
|
-
r.name.includes('configuration') &&
|
|
258
|
-
!r.name.includes('Overall')
|
|
308
|
+
const fieldResults = results.filter(r =>
|
|
309
|
+
r.name.includes('configuration') && !r.name.includes('Overall')
|
|
259
310
|
);
|
|
260
|
-
|
|
311
|
+
|
|
261
312
|
const configuredItemsCount = fieldResults.filter(r => r.success).length;
|
|
262
|
-
const
|
|
263
|
-
const configurationPercentage = Math.round((configuredItemsCount / totalItems) * 100);
|
|
313
|
+
const configurationPercentage = Math.round((configuredItemsCount / fieldResults.length) * 100);
|
|
264
314
|
|
|
265
315
|
summaryResults.push({
|
|
266
316
|
name: 'Overall configuration status',
|
|
267
317
|
success: configurationPercentage > 50 && missingRequiredKeys.length === 0,
|
|
268
|
-
details: `${configurationPercentage}% of specs.json has been configured (${configuredItemsCount}/${
|
|
318
|
+
details: `${configurationPercentage}% of specs.json has been configured (${configuredItemsCount}/${fieldResults.length} items)`
|
|
269
319
|
});
|
|
270
320
|
|
|
271
321
|
return summaryResults;
|
|
272
322
|
}
|
|
273
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Load and parse configuration files
|
|
326
|
+
* @param {string} projectRoot - Root directory of the project
|
|
327
|
+
* @returns {Object} - Object containing parsed specs and file paths
|
|
328
|
+
*/
|
|
329
|
+
function loadConfigurationFiles(projectRoot) {
|
|
330
|
+
const projectSpecsPath = path.join(projectRoot, 'specs.json');
|
|
331
|
+
const defaultSpecsPath = path.join(
|
|
332
|
+
__dirname, '..', 'install-from-boilerplate', 'boilerplate', 'specs.json'
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const fileCheckResults = checkFilesExist(projectSpecsPath, defaultSpecsPath);
|
|
336
|
+
if (fileCheckResults) {
|
|
337
|
+
return { error: fileCheckResults };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const projectSpecs = JSON.parse(fs.readFileSync(projectSpecsPath, 'utf8'));
|
|
341
|
+
const defaultSpecs = JSON.parse(fs.readFileSync(defaultSpecsPath, 'utf8'));
|
|
342
|
+
|
|
343
|
+
return { projectSpecs, defaultSpecs };
|
|
344
|
+
}
|
|
345
|
+
|
|
274
346
|
/**
|
|
275
347
|
* Check if specs.json has been configured from default
|
|
276
348
|
* @param {string} projectRoot - Root directory of the project
|
|
@@ -278,27 +350,8 @@ function generateSummaryResults(results, missingRequiredKeys, unexpectedKeys) {
|
|
|
278
350
|
*/
|
|
279
351
|
async function checkSpecsJsonConfiguration(projectRoot) {
|
|
280
352
|
try {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// Path to the default boilerplate specs.json
|
|
285
|
-
const defaultSpecsPath = path.join(
|
|
286
|
-
__dirname,
|
|
287
|
-
'..',
|
|
288
|
-
'install-from-boilerplate',
|
|
289
|
-
'boilerplate',
|
|
290
|
-
'specs.json'
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
// Check if required files exist
|
|
294
|
-
const fileCheckResults = checkFilesExist(projectSpecsPath, defaultSpecsPath);
|
|
295
|
-
if (fileCheckResults) {
|
|
296
|
-
return fileCheckResults;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Read both files
|
|
300
|
-
const projectSpecs = JSON.parse(fs.readFileSync(projectSpecsPath, 'utf8'));
|
|
301
|
-
const defaultSpecs = JSON.parse(fs.readFileSync(defaultSpecsPath, 'utf8'));
|
|
353
|
+
const { error, projectSpecs, defaultSpecs } = loadConfigurationFiles(projectRoot);
|
|
354
|
+
if (error) return error;
|
|
302
355
|
|
|
303
356
|
const results = [{
|
|
304
357
|
name: 'specs.json exists',
|
|
@@ -306,36 +359,17 @@ async function checkSpecsJsonConfiguration(projectRoot) {
|
|
|
306
359
|
details: 'Project specs.json file found'
|
|
307
360
|
}];
|
|
308
361
|
|
|
309
|
-
// Define required and optional fields based on the default specs.json
|
|
310
362
|
const defaultSpecKeys = Object.keys(defaultSpecs.specs?.[0] || {});
|
|
311
|
-
const {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const missingRequiredKeys = [];
|
|
363
|
+
const { results: fieldResults, missingRequiredKeys } = processFieldValidation(
|
|
364
|
+
projectSpecs, defaultSpecs, defaultSpecKeys
|
|
365
|
+
);
|
|
315
366
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
missingRequiredKeys.push(field.key);
|
|
320
|
-
}
|
|
321
|
-
results.push(result);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Check optional fields
|
|
325
|
-
for (const field of optionalFields) {
|
|
326
|
-
results.push(evaluateOptionalField(field, projectSpecs, defaultSpecs));
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Check for unexpected fields
|
|
330
|
-
const allStandardKeys = [...requiredFields, ...optionalFields].map(f => f.key);
|
|
331
|
-
const unexpectedKeys = Object.keys(projectSpecs.specs?.[0] || {})
|
|
332
|
-
.filter(key => !allStandardKeys.includes(key));
|
|
333
|
-
|
|
334
|
-
// Add summary results
|
|
367
|
+
results.push(...fieldResults);
|
|
368
|
+
|
|
369
|
+
const unexpectedKeys = findUnexpectedFields(projectSpecs, defaultSpecKeys);
|
|
335
370
|
const summaryResults = generateSummaryResults(results, missingRequiredKeys, unexpectedKeys);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return results;
|
|
371
|
+
|
|
372
|
+
return [...results, ...summaryResults];
|
|
339
373
|
|
|
340
374
|
} catch (error) {
|
|
341
375
|
console.error('Error checking specs.json configuration:', error);
|
package/src/health-check.js
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Spec-Up-T Health Check Tool
|
|
5
|
+
*
|
|
6
|
+
* This script performs comprehensive health checks on Spec-Up-T projects,
|
|
7
|
+
* validating configuration, external references, term definitions, and more.
|
|
8
|
+
* Generates an HTML report with detailed results and actionable feedback.
|
|
9
|
+
*
|
|
10
|
+
* @author Spec-Up-T Team
|
|
11
|
+
* @version 1.0.0
|
|
12
|
+
* @since 2025-06-06
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} HealthCheckResult
|
|
17
|
+
* @property {string} name - Name of the specific check
|
|
18
|
+
* @property {boolean|string} success - Success status (true, false, or 'partial')
|
|
19
|
+
* @property {string} [status] - Status override ('warning', 'pass', 'fail')
|
|
20
|
+
* @property {string} [details] - Additional details about the check result
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} HealthCheckSection
|
|
25
|
+
* @property {string} title - Title of the check section
|
|
26
|
+
* @property {HealthCheckResult[]} results - Array of individual check results
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} RepositoryInfo
|
|
31
|
+
* @property {string} host - Git hosting service (e.g., 'github')
|
|
32
|
+
* @property {string} account - Account/organization name
|
|
33
|
+
* @property {string} repo - Repository name
|
|
34
|
+
* @property {string} [branch] - Branch name
|
|
35
|
+
* @property {boolean} [verified] - Whether repository existence was verified
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {Object} StatusDisplay
|
|
40
|
+
* @property {string} statusClass - CSS class for styling
|
|
41
|
+
* @property {string} statusIcon - HTML icon element
|
|
42
|
+
* @property {string} statusText - Display text for status
|
|
43
|
+
*/
|
|
44
|
+
|
|
3
45
|
const fs = require('fs');
|
|
4
46
|
const path = require('path');
|
|
5
47
|
const https = require('https');
|
|
@@ -13,7 +55,10 @@ const termsIntroChecker = require('./health-check/terms-intro-checker');
|
|
|
13
55
|
const destinationGitignoreChecker = require('./health-check/destination-gitignore-checker');
|
|
14
56
|
const trefTermChecker = require('./health-check/tref-term-checker');
|
|
15
57
|
|
|
16
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Directory where health check reports are generated
|
|
60
|
+
* @constant {string}
|
|
61
|
+
*/
|
|
17
62
|
const OUTPUT_DIR = path.join(process.cwd(), '.cache');
|
|
18
63
|
|
|
19
64
|
// Create output directory if it doesn't exist
|
|
@@ -21,7 +66,21 @@ if (!fs.existsSync(OUTPUT_DIR)) {
|
|
|
21
66
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
22
67
|
}
|
|
23
68
|
|
|
24
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Retrieves repository information from specs.json file
|
|
71
|
+
*
|
|
72
|
+
* Reads the specs.json file to extract repository configuration including
|
|
73
|
+
* host, account, repo name, and branch. Falls back to default values if
|
|
74
|
+
* the file doesn't exist or is missing required fields.
|
|
75
|
+
*
|
|
76
|
+
* @async
|
|
77
|
+
* @function getRepoInfo
|
|
78
|
+
* @returns {Promise<RepositoryInfo>} Repository information object
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* const repoInfo = await getRepoInfo();
|
|
82
|
+
* console.log(repoInfo.account); // 'blockchain-bird'
|
|
83
|
+
*/
|
|
25
84
|
async function getRepoInfo() {
|
|
26
85
|
try {
|
|
27
86
|
// Path to the default boilerplate specs.json
|
|
@@ -132,7 +191,24 @@ async function getRepoInfo() {
|
|
|
132
191
|
};
|
|
133
192
|
}
|
|
134
193
|
|
|
135
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Checks if a Git repository exists and is accessible
|
|
196
|
+
*
|
|
197
|
+
* Makes an HTTP HEAD request to verify repository existence without
|
|
198
|
+
* downloading the full repository content. Handles timeouts and errors gracefully.
|
|
199
|
+
*
|
|
200
|
+
* @function checkRepositoryExists
|
|
201
|
+
* @param {string} host - Git hosting service (e.g., 'github')
|
|
202
|
+
* @param {string} account - Account or organization name
|
|
203
|
+
* @param {string} repo - Repository name
|
|
204
|
+
* @returns {Promise<boolean>} True if repository exists and is accessible
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* const exists = await checkRepositoryExists('github', 'blockchain-bird', 'spec-up-t');
|
|
208
|
+
* if (exists) {
|
|
209
|
+
* console.log('Repository is accessible');
|
|
210
|
+
* }
|
|
211
|
+
*/
|
|
136
212
|
function checkRepositoryExists(host, account, repo) {
|
|
137
213
|
return new Promise((resolve) => {
|
|
138
214
|
const url = `https://${host}.com/${account}/${repo}`;
|
|
@@ -163,7 +239,19 @@ function checkRepositoryExists(host, account, repo) {
|
|
|
163
239
|
});
|
|
164
240
|
}
|
|
165
241
|
|
|
166
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Formats current timestamp for use in filenames
|
|
244
|
+
*
|
|
245
|
+
* Generates a timestamp string that is safe to use in filenames by
|
|
246
|
+
* replacing special characters with hyphens. Format: YYYY-MM-DD-HH-mm-ssZ
|
|
247
|
+
*
|
|
248
|
+
* @function getFormattedTimestamp
|
|
249
|
+
* @returns {string} Formatted timestamp string suitable for filenames
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* const timestamp = getFormattedTimestamp();
|
|
253
|
+
* console.log(timestamp); // "2025-06-06-14-30-25Z"
|
|
254
|
+
*/
|
|
167
255
|
function getFormattedTimestamp() {
|
|
168
256
|
const now = new Date();
|
|
169
257
|
return now.toISOString()
|
|
@@ -172,12 +260,37 @@ function getFormattedTimestamp() {
|
|
|
172
260
|
.replace(/Z/g, 'Z');
|
|
173
261
|
}
|
|
174
262
|
|
|
175
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Generates a human-readable timestamp for display in reports
|
|
265
|
+
*
|
|
266
|
+
* Creates a localized timestamp string for display purposes,
|
|
267
|
+
* using the system's default locale and timezone.
|
|
268
|
+
*
|
|
269
|
+
* @function getHumanReadableTimestamp
|
|
270
|
+
* @returns {string} Human-readable timestamp string
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* const readable = getHumanReadableTimestamp();
|
|
274
|
+
* console.log(readable); // "6/6/2025, 2:30:25 PM"
|
|
275
|
+
*/
|
|
176
276
|
function getHumanReadableTimestamp() {
|
|
177
277
|
return new Date().toLocaleString();
|
|
178
278
|
}
|
|
179
279
|
|
|
180
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Determines status display parameters based on check result
|
|
282
|
+
*
|
|
283
|
+
* Analyzes check results to determine appropriate CSS classes,
|
|
284
|
+
* icons, and text for visual status representation in the HTML report.
|
|
285
|
+
*
|
|
286
|
+
* @function getStatusDisplay
|
|
287
|
+
* @param {HealthCheckResult} result - Check result object
|
|
288
|
+
* @returns {StatusDisplay} Status display configuration
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* const display = getStatusDisplay({ success: true });
|
|
292
|
+
* console.log(display.statusText); // "Pass"
|
|
293
|
+
*/
|
|
181
294
|
function getStatusDisplay(result) {
|
|
182
295
|
if (result.status === 'warning' || result.success === 'partial') {
|
|
183
296
|
// Warning status
|
|
@@ -203,7 +316,34 @@ function getStatusDisplay(result) {
|
|
|
203
316
|
}
|
|
204
317
|
}
|
|
205
318
|
|
|
206
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Main function to run all health checks and generate the report
|
|
321
|
+
*
|
|
322
|
+
* Orchestrates the execution of all available health check modules,
|
|
323
|
+
* collects results, and generates a comprehensive HTML report.
|
|
324
|
+
* Handles errors gracefully and ensures proper cleanup.
|
|
325
|
+
*
|
|
326
|
+
* @async
|
|
327
|
+
* @function runHealthCheck
|
|
328
|
+
* @throws {Error} When health check execution fails
|
|
329
|
+
*
|
|
330
|
+
* @description
|
|
331
|
+
* The function performs the following checks:
|
|
332
|
+
* - Term reference checks in external specs
|
|
333
|
+
* - External specs URL validation
|
|
334
|
+
* - Term references validation
|
|
335
|
+
* - specs.json configuration validation
|
|
336
|
+
* - Terms introduction file validation
|
|
337
|
+
* - .gitignore destination directory check
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* try {
|
|
341
|
+
* await runHealthCheck();
|
|
342
|
+
* console.log('Health check completed successfully');
|
|
343
|
+
* } catch (error) {
|
|
344
|
+
* console.error('Health check failed:', error);
|
|
345
|
+
* }
|
|
346
|
+
*/
|
|
207
347
|
async function runHealthCheck() {
|
|
208
348
|
console.log('Running health checks...');
|
|
209
349
|
|
|
@@ -264,7 +404,27 @@ async function runHealthCheck() {
|
|
|
264
404
|
}
|
|
265
405
|
}
|
|
266
406
|
|
|
267
|
-
|
|
407
|
+
/**
|
|
408
|
+
* Generates and opens an HTML health check report
|
|
409
|
+
*
|
|
410
|
+
* Creates a comprehensive HTML report with all health check results,
|
|
411
|
+
* saves it to the cache directory, and opens it in the default browser.
|
|
412
|
+
* The report includes interactive features like filtering and status indicators.
|
|
413
|
+
*
|
|
414
|
+
* @async
|
|
415
|
+
* @function generateReport
|
|
416
|
+
* @param {HealthCheckSection[]} checkResults - Array of health check result objects
|
|
417
|
+
* @throws {Error} When report generation or file operations fail
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* const results = [
|
|
421
|
+
* {
|
|
422
|
+
* title: 'Configuration Check',
|
|
423
|
+
* results: [{ name: 'specs.json', success: true, details: 'Valid' }]
|
|
424
|
+
* }
|
|
425
|
+
* ];
|
|
426
|
+
* await generateReport(results);
|
|
427
|
+
*/
|
|
268
428
|
async function generateReport(checkResults) {
|
|
269
429
|
const timestamp = getFormattedTimestamp();
|
|
270
430
|
// Get repository information from specs.json
|
|
@@ -289,7 +449,26 @@ async function generateReport(checkResults) {
|
|
|
289
449
|
}
|
|
290
450
|
}
|
|
291
451
|
|
|
292
|
-
|
|
452
|
+
/**
|
|
453
|
+
* Generates HTML content for the health check report
|
|
454
|
+
*
|
|
455
|
+
* Creates a complete HTML document with Bootstrap styling, interactive features,
|
|
456
|
+
* and comprehensive health check results. Includes repository verification,
|
|
457
|
+
* status filtering, and detailed result tables.
|
|
458
|
+
*
|
|
459
|
+
* @function generateHtmlReport
|
|
460
|
+
* @param {HealthCheckSection[]} checkResults - Array of health check result sections
|
|
461
|
+
* @param {string} timestamp - Human-readable timestamp for the report
|
|
462
|
+
* @param {RepositoryInfo} repoInfo - Repository information object
|
|
463
|
+
* @returns {string} Complete HTML document as string
|
|
464
|
+
*
|
|
465
|
+
* @example
|
|
466
|
+
* const html = generateHtmlReport(results, '6/6/2025, 2:30:25 PM', {
|
|
467
|
+
* host: 'github',
|
|
468
|
+
* account: 'blockchain-bird',
|
|
469
|
+
* repo: 'spec-up-t'
|
|
470
|
+
* });
|
|
471
|
+
*/
|
|
293
472
|
function generateHtmlReport(checkResults, timestamp, repoInfo) {
|
|
294
473
|
let resultsHtml = '';
|
|
295
474
|
|
|
@@ -457,5 +636,15 @@ function generateHtmlReport(checkResults, timestamp, repoInfo) {
|
|
|
457
636
|
`;
|
|
458
637
|
}
|
|
459
638
|
|
|
460
|
-
|
|
639
|
+
/**
|
|
640
|
+
* Script execution entry point
|
|
641
|
+
*
|
|
642
|
+
* Immediately executes the health check when this script is run directly.
|
|
643
|
+
* This allows the script to be used as a standalone command-line tool.
|
|
644
|
+
*
|
|
645
|
+
* @example
|
|
646
|
+
* // Run from command line:
|
|
647
|
+
* // node src/health-check.js
|
|
648
|
+
* // npm run healthCheck
|
|
649
|
+
*/
|
|
461
650
|
runHealthCheck();
|
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file fetchTermsFromIndex.js
|
|
3
|
-
* @description Fetches terms and definitions from external repository's index.html
|
|
4
|
-
* @author Generated with assistance from GitHub Copilot
|
|
5
|
-
* @version 1.0.0
|
|
6
|
-
* @since 2025-04-15
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const { JSDOM } = require('jsdom');
|
|
12
|
-
const axios = require('axios');
|
|
13
|
-
const { addPath, getPath, getAllPaths } = require('../../config/paths');
|
|
14
|
-
const crypto = require('crypto');
|
|
15
|
-
|
|
16
|
-
// Directory to store cached files
|
|
17
|
-
const CACHE_DIR = getPath('githubcache');
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Generates a cache key based on repository information
|
|
21
|
-
* @param {string} owner - Repository owner
|
|
22
|
-
* @param {string} repo - Repository name
|
|
23
|
-
* @returns {string} - Cache key
|
|
24
|
-
*/
|
|
25
|
-
function generateCacheKey(owner, repo) {
|
|
26
|
-
const input = `${owner}-${repo}-index`;
|
|
27
|
-
return crypto.createHash('md5').update(input).digest('hex');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Checks if a cached version exists and is valid
|
|
32
|
-
* @param {string} cacheKey - Cache key
|
|
33
|
-
* @param {object} options - Options object
|
|
34
|
-
* @param {number} options.cacheTTL - Time-to-live for cache in milliseconds (default: 24 hours)
|
|
35
|
-
* @returns {object|null} - Cached data or null if not found or expired
|
|
36
|
-
* @example
|
|
37
|
-
* const cacheTTL = options.cacheTTL || 24 * 60 * 60 * 1000; // Default: 24 hours
|
|
38
|
-
*/
|
|
39
|
-
function getFromCache(cacheKey, options = {}) {
|
|
40
|
-
const cachePath = path.join(CACHE_DIR, `${cacheKey}.json`);
|
|
41
|
-
// Use default 24h TTL instead of 0
|
|
42
|
-
const cacheTTL = options.cacheTTL || 24 * 60 * 60 * 1000;
|
|
43
|
-
|
|
44
|
-
// Early return if file doesn't exist
|
|
45
|
-
if (!fs.existsSync(cachePath)) {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Try to read and parse the cache file
|
|
50
|
-
let cacheData;
|
|
51
|
-
try {
|
|
52
|
-
cacheData = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
53
|
-
} catch (error) {
|
|
54
|
-
console.log(`Error reading cache for key: ${cacheKey}`);
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Check if cache has timestamp and is not expired
|
|
59
|
-
const cacheTime = new Date(cacheData.timestamp).getTime();
|
|
60
|
-
const currentTime = new Date().getTime();
|
|
61
|
-
const isExpired = currentTime - cacheTime > cacheTTL;
|
|
62
|
-
|
|
63
|
-
if (isExpired) {
|
|
64
|
-
console.log(`Cache expired for key: ${cacheKey}`);
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
console.log(`Using cached data for key: ${cacheKey}`);
|
|
69
|
-
return cacheData;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Saves data to cache
|
|
74
|
-
* @param {string} cacheKey - Cache key
|
|
75
|
-
* @param {object} data - Data to cache
|
|
76
|
-
*/
|
|
77
|
-
function saveToCache(cacheKey, data) {
|
|
78
|
-
const cachePath = path.join(CACHE_DIR, `${cacheKey}.json`);
|
|
79
|
-
const cacheData = {
|
|
80
|
-
timestamp: new Date().toISOString(),
|
|
81
|
-
...data
|
|
82
|
-
};
|
|
83
|
-
fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2));
|
|
84
|
-
console.log(`Saved to cache: ${cacheKey}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Fetches the latest commit hash for a specific file in a repository
|
|
89
|
-
* @param {string} token - GitHub API Token
|
|
90
|
-
* @param {string} owner - Repository owner
|
|
91
|
-
* @param {string} repo - Repository name
|
|
92
|
-
* @param {string} filePath - Path to the file in the repository
|
|
93
|
-
* @param {object} headers - Request headers
|
|
94
|
-
* @returns {string|null} - Latest commit hash or null if error
|
|
95
|
-
*/
|
|
96
|
-
async function getFileCommitHash(token, owner, repo, filePath, headers) {
|
|
97
|
-
try {
|
|
98
|
-
// Normalize the file path to ensure it doesn't have leading slash
|
|
99
|
-
const normalizedPath = filePath.replace(/^\//, '');
|
|
100
|
-
|
|
101
|
-
// Construct API URL to get commits for the specific file
|
|
102
|
-
const commitsUrl = `https://api.github.com/repos/${owner}/${repo}/commits?path=${normalizedPath}&per_page=1`;
|
|
103
|
-
console.log(`Fetching latest commit for file: ${commitsUrl}`);
|
|
104
|
-
|
|
105
|
-
const response = await axios.get(commitsUrl, { headers });
|
|
106
|
-
|
|
107
|
-
if (response.status !== 200 || !response.data || response.data.length === 0) {
|
|
108
|
-
console.log(`❌ Could not find commit information for ${filePath}`);
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Return the SHA of the latest commit
|
|
113
|
-
return response.data[0].sha;
|
|
114
|
-
} catch (error) {
|
|
115
|
-
console.error(`❌ Error fetching commit hash: ${error.message}`);
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Fetches all terms and definitions from a repository's index.html
|
|
122
|
-
* @param {string} token - GitHub API Token
|
|
123
|
-
* @param {string} owner - Repository owner
|
|
124
|
-
* @param {string} repo - Repository name
|
|
125
|
-
* @param {object} options - Additional options
|
|
126
|
-
* @returns {object|null} - Object containing all terms or null if error
|
|
127
|
-
*/
|
|
128
|
-
async function fetchAllTermsFromIndex(token, owner, repo, options = {}) {
|
|
129
|
-
try {
|
|
130
|
-
// Generate cache key based on repo information
|
|
131
|
-
const cacheKey = generateCacheKey(owner, repo);
|
|
132
|
-
let cachedData = null;
|
|
133
|
-
|
|
134
|
-
// Check cache first if caching is enabled
|
|
135
|
-
if (options.cache !== false) {
|
|
136
|
-
cachedData = getFromCache(cacheKey, options);
|
|
137
|
-
if (cachedData) {
|
|
138
|
-
return cachedData;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Configure headers for GitHub API
|
|
143
|
-
const headers = {};
|
|
144
|
-
if (token) {
|
|
145
|
-
headers['Authorization'] = `token ${token}`;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Get the specs.json content from the repository to find the output_path
|
|
149
|
-
const specsJsonUrl = `https://api.github.com/repos/${owner}/${repo}/contents/specs.json`;
|
|
150
|
-
console.log(`Fetching specs.json from: ${specsJsonUrl}`);
|
|
151
|
-
|
|
152
|
-
// Fetch specs.json content
|
|
153
|
-
const specsJsonResponse = await axios.get(specsJsonUrl, { headers });
|
|
154
|
-
if (specsJsonResponse.status !== 200) {
|
|
155
|
-
console.log(`❌ Could not find specs.json in repository ${owner}/${repo}`);
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Decode specs.json content from base64
|
|
160
|
-
const specsJsonContent = Buffer.from(specsJsonResponse.data.content, 'base64').toString('utf8');
|
|
161
|
-
const specsJson = JSON.parse(specsJsonContent);
|
|
162
|
-
|
|
163
|
-
// Get the output_path from specs.json
|
|
164
|
-
const outputPath = specsJson.specs[0].output_path;
|
|
165
|
-
if (!outputPath) {
|
|
166
|
-
console.log(`❌ No output_path found in specs.json for repository ${owner}/${repo}`);
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Fix: Properly normalize the output path to ensure it doesn't have leading "./" or trailing "/"
|
|
171
|
-
const normalizedOutputPath = outputPath.replace(/^\.\//, '').replace(/\/$/, '');
|
|
172
|
-
|
|
173
|
-
// Create the path to the index.html file
|
|
174
|
-
const indexHtmlPath = `${normalizedOutputPath}/index.html`;
|
|
175
|
-
|
|
176
|
-
// Fetch the index.html content with properly constructed URL
|
|
177
|
-
const indexHtmlUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/${indexHtmlPath}`;
|
|
178
|
-
console.log(`Fetching index.html from: ${indexHtmlUrl}`);
|
|
179
|
-
|
|
180
|
-
const indexHtmlResponse = await axios.get(indexHtmlUrl, { headers });
|
|
181
|
-
if (indexHtmlResponse.status !== 200) {
|
|
182
|
-
console.log(`❌ Could not find index.html at ${indexHtmlUrl}`);
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Get the commit hash for the index.html file
|
|
187
|
-
const commitHash = await getFileCommitHash(token, owner, repo, indexHtmlPath, headers);
|
|
188
|
-
if (!commitHash) {
|
|
189
|
-
console.log(`⚠️ Could not get commit hash for index.html, continuing without it`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const htmlContent = indexHtmlResponse.data;
|
|
193
|
-
|
|
194
|
-
// Parse HTML using JSDOM
|
|
195
|
-
const dom = new JSDOM(htmlContent);
|
|
196
|
-
const document = dom.window.document;
|
|
197
|
-
|
|
198
|
-
// Find all term definition lists with class "terms-and-definitions-list"
|
|
199
|
-
const termDlList = document.querySelector('dl.terms-and-definitions-list');
|
|
200
|
-
if (!termDlList) {
|
|
201
|
-
console.log(`❌ No terms-and-definitions-list found in ${indexHtmlUrl}`);
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Extract all terms and definitions
|
|
206
|
-
const terms = [];
|
|
207
|
-
let dtElements = termDlList.querySelectorAll('dt');
|
|
208
|
-
|
|
209
|
-
dtElements.forEach(dt => {
|
|
210
|
-
// Find the term span that starts with id="term:
|
|
211
|
-
const termSpan = dt.querySelector('span[id^="term:"]');
|
|
212
|
-
if (!termSpan) return;
|
|
213
|
-
|
|
214
|
-
// Get the term text (all text content, excluding nested spans)
|
|
215
|
-
let termText = '';
|
|
216
|
-
for (let node of termSpan.childNodes) {
|
|
217
|
-
if (node.nodeType === dom.window.Node.TEXT_NODE) {
|
|
218
|
-
termText += node.textContent.trim();
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// If no text found, try to get the full text content
|
|
223
|
-
if (!termText) {
|
|
224
|
-
termText = termSpan.textContent.trim();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Skip empty terms
|
|
228
|
-
if (!termText) return;
|
|
229
|
-
|
|
230
|
-
// Find all corresponding definition elements
|
|
231
|
-
let dds = [];
|
|
232
|
-
let nextElement = dt.nextElementSibling;
|
|
233
|
-
|
|
234
|
-
// Collect all consecutive <dd> elements until we reach another <dt>
|
|
235
|
-
while (nextElement && nextElement.tagName.toLowerCase() === 'dd') {
|
|
236
|
-
dds.push(nextElement.outerHTML);
|
|
237
|
-
nextElement = nextElement.nextElementSibling;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
terms.push({
|
|
241
|
-
term: termText,
|
|
242
|
-
definition: dds.join('\n')
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// Store all terms in a JSON file with timestamp
|
|
247
|
-
const timestamp = Date.now();
|
|
248
|
-
const outputDir = path.join(CACHE_DIR);
|
|
249
|
-
if (!fs.existsSync(outputDir)) {
|
|
250
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Create output filename with timestamp
|
|
254
|
-
const outputFileName = `${timestamp}-${owner}-${repo}-terms.json`;
|
|
255
|
-
const outputFilePath = path.join(outputDir, outputFileName);
|
|
256
|
-
|
|
257
|
-
// Create the result object
|
|
258
|
-
const result = {
|
|
259
|
-
timestamp,
|
|
260
|
-
repository: `${owner}/${repo}`,
|
|
261
|
-
terms,
|
|
262
|
-
sha: commitHash, // Use the commit hash of the index.html file
|
|
263
|
-
avatarUrl: null,
|
|
264
|
-
outputFileName
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
// Save all terms to file
|
|
268
|
-
fs.writeFileSync(outputFilePath, JSON.stringify(result, null, 2));
|
|
269
|
-
console.log(`✅ Saved ${terms.length} terms to ${outputFilePath}`);
|
|
270
|
-
|
|
271
|
-
// Save to cache if enabled
|
|
272
|
-
if (options.cache !== false) {
|
|
273
|
-
saveToCache(cacheKey, result);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return result;
|
|
277
|
-
|
|
278
|
-
} catch (error) {
|
|
279
|
-
if (error.response) {
|
|
280
|
-
if (error.response.status === 404) {
|
|
281
|
-
console.log(`❌ Resource not found: ${error.config.url}`);
|
|
282
|
-
} else if (error.response.status === 403 && error.response.headers['x-ratelimit-remaining'] === '0') {
|
|
283
|
-
const resetTime = new Date(parseInt(error.response.headers['x-ratelimit-reset']) * 1000);
|
|
284
|
-
console.error(`❌ GitHub API rate limit exceeded. Try again after ${resetTime.toLocaleString()}`);
|
|
285
|
-
} else {
|
|
286
|
-
console.error(`❌ Error fetching data: ${error.response.status} ${error.response.statusText}`);
|
|
287
|
-
}
|
|
288
|
-
} else {
|
|
289
|
-
console.error(`❌ Error fetching term: ${error.message}`);
|
|
290
|
-
}
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Fetches a specific term from repository's index.html
|
|
297
|
-
* This is a wrapper that uses fetchAllTermsFromIndex for efficiency
|
|
298
|
-
* @param {string} token - GitHub API Token
|
|
299
|
-
* @param {string} term - The specific term to look for
|
|
300
|
-
* @param {string} owner - Repository owner
|
|
301
|
-
* @param {string} repo - Repository name
|
|
302
|
-
* @param {string} termsDir - Directory containing term definitions (not used in this implementation)
|
|
303
|
-
* @param {object} options - Additional options
|
|
304
|
-
* @returns {object|null} - Found term data or null if not found
|
|
305
|
-
*/
|
|
306
|
-
async function fetchTermsFromIndex(token, term, owner, repo, termsDir, options = {}) {
|
|
307
|
-
// First get all terms from the repository (which is cached)
|
|
308
|
-
const allTermsData = await fetchAllTermsFromIndex(token, owner, repo, options);
|
|
309
|
-
|
|
310
|
-
if (!allTermsData || !allTermsData.terms) {
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Find the specific term
|
|
315
|
-
const foundTerm = allTermsData.terms.find(t => t.term.toLowerCase() === term.toLowerCase());
|
|
316
|
-
|
|
317
|
-
if (foundTerm) {
|
|
318
|
-
console.log(`Found term '${term}' in repository ${owner}/${repo}`);
|
|
319
|
-
return {
|
|
320
|
-
term: foundTerm.term,
|
|
321
|
-
content: foundTerm.definition,
|
|
322
|
-
sha: allTermsData.sha,
|
|
323
|
-
repository: {
|
|
324
|
-
owner: {
|
|
325
|
-
login: owner,
|
|
326
|
-
avatar_url: allTermsData.avatarUrl
|
|
327
|
-
},
|
|
328
|
-
name: repo
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
} else {
|
|
332
|
-
console.log(`❌ Term "${term}" not found in repository ${owner}/${repo}`);
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
module.exports = {
|
|
338
|
-
fetchTermsFromIndex,
|
|
339
|
-
fetchAllTermsFromIndex // Export the function to fetch all terms as well
|
|
340
|
-
};
|