fix: restore installer edge-case handling and path normalization
This commit is contained in:
parent
c108ae4314
commit
dde139a560
|
|
@ -20,6 +20,7 @@ const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder');
|
||||||
const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator');
|
const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator');
|
||||||
const { WorkflowCommandGenerator } = require('../tools/cli/installers/lib/ide/shared/workflow-command-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 { 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 { IdeManager } = require('../tools/cli/installers/lib/ide/manager');
|
||||||
const { CodexSetup } = require('../tools/cli/installers/lib/ide/codex');
|
const { CodexSetup } = require('../tools/cli/installers/lib/ide/codex');
|
||||||
const { ModuleManager } = require('../tools/cli/installers/lib/modules/manager');
|
const { ModuleManager } = require('../tools/cli/installers/lib/modules/manager');
|
||||||
|
|
@ -744,6 +745,117 @@ internal: true
|
||||||
|
|
||||||
console.log('');
|
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
|
// Summary
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,12 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
*/
|
*/
|
||||||
async installToTarget(projectDir, bmadDir, config, options) {
|
async installToTarget(projectDir, bmadDir, config, options) {
|
||||||
const { target_dir, template_type, artifact_types } = config;
|
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);
|
const targetPath = path.join(projectDir, target_dir);
|
||||||
await this.ensureDir(targetPath);
|
await this.ensureDir(targetPath);
|
||||||
|
|
||||||
|
|
@ -250,12 +256,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
// Check for separate header/body templates
|
// Check for separate header/body templates
|
||||||
if (header_template || body_template) {
|
if (header_template || body_template) {
|
||||||
const template = await this.loadSplitTemplates(templateType, artifactType, 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
|
// Load combined template with extension detection
|
||||||
|
const templateBaseName = artifactType ? `${templateType}-${artifactType}` : templateType;
|
||||||
for (const extension of supportedExtensions) {
|
for (const extension of supportedExtensions) {
|
||||||
const templateName = `${templateType}-${artifactType}${extension}`;
|
const templateName = `${templateBaseName}${extension}`;
|
||||||
const templatePath = path.join(__dirname, 'templates', 'combined', templateName);
|
const templatePath = path.join(__dirname, 'templates', 'combined', templateName);
|
||||||
if (await fs.pathExists(templatePath)) {
|
if (await fs.pathExists(templatePath)) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -329,6 +336,19 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
return `${header}\n${body}`;
|
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
|
* Get default minimal template
|
||||||
* @param {string} artifactType - Artifact type
|
* @param {string} artifactType - Artifact type
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,8 @@ class WorkflowCommandGenerator {
|
||||||
for (const workflow of allWorkflows) {
|
for (const workflow of allWorkflows) {
|
||||||
const commandContent = await this.generateCommandContent(workflow, bmadDir);
|
const commandContent = await this.generateCommandContent(workflow, bmadDir);
|
||||||
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.md)
|
// 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
|
// Remove _bmad/ prefix if present to get relative path from project root
|
||||||
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
|
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
|
||||||
if (workflowRelPath.includes('_bmad/')) {
|
if (workflowRelPath.includes('_bmad/')) {
|
||||||
|
|
@ -76,6 +77,11 @@ class WorkflowCommandGenerator {
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
workflowRelPath = parts.slice(1).join('/');
|
workflowRelPath = parts.slice(1).join('/');
|
||||||
}
|
}
|
||||||
|
} else if (workflowRelPath.includes('/src/')) {
|
||||||
|
const match = workflowRelPath.match(/\/src\/([^/]+)\/(.+)/);
|
||||||
|
if (match) {
|
||||||
|
workflowRelPath = `${match[1]}/${match[2]}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
artifacts.push({
|
artifacts.push({
|
||||||
type: 'workflow-command',
|
type: 'workflow-command',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue