feat: Windows 下 zmo upgrade 支持自更新
Some checks failed
compliance-close / close-non-compliant (push) Has been cancelled
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user