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.
@@ -0,0 +1,3 @@
1
+ - All code will have to pass SonarQube analysis
2
+ - Cognitive complexity should be kept ideally below 15
3
+ - Remove code if possible, instead of adding code
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
- // `doc` is markdown
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.7",
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. Validates GitHub PAT availability and external repository configurations
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(explanationPAT);
317
- const userInput = readlineSync.question('ℹ️ Press any key');
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
- else if (externalSpecsRepos.length === 0) {
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(key => ({
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(key => ({
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 required field and generate result
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 evaluateRequiredField(field, projectSpecs, defaultSpecs) {
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: false,
136
- details: `Required "${field.key}" key is missing in specs.json`
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
- let configured = isFieldConfigured(projectValue, defaultValue);
238
+ const isConfigured = field.allowDefaultValue || isFieldConfigured(projectValue, defaultValue);
143
239
 
144
- // For fields that can keep their default values, mark as configured
145
- if (field.allowDefaultValue) {
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
- if (!configured) {
153
- if (field.mustChange) {
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
- } else {
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
- const hasField = projectSpecs.specs?.[0]?.hasOwnProperty(field.key);
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
- // Add a summary of missing required fields
232
- if (missingRequiredKeys.length > 0) {
233
- summaryResults.push({
234
- name: 'Required fields check',
235
- success: false,
236
- details: `Missing required fields: ${missingRequiredKeys.join(', ')}`
237
- });
238
- } else {
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
- // Check for unexpected fields
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 totalItems = fieldResults.length;
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}/${totalItems} items)`
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
- // Path to the project's specs.json
282
- const projectSpecsPath = path.join(projectRoot, 'specs.json');
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 { requiredFields, optionalFields } = categorizeFields(defaultSpecKeys);
312
-
313
- // Check each required field
314
- const missingRequiredKeys = [];
363
+ const { results: fieldResults, missingRequiredKeys } = processFieldValidation(
364
+ projectSpecs, defaultSpecs, defaultSpecKeys
365
+ );
315
366
 
316
- for (const field of requiredFields) {
317
- const result = evaluateRequiredField(field, projectSpecs, defaultSpecs);
318
- if (!result.success && result.details.includes('missing')) {
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
- results.push(...summaryResults);
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);
@@ -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
- // Configuration
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
- // Helper function to read specs.json file
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
- // Helper function to check if a repository exists
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
- // Helper function to format current time for the filename
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
- // Helper function to generate a human-readable timestamp for display
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
- // Helper function to determine status display parameters based on result
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
- // Main function to run all checks and generate the report
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
- // Generate HTML report
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
- // Generate HTML content
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
- // Run the health check
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
- };