feat(coding-agent): added self-update command with bundled templates

- Added `pi update` command for checking and installing CLI updates from GitHub releases or bun.
- Added cross-platform installer scripts (shell and PowerShell) with automatic PATH configuration.
- Changed HTML export to use compile-time bundled templates via Bun macros.
- Changed `exportToHtml` and `exportFromFile` functions to async operations.
This commit is contained in:
can1357
2026-01-03 14:16:18 +01:00
parent a6715fb8f0
commit a42061d9ee
17 changed files with 475 additions and 157 deletions
+1 -1
View File
@@ -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
+29
View File
@@ -19,6 +19,35 @@
Fork of <a href="https://github.com/badlogic/pi-mono">badlogic/pi-mono</a> by <a href="https://github.com/mariozechner">@mariozechner</a>
</p>
---
## 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)
+7
View File
@@ -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
+1 -13
View File
@@ -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;
+273
View File
@@ -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<ReleaseInfo> {
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<void> {
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<void> {
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<void> {
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
`);
}
@@ -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<string> {
const themeName = this.settingsManager.getTheme();
return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName });
}
@@ -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+</g, "><") // 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<string> {
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("<theme-vars/>", `<style>:root { ${themeVars} }</style>`)
.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<string> {
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<string> {
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;
@@ -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 {
@@ -4,9 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Session Export</title>
<style>
{{CSS}}
</style>
<template-css/>
<theme-vars/>
</head>
<body>
<button id="hamburger" title="Open sidebar"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="6" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="12" r="2.5"/><rect x="5" y="6" width="2" height="12"/><path d="M6 12h10c1 0 2 0 2-2V8"/></svg></button>
@@ -41,6 +40,6 @@
<script id="session-data" type="application/json">{{SESSION_DATA}}</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.4/marked.min.js" integrity="sha512-VmLxPVdDGeR+F0DzUHVqzHwaR4ZSSh1g/7aYXwKT1PAGVxunOEcysta+4H5Utvmpr2xExEPybZ8q+iM9F1tGdw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>{{JS}}</script>
<template-js/>
</body>
</html>
@@ -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<string> {
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("<template-css/>", `<style>${minifiedCss}</style>`)
.replace("<template-js/>", `<script>${js}</script>`);
}
+13 -1
View File
@@ -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) {
@@ -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;
@@ -401,7 +401,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
}
case "export_html": {
const path = session.exportToHtml(command.outputPath);
const path = await session.exportToHtml(command.outputPath);
return success(id, "export_html", { path });
}
-9
View File
@@ -1,9 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'bun',
testTimeout: 30000, // 30 seconds for API calls
}
});
+35
View File
@@ -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!"
}
+52
View File
@@ -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