promptgraph-mcp 2.2.7 → 2.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-import.js +138 -42
- package/index.js +70 -0
- package/package.json +1 -1
package/github-import.js
CHANGED
|
@@ -1,43 +1,121 @@
|
|
|
1
1
|
import { spawnSync } from 'child_process';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
3
|
import fs from 'fs';
|
|
5
4
|
import https from 'https';
|
|
6
5
|
import { globSync } from 'glob';
|
|
7
6
|
import { indexAll, indexSource } from './indexer.js';
|
|
8
7
|
import { loadConfig, saveConfig, SKILLS_STORE_DIR } from './config.js';
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
const SKILL_DIRS = ['skills', 'commands', 'prompts', 'agents', 'skills-store', 'slash-commands', 'custom-commands', 'templates'];
|
|
10
|
+
|
|
11
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function httpsGet(url) {
|
|
14
|
+
return new Promise((res, rej) => {
|
|
15
|
+
const req = https.get(url, { headers: { 'User-Agent': 'promptgraph-mcp' } }, r => {
|
|
16
|
+
if (r.statusCode >= 300 && r.statusCode < 400 && r.headers.location)
|
|
17
|
+
return httpsGet(r.headers.location).then(res, rej);
|
|
18
|
+
if (r.statusCode !== 200) { r.resume(); return rej(new Error(`HTTP ${r.statusCode}`)); }
|
|
19
|
+
let d = ''; r.setEncoding('utf8'); r.on('data', c => d += c); r.on('end', () => res(d));
|
|
20
|
+
});
|
|
21
|
+
req.on('error', rej);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function repoExists(ownerRepo) {
|
|
14
26
|
return new Promise(resolve => {
|
|
15
27
|
const req = https.request(
|
|
16
|
-
{ host: 'github.com', path: `/${
|
|
17
|
-
|
|
28
|
+
{ host: 'github.com', path: `/${ownerRepo}`, method: 'HEAD', headers: { 'User-Agent': 'promptgraph-mcp' } },
|
|
29
|
+
r => resolve(r.statusCode < 400)
|
|
18
30
|
);
|
|
19
31
|
req.on('error', () => resolve(false));
|
|
20
32
|
req.end();
|
|
21
33
|
});
|
|
22
34
|
}
|
|
23
35
|
|
|
24
|
-
//
|
|
25
|
-
|
|
36
|
+
// Ask GitHub API which skill subdir exists (without cloning anything)
|
|
37
|
+
async function detectSkillsDirFromAPI(ownerRepo) {
|
|
38
|
+
try {
|
|
39
|
+
const json = await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents`);
|
|
40
|
+
const entries = JSON.parse(json);
|
|
41
|
+
const dirs = new Set(entries.filter(e => e.type === 'dir').map(e => e.name.toLowerCase()));
|
|
42
|
+
for (const d of SKILL_DIRS) {
|
|
43
|
+
if (dirs.has(d)) return d;
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
return null; // use repo root
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function git(args, cwd, stdio = 'inherit') {
|
|
50
|
+
return spawnSync('git', args, { cwd, stdio });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Clone only the skills subdir via sparse-checkout
|
|
54
|
+
function sparseClone(url, dest, subdir) {
|
|
55
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
56
|
+
|
|
57
|
+
// 1. init + add remote
|
|
58
|
+
if (git(['init'], dest, 'pipe').status !== 0) return false;
|
|
59
|
+
if (git(['remote', 'add', 'origin', url], dest, 'pipe').status !== 0) return false;
|
|
60
|
+
|
|
61
|
+
// 2. sparse-checkout config
|
|
62
|
+
git(['sparse-checkout', 'init', '--cone'], dest, 'pipe');
|
|
63
|
+
git(['sparse-checkout', 'set', subdir], dest, 'pipe');
|
|
64
|
+
|
|
65
|
+
// 3. fetch + checkout (depth=1)
|
|
66
|
+
const fetch = git(['fetch', '--depth=1', 'origin'], dest);
|
|
67
|
+
if (fetch.status !== 0) return false;
|
|
68
|
+
|
|
69
|
+
// Try HEAD, then main, then master
|
|
70
|
+
for (const branch of ['HEAD', 'main', 'master']) {
|
|
71
|
+
const r = git(['checkout', branch === 'HEAD' ? 'FETCH_HEAD' : branch], dest, 'pipe');
|
|
72
|
+
if (r.status === 0) return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Update sparse repo — fetch + reset
|
|
78
|
+
function sparseUpdate(dest, subdir) {
|
|
79
|
+
const fetch = git(['fetch', '--depth=1', 'origin'], dest);
|
|
80
|
+
if (fetch.status !== 0) return false;
|
|
26
81
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
82
|
+
// Ensure sparse-checkout still set correctly
|
|
83
|
+
git(['sparse-checkout', 'set', subdir], dest, 'pipe');
|
|
84
|
+
|
|
85
|
+
for (const ref of ['origin/HEAD', 'origin/main', 'origin/master']) {
|
|
86
|
+
const r = git(['reset', '--hard', ref], dest, 'pipe');
|
|
87
|
+
if (r.status === 0) return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Fallback: full clone (when no subdir found)
|
|
93
|
+
function fullClone(url, dest) {
|
|
94
|
+
if (fs.existsSync(dest)) {
|
|
95
|
+
const fetch = git(['fetch', '--depth=1', 'origin'], dest);
|
|
96
|
+
if (fetch.status !== 0) return false;
|
|
97
|
+
for (const ref of ['origin/HEAD', 'origin/main', 'origin/master']) {
|
|
98
|
+
if (git(['reset', '--hard', ref], dest, 'pipe').status === 0) return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return git(['clone', '--depth=1', url, dest]).status === 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// After clone: detect actual skills dir on disk
|
|
106
|
+
function detectSkillsDirLocal(repoRoot) {
|
|
31
107
|
for (const dir of SKILL_DIRS) {
|
|
32
108
|
const candidate = path.join(repoRoot, dir);
|
|
33
109
|
if (fs.existsSync(candidate)) {
|
|
34
110
|
const files = globSync(`${candidate}/**/*.md`);
|
|
35
|
-
if (files.length >= 2) return { dir: candidate,
|
|
111
|
+
if (files.length >= 2) return { dir: candidate, label: dir, sparse: true };
|
|
36
112
|
}
|
|
37
113
|
}
|
|
38
|
-
return { dir: repoRoot,
|
|
114
|
+
return { dir: repoRoot, label: '(root)', sparse: false };
|
|
39
115
|
}
|
|
40
116
|
|
|
117
|
+
// ── main export ───────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
41
119
|
export async function importFromGitHub(repoUrl) {
|
|
42
120
|
if (!repoUrl) {
|
|
43
121
|
console.error('Usage: promptgraph-mcp import <github-url-or-owner/repo>');
|
|
@@ -45,58 +123,76 @@ export async function importFromGitHub(repoUrl) {
|
|
|
45
123
|
}
|
|
46
124
|
|
|
47
125
|
const url = repoUrl.startsWith('http') ? repoUrl : `https://github.com/${repoUrl}`;
|
|
48
|
-
const
|
|
126
|
+
const ownerRepo = url.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '');
|
|
127
|
+
const repoName = ownerRepo.replace('/', '-');
|
|
49
128
|
const dest = path.join(SKILLS_STORE_DIR, 'github', repoName);
|
|
50
129
|
|
|
51
|
-
|
|
52
|
-
|
|
130
|
+
const isNew = !fs.existsSync(dest);
|
|
131
|
+
|
|
132
|
+
if (isNew) {
|
|
133
|
+
const exists = await repoExists(ownerRepo);
|
|
53
134
|
if (!exists) throw new Error(`Repository not found (404): ${url}`);
|
|
54
135
|
}
|
|
55
136
|
|
|
56
137
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
57
138
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
139
|
+
let skillsSubdir = null;
|
|
140
|
+
let cloneOk = false;
|
|
141
|
+
|
|
142
|
+
if (isNew) {
|
|
143
|
+
// Detect skills dir via API before cloning
|
|
144
|
+
console.log(`Detecting skills directory for ${ownerRepo}...`);
|
|
145
|
+
skillsSubdir = await detectSkillsDirFromAPI(ownerRepo);
|
|
146
|
+
|
|
147
|
+
if (skillsSubdir) {
|
|
148
|
+
console.log(`Sparse-cloning ${url} (${skillsSubdir}/ only)...`);
|
|
149
|
+
cloneOk = sparseClone(url, dest, skillsSubdir);
|
|
150
|
+
if (!cloneOk) {
|
|
151
|
+
// Sparse failed — fall back to full clone
|
|
152
|
+
console.log('Sparse-checkout failed, falling back to full clone...');
|
|
153
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
154
|
+
cloneOk = fullClone(url, dest);
|
|
155
|
+
skillsSubdir = null;
|
|
70
156
|
}
|
|
157
|
+
} else {
|
|
158
|
+
console.log(`Cloning ${url}...`);
|
|
159
|
+
cloneOk = fullClone(url, dest);
|
|
71
160
|
}
|
|
161
|
+
|
|
162
|
+
if (!cloneOk) throw new Error(`Clone failed for ${url}`);
|
|
72
163
|
} else {
|
|
73
|
-
console.log(`
|
|
74
|
-
|
|
75
|
-
|
|
164
|
+
console.log(`Updating ${repoName}...`);
|
|
165
|
+
// Detect existing sparse subdir
|
|
166
|
+
const isSparse = git(['sparse-checkout', 'list'], dest, 'pipe').status === 0;
|
|
167
|
+
const sparseList = isSparse
|
|
168
|
+
? spawnSync('git', ['sparse-checkout', 'list'], { cwd: dest, encoding: 'utf8' }).stdout.trim()
|
|
169
|
+
: '';
|
|
170
|
+
skillsSubdir = sparseList.split('\n').find(l => SKILL_DIRS.includes(l.trim())) || null;
|
|
171
|
+
|
|
172
|
+
cloneOk = skillsSubdir
|
|
173
|
+
? sparseUpdate(dest, skillsSubdir)
|
|
174
|
+
: fullClone(url, dest);
|
|
175
|
+
if (!cloneOk) throw new Error(`Update failed for ${repoName}`);
|
|
76
176
|
}
|
|
77
177
|
|
|
78
|
-
const { dir: skillsDir,
|
|
178
|
+
const { dir: skillsDir, label } = detectSkillsDirLocal(dest);
|
|
79
179
|
const mdFiles = globSync(`${skillsDir}/**/*.md`);
|
|
80
180
|
|
|
81
|
-
if (
|
|
82
|
-
console.log(`
|
|
181
|
+
if (skillsSubdir) {
|
|
182
|
+
console.log(`Sparse-checkout: ${label}/ only (${mdFiles.length} .md files, no other repo files)`);
|
|
83
183
|
} else {
|
|
84
|
-
console.log(`
|
|
184
|
+
console.log(`Full clone: scanning ${label} (${mdFiles.length} .md files)`);
|
|
85
185
|
}
|
|
86
186
|
|
|
87
|
-
if (mdFiles.length < 1)
|
|
88
|
-
console.warn('Warning: no .md files found');
|
|
89
|
-
}
|
|
187
|
+
if (mdFiles.length < 1) console.warn('Warning: no .md files found');
|
|
90
188
|
|
|
91
189
|
const config = loadConfig();
|
|
92
190
|
const repoSource = `github:${repoName}`;
|
|
93
191
|
if (!config.sources.find(s => s.dir === skillsDir)) {
|
|
94
|
-
// Remove any old entry pointing at the full repo root if we now have a subdir
|
|
95
192
|
const oldIdx = config.sources.findIndex(s => s.source === repoSource);
|
|
96
193
|
if (oldIdx !== -1) config.sources.splice(oldIdx, 1);
|
|
97
194
|
config.sources.push({ dir: skillsDir, source: repoSource });
|
|
98
195
|
saveConfig(config);
|
|
99
|
-
console.log(`Indexing from: ${skillsDir}`);
|
|
100
196
|
}
|
|
101
197
|
|
|
102
198
|
console.log();
|
package/index.js
CHANGED
|
@@ -34,6 +34,7 @@ function showHelp() {
|
|
|
34
34
|
['import <owner/repo>', 'Import skills from GitHub'],
|
|
35
35
|
['status', 'Show installed skills, repos, and bundles'],
|
|
36
36
|
['marketplace', 'Interactive TUI: browse + search + install skills & bundles'],
|
|
37
|
+
['bundle update [id]', 'Update all (or one) installed GitHub bundles'],
|
|
37
38
|
['validate <file.md>', 'Validate a skill before publishing'],
|
|
38
39
|
['doctor', 'Clean orphaned chunks/edges/ratings'],
|
|
39
40
|
['update', 'Update to the latest version from npm'],
|
|
@@ -329,6 +330,75 @@ if (args[0] === 'search') {
|
|
|
329
330
|
}
|
|
330
331
|
|
|
331
332
|
if (args[0] === 'bundle') {
|
|
333
|
+
if (args[1] === 'update') {
|
|
334
|
+
const { loadConfig: _lcUpd, SKILLS_STORE_DIR: _ssDir } = await import('./config.js');
|
|
335
|
+
const { indexSource } = await import('./indexer.js');
|
|
336
|
+
const cfg = _lcUpd();
|
|
337
|
+
const githubSources = cfg.sources.filter(s => s.source.startsWith('github:'));
|
|
338
|
+
|
|
339
|
+
if (!githubSources.length) { info('No GitHub bundles installed.'); process.exit(0); }
|
|
340
|
+
|
|
341
|
+
const targetId = args[2]; // optional: pg bundle update <id>
|
|
342
|
+
const toUpdate = targetId
|
|
343
|
+
? githubSources.filter(s => s.source.toLowerCase().includes(targetId.toLowerCase()))
|
|
344
|
+
: githubSources;
|
|
345
|
+
|
|
346
|
+
if (!toUpdate.length) { error(`No installed bundle matching "${targetId}"`); process.exit(1); }
|
|
347
|
+
|
|
348
|
+
let updated = 0, unchanged = 0, failed = 0;
|
|
349
|
+
|
|
350
|
+
for (const src of toUpdate) {
|
|
351
|
+
const repoName = src.source.replace('github:', '');
|
|
352
|
+
const dest = src.dir.replace(/[/\\]skills$|[/\\]commands$|[/\\]prompts$/, ''); // get repo root
|
|
353
|
+
const repoRoot = fs.existsSync(path.join(dest, '.git')) ? dest : src.dir;
|
|
354
|
+
|
|
355
|
+
if (!fs.existsSync(path.join(repoRoot, '.git'))) {
|
|
356
|
+
console.log(chalk.gray(` skip ${repoName} (not a git repo)`));
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
process.stdout.write(` Checking ${chalk.white(repoName)}... `);
|
|
361
|
+
|
|
362
|
+
// Get current HEAD hash
|
|
363
|
+
const before = spawnSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).stdout.trim();
|
|
364
|
+
|
|
365
|
+
// Fetch + reset (same as install)
|
|
366
|
+
const fetch = spawnSync('git', ['-C', repoRoot, 'fetch', '--depth=1', 'origin'], { stdio: 'pipe' });
|
|
367
|
+
if (fetch.status !== 0) {
|
|
368
|
+
console.log(chalk.red('fetch failed'));
|
|
369
|
+
failed++;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const reset = spawnSync('git', ['-C', repoRoot, 'reset', '--hard', 'origin/HEAD'], { stdio: 'pipe' });
|
|
373
|
+
if (reset.status !== 0) {
|
|
374
|
+
spawnSync('git', ['-C', repoRoot, 'reset', '--hard', 'origin/main'], { stdio: 'pipe' });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const after = spawnSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).stdout.trim();
|
|
378
|
+
|
|
379
|
+
if (before === after) {
|
|
380
|
+
console.log(chalk.gray('already up to date'));
|
|
381
|
+
unchanged++;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Count changed .md files
|
|
386
|
+
const diff = spawnSync('git', ['-C', repoRoot, 'diff', '--name-only', before, after], { encoding: 'utf8' });
|
|
387
|
+
const changedMd = (diff.stdout || '').split('\n').filter(f => f.endsWith('.md')).length;
|
|
388
|
+
console.log(chalk.green(`${changedMd} files changed`) + chalk.gray(` (${before.slice(0,7)} → ${after.slice(0,7)})`));
|
|
389
|
+
|
|
390
|
+
// Reindex only this source — incremental hash check skips unchanged files
|
|
391
|
+
await indexSource(src.dir, src.source);
|
|
392
|
+
updated++;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.log();
|
|
396
|
+
if (updated) success(`Updated ${updated} bundle(s)`);
|
|
397
|
+
if (unchanged) info(chalk.gray(`${unchanged} already up to date`));
|
|
398
|
+
if (failed) error(`${failed} failed`);
|
|
399
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
400
|
+
}
|
|
401
|
+
|
|
332
402
|
if (args[1] === 'install') {
|
|
333
403
|
const { installBundle } = await import('./marketplace.js');
|
|
334
404
|
const result = await installBundle(args[2]);
|