|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | + |
| 4 | +// scripts/src/opencode-release.js |
| 5 | +var fs = require("fs"); |
| 6 | +var path = require("path"); |
| 7 | +var { execSync } = require("child_process"); |
| 8 | +function getLatestTag(pluginName = null) { |
| 9 | + try { |
| 10 | + const pattern = pluginName ? `${pluginName}-v*` : "*"; |
| 11 | + const tags = execSync(`git tag -l '${pattern}' --sort=-version:refname`, { encoding: "utf8" }).trim().split("\n").filter(Boolean); |
| 12 | + return tags[0] || null; |
| 13 | + } catch (err) { |
| 14 | + return null; |
| 15 | + } |
| 16 | +} |
| 17 | +function getChangedFiles(since, pathFilter = "") { |
| 18 | + try { |
| 19 | + const cmd = pathFilter ? `git diff --name-only ${since}...HEAD -- ${pathFilter}` : `git diff --name-only ${since}...HEAD`; |
| 20 | + const output = execSync(cmd, { encoding: "utf8" }).trim(); |
| 21 | + return output ? output.split("\n") : []; |
| 22 | + } catch (err) { |
| 23 | + console.error(`Error getting changed files: ${err.message}`); |
| 24 | + return []; |
| 25 | + } |
| 26 | +} |
| 27 | +function parseConventionalCommits(since, pathFilter = "") { |
| 28 | + try { |
| 29 | + const cmd = pathFilter ? `git log ${since}...HEAD --pretty=format:"%s" -- ${pathFilter}` : `git log ${since}...HEAD --pretty=format:"%s"`; |
| 30 | + const output = execSync(cmd, { encoding: "utf8" }).trim(); |
| 31 | + const commits = output ? output.split("\n") : []; |
| 32 | + const categories = { |
| 33 | + features: [], |
| 34 | + fixes: [], |
| 35 | + other: [] |
| 36 | + }; |
| 37 | + commits.forEach((commit) => { |
| 38 | + if (commit.startsWith("feat:") || commit.startsWith("feat(")) { |
| 39 | + categories.features.push(commit.replace(/^feat(\([^)]+\))?:\s*/, "")); |
| 40 | + } else if (commit.startsWith("fix:") || commit.startsWith("fix(")) { |
| 41 | + categories.fixes.push(commit.replace(/^fix(\([^)]+\))?:\s*/, "")); |
| 42 | + } else if (!commit.startsWith("chore:") && !commit.startsWith("chore(")) { |
| 43 | + categories.other.push(commit.replace(/^[a-z]+(\([^)]+\))?:\s*/, "")); |
| 44 | + } |
| 45 | + }); |
| 46 | + return categories; |
| 47 | + } catch (err) { |
| 48 | + console.error(`Error parsing commits: ${err.message}`); |
| 49 | + return { features: [], fixes: [], other: [] }; |
| 50 | + } |
| 51 | +} |
| 52 | +function generateChangelog(pluginName, version, commits) { |
| 53 | + const lines = [ |
| 54 | + `# Changelog - ${pluginName} v${version}`, |
| 55 | + "" |
| 56 | + ]; |
| 57 | + if (commits.features.length > 0) { |
| 58 | + lines.push("## Features"); |
| 59 | + commits.features.forEach((feat) => lines.push(`- ${feat}`)); |
| 60 | + lines.push(""); |
| 61 | + } |
| 62 | + if (commits.fixes.length > 0) { |
| 63 | + lines.push("## Bug Fixes"); |
| 64 | + commits.fixes.forEach((fix) => lines.push(`- ${fix}`)); |
| 65 | + lines.push(""); |
| 66 | + } |
| 67 | + if (commits.other.length > 0) { |
| 68 | + lines.push("## Other Changes"); |
| 69 | + commits.other.forEach((change) => lines.push(`- ${change}`)); |
| 70 | + lines.push(""); |
| 71 | + } |
| 72 | + return lines.join("\n"); |
| 73 | +} |
| 74 | +function generateInstallGuide(pluginName, version) { |
| 75 | + return `# Installation - ${pluginName} v${version} |
| 76 | +
|
| 77 | +## Quick Install |
| 78 | +
|
| 79 | +\`\`\`bash |
| 80 | +# Download and extract to OpenCode plugins directory |
| 81 | +curl -L https://github.com/bitcomplete/bc-llm-skills/releases/download/${pluginName}-v${version}/${pluginName}.zip -o ${pluginName}.zip |
| 82 | +unzip ${pluginName}.zip -d ~/.config/opencode/plugins/ |
| 83 | +rm ${pluginName}.zip |
| 84 | +\`\`\` |
| 85 | +
|
| 86 | +## Manual Install |
| 87 | +
|
| 88 | +1. Download \`${pluginName}.zip\` from this release |
| 89 | +2. Extract to \`~/.config/opencode/plugins/${pluginName}/\` |
| 90 | +3. Restart OpenCode |
| 91 | +
|
| 92 | +## Verify Installation |
| 93 | +
|
| 94 | +After restarting OpenCode, the plugin commands should appear in autocomplete. |
| 95 | +
|
| 96 | +## Platform Notes |
| 97 | +
|
| 98 | +**Linux/macOS**: Default location is \`~/.config/opencode/plugins/\` |
| 99 | +
|
| 100 | +**Windows**: Use \`%USERPROFILE%\\.config\\opencode\\plugins\\\` |
| 101 | +
|
| 102 | +## Compatibility |
| 103 | +
|
| 104 | +This plugin works with both OpenCode and Claude Code. Paths are auto-detected at runtime. |
| 105 | +`; |
| 106 | +} |
| 107 | +function createPluginZip(pluginPath, outputPath, changelog, installGuide) { |
| 108 | + try { |
| 109 | + const tempDir = path.join(process.cwd(), ".tmp-release"); |
| 110 | + const pluginName = path.basename(pluginPath); |
| 111 | + const stagingDir = path.join(tempDir, pluginName); |
| 112 | + if (fs.existsSync(tempDir)) { |
| 113 | + fs.rmSync(tempDir, { recursive: true }); |
| 114 | + } |
| 115 | + fs.mkdirSync(stagingDir, { recursive: true }); |
| 116 | + execSync(`cp -R "${pluginPath}"/* "${stagingDir}/"`, { stdio: "inherit" }); |
| 117 | + fs.writeFileSync(path.join(stagingDir, "CHANGELOG.md"), changelog); |
| 118 | + fs.writeFileSync(path.join(stagingDir, "INSTALL.md"), installGuide); |
| 119 | + const zipFile = path.basename(outputPath); |
| 120 | + execSync(`cd "${tempDir}" && zip -r "${zipFile}" "${pluginName}"`, { stdio: "inherit" }); |
| 121 | + execSync(`mv "${tempDir}/${zipFile}" "${outputPath}"`, { stdio: "inherit" }); |
| 122 | + fs.rmSync(tempDir, { recursive: true }); |
| 123 | + console.log(`\u2713 Created ${outputPath}`); |
| 124 | + } catch (err) { |
| 125 | + console.error(`Error creating zip: ${err.message}`); |
| 126 | + throw err; |
| 127 | + } |
| 128 | +} |
| 129 | +function createGitHubRelease(pluginName, version, zipPath, changelog) { |
| 130 | + try { |
| 131 | + const tag = `${pluginName}-v${version}`; |
| 132 | + const title = `${pluginName} v${version}`; |
| 133 | + const notes = `OpenCode-compatible release of ${pluginName}. |
| 134 | +
|
| 135 | +${changelog} |
| 136 | +
|
| 137 | +## Installation |
| 138 | +
|
| 139 | +Download \`${pluginName}.zip\` and extract to \`~/.config/opencode/plugins/\` |
| 140 | +
|
| 141 | +See INSTALL.md in the zip for detailed instructions. |
| 142 | +`; |
| 143 | + const notesFile = path.join(process.cwd(), `.tmp-notes-${pluginName}.md`); |
| 144 | + fs.writeFileSync(notesFile, notes); |
| 145 | + execSync( |
| 146 | + `gh release create "${tag}" "${zipPath}" --title "${title}" --notes-file "${notesFile}"`, |
| 147 | + { stdio: "inherit" } |
| 148 | + ); |
| 149 | + fs.unlinkSync(notesFile); |
| 150 | + console.log(`\u2713 Created release: ${tag}`); |
| 151 | + } catch (err) { |
| 152 | + console.error(`Error creating GitHub release: ${err.message}`); |
| 153 | + throw err; |
| 154 | + } |
| 155 | +} |
| 156 | +function detectChangedPlugins(marketplacePath) { |
| 157 | + if (!fs.existsSync(marketplacePath)) { |
| 158 | + console.error(`Marketplace file not found: ${marketplacePath}`); |
| 159 | + return []; |
| 160 | + } |
| 161 | + const marketplace = JSON.parse(fs.readFileSync(marketplacePath, "utf8")); |
| 162 | + const changedPlugins = []; |
| 163 | + for (const plugin of marketplace.plugins) { |
| 164 | + const pluginName = plugin.name; |
| 165 | + const pluginPath = path.dirname(plugin.source); |
| 166 | + const lastTag = getLatestTag(pluginName) || getLatestTag(); |
| 167 | + if (!lastTag) { |
| 168 | + console.log(`No previous releases found for ${pluginName}, skipping`); |
| 169 | + continue; |
| 170 | + } |
| 171 | + const changedFiles = getChangedFiles(lastTag, pluginPath); |
| 172 | + if (changedFiles.length > 0) { |
| 173 | + console.log(`\u2713 Changes detected in ${pluginName} (${changedFiles.length} files)`); |
| 174 | + changedPlugins.push({ |
| 175 | + name: pluginName, |
| 176 | + path: pluginPath, |
| 177 | + lastTag, |
| 178 | + changedFiles |
| 179 | + }); |
| 180 | + } |
| 181 | + } |
| 182 | + return changedPlugins; |
| 183 | +} |
| 184 | +function main() { |
| 185 | + const command = process.argv[2]; |
| 186 | + if (command !== "opencode-release") { |
| 187 | + console.log("Usage: node opencode-release.js opencode-release"); |
| 188 | + process.exit(1); |
| 189 | + } |
| 190 | + const marketplacePath = path.join(".claude-plugin", "marketplace.json"); |
| 191 | + const releasesDir = path.join(process.cwd(), ".releases"); |
| 192 | + const version = process.env.RELEASE_VERSION || (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "."); |
| 193 | + console.log("Detecting changed plugins..."); |
| 194 | + const changedPlugins = detectChangedPlugins(marketplacePath); |
| 195 | + if (changedPlugins.length === 0) { |
| 196 | + console.log("No plugins with changes detected"); |
| 197 | + process.exit(0); |
| 198 | + } |
| 199 | + if (!fs.existsSync(releasesDir)) { |
| 200 | + fs.mkdirSync(releasesDir, { recursive: true }); |
| 201 | + } |
| 202 | + let releasesCreated = 0; |
| 203 | + for (const plugin of changedPlugins) { |
| 204 | + console.log(` |
| 205 | +Creating release for ${plugin.name}...`); |
| 206 | + const commits = parseConventionalCommits(plugin.lastTag, plugin.path); |
| 207 | + const changelog = generateChangelog(plugin.name, version, commits); |
| 208 | + const installGuide = generateInstallGuide(plugin.name, version); |
| 209 | + const zipPath = path.join(releasesDir, `${plugin.name}.zip`); |
| 210 | + createPluginZip(plugin.path, zipPath, changelog, installGuide); |
| 211 | + createGitHubRelease(plugin.name, version, zipPath, changelog); |
| 212 | + releasesCreated++; |
| 213 | + } |
| 214 | + console.log(` |
| 215 | +\u2713 Created ${releasesCreated} OpenCode release(s)`); |
| 216 | + if (process.env.GITHUB_OUTPUT) { |
| 217 | + fs.appendFileSync(process.env.GITHUB_OUTPUT, `count=${releasesCreated} |
| 218 | +`); |
| 219 | + } |
| 220 | +} |
| 221 | +if (require.main === module) { |
| 222 | + main(); |
| 223 | +} |
| 224 | +module.exports = { |
| 225 | + getLatestTag, |
| 226 | + getChangedFiles, |
| 227 | + parseConventionalCommits, |
| 228 | + generateChangelog, |
| 229 | + generateInstallGuide, |
| 230 | + createPluginZip, |
| 231 | + createGitHubRelease, |
| 232 | + detectChangedPlugins |
| 233 | +}; |
0 commit comments