spec-up-t 1.2.7 → 1.2.9
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 +4 -0
- package/assets/compiled/body.js +2 -1
- package/assets/compiled/head.css +1 -0
- package/assets/compiled/refs.json +1 -1
- package/assets/css/highlight-heading-plus-sibling-nodes.css +6 -0
- package/assets/css/index.css +9 -0
- package/assets/js/collapse-definitions.js +0 -6
- package/assets/js/highlight-heading-plus-sibling-nodes.js +259 -0
- package/config/asset-map.json +2 -0
- package/gulpfile.js +8 -2
- package/index.js +24 -16
- package/package.json +1 -1
- package/src/collect-external-references.js +10 -23
- package/src/escape-handler.js +67 -0
- 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/markdown-it-extensions.js +8 -0
- package/src/collectExternalReferences/fetchTermsFromIndex.1.js +0 -340
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,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { ESCAPED_PLACEHOLDER } = require('./escape-handler');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Configuration for custom template syntax [[example]] used throughout the markdown parsing
|
|
5
7
|
* These constants define how template markers are identified and processed
|
|
@@ -64,6 +66,12 @@ module.exports = function (md, templates = {}) {
|
|
|
64
66
|
md.inline.ruler.after('emphasis', 'templates', function templates_ruler(state, silent) {
|
|
65
67
|
// Get the current parsing position
|
|
66
68
|
var start = state.pos;
|
|
69
|
+
|
|
70
|
+
// Check if we're at an escaped placeholder - if so, skip processing
|
|
71
|
+
if (state.src.slice(start, start + ESCAPED_PLACEHOLDER.length) === ESCAPED_PLACEHOLDER) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
// Check if we're at a template opening marker
|
|
68
76
|
let prefix = state.src.slice(start, start + levels);
|
|
69
77
|
if (prefix !== openString) return false;
|
|
@@ -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
|
-
};
|