refactor(migration): add backup creation and category migration
- Create timestamped backup before migration writes - Add migrateAgentConfigToCategory() for model→category migration - Add shouldDeleteAgentConfig() for cleanup when matching defaults - Add Prometheus and Metis to agent name map - Comprehensive test coverage for new functionality 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { describe, test, expect, afterEach } from "bun:test"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import {
|
||||
AGENT_NAME_MAP,
|
||||
HOOK_NAME_MAP,
|
||||
migrateAgentNames,
|
||||
migrateHookNames,
|
||||
migrateConfigFile,
|
||||
migrateAgentConfigToCategory,
|
||||
shouldDeleteAgentConfig,
|
||||
} from "./migration"
|
||||
|
||||
describe("migrateAgentNames", () => {
|
||||
@@ -19,10 +23,10 @@ describe("migrateAgentNames", () => {
|
||||
// #when: Migrate agent names
|
||||
const { migrated, changed } = migrateAgentNames(agents)
|
||||
|
||||
// #then: Legacy names should be migrated to Sisyphus
|
||||
// #then: Legacy names should be migrated to Sisyphus/Prometheus
|
||||
expect(changed).toBe(true)
|
||||
expect(migrated["Sisyphus"]).toEqual({ temperature: 0.5 })
|
||||
expect(migrated["Planner-Sisyphus"]).toEqual({ prompt: "custom prompt" })
|
||||
expect(migrated["Prometheus (Planner)"]).toEqual({ prompt: "custom prompt" })
|
||||
expect(migrated["omo"]).toBeUndefined()
|
||||
expect(migrated["OmO"]).toBeUndefined()
|
||||
expect(migrated["OmO-Plan"]).toBeUndefined()
|
||||
@@ -50,7 +54,7 @@ describe("migrateAgentNames", () => {
|
||||
// #given: Config with mixed case agent names
|
||||
const agents = {
|
||||
SISYPHUS: { model: "test" },
|
||||
"PLANNER-SISYPHUS": { prompt: "test" },
|
||||
"planner-sisyphus": { prompt: "test" },
|
||||
}
|
||||
|
||||
// #when: Migrate agent names
|
||||
@@ -58,7 +62,7 @@ describe("migrateAgentNames", () => {
|
||||
|
||||
// #then: Case-insensitive lookup should migrate correctly
|
||||
expect(migrated["Sisyphus"]).toEqual({ model: "test" })
|
||||
expect(migrated["Planner-Sisyphus"]).toEqual({ prompt: "test" })
|
||||
expect(migrated["Prometheus (Planner)"]).toEqual({ prompt: "test" })
|
||||
})
|
||||
|
||||
test("passes through unknown agent names unchanged", () => {
|
||||
@@ -220,7 +224,7 @@ describe("migrateConfigFile", () => {
|
||||
expect(rawConfig.omo_agent).toBeUndefined()
|
||||
const agents = rawConfig.agents as Record<string, unknown>
|
||||
expect(agents["Sisyphus"]).toBeDefined()
|
||||
expect(agents["Planner-Sisyphus"]).toBeDefined()
|
||||
expect(agents["Prometheus (Planner)"]).toBeDefined()
|
||||
expect(rawConfig.disabled_hooks).toContain("anthropic-context-window-limit-recovery")
|
||||
})
|
||||
})
|
||||
@@ -231,13 +235,404 @@ describe("migration maps", () => {
|
||||
// #then: Should contain all legacy → current mappings
|
||||
expect(AGENT_NAME_MAP["omo"]).toBe("Sisyphus")
|
||||
expect(AGENT_NAME_MAP["OmO"]).toBe("Sisyphus")
|
||||
expect(AGENT_NAME_MAP["OmO-Plan"]).toBe("Planner-Sisyphus")
|
||||
expect(AGENT_NAME_MAP["omo-plan"]).toBe("Planner-Sisyphus")
|
||||
expect(AGENT_NAME_MAP["OmO-Plan"]).toBe("Prometheus (Planner)")
|
||||
expect(AGENT_NAME_MAP["omo-plan"]).toBe("Prometheus (Planner)")
|
||||
expect(AGENT_NAME_MAP["Planner-Sisyphus"]).toBe("Prometheus (Planner)")
|
||||
expect(AGENT_NAME_MAP["plan-consultant"]).toBe("Metis (Plan Consultant)")
|
||||
})
|
||||
|
||||
test("HOOK_NAME_MAP contains anthropic-auto-compact migration", () => {
|
||||
// #given/#when: Check HOOK_NAME_MAP
|
||||
// #then: Should contain the legacy hook name mapping
|
||||
// #then: Should contain be legacy hook name mapping
|
||||
expect(HOOK_NAME_MAP["anthropic-auto-compact"]).toBe("anthropic-context-window-limit-recovery")
|
||||
})
|
||||
})
|
||||
|
||||
describe("migrateAgentConfigToCategory", () => {
|
||||
test("migrates model to category when mapping exists", () => {
|
||||
// #given: Config with a model that has a category mapping
|
||||
const config = {
|
||||
model: "google/gemini-3-pro-preview",
|
||||
temperature: 0.5,
|
||||
top_p: 0.9,
|
||||
}
|
||||
|
||||
// #when: Migrate agent config to category
|
||||
const { migrated, changed } = migrateAgentConfigToCategory(config)
|
||||
|
||||
// #then: Model should be replaced with category
|
||||
expect(changed).toBe(true)
|
||||
expect(migrated.category).toBe("visual-engineering")
|
||||
expect(migrated.model).toBeUndefined()
|
||||
expect(migrated.temperature).toBe(0.5)
|
||||
expect(migrated.top_p).toBe(0.9)
|
||||
})
|
||||
|
||||
test("does not migrate when model is not in map", () => {
|
||||
// #given: Config with a model that has no mapping
|
||||
const config = {
|
||||
model: "custom/model",
|
||||
temperature: 0.5,
|
||||
}
|
||||
|
||||
// #when: Migrate agent config to category
|
||||
const { migrated, changed } = migrateAgentConfigToCategory(config)
|
||||
|
||||
// #then: Config should remain unchanged
|
||||
expect(changed).toBe(false)
|
||||
expect(migrated).toEqual(config)
|
||||
})
|
||||
|
||||
test("does not migrate when model is not a string", () => {
|
||||
// #given: Config with non-string model
|
||||
const config = {
|
||||
model: { name: "test" },
|
||||
temperature: 0.5,
|
||||
}
|
||||
|
||||
// #when: Migrate agent config to category
|
||||
const { migrated, changed } = migrateAgentConfigToCategory(config)
|
||||
|
||||
// #then: Config should remain unchanged
|
||||
expect(changed).toBe(false)
|
||||
expect(migrated).toEqual(config)
|
||||
})
|
||||
|
||||
test("handles all mapped models correctly", () => {
|
||||
// #given: Configs for each mapped model
|
||||
const configs = [
|
||||
{ model: "google/gemini-3-pro-preview" },
|
||||
{ model: "openai/gpt-5.2" },
|
||||
{ model: "anthropic/claude-haiku-4-5" },
|
||||
{ model: "anthropic/claude-opus-4-5" },
|
||||
{ model: "anthropic/claude-sonnet-4-5" },
|
||||
]
|
||||
|
||||
const expectedCategories = ["visual-engineering", "high-iq", "quick", "most-capable", "general"]
|
||||
|
||||
// #when: Migrate each config
|
||||
const results = configs.map(migrateAgentConfigToCategory)
|
||||
|
||||
// #then: Each model should map to correct category
|
||||
results.forEach((result, index) => {
|
||||
expect(result.changed).toBe(true)
|
||||
expect(result.migrated.category).toBe(expectedCategories[index])
|
||||
expect(result.migrated.model).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves non-model fields during migration", () => {
|
||||
// #given: Config with multiple fields
|
||||
const config = {
|
||||
model: "openai/gpt-5.2",
|
||||
temperature: 0.1,
|
||||
top_p: 0.95,
|
||||
maxTokens: 4096,
|
||||
prompt_append: "custom instruction",
|
||||
}
|
||||
|
||||
// #when: Migrate agent config to category
|
||||
const { migrated } = migrateAgentConfigToCategory(config)
|
||||
|
||||
// #then: All non-model fields should be preserved
|
||||
expect(migrated.category).toBe("high-iq")
|
||||
expect(migrated.temperature).toBe(0.1)
|
||||
expect(migrated.top_p).toBe(0.95)
|
||||
expect(migrated.maxTokens).toBe(4096)
|
||||
expect(migrated.prompt_append).toBe("custom instruction")
|
||||
})
|
||||
})
|
||||
|
||||
describe("shouldDeleteAgentConfig", () => {
|
||||
test("returns true when config only has category field", () => {
|
||||
// #given: Config with only category field (no overrides)
|
||||
const config = { category: "visual-engineering" }
|
||||
|
||||
// #when: Check if config should be deleted
|
||||
const shouldDelete = shouldDeleteAgentConfig(config, "visual-engineering")
|
||||
|
||||
// #then: Should return true (matches category defaults)
|
||||
expect(shouldDelete).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false when category does not exist", () => {
|
||||
// #given: Config with unknown category
|
||||
const config = { category: "unknown" }
|
||||
|
||||
// #when: Check if config should be deleted
|
||||
const shouldDelete = shouldDeleteAgentConfig(config, "unknown")
|
||||
|
||||
// #then: Should return false (category not found)
|
||||
expect(shouldDelete).toBe(false)
|
||||
})
|
||||
|
||||
test("returns true when all fields match category defaults", () => {
|
||||
// #given: Config with fields matching category defaults
|
||||
const config = {
|
||||
category: "visual-engineering",
|
||||
model: "google/gemini-3-pro-preview",
|
||||
temperature: 0.7,
|
||||
}
|
||||
|
||||
// #when: Check if config should be deleted
|
||||
const shouldDelete = shouldDeleteAgentConfig(config, "visual-engineering")
|
||||
|
||||
// #then: Should return true (all fields match defaults)
|
||||
expect(shouldDelete).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false when fields differ from category defaults", () => {
|
||||
// #given: Config with custom temperature override
|
||||
const config = {
|
||||
category: "visual-engineering",
|
||||
temperature: 0.9, // Different from default (0.7)
|
||||
}
|
||||
|
||||
// #when: Check if config should be deleted
|
||||
const shouldDelete = shouldDeleteAgentConfig(config, "visual-engineering")
|
||||
|
||||
// #then: Should return false (has custom override)
|
||||
expect(shouldDelete).toBe(false)
|
||||
})
|
||||
|
||||
test("handles different categories with their defaults", () => {
|
||||
// #given: Configs for different categories
|
||||
const configs = [
|
||||
{ category: "high-iq", temperature: 0.1 },
|
||||
{ category: "quick", temperature: 0.3 },
|
||||
{ category: "most-capable", temperature: 0.1 },
|
||||
{ category: "general", temperature: 0.3 },
|
||||
]
|
||||
|
||||
// #when: Check each config
|
||||
const results = configs.map((config) => shouldDeleteAgentConfig(config, config.category as string))
|
||||
|
||||
// #then: All should be true (all match defaults)
|
||||
results.forEach((result) => {
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test("returns false when additional fields are present", () => {
|
||||
// #given: Config with extra fields
|
||||
const config = {
|
||||
category: "visual-engineering",
|
||||
temperature: 0.7,
|
||||
custom_field: "value", // Extra field not in defaults
|
||||
}
|
||||
|
||||
// #when: Check if config should be deleted
|
||||
const shouldDelete = shouldDeleteAgentConfig(config, "visual-engineering")
|
||||
|
||||
// #then: Should return false (has extra field)
|
||||
expect(shouldDelete).toBe(false)
|
||||
})
|
||||
|
||||
test("handles complex config with multiple overrides", () => {
|
||||
// #given: Config with multiple custom overrides
|
||||
const config = {
|
||||
category: "visual-engineering",
|
||||
temperature: 0.5, // Different from default
|
||||
top_p: 0.8, // Different from default
|
||||
prompt_append: "custom prompt", // Custom field
|
||||
}
|
||||
|
||||
// #when: Check if config should be deleted
|
||||
const shouldDelete = shouldDeleteAgentConfig(config, "visual-engineering")
|
||||
|
||||
// #then: Should return false (has overrides)
|
||||
expect(shouldDelete).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("migrateConfigFile with backup", () => {
|
||||
const cleanupPaths: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
cleanupPaths.forEach((p) => {
|
||||
try {
|
||||
fs.unlinkSync(p)
|
||||
} catch {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test("creates backup file with timestamp when migration needed", () => {
|
||||
// #given: Config file path and config needing migration
|
||||
const testConfigPath = "/tmp/test-config-migration.json"
|
||||
const testConfigContent = globalThis.JSON.stringify({ agents: { oracle: { model: "openai/gpt-5.2" } } }, null, 2)
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
},
|
||||
}
|
||||
|
||||
fs.writeFileSync(testConfigPath, testConfigContent)
|
||||
cleanupPaths.push(testConfigPath)
|
||||
|
||||
// #when: Migrate config file
|
||||
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
|
||||
|
||||
// #then: Backup file should be created with timestamp
|
||||
expect(needsWrite).toBe(true)
|
||||
|
||||
const dir = path.dirname(testConfigPath)
|
||||
const basename = path.basename(testConfigPath)
|
||||
const files = fs.readdirSync(dir)
|
||||
const backupFiles = files.filter((f) => f.startsWith(`${basename}.bak.`))
|
||||
expect(backupFiles.length).toBeGreaterThan(0)
|
||||
|
||||
const backupFile = backupFiles[0]
|
||||
const backupPath = path.join(dir, backupFile)
|
||||
cleanupPaths.push(backupPath)
|
||||
|
||||
expect(backupFile).toMatch(/test-config-migration\.json\.bak\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}/)
|
||||
|
||||
const backupContent = fs.readFileSync(backupPath, "utf-8")
|
||||
expect(backupContent).toBe(testConfigContent)
|
||||
})
|
||||
|
||||
test("deletes agent config when all fields match category defaults", () => {
|
||||
// #given: Config with agent matching category defaults
|
||||
const testConfigPath = "/tmp/test-config-delete.json"
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
oracle: {
|
||||
model: "openai/gpt-5.2",
|
||||
temperature: 0.1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fs.writeFileSync(testConfigPath, globalThis.JSON.stringify({ agents: { oracle: { model: "openai/gpt-5.2" } } }, null, 2))
|
||||
cleanupPaths.push(testConfigPath)
|
||||
|
||||
// #when: Migrate config file
|
||||
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
|
||||
|
||||
// #then: Agent should be deleted (matches strategic category defaults)
|
||||
expect(needsWrite).toBe(true)
|
||||
|
||||
const migratedConfig = JSON.parse(fs.readFileSync(testConfigPath, "utf-8"))
|
||||
expect(migratedConfig.agents).toEqual({})
|
||||
|
||||
const dir = path.dirname(testConfigPath)
|
||||
const basename = path.basename(testConfigPath)
|
||||
const files = fs.readdirSync(dir)
|
||||
const backupFiles = files.filter((f) => f.startsWith(`${basename}.bak.`))
|
||||
backupFiles.forEach((f) => cleanupPaths.push(path.join(dir, f)))
|
||||
})
|
||||
|
||||
test("keeps agent config with category when fields differ from defaults", () => {
|
||||
// #given: Config with agent having custom temperature override
|
||||
const testConfigPath = "/tmp/test-config-keep.json"
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
oracle: {
|
||||
model: "openai/gpt-5.2",
|
||||
temperature: 0.5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fs.writeFileSync(testConfigPath, globalThis.JSON.stringify({ agents: { oracle: { model: "openai/gpt-5.2" } } }, null, 2))
|
||||
cleanupPaths.push(testConfigPath)
|
||||
|
||||
// #when: Migrate config file
|
||||
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
|
||||
|
||||
// #then: Agent should be kept with category and custom override
|
||||
expect(needsWrite).toBe(true)
|
||||
|
||||
const migratedConfig = JSON.parse(fs.readFileSync(testConfigPath, "utf-8"))
|
||||
const agents = migratedConfig.agents as Record<string, unknown>
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect((agents.oracle as Record<string, unknown>).category).toBe("high-iq")
|
||||
expect((agents.oracle as Record<string, unknown>).temperature).toBe(0.5)
|
||||
expect((agents.oracle as Record<string, unknown>).model).toBeUndefined()
|
||||
|
||||
const dir = path.dirname(testConfigPath)
|
||||
const basename = path.basename(testConfigPath)
|
||||
const files = fs.readdirSync(dir)
|
||||
const backupFiles = files.filter((f) => f.startsWith(`${basename}.bak.`))
|
||||
backupFiles.forEach((f) => cleanupPaths.push(path.join(dir, f)))
|
||||
})
|
||||
|
||||
test("does not write when no migration needed", () => {
|
||||
// #given: Config with no migrations needed
|
||||
const testConfigPath = "/tmp/test-config-no-migration.json"
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
Sisyphus: { model: "test" },
|
||||
},
|
||||
}
|
||||
|
||||
fs.writeFileSync(testConfigPath, globalThis.JSON.stringify({ agents: { Sisyphus: { model: "test" } } }, null, 2))
|
||||
cleanupPaths.push(testConfigPath)
|
||||
|
||||
// #when: Migrate config file
|
||||
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
|
||||
|
||||
// #then: Should not write or create backup
|
||||
expect(needsWrite).toBe(false)
|
||||
|
||||
const dir = path.dirname(testConfigPath)
|
||||
const basename = path.basename(testConfigPath)
|
||||
const files = fs.readdirSync(dir)
|
||||
const backupFiles = files.filter((f) => f.startsWith(`${basename}.bak.`))
|
||||
expect(backupFiles.length).toBe(0)
|
||||
})
|
||||
|
||||
test("handles multiple agent migrations correctly", () => {
|
||||
// #given: Config with multiple agents needing migration
|
||||
const testConfigPath = "/tmp/test-config-multi-agent.json"
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
librarian: { model: "anthropic/claude-sonnet-4-5" },
|
||||
frontend: {
|
||||
model: "google/gemini-3-pro-preview",
|
||||
temperature: 0.9,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
testConfigPath,
|
||||
globalThis.JSON.stringify(
|
||||
{
|
||||
agents: {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
librarian: { model: "anthropic/claude-sonnet-4-5" },
|
||||
frontend: { model: "google/gemini-3-pro-preview" },
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
cleanupPaths.push(testConfigPath)
|
||||
|
||||
// #when: Migrate config file
|
||||
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
|
||||
|
||||
// #then: Should migrate correctly
|
||||
expect(needsWrite).toBe(true)
|
||||
|
||||
const migratedConfig = JSON.parse(fs.readFileSync(testConfigPath, "utf-8"))
|
||||
const agents = migratedConfig.agents as Record<string, unknown>
|
||||
|
||||
expect(agents.oracle).toBeUndefined()
|
||||
expect(agents.librarian).toBeUndefined()
|
||||
|
||||
expect(agents.frontend).toBeDefined()
|
||||
expect((agents.frontend as Record<string, unknown>).category).toBe("visual-engineering")
|
||||
expect((agents.frontend as Record<string, unknown>).temperature).toBe(0.9)
|
||||
|
||||
const dir = path.dirname(testConfigPath)
|
||||
const basename = path.basename(testConfigPath)
|
||||
const files = fs.readdirSync(dir)
|
||||
const backupFiles = files.filter((f) => f.startsWith(`${basename}.bak.`))
|
||||
backupFiles.forEach((f) => cleanupPaths.push(path.join(dir, f)))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,14 +3,16 @@ import { log } from "./logger"
|
||||
|
||||
// Migration map: old keys → new keys (for backward compatibility)
|
||||
export const AGENT_NAME_MAP: Record<string, string> = {
|
||||
// Legacy names (backward compatibility)
|
||||
omo: "Sisyphus",
|
||||
"OmO": "Sisyphus",
|
||||
"OmO-Plan": "Planner-Sisyphus",
|
||||
"omo-plan": "Planner-Sisyphus",
|
||||
// Current names
|
||||
sisyphus: "Sisyphus",
|
||||
"planner-sisyphus": "Planner-Sisyphus",
|
||||
"OmO-Plan": "Prometheus (Planner)",
|
||||
"omo-plan": "Prometheus (Planner)",
|
||||
"Planner-Sisyphus": "Prometheus (Planner)",
|
||||
"planner-sisyphus": "Prometheus (Planner)",
|
||||
prometheus: "Prometheus (Planner)",
|
||||
"plan-consultant": "Metis (Plan Consultant)",
|
||||
metis: "Metis (Plan Consultant)",
|
||||
build: "build",
|
||||
oracle: "oracle",
|
||||
librarian: "librarian",
|
||||
@@ -26,6 +28,15 @@ export const HOOK_NAME_MAP: Record<string, string> = {
|
||||
"anthropic-auto-compact": "anthropic-context-window-limit-recovery",
|
||||
}
|
||||
|
||||
// Model to category mapping for auto-migration
|
||||
export const MODEL_TO_CATEGORY_MAP: Record<string, string> = {
|
||||
"google/gemini-3-pro-preview": "visual-engineering",
|
||||
"openai/gpt-5.2": "high-iq",
|
||||
"anthropic/claude-haiku-4-5": "quick",
|
||||
"anthropic/claude-opus-4-5": "most-capable",
|
||||
"anthropic/claude-sonnet-4-5": "general",
|
||||
}
|
||||
|
||||
export function migrateAgentNames(agents: Record<string, unknown>): { migrated: Record<string, unknown>; changed: boolean } {
|
||||
const migrated: Record<string, unknown> = {}
|
||||
let changed = false
|
||||
@@ -56,6 +67,45 @@ export function migrateHookNames(hooks: string[]): { migrated: string[]; changed
|
||||
return { migrated, changed }
|
||||
}
|
||||
|
||||
export function migrateAgentConfigToCategory(config: Record<string, unknown>): {
|
||||
migrated: Record<string, unknown>
|
||||
changed: boolean
|
||||
} {
|
||||
const { model, ...rest } = config
|
||||
if (typeof model !== "string") {
|
||||
return { migrated: config, changed: false }
|
||||
}
|
||||
|
||||
const category = MODEL_TO_CATEGORY_MAP[model]
|
||||
if (!category) {
|
||||
return { migrated: config, changed: false }
|
||||
}
|
||||
|
||||
return {
|
||||
migrated: { category, ...rest },
|
||||
changed: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldDeleteAgentConfig(
|
||||
config: Record<string, unknown>,
|
||||
category: string
|
||||
): boolean {
|
||||
const { DEFAULT_CATEGORIES } = require("../tools/sisyphus-task/constants")
|
||||
const defaults = DEFAULT_CATEGORIES[category]
|
||||
if (!defaults) return false
|
||||
|
||||
const keys = Object.keys(config).filter((k) => k !== "category")
|
||||
if (keys.length === 0) return true
|
||||
|
||||
for (const key of keys) {
|
||||
if (config[key] !== (defaults as Record<string, unknown>)[key]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function migrateConfigFile(configPath: string, rawConfig: Record<string, unknown>): boolean {
|
||||
let needsWrite = false
|
||||
|
||||
@@ -67,6 +117,22 @@ export function migrateConfigFile(configPath: string, rawConfig: Record<string,
|
||||
}
|
||||
}
|
||||
|
||||
if (rawConfig.agents && typeof rawConfig.agents === "object") {
|
||||
const agents = rawConfig.agents as Record<string, Record<string, unknown>>
|
||||
for (const [name, config] of Object.entries(agents)) {
|
||||
const { migrated, changed } = migrateAgentConfigToCategory(config)
|
||||
if (changed) {
|
||||
const category = migrated.category as string
|
||||
if (shouldDeleteAgentConfig(migrated, category)) {
|
||||
delete agents[name]
|
||||
} else {
|
||||
agents[name] = migrated
|
||||
}
|
||||
needsWrite = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rawConfig.omo_agent) {
|
||||
rawConfig.sisyphus_agent = rawConfig.omo_agent
|
||||
delete rawConfig.omo_agent
|
||||
@@ -83,8 +149,12 @@ export function migrateConfigFile(configPath: string, rawConfig: Record<string,
|
||||
|
||||
if (needsWrite) {
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
||||
const backupPath = `${configPath}.bak.${timestamp}`
|
||||
fs.copyFileSync(configPath, backupPath)
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n", "utf-8")
|
||||
log(`Migrated config file: ${configPath}`)
|
||||
log(`Migrated config file: ${configPath} (backup: ${backupPath})`)
|
||||
} catch (err) {
|
||||
log(`Failed to write migrated config to ${configPath}:`, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user