diff --git a/.claude/commands/release.md b/.claude/commands/release.md index 49e447349..e9109ef30 100644 --- a/.claude/commands/release.md +++ b/.claude/commands/release.md @@ -4,7 +4,7 @@ Release all packages with the specified version. ## Arguments -- `$ARGUMENTS`: The version number (e.g., `1.342.0`) +- `$ARGUMENTS`: The version number (semver) ## Pre-flight Checks diff --git a/README.md b/README.md index 4b0e3e045..914d1757f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,35 @@ Fork of badlogic/pi-mono by @mariozechner

+--- + +## Installation + +### Via Bun (recommended) + +Requires [Bun](https://bun.sh) runtime: + +```bash +bun install -g @oh-my-pi/pi-coding-agent +``` + +### Via installer script + +**Linux / macOS:** +```bash +curl -fsSL https://raw.githubusercontent.com/can1357/oh-my-pi/main/scripts/install.sh | sh +``` + +**Windows (PowerShell):** +```powershell +irm https://raw.githubusercontent.com/can1357/oh-my-pi/main/scripts/install.ps1 | iex +``` + +### Manual download + +Download binaries directly from [GitHub Releases](https://github.com/can1357/oh-my-pi/releases/latest). + +--- ## + LSP Integration (Language Server Protocol) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 616ceb312..c37394335 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,8 +1,15 @@ # Changelog ## [Unreleased] + +### Added + +- Added `pi update` command to check for and install updates from GitHub releases or via bun + ### Changed +- Changed HTML export to use compile-time bundled templates via Bun macros for improved performance +- Changed `exportToHtml` and `exportFromFile` functions to be async - Simplified build process by embedding assets (themes, templates, agents, commands) directly into the binary at compile time - Removed separate asset copying steps from build scripts diff --git a/packages/coding-agent/src/bun-imports.d.ts b/packages/coding-agent/src/bun-imports.d.ts index 87e251caa..169a6791d 100644 --- a/packages/coding-agent/src/bun-imports.d.ts +++ b/packages/coding-agent/src/bun-imports.d.ts @@ -1,6 +1,6 @@ /** * Type declarations for Bun's import attributes. - * These allow importing non-JS files as text or JSON at build time. + * These allow importing non-JS files as text at build time. */ // Markdown files imported as text @@ -9,18 +9,6 @@ declare module "*.md" { export default content; } -// CSS files imported as text -declare module "*.css" { - const content: string; - export default content; -} - -// HTML files imported as text -declare module "*.html" { - const content: string; - export default content; -} - // Text files imported as text declare module "*.txt" { const content: string; diff --git a/packages/coding-agent/src/cli/update-cli.ts b/packages/coding-agent/src/cli/update-cli.ts new file mode 100644 index 000000000..66dc2123a --- /dev/null +++ b/packages/coding-agent/src/cli/update-cli.ts @@ -0,0 +1,273 @@ +/** + * Update CLI command handler. + * + * Handles `pi update` to check for and install updates. + * Uses bun if available, otherwise downloads binary from GitHub releases. + */ + +import { execSync, spawnSync } from "node:child_process"; +import { createWriteStream, existsSync, renameSync, unlinkSync } from "node:fs"; +import { dirname } from "node:path"; +import { Readable } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import chalk from "chalk"; +import { VERSION } from "../config"; + +/** + * Detect if we're running as a Bun compiled binary. + */ +const isBunBinary = + import.meta.url.includes("$bunfs") || import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN"); + +const REPO = "can1357/oh-my-pi"; +const PACKAGE = "@oh-my-pi/pi-coding-agent"; + +interface ReleaseInfo { + tag: string; + version: string; + assets: Array<{ name: string; url: string }>; +} + +/** + * Parse update subcommand arguments. + * Returns undefined if not an update command. + */ +export function parseUpdateArgs(args: string[]): { force: boolean; check: boolean } | undefined { + if (args.length === 0 || args[0] !== "update") { + return undefined; + } + + return { + force: args.includes("--force") || args.includes("-f"), + check: args.includes("--check") || args.includes("-c"), + }; +} + +/** + * Check if bun is available in PATH. + */ +function hasBun(): boolean { + try { + const result = spawnSync("bun", ["--version"], { encoding: "utf-8", stdio: "pipe" }); + return result.status === 0; + } catch { + return false; + } +} + +/** + * Get the latest release info from GitHub. + */ +async function getLatestRelease(): Promise { + const response = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`); + if (!response.ok) { + throw new Error(`Failed to fetch release info: ${response.statusText}`); + } + + const data = (await response.json()) as { + tag_name: string; + assets: Array<{ name: string; browser_download_url: string }>; + }; + + return { + tag: data.tag_name, + version: data.tag_name.replace(/^v/, ""), + assets: data.assets.map((a) => ({ name: a.name, url: a.browser_download_url })), + }; +} + +/** + * Compare semver versions. Returns: + * - negative if a < b + * - 0 if a == b + * - positive if a > b + */ +function compareVersions(a: string, b: string): number { + const pa = a.split(".").map(Number); + const pb = b.split(".").map(Number); + + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const na = pa[i] || 0; + const nb = pb[i] || 0; + if (na !== nb) return na - nb; + } + return 0; +} + +/** + * Get the appropriate binary name for this platform. + */ +function getBinaryName(): string { + const platform = process.platform; + const arch = process.arch; + + let os: string; + switch (platform) { + case "linux": + os = "linux"; + break; + case "darwin": + os = "darwin"; + break; + case "win32": + os = "windows"; + break; + default: + throw new Error(`Unsupported platform: ${platform}`); + } + + let archName: string; + switch (arch) { + case "x64": + archName = "x64"; + break; + case "arm64": + archName = "arm64"; + break; + default: + throw new Error(`Unsupported architecture: ${arch}`); + } + + if (os === "windows") { + return `pi-${os}-${archName}.exe`; + } + return `pi-${os}-${archName}`; +} + +/** + * Update via bun package manager. + */ +async function updateViaBun(): Promise { + console.log(chalk.dim("Updating via bun...")); + + try { + execSync(`bun update -g ${PACKAGE}`, { stdio: "inherit" }); + console.log(chalk.green("\nāœ“ Update complete")); + } catch { + throw new Error("bun update failed"); + } +} + +/** + * Update by downloading binary from GitHub releases. + */ +async function updateViaBinary(release: ReleaseInfo): Promise { + const binaryName = getBinaryName(); + const asset = release.assets.find((a) => a.name === binaryName); + + if (!asset) { + throw new Error(`No binary found for ${binaryName}`); + } + + const execPath = process.execPath; + const _execDir = dirname(execPath); + const tempPath = `${execPath}.new`; + const backupPath = `${execPath}.bak`; + + console.log(chalk.dim(`Downloading ${binaryName}...`)); + + // Download to temp file + const response = await fetch(asset.url, { redirect: "follow" }); + if (!response.ok || !response.body) { + throw new Error(`Download failed: ${response.statusText}`); + } + + const fileStream = createWriteStream(tempPath, { mode: 0o755 }); + const nodeStream = Readable.fromWeb(response.body as import("stream/web").ReadableStream); + await pipeline(nodeStream, fileStream); + + // Replace current binary + console.log(chalk.dim("Installing update...")); + + try { + // Backup current binary + if (existsSync(backupPath)) { + unlinkSync(backupPath); + } + renameSync(execPath, backupPath); + + // Move new binary into place + renameSync(tempPath, execPath); + + // Clean up backup + unlinkSync(backupPath); + + console.log(chalk.green(`\nāœ“ Updated to ${release.version}`)); + console.log(chalk.dim("Restart pi to use the new version")); + } catch (err) { + // Restore from backup if possible + if (existsSync(backupPath) && !existsSync(execPath)) { + renameSync(backupPath, execPath); + } + if (existsSync(tempPath)) { + unlinkSync(tempPath); + } + throw err; + } +} + +/** + * Run the update command. + */ +export async function runUpdateCommand(opts: { force: boolean; check: boolean }): Promise { + console.log(chalk.dim(`Current version: ${VERSION}`)); + + // Check for updates + let release: ReleaseInfo; + try { + release = await getLatestRelease(); + } catch (err) { + console.error(chalk.red(`Failed to check for updates: ${err}`)); + process.exit(1); + } + + const comparison = compareVersions(release.version, VERSION); + + if (comparison <= 0 && !opts.force) { + console.log(chalk.green("āœ“ Already up to date")); + return; + } + + if (comparison > 0) { + console.log(chalk.cyan(`New version available: ${release.version}`)); + } else { + console.log(chalk.yellow(`Forcing reinstall of ${release.version}`)); + } + + if (opts.check) { + // Just check, don't install + return; + } + + // Choose update method + try { + if (!isBunBinary && hasBun()) { + await updateViaBun(); + } else { + await updateViaBinary(release); + } + } catch (err) { + console.error(chalk.red(`Update failed: ${err}`)); + process.exit(1); + } +} + +/** + * Print update command help. + */ +export function printUpdateHelp(): void { + console.log(`${chalk.bold("pi update")} - Check for and install updates + +${chalk.bold("Usage:")} + pi update [options] + +${chalk.bold("Options:")} + -c, --check Check for updates without installing + -f, --force Force reinstall even if up to date + +${chalk.bold("Examples:")} + pi update Update to latest version + pi update --check Check if updates are available + pi update --force Force reinstall +`); +} diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index d5943e0cb..04335ba78 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1900,7 +1900,7 @@ export class AgentSession { * @param outputPath Optional output path (defaults to session directory) * @returns Path to exported file */ - exportToHtml(outputPath?: string): string { + async exportToHtml(outputPath?: string): Promise { const themeName = this.settingsManager.getTheme(); return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName }); } diff --git a/packages/coding-agent/src/core/export-html/index.ts b/packages/coding-agent/src/core/export-html/index.ts index 07146a7d3..372014860 100644 --- a/packages/coding-agent/src/core/export-html/index.ts +++ b/packages/coding-agent/src/core/export-html/index.ts @@ -4,41 +4,16 @@ import type { AgentState } from "@oh-my-pi/pi-agent-core"; import { APP_NAME } from "../../config"; import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme"; import { SessionManager } from "../session-manager"; -// Embed template files at build time -import templateCss from "./template-css.txt" with { type: "text" }; -import templateHtml from "./template-html.txt" with { type: "text" }; -import templateJs from "./template-js.txt" with { type: "text" }; -/** Minify CSS by removing comments, unnecessary whitespace, and newlines. */ -function minifyCss(css: string): string { - return css - .replace(/\/\*[\s\S]*?\*\//g, "") // Remove comments - .replace(/\s+/g, " ") // Collapse whitespace - .replace(/\s*([{}:;,>+~])\s*/g, "$1") // Remove space around punctuation - .replace(/;}/g, "}") // Remove trailing semicolons - .trim(); -} - -/** Minify JS using Bun's transpiler. */ -function minifyJs(js: string): string { - const transpiler = new Bun.Transpiler({ loader: "js", minifyWhitespace: true }); - return transpiler.transformSync(js); -} - -/** Minify HTML by collapsing whitespace outside of tags. */ -function minifyHtml(html: string): string { - return html - .replace(/>\s+<") // Remove whitespace between tags - .replace(/\s{2,}/g, " ") // Collapse multiple spaces - .trim(); -} +// Bun macro: bundles HTML+CSS+JS at compile time, evaluated at bundle time +import { getTemplate } from "./template.macro" with { type: "macro" }; export interface ExportOptions { outputPath?: string; themeName?: string; } -/** Parse a color string to RGB values. Supports hex (#RRGGBB) and rgb(r,g,b) formats. */ +/** Parse a color string to RGB values. */ function parseColor(color: string): { r: number; g: number; b: number } | undefined { const hexMatch = color.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/); if (hexMatch) { @@ -68,7 +43,7 @@ function getLuminance(r: number, g: number, b: number): number { return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b); } -/** Adjust color brightness. Factor > 1 lightens, < 1 darkens. */ +/** Adjust color brightness. */ function adjustBrightness(color: string, factor: number): string { const parsed = parseColor(color); if (!parsed) return color; @@ -76,21 +51,15 @@ function adjustBrightness(color: string, factor: number): string { return `rgb(${adjust(parsed.r)}, ${adjust(parsed.g)}, ${adjust(parsed.b)})`; } -/** Derive export background colors from a base color (e.g., userMessageBg). */ +/** Derive export background colors from a base color. */ function deriveExportColors(baseColor: string): { pageBg: string; cardBg: string; infoBg: string } { const parsed = parseColor(baseColor); if (!parsed) { - return { - pageBg: "rgb(24, 24, 30)", - cardBg: "rgb(30, 30, 36)", - infoBg: "rgb(60, 55, 40)", - }; + return { pageBg: "rgb(24, 24, 30)", cardBg: "rgb(30, 30, 36)", infoBg: "rgb(60, 55, 40)" }; } const luminance = getLuminance(parsed.r, parsed.g, parsed.b); - const isLight = luminance > 0.5; - - if (isLight) { + if (luminance > 0.5) { return { pageBg: adjustBrightness(baseColor, 0.96), cardBg: baseColor, @@ -104,9 +73,7 @@ function deriveExportColors(baseColor: string): { pageBg: string; cardBg: string }; } -/** - * Generate CSS custom property declarations from theme colors. - */ +/** Generate CSS custom properties for theme. */ function generateThemeVars(themeName?: string): string { const colors = getResolvedThemeColors(themeName); const lines: string[] = []; @@ -114,16 +81,15 @@ function generateThemeVars(themeName?: string): string { lines.push(`--${key}: ${value};`); } - // Use explicit theme export colors if available, otherwise derive from userMessageBg const themeExport = getThemeExportColors(themeName); const userMessageBg = colors.userMessageBg || "#343541"; - const derivedColors = deriveExportColors(userMessageBg); + const derived = deriveExportColors(userMessageBg); - lines.push(`--exportPageBg: ${themeExport.pageBg ?? derivedColors.pageBg};`); - lines.push(`--exportCardBg: ${themeExport.cardBg ?? derivedColors.cardBg};`); - lines.push(`--exportInfoBg: ${themeExport.infoBg ?? derivedColors.infoBg};`); + lines.push(`--body-bg: ${themeExport.pageBg ?? derived.pageBg};`); + lines.push(`--container-bg: ${themeExport.cardBg ?? derived.cardBg};`); + lines.push(`--info-bg: ${themeExport.infoBg ?? derived.infoBg};`); - return lines.join("\n "); + return lines.join(" "); } interface SessionData { @@ -134,61 +100,28 @@ interface SessionData { tools?: { name: string; description: string }[]; } -// Pre-minified embedded assets (cached on first use) -let cachedTemplate: string | null = null; -let cachedJs: string | null = null; - -/** - * Core HTML generation logic shared by both export functions. - */ -function generateHtml(sessionData: SessionData, themeName?: string): string { - // Minify embedded assets on first use - if (!cachedTemplate) { - cachedTemplate = minifyHtml(templateHtml); - } - if (!cachedJs) { - cachedJs = minifyJs(templateJs); - } - +/** Generate HTML from bundled template with runtime substitutions. */ +async function generateHtml(sessionData: SessionData, themeName?: string): Promise { const themeVars = generateThemeVars(themeName); - const colors = getResolvedThemeColors(themeName); - const exportColors = deriveExportColors(colors.userMessageBg || "#343541"); - const bodyBg = exportColors.pageBg; - const containerBg = exportColors.cardBg; - const infoBg = exportColors.infoBg; - - // Base64 encode session data to avoid escaping issues const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString("base64"); + const template = await getTemplate(); - // Build and minify the CSS with theme variables injected - const css = minifyCss( - templateCss - .replace("{{THEME_VARS}}", themeVars) - .replace("{{BODY_BG}}", bodyBg) - .replace("{{CONTAINER_BG}}", containerBg) - .replace("{{INFO_BG}}", infoBg), - ); - - return cachedTemplate - .replace("{{CSS}}", css) - .replace("{{JS}}", cachedJs) + return template + .replace("", ``) .replace("{{SESSION_DATA}}", sessionDataBase64); } -/** - * Export session to HTML using SessionManager and AgentState. - * Used by TUI's /export command. - */ -export function exportSessionToHtml(sm: SessionManager, state?: AgentState, options?: ExportOptions | string): string { +/** Export session to HTML using SessionManager and AgentState. */ +export async function exportSessionToHtml( + sm: SessionManager, + state?: AgentState, + options?: ExportOptions | string, +): Promise { const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {}; const sessionFile = sm.getSessionFile(); - if (!sessionFile) { - throw new Error("Cannot export in-memory session to HTML"); - } - if (!existsSync(sessionFile)) { - throw new Error("Nothing to export yet - start a conversation first"); - } + if (!sessionFile) throw new Error("Cannot export in-memory session to HTML"); + if (!existsSync(sessionFile)) throw new Error("Nothing to export yet - start a conversation first"); const sessionData: SessionData = { header: sm.getHeader(), @@ -198,46 +131,28 @@ export function exportSessionToHtml(sm: SessionManager, state?: AgentState, opti tools: state?.tools?.map((t) => ({ name: t.name, description: t.description })), }; - const html = generateHtml(sessionData, opts.themeName); - - let outputPath = opts.outputPath; - if (!outputPath) { - const sessionBasename = basename(sessionFile, ".jsonl"); - outputPath = `${APP_NAME}-session-${sessionBasename}.html`; - } + const html = await generateHtml(sessionData, opts.themeName); + const outputPath = opts.outputPath || `${APP_NAME}-session-${basename(sessionFile, ".jsonl")}.html`; writeFileSync(outputPath, html, "utf8"); return outputPath; } -/** - * Export session file to HTML (standalone, without AgentState). - * Used by CLI for exporting arbitrary session files. - */ -export function exportFromFile(inputPath: string, options?: ExportOptions | string): string { +/** Export session file to HTML (standalone). */ +export async function exportFromFile(inputPath: string, options?: ExportOptions | string): Promise { const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {}; - if (!existsSync(inputPath)) { - throw new Error(`File not found: ${inputPath}`); - } + if (!existsSync(inputPath)) throw new Error(`File not found: ${inputPath}`); const sm = SessionManager.open(inputPath); - const sessionData: SessionData = { header: sm.getHeader(), entries: sm.getEntries(), leafId: sm.getLeafId(), - systemPrompt: undefined, - tools: undefined, }; - const html = generateHtml(sessionData, opts.themeName); - - let outputPath = opts.outputPath; - if (!outputPath) { - const inputBasename = basename(inputPath, ".jsonl"); - outputPath = `${APP_NAME}-session-${inputBasename}.html`; - } + const html = await generateHtml(sessionData, opts.themeName); + const outputPath = opts.outputPath || `${APP_NAME}-session-${basename(inputPath, ".jsonl")}.html`; writeFileSync(outputPath, html, "utf8"); return outputPath; diff --git a/packages/coding-agent/src/core/export-html/template-css.txt b/packages/coding-agent/src/core/export-html/template.css similarity index 99% rename from packages/coding-agent/src/core/export-html/template-css.txt rename to packages/coding-agent/src/core/export-html/template.css index 0763a3fdb..31243ec85 100644 --- a/packages/coding-agent/src/core/export-html/template-css.txt +++ b/packages/coding-agent/src/core/export-html/template.css @@ -1,10 +1,3 @@ - :root { - {{THEME_VARS}} - --body-bg: {{BODY_BG}}; - --container-bg: {{CONTAINER_BG}}; - --info-bg: {{INFO_BG}}; - } - * { margin: 0; padding: 0; box-sizing: border-box; } :root { diff --git a/packages/coding-agent/src/core/export-html/template-html.txt b/packages/coding-agent/src/core/export-html/template.html similarity index 97% rename from packages/coding-agent/src/core/export-html/template-html.txt rename to packages/coding-agent/src/core/export-html/template.html index 76673d8ac..3afb4beb3 100644 --- a/packages/coding-agent/src/core/export-html/template-html.txt +++ b/packages/coding-agent/src/core/export-html/template.html @@ -4,9 +4,8 @@ Session Export - + + @@ -41,6 +40,6 @@ - + diff --git a/packages/coding-agent/src/core/export-html/template-js.txt b/packages/coding-agent/src/core/export-html/template.js similarity index 100% rename from packages/coding-agent/src/core/export-html/template-js.txt rename to packages/coding-agent/src/core/export-html/template.js diff --git a/packages/coding-agent/src/core/export-html/template.macro.ts b/packages/coding-agent/src/core/export-html/template.macro.ts new file mode 100644 index 000000000..86bef1258 --- /dev/null +++ b/packages/coding-agent/src/core/export-html/template.macro.ts @@ -0,0 +1,24 @@ +/** + * Bun macro that inlines HTML template with CSS/JS at compile time. + * This runs during `bun build` and embeds the result as a string. + */ +export async function getTemplate(): Promise { + const dir = new URL(".", import.meta.url).pathname; + + // Read all files + const html = await Bun.file(`${dir}template.html`).text(); + const css = await Bun.file(`${dir}template.css`).text(); + const js = await Bun.file(`${dir}template.js`).text(); + + // Minify CSS + const minifiedCss = css + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(/\s+/g, " ") + .replace(/\s*([{}:;,])\s*/g, "$1") + .trim(); + + // Inline everything + return html + .replace("", ``) + .replace("", ``); +} diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index f1a7d37f5..955f68dcd 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -14,6 +14,7 @@ import { processFileArguments } from "./cli/file-processor"; import { listModels } from "./cli/list-models"; import { parsePluginArgs, printPluginHelp, runPluginCommand } from "./cli/plugin-cli"; import { selectSession } from "./cli/session-picker"; +import { parseUpdateArgs, printUpdateHelp, runUpdateCommand } from "./cli/update-cli"; import { CONFIG_DIR_NAME, getAgentDir, getModelsPath, VERSION } from "./config"; import type { AgentSession } from "./core/agent-session"; import type { LoadedCustomTool } from "./core/custom-tools/index"; @@ -308,6 +309,17 @@ export async function main(args: string[]) { return; } + // Handle update subcommand + const updateCmd = parseUpdateArgs(args); + if (updateCmd) { + if (args.includes("--help") || args.includes("-h")) { + printUpdateHelp(); + return; + } + await runUpdateCommand(updateCmd); + return; + } + // Run migrations const { migratedAuthProviders: migratedProviders } = runMigrations(); @@ -338,7 +350,7 @@ export async function main(args: string[]) { if (parsed.export) { try { const outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined; - const result = exportFromFile(parsed.export, outputPath); + const result = await exportFromFile(parsed.export, outputPath); console.log(`Exported to: ${result}`); return; } catch (error: unknown) { diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 87ca6545f..e0d68420f 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -2130,7 +2130,7 @@ export class InteractiveMode { // HTML file export try { - const filePath = this.session.exportToHtml(arg); + const filePath = await this.session.exportToHtml(arg); this.showStatus(`Session exported to: ${filePath}`); } catch (error: unknown) { this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`); @@ -2153,7 +2153,7 @@ export class InteractiveMode { // Export to a temp file const tmpFile = path.join(os.tmpdir(), "session.html"); try { - this.session.exportToHtml(tmpFile); + await this.session.exportToHtml(tmpFile); } catch (error: unknown) { this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`); return; diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 3898618cf..64ad9f762 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -401,7 +401,7 @@ export async function runRpcMode(session: AgentSession): Promise { } case "export_html": { - const path = session.exportToHtml(command.outputPath); + const path = await session.exportToHtml(command.outputPath); return success(id, "export_html", { path }); } diff --git a/packages/coding-agent/vitest.config.ts b/packages/coding-agent/vitest.config.ts deleted file mode 100644 index ec184766e..000000000 --- a/packages/coding-agent/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'bun', - testTimeout: 30000, // 30 seconds for API calls - } -}); diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 000000000..64b0a16b7 --- /dev/null +++ b/scripts/install.ps1 @@ -0,0 +1,35 @@ +# Pi Coding Agent Installer for Windows +# Usage: irm https://raw.githubusercontent.com/can1357/oh-my-pi/main/scripts/install.ps1 | iex + +$ErrorActionPreference = "Stop" + +$Repo = "can1357/oh-my-pi" +$InstallDir = if ($env:PI_INSTALL_DIR) { $env:PI_INSTALL_DIR } else { "$env:LOCALAPPDATA\pi" } +$Binary = "pi-windows-x64.exe" + +# Get latest release +Write-Host "Fetching latest release..." +$Release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" +$Latest = $Release.tag_name +Write-Host "Latest version: $Latest" + +# Download binary +$Url = "https://github.com/$Repo/releases/download/$Latest/$Binary" +Write-Host "Downloading $Binary..." + +New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null +$OutPath = Join-Path $InstallDir "pi.exe" +Invoke-WebRequest -Uri $Url -OutFile $OutPath + +Write-Host "" +Write-Host "Installed pi to $OutPath" + +# Add to PATH if not already there +$UserPath = [Environment]::GetEnvironmentVariable("Path", "User") +if ($UserPath -notlike "*$InstallDir*") { + Write-Host "Adding $InstallDir to PATH..." + [Environment]::SetEnvironmentVariable("Path", "$UserPath;$InstallDir", "User") + Write-Host "Restart your terminal, then run 'pi' to get started!" +} else { + Write-Host "Run 'pi' to get started!" +} diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 000000000..13f52a620 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,52 @@ +#!/bin/sh +set -e + +# Pi Coding Agent Installer +# Usage: curl -fsSL https://raw.githubusercontent.com/can1357/oh-my-pi/main/scripts/install.sh | sh + +REPO="can1357/oh-my-pi" +INSTALL_DIR="${PI_INSTALL_DIR:-$HOME/.local/bin}" + +# Detect platform +OS="$(uname -s)" +ARCH="$(uname -m)" + +case "$OS" in + Linux) PLATFORM="linux" ;; + Darwin) PLATFORM="darwin" ;; + *) echo "Unsupported OS: $OS"; exit 1 ;; +esac + +case "$ARCH" in + x86_64|amd64) ARCH="x64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; +esac + +BINARY="pi-${PLATFORM}-${ARCH}" + +# Get latest release tag +echo "Fetching latest release..." +LATEST=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') +if [ -z "$LATEST" ]; then + echo "Failed to fetch latest release" + exit 1 +fi +echo "Latest version: $LATEST" + +# Download binary +URL="https://github.com/${REPO}/releases/download/${LATEST}/${BINARY}" +echo "Downloading ${BINARY}..." + +mkdir -p "$INSTALL_DIR" +curl -fsSL "$URL" -o "${INSTALL_DIR}/pi" +chmod +x "${INSTALL_DIR}/pi" + +echo "" +echo "Installed pi to ${INSTALL_DIR}/pi" + +# Check if in PATH +case ":$PATH:" in + *":$INSTALL_DIR:"*) echo "Run 'pi' to get started!" ;; + *) echo "Add ${INSTALL_DIR} to your PATH, then run 'pi'" ;; +esac