2
0
Эх сурвалжийг харах

beginning of upgrade command

Dax Raad 8 сар өмнө
parent
commit
38879dee2d

+ 2 - 2
packages/opencode/AGENTS.md

@@ -1,4 +1,4 @@
-# OpenCode Agent Guidelines
+# opencode agent guidelines
 
 ## Build/Test Commands
 
@@ -19,7 +19,7 @@
 
 ## IMPORTANT
 
-- Try to keep things in one function unless composable or reusable
+- Try to keep things in one function unless composable or reusablte
 - DO NOT do unnecessary destructuring of variables
 - DO NOT use else statements unless necessary
 - DO NOT use try catch if it can be avoided

+ 184 - 0
packages/opencode/src/cli/cmd/upgrade.ts

@@ -0,0 +1,184 @@
+import type { Argv } from "yargs"
+import { UI } from "../ui"
+import { VERSION } from "../version"
+import path from "path"
+import fs from "fs/promises"
+import os from "os"
+import * as prompts from "@clack/prompts"
+import { Global } from "../../global"
+
+const API = "https://api.github.com/repos/sst/opencode"
+
+interface Release {
+  tag_name: string
+  name: string
+  assets: Array<{
+    name: string
+    browser_download_url: string
+  }>
+}
+
+function asset(): string {
+  const platform = os.platform()
+  const arch = os.arch()
+
+  if (platform === "darwin") {
+    return arch === "arm64"
+      ? "opencode-darwin-arm64.zip"
+      : "opencode-darwin-x64.zip"
+  }
+  if (platform === "linux") {
+    return arch === "arm64"
+      ? "opencode-linux-arm64.zip"
+      : "opencode-linux-x64.zip"
+  }
+  if (platform === "win32") {
+    return "opencode-windows-x64.zip"
+  }
+
+  throw new Error(`Unsupported platform: ${platform}-${arch}`)
+}
+
+function compare(current: string, latest: string): number {
+  const a = current.replace(/^v/, "")
+  const b = latest.replace(/^v/, "")
+
+  const aParts = a.split(".").map(Number)
+  const bParts = b.split(".").map(Number)
+
+  for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
+    const aPart = aParts[i] || 0
+    const bPart = bParts[i] || 0
+
+    if (aPart < bPart) return -1
+    if (aPart > bPart) return 1
+  }
+
+  return 0
+}
+
+async function latest(): Promise<Release> {
+  const response = await fetch(`${API}/releases/latest`)
+  if (!response.ok) {
+    throw new Error(`Failed to fetch latest release: ${response.statusText}`)
+  }
+  return response.json()
+}
+
+async function specific(version: string): Promise<Release> {
+  const tag = version.startsWith("v") ? version : `v${version}`
+  const response = await fetch(`${API}/releases/tags/${tag}`)
+  if (!response.ok) {
+    throw new Error(`Failed to fetch release ${tag}: ${response.statusText}`)
+  }
+  return response.json()
+}
+
+async function download(url: string): Promise<string> {
+  const response = await fetch(url)
+  if (!response.ok) {
+    throw new Error(`Failed to download: ${response.statusText}`)
+  }
+
+  const buffer = await response.arrayBuffer()
+  const temp = path.join(Global.Path.cache, `opencode-update-${Date.now()}.zip`)
+
+  await Bun.write(temp, buffer)
+
+  const extractDir = path.join(
+    Global.Path.cache,
+    `opencode-extract-${Date.now()}`,
+  )
+  await fs.mkdir(extractDir, { recursive: true })
+
+  const proc = Bun.spawn(["unzip", "-o", temp, "-d", extractDir], {
+    stdout: "pipe",
+    stderr: "pipe",
+  })
+
+  const result = await proc.exited
+  if (result !== 0) {
+    throw new Error("Failed to extract update")
+  }
+
+  await fs.unlink(temp)
+
+  const binary = path.join(extractDir, "opencode")
+  await fs.chmod(binary, 0o755)
+
+  return binary
+}
+
+export const UpgradeCommand = {
+  command: "upgrade [target]",
+  describe: "Upgrade opencode to the latest version or a specific version",
+  builder: (yargs: Argv) => {
+    return yargs.positional("target", {
+      describe: "Specific version to upgrade to (e.g., '0.1.48' or 'v0.1.48')",
+      type: "string",
+    })
+  },
+  handler: async (args: { target?: string }) => {
+    UI.empty()
+    UI.println(UI.logo("  "))
+    UI.empty()
+    prompts.intro("upgrade")
+
+    if (!process.execPath.includes(path.join(".opencode", "bin")) && false) {
+      prompts.log.error(
+        `opencode is installed to ${process.execPath} and seems to be managed by a package manager`,
+      )
+      prompts.outro("Done")
+      return
+    }
+
+    const release = args.target ? await specific(args.target) : await latest()
+    const target = release.tag_name
+
+    prompts.log.info(`Upgrade ${VERSION} → ${target}`)
+
+    if (VERSION !== "dev" && compare(VERSION, target) >= 0) {
+      prompts.log.success(`Already up to date`)
+      prompts.outro("Done")
+      return
+    }
+
+    const name = asset()
+    const found = release.assets.find((a) => a.name === name)
+
+    if (!found) {
+      prompts.log.error(`No binary found for platform: ${name}`)
+      prompts.outro("Done")
+      return
+    }
+
+    const spinner = prompts.spinner()
+    spinner.start("Downloading update...")
+
+    let downloadPath: string
+    try {
+      downloadPath = await download(found.browser_download_url)
+      spinner.stop("Download complete")
+    } catch (downloadError) {
+      spinner.stop("Download failed")
+      prompts.log.error(
+        `Download failed: ${downloadError instanceof Error ? downloadError.message : String(downloadError)}`,
+      )
+      prompts.outro("Done")
+      return
+    }
+
+    try {
+      await fs.rename(downloadPath, process.execPath)
+      prompts.log.success(`Successfully upgraded to ${target}`)
+    } catch (installError) {
+      prompts.log.error(
+        `Install failed: ${installError instanceof Error ? installError.message : String(installError)}`,
+      )
+      // Clean up downloaded file
+      await fs.unlink(downloadPath).catch(() => {})
+    }
+
+    prompts.outro("Done")
+  },
+}

+ 0 - 193
packages/opencode/src/cli/router.ts

@@ -1,193 +0,0 @@
-import { createCli, type TrpcCliMeta } from "trpc-cli"
-import { initTRPC } from "@trpc/server"
-import { z } from "zod"
-import { Server } from "../server/server"
-import { AuthAnthropic } from "../auth/anthropic"
-import { UI } from "./ui"
-import { App } from "../app/app"
-import { Bus } from "../bus"
-import { Provider } from "../provider/provider"
-import { Session } from "../session"
-import { Share } from "../share/share"
-import { Message } from "../session/message"
-import { VERSION } from "./version"
-import { LSP } from "../lsp"
-import fs from "fs/promises"
-import path from "path"
-
-const t = initTRPC.meta<TrpcCliMeta>().create()
-
-export const router = t.router({
-  generate: t.procedure
-    .meta({
-      description: "Generate OpenAPI and event specs",
-    })
-    .input(z.object({}))
-    .mutation(async () => {
-      const specs = await Server.openapi()
-      const dir = "gen"
-      await fs.rmdir(dir, { recursive: true }).catch(() => {})
-      await fs.mkdir(dir, { recursive: true })
-      await Bun.write(
-        path.join(dir, "openapi.json"),
-        JSON.stringify(specs, null, 2),
-      )
-      return "Generated OpenAPI specs in gen/ directory"
-    }),
-
-  run: t.procedure
-    .meta({
-      description: "Run OpenCode with a message",
-    })
-    .input(
-      z.object({
-        message: z.array(z.string()).default([]).describe("Message to send"),
-        session: z.string().optional().describe("Session ID to continue"),
-      }),
-    )
-    .mutation(
-      async ({ input }: { input: { message: string[]; session?: string } }) => {
-        const message = input.message.join(" ")
-        await App.provide(
-          {
-            cwd: process.cwd(),
-            version: "0.0.0",
-          },
-          async () => {
-            await Share.init()
-            const session = input.session
-              ? await Session.get(input.session)
-              : await Session.create()
-
-            UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + "◍  OpenCode", VERSION)
-            UI.empty()
-            UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
-            UI.empty()
-            UI.println(
-              UI.Style.TEXT_INFO_BOLD +
-                "~  https://dev.opencode.ai/s?id=" +
-                session.id.slice(-8),
-            )
-            UI.empty()
-
-            function printEvent(color: string, type: string, title: string) {
-              UI.println(
-                color + `|`,
-                UI.Style.TEXT_NORMAL +
-                  UI.Style.TEXT_DIM +
-                  ` ${type.padEnd(7, " ")}`,
-                "",
-                UI.Style.TEXT_NORMAL + title,
-              )
-            }
-
-            Bus.subscribe(Message.Event.PartUpdated, async (message) => {
-              const part = message.properties.part
-              if (
-                part.type === "tool-invocation" &&
-                part.toolInvocation.state === "result"
-              ) {
-                if (part.toolInvocation.toolName === "opencode_todowrite")
-                  return
-
-                const args = part.toolInvocation.args as any
-                const tool = part.toolInvocation.toolName
-
-                if (tool === "opencode_edit")
-                  printEvent(UI.Style.TEXT_SUCCESS_BOLD, "Edit", args.filePath)
-                if (tool === "opencode_bash")
-                  printEvent(
-                    UI.Style.TEXT_WARNING_BOLD,
-                    "Execute",
-                    args.command,
-                  )
-                if (tool === "opencode_read")
-                  printEvent(UI.Style.TEXT_INFO_BOLD, "Read", args.filePath)
-                if (tool === "opencode_write")
-                  printEvent(
-                    UI.Style.TEXT_SUCCESS_BOLD,
-                    "Create",
-                    args.filePath,
-                  )
-                if (tool === "opencode_list")
-                  printEvent(UI.Style.TEXT_INFO_BOLD, "List", args.path)
-                if (tool === "opencode_glob")
-                  printEvent(
-                    UI.Style.TEXT_INFO_BOLD,
-                    "Glob",
-                    args.pattern + (args.path ? " in " + args.path : ""),
-                  )
-              }
-
-              if (part.type === "text") {
-                if (part.text.includes("\n")) {
-                  UI.empty()
-                  UI.println(part.text)
-                  UI.empty()
-                  return
-                }
-                printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
-              }
-            })
-
-            const { providerID, modelID } = await Provider.defaultModel()
-            await Session.chat({
-              sessionID: session.id,
-              providerID,
-              modelID,
-              parts: [
-                {
-                  type: "text",
-                  text: message,
-                },
-              ],
-            })
-            UI.empty()
-          },
-        )
-        return "Session completed"
-      },
-    ),
-
-  scrap: t.procedure
-    .meta({
-      description: "Test command for scraping files",
-    })
-    .input(
-      z.object({
-        file: z.string().describe("File to process"),
-      }),
-    )
-    .mutation(async ({ input }: { input: { file: string } }) => {
-      await App.provide({ cwd: process.cwd(), version: VERSION }, async () => {
-        await LSP.touchFile(input.file, true)
-        await LSP.diagnostics()
-      })
-      return `Processed file: ${input.file}`
-    }),
-
-  login: t.router({
-    anthropic: t.procedure
-      .meta({
-        description: "Login to Anthropic",
-      })
-      .input(z.object({}))
-      .mutation(async () => {
-        const { url, verifier } = await AuthAnthropic.authorize()
-
-        UI.println("Login to Anthropic")
-        UI.println("Open the following URL in your browser:")
-        UI.println(url)
-        UI.println("")
-
-        const code = await UI.input("Paste the authorization code here: ")
-        await AuthAnthropic.exchange(code, verifier)
-        return "Successfully logged in to Anthropic"
-      }),
-  }),
-})
-
-export function createOpenCodeCli() {
-  return createCli({ router })
-}
-

+ 3 - 1
packages/opencode/src/index.ts

@@ -13,6 +13,7 @@ import { VERSION } from "./cli/version"
 import { ScrapCommand } from "./cli/cmd/scrap"
 import { Log } from "./util/log"
 import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
+import { UpgradeCommand } from "./cli/cmd/upgrade"
 import { Provider } from "./provider/provider"
 import { UI } from "./cli/ui"
 
@@ -33,7 +34,7 @@ const cli = yargs(hideBin(process.argv))
   .usage("\n" + UI.logo())
   .command({
     command: "$0 [project]",
-    describe: "Start OpenCode TUI",
+    describe: "Start opencode TUI",
     builder: (yargs) =>
       yargs.positional("project", {
         type: "string",
@@ -102,6 +103,7 @@ const cli = yargs(hideBin(process.argv))
   .command(GenerateCommand)
   .command(ScrapCommand)
   .command(AuthCommand)
+  .command(UpgradeCommand)
   .fail((msg, err) => {
     if (
       msg.startsWith("Unknown argument") ||