promptgraph-mcp 2.9.50 → 2.9.52
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/commands/marketplace.js +4 -3
- package/github-import.js +44 -24
- package/package.json +1 -1
- package/tui.js +2 -1
package/commands/marketplace.js
CHANGED
|
@@ -42,6 +42,7 @@ export default async function handler(args, bin) {
|
|
|
42
42
|
const { getCachedCount, setCachedCount, refreshCountsInBackground } = await import('../bundle-counts.js');
|
|
43
43
|
const { SKILLS_STORE_DIR } = await import('../config.js');
|
|
44
44
|
const { globSync } = await import('glob');
|
|
45
|
+
const { SCRIPT_GLOBS } = await import('../github-import.js');
|
|
45
46
|
const githubDir = path.join(SKILLS_STORE_DIR, 'github');
|
|
46
47
|
|
|
47
48
|
// For each bundle: if installed on disk — use real file count + detect scripts; otherwise use cache
|
|
@@ -51,14 +52,14 @@ export default async function handler(args, bin) {
|
|
|
51
52
|
const repo = b.repo_url.split('/')[1];
|
|
52
53
|
const clonedDir = path.join(githubDir, `${owner}-${repo}`);
|
|
53
54
|
if (fs.existsSync(clonedDir) && fs.readdirSync(clonedDir).length > 0) {
|
|
54
|
-
const realCount = globSync(`${clonedDir}/**/*.md
|
|
55
|
-
const hasScripts = globSync(`${clonedDir}
|
|
55
|
+
const realCount = globSync(`${clonedDir}/**/*.md`, { absolute: true }).length;
|
|
56
|
+
const hasScripts = globSync(SCRIPT_GLOBS.map(p => `${clonedDir}/${p}`), { absolute: true }).length > 0;
|
|
56
57
|
setCachedCount(b.repo_url, realCount);
|
|
57
58
|
return { ...b, skillCount: realCount, has_tools: b.has_tools || hasScripts };
|
|
58
59
|
}
|
|
59
60
|
const cached = getCachedCount(b.repo_url);
|
|
60
61
|
const knownCount = cached ?? b.skill_count ?? null;
|
|
61
|
-
return knownCount !== null ? { ...b, skillCount: knownCount } : b;
|
|
62
|
+
return knownCount !== null ? { ...b, skillCount: knownCount, has_tools: b.has_tools } : b;
|
|
62
63
|
});
|
|
63
64
|
refreshCountsInBackground(bundlesWithCounts);
|
|
64
65
|
|
package/github-import.js
CHANGED
|
@@ -14,6 +14,10 @@ const downloadRateLimiter = new RateLimiter({ maxRequests: RATE_LIMIT_REQUESTS *
|
|
|
14
14
|
|
|
15
15
|
const SKILL_DIRS = ['skills', 'commands', 'prompts', 'agents', 'skills-store', 'slash-commands', 'custom-commands', 'templates'];
|
|
16
16
|
|
|
17
|
+
// glob v13 brace-expansion ({py,sh,...}) returns nothing on Windows — use an
|
|
18
|
+
// array of explicit patterns instead so script detection works cross-platform.
|
|
19
|
+
export const SCRIPT_GLOBS = ['**/*.py', '**/*.sh', '**/*.bash', '**/*.js', '**/*.ts', '**/*.rb'];
|
|
20
|
+
|
|
17
21
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
18
22
|
|
|
19
23
|
const repoStats = new Map()
|
|
@@ -768,26 +772,25 @@ function detectSubdirFromTree(repoRoot) {
|
|
|
768
772
|
|
|
769
773
|
// ── light version (git sparse-checkout, no API rate limits) ───────────────────
|
|
770
774
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const destBase = path.join(getSkillsStoreDir(), 'github', repoName);
|
|
775
|
+
// Core clone + filter pipeline. Single source of truth for both real installs
|
|
776
|
+
// and offline validation. Materializes the skills subdir into destBase, applies
|
|
777
|
+
// isSkillFile + validateSkill filters, and returns the ground-truth result.
|
|
778
|
+
// NO side effects (no config write, no indexing) — caller decides what to do.
|
|
779
|
+
// Throws 'No valid skills...' if nothing survives filtering.
|
|
780
|
+
export function cloneAndFilterRepo(ownerRepo, destBase, prog = () => {}) {
|
|
778
781
|
const cloneUrl = `https://github.com/${ownerRepo}.git`;
|
|
779
|
-
|
|
780
782
|
if (fs.existsSync(destBase)) fs.rmSync(destBase, { recursive: true, force: true });
|
|
781
783
|
fs.mkdirSync(destBase, { recursive: true });
|
|
782
784
|
|
|
783
785
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
784
|
-
|
|
786
|
+
// core.longpaths=true — required on Windows for repos with deep nested paths
|
|
787
|
+
// (>260 chars) that otherwise fail checkout with "UNKNOWN: open" / "Checkout failed".
|
|
788
|
+
const LP = ['-c', 'core.longpaths=true'];
|
|
785
789
|
|
|
786
790
|
// Step 1: treeless clone — gets file tree instantly, no blob download, no API
|
|
787
791
|
prog(`Cloning ${ownerRepo}...`);
|
|
788
|
-
const init = spawnSync('git', ['clone', '--depth=1', '--filter=blob:none', '--no-checkout', '--progress', cloneUrl, destBase], { stdio: 'pipe', env: gitEnv, timeout: 120000 });
|
|
792
|
+
const init = spawnSync('git', [...LP, 'clone', '--depth=1', '--filter=blob:none', '--no-checkout', '--progress', cloneUrl, destBase], { stdio: 'pipe', env: gitEnv, timeout: 120000 });
|
|
789
793
|
if (init.status !== 0) {
|
|
790
|
-
process.stderr.write('\n');
|
|
791
794
|
fs.rmSync(destBase, { recursive: true, force: true });
|
|
792
795
|
throw new Error(`Failed to clone ${ownerRepo}: ${(init.stderr?.toString() || '').trim().slice(0, 120)}`);
|
|
793
796
|
}
|
|
@@ -798,15 +801,15 @@ export async function importFromGitHubLight(repoUrl) {
|
|
|
798
801
|
|
|
799
802
|
// Step 3: sparse-checkout skills subdir — .md files + scripts (.py/.sh/.js/.ts/.rb/.bash)
|
|
800
803
|
prog(`Setting up sparse checkout${subdir ? ` (${subdir}/)` : ''}...`);
|
|
801
|
-
spawnSync('git', ['-C', destBase, 'sparse-checkout', 'init'], { stdio: 'pipe', env: gitEnv });
|
|
804
|
+
spawnSync('git', [...LP, '-C', destBase, 'sparse-checkout', 'init'], { stdio: 'pipe', env: gitEnv });
|
|
802
805
|
if (subdir) {
|
|
803
|
-
spawnSync('git', ['-C', destBase, 'sparse-checkout', 'set', '--no-cone',
|
|
806
|
+
spawnSync('git', [...LP, '-C', destBase, 'sparse-checkout', 'set', '--no-cone',
|
|
804
807
|
`${subdir}/*.md`, `${subdir}/**/*.md`,
|
|
805
808
|
`${subdir}/**/*.py`, `${subdir}/**/*.sh`, `${subdir}/**/*.js`,
|
|
806
809
|
`${subdir}/**/*.ts`, `${subdir}/**/*.rb`, `${subdir}/**/*.bash`,
|
|
807
810
|
], { stdio: 'pipe', env: gitEnv });
|
|
808
811
|
} else {
|
|
809
|
-
spawnSync('git', ['-C', destBase, 'sparse-checkout', 'set', '--no-cone',
|
|
812
|
+
spawnSync('git', [...LP, '-C', destBase, 'sparse-checkout', 'set', '--no-cone',
|
|
810
813
|
'*.md', '**/*.md',
|
|
811
814
|
'**/*.py', '**/*.sh', '**/*.js', '**/*.ts', '**/*.rb', '**/*.bash',
|
|
812
815
|
], { stdio: 'pipe', env: gitEnv });
|
|
@@ -814,26 +817,29 @@ export async function importFromGitHubLight(repoUrl) {
|
|
|
814
817
|
|
|
815
818
|
// Step 4: checkout to materialize only the selected files
|
|
816
819
|
prog(`Downloading .md files...`);
|
|
817
|
-
const co = spawnSync('git', ['-C', destBase, 'checkout'], { stdio: 'pipe', env: gitEnv, timeout: 120000 });
|
|
820
|
+
const co = spawnSync('git', [...LP, '-C', destBase, 'checkout'], { stdio: 'pipe', env: gitEnv, timeout: 120000 });
|
|
818
821
|
if (co.status !== 0) {
|
|
819
|
-
process.stderr.write('\n');
|
|
820
822
|
fs.rmSync(destBase, { recursive: true, force: true });
|
|
821
823
|
throw new Error(`Checkout failed for ${ownerRepo}`);
|
|
822
824
|
}
|
|
823
825
|
// Force blob materialization (needed for partial clones on Windows)
|
|
824
|
-
spawnSync('git', ['-C', destBase, 'checkout', 'HEAD', '--', '.'], { stdio: 'pipe', env: gitEnv, timeout: 120000 });
|
|
825
|
-
process.stderr.write('\n');
|
|
826
|
+
spawnSync('git', [...LP, '-C', destBase, 'checkout', 'HEAD', '--', '.'], { stdio: 'pipe', env: gitEnv, timeout: 120000 });
|
|
826
827
|
|
|
827
828
|
// Make scripts executable on unix
|
|
828
829
|
if (process.platform !== 'win32') {
|
|
829
830
|
try {
|
|
830
|
-
const scripts = globSync(`${destBase}
|
|
831
|
+
const scripts = globSync(SCRIPT_GLOBS.map(p => `${destBase}/${p}`), { absolute: true });
|
|
831
832
|
for (const s of scripts) { try { fs.chmodSync(s, 0o755); } catch {} }
|
|
832
833
|
} catch {}
|
|
833
834
|
}
|
|
834
835
|
|
|
836
|
+
// hasScripts = real scripts on disk in the materialized subdir (ground truth)
|
|
837
|
+
const hasScripts = globSync(SCRIPT_GLOBS.map(p => `${destBase}/${p}`), { absolute: true }).length > 0;
|
|
838
|
+
|
|
835
839
|
// Step 5: filter out non-skill files locally
|
|
836
|
-
|
|
840
|
+
// absolute:true — otherwise glob returns cwd-relative paths; when destBase is not
|
|
841
|
+
// under cwd those contain ".." and validateSkill flags every file as path-traversal.
|
|
842
|
+
const allMd = globSync(`${destBase}/**/*.md`, { absolute: true });
|
|
837
843
|
prog(`Filtering ${allMd.length} files...`);
|
|
838
844
|
let removed = 0;
|
|
839
845
|
for (const fp of allMd) {
|
|
@@ -841,7 +847,7 @@ export async function importFromGitHubLight(repoUrl) {
|
|
|
841
847
|
}
|
|
842
848
|
if (removed > 0) removeEmptyDirs(destBase);
|
|
843
849
|
|
|
844
|
-
const remaining = globSync(`${destBase}/**/*.md
|
|
850
|
+
const remaining = globSync(`${destBase}/**/*.md`, { absolute: true });
|
|
845
851
|
prog(`Validating ${remaining.length} skill files...`);
|
|
846
852
|
let removedV = 0;
|
|
847
853
|
for (const fp of remaining) {
|
|
@@ -850,15 +856,29 @@ export async function importFromGitHubLight(repoUrl) {
|
|
|
850
856
|
}
|
|
851
857
|
if (removedV > 0) removeEmptyDirs(destBase);
|
|
852
858
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
const realCount = globSync(`${destBase}/**/*.md`).length;
|
|
859
|
+
const realCount = globSync(`${destBase}/**/*.md`, { absolute: true }).length;
|
|
856
860
|
|
|
857
861
|
if (realCount < 1) {
|
|
858
862
|
fs.rmSync(destBase, { recursive: true, force: true });
|
|
859
863
|
throw new Error('No valid skills in repo — all files were filtered out');
|
|
860
864
|
}
|
|
861
865
|
|
|
866
|
+
return { realCount, hasScripts, subdir };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export async function importFromGitHubLight(repoUrl) {
|
|
870
|
+
if (!repoUrl) throw new Error('Missing repoUrl');
|
|
871
|
+
|
|
872
|
+
const url = repoUrl.startsWith('http') ? repoUrl : `https://github.com/${repoUrl}`;
|
|
873
|
+
const ownerRepo = url.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '');
|
|
874
|
+
const repoName = ownerRepo.replace('/', '-');
|
|
875
|
+
const destBase = path.join(getSkillsStoreDir(), 'github', repoName);
|
|
876
|
+
|
|
877
|
+
const prog = (msg) => process.stderr.write(`\r\x1b[K ${msg}`);
|
|
878
|
+
|
|
879
|
+
const { realCount, subdir } = cloneAndFilterRepo(ownerRepo, destBase, prog);
|
|
880
|
+
process.stderr.write('\n');
|
|
881
|
+
|
|
862
882
|
// Update both caches with the real post-filter count
|
|
863
883
|
try {
|
|
864
884
|
const { setCachedCount } = await import('./bundle-counts.js');
|
package/package.json
CHANGED
package/tui.js
CHANGED
|
@@ -194,7 +194,8 @@ function render(state, installedSet = new Set()) {
|
|
|
194
194
|
: dim(' Enter') + chalk.white(' install') + dim(' ');
|
|
195
195
|
write(instLabel + dim('Tab') + ' all/skills/bundles/recent ' + dim('/') + ' search ' + dim('q') + ' quit' + CLEAR_EOL + '\n');
|
|
196
196
|
const ghUrl = sel.repo_url ? chalk.hex('#3B82F6')(` ↗ github.com/${sel.repo_url}`) : '';
|
|
197
|
-
|
|
197
|
+
const toolsNote = sel.has_tools ? chalk.hex('#F59E0B')(' 🔧 includes scripts (py/sh/js)') : '';
|
|
198
|
+
write(dim(` → pg ${installCmd}`) + ghUrl + toolsNote + CLEAR_EOL + '\n');
|
|
198
199
|
} else if (state.confirming) {
|
|
199
200
|
write(chalk.red.bold(' Remove ') + chalk.white(state.confirming.name) + chalk.red('? ') +
|
|
200
201
|
chalk.white.bold('[y]') + chalk.gray('es ') + chalk.white.bold('[n]') + chalk.gray('o') + CLEAR_EOL + '\n');
|