From 71fd7a918c1c51aefdb75506929449c0395d158f Mon Sep 17 00:00:00 2001 From: salmonumbrella <182032677+salmonumbrella@users.noreply.github.com> Date: Sun, 9 Aug 2026 07:23:10 +0000 Subject: [PATCH 1/5] fix(cli): keep server stable installs on selected channel --- packages/cli/README.md | 8 +-- packages/cli/src/commands/install/server.ts | 2 +- packages/cli/src/commands/setup.ts | 2 +- .../cli/src/commands/targets/add/desktop.ts | 2 +- .../cli/src/commands/targets/add/server.ts | 2 +- packages/cli/src/lib/installations.ts | 42 +++++++++--- packages/cli/test/cli-smoke.ts | 23 +++++-- packages/cli/test/installations.test.ts | 67 +++++++++++++++++++ 8 files changed, 126 insertions(+), 22 deletions(-) create mode 100644 packages/cli/test/installations.test.ts diff --git a/packages/cli/README.md b/packages/cli/README.md index f1167857..3b03d09b 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -431,7 +431,7 @@ Flags: | `--oauth` | boolean | Authorize the target with browser OAuth/PKCE | | `--remote=` | option | Connect to a remote Beeper Desktop or Server URL | | `--server` | boolean | Set up a local Beeper Server target | -| `--server-env=` | option | Server environment. Staging forces nightly. Default: production | +| `--server-env=` | option | Server environment (production or staging) Default: production | | `--username=` | option | Username to use if setup creates a new account | Examples: @@ -480,7 +480,7 @@ Flags: | Flag | Type | Description | | --- | --- | --- | | `--channel=` | option | Server release channel Default: stable | -| `--server-env=` | option | Server environment. Staging forces nightly. Default: production | +| `--server-env=` | option | Server environment (production or staging) Default: production | Examples: @@ -573,7 +573,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Desktop will expose its API on | -| `--server-env=` | option | Server environment. Staging forces nightly. Default: production | +| `--server-env=` | option | Server environment (production or staging) Default: production | Examples: @@ -602,7 +602,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Server will expose its API on | -| `--server-env=` | option | Server environment. Staging forces nightly. Default: production | +| `--server-env=` | option | Server environment (production or staging) Default: production | Examples: diff --git a/packages/cli/src/commands/install/server.ts b/packages/cli/src/commands/install/server.ts index c612a131..a647943b 100644 --- a/packages/cli/src/commands/install/server.ts +++ b/packages/cli/src/commands/install/server.ts @@ -8,7 +8,7 @@ export default class SetupInstallServer extends BeeperCommand { static override summary = 'Install Beeper Server locally' static override flags = { channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Server release channel' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment. Staging forces nightly.' }), + 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment (production or staging)' }), } async run(): Promise { diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 5b45031b..f59baab6 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -34,7 +34,7 @@ export default class Setup extends BeeperCommand { desktop: Flags.boolean({ default: false, description: 'Set up a local Beeper Desktop target' }), install: Flags.boolean({ default: false, description: 'Allow installing missing managed runtime' }), channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Install release channel' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment. Staging forces nightly.' }), + 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment (production or staging)' }), email: Flags.string({ description: 'Sign in with an email address' }), username: Flags.string({ description: 'Username to use if setup creates a new account' }), } diff --git a/packages/cli/src/commands/targets/add/desktop.ts b/packages/cli/src/commands/targets/add/desktop.ts index 34af8af4..5796a042 100644 --- a/packages/cli/src/commands/targets/add/desktop.ts +++ b/packages/cli/src/commands/targets/add/desktop.ts @@ -9,7 +9,7 @@ export default class TargetsAddDesktop extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Desktop will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment. Staging forces nightly.' }), + 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment (production or staging)' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddDesktop) diff --git a/packages/cli/src/commands/targets/add/server.ts b/packages/cli/src/commands/targets/add/server.ts index b5b10aa2..da43b5c4 100644 --- a/packages/cli/src/commands/targets/add/server.ts +++ b/packages/cli/src/commands/targets/add/server.ts @@ -9,7 +9,7 @@ export default class TargetsAddServer extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Server will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment. Staging forces nightly.' }), + 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment (production or staging)' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddServer) diff --git a/packages/cli/src/lib/installations.ts b/packages/cli/src/lib/installations.ts index 19abe049..bcda577f 100644 --- a/packages/cli/src/lib/installations.ts +++ b/packages/cli/src/lib/installations.ts @@ -87,11 +87,8 @@ export function normalizeInstallRequest(options: { bundleID: string apiBaseURL: string } { - // TODO: switch Server installs back to production once the production download - // endpoint returns a beeper-server artifact instead of the Desktop app bundle. - const serverEnv = options.kind === 'server' ? 'staging' : normalizeServerEnv(options.serverEnv) - let channel = options.channel ?? 'stable' - if (serverEnv === 'staging') channel = 'nightly' + const serverEnv = normalizeServerEnv(options.serverEnv) + const channel = options.channel ?? 'stable' const platform = normalizeDownloadPlatform(options.platform ?? process.platform) const feedPlatform = normalizeFeedPlatform(options.platform ?? process.platform) const arch = normalizeArch(options.arch ?? process.arch) @@ -104,7 +101,7 @@ export function normalizeInstallRequest(options: { feedPlatform, arch, bundleID, - apiBaseURL: options.kind === 'server' || serverEnv === 'staging' ? 'https://api.beeper-staging.com' : 'https://api.beeper.com', + apiBaseURL: serverEnv === 'staging' ? 'https://api.beeper-staging.com' : 'https://api.beeper.com', } } @@ -118,8 +115,7 @@ export function feedURLFor(options: ReturnType): } export function downloadURLFor(options: ReturnType): string { - const channelSegment = options.serverEnv === 'staging' && options.kind === 'server' ? 'stable' : options.channel - return `${options.apiBaseURL}/desktop/download/${options.platform}/${options.arch}/${channelSegment}/${options.bundleID}` + return `${options.apiBaseURL}/desktop/download/${options.platform}/${options.arch}/${options.channel}/${options.bundleID}` } export async function fetchFeed(feedURL: string): Promise { @@ -181,8 +177,18 @@ export async function installServer(options: { channel?: InstallChannel; serverE if (process.platform === 'win32') throw new Error('Beeper Server install is not available on Windows.') const request = normalizeInstallRequest({ kind: 'server', channel: options.channel, serverEnv: options.serverEnv }) const feedURL = feedURLFor(request) - const downloadURL = downloadURLFor(request) - const feed = await fetchFeed(feedURL).catch(() => ({ raw: undefined, version: undefined })) + let feed: FeedInfo + try { + feed = await fetchFeed(feedURL) + } catch (error) { + const reason = error instanceof Error ? ` ${error.message}` : '' + throw new Error(`Beeper Server ${request.channel} artifact is unavailable from the ${request.serverEnv} update feed; refusing to install a different channel.${reason}`) + } + const downloadURL = feed.url + if (!downloadURL) { + throw new Error(`Beeper Server ${request.channel} update feed did not include an artifact URL; refusing to install a different channel.`) + } + assertServerArtifactChannel(downloadURL, request.channel) const version = feed.version ?? 'unknown' const stageDir = join(serverInstallRoot(), `${request.channel}-${version}-${Date.now()}`) await mkdir(stageDir, { recursive: true }) @@ -206,6 +212,22 @@ export async function installServer(options: { channel?: InstallChannel; serverE }) } +function assertServerArtifactChannel(downloadURL: string, channel: InstallChannel): void { + let filename: string + try { + filename = decodeURIComponent(basename(new URL(downloadURL).pathname)).toLowerCase() + } catch { + throw new Error(`Beeper Server ${channel} update feed returned an invalid artifact URL; refusing to install it.`) + } + if (!filename.startsWith('beeper-server-')) { + throw new Error(`Beeper Server ${channel} update feed returned a non-Server artifact; refusing to install it.`) + } + const artifactChannel: InstallChannel = filename.includes('nightly') ? 'nightly' : 'stable' + if (artifactChannel !== channel) { + throw new Error(`Beeper Server ${channel} update feed returned a ${artifactChannel} artifact; refusing to install a different channel.`) + } +} + export async function updateServerInstallation(installation: Installation): Promise { return installServer({ channel: installation.channel, serverEnv: installation.serverEnv }) } diff --git a/packages/cli/test/cli-smoke.ts b/packages/cli/test/cli-smoke.ts index f10fb246..8a0769bd 100644 --- a/packages/cli/test/cli-smoke.ts +++ b/packages/cli/test/cli-smoke.ts @@ -250,11 +250,26 @@ assert.equal(rpcLine.id, 1) assert.equal(rpcLine.ok, true) assert.match(rpcLine.stdout, /"success": true/) +const serverStableRequest = normalizeInstallRequest({ kind: 'server', channel: 'stable', platform: 'darwin', arch: 'arm64' }) +assert.equal(serverStableRequest.channel, 'stable') +assert.equal(serverStableRequest.serverEnv, 'production') +assert.equal(serverStableRequest.bundleID, 'com.automattic.beeper.server') +assert.equal(feedURLFor(serverStableRequest), 'https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') +assert.equal(downloadURLFor(serverStableRequest), 'https://api.beeper.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') + const stagingServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'staging', channel: 'stable', platform: 'darwin', arch: 'arm64' }) -assert.equal(stagingServerRequest.channel, 'nightly') -assert.equal(stagingServerRequest.bundleID, 'com.automattic.beeper.server.nightly') -assert.equal(feedURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=darwin&channel=nightly&arch=arm64') -assert.equal(downloadURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server.nightly') +assert.equal(stagingServerRequest.channel, 'stable') +assert.equal(stagingServerRequest.serverEnv, 'staging') +assert.equal(stagingServerRequest.bundleID, 'com.automattic.beeper.server') +assert.equal(feedURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') +assert.equal(downloadURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') + +const serverNightlyRequest = normalizeInstallRequest({ kind: 'server', channel: 'nightly', platform: 'darwin', arch: 'arm64' }) +assert.equal(serverNightlyRequest.channel, 'nightly') +assert.equal(serverNightlyRequest.serverEnv, 'production') +assert.equal(serverNightlyRequest.bundleID, 'com.automattic.beeper.server.nightly') +assert.equal(feedURLFor(serverNightlyRequest), 'https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=darwin&channel=nightly&arch=arm64') +assert.equal(downloadURLFor(serverNightlyRequest), 'https://api.beeper.com/desktop/download/macos/arm64/nightly/com.automattic.beeper.server.nightly') const desktopNightlyRequest = normalizeInstallRequest({ kind: 'desktop', channel: 'nightly', platform: 'darwin', arch: 'arm64' }) assert.equal(downloadURLFor(desktopNightlyRequest), 'https://api.beeper.com/desktop/download/macos/arm64/nightly/com.automattic.beeper.desktop.nightly') diff --git a/packages/cli/test/installations.test.ts b/packages/cli/test/installations.test.ts new file mode 100644 index 00000000..35561b51 --- /dev/null +++ b/packages/cli/test/installations.test.ts @@ -0,0 +1,67 @@ +import { afterEach, describe, expect, it } from 'bun:test' +import { downloadURLFor, feedURLFor, installServer, normalizeInstallRequest } from '../src/lib/installations.js' + +const originalFetch = globalThis.fetch + +afterEach(() => { + globalThis.fetch = originalFetch +}) + +describe('server installation artifact selection', () => { + it('uses the production stable Server artifact by default', () => { + const request = normalizeInstallRequest({ kind: 'server', platform: 'linux', arch: 'x64' }) + + expect(request.channel).toBe('stable') + expect(request.serverEnv).toBe('production') + expect(request.bundleID).toBe('com.automattic.beeper.server') + expect(request.apiBaseURL).toBe('https://api.beeper.com') + expect(feedURLFor(request)).toBe('https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=linux&channel=stable&arch=x64') + expect(downloadURLFor(request)).toBe('https://api.beeper.com/desktop/download/linux/x64/stable/com.automattic.beeper.server') + }) + + it('keeps staging stable when staging is explicitly selected', () => { + const request = normalizeInstallRequest({ kind: 'server', serverEnv: 'staging', channel: 'stable', platform: 'linux', arch: 'x64' }) + + expect(request.channel).toBe('stable') + expect(request.serverEnv).toBe('staging') + expect(request.bundleID).toBe('com.automattic.beeper.server') + expect(request.apiBaseURL).toBe('https://api.beeper-staging.com') + expect(downloadURLFor(request)).toBe('https://api.beeper-staging.com/desktop/download/linux/x64/stable/com.automattic.beeper.server') + }) + + it('keeps nightly explicit instead of deriving it from the environment', () => { + const request = normalizeInstallRequest({ kind: 'server', channel: 'nightly', platform: 'linux', arch: 'x64' }) + + expect(request.channel).toBe('nightly') + expect(request.serverEnv).toBe('production') + expect(request.bundleID).toBe('com.automattic.beeper.server.nightly') + expect(request.apiBaseURL).toBe('https://api.beeper.com') + expect(downloadURLFor(request)).toBe('https://api.beeper.com/desktop/download/linux/x64/nightly/com.automattic.beeper.server.nightly') + }) + + it('fails closed when the selected Server feed has no artifact URL', async () => { + let calls = 0 + globalThis.fetch = async () => { + calls += 1 + return calls === 1 + ? new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } }) + : new Response('unexpected download', { status: 404, statusText: 'Not Found' }) + } + + await expect(installServer()).rejects.toThrow('Beeper Server stable update feed did not include an artifact URL; refusing to install a different channel.') + expect(calls).toBe(1) + }) + + it('fails closed when the stable feed returns a nightly artifact', async () => { + let calls = 0 + globalThis.fetch = async () => { + calls += 1 + return calls === 1 + ? Response.json({ url: 'https://downloads.beeper.com/beeper-server-nightly-4.3.23-linux-x64.tar.gz' }) + : new Response('unexpected download', { status: 404, statusText: 'Not Found' }) + } + + await expect(installServer()).rejects.toThrow('Beeper Server stable update feed returned a nightly artifact; refusing to install a different channel.') + expect(calls).toBe(1) + }) +}) From 0d9e5f397ea493d2921531e2d7231bf15264e1c3 Mon Sep 17 00:00:00 2001 From: suatsulun Date: Wed, 9 Sep 2026 12:04:08 +0300 Subject: [PATCH 2/5] fix(cli): stage downloads on the destination filesystem --- packages/cli/src/lib/installations.ts | 3 +-- packages/cli/test/installations.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 packages/cli/test/installations.test.ts diff --git a/packages/cli/src/lib/installations.ts b/packages/cli/src/lib/installations.ts index 19abe049..1388ef31 100644 --- a/packages/cli/src/lib/installations.ts +++ b/packages/cli/src/lib/installations.ts @@ -1,6 +1,5 @@ import { createWriteStream } from 'node:fs' import { chmod, cp, mkdir, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' import { basename, dirname, extname, join } from 'node:path' import { Readable } from 'node:stream' import { pipeline } from 'node:stream/promises' @@ -216,7 +215,7 @@ export async function downloadArtifact(url: string, destinationDir: string): Pro if (!response.ok || !response.body) throw new Error(`Download returned ${response.status} ${response.statusText}`) const filename = filenameFromResponse(response) ?? (basename(new URL(response.url).pathname) || `beeper-download-${Date.now()}`) const finalPath = join(destinationDir, filename) - const tmpPath = join(tmpdir(), `${filename}.${process.pid}.${Date.now()}.tmp`) + const tmpPath = join(destinationDir, `${filename}.${process.pid}.${Date.now()}.tmp`) await writeResponseToFile(response, tmpPath) await rename(tmpPath, finalPath) return finalPath diff --git a/packages/cli/test/installations.test.ts b/packages/cli/test/installations.test.ts new file mode 100644 index 00000000..59f44f6e --- /dev/null +++ b/packages/cli/test/installations.test.ts @@ -0,0 +1,25 @@ +import { expect, it } from 'bun:test' +import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { downloadArtifact } from '../src/lib/installations.js' + +it('installs a complete download on the destination filesystem', async () => { + // The checkout can be on a different filesystem from the system temp directory. + const destination = await mkdtemp(join(fileURLToPath(new URL('..', import.meta.url)), '.download-test-')) + const server = Bun.serve({ + hostname: '127.0.0.1', + port: 0, + fetch: () => new Response('server artifact'), + }) + try { + const artifact = await downloadArtifact(new URL('/beeper-server.tar.gz', server.url).href, destination) + + expect(artifact).toBe(join(destination, 'beeper-server.tar.gz')) + expect(await readFile(artifact, 'utf8')).toBe('server artifact') + expect(await readdir(destination)).toEqual(['beeper-server.tar.gz']) + } finally { + server.stop(true) + await rm(destination, { recursive: true, force: true }) + } +}) From 2e9de0129fb65e09fe7eb21dcd207c322e6ed7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Fri, 25 Sep 2026 15:39:08 +0000 Subject: [PATCH 3/5] test(cli): prove downloads skip the system temp directory Poison TMPDIR instead of relying on the checkout and /tmp being on different filesystems, so the regression test fails on a single-filesystem CI runner too, and stop writing test directories into the package checkout. --- packages/cli/test/installations.test.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/cli/test/installations.test.ts b/packages/cli/test/installations.test.ts index 59f44f6e..71b38187 100644 --- a/packages/cli/test/installations.test.ts +++ b/packages/cli/test/installations.test.ts @@ -1,12 +1,14 @@ import { expect, it } from 'bun:test' import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' import { join } from 'node:path' -import { fileURLToPath } from 'node:url' import { downloadArtifact } from '../src/lib/installations.js' -it('installs a complete download on the destination filesystem', async () => { - // The checkout can be on a different filesystem from the system temp directory. - const destination = await mkdtemp(join(fileURLToPath(new URL('..', import.meta.url)), '.download-test-')) +it('installs a complete download without staging it in the system temp directory', async () => { + const destination = await mkdtemp(join(tmpdir(), 'beeper-download-')) + const originalTmpdir = process.env.TMPDIR + // An unusable temp directory fails the same way a temp directory on another filesystem does. + process.env.TMPDIR = join(destination, 'unavailable') const server = Bun.serve({ hostname: '127.0.0.1', port: 0, @@ -19,7 +21,10 @@ it('installs a complete download on the destination filesystem', async () => { expect(await readFile(artifact, 'utf8')).toBe('server artifact') expect(await readdir(destination)).toEqual(['beeper-server.tar.gz']) } finally { + if (originalTmpdir === undefined) delete process.env.TMPDIR + else process.env.TMPDIR = originalTmpdir server.stop(true) await rm(destination, { recursive: true, force: true }) } }) + From 85f5090806e45679223e9ef45169bbb34da487a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Fri, 25 Sep 2026 15:39:08 +0000 Subject: [PATCH 4/5] fix(cli): remove partial downloads from the install directory Now that downloads stage next to the final artifact, a failed or interrupted download would leave a .tmp file in the install directory. --- packages/cli/src/lib/installations.ts | 9 +++++++-- packages/cli/test/installations.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/installations.ts b/packages/cli/src/lib/installations.ts index 1388ef31..1f42d5a3 100644 --- a/packages/cli/src/lib/installations.ts +++ b/packages/cli/src/lib/installations.ts @@ -216,8 +216,13 @@ export async function downloadArtifact(url: string, destinationDir: string): Pro const filename = filenameFromResponse(response) ?? (basename(new URL(response.url).pathname) || `beeper-download-${Date.now()}`) const finalPath = join(destinationDir, filename) const tmpPath = join(destinationDir, `${filename}.${process.pid}.${Date.now()}.tmp`) - await writeResponseToFile(response, tmpPath) - await rename(tmpPath, finalPath) + try { + await writeResponseToFile(response, tmpPath) + await rename(tmpPath, finalPath) + } catch (error) { + await rm(tmpPath, { force: true }) + throw error + } return finalPath } diff --git a/packages/cli/test/installations.test.ts b/packages/cli/test/installations.test.ts index 71b38187..bb7787cd 100644 --- a/packages/cli/test/installations.test.ts +++ b/packages/cli/test/installations.test.ts @@ -1,5 +1,6 @@ import { expect, it } from 'bun:test' import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises' +import { createServer, type Socket } from 'node:net' import { tmpdir } from 'node:os' import { join } from 'node:path' import { downloadArtifact } from '../src/lib/installations.js' @@ -28,3 +29,24 @@ it('installs a complete download without staging it in the system temp directory } }) +it('removes the partial file when a download is interrupted', async () => { + const destination = await mkdtemp(join(tmpdir(), 'beeper-download-')) + let connection: Socket | undefined + const server = createServer(socket => { + connection = socket + socket.write('HTTP/1.1 200 OK\r\nContent-Length: 1000\r\n\r\npartial') + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const { port } = server.address() as { port: number } + try { + const download = downloadArtifact(`http://127.0.0.1:${port}/beeper-server.tar.gz`, destination) + while ((await readdir(destination)).length === 0) await Bun.sleep(5) + connection?.destroy() + + await expect(download).rejects.toThrow() + expect(await readdir(destination)).toEqual([]) + } finally { + server.close() + await rm(destination, { recursive: true, force: true }) + } +}) From 33e10aec2bdf45f17e4ac8af001e795a2d65a54c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Fri, 25 Sep 2026 15:32:42 +0000 Subject: [PATCH 5/5] refactor(cli): drop unused download URL builder Installs now download the artifact URL from the update feed, so downloadURLFor and its platform mapping have no callers. Remove them with their assertions, and drop the cli-smoke install-request checks that installations.test.ts now owns. --- packages/cli/src/lib/installations.ts | 14 -------------- packages/cli/test/cli-smoke.ts | 25 ------------------------- packages/cli/test/installations.test.ts | 5 +---- 3 files changed, 1 insertion(+), 43 deletions(-) diff --git a/packages/cli/src/lib/installations.ts b/packages/cli/src/lib/installations.ts index 9f3a6fe0..b10a3f6d 100644 --- a/packages/cli/src/lib/installations.ts +++ b/packages/cli/src/lib/installations.ts @@ -80,7 +80,6 @@ export function normalizeInstallRequest(options: { kind: InstallKind channel: InstallChannel serverEnv: ServerEnv - platform: 'macos' | 'windows' | 'linux' feedPlatform: 'darwin' | 'win32' | 'linux' arch: 'x64' | 'arm64' bundleID: string @@ -88,7 +87,6 @@ export function normalizeInstallRequest(options: { } { const serverEnv = normalizeServerEnv(options.serverEnv) const channel = options.channel ?? 'stable' - const platform = normalizeDownloadPlatform(options.platform ?? process.platform) const feedPlatform = normalizeFeedPlatform(options.platform ?? process.platform) const arch = normalizeArch(options.arch ?? process.arch) const bundleID = bundleIDFor(options.kind, channel) @@ -96,7 +94,6 @@ export function normalizeInstallRequest(options: { kind: options.kind, channel, serverEnv, - platform, feedPlatform, arch, bundleID, @@ -113,10 +110,6 @@ export function feedURLFor(options: ReturnType): return url.toString() } -export function downloadURLFor(options: ReturnType): string { - return `${options.apiBaseURL}/desktop/download/${options.platform}/${options.arch}/${options.channel}/${options.bundleID}` -} - export async function fetchFeed(feedURL: string): Promise { const response = await fetch(feedURL, { signal: AbortSignal.timeout(30_000) }) if (!response.ok) throw new Error(`Update feed returned ${response.status} ${response.statusText}`) @@ -358,13 +351,6 @@ function normalizeServerEnv(value?: string): ServerEnv { throw new Error(`Unsupported server env "${value}". Expected production or staging.`) } -function normalizeDownloadPlatform(platform: NodeJS.Platform): 'macos' | 'windows' | 'linux' { - if (platform === 'darwin') return 'macos' - if (platform === 'win32') return 'windows' - if (platform === 'linux') return 'linux' - throw new Error(`Unsupported platform "${platform}".`) -} - function normalizeFeedPlatform(platform: NodeJS.Platform): 'darwin' | 'win32' | 'linux' { if (platform === 'darwin' || platform === 'win32' || platform === 'linux') return platform throw new Error(`Unsupported platform "${platform}".`) diff --git a/packages/cli/test/cli-smoke.ts b/packages/cli/test/cli-smoke.ts index 8a0769bd..623f2106 100644 --- a/packages/cli/test/cli-smoke.ts +++ b/packages/cli/test/cli-smoke.ts @@ -5,7 +5,6 @@ import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { commandManifest } from '../dist/lib/manifest.js' import { resolveAccountID, resolveAccountIDs, resolveChatID } from '../dist/lib/resolve.js' -import { downloadURLFor, feedURLFor, normalizeInstallRequest } from '../dist/lib/installations.js' const root = fileURLToPath(new URL('..', import.meta.url)) const configDir = '/tmp/beeper-cli-test' @@ -250,30 +249,6 @@ assert.equal(rpcLine.id, 1) assert.equal(rpcLine.ok, true) assert.match(rpcLine.stdout, /"success": true/) -const serverStableRequest = normalizeInstallRequest({ kind: 'server', channel: 'stable', platform: 'darwin', arch: 'arm64' }) -assert.equal(serverStableRequest.channel, 'stable') -assert.equal(serverStableRequest.serverEnv, 'production') -assert.equal(serverStableRequest.bundleID, 'com.automattic.beeper.server') -assert.equal(feedURLFor(serverStableRequest), 'https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') -assert.equal(downloadURLFor(serverStableRequest), 'https://api.beeper.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') - -const stagingServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'staging', channel: 'stable', platform: 'darwin', arch: 'arm64' }) -assert.equal(stagingServerRequest.channel, 'stable') -assert.equal(stagingServerRequest.serverEnv, 'staging') -assert.equal(stagingServerRequest.bundleID, 'com.automattic.beeper.server') -assert.equal(feedURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') -assert.equal(downloadURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') - -const serverNightlyRequest = normalizeInstallRequest({ kind: 'server', channel: 'nightly', platform: 'darwin', arch: 'arm64' }) -assert.equal(serverNightlyRequest.channel, 'nightly') -assert.equal(serverNightlyRequest.serverEnv, 'production') -assert.equal(serverNightlyRequest.bundleID, 'com.automattic.beeper.server.nightly') -assert.equal(feedURLFor(serverNightlyRequest), 'https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=darwin&channel=nightly&arch=arm64') -assert.equal(downloadURLFor(serverNightlyRequest), 'https://api.beeper.com/desktop/download/macos/arm64/nightly/com.automattic.beeper.server.nightly') - -const desktopNightlyRequest = normalizeInstallRequest({ kind: 'desktop', channel: 'nightly', platform: 'darwin', arch: 'arm64' }) -assert.equal(downloadURLFor(desktopNightlyRequest), 'https://api.beeper.com/desktop/download/macos/arm64/nightly/com.automattic.beeper.desktop.nightly') - const fakeClient = { accounts: { list: async () => [ diff --git a/packages/cli/test/installations.test.ts b/packages/cli/test/installations.test.ts index 5d50b3d7..c6c8a037 100644 --- a/packages/cli/test/installations.test.ts +++ b/packages/cli/test/installations.test.ts @@ -3,7 +3,7 @@ import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises' import { createServer, type Socket } from 'node:net' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { downloadArtifact, downloadURLFor, feedURLFor, installServer, normalizeInstallRequest } from '../src/lib/installations.js' +import { downloadArtifact, feedURLFor, installServer, normalizeInstallRequest } from '../src/lib/installations.js' const originalFetch = globalThis.fetch @@ -66,7 +66,6 @@ describe('server installation artifact selection', () => { expect(request.bundleID).toBe('com.automattic.beeper.server') expect(request.apiBaseURL).toBe('https://api.beeper.com') expect(feedURLFor(request)).toBe('https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=linux&channel=stable&arch=x64') - expect(downloadURLFor(request)).toBe('https://api.beeper.com/desktop/download/linux/x64/stable/com.automattic.beeper.server') }) it('keeps staging stable when staging is explicitly selected', () => { @@ -76,7 +75,6 @@ describe('server installation artifact selection', () => { expect(request.serverEnv).toBe('staging') expect(request.bundleID).toBe('com.automattic.beeper.server') expect(request.apiBaseURL).toBe('https://api.beeper-staging.com') - expect(downloadURLFor(request)).toBe('https://api.beeper-staging.com/desktop/download/linux/x64/stable/com.automattic.beeper.server') }) it('keeps nightly explicit instead of deriving it from the environment', () => { @@ -86,7 +84,6 @@ describe('server installation artifact selection', () => { expect(request.serverEnv).toBe('production') expect(request.bundleID).toBe('com.automattic.beeper.server.nightly') expect(request.apiBaseURL).toBe('https://api.beeper.com') - expect(downloadURLFor(request)).toBe('https://api.beeper.com/desktop/download/linux/x64/nightly/com.automattic.beeper.server.nightly') }) it('fails closed when the selected Server feed has no artifact URL', async () => {