BMAD-METHOD/tools/cli/installers/lib/ide/shared/path-utils.js

296 lines
9.3 KiB
JavaScript

/**
* Path transformation utilities for IDE installer standardization
*
* Provides utilities to convert hierarchical paths to flat naming conventions.
*
* DASH-BASED NAMING (new standard):
* - Agents: bmad-agent-module-name.md (with bmad-agent- prefix)
* - Workflows/Tasks/Tools: bmad-module-name.md
*
* Example outputs:
* - cis/agents/storymaster.md → bmad-agent-cis-storymaster.md
* - bmm/workflows/plan-project.md → bmad-bmm-plan-project.md
* - bmm/tasks/create-story.md → bmad-bmm-create-story.md
* - core/agents/brainstorming.md → bmad-agent-brainstorming.md (core agents skip module name)
*/
// Type segments - agents are included in naming, others are filtered out
const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
const AGENT_SEGMENT = 'agents';
/**
* Convert hierarchical path to flat dash-separated name (NEW STANDARD)
* Converts: 'bmm', 'agents', 'pm' → 'bmad-agent-bmm-pm.md'
* Converts: 'bmm', 'workflows', 'correct-course' → 'bmad-bmm-correct-course.md'
* Converts: 'core', 'agents', 'brainstorming' → 'bmad-agent-brainstorming.md' (core agents skip module name)
*
* @param {string} module - Module name (e.g., 'bmm', 'core')
* @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools')
* @param {string} name - Artifact name (e.g., 'pm', 'brainstorming')
* @returns {string} Flat filename like 'bmad-agent-bmm-pm.md' or 'bmad-bmm-correct-course.md'
*/
function toDashName(module, type, name) {
const isAgent = type === AGENT_SEGMENT;
// For core module, skip the module name: use 'bmad-agent-name.md' instead of 'bmad-agent-core-name.md'
if (module === 'core') {
return isAgent ? `bmad-agent-${name}.md` : `bmad-${name}.md`;
}
// Module artifacts: bmad-module-name.md or bmad-agent-module-name.md
// eslint-disable-next-line unicorn/prefer-string-replace-all -- regex replace is intentional here
const dashName = name.replace(/\//g, '-'); // Flatten nested paths
return isAgent ? `bmad-agent-${module}-${dashName}.md` : `bmad-${module}-${dashName}.md`;
}
/**
* Convert relative path to flat dash-separated name
* Converts: 'bmm/agents/pm.md' → 'bmad-agent-bmm-pm.md'
* Converts: 'bmm/agents/tech-writer/tech-writer.md' → 'bmad-agent-bmm-tech-writer.md' (uses folder name)
* Converts: 'bmm/workflows/correct-course.md' → 'bmad-bmm-correct-course.md'
* Converts: 'core/agents/brainstorming.md' → 'bmad-agent-brainstorming.md' (core agents skip module name)
*
* @param {string} relativePath - Path like 'bmm/agents/pm.md'
* @returns {string} Flat filename like 'bmad-agent-bmm-pm.md' or 'bmad-brainstorming.md'
*/
function toDashPath(relativePath) {
if (!relativePath || typeof relativePath !== 'string') {
// Return a safe default for invalid input
return 'bmad-unknown.md';
}
// Strip common file extensions to avoid double extensions in generated filenames
// e.g., 'create-story.xml' → 'create-story', 'workflow.yaml' → 'workflow'
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
const parts = withoutExt.split(/[/\\]/);
const module = parts[0];
const type = parts[1];
let name;
// For agents, if nested in a folder (more than 3 parts), use the folder name only
// e.g., 'bmm/agents/tech-writer/tech-writer' → 'tech-writer' (not 'tech-writer-tech-writer')
if (type === 'agents' && parts.length > 3) {
// Use the folder name (parts[2]) as the name, ignore the file name
name = parts[2];
} else {
// For non-nested or non-agents, join all parts after type
name = parts.slice(2).join('-');
}
return toDashName(module, type, name);
}
/**
* Create custom agent dash name
* Creates: 'bmad-custom-agent-fred-commit-poet.md'
*
* @param {string} agentName - Custom agent name
* @returns {string} Flat filename like 'bmad-custom-agent-fred-commit-poet.md'
*/
function customAgentDashName(agentName) {
return `bmad-custom-agent-${agentName}.md`;
}
/**
* Check if a filename uses dash format
* @param {string} filename - Filename to check
* @returns {boolean} True if filename uses dash format
*/
function isDashFormat(filename) {
return filename.startsWith('bmad-') && filename.includes('-');
}
/**
* Extract parts from a dash-formatted filename
* Parses: 'bmad-agent-bmm-pm.md' → { prefix: 'bmad', module: 'bmm', type: 'agents', name: 'pm' }
* Parses: 'bmad-bmm-correct-course.md' → { prefix: 'bmad', module: 'bmm', type: 'workflows', name: 'correct-course' }
* Parses: 'bmad-agent-brainstorming.md' → { prefix: 'bmad', module: 'core', type: 'agents', name: 'brainstorming' } (core agents)
* Parses: 'bmad-brainstorming.md' → { prefix: 'bmad', module: 'core', type: 'workflows', name: 'brainstorming' } (core workflows)
*
* @param {string} filename - Dash-formatted filename
* @returns {Object|null} Parsed parts or null if invalid format
*/
function parseDashName(filename) {
const withoutExt = filename.replace('.md', '');
const parts = withoutExt.split('-');
if (parts.length < 2 || parts[0] !== 'bmad') {
return null;
}
// Check if this is an agent file (has 'agent' as second part)
const isAgent = parts[1] === 'agent';
if (isAgent) {
// This is an agent file
// Format: bmad-agent-name (core) or bmad-agent-module-name
if (parts.length === 3) {
// Core agent: bmad-agent-name
return {
prefix: parts[0],
module: 'core',
type: 'agents',
name: parts[2],
};
} else {
// Module agent: bmad-agent-module-name
return {
prefix: parts[0],
module: parts[2],
type: 'agents',
name: parts.slice(3).join('-'),
};
}
}
// Not an agent file - must be a workflow/tool/task
// If only 2 parts (bmad-name), it's a core workflow/tool/task
if (parts.length === 2) {
return {
prefix: parts[0],
module: 'core',
type: 'workflows', // Default to workflows for non-agent core items
name: parts[1],
};
}
// Otherwise, it's a module workflow/tool/task (bmad-module-name)
return {
prefix: parts[0],
module: parts[1],
type: 'workflows', // Default to workflows for non-agent module items
name: parts.slice(2).join('-'),
};
}
// ============================================================================
// LEGACY FUNCTIONS (underscore format) - kept for backward compatibility
// ============================================================================
/**
* Convert hierarchical path to flat underscore-separated name (LEGACY)
* @deprecated Use toDashName instead
*/
function toUnderscoreName(module, type, name) {
const isAgent = type === AGENT_SEGMENT;
if (module === 'core') {
return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`;
}
return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`;
}
/**
* Convert relative path to flat underscore-separated name (LEGACY)
* @deprecated Use toDashPath instead
*/
function toUnderscorePath(relativePath) {
// Strip common file extensions (same as toDashPath for consistency)
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
const parts = withoutExt.split(/[/\\]/);
const module = parts[0];
const type = parts[1];
const name = parts.slice(2).join('_');
return toUnderscoreName(module, type, name);
}
/**
* Create custom agent underscore name (LEGACY)
* @deprecated Use customAgentDashName instead
*/
function customAgentUnderscoreName(agentName) {
return `bmad_custom_${agentName}.md`;
}
/**
* Check if a filename uses underscore format (LEGACY)
* @deprecated Use isDashFormat instead
*/
function isUnderscoreFormat(filename) {
return filename.startsWith('bmad_') && filename.includes('_');
}
/**
* Extract parts from an underscore-formatted filename (LEGACY)
* @deprecated Use parseDashName instead
*/
function parseUnderscoreName(filename) {
const withoutExt = filename.replace('.md', '');
const parts = withoutExt.split('_');
if (parts.length < 2 || parts[0] !== 'bmad') {
return null;
}
const agentIndex = parts.indexOf('agent');
if (agentIndex !== -1) {
if (agentIndex === 1) {
return {
prefix: parts[0],
module: 'core',
type: 'agents',
name: parts.slice(agentIndex + 1).join('_'),
};
} else {
return {
prefix: parts[0],
module: parts[1],
type: 'agents',
name: parts.slice(agentIndex + 1).join('_'),
};
}
}
if (parts.length === 2) {
return {
prefix: parts[0],
module: 'core',
type: 'workflows',
name: parts[1],
};
}
return {
prefix: parts[0],
module: parts[1],
type: 'workflows',
name: parts.slice(2).join('_'),
};
}
// Backward compatibility aliases (colon format was same as underscore)
const toColonName = toUnderscoreName;
const toColonPath = toUnderscorePath;
const customAgentColonName = customAgentUnderscoreName;
const isColonFormat = isUnderscoreFormat;
const parseColonName = parseUnderscoreName;
module.exports = {
// New standard (dash-based)
toDashName,
toDashPath,
customAgentDashName,
isDashFormat,
parseDashName,
// Legacy (underscore-based) - kept for backward compatibility
toUnderscoreName,
toUnderscorePath,
customAgentUnderscoreName,
isUnderscoreFormat,
parseUnderscoreName,
// Backward compatibility aliases
toColonName,
toColonPath,
customAgentColonName,
isColonFormat,
parseColonName,
TYPE_SEGMENTS,
AGENT_SEGMENT,
};