spec-up-t 1.1.53 → 1.1.55

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,445 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const http = require('http');
6
+ const https = require('https');
7
+ const { execSync } = require('child_process');
8
+
9
+ // Import modules from the health-check directory
10
+ const externalSpecsChecker = require('./health-check/external-specs-checker');
11
+ const termReferencesChecker = require('./health-check/term-references-checker');
12
+ const specsConfigurationChecker = require('./health-check/specs-configuration-checker');
13
+ const termsIntroChecker = require('./health-check/terms-intro-checker');
14
+ const outputGitignoreChecker = require('./health-check/output-gitignore-checker');
15
+ const trefTermChecker = require('./health-check/tref-term-checker');
16
+
17
+ // Configuration
18
+ const OUTPUT_DIR = path.join(process.cwd(), 'output');
19
+
20
+ // Create output directory if it doesn't exist
21
+ if (!fs.existsSync(OUTPUT_DIR)) {
22
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
23
+ }
24
+
25
+ // Helper function to read specs.json file
26
+ function getRepoInfo() {
27
+ try {
28
+ // Path to the default boilerplate specs.json
29
+ const defaultSpecsPath = path.join(
30
+ __dirname,
31
+ 'install-from-boilerplate',
32
+ 'boilerplate',
33
+ 'specs.json'
34
+ );
35
+
36
+ let defaultValues = {
37
+ host: 'github',
38
+ account: 'blockchain-bird',
39
+ repo: 'spec-up-t'
40
+ };
41
+
42
+ // Try to load default values from boilerplate specs.json
43
+ if (fs.existsSync(defaultSpecsPath)) {
44
+ try {
45
+ const defaultSpecsContent = fs.readFileSync(defaultSpecsPath, 'utf8');
46
+ const defaultSpecs = JSON.parse(defaultSpecsContent);
47
+
48
+ if (defaultSpecs?.specs?.[0]?.source) {
49
+ const source = defaultSpecs.specs[0].source;
50
+ defaultValues = {
51
+ host: source.host || defaultValues.host,
52
+ account: source.account || defaultValues.account,
53
+ repo: source.repo || defaultValues.repo
54
+ };
55
+ }
56
+ } catch (error) {
57
+ console.error('Error reading boilerplate specs.json:', error);
58
+ }
59
+ }
60
+
61
+ // Look for specs.json in the current working directory (where the command is run from)
62
+ const specsPath = path.join(process.cwd(), 'specs.json');
63
+ // console.log(`Looking for specs.json at: ${specsPath}`);
64
+
65
+ if (fs.existsSync(specsPath)) {
66
+ // console.log('specs.json found!');
67
+ const specsContent = fs.readFileSync(specsPath, 'utf8');
68
+ const specs = JSON.parse(specsContent);
69
+
70
+ // Check if source field exists and has required properties
71
+ if (specs?.specs?.[0]?.source &&
72
+ specs.specs[0].source.host &&
73
+ specs.specs[0].source.account &&
74
+ specs.specs[0].source.repo) {
75
+
76
+ const sourceInfo = specs.specs[0].source;
77
+
78
+ // Check if values have been changed from defaults
79
+ const valuesChanged =
80
+ sourceInfo.host !== defaultValues.host ||
81
+ sourceInfo.account !== defaultValues.account ||
82
+ sourceInfo.repo !== defaultValues.repo;
83
+
84
+ // If values haven't changed, just return the default values
85
+ // This allows the user to manually change these later
86
+ if (!valuesChanged) {
87
+ return {
88
+ host: sourceInfo.host,
89
+ account: sourceInfo.account,
90
+ repo: sourceInfo.repo,
91
+ branch: sourceInfo.branch || defaultValues.branch
92
+ };
93
+ }
94
+
95
+ // If values have changed, verify the repository exists
96
+ const repoExists = checkRepositoryExists(
97
+ sourceInfo.host,
98
+ sourceInfo.account,
99
+ sourceInfo.repo
100
+ );
101
+
102
+ if (repoExists) {
103
+ // Repository exists, return the verified info
104
+ return {
105
+ host: sourceInfo.host,
106
+ account: sourceInfo.account,
107
+ repo: sourceInfo.repo,
108
+ branch: sourceInfo.branch || defaultValues.branch,
109
+ verified: true
110
+ };
111
+ } else {
112
+ // Repository doesn't exist, but values have been changed
113
+ // Return the values with a verification failed flag
114
+ return {
115
+ host: sourceInfo.host,
116
+ account: sourceInfo.account,
117
+ repo: sourceInfo.repo,
118
+ branch: sourceInfo.branch || defaultValues.branch,
119
+ verified: false
120
+ };
121
+ }
122
+ }
123
+ } else {
124
+ console.log('specs.json not found');
125
+ }
126
+ } catch (error) {
127
+ console.error('Error reading specs.json:', error);
128
+ }
129
+
130
+ // Return default values if specs.json doesn't exist or doesn't contain the required information
131
+ // console.log('Using default repository values');
132
+ return {
133
+ host: 'github',
134
+ account: 'blockchain-bird',
135
+ repo: 'spec-up-t',
136
+ branch: 'main'
137
+ };
138
+ }
139
+
140
+ // Helper function to check if a repository exists
141
+ function checkRepositoryExists(host, account, repo) {
142
+ try {
143
+ // For synchronous checking, we'll use a simple HTTP HEAD request
144
+ const url = `https://${host}.com/${account}/${repo}`;
145
+
146
+ // Simple synchronous HTTP request using execSync
147
+ const cmd = process.platform === 'win32'
148
+ ? `curl -s -o /nul -w "%{http_code}" -I ${url}`
149
+ : `curl -s -o /dev/null -w "%{http_code}" -I ${url}`;
150
+
151
+ const statusCode = execSync(cmd).toString().trim();
152
+
153
+ // 200, 301, 302 status codes indicate the repo exists
154
+ return ['200', '301', '302'].includes(statusCode);
155
+ } catch (error) {
156
+ console.error('Error checking repository existence:', error);
157
+ return false;
158
+ }
159
+ }
160
+
161
+ // Helper function to format current time for the filename
162
+ function getFormattedTimestamp() {
163
+ const now = new Date();
164
+ return now.toISOString()
165
+ .replace(/[T:]/g, '-')
166
+ .replace(/\..+/, '')
167
+ .replace(/[Z]/g, '');
168
+ }
169
+
170
+ // Helper function to generate a human-readable timestamp for display
171
+ function getHumanReadableTimestamp() {
172
+ return new Date().toLocaleString();
173
+ }
174
+
175
+ // Main function to run all checks and generate the report
176
+ async function runHealthCheck() {
177
+ console.log('Running health checks...');
178
+
179
+ // Collection to store all check results
180
+ const results = [];
181
+
182
+ try {
183
+
184
+ // Run term reference tref check
185
+ const trefTermResults = await trefTermChecker.checkTrefTerms(process.cwd());
186
+ results.push({
187
+ title: 'Check Trefs in all external specs',
188
+ results: trefTermResults
189
+ });
190
+
191
+ // Run external specs URL check
192
+ const externalSpecsResults = await externalSpecsChecker.checkExternalSpecs(process.cwd());
193
+ results.push({
194
+ title: 'Check External Specs URL',
195
+ results: externalSpecsResults
196
+ });
197
+
198
+ // Run term references check
199
+ const termReferencesResults = await termReferencesChecker.checkTermReferences(process.cwd());
200
+ results.push({
201
+ title: 'Check Term References',
202
+ results: termReferencesResults
203
+ });
204
+
205
+ // Run specs.json configuration check
206
+ const specsConfigResults = await specsConfigurationChecker.checkSpecsJsonConfiguration(process.cwd());
207
+ results.push({
208
+ title: 'Check <code>specs.json</code> configuration',
209
+ results: specsConfigResults
210
+ });
211
+
212
+ // Run terms-and-definitions-intro.md check
213
+ const termsIntroResults = await termsIntroChecker.checkTermsIntroFile(process.cwd());
214
+ results.push({
215
+ title: 'Check <code>terms-and-definitions-intro.md</code>',
216
+ results: termsIntroResults
217
+ });
218
+
219
+ // Run output directory gitignore check
220
+ const outputGitignoreResults = await outputGitignoreChecker.checkOutputDirGitIgnore(process.cwd());
221
+ results.push({
222
+ title: 'Check <code>.gitignore</code>',
223
+ results: outputGitignoreResults
224
+ });
225
+
226
+ // Add more checks here in the future
227
+
228
+ // Generate and open the report
229
+ generateReport(results);
230
+ } catch (error) {
231
+ console.error('Error running health checks:', error);
232
+ process.exit(1);
233
+ }
234
+ }
235
+
236
+ // Generate HTML report
237
+ function generateReport(checkResults) {
238
+ const timestamp = getFormattedTimestamp();
239
+ // Get repository information from specs.json
240
+ const repoInfo = getRepoInfo();
241
+ const reportFileName = `health-check-${timestamp}-${repoInfo.account}-${repoInfo.repo}.html`;
242
+ const reportPath = path.join(OUTPUT_DIR, reportFileName);
243
+
244
+
245
+ const htmlContent = generateHtmlReport(checkResults, getHumanReadableTimestamp(), repoInfo);
246
+
247
+ fs.writeFileSync(reportPath, htmlContent);
248
+ console.log(`Report generated: ${reportPath}`);
249
+
250
+ // Open the report in the default browser
251
+ try {
252
+ const openCommand = process.platform === 'win32' ? 'start' :
253
+ process.platform === 'darwin' ? 'open' : 'xdg-open';
254
+ execSync(`${openCommand} "${reportPath}"`);
255
+ } catch (error) {
256
+ console.error('Failed to open the report:', error);
257
+ }
258
+ }
259
+
260
+ // Generate HTML content
261
+ function generateHtmlReport(checkResults, timestamp, repoInfo) {
262
+ let resultsHtml = '';
263
+
264
+ // Add repository verification check at the beginning if needed
265
+ if (repoInfo && repoInfo.verified === false) {
266
+ // Create a new section at the top for repository verification
267
+ resultsHtml += `
268
+ <div class="card mb-4 results-card alert-danger" data-section="repository-verification">
269
+ <div class="card-header bg-danger text-white">
270
+ <h5>Repository Verification</h5>
271
+ </div>
272
+ <div class="card-body">
273
+ <table class="table table-striped">
274
+ <thead>
275
+ <tr>
276
+ <th>Status</th>
277
+ <th>Check</th>
278
+ <th>Details</th>
279
+ </tr>
280
+ </thead>
281
+ <tbody>
282
+ <tr data-status="fail" class="check-row">
283
+ <td class="text-danger" style="white-space: nowrap;">
284
+ <i class="bi bi-x-circle-fill"></i> <span style="vertical-align: middle;">Fail</span>
285
+ </td>
286
+ <td>Repository existence check</td>
287
+ <td>The repository at https://${repoInfo.host}.com/${repoInfo.account}/${repoInfo.repo} does not exist or is not accessible. Please verify the repository information in specs.json.</td>
288
+ </tr>
289
+ </tbody>
290
+ </table>
291
+ </div>
292
+ </div>
293
+ `;
294
+ }
295
+
296
+ checkResults.forEach(section => {
297
+ resultsHtml += `
298
+ <div class="card mb-4 results-card" data-section="${section.title.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}">
299
+ <div class="card-header">
300
+ <h5>${section.title}</h5>
301
+ </div>
302
+ <div class="card-body">
303
+ <table class="table table-striped">
304
+ <thead>
305
+ <tr>
306
+ <th>Status</th>
307
+ <th>Check</th>
308
+ <th>Details</th>
309
+ </tr>
310
+ </thead>
311
+ <tbody>
312
+ `;
313
+
314
+ section.results.forEach(result => {
315
+ let statusClass, statusIcon, statusText;
316
+
317
+ if (result.status === 'warning') {
318
+ // Warning status
319
+ statusClass = 'text-warning';
320
+ statusIcon = '<i class="bi bi-exclamation-triangle-fill"></i>';
321
+ statusText = 'Warning';
322
+ } else if (result.success === 'partial') {
323
+ // Partial success (warning) status
324
+ statusClass = 'text-warning';
325
+ statusIcon = '<i class="bi bi-exclamation-triangle-fill"></i>';
326
+ statusText = 'Warning';
327
+ } else if (result.success) {
328
+ // Pass status
329
+ statusClass = 'text-success';
330
+ statusIcon = '<i class="bi bi-check-circle-fill"></i>';
331
+ statusText = 'Pass';
332
+ } else {
333
+ // Fail status
334
+ statusClass = 'text-danger';
335
+ statusIcon = '<i class="bi bi-x-circle-fill"></i>';
336
+ statusText = 'Fail';
337
+ }
338
+
339
+ // Add data-status attribute to identify rows by status and reorder columns to put status first
340
+ resultsHtml += `
341
+ <tr data-status="${statusText.toLowerCase()}" class="check-row">
342
+ <td class="${statusClass}" style="white-space: nowrap;">
343
+ ${statusIcon} <span style="vertical-align: middle;">${statusText}</span>
344
+ </td>
345
+ <td>${result.name}</td>
346
+ <td>${result.details || ''}</td>
347
+ </tr>
348
+ `;
349
+ });
350
+
351
+ resultsHtml += `
352
+ </tbody>
353
+ </table>
354
+ </div>
355
+ </div>
356
+ `;
357
+ });
358
+
359
+ return `
360
+ <!DOCTYPE html>
361
+ <html lang="en">
362
+ <head>
363
+ <meta charset="UTF-8">
364
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
365
+ <title>Spec-Up-T Health Check Report</title>
366
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
367
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
368
+ <style>
369
+ body {
370
+ padding-top: 2rem;
371
+ padding-bottom: 2rem;
372
+ }
373
+ .report-header {
374
+ margin-bottom: 2rem;
375
+ }
376
+ .timestamp {
377
+ color: #6c757d;
378
+ }
379
+ .filter-toggle {
380
+ margin-bottom: 1rem;
381
+ }
382
+ .hidden-item {
383
+ display: none;
384
+ }
385
+ </style>
386
+ </head>
387
+ <body>
388
+ <div class="container">
389
+ <div class="report-header">
390
+ <h1>Spec-Up-T Health Check Report</h1>
391
+ <p class="timestamp">Generated: ${timestamp}</p>
392
+ <p class="repo-info">
393
+ Repository: <a href="https://${repoInfo.host}.com/${repoInfo.account}/${repoInfo.repo}" target="_blank">https://${repoInfo.host}.com/${repoInfo.account}/${repoInfo.repo}</a><br>
394
+ Username: ${repoInfo.account}<br>
395
+ Repo name: ${repoInfo.repo}
396
+ </p>
397
+ </div>
398
+
399
+ <div class="filter-toggle form-check form-switch">
400
+ <input class="form-check-input" type="checkbox" id="togglePassingChecks" checked>
401
+ <label class="form-check-label" for="togglePassingChecks">Show / hide passing checks</label>
402
+ </div>
403
+
404
+ <div class="results">
405
+ ${resultsHtml}
406
+ </div>
407
+ </div>
408
+
409
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
410
+ <script>
411
+ // Toggle function for passing checks
412
+ document.getElementById('togglePassingChecks').addEventListener('change', function() {
413
+ const showPassing = this.checked;
414
+ const passingRows = document.querySelectorAll('tr[data-status="pass"]');
415
+
416
+ // Hide/show passing rows
417
+ passingRows.forEach(row => {
418
+ if (showPassing) {
419
+ row.classList.remove('hidden-item');
420
+ } else {
421
+ row.classList.add('hidden-item');
422
+ }
423
+ });
424
+
425
+ // Check each results card to see if it should be hidden
426
+ document.querySelectorAll('.results-card').forEach(card => {
427
+ const visibleRows = card.querySelectorAll('tr.check-row:not(.hidden-item)');
428
+
429
+ if (visibleRows.length === 0) {
430
+ // If no visible rows, hide the entire card
431
+ card.classList.add('hidden-item');
432
+ } else {
433
+ // Otherwise show it
434
+ card.classList.remove('hidden-item');
435
+ }
436
+ });
437
+ });
438
+ </script>
439
+ </body>
440
+ </html>
441
+ `;
442
+ }
443
+
444
+ // Run the health check
445
+ runHealthCheck();
@@ -11,6 +11,7 @@ const configScriptsKeys = {
11
11
  "menu": "bash ./node_modules/spec-up-t/src/install-from-boilerplate/menu.sh",
12
12
  "addremovexrefsource": "node --no-warnings -e \"require('spec-up-t/src/add-remove-xref-source.js')\"",
13
13
  "configure": "node --no-warnings -e \"require('spec-up-t/src/configure.js')\"",
14
+ "healthCheck": "node --no-warnings -e \"require('spec-up-t/src/health-check.js')\"",
14
15
  "custom-update": "npm update && node -e \"require('spec-up-t/src/install-from-boilerplate/custom-update.js')\""
15
16
  };
16
17
 
@@ -28,6 +29,7 @@ const configOverwriteScriptsKeys = {
28
29
  "menu": true,
29
30
  "addremovexrefsource": true,
30
31
  "configure": true,
32
+ "healthCheck": true,
31
33
  "custom-update": true
32
34
  };
33
35
 
@@ -10,11 +10,12 @@ function handle_choice() {
10
10
  "Collect external references (no cache, slower)" "collect_external_references_no_cache"
11
11
  "Add, remove or view xref source" "do_add_remove_xref_source"
12
12
  "Configure" "do_configure"
13
+ "Run health check" "do_health_check"
13
14
  "Open documentation website" "do_help"
14
15
  "Freeze specification" "do_freeze"
15
16
  )
16
17
 
17
- if [[ "$choice" =~ ^[0-8]$ ]]; then
18
+ if [[ "$choice" =~ ^[0-9]$ ]]; then
18
19
  local index=$((choice * 2))
19
20
  echo -e "\n\n ************************************"
20
21
  echo " ${options[index]}"
@@ -50,8 +51,9 @@ function display_intro() {
50
51
  [4] Collect external references (no cache, slower)
51
52
  [5] Add, remove or view xref source
52
53
  [6] Configure
53
- [7] Open documentation website
54
- [8] Freeze specification
54
+ [7] Run health check
55
+ [8] Open documentation website
56
+ [9] Freeze specification
55
57
  [Q] Quit
56
58
 
57
59
  An xref is a reference to another repository.
@@ -76,6 +78,7 @@ function collect_external_references_cache() { clear; npm run collectExternalRef
76
78
  function collect_external_references_no_cache() { clear; npm run collectExternalReferencesNoCache; }
77
79
  function do_add_remove_xref_source() { clear; npm run addremovexrefsource; }
78
80
  function do_configure() { clear; npm run configure; }
81
+ function do_health_check() { clear; npm run healthCheck; }
79
82
  function do_freeze() { clear; npm run freeze; }
80
83
 
81
84
  function do_help() {
@@ -94,6 +94,111 @@ module.exports = function (md, templates = {}) {
94
94
  if (targetIndex !== -1 && idx > targetIndex && !classAdded) {
95
95
  tokens[idx].attrPush(['class', 'terms-and-definitions-list']);
96
96
  classAdded = true;
97
+
98
+ /* Sort terms and definitions alphabetically
99
+ Sort dt/dd pairs case-insensitively based on dt content
100
+
101
+ 1: Token-based Markdown Processing: Spec-Up-T uses a token-based approach to parse and render Markdown. When Markdown is processed, it's converted into a series of tokens that represent different elements (like dt_open, dt_content, dt_close, dd_open, dd_content, dd_close). We're not dealing with simple strings but with structured tokens.
102
+
103
+ 2: Preserving Relationships: When sorting terms, we need to ensure that each definition term (<dt>) stays connected to its corresponding definition description (<dd>). It's not as simple as sorting an array of strings - we're sorting complex structures.
104
+
105
+ 3: Implementation Details: The implementation includes:
106
+
107
+ - Finding the terminology section in the document
108
+ - Collecting term starts, ends, and their contents
109
+ - Creating a sorted index based on case-insensitive comparisons
110
+ - Rebuilding the token array in the correct order
111
+ - Ensuring all relationships between terms and definitions are preserved
112
+ - Handling special cases and edge conditions
113
+
114
+ The complexity is unavoidable because:
115
+
116
+ - We're working with the markdown-it rendering pipeline, not just manipulating DOM
117
+ - The terms and definitions exist as tokens before they become HTML
118
+ - We need to preserve all the token relationships while reordering
119
+ - We're intercepting the rendering process to modify the token structure
120
+
121
+ If we were just sorting DOM elements after the page rendered, it would be simpler. But by doing the sorting during the Markdown processing, we ensure the HTML output is correct from the beginning, which is more efficient and leads to better performance.
122
+ */
123
+ let dtStartIndices = [];
124
+ let dtEndIndices = [];
125
+ let dtContents = [];
126
+
127
+ // First pass: collect all dt blocks and their contents
128
+ for (let i = idx + 1; i < tokens.length; i++) {
129
+ if (tokens[i].type === 'dl_close') {
130
+ break;
131
+ }
132
+ if (tokens[i].type === 'dt_open') {
133
+ const startIdx = i;
134
+ let content = '';
135
+
136
+ // Find the end of this dt block and capture its content
137
+ for (let j = i + 1; j < tokens.length; j++) {
138
+ if (tokens[j].type === 'dt_close') {
139
+ dtStartIndices.push(startIdx);
140
+ dtEndIndices.push(j);
141
+ dtContents.push(content.toLowerCase()); // Store lowercase for case-insensitive sorting
142
+ break;
143
+ }
144
+ // Collect the content inside the dt (including spans with term IDs)
145
+ if (tokens[j].content) {
146
+ content += tokens[j].content;
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ // Create indices sorted by case-insensitive term content
153
+ const sortedIndices = dtContents.map((_, idx) => idx)
154
+ .sort((a, b) => dtContents[a].localeCompare(dtContents[b]));
155
+
156
+ // Reorder the tokens based on the sorted indices
157
+ if (sortedIndices.length > 0) {
158
+ // Create a new array of tokens
159
+ const newTokens = tokens.slice(0, idx + 1); // Include dl_open
160
+
161
+ // For each dt/dd pair in sorted order
162
+ for (let i = 0; i < sortedIndices.length; i++) {
163
+ const originalIndex = sortedIndices[i];
164
+ const dtStart = dtStartIndices[originalIndex];
165
+ const dtEnd = dtEndIndices[originalIndex];
166
+
167
+ // Add dt tokens
168
+ for (let j = dtStart; j <= dtEnd; j++) {
169
+ newTokens.push(tokens[j]);
170
+ }
171
+
172
+ // Find and add dd tokens
173
+ let ddFound = false;
174
+ for (let j = dtEnd + 1; j < tokens.length; j++) {
175
+ if (tokens[j].type === 'dt_open' || tokens[j].type === 'dl_close') {
176
+ break;
177
+ }
178
+ if (tokens[j].type === 'dd_open') {
179
+ ddFound = true;
180
+ }
181
+ if (ddFound) {
182
+ newTokens.push(tokens[j]);
183
+ if (tokens[j].type === 'dd_close') {
184
+ break;
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ // Add the closing dl token
191
+ for (let i = idx + 1; i < tokens.length; i++) {
192
+ if (tokens[i].type === 'dl_close') {
193
+ newTokens.push(tokens[i]);
194
+ break;
195
+ }
196
+ }
197
+
198
+ // Replace the old tokens with the new sorted ones
199
+ tokens.splice(idx, newTokens.length, ...newTokens);
200
+ }
201
+ // END Sort terms and definitions alphabetically
97
202
  }
98
203
 
99
204
  let lastDdIndex = -1;