fix(installer): address Augment review findings
- Fix plugins[0] fragility: extract highest version across all plugins in marketplace.json instead of assuming first entry (ui.js, installer.js, manifest.js) - Fix _readMarketplaceVersion ignoring moduleSourcePath: custom modules can now source their own marketplace.json by walking up from source path - Hard-exclude bmad-os-* utility skills in both surgical and legacy cleanup modes, preventing accidental deletion if tracked in manifests - Distinguish missing file vs parse error in skill-manifest.csv reading: warn on corrupt CSV instead of silently skipping cleanup
This commit is contained in:
parent
2333ee9488
commit
3c155e3cbc
|
|
@ -39,7 +39,7 @@ class Installer {
|
||||||
if (await fs.pathExists(marketplacePath)) {
|
if (await fs.pathExists(marketplacePath)) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
||||||
return data.plugins?.[0]?.version || '';
|
return this._extractMarketplaceVersion(data);
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
@ -51,6 +51,19 @@ class Installer {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the highest version from marketplace.json plugins array
|
||||||
|
*/
|
||||||
|
_extractMarketplaceVersion(data) {
|
||||||
|
const plugins = data?.plugins;
|
||||||
|
if (!Array.isArray(plugins) || plugins.length === 0) return '';
|
||||||
|
let best = '';
|
||||||
|
for (const p of plugins) {
|
||||||
|
if (p.version && (!best || p.version > best)) best = p.version;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main installation method
|
* Main installation method
|
||||||
* @param {Object} config - Installation configuration
|
* @param {Object} config - Installation configuration
|
||||||
|
|
@ -95,17 +108,17 @@ class Installer {
|
||||||
// Capture previously installed skill IDs before they get overwritten
|
// Capture previously installed skill IDs before they get overwritten
|
||||||
const previousSkillIds = new Set();
|
const previousSkillIds = new Set();
|
||||||
const prevCsvPath = path.join(paths.bmadDir, '_config', 'skill-manifest.csv');
|
const prevCsvPath = path.join(paths.bmadDir, '_config', 'skill-manifest.csv');
|
||||||
try {
|
if (await fs.pathExists(prevCsvPath)) {
|
||||||
if (await fs.pathExists(prevCsvPath)) {
|
try {
|
||||||
const csvParse = require('csv-parse/sync');
|
const csvParse = require('csv-parse/sync');
|
||||||
const content = await fs.readFile(prevCsvPath, 'utf8');
|
const content = await fs.readFile(prevCsvPath, 'utf8');
|
||||||
const records = csvParse.parse(content, { columns: true, skip_empty_lines: true });
|
const records = csvParse.parse(content, { columns: true, skip_empty_lines: true });
|
||||||
for (const r of records) {
|
for (const r of records) {
|
||||||
if (r.canonicalId) previousSkillIds.add(r.canonicalId);
|
if (r.canonicalId) previousSkillIds.add(r.canonicalId);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await prompts.log.warn(`Failed to parse skill-manifest.csv: ${error.message}`);
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// No previous manifest - fresh install
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._cacheCustomModules(paths, addResult);
|
await this._cacheCustomModules(paths, addResult);
|
||||||
|
|
|
||||||
|
|
@ -841,7 +841,7 @@ class Manifest {
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
|
|
||||||
// All module versions come from .claude-plugin/marketplace.json
|
// All module versions come from .claude-plugin/marketplace.json
|
||||||
const version = await this._readMarketplaceVersion(moduleName);
|
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
|
||||||
|
|
||||||
// Determine source type
|
// Determine source type
|
||||||
if (['core', 'bmm'].includes(moduleName)) {
|
if (['core', 'bmm'].includes(moduleName)) {
|
||||||
|
|
@ -900,13 +900,29 @@ class Manifest {
|
||||||
* @param {string} moduleName - Module code
|
* @param {string} moduleName - Module code
|
||||||
* @returns {string|null} Version or null
|
* @returns {string|null} Version or null
|
||||||
*/
|
*/
|
||||||
async _readMarketplaceVersion(moduleName) {
|
async _readMarketplaceVersion(moduleName, moduleSourcePath = null) {
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
let marketplacePath;
|
let marketplacePath;
|
||||||
|
|
||||||
if (['core', 'bmm'].includes(moduleName)) {
|
if (['core', 'bmm'].includes(moduleName)) {
|
||||||
marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
|
marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
|
||||||
} else {
|
} else if (moduleSourcePath) {
|
||||||
|
// Walk up from source path to find marketplace.json
|
||||||
|
let dir = moduleSourcePath;
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const candidate = path.join(dir, '.claude-plugin', 'marketplace.json');
|
||||||
|
if (await fs.pathExists(candidate)) {
|
||||||
|
marketplacePath = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const parent = path.dirname(dir);
|
||||||
|
if (parent === dir) break;
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to external module cache
|
||||||
|
if (!marketplacePath) {
|
||||||
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
|
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
|
||||||
marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
|
marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
|
||||||
}
|
}
|
||||||
|
|
@ -914,7 +930,13 @@ class Manifest {
|
||||||
try {
|
try {
|
||||||
if (await fs.pathExists(marketplacePath)) {
|
if (await fs.pathExists(marketplacePath)) {
|
||||||
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
||||||
return data.plugins?.[0]?.version || null;
|
const plugins = data?.plugins;
|
||||||
|
if (!Array.isArray(plugins) || plugins.length === 0) return null;
|
||||||
|
let best = null;
|
||||||
|
for (const p of plugins) {
|
||||||
|
if (p.version && (!best || p.version > best)) best = p.version;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
|
|
|
||||||
|
|
@ -428,8 +428,11 @@ class ConfigDrivenIdeSetup {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry || typeof entry !== 'string') continue;
|
if (!entry || typeof entry !== 'string') continue;
|
||||||
|
|
||||||
|
// Always preserve bmad-os-* utility skills regardless of cleanup mode
|
||||||
|
if (entry.startsWith('bmad-os-')) continue;
|
||||||
|
|
||||||
// Surgical removal from set, or legacy prefix matching when set is null
|
// Surgical removal from set, or legacy prefix matching when set is null
|
||||||
const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad') && !entry.startsWith('bmad-os-');
|
const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad');
|
||||||
|
|
||||||
if (shouldRemove) {
|
if (shouldRemove) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ async function getMarketplaceVersion(moduleCode) {
|
||||||
try {
|
try {
|
||||||
if (await fs.pathExists(marketplacePath)) {
|
if (await fs.pathExists(marketplacePath)) {
|
||||||
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
||||||
return data.plugins?.[0]?.version || '';
|
return _extractMarketplaceVersion(data);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
|
|
@ -31,6 +31,23 @@ async function getMarketplaceVersion(moduleCode) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the highest version from marketplace.json plugins array.
|
||||||
|
* Handles multiple plugins per file safely.
|
||||||
|
* @param {Object} data - Parsed marketplace.json
|
||||||
|
* @returns {string} Version string or empty string
|
||||||
|
*/
|
||||||
|
function _extractMarketplaceVersion(data) {
|
||||||
|
const plugins = data?.plugins;
|
||||||
|
if (!Array.isArray(plugins) || plugins.length === 0) return '';
|
||||||
|
// Use the highest version across all plugins in the file
|
||||||
|
let best = '';
|
||||||
|
for (const p of plugins) {
|
||||||
|
if (p.version && (!best || p.version > best)) best = p.version;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
// Separator class for visual grouping in select/multiselect prompts
|
// Separator class for visual grouping in select/multiselect prompts
|
||||||
// Note: @clack/prompts doesn't support separators natively, they are filtered out
|
// Note: @clack/prompts doesn't support separators natively, they are filtered out
|
||||||
class Separator {
|
class Separator {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue