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.
This commit is contained in:
can1357
2026-01-03 13:45:38 +01:00
parent d657e2cf04
commit a6715fb8f0
14 changed files with 140 additions and 161 deletions
+3 -3
View File
@@ -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"
+4
View File
@@ -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
+3 -5
View File
@@ -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": {
+28
View File
@@ -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;
}
+25 -77
View File
@@ -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/*)
// =============================================================================
@@ -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 = {};
}
@@ -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");
@@ -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<string, string
}
/**
* Load a single agent from a markdown file.
* Parse an agent from embedded content.
*/
function loadAgentFromFile(filePath: string, source: AgentSource): AgentDefinition | null {
let content: string;
try {
content = fs.readFileSync(filePath, "utf-8");
} catch {
return null;
}
function parseAgent(fileName: string, content: string, source: AgentSource): AgentDefinition | null {
const { frontmatter, body } = parseFrontmatter(content);
if (!frontmatter.name || !frontmatter.description) {
@@ -79,7 +79,7 @@ function loadAgentFromFile(filePath: string, source: AgentSource): AgentDefiniti
recursive,
systemPrompt: body,
source,
filePath,
filePath: `embedded:${fileName}`,
};
}
@@ -87,7 +87,7 @@ function loadAgentFromFile(filePath: string, source: AgentSource): AgentDefiniti
let bundledAgentsCache: AgentDefinition[] | null = null;
/**
* Load all bundled agents from the bundled-agents directory.
* Load all bundled agents from embedded content.
* Results are cached after first load.
*/
export function loadBundledAgents(): AgentDefinition[] {
@@ -97,24 +97,8 @@ export function loadBundledAgents(): AgentDefinition[] {
const agents: AgentDefinition[] = [];
if (!fs.existsSync(BUNDLED_AGENTS_DIR)) {
bundledAgentsCache = agents;
return agents;
}
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(BUNDLED_AGENTS_DIR, { withFileTypes: true });
} catch {
bundledAgentsCache = agents;
return agents;
}
for (const entry of entries) {
if (!entry.name.endsWith(".md")) continue;
const filePath = path.join(BUNDLED_AGENTS_DIR, entry.name);
const agent = loadAgentFromFile(filePath, "bundled");
for (const { name, content } of EMBEDDED_AGENTS) {
const agent = parseAgent(name, content, "bundled");
if (agent) {
agents.push(agent);
}
@@ -1,17 +1,23 @@
/**
* Workflow commands for orchestrating multi-agent workflows.
*
* Commands are loaded from .md files with YAML frontmatter.
* They define multi-step workflows that chain agent outputs.
* Commands are embedded at build time via Bun's import with { type: "text" }.
*/
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const BUNDLED_COMMANDS_DIR = path.join(__dirname, "bundled-commands");
// Embed command markdown files at build time
import architectPlanMd from "./bundled-commands/architect-plan.md" with { type: "text" };
import implementMd from "./bundled-commands/implement.md" with { type: "text" };
import implementWithCriticMd from "./bundled-commands/implement-with-critic.md" with { type: "text" };
const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
{ name: "architect-plan.md", content: architectPlanMd },
{ name: "implement-with-critic.md", content: implementWithCriticMd },
{ name: "implement.md", content: implementMd },
];
/** Workflow command definition */
export interface WorkflowCommand {
@@ -56,9 +62,9 @@ function parseFrontmatter(content: string): { frontmatter: Record<string, string
}
/**
* Load commands from a directory.
* Load commands from a directory (for user/project commands).
*/
function loadCommandsFromDir(dir: string, source: "bundled" | "user" | "project"): WorkflowCommand[] {
function loadCommandsFromDir(dir: string, source: "user" | "project"): WorkflowCommand[] {
const commands: WorkflowCommand[] = [];
if (!fs.existsSync(dir)) {
@@ -137,15 +143,30 @@ function findNearestDir(cwd: string, relPath: string): string | null {
let bundledCommandsCache: WorkflowCommand[] | null = null;
/**
* Load all bundled commands.
* Load all bundled commands from embedded content.
*/
export function loadBundledCommands(): WorkflowCommand[] {
if (bundledCommandsCache !== null) {
return bundledCommandsCache;
}
bundledCommandsCache = loadCommandsFromDir(BUNDLED_COMMANDS_DIR, "bundled");
return bundledCommandsCache;
const commands: WorkflowCommand[] = [];
for (const { name, content } of EMBEDDED_COMMANDS) {
const { frontmatter, body } = parseFrontmatter(content);
const cmdName = name.replace(/\.md$/, "");
commands.push({
name: cmdName,
description: frontmatter.description || "",
instructions: body,
source: "bundled",
filePath: `embedded:${name}`,
});
}
bundledCommandsCache = commands;
return commands;
}
/**
@@ -5,8 +5,11 @@ import { type Static, Type } from "@sinclair/typebox";
import { TypeCompiler } from "@sinclair/typebox/compiler";
import chalk from "chalk";
import { highlight, supportsLanguage } from "cli-highlight";
import { getCustomThemesDir, getThemesDir } from "../../../config";
import { getCustomThemesDir } from "../../../config";
import { logger } from "../../../core/logger";
// Embed theme JSON files at build time
import darkThemeJson from "./dark.json" with { type: "json" };
import lightThemeJson from "./light.json" with { type: "json" };
// ============================================================================
// Types & Schema
@@ -450,18 +453,12 @@ export class Theme {
// Theme Loading
// ============================================================================
let BUILTIN_THEMES: Record<string, ThemeJson> | undefined;
const BUILTIN_THEMES: Record<string, ThemeJson> = {
dark: darkThemeJson as ThemeJson,
light: lightThemeJson as ThemeJson,
};
function getBuiltinThemes(): Record<string, ThemeJson> {
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;
}
+2 -2
View File
@@ -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"]
}