fix: restore installer edge-case handling and path normalization

This commit is contained in:
Dicky Moore 2026-02-08 20:04:58 +00:00
parent c108ae4314
commit dde139a560
3 changed files with 141 additions and 3 deletions

View File

@ -20,6 +20,7 @@ const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder');
const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator');
const { WorkflowCommandGenerator } = require('../tools/cli/installers/lib/ide/shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('../tools/cli/installers/lib/ide/shared/task-tool-command-generator');
const { ConfigDrivenIdeSetup } = require('../tools/cli/installers/lib/ide/_config-driven');
const { IdeManager } = require('../tools/cli/installers/lib/ide/manager');
const { CodexSetup } = require('../tools/cli/installers/lib/ide/codex');
const { ModuleManager } = require('../tools/cli/installers/lib/modules/manager');
@ -744,6 +745,117 @@ internal: true
console.log('');
// ============================================================
// Test 19: Empty Target Artifact Filter Guard
// ============================================================
console.log(`${colors.yellow}Test Suite 19: Empty Artifact Target Guard${colors.reset}\n`);
try {
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-empty-target-'));
const projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.ensureDir(projectDir);
await fs.ensureDir(bmadDir);
const setup = new ConfigDrivenIdeSetup('test', {
name: 'Test IDE',
preferred: false,
installer: { target_dir: '.test', template_type: 'default' },
});
const result = await setup.installToTarget(
projectDir,
bmadDir,
{ target_dir: '.test', template_type: 'default', artifact_types: [] },
{ silent: true },
);
assert(
result.success &&
result.results.agents === 0 &&
result.results.workflows === 0 &&
result.results.tasks === 0 &&
result.results.tools === 0,
'Installer short-circuits explicit empty artifact target',
);
assert(!(await fs.pathExists(path.join(projectDir, '.test'))), 'Installer does not create output directory for empty artifact target');
await fs.remove(tmpRoot);
} catch (error) {
assert(false, 'Empty artifact target guard runs', error.message);
}
console.log('');
// ============================================================
// Test 20: Split Template Extension Override Guard
// ============================================================
console.log(`${colors.yellow}Test Suite 20: Split Template Extension Guard${colors.reset}\n`);
try {
const setup = new ConfigDrivenIdeSetup('test', {
name: 'Test IDE',
preferred: false,
installer: { target_dir: '.test', template_type: 'default' },
});
setup.loadSplitTemplates = async () => 'template-content';
const loaded = await setup.loadTemplateWithMetadata('default', 'workflow', {
header_template: 'header.md',
extension: 'toml',
});
assert(loaded.extension === '.toml', 'Split template loader preserves configured extension override');
} catch (error) {
assert(false, 'Split template extension guard runs', error.message);
}
console.log('');
// ============================================================
// Test 21: Workflow Path Normalization Guard
// ============================================================
console.log(`${colors.yellow}Test Suite 21: Workflow Path Normalization Guard${colors.reset}\n`);
try {
const generator = new WorkflowCommandGenerator();
generator.loadWorkflowManifest = async () => [
{
name: 'create-story',
description: 'Create Story',
module: 'bmm',
path: String.raw`C:\repo\src\bmm\workflows\4-implementation\create-story\workflow.md`,
},
{
name: 'validate-workflow',
description: 'Validate Workflow',
module: 'core',
path: String.raw`C:\repo\_bmad\core\workflows\validate-workflow\workflow.md`,
},
];
generator.generateCommandContent = async () => 'content';
const { artifacts } = await generator.collectWorkflowArtifacts('/tmp');
const createStory = artifacts.find((artifact) => artifact.name === 'create-story');
const validateWorkflow = artifacts.find((artifact) => artifact.name === 'validate-workflow');
assert(
createStory?.workflowPath === 'bmm/workflows/4-implementation/create-story/workflow.md',
'Workflow artifact path normalizes Windows src path to module-relative path',
createStory?.workflowPath,
);
assert(
validateWorkflow?.workflowPath === 'core/workflows/validate-workflow/workflow.md',
'Workflow artifact path strips _bmad prefix after separator normalization',
validateWorkflow?.workflowPath,
);
} catch (error) {
assert(false, 'Workflow path normalization guard runs', error.message);
}
console.log('');
// ============================================================
// Summary
// ============================================================

View File

@ -66,6 +66,12 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
*/
async installToTarget(projectDir, bmadDir, config, options) {
const { target_dir, template_type, artifact_types } = config;
// Skip explicitly empty targets to avoid creating empty command directories.
if (Array.isArray(artifact_types) && artifact_types.length === 0) {
return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0 } };
}
const targetPath = path.join(projectDir, target_dir);
await this.ensureDir(targetPath);
@ -250,12 +256,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
// Check for separate header/body templates
if (header_template || body_template) {
const template = await this.loadSplitTemplates(templateType, artifactType, header_template, body_template);
return { template, extension: '.md' };
return { template, extension: this.normalizeExtension(config.extension) };
}
// Load combined template with extension detection
const templateBaseName = artifactType ? `${templateType}-${artifactType}` : templateType;
for (const extension of supportedExtensions) {
const templateName = `${templateType}-${artifactType}${extension}`;
const templateName = `${templateBaseName}${extension}`;
const templatePath = path.join(__dirname, 'templates', 'combined', templateName);
if (await fs.pathExists(templatePath)) {
return {
@ -329,6 +336,19 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
return `${header}\n${body}`;
}
normalizeExtension(extension) {
if (!extension) {
return '.md';
}
const trimmed = String(extension).trim();
if (trimmed === '') {
return '.md';
}
return trimmed.startsWith('.') ? trimmed : `.${trimmed}`;
}
/**
* Get default minimal template
* @param {string} artifactType - Artifact type

View File

@ -68,7 +68,8 @@ class WorkflowCommandGenerator {
for (const workflow of allWorkflows) {
const commandContent = await this.generateCommandContent(workflow, bmadDir);
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.md)
let workflowRelPath = workflow.path;
let workflowRelPath = workflow.path || '';
workflowRelPath = workflowRelPath.replaceAll('\\', '/');
// Remove _bmad/ prefix if present to get relative path from project root
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
if (workflowRelPath.includes('_bmad/')) {
@ -76,6 +77,11 @@ class WorkflowCommandGenerator {
if (parts.length > 1) {
workflowRelPath = parts.slice(1).join('/');
}
} else if (workflowRelPath.includes('/src/')) {
const match = workflowRelPath.match(/\/src\/([^/]+)\/(.+)/);
if (match) {
workflowRelPath = `${match[1]}/${match[2]}`;
}
}
artifacts.push({
type: 'workflow-command',