Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
121 changes: 109 additions & 12 deletions bun.lock

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
"devDependencies": {
"@changesets/changelog-github": "^0.6.0",
"@changesets/cli": "^2.31.0",
"@types/bun": "^1.3.3",
"@types/node": "^20.0.0",
"@types/bun": "^1.3.14",
"@types/fs-extra": "^11.0.4",
"@types/node": "^20.19.42",
"eslint": "^9.39.4",
"eslint-config-oclif": "^6.0.165",
"eslint-config-oclif": "^6.0.167",
"eslint-config-prettier": "^10.1.8",
"tsdown": "^0.21.10",
"typescript": "^5.7.2"
"typescript": "^5.9.3"
}
}
}
5 changes: 3 additions & 2 deletions packages/cli/bin/run.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/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 { chmod, mkdir, readFile, rm } from 'node:fs/promises'
import { move } from 'fs-extra'
import { get } from 'node:https'
import { homedir, tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
Expand Down Expand Up @@ -63,7 +64,7 @@ async function ensureExecutable() {
if (platform !== 'windows') await chmod(extractedExecutable, 0o755)
await rm(cacheDir, { recursive: true, force: true })
await mkdir(cacheDir, { recursive: true })
await rename(extractedExecutable, cachedExecutable)
await move(extractedExecutable, cachedExecutable)
await rm(tmpDir, { recursive: true, force: true })
return cachedExecutable
}
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
}
},
"dependencies": {
"@beeper/desktop-api": "github:beeper/desktop-api-js#next",
"@beeper/desktop-api": "^5.0.0",
"@oclif/core": "^4.11.2",
"@oclif/plugin-autocomplete": "^3.2.49",
"@oclif/plugin-help": "^6.2.48",
Expand All @@ -139,4 +139,4 @@
"@types/ws": "^8.18.1",
"typescript": "^5.7.2"
}
}
}
17 changes: 8 additions & 9 deletions packages/cli/src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
checkInstallationUpdate,
readInstallations,
updateServerInstallation,
updateDesktopInstallation,
type Installation,
} from '../lib/installations.js'
import { profileStatus, startProfile, stopProfile } from '../lib/profiles.js'
Expand Down Expand Up @@ -33,7 +34,13 @@ export default class Update extends BeeperCommand {
}

if ((!selected || flags.desktop) && installations.desktop) {
results.push({ kind: 'desktop', ...(await checkDesktop(installations.desktop)) })
const check = await checkInstallationUpdate(installations.desktop)
if (check.available && !flags.check) {
const updated = await updateDesktopInstallation(installations.desktop)
results.push({ kind: 'desktop', updated: true, previousVersion: installations.desktop.version, currentVersion: updated.version, path: updated.path, action: 'Restart the app to apply the update.' })
} else {
results.push({ kind: 'desktop', ...check })
}
} else if ((!selected || flags.desktop) && !installations.desktop) {
results.push({ kind: 'desktop', installed: false, action: 'Run: beeper install desktop' })
}
Expand Down Expand Up @@ -71,14 +78,6 @@ async function runningServerProfiles(): Promise<Awaited<ReturnType<typeof listTa
return running
}

async function checkDesktop(installation: Installation): Promise<Record<string, unknown>> {
const check = await checkInstallationUpdate(installation)
return {
...check,
action: 'Update Beeper Desktop in the app.',
}
}

async function checkCLI(): Promise<Record<string, unknown>> {
const currentVersion = pkg.version
const installMethod = detectCLIInstallMethod()
Expand Down
49 changes: 40 additions & 9 deletions packages/cli/src/lib/installations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createWriteStream } from 'node:fs'
import { chmod, cp, mkdir, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { chmod, cp, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'
import { move } from 'fs-extra'
import { tmpdir, homedir } from 'node:os'
import { basename, dirname, extname, join } from 'node:path'
import { Readable } from 'node:stream'
import { pipeline } from 'node:stream/promises'
Expand Down Expand Up @@ -141,10 +142,7 @@ export async function checkInstallationUpdate(installation: Installation): Promi
available,
latestVersion,
currentVersion: installation.version,
action: installation.kind === 'desktop'
? 'Update Beeper Desktop in the app.'
: available ? 'Run: beeper update --server' : 'Beeper Server is up to date.',
feedURL: installation.feedURL,
action: available ? installation.kind === 'desktop' ? 'Run: beeper update --desktop' : 'Run: beeper update --server' : 'Installation is up to date.'
}
}

Expand Down Expand Up @@ -206,6 +204,10 @@ export async function installServer(options: { channel?: InstallChannel; serverE
})
}

export async function updateDesktopInstallation(installation: Installation): Promise<Installation> {
return installDesktop({ channel: installation.channel, serverEnv: installation.serverEnv })
}

export async function updateServerInstallation(installation: Installation): Promise<Installation> {
return installServer({ channel: installation.channel, serverEnv: installation.serverEnv })
}
Expand All @@ -218,7 +220,7 @@ export async function downloadArtifact(url: string, destinationDir: string): Pro
const finalPath = join(destinationDir, filename)
const tmpPath = join(tmpdir(), `${filename}.${process.pid}.${Date.now()}.tmp`)
await writeResponseToFile(response, tmpPath)
await rename(tmpPath, finalPath)
await move(tmpPath, finalPath)
return finalPath
}

Expand All @@ -234,14 +236,14 @@ async function extractServerArtifact(artifactPath: string, destinationDir: strin
else await execFileAsync('unzip', ['-q', artifactPath, '-d', extractDir])
} else {
const executable = join(destinationDir, 'beeper-server')
await rename(artifactPath, executable)
await move(artifactPath, executable)
await chmod(executable, 0o755)
return executable
}

const executable = await findServerExecutable(extractDir)
const finalPath = join(destinationDir, 'beeper-server')
await rename(executable, finalPath)
await move(executable, finalPath)
await chmod(finalPath, 0o755)
return finalPath
}
Expand Down Expand Up @@ -273,6 +275,14 @@ async function extractDesktopArtifact(artifactPath: string, destinationDir: stri
return finalPath
}

if (artifactPath.endsWith('.AppImage')) {
const finalPath = join(desktopInstallDir(), basename(artifactPath));
await move(artifactPath, finalPath);
await chmod(finalPath, 0o755);
await createDesktopEntry(finalPath);
return finalPath;
}

return artifactPath
}

Expand Down Expand Up @@ -310,6 +320,27 @@ async function findAppBundle(dir: string): Promise<string> {
throw new Error('Downloaded Beeper Desktop artifact did not contain an app bundle.')
}

async function createDesktopEntry(executablePath: string): Promise<void> {
const iconPath = join(homedir(), '.local/share/icons/beeper.png')
const desktopEntry = `[Desktop Entry]
Name=Beeper
Exec=${executablePath} %u
Icon=${iconPath}
Type=Application
Terminal=false
Comment=The ultimate messaging app
Categories=Network;Chat;
MimeType=x-scheme-handler/beeper;
StartupWMClass=Beeper
StartupNotify=true
X-Desktop-File-Install-Version=0.28
`
const desktopDir = join(homedir(), '.local/share/applications')
await mkdir(desktopDir, { recursive: true })
const desktopFilePath = join(desktopDir, 'Beeper.desktop')
await writeFile(desktopFilePath, desktopEntry, { mode: 0o644 })
}

async function findServerExecutable(dir: string): Promise<string> {
const { readdir, stat } = await import('node:fs/promises')
const entries = await readdir(dir)
Expand Down
107 changes: 93 additions & 14 deletions packages/cli/src/lib/profiles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { spawn } from 'node:child_process'
import { execFile } from 'node:child_process'
import { closeSync, openSync } from 'node:fs'
import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { closeSync, openSync, existsSync } from 'node:fs'
import { access, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { promisify } from 'node:util'
Expand Down Expand Up @@ -54,23 +54,42 @@ export async function startProfile(target: Target): Promise<ProfileRun | { id: s

export async function launchDesktopApp(target?: Target): Promise<{ id: string; startedAt: string }> {
const installations = await readInstallations().catch(() => ({ desktop: undefined }))
const appPath = installations.desktop?.path ?? await findDesktopAppPath()
const args = appPath ? ['-n', appPath, '--args'] : ['-n', '-a', 'Beeper', '--args']
args.push('--no-enforce-app-location')
if (target?.port) args.push(`--pas-port=${target.port}`)
if (target?.serverEnv) args.push(`--server-env=${target.serverEnv}`)
const env = target?.dataDir
? {

const appPath = installations.desktop?.path && existsSync(installations.desktop.path) ? installations.desktop.path : await findDesktopAppPath()

if (process.platform === 'darwin') {
const args = appPath ? ['-n', appPath, '--args'] : ['-n', '-a', 'Beeper', '--args']
args.push('--no-enforce-app-location')
if (target?.port) args.push(`--pas-port=${target.port}`)
if (target?.serverEnv) args.push(`--server-env=${target.serverEnv}`)
const env = target?.dataDir
? {
...process.env,
ALLOW_MULTIPLE_INSTANCES: 'true',
BEEPER_PROFILE: target.profile ?? target.id,
BEEPER_USER_DATA_DIR: target.dataDir,
}
: process.env
spawn('open', args, { detached: true, stdio: 'ignore', env }).unref()
}

if (process.platform === 'linux' || process.platform === 'win32') {
if (!appPath) throw new Error("Beeper Desktop not found. Please install beeper using 'beeper install --desktop' or add Beeper Desktop to the PATH")
const args = ['--no-enforce-app-location']
if (target?.port) args.push(`--pas-port=${target.port}`)
if (target?.serverEnv) args.push(`--server-env=${target.serverEnv}`)
const env = target?.dataDir
? {
...process.env,
ALLOW_MULTIPLE_INSTANCES: 'true',
BEEPER_PROFILE: target.profile ?? target.id,
BEEPER_USER_DATA_DIR: target.dataDir,
}
: process.env
spawn('open', args, { detached: true, stdio: 'ignore', env }).unref()
: process.env
spawn(appPath, args, { detached: true, stdio: 'ignore', env }).unref()
}
return { id: target?.id ?? 'desktop', startedAt: new Date().toISOString() }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

export async function findDesktopAppPath(): Promise<string | undefined> {
const installations = await readInstallations().catch(() => ({ desktop: undefined }))
if (installations.desktop?.path && await isBeeperDesktopApp(installations.desktop.path)) return installations.desktop.path
Expand All @@ -96,8 +115,68 @@ export async function findDesktopAppPath(): Promise<string | undefined> {
}

if (process.platform === 'linux') {
for (const path of ['/usr/bin/beeper', '/usr/local/bin/beeper']) {
if (await pathExists(path)) return path
// 1. Check ~/.beeper/apps/desktop directory for AppImage first
const desktopAppDir = join(homedir(), '.beeper', 'apps', 'desktop');
try {
const files = await readdir(desktopAppDir);
for (const file of files) {
const filePath = join(desktopAppDir, file);
if (file.match(/^Beeper-.*\.AppImage$/i) && await pathExists(filePath)) {
return filePath;
}
}
} catch {
// Directory doesn't exist or cannot be read, continue to next strategy
}

// 2. Look for Beeper.desktop file and extract the executable from that
const desktopDirs = [
join(homedir(), '.local', 'share', 'applications'),
'/usr/share/applications',
];

for (const dir of desktopDirs) {
try {
const desktopFilePath = join(dir, 'Beeper.desktop');
if (await pathExists(desktopFilePath)) {
const content = await readFile(desktopFilePath, 'utf8');
const execLine = content
.split('\n')
.find(line => line.startsWith('Exec='));

if (execLine) {
const exec = execLine.slice(5).trim();
const match = exec.match(/^"([^"]+)"|^([^\s]+)/);
const executable = match?.[1] ?? match?.[2];

if (executable && await pathExists(executable)) {
return executable;
}
}
}
} catch {
// Ignore unreadable files, continue to next strategy
}
}

// 3. Search PATH environment variable for AppImage
const pathEnv = process.env.PATH || '';
for (const dir of pathEnv.split(':')) {
if (dir) {
try {
const files = await readdir(dir);
for (const file of files) {
if (file.match(/^Beeper-.*\.AppImage$/i)) {
const filePath = join(dir, file);
if (await pathExists(filePath)) {
return filePath;
}
}
}
} catch {
// Directory doesn't exist or cannot be read, continue to next
}
}
}
}

Expand Down