7471fcfe73
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
137 lines
4.1 KiB
TypeScript
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 }
|
|
}
|
|
}
|