From a6715fb8f03ffe06d72394ab0f83f9acb84346cd Mon Sep 17 00:00:00 2001 From: can1357 Date: Sat, 3 Jan 2026 13:45:38 +0100 Subject: [PATCH] build(coding-agent): embedded assets into binary to eliminate runtime reads - Embedded assets (themes, templates, agents, commands) directly into binary at compile time using Bun imports. - Removed runtime filesystem reads for bundled assets in favor of static build-time imports. - Simplified build scripts by removing copy-assets steps and compiling directly from TypeScript source. - Updated publish workflow to use bun publish with NPM_CONFIG_TOKEN environment variable. - Added TypeScript declarations for importing .md, .css, .html, and .js files as text strings. --- .github/workflows/publish.yml | 6 +- packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/package.json | 8 +- packages/coding-agent/src/bun-imports.d.ts | 28 +++++ packages/coding-agent/src/config.ts | 102 +++++------------- .../coding-agent/src/core/auth-storage.ts | 5 +- .../src/core/export-html/index.ts | 28 ++--- .../{template.css => template-css.txt} | 0 .../{template.html => template-html.txt} | 0 .../{template.js => template-js.txt} | 0 .../src/core/tools/task/agents.ts | 56 ++++------ .../src/core/tools/task/commands.ts | 41 +++++-- .../src/modes/interactive/theme/theme.ts | 19 ++-- packages/coding-agent/tsconfig.build.json | 4 +- 14 files changed, 140 insertions(+), 161 deletions(-) create mode 100644 packages/coding-agent/src/bun-imports.d.ts rename packages/coding-agent/src/core/export-html/{template.css => template-css.txt} (100%) rename packages/coding-agent/src/core/export-html/{template.html => template-html.txt} (100%) rename packages/coding-agent/src/core/export-html/{template.js => template-js.txt} (100%) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c810e24eb..a44a7261e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -97,14 +97,14 @@ jobs: - name: Publish packages to npm if: ${{ !inputs.skip_npm }} env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} run: | for pkg in packages/*/; do pkg_name=$(basename "$pkg") echo "Publishing $pkg_name..." - output=$(cd "$pkg" && npm publish --access public --provenance 2>&1) && echo "$output" && continue + output=$(cd "$pkg" && bun publish --access public 2>&1) && echo "$output" && continue echo "$output" - if echo "$output" | grep -q "previously published\|cannot publish over"; then + if echo "$output" | grep -q "previously published\|cannot publish over\|You cannot publish over"; then echo "✓ Already published, skipping" else echo "✗ Failed to publish $pkg_name" diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c869ddbd4..616ceb312 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Changed + +- 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 ## [2.0.1337] - 2026-01-03 ### Added diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index a3f4ad77e..3946c2dc6 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -31,11 +31,9 @@ "scripts": { "check": "tsgo --noEmit", "clean": "rm -rf dist", - "build": "tsgo -p tsconfig.build.json && chmod +x dist/cli.js && npm run copy-assets", - "build:binary": "npm run build && bun build --compile ./dist/cli.js --outfile dist/pi && npm run copy-binary-assets", - "copy-assets": "mkdir -p dist/modes/interactive/theme && cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && mkdir -p dist/core/export-html && cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && mkdir -p dist/core/tools/task/bundled-agents && cp src/core/tools/task/bundled-agents/*.md dist/core/tools/task/bundled-agents/", - "copy-binary-assets": "cp package.json dist/ && cp README.md dist/ && cp CHANGELOG.md dist/ && mkdir -p dist/theme && cp src/modes/interactive/theme/*.json dist/theme/ && mkdir -p dist/export-html && cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/export-html/ && cp -r docs dist/ && cp -r examples dist/", - "test": "vitest --run", + "build": "tsgo -p tsconfig.build.json && chmod +x dist/cli.js", + "build:binary": "bun build --compile ./src/cli.ts --outfile dist/pi", + "test": "bun test", "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { diff --git a/packages/coding-agent/src/bun-imports.d.ts b/packages/coding-agent/src/bun-imports.d.ts new file mode 100644 index 000000000..87e251caa --- /dev/null +++ b/packages/coding-agent/src/bun-imports.d.ts @@ -0,0 +1,28 @@ +/** + * Type declarations for Bun's import attributes. + * These allow importing non-JS files as text or JSON at build time. + */ + +// Markdown files imported as text +declare module "*.md" { + const content: string; + 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; + export default content; +} diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index dcf5c0e8d..e41622540 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -1,34 +1,31 @@ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, join, resolve } from "node:path"; +// Embed package.json at build time for config +import packageJson from "../package.json" with { type: "json" }; + // ============================================================================= -// Package Detection +// App Config (from embedded package.json) +// ============================================================================= + +export const APP_NAME: string = (packageJson as { piConfig?: { name?: string } }).piConfig?.name || "pi"; +export const CONFIG_DIR_NAME: string = + (packageJson as { piConfig?: { configDir?: string } }).piConfig?.configDir || ".pi"; +export const VERSION: string = (packageJson as { version: string }).version; + +// e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR +export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`; + +// ============================================================================= +// Package Directory (for optional external docs/examples) // ============================================================================= /** - * Detect if we're running as a Bun compiled binary. - * Bun binaries have import.meta.url containing "$bunfs", "~BUN", or "%7EBUN" (Bun's virtual filesystem path) - */ -export const isBunBinary = - import.meta.url.includes("$bunfs") || import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN"); - -// ============================================================================= -// Package Asset Paths (shipped with executable) -// ============================================================================= - -/** - * 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) + * Get the base directory for resolving optional package assets (docs, examples). + * Walk up from import.meta.dir until we find package.json, or fall back to cwd. */ export function getPackageDir(): string { - if (isBunBinary) { - // Bun binary: process.execPath points to the compiled executable - return dirname(process.execPath); - } - // Node.js: walk up from import.meta.dir until we find package.json let dir = import.meta.dir; while (dir !== dirname(dir)) { if (existsSync(join(dir, "package.json"))) { @@ -36,79 +33,30 @@ export function getPackageDir(): string { } dir = dirname(dir); } - // Fallback (shouldn't happen) - return import.meta.dir; + // Fallback to cwd (docs/examples won't be found, but that's fine) + return process.cwd(); } -/** - * Get path to built-in themes directory (shipped with package) - * - For Bun binary: theme/ next to executable - * - For Node.js (dist/): dist/modes/interactive/theme/ - * - For tsx (src/): src/modes/interactive/theme/ - */ -export function getThemesDir(): string { - if (isBunBinary) { - return join(dirname(process.execPath), "theme"); - } - // Theme is in modes/interactive/theme/ relative to src/ or dist/ - const packageDir = getPackageDir(); - const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist"; - return join(packageDir, srcOrDist, "modes", "interactive", "theme"); -} - -/** - * Get path to HTML export template directory (shipped with package) - * - For Bun binary: export-html/ next to executable - * - For Node.js (dist/): dist/core/export-html/ - * - For tsx (src/): src/core/export-html/ - */ -export function getExportTemplateDir(): string { - if (isBunBinary) { - return join(dirname(process.execPath), "export-html"); - } - const packageDir = getPackageDir(); - const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist"; - return join(packageDir, srcOrDist, "core", "export-html"); -} - -/** Get path to package.json */ -export function getPackageJsonPath(): string { - return join(getPackageDir(), "package.json"); -} - -/** Get path to README.md */ +/** Get path to README.md (optional, may not exist in binary) */ export function getReadmePath(): string { return resolve(join(getPackageDir(), "README.md")); } -/** Get path to docs directory */ +/** Get path to docs directory (optional, may not exist in binary) */ export function getDocsPath(): string { return resolve(join(getPackageDir(), "docs")); } -/** Get path to examples directory */ +/** Get path to examples directory (optional, may not exist in binary) */ export function getExamplesPath(): string { return resolve(join(getPackageDir(), "examples")); } -/** Get path to CHANGELOG.md */ +/** Get path to CHANGELOG.md (optional, may not exist in binary) */ export function getChangelogPath(): string { return resolve(join(getPackageDir(), "CHANGELOG.md")); } -// ============================================================================= -// App Config (from package.json piConfig) -// ============================================================================= - -const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8")); - -export const APP_NAME: string = pkg.piConfig?.name || "pi"; -export const CONFIG_DIR_NAME: string = pkg.piConfig?.configDir || ".pi"; -export const VERSION: string = pkg.version; - -// e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR -export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`; - // ============================================================================= // User Config Paths (~/.pi/agent/*) // ============================================================================= diff --git a/packages/coding-agent/src/core/auth-storage.ts b/packages/coding-agent/src/core/auth-storage.ts index 8cfc512c4..dc53517e9 100644 --- a/packages/coding-agent/src/core/auth-storage.ts +++ b/packages/coding-agent/src/core/auth-storage.ts @@ -3,7 +3,7 @@ * Handles loading, saving, and refreshing credentials from auth.json. */ -import { chmodSync, existsSync, mkdirSync } from "node:fs"; +import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs"; import { dirname } from "node:path"; import { getEnvApiKey, @@ -73,8 +73,7 @@ export class AuthStorage { return; } try { - const file = Bun.file(this.authPath); - this.data = JSON.parse(file.text() as unknown as string); + this.data = JSON.parse(readFileSync(this.authPath, "utf-8")); } catch { this.data = {}; } diff --git a/packages/coding-agent/src/core/export-html/index.ts b/packages/coding-agent/src/core/export-html/index.ts index 7d3887724..07146a7d3 100644 --- a/packages/coding-agent/src/core/export-html/index.ts +++ b/packages/coding-agent/src/core/export-html/index.ts @@ -1,13 +1,13 @@ -import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { basename, join } from "node:path"; +import { existsSync, writeFileSync } from "node:fs"; +import { basename } from "node:path"; import type { AgentState } from "@oh-my-pi/pi-agent-core"; -import { APP_NAME, getExportTemplateDir } from "../../config"; +import { APP_NAME } from "../../config"; import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme"; import { SessionManager } from "../session-manager"; - -// Cached minified assets (populated on first use) -let cachedTemplate: string | null = null; -let cachedJs: string | null = null; +// 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 { @@ -134,22 +134,22 @@ 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 { - const templateDir = getExportTemplateDir(); - - // Load and minify assets on first use + // Minify embedded assets on first use if (!cachedTemplate) { - cachedTemplate = minifyHtml(readFileSync(join(templateDir, "template.html"), "utf-8")); + cachedTemplate = minifyHtml(templateHtml); } if (!cachedJs) { - cachedJs = minifyJs(readFileSync(join(templateDir, "template.js"), "utf-8")); + cachedJs = minifyJs(templateJs); } - const templateCss = readFileSync(join(templateDir, "template.css"), "utf-8"); - const themeVars = generateThemeVars(themeName); const colors = getResolvedThemeColors(themeName); const exportColors = deriveExportColors(colors.userMessageBg || "#343541"); diff --git a/packages/coding-agent/src/core/export-html/template.css b/packages/coding-agent/src/core/export-html/template-css.txt similarity index 100% rename from packages/coding-agent/src/core/export-html/template.css rename to packages/coding-agent/src/core/export-html/template-css.txt diff --git a/packages/coding-agent/src/core/export-html/template.html b/packages/coding-agent/src/core/export-html/template-html.txt similarity index 100% rename from packages/coding-agent/src/core/export-html/template.html rename to packages/coding-agent/src/core/export-html/template-html.txt diff --git a/packages/coding-agent/src/core/export-html/template.js b/packages/coding-agent/src/core/export-html/template-js.txt similarity index 100% rename from packages/coding-agent/src/core/export-html/template.js rename to packages/coding-agent/src/core/export-html/template-js.txt diff --git a/packages/coding-agent/src/core/tools/task/agents.ts b/packages/coding-agent/src/core/tools/task/agents.ts index 0ad08febb..ccd931e97 100644 --- a/packages/coding-agent/src/core/tools/task/agents.ts +++ b/packages/coding-agent/src/core/tools/task/agents.ts @@ -1,17 +1,24 @@ /** * Bundled agent definitions. * - * Agents are loaded from .md files in the bundled-agents directory. - * These serve as defaults when no user/project agents are discovered. + * Agents are embedded at build time via Bun's import with { type: "text" }. */ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { fileURLToPath } from "node:url"; +// Embed agent markdown files at build time +import browserMd from "./bundled-agents/browser.md" with { type: "text" }; +import exploreMd from "./bundled-agents/explore.md" with { type: "text" }; +import planMd from "./bundled-agents/plan.md" with { type: "text" }; +import reviewerMd from "./bundled-agents/reviewer.md" with { type: "text" }; +import taskMd from "./bundled-agents/task.md" with { type: "text" }; import type { AgentDefinition, AgentSource } from "./types"; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const BUNDLED_AGENTS_DIR = path.join(__dirname, "bundled-agents"); +const EMBEDDED_AGENTS: { name: string; content: string }[] = [ + { name: "browser.md", content: browserMd }, + { name: "explore.md", content: exploreMd }, + { name: "plan.md", content: planMd }, + { name: "reviewer.md", content: reviewerMd }, + { name: "task.md", content: taskMd }, +]; /** * Parse YAML frontmatter from markdown content. @@ -47,16 +54,9 @@ function parseFrontmatter(content: string): { frontmatter: Record | undefined; +const BUILTIN_THEMES: Record = { + dark: darkThemeJson as ThemeJson, + light: lightThemeJson as ThemeJson, +}; function getBuiltinThemes(): Record { - if (!BUILTIN_THEMES) { - const themesDir = getThemesDir(); - const darkPath = path.join(themesDir, "dark.json"); - const lightPath = path.join(themesDir, "light.json"); - BUILTIN_THEMES = { - dark: JSON.parse(fs.readFileSync(darkPath, "utf-8")) as ThemeJson, - light: JSON.parse(fs.readFileSync(lightPath, "utf-8")) as ThemeJson, - }; - } return BUILTIN_THEMES; } diff --git a/packages/coding-agent/tsconfig.build.json b/packages/coding-agent/tsconfig.build.json index 695dd9ad2..94bc26d4e 100644 --- a/packages/coding-agent/tsconfig.build.json +++ b/packages/coding-agent/tsconfig.build.json @@ -4,6 +4,6 @@ "outDir": "./dist", "rootDir": "./src" }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist", "**/*.d.ts", "src/**/*.d.ts"] + "include": ["src/**/*.ts", "src/bun-imports.d.ts"], + "exclude": ["node_modules", "dist"] }