promptgraph-mcp 2.4.3 → 2.4.5
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/bundle-counts.js +4 -1
- package/github-import.js +35 -3
- package/marketplace.js +72 -15
- package/package.json +1 -1
- package/validate-repo-action.js +139 -0
package/bundle-counts.js
CHANGED
|
@@ -16,8 +16,11 @@ const SKIP_ROOT_DIRS = new Set(['.github', 'docs', 'doc', 'assets', 'images', 'i
|
|
|
16
16
|
const SKIP_NAMES = /^(readme|changelog|license|contributing|security|authors|credits|install|installation|usage|promotion|faq|glossary|index|overview|summary|roadmap|todo|notes|template|example|sample|demo|guide|tutorial|walkthrough|architecture|design|spec|requirements|privacy|terms|disclaimer|notice|copying|warranty|funding)/i;
|
|
17
17
|
|
|
18
18
|
function httpsGet(url) {
|
|
19
|
+
const token = process.env.GITHUB_TOKEN;
|
|
20
|
+
const headers = { 'User-Agent': 'promptgraph-mcp' };
|
|
21
|
+
if (token && url.startsWith('https://api.github.com/')) headers['Authorization'] = `Bearer ${token}`;
|
|
19
22
|
return new Promise((res, rej) => {
|
|
20
|
-
const req = https.get(url, { headers
|
|
23
|
+
const req = https.get(url, { headers }, r => {
|
|
21
24
|
if (r.statusCode === 403 || r.statusCode === 429) return rej(new Error(`Rate limited`));
|
|
22
25
|
if (r.statusCode !== 200) { r.resume(); return rej(new Error(`HTTP ${r.statusCode}`)); }
|
|
23
26
|
let d = ''; r.setEncoding('utf8'); r.on('data', c => d += c); r.on('end', () => res(d));
|
package/github-import.js
CHANGED
|
@@ -12,8 +12,11 @@ const SKILL_DIRS = ['skills', 'commands', 'prompts', 'agents', 'skills-store', '
|
|
|
12
12
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
13
13
|
|
|
14
14
|
function httpsGet(url) {
|
|
15
|
+
const token = process.env.GITHUB_TOKEN;
|
|
16
|
+
const headers = { 'User-Agent': 'promptgraph-mcp' };
|
|
17
|
+
if (token && url.startsWith('https://api.github.com/')) headers['Authorization'] = `Bearer ${token}`;
|
|
15
18
|
return new Promise((res, rej) => {
|
|
16
|
-
const req = https.get(url, { headers
|
|
19
|
+
const req = https.get(url, { headers }, r => {
|
|
17
20
|
if (r.statusCode >= 300 && r.statusCode < 400 && r.headers.location)
|
|
18
21
|
return httpsGet(r.headers.location).then(res, rej);
|
|
19
22
|
if (r.statusCode !== 200) { r.resume(); return rej(new Error(`HTTP ${r.statusCode}`)); }
|
|
@@ -192,6 +195,9 @@ function cleanupRepoRoot(repoRoot) {
|
|
|
192
195
|
if (SKIP_DIRS_LOCAL.has(entry.name.toLowerCase())) {
|
|
193
196
|
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
194
197
|
removed++;
|
|
198
|
+
} else {
|
|
199
|
+
// Recurse into subdirectory to remove doc files
|
|
200
|
+
removed += cleanupRepoDir(fullPath, SKIP_RE);
|
|
195
201
|
}
|
|
196
202
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
197
203
|
const base = entry.name.replace(/\.md$/i, '').toLowerCase();
|
|
@@ -200,13 +206,36 @@ function cleanupRepoRoot(repoRoot) {
|
|
|
200
206
|
removed++;
|
|
201
207
|
}
|
|
202
208
|
} else if (entry.isFile() && !entry.name.endsWith('.md')) {
|
|
203
|
-
// Remove non-md files (gitignore, LICENSE, Makefile, etc.) — keep only .md
|
|
204
209
|
if (entry.name !== '.gitignore') {
|
|
205
210
|
try { fs.unlinkSync(fullPath); removed++; } catch {}
|
|
206
211
|
}
|
|
207
212
|
}
|
|
208
213
|
}
|
|
209
|
-
if (removed > 0) console.log(`Cleaned up ${removed} non-skill files/dirs
|
|
214
|
+
if (removed > 0) console.log(`Cleaned up ${removed} non-skill files/dirs`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Recursively remove doc .md files from subdirectories
|
|
218
|
+
function cleanupRepoDir(dirPath, SKIP_RE) {
|
|
219
|
+
let removed = 0;
|
|
220
|
+
let entries;
|
|
221
|
+
try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); } catch { return 0; }
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
224
|
+
if (entry.isDirectory()) {
|
|
225
|
+
removed += cleanupRepoDir(fullPath, SKIP_RE);
|
|
226
|
+
// Remove empty dirs after cleanup
|
|
227
|
+
try { if (fs.readdirSync(fullPath).length === 0) fs.rmdirSync(fullPath); } catch {}
|
|
228
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
229
|
+
const base = entry.name.replace(/\.md$/i, '').toLowerCase();
|
|
230
|
+
if (SKIP_RE.test(base)) {
|
|
231
|
+
fs.unlinkSync(fullPath);
|
|
232
|
+
removed++;
|
|
233
|
+
}
|
|
234
|
+
} else if (entry.isFile() && !entry.name.endsWith('.md')) {
|
|
235
|
+
try { fs.unlinkSync(fullPath); removed++; } catch {}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return removed;
|
|
210
239
|
}
|
|
211
240
|
|
|
212
241
|
// Update sparse repo — fetch + reset
|
|
@@ -321,6 +350,9 @@ export async function importFromGitHub(repoUrl) {
|
|
|
321
350
|
if (!cloneOk) throw new Error(`Update failed for ${repoName}`);
|
|
322
351
|
}
|
|
323
352
|
|
|
353
|
+
// Remove doc files anywhere in the cloned tree
|
|
354
|
+
cleanupRepoRoot(dest);
|
|
355
|
+
|
|
324
356
|
const { dir: skillsDir, label } = detectSkillsDirLocal(dest);
|
|
325
357
|
const mdFiles = globSync(`${skillsDir}/**/*.md`);
|
|
326
358
|
|
package/marketplace.js
CHANGED
|
@@ -11,6 +11,7 @@ import { loadConfig, saveConfig, PROMPTGRAPH_DIR, SKILLS_STORE_DIR } from './con
|
|
|
11
11
|
import { importFromGitHub, validateRepoSkills } from './github-import.js';
|
|
12
12
|
|
|
13
13
|
const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
|
|
14
|
+
const SKILL_COUNT_CACHE = path.join(PROMPTGRAPH_DIR, 'skill-counts.json');
|
|
14
15
|
const SKILLS_DIR = path.join(SKILLS_STORE_DIR, 'marketplace');
|
|
15
16
|
|
|
16
17
|
// Atomically write content to dest via tmp — cleans up on failure
|
|
@@ -81,6 +82,29 @@ async function rawFetch(url) {
|
|
|
81
82
|
|
|
82
83
|
// Disk cache for the registry (network to GitHub raw can be slow on some networks).
|
|
83
84
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
85
|
+
const SKILL_COUNT_TTL = 24 * 60 * 60 * 1000; // 24 hours for skill counts
|
|
86
|
+
|
|
87
|
+
// Return GitHub API auth headers if GITHUB_TOKEN is set
|
|
88
|
+
function githubAuthHeaders() {
|
|
89
|
+
const token = process.env.GITHUB_TOKEN;
|
|
90
|
+
if (!token) return { 'User-Agent': 'promptgraph-mcp' };
|
|
91
|
+
return { 'User-Agent': 'promptgraph-mcp', 'Authorization': `Bearer ${token}` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Read skill count cache, returns {} on miss
|
|
95
|
+
function readSkillCountCache() {
|
|
96
|
+
try {
|
|
97
|
+
if (fs.existsSync(SKILL_COUNT_CACHE)) return JSON.parse(fs.readFileSync(SKILL_COUNT_CACHE, 'utf8'));
|
|
98
|
+
} catch {}
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function writeSkillCountCache(data) {
|
|
103
|
+
try {
|
|
104
|
+
fs.mkdirSync(path.dirname(SKILL_COUNT_CACHE), { recursive: true });
|
|
105
|
+
fs.writeFileSync(SKILL_COUNT_CACHE, JSON.stringify(data));
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
84
108
|
|
|
85
109
|
async function fetchText(url) {
|
|
86
110
|
const cacheFile = path.join(PROMPTGRAPH_DIR, 'registry-cache.json');
|
|
@@ -177,14 +201,20 @@ export async function installSkill(query) {
|
|
|
177
201
|
}
|
|
178
202
|
}
|
|
179
203
|
|
|
204
|
+
// ── filter skill files (exclude docs) ─────────────────────────────────────────
|
|
205
|
+
const SKIP_DOCS = /^(readme|license|changelog|contributing|code.?of.?conduct|security|authors|credits|install|faq|index|overview|summary|todo|notes|template|copying|warranty|funding|roadmap)/i;
|
|
206
|
+
function isSkillFile(path) {
|
|
207
|
+
const name = path.split('/').pop().toLowerCase();
|
|
208
|
+
return name.endsWith('.md') && !SKIP_DOCS.test(name.replace(/\.md$/i, ''));
|
|
209
|
+
}
|
|
210
|
+
|
|
180
211
|
async function countRepoSkills(repoUrl) {
|
|
181
212
|
try {
|
|
182
213
|
const apiUrl = `https://api.github.com/repos/${repoUrl}/git/trees/HEAD?recursive=1`;
|
|
183
|
-
const res = await fetch(apiUrl, { headers:
|
|
214
|
+
const res = await fetch(apiUrl, { headers: githubAuthHeaders() });
|
|
184
215
|
if (!res.ok) return null;
|
|
185
216
|
const data = await res.json();
|
|
186
|
-
|
|
187
|
-
return (data.tree || []).filter(f => f.type === 'blob' && exts.some(e => f.path.toLowerCase().endsWith(e))).length;
|
|
217
|
+
return (data.tree || []).filter(f => f.type === 'blob' && isSkillFile(f.path)).length;
|
|
188
218
|
} catch { return null; }
|
|
189
219
|
}
|
|
190
220
|
|
|
@@ -193,13 +223,33 @@ export async function browseBundles(topK = 20) {
|
|
|
193
223
|
const text = await fetchText(REGISTRY_URL);
|
|
194
224
|
const registry = JSON.parse(text);
|
|
195
225
|
const bundles = registry.bundles || [];
|
|
226
|
+
const cache = readSkillCountCache();
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
let changed = false;
|
|
229
|
+
|
|
196
230
|
await Promise.all(bundles.map(async b => {
|
|
197
|
-
if (b.repo_url
|
|
231
|
+
if (!b.repo_url) return;
|
|
232
|
+
const cached = cache[b.repo_url];
|
|
233
|
+
// Use cached count if fresh, else fetch from API
|
|
234
|
+
if (cached && (now - cached.ts) < SKILL_COUNT_TTL) {
|
|
235
|
+
b.skillCount = cached.count;
|
|
236
|
+
} else {
|
|
198
237
|
const count = await countRepoSkills(b.repo_url);
|
|
199
|
-
if (count !== null)
|
|
238
|
+
if (count !== null) {
|
|
239
|
+
b.skillCount = count;
|
|
240
|
+
cache[b.repo_url] = { count, ts: now };
|
|
241
|
+
changed = true;
|
|
242
|
+
} else {
|
|
243
|
+
// API failed — use stale cache if exists, else keep registry value as fallback
|
|
244
|
+
b.skillCount = cached?.count ?? b.skillCount ?? 0;
|
|
245
|
+
}
|
|
200
246
|
}
|
|
201
247
|
}));
|
|
248
|
+
|
|
249
|
+
if (changed) writeSkillCountCache(cache);
|
|
250
|
+
|
|
202
251
|
return bundles
|
|
252
|
+
.filter(b => !b.repo_url || b.skillCount > 0)
|
|
203
253
|
.map(b => ({ ...b, code: b.code || codeFor(b.id) }))
|
|
204
254
|
.sort((a, b) => (b.stars || 0) - (a.stars || 0))
|
|
205
255
|
.slice(0, topK);
|
|
@@ -317,16 +367,17 @@ export async function publishBundle(bundleDef) {
|
|
|
317
367
|
return { error: 'Bundle validation failed', issues: validation.errors, warnings: validation.warnings };
|
|
318
368
|
}
|
|
319
369
|
|
|
320
|
-
//
|
|
370
|
+
// Best-effort repo validation (client-side). GitHub Actions is the source of truth.
|
|
371
|
+
let repoWarnings = [];
|
|
321
372
|
if (def.repo_url) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
};
|
|
373
|
+
try {
|
|
374
|
+
const ownerRepo = def.repo_url.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '');
|
|
375
|
+
const repoValidation = await validateRepoSkills(ownerRepo);
|
|
376
|
+
if (!repoValidation.ok) {
|
|
377
|
+
repoWarnings = repoValidation.errors;
|
|
378
|
+
}
|
|
379
|
+
} catch (e) {
|
|
380
|
+
repoWarnings = [`Repo validation warning: ${e.message} (will be checked by CI)`];
|
|
330
381
|
}
|
|
331
382
|
}
|
|
332
383
|
|
|
@@ -340,6 +391,7 @@ export async function publishBundle(bundleDef) {
|
|
|
340
391
|
|
|
341
392
|
if (gh.no_gh) {
|
|
342
393
|
const issueUrl = `${REGISTRY_ISSUES}?title=Bundle%3A+${encodeURIComponent(def.name)}&body=${encodeURIComponent('Bundle definition:\n\n```json\n' + bundleJson + '\n```')}`;
|
|
394
|
+
const actionNote = def.repo_url ? `\n\nNote: Your repo will be validated by CI (GitHub Actions) after submission.\nRun locally: node validate-repo-action.js ${def.repo_url}` : '';
|
|
343
395
|
return {
|
|
344
396
|
success: true,
|
|
345
397
|
gh_not_installed: true,
|
|
@@ -347,6 +399,8 @@ export async function publishBundle(bundleDef) {
|
|
|
347
399
|
'1. Install gh CLI: https://cli.github.com',
|
|
348
400
|
` OR open this pre-filled issue directly: ${issueUrl}`,
|
|
349
401
|
'2. Paste the bundle JSON shown below into the issue body',
|
|
402
|
+
...(repoWarnings.length ? ['', '⚠ Repo warnings (CI will re-check):', ...repoWarnings.map(w => ' - ' + w)] : []),
|
|
403
|
+
...(def.repo_url ? ['', 'Your repo will be validated by CI when submitted.'] : []),
|
|
350
404
|
].join('\n'),
|
|
351
405
|
bundle_json: bundleJson,
|
|
352
406
|
submit_url: issueUrl,
|
|
@@ -354,7 +408,10 @@ export async function publishBundle(bundleDef) {
|
|
|
354
408
|
}
|
|
355
409
|
if (!gh.ok) return { error: gh.error };
|
|
356
410
|
const issueUrl = `${REGISTRY_ISSUES}?title=Bundle%3A+${encodeURIComponent(def.name)}&body=Gist%3A+${encodeURIComponent(gh.url)}`;
|
|
357
|
-
|
|
411
|
+
const msg = def.repo_url
|
|
412
|
+
? `Bundle published! Submit: ${issueUrl}\n\nRepo will be validated by CI. Run: node validate-repo-action.js ${def.repo_url}`
|
|
413
|
+
: `Bundle published! Submit: ${issueUrl}`;
|
|
414
|
+
return { success: true, gist_url: gh.url, submit_url: issueUrl, message: msg };
|
|
358
415
|
}
|
|
359
416
|
|
|
360
417
|
export function getTopRated(topK = 10) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
import { validateSkill } from './validator.js';
|
|
5
|
+
|
|
6
|
+
// ── doc filename filter — same as cleanupRepoRoot in github-import.js ──────────
|
|
7
|
+
const SKIP_RE = /^(readme|changelog|license|contributing|code\.?of\.?conduct|security|authors|credits|install|installation|usage|promotion|faq|glossary|index|overview|summary|roadmap|todo|notes|template|example|sample|demo|guide|tutorial|walkthrough|architecture|design|spec|requirements|privacy|terms|disclaimer|notice|copying|warranty|funding|changelog)/i;
|
|
8
|
+
|
|
9
|
+
const SKIP_DIRS_LOCAL = new Set([
|
|
10
|
+
'.github', 'docs', 'doc', 'documentation', 'assets', 'images', 'img',
|
|
11
|
+
'screenshots', 'media', 'static', 'scripts', 'ci_scripts',
|
|
12
|
+
'node_modules', 'vendor', 'dist', 'build', 'tests', 'test',
|
|
13
|
+
'examples', 'example', 'fixtures',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function isDocFile(name) {
|
|
17
|
+
const base = path.basename(name, '.md').toLowerCase();
|
|
18
|
+
return SKIP_RE.test(base);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isSkipDir(name) {
|
|
22
|
+
return SKIP_DIRS_LOCAL.has(name.toLowerCase());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── find .md files in subdirectories only (never root) ────────────────────────
|
|
26
|
+
function findSubdirMdFiles(repoRoot) {
|
|
27
|
+
const files = [];
|
|
28
|
+
const entries = fs.readdirSync(repoRoot, { withFileTypes: true });
|
|
29
|
+
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (entry.name === '.git') continue;
|
|
32
|
+
const fullPath = path.join(repoRoot, entry.name);
|
|
33
|
+
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
if (isSkipDir(entry.name)) continue;
|
|
36
|
+
// Walk subdirectory recursively
|
|
37
|
+
walkDir(fullPath, entry.name, files);
|
|
38
|
+
}
|
|
39
|
+
// Root files are SKIPPED — never count them
|
|
40
|
+
}
|
|
41
|
+
return files;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function walkDir(dirPath, relativePrefix, out) {
|
|
45
|
+
let entries;
|
|
46
|
+
try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); } catch { return; }
|
|
47
|
+
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
50
|
+
const relativePath = relativePrefix + '/' + entry.name;
|
|
51
|
+
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
if (isSkipDir(entry.name)) continue;
|
|
54
|
+
walkDir(fullPath, relativePath, out);
|
|
55
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
56
|
+
if (isDocFile(entry.name)) continue;
|
|
57
|
+
out.push({ path: fullPath, relative: relativePath });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── main ──────────────────────────────────────────────────────────────────────
|
|
63
|
+
async function main() {
|
|
64
|
+
const args = process.argv.slice(2);
|
|
65
|
+
const repoArg = args.find(a => a.startsWith('--repo='))?.split('=').slice(1).join('=') || args[0];
|
|
66
|
+
|
|
67
|
+
if (!repoArg) {
|
|
68
|
+
console.error(JSON.stringify({ ok: false, errors: ['Usage: node validate-repo-action.js <owner/repo>'] }));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const repo = repoArg.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '').replace(/\/$/, '');
|
|
73
|
+
const cloneUrl = `https://github.com/${repo}.git`;
|
|
74
|
+
const tmpDir = path.join(process.env.RUNNER_TEMP || process.env.TMPDIR || '/tmp', `validate-${repo.replace('/', '-')}`);
|
|
75
|
+
|
|
76
|
+
// Clean any previous run
|
|
77
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
78
|
+
|
|
79
|
+
// Clone with depth 1 (no API calls, no rate limits)
|
|
80
|
+
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
81
|
+
const clone = spawnSync('git', ['clone', '--depth=1', cloneUrl, tmpDir], {
|
|
82
|
+
stdio: 'pipe', env: gitEnv,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (clone.status !== 0) {
|
|
86
|
+
const msg = (clone.stderr?.toString() || 'unknown error').trim();
|
|
87
|
+
console.error(JSON.stringify({ ok: false, errors: [`Failed to clone ${repo}: ${msg}`] }));
|
|
88
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Find .md files in subdirectories only
|
|
93
|
+
const candidates = findSubdirMdFiles(tmpDir);
|
|
94
|
+
|
|
95
|
+
if (candidates.length === 0) {
|
|
96
|
+
console.error(JSON.stringify({
|
|
97
|
+
ok: false,
|
|
98
|
+
errors: ['No valid .md skill files found in subdirectories'],
|
|
99
|
+
detail: 'Root-level .md files are ignored. Skills must be in a subdirectory (skills/, prompts/, etc.).',
|
|
100
|
+
}));
|
|
101
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Validate each .md file via validateSkill()
|
|
106
|
+
const results = [];
|
|
107
|
+
let totalErrors = 0;
|
|
108
|
+
|
|
109
|
+
for (const file of candidates) {
|
|
110
|
+
const result = validateSkill(file.path);
|
|
111
|
+
results.push({
|
|
112
|
+
file: file.relative,
|
|
113
|
+
ok: result.ok,
|
|
114
|
+
errors: result.errors,
|
|
115
|
+
warnings: result.warnings,
|
|
116
|
+
});
|
|
117
|
+
if (!result.ok) totalErrors++;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Summary
|
|
121
|
+
const summary = {
|
|
122
|
+
ok: totalErrors === 0,
|
|
123
|
+
repo,
|
|
124
|
+
total_md_files: candidates.length,
|
|
125
|
+
passed: candidates.length - totalErrors,
|
|
126
|
+
failed: totalErrors,
|
|
127
|
+
results,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
131
|
+
|
|
132
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
133
|
+
process.exit(totalErrors === 0 ? 0 : 1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main().catch(e => {
|
|
137
|
+
console.error(JSON.stringify({ ok: false, errors: [e.message] }));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|