From 5b1066d4973505b14b0ba14e64e9663e1f06c9ef Mon Sep 17 00:00:00 2001 From: unaimeds Date: Fri, 27 Mar 2026 13:37:49 +0100 Subject: [PATCH 1/2] fix: 'text file is busy' error when updating on linux --- package.json | 82 +++++++++++++++++++++--------------------- src/commands/update.ts | 18 ++++++---- 2 files changed, 52 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index d6785f4..3f5477c 100644 --- a/package.json +++ b/package.json @@ -1,43 +1,43 @@ { - "name": "polar-cli", - "version": "1.3.0", - "description": "", - "bin": "bin/cli.js", - "type": "module", - "scripts": { - "build": "tsup ./src/cli.ts --format esm --outDir bin", - "dev": "tsc --watch", - "test": "echo \"Error: no test specified\" && exit 1", - "check": "biome check --write ./src", - "build:binary": "bun build ./src/cli.ts --compile --outfile polar", - "build:binary:darwin-arm64": "bun build ./src/cli.ts --compile --target=bun-darwin-arm64 --outfile polar", - "build:binary:darwin-x64": "bun build ./src/cli.ts --compile --target=bun-darwin-x64 --outfile polar", - "build:binary:linux-x64": "bun build ./src/cli.ts --compile --target=bun-linux-x64 --outfile polar" - }, - "files": [ - "bin", - "dist" - ], - "keywords": [], - "author": "", - "license": "Apache-2.0", - "packageManager": "pnpm@10.5.2", - "dependencies": { - "@effect/cli": "^0.69.0", - "@effect/platform": "^0.90.2", - "@effect/platform-bun": "^0.87.1", - "@lemonsqueezy/lemonsqueezy.js": "^4.0.0", - "@polar-sh/sdk": "^0.43.1", - "effect": "^3.17.7", - "eventsource": "^4.1.0", - "open": "^10.2.0" - }, - "devDependencies": { - "@biomejs/biome": "^2.1.4", - "@effect/language-service": "^0.35.2", - "@sindresorhus/tsconfig": "^8.0.1", - "@types/node": "^24.2.1", - "tsup": "^8.5.0", - "typescript": "^5.9.2" - } + "name": "polar-cli", + "version": "1.3.0", + "description": "", + "bin": "bin/cli.js", + "type": "module", + "scripts": { + "build": "tsup ./src/cli.ts --format esm --outDir bin", + "dev": "tsc --watch", + "test": "echo \"Error: no test specified\" && exit 1", + "check": "biome check --write ./src", + "build:binary": "bun build ./src/cli.ts --compile --outfile polar", + "build:binary:darwin-arm64": "bun build ./src/cli.ts --compile --target=bun-darwin-arm64 --outfile polar", + "build:binary:darwin-x64": "bun build ./src/cli.ts --compile --target=bun-darwin-x64 --outfile polar", + "build:binary:linux-x64": "bun build ./src/cli.ts --compile --target=bun-linux-x64 --outfile polar" + }, + "files": [ + "bin", + "dist" + ], + "keywords": [], + "author": "", + "license": "Apache-2.0", + "packageManager": "pnpm@10.5.2", + "dependencies": { + "@effect/cli": "^0.69.0", + "@effect/platform": "^0.90.2", + "@effect/platform-bun": "^0.87.1", + "@lemonsqueezy/lemonsqueezy.js": "^4.0.0", + "@polar-sh/sdk": "^0.43.1", + "effect": "^3.17.7", + "eventsource": "^4.1.0", + "open": "^10.2.0" + }, + "devDependencies": { + "@biomejs/biome": "^2.1.4", + "@effect/language-service": "^0.35.2", + "@sindresorhus/tsconfig": "^8.0.1", + "@types/node": "^24.2.1", + "tsup": "^8.5.0", + "typescript": "^5.9.2" + } } diff --git a/src/commands/update.ts b/src/commands/update.ts index 08ee83b..c4ad0c8 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -1,9 +1,9 @@ import { Command } from "@effect/cli"; import { Console, Effect, Schema } from "effect"; import { createHash } from "crypto"; -import { chmod, mkdtemp, rm } from "fs/promises"; +import { chmod, mkdtemp, rename, rm, unlink } from "fs/promises"; import { tmpdir } from "os"; -import { join } from "path"; +import { dirname, join } from "path"; import { VERSION } from "../version"; const REPO = "polarsource/cli"; @@ -177,19 +177,23 @@ const downloadAndUpdate = ( const binaryPath = process.execPath; const newBinaryPath = join(tempDir, "polar"); + const tempBinaryPath = join(dirname(binaryPath), `.polar-update-${Date.now()}`); yield* Console.log(`${dim}Replacing binary...${reset}`); yield* Effect.tryPromise({ try: async () => { const newBinary = await Bun.file(newBinaryPath).arrayBuffer(); - await Bun.write(binaryPath, newBinary); - await chmod(binaryPath, 0o755); + await Bun.write(tempBinaryPath, newBinary); + await chmod(tempBinaryPath, 0o755); + await rename(tempBinaryPath, binaryPath); }, - catch: (e) => - new Error( + catch: async (e) => { + await unlink(tempBinaryPath).catch(() => {}); + return new Error( `Failed to replace binary: ${e instanceof Error ? e.message : e}`, - ), + ); + }, }); yield* Console.log(""); From 2d5c8f97fa09b9f01a7c0bb2e103f6719c873a08 Mon Sep 17 00:00:00 2001 From: unaimeds Date: Fri, 27 Mar 2026 14:01:19 +0100 Subject: [PATCH 2/2] fix: no permissions error when updating in non-user owned directory --- src/commands/update.ts | 43 ++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/commands/update.ts b/src/commands/update.ts index c4ad0c8..6d2e420 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -177,23 +177,46 @@ const downloadAndUpdate = ( const binaryPath = process.execPath; const newBinaryPath = join(tempDir, "polar"); - const tempBinaryPath = join(dirname(binaryPath), `.polar-update-${Date.now()}`); + const tempPath = join(dirname(binaryPath), `.polar-update-${Date.now()}`); yield* Console.log(`${dim}Replacing binary...${reset}`); yield* Effect.tryPromise({ try: async () => { - const newBinary = await Bun.file(newBinaryPath).arrayBuffer(); - await Bun.write(tempBinaryPath, newBinary); - await chmod(tempBinaryPath, 0o755); - await rename(tempBinaryPath, binaryPath); + await chmod(newBinaryPath, 0o755); + + let needsSudo = false; + try { + const newBinary = await Bun.file(newBinaryPath).arrayBuffer(); + await Bun.write(tempPath, newBinary); + await rename(tempPath, binaryPath); + } catch (e: any) { + unlink(tempPath).catch(() => {}); + if (e?.code === "EACCES") { + needsSudo = true; + } else { + throw e; + } + } + + if (needsSudo) { + const proc = Bun.spawn(["sudo", "mv", newBinaryPath, binaryPath], { + stdout: "inherit", + stderr: "inherit", + stdin: "inherit", + }); + const exitCode = await proc.exited; + if (exitCode !== 0) { + throw new Error("sudo mv failed"); + } + } + + await chmod(binaryPath, 0o755); }, - catch: async (e) => { - await unlink(tempBinaryPath).catch(() => {}); - return new Error( + catch: (e) => + new Error( `Failed to replace binary: ${e instanceof Error ? e.message : e}`, - ); - }, + ), }); yield* Console.log("");