refactor(installer): remove dead task/tool/workflow manifest code
The remove-skill-manifest-yaml branch deleted the scanners that discover tasks, tools, and workflows but left behind the code that writes their manifest CSVs. Remove collectTasks/Tools/Workflows, writeTaskManifest/ToolManifest/WorkflowManifest, their helpers, and the now-unreachable getPreservedCsvRows/upgradeRowToSchema methods. Update installer pre-registration and test assertions accordingly.
This commit is contained in:
parent
02879f7b6b
commit
0a0d9400bb
|
|
@ -98,17 +98,6 @@ async function createSkillCollisionFixture() {
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
);
|
);
|
||||||
|
|
||||||
await fs.writeFile(
|
|
||||||
path.join(configDir, 'workflow-manifest.csv'),
|
|
||||||
[
|
|
||||||
'name,description,module,path,canonicalId',
|
|
||||||
'"help","Workflow help","core","_bmad/core/workflows/help/workflow.md","bmad-help"',
|
|
||||||
'',
|
|
||||||
].join('\n'),
|
|
||||||
);
|
|
||||||
|
|
||||||
await fs.writeFile(path.join(configDir, 'task-manifest.csv'), 'name,displayName,description,module,path,standalone,canonicalId\n');
|
|
||||||
await fs.writeFile(path.join(configDir, 'tool-manifest.csv'), 'name,displayName,description,module,path,standalone,canonicalId\n');
|
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(configDir, 'skill-manifest.csv'),
|
path.join(configDir, 'skill-manifest.csv'),
|
||||||
[
|
[
|
||||||
|
|
@ -1549,7 +1538,7 @@ async function runTests() {
|
||||||
'---\nname: Regular Workflow\ndescription: A regular workflow not a skill\n---\n\nWorkflow body\n',
|
'---\nname: Regular Workflow\ndescription: A regular workflow not a skill\n---\n\nWorkflow body\n',
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Skill inside workflows/ dir: core/workflows/wf-skill/ (exercises findWorkflows skip logic) ---
|
// --- Skill inside workflows/ dir: core/workflows/wf-skill/ ---
|
||||||
const wfSkillDir29 = path.join(tempFixture29, 'core', 'workflows', 'wf-skill');
|
const wfSkillDir29 = path.join(tempFixture29, 'core', 'workflows', 'wf-skill');
|
||||||
await fs.ensureDir(wfSkillDir29);
|
await fs.ensureDir(wfSkillDir29);
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
|
|
@ -1593,18 +1582,10 @@ async function runTests() {
|
||||||
'Skill path includes relative path from module root',
|
'Skill path includes relative path from module root',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Skill should NOT be in workflows
|
|
||||||
const inWorkflows29 = generator29.workflows.find((w) => w.name === 'my-skill');
|
|
||||||
assert(inWorkflows29 === undefined, 'Skill at unusual path does NOT appear in workflows[]');
|
|
||||||
|
|
||||||
// Skill in tasks/ dir should be in skills
|
// Skill in tasks/ dir should be in skills
|
||||||
const taskSkillEntry29 = generator29.skills.find((s) => s.canonicalId === 'task-skill');
|
const taskSkillEntry29 = generator29.skills.find((s) => s.canonicalId === 'task-skill');
|
||||||
assert(taskSkillEntry29 !== undefined, 'Skill in tasks/ dir appears in skills[]');
|
assert(taskSkillEntry29 !== undefined, 'Skill in tasks/ dir appears in skills[]');
|
||||||
|
|
||||||
// Skill in tasks/ should NOT appear in tasks[]
|
|
||||||
const inTasks29 = generator29.tasks.find((t) => t.name === 'task-skill');
|
|
||||||
assert(inTasks29 === undefined, 'Skill in tasks/ dir does NOT appear in tasks[]');
|
|
||||||
|
|
||||||
// Native agent entrypoint should be installed as a verbatim skill and also
|
// Native agent entrypoint should be installed as a verbatim skill and also
|
||||||
// remain visible to the agent manifest pipeline.
|
// remain visible to the agent manifest pipeline.
|
||||||
const nativeAgentEntry29 = generator29.skills.find((s) => s.canonicalId === 'bmad-tea');
|
const nativeAgentEntry29 = generator29.skills.find((s) => s.canonicalId === 'bmad-tea');
|
||||||
|
|
@ -1616,18 +1597,13 @@ async function runTests() {
|
||||||
const nativeAgentManifest29 = generator29.agents.find((a) => a.name === 'bmad-tea');
|
const nativeAgentManifest29 = generator29.agents.find((a) => a.name === 'bmad-tea');
|
||||||
assert(nativeAgentManifest29 !== undefined, 'Native type:agent SKILL.md dir appears in agents[] for agent metadata');
|
assert(nativeAgentManifest29 !== undefined, 'Native type:agent SKILL.md dir appears in agents[] for agent metadata');
|
||||||
|
|
||||||
// Regular workflow should be in workflows, NOT in skills
|
// Regular type:workflow should NOT appear in skills[]
|
||||||
const regularWf29 = generator29.workflows.find((w) => w.name === 'Regular Workflow');
|
|
||||||
assert(regularWf29 !== undefined, 'Regular type:workflow appears in workflows[]');
|
|
||||||
|
|
||||||
const regularInSkills29 = generator29.skills.find((s) => s.canonicalId === 'regular-wf');
|
const regularInSkills29 = generator29.skills.find((s) => s.canonicalId === 'regular-wf');
|
||||||
assert(regularInSkills29 === undefined, 'Regular type:workflow does NOT appear in skills[]');
|
assert(regularInSkills29 === undefined, 'Regular type:workflow does NOT appear in skills[]');
|
||||||
|
|
||||||
// Skill inside workflows/ should be in skills[], NOT in workflows[] (exercises findWorkflows skip at lines 311/322)
|
// Skill inside workflows/ should be in skills[]
|
||||||
const wfSkill29 = generator29.skills.find((s) => s.canonicalId === 'wf-skill');
|
const wfSkill29 = generator29.skills.find((s) => s.canonicalId === 'wf-skill');
|
||||||
assert(wfSkill29 !== undefined, 'Skill in workflows/ dir appears in skills[]');
|
assert(wfSkill29 !== undefined, 'Skill in workflows/ dir appears in skills[]');
|
||||||
const wfSkillInWorkflows29 = generator29.workflows.find((w) => w.name === 'wf-skill');
|
|
||||||
assert(wfSkillInWorkflows29 === undefined, 'Skill in workflows/ dir does NOT appear in workflows[]');
|
|
||||||
|
|
||||||
// Test scanInstalledModules recognizes skill-only modules
|
// Test scanInstalledModules recognizes skill-only modules
|
||||||
const skillOnlyModDir29 = path.join(tempFixture29, 'skill-only-mod');
|
const skillOnlyModDir29 = path.join(tempFixture29, 'skill-only-mod');
|
||||||
|
|
|
||||||
|
|
@ -1124,11 +1124,9 @@ class Installer {
|
||||||
// Pre-register manifest files
|
// Pre-register manifest files
|
||||||
const cfgDir = path.join(bmadDir, '_config');
|
const cfgDir = path.join(bmadDir, '_config');
|
||||||
this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
|
this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
|
||||||
this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv'));
|
|
||||||
this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv'));
|
this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv'));
|
||||||
this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv'));
|
|
||||||
|
|
||||||
// Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes
|
// Generate CSV manifests for agents, skills AND ALL FILES with hashes
|
||||||
// This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv
|
// This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv
|
||||||
message('Generating manifests...');
|
message('Generating manifests...');
|
||||||
const manifestGen = new ManifestGenerator();
|
const manifestGen = new ManifestGenerator();
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,12 @@ const {
|
||||||
const packageJson = require('../../../../../package.json');
|
const packageJson = require('../../../../../package.json');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates manifest files for installed workflows, agents, and tasks
|
* Generates manifest files for installed skills and agents
|
||||||
*/
|
*/
|
||||||
class ManifestGenerator {
|
class ManifestGenerator {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.workflows = [];
|
|
||||||
this.skills = [];
|
this.skills = [];
|
||||||
this.agents = [];
|
this.agents = [];
|
||||||
this.tasks = [];
|
|
||||||
this.tools = [];
|
|
||||||
this.modules = [];
|
this.modules = [];
|
||||||
this.files = [];
|
this.files = [];
|
||||||
this.selectedIdes = [];
|
this.selectedIdes = [];
|
||||||
|
|
@ -85,10 +82,6 @@ class ManifestGenerator {
|
||||||
this.modules = allModules;
|
this.modules = allModules;
|
||||||
this.updatedModules = allModules; // Include ALL modules (including custom) for scanning
|
this.updatedModules = allModules; // Include ALL modules (including custom) for scanning
|
||||||
|
|
||||||
// For CSV manifests, we need to include ALL modules that are installed
|
|
||||||
// preservedModules controls which modules stay as-is in the CSV (don't get rescanned)
|
|
||||||
// But all modules should be included in the final manifest
|
|
||||||
this.preservedModules = allModules; // Include ALL modules (including custom)
|
|
||||||
this.bmadDir = bmadDir;
|
this.bmadDir = bmadDir;
|
||||||
this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '_bmad' or 'bmad')
|
this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '_bmad' or 'bmad')
|
||||||
this.allInstalledFiles = installedFiles;
|
this.allInstalledFiles = installedFiles;
|
||||||
|
|
@ -111,35 +104,20 @@ class ManifestGenerator {
|
||||||
// Collect skills first (populates skillClaimedDirs before legacy collectors run)
|
// Collect skills first (populates skillClaimedDirs before legacy collectors run)
|
||||||
await this.collectSkills();
|
await this.collectSkills();
|
||||||
|
|
||||||
// Collect workflow data
|
|
||||||
await this.collectWorkflows(selectedModules);
|
|
||||||
|
|
||||||
// Collect agent data - use updatedModules which includes all installed modules
|
// Collect agent data - use updatedModules which includes all installed modules
|
||||||
await this.collectAgents(this.updatedModules);
|
await this.collectAgents(this.updatedModules);
|
||||||
|
|
||||||
// Collect task data
|
|
||||||
await this.collectTasks(this.updatedModules);
|
|
||||||
|
|
||||||
// Collect tool data
|
|
||||||
await this.collectTools(this.updatedModules);
|
|
||||||
|
|
||||||
// Write manifest files and collect their paths
|
// Write manifest files and collect their paths
|
||||||
const manifestFiles = [
|
const manifestFiles = [
|
||||||
await this.writeMainManifest(cfgDir),
|
await this.writeMainManifest(cfgDir),
|
||||||
await this.writeWorkflowManifest(cfgDir),
|
|
||||||
await this.writeSkillManifest(cfgDir),
|
await this.writeSkillManifest(cfgDir),
|
||||||
await this.writeAgentManifest(cfgDir),
|
await this.writeAgentManifest(cfgDir),
|
||||||
await this.writeTaskManifest(cfgDir),
|
|
||||||
await this.writeToolManifest(cfgDir),
|
|
||||||
await this.writeFilesManifest(cfgDir),
|
await this.writeFilesManifest(cfgDir),
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
skills: this.skills.length,
|
skills: this.skills.length,
|
||||||
workflows: this.workflows.length,
|
|
||||||
agents: this.agents.length,
|
agents: this.agents.length,
|
||||||
tasks: this.tasks.length,
|
|
||||||
tools: this.tools.length,
|
|
||||||
files: this.files.length,
|
files: this.files.length,
|
||||||
manifestFiles: manifestFiles,
|
manifestFiles: manifestFiles,
|
||||||
};
|
};
|
||||||
|
|
@ -289,153 +267,6 @@ class ManifestGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect all workflows from core and selected modules
|
|
||||||
* Scans the INSTALLED bmad directory, not the source
|
|
||||||
*/
|
|
||||||
async collectWorkflows(selectedModules) {
|
|
||||||
this.workflows = [];
|
|
||||||
|
|
||||||
// Use updatedModules which already includes deduplicated 'core' + selectedModules
|
|
||||||
for (const moduleName of this.updatedModules) {
|
|
||||||
const modulePath = path.join(this.bmadDir, moduleName);
|
|
||||||
|
|
||||||
if (await fs.pathExists(modulePath)) {
|
|
||||||
const moduleWorkflows = await this.getWorkflowsFromPath(modulePath, moduleName);
|
|
||||||
this.workflows.push(...moduleWorkflows);
|
|
||||||
|
|
||||||
// Also scan tasks/ for type:skill entries (skills can live anywhere)
|
|
||||||
const tasksSkills = await this.getWorkflowsFromPath(modulePath, moduleName, 'tasks');
|
|
||||||
this.workflows.push(...tasksSkills);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively find and parse workflow.md files
|
|
||||||
*/
|
|
||||||
async getWorkflowsFromPath(basePath, moduleName, subDir = 'workflows') {
|
|
||||||
const workflows = [];
|
|
||||||
const workflowsPath = path.join(basePath, subDir);
|
|
||||||
const debug = process.env.BMAD_DEBUG_MANIFEST === 'true';
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log(`[DEBUG] Scanning workflows in: ${workflowsPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(workflowsPath))) {
|
|
||||||
if (debug) {
|
|
||||||
console.log(`[DEBUG] Workflows path does not exist: ${workflowsPath}`);
|
|
||||||
}
|
|
||||||
return workflows;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively find workflow.md files
|
|
||||||
const findWorkflows = async (dir, relativePath = '') => {
|
|
||||||
// Skip directories already claimed as skills
|
|
||||||
if (this.skillClaimedDirs && this.skillClaimedDirs.has(dir)) return;
|
|
||||||
|
|
||||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
||||||
// Load skill manifest for this directory (if present)
|
|
||||||
const skillManifest = await this.loadSkillManifest(dir);
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(dir, entry.name);
|
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
// Skip directories claimed by collectSkills
|
|
||||||
if (this.skillClaimedDirs && this.skillClaimedDirs.has(fullPath)) continue;
|
|
||||||
// Recurse into subdirectories
|
|
||||||
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
||||||
await findWorkflows(fullPath, newRelativePath);
|
|
||||||
} else if (entry.name === 'workflow.md' || (entry.name.startsWith('workflow-') && entry.name.endsWith('.md'))) {
|
|
||||||
// Parse workflow file (both YAML and MD formats)
|
|
||||||
if (debug) {
|
|
||||||
console.log(`[DEBUG] Found workflow file: ${fullPath}`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Read and normalize line endings (fix Windows CRLF issues)
|
|
||||||
const rawContent = await fs.readFile(fullPath, 'utf8');
|
|
||||||
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
||||||
|
|
||||||
// Parse MD workflow with YAML frontmatter
|
|
||||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
||||||
if (!frontmatterMatch) {
|
|
||||||
if (debug) {
|
|
||||||
console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`);
|
|
||||||
}
|
|
||||||
continue; // Skip MD files without frontmatter
|
|
||||||
}
|
|
||||||
const workflow = yaml.parse(frontmatterMatch[1]);
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log(`[DEBUG] Parsed: name="${workflow.name}", description=${workflow.description ? 'OK' : 'MISSING'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip template workflows (those with placeholder values)
|
|
||||||
if (workflow.name && workflow.name.includes('{') && workflow.name.includes('}')) {
|
|
||||||
if (debug) {
|
|
||||||
console.log(`[DEBUG] Skipped (template placeholder): ${workflow.name}`);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip workflows marked as non-standalone (reference/example workflows)
|
|
||||||
if (workflow.standalone === false) {
|
|
||||||
if (debug) {
|
|
||||||
console.log(`[DEBUG] Skipped (standalone=false): ${workflow.name}`);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workflow.name && workflow.description) {
|
|
||||||
// Build relative path for installation
|
|
||||||
const installPath =
|
|
||||||
moduleName === 'core'
|
|
||||||
? `${this.bmadFolderName}/core/${subDir}/${relativePath}/${entry.name}`
|
|
||||||
: `${this.bmadFolderName}/${moduleName}/${subDir}/${relativePath}/${entry.name}`;
|
|
||||||
|
|
||||||
// Workflows with standalone: false are filtered out above
|
|
||||||
workflows.push({
|
|
||||||
name: workflow.name,
|
|
||||||
description: this.cleanForCSV(workflow.description),
|
|
||||||
module: moduleName,
|
|
||||||
path: installPath,
|
|
||||||
canonicalId: this.getCanonicalId(skillManifest, entry.name),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add to files list
|
|
||||||
this.files.push({
|
|
||||||
type: 'workflow',
|
|
||||||
name: workflow.name,
|
|
||||||
module: moduleName,
|
|
||||||
path: installPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log(`[DEBUG] ✓ Added workflow: ${workflow.name} (${moduleName})`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (debug) {
|
|
||||||
console.log(`[DEBUG] Skipped (missing name or description): ${fullPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
await prompts.log.warn(`Failed to parse workflow at ${fullPath}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await findWorkflows(workflowsPath);
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log(`[DEBUG] Total workflows found in ${moduleName}: ${workflows.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return workflows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect all agents from core and selected modules
|
* Collect all agents from core and selected modules
|
||||||
* Scans the INSTALLED bmad directory, not the source
|
* Scans the INSTALLED bmad directory, not the source
|
||||||
|
|
@ -589,212 +420,6 @@ class ManifestGenerator {
|
||||||
return agents;
|
return agents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect all tasks from core and selected modules
|
|
||||||
* Scans the INSTALLED bmad directory, not the source
|
|
||||||
*/
|
|
||||||
async collectTasks(selectedModules) {
|
|
||||||
this.tasks = [];
|
|
||||||
|
|
||||||
// Use updatedModules which already includes deduplicated 'core' + selectedModules
|
|
||||||
for (const moduleName of this.updatedModules) {
|
|
||||||
const tasksPath = path.join(this.bmadDir, moduleName, 'tasks');
|
|
||||||
|
|
||||||
if (await fs.pathExists(tasksPath)) {
|
|
||||||
const moduleTasks = await this.getTasksFromDir(tasksPath, moduleName);
|
|
||||||
this.tasks.push(...moduleTasks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get tasks from a directory
|
|
||||||
*/
|
|
||||||
async getTasksFromDir(dirPath, moduleName) {
|
|
||||||
// Skip directories claimed by collectSkills
|
|
||||||
if (this.skillClaimedDirs && this.skillClaimedDirs.has(dirPath)) return [];
|
|
||||||
const tasks = [];
|
|
||||||
const files = await fs.readdir(dirPath);
|
|
||||||
// Load skill manifest for this directory (if present)
|
|
||||||
const skillManifest = await this.loadSkillManifest(dirPath);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
// Check for both .xml and .md files
|
|
||||||
if (file.endsWith('.xml') || file.endsWith('.md')) {
|
|
||||||
const filePath = path.join(dirPath, file);
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
|
|
||||||
// Skip internal/engine files (not user-facing tasks)
|
|
||||||
if (content.includes('internal="true"')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = file.replace(/\.(xml|md)$/, '');
|
|
||||||
let displayName = name;
|
|
||||||
let description = '';
|
|
||||||
let standalone = false;
|
|
||||||
|
|
||||||
if (file.endsWith('.md')) {
|
|
||||||
// Parse YAML frontmatter for .md tasks
|
|
||||||
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
||||||
if (frontmatterMatch) {
|
|
||||||
try {
|
|
||||||
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
|
||||||
name = frontmatter.name || name;
|
|
||||||
displayName = frontmatter.displayName || frontmatter.name || name;
|
|
||||||
description = this.cleanForCSV(frontmatter.description || '');
|
|
||||||
// Tasks are standalone by default unless explicitly false (internal=true is already filtered above)
|
|
||||||
standalone = frontmatter.standalone !== false && frontmatter.standalone !== 'false';
|
|
||||||
} catch {
|
|
||||||
// If YAML parsing fails, use defaults
|
|
||||||
standalone = true; // Default to standalone
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
standalone = true; // No frontmatter means standalone
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For .xml tasks, extract from tag attributes
|
|
||||||
const nameMatch = content.match(/name="([^"]+)"/);
|
|
||||||
displayName = nameMatch ? nameMatch[1] : name;
|
|
||||||
|
|
||||||
const descMatch = content.match(/description="([^"]+)"/);
|
|
||||||
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
|
|
||||||
description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '');
|
|
||||||
|
|
||||||
const standaloneFalseMatch = content.match(/<task[^>]+standalone="false"/);
|
|
||||||
standalone = !standaloneFalseMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build relative path for installation
|
|
||||||
const installPath =
|
|
||||||
moduleName === 'core' ? `${this.bmadFolderName}/core/tasks/${file}` : `${this.bmadFolderName}/${moduleName}/tasks/${file}`;
|
|
||||||
|
|
||||||
tasks.push({
|
|
||||||
name: name,
|
|
||||||
displayName: displayName,
|
|
||||||
description: description,
|
|
||||||
module: moduleName,
|
|
||||||
path: installPath,
|
|
||||||
standalone: standalone,
|
|
||||||
canonicalId: this.getCanonicalId(skillManifest, file),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add to files list
|
|
||||||
this.files.push({
|
|
||||||
type: 'task',
|
|
||||||
name: name,
|
|
||||||
module: moduleName,
|
|
||||||
path: installPath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect all tools from core and selected modules
|
|
||||||
* Scans the INSTALLED bmad directory, not the source
|
|
||||||
*/
|
|
||||||
async collectTools(selectedModules) {
|
|
||||||
this.tools = [];
|
|
||||||
|
|
||||||
// Use updatedModules which already includes deduplicated 'core' + selectedModules
|
|
||||||
for (const moduleName of this.updatedModules) {
|
|
||||||
const toolsPath = path.join(this.bmadDir, moduleName, 'tools');
|
|
||||||
|
|
||||||
if (await fs.pathExists(toolsPath)) {
|
|
||||||
const moduleTools = await this.getToolsFromDir(toolsPath, moduleName);
|
|
||||||
this.tools.push(...moduleTools);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get tools from a directory
|
|
||||||
*/
|
|
||||||
async getToolsFromDir(dirPath, moduleName) {
|
|
||||||
// Skip directories claimed by collectSkills
|
|
||||||
if (this.skillClaimedDirs && this.skillClaimedDirs.has(dirPath)) return [];
|
|
||||||
const tools = [];
|
|
||||||
const files = await fs.readdir(dirPath);
|
|
||||||
// Load skill manifest for this directory (if present)
|
|
||||||
const skillManifest = await this.loadSkillManifest(dirPath);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
// Check for both .xml and .md files
|
|
||||||
if (file.endsWith('.xml') || file.endsWith('.md')) {
|
|
||||||
const filePath = path.join(dirPath, file);
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
|
|
||||||
// Skip internal tools (same as tasks)
|
|
||||||
if (content.includes('internal="true"')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = file.replace(/\.(xml|md)$/, '');
|
|
||||||
let displayName = name;
|
|
||||||
let description = '';
|
|
||||||
let standalone = false;
|
|
||||||
|
|
||||||
if (file.endsWith('.md')) {
|
|
||||||
// Parse YAML frontmatter for .md tools
|
|
||||||
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
||||||
if (frontmatterMatch) {
|
|
||||||
try {
|
|
||||||
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
|
||||||
name = frontmatter.name || name;
|
|
||||||
displayName = frontmatter.displayName || frontmatter.name || name;
|
|
||||||
description = this.cleanForCSV(frontmatter.description || '');
|
|
||||||
// Tools are standalone by default unless explicitly false (internal=true is already filtered above)
|
|
||||||
standalone = frontmatter.standalone !== false && frontmatter.standalone !== 'false';
|
|
||||||
} catch {
|
|
||||||
// If YAML parsing fails, use defaults
|
|
||||||
standalone = true; // Default to standalone
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
standalone = true; // No frontmatter means standalone
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For .xml tools, extract from tag attributes
|
|
||||||
const nameMatch = content.match(/name="([^"]+)"/);
|
|
||||||
displayName = nameMatch ? nameMatch[1] : name;
|
|
||||||
|
|
||||||
const descMatch = content.match(/description="([^"]+)"/);
|
|
||||||
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
|
|
||||||
description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '');
|
|
||||||
|
|
||||||
const standaloneFalseMatch = content.match(/<tool[^>]+standalone="false"/);
|
|
||||||
standalone = !standaloneFalseMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build relative path for installation
|
|
||||||
const installPath =
|
|
||||||
moduleName === 'core' ? `${this.bmadFolderName}/core/tools/${file}` : `${this.bmadFolderName}/${moduleName}/tools/${file}`;
|
|
||||||
|
|
||||||
tools.push({
|
|
||||||
name: name,
|
|
||||||
displayName: displayName,
|
|
||||||
description: description,
|
|
||||||
module: moduleName,
|
|
||||||
path: installPath,
|
|
||||||
standalone: standalone,
|
|
||||||
canonicalId: this.getCanonicalId(skillManifest, file),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add to files list
|
|
||||||
this.files.push({
|
|
||||||
type: 'tool',
|
|
||||||
name: name,
|
|
||||||
module: moduleName,
|
|
||||||
path: installPath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tools;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write main manifest as YAML with installation info only
|
* Write main manifest as YAML with installation info only
|
||||||
* Fetches fresh version info for all modules
|
* Fetches fresh version info for all modules
|
||||||
|
|
@ -880,131 +505,6 @@ class ManifestGenerator {
|
||||||
return manifestPath;
|
return manifestPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Read existing CSV and preserve rows for modules NOT being updated
|
|
||||||
* @param {string} csvPath - Path to existing CSV file
|
|
||||||
* @param {number} moduleColumnIndex - Which column contains the module name (0-indexed)
|
|
||||||
* @param {Array<string>} expectedColumns - Expected column names in order
|
|
||||||
* @param {Object} defaultValues - Default values for missing columns
|
|
||||||
* @returns {Array} Preserved CSV rows (without header), upgraded to match expected columns
|
|
||||||
*/
|
|
||||||
async getPreservedCsvRows(csvPath, moduleColumnIndex, expectedColumns, defaultValues = {}) {
|
|
||||||
if (!(await fs.pathExists(csvPath)) || this.preservedModules.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(csvPath, 'utf8');
|
|
||||||
const lines = content.trim().split('\n');
|
|
||||||
|
|
||||||
if (lines.length < 2) {
|
|
||||||
return []; // No data rows
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse header to understand old schema
|
|
||||||
const header = lines[0];
|
|
||||||
const headerColumns = header.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || [];
|
|
||||||
const oldColumns = headerColumns.map((c) => c.replaceAll(/^"|"$/g, ''));
|
|
||||||
|
|
||||||
// Skip header row for data
|
|
||||||
const dataRows = lines.slice(1);
|
|
||||||
const preservedRows = [];
|
|
||||||
|
|
||||||
for (const row of dataRows) {
|
|
||||||
// Simple CSV parsing (handles quoted values)
|
|
||||||
const columns = row.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || [];
|
|
||||||
const cleanColumns = columns.map((c) => c.replaceAll(/^"|"$/g, ''));
|
|
||||||
|
|
||||||
const moduleValue = cleanColumns[moduleColumnIndex];
|
|
||||||
|
|
||||||
// Keep this row if it belongs to a preserved module
|
|
||||||
if (this.preservedModules.includes(moduleValue)) {
|
|
||||||
// Upgrade row to match expected schema
|
|
||||||
const upgradedRow = this.upgradeRowToSchema(cleanColumns, oldColumns, expectedColumns, defaultValues);
|
|
||||||
preservedRows.push(upgradedRow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return preservedRows;
|
|
||||||
} catch (error) {
|
|
||||||
await prompts.log.warn(`Failed to read existing CSV ${csvPath}: ${error.message}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upgrade a CSV row from old schema to new schema
|
|
||||||
* @param {Array<string>} rowValues - Values from old row
|
|
||||||
* @param {Array<string>} oldColumns - Old column names
|
|
||||||
* @param {Array<string>} newColumns - New column names
|
|
||||||
* @param {Object} defaultValues - Default values for missing columns
|
|
||||||
* @returns {string} Upgraded CSV row
|
|
||||||
*/
|
|
||||||
upgradeRowToSchema(rowValues, oldColumns, newColumns, defaultValues) {
|
|
||||||
const upgradedValues = [];
|
|
||||||
|
|
||||||
for (const newCol of newColumns) {
|
|
||||||
const oldIndex = oldColumns.indexOf(newCol);
|
|
||||||
|
|
||||||
if (oldIndex !== -1 && oldIndex < rowValues.length) {
|
|
||||||
// Column exists in old schema, use its value
|
|
||||||
upgradedValues.push(rowValues[oldIndex]);
|
|
||||||
} else if (defaultValues[newCol] === undefined) {
|
|
||||||
// Column missing, no default provided
|
|
||||||
upgradedValues.push('');
|
|
||||||
} else {
|
|
||||||
// Column missing, use default value
|
|
||||||
upgradedValues.push(defaultValues[newCol]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Properly quote values and join
|
|
||||||
return upgradedValues.map((v) => `"${v}"`).join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write workflow manifest CSV
|
|
||||||
* @returns {string} Path to the manifest file
|
|
||||||
*/
|
|
||||||
async writeWorkflowManifest(cfgDir) {
|
|
||||||
const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
|
|
||||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
|
||||||
|
|
||||||
// Create CSV header - standalone column removed, canonicalId added as optional column
|
|
||||||
let csv = 'name,description,module,path,canonicalId\n';
|
|
||||||
|
|
||||||
// Build workflows map from discovered workflows only
|
|
||||||
// Old entries are NOT preserved - the manifest reflects what actually exists on disk
|
|
||||||
const allWorkflows = new Map();
|
|
||||||
|
|
||||||
// Only add workflows that were actually discovered in this scan
|
|
||||||
for (const workflow of this.workflows) {
|
|
||||||
const key = `${workflow.module}:${workflow.name}`;
|
|
||||||
allWorkflows.set(key, {
|
|
||||||
name: workflow.name,
|
|
||||||
description: workflow.description,
|
|
||||||
module: workflow.module,
|
|
||||||
path: workflow.path,
|
|
||||||
canonicalId: workflow.canonicalId || '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write all workflows
|
|
||||||
for (const [, value] of allWorkflows) {
|
|
||||||
const row = [
|
|
||||||
escapeCsv(value.name),
|
|
||||||
escapeCsv(value.description),
|
|
||||||
escapeCsv(value.module),
|
|
||||||
escapeCsv(value.path),
|
|
||||||
escapeCsv(value.canonicalId),
|
|
||||||
].join(',');
|
|
||||||
csv += row + '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(csvPath, csv);
|
|
||||||
return csvPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write skill manifest CSV
|
* Write skill manifest CSV
|
||||||
* @returns {string} Path to the manifest file
|
* @returns {string} Path to the manifest file
|
||||||
|
|
@ -1105,134 +605,6 @@ class ManifestGenerator {
|
||||||
return csvPath;
|
return csvPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Write task manifest CSV
|
|
||||||
* @returns {string} Path to the manifest file
|
|
||||||
*/
|
|
||||||
async writeTaskManifest(cfgDir) {
|
|
||||||
const csvPath = path.join(cfgDir, 'task-manifest.csv');
|
|
||||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
|
||||||
|
|
||||||
// Read existing manifest to preserve entries
|
|
||||||
const existingEntries = new Map();
|
|
||||||
if (await fs.pathExists(csvPath)) {
|
|
||||||
const content = await fs.readFile(csvPath, 'utf8');
|
|
||||||
const records = csv.parse(content, {
|
|
||||||
columns: true,
|
|
||||||
skip_empty_lines: true,
|
|
||||||
});
|
|
||||||
for (const record of records) {
|
|
||||||
existingEntries.set(`${record.module}:${record.name}`, record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create CSV header with standalone and canonicalId columns
|
|
||||||
let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n';
|
|
||||||
|
|
||||||
// Combine existing and new tasks
|
|
||||||
const allTasks = new Map();
|
|
||||||
|
|
||||||
// Add existing entries
|
|
||||||
for (const [key, value] of existingEntries) {
|
|
||||||
allTasks.set(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add/update new tasks
|
|
||||||
for (const task of this.tasks) {
|
|
||||||
const key = `${task.module}:${task.name}`;
|
|
||||||
allTasks.set(key, {
|
|
||||||
name: task.name,
|
|
||||||
displayName: task.displayName,
|
|
||||||
description: task.description,
|
|
||||||
module: task.module,
|
|
||||||
path: task.path,
|
|
||||||
standalone: task.standalone,
|
|
||||||
canonicalId: task.canonicalId || '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write all tasks
|
|
||||||
for (const [, record] of allTasks) {
|
|
||||||
const row = [
|
|
||||||
escapeCsv(record.name),
|
|
||||||
escapeCsv(record.displayName),
|
|
||||||
escapeCsv(record.description),
|
|
||||||
escapeCsv(record.module),
|
|
||||||
escapeCsv(record.path),
|
|
||||||
escapeCsv(record.standalone),
|
|
||||||
escapeCsv(record.canonicalId),
|
|
||||||
].join(',');
|
|
||||||
csvContent += row + '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(csvPath, csvContent);
|
|
||||||
return csvPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write tool manifest CSV
|
|
||||||
* @returns {string} Path to the manifest file
|
|
||||||
*/
|
|
||||||
async writeToolManifest(cfgDir) {
|
|
||||||
const csvPath = path.join(cfgDir, 'tool-manifest.csv');
|
|
||||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
|
||||||
|
|
||||||
// Read existing manifest to preserve entries
|
|
||||||
const existingEntries = new Map();
|
|
||||||
if (await fs.pathExists(csvPath)) {
|
|
||||||
const content = await fs.readFile(csvPath, 'utf8');
|
|
||||||
const records = csv.parse(content, {
|
|
||||||
columns: true,
|
|
||||||
skip_empty_lines: true,
|
|
||||||
});
|
|
||||||
for (const record of records) {
|
|
||||||
existingEntries.set(`${record.module}:${record.name}`, record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create CSV header with standalone and canonicalId columns
|
|
||||||
let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n';
|
|
||||||
|
|
||||||
// Combine existing and new tools
|
|
||||||
const allTools = new Map();
|
|
||||||
|
|
||||||
// Add existing entries
|
|
||||||
for (const [key, value] of existingEntries) {
|
|
||||||
allTools.set(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add/update new tools
|
|
||||||
for (const tool of this.tools) {
|
|
||||||
const key = `${tool.module}:${tool.name}`;
|
|
||||||
allTools.set(key, {
|
|
||||||
name: tool.name,
|
|
||||||
displayName: tool.displayName,
|
|
||||||
description: tool.description,
|
|
||||||
module: tool.module,
|
|
||||||
path: tool.path,
|
|
||||||
standalone: tool.standalone,
|
|
||||||
canonicalId: tool.canonicalId || '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write all tools
|
|
||||||
for (const [, record] of allTools) {
|
|
||||||
const row = [
|
|
||||||
escapeCsv(record.name),
|
|
||||||
escapeCsv(record.displayName),
|
|
||||||
escapeCsv(record.description),
|
|
||||||
escapeCsv(record.module),
|
|
||||||
escapeCsv(record.path),
|
|
||||||
escapeCsv(record.standalone),
|
|
||||||
escapeCsv(record.canonicalId),
|
|
||||||
].join(',');
|
|
||||||
csvContent += row + '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(csvPath, csvContent);
|
|
||||||
return csvPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write files manifest CSV
|
* Write files manifest CSV
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue