Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
5c244b2
feat: imported-app commands — create (incl. --blank), chat, git, prev…
Sep 15, 2026
023b03a
feat(imported): live turn stream for chat and create --prompt
Sep 15, 2026
3ed5c42
fix(imported): pretty turn stream + authoritative completion
Sep 15, 2026
5448b55
feat(imported): positional create — one name for directory, repo, and…
Sep 15, 2026
dd31b24
fix(imported): a turn settles on a TERMINAL outcome, not the pending …
Sep 15, 2026
323e9d9
feat(imported): agentic turn UX — live status line, aliases, compact …
Sep 15, 2026
eadbabf
chore: unexport internal TurnStream interface (knip)
Sep 15, 2026
4988c4f
feat(imported): iteration mode after runs + clickable preview + edito…
Sep 15, 2026
13d4bd3
feat(imported): rotating idle musings on the status line
Sep 15, 2026
a2597a1
feat(imported): compact create output — identity up front, agent keep…
Sep 15, 2026
b4cb9dd
feat(imported): sticky link footer + truncated-args salvage
Sep 15, 2026
af68599
feat(imported): editor-parity tool titles, per-tool durations, turn t…
Sep 15, 2026
8c0fb7f
feat: base44 target + top-level new — the CLI carries its own sugar
Sep 15, 2026
9261fa5
chore: unexport internal StoredTarget interface (knip)
Sep 15, 2026
6723c21
feat(imported): unified live footer — preview joins the pinned links
Sep 15, 2026
6e5561d
feat(imported): Claude-Code-style interactive session
Sep 15, 2026
307b8be
feat(imported): session polish — fresh viewport, rule footer, hint be…
Sep 15, 2026
c3415ce
fix(imported): created dirs are discoverable + kickoff wait is visible
Sep 15, 2026
b0f5927
feat(imported): port the interactive session to Ink
Sep 15, 2026
b7f6f5e
feat(imported): Claude-Code-style bottom widget — bordered input, lin…
Sep 15, 2026
11ff257
feat(imported): session opens bottom-anchored, full-height
Sep 15, 2026
288d01d
feat(imported): Base44 Code welcome header
Sep 15, 2026
2ae9d13
fix(imported): header at top, widget at bottom; full-circle logo
Sep 15, 2026
a541f67
feat(imported): invented repo names — new works from just a prompt
Sep 15, 2026
5b1c62d
fix(imported): true CC scroll model, boot inside the page, logo slice…
Sep 15, 2026
c045ea2
polish(imported): rasterized circle logo
Sep 15, 2026
6a5dd4f
feat: base44 code — the session IS the entry point
Sep 15, 2026
7014559
fix(code): linked-project detection uses real app-context resolution
Sep 15, 2026
319d0fa
polish(imported): widen the logo to a true visual circle
Sep 15, 2026
daa414f
fix(imported): clean paste in the session input
Sep 15, 2026
5853a71
fix(imported): stable layout while the input wraps
Sep 15, 2026
ed925c7
feat(imported): clickable link chips, breathing room, params on their…
Sep 15, 2026
917dbff
fix(imported): two-tense tool labels split per moment
Sep 15, 2026
15ed3ff
feat(imported): honest signals for invisible arms and input waits
Sep 15, 2026
2c8a7b3
feat(imported): alternate-screen session with internal scroll — the r…
Sep 15, 2026
361e1dc
feat(imported): prompt expansions — /headless
Sep 15, 2026
f64da26
fix: expansion test matched the token inside the skill URL; unexport …
Sep 15, 2026
445c47f
fix(imported): rock-stable widget heights while scrolling
Sep 15, 2026
b6c3507
fix(imported): don't cry 'send failed' when the edge drops a long turn
Sep 15, 2026
6858c69
feat(imported): sunset-slat logo + shimmer animation on the boot screen
Sep 15, 2026
f0f2882
fix(imported): correct Base44 mark — dome + shortening bars; drop the…
Sep 15, 2026
83c1cee
feat(imported): /headless warns the agent about the scaffold's nested…
Sep 15, 2026
91da8f4
fix(imported): correct /headless git-init attribution
Sep 15, 2026
fa0a2a1
feat(cli): pick the builder model (command + in-session /model), time…
Sep 16, 2026
7237536
fix(cli): logo is the real Base44 setting-sun (disc with slit bars), …
Sep 16, 2026
f2e21bd
fix(cli): rounder logo sun (radius 10)
Sep 16, 2026
85aeaba
fix(cli): smaller logo, 3 stripes starting from the middle
Sep 16, 2026
d201ca0
fix(cli): logo is a round sun with a single blank stripe
Sep 16, 2026
4000674
fix(cli): compact left-aligned header (logo left, text right), smalle…
Sep 16, 2026
634027f
feat(cli): Esc stops the running turn (like the editor's stop button)
Sep 16, 2026
1478113
fix(cli): remove the session timer from the footer
Sep 16, 2026
7ba9246
fix(cli): wrapped URLs in the stream stay fully clickable (OSC 8)
Sep 16, 2026
252f81b
feat(cli): /headless expansion front-loads Wix preview setup
Sep 16, 2026
f019db0
fix(cli): /headless — give the exact wix-dev host-allowlist fix so th…
Sep 16, 2026
ca0c991
fix(cli): /headless — separate the two Astro host settings + fix the …
Sep 16, 2026
7913f0c
fix(cli): /headless — reword preview guidance to prose so the WAF sto…
Sep 16, 2026
787096b
fix(cli): /headless — prefer a flat root layout to stop later-turn pa…
Sep 16, 2026
874ed28
fix(cli): unstick the session poller + stop the post-move dep reinstall
Sep 16, 2026
ad6d7e8
fix(cli): /headless — anchor all work on /app, never the OS filesyste…
Sep 16, 2026
a3713ce
fix(cli): /headless — flatten timing (wait for bg install+seed) + che…
Sep 16, 2026
2362c2b
fix(cli): /headless — pass --flatten to fast-path (opt-in, deploy-lag…
Sep 16, 2026
baeaeba
fix(cli): /headless — trim nested-.git guidance now that fast-path sc…
Sep 16, 2026
af64ea0
fix(cli): /headless — fetch the skill with curl, not a web-page reader
Sep 16, 2026
b892d48
fix(cli): rounder logo — quadrant blocks (2x2) + aspect correction
Sep 16, 2026
189be0b
fix(cli): smooth logo poles — rows=7, drop the +0.3 fill tolerance
Sep 16, 2026
62bc314
fix(cli): bigger, rounder logo sun (rows=10)
Sep 16, 2026
e4bc9c3
fix(cli): logo size back to compact (rows=8) — round without being huge
Sep 16, 2026
6f0d154
fix(cli): smaller logo (rows=6) with a visible slit
Sep 16, 2026
90763cd
fix(cli): logo — one more row below the slit (rows=7, stripe at 8)
Sep 16, 2026
821d814
fix(cli): logo — move the slit one row down (stripe at 10)
Sep 16, 2026
d6eaa52
fix(cli): smaller logo circle (rows=6)
Sep 16, 2026
1a0bb95
fix(cli): /headless — navigate MPA pages via full reload, don't click…
Sep 17, 2026
e60e0da
feat(cli): base44 code builds a normal Base44 app by default; --impor…
Sep 17, 2026
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
1,230 changes: 666 additions & 564 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
26 changes: 24 additions & 2 deletions packages/cli/infra/build.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { watch, copyFileSync, mkdirSync } from "node:fs";
import { copyFileSync, mkdirSync, watch } from "node:fs";
import type { BuildConfig } from "bun";
import chalk from "chalk";

Expand Down Expand Up @@ -32,7 +32,10 @@ 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`,
Expand All @@ -48,11 +51,30 @@ const copyBackendRuntime = () => {
// 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.
const stubReactDevtools = {
name: "stub-react-devtools",
setup(build: { onResolve: Function; onLoad: Function }) {
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",
}));
},
};

const runAllBuilds = async () => {
const cli = await runBuild({
entrypoints: ["./src/cli/index.ts"],
outdir: "./dist/cli",
external: RUNTIME_EXTERNALS,
// biome-ignore lint/suspicious/noExplicitAny: BunPlugin's setup type is stricter than the minimal stub needs
plugins: [stubReactDevtools as any],
});
const backendRuntimePath = copyBackendRuntime();
return {
Expand Down
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"
}
}
106 changes: 106 additions & 0 deletions packages/cli/src/cli/commands/code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import chalk from "chalk";
import {
bootstrapBlankApp,
bootstrapBuilderApp,
} from "@/cli/commands/imported/create.js";
import {
runGenesisSession,
runInteractiveSession,
} from "@/cli/commands/imported/session.js";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command } from "@/cli/utils/index.js";
import { InvalidInputError } from "@/core/errors.js";
import { initAppContext } from "@/core/project/app-config.js";
import { soleActiveBranchId } from "@/core/resources/imported/api.js";

interface CodeOptions {
import?: boolean;
builder?: boolean;
}

async function codeAction(
_ctx: CLIContext,
options: CodeOptions,
): Promise<RunCommandResult> {
if (process.stdout.isTTY !== true) {
throw new InvalidInputError(
"base44 code is an interactive session and needs a terminal.",
);
}
if (options.import && options.builder) {
throw new InvalidInputError(
"Pass either --builder or --import, not both (the default is --builder).",
);
}
const importMode = options.import === true;

// Inside a linked app project, open the session on that app; anywhere
// else, the first prompt creates one from scratch. Resolution must be the
// real app-context lookup — an existence glob is recursive and would match
// apps in SUBdirectories of an unlinked cwd.
let linked = false;
try {
await initAppContext();
linked = true;
} catch {
// Not a linked project — genesis mode below.
}
if (linked) {
const branchId = await soleActiveBranchId().catch(() => undefined);
await runInteractiveSession({
branchId,
footer: [],
primeFirstPoll: true,
idleHint: "what should the agent do next?",
});
return { outroMessage: "Done." };
}

// Genesis mode picks the app type: --builder (default) makes a normal Base44
// app (template + agent); --import makes an imported app (a repo, or
// /headless → Wix Headless). The choice is a header/footer label + which
// bootstrap runs; the session itself is identical either way.
const footer: string[] = [
importMode
? chalk.dim(`${chalk.hex("#E86B3C")("●")} import`)
: chalk.dim(`${chalk.hex("#E86B3C")("●")} builder`),
];
await runGenesisSession(
importMode
? {
idleHint:
"describe the app, or import a repo — /headless for Wix Headless",
creatingLabel: "creating your repository and app",
modeLabel: "Import — your repo or /headless",
footer,
createApp: (prompt, emit) => bootstrapBlankApp(prompt, footer, emit),
}
: {
idleHint: "describe the app you want to build",
creatingLabel: "creating your app",
modeLabel: "Builder — Base44 template + agent",
footer,
createApp: (prompt, emit) =>
bootstrapBuilderApp(prompt, footer, emit),
},
);
return { outroMessage: "Done." };
}

export function getCodeCommand(): Base44Command {
const command = new Base44Command("code", { requireAppContext: false });
command
.description(
"Open Base44 Code: an interactive agent session — in an empty directory, your first prompt creates the app (default: the Base44 builder; --import for a repo / Wix Headless)",
)
.option(
"--builder",
"From scratch with the Base44 builder agent + template (default)",
)
.option(
"--import",
"Start from a GitHub repo, or /headless for Wix Headless, instead of the Base44 template",
)
.action(codeAction);
return command;
}
111 changes: 111 additions & 0 deletions packages/cli/src/cli/commands/imported/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import chalk from "chalk";
import { expandPrompt } from "@/cli/commands/imported/expansions.js";
import {
createTurnStream,
terminalLink,
} from "@/cli/commands/imported/render.js";
import { runInteractiveSession } from "@/cli/commands/imported/session.js";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command } from "@/cli/utils/index.js";
import { getBase44ApiUrl } from "@/core/config.js";
import { getAppContext } from "@/core/project/app-config.js";
import type { ImportedChatTurn } from "@/core/resources/imported/api.js";
import {
sendImportedChatMessage,
soleActiveBranchId,
} from "@/core/resources/imported/api.js";
import { streamConversationDuring } from "@/core/resources/imported/stream.js";

function lastAssistantReply(turn: ImportedChatTurn): string | undefined {
const messages = turn.conversation?.messages ?? [];
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (message.role !== "assistant") continue;
if (typeof message.content === "string" && message.content.trim()) {
return message.content.trim();
}
}
return undefined;
}

function chatFooter(): string[] {
try {
const editorUrl = `${getBase44ApiUrl()}/apps/${getAppContext().id}/editor/preview`;
return [terminalLink("editor", editorUrl)];
} catch {
return [];
}
}

function turnOutro(turn: ImportedChatTurn): string {
const state = turn.status?.state ?? "ready";
if (state === "error") {
return `Turn failed (${turn.status?.error_source ?? "unknown"}) — see the editor for details.`;
}
return "Turn finished.";
}

async function chatAction(
{ runTask, jsonMode, branchId: explicitBranchId }: CLIContext,
message: string,
): Promise<RunCommandResult> {
message = expandPrompt(message).text;
// Messages must land on the app's working branch: an unscoped send goes to
// the main line, whose sandbox is separate and never pushed.
const branchId =
explicitBranchId ?? (await soleActiveBranchId().catch(() => undefined));

if (jsonMode) {
const turn = await runTask("Agent working (a turn can take minutes)", () =>
sendImportedChatMessage(message, branchId),
);
if (turn.queued) return { stdout: `${JSON.stringify({ queued: true })}\n` };
return {
stdout: `${JSON.stringify({
status: turn.status?.state ?? "ready",
error_source: turn.status?.error_source ?? null,
reply: lastAssistantReply(turn) ?? null,
})}\n`,
};
}

if (process.stdout.isTTY !== true) {
const stream = createTurnStream(false);
let turn: ImportedChatTurn;
try {
turn = await streamConversationDuring(
() => sendImportedChatMessage(message, branchId),
stream.onEvent,
{ branchId },
);
} finally {
stream.stop();
}
if (turn.queued) {
return {
outroMessage:
"The agent is busy with an earlier message — yours was queued and will run next.",
};
}
return { outroMessage: turnOutro(turn) };
}

await runInteractiveSession({
branchId,
footer: chatFooter(),
primeFirstPoll: true,
initialMessage: message,
});
return { outroMessage: "Done." };
}

export function getImportedChatCommand(): Base44Command {
const command = new Base44Command("chat", { supportsBranch: true });
command
.description(
"Open an interactive agent session on the app, starting with this message",
)
.argument("<message>", "What you want the agent to do")
.action(chatAction);
return command;
}
Loading
Loading