diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 8a4219e8..7db7924a 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -70,7 +70,7 @@ jobs: # Keep one artifact per OS to stay under the repo's tight artifact quota. # macOS arm64 only - Apple Silicon covers current Macs. The Intel (macos-13) # runner is queue-starved and the x64 cross-compile is unreliable, so it is dropped. - mac='{"host":"macos-14","target":"darwin-arm64","platform_flag":"--mac dmg --arm64","group":"mac"}' + mac='{"host":"macos-14","target":"darwin-arm64","platform_flag":"--mac dmg zip --arm64","group":"mac"}' linux='{"host":"ubuntu-24.04","target":"linux-amd64","platform_flag":"--linux deb --x64","group":"linux"}' win='{"host":"windows-2025","target":"win-x64","platform_flag":"--win nsis --x64","group":"win"}' case "$PLATFORMS" in @@ -154,7 +154,10 @@ jobs: compression-level: 9 retention-days: 1 path: | + packages/desktop/dist/latest*.yml + packages/desktop/dist/*.blockmap packages/desktop/dist/*.dmg + packages/desktop/dist/*.zip packages/desktop/dist/*.deb packages/desktop/dist/*.exe @@ -181,11 +184,17 @@ jobs: version=$(node -p 'require("./packages/desktop/package.json").version.replace(/-.*/, "")') tag="app-v${version}-main.${GITHUB_RUN_NUMBER}" - mapfile -t assets < <(find release-assets -type f \( -name '*.dmg' -o -name '*.deb' -o -name '*.exe' \) | sort) + mapfile -t assets < <(find release-assets -type f \( -name 'latest*.yml' -o -name '*.blockmap' -o -name '*.dmg' -o -name '*.zip' -o -name '*.deb' -o -name '*.exe' \) | sort) if [ "${#assets[@]}" -eq 0 ]; then echo "No desktop release assets found" exit 1 fi + for required in latest.yml latest-mac.yml; do + if ! find release-assets -type f -name "$required" | grep -q .; then + echo "Missing updater metadata: $required" + exit 1 + fi + done gh release list --repo "$REPOSITORY" --limit 100 --json tagName \ --jq '.[] | select(.tagName | test("^app-v[0-9]+\\.[0-9]+\\.[0-9]+-main\\.")) | .tagName' \ diff --git a/bun.lock b/bun.lock index fd8ff6e1..89c75c5f 100644 --- a/bun.lock +++ b/bun.lock @@ -455,7 +455,7 @@ }, "packages/desktop": { "name": "@deepagent-code/desktop", - "version": "1.0.0-beta", + "version": "1.0.1", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", diff --git a/packages/app/src/components/dialog-select-model.test.ts b/packages/app/src/components/dialog-select-model.test.ts new file mode 100644 index 00000000..380a07a8 --- /dev/null +++ b/packages/app/src/components/dialog-select-model.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, test } from "bun:test" +import { showFreeModelTag } from "./model-tags" + +describe("showFreeModelTag", () => { + test("only labels DeepAgent managed zero-cost models as free", () => { + expect(showFreeModelTag({ provider: { id: "deepagent-code" }, cost: { input: 0 } })).toBe(true) + expect(showFreeModelTag({ provider: { id: "zhipuai" }, cost: { input: 0 } })).toBe(false) + expect(showFreeModelTag({ provider: { id: "deepagent-code" }, cost: { input: 1 } })).toBe(false) + }) +}) diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 4f039011..cb98b824 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -12,8 +12,7 @@ import { List } from "@deepagent-code/ui/list" import { Tooltip } from "@deepagent-code/ui/tooltip" import { ModelTooltip } from "./model-tooltip" import { useLanguage } from "@/context/language" - -const isFree = (cost: { input: number } | undefined) => cost?.input === 0 +import { showFreeModelTag } from "./model-tags" type ModelState = ReturnType["model"] @@ -57,7 +56,7 @@ const ModelList: Component<{ class="w-full" placement="right-start" gutter={12} - value={} + value={} > {node} @@ -72,7 +71,7 @@ const ModelList: Component<{ {(i) => (
{i.name} - + {language.t("model.tag.free")} diff --git a/packages/app/src/components/model-tags.ts b/packages/app/src/components/model-tags.ts new file mode 100644 index 00000000..d91b6e14 --- /dev/null +++ b/packages/app/src/components/model-tags.ts @@ -0,0 +1,3 @@ +export function showFreeModelTag(model: { cost?: { input: number }; provider: { id: string } }) { + return model.provider.id === "deepagent-code" && model.cost?.input === 0 +} diff --git a/packages/app/src/components/updater-action.test.ts b/packages/app/src/components/updater-action.test.ts index 46e7c732..7623ab5f 100644 --- a/packages/app/src/components/updater-action.test.ts +++ b/packages/app/src/components/updater-action.test.ts @@ -19,6 +19,12 @@ describe("updaterAction", () => { label: "toast.update.action.installRestart", run: "install", }) + expect( + updaterAction({ status: "ready", version: "2.0.0", manualUrl: "https://example.com/deepagent-code.deb" }), + ).toEqual({ + label: "toast.update.action.downloadDeb", + run: "install", + }) expect(updaterAction({ status: "installing", version: "2.0.0" })).toEqual({ label: "settings.updates.action.installing", }) diff --git a/packages/app/src/components/updater-action.ts b/packages/app/src/components/updater-action.ts index 9c13c542..c86dae68 100644 --- a/packages/app/src/components/updater-action.ts +++ b/packages/app/src/components/updater-action.ts @@ -12,6 +12,7 @@ export function updaterAction(state: UpdaterState | undefined) { case "downloading": return { label: "settings.updates.action.downloading" as const } case "ready": + if (state.manualUrl) return { label: "toast.update.action.downloadDeb" as const, run: "install" as const } return { label: "toast.update.action.installRestart" as const, run: "install" as const } case "installing": return { label: "settings.updates.action.installing" as const } diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 924ee1e3..7464c09e 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -94,7 +94,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const validModel = (model: ModelKey) => { const provider = providers.all().get(model.providerID) - return !!provider?.models[model.modelID] && connected().has(model.providerID) + return !!provider?.models[model.modelID] && connected().has(model.providerID) && models.visible(model) } const firstModel = (...items: Array<() => ModelKey | undefined>) => { diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 1ade147a..b87316b0 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -571,7 +571,9 @@ export const dict = { "toast.update.title": "Update available", "toast.update.description": "A new version of DeepAgent Code ({{version}}) is now available to install.", + "toast.update.description.deb": "A new version of DeepAgent Code ({{version}}) is available as a Linux .deb package.", "toast.update.action.installRestart": "Install and restart", + "toast.update.action.downloadDeb": "Download .deb", "toast.update.action.notYet": "Not yet", "error.page.title": "Something went wrong", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 58895feb..ba04a5d0 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -507,7 +507,9 @@ export const dict = { "toast.session.listFailed.title": "无法加载 {{project}} 的会话", "toast.update.title": "有可用更新", "toast.update.description": "DeepAgent Code 有新版本 ({{version}}) 可安装。", + "toast.update.description.deb": "DeepAgent Code 有新版本 ({{version}}) 可下载 Linux .deb 安装包。", "toast.update.action.installRestart": "安装并重启", + "toast.update.action.downloadDeb": "下载 .deb", "toast.update.action.notYet": "稍后", "error.page.title": "出了点问题", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index a51daa6e..e8ec2e4b 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -440,7 +440,9 @@ export const dict = { "toast.update.title": "有可用更新", "toast.update.description": "DeepAgent Code 有新版本 ({{version}}) 可安裝。", + "toast.update.description.deb": "DeepAgent Code 有新版本 ({{version}}) 可下載 Linux .deb 安裝包。", "toast.update.action.installRestart": "安裝並重新啟動", + "toast.update.action.downloadDeb": "下載 .deb", "toast.update.action.notYet": "稍後", "error.page.title": "出了點問題", diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 71470051..8082aa10 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -67,6 +67,7 @@ import { Titlebar, type TitlebarUpdate } from "@/components/titlebar" import { useDirectoryPicker } from "@/components/directory-picker" import { ServerConnection, useServer } from "@/context/server" import { useLanguage, type Locale } from "@/context/language" +import type { UpdaterState } from "@/updater" import { pathKey } from "@/utils/path-key" import { displayName, @@ -167,14 +168,14 @@ export default function Layout(props: ParentProps) { peeked: false, }) - const updateVersion = () => { + const readyUpdate = () => { const state = platform.updater?.state() if (state?.status !== "ready") return - return state.version + return state } const installUpdate = () => void platform.updater?.install() const titlebarUpdate: TitlebarUpdate = { - version: updateVersion, + version: () => readyUpdate()?.version, installing: () => platform.updater?.state().status === "installing", install: installUpdate, } @@ -2378,8 +2379,8 @@ export default function Layout(props: ParentProps) {
{autoselecting() ?? ""} - - + + {(update) => }
@@ -2530,21 +2531,24 @@ export default function Layout(props: ParentProps) { } function UpdateAvailableToast(props: { - version: string + update: Extract install: () => void language: ReturnType }) { let toastId: number | undefined onMount(() => { + const manual = Boolean(props.update.manualUrl) toastId = showToast({ persistent: true, icon: "download", title: props.language.t("toast.update.title"), - description: props.language.t("toast.update.description", { version: props.version }), + description: props.language.t(manual ? "toast.update.description.deb" : "toast.update.description", { + version: props.update.version, + }), actions: [ { - label: props.language.t("toast.update.action.installRestart"), + label: props.language.t(manual ? "toast.update.action.downloadDeb" : "toast.update.action.installRestart"), onClick: props.install, }, { diff --git a/packages/app/src/updater.ts b/packages/app/src/updater.ts index 22874348..9839781b 100644 --- a/packages/app/src/updater.ts +++ b/packages/app/src/updater.ts @@ -5,9 +5,9 @@ export type UpdaterState = | { status: "idle" } | { status: "checking" } | { status: "downloading"; version: string; percent?: number } - | { status: "ready"; version: string } + | { status: "ready"; version: string; manualUrl?: string } | { status: "up-to-date" } - | { status: "installing"; version: string } + | { status: "installing"; version: string; manualUrl?: string } | { status: "error"; message: string } export type UpdaterPlatform = { diff --git a/packages/console/app/src/routes/download/[channel]/[platform].ts b/packages/console/app/src/routes/download/[channel]/[platform].ts index 7370d3cd..7ea162e5 100644 --- a/packages/console/app/src/routes/download/[channel]/[platform].ts +++ b/packages/console/app/src/routes/download/[channel]/[platform].ts @@ -6,8 +6,6 @@ const prodAssetNames: Record = { "darwin-x64-dmg": "deepagent-code-desktop-mac-x64.dmg", "windows-x64-nsis": "deepagent-code-desktop-win-x64.exe", "linux-x64-deb": "deepagent-code-desktop-linux-amd64.deb", - "linux-x64-appimage": "deepagent-code-desktop-linux-x86_64.AppImage", - "linux-x64-rpm": "deepagent-code-desktop-linux-x86_64.rpm", } satisfies Record const betaAssetNames: Record = { @@ -15,8 +13,6 @@ const betaAssetNames: Record = { "darwin-x64-dmg": "deepagent-code-desktop-mac-x64.dmg", "windows-x64-nsis": "deepagent-code-desktop-win-x64.exe", "linux-x64-deb": "deepagent-code-desktop-linux-amd64.deb", - "linux-x64-appimage": "deepagent-code-desktop-linux-x86_64.AppImage", - "linux-x64-rpm": "deepagent-code-desktop-linux-x86_64.rpm", } satisfies Record // Doing this on the server lets us preserve the original name for platforms we don't care to rename for diff --git a/packages/console/app/src/routes/download/index.tsx b/packages/console/app/src/routes/download/index.tsx index f5efe182..0b71b551 100644 --- a/packages/console/app/src/routes/download/index.tsx +++ b/packages/console/app/src/routes/download/index.tsx @@ -234,39 +234,6 @@ export default function Download() { {i18n.t("download.action.download")}
-
-
- - - - - - {i18n.t("download.platform.linuxRpm")} -
- - {i18n.t("download.action.download")} - -
- {/* Disabled temporarily as it doesn't work */} - {/*
-
- - - - - - Linux (.AppImage) -
- - Download - -
*/}
diff --git a/packages/console/app/src/routes/download/types.ts b/packages/console/app/src/routes/download/types.ts index 916f9702..a63c301f 100644 --- a/packages/console/app/src/routes/download/types.ts +++ b/packages/console/app/src/routes/download/types.ts @@ -1,4 +1,4 @@ export type DownloadPlatform = | `darwin-${"x64" | "aarch64"}-dmg` | "windows-x64-nsis" - | `linux-x64-${"deb" | "rpm" | "appimage"}` + | "linux-x64-deb" diff --git a/packages/deepagent-code/src/provider/provider.ts b/packages/deepagent-code/src/provider/provider.ts index ed8e461b..d816f674 100644 --- a/packages/deepagent-code/src/provider/provider.ts +++ b/packages/deepagent-code/src/provider/provider.ts @@ -1148,6 +1148,13 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] { return result } +function inferredReasoning(providerID: string, apiID: string, modelID = apiID) { + const provider = providerID.toLowerCase() + const model = `${apiID} ${modelID}`.toLowerCase() + if (!["zai", "zhipuai"].some((id) => provider.includes(id))) return false + return ["glm-5.2", "glm-5-2", "glm-5p2"].some((id) => model.includes(id)) +} + function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { const base: Model = { id: ModelV2.ID.make(model.id), @@ -1170,7 +1177,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model }, capabilities: { temperature: model.temperature ?? false, - reasoning: model.reasoning ?? false, + reasoning: model.reasoning || inferredReasoning(provider.id, model.id), attachment: model.attachment ?? false, toolcall: model.tool_call ?? true, input: { @@ -1397,7 +1404,8 @@ export const layer = Layer.effect( providerID: ProviderV2.ID.make(providerID), capabilities: { temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false, - reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false, + reasoning: + model.reasoning ?? (existingModel?.capabilities.reasoning || inferredReasoning(providerID, apiID, modelID)), attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false, toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true, input: { diff --git a/packages/deepagent-code/src/provider/transform.ts b/packages/deepagent-code/src/provider/transform.ts index 9a093f51..dbab4fb9 100644 --- a/packages/deepagent-code/src/provider/transform.ts +++ b/packages/deepagent-code/src/provider/transform.ts @@ -475,6 +475,12 @@ export function temperature(model: Provider.Model) { return undefined } +function glm52(model: Provider.Model) { + const id = model.id.toLowerCase() + const apiID = model.api.id.toLowerCase() + return ["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => id.includes(name) || apiID.includes(name)) +} + export function topP(model: Provider.Model) { const id = model.id.toLowerCase() if (id.includes("qwen")) return 1 @@ -644,13 +650,34 @@ export function variants(model: Provider.Model): Record { expect(model.release_date).toBe("") }) +test("models.dev normalization infers reasoning for zhipuai glm-5.2", () => { + const provider = { + id: "zhipuai", + name: "Zhipu AI", + env: [], + api: "https://open.bigmodel.cn/api/paas/v4", + npm: "@ai-sdk/openai-compatible", + models: { + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + family: "glm", + reasoning: false, + cost: { input: 0, output: 0 }, + limit: { context: 128_000, output: 8192 }, + }, + }, + } as unknown as ModelsDev.Provider + + const model = Provider.fromModelsDevProvider(provider).models["glm-5.2"] + expect(model.capabilities.reasoning).toBe(true) + expect(model.variants).toEqual({ + high: { reasoningEffort: "high" }, + max: { reasoningEffort: "max" }, + }) +}) + it.instance("model variants are generated for reasoning models", () => Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") diff --git a/packages/deepagent-code/test/provider/transform.test.ts b/packages/deepagent-code/test/provider/transform.test.ts index 09cd7354..ed90698f 100644 --- a/packages/deepagent-code/test/provider/transform.test.ts +++ b/packages/deepagent-code/test/provider/transform.test.ts @@ -2471,6 +2471,24 @@ describe("ProviderTransform.variants", () => { expect(result).toEqual({}) }) + test("glm-5.2 openai-compatible returns reasoning effort variants", () => { + const model = createMockModel({ + id: "zhipuai/glm-5.2", + providerID: "zhipuai", + api: { + id: "glm-5.2", + url: "https://open.bigmodel.cn/api/paas/v4", + npm: "@ai-sdk/openai-compatible", + }, + capabilities: { reasoning: true }, + }) + const result = ProviderTransform.variants(model) + expect(result).toEqual({ + high: { reasoningEffort: "high" }, + max: { reasoningEffort: "max" }, + }) + }) + test("mistral models with reasoning support return variants", () => { const model = createMockModel({ id: "mistral/mistral-small-latest", diff --git a/packages/desktop/electron-builder.config.ts b/packages/desktop/electron-builder.config.ts index fb355d5b..3ff554dc 100644 --- a/packages/desktop/electron-builder.config.ts +++ b/packages/desktop/electron-builder.config.ts @@ -96,7 +96,7 @@ const getBase = (): Configuration => ({ linux: { icon: `resources/icons`, category: "Development", - target: ["AppImage", "deb", "rpm"], + target: ["deb"], }, }) @@ -118,7 +118,7 @@ function getConfig() { appId: "ai.deepagent-code.desktop.beta", productName: "DeepAgent Code Beta", protocols: { name: "DeepAgent Code Beta", schemes: ["deepagent-code"] }, - publish: { provider: "github", owner: "anomalyco", repo: "deepagent-code-beta", channel: "latest" }, + publish: { provider: "github", owner: "deepagent-ltd", repo: "deepagent-code", channel: "latest" }, rpm: { packageName: "deepagent-code-beta" }, } } @@ -128,7 +128,7 @@ function getConfig() { appId: "ai.deepagent-code.desktop", productName: "DeepAgent Code", protocols: { name: "DeepAgent Code", schemes: ["deepagent-code"] }, - publish: { provider: "github", owner: "anomalyco", repo: "deepagent-code", channel: "latest" }, + publish: { provider: "github", owner: "deepagent-ltd", repo: "deepagent-code", channel: "latest" }, rpm: { packageName: "deepagent-code" }, } } diff --git a/packages/desktop/icons/beta/dock.png b/packages/desktop/icons/beta/dock.png index 23033b82..84d87262 100644 Binary files a/packages/desktop/icons/beta/dock.png and b/packages/desktop/icons/beta/dock.png differ diff --git a/packages/desktop/icons/beta/icon.icns b/packages/desktop/icons/beta/icon.icns index bdfd14d2..ca93f1ec 100644 Binary files a/packages/desktop/icons/beta/icon.icns and b/packages/desktop/icons/beta/icon.icns differ diff --git a/packages/desktop/icons/dev/dock.png b/packages/desktop/icons/dev/dock.png index 23033b82..84d87262 100644 Binary files a/packages/desktop/icons/dev/dock.png and b/packages/desktop/icons/dev/dock.png differ diff --git a/packages/desktop/icons/dev/icon.icns b/packages/desktop/icons/dev/icon.icns index bdfd14d2..ca93f1ec 100644 Binary files a/packages/desktop/icons/dev/icon.icns and b/packages/desktop/icons/dev/icon.icns differ diff --git a/packages/desktop/icons/prod/dock.png b/packages/desktop/icons/prod/dock.png index 23033b82..84d87262 100644 Binary files a/packages/desktop/icons/prod/dock.png and b/packages/desktop/icons/prod/dock.png differ diff --git a/packages/desktop/icons/prod/icon.icns b/packages/desktop/icons/prod/icon.icns index bdfd14d2..ca93f1ec 100644 Binary files a/packages/desktop/icons/prod/icon.icns and b/packages/desktop/icons/prod/icon.icns differ diff --git a/packages/desktop/package.json b/packages/desktop/package.json index d357e9aa..1e52038a 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@deepagent-code/desktop", "private": true, - "version": "1.0.0-beta", + "version": "1.0.1", "type": "module", "license": "AGPL-3.0-or-later", "homepage": "https://deepagent-code.ai", diff --git a/packages/desktop/scripts/finalize-latest-json.ts b/packages/desktop/scripts/finalize-latest-json.ts index e2e6267b..bb893425 100644 --- a/packages/desktop/scripts/finalize-latest-json.ts +++ b/packages/desktop/scripts/finalize-latest-json.ts @@ -164,11 +164,7 @@ const macxTarGz = "deepagent-code-desktop-mac-x64.app.tar.gz" const macaTarGz = "deepagent-code-desktop-mac-arm64.app.tar.gz" const linxDeb = pick(linx?.files ?? [], [".deb"]) -const linxRpm = pick(linx?.files ?? [], [".rpm"]) -const linxAppImage = pick(linx?.files ?? [], [".appimage"]) const linaDeb = pick(lina?.files ?? [], [".deb"]) -const linaRpm = pick(lina?.files ?? [], [".rpm"]) -const linaAppImage = pick(lina?.files ?? [], [".appimage"]) await add(out, "windows-x86_64-nsis", winxexe) await add(out, "windows-aarch64-nsis", winaexe) @@ -176,11 +172,7 @@ await add(out, "darwin-x86_64-app", macxTarGz) await add(out, "darwin-aarch64-app", macaTarGz) await add(out, "linux-x86_64-deb", linxDeb) -await add(out, "linux-x86_64-rpm", linxRpm) -await add(out, "linux-x86_64-appimage", linxAppImage) await add(out, "linux-aarch64-deb", linaDeb) -await add(out, "linux-aarch64-rpm", linaRpm) -await add(out, "linux-aarch64-appimage", linaAppImage) alias(out, "windows-x86_64", "windows-x86_64-nsis") alias(out, "windows-aarch64", "windows-aarch64-nsis") diff --git a/packages/desktop/src/main/updater-controller.test.ts b/packages/desktop/src/main/updater-controller.test.ts index ff3f7d1d..df28f619 100644 --- a/packages/desktop/src/main/updater-controller.test.ts +++ b/packages/desktop/src/main/updater-controller.test.ts @@ -76,6 +76,53 @@ describe("updater controller", () => { expect(app.calls).toEqual(["check", "download"]) }) + test("publishes manual download updates without downloading an installer", async () => { + const calls: string[] = [] + let ready: UpdaterReadyRecord | undefined + const controller = createUpdaterController({ + enabled: true, + currentVersion: "1.0.0", + backend: { + async checkForUpdates() { + calls.push("check") + return { + isUpdateAvailable: true, + updateInfo: { version: "2.0.0", manualUrl: "https://example.com/deepagent-code.deb" }, + } + }, + async downloadUpdate() { + calls.push("download") + }, + quitAndInstall() { + calls.push("open") + }, + }, + persistence: { + get: () => ready, + set: (value) => { + ready = value + }, + clear: () => { + ready = undefined + }, + }, + stop: async () => { + calls.push("stop") + }, + }) + + await controller.start() + await controller.install() + + expect(calls).toEqual(["check", "open"]) + expect(ready).toEqual({ version: "2.0.0", manualUrl: "https://example.com/deepagent-code.deb" }) + expect(controller.getState()).toEqual({ + status: "ready", + version: "2.0.0", + manualUrl: "https://example.com/deepagent-code.deb", + }) + }) + test("returns to ready when quitAndInstall returns without exiting", async () => { const app = setup() await app.controller.start() diff --git a/packages/desktop/src/main/updater-controller.ts b/packages/desktop/src/main/updater-controller.ts index 0df9d8be..9281eae4 100644 --- a/packages/desktop/src/main/updater-controller.ts +++ b/packages/desktop/src/main/updater-controller.ts @@ -2,10 +2,12 @@ import type { UpdaterState } from "@deepagent-code/app/updater" export type { UpdaterState } from "@deepagent-code/app/updater" -export type UpdaterReadyRecord = { version: string } +export type UpdaterReadyRecord = { version: string; manualUrl?: string } export type UpdaterBackend = { - checkForUpdates(): Promise<{ isUpdateAvailable?: boolean; updateInfo?: { version?: string } } | null | undefined> + checkForUpdates(): Promise< + { isUpdateAvailable?: boolean; updateInfo?: { version?: string; manualUrl?: string } } | null | undefined + > downloadUpdate(): Promise quitAndInstall(): void } @@ -48,11 +50,19 @@ export function createUpdaterController(input: { await input.persistence.clear() return transition({ status: "up-to-date" }) } + const ready = { + version, + ...(result.updateInfo?.manualUrl ? { manualUrl: result.updateInfo.manualUrl } : {}), + } + if (ready.manualUrl) { + await input.persistence.set(ready) + return transition({ status: "ready", ...ready }) + } transition({ status: "downloading", version }) await input.backend.downloadUpdate() - await input.persistence.set({ version }) - return transition({ status: "ready", version }) + await input.persistence.set(ready) + return transition({ status: "ready", ...ready }) })() .catch((error) => transition({ status: "error", message: error instanceof Error ? error.message : String(error) }), @@ -79,7 +89,13 @@ export function createUpdaterController(input: { async install() { if (state.status !== "ready") throw new Error("Update is not ready to install") const version = state.version - transition({ status: "installing", version }) + const manualUrl = state.manualUrl + transition({ status: "installing", version, ...(manualUrl ? { manualUrl } : {}) }) + if (manualUrl) { + input.backend.quitAndInstall() + transition({ status: "ready", version, manualUrl }) + return + } await input .stop() .then(() => { diff --git a/packages/desktop/src/main/updater.ts b/packages/desktop/src/main/updater.ts index c87ea83a..7168def9 100644 --- a/packages/desktop/src/main/updater.ts +++ b/packages/desktop/src/main/updater.ts @@ -1,12 +1,13 @@ -import { app, dialog } from "electron" +import { app, dialog, shell } from "electron" import pkg from "electron-updater" import { UPDATER_ENABLED } from "./constants" -import { createUpdaterController, type UpdaterReadyRecord } from "./updater-controller" +import { createUpdaterController, type UpdaterBackend, type UpdaterReadyRecord } from "./updater-controller" import { getLogger } from "./logging" import { getStore } from "./store" const { autoUpdater } = pkg const key = "ready" +const releaseURL = "https://api.github.com/repos/deepagent-ltd/deepagent-code/releases/latest" export function setupAutoUpdater(stop: () => Promise) { const logger = getLogger() @@ -27,12 +28,15 @@ export function setupAutoUpdater(stop: () => Promise) { return createUpdaterController({ enabled: UPDATER_ENABLED, currentVersion: app.getVersion(), - backend: autoUpdater, + backend: process.platform === "linux" ? linuxDebUpdater(app.getVersion()) : autoUpdater, persistence: { get() { const value = store.get(key) if (!value || typeof value !== "object" || !("version" in value) || typeof value.version !== "string") return - return { version: value.version } satisfies UpdaterReadyRecord + return { + version: value.version, + ...("manualUrl" in value && typeof value.manualUrl === "string" ? { manualUrl: value.manualUrl } : {}), + } satisfies UpdaterReadyRecord }, set: (value) => store.set(key, value), clear: () => store.delete(key), @@ -56,6 +60,19 @@ export async function showUpdaterDialog(controller: ReturnType item.name?.endsWith(".deb") && item.browser_download_url) + if (!version || !asset?.browser_download_url || compareVersion(version, currentVersion) <= 0) { + manualUrl = undefined + return { isUpdateAvailable: false, updateInfo: version ? { version } : undefined } + } + + manualUrl = asset.browser_download_url + return { isUpdateAvailable: true, updateInfo: { version, manualUrl } } + }, + async downloadUpdate() {}, + quitAndInstall() { + if (manualUrl) void shell.openExternal(manualUrl) + }, + } +} + +function releaseVersion(tag: string | undefined) { + return /^app-v(\d+\.\d+\.\d+)(?:-|$)/.exec(tag ?? "")?.[1] +} + +function compareVersion(a: string, b: string) { + const left = a.split(".").map((item) => Number.parseInt(item, 10)) + const right = b.replace(/-.*/, "").split(".").map((item) => Number.parseInt(item, 10)) + return [0, 1, 2].reduce((result, index) => { + if (result !== 0) return result + return (left[index] ?? 0) - (right[index] ?? 0) + }, 0) +}