Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 10 additions & 22 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,13 @@ jobs:
gh release create "${tag}" --title "${tag}" --generate-notes --verify-tag
fi

# Binary/Homebrew release assets are intentionally disabled until binary
# releases are ready to ship. Re-enable these steps with the package scripts
# already kept in packages/cli/package.json:
#
# - name: Build Homebrew archive
# run: bun run --filter beeper-cli pack:homebrew
#
# - name: Publish GitHub release assets
# env:
# GH_TOKEN: ${{ github.token }}
# run: |
# set -euo pipefail
# tag="${GITHUB_REF_NAME}"
# if ! gh release view "${tag}" >/dev/null 2>&1; then
# gh release create "${tag}" --title "${tag}" --generate-notes --verify-tag
# fi
# gh release upload "${tag}" packages/cli/dist/bin/beeper-* packages/cli/dist/bin/binaries.json packages/cli/dist/release/*.tar.gz packages/cli/dist/release/homebrew.json --clobber
#
# - name: Publish Homebrew formula
# env:
# HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
# run: bun scripts/publish-homebrew-formula.ts
- name: Build binary release assets
run: bun run --filter beeper-cli binary

- name: Publish GitHub release assets
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
gh release upload "${tag}" packages/cli/dist/bin/beeper-* packages/cli/dist/bin/binaries.json --clobber
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "desktop-api-cli-monorepo",
"name": "@beeper/cli",
"private": true,
"type": "module",
"packageManager": "bun@1.3.10",
Expand All @@ -16,6 +16,7 @@
"changeset": "changeset",
"lint": "eslint eslint.config.mjs packages/cli-plugin-cloudflare/src packages/cli-plugin-cloudflare/test",
"pack:packages": "mkdir -p .packs && (cd packages/cli && bun pm pack --destination ../../.packs) && (cd packages/cli-plugin-cloudflare && bun pm pack --destination ../../.packs)",
"publish:packages": "bun scripts/publish-packages.ts",
"release": "bun run check && bun changeset publish",
"test": "bun run --workspaces --sequential test",
"typecheck": "bun run --filter beeper-cli build && bun run --workspaces --sequential typecheck",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli-plugin-cloudflare/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@beeper/cli-plugin-cloudflare",
"version": "0.0.0",
"version": "0.6.0",
"description": "Cloudflare Tunnel commands for Beeper CLI",
"license": "MIT",
"type": "module",
Expand Down Expand Up @@ -34,7 +34,7 @@
},
"dependencies": {
"@oclif/core": "^4.11.2",
"beeper-cli": "workspace:*"
"beeper-cli": "^0.6.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli-plugin-cloudflare/src/lib/cloudflared.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { access, chmod, mkdir, rename, rm } from 'node:fs/promises'
import { access, chmod, mkdir, rename, rm, writeFile } from 'node:fs/promises'
import { arch, platform } from 'node:os'
import { basename, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
Expand Down Expand Up @@ -193,7 +193,7 @@ function downloadURL(system = platform(), cpu = arch()): string {
async function downloadFile(url: string, to: string): Promise<void> {
const response = await fetch(url, { redirect: 'follow' })
if (!response.ok || !response.body) throw new Error(`Could not download ${url}: ${response.status} ${response.statusText}`)
await Bun.write(to, response)
await writeFile(to, Buffer.from(await response.arrayBuffer()))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

export function findTunnelURL(data: string, domain = cloudflaredDomain()): string | undefined {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/bin/binary-bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ void (async () => {
const payloadHash = createHash('sha256').update(archive).digest('hex').slice(0, 16)
const cacheRoot = process.env.BEEPER_CLI_BINARY_CACHE_DIR || join(homedir(), '.cache', 'beeper-cli', 'binary')
const payloadRoot = join(cacheRoot, payloadHash)
const entrypoint = join(payloadRoot, 'bin', 'run.js')
const entrypoint = join(payloadRoot, 'bin', 'cli.js')

if (!existsSync(entrypoint)) {
const tempArchive = join(tmpdir(), `beeper-cli-${payloadHash}.tar.gz`)
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bun
import { execute } from '@oclif/core'
import { renderStartupLogo } from './logo.js'

void (async () => {
if (process.argv.slice(2).length === 0 && process.env.BEEPER_NO_LOGO !== '1') {
process.stdout.write(`${renderStartupLogo()}\n\n`)
}

await execute({ dir: import.meta.url })
})()
123 changes: 115 additions & 8 deletions packages/cli/bin/run.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,118 @@
#!/usr/bin/env bun
import { execute } from '@oclif/core'
import { renderStartupLogo } from './logo.js'
#!/usr/bin/env node
import { createHash } from 'node:crypto'
import { createWriteStream, existsSync } from 'node:fs'
import { chmod, mkdir, readFile, rename, rm } from 'node:fs/promises'
import { get } from 'node:https'
import { homedir, tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
import { pipeline } from 'node:stream/promises'
import { fileURLToPath } from 'node:url'
import { spawn } from 'node:child_process'

void (async () => {
if (process.argv.slice(2).length === 0 && process.env.BEEPER_NO_LOGO !== '1') {
process.stdout.write(`${renderStartupLogo()}\n\n`)
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
const pkg = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'))
const version = pkg.version
const platform = normalizePlatform(process.platform)
const arch = normalizeArch(process.arch)
const extension = platform === 'windows' ? '.exe' : ''
const executableName = `beeper-${platform}-${arch}${extension}`
const releaseTag = process.env.BEEPER_CLI_RELEASE_TAG || `v${version}`
const releaseBaseURL = (process.env.BEEPER_CLI_RELEASE_BASE_URL || `https://github.com/beeper/desktop-api-cli/releases/download/${releaseTag}`).replace(/\/$/, '')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const cacheRoot = process.env.BEEPER_CLI_BINARY_CACHE_DIR || join(homedir(), '.cache', 'beeper-cli')
const cacheDir = join(cacheRoot, version, `${platform}-${arch}`)
const cachedExecutable = join(cacheDir, platform === 'windows' ? 'beeper.exe' : 'beeper')

try {
const executable = await ensureExecutable()
const child = spawn(executable, process.argv.slice(2), {
env: process.env,
stdio: 'inherit',
})
child.once('exit', (code, signal) => {
if (signal) process.kill(process.pid, signal)
process.exit(code ?? 1)
})
child.once('error', error => {
console.error(`beeper-cli: failed to start downloaded binary: ${error.message}`)
process.exit(1)
})
} catch (error) {
console.error(`beeper-cli: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
}

async function ensureExecutable() {
if (existsSync(cachedExecutable)) return cachedExecutable

await mkdir(cacheDir, { recursive: true })
const tmpPath = join(tmpdir(), `${executableName}.${process.pid}.${Date.now()}.download`)
const url = `${releaseBaseURL}/${executableName}`
console.error(`beeper-cli: downloading ${url}`)
await download(url, tmpPath)

const expectedHash = await fetchExpectedHash().catch(() => undefined)
if (expectedHash) {
const actualHash = await sha256(tmpPath)
if (actualHash !== expectedHash) {
await rm(tmpPath, { force: true })
throw new Error(`downloaded binary checksum mismatch for ${executableName}`)
}
} else if (process.env.BEEPER_CLI_REQUIRE_CHECKSUM === '1') {
await rm(tmpPath, { force: true })
throw new Error(`no checksum found for ${executableName}`)
}

if (platform !== 'windows') await chmod(tmpPath, 0o755)
await rename(tmpPath, cachedExecutable)
return cachedExecutable
}

function normalizePlatform(value) {
if (value === 'darwin') return 'darwin'
if (value === 'linux') return 'linux'
if (value === 'win32') return 'windows'
throw new Error(`unsupported platform: ${value}`)
}

function normalizeArch(value) {
if (value === 'x64') return 'x64'
if (value === 'arm64') return 'arm64'
throw new Error(`unsupported architecture: ${value}`)
}

async function fetchExpectedHash() {
const manifestURL = `${releaseBaseURL}/binaries.json`
const manifestPath = join(tmpdir(), `beeper-cli-binaries-${version}-${process.pid}-${Date.now()}.json`)
try {
await download(manifestURL, manifestPath, { quiet: true })
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
return manifest.artifacts?.find(artifact => artifact.file === executableName)?.sha256
} finally {
await rm(manifestPath, { force: true })
}
}

async function download(url, destination, options = {}) {
await mkdir(dirname(destination), { recursive: true })
await new Promise((resolve, reject) => {
const request = get(url, response => {
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
response.resume()
download(new URL(response.headers.location, url).toString(), destination, options).then(resolve, reject)
return
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if (response.statusCode !== 200) {
response.resume()
reject(new Error(`download failed for ${basename(url)}: HTTP ${response.statusCode}`))
return
}
pipeline(response, createWriteStream(destination)).then(resolve, reject)
})
request.once('error', reject)
request.setTimeout(120_000, () => request.destroy(new Error(`download timed out: ${url}`)))
})
}

await execute({ dir: import.meta.url })
})()
async function sha256(path) {
return createHash('sha256').update(await readFile(path)).digest('hex')
}
5 changes: 3 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "MIT",
"type": "module",
"bin": {
"beeper": "./bin/run.js"
"beeper": "bin/run.js"
},
"exports": {
"./plugin-sdk": {
Expand All @@ -27,6 +27,7 @@
"check:readme": "bun run build && bun scripts/generate-readme.ts --check",
"clean": "rm -rf dist",
"dev": "bun ./bin/dev.js",
"dev:shim": "node ./bin/run.js",
"e2e:staging": "bun run build && bun test/e2e-staging.ts",
"pack:homebrew": "bun run binary && bun scripts/build-homebrew-archive.ts",
"readme": "bun run build && bun scripts/generate-readme.ts",
Expand All @@ -53,7 +54,7 @@
"scope": "beeper",
"pluginPrefix": "plugin",
"jitPlugins": {
"@beeper/cli-plugin-cloudflare": "^0.0.0"
"@beeper/cli-plugin-cloudflare": "^0.6.0"
},
"plugins": [
"@oclif/plugin-autocomplete",
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/scripts/build-binaries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env bun
import { createHash } from 'node:crypto'
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import { fileURLToPath } from 'node:url'

Expand Down Expand Up @@ -57,7 +58,7 @@ async function hashFile(path) {
}

async function buildPayload() {
const workDir = await mkdtemp(join('/private/tmp', 'beeper-cli-payload-'))
const workDir = await mkdtemp(join(tmpdir(), 'beeper-cli-payload-'))
try {
await cp(join(root, 'package.json'), join(workDir, 'package.json'))
await cp(join(root, 'bin'), join(workDir, 'bin'), { recursive: true })
Expand All @@ -79,7 +80,7 @@ async function buildPayload() {
async function run(command, args, options = {}) {
const child = Bun.spawn([command, ...args], {
cwd: options.cwd || root,
env: { ...process.env, TMPDIR: '/private/tmp' },
env: process.env,
stdin: 'inherit',
stdout: 'inherit',
stderr: 'inherit',
Expand Down
Loading
Loading