Merge branch 'dev' into schaltwerk/kilocode-mcp-migration
Merged dev branch which includes: - Workflows migration (Phase 3) - Rules migration (Phase 2) Combined with MCP migration (Phase 4) from this branch. Resolved merge conflicts in: - packages/opencode/src/config/config.ts - Combined all four migrators - packages/opencode/src/kilocode/index.ts - Export all migrators
This commit is contained in:
@@ -29,6 +29,8 @@ 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
|
||||
import { WorkflowsMigrator } from "../kilocode/workflows-migrator" // kilocode_change
|
||||
import { McpMigrator } from "../kilocode/mcp-migrator" // kilocode_change
|
||||
|
||||
export namespace Config {
|
||||
@@ -49,11 +51,73 @@ export namespace Config {
|
||||
export const state = Instance.state(async () => {
|
||||
const auth = await Auth.all()
|
||||
|
||||
// Load remote/well-known config first as the base layer (lowest precedence)
|
||||
// This allows organizations to provide default configs that users can override
|
||||
const kilocodeMcp = await McpMigrator.loadMcpConfig(Instance.directory) // kilocode_change
|
||||
let result: Info = Object.keys(kilocodeMcp).length > 0 ? { mcp: kilocodeMcp } : {}
|
||||
// kilocode_change start - Load Kilocode configs first (lowest precedence)
|
||||
// This ensures Opencode native configs always take precedence over legacy Kilocode configs
|
||||
let result: Info = {}
|
||||
|
||||
// Load Kilocode custom modes (legacy fallback)
|
||||
try {
|
||||
const kilocodeMigration = await ModesMigrator.migrate({
|
||||
projectDir: Instance.directory,
|
||||
})
|
||||
if (Object.keys(kilocodeMigration.agents).length > 0) {
|
||||
result = mergeConfigConcatArrays(result, { agent: kilocodeMigration.agents })
|
||||
log.debug("loaded kilocode custom modes", {
|
||||
count: Object.keys(kilocodeMigration.agents).length,
|
||||
modes: Object.keys(kilocodeMigration.agents),
|
||||
})
|
||||
}
|
||||
for (const skipped of kilocodeMigration.skipped) {
|
||||
log.debug("skipped kilocode mode", { slug: skipped.slug, reason: skipped.reason })
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn("failed to load kilocode modes", { error: err })
|
||||
}
|
||||
|
||||
// Load Kilocode workflows as commands (legacy fallback)
|
||||
try {
|
||||
const workflowsMigration = await WorkflowsMigrator.migrate({
|
||||
projectDir: Instance.directory,
|
||||
})
|
||||
if (Object.keys(workflowsMigration.commands).length > 0) {
|
||||
result = mergeConfigConcatArrays(result, { command: workflowsMigration.commands })
|
||||
log.debug("loaded kilocode workflows as commands", {
|
||||
count: Object.keys(workflowsMigration.commands).length,
|
||||
commands: Object.keys(workflowsMigration.commands),
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn("failed to load kilocode workflows", { error: err })
|
||||
}
|
||||
|
||||
// Load Kilocode rules (legacy fallback)
|
||||
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 })
|
||||
}
|
||||
|
||||
// Load Kilocode MCP servers (legacy fallback)
|
||||
const kilocodeMcp = await McpMigrator.loadMcpConfig(Instance.directory)
|
||||
if (Object.keys(kilocodeMcp).length > 0) {
|
||||
result = mergeConfigConcatArrays(result, { mcp: kilocodeMcp })
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
// Load remote/well-known config (overrides Kilocode legacy configs)
|
||||
// This allows organizations to provide default configs that users can override
|
||||
for (const [key, value] of Object.entries(auth)) {
|
||||
if (value.type === "wellknown") {
|
||||
process.env[value.key] = value.token
|
||||
@@ -99,26 +163,6 @@ export namespace Config {
|
||||
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
|
||||
}
|
||||
|
||||
// kilocode_change start - Load Kilocode custom modes
|
||||
try {
|
||||
const kilocodeMigration = await ModesMigrator.migrate({
|
||||
projectDir: Instance.directory,
|
||||
})
|
||||
if (Object.keys(kilocodeMigration.agents).length > 0) {
|
||||
result = mergeConfigConcatArrays(result, { agent: kilocodeMigration.agents })
|
||||
log.debug("loaded kilocode custom modes", {
|
||||
count: Object.keys(kilocodeMigration.agents).length,
|
||||
modes: Object.keys(kilocodeMigration.agents),
|
||||
})
|
||||
}
|
||||
for (const skipped of kilocodeMigration.skipped) {
|
||||
log.debug("skipped kilocode mode", { slug: skipped.slug, reason: skipped.reason })
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn("failed to load kilocode modes", { error: err })
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
result.agent = result.agent || {}
|
||||
result.mode = result.mode || {}
|
||||
result.plugin = result.plugin || []
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Config } from "../config/config"
|
||||
import { ModesMigrator } from "./modes-migrator"
|
||||
import { RulesMigrator } from "./rules-migrator" // kilocode_change
|
||||
import { WorkflowsMigrator } from "./workflows-migrator"
|
||||
|
||||
export namespace KilocodeConfigInjector {
|
||||
export interface InjectionResult {
|
||||
@@ -12,10 +14,15 @@ 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[] = []
|
||||
|
||||
// Migrate custom modes only
|
||||
// Build config object
|
||||
const config: Partial<Config.Info> = {}
|
||||
|
||||
// Migrate custom modes
|
||||
const modesMigration = await ModesMigrator.migrate(options)
|
||||
|
||||
// Log skipped default modes (for debugging)
|
||||
@@ -23,13 +30,35 @@ export namespace KilocodeConfigInjector {
|
||||
warnings.push(`Mode '${skipped.slug}' skipped: ${skipped.reason}`)
|
||||
}
|
||||
|
||||
// Build config object
|
||||
const config: Partial<Config.Info> = {}
|
||||
|
||||
if (Object.keys(modesMigration.agents).length > 0) {
|
||||
config.agent = modesMigration.agents
|
||||
}
|
||||
|
||||
// Migrate workflows to commands
|
||||
const workflowsMigration = await WorkflowsMigrator.migrate(options)
|
||||
|
||||
warnings.push(...workflowsMigration.warnings)
|
||||
|
||||
if (Object.keys(workflowsMigration.commands).length > 0) {
|
||||
config.command = workflowsMigration.commands
|
||||
}
|
||||
|
||||
// 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,3 +1,5 @@
|
||||
export { ModesMigrator } from "./modes-migrator"
|
||||
export { RulesMigrator } from "./rules-migrator"
|
||||
export { WorkflowsMigrator } from "./workflows-migrator"
|
||||
export { McpMigrator } from "./mcp-migrator"
|
||||
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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import os from "os"
|
||||
import type { Config } from "../config/config"
|
||||
|
||||
export namespace WorkflowsMigrator {
|
||||
const KILOCODE_WORKFLOWS_DIR = ".kilocode/workflows"
|
||||
const GLOBAL_WORKFLOWS_DIR = path.join(os.homedir(), ".kilocode", "workflows")
|
||||
|
||||
// Get platform-specific VSCode global storage path (same as modes-migrator)
|
||||
function getVSCodeGlobalStoragePath(): string {
|
||||
const home = os.homedir()
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
return path.join(home, "Library", "Application Support", "Code", "User", "globalStorage", "kilocode.kilo-code")
|
||||
case "win32":
|
||||
return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "Code", "User", "globalStorage", "kilocode.kilo-code")
|
||||
default: // linux
|
||||
return path.join(home, ".config", "Code", "User", "globalStorage", "kilocode.kilo-code")
|
||||
}
|
||||
}
|
||||
|
||||
export interface KilocodeWorkflow {
|
||||
name: string
|
||||
path: string
|
||||
content: string
|
||||
source: "global" | "project"
|
||||
}
|
||||
|
||||
export interface MigrationResult {
|
||||
commands: Record<string, Config.Command>
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
async function directoryExists(dirPath: string): Promise<boolean> {
|
||||
const stat = await fs.stat(dirPath).catch(() => null)
|
||||
return stat?.isDirectory() ?? false
|
||||
}
|
||||
|
||||
async function findWorkflowFiles(dir: string): Promise<string[]> {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => [])
|
||||
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => path.join(dir, e.name))
|
||||
}
|
||||
|
||||
export function extractNameFromFilename(filename: string): string {
|
||||
return path.basename(filename, ".md")
|
||||
}
|
||||
|
||||
export function extractDescription(content: string): string | undefined {
|
||||
const lines = content.split("\n")
|
||||
let foundTitle = false
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed.startsWith("#")) {
|
||||
foundTitle = true
|
||||
continue
|
||||
}
|
||||
if (foundTitle && trimmed.length > 0) {
|
||||
return trimmed.slice(0, 200)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function loadWorkflowsFromDir(dir: string, source: "global" | "project"): Promise<KilocodeWorkflow[]> {
|
||||
if (!(await directoryExists(dir))) return []
|
||||
const files = await findWorkflowFiles(dir)
|
||||
const workflows: KilocodeWorkflow[] = []
|
||||
for (const file of files) {
|
||||
const content = await fs.readFile(file, "utf-8")
|
||||
workflows.push({
|
||||
name: extractNameFromFilename(file),
|
||||
path: file,
|
||||
content: content.trim(),
|
||||
source,
|
||||
})
|
||||
}
|
||||
return workflows
|
||||
}
|
||||
|
||||
export async function discoverWorkflows(projectDir: string, skipGlobalPaths?: boolean): Promise<KilocodeWorkflow[]> {
|
||||
const workflows: KilocodeWorkflow[] = []
|
||||
|
||||
if (!skipGlobalPaths) {
|
||||
// 1. VSCode extension global storage (primary location for global workflows)
|
||||
const vscodeWorkflowsDir = path.join(getVSCodeGlobalStoragePath(), "workflows")
|
||||
workflows.push(...(await loadWorkflowsFromDir(vscodeWorkflowsDir, "global")))
|
||||
|
||||
// 2. Home directory ~/.kilocode/workflows (fallback/alternative location)
|
||||
workflows.push(...(await loadWorkflowsFromDir(GLOBAL_WORKFLOWS_DIR, "global")))
|
||||
}
|
||||
|
||||
// 3. Project workflows (.kilocode/workflows/)
|
||||
const projectWorkflowsDir = path.join(projectDir, KILOCODE_WORKFLOWS_DIR)
|
||||
workflows.push(...(await loadWorkflowsFromDir(projectWorkflowsDir, "project")))
|
||||
|
||||
return workflows
|
||||
}
|
||||
|
||||
export function convertToCommand(workflow: KilocodeWorkflow): Config.Command {
|
||||
return {
|
||||
template: workflow.content,
|
||||
description: extractDescription(workflow.content) ?? `Workflow: ${workflow.name}`,
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrate(options: {
|
||||
projectDir: string
|
||||
/** Skip reading from global paths. Used for testing. */
|
||||
skipGlobalPaths?: boolean
|
||||
}): Promise<MigrationResult> {
|
||||
const warnings: string[] = []
|
||||
const commands: Record<string, Config.Command> = {}
|
||||
|
||||
const workflows = await discoverWorkflows(options.projectDir, options.skipGlobalPaths)
|
||||
|
||||
// Deduplicate by name (project takes precedence over global)
|
||||
const workflowsByName = new Map<string, KilocodeWorkflow>()
|
||||
|
||||
// Add global first
|
||||
for (const workflow of workflows.filter((w) => w.source === "global")) {
|
||||
workflowsByName.set(workflow.name, workflow)
|
||||
}
|
||||
|
||||
// Project overwrites global
|
||||
for (const workflow of workflows.filter((w) => w.source === "project")) {
|
||||
if (workflowsByName.has(workflow.name)) {
|
||||
warnings.push(`Project workflow '${workflow.name}' overrides global workflow`)
|
||||
}
|
||||
workflowsByName.set(workflow.name, workflow)
|
||||
}
|
||||
|
||||
// Convert to commands
|
||||
for (const [name, workflow] of workflowsByName) {
|
||||
commands[name] = convertToCommand(workflow)
|
||||
}
|
||||
|
||||
return { commands, warnings }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user