Compare commits
1 Commits
d0f2518d5f
...
3767f06d6a
| Author | SHA1 | Date |
|---|---|---|
|
|
3767f06d6a |
|
|
@ -835,15 +835,14 @@ class Manifest {
|
|||
// Check if this is a custom module (from user-provided URL)
|
||||
const { CustomModuleManager } = require('../modules/custom-module-manager');
|
||||
const customMgr = new CustomModuleManager();
|
||||
const resolved = customMgr.getResolution(moduleName);
|
||||
const customSource = await customMgr.findModuleSourceByCode(moduleName);
|
||||
if (customSource || resolved) {
|
||||
const customVersion = resolved?.version || (await this._readMarketplaceVersion(moduleName, moduleSourcePath));
|
||||
if (customSource) {
|
||||
const customVersion = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
|
||||
return {
|
||||
version: customVersion,
|
||||
source: 'custom',
|
||||
npmPackage: null,
|
||||
repoUrl: resolved?.repoUrl || null,
|
||||
repoUrl: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,6 @@ class CustomModuleManager {
|
|||
* @param {string} repoUrl - GitHub repository URL
|
||||
* @param {Object} [options] - Clone options
|
||||
* @param {boolean} [options.silent] - Suppress spinner output
|
||||
* @param {boolean} [options.skipInstall] - Skip npm install (for browsing before user confirms)
|
||||
* @returns {string} Path to the cloned repository
|
||||
*/
|
||||
async cloneRepo(repoUrl, options = {}) {
|
||||
|
|
@ -160,9 +159,9 @@ class CustomModuleManager {
|
|||
}
|
||||
}
|
||||
|
||||
// Install dependencies if package.json exists (skip during browsing/analysis)
|
||||
// Install dependencies if package.json exists
|
||||
const packageJsonPath = path.join(repoCacheDir, 'package.json');
|
||||
if (!options.skipInstall && (await fs.pathExists(packageJsonPath))) {
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
const installSpinner = await createSpinner();
|
||||
installSpinner.start(`Installing dependencies for ${owner}/${repo}...`);
|
||||
try {
|
||||
|
|
@ -188,17 +187,15 @@ class CustomModuleManager {
|
|||
* Results are cached in _resolutionCache keyed by module code.
|
||||
* @param {string} repoPath - Absolute path to the cloned repository
|
||||
* @param {Object} plugin - Raw plugin object from marketplace.json
|
||||
* @param {string} [repoUrl] - Original GitHub URL for manifest tracking
|
||||
* @returns {Promise<Array<Object>>} Array of ResolvedModule objects
|
||||
*/
|
||||
async resolvePlugin(repoPath, plugin, repoUrl) {
|
||||
async resolvePlugin(repoPath, plugin) {
|
||||
const { PluginResolver } = require('./plugin-resolver');
|
||||
const resolver = new PluginResolver();
|
||||
const resolved = await resolver.resolve(repoPath, plugin);
|
||||
|
||||
// Stamp repo URL onto each resolved module for manifest tracking
|
||||
// Cache each resolved module by its code for lookup during install
|
||||
for (const mod of resolved) {
|
||||
if (repoUrl) mod.repoUrl = repoUrl;
|
||||
CustomModuleManager._resolutionCache.set(mod.code, mod);
|
||||
}
|
||||
|
||||
|
|
@ -291,8 +288,6 @@ class CustomModuleManager {
|
|||
|
||||
// Search through all custom repo caches
|
||||
try {
|
||||
const { PluginResolver } = require('./plugin-resolver');
|
||||
const resolver = new PluginResolver();
|
||||
const owners = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||
for (const ownerEntry of owners) {
|
||||
if (!ownerEntry.isDirectory()) continue;
|
||||
|
|
@ -308,37 +303,14 @@ class CustomModuleManager {
|
|||
try {
|
||||
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
||||
for (const plugin of data.plugins || []) {
|
||||
// Direct name match (legacy behavior)
|
||||
if (plugin.name === moduleCode) {
|
||||
// Found the module - find its source
|
||||
const sourcePath = plugin.source ? path.join(repoPath, plugin.source) : repoPath;
|
||||
const moduleYaml = path.join(sourcePath, 'module.yaml');
|
||||
if (await fs.pathExists(moduleYaml)) {
|
||||
return sourcePath;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve plugin to check if any module.yaml code matches
|
||||
if (plugin.skills && plugin.skills.length > 0) {
|
||||
try {
|
||||
const resolved = await resolver.resolve(repoPath, plugin);
|
||||
for (const mod of resolved) {
|
||||
if (mod.code === moduleCode) {
|
||||
// Derive repo URL from cache path for manifest tracking
|
||||
const repoUrl = `https://github.com/${ownerEntry.name}/${repoEntry.name}`;
|
||||
mod.repoUrl = repoUrl;
|
||||
CustomModuleManager._resolutionCache.set(mod.code, mod);
|
||||
if (mod.moduleYamlPath) {
|
||||
return path.dirname(mod.moduleYamlPath);
|
||||
}
|
||||
if (mod.skillPaths && mod.skillPaths.length > 0) {
|
||||
return path.dirname(mod.skillPaths[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip unresolvable plugins
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed marketplace.json
|
||||
|
|
|
|||
|
|
@ -326,20 +326,15 @@ class OfficialModules {
|
|||
if (fileTrackingCallback) fileTrackingCallback(helpTarget);
|
||||
}
|
||||
|
||||
// Create directories declared in module.yaml (strategies 1-4 may have these)
|
||||
if (!options.skipModuleInstaller) {
|
||||
await this.createModuleDirectories(resolved.code, bmadDir, options);
|
||||
}
|
||||
|
||||
// Update manifest
|
||||
const { Manifest } = require('../core/manifest');
|
||||
const manifestObj = new Manifest();
|
||||
|
||||
await manifestObj.addModule(bmadDir, resolved.code, {
|
||||
version: resolved.version || null,
|
||||
source: 'custom',
|
||||
npmPackage: null,
|
||||
repoUrl: resolved.repoUrl || null,
|
||||
version: resolved.version || '',
|
||||
source: `custom:${resolved.pluginName}`,
|
||||
npmPackage: '',
|
||||
repoUrl: '',
|
||||
});
|
||||
|
||||
return { success: true, module: resolved.code, path: targetPath, versionInfo: { version: resolved.version || '' } };
|
||||
|
|
|
|||
|
|
@ -33,16 +33,11 @@ class PluginResolver {
|
|||
return [];
|
||||
}
|
||||
|
||||
// Resolve skill paths to absolute, constrain to repo root, filter non-existent
|
||||
const repoRoot = path.resolve(repoPath);
|
||||
// Resolve skill paths to absolute and filter out non-existent
|
||||
const skillPaths = [];
|
||||
for (const rel of skillRelPaths) {
|
||||
const normalized = rel.replace(/^\.\//, '');
|
||||
const abs = path.resolve(repoPath, normalized);
|
||||
// Guard against path traversal (.. segments, absolute paths in marketplace.json)
|
||||
if (!abs.startsWith(repoRoot + path.sep) && abs !== repoRoot) {
|
||||
continue;
|
||||
}
|
||||
const abs = path.join(repoPath, normalized);
|
||||
if (await fs.pathExists(abs)) {
|
||||
skillPaths.push(abs);
|
||||
}
|
||||
|
|
@ -389,7 +384,7 @@ class PluginResolver {
|
|||
_escapeCSVField(value) {
|
||||
if (!value) return '';
|
||||
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
||||
return `"${value.replaceAll('"', '""')}"`;
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -863,11 +863,11 @@ class UI {
|
|||
'UNVERIFIED MODULE: This module has not been reviewed by the BMad team.\n' + ' Only install modules from sources you trust.',
|
||||
);
|
||||
|
||||
// Clone the repo so we can resolve plugin structures (skip npm install until user confirms)
|
||||
// Clone the repo so we can resolve plugin structures
|
||||
s.start('Cloning repository...');
|
||||
let repoPath;
|
||||
try {
|
||||
repoPath = await customMgr.cloneRepo(url.trim(), { skipInstall: true });
|
||||
repoPath = await customMgr.cloneRepo(url.trim());
|
||||
s.stop('Repository cloned');
|
||||
} catch (cloneError) {
|
||||
s.error('Failed to clone repository');
|
||||
|
|
@ -881,7 +881,7 @@ class UI {
|
|||
const allResolved = [];
|
||||
for (const plugin of plugins) {
|
||||
try {
|
||||
const resolved = await customMgr.resolvePlugin(repoPath, plugin.rawPlugin, url.trim());
|
||||
const resolved = await customMgr.resolvePlugin(repoPath, plugin.rawPlugin);
|
||||
if (resolved.length > 0) {
|
||||
allResolved.push(...resolved);
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in New Issue