Bootstrap OmO agents for package installs

This commit is contained in:
YeonGyu-Kim
2026-04-13 02:03:48 +09:00
parent 3d2eb6e471
commit ec404fa117
6 changed files with 236 additions and 0 deletions
+11
View File
@@ -4,10 +4,13 @@ import type { InstallArgs } from "./types"
import {
addPluginToOpenCodeConfig,
detectCurrentConfig,
getConfigDir,
getOpenCodeVersion,
isOpenCodeInstalled,
writeOmoConfig,
} from "./config-manager"
import { loadPluginConfig } from "../plugin-config"
import { syncBootstrapAgents } from "../plugin-handlers/opencode-agent-bootstrap"
import {
SYMBOLS,
argsToConfig,
@@ -124,6 +127,14 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
}
printSuccess(`Config written ${SYMBOLS.arrow} ${color.dim(omoResult.configPath)}`)
const pluginConfig = loadPluginConfig(getConfigDir(), {})
const bootstrapResult = await syncBootstrapAgents({
directory: getConfigDir(),
targetDir: getConfigDir(),
pluginConfig,
})
printSuccess(`Bootstrapped ${bootstrapResult.agentCount} OpenCode agent files`)
printBox(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete")
if (!config.hasClaude) {
+1
View File
@@ -2,6 +2,7 @@ export type { ConfigContext } from "./config-manager/config-context"
export {
initConfigContext,
getConfigContext,
getConfigDir,
resetConfigContext,
} from "./config-manager/config-context"
+12
View File
@@ -5,10 +5,13 @@ import type { InstallArgs } from "./types"
import {
addPluginToOpenCodeConfig,
detectCurrentConfig,
getConfigDir,
getOpenCodeVersion,
isOpenCodeInstalled,
writeOmoConfig,
} from "./config-manager"
import { loadPluginConfig } from "../plugin-config"
import { syncBootstrapAgents } from "../plugin-handlers/opencode-agent-bootstrap"
import { detectedToInitialValues, formatConfigSummary, SYMBOLS } from "./install-validators"
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
import { promptInstallConfig } from "./tui-install-prompts"
@@ -70,6 +73,15 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
}
spinner.stop(`Config written to ${color.cyan(omoResult.configPath)}`)
spinner.start("Bootstrapping OpenCode agent files")
const pluginConfig = loadPluginConfig(getConfigDir(), {})
const bootstrapResult = await syncBootstrapAgents({
directory: getConfigDir(),
targetDir: getConfigDir(),
pluginConfig,
})
spinner.stop(`Bootstrapped ${bootstrapResult.agentCount} OpenCode agent files`)
if (!config.hasClaude) {
p.log.info(
`${color.bold("Note:")} Sisyphus agent performs best with Claude Opus 4.5+.\n` +
+8
View File
@@ -19,6 +19,7 @@ import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shar
import { startBackgroundCheck as startTmuxCheck } from "./tools/interactive-bash"
import { lspManager } from "./tools/lsp/client"
import { createPluginPostHog, getPostHogDistinctId } from "./shared/posthog"
import { syncBootstrapAgents } from "./plugin-handlers/opencode-agent-bootstrap"
let activePluginDispose: PluginDispose | null = null
@@ -39,6 +40,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const pluginConfig = loadPluginConfig(ctx.directory, ctx)
syncBootstrapAgents({
directory: ctx.directory,
pluginConfig,
}).catch((error) => {
log("[OhMyOpenCodePlugin] bootstrap agent sync failed", { error: String(error) })
})
const posthog = createPluginPostHog()
const distinctId = getPostHogDistinctId()
try {
@@ -0,0 +1,31 @@
/// <reference types="bun-types" />
import { describe, expect, test } from "bun:test"
import { buildBootstrapAgentFiles } from "./opencode-agent-bootstrap"
describe("buildBootstrapAgentFiles", () => {
test("keeps ZWSP runtime names while writing plain-key agent files", () => {
const files = buildBootstrapAgentFiles({
"\u200BSisyphus - Ultraworker": {
name: "\u200BSisyphus - Ultraworker",
mode: "primary",
model: "openai/gpt-5.4",
topP: 0.9,
prompt: "sisyphus prompt",
},
build: {
name: "build",
mode: "primary",
prompt: "build prompt",
},
})
expect(files).toHaveLength(1)
expect(files[0]?.fileName).toBe("sisyphus.md")
expect(files[0]?.content).toContain("name: \u200BSisyphus - Ultraworker")
expect(files[0]?.content).toContain("top_p: 0.9")
expect(files[0]?.content).toContain("sisyphus prompt")
expect(files[0]?.content).not.toContain("build prompt")
})
})
@@ -0,0 +1,173 @@
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import yaml from "js-yaml"
import type { OhMyOpenCodeConfig } from "../config"
import { detectPluginConfigFile, getOpenCodeConfigDir, parseJsonc } from "../shared"
import { getAgentConfigKey } from "../shared/agent-display-names"
import { applyAgentConfig } from "./agent-config-handler"
import { applyToolConfig } from "./tool-config-handler"
const MANIFEST_FILE_NAME = ".oh-my-openagent-agent-bootstrap.json"
const AGENT_DIRECTORY_NAME = "agent"
const EXCLUDED_AGENT_KEYS = new Set(["build", "plan"])
type PluginComponents = {
commands: Record<string, unknown>
skills: Record<string, unknown>
agents: Record<string, unknown>
mcpServers: Record<string, unknown>
hooksConfigs: Array<{ hooks?: Record<string, unknown> }>
plugins: Array<{ name: string; version: string }>
errors: Array<{ pluginKey: string; installPath: string; error: string }>
}
type AgentFileEntry = {
fileName: string
content: string
}
type AgentManifest = {
files: string[]
}
function createEmptyPluginComponents(): PluginComponents {
return {
commands: {},
skills: {},
agents: {},
mcpServers: {},
hooksConfigs: [],
plugins: [],
errors: [],
}
}
function normalizeFrontmatterValue(value: Record<string, unknown>): Record<string, unknown> {
const normalized = { ...value }
if ("prompt" in normalized) {
delete normalized.prompt
}
if ("topP" in normalized && normalized.topP !== undefined) {
normalized["top_p"] = normalized.topP
delete normalized.topP
}
return normalized
}
export function buildBootstrapAgentFiles(agentResult: Record<string, unknown>): AgentFileEntry[] {
const files: AgentFileEntry[] = []
for (const [runtimeKey, agentConfig] of Object.entries(agentResult)) {
if (!agentConfig || typeof agentConfig !== "object") {
continue
}
const configKey = getAgentConfigKey(runtimeKey)
if (!configKey || EXCLUDED_AGENT_KEYS.has(configKey)) {
continue
}
const normalized = normalizeFrontmatterValue(agentConfig as Record<string, unknown>)
const prompt = typeof (agentConfig as Record<string, unknown>).prompt === "string"
? ((agentConfig as Record<string, unknown>).prompt as string)
: ""
const frontmatter = yaml.dump(normalized, {
lineWidth: 1000,
noRefs: true,
sortKeys: true,
}).trim()
files.push({
fileName: `${configKey}.md`,
content: `---\n${frontmatter}\n---\n${prompt}\n`,
})
}
return files
}
function readManifest(manifestPath: string): AgentManifest {
if (!existsSync(manifestPath)) {
return { files: [] }
}
try {
const content = readFileSync(manifestPath, "utf-8")
const parsed = parseJsonc<AgentManifest>(content)
if (Array.isArray(parsed?.files)) {
return { files: parsed.files.filter((file) => typeof file === "string") }
}
} catch {
// Ignore malformed manifests and rebuild from scratch.
}
return { files: [] }
}
function writeManifest(manifestPath: string, files: string[]): void {
writeFileSync(manifestPath, `${JSON.stringify({ files }, null, 2)}\n`, "utf-8")
}
export function resolveBootstrapTargetDir(directory: string): string {
const projectPluginConfig = detectPluginConfigFile(join(directory, ".opencode"))
if (projectPluginConfig.format !== "none") {
return join(directory, ".opencode")
}
return getOpenCodeConfigDir({ binary: "opencode" })
}
export async function syncBootstrapAgents(params: {
directory: string
pluginConfig: OhMyOpenCodeConfig
targetDir?: string
}): Promise<{ agentCount: number; targetDir: string }> {
const targetDir = params.targetDir ?? resolveBootstrapTargetDir(params.directory)
const config: Record<string, unknown> = { agent: {} }
const agentResult = await applyAgentConfig({
config,
pluginConfig: params.pluginConfig,
ctx: { directory: params.directory },
pluginComponents: createEmptyPluginComponents(),
})
applyToolConfig({
config,
pluginConfig: params.pluginConfig,
agentResult,
})
const files = buildBootstrapAgentFiles(agentResult)
const agentDir = join(targetDir, AGENT_DIRECTORY_NAME)
const manifestPath = join(targetDir, MANIFEST_FILE_NAME)
const previousManifest = readManifest(manifestPath)
mkdirSync(agentDir, { recursive: true })
const nextFileNames = new Set(files.map((file) => file.fileName))
for (const previousFile of previousManifest.files) {
if (nextFileNames.has(previousFile)) {
continue
}
rmSync(join(agentDir, previousFile), { force: true })
}
for (const file of files) {
writeFileSync(join(agentDir, file.fileName), file.content, "utf-8")
}
writeManifest(manifestPath, files.map((file) => file.fileName))
return {
agentCount: files.length,
targetDir,
}
}