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:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
-7
@@ -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 {
|
||||
+3
-4
@@ -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>`);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'bun',
|
||||
testTimeout: 30000, // 30 seconds for API calls
|
||||
}
|
||||
});
|
||||
@@ -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!"
|
||||
}
|
||||
Executable
+52
@@ -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
|
||||
Reference in New Issue
Block a user