mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-11-03 06:32:38 +08:00
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
This commit is contained in:
@@ -29,6 +29,7 @@ import { Bus } from "@/bus"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { Event } from "../server/event"
|
||||
import { ModesMigrator } from "../kilocode/modes-migrator" // kilocode_change
|
||||
import { RulesMigrator } from "../kilocode/rules-migrator" // kilocode_change
|
||||
|
||||
export namespace Config {
|
||||
const log = Log.create({ service: "config" })
|
||||
@@ -116,6 +117,26 @@ export namespace Config {
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start - Load Kilocode rules
|
||||
try {
|
||||
const kilocodeRules = await RulesMigrator.migrate({
|
||||
projectDir: Instance.directory,
|
||||
})
|
||||
if (kilocodeRules.instructions.length > 0) {
|
||||
result = mergeConfigConcatArrays(result, { instructions: kilocodeRules.instructions })
|
||||
log.debug("loaded kilocode rules", {
|
||||
count: kilocodeRules.instructions.length,
|
||||
files: kilocodeRules.instructions,
|
||||
})
|
||||
}
|
||||
for (const warning of kilocodeRules.warnings) {
|
||||
log.debug("kilocode rules warning", { warning })
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn("failed to load kilocode rules", { error: err })
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
result.agent = result.agent || {}
|
||||
result.mode = result.mode || {}
|
||||
result.plugin = result.plugin || []
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Config } from "../config/config"
|
||||
import { ModesMigrator } from "./modes-migrator"
|
||||
import { RulesMigrator } from "./rules-migrator" // kilocode_change
|
||||
|
||||
export namespace KilocodeConfigInjector {
|
||||
export interface InjectionResult {
|
||||
@@ -12,6 +13,8 @@ export namespace KilocodeConfigInjector {
|
||||
globalSettingsDir?: string
|
||||
/** Skip reading from global paths (VSCode storage, home dir). Used for testing. */
|
||||
skipGlobalPaths?: boolean
|
||||
/** Include rules migration. Defaults to true. */
|
||||
includeRules?: boolean
|
||||
}): Promise<InjectionResult> {
|
||||
const warnings: string[] = []
|
||||
|
||||
@@ -30,6 +33,22 @@ export namespace KilocodeConfigInjector {
|
||||
config.agent = modesMigration.agents
|
||||
}
|
||||
|
||||
// kilocode_change start - Rules migration
|
||||
if (options.includeRules !== false) {
|
||||
const rulesMigration = await RulesMigrator.migrate({
|
||||
projectDir: options.projectDir,
|
||||
includeGlobal: !options.skipGlobalPaths,
|
||||
includeModeSpecific: true,
|
||||
})
|
||||
|
||||
warnings.push(...rulesMigration.warnings)
|
||||
|
||||
if (rulesMigration.instructions.length > 0) {
|
||||
config.instructions = rulesMigration.instructions
|
||||
}
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
return {
|
||||
configJson: JSON.stringify(config),
|
||||
warnings,
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# Kilocode Rules Migration
|
||||
|
||||
This document explains how Kilocode rules are automatically migrated to Opencode's `instructions` config array.
|
||||
|
||||
## Overview
|
||||
|
||||
Kilocode stores rules in various file locations. When Opencode starts, it reads these files and injects their paths into the `instructions` config array, which Opencode then loads as part of the system prompt.
|
||||
|
||||
## Key Guarantees
|
||||
|
||||
### 1. Read-Only Migration
|
||||
The migration **never modifies project files**. We only:
|
||||
- Read existing rule files from disk
|
||||
- Inject file paths into the config's `instructions` array
|
||||
- Never write to the project or modify any files
|
||||
|
||||
### 2. Combines with Existing Config (Never Overwrites)
|
||||
If you have existing opencode config with `instructions`, the Kilocode rules are **combined**, not replaced:
|
||||
|
||||
```typescript
|
||||
// Example: User has opencode.json with:
|
||||
{ "instructions": ["AGENTS.md", "custom-rules.md"] }
|
||||
|
||||
// Kilocode rules add:
|
||||
{ "instructions": [".kilocoderules", ".kilocode/rules/coding.md"] }
|
||||
|
||||
// Result (combined, deduplicated):
|
||||
{ "instructions": ["AGENTS.md", "custom-rules.md", ".kilocoderules", ".kilocode/rules/coding.md"] }
|
||||
```
|
||||
|
||||
### 3. Restart to Pick Up Changes
|
||||
If you change your Kilocode configuration (e.g., edit `.kilocoderules`), simply restart kilo-cli to pick up the new config. No manual migration or conversion needed.
|
||||
|
||||
## Source Locations
|
||||
|
||||
The migrator reads rules from these locations:
|
||||
|
||||
### Project Rules
|
||||
|
||||
| Location | Description |
|
||||
|----------|-------------|
|
||||
| `.kilocoderules` | Legacy single-file rules in project root |
|
||||
| `.kilocode/rules/*.md` | Directory-based rules (multiple markdown files) |
|
||||
| `.kilocoderules-{mode}` | Mode-specific legacy rules (e.g., `.kilocoderules-code`) |
|
||||
| `.kilocode/rules-{mode}/*.md` | Mode-specific rule directories |
|
||||
|
||||
### Global Rules
|
||||
|
||||
| Location | Description |
|
||||
|----------|-------------|
|
||||
| `~/.kilocode/rules/*.md` | Global rules directory |
|
||||
|
||||
## File Mapping
|
||||
|
||||
| Kilocode Location | Opencode Equivalent |
|
||||
|-------------------|---------------------|
|
||||
| `.kilocoderules` | `instructions: [".kilocoderules"]` |
|
||||
| `.kilocoderules-{mode}` | `instructions: [".kilocoderules-{mode}"]` |
|
||||
| `.kilocode/rules/*.md` | `instructions: [".kilocode/rules/file.md", ...]` |
|
||||
| `.kilocode/rules-{mode}/*.md` | `instructions: [".kilocode/rules-{mode}/file.md", ...]` |
|
||||
| `~/.kilocode/rules/*.md` | `instructions: ["~/.kilocode/rules/file.md", ...]` |
|
||||
|
||||
## AGENTS.md Compatibility
|
||||
|
||||
`AGENTS.md` is loaded **natively** by Opencode - no migration needed. Opencode automatically loads:
|
||||
- `AGENTS.md` in project root
|
||||
- `CLAUDE.md` in project root
|
||||
- `~/.opencode/AGENTS.md` (global)
|
||||
|
||||
## Not Migrated
|
||||
|
||||
The following are **not** migrated:
|
||||
- `.roorules` - Roo-specific rules
|
||||
- `.clinerules` - Cline-specific rules
|
||||
|
||||
Only Kilocode-specific files (`.kilocoderules`, `.kilocode/rules/`) are migrated.
|
||||
|
||||
## Mode-Specific Rules
|
||||
|
||||
Mode-specific rules (e.g., `.kilocoderules-code`, `.kilocode/rules-architect/`) are included by default. All mode-specific rules are loaded regardless of the current mode.
|
||||
|
||||
## Warnings
|
||||
|
||||
The migrator generates warnings for:
|
||||
- **Legacy files**: When `.kilocoderules` is found, a warning suggests migrating to `.kilocode/rules/` directory structure
|
||||
|
||||
## Example
|
||||
|
||||
### Before (Kilocode)
|
||||
|
||||
```
|
||||
project/
|
||||
├── .kilocoderules # Legacy rules
|
||||
├── .kilocoderules-code # Code-mode specific
|
||||
└── .kilocode/
|
||||
└── rules/
|
||||
├── coding.md # Coding standards
|
||||
└── testing.md # Testing guidelines
|
||||
```
|
||||
|
||||
### After (Opencode Config)
|
||||
|
||||
```json
|
||||
{
|
||||
"instructions": [
|
||||
"/path/to/project/.kilocode/rules/coding.md",
|
||||
"/path/to/project/.kilocode/rules/testing.md",
|
||||
"/path/to/project/.kilocoderules",
|
||||
"/path/to/project/.kilocoderules-code"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Rules not appearing
|
||||
|
||||
1. Check the file exists at the expected location
|
||||
2. Ensure markdown files have `.md` extension
|
||||
3. Restart kilo-cli to pick up changes
|
||||
|
||||
### Duplicate rules
|
||||
|
||||
The `mergeConfigConcatArrays` function automatically deduplicates the `instructions` array using `Array.from(new Set([...]))`.
|
||||
|
||||
## Related Files
|
||||
|
||||
- [`rules-migrator.ts`](../rules-migrator.ts) - Core migration logic
|
||||
- [`config-injector.ts`](../config-injector.ts) - Config building and injection
|
||||
- [`modes-migration.md`](./modes-migration.md) - Modes migration documentation
|
||||
@@ -1,2 +1,3 @@
|
||||
export { ModesMigrator } from "./modes-migrator"
|
||||
export { RulesMigrator } from "./rules-migrator" // kilocode_change
|
||||
export { KilocodeConfigInjector } from "./config-injector"
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
// 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 }
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { test, expect, describe } from "bun:test"
|
||||
import { KilocodeConfigInjector } from "../../src/kilocode/config-injector"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
|
||||
describe("KilocodeConfigInjector", () => {
|
||||
describe("buildConfig", () => {
|
||||
@@ -59,6 +60,91 @@ describe("KilocodeConfigInjector", () => {
|
||||
expect(result.warnings[0]).toContain("code")
|
||||
expect(result.warnings[0]).toContain("skipped")
|
||||
})
|
||||
|
||||
// kilocode_change start - Rules migration tests
|
||||
test("includes rules in config", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await fs.mkdir(path.join(dir, ".kilocode", "rules"), { recursive: true })
|
||||
await Bun.write(path.join(dir, ".kilocode", "rules", "main.md"), "# Rules")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await KilocodeConfigInjector.buildConfig({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
})
|
||||
const config = JSON.parse(result.configJson)
|
||||
|
||||
expect(config.instructions).toBeDefined()
|
||||
expect(config.instructions).toHaveLength(1)
|
||||
expect(config.instructions[0]).toContain("main.md")
|
||||
})
|
||||
|
||||
test("skips rules when includeRules is false", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await fs.mkdir(path.join(dir, ".kilocode", "rules"), { recursive: true })
|
||||
await Bun.write(path.join(dir, ".kilocode", "rules", "main.md"), "# Rules")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await KilocodeConfigInjector.buildConfig({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
includeRules: false,
|
||||
})
|
||||
const config = JSON.parse(result.configJson)
|
||||
|
||||
expect(config.instructions).toBeUndefined()
|
||||
})
|
||||
|
||||
test("combines modes and rules in config", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
// Add custom mode
|
||||
await Bun.write(
|
||||
path.join(dir, ".kilocodemodes"),
|
||||
`customModes:
|
||||
- slug: translate
|
||||
name: Translate
|
||||
roleDefinition: You are a translator
|
||||
groups:
|
||||
- read`,
|
||||
)
|
||||
// Add rules
|
||||
await fs.mkdir(path.join(dir, ".kilocode", "rules"), { recursive: true })
|
||||
await Bun.write(path.join(dir, ".kilocode", "rules", "main.md"), "# Rules")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await KilocodeConfigInjector.buildConfig({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
})
|
||||
const config = JSON.parse(result.configJson)
|
||||
|
||||
expect(config.agent).toBeDefined()
|
||||
expect(config.agent.translate).toBeDefined()
|
||||
expect(config.instructions).toBeDefined()
|
||||
expect(config.instructions).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("adds warnings for legacy rule files", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocoderules"), "# Legacy rules")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await KilocodeConfigInjector.buildConfig({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
})
|
||||
|
||||
expect(result.warnings.some((w) => w.includes("Legacy"))).toBe(true)
|
||||
})
|
||||
// kilocode_change end
|
||||
})
|
||||
|
||||
describe("getEnvVars", () => {
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { test, expect, describe } from "bun:test"
|
||||
import { RulesMigrator } from "../../src/kilocode/rules-migrator"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
|
||||
describe("RulesMigrator", () => {
|
||||
describe("discoverRules", () => {
|
||||
test("discovers legacy .kilocoderules file", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocoderules"), "# Project rules")
|
||||
},
|
||||
})
|
||||
|
||||
const rules = await RulesMigrator.discoverRules(tmp.path)
|
||||
|
||||
expect(rules).toHaveLength(1)
|
||||
expect(rules[0].source).toBe("legacy")
|
||||
expect(rules[0].path).toContain(".kilocoderules")
|
||||
expect(rules[0].mode).toBeUndefined()
|
||||
})
|
||||
|
||||
test("discovers .kilocode/rules/ directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await fs.mkdir(path.join(dir, ".kilocode", "rules"), { recursive: true })
|
||||
await Bun.write(path.join(dir, ".kilocode", "rules", "coding.md"), "# Coding rules")
|
||||
await Bun.write(path.join(dir, ".kilocode", "rules", "testing.md"), "# Testing rules")
|
||||
},
|
||||
})
|
||||
|
||||
const rules = await RulesMigrator.discoverRules(tmp.path)
|
||||
|
||||
expect(rules).toHaveLength(2)
|
||||
expect(rules.every((r) => r.source === "project")).toBe(true)
|
||||
expect(rules.every((r) => r.mode === undefined)).toBe(true)
|
||||
})
|
||||
|
||||
test("discovers mode-specific directory rules", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await fs.mkdir(path.join(dir, ".kilocode", "rules-code"), { recursive: true })
|
||||
await Bun.write(path.join(dir, ".kilocode", "rules-code", "style.md"), "# Code style")
|
||||
},
|
||||
})
|
||||
|
||||
const rules = await RulesMigrator.discoverRules(tmp.path)
|
||||
|
||||
expect(rules).toHaveLength(1)
|
||||
expect(rules[0].source).toBe("project")
|
||||
expect(rules[0].mode).toBe("code")
|
||||
})
|
||||
|
||||
test("discovers mode-specific legacy file", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocoderules-architect"), "# Architect rules")
|
||||
},
|
||||
})
|
||||
|
||||
const rules = await RulesMigrator.discoverRules(tmp.path)
|
||||
|
||||
expect(rules).toHaveLength(1)
|
||||
expect(rules[0].source).toBe("legacy")
|
||||
expect(rules[0].mode).toBe("architect")
|
||||
})
|
||||
|
||||
test("ignores non-markdown files in rules directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await fs.mkdir(path.join(dir, ".kilocode", "rules"), { recursive: true })
|
||||
await Bun.write(path.join(dir, ".kilocode", "rules", "rules.md"), "# Rules")
|
||||
await Bun.write(path.join(dir, ".kilocode", "rules", "notes.txt"), "Notes")
|
||||
await Bun.write(path.join(dir, ".kilocode", "rules", "config.json"), "{}")
|
||||
},
|
||||
})
|
||||
|
||||
const rules = await RulesMigrator.discoverRules(tmp.path)
|
||||
|
||||
expect(rules).toHaveLength(1)
|
||||
expect(rules[0].path).toContain("rules.md")
|
||||
})
|
||||
|
||||
test("returns empty array for project without rules", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
const rules = await RulesMigrator.discoverRules(tmp.path)
|
||||
|
||||
expect(rules).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("discovers multiple rule sources together", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
// Legacy file
|
||||
await Bun.write(path.join(dir, ".kilocoderules"), "# Legacy rules")
|
||||
// Directory rules
|
||||
await fs.mkdir(path.join(dir, ".kilocode", "rules"), { recursive: true })
|
||||
await Bun.write(path.join(dir, ".kilocode", "rules", "main.md"), "# Main rules")
|
||||
// Mode-specific
|
||||
await Bun.write(path.join(dir, ".kilocoderules-code"), "# Code rules")
|
||||
},
|
||||
})
|
||||
|
||||
const rules = await RulesMigrator.discoverRules(tmp.path)
|
||||
|
||||
expect(rules).toHaveLength(3)
|
||||
expect(rules.some((r) => r.source === "legacy" && !r.mode)).toBe(true)
|
||||
expect(rules.some((r) => r.source === "project")).toBe(true)
|
||||
expect(rules.some((r) => r.mode === "code")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("migrate", () => {
|
||||
test("returns instructions array with discovered rules", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await fs.mkdir(path.join(dir, ".kilocode", "rules"), { recursive: true })
|
||||
await Bun.write(path.join(dir, ".kilocode", "rules", "main.md"), "# Main rules")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await RulesMigrator.migrate({ projectDir: tmp.path })
|
||||
|
||||
expect(result.instructions).toHaveLength(1)
|
||||
expect(result.instructions[0]).toContain("main.md")
|
||||
})
|
||||
|
||||
test("warns about legacy files", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocoderules"), "# Legacy rules")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await RulesMigrator.migrate({ projectDir: tmp.path })
|
||||
|
||||
expect(result.warnings.some((w) => w.includes("Legacy"))).toBe(true)
|
||||
expect(result.warnings.some((w) => w.includes(".kilocode/rules/"))).toBe(true)
|
||||
})
|
||||
|
||||
test("skips mode-specific rules when includeModeSpecific is false", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocoderules-code"), "# Code rules")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await RulesMigrator.migrate({
|
||||
projectDir: tmp.path,
|
||||
includeModeSpecific: false,
|
||||
})
|
||||
|
||||
expect(result.instructions).toHaveLength(0)
|
||||
expect(result.warnings.some((w) => w.includes("skipped"))).toBe(true)
|
||||
})
|
||||
|
||||
test("includes mode-specific rules by default", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocoderules-code"), "# Code rules")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await RulesMigrator.migrate({ projectDir: tmp.path })
|
||||
|
||||
expect(result.instructions).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("returns empty result for project without rules", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
const result = await RulesMigrator.migrate({ projectDir: tmp.path })
|
||||
|
||||
expect(result.instructions).toHaveLength(0)
|
||||
expect(result.warnings).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("combines all rule sources", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocoderules"), "# Legacy")
|
||||
await fs.mkdir(path.join(dir, ".kilocode", "rules"), { recursive: true })
|
||||
await Bun.write(path.join(dir, ".kilocode", "rules", "main.md"), "# Main")
|
||||
await Bun.write(path.join(dir, ".kilocoderules-architect"), "# Architect")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await RulesMigrator.migrate({ projectDir: tmp.path })
|
||||
|
||||
expect(result.instructions).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user