feat: Windows 下 zmo upgrade 支持自更新
Some checks failed
compliance-close / close-non-compliant (push) Has been cancelled

- installation/index.ts 新增 Windows 升级路径:下载 release zip →
  PowerShell 解压 → 写 .cmd 在进程退出后替换 exe(绕过运行中文件锁)
- method() 在 Windows 兜底返回 curl,让手动下载的 exe 也能走自动更新
- 修 cmd/upgrade.ts 残留的 "opencode upgrade skipped" 文案

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 18:06:09 +08:00
parent 5bd92a3535
commit c034a178b9
2 changed files with 56 additions and 1 deletions

View File

@@ -46,7 +46,7 @@ export const UpgradeCommand = {
const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest()
if (InstallationVersion === target) {
prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)
prompts.log.warn(`zmo upgrade skipped: ${target} is already installed`)
prompts.outro("Done")
return
}

View File

@@ -6,6 +6,8 @@ import { errorMessage } from "@/util/error"
import { ChildProcess } from "effect/unstable/process"
import { AppProcess } from "@opencode-ai/core/process"
import path from "path"
import os from "os"
import fs from "fs"
import { EventV2 } from "@opencode-ai/core/event"
import * as Log from "@opencode-ai/core/util/log"
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
@@ -155,8 +157,58 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProce
return "sh"
})
const upgradeWindows = Effect.fnUntraced(function* (target: string) {
// Windows 自更新:运行中的 exe 无法直接覆盖自身,
// 因此下载新 zip → 解压 → 写一个 .cmd 在本进程退出后替换 exe。
const exePath = process.execPath
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "zmo-upgrade-"))
const zipPath = path.join(tmpDir, "zmo-windows-x64.zip")
const url = `https://github.com/helpLogin/zmo-cli/releases/download/v${target}/zmo-windows-x64.zip`
// 1. 下载新版 zip
const res = yield* httpOk.execute(HttpClientRequest.get(url))
const buf = yield* res.arrayBuffer
fs.writeFileSync(zipPath, Buffer.from(buf))
// 2. 用 PowerShell 解压Win10+ 自带 Expand-Archive
const unzip = yield* run([
"powershell",
"-NoProfile",
"-Command",
`Expand-Archive -Path '${zipPath}' -DestinationPath '${tmpDir}' -Force`,
])
if (unzip.code !== 0) return unzip
const newExe = path.join(tmpDir, "zmo.exe")
if (!fs.existsSync(newExe)) {
return { code: 1, stdout: "", stderr: "downloaded archive did not contain zmo.exe" }
}
// 3. 写 .cmd等待本进程退出 → 覆盖旧 exe → 清理临时目录
const cmdPath = path.join(tmpDir, "zmo-replace.cmd")
const script = [
"@echo off",
"ping 127.0.0.1 -n 3 >nul",
`:retry`,
`move /Y "${newExe}" "${exePath}" >nul 2>&1`,
`if errorlevel 1 (`,
` ping 127.0.0.1 -n 2 >nul`,
` goto retry`,
`)`,
`rmdir /S /Q "${tmpDir}" >nul 2>&1`,
].join("\r\n")
fs.writeFileSync(cmdPath, script)
// 4. detached 启动 cmd本进程随后正常退出让 cmd 完成替换
Bun.spawn(["cmd", "/c", cmdPath], { stdio: ["ignore", "ignore", "ignore"] }).unref()
return { code: 0, stdout: `zmo will finish updating to v${target} after exit. Please restart zmo.`, stderr: "" }
}, Effect.catch((err) => Effect.succeed({ code: 1, stdout: "", stderr: errorMessage(err) })))
const upgradeCurl = Effect.fnUntraced(
function* (target: string) {
if (process.platform === "win32") {
return yield* upgradeWindows(target)
}
const response = yield* httpOk.execute(HttpClientRequest.get("https://raw.githubusercontent.com/helpLogin/zmo-cli/main/install"))
const body = yield* response.text
const bodyBytes = new TextEncoder().encode(body)
@@ -216,6 +268,9 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProce
}
}
// Windows 上手动下载的 exe 不在包管理器里,但我们的 Windows 升级路径
// 可自我替换任意位置的 exe所以兜底按 curl 处理以启用自动更新。
if (process.platform === "win32") return "curl" as Method
return "unknown" as Method
}),
latest: Effect.fn("Installation.latest")(function* (installMethod?: Method) {