Bootstrap OmO agents for package installs
This commit is contained in:
@@ -4,10 +4,13 @@ import type { InstallArgs } from "./types"
|
|||||||
import {
|
import {
|
||||||
addPluginToOpenCodeConfig,
|
addPluginToOpenCodeConfig,
|
||||||
detectCurrentConfig,
|
detectCurrentConfig,
|
||||||
|
getConfigDir,
|
||||||
getOpenCodeVersion,
|
getOpenCodeVersion,
|
||||||
isOpenCodeInstalled,
|
isOpenCodeInstalled,
|
||||||
writeOmoConfig,
|
writeOmoConfig,
|
||||||
} from "./config-manager"
|
} from "./config-manager"
|
||||||
|
import { loadPluginConfig } from "../plugin-config"
|
||||||
|
import { syncBootstrapAgents } from "../plugin-handlers/opencode-agent-bootstrap"
|
||||||
import {
|
import {
|
||||||
SYMBOLS,
|
SYMBOLS,
|
||||||
argsToConfig,
|
argsToConfig,
|
||||||
@@ -124,6 +127,14 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
|||||||
}
|
}
|
||||||
printSuccess(`Config written ${SYMBOLS.arrow} ${color.dim(omoResult.configPath)}`)
|
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")
|
printBox(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete")
|
||||||
|
|
||||||
if (!config.hasClaude) {
|
if (!config.hasClaude) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export type { ConfigContext } from "./config-manager/config-context"
|
|||||||
export {
|
export {
|
||||||
initConfigContext,
|
initConfigContext,
|
||||||
getConfigContext,
|
getConfigContext,
|
||||||
|
getConfigDir,
|
||||||
resetConfigContext,
|
resetConfigContext,
|
||||||
} from "./config-manager/config-context"
|
} from "./config-manager/config-context"
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import type { InstallArgs } from "./types"
|
|||||||
import {
|
import {
|
||||||
addPluginToOpenCodeConfig,
|
addPluginToOpenCodeConfig,
|
||||||
detectCurrentConfig,
|
detectCurrentConfig,
|
||||||
|
getConfigDir,
|
||||||
getOpenCodeVersion,
|
getOpenCodeVersion,
|
||||||
isOpenCodeInstalled,
|
isOpenCodeInstalled,
|
||||||
writeOmoConfig,
|
writeOmoConfig,
|
||||||
} from "./config-manager"
|
} from "./config-manager"
|
||||||
|
import { loadPluginConfig } from "../plugin-config"
|
||||||
|
import { syncBootstrapAgents } from "../plugin-handlers/opencode-agent-bootstrap"
|
||||||
import { detectedToInitialValues, formatConfigSummary, SYMBOLS } from "./install-validators"
|
import { detectedToInitialValues, formatConfigSummary, SYMBOLS } from "./install-validators"
|
||||||
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
|
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
|
||||||
import { promptInstallConfig } from "./tui-install-prompts"
|
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.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) {
|
if (!config.hasClaude) {
|
||||||
p.log.info(
|
p.log.info(
|
||||||
`${color.bold("Note:")} Sisyphus agent performs best with Claude Opus 4.5+.\n` +
|
`${color.bold("Note:")} Sisyphus agent performs best with Claude Opus 4.5+.\n` +
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shar
|
|||||||
import { startBackgroundCheck as startTmuxCheck } from "./tools/interactive-bash"
|
import { startBackgroundCheck as startTmuxCheck } from "./tools/interactive-bash"
|
||||||
import { lspManager } from "./tools/lsp/client"
|
import { lspManager } from "./tools/lsp/client"
|
||||||
import { createPluginPostHog, getPostHogDistinctId } from "./shared/posthog"
|
import { createPluginPostHog, getPostHogDistinctId } from "./shared/posthog"
|
||||||
|
import { syncBootstrapAgents } from "./plugin-handlers/opencode-agent-bootstrap"
|
||||||
|
|
||||||
let activePluginDispose: PluginDispose | null = null
|
let activePluginDispose: PluginDispose | null = null
|
||||||
|
|
||||||
@@ -39,6 +40,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
|
|
||||||
const pluginConfig = loadPluginConfig(ctx.directory, 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 posthog = createPluginPostHog()
|
||||||
const distinctId = getPostHogDistinctId()
|
const distinctId = getPostHogDistinctId()
|
||||||
try {
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user