promptgraph-mcp 2.9.29 → 2.9.30
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/init.js +16 -88
- package/commands/setup.js +42 -19
- package/config.js +48 -31
- package/github-import.js +3 -3
- package/marketplace.js +18 -17
- package/package.json +1 -1
package/commands/init.js
CHANGED
|
@@ -1,99 +1,27 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
3
|
-
import { success, error, info } from '../cli.js';
|
|
4
|
-
|
|
5
|
-
const PLATFORM_CONFIGS = {
|
|
6
|
-
'claude-code': {
|
|
7
|
-
label: 'Claude Code — ~/.claude/settings.json',
|
|
8
|
-
snippet: { mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } },
|
|
9
|
-
},
|
|
10
|
-
'claude-desktop': {
|
|
11
|
-
label: 'Claude Desktop — claude_desktop_config.json',
|
|
12
|
-
snippet: { mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } },
|
|
13
|
-
},
|
|
14
|
-
'opencode': {
|
|
15
|
-
label: 'OpenCode — opencode.json',
|
|
16
|
-
snippet: { mcp: { promptgraph: { type: 'local', command: ['cmd', '/c', 'npx', 'promptgraph-mcp'], enabled: true } } },
|
|
17
|
-
},
|
|
18
|
-
'cursor': {
|
|
19
|
-
label: 'Cursor — ~/.cursor/mcp.json',
|
|
20
|
-
snippet: { mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } },
|
|
21
|
-
},
|
|
22
|
-
'windsurf': {
|
|
23
|
-
label: 'Windsurf — mcp_config.json',
|
|
24
|
-
snippet: { mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } },
|
|
25
|
-
},
|
|
26
|
-
'cline': {
|
|
27
|
-
label: 'Cline — ~/.vscode/mcp.json',
|
|
28
|
-
snippet: { servers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } },
|
|
29
|
-
},
|
|
30
|
-
'codex': {
|
|
31
|
-
label: 'OpenAI Codex CLI — ~/.codex/config.json',
|
|
32
|
-
snippet: { mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } },
|
|
33
|
-
},
|
|
34
|
-
};
|
|
2
|
+
import { success, info } from '../cli.js';
|
|
35
3
|
|
|
36
4
|
export default async function handler(args, bin) {
|
|
37
|
-
const {
|
|
38
|
-
const
|
|
39
|
-
const { detectPlatforms, PLATFORMS } = await import('../platform.js');
|
|
40
|
-
|
|
41
|
-
if (!args.includes('--yes') && !args.includes('-y')) {
|
|
42
|
-
const { createInterface } = await import('readline');
|
|
43
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
44
|
-
const answer = await new Promise(r => rl.question(
|
|
45
|
-
chalk.yellow(' ⚠') + chalk.gray(' First run downloads ~23 MB embedding model (BGE-Small-EN).\n Proceed? [Y/n] '), r
|
|
46
|
-
));
|
|
47
|
-
rl.close();
|
|
48
|
-
if (answer.trim().toLowerCase() === 'n') { info('Aborted.'); process.exit(0); }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
console.log(chalk.gray('\n Downloading embedding model (~23 MB, one-time)...\n'));
|
|
52
|
-
await promptConfig();
|
|
53
|
-
await indexAll();
|
|
54
|
-
console.log();
|
|
5
|
+
const { detectPlatforms } = await import('../platform.js');
|
|
6
|
+
const setupHandler = (await import('./setup.js')).default;
|
|
55
7
|
|
|
56
8
|
const detected = detectPlatforms();
|
|
57
|
-
const detectedIds = new Set(detected.map(p => p.id));
|
|
58
9
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
p.addMcp(p);
|
|
64
|
-
written.push(p.name || p.id);
|
|
65
|
-
} catch (e) {
|
|
66
|
-
writeErrors.push(`${p.id}: ${e.message}`);
|
|
67
|
-
}
|
|
10
|
+
if (detected.length === 0) {
|
|
11
|
+
info('No editor detected. Run: ' + chalk.white(`${bin} setup <platform>`));
|
|
12
|
+
info(chalk.gray('Platforms: claude-code, opencode, cursor, windsurf, cline, codex'));
|
|
13
|
+
process.exit(0);
|
|
68
14
|
}
|
|
69
15
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
for (const id of toShow) {
|
|
75
|
-
const cfg = PLATFORM_CONFIGS[id];
|
|
76
|
-
if (!cfg) continue;
|
|
77
|
-
console.log(
|
|
78
|
-
boxen(
|
|
79
|
-
chalk.white.bold(cfg.label) + '\n\n' +
|
|
80
|
-
chalk.gray(JSON.stringify(cfg.snippet, null, 2)),
|
|
81
|
-
{ padding: 1, borderStyle: 'round', borderColor: '#7C3AED', dimBorder: true }
|
|
82
|
-
)
|
|
83
|
-
);
|
|
16
|
+
if (detected.length === 1) {
|
|
17
|
+
info(`Detected: ${chalk.white(detected[0].name)}`);
|
|
18
|
+
await setupHandler([args[0], detected[0].id], bin);
|
|
19
|
+
return;
|
|
84
20
|
}
|
|
85
21
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
console.log(chalk.gray(` Or run: `) + chalk.white(`${bin} setup <platform>`) + chalk.gray(' (claude-code, opencode, cursor, windsurf, cline, codex)\n'));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (writeErrors.length > 0) {
|
|
95
|
-
for (const e of writeErrors) console.log(chalk.red(' ✗') + chalk.gray(' ' + e));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
process.exit(0);
|
|
22
|
+
// Multiple editors — set up all, use first as primary skills dir
|
|
23
|
+
info(`Detected ${detected.length} editors:`);
|
|
24
|
+
for (const p of detected) info(` ${chalk.white(p.id.padEnd(16))} ${chalk.gray(p.name)}`);
|
|
25
|
+
console.log();
|
|
26
|
+
await setupHandler([args[0], detected[0].id], bin);
|
|
99
27
|
}
|
package/commands/setup.js
CHANGED
|
@@ -1,19 +1,42 @@
|
|
|
1
|
-
import { colors,
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
|
|
4
|
-
export default async function handler(args, bin) {
|
|
5
|
-
const { detectPlatforms, PLATFORMS } = await import('../platform.js');
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
1
|
+
import { colors, success, error, info, section } from '../cli.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
export default async function handler(args, bin) {
|
|
5
|
+
const { detectPlatforms, PLATFORMS } = await import('../platform.js');
|
|
6
|
+
const { setupForPlatform, PLATFORM_SKILLS_DIRS } = await import('../config.js');
|
|
7
|
+
const { indexAll } = await import('../indexer.js');
|
|
8
|
+
const platformId = args[1];
|
|
9
|
+
|
|
10
|
+
if (!platformId) {
|
|
11
|
+
section('Detected platforms');
|
|
12
|
+
detectPlatforms().forEach(p => info(`${chalk.white(p.id.padEnd(16))} ${chalk.gray(p.name)}`));
|
|
13
|
+
console.log(chalk.gray('\n Usage: pg setup <platform>\n'));
|
|
14
|
+
console.log(chalk.gray(' Platforms: claude-code, claude-desktop, opencode, cursor, windsurf, cline, codex\n'));
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const platform = PLATFORMS[platformId];
|
|
19
|
+
if (!platform) { error(`Unknown platform: ${platformId}`); process.exit(1); }
|
|
20
|
+
|
|
21
|
+
// 1. Write MCP config
|
|
22
|
+
try {
|
|
23
|
+
platform.addMcp(platform);
|
|
24
|
+
success(`MCP registered in ${chalk.white(platform.name)}`);
|
|
25
|
+
info(chalk.gray(` Config: ${platform.configPath}`));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
error(`Failed to write MCP config: ${e.message}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. Set skills dir for this platform
|
|
31
|
+
const config = setupForPlatform(platformId);
|
|
32
|
+
const skillsDir = config.skillsDir;
|
|
33
|
+
success(`Skills directory: ${chalk.white(skillsDir)}`);
|
|
34
|
+
info(chalk.gray(' Marketplace installs and pg import will save here'));
|
|
35
|
+
|
|
36
|
+
// 3. Reindex
|
|
37
|
+
console.log(chalk.gray('\n Indexing skills...\n'));
|
|
38
|
+
await indexAll();
|
|
39
|
+
|
|
40
|
+
console.log(chalk.gray(`\n Restart ${platform.name} to activate.\n`));
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
package/config.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
-
import readline from 'readline';
|
|
5
4
|
|
|
6
|
-
const
|
|
5
|
+
const HOME = os.homedir();
|
|
6
|
+
const CLAUDE_DIR = path.join(HOME, '.claude');
|
|
7
7
|
export const PROMPTGRAPH_DIR = path.join(CLAUDE_DIR, '.promptgraph');
|
|
8
|
-
export const SKILLS_STORE_DIR = path.join(CLAUDE_DIR, 'skills-store');
|
|
9
8
|
const CONFIG_PATH = path.join(PROMPTGRAPH_DIR, 'config.json');
|
|
10
9
|
|
|
10
|
+
export const PLATFORM_SKILLS_DIRS = {
|
|
11
|
+
'claude-code': path.join(HOME, '.claude', 'skills-store'),
|
|
12
|
+
'claude-desktop': path.join(HOME, '.claude', 'skills-store'),
|
|
13
|
+
'opencode': path.join(HOME, '.config', 'opencode', 'skills'),
|
|
14
|
+
'cursor': path.join(HOME, '.cursor', 'skills'),
|
|
15
|
+
'windsurf': path.join(HOME, '.codeium', 'windsurf', 'skills'),
|
|
16
|
+
'cline': path.join(HOME, '.vscode', 'skills'),
|
|
17
|
+
'codex': path.join(HOME, '.codex', 'skills'),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function getSkillsStoreDir(config) {
|
|
21
|
+
const cfg = config || loadConfig();
|
|
22
|
+
if (cfg.skillsDir) return cfg.skillsDir;
|
|
23
|
+
return path.join(CLAUDE_DIR, 'skills-store');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const SKILLS_STORE_DIR = path.join(CLAUDE_DIR, 'skills-store');
|
|
27
|
+
|
|
11
28
|
export const MAX_DOWNLOAD_SIZE = 50 * 1024 * 1024 // 50 MB per file
|
|
12
29
|
export const MAX_FILE_COUNT = 50000 // max files per repo
|
|
13
30
|
export const MAX_REPO_SIZE = 500 * 1024 * 1024 // 500 MB per repo
|
|
@@ -15,20 +32,25 @@ export const RATE_LIMIT_REQUESTS = 30 // requests per window
|
|
|
15
32
|
export const RATE_LIMIT_WINDOW_MS = 60000 // 1 minute window
|
|
16
33
|
export const BATCH_SIZE = 100 // batch indexing size
|
|
17
34
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
35
|
+
function makeDefaults(skillsDir) {
|
|
36
|
+
const base = skillsDir || path.join(CLAUDE_DIR, 'skills-store');
|
|
37
|
+
return {
|
|
38
|
+
skillsDir: base,
|
|
39
|
+
sources: [
|
|
40
|
+
{ dir: base, source: 'skills-store' },
|
|
41
|
+
{ dir: path.join(base, 'marketplace'), source: 'marketplace' },
|
|
42
|
+
{ dir: path.join(CLAUDE_DIR, 'skills'), source: 'skills' },
|
|
43
|
+
{ dir: path.join(CLAUDE_DIR, 'commands'), source: 'commands' },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
25
47
|
|
|
26
48
|
|
|
27
49
|
export function loadConfig() {
|
|
28
50
|
if (fs.existsSync(CONFIG_PATH)) {
|
|
29
51
|
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
30
52
|
}
|
|
31
|
-
return JSON.parse(JSON.stringify(
|
|
53
|
+
return JSON.parse(JSON.stringify(makeDefaults()));
|
|
32
54
|
}
|
|
33
55
|
|
|
34
56
|
export function saveConfig(config) {
|
|
@@ -37,30 +59,25 @@ export function saveConfig(config) {
|
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
export async function promptConfig() {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.log('Default skill directories:');
|
|
45
|
-
DEFAULTS.sources.forEach((s, i) => console.log(` ${i + 1}. ${s.dir}`));
|
|
46
|
-
|
|
47
|
-
const extra = await ask('\nAdd extra skill directories? (comma-separated paths, or press Enter to skip): ');
|
|
48
|
-
rl.close();
|
|
49
|
-
|
|
50
|
-
const config = structuredClone(DEFAULTS);
|
|
62
|
+
const config = makeDefaults();
|
|
63
|
+
saveConfig(config);
|
|
64
|
+
return config;
|
|
65
|
+
}
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
export function setupForPlatform(platformId) {
|
|
68
|
+
const skillsDir = PLATFORM_SKILLS_DIRS[platformId] || path.join(CLAUDE_DIR, 'skills-store');
|
|
69
|
+
const existing = loadConfig();
|
|
70
|
+
const config = makeDefaults(skillsDir);
|
|
71
|
+
config.platform = platformId;
|
|
72
|
+
// preserve any custom sources the user added
|
|
73
|
+
if (existing.sources) {
|
|
74
|
+
for (const s of existing.sources) {
|
|
75
|
+
if (s.source && s.source.startsWith('custom:') && !config.sources.find(x => x.dir === s.dir)) {
|
|
76
|
+
config.sources.push(s);
|
|
77
|
+
}
|
|
59
78
|
}
|
|
60
79
|
}
|
|
61
|
-
|
|
62
80
|
saveConfig(config);
|
|
63
|
-
console.log(`\nConfig saved to ${CONFIG_PATH}`);
|
|
64
81
|
return config;
|
|
65
82
|
}
|
|
66
83
|
|
package/github-import.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'fs';
|
|
|
4
4
|
import https from 'https';
|
|
5
5
|
import { globSync } from 'glob';
|
|
6
6
|
import { indexAll, indexSource } from './indexer.js';
|
|
7
|
-
import { loadConfig, saveConfig, PROMPTGRAPH_DIR,
|
|
7
|
+
import { loadConfig, saveConfig, PROMPTGRAPH_DIR, getSkillsStoreDir, MAX_DOWNLOAD_SIZE, MAX_FILE_COUNT, MAX_REPO_SIZE, RATE_LIMIT_REQUESTS, RATE_LIMIT_WINDOW_MS } from './config.js';
|
|
8
8
|
import { validateSkill } from './validator.js';
|
|
9
9
|
import { isSkillFile, filterWithClassifier, parseSkillFile } from './parser.js';
|
|
10
10
|
import { RateLimiter } from './src/utils/rate-limiter.js';
|
|
@@ -478,7 +478,7 @@ export async function importFromGitHub(repoUrl) {
|
|
|
478
478
|
const url = repoUrl.startsWith('http') ? repoUrl : `https://github.com/${repoUrl}`;
|
|
479
479
|
const ownerRepo = url.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '');
|
|
480
480
|
const repoName = ownerRepo.replace('/', '-');
|
|
481
|
-
const dest = path.join(
|
|
481
|
+
const dest = path.join(getSkillsStoreDir(), 'github', repoName);
|
|
482
482
|
|
|
483
483
|
const isNew = !fs.existsSync(dest);
|
|
484
484
|
|
|
@@ -671,7 +671,7 @@ export async function importFromGitHubLight(repoUrl) {
|
|
|
671
671
|
const url = repoUrl.startsWith('http') ? repoUrl : `https://github.com/${repoUrl}`;
|
|
672
672
|
const ownerRepo = url.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '');
|
|
673
673
|
const repoName = ownerRepo.replace('/', '-');
|
|
674
|
-
const destBase = path.join(
|
|
674
|
+
const destBase = path.join(getSkillsStoreDir(), 'github', repoName);
|
|
675
675
|
const cloneUrl = `https://github.com/${ownerRepo}.git`;
|
|
676
676
|
|
|
677
677
|
if (fs.existsSync(destBase)) fs.rmSync(destBase, { recursive: true, force: true });
|
package/marketplace.js
CHANGED
|
@@ -7,14 +7,15 @@ import { createRequire } from 'module';
|
|
|
7
7
|
import { getDb } from './db.js';
|
|
8
8
|
import { globSync } from 'glob';
|
|
9
9
|
import { validateSkill, validateBundle } from './validator.js';
|
|
10
|
-
import { loadConfig, saveConfig, PROMPTGRAPH_DIR,
|
|
10
|
+
import { loadConfig, saveConfig, PROMPTGRAPH_DIR, getSkillsStoreDir } from './config.js';
|
|
11
11
|
import { importFromGitHubLight, validateRepoSkills } from './github-import.js';
|
|
12
12
|
import { isSkillFile } from './parser.js';
|
|
13
13
|
|
|
14
14
|
const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
|
|
15
15
|
const SKILL_COUNT_CACHE = path.join(PROMPTGRAPH_DIR, 'skill-counts.json');
|
|
16
16
|
const DEAD_REPOS_FILE = path.join(PROMPTGRAPH_DIR, 'dead-repos.json');
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
function getSkillsDir() { return path.join(getSkillsStoreDir(), 'marketplace'); }
|
|
18
19
|
|
|
19
20
|
// Atomically write content to dest via tmp — cleans up on failure
|
|
20
21
|
function writeSkillAtomic(dest, content) {
|
|
@@ -203,14 +204,14 @@ export async function installSkillFromUrl(url) {
|
|
|
203
204
|
try {
|
|
204
205
|
const rawUrl = githubToRaw(url) || url;
|
|
205
206
|
const content = await fetchText(rawUrl);
|
|
206
|
-
fs.mkdirSync(
|
|
207
|
+
fs.mkdirSync(getSkillsDir(), { recursive: true });
|
|
207
208
|
ensureMarketplaceSource();
|
|
208
209
|
|
|
209
210
|
// derive filename from URL
|
|
210
211
|
const urlName = rawUrl.split('/').pop().replace(/[^a-z0-9-_.]/gi, '-');
|
|
211
|
-
const dest = path.join(
|
|
212
|
+
const dest = path.join(getSkillsDir(), urlName.endsWith('.md') ? urlName : urlName + '.md');
|
|
212
213
|
const resolvedDest = path.resolve(dest);
|
|
213
|
-
if (!resolvedDest.startsWith(path.resolve(
|
|
214
|
+
if (!resolvedDest.startsWith(path.resolve(getSkillsDir()))) {
|
|
214
215
|
return { error: 'Path traversal blocked: destination outside marketplace directory' };
|
|
215
216
|
}
|
|
216
217
|
const v = writeSkillAtomic(dest, content);
|
|
@@ -248,11 +249,11 @@ export async function installSkill(query) {
|
|
|
248
249
|
if (!skill.raw_url) return { error: `Skill "${skill.id}" has no download URL` };
|
|
249
250
|
const skillId = skill.id;
|
|
250
251
|
|
|
251
|
-
fs.mkdirSync(
|
|
252
|
+
fs.mkdirSync(getSkillsDir(), { recursive: true });
|
|
252
253
|
ensureMarketplaceSource();
|
|
253
|
-
const dest = path.join(
|
|
254
|
+
const dest = path.join(getSkillsDir(), `${skillId}.md`);
|
|
254
255
|
const resolvedDest = path.resolve(dest);
|
|
255
|
-
if (!resolvedDest.startsWith(path.resolve(
|
|
256
|
+
if (!resolvedDest.startsWith(path.resolve(getSkillsDir()))) {
|
|
256
257
|
return { error: 'Path traversal blocked: destination outside marketplace directory' };
|
|
257
258
|
}
|
|
258
259
|
|
|
@@ -348,7 +349,7 @@ export async function browseBundles(topK = 20) {
|
|
|
348
349
|
function ensureMarketplaceSource() {
|
|
349
350
|
const config = loadConfig();
|
|
350
351
|
if (!config.sources.find(s => s.source === 'marketplace')) {
|
|
351
|
-
config.sources.push({ dir:
|
|
352
|
+
config.sources.push({ dir: getSkillsDir(), source: 'marketplace' });
|
|
352
353
|
saveConfig(config);
|
|
353
354
|
}
|
|
354
355
|
}
|
|
@@ -396,7 +397,7 @@ async function _execRepoInstall(bundle) {
|
|
|
396
397
|
}
|
|
397
398
|
|
|
398
399
|
async function _execSkillsInstall(bundle, validSkills) {
|
|
399
|
-
fs.mkdirSync(
|
|
400
|
+
fs.mkdirSync(getSkillsDir(), { recursive: true });
|
|
400
401
|
ensureMarketplaceSource();
|
|
401
402
|
const installed = [];
|
|
402
403
|
const failed = [];
|
|
@@ -407,15 +408,15 @@ async function _execSkillsInstall(bundle, validSkills) {
|
|
|
407
408
|
try {
|
|
408
409
|
if (installed.length > 0) await delay(300);
|
|
409
410
|
const content = await fetchText(skill.raw_url);
|
|
410
|
-
const dest = path.join(
|
|
411
|
+
const dest = path.join(getSkillsDir(), `${skillId}.md`);
|
|
411
412
|
const resolvedDest = path.resolve(dest);
|
|
412
|
-
if (!resolvedDest.startsWith(path.resolve(
|
|
413
|
+
if (!resolvedDest.startsWith(path.resolve(getSkillsDir()))) { failed.push(skillId); continue; }
|
|
413
414
|
const v = writeSkillAtomic(dest, content);
|
|
414
415
|
if (!v.ok) { failed.push(skillId); continue; }
|
|
415
416
|
installed.push(skillId);
|
|
416
417
|
} catch { failed.push(skillId); }
|
|
417
418
|
}
|
|
418
|
-
return { success: true, bundle: bundle.name, installed, failed, dir:
|
|
419
|
+
return { success: true, bundle: bundle.name, installed, failed, dir: getSkillsDir() };
|
|
419
420
|
}
|
|
420
421
|
|
|
421
422
|
export async function installBundle(bundleId) {
|
|
@@ -770,13 +771,13 @@ export async function incrementDownloads(name) {
|
|
|
770
771
|
|
|
771
772
|
export function validateAndPruneMarketplace() {
|
|
772
773
|
const results = { valid: [], removed: [], errors: [] };
|
|
773
|
-
if (!fs.existsSync(
|
|
774
|
+
if (!fs.existsSync(getSkillsDir())) {
|
|
774
775
|
return { ...results, message: 'No marketplace directory found.' };
|
|
775
776
|
}
|
|
776
777
|
|
|
777
|
-
const mdFiles = globSync(`${
|
|
778
|
+
const mdFiles = globSync(`${getSkillsDir()}/**/*.md`, { absolute: true });
|
|
778
779
|
for (const fp of mdFiles) {
|
|
779
|
-
const name = path.relative(
|
|
780
|
+
const name = path.relative(getSkillsDir(), fp);
|
|
780
781
|
try {
|
|
781
782
|
const v = validateSkill(fp);
|
|
782
783
|
if (!v.ok) {
|
|
@@ -790,7 +791,7 @@ export function validateAndPruneMarketplace() {
|
|
|
790
791
|
}
|
|
791
792
|
|
|
792
793
|
// Clean up empty dirs left behind
|
|
793
|
-
removeEmptyDirs(
|
|
794
|
+
removeEmptyDirs(getSkillsDir());
|
|
794
795
|
|
|
795
796
|
// Also remove DB entries for deleted files
|
|
796
797
|
const db = getDb();
|