Skip to content
Open
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
127 changes: 114 additions & 13 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"tests/**/testkit/index.ts"
],
"project": [
"src/**/*.ts",
"src/**/*.{ts,tsx}",
"tests/**/*.ts"
],
"ignore": [
Expand Down
40 changes: 16 additions & 24 deletions packages/cli/infra/build-binaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
*
* Steps:
* 1. Create dist/assets.tar.gz from dist/assets/ (templates + backend-runtime)
* 2. Cross-compile for each platform with `bun build --compile`
* 2. Cross-compile for each platform with Bun.build({ compile })
*
* After this, run `bun run package:binaries` to archive and checksum.
*/
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import chalk from "chalk";
import { RUNTIME_EXTERNALS, stubReactDevtools } from "./bundle.js";

function collectFiles(
dir: string,
Expand Down Expand Up @@ -84,36 +85,27 @@ for (const { target, output } of TARGETS) {
const outPath = join(BINARIES_DIR, output);
console.log(chalk.dim(` Compiling ${output}...`));

const args = [
"bun",
"build",
"--compile",
`--target=${target}`,
ENTRY,
"--outfile",
outPath,
const result = await Bun.build({
entrypoints: [ENTRY],
// The workerd function runtime cannot ship inside a compiled binary
// (native executables and WASM cannot be embedded), so its packages are
// excluded here; the runtime probe in function-runtime.ts fails to
// import miniflare at runtime and `base44 dev` falls back to Deno.
"--external",
"miniflare",
"--external",
"esbuild",
"--external",
"@deno/loader",
];

// --windows-icon is only supported when the build host is Windows
if (target.includes("windows") && process.platform === "win32") {
args.push(`--windows-icon=${WINDOWS_ICON}`);
}

const result = Bun.spawnSync(args, { cwd: ROOT });
external: RUNTIME_EXTERNALS,
plugins: [stubReactDevtools],
compile: {
target,
outfile: outPath,
// A Windows icon can only be applied when the build host is Windows.
...(target.includes("windows") && process.platform === "win32"
? { windows: { icon: WINDOWS_ICON } }
: {}),
},
});

if (!result.success) {
console.error(chalk.red(`\n✗ Failed to compile ${output}\n`));
console.error(result.stderr.toString());
for (const log of result.logs) console.error(chalk.red(` ${log}`));
process.exit(1);
}
}
Expand Down
17 changes: 7 additions & 10 deletions packages/cli/infra/build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { watch, copyFileSync, mkdirSync } from "node:fs";
import { copyFileSync, mkdirSync, watch } from "node:fs";
import type { BuildConfig } from "bun";
import chalk from "chalk";
import { RUNTIME_EXTERNALS, stubReactDevtools } from "./bundle.js";

const runBuild = async (config: BuildConfig) => {
const defaultBuildOptions: Partial<BuildConfig> = {
Expand Down Expand Up @@ -32,27 +33,23 @@ const copyBackendRuntime = () => {
copyFileSync("./backend-runtime/exec.ts", `${outDir}/exec.ts`);
// The import map and the module it points at must land next to main.ts —
// function-manager.ts resolves the config relative to the wrapper.
copyFileSync("./backend-runtime/import-map.json", `${outDir}/import-map.json`);
copyFileSync(
"./backend-runtime/import-map.json",
`${outDir}/import-map.json`,
);
copyFileSync(
"./backend-runtime/base44-runtime.ts",
`${outDir}/base44-runtime.ts`,
);
return outDir;
};

// Runtime dependencies of the local workerd function runtime. They cannot be
// bundled (workerd and esbuild ship native binaries; @deno/loader ships WASM),
// so they are real npm `dependencies` resolved from node_modules at runtime —
// the one deliberate exception to the zero-dependency distribution rule. The
// standalone binary excludes them too and `base44 dev` falls back to the Deno
// runtime there.
export const RUNTIME_EXTERNALS = ["miniflare", "esbuild", "@deno/loader"];

const runAllBuilds = async () => {
const cli = await runBuild({
entrypoints: ["./src/cli/index.ts"],
outdir: "./dist/cli",
external: RUNTIME_EXTERNALS,
plugins: [stubReactDevtools],
});
const backendRuntimePath = copyBackendRuntime();
return {
Expand Down
27 changes: 27 additions & 0 deletions packages/cli/infra/bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { BunPlugin } from "bun";

// Runtime dependencies of the local workerd function runtime. They cannot be
// bundled (workerd and esbuild ship native binaries; @deno/loader ships WASM),
// so they are real npm `dependencies` resolved from node_modules at runtime —
// the one deliberate exception to the zero-dependency distribution rule. The
// standalone binary excludes them too and `base44 dev` falls back to the Deno
// runtime there.
export const RUNTIME_EXTERNALS = ["miniflare", "esbuild", "@deno/loader"];

// Ink's dev-only react-devtools bridge would otherwise land in the bundle as
// an eager import of a package we don't ship; the code path is dead outside
// DEV=true, so it compiles to an inert stub. Marking it external instead is
// not enough: the compiled binary hoists the import and fails at startup.
export const stubReactDevtools: BunPlugin = {
name: "stub-react-devtools",
setup(build) {
build.onResolve({ filter: /^react-devtools-core$/ }, () => ({
path: "react-devtools-core-stub",
namespace: "stub",
}));
build.onLoad({ filter: /.*/, namespace: "stub" }, () => ({
contents: "export default {}; export const connectToDevTools = () => {};",
loader: "js",
}));
},
};
6 changes: 5 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@types/ms": "^2.1.0",
"@types/multer": "^2.0.0",
"@types/node": "^22.10.5",
"@types/react": "^18",
"@vercel/detect-agent": "^1.1.0",
"chalk": "^5.6.2",
"chokidar": "^5.0.0",
Expand Down Expand Up @@ -98,6 +99,9 @@
"dependencies": {
"@deno/loader": "https://npm.jsr.io/~/11/@jsr/deno__loader/0.5.0.tgz",
"esbuild": "0.28.0",
"miniflare": "4.20260722.0"
"ink": "^5",
"ink-text-input": "^6",
"miniflare": "4.20260722.0",
"react": "^18"
}
}
18 changes: 18 additions & 0 deletions packages/cli/src/cli/commands/builder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Command } from "commander";
import { getModelCommand } from "@/cli/commands/builder/model.js";
import { getNewCommand } from "@/cli/commands/builder/new.js";
import { getSendCommand } from "@/cli/commands/builder/send.js";
import { getStatusCommand } from "@/cli/commands/builder/status.js";
import { getStopCommand } from "@/cli/commands/builder/stop.js";

export function getBuilderCommand(): Command {
return new Command("builder")
.description(
"Build an app with the Base44 builder agent, non-interactively: create it, send turns, read status, stop, pick the model",
)
.addCommand(getNewCommand())
.addCommand(getSendCommand())
.addCommand(getStatusCommand())
.addCommand(getStopCommand())
.addCommand(getModelCommand());
}
63 changes: 63 additions & 0 deletions packages/cli/src/cli/commands/builder/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command, theme } from "@/cli/utils/index.js";
import {
displayName,
getMe,
MODELS,
resolvePick,
saveBuilderModel,
} from "@/core/model.js";

async function modelAction(
{ log, jsonMode }: CLIContext,
input: string | undefined,
): Promise<RunCommandResult> {
const me = await getMe();
const current = me.builder_model ?? null;

if (!input) {
if (jsonMode) {
return {
stdout: `${JSON.stringify({
current,
models: MODELS.map((m) => ({ name: m.name, id: m.id })),
})}\n`,
};
}
for (const m of MODELS) {
const active = (m.id ?? null) === current;
const marker = active ? theme.styles.bold("●") : theme.styles.dim("○");
const note = m.note ? theme.styles.dim(` (${m.note})`) : "";
log.message(
`${marker} ${active ? theme.styles.bold(m.name) : m.name}${note}`,
);
}
return {
outroMessage: `Current: ${theme.styles.bold(displayName(current))}. Set with \`base44 builder model <name>\`.`,
};
}

const pick = resolvePick(input);
if ((pick.id ?? null) === current) {
return { outroMessage: `Already on ${theme.styles.bold(pick.name)}.` };
}
await saveBuilderModel(me.id, pick.id);
if (jsonMode) return { stdout: `${JSON.stringify({ current: pick.id })}\n` };
return {
outroMessage:
pick.id === null
? "Model reset — Base44 chooses per app again."
: `Builder model set to ${theme.styles.bold(pick.name)} for every new turn.`,
};
}

export function getModelCommand(): Base44Command {
const command = new Base44Command("model", { requireAppContext: false });
command
.description(
"Pick the builder model for your turns (account-wide). No argument lists models and the current pick; `default` clears it",
)
.argument("[model]", 'Model name or id, e.g. "Opus 5" or default')
.action(modelAction);
return command;
}
Loading
Loading