( outputRoot: string, bundle: OpenCodeBundle, scope?: string, )
| 61 | } |
| 62 | |
| 63 | export async function writeOpenCodeBundle( |
| 64 | outputRoot: string, |
| 65 | bundle: OpenCodeBundle, |
| 66 | scope?: string, |
| 67 | ): Promise<void> { |
| 68 | const pluginName = bundle.pluginName ? sanitizeManagedPluginName(bundle.pluginName) : undefined |
| 69 | const openCodePaths = resolveOpenCodePaths(outputRoot, pluginName, scope) |
| 70 | const manifest = pluginName |
| 71 | ? await readManagedInstallManifestWithLegacyFallback(openCodePaths.managedDir, pluginName) |
| 72 | : null |
| 73 | const currentAgents = bundle.agents.map((agent) => `${sanitizePathName(agent.name)}.md`) |
| 74 | const currentCommands = bundle.commandFiles.map((commandFile) => `${commandNameToRelativePath(commandFile.name)}.md`) |
| 75 | const currentPlugins = bundle.plugins.map((plugin) => plugin.name) |
| 76 | const currentSkills = bundle.skillDirs.map((skill) => sanitizePathName(skill.name)) |
| 77 | |
| 78 | await ensureDir(openCodePaths.root) |
| 79 | await cleanupRemovedManagedFiles(openCodePaths.agentsDir, manifest, "agents", currentAgents) |
| 80 | await cleanupRemovedManagedFiles(openCodePaths.commandDir, manifest, "commands", currentCommands) |
| 81 | await cleanupRemovedManagedFiles(openCodePaths.pluginsDir, manifest, "plugins", currentPlugins) |
| 82 | await cleanupRemovedManagedDirectories(openCodePaths.skillsDir, manifest, "skills", currentSkills) |
| 83 | |
| 84 | const hadExistingConfig = await pathExists(openCodePaths.configPath) |
| 85 | const backupPath = await backupFile(openCodePaths.configPath) |
| 86 | if (backupPath) { |
| 87 | console.log(`Backed up existing config to ${backupPath}`) |
| 88 | } |
| 89 | const merged = await mergeOpenCodeConfig(openCodePaths.configPath, bundle.config) |
| 90 | await writeJson(openCodePaths.configPath, merged) |
| 91 | if (hadExistingConfig) { |
| 92 | console.log("Merged plugin config into existing opencode.json (user settings preserved)") |
| 93 | } |
| 94 | |
| 95 | const seenAgents = new Set<string>() |
| 96 | for (const agent of bundle.agents) { |
| 97 | const safeName = sanitizePathName(agent.name) |
| 98 | if (seenAgents.has(safeName)) { |
| 99 | console.warn(`Skipping agent "${agent.name}": sanitized name "${safeName}" collides with another agent`) |
| 100 | continue |
| 101 | } |
| 102 | seenAgents.add(safeName) |
| 103 | await writeText(path.join(openCodePaths.agentsDir, `${safeName}.md`), agent.content + "\n") |
| 104 | } |
| 105 | |
| 106 | for (const commandFile of bundle.commandFiles) { |
| 107 | const dest = path.join(openCodePaths.commandDir, ...commandNameToRelativePath(commandFile.name).split("/")) + ".md" |
| 108 | const cmdBackupPath = await backupFile(dest) |
| 109 | if (cmdBackupPath) { |
| 110 | console.log(`Backed up existing command file to ${cmdBackupPath}`) |
| 111 | } |
| 112 | await writeText(dest, commandFile.content + "\n") |
| 113 | } |
| 114 | |
| 115 | if (bundle.plugins.length > 0) { |
| 116 | for (const plugin of bundle.plugins) { |
| 117 | await writeText(path.join(openCodePaths.pluginsDir, plugin.name), plugin.content + "\n") |
| 118 | } |
| 119 | } |
| 120 |
no test coverage detected