Files
Kilo-Org_kilo/packages/opencode/src/kilocode/rules-migrator.ts
T
Marius Wichtner 7471fcfe73 feat(kilocode): add rules migration (Phase 2)
Migrate Kilocode rules from .kilocoderules and .kilocode/rules/ to
Opencode's instructions config array.

Features:
- Discover rules from .kilocoderules, .kilocode/rules/*.md, and
  mode-specific variants
- Support global rules from ~/.kilocode/rules/
- Read-only migration (never modifies project files)
- Combines with existing opencode config (never overwrites)
- Kilocode-only (no .roorules or .clinerules migration)

Files:
- rules-migrator.ts: Core migration logic
- config.ts: Integration point for direct CLI usage
- config-injector.ts: Integration for VSCode extension path
- rules-migrator.test.ts: 13 unit tests
- config-injector.test.ts: 4 new integration tests
- rules-migration.md: Documentation
2026-01-26 17:50:58 +01:00

137 lines
4.1 KiB
TypeScript

// kilocode_change - new file
import * as fs from "fs/promises"
import * as path from "path"
import os from "os"
export namespace RulesMigrator {
// Only support .kilocoderules (no migration for .roorules or .clinerules)
const LEGACY_RULE_FILE = ".kilocoderules"
// Directory-based rules
const KILOCODE_RULES_DIR = ".kilocode/rules"
const GLOBAL_RULES_DIR = path.join(os.homedir(), ".kilocode", "rules")
// Known modes for mode-specific rule discovery
const KNOWN_MODES = ["code", "architect", "ask", "debug", "orchestrator"]
export interface RuleFile {
path: string
source: "global" | "project" | "legacy"
mode?: string // e.g., "code", "architect" - undefined means applies to all modes
}
export interface MigrationResult {
instructions: string[]
warnings: string[]
}
async function exists(filepath: string): Promise<boolean> {
return Bun.file(filepath).exists()
}
async function isDirectory(filepath: string): Promise<boolean> {
try {
const stat = await fs.stat(filepath)
return stat.isDirectory()
} catch {
return false
}
}
async function findMarkdownFiles(dir: string): Promise<string[]> {
try {
const entries = await fs.readdir(dir, { withFileTypes: true })
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => path.join(dir, e.name))
} catch {
return []
}
}
export async function discoverRules(projectDir: string): Promise<RuleFile[]> {
const rules: RuleFile[] = []
// 1. Global rules directory (~/.kilocode/rules/*.md)
if (await isDirectory(GLOBAL_RULES_DIR)) {
const files = await findMarkdownFiles(GLOBAL_RULES_DIR)
for (const file of files) {
rules.push({ path: file, source: "global" })
}
}
// 2. Project .kilocode/rules/ directory
const projectRulesDir = path.join(projectDir, KILOCODE_RULES_DIR)
if (await isDirectory(projectRulesDir)) {
const files = await findMarkdownFiles(projectRulesDir)
for (const file of files) {
rules.push({ path: file, source: "project" })
}
}
// 3. Legacy .kilocoderules file (only kilocode, not roo/cline)
const legacyFile = path.join(projectDir, LEGACY_RULE_FILE)
if (await exists(legacyFile)) {
rules.push({ path: legacyFile, source: "legacy" })
}
// 4. Mode-specific rules
for (const mode of KNOWN_MODES) {
// Mode-specific directory (.kilocode/rules-{mode}/*.md)
const modeDir = path.join(projectDir, `.kilocode/rules-${mode}`)
if (await isDirectory(modeDir)) {
const files = await findMarkdownFiles(modeDir)
for (const file of files) {
rules.push({ path: file, source: "project", mode })
}
}
// Legacy mode-specific file (.kilocoderules-{mode})
const legacyModeFile = path.join(projectDir, `.kilocoderules-${mode}`)
if (await exists(legacyModeFile)) {
rules.push({ path: legacyModeFile, source: "legacy", mode })
}
}
return rules
}
export async function migrate(options: {
projectDir: string
includeGlobal?: boolean
includeModeSpecific?: boolean
}): Promise<MigrationResult> {
const warnings: string[] = []
const instructions: string[] = []
const includeGlobal = options.includeGlobal ?? true
const includeModeSpecific = options.includeModeSpecific ?? true
const rules = await discoverRules(options.projectDir)
for (const rule of rules) {
// Skip global if not requested
if (rule.source === "global" && !includeGlobal) {
continue
}
// Skip mode-specific if not requested
if (rule.mode && !includeModeSpecific) {
warnings.push(`Mode-specific rule '${path.basename(rule.path)}' skipped (mode: ${rule.mode})`)
continue
}
// Add to instructions array
instructions.push(rule.path)
// Warn about legacy files
if (rule.source === "legacy") {
warnings.push(
`Legacy rule file '${path.basename(rule.path)}' found. ` +
`Consider migrating to .kilocode/rules/ directory.`,
)
}
}
return { instructions, warnings }
}
}