feat: standalone binary support with Bun

- Add build:binary script for Bun compilation
- Add paths.ts for cross-platform asset resolution (npm/bun/tsx)
- Add GitHub Actions workflow for automated binary releases
- Update README with installation options

Based on #89 by @steipete
This commit is contained in:
Mario Zechner 2025-12-02 12:18:42 +01:00
parent 4a60bffe3b
commit c4a65ad8b9
17 changed files with 626 additions and 65 deletions

View file

@ -1,6 +1,4 @@
import { existsSync, readFileSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
export interface ChangelogEntry {
major: number;
@ -97,11 +95,5 @@ export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): C
return entries.filter((entry) => compareVersions(entry, last) > 0);
}
/**
* Get the path to the CHANGELOG.md file
*/
export function getChangelogPath(): string {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
return join(__dirname, "../CHANGELOG.md");
}
// Re-export getChangelogPath from paths.ts for convenience
export { getChangelogPath } from "./paths.js";

View file

@ -2,14 +2,12 @@ import type { AgentState } from "@mariozechner/pi-agent-core";
import type { AssistantMessage, Message, ToolResultMessage, UserMessage } from "@mariozechner/pi-ai";
import { existsSync, readFileSync, writeFileSync } from "fs";
import { homedir } from "os";
import { basename, dirname, join } from "path";
import { fileURLToPath } from "url";
import { basename } from "path";
import { getPackageJsonPath } from "./paths.js";
import type { SessionManager } from "./session-manager.js";
// Get version from package.json
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
const packageJson = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
const VERSION = packageJson.version;
/**

View file

@ -4,11 +4,11 @@ import { ProcessTerminal, TUI } from "@mariozechner/pi-tui";
import chalk from "chalk";
import { existsSync, readFileSync, statSync } from "fs";
import { homedir } from "os";
import { dirname, extname, join, resolve } from "path";
import { fileURLToPath } from "url";
import { extname, join, resolve } from "path";
import { getChangelogPath, getNewEntries, parseChangelog } from "./changelog.js";
import { exportFromFile } from "./export-html.js";
import { findModel, getApiKeyForModel, getAvailableModels } from "./model-config.js";
import { getPackageJsonPath, getReadmePath } from "./paths.js";
import { SessionManager } from "./session-manager.js";
import { SettingsManager } from "./settings-manager.js";
import { expandSlashCommand, loadSlashCommands } from "./slash-commands.js";
@ -19,9 +19,7 @@ import { SessionSelectorComponent } from "./tui/session-selector.js";
import { TuiRenderer } from "./tui/tui-renderer.js";
// Get version from package.json
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
const packageJson = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
const VERSION = packageJson.version;
const defaultModelPerProvider: Record<KnownProvider, string> = {
@ -374,7 +372,7 @@ function buildSystemPrompt(customPrompt?: string, selectedTools?: ToolName[]): s
});
// Get absolute path to README.md
const readmePath = resolve(join(__dirname, "../README.md"));
const readmePath = getReadmePath();
// Build tools list based on selected tools
const tools = selectedTools || (["read", "bash", "edit", "write"] as ToolName[]);

View file

@ -0,0 +1,66 @@
import { existsSync } from "fs";
import { dirname, join, resolve } from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Detect if we're running as a Bun compiled binary.
* Bun binaries have import.meta.url starting with "file:///$bunfs/"
*/
export const isBunBinary = import.meta.url.startsWith("file:///$bunfs/");
/**
* Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md).
* - For Bun binary: returns the directory containing the executable
* - For Node.js (dist/): returns __dirname (the dist/ directory)
* - For tsx (src/): returns parent directory (the package root)
*/
export function getPackageDir(): string {
if (isBunBinary) {
// Bun binary: resolve relative to the executable
return dirname(process.execPath);
}
// Node.js: check if package.json exists in __dirname (dist/) or parent (src/ case)
if (existsSync(join(__dirname, "package.json"))) {
return __dirname;
}
// Running from src/ via tsx - go up one level to package root
return dirname(__dirname);
}
/**
* Get path to the theme directory
* - For Bun binary: dist/theme/ next to executable
* - For Node.js (dist/): dist/theme/
* - For tsx (src/): src/theme/
*/
export function getThemeDir(): string {
if (isBunBinary) {
return join(dirname(process.execPath), "theme");
}
// __dirname is either dist/ or src/ - theme is always a subdirectory
return join(__dirname, "theme");
}
/**
* Get path to package.json
*/
export function getPackageJsonPath(): string {
return join(getPackageDir(), "package.json");
}
/**
* Get path to README.md
*/
export function getReadmePath(): string {
return resolve(join(getPackageDir(), "README.md"));
}
/**
* Get path to CHANGELOG.md
*/
export function getChangelogPath(): string {
return resolve(join(getPackageDir(), "CHANGELOG.md"));
}

View file

@ -1,13 +1,11 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@mariozechner/pi-tui";
import { type Static, Type } from "@sinclair/typebox";
import { TypeCompiler } from "@sinclair/typebox/compiler";
import chalk from "chalk";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
import { getThemeDir } from "../paths.js";
// ============================================================================
// Types & Schema
@ -321,8 +319,9 @@ let BUILTIN_THEMES: Record<string, ThemeJson> | undefined;
function getBuiltinThemes(): Record<string, ThemeJson> {
if (!BUILTIN_THEMES) {
const darkPath = path.join(__dirname, "dark.json");
const lightPath = path.join(__dirname, "light.json");
const themeDir = getThemeDir();
const darkPath = path.join(themeDir, "dark.json");
const lightPath = path.join(themeDir, "light.json");
BUILTIN_THEMES = {
dark: JSON.parse(fs.readFileSync(darkPath, "utf-8")) as ThemeJson,
light: JSON.parse(fs.readFileSync(lightPath, "utf-8")) as ThemeJson,