Vendor opencode source for docker build
This commit is contained in:
147
opencode/script/beta.ts
Executable file
147
opencode/script/beta.ts
Executable file
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
|
||||
interface PR {
|
||||
number: number
|
||||
title: string
|
||||
author: { login: string }
|
||||
labels: Array<{ name: string }>
|
||||
}
|
||||
|
||||
interface FailedPR {
|
||||
number: number
|
||||
title: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
async function commentOnPR(prNumber: number, reason: string) {
|
||||
const body = `⚠️ **Blocking Beta Release**
|
||||
|
||||
This PR cannot be merged into the beta branch due to: **${reason}**
|
||||
|
||||
Please resolve this issue to include this PR in the next beta release.`
|
||||
|
||||
try {
|
||||
await $`gh pr comment ${prNumber} --body ${body}`
|
||||
console.log(` Posted comment on PR #${prNumber}`)
|
||||
} catch (err) {
|
||||
console.log(` Failed to post comment on PR #${prNumber}: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Fetching open PRs with beta label...")
|
||||
|
||||
const stdout = await $`gh pr list --state open --label beta --json number,title,author,labels --limit 100`.text()
|
||||
const prs: PR[] = JSON.parse(stdout)
|
||||
|
||||
console.log(`Found ${prs.length} open PRs with beta label`)
|
||||
|
||||
if (prs.length === 0) {
|
||||
console.log("No team PRs to merge")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Fetching latest dev branch...")
|
||||
await $`git fetch origin dev`
|
||||
|
||||
console.log("Checking out beta branch...")
|
||||
await $`git checkout -B beta origin/dev`
|
||||
|
||||
const applied: number[] = []
|
||||
const failed: FailedPR[] = []
|
||||
|
||||
for (const pr of prs) {
|
||||
console.log(`\nProcessing PR #${pr.number}: ${pr.title}`)
|
||||
|
||||
console.log(" Fetching PR head...")
|
||||
try {
|
||||
await $`git fetch origin pull/${pr.number}/head:pr/${pr.number}`
|
||||
} catch (err) {
|
||||
console.log(` Failed to fetch: ${err}`)
|
||||
failed.push({ number: pr.number, title: pr.title, reason: "Fetch failed" })
|
||||
await commentOnPR(pr.number, "Fetch failed")
|
||||
continue
|
||||
}
|
||||
|
||||
console.log(" Merging...")
|
||||
try {
|
||||
await $`git merge --no-commit --no-ff pr/${pr.number}`
|
||||
} catch {
|
||||
console.log(" Failed to merge (conflicts)")
|
||||
try {
|
||||
await $`git merge --abort`
|
||||
} catch {}
|
||||
try {
|
||||
await $`git checkout -- .`
|
||||
} catch {}
|
||||
try {
|
||||
await $`git clean -fd`
|
||||
} catch {}
|
||||
failed.push({ number: pr.number, title: pr.title, reason: "Merge conflicts" })
|
||||
await commentOnPR(pr.number, "Merge conflicts with dev branch")
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
await $`git rev-parse -q --verify MERGE_HEAD`.text()
|
||||
} catch {
|
||||
console.log(" No changes, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
await $`git add -A`
|
||||
} catch {
|
||||
console.log(" Failed to stage changes")
|
||||
failed.push({ number: pr.number, title: pr.title, reason: "Staging failed" })
|
||||
await commentOnPR(pr.number, "Failed to stage changes")
|
||||
continue
|
||||
}
|
||||
|
||||
const commitMsg = `Apply PR #${pr.number}: ${pr.title}`
|
||||
try {
|
||||
await $`git commit -m ${commitMsg}`
|
||||
} catch (err) {
|
||||
console.log(` Failed to commit: ${err}`)
|
||||
failed.push({ number: pr.number, title: pr.title, reason: "Commit failed" })
|
||||
await commentOnPR(pr.number, "Failed to commit changes")
|
||||
continue
|
||||
}
|
||||
|
||||
console.log(" Applied successfully")
|
||||
applied.push(pr.number)
|
||||
}
|
||||
|
||||
console.log("\n--- Summary ---")
|
||||
console.log(`Applied: ${applied.length} PRs`)
|
||||
applied.forEach((num) => console.log(` - PR #${num}`))
|
||||
|
||||
if (failed.length > 0) {
|
||||
console.log(`Failed: ${failed.length} PRs`)
|
||||
failed.forEach((f) => console.log(` - PR #${f.number}: ${f.reason}`))
|
||||
throw new Error(`${failed.length} PR(s) failed to merge`)
|
||||
}
|
||||
|
||||
console.log("\nChecking if beta branch has changes...")
|
||||
await $`git fetch origin beta`
|
||||
|
||||
const localTree = await $`git rev-parse beta^{tree}`.text()
|
||||
const remoteTree = await $`git rev-parse origin/beta^{tree}`.text()
|
||||
|
||||
if (localTree.trim() === remoteTree.trim()) {
|
||||
console.log("Beta branch has identical contents, no push needed")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Force pushing beta branch...")
|
||||
await $`git push origin beta --force --no-verify`
|
||||
|
||||
console.log("Successfully synced beta branch")
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Error:", err)
|
||||
process.exit(1)
|
||||
})
|
||||
306
opencode/script/changelog.ts
Executable file
306
opencode/script/changelog.ts
Executable file
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import { createOpencode } from "@opencode-ai/sdk/v2"
|
||||
import { parseArgs } from "util"
|
||||
import { Script } from "@opencode-ai/script"
|
||||
|
||||
type Release = {
|
||||
tag_name: string
|
||||
draft: boolean
|
||||
prerelease: boolean
|
||||
}
|
||||
|
||||
export async function getLatestRelease(skip?: string) {
|
||||
const data = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=100").then((res) => {
|
||||
if (!res.ok) throw new Error(res.statusText)
|
||||
return res.json()
|
||||
})
|
||||
|
||||
const releases = data as Release[]
|
||||
const target = skip?.replace(/^v/, "")
|
||||
|
||||
for (const release of releases) {
|
||||
if (release.draft) continue
|
||||
const tag = release.tag_name.replace(/^v/, "")
|
||||
if (target && tag === target) continue
|
||||
return tag
|
||||
}
|
||||
|
||||
throw new Error("No releases found")
|
||||
}
|
||||
|
||||
type Commit = {
|
||||
hash: string
|
||||
author: string | null
|
||||
message: string
|
||||
areas: Set<string>
|
||||
}
|
||||
|
||||
export async function getCommits(from: string, to: string): Promise<Commit[]> {
|
||||
const fromRef = from.startsWith("v") ? from : `v${from}`
|
||||
const toRef = to === "HEAD" ? to : to.startsWith("v") ? to : `v${to}`
|
||||
|
||||
// Get commit data with GitHub usernames from the API
|
||||
const compare =
|
||||
await $`gh api "/repos/anomalyco/opencode/compare/${fromRef}...${toRef}" --jq '.commits[] | {sha: .sha, login: .author.login, message: .commit.message}'`.text()
|
||||
|
||||
const commitData = new Map<string, { login: string | null; message: string }>()
|
||||
for (const line of compare.split("\n").filter(Boolean)) {
|
||||
const data = JSON.parse(line) as { sha: string; login: string | null; message: string }
|
||||
commitData.set(data.sha, { login: data.login, message: data.message.split("\n")[0] ?? "" })
|
||||
}
|
||||
|
||||
// Get commits that touch the relevant packages
|
||||
const log =
|
||||
await $`git log ${fromRef}..${toRef} --oneline --format="%H" -- packages/opencode packages/sdk packages/plugin packages/desktop packages/app sdks/vscode packages/extensions github`.text()
|
||||
const hashes = log.split("\n").filter(Boolean)
|
||||
|
||||
const commits: Commit[] = []
|
||||
for (const hash of hashes) {
|
||||
const data = commitData.get(hash)
|
||||
if (!data) continue
|
||||
|
||||
const message = data.message
|
||||
if (message.match(/^(ignore:|test:|chore:|ci:|release:)/i)) continue
|
||||
|
||||
const files = await $`git diff-tree --no-commit-id --name-only -r ${hash}`.text()
|
||||
const areas = new Set<string>()
|
||||
|
||||
for (const file of files.split("\n").filter(Boolean)) {
|
||||
if (file.startsWith("packages/opencode/src/cli/cmd/")) areas.add("tui")
|
||||
else if (file.startsWith("packages/opencode/")) areas.add("core")
|
||||
else if (file.startsWith("packages/desktop/src-tauri/")) areas.add("tauri")
|
||||
else if (file.startsWith("packages/desktop/")) areas.add("app")
|
||||
else if (file.startsWith("packages/app/")) areas.add("app")
|
||||
else if (file.startsWith("packages/sdk/")) areas.add("sdk")
|
||||
else if (file.startsWith("packages/plugin/")) areas.add("plugin")
|
||||
else if (file.startsWith("packages/extensions/")) areas.add("extensions/zed")
|
||||
else if (file.startsWith("sdks/vscode/")) areas.add("extensions/vscode")
|
||||
else if (file.startsWith("github/")) areas.add("github")
|
||||
}
|
||||
|
||||
if (areas.size === 0) continue
|
||||
|
||||
commits.push({
|
||||
hash: hash.slice(0, 7),
|
||||
author: data.login,
|
||||
message,
|
||||
areas,
|
||||
})
|
||||
}
|
||||
|
||||
return filterRevertedCommits(commits)
|
||||
}
|
||||
|
||||
function filterRevertedCommits(commits: Commit[]): Commit[] {
|
||||
const revertPattern = /^Revert "(.+)"$/
|
||||
const seen = new Map<string, Commit>()
|
||||
|
||||
for (const commit of commits) {
|
||||
const match = commit.message.match(revertPattern)
|
||||
if (match) {
|
||||
// It's a revert - remove the original if we've seen it
|
||||
const original = match[1]!
|
||||
if (seen.has(original)) seen.delete(original)
|
||||
else seen.set(commit.message, commit) // Keep revert if original not in range
|
||||
} else {
|
||||
// Regular commit - remove if its revert exists, otherwise add
|
||||
const revertMsg = `Revert "${commit.message}"`
|
||||
if (seen.has(revertMsg)) seen.delete(revertMsg)
|
||||
else seen.set(commit.message, commit)
|
||||
}
|
||||
}
|
||||
|
||||
return [...seen.values()]
|
||||
}
|
||||
|
||||
const sections = {
|
||||
core: "Core",
|
||||
tui: "TUI",
|
||||
app: "Desktop",
|
||||
tauri: "Desktop",
|
||||
sdk: "SDK",
|
||||
plugin: "SDK",
|
||||
"extensions/zed": "Extensions",
|
||||
"extensions/vscode": "Extensions",
|
||||
github: "Extensions",
|
||||
} as const
|
||||
|
||||
function getSection(areas: Set<string>): string {
|
||||
// Priority order for multi-area commits
|
||||
const priority = ["core", "tui", "app", "tauri", "sdk", "plugin", "extensions/zed", "extensions/vscode", "github"]
|
||||
for (const area of priority) {
|
||||
if (areas.has(area)) return sections[area as keyof typeof sections]
|
||||
}
|
||||
return "Core"
|
||||
}
|
||||
|
||||
async function summarizeCommit(opencode: Awaited<ReturnType<typeof createOpencode>>, message: string): Promise<string> {
|
||||
console.log("summarizing commit:", message)
|
||||
const session = await opencode.client.session.create()
|
||||
const result = await opencode.client.session
|
||||
.prompt(
|
||||
{
|
||||
sessionID: session.data!.id,
|
||||
model: { providerID: "opencode", modelID: "claude-sonnet-4-5" },
|
||||
tools: {
|
||||
"*": false,
|
||||
},
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Summarize this commit message for a changelog entry. Return ONLY a single line summary starting with a capital letter. Be concise but specific. If the commit message is already well-written, just clean it up (capitalize, fix typos, proper grammar). Do not include any prefixes like "fix:" or "feat:".
|
||||
|
||||
Commit: ${message}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
signal: AbortSignal.timeout(120_000),
|
||||
},
|
||||
)
|
||||
.then((x) => x.data?.parts?.find((y) => y.type === "text")?.text ?? message)
|
||||
return result.trim()
|
||||
}
|
||||
|
||||
export async function generateChangelog(commits: Commit[], opencode: Awaited<ReturnType<typeof createOpencode>>) {
|
||||
// Summarize commits in parallel with max 10 concurrent requests
|
||||
const BATCH_SIZE = 10
|
||||
const summaries: string[] = []
|
||||
for (let i = 0; i < commits.length; i += BATCH_SIZE) {
|
||||
const batch = commits.slice(i, i + BATCH_SIZE)
|
||||
const results = await Promise.all(batch.map((c) => summarizeCommit(opencode, c.message)))
|
||||
summaries.push(...results)
|
||||
}
|
||||
|
||||
const grouped = new Map<string, string[]>()
|
||||
for (let i = 0; i < commits.length; i++) {
|
||||
const commit = commits[i]!
|
||||
const section = getSection(commit.areas)
|
||||
const attribution = commit.author && !Script.team.includes(commit.author) ? ` (@${commit.author})` : ""
|
||||
const entry = `- ${summaries[i]}${attribution}`
|
||||
|
||||
if (!grouped.has(section)) grouped.set(section, [])
|
||||
grouped.get(section)!.push(entry)
|
||||
}
|
||||
|
||||
const sectionOrder = ["Core", "TUI", "Desktop", "SDK", "Extensions"]
|
||||
const lines: string[] = []
|
||||
for (const section of sectionOrder) {
|
||||
const entries = grouped.get(section)
|
||||
if (!entries || entries.length === 0) continue
|
||||
lines.push(`## ${section}`)
|
||||
lines.push(...entries)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
export async function getContributors(from: string, to: string) {
|
||||
const fromRef = from.startsWith("v") ? from : `v${from}`
|
||||
const toRef = to === "HEAD" ? to : to.startsWith("v") ? to : `v${to}`
|
||||
const compare =
|
||||
await $`gh api "/repos/anomalyco/opencode/compare/${fromRef}...${toRef}" --jq '.commits[] | {login: .author.login, message: .commit.message}'`.text()
|
||||
const contributors = new Map<string, Set<string>>()
|
||||
|
||||
for (const line of compare.split("\n").filter(Boolean)) {
|
||||
const { login, message } = JSON.parse(line) as { login: string | null; message: string }
|
||||
const title = message.split("\n")[0] ?? ""
|
||||
if (title.match(/^(ignore:|test:|chore:|ci:|release:)/i)) continue
|
||||
|
||||
if (login && !Script.team.includes(login)) {
|
||||
if (!contributors.has(login)) contributors.set(login, new Set())
|
||||
contributors.get(login)!.add(title)
|
||||
}
|
||||
}
|
||||
|
||||
return contributors
|
||||
}
|
||||
|
||||
export async function buildNotes(from: string, to: string) {
|
||||
const commits = await getCommits(from, to)
|
||||
|
||||
if (commits.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
console.log("generating changelog since " + from)
|
||||
|
||||
const opencode = await createOpencode({ port: 0 })
|
||||
const notes: string[] = []
|
||||
|
||||
try {
|
||||
const lines = await generateChangelog(commits, opencode)
|
||||
notes.push(...lines)
|
||||
console.log("---- Generated Changelog ----")
|
||||
console.log(notes.join("\n"))
|
||||
console.log("-----------------------------")
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === "TimeoutError") {
|
||||
console.log("Changelog generation timed out, using raw commits")
|
||||
for (const commit of commits) {
|
||||
const attribution = commit.author && !team.includes(commit.author) ? ` (@${commit.author})` : ""
|
||||
notes.push(`- ${commit.message}${attribution}`)
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
await opencode.server.close()
|
||||
}
|
||||
console.log("changelog generation complete")
|
||||
|
||||
const contributors = await getContributors(from, to)
|
||||
|
||||
if (contributors.size > 0) {
|
||||
notes.push("")
|
||||
notes.push(`**Thank you to ${contributors.size} community contributor${contributors.size > 1 ? "s" : ""}:**`)
|
||||
for (const [username, userCommits] of contributors) {
|
||||
notes.push(`- @${username}:`)
|
||||
for (const c of userCommits) {
|
||||
notes.push(` - ${c}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return notes
|
||||
}
|
||||
|
||||
// CLI entrypoint
|
||||
if (import.meta.main) {
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
from: { type: "string", short: "f" },
|
||||
to: { type: "string", short: "t", default: "HEAD" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
},
|
||||
})
|
||||
|
||||
if (values.help) {
|
||||
console.log(`
|
||||
Usage: bun script/changelog.ts [options]
|
||||
|
||||
Options:
|
||||
-f, --from <version> Starting version (default: latest GitHub release)
|
||||
-t, --to <ref> Ending ref (default: HEAD)
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
bun script/changelog.ts # Latest release to HEAD
|
||||
bun script/changelog.ts --from 1.0.200 # v1.0.200 to HEAD
|
||||
bun script/changelog.ts -f 1.0.200 -t 1.0.205
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const to = values.to!
|
||||
const from = values.from ?? (await getLatestRelease())
|
||||
|
||||
console.log(`Generating changelog: v${from} -> ${to}\n`)
|
||||
|
||||
const notes = await buildNotes(from, to)
|
||||
console.log("\n=== Final Notes ===")
|
||||
console.log(notes.join("\n"))
|
||||
}
|
||||
79
opencode/script/duplicate-pr.ts
Executable file
79
opencode/script/duplicate-pr.ts
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "bun"
|
||||
import { createOpencode } from "@opencode-ai/sdk"
|
||||
import { parseArgs } from "util"
|
||||
|
||||
async function main() {
|
||||
const { values, positionals } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
file: { type: "string", short: "f" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
},
|
||||
allowPositionals: true,
|
||||
})
|
||||
|
||||
if (values.help) {
|
||||
console.log(`
|
||||
Usage: bun script/duplicate-pr.ts [options] <message>
|
||||
|
||||
Options:
|
||||
-f, --file <path> File to attach to the prompt
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
bun script/duplicate-pr.ts -f pr_info.txt "Check the attached file for PR details"
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const message = positionals.join(" ")
|
||||
if (!message) {
|
||||
console.error("Error: message is required")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const opencode = await createOpencode({ port: 0 })
|
||||
|
||||
try {
|
||||
const parts: Array<{ type: "text"; text: string } | { type: "file"; url: string; filename: string; mime: string }> =
|
||||
[]
|
||||
|
||||
if (values.file) {
|
||||
const resolved = path.resolve(process.cwd(), values.file)
|
||||
const file = Bun.file(resolved)
|
||||
if (!(await file.exists())) {
|
||||
console.error(`Error: file not found: ${values.file}`)
|
||||
process.exit(1)
|
||||
}
|
||||
parts.push({
|
||||
type: "file",
|
||||
url: pathToFileURL(resolved).href,
|
||||
filename: path.basename(resolved),
|
||||
mime: "text/plain",
|
||||
})
|
||||
}
|
||||
|
||||
parts.push({ type: "text", text: message })
|
||||
|
||||
const session = await opencode.client.session.create()
|
||||
const result = await opencode.client.session
|
||||
.prompt({
|
||||
path: { id: session.data!.id },
|
||||
body: {
|
||||
agent: "duplicate-pr",
|
||||
parts,
|
||||
},
|
||||
signal: AbortSignal.timeout(120_000),
|
||||
})
|
||||
.then((x) => x.data?.parts?.find((y) => y.type === "text")?.text ?? "")
|
||||
|
||||
console.log(result.trim())
|
||||
} finally {
|
||||
opencode.server.close()
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
5
opencode/script/format.ts
Executable file
5
opencode/script/format.ts
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
|
||||
await $`bun run prettier --ignore-unknown --write .`
|
||||
9
opencode/script/generate.ts
Executable file
9
opencode/script/generate.ts
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
|
||||
await $`bun ./packages/sdk/js/script/build.ts`
|
||||
|
||||
await $`bun dev generate > ../sdk/openapi.json`.cwd("packages/opencode")
|
||||
|
||||
await $`./script/format.ts`
|
||||
19
opencode/script/hooks
Executable file
19
opencode/script/hooks
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ ! -d ".git" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p .git/hooks
|
||||
|
||||
cat > .git/hooks/pre-push << 'EOF'
|
||||
#!/bin/sh
|
||||
# Ensure dependencies are installed before typecheck
|
||||
if command -v bun >/dev/null 2>&1; then
|
||||
bun install >/dev/null 2>&1 || true
|
||||
fi
|
||||
bun run typecheck
|
||||
EOF
|
||||
|
||||
chmod +x .git/hooks/pre-push
|
||||
echo "✅ Pre-push hook installed"
|
||||
79
opencode/script/publish.ts
Executable file
79
opencode/script/publish.ts
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import { Script } from "@opencode-ai/script"
|
||||
|
||||
const highlightsTemplate = `
|
||||
<!--
|
||||
Add highlights before publishing. Delete this section if no highlights.
|
||||
|
||||
- For multiple highlights, use multiple <highlight> tags
|
||||
- Highlights with the same source attribute get grouped together
|
||||
-->
|
||||
|
||||
<!--
|
||||
<highlight source="SourceName (TUI/Desktop/Web/Core)">
|
||||
<h2>Feature title goes here</h2>
|
||||
<p short="Short description used for Desktop Recap">
|
||||
Full description of the feature or change
|
||||
</p>
|
||||
|
||||
https://github.com/user-attachments/assets/uuid-for-video (you will want to drag & drop the video or picture)
|
||||
|
||||
<img
|
||||
width="1912"
|
||||
height="1164"
|
||||
alt="image"
|
||||
src="https://github.com/user-attachments/assets/uuid-for-image"
|
||||
/>
|
||||
</highlight>
|
||||
-->
|
||||
|
||||
`
|
||||
|
||||
console.log("=== publishing ===\n")
|
||||
|
||||
const pkgjsons = await Array.fromAsync(
|
||||
new Bun.Glob("**/package.json").scan({
|
||||
absolute: true,
|
||||
}),
|
||||
).then((arr) => arr.filter((x) => !x.includes("node_modules") && !x.includes("dist")))
|
||||
|
||||
for (const file of pkgjsons) {
|
||||
let pkg = await Bun.file(file).text()
|
||||
pkg = pkg.replaceAll(/"version": "[^"]+"/g, `"version": "${Script.version}"`)
|
||||
console.log("updated:", file)
|
||||
await Bun.file(file).write(pkg)
|
||||
}
|
||||
|
||||
const extensionToml = new URL("../packages/extensions/zed/extension.toml", import.meta.url).pathname
|
||||
let toml = await Bun.file(extensionToml).text()
|
||||
toml = toml.replace(/^version = "[^"]+"/m, `version = "${Script.version}"`)
|
||||
toml = toml.replaceAll(/releases\/download\/v[^/]+\//g, `releases/download/v${Script.version}/`)
|
||||
console.log("updated:", extensionToml)
|
||||
await Bun.file(extensionToml).write(toml)
|
||||
|
||||
await $`bun install`
|
||||
await import(`../packages/sdk/js/script/build.ts`)
|
||||
|
||||
if (Script.release) {
|
||||
await $`git commit -am "release: v${Script.version}"`
|
||||
await $`git tag v${Script.version}`
|
||||
await $`git fetch origin`
|
||||
await $`git cherry-pick HEAD..origin/dev`.nothrow()
|
||||
await $`git push origin HEAD --tags --no-verify --force-with-lease`
|
||||
await new Promise((resolve) => setTimeout(resolve, 5_000))
|
||||
await $`gh release edit v${Script.version} --draft=false`
|
||||
}
|
||||
|
||||
console.log("\n=== cli ===\n")
|
||||
await import(`../packages/opencode/script/publish.ts`)
|
||||
|
||||
console.log("\n=== sdk ===\n")
|
||||
await import(`../packages/sdk/js/script/publish.ts`)
|
||||
|
||||
console.log("\n=== plugin ===\n")
|
||||
await import(`../packages/plugin/script/publish.ts`)
|
||||
|
||||
const dir = new URL("..", import.meta.url).pathname
|
||||
process.chdir(dir)
|
||||
5
opencode/script/release
Executable file
5
opencode/script/release
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BUMP_TYPE=${1:-patch}
|
||||
|
||||
gh workflow run publish.yml -f bump="$BUMP_TYPE"
|
||||
225
opencode/script/stats.ts
Executable file
225
opencode/script/stats.ts
Executable file
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
async function sendToPostHog(event: string, properties: Record<string, any>) {
|
||||
const key = process.env["POSTHOG_KEY"]
|
||||
|
||||
if (!key) {
|
||||
console.warn("POSTHOG_API_KEY not set, skipping PostHog event")
|
||||
return
|
||||
}
|
||||
|
||||
const response = await fetch("https://us.i.posthog.com/i/v0/e/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
distinct_id: "download",
|
||||
api_key: key,
|
||||
event,
|
||||
properties: {
|
||||
...properties,
|
||||
},
|
||||
}),
|
||||
}).catch(() => null)
|
||||
|
||||
if (response && !response.ok) {
|
||||
console.warn(`PostHog API error: ${response.status}`)
|
||||
}
|
||||
}
|
||||
|
||||
interface Asset {
|
||||
name: string
|
||||
download_count: number
|
||||
}
|
||||
|
||||
interface Release {
|
||||
tag_name: string
|
||||
name: string
|
||||
assets: Asset[]
|
||||
}
|
||||
|
||||
interface NpmDownloadsRange {
|
||||
start: string
|
||||
end: string
|
||||
package: string
|
||||
downloads: Array<{
|
||||
downloads: number
|
||||
day: string
|
||||
}>
|
||||
}
|
||||
|
||||
async function fetchNpmDownloads(packageName: string): Promise<number> {
|
||||
try {
|
||||
// Use a range from 2020 to current year + 5 years to ensure it works forever
|
||||
const currentYear = new Date().getFullYear()
|
||||
const endYear = currentYear + 5
|
||||
const response = await fetch(`https://api.npmjs.org/downloads/range/2020-01-01:${endYear}-12-31/${packageName}`)
|
||||
if (!response.ok) {
|
||||
console.warn(`Failed to fetch npm downloads for ${packageName}: ${response.status}`)
|
||||
return 0
|
||||
}
|
||||
const data: NpmDownloadsRange = await response.json()
|
||||
return data.downloads.reduce((total, day) => total + day.downloads, 0)
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching npm downloads for ${packageName}:`, error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchReleases(): Promise<Release[]> {
|
||||
const releases: Release[] = []
|
||||
let page = 1
|
||||
const per = 100
|
||||
|
||||
while (true) {
|
||||
const url = `https://api.github.com/repos/anomalyco/opencode/releases?page=${page}&per_page=${per}`
|
||||
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const batch: Release[] = await response.json()
|
||||
if (batch.length === 0) break
|
||||
|
||||
releases.push(...batch)
|
||||
console.log(`Fetched page ${page} with ${batch.length} releases`)
|
||||
|
||||
if (batch.length < per) break
|
||||
page++
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
|
||||
return releases
|
||||
}
|
||||
|
||||
function calculate(releases: Release[]) {
|
||||
let total = 0
|
||||
const stats = []
|
||||
|
||||
for (const release of releases) {
|
||||
let downloads = 0
|
||||
const assets = []
|
||||
|
||||
for (const asset of release.assets) {
|
||||
downloads += asset.download_count
|
||||
assets.push({
|
||||
name: asset.name,
|
||||
downloads: asset.download_count,
|
||||
})
|
||||
}
|
||||
|
||||
total += downloads
|
||||
stats.push({
|
||||
tag: release.tag_name,
|
||||
name: release.name,
|
||||
downloads,
|
||||
assets,
|
||||
})
|
||||
}
|
||||
|
||||
return { total, stats }
|
||||
}
|
||||
|
||||
async function save(githubTotal: number, npmDownloads: number) {
|
||||
const file = "STATS.md"
|
||||
const date = new Date().toISOString().split("T")[0]
|
||||
const total = githubTotal + npmDownloads
|
||||
|
||||
let previousGithub = 0
|
||||
let previousNpm = 0
|
||||
let previousTotal = 0
|
||||
let content = ""
|
||||
|
||||
try {
|
||||
content = await Bun.file(file).text()
|
||||
const lines = content.trim().split("\n")
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i].trim()
|
||||
if (line.startsWith("|") && !line.includes("Date") && !line.includes("---")) {
|
||||
const match = line.match(
|
||||
/\|\s*[\d-]+\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|/,
|
||||
)
|
||||
if (match) {
|
||||
previousGithub = parseInt(match[1].replace(/,/g, ""))
|
||||
previousNpm = parseInt(match[2].replace(/,/g, ""))
|
||||
previousTotal = parseInt(match[3].replace(/,/g, ""))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
content =
|
||||
"# Download Stats\n\n| Date | GitHub Downloads | npm Downloads | Total |\n|------|------------------|---------------|-------|\n"
|
||||
}
|
||||
|
||||
const githubChange = githubTotal - previousGithub
|
||||
const npmChange = npmDownloads - previousNpm
|
||||
const totalChange = total - previousTotal
|
||||
|
||||
const githubChangeStr =
|
||||
githubChange > 0
|
||||
? ` (+${githubChange.toLocaleString()})`
|
||||
: githubChange < 0
|
||||
? ` (${githubChange.toLocaleString()})`
|
||||
: " (+0)"
|
||||
const npmChangeStr =
|
||||
npmChange > 0 ? ` (+${npmChange.toLocaleString()})` : npmChange < 0 ? ` (${npmChange.toLocaleString()})` : " (+0)"
|
||||
const totalChangeStr =
|
||||
totalChange > 0
|
||||
? ` (+${totalChange.toLocaleString()})`
|
||||
: totalChange < 0
|
||||
? ` (${totalChange.toLocaleString()})`
|
||||
: " (+0)"
|
||||
const line = `| ${date} | ${githubTotal.toLocaleString()}${githubChangeStr} | ${npmDownloads.toLocaleString()}${npmChangeStr} | ${total.toLocaleString()}${totalChangeStr} |\n`
|
||||
|
||||
if (!content.includes("# Download Stats")) {
|
||||
content =
|
||||
"# Download Stats\n\n| Date | GitHub Downloads | npm Downloads | Total |\n|------|------------------|---------------|-------|\n"
|
||||
}
|
||||
|
||||
await Bun.write(file, content + line)
|
||||
await Bun.spawn(["bunx", "prettier", "--write", file]).exited
|
||||
|
||||
console.log(
|
||||
`\nAppended stats to ${file}: GitHub ${githubTotal.toLocaleString()}${githubChangeStr}, npm ${npmDownloads.toLocaleString()}${npmChangeStr}, Total ${total.toLocaleString()}${totalChangeStr}`,
|
||||
)
|
||||
}
|
||||
|
||||
console.log("Fetching GitHub releases for anomalyco/opencode...\n")
|
||||
|
||||
const releases = await fetchReleases()
|
||||
console.log(`\nFetched ${releases.length} releases total\n`)
|
||||
|
||||
const { total: githubTotal, stats } = calculate(releases)
|
||||
|
||||
console.log("Fetching npm all-time downloads for opencode-ai...\n")
|
||||
const npmDownloads = await fetchNpmDownloads("opencode-ai")
|
||||
console.log(`Fetched npm all-time downloads: ${npmDownloads.toLocaleString()}\n`)
|
||||
|
||||
await save(githubTotal, npmDownloads)
|
||||
|
||||
await sendToPostHog("download", {
|
||||
count: githubTotal,
|
||||
source: "github",
|
||||
})
|
||||
|
||||
await sendToPostHog("download", {
|
||||
count: npmDownloads,
|
||||
source: "npm",
|
||||
})
|
||||
|
||||
const totalDownloads = githubTotal + npmDownloads
|
||||
|
||||
console.log("=".repeat(60))
|
||||
console.log(`TOTAL DOWNLOADS: ${totalDownloads.toLocaleString()}`)
|
||||
console.log(` GitHub: ${githubTotal.toLocaleString()}`)
|
||||
console.log(` npm: ${npmDownloads.toLocaleString()}`)
|
||||
console.log("=".repeat(60))
|
||||
|
||||
console.log("-".repeat(60))
|
||||
console.log(`GitHub Total: ${githubTotal.toLocaleString()} downloads across ${releases.length} releases`)
|
||||
console.log(`npm Total: ${npmDownloads.toLocaleString()} downloads`)
|
||||
console.log(`Combined Total: ${totalDownloads.toLocaleString()} downloads`)
|
||||
130
opencode/script/sync-zed.ts
Executable file
130
opencode/script/sync-zed.ts
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import { tmpdir } from "os"
|
||||
import { join } from "path"
|
||||
|
||||
const FORK_REPO = "anomalyco/zed-extensions"
|
||||
const UPSTREAM_REPO = "zed-industries/extensions"
|
||||
const EXTENSION_NAME = "opencode"
|
||||
|
||||
async function main() {
|
||||
const version = process.argv[2]
|
||||
if (!version) throw new Error("Version argument required, ex: bun script/sync-zed.ts v1.0.52")
|
||||
|
||||
const token = process.env.ZED_EXTENSIONS_PAT
|
||||
if (!token) throw new Error("ZED_EXTENSIONS_PAT environment variable required")
|
||||
|
||||
const prToken = process.env.ZED_PR_PAT
|
||||
if (!prToken) throw new Error("ZED_PR_PAT environment variable required")
|
||||
|
||||
const cleanVersion = version.replace(/^v/, "")
|
||||
console.log(`📦 Syncing Zed extension for version ${cleanVersion}`)
|
||||
|
||||
const commitSha = await $`git rev-parse ${version}`.text()
|
||||
const sha = commitSha.trim()
|
||||
console.log(`🔍 Found commit SHA: ${sha}`)
|
||||
|
||||
const extensionToml = await $`git show ${version}:packages/extensions/zed/extension.toml`.text()
|
||||
const parsed = Bun.TOML.parse(extensionToml) as { version: string }
|
||||
const extensionVersion = parsed.version
|
||||
|
||||
if (extensionVersion !== cleanVersion) {
|
||||
throw new Error(`Version mismatch: extension.toml has ${extensionVersion} but tag is ${cleanVersion}`)
|
||||
}
|
||||
console.log(`✅ Version ${extensionVersion} matches tag`)
|
||||
|
||||
// Clone the fork to a temp directory
|
||||
const workDir = join(tmpdir(), `zed-extensions-${Date.now()}`)
|
||||
console.log(`📁 Working in ${workDir}`)
|
||||
|
||||
await $`git clone https://x-access-token:${token}@github.com/${FORK_REPO}.git ${workDir}`
|
||||
process.chdir(workDir)
|
||||
|
||||
// Configure git identity
|
||||
await $`git config user.name "Aiden Cline"`
|
||||
await $`git config user.email "63023139+rekram1-node@users.noreply.github.com "`
|
||||
|
||||
// Sync fork with upstream (force reset to match exactly)
|
||||
console.log(`🔄 Syncing fork with upstream...`)
|
||||
await $`git remote add upstream https://github.com/${UPSTREAM_REPO}.git`
|
||||
await $`git fetch upstream`
|
||||
await $`git checkout main`
|
||||
await $`git reset --hard upstream/main`
|
||||
await $`git push origin main --force`
|
||||
console.log(`✅ Fork synced (force reset to upstream)`)
|
||||
|
||||
// Create a new branch
|
||||
const branchName = `update-${EXTENSION_NAME}-${cleanVersion}`
|
||||
console.log(`🌿 Creating branch ${branchName}`)
|
||||
await $`git checkout -b ${branchName}`
|
||||
|
||||
const submodulePath = `extensions/${EXTENSION_NAME}`
|
||||
console.log(`📌 Updating submodule to commit ${sha}`)
|
||||
await $`git submodule update --init ${submodulePath}`
|
||||
process.chdir(submodulePath)
|
||||
await $`git fetch`
|
||||
await $`git checkout ${sha}`
|
||||
process.chdir(workDir)
|
||||
await $`git add ${submodulePath}`
|
||||
|
||||
console.log(`📝 Updating extensions.toml`)
|
||||
const extensionsTomlPath = "extensions.toml"
|
||||
const extensionsToml = await Bun.file(extensionsTomlPath).text()
|
||||
|
||||
const versionRegex = new RegExp(`(\\[${EXTENSION_NAME}\\][\\s\\S]*?)version = "[^"]+"`)
|
||||
const updatedToml = extensionsToml.replace(versionRegex, `$1version = "${cleanVersion}"`)
|
||||
|
||||
if (updatedToml === extensionsToml) {
|
||||
throw new Error(`Failed to update version in extensions.toml - pattern not found`)
|
||||
}
|
||||
|
||||
await Bun.write(extensionsTomlPath, updatedToml)
|
||||
await $`git add extensions.toml`
|
||||
|
||||
const commitMessage = `Update ${EXTENSION_NAME} to v${cleanVersion}`
|
||||
|
||||
await $`git commit -m ${commitMessage}`
|
||||
console.log(`✅ Changes committed`)
|
||||
|
||||
// Delete any existing branches for opencode updates
|
||||
console.log(`🔍 Checking for existing branches...`)
|
||||
const branches = await $`git ls-remote --heads https://x-access-token:${token}@github.com/${FORK_REPO}.git`.text()
|
||||
const branchPattern = `refs/heads/update-${EXTENSION_NAME}-`
|
||||
const oldBranches = branches
|
||||
.split("\n")
|
||||
.filter((line) => line.includes(branchPattern))
|
||||
.map((line) => line.split("refs/heads/")[1])
|
||||
.filter(Boolean)
|
||||
|
||||
if (oldBranches.length > 0) {
|
||||
console.log(`🗑️ Found ${oldBranches.length} old branch(es), deleting...`)
|
||||
for (const branch of oldBranches) {
|
||||
await $`git push https://x-access-token:${token}@github.com/${FORK_REPO}.git --delete ${branch}`
|
||||
console.log(`✅ Deleted branch ${branch}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🚀 Pushing to fork...`)
|
||||
await $`git push https://x-access-token:${token}@github.com/${FORK_REPO}.git ${branchName}`
|
||||
|
||||
console.log(`📬 Creating pull request...`)
|
||||
const prResult =
|
||||
await $`gh pr create --repo ${UPSTREAM_REPO} --base main --head ${FORK_REPO.split("/")[0]}:${branchName} --title "Update ${EXTENSION_NAME} to v${cleanVersion}" --body "Updating OpenCode extension to v${cleanVersion}"`
|
||||
.env({ ...process.env, GH_TOKEN: prToken })
|
||||
.nothrow()
|
||||
|
||||
if (prResult.exitCode !== 0) {
|
||||
console.error("stderr:", prResult.stderr.toString())
|
||||
throw new Error(`Failed with exit code ${prResult.exitCode}`)
|
||||
}
|
||||
|
||||
const prUrl = prResult.stdout.toString().trim()
|
||||
console.log(`✅ Pull request created: ${prUrl}`)
|
||||
console.log(`🎉 Done!`)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("❌ Error:", err.message)
|
||||
process.exit(1)
|
||||
})
|
||||
26
opencode/script/version.ts
Executable file
26
opencode/script/version.ts
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { Script } from "@opencode-ai/script"
|
||||
import { $ } from "bun"
|
||||
import { buildNotes, getLatestRelease } from "./changelog"
|
||||
|
||||
const output = [`version=${Script.version}`]
|
||||
|
||||
if (!Script.preview) {
|
||||
const previous = await getLatestRelease()
|
||||
const notes = await buildNotes(previous, "HEAD")
|
||||
const body = notes.join("\n") || "No notable changes"
|
||||
const dir = process.env.RUNNER_TEMP ?? "/tmp"
|
||||
const file = `${dir}/opencode-release-notes.txt`
|
||||
await Bun.write(file, body)
|
||||
await $`gh release create v${Script.version} -d --title "v${Script.version}" --notes-file ${file}`
|
||||
const release = await $`gh release view v${Script.version} --json tagName,databaseId`.json()
|
||||
output.push(`release=${release.databaseId}`)
|
||||
output.push(`tag=${release.tagName}`)
|
||||
}
|
||||
|
||||
if (process.env.GITHUB_OUTPUT) {
|
||||
await Bun.write(process.env.GITHUB_OUTPUT, output.join("\n"))
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
Reference in New Issue
Block a user