Compare commits

..

No commits in common. "7302f350b566f8d17393eb688edd7a591aa636ad" and "d03ba50a6058dd624cfd91d99aefd6b6317944c0" have entirely different histories.

5 changed files with 18 additions and 57 deletions

View File

@ -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,
};
}

View File

@ -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

View File

@ -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 || '' } };

View File

@ -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;
}

View File

@ -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 {