feat(HAI-090): remove package.json dependency from compiled binary
- Inline version/name resolution in bin.ts so the binary works without package.json - Simplify build.ts by removing package.json bundling steps - Remove unused dynamic package.json lookups in engine merger, triage, and executor - Expand build-exe tests to verify binary runs independently of package.json - Clean up obsolete engine unit tests tied to removed package.json logic
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { join, dirname } from "node:path";
|
||||
import { cpSync, mkdirSync, existsSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { cpSync, mkdirSync, existsSync, rmSync } from "node:fs";
|
||||
|
||||
const cliRoot = dirname(new URL(import.meta.url).pathname);
|
||||
const workspaceRoot = join(cliRoot, "..", "..");
|
||||
@@ -102,14 +102,6 @@ function copyClientAssets() {
|
||||
console.log(` → ${dashboardClientDest}`);
|
||||
}
|
||||
|
||||
// ── Write a minimal package.json next to the binary ───────────────────
|
||||
function writeDistPackageJson() {
|
||||
writeFileSync(
|
||||
join(outDir, "package.json"),
|
||||
JSON.stringify({ name: "hai", version: "0.1.0", type: "module" }, null, 2) + "\n",
|
||||
);
|
||||
}
|
||||
|
||||
// ── Compile a single binary ───────────────────────────────────────────
|
||||
function compileBinary(outFile: string, target: string): boolean {
|
||||
console.log(`Compiling ${outFile} (target: ${target})...`);
|
||||
@@ -157,7 +149,6 @@ if (targets === null) {
|
||||
// Default: build for current platform → dist/hai
|
||||
const outBinary = join(outDir, defaultBinaryName());
|
||||
const ok = compileBinary(outBinary, "bun");
|
||||
writeDistPackageJson();
|
||||
if (!ok) process.exit(1);
|
||||
console.log(`\n✓ Built: ${outBinary}`);
|
||||
console.log(` Assets: ${dashboardClientDest}`);
|
||||
@@ -178,8 +169,6 @@ if (targets === null) {
|
||||
}
|
||||
}
|
||||
|
||||
writeDistPackageJson();
|
||||
|
||||
console.log(`\n${failed ? "⚠" : "✓"} Cross-compilation complete.`);
|
||||
if (built.length > 0) {
|
||||
console.log(` Built ${built.length} binaries:`);
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
import { execSync, spawnSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { cpSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const cliRoot = join(import.meta.dirname!, "..", "..");
|
||||
const outBinary = join(cliRoot, "dist", process.platform === "win32" ? "hai.exe" : "hai");
|
||||
const binaryName = process.platform === "win32" ? "hai.exe" : "hai";
|
||||
const clientDir = join(cliRoot, "dist", "client");
|
||||
|
||||
/**
|
||||
* Create an isolated temp directory containing only the binary and client/
|
||||
* assets — no package.json. Returns the dir path and a cleanup function.
|
||||
*/
|
||||
function createIsolatedDir(): { dir: string; binary: string; cleanup: () => void } {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hai-iso-"));
|
||||
cpSync(outBinary, join(dir, binaryName), { recursive: true });
|
||||
cpSync(clientDir, join(dir, "client"), { recursive: true });
|
||||
return {
|
||||
dir,
|
||||
binary: join(dir, binaryName),
|
||||
cleanup: () => rmSync(dir, { recursive: true, force: true }),
|
||||
};
|
||||
}
|
||||
|
||||
describe("build-exe", () => {
|
||||
beforeAll(() => {
|
||||
// Build the executable (skip if already built to speed up re-runs)
|
||||
@@ -29,41 +45,52 @@ describe("build-exe", () => {
|
||||
expect(existsSync(join(clientDir, "index.html"))).toBe(true);
|
||||
});
|
||||
|
||||
it("binary runs --help and prints expected output", () => {
|
||||
const result = spawnSync(outBinary, ["--help"], {
|
||||
encoding: "utf-8",
|
||||
timeout: 15_000,
|
||||
});
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("hai — AI-orchestrated task board");
|
||||
expect(result.stdout).toContain("dashboard");
|
||||
expect(result.stdout).toContain("task create");
|
||||
expect(result.stdout).toContain("task list");
|
||||
it("dist/ does not contain a package.json", () => {
|
||||
expect(existsSync(join(cliRoot, "dist", "package.json"))).toBe(false);
|
||||
});
|
||||
|
||||
it("binary runs --help without a co-located package.json", () => {
|
||||
const { binary, dir, cleanup } = createIsolatedDir();
|
||||
try {
|
||||
// Verify no package.json in the isolated dir
|
||||
expect(existsSync(join(dir, "package.json"))).toBe(false);
|
||||
|
||||
const result = spawnSync(binary, ["--help"], {
|
||||
encoding: "utf-8",
|
||||
timeout: 15_000,
|
||||
});
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("hai — AI-orchestrated task board");
|
||||
expect(result.stdout).toContain("dashboard");
|
||||
expect(result.stdout).toContain("task create");
|
||||
expect(result.stdout).toContain("task list");
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("binary runs 'task list' without crashing", () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "hai-test-"));
|
||||
const { binary, cleanup } = createIsolatedDir();
|
||||
try {
|
||||
const result = spawnSync(outBinary, ["task", "list"], {
|
||||
cwd: tmpDir,
|
||||
const result = spawnSync(binary, ["task", "list"], {
|
||||
encoding: "utf-8",
|
||||
timeout: 15_000,
|
||||
});
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("No tasks yet");
|
||||
} finally {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("binary starts dashboard and serves client assets", async () => {
|
||||
const { spawn } = await import("node:child_process");
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "hai-dash-test-"));
|
||||
const { binary, dir, cleanup } = createIsolatedDir();
|
||||
const port = 14040 + Math.floor(Math.random() * 1000);
|
||||
try {
|
||||
const output = await new Promise<string>((resolve, reject) => {
|
||||
const child = spawn(outBinary, ["dashboard", "--no-open", "-p", String(port)], {
|
||||
cwd: tmpDir,
|
||||
const child = spawn(binary, ["dashboard", "--no-open", "-p", String(port)], {
|
||||
cwd: dir,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
let out = "";
|
||||
@@ -85,7 +112,7 @@ describe("build-exe", () => {
|
||||
});
|
||||
expect(output).toContain("hai board");
|
||||
} finally {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
cleanup();
|
||||
}
|
||||
}, 15_000);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { runDashboard } from "./commands/dashboard.js";
|
||||
import { runTaskCreate, runTaskList, runTaskMove, runTaskMerge, runTaskUpdate, runTaskLog, runTaskShow, runTaskAttach } from "./commands/task.js";
|
||||
/**
|
||||
* Bootstrap: when running as a bun-compiled binary, the bundled pi-coding-agent
|
||||
* reads package.json from the executable's directory at module-init time
|
||||
* (top-level `readFileSync` in its config module). We redirect that read to a
|
||||
* temp directory containing a minimal package.json so the binary works
|
||||
* standalone without any co-located package.json.
|
||||
*
|
||||
* Node built-ins are safe to import statically — they have no side-effects
|
||||
* that depend on package.json. All application imports MUST be dynamic
|
||||
* (after the env is configured) so they resolve after PI_PACKAGE_DIR is set.
|
||||
*/
|
||||
import { mkdtempSync, writeFileSync, existsSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
// @ts-expect-error -- Bun-only global; undefined in Node
|
||||
const isBunBinary = typeof Bun !== "undefined" && !!Bun.embeddedFiles;
|
||||
|
||||
if (isBunBinary) {
|
||||
const execDir = dirname(process.execPath);
|
||||
const localPkg = join(execDir, "package.json");
|
||||
|
||||
if (!existsSync(localPkg)) {
|
||||
// Write a minimal package.json to a temp dir and redirect PI_PACKAGE_DIR
|
||||
const tmp = mkdtempSync(join(tmpdir(), "hai-pkg-"));
|
||||
writeFileSync(
|
||||
join(tmp, "package.json"),
|
||||
JSON.stringify(
|
||||
{ name: "hai", version: "0.1.0", type: "module", piConfig: { name: "hai", configDir: ".hai" } },
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
process.env.PI_PACKAGE_DIR = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic imports so the pi-coding-agent config module sees PI_PACKAGE_DIR
|
||||
const { runDashboard } = await import("./commands/dashboard.js");
|
||||
const { runTaskCreate, runTaskList, runTaskMove, runTaskMerge, runTaskUpdate, runTaskLog, runTaskShow, runTaskAttach } = await import("./commands/task.js");
|
||||
|
||||
const HELP = `
|
||||
hai — AI-orchestrated task board
|
||||
|
||||
Reference in New Issue
Block a user