Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
"types": "./dist/npm/opentui/index.d.ts",
"import": "./dist/npm/opentui/index.js"
},
"./embedded": {
"types": "./dist/npm/embedded/index.d.ts",
"import": "./dist/npm/embedded/index.js"
},
"./package.json": "./package.json"
},
"publishConfig": {
Expand Down
105 changes: 75 additions & 30 deletions scripts/build-npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,39 @@ const repoRoot = path.resolve(import.meta.dir, "..");
const outdir = path.join(repoRoot, "dist", "npm");
const typesOutdir = path.join(repoRoot, "dist", "npm-types");
const opentuiOutdir = path.join(outdir, "opentui");
const opentuiTypesDir = path.join(typesOutdir, "opentui");
const opentuiTypesDir = path.join(typesOutdir, "src", "opentui");
const embeddedOutdir = path.join(outdir, "embedded");
const embeddedTypesDir = path.join(typesOutdir, "src", "embedded");
const libraryExternals = [
"react",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"@opentui/core",
"@opentui/react",
"@opentui/react/jsx-runtime",
"@opentui/react/jsx-dev-runtime",
"@pierre/diffs",
];

const bunEnv = {
...process.env,
BUN_TMPDIR: path.join(repoRoot, ".bun-tmp"),
BUN_INSTALL: path.join(repoRoot, ".bun-install"),
};

type LibraryBuildLog = Awaited<ReturnType<typeof Bun.build>>["logs"][number];

interface BuildLibraryExportOptions {
entrypoint: string;
name: string;
outputDirectory: string;
}

interface FormatBuildLibraryExportErrorOptions {
logs: readonly LibraryBuildLog[];
name: string;
}

function runBun(args: string[]) {
const proc = Bun.spawnSync(["bun", ...args], {
cwd: repoRoot,
Expand All @@ -29,9 +54,42 @@ function runBun(args: string[]) {
}
}

/** Format a Bun.build failure so the runtime reports the build diagnostics once. */
function formatBuildLibraryExportError({ logs, name }: FormatBuildLibraryExportErrorOptions) {
const details = logs
.map((log) => log.message)
.filter((message) => message.length > 0)
.join("\n");

return details
? `Failed to build ${name} export:\n${details}`
: `Failed to build ${name} export.`;
}

/** Build one npm package subpath export. */
async function buildLibraryExport({
entrypoint,
name,
outputDirectory,
}: BuildLibraryExportOptions) {
const build = await Bun.build({
entrypoints: [entrypoint],
target: "node",
format: "esm",
outdir: outputDirectory,
naming: { entry: "index.js" },
external: libraryExternals,
});

if (!build.success) {
throw new Error(formatBuildLibraryExportError({ logs: build.logs, name }));
}
}

rmSync(outdir, { recursive: true, force: true });
rmSync(typesOutdir, { recursive: true, force: true });
mkdirSync(opentuiOutdir, { recursive: true });
mkdirSync(embeddedOutdir, { recursive: true });

runBun([
"build",
Expand All @@ -52,44 +110,31 @@ if (process.platform !== "win32") {
chmodSync(mainJs, 0o755);
}

runBun([
"build",
path.join(repoRoot, "src", "opentui", "index.ts"),
"--target",
"node",
"--format",
"esm",
"--external",
"react",
"--external",
"react/jsx-runtime",
"--external",
"react/jsx-dev-runtime",
"--external",
"@opentui/core",
"--external",
"@opentui/react",
"--external",
"@opentui/react/jsx-runtime",
"--external",
"@opentui/react/jsx-dev-runtime",
"--external",
"@pierre/diffs",
"--outdir",
opentuiOutdir,
"--entry-naming",
"index.js",
]);
await buildLibraryExport({
entrypoint: path.join(repoRoot, "src", "opentui", "index.ts"),
name: "OpenTUI",
outputDirectory: opentuiOutdir,
});
await buildLibraryExport({
entrypoint: path.join(repoRoot, "src", "embedded", "index.ts"),
name: "embedded Hunk",
outputDirectory: embeddedOutdir,
});

runBun(["x", "tsc", "-p", path.join(repoRoot, "tsconfig.opentui.json")]);
runBun(["x", "tsc", "-p", path.join(repoRoot, "tsconfig.npm-exports.json")]);

for (const entry of readdirSync(opentuiTypesDir)) {
if (entry.endsWith(".d.ts")) {
copyFileSync(path.join(opentuiTypesDir, entry), path.join(opentuiOutdir, entry));
}
}

for (const entry of ["index.d.ts", "types.d.ts"]) {
copyFileSync(path.join(embeddedTypesDir, entry), path.join(embeddedOutdir, entry));
}

rmSync(typesOutdir, { recursive: true, force: true });

console.log(`Built ${mainJs}`);
console.log(`Built ${path.join(opentuiOutdir, "index.js")}`);
console.log(`Built ${path.join(embeddedOutdir, "index.js")}`);
2 changes: 2 additions & 0 deletions scripts/check-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ const publishedPaths = new Set(pack.files.map((file) => file.path));
const requiredPaths = [
"bin/hunk.cjs",
"dist/npm/main.js",
"dist/npm/embedded/index.d.ts",
"dist/npm/embedded/index.js",
"dist/npm/opentui/index.d.ts",
"dist/npm/opentui/index.js",
"README.md",
Expand Down
2 changes: 2 additions & 0 deletions scripts/check-prebuilt-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ const metaPack = runPackDryRun(metaDir);
assertPaths(metaPack, [
"bin/hunk.cjs",
"dist/npm/main.js",
"dist/npm/embedded/index.d.ts",
"dist/npm/embedded/index.js",
"dist/npm/opentui/index.d.ts",
"dist/npm/opentui/index.js",
"skills/hunk-review/SKILL.md",
Expand Down
52 changes: 52 additions & 0 deletions src/embedded/daemon.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, test } from "bun:test";
import { createEmbeddedSessionBrokerAvailability } from "./daemon";
import { resolveSessionBrokerConfig } from "../session-broker/brokerConfig";
import type { EnsureSessionBrokerAvailableOptions } from "../session-broker/brokerLauncher";

const testConfig = resolveSessionBrokerConfig({ HUNK_MCP_PORT: "47657" });

describe("embedded session broker daemon launcher", () => {
test("passes Hunk package-bin launch options through the broker availability adapter", async () => {
let captured: EnsureSessionBrokerAvailableOptions | undefined;
const ensureBroker = createEmbeddedSessionBrokerAvailability({
cwd: "/repo",
env: { HUNK_MCP_PORT: "48658" },
hunkCliPath: "/deps/hunkdiff/bin/hunk.cjs",
runtimePath: "/usr/local/bin/node",
timeoutMs: 1234,
ensureAvailable: async (options) => {
captured = options;
},
});

await ensureBroker(testConfig);

expect(captured).toEqual({
argv: ["/usr/local/bin/node", "/deps/hunkdiff/bin/hunk.cjs"],
config: testConfig,
cwd: "/repo",
env: { HUNK_MCP_PORT: "48658" },
execPath: "/usr/local/bin/node",
timeoutMs: 1234,
});
});

test("passes direct Hunk executable paths through without a runtime wrapper", async () => {
let captured: EnsureSessionBrokerAvailableOptions | undefined;
const ensureBroker = createEmbeddedSessionBrokerAvailability({
cwd: "/repo",
hunkCliPath: "/deps/hunkdiff/bin/hunk",
runtimePath: "/usr/local/bin/node",
ensureAvailable: async (options) => {
captured = options;
},
});

await ensureBroker(testConfig);

expect(captured).toMatchObject({
argv: ["/deps/hunkdiff/bin/hunk"],
execPath: "/deps/hunkdiff/bin/hunk",
});
});
});
51 changes: 51 additions & 0 deletions src/embedded/daemon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import {
ensureSessionBrokerAvailable,
type EnsureSessionBrokerAvailableOptions,
} from "../session-broker/brokerLauncher";
import type { ResolvedSessionBrokerConfig } from "../session-broker/brokerConfig";
import type { EnsureSessionBrokerAdapter } from "../session-broker/brokerClient";

const require = createRequire(import.meta.url);
const JAVASCRIPT_ENTRYPOINT_PATTERN = /\.(?:[cm]?js|tsx?)$/;

type EmbeddedEnsureSessionBroker = typeof ensureSessionBrokerAvailable;

export interface EmbeddedSessionBrokerAvailabilityOptions {
cwd: string;
env?: NodeJS.ProcessEnv;
ensureAvailable?: EmbeddedEnsureSessionBroker;
hunkCliPath?: string;
runtimePath?: string;
timeoutMs?: number;
}

/** Create the embedded broker availability adapter used by embedded Hunk sessions. */
export function createEmbeddedSessionBrokerAvailability({
cwd,
env = process.env,
ensureAvailable = ensureSessionBrokerAvailable,
hunkCliPath = join(dirname(require.resolve("hunkdiff/package.json")), "bin", "hunk.cjs"),
runtimePath = process.execPath,
timeoutMs,
}: EmbeddedSessionBrokerAvailabilityOptions): EnsureSessionBrokerAdapter {
return (config: ResolvedSessionBrokerConfig) => {
// The published package bin is a JS wrapper, so launch it through the active runtime instead
// of spawning the script path directly. Direct executable overrides still run as-is.
const scriptEntrypoint = JAVASCRIPT_ENTRYPOINT_PATTERN.test(hunkCliPath);
const options: EnsureSessionBrokerAvailableOptions = {
argv: scriptEntrypoint ? [runtimePath, hunkCliPath] : [hunkCliPath],
config,
cwd,
env,
execPath: scriptEntrypoint ? runtimePath : hunkCliPath,
};

if (timeoutMs !== undefined) {
options.timeoutMs = timeoutMs;
}

return ensureAvailable(options);
};
}
Loading