fix: trim activation header to avoid YAML formatting issues in kilo installer (#1537)

* fix: trim activation header to avoid YAML formatting issues in kilo installer

* refactor: convert kilo installer to use YAML object serialization and add workflow support

- Replace string concatenation with yaml.parse/stringify for proper YAML handling
- Add workflow command generation and export to .kilocode/workflows/
- Implement clearBmadWorkflows to remove old BMAD workflow files
- Convert createModeEntry to createModeObject returning structured objects
- Update cleanup to use YAML parsing for proper mode filtering
- Update installCustomAgentLauncher to use object-based config

* fix: add task and tool command generation to kilo installer

---------

Co-authored-by: Brian <bmadcode@gmail.com>
This commit is contained in:
Davor Racic 2026-02-06 02:00:52 +01:00 committed by GitHub
parent 2aab028f96
commit 311b237d85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 130 additions and 108 deletions

View File

@ -1,7 +1,10 @@
const path = require('node:path'); const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk'); const chalk = require('chalk');
const yaml = require('yaml');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
/** /**
* KiloCode IDE setup handler * KiloCode IDE setup handler
@ -22,76 +25,94 @@ class KiloSetup extends BaseIdeSetup {
async setup(projectDir, bmadDir, options = {}) { async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`)); console.log(chalk.cyan(`Setting up ${this.name}...`));
// Check for existing .kilocodemodes file // Clean up any old BMAD installation first
await this.cleanup(projectDir);
// Load existing config (may contain non-BMAD modes and other settings)
const kiloModesPath = path.join(projectDir, this.configFile); const kiloModesPath = path.join(projectDir, this.configFile);
let existingModes = []; let config = {};
let existingContent = '';
if (await this.pathExists(kiloModesPath)) { if (await this.pathExists(kiloModesPath)) {
existingContent = await this.readFile(kiloModesPath); const existingContent = await this.readFile(kiloModesPath);
// Parse existing modes try {
const modeMatches = existingContent.matchAll(/- slug: ([\w-]+)/g); config = yaml.parse(existingContent) || {};
for (const match of modeMatches) { } catch {
existingModes.push(match[1]); // If parsing fails, start fresh but warn user
console.log(chalk.yellow('Warning: Could not parse existing .kilocodemodes, starting fresh'));
config = {};
} }
console.log(chalk.yellow(`Found existing .kilocodemodes file with ${existingModes.length} modes`)); }
// Ensure customModes array exists
if (!Array.isArray(config.customModes)) {
config.customModes = [];
} }
// Generate agent launchers // Generate agent launchers
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
// Create modes content // Create mode objects and add to config
let newModesContent = '';
let addedCount = 0; let addedCount = 0;
let skippedCount = 0;
for (const artifact of agentArtifacts) { for (const artifact of agentArtifacts) {
const slug = `bmad-${artifact.module}-${artifact.name}`; const modeObject = await this.createModeObject(artifact, projectDir);
config.customModes.push(modeObject);
// Skip if already exists
if (existingModes.includes(slug)) {
console.log(chalk.dim(` Skipping ${slug} - already exists`));
skippedCount++;
continue;
}
const modeEntry = await this.createModeEntry(artifact, projectDir);
newModesContent += modeEntry;
addedCount++; addedCount++;
} }
// Build final content // Write .kilocodemodes file with proper YAML structure
let finalContent = ''; const finalContent = yaml.stringify(config, { lineWidth: 0 });
if (existingContent) {
finalContent = existingContent.trim() + '\n' + newModesContent;
} else {
finalContent = 'customModes:\n' + newModesContent;
}
// Write .kilocodemodes file
await this.writeFile(kiloModesPath, finalContent); await this.writeFile(kiloModesPath, finalContent);
// Generate workflow commands
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
// Write to .kilocode/workflows/ directory
const workflowsDir = path.join(projectDir, '.kilocode', 'workflows');
await this.ensureDir(workflowsDir);
// Clear old BMAD workflows before writing new ones
await this.clearBmadWorkflows(workflowsDir);
// Write workflow files
const workflowCount = await workflowGenerator.writeDashArtifacts(workflowsDir, workflowArtifacts);
// Generate task and tool commands
const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName);
const { artifacts: taskToolArtifacts, counts: taskToolCounts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
// Write task/tool files to workflows directory (same location as workflows)
await taskToolGen.writeDashArtifacts(workflowsDir, taskToolArtifacts);
const taskCount = taskToolCounts.tasks || 0;
const toolCount = taskToolCounts.tools || 0;
console.log(chalk.green(`${this.name} configured:`)); console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${addedCount} modes added`)); console.log(chalk.dim(` - ${addedCount} modes added`));
if (skippedCount > 0) { console.log(chalk.dim(` - ${workflowCount} workflows exported`));
console.log(chalk.dim(` - ${skippedCount} modes skipped (already exist)`)); console.log(chalk.dim(` - ${taskCount} tasks exported`));
} console.log(chalk.dim(` - ${toolCount} tools exported`));
console.log(chalk.dim(` - Configuration file: ${this.configFile}`)); console.log(chalk.dim(` - Configuration file: ${this.configFile}`));
console.log(chalk.dim(` - Workflows directory: .kilocode/workflows/`));
console.log(chalk.dim('\n Modes will be available when you open this project in KiloCode')); console.log(chalk.dim('\n Modes will be available when you open this project in KiloCode'));
return { return {
success: true, success: true,
modes: addedCount, modes: addedCount,
skipped: skippedCount, workflows: workflowCount,
tasks: taskCount,
tools: toolCount,
}; };
} }
/** /**
* Create a mode entry for an agent * Create a mode object for an agent
* @param {Object} artifact - Agent artifact
* @param {string} projectDir - Project directory
* @returns {Object} Mode object for YAML serialization
*/ */
async createModeEntry(artifact, projectDir) { async createModeObject(artifact, projectDir) {
// Extract metadata from launcher content // Extract metadata from launcher content
const titleMatch = artifact.content.match(/title="([^"]+)"/); const titleMatch = artifact.content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name); const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name);
@ -102,8 +123,8 @@ class KiloSetup extends BaseIdeSetup {
const whenToUseMatch = artifact.content.match(/whenToUse="([^"]+)"/); const whenToUseMatch = artifact.content.match(/whenToUse="([^"]+)"/);
const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`; const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
// Get the activation header from central template // Get the activation header from central template (trim to avoid YAML formatting issues)
const activationHeader = await this.getAgentCommandHeader(); const activationHeader = (await this.getAgentCommandHeader()).trim();
const roleDefinitionMatch = artifact.content.match(/roleDefinition="([^"]+)"/); const roleDefinitionMatch = artifact.content.match(/roleDefinition="([^"]+)"/);
const roleDefinition = roleDefinitionMatch const roleDefinition = roleDefinitionMatch
@ -113,22 +134,15 @@ class KiloSetup extends BaseIdeSetup {
// Get relative path // Get relative path
const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/'); const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/');
// Build mode entry (KiloCode uses same schema as Roo) // Build mode object (KiloCode uses same schema as Roo)
const slug = `bmad-${artifact.module}-${artifact.name}`; return {
let modeEntry = ` - slug: ${slug}\n`; slug: `bmad-${artifact.module}-${artifact.name}`,
modeEntry += ` name: '${icon} ${title}'\n`; name: `${icon} ${title}`,
modeEntry += ` roleDefinition: ${roleDefinition}\n`; roleDefinition: roleDefinition,
modeEntry += ` whenToUse: ${whenToUse}\n`; whenToUse: whenToUse,
modeEntry += ` customInstructions: |\n`; customInstructions: `${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`,
modeEntry += ` ${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`; groups: ['read', 'edit', 'browser', 'command', 'mcp'],
modeEntry += ` groups:\n`; };
modeEntry += ` - read\n`;
modeEntry += ` - edit\n`;
modeEntry += ` - browser\n`;
modeEntry += ` - command\n`;
modeEntry += ` - mcp\n`;
return modeEntry;
} }
/** /**
@ -141,6 +155,22 @@ class KiloSetup extends BaseIdeSetup {
.join(' '); .join(' ');
} }
/**
* Clear old BMAD workflow files from workflows directory
* @param {string} workflowsDir - Workflows directory path
*/
async clearBmadWorkflows(workflowsDir) {
const fs = require('fs-extra');
if (!(await fs.pathExists(workflowsDir))) return;
const entries = await fs.readdir(workflowsDir);
for (const entry of entries) {
if (entry.startsWith('bmad-') && entry.endsWith('.md')) {
await fs.remove(path.join(workflowsDir, entry));
}
}
}
/** /**
* Cleanup KiloCode configuration * Cleanup KiloCode configuration
*/ */
@ -151,28 +181,29 @@ class KiloSetup extends BaseIdeSetup {
if (await fs.pathExists(kiloModesPath)) { if (await fs.pathExists(kiloModesPath)) {
const content = await fs.readFile(kiloModesPath, 'utf8'); const content = await fs.readFile(kiloModesPath, 'utf8');
// Remove BMAD modes only try {
const lines = content.split('\n'); const config = yaml.parse(content) || {};
const filteredLines = [];
let skipMode = false;
let removedCount = 0;
for (const line of lines) { if (Array.isArray(config.customModes)) {
if (/^\s*- slug: bmad-/.test(line)) { const originalCount = config.customModes.length;
skipMode = true; // Remove BMAD modes only (keep non-BMAD modes)
removedCount++; config.customModes = config.customModes.filter((mode) => !mode.slug || !mode.slug.startsWith('bmad-'));
} else if (skipMode && /^\s*- slug: /.test(line)) { const removedCount = originalCount - config.customModes.length;
skipMode = false;
}
if (!skipMode) { if (removedCount > 0) {
filteredLines.push(line); await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 }));
console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .kilocodemodes`));
}
} }
} catch {
// If parsing fails, leave file as-is
console.log(chalk.yellow('Warning: Could not parse .kilocodemodes for cleanup'));
} }
await fs.writeFile(kiloModesPath, filteredLines.join('\n'));
console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .kilocodemodes`));
} }
// Clean up workflow files
const workflowsDir = path.join(projectDir, '.kilocode', 'workflows');
await this.clearBmadWorkflows(workflowsDir);
} }
/** /**
@ -185,31 +216,28 @@ class KiloSetup extends BaseIdeSetup {
*/ */
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
const kilocodemodesPath = path.join(projectDir, this.configFile); const kilocodemodesPath = path.join(projectDir, this.configFile);
let existingContent = ''; let config = {};
// Read existing .kilocodemodes file // Read existing .kilocodemodes file
if (await this.pathExists(kilocodemodesPath)) { if (await this.pathExists(kilocodemodesPath)) {
existingContent = await this.readFile(kilocodemodesPath); const existingContent = await this.readFile(kilocodemodesPath);
try {
config = yaml.parse(existingContent) || {};
} catch {
config = {};
}
} }
// Create custom agent mode entry // Ensure customModes array exists
if (!Array.isArray(config.customModes)) {
config.customModes = [];
}
// Create custom agent mode object
const slug = `bmad-custom-${agentName.toLowerCase()}`; const slug = `bmad-custom-${agentName.toLowerCase()}`;
const modeEntry = ` - slug: ${slug}
name: 'BMAD Custom: ${agentName}'
description: |
Custom BMAD agent: ${agentName}
** IMPORTANT**: Run @${agentPath} first to load the complete agent!
This is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file.
prompt: |
@${agentPath}
always: false
permissions: all
`;
// Check if mode already exists // Check if mode already exists
if (existingContent.includes(slug)) { if (config.customModes.some((mode) => mode.slug === slug)) {
return { return {
ide: 'kilo', ide: 'kilo',
path: this.configFile, path: this.configFile,
@ -219,24 +247,18 @@ class KiloSetup extends BaseIdeSetup {
}; };
} }
// Build final content // Add custom mode object
let finalContent = ''; config.customModes.push({
if (existingContent) { slug: slug,
// Find customModes section or add it name: `BMAD Custom: ${agentName}`,
if (existingContent.includes('customModes:')) { description: `Custom BMAD agent: ${agentName}\n\n**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!\n\nThis is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file.\n`,
// Append to existing customModes prompt: `@${agentPath}\n`,
finalContent = existingContent + modeEntry; always: false,
} else { permissions: 'all',
// Add customModes section });
finalContent = existingContent.trim() + '\n\ncustomModes:\n' + modeEntry;
}
} else {
// Create new .kilocodemodes file with customModes
finalContent = 'customModes:\n' + modeEntry;
}
// Write .kilocodemodes file // Write .kilocodemodes file with proper YAML structure
await this.writeFile(kilocodemodesPath, finalContent); await this.writeFile(kilocodemodesPath, yaml.stringify(config, { lineWidth: 0 }));
return { return {
ide: 'kilo', ide: 'kilo',